Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ someone who wants to read the code, contribute, or fork.
- provider payload transforms and protocol quirks (for example, Yutori tool serialization policy)
- canonical CUA tool-definition exports
- `@onkernel/cua-agent` owns browser execution orchestration:
- `CuaAgent` / `CuaHarness` class wiring around `pi-agent-core`
- `CuaAgent` / `CuaAgentHarness` class wiring around vendored pi agent core
- executing canonical CUA tool calls against Kernel browsers
- typed executor coverage and translator integration

Expand All @@ -66,7 +66,7 @@ Dev/test:
└── @onkernel/ptywright (PTY-backed TUI regression harness)

External:
├── @earendil-works/pi-agent-core # Agent loop, tool execution, streaming, steering
├── vendored pi agent core # Agent loop, tool execution, streaming, steering
│ └── @earendil-works/pi-ai # Provider transport (OpenAI Responses, Anthropic Messages, Google GenAI)
├── @earendil-works/pi-coding-agent # bash / read / write / edit / grep / find / ls AgentTools + SessionManager + skills
├── @earendil-works/pi-tui # Terminal, Editor, Image, differential renderer
Expand Down
17 changes: 2 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 44 additions & 19 deletions packages/agent/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# `@onkernel/cua-agent`

Kernel browser computer-use classes built on
[`@earendil-works/pi-agent-core`](https://github.com/earendil-works/pi/tree/main/packages/agent).
Kernel browser computer-use classes built on vendored pi `Agent` and
`AgentHarness` source.

This package keeps pi-agent-core semantics intact and adds browser execution
This package keeps pi agent semantics intact and adds browser execution
plumbing for canonical CUA tools.

## Installation
Expand Down Expand Up @@ -33,45 +33,59 @@ const agent = new CuaAgent({
await agent.prompt("Open news.ycombinator.com and summarize the top story.");
```

## Quick Start (`CuaHarness`)
## Quick Start (`CuaAgentHarness`)

```ts
import { CuaHarness } from "@onkernel/cua-agent";
import { CuaAgentHarness, InMemorySessionRepo, NodeExecutionEnv } from "@onkernel/cua-agent";

const harness = new CuaHarness({
const sessionRepo = new InMemorySessionRepo();
const session = await sessionRepo.create({ id: "example" });

const harness = new CuaAgentHarness({
browser,
client,
env: new NodeExecutionEnv({ cwd: process.cwd() }),
model: "openai:gpt-5.5",
session,
});

await harness.prompt("Open example.com and tell me the current URL.");
const transcript = harness.getTranscript();
console.log("messages in transcript:", transcript.length);
const response = await harness.prompt("Open example.com and tell me the current URL.");
const branch = await session.getBranch();
const lastAssistant = [...branch]
.reverse()
.flatMap((entry) =>
entry.type === "message" && entry.message.role === "assistant" ? [entry.message] : [],
)[0];
const assistant = lastAssistant ?? response;
const assistantText = assistant.content
.flatMap((block) => (block.type === "text" ? [block.text] : []))
.join("")
.trim();
console.log("assistant stopReason:", assistant.stopReason);
console.log("assistant text:", assistantText || "(no text)");
```

Use `CuaAgent` when you want direct pi `Agent` control: raw transcript state,
Use `CuaAgent` when you want direct pi `Agent` control: raw message state,
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
hooks, active tool selection, compaction/tree workflows, and higher-level queue
events. `CuaHarness` is the thin CUA version of that shape today: it installs
CUA defaults, delegates runtime methods to the wrapped `Agent`, and adds
`getTranscript()`.
session-backed turns, resource and prompt entry points, provider/auth hooks,
active tool selection, compaction/tree workflows, and higher-level queue events.
`CuaAgentHarness` extends pi `AgentHarness`, installs CUA defaults, and refreshes
provider-specific runtime state when `setModel()` changes models.

## Core Concepts

### Class-First API

- `CuaAgent extends Agent`
- `CuaHarness` wraps a pi `Agent` with a harness-style constructor and
delegated runtime methods.
- `CuaAgentHarness extends AgentHarness`

Both classes mirror pi constructor shapes and behavior, with minimal additions:
- `browser` (Kernel browser response)
- `client` (Kernel SDK client)
- CUA model refs (`"provider:model"`) accepted where pi expects a concrete model

If `getApiKey` is omitted, both classes default to CUA env var conventions:
If auth callbacks are omitted, both classes default to CUA env var conventions:
- OpenAI: `OPENAI_API_KEY`
- Anthropic: `ANTHROPIC_OAUTH_TOKEN` or `ANTHROPIC_API_KEY`
- Gemini: `GOOGLE_API_KEY` or `GEMINI_API_KEY`
Expand All @@ -84,6 +98,17 @@ If tools are omitted, the classes install canonical CUA computer tool executors
using runtime specs from `@onkernel/cua-ai`. If tools are provided, they are
used exactly.

### Model Switching

`CuaAgent` follows pi `Agent` semantics: assign `agent.state.model` to a
concrete model or CUA model ref. CUA-owned tools and the default system prompt
refresh with the new provider runtime.

`CuaAgentHarness` follows pi `AgentHarness` semantics: call
`await harness.setModel(model)`. The harness updates its model through pi's
snapshot machinery and refreshes CUA-owned tools and default prompt state for
the next provider request.

### Tool Composition

Use `createCuaComputerTools()` to compose your own tool list from canonical
Expand All @@ -105,4 +130,4 @@ const tools = [
```

For full event semantics, steering, follow-up queues, and tool execution
details, see the pi-agent-core README.
details, see the pi agent core source vendored in this package.
14 changes: 4 additions & 10 deletions packages/agent/examples/agent-openai-smoke.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Kernel from "@onkernel/sdk";
import { requireCuaEnvApiKeyForModel, type CuaModelRef } from "@onkernel/cua-ai";
import { CuaAgent } from "../src/index";
import { logAgentEvent, logAssistant } from "./shared/logging";
import { SCENARIOS } from "./shared/scenarios";

const modelRef = (process.env.MODEL_REF as CuaModelRef | undefined) ?? "openai:gpt-5.5";
Expand All @@ -19,20 +20,13 @@ async function main(): Promise<void> {
initialState: { model: modelRef },
});

agent.subscribe((event) => {
if (event.type === "tool_execution_start") {
console.log(`[tool:start] ${event.toolName}`);
}
if (event.type === "tool_execution_end") {
console.log(`[tool:end] ${event.toolName} error=${event.isError}`);
}
});
agent.subscribe(logAgentEvent);

const scenario = SCENARIOS[0]!;
console.log(`running scenario: ${scenario.name}`);
console.log(`running scenario: ${scenario.name} model=${modelRef}`);
await agent.prompt(scenario.prompt);
const assistant = [...agent.state.messages].reverse().find((message) => message.role === "assistant");
console.log("assistant stopReason:", assistant?.role === "assistant" ? assistant.stopReason : "unknown");
logAssistant(assistant?.role === "assistant" ? assistant : undefined);
} finally {
await client.browsers.deleteByID(browser.session_id);
}
Expand Down
4 changes: 4 additions & 0 deletions packages/agent/examples/agent-provider-matrix.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Kernel from "@onkernel/sdk";
import { requireCuaEnvApiKeyForModel, type CuaModelRef } from "@onkernel/cua-ai";
import { CuaAgent } from "../src/index";
import { logAgentEvent, logAssistant } from "./shared/logging";
import { SCENARIOS } from "./shared/scenarios";

const modelRef = (process.env.MODEL_REF as CuaModelRef | undefined) ?? "openai:gpt-5.5";
Expand All @@ -20,8 +21,11 @@ async function main(): Promise<void> {
client,
initialState: { model: modelRef },
});
agent.subscribe(logAgentEvent);
console.log(`model=${modelRef} scenario=${scenario.name}`);
await agent.prompt(scenario.prompt);
const assistant = [...agent.state.messages].reverse().find((message) => message.role === "assistant");
logAssistant(assistant?.role === "assistant" ? assistant : undefined);
} finally {
await client.browsers.deleteByID(browser.session_id);
}
Expand Down
33 changes: 17 additions & 16 deletions packages/agent/examples/harness-openai-smoke.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Kernel from "@onkernel/sdk";
import { requireCuaEnvApiKeyForModel, type CuaModelRef } from "@onkernel/cua-ai";
import { CuaHarness } from "../src/index";
import { CuaAgentHarness, InMemorySessionRepo, NodeExecutionEnv } from "../src/index";
import { logAgentEvent, logAssistant } from "./shared/logging";
import { SCENARIOS } from "./shared/scenarios";

const modelRef = (process.env.MODEL_REF as CuaModelRef | undefined) ?? "openai:gpt-5.5";
Expand All @@ -13,28 +14,28 @@ async function main(): Promise<void> {
const browser = await client.browsers.create({ stealth: true });

try {
const harness = new CuaHarness({
const sessionRepo = new InMemorySessionRepo();
const session = await sessionRepo.create({ id: "harness-openai-smoke" });
const harness = new CuaAgentHarness({
browser,
client,
env: new NodeExecutionEnv({ cwd: process.cwd() }),
model: modelRef,
session,
});

harness.subscribe((event) => {
if (event.type === "tool_execution_start") {
console.log(`[tool:start] ${event.toolName}`);
}
if (event.type === "tool_execution_end") {
console.log(`[tool:end] ${event.toolName} error=${event.isError}`);
}
});
harness.subscribe(logAgentEvent);

const scenario = SCENARIOS[0]!;
console.log(`running scenario: ${scenario.name}`);
await harness.prompt(scenario.prompt);
const transcript = harness.getTranscript();
const lastAssistant = [...transcript].reverse().find((message) => message.role === "assistant");
console.log("transcript messages:", transcript.length);
console.log("assistant stopReason:", lastAssistant?.role === "assistant" ? lastAssistant.stopReason : "unknown");
console.log(`running scenario: ${scenario.name} model=${modelRef}`);
const response = await harness.prompt(scenario.prompt);
const branch = await session.getBranch();
const lastAssistant = [...branch]
.reverse()
.flatMap((entry) =>
entry.type === "message" && entry.message.role === "assistant" ? [entry.message] : [],
)[0];
logAssistant(lastAssistant ?? response);
} finally {
await client.browsers.deleteByID(browser.session_id);
}
Expand Down
20 changes: 16 additions & 4 deletions packages/agent/examples/harness-provider-matrix.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Kernel from "@onkernel/sdk";
import { requireCuaEnvApiKeyForModel, type CuaModelRef } from "@onkernel/cua-ai";
import { CuaHarness } from "../src/index";
import { CuaAgentHarness, InMemorySessionRepo, NodeExecutionEnv } from "../src/index";
import { logAgentEvent, logAssistant } from "./shared/logging";
import { SCENARIOS } from "./shared/scenarios";

const modelRef = (process.env.MODEL_REF as CuaModelRef | undefined) ?? "openai:gpt-5.5";
Expand All @@ -15,14 +16,25 @@ async function main(): Promise<void> {
const scenario = SCENARIOS.find((entry) => entry.name === scenarioName) ?? SCENARIOS[0]!;

try {
const harness = new CuaHarness({
const sessionRepo = new InMemorySessionRepo();
const session = await sessionRepo.create({ id: `harness-provider-matrix-${scenario.name}` });
const harness = new CuaAgentHarness({
browser,
client,
env: new NodeExecutionEnv({ cwd: process.cwd() }),
model: modelRef,
session,
});
harness.subscribe(logAgentEvent);
console.log(`model=${modelRef} scenario=${scenario.name}`);
await harness.prompt(scenario.prompt);
console.log("transcript messages:", harness.getTranscript().length);
const response = await harness.prompt(scenario.prompt);
const branch = await session.getBranch();
const lastAssistant = [...branch]
.reverse()
.flatMap((entry) =>
entry.type === "message" && entry.message.role === "assistant" ? [entry.message] : [],
)[0];
logAssistant(lastAssistant ?? response);
} finally {
await client.browsers.deleteByID(browser.session_id);
}
Expand Down
37 changes: 37 additions & 0 deletions packages/agent/examples/shared/logging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { AgentEvent, AgentHarnessEvent } from "../../src/index";

type AssistantLike = {
content: Array<{ type: string; text?: string }>;
stopReason?: string;
};

export function logAgentEvent(event: AgentEvent | AgentHarnessEvent): void {
if (event.type === "tool_execution_start") {
console.log(`[tool:start] ${event.toolName} args=${formatJson(event.args)}`);
return;
}
if (event.type === "tool_execution_end") {
const result = event.result as { details?: unknown } | undefined;
console.log(`[tool:end] ${event.toolName} error=${event.isError}${formatDetails(result?.details)}`);
}
}

export function logAssistant(assistant: AssistantLike | undefined): void {
const text =
assistant?.content
.flatMap((block) => (block.type === "text" && typeof block.text === "string" ? [block.text] : []))
.join("")
.trim() ?? "";
console.log("assistant stopReason:", assistant?.stopReason ?? "unknown");
console.log("assistant text:", text || "(no text)");
}

function formatDetails(details: unknown): string {
if (!details) return "";
return ` details=${formatJson(details)}`;
}

function formatJson(value: unknown): string {
const text = JSON.stringify(value, null, 2) ?? "undefined";
return text.length > 1200 ? `${text.slice(0, 1197)}...` : text;
}
8 changes: 5 additions & 3 deletions packages/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"dist",
"examples",
"README.md",
"CHANGELOG.md"
"CHANGELOG.md",
"src/vendor/pi-agent-core/LICENSE",
"src/vendor/pi-agent-core/README.md"
],
"scripts": {
"build": "tsc -b",
Expand All @@ -26,10 +28,10 @@
"test": "vitest --run"
},
"dependencies": {
"@earendil-works/pi-agent-core": "^0.74.0",
"@earendil-works/pi-ai": "^0.74.0",
"@onkernel/cua-ai": "0.1.0",
"@onkernel/sdk": "0.49.0"
"@onkernel/sdk": "0.49.0",
"typebox": "^1.1.38"
},
"devDependencies": {
"vitest": "^3.2.4"
Expand Down
Loading
Loading