experimental-ash 0.21.0 → 0.22.1
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/hooks.md +39 -6
- package/dist/docs/public/skills.md +6 -1
- package/dist/src/chunks/{dev-authored-source-watcher-XmRMDVDO.js → dev-authored-source-watcher-Bj0TDis0.js} +1 -1
- package/dist/src/chunks/host-BDf7A9G7.js +22 -0
- package/dist/src/chunks/paths-DxgjK2PI.js +88 -0
- package/dist/src/chunks/{prewarm-Om8D2hpG.js → prewarm-xQtl6kAD.js} +1 -1
- package/dist/src/cli/commands/info.js +1 -1
- package/dist/src/cli/run.js +1 -1
- package/dist/src/compiled/.vendor-stamp.json +3 -3
- package/dist/src/compiled/@ai-sdk/anthropic/_provider-utils.d.ts +15 -0
- package/dist/src/compiled/@ai-sdk/anthropic/index.d.ts +1259 -6
- package/dist/src/compiled/@ai-sdk/google/_provider-utils.d.ts +11 -0
- package/dist/src/compiled/@ai-sdk/google/index.d.ts +561 -6
- package/dist/src/compiled/@ai-sdk/google/index.js +4 -4
- package/dist/src/compiled/@ai-sdk/google/package.json +1 -1
- package/dist/src/compiled/@ai-sdk/openai/_provider-utils.d.ts +15 -0
- package/dist/src/compiled/@ai-sdk/openai/index.d.ts +1255 -6
- package/dist/src/compiled/@ai-sdk/openai/index.js +1 -1
- package/dist/src/compiled/@ai-sdk/openai/package.json +1 -1
- package/dist/src/compiler/normalize-skill.js +5 -1
- package/dist/src/compiler/workspace-resources.js +12 -5
- package/dist/src/context/hook-lifecycle.d.ts +8 -8
- package/dist/src/context/hook-lifecycle.js +88 -36
- package/dist/src/evals/cli/eval.js +1 -1
- package/dist/src/execution/skills/instructions.d.ts +8 -4
- package/dist/src/execution/skills/instructions.js +3 -2
- 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 +295 -146
- 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/authored-definition/core.js +16 -1
- package/dist/src/internal/nitro/host/build-application.js +65 -1
- package/dist/src/internal/workflow-bundle/vercel-workflow-output.d.ts +15 -0
- package/dist/src/internal/workflow-bundle/vercel-workflow-output.js +156 -1
- package/dist/src/public/definitions/connections/mcp.js +2 -0
- package/dist/src/public/definitions/hook.d.ts +11 -3
- package/dist/src/public/definitions/skill.d.ts +4 -3
- package/dist/src/public/definitions/skill.js +1 -1
- package/dist/src/public/definitions/tool.js +2 -0
- package/dist/src/public/skills/index.d.ts +1 -1
- package/dist/src/public/skills/index.js +1 -1
- 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/dist/src/shared/skill-definition.d.ts +20 -2
- package/dist/src/shared/skill-package.d.ts +43 -0
- package/dist/src/shared/skill-package.js +100 -0
- package/package.json +3 -3
- package/dist/src/chunks/host-CyC15ErG.js +0 -22
- package/dist/src/chunks/paths-BgL4fqtv.js +0 -88
|
@@ -2,8 +2,10 @@ import { getAdapterKind } from "../channel/adapter.js";
|
|
|
2
2
|
import { emitRecoverableFailedTurn, emitTurnPreamble, getHarnessEmissionState, setHarnessEmissionState, } from "../harness/emission.js";
|
|
3
3
|
import { createLogger } from "../internal/logging.js";
|
|
4
4
|
import { toErrorMessage } from "../shared/errors.js";
|
|
5
|
+
import { normalizeSkillPackage, writeSkillPackageToSandbox, } from "../shared/skill-package.js";
|
|
6
|
+
import { formatAvailableSkillsSection } from "#execution/skills/instructions.js";
|
|
5
7
|
import { contextStorage } from "./container.js";
|
|
6
|
-
import { BundleKey, ChannelKey, ContinuationTokenKey, SessionIdKey, SessionPreparedKey, } from "./keys.js";
|
|
8
|
+
import { BundleKey, ChannelKey, ContinuationTokenKey, SandboxKey, SessionIdKey, SessionPreparedKey, } from "./keys.js";
|
|
7
9
|
const log = createLogger("hooks.lifecycle");
|
|
8
10
|
/**
|
|
9
11
|
* Runs the per-turn hook lifecycle inside the active ALS scope and
|
|
@@ -20,11 +22,12 @@ const log = createLogger("hooks.lifecycle");
|
|
|
20
22
|
*
|
|
21
23
|
* 2. `lifecycle.turn` — runs once per fresh delivery (the caller
|
|
22
24
|
* gates dispatch with `isHarnessBetweenTurns(session)`). Each
|
|
23
|
-
* hook may return `{ modelContext }`;
|
|
24
|
-
* concatenated in registry order
|
|
25
|
-
* the
|
|
26
|
-
*
|
|
27
|
-
* the
|
|
25
|
+
* hook may return `{ modelContext, skills }`; model-context
|
|
26
|
+
* contributions are concatenated in registry order and skills are
|
|
27
|
+
* materialized into the live sandbox before the model call. A
|
|
28
|
+
* thrown hook is caught here, the recoverable `turn.failed`
|
|
29
|
+
* cascade is emitted, and the dispatcher returns
|
|
30
|
+
* `kind: "turn-failed"` so the caller can park the session.
|
|
28
31
|
*
|
|
29
32
|
* Errors:
|
|
30
33
|
*
|
|
@@ -48,56 +51,63 @@ export async function dispatchHookLifecycle(input) {
|
|
|
48
51
|
turn: { sequence, turnId },
|
|
49
52
|
};
|
|
50
53
|
const modelContext = [];
|
|
54
|
+
let nextSession = input.session;
|
|
51
55
|
// ---------- lifecycle.session ----------
|
|
52
56
|
if (registry.session.length > 0 && ctx.get(SessionPreparedKey) !== true) {
|
|
53
57
|
// Set the flag BEFORE running the chain. A thrown session hook is
|
|
54
58
|
// a terminal session failure; the next turn must not retry.
|
|
55
59
|
ctx.set(SessionPreparedKey, true);
|
|
60
|
+
const stageSkills = [];
|
|
56
61
|
for (const entry of registry.session) {
|
|
57
62
|
const result = (await entry.handler(lifecycleInput, hookCtx));
|
|
58
63
|
if (result?.modelContext !== undefined && result.modelContext.length > 0) {
|
|
59
64
|
modelContext.push(...result.modelContext);
|
|
60
65
|
}
|
|
66
|
+
stageSkills.push(...normalizeLifecycleSkillResults(ctx, result));
|
|
61
67
|
}
|
|
68
|
+
nextSession = await materializeLifecycleSkills(ctx, nextSession, stageSkills);
|
|
62
69
|
}
|
|
63
70
|
// ---------- lifecycle.turn ----------
|
|
64
71
|
let preambleEmitted = false;
|
|
65
72
|
let preambleState = emissionState;
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
try {
|
|
74
|
+
const stageSkills = [];
|
|
75
|
+
for (const entry of registry.turn) {
|
|
68
76
|
const result = (await entry.handler(lifecycleInput, hookCtx));
|
|
69
77
|
if (result?.modelContext !== undefined && result.modelContext.length > 0) {
|
|
70
78
|
modelContext.push(...result.modelContext);
|
|
71
79
|
}
|
|
80
|
+
stageSkills.push(...normalizeLifecycleSkillResults(ctx, result));
|
|
72
81
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
code: "HOOK_TURN_FAILED",
|
|
82
|
-
message,
|
|
83
|
-
});
|
|
84
|
-
return {
|
|
85
|
-
kind: "turn-failed",
|
|
86
|
-
message,
|
|
87
|
-
nextSession: setHarnessEmissionState(input.session, failedState),
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
catch (cascadeError) {
|
|
91
|
-
// An event hook subscribed to one of the failure-cascade
|
|
92
|
-
// events (eg. `events["turn.failed"]`) threw while we were
|
|
93
|
-
// emitting the recoverable failure. Re-throwing escalates to
|
|
94
|
-
// a terminal `session.failed` via the runtime's outer error
|
|
95
|
-
// path. Document this as the bounded second-order behavior:
|
|
96
|
-
// when both a `lifecycle.turn` and a `turn.failed` event hook
|
|
97
|
-
// throw, the session ends.
|
|
98
|
-
log.error(`Event hook threw while emitting the turn.failed cascade for hook "${entry.slug}" — escalating to session.failed.`, { error: toErrorMessage(cascadeError) });
|
|
99
|
-
throw cascadeError;
|
|
82
|
+
nextSession = await materializeLifecycleSkills(ctx, nextSession, stageSkills);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
const message = toErrorMessage(error);
|
|
86
|
+
try {
|
|
87
|
+
if (!preambleEmitted) {
|
|
88
|
+
preambleState = await emitTurnPreamble(emit, { message: input.input.message }, emissionState, input.runtimeIdentity);
|
|
89
|
+
preambleEmitted = true;
|
|
100
90
|
}
|
|
91
|
+
const failedState = await emitRecoverableFailedTurn(emit, preambleState, {
|
|
92
|
+
code: "HOOK_TURN_FAILED",
|
|
93
|
+
message,
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
kind: "turn-failed",
|
|
97
|
+
message,
|
|
98
|
+
nextSession: setHarnessEmissionState(nextSession, failedState),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch (cascadeError) {
|
|
102
|
+
// An event hook subscribed to one of the failure-cascade events
|
|
103
|
+
// (eg. `events["turn.failed"]`) threw while we were emitting the
|
|
104
|
+
// recoverable failure. Re-throwing escalates to a terminal
|
|
105
|
+
// `session.failed` via the runtime's outer error path. Document
|
|
106
|
+
// this as the bounded second-order behavior: when both a
|
|
107
|
+
// `lifecycle.turn` and a `turn.failed` event hook throw, the
|
|
108
|
+
// session ends.
|
|
109
|
+
log.error("Event hook threw while emitting the turn.failed cascade for a lifecycle.turn failure — escalating to session.failed.", { error: toErrorMessage(cascadeError) });
|
|
110
|
+
throw cascadeError;
|
|
101
111
|
}
|
|
102
112
|
}
|
|
103
113
|
const channelContext = input.input.modelContext ?? [];
|
|
@@ -107,7 +117,49 @@ export async function dispatchHookLifecycle(input) {
|
|
|
107
117
|
input: mergedModelContext.length > 0
|
|
108
118
|
? { ...input.input, modelContext: mergedModelContext }
|
|
109
119
|
: input.input,
|
|
110
|
-
nextSession
|
|
120
|
+
nextSession,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function normalizeLifecycleSkillResults(ctx, result) {
|
|
124
|
+
if (result?.skills === undefined || result.skills.length === 0) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
const authoredSkillNames = new Set(ctx.require(BundleKey).resolvedAgent.skills.map((skill) => skill.name));
|
|
128
|
+
const dynamicSkills = new Map();
|
|
129
|
+
for (const skill of result.skills) {
|
|
130
|
+
const normalized = normalizeSkillPackage(skill);
|
|
131
|
+
if (authoredSkillNames.has(normalized.name)) {
|
|
132
|
+
throw new Error(`Hook-contributed skill "${normalized.name}" conflicts with an authored skill.`);
|
|
133
|
+
}
|
|
134
|
+
dynamicSkills.set(normalized.name, normalized);
|
|
135
|
+
}
|
|
136
|
+
return [...dynamicSkills.values()];
|
|
137
|
+
}
|
|
138
|
+
async function materializeLifecycleSkills(ctx, session, skills) {
|
|
139
|
+
if (skills.length === 0) {
|
|
140
|
+
return session;
|
|
141
|
+
}
|
|
142
|
+
const dedupedSkills = new Map(skills.map((skill) => [skill.name, skill]));
|
|
143
|
+
const sandbox = await ctx.require(SandboxKey).get();
|
|
144
|
+
if (sandbox === null) {
|
|
145
|
+
throw new Error("Dynamic skills require a sandbox for the current agent.");
|
|
146
|
+
}
|
|
147
|
+
for (const skill of dedupedSkills.values()) {
|
|
148
|
+
await writeSkillPackageToSandbox({ sandbox, skill });
|
|
149
|
+
}
|
|
150
|
+
const announcement = formatAvailableSkillsSection([...dedupedSkills.values()]);
|
|
151
|
+
if (announcement === null) {
|
|
152
|
+
return session;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
...session,
|
|
156
|
+
history: [
|
|
157
|
+
...session.history,
|
|
158
|
+
{
|
|
159
|
+
role: "system",
|
|
160
|
+
content: announcement,
|
|
161
|
+
},
|
|
162
|
+
],
|
|
111
163
|
};
|
|
112
164
|
}
|
|
113
165
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{n as e}from"../../chunks/paths-
|
|
1
|
+
import{n as e}from"../../chunks/paths-DxgjK2PI.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-BDf7A9G7.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,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
export interface AvailableSkillDescription {
|
|
2
|
+
readonly description: string;
|
|
3
|
+
readonly name: string;
|
|
4
|
+
}
|
|
2
5
|
/**
|
|
3
6
|
* Formats the "Available skills" system prompt section.
|
|
4
7
|
*
|
|
@@ -7,7 +10,8 @@ import type { ResolvedSkillDefinition } from "#runtime/types.js";
|
|
|
7
10
|
* has them from the `load_skill` tool result. This keeps the system
|
|
8
11
|
* prompt identical across the entire session, preserving prompt caching.
|
|
9
12
|
*
|
|
10
|
-
*
|
|
11
|
-
* agent's static instructions
|
|
13
|
+
* Authored skills call this at graph resolution time so the section is
|
|
14
|
+
* part of the turn agent's static instructions. Hook-contributed skills
|
|
15
|
+
* reuse the same formatter for durable history announcements.
|
|
12
16
|
*/
|
|
13
|
-
export declare function formatAvailableSkillsSection(skills: readonly
|
|
17
|
+
export declare function formatAvailableSkillsSection(skills: readonly AvailableSkillDescription[]): string | null;
|
|
@@ -7,8 +7,9 @@ import { WORKSPACE_ROOT } from "#runtime/workspace/types.js";
|
|
|
7
7
|
* has them from the `load_skill` tool result. This keeps the system
|
|
8
8
|
* prompt identical across the entire session, preserving prompt caching.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* agent's static instructions
|
|
10
|
+
* Authored skills call this at graph resolution time so the section is
|
|
11
|
+
* part of the turn agent's static instructions. Hook-contributed skills
|
|
12
|
+
* reuse the same formatter for durable history announcements.
|
|
12
13
|
*/
|
|
13
14
|
export function formatAvailableSkillsSection(skills) {
|
|
14
15
|
if (skills.length === 0) {
|
|
@@ -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
|
};
|