@superdurable/dex-agents 0.3.0 → 0.5.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 (139) hide show
  1. package/README.md +598 -45
  2. package/dist/src/agent-context-compaction.d.ts +1 -1
  3. package/dist/src/agent-framework-runner.d.ts +210 -10
  4. package/dist/src/agent-framework-runner.js +30 -0
  5. package/dist/src/agent-framework-runner.js.map +1 -1
  6. package/dist/src/agent-local-directory-media-store.d.ts +111 -0
  7. package/dist/src/agent-local-directory-media-store.js +417 -0
  8. package/dist/src/agent-local-directory-media-store.js.map +1 -0
  9. package/dist/src/agent-media.d.ts +279 -0
  10. package/dist/src/agent-media.js +457 -0
  11. package/dist/src/agent-media.js.map +1 -0
  12. package/dist/src/agent-session-codecs.d.ts +3 -1
  13. package/dist/src/agent-session-codecs.js +53 -4
  14. package/dist/src/agent-session-codecs.js.map +1 -1
  15. package/dist/src/agent-session-failure.d.ts +4 -2
  16. package/dist/src/agent-session-failure.js +6 -2
  17. package/dist/src/agent-session-failure.js.map +1 -1
  18. package/dist/src/agent-session-flow-graph.d.ts +46 -0
  19. package/dist/src/agent-session-flow-graph.js +51 -0
  20. package/dist/src/agent-session-flow-graph.js.map +1 -1
  21. package/dist/src/agent-session-flow-graph.json +98 -81
  22. package/dist/src/agent-session-flow.d.ts +5 -3
  23. package/dist/src/agent-session-flow.js +10 -1
  24. package/dist/src/agent-session-flow.js.map +1 -1
  25. package/dist/src/agent-session-start.d.ts +1 -1
  26. package/dist/src/agent-session-start.js +2 -2
  27. package/dist/src/agent-session-start.js.map +1 -1
  28. package/dist/src/agent-session-steps.d.ts +9 -0
  29. package/dist/src/agent-session-steps.js +219 -34
  30. package/dist/src/agent-session-steps.js.map +1 -1
  31. package/dist/src/agent-session-tracer.js +1 -1
  32. package/dist/src/agent-session-tracer.js.map +1 -1
  33. package/dist/src/agent-session-transitions.d.ts +4 -0
  34. package/dist/src/agent-session-transitions.js +29 -1
  35. package/dist/src/agent-session-transitions.js.map +1 -1
  36. package/dist/src/agent-session-values.d.ts +112 -1
  37. package/dist/src/agent-session-values.js +6 -0
  38. package/dist/src/agent-session-values.js.map +1 -1
  39. package/dist/src/agent-session-wait-steps.js +17 -5
  40. package/dist/src/agent-session-wait-steps.js.map +1 -1
  41. package/dist/src/ai-sdk/ai-sdk-model-client.d.ts +15 -3
  42. package/dist/src/ai-sdk/ai-sdk-model-client.js +36 -7
  43. package/dist/src/ai-sdk/ai-sdk-model-client.js.map +1 -1
  44. package/dist/src/ai-sdk/index.d.ts +1 -1
  45. package/dist/src/ai-sdk/index.js +1 -1
  46. package/dist/src/ai-sdk/index.js.map +1 -1
  47. package/dist/src/{stepwise → builtin-loop}/agent-definition.d.ts +120 -5
  48. package/dist/src/builtin-loop/agent-definition.js.map +1 -0
  49. package/dist/src/builtin-loop/builtin-agent-session-flow.d.ts +69 -0
  50. package/dist/src/{stepwise/stepwise-agent-session-flow.js → builtin-loop/builtin-agent-session-flow.js} +15 -9
  51. package/dist/src/builtin-loop/builtin-agent-session-flow.js.map +1 -0
  52. package/dist/src/{stepwise/stepwise-agent-runner.d.ts → builtin-loop/builtin-loop-agent-runner.d.ts} +85 -26
  53. package/dist/src/{stepwise/stepwise-agent-runner.js → builtin-loop/builtin-loop-agent-runner.js} +320 -213
  54. package/dist/src/builtin-loop/builtin-loop-agent-runner.js.map +1 -0
  55. package/dist/src/{stepwise/stepwise-input-items.d.ts → builtin-loop/builtin-loop-input-items.d.ts} +7 -3
  56. package/dist/src/{stepwise/stepwise-input-items.js → builtin-loop/builtin-loop-input-items.js} +57 -10
  57. package/dist/src/builtin-loop/builtin-loop-input-items.js.map +1 -0
  58. package/dist/src/builtin-loop/function-tool.d.ts +282 -0
  59. package/dist/src/{stepwise → builtin-loop}/function-tool.js +63 -3
  60. package/dist/src/builtin-loop/function-tool.js.map +1 -0
  61. package/dist/src/builtin-loop/index.d.ts +8 -0
  62. package/dist/src/builtin-loop/index.js +13 -0
  63. package/dist/src/builtin-loop/index.js.map +1 -0
  64. package/dist/src/builtin-loop/json-schema-validation.js.map +1 -0
  65. package/dist/src/{stepwise → builtin-loop}/model-client.d.ts +36 -7
  66. package/dist/src/builtin-loop/model-client.js.map +1 -0
  67. package/dist/src/{stepwise → builtin-loop}/scripted-model-client.d.ts +8 -5
  68. package/dist/src/{stepwise → builtin-loop}/scripted-model-client.js +8 -5
  69. package/dist/src/builtin-loop/scripted-model-client.js.map +1 -0
  70. package/dist/src/{stepwise → builtin-loop}/tool-source.d.ts +13 -3
  71. package/dist/src/builtin-loop/tool-source.js.map +1 -0
  72. package/dist/src/connectors/connector-input-schema.d.ts +19 -0
  73. package/dist/src/connectors/connector-input-schema.js +228 -0
  74. package/dist/src/connectors/connector-input-schema.js.map +1 -0
  75. package/dist/src/connectors/connector-step-context.d.ts +94 -0
  76. package/dist/src/connectors/connector-step-context.js +180 -0
  77. package/dist/src/connectors/connector-step-context.js.map +1 -0
  78. package/dist/src/connectors/connector-tool-source.d.ts +401 -0
  79. package/dist/src/connectors/connector-tool-source.js +628 -0
  80. package/dist/src/connectors/connector-tool-source.js.map +1 -0
  81. package/dist/src/connectors/index.d.ts +3 -0
  82. package/dist/src/connectors/index.js +11 -0
  83. package/dist/src/connectors/index.js.map +1 -0
  84. package/dist/src/index.d.ts +7 -3
  85. package/dist/src/index.js +6 -3
  86. package/dist/src/index.js.map +1 -1
  87. package/dist/src/mcp/index.d.ts +1 -0
  88. package/dist/src/mcp/index.js +1 -0
  89. package/dist/src/mcp/index.js.map +1 -1
  90. package/dist/src/mcp/mcp-dex-conventions.d.ts +57 -0
  91. package/dist/src/mcp/mcp-dex-conventions.js +90 -0
  92. package/dist/src/mcp/mcp-dex-conventions.js.map +1 -0
  93. package/dist/src/mcp/mcp-image-content.d.ts +20 -0
  94. package/dist/src/mcp/mcp-image-content.js +82 -0
  95. package/dist/src/mcp/mcp-image-content.js.map +1 -0
  96. package/dist/src/mcp/mcp-tool-source.d.ts +178 -17
  97. package/dist/src/mcp/mcp-tool-source.js +237 -34
  98. package/dist/src/mcp/mcp-tool-source.js.map +1 -1
  99. package/dist/src/s3/index.d.ts +1 -0
  100. package/dist/src/s3/index.js +9 -0
  101. package/dist/src/s3/index.js.map +1 -0
  102. package/dist/src/s3/s3-media-store.d.ts +138 -0
  103. package/dist/src/s3/s3-media-store.js +255 -0
  104. package/dist/src/s3/s3-media-store.js.map +1 -0
  105. package/dist/src/stepwise/index.d.ts +41 -8
  106. package/dist/src/stepwise/index.js +35 -5
  107. package/dist/src/stepwise/index.js.map +1 -1
  108. package/dist/src/testing/agent-session-test-client.d.ts +6 -3
  109. package/dist/src/testing/agent-session-test-client.js +10 -3
  110. package/dist/src/testing/agent-session-test-client.js.map +1 -1
  111. package/dist/src/testing/scripted-model-client.d.ts +1 -1
  112. package/dist/src/testing/scripted-model-client.js +2 -2
  113. package/dist/src/testing/scripted-model-client.js.map +1 -1
  114. package/dist/src/tool-source-router.d.ts +211 -0
  115. package/dist/src/tool-source-router.js +380 -0
  116. package/dist/src/tool-source-router.js.map +1 -0
  117. package/dist/src/ui-stream/agent-event-json.d.ts +2 -1
  118. package/dist/src/ui-stream/agent-event-json.js +3 -1
  119. package/dist/src/ui-stream/agent-event-json.js.map +1 -1
  120. package/dist/src/ui-stream/agent-ui-messages.d.ts +11 -3
  121. package/dist/src/ui-stream/agent-ui-messages.js +29 -5
  122. package/dist/src/ui-stream/agent-ui-messages.js.map +1 -1
  123. package/package.json +25 -3
  124. package/dist/src/stepwise/agent-definition.js.map +0 -1
  125. package/dist/src/stepwise/function-tool.d.ts +0 -130
  126. package/dist/src/stepwise/function-tool.js.map +0 -1
  127. package/dist/src/stepwise/json-schema-validation.js.map +0 -1
  128. package/dist/src/stepwise/model-client.js.map +0 -1
  129. package/dist/src/stepwise/scripted-model-client.js.map +0 -1
  130. package/dist/src/stepwise/stepwise-agent-runner.js.map +0 -1
  131. package/dist/src/stepwise/stepwise-agent-session-flow.d.ts +0 -35
  132. package/dist/src/stepwise/stepwise-agent-session-flow.js.map +0 -1
  133. package/dist/src/stepwise/stepwise-input-items.js.map +0 -1
  134. package/dist/src/stepwise/tool-source.js.map +0 -1
  135. /package/dist/src/{stepwise → builtin-loop}/agent-definition.js +0 -0
  136. /package/dist/src/{stepwise → builtin-loop}/json-schema-validation.d.ts +0 -0
  137. /package/dist/src/{stepwise → builtin-loop}/json-schema-validation.js +0 -0
  138. /package/dist/src/{stepwise → builtin-loop}/model-client.js +0 -0
  139. /package/dist/src/{stepwise → builtin-loop}/tool-source.js +0 -0
