Skip to content

Add provider-aware CUA AgentHarness - #12

Merged
rgarcia merged 3 commits into
mainfrom
hypeship/cua-agent-harness
May 14, 2026
Merged

Add provider-aware CUA AgentHarness#12
rgarcia merged 3 commits into
mainfrom
hypeship/cua-agent-harness

Conversation

@rgarcia

@rgarcia rgarcia commented May 13, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replace the thin CUA harness wrapper with CuaAgentHarness extending pi AgentHarness.
  • Vendor the pinned pi agent core/harness source with attribution and a TypeScript refresh script, so @onkernel/cua-agent can use AgentHarness, Session, and prepareNextTurn before those APIs are available from the published pi package.
  • Add provider-aware runtime refresh so CuaAgent handles state.model changes and CuaAgentHarness handles setModel() changes, refreshing CUA-owned tools/system prompts and payload transforms.
  • Preserve runtime setActiveTools() selections across CuaAgentHarness.setModel() refreshes.
  • Keep pi harness env and session requirements intact, add TSDoc for the public CUA agent types/classes, remove the getTranscript() helper, and update docs/examples/tests to use session-backed AgentHarness APIs.
  • Fix Kernel computer keypress translation for shortcuts by sending modifier keys via hold_keys, and add shared example logging that prints tool args, result details, URL reads, and assistant text.

Notes

  • packages/agent/CHANGELOG.md is intentionally unchanged; release workflow should update it.
  • Vendored pi source is pinned to earendil-works/pi@40c05f55391663024a6a05ad33249b616a04e7a1 and includes the upstream MIT license in the package contents.

Verification

  • npx tsx packages/agent/scripts/vendor-pi-agent-harness.ts
  • npm run build --workspace @onkernel/cua-agent
  • npm test --workspace @onkernel/cua-agent
  • npm run typecheck
  • npm pack --dry-run --workspace @onkernel/cua-agent
  • npm run example:harness --workspace @onkernel/cua-agent

Note

High Risk
High risk because it replaces the @earendil-works/pi-agent-core dependency with a large vendored copy (including session/harness/compaction logic) and changes core agent/harness runtime behavior around model switching and tool/payload handling.

Overview
Adds CuaAgentHarness by vendoring a pinned subset of pi agent core/harness source into @onkernel/cua-agent, removing the @earendil-works/pi-agent-core dependency and exporting pi types/APIs from the vendored entrypoint.

Updates CuaAgent and the new harness to be provider-aware when models change: assigning agent.state.model or calling harness.setModel() now re-resolves the CUA runtime spec and refreshes CUA-owned defaults (tools, system prompt, payload transforms), while preserving caller-supplied tools/prompts and keeping active tool selections stable across refreshes.

Updates README/examples to use session-backed harness APIs (adds InMemorySessionRepo/NodeExecutionEnv usage), adds shared example logging helpers, includes vendored LICENSE/README in the package output, and fixes Kernel keypress batching by translating modifiers into hold_keys.

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

@rgarcia
rgarcia marked this pull request as ready for review May 13, 2026 23:32
@firetiger-agent

Copy link
Copy Markdown

Firetiger deploy monitoring skipped

This PR didn't match the auto-monitor filter configured on your GitHub connection:

Any PR that changes the kernel API. Monitor changes to API endpoints (packages/api/cmd/api/) and Temporal workflows (packages/api/lib/temporal) in the kernel repo

Reason: PR modifies CUA agent harness in packages/@onkernel/cua-agent, not the kernel API endpoints (packages/api/cmd/api/) or Temporal workflows (packages/api/lib/temporal) specified in the filter.

To monitor this PR anyway, reply with @firetiger monitor this.

@rgarcia
rgarcia force-pushed the hypeship/cua-agent-harness branch from 37a8a22 to 8d9ad6f Compare May 13, 2026 23:42
@rgarcia
rgarcia marked this pull request as draft May 13, 2026 23:42
@rgarcia
rgarcia force-pushed the hypeship/cua-agent-harness branch from 8d9ad6f to ad18df8 Compare May 13, 2026 23:48
@rgarcia
rgarcia marked this pull request as ready for review May 14, 2026 01:00
@firetiger-agent

