feat: add Yutori Navigator n1.5 as a computer-use (CUA) agent provider#2194
feat: add Yutori Navigator n1.5 as a computer-use (CUA) agent provider#2194lawrencechen98 wants to merge 9 commits into
Conversation
🦋 Changeset detectedLatest commit: 06d9a74 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
This PR is from an external contributor and must be approved by a stagehand team member with write access before CI can run. |
931e0b7 to
ce6c06e
Compare
There was a problem hiding this comment.
2 issues found across 23 files
Confidence score: 3/5
- There is some real merge risk:
stopAndSummarize()inpackages/core/lib/v3/agent/YutoriCUAClient.tsbypasses expectedflowLoggerinstrumentation, which can reduce traceability and make agent behavior/debugging less reliable. packages/core/lib/v3/handlers/v3CuaAgentHandler.tshas a concrete replay correctness concern—modifiers used during CUA action execution are not persisted, so replay can diverge for modifier-dependent actions.- Given a high-confidence medium/high-severity instrumentation gap plus a replay-divergence bug, this looks mergeable only with caution rather than a low-risk merge.
- Pay close attention to
packages/core/lib/v3/agent/YutoriCUAClient.tsandpackages/core/lib/v3/handlers/v3CuaAgentHandler.ts- missing flow logging and non-persisted modifiers can cause observability and replay consistency issues.
Architecture diagram
sequenceDiagram
participant User as User Code
participant Stagehand as Stagehand Instance
participant Agent as Agent Provider
participant Client as YutoriCUAClient
participant Handler as V3CuaAgentHandler
participant Page as Understudy Page
participant CDP as Chrome DevTools Protocol
participant Navigator as Yutori Navigator API
Note over User,Navigator: NEW: Yutori Navigator n1.5 CUA Agent Provider
User->>Stagehand: stagehand.agent({ mode: "cua", model: "yutori/n1.5-latest" })
Stagehand->>Agent: create provider (modelToAgentProviderMap)
Agent->>Client: new YutoriCUAClient(type, model, instructions, clientOptions)
alt API key missing
Client-->>Agent: throw Error
end
Client->>Client: configure toolSet, disableTools, jsonSchema, userTimezone, userLocation
Stagehand-->>User: agent instance
User->>Stagehand: agent.execute({ instruction, maxSteps })
Stagehand->>Handler: new V3CuaAgentHandler(..., client=YutoriCUAClient)
Handler->>Client: setScreenshotProvider()
Handler->>Client: setActionHandler()
Handler->>Handler: capture screenshot (page.screenshot)
Handler->>Client: setCurrentUrl(page.url())
Client->>Client: build message history (system prompt + user instruction with location/timezone context)
loop Step Loop (maxSteps)
Client->>Client: clone messages for request
Client->>Client: trimImagesToFit (drop old screenshots under ~9.5 MB, keep latest)
Client->>Navigator: POST /v1/chat/completions (OpenAI-compatible)
Note over Client,Navigator: Extra params: tool_set, disable_tools, json_schema
Navigator-->>Client: response with tool_calls (1000x1000 normalized coordinates)
Client->>Client: parse tool_calls from assistant message
alt No tool_calls
Client-->>Handler: return final result (completed)
else Has tool_calls
par For each tool_call
Client->>Client: denormalizeCoordinates (1000→viewport pixels)
Client->>Client: mapNavigatorKeyToPlaywright (Navigator keys→Playwright keys)
Client->>Handler: actionHandler(action)
alt Action type: click (with possible modifier)
Handler->>Page: click(x, y, { modifiers })
Page->>CDP: dispatchMouseEvent(..., modifiers bitmask)
else Action type: scroll (with possible modifier)
Handler->>Page: scroll(x, y, deltaX, deltaY, { modifiers })
Page->>CDP: dispatchMouseEvent(..., modifiers bitmask)
else Action type: keypress (with optional holdMs delay)
Handler->>Page: keyPress(key, { delay })
else Action type: refresh
Handler->>Page: reload({ waitUntil: "load" })
Page->>CDP: Page.reload
else Action type: type, goto, back, forward, wait, drag
Handler->>Page: execute action
end
alt Action succeeded
Page-->>Handler: success
else Action threw error
Page-->>Handler: error
Handler->>Page: still update client URL (page.url())
Handler-->>Client: action result with [ERROR]
end
end
Client->>Client: append tool result (role:"tool" + "Current URL:" suffix)
Client->>Client: captureScreenshot() for next turn
end
alt Payload size > max bytes
Client->>Client: trimImagesToFit (strip old screenshots, keep recent)
end
end
alt Max steps reached (no completion)
Client->>Client: formatStopAndSummarize(task)
Client->>Navigator: final request with summarize prompt (no json_schema)
Navigator-->>Client: summary text
Client-->>Handler: return result (completed=false, summary message)
end
Handler-->>Stagehand: AgentResult (output may include parsed_json)
Stagehand-->>User: execution result
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
ef9c84b to
eb906ec
Compare
…tion Rework of browserbase#2194 so the Navigator n1.5 integration matches the existing CUA provider boundary: all provider-specific logic lives in YutoriCUAClient, and cross-cutting needs flow through abstractions Stagehand already has. - Revert all understudy/handler/cache/replay/public-type changes to base; drop the expanded-tool stack (page bridge, ref registry, snapshot renderer) and the five Yutori fields on ClientOptions / cloud API schema (API surface change is now the yutori provider enum entries only). - Custom page capabilities arrive as user tools via agent({ tools }): advertised as OpenAI function tools, executed in-client (Anthropic pattern), recorded as custom_tool trajectory actions (Google pattern). - Structured output via execute({ output }) -> Navigator json_schema -> result.output; execute({ excludeTools }) -> disable_tools, unioned with defaults [mouse_down, mouse_up, hold_key]. - Graceful degradation in-client: refresh -> goto(current URL), modifier clicks return a tool error, hold_key degrades to a plain press. - New CUA_SUPPORTED_EXECUTE_OPTIONS capability map on the provider registry lets validateExperimentalFeatures accept excludeTools/output for providers that implement them (still experimental-gated), instead of rejecting them wholesale in CUA mode. - Example doubles as the migration recipe for the expanded tools (extractLinks / executeJs as tool() closures over the page). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Integrates Yutori's Navigator n1.5 computer-use model as a Stagehand CUA provider, mirroring the existing OpenAI/Anthropic/Google/Microsoft CUA clients. Navigator is OpenAI-compatible Chat Completions at https://api.yutori.com/v1 (screenshot in, coordinate tool_calls out in a normalized 1000x1000 space), so this reuses the existing `openai` dependency and the provider-agnostic CUA handler — no new dependencies. Usage: stagehand.agent({ mode: "cua", model: "yutori/n1.5-latest" }). Auth via YUTORI_API_KEY or clientOptions (apiKey/baseURL). Core tool set. - YutoriCUAClient: screenshot-per-turn loop; tool_call -> AgentAction with 1000x1000 coordinate denormalization; role:"tool" results with a current-URL suffix; payload trimming; completion when no tool calls; stop-and-summarize on max steps. Faithful to the Yutori Python SDK reference loop. - Provider registration (AgentProvider, AVAILABLE_CUA_MODELS, AgentType, providerEnvVarMap) and Navigator ClientOptions (toolSet/disableTools/ jsonSchema/userTimezone/userLocation), incl. the cloud API + OpenAPI schema. - Keyboard modifiers via a general page.click/scroll `modifiers` option (sets the CDP mouse-event modifiers bitmask); hold-key; refresh via page.reload with a faithful agent-replay step. - Evals: skip building an AI-SDK text client for CUA-only models in the local bench harness path (also unblocks microsoft/fara-7b). - Unit tests + usage example. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review: stopAndSummarize() made a direct chat.completions.create call without FlowLogger instrumentation. Wrap it with FlowLogger.logLlmRequest/logLlmResponse, mirroring predict(), so every direct Navigator LLM call is flow-logged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rolls Address review (P2): modifiers applied during CUA action execution were not captured in recorded agent-replay steps, so a cached replay re-ran a chorded click/scroll as a plain one. Thread modifiers through the selector-based path symmetrically with the coordinate path: - Add a shared `cdpModifierMask` helper (understudy/modifiers.ts); reuse it in Page (dedupes the previous private copy) and add modifier support to Locator.click via the CDP mouse-event modifiers bitmask. - Action gains optional `modifiers`; performUnderstudyMethod forwards them to the locator click; takeDeterministicAction passes action.modifiers. - The CUA handler records modifiers on the replay step for click (Action) and scroll (AgentReplayScrollStep); AgentCache re-applies them on replay. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…est)
Enable the expanded Navigator tool set (browser_tools_expanded-20260403) and
make it the default for yutori/n1.5-latest, on top of the core coordinate tools.
- DOM tools backed by Stagehand's a11y snapshot + deepLocator:
- extract_elements / find render the hybrid accessibility tree in Navigator's
format, minting stable ref_N tokens (NavigatorRefRegistry).
- set_element_value resolves a ref to its xpath and fills via deepLocator.
- execute_js evaluates JS in the page (expression-first, body fallback).
- ref-targeted coordinate tools: click/scroll/etc. may carry a `ref` instead of
coordinates; it resolves to the element's on-screen center (deepLocator
centroid, scroll-into-view), taking priority over model coordinates and
falling back to them. A ref'd scroll scrolls the element into view.
- The CUA handler supplies a generic page bridge (a11y snapshot + evaluate +
elementCenter); all Navigator-specific logic stays in the client.
- Unknown/stale refs return a recoverable error so the model re-extracts.
Unit tests cover rendering/find/ref resolution + the four-tool dispatch,
tool-set selection, scroll-into-view, and stale-ref handling; a
YUTORI_API_KEY-gated integration spec exercises the tools live.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion Rework of browserbase#2194 so the Navigator n1.5 integration matches the existing CUA provider boundary: all provider-specific logic lives in YutoriCUAClient, and cross-cutting needs flow through abstractions Stagehand already has. - Revert all understudy/handler/cache/replay/public-type changes to base; drop the expanded-tool stack (page bridge, ref registry, snapshot renderer) and the five Yutori fields on ClientOptions / cloud API schema (API surface change is now the yutori provider enum entries only). - Custom page capabilities arrive as user tools via agent({ tools }): advertised as OpenAI function tools, executed in-client (Anthropic pattern), recorded as custom_tool trajectory actions (Google pattern). - Structured output via execute({ output }) -> Navigator json_schema -> result.output; execute({ excludeTools }) -> disable_tools, unioned with defaults [mouse_down, mouse_up, hold_key]. - Graceful degradation in-client: refresh -> goto(current URL), modifier clicks return a tool error, hold_key degrades to a plain press. - New CUA_SUPPORTED_EXECUTE_OPTIONS capability map on the provider registry lets validateExperimentalFeatures accept excludeTools/output for providers that implement them (still experimental-gated), instead of rejecting them wholesale in CUA mode. - Example doubles as the migration recipe for the expanded tools (extractLinks / executeJs as tool() closures over the page). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n shim Two CUA-loop fixes surfaced while running the Yutori Navigator (n1.5) provider through the eval harness: 1. Restore the post-action Current-URL refresh. The provider-containment refactor kept updateClientUrl() but dropped its post-action call, so the client reported the pre-action URL in tool results for the rest of each step. Re-add `await this.updateClientUrl()` in the action handler's finally. 2. Add a native <select> dropdown shim (utils/replaceNativeSelect.ts). A native <select>'s option list is a browser/OS control, not regular DOM, so a CUA agent's clicks don't register as selections. Before each screenshot we inject an idempotent in-page HTML overlay: options become real, clickable DOM, committed via the native value setter (+ input/change) so framework- controlled selects accept them; the overlay is viewport-positioned so options stay on-screen and closes on an outside click. The overlay is resolved by id (re-created if the page tears it down) so a select bound on an earlier step never targets a stale node; state on window keeps per-step re-injection idempotent. Disabled options are non-clickable, hidden selects skipped. Covered by tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The OnlineMind2Web task injected a generic harness system prompt into every agent. Navigator (n1.5) ships its own tuned system prompt and is degraded by the harness's generic instructions, so skip systemPrompt for Navigator models (yutori/*) and let it use its default. Every other model — including other CUA providers — keeps the harness prompt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
eb906ec to
d95e8e5
Compare
There was a problem hiding this comment.
8 issues found across 21 files
Confidence score: 3/5
- The biggest risk is in
packages/core/lib/v3/agent/YutoriCUAClient.ts: long Navigator sessions keep full base64 screenshots in persistent history even after request trimming, so memory usage can grow with each step and eventually destabilize long runs — trim/prune the stored history itself before send so dropped images can be garbage-collected. - Select interaction handling in
packages/core/lib/v3/handlers/v3CuaAgentHandler.tsandpackages/core/lib/v3/agent/utils/replaceNativeSelect.tshas a concrete regression path (shadow-DOM retargeting, missing nested open-shadow-root shims, and viewport/offset mismatch), which can make dropdowns close immediately or become unclickable — update event targeting to usee.composedPath(), recurse all discovered open shadow roots, and compute overlay positioning in viewport coordinates before merging. packages/core/lib/v3/handlers/v3CuaAgentHandler.tscan unintentionally hijack a pre-existing page element withid="__sh-select-overlay", potentially clearing/restyling app UI when any select opens — only reuse helper-owned overlays and create a fresh element when ownership is unknown.packages/core/lib/v3/v3.tsandpackages/evals/tasks/bench/agent/onlineMind2Web.tsleave newly enabled YutoriexcludeTools/outputand prompt-selection behavior under-covered, so forwarding regressions could slip through silently;packages/core/lib/v3/agent/AgentProvider.tsdocs are also stale — add V3/provider-resolution and prompt-selection tests and align the option docs before merge.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/evals/tasks/bench/agent/onlineMind2Web.ts">
<violation number="1" location="packages/evals/tasks/bench/agent/onlineMind2Web.ts:52">
P3: Navigator prompt selection has no regression coverage; add focused cases asserting `yutori/n1.5-latest` omits `systemPrompt` and a non-Yutori model retains it.
(Based on your team's feedback about unit-test coverage.) [FEEDBACK_USED].</violation>
</file>
<file name="packages/core/lib/v3/agent/AgentProvider.ts">
<violation number="1" location="packages/core/lib/v3/agent/AgentProvider.ts:52">
P3: Yutori users are documented away from both newly enabled execute options. Update `AgentExecuteOptionsBase.excludeTools` and `output` docs to describe the Navigator CUA exception and its server-side result behavior.</violation>
</file>
<file name="packages/core/lib/v3/v3.ts">
<violation number="1" location="packages/core/lib/v3/v3.ts:2099">
P3: Yutori `excludeTools`/`output` support now depends on V3 resolving and forwarding this map, but that wiring has no coverage. Add V3-level cases for model-ID and explicit-provider resolution so a regression does not silently restore the unsupported-feature error.
(Based on your team's feedback about adding unit tests for new behavior.) [FEEDBACK_USED].</violation>
</file>
<file name="packages/core/lib/v3/handlers/v3CuaAgentHandler.ts">
<violation number="1" location="packages/core/lib/v3/handlers/v3CuaAgentHandler.ts:98">
P3: A page already using `id="__sh-select-overlay"` can have its application element emptied and restyled when an agent opens any select. Mark helper-owned overlays and create a new element when the matching ID is not owned by the shim.</violation>
<violation number="2" location="packages/core/lib/v3/handlers/v3CuaAgentHandler.ts:98">
P2: Selecting an open-shadow-root `<select>` fails because its opening `mousedown` is retargeted to the shadow host at the document listener, which immediately closes the overlay. Check `e.composedPath()` for the active select (or skip shadow-root selects) before treating the event as outside.</violation>
</file>
<file name="packages/core/lib/v3/agent/YutoriCUAClient.ts">
<violation number="1" location="packages/core/lib/v3/agent/YutoriCUAClient.ts:412">
P2: Long Navigator runs retain every full base64 screenshot despite request trimming, causing heap growth proportional to steps. Trim the persistent history before sending so dropped images can be collected.</violation>
</file>
<file name="packages/core/lib/v3/agent/utils/replaceNativeSelect.ts">
<violation number="1" location="packages/core/lib/v3/agent/utils/replaceNativeSelect.ts:46">
P2: On pages with a positioned body or html element, the overlay offsets are evaluated from that ancestor while the select rectangle is viewport-relative, shifting the list away from screenshot click coordinates. Use viewport-fixed positioning with rectangle coordinates, or convert coordinates to the actual offset parent.</violation>
<violation number="2" location="packages/core/lib/v3/agent/utils/replaceNativeSelect.ts:112">
P2: Selects below a second open shadow root never receive the shim, leaving their native popup unselectable by coordinate actions. Recursively walk each discovered open shadow root rather than querying only the document's direct roots.</violation>
</file>
Architecture diagram
sequenceDiagram
participant User as User/Client Code
participant Stagehand as Stagehand V3
participant Handler as V3CuaAgentHandler
participant AgentProv as AgentProvider
participant Navigator as YutoriCUAClient
participant YutoriAPI as Yutori API (api.yutori.com)
participant Browser as Browser (Playwright Page)
participant SelectShim as Native Select Overlay
Note over User,SelectShim: CUA Agent Execution with Yutori Navigator n1.5
User->>Stagehand: stagehand.agent({ mode:"cua", model:"yutori/n1.5-latest", tools })
Stagehand->>AgentProv: getAgentProvider("yutori/n1.5-latest")
AgentProv-->>Stagehand: "yutori"
Stagehand->>Navigator: new YutoriCUAClient(...)
Note over Navigator: Strips "yutori/" prefix -> "n1.5-latest"
Stagehand-->>User: agent instance
User->>Stagehand: agent.execute({ instruction, maxSteps, output?, excludeTools? })
Stagehand->>Stagehand: validateExperimentalFeatures(cuaSupportedExecuteOptions=yutori)
Note over Stagehand: Accepts excludeTools & output for yutori
Stagehand->>Handler: execute()
Handler->>Navigator: setScreenshotProvider()
Handler->>Handler: define screenshotProvider callback
Handler->>Navigator: setActionHandler()
loop Each Step (until done or maxSteps)
Navigator->>Handler: captureScreenshot()
Handler->>Browser: page.evaluate(REPLACE_NATIVE_SELECT_SCRIPT)
Browser->>SelectShim: Inject or reuse overlay (idempotent)
Handler->>Browser: page.screenshot({fullPage:false, type:"png"})
Browser-->>Handler: screenshot buffer (base64)
Handler-->>Navigator: base64 image
Navigator->>Navigator: cloneMessagesForRequest()
Navigator->>Navigator: trimImagesToFit() (drop old screenshots if oversize)
Navigator->>Navigator: buildRequest({model, messages, temperature, tool_set:"browser_tools_core-20260403", disable_tools, json_schema?, tools?})
alt Custom tools provided
Navigator->>Navigator: customToolParams() -> function tools
end
alt output schema provided
Navigator->>Navigator: include json_schema
end
Navigator->>YutoriAPI: POST /v1/chat/completions
Note over Navigator,YutoriAPI: Non-streaming, extra params (tool_set, disable_tools, json_schema)
YutoriAPI-->>Navigator: ChatCompletion response (assistant message with tool_calls or content)
opt response has parsed_json (structured output)
Navigator->>Navigator: store parsedJson
end
alt No tool_calls → Done
Navigator->>Navigator: completion!
Navigator-->>Handler: AgentResult { success: true, output: parsedJson? }
Handler-->>Stagehand: result
Stagehand-->>User: AgentResult
else Max steps reached
Navigator->>Navigator: formatStopAndSummarize(instruction)
Navigator->>YutoriAPI: Send stop-and-summarize message (last screenshot)
YutoriAPI-->>Navigator: summary text
Navigator->>Navigator: stop and summarize
Navigator-->>Handler: AgentResult { success: true, message }
Handler-->>Stagehand: result
else Tool calls received
loop For each tool_call
Navigator->>Navigator: denormalizeCoordinates(call.args)
Note over Navigator: 1000x1000 → viewport pixels
alt Unsupported/modifier keys on click/scroll
Navigator->>Navigator: [ERROR] modifier keys not supported
else Valid action
Navigator->>Handler: actionHandler(action)
Handler->>Browser: perform action (click, type, scroll, etc.)
alt Action succeeds
Browser-->>Handler: ok
else Action throws
Handler->>Handler: catch error
Handler-->>Navigator: error as [ERROR] result
end
end
Navigator->>Navigator: record tool result (with "Current URL: {url}")
end
Handler->>Navigator: updateClientUrl() (finally block – post-action URL refresh)
Note over Handler,Navigator: Restored per-step refresh so tool result reports current URL
Navigator->>Navigator: append tool results to messages
Navigator->>Navigator: continue loop
end
end
Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.
Re-trigger cubic
| // never changes). The overlay re-renders the options as real, clickable | ||
| // in-page DOM. Idempotent, so it is safe to re-run every step; failures | ||
| // are swallowed. | ||
| await page.evaluate(REPLACE_NATIVE_SELECT_SCRIPT).catch(() => {}); |
There was a problem hiding this comment.
P2: Selecting an open-shadow-root <select> fails because its opening mousedown is retargeted to the shadow host at the document listener, which immediately closes the overlay. Check e.composedPath() for the active select (or skip shadow-root selects) before treating the event as outside.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/lib/v3/handlers/v3CuaAgentHandler.ts, line 98:
<comment>Selecting an open-shadow-root `<select>` fails because its opening `mousedown` is retargeted to the shadow host at the document listener, which immediately closes the overlay. Check `e.composedPath()` for the active select (or skip shadow-root selects) before treating the event as outside.</comment>
<file context>
@@ -87,6 +88,15 @@ export class V3CuaAgentHandler {
+ // never changes). The overlay re-renders the options as real, clickable
+ // in-page DOM. Idempotent, so it is safe to re-run every step; failures
+ // are swallowed.
+ await page.evaluate(REPLACE_NATIVE_SELECT_SCRIPT).catch(() => {});
+
const screenshotBuffer = await page.screenshot({
</file context>
| (last as { content: unknown }).content = content; | ||
|
|
||
| // Trim old screenshots from a request copy to stay under the size cap. | ||
| const requestMessages = cloneMessagesForRequest(this.messages); |
There was a problem hiding this comment.
P2: Long Navigator runs retain every full base64 screenshot despite request trimming, causing heap growth proportional to steps. Trim the persistent history before sending so dropped images can be collected.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/lib/v3/agent/YutoriCUAClient.ts, line 412:
<comment>Long Navigator runs retain every full base64 screenshot despite request trimming, causing heap growth proportional to steps. Trim the persistent history before sending so dropped images can be collected.</comment>
<file context>
@@ -0,0 +1,907 @@
+ (last as { content: unknown }).content = content;
+
+ // Trim old screenshots from a request copy to stay under the size cap.
+ const requestMessages = cloneMessagesForRequest(this.messages);
+ const { removed, sizeBytes } = trimImagesToFit(
+ requestMessages,
</file context>
| export const CUA_SUPPORTED_EXECUTE_OPTIONS: Partial< | ||
| Record<AgentProviderType, { excludeTools?: boolean; output?: boolean }> | ||
| > = { | ||
| yutori: { excludeTools: true, output: true }, |
There was a problem hiding this comment.
P3: Yutori users are documented away from both newly enabled execute options. Update AgentExecuteOptionsBase.excludeTools and output docs to describe the Navigator CUA exception and its server-side result behavior.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/lib/v3/agent/AgentProvider.ts, line 52:
<comment>Yutori users are documented away from both newly enabled execute options. Update `AgentExecuteOptionsBase.excludeTools` and `output` docs to describe the Navigator CUA exception and its server-side result behavior.</comment>
<file context>
@@ -37,6 +38,18 @@ export const modelToAgentProviderMap: Record<string, AgentProviderType> = {
+export const CUA_SUPPORTED_EXECUTE_OPTIONS: Partial<
+ Record<AgentProviderType, { excludeTools?: boolean; output?: boolean }>
+> = {
+ yutori: { excludeTools: true, output: true },
};
</file context>
| typeof instructionOrOptions === "object" | ||
| ? instructionOrOptions | ||
| : null, | ||
| cuaSupportedExecuteOptions, |
There was a problem hiding this comment.
P3: Yutori excludeTools/output support now depends on V3 resolving and forwarding this map, but that wiring has no coverage. Add V3-level cases for model-ID and explicit-provider resolution so a regression does not silently restore the unsupported-feature error.
(Based on your team's feedback about adding unit tests for new behavior.) .
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/lib/v3/v3.ts, line 2099:
<comment>Yutori `excludeTools`/`output` support now depends on V3 resolving and forwarding this map, but that wiring has no coverage. Add V3-level cases for model-ID and explicit-provider resolution so a regression does not silently restore the unsupported-feature error.
(Based on your team's feedback about adding unit tests for new behavior.) .</comment>
<file context>
@@ -2078,6 +2096,7 @@ export class V3 {
typeof instructionOrOptions === "object"
? instructionOrOptions
: null,
+ cuaSupportedExecuteOptions,
});
</file context>
| // never changes). The overlay re-renders the options as real, clickable | ||
| // in-page DOM. Idempotent, so it is safe to re-run every step; failures | ||
| // are swallowed. | ||
| await page.evaluate(REPLACE_NATIVE_SELECT_SCRIPT).catch(() => {}); |
There was a problem hiding this comment.
P3: A page already using id="__sh-select-overlay" can have its application element emptied and restyled when an agent opens any select. Mark helper-owned overlays and create a new element when the matching ID is not owned by the shim.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/lib/v3/handlers/v3CuaAgentHandler.ts, line 98:
<comment>A page already using `id="__sh-select-overlay"` can have its application element emptied and restyled when an agent opens any select. Mark helper-owned overlays and create a new element when the matching ID is not owned by the shim.</comment>
<file context>
@@ -87,6 +88,15 @@ export class V3CuaAgentHandler {
+ // never changes). The overlay re-renders the options as real, clickable
+ // in-page DOM. Idempotent, so it is safe to re-run every step; failures
+ // are swallowed.
+ await page.evaluate(REPLACE_NATIVE_SELECT_SCRIPT).catch(() => {});
+
const screenshotBuffer = await page.screenshot({
</file context>
- YutoriCUAClient: implement AgentClient's ScreenshotProviderResult provider
type and build the data URL from base64/mediaType (was Promise<string>,
which broke against upstream's updated screenshot contract)
- YutoriCUAClient: fall back to 5s when a non-finite wait duration is given
- YutoriCUAClient: normalize scroll direction (lowercase + trim)
- replaceNativeSelect: render option.label (native display text) not .text
- tests: update screenshot-provider mocks to the {base64, mediaType} shape
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- YutoriCUAClient: custom tools can no longer shadow Navigator core action names — core actions take precedence at dispatch - replaceNativeSelect: options inside a disabled <optgroup> now render as disabled (option.disabled does not reflect the ancestor group) - docs: note excludeTools/output ARE supported for Navigator CUA (validateExperimentalFeatures + AgentExecuteOptionsBase), not universally unsupported Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
2 issues found across 4 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/core/lib/v3/agent/utils/replaceNativeSelect.ts">
<violation number="1" location="packages/core/lib/v3/agent/utils/replaceNativeSelect.ts:68">
P3: Disabled `<optgroup>` behavior has no regression test, although it differs from the existing directly-disabled-option case. Add a test asserting its child row cannot change the select value.
(Based on your team's feedback about unit tests for new behavior.) [FEEDBACK_USED]</violation>
</file>
<file name="packages/core/lib/v3/agent/YutoriCUAClient.ts">
<violation number="1" location="packages/core/lib/v3/agent/YutoriCUAClient.ts:597">
P3: Core/custom precedence has no regression test, so a future edit can reintroduce browser-action hijacking without failing CI. Add a custom `left_click` tool test that asserts its `execute` is skipped and the action handler receives the core click.
(Based on your team's feedback about adding unit tests for new behavior.) [FEEDBACK_USED]</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| Array.from(select.options).forEach((opt) => { | ||
| // Native selects disable every option inside a disabled <optgroup>, but | ||
| // option.disabled does not reflect the ancestor group — derive it here. | ||
| const disabled = |
There was a problem hiding this comment.
P3: Disabled <optgroup> behavior has no regression test, although it differs from the existing directly-disabled-option case. Add a test asserting its child row cannot change the select value.
(Based on your team's feedback about unit tests for new behavior.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/lib/v3/agent/utils/replaceNativeSelect.ts, line 68:
<comment>Disabled `<optgroup>` behavior has no regression test, although it differs from the existing directly-disabled-option case. Add a test asserting its child row cannot change the select value.
(Based on your team's feedback about unit tests for new behavior.) </comment>
<file context>
@@ -63,12 +63,19 @@ export const REPLACE_NATIVE_SELECT_SCRIPT = `(() => {
Array.from(select.options).forEach((opt) => {
+ // Native selects disable every option inside a disabled <optgroup>, but
+ // option.disabled does not reflect the ancestor group — derive it here.
+ const disabled =
+ opt.disabled ||
+ (opt.parentNode &&
</file context>
| // Custom user tool (stagehand.agent({ tools })): execute it directly — | ||
| // it is not a page action, so it never goes through the action handler — | ||
| // and record it in the trajectory like the other CUA providers do. | ||
| if (this.tools && name in this.tools && !CORE_ACTION_NAMES.has(name)) { |
There was a problem hiding this comment.
P3: Core/custom precedence has no regression test, so a future edit can reintroduce browser-action hijacking without failing CI. Add a custom left_click tool test that asserts its execute is skipped and the action handler receives the core click.
(Based on your team's feedback about adding unit tests for new behavior.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/lib/v3/agent/YutoriCUAClient.ts, line 597:
<comment>Core/custom precedence has no regression test, so a future edit can reintroduce browser-action hijacking without failing CI. Add a custom `left_click` tool test that asserts its `execute` is skipped and the action handler receives the core click.
(Based on your team's feedback about adding unit tests for new behavior.) </comment>
<file context>
@@ -572,7 +594,7 @@ export class YutoriCUAClient extends AgentClient {
// it is not a page action, so it never goes through the action handler —
// and record it in the trajectory like the other CUA providers do.
- if (this.tools && name in this.tools) {
+ if (this.tools && name in this.tools && !CORE_ACTION_NAMES.has(name)) {
const action: AgentAction = {
type: "custom_tool",
</file context>
Summary
Adds Yutori Navigator n1.5 as a computer-use (CUA) agent provider, alongside the existing OpenAI / Anthropic / Google CUA clients. Navigator takes a screenshot per turn and returns coordinate-based
tool_callsin a normalized 1000×1000 space, served over an OpenAI-compatible Chat Completions API athttps://api.yutori.com/v1— so it reuses Stagehand's existingopenaidependency and the provider-agnosticV3CuaAgentHandler. No new dependencies.Auth via
YUTORI_API_KEY(orclientOptions.apiKey/baseURL).What's included
1. Navigator provider (
YutoriCUAClient+yutoriActions)Screenshot-per-turn loop;
tool_call→AgentActionwith 1000×1000 → viewport denormalization;role:"tool"results carrying aCurrent URL:suffix; request-payload trimming that bounds recent screenshots; completion when there are notool_calls; stop-and-summarize atmaxSteps. Registered inAgentProvider, the model/agent public types, andproviderEnvVarMap(yutori→YUTORI_API_KEY), plus the hosted API schema (openapi.v3.yaml).2. Contained to the existing CUA provider boundary
All provider-specific logic lives in the client — no handler-shape changes for other providers:
agent({ tools })(advertised as function tools, executed in-client).execute({ output })→ Navigatorjson_schema→result.output.execute({ excludeTools })→disable_tools(unioned with defaults); aCUA_SUPPORTED_EXECUTE_OPTIONScapability map letsvalidateExperimentalFeaturesaccept these for providers that implement them.refresh→goto(current URL), modifier-clicks return a tool error,hold_keydegrades to a plain press.3. Harness fixes (surfaced running Navigator through the eval harness)
V3CuaAgentHandler) — restore the per-stepupdateClientUrl()call so a tool result reports the post-action URL rather than the pre-action one.<select>dropdown shim (utils/replaceNativeSelect.ts) — a native<select>'s option list isn't clickable DOM for a CUA agent, so it re-clicks the closed control and the value never changes. We inject an idempotent in-page HTML overlay before each screenshot so options are visible and selectable (viewport-positioned; disabled options non-clickable; framework-controlled selects committed via the native value setter).yutori/*models use their own tuned system prompt instead of the harness's generic one; all other models are unchanged.Tests
Unit coverage for the client (action mapping, message/trajectory shape, structured output, error recovery, stop-and-summarize), the action helpers (coordinate denorm/validation, payload trimming), the dropdown shim,
validateExperimentalFeatures, and API serialization — plus a runnable usage example. Core build (esm + cjs), the new unit suites, and the evals typecheck all pass locally; the provider has been exercised end-to-end against the live Navigator API.Notes for reviewers
Rebased on latest
main; mergeable, linear history. As a fork PR the build/test workflows are awaiting maintainer approval to run.