@@ -9,32 +9,47 @@ import { builtInToolDefinitions } from "../agent-built-in-tools.js";
9
9
  import { buildContextSummaryNote, buildContextSummaryOmissionNote, contextSummaryInstructions, contextSummaryRequest, } from "../agent-context-compaction.js";
10
10
  export { buildContextSummaryNote, buildContextSummaryOmissionNote, contextSummaryInstructions, contextSummaryNoteHeader, contextSummaryRequest, } from "../agent-context-compaction.js";
11
11
  import { ToolExecutionError, } from "../agent-framework-runner.js";
12
+ import { loadImages } from "../agent-media.js";
12
13
  import { buildPlanNote } from "../agent-plans.js";
13
- import { describeToolCallOutcome, } from "../agent-session-values.js";
14
+ import { describeToolCallOutcome, emptyAgentUsage, } from "../agent-session-values.js";
15
+ import { isFunctionToolResult } from "./function-tool.js";
14
16
  import { findJsonSchemaViolation, serializeJsonWithSpacedSeparators } from "./json-schema-validation.js";
15
- import { pairToolCallsWithResults, readStoredModelInputItem, toStepwiseFrameworkMessage, } from "./stepwise-input-items.js";
16
- const toolSourceMethods = ["listTools", "deferredToolCall", "executeDeferredToolCall", "pollToolTask"];
17
+ import { pairToolCallsWithResults, readStoredModelInputItem, toBuiltinLoopFrameworkMessage, } from "./builtin-loop-input-items.js";
18
+ import { ToolSourceRouter, defaultToolSourceListingTimeoutMs, readToolSourceEnvelope as readRouterToolSourceEnvelope, writeToolSourceEnvelope as writeRouterToolSourceEnvelope, } from "../tool-source-router.js";
19
+ export { defaultToolSourceListingTimeoutMs } from "../tool-source-router.js";
17
20
  // The runner's envelope around a source's toolSourceReference: the building source's name.
18
21
  const toolSourceNameKey = "stepwise_tool_source";
19
22
  // An unlisted name never reaches a source, so it fails without asking anyone to approve it.
20
23
  const unlistedSourceToolPolicy = { effect: "unclassified", requiresApproval: false };
21
- const toolSourceReferenceKey = "reference";
22
- /** Default {@link AgentDefinition.toolSourceListingTimeoutMs}: 10 seconds. */
23
- export const defaultToolSourceListingTimeoutMs = 10_000;
24
24
  export const defaultMaxModelCallsPerTurn = 100;
25
+ /**
26
+ * How many {@link ContinueTurn} results of {@link AgentDefinition.decideTurnContinuation} may follow answers without
27
+ * tool calls in a row. A model response with tool calls resets the count.
28
+ * @since 0.5.0
29
+ */
30
+ export const maxConsecutiveTurnContinuations = 8;
25
31
  /** Result of a tool call the model made after the turn's model call limit; the same text as the Python core. */
26
32
  export function buildToolCallLimitReachedResult(maxModelCallsPerTurn) {
27
33
  return `The tool call was not run: this turn reached its limit of ${maxModelCallsPerTurn} model calls.`;
28
34
  }
35
+ /**
36
+ * The `lastTurnError` of a turn whose `decideTurnContinuation` returned a {@link ContinueTurn} once too often in a
37
+ * row; the same text as the Python core.
38
+ * @param limit - {@link maxConsecutiveTurnContinuations}.
39
+ * @returns The error text.
40
+ */
41
+ export function buildTurnContinuationLimitError(limit) {
42
+ return `the turn reached its limit of ${limit} consecutive continuations without a tool call`;
43
+ }
29
44
  // The summary is not the assistant's answer, so its live text goes nowhere.
