diff --git a/docs/architecture.md b/docs/architecture.md index 5840bd8a..2d3ea280 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -127,17 +127,24 @@ The mode is set at construction (`mode` on `CuaAgent`/`CuaAgentHarness`, TUI's `/mode` command), which refreshes CUA-owned tools and the default system prompt. -**Native tools.** `resolveCuaRuntimeSpec(model, { nativeTool })` drives an -Anthropic model through its provider-defined tool schema instead of the -canonical function tools: `computer_20260701` pairs with `computer` mode and -`browser_20260701` with `browser` mode (mismatches throw, mirroring the API's -own rejection of mixed frames). The spec routes the model to a CUA-owned api -id; the registered `anthropic` provider dispatches it to pi's builtin -`anthropic-messages` transport with the tool's `anthropic-beta` header, an -`onPayload` hook swaps the placeholder tool for the native declaration, and -`providers/anthropic/native.ts` maps incoming `tool_use` inputs onto the same -canonical actions the mode uses — so canonical vs native is purely a wire -format difference over one execution path. +**Native tools.** `resolveCuaRuntimeSpec(model, { nativeTool })` can drive an +Anthropic model through an allowlisted, Anthropic-API-only early-access tool +schema instead of the canonical function tools: `computer_20260701` pairs with +`computer` mode and `browser_20260701` with `browser` mode. Model and mode +mismatches throw locally before a browser is provisioned. The live-verified +model families are `claude-fable-5`, `claude-opus-4-8`, and `claude-sonnet-5` +for `computer_20260701`; only `claude-opus-4-8` and `claude-sonnet-5` support +`browser_20260701`. The API key's organization must also have the matching +beta entitlement. + +The spec routes the model to a CUA-owned api id; the registered `anthropic` +provider dispatches it to pi's builtin `anthropic-messages` transport with the +tool-specific `anthropic-beta` header, an `onPayload` hook swaps the placeholder +tool for the native declaration, and `providers/anthropic/native.ts` maps +incoming `tool_use` inputs onto the same canonical actions the mode uses. The +runtime spec also carries the native tool's stop-on-first-failure result text, +which cua-agent applies without a provider conditional. Canonical vs native is +therefore a wire-format and turn-contract difference over one execution path. ## Layers @@ -353,7 +360,6 @@ flowchart LR | Feature | Status | Notes | | -------------------------------------------------- | -------- | ------------------------------------------------- | -| Anthropic `hold_key` / `zoom` | deferred | Translator returns errors so the model adapts | | `--local` Docker-backed browser | deferred | Remote Kernel cloud only | | pi-tui `SelectList`-based session picker for `-r` | deferred | Plain readline picker today | | Auto-compaction in the harness run loop | deferred | Manual `/compact` from the TUI; `shouldCompact` + `estimateContextTokens` are available from cua-agent re-exports for a future auto-trigger | diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 6229b498..82f4e55c 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +- Native Anthropic multi-action turns now stop after the first failed tool call. + Every remaining call in that assistant turn receives the provider-required + error result instead of executing against stale browser state. This applies + to both `CuaAgent` and `CuaAgentHarness` via provider-neutral runtime data. + ## 0.7.0 - 2026-07-17 - `CuaAgent` and `CuaAgentHarness` support Moonshot Kimi K3 diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index 7761da99..a3237aeb 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -255,6 +255,10 @@ class CuaRuntimeController { return this.runtimeSpec.mode; } + get stopOnFirstToolFailureMessage(): string | undefined { + return this.runtimeSpec.stopOnFirstToolFailureMessage; + } + setMode(mode: CuaMode): void { if (mode === this.runtimeSpec.mode) return; this.beginSwitch(this.resolveSpec(this.runtimeSpec.model, mode)); @@ -509,9 +513,15 @@ export class CuaAgent extends Agent { }; return retryingStream(model, context, optionsWithCuaRuntime); }; + const guardedToolHooks = stopToolTurnAfterFailure( + runtime.stopOnFirstToolFailureMessage, + agentOptions.beforeToolCall, + agentOptions.afterToolCall, + ); super({ ...agentOptions, + ...guardedToolHooks, getApiKey: agentOptions.getApiKey ?? getCuaEnvApiKey, streamFn: wrappedStreamFn, transformContext: async (messages, signal) => @@ -654,6 +664,8 @@ export class CuaAgentHarness< private requestedActiveToolNames?: string[]; private emptyResponseRecoveryAttempts = 0; private hasPendingActiveQueue = false; + private toolTurnFailed = false; + private removeToolFailureGuard?: () => void; constructor(options: CuaAgentHarnessOptions) { const { @@ -705,6 +717,20 @@ export class CuaAgentHarness< this.runtime = runtime; this.requestedActiveToolNames = activeToolNames; + if (runtime.stopOnFirstToolFailureMessage) { + this.installToolFailureGuard(runtime.stopOnFirstToolFailureMessage); + this.subscribe((event) => { + if (event.type === "message_end" && event.message.role === "assistant") { + this.toolTurnFailed = false; + // Harness hooks are last-result-wins. Reinsert the guard after caller + // hooks at the start of each assistant tool turn so it cannot be + // accidentally overridden once a prior action has failed. + this.installToolFailureGuard(runtime.stopOnFirstToolFailureMessage!); + } else if (event.type === "tool_execution_end" && event.isError) { + this.toolTurnFailed = true; + } + }); + } if (recovery && recovery.maxAttempts > 0) { this.on("before_agent_start", () => { this.emptyResponseRecoveryAttempts = 0; @@ -727,6 +753,13 @@ export class CuaAgentHarness< }); } + private installToolFailureGuard(message: string): void { + this.removeToolFailureGuard?.(); + this.removeToolFailureGuard = this.on("tool_call", () => + this.toolTurnFailed ? { block: true, reason: message } : undefined, + ); + } + private async recoverFromEmptyResponse( recovery: CuaEmptyResponseRecoveryOptions, signal?: AbortSignal, @@ -809,3 +842,36 @@ function composeOnPayload(first: AgentOptions["onPayload"], second: AgentOptions return second(afterFirst ?? payload, modelRef); }; } + +function stopToolTurnAfterFailure( + message: string | undefined, + before: AgentOptions["beforeToolCall"], + after: AgentOptions["afterToolCall"], +): Pick { + if (!message) return { beforeToolCall: before, afterToolCall: after }; + const failedTurns = new WeakSet(); + + return { + beforeToolCall: async (context, signal) => { + if (failedTurns.has(context.assistantMessage)) return { block: true, reason: message }; + try { + const result = await before?.(context, signal); + if (result?.block) failedTurns.add(context.assistantMessage); + return result; + } catch (error) { + failedTurns.add(context.assistantMessage); + throw error; + } + }, + afterToolCall: async (context, signal) => { + try { + const result = await after?.(context, signal); + if (result?.isError ?? context.isError) failedTurns.add(context.assistantMessage); + return result; + } catch (error) { + failedTurns.add(context.assistantMessage); + throw error; + } + }, + }; +} diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts index b1d8683c..99c53276 100644 --- a/packages/agent/test/agent.test.ts +++ b/packages/agent/test/agent.test.ts @@ -88,11 +88,11 @@ function createScriptedStream(texts: Array, contexts?: Array return { streamFn, calls: () => providerCalls }; } -function createModelsFromStream(streamFn: StreamFn) { +function createModelsFromStream(streamFn: StreamFn, provider = "openai") { const models = createCuaModels(); models.setProvider({ - id: "openai", - name: "scripted openai", + id: provider, + name: `scripted ${provider}`, auth: { apiKey: { name: "test key", @@ -259,7 +259,7 @@ describe("CuaAgent", () => { client, nativeTool: { type: "browser_20260701" }, initialState: { - model: "anthropic:claude-opus-4-5", + model: "anthropic:claude-opus-4-8", }, }); expect(agent.getMode()).toBe("browser"); @@ -511,6 +511,47 @@ describe("CuaAgent", () => { expect(fedBack!.content.some((block) => block.type === "image" && block.mimeType === "image/png")).toBe(true); }); + it("stops a native multi-action turn after its first failed action", async () => { + const contexts: Context[] = []; + let providerCalls = 0; + const streamFn: StreamFn = (model, context) => { + contexts.push({ ...context, messages: structuredClone(context.messages) }); + const stream = createAssistantMessageEventStream(); + const message = createAssistantMessage(model); + if (providerCalls++ === 0) { + message.content = [ + { type: "toolCall", id: "tool-1", name: "browser", arguments: { action: "warp" } }, + { type: "toolCall", id: "tool-2", name: "browser", arguments: { action: "navigate", url: "https://example.com" } }, + ]; + message.stopReason = "toolUse"; + } else { + message.content = [{ type: "text", text: "done" }]; + } + stream.push({ type: "start", partial: message }); + stream.push({ type: "done", reason: message.stopReason, message }); + stream.end(message); + return stream; + }; + const agent = new CuaAgent({ + browser, + client, + streamFn, + nativeTool: { type: "browser_20260701" }, + initialState: { model: "anthropic:claude-opus-4-8" }, + }); + + await agent.prompt("run two browser actions"); + + const results = contexts[1]!.messages.filter((message) => message.role === "toolResult"); + expect(results).toHaveLength(2); + expect(results[0]).toMatchObject({ toolCallId: "tool-1", isError: true }); + expect(results[1]).toMatchObject({ + toolCallId: "tool-2", + isError: true, + content: [{ type: "text", text: "Not executed: an earlier action in this turn failed." }], + }); + }); + it("applies screenshot projection after a caller context transform", async () => { const history: AgentMessage[] = []; for (let index = 1; index <= 5; index += 1) { @@ -1160,6 +1201,48 @@ describe("CuaAgentHarness", () => { expect(calls).toBe(2); }); + it("stops a native harness turn after its first failed action", async () => { + const contexts: Context[] = []; + let calls = 0; + const streamFn: StreamFn = (model, context) => { + contexts.push({ ...context, messages: structuredClone(context.messages) }); + const stream = createAssistantMessageEventStream(); + const message = createAssistantMessage(model); + if (calls++ === 0) { + message.content = [ + { type: "toolCall", id: "tool-1", name: "computer", arguments: { action: "warp" } }, + { type: "toolCall", id: "tool-2", name: "computer", arguments: { action: "screenshot" } }, + ]; + message.stopReason = "toolUse"; + } else { + message.content = [{ type: "text", text: "done" }]; + } + stream.push({ type: "start", partial: message }); + stream.push({ type: "done", reason: message.stopReason, message }); + stream.end(message); + return stream; + }; + const harness = new CuaAgentHarness({ + ...(await createHarnessServices()), + browser, + client, + model: "anthropic:claude-opus-4-8", + models: createModelsFromStream(streamFn, "anthropic"), + nativeTool: { type: "computer_20260701" }, + }); + + await harness.prompt("run two computer actions"); + + const results = contexts[1]!.messages.filter((message) => message.role === "toolResult"); + expect(results).toHaveLength(2); + expect(results[0]).toMatchObject({ toolCallId: "tool-1", isError: true }); + expect(results[1]).toMatchObject({ + toolCallId: "tool-2", + isError: true, + content: [{ type: "text", text: "Not executed: an earlier computer action in this turn failed." }], + }); + }); + it.each([[-1], [1.5], [Number.POSITIVE_INFINITY], [Number.NaN]])( "rejects invalid maxAttempts %s", async (maxAttempts) => { diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index e635e1c9..63e3b2ed 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## Unreleased + +- Fail locally when `computer_20260701` or `browser_20260701` is paired with + an Anthropic model that the live early-access API rejects. The verified + computer models are Claude Fable 5, Claude Opus 4.8, and Claude Sonnet 5; + the verified browser models are Claude Opus 4.8 and Claude Sonnet 5. +- Carry each native tool's stop-on-first-failure result contract through + `CuaRuntimeSpec`, allowing cua-agent to skip unsafe remaining actions without + a provider conditional. +- Add opt-in live integration coverage for the beta header, native declaration, + and pi-ai serialization path of both July 2026 tools. + ## 0.7.0 - 2026-07-17 - Added Moonshot Kimi K3 computer-use support: `moonshotai:kimi-k3` @@ -47,8 +59,9 @@ Introduces action planes (modes) and Anthropic native computer-use tools. (`cuaToolNameForAction`), descriptions, schemas, and system prompts. - New `nativeTool` option drives Anthropic models through their native computer-use declarations: `computer_20260701` (computer mode, with - `enable_zoom`) and `browser_20260701` (browser mode) behind - `anthropic-beta: computer-use-2026-07-01`. + `enable_zoom`) behind `anthropic-beta: computer-use-2026-07-01`, and + `browser_20260701` (browser mode) behind + `anthropic-beta: browser-use-2026-07-01`. - JavaScript execution is on by default: `browser_evaluate` is part of the default browser/hybrid action sets, and native `browser_20260701` declarations default `enable_javascript_exec` to true (an explicit value on diff --git a/packages/ai/src/native-tools.ts b/packages/ai/src/native-tools.ts index 99c2dbeb..ce7c4cb6 100644 --- a/packages/ai/src/native-tools.ts +++ b/packages/ai/src/native-tools.ts @@ -54,11 +54,29 @@ interface NativeToolInfo { provider: "anthropic"; betaHeader: string; defaultName: string; + /** Model families verified against the live early-access API. */ + supportedModelFamilies: readonly string[]; + /** Required error result for actions skipped after an earlier action failed. */ + skippedAfterFailureMessage: string; } const NATIVE_TOOL_INFO: Record = { - computer_20260701: { mode: "computer", provider: "anthropic", betaHeader: "computer-use-2026-07-01", defaultName: "computer" }, - browser_20260701: { mode: "browser", provider: "anthropic", betaHeader: "browser-use-2026-07-01", defaultName: "browser" }, + computer_20260701: { + mode: "computer", + provider: "anthropic", + betaHeader: "computer-use-2026-07-01", + defaultName: "computer", + supportedModelFamilies: ["claude-fable-5", "claude-opus-4-8", "claude-sonnet-5"], + skippedAfterFailureMessage: "Not executed: an earlier computer action in this turn failed.", + }, + browser_20260701: { + mode: "browser", + provider: "anthropic", + betaHeader: "browser-use-2026-07-01", + defaultName: "browser", + supportedModelFamilies: ["claude-opus-4-8", "claude-sonnet-5"], + skippedAfterFailureMessage: "Not executed: an earlier action in this turn failed.", + }, }; /** The {@link CuaMode} a native tool requires. */ @@ -85,6 +103,8 @@ export interface ResolvedCuaNativeTool { name: string; /** Required `anthropic-beta` header value. */ betaHeader: string; + /** Error result required for later tool calls after the first failure in a turn. */ + skippedAfterFailureMessage: string; mode: CuaMode; } @@ -101,6 +121,12 @@ export function resolveNativeTool(spec: CuaNativeToolSpec, model: Model, mo if (model.provider !== info.provider) { throw new Error(`native tool "${spec.type}" requires an ${info.provider} model paired with mode "${info.mode}"; got provider "${model.provider}"`); } + if (!info.supportedModelFamilies.some((family) => isModelFamily(model.id, family))) { + throw new Error( + `native tool "${spec.type}" is an allowlisted Anthropic API beta and does not support model "${model.id}"; ` + + `supported model families: ${info.supportedModelFamilies.join(", ")}`, + ); + } if (mode !== info.mode) { throw new Error(`native tool "${spec.type}" requires mode "${info.mode}"; got "${mode}"`); } @@ -110,6 +136,17 @@ export function resolveNativeTool(spec: CuaNativeToolSpec, model: Model, mo declaration: { ...spec, name }, name, betaHeader: info.betaHeader, + skippedAfterFailureMessage: info.skippedAfterFailureMessage, mode, }; } + +function isModelFamily(modelId: string, family: string): boolean { + const id = modelId.toLowerCase(); + if (id === family) return true; + if (!id.startsWith(`${family}-`)) return false; + return id + .slice(family.length + 1) + .split("-") + .every((segment) => /^\d+$/.test(segment)); +} diff --git a/packages/ai/src/providers/anthropic/native.ts b/packages/ai/src/providers/anthropic/native.ts index a1f2b18c..986df8d3 100644 --- a/packages/ai/src/providers/anthropic/native.ts +++ b/packages/ai/src/providers/anthropic/native.ts @@ -31,7 +31,7 @@ export function withAnthropicBetaHeader(options: T | un // The native tool's input schema is Anthropic-defined and validated // server-side; the local placeholder schema stays permissive and the // executor validates during mapping. -const NativeActionSchema = Type.Object({ action: Type.String() }, { additionalProperties: true }); +const NativeActionSchema = Type.Object({ action: Type.Optional(Type.String()) }, { additionalProperties: true }); /** * Build the single execution adapter for a native Anthropic tool: tool calls diff --git a/packages/ai/src/providers/common.ts b/packages/ai/src/providers/common.ts index d55f1567..07c2a456 100644 --- a/packages/ai/src/providers/common.ts +++ b/packages/ai/src/providers/common.ts @@ -373,6 +373,8 @@ export interface CuaRuntimeSpec { mode: CuaMode; /** Present when the model is driven through a provider-native tool declaration. */ nativeTool?: ResolvedCuaNativeTool; + /** When set, stop executing a turn's remaining tool calls after its first failure and return this error text for each. */ + stopOnFirstToolFailureMessage?: string; /** Provider-facing CUA tool definitions used for model requests. */ toolDefinitions: Tool[]; /** Local execution adapters that turn provider tool calls into canonical CUA actions. */ diff --git a/packages/ai/src/runtime-spec.ts b/packages/ai/src/runtime-spec.ts index 2777ac81..cda0a797 100644 --- a/packages/ai/src/runtime-spec.ts +++ b/packages/ai/src/runtime-spec.ts @@ -65,6 +65,7 @@ export function resolveCuaRuntimeSpec(input: CuaRuntimeSpecInput, options: CuaRu provider, mode, nativeTool, + stopOnFirstToolFailureMessage: nativeTool.skippedAfterFailureMessage, toolDefinitions: executors.map((executor) => executor.definition), toolExecutors: executors, defaultSystemPrompt: mod.buildSystemPrompt({ mode }), diff --git a/packages/ai/test/anthropic-native.integration.test.ts b/packages/ai/test/anthropic-native.integration.test.ts new file mode 100644 index 00000000..42b63766 --- /dev/null +++ b/packages/ai/test/anthropic-native.integration.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { createCuaModels, resolveCuaRuntimeSpec, type CuaNativeToolSpec } from "../src/index"; + +const apiKey = process.env.ANTHROPIC_API_KEY; +const liveIt = apiKey ? it : it.skip; + +const cases: Array<{ + name: string; + nativeTool: CuaNativeToolSpec; + prompt: string; + expectedAction: string; +}> = [ + { + name: "computer_20260701", + nativeTool: { type: "computer_20260701", enable_zoom: true }, + prompt: "Use the computer tool to take one screenshot.", + expectedAction: "screenshot", + }, + { + name: "browser_20260701", + nativeTool: { type: "browser_20260701", enable_javascript_exec: true }, + prompt: "Use the browser tool to navigate to example.com.", + expectedAction: "navigate", + }, +]; + +describe("Anthropic early-access native tools", () => { + for (const current of cases) { + liveIt(`${current.name} survives CUA's pi-ai serialization`, async () => { + const spec = resolveCuaRuntimeSpec("anthropic:claude-opus-4-8", { nativeTool: current.nativeTool }); + const response = await createCuaModels().complete( + spec.model, + { + systemPrompt: spec.defaultSystemPrompt, + messages: [{ role: "user", content: current.prompt, timestamp: Date.now() }], + tools: spec.toolDefinitions, + }, + { + apiKey, + maxTokens: 96, + onPayload: spec.onPayload, + }, + ); + + expect(response.stopReason, response.errorMessage).toBe("toolUse"); + expect(response.content).toContainEqual( + expect.objectContaining({ + type: "toolCall", + name: current.name.startsWith("computer") ? "computer" : "browser", + arguments: expect.objectContaining({ action: current.expectedAction }), + }), + ); + }, 60_000); + } +}); diff --git a/packages/ai/test/native-tools.test.ts b/packages/ai/test/native-tools.test.ts index dd118e92..bcc4bb07 100644 --- a/packages/ai/test/native-tools.test.ts +++ b/packages/ai/test/native-tools.test.ts @@ -25,11 +25,11 @@ describe("native tool validation", () => { }); it("rejects a native tool with a conflicting mode", () => { - expect(() => resolveCuaRuntimeSpec("anthropic:claude-opus-4-5", { mode: "browser", nativeTool: { type: "computer_20260701" } })).toThrow( + expect(() => resolveCuaRuntimeSpec("anthropic:claude-opus-4-8", { mode: "browser", nativeTool: { type: "computer_20260701" } })).toThrow( /requires mode "computer"/, ); expect(() => - resolveCuaRuntimeSpec("anthropic:claude-opus-4-5", { mode: "hybrid", nativeTool: { type: "browser_20260701" } }), + resolveCuaRuntimeSpec("anthropic:claude-opus-4-8", { mode: "hybrid", nativeTool: { type: "browser_20260701" } }), ).toThrow(/requires mode "browser"/); }); @@ -38,38 +38,58 @@ describe("native tool validation", () => { /requires an anthropic model paired with mode "computer"/, ); }); + + it("fails locally when the model is ineligible for the early-access tool version", () => { + expect(() => resolveCuaRuntimeSpec("anthropic:claude-opus-4-7", { nativeTool: { type: "computer_20260701" } })).toThrow( + /native tool "computer_20260701" is an allowlisted Anthropic API beta.*claude-opus-4-8/s, + ); + expect(() => resolveCuaRuntimeSpec("anthropic:claude-fable-5", { nativeTool: { type: "browser_20260701" } })).toThrow( + /supported model families: claude-opus-4-8, claude-sonnet-5/, + ); + }); + + it("accepts each live-verified native-tool model family", () => { + for (const model of ["claude-fable-5", "claude-opus-4-8", "claude-sonnet-5"] as const) { + expect(() => resolveCuaRuntimeSpec(`anthropic:${model}`, { nativeTool: { type: "computer_20260701" } })).not.toThrow(); + } + for (const model of ["claude-opus-4-8", "claude-sonnet-5"] as const) { + expect(() => resolveCuaRuntimeSpec(`anthropic:${model}`, { nativeTool: { type: "browser_20260701" } })).not.toThrow(); + } + }); }); describe("native runtime specs", () => { it("routes computer_20260701 to the native api with a single placeholder tool", () => { - const spec = resolveCuaRuntimeSpec("anthropic:claude-opus-4-5", { nativeTool: { type: "computer_20260701", enable_zoom: true } }); + const spec = resolveCuaRuntimeSpec("anthropic:claude-opus-4-8", { nativeTool: { type: "computer_20260701", enable_zoom: true } }); expect(spec.mode).toBe("computer"); expect(spec.model.api).toBe(ANTHROPIC_NATIVE_COMPUTER_MESSAGES_API); expect(spec.nativeTool?.betaHeader).toBe("computer-use-2026-07-01"); + expect(spec.stopOnFirstToolFailureMessage).toBe("Not executed: an earlier computer action in this turn failed."); expect(spec.toolDefinitions.map((tool) => tool.name)).toEqual(["computer"]); }); it("routes browser_20260701 to the native api under the default name", () => { - const spec = resolveCuaRuntimeSpec("anthropic:claude-opus-4-5", { nativeTool: { type: "browser_20260701" } }); + const spec = resolveCuaRuntimeSpec("anthropic:claude-opus-4-8", { nativeTool: { type: "browser_20260701" } }); expect(spec.mode).toBe("browser"); expect(spec.model.api).toBe(ANTHROPIC_NATIVE_BROWSER_MESSAGES_API); + expect(spec.stopOnFirstToolFailureMessage).toBe("Not executed: an earlier action in this turn failed."); expect(spec.toolDefinitions.map((tool) => tool.name)).toEqual(["browser"]); }); it("enables javascript exec on the native browser declaration unless the spec is explicit", () => { - const defaulted = resolveCuaRuntimeSpec("anthropic:claude-opus-4-5", { + const defaulted = resolveCuaRuntimeSpec("anthropic:claude-opus-4-8", { nativeTool: { type: "browser_20260701" }, }); expect(defaulted.nativeTool?.declaration.enable_javascript_exec).toBe(true); - const explicit = resolveCuaRuntimeSpec("anthropic:claude-opus-4-5", { + const explicit = resolveCuaRuntimeSpec("anthropic:claude-opus-4-8", { nativeTool: { type: "browser_20260701", enable_javascript_exec: false }, }); expect(explicit.nativeTool?.declaration.enable_javascript_exec).toBe(false); }); it("swaps the placeholder tool for the native declaration in the payload", async () => { - const spec = resolveCuaRuntimeSpec("anthropic:claude-opus-4-5", { nativeTool: { type: "computer_20260701", enable_zoom: true } }); + const spec = resolveCuaRuntimeSpec("anthropic:claude-opus-4-8", { nativeTool: { type: "computer_20260701", enable_zoom: true } }); const payload = { tools: [ { name: "computer", description: "placeholder", input_schema: {} }, @@ -168,10 +188,11 @@ describe("browser_20260701 action mapping", () => { describe("native tool executors", () => { it("translate native tool calls through the runtime spec executors", () => { - const spec = resolveCuaRuntimeSpec("anthropic:claude-opus-4-5", { nativeTool: { type: "browser_20260701" } }); + const spec = resolveCuaRuntimeSpec("anthropic:claude-opus-4-8", { nativeTool: { type: "browser_20260701" } }); const executor = spec.toolExecutors[0]!; const actions: CuaAction[] = executor.toActions({ action: "left_click", target: { type: "ref", ref: "e3" } }); expect(actions).toEqual([{ type: "browser_click", ref: "e3" }]); + expect(() => executor.toActions({})).toThrow(/expected an object with an "action" field/); }); it("exports the anthropic namespace surface", () => { diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 41cd1b9d..701e5e34 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -72,9 +72,11 @@ Options: computer: OS-level input only. browser: CDP page tools (snapshot, find, click-by-ref, navigate, tabs). hybrid: both, deduplicated (computer_* + browser_* tools). - --native-tool Drive an Anthropic model through its native tool schema: - computer_20260701 (requires --mode computer) or - browser_20260701 (requires --mode browser) + --native-tool Drive an entitled Anthropic API key through an early-access + native schema. computer_20260701 requires --mode computer + and claude-fable-5, claude-opus-4-8, or claude-sonnet-5. + browser_20260701 requires --mode browser and + claude-opus-4-8 or claude-sonnet-5. --out Output file for screenshot subcommand --filter Restrict \`cua snapshot\` to interactive elements -o, --output Output format for --print: text (default) | jsonl diff --git a/packages/cli/test/cli-harness-validation.test.ts b/packages/cli/test/cli-harness-validation.test.ts index c8f20ce3..18158129 100644 --- a/packages/cli/test/cli-harness-validation.test.ts +++ b/packages/cli/test/cli-harness-validation.test.ts @@ -55,6 +55,18 @@ describe("mode/native-tool validation before provisioning", () => { expect(provisionBrowser).not.toHaveBeenCalled(); }); + it("rejects an ineligible Anthropic model without provisioning a browser", async () => { + await expect( + runActionCommand("url", [], flagsWith({ + model: "anthropic:claude-opus-4-7", + nativeTool: "browser_20260701", + })), + ).rejects.toThrow( + 'native tool "browser_20260701" is an allowlisted Anthropic API beta and does not support model "claude-opus-4-7"', + ); + expect(provisionBrowser).not.toHaveBeenCalled(); + }); + it("rejects an unsupported provider/mode pair without provisioning a browser", async () => { await expect( runActionCommand("url", [], flagsWith({