@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,1272 @@
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 { ExecuteFailure, StepMovement, int64Codec, StepRef, StepAttemptProgress, Timer, Wait, deadEnd, forceFail, goTo, goToMany, stringCodec, voidCodec, } from "@superdurable/dex";
9
+ import { buildPlanClearedOutput, buildPlanUpdatedOutput, isBlankText, validateAskUserArguments, validateDurableWaitArguments, validateWriteTodosArguments, writeTodosCalledMoreThanOnce, } from "./agent-built-in-tools.js";
10
+ import { isCompactionTriggered, isContextCompactionRunner, readRetainedTokenBudget, } from "./agent-context-compaction.js";
11
+ import { ToolExecutionError, } from "./agent-framework-runner.js";
12
+ import { buildPlanExecutionMessage, planningTurnToolRejection, planStatusAfterWrite } from "./agent-plans.js";
13
+ import { agentSessionStartInputCodec, turnFailureRouteInputCodec } from "./agent-session-codecs.js";
14
+ import { messageStepOptions, minuteMs, modelCallStepOptions, recoveryRetry, syncDurability, } from "./agent-session-step-options.js";
15
+ import { agentSessionStepTypes, allPendingMessages, cancelledToolCall, isAbandonedToolTask, isCancellableByTurnControl, readRecoveryDetail, recoveryErrorDetail, toRunnerToolCall, toolStepTypeForEffect, ToolStepHeartbeats, writePlanUpdated, } from "./agent-session-transitions.js";
16
+ import { messageSequenceKey, planExecutionKey, } from "./agent-session-persistence.js";
17
+ import { checkStoredSessionSettings } from "./agent-session-settings.js";
18
+ import { approvalDeadlinePassed, blankToolTaskId, buildTurnFailureNote, emptyAgentUsage, invalidToolTaskPollInterval, isFinalToolCallStatus, missingToolTaskPoller, toolCallNotRunErrorPrefix, toolTaskAbandonedError, } from "./agent-session-values.js";
19
+ /** Longest wait between two polls of a tool task, as in the Python core; a longer interval is shortened. */
20
+ const maximumToolTaskPollIntervalSeconds = 86_400;
21
+ // An int64 Value, as the Python core's Step[int] encodes it.
22
+ const expectedResultCountCodec = {
23
+ typeName: "ExpectedResultCount",
24
+ wireKind: "int64",
25
+ encode: (value) => int64Codec.encode(BigInt(requirePositiveResultCount(value))),
26
+ decode: (value) => requirePositiveResultCount(Number(int64Codec.decode(value))),
27
+ };
28
+ /** Start of the Flow failure reason when `StartAgentSession` refuses the start input's settings. */
29
+ export const invalidStartSessionSettingsReason = "dex-agents: the start input's session_settings are not valid session settings";
30
+ export class StartAgentSessionStep {
31
+ transitions;
32
+ inputCodec = agentSessionStartInputCodec;
33
+ constructor(transitions) {
34
+ this.transitions = transitions;
35
+ }
36
+ getStepType() {
37
+ return agentSessionStepTypes.startAgentSession;
38
+ }
39
+ getStepOptions() {
40
+ return { ...syncDurability, executeLoadAttributeMaps: [this.transitions.persistence.agentMessages] };
41
+ }
42
+ execute(context, input) {
43
+ const persistence = this.transitions.persistence;
44
+ const state = {
45
+ status: "waiting_for_user_message",
46
+ turnCount: 0,
47
+ activeTurnId: null,
48
+ nextMessageSequence: 1,
49
+ lastTurnError: null,
50
+ acceptsTurnControl: false,
51
+ };
52
+ const sessionSettings = input.sessionSettings ?? null;
53
+ if (sessionSettings !== null) {
54
+ try {
55
+ checkStoredSessionSettings(sessionSettings);
56
+ }
57
+ catch (error) {
58
+ // Every retry would fail the same way, so the session fails now.
59
+ return forceFail(`${invalidStartSessionSettingsReason}: ${error instanceof Error ? error.message : String(error)}`);
60
+ }
61
+ if (Object.keys(sessionSettings).length > 0) {
62
+ persistence.sessionSettings.set(context, sessionSettings);
63
+ }
64
+ }
65
+ this.transitions.writeAgentState(context, state);
66
+ persistence.agentUsage.set(context, emptyAgentUsage());
67
+ persistence.pendingInterrupts.set(context, []);
68
+ const initialMessage = input.initialMessage;
69
+ if (initialMessage === null || isBlankText(initialMessage)) {
70
+ return this.transitions.goToAwaitUserMessage();
71
+ }
72
+ return this.transitions.beginTurn(context, state, [{ text: initialMessage }], this.getStepType(), "chat", undefined, false, sessionSettings);
73
+ }
74
+ }
75
+ /** Waits for one queued user message, or an approved plan execution, and starts its turn. */
76
+ export class AwaitUserMessageStep {
77
+ transitions;
78
+ inputCodec = voidCodec;
79
+ constructor(transitions) {
80
+ this.transitions = transitions;
81
+ }
82
+ getStepType() {
83
+ return agentSessionStepTypes.awaitUserMessage;
84
+ }
85
+ getStepOptions() {
86
+ // The lock keeps a turn from starting while cancel_turn decides about the last one.
87
+ // Storing the turn's user messages may move the oldest window messages into a chunk.
88
+ return {
89
+ ...syncDurability,
90
+ executeLoadAttributeMaps: [this.transitions.persistence.agentMessages],
91
+ executeLockAttributes: [this.transitions.persistence.turnCancellationRequest.lock()],
92
+ };
93
+ }
94
+ waitFor(context) {
95
+ const persistence = this.transitions.persistence;
96
+ // Dex selects Channel conditions in argument order: a steer, a queued message, a plan.
97
+ const conditions = [
98
+ persistence.turnControlRequests.range(1, allPendingMessages),
99
+ persistence.queuedUserMessages.forOne(),
100
+ ];
101
+ const plan = this.transitions.readOptionalAgentPlan(context);
102
+ // Keyed by revision, so an approval of an earlier revision never starts a turn.
103
+ if (plan !== null && isExecutablePlanStatus(plan.status)) {
104
+ conditions.push(persistence.planExecutions.forOne(planExecutionKey(plan.revision)));
105
+ }
106
+ return Wait.anyOf(...conditions);
107
+ }
108
+ execute(context) {
109
+ const transitions = this.transitions;
110
+ const persistence = transitions.persistence;
111
+ if (context.channelResults(persistence.turnControlRequests).length > 0) {
112
+ return transitions.goToApplyTurnControl(context);
113
+ }
114
+ const [message] = context.channelResults(persistence.queuedUserMessages);
115
+ if (message !== undefined) {
116
+ if (isBlankText(message.text)) {
117
+ return transitions.goToAwaitUserMessage();
118
+ }
119
+ return this.beginUserMessageTurn(context, message);
120
+ }
121
+ const plan = transitions.readOptionalAgentPlan(context);
122
+ const [execution] = plan === null ? [] : context.channelResults(persistence.planExecutions, planExecutionKey(plan.revision));
123
+ if (execution === undefined) {
124
+ return transitions.goToAwaitUserMessage();
125
+ }
126
+ if (persistence.pendingPlanExecution.get(context) !== undefined) {
127
+ persistence.pendingPlanExecution.delete(context);
128
+ }
129
+ if (plan === null || execution.revision !== plan.revision || !isExecutablePlanStatus(plan.status)) {
130
+ return transitions.goToAwaitUserMessage();
131
+ }
132
+ const wasPlanDraft = plan.status !== "active";
133
+ const executedPlan = wasPlanDraft
134
+ ? { ...plan, status: "active", updatedAt: new Date().toISOString() }
135
+ : plan;
136
+ if (wasPlanDraft) {
137
+ persistence.agentPlan.set(context, executedPlan);
138
+ }
139
+ return transitions.beginTurn(context, transitions.readAgentState(context), [{ text: buildPlanExecutionMessage(plan.revision) }], this.getStepType(), "executing", executedPlan, wasPlanDraft);
140
+ }
141
+ beginUserMessageTurn(context, message) {
142
+ const transitions = this.transitions;
143
+ return transitions.beginTurn(context, transitions.readAgentState(context), [message], this.getStepType(), transitions.decideInteractionMode(context, [message]));
144
+ }
145
+ }
146
+ /** Plan statuses that `execute_plan` accepts and `AwaitUserMessage` waits for. */
147
+ export function isExecutablePlanStatus(status) {
148
+ return status === "draft" || status === "active";
149
+ }
150
+ /** Makes the turn's next model call and fans the requested tool calls out to parallel Steps. */
151
+ export class RunModelStep {
152
+ transitions;
153
+ inputCodec = voidCodec;
154
+ constructor(transitions) {
155
+ this.transitions = transitions;
156
+ }
157
+ getStepType() {
158
+ return agentSessionStepTypes.runModel;
159
+ }
160
+ getStepOptions() {
161
+ // Each movement into RunModel adds the archive chunks of the model input.
162
+ return messageStepOptions(this.transitions.persistence, this.getStepType(), staticMessageChunkLoads(this.transitions));
163
+ }
164
+ async execute(context) {
165
+ const transitions = this.transitions;
166
+ const state = transitions.readAgentState(context);
167
+ const turnId = state.activeTurnId;
168
+ if (turnId === null) {
169
+ throw new Error(`the agent session ${context.flowId} has no active turn`);
170
+ }
171
+ const toolCalls = transitions.readTurnToolCalls(context, turnId);
172
+ const openToolCall = toolCalls.find((toolCall) => !isFinalToolCallStatus(toolCall.status));
173
+ if (openToolCall !== undefined) {
174
+ throw new Error(`RunModel for ${turnId} started before tool call ${openToolCall.toolCallId} finished`);
175
+ }
176
+ const decision = this.decideBeforeModelCall(context, turnId);
177
+ if (decision !== null) {
178
+ return decision;
179
+ }
180
+ const events = transitions.createEventWriter(context, turnId);
181
+ try {
182
+ const turnContext = transitions.createTurnContext(context, state, turnId, toolCalls, events, () => transitions.recordHeartbeat(context));
183
+ const throughSequence = await this.selectCompactionBeforeModelCall(turnContext, state);
184
+ if (throughSequence !== null) {
185
+ return this.startCompaction(context, state, turnContext.contextSummary ?? null, throughSequence);
186
+ }
187
+ return await transitions.tracer.traceModelCall(context, this.getStepType(), turnContext, async (modelCallSpan) => {
188
+ const outcome = await transitions.runWithHeartbeats(context, () => transitions.runner.runModelStep(turnContext));
189
+ switch (outcome?.kind) {
190
+ case "completed": {
191
+ modelCallSpan.recordOutcome(outcome, appendedMessageKeys(state, outcome.newMessages));
192
+ // The index keeps where the turn's replies begin, so the count includes messages summarized away.
193
+ const storedTurnMessageCount = transitions.countStoredTurnReplies(context, state, turnId);
194
+ return this.completeTurn(context, state, turnId, toolCalls, outcome, storedTurnMessageCount, events);
195
+ }
196
+ case "tool_calls_requested":
197
+ if ((outcome.newMessages?.length ?? 0) > 0 && transitions.runner.buildUnfinishedTurnMessages === undefined) {
198
+ // A runner defect: a retry would only repeat the paid model call, so the turn fails now.
199
+ modelCallSpan.recordOutcome(outcome, []);
200
+ transitions.recordUsage(context, outcome.usage, events);
201
+ return recordFailedTurn(transitions, context, state, toolCalls, [], missingUnfinishedTurnMessageBuilder);
202
+ }
203
+ modelCallSpan.recordOutcome(outcome, appendedMessageKeys(state, outcome.newMessages ?? []));
204
+ return this.requestToolCalls(context, state, turnId, toolCalls, outcome, events);
205
+ default:
206
+ throw new TypeError("runModelStep must return a TurnCompleted or ToolCallsRequested outcome");
207
+ }
208
+ });
209
+ }
210
+ finally {
211
+ events.close();
212
+ }
213
+ }
214
+ /** Returns RunModel's move instead of a model call, or `null` to call the model. */
215
+ decideBeforeModelCall(context, turnId) {
216
+ // Execute state is fixed at the first attempt, so this and the compaction check are RunModel's decision points.
217
+ const transitions = this.transitions;
218
+ if (transitions.readTurnControlReason(context, turnId) === null) {
219
+ return null;
220
+ }
221
+ // Ending the turn in this commit leaves FinishTurnCancellation no pending turn end to race.
222
+ transitions.endControlledTurn(context, transitions.readAgentState(context), turnId, context.channelSize(transitions.persistence.steeredUserMessages));
223
+ return transitions.goToApplyTurnControl(context);
224
+ }
225
+ completeTurn(context, state, turnId, toolCalls, outcome, storedTurnMessageCount, events) {
226
+ const transitions = this.transitions;
227
+ const persistence = transitions.persistence;
228
+ const stateWithMessages = transitions.appendMessages(context, state, turnId, outcome.newMessages);
229
+ for (const toolCall of toolCalls) {
230
+ persistence.agentToolCalls.delete(context, toolCall.toolCallId);
231
+ }
232
+ persistence.frameworkRunState.delete(context);
233
+ transitions.recordUsage(context, outcome.usage, events);
234
+ transitions.writeAgentState(context, {
235
+ ...stateWithMessages,
236
+ status: "waiting_for_user_message",
237
+ turnCount: state.turnCount + 1,
238
+ activeTurnId: null,
239
+ lastTurnError: null,
240
+ acceptsTurnControl: false,
241
+ });
242
+ // The turn's user messages are stored but not counted; incremental runners stored more before now.
243
+ const messageCount = storedTurnMessageCount + outcome.newMessages.length;
244
+ events.writeEvent("turn_completed", { message_count: messageCount });
245
+ return transitions.goToAwaitUserMessage();
246
+ }
247
+ /** Returns the sequence to summarize through before this model call, or `null` to make the call. */
248
+ async selectCompactionBeforeModelCall(turn, state) {
249
+ const transitions = this.transitions;
250
+ const policy = transitions.config.compaction;
251
+ const runner = transitions.runner;
252
+ if (policy === undefined || !isContextCompactionRunner(runner)) {
253
+ return null;
254
+ }
255
+ if (!isCompactionTriggered(policy, await runner.estimateInputTokens(turn))) {
256
+ return null;
257
+ }
258
+ const cutoff = await runner.selectCompactionCutoff(turn, readRetainedTokenBudget(policy));
259
+ // A cutoff that does not advance means nothing more to summarize, so the pair cannot loop.
260
+ if (cutoff === null || cutoff <= (turn.contextSummary?.lastSequence ?? 0)) {
261
+ return null;
262
+ }
263
+ if (!Number.isSafeInteger(cutoff) || cutoff >= state.nextMessageSequence) {
264
+ throw new TypeError(`selectCompactionCutoff returned ${String(cutoff)}, which is not a stored message`);
265
+ }
266
+ return cutoff;
267
+ }
268
+ startCompaction(context, state, previousSummary, throughSequence) {
269
+ const persistence = this.transitions.persistence;
270
+ // CompactContext commits once, so this commit is where the status becomes visible.
271
+ this.transitions.writeAgentState(context, { ...state, status: "compacting_context" });
272
+ const firstSequence = this.transitions.readModelInputFirstSequence(context, previousSummary);
273
+ const chunkLoads = this.transitions.messageChunkLoads(context, firstSequence, throughSequence + 1);
274
+ const input = { throughSequence };
275
+ return goToMany(StepMovement.of(new StepRef(agentSessionStepTypes.compactContext), input, {
276
+ ...syncDurability,
277
+ ...modelCallStepOptions,
278
+ executeLoadAttributeMaps: [persistence.agentMessages],
279
+ ...(chunkLoads.instances.length === 0 ? {} : { executeLoadAttributeMapInstances: chunkLoads.instances }),
280
+ }));
281
+ }
282
+ requestToolCalls(context, state, turnId, toolCalls, outcome, events) {
283
+ const transitions = this.transitions;
284
+ validateToolCallsRequested(turnId, toolCalls, outcome);
285
+ const newMessages = outcome.newMessages ?? [];
286
+ const stateWithMessages = newMessages.length === 0 ? state : transitions.appendMessages(context, state, turnId, newMessages);
287
+ transitions.persistence.frameworkRunState.set(context, outcome.frameworkRunState);
288
+ const createdAt = new Date();
289
+ const batch = {
290
+ context,
291
+ turnId,
292
+ createdAt: createdAt.toISOString(),
293
+ interruptDeadline: transitions.config.interruptDeadlineMs === undefined
294
+ ? null
295
+ : new Date(createdAt.getTime() + transitions.config.interruptDeadlineMs).toISOString(),
296
+ interrupts: [],
297
+ traceContext: transitions.tracer.readActivationTraceparent(context),
298
+ movements: [],
299
+ events,
300
+ interactionMode: state.interactionMode ?? "chat",
301
+ writeTodosCallCount: outcome.toolCalls.filter((toolCall) => toolCall.toolName === "write_todos" && transitions.isEnabledBuiltInTool(toolCall.toolName)).length,
302
+ };
303
+ for (const [index, requestedToolCall] of outcome.toolCalls.entries()) {
304
+ const deferralOrder = toolCalls.length + index;
305
+ const toolName = requestedToolCall.toolName;
306
+ if (batch.interactionMode === "planning" && !(toolName === "write_todos" && transitions.isEnabledBuiltInTool(toolName))) {
307
+ this.rejectPlanningTurnToolCall(batch, requestedToolCall, deferralOrder);
308
+ }
309
+ else if (transitions.isEnabledBuiltInTool(toolName)) {
310
+ this.deferBuiltInToolCall(batch, requestedToolCall, toolName, deferralOrder);
311
+ }
312
+ else {
313
+ this.deferApplicationToolCall(batch, requestedToolCall, deferralOrder);
314
+ }
315
+ }
316
+ transitions.persistence.pendingInterrupts.set(context, batch.interrupts);
317
+ transitions.recordUsage(context, outcome.usage, events);
318
+ transitions.writeAgentState(context, { ...stateWithMessages, status: "running_tools" });
319
+ batch.movements.push(StepMovement.of(new StepRef(agentSessionStepTypes.resumeTurn), outcome.toolCalls.length));
320
+ return goToMany(...batch.movements);
321
+ }
322
+ /** A planning turn hides executable tools; a call to one never runs. */
323
+ rejectPlanningTurnToolCall(batch, requestedToolCall, deferralOrder) {
324
+ const isBuiltInTool = this.transitions.isEnabledBuiltInTool(requestedToolCall.toolName);
325
+ const record = createToolCallRecord(batch, requestedToolCall, isBuiltInTool ? "read" : requestedToolCall.policy.effect, isBuiltInTool ? false : requestedToolCall.policy.requiresApproval, deferralOrder);
326
+ writeToolCallDeferred(batch.events, record);
327
+ this.transitions.finishToolCall(batch.context, { ...record, status: "rejected", rejectionMessage: planningTurnToolRejection }, batch.events);
328
+ }
329
+ deferApplicationToolCall(batch, requestedToolCall, deferralOrder) {
330
+ const transitions = this.transitions;
331
+ const requiresApproval = requestedToolCall.policy.requiresApproval;
332
+ const effect = requestedToolCall.policy.effect;
333
+ const record = createToolCallRecord(batch, requestedToolCall, effect, requiresApproval, deferralOrder);
334
+ transitions.writeToolCall(batch.context, {
335
+ ...record,
336
+ status: requiresApproval ? "awaiting_approval" : "approved",
337
+ });
338
+ writeToolCallDeferred(batch.events, record);
339
+ const nextStepType = requiresApproval
340
+ ? agentSessionStepTypes.awaitToolApproval
341
+ : toolStepTypeForEffect(effect);
342
+ batch.movements.push(toolStepMovement(transitions.persistence, nextStepType, requestedToolCall.toolCallId));
343
+ if (!requiresApproval) {
344
+ return;
345
+ }
346
+ const interrupt = {
347
+ interruptId: requestedToolCall.toolCallId,
348
+ kind: "tool_approval",
349
+ toolCallId: requestedToolCall.toolCallId,
350
+ toolName: requestedToolCall.toolName,
351
+ arguments: requestedToolCall.arguments,
352
+ createdAt: batch.createdAt,
353
+ deadline: batch.interruptDeadline,
354
+ question: null,
355
+ choices: [],
356
+ allowsFreeText: true,
357
+ };
358
+ batch.interrupts.push(interrupt);
359
+ transitions.writeInterruptRaised(batch.events, interrupt);
360
+ }
361
+ /** Records a built-in call with its fixed policy and routes it to its wait Step; invalid arguments fail at once. */
362
+ deferBuiltInToolCall(batch, requestedToolCall, builtInTool, deferralOrder) {
363
+ const transitions = this.transitions;
364
+ const persistence = transitions.persistence;
365
+ const record = createToolCallRecord(batch, requestedToolCall, "read", false, deferralOrder);
366
+ writeToolCallDeferred(batch.events, record);
367
+ switch (builtInTool) {
368
+ case "ask_user": {
369
+ const validation = validateAskUserArguments(record.arguments);
370
+ if (!validation.isValid) {
371
+ this.failBuiltInToolCall(batch, record, validation.error);
372
+ return;
373
+ }
374
+ transitions.writeToolCall(batch.context, { ...record, status: "awaiting_answer" });
375
+ const interrupt = {
376
+ interruptId: record.toolCallId,
377
+ kind: "question",
378
+ toolCallId: record.toolCallId,
379
+ toolName: record.toolName,
380
+ arguments: record.arguments,
381
+ createdAt: batch.createdAt,
382
+ deadline: batch.interruptDeadline,
383
+ question: validation.arguments.question,
384
+ choices: validation.arguments.choices,
385
+ allowsFreeText: validation.arguments.allowsFreeText,
386
+ };
387
+ batch.interrupts.push(interrupt);
388
+ transitions.writeInterruptRaised(batch.events, interrupt);
389
+ batch.movements.push(questionAnswerMovement(persistence, { ...record, status: "awaiting_answer" }));
390
+ return;
391
+ }
392
+ case "durable_wait": {
393
+ const maxDurableWaitSeconds = Math.floor(transitions.config.maxDurableWaitMs / 1_000);
394
+ const validation = validateDurableWaitArguments(record.arguments, maxDurableWaitSeconds);
395
+ if (!validation.isValid) {
396
+ this.failBuiltInToolCall(batch, record, validation.error);
397
+ return;
398
+ }
399
+ const waitUntil = new Date(Date.parse(batch.createdAt) + validation.arguments.durationSeconds * 1_000).toISOString();
400
+ transitions.writeToolCall(batch.context, { ...record, status: "waiting_for_timer", waitUntil });
401
+ batch.events.writeEvent("timer_started", {
402
+ tool_call_id: record.toolCallId,
403
+ wait_until: waitUntil,
404
+ reason: validation.arguments.reason,
405
+ });
406
+ batch.movements.push(toolStepMovement(persistence, agentSessionStepTypes.awaitTimer, record.toolCallId));
407
+ return;
408
+ }
409
+ case "write_todos":
410
+ this.writeTodos(batch, record);
411
+ return;
412
+ }
413
+ }
414
+ /** Replaces the plan and records the call final, in this commit; no tool Step runs. */
415
+ writeTodos(batch, record) {
416
+ const transitions = this.transitions;
417
+ if (batch.writeTodosCallCount > 1) {
418
+ this.failBuiltInToolCall(batch, record, writeTodosCalledMoreThanOnce);
419
+ return;
420
+ }
421
+ const validation = validateWriteTodosArguments(record.arguments);
422
+ if (!validation.isValid) {
423
+ this.failBuiltInToolCall(batch, record, validation.error);
424
+ return;
425
+ }
426
+ const tasks = validation.arguments;
427
+ const previous = transitions.readOptionalAgentPlan(batch.context);
428
+ const plan = {
429
+ revision: (previous?.revision ?? 0) + 1,
430
+ status: planStatusAfterWrite(tasks, batch.interactionMode),
431
+ tasks,
432
+ updatedAt: batch.createdAt,
433
+ };
434
+ transitions.persistence.agentPlan.set(batch.context, plan);
435
+ writePlanUpdated(batch.events, plan);
436
+ const output = plan.status === "cleared" ? buildPlanClearedOutput(plan.revision) : buildPlanUpdatedOutput(plan.revision, tasks.length);
437
+ transitions.finishToolCall(batch.context, { ...record, status: "succeeded", output }, batch.events);
438
+ }
439
+ failBuiltInToolCall(batch, record, error) {
440
+ this.transitions.finishToolCall(batch.context, { ...record, status: "failed", error }, batch.events);
441
+ }
442
+ }
443
+ /** Summarizes the messages through a sequence for the model input, then moves back to `RunModel`. */
444
+ export class CompactContextStep {
445
+ transitions;
446
+ inputCodec = turnFailureRouteInputCodec;
447
+ constructor(transitions) {
448
+ this.transitions = transitions;
449
+ }
450
+ getStepType() {
451
+ return agentSessionStepTypes.compactContext;
452
+ }
453
+ getStepOptions() {
454
+ return {
455
+ ...syncDurability,
456
+ ...modelCallStepOptions,
457
+ executeLoadAttributeMaps: [this.transitions.persistence.agentMessages],
458
+ };
459
+ }
460
+ async execute(context, input) {
461
+ const transitions = this.transitions;
462
+ if (input === undefined) {
463
+ throw new TypeError("CompactContext needs a CompactContextInput");
464
+ }
465
+ const state = transitions.readAgentState(context);
466
+ const turnId = state.activeTurnId;
467
+ if (turnId === null) {
468
+ throw new Error(`the agent session ${context.flowId} has no active turn`);
469
+ }
470
+ const previousSummary = transitions.readContextSummary(context);
471
+ const firstSequence = previousSummary === null ? 1 : previousSummary.lastSequence + 1;
472
+ const throughSequence = input.throughSequence;
473
+ if (throughSequence < firstSequence || throughSequence >= state.nextMessageSequence) {
474
+ throw new Error(`CompactContext cannot summarize through message ${throughSequence}`);
475
+ }
476
+ const runner = transitions.runner;
477
+ if (!isContextCompactionRunner(runner)) {
478
+ throw new TypeError("CompactContext needs a runner that implements the compaction methods");
479
+ }
480
+ const events = transitions.createEventWriter(context, turnId);
481
+ try {
482
+ events.writeEvent("compaction_started", { through_sequence: throughSequence });
483
+ const result = await this.summarize(context, runner, turnId, previousSummary, throughSequence, events);
484
+ const summary = {
485
+ generation: previousSummary === null ? 1 : previousSummary.generation + 1,
486
+ firstSequence: 1,
487
+ lastSequence: throughSequence,
488
+ text: result.text,
489
+ createdAt: new Date().toISOString(),
490
+ };
491
+ transitions.persistence.contextSummary.set(context, summary);
492
+ const index = transitions.readMessageIndex(context);
493
+ if (index !== null) {
494
+ const retainedIndex = transitions.applyMessageRetention(context, index, state.nextMessageSequence, summary);
495
+ if (retainedIndex !== index) {
496
+ transitions.persistence.agentMessageIndex.set(context, retainedIndex);
497
+ }
498
+ }
499
+ transitions.recordUsage(context, result.usage, events);
500
+ transitions.writeAgentState(context, { ...state, status: "running_model" });
501
+ events.writeEvent("compaction_finished", { generation: summary.generation, through_sequence: throughSequence });
502
+ return transitions.goToRunModel(context, summary);
503
+ }
504
+ finally {
505
+ events.close();
506
+ }
507
+ }
508
+ async summarize(context, runner, turnId, previousSummary, throughSequence, events) {
509
+ const transitions = this.transitions;
510
+ const firstSequence = transitions.readModelInputFirstSequence(context, previousSummary);
511
+ const messages = transitions.readMessages(context, firstSequence, throughSequence + 1);
512
+ const policy = transitions.config.compaction;
513
+ return transitions.tracer.traceContextSummary(context, this.getStepType(), turnId, throughSequence, messages, async (summarySpan) => {
514
+ const result = await transitions.runWithHeartbeats(context, () => runner.summarizeContext({
515
+ flowId: context.flowId,
516
+ turnId,
517
+ attempt: context.attempt,
518
+ stepExecutionId: context.stepExecutionId,
519
+ previousSummary,
520
+ messages,
521
+ throughSequence,
522
+ events,
523
+ cancellationSignal: context.cancellationSignal,
524
+ deadline: context.deadline,
525
+ ...(policy === undefined ? {} : { inputTokenBudget: policy.contextWindowTokens }),
526
+ sessionSettings: transitions.readTurnSessionSettings(context),
527
+ heartbeat: () => transitions.recordHeartbeat(context),
528
+ }));
529
+ if (typeof result?.text !== "string" || result.text.trim() === "" || typeof result.usage !== "object") {
530
+ throw new TypeError("summarizeContext must return summary text and usage");
531
+ }
532
+ summarySpan.recordResult(result);
533
+ return result;
534
+ });
535
+ }
536
+ }
537
+ /** Waits for one tool call's approval or its deadline, then runs or rejects the call. */
538
+ export class AwaitToolApprovalStep {
539
+ transitions;
540
+ inputCodec = stringCodec;
541
+ constructor(transitions) {
542
+ this.transitions = transitions;
543
+ }
544
+ getStepType() {
545
+ return agentSessionStepTypes.awaitToolApproval;
546
+ }
547
+ getStepOptions() {
548
+ return toolStepOptions(this.transitions.persistence, agentSessionStepTypes.awaitToolApproval);
549
+ }
550
+ waitFor(context, toolCallId) {
551
+ const transitions = this.transitions;
552
+ const conditions = [
553
+ transitions.persistence.toolCallCancellations.forOne(toolCallId),
554
+ transitions.persistence.interruptResolutions.forOne(toolCallId),
555
+ ];
556
+ // A resolved interrupt left PendingInterrupts, so its deadline no longer applies.
557
+ const interrupt = transitions
558
+ .readPendingInterrupts(context)
559
+ .find((pendingInterrupt) => pendingInterrupt.interruptId === toolCallId);
560
+ if (interrupt?.deadline != null) {
561
+ // Dex Timers take whole seconds; rounding up can only fire late, never early.
562
+ const remainingMs = Math.max(0, Date.parse(interrupt.deadline) - Date.now());
563
+ conditions.push(Timer.byDuration(Math.ceil(remainingMs / 1_000) * 1_000));
564
+ }
565
+ return Wait.anyOf(...conditions);
566
+ }
567
+ execute(context, toolCallId) {
568
+ const transitions = this.transitions;
569
+ const persistence = transitions.persistence;
570
+ const toolCall = transitions.readToolCall(context, toolCallId);
571
+ if (toolCall.status !== "awaiting_approval") {
572
+ return deadEnd();
573
+ }
574
+ if (context.channelResults(persistence.toolCallCancellations, toolCallId).length > 0) {
575
+ return cancelWaitingToolCall(this.transitions, context, toolCall, { interruptId: toolCallId, kind: "tool_approval" }, toolStepMovement(persistence, agentSessionStepTypes.awaitToolApproval, toolCallId));
576
+ }
577
+ const [resolution] = context.channelResults(persistence.interruptResolutions, toolCallId);
578
+ if (resolution === undefined) {
579
+ if (!context.hasTimerFired()) {
580
+ throw new Error(`the approval wait for ${toolCallId} completed without an answer`);
581
+ }
582
+ const pendingInterrupts = transitions.readPendingInterrupts(context);
583
+ const interrupt = pendingInterrupts.find((pending) => pending.interruptId === toolCallId);
584
+ // resolve_interrupt answered first, or the Timer fired early; wait again.
585
+ if (interrupt?.deadline == null || Date.parse(interrupt.deadline) > Date.now()) {
586
+ return goToMany(toolStepMovement(persistence, agentSessionStepTypes.awaitToolApproval, toolCallId));
587
+ }
588
+ persistence.pendingInterrupts.set(context, pendingInterrupts.filter((interrupt) => interrupt.interruptId !== toolCallId));
589
+ return this.applyResolution(context, toolCall, false, approvalDeadlinePassed, true);
590
+ }
591
+ return this.applyResolution(context, toolCall, resolution.approved, resolution.rejectionMessage, false);
592
+ }
593
+ applyResolution(context, toolCall, isApproved, rejectionMessage, isDeadlinePassed) {
594
+ return this.transitions.tracer.traceInterruptActivation(context, this.getStepType(), toolCall.turnId, toolCall.toolCallId, isApproved, isDeadlinePassed, (activation) => {
595
+ if (!isApproved) {
596
+ return this.rejectToolCall(context, toolCall, rejectionMessage);
597
+ }
598
+ // The approval starts a new activation, so the tool's spans join its root, not the RunModel one.
599
+ const { traceContext: _deferredTraceContext, ...deferredToolCall } = toolCall;
600
+ this.transitions.writeToolCall(context, {
601
+ ...deferredToolCall,
602
+ status: "approved",
603
+ cancellationReason: null,
604
+ ...(activation.traceparent === null ? {} : { traceContext: activation.traceparent }),
605
+ });
606
+ this.transitions.writeInterruptResolved(context, toolCall.turnId, {
607
+ interruptId: toolCall.toolCallId,
608
+ kind: "tool_approval",
609
+ isApproved: true,
610
+ isAnswered: false,
611
+ rejectionMessage: null,
612
+ });
613
+ return goToMany(toolStepMovement(this.transitions.persistence, toolStepTypeForEffect(toolCall.effect), toolCall.toolCallId));
614
+ });
615
+ }
616
+ rejectToolCall(context, toolCall, rejectionMessage) {
617
+ this.transitions.writeInterruptResolved(context, toolCall.turnId, {
618
+ interruptId: toolCall.toolCallId,
619
+ kind: "tool_approval",
620
+ isApproved: false,
621
+ isAnswered: false,
622
+ rejectionMessage,
623
+ });
624
+ this.transitions.finishToolCall(context, {
625
+ ...toolCall,
626
+ status: "rejected",
627
+ rejectionMessage,
628
+ cancellationReason: null,
629
+ });
630
+ return deadEnd();
631
+ }
632
+ }
633
+ /** Runs one tool call; one class serves `CallTool` and `CallSingleAttemptTool`. */
634
+ export class ToolCallStep {
635
+ transitions;
636
+ stepType;
637
+ inputCodec = stringCodec;
638
+ constructor(transitions, stepType) {
639
+ this.transitions = transitions;
640
+ this.stepType = stepType;
641
+ }
642
+ getStepType() {
643
+ return this.stepType;
644
+ }
645
+ getStepOptions() {
646
+ return toolStepOptions(this.transitions.persistence, this.stepType);
647
+ }
648
+ async execute(context, toolCallId) {
649
+ const transitions = this.transitions;
650
+ const toolCall = transitions.readToolCall(context, toolCallId);
651
+ if (isFinalToolCallStatus(toolCall.status)) {
652
+ return deadEnd();
653
+ }
654
+ if (toolCall.status !== "approved" || toolStepTypeForEffect(toolCall.effect) !== this.stepType) {
655
+ throw new Error(`tool call ${toolCallId} cannot run in ${this.stepType}`);
656
+ }
657
+ // Retries reuse the first attempt's state, so a call that started is never abandoned.
658
+ const reason = transitions.readTurnControlReason(context, toolCall.turnId);
659
+ if (reason !== null) {
660
+ transitions.finishToolCall(context, cancelledToolCall(toolCall, reason));
661
+ return deadEnd();
662
+ }
663
+ const events = transitions.createEventWriter(context, toolCall.turnId);
664
+ try {
665
+ return await transitions.tracer.traceToolCall(context, this.stepType, toolCall, async (toolCallSpan) => {
666
+ events.writeToolCallStarted(toolCall.toolCallId, toolCall.toolName);
667
+ const heartbeats = new ToolStepHeartbeats(context);
668
+ // Output first: Dex returns an attempt as not run when its stream half-closes before any output.
669
+ await heartbeats.record();
670
+ const execution = {
671
+ flowId: context.flowId,
672
+ toolCall: toRunnerToolCall(toolCall),
673
+ idempotencyKey: `${context.flowId}/${toolCall.turnId}/${toolCall.toolCallId}`,
674
+ attempt: context.attempt,
675
+ events,
676
+ cancellationSignal: context.cancellationSignal,
677
+ deadline: context.deadline,
678
+ inputAnswers: toolCall.inputAnswers,
679
+ supportsToolTasks: transitions.runner.pollToolTask !== undefined,
680
+ heartbeat: () => heartbeats.record(),
681
+ ...(heartbeats.previousCheckpoint === undefined ? {} : { heartbeatCheckpoint: heartbeats.previousCheckpoint }),
682
+ saveHeartbeatCheckpoint: (checkpoint) => heartbeats.saveCheckpoint(checkpoint),
683
+ };
684
+ let outcome;
685
+ try {
686
+ outcome = await transitions.runWithHeartbeats(context, () => transitions.runner.executeDeferredToolCall(execution), () => heartbeats.record());
687
+ }
688
+ catch (error) {
689
+ if (!(error instanceof ToolExecutionError)) {
690
+ throw error;
691
+ }
692
+ const failed = { ...toolCall, status: "failed", error: error.message };
693
+ toolCallSpan.recordFinished(failed);
694
+ transitions.finishToolCall(context, failed, events);
695
+ return deadEnd();
696
+ }
697
+ if (isToolInputRequested(outcome)) {
698
+ return this.requestToolInput(context, toolCall, outcome, events, toolCallSpan);
699
+ }
700
+ if (isToolTaskStarted(outcome)) {
701
+ return this.startToolTask(context, toolCall, outcome, events, toolCallSpan);
702
+ }
703
+ if (typeof outcome?.output !== "string") {
704
+ throw new TypeError(`executeDeferredToolCall returned no output for ${toolCallId}`);
705
+ }
706
+ const succeeded = { ...toolCall, status: "succeeded", output: outcome.output };
707
+ toolCallSpan.recordFinished(succeeded);
708
+ transitions.finishToolCall(context, succeeded, events);
709
+ return deadEnd();
710
+ });
711
+ }
712
+ finally {
713
+ events.close();
714
+ }
715
+ }
716
+ /** Records `awaiting_answer` with the tool's question; `AwaitQuestionAnswer` raises it under the lock. */
717
+ requestToolInput(context, toolCall, request, events, toolCallSpan) {
718
+ const transitions = this.transitions;
719
+ const waiting = recordToolInputRequest(toolCall, request);
720
+ toolCallSpan.recordFinished(waiting);
721
+ if (isFinalToolCallStatus(waiting.status)) {
722
+ transitions.finishToolCall(context, waiting, events);
723
+ return deadEnd();
724
+ }
725
+ // This Step runs unlocked, so it leaves PendingInterrupts to AwaitQuestionAnswer.
726
+ transitions.writeToolCall(context, waiting);
727
+ return goToMany(questionAnswerMovement(transitions.persistence, waiting));
728
+ }
729
+ /** Records `waiting_for_task` and moves to `PollToolTask`, or records a task it cannot poll by effect. */
730
+ startToolTask(context, toolCall, started, events, toolCallSpan) {
731
+ const transitions = this.transitions;
732
+ const pollIntervalSeconds = toolTaskPollIntervalSeconds(started.pollIntervalSeconds);
733
+ const defect = transitions.runner.pollToolTask === undefined
734
+ ? missingToolTaskPoller
735
+ : typeof started.taskId !== "string" || isBlankText(started.taskId)
736
+ ? blankToolTaskId
737
+ : pollIntervalSeconds === null
738
+ ? invalidToolTaskPollInterval
739
+ : undefined;
740
+ if (defect !== undefined || pollIntervalSeconds === null) {
741
+ const finished = recordToolTaskDefect(toolCall, defect ?? invalidToolTaskPollInterval);
742
+ toolCallSpan.recordFinished(finished);
743
+ transitions.finishToolCall(context, finished, events);
744
+ return deadEnd();
745
+ }
746
+ // The tool consumed any continuation of its own questions when it ran again.
747
+ const waiting = {
748
+ ...toolCall,
749
+ status: "waiting_for_task",
750
+ taskId: started.taskId,
751
+ taskPollIntervalSeconds: pollIntervalSeconds,
752
+ inputContinuation: null,
753
+ ...noPendingToolQuestion,
754
+ };
755
+ toolCallSpan.recordFinished(waiting);
756
+ transitions.writeToolCall(context, waiting);
757
+ events.writeEvent("tool_task_started", {
758
+ tool_call_id: waiting.toolCallId,
759
+ tool_name: waiting.toolName,
760
+ task_id: started.taskId,
761
+ });
762
+ return goToMany(toolTaskPollMovement(transitions.persistence, waiting));
763
+ }
764
+ }
765
+ /**
766
+ * Failure route of a tool Step; it receives the failed Step's input, the tool call ID.
767
+ * `RecordToolFailure` records `failed` for a `read` call and `outcome_unknown` for a write;
768
+ * `RecordToolOutcomeUnknown` always records `outcome_unknown`. Either records `failed` with
769
+ * {@link toolCallNotRunErrorPrefix} when the tool certainly never ran.
770
+ */
771
+ export class ToolCallFailureRecordingStep {
772
+ transitions;
773
+ stepType;
774
+ isReadFailureRecorded;
775
+ inputCodec = stringCodec;
776
+ constructor(transitions, stepType, isReadFailureRecorded) {
777
+ this.transitions = transitions;
778
+ this.stepType = stepType;
779
+ this.isReadFailureRecorded = isReadFailureRecorded;
780
+ }
781
+ getStepType() {
782
+ return this.stepType;
783
+ }
784
+ getStepOptions() {
785
+ return toolStepOptions(this.transitions.persistence, this.stepType);
786
+ }
787
+ execute(context, toolCallId) {
788
+ const transitions = this.transitions;
789
+ const toolCall = transitions.readToolCall(context, toolCallId);
790
+ if (isFinalToolCallStatus(toolCall.status)) {
791
+ return deadEnd();
792
+ }
793
+ const status = this.isReadFailureRecorded && toolCall.effect === "read" ? "failed" : "outcome_unknown";
794
+ transitions.finishToolCall(context, recoveredToolCall(toolCall, context.recoveryError, status));
795
+ return deadEnd();
796
+ }
797
+ }
798
+ /** Returns the final record that a tool Step's failure route writes. */
799
+ export function recoveredToolCall(toolCall, recoveryError, status) {
800
+ // An abandoned task keeps the text CancelToolTask records, whatever made its attempts fail.
801
+ const error = isAbandonedToolTask(toolCall) && (toolCall.cancellationReason ?? null) !== null
802
+ ? toolTaskAbandonedError
803
+ : recoveryErrorDetail(recoveryError);
804
+ if (isToolCallNotRun(toolCall, recoveryError)) {
805
+ return { ...toolCall, status: "failed", error: `${toolCallNotRunErrorPrefix}: ${error}` };
806
+ }
807
+ return { ...toolCall, status, error };
808
+ }
809
+ /** Returns whether the failed Step certainly never ran the call's tool. */
810
+ function isToolCallNotRun(toolCall, recoveryError) {
811
+ // Polls and cancellations follow a tool that already ran, as does an answered question's call.
812
+ return (recoveryError?.attemptProgress === StepAttemptProgress.NOT_STARTED &&
813
+ toolCall.status === "approved" &&
814
+ toolCall.inputAnswers.length === 0);
815
+ }
816
+ /**
817
+ * Waits until every tool call of the batch has a final record, then makes the next model call.
818
+ *
819
+ * A steer wakes it early: it cancels the turn's pending interrupts and Timers, and waits again. After a steer or
820
+ * `cancel_turn`, once every result is in, it moves to `ApplyTurnControl` instead of `RunModel`.
821
+ */
822
+ export class ResumeTurnStep {
823
+ transitions;
824
+ inputCodec = expectedResultCountCodec;
825
+ constructor(transitions) {
826
+ this.transitions = transitions;
827
+ }
828
+ getStepType() {
829
+ return agentSessionStepTypes.resumeTurn;
830
+ }
831
+ getStepOptions() {
832
+ const persistence = this.transitions.persistence;
833
+ // A control wake rewrites PendingInterrupts, which the interrupt RPCs also write.
834
+ return {
835
+ ...syncDurability,
836
+ executeLoadAttributeMaps: [persistence.agentToolCalls],
837
+ executeLockAttributes: [persistence.pendingInterrupts.lock()],
838
+ };
839
+ }
840
+ waitFor(_context, expectedResultCount) {
841
+ const persistence = this.transitions.persistence;
842
+ return Wait.anyOf(persistence.toolCallResults.forN(expectedResultCount), persistence.turnControlRequests.range(1, allPendingMessages));
843
+ }
844
+ execute(context, expectedResultCount) {
845
+ const transitions = this.transitions;
846
+ const state = transitions.readAgentState(context);
847
+ const turnId = state.activeTurnId;
848
+ const toolCalls = transitions.readTurnToolCalls(context, turnId);
849
+ const reason = transitions.readTurnControlReason(context, turnId);
850
+ if (context.channelResults(transitions.persistence.turnControlRequests).length > 0) {
851
+ // FinishTurnCancellation, not this wake, ends the waits of a cancelled turn.
852
+ if (reason === "steered" && turnId !== null) {
853
+ this.wakeWaitingToolCalls(context, turnId, toolCalls, reason);
854
+ }
855
+ // The results did not all arrive, so wait again for the same count.
856
+ return goTo(new StepRef(agentSessionStepTypes.resumeTurn), expectedResultCount);
857
+ }
858
+ const unfinishedCount = toolCalls.filter((toolCall) => !isFinalToolCallStatus(toolCall.status)).length;
859
+ if (unfinishedCount > 0) {
860
+ // A consumed result from an earlier turn must not resume this one early.
861
+ return goTo(new StepRef(agentSessionStepTypes.resumeTurn), unfinishedCount);
862
+ }
863
+ if (reason !== null) {
864
+ return transitions.goToApplyTurnControl(context);
865
+ }
866
+ transitions.writeAgentState(context, { ...state, status: "running_model" });
867
+ return transitions.goToRunModel(context);
868
+ }
869
+ /** Cancels the turn's pending interrupts and wakes each Step waiting for a person or a Timer. */
870
+ wakeWaitingToolCalls(context, turnId, toolCalls, reason) {
871
+ const transitions = this.transitions;
872
+ const persistence = transitions.persistence;
873
+ const interrupts = transitions.readPendingInterrupts(context);
874
+ const cancelledInterruptIds = new Set();
875
+ for (const toolCall of toolCalls) {
876
+ if (!isCancellableByTurnControl(toolCall)) {
877
+ continue;
878
+ }
879
+ if (toolCall.status === "waiting_for_timer") {
880
+ // A later control wake finds the first message still pending and adds none.
881
+ if (context.channelSize(persistence.toolCallCancellations, toolCall.toolCallId) === 0) {
882
+ persistence.toolCallCancellations.publish(context, toolCall.toolCallId, { reason });
883
+ }
884
+ continue;
885
+ }
886
+ const isApproval = toolCall.status === "awaiting_approval";
887
+ const interruptId = isApproval ? toolCall.toolCallId : questionInterruptId(toolCall);
888
+ const kind = isApproval ? "tool_approval" : "question";
889
+ // An answered interrupt is gone, so its answer wins; an unraised question checks flags.
890
+ if (interrupts.some((interrupt) => interrupt.interruptId === interruptId && interrupt.toolCallId === toolCall.toolCallId)) {
891
+ cancelledInterruptIds.add(interruptId);
892
+ transitions.writeInterruptCancelled(context, turnId, interruptId, kind, reason);
893
+ persistence.toolCallCancellations.publish(context, toolCall.toolCallId, { reason });
894
+ }
895
+ }
896
+ if (cancelledInterruptIds.size > 0) {
897
+ persistence.pendingInterrupts.set(context, interrupts.filter((interrupt) => !cancelledInterruptIds.has(interrupt.interruptId)));
898
+ }
899
+ }
900
+ }
901
+ /** Ends a steered or cancelled turn, then starts a turn with the steered messages. */
902
+ export class ApplyTurnControlStep {
903
+ transitions;
904
+ inputCodec = voidCodec;
905
+ constructor(transitions) {
906
+ this.transitions = transitions;
907
+ }
908
+ getStepType() {
909
+ return agentSessionStepTypes.applyTurnControl;
910
+ }
911
+ getStepOptions() {
912
+ // Each movement into ApplyTurnControl adds the archive chunks of the model input.
913
+ // The lock keeps a turn from starting while cancel_turn decides about the last one.
914
+ return {
915
+ ...messageStepOptions(this.transitions.persistence, this.getStepType(), staticMessageChunkLoads(this.transitions)),
916
+ executeLockAttributes: [this.transitions.persistence.turnCancellationRequest.lock()],
917
+ };
918
+ }
919
+ waitFor() {
920
+ const persistence = this.transitions.persistence;
921
+ // Both conditions are non-blocking: the wait consumes every steered message and stale wake.
922
+ return Wait.allOf(persistence.steeredUserMessages.atMost(allPendingMessages), persistence.turnControlRequests.atMost(allPendingMessages));
923
+ }
924
+ execute(context) {
925
+ const transitions = this.transitions;
926
+ let state = transitions.readAgentState(context);
927
+ const steeredMessages = context.channelResults(transitions.persistence.steeredUserMessages);
928
+ if (state.activeTurnId !== null) {
929
+ state = transitions.endControlledTurn(context, state, state.activeTurnId, steeredMessages.length);
930
+ }
931
+ if (steeredMessages.length === 0) {
932
+ return transitions.goToAwaitUserMessage();
933
+ }
934
+ // Any steered message with planMode, or a draft plan, makes it a planning turn.
935
+ return transitions.beginTurn(context, state, steeredMessages, this.getStepType(), transitions.decideInteractionMode(context, steeredMessages));
936
+ }
937
+ }
938
+ /** Ends a failed turn; it is the failure route of `RunModel` and `CompactContext`, so it takes either's input. */
939
+ export class RecordTurnFailureStep {
940
+ transitions;
941
+ inputCodec = turnFailureRouteInputCodec;
942
+ constructor(transitions) {
943
+ this.transitions = transitions;
944
+ }
945
+ getStepType() {
946
+ return agentSessionStepTypes.recordTurnFailure;
947
+ }
948
+ getStepOptions() {
949
+ const persistence = this.transitions.persistence;
950
+ const hasUnfinishedTurnMessages = this.transitions.runner.buildUnfinishedTurnMessages !== undefined;
951
+ return {
952
+ ...syncDurability,
953
+ executeRetry: recoveryRetry,
954
+ // A failure route has only static options, so an incremental runner loads every retained chunk.
955
+ executeLoadAttributeMaps: hasUnfinishedTurnMessages
956
+ ? [persistence.agentToolCalls, persistence.agentMessages, persistence.agentMessageChunks]
957
+ : [persistence.agentToolCalls, persistence.agentMessages],
958
+ };
959
+ }
960
+ execute(context) {
961
+ const transitions = this.transitions;
962
+ const state = transitions.readAgentState(context);
963
+ const toolCalls = transitions.readTurnToolCalls(context, state.activeTurnId);
964
+ const finalToolCalls = toolCalls.filter((toolCall) => isFinalToolCallStatus(toolCall.status));
965
+ const unfinishedTurnMessages = state.activeTurnId === null
966
+ ? null
967
+ : transitions.buildUnfinishedTurnMessages(context, state, state.activeTurnId, finalToolCalls);
968
+ return recordFailedTurn(transitions, context, state, toolCalls, unfinishedTurnMessages ?? [], readRecoveryDetail(context));
969
+ }
970
+ }
971
+ /**
972
+ * Ends the active turn as failed in the current commit: appends the unfinished-turn messages, then the
973
+ * turn-failure note when the turn has final records, and clears the turn.
974
+ */
975
+ function recordFailedTurn(transitions, context, initialState, toolCalls, unfinishedTurnMessages, turnError) {
976
+ const persistence = transitions.persistence;
977
+ const turnId = initialState.activeTurnId;
978
+ let state = initialState;
979
+ const finalToolCalls = toolCalls.filter((toolCall) => isFinalToolCallStatus(toolCall.status));
980
+ if (turnId !== null) {
981
+ state = transitions.appendMessages(context, state, turnId, unfinishedTurnMessages);
982
+ }
983
+ // The records are deleted below; the note keeps the next model from repeating writes.
984
+ if (turnId !== null && finalToolCalls.length > 0) {
985
+ const note = transitions.runner.buildUserMessage(buildTurnFailureNote(finalToolCalls));
986
+ if (note.role !== "user") {
987
+ throw new TypeError("buildUserMessage must return a message with role user");
988
+ }
989
+ state = transitions.appendMessages(context, state, turnId, [note]);
990
+ }
991
+ for (const toolCall of toolCalls) {
992
+ persistence.agentToolCalls.delete(context, toolCall.toolCallId);
993
+ }
994
+ persistence.frameworkRunState.delete(context);
995
+ persistence.pendingInterrupts.set(context, []);
996
+ transitions.writeAgentState(context, {
997
+ ...state,
998
+ status: "waiting_for_user_message",
999
+ turnCount: state.turnCount + 1,
1000
+ activeTurnId: null,
1001
+ lastTurnError: turnError,
1002
+ acceptsTurnControl: false,
1003
+ });
1004
+ transitions.writeEvents(context, turnId, (events) => events.writeEvent("turn_failed", { error: turnError }));
1005
+ return transitions.goToAwaitUserMessage();
1006
+ }
1007
+ /**
1008
+ * Chunk loads of a Step's static options, used by a retry or by a movement an earlier version scheduled: none
1009
+ * beyond the window with compaction, else every chunk.
1010
+ */
1011
+ function staticMessageChunkLoads(transitions) {
1012
+ return { isWholeMap: transitions.config.compaction === undefined, instances: [] };
1013
+ }
1014
+ /** The `AgentMessages` keys that appending these messages after the state's last one will use. */
1015
+ function appendedMessageKeys(state, messages) {
1016
+ return messages.map((_message, offset) => messageSequenceKey(state.nextMessageSequence + offset));
1017
+ }
1018
+ /** `last_turn_error` of a turn whose runner stored messages incrementally without the builder. */
1019
+ export const missingUnfinishedTurnMessageBuilder = "a runner that sets ToolCallsRequested.newMessages must implement buildUnfinishedTurnMessages";
1020
+ function validateToolCallsRequested(turnId, existingToolCalls, outcome) {
1021
+ if (typeof outcome.frameworkRunState !== "string" || outcome.frameworkRunState === "") {
1022
+ throw new TypeError(`ToolCallsRequested for ${turnId} needs a non-empty frameworkRunState`);
1023
+ }
1024
+ if (outcome.newMessages !== undefined && !Array.isArray(outcome.newMessages)) {
1025
+ throw new TypeError(`ToolCallsRequested.newMessages for ${turnId} must be an array`);
1026
+ }
1027
+ if (outcome.toolCalls.length === 0) {
1028
+ throw new TypeError(`ToolCallsRequested for ${turnId} needs at least one tool call`);
1029
+ }
1030
+ const seenIds = new Set(existingToolCalls.map((toolCall) => toolCall.toolCallId));
1031
+ for (const requestedToolCall of outcome.toolCalls) {
1032
+ const toolCallId = requestedToolCall.toolCallId;
1033
+ if (toolCallId.trim() === "" || toolCallId.includes("/")) {
1034
+ throw new TypeError(`tool call ID ${JSON.stringify(toolCallId)} is empty or contains '/'`);
1035
+ }
1036
+ if (seenIds.has(toolCallId)) {
1037
+ throw new TypeError(`tool call ${toolCallId} was already requested in ${turnId}`);
1038
+ }
1039
+ seenIds.add(toolCallId);
1040
+ }
1041
+ }
1042
+ /**
1043
+ * Returns the full options of a tool Step type. With a tool call ID, the Step loads only that call's
1044
+ * record; a movement's options replace the Step's static options, so every option is repeated here.
1045
+ */
1046
+ export function toolStepOptions(persistence, stepType, toolCallId) {
1047
+ const loads = toolCallId === undefined
1048
+ ? { executeLoadAttributeMaps: [persistence.agentToolCalls] }
1049
+ : { executeLoadAttributeMapInstances: [persistence.agentToolCalls.load(toolCallId)] };
1050
+ const waitForLoads = toolCallId === undefined
1051
+ ? { waitForLoadAttributeMaps: [persistence.agentToolCalls] }
1052
+ : { waitForLoadAttributeMapInstances: [persistence.agentToolCalls.load(toolCallId)] };
1053
+ switch (stepType) {
1054
+ case agentSessionStepTypes.awaitToolApproval:
1055
+ // The deadline path rewrites PendingInterrupts, which resolve_interrupt and sibling Steps also write.
1056
+ return { ...syncDurability, ...loads, executeLockAttributes: [persistence.pendingInterrupts.lock()] };
1057
+ case agentSessionStepTypes.awaitQuestionAnswer:
1058
+ // Raising a tool's question and the deadline path rewrite PendingInterrupts, as in AwaitToolApproval.
1059
+ return {
1060
+ ...syncDurability,
1061
+ ...loads,
1062
+ ...waitForLoads,
1063
+ // A movement loads only its question's instance; see questionAnswerMovement.
1064
+ ...(toolCallId === undefined
1065
+ ? {
1066
+ waitForLoadChannelMaps: [persistence.interruptResolutions],
1067
+ executeLoadChannelMaps: [persistence.interruptResolutions],
1068
+ }
1069
+ : {}),
1070
+ executeLockAttributes: [persistence.pendingInterrupts.lock()],
1071
+ };
1072
+ case agentSessionStepTypes.awaitTimer:
1073
+ return { ...syncDurability, ...loads, ...waitForLoads };
1074
+ case agentSessionStepTypes.callTool:
1075
+ return {
1076
+ ...syncDurability,
1077
+ ...loads,
1078
+ executeRetry: { maximumAttempts: 3 },
1079
+ executeFailure: ExecuteFailure.proceedTo(new StepRef(agentSessionStepTypes.recordToolFailure), toolStepOptions(persistence, agentSessionStepTypes.recordToolFailure, toolCallId)),
1080
+ };
1081
+ case agentSessionStepTypes.callSingleAttemptTool:
1082
+ return {
1083
+ ...syncDurability,
1084
+ ...loads,
1085
+ executeRetry: { maximumAttempts: 1 },
1086
+ executeFailure: ExecuteFailure.proceedTo(new StepRef(agentSessionStepTypes.recordToolOutcomeUnknown), toolStepOptions(persistence, agentSessionStepTypes.recordToolOutcomeUnknown, toolCallId)),
1087
+ };
1088
+ case agentSessionStepTypes.pollToolTask:
1089
+ return toolTaskPollOptions(persistence, agentSessionStepTypes.recordToolFailure, toolCallId);
1090
+ case agentSessionStepTypes.recordToolFailure:
1091
+ case agentSessionStepTypes.recordToolOutcomeUnknown:
1092
+ return { ...syncDurability, ...loads, executeRetry: recoveryRetry };
1093
+ case agentSessionStepTypes.cancelToolTask:
1094
+ // Retries that run out still record the call, so the cancelled turn always ends.
1095
+ return {
1096
+ ...syncDurability,
1097
+ ...loads,
1098
+ executeMethodTimeoutMs: 30_000,
1099
+ executeRetry: { maximumAttempts: 3, totalDurationMs: 2 * minuteMs },
1100
+ executeFailure: ExecuteFailure.proceedTo(new StepRef(agentSessionStepTypes.recordToolOutcomeUnknown), toolStepOptions(persistence, agentSessionStepTypes.recordToolOutcomeUnknown, toolCallId)),
1101
+ };
1102
+ default:
1103
+ throw new TypeError(`${stepType} is not a tool Step type`);
1104
+ }
1105
+ }
1106
+ export function toolStepMovement(persistence, stepType, toolCallId) {
1107
+ return StepMovement.of(new StepRef(stepType), toolCallId, toolStepOptions(persistence, stepType, toolCallId));
1108
+ }
1109
+ /**
1110
+ * Returns the options of `PollToolTask`. It takes no lock and never waits on a cancellation: it owns the
1111
+ * record, and a started task runs on.
1112
+ */
1113
+ function toolTaskPollOptions(persistence, recoveryStepType, toolCallId) {
1114
+ const loads = toolCallId === undefined
1115
+ ? {
1116
+ waitForLoadAttributeMaps: [persistence.agentToolCalls],
1117
+ executeLoadAttributeMaps: [persistence.agentToolCalls],
1118
+ }
1119
+ : {
1120
+ waitForLoadAttributeMapInstances: [persistence.agentToolCalls.load(toolCallId)],
1121
+ executeLoadAttributeMapInstances: [persistence.agentToolCalls.load(toolCallId)],
1122
+ };
1123
+ return {
1124
+ ...syncDurability,
1125
+ ...loads,
1126
+ executeRetry: { maximumAttempts: 3 },
1127
+ executeFailure: ExecuteFailure.proceedTo(new StepRef(recoveryStepType), toolStepOptions(persistence, recoveryStepType, toolCallId)),
1128
+ };
1129
+ }
1130
+ /** Moves to `PollToolTask`; a poll that keeps failing leaves the task running, so the call is recorded by effect. */
1131
+ export function toolTaskPollMovement(persistence, toolCall) {
1132
+ const recoveryStepType = toolStepTypeForEffect(toolCall.effect) === agentSessionStepTypes.callTool
1133
+ ? agentSessionStepTypes.recordToolFailure
1134
+ : agentSessionStepTypes.recordToolOutcomeUnknown;
1135
+ return StepMovement.of(new StepRef(agentSessionStepTypes.pollToolTask), toolCall.toolCallId, toolTaskPollOptions(persistence, recoveryStepType, toolCall.toolCallId));
1136
+ }
1137
+ /** Returns the record of a tool's question: `awaiting_answer`, or `failed` when it is blank. */
1138
+ export function recordToolInputRequest(toolCall, request) {
1139
+ const choices = request.choices ?? [];
1140
+ if (typeof request.question !== "string" ||
1141
+ isBlankText(request.question) ||
1142
+ !choices.every((choice) => typeof choice === "string" && !isBlankText(choice))) {
1143
+ return { ...toolCall, status: "failed", error: "the tool asked for input with a blank question or choice" };
1144
+ }
1145
+ return {
1146
+ ...toolCall,
1147
+ status: "awaiting_answer",
1148
+ inputContinuation: request.continuation,
1149
+ inputQuestion: request.question,
1150
+ inputChoices: [...choices],
1151
+ inputAllowsFreeText: (request.allowsFreeText ?? true) || choices.length === 0,
1152
+ };
1153
+ }
1154
+ /** Returns a final record for a task that cannot be polled: `failed` for a read, else `outcome_unknown`. */
1155
+ export function recordToolTaskDefect(toolCall, error) {
1156
+ return {
1157
+ ...toolCall,
1158
+ status: toolCall.effect === "read" ? "failed" : "outcome_unknown",
1159
+ error,
1160
+ ...noPendingToolQuestion,
1161
+ };
1162
+ }
1163
+ /** Returns a poll interval in whole seconds from 1 to a day, or `null` when it is not a finite number. */
1164
+ export function toolTaskPollIntervalSeconds(seconds) {
1165
+ if (typeof seconds !== "number" || !Number.isFinite(seconds)) {
1166
+ return null;
1167
+ }
1168
+ // Dex Timers take whole seconds, and a zero interval would poll without a pause.
1169
+ return Math.min(Math.max(1, Math.ceil(seconds)), maximumToolTaskPollIntervalSeconds);
1170
+ }
1171
+ function createToolCallRecord(batch, requestedToolCall, effect, requiresApproval, deferralOrder) {
1172
+ return {
1173
+ toolCallId: requestedToolCall.toolCallId,
1174
+ turnId: batch.turnId,
1175
+ toolName: requestedToolCall.toolName,
1176
+ arguments: requestedToolCall.arguments,
1177
+ effect,
1178
+ requiresApproval,
1179
+ status: "approved",
1180
+ output: null,
1181
+ error: null,
1182
+ rejectionMessage: null,
1183
+ deferralOrder,
1184
+ ...(requestedToolCall.toolSourceReference === undefined
1185
+ ? {}
1186
+ : { toolSourceReference: requestedToolCall.toolSourceReference }),
1187
+ ...(batch.traceContext === null ? {} : { traceContext: batch.traceContext }),
1188
+ waitUntil: null,
1189
+ inputContinuation: null,
1190
+ inputAnswers: [],
1191
+ ...noPendingToolQuestion,
1192
+ cancellationReason: null,
1193
+ taskId: null,
1194
+ taskPollIntervalSeconds: null,
1195
+ taskAnswerPending: false,
1196
+ };
1197
+ }
1198
+ function writeToolCallDeferred(events, record) {
1199
+ events.writeEvent("tool_call_deferred", {
1200
+ tool_call_id: record.toolCallId,
1201
+ tool_name: record.toolName,
1202
+ effect: record.effect,
1203
+ requires_approval: record.requiresApproval,
1204
+ });
1205
+ }
1206
+ /** Record fields of a call without a pending tool question. */
1207
+ export const noPendingToolQuestion = {
1208
+ inputQuestion: null,
1209
+ inputChoices: [],
1210
+ inputAllowsFreeText: true,
1211
+ };
1212
+ /** Moves to `AwaitQuestionAnswer`, which reads the answer count to tell an unraised question from an answer. */
1213
+ export function questionAnswerMovement(persistence, toolCall) {
1214
+ const options = toolStepOptions(persistence, agentSessionStepTypes.awaitQuestionAnswer, toolCall.toolCallId);
1215
+ const interruptId = questionInterruptId(toolCall);
1216
+ const answerLoad = persistence.interruptResolutions.loadMessages(interruptId);
1217
+ // A record keyed by the question's interrupt ID means another call already owns that ID.
1218
+ const recordLoads = interruptId === toolCall.toolCallId
1219
+ ? [persistence.agentToolCalls.load(toolCall.toolCallId)]
1220
+ : [persistence.agentToolCalls.load(toolCall.toolCallId), persistence.agentToolCalls.load(interruptId)];
1221
+ return StepMovement.of(new StepRef(agentSessionStepTypes.awaitQuestionAnswer), toolCall.toolCallId, {
1222
+ ...options,
1223
+ waitForLoadAttributeMapInstances: recordLoads,
1224
+ executeLoadAttributeMapInstances: recordLoads,
1225
+ waitForLoadChannelMapInstances: [answerLoad],
1226
+ executeLoadChannelMapInstances: [answerLoad],
1227
+ });
1228
+ }
1229
+ /** An `ask_user` question uses the tool call ID; a tool's `n`-th question appends `-input-<n>`. */
1230
+ export function questionInterruptId(toolCall) {
1231
+ return toolCall.inputQuestion === null
1232
+ ? toolCall.toolCallId
1233
+ : `${toolCall.toolCallId}-input-${toolCall.inputAnswers.length + 1}`;
1234
+ }
1235
+ /**
1236
+ * Records a call that `ToolCallCancellations` woke as cancelled, or waits again. Pass `interrupt` `null` for a
1237
+ * durable wait.
1238
+ */
1239
+ export function cancelWaitingToolCall(transitions, context, toolCall, interrupt, waitAgain) {
1240
+ const persistence = transitions.persistence;
1241
+ // The flags decide; a wake-up left from an earlier turn's call ID changes nothing.
1242
+ const reason = transitions.readTurnControlReason(context, toolCall.turnId);
1243
+ if (reason === null || !isCancellableByTurnControl(toolCall)) {
1244
+ return goToMany(waitAgain);
1245
+ }
1246
+ if (interrupt !== null) {
1247
+ const pendingInterrupts = transitions.readPendingInterrupts(context);
1248
+ if (pendingInterrupts.some((pending) => pending.interruptId === interrupt.interruptId)) {
1249
+ persistence.pendingInterrupts.set(context, pendingInterrupts.filter((pending) => pending.interruptId !== interrupt.interruptId));
1250
+ transitions.writeInterruptCancelled(context, toolCall.turnId, interrupt.interruptId, interrupt.kind, reason);
1251
+ }
1252
+ else if (context.channelSize(persistence.interruptResolutions, interrupt.interruptId) > 0) {
1253
+ // The answer committed first; the next wait consumes it.
1254
+ return goToMany(waitAgain);
1255
+ }
1256
+ }
1257
+ transitions.finishToolCall(context, cancelledToolCall(toolCall, reason));
1258
+ return deadEnd();
1259
+ }
1260
+ export function isToolInputRequested(outcome) {
1261
+ return outcome?.kind === "tool_input_requested";
1262
+ }
1263
+ function isToolTaskStarted(outcome) {
1264
+ return outcome?.kind === "tool_task_started";
1265
+ }
1266
+ function requirePositiveResultCount(value) {
1267
+ if (!Number.isSafeInteger(value) || value <= 0) {
1268
+ throw new TypeError(`ResumeTurn input must be a positive integer: ${value}`);
1269
+ }
1270
+ return value;
1271
+ }
1272
+ //# sourceMappingURL=agent-session-steps.js.map