Skip to content

Retire CuaAgent and CuaAgentHarness in favor of the handle - #80

Merged
rgarcia merged 2 commits into
mainfrom
hypeship/retire-agent-classes
Aug 14, 2026
Merged

Retire CuaAgent and CuaAgentHarness in favor of the handle#80
rgarcia merged 2 commits into
mainfrom
hypeship/retire-agent-classes

Conversation

@rgarcia

@rgarcia rgarcia commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Phase 4b: the CLI builds a stock pi AgentHarness from attach(), and CuaAgent / CuaAgentHarness are gone. Net −869 lines.

What replaces the classes

The CLI keeps the live selection in its own CuaCliCatalog — about 40 lines holding the handle, the model input, and the requested tool list:

private async swap(model: CuaModelInput, tools: readonly CuaCliTool[]): Promise<void> {
  const compiled = this.handle.compile<ExecutionToolContext>({ model, tools });
  await compiled.apply(this.harness);
  this.selection = model;
  this.requested = [...tools];
}

That is the whole mutation story now. compile() throws before anything reaches pi, and apply() restores the previous pair if pi rejects the new one, so the atomicity setTools() provided survives — but there is no longer a second copy of the state to drift from pi's.

buildCuaHarness() returns { harness, catalog }. The 18 TUI call sites that use pi's own lifecycle (prompt, subscribe, abort, compact, thinking level) were untouched; only the 5 selection sites moved.

The bug this turned up

withCatalogModels reads manager.catalog lazily, at stream time. The classes got away with that because their manager was mutable and commit() updated it in place. A per-compile manager does not move, and pi fixes models at construction with no setter — so the naive migration leaves a swapped harness streaming the first catalog's headers, payload transforms and tool plan for the rest of the session, silently.

So the handle now owns one Models collection that serves whichever pair is active, and activate() is what redirects it. apply() calls it. The test fails against a per-compile collection:

expected undefined to be 'computer'   // cuaIncomingToolPlan.openaiComputerName

Two smaller ones found while reviewing my own diff: a stale release() cleared the handle's grip on the pair that was live, so the next activate() could not release it (asserted by counting net live handler registrations — 8 instead of 4), and install() is renamed activate() because it no longer only installs handlers.

Capability removed, deliberately

Deleting the mutation path also deletes cache-preserving deferred tool addition: a tool that added tools mid-execution had those names recorded on its result as addedToolNames, letting pi extend an OpenAI request without invalidating the prompt-cache prefix. Nothing in the repo produced them outside that path, and with an immutable manager the code could no longer fire — keeping it would have been dead code, not a preserved feature. The transport still consumes addedToolNames on a transcript that carries them, and cua-ai keeps its own coverage of the namespace replay (reaches the CUA adapter when the transcript carries a deferred tool-search addition, pairs replayed namespaces by call id, not ordinal). If we want the capability back it should return as a first-class handle feature rather than a side effect of mutation.

Coverage

agent.test.ts (634 lines) and openai-deferred-tools.test.ts are deleted, but the behaviors that survive were ported to drive a handle plus stock pi rather than dropped: the OpenAI native-computer screenshot exemption, exact catalog composition, registry fallback for an unlisted ref, failed-turn state clearing, transport-follows-selection, and the model-change accounting for both a tools-only change and a transport-moving switch. harness-context.test.ts, published-declarations.test.ts, tool-manager.test.ts and the live e2e suite are ported rather than deleted.

published-declarations.test.ts caught a real constraint while being ported: a swap must compile for the same tool context the harness delivers, or the tool types do not line up. That is now asserted in the consumer.

Verified by exit status, not by grepping output: typecheck 0, cua-ai 94, cua-agent 281, cua-cli 143, all three builds 0.

Follow-ups, not in this PR

  • packages/agent/README.md, docs/architecture.md and both design records are updated; the two records get a superseded banner rather than a rewrite, since they are the record of decisions that have now been superseded.
  • The release skill's publish check still asserts typeof m.CuaAgent === "function"; it needs a new smoke symbol before the next release.
  • Phase 5 folds the two packages together and sweeps external consumers, which is where the CuaAgent removal reaches anyone outside this repo.

Note

High Risk
Major breaking public API removal and refactor of how model transport, tool catalogs, and streaming metadata stay in sync across runtime swaps.

Overview
Breaking: Removes CuaAgent and CuaAgentHarness. Consumers bind a Kernel browser with attach(), call compile({ model, tools }) for plain pi model / tools / models, construct Agent or AgentHarness themselves, and use activate() / apply() to swap catalogs on a running harness.

The handle keeps one shared Models collection (pi fixes models at construction while headers and payload transforms are per-catalog). activate() points it at the live compiled pair; apply() swaps model/tools atomically and rolls back if pi rejects the change. install() is renamed to activate().

CuaToolManager is immutable per compile—getTools / setTools / setModel / execution-scope mutation and cache-preserving mid-turn addedToolNames production are gone. The CLI composes a stock pi harness via buildCuaHarness(){ harness, catalog }, with CuaCliCatalog owning /model and /tools recompiles.

Docs, changelogs, release smoke check (attach instead of CuaAgent), and tests are updated; large CuaAgent-centric suites are removed or ported to the handle + pi path.

Reviewed by Cursor Bugbot for commit 9f03fa3. Bugbot is set up for automated code reviews on this repo. Configure here.

The CLI now builds a stock pi AgentHarness from attach() and keeps the
live (model, tools) selection in its own CuaCliCatalog. With no class
owning mutable agent state, setTools/setModel/setModelAndTools and the
tool manager's prepare/commit machinery go away: a change compiles a new
pair and applies it.

compile() throws before anything reaches pi and apply() restores the
previous pair if pi rejects the new one, so the atomicity the mutation
methods provided is preserved without a second copy of the state.

The handle now owns one Models collection serving whichever pair is
active. pi fixes models at construction while the headers, payload
transforms and tool plan it carries are per-catalog, so a per-compile
collection would have left a swapped harness streaming the first
catalog's plan for the rest of the session.
The published-artifact check imported CuaAgent, which this branch deletes.
@rgarcia
rgarcia merged commit 8ffce0c into main Aug 14, 2026
6 checks passed
@rgarcia
rgarcia deleted the hypeship/retire-agent-classes branch August 14, 2026 11:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant