diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 798e6e62..1aac9651 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,14 +94,20 @@ jobs: run: npm test --workspace @onkernel/cua-cli - name: Build cua-cli run: npm run build --workspace @onkernel/cua-cli - - name: Pack tarball - run: npm pack --workspace @onkernel/cua-cli --pack-destination "$RUNNER_TEMP" + - name: Pack tarballs + # Pack the workspace dependencies too so the smoke install resolves + # them from the tarballs instead of the registry, where the versions + # under development are not published yet. + run: | + npm pack --workspace @onkernel/cua-ai --pack-destination "$RUNNER_TEMP" + npm pack --workspace @onkernel/cua-agent --pack-destination "$RUNNER_TEMP" + npm pack --workspace @onkernel/cua-cli --pack-destination "$RUNNER_TEMP" - name: CLI bin smoke test run: | SMOKE_DIR=$(mktemp -d) cd "$SMOKE_DIR" npm init -y > /dev/null - npm install "$RUNNER_TEMP"/onkernel-cua-cli-*.tgz + npm install "$RUNNER_TEMP"/onkernel-cua-ai-*.tgz "$RUNNER_TEMP"/onkernel-cua-agent-*.tgz "$RUNNER_TEMP"/onkernel-cua-cli-*.tgz OUTPUT=$(./node_modules/.bin/cua --help) echo "$OUTPUT" echo "$OUTPUT" | grep -q "Usage:" diff --git a/docs/architecture.md b/docs/architecture.md index 0c7b1fac..81f7e201 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -247,7 +247,7 @@ user prompt ├─► resolveCuaRuntimeSpec(model) provides toolDefinitions, │ toolExecutors, defaultSystemPrompt, onPayload, … ├─► onPayload (cua-ai) — provider payload transforms - ├─► pi-ai streamSimple → provider HTTP SSE + ├─► pi Models.streamSimple (cuaModels) → provider HTTP SSE │ (OpenAI Responses / Anthropic Messages / Google GenAI / Tzafon / Yutori) ├─► tool_call events carry canonical CuaAction args │ └─► cua-agent executor @@ -286,7 +286,7 @@ flowchart LR browserMod --> sdk[("@onkernel/sdk")] harness --> agentPkg["cua-agent: CuaAgentHarness"] agentPkg --> piHarness["pi-agent-core AgentHarness"] - piHarness --> piAi["pi-ai streamSimple"] + piHarness --> piAi["pi Models.streamSimple (cuaModels)"] piAi -->|"openai-responses"| openai[("api.openai.com /v1/responses")] piAi -->|"anthropic-messages + beta header"| anthro[("api.anthropic.com /v1/messages")] piAi -->|"google-generative-ai"| gemini[("generativelanguage.googleapis.com")] diff --git a/docs/cua-cli-harness-migration.md b/docs/cua-cli-harness-migration.md index 5cefdcdb..f89bb4cc 100644 --- a/docs/cua-cli-harness-migration.md +++ b/docs/cua-cli-harness-migration.md @@ -46,9 +46,10 @@ Goals, in priority order: interactive UI is rebuilt on pi-tui primitives instead. pi-tui 0.79 provides `Markdown`, `Image` (kitty/iTerm2), `Editor` (autocomplete), `SelectList`, overlays, keybindings, `ProcessTerminal`. -- pi-ai's `registerApiProvider` registry is process-global: a test fixture can - register a scripted `streamSimple` for an API id and the real harness will - route model calls through it. This replaces the `InteractiveDriver` test seam. +- The harness streams through a pi `Models` collection: a test fixture can + build a `createCuaModels()` collection with a scripted provider and pass it + to `buildCuaHarness({ models })`, and the real harness routes model calls + through it. This replaces the `InteractiveDriver` test seam. - `@onkernel/cua-ai` owns the model catalog: `listCuaModels`, `parseCuaModelRef`, `getCuaModel`, `CuaModelRef` (`provider:model`), and the documented API-key env-var conventions (`getCuaEnvApiKey`). @@ -128,10 +129,10 @@ Module map (current → target): ## Test strategy -- **Scripted provider fixture**: a test-only pi-ai provider registered with - `registerApiProvider`, whose `streamSimple` replays declarative steps (text - deltas, canonical CUA tool calls, errors, await-abort) — port of the current - `ScriptedDriver` JSON step DSL, moved below the harness. +- **Scripted provider fixture**: a test-only pi provider placed in a + `createCuaModels()` collection, whose `streamSimple` replays declarative + steps (text deltas, canonical CUA tool calls, errors, await-abort) — port + of the current `ScriptedDriver` JSON step DSL, moved below the harness. - **Fake Kernel client**: plain object stubbing `browsers.computer.{batch,captureScreenshot,readClipboard}` etc. (pattern already proven in `packages/agent/test`). diff --git a/package-lock.json b/package-lock.json index 6f5d0f25..75af476a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,26 @@ "node": ">=22.19.0" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", @@ -526,7 +546,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.29.2", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -547,12 +569,12 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.79.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.1.tgz", - "integrity": "sha512-PBPjBa2YBm9jauiLtHAKaSfVJ4Dvm3/nK/bR/oHebLjwBCS2tGx3aQDX7MSGAOXi6BejlhzbB/z82BkyAyNjjQ==", + "version": "0.80.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.80.3.tgz", + "integrity": "sha512-3qw0/GeRQBU/nlGjDe5Yb7ePKTmoxefx2YxyKMFAviFUMXpFexBG/hS7mBtwFahFvzrrTPPoRT6sFIDjwoDWPQ==", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.1", + "@earendil-works/pi-ai": "^0.80.3", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -562,15 +584,16 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.79.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.1.tgz", - "integrity": "sha512-UnORwrcsTNLm4StEvoM8iEom0u87Te7BXEWxhec3iNXygWD6eEBosUoq9ddcveqtj/QpUZBMPWUu81cCtZxzkQ==", + "version": "0.80.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.80.3.tgz", + "integrity": "sha512-jPZLMeGL5kkMSEAwAklfXTMHqZvfhsJtCCpKGIr5Duk7mc0n4skjB1dugk7y0z3z8ZHIUCmPAWHdyDqgUz5vdA==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", "@google/genai": "1.52.0", - "@mistralai/mistralai": "2.2.1", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", @@ -585,47 +608,16 @@ "node": ">=22.19.0" } }, - "node_modules/@earendil-works/pi-ai/node_modules/@anthropic-ai/sdk": { - "version": "0.91.1", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", - "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", - "license": "MIT", - "dependencies": { - "json-schema-to-ts": "^3.1.1" - }, - "bin": { - "anthropic-ai-sdk": "bin/cli" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } - } - }, - "node_modules/@earendil-works/pi-ai/node_modules/@mistralai/mistralai": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", - "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", - "license": "Apache-2.0", - "dependencies": { - "ws": "^8.18.0", - "zod": "^3.25.0 || ^4.0.0", - "zod-to-json-schema": "^3.25.0" - } - }, "node_modules/@earendil-works/pi-coding-agent": { - "version": "0.79.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.79.1.tgz", - "integrity": "sha512-dLnje4U5H3/ZytJpvhjhPINeDT/yvx85e4OH/ziMQRLpPlfNP12/peY9jRQd4W11Xth2+y2xGAFwS+NeVf2ZwA==", + "version": "0.80.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.80.3.tgz", + "integrity": "sha512-TIggw9gCXpA+Ph7OjdTA7ka2NPwTVuPmy39KDSyUzaKq8VvHfMGR7vtRz4JB7Um/RMRblmzhu4p9tUCk6MTgGA==", "hasShrinkwrap": true, "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.1", - "@earendil-works/pi-ai": "^0.79.1", - "@earendil-works/pi-tui": "^0.79.1", + "@earendil-works/pi-agent-core": "^0.80.3", + "@earendil-works/pi-ai": "^0.80.3", + "@earendil-works/pi-tui": "^0.80.3", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -637,8 +629,9 @@ "jiti": "2.7.0", "minimatch": "10.2.5", "proper-lockfile": "4.1.2", + "semver": "7.8.0", "typebox": "1.1.38", - "undici": "8.3.0", + "undici": "8.5.0", "yaml": "2.9.0" }, "bin": { @@ -1087,11 +1080,11 @@ } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core": { - "version": "0.79.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.1.tgz", + "version": "0.80.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.80.3.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.1", + "@earendil-works/pi-ai": "^0.80.3", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -1101,14 +1094,15 @@ } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": { - "version": "0.79.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.1.tgz", + "version": "0.80.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.80.3.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", "@google/genai": "1.52.0", - "@mistralai/mistralai": "2.2.1", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", @@ -1124,12 +1118,12 @@ } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui": { - "version": "0.79.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.1.tgz", + "version": "0.80.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.3.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", - "marked": "15.0.12" + "marked": "18.0.5" }, "engines": { "node": ">=22.19.0" @@ -1339,14 +1333,23 @@ } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@mistralai/mistralai": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", - "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", + "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", "license": "Apache-2.0", "dependencies": { + "@opentelemetry/semantic-conventions": "^1.40.0", "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@nodable/entities": { @@ -1361,6 +1364,24 @@ ], "license": "MIT" }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -1380,9 +1401,9 @@ "license": "BSD-3-Clause" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", "license": "BSD-3-Clause" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/fetch": { @@ -1400,12 +1421,6 @@ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", "license": "BSD-3-Clause" }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/inquire": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", - "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", - "license": "BSD-3-Clause" - }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", @@ -1997,15 +2012,15 @@ } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/marked": { - "version": "15.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", - "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", "license": "MIT", "bin": { "marked": "bin/marked.js" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/minimatch": { @@ -2183,24 +2198,23 @@ } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/protobufjs": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.9.tgz", - "integrity": "sha512-Od4muIm3HW1AouyHF5lONOf1FWo3hY1NbFDoy191X9GzhpgW1clCoaFjfVs2rKJNFYpTNJbje4cbAIDBZJ63ZA==", + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" }, "engines": { "node": ">=12.0.0" @@ -2235,6 +2249,18 @@ ], "license": "MIT" }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@earendil-works/pi-coding-agent/node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2293,9 +2319,9 @@ "license": "MIT" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/undici": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz", - "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz", + "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==", "license": "MIT", "engines": { "node": ">=22.19.0" @@ -2332,9 +2358,9 @@ } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -2387,7 +2413,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -2402,13 +2427,13 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.79.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.1.tgz", - "integrity": "sha512-YvZCMfSE0YDSLNklAwMY6LC6SyEgnP0zMOoioTLNnXFNdexrCexMJdee7iDJsNcFlKt7+DVLccomuURtZS1C6g==", + "version": "0.80.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.3.tgz", + "integrity": "sha512-2BJI6qwRQfnM0Q7seL1+SbacU/jRRjBnN7Hu3n9BjAn7/s5FaBNnvdD1qBQYRsFTHfjqMaDsjYqanPyqwXj99w==", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", - "marked": "15.0.12" + "marked": "18.0.5" }, "engines": { "node": ">=22.19.0" @@ -3384,6 +3409,26 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mistralai/mistralai": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", + "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.40.0", + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", @@ -3435,6 +3480,24 @@ "version": "0.49.0", "license": "Apache-2.0" }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.42.0.tgz", + "integrity": "sha512-icc5xCzndZfhuJMy5oqk5AvloWquR7jtae74qzpkKkhGp8BivK+oCcEXgGnjCdTfp8hA44l+w8gE8yYJbocJJw==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@oxc-project/types": { "version": "0.134.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.134.0.tgz", @@ -4959,6 +5022,8 @@ }, "node_modules/json-schema-to-ts": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", @@ -5007,13 +5072,15 @@ } }, "node_modules/marked": { - "version": "15.0.12", + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", "license": "MIT", "bin": { "marked": "bin/marked.js" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/ms": { @@ -5288,7 +5355,6 @@ "integrity": "sha512-zpMvlJhs5PkXRTtKc0CaLBVI9AR/VDiJFpM+kx//hgToEca7FgMlGjaRIisXBcb19T76LswgmKECSQ96hjWr5A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@oxc-project/types": "=0.134.0", "@rolldown/pluginutils": "^1.0.0" @@ -5626,6 +5692,8 @@ }, "node_modules/ts-algebra": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", "license": "MIT" }, "node_modules/tsdown": { @@ -5731,7 +5799,6 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -5756,7 +5823,6 @@ "version": "5.9.3", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5789,7 +5855,6 @@ "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -6031,13 +6096,14 @@ "node_modules/zod": { "version": "4.3.6", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-to-json-schema": { "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", "license": "ISC", "peerDependencies": { "zod": "^3.25.28 || ^4" @@ -6045,12 +6111,12 @@ }, "packages/agent": { "name": "@onkernel/cua-agent", - "version": "0.3.5", + "version": "0.4.0", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "0.79.1", - "@earendil-works/pi-ai": "0.79.1", - "@onkernel/cua-ai": "0.3.2", + "@earendil-works/pi-agent-core": "0.80.3", + "@earendil-works/pi-ai": "0.80.3", + "@onkernel/cua-ai": "0.4.0", "@onkernel/sdk": "0.49.0", "sharp": "^0.34.5" }, @@ -6059,23 +6125,12 @@ "vitest": "^3.2.4" } }, - "packages/agent/node_modules/@onkernel/cua-ai": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@onkernel/cua-ai/-/cua-ai-0.3.2.tgz", - "integrity": "sha512-EmxKIElQ+e54wx5RcxCWCEn31XxXtEuTYEHtumooDAvy2rKagD5OiI41FJUBWjZcubLFFJ7oQgKtRGTIaKUg7g==", - "license": "MIT", - "dependencies": { - "@earendil-works/pi-ai": "0.79.1", - "@tzafon/lightcone": "^0.7.0", - "openai": "^6.26.0" - } - }, "packages/ai": { "name": "@onkernel/cua-ai", - "version": "0.3.4", + "version": "0.4.0", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "0.79.1", + "@earendil-works/pi-ai": "0.80.3", "@tzafon/lightcone": "^0.7.0", "openai": "^6.26.0" }, @@ -6086,13 +6141,13 @@ }, "packages/cli": { "name": "@onkernel/cua-cli", - "version": "0.1.4", + "version": "0.2.0", "license": "MIT", "dependencies": { - "@earendil-works/pi-coding-agent": "0.79.1", - "@earendil-works/pi-tui": "0.79.1", - "@onkernel/cua-agent": "0.3.5", - "@onkernel/cua-ai": "0.3.2", + "@earendil-works/pi-coding-agent": "0.80.3", + "@earendil-works/pi-tui": "0.80.3", + "@onkernel/cua-agent": "0.4.0", + "@onkernel/cua-ai": "0.4.0", "@onkernel/sdk": "0.49.0" }, "bin": { @@ -6107,17 +6162,6 @@ "node": ">=22.19.0" } }, - "packages/cli/node_modules/@onkernel/cua-ai": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@onkernel/cua-ai/-/cua-ai-0.3.2.tgz", - "integrity": "sha512-EmxKIElQ+e54wx5RcxCWCEn31XxXtEuTYEHtumooDAvy2rKagD5OiI41FJUBWjZcubLFFJ7oQgKtRGTIaKUg7g==", - "license": "MIT", - "dependencies": { - "@earendil-works/pi-ai": "0.79.1", - "@tzafon/lightcone": "^0.7.0", - "openai": "^6.26.0" - } - }, "packages/ptywright": { "name": "@onkernel/ptywright", "version": "0.1.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 68e68da2..e8c88f6a 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 0.4.0 - 2026-07-07 + +Breaking: follows pi-agent-core 0.80's `Models`-based harness. + +- `CuaAgentHarness` accepts an optional `models` (a pi `Models` collection) + and defaults to `cuaModels()` from `@onkernel/cua-ai`. The + `getApiKeyAndHeaders` option is gone — pi-agent-core 0.80 resolves auth + through provider auth on the collection; pass a custom `models` to override + resolution (e.g. in tests). +- `CuaAgent`'s default stream path is `cuaModels().streamSimple` instead of + pi-ai's removed global `streamSimple`. Custom `streamFn` options work + unchanged. +- Updated `@earendil-works/pi-agent-core` and `@earendil-works/pi-ai` to + 0.80.3 and `@onkernel/cua-ai` to 0.4.0. + ## 0.3.5 - 2026-06-24 - Update the `@onkernel/cua-ai` dependency to 0.3.2, adding computer-use diff --git a/packages/agent/package.json b/packages/agent/package.json index 5e7230bf..8c0b0b78 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@onkernel/cua-agent", - "version": "0.3.5", + "version": "0.4.0", "description": "Kernel browser computer-use Agent and AgentHarness classes built on pi-agent-core", "license": "MIT", "type": "module", @@ -40,9 +40,9 @@ "typecheck": "tsc -b" }, "dependencies": { - "@earendil-works/pi-agent-core": "0.79.1", - "@earendil-works/pi-ai": "0.79.1", - "@onkernel/cua-ai": "0.3.2", + "@earendil-works/pi-agent-core": "0.80.3", + "@earendil-works/pi-ai": "0.80.3", + "@onkernel/cua-ai": "0.4.0", "@onkernel/sdk": "0.49.0", "sharp": "^0.34.5" }, diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index 3e158f2e..1c289c56 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -13,20 +13,25 @@ import { type Api, CUA_NAVIGATION_TOOL_NAME, CUA_PLAYWRIGHT_TOOL_NAME, + cuaModels, type CuaModelRef, type CuaRuntimeSpec, type CuaSimpleStreamOptions, getCuaEnvApiKey, type Model, + type Models, resolveCuaRuntimeSpec, type SimpleStreamOptions, - streamSimple, } from "@onkernel/cua-ai"; import type Kernel from "@onkernel/sdk"; import { buildCuaComputerTools } from "./tools"; import { InternalComputerTranslator, type KernelBrowser } from "./translator/translator"; -/** A CUA model reference string or a concrete pi model object. */ +/** + * A model selection: a CUA model ref like `"openai:gpt-5.5"` or a concrete + * pi model object. Selects *which* model runs; *how* requests stream and + * authenticate is the `models` collection's concern. + */ type CuaRuntimeInput = CuaModelRef | Model; /** @@ -81,13 +86,22 @@ export type CuaAgentOptions = Omit & { export type CuaAgentHarnessOptions< TSkill extends Skill = Skill, TPromptTemplate extends PromptTemplate = PromptTemplate, -> = Omit, "model" | "tools"> & { +> = Omit, "model" | "tools" | "models"> & { /** Kernel browser session used by default CUA tools. */ browser: KernelBrowser; /** Kernel SDK client used by default CUA tools. */ client: Kernel; - /** Model used by the harness. CUA refs are resolved before pi sees the model. */ + /** + * The model the harness starts with (switch later with `setModel()`). + * CUA refs are resolved before pi sees the model. + */ model: CuaRuntimeInput; + /** + * pi `Models` provider collection requests stream through — providers, + * auth, and stream dispatch, mirroring pi's `AgentHarnessOptions.models`. + * Defaults to {@link cuaModels}. Not the model selection; that is `model`. + */ + models?: Models; /** Add your own pi tools alongside the built-in browser tools. */ extraTools?: AgentTool[]; /** Expose a helper for browser navigation and URL reads. */ @@ -180,11 +194,8 @@ class CuaRuntimeController { } } -/** Harness auth default following the documented CUA env-var convention. */ -async function getCuaEnvApiKeyAndHeaders(model: Model): Promise<{ apiKey: string } | undefined> { - const apiKey = getCuaEnvApiKey(model.provider); - return apiKey ? { apiKey } : undefined; -} +/** Default stream path: the shared CUA `Models` collection. */ +const defaultCuaStream: StreamFn = (model, context, options) => cuaModels().streamSimple(model, context, options); /** * Pi `Agent` configured for Kernel browser computer use. @@ -229,7 +240,7 @@ export class CuaAgent extends Agent { onPayload: runtime.onPayload(), keepToolNames: runtime.keepToolNames(), }; - return (streamFn ?? streamSimple)(model, context, optionsWithCuaRuntime); + return (streamFn ?? defaultCuaStream)(model, context, optionsWithCuaRuntime); }; super({ @@ -334,11 +345,11 @@ export class CuaAgentHarness< browser, client, model, + models, extraTools, computerUseExtra, playwright, systemPrompt, - getApiKeyAndHeaders, onPayload, activeToolNames, ...harnessOptions @@ -357,9 +368,9 @@ export class CuaAgentHarness< super({ ...harnessOptions, model: runtime.model, + models: models ?? cuaModels(), tools: resolvedTools, systemPrompt: systemPrompt ?? (() => runtime.systemPrompt), - getApiKeyAndHeaders: getApiKeyAndHeaders ?? getCuaEnvApiKeyAndHeaders, activeToolNames: activeToolNames ?? resolvedTools.map((tool) => tool.name), }); diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index be937623..7f0dd654 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## 0.4.0 - 2026-07-07 + +Breaking: adopts pi-ai 0.80's instance-based `Models` API and drops the +global api-registry surface. + +- Requests now stream through a pi `Models` collection. New exports: + `createCuaModels(options?)` builds a collection with pi's builtin providers + plus CUA's adjustments (OpenAI routed through `openai-cua-responses`, + Google accepting `GOOGLE_API_KEY` or `GEMINI_API_KEY`, Tzafon and Yutori + registered); `cuaModels()` returns the shared default collection. +- Removed `registerCuaProviders()` and the import-time registration side + effect. Build a collection with `createCuaModels()` instead; nothing global + is mutated. +- The pi-ai re-export now follows pi-ai 0.80: the free functions `complete`, + `stream`, `completeSimple`, `streamSimple`, `getModel`, `getModels`, and + the api-registry (`registerApiProvider`, `getApiProvider`, + `resetApiProviders`, …) are gone. Call the equivalent methods on + `cuaModels()`. +- API keys resolve from the documented env-var convention through provider + auth when streaming via the collection; explicit `apiKey` stream options + still take precedence. +- Removed the `claude-sonnet-5` and `gpt-5.5` model overrides (pi-ai 0.80's + registry carries both) and the `gpt-5.5-2026-04-23` dated-snapshot + override. Dated snapshot refs no longer resolve — use the family id + (`openai:gpt-5.5`). +- Updated `@earendil-works/pi-ai` to 0.80.3. + ## 0.3.4 - 2026-06-30 - Adapt newer Anthropic models to the adaptive thinking payload format, including `claude-sonnet-5`, `claude-opus-4-8`, and `claude-opus-4-7`. diff --git a/packages/ai/README.md b/packages/ai/README.md index 4e7c2c43..97ab4b14 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -36,16 +36,14 @@ The exported helpers wrap this table: Pass the resolved key as the `apiKey` stream option (as in the Quick Start below) so a missing key fails loudly before any request is made. If you omit -`apiKey`, pi-ai's built-in providers fall back to their own env lookup -(`OPENAI_API_KEY`; `ANTHROPIC_OAUTH_TOKEN`/`ANTHROPIC_API_KEY`; for `google` -only `GEMINI_API_KEY`, not `GOOGLE_API_KEY`), and this package's Tzafon/Yutori -stream adapters read `TZAFON_API_KEY`/`YUTORI_API_KEY`. +`apiKey`, the `cuaModels()` collection resolves auth per provider from the +same table above. ## Quick Start ```ts import { readFile } from "node:fs/promises"; -import { complete, getCuaModel, openai, requireCuaEnvApiKeyForModel } from "@onkernel/cua-ai"; +import { cuaModels, getCuaModel, openai, requireCuaEnvApiKeyForModel } from "@onkernel/cua-ai"; const model = getCuaModel("openai:gpt-5.5"); const apiKey = requireCuaEnvApiKeyForModel("openai:gpt-5.5"); // throws unless OPENAI_API_KEY is set @@ -54,7 +52,11 @@ const apiKey = requireCuaEnvApiKeyForModel("openai:gpt-5.5"); // throws unless O // module so the snippet does not depend on the process working directory. const screenshot = await readFile(new URL("./screenshot.png", import.meta.url)); -const response = await complete( +// The default CUA model collection: pi's builtin providers plus Kernel's +// computer-use providers. Use createCuaModels() for an isolated collection. +const models = cuaModels(); + +const response = await models.complete( model, { systemPrompt: "You are a browser automation agent.", @@ -91,8 +93,8 @@ e.g. `CUA_MODEL=anthropic:claude-opus-4-7`. ## Error Handling -pi-ai's `complete()` and `stream()` **resolve instead of throwing** when a -request fails. The returned `AssistantMessage` carries the outcome on +pi-ai's `complete()` and `stream()` methods **resolve instead of throwing** +when a request fails. The returned `AssistantMessage` carries the outcome on `stopReason`: - `"stop"`, `"length"`, `"toolUse"` — success; `content` holds the response. @@ -139,8 +141,9 @@ type ToolResultMessage = { A minimal two-turn loop: ```ts -import { complete, getCuaModel, openai, requireCuaEnvApiKeyForModel, type Message } from "@onkernel/cua-ai"; +import { cuaModels, getCuaModel, openai, requireCuaEnvApiKeyForModel, type Message } from "@onkernel/cua-ai"; +const models = cuaModels(); const model = getCuaModel("openai:gpt-5.5"); const apiKey = requireCuaEnvApiKeyForModel("openai:gpt-5.5"); const tools = openai.computerTools({ actions: ["click", "type", "screenshot"] }); @@ -157,7 +160,7 @@ const messages: Message[] = [ ]; // Turn 1: the model responds with tool calls. -const first = await complete(model, { messages, tools }, { apiKey }); +const first = await models.complete(model, { messages, tools }, { apiKey }); if (first.stopReason === "error" || first.stopReason === "aborted") { throw new Error(first.errorMessage); } @@ -182,7 +185,7 @@ for (const block of first.content) { } // Turn 2: the model sees the results and plans the next action. -const second = await complete(model, { messages, tools }, { apiKey }); +const second = await models.complete(model, { messages, tools }, { apiKey }); ``` ## Core Concepts @@ -190,10 +193,13 @@ const second = await complete(model, { messages, tools }, { apiKey }); `@onkernel/cua-ai` re-exports the full surface of [`@earendil-works/pi-ai`](https://github.com/earendil-works/pi/tree/main/packages/ai) (`export * from "@earendil-works/pi-ai"`), including the core primitives: -`Model`, `Context`, `Message`, `Tool`, `complete`, `stream`, `completeSimple`, -`streamSimple`, `Type`, `Static`, `TSchema`, and the event/validation helpers. -Some familiarity with pi-ai is assumed; Kernel adds the computer-use model -catalog and provider/tool metadata. +`Model`, `Context`, `Message`, `Tool`, `Models`, `createModels`, +`createProvider`, `Type`, `Static`, `TSchema`, and the event/validation +helpers. Requests stream through a pi `Models` collection — use +`cuaModels()` (or build your own with `createCuaModels()`) and call +`complete`/`stream`/`completeSimple`/`streamSimple` on it. Some familiarity +with pi-ai is assumed; Kernel adds the computer-use model catalog and +provider/tool metadata. ### Model Refs @@ -208,9 +214,9 @@ getCuaModel("tzafon:tzafon.northstar-cua-fast"); getCuaModel("yutori:n1.5-latest"); ``` -`getCuaModel(ref)` returns a pi-ai `Model` you can pass to `complete()` -or `stream()`. It throws when the ref names a model without a CUA-support -annotation. +`getCuaModel(ref)` returns a pi-ai `Model` you can pass to +`cuaModels().complete()` or `cuaModels().stream()`. It throws when the ref +names a model without a CUA-support annotation. See [`docs/supported-models.md`](./docs/supported-models.md) for the current list of CUA-supporting models per provider. @@ -320,17 +326,24 @@ definitions and executors; it is forwarded to the provider module's `CuaNavigationInput`, `CuaPlaywrightInput`, `CuaToolExecutorSpec`, `ComputerToolsOptions`, `ComputerToolCoordinateSystem` -### Provider registration +### Model collections -- `registerCuaProviders(): void` — re-register the Yutori/Tzafon stream - providers with pi-ai's global registry (runs automatically on import; - idempotent; call it after any pi-ai registry mutator) +- `createCuaModels(options?): MutableModels` — build a pi `Models` collection + with pi's builtin providers plus CUA's adjustments: OpenAI models route + through cua's `openai-cua-responses` stream (threads + `previous_response_id`), Google resolves `GOOGLE_API_KEY` or + `GEMINI_API_KEY`, and the Tzafon/Yutori providers are registered. Each call + returns an independent collection. +- `cuaModels(): MutableModels` — the shared default collection, created on + first use. `CuaAgent` and `CuaAgentHarness` stream through it unless given + another one. ## Provider Tools Provider namespaces expose `computerTools({ actions? })` for building the provider's default CUA `Tool[]` definitions. These are the tools -sent to the model when you call `complete()` or `stream()` directly. The +sent to the model when you call `cuaModels().complete()` or +`cuaModels().stream()` directly. The default set can differ by provider: Anthropic includes its `computer_batch` tool from the computer-use best-practices reference, while providers such as OpenAI currently expose individual canonical browser actions. Omit `actions` diff --git a/packages/ai/examples/quickstart.ts b/packages/ai/examples/quickstart.ts index 5c0e64fd..cf9256e0 100644 --- a/packages/ai/examples/quickstart.ts +++ b/packages/ai/examples/quickstart.ts @@ -1,6 +1,6 @@ import { readFile } from "node:fs/promises"; import { - complete, + cuaModels, getCuaModel, requireCuaEnvApiKeyForModel, resolveCuaRuntimeSpec, @@ -23,7 +23,13 @@ const spec = resolveCuaRuntimeSpec(modelRef, { actions: ["click"] }); const screenshot = await readFile(new URL("./screenshot.png", import.meta.url)); -const response = await complete( +// The default CUA model collection: pi's builtin providers plus Kernel's +// computer-use providers, with API keys resolved from the env vars above. +// Use createCuaModels() instead for an isolated collection (e.g. with a +// custom credential store). +const models = cuaModels(); + +const response = await models.complete( model, { systemPrompt: [ diff --git a/packages/ai/package.json b/packages/ai/package.json index f93c1dd6..2e5bc4d6 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@onkernel/cua-ai", - "version": "0.3.4", + "version": "0.4.0", "description": "Kernel-curated computer-use model access built on pi-ai", "license": "MIT", "type": "module", @@ -41,7 +41,7 @@ "test:integration": "vitest --run --config vitest.integration.config.ts" }, "dependencies": { - "@earendil-works/pi-ai": "0.79.1", + "@earendil-works/pi-ai": "0.80.3", "@tzafon/lightcone": "^0.7.0", "openai": "^6.26.0" }, diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 4bed13fb..e71e3e61 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -1,8 +1,6 @@ -import { registerCuaProviders } from "./providers"; - export * from "@earendil-works/pi-ai"; -export { registerCuaProviders } from "./providers"; +export { createCuaModels, cuaModels } from "./providers"; export * from "./models"; export * from "./api-keys"; export * from "./runtime-spec"; @@ -12,5 +10,3 @@ export * as gemini from "./providers/gemini/index"; export * as openai from "./providers/openai/index"; export * as tzafon from "./providers/tzafon/index"; export * as yutori from "./providers/yutori/index"; - -registerCuaProviders(); diff --git a/packages/ai/src/models.ts b/packages/ai/src/models.ts index 23899e68..5653e901 100644 --- a/packages/ai/src/models.ts +++ b/packages/ai/src/models.ts @@ -1,9 +1,5 @@ -import { - type Api, - type Model, - getModel, - getModels, -} from "@earendil-works/pi-ai"; +import type { Api, Model } from "@earendil-works/pi-ai"; +import { getBuiltinModel, getBuiltinModels } from "@earendil-works/pi-ai/providers/all"; import { OPENAI_CUA_RESPONSES_API } from "./providers/openai/provider"; /** Providers with curated computer-use model support. */ @@ -101,13 +97,8 @@ export const CUA_MODEL_ANNOTATIONS: Record[]> = { - openai: [ - cuaModel("openai", "gpt-5.5", "GPT-5.5"), - cuaModel("openai", "gpt-5.5-2026-04-23", "GPT-5.5 (2026-04-23)"), - ], - anthropic: [ - cuaModel("anthropic", "claude-sonnet-5", "Claude Sonnet 5"), - ], + openai: [], + anthropic: [], google: [], tzafon: [ cuaModel("tzafon", "tzafon.northstar-cua-fast", "Tzafon Northstar CUA Fast"), @@ -122,6 +113,11 @@ const CUA_MODEL_OVERRIDES: Record[]> = { ], }; +/** Models CUA supports that pi-ai's registry does not carry for a provider. */ +export function cuaOverrideModels(provider: CuaProvider): readonly Model[] { + return CUA_MODEL_OVERRIDES[provider]; +} + /** * Split a provider-qualified ref like `"openai:gpt-5.5"` into its parts. * @@ -162,7 +158,7 @@ export function listCuaModels(provider?: CuaProvider): CuaModelInfo[] { const ref = formatCuaModelRef(p, model.id); byRef.set(ref, { ref, provider: p, model: model.id, name: model.name }); } - for (const model of getModels(p as never) as Model[]) { + for (const model of getBuiltinModels(p as never) as Model[]) { if (!supportsCuaProvider(p, model.id)) continue; const ref = formatCuaModelRef(p, model.id); if (byRef.has(ref)) continue; @@ -190,7 +186,7 @@ export function getCuaModel(ref: CuaModelRef): Model { if (!supportsCuaProvider(provider, modelId)) { throw new Error(`unsupported CUA model "${ref}"`); } - const fromRegistry = getModel(provider as never, modelId as never) as Model | undefined; + const fromRegistry = getBuiltinModel(provider as never, modelId as never) as Model | undefined; if (fromRegistry) return routeCuaApi(fromRegistry); const override = CUA_MODEL_OVERRIDES[provider].find((m) => m.id === modelId); if (override) return routeCuaApi(override); diff --git a/packages/ai/src/providers.ts b/packages/ai/src/providers.ts index 75ed36b6..29492684 100644 --- a/packages/ai/src/providers.ts +++ b/packages/ai/src/providers.ts @@ -1,39 +1,102 @@ -import { registerApiProvider } from "@earendil-works/pi-ai"; +import { + createProvider, + envApiKeyAuth, + type Api, + type Context, + type CreateModelsOptions, + type Model, + type MutableModels, + type Provider, + type SimpleStreamOptions, + type StreamOptions, +} from "@earendil-works/pi-ai"; +import { builtinModels } from "@earendil-works/pi-ai/providers/all"; +import { cuaApiKeyEnvVarsForProvider } from "./api-keys"; +import { cuaOverrideModels } from "./models"; import { OPENAI_CUA_RESPONSES_API, streamOpenAIResponses, streamSimpleOpenAIResponses } from "./providers/openai/provider"; import { streamSimpleTzafonResponses, streamTzafonResponses, TZAFON_RESPONSES_API } from "./providers/tzafon/provider"; import { streamSimpleYutori, streamYutori, YUTORI_CHAT_COMPLETIONS_API } from "./providers/yutori/provider"; -// pi-ai eagerly registers openai-responses, anthropic-messages, and -// google-generative-ai when its index module loads (see -// node_modules/@earendil-works/pi-ai/dist/providers/register-builtins.js). -// CUA adds the providers pi-ai does not ship (Tzafon, Yutori) plus its own -// openai-cua-responses, which threads previous_response_id and is left -// alongside pi-ai's untouched openai-responses builtin. +/** + * Build the pi `Models` collection CUA streams through: pi's builtin + * providers, adjusted for CUA: + * + * - `openai` intercepts the `openai-cua-responses` api that + * {@link getCuaModel} routes OpenAI models to, threading + * `previous_response_id`; every other api falls through to pi's builtin + * provider. + * - `google` resolves its API key from `GOOGLE_API_KEY` or `GEMINI_API_KEY` + * (pi's builtin only reads `GEMINI_API_KEY`). + * - `tzafon` and `yutori` are CUA-only providers pi does not ship. + * + * Each call returns an independent collection; register additional providers + * or credentials on it freely. Use {@link cuaModels} for the shared default. + */ +export function createCuaModels(options?: CreateModelsOptions): MutableModels { + const models = builtinModels(options); + const openai = models.getProvider("openai"); + if (openai) models.setProvider(withOpenAICuaResponses(openai)); + const google = models.getProvider("google"); + if (google) models.setProvider(withGoogleEnvKeys(google)); + models.setProvider(tzafonProvider()); + models.setProvider(yutoriProvider()); + return models; +} + +let defaultCuaModels: MutableModels | undefined; /** - * Register the Yutori and Tzafon stream providers with pi-ai's global API - * registry. Importing `@onkernel/cua-ai` calls this automatically. + * Shared default {@link createCuaModels} collection, created on first use. * - * The pi-ai registry mutators this package re-exports (`clearApiProviders`, - * `resetApiProviders`, `unregisterApiProviders`) deregister these providers, - * after which Yutori/Tzafon streaming fails until they are registered again. - * Call this to restore them; it is idempotent and safe to call repeatedly. + * `CuaAgent` and `CuaAgentHarness` stream through this instance unless given + * another one. Auth resolves from the documented CUA env-var convention (see + * `cuaApiKeyEnvVarsForProvider`); pass an explicit `options.apiKey` per + * request to override. */ -export function registerCuaProviders(): void { - registerApiProvider({ - api: YUTORI_CHAT_COMPLETIONS_API, - stream: streamYutori, - streamSimple: streamSimpleYutori, - }); - registerApiProvider({ - api: TZAFON_RESPONSES_API, - stream: streamTzafonResponses, - streamSimple: streamSimpleTzafonResponses, +export function cuaModels(): MutableModels { + return (defaultCuaModels ??= createCuaModels()); +} + +// pi's builtin openai provider only streams its own api ids. CUA routes +// OpenAI models to OPENAI_CUA_RESPONSES_API (see routeCuaApi), so the +// registered provider must dispatch that api to cua's threading stream fns. +function withOpenAICuaResponses(base: Provider): Provider { + return { + ...base, + stream: (model: Model, context: Context, options?: StreamOptions) => + model.api === OPENAI_CUA_RESPONSES_API + ? streamOpenAIResponses(model as never, context, options) + : base.stream(model, context, options), + streamSimple: (model: Model, context: Context, options?: SimpleStreamOptions) => + model.api === OPENAI_CUA_RESPONSES_API + ? streamSimpleOpenAIResponses(model as never, context, options) + : base.streamSimple(model, context, options), + }; +} + +function withGoogleEnvKeys(base: Provider): Provider { + return { ...base, auth: { ...base.auth, apiKey: envApiKeyAuth("Google API key", cuaApiKeyEnvVarsForProvider("google")) } }; +} + +function tzafonProvider(): Provider { + return createProvider({ + id: "tzafon", + name: "Tzafon", + baseUrl: "https://api.lightcone.ai", + auth: { apiKey: envApiKeyAuth("Tzafon API key", cuaApiKeyEnvVarsForProvider("tzafon")) }, + models: cuaOverrideModels("tzafon"), + api: { stream: streamTzafonResponses, streamSimple: streamSimpleTzafonResponses }, }); - registerApiProvider({ - api: OPENAI_CUA_RESPONSES_API, - stream: streamOpenAIResponses, - streamSimple: streamSimpleOpenAIResponses, +} + +function yutoriProvider(): Provider { + return createProvider({ + id: "yutori", + name: "Yutori", + baseUrl: "https://api.yutori.com/v1", + auth: { apiKey: envApiKeyAuth("Yutori API key", cuaApiKeyEnvVarsForProvider("yutori")) }, + models: cuaOverrideModels("yutori"), + api: { stream: streamYutori, streamSimple: streamSimpleYutori }, }); } diff --git a/packages/ai/src/providers/openai/provider.ts b/packages/ai/src/providers/openai/provider.ts index bfb31c21..cb8bc75d 100644 --- a/packages/ai/src/providers/openai/provider.ts +++ b/packages/ai/src/providers/openai/provider.ts @@ -1,12 +1,14 @@ import { - streamOpenAIResponses as piStreamOpenAIResponses, - streamSimpleOpenAIResponses as piStreamSimpleOpenAIResponses, type Context, type OpenAIResponsesOptions as PiOpenAIResponsesOptions, type SimpleStreamOptions, type StreamFunction, type StreamOptions, } from "@earendil-works/pi-ai"; +import { + stream as piStreamOpenAIResponses, + streamSimple as piStreamSimpleOpenAIResponses, +} from "@earendil-works/pi-ai/api/openai-responses"; import { responseThreadingDelta, responseThreadingEnabled, type ResponseThreadingOptions } from "../common"; export const OPENAI_CUA_RESPONSES_API = "openai-cua-responses"; diff --git a/packages/ai/test/computer-tool.integration.test.ts b/packages/ai/test/computer-tool.integration.test.ts index 7eb53588..3aa06717 100644 --- a/packages/ai/test/computer-tool.integration.test.ts +++ b/packages/ai/test/computer-tool.integration.test.ts @@ -8,7 +8,7 @@ import { type CuaActionType, type CuaProvider, anthropic, - complete, + cuaModels, gemini, getCuaModel, openai, @@ -114,7 +114,7 @@ describe("individual computer action integration", () => { (ciEnabled ? test : it.skip)(`${c.provider} returns a canonical click tool call`, async () => { const model = getCuaModel(c.modelRef as never); const context = await buildContext(c.tools); - const response = await complete(model, context, { + const response = await cuaModels().complete(model, context, { apiKey: process.env[c.envVar], maxTokens: 1024, ...c.extraOptions, @@ -148,7 +148,7 @@ describe("individual computer action integration", () => { async () => { const model = getCuaModel("yutori:n1.5-latest"); const context = await buildYutoriContext(); - const response = await complete(model, context, { + const response = await cuaModels().complete(model, context, { apiKey: process.env.YUTORI_API_KEY, maxTokens: 1024, }); diff --git a/packages/ai/test/models.test.ts b/packages/ai/test/models.test.ts index cc3b6829..f383547a 100644 --- a/packages/ai/test/models.test.ts +++ b/packages/ai/test/models.test.ts @@ -58,17 +58,16 @@ describe("CUA model refs", () => { }); it("routes OpenAI CUA models to the threading-aware openai-cua-responses api", () => { - // gpt-5.5 resolves from pi-ai's registry (carrying its builtin - // "openai-responses" api) while gpt-5.5-2026-04-23 is a local override; - // both must be routed to cua's own previous_response_id-threading provider. + // Registry models carry pi-ai's builtin "openai-responses" api and must + // be routed to cua's own previous_response_id-threading provider. expect(getCuaModel("openai:gpt-5.5").api).toBe(openai.OPENAI_CUA_RESPONSES_API); - expect(getCuaModel("openai:gpt-5.5-2026-04-23").api).toBe(openai.OPENAI_CUA_RESPONSES_API); expect(getCuaModel("openai:gpt-5.4-mini").api).toBe(openai.OPENAI_CUA_RESPONSES_API); }); it("rejects supported model IDs that are not in pi-ai or overrides", () => { - // Matches the openai allowlist but has no pi-ai or override entry. - expect(() => getCuaModel("openai:gpt-5.4-2099-01-01")).toThrow( + // Dated snapshots match the family annotation but pi-ai's registry + // (generated from models.dev) only carries family roots. + expect(() => getCuaModel("openai:gpt-5.5-2026-04-23")).toThrow( /not registered/, ); }); diff --git a/packages/ai/test/providers.test.ts b/packages/ai/test/providers.test.ts index 7804afff..c2b89737 100644 --- a/packages/ai/test/providers.test.ts +++ b/packages/ai/test/providers.test.ts @@ -1,43 +1,52 @@ import { describe, expect, it } from "vitest"; -import { getApiProvider, registerCuaProviders, resetApiProviders, tzafon, yutori } from "../src/index"; +import { createCuaModels, cuaModels, tzafon, yutori } from "../src/index"; +import { OPENAI_CUA_RESPONSES_API } from "../src/providers/openai/provider"; const TZAFON_RESPONSES_API = tzafon.TZAFON_RESPONSES_API; const YUTORI_CHAT_COMPLETIONS_API = yutori.YUTORI_CHAT_COMPLETIONS_API; -describe("CUA provider registration", () => { - it("registers Tzafon and Yutori APIs that pi-ai does not ship", () => { - expect(getApiProvider(YUTORI_CHAT_COMPLETIONS_API)).toBeDefined(); - expect(getApiProvider(TZAFON_RESPONSES_API)).toBeDefined(); +describe("createCuaModels", () => { + it("registers the CUA-only providers alongside pi's builtins", () => { + const models = createCuaModels(); + for (const id of ["openai", "anthropic", "google", "tzafon", "yutori"]) { + const provider = models.getProvider(id); + expect(provider, id).toBeDefined(); + expect(provider?.stream).toBeTypeOf("function"); + expect(provider?.streamSimple).toBeTypeOf("function"); + } }); - it("leaves pi-ai built-ins registered for the providers CUA targets", () => { - expect(getApiProvider("openai-responses")).toBeDefined(); - expect(getApiProvider("anthropic-messages")).toBeDefined(); - expect(getApiProvider("google-generative-ai")).toBeDefined(); + it("lists the tzafon and yutori catalogs on their providers", () => { + const models = createCuaModels(); + const tzafonIds = models.getModels("tzafon").map((m) => m.id); + expect(tzafonIds).toContain("tzafon.northstar-cua-fast"); + const yutoriIds = models.getModels("yutori").map((m) => m.id); + expect(yutoriIds).toContain("n1.5-latest"); + expect(models.getModel("tzafon", "tzafon.northstar-cua-fast")?.api).toBe(TZAFON_RESPONSES_API); + expect(models.getModel("yutori", "n1.5-latest")?.api).toBe(YUTORI_CHAT_COMPLETIONS_API); }); - it("exposes a stream function on every CUA-target API", () => { - for (const api of [ - "openai-responses", - "anthropic-messages", - "google-generative-ai", - YUTORI_CHAT_COMPLETIONS_API, - TZAFON_RESPONSES_API, - ] as const) { - const provider = getApiProvider(api); - expect(provider?.stream).toBeTypeOf("function"); - expect(provider?.streamSimple).toBeTypeOf("function"); - } + it("keeps pi's builtin openai catalog on the wrapped openai provider", () => { + const models = createCuaModels(); + const openaiIds = models.getModels("openai").map((m) => m.id); + expect(openaiIds).toContain("gpt-5.4"); + // The catalog keeps pi's api ids; getCuaModel() routes to + // openai-cua-responses, which the wrapped provider dispatches. + expect(models.getModel("openai", "gpt-5.4")?.api).not.toBe(OPENAI_CUA_RESPONSES_API); }); - it("restores CUA providers after pi-ai registry mutators clobber them", () => { - resetApiProviders(); - expect(getApiProvider(YUTORI_CHAT_COMPLETIONS_API)).toBeUndefined(); - expect(getApiProvider(TZAFON_RESPONSES_API)).toBeUndefined(); + it("returns independent collections", () => { + const a = createCuaModels(); + const b = createCuaModels(); + a.deleteProvider("tzafon"); + expect(a.getProvider("tzafon")).toBeUndefined(); + expect(b.getProvider("tzafon")).toBeDefined(); + }); +}); - registerCuaProviders(); - registerCuaProviders(); - expect(getApiProvider(YUTORI_CHAT_COMPLETIONS_API)).toBeDefined(); - expect(getApiProvider(TZAFON_RESPONSES_API)).toBeDefined(); +describe("cuaModels", () => { + it("memoizes the default collection", () => { + expect(cuaModels()).toBe(cuaModels()); + expect(cuaModels().getProvider("yutori")).toBeDefined(); }); }); diff --git a/packages/cli/package.json b/packages/cli/package.json index 814cf959..ae251bb8 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@onkernel/cua-cli", - "version": "0.1.4", + "version": "0.2.0", "description": "Kernel-cloud-browser computer-use TUI built on @onkernel/cua-agent and pi-tui", "license": "MIT", "type": "module", @@ -34,10 +34,10 @@ "typecheck": "tsc -b" }, "dependencies": { - "@earendil-works/pi-coding-agent": "0.79.1", - "@earendil-works/pi-tui": "0.79.1", - "@onkernel/cua-agent": "0.3.5", - "@onkernel/cua-ai": "0.3.2", + "@earendil-works/pi-coding-agent": "0.80.3", + "@earendil-works/pi-tui": "0.80.3", + "@onkernel/cua-agent": "0.4.0", + "@onkernel/cua-ai": "0.4.0", "@onkernel/sdk": "0.49.0" }, "devDependencies": { diff --git a/packages/cli/src/harness.ts b/packages/cli/src/harness.ts index a7488115..9ee9e4bd 100644 --- a/packages/cli/src/harness.ts +++ b/packages/cli/src/harness.ts @@ -12,7 +12,7 @@ import { type Api, type CuaModelRef, type Model, - getCuaEnvApiKey, + type Models, getCuaModel, resolveCuaRuntimeSpec, } from "@onkernel/cua-ai"; @@ -35,8 +35,8 @@ export interface BuildCuaHarnessOptions { playwright?: boolean; /** Override the default coding-tools extraTools (bash/read/edit/write/grep/find/ls). */ extraTools?: CuaAgentHarnessOptions["extraTools"]; - /** Override env-var API-key resolution (mainly for tests). */ - getApiKeyAndHeaders?: CuaAgentHarnessOptions["getApiKeyAndHeaders"]; + /** Override the pi `Models` collection requests stream through (mainly for tests). */ + models?: Models; /** Override the catalog `baseUrl` on the resolved model (e.g. from `_BASE_URL`). */ modelBaseUrl?: string; } @@ -44,9 +44,9 @@ export interface BuildCuaHarnessOptions { /** * Build a `CuaAgentHarness` wired with cua-cli's defaults: pi `NodeExecutionEnv`, * caller-supplied jsonl `Session`, pi-coding-agent's `createCodingTools` as - * `extraTools`, env-var API-key resolution (via cua-ai conventions), and a - * `systemPrompt` that composes the runtime spec's default prompt with the - * formatted skill block. + * `extraTools`, the shared CUA `Models` collection (env-var API-key + * resolution via cua-ai conventions), and a `systemPrompt` that composes the + * runtime spec's default prompt with the formatted skill block. */ export function buildCuaHarness(opts: BuildCuaHarnessOptions): CuaAgentHarness { const skills = opts.skills ?? []; @@ -69,12 +69,7 @@ export function buildCuaHarness(opts: BuildCuaHarnessOptions): CuaAgentHarness { const runtime = resolveCuaRuntimeSpec(activeModel); return composeSystemPrompt(runtime.defaultSystemPrompt, resources.skills ?? [], contextFiles); }, - getApiKeyAndHeaders: - opts.getApiKeyAndHeaders ?? - (async (resolvedModel) => { - const apiKey = getCuaEnvApiKey(resolvedModel.provider); - return apiKey ? { apiKey } : undefined; - }), + models: opts.models, }); } diff --git a/packages/cli/test/action-runner.test.ts b/packages/cli/test/action-runner.test.ts index 0d521bbc..4ab513d8 100644 --- a/packages/cli/test/action-runner.test.ts +++ b/packages/cli/test/action-runner.test.ts @@ -1,14 +1,9 @@ -import { afterEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { runAction } from "../src/action/harness-runner"; import { buildTestHarness, type TestHarnessFixture } from "./fixtures/harness"; let fixture: TestHarnessFixture | undefined; -afterEach(async () => { - await fixture?.dispose(); - fixture = undefined; -}); - describe("action harness-runner", () => { it("exits 0 with formatted result when a click action succeeds", async () => { fixture = await buildTestHarness({ diff --git a/packages/cli/test/fixtures/harness.ts b/packages/cli/test/fixtures/harness.ts index a32b7583..83c0ef23 100644 --- a/packages/cli/test/fixtures/harness.ts +++ b/packages/cli/test/fixtures/harness.ts @@ -6,10 +6,11 @@ import { import { tmpdir } from "node:os"; import { mkdtempSync } from "node:fs"; import { join } from "node:path"; +import { parseCuaModelRef } from "@onkernel/cua-ai"; import { buildCuaHarness } from "../../src/harness"; import { createFakeKernelEnvironment, type FakeKernelEnvironment } from "./fake-kernel"; import type { ScriptedProviderHandle, ScriptedTurn } from "./scripted-provider"; -import { registerScriptedProvider } from "./scripted-provider"; +import { createScriptedCuaModels } from "./scripted-provider"; export interface TestHarnessFixture { provider: ScriptedProviderHandle; @@ -17,7 +18,6 @@ export interface TestHarnessFixture { session: Session; cwd: string; harness: ReturnType; - dispose(): Promise; } export interface BuildTestHarnessOptions { @@ -25,20 +25,11 @@ export interface BuildTestHarnessOptions { skills?: Skill[]; /** CUA model ref. Defaults to "openai:gpt-5.5". */ modelRef?: string; - /** API id the scripted provider serves. Default infers from modelRef. */ - api?: string; } -const DEFAULT_API_FOR_MODEL: Record = { - "openai:gpt-5.5": "openai-cua-responses", - "anthropic:claude-opus-4-7": "anthropic-messages", - "google:gemini-3-flash-preview": "google-generative-ai", -}; - export async function buildTestHarness(opts: BuildTestHarnessOptions): Promise { const modelRef = opts.modelRef ?? "openai:gpt-5.5"; - const api = opts.api ?? DEFAULT_API_FOR_MODEL[modelRef] ?? "openai-responses"; - const provider = registerScriptedProvider(api, opts.turns); + const provider = createScriptedCuaModels(parseCuaModelRef(modelRef).provider, opts.turns); const kernel = createFakeKernelEnvironment(); const cwd = mkdtempSync(join(tmpdir(), "cua-cli-test-")); @@ -54,7 +45,7 @@ export async function buildTestHarness(opts: BuildTestHarnessOptions): Promise ({ apiKey: "test-key" }), + models: provider.models, }); return { @@ -63,8 +54,5 @@ export async function buildTestHarness(opts: BuildTestHarnessOptions): Promise { - provider.dispose(); - }, }; } diff --git a/packages/cli/test/fixtures/scripted-provider.ts b/packages/cli/test/fixtures/scripted-provider.ts index f8def664..1fa8da41 100644 --- a/packages/cli/test/fixtures/scripted-provider.ts +++ b/packages/cli/test/fixtures/scripted-provider.ts @@ -3,10 +3,9 @@ import { type AssistantMessage, type Context, createAssistantMessageEventStream, + createCuaModels, type Model, - registerApiProvider, - type SimpleStreamOptions, - unregisterApiProviders, + type MutableModels, } from "@onkernel/cua-ai"; /** One scripted step replayed when the harness asks the provider for a turn. */ @@ -26,48 +25,44 @@ export interface ScriptedTurn { } export interface ScriptedProviderHandle { + /** pi `Models` collection whose named provider replays the scripted turns. */ + models: MutableModels; /** Reset the turn cursor; the next provider call replays the first turn. */ reset(): void; /** Number of provider calls dispatched so far. */ callCount(): number; /** Latest context the provider was called with (assistant-side mock). */ lastContext(): Context | undefined; - /** Remove the registered provider. Safe to call from `afterEach`. */ - dispose(): void; } -const sourceCounter = { value: 0 }; - /** - * Register a scripted provider on the pi-ai api registry. The provider - * replays one `ScriptedTurn` per provider call against the supplied API - * id; the harness drives this exactly like a real provider. + * Build a CUA `Models` collection whose named provider is replaced with a + * scripted double that replays one `ScriptedTurn` per provider call, + * regardless of the model's api id. Pass `handle.models` to + * `buildCuaHarness`; nothing global is mutated. */ -export function registerScriptedProvider(api: Api, turns: ScriptedTurn[]): ScriptedProviderHandle { - const sourceId = `cua-cli-test-${++sourceCounter.value}`; +export function createScriptedCuaModels(providerId: string, turns: ScriptedTurn[]): ScriptedProviderHandle { const state = { index: 0, lastContext: undefined as Context | undefined, }; - registerApiProvider( - { - api, - streamSimple: (model, context, options?: SimpleStreamOptions) => { - state.lastContext = context; - const turn = turns[state.index]; - state.index += 1; - return buildStream(model, turn, options?.signal); - }, - stream: (model, context, options) => { - state.lastContext = context; - const turn = turns[state.index]; - state.index += 1; - return buildStream(model, turn, options?.signal); - }, - }, - sourceId, - ); + const dispatch = (model: Model, context: Context, signal?: AbortSignal) => { + state.lastContext = context; + const turn = turns[state.index]; + state.index += 1; + return buildStream(model, turn, signal); + }; + const models = createCuaModels(); + models.setProvider({ + id: providerId, + name: `Scripted ${providerId}`, + auth: { apiKey: { name: "scripted test key", resolve: async () => ({ auth: { apiKey: "test-key" } }) } }, + getModels: () => [], + stream: (model, context, options) => dispatch(model, context, options?.signal), + streamSimple: (model, context, options) => dispatch(model, context, options?.signal), + }); return { + models, reset(): void { state.index = 0; }, @@ -77,9 +72,6 @@ export function registerScriptedProvider(api: Api, turns: ScriptedTurn[]): Scrip lastContext(): Context | undefined { return state.lastContext; }, - dispose(): void { - unregisterApiProviders(sourceId); - }, }; } diff --git a/packages/cli/test/fixtures/tui-fixture-runner.ts b/packages/cli/test/fixtures/tui-fixture-runner.ts index f0f6bacf..fa33f705 100644 --- a/packages/cli/test/fixtures/tui-fixture-runner.ts +++ b/packages/cli/test/fixtures/tui-fixture-runner.ts @@ -6,29 +6,22 @@ * the interactive TUI. */ import { InMemorySessionRepo, type Skill } from "@onkernel/cua-agent"; -import type { CuaModelRef } from "@onkernel/cua-ai"; +import { parseCuaModelRef, type CuaModelRef } from "@onkernel/cua-ai"; import { readFile } from "node:fs/promises"; import { resolve } from "node:path"; import { buildCuaHarness } from "../../src/harness"; import type { ContextFile } from "../../src/harness-skills"; import { runInteractive } from "../../src/tui/main"; import { createFakeKernelEnvironment } from "./fake-kernel"; -import { registerScriptedProvider, type ScriptedTurn } from "./scripted-provider"; +import { createScriptedCuaModels, type ScriptedTurn } from "./scripted-provider"; interface TuiFixture { modelRef?: string; - api?: string; turns: ScriptedTurn[]; skills?: Skill[]; contextFiles?: ContextFile[]; } -const DEFAULT_API_FOR_MODEL: Record = { - "openai:gpt-5.5": "openai-cua-responses", - "anthropic:claude-opus-4-7": "anthropic-messages", - "google:gemini-3-flash-preview": "google-generative-ai", -}; - async function main(): Promise { const fixtureArg = process.argv[2]; if (!fixtureArg) { @@ -38,8 +31,7 @@ async function main(): Promise { const fixture = JSON.parse(await readFile(fixturePath, "utf8")) as TuiFixture; const modelRef = fixture.modelRef ?? "openai:gpt-5.5"; - const api = fixture.api ?? DEFAULT_API_FOR_MODEL[modelRef] ?? "openai-responses"; - registerScriptedProvider(api, fixture.turns); + const scripted = createScriptedCuaModels(parseCuaModelRef(modelRef).provider, fixture.turns); const kernel = createFakeKernelEnvironment(); const sessionRepo = new InMemorySessionRepo(); @@ -56,7 +48,7 @@ async function main(): Promise { skills, contextFiles, extraTools: [], - getApiKeyAndHeaders: async () => ({ apiKey: "fixture-key" }), + models: scripted.models, }); const code = await runInteractive({ diff --git a/packages/cli/test/fixtures/tui-fixtures/abort.json b/packages/cli/test/fixtures/tui-fixtures/abort.json index b122696f..5742ca56 100644 --- a/packages/cli/test/fixtures/tui-fixtures/abort.json +++ b/packages/cli/test/fixtures/tui-fixtures/abort.json @@ -1,16 +1,25 @@ { "modelRef": "openai:gpt-5.5", - "api": "openai-cua-responses", "turns": [ { "steps": [ - { "type": "text", "text": "working...", "chunkSize": 5, "chunkMs": 10 }, - { "type": "wait_abort" } + { + "type": "text", + "text": "working...", + "chunkSize": 5, + "chunkMs": 10 + }, + { + "type": "wait_abort" + } ] }, { "steps": [ - { "type": "text", "text": "fixture response" } + { + "type": "text", + "text": "fixture response" + } ] } ] diff --git a/packages/cli/test/fixtures/tui-fixtures/error.json b/packages/cli/test/fixtures/tui-fixtures/error.json index e849d55d..ad53ac9b 100644 --- a/packages/cli/test/fixtures/tui-fixtures/error.json +++ b/packages/cli/test/fixtures/tui-fixtures/error.json @@ -1,10 +1,12 @@ { "modelRef": "openai:gpt-5.5", - "api": "openai-cua-responses", "turns": [ { "steps": [ - { "type": "error", "message": "fixture provider failed" } + { + "type": "error", + "message": "fixture provider failed" + } ] } ] diff --git a/packages/cli/test/fixtures/tui-fixtures/multiline.json b/packages/cli/test/fixtures/tui-fixtures/multiline.json index aa70cb60..6254d72c 100644 --- a/packages/cli/test/fixtures/tui-fixtures/multiline.json +++ b/packages/cli/test/fixtures/tui-fixtures/multiline.json @@ -1,10 +1,12 @@ { "modelRef": "openai:gpt-5.5", - "api": "openai-cua-responses", "turns": [ { "steps": [ - { "type": "text", "text": "multiline ok" } + { + "type": "text", + "text": "multiline ok" + } ] } ] diff --git a/packages/cli/test/fixtures/tui-fixtures/resources.json b/packages/cli/test/fixtures/tui-fixtures/resources.json index 61a3a0a0..247484e6 100644 --- a/packages/cli/test/fixtures/tui-fixtures/resources.json +++ b/packages/cli/test/fixtures/tui-fixtures/resources.json @@ -1,6 +1,5 @@ { "modelRef": "openai:gpt-5.5", - "api": "openai-cua-responses", "skills": [ { "name": "deploy-skill", @@ -16,11 +15,19 @@ } ], "contextFiles": [ - { "path": "/tmp/project/AGENTS.md", "content": "Be concise." } + { + "path": "/tmp/project/AGENTS.md", + "content": "Be concise." + } ], "turns": [ { - "steps": [{ "type": "text", "text": "fixture response" }] + "steps": [ + { + "type": "text", + "text": "fixture response" + } + ] } ] } diff --git a/packages/cli/test/fixtures/tui-fixtures/streaming.json b/packages/cli/test/fixtures/tui-fixtures/streaming.json index 3f43ab95..f3496d03 100644 --- a/packages/cli/test/fixtures/tui-fixtures/streaming.json +++ b/packages/cli/test/fixtures/tui-fixtures/streaming.json @@ -1,10 +1,14 @@ { "modelRef": "openai:gpt-5.5", - "api": "openai-cua-responses", "turns": [ { "steps": [ - { "type": "text", "text": "fixture response", "chunkSize": 8, "chunkMs": 5 } + { + "type": "text", + "text": "fixture response", + "chunkSize": 8, + "chunkMs": 5 + } ] } ] diff --git a/packages/cli/test/harness-assembly.test.ts b/packages/cli/test/harness-assembly.test.ts index ef6b7c50..a1da9cea 100644 --- a/packages/cli/test/harness-assembly.test.ts +++ b/packages/cli/test/harness-assembly.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { formatSkillsForSystemPrompt, InMemorySessionRepo, @@ -11,17 +11,10 @@ import { join } from "node:path"; import { resolveCuaRuntimeSpec } from "@onkernel/cua-ai"; import { buildCuaHarness } from "../src/harness"; import { createFakeKernelEnvironment } from "./fixtures/fake-kernel"; -import { registerScriptedProvider, type ScriptedProviderHandle } from "./fixtures/scripted-provider"; - -let provider: ScriptedProviderHandle | undefined; - -afterEach(() => { - provider?.dispose(); - provider = undefined; -}); +import { createScriptedCuaModels } from "./fixtures/scripted-provider"; describe("buildCuaHarness", () => { - it("installs createCodingTools as extraTools by default (pi-coding-agent 0.79 type compatibility)", async () => { + it("installs createCodingTools as extraTools by default", async () => { const cwd = mkdtempSync(join(tmpdir(), "cua-cli-harness-")); const kernel = createFakeKernelEnvironment(); const session = await new InMemorySessionRepo().create(); @@ -40,7 +33,7 @@ describe("buildCuaHarness", () => { }); it("composes the cua-ai default system prompt with the skill block", async () => { - provider = registerScriptedProvider("openai-cua-responses", [ + const provider = createScriptedCuaModels("openai", [ { steps: [{ type: "text", text: "ok" }] }, ]); const cwd = mkdtempSync(join(tmpdir(), "cua-cli-harness-")); @@ -60,7 +53,7 @@ describe("buildCuaHarness", () => { model: "openai:gpt-5.5", skills: [skill], extraTools: [], - getApiKeyAndHeaders: async () => ({ apiKey: "test-key" }), + models: provider.models, }); let capturedSystemPrompt: string | undefined; harness.on("before_agent_start", (event) => { @@ -75,7 +68,7 @@ describe("buildCuaHarness", () => { }); it("injects loaded context files into the system prompt", async () => { - provider = registerScriptedProvider("openai-cua-responses", [ + const provider = createScriptedCuaModels("openai", [ { steps: [{ type: "text", text: "ok" }] }, ]); const cwd = mkdtempSync(join(tmpdir(), "cua-cli-harness-")); @@ -89,7 +82,7 @@ describe("buildCuaHarness", () => { model: "openai:gpt-5.5", contextFiles: [{ path: join(cwd, "AGENTS.md"), content: "Always prefer tabs over spaces." }], extraTools: [], - getApiKeyAndHeaders: async () => ({ apiKey: "test-key" }), + models: provider.models, }); let capturedSystemPrompt: string | undefined; harness.on("before_agent_start", (event) => { @@ -102,7 +95,7 @@ describe("buildCuaHarness", () => { }); it("delivers the first prompt with an image attached via harness.prompt({ images })", async () => { - provider = registerScriptedProvider("openai-cua-responses", [ + const provider = createScriptedCuaModels("openai", [ { steps: [{ type: "text", text: "done" }] }, ]); @@ -116,7 +109,7 @@ describe("buildCuaHarness", () => { session, model: "openai:gpt-5.5", extraTools: [], - getApiKeyAndHeaders: async () => ({ apiKey: "test-key" }), + models: provider.models, }); const tinyPngBase64 = diff --git a/packages/cli/test/print.test.ts b/packages/cli/test/print.test.ts index 8223f1c4..282488b6 100644 --- a/packages/cli/test/print.test.ts +++ b/packages/cli/test/print.test.ts @@ -1,14 +1,9 @@ -import { afterEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { runPrint } from "../src/print"; import { buildTestHarness, type TestHarnessFixture } from "./fixtures/harness"; let fixture: TestHarnessFixture | undefined; -afterEach(async () => { - await fixture?.dispose(); - fixture = undefined; -}); - describe("runPrint", () => { it("streams assistant text in plain text mode", async () => { fixture = await buildTestHarness({