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.
- package/CHANGELOG.md +17 -0
- package/dist/docs/public/sandbox.md +25 -0
- package/dist/src/chunks/{dev-authored-source-watcher-DKDaaPea.js → dev-authored-source-watcher-BLzYWh05.js} +1 -1
- package/dist/src/chunks/host-DREC8e8Z.js +65 -0
- package/dist/src/chunks/{paths-DZTgjrW-.js → paths-C6sp4T2U.js} +25 -25
- package/dist/src/chunks/{prewarm-BELT37PI.js → prewarm-hz8p2jlZ.js} +1 -1
- package/dist/src/cli/commands/info.js +1 -1
- package/dist/src/cli/run.js +1 -1
- package/dist/src/evals/cli/eval.js +1 -1
- package/dist/src/execution/sandbox/bindings/vercel.d.ts +1 -1
- package/dist/src/execution/sandbox/bindings/vercel.js +38 -6
- package/dist/src/harness/action-result-helpers.d.ts +9 -6
- package/dist/src/harness/action-result-helpers.js +23 -16
- package/dist/src/harness/model-call-error.d.ts +16 -0
- package/dist/src/harness/model-call-error.js +71 -0
- package/dist/src/harness/provider-tools.d.ts +33 -2
- package/dist/src/harness/provider-tools.js +81 -0
- package/dist/src/harness/step-hooks.d.ts +21 -0
- package/dist/src/harness/step-hooks.js +7 -2
- package/dist/src/harness/tool-loop.js +284 -143
- package/dist/src/harness/tools.d.ts +12 -0
- package/dist/src/harness/tools.js +23 -5
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/nitro/host/build-application.js +67 -1
- package/dist/src/internal/workflow-bundle/ash-service-route-output.d.ts +4 -0
- package/dist/src/internal/workflow-bundle/ash-service-route-output.js +134 -0
- package/dist/src/internal/workflow-bundle/vercel-workflow-output.d.ts +17 -0
- package/dist/src/internal/workflow-bundle/vercel-workflow-output.js +141 -1
- package/dist/src/public/definitions/connections/mcp.js +2 -0
- package/dist/src/public/definitions/tool.js +2 -0
- package/dist/src/public/next/index.js +7 -2
- package/dist/src/public/sandbox/backends/vercel.d.ts +7 -0
- package/dist/src/public/sandbox/backends/vercel.js +7 -0
- package/dist/src/public/sandbox/vercel-sandbox.d.ts +14 -4
- package/dist/src/public/tool-result-narrowing.d.ts +10 -7
- package/dist/src/public/tool-result-narrowing.js +42 -13
- package/dist/src/runtime/resolve-connection.js +5 -2
- package/dist/src/runtime/resolve-tool.js +5 -2
- package/package.json +1 -1
- 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-
|
|
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-
|
|
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};
|
package/dist/src/cli/run.js
CHANGED
|
@@ -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-
|
|
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-
|
|
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" | "
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
*
|
|
11
|
+
* Coerces an arbitrary value to a JSON-safe {@link JsonValue} without
|
|
12
|
+
* premature stringification.
|
|
11
13
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
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
|
-
*
|
|
2
|
+
* Coerces an arbitrary value to a JSON-safe {@link JsonValue} without
|
|
3
|
+
* premature stringification.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
10
|
-
if (
|
|
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
|
-
|
|
17
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
68
|
+
return toJsonValue(output.value);
|
|
62
69
|
case "execution-denied":
|
|
63
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
};
|