@tanstack/ai 0.42.0 → 0.43.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/README.md +15 -1
- package/dist/esm/activities/chat/adapter.js +23 -16
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/agent-loop-strategies.d.ts +5 -36
- package/dist/esm/activities/chat/agent-loop-strategies.js +75 -21
- package/dist/esm/activities/chat/agent-loop-strategies.js.map +1 -1
- package/dist/esm/activities/chat/cancel.d.ts +40 -0
- package/dist/esm/activities/chat/cancel.js +54 -0
- package/dist/esm/activities/chat/cancel.js.map +1 -0
- package/dist/esm/activities/chat/index.d.ts +28 -21
- package/dist/esm/activities/chat/index.js +2100 -1813
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/mcp/manager.d.ts +2 -2
- package/dist/esm/activities/chat/mcp/manager.js +90 -77
- package/dist/esm/activities/chat/mcp/manager.js.map +1 -1
- package/dist/esm/activities/chat/mcp/types.d.ts +2 -2
- package/dist/esm/activities/chat/messages.js +397 -346
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/middleware/builder.js +17 -15
- package/dist/esm/activities/chat/middleware/builder.js.map +1 -1
- package/dist/esm/activities/chat/middleware/capabilities.js +78 -43
- package/dist/esm/activities/chat/middleware/capabilities.js.map +1 -1
- package/dist/esm/activities/chat/middleware/compose.d.ts +94 -1
- package/dist/esm/activities/chat/middleware/compose.js +623 -531
- package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
- package/dist/esm/activities/chat/middleware/define.js +12 -5
- package/dist/esm/activities/chat/middleware/define.js.map +1 -1
- package/dist/esm/activities/chat/middleware/index.d.ts +5 -1
- package/dist/esm/activities/chat/middleware/locks.d.ts +50 -0
- package/dist/esm/activities/chat/middleware/locks.js +71 -0
- package/dist/esm/activities/chat/middleware/locks.js.map +1 -0
- package/dist/esm/activities/chat/middleware/pending-turn.d.ts +15 -0
- package/dist/esm/activities/chat/middleware/pending-turn.js +35 -0
- package/dist/esm/activities/chat/middleware/pending-turn.js.map +1 -0
- package/dist/esm/activities/chat/middleware/run-disconnect.d.ts +23 -0
- package/dist/esm/activities/chat/middleware/run-disconnect.js +42 -0
- package/dist/esm/activities/chat/middleware/run-disconnect.js.map +1 -0
- package/dist/esm/activities/chat/middleware/run-store.d.ts +283 -0
- package/dist/esm/activities/chat/middleware/run-store.js +176 -0
- package/dist/esm/activities/chat/middleware/run-store.js.map +1 -0
- package/dist/esm/activities/chat/middleware/sandbox-runtime.js +14 -8
- package/dist/esm/activities/chat/middleware/sandbox-runtime.js.map +1 -1
- package/dist/esm/activities/chat/middleware/tool-cache-middleware.js +79 -70
- package/dist/esm/activities/chat/middleware/tool-cache-middleware.js.map +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +59 -2
- package/dist/esm/activities/chat/middleware/validate.js +23 -28
- package/dist/esm/activities/chat/middleware/validate.js.map +1 -1
- package/dist/esm/activities/chat/stream/json-parser.js +39 -25
- package/dist/esm/activities/chat/stream/json-parser.js.map +1 -1
- package/dist/esm/activities/chat/stream/message-updaters.js +275 -234
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +24 -4
- package/dist/esm/activities/chat/stream/processor.js +1341 -1542
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/stream/strategies.js +69 -53
- package/dist/esm/activities/chat/stream/strategies.js.map +1 -1
- package/dist/esm/activities/chat/tools/approval-schema.d.ts +19 -0
- package/dist/esm/activities/chat/tools/approval-schema.js +117 -0
- package/dist/esm/activities/chat/tools/approval-schema.js.map +1 -0
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js +164 -191
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
- package/dist/esm/activities/chat/tools/lazy-tools.js +24 -12
- package/dist/esm/activities/chat/tools/lazy-tools.js.map +1 -1
- package/dist/esm/activities/chat/tools/schema-converter.js +293 -146
- package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +18 -2
- package/dist/esm/activities/chat/tools/tool-calls.js +522 -531
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-definition.d.ts +75 -16
- package/dist/esm/activities/chat/tools/tool-definition.js +95 -23
- package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
- package/dist/esm/activities/error-payload.js +85 -47
- package/dist/esm/activities/error-payload.js.map +1 -1
- package/dist/esm/activities/generateAudio/adapter.js +22 -15
- package/dist/esm/activities/generateAudio/adapter.js.map +1 -1
- package/dist/esm/activities/generateAudio/index.d.ts +4 -0
- package/dist/esm/activities/generateAudio/index.js +141 -105
- package/dist/esm/activities/generateAudio/index.js.map +1 -1
- package/dist/esm/activities/generateImage/adapter.js +22 -15
- package/dist/esm/activities/generateImage/adapter.js.map +1 -1
- package/dist/esm/activities/generateImage/index.d.ts +4 -0
- package/dist/esm/activities/generateImage/index.js +155 -111
- package/dist/esm/activities/generateImage/index.js.map +1 -1
- package/dist/esm/activities/generateSpeech/adapter.js +22 -15
- package/dist/esm/activities/generateSpeech/adapter.js.map +1 -1
- package/dist/esm/activities/generateSpeech/index.d.ts +4 -0
- package/dist/esm/activities/generateSpeech/index.js +159 -110
- package/dist/esm/activities/generateSpeech/index.js.map +1 -1
- package/dist/esm/activities/generateTranscription/adapter.js +22 -15
- package/dist/esm/activities/generateTranscription/adapter.js.map +1 -1
- package/dist/esm/activities/generateTranscription/index.d.ts +4 -0
- package/dist/esm/activities/generateTranscription/index.js +159 -100
- package/dist/esm/activities/generateTranscription/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/adapter.js +36 -29
- package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.d.ts +143 -19
- package/dist/esm/activities/generateVideo/index.js +456 -279
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/snap.js +60 -48
- package/dist/esm/activities/generateVideo/snap.js.map +1 -1
- package/dist/esm/activities/index.js +8 -34
- package/dist/esm/activities/middleware/index.d.ts +1 -1
- package/dist/esm/activities/middleware/run.d.ts +10 -0
- package/dist/esm/activities/middleware/run.js +53 -29
- package/dist/esm/activities/middleware/run.js.map +1 -1
- package/dist/esm/activities/middleware/types.d.ts +44 -6
- package/dist/esm/activities/stream-generation-result.d.ts +4 -1
- package/dist/esm/activities/stream-generation-result.js +79 -44
- package/dist/esm/activities/stream-generation-result.js.map +1 -1
- package/dist/esm/activities/summarize/adapter.js +22 -15
- package/dist/esm/activities/summarize/adapter.js.map +1 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.js +252 -202
- package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
- package/dist/esm/activities/summarize/index.d.ts +27 -0
- package/dist/esm/activities/summarize/index.js +268 -102
- package/dist/esm/activities/summarize/index.js.map +1 -1
- package/dist/esm/adapter-internals.d.ts +2 -1
- package/dist/esm/adapter-internals.js +4 -11
- package/dist/esm/client.d.ts +25 -3
- package/dist/esm/client.js +131 -64
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/custom-events.d.ts +76 -0
- package/dist/esm/custom-events.js +37 -0
- package/dist/esm/custom-events.js.map +1 -0
- package/dist/esm/delivery-detach.d.ts +50 -0
- package/dist/esm/delivery-detach.js +71 -0
- package/dist/esm/delivery-detach.js.map +1 -0
- package/dist/esm/delivery-disconnect.d.ts +62 -0
- package/dist/esm/delivery-disconnect.js +81 -0
- package/dist/esm/delivery-disconnect.js.map +1 -0
- package/dist/esm/extend-adapter.js +19 -17
- package/dist/esm/extend-adapter.js.map +1 -1
- package/dist/esm/index.d.ts +24 -6
- package/dist/esm/index.js +30 -98
- package/dist/esm/interrupt-resume.d.ts +71 -0
- package/dist/esm/interrupt-resume.js +438 -0
- package/dist/esm/interrupt-resume.js.map +1 -0
- package/dist/esm/interrupt-serialization.d.ts +12 -0
- package/dist/esm/interrupt-serialization.js +178 -0
- package/dist/esm/interrupt-serialization.js.map +1 -0
- package/dist/esm/interrupts.d.ts +84 -0
- package/dist/esm/interrupts.js +31 -0
- package/dist/esm/interrupts.js.map +1 -0
- package/dist/esm/locks.d.ts +10 -0
- package/dist/esm/locks.js +2 -0
- package/dist/esm/logger/console-logger.js +101 -78
- package/dist/esm/logger/console-logger.js.map +1 -1
- package/dist/esm/logger/internal-logger.js +104 -89
- package/dist/esm/logger/internal-logger.js.map +1 -1
- package/dist/esm/logger/resolve.js +54 -49
- package/dist/esm/logger/resolve.js.map +1 -1
- package/dist/esm/logger/types.d.ts +1 -1
- package/dist/esm/middlewares/content-guard.js +142 -148
- package/dist/esm/middlewares/content-guard.js.map +1 -1
- package/dist/esm/middlewares/index.js +2 -6
- package/dist/esm/middlewares/otel.d.ts +3 -1
- package/dist/esm/middlewares/otel.js +599 -732
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/middlewares/usage-attributes.js +47 -40
- package/dist/esm/middlewares/usage-attributes.js.map +1 -1
- package/dist/esm/realtime/event-emitter.js +24 -25
- package/dist/esm/realtime/event-emitter.js.map +1 -1
- package/dist/esm/realtime/index.d.ts +5 -9
- package/dist/esm/realtime/index.js +29 -6
- package/dist/esm/realtime/index.js.map +1 -1
- package/dist/esm/scope.d.ts +47 -0
- package/dist/esm/stream-durability.d.ts +171 -0
- package/dist/esm/stream-durability.js +295 -0
- package/dist/esm/stream-durability.js.map +1 -0
- package/dist/esm/stream-to-response.d.ts +178 -13
- package/dist/esm/stream-to-response.js +663 -115
- package/dist/esm/stream-to-response.js.map +1 -1
- package/dist/esm/strip-to-spec-middleware.js +30 -16
- package/dist/esm/strip-to-spec-middleware.js.map +1 -1
- package/dist/esm/system-prompts.js +27 -21
- package/dist/esm/system-prompts.js.map +1 -1
- package/dist/esm/tool-registry.js +72 -45
- package/dist/esm/tool-registry.js.map +1 -1
- package/dist/esm/tools/provider-tool.js +14 -5
- package/dist/esm/tools/provider-tool.js.map +1 -1
- package/dist/esm/types.d.ts +321 -42
- package/dist/esm/types.js +2 -0
- package/dist/esm/utilities/ag-ui-wire.js +79 -93
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
- package/dist/esm/utilities/chat-params.d.ts +26 -4
- package/dist/esm/utilities/chat-params.js +218 -92
- package/dist/esm/utilities/chat-params.js.map +1 -1
- package/dist/esm/utilities/errors.js +28 -18
- package/dist/esm/utilities/errors.js.map +1 -1
- package/dist/esm/utilities/media-prompt.js +46 -41
- package/dist/esm/utilities/media-prompt.js.map +1 -1
- package/dist/esm/utilities/numbers.js +13 -10
- package/dist/esm/utilities/numbers.js.map +1 -1
- package/dist/esm/utilities/provider-executed.js +20 -11
- package/dist/esm/utilities/provider-executed.js.map +1 -1
- package/dist/esm/utilities/sampling-keys.js +31 -19
- package/dist/esm/utilities/sampling-keys.js.map +1 -1
- package/dist/esm/utilities/tool-result.js +42 -30
- package/dist/esm/utilities/tool-result.js.map +1 -1
- package/dist/esm/utilities/usage.js +27 -9
- package/dist/esm/utilities/usage.js.map +1 -1
- package/dist/esm/utils.js +26 -18
- package/dist/esm/utils.js.map +1 -1
- package/package.json +10 -6
- package/skills/ai-core/SKILL.md +69 -18
- package/skills/ai-core/adapter-configuration/SKILL.md +44 -21
- package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +1 -3
- package/skills/ai-core/adapter-configuration/references/byteplus-adapter.md +148 -0
- package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +2 -6
- package/skills/ai-core/adapter-configuration/references/groq-adapter.md +2 -6
- package/skills/ai-core/adapter-configuration/references/openai-adapter.md +1 -3
- package/skills/ai-core/ag-ui-protocol/SKILL.md +1 -1
- package/skills/ai-core/chat-experience/SKILL.md +98 -11
- package/skills/ai-core/client-persistence/SKILL.md +277 -0
- package/skills/ai-core/custom-backend-integration/SKILL.md +1 -1
- package/skills/ai-core/debug-logging/SKILL.md +1 -1
- package/skills/ai-core/locks/SKILL.md +143 -0
- package/skills/ai-core/media-generation/SKILL.md +144 -12
- package/skills/ai-core/middleware/SKILL.md +258 -33
- package/skills/ai-core/structured-outputs/SKILL.md +1 -1
- package/skills/ai-core/tool-calling/SKILL.md +54 -61
- package/src/activities/chat/agent-loop-strategies.ts +5 -39
- package/src/activities/chat/cancel.ts +81 -0
- package/src/activities/chat/index.ts +1091 -200
- package/src/activities/chat/mcp/manager.ts +4 -4
- package/src/activities/chat/mcp/types.ts +2 -2
- package/src/activities/chat/messages.ts +5 -3
- package/src/activities/chat/middleware/builder.ts +1 -1
- package/src/activities/chat/middleware/compose.ts +186 -9
- package/src/activities/chat/middleware/index.ts +26 -0
- package/src/activities/chat/middleware/locks.ts +102 -0
- package/src/activities/chat/middleware/pending-turn.ts +47 -0
- package/src/activities/chat/middleware/run-disconnect.ts +62 -0
- package/src/activities/chat/middleware/run-store.ts +412 -0
- package/src/activities/chat/middleware/types.ts +62 -1
- package/src/activities/chat/stream/processor.ts +189 -5
- package/src/activities/chat/tools/approval-schema.ts +205 -0
- package/src/activities/chat/tools/tool-calls.ts +106 -13
- package/src/activities/chat/tools/tool-definition.ts +210 -39
- package/src/activities/generateAudio/index.ts +20 -3
- package/src/activities/generateImage/index.ts +20 -3
- package/src/activities/generateSpeech/index.ts +25 -3
- package/src/activities/generateTranscription/index.ts +26 -3
- package/src/activities/generateVideo/index.ts +345 -82
- package/src/activities/middleware/index.ts +2 -0
- package/src/activities/middleware/run.ts +31 -0
- package/src/activities/middleware/types.ts +49 -5
- package/src/activities/stream-generation-result.ts +30 -2
- package/src/activities/summarize/chat-stream-summarize.ts +5 -0
- package/src/activities/summarize/index.ts +200 -10
- package/src/adapter-internals.ts +10 -1
- package/src/client.ts +244 -0
- package/src/custom-events.ts +107 -0
- package/src/delivery-detach.ts +72 -0
- package/src/delivery-disconnect.ts +84 -0
- package/src/index.ts +138 -1
- package/src/interrupt-resume.ts +824 -0
- package/src/interrupt-serialization.ts +183 -0
- package/src/interrupts.ts +146 -0
- package/src/locks.ts +17 -0
- package/src/logger/types.ts +1 -1
- package/src/middlewares/otel.ts +23 -5
- package/src/realtime/index.ts +5 -9
- package/src/scope.ts +47 -0
- package/src/stream-durability.ts +598 -0
- package/src/stream-to-response.ts +1051 -95
- package/src/strip-to-spec-middleware.ts +3 -3
- package/src/types.ts +405 -45
- package/src/utilities/chat-params.ts +245 -55
- package/dist/esm/activities/index.js.map +0 -1
- package/dist/esm/adapter-internals.js.map +0 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/middlewares/index.js.map +0 -1
|
@@ -1,1842 +1,2129 @@
|
|
|
1
|
-
import { devtoolsMiddleware } from "@tanstack/ai-event-client";
|
|
2
|
-
import { undoNullWidening } from "@tanstack/ai-utils";
|
|
3
1
|
import { stripToSpecMiddleware } from "../../strip-to-spec-middleware.js";
|
|
4
|
-
import {
|
|
2
|
+
import { isCancelRequestedReason } from "./cancel.js";
|
|
3
|
+
import { CapabilityRegistry } from "./middleware/capabilities.js";
|
|
4
|
+
import { getRunDetached } from "./middleware/run-store.js";
|
|
5
|
+
import { publishRunDetachedSignal } from "../../delivery-detach.js";
|
|
6
|
+
import { publishRunDisconnectHandler } from "../../delivery-disconnect.js";
|
|
7
|
+
import { EventType } from "../../types.js";
|
|
5
8
|
import { resolveDebugOption } from "../../logger/resolve.js";
|
|
9
|
+
import { streamToText } from "../../stream-to-response.js";
|
|
10
|
+
import { canonicalInterruptJson, digestInterruptJson } from "../../interrupt-serialization.js";
|
|
11
|
+
import "../../interrupts.js";
|
|
12
|
+
import { convertSchemaForStructuredOutput, convertSchemaToJsonSchema, isStandardSchema, parseWithStandardSchema } from "./tools/schema-converter.js";
|
|
13
|
+
import { hashSchemaInput, normalizeApprovalSchema } from "./tools/approval-schema.js";
|
|
14
|
+
import { INTERRUPT_BINDING_METADATA_KEY, InterruptResumeValidationError, readUnopenedInterruptBinding, validateInterruptResumeBatch } from "../../interrupt-resume.js";
|
|
6
15
|
import { normalizeToolResult } from "../../utilities/tool-result.js";
|
|
7
16
|
import { isProviderExecutedToolCall } from "../../utilities/provider-executed.js";
|
|
8
17
|
import { LazyToolManager } from "./tools/lazy-tool-manager.js";
|
|
9
|
-
import {
|
|
10
|
-
import { convertSchemaForStructuredOutput, isStandardSchema, parseWithStandardSchema, convertSchemaToJsonSchema } from "./tools/schema-converter.js";
|
|
18
|
+
import { MiddlewareAbortError, ToolCallManager, executeToolCalls } from "./tools/tool-calls.js";
|
|
11
19
|
import { maxIterations } from "./agent-loop-strategies.js";
|
|
12
20
|
import { convertMessagesToModelMessages, generateMessageId } from "./messages.js";
|
|
13
21
|
import { MiddlewareRunner } from "./middleware/compose.js";
|
|
14
22
|
import { provideSandboxRuntime } from "./middleware/sandbox-runtime.js";
|
|
15
|
-
import {
|
|
23
|
+
import { provideRunDisconnect } from "./middleware/run-disconnect.js";
|
|
16
24
|
import { validateCapabilities } from "./middleware/validate.js";
|
|
17
25
|
import { MCPManager } from "./mcp/manager.js";
|
|
18
|
-
import
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
import "./adapter.js";
|
|
27
|
+
import { devtoolsMiddleware } from "@tanstack/ai-event-client";
|
|
28
|
+
import { undoNullWidening } from "@tanstack/ai-utils";
|
|
29
|
+
//#region src/activities/chat/index.ts
|
|
30
|
+
/**
|
|
31
|
+
* Text Activity
|
|
32
|
+
*
|
|
33
|
+
* Handles agentic text generation, one-shot text generation, and agentic structured output.
|
|
34
|
+
* This is a self-contained module with implementation, types, and JSDoc.
|
|
35
|
+
*/
|
|
36
|
+
/** The adapter kind this activity handles */
|
|
37
|
+
var kind = "text";
|
|
38
|
+
var interruptBindingMetadataKey = INTERRUPT_BINDING_METADATA_KEY;
|
|
39
|
+
function isInterruptSubmissionError(value) {
|
|
40
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
41
|
+
if (!("scope" in value) || !("code" in value) || !("message" in value) || !("source" in value) || !("retryable" in value) || !("threadId" in value) || !("interruptedRunId" in value) || !("generation" in value) || typeof value.code !== "string" || typeof value.message !== "string" || typeof value.retryable !== "boolean" || typeof value.threadId !== "string" || typeof value.interruptedRunId !== "string" || typeof value.generation !== "number") return false;
|
|
42
|
+
if (value.scope === "item") return "interruptId" in value && typeof value.interruptId === "string" && (value.source === "client" || value.source === "server");
|
|
43
|
+
return value.scope === "batch" && "interruptIds" in value && Array.isArray(value.interruptIds) && value.interruptIds.every((id) => typeof id === "string") && (value.source === "client" || value.source === "server" || value.source === "transport");
|
|
22
44
|
}
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
return Math.floor(cap);
|
|
45
|
+
function structuralInterruptFailure(error) {
|
|
46
|
+
if (!(error instanceof Error) || error.name !== "InterruptResumeValidationError" || !("errors" in error) || !Array.isArray(error.errors) || error.errors.length === 0 || !error.errors.every(isInterruptSubmissionError)) return;
|
|
47
|
+
return {
|
|
48
|
+
error,
|
|
49
|
+
errors: error.errors
|
|
50
|
+
};
|
|
31
51
|
}
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
52
|
+
function normalizePublicInterruptBinding(value, expectedInterruptId) {
|
|
53
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return;
|
|
54
|
+
const binding = Object.fromEntries(Object.entries(value));
|
|
55
|
+
if (binding.interruptId !== expectedInterruptId || binding.v !== void 0 && binding.v !== 1 || typeof binding.interruptedRunId !== "string" || typeof binding.generation !== "number" || !Number.isInteger(binding.generation) || binding.generation < 0 || typeof binding.responseSchemaHash !== "string" || binding.expiresAt !== void 0 && typeof binding.expiresAt !== "string") return;
|
|
56
|
+
const base = {
|
|
57
|
+
v: 1,
|
|
58
|
+
interruptId: binding.interruptId,
|
|
59
|
+
interruptedRunId: binding.interruptedRunId,
|
|
60
|
+
generation: binding.generation,
|
|
61
|
+
responseSchemaHash: binding.responseSchemaHash,
|
|
62
|
+
...typeof binding.expiresAt === "string" ? { expiresAt: binding.expiresAt } : {}
|
|
63
|
+
};
|
|
64
|
+
if (binding.kind === "generic") return {
|
|
65
|
+
kind: binding.kind,
|
|
66
|
+
...base
|
|
67
|
+
};
|
|
68
|
+
if (typeof binding.toolName !== "string" || typeof binding.toolCallId !== "string") return;
|
|
69
|
+
if (binding.kind === "client-tool-execution" && typeof binding.outputSchemaHash === "string") return {
|
|
70
|
+
kind: binding.kind,
|
|
71
|
+
...base,
|
|
72
|
+
toolName: binding.toolName,
|
|
73
|
+
toolCallId: binding.toolCallId,
|
|
74
|
+
outputSchemaHash: binding.outputSchemaHash
|
|
75
|
+
};
|
|
76
|
+
if (binding.kind === "tool-approval" && Object.prototype.hasOwnProperty.call(binding, "originalArgs") && typeof binding.inputSchemaHash === "string" && typeof binding.approvalSchemaHash === "string") return {
|
|
77
|
+
kind: binding.kind,
|
|
78
|
+
...base,
|
|
79
|
+
toolName: binding.toolName,
|
|
80
|
+
toolCallId: binding.toolCallId,
|
|
81
|
+
originalArgs: binding.originalArgs,
|
|
82
|
+
inputSchemaHash: binding.inputSchemaHash,
|
|
83
|
+
approvalSchemaHash: binding.approvalSchemaHash
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Create typed options for the chat() function without executing.
|
|
88
|
+
* This is useful for pre-defining configurations with full type inference.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* const chatOptions = createChatOptions({
|
|
93
|
+
* adapter: anthropicText('claude-sonnet-4-5'),
|
|
94
|
+
* })
|
|
95
|
+
*
|
|
96
|
+
* const stream = chat({ ...chatOptions, messages })
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
function createChatOptions(options) {
|
|
100
|
+
return options;
|
|
44
101
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
/** Tool calls in the most recent budgeted batch (0 when none). */
|
|
63
|
-
lastTurnToolCallCount = 0;
|
|
64
|
-
/** Tool call IDs already counted toward `toolCallCount` (avoids double-count on resume). */
|
|
65
|
-
countedToolCallIds = /* @__PURE__ */ new Set();
|
|
66
|
-
lastFinishReason = null;
|
|
67
|
-
streamStartTime = 0;
|
|
68
|
-
totalChunkCount = 0;
|
|
69
|
-
currentMessageId = null;
|
|
70
|
-
accumulatedContent = "";
|
|
71
|
-
accumulatedThinking = [];
|
|
72
|
-
currentThinkingContent = "";
|
|
73
|
-
currentThinkingSignature = "";
|
|
74
|
-
eventOptions;
|
|
75
|
-
eventToolNames;
|
|
76
|
-
finishedEvent = null;
|
|
77
|
-
earlyTermination = false;
|
|
78
|
-
toolPhase = "continue";
|
|
79
|
-
cyclePhase = "processText";
|
|
80
|
-
maxToolCallsPerTurn;
|
|
81
|
-
// Client state extracted from initial messages (before conversion to ModelMessage)
|
|
82
|
-
initialApprovals;
|
|
83
|
-
initialClientToolResults;
|
|
84
|
-
// AG-UI protocol IDs
|
|
85
|
-
threadId;
|
|
86
|
-
runIdOverride;
|
|
87
|
-
parentRunIdOverride;
|
|
88
|
-
// Middleware support
|
|
89
|
-
middlewareRunner;
|
|
90
|
-
middlewareCtx;
|
|
91
|
-
sandboxFileQueue = [];
|
|
92
|
-
deferredPromises = [];
|
|
93
|
-
abortReason;
|
|
94
|
-
middlewareAbortController;
|
|
95
|
-
// Combines the caller's signal with middleware abort() so running tools
|
|
96
|
-
// observe both cancellation sources via ctx.abortSignal.
|
|
97
|
-
toolAbortSignal;
|
|
98
|
-
terminalHookCalled = false;
|
|
99
|
-
logger;
|
|
100
|
-
// Structured-output finalization state (populated by runStructuredFinalization)
|
|
101
|
-
structuredOutputResult = null;
|
|
102
|
-
// Native combined mode: tracks whether we've already emitted the synthetic
|
|
103
|
-
// `structured-output.start` event before the schema-constrained final-turn
|
|
104
|
-
// text begins streaming. The event must precede the first
|
|
105
|
-
// TEXT_MESSAGE_START so the client-side StreamProcessor routes the JSON
|
|
106
|
-
// deltas into a StructuredOutputPart instead of a plain TextPart.
|
|
107
|
-
combinedStartEmitted = false;
|
|
108
|
-
// Native combined mode: messageId we want the synthetic
|
|
109
|
-
// `structured-output.start` (and any error emitted before deltas arrive)
|
|
110
|
-
// to carry, so the client matches it to the streaming text deltas.
|
|
111
|
-
combinedStructuredMessageId = null;
|
|
112
|
-
// Holds the validated value when `finalStructuredOutput.validate` is provided
|
|
113
|
-
// and succeeds. Distinct from `structuredOutputResult.data` (the normalized
|
|
114
|
-
// but unvalidated payload from the structured-output.complete chunk).
|
|
115
|
-
validatedStructuredOutput = void 0;
|
|
116
|
-
hasValidatedStructuredOutput = false;
|
|
117
|
-
finalizationError = null;
|
|
118
|
-
finalStructuredOutput;
|
|
119
|
-
constructor(config, logger) {
|
|
120
|
-
this.logger = logger;
|
|
121
|
-
this.adapter = config.adapter;
|
|
122
|
-
this.finalStructuredOutput = config.finalStructuredOutput;
|
|
123
|
-
this.params = config.params;
|
|
124
|
-
this.systemPrompts = config.params.systemPrompts || [];
|
|
125
|
-
this.loopStrategy = config.params.agentLoopStrategy || maxIterations(5);
|
|
126
|
-
this.maxToolCallsPerTurn = resolveMaxToolCallsPerTurn(
|
|
127
|
-
config.params.maxToolCallsPerTurn
|
|
128
|
-
);
|
|
129
|
-
this.initialMessageCount = config.params.messages.length;
|
|
130
|
-
const { approvals, clientToolResults } = this.extractClientStateFromOriginalMessages(
|
|
131
|
-
config.params.messages
|
|
132
|
-
);
|
|
133
|
-
this.initialApprovals = approvals;
|
|
134
|
-
this.initialClientToolResults = clientToolResults;
|
|
135
|
-
this.messages = convertMessagesToModelMessages(config.params.messages);
|
|
136
|
-
this.lazyToolManager = new LazyToolManager(
|
|
137
|
-
config.params.tools || [],
|
|
138
|
-
this.messages,
|
|
139
|
-
config.params.lazyToolsConfig
|
|
140
|
-
);
|
|
141
|
-
this.tools = this.lazyToolManager.getActiveTools();
|
|
142
|
-
this.toolCallManager = new ToolCallManager(this.tools);
|
|
143
|
-
this.requestId = this.createId("chat");
|
|
144
|
-
this.streamId = this.createId("stream");
|
|
145
|
-
this.effectiveRequest = config.params.abortController ? { signal: config.params.abortController.signal } : void 0;
|
|
146
|
-
this.effectiveSignal = config.params.abortController?.signal;
|
|
147
|
-
this.threadId = config.params.threadId || config.params.conversationId || this.createId("thread");
|
|
148
|
-
this.runIdOverride = config.params.runId;
|
|
149
|
-
this.parentRunIdOverride = config.params.parentRunId;
|
|
150
|
-
const allMiddleware = [
|
|
151
|
-
devtoolsMiddleware(),
|
|
152
|
-
...config.middleware || [],
|
|
153
|
-
stripToSpecMiddleware()
|
|
154
|
-
];
|
|
155
|
-
this.middlewareRunner = new MiddlewareRunner(allMiddleware, logger);
|
|
156
|
-
this.middlewareAbortController = new AbortController();
|
|
157
|
-
this.toolAbortSignal = combineAbortSignals(
|
|
158
|
-
this.effectiveSignal,
|
|
159
|
-
this.middlewareAbortController.signal
|
|
160
|
-
);
|
|
161
|
-
this.middlewareCtx = {
|
|
162
|
-
requestId: this.requestId,
|
|
163
|
-
streamId: this.streamId,
|
|
164
|
-
runId: this.runIdOverride ?? this.requestId,
|
|
165
|
-
threadId: this.threadId,
|
|
166
|
-
// Legacy alias kept on the ctx so middleware that reads
|
|
167
|
-
// `ctx.conversationId` keeps working. Always equals `threadId`.
|
|
168
|
-
conversationId: this.threadId,
|
|
169
|
-
phase: "init",
|
|
170
|
-
iteration: 0,
|
|
171
|
-
chunkIndex: 0,
|
|
172
|
-
signal: this.effectiveSignal,
|
|
173
|
-
abort: (reason) => {
|
|
174
|
-
this.abortReason = reason;
|
|
175
|
-
this.middlewareAbortController?.abort(reason);
|
|
176
|
-
},
|
|
177
|
-
context: config.context,
|
|
178
|
-
defer: (promise) => {
|
|
179
|
-
this.deferredPromises.push(promise);
|
|
180
|
-
},
|
|
181
|
-
// Provider / adapter info
|
|
182
|
-
activity: "chat",
|
|
183
|
-
provider: config.adapter.name,
|
|
184
|
-
model: config.params.model,
|
|
185
|
-
source: "server",
|
|
186
|
-
streaming: true,
|
|
187
|
-
// Config-derived (updated in beforeRun and applyMiddlewareConfig)
|
|
188
|
-
systemPrompts: this.systemPrompts,
|
|
189
|
-
toolNames: void 0,
|
|
190
|
-
options: void 0,
|
|
191
|
-
modelOptions: config.params.modelOptions,
|
|
192
|
-
// Computed
|
|
193
|
-
messageCount: this.initialMessageCount,
|
|
194
|
-
hasTools: this.tools.length > 0,
|
|
195
|
-
// Mutable per-iteration
|
|
196
|
-
currentMessageId: null,
|
|
197
|
-
accumulatedContent: "",
|
|
198
|
-
// References
|
|
199
|
-
messages: this.messages,
|
|
200
|
-
createId: (prefix) => this.createId(prefix),
|
|
201
|
-
// Capability bookkeeping for this request (populated by middleware setup)
|
|
202
|
-
capabilities: new CapabilityRegistry(),
|
|
203
|
-
// Convenience accessors that delegate to a capability handle's own
|
|
204
|
-
// tuple getter/provider, keyed by this context. `getX(ctx)` and
|
|
205
|
-
// `ctx.get(X)` are interchangeable.
|
|
206
|
-
get: (capability) => capability[0](this.middlewareCtx),
|
|
207
|
-
getOptional: (capability) => capability[0](this.middlewareCtx, { optional: true }),
|
|
208
|
-
provide: (capability, value) => capability[1](this.middlewareCtx, value)
|
|
209
|
-
};
|
|
210
|
-
provideSandboxRuntime(this.middlewareCtx, {
|
|
211
|
-
logger: this.logger,
|
|
212
|
-
emit: (event) => {
|
|
213
|
-
this.logger.sandbox(`file ${event.type} ${event.path}`, {
|
|
214
|
-
event: {
|
|
215
|
-
type: event.type,
|
|
216
|
-
path: event.path,
|
|
217
|
-
timestamp: event.timestamp
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
void this.middlewareRunner.runSandboxFile(this.middlewareCtx, event).catch((err) => {
|
|
221
|
-
this.logger.errors("sandbox file hook failed", { error: err });
|
|
222
|
-
});
|
|
223
|
-
this.sandboxFileQueue.push(
|
|
224
|
-
this.createCustomEventChunk("sandbox.file", {
|
|
225
|
-
type: event.type,
|
|
226
|
-
path: event.path,
|
|
227
|
-
timestamp: event.timestamp
|
|
228
|
-
})
|
|
229
|
-
);
|
|
230
|
-
},
|
|
231
|
-
emitFileDiff: (value) => {
|
|
232
|
-
this.sandboxFileQueue.push(
|
|
233
|
-
this.createCustomEventChunk("sandbox.file.diff", value)
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
/** Get the accumulated content after the chat loop completes */
|
|
239
|
-
getAccumulatedContent() {
|
|
240
|
-
return this.accumulatedContent;
|
|
241
|
-
}
|
|
242
|
-
/** Get the final messages array after the chat loop completes */
|
|
243
|
-
getMessages() {
|
|
244
|
-
return this.messages;
|
|
245
|
-
}
|
|
246
|
-
/** Returns the structured-output result if finalization ran successfully. */
|
|
247
|
-
getStructuredOutputResult() {
|
|
248
|
-
return this.structuredOutputResult;
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Returns the validated structured-output value (the result of running
|
|
252
|
-
* `finalStructuredOutput.validate` against the raw structured-output data)
|
|
253
|
-
* wrapped in a `{ value }` object so callers can distinguish "no validation
|
|
254
|
-
* happened" from "validation produced undefined". Returns `null` when no
|
|
255
|
-
* validator was configured or validation hasn't been performed yet.
|
|
256
|
-
*/
|
|
257
|
-
getValidatedStructuredOutput() {
|
|
258
|
-
return this.hasValidatedStructuredOutput ? { value: this.validatedStructuredOutput } : null;
|
|
259
|
-
}
|
|
260
|
-
/** Returns the recorded finalization error, if any. */
|
|
261
|
-
getFinalizationError() {
|
|
262
|
-
return this.finalizationError;
|
|
263
|
-
}
|
|
264
|
-
async *run() {
|
|
265
|
-
this.beforeRun();
|
|
266
|
-
this.logger.agentLoop("run started", {
|
|
267
|
-
threadId: this.middlewareCtx.threadId
|
|
268
|
-
});
|
|
269
|
-
try {
|
|
270
|
-
await this.middlewareRunner.runSetup(this.middlewareCtx);
|
|
271
|
-
this.middlewareCtx.phase = "init";
|
|
272
|
-
const initialConfig = this.buildMiddlewareConfig();
|
|
273
|
-
const transformedConfig = await this.middlewareRunner.runOnConfig(
|
|
274
|
-
this.middlewareCtx,
|
|
275
|
-
initialConfig
|
|
276
|
-
);
|
|
277
|
-
this.applyMiddlewareConfig(transformedConfig);
|
|
278
|
-
await this.middlewareRunner.runOnStart(this.middlewareCtx);
|
|
279
|
-
const pendingPhase = yield* this.checkForPendingToolCalls();
|
|
280
|
-
if (pendingPhase === "wait") {
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
const skipAgentLoop = !!this.finalStructuredOutput && this.tools.length === 0 && this.finalStructuredOutput.nativeCombined !== true;
|
|
284
|
-
if (!skipAgentLoop) {
|
|
285
|
-
do {
|
|
286
|
-
if (this.earlyTermination || this.isCancelled()) {
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
this.logger.agentLoop(`iteration=${this.middlewareCtx.iteration}`, {
|
|
290
|
-
iteration: this.middlewareCtx.iteration
|
|
291
|
-
});
|
|
292
|
-
await this.beginCycle();
|
|
293
|
-
if (this.cyclePhase === "processText") {
|
|
294
|
-
this.middlewareCtx.phase = "beforeModel";
|
|
295
|
-
this.middlewareCtx.iteration = this.iterationCount;
|
|
296
|
-
const iterConfig = this.buildMiddlewareConfig();
|
|
297
|
-
const iterTransformedConfig = await this.middlewareRunner.runOnConfig(
|
|
298
|
-
this.middlewareCtx,
|
|
299
|
-
iterConfig
|
|
300
|
-
);
|
|
301
|
-
this.applyMiddlewareConfig(iterTransformedConfig);
|
|
302
|
-
yield* this.streamModelResponse();
|
|
303
|
-
} else {
|
|
304
|
-
yield* this.processToolCalls();
|
|
305
|
-
}
|
|
306
|
-
this.endCycle();
|
|
307
|
-
} while (this.shouldContinue());
|
|
308
|
-
}
|
|
309
|
-
this.logger.agentLoop("run finished", {
|
|
310
|
-
finishReason: this.lastFinishReason
|
|
311
|
-
});
|
|
312
|
-
if (this.finalStructuredOutput && !this.isCancelled() && !this.finalizationError) {
|
|
313
|
-
if (this.finalStructuredOutput.nativeCombined === true) {
|
|
314
|
-
yield* this.harvestCombinedStructuredOutput();
|
|
315
|
-
} else {
|
|
316
|
-
yield* this.runStructuredFinalization();
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
if (!this.terminalHookCalled && this.toolPhase !== "wait" && !this.isCancelled()) {
|
|
320
|
-
if (this.finalizationError) {
|
|
321
|
-
this.terminalHookCalled = true;
|
|
322
|
-
const errForHook = new Error(
|
|
323
|
-
this.finalizationError.message,
|
|
324
|
-
this.finalizationError.cause !== void 0 ? { cause: this.finalizationError.cause } : void 0
|
|
325
|
-
);
|
|
326
|
-
if (this.finalizationError.code !== void 0) {
|
|
327
|
-
Object.defineProperty(errForHook, "code", {
|
|
328
|
-
value: this.finalizationError.code,
|
|
329
|
-
enumerable: true
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
await this.middlewareRunner.runOnError(this.middlewareCtx, {
|
|
333
|
-
error: errForHook,
|
|
334
|
-
duration: Date.now() - this.streamStartTime
|
|
335
|
-
});
|
|
336
|
-
} else {
|
|
337
|
-
this.terminalHookCalled = true;
|
|
338
|
-
await this.middlewareRunner.runOnFinish(this.middlewareCtx, {
|
|
339
|
-
finishReason: this.lastFinishReason,
|
|
340
|
-
duration: Date.now() - this.streamStartTime,
|
|
341
|
-
content: this.accumulatedContent,
|
|
342
|
-
usage: this.finishedEvent?.usage
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
} catch (error) {
|
|
347
|
-
if (!this.terminalHookCalled) {
|
|
348
|
-
this.terminalHookCalled = true;
|
|
349
|
-
if (error instanceof MiddlewareAbortError) {
|
|
350
|
-
this.abortReason = error.message;
|
|
351
|
-
await this.middlewareRunner.runOnAbort(this.middlewareCtx, {
|
|
352
|
-
reason: error.message,
|
|
353
|
-
duration: Date.now() - this.streamStartTime
|
|
354
|
-
});
|
|
355
|
-
} else {
|
|
356
|
-
this.logger.errors("chat run failed", {
|
|
357
|
-
error,
|
|
358
|
-
threadId: this.middlewareCtx.threadId
|
|
359
|
-
});
|
|
360
|
-
await this.middlewareRunner.runOnError(this.middlewareCtx, {
|
|
361
|
-
error,
|
|
362
|
-
duration: Date.now() - this.streamStartTime
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
if (!(error instanceof MiddlewareAbortError)) {
|
|
367
|
-
throw error;
|
|
368
|
-
}
|
|
369
|
-
} finally {
|
|
370
|
-
if (!this.terminalHookCalled && this.isCancelled()) {
|
|
371
|
-
this.terminalHookCalled = true;
|
|
372
|
-
await this.middlewareRunner.runOnAbort(this.middlewareCtx, {
|
|
373
|
-
reason: this.abortReason,
|
|
374
|
-
duration: Date.now() - this.streamStartTime
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
if (this.deferredPromises.length > 0) {
|
|
378
|
-
await Promise.allSettled(this.deferredPromises);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
beforeRun() {
|
|
383
|
-
this.streamStartTime = Date.now();
|
|
384
|
-
const { tools, metadata } = this.params;
|
|
385
|
-
const options = {};
|
|
386
|
-
if (metadata !== void 0) options.metadata = metadata;
|
|
387
|
-
this.eventOptions = Object.keys(options).length > 0 ? options : void 0;
|
|
388
|
-
this.eventToolNames = tools?.map((t) => t.name);
|
|
389
|
-
this.middlewareCtx.options = this.eventOptions;
|
|
390
|
-
this.middlewareCtx.toolNames = this.eventToolNames;
|
|
391
|
-
}
|
|
392
|
-
async beginCycle() {
|
|
393
|
-
if (this.cyclePhase === "processText") {
|
|
394
|
-
await this.beginIteration();
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
endCycle() {
|
|
398
|
-
if (this.cyclePhase === "processText") {
|
|
399
|
-
this.cyclePhase = "executeToolCalls";
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
this.cyclePhase = "processText";
|
|
403
|
-
this.iterationCount++;
|
|
404
|
-
}
|
|
405
|
-
async beginIteration() {
|
|
406
|
-
this.currentMessageId = this.createId("msg");
|
|
407
|
-
this.accumulatedContent = "";
|
|
408
|
-
this.accumulatedThinking = [];
|
|
409
|
-
this.currentThinkingContent = "";
|
|
410
|
-
this.currentThinkingSignature = "";
|
|
411
|
-
this.finishedEvent = null;
|
|
412
|
-
this.middlewareCtx.currentMessageId = this.currentMessageId;
|
|
413
|
-
this.middlewareCtx.accumulatedContent = "";
|
|
414
|
-
await this.middlewareRunner.runOnIteration(this.middlewareCtx, {
|
|
415
|
-
iteration: this.iterationCount,
|
|
416
|
-
messageId: this.currentMessageId
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
async *streamModelResponse() {
|
|
420
|
-
const { metadata, modelOptions } = this.params;
|
|
421
|
-
const tools = this.tools;
|
|
422
|
-
const toolsWithJsonSchemas = tools.map((tool) => ({
|
|
423
|
-
...tool,
|
|
424
|
-
inputSchema: tool.inputSchema ? convertSchemaToJsonSchema(tool.inputSchema) : void 0,
|
|
425
|
-
outputSchema: tool.outputSchema ? convertSchemaToJsonSchema(tool.outputSchema) : void 0
|
|
426
|
-
}));
|
|
427
|
-
this.middlewareCtx.phase = "modelStream";
|
|
428
|
-
const providerName = this.adapter.provider ?? this.adapter.name;
|
|
429
|
-
this.logger.request(
|
|
430
|
-
`activity=chat provider=${providerName} model=${this.params.model} messages=${this.messages.length} tools=${this.tools.length} stream=true`,
|
|
431
|
-
{
|
|
432
|
-
provider: providerName,
|
|
433
|
-
model: this.params.model,
|
|
434
|
-
messageCount: this.messages.length,
|
|
435
|
-
toolCount: this.tools.length
|
|
436
|
-
}
|
|
437
|
-
);
|
|
438
|
-
const combinedSchema = this.finalStructuredOutput?.nativeCombined === true ? this.finalStructuredOutput.jsonSchema : void 0;
|
|
439
|
-
for await (const chunk of this.adapter.chatStream({
|
|
440
|
-
model: this.params.model,
|
|
441
|
-
messages: this.messages,
|
|
442
|
-
tools: toolsWithJsonSchemas,
|
|
443
|
-
metadata,
|
|
444
|
-
request: this.effectiveRequest,
|
|
445
|
-
modelOptions,
|
|
446
|
-
systemPrompts: this.systemPrompts,
|
|
447
|
-
logger: this.logger,
|
|
448
|
-
threadId: this.threadId,
|
|
449
|
-
runId: this.runIdOverride,
|
|
450
|
-
parentRunId: this.parentRunIdOverride,
|
|
451
|
-
// Expose provided capabilities (e.g. sandbox) to harness adapters.
|
|
452
|
-
capabilities: this.middlewareCtx,
|
|
453
|
-
// Client approval decisions, for harness interactive-approval resolution.
|
|
454
|
-
approvals: this.initialApprovals,
|
|
455
|
-
...combinedSchema ? { outputSchema: combinedSchema } : {}
|
|
456
|
-
})) {
|
|
457
|
-
if (this.isCancelled()) {
|
|
458
|
-
break;
|
|
459
|
-
}
|
|
460
|
-
this.totalChunkCount++;
|
|
461
|
-
this.handleStreamChunk(chunk);
|
|
462
|
-
if (this.finalStructuredOutput?.nativeCombined === true && this.finalStructuredOutput.yieldChunks && !this.combinedStartEmitted && chunk.type === EventType.TEXT_MESSAGE_START) {
|
|
463
|
-
this.combinedStartEmitted = true;
|
|
464
|
-
const messageId = typeof chunk.messageId === "string" && chunk.messageId !== "" ? chunk.messageId : generateMessageId();
|
|
465
|
-
this.combinedStructuredMessageId = messageId;
|
|
466
|
-
const synthStart = {
|
|
467
|
-
type: EventType.CUSTOM,
|
|
468
|
-
name: "structured-output.start",
|
|
469
|
-
value: { messageId },
|
|
470
|
-
model: this.params.model,
|
|
471
|
-
timestamp: Date.now(),
|
|
472
|
-
threadId: this.threadId,
|
|
473
|
-
...this.runIdOverride ? { runId: this.runIdOverride } : {}
|
|
474
|
-
};
|
|
475
|
-
const synthOutputs = await this.middlewareRunner.runOnChunk(
|
|
476
|
-
this.middlewareCtx,
|
|
477
|
-
synthStart
|
|
478
|
-
);
|
|
479
|
-
for (const outputChunk of synthOutputs) {
|
|
480
|
-
yield outputChunk;
|
|
481
|
-
this.middlewareCtx.chunkIndex++;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
const outputChunks = await this.middlewareRunner.runOnChunk(
|
|
485
|
-
this.middlewareCtx,
|
|
486
|
-
chunk
|
|
487
|
-
);
|
|
488
|
-
const suppressAgentLifecycle = !!this.finalStructuredOutput && this.finalStructuredOutput.yieldChunks && this.finalStructuredOutput.nativeCombined !== true;
|
|
489
|
-
for (const outputChunk of outputChunks) {
|
|
490
|
-
if (suppressAgentLifecycle && (outputChunk.type === EventType.RUN_STARTED || outputChunk.type === EventType.RUN_FINISHED)) {
|
|
491
|
-
continue;
|
|
492
|
-
}
|
|
493
|
-
this.logger.output(`type=${outputChunk.type}`, { chunk: outputChunk });
|
|
494
|
-
yield outputChunk;
|
|
495
|
-
this.middlewareCtx.chunkIndex++;
|
|
496
|
-
}
|
|
497
|
-
if (chunk.type === "RUN_FINISHED" && chunk.usage) {
|
|
498
|
-
await this.middlewareRunner.runOnUsage(this.middlewareCtx, chunk.usage);
|
|
499
|
-
}
|
|
500
|
-
yield* this.drainSandboxFileQueue();
|
|
501
|
-
if (this.earlyTermination) {
|
|
502
|
-
break;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
yield* this.drainSandboxFileQueue();
|
|
506
|
-
}
|
|
507
|
-
handleStreamChunk(chunk) {
|
|
508
|
-
switch (chunk.type) {
|
|
509
|
-
// AG-UI Events
|
|
510
|
-
case "TEXT_MESSAGE_CONTENT":
|
|
511
|
-
this.handleTextMessageContentEvent(chunk);
|
|
512
|
-
break;
|
|
513
|
-
case "TOOL_CALL_START":
|
|
514
|
-
this.handleToolCallStartEvent(chunk);
|
|
515
|
-
break;
|
|
516
|
-
case "TOOL_CALL_ARGS":
|
|
517
|
-
this.handleToolCallArgsEvent(chunk);
|
|
518
|
-
break;
|
|
519
|
-
case "TOOL_CALL_END":
|
|
520
|
-
this.handleToolCallEndEvent(chunk);
|
|
521
|
-
break;
|
|
522
|
-
case "RUN_FINISHED":
|
|
523
|
-
this.handleRunFinishedEvent(chunk);
|
|
524
|
-
break;
|
|
525
|
-
case "RUN_ERROR":
|
|
526
|
-
this.handleRunErrorEvent(chunk);
|
|
527
|
-
break;
|
|
528
|
-
case "STEP_STARTED":
|
|
529
|
-
this.handleStepStartedEvent();
|
|
530
|
-
break;
|
|
531
|
-
case "STEP_FINISHED":
|
|
532
|
-
this.handleStepFinishedEvent(chunk);
|
|
533
|
-
break;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
// ===========================
|
|
537
|
-
// AG-UI Event Handlers
|
|
538
|
-
// ===========================
|
|
539
|
-
handleTextMessageContentEvent(chunk) {
|
|
540
|
-
if (chunk.content) {
|
|
541
|
-
this.accumulatedContent = chunk.content;
|
|
542
|
-
} else {
|
|
543
|
-
this.accumulatedContent += chunk.delta;
|
|
544
|
-
}
|
|
545
|
-
this.middlewareCtx.accumulatedContent = this.accumulatedContent;
|
|
546
|
-
}
|
|
547
|
-
handleToolCallStartEvent(chunk) {
|
|
548
|
-
this.toolCallManager.addToolCallStartEvent(chunk);
|
|
549
|
-
}
|
|
550
|
-
handleToolCallArgsEvent(chunk) {
|
|
551
|
-
this.toolCallManager.addToolCallArgsEvent(chunk);
|
|
552
|
-
}
|
|
553
|
-
handleToolCallEndEvent(chunk) {
|
|
554
|
-
this.toolCallManager.completeToolCall(chunk);
|
|
555
|
-
}
|
|
556
|
-
handleRunFinishedEvent(chunk) {
|
|
557
|
-
this.finishedEvent = chunk;
|
|
558
|
-
this.lastFinishReason = chunk.finishReason ?? null;
|
|
559
|
-
}
|
|
560
|
-
handleRunErrorEvent(_chunk) {
|
|
561
|
-
this.earlyTermination = true;
|
|
562
|
-
}
|
|
563
|
-
finalizeCurrentThinkingStep() {
|
|
564
|
-
if (this.currentThinkingContent) {
|
|
565
|
-
this.accumulatedThinking.push({
|
|
566
|
-
content: this.currentThinkingContent,
|
|
567
|
-
...this.currentThinkingSignature && {
|
|
568
|
-
signature: this.currentThinkingSignature
|
|
569
|
-
}
|
|
570
|
-
});
|
|
571
|
-
this.currentThinkingContent = "";
|
|
572
|
-
this.currentThinkingSignature = "";
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
handleStepStartedEvent() {
|
|
576
|
-
this.finalizeCurrentThinkingStep();
|
|
577
|
-
}
|
|
578
|
-
handleStepFinishedEvent(chunk) {
|
|
579
|
-
if (chunk.delta) {
|
|
580
|
-
this.currentThinkingContent += chunk.delta;
|
|
581
|
-
}
|
|
582
|
-
if (chunk.signature) {
|
|
583
|
-
this.currentThinkingSignature = chunk.signature;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
/**
|
|
587
|
-
* Tools available for execution this turn. The discovery tool is dropped
|
|
588
|
-
* from the advertised set (`this.tools`) once every lazy tool is discovered,
|
|
589
|
-
* but a model may still re-request discovery; this widens execution lookup
|
|
590
|
-
* to include it so such calls don't fail with "Unknown tool". Centralised so
|
|
591
|
-
* both execution sites (`processToolCalls` and `checkForPendingToolCalls`)
|
|
592
|
-
* stay in sync.
|
|
593
|
-
*/
|
|
594
|
-
resolveExecutableTools(toolCalls) {
|
|
595
|
-
return this.lazyToolManager.getExecutableTools(
|
|
596
|
-
this.tools,
|
|
597
|
-
toolCalls.map((tc) => tc.function.name)
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
async *checkForPendingToolCalls() {
|
|
601
|
-
const pendingToolCalls = this.getPendingToolCallsFromMessages();
|
|
602
|
-
if (pendingToolCalls.length === 0) {
|
|
603
|
-
return "continue";
|
|
604
|
-
}
|
|
605
|
-
const finishEvent = this.createSyntheticFinishedEvent();
|
|
606
|
-
const { toExecute: budgetedToolCalls, skippedResults } = this.applyToolCallBudget(pendingToolCalls);
|
|
607
|
-
const undiscoveredLazyResults = [];
|
|
608
|
-
const executablePendingCalls = budgetedToolCalls.filter((tc) => {
|
|
609
|
-
if (this.lazyToolManager.isUndiscoveredLazyTool(tc.function.name)) {
|
|
610
|
-
undiscoveredLazyResults.push({
|
|
611
|
-
toolCallId: tc.id,
|
|
612
|
-
toolName: tc.function.name,
|
|
613
|
-
result: {
|
|
614
|
-
error: this.lazyToolManager.getUndiscoveredToolError(
|
|
615
|
-
tc.function.name
|
|
616
|
-
)
|
|
617
|
-
},
|
|
618
|
-
state: "output-error"
|
|
619
|
-
});
|
|
620
|
-
return false;
|
|
621
|
-
}
|
|
622
|
-
return true;
|
|
623
|
-
});
|
|
624
|
-
const deferredErrorResults = [...undiscoveredLazyResults, ...skippedResults];
|
|
625
|
-
const argsMap = /* @__PURE__ */ new Map();
|
|
626
|
-
for (const tc of pendingToolCalls) {
|
|
627
|
-
argsMap.set(tc.id, tc.function.arguments);
|
|
628
|
-
}
|
|
629
|
-
if (executablePendingCalls.length === 0) {
|
|
630
|
-
if (deferredErrorResults.length > 0) {
|
|
631
|
-
for (const chunk of this.buildToolResultChunks(
|
|
632
|
-
deferredErrorResults,
|
|
633
|
-
finishEvent,
|
|
634
|
-
argsMap
|
|
635
|
-
)) {
|
|
636
|
-
yield* this.pipeThroughMiddleware(chunk);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
return "continue";
|
|
640
|
-
}
|
|
641
|
-
const { approvals, clientToolResults } = this.collectClientState();
|
|
642
|
-
const generator = executeToolCalls(
|
|
643
|
-
executablePendingCalls,
|
|
644
|
-
this.resolveExecutableTools(executablePendingCalls),
|
|
645
|
-
approvals,
|
|
646
|
-
clientToolResults,
|
|
647
|
-
(eventName, data) => this.createCustomEventChunk(eventName, data),
|
|
648
|
-
{
|
|
649
|
-
onBeforeToolCall: async (toolCall, tool, args) => {
|
|
650
|
-
this.logger.tools(`phase=before name=${toolCall.function.name}`, {
|
|
651
|
-
name: toolCall.function.name,
|
|
652
|
-
args
|
|
653
|
-
});
|
|
654
|
-
const hookCtx = {
|
|
655
|
-
toolCall,
|
|
656
|
-
tool,
|
|
657
|
-
args,
|
|
658
|
-
toolName: toolCall.function.name,
|
|
659
|
-
toolCallId: toolCall.id
|
|
660
|
-
};
|
|
661
|
-
return this.middlewareRunner.runOnBeforeToolCall(
|
|
662
|
-
this.middlewareCtx,
|
|
663
|
-
hookCtx
|
|
664
|
-
);
|
|
665
|
-
},
|
|
666
|
-
onAfterToolCall: async (info) => {
|
|
667
|
-
this.logger.tools(`phase=after name=${info.toolName}`, {
|
|
668
|
-
name: info.toolName,
|
|
669
|
-
result: info.result
|
|
670
|
-
});
|
|
671
|
-
await this.middlewareRunner.runOnAfterToolCall(
|
|
672
|
-
this.middlewareCtx,
|
|
673
|
-
info
|
|
674
|
-
);
|
|
675
|
-
}
|
|
676
|
-
},
|
|
677
|
-
this.middlewareCtx.context,
|
|
678
|
-
this.toolAbortSignal
|
|
679
|
-
);
|
|
680
|
-
const executionResult = yield* this.drainToolCallGenerator(generator);
|
|
681
|
-
if (this.isMiddlewareAborted()) {
|
|
682
|
-
this.setToolPhase("stop");
|
|
683
|
-
return "stop";
|
|
684
|
-
}
|
|
685
|
-
const allResults = [...executionResult.results, ...deferredErrorResults];
|
|
686
|
-
await this.middlewareRunner.runOnToolPhaseComplete(this.middlewareCtx, {
|
|
687
|
-
toolCalls: pendingToolCalls,
|
|
688
|
-
results: allResults,
|
|
689
|
-
needsApproval: executionResult.needsApproval,
|
|
690
|
-
needsClientExecution: executionResult.needsClientExecution
|
|
691
|
-
});
|
|
692
|
-
if (executionResult.needsApproval.length > 0 || executionResult.needsClientExecution.length > 0) {
|
|
693
|
-
if (allResults.length > 0) {
|
|
694
|
-
for (const chunk of this.buildToolResultChunks(
|
|
695
|
-
allResults,
|
|
696
|
-
finishEvent,
|
|
697
|
-
argsMap
|
|
698
|
-
)) {
|
|
699
|
-
yield* this.pipeThroughMiddleware(chunk);
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
for (const chunk of this.buildApprovalChunks(
|
|
703
|
-
executionResult.needsApproval,
|
|
704
|
-
finishEvent
|
|
705
|
-
)) {
|
|
706
|
-
yield* this.pipeThroughMiddleware(chunk);
|
|
707
|
-
}
|
|
708
|
-
for (const chunk of this.buildClientToolChunks(
|
|
709
|
-
executionResult.needsClientExecution,
|
|
710
|
-
finishEvent
|
|
711
|
-
)) {
|
|
712
|
-
yield* this.pipeThroughMiddleware(chunk);
|
|
713
|
-
}
|
|
714
|
-
this.setToolPhase("wait");
|
|
715
|
-
return "wait";
|
|
716
|
-
}
|
|
717
|
-
const toolResultChunks = this.buildToolResultChunks(
|
|
718
|
-
allResults,
|
|
719
|
-
finishEvent,
|
|
720
|
-
argsMap
|
|
721
|
-
);
|
|
722
|
-
for (const chunk of toolResultChunks) {
|
|
723
|
-
yield* this.pipeThroughMiddleware(chunk);
|
|
724
|
-
}
|
|
725
|
-
return "continue";
|
|
726
|
-
}
|
|
727
|
-
async *processToolCalls() {
|
|
728
|
-
if (!this.shouldExecuteToolPhase()) {
|
|
729
|
-
this.lastTurnToolCallCount = 0;
|
|
730
|
-
this.setToolPhase("stop");
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
const toolCalls = this.toolCallManager.getToolCalls();
|
|
734
|
-
const finishEvent = this.finishedEvent;
|
|
735
|
-
if (!finishEvent || toolCalls.length === 0) {
|
|
736
|
-
this.lastTurnToolCallCount = 0;
|
|
737
|
-
this.setToolPhase("stop");
|
|
738
|
-
return;
|
|
739
|
-
}
|
|
740
|
-
const { toExecute: budgetedToolCalls, skippedResults } = this.applyToolCallBudget(toolCalls);
|
|
741
|
-
this.addAssistantToolCallMessage(toolCalls);
|
|
742
|
-
const undiscoveredLazyResults = [];
|
|
743
|
-
const executableToolCalls = budgetedToolCalls.filter((tc) => {
|
|
744
|
-
if (this.lazyToolManager.isUndiscoveredLazyTool(tc.function.name)) {
|
|
745
|
-
undiscoveredLazyResults.push({
|
|
746
|
-
toolCallId: tc.id,
|
|
747
|
-
toolName: tc.function.name,
|
|
748
|
-
result: {
|
|
749
|
-
error: this.lazyToolManager.getUndiscoveredToolError(
|
|
750
|
-
tc.function.name
|
|
751
|
-
)
|
|
752
|
-
},
|
|
753
|
-
state: "output-error"
|
|
754
|
-
});
|
|
755
|
-
return false;
|
|
756
|
-
}
|
|
757
|
-
return true;
|
|
758
|
-
});
|
|
759
|
-
const deferredErrorResults = [...undiscoveredLazyResults, ...skippedResults];
|
|
760
|
-
if (executableToolCalls.length === 0) {
|
|
761
|
-
if (deferredErrorResults.length > 0) {
|
|
762
|
-
for (const chunk of this.buildToolResultChunks(
|
|
763
|
-
deferredErrorResults,
|
|
764
|
-
finishEvent
|
|
765
|
-
)) {
|
|
766
|
-
yield* this.pipeThroughMiddleware(chunk);
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
this.toolCallManager.clear();
|
|
770
|
-
this.setToolPhase("continue");
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
this.middlewareCtx.phase = "beforeTools";
|
|
774
|
-
const { approvals, clientToolResults } = this.collectClientState();
|
|
775
|
-
const generator = executeToolCalls(
|
|
776
|
-
executableToolCalls,
|
|
777
|
-
this.resolveExecutableTools(executableToolCalls),
|
|
778
|
-
approvals,
|
|
779
|
-
clientToolResults,
|
|
780
|
-
(eventName, data) => this.createCustomEventChunk(eventName, data),
|
|
781
|
-
{
|
|
782
|
-
onBeforeToolCall: async (toolCall, tool, args) => {
|
|
783
|
-
this.logger.tools(`phase=before name=${toolCall.function.name}`, {
|
|
784
|
-
name: toolCall.function.name,
|
|
785
|
-
args
|
|
786
|
-
});
|
|
787
|
-
const hookCtx = {
|
|
788
|
-
toolCall,
|
|
789
|
-
tool,
|
|
790
|
-
args,
|
|
791
|
-
toolName: toolCall.function.name,
|
|
792
|
-
toolCallId: toolCall.id
|
|
793
|
-
};
|
|
794
|
-
return this.middlewareRunner.runOnBeforeToolCall(
|
|
795
|
-
this.middlewareCtx,
|
|
796
|
-
hookCtx
|
|
797
|
-
);
|
|
798
|
-
},
|
|
799
|
-
onAfterToolCall: async (info) => {
|
|
800
|
-
this.logger.tools(`phase=after name=${info.toolName}`, {
|
|
801
|
-
name: info.toolName,
|
|
802
|
-
result: info.result
|
|
803
|
-
});
|
|
804
|
-
await this.middlewareRunner.runOnAfterToolCall(
|
|
805
|
-
this.middlewareCtx,
|
|
806
|
-
info
|
|
807
|
-
);
|
|
808
|
-
}
|
|
809
|
-
},
|
|
810
|
-
this.middlewareCtx.context,
|
|
811
|
-
this.toolAbortSignal
|
|
812
|
-
);
|
|
813
|
-
const executionResult = yield* this.drainToolCallGenerator(generator);
|
|
814
|
-
this.middlewareCtx.phase = "afterTools";
|
|
815
|
-
if (this.isMiddlewareAborted()) {
|
|
816
|
-
this.setToolPhase("stop");
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
819
|
-
const allResults = [...executionResult.results, ...deferredErrorResults];
|
|
820
|
-
await this.middlewareRunner.runOnToolPhaseComplete(this.middlewareCtx, {
|
|
821
|
-
toolCalls,
|
|
822
|
-
results: allResults,
|
|
823
|
-
needsApproval: executionResult.needsApproval,
|
|
824
|
-
needsClientExecution: executionResult.needsClientExecution
|
|
825
|
-
});
|
|
826
|
-
if (executionResult.needsApproval.length > 0 || executionResult.needsClientExecution.length > 0) {
|
|
827
|
-
if (allResults.length > 0) {
|
|
828
|
-
for (const chunk of this.buildToolResultChunks(
|
|
829
|
-
allResults,
|
|
830
|
-
finishEvent
|
|
831
|
-
)) {
|
|
832
|
-
yield* this.pipeThroughMiddleware(chunk);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
for (const chunk of this.buildApprovalChunks(
|
|
836
|
-
executionResult.needsApproval,
|
|
837
|
-
finishEvent
|
|
838
|
-
)) {
|
|
839
|
-
yield* this.pipeThroughMiddleware(chunk);
|
|
840
|
-
}
|
|
841
|
-
for (const chunk of this.buildClientToolChunks(
|
|
842
|
-
executionResult.needsClientExecution,
|
|
843
|
-
finishEvent
|
|
844
|
-
)) {
|
|
845
|
-
yield* this.pipeThroughMiddleware(chunk);
|
|
846
|
-
}
|
|
847
|
-
this.setToolPhase("wait");
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
const toolResultChunks = this.buildToolResultChunks(allResults, finishEvent);
|
|
851
|
-
for (const chunk of toolResultChunks) {
|
|
852
|
-
yield* this.pipeThroughMiddleware(chunk);
|
|
853
|
-
}
|
|
854
|
-
if (this.lazyToolManager.hasNewlyDiscoveredTools()) {
|
|
855
|
-
this.tools = this.lazyToolManager.getActiveTools();
|
|
856
|
-
this.toolCallManager = new ToolCallManager(this.tools);
|
|
857
|
-
this.setToolPhase("continue");
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
860
|
-
this.toolCallManager.clear();
|
|
861
|
-
this.setToolPhase("continue");
|
|
862
|
-
}
|
|
863
|
-
shouldExecuteToolPhase() {
|
|
864
|
-
return this.finishedEvent?.finishReason === "tool_calls" && this.tools.length > 0 && this.toolCallManager.hasToolCalls();
|
|
865
|
-
}
|
|
866
|
-
addAssistantToolCallMessage(toolCalls) {
|
|
867
|
-
this.finalizeCurrentThinkingStep();
|
|
868
|
-
this.messages = [
|
|
869
|
-
...this.messages,
|
|
870
|
-
{
|
|
871
|
-
role: "assistant",
|
|
872
|
-
content: this.accumulatedContent || null,
|
|
873
|
-
toolCalls,
|
|
874
|
-
...this.accumulatedThinking.length > 0 && {
|
|
875
|
-
thinking: this.accumulatedThinking
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
];
|
|
879
|
-
}
|
|
880
|
-
/**
|
|
881
|
-
* Extract client state (approvals and client tool results) from original messages.
|
|
882
|
-
* This is called in the constructor BEFORE converting to ModelMessage format,
|
|
883
|
-
* because the parts array (which contains approval state) is lost during conversion.
|
|
884
|
-
*/
|
|
885
|
-
extractClientStateFromOriginalMessages(originalMessages) {
|
|
886
|
-
const approvals = /* @__PURE__ */ new Map();
|
|
887
|
-
const clientToolResults = /* @__PURE__ */ new Map();
|
|
888
|
-
for (const message of originalMessages) {
|
|
889
|
-
if (message.role === "assistant" && message.parts) {
|
|
890
|
-
for (const part of message.parts) {
|
|
891
|
-
if (part.type === "tool-call") {
|
|
892
|
-
if (part.output !== void 0 && !part.approval) {
|
|
893
|
-
clientToolResults.set(part.id, part.output);
|
|
894
|
-
}
|
|
895
|
-
if (part.approval?.id && part.approval?.approved !== void 0 && part.state === "approval-responded") {
|
|
896
|
-
approvals.set(part.approval.id, part.approval.approved);
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
return { approvals, clientToolResults };
|
|
903
|
-
}
|
|
904
|
-
collectClientState() {
|
|
905
|
-
const approvals = new Map(this.initialApprovals);
|
|
906
|
-
const clientToolResults = new Map(this.initialClientToolResults);
|
|
907
|
-
for (const message of this.messages) {
|
|
908
|
-
if (message.role === "tool" && message.toolCallId) {
|
|
909
|
-
let output;
|
|
910
|
-
if (Array.isArray(message.content)) {
|
|
911
|
-
output = message.content;
|
|
912
|
-
} else {
|
|
913
|
-
try {
|
|
914
|
-
output = JSON.parse(message.content);
|
|
915
|
-
} catch {
|
|
916
|
-
output = message.content;
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
if (output && typeof output === "object" && output.pendingExecution === true) {
|
|
920
|
-
continue;
|
|
921
|
-
}
|
|
922
|
-
clientToolResults.set(message.toolCallId, output);
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
return { approvals, clientToolResults };
|
|
926
|
-
}
|
|
927
|
-
buildApprovalChunks(approvals, finishEvent) {
|
|
928
|
-
const chunks = [];
|
|
929
|
-
for (const approval of approvals) {
|
|
930
|
-
chunks.push({
|
|
931
|
-
type: "CUSTOM",
|
|
932
|
-
timestamp: Date.now(),
|
|
933
|
-
model: finishEvent.model,
|
|
934
|
-
name: "approval-requested",
|
|
935
|
-
value: {
|
|
936
|
-
toolCallId: approval.toolCallId,
|
|
937
|
-
toolName: approval.toolName,
|
|
938
|
-
input: approval.input,
|
|
939
|
-
approval: {
|
|
940
|
-
id: approval.approvalId,
|
|
941
|
-
needsApproval: true
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
return chunks;
|
|
947
|
-
}
|
|
948
|
-
buildClientToolChunks(clientRequests, finishEvent) {
|
|
949
|
-
const chunks = [];
|
|
950
|
-
for (const clientTool of clientRequests) {
|
|
951
|
-
chunks.push({
|
|
952
|
-
type: "CUSTOM",
|
|
953
|
-
timestamp: Date.now(),
|
|
954
|
-
model: finishEvent.model,
|
|
955
|
-
name: "tool-input-available",
|
|
956
|
-
value: {
|
|
957
|
-
toolCallId: clientTool.toolCallId,
|
|
958
|
-
toolName: clientTool.toolName,
|
|
959
|
-
input: clientTool.input
|
|
960
|
-
}
|
|
961
|
-
});
|
|
962
|
-
}
|
|
963
|
-
return chunks;
|
|
964
|
-
}
|
|
965
|
-
buildToolResultChunks(results, finishEvent, argsMap) {
|
|
966
|
-
const chunks = [];
|
|
967
|
-
for (const result of results) {
|
|
968
|
-
const content = normalizeToolResult(result.result);
|
|
969
|
-
const wireContent = typeof content === "string" ? content : JSON.stringify(content);
|
|
970
|
-
if (argsMap) {
|
|
971
|
-
chunks.push({
|
|
972
|
-
type: "TOOL_CALL_START",
|
|
973
|
-
timestamp: Date.now(),
|
|
974
|
-
model: finishEvent.model,
|
|
975
|
-
toolCallId: result.toolCallId,
|
|
976
|
-
toolCallName: result.toolName,
|
|
977
|
-
toolName: result.toolName
|
|
978
|
-
});
|
|
979
|
-
const args = argsMap.get(result.toolCallId) ?? "{}";
|
|
980
|
-
chunks.push({
|
|
981
|
-
type: "TOOL_CALL_ARGS",
|
|
982
|
-
timestamp: Date.now(),
|
|
983
|
-
model: finishEvent.model,
|
|
984
|
-
toolCallId: result.toolCallId,
|
|
985
|
-
delta: args,
|
|
986
|
-
args
|
|
987
|
-
});
|
|
988
|
-
chunks.push({
|
|
989
|
-
type: "TOOL_CALL_END",
|
|
990
|
-
timestamp: Date.now(),
|
|
991
|
-
model: finishEvent.model,
|
|
992
|
-
toolCallId: result.toolCallId,
|
|
993
|
-
toolCallName: result.toolName,
|
|
994
|
-
toolName: result.toolName,
|
|
995
|
-
result: wireContent,
|
|
996
|
-
...result.state !== void 0 && { state: result.state }
|
|
997
|
-
});
|
|
998
|
-
}
|
|
999
|
-
chunks.push({
|
|
1000
|
-
type: "TOOL_CALL_RESULT",
|
|
1001
|
-
timestamp: Date.now(),
|
|
1002
|
-
model: finishEvent.model,
|
|
1003
|
-
messageId: this.createId("tool-result"),
|
|
1004
|
-
toolCallId: result.toolCallId,
|
|
1005
|
-
content: wireContent,
|
|
1006
|
-
role: "tool",
|
|
1007
|
-
...result.state !== void 0 && { state: result.state }
|
|
1008
|
-
});
|
|
1009
|
-
const placeholderIdx = this.messages.findIndex((m) => {
|
|
1010
|
-
if (m.role !== "tool" || m.toolCallId !== result.toolCallId) {
|
|
1011
|
-
return false;
|
|
1012
|
-
}
|
|
1013
|
-
if (typeof m.content !== "string") return false;
|
|
1014
|
-
try {
|
|
1015
|
-
return JSON.parse(m.content)?.pendingExecution === true;
|
|
1016
|
-
} catch {
|
|
1017
|
-
return false;
|
|
1018
|
-
}
|
|
1019
|
-
});
|
|
1020
|
-
const newToolMessage = {
|
|
1021
|
-
role: "tool",
|
|
1022
|
-
content,
|
|
1023
|
-
toolCallId: result.toolCallId
|
|
1024
|
-
};
|
|
1025
|
-
if (placeholderIdx >= 0) {
|
|
1026
|
-
this.messages = [
|
|
1027
|
-
...this.messages.slice(0, placeholderIdx),
|
|
1028
|
-
newToolMessage,
|
|
1029
|
-
...this.messages.slice(placeholderIdx + 1)
|
|
1030
|
-
];
|
|
1031
|
-
} else {
|
|
1032
|
-
this.messages = [...this.messages, newToolMessage];
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
return chunks;
|
|
1036
|
-
}
|
|
1037
|
-
getPendingToolCallsFromMessages() {
|
|
1038
|
-
const completedToolIds = /* @__PURE__ */ new Set();
|
|
1039
|
-
for (const message of this.messages) {
|
|
1040
|
-
if (message.role === "tool" && message.toolCallId) {
|
|
1041
|
-
let hasPendingExecution = false;
|
|
1042
|
-
if (typeof message.content === "string") {
|
|
1043
|
-
try {
|
|
1044
|
-
const parsed = JSON.parse(message.content);
|
|
1045
|
-
if (parsed.pendingExecution === true) {
|
|
1046
|
-
hasPendingExecution = true;
|
|
1047
|
-
}
|
|
1048
|
-
} catch {
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
if (!hasPendingExecution) {
|
|
1052
|
-
completedToolIds.add(message.toolCallId);
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
const pending = [];
|
|
1057
|
-
for (const message of this.messages) {
|
|
1058
|
-
if (message.role === "assistant" && message.toolCalls) {
|
|
1059
|
-
for (const toolCall of message.toolCalls) {
|
|
1060
|
-
if (isProviderExecutedToolCall(toolCall)) {
|
|
1061
|
-
continue;
|
|
1062
|
-
}
|
|
1063
|
-
if (!completedToolIds.has(toolCall.id)) {
|
|
1064
|
-
pending.push(toolCall);
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
return pending;
|
|
1070
|
-
}
|
|
1071
|
-
createSyntheticFinishedEvent() {
|
|
1072
|
-
return {
|
|
1073
|
-
type: "RUN_FINISHED",
|
|
1074
|
-
runId: this.createId("pending"),
|
|
1075
|
-
threadId: this.threadId,
|
|
1076
|
-
model: this.params.model,
|
|
1077
|
-
timestamp: Date.now(),
|
|
1078
|
-
finishReason: "tool_calls"
|
|
1079
|
-
};
|
|
1080
|
-
}
|
|
1081
|
-
shouldContinue() {
|
|
1082
|
-
if (this.cyclePhase === "executeToolCalls") {
|
|
1083
|
-
return true;
|
|
1084
|
-
}
|
|
1085
|
-
return this.loopStrategy({
|
|
1086
|
-
iterationCount: this.iterationCount,
|
|
1087
|
-
messages: this.messages,
|
|
1088
|
-
finishReason: this.lastFinishReason,
|
|
1089
|
-
toolCallCount: this.toolCallCount,
|
|
1090
|
-
lastTurnToolCallCount: this.lastTurnToolCallCount
|
|
1091
|
-
}) && this.toolPhase === "continue";
|
|
1092
|
-
}
|
|
1093
|
-
/**
|
|
1094
|
-
* Record tool calls (deduped by id) and return the subset that should be
|
|
1095
|
-
* executed after applying `maxToolCallsPerTurn`. Excess calls get synthetic
|
|
1096
|
-
* error results so every tool_call still has a matching result.
|
|
1097
|
-
*
|
|
1098
|
-
* Used for both live model turns and pending/resume batches. IDs already
|
|
1099
|
-
* counted in this run (e.g. wait→resume after a live turn) are not
|
|
1100
|
-
* re-added to `toolCallCount`.
|
|
1101
|
-
*/
|
|
1102
|
-
applyToolCallBudget(toolCalls) {
|
|
1103
|
-
this.lastTurnToolCallCount = toolCalls.length;
|
|
1104
|
-
let newlyCounted = 0;
|
|
1105
|
-
for (const tc of toolCalls) {
|
|
1106
|
-
if (!this.countedToolCallIds.has(tc.id)) {
|
|
1107
|
-
this.countedToolCallIds.add(tc.id);
|
|
1108
|
-
newlyCounted++;
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
this.toolCallCount += newlyCounted;
|
|
1112
|
-
const cap = this.maxToolCallsPerTurn;
|
|
1113
|
-
if (cap == null || toolCalls.length <= cap) {
|
|
1114
|
-
return { toExecute: toolCalls, skippedResults: [] };
|
|
1115
|
-
}
|
|
1116
|
-
this.logger.agentLoop(
|
|
1117
|
-
`maxToolCallsPerTurn=${cap} skipped=${toolCalls.length - cap}`,
|
|
1118
|
-
{
|
|
1119
|
-
maxToolCallsPerTurn: cap,
|
|
1120
|
-
emitted: toolCalls.length,
|
|
1121
|
-
skipped: toolCalls.length - cap
|
|
1122
|
-
}
|
|
1123
|
-
);
|
|
1124
|
-
const toExecute = toolCalls.slice(0, cap);
|
|
1125
|
-
const skippedResults = toolCalls.slice(cap).map((tc) => ({
|
|
1126
|
-
toolCallId: tc.id,
|
|
1127
|
-
toolName: tc.function.name,
|
|
1128
|
-
result: {
|
|
1129
|
-
error: `Skipped: exceeded maxToolCallsPerTurn (${cap})`
|
|
1130
|
-
},
|
|
1131
|
-
state: "output-error"
|
|
1132
|
-
}));
|
|
1133
|
-
return { toExecute, skippedResults };
|
|
1134
|
-
}
|
|
1135
|
-
isAborted() {
|
|
1136
|
-
return !!this.effectiveSignal?.aborted;
|
|
1137
|
-
}
|
|
1138
|
-
isMiddlewareAborted() {
|
|
1139
|
-
return !!this.middlewareAbortController?.signal.aborted;
|
|
1140
|
-
}
|
|
1141
|
-
isCancelled() {
|
|
1142
|
-
return this.isAborted() || this.isMiddlewareAborted();
|
|
1143
|
-
}
|
|
1144
|
-
/**
|
|
1145
|
-
* Run the final structured-output adapter call through the middleware
|
|
1146
|
-
* pipeline. Yields chunks to the caller only when
|
|
1147
|
-
* `this.finalStructuredOutput.yieldChunks` is true; otherwise consumes
|
|
1148
|
-
* silently while still piping through middleware.
|
|
1149
|
-
*
|
|
1150
|
-
* On success, populates this.structuredOutputResult.
|
|
1151
|
-
* On failure, populates this.finalizationError.
|
|
1152
|
-
*/
|
|
1153
|
-
async *runStructuredFinalization() {
|
|
1154
|
-
if (!this.finalStructuredOutput) {
|
|
1155
|
-
throw new Error(
|
|
1156
|
-
"runStructuredFinalization called without finalStructuredOutput config"
|
|
1157
|
-
);
|
|
1158
|
-
}
|
|
1159
|
-
this.middlewareCtx.phase = "structuredOutput";
|
|
1160
|
-
const baseConfig = this.buildMiddlewareConfig();
|
|
1161
|
-
const { tools: _omitTools, ...baseWithoutTools } = baseConfig;
|
|
1162
|
-
let structuredConfig = {
|
|
1163
|
-
...baseWithoutTools,
|
|
1164
|
-
outputSchema: this.finalStructuredOutput.jsonSchema
|
|
1165
|
-
};
|
|
1166
|
-
structuredConfig = await this.middlewareRunner.runOnStructuredOutputConfig(
|
|
1167
|
-
this.middlewareCtx,
|
|
1168
|
-
structuredConfig
|
|
1169
|
-
);
|
|
1170
|
-
const { outputSchema: pinnedSchema, ...chatConfigSlice } = structuredConfig;
|
|
1171
|
-
const postOnConfig = await this.middlewareRunner.runOnConfig(
|
|
1172
|
-
this.middlewareCtx,
|
|
1173
|
-
{ ...chatConfigSlice, tools: baseConfig.tools }
|
|
1174
|
-
);
|
|
1175
|
-
this.applyMiddlewareConfig(postOnConfig);
|
|
1176
|
-
const structuredCallOptions = {
|
|
1177
|
-
chatOptions: {
|
|
1178
|
-
model: this.params.model,
|
|
1179
|
-
messages: this.messages,
|
|
1180
|
-
metadata: postOnConfig.metadata,
|
|
1181
|
-
modelOptions: postOnConfig.modelOptions,
|
|
1182
|
-
systemPrompts: postOnConfig.systemPrompts,
|
|
1183
|
-
logger: this.logger,
|
|
1184
|
-
threadId: this.threadId,
|
|
1185
|
-
runId: this.runIdOverride,
|
|
1186
|
-
parentRunId: this.parentRunIdOverride,
|
|
1187
|
-
...this.effectiveRequest ? { request: this.effectiveRequest } : {}
|
|
1188
|
-
},
|
|
1189
|
-
outputSchema: pinnedSchema
|
|
1190
|
-
};
|
|
1191
|
-
let fallbackAdapterError = void 0;
|
|
1192
|
-
const providerStream = this.adapter.structuredOutputStream ? this.adapter.structuredOutputStream(structuredCallOptions) : fallbackStructuredOutputStream(
|
|
1193
|
-
this.adapter,
|
|
1194
|
-
structuredCallOptions,
|
|
1195
|
-
(err) => {
|
|
1196
|
-
fallbackAdapterError = err;
|
|
1197
|
-
}
|
|
1198
|
-
);
|
|
1199
|
-
let startEmitted = false;
|
|
1200
|
-
let structuredMessageId = null;
|
|
1201
|
-
const extractMessageId = (c) => {
|
|
1202
|
-
if (c.type === EventType.TEXT_MESSAGE_START || c.type === EventType.TEXT_MESSAGE_CONTENT || c.type === EventType.TEXT_MESSAGE_END) {
|
|
1203
|
-
return typeof c.messageId === "string" && c.messageId !== "" ? c.messageId : null;
|
|
1204
|
-
}
|
|
1205
|
-
return null;
|
|
1206
|
-
};
|
|
1207
|
-
const buildSynthesizedStart = () => {
|
|
1208
|
-
const idForStart = structuredMessageId ?? generateMessageId();
|
|
1209
|
-
structuredMessageId = idForStart;
|
|
1210
|
-
return {
|
|
1211
|
-
type: EventType.CUSTOM,
|
|
1212
|
-
name: "structured-output.start",
|
|
1213
|
-
value: { messageId: idForStart },
|
|
1214
|
-
model: this.params.model,
|
|
1215
|
-
timestamp: Date.now(),
|
|
1216
|
-
threadId: this.threadId,
|
|
1217
|
-
...this.runIdOverride ? { runId: this.runIdOverride } : {}
|
|
1218
|
-
};
|
|
1219
|
-
};
|
|
1220
|
-
const pipeThroughMiddleware = async (synthChunk) => this.middlewareRunner.runOnChunk(this.middlewareCtx, synthChunk);
|
|
1221
|
-
let runErrorYielded = false;
|
|
1222
|
-
for await (const chunk of providerStream) {
|
|
1223
|
-
if (this.isCancelled()) {
|
|
1224
|
-
break;
|
|
1225
|
-
}
|
|
1226
|
-
if (!startEmitted && chunk.type === EventType.CUSTOM && chunk.name === "structured-output.start") {
|
|
1227
|
-
startEmitted = true;
|
|
1228
|
-
}
|
|
1229
|
-
if (!structuredMessageId) {
|
|
1230
|
-
const extracted = extractMessageId(chunk);
|
|
1231
|
-
if (extracted) structuredMessageId = extracted;
|
|
1232
|
-
}
|
|
1233
|
-
if (this.finalStructuredOutput.yieldChunks) {
|
|
1234
|
-
if (!startEmitted && (chunk.type === EventType.TEXT_MESSAGE_START || chunk.type === EventType.TEXT_MESSAGE_CONTENT || chunk.type === EventType.TEXT_MESSAGE_END)) {
|
|
1235
|
-
startEmitted = true;
|
|
1236
|
-
const synthStart = buildSynthesizedStart();
|
|
1237
|
-
const synthOutputs = await pipeThroughMiddleware(synthStart);
|
|
1238
|
-
for (const outputChunk of synthOutputs) {
|
|
1239
|
-
yield outputChunk;
|
|
1240
|
-
this.middlewareCtx.chunkIndex++;
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
if (!startEmitted && chunk.type === EventType.RUN_ERROR) {
|
|
1244
|
-
startEmitted = true;
|
|
1245
|
-
const synthStart = buildSynthesizedStart();
|
|
1246
|
-
const synthOutputs = await pipeThroughMiddleware(synthStart);
|
|
1247
|
-
for (const outputChunk of synthOutputs) {
|
|
1248
|
-
yield outputChunk;
|
|
1249
|
-
this.middlewareCtx.chunkIndex++;
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
let outboundChunk = chunk;
|
|
1254
|
-
if (chunk.type === EventType.CUSTOM && chunk.name === "structured-output.complete") {
|
|
1255
|
-
const parsed = readStructuredOutputCompleteValue(chunk.value);
|
|
1256
|
-
if (parsed) {
|
|
1257
|
-
const object = this.finalStructuredOutput.normalize ? this.finalStructuredOutput.normalize(parsed.object) : parsed.object;
|
|
1258
|
-
this.structuredOutputResult = { data: object, rawText: parsed.raw };
|
|
1259
|
-
const value = chunk.value;
|
|
1260
|
-
if (object !== parsed.object && value && typeof value === "object") {
|
|
1261
|
-
outboundChunk = { ...chunk, value: { ...value, object } };
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
if (chunk.type === EventType.RUN_FINISHED && chunk.usage) {
|
|
1266
|
-
await this.middlewareRunner.runOnUsage(this.middlewareCtx, chunk.usage);
|
|
1267
|
-
}
|
|
1268
|
-
if (chunk.type === EventType.RUN_ERROR) {
|
|
1269
|
-
this.finalizationError = {
|
|
1270
|
-
message: chunk.message,
|
|
1271
|
-
...chunk.code ? { code: chunk.code } : {},
|
|
1272
|
-
...fallbackAdapterError !== void 0 ? { cause: fallbackAdapterError } : {}
|
|
1273
|
-
};
|
|
1274
|
-
}
|
|
1275
|
-
const outputChunks = await this.middlewareRunner.runOnChunk(
|
|
1276
|
-
this.middlewareCtx,
|
|
1277
|
-
outboundChunk
|
|
1278
|
-
);
|
|
1279
|
-
if (this.finalStructuredOutput.yieldChunks) {
|
|
1280
|
-
for (const outputChunk of outputChunks) {
|
|
1281
|
-
if (outputChunk.type === EventType.RUN_ERROR) {
|
|
1282
|
-
runErrorYielded = true;
|
|
1283
|
-
}
|
|
1284
|
-
yield outputChunk;
|
|
1285
|
-
this.middlewareCtx.chunkIndex++;
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
if (this.finalizationError) {
|
|
1289
|
-
break;
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
if (this.isCancelled()) {
|
|
1293
|
-
return;
|
|
1294
|
-
}
|
|
1295
|
-
if (!this.structuredOutputResult && !this.finalizationError) {
|
|
1296
|
-
this.finalizationError = {
|
|
1297
|
-
message: "missing structured result",
|
|
1298
|
-
code: "structured-output-missing-result"
|
|
1299
|
-
};
|
|
1300
|
-
}
|
|
1301
|
-
if (this.structuredOutputResult && !this.finalizationError && this.finalStructuredOutput.validate) {
|
|
1302
|
-
try {
|
|
1303
|
-
const validated = this.finalStructuredOutput.validate(
|
|
1304
|
-
this.structuredOutputResult.data
|
|
1305
|
-
);
|
|
1306
|
-
this.validatedStructuredOutput = validated;
|
|
1307
|
-
this.hasValidatedStructuredOutput = true;
|
|
1308
|
-
} catch (err) {
|
|
1309
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1310
|
-
this.finalizationError = {
|
|
1311
|
-
message,
|
|
1312
|
-
code: "structured-output-validation-failed",
|
|
1313
|
-
cause: err
|
|
1314
|
-
};
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
if (this.finalizationError && this.finalStructuredOutput.yieldChunks && !runErrorYielded) {
|
|
1318
|
-
if (!startEmitted) {
|
|
1319
|
-
const synthStart = buildSynthesizedStart();
|
|
1320
|
-
const startOutputs = await pipeThroughMiddleware(synthStart);
|
|
1321
|
-
for (const outputChunk of startOutputs) {
|
|
1322
|
-
yield outputChunk;
|
|
1323
|
-
this.middlewareCtx.chunkIndex++;
|
|
1324
|
-
}
|
|
1325
|
-
startEmitted = true;
|
|
1326
|
-
}
|
|
1327
|
-
const errChunk = {
|
|
1328
|
-
type: EventType.RUN_ERROR,
|
|
1329
|
-
runId: this.runIdOverride ?? this.requestId,
|
|
1330
|
-
model: this.params.model,
|
|
1331
|
-
timestamp: Date.now(),
|
|
1332
|
-
threadId: this.threadId,
|
|
1333
|
-
message: this.finalizationError.message,
|
|
1334
|
-
...this.finalizationError.code ? { code: this.finalizationError.code } : {},
|
|
1335
|
-
error: {
|
|
1336
|
-
message: this.finalizationError.message,
|
|
1337
|
-
...this.finalizationError.code ? { code: this.finalizationError.code } : {}
|
|
1338
|
-
}
|
|
1339
|
-
};
|
|
1340
|
-
const outputChunks = await this.middlewareRunner.runOnChunk(
|
|
1341
|
-
this.middlewareCtx,
|
|
1342
|
-
errChunk
|
|
1343
|
-
);
|
|
1344
|
-
for (const outputChunk of outputChunks) {
|
|
1345
|
-
yield outputChunk;
|
|
1346
|
-
this.middlewareCtx.chunkIndex++;
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
/**
|
|
1351
|
-
* Native combined mode: harvest the structured output from the agent
|
|
1352
|
-
* loop's accumulated final-turn text (no separate provider call).
|
|
1353
|
-
*
|
|
1354
|
-
* The adapter wired `outputSchema` into the regular `chatStream` request,
|
|
1355
|
-
* so the model's final-turn text is the schema-constrained JSON. We parse
|
|
1356
|
-
* `this.accumulatedContent`, populate `this.structuredOutputResult`, emit
|
|
1357
|
-
* a synthetic `structured-output.complete` (and a `structured-output.start`
|
|
1358
|
-
* if one wasn't emitted earlier — only happens on the streaming path when
|
|
1359
|
-
* the model returned no text at all), and run the validate callback when
|
|
1360
|
-
* present. Failures populate `this.finalizationError` so the engine's
|
|
1361
|
-
* terminal-hook chooser routes to `onError` (per spec §7.3).
|
|
1362
|
-
*
|
|
1363
|
-
* The `'structuredOutput'` middleware phase intentionally does NOT fire on
|
|
1364
|
-
* this path — middleware sees the run through `beforeModel` / `modelStream`
|
|
1365
|
-
* as usual. See PR #605 / issue #605 for the design rationale.
|
|
1366
|
-
*/
|
|
1367
|
-
async *harvestCombinedStructuredOutput() {
|
|
1368
|
-
if (!this.finalStructuredOutput) {
|
|
1369
|
-
throw new Error(
|
|
1370
|
-
"harvestCombinedStructuredOutput called without finalStructuredOutput config"
|
|
1371
|
-
);
|
|
1372
|
-
}
|
|
1373
|
-
const yieldChunks = this.finalStructuredOutput.yieldChunks;
|
|
1374
|
-
const rawText = this.accumulatedContent;
|
|
1375
|
-
if (rawText.length === 0) {
|
|
1376
|
-
this.finalizationError = {
|
|
1377
|
-
message: "missing structured result",
|
|
1378
|
-
code: "structured-output-missing-result"
|
|
1379
|
-
};
|
|
1380
|
-
} else {
|
|
1381
|
-
try {
|
|
1382
|
-
const parsed = JSON.parse(rawText);
|
|
1383
|
-
const data = this.finalStructuredOutput.normalize ? this.finalStructuredOutput.normalize(parsed) : parsed;
|
|
1384
|
-
this.structuredOutputResult = { data, rawText };
|
|
1385
|
-
} catch (err) {
|
|
1386
|
-
const detail = rawText.slice(0, 200) + (rawText.length > 200 ? "..." : "");
|
|
1387
|
-
this.finalizationError = {
|
|
1388
|
-
message: `Failed to parse structured output as JSON. Content: ${detail}`,
|
|
1389
|
-
code: "structured-output-parse-failed",
|
|
1390
|
-
cause: err
|
|
1391
|
-
};
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
if (this.structuredOutputResult && !this.finalizationError && this.finalStructuredOutput.validate) {
|
|
1395
|
-
try {
|
|
1396
|
-
const validated = this.finalStructuredOutput.validate(
|
|
1397
|
-
this.structuredOutputResult.data
|
|
1398
|
-
);
|
|
1399
|
-
this.validatedStructuredOutput = validated;
|
|
1400
|
-
this.hasValidatedStructuredOutput = true;
|
|
1401
|
-
} catch (err) {
|
|
1402
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1403
|
-
this.finalizationError = {
|
|
1404
|
-
message,
|
|
1405
|
-
code: "structured-output-validation-failed",
|
|
1406
|
-
cause: err
|
|
1407
|
-
};
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
if (!yieldChunks) {
|
|
1411
|
-
return;
|
|
1412
|
-
}
|
|
1413
|
-
if (!this.combinedStartEmitted) {
|
|
1414
|
-
this.combinedStartEmitted = true;
|
|
1415
|
-
const messageId = this.combinedStructuredMessageId ?? generateMessageId();
|
|
1416
|
-
this.combinedStructuredMessageId = messageId;
|
|
1417
|
-
const synthStart = {
|
|
1418
|
-
type: EventType.CUSTOM,
|
|
1419
|
-
name: "structured-output.start",
|
|
1420
|
-
value: { messageId },
|
|
1421
|
-
model: this.params.model,
|
|
1422
|
-
timestamp: Date.now(),
|
|
1423
|
-
threadId: this.threadId,
|
|
1424
|
-
...this.runIdOverride ? { runId: this.runIdOverride } : {}
|
|
1425
|
-
};
|
|
1426
|
-
const startOutputs = await this.middlewareRunner.runOnChunk(
|
|
1427
|
-
this.middlewareCtx,
|
|
1428
|
-
synthStart
|
|
1429
|
-
);
|
|
1430
|
-
for (const outputChunk of startOutputs) {
|
|
1431
|
-
yield outputChunk;
|
|
1432
|
-
this.middlewareCtx.chunkIndex++;
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
if (this.structuredOutputResult && !this.finalizationError) {
|
|
1436
|
-
const completeChunk = {
|
|
1437
|
-
type: EventType.CUSTOM,
|
|
1438
|
-
name: "structured-output.complete",
|
|
1439
|
-
value: {
|
|
1440
|
-
object: this.structuredOutputResult.data,
|
|
1441
|
-
raw: this.structuredOutputResult.rawText,
|
|
1442
|
-
...this.combinedStructuredMessageId ? { messageId: this.combinedStructuredMessageId } : {}
|
|
1443
|
-
},
|
|
1444
|
-
model: this.params.model,
|
|
1445
|
-
timestamp: Date.now(),
|
|
1446
|
-
threadId: this.threadId,
|
|
1447
|
-
...this.runIdOverride ? { runId: this.runIdOverride } : {}
|
|
1448
|
-
};
|
|
1449
|
-
const completeOutputs = await this.middlewareRunner.runOnChunk(
|
|
1450
|
-
this.middlewareCtx,
|
|
1451
|
-
completeChunk
|
|
1452
|
-
);
|
|
1453
|
-
for (const outputChunk of completeOutputs) {
|
|
1454
|
-
yield outputChunk;
|
|
1455
|
-
this.middlewareCtx.chunkIndex++;
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
if (this.finalizationError) {
|
|
1459
|
-
const errChunk = {
|
|
1460
|
-
type: EventType.RUN_ERROR,
|
|
1461
|
-
runId: this.runIdOverride ?? this.requestId,
|
|
1462
|
-
model: this.params.model,
|
|
1463
|
-
timestamp: Date.now(),
|
|
1464
|
-
threadId: this.threadId,
|
|
1465
|
-
message: this.finalizationError.message,
|
|
1466
|
-
...this.finalizationError.code ? { code: this.finalizationError.code } : {},
|
|
1467
|
-
error: {
|
|
1468
|
-
message: this.finalizationError.message,
|
|
1469
|
-
...this.finalizationError.code ? { code: this.finalizationError.code } : {}
|
|
1470
|
-
}
|
|
1471
|
-
};
|
|
1472
|
-
const errOutputs = await this.middlewareRunner.runOnChunk(
|
|
1473
|
-
this.middlewareCtx,
|
|
1474
|
-
errChunk
|
|
1475
|
-
);
|
|
1476
|
-
for (const outputChunk of errOutputs) {
|
|
1477
|
-
yield outputChunk;
|
|
1478
|
-
this.middlewareCtx.chunkIndex++;
|
|
1479
|
-
}
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
buildMiddlewareConfig() {
|
|
1483
|
-
return {
|
|
1484
|
-
messages: this.messages,
|
|
1485
|
-
systemPrompts: [...this.systemPrompts],
|
|
1486
|
-
tools: [...this.tools],
|
|
1487
|
-
metadata: this.params.metadata,
|
|
1488
|
-
modelOptions: this.params.modelOptions
|
|
1489
|
-
};
|
|
1490
|
-
}
|
|
1491
|
-
applyMiddlewareConfig(config) {
|
|
1492
|
-
this.messages = config.messages;
|
|
1493
|
-
this.systemPrompts = config.systemPrompts;
|
|
1494
|
-
this.tools = config.tools;
|
|
1495
|
-
this.params = {
|
|
1496
|
-
...this.params,
|
|
1497
|
-
metadata: config.metadata,
|
|
1498
|
-
modelOptions: config.modelOptions
|
|
1499
|
-
};
|
|
1500
|
-
this.middlewareCtx.messages = this.messages;
|
|
1501
|
-
this.middlewareCtx.systemPrompts = this.systemPrompts;
|
|
1502
|
-
this.middlewareCtx.hasTools = this.tools.length > 0;
|
|
1503
|
-
this.middlewareCtx.toolNames = this.tools.map((t) => t.name);
|
|
1504
|
-
this.middlewareCtx.modelOptions = config.modelOptions;
|
|
1505
|
-
}
|
|
1506
|
-
setToolPhase(phase) {
|
|
1507
|
-
this.toolPhase = phase;
|
|
1508
|
-
}
|
|
1509
|
-
/**
|
|
1510
|
-
* Pipe a single chunk through the middleware pipeline (strip-to-spec, devtools, etc.)
|
|
1511
|
-
* and yield all resulting output chunks.
|
|
1512
|
-
*/
|
|
1513
|
-
async *pipeThroughMiddleware(chunk) {
|
|
1514
|
-
const outputChunks = await this.middlewareRunner.runOnChunk(
|
|
1515
|
-
this.middlewareCtx,
|
|
1516
|
-
chunk
|
|
1517
|
-
);
|
|
1518
|
-
for (const outputChunk of outputChunks) {
|
|
1519
|
-
yield outputChunk;
|
|
1520
|
-
this.middlewareCtx.chunkIndex++;
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
/**
|
|
1524
|
-
* Drain queued `sandbox.file` chunks (emitted via the SandboxRuntime sink)
|
|
1525
|
-
* through the middleware pipeline and into the public stream.
|
|
1526
|
-
*/
|
|
1527
|
-
async *drainSandboxFileQueue() {
|
|
1528
|
-
while (this.sandboxFileQueue.length > 0) {
|
|
1529
|
-
const chunk = this.sandboxFileQueue.shift();
|
|
1530
|
-
if (chunk) yield* this.pipeThroughMiddleware(chunk);
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
/**
|
|
1534
|
-
* Drain an executeToolCalls async generator, yielding any CustomEvent chunks
|
|
1535
|
-
* through the middleware pipeline and returning the final ExecuteToolCallsResult.
|
|
1536
|
-
*/
|
|
1537
|
-
async *drainToolCallGenerator(generator) {
|
|
1538
|
-
let next = await generator.next();
|
|
1539
|
-
while (!next.done) {
|
|
1540
|
-
yield* this.pipeThroughMiddleware(next.value);
|
|
1541
|
-
next = await generator.next();
|
|
1542
|
-
}
|
|
1543
|
-
return next.value;
|
|
1544
|
-
}
|
|
1545
|
-
createCustomEventChunk(eventName, value) {
|
|
1546
|
-
return {
|
|
1547
|
-
type: "CUSTOM",
|
|
1548
|
-
timestamp: Date.now(),
|
|
1549
|
-
model: this.params.model,
|
|
1550
|
-
name: eventName,
|
|
1551
|
-
value
|
|
1552
|
-
};
|
|
1553
|
-
}
|
|
1554
|
-
createId(prefix) {
|
|
1555
|
-
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
1556
|
-
}
|
|
102
|
+
/**
|
|
103
|
+
* Combine two optional AbortSignals into one that aborts when either does.
|
|
104
|
+
* Returns the other signal directly when one is absent or already aborted.
|
|
105
|
+
* (Manual implementation — `AbortSignal.any` requires Node >= 20.3.)
|
|
106
|
+
*/
|
|
107
|
+
function combineAbortSignals(a, b) {
|
|
108
|
+
if (!a) return b;
|
|
109
|
+
if (!b) return a;
|
|
110
|
+
if (a.aborted) return a;
|
|
111
|
+
if (b.aborted) return b;
|
|
112
|
+
const controller = new AbortController();
|
|
113
|
+
const onAbort = (source) => () => {
|
|
114
|
+
controller.abort(source.reason);
|
|
115
|
+
};
|
|
116
|
+
a.addEventListener("abort", onAbort(a), { once: true });
|
|
117
|
+
b.addEventListener("abort", onAbort(b), { once: true });
|
|
118
|
+
return controller.signal;
|
|
1557
119
|
}
|
|
120
|
+
var TextEngine = class {
|
|
121
|
+
adapter;
|
|
122
|
+
params;
|
|
123
|
+
systemPrompts;
|
|
124
|
+
tools;
|
|
125
|
+
loopStrategy;
|
|
126
|
+
toolCallManager;
|
|
127
|
+
lazyToolManager;
|
|
128
|
+
initialMessageCount;
|
|
129
|
+
requestId;
|
|
130
|
+
streamId;
|
|
131
|
+
effectiveRequest;
|
|
132
|
+
effectiveSignal;
|
|
133
|
+
messages;
|
|
134
|
+
iterationCount = 0;
|
|
135
|
+
/** Cumulative tool calls counted in this run (emitted + pending resume). */
|
|
136
|
+
toolCallCount = 0;
|
|
137
|
+
/** Tool calls in the most recent budgeted batch (0 when none). */
|
|
138
|
+
lastTurnToolCallCount = 0;
|
|
139
|
+
/** Tool call IDs already counted toward `toolCallCount` (avoids double-count on resume). */
|
|
140
|
+
countedToolCallIds = /* @__PURE__ */ new Set();
|
|
141
|
+
lastFinishReason = null;
|
|
142
|
+
streamStartTime = 0;
|
|
143
|
+
totalChunkCount = 0;
|
|
144
|
+
currentMessageId = null;
|
|
145
|
+
accumulatedContent = "";
|
|
146
|
+
accumulatedThinking = [];
|
|
147
|
+
currentThinkingContent = "";
|
|
148
|
+
currentThinkingSignature = "";
|
|
149
|
+
eventOptions;
|
|
150
|
+
eventToolNames;
|
|
151
|
+
finishedEvent = null;
|
|
152
|
+
deferredToolCallRunFinishedChunks = [];
|
|
153
|
+
earlyTermination = false;
|
|
154
|
+
toolPhase = "continue";
|
|
155
|
+
cyclePhase = "processText";
|
|
156
|
+
initialApprovals;
|
|
157
|
+
initialClientToolResults;
|
|
158
|
+
resumeApprovals = /* @__PURE__ */ new Map();
|
|
159
|
+
resumeClientToolResults = /* @__PURE__ */ new Map();
|
|
160
|
+
resumeDeniedToolResults = /* @__PURE__ */ new Map();
|
|
161
|
+
resumeCancelledToolCallIds = /* @__PURE__ */ new Set();
|
|
162
|
+
threadId;
|
|
163
|
+
runIdOverride;
|
|
164
|
+
parentRunIdOverride;
|
|
165
|
+
middlewareRunner;
|
|
166
|
+
middlewareCtx;
|
|
167
|
+
sandboxFileQueue = [];
|
|
168
|
+
deferredPromises = [];
|
|
169
|
+
abortReason;
|
|
170
|
+
middlewareAbortController;
|
|
171
|
+
toolAbortSignal;
|
|
172
|
+
terminalHookCalled = false;
|
|
173
|
+
/**
|
|
174
|
+
* Latched the first time the delivery socket closes; see `notifyDisconnected`.
|
|
175
|
+
* Also read by `subscribe` so a listener registered AFTER the disconnect (a
|
|
176
|
+
* middleware whose `setup` was still running at the time — the common case) is
|
|
177
|
+
* called immediately rather than never.
|
|
178
|
+
*/
|
|
179
|
+
disconnected = false;
|
|
180
|
+
disconnectListeners = [];
|
|
181
|
+
logger;
|
|
182
|
+
structuredOutputResult = null;
|
|
183
|
+
combinedStartEmitted = false;
|
|
184
|
+
combinedStructuredMessageId = null;
|
|
185
|
+
validatedStructuredOutput = void 0;
|
|
186
|
+
hasValidatedStructuredOutput = false;
|
|
187
|
+
finalizationError = null;
|
|
188
|
+
finalStructuredOutput;
|
|
189
|
+
constructor(config, logger) {
|
|
190
|
+
this.logger = logger;
|
|
191
|
+
this.adapter = config.adapter;
|
|
192
|
+
this.finalStructuredOutput = config.finalStructuredOutput;
|
|
193
|
+
this.params = config.params;
|
|
194
|
+
this.systemPrompts = config.params.systemPrompts || [];
|
|
195
|
+
this.loopStrategy = config.params.agentLoopStrategy || maxIterations(5);
|
|
196
|
+
this.initialMessageCount = config.params.messages.length;
|
|
197
|
+
const { approvals, clientToolResults } = this.extractClientStateFromOriginalMessages(config.params.messages);
|
|
198
|
+
this.initialApprovals = approvals;
|
|
199
|
+
this.initialClientToolResults = clientToolResults;
|
|
200
|
+
this.messages = convertMessagesToModelMessages(config.params.messages);
|
|
201
|
+
this.lazyToolManager = new LazyToolManager(config.params.tools || [], this.messages, config.params.lazyToolsConfig);
|
|
202
|
+
this.tools = this.lazyToolManager.getActiveTools();
|
|
203
|
+
this.toolCallManager = new ToolCallManager(this.tools);
|
|
204
|
+
this.requestId = this.createId("chat");
|
|
205
|
+
this.streamId = this.createId("stream");
|
|
206
|
+
this.effectiveRequest = config.params.abortController ? { signal: config.params.abortController.signal } : void 0;
|
|
207
|
+
this.effectiveSignal = config.params.abortController?.signal;
|
|
208
|
+
this.threadId = config.params.threadId || config.params.conversationId || this.createId("thread");
|
|
209
|
+
this.runIdOverride = config.params.runId;
|
|
210
|
+
this.parentRunIdOverride = config.params.parentRunId;
|
|
211
|
+
const allMiddleware = [
|
|
212
|
+
devtoolsMiddleware(),
|
|
213
|
+
...config.middleware || [],
|
|
214
|
+
stripToSpecMiddleware()
|
|
215
|
+
];
|
|
216
|
+
this.middlewareRunner = new MiddlewareRunner(allMiddleware, logger);
|
|
217
|
+
this.middlewareAbortController = new AbortController();
|
|
218
|
+
this.toolAbortSignal = combineAbortSignals(this.effectiveSignal, this.middlewareAbortController.signal);
|
|
219
|
+
this.middlewareCtx = {
|
|
220
|
+
requestId: this.requestId,
|
|
221
|
+
streamId: this.streamId,
|
|
222
|
+
runId: this.runIdOverride ?? this.requestId,
|
|
223
|
+
parentRunId: this.parentRunIdOverride,
|
|
224
|
+
threadId: this.threadId,
|
|
225
|
+
conversationId: this.threadId,
|
|
226
|
+
phase: "init",
|
|
227
|
+
iteration: 0,
|
|
228
|
+
chunkIndex: 0,
|
|
229
|
+
signal: this.effectiveSignal,
|
|
230
|
+
abort: (reason) => {
|
|
231
|
+
this.abortReason = reason;
|
|
232
|
+
this.middlewareAbortController?.abort(reason);
|
|
233
|
+
},
|
|
234
|
+
context: config.context,
|
|
235
|
+
defer: (promise) => {
|
|
236
|
+
this.deferredPromises.push(promise);
|
|
237
|
+
},
|
|
238
|
+
activity: "chat",
|
|
239
|
+
provider: config.adapter.name,
|
|
240
|
+
model: config.params.model,
|
|
241
|
+
source: "server",
|
|
242
|
+
streaming: true,
|
|
243
|
+
systemPrompts: this.systemPrompts,
|
|
244
|
+
toolNames: void 0,
|
|
245
|
+
options: void 0,
|
|
246
|
+
modelOptions: config.params.modelOptions,
|
|
247
|
+
messageCount: this.initialMessageCount,
|
|
248
|
+
hasTools: this.tools.length > 0,
|
|
249
|
+
currentMessageId: null,
|
|
250
|
+
accumulatedContent: "",
|
|
251
|
+
messages: this.messages,
|
|
252
|
+
createId: (prefix) => this.createId(prefix),
|
|
253
|
+
capabilities: new CapabilityRegistry(),
|
|
254
|
+
get: (capability) => capability[0](this.middlewareCtx),
|
|
255
|
+
getOptional: (capability) => capability[0](this.middlewareCtx, { optional: true }),
|
|
256
|
+
provide: (capability, value) => capability[1](this.middlewareCtx, value)
|
|
257
|
+
};
|
|
258
|
+
provideRunDisconnect(this.middlewareCtx, { subscribe: (listener) => {
|
|
259
|
+
this.disconnectListeners.push(listener);
|
|
260
|
+
if (this.disconnected) this.runDisconnectListener(listener);
|
|
261
|
+
} });
|
|
262
|
+
provideSandboxRuntime(this.middlewareCtx, {
|
|
263
|
+
logger: this.logger,
|
|
264
|
+
emit: (event) => {
|
|
265
|
+
this.logger.sandbox(`file ${event.type} ${event.path}`, { event: {
|
|
266
|
+
type: event.type,
|
|
267
|
+
path: event.path,
|
|
268
|
+
timestamp: event.timestamp
|
|
269
|
+
} });
|
|
270
|
+
this.middlewareRunner.runSandboxFile(this.middlewareCtx, event).catch((err) => {
|
|
271
|
+
this.logger.errors("sandbox file hook failed", { error: err });
|
|
272
|
+
});
|
|
273
|
+
this.sandboxFileQueue.push(this.createCustomEventChunk("sandbox.file", {
|
|
274
|
+
type: event.type,
|
|
275
|
+
path: event.path,
|
|
276
|
+
timestamp: event.timestamp
|
|
277
|
+
}));
|
|
278
|
+
},
|
|
279
|
+
emitFileDiff: (value) => {
|
|
280
|
+
this.sandboxFileQueue.push(this.createCustomEventChunk("sandbox.file.diff", value));
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
/** Get the accumulated content after the chat loop completes */
|
|
285
|
+
getAccumulatedContent() {
|
|
286
|
+
return this.accumulatedContent;
|
|
287
|
+
}
|
|
288
|
+
/** Get the final messages array after the chat loop completes */
|
|
289
|
+
getMessages() {
|
|
290
|
+
return this.messages;
|
|
291
|
+
}
|
|
292
|
+
/** Returns the structured-output result if finalization ran successfully. */
|
|
293
|
+
getStructuredOutputResult() {
|
|
294
|
+
return this.structuredOutputResult;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Returns the validated structured-output value (the result of running
|
|
298
|
+
* `finalStructuredOutput.validate` against the raw structured-output data)
|
|
299
|
+
* wrapped in a `{ value }` object so callers can distinguish "no validation
|
|
300
|
+
* happened" from "validation produced undefined". Returns `null` when no
|
|
301
|
+
* validator was configured or validation hasn't been performed yet.
|
|
302
|
+
*/
|
|
303
|
+
getValidatedStructuredOutput() {
|
|
304
|
+
return this.hasValidatedStructuredOutput ? { value: this.validatedStructuredOutput } : null;
|
|
305
|
+
}
|
|
306
|
+
/** Returns the recorded finalization error, if any. */
|
|
307
|
+
getFinalizationError() {
|
|
308
|
+
return this.finalizationError;
|
|
309
|
+
}
|
|
310
|
+
async *run() {
|
|
311
|
+
this.beforeRun();
|
|
312
|
+
this.logger.agentLoop("run started", { threadId: this.middlewareCtx.threadId });
|
|
313
|
+
try {
|
|
314
|
+
await this.middlewareRunner.runSetup(this.middlewareCtx);
|
|
315
|
+
this.middlewareCtx.phase = "init";
|
|
316
|
+
const initialConfig = this.buildMiddlewareConfig();
|
|
317
|
+
const transformedConfig = await this.middlewareRunner.runOnConfig(this.middlewareCtx, initialConfig);
|
|
318
|
+
this.applyMiddlewareConfig(transformedConfig);
|
|
319
|
+
await this.applyEphemeralInterruptResume(transformedConfig);
|
|
320
|
+
await this.middlewareRunner.runOnStart(this.middlewareCtx);
|
|
321
|
+
if ((yield* this.checkForPendingToolCalls()) === "wait") return;
|
|
322
|
+
if (!(!!this.finalStructuredOutput && this.tools.length === 0 && this.finalStructuredOutput.nativeCombined !== true)) do {
|
|
323
|
+
if (this.earlyTermination || this.isCancelled()) return;
|
|
324
|
+
this.logger.agentLoop(`iteration=${this.middlewareCtx.iteration}`, { iteration: this.middlewareCtx.iteration });
|
|
325
|
+
await this.beginCycle();
|
|
326
|
+
if (this.cyclePhase === "processText") {
|
|
327
|
+
this.middlewareCtx.phase = "beforeModel";
|
|
328
|
+
this.middlewareCtx.iteration = this.iterationCount;
|
|
329
|
+
const iterConfig = this.buildMiddlewareConfig();
|
|
330
|
+
const iterTransformedConfig = await this.middlewareRunner.runOnConfig(this.middlewareCtx, iterConfig);
|
|
331
|
+
this.applyMiddlewareConfig(iterTransformedConfig);
|
|
332
|
+
yield* this.streamModelResponse();
|
|
333
|
+
} else yield* this.processToolCalls();
|
|
334
|
+
this.endCycle();
|
|
335
|
+
} while (await this.shouldContinue());
|
|
336
|
+
this.logger.agentLoop("run finished", { finishReason: this.lastFinishReason });
|
|
337
|
+
if (this.finalStructuredOutput && this.toolPhase !== "wait" && !this.isCancelled() && !this.finalizationError) if (this.finalStructuredOutput.nativeCombined === true) yield* this.harvestCombinedStructuredOutput();
|
|
338
|
+
else yield* this.runStructuredFinalization();
|
|
339
|
+
if (!this.terminalHookCalled && this.toolPhase !== "wait" && !this.isCancelled()) if (this.finalizationError) {
|
|
340
|
+
this.terminalHookCalled = true;
|
|
341
|
+
const errForHook = new Error(this.finalizationError.message, this.finalizationError.cause !== void 0 ? { cause: this.finalizationError.cause } : void 0);
|
|
342
|
+
if (this.finalizationError.code !== void 0) Object.defineProperty(errForHook, "code", {
|
|
343
|
+
value: this.finalizationError.code,
|
|
344
|
+
enumerable: true
|
|
345
|
+
});
|
|
346
|
+
await this.middlewareRunner.runOnError(this.middlewareCtx, {
|
|
347
|
+
error: errForHook,
|
|
348
|
+
duration: Date.now() - this.streamStartTime
|
|
349
|
+
});
|
|
350
|
+
} else {
|
|
351
|
+
this.terminalHookCalled = true;
|
|
352
|
+
await this.middlewareRunner.runOnFinish(this.middlewareCtx, {
|
|
353
|
+
finishReason: this.lastFinishReason,
|
|
354
|
+
duration: Date.now() - this.streamStartTime,
|
|
355
|
+
content: this.accumulatedContent,
|
|
356
|
+
usage: this.finishedEvent?.usage
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
} catch (error) {
|
|
360
|
+
if (error instanceof Error && error.name === "InterruptReplaySignal" && "continuationRunId" in error && typeof error.continuationRunId === "string") {
|
|
361
|
+
this.terminalHookCalled = true;
|
|
362
|
+
yield {
|
|
363
|
+
type: EventType.RUN_FINISHED,
|
|
364
|
+
timestamp: Date.now(),
|
|
365
|
+
threadId: this.threadId,
|
|
366
|
+
runId: this.runIdOverride ?? this.requestId,
|
|
367
|
+
finishReason: "stop",
|
|
368
|
+
outcome: { type: "success" },
|
|
369
|
+
result: {
|
|
370
|
+
replayed: true,
|
|
371
|
+
continuationRunId: error.continuationRunId
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const interruptFailure = structuralInterruptFailure(error);
|
|
377
|
+
if (interruptFailure) {
|
|
378
|
+
this.terminalHookCalled = true;
|
|
379
|
+
this.logger.errors("chat interrupt resume failed", {
|
|
380
|
+
error,
|
|
381
|
+
threadId: this.middlewareCtx.threadId
|
|
382
|
+
});
|
|
383
|
+
await this.middlewareRunner.runOnError(this.middlewareCtx, {
|
|
384
|
+
error: interruptFailure.error,
|
|
385
|
+
duration: Date.now() - this.streamStartTime
|
|
386
|
+
});
|
|
387
|
+
yield this.buildInterruptRunErrorChunk(error);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (!this.terminalHookCalled) {
|
|
391
|
+
this.terminalHookCalled = true;
|
|
392
|
+
if (error instanceof MiddlewareAbortError) {
|
|
393
|
+
this.abortReason = error.message;
|
|
394
|
+
await this.middlewareRunner.runOnAbort(this.middlewareCtx, {
|
|
395
|
+
reason: error.message,
|
|
396
|
+
duration: Date.now() - this.streamStartTime,
|
|
397
|
+
cancelRequested: isCancelRequestedReason(error.message)
|
|
398
|
+
});
|
|
399
|
+
} else {
|
|
400
|
+
this.logger.errors("chat run failed", {
|
|
401
|
+
error,
|
|
402
|
+
threadId: this.middlewareCtx.threadId
|
|
403
|
+
});
|
|
404
|
+
await this.middlewareRunner.runOnError(this.middlewareCtx, {
|
|
405
|
+
error,
|
|
406
|
+
duration: Date.now() - this.streamStartTime
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (!(error instanceof MiddlewareAbortError)) throw error;
|
|
411
|
+
} finally {
|
|
412
|
+
if (!this.terminalHookCalled && this.isCancelled()) {
|
|
413
|
+
this.terminalHookCalled = true;
|
|
414
|
+
const reason = this.resolveAbortReason();
|
|
415
|
+
await this.middlewareRunner.runOnAbort(this.middlewareCtx, {
|
|
416
|
+
reason,
|
|
417
|
+
duration: Date.now() - this.streamStartTime,
|
|
418
|
+
cancelRequested: isCancelRequestedReason(reason)
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
if (this.deferredPromises.length > 0) await Promise.allSettled(this.deferredPromises);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
beforeRun() {
|
|
425
|
+
this.streamStartTime = Date.now();
|
|
426
|
+
const { tools, metadata } = this.params;
|
|
427
|
+
const options = {};
|
|
428
|
+
if (metadata !== void 0) options.metadata = metadata;
|
|
429
|
+
this.eventOptions = Object.keys(options).length > 0 ? options : void 0;
|
|
430
|
+
this.eventToolNames = tools?.map((t) => t.name);
|
|
431
|
+
this.middlewareCtx.options = this.eventOptions;
|
|
432
|
+
this.middlewareCtx.toolNames = this.eventToolNames;
|
|
433
|
+
}
|
|
434
|
+
async beginCycle() {
|
|
435
|
+
if (this.cyclePhase === "processText") await this.beginIteration();
|
|
436
|
+
}
|
|
437
|
+
endCycle() {
|
|
438
|
+
if (this.cyclePhase === "processText") {
|
|
439
|
+
this.cyclePhase = "executeToolCalls";
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
this.cyclePhase = "processText";
|
|
443
|
+
this.iterationCount++;
|
|
444
|
+
}
|
|
445
|
+
async beginIteration() {
|
|
446
|
+
this.currentMessageId = this.createId("msg");
|
|
447
|
+
this.accumulatedContent = "";
|
|
448
|
+
this.accumulatedThinking = [];
|
|
449
|
+
this.currentThinkingContent = "";
|
|
450
|
+
this.currentThinkingSignature = "";
|
|
451
|
+
this.finishedEvent = null;
|
|
452
|
+
this.middlewareCtx.currentMessageId = this.currentMessageId;
|
|
453
|
+
this.middlewareCtx.accumulatedContent = "";
|
|
454
|
+
await this.middlewareRunner.runOnIteration(this.middlewareCtx, {
|
|
455
|
+
iteration: this.iterationCount,
|
|
456
|
+
messageId: this.currentMessageId
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
async *streamModelResponse() {
|
|
460
|
+
const { metadata, modelOptions } = this.params;
|
|
461
|
+
const toolsWithJsonSchemas = this.tools.map((tool) => ({
|
|
462
|
+
...tool,
|
|
463
|
+
inputSchema: tool.inputSchema ? convertSchemaToJsonSchema(tool.inputSchema) : void 0,
|
|
464
|
+
outputSchema: tool.outputSchema ? convertSchemaToJsonSchema(tool.outputSchema) : void 0
|
|
465
|
+
}));
|
|
466
|
+
this.middlewareCtx.phase = "modelStream";
|
|
467
|
+
const providerName = this.adapter.provider ?? this.adapter.name;
|
|
468
|
+
this.logger.request(`activity=chat provider=${providerName} model=${this.params.model} messages=${this.messages.length} tools=${this.tools.length} stream=true`, {
|
|
469
|
+
provider: providerName,
|
|
470
|
+
model: this.params.model,
|
|
471
|
+
messageCount: this.messages.length,
|
|
472
|
+
toolCount: this.tools.length
|
|
473
|
+
});
|
|
474
|
+
const combinedSchema = this.finalStructuredOutput?.nativeCombined === true ? this.finalStructuredOutput.jsonSchema : void 0;
|
|
475
|
+
const { approvals } = this.collectClientState();
|
|
476
|
+
const adapterApprovals = /* @__PURE__ */ new Map();
|
|
477
|
+
for (const [approvalId, resolution] of approvals) adapterApprovals.set(approvalId, typeof resolution === "boolean" ? resolution : resolution.approved);
|
|
478
|
+
for await (const chunk of this.adapter.chatStream({
|
|
479
|
+
model: this.params.model,
|
|
480
|
+
messages: this.messages,
|
|
481
|
+
tools: toolsWithJsonSchemas,
|
|
482
|
+
metadata,
|
|
483
|
+
request: this.effectiveRequest,
|
|
484
|
+
modelOptions,
|
|
485
|
+
systemPrompts: this.systemPrompts,
|
|
486
|
+
logger: this.logger,
|
|
487
|
+
threadId: this.threadId,
|
|
488
|
+
runId: this.runIdOverride,
|
|
489
|
+
parentRunId: this.parentRunIdOverride,
|
|
490
|
+
capabilities: this.middlewareCtx,
|
|
491
|
+
approvals: adapterApprovals,
|
|
492
|
+
...combinedSchema ? { outputSchema: combinedSchema } : {}
|
|
493
|
+
})) {
|
|
494
|
+
if (this.isCancelled()) break;
|
|
495
|
+
this.totalChunkCount++;
|
|
496
|
+
this.handleStreamChunk(chunk);
|
|
497
|
+
if (this.finalStructuredOutput?.nativeCombined === true && this.finalStructuredOutput.yieldChunks && !this.combinedStartEmitted && chunk.type === EventType.TEXT_MESSAGE_START) {
|
|
498
|
+
this.combinedStartEmitted = true;
|
|
499
|
+
const messageId = typeof chunk.messageId === "string" && chunk.messageId !== "" ? chunk.messageId : generateMessageId();
|
|
500
|
+
this.combinedStructuredMessageId = messageId;
|
|
501
|
+
const synthStart = {
|
|
502
|
+
type: EventType.CUSTOM,
|
|
503
|
+
name: "structured-output.start",
|
|
504
|
+
value: { messageId },
|
|
505
|
+
model: this.params.model,
|
|
506
|
+
timestamp: Date.now(),
|
|
507
|
+
threadId: this.threadId,
|
|
508
|
+
...this.runIdOverride ? { runId: this.runIdOverride } : {}
|
|
509
|
+
};
|
|
510
|
+
const synthOutputs = await this.middlewareRunner.runOnChunk(this.middlewareCtx, synthStart);
|
|
511
|
+
for (const outputChunk of synthOutputs) {
|
|
512
|
+
yield outputChunk;
|
|
513
|
+
this.middlewareCtx.chunkIndex++;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const outputChunks = await this.middlewareRunner.runOnChunk(this.middlewareCtx, chunk);
|
|
517
|
+
const suppressAgentLifecycle = !!this.finalStructuredOutput && this.finalStructuredOutput.yieldChunks && this.finalStructuredOutput.nativeCombined !== true;
|
|
518
|
+
for (const outputChunk of outputChunks) {
|
|
519
|
+
if (suppressAgentLifecycle && (outputChunk.type === EventType.RUN_STARTED || outputChunk.type === EventType.RUN_FINISHED)) continue;
|
|
520
|
+
if (this.shouldDeferToolCallRunFinished(outputChunk)) {
|
|
521
|
+
this.deferredToolCallRunFinishedChunks.push(outputChunk);
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
this.logger.output(`type=${outputChunk.type}`, { chunk: outputChunk });
|
|
525
|
+
yield outputChunk;
|
|
526
|
+
this.middlewareCtx.chunkIndex++;
|
|
527
|
+
}
|
|
528
|
+
if (chunk.type === "RUN_FINISHED" && chunk.usage) await this.middlewareRunner.runOnUsage(this.middlewareCtx, chunk.usage);
|
|
529
|
+
yield* this.drainSandboxFileQueue();
|
|
530
|
+
if (this.earlyTermination) break;
|
|
531
|
+
}
|
|
532
|
+
yield* this.drainSandboxFileQueue();
|
|
533
|
+
}
|
|
534
|
+
handleStreamChunk(chunk) {
|
|
535
|
+
switch (chunk.type) {
|
|
536
|
+
case "TEXT_MESSAGE_CONTENT":
|
|
537
|
+
this.handleTextMessageContentEvent(chunk);
|
|
538
|
+
break;
|
|
539
|
+
case "TOOL_CALL_START":
|
|
540
|
+
this.handleToolCallStartEvent(chunk);
|
|
541
|
+
break;
|
|
542
|
+
case "TOOL_CALL_ARGS":
|
|
543
|
+
this.handleToolCallArgsEvent(chunk);
|
|
544
|
+
break;
|
|
545
|
+
case "TOOL_CALL_END":
|
|
546
|
+
this.handleToolCallEndEvent(chunk);
|
|
547
|
+
break;
|
|
548
|
+
case "RUN_FINISHED":
|
|
549
|
+
this.handleRunFinishedEvent(chunk);
|
|
550
|
+
break;
|
|
551
|
+
case "RUN_ERROR":
|
|
552
|
+
this.handleRunErrorEvent(chunk);
|
|
553
|
+
break;
|
|
554
|
+
case "STEP_STARTED":
|
|
555
|
+
this.handleStepStartedEvent();
|
|
556
|
+
break;
|
|
557
|
+
case "STEP_FINISHED":
|
|
558
|
+
this.handleStepFinishedEvent(chunk);
|
|
559
|
+
break;
|
|
560
|
+
case "TOOL_CALL_RESULT": break;
|
|
561
|
+
case "REASONING_START":
|
|
562
|
+
case "REASONING_MESSAGE_START":
|
|
563
|
+
case "REASONING_MESSAGE_CONTENT":
|
|
564
|
+
case "REASONING_MESSAGE_END":
|
|
565
|
+
case "REASONING_END": break;
|
|
566
|
+
default: break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
handleTextMessageContentEvent(chunk) {
|
|
570
|
+
if (chunk.content) this.accumulatedContent = chunk.content;
|
|
571
|
+
else this.accumulatedContent += chunk.delta;
|
|
572
|
+
this.middlewareCtx.accumulatedContent = this.accumulatedContent;
|
|
573
|
+
}
|
|
574
|
+
handleToolCallStartEvent(chunk) {
|
|
575
|
+
this.toolCallManager.addToolCallStartEvent(chunk);
|
|
576
|
+
}
|
|
577
|
+
handleToolCallArgsEvent(chunk) {
|
|
578
|
+
this.toolCallManager.addToolCallArgsEvent(chunk);
|
|
579
|
+
}
|
|
580
|
+
handleToolCallEndEvent(chunk) {
|
|
581
|
+
this.toolCallManager.completeToolCall(chunk);
|
|
582
|
+
}
|
|
583
|
+
handleRunFinishedEvent(chunk) {
|
|
584
|
+
this.finishedEvent = chunk;
|
|
585
|
+
this.lastFinishReason = chunk.finishReason ?? null;
|
|
586
|
+
}
|
|
587
|
+
handleRunErrorEvent(_chunk) {
|
|
588
|
+
this.earlyTermination = true;
|
|
589
|
+
}
|
|
590
|
+
finalizeCurrentThinkingStep() {
|
|
591
|
+
if (this.currentThinkingContent) {
|
|
592
|
+
this.accumulatedThinking.push({
|
|
593
|
+
content: this.currentThinkingContent,
|
|
594
|
+
...this.currentThinkingSignature && { signature: this.currentThinkingSignature }
|
|
595
|
+
});
|
|
596
|
+
this.currentThinkingContent = "";
|
|
597
|
+
this.currentThinkingSignature = "";
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
handleStepStartedEvent() {
|
|
601
|
+
this.finalizeCurrentThinkingStep();
|
|
602
|
+
}
|
|
603
|
+
handleStepFinishedEvent(chunk) {
|
|
604
|
+
if (chunk.delta) this.currentThinkingContent += chunk.delta;
|
|
605
|
+
if (chunk.signature) this.currentThinkingSignature = chunk.signature;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Tools available for execution this turn. The discovery tool is dropped
|
|
609
|
+
* from the advertised set (`this.tools`) once every lazy tool is discovered,
|
|
610
|
+
* but a model may still re-request discovery; this widens execution lookup
|
|
611
|
+
* to include it so such calls don't fail with "Unknown tool". Centralised so
|
|
612
|
+
* both execution sites (`processToolCalls` and `checkForPendingToolCalls`)
|
|
613
|
+
* stay in sync.
|
|
614
|
+
*/
|
|
615
|
+
resolveExecutableTools(toolCalls) {
|
|
616
|
+
return this.lazyToolManager.getExecutableTools(this.tools, toolCalls.map((tc) => tc.function.name));
|
|
617
|
+
}
|
|
618
|
+
async *checkForPendingToolCalls() {
|
|
619
|
+
const pendingToolCalls = this.getPendingToolCallsFromMessages();
|
|
620
|
+
if (pendingToolCalls.length === 0) return "continue";
|
|
621
|
+
const finishEvent = this.createSyntheticFinishedEvent();
|
|
622
|
+
this.recordToolCalls(pendingToolCalls);
|
|
623
|
+
const undiscoveredLazyResults = [];
|
|
624
|
+
const executablePendingCalls = pendingToolCalls.filter((tc) => {
|
|
625
|
+
if (this.lazyToolManager.isUndiscoveredLazyTool(tc.function.name)) {
|
|
626
|
+
undiscoveredLazyResults.push({
|
|
627
|
+
toolCallId: tc.id,
|
|
628
|
+
toolName: tc.function.name,
|
|
629
|
+
result: { error: this.lazyToolManager.getUndiscoveredToolError(tc.function.name) },
|
|
630
|
+
state: "output-error"
|
|
631
|
+
});
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
return true;
|
|
635
|
+
});
|
|
636
|
+
const deferredErrorResults = [...undiscoveredLazyResults];
|
|
637
|
+
const argsMap = /* @__PURE__ */ new Map();
|
|
638
|
+
for (const tc of pendingToolCalls) argsMap.set(tc.id, tc.function.arguments);
|
|
639
|
+
if (executablePendingCalls.length === 0) {
|
|
640
|
+
if (deferredErrorResults.length > 0) for (const chunk of this.buildToolResultChunks(deferredErrorResults, finishEvent, argsMap)) yield* this.pipeThroughMiddleware(chunk);
|
|
641
|
+
return "continue";
|
|
642
|
+
}
|
|
643
|
+
const { approvals, clientToolResults } = this.collectClientState();
|
|
644
|
+
const generator = executeToolCalls(executablePendingCalls, this.resolveExecutableTools(executablePendingCalls), approvals, clientToolResults, (eventName, data) => this.createCustomEventChunk(eventName, data), {
|
|
645
|
+
onBeforeToolCall: async (toolCall, tool, args) => {
|
|
646
|
+
this.logger.tools(`phase=before name=${toolCall.function.name}`, {
|
|
647
|
+
name: toolCall.function.name,
|
|
648
|
+
args
|
|
649
|
+
});
|
|
650
|
+
const hookCtx = {
|
|
651
|
+
toolCall,
|
|
652
|
+
tool,
|
|
653
|
+
args,
|
|
654
|
+
toolName: toolCall.function.name,
|
|
655
|
+
toolCallId: toolCall.id
|
|
656
|
+
};
|
|
657
|
+
return this.middlewareRunner.runOnBeforeToolCall(this.middlewareCtx, hookCtx);
|
|
658
|
+
},
|
|
659
|
+
onAfterToolCall: async (info) => {
|
|
660
|
+
this.logger.tools(`phase=after name=${info.toolName}`, {
|
|
661
|
+
name: info.toolName,
|
|
662
|
+
result: info.result
|
|
663
|
+
});
|
|
664
|
+
await this.middlewareRunner.runOnAfterToolCall(this.middlewareCtx, info);
|
|
665
|
+
}
|
|
666
|
+
}, this.middlewareCtx.context, this.toolAbortSignal, {
|
|
667
|
+
deniedToolResults: this.resumeDeniedToolResults,
|
|
668
|
+
cancelledToolCallIds: this.resumeCancelledToolCallIds
|
|
669
|
+
});
|
|
670
|
+
const executionResult = yield* this.drainToolCallGenerator(generator);
|
|
671
|
+
if (this.isMiddlewareAborted()) {
|
|
672
|
+
this.setToolPhase("stop");
|
|
673
|
+
return "stop";
|
|
674
|
+
}
|
|
675
|
+
const allResults = [...executionResult.results, ...deferredErrorResults];
|
|
676
|
+
await this.middlewareRunner.runOnToolPhaseComplete(this.middlewareCtx, {
|
|
677
|
+
toolCalls: pendingToolCalls,
|
|
678
|
+
results: allResults,
|
|
679
|
+
needsApproval: executionResult.needsApproval,
|
|
680
|
+
needsClientExecution: executionResult.needsClientExecution
|
|
681
|
+
});
|
|
682
|
+
if (executionResult.needsApproval.length > 0 || executionResult.needsClientExecution.length > 0) {
|
|
683
|
+
this.discardDeferredToolCallRunFinishedChunks();
|
|
684
|
+
if (allResults.length > 0) for (const chunk of this.buildToolResultChunks(allResults, finishEvent)) yield* this.pipeThroughMiddleware(chunk);
|
|
685
|
+
const emitted = yield* this.emitActionableInterruptBoundary(finishEvent, executionResult.needsApproval, executionResult.needsClientExecution);
|
|
686
|
+
this.setToolPhase(emitted ? "wait" : "stop");
|
|
687
|
+
return emitted ? "wait" : "stop";
|
|
688
|
+
}
|
|
689
|
+
const toolResultChunks = this.buildToolResultChunks(allResults, finishEvent);
|
|
690
|
+
for (const chunk of toolResultChunks) yield* this.pipeThroughMiddleware(chunk);
|
|
691
|
+
return "continue";
|
|
692
|
+
}
|
|
693
|
+
async *processToolCalls() {
|
|
694
|
+
if (!this.shouldExecuteToolPhase()) {
|
|
695
|
+
this.lastTurnToolCallCount = 0;
|
|
696
|
+
this.setToolPhase("stop");
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const toolCalls = this.toolCallManager.getToolCalls();
|
|
700
|
+
const finishEvent = this.finishedEvent;
|
|
701
|
+
if (!finishEvent || toolCalls.length === 0) {
|
|
702
|
+
this.lastTurnToolCallCount = 0;
|
|
703
|
+
this.setToolPhase("stop");
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
this.recordToolCalls(toolCalls);
|
|
707
|
+
this.addAssistantToolCallMessage(toolCalls);
|
|
708
|
+
const undiscoveredLazyResults = [];
|
|
709
|
+
const executableToolCalls = toolCalls.filter((tc) => {
|
|
710
|
+
if (this.lazyToolManager.isUndiscoveredLazyTool(tc.function.name)) {
|
|
711
|
+
undiscoveredLazyResults.push({
|
|
712
|
+
toolCallId: tc.id,
|
|
713
|
+
toolName: tc.function.name,
|
|
714
|
+
result: { error: this.lazyToolManager.getUndiscoveredToolError(tc.function.name) },
|
|
715
|
+
state: "output-error"
|
|
716
|
+
});
|
|
717
|
+
return false;
|
|
718
|
+
}
|
|
719
|
+
return true;
|
|
720
|
+
});
|
|
721
|
+
const deferredErrorResults = [...undiscoveredLazyResults];
|
|
722
|
+
if (executableToolCalls.length === 0) {
|
|
723
|
+
yield* this.flushDeferredToolCallRunFinishedChunks();
|
|
724
|
+
if (deferredErrorResults.length > 0) for (const chunk of this.buildToolResultChunks(deferredErrorResults, finishEvent)) yield* this.pipeThroughMiddleware(chunk);
|
|
725
|
+
this.toolCallManager.clear();
|
|
726
|
+
this.setToolPhase("continue");
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
this.middlewareCtx.phase = "beforeTools";
|
|
730
|
+
const { approvals, clientToolResults } = this.collectClientState();
|
|
731
|
+
const generator = executeToolCalls(executableToolCalls, this.resolveExecutableTools(executableToolCalls), approvals, clientToolResults, (eventName, data) => this.createCustomEventChunk(eventName, data), {
|
|
732
|
+
onBeforeToolCall: async (toolCall, tool, args) => {
|
|
733
|
+
this.logger.tools(`phase=before name=${toolCall.function.name}`, {
|
|
734
|
+
name: toolCall.function.name,
|
|
735
|
+
args
|
|
736
|
+
});
|
|
737
|
+
const hookCtx = {
|
|
738
|
+
toolCall,
|
|
739
|
+
tool,
|
|
740
|
+
args,
|
|
741
|
+
toolName: toolCall.function.name,
|
|
742
|
+
toolCallId: toolCall.id
|
|
743
|
+
};
|
|
744
|
+
return this.middlewareRunner.runOnBeforeToolCall(this.middlewareCtx, hookCtx);
|
|
745
|
+
},
|
|
746
|
+
onAfterToolCall: async (info) => {
|
|
747
|
+
this.logger.tools(`phase=after name=${info.toolName}`, {
|
|
748
|
+
name: info.toolName,
|
|
749
|
+
result: info.result
|
|
750
|
+
});
|
|
751
|
+
await this.middlewareRunner.runOnAfterToolCall(this.middlewareCtx, info);
|
|
752
|
+
}
|
|
753
|
+
}, this.middlewareCtx.context, this.toolAbortSignal, {
|
|
754
|
+
deniedToolResults: this.resumeDeniedToolResults,
|
|
755
|
+
cancelledToolCallIds: this.resumeCancelledToolCallIds
|
|
756
|
+
});
|
|
757
|
+
const executionResult = yield* this.drainToolCallGenerator(generator);
|
|
758
|
+
this.middlewareCtx.phase = "afterTools";
|
|
759
|
+
if (this.isMiddlewareAborted()) {
|
|
760
|
+
this.setToolPhase("stop");
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
const allResults = [...executionResult.results, ...deferredErrorResults];
|
|
764
|
+
await this.middlewareRunner.runOnToolPhaseComplete(this.middlewareCtx, {
|
|
765
|
+
toolCalls,
|
|
766
|
+
results: allResults,
|
|
767
|
+
needsApproval: executionResult.needsApproval,
|
|
768
|
+
needsClientExecution: executionResult.needsClientExecution
|
|
769
|
+
});
|
|
770
|
+
if (executionResult.needsApproval.length > 0 || executionResult.needsClientExecution.length > 0) {
|
|
771
|
+
if (allResults.length > 0) for (const chunk of this.buildToolResultChunks(allResults, finishEvent)) yield* this.pipeThroughMiddleware(chunk);
|
|
772
|
+
const emitted = yield* this.emitActionableInterruptBoundary(finishEvent, executionResult.needsApproval, executionResult.needsClientExecution);
|
|
773
|
+
this.setToolPhase(emitted ? "wait" : "stop");
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
yield* this.flushDeferredToolCallRunFinishedChunks();
|
|
777
|
+
const toolResultChunks = this.buildToolResultChunks(allResults, finishEvent);
|
|
778
|
+
for (const chunk of toolResultChunks) yield* this.pipeThroughMiddleware(chunk);
|
|
779
|
+
if (this.lazyToolManager.hasNewlyDiscoveredTools()) {
|
|
780
|
+
this.tools = this.lazyToolManager.getActiveTools();
|
|
781
|
+
this.toolCallManager = new ToolCallManager(this.tools);
|
|
782
|
+
this.setToolPhase("continue");
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
this.toolCallManager.clear();
|
|
786
|
+
this.setToolPhase("continue");
|
|
787
|
+
}
|
|
788
|
+
shouldDeferToolCallRunFinished(chunk) {
|
|
789
|
+
return chunk.type === EventType.RUN_FINISHED && this.finishedEvent?.finishReason === "tool_calls" && this.tools.length > 0 && this.toolCallManager.hasToolCalls();
|
|
790
|
+
}
|
|
791
|
+
*flushDeferredToolCallRunFinishedChunks() {
|
|
792
|
+
for (const chunk of this.deferredToolCallRunFinishedChunks) {
|
|
793
|
+
this.logger.output(`type=${chunk.type}`, { chunk });
|
|
794
|
+
yield chunk;
|
|
795
|
+
this.middlewareCtx.chunkIndex++;
|
|
796
|
+
}
|
|
797
|
+
this.deferredToolCallRunFinishedChunks = [];
|
|
798
|
+
}
|
|
799
|
+
discardDeferredToolCallRunFinishedChunks() {
|
|
800
|
+
this.deferredToolCallRunFinishedChunks = [];
|
|
801
|
+
}
|
|
802
|
+
shouldExecuteToolPhase() {
|
|
803
|
+
return this.finishedEvent?.finishReason === "tool_calls" && this.tools.length > 0 && this.toolCallManager.hasToolCalls();
|
|
804
|
+
}
|
|
805
|
+
addAssistantToolCallMessage(toolCalls) {
|
|
806
|
+
this.finalizeCurrentThinkingStep();
|
|
807
|
+
this.messages = [...this.messages, {
|
|
808
|
+
role: "assistant",
|
|
809
|
+
content: this.accumulatedContent || null,
|
|
810
|
+
toolCalls,
|
|
811
|
+
...this.accumulatedThinking.length > 0 && { thinking: this.accumulatedThinking }
|
|
812
|
+
}];
|
|
813
|
+
this.middlewareCtx.messages = this.messages;
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Extract client state (approvals and client tool results) from original messages.
|
|
817
|
+
* This is called in the constructor BEFORE converting to ModelMessage format,
|
|
818
|
+
* because the parts array (which contains approval state) is lost during conversion.
|
|
819
|
+
*/
|
|
820
|
+
extractClientStateFromOriginalMessages(originalMessages) {
|
|
821
|
+
const approvals = /* @__PURE__ */ new Map();
|
|
822
|
+
const clientToolResults = /* @__PURE__ */ new Map();
|
|
823
|
+
for (const message of originalMessages) if (message.role === "assistant" && message.parts) {
|
|
824
|
+
for (const part of message.parts) if (part.type === "tool-call") {
|
|
825
|
+
if (part.output !== void 0 && !part.approval) clientToolResults.set(part.id, part.output);
|
|
826
|
+
if (part.approval?.id && part.approval?.approved !== void 0 && part.state === "approval-responded") approvals.set(part.approval.id, part.approval.approved);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return {
|
|
830
|
+
approvals,
|
|
831
|
+
clientToolResults
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
collectClientState() {
|
|
835
|
+
const approvals = new Map(this.initialApprovals);
|
|
836
|
+
const clientToolResults = new Map(this.initialClientToolResults);
|
|
837
|
+
for (const [approvalId, approved] of this.resumeApprovals) approvals.set(approvalId, approved);
|
|
838
|
+
for (const [toolCallId, result] of this.resumeClientToolResults) clientToolResults.set(toolCallId, result);
|
|
839
|
+
for (const message of this.messages) if (message.role === "tool" && message.toolCallId) {
|
|
840
|
+
let output;
|
|
841
|
+
if (Array.isArray(message.content)) output = message.content;
|
|
842
|
+
else try {
|
|
843
|
+
output = JSON.parse(message.content);
|
|
844
|
+
} catch {
|
|
845
|
+
output = message.content;
|
|
846
|
+
}
|
|
847
|
+
if (output && typeof output === "object" && output.pendingExecution === true) continue;
|
|
848
|
+
clientToolResults.set(message.toolCallId, output);
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
approvals,
|
|
852
|
+
clientToolResults
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
buildActionableInterrupts(approvals, clientRequests) {
|
|
856
|
+
const interrupts = [];
|
|
857
|
+
for (const approval of approvals) {
|
|
858
|
+
const tool = this.tools.find((candidate) => candidate.name === approval.toolName);
|
|
859
|
+
const normalized = normalizeApprovalSchema(tool?.approvalSchema, tool?.inputSchema);
|
|
860
|
+
interrupts.push({
|
|
861
|
+
id: approval.approvalId,
|
|
862
|
+
reason: "tool_call",
|
|
863
|
+
message: `Approval required to run ${approval.toolName}`,
|
|
864
|
+
toolCallId: approval.toolCallId,
|
|
865
|
+
responseSchema: normalized.responseSchema,
|
|
866
|
+
metadata: {
|
|
867
|
+
kind: "approval",
|
|
868
|
+
toolName: approval.toolName,
|
|
869
|
+
input: approval.input,
|
|
870
|
+
[interruptBindingMetadataKey]: {
|
|
871
|
+
v: 1,
|
|
872
|
+
kind: "tool-approval",
|
|
873
|
+
interruptId: approval.approvalId,
|
|
874
|
+
toolName: approval.toolName,
|
|
875
|
+
toolCallId: approval.toolCallId,
|
|
876
|
+
originalArgs: approval.input,
|
|
877
|
+
inputSchemaHash: hashSchemaInput(tool?.inputSchema),
|
|
878
|
+
approvalSchemaHash: normalized.approvalSchemaHash,
|
|
879
|
+
responseSchemaHash: normalized.responseSchemaHash
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
for (const clientTool of clientRequests) {
|
|
885
|
+
const tool = this.tools.find((candidate) => candidate.name === clientTool.toolName);
|
|
886
|
+
const responseSchema = convertSchemaToJsonSchema(tool?.outputSchema) ?? {};
|
|
887
|
+
interrupts.push({
|
|
888
|
+
id: `client_tool_${clientTool.toolCallId}`,
|
|
889
|
+
reason: "tanstack:client_tool_execution",
|
|
890
|
+
message: `Client tool ${clientTool.toolName} is ready to run`,
|
|
891
|
+
toolCallId: clientTool.toolCallId,
|
|
892
|
+
responseSchema,
|
|
893
|
+
metadata: {
|
|
894
|
+
kind: "client_tool",
|
|
895
|
+
toolName: clientTool.toolName,
|
|
896
|
+
input: clientTool.input,
|
|
897
|
+
[interruptBindingMetadataKey]: {
|
|
898
|
+
v: 1,
|
|
899
|
+
kind: "client-tool-execution",
|
|
900
|
+
interruptId: `client_tool_${clientTool.toolCallId}`,
|
|
901
|
+
toolName: clientTool.toolName,
|
|
902
|
+
toolCallId: clientTool.toolCallId,
|
|
903
|
+
outputSchemaHash: hashSchemaInput(tool?.outputSchema),
|
|
904
|
+
responseSchemaHash: digestInterruptJson(canonicalInterruptJson(responseSchema))
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
return interrupts;
|
|
910
|
+
}
|
|
911
|
+
buildInterruptFinishedChunk(finishEvent, approvals, clientRequests) {
|
|
912
|
+
return {
|
|
913
|
+
...finishEvent,
|
|
914
|
+
timestamp: Date.now(),
|
|
915
|
+
outcome: {
|
|
916
|
+
type: "interrupt",
|
|
917
|
+
interrupts: this.buildActionableInterrupts(approvals, clientRequests)
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
buildMessagesSnapshotChunk() {
|
|
922
|
+
const messages = this.messages.map((message, index) => {
|
|
923
|
+
const content = typeof message.content === "string" ? message.content : message.content === null ? void 0 : JSON.stringify(message.content);
|
|
924
|
+
return {
|
|
925
|
+
id: `snapshot_${this.runIdOverride ?? this.requestId}_${index}`,
|
|
926
|
+
role: message.role,
|
|
927
|
+
...content !== void 0 ? { content } : {},
|
|
928
|
+
..."toolCalls" in message && message.toolCalls ? { toolCalls: message.toolCalls } : {},
|
|
929
|
+
..."toolCallId" in message && message.toolCallId ? { toolCallId: message.toolCallId } : {}
|
|
930
|
+
};
|
|
931
|
+
});
|
|
932
|
+
return {
|
|
933
|
+
type: EventType.MESSAGES_SNAPSHOT,
|
|
934
|
+
timestamp: Date.now(),
|
|
935
|
+
model: this.params.model,
|
|
936
|
+
messages
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
publicInterruptTerminal(chunk) {
|
|
940
|
+
if (chunk.type !== EventType.RUN_FINISHED || chunk.outcome?.type !== "interrupt") return chunk;
|
|
941
|
+
return {
|
|
942
|
+
...chunk,
|
|
943
|
+
outcome: {
|
|
944
|
+
...chunk.outcome,
|
|
945
|
+
interrupts: chunk.outcome.interrupts.map((interrupt) => {
|
|
946
|
+
if (!interrupt.metadata || typeof interrupt.metadata !== "object" || Array.isArray(interrupt.metadata)) return interrupt;
|
|
947
|
+
const metadata = { ...interrupt.metadata };
|
|
948
|
+
const binding = normalizePublicInterruptBinding(metadata[interruptBindingMetadataKey], interrupt.id);
|
|
949
|
+
if (binding) metadata[interruptBindingMetadataKey] = binding;
|
|
950
|
+
else delete metadata[interruptBindingMetadataKey];
|
|
951
|
+
return {
|
|
952
|
+
...interrupt,
|
|
953
|
+
metadata
|
|
954
|
+
};
|
|
955
|
+
})
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
interruptFailure(error) {
|
|
960
|
+
const structured = structuralInterruptFailure(error);
|
|
961
|
+
if (structured) return {
|
|
962
|
+
message: structured.error.message,
|
|
963
|
+
code: structured.errors[0]?.code ?? "server",
|
|
964
|
+
errors: structured.errors
|
|
965
|
+
};
|
|
966
|
+
if (error && typeof error === "object" && "errors" in error) {
|
|
967
|
+
const errors = error.errors;
|
|
968
|
+
if (Array.isArray(errors)) {
|
|
969
|
+
const first = errors[0];
|
|
970
|
+
if (first && typeof first === "object") return {
|
|
971
|
+
message: "message" in first && typeof first.message === "string" ? first.message : "Interrupt persistence failed.",
|
|
972
|
+
code: "code" in first && typeof first.code === "string" ? first.code : "server"
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return {
|
|
977
|
+
message: error instanceof Error ? error.message : "Interrupt persistence failed.",
|
|
978
|
+
code: "server"
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
buildInterruptRunErrorChunk(error) {
|
|
982
|
+
const failure = this.interruptFailure(error);
|
|
983
|
+
return {
|
|
984
|
+
type: EventType.RUN_ERROR,
|
|
985
|
+
timestamp: Date.now(),
|
|
986
|
+
runId: this.runIdOverride ?? this.requestId,
|
|
987
|
+
threadId: this.threadId,
|
|
988
|
+
message: failure.message,
|
|
989
|
+
code: failure.code,
|
|
990
|
+
error: {
|
|
991
|
+
message: failure.message,
|
|
992
|
+
code: failure.code
|
|
993
|
+
},
|
|
994
|
+
...failure.errors !== void 0 ? { "tanstack:interruptErrors": failure.errors } : {}
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
async *emitInterruptRunError(error) {
|
|
998
|
+
const failure = this.interruptFailure(error);
|
|
999
|
+
this.finalizationError = {
|
|
1000
|
+
message: failure.message,
|
|
1001
|
+
code: failure.code,
|
|
1002
|
+
cause: error
|
|
1003
|
+
};
|
|
1004
|
+
yield* this.pipeThroughMiddleware(this.buildInterruptRunErrorChunk(error));
|
|
1005
|
+
}
|
|
1006
|
+
async *emitActionableInterruptBoundary(finishEvent, approvals, clientRequests) {
|
|
1007
|
+
const terminal = this.completeEphemeralInterruptBindings(this.buildInterruptFinishedChunk(finishEvent, approvals, clientRequests));
|
|
1008
|
+
let terminalOutputs;
|
|
1009
|
+
try {
|
|
1010
|
+
terminalOutputs = await this.middlewareRunner.runOnChunk(this.middlewareCtx, terminal);
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
yield* this.emitInterruptRunError(error);
|
|
1013
|
+
return false;
|
|
1014
|
+
}
|
|
1015
|
+
yield* this.pipeThroughMiddleware(this.buildMessagesSnapshotChunk());
|
|
1016
|
+
if (this.params.state !== void 0) yield* this.pipeThroughMiddleware({
|
|
1017
|
+
type: EventType.STATE_SNAPSHOT,
|
|
1018
|
+
timestamp: Date.now(),
|
|
1019
|
+
model: this.params.model,
|
|
1020
|
+
snapshot: this.params.state
|
|
1021
|
+
});
|
|
1022
|
+
for (const output of terminalOutputs) {
|
|
1023
|
+
yield this.publicInterruptTerminal(output);
|
|
1024
|
+
this.middlewareCtx.chunkIndex++;
|
|
1025
|
+
}
|
|
1026
|
+
return true;
|
|
1027
|
+
}
|
|
1028
|
+
completeEphemeralInterruptBindings(chunk) {
|
|
1029
|
+
if (chunk.type !== EventType.RUN_FINISHED || chunk.outcome?.type !== "interrupt") return chunk;
|
|
1030
|
+
const interruptedRunId = this.runIdOverride ?? this.requestId;
|
|
1031
|
+
return {
|
|
1032
|
+
...chunk,
|
|
1033
|
+
outcome: {
|
|
1034
|
+
...chunk.outcome,
|
|
1035
|
+
interrupts: chunk.outcome.interrupts.map((interrupt) => {
|
|
1036
|
+
if (!interrupt.metadata || typeof interrupt.metadata !== "object" || Array.isArray(interrupt.metadata)) return interrupt;
|
|
1037
|
+
const metadata = { ...interrupt.metadata };
|
|
1038
|
+
const unopened = metadata[interruptBindingMetadataKey];
|
|
1039
|
+
if (unopened === null || typeof unopened !== "object" || Array.isArray(unopened)) return interrupt;
|
|
1040
|
+
metadata[interruptBindingMetadataKey] = {
|
|
1041
|
+
...unopened,
|
|
1042
|
+
interruptedRunId,
|
|
1043
|
+
generation: 0
|
|
1044
|
+
};
|
|
1045
|
+
return {
|
|
1046
|
+
...interrupt,
|
|
1047
|
+
metadata
|
|
1048
|
+
};
|
|
1049
|
+
})
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
buildToolResultChunks(results, finishEvent, argsMap) {
|
|
1054
|
+
const chunks = [];
|
|
1055
|
+
for (const result of results) {
|
|
1056
|
+
const content = normalizeToolResult(result.result);
|
|
1057
|
+
const wireContent = typeof content === "string" ? content : JSON.stringify(content);
|
|
1058
|
+
if (argsMap) {
|
|
1059
|
+
chunks.push({
|
|
1060
|
+
type: "TOOL_CALL_START",
|
|
1061
|
+
timestamp: Date.now(),
|
|
1062
|
+
model: finishEvent.model,
|
|
1063
|
+
toolCallId: result.toolCallId,
|
|
1064
|
+
toolCallName: result.toolName,
|
|
1065
|
+
toolName: result.toolName
|
|
1066
|
+
});
|
|
1067
|
+
const args = argsMap.get(result.toolCallId) ?? "{}";
|
|
1068
|
+
chunks.push({
|
|
1069
|
+
type: "TOOL_CALL_ARGS",
|
|
1070
|
+
timestamp: Date.now(),
|
|
1071
|
+
model: finishEvent.model,
|
|
1072
|
+
toolCallId: result.toolCallId,
|
|
1073
|
+
delta: args,
|
|
1074
|
+
args
|
|
1075
|
+
});
|
|
1076
|
+
chunks.push({
|
|
1077
|
+
type: "TOOL_CALL_END",
|
|
1078
|
+
timestamp: Date.now(),
|
|
1079
|
+
model: finishEvent.model,
|
|
1080
|
+
toolCallId: result.toolCallId,
|
|
1081
|
+
toolCallName: result.toolName,
|
|
1082
|
+
toolName: result.toolName,
|
|
1083
|
+
result: wireContent,
|
|
1084
|
+
...result.input !== void 0 && { input: result.input },
|
|
1085
|
+
...result.output !== void 0 && { output: result.output },
|
|
1086
|
+
...result.state !== void 0 && { state: result.state }
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
chunks.push({
|
|
1090
|
+
type: "TOOL_CALL_RESULT",
|
|
1091
|
+
timestamp: Date.now(),
|
|
1092
|
+
model: finishEvent.model,
|
|
1093
|
+
messageId: this.createId("tool-result"),
|
|
1094
|
+
toolCallId: result.toolCallId,
|
|
1095
|
+
content: wireContent,
|
|
1096
|
+
role: "tool",
|
|
1097
|
+
...result.state !== void 0 && { state: result.state }
|
|
1098
|
+
});
|
|
1099
|
+
const placeholderIdx = this.messages.findIndex((m) => {
|
|
1100
|
+
if (m.role !== "tool" || m.toolCallId !== result.toolCallId) return false;
|
|
1101
|
+
if (typeof m.content !== "string") return false;
|
|
1102
|
+
try {
|
|
1103
|
+
return JSON.parse(m.content)?.pendingExecution === true;
|
|
1104
|
+
} catch {
|
|
1105
|
+
return false;
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
const newToolMessage = {
|
|
1109
|
+
role: "tool",
|
|
1110
|
+
content,
|
|
1111
|
+
toolCallId: result.toolCallId
|
|
1112
|
+
};
|
|
1113
|
+
if (placeholderIdx >= 0) this.messages = [
|
|
1114
|
+
...this.messages.slice(0, placeholderIdx),
|
|
1115
|
+
newToolMessage,
|
|
1116
|
+
...this.messages.slice(placeholderIdx + 1)
|
|
1117
|
+
];
|
|
1118
|
+
else this.messages = [...this.messages, newToolMessage];
|
|
1119
|
+
this.middlewareCtx.messages = this.messages;
|
|
1120
|
+
}
|
|
1121
|
+
return chunks;
|
|
1122
|
+
}
|
|
1123
|
+
getPendingToolCallsFromMessages() {
|
|
1124
|
+
const completedToolIds = /* @__PURE__ */ new Set();
|
|
1125
|
+
for (const message of this.messages) if (message.role === "tool" && message.toolCallId) {
|
|
1126
|
+
let hasPendingExecution = false;
|
|
1127
|
+
if (typeof message.content === "string") try {
|
|
1128
|
+
if (JSON.parse(message.content).pendingExecution === true) hasPendingExecution = true;
|
|
1129
|
+
} catch {}
|
|
1130
|
+
if (!hasPendingExecution) completedToolIds.add(message.toolCallId);
|
|
1131
|
+
}
|
|
1132
|
+
const pending = [];
|
|
1133
|
+
for (const message of this.messages) if (message.role === "assistant" && message.toolCalls) for (const toolCall of message.toolCalls) {
|
|
1134
|
+
if (isProviderExecutedToolCall(toolCall)) continue;
|
|
1135
|
+
if (!completedToolIds.has(toolCall.id)) pending.push(toolCall);
|
|
1136
|
+
}
|
|
1137
|
+
return pending;
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Find a tool call by id in message history (including already-completed ones).
|
|
1141
|
+
* Used when the client has already attached a tool result for UI before resume.
|
|
1142
|
+
*/
|
|
1143
|
+
findToolCallInMessages(toolCallId) {
|
|
1144
|
+
for (const message of this.messages) {
|
|
1145
|
+
if (message.role !== "assistant" || !message.toolCalls) continue;
|
|
1146
|
+
for (const toolCall of message.toolCalls) if (toolCall.id === toolCallId) return toolCall;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Tool calls that must be reconstructed as interrupt pending for ephemeral
|
|
1151
|
+
* resume. Includes outstanding tools plus client tools that already have
|
|
1152
|
+
* results in history when the resume batch still carries `client_tool_*`
|
|
1153
|
+
* entries (the client writes local tool results before submitting resume).
|
|
1154
|
+
*/
|
|
1155
|
+
getToolCallsForEphemeralResume(resume) {
|
|
1156
|
+
const pending = this.getPendingToolCallsFromMessages();
|
|
1157
|
+
const byId = new Map(pending.map((toolCall) => [toolCall.id, toolCall]));
|
|
1158
|
+
for (const entry of resume ?? []) {
|
|
1159
|
+
let toolCallId;
|
|
1160
|
+
if (entry.interruptId.startsWith("client_tool_")) toolCallId = entry.interruptId.slice(12);
|
|
1161
|
+
else if (entry.interruptId.startsWith("approval_")) toolCallId = entry.interruptId.slice(9);
|
|
1162
|
+
if (toolCallId === void 0 || byId.has(toolCallId)) continue;
|
|
1163
|
+
const toolCall = this.findToolCallInMessages(toolCallId);
|
|
1164
|
+
if (toolCall && !isProviderExecutedToolCall(toolCall)) {
|
|
1165
|
+
pending.push(toolCall);
|
|
1166
|
+
byId.set(toolCallId, toolCall);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return pending;
|
|
1170
|
+
}
|
|
1171
|
+
createSyntheticFinishedEvent() {
|
|
1172
|
+
return {
|
|
1173
|
+
type: "RUN_FINISHED",
|
|
1174
|
+
runId: this.createId("pending"),
|
|
1175
|
+
threadId: this.threadId,
|
|
1176
|
+
model: this.params.model,
|
|
1177
|
+
timestamp: Date.now(),
|
|
1178
|
+
finishReason: "tool_calls"
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
async shouldContinue() {
|
|
1182
|
+
if (this.cyclePhase === "executeToolCalls") return true;
|
|
1183
|
+
const state = {
|
|
1184
|
+
iterationCount: this.iterationCount,
|
|
1185
|
+
messages: this.messages,
|
|
1186
|
+
finishReason: this.lastFinishReason,
|
|
1187
|
+
toolCallCount: this.toolCallCount,
|
|
1188
|
+
lastTurnToolCallCount: this.lastTurnToolCallCount
|
|
1189
|
+
};
|
|
1190
|
+
const strategyContinues = this.loopStrategy(state);
|
|
1191
|
+
const middlewareContinues = await this.middlewareRunner.runOnShouldContinue(this.middlewareCtx, state);
|
|
1192
|
+
return strategyContinues && middlewareContinues && this.toolPhase === "continue";
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Record tool calls (deduped by id) toward `toolCallCount` /
|
|
1196
|
+
* `lastTurnToolCallCount` for strategies and middleware `onShouldContinue`.
|
|
1197
|
+
*
|
|
1198
|
+
* Used for both live model turns and pending/resume batches. IDs already
|
|
1199
|
+
* counted in this run (e.g. wait→resume after a live turn) are not
|
|
1200
|
+
* re-added to `toolCallCount`. Per-turn execution caps are app middleware
|
|
1201
|
+
* (`onBeforeToolCall` skip), not engine policy.
|
|
1202
|
+
*/
|
|
1203
|
+
recordToolCalls(toolCalls) {
|
|
1204
|
+
this.lastTurnToolCallCount = toolCalls.length;
|
|
1205
|
+
let newlyCounted = 0;
|
|
1206
|
+
for (const tc of toolCalls) if (!this.countedToolCallIds.has(tc.id)) {
|
|
1207
|
+
this.countedToolCallIds.add(tc.id);
|
|
1208
|
+
newlyCounted++;
|
|
1209
|
+
}
|
|
1210
|
+
this.toolCallCount += newlyCounted;
|
|
1211
|
+
}
|
|
1212
|
+
isAborted() {
|
|
1213
|
+
return !!this.effectiveSignal?.aborted;
|
|
1214
|
+
}
|
|
1215
|
+
isMiddlewareAborted() {
|
|
1216
|
+
return !!this.middlewareAbortController?.signal.aborted;
|
|
1217
|
+
}
|
|
1218
|
+
isCancelled() {
|
|
1219
|
+
return this.isAborted() || this.isMiddlewareAborted();
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* The reason to report on `AbortInfo` for a cancelled run.
|
|
1223
|
+
*
|
|
1224
|
+
* `this.abortReason` only ever holds a *middleware*-initiated reason
|
|
1225
|
+
* (`ctx.abort(reason)` / `MiddlewareAbortError`). A caller that aborts its own
|
|
1226
|
+
* controller — `abortController.abort(RUN_CANCEL_REASON)`, the in-process
|
|
1227
|
+
* cancel channel — never touches that field, so the reason has to be read back
|
|
1228
|
+
* off the caller's signal, which is the signal `isCancelled()` consults via
|
|
1229
|
+
* `isAborted()`. A signal aborted with no reason carries a DOMException rather
|
|
1230
|
+
* than a string, so non-string reasons are reported as absent.
|
|
1231
|
+
*/
|
|
1232
|
+
resolveAbortReason() {
|
|
1233
|
+
if (this.abortReason !== void 0) return this.abortReason;
|
|
1234
|
+
const signalReason = this.effectiveSignal?.reason;
|
|
1235
|
+
return typeof signalReason === "string" ? signalReason : void 0;
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Whether this run's teardown declared its abort a DETACH — see
|
|
1239
|
+
* {@link RunDetachedCapability}. Only `withSandbox`'s `onAbort` publishes it,
|
|
1240
|
+
* and only for a plain, intentless disconnect of a detachable run, so every
|
|
1241
|
+
* other exit path answers `false`.
|
|
1242
|
+
*
|
|
1243
|
+
* Surfaced on the engine (rather than the ctx being handed out) so the
|
|
1244
|
+
* capability read stays inside core, and so the delivery sink learns the
|
|
1245
|
+
* verdict through {@link publishRunDetachedSignal} instead of reaching into a
|
|
1246
|
+
* middleware context it has no business holding.
|
|
1247
|
+
*
|
|
1248
|
+
* @internal
|
|
1249
|
+
*/
|
|
1250
|
+
wasDetached() {
|
|
1251
|
+
return getRunDetached(this.middlewareCtx, { optional: true }) === true;
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* The delivery socket closed while this run was still going.
|
|
1255
|
+
*
|
|
1256
|
+
* Notifies every subscriber (see {@link RunDisconnectCapability}) and RETURNS
|
|
1257
|
+
* IMMEDIATELY. Synchronous on purpose: it is called from
|
|
1258
|
+
* `ReadableStream.cancel()`, which must not be made to wait on a run-store
|
|
1259
|
+
* write, and the caller ({@link notifyRunDisconnected}) has no consumer left to
|
|
1260
|
+
* report to anyway.
|
|
1261
|
+
*
|
|
1262
|
+
* Subscribers therefore run CONCURRENTLY with the still-executing run — which is
|
|
1263
|
+
* the entire point. The run is typically suspended inside a slow middleware
|
|
1264
|
+
* `setup` at this moment, so anything dispatched from the run's own unwinding
|
|
1265
|
+
* would be minutes late. Nothing on this path aborts the run: a durable run
|
|
1266
|
+
* outlives its viewer.
|
|
1267
|
+
*
|
|
1268
|
+
* Each subscriber's promise is parked on `deferredPromises`, which the run awaits
|
|
1269
|
+
* in its `finally`, so bookkeeping cannot be lost to a race with the run's own
|
|
1270
|
+
* completion even though nothing awaits it here.
|
|
1271
|
+
*
|
|
1272
|
+
* IDEMPOTENT. A second cancel, or one arriving after a terminal hook already ran,
|
|
1273
|
+
* is ignored: the terminal hooks own the run's outcome, and re-stamping
|
|
1274
|
+
* `detachedSince` on a run that has already finished would hand a completed run
|
|
1275
|
+
* to the reaper as reclaimable work.
|
|
1276
|
+
*
|
|
1277
|
+
* @internal
|
|
1278
|
+
*/
|
|
1279
|
+
notifyDisconnected() {
|
|
1280
|
+
if (this.disconnected || this.terminalHookCalled) return;
|
|
1281
|
+
this.disconnected = true;
|
|
1282
|
+
for (const listener of this.disconnectListeners) this.runDisconnectListener(listener);
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Invoke one disconnect listener, isolated and with its failure SWALLOWED after
|
|
1286
|
+
* logging.
|
|
1287
|
+
*
|
|
1288
|
+
* There is no caller left to report to — the socket this would report on is the
|
|
1289
|
+
* one that just closed — and a rejection parked on `deferredPromises` would
|
|
1290
|
+
* surface as the run's failure, replacing a healthy outcome with a bookkeeping
|
|
1291
|
+
* error. Isolation matters for the usual reason too: one subscriber's failing
|
|
1292
|
+
* write must not skip the next one's.
|
|
1293
|
+
*/
|
|
1294
|
+
runDisconnectListener(listener) {
|
|
1295
|
+
let result;
|
|
1296
|
+
try {
|
|
1297
|
+
result = listener();
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
this.logger.errors("run disconnect listener failed", { error });
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
if (result === void 0) return;
|
|
1303
|
+
this.deferredPromises.push(result.catch((error) => {
|
|
1304
|
+
this.logger.errors("run disconnect listener failed", { error });
|
|
1305
|
+
}));
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Run the final structured-output adapter call through the middleware
|
|
1309
|
+
* pipeline. Yields chunks to the caller only when
|
|
1310
|
+
* `this.finalStructuredOutput.yieldChunks` is true; otherwise consumes
|
|
1311
|
+
* silently while still piping through middleware.
|
|
1312
|
+
*
|
|
1313
|
+
* On success, populates this.structuredOutputResult.
|
|
1314
|
+
* On failure, populates this.finalizationError.
|
|
1315
|
+
*/
|
|
1316
|
+
async *runStructuredFinalization() {
|
|
1317
|
+
if (!this.finalStructuredOutput) throw new Error("runStructuredFinalization called without finalStructuredOutput config");
|
|
1318
|
+
this.middlewareCtx.phase = "structuredOutput";
|
|
1319
|
+
const baseConfig = this.buildMiddlewareConfig();
|
|
1320
|
+
const { tools: _omitTools, ...baseWithoutTools } = baseConfig;
|
|
1321
|
+
let structuredConfig = {
|
|
1322
|
+
...baseWithoutTools,
|
|
1323
|
+
outputSchema: this.finalStructuredOutput.jsonSchema
|
|
1324
|
+
};
|
|
1325
|
+
structuredConfig = await this.middlewareRunner.runOnStructuredOutputConfig(this.middlewareCtx, structuredConfig);
|
|
1326
|
+
const { outputSchema: pinnedSchema, ...chatConfigSlice } = structuredConfig;
|
|
1327
|
+
const postOnConfig = await this.middlewareRunner.runOnConfig(this.middlewareCtx, {
|
|
1328
|
+
...chatConfigSlice,
|
|
1329
|
+
tools: baseConfig.tools
|
|
1330
|
+
});
|
|
1331
|
+
this.applyMiddlewareConfig(postOnConfig);
|
|
1332
|
+
const structuredCallOptions = {
|
|
1333
|
+
chatOptions: {
|
|
1334
|
+
model: this.params.model,
|
|
1335
|
+
messages: this.messages,
|
|
1336
|
+
metadata: postOnConfig.metadata,
|
|
1337
|
+
modelOptions: postOnConfig.modelOptions,
|
|
1338
|
+
systemPrompts: postOnConfig.systemPrompts,
|
|
1339
|
+
logger: this.logger,
|
|
1340
|
+
threadId: this.threadId,
|
|
1341
|
+
runId: this.runIdOverride,
|
|
1342
|
+
parentRunId: this.parentRunIdOverride,
|
|
1343
|
+
...this.effectiveRequest ? { request: this.effectiveRequest } : {}
|
|
1344
|
+
},
|
|
1345
|
+
outputSchema: pinnedSchema
|
|
1346
|
+
};
|
|
1347
|
+
let fallbackAdapterError = void 0;
|
|
1348
|
+
const providerStream = this.adapter.structuredOutputStream ? this.adapter.structuredOutputStream(structuredCallOptions) : fallbackStructuredOutputStream(this.adapter, structuredCallOptions, (err) => {
|
|
1349
|
+
fallbackAdapterError = err;
|
|
1350
|
+
});
|
|
1351
|
+
let startEmitted = false;
|
|
1352
|
+
let structuredMessageId = null;
|
|
1353
|
+
const extractMessageId = (c) => {
|
|
1354
|
+
if (c.type === EventType.TEXT_MESSAGE_START || c.type === EventType.TEXT_MESSAGE_CONTENT || c.type === EventType.TEXT_MESSAGE_END) return typeof c.messageId === "string" && c.messageId !== "" ? c.messageId : null;
|
|
1355
|
+
return null;
|
|
1356
|
+
};
|
|
1357
|
+
const buildSynthesizedStart = () => {
|
|
1358
|
+
const idForStart = structuredMessageId ?? generateMessageId();
|
|
1359
|
+
structuredMessageId = idForStart;
|
|
1360
|
+
return {
|
|
1361
|
+
type: EventType.CUSTOM,
|
|
1362
|
+
name: "structured-output.start",
|
|
1363
|
+
value: { messageId: idForStart },
|
|
1364
|
+
model: this.params.model,
|
|
1365
|
+
timestamp: Date.now(),
|
|
1366
|
+
threadId: this.threadId,
|
|
1367
|
+
...this.runIdOverride ? { runId: this.runIdOverride } : {}
|
|
1368
|
+
};
|
|
1369
|
+
};
|
|
1370
|
+
const pipeThroughMiddleware = async (synthChunk) => this.middlewareRunner.runOnChunk(this.middlewareCtx, synthChunk);
|
|
1371
|
+
let runErrorYielded = false;
|
|
1372
|
+
for await (const chunk of providerStream) {
|
|
1373
|
+
if (this.isCancelled()) break;
|
|
1374
|
+
if (!startEmitted && chunk.type === EventType.CUSTOM && chunk.name === "structured-output.start") startEmitted = true;
|
|
1375
|
+
if (!structuredMessageId) {
|
|
1376
|
+
const extracted = extractMessageId(chunk);
|
|
1377
|
+
if (extracted) structuredMessageId = extracted;
|
|
1378
|
+
}
|
|
1379
|
+
if (this.finalStructuredOutput.yieldChunks) {
|
|
1380
|
+
if (!startEmitted && (chunk.type === EventType.TEXT_MESSAGE_START || chunk.type === EventType.TEXT_MESSAGE_CONTENT || chunk.type === EventType.TEXT_MESSAGE_END)) {
|
|
1381
|
+
startEmitted = true;
|
|
1382
|
+
const synthOutputs = await pipeThroughMiddleware(buildSynthesizedStart());
|
|
1383
|
+
for (const outputChunk of synthOutputs) {
|
|
1384
|
+
yield outputChunk;
|
|
1385
|
+
this.middlewareCtx.chunkIndex++;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
if (!startEmitted && chunk.type === EventType.RUN_ERROR) {
|
|
1389
|
+
startEmitted = true;
|
|
1390
|
+
const synthOutputs = await pipeThroughMiddleware(buildSynthesizedStart());
|
|
1391
|
+
for (const outputChunk of synthOutputs) {
|
|
1392
|
+
yield outputChunk;
|
|
1393
|
+
this.middlewareCtx.chunkIndex++;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
let outboundChunk = chunk;
|
|
1398
|
+
if (chunk.type === EventType.CUSTOM && chunk.name === "structured-output.complete") {
|
|
1399
|
+
const parsed = readStructuredOutputCompleteValue(chunk.value);
|
|
1400
|
+
if (parsed) {
|
|
1401
|
+
const object = this.finalStructuredOutput.normalize ? this.finalStructuredOutput.normalize(parsed.object) : parsed.object;
|
|
1402
|
+
this.structuredOutputResult = {
|
|
1403
|
+
data: object,
|
|
1404
|
+
rawText: parsed.raw
|
|
1405
|
+
};
|
|
1406
|
+
const value = chunk.value;
|
|
1407
|
+
if (object !== parsed.object && value && typeof value === "object") outboundChunk = {
|
|
1408
|
+
...chunk,
|
|
1409
|
+
value: {
|
|
1410
|
+
...value,
|
|
1411
|
+
object
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
if (chunk.type === EventType.RUN_FINISHED && chunk.usage) await this.middlewareRunner.runOnUsage(this.middlewareCtx, chunk.usage);
|
|
1417
|
+
if (chunk.type === EventType.RUN_ERROR) this.finalizationError = {
|
|
1418
|
+
message: chunk.message,
|
|
1419
|
+
...chunk.code ? { code: chunk.code } : {},
|
|
1420
|
+
...fallbackAdapterError !== void 0 ? { cause: fallbackAdapterError } : {}
|
|
1421
|
+
};
|
|
1422
|
+
const outputChunks = await this.middlewareRunner.runOnChunk(this.middlewareCtx, outboundChunk);
|
|
1423
|
+
if (this.finalStructuredOutput.yieldChunks) for (const outputChunk of outputChunks) {
|
|
1424
|
+
if (outputChunk.type === EventType.RUN_ERROR) runErrorYielded = true;
|
|
1425
|
+
yield outputChunk;
|
|
1426
|
+
this.middlewareCtx.chunkIndex++;
|
|
1427
|
+
}
|
|
1428
|
+
if (this.finalizationError) break;
|
|
1429
|
+
}
|
|
1430
|
+
if (this.isCancelled()) return;
|
|
1431
|
+
if (!this.structuredOutputResult && !this.finalizationError) this.finalizationError = {
|
|
1432
|
+
message: "missing structured result",
|
|
1433
|
+
code: "structured-output-missing-result"
|
|
1434
|
+
};
|
|
1435
|
+
if (this.structuredOutputResult && !this.finalizationError && this.finalStructuredOutput.validate) try {
|
|
1436
|
+
const validated = this.finalStructuredOutput.validate(this.structuredOutputResult.data);
|
|
1437
|
+
this.validatedStructuredOutput = validated;
|
|
1438
|
+
this.hasValidatedStructuredOutput = true;
|
|
1439
|
+
} catch (err) {
|
|
1440
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1441
|
+
this.finalizationError = {
|
|
1442
|
+
message,
|
|
1443
|
+
code: "structured-output-validation-failed",
|
|
1444
|
+
cause: err
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
if (this.finalizationError && this.finalStructuredOutput.yieldChunks && !runErrorYielded) {
|
|
1448
|
+
if (!startEmitted) {
|
|
1449
|
+
const startOutputs = await pipeThroughMiddleware(buildSynthesizedStart());
|
|
1450
|
+
for (const outputChunk of startOutputs) {
|
|
1451
|
+
yield outputChunk;
|
|
1452
|
+
this.middlewareCtx.chunkIndex++;
|
|
1453
|
+
}
|
|
1454
|
+
startEmitted = true;
|
|
1455
|
+
}
|
|
1456
|
+
const errChunk = {
|
|
1457
|
+
type: EventType.RUN_ERROR,
|
|
1458
|
+
runId: this.runIdOverride ?? this.requestId,
|
|
1459
|
+
model: this.params.model,
|
|
1460
|
+
timestamp: Date.now(),
|
|
1461
|
+
threadId: this.threadId,
|
|
1462
|
+
message: this.finalizationError.message,
|
|
1463
|
+
...this.finalizationError.code ? { code: this.finalizationError.code } : {},
|
|
1464
|
+
error: {
|
|
1465
|
+
message: this.finalizationError.message,
|
|
1466
|
+
...this.finalizationError.code ? { code: this.finalizationError.code } : {}
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
const outputChunks = await this.middlewareRunner.runOnChunk(this.middlewareCtx, errChunk);
|
|
1470
|
+
for (const outputChunk of outputChunks) {
|
|
1471
|
+
yield outputChunk;
|
|
1472
|
+
this.middlewareCtx.chunkIndex++;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Native combined mode: harvest the structured output from the agent
|
|
1478
|
+
* loop's accumulated final-turn text (no separate provider call).
|
|
1479
|
+
*
|
|
1480
|
+
* The adapter wired `outputSchema` into the regular `chatStream` request,
|
|
1481
|
+
* so the model's final-turn text is the schema-constrained JSON. We parse
|
|
1482
|
+
* `this.accumulatedContent`, populate `this.structuredOutputResult`, emit
|
|
1483
|
+
* a synthetic `structured-output.complete` (and a `structured-output.start`
|
|
1484
|
+
* if one wasn't emitted earlier — only happens on the streaming path when
|
|
1485
|
+
* the model returned no text at all), and run the validate callback when
|
|
1486
|
+
* present. Failures populate `this.finalizationError` so the engine's
|
|
1487
|
+
* terminal-hook chooser routes to `onError` (per spec §7.3).
|
|
1488
|
+
*
|
|
1489
|
+
* The `'structuredOutput'` middleware phase intentionally does NOT fire on
|
|
1490
|
+
* this path — middleware sees the run through `beforeModel` / `modelStream`
|
|
1491
|
+
* as usual. See PR #605 / issue #605 for the design rationale.
|
|
1492
|
+
*/
|
|
1493
|
+
async *harvestCombinedStructuredOutput() {
|
|
1494
|
+
if (!this.finalStructuredOutput) throw new Error("harvestCombinedStructuredOutput called without finalStructuredOutput config");
|
|
1495
|
+
const yieldChunks = this.finalStructuredOutput.yieldChunks;
|
|
1496
|
+
const rawText = this.accumulatedContent;
|
|
1497
|
+
if (rawText.length === 0) this.finalizationError = {
|
|
1498
|
+
message: "missing structured result",
|
|
1499
|
+
code: "structured-output-missing-result"
|
|
1500
|
+
};
|
|
1501
|
+
else try {
|
|
1502
|
+
const parsed = JSON.parse(rawText);
|
|
1503
|
+
const data = this.finalStructuredOutput.normalize ? this.finalStructuredOutput.normalize(parsed) : parsed;
|
|
1504
|
+
this.structuredOutputResult = {
|
|
1505
|
+
data,
|
|
1506
|
+
rawText
|
|
1507
|
+
};
|
|
1508
|
+
} catch (err) {
|
|
1509
|
+
const detail = rawText.slice(0, 200) + (rawText.length > 200 ? "..." : "");
|
|
1510
|
+
this.finalizationError = {
|
|
1511
|
+
message: `Failed to parse structured output as JSON. Content: ${detail}`,
|
|
1512
|
+
code: "structured-output-parse-failed",
|
|
1513
|
+
cause: err
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
if (this.structuredOutputResult && !this.finalizationError && this.finalStructuredOutput.validate) try {
|
|
1517
|
+
const validated = this.finalStructuredOutput.validate(this.structuredOutputResult.data);
|
|
1518
|
+
this.validatedStructuredOutput = validated;
|
|
1519
|
+
this.hasValidatedStructuredOutput = true;
|
|
1520
|
+
} catch (err) {
|
|
1521
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1522
|
+
this.finalizationError = {
|
|
1523
|
+
message,
|
|
1524
|
+
code: "structured-output-validation-failed",
|
|
1525
|
+
cause: err
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
if (!yieldChunks) return;
|
|
1529
|
+
if (!this.combinedStartEmitted) {
|
|
1530
|
+
this.combinedStartEmitted = true;
|
|
1531
|
+
const messageId = this.combinedStructuredMessageId ?? generateMessageId();
|
|
1532
|
+
this.combinedStructuredMessageId = messageId;
|
|
1533
|
+
const synthStart = {
|
|
1534
|
+
type: EventType.CUSTOM,
|
|
1535
|
+
name: "structured-output.start",
|
|
1536
|
+
value: { messageId },
|
|
1537
|
+
model: this.params.model,
|
|
1538
|
+
timestamp: Date.now(),
|
|
1539
|
+
threadId: this.threadId,
|
|
1540
|
+
...this.runIdOverride ? { runId: this.runIdOverride } : {}
|
|
1541
|
+
};
|
|
1542
|
+
const startOutputs = await this.middlewareRunner.runOnChunk(this.middlewareCtx, synthStart);
|
|
1543
|
+
for (const outputChunk of startOutputs) {
|
|
1544
|
+
yield outputChunk;
|
|
1545
|
+
this.middlewareCtx.chunkIndex++;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
if (this.structuredOutputResult && !this.finalizationError) {
|
|
1549
|
+
const completeChunk = {
|
|
1550
|
+
type: EventType.CUSTOM,
|
|
1551
|
+
name: "structured-output.complete",
|
|
1552
|
+
value: {
|
|
1553
|
+
object: this.structuredOutputResult.data,
|
|
1554
|
+
raw: this.structuredOutputResult.rawText,
|
|
1555
|
+
...this.combinedStructuredMessageId ? { messageId: this.combinedStructuredMessageId } : {}
|
|
1556
|
+
},
|
|
1557
|
+
model: this.params.model,
|
|
1558
|
+
timestamp: Date.now(),
|
|
1559
|
+
threadId: this.threadId,
|
|
1560
|
+
...this.runIdOverride ? { runId: this.runIdOverride } : {}
|
|
1561
|
+
};
|
|
1562
|
+
const completeOutputs = await this.middlewareRunner.runOnChunk(this.middlewareCtx, completeChunk);
|
|
1563
|
+
for (const outputChunk of completeOutputs) {
|
|
1564
|
+
yield outputChunk;
|
|
1565
|
+
this.middlewareCtx.chunkIndex++;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
if (this.finalizationError) {
|
|
1569
|
+
const errChunk = {
|
|
1570
|
+
type: EventType.RUN_ERROR,
|
|
1571
|
+
runId: this.runIdOverride ?? this.requestId,
|
|
1572
|
+
model: this.params.model,
|
|
1573
|
+
timestamp: Date.now(),
|
|
1574
|
+
threadId: this.threadId,
|
|
1575
|
+
message: this.finalizationError.message,
|
|
1576
|
+
...this.finalizationError.code ? { code: this.finalizationError.code } : {},
|
|
1577
|
+
error: {
|
|
1578
|
+
message: this.finalizationError.message,
|
|
1579
|
+
...this.finalizationError.code ? { code: this.finalizationError.code } : {}
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
const errOutputs = await this.middlewareRunner.runOnChunk(this.middlewareCtx, errChunk);
|
|
1583
|
+
for (const outputChunk of errOutputs) {
|
|
1584
|
+
yield outputChunk;
|
|
1585
|
+
this.middlewareCtx.chunkIndex++;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
buildMiddlewareConfig() {
|
|
1590
|
+
return {
|
|
1591
|
+
messages: this.messages,
|
|
1592
|
+
systemPrompts: [...this.systemPrompts],
|
|
1593
|
+
tools: [...this.tools],
|
|
1594
|
+
resume: this.params.resume,
|
|
1595
|
+
resumeToolState: {
|
|
1596
|
+
approvals: this.resumeApprovals,
|
|
1597
|
+
clientToolResults: this.resumeClientToolResults,
|
|
1598
|
+
deniedToolResults: this.resumeDeniedToolResults,
|
|
1599
|
+
cancelledToolCallIds: this.resumeCancelledToolCallIds
|
|
1600
|
+
},
|
|
1601
|
+
metadata: this.params.metadata,
|
|
1602
|
+
modelOptions: this.params.modelOptions
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
async applyEphemeralInterruptResume(config) {
|
|
1606
|
+
if ((config.resume?.length ?? 0) === 0) return;
|
|
1607
|
+
const interruptedRunId = this.parentRunIdOverride;
|
|
1608
|
+
if (!interruptedRunId) throw new InterruptResumeValidationError([{
|
|
1609
|
+
scope: "batch",
|
|
1610
|
+
threadId: this.threadId,
|
|
1611
|
+
interruptedRunId: this.runIdOverride ?? this.requestId,
|
|
1612
|
+
generation: 0,
|
|
1613
|
+
interruptIds: config.resume?.map((entry) => entry.interruptId) ?? [],
|
|
1614
|
+
code: "stale",
|
|
1615
|
+
message: "Interrupt continuation requires parentRunId to identify the interrupted run.",
|
|
1616
|
+
source: "server",
|
|
1617
|
+
retryable: false
|
|
1618
|
+
}]);
|
|
1619
|
+
const approvalRequests = [];
|
|
1620
|
+
const clientRequests = [];
|
|
1621
|
+
const pendingToolCalls = this.getToolCallsForEphemeralResume(config.resume);
|
|
1622
|
+
const resumeInterruptIds = new Set(config.resume?.map((entry) => entry.interruptId));
|
|
1623
|
+
const toolInputs = /* @__PURE__ */ new Map();
|
|
1624
|
+
const toolsByCallId = /* @__PURE__ */ new Map();
|
|
1625
|
+
const clientExecutionCallIds = /* @__PURE__ */ new Set();
|
|
1626
|
+
for (const toolCall of pendingToolCalls) {
|
|
1627
|
+
const tool = this.tools.find((candidate) => candidate.name === toolCall.function.name);
|
|
1628
|
+
if (!tool) continue;
|
|
1629
|
+
toolsByCallId.set(toolCall.id, tool);
|
|
1630
|
+
let input = {};
|
|
1631
|
+
try {
|
|
1632
|
+
const parsed = JSON.parse(toolCall.function.arguments.trim() || "{}");
|
|
1633
|
+
input = parsed && typeof parsed === "object" ? parsed : {};
|
|
1634
|
+
} catch {
|
|
1635
|
+
input = {};
|
|
1636
|
+
}
|
|
1637
|
+
toolInputs.set(toolCall.id, input);
|
|
1638
|
+
if (!tool.execute && resumeInterruptIds.has(`client_tool_${toolCall.id}`)) clientExecutionCallIds.add(toolCall.id);
|
|
1639
|
+
}
|
|
1640
|
+
for (const toolCall of pendingToolCalls) if (toolsByCallId.get(toolCall.id)?.needsApproval && !clientExecutionCallIds.has(toolCall.id)) approvalRequests.push({
|
|
1641
|
+
toolCallId: toolCall.id,
|
|
1642
|
+
toolName: toolCall.function.name,
|
|
1643
|
+
input: toolInputs.get(toolCall.id) ?? {},
|
|
1644
|
+
approvalId: `approval_${toolCall.id}`
|
|
1645
|
+
});
|
|
1646
|
+
for (const toolCall of pendingToolCalls) {
|
|
1647
|
+
const tool = toolsByCallId.get(toolCall.id);
|
|
1648
|
+
if (tool !== void 0 && !tool.execute && (!tool.needsApproval || clientExecutionCallIds.has(toolCall.id))) clientRequests.push({
|
|
1649
|
+
toolCallId: toolCall.id,
|
|
1650
|
+
toolName: toolCall.function.name,
|
|
1651
|
+
input: toolInputs.get(toolCall.id) ?? {}
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
const pending = this.buildActionableInterrupts(approvalRequests, clientRequests).flatMap((descriptor) => {
|
|
1655
|
+
const unopened = readUnopenedInterruptBinding(descriptor);
|
|
1656
|
+
return unopened ? [{
|
|
1657
|
+
interruptId: descriptor.id,
|
|
1658
|
+
payload: descriptor,
|
|
1659
|
+
binding: {
|
|
1660
|
+
...unopened,
|
|
1661
|
+
interruptedRunId,
|
|
1662
|
+
generation: 0
|
|
1663
|
+
}
|
|
1664
|
+
}] : [];
|
|
1665
|
+
});
|
|
1666
|
+
const validated = await validateInterruptResumeBatch({
|
|
1667
|
+
threadId: this.threadId,
|
|
1668
|
+
interruptedRunId,
|
|
1669
|
+
generation: 0,
|
|
1670
|
+
pending,
|
|
1671
|
+
resume: config.resume,
|
|
1672
|
+
tools: this.tools
|
|
1673
|
+
});
|
|
1674
|
+
if (validated.errors.length > 0 || !validated.resumeToolState) throw new InterruptResumeValidationError(validated.errors);
|
|
1675
|
+
const approvals = new Map(validated.resumeToolState.approvals);
|
|
1676
|
+
for (const request of clientRequests) if (toolsByCallId.get(request.toolCallId)?.needsApproval) approvals.set(request.toolCallId, true);
|
|
1677
|
+
this.applyResumeToolState({
|
|
1678
|
+
...validated.resumeToolState,
|
|
1679
|
+
approvals
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
applyResumeToolState(state) {
|
|
1683
|
+
if (state?.approvals) for (const [approvalId, resolution] of state.approvals) this.resumeApprovals.set(approvalId, resolution);
|
|
1684
|
+
if (state?.clientToolResults) for (const [toolCallId, result] of state.clientToolResults) this.resumeClientToolResults.set(toolCallId, result);
|
|
1685
|
+
if (state?.deniedToolResults) for (const [toolCallId, result] of state.deniedToolResults) this.resumeDeniedToolResults.set(toolCallId, result);
|
|
1686
|
+
if (state?.cancelledToolCallIds) for (const toolCallId of state.cancelledToolCallIds) this.resumeCancelledToolCallIds.add(toolCallId);
|
|
1687
|
+
}
|
|
1688
|
+
applyMiddlewareConfig(config) {
|
|
1689
|
+
this.applyResumeToolState(config.resumeToolState);
|
|
1690
|
+
this.messages = config.messages;
|
|
1691
|
+
this.systemPrompts = config.systemPrompts;
|
|
1692
|
+
this.tools = config.tools;
|
|
1693
|
+
this.params = {
|
|
1694
|
+
...this.params,
|
|
1695
|
+
metadata: config.metadata,
|
|
1696
|
+
modelOptions: config.modelOptions
|
|
1697
|
+
};
|
|
1698
|
+
this.middlewareCtx.messages = this.messages;
|
|
1699
|
+
this.middlewareCtx.systemPrompts = this.systemPrompts;
|
|
1700
|
+
this.middlewareCtx.hasTools = this.tools.length > 0;
|
|
1701
|
+
this.middlewareCtx.toolNames = this.tools.map((t) => t.name);
|
|
1702
|
+
this.middlewareCtx.modelOptions = config.modelOptions;
|
|
1703
|
+
}
|
|
1704
|
+
setToolPhase(phase) {
|
|
1705
|
+
this.toolPhase = phase;
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Pipe a single chunk through the middleware pipeline (strip-to-spec, devtools, etc.)
|
|
1709
|
+
* and yield all resulting output chunks.
|
|
1710
|
+
*/
|
|
1711
|
+
async *pipeThroughMiddleware(chunk) {
|
|
1712
|
+
const outputChunks = await this.middlewareRunner.runOnChunk(this.middlewareCtx, chunk);
|
|
1713
|
+
for (const outputChunk of outputChunks) {
|
|
1714
|
+
yield outputChunk;
|
|
1715
|
+
this.middlewareCtx.chunkIndex++;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
/**
|
|
1719
|
+
* Drain queued `sandbox.file` chunks (emitted via the SandboxRuntime sink)
|
|
1720
|
+
* through the middleware pipeline and into the public stream.
|
|
1721
|
+
*/
|
|
1722
|
+
async *drainSandboxFileQueue() {
|
|
1723
|
+
while (this.sandboxFileQueue.length > 0) {
|
|
1724
|
+
const chunk = this.sandboxFileQueue.shift();
|
|
1725
|
+
if (chunk) yield* this.pipeThroughMiddleware(chunk);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Drain an executeToolCalls async generator, yielding any CustomEvent chunks
|
|
1730
|
+
* through the middleware pipeline and returning the final ExecuteToolCallsResult.
|
|
1731
|
+
*/
|
|
1732
|
+
async *drainToolCallGenerator(generator) {
|
|
1733
|
+
let next = await generator.next();
|
|
1734
|
+
while (!next.done) {
|
|
1735
|
+
yield* this.pipeThroughMiddleware(next.value);
|
|
1736
|
+
next = await generator.next();
|
|
1737
|
+
}
|
|
1738
|
+
return next.value;
|
|
1739
|
+
}
|
|
1740
|
+
createCustomEventChunk(eventName, value) {
|
|
1741
|
+
return {
|
|
1742
|
+
type: "CUSTOM",
|
|
1743
|
+
timestamp: Date.now(),
|
|
1744
|
+
model: this.params.model,
|
|
1745
|
+
name: eventName,
|
|
1746
|
+
value
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
createId(prefix) {
|
|
1750
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
/**
|
|
1754
|
+
* Text activity - handles agentic text generation, one-shot text generation, and agentic structured output.
|
|
1755
|
+
*
|
|
1756
|
+
* This activity supports four modes:
|
|
1757
|
+
* 1. **Streaming agentic text**: Stream responses with automatic tool execution
|
|
1758
|
+
* 2. **Streaming one-shot text**: Simple streaming request/response without tools
|
|
1759
|
+
* 3. **Non-streaming text**: Returns collected text as a string (stream: false)
|
|
1760
|
+
* 4. **Agentic structured output**: Run tools, then return structured data
|
|
1761
|
+
*
|
|
1762
|
+
* @example Full agentic text (streaming with tools)
|
|
1763
|
+
* ```ts
|
|
1764
|
+
* import { chat } from '@tanstack/ai'
|
|
1765
|
+
* import { openaiText } from '@tanstack/ai-openai'
|
|
1766
|
+
*
|
|
1767
|
+
* for await (const chunk of chat({
|
|
1768
|
+
* adapter: openaiText('gpt-5.5'),
|
|
1769
|
+
* messages: [{ role: 'user', content: 'What is the weather?' }],
|
|
1770
|
+
* tools: [weatherTool]
|
|
1771
|
+
* })) {
|
|
1772
|
+
* if (chunk.type === 'TEXT_MESSAGE_CONTENT') {
|
|
1773
|
+
* console.log(chunk.delta)
|
|
1774
|
+
* }
|
|
1775
|
+
* }
|
|
1776
|
+
* ```
|
|
1777
|
+
*
|
|
1778
|
+
* @example One-shot text (streaming without tools)
|
|
1779
|
+
* ```ts
|
|
1780
|
+
* for await (const chunk of chat({
|
|
1781
|
+
* adapter: openaiText('gpt-5.5'),
|
|
1782
|
+
* messages: [{ role: 'user', content: 'Hello!' }]
|
|
1783
|
+
* })) {
|
|
1784
|
+
* console.log(chunk)
|
|
1785
|
+
* }
|
|
1786
|
+
* ```
|
|
1787
|
+
*
|
|
1788
|
+
* @example Non-streaming text (stream: false)
|
|
1789
|
+
* ```ts
|
|
1790
|
+
* const text = await chat({
|
|
1791
|
+
* adapter: openaiText('gpt-5.5'),
|
|
1792
|
+
* messages: [{ role: 'user', content: 'Hello!' }],
|
|
1793
|
+
* stream: false
|
|
1794
|
+
* })
|
|
1795
|
+
* // text is a string with the full response
|
|
1796
|
+
* ```
|
|
1797
|
+
*
|
|
1798
|
+
* @example Agentic structured output (tools + structured response)
|
|
1799
|
+
* ```ts
|
|
1800
|
+
* import { z } from 'zod'
|
|
1801
|
+
*
|
|
1802
|
+
* const result = await chat({
|
|
1803
|
+
* adapter: openaiText('gpt-5.5'),
|
|
1804
|
+
* messages: [{ role: 'user', content: 'Research and summarize the topic' }],
|
|
1805
|
+
* tools: [researchTool, analyzeTool],
|
|
1806
|
+
* outputSchema: z.object({
|
|
1807
|
+
* summary: z.string(),
|
|
1808
|
+
* keyPoints: z.array(z.string())
|
|
1809
|
+
* })
|
|
1810
|
+
* })
|
|
1811
|
+
* // result is { summary: string, keyPoints: string[] }
|
|
1812
|
+
* ```
|
|
1813
|
+
*/
|
|
1558
1814
|
function chat(options) {
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1815
|
+
validateCapabilities(options.middleware ?? [], options.adapter);
|
|
1816
|
+
const { outputSchema, stream } = options;
|
|
1817
|
+
if (outputSchema && stream === true) return runStreamingStructuredOutput({
|
|
1818
|
+
...options,
|
|
1819
|
+
outputSchema,
|
|
1820
|
+
stream
|
|
1821
|
+
});
|
|
1822
|
+
if (outputSchema) return runAgenticStructuredOutput({
|
|
1823
|
+
...options,
|
|
1824
|
+
outputSchema
|
|
1825
|
+
});
|
|
1826
|
+
if (stream === false) return runNonStreamingText({
|
|
1827
|
+
...options,
|
|
1828
|
+
outputSchema: void 0,
|
|
1829
|
+
stream
|
|
1830
|
+
});
|
|
1831
|
+
return runStreamingText({
|
|
1832
|
+
...options,
|
|
1833
|
+
outputSchema: void 0,
|
|
1834
|
+
stream
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Publish both delivery-side seams for `stream`.
|
|
1839
|
+
*
|
|
1840
|
+
* Shared by the two streaming paths so they cannot drift apart — the
|
|
1841
|
+
* structured-output path having been wired for one seam and not the other is
|
|
1842
|
+
* exactly the bug `publishRunDetachedSignal` picked up last time (a durable
|
|
1843
|
+
* `chat({ outputSchema, stream: true })` could never detach).
|
|
1844
|
+
*/
|
|
1845
|
+
function publishDeliverySeams(stream, engineRef) {
|
|
1846
|
+
publishRunDetachedSignal(stream, () => engineRef.current?.wasDetached() === true);
|
|
1847
|
+
publishRunDisconnectHandler(stream, () => {
|
|
1848
|
+
engineRef.current?.notifyDisconnected();
|
|
1849
|
+
});
|
|
1586
1850
|
}
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
},
|
|
1603
|
-
logger
|
|
1604
|
-
);
|
|
1605
|
-
try {
|
|
1606
|
-
for await (const chunk of engine.run()) {
|
|
1607
|
-
yield chunk;
|
|
1608
|
-
}
|
|
1609
|
-
} finally {
|
|
1610
|
-
await mcpManager.dispose();
|
|
1611
|
-
}
|
|
1851
|
+
/**
|
|
1852
|
+
* Run streaming text (agentic or one-shot depending on tools).
|
|
1853
|
+
*
|
|
1854
|
+
* A thin, NON-generator wrapper, because the stream object is also the key the
|
|
1855
|
+
* durable delivery sink looks the run's detach verdict up under (see
|
|
1856
|
+
* `../../delivery-detach`) and delivers its disconnect notification through (see
|
|
1857
|
+
* `../../delivery-disconnect`). A generator function cannot reach the generator it
|
|
1858
|
+
* returns, so the identity has to be minted out here and the engine reached back
|
|
1859
|
+
* through `engineRef`, which the body fills as soon as its engine exists.
|
|
1860
|
+
*/
|
|
1861
|
+
function runStreamingText(options) {
|
|
1862
|
+
const engineRef = {};
|
|
1863
|
+
const stream = streamTextChunks(options, engineRef);
|
|
1864
|
+
publishDeliverySeams(stream, engineRef);
|
|
1865
|
+
return stream;
|
|
1612
1866
|
}
|
|
1867
|
+
async function* streamTextChunks(options, engineRef) {
|
|
1868
|
+
const { adapter, middleware, context, debug, mcp, ...textOptions } = options;
|
|
1869
|
+
const model = adapter.model;
|
|
1870
|
+
const logger = resolveDebugOption(debug);
|
|
1871
|
+
const mcpManager = MCPManager.from(mcp);
|
|
1872
|
+
const mcpTools = await mcpManager.discover();
|
|
1873
|
+
if (mcpTools.length > 0) textOptions.tools = [...textOptions.tools ?? [], ...mcpTools];
|
|
1874
|
+
const engine = new TextEngine({
|
|
1875
|
+
adapter,
|
|
1876
|
+
params: {
|
|
1877
|
+
...textOptions,
|
|
1878
|
+
model,
|
|
1879
|
+
logger
|
|
1880
|
+
},
|
|
1881
|
+
middleware,
|
|
1882
|
+
context
|
|
1883
|
+
}, logger);
|
|
1884
|
+
engineRef.current = engine;
|
|
1885
|
+
try {
|
|
1886
|
+
for await (const chunk of engine.run()) yield chunk;
|
|
1887
|
+
} finally {
|
|
1888
|
+
await mcpManager.dispose();
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
/**
|
|
1892
|
+
* Run non-streaming text - collects all content and returns as a string.
|
|
1893
|
+
* Runs the full agentic loop (if tools are provided) but returns collected text.
|
|
1894
|
+
*/
|
|
1613
1895
|
function runNonStreamingText(options) {
|
|
1614
|
-
|
|
1615
|
-
// eslint-disable-next-line no-restricted-syntax -- generic-stream remap: caller is non-streaming (false), but runStreamingText is invoked internally to collect text; concrete `false`→`true` literals don't structurally overlap.
|
|
1616
|
-
options
|
|
1617
|
-
);
|
|
1618
|
-
return streamToText(stream);
|
|
1896
|
+
return streamToText(runStreamingText(options));
|
|
1619
1897
|
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Run agentic structured output:
|
|
1900
|
+
* 1. Execute the full agentic loop (with tools)
|
|
1901
|
+
* 2. Once complete, call adapter.structuredOutput with the conversation context
|
|
1902
|
+
* 3. Validate and return the structured result
|
|
1903
|
+
*/
|
|
1620
1904
|
async function runAgenticStructuredOutput(options) {
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
const finalizationError = engine.getFinalizationError();
|
|
1670
|
-
if (finalizationError) {
|
|
1671
|
-
const err = new Error(
|
|
1672
|
-
finalizationError.message,
|
|
1673
|
-
finalizationError.cause !== void 0 ? { cause: finalizationError.cause } : void 0
|
|
1674
|
-
);
|
|
1675
|
-
if (finalizationError.code !== void 0) {
|
|
1676
|
-
Object.defineProperty(err, "code", {
|
|
1677
|
-
value: finalizationError.code,
|
|
1678
|
-
enumerable: true
|
|
1679
|
-
});
|
|
1680
|
-
}
|
|
1681
|
-
throw err;
|
|
1682
|
-
}
|
|
1683
|
-
const validated = engine.getValidatedStructuredOutput();
|
|
1684
|
-
if (validated) {
|
|
1685
|
-
return validated.value;
|
|
1686
|
-
}
|
|
1687
|
-
const result = engine.getStructuredOutputResult();
|
|
1688
|
-
if (!result) {
|
|
1689
|
-
throw new Error("structured output finalization produced no result");
|
|
1690
|
-
}
|
|
1691
|
-
return result.data;
|
|
1905
|
+
const { adapter, outputSchema, middleware, context, debug, mcp, ...textOptions } = options;
|
|
1906
|
+
const model = adapter.model;
|
|
1907
|
+
const logger = resolveDebugOption(debug);
|
|
1908
|
+
if (!outputSchema) throw new Error("outputSchema is required for structured output");
|
|
1909
|
+
const { jsonSchema, nullWideningMap } = convertSchemaForStructuredOutput(outputSchema);
|
|
1910
|
+
if (!jsonSchema) throw new Error("Failed to convert output schema to JSON Schema");
|
|
1911
|
+
const normalize = (data) => undoNullWidening(data, nullWideningMap);
|
|
1912
|
+
const validate = isStandardSchema(outputSchema) ? (data) => parseWithStandardSchema(outputSchema, data) : void 0;
|
|
1913
|
+
const nativeCombined = adapter.supportsCombinedToolsAndSchema?.(options.modelOptions) === true;
|
|
1914
|
+
const mcpManager = MCPManager.from(mcp);
|
|
1915
|
+
const mcpTools = await mcpManager.discover();
|
|
1916
|
+
if (mcpTools.length > 0) textOptions.tools = [...textOptions.tools ?? [], ...mcpTools];
|
|
1917
|
+
const engine = new TextEngine({
|
|
1918
|
+
adapter,
|
|
1919
|
+
params: {
|
|
1920
|
+
...textOptions,
|
|
1921
|
+
model,
|
|
1922
|
+
logger
|
|
1923
|
+
},
|
|
1924
|
+
middleware,
|
|
1925
|
+
context,
|
|
1926
|
+
finalStructuredOutput: {
|
|
1927
|
+
jsonSchema,
|
|
1928
|
+
yieldChunks: false,
|
|
1929
|
+
normalize,
|
|
1930
|
+
...validate ? { validate } : {},
|
|
1931
|
+
...nativeCombined ? { nativeCombined: true } : {}
|
|
1932
|
+
}
|
|
1933
|
+
}, logger);
|
|
1934
|
+
try {
|
|
1935
|
+
for await (const _chunk of engine.run());
|
|
1936
|
+
} finally {
|
|
1937
|
+
await mcpManager.dispose();
|
|
1938
|
+
}
|
|
1939
|
+
const finalizationError = engine.getFinalizationError();
|
|
1940
|
+
if (finalizationError) {
|
|
1941
|
+
const err = new Error(finalizationError.message, finalizationError.cause !== void 0 ? { cause: finalizationError.cause } : void 0);
|
|
1942
|
+
if (finalizationError.code !== void 0) Object.defineProperty(err, "code", {
|
|
1943
|
+
value: finalizationError.code,
|
|
1944
|
+
enumerable: true
|
|
1945
|
+
});
|
|
1946
|
+
throw err;
|
|
1947
|
+
}
|
|
1948
|
+
const validated = engine.getValidatedStructuredOutput();
|
|
1949
|
+
if (validated) return validated.value;
|
|
1950
|
+
const result = engine.getStructuredOutputResult();
|
|
1951
|
+
if (!result) throw new Error("structured output finalization produced no result");
|
|
1952
|
+
return result.data;
|
|
1692
1953
|
}
|
|
1954
|
+
/**
|
|
1955
|
+
* Parse the `value` payload of a `structured-output.complete` CUSTOM event
|
|
1956
|
+
* into a typed shape, returning `null` if the runtime payload doesn't match.
|
|
1957
|
+
*
|
|
1958
|
+
* Uses an `unknown`-input runtime check rather than `as` casts so the engine
|
|
1959
|
+
* stays cast-free in its hot path.
|
|
1960
|
+
*/
|
|
1693
1961
|
function readStructuredOutputCompleteValue(value) {
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1962
|
+
if (typeof value !== "object" || value === null) return null;
|
|
1963
|
+
if (!("object" in value) || !("raw" in value)) return null;
|
|
1964
|
+
const raw = value.raw;
|
|
1965
|
+
if (typeof raw !== "string") return null;
|
|
1966
|
+
const reasoningField = value.reasoning;
|
|
1967
|
+
const reasoning = typeof reasoningField === "string" ? reasoningField : void 0;
|
|
1968
|
+
return {
|
|
1969
|
+
object: value.object,
|
|
1970
|
+
raw,
|
|
1971
|
+
...reasoning !== void 0 ? { reasoning } : {}
|
|
1972
|
+
};
|
|
1705
1973
|
}
|
|
1974
|
+
/**
|
|
1975
|
+
* Synthesize a streaming structured-output stream by wrapping a non-streaming
|
|
1976
|
+
* `structuredOutput` call. Used when an adapter doesn't implement
|
|
1977
|
+
* `structuredOutputStream` natively.
|
|
1978
|
+
*
|
|
1979
|
+
* `onAdapterError`, when provided, is invoked with the raw error from
|
|
1980
|
+
* `adapter.structuredOutput` before the synthesized RUN_ERROR is yielded.
|
|
1981
|
+
* The engine uses this to preserve the original error (stack, cause, custom
|
|
1982
|
+
* properties like provider `status`/`code`) as `finalizationError.cause`,
|
|
1983
|
+
* because the RUN_ERROR wire shape only carries `message` and `code`.
|
|
1984
|
+
*/
|
|
1706
1985
|
async function* fallbackStructuredOutputStream(adapter, options, onAdapterError) {
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
...result.usage ? { usage: result.usage } : {}
|
|
1778
|
-
};
|
|
1986
|
+
const { chatOptions } = options;
|
|
1987
|
+
const fallbackRand = Math.random().toString(36).slice(2);
|
|
1988
|
+
const runId = chatOptions.runId ?? `fallback-${Date.now()}-${fallbackRand}`;
|
|
1989
|
+
const threadId = chatOptions.threadId ?? `fallback-${Date.now()}-${fallbackRand}`;
|
|
1990
|
+
const messageId = `fallback-${Date.now()}-${fallbackRand}`;
|
|
1991
|
+
const model = chatOptions.model;
|
|
1992
|
+
const timestamp = Date.now();
|
|
1993
|
+
yield {
|
|
1994
|
+
type: EventType.RUN_STARTED,
|
|
1995
|
+
runId,
|
|
1996
|
+
threadId,
|
|
1997
|
+
model,
|
|
1998
|
+
timestamp
|
|
1999
|
+
};
|
|
2000
|
+
let result;
|
|
2001
|
+
try {
|
|
2002
|
+
result = await adapter.structuredOutput(options);
|
|
2003
|
+
} catch (error) {
|
|
2004
|
+
onAdapterError?.(error);
|
|
2005
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2006
|
+
yield {
|
|
2007
|
+
type: EventType.RUN_ERROR,
|
|
2008
|
+
runId,
|
|
2009
|
+
threadId,
|
|
2010
|
+
model,
|
|
2011
|
+
timestamp,
|
|
2012
|
+
message,
|
|
2013
|
+
error: { message }
|
|
2014
|
+
};
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
yield {
|
|
2018
|
+
type: EventType.TEXT_MESSAGE_START,
|
|
2019
|
+
messageId,
|
|
2020
|
+
role: "assistant",
|
|
2021
|
+
model,
|
|
2022
|
+
timestamp
|
|
2023
|
+
};
|
|
2024
|
+
yield {
|
|
2025
|
+
type: EventType.TEXT_MESSAGE_CONTENT,
|
|
2026
|
+
messageId,
|
|
2027
|
+
delta: result.rawText,
|
|
2028
|
+
model,
|
|
2029
|
+
timestamp
|
|
2030
|
+
};
|
|
2031
|
+
yield {
|
|
2032
|
+
type: EventType.TEXT_MESSAGE_END,
|
|
2033
|
+
messageId,
|
|
2034
|
+
model,
|
|
2035
|
+
timestamp
|
|
2036
|
+
};
|
|
2037
|
+
yield {
|
|
2038
|
+
type: EventType.CUSTOM,
|
|
2039
|
+
name: "structured-output.complete",
|
|
2040
|
+
value: {
|
|
2041
|
+
object: result.data,
|
|
2042
|
+
raw: result.rawText
|
|
2043
|
+
},
|
|
2044
|
+
model,
|
|
2045
|
+
timestamp
|
|
2046
|
+
};
|
|
2047
|
+
yield {
|
|
2048
|
+
type: EventType.RUN_FINISHED,
|
|
2049
|
+
runId,
|
|
2050
|
+
threadId,
|
|
2051
|
+
model,
|
|
2052
|
+
timestamp,
|
|
2053
|
+
finishReason: "stop",
|
|
2054
|
+
...result.usage ? { usage: result.usage } : {}
|
|
2055
|
+
};
|
|
1779
2056
|
}
|
|
2057
|
+
/**
|
|
2058
|
+
* Run streaming structured output via the TextEngine, with the engine's
|
|
2059
|
+
* `finalStructuredOutput.yieldChunks: true` mode. The agent loop's
|
|
2060
|
+
* RUN_STARTED/RUN_FINISHED are suppressed; the structured-output finalization
|
|
2061
|
+
* step's pair brackets the run for the consumer.
|
|
2062
|
+
*
|
|
2063
|
+
* Standard Schema *validation* is intentionally NOT run on this path — it is
|
|
2064
|
+
* the consumer's responsibility. This is a deliberate asymmetry vs.
|
|
2065
|
+
* `runAgenticStructuredOutput` (Promise<T> path), which DOES validate inside
|
|
2066
|
+
* the engine and routes validation failures through `onError`. The reason:
|
|
2067
|
+
* streaming consumers typically render partial JSON progressively (via
|
|
2068
|
+
* `parsePartialJSON` or `useChat`'s `partial` slot) and validate downstream
|
|
2069
|
+
* after assembly. Running validation server-side would force a hard error
|
|
2070
|
+
* on partial-by-design payloads. See `docs/structured-outputs/overview.md`.
|
|
2071
|
+
*
|
|
2072
|
+
* Null-widening normalization, however, IS run on both paths: the
|
|
2073
|
+
* `structured-output.complete` CUSTOM event is forwarded with its `value.object`
|
|
2074
|
+
* already un-widened (synthesized strict-mode nulls dropped, genuine
|
|
2075
|
+
* `.nullable()` nulls kept), so a consumer validating the assembled object
|
|
2076
|
+
* against the original schema doesn't choke on a `null` for an `.optional()`
|
|
2077
|
+
* field. Same `convertSchemaForStructuredOutput` pass and same
|
|
2078
|
+
* `undoNullWidening` map as the Promise<T> path — the two must not diverge.
|
|
2079
|
+
*
|
|
2080
|
+
* Pre-flight validation (missing schema, unconvertible schema) throws
|
|
2081
|
+
* synchronously at call time rather than as a yielded RUN_ERROR mid-stream —
|
|
2082
|
+
* those are programmer errors, not runtime conditions.
|
|
2083
|
+
*/
|
|
1780
2084
|
function runStreamingStructuredOutput(options) {
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
return runStreamingStructuredOutputImpl(
|
|
1791
|
-
options,
|
|
1792
|
-
jsonSchema,
|
|
1793
|
-
normalize
|
|
1794
|
-
);
|
|
2085
|
+
const { outputSchema } = options;
|
|
2086
|
+
if (!outputSchema) throw new Error("outputSchema is required for streaming structured output");
|
|
2087
|
+
const { jsonSchema, nullWideningMap } = convertSchemaForStructuredOutput(outputSchema);
|
|
2088
|
+
if (!jsonSchema) throw new Error("Failed to convert output schema to JSON Schema");
|
|
2089
|
+
const normalize = (data) => undoNullWidening(data, nullWideningMap);
|
|
2090
|
+
const engineRef = {};
|
|
2091
|
+
const stream = runStreamingStructuredOutputImpl(options, jsonSchema, normalize, engineRef);
|
|
2092
|
+
publishDeliverySeams(stream, engineRef);
|
|
2093
|
+
return stream;
|
|
1795
2094
|
}
|
|
1796
|
-
async function* runStreamingStructuredOutputImpl(options, jsonSchema, normalize) {
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
},
|
|
1827
|
-
logger
|
|
1828
|
-
);
|
|
1829
|
-
try {
|
|
1830
|
-
for await (const chunk of engine.run()) {
|
|
1831
|
-
yield chunk;
|
|
1832
|
-
}
|
|
1833
|
-
} finally {
|
|
1834
|
-
await mcpManager.dispose();
|
|
1835
|
-
}
|
|
2095
|
+
async function* runStreamingStructuredOutputImpl(options, jsonSchema, normalize, engineRef) {
|
|
2096
|
+
const { adapter, outputSchema, middleware, context, debug, mcp, ...textOptions } = options;
|
|
2097
|
+
const model = adapter.model;
|
|
2098
|
+
const logger = resolveDebugOption(debug);
|
|
2099
|
+
const nativeCombined = adapter.supportsCombinedToolsAndSchema?.(options.modelOptions) === true;
|
|
2100
|
+
const mcpManager = MCPManager.from(mcp);
|
|
2101
|
+
const mcpTools = await mcpManager.discover();
|
|
2102
|
+
if (mcpTools.length > 0) textOptions.tools = [...textOptions.tools ?? [], ...mcpTools];
|
|
2103
|
+
const engine = new TextEngine({
|
|
2104
|
+
adapter,
|
|
2105
|
+
params: {
|
|
2106
|
+
...textOptions,
|
|
2107
|
+
model,
|
|
2108
|
+
logger
|
|
2109
|
+
},
|
|
2110
|
+
middleware,
|
|
2111
|
+
context,
|
|
2112
|
+
finalStructuredOutput: {
|
|
2113
|
+
jsonSchema,
|
|
2114
|
+
yieldChunks: true,
|
|
2115
|
+
normalize,
|
|
2116
|
+
...nativeCombined ? { nativeCombined: true } : {}
|
|
2117
|
+
}
|
|
2118
|
+
}, logger);
|
|
2119
|
+
engineRef.current = engine;
|
|
2120
|
+
try {
|
|
2121
|
+
for await (const chunk of engine.run()) yield chunk;
|
|
2122
|
+
} finally {
|
|
2123
|
+
await mcpManager.dispose();
|
|
2124
|
+
}
|
|
1836
2125
|
}
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
};
|
|
1842
|
-
//# sourceMappingURL=index.js.map
|
|
2126
|
+
//#endregion
|
|
2127
|
+
export { chat, createChatOptions, kind };
|
|
2128
|
+
|
|
2129
|
+
//# sourceMappingURL=index.js.map
|