Copy link
Copy Markdown

Firetiger deploy monitoring skipped

This PR didn't match the auto-monitor filter configured on your GitHub connection:

Any PR that changes the kernel API. Monitor changes to API endpoints (packages/api/cmd/api/) and Temporal workflows (packages/api/lib/temporal) in the kernel repo

Reason: PR modifies agent harness code in packages/agent/, not kernel API endpoints (packages/api/cmd/api/) or Temporal workflows (packages/api/lib/temporal) as specified in the filter.

To monitor this PR anyway, reply with @firetiger monitor this.

Comment thread packages/agent/src/agent.ts Outdated
systemPrompt?: string;
systemPrompt?: AgentHarnessOptions<TSkill, TPromptTemplate, AgentTool>["systemPrompt"];
getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
getApiKeyAndHeaders?: AgentHarnessOptions<TSkill, TPromptTemplate, AgentTool>["getApiKeyAndHeaders"];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why both of these methods (getApiKey and getApiKeyAndHeaders)? seems like pi's AgentHarnessOptions only has getApiKeyAndHeaders so i lean towards simplifying to that. also why does BaseCuaAgentHarnessOptions omit getApiKeyAndHeaders from AgentHarnessOptions only to add it back verbatim from AgentHarnessOptions?

Comment thread packages/agent/src/agent.ts Outdated
export type CuaHarnessOptions = Omit<AgentOptions, "initialState"> & {
type BaseCuaAgentHarnessOptions<TSkill extends Skill, TPromptTemplate extends PromptTemplate> = Omit<
AgentHarnessOptions<TSkill, TPromptTemplate, AgentTool>,
"env" | "session" | "model" | "tools" | "systemPrompt" | "getApiKeyAndHeaders"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why omit env and session tools systemPrompt and getApiKeyAndHeaders only to add them back in what appears to be basically the same definition that AgentHarnessOptions defines?

Comment thread packages/agent/src/agent.ts
@@ -0,0 +1,55 @@
import { mkdir, writeFile } from "node:fs/promises";

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this file should be typescript not mjs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should also have a tsdoc style comment at the top explaining why it exists

import { InMemorySessionStorage } from "./vendor/pi-agent-core/harness/session/storage/memory";

type CuaRuntimeInput = CuaModelRef | Model<Api>;
type CuaRuntimeSpec = ReturnType<typeof resolveCuaRuntimeSpec>;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all main types should have tsdoc-style comments explaining to a newbie what the type's purpose is


clearFollowUpQueue(): void {
this.agent.clearFollowUpQueue();
override get state(): CuaAgentState {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment explaining the override puprose here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the point of this method? does AgentHarness not expose this already via session or whatever? i think we should remove this and if people want a transcript they can access agent.state.messages

@@ -1,7 +1,6 @@
import Kernel from "@onkernel/sdk";

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there should be an e2e live test for Agent and AgentHarness model switching after a turn and verifying the next turn works

Comment thread packages/agent/README.md Outdated
@@ -54,17 +54,15 @@ lifecycle events, custom streaming, and explicit prompt/continue/queue control.
Reach for the harness shape when you want an app layer around the loop:
session/transcript helpers, resource and prompt entry points, provider/auth

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove transcript

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this example should use something from AgentHarness, e.g. session, to do this. and we should get rid of gettranscript

@rgarcia
rgarcia force-pushed the hypeship/cua-agent-harness branch 4 times, most recently from 56040f6 to e3f2fc1 Compare May 14, 2026 02:38

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: setModel reverts active tool selection to constructor value
    • CuaAgentHarness now updates its tracked requested active tools in an overridden setActiveTools method so setModel preserves runtime tool selections, with a test added to prevent regression.

Create PR

Or push these changes by commenting:

@cursor push 3b9b2810e6
Preview (3b9b2810e6)
diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts
--- a/packages/agent/src/agent.ts
+++ b/packages/agent/src/agent.ts
@@ -263,7 +263,7 @@
 	TPromptTemplate extends PromptTemplate = PromptTemplate,
 > extends AgentHarness<TSkill, TPromptTemplate, AgentTool> {
 	private readonly runtime: CuaRuntimeController;
-	private readonly requestedActiveToolNames?: string[];
+	private requestedActiveToolNames?: string[];
 
 	constructor(options: CuaAgentHarnessOptions<TSkill, TPromptTemplate>) {
 		const {
@@ -318,6 +318,11 @@
 		}
 		await super.setModel(this.runtime.model);
 	}
+
+	override async setActiveTools(toolNames: string[]): Promise<void> {
+		this.requestedActiveToolNames = [...toolNames];
+		await super.setActiveTools(toolNames);
+	}
 }
 
 function composeOnPayload(first: AgentOptions["onPayload"], second: AgentOptions["onPayload"]): AgentOptions["onPayload"] {

diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts
--- a/packages/agent/test/agent.test.ts
+++ b/packages/agent/test/agent.test.ts
@@ -217,4 +217,18 @@
 		expect(harness.agent.state.model.id).toBe(runtime.model.id);
 		expect(harness.agent.state.tools).toHaveLength(runtime.toolDefinitions.length);
 	});
+
+	it("preserves active tool selection when setModel refreshes tools", async () => {
+		const harness = new CuaAgentHarness({
+			...(await createHarnessServices()),
+			browser,
+			client,
+			model: "openai:gpt-5.5",
+		});
+
+		await harness.setActiveTools([]);
+		await harness.setModel("google:gemini-3-pro-preview");
+
+		expect(harness.agent.state.tools).toEqual([]);
+	});
 });

