""" Static schema + prompt blob for the browser sub-agent. One big single-responsibility constant file: tool schema, action map, system prompt, and the turn/report invariants. Exceeds the 300-LOC soft ceiling on purpose because it is one cohesive data blob, not multiple responsibilities. """ # Two prompt levers that A/B-proved out and now ship unconditionally. THINK_SHORTER (no prose beside action tools; ReportProgress IS the thinking) cut per-turn output ~28% and roughly halved narration turns. MERGE_VERIFY (a confirmed `expect` is the proof, skip the re-check) drops a wasted round-trip at the end. P_THINK_SHORTER = ( "Do NOT write a free-text sentence next to your action tools: your ReportProgress " "fields ARE your thinking, and a separate prose explanation just repeats them and slows " "the turn. Don't narrate to the user as you go either. When the task is done you finish by " "calling the Done tool (never by typing a sentence); every other turn is ReportProgress + " "tools, no prose.\n" ) P_MERGE_VERIFY = ( "When that `expect` CONFIRMS (the result says 'Confirmed: ...'), that IS your " "verification: go STRAIGHT to calling Done. Do NOT spend an " "extra screenshot or read turn to re-check what the confirmation already proved, that " "is a wasted round-trip. Only take a separate verification step when `expect` came " "back 'NOT confirmed' or you forgot to pass one.\n" ) MODEL_MAP = { "sonnet": "claude-sonnet-4-6", "opus": "claude-opus-4-6", "haiku": "claude-haiku-4-5-20251001", } # The change an action should cause, declared by the agent and CONFIRMED after the action runs (success is observed, never assumed). A hit returns fast; a miss tells the agent it may not have worked instead of letting it claim a false success. P_EXPECT_DESC = { "type": "string", "description": ( "Optional but recommended: LITERAL text that should be VISIBLE on the page " "after this action; an exact button label, a person's name, the exact text you " "just typed. Never a description of the change: 'message appears in box' is not " "page text, can never match, and will always come back NOT confirmed. It's " "checked right after, so you learn whether it actually worked. REQUIRED for " "anything you can't undo (Send/Submit/Pay/Post): set it to proof the action " "landed (for typing, the typed text itself)." ), } BROWSER_TOOLS_SCHEMA = [ { "name": "ReportProgress", "description": ( "Record your assessment of the previous action and your plan for the " "next one. You MUST call this BEFORE any browser action tools in every " "turn (after the very first turn). This is how you reflect on what just " "happened, track what you've learned about this site, and articulate what " "you're trying to do next. Skipping it is not allowed and will be rejected." ), "input_schema": { "type": "object", "properties": { "evaluation_previous": { "type": "string", "description": ( "OPTIONAL. Only when the previous action SURPRISED you (failed, " "landed somewhere unexpected): say what happened and why, briefly. " "Omit entirely when the attached page state already shows the outcome." ), }, "working_memory": { "type": "string", "description": ( "Short notes about what you've learned about this site so far; " "selectors that work, keyboard shortcuts, layout quirks, what " "you've tried that failed. Carry this forward across turns." ), }, "next_goal": { "type": "string", "description": ( "What you're trying to achieve with the action(s) you're about " "to take next. Be concrete." ), }, }, "required": ["working_memory", "next_goal"], }, }, { "name": "Done", "description": ( "Call this the moment the task is finished (or you've hit a wall you " "can't get past) to deliver your final reply to the user. This ends the " "run. Do NOT type a sentence to finish, always finish by calling Done." ), "input_schema": { "type": "object", "properties": { "message": { "type": "string", "description": ( "What the user reads, so write it like a quick text to a friend: " "what you did and the proof a person actually cares about (the name, " "the time, what's now on screen). One or two plain sentences. Use ZERO " "interface words, no 'button', 'box', 'textbox', 'composer', 'field', " "'element', index numbers, or coordinates, and don't mechanically repeat " "the task back. If you couldn't finish, say what's missing in that same " "plain voice and set success to false." ), }, "success": { "type": "boolean", "description": ( "true if you accomplished what the user asked, false if you couldn't " "(login wall, missing info, something blocked you). Default true." ), }, "keep_open": { "type": "boolean", "description": ( "Set true ONLY when the result IS the open page and the user will keep " "using it right now: a video or audio playing, a page you opened for them " "to read or watch, a download you started, or a place left ready for them " "to take over. The browser then stays put instead of closing. Leave false " "(default) for info tasks where you just look something up and report the " "answer back, since there's nothing left to keep on screen." ), }, }, "required": ["message"], }, }, { "name": "BrowserScreenshot", "description": ( "Capture a screenshot of the browser page. Returns the screenshot as a " "base64-encoded PNG image. Use this to see what is currently displayed. " "Elements from your last BrowserListInteractives come back with numbered " "colored boxes drawn on them, the same numbers you click with, so you can " "go straight from what you see to BrowserClickIndex." ), "input_schema": { "type": "object", "properties": { "annotate": { "type": "boolean", "description": ( "Default true. Pass false for a clean, unannotated shot, e.g. " "when capturing proof of a completed action." ), }, }, "required": [], }, }, { "name": "BrowserGetText", "description": ( "Get the visible text content of the browser page. Returns up to 15000 characters." ), "input_schema": { "type": "object", "properties": {}, "required": [], }, }, { "name": "BrowserExtract", "description": ( "Pull STRUCTURED data off the current page in one call: say what you " "want (and optionally the JSON shape) and a fast helper model reads " "the page text for you, returning just the JSON. Use this instead of " "BrowserGetText whenever you need specific fields (each result's name " "+ headline + URL, a listing's price/title/rating, table rows): the " "page's 15k chars stay out of your context, so it's faster and " "cheaper than reading it yourself. Read-only. If the data isn't on " "the page you get {\"not_found\": true}, never a guess." ), "input_schema": { "type": "object", "properties": { "instruction": { "type": "string", "description": ( "What to extract, concretely (e.g. 'every search result: " "full name, headline, profile URL')." ), }, "schema": { "type": "object", "description": "Optional JSON schema describing the exact output shape.", }, }, "required": ["instruction"], }, }, { "name": "BrowserSaveData", "description": ( "Save a LARGE dataset you've assembled on the page straight to a file. " "Use this for 'every comment / all N results / the full list' once you've " "collected it into a page variable: trying to return hundreds of rows in your " "reply TRUNCATES, so you'd otherwise waste turns chunking it. Give a JS " "expression that returns the data as a string (almost always " "JSON.stringify(window.__yourVar)) plus a filename; the whole dataset is " "written to the workspace and you get back just the file path, not the data. " "It only writes your own workspace file (never the web). Put the returned path " "in your Done message so the user knows where it landed." ), "input_schema": { "type": "object", "properties": { "expression": { "type": "string", "description": ( "JS that returns the data as a string, e.g. " "JSON.stringify(window.__rows) or a CSV string you build." ), }, "filename": { "type": "string", "description": ( "Plain data filename, e.g. results.json or comments.csv " "(allowed: .json .ndjson .csv .tsv .txt .md)." ), }, }, "required": ["expression", "filename"], }, }, { "name": "BrowserGetConsole", "description": ( "Read the page's OWN recent JavaScript console warnings and errors " "(uncaught exceptions, failed resource loads like a 403/500, React " "errors). Use this when an action isn't working and the page looks " "fine: it tells you WHY the page is broken (an API call failed, the " "app crashed) so you fix the real cause instead of retrying blindly. " "Read-only; returns nothing if the page logged no warnings or errors." ), "input_schema": { "type": "object", "properties": {}, "required": [], }, }, { "name": "BrowserNavigate", "description": "Navigate the browser to a URL.", "input_schema": { "type": "object", "properties": { "url": {"type": "string", "description": "The URL to navigate to."}, }, "required": ["url"], }, }, { "name": "BrowserClick", "description": "Click an element identified by a CSS selector. Use BrowserGetElements first to discover valid selectors.", "input_schema": { "type": "object", "properties": { "selector": {"type": "string", "description": "CSS selector of the element to click."}, "expect": P_EXPECT_DESC, }, "required": ["selector"], }, }, { "name": "BrowserType", "description": "Type text into an input element. Clears existing value first.", "input_schema": { "type": "object", "properties": { "selector": {"type": "string", "description": "CSS selector of the input element."}, "text": {"type": "string", "description": "The text to type."}, }, "required": ["selector", "text"], }, }, { "name": "BrowserEvaluate", "description": "Evaluate a JavaScript expression in the browser page and return the result.", "input_schema": { "type": "object", "properties": { "expression": {"type": "string", "description": "JavaScript expression to evaluate."}, }, "required": ["expression"], }, }, { "name": "BrowserGetElements", "description": ( "Get a list of interactive elements on the page with CSS selectors. " "Call this BEFORE clicking or typing so you know which selectors are valid." ), "input_schema": { "type": "object", "properties": { "selector": { "type": "string", "description": "Optional CSS selector to scope the search (e.g. 'form', '#main'). Defaults to 'body'.", }, }, "required": [], }, }, { "name": "BrowserScroll", "description": ( "Scroll the page up or down. Automatically finds the correct scrollable " "container (works on SPAs like Notion, Gmail, etc. that use nested scroll " "containers instead of window-level scrolling). Returns scroll position info " "including whether top/bottom has been reached." ), "input_schema": { "type": "object", "properties": { "direction": { "type": "string", "enum": ["up", "down"], "description": "Scroll direction. Defaults to 'down'.", }, "amount": { "type": "number", "description": "Pixels to scroll. Defaults to 500.", }, }, "required": [], }, }, { "name": "BrowserListInteractives", "description": ( "Get a NUMBERED LIST of interactive elements on the page using the " "browser's accessibility tree. Returns elements like [1]