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
|
@@ -9,6 +9,7 @@ import { createCompactionCompletedEvent, createCompactionRequestedEvent, createI
|
|
|
9
9
|
import { ASK_QUESTION_TOOL_NAME } from "#runtime/framework-tools/ask-question.js";
|
|
10
10
|
import { ConnectionRegistryKey, DiscoveredConnectionToolsKey, } from "#runtime/framework-tools/connection-search.js";
|
|
11
11
|
import { resolveConnectionToolsFromState } from "#runtime/framework-tools/connection-tools.js";
|
|
12
|
+
import { WEB_SEARCH_TOOL_DEFINITION } from "#runtime/framework-tools/web-search.js";
|
|
12
13
|
import { hydrateSandboxAttachments, stageAttachmentsToSandbox, } from "#harness/attachment-staging.js";
|
|
13
14
|
import { compactMessages, getInputTokenCount, resolveCompactionModel, shouldCompact, } from "#harness/compaction.js";
|
|
14
15
|
import { advanceStep, emitFailedStep, emitRecoverableFailedTurn, emitStreamContent, emitTurnEpilogue, emitTurnPreamble, getHarnessEmissionState, setHarnessEmissionState, } from "#harness/emission.js";
|
|
@@ -16,9 +17,10 @@ import { extractQuestionInputRequests, extractToolApprovalInputRequests, } from
|
|
|
16
17
|
import { consumeDeferredStepInput, getApprovedTools, hasDeferredStepInput, hasStepInput, resolvePendingInput, setPendingInputBatch, } from "#harness/input-requests.js";
|
|
17
18
|
import { getInstrumentationConfig } from "#harness/instrumentation-config.js";
|
|
18
19
|
import { resolveAssistantStepText } from "#harness/messages.js";
|
|
19
|
-
import { classifyModelCallError, extractModelCallErrorDetails, summarizeKnownModelCallConfigError, summarizeKnownModelCallRequestError, } from "#harness/model-call-error.js";
|
|
20
|
+
import { classifyModelCallError, extractModelCallErrorDetails, extractUnsupportedProviderToolTypes, summarizeKnownModelCallConfigError, summarizeKnownModelCallRequestError, } from "#harness/model-call-error.js";
|
|
20
21
|
import { ensureOtelIntegration } from "#harness/otel-integration.js";
|
|
21
22
|
import { applyLastToolCacheBreakpoint, detectPromptCachePath, getAnthropicCacheMarker, } from "#harness/prompt-cache.js";
|
|
23
|
+
import { resolveFrameworkToolFromUpstreamType, resolveGatewayPinForWebSearchBackend, resolveWebSearchBackend, } from "#harness/provider-tools.js";
|
|
22
24
|
import { createRuntimeActionRequestFromToolCall, resolvePendingRuntimeActions, setPendingRuntimeActionBatch, } from "#harness/runtime-actions.js";
|
|
23
25
|
import { buildStepHooks, emitStepActions, isInvalidToolCall, } from "#harness/step-hooks.js";
|
|
24
26
|
import { pruneToolResults } from "#harness/tool-result-pruning.js";
|
|
@@ -81,6 +83,36 @@ function buildTelemetryRuntimeContext(authored, session) {
|
|
|
81
83
|
"ash.version": ashVersion,
|
|
82
84
|
};
|
|
83
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolves the gateway provider slug to pin via
|
|
88
|
+
* `providerOptions.gateway.only` for one harness step, or `undefined`
|
|
89
|
+
* when no pin is needed.
|
|
90
|
+
*
|
|
91
|
+
* A pin is added when all of:
|
|
92
|
+
* 1. The model is gateway-routed (the `gateway-auto` cache path —
|
|
93
|
+
* matches the existing `gateway.caching` hint condition).
|
|
94
|
+
* 2. The effective toolset includes a framework provider tool whose
|
|
95
|
+
* backend pins to one provider (e.g. `web_search` on Anthropic).
|
|
96
|
+
*
|
|
97
|
+
* The author keeps the final say via `providerOptions.gateway.only` or
|
|
98
|
+
* `.order` on their model reference — those overrides flow through
|
|
99
|
+
* {@link mergeGatewayProviderPin} which is a no-op when either field is
|
|
100
|
+
* already set.
|
|
101
|
+
*/
|
|
102
|
+
function resolveGatewayPinForStep(input) {
|
|
103
|
+
if (input.cachePath.kind !== "gateway-auto") {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
if (input.tools[WEB_SEARCH_TOOL_DEFINITION.name] === undefined) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const backend = resolveWebSearchBackend(input.modelReference);
|
|
110
|
+
if (backend === null) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
const pin = resolveGatewayPinForWebSearchBackend(backend);
|
|
114
|
+
return pin ?? undefined;
|
|
115
|
+
}
|
|
84
116
|
/**
|
|
85
117
|
* Builds AI Gateway app attribution headers when the model is gateway-routed.
|
|
86
118
|
*
|
|
@@ -260,33 +292,11 @@ export function createToolLoopHarness(config) {
|
|
|
260
292
|
telemetry: enrichTelemetry(telemetryConfig, agentName) ?? undefined,
|
|
261
293
|
}));
|
|
262
294
|
const approvedTools = getApprovedTools(session);
|
|
263
|
-
const tools = await buildToolSetWithProviderTools({
|
|
264
|
-
approvedTools,
|
|
265
|
-
capabilities: config.capabilities,
|
|
266
|
-
modelReference: session.agent.modelReference,
|
|
267
|
-
tools: config.tools,
|
|
268
|
-
});
|
|
269
295
|
// Inject connection tools discovered by a prior `connection_search`.
|
|
270
296
|
// Direct harness unit tests may run without an ambient context.
|
|
271
297
|
const ctx = contextStorage.getStore();
|
|
272
298
|
const registry = ctx?.get(ConnectionRegistryKey);
|
|
273
299
|
const discovered = ctx?.get(DiscoveredConnectionToolsKey);
|
|
274
|
-
if (registry !== undefined && discovered !== undefined) {
|
|
275
|
-
const connectionTools = await resolveConnectionToolsFromState(registry, discovered, {
|
|
276
|
-
approvedTools,
|
|
277
|
-
existingToolNames: new Set(Object.keys(tools)),
|
|
278
|
-
});
|
|
279
|
-
Object.assign(tools, connectionTools);
|
|
280
|
-
}
|
|
281
|
-
const effectiveTools = marker ? applyLastToolCacheBreakpoint(tools, marker) : tools;
|
|
282
|
-
// --- Build hooks --------------------------------------------------------
|
|
283
|
-
const hooks = buildStepHooks({
|
|
284
|
-
cachePath,
|
|
285
|
-
emit,
|
|
286
|
-
emissionState,
|
|
287
|
-
marker,
|
|
288
|
-
session,
|
|
289
|
-
});
|
|
290
300
|
// --- Execute via ToolLoopAgent ------------------------------------------
|
|
291
301
|
/*
|
|
292
302
|
* The `onError` override suppresses the AI SDK's default
|
|
@@ -321,146 +331,215 @@ export function createToolLoopHarness(config) {
|
|
|
321
331
|
}
|
|
322
332
|
}
|
|
323
333
|
}
|
|
324
|
-
const instructions = systemMessages.length > 0
|
|
325
|
-
? [
|
|
326
|
-
...(session.agent.system
|
|
327
|
-
? [{ role: "system", content: session.agent.system }]
|
|
328
|
-
: []),
|
|
329
|
-
...systemMessages,
|
|
330
|
-
]
|
|
331
|
-
: session.agent.system || undefined;
|
|
332
|
-
const agentSettings = {
|
|
333
|
-
headers: attributionHeaders,
|
|
334
|
-
instructions,
|
|
335
|
-
model,
|
|
336
|
-
onError() { },
|
|
337
|
-
onStepFinish: hooks.onStepFinish,
|
|
338
|
-
prepareStep: hooks.prepareStep,
|
|
339
|
-
runtimeContext: buildTelemetryRuntimeContext(telemetryConfig, session),
|
|
340
|
-
stopWhen: isStepCount(1),
|
|
341
|
-
telemetry: enrichTelemetry(telemetryConfig, agentName),
|
|
342
|
-
tools: effectiveTools,
|
|
343
|
-
};
|
|
344
|
-
const agent = new ToolLoopAgent(agentSettings);
|
|
345
334
|
const modelMessages = nonSystemMessages;
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Assembles the effective toolset, instructions, and ToolLoopAgent
|
|
337
|
+
* for one attempt of this step, then runs the model call.
|
|
338
|
+
*
|
|
339
|
+
* Called twice in the recovery path: once for the original attempt
|
|
340
|
+
* with full tools, and once after the gateway rejects a
|
|
341
|
+
* provider-specific tool — the retry passes `disabledProviderTools`
|
|
342
|
+
* to drop the offending tool and `extraSystemNote` to tell the
|
|
343
|
+
* model why a capability was removed.
|
|
344
|
+
*/
|
|
345
|
+
const runOneModelCall = async (opts) => {
|
|
346
|
+
const tools = await buildToolSetWithProviderTools({
|
|
347
|
+
approvedTools,
|
|
348
|
+
capabilities: config.capabilities,
|
|
349
|
+
disabledProviderTools: opts.disabledProviderTools,
|
|
350
|
+
modelReference: session.agent.modelReference,
|
|
351
|
+
tools: config.tools,
|
|
352
|
+
});
|
|
353
|
+
if (registry !== undefined && discovered !== undefined) {
|
|
354
|
+
const connectionTools = await resolveConnectionToolsFromState(registry, discovered, {
|
|
355
|
+
approvedTools,
|
|
356
|
+
existingToolNames: new Set(Object.keys(tools)),
|
|
356
357
|
});
|
|
357
|
-
|
|
358
|
-
/*
|
|
359
|
-
* AI SDK `StepResult` is a class whose `content`,
|
|
360
|
-
* `toolCalls`, `toolResults`, `text` are prototype getters.
|
|
361
|
-
* Each field is read explicitly here rather than via spread
|
|
362
|
-
* so the returned plain object carries the values — spread
|
|
363
|
-
* would copy only own enumerable properties and the
|
|
364
|
-
* downstream `extractQuestionInputRequests` would crash on
|
|
365
|
-
* `toolCalls === undefined`.
|
|
366
|
-
*/
|
|
367
|
-
return {
|
|
368
|
-
content: stepResult.content,
|
|
369
|
-
finishReason: stepResult.finishReason,
|
|
370
|
-
response: {
|
|
371
|
-
...stepResult.response,
|
|
372
|
-
messages: [
|
|
373
|
-
{ role: "tool", content: [...inlineToolResultParts] },
|
|
374
|
-
...stepResult.response.messages,
|
|
375
|
-
],
|
|
376
|
-
},
|
|
377
|
-
text: stepResult.text,
|
|
378
|
-
toolCalls: stepResult.toolCalls,
|
|
379
|
-
toolResults: stepResult.toolResults,
|
|
380
|
-
usage: stepResult.usage,
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
return stepResult;
|
|
358
|
+
Object.assign(tools, connectionTools);
|
|
384
359
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
360
|
+
const effectiveTools = marker ? applyLastToolCacheBreakpoint(tools, marker) : tools;
|
|
361
|
+
// Pin gateway routing to the provider that owns any
|
|
362
|
+
// provider-specific tool in this step's toolset. Converts a
|
|
363
|
+
// transient primary outage into a retryable 503 instead of
|
|
364
|
+
// routing to an incompatible fallback provider. Skipped on the
|
|
365
|
+
// recovery retry because the offending tool was dropped — any
|
|
366
|
+
// provider can serve the request now.
|
|
367
|
+
const gatewayPinProvider = resolveGatewayPinForStep({
|
|
368
|
+
cachePath,
|
|
369
|
+
modelReference: session.agent.modelReference,
|
|
370
|
+
tools: effectiveTools,
|
|
371
|
+
});
|
|
372
|
+
// Build instructions, optionally prepending the recovery
|
|
373
|
+
// system note so the model sees one explicit signal that a
|
|
374
|
+
// capability was removed for this attempt.
|
|
375
|
+
const extraSystemEntry = opts.extraSystemNote
|
|
376
|
+
? [{ role: "system", content: opts.extraSystemNote }]
|
|
377
|
+
: [];
|
|
378
|
+
const baseSystemEntry = session.agent.system
|
|
379
|
+
? [{ role: "system", content: session.agent.system }]
|
|
380
|
+
: [];
|
|
381
|
+
const instructions = systemMessages.length > 0 || extraSystemEntry.length > 0
|
|
382
|
+
? [...extraSystemEntry, ...baseSystemEntry, ...systemMessages]
|
|
383
|
+
: session.agent.system || undefined;
|
|
384
|
+
const hooks = buildStepHooks({
|
|
385
|
+
cachePath,
|
|
386
|
+
emit,
|
|
387
|
+
emissionState,
|
|
388
|
+
emitStepStarted: opts.suppressStepStartedEmission !== true,
|
|
389
|
+
gatewayPinProvider,
|
|
390
|
+
marker,
|
|
391
|
+
session,
|
|
392
|
+
});
|
|
393
|
+
const agentSettings = {
|
|
394
|
+
headers: attributionHeaders,
|
|
395
|
+
instructions,
|
|
396
|
+
model,
|
|
397
|
+
onError() { },
|
|
398
|
+
onStepFinish: hooks.onStepFinish,
|
|
399
|
+
prepareStep: hooks.prepareStep,
|
|
400
|
+
runtimeContext: buildTelemetryRuntimeContext(telemetryConfig, session),
|
|
401
|
+
stopWhen: isStepCount(1),
|
|
402
|
+
telemetry: enrichTelemetry(telemetryConfig, agentName),
|
|
403
|
+
tools: effectiveTools,
|
|
404
|
+
};
|
|
405
|
+
const agent = new ToolLoopAgent(agentSettings);
|
|
406
|
+
const executeModelCall = async () => {
|
|
407
|
+
if (emit) {
|
|
408
|
+
const streamResult = await agent.stream({ messages: modelMessages });
|
|
409
|
+
const { inlineActionResultCallIds, inlineToolResultParts } = await emitStreamContent(emit, emissionState, streamResult.fullStream);
|
|
410
|
+
const stepResult = await hooks.stepResult;
|
|
411
|
+
await emitStepActions(emit, emissionState, stepResult, {
|
|
412
|
+
excludedActionToolNames: new Set([ASK_QUESTION_TOOL_NAME]),
|
|
413
|
+
inlineActionResultCallIds,
|
|
414
|
+
tools: config.tools,
|
|
415
|
+
});
|
|
416
|
+
if (inlineToolResultParts.length > 0) {
|
|
417
|
+
/*
|
|
418
|
+
* AI SDK `StepResult` is a class whose `content`,
|
|
419
|
+
* `toolCalls`, `toolResults`, `text` are prototype getters.
|
|
420
|
+
* Each field is read explicitly here rather than via spread
|
|
421
|
+
* so the returned plain object carries the values — spread
|
|
422
|
+
* would copy only own enumerable properties and the
|
|
423
|
+
* downstream `extractQuestionInputRequests` would crash on
|
|
424
|
+
* `toolCalls === undefined`.
|
|
425
|
+
*/
|
|
426
|
+
return {
|
|
427
|
+
content: stepResult.content,
|
|
428
|
+
finishReason: stepResult.finishReason,
|
|
429
|
+
response: {
|
|
430
|
+
...stepResult.response,
|
|
431
|
+
messages: [
|
|
432
|
+
{ role: "tool", content: [...inlineToolResultParts] },
|
|
433
|
+
...stepResult.response.messages,
|
|
434
|
+
],
|
|
435
|
+
},
|
|
436
|
+
text: stepResult.text,
|
|
437
|
+
toolCalls: stepResult.toolCalls,
|
|
438
|
+
toolResults: stepResult.toolResults,
|
|
439
|
+
usage: stepResult.usage,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
return stepResult;
|
|
443
|
+
}
|
|
444
|
+
await agent.generate({ messages: modelMessages });
|
|
445
|
+
return await hooks.stepResult;
|
|
446
|
+
};
|
|
447
|
+
return runModelCallWithRetries(executeModelCall, {
|
|
390
448
|
sessionId: session.sessionId,
|
|
391
449
|
turnId: emissionState.turnId,
|
|
392
450
|
});
|
|
451
|
+
};
|
|
452
|
+
let result;
|
|
453
|
+
try {
|
|
454
|
+
result = await runOneModelCall({});
|
|
393
455
|
}
|
|
394
456
|
catch (error) {
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
if (turnSpan) {
|
|
402
|
-
recordErrorOnSpan(turnSpan, error);
|
|
403
|
-
}
|
|
404
|
-
if (!emit) {
|
|
405
|
-
// Internal harness callers without an emit fn (tests, task-only
|
|
406
|
-
// code paths) get the raw throw. Only runtime-connected harness
|
|
407
|
-
// calls go through the structured failure path below.
|
|
408
|
-
throw error;
|
|
409
|
-
}
|
|
410
|
-
const classification = classifyModelCallError(error);
|
|
411
|
-
const errorId = createErrorId();
|
|
412
|
-
const configSummary = classification === "terminal" ? summarizeKnownModelCallConfigError(error) : null;
|
|
413
|
-
const requestSummary = configSummary === null ? summarizeKnownModelCallRequestError(error) : null;
|
|
414
|
-
const errorMessage = configSummary?.message ?? requestSummary?.message ?? toErrorMessage(error);
|
|
415
|
-
const modelCallDetails = extractModelCallErrorDetails(error);
|
|
416
|
-
const details = buildModelCallFailureDetails({
|
|
417
|
-
configSummary,
|
|
457
|
+
// First, try to recover by dropping any provider-specific tool
|
|
458
|
+
// that the AI Gateway's fallback provider rejected. This converts
|
|
459
|
+
// the otherwise-terminal failure into one extra model call with a
|
|
460
|
+
// degraded toolset — the agent's turn proceeds without the
|
|
461
|
+
// capability the chosen provider could not serve.
|
|
462
|
+
const recoveryResult = await attemptUnsupportedProviderToolRecovery({
|
|
418
463
|
error,
|
|
419
|
-
|
|
420
|
-
modelCallDetails,
|
|
421
|
-
requestSummary,
|
|
422
|
-
});
|
|
423
|
-
const modelCallLogFields = buildModelCallFailureLogFields({
|
|
424
|
-
error,
|
|
425
|
-
errorId,
|
|
426
|
-
modelCallDetails,
|
|
427
|
-
requestSummary,
|
|
464
|
+
runOneModelCall,
|
|
428
465
|
sessionId: session.sessionId,
|
|
429
466
|
turnId: emissionState.turnId,
|
|
430
467
|
});
|
|
431
|
-
if (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
468
|
+
if (recoveryResult.outcome === "recovered") {
|
|
469
|
+
result = recoveryResult.result;
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
// Surface the full cause chain + upstream responseBody to OTel
|
|
473
|
+
// via the turn span. The AI SDK's automatic
|
|
474
|
+
// `span.recordException(err)` on its own `ai.streamText` span
|
|
475
|
+
// only captures `error.stack` and does not traverse `cause`,
|
|
476
|
+
// so the gateway-wrapped upstream 4xx body would otherwise be
|
|
477
|
+
// invisible to OTel providers.
|
|
478
|
+
const finalError = recoveryResult.error;
|
|
479
|
+
if (turnSpan) {
|
|
480
|
+
recordErrorOnSpan(turnSpan, finalError);
|
|
481
|
+
}
|
|
482
|
+
if (!emit) {
|
|
483
|
+
// Internal harness callers without an emit fn (tests, task-only
|
|
484
|
+
// code paths) get the raw throw. Only runtime-connected harness
|
|
485
|
+
// calls go through the structured failure path below.
|
|
486
|
+
throw finalError;
|
|
487
|
+
}
|
|
488
|
+
const classification = classifyModelCallError(finalError);
|
|
489
|
+
const errorId = createErrorId();
|
|
490
|
+
const configSummary = classification === "terminal" ? summarizeKnownModelCallConfigError(finalError) : null;
|
|
491
|
+
const requestSummary = configSummary === null ? summarizeKnownModelCallRequestError(finalError) : null;
|
|
492
|
+
const errorMessage = configSummary?.message ?? requestSummary?.message ?? toErrorMessage(finalError);
|
|
493
|
+
const modelCallDetails = extractModelCallErrorDetails(finalError);
|
|
494
|
+
const details = buildModelCallFailureDetails({
|
|
495
|
+
configSummary,
|
|
496
|
+
error: finalError,
|
|
497
|
+
errorId,
|
|
498
|
+
modelCallDetails,
|
|
499
|
+
requestSummary,
|
|
500
|
+
});
|
|
501
|
+
const modelCallLogFields = buildModelCallFailureLogFields({
|
|
502
|
+
error: finalError,
|
|
503
|
+
errorId,
|
|
504
|
+
modelCallDetails,
|
|
505
|
+
requestSummary,
|
|
506
|
+
sessionId: session.sessionId,
|
|
507
|
+
turnId: emissionState.turnId,
|
|
508
|
+
});
|
|
509
|
+
if (classification === "terminal") {
|
|
510
|
+
if (configSummary !== null) {
|
|
511
|
+
// Recognized configuration failure: log a concise single line
|
|
512
|
+
// and skip the structured SDK dump so the user sees an
|
|
513
|
+
// actionable hint instead of a wall of inspector output.
|
|
514
|
+
log.error(`${configSummary.name}: ${configSummary.message}`, {
|
|
515
|
+
errorId,
|
|
516
|
+
sessionId: session.sessionId,
|
|
517
|
+
turnId: emissionState.turnId,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
log.error(requestSummary?.message ?? "model call failed terminally", modelCallLogFields);
|
|
522
|
+
}
|
|
523
|
+
await emitFailedStep(emit, emissionState, {
|
|
524
|
+
code: "MODEL_CALL_FAILED",
|
|
525
|
+
details,
|
|
526
|
+
message: errorMessage,
|
|
438
527
|
sessionId: session.sessionId,
|
|
439
|
-
turnId: emissionState.turnId,
|
|
440
528
|
});
|
|
529
|
+
return {
|
|
530
|
+
next: { done: true, output: "" },
|
|
531
|
+
session,
|
|
532
|
+
};
|
|
441
533
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
await emitFailedStep(emit, emissionState, {
|
|
534
|
+
log.error(requestSummary?.message ?? "model call failed — parking session for retry by the user", modelCallLogFields);
|
|
535
|
+
emissionState = await emitRecoverableFailedTurn(emit, emissionState, {
|
|
446
536
|
code: "MODEL_CALL_FAILED",
|
|
447
537
|
details,
|
|
448
538
|
message: errorMessage,
|
|
449
|
-
sessionId: session.sessionId,
|
|
450
539
|
});
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
session,
|
|
454
|
-
};
|
|
540
|
+
const parkedSession = setHarnessEmissionState(session, emissionState);
|
|
541
|
+
return { next: null, session: parkedSession };
|
|
455
542
|
}
|
|
456
|
-
log.error(requestSummary?.message ?? "model call failed — parking session for retry by the user", modelCallLogFields);
|
|
457
|
-
emissionState = await emitRecoverableFailedTurn(emit, emissionState, {
|
|
458
|
-
code: "MODEL_CALL_FAILED",
|
|
459
|
-
details,
|
|
460
|
-
message: errorMessage,
|
|
461
|
-
});
|
|
462
|
-
const parkedSession = setHarnessEmissionState(session, emissionState);
|
|
463
|
-
return { next: null, session: parkedSession };
|
|
464
543
|
}
|
|
465
544
|
// --- Handle result ------------------------------------------------------
|
|
466
545
|
return handleStepResult({
|
|
@@ -533,6 +612,68 @@ function buildModelCallFailureLogFields(input) {
|
|
|
533
612
|
}
|
|
534
613
|
return { ...base, error: input.error };
|
|
535
614
|
}
|
|
615
|
+
/**
|
|
616
|
+
* Inspects a model-call failure for the "tool type 'X' is not supported"
|
|
617
|
+
* provider-attempt rejection that AI Gateway returns when a fallback
|
|
618
|
+
* provider cannot serve a provider-specific tool. On a match, retries the
|
|
619
|
+
* step once with the offending tool dropped and a one-shot system note
|
|
620
|
+
* telling the model which capability has been removed.
|
|
621
|
+
*
|
|
622
|
+
* Returns `recovered` when the retry succeeded so the caller can hand
|
|
623
|
+
* the result off to the usual post-step handler. Returns `failed`
|
|
624
|
+
* (with the original error, or the retry's error if the retry also
|
|
625
|
+
* threw) otherwise so the caller's existing terminal/recoverable
|
|
626
|
+
* cascade still runs.
|
|
627
|
+
*
|
|
628
|
+
* Recovery is intentionally scoped to known provider tools — entries in
|
|
629
|
+
* {@link UPSTREAM_TOOL_TYPE_TO_FRAMEWORK_NAME} — so an unrelated
|
|
630
|
+
* upstream rejection cannot accidentally drop a user-authored tool.
|
|
631
|
+
*/
|
|
632
|
+
async function attemptUnsupportedProviderToolRecovery(input) {
|
|
633
|
+
const unsupportedTypes = extractUnsupportedProviderToolTypes(input.error);
|
|
634
|
+
if (unsupportedTypes.length === 0) {
|
|
635
|
+
return { outcome: "failed", error: input.error };
|
|
636
|
+
}
|
|
637
|
+
const toolsToDisable = [];
|
|
638
|
+
for (const type of unsupportedTypes) {
|
|
639
|
+
const frameworkName = resolveFrameworkToolFromUpstreamType(type);
|
|
640
|
+
if (frameworkName !== null && !toolsToDisable.includes(frameworkName)) {
|
|
641
|
+
toolsToDisable.push(frameworkName);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (toolsToDisable.length === 0) {
|
|
645
|
+
return { outcome: "failed", error: input.error };
|
|
646
|
+
}
|
|
647
|
+
log.warn("disabling unsupported provider tool(s); retrying step once", {
|
|
648
|
+
disabled: toolsToDisable,
|
|
649
|
+
sessionId: input.sessionId,
|
|
650
|
+
turnId: input.turnId,
|
|
651
|
+
upstreamTypes: unsupportedTypes,
|
|
652
|
+
});
|
|
653
|
+
try {
|
|
654
|
+
const result = await input.runOneModelCall({
|
|
655
|
+
disabledProviderTools: new Set(toolsToDisable),
|
|
656
|
+
extraSystemNote: buildDisabledToolNote(toolsToDisable),
|
|
657
|
+
suppressStepStartedEmission: true,
|
|
658
|
+
});
|
|
659
|
+
return { outcome: "recovered", result };
|
|
660
|
+
}
|
|
661
|
+
catch (retryError) {
|
|
662
|
+
return { outcome: "failed", error: retryError };
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Builds the one-shot system note prepended to the recovery retry's
|
|
667
|
+
* instructions so the model has explicit context for why a capability
|
|
668
|
+
* disappeared mid-turn.
|
|
669
|
+
*/
|
|
670
|
+
function buildDisabledToolNote(toolNames) {
|
|
671
|
+
const list = toolNames.join(", ");
|
|
672
|
+
const noun = toolNames.length === 1 ? "tool is" : "tools are";
|
|
673
|
+
return (`The following ${noun} not available with the current model and ` +
|
|
674
|
+
`has been removed: ${list}. Proceed using the remaining tools or your ` +
|
|
675
|
+
`training knowledge.`);
|
|
676
|
+
}
|
|
536
677
|
// ---------------------------------------------------------------------------
|
|
537
678
|
// Post-step result handling
|
|
538
679
|
// ---------------------------------------------------------------------------
|
|
@@ -12,10 +12,16 @@ import type { HarnessToolMap } from "#harness/types.js";
|
|
|
12
12
|
* {@link SessionCapabilities.requestInput} is `true`. Sessions without
|
|
13
13
|
* the HITL capability (scheduled task roots and any subagent chain
|
|
14
14
|
* descending from one) never see the tool.
|
|
15
|
+
*
|
|
16
|
+
* Entries listed in `disabledProviderTools` are skipped entirely. Used
|
|
17
|
+
* by the harness recovery path when a gateway fallback provider has
|
|
18
|
+
* rejected a provider-specific tool — the tool is dropped for the
|
|
19
|
+
* retry call so the request can proceed without it.
|
|
15
20
|
*/
|
|
16
21
|
export declare function buildToolSet(input: {
|
|
17
22
|
readonly approvedTools?: ReadonlySet<string>;
|
|
18
23
|
readonly capabilities?: SessionCapabilities;
|
|
24
|
+
readonly disabledProviderTools?: ReadonlySet<string>;
|
|
19
25
|
readonly tools: HarnessToolMap;
|
|
20
26
|
}): ToolSet;
|
|
21
27
|
/**
|
|
@@ -29,10 +35,16 @@ export declare function buildToolSet(input: {
|
|
|
29
35
|
* When a user overrides a provider-managed tool via `defineTool()`, their
|
|
30
36
|
* tool has a real executor and flows through the normal path — no
|
|
31
37
|
* replacement occurs.
|
|
38
|
+
*
|
|
39
|
+
* Tool names listed in `disabledProviderTools` are skipped entirely —
|
|
40
|
+
* both the framework definition and the injected provider tool are
|
|
41
|
+
* omitted from the returned set. Used by the harness recovery path when
|
|
42
|
+
* a gateway fallback provider has rejected a provider-specific tool.
|
|
32
43
|
*/
|
|
33
44
|
export declare function buildToolSetWithProviderTools(input: {
|
|
34
45
|
readonly approvedTools?: ReadonlySet<string>;
|
|
35
46
|
readonly capabilities?: SessionCapabilities;
|
|
47
|
+
readonly disabledProviderTools?: ReadonlySet<string>;
|
|
36
48
|
readonly modelReference: RuntimeModelReference;
|
|
37
49
|
readonly tools: HarnessToolMap;
|
|
38
50
|
}): Promise<ToolSet>;
|
|
@@ -13,14 +13,23 @@ import { resolveWebSearchBackend, resolveWebSearchProviderTool } from "#harness/
|
|
|
13
13
|
* {@link SessionCapabilities.requestInput} is `true`. Sessions without
|
|
14
14
|
* the HITL capability (scheduled task roots and any subagent chain
|
|
15
15
|
* descending from one) never see the tool.
|
|
16
|
+
*
|
|
17
|
+
* Entries listed in `disabledProviderTools` are skipped entirely. Used
|
|
18
|
+
* by the harness recovery path when a gateway fallback provider has
|
|
19
|
+
* rejected a provider-specific tool — the tool is dropped for the
|
|
20
|
+
* retry call so the request can proceed without it.
|
|
16
21
|
*/
|
|
17
22
|
export function buildToolSet(input) {
|
|
18
23
|
const tools = {};
|
|
19
24
|
const canRequestInput = input.capabilities?.requestInput === true;
|
|
25
|
+
const disabled = input.disabledProviderTools;
|
|
20
26
|
for (const definition of input.tools.values()) {
|
|
21
27
|
if (definition.name === ASK_QUESTION_TOOL_NAME && !canRequestInput) {
|
|
22
28
|
continue;
|
|
23
29
|
}
|
|
30
|
+
if (disabled?.has(definition.name)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
24
33
|
tools[definition.name] = tool({
|
|
25
34
|
description: definition.description,
|
|
26
35
|
execute: definition.execute,
|
|
@@ -46,22 +55,31 @@ export function buildToolSet(input) {
|
|
|
46
55
|
* When a user overrides a provider-managed tool via `defineTool()`, their
|
|
47
56
|
* tool has a real executor and flows through the normal path — no
|
|
48
57
|
* replacement occurs.
|
|
58
|
+
*
|
|
59
|
+
* Tool names listed in `disabledProviderTools` are skipped entirely —
|
|
60
|
+
* both the framework definition and the injected provider tool are
|
|
61
|
+
* omitted from the returned set. Used by the harness recovery path when
|
|
62
|
+
* a gateway fallback provider has rejected a provider-specific tool.
|
|
49
63
|
*/
|
|
50
64
|
export async function buildToolSetWithProviderTools(input) {
|
|
65
|
+
const disabled = input.disabledProviderTools;
|
|
51
66
|
const tools = {
|
|
52
67
|
...buildToolSet({
|
|
53
68
|
approvedTools: input.approvedTools,
|
|
54
69
|
capabilities: input.capabilities,
|
|
70
|
+
disabledProviderTools: disabled,
|
|
55
71
|
tools: input.tools,
|
|
56
72
|
}),
|
|
57
73
|
};
|
|
58
74
|
// Inject the real provider tool for web_search when the definition has
|
|
59
75
|
// no local execute (i.e. the framework definition uses the provider sentinel).
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
76
|
+
if (!disabled?.has(WEB_SEARCH_TOOL_DEFINITION.name)) {
|
|
77
|
+
const webSearchTool = input.tools.get(WEB_SEARCH_TOOL_DEFINITION.name);
|
|
78
|
+
if (webSearchTool !== undefined && webSearchTool.execute === undefined) {
|
|
79
|
+
const backend = resolveWebSearchBackend(input.modelReference);
|
|
80
|
+
if (backend !== null) {
|
|
81
|
+
tools[WEB_SEARCH_TOOL_DEFINITION.name] = await resolveWebSearchProviderTool(backend);
|
|
82
|
+
}
|
|
65
83
|
}
|
|
66
84
|
}
|
|
67
85
|
return tools;
|
|
@@ -6,7 +6,7 @@ import { ASH_PACKAGE_NAME } from "#package-name.js";
|
|
|
6
6
|
let cachedPackageInfo;
|
|
7
7
|
// The package build stamps the published version into `dist` so bundled
|
|
8
8
|
// deployments can still report package metadata without resolving package.json.
|
|
9
|
-
const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.22.
|
|
9
|
+
const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.22.2";
|
|
10
10
|
const BUNDLED_FALLBACK_PACKAGE_VERSION_PLACEHOLDER = "__ASH_PACKAGE_VERSION_PLACEHOLDER__";
|
|
11
11
|
const WORKFLOW_MODULE_ALIASES = {
|
|
12
12
|
"workflow/api": "src/compiled/@workflow/core/runtime.js",
|