You can send follow-ups to the cloud agent here.

Comment thread packages/agent/src/agent.ts Outdated
@rgarcia
rgarcia merged commit 18af410 into main May 14, 2026
4 checks passed

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Runtime spec update not rolled back on setModel failure
    • CuaAgentHarness.setModel now resolves the target runtime spec up front and only updates the controller runtime after tool and model updates succeed, preventing stale runtime mutations on failure.
  • ✅ Fixed: onPayloadFor redundantly re-resolves the CUA runtime spec
    • CuaRuntimeController.onPayloadFor now reuses the cached runtime spec when the requested model matches the current runtime model and only re-resolves on mismatch.

Create PR

Or push these changes by commenting:

@cursor push 2b8d964ed8
Preview (2b8d964ed8)
diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts
--- a/packages/agent/src/agent.ts
+++ b/packages/agent/src/agent.ts
@@ -129,19 +129,30 @@
 		this.runtimeSpec = resolveCuaRuntimeSpec(model);
 	}
 
-	tools(): AgentTool[] {
+	private buildTools(runtimeSpec: CuaRuntimeSpec): AgentTool[] {
 		return (
 			this.options.tools ??
 			createCuaComputerTools({
 				browser: this.options.browser,
 				client: this.options.client,
-				toolDefinitions: this.runtimeSpec.toolDefinitions,
+				toolDefinitions: runtimeSpec.toolDefinitions,
 			})
 		);
 	}
 
+	tools(): AgentTool[] {
+		return this.buildTools(this.runtimeSpec);
+	}
+
+	toolsFor(runtimeSpec: CuaRuntimeSpec): AgentTool[] {
+		return this.buildTools(runtimeSpec);
+	}
+
 	onPayloadFor(model: CuaRuntimeInput): SimpleStreamOptions["onPayload"] {
-		const runtimeSpec = resolveCuaRuntimeSpec(model);
+		const runtimeSpec =
+			typeof model === "string" || !isSameModel(model, this.runtimeSpec.model)
+				? resolveCuaRuntimeSpec(model)
+				: this.runtimeSpec;
 		return composeOnPayload(runtimeSpec.onPayload, this.options.onPayload);
 	}
 }
