@superdurable/dex-agents 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +1981 -2
  3. package/dist/src/agent-built-in-tools.d.ts +81 -0
  4. package/dist/src/agent-built-in-tools.js +246 -0
  5. package/dist/src/agent-built-in-tools.js.map +1 -0
  6. package/dist/src/agent-context-compaction.d.ts +108 -0
  7. package/dist/src/agent-context-compaction.js +39 -0
  8. package/dist/src/agent-context-compaction.js.map +1 -0
  9. package/dist/src/agent-event-writer.d.ts +26 -0
  10. package/dist/src/agent-event-writer.js +110 -0
  11. package/dist/src/agent-event-writer.js.map +1 -0
  12. package/dist/src/agent-framework-runner.d.ts +808 -0
  13. package/dist/src/agent-framework-runner.js +82 -0
  14. package/dist/src/agent-framework-runner.js.map +1 -0
  15. package/dist/src/agent-plans.d.ts +48 -0
  16. package/dist/src/agent-plans.js +86 -0
  17. package/dist/src/agent-plans.js.map +1 -0
  18. package/dist/src/agent-session-cancellation-steps.d.ts +28 -0
  19. package/dist/src/agent-session-cancellation-steps.js +154 -0
  20. package/dist/src/agent-session-cancellation-steps.js.map +1 -0
  21. package/dist/src/agent-session-codecs.d.ts +105 -0
  22. package/dist/src/agent-session-codecs.js +689 -0
  23. package/dist/src/agent-session-codecs.js.map +1 -0
  24. package/dist/src/agent-session-flow.d.ts +416 -0
  25. package/dist/src/agent-session-flow.js +623 -0
  26. package/dist/src/agent-session-flow.js.map +1 -0
  27. package/dist/src/agent-session-persistence.d.ts +133 -0
  28. package/dist/src/agent-session-persistence.js +74 -0
  29. package/dist/src/agent-session-persistence.js.map +1 -0
  30. package/dist/src/agent-session-settings.d.ts +58 -0
  31. package/dist/src/agent-session-settings.js +81 -0
  32. package/dist/src/agent-session-settings.js.map +1 -0
  33. package/dist/src/agent-session-step-options.d.ts +27 -0
  34. package/dist/src/agent-session-step-options.js +39 -0
  35. package/dist/src/agent-session-step-options.js.map +1 -0
  36. package/dist/src/agent-session-step-types.d.ts +20 -0
  37. package/dist/src/agent-session-step-types.js +28 -0
  38. package/dist/src/agent-session-step-types.js.map +1 -0
  39. package/dist/src/agent-session-steps.d.ts +179 -0
  40. package/dist/src/agent-session-steps.js +1272 -0
  41. package/dist/src/agent-session-steps.js.map +1 -0
  42. package/dist/src/agent-session-tracer.d.ts +61 -0
  43. package/dist/src/agent-session-tracer.js +608 -0
  44. package/dist/src/agent-session-tracer.js.map +1 -0
  45. package/dist/src/agent-session-transitions.d.ts +224 -0
  46. package/dist/src/agent-session-transitions.js +850 -0
  47. package/dist/src/agent-session-transitions.js.map +1 -0
  48. package/dist/src/agent-session-values.d.ts +998 -0
  49. package/dist/src/agent-session-values.js +162 -0
  50. package/dist/src/agent-session-values.js.map +1 -0
  51. package/dist/src/agent-session-wait-steps.d.ts +53 -0
  52. package/dist/src/agent-session-wait-steps.js +389 -0
  53. package/dist/src/agent-session-wait-steps.js.map +1 -0
  54. package/dist/src/agent-tracing-options.d.ts +107 -0
  55. package/dist/src/agent-tracing-options.js +20 -0
  56. package/dist/src/agent-tracing-options.js.map +1 -0
  57. package/dist/src/ai-sdk/ai-sdk-model-client.d.ts +40 -0
  58. package/dist/src/ai-sdk/ai-sdk-model-client.js +235 -0
  59. package/dist/src/ai-sdk/ai-sdk-model-client.js.map +1 -0
  60. package/dist/src/ai-sdk/index.d.ts +1 -0
  61. package/dist/src/ai-sdk/index.js +9 -0
  62. package/dist/src/ai-sdk/index.js.map +1 -0
  63. package/dist/src/index.d.ts +10 -0
  64. package/dist/src/index.js +17 -0
  65. package/dist/src/index.js.map +1 -0
  66. package/dist/src/mcp/index.d.ts +3 -0
  67. package/dist/src/mcp/index.js +10 -0
  68. package/dist/src/mcp/index.js.map +1 -0
  69. package/dist/src/mcp/mcp-resource-and-prompt-tools.d.ts +29 -0
  70. package/dist/src/mcp/mcp-resource-and-prompt-tools.js +185 -0
  71. package/dist/src/mcp/mcp-resource-and-prompt-tools.js.map +1 -0
  72. package/dist/src/mcp/mcp-server-config.d.ts +86 -0
  73. package/dist/src/mcp/mcp-server-config.js +38 -0
  74. package/dist/src/mcp/mcp-server-config.js.map +1 -0
  75. package/dist/src/mcp/mcp-tool-source.d.ts +470 -0
  76. package/dist/src/mcp/mcp-tool-source.js +1204 -0
  77. package/dist/src/mcp/mcp-tool-source.js.map +1 -0
  78. package/dist/src/stepwise/agent-definition.d.ts +158 -0
  79. package/dist/src/stepwise/agent-definition.js +9 -0
  80. package/dist/src/stepwise/agent-definition.js.map +1 -0
  81. package/dist/src/stepwise/function-tool.d.ts +130 -0
  82. package/dist/src/stepwise/function-tool.js +75 -0
  83. package/dist/src/stepwise/function-tool.js.map +1 -0
  84. package/dist/src/stepwise/index.d.ts +7 -0
  85. package/dist/src/stepwise/index.js +12 -0
  86. package/dist/src/stepwise/index.js.map +1 -0
  87. package/dist/src/stepwise/json-schema-validation.d.ts +9 -0
  88. package/dist/src/stepwise/json-schema-validation.js +141 -0
  89. package/dist/src/stepwise/json-schema-validation.js.map +1 -0
  90. package/dist/src/stepwise/model-client.d.ts +194 -0
  91. package/dist/src/stepwise/model-client.js +9 -0
  92. package/dist/src/stepwise/model-client.js.map +1 -0
  93. package/dist/src/stepwise/scripted-model-client.d.ts +99 -0
  94. package/dist/src/stepwise/scripted-model-client.js +156 -0
  95. package/dist/src/stepwise/scripted-model-client.js.map +1 -0
  96. package/dist/src/stepwise/stepwise-agent-runner.d.ts +189 -0
  97. package/dist/src/stepwise/stepwise-agent-runner.js +804 -0
  98. package/dist/src/stepwise/stepwise-agent-runner.js.map +1 -0
  99. package/dist/src/stepwise/stepwise-agent-session-flow.d.ts +35 -0
  100. package/dist/src/stepwise/stepwise-agent-session-flow.js +56 -0
  101. package/dist/src/stepwise/stepwise-agent-session-flow.js.map +1 -0
  102. package/dist/src/stepwise/stepwise-input-items.d.ts +12 -0
  103. package/dist/src/stepwise/stepwise-input-items.js +172 -0
  104. package/dist/src/stepwise/stepwise-input-items.js.map +1 -0
  105. package/dist/src/stepwise/tool-source.d.ts +103 -0
  106. package/dist/src/stepwise/tool-source.js +9 -0
  107. package/dist/src/stepwise/tool-source.js.map +1 -0
  108. package/dist/src/testing/agent-session-test-client.d.ts +205 -0
  109. package/dist/src/testing/agent-session-test-client.js +290 -0
  110. package/dist/src/testing/agent-session-test-client.js.map +1 -0
  111. package/dist/src/testing/agent-test-environment.d.ts +52 -0
  112. package/dist/src/testing/agent-test-environment.js +86 -0
  113. package/dist/src/testing/agent-test-environment.js.map +1 -0
  114. package/dist/src/testing/agent-worker-process.d.ts +91 -0
  115. package/dist/src/testing/agent-worker-process.js +150 -0
  116. package/dist/src/testing/agent-worker-process.js.map +1 -0
  117. package/dist/src/testing/child-process-group.d.ts +33 -0
  118. package/dist/src/testing/child-process-group.js +164 -0
  119. package/dist/src/testing/child-process-group.js.map +1 -0
  120. package/dist/src/testing/coordination.d.ts +99 -0
  121. package/dist/src/testing/coordination.js +155 -0
  122. package/dist/src/testing/coordination.js.map +1 -0
  123. package/dist/src/testing/index.d.ts +8 -0
  124. package/dist/src/testing/index.js +16 -0
  125. package/dist/src/testing/index.js.map +1 -0
  126. package/dist/src/testing/polling.d.ts +31 -0
  127. package/dist/src/testing/polling.js +51 -0
  128. package/dist/src/testing/polling.js.map +1 -0
  129. package/dist/src/testing/private-dex-engine.d.ts +101 -0
  130. package/dist/src/testing/private-dex-engine.js +223 -0
  131. package/dist/src/testing/private-dex-engine.js.map +1 -0
  132. package/dist/src/testing/scripted-model-client.d.ts +1 -0
  133. package/dist/src/testing/scripted-model-client.js +10 -0
  134. package/dist/src/testing/scripted-model-client.js.map +1 -0
  135. package/dist/src/testing/step-history.d.ts +40 -0
  136. package/dist/src/testing/step-history.js +59 -0
  137. package/dist/src/testing/step-history.js.map +1 -0
  138. package/dist/src/ui-stream/agent-event-json.d.ts +30 -0
  139. package/dist/src/ui-stream/agent-event-json.js +64 -0
  140. package/dist/src/ui-stream/agent-event-json.js.map +1 -0
  141. package/dist/src/ui-stream/agent-event-sse.d.ts +154 -0
  142. package/dist/src/ui-stream/agent-event-sse.js +159 -0
  143. package/dist/src/ui-stream/agent-event-sse.js.map +1 -0
  144. package/dist/src/ui-stream/agent-event-stream-positions.d.ts +42 -0
  145. package/dist/src/ui-stream/agent-event-stream-positions.js +153 -0
  146. package/dist/src/ui-stream/agent-event-stream-positions.js.map +1 -0
  147. package/dist/src/ui-stream/agent-session-record-reader.d.ts +72 -0
  148. package/dist/src/ui-stream/agent-session-record-reader.js +241 -0
  149. package/dist/src/ui-stream/agent-session-record-reader.js.map +1 -0
  150. package/dist/src/ui-stream/agent-ui-message-stream-responses.d.ts +219 -0
  151. package/dist/src/ui-stream/agent-ui-message-stream-responses.js +440 -0
  152. package/dist/src/ui-stream/agent-ui-message-stream-responses.js.map +1 -0
  153. package/dist/src/ui-stream/agent-ui-message-stream.d.ts +138 -0
  154. package/dist/src/ui-stream/agent-ui-message-stream.js +681 -0
  155. package/dist/src/ui-stream/agent-ui-message-stream.js.map +1 -0
  156. package/dist/src/ui-stream/agent-ui-message-types.d.ts +238 -0
  157. package/dist/src/ui-stream/agent-ui-message-types.js +57 -0
  158. package/dist/src/ui-stream/agent-ui-message-types.js.map +1 -0
  159. package/dist/src/ui-stream/agent-ui-messages.d.ts +66 -0
  160. package/dist/src/ui-stream/agent-ui-messages.js +220 -0
  161. package/dist/src/ui-stream/agent-ui-messages.js.map +1 -0
  162. package/dist/src/ui-stream/agent-ui-stream-cursor.d.ts +64 -0
  163. package/dist/src/ui-stream/agent-ui-stream-cursor.js +186 -0
  164. package/dist/src/ui-stream/agent-ui-stream-cursor.js.map +1 -0
  165. package/dist/src/ui-stream/index.d.ts +14 -0
  166. package/dist/src/ui-stream/index.js +22 -0
  167. package/dist/src/ui-stream/index.js.map +1 -0
  168. package/dist/src/ui-stream/server-sent-event-responses.d.ts +6 -0
  169. package/dist/src/ui-stream/server-sent-event-responses.js +34 -0
  170. package/dist/src/ui-stream/server-sent-event-responses.js.map +1 -0
  171. package/package.json +86 -4