30
45
  const discardedModelOutput = {
31
46
  writeTextDelta: () => undefined,
32
47
  writeReasoningDelta: () => undefined,
33
48
  };
34
49
  /**
35
- * The {@link AgentFrameworkRunner} of the built-in Stepwise loop.
50
+ * The {@link AgentFrameworkRunner} of the built-in agent loop.
36
51
  *
37
- * {@link createStepwiseAgentSessionFlow} creates one; construct it directly only to pass it to
52
+ * {@link createBuiltinAgentSessionFlow} creates one; construct it directly only to pass it to
38
53
  * `createAgentSessionFlow` with options of your own. Each `RunModel` makes at most one model call
39
54
  * through {@link AgentDefinition.model}, from committed state only: the stored messages, the results
40
55
  * of the previous model response from the turn's final records, and the instructions. The runner
@@ -46,34 +61,55 @@ const discardedModelOutput = {
46
61
  * tasks. Its {@link cancelToolTask} passes a task that `cancel_turn` abandoned to that source's optional
47
62
  * `cancelToolTask`.
48
63
  *
64
+ * With a media store, the runner shows images to the model. It then has `buildUserMessageWithImages` of
65
+ * {@link AgentFrameworkRunner}, as an own property, so `send_user_message` accepts images: the stored `user_text` item keeps their references after the text. A tool
66
+ * result item keeps the references of `AgentToolCall.images`. Before each model call, the runner reads the bytes of
67
+ * every image in the request with `loadImages` and passes them in `ModelRequest.imageBytes`; the model client
68
+ * converts each image to the provider's image part. A failed read fails that `RunModel` attempt, which retries. The
69
+ * model call that writes a context summary sees the text of the items only. Without a store the session refuses
70
+ * user images, and a tool source result's images are stored but not shown to the model. A function tool's images fail
71
+ * the call instead, without a retry. The class declares neither member, so a subclass may still define its own
72
+ * `buildUserMessageWithImages` or `mediaStore`; a subclass method stays in use with a store.
73
+ *
49
74
  * @example
50
75
  * ```ts
51
- * const runner = new StepwiseAgentRunner(definition);
76
+ * const runner = new BuiltinLoopAgentRunner(definition);
52
77
  * const flow = createAgentSessionFlow(runner, "SupportAgent", { builtInTools: ["ask_user"] });
53
78
  * ```
79
+ *
80
+ * It replaces `StepwiseAgentRunner`, which `@superdurable/dex-agents/stepwise` keeps as a deprecated alias.
81
+ *
82
+ * @since 0.5.0
54
83
  */
