experimental-ash 0.22.0 → 0.22.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/docs/public/sandbox.md +25 -0
  3. package/dist/src/chunks/{dev-authored-source-watcher-DKDaaPea.js → dev-authored-source-watcher-BLzYWh05.js} +1 -1
  4. package/dist/src/chunks/host-DREC8e8Z.js +65 -0
  5. package/dist/src/chunks/{paths-DZTgjrW-.js → paths-C6sp4T2U.js} +25 -25
  6. package/dist/src/chunks/{prewarm-BELT37PI.js → prewarm-hz8p2jlZ.js} +1 -1
  7. package/dist/src/cli/commands/info.js +1 -1
  8. package/dist/src/cli/run.js +1 -1
  9. package/dist/src/evals/cli/eval.js +1 -1
  10. package/dist/src/execution/sandbox/bindings/vercel.d.ts +1 -1
  11. package/dist/src/execution/sandbox/bindings/vercel.js +38 -6
  12. package/dist/src/harness/action-result-helpers.d.ts +9 -6
  13. package/dist/src/harness/action-result-helpers.js +23 -16
  14. package/dist/src/harness/model-call-error.d.ts +16 -0
  15. package/dist/src/harness/model-call-error.js +71 -0
  16. package/dist/src/harness/provider-tools.d.ts +33 -2
  17. package/dist/src/harness/provider-tools.js +81 -0
  18. package/dist/src/harness/step-hooks.d.ts +21 -0
  19. package/dist/src/harness/step-hooks.js +7 -2
  20. package/dist/src/harness/tool-loop.js +284 -143
  21. package/dist/src/harness/tools.d.ts +12 -0
  22. package/dist/src/harness/tools.js +23 -5
  23. package/dist/src/internal/application/package.js +1 -1
  24. package/dist/src/internal/nitro/host/build-application.js +67 -1
  25. package/dist/src/internal/workflow-bundle/ash-service-route-output.d.ts +4 -0
  26. package/dist/src/internal/workflow-bundle/ash-service-route-output.js +134 -0
  27. package/dist/src/internal/workflow-bundle/vercel-workflow-output.d.ts +17 -0
  28. package/dist/src/internal/workflow-bundle/vercel-workflow-output.js +141 -1
  29. package/dist/src/public/definitions/connections/mcp.js +2 -0
  30. package/dist/src/public/definitions/tool.js +2 -0
  31. package/dist/src/public/next/index.js +7 -2
  32. package/dist/src/public/sandbox/backends/vercel.d.ts +7 -0
  33. package/dist/src/public/sandbox/backends/vercel.js +7 -0
  34. package/dist/src/public/sandbox/vercel-sandbox.d.ts +14 -4
  35. package/dist/src/public/tool-result-narrowing.d.ts +10 -7
  36. package/dist/src/public/tool-result-narrowing.js +42 -13
  37. package/dist/src/runtime/resolve-connection.js +5 -2
  38. package/dist/src/runtime/resolve-tool.js +5 -2
  39. package/package.json +1 -1
  40. package/dist/src/chunks/host-Btr4S69C.js +0 -22