@@ -0,0 +1,804 @@
1
+ // Copyright (c) 2026 Super Durable, Inc.
2
+ //
3
+ // Licensed under the Sustainable Use License 1.0.
4
+ // You may not use this file except in compliance with the License.
5
+ // See the LICENSE file in the repository root.
6
+ //
7
+ // SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0
8
+ import { builtInToolDefinitions } from "../agent-built-in-tools.js";
9
+ import { ToolExecutionError, } from "../agent-framework-runner.js";
10
+ import { buildPlanNote } from "../agent-plans.js";
11
+ import { describeToolCallOutcome, } from "../agent-session-values.js";
12
+ import { findJsonSchemaViolation, serializeJsonWithSpacedSeparators } from "./json-schema-validation.js";
13
+ import { pairToolCallsWithResults, readStoredModelInputItem, toStepwiseFrameworkMessage, } from "./stepwise-input-items.js";
14
+ const toolSourceMethods = ["listTools", "deferredToolCall", "executeDeferredToolCall", "pollToolTask"];
15
+ // The runner's envelope around a source's toolSourceReference: the building source's name.
16
+ const toolSourceNameKey = "stepwise_tool_source";
17
+ // An unlisted name never reaches a source, so it fails without asking anyone to approve it.
18
+ const unlistedSourceToolPolicy = { effect: "unclassified", requiresApproval: false };
19
+ const toolSourceReferenceKey = "reference";
20
+ /** Default {@link AgentDefinition.toolSourceListingTimeoutMs}: 10 seconds. */
21
+ export const defaultToolSourceListingTimeoutMs = 10_000;
22
+ export const defaultMaxModelCallsPerTurn = 100;
23
+ /** First line of the user text that carries the {@link ContextSummary} into the model input. */
24
+ export const contextSummaryNoteHeader = "[Dex] Summary of the earlier conversation:";
25
+ /** Instructions of the model call that writes a {@link ContextSummary}. */
26
+ export const contextSummaryInstructions = "You summarize the earlier part of an agent conversation. The agent continues from your " +
27
+ "summary and the messages after it, so keep every fact, decision, user request and " +
28
+ "preference, open task, and tool result that it still needs, including what an earlier " +
29
+ "summary says. Write plain text.";
30
+ /** Last user text of the model call that writes a {@link ContextSummary}. */
31
+ export const contextSummaryRequest = "[Dex] Summarize the conversation above. Reply with the summary only.";
32
+ /** Result of a tool call the model made after the turn's model call limit; the same text as the Python core. */
33
+ export function buildToolCallLimitReachedResult(maxModelCallsPerTurn) {
34
+ return `The tool call was not run: this turn reached its limit of ${maxModelCallsPerTurn} model calls.`;
35
+ }
36
+ /**
37
+ * Returns the user text that says how many of the oldest messages a summary request left out.
38
+ * @param omittedCount - Messages left out, at least 1.
39
+ * @returns `[Dex] <omittedCount> earlier messages did not fit in this request and are left out.`, or
40
+ * `[Dex] 1 earlier message did not fit in this request and is left out.` for one message.
41
+ */
42
+ export function buildContextSummaryOmissionNote(omittedCount) {
43
+ if (omittedCount === 1) {
44
+ return "[Dex] 1 earlier message did not fit in this request and is left out.";
45
+ }
46
+ return `[Dex] ${omittedCount} earlier messages did not fit in this request and are left out.`;
47
+ }
48
+ /**
49
+ * Returns the user text that puts a summary before the newer messages in a model input.
50
+ * @param summary - The session's summary.
51
+ * @returns {@link contextSummaryNoteHeader}, a newline, and the summary text.
52
+ */
53
+ export function buildContextSummaryNote(summary) {
54
+ return `${contextSummaryNoteHeader}\n${summary.text}`;
55
+ }
56
+ // The summary is not the assistant's answer, so its live text goes nowhere.
57
+ const discardedModelOutput = {
58
+ writeTextDelta: () => undefined,
59
+ writeReasoningDelta: () => undefined,
60
+ };
61
+ /**
62
+ * The {@link AgentFrameworkRunner} of the built-in Stepwise loop.
63
+ *
64
+ * {@link createStepwiseAgentSessionFlow} creates one; construct it directly only to pass it to
65
+ * `createAgentSessionFlow` with options of your own. Each `RunModel` makes at most one model call
66
+ * through {@link AgentDefinition.model}, from committed state only: the stored messages, the results
67
+ * of the previous model response from the turn's final records, and the instructions. The runner
68
+ * stores the turn's transcript as it goes through {@link ToolCallsRequested.newMessages}, so a long turn
69
+ * never rewrites a growing run state. It supports plan mode: a `planning` turn offers only `write_todos`, and
70
+ * every model call shows the plan with {@link buildPlanNote} before the turn's messages. It implements the
71
+ * compaction methods, so a session with a compaction policy starts the model input with the summary. Its
72
+ * {@link pollToolTask} passes a tool task to the {@link ToolSource} that started it; function tools start no
73
+ * tasks. Its {@link cancelToolTask} passes a task that `cancel_turn` abandoned to that source's optional
74
+ * `cancelToolTask`.
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * const runner = new StepwiseAgentRunner(definition);
79
+ * const flow = createAgentSessionFlow(runner, "SupportAgent", { builtInTools: ["ask_user"] });
80
+ * ```
81
+ */
82
+ export class StepwiseAgentRunner {
83
+ /** The agent definition, frozen at construction. */
84
+ definition;
85
+ /** The runner honors {@link AgentTurnContext.interactionMode}, so plan mode can be enabled. */
86
+ supportsPlanMode = true;
87
+ functionToolsByName;
88
+ toolSources;
89
+ toolSourcesByName;
90
+ maxModelCallsPerTurn;
91
+ toolSourceListingTimeoutMs;
92
+ /** The last listing of each source, so estimates count its tools without listing again. */
93
+ lastListings = new Map();
94
+ /**
95
+ * Creates the runner and checks the definition.
96
+ * @param definition - The agent definition.
97
+ * @throws {TypeError} When the name, instructions, model, or tools are invalid, two tools share a
98
+ * name, a function tool uses the name of an enabled built-in tool, `planModeEnabled` is set without
99
+ * `write_todos`, `planningToolChoice` is neither `auto` nor `required`, or a tool source lacks a
100
+ * {@link ToolSource} method.
101
+ * @throws {RangeError} When `maxModelCallsPerTurn` is not a positive integer.
102
+ */
103
+ constructor(definition) {
104
+ if (typeof definition?.name !== "string" || definition.name.trim() === "") {
105
+ throw new TypeError("AgentDefinition.name must be a non-blank string");
106
+ }
107
+ if (typeof definition.instructions !== "string" && typeof definition.instructions !== "function") {
108
+ throw new TypeError("AgentDefinition.instructions must be a string or a function");
109
+ }
110
+ if (typeof definition.model?.callModel !== "function" || typeof definition.model.estimateInputTokens !== "function") {
111
+ throw new TypeError("AgentDefinition.model must be a ModelClient");
112
+ }
113
+ if (definition.resolveModel !== undefined && typeof definition.resolveModel !== "function") {
114
+ throw new TypeError("AgentDefinition.resolveModel must be a function");
115
+ }
116
+ const enabledBuiltInTools = new Set(definition.builtInTools ?? []);
117
+ const functionToolsByName = new Map();
118
+ for (const tool of definition.tools ?? []) {
119
+ if (tool?.kind !== "function_tool") {
120
+ throw new TypeError("AgentDefinition.tools must hold tools created by functionTool");
121
+ }
122
+ if (functionToolsByName.has(tool.name)) {
123
+ throw new TypeError(`two tools of agent ${definition.name} are named ${tool.name}`);
124
+ }
125
+ if (enabledBuiltInTools.has(tool.name)) {
126
+ throw new TypeError(`tool ${tool.name} uses the name of an enabled built-in tool`);
127
+ }
128
+ functionToolsByName.set(tool.name, tool);
129
+ }
130
+ const toolSources = [...(definition.toolSources ?? [])];
131
+ for (const source of toolSources) {
132
+ const missing = toolSourceMethods.find((method) => typeof source?.[method] !== "function");
133
+ if (missing !== undefined) {
134
+ throw new TypeError(`tool source ${String(source)} has no ${missing} method`);
135
+ }
136
+ }
137
+ const toolSourcesByName = new Map();
138
+ for (const source of toolSources) {
139
+ if (typeof source.sourceName !== "string" || source.sourceName.trim() === "") {
140
+ throw new TypeError(`tool source ${String(source)} needs a non-blank sourceName`);
141
+ }
142
+ if (toolSourcesByName.has(source.sourceName)) {
143
+ throw new TypeError(`two tool sources are named ${source.sourceName}`);
144
+ }
145
+ toolSourcesByName.set(source.sourceName, source);
146
+ }
147
+ refuseDeclaredToolNameCollisions(toolSources, new Set(functionToolsByName.keys()), enabledBuiltInTools);
148
+ const toolSourceListingTimeoutMs = definition.toolSourceListingTimeoutMs ?? defaultToolSourceListingTimeoutMs;
149
+ if (!(toolSourceListingTimeoutMs > 0)) {
150
+ throw new RangeError("AgentDefinition.toolSourceListingTimeoutMs must be positive");
151
+ }
152
+ if (definition.planModeEnabled === true && !enabledBuiltInTools.has("write_todos")) {
153
+ throw new TypeError("planModeEnabled requires the write_todos built-in tool");
154
+ }
155
+ const planningToolChoice = definition.planningToolChoice ?? "auto";
156
+ if (planningToolChoice !== "auto" && planningToolChoice !== "required") {
157
+ throw new TypeError(`planningToolChoice must be "auto" or "required", not ${String(planningToolChoice)}`);
158
+ }
159
+ const maxModelCallsPerTurn = definition.maxModelCallsPerTurn ?? defaultMaxModelCallsPerTurn;
160
+ if (!Number.isSafeInteger(maxModelCallsPerTurn) || maxModelCallsPerTurn <= 0) {
161
+ throw new RangeError("AgentDefinition.maxModelCallsPerTurn must be a positive integer");
162
+ }
163
+ this.definition = Object.freeze({ ...definition });
164
+ this.functionToolsByName = functionToolsByName;
165
+ this.toolSources = toolSources;
166
+ this.toolSourcesByName = toolSourcesByName;
167
+ this.toolSourceListingTimeoutMs = toolSourceListingTimeoutMs;
168
+ this.maxModelCallsPerTurn = maxModelCallsPerTurn;
169
+ }
170
+ /**
171
+ * Converts user text into a stored `user_text` item.
172
+ * @param text - User message text.
173
+ * @returns The message with role `user`.
174
+ */
175
+ buildUserMessage(text) {
176
+ return toStepwiseFrameworkMessage({ type: "user_text", text });
177
+ }
178
+ /**
179
+ * Makes the turn's next model call.
180
+ * @param turn - The turn context.
181
+ * @returns {@link TurnCompleted} with the results and final answer not stored yet, or
182
+ * {@link ToolCallsRequested} with the previous results, this response, and its tool calls.
183
+ * @throws {TypeError} When a function tool uses the name of an enabled built-in tool, or a tool source lists a
184
+ * name that repeats a function tool, an enabled built-in tool, or another source's tool. No model request is
185
+ * made.
186
+ * @throws Any error of the model call or of a tool source's `listTools`, which retries the `RunModel` attempt.
187
+ */
188
+ async runModelStep(turn) {
189
+ this.requireNoBuiltInToolNameClash(turn);
190
+ const modelCallCount = readModelCallCount(turn.frameworkRunState);
191
+ const unstoredToolResults = buildUnstoredToolResults(turn);
192
+ const isToolBudgetSpent = modelCallCount >= this.maxModelCallsPerTurn;
193
+ const sourceTools = isToolBudgetSpent || turn.interactionMode === "planning" ? new Map() : await this.listSourceTools(turn);
194
+ const request = await this.buildModelRequest(turn, unstoredToolResults, modelCallCount, isToolBudgetSpent, sourceTools);
195
+ const response = await this.resolveModel(turn.sessionSettings).callModel(request, turn.events);
196
+ requireModelResponse(response);
197
+ const usage = { ...response.usage, modelCalls: 1 };
198
+ const newMessages = [...unstoredToolResults, ...response.outputItems].map(toStepwiseFrameworkMessage);
199
+ if (response.toolCalls.length === 0) {
200
+ return { kind: "completed", newMessages, usage };
201
+ }
202
+ if (isToolBudgetSpent) {
203
+ // The model asked for tools although none were offered; end the turn so it cannot loop.
204
+ const notRunResults = response.toolCalls.map((toolCall) => ({
205
+ type: "tool_result",
206
+ toolCallId: toolCall.toolCallId,
207
+ toolName: toolCall.toolName,
208
+ output: buildToolCallLimitReachedResult(this.maxModelCallsPerTurn),
209
+ }));
210
+ return {
211
+ kind: "completed",
212
+ newMessages: [...newMessages, ...notRunResults.map(toStepwiseFrameworkMessage)],
213
+ usage,
214
+ };
215
+ }
216
+ return {
217
+ kind: "tool_calls_requested",
218
+ frameworkRunState: serializeJsonWithSpacedSeparators({ model_calls: modelCallCount + 1 }),
219
+ newMessages,
220
+ toolCalls: response.toolCalls.map((toolCall) => {
221
+ const listed = sourceTools.get(toolCall.toolName);
222
+ if (listed !== undefined) {
223
+ const built = listed.source.deferredToolCall(turn, { ...toolCall, listedTool: listed.tool });
224
+ return {
225
+ ...built,
226
+ toolSourceReference: writeToolSourceEnvelope(listed.source.sourceName, built.toolSourceReference),
227
+ };
228
+ }
229
+ return {
230
+ toolCallId: toolCall.toolCallId,
231
+ toolName: toolCall.toolName,
232
+ arguments: toolCall.arguments,
233
+ policy: this.resolveModelToolPolicy(turn, toolCall.toolName),
234
+ };
235
+ }),
236
+ usage,
237
+ };
238
+ }
239
+ /**
240
+ * Runs one function tool call after checking its arguments against the tool's schema, or passes a call that a
241
+ * tool source built in `runModelStep` to that source, with the source's own `toolSourceReference` and without a
242
+ * listing. Any other name is an unknown tool and never reaches a source.
243
+ * @param call - The deferred call.
244
+ * @returns The output: a `string` result unchanged, any other result as JSON; the tool's
245
+ * {@link ToolInputRequested}; or a source's outcome, such as {@link ToolTaskStarted}.
246
+ * @throws {ToolExecutionError} For an unknown tool, arguments that are not a JSON object or break the
247
+ * schema, or a `ToolExecutionError` from the tool or its source.
248
+ * @throws {RangeError} When a source built the call but the definition no longer has it, so the effect decides
249
+ * the outcome.
250
+ * @throws Any other error of the tool source.
251
+ */
252
+ async executeDeferredToolCall(call) {
253
+ const toolCall = call.toolCall;
254
+ const tool = this.functionToolsByName.get(toolCall.toolName);
255
+ if (tool === undefined) {
256
+ const routed = this.routeToToolSource(toolCall);
257
+ if (routed === undefined) {
258
+ throw new ToolExecutionError(buildUnknownToolError(toolCall.toolName));
259
+ }
260
+ return routed.source.executeDeferredToolCall({ ...call, toolCall: withToolSourceReference(toolCall, routed.reference) });
261
+ }
262
+ const toolArguments = parseToolArguments(tool.name, toolCall.arguments);
263
+ const violation = findJsonSchemaViolation(toolArguments, tool.parameters, "arguments");
264
+ if (violation !== undefined) {
265
+ throw new ToolExecutionError(`invalid arguments for ${tool.name}: ${violation}`);
266
+ }
267
+ const context = {
268
+ flowId: call.flowId,
269
+ turnId: toolCall.turnId,
270
+ toolCallId: toolCall.toolCallId,
271
+ idempotencyKey: call.idempotencyKey,
272
+ attempt: call.attempt,
273
+ events: call.events,
274
+ cancellationSignal: call.cancellationSignal,
275
+ deadline: call.deadline,
276
+ inputAnswers: call.inputAnswers ?? [],
277
+ inputContinuation: toolCall.inputContinuation ?? null,
278
+ heartbeat: () => call.heartbeat(),
279
+ };
280
+ const result = await tool.execute(toolArguments, context);
281
+ if (typeof result === "string") {
282
+ return { output: result };
283
+ }
284
+ if (result?.kind === "tool_input_requested") {
285
+ return result;
286
+ }
287
+ return { output: serializeJsonWithSpacedSeparators(result === undefined ? null : result) };
288
+ }
289
+ /**
290
+ * Polls a tool source's task once, through the source that built the call.
291
+ * @param poll - The call's record from `PollToolTask`.
292
+ * @returns The source's `pollToolTask` outcome.
293
+ * @throws {RangeError} When no tool source built the call. The task may still run, so `PollToolTask` retries
294
+ * and then records the call by its effect.
295
+ * @throws Any error of the source.
296
+ */
297
+ async pollToolTask(poll) {
298
+ const routed = this.routeToToolSource(poll.toolCall);
299
+ if (routed === undefined) {
300
+ throw new RangeError(`no tool source built the call of tool ${poll.toolCall.toolName}`);
301
+ }
302
+ return routed.source.pollToolTask({ ...poll, toolCall: withToolSourceReference(poll.toolCall, routed.reference) });
303
+ }
304
+ /**
305
+ * Cancels a tool source's task through the source that built the call; a source without `cancelToolTask` leaves
306
+ * its task running.
307
+ * @param cancellation - The abandoned call's record from `CancelToolTask`.
308
+ * @returns A Promise that resolves once the source took the cancellation.
309
+ * @throws {RangeError} When no tool source built the call; `CancelToolTask` retries and then records the call
310
+ * `outcome_unknown`.
311
+ * @throws Any error of the source.
312
+ */
313
+ async cancelToolTask(cancellation) {
314
+ const routed = this.routeToToolSource(cancellation.toolCall);
315
+ if (routed === undefined) {
316
+ throw new RangeError(`no tool source built the call of tool ${cancellation.toolCall.toolName}`);
317
+ }
318
+ await routed.source.cancelToolTask?.({
319
+ ...cancellation,
320
+ toolCall: withToolSourceReference(cancellation.toolCall, routed.reference),
321
+ });
322
+ }
323
+ /**
324
+ * Returns the results of the turn's tool calls that are not stored yet.
325
+ * @param turn - The ended turn, with every record final.
326
+ * @returns One `tool_result` message per unstored result, in deferral order.
327
+ */
328
+ buildUnfinishedTurnMessages(turn) {
329
+ return buildUnstoredToolResults(turn).map(toStepwiseFrameworkMessage);
330
+ }
331
+ /**
332
+ * Estimates the model input of the next `runModelStep` with the model client.
333
+ * @param turn - The context of the next model call.
334
+ * @returns `ModelClient.estimateInputTokens` of the request `runModelStep` would send: instructions,
335
+ * summary, messages, unstored tool results, and tools. Tool source tools come from each source's `cachedTools`
336
+ * when it has one, else from the runner's last listing, as in the Python core.
337
+ */
338
+ async estimateInputTokens(turn) {
339
+ const modelCallCount = readModelCallCount(turn.frameworkRunState);
340
+ const isToolBudgetSpent = modelCallCount >= this.maxModelCallsPerTurn;
341
+ const request = await this.buildModelRequest(turn, buildUnstoredToolResults(turn), modelCallCount, isToolBudgetSpent, this.readCachedSourceTools());
342
+ return this.resolveModel(turn.sessionSettings).estimateInputTokens(request);
343
+ }
344
+ /**
345
+ * Chooses the last message to summarize, at a boundary between model responses.
346
+ *
347
+ * The runner cuts only before a user message or before the first output item of a model response whose
348
+ * earlier tool calls all have stored results, so no tool call is separated from its result and no
349
+ * response is split. It keeps the newest messages that fit the budget, and never summarizes the newest
350
+ * stored message.
351
+ *
352
+ * @param turn - The context of the model call that crossed the threshold.
353
+ * @param retainedTokenBudget - Tokens that the kept messages may use.
354
+ * @returns The sequence of the last message to summarize: the earliest cut whose kept messages fit the
355
+ * budget, else the latest cut; `null` when every message already fits or no cut is possible.
356
+ */
357
+ selectCompactionCutoff(turn, retainedTokenBudget) {
358
+ const items = turn.messages.map(readStoredModelInputItem);
359
+ const cuts = findCompactionCuts(items);
360
+ if (cuts.length === 0) {
361
+ return null;
362
+ }
363
+ const keptTokens = new Array(items.length + 1).fill(0);
364
+ keptTokens[items.length] = this.estimateItems(turn, buildUnstoredToolResults(turn));
365
+ for (let index = items.length - 1; index >= 0; index -= 1) {
366
+ keptTokens[index] = (keptTokens[index + 1] ?? 0) + this.estimateItems(turn, items.slice(index, index + 1));
367
+ }
368
+ if ((keptTokens[0] ?? 0) <= retainedTokenBudget) {
369
+ return null;
370
+ }
371
+ const cut = cuts.find((candidate) => (keptTokens[candidate] ?? 0) <= retainedTokenBudget) ?? cuts.at(-1);
372
+ return cut === undefined ? null : (turn.messages[cut - 1]?.sequence ?? null);
373
+ }
374
+ /**
375
+ * Folds the previous summary and the messages of the range into new text, with one model call.
376
+ *
377
+ * The call uses {@link contextSummaryInstructions}, no tools, the previous summary as a note, the
378
+ * range's items, and {@link contextSummaryRequest} as the last user text. Its text streams nowhere,
379
+ * because it is not the assistant's answer. When the request would exceed `request.inputTokenBudget`,
380
+ * such as after a smaller context window or when compaction starts on a long session, the runner leaves
381
+ * out the oldest items of the range and says so with {@link buildContextSummaryOmissionNote}, so the
382
+ * call and its `maxOutputTokens`, when set, still fit. It leaves them out up to a boundary between model responses, as
383
+ * {@link selectCompactionCutoff} cuts, so it never splits a response or separates a tool call from its
384
+ * result.
385
+ *
386
+ * @param request - The previous summary and the messages to summarize.
387
+ * @returns The response text and the usage of the call.
388
+ * @throws {Error} When the model returned no text, or any error of the model call.
389
+ */
390
+ async summarizeContext(request) {
391
+ const model = this.resolveModel(request.sessionSettings);
392
+ const rangeItems = request.messages.map(readStoredModelInputItem);
393
+ let omittedCount = 0;
394
+ const budget = request.inputTokenBudget === undefined
395
+ ? undefined
396
+ : // The window also holds the summary the call writes.
397
+ request.inputTokenBudget - (this.definition.modelSettings?.maxOutputTokens ?? 0);
398
+ if (budget !== undefined) {
399
+ // Estimate by parts, then leave out the oldest items up to the first cut that fits.
400
+ const itemTokens = rangeItems.map((item) => model.estimateInputTokens({ ...this.buildSummaryRequest(request, [], 0), instructions: "", inputItems: [item] }));
401
+ let tokens = model.estimateInputTokens(this.buildSummaryRequest(request, [], rangeItems.length)) +
402
+ itemTokens.reduce((total, itemToken) => total + itemToken, 0);
403
+ // A cut never splits a model response or separates a tool call from its result.
404
+ for (const cut of [...findCompactionCuts(rangeItems), rangeItems.length]) {
405
+ if (tokens <= budget) {
406
+ break;
407
+ }
408
+ tokens -= itemTokens.slice(omittedCount, cut).reduce((total, itemToken) => total + itemToken, 0);
409
+ omittedCount = cut;
410
+ }
411
+ }
412
+ const response = await model.callModel(this.buildSummaryRequest(request, rangeItems.slice(omittedCount), omittedCount), discardedModelOutput);
413
+ requireModelResponse(response);
414
+ if (response.text.trim() === "") {
415
+ throw new Error("the summary model call returned no text");
416
+ }
417
+ return { text: response.text.trim(), usage: { ...response.usage, modelCalls: 1 } };
418
+ }
419
+ buildSummaryRequest(request, rangeItems, omittedCount) {
420
+ const items = [];
421
+ if (request.previousSummary !== null) {
422
+ items.push({ type: "user_text", text: buildContextSummaryNote(request.previousSummary) });
423
+ }
424
+ if (omittedCount > 0) {
425
+ items.push({ type: "user_text", text: buildContextSummaryOmissionNote(omittedCount) });
426
+ }
427
+ items.push(...rangeItems, { type: "user_text", text: contextSummaryRequest });
428
+ return {
429
+ instructions: contextSummaryInstructions,
430
+ inputItems: pairToolCallsWithResults(items),
431
+ tools: [],
432
+ toolChoice: "none",
433
+ settings: this.definition.modelSettings ?? {},
434
+ metadata: {
435
+ flowId: request.flowId,
436
+ turnId: request.turnId,
437
+ stepExecutionId: request.stepExecutionId,
438
+ attempt: request.attempt,
439
+ },
440
+ cancellationSignal: request.cancellationSignal,
441
+ };
442
+ }
443
+ async buildModelRequest(turn, unstoredToolResults, modelCallCount, isToolBudgetSpent, sourceTools) {
444
+ const contextSummary = turn.contextSummary ?? null;
445
+ const summaryItems = contextSummary === null ? [] : [{ type: "user_text", text: buildContextSummaryNote(contextSummary) }];
446
+ let toolChoice = isToolBudgetSpent ? "none" : "auto";
447
+ if (!isToolBudgetSpent && this.definition.planningToolChoice === "required" && mustWritePlanFirst(turn, modelCallCount)) {
448
+ toolChoice = { toolName: "write_todos" };
449
+ }
450
+ return {
451
+ instructions: await this.resolveInstructions(turn),
452
+ // The plan note comes after the summary, so compaction never hides the plan.
453
+ inputItems: pairToolCallsWithResults([
454
+ ...summaryItems,
455
+ ...readStoredInputItemsWithPlanNote(turn),
456
+ ...unstoredToolResults,
457
+ ]),
458
+ tools: isToolBudgetSpent ? [] : this.listOfferedTools(turn, sourceTools),
459
+ toolChoice,
460
+ settings: this.definition.modelSettings ?? {},
461
+ metadata: modelRequestMetadata(turn),
462
+ cancellationSignal: turn.cancellationSignal,
463
+ };
464
+ }
465
+ estimateItems(turn, items) {
466
+ if (items.length === 0) {
467
+ return 0;
468
+ }
469
+ return this.resolveModel(turn.sessionSettings).estimateInputTokens({
470
+ instructions: "",
471
+ inputItems: items,
472
+ tools: [],
473
+ toolChoice: "none",
474
+ settings: this.definition.modelSettings ?? {},
475
+ metadata: modelRequestMetadata(turn),
476
+ cancellationSignal: turn.cancellationSignal,
477
+ });
478
+ }
479
+ /** A built-in enabled only in the session options must not share a function tool's name. */
480
+ requireNoBuiltInToolNameClash(turn) {
481
+ for (const builtInTool of turn.enabledBuiltInTools ?? []) {
482
+ if (this.functionToolsByName.has(builtInTool)) {
483
+ throw new TypeError(`tool ${builtInTool} uses the name of an enabled built-in tool`);
484
+ }
485
+ }
486
+ }
487
+ async listSourceTools(turn) {
488
+ const enabledBuiltInTools = turn.enabledBuiltInTools ?? new Set();
489
+ const functionToolNames = new Set(this.functionToolsByName.keys());
490
+ const listed = new Map();
491
+ for (const [index, source] of this.toolSources.entries()) {
492
+ for (const tool of await this.listOneSource(index, source)) {
493
+ refuseSourceToolName(tool.name, functionToolNames, enabledBuiltInTools, listed);
494
+ listed.set(tool.name, { index, source, tool });
495
+ }
496
+ }
497
+ return listed;
498
+ }
499
+ async listOneSource(index, source) {
500
+ const listing = source.listTools();
501
+ // A listing that outlives this call keeps running; nobody else reads its error.
502
+ listing.catch(() => undefined);
503
+ let timer;
504
+ const timedOut = new Promise((resolve) => {
505
+ timer = setTimeout(() => resolve("timed_out"), this.toolSourceListingTimeoutMs);
506
+ });
507
+ try {
508
+ const tools = await Promise.race([listing, timedOut]);
509
+ if (tools === "timed_out") {
510
+ console.warn(`tool source ${index} listed nothing within ${this.toolSourceListingTimeoutMs} ms`);
511
+ return [];
512
+ }
513
+ this.lastListings.set(index, tools);
514
+ return tools;
515
+ }
516
+ catch (error) {
517
+ console.warn(`tool source ${index} could not list its tools: ${error instanceof Error ? error.message : String(error)}`);
518
+ return [];
519
+ }
520
+ finally {
521
+ clearTimeout(timer);
522
+ }
523
+ }
524
+ readCachedSourceTools() {
525
+ const cached = new Map();
526
+ for (const [index, source] of this.toolSources.entries()) {
527
+ const tools = source.cachedTools?.() ?? this.lastListings.get(index) ?? [];
528
+ for (const tool of tools) {
529
+ if (!cached.has(tool.name)) {
530
+ cached.set(tool.name, { index, source, tool });
531
+ }
532
+ }
533
+ }
534
+ return cached;
535
+ }
536
+ routeToToolSource(toolCall) {
537
+ const envelope = readToolSourceEnvelope(toolCall.toolSourceReference);
538
+ if (envelope !== undefined) {
539
+ const source = this.toolSourcesByName.get(envelope.sourceName);
540
+ if (source === undefined) {
541
+ throw new RangeError(`tool source ${envelope.sourceName}, which built the call of tool ${toolCall.toolName}, is not configured on this Worker`);
542
+ }
543
+ return { source, reference: envelope.reference };
544
+ }
545
+ const reference = toolCall.toolSourceReference ?? undefined;
546
+ if (reference === undefined || this.toolSources.length === 0) {
547
+ return undefined;
548
+ }
549
+ const onlySource = this.toolSources.length === 1 ? this.toolSources[0] : undefined;
550
+ if (onlySource !== undefined) {
551
+ // A reference the source built itself, such as in a runner subclass.
552
+ return { source: onlySource, reference };
553
+ }
554
+ throw new RangeError(`cannot tell which tool source built the call of ${toolCall.toolName}`);
555
+ }
556
+ listOfferedTools(turn, sourceTools) {
557
+ const isPlanning = turn.interactionMode === "planning";
558
+ const enabledBuiltInTools = turn.enabledBuiltInTools ?? new Set();
559
+ const builtInTools = builtInToolDefinitions
560
+ .filter((definition) => enabledBuiltInTools.has(definition.name))
561
+ .filter((definition) => !isPlanning || definition.name === "write_todos")
562
+ .map((definition) => ({
563
+ name: definition.name,
564
+ description: definition.description,
565
+ parameters: definition.parameters,
566
+ }));
567
+ if (isPlanning) {
568
+ return builtInTools;
569
+ }
570
+ const functionTools = [...this.functionToolsByName.values()].map((tool) => ({
571
+ name: tool.name,
572
+ description: tool.description,
573
+ parameters: tool.parameters,
574
+ }));
575
+ const offeredSourceTools = [...sourceTools.values()].map(({ tool }) => ({
576
+ name: tool.name,
577
+ description: tool.description,
578
+ parameters: tool.inputSchema,
579
+ }));
580
+ return [...functionTools, ...offeredSourceTools, ...builtInTools];
581
+ }
582
+ /** A session policy wins; otherwise a function tool's own effect and approval apply, as in the Python core. */
583
+ resolveModelToolPolicy(turn, toolName) {
584
+ const tool = this.functionToolsByName.get(toolName);
585
+ const isBuiltIn = turn.enabledBuiltInTools?.has(toolName) === true;
586
+ if (tool === undefined && this.toolSources.length > 0 && !isBuiltIn) {
587
+ return unlistedSourceToolPolicy;
588
+ }
589
+ const sessionPolicies = turn.toolPolicies ?? this.definition.toolPolicies ?? {};
590
+ if (Object.hasOwn(sessionPolicies, toolName)) {
591
+ return turn.resolveToolPolicy(toolName);
592
+ }
593
+ if (tool === undefined) {
594
+ // The session fixes a built-in call's policy; an unknown tool fails fast in CallTool, so the model sees why.
595
+ return { effect: "read", requiresApproval: false };
596
+ }
597
+ const policy = turn.resolveToolPolicy(toolName, tool.effect);
598
+ return tool.requiresApproval === undefined
599
+ ? policy
600
+ : { effect: policy.effect, requiresApproval: tool.requiresApproval };
601
+ }
602
+ resolveModel(sessionSettings) {
603
+ const resolveModel = this.definition.resolveModel;
604
+ if (resolveModel === undefined) {
605
+ return this.definition.model;
606
+ }
607
+ const model = resolveModel(sessionSettings ?? {});
608
+ if (typeof model?.callModel !== "function" || typeof model.estimateInputTokens !== "function") {
609
+ throw new TypeError(`resolveModel of agent ${this.definition.name} must return a ModelClient`);
610
+ }
611
+ return model;
612
+ }
613
+ async resolveInstructions(turn) {
614
+ const instructions = this.definition.instructions;
615
+ if (typeof instructions === "string") {
616
+ return instructions;
617
+ }
618
+ const resolved = await instructions({
619
+ flowId: turn.flowId,
620
+ turnId: turn.turnId,
621
+ attempt: turn.attempt,
622
+ interactionMode: turn.interactionMode ?? "chat",
623
+ plan: turn.plan ?? null,
624
+ sessionSettings: turn.sessionSettings ?? {},
625
+ });
626
+ if (typeof resolved !== "string") {
627
+ throw new TypeError(`the instructions of agent ${this.definition.name} must resolve to a string`);
628
+ }
629
+ return resolved;
630
+ }
631
+ }
632
+ /** A planning turn without a draft drafts one first; feedback on a draft may only answer. */
633
+ function mustWritePlanFirst(turn, modelCallCount) {
634
+ return (turn.interactionMode === "planning" &&
635
+ modelCallCount === 0 &&
636
+ turn.enabledBuiltInTools?.has("write_todos") === true &&
637
+ turn.plan?.status !== "draft");
638
+ }
639
+ function readStoredInputItemsWithPlanNote(turn) {
640
+ const items = turn.messages.map(readStoredModelInputItem);
641
+ const planNote = buildPlanNote(turn.plan, turn.interactionMode);
642
+ if (planNote === undefined) {
643
+ return items;
644
+ }
645
+ // Before the turn's messages, so a plan change rewrites only the turn's tail of the input.
646
+ const turnStart = turn.messages.findIndex((message) => message.turnId === turn.turnId);
647
+ items.splice(turnStart === -1 ? items.length : turnStart, 0, { type: "user_text", text: planNote });
648
+ return items;
649
+ }
650
+ /** Results of the turn's final records that no stored `tool_result` message holds yet, in deferral order. */
651
+ function buildUnstoredToolResults(turn) {
652
+ const turnMessages = turn.messages.filter((message) => message.turnId === turn.turnId);
653
+ const storedResultIds = new Set(turnMessages.filter((message) => message.role === "tool").map((message) => message.toolCallId));
654
+ // A call summarized away was summarized with its result, so only shown calls can lack one.
655
+ const shownCallIds = (turn.contextSummary ?? null) === null
656
+ ? undefined
657
+ : new Set(turnMessages
658
+ .map(readStoredModelInputItem)
659
+ .flatMap((item) => (item.type === "model_output" ? item.toolCalls : []))
660
+ .map((toolCall) => toolCall.toolCallId));
661
+ return [...turn.toolCalls]
662
+ .sort((left, right) => left.deferralOrder - right.deferralOrder)
663
+ .filter((record) => shownCallIds === undefined || shownCallIds.has(record.toolCallId))
664
+ .filter((record) => !storedResultIds.has(record.toolCallId))
665
+ .map((record) => ({
666
+ type: "tool_result",
667
+ toolCallId: record.toolCallId,
668
+ toolName: record.toolName,
669
+ output: describeToolCallOutcome(record),
670
+ }));
671
+ }
672
+ function parseToolArguments(toolName, argumentsText) {
673
+ let parsed;
674
+ try {
675
+ parsed = JSON.parse(argumentsText === "" ? "{}" : argumentsText);
676
+ }
677
+ catch {
678
+ // A fixed text, because the two cores' JSON parsers word their errors differently.
679
+ throw new ToolExecutionError(`invalid arguments for ${toolName}: arguments are not valid JSON`);
680
+ }
681
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
682
+ throw new ToolExecutionError(`invalid arguments for ${toolName}: expected a JSON object`);
683
+ }
684
+ return parsed;
685
+ }
686
+ function readModelCallCount(frameworkRunState) {
687
+ if (frameworkRunState === null) {
688
+ return 0;
689
+ }
690
+ const parsed = JSON.parse(frameworkRunState);
691
+ if (typeof parsed.model_calls !== "number" || !Number.isSafeInteger(parsed.model_calls)) {
692
+ throw new TypeError("FrameworkRunState is not a Stepwise run state");
693
+ }
694
+ return parsed.model_calls;
695
+ }
696
+ function requireModelResponse(response) {
697
+ if (!Array.isArray(response?.outputItems) ||
698
+ !Array.isArray(response.toolCalls) ||
699
+ !response.outputItems.every((item) => item?.type === "model_output")) {
700
+ throw new TypeError("ModelClient.callModel must return output items and tool calls");
701
+ }
702
+ }
703
+ /** Each index that may start the kept items, ascending; never the last item's index. */
704
+ function findCompactionCuts(items) {
705
+ const cuts = [];
706
+ const openCallIds = new Set();
707
+ for (let index = 0; index < items.length - 1; index += 1) {
708
+ const item = items[index];
709
+ const following = items[index + 1];
710
+ if (item === undefined || following === undefined) {
711
+ continue;
712
+ }
713
+ if (item.type === "model_output") {
714
+ for (const toolCall of item.toolCalls) {
715
+ openCallIds.add(toolCall.toolCallId);
716
+ }
717
+ }
718
+ else if (item.type === "tool_result") {
719
+ openCallIds.delete(item.toolCallId);
720
+ }
721
+ else {
722
+ // User text closes calls left open, as pairToolCallsWithResults does.
723
+ openCallIds.clear();
724
+ }
725
+ if (openCallIds.size > 0 || following.type === "tool_result") {
726
+ continue;
727
+ }
728
+ // Consecutive output items are one model response, which must stay whole.
729
+ if (item.type === "model_output" && following.type === "model_output") {
730
+ continue;
731
+ }
732
+ cuts.push(index + 1);
733
+ }
734
+ return cuts;
735
+ }
736
+ function modelRequestMetadata(turn) {
737
+ return {
738
+ flowId: turn.flowId,
739
+ turnId: turn.turnId,
740
+ stepExecutionId: turn.stepExecutionId ?? "",
741
+ attempt: turn.attempt,
742
+ };
743
+ }
744
+ function refuseSourceToolName(toolName, functionToolNames, builtInTools, otherSourceTools) {
745
+ if (builtInTools.has(toolName)) {
746
+ throw new TypeError(`tool ${toolName} uses the name of an enabled built-in tool`);
747
+ }
748
+ if (functionToolNames.has(toolName)) {
749
+ throw new TypeError(`tool ${toolName} of a tool source uses the name of a function tool`);
750
+ }
751
+ if (otherSourceTools.has(toolName)) {
752
+ throw new TypeError(`two tool sources list a tool named ${toolName}`);
753
+ }
754
+ }
755
+ function refuseDeclaredToolNameCollisions(toolSources, functionToolNames, builtInTools) {
756
+ const declared = new Map();
757
+ for (const source of toolSources) {
758
+ const names = [...(source.declaredToolNames?.() ?? [])].sort();
759
+ for (const toolName of names) {
760
+ refuseSourceToolName(toolName, functionToolNames, builtInTools, declared);
761
+ }
762
+ for (const toolName of names) {
763
+ declared.set(toolName, source);
764
+ }
765
+ }
766
+ }
767
+ /** Error of a call to a tool that no function tool or source listing offers; the same text as the Python core. */
768
+ export function buildUnknownToolError(toolName) {
769
+ return `unknown tool ${toolName}`;
770
+ }
771
+ /** The runner's `toolSourceReference`: the source's name and its own reference, as JSON text. */
772
+ export function writeToolSourceEnvelope(sourceName, reference) {
773
+ return JSON.stringify({ [toolSourceNameKey]: sourceName, [toolSourceReferenceKey]: reference ?? null });
774
+ }
775
+ export function readToolSourceEnvelope(toolSourceReference) {
776
+ if (toolSourceReference === undefined || toolSourceReference === null) {
777
+ return undefined;
778
+ }
779
+ let envelope;
780
+ try {
781
+ envelope = JSON.parse(toolSourceReference);
782
+ }
783
+ catch {
784
+ return undefined;
785
+ }
786
+ if (typeof envelope !== "object" || envelope === null || Array.isArray(envelope)) {
787
+ return undefined;
788
+ }
789
+ const sourceName = envelope[toolSourceNameKey];
790
+ const reference = envelope[toolSourceReferenceKey];
791
+ if (typeof sourceName !== "string") {
792
+ return undefined;
793
+ }
794
+ if (reference !== null && reference !== undefined && typeof reference !== "string") {
795
+ return undefined;
796
+ }
797
+ return { sourceName, reference: reference ?? undefined };
798
+ }
799
+ /** The record as the source built it: its own reference instead of the runner's envelope. */
800
+ function withToolSourceReference(toolCall, reference) {
801
+ const { toolSourceReference: _envelope, ...rest } = toolCall;
802
+ return (reference === undefined ? rest : { ...rest, toolSourceReference: reference });
803
+ }
804
+ //# sourceMappingURL=stepwise-agent-runner.js.map