@@ -311,12 +322,19 @@
 	 * concrete model selected by `@onkernel/cua-ai`.
 	 */
 	override async setModel(model: CuaRuntimeInput): Promise<void> {
-		this.runtime.setModel(model);
+		const runtimeSpec = resolveCuaRuntimeSpec(model);
 		if (this.runtime.ownsTools) {
-			const tools = this.runtime.tools();
-			await super.setTools(tools, this.requestedActiveToolNames ?? tools.map((tool) => tool.name));
+			const tools = this.runtime.toolsFor(runtimeSpec);
+			const activeToolNames = this.requestedActiveToolNames ?? tools.map((tool) => tool.name);
+			const toolNameSet = new Set(tools.map((tool) => tool.name));
+			const missingToolNames = activeToolNames.filter((toolName) => !toolNameSet.has(toolName));
+			if (missingToolNames.length > 0) {
+				throw new Error(`Unknown tool(s): ${missingToolNames.join(", ")}`);
+			}
+			await super.setTools(tools, activeToolNames);
 		}
-		await super.setModel(this.runtime.model);
+		await super.setModel(runtimeSpec.model);
+		this.runtime.setModel(runtimeSpec.model);
 	}
 
 	override async setActiveTools(toolNames: string[]): Promise<void> {
@@ -333,3 +351,7 @@
 		return second(afterFirst ?? payload, modelRef);
 	};
 }
+
+function isSameModel(left: Model<Api>, right: Model<Api>): boolean {
+	return left.api === right.api && left.provider === right.provider && left.id === right.id;
+}

diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts
--- a/packages/agent/test/agent.test.ts
+++ b/packages/agent/test/agent.test.ts
@@ -231,4 +231,20 @@
 
 		expect(harness.agent.state.tools).toEqual([]);
 	});
+
+	it("keeps runtime spec unchanged if setModel fails validation", async () => {
+		const runtime = resolveCuaRuntimeSpec("openai:gpt-5.5");
+		const harness = new CuaAgentHarness({
+			...(await createHarnessServices()),
+			browser,
+			client,
+			model: "openai:gpt-5.5",
+		});
+
+		(harness as unknown as { requestedActiveToolNames?: string[] }).requestedActiveToolNames = ["missing-tool"];
+
+		await expect(harness.setModel("google:gemini-3-pro-preview")).rejects.toThrow("Unknown tool(s): missing-tool");
+		expect(harness.agent.state.model.id).toBe(runtime.model.id);
+		expect((harness as unknown as { runtime: { model: { id: string } } }).runtime.model.id).toBe(runtime.model.id);
+	});
 });

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 35399e8. Configure here.

const tools = this.runtime.tools();
await super.setTools(tools, this.requestedActiveToolNames ?? tools.map((tool) => tool.name));
}
await super.setModel(this.runtime.model);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Runtime spec update not rolled back on setModel failure

Low Severity

In CuaAgentHarness.setModel, this.runtime.setModel(model) eagerly updates the internal runtimeSpec before super.setTools and super.setModel are called. If super.setTools throws (e.g., validateToolNames fails because requestedActiveToolNames contains names absent from the new provider's tools), the runtimeSpec is left pointing to the new model while the harness model, session, and active tools remain unchanged. Subsequent reads of runtime.model, runtime.systemPrompt, or runtime.tools() would return values for the new model rather than the old one, creating an inconsistent internal state.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 35399e8. Configure here.

return this.agent.subscribe(listener);
onPayloadFor(model: CuaRuntimeInput): SimpleStreamOptions["onPayload"] {
const runtimeSpec = resolveCuaRuntimeSpec(model);
return composeOnPayload(runtimeSpec.onPayload, this.options.onPayload);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onPayloadFor redundantly re-resolves the CUA runtime spec

Low Severity

CuaRuntimeController.onPayloadFor calls resolveCuaRuntimeSpec(model) on every invocation to build a fresh spec from the model parameter, completely ignoring the already-cached this.runtimeSpec. Every provider request triggers this redundant resolution. While the behavior is correct, onPayloadFor could read onPayload from this.runtimeSpec when the model matches, avoiding repeated work.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 35399e8. Configure here.

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