@@ -1,4 +1,4 @@
1
- import{E as e,S as t,T as n,_ as r,a as i,c as a,d as o,g as s,h as c,l,m as u,p as ee,s as te,u as d,x as f,y as p}from"./paths-DZTgjrW-.js";import{t as m}from"./authored-module-loader-XcFLnl49.js";import{t as h}from"./errors-DsO9xmQL.js";import{i as g,t as _}from"./package-DmsQgn4v.js";import{join as v,posix as y}from"node:path";import{mkdir as ne,readFile as re,readdir as ie,realpath as b,writeFile as x}from"node:fs/promises";import{createHash as S}from"node:crypto";import{existsSync as C}from"node:fs";function w(e){return e.dev?{appRoot:e.appRoot,dev:e.dev,moduleMapLoaderPath:g(`src/internal/authored-module-map-loader.ts`)}:{appRoot:e.appRoot,dev:e.dev}}const T=`#ash-channel/`;function E(e){let t=e.compileResult.manifest.channels,n=new Set,r=[],i=new Set,o=a();for(let e of t){if(e.kind===`disabled`){if(!o.has(e.name))throw Error(`agent/channels/${e.name}.ts exports disableRoute() but "${e.name}" is not a framework channel. Rename the file to one of: ${[...o].sort().join(`, `)}.`);i.add(e.name);continue}n.add(e.name),r.push({method:e.method,route:e.urlPath})}let s=l().filter(e=>!n.has(e.name)&&!i.has(e.name)).map(e=>({method:e.method,route:e.urlPath})),c=new Set,u=[];for(let e of[...s,...r]){let t=k(e);c.has(t)||(c.add(t),u.push(e))}return u}function D(e,t){for(let n of t.registrations)A(e,{artifactsConfig:t.artifactsConfig,method:n.method,route:n.route})}function O(e,t){return N(t.previous,t.next)?!1:(j(e),D(e,{artifactsConfig:t.artifactsConfig,registrations:t.next}),e.routing.sync(),!0)}function k(e){return`${e.method.toUpperCase()} ${e.route}`}function A(e,t){let r=k(t),i=`${T}${r}`,a=n(g(`src/internal/nitro/routes/channel-dispatch.ts`));e.options.handlers.push({handler:i,method:t.method,route:t.route}),e.options.virtual[i]=[`import { dispatchChannelRequest } from ${a};`,`const config = ${JSON.stringify(t.artifactsConfig)};`,`export default (event) => dispatchChannelRequest(event, ${JSON.stringify(r)}, config);`].join(`
1
+ import{E as e,S as t,T as n,_ as r,a as i,c as a,d as o,g as s,h as c,l,m as u,p as ee,s as te,u as d,x as f,y as p}from"./paths-C6sp4T2U.js";import{t as m}from"./authored-module-loader-XcFLnl49.js";import{t as h}from"./errors-DsO9xmQL.js";import{i as g,t as _}from"./package-DmsQgn4v.js";import{join as v,posix as y}from"node:path";import{mkdir as ne,readFile as re,readdir as ie,realpath as b,writeFile as x}from"node:fs/promises";import{createHash as S}from"node:crypto";import{existsSync as C}from"node:fs";function w(e){return e.dev?{appRoot:e.appRoot,dev:e.dev,moduleMapLoaderPath:g(`src/internal/authored-module-map-loader.ts`)}:{appRoot:e.appRoot,dev:e.dev}}const T=`#ash-channel/`;function E(e){let t=e.compileResult.manifest.channels,n=new Set,r=[],i=new Set,o=a();for(let e of t){if(e.kind===`disabled`){if(!o.has(e.name))throw Error(`agent/channels/${e.name}.ts exports disableRoute() but "${e.name}" is not a framework channel. Rename the file to one of: ${[...o].sort().join(`, `)}.`);i.add(e.name);continue}n.add(e.name),r.push({method:e.method,route:e.urlPath})}let s=l().filter(e=>!n.has(e.name)&&!i.has(e.name)).map(e=>({method:e.method,route:e.urlPath})),c=new Set,u=[];for(let e of[...s,...r]){let t=k(e);c.has(t)||(c.add(t),u.push(e))}return u}function D(e,t){for(let n of t.registrations)A(e,{artifactsConfig:t.artifactsConfig,method:n.method,route:n.route})}function O(e,t){return N(t.previous,t.next)?!1:(j(e),D(e,{artifactsConfig:t.artifactsConfig,registrations:t.next}),e.routing.sync(),!0)}function k(e){return`${e.method.toUpperCase()} ${e.route}`}function A(e,t){let r=k(t),i=`${T}${r}`,a=n(g(`src/internal/nitro/routes/channel-dispatch.ts`));e.options.handlers.push({handler:i,method:t.method,route:t.route}),e.options.virtual[i]=[`import { dispatchChannelRequest } from ${a};`,`const config = ${JSON.stringify(t.artifactsConfig)};`,`export default (event) => dispatchChannelRequest(event, ${JSON.stringify(r)}, config);`].join(`
2
2
  `)}function j(e){for(let t=e.options.handlers.length-1;t>=0;--t){let n=e.options.handlers[t];n!==void 0&&M(n)&&e.options.handlers.splice(t,1)}for(let t of Object.keys(e.options.virtual))t.startsWith(T)&&delete e.options.virtual[t]}function M(e){return e.handler.startsWith(T)}function N(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1){let r=e[n],i=t[n];if(r===void 0||i===void 0||r.method!==i.method||r.route!==i.route)return!1}return!0}const P=`ash.schedule.`;var F=class extends Error{scheduleId;sourceId;taskName;constructor(e,t={}){super(e),this.name=`ScheduleRegistrationError`,t.scheduleId!==void 0&&(this.scheduleId=t.scheduleId),t.sourceId!==void 0&&(this.sourceId=t.sourceId),t.taskName!==void 0&&(this.taskName=t.taskName)}};function I(e){let t=e.map(e=>({cron:e.cron,description:`Run Ash schedule "${e.name}" from "${e.logicalPath}".`,logicalPath:e.logicalPath,scheduleId:e.name,sourceId:e.sourceId,taskName:R(e.sourceId)})).sort((e,t)=>e.sourceId.localeCompare(t.sourceId));return L(t),t}function L(e){let t=new Map;for(let n of e){let e=t.get(n.scheduleId);if(e===void 0){t.set(n.scheduleId,n);continue}throw new F(`Duplicate authored schedule id "${n.scheduleId}" found in "${e.logicalPath}" and "${n.logicalPath}".`,{scheduleId:n.scheduleId,sourceId:n.sourceId,taskName:n.taskName})}}function R(e){return`${P}${Buffer.from(e,`utf8`).toString(`base64url`)}`}const z=`#ash-schedule-task/`;function B(e,t){if(t.registrations.length!==0){e.options.experimental.tasks=!0;for(let n of t.registrations)U(e,{artifactsConfig:t.artifactsConfig,dispatchModulePath:t.dispatchModulePath,registration:n})}}function V(e,t){let n=!G(t.previous,t.next);return H(e),B(e,{artifactsConfig:t.artifactsConfig,dispatchModulePath:t.dispatchModulePath,registrations:t.next}),n}function H(e){for(let t of Object.keys(e.options.tasks))t.startsWith(`ash.schedule.`)&&delete e.options.tasks[t];for(let t of Object.keys(e.options.virtual))t.startsWith(z)&&delete e.options.virtual[t];for(let[t,n]of Object.entries(e.options.scheduledTasks)){let r=W(n).filter(e=>!e.startsWith(P));if(r.length===0){delete e.options.scheduledTasks[t];continue}if(r.length===1){let[n]=r;n!==void 0&&(e.options.scheduledTasks[t]=n);continue}e.options.scheduledTasks[t]=r}}function U(e,t){let r=`${z}${t.registration.taskName}`,i=n(t.dispatchModulePath);e.options.tasks[t.registration.taskName]={description:t.registration.description,handler:r},e.options.virtual[r]=[`import { dispatchScheduleTask } from ${i};`,`const config = ${JSON.stringify(t.artifactsConfig)};`,`export default {`,` meta: { description: ${JSON.stringify(t.registration.description)} },`,` async run(event) {`,` return { result: await dispatchScheduleTask(event.name, config) };`,` },`,`};`].join(`
3
3
  `),ae(e,t.registration.cron,t.registration.taskName)}function ae(e,t,n){let r=e.options.scheduledTasks[t];if(r===void 0){e.options.scheduledTasks[t]=n;return}if(typeof r==`string`){e.options.scheduledTasks[t]=[r,n];return}r.includes(n)||r.push(n)}function W(e){return typeof e==`string`?[e]:[...e]}function G(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1){let r=e[n],i=t[n];if(r===void 0||i===void 0||r.cron!==i.cron||r.description!==i.description||r.logicalPath!==i.logicalPath||r.scheduleId!==i.scheduleId||r.sourceId!==i.sourceId||r.taskName!==i.taskName)return!1}return!0}async function K(e){return[...e.manifest.schedules].map(e=>{let t={cron:e.cron,hasRun:e.hasRun,logicalPath:e.logicalPath,name:e.name,sourceId:e.sourceId,sourceKind:e.sourceKind};return e.markdown===void 0?t:{...t,markdown:e.markdown}})}async function q(e){return await K({manifest:await o({compiledArtifactsSource:e.compiledArtifactsSource})})}async function J(e){let t=v(e.outDir,`compiled-artifacts-bootstrap.mjs`),n=v(e.outDir,`compiled-artifacts-instrumentation.mjs`),r=se(e.compileResult.manifest.agentRoot);await ne(e.outDir,{recursive:!0}),await x(t,await le({compileResult:e.compileResult,installModulePath:g(`src/runtime/loaders/bundled-artifacts.ts`),moduleMapPath:t,metadata:e.compileResult.metadata})),r!==void 0&&await x(n,ue({agentName:e.compileResult.manifest.config.name,instrumentationPath:r,registerConfigPath:g(`src/harness/instrumentation-config.ts`)}));let i={bootstrapPath:t};return r!==void 0&&(i.instrumentationPluginPath=n,i.instrumentationSourcePath=r),i}const oe=[`.ts`,`.mts`,`.js`,`.mjs`];function se(e){for(let t of oe){let n=v(e,`instrumentation${t}`);if(C(n))return n}}function ce(e){return e.replace(/^export const moduleMap = /m,`const moduleMap = `).replace(/\nexport default moduleMap;\n?$/,`
4
4
  `)}async function le(e){let r=ce(t({importSpecifierStyle:`absolute`,manifest:e.compileResult.manifest,moduleMapPath:e.moduleMapPath})).trim();return[`// Generated by Ash. Do not edit by hand.`,`import { installBundledCompiledArtifacts } from ${n(e.installModulePath)};`,``,r,``,`const metadata = ${JSON.stringify(e.metadata,null,2)};`,``,`const manifest = ${JSON.stringify(e.compileResult.manifest,null,2)};`,``,`export function installCompiledArtifactsBootstrap() {`,` installBundledCompiledArtifacts({`,` manifest,`,` metadata,`,` moduleMap,`,` });`,`}`,``,`installCompiledArtifactsBootstrap();`,``,`// Default export satisfies the Nitro plugin contract so this file`,`// can be used directly as a Nitro plugin without a separate wrapper.`,`export default function installCompiledArtifactsPlugin() {`,` // Already installed on import above.`,`}`,``,`export async function __ashInstallCompiledArtifactsStep() {`,` "use step";`,` return null;`,`}`,``].join(`
@@ -1,2 +1,2 @@
1
- import{D as e,b as t,t as n,v as r,y as i}from"../../chunks/paths-DZTgjrW-.js";import{d as a,f as o,h as s}from"../../chunks/types-MZUhN0Zy.js";import{createCliTheme as c,renderCliBanner as l,renderCliSection as u}from"../ui/output.js";async function d(e){let t=await f(e);return{application:n(t?.project.appRoot??e),compiledState:t,messaging:{createSessionRoutePath:o,continueSessionRoutePattern:a,streamRoutePattern:s}}}async function f(n){try{return await i({startPath:n})}catch(n){if(n instanceof r)return n.result;if(n instanceof e||n instanceof t)return null;throw n}}function p(e,t){return`${e} ${t}${e===1?``:`s`}`}function m(e,t){return`${`${e} error${e===1?``:`s`}`}, ${`${t} warning${t===1?``:`s`}`}`}function h(e){switch(e){case`ready`:return`success`;case`failed`:return`danger`;default:return`warning`}}async function g(e,t){let n=await d(t),r=n.compiledState,i=n.application,a=c(),o=[{label:`App Root`,value:i.appRoot}],s=[{label:`Workflow Build`,value:i.workflowBuildDir},{label:`Output`,value:i.outputDir}],f=[];r===null?o.push({label:`Compile`,tone:`warning`,value:`unavailable`}):(o.push({label:`Agent Root`,value:r.project.agentRoot},{label:`Layout`,value:r.project.layout},{label:`Compile`,tone:h(r.metadata.status),value:r.metadata.status},{label:`Diagnostics`,tone:r.metadata.discovery.summary.errors>0?`danger`:r.metadata.discovery.summary.warnings>0?`warning`:`success`,value:m(r.metadata.discovery.summary.errors,r.metadata.discovery.summary.warnings)},{label:`Instructions`,value:r.manifest.instructions?.logicalPath??`none`},{label:`Skills`,value:p(r.manifest.skills.length,`skill`)}),s.unshift({label:`Compiled Manifest`,value:r.paths.compiledManifestPath},{label:`Discovery Manifest`,value:r.paths.discoveryManifestPath},{label:`Diagnostics`,value:r.paths.diagnosticsPath},{label:`Module Map`,value:r.paths.moduleMapPath},{label:`Metadata`,value:r.paths.compileMetadataPath}),f.push(r.manifest.instructions===void 0?{label:`Instructions`,value:`No instructions prompt discovered.`}:{label:`Instructions`,value:r.manifest.instructions.logicalPath})),e.log([l(a,{subtitle:`Resolved application paths and the active message contract.`,title:`Ash Info`}),``,u(a,{rows:o,title:`Application`}),``,u(a,{rows:s,title:`Artifacts`}),...r===null?[]:[``,u(a,{rows:f,title:`Instructions`})],``,u(a,{rows:[{label:`Workflow ID`,value:i.workflowId},{label:`Source Dir`,value:i.workflowSourceDir},{label:`Create`,tone:`info`,value:`POST ${n.messaging.createSessionRoutePath}`},{label:`Continue`,tone:`info`,value:`POST ${n.messaging.continueSessionRoutePattern}`},{label:`Stream`,tone:`info`,value:`GET ${n.messaging.streamRoutePattern}`}],title:`Messaging`})].join(`
1
+ import{D as e,b as t,t as n,v as r,y as i}from"../../chunks/paths-C6sp4T2U.js";import{d as a,f as o,h as s}from"../../chunks/types-MZUhN0Zy.js";import{createCliTheme as c,renderCliBanner as l,renderCliSection as u}from"../ui/output.js";async function d(e){let t=await f(e);return{application:n(t?.project.appRoot??e),compiledState:t,messaging:{createSessionRoutePath:o,continueSessionRoutePattern:a,streamRoutePattern:s}}}async function f(n){try{return await i({startPath:n})}catch(n){if(n instanceof r)return n.result;if(n instanceof e||n instanceof t)return null;throw n}}function p(e,t){return`${e} ${t}${e===1?``:`s`}`}function m(e,t){return`${`${e} error${e===1?``:`s`}`}, ${`${t} warning${t===1?``:`s`}`}`}function h(e){switch(e){case`ready`:return`success`;case`failed`:return`danger`;default:return`warning`}}async function g(e,t){let n=await d(t),r=n.compiledState,i=n.application,a=c(),o=[{label:`App Root`,value:i.appRoot}],s=[{label:`Workflow Build`,value:i.workflowBuildDir},{label:`Output`,value:i.outputDir}],f=[];r===null?o.push({label:`Compile`,tone:`warning`,value:`unavailable`}):(o.push({label:`Agent Root`,value:r.project.agentRoot},{label:`Layout`,value:r.project.layout},{label:`Compile`,tone:h(r.metadata.status),value:r.metadata.status},{label:`Diagnostics`,tone:r.metadata.discovery.summary.errors>0?`danger`:r.metadata.discovery.summary.warnings>0?`warning`:`success`,value:m(r.metadata.discovery.summary.errors,r.metadata.discovery.summary.warnings)},{label:`Instructions`,value:r.manifest.instructions?.logicalPath??`none`},{label:`Skills`,value:p(r.manifest.skills.length,`skill`)}),s.unshift({label:`Compiled Manifest`,value:r.paths.compiledManifestPath},{label:`Discovery Manifest`,value:r.paths.discoveryManifestPath},{label:`Diagnostics`,value:r.paths.diagnosticsPath},{label:`Module Map`,value:r.paths.moduleMapPath},{label:`Metadata`,value:r.paths.compileMetadataPath}),f.push(r.manifest.instructions===void 0?{label:`Instructions`,value:`No instructions prompt discovered.`}:{label:`Instructions`,value:r.manifest.instructions.logicalPath})),e.log([l(a,{subtitle:`Resolved application paths and the active message contract.`,title:`Ash Info`}),``,u(a,{rows:o,title:`Application`}),``,u(a,{rows:s,title:`Artifacts`}),...r===null?[]:[``,u(a,{rows:f,title:`Instructions`})],``,u(a,{rows:[{label:`Workflow ID`,value:i.workflowId},{label:`Source Dir`,value:i.workflowSourceDir},{label:`Create`,tone:`info`,value:`POST ${n.messaging.createSessionRoutePath}`},{label:`Continue`,tone:`info`,value:`POST ${n.messaging.continueSessionRoutePattern}`},{label:`Stream`,tone:`info`,value:`GET ${n.messaging.streamRoutePattern}`}],title:`Messaging`})].join(`
2
2
  `))}export{g as printApplicationInfo};
@@ -1,3 +1,3 @@
1
- import{t as e}from"../chunks/package-DmsQgn4v.js";import{createCliTheme as t,renderCliTaggedLine as n}from"./ui/output.js";import{i as r,n as i,r as a,t as o}from"../chunks/url-BVRhVE2O.js";import{resolve as s}from"node:path";async function c(){return(await import(`../chunks/host-Btr4S69C.js`).then(e=>e.t)).buildHost}async function l(){return(await import(`./commands/info.js`)).printApplicationInfo}async function u(){return(await import(`./dev/repl.js`)).runDevelopmentRepl}async function d(){return(await import(`../evals/cli/eval.js`)).runEvalCommand}async function f(){return(await import(`../chunks/host-Btr4S69C.js`).then(e=>e.t)).startHost}function p(e=process.cwd()){return s(e)}function m(e){return`Ash (v${e})`}function h(e){return e.name()===`info`||e.name()===`dev`}async function g(e){await new Promise((t,n)=>{let r=!1,i=()=>{process.off(`SIGINT`,a),process.off(`SIGTERM`,a)},a=()=>{r||(r=!0,i(),e.close().then(t,n))};process.once(`SIGINT`,a),process.once(`SIGTERM`,a)})}function _(e){if(!/^-?\d+$/.test(e))throw new r(`Expected a numeric port, received "${e}".`);let t=Number(e);if(!Number.isInteger(t))throw new r(`Expected a numeric port, received "${e}".`);if(t<0||t>65535)throw new r(`Expected a port between 0 and 65535, received "${e}".`);return t}function v(){return!!(process.stdin.isTTY&&process.stdout.isTTY)}function y(e){let t=e[1];return e[0]!==`dev`||e.length!==2||t===void 0||t.startsWith(`-`)?[...e]:[`dev`,`--url`,t]}function b(e){if(e.url){if(e.host!==void 0)throw new r(`The --host option cannot be used with --url.`);if(e.port!==void 0)throw new r(`The --port option cannot be used with --url.`);if(e.repl===!1)throw new r(`The --no-repl option cannot be used with --url.`);return e.url}}function x(r,a){let s=p(),y=e().version,x=new i,S=t();return x.name(`ash`).description(`Build and run an Ash application.`).version(y).showHelpAfterError().exitOverride().hook(`preAction`,(e,t)=>{h(t)&&r.log(m(y))}).configureOutput({writeErr:e=>{r.error(e.trimEnd())},writeOut:e=>{r.log(e.trimEnd())}}),x.command(`build`).description(`Build the current Ash application.`).action(async()=>{let{loadDevelopmentEnvironmentFiles:e}=await import(`./dev/environment.js`);e(s);let t=await(a.buildHost??await c())(s);r.log(n(S,{message:`built output at ${t}`,tag:`build`,tone:`success`}))}),x.command(`dev`).description(`Start the Ash development server or connect the REPL to an existing URL.`).option(`--host <host>`,`Host interface to bind`).option(`--no-repl`,`Start the server without the interactive REPL`).option(`--port <port>`,`Port to listen on (defaults to $PORT, then 3000)`,_).option(`--schedules`,`Run scheduled tasks during development (off by default)`).option(`-u, --url <url>`,`Connect the REPL to an existing server URL`,o).addHelpText(`after`,`
1
+ import{t as e}from"../chunks/package-DmsQgn4v.js";import{createCliTheme as t,renderCliTaggedLine as n}from"./ui/output.js";import{i as r,n as i,r as a,t as o}from"../chunks/url-BVRhVE2O.js";import{resolve as s}from"node:path";async function c(){return(await import(`../chunks/host-DREC8e8Z.js`).then(e=>e.t)).buildHost}async function l(){return(await import(`./commands/info.js`)).printApplicationInfo}async function u(){return(await import(`./dev/repl.js`)).runDevelopmentRepl}async function d(){return(await import(`../evals/cli/eval.js`)).runEvalCommand}async function f(){return(await import(`../chunks/host-DREC8e8Z.js`).then(e=>e.t)).startHost}function p(e=process.cwd()){return s(e)}function m(e){return`Ash (v${e})`}function h(e){return e.name()===`info`||e.name()===`dev`}async function g(e){await new Promise((t,n)=>{let r=!1,i=()=>{process.off(`SIGINT`,a),process.off(`SIGTERM`,a)},a=()=>{r||(r=!0,i(),e.close().then(t,n))};process.once(`SIGINT`,a),process.once(`SIGTERM`,a)})}function _(e){if(!/^-?\d+$/.test(e))throw new r(`Expected a numeric port, received "${e}".`);let t=Number(e);if(!Number.isInteger(t))throw new r(`Expected a numeric port, received "${e}".`);if(t<0||t>65535)throw new r(`Expected a port between 0 and 65535, received "${e}".`);return t}function v(){return!!(process.stdin.isTTY&&process.stdout.isTTY)}function y(e){let t=e[1];return e[0]!==`dev`||e.length!==2||t===void 0||t.startsWith(`-`)?[...e]:[`dev`,`--url`,t]}function b(e){if(e.url){if(e.host!==void 0)throw new r(`The --host option cannot be used with --url.`);if(e.port!==void 0)throw new r(`The --port option cannot be used with --url.`);if(e.repl===!1)throw new r(`The --no-repl option cannot be used with --url.`);return e.url}}function x(r,a){let s=p(),y=e().version,x=new i,S=t();return x.name(`ash`).description(`Build and run an Ash application.`).version(y).showHelpAfterError().exitOverride().hook(`preAction`,(e,t)=>{h(t)&&r.log(m(y))}).configureOutput({writeErr:e=>{r.error(e.trimEnd())},writeOut:e=>{r.log(e.trimEnd())}}),x.command(`build`).description(`Build the current Ash application.`).action(async()=>{let{loadDevelopmentEnvironmentFiles:e}=await import(`./dev/environment.js`);e(s);let t=await(a.buildHost??await c())(s);r.log(n(S,{message:`built output at ${t}`,tag:`build`,tone:`success`}))}),x.command(`dev`).description(`Start the Ash development server or connect the REPL to an existing URL.`).option(`--host <host>`,`Host interface to bind`).option(`--no-repl`,`Start the server without the interactive REPL`).option(`--port <port>`,`Port to listen on (defaults to $PORT, then 3000)`,_).option(`--schedules`,`Run scheduled tasks during development (off by default)`).option(`-u, --url <url>`,`Connect the REPL to an existing server URL`,o).addHelpText(`after`,`
2
2
  You can also pass a bare URL as the only argument, for example: ash dev https://example.com
3
3
  `).action(async e=>{let t=b(e),{loadDevelopmentEnvironmentFiles:i}=await import(`./dev/environment.js`);if(i(s),t){if(r.log(n(S,{message:`REPL connecting to ${t}`,tag:`dev`,tone:`info`})),!v()){r.log(n(S,{message:`Interactive REPL disabled because the current terminal is not a TTY.`,tag:`dev`,tone:`warning`}));return}r.log(``),await(a.runDevelopmentRepl??await u())({serverUrl:t});return}let o=await(a.startHost??await f())(s,{host:e.host,port:e.port,schedules:e.schedules===!0}),c=!1,l=async()=>{c||(c=!0,await o.close())};try{if(r.log(n(S,{message:`server listening at ${o.url}`,tag:`dev`,tone:`success`})),e.repl===!1)return await g({close:l});if(!v())return r.log(n(S,{message:`Interactive REPL disabled because the current terminal is not a TTY.`,tag:`dev`,tone:`warning`})),await g({close:l});r.log(``),await(a.runDevelopmentRepl??await u())({serverUrl:o.url})}finally{await l()}}),x.command(`info`).description(`Print resolved application information.`).action(async()=>{await(a.printApplicationInfo??await l())(r,s)}),x.command(`eval`).description(`Run eval suites against an Ash agent.`).option(`--suite <id...>`,`Suite IDs to run (repeatable)`).option(`--all`,`Run all discovered suites`).option(`--url <url>`,`Remote agent URL (skip local host startup)`).option(`--timeout <ms>`,`Per-case timeout in milliseconds`).option(`--max-concurrency <n>`,`Max concurrent case executions per suite`).option(`--json`,`Output results as JSON`).option(`--list-suites`,`List discovered suites and exit`).option(`--skip-report`,`Skip suite-defined reporters (e.g. Braintrust)`).action(async e=>{await(a.runEvalCommand??await d())(e,r)}),x}async function S(e=process.argv.slice(2),t=console,n={}){let r=x(t,n),i=e.length===0?[`info`]:y(e);try{await r.parseAsync(i,{from:`user`})}catch(e){if(e instanceof a){if(e.exitCode===0)return;throw Error(e.message)}throw e}}export{S as runCli};
@@ -1 +1 @@
1
- import{n as e}from"../../chunks/paths-DZTgjrW-.js";import{loadDevelopmentEnvironmentFiles as t}from"../../cli/dev/environment.js";import{a as n,n as r,t as i}from"../../chunks/client-CKsU8Li3.js";import{n as a}from"../../chunks/host-Btr4S69C.js";import{discoverAndImportSuites as o,discoverSuiteFiles as s,importSuiteFile as c}from"../runner/discover.js";import{executeSuite as l}from"../runner/execute-suite.js";import{ConsoleReporter as u}from"../runner/reporters/console.js";var d=n();function f(e,t){e.command(`eval`).description(`Run eval suites against an Ash agent.`).option(`--suite <id...>`,`Suite IDs to run (repeatable)`).option(`--all`,`Run all discovered suites`).option(`--url <url>`,`Remote agent URL (skip local host startup)`).option(`--timeout <ms>`,`Per-case timeout in milliseconds`).option(`--max-concurrency <n>`,`Max concurrent case executions per suite`).option(`--json`,`Output results as JSON`).option(`--list-suites`,`List discovered suites and exit`).option(`--skip-report`,`Skip suite-defined reporters (e.g. Braintrust)`).action(async e=>{await p(e,t)})}async function p(n,r){let i=e();if(t(i),n.listSuites){await y(i,r);return}let s=n.suite,c=await o(i,s);if(c.length===0){s&&s.length>0?r.error(`No suites found matching: ${s.join(`, `)}`):r.error(`No eval suites found. Create suite files under evals/ with the *.eval.ts extension.`),process.exitCode=1;return}let u,d;n.url?d={kind:`remote`,url:n.url}:(u=await a(i,{host:`127.0.0.1`,port:0}),d={kind:`local`,url:u.url});let f=m(d);try{let e=[];for(let t of c){let r=_(t,n),a=v(r,{json:n.json===!0,skipReport:n.skipReport===!0}),o=await l({suite:r,target:d,reporters:a,appRoot:i,client:f});e.push(o)}n.json&&r.log(JSON.stringify(e,null,2)),e.some(e=>e.errored>0)&&(process.exitCode=1)}finally{u&&await u.close()}process.exit(process.exitCode??0)}function m(e){if(e.kind===`local`)return new i({host:e.url});let t={},n=process.env.VERCEL_AUTOMATION_BYPASS_SECRET?.trim();return n&&(t[r]=n),new i({auth:h(),headers:Object.keys(t).length>0?t:void 0,host:e.url})}function h(){let e=process.env.ASH_EVAL_AUTH_TOKEN?.trim();return e?{bearer:e}:{bearer:g}}async function g(){try{let e=(await(0,d.getVercelOidcToken)()).trim();if(e.length>0)return e}catch{}return process.env.VERCEL_OIDC_TOKEN?.trim()??``}function _(e,t){let n=t.maxConcurrency?Number.parseInt(t.maxConcurrency,10):void 0,r=t.timeout?Number.parseInt(t.timeout,10):void 0;if(n===void 0&&r===void 0)return e;let i={...e};return n!==void 0&&(i.maxConcurrency=n),r!==void 0&&(i.timeoutMs=r),i}function v(e,t){let n=t.json?[]:[new u];return!t.skipReport&&e.reporters&&n.push(...e.reporters),n}async function y(e,t){let n=await s(e);if(n.length===0){t.log(`No eval suites found.`);return}t.log(`Found ${n.length} eval suite file(s):\n`);for(let r of n){let n=await c(e,r);t.log(` ${n.id}${n.description?` - ${n.description}`:``}`)}}export{f as registerEvalCommand,p as runEvalCommand};
1
+ import{n as e}from"../../chunks/paths-C6sp4T2U.js";import{loadDevelopmentEnvironmentFiles as t}from"../../cli/dev/environment.js";import{a as n,n as r,t as i}from"../../chunks/client-CKsU8Li3.js";import{n as a}from"../../chunks/host-DREC8e8Z.js";import{discoverAndImportSuites as o,discoverSuiteFiles as s,importSuiteFile as c}from"../runner/discover.js";import{executeSuite as l}from"../runner/execute-suite.js";import{ConsoleReporter as u}from"../runner/reporters/console.js";var d=n();function f(e,t){e.command(`eval`).description(`Run eval suites against an Ash agent.`).option(`--suite <id...>`,`Suite IDs to run (repeatable)`).option(`--all`,`Run all discovered suites`).option(`--url <url>`,`Remote agent URL (skip local host startup)`).option(`--timeout <ms>`,`Per-case timeout in milliseconds`).option(`--max-concurrency <n>`,`Max concurrent case executions per suite`).option(`--json`,`Output results as JSON`).option(`--list-suites`,`List discovered suites and exit`).option(`--skip-report`,`Skip suite-defined reporters (e.g. Braintrust)`).action(async e=>{await p(e,t)})}async function p(n,r){let i=e();if(t(i),n.listSuites){await y(i,r);return}let s=n.suite,c=await o(i,s);if(c.length===0){s&&s.length>0?r.error(`No suites found matching: ${s.join(`, `)}`):r.error(`No eval suites found. Create suite files under evals/ with the *.eval.ts extension.`),process.exitCode=1;return}let u,d;n.url?d={kind:`remote`,url:n.url}:(u=await a(i,{host:`127.0.0.1`,port:0}),d={kind:`local`,url:u.url});let f=m(d);try{let e=[];for(let t of c){let r=_(t,n),a=v(r,{json:n.json===!0,skipReport:n.skipReport===!0}),o=await l({suite:r,target:d,reporters:a,appRoot:i,client:f});e.push(o)}n.json&&r.log(JSON.stringify(e,null,2)),e.some(e=>e.errored>0)&&(process.exitCode=1)}finally{u&&await u.close()}process.exit(process.exitCode??0)}function m(e){if(e.kind===`local`)return new i({host:e.url});let t={},n=process.env.VERCEL_AUTOMATION_BYPASS_SECRET?.trim();return n&&(t[r]=n),new i({auth:h(),headers:Object.keys(t).length>0?t:void 0,host:e.url})}function h(){let e=process.env.ASH_EVAL_AUTH_TOKEN?.trim();return e?{bearer:e}:{bearer:g}}async function g(){try{let e=(await(0,d.getVercelOidcToken)()).trim();if(e.length>0)return e}catch{}return process.env.VERCEL_OIDC_TOKEN?.trim()??``}function _(e,t){let n=t.maxConcurrency?Number.parseInt(t.maxConcurrency,10):void 0,r=t.timeout?Number.parseInt(t.timeout,10):void 0;if(n===void 0&&r===void 0)return e;let i={...e};return n!==void 0&&(i.maxConcurrency=n),r!==void 0&&(i.timeoutMs=r),i}function v(e,t){let n=t.json?[]:[new u];return!t.skipReport&&e.reporters&&n.push(...e.reporters),n}async function y(e,t){let n=await s(e);if(n.length===0){t.log(`No eval suites found.`);return}t.log(`Found ${n.length} eval suite file(s):\n`);for(let r of n){let n=await c(e,r);t.log(` ${n.id}${n.description?` - ${n.description}`:``}`)}}export{f as registerEvalCommand,p as runEvalCommand};
@@ -6,7 +6,7 @@ type VercelSandboxModule = typeof VercelSandboxSdk;
6
6
  /**
7
7
  * User-controllable subset of `Sandbox.create` parameters.
8
8
  */
9
- export type VercelSandboxCreateOptions = Omit<NonNullable<Parameters<typeof SdkSandbox.create>[0]>, "name" | "persistent" | "source" | "signal">;
9
+ export type VercelSandboxCreateOptions = Omit<NonNullable<Parameters<typeof SdkSandbox.create>[0]>, "name" | "persistent" | "signal">;
10
10
  /**
11
11
  * Construction input for {@link createVercelSandboxBackend}. Internal —
12
12
  * the public surface is the `vercelBackend()` factory under
@@ -115,7 +115,21 @@ async function ensureTemplate(input) {
115
115
  else {
116
116
  await ensureVercelSandboxTags(sandbox, tags);
117
117
  }
118
- if (typeof sandbox.currentSnapshotId === "string" && sandbox.currentSnapshotId.length > 0) {
118
+ /*
119
+ * A non-empty `currentSnapshotId` normally means "this template was
120
+ * prewarmed in a previous run — reuse it." But with an author-supplied
121
+ * `source: snapshot`, the SDK pre-populates `currentSnapshotId` with
122
+ * the *author's* snapshotId both on a fresh create and on every
123
+ * subsequent `getNamedSandbox` reuse until we run our own snapshot.
124
+ * So we ignore that exact value: it's the author's base layer, not a
125
+ * framework snapshot, and we still owe `ensureSandboxWorkingDirectory`,
126
+ * bootstrap, seed file writes, and `sandbox.snapshot()` on top.
127
+ */
128
+ const authorSnapshotId = extractAuthorSnapshotId(input.createOptions);
129
+ const hasFrameworkSnapshot = typeof sandbox.currentSnapshotId === "string" &&
130
+ sandbox.currentSnapshotId.length > 0 &&
131
+ sandbox.currentSnapshotId !== authorSnapshotId;
132
+ if (hasFrameworkSnapshot) {
119
133
  return {
120
134
  sandboxName: sandbox.name,
121
135
  snapshotId: sandbox.currentSnapshotId,
@@ -156,11 +170,16 @@ async function ensureSession(input) {
156
170
  await ensureVercelSandboxTags(existing, input.tags);
157
171
  return existing;
158
172
  }
159
- // The Vercel SDK rejects `runtime` when `source` is a snapshot — the
160
- // runtime is already baked into the snapshot's filesystem. Strip it
161
- // from the consumer-supplied create options for the session path
162
- // only; template prewarm still honors `runtime`.
163
- const { runtime: _runtime, ...sessionCreateOptions } = input.createOptions;
173
+ /*
174
+ * Strip both `source` and `runtime` from author-supplied create
175
+ * options for the session path. The framework owns the session
176
+ * source sessions always derive from the prewarmed template's
177
+ * snapshot, never the author's external source. And the Vercel SDK
178
+ * rejects `runtime` when `source` is a snapshot because the runtime
179
+ * is already baked into the snapshot's filesystem. Template prewarm
180
+ * still honors both fields.
181
+ */
182
+ const { runtime: _runtime, source: _source, ...sessionCreateOptions } = input.createOptions;
164
183
  const createParams = {
165
184
  ...sessionCreateOptions,
166
185
  name: sandboxName,
@@ -276,6 +295,19 @@ function isSandboxMissingError(error) {
276
295
  error.cause?.response?.status;
277
296
  return status === 404;
278
297
  }
298
+ /**
299
+ * Pulls the snapshotId out of an author-supplied `source: { type:
300
+ * "snapshot", ... }`. Returns undefined for git/tarball sources or when
301
+ * no source was supplied — those don't seed `currentSnapshotId` with a
302
+ * pre-existing value the way snapshot sources do.
303
+ */
304
+ function extractAuthorSnapshotId(createOptions) {
305
+ const source = createOptions.source;
306
+ if (source?.type === "snapshot" && typeof source.snapshotId === "string") {
307
+ return source.snapshotId;
308
+ }
309
+ return undefined;
310
+ }
279
311
  function getVercelSandboxName(metadata) {
280
312
  const sandboxName = metadata?.sandboxName;
281
313
  return typeof sandboxName === "string" ? sandboxName : undefined;
@@ -1,5 +1,6 @@
1
1
  import type { ModelMessage, ToolSet, TypedToolResult } from "ai";
2
2
  import type { RuntimeToolResultActionResult } from "#runtime/actions/types.js";
3
+ import type { JsonValue } from "#shared/json.js";
3
4
  type ToolResponsePart = Extract<ModelMessage, {
4
5
  role: "tool";
5
6
  }>["content"][number];
@@ -7,14 +8,16 @@ type ToolResultPart = Extract<ToolResponsePart, {
7
8
  type: "tool-result";
8
9
  }>;
9
10
  /**
10
- * Serializes an arbitrary tool result payload to a stable string form.
11
+ * Coerces an arbitrary value to a JSON-safe {@link JsonValue} without
12
+ * premature stringification.
11
13
  *
12
- * String inputs pass through. `Error` instances surface their message so
13
- * stack traces never leak into protocol events. Everything else is
14
- * `JSON.stringify`'d, with a `String(value)` fallback for values JSON
15
- * cannot represent (functions, symbols, BigInts).
14
+ * - Strings, numbers, booleans, and `null` pass through as primitives.
15
+ * - `Error` instances surface only their message (no stack leak).
16
+ * - Plain objects and arrays pass through structurally.
17
+ * - Non-JSON-representable values (functions, symbols, BigInts) fall
18
+ * back to `String(value)`.
16
19
  */
17
- export declare function serializeActionOutput(value: unknown): string;
20
+ export declare function toJsonValue(value: unknown): JsonValue;
18
21
  /**
19
22
  * Builds a `RuntimeToolResultActionResult` from one AI SDK
20
23
  * {@link TypedToolResult}. Used for tool results captured on the AI SDK
@@ -1,20 +1,27 @@
1
1
  /**
2
- * Serializes an arbitrary tool result payload to a stable string form.
2
+ * Coerces an arbitrary value to a JSON-safe {@link JsonValue} without
3
+ * premature stringification.
3
4
  *
4
- * String inputs pass through. `Error` instances surface their message so
5
- * stack traces never leak into protocol events. Everything else is
6
- * `JSON.stringify`'d, with a `String(value)` fallback for values JSON
7
- * cannot represent (functions, symbols, BigInts).
5
+ * - Strings, numbers, booleans, and `null` pass through as primitives.
6
+ * - `Error` instances surface only their message (no stack leak).
7
+ * - Plain objects and arrays pass through structurally.
8
+ * - Non-JSON-representable values (functions, symbols, BigInts) fall
9
+ * back to `String(value)`.
8
10
  */
9
- export function serializeActionOutput(value) {
10
- if (typeof value === "string") {
11
+ export function toJsonValue(value) {
12
+ if (value === null ||
13
+ typeof value === "string" ||
14
+ typeof value === "number" ||
15
+ typeof value === "boolean") {
11
16
  return value;
12
17
  }
13
18
  if (value instanceof Error) {
14
19
  return value.message;
15
20
  }
16
- const serialized = JSON.stringify(value);
17
- return serialized === undefined ? String(value) : serialized;
21
+ if (typeof value === "object") {
22
+ return value;
23
+ }
24
+ return String(value);
18
25
  }
19
26
  /**
20
27
  * Builds a `RuntimeToolResultActionResult` from one AI SDK
@@ -25,7 +32,7 @@ export function createRuntimeToolResultFromStepResult(toolResult) {
25
32
  return {
26
33
  callId: toolResult.toolCallId,
27
34
  kind: "tool-result",
28
- output: serializeActionOutput(toolResult.output),
35
+ output: toJsonValue(toolResult.output),
29
36
  toolName: toolResult.toolName,
30
37
  };
31
38
  }
@@ -40,7 +47,7 @@ export function createRuntimeToolResultFromMessagePart(part) {
40
47
  const result = {
41
48
  callId: part.toolCallId,
42
49
  kind: "tool-result",
43
- output: serializeToolResultOutput(part.output),
50
+ output: toolResultOutputToJsonValue(part.output),
44
51
  toolName: part.toolName,
45
52
  };
46
53
  if (isToolResultError(part.output)) {
@@ -51,21 +58,21 @@ export function createRuntimeToolResultFromMessagePart(part) {
51
58
  }
52
59
  return result;
53
60
  }
54
- function serializeToolResultOutput(output) {
61
+ function toolResultOutputToJsonValue(output) {
55
62
  switch (output.type) {
56
63
  case "text":
57
64
  case "error-text":
58
65
  return output.value;
59
66
  case "json":
60
67
  case "error-json":
61
- return serializeActionOutput(output.value);
68
+ return toJsonValue(output.value);
62
69
  case "execution-denied":
63
- return serializeActionOutput({
70
+ return {
64
71
  code: "TOOL_EXECUTION_DENIED",
65
72
  message: output.reason ?? "Tool execution was denied.",
66
- });
73
+ };
67
74
  case "content":
68
- return serializeActionOutput(output.value);
75
+ return toJsonValue(output.value);
69
76
  }
70
77
  }
71
78
  function isToolResultError(output) {
@@ -24,6 +24,22 @@ export declare function summarizeKnownModelCallConfigError(error: unknown): Mode
24
24
  * response, so the user-facing message should avoid implying a bad tool call.
25
25
  */
26
26
  export declare function summarizeKnownModelCallRequestError(error: unknown): ModelCallConfigErrorSummary | null;
27
+ /**
28
+ * Returns the distinct upstream tool types referenced by any
29
+ * "tool type 'X' is not supported" rejection in an AI Gateway error's
30
+ * provider attempt list.
31
+ *
32
+ * Walks the cause chain to find the gateway error and inspects both the
33
+ * structured `data` field and the raw `responseBody` JSON. Returns an
34
+ * empty array for errors that are not of this shape.
35
+ *
36
+ * Used by the harness recovery path to identify which framework tools
37
+ * to drop before retrying the failing step. Detection is by string
38
+ * match on the upstream tool type — see
39
+ * {@link resolveFrameworkToolFromUpstreamType} for the mapping back to
40
+ * framework tool names.
41
+ */
42
+ export declare function extractUnsupportedProviderToolTypes(error: unknown): readonly string[];
27
43
  /**
28
44
  * Extracts compact, structured diagnostics from AI SDK / AI Gateway model-call
29
45
  * errors. The full SDK error can include very large request bodies (especially
@@ -1,6 +1,17 @@
1
1
  import { isObject } from "#shared/guards.js";
2
2
  const RESPONSE_BODY_SNIPPET_LIMIT = 1_000;
3
3
  const GATEWAY_MODEL_REQUEST_REJECTED_MESSAGE = "AI Gateway rejected the model request before the agent produced a response.";
4
+ /**
5
+ * Anchored regex for the upstream "unsupported tool" rejection message
6
+ * that AI Gateway returns when a fallback provider cannot serve a
7
+ * provider-specific tool (e.g. Bedrock rejecting `web_search_20250305`).
8
+ *
9
+ * The phrasing comes from the gateway's own provider attempt projection
10
+ * and is stable across the Bedrock and Vertex Anthropic backends. We
11
+ * anchor the match on the literal `tool type` prefix to avoid sweeping
12
+ * in unrelated "not supported" errors.
13
+ */
14
+ const UNSUPPORTED_TOOL_TYPE_REGEX = /tool type ['"]([\w.-]+)['"] is not supported/i;
4
15
  /**
5
16
  * Returns a concise actionable summary for known terminal configuration
6
17
  * errors raised during a model call. Returns `null` for everything else
@@ -56,6 +67,66 @@ export function summarizeKnownModelCallRequestError(error) {
56
67
  }
57
68
  return null;
58
69
  }
70
+ /**
71
+ * Returns the distinct upstream tool types referenced by any
72
+ * "tool type 'X' is not supported" rejection in an AI Gateway error's
73
+ * provider attempt list.
74
+ *
75
+ * Walks the cause chain to find the gateway error and inspects both the
76
+ * structured `data` field and the raw `responseBody` JSON. Returns an
77
+ * empty array for errors that are not of this shape.
78
+ *
79
+ * Used by the harness recovery path to identify which framework tools
80
+ * to drop before retrying the failing step. Detection is by string
81
+ * match on the upstream tool type — see
82
+ * {@link resolveFrameworkToolFromUpstreamType} for the mapping back to
83
+ * framework tool names.
84
+ */
85
+ export function extractUnsupportedProviderToolTypes(error) {
86
+ const found = new Set();
87
+ for (const candidate of walkCauseChain(error)) {
88
+ collectUnsupportedToolTypesFromValue(readObjectField(candidate, "data"), found);
89
+ const responseBody = readStringField(candidate, "responseBody");
90
+ if (responseBody !== undefined) {
91
+ try {
92
+ collectUnsupportedToolTypesFromValue(JSON.parse(responseBody), found);
93
+ }
94
+ catch {
95
+ // The response body may be truncated mid-JSON when the upstream
96
+ // includes a large request snapshot. Fall back to a raw string
97
+ // scan so we still surface the tool name when the regex match
98
+ // lies before the truncation boundary.
99
+ const match = UNSUPPORTED_TOOL_TYPE_REGEX.exec(responseBody);
100
+ if (match?.[1] !== undefined) {
101
+ found.add(match[1]);
102
+ }
103
+ }
104
+ }
105
+ }
106
+ return [...found];
107
+ }
108
+ function collectUnsupportedToolTypesFromValue(value, out) {
109
+ if (value === null || value === undefined)
110
+ return;
111
+ if (typeof value === "string") {
112
+ const match = UNSUPPORTED_TOOL_TYPE_REGEX.exec(value);
113
+ if (match?.[1] !== undefined) {
114
+ out.add(match[1]);
115
+ }
116
+ return;
117
+ }
118
+ if (Array.isArray(value)) {
119
+ for (const entry of value) {
120
+ collectUnsupportedToolTypesFromValue(entry, out);
121
+ }
122
+ return;
123
+ }
124
+ if (isObject(value)) {
125
+ for (const entry of Object.values(value)) {
126
+ collectUnsupportedToolTypesFromValue(entry, out);
127
+ }
128
+ }
129
+ }
59
130
  /**
60
131
  * Extracts compact, structured diagnostics from AI SDK / AI Gateway model-call
61
132
  * errors. The full SDK error can include very large request bodies (especially
@@ -3,7 +3,39 @@ import type { RuntimeModelReference } from "#runtime/agent/bootstrap.js";
3
3
  /**
4
4
  * The provider backend resolved for one web search tool invocation.
5
5
  */
6
- type WebSearchBackend = "anthropic" | "gateway" | "google" | "openai";
6
+ export type WebSearchBackend = "anthropic" | "gateway" | "google" | "openai";
7
+ /**
8
+ * Returns the framework tool name that produced an upstream provider tool
9
+ * `type`, or `null` when the type is not one we know how to remove.
10
+ *
11
+ * Used by the harness recovery path to decide which tools to drop when a
12
+ * gateway fallback provider rejects a tool. Unknown types fall through to
13
+ * the existing terminal/recoverable handling.
14
+ */
15
+ export declare function resolveFrameworkToolFromUpstreamType(type: string): string | null;
16
+ /**
17
+ * Maps a {@link WebSearchBackend} to the gateway provider slug used in
18
+ * `providerOptions.gateway.only` to pin routing to that provider.
19
+ *
20
+ * Returns `null` for the `"gateway"` backend (Perplexity via AI Gateway),
21
+ * which is served by the gateway directly and does not need pinning.
22
+ */
23
+ export declare function resolveGatewayPinForWebSearchBackend(backend: WebSearchBackend): string | null;
24
+ /**
25
+ * Returns a new `providerOptions` object with
26
+ * `gateway.only = [provider]` merged into the existing `gateway`
27
+ * sub-object so the AI Gateway only attempts the given provider.
28
+ *
29
+ * Used by the harness to pin routing when a provider-specific tool
30
+ * (e.g. Anthropic's `web_search_20250305`) is in the per-step toolset,
31
+ * so a transient primary outage produces a clean retryable 503 instead
32
+ * of a fallback-to-incompatible-provider 400.
33
+ *
34
+ * Author overrides win — if `base.gateway.only` or `base.gateway.order`
35
+ * is already set, the input is returned unchanged so explicit routing
36
+ * preferences are never silently overwritten.
37
+ */
38
+ export declare function mergeGatewayProviderPin(base: Readonly<Record<string, unknown>> | undefined, provider: string): Record<string, unknown>;
7
39
  /**
8
40
  * Determines the web search backend for a model reference.
9
41
  *
@@ -22,4 +54,3 @@ export declare function resolveWebSearchBackend(modelRef: RuntimeModelReference)
22
54
  * provider matching the current model is loaded.
23
55
  */
24
56
  export declare function resolveWebSearchProviderTool(backend: WebSearchBackend): Promise<ToolSet[string]>;
25
- export {};
@@ -1,3 +1,84 @@
1
+ import { WEB_SEARCH_TOOL_DEFINITION } from "#runtime/framework-tools/web-search.js";
2
+ import { isObject } from "#shared/guards.js";
3
+ /**
4
+ * Maps an upstream provider tool type (the literal `type` string the AI SDK
5
+ * sends to the provider) back to the framework tool name that injected it.
6
+ *
7
+ * Used when the AI Gateway routes a request to a fallback provider that
8
+ * does not support a provider-specific tool — the upstream error references
9
+ * the provider-specific type (e.g. `web_search_20250305`), but the harness
10
+ * needs to drop the framework tool by its public name (`web_search`).
11
+ *
12
+ * Adding a new provider tool requires adding the corresponding mapping
13
+ * entry here alongside its {@link resolveWebSearchProviderTool} switch
14
+ * arm so detection stays in lockstep with injection.
15
+ */
16
+ const UPSTREAM_TOOL_TYPE_TO_FRAMEWORK_NAME = {
17
+ // Anthropic's stable web search tool. The Bedrock and Vertex
18
+ // Anthropic backends reject this type because they only host the
19
+ // older Claude Messages surface.
20
+ web_search_20250305: WEB_SEARCH_TOOL_DEFINITION.name,
21
+ };
22
+ /**
23
+ * Returns the framework tool name that produced an upstream provider tool
24
+ * `type`, or `null` when the type is not one we know how to remove.
25
+ *
26
+ * Used by the harness recovery path to decide which tools to drop when a
27
+ * gateway fallback provider rejects a tool. Unknown types fall through to
28
+ * the existing terminal/recoverable handling.
29
+ */
30
+ export function resolveFrameworkToolFromUpstreamType(type) {
31
+ return UPSTREAM_TOOL_TYPE_TO_FRAMEWORK_NAME[type] ?? null;
32
+ }
33
+ /**
34
+ * Maps a {@link WebSearchBackend} to the gateway provider slug used in
35
+ * `providerOptions.gateway.only` to pin routing to that provider.
36
+ *
37
+ * Returns `null` for the `"gateway"` backend (Perplexity via AI Gateway),
38
+ * which is served by the gateway directly and does not need pinning.
39
+ */
40
+ export function resolveGatewayPinForWebSearchBackend(backend) {
41
+ switch (backend) {
42
+ case "anthropic":
43
+ return "anthropic";
44
+ case "openai":
45
+ return "openai";
46
+ case "google":
47
+ return "google";
48
+ case "gateway":
49
+ return null;
50
+ }
51
+ }
52
+ /**
53
+ * Returns a new `providerOptions` object with
54
+ * `gateway.only = [provider]` merged into the existing `gateway`
55
+ * sub-object so the AI Gateway only attempts the given provider.
56
+ *
57
+ * Used by the harness to pin routing when a provider-specific tool
58
+ * (e.g. Anthropic's `web_search_20250305`) is in the per-step toolset,
59
+ * so a transient primary outage produces a clean retryable 503 instead
60
+ * of a fallback-to-incompatible-provider 400.
61
+ *
62
+ * Author overrides win — if `base.gateway.only` or `base.gateway.order`
63
+ * is already set, the input is returned unchanged so explicit routing
64
+ * preferences are never silently overwritten.
65
+ */
66
+ export function mergeGatewayProviderPin(base, provider) {
67
+ const baseGateway = isObject(base?.gateway)
68
+ ? base.gateway
69
+ : undefined;
70
+ if (baseGateway?.only !== undefined || baseGateway?.order !== undefined) {
71
+ return { ...base };
72
+ }
73
+ const mergedGateway = {
74
+ ...baseGateway,
75
+ only: [provider],
76
+ };
77
+ return {
78
+ ...base,
79
+ gateway: mergedGateway,
80
+ };
81
+ }
1
82
  /**
2
83
  * Determines the web search backend for a model reference.
3
84
  *
@@ -16,6 +16,27 @@ export interface StepHooksInput {
16
16
  readonly cachePath: PromptCachePath;
17
17
  readonly emit?: HarnessEmitFn;
18
18
  readonly emissionState: HarnessEmissionState;
19
+ /**
20
+ * When `false`, `prepareStep` skips the `step.started` emission.
21
+ * Used by the harness recovery path to avoid emitting `step.started`
22
+ * twice when retrying the same step with a degraded toolset.
23
+ *
24
+ * Defaults to `true`.
25
+ */
26
+ readonly emitStepStarted?: boolean;
27
+ /**
28
+ * When set on the `gateway-auto` cache path, merges
29
+ * `providerOptions.gateway.only = [gatewayPinProvider]` so the AI
30
+ * Gateway only routes to the given provider. Used to keep
31
+ * provider-specific tools (e.g. Anthropic's `web_search_20250305`)
32
+ * on a provider that can serve them, converting a transient outage
33
+ * into a clean retryable 503 rather than a fallback-to-incompatible
34
+ * provider 400.
35
+ *
36
+ * Ignored when the author already set `gateway.only` or
37
+ * `gateway.order` on the model reference's provider options.
38
+ */
39
+ readonly gatewayPinProvider?: string;
19
40
  readonly marker: AnthropicCacheMarker | undefined;
20
41
  readonly session: HarnessSession;
21
42
  }
@@ -3,6 +3,7 @@ import { createRuntimeToolResultFromMessagePart, createRuntimeToolResultFromStep
3
3
  import { emitStepStarted, normalizeAssistantStepFinishReason } from "#harness/emission.js";
4
4
  import { extractToolApprovalInputRequests } from "#harness/input-extraction.js";
5
5
  import { applyConversationCacheControl, mergeGatewayAutoCaching, } from "#harness/prompt-cache.js";
6
+ import { mergeGatewayProviderPin } from "#harness/provider-tools.js";
6
7
  import { createRuntimeActionRequestFromToolCall } from "#harness/runtime-actions.js";
7
8
  // ---------------------------------------------------------------------------
8
9
  // Builder
@@ -31,7 +32,7 @@ export function buildStepHooks(input) {
31
32
  // -------------------------------------------------------------------------
32
33
  const prepareStep = async ({ messages }) => {
33
34
  let processed = messages;
34
- if (emit) {
35
+ if (emit && input.emitStepStarted !== false) {
35
36
  await emitStepStarted(emit, input.emissionState);
36
37
  }
37
38
  if (input.cachePath.kind === "anthropic-direct" && input.marker) {
@@ -41,7 +42,11 @@ export function buildStepHooks(input) {
41
42
  messages: processed,
42
43
  };
43
44
  if (input.cachePath.kind === "gateway-auto") {
44
- stepResult.providerOptions = mergeGatewayAutoCaching(session.agent.modelReference.providerOptions);
45
+ let providerOptions = mergeGatewayAutoCaching(session.agent.modelReference.providerOptions);
46
+ if (input.gatewayPinProvider !== undefined) {
47
+ providerOptions = mergeGatewayProviderPin(providerOptions, input.gatewayPinProvider);
48
+ }
49
+ stepResult.providerOptions = providerOptions;
45
50
  }
46
51
  return stepResult;
47
52
  };