From c4a454429b36940f40eafccad6d136d5eb45fc8f Mon Sep 17 00:00:00 2001 From: Anderson Martinez Date: Thu, 23 Jul 2026 16:27:02 -0500 Subject: [PATCH 1/4] feat: add support for self-reported prompts in session commands --- src/tools/agent.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/tools/agent.ts b/src/tools/agent.ts index abb58b9..5b16224 100644 --- a/src/tools/agent.ts +++ b/src/tools/agent.ts @@ -678,6 +678,7 @@ export function registerAgentTools( // else the first URL seen this batch — so [goto A, goto B, snapshot] // still detects the A→snapshot cross-origin transition. let crossOriginBaseline: string | undefined = agentSession.lastUrl; + let promptSent = false; for (const cmd of commands) { if (cmd.method === 'close') { closeSession( @@ -706,6 +707,12 @@ export function registerAgentTools( outboundParams = { ...cmd.params }; delete (outboundParams as Record).toDisk; } + // Cascade the self-reported prompt once per session so the server can + // author a first-party skill from the run (server keeps the first). + if (prompt && !promptSent) { + outboundParams = { ...outboundParams, _prompt: prompt }; + promptSent = true; + } let resp; try { From 29630f90fcaefd5bc92ca0bc0d9d93a54dc61c49 Mon Sep 17 00:00:00 2001 From: Anderson Martinez Date: Thu, 23 Jul 2026 16:38:20 -0500 Subject: [PATCH 2/4] feat: add reporting for loaded site recipes in system prompt --- src/skills/system-prompt.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/skills/system-prompt.ts b/src/skills/system-prompt.ts index 20119b4..d82be09 100644 --- a/src/skills/system-prompt.ts +++ b/src/skills/system-prompt.ts @@ -12,6 +12,7 @@ export const AGENT_SYSTEM_PROMPT = `READ CAREFULLY: Execute browser commands in ## Site recipes (site-specific, NOT auto-injected) — CHECK FIRST Many specific sites (marketplaces, gov portals, travel, real-estate, etc.) have a **tuned recipe** for a given task — proven selectors, API shortcuts, proxy needs, and known gotchas that a from-scratch plan will miss. These are **not** auto-injected; you must ask for them, and a recipe **overrides** any plan you'd build yourself (including "just use a prefiltered URL + evaluate"). **This is step 0 of every task — do it before your first \`goto\`.** The moment you know the target host (the user named the site, or you resolved which site to use), call \`browserless_skill { site: "" }\` — e.g. \`{ site: "airbnb.com" }\`. If it lists a recipe matching your task, load it with \`browserless_skill { id: "/" }\` and follow it. Only when there's no match do you plan the steps yourself. Skipping this check on a supported site is a mistake — it's one cheap call. +**Report the outcome (only if you loaded a site recipe).** As your final command in the run, send \`{ method: "reportSkillOutcome", params: { domain: "", task: "", success: } }\` inside \`commands\` — where \`domain\`/\`task\` are the loaded recipe's \`\`/\`\` and \`success\` is whether the recipe actually got you the result. This refines shared recipes and retires ones that stop working. Send it once, and only when you loaded a recipe — never for a self-planned run. ## Proxy (optional) Proxy config is a **top-level tool argument** (\`proxy\`, \`proxyCountry\`, etc. on the tool call itself) — it is applied when the session is opened. **NEVER call \`proxy\` as a method inside \`commands\`** — a \`{ method: "proxy", ... }\` JSON-RPC mutation does NOT change the upstream proxy on an already-open session and will silently no-op. From 8ee279a9d2ce950e996d0256b5cf550aba900bc3 Mon Sep 17 00:00:00 2001 From: Anderson Martinez Date: Fri, 24 Jul 2026 09:11:27 -0500 Subject: [PATCH 3/4] feat: forward origin tag for skill attribution in ws connections --- src/@types/types.d.ts | 3 +++ src/lib/agent-client.ts | 13 ++++++++++++- src/tools/agent.ts | 2 ++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/@types/types.d.ts b/src/@types/types.d.ts index 1f6e9ee..d511330 100644 --- a/src/@types/types.d.ts +++ b/src/@types/types.d.ts @@ -195,6 +195,9 @@ export interface ActiveSession { // The creation session id returned by POST /profile. Reconnects attach to it // via /chromium/agent?sessionId rather than launching a new browser. creationSessionId?: string; + // Origin tag forwarded to the browser WS as `x-browserless-mcp-source` so the + // server can attribute captured skills (mcp_client, cli_agent, …). + readonly source?: string; reconnecting?: Promise; skillState: SkillFireState; lastUsedAt: number; diff --git a/src/lib/agent-client.ts b/src/lib/agent-client.ts index 5b7b303..9605e89 100644 --- a/src/lib/agent-client.ts +++ b/src/lib/agent-client.ts @@ -486,6 +486,7 @@ const connect = ( profile?: string, sessionId?: string, compliant = false, + source?: string, ): Promise => new Promise((resolve, reject) => { const wsUrl = buildAgentWsUrl( @@ -496,7 +497,12 @@ const connect = ( sessionId, compliant, ); - const ws = new WebSocket(wsUrl); + // Forward the origin on the upgrade so the server can attribute captured + // skills; reuses the same header the MCP already receives on its inbound. + const ws = new WebSocket( + wsUrl, + source ? { headers: { 'x-browserless-mcp-source': source } } : undefined, + ); let settled = false; const settle = (err: Error | null, value?: WebSocket): void => { @@ -621,6 +627,7 @@ export const getOrCreateSession = async ( createProfile?: CreateProfileParams, attachSessionId?: string, compliant = false, + source?: string, ): Promise => { sweepSessions(); const key = getSessionKey( @@ -673,6 +680,7 @@ export const getOrCreateSession = async ( profile, creationSessionId, compliant, + source, ); const session: ActiveSession = { ws, @@ -683,6 +691,7 @@ export const getOrCreateSession = async ( profile, createProfile, creationSessionId, + source, skillState: createSkillState(), lastUsedAt: Date.now(), }; @@ -732,6 +741,8 @@ export const send = async ( session.proxy, session.profile, session.creationSessionId, + false, + session.source, ).finally(() => { session.reconnecting = undefined; }); diff --git a/src/tools/agent.ts b/src/tools/agent.ts index 5b16224..ed6b350 100644 --- a/src/tools/agent.ts +++ b/src/tools/agent.ts @@ -628,6 +628,7 @@ export function registerAgentTools( createProfile, attachSessionId, compliant, + mcpSource.source, ); } catch (connErr: unknown) { sendAnalytics(false); @@ -652,6 +653,7 @@ export function registerAgentTools( createProfile, attachSessionId, compliant, + mcpSource.source, ); } catch (connErr: unknown) { // No retry when the server gave a definitive 4xx — re-attempting From a2b99853123bf82ea8d4edd4075cd62bf2708e27 Mon Sep 17 00:00:00 2001 From: Anderson Martinez Date: Fri, 24 Jul 2026 15:09:01 -0500 Subject: [PATCH 4/4] feat: update session handling logic --- src/@types/types.d.ts | 1 + src/lib/agent-client.ts | 9 +++++++-- src/skills/system-prompt.ts | 2 +- src/tools/agent.ts | 18 ++++++++++++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/@types/types.d.ts b/src/@types/types.d.ts index d511330..75ffcbe 100644 --- a/src/@types/types.d.ts +++ b/src/@types/types.d.ts @@ -198,6 +198,7 @@ export interface ActiveSession { // Origin tag forwarded to the browser WS as `x-browserless-mcp-source` so the // server can attribute captured skills (mcp_client, cli_agent, …). readonly source?: string; + readonly compliant: boolean; reconnecting?: Promise; skillState: SkillFireState; lastUsedAt: number; diff --git a/src/lib/agent-client.ts b/src/lib/agent-client.ts index 9605e89..f71293c 100644 --- a/src/lib/agent-client.ts +++ b/src/lib/agent-client.ts @@ -640,7 +640,11 @@ export const getOrCreateSession = async ( ); const existing = sessions.get(key); - if (existing && existing.ws.readyState === WebSocket.OPEN) { + if ( + existing && + existing.ws.readyState === WebSocket.OPEN && + existing.source === source + ) { existing.lastUsedAt = Date.now(); return existing; } @@ -692,6 +696,7 @@ export const getOrCreateSession = async ( createProfile, creationSessionId, source, + compliant, skillState: createSkillState(), lastUsedAt: Date.now(), }; @@ -741,7 +746,7 @@ export const send = async ( session.proxy, session.profile, session.creationSessionId, - false, + session.compliant, session.source, ).finally(() => { session.reconnecting = undefined; diff --git a/src/skills/system-prompt.ts b/src/skills/system-prompt.ts index d82be09..30098c6 100644 --- a/src/skills/system-prompt.ts +++ b/src/skills/system-prompt.ts @@ -12,7 +12,7 @@ export const AGENT_SYSTEM_PROMPT = `READ CAREFULLY: Execute browser commands in ## Site recipes (site-specific, NOT auto-injected) — CHECK FIRST Many specific sites (marketplaces, gov portals, travel, real-estate, etc.) have a **tuned recipe** for a given task — proven selectors, API shortcuts, proxy needs, and known gotchas that a from-scratch plan will miss. These are **not** auto-injected; you must ask for them, and a recipe **overrides** any plan you'd build yourself (including "just use a prefiltered URL + evaluate"). **This is step 0 of every task — do it before your first \`goto\`.** The moment you know the target host (the user named the site, or you resolved which site to use), call \`browserless_skill { site: "" }\` — e.g. \`{ site: "airbnb.com" }\`. If it lists a recipe matching your task, load it with \`browserless_skill { id: "/" }\` and follow it. Only when there's no match do you plan the steps yourself. Skipping this check on a supported site is a mistake — it's one cheap call. -**Report the outcome (only if you loaded a site recipe).** As your final command in the run, send \`{ method: "reportSkillOutcome", params: { domain: "", task: "", success: } }\` inside \`commands\` — where \`domain\`/\`task\` are the loaded recipe's \`\`/\`\` and \`success\` is whether the recipe actually got you the result. This refines shared recipes and retires ones that stop working. Send it once, and only when you loaded a recipe — never for a self-planned run. +**Report the outcome (only if you loaded a site recipe).** As your final command in the run, send \`{ method: "reportSkillOutcome", params: { domain: "", task: "", success: } }\` inside \`commands\` — where \`domain\`/\`task\` are the loaded recipe's \`\`/\`\` and \`success\` is whether the recipe actually got you the result. This refines shared recipes and retires ones that stop working. Send it once, and only when you loaded a recipe — never for a self-planned run. Send it as your last command **before** any \`close\` (close ends the run and anything after it is dropped). ## Proxy (optional) Proxy config is a **top-level tool argument** (\`proxy\`, \`proxyCountry\`, etc. on the tool call itself) — it is applied when the session is opened. **NEVER call \`proxy\` as a method inside \`commands\`** — a \`{ method: "proxy", ... }\` JSON-RPC mutation does NOT change the upstream proxy on an already-open session and will silently no-op. diff --git a/src/tools/agent.ts b/src/tools/agent.ts index ed6b350..58d9204 100644 --- a/src/tools/agent.ts +++ b/src/tools/agent.ts @@ -695,6 +695,14 @@ export function registerAgentTools( closedDuringBatch = true; break; } + if (cmd.method === 'reportSkillOutcome') { + try { + await send(agentSession, cmd.method, cmd.params); + } catch { + // noop + } + continue; + } log.info(`agent: ${cmd.method} ${JSON.stringify(cmd.params)}`); @@ -822,6 +830,16 @@ export function registerAgentTools( // If the batch ended with close, format the result around the // command before close (close itself has no useful payload). const reportable = closedDuringBatch ? results.slice(0, -1) : results; + // Nothing user-facing ran (batch was only close and/or an internal + // reportSkillOutcome) — the deref below would throw, so short-circuit. + if (reportable.length === 0) { + return [ + { + type: 'text' as const, + text: closedDuringBatch ? 'Browser session closed.' : 'Done.', + }, + ]; + } const last = reportable[reportable.length - 1]; const lastResult = last.result as Record; const lastCmd = commands[reportable.length - 1];