55
- export class StepwiseAgentRunner {
84
+ export class BuiltinLoopAgentRunner {
56
85
  /** The agent definition, frozen at construction. */
57
86
  definition;
58
87
  /** The runner honors {@link AgentTurnContext.interactionMode}, so plan mode can be enabled. */
59
88
  supportsPlanMode = true;
60
89
  functionToolsByName;
61
90
  toolSources;
62
- toolSourcesByName;
63
91
  maxModelCallsPerTurn;
64
- toolSourceListingTimeoutMs;
65
- /** The last listing of each source, so estimates count its tools without listing again. */
66
- lastListings = new Map();
92
+ // An ES private field, so it never collides with a subclass member.
93
+ #mediaStore;
67
94
  /**
68
95
  * Creates the runner and checks the definition.
69
96
  * @param definition - The agent definition.
70
- * @throws {TypeError} When the name, instructions, model, or tools are invalid, two tools share a
71
- * name, a function tool uses the name of an enabled built-in tool, `planModeEnabled` is set without
97
+ * @param mediaStore - The store that holds the session's images, such as `S3MediaStore`. With it, the runner
98
+ * accepts user messages with images and shows them and tool result images to the model, reading the bytes for
99
+ * each model call; stored messages hold only references. It also gets an own `buildUserMessageWithImages`, which
100
+ * stores user text and its images as one `user_text` item that holds the references, not the bytes, unless a
101
+ * subclass defines its own `buildUserMessageWithImages`, which then stays in use. Use the store
102
+ * the clients and tools upload to. Omit it to refuse user images and show tool result images to the model as text
103
+ * only. Since 0.5.0.
104
+ *
105
+ * Changed in 0.5.0: takes `mediaStore`.
106
+ * @throws {TypeError} When the name, instructions, model, tools, or `decideTurnContinuation` are invalid, two tools
107
+ * share a name, a function tool uses the name of an enabled built-in tool, `planModeEnabled` is set without
72
108
  * `write_todos`, `planningToolChoice` is neither `auto` nor `required`, or a tool source lacks a
73
109
  * {@link ToolSource} method.
74
110
  * @throws {RangeError} When `maxModelCallsPerTurn` is not a positive integer.
75
111
  */
76
- constructor(definition) {
112
+ constructor(definition, mediaStore = undefined) {
77
113
  if (typeof definition?.name !== "string" || definition.name.trim() === "") {
78
114
  throw new TypeError("AgentDefinition.name must be a non-blank string");
79
115
  }
@@ -86,6 +122,9 @@ export class StepwiseAgentRunner {
86
122
  if (definition.resolveModel !== undefined && typeof definition.resolveModel !== "function") {
87
123
  throw new TypeError("AgentDefinition.resolveModel must be a function");
88
124
  }
125
+ if (definition.decideTurnContinuation !== undefined && typeof definition.decideTurnContinuation !== "function") {
126
+ throw new TypeError("AgentDefinition.decideTurnContinuation must be a function");
127
+ }
89
128
  const enabledBuiltInTools = new Set(definition.builtInTools ?? []);
90
129
  const functionToolsByName = new Map();
91
130
  for (const tool of definition.tools ?? []) {
@@ -100,28 +139,17 @@ export class StepwiseAgentRunner {
100
139
  }
101
140
  functionToolsByName.set(tool.name, tool);
102
141
  }
103
- const toolSources = [...(definition.toolSources ?? [])];
104
- for (const source of toolSources) {
105
- const missing = toolSourceMethods.find((method) => typeof source?.[method] !== "function");
106
- if (missing !== undefined) {
107
- throw new TypeError(`tool source ${String(source)} has no ${missing} method`);
108
- }
109
- }
110
- const toolSourcesByName = new Map();
111
- for (const source of toolSources) {
112
- if (typeof source.sourceName !== "string" || source.sourceName.trim() === "") {
113
- throw new TypeError(`tool source ${String(source)} needs a non-blank sourceName`);
114
- }
115
- if (toolSourcesByName.has(source.sourceName)) {
116
- throw new TypeError(`two tool sources are named ${source.sourceName}`);
117
- }
118
- toolSourcesByName.set(source.sourceName, source);
119
- }
120
- refuseDeclaredToolNameCollisions(toolSources, new Set(functionToolsByName.keys()), enabledBuiltInTools);
121
142
  const toolSourceListingTimeoutMs = definition.toolSourceListingTimeoutMs ?? defaultToolSourceListingTimeoutMs;
122
143
  if (!(toolSourceListingTimeoutMs > 0)) {
123
144
  throw new RangeError("AgentDefinition.toolSourceListingTimeoutMs must be positive");
124
145
  }
146
+ const toolSources = new ToolSourceRouter(definition.toolSources ?? [], {
147
+ sourceNameKey: toolSourceNameKey,
148
+ localToolNames: new Set(functionToolsByName.keys()),
149
+ localToolDescription: "a function tool",
150
+ builtInToolNames: enabledBuiltInTools,
151
+ listingTimeoutMs: toolSourceListingTimeoutMs,
152
+ });
125
153
  if (definition.planModeEnabled === true && !enabledBuiltInTools.has("write_todos")) {
126
154
  throw new TypeError("planModeEnabled requires the write_todos built-in tool");
127
155
  }
@@ -136,9 +164,14 @@ export class StepwiseAgentRunner {
136
164
  this.definition = Object.freeze({ ...definition });
137
165
  this.functionToolsByName = functionToolsByName;
138
166
  this.toolSources = toolSources;
139
- this.toolSourcesByName = toolSourcesByName;
140
- this.toolSourceListingTimeoutMs = toolSourceListingTimeoutMs;
141
167
  this.maxModelCallsPerTurn = maxModelCallsPerTurn;
168
+ this.#mediaStore = mediaStore;
169
+ if (mediaStore !== undefined) {
170
+ // The session accepts images only from a runner with this method; the class leaves it undeclared for subclasses.
171
+ const runner = this;
172
+ // A subclass's own method is already on the prototype chain, and it stays in use.
173
+ runner.buildUserMessageWithImages ??= (text, images) => toBuiltinLoopFrameworkMessage({ type: "user_text", text, images: [...images] });
174
+ }
142
175
  }
143
176
  /**
144
177
  * Converts user text into a stored `user_text` item.
@@ -146,31 +179,76 @@ export class StepwiseAgentRunner {
146
179
  * @returns The message with role `user`.
147
180
  */
148
181
  buildUserMessage(text) {
149
- return toStepwiseFrameworkMessage({ type: "user_text", text });
182
+ return toBuiltinLoopFrameworkMessage({ type: "user_text", text });
150
183
  }
151
184
  /**
152
185
  * Makes the turn's next model call.
153
186
  * @param turn - The turn context.
154
187
  * @returns {@link TurnCompleted} with the results and final answer not stored yet, or
155
- * {@link ToolCallsRequested} with the previous results, this response, and its tool calls.
188
+ * {@link ToolCallsRequested} with the previous results, this response, and its tool calls. When
189
+ * {@link AgentDefinition.decideTurnContinuation} returns `"end"` after tool calls, a {@link TurnCompleted} with only
190
+ * the results and zero usage, without a model call. When it continues after tool calls, its message follows the
191
+ * results in the request and in the stored messages. When it continues after an answer, a {@link TurnContinued}
192
+ * with the previous results, this response, and the message as user text, or a {@link TurnCompleted} with
193
+ * {@link TurnCompleted.turnError} once {@link maxConsecutiveTurnContinuations} is exceeded.
156
194
  * @throws {TypeError} When a function tool uses the name of an enabled built-in tool, or a tool source lists a
157
195
  * name that repeats a function tool, an enabled built-in tool, or another source's tool. No model request is
158
- * made.
159
- * @throws Any error of the model call or of a tool source's `listTools`, which retries the `RunModel` attempt.
196
+ * made. Also when `decideTurnContinuation` returns another value.
197
+ * @throws Any error of the model call, of the media store, of a tool source's `listTools`, or of
198
+ * `decideTurnContinuation`, which retries the `RunModel` attempt.
160
199
  */
161
200
  async runModelStep(turn) {
162
201
  this.requireNoBuiltInToolNameClash(turn);
163
- const modelCallCount = readModelCallCount(turn.frameworkRunState);
202
+ const runState = readBuiltinLoopRunState(turn.frameworkRunState);
203
+ const modelCallCount = runState.modelCalls;
164
204
  const unstoredToolResults = buildUnstoredToolResults(turn);
205
+ const unstoredItems = [...unstoredToolResults];
206
+ if (unstoredToolResults.length > 0 && this.definition.decideTurnContinuation !== undefined) {
207
+ const decision = await this.decideTurnContinuation(turn, readLatestModelOutput(turn), unstoredToolResults);
208
+ if (decision === "end") {
209
+ return {
210
+ kind: "completed",
211
+ newMessages: unstoredToolResults.map(toBuiltinLoopFrameworkMessage),
212
+ usage: emptyAgentUsage(),
213
+ };
214
+ }
215
+ if (decision !== "default") {
216
+ unstoredItems.push({ type: "user_text", text: decision.message });
217
+ }
218
+ }
165
219
  const isToolBudgetSpent = modelCallCount >= this.maxModelCallsPerTurn;
166
- const sourceTools = isToolBudgetSpent || turn.interactionMode === "planning" ? new Map() : await this.listSourceTools(turn);
167
- const request = await this.buildModelRequest(turn, unstoredToolResults, modelCallCount, isToolBudgetSpent, sourceTools);
168
- const response = await this.resolveModel(turn.sessionSettings).callModel(request, turn.events);
220
+ const sourceTools = isToolBudgetSpent || turn.interactionMode === "planning"
221
+ ? new Map()
222
+ : await this.toolSources.listSourceTools(turn.enabledBuiltInTools ?? new Set());
223
+ const request = await this.buildModelRequest(turn, unstoredItems, modelCallCount, isToolBudgetSpent, sourceTools);
224
+ const response = await this.resolveModel(turn.sessionSettings).callModel(await this.addImageBytes(request), turn.events);
169
225
  requireModelResponse(response);
170
226
  const usage = { ...response.usage, modelCalls: 1 };
171
- const newMessages = [...unstoredToolResults, ...response.outputItems].map(toStepwiseFrameworkMessage);
227
+ const newMessages = [...unstoredItems, ...response.outputItems].map(toBuiltinLoopFrameworkMessage);
172
228
  if (response.toolCalls.length === 0) {
173
- return { kind: "completed", newMessages, usage };
229
+ const decision = await this.decideTurnContinuation(turn, response.outputItems, []);
230
+ if (decision === "end" || decision === "default") {
231
+ return { kind: "completed", newMessages, usage };
232
+ }
233
+ const consecutiveContinuations = runState.consecutiveContinuations + 1;
234
+ if (consecutiveContinuations > maxConsecutiveTurnContinuations) {
235
+ return {
236
+ kind: "completed",
237
+ newMessages,
238
+ usage,
239
+ turnError: buildTurnContinuationLimitError(maxConsecutiveTurnContinuations),
240
+ };
241
+ }
242
+ return {
243
+ kind: "turn_continued",
244
+ frameworkRunState: serializeJsonWithSpacedSeparators({
245
+ model_calls: modelCallCount + 1,
246
+ consecutive_continuations: consecutiveContinuations,
247
+ }),
248
+ // The message makes the next request end with user text, as every provider requires.
249
+ newMessages: [...newMessages, toBuiltinLoopFrameworkMessage({ type: "user_text", text: decision.message })],
250
+ usage,
251
+ };
174
252
  }
175
253
  if (isToolBudgetSpent) {
176
254
  // The model asked for tools although none were offered; end the turn so it cannot loop.
@@ -182,7 +260,7 @@ export class StepwiseAgentRunner {
182
260
  }));
183
261
  return {
184
262
  kind: "completed",
185
- newMessages: [...newMessages, ...notRunResults.map(toStepwiseFrameworkMessage)],
263
+ newMessages: [...newMessages, ...notRunResults.map(toBuiltinLoopFrameworkMessage)],
186
264
  usage,
187
265
  };
188
266
  }
@@ -193,11 +271,7 @@ export class StepwiseAgentRunner {
193
271
  toolCalls: response.toolCalls.map((toolCall) => {
194
272
  const listed = sourceTools.get(toolCall.toolName);
195
273
  if (listed !== undefined) {
196
- const built = listed.source.deferredToolCall(turn, { ...toolCall, listedTool: listed.tool });
197
- return {
198
- ...built,
199
- toolSourceReference: writeToolSourceEnvelope(listed.source.sourceName, built.toolSourceReference),
200
- };
274
+ return this.toolSources.buildDeferredToolCall(turn, listed, toolCall);
201
275
  }
202
276
  return {
203
277
  toolCallId: toolCall.toolCallId,
@@ -210,14 +284,22 @@ export class StepwiseAgentRunner {
210
284
  };
211
285
  }
212
286
  /**
213
- * Runs one function tool call after checking its arguments against the tool's schema, or passes a call that a
287
+ * Runs one function tool call after converting its arguments with the tool's `prepareArguments`, when it has
288
+ * one, and checking them against the tool's schema, or passes a call that a
214
289
  * tool source built in `runModelStep` to that source, with the source's own `toolSourceReference` and without a
215
290
  * listing. Any other name is an unknown tool and never reaches a source.
291
+ * A function tool's {@link ToolCallContext.mediaStore} is this runner's store.
292
+ *
293
+ * Changed in 0.5.0: a function tool receives the media store and may return a {@link FunctionToolResult} with
294
+ * images.
216
295
  * @param call - The deferred call.
217
- * @returns The output: a `string` result unchanged, any other result as JSON; the tool's
218
- * {@link ToolInputRequested}; or a source's outcome, such as {@link ToolTaskStarted}.
296
+ * @returns The output: a `string` result unchanged, a {@link FunctionToolResult} as its output and images, any
297
+ * other result as JSON; the tool's {@link ToolInputRequested}; or a source's outcome, such as
298
+ * {@link ToolTaskStarted}.
219
299
  * @throws {ToolExecutionError} For an unknown tool, arguments that are not a JSON object or break the
220
- * schema, or a `ToolExecutionError` from the tool or its source.
300
+ * schema, a non-cancellation error or a thenable result of the tool's `prepareArguments`, a
301
+ * `ToolExecutionError` from the tool or its source, or a function tool that returned images while this runner has
302
+ * no media store.
221
303
  * @throws {RangeError} When a source built the call but the definition no longer has it, so the effect decides
222
304
  * the outcome.
223
305
  * @throws Any other error of the tool source.
@@ -226,13 +308,12 @@ export class StepwiseAgentRunner {
226
308
  const toolCall = call.toolCall;
227
309
  const tool = this.functionToolsByName.get(toolCall.toolName);
228
310
  if (tool === undefined) {
229
- const routed = this.routeToToolSource(toolCall);
230
- if (routed === undefined) {
311
+ if (!this.toolSources.isSourceToolCall(toolCall)) {
231
312
  throw new ToolExecutionError(buildUnknownToolError(toolCall.toolName));
232
313
  }
233
- return routed.source.executeDeferredToolCall({ ...call, toolCall: withToolSourceReference(toolCall, routed.reference) });
314
+ return this.toolSources.executeDeferredToolCall(call);
234
315
  }
235
- const toolArguments = parseToolArguments(tool.name, toolCall.arguments);
316
+ const toolArguments = prepareToolArguments(tool, toolCall.arguments, call.cancellationSignal);
236
317
  const violation = findJsonSchemaViolation(toolArguments, tool.parameters, "arguments");
237
318
  if (violation !== undefined) {
238
319
  throw new ToolExecutionError(`invalid arguments for ${tool.name}: ${violation}`);
@@ -243,12 +324,14 @@ export class StepwiseAgentRunner {
243
324
  toolCallId: toolCall.toolCallId,
244
325
  idempotencyKey: call.idempotencyKey,
245
326
  attempt: call.attempt,
327
+ effect: toolCall.effect,
246
328
  events: call.events,
247
329
  cancellationSignal: call.cancellationSignal,
248
330
  deadline: call.deadline,
249
331
  inputAnswers: call.inputAnswers ?? [],
250
332
  inputContinuation: toolCall.inputContinuation ?? null,
251
333
  heartbeat: () => call.heartbeat(),
334
+ ...(this.#mediaStore === undefined ? {} : { mediaStore: this.#mediaStore }),
252
335
  };
253
336
  const result = await tool.execute(toolArguments, context);
254
337
  if (typeof result === "string") {
@@ -257,8 +340,20 @@ export class StepwiseAgentRunner {
257
340
  if (result?.kind === "tool_input_requested") {
258
341
  return result;
259
342
  }
343
+ if (isFunctionToolResult(result)) {
344
+ return this.toToolExecutionResult(tool.name, result);
345
+ }
260
346
  return { output: serializeJsonWithSpacedSeparators(result === undefined ? null : result) };
261
347
  }
348
+ toToolExecutionResult(toolName, result) {
349
+ if (result.images === undefined || result.images.length === 0) {
350
+ return { output: result.output };
351
+ }
352
+ if (this.#mediaStore === undefined) {
353
+ throw new ToolExecutionError(`function tool ${toolName} returned images, but the runner has no media store`);
354
+ }
355
+ return { output: result.output, images: result.images };
356
+ }
262
357
  /**
263
358
  * Polls a tool source's task once, through the source that built the call.
264
359
  * @param poll - The call's record from `PollToolTask`.
@@ -268,11 +363,22 @@ export class StepwiseAgentRunner {
268
363
  * @throws Any error of the source.
269
364
  */
270
365
  async pollToolTask(poll) {
271
- const routed = this.routeToToolSource(poll.toolCall);
272
- if (routed === undefined) {
273
- throw new RangeError(`no tool source built the call of tool ${poll.toolCall.toolName}`);
366
+ return this.toolSources.pollToolTask(poll);
367
+ }
368
+ /**
369
+ * Returns a tool source's options for one call's tool Step. Function tools and sources without
370
+ * `toolCallStepOptions` keep the tool Steps' defaults.
371
+ * @param toolCall - The record whose tool Step starts.
372
+ * @returns The source's options, or `undefined` for the defaults.
373
+ * @throws {RangeError} When a source built the call but the definition no longer has it; the session then uses
374
+ * the defaults.
375
+ * @since 0.4.0
376
+ */
377
+ toolCallStepOptions(toolCall) {
378
+ if (this.functionToolsByName.has(toolCall.toolName)) {
379
+ return undefined;
274
380
  }
275
- return routed.source.pollToolTask({ ...poll, toolCall: withToolSourceReference(poll.toolCall, routed.reference) });
381
+ return this.toolSources.toolCallStepOptions(toolCall);
276
382
  }
277
383
  /**
278
384
  * Cancels a tool source's task through the source that built the call; a source without `cancelToolTask` leaves
@@ -284,14 +390,7 @@ export class StepwiseAgentRunner {
284
390
  * @throws Any error of the source.
285
391
  */
286
392
  async cancelToolTask(cancellation) {
287
- const routed = this.routeToToolSource(cancellation.toolCall);
288
- if (routed === undefined) {
289
- throw new RangeError(`no tool source built the call of tool ${cancellation.toolCall.toolName}`);
290
- }
291
- await routed.source.cancelToolTask?.({
292
- ...cancellation,
293
- toolCall: withToolSourceReference(cancellation.toolCall, routed.reference),
294
- });
393
+ await this.toolSources.cancelToolTask(cancellation);
295
394
  }
296
395
  /**
297
396
  * Returns the results of the turn's tool calls that are not stored yet.
@@ -299,7 +398,7 @@ export class StepwiseAgentRunner {
299
398
  * @returns One `tool_result` message per unstored result, in deferral order.
300
399
  */
301
400
  buildUnfinishedTurnMessages(turn) {
302
- return buildUnstoredToolResults(turn).map(toStepwiseFrameworkMessage);
401
+ return buildUnstoredToolResults(turn).map(toBuiltinLoopFrameworkMessage);
303
402
  }
304
403
  /**
305
404
  * Estimates the model input of the next `runModelStep` with the model client.
@@ -309,10 +408,10 @@ export class StepwiseAgentRunner {
309
408
  * when it has one, else from the runner's last listing, as in the Python core.
310
409
  */
311
410
  async estimateInputTokens(turn) {
312
- const modelCallCount = readModelCallCount(turn.frameworkRunState);
411
+ const modelCallCount = readBuiltinLoopRunState(turn.frameworkRunState).modelCalls;
313
412
  const isToolBudgetSpent = modelCallCount >= this.maxModelCallsPerTurn;
314
- const request = await this.buildModelRequest(turn, buildUnstoredToolResults(turn), modelCallCount, isToolBudgetSpent, this.readCachedSourceTools());
315
- return this.resolveModel(turn.sessionSettings).estimateInputTokens(request);
413
+ const request = await this.buildModelRequest(turn, buildUnstoredToolResults(turn), modelCallCount, isToolBudgetSpent, this.toolSources.readCachedSourceTools());
414
+ return this.resolveModel(turn.sessionSettings).estimateInputTokens(this.keepShownImagesOnly(request));
316
415
  }
317
416
  /**
318
417
  * Chooses the last message to summarize, at a boundary between model responses.
@@ -370,7 +469,11 @@ export class StepwiseAgentRunner {
370
469
  request.inputTokenBudget - (this.definition.modelSettings?.maxOutputTokens ?? 0);
371
470
  if (budget !== undefined) {
372
471
  // Estimate by parts, then leave out the oldest items up to the first cut that fits.
373
- const itemTokens = rangeItems.map((item) => model.estimateInputTokens({ ...this.buildSummaryRequest(request, [], 0), instructions: "", inputItems: [item] }));
472
+ const itemTokens = rangeItems.map((item) => model.estimateInputTokens({
473
+ ...this.buildSummaryRequest(request, [], 0),
474
+ instructions: "",
475
+ inputItems: stripImages([item]),
476
+ }));
374
477
  let tokens = model.estimateInputTokens(this.buildSummaryRequest(request, [], rangeItems.length)) +
375
478
  itemTokens.reduce((total, itemToken) => total + itemToken, 0);
376
479
  // A cut never splits a model response or separates a tool call from its result.
@@ -397,7 +500,8 @@ export class StepwiseAgentRunner {
397
500
  if (omittedCount > 0) {
398
501
  items.push({ type: "user_text", text: buildContextSummaryOmissionNote(omittedCount) });
399
502
  }
400
- items.push(...rangeItems, { type: "user_text", text: contextSummaryRequest });
503
+ // The summary call reads no image bytes, so it sees each item's text only.
504
+ items.push(...stripImages(rangeItems), { type: "user_text", text: contextSummaryRequest });
401
505
  return {
402
506
  instructions: contextSummaryInstructions,
403
507
  inputItems: pairToolCallsWithResults(items),
@@ -413,7 +517,7 @@ export class StepwiseAgentRunner {
413
517
  cancellationSignal: request.cancellationSignal,
414
518
  };
415
519
  }
416
- async buildModelRequest(turn, unstoredToolResults, modelCallCount, isToolBudgetSpent, sourceTools) {
520
+ async buildModelRequest(turn, unstoredItems, modelCallCount, isToolBudgetSpent, sourceTools) {
417
521
  const contextSummary = turn.contextSummary ?? null;
418
522
  const summaryItems = contextSummary === null ? [] : [{ type: "user_text", text: buildContextSummaryNote(contextSummary) }];
419
523
  let toolChoice = isToolBudgetSpent ? "none" : "auto";
@@ -426,7 +530,7 @@ export class StepwiseAgentRunner {
426
530
  inputItems: pairToolCallsWithResults([
427
531
  ...summaryItems,
428
532
  ...readStoredInputItemsWithPlanNote(turn),
429
- ...unstoredToolResults,
533
+ ...unstoredItems,
430
534
  ]),
431
535
  tools: isToolBudgetSpent ? [] : this.listOfferedTools(turn, sourceTools),
432
536
  toolChoice,
@@ -441,7 +545,7 @@ export class StepwiseAgentRunner {
441
545
  }
442
546
  return this.resolveModel(turn.sessionSettings).estimateInputTokens({
443
547
  instructions: "",
444
- inputItems: items,
548
+ inputItems: this.#mediaStore === undefined ? stripImages(items) : items,
445
549
  tools: [],
446
550
  toolChoice: "none",
447
551
  settings: this.definition.modelSettings ?? {},
@@ -449,6 +553,21 @@ export class StepwiseAgentRunner {
449
553
  cancellationSignal: turn.cancellationSignal,
450
554
  });
451
555
  }
556
+ /** Returns the request with the bytes of its images, read from the store right now. */
557
+ async addImageBytes(request) {
558
+ if (this.#mediaStore === undefined) {
559
+ return this.keepShownImagesOnly(request);
560
+ }
561
+ const references = request.inputItems.flatMap((item) => item.type === "model_output" ? [] : (item.images ?? []));
562
+ if (references.length === 0) {
563
+ return request;
564
+ }
565
+ return { ...request, imageBytes: await loadImages(this.#mediaStore, references) };
566
+ }
567
+ keepShownImagesOnly(request) {
568
+ // Without a store the model sees no image, such as a tool source's.
569
+ return this.#mediaStore === undefined ? { ...request, inputItems: stripImages(request.inputItems) } : request;
570
+ }
452
571
  /** A built-in enabled only in the session options must not share a function tool's name. */
453
572
  requireNoBuiltInToolNameClash(turn) {
454
573
  for (const builtInTool of turn.enabledBuiltInTools ?? []) {
@@ -457,75 +576,6 @@ export class StepwiseAgentRunner {
457
576
  }
458
577
  }
459
578
  }
460
- async listSourceTools(turn) {
461
- const enabledBuiltInTools = turn.enabledBuiltInTools ?? new Set();
462
- const functionToolNames = new Set(this.functionToolsByName.keys());
463
- const listed = new Map();
464
- for (const [index, source] of this.toolSources.entries()) {
465
- for (const tool of await this.listOneSource(index, source)) {
466
- refuseSourceToolName(tool.name, functionToolNames, enabledBuiltInTools, listed);
467
- listed.set(tool.name, { index, source, tool });
468
- }
469
- }
470
- return listed;
471
- }
472
- async listOneSource(index, source) {
473
- const listing = source.listTools();
474
- // A listing that outlives this call keeps running; nobody else reads its error.
475
- listing.catch(() => undefined);
476
- let timer;
477
- const timedOut = new Promise((resolve) => {
478
- timer = setTimeout(() => resolve("timed_out"), this.toolSourceListingTimeoutMs);
479
- });
480
- try {
481
- const tools = await Promise.race([listing, timedOut]);
482
- if (tools === "timed_out") {
483
- console.warn(`tool source ${index} listed nothing within ${this.toolSourceListingTimeoutMs} ms`);
484
- return [];
485
- }
486
- this.lastListings.set(index, tools);
487
- return tools;
488
- }
489
- catch (error) {
490
- console.warn(`tool source ${index} could not list its tools: ${error instanceof Error ? error.message : String(error)}`);
491
- return [];
492
- }
493
- finally {
494
- clearTimeout(timer);
495
- }
496
- }
497
- readCachedSourceTools() {
498
- const cached = new Map();
499
- for (const [index, source] of this.toolSources.entries()) {
500
- const tools = source.cachedTools?.() ?? this.lastListings.get(index) ?? [];
501
- for (const tool of tools) {
502
- if (!cached.has(tool.name)) {
503
- cached.set(tool.name, { index, source, tool });
504
- }
505
- }
506
- }
507
- return cached;
508
- }
509
- routeToToolSource(toolCall) {
510
- const envelope = readToolSourceEnvelope(toolCall.toolSourceReference);
511
- if (envelope !== undefined) {
512
- const source = this.toolSourcesByName.get(envelope.sourceName);
513
- if (source === undefined) {
514
- throw new RangeError(`tool source ${envelope.sourceName}, which built the call of tool ${toolCall.toolName}, is not configured on this Worker`);
515
- }
516
- return { source, reference: envelope.reference };
517
- }
518
- const reference = toolCall.toolSourceReference ?? undefined;
519
- if (reference === undefined || this.toolSources.length === 0) {
520
- return undefined;
521
- }
522
- const onlySource = this.toolSources.length === 1 ? this.toolSources[0] : undefined;
523
- if (onlySource !== undefined) {
524
- // A reference the source built itself, such as in a runner subclass.
525
- return { source: onlySource, reference };
526
- }
527
- throw new RangeError(`cannot tell which tool source built the call of ${toolCall.toolName}`);
528
- }
529
579
  listOfferedTools(turn, sourceTools) {
530
580
  const isPlanning = turn.interactionMode === "planning";
531
581
  const enabledBuiltInTools = turn.enabledBuiltInTools ?? new Set();
@@ -556,7 +606,7 @@ export class StepwiseAgentRunner {
556
606
  resolveModelToolPolicy(turn, toolName) {
557
607
  const tool = this.functionToolsByName.get(toolName);
558
608
  const isBuiltIn = turn.enabledBuiltInTools?.has(toolName) === true;
559
- if (tool === undefined && this.toolSources.length > 0 && !isBuiltIn) {
609
+ if (tool === undefined && this.toolSources.hasToolSources && !isBuiltIn) {
560
610
  return unlistedSourceToolPolicy;
561
611
  }
562
612
  const sessionPolicies = turn.toolPolicies ?? this.definition.toolPolicies ?? {};
@@ -601,6 +651,43 @@ export class StepwiseAgentRunner {
601
651
  }
602
652
  return resolved;
603
653
  }
654
+ /** Returns the hook's decision for one iteration, or `"default"` without a hook. */
655
+ async decideTurnContinuation(turn, modelOutput, toolResults) {
656
+ const decide = this.definition.decideTurnContinuation;
657
+ if (decide === undefined) {
658
+ return "default";
659
+ }
660
+ const decision = await decide({
661
+ flowId: turn.flowId,
662
+ turnId: turn.turnId,
663
+ sessionSettings: turn.sessionSettings ?? {},
664
+ modelOutput,
665
+ toolResults,
666
+ });
667
+ if (decision === "end" || decision === "default") {
668
+ return decision;
669
+ }
670
+ const continuation = decision;
671
+ if (typeof continuation === "object" &&
672
+ continuation !== null &&
673
+ continuation.action === "continue" &&
674
+ typeof continuation.message === "string" &&
675
+ continuation.message.trim() !== "") {
676
+ return { action: "continue", message: continuation.message };
677
+ }
678
+ throw new TypeError(`decideTurnContinuation of agent ${this.definition.name} must return "end", "default", or ` +
679
+ `{ action: "continue", message } with a non-blank message, not ${describeTurnContinuation(decision)}`);
680
+ }
681
+ }
682
+ /** Names an invalid hook result without its message text, which may hold application data. */
683
+ function describeTurnContinuation(decision) {
684
+ if (typeof decision === "string") {
685
+ return JSON.stringify(decision);
686
+ }
687
+ if (typeof decision === "object" && decision !== null) {
688
+ return `an object with action ${JSON.stringify(decision.action) ?? "undefined"}`;
689
+ }
690
+ return String(decision);
604
691
  }
605
692
  /** A planning turn without a draft drafts one first; feedback on a draft may only answer. */
606
693
  function mustWritePlanFirst(turn, modelCallCount) {
@@ -640,31 +727,100 @@ function buildUnstoredToolResults(turn) {
640
727
  toolCallId: record.toolCallId,
641
728
  toolName: record.toolName,
642
729
  output: describeToolCallOutcome(record),
730
+ ...((record.images ?? []).length === 0 ? {} : { images: record.images }),
643
731
  }));
644
732
  }
645
- function parseToolArguments(toolName, argumentsText) {
733
+ function stripImages(items) {
734
+ return items.map((item) => {
735
+ if (item.type === "model_output" || (item.images ?? []).length === 0) {
736
+ return item;
737
+ }
738
+ const { images: _images, ...withoutImages } = item;
739
+ return withoutImages;
740
+ });
741
+ }
742
+ function prepareToolArguments(tool, argumentsText, cancellationSignal) {
646
743
  let parsed;
647
744
  try {
648
745
  parsed = JSON.parse(argumentsText === "" ? "{}" : argumentsText);
649
746
  }
650
747
  catch {
651
748
  // A fixed text, because the two cores' JSON parsers word their errors differently.
652
- throw new ToolExecutionError(`invalid arguments for ${toolName}: arguments are not valid JSON`);
749
+ throw new ToolExecutionError(`invalid arguments for ${tool.name}: arguments are not valid JSON`);
750
+ }
751
+ if (tool.prepareArguments !== undefined) {
752
+ try {
753
+ parsed = tool.prepareArguments(parsed);
754
+ }
755
+ catch (error) {
756
+ if (isCancellation(error, cancellationSignal)) {
757
+ throw error;
758
+ }
759
+ // Any other error, so a failed conversion never retries or records outcome_unknown.
760
+ const message = error instanceof Error ? error.message : String(error);
761
+ throw new ToolExecutionError(`invalid arguments for ${tool.name}: ${message}`);
762
+ }
763
+ if (isThenable(parsed)) {
764
+ if (parsed instanceof Promise) {
765
+ observePromiseRejection(parsed);
766
+ }
767
+ throw new ToolExecutionError(`invalid arguments for ${tool.name}: argument conversion must be synchronous`);
768
+ }
653
769
  }
654
770
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
655
- throw new ToolExecutionError(`invalid arguments for ${toolName}: expected a JSON object`);
771
+ throw new ToolExecutionError(`invalid arguments for ${tool.name}: expected a JSON object`);
656
772
  }
657
773
  return parsed;
658
774
  }
659
- function readModelCallCount(frameworkRunState) {
775
+ /** Reads `then` without calling it; a throwing `then` getter counts as a thenable, so it is refused too. */
776
+ function isThenable(value) {
777
+ if ((typeof value !== "object" || value === null) && typeof value !== "function") {
778
+ return false;
779
+ }
780
+ try {
781
+ return typeof value.then === "function";
782
+ }
783
+ catch {
784
+ return true;
785
+ }
786
+ }
787
+ /** Observes a refused Promise's rejection through the native `then`, so a subclass's own `then` never runs. */
788
+ function observePromiseRejection(promise) {
789
+ try {
790
+ void Promise.prototype.then.call(promise, undefined, () => undefined);
791
+ }
792
+ catch {
793
+ // A subclass constructor may throw; the result is refused either way.
794
+ }
795
+ }
796
+ function isCancellation(error, cancellationSignal) {
797
+ if (cancellationSignal.aborted && error === cancellationSignal.reason) {
798
+ return true;
799
+ }
800
+ return error?.name === "AbortError";
801
+ }
802
+ function readBuiltinLoopRunState(frameworkRunState) {
660
803
  if (frameworkRunState === null) {
661
- return 0;
804
+ return { modelCalls: 0, consecutiveContinuations: 0 };
662
805
  }
663
806
  const parsed = JSON.parse(frameworkRunState);
664
- if (typeof parsed.model_calls !== "number" || !Number.isSafeInteger(parsed.model_calls)) {
665
- throw new TypeError("FrameworkRunState is not a Stepwise run state");
666
- }
667
- return parsed.model_calls;
807
+ const consecutiveContinuations = parsed.consecutive_continuations ?? 0;
808
+ if (typeof parsed.model_calls !== "number" ||
809
+ !Number.isSafeInteger(parsed.model_calls) ||
810
+ typeof consecutiveContinuations !== "number" ||
811
+ !Number.isSafeInteger(consecutiveContinuations)) {
812
+ throw new TypeError("FrameworkRunState is not a built-in loop run state");
813
+ }
814
+ return { modelCalls: parsed.model_calls, consecutiveContinuations };
815
+ }
816
+ /** The output items of the turn's latest model response: the turn's trailing stored `model_output` items. */
817
+ function readLatestModelOutput(turn) {
818
+ const items = turn.messages.filter((message) => message.turnId === turn.turnId).map(readStoredModelInputItem);
819
+ let start = items.length;
820
+ while (start > 0 && items[start - 1]?.type === "model_output") {
821
+ start -= 1;
822
+ }
823
+ return items.slice(start);
668
824
  }
669
825
  function requireModelResponse(response) {
670
826
  if (!Array.isArray(response?.outputItems) ||
@@ -714,64 +870,15 @@ function modelRequestMetadata(turn) {
714
870
  attempt: turn.attempt,
715
871
  };
716
872
  }
717
- function refuseSourceToolName(toolName, functionToolNames, builtInTools, otherSourceTools) {
718
- if (builtInTools.has(toolName)) {
719
- throw new TypeError(`tool ${toolName} uses the name of an enabled built-in tool`);
720
- }
721
- if (functionToolNames.has(toolName)) {
722
- throw new TypeError(`tool ${toolName} of a tool source uses the name of a function tool`);
723
- }
724
- if (otherSourceTools.has(toolName)) {
725
- throw new TypeError(`two tool sources list a tool named ${toolName}`);
726
- }
727
- }
728
- function refuseDeclaredToolNameCollisions(toolSources, functionToolNames, builtInTools) {
729
- const declared = new Map();
730
- for (const source of toolSources) {
731
- const names = [...(source.declaredToolNames?.() ?? [])].sort();
732
- for (const toolName of names) {
733
- refuseSourceToolName(toolName, functionToolNames, builtInTools, declared);
734
- }
735
- for (const toolName of names) {
736
- declared.set(toolName, source);
737
- }
738
- }
739
- }
740
873
  /** Error of a call to a tool that no function tool or source listing offers; the same text as the Python core. */
741
874
  export function buildUnknownToolError(toolName) {
742
875
  return `unknown tool ${toolName}`;
743
876
  }
744
877
  /** The runner's `toolSourceReference`: the source's name and its own reference, as JSON text. */
745
878
  export function writeToolSourceEnvelope(sourceName, reference) {
746
- return JSON.stringify({ [toolSourceNameKey]: sourceName, [toolSourceReferenceKey]: reference ?? null });
879
+ return writeRouterToolSourceEnvelope(toolSourceNameKey, sourceName, reference);
747
880
  }
748
881
  export function readToolSourceEnvelope(toolSourceReference) {
749
- if (toolSourceReference === undefined || toolSourceReference === null) {
750
- return undefined;
751
- }
752
- let envelope;
753
- try {
754
- envelope = JSON.parse(toolSourceReference);
755
- }
756
- catch {
757
- return undefined;
758
- }
759
- if (typeof envelope !== "object" || envelope === null || Array.isArray(envelope)) {
760
- return undefined;
761
- }
762
- const sourceName = envelope[toolSourceNameKey];
763
- const reference = envelope[toolSourceReferenceKey];
764
- if (typeof sourceName !== "string") {
765
- return undefined;
766
- }
767
- if (reference !== null && reference !== undefined && typeof reference !== "string") {
768
- return undefined;
769
- }
770
- return { sourceName, reference: reference ?? undefined };
771
- }
772
- /** The record as the source built it: its own reference instead of the runner's envelope. */
773
- function withToolSourceReference(toolCall, reference) {
774
- const { toolSourceReference: _envelope, ...rest } = toolCall;
775
- return (reference === undefined ? rest : { ...rest, toolSourceReference: reference });
882
+ return readRouterToolSourceEnvelope(toolSourceNameKey, toolSourceReference);
776
883
  }
777
- //# sourceMappingURL=stepwise-agent-runner.js.map
884
+ //# sourceMappingURL=builtin-loop-agent-runner.js.map