Add attach(), the handle the agent classes will retire into - #79
Merged
Conversation
`attach({ browser, client })` binds a Kernel browser to CUA's execution
resources and compiles (model, tools) pairs into plain pi objects: the model
carrying the transport its tools derive, executables materialized against the
handle's pool, a `Models` collection adding provider retry, required headers,
the catalog's payload transforms and the tool-result image bound, and an
`install(harness)` for the behaviors that are pi event handlers rather than
constructor options.
The handle is what persists — the Kernel client and browser, the canonical
translator, the lazily created raw-CDP executor, ref and frame state — so a
spec materializes exactly once across repeat compiles. That is the lifetime a
one-shot function would have misrepresented.
The agent classes keep their behavior and now share these internals rather than
owning private copies, so the two cannot drift while both exist. Tests drive a
plain pi `Agent` and a plain pi `AgentHarness` from a handle with no CUA agent
class involved, which is the seam the classes will retire into.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Recovery attempts counted too early
- Moved the recovery-attempt increment to run only after await harness.followUp succeeds so rejected follow-ups no longer consume an attempt.
Or push these changes by commenting:
@cursor push 0ea9a56060
Preview (0ea9a56060)
diff --git a/packages/agent/src/attach.ts b/packages/agent/src/attach.ts
--- a/packages/agent/src/attach.ts
+++ b/packages/agent/src/attach.ts
@@ -161,15 +161,15 @@
hasPendingQueue = false;
return undefined;
}));
- offs.push(harness.subscribe((event: any, signal?: AbortSignal) => {
+ offs.push(harness.subscribe(async (event: any, signal?: AbortSignal) => {
if (event.type === "message_end" && event.message.role === "assistant") turnFailed = false;
else if (event.type === "tool_execution_end" && event.isError) turnFailed = true;
else if (event.type === "queue_update") hasPendingQueue = event.steer.length > 0 || event.followUp.length > 0;
if (!recovery || recovery.maxAttempts <= 0) return;
if (event.type !== "turn_end" || !isEmptyAssistantResponse(event.message)) return;
if (signal?.aborted || recoveryAttempts >= recovery.maxAttempts || hasPendingQueue) return;
+ await harness.followUp(recovery.followUp);
recoveryAttempts += 1;
- return harness.followUp(recovery.followUp);
}));
return () => {You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 327f36a. Configure here.
Nothing above cua-ai/cua-agent 0.10.0 and cua-cli 0.9.0 has been published, so the version headings above them claimed releases that never happened. Merging the sections exposed entries that contradicted each other or described states that never shipped, so each Unreleased section now reads as the net change from the last released version.
installCuaBehaviors incremented the attempt counter before awaiting followUp, so a rejected queue consumed a retry that the agent classes would not have spent. Match them: await first, then count.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary
Phase 4, step one of two. The decision recorded in the plan doc is
cua.attach({ browser, client })returning a handle, withCuaAgent/CuaAgentHarnessretiring behind it. This PR adds the handle and proves the seam; a follow-up migrates the CLI and deletes the classes.Splitting it this way keeps each step reviewable and green. The classes are 635 lines with ~25 forwarding methods and six non-forwarding behaviors; deleting them and rewriting the CLI in the same change would be one large untestable jump.
What
attach()gives youmodeltools/agentToolsmodelsinstall(harness)The handle is what persists — the Kernel client and browser, the canonical translator, the lazily created raw-CDP executor, ref and frame state. A spec materializes exactly once across repeat compiles, which is the lifetime a one-shot function would have misrepresented, and the reason the two-step shape was chosen.
No duplication while both exist
The classes now share these internals rather than owning private copies —
agent.tsimports the payload/context/recovery helpers fromattach.tsinstead of defining its own. Behavior is unchanged; the 302 existing cua-agent tests pass untouched. That matters because a copied helper block is exactly the drift this plan keeps finding.Testing
npm run typecheckclean; 94 / 302 / 143 tests pass.Agentand a plain piAgentHarnessfrom a handle with no CUA agent class involved — that is the seam the classes retire into. Also covered: transport derivation through the handle, and materialization stability across compiles.What step two removes
setTools/setModel/setModelAndToolsexist because the classes own mutable agent state. With a handle the caller recompiles and hands pi a fresh pair, which structurally deletes the two mutation-path bugs this plan already hit — a compiled model that never reached pi, and a derived api pinned across a recompile.It may also retire the execution-scope wrapper: that machinery exists to detect a tool calling
setTools()mid-execution. If mutation goes away, so does the thing it guards.Changelog housekeeping
Every merge since the last release added its own version heading, which claimed a release that never happened: nothing above
cua-ai/cua-agent0.10.0 andcua-cli0.9.0 is on npm, and the internal deps are still pinned at 0.10.0 while the entries above them announced bumps to 0.12/0.13/0.14. All of it now sits under one## Unreleasedper package.Collapsing the sections surfaced entries that only made sense as a sequence: a note that Google and Tzafon both keep their continuation protocol, in the same section that deletes Tzafon; Meta gaining stateless reasoning replay and then being removed;
routeCuaApirescoped twice before being deleted. Consumers upgrade from the last release, not through the steps, so each section now reads as the net change and drops what nobody can observe.The release skill said to add a new top entry, which is what produced this. It now says to rename
## Unreleasedin place at release time, to reconcile the accumulated section before tagging, and never to invent a version heading for a merge.Note
Medium Risk
New public API and a large internal move of streaming/catalog behavior, though existing agent classes and tests are intended to stay behavior-identical until a follow-up migration.
Overview
Introduces
attach({ browser, client }), a long-lived handle over Kernel browser execution resources.compile({ model, tools })returns pi-readymodel, harness/agent tools, a wrappedmodelscollection (retry, headers, payload transforms, tool-result image limits), andinstall(harness)for turn-failure blocking and empty-response recovery.CuaAgent/CuaAgentHarnessstay but are documented as retiring behind this API; shared logic moves fromagent.tsintoattach.tsso behavior does not diverge while both paths exist.Exports and Vitest coverage prove the seam: plain pi
Agent/AgentHarnesscan run without the CUA agent classes, including transport derivation from tool selection and stable tool materialization across recompiles.Changelog / release skill: per-package version headings that never shipped are folded into a single
## Unreleasedsection with net consumer-facing notes;.agents/skills/release/SKILL.mdnow says merges append to Unreleased and releases rename that heading in place (reconcile contradictions before tagging).Reviewed by Cursor Bugbot for commit a7de6f8. Bugbot is set up for automated code reviews on this repo. Configure here.