@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,850 @@
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 { AttributeMapNotLoadedError, StepMovement, StepRef, goTo, goToMany, stringCodec, } from "@superdurable/dex";
9
+ import { agentSessionStepTypes } from "./agent-session-step-types.js";
10
+ import { isContextCompactionRunner } from "./agent-context-compaction.js";
11
+ import { StreamAgentEventWriter } from "./agent-event-writer.js";
12
+ import { encodeAgentMessage } from "./agent-session-codecs.js";
13
+ import { resolveToolPolicy, } from "./agent-framework-runner.js";
14
+ import { messageSequenceKey } from "./agent-session-persistence.js";
15
+ import { messageStepOptions } from "./agent-session-step-options.js";
16
+ import { addAgentUsage, buildToolCallNote, emptyAgentUsage, isFinalToolCallStatus, toolCallCancelledBySteeringResult, toolCallCancelledByTurnCancellationResult, turnCancelledNoteHeader, turnSteeredNoteHeader, } from "./agent-session-values.js";
17
+ export { agentSessionStepTypes } from "./agent-session-step-types.js";
18
+ /** Most archive chunks one commit writes, so a large 0.1.0 history migrates over several commits. */
19
+ export const messageRolloverMaxChunksPerCommit = 4;
20
+ /**
21
+ * Most chunk bytes one commit writes, 8 MiB, which keeps a Step result far under the engine's frame limit. A
22
+ * single chunk larger than it still moves, alone.
23
+ */
24
+ export const messageRolloverMaxBytesPerCommit = 8_388_608;
25
+ // `{"messages":[` and `]}` around the comma-separated messages.
26
+ const messageChunkOverheadBytes = 15;
27
+ const utf8Encoder = new TextEncoder();
28
+ /** Consumes every pending message of a Channel at once; the plan's MAX. */
29
+ export const allPendingMessages = 2_147_483_647;
30
+ /** The model-visible result of a call that steering or `cancel_turn` abandoned, by reason. */
31
+ export const toolCallCancellationResults = {
32
+ steered: toolCallCancelledBySteeringResult,
33
+ turn_cancelled: toolCallCancelledByTurnCancellationResult,
34
+ };
35
+ // Keeps liveness well inside the shortest heartbeat timeout, one minute.
36
+ const heartbeatIntervalMs = 20_000;
37
+ /** Shared state reads, writes, and routing used by every session Step. */
38
+ export class AgentSessionTransitions {
39
+ persistence;
40
+ runner;
41
+ config;
42
+ tracer;
43
+ constructor(persistence, runner, config, tracer) {
44
+ this.persistence = persistence;
45
+ this.runner = runner;
46
+ this.config = config;
47
+ this.tracer = tracer;
48
+ }
49
+ readOptionalAgentState(context) {
50
+ return this.persistence.agentState.get(context) ?? null;
51
+ }
52
+ readAgentState(context) {
53
+ const state = this.readOptionalAgentState(context);
54
+ if (state === null) {
55
+ throw new Error(`the agent session ${context.flowId} is not initialized`);
56
+ }
57
+ return state;
58
+ }
59
+ writeAgentState(context, state) {
60
+ this.persistence.agentState.set(context, state);
61
+ }
62
+ readPendingInterrupts(context) {
63
+ return this.persistence.pendingInterrupts.get(context) ?? [];
64
+ }
65
+ readOptionalAgentPlan(context) {
66
+ return this.persistence.agentPlan.get(context) ?? null;
67
+ }
68
+ readAgentUsage(context) {
69
+ return this.persistence.agentUsage.get(context) ?? emptyAgentUsage();
70
+ }
71
+ /** The settings the next turn starts with, or `null` when the session has none. */
72
+ readSessionSettings(context) {
73
+ return this.persistence.sessionSettings.get(context) ?? null;
74
+ }
75
+ /** The settings the active turn started with; see {@link selectTurnSessionSettings}. */
76
+ readTurnSessionSettings(context) {
77
+ return selectTurnSessionSettings(this.persistence.turnSessionSettings.get(context) ?? null, this.readOptionalAgentState(context)?.activeTurnId ?? null);
78
+ }
79
+ readContextSummary(context) {
80
+ return this.persistence.contextSummary.get(context) ?? null;
81
+ }
82
+ /** The summary the runner sees; a runner without the compaction methods keeps every message instead. */
83
+ readRunnerContextSummary(context) {
84
+ return isContextCompactionRunner(this.runner) ? this.readContextSummary(context) : null;
85
+ }
86
+ readFrameworkRunState(context) {
87
+ const runState = this.persistence.frameworkRunState.get(context);
88
+ return runState === undefined || runState === "" ? null : runState;
89
+ }
90
+ readMessageIndex(context) {
91
+ return this.persistence.agentMessageIndex.get(context) ?? null;
92
+ }
93
+ /** Sequence of the oldest stored message: retention deleted the earlier ones. */
94
+ readFirstStoredSequence(context) {
95
+ return this.readMessageIndex(context)?.firstSequence ?? 1;
96
+ }
97
+ /**
98
+ * Returns the stored messages with sequences in [start, end), from the `AgentMessages` window and the
99
+ * `AgentMessageChunks` instances. It requires the whole window and every chunk it reads to be loaded.
100
+ */
101
+ readMessages(context, start, end) {
102
+ const index = this.readMessageIndex(context);
103
+ const messages = [];
104
+ const chunks = new Map();
105
+ for (let sequence = Math.max(index?.firstSequence ?? 1, start); sequence < end; sequence += 1) {
106
+ const range = index === null ? undefined : findMessageChunkRange(index, sequence);
107
+ if (range === undefined) {
108
+ messages.push(this.readWindowMessage(context, sequence));
109
+ continue;
110
+ }
111
+ let chunk = chunks.get(range.firstSequence);
112
+ if (chunk === undefined) {
113
+ chunk = this.readChunkMessages(context, range);
114
+ chunks.set(range.firstSequence, chunk);
115
+ }
116
+ messages.push(chunk[sequence - range.firstSequence]);
117
+ }
118
+ return messages;
119
+ }
120
+ readWindowMessage(context, sequence) {
121
+ const message = this.persistence.agentMessages.get(context, messageSequenceKey(sequence));
122
+ if (message === undefined) {
123
+ throw new Error(`AgentMessages has no message ${sequence} in session ${context.flowId}`);
124
+ }
125
+ return message;
126
+ }
127
+ /** Reads one loaded chunk and checks that it holds exactly the messages its range names. */
128
+ readChunkMessages(context, range) {
129
+ const key = messageSequenceKey(range.firstSequence);
130
+ let chunk;
131
+ try {
132
+ chunk = this.persistence.agentMessageChunks.get(context, key);
133
+ }
134
+ catch (error) {
135
+ if (error instanceof AttributeMapNotLoadedError) {
136
+ throw new Error(`AgentMessageChunks instance ${key} is not loaded`);
137
+ }
138
+ throw error;
139
+ }
140
+ if (chunk === undefined) {
141
+ throw new Error(`AgentMessageChunks instance ${key} is not loaded`);
142
+ }
143
+ if (!isMessageChunkOfRange(chunk, range)) {
144
+ throw new Error(`AgentMessageChunks instance ${key} does not hold messages ${range.firstSequence} through ${range.lastSequence}`);
145
+ }
146
+ return chunk.messages;
147
+ }
148
+ /**
149
+ * Counts the active turn's stored messages after the user messages that started it, before this Step's
150
+ * appends. A turn without its `TurnReplySequence`, such as one a 0.1.0 Worker started, is counted from its
151
+ * stored messages instead.
152
+ */
153
+ countStoredTurnReplies(context, state, turnId) {
154
+ return state.nextMessageSequence - this.readTurnReplySequence(context, state, turnId);
155
+ }
156
+ /** Returns the first sequence after the turn's user messages, from `TurnReplySequence` or the stored messages. */
157
+ readTurnReplySequence(context, state, turnId) {
158
+ const stored = this.persistence.turnReplySequence.get(context);
159
+ if (stored !== undefined && stored.turnId === turnId) {
160
+ return stored.sequence;
161
+ }
162
+ const turnMessages = this.readStoredTurnMessages(context, state, turnId);
163
+ return state.nextMessageSequence - countMessagesAfterUserMessages(turnMessages, turnId);
164
+ }
165
+ /**
166
+ * Returns the turn's newest stored messages, read back until another turn's, the oldest stored message, or a
167
+ * chunk this Step did not load.
168
+ */
169
+ readStoredTurnMessages(context, state, turnId) {
170
+ const index = this.readMessageIndex(context);
171
+ const turnMessages = [];
172
+ for (let sequence = state.nextMessageSequence - 1; sequence >= (index?.firstSequence ?? 1); sequence -= 1) {
173
+ const message = this.readLoadedMessage(context, index, sequence);
174
+ if (message === undefined || message.turnId !== turnId) {
175
+ break;
176
+ }
177
+ turnMessages.push(message);
178
+ }
179
+ return turnMessages.reverse();
180
+ }
181
+ /** Reads one stored message from the window or its chunk, or returns `undefined` when its chunk is not loaded. */
182
+ readLoadedMessage(context, index, sequence) {
183
+ const range = index === null ? undefined : findMessageChunkRange(index, sequence);
184
+ if (range === undefined) {
185
+ return this.readWindowMessage(context, sequence);
186
+ }
187
+ try {
188
+ return this.readChunkMessages(context, range)[sequence - range.firstSequence];
189
+ }
190
+ catch (error) {
191
+ if (error instanceof Error && error.message.endsWith("is not loaded")) {
192
+ return undefined;
193
+ }
194
+ throw error;
195
+ }
196
+ }
197
+ /** First sequence of the model input: after the runner's summary, and never before the oldest stored message. */
198
+ readModelInputFirstSequence(context, summary) {
199
+ return Math.max(this.readFirstStoredSequence(context), summary === null ? 1 : summary.lastSequence + 1);
200
+ }
201
+ /**
202
+ * Returns the archive chunks that hold messages in [start, end): every chunk when compaction is off, since
203
+ * the model input then covers them all, else exact instances.
204
+ */
205
+ messageChunkLoads(context, start, end = Number.MAX_SAFE_INTEGER) {
206
+ const index = this.readMessageIndex(context);
207
+ if (index === null) {
208
+ return { isWholeMap: false, instances: [] };
209
+ }
210
+ const instances = index.chunks
211
+ .filter((range) => range.lastSequence >= start && range.firstSequence < end)
212
+ .map((range) => this.persistence.agentMessageChunks.load(messageSequenceKey(range.firstSequence)));
213
+ return { isWholeMap: false, instances };
214
+ }
215
+ /** The chunk loads of a Step that rebuilds the model input. */
216
+ modelInputChunkLoads(context, summary) {
217
+ if (this.config.compaction === undefined) {
218
+ return { isWholeMap: true, instances: [] };
219
+ }
220
+ return this.messageChunkLoads(context, this.readModelInputFirstSequence(context, summary));
221
+ }
222
+ /** Moves to `RunModel`, loading the window and the archive chunks of the model input. */
223
+ goToRunModel(context, summary = this.readRunnerContextSummary(context)) {
224
+ return this.goToModelInputStep(context, agentSessionStepTypes.runModel, summary);
225
+ }
226
+ /** Moves to `ApplyTurnControl`, which may rebuild the model input for unfinished-turn messages. */
227
+ goToApplyTurnControl(context) {
228
+ return this.goToModelInputStep(context, agentSessionStepTypes.applyTurnControl, this.readRunnerContextSummary(context));
229
+ }
230
+ goToModelInputStep(context, stepType, summary) {
231
+ const options = messageStepOptions(this.persistence, stepType, this.modelInputChunkLoads(context, summary));
232
+ return goToMany(StepMovement.of(new StepRef(stepType), undefined, options));
233
+ }
234
+ /** Returns the turn's tool call records in deferral order; requires the AgentToolCalls load. */
235
+ readTurnToolCalls(context, turnId) {
236
+ if (turnId === null) {
237
+ return [];
238
+ }
239
+ const toolCalls = this.persistence.agentToolCalls;
240
+ return toolCalls
241
+ .getAllInstanceKeys(context)
242
+ .map((key) => toolCalls.get(context, key))
243
+ .filter((toolCall) => toolCall.turnId === turnId)
244
+ .sort((left, right) => left.deferralOrder - right.deferralOrder);
245
+ }
246
+ /** Returns one tool call record; requires the AgentToolCalls load. */
247
+ readToolCall(context, toolCallId) {
248
+ const toolCall = this.persistence.agentToolCalls.get(context, toolCallId);
249
+ if (toolCall === undefined) {
250
+ throw new Error(`tool call ${toolCallId} has no record in session ${context.flowId}`);
251
+ }
252
+ return toolCall;
253
+ }
254
+ /** Tells `ResumeTurn` that one tool call of the batch has a final record. */
255
+ publishToolCallResult(context, toolCallId) {
256
+ this.persistence.toolCallResults.publish(context, { toolCallId });
257
+ }
258
+ writeToolCall(context, toolCall) {
259
+ this.persistence.agentToolCalls.set(context, toolCall.toolCallId, toolCall);
260
+ }
261
+ /**
262
+ * Writes a final record, its `tool_call_finished` event, and its `ToolCallResults` notice in one commit.
263
+ * Pass the Step's open events writer, if it has one, so the event keeps its order.
264
+ */
265
+ finishToolCall(context, toolCall, events) {
266
+ if (!isFinalToolCallStatus(toolCall.status)) {
267
+ throw new TypeError(`tool call ${toolCall.toolCallId} is not final: ${toolCall.status}`);
268
+ }
269
+ this.writeToolCall(context, toolCall);
270
+ const finalStatus = toolCall.status;
271
+ if (events === undefined) {
272
+ this.writeEvents(context, toolCall.turnId, (writer) => writer.writeToolCallFinished(toolCall.toolCallId, toolCall.toolName, finalStatus));
273
+ }
274
+ else {
275
+ events.writeToolCallFinished(toolCall.toolCallId, toolCall.toolName, finalStatus);
276
+ }
277
+ this.publishToolCallResult(context, toolCall.toolCallId);
278
+ }
279
+ writeInterruptRaised(events, interrupt) {
280
+ events.writeEvent("interrupt_raised", {
281
+ interrupt_id: interrupt.interruptId,
282
+ kind: interrupt.kind,
283
+ tool_call_id: interrupt.toolCallId,
284
+ tool_name: interrupt.toolName,
285
+ deadline: interrupt.deadline,
286
+ question: interrupt.question ?? null,
287
+ choices: interrupt.choices ?? [],
288
+ allows_free_text: interrupt.allowsFreeText ?? true,
289
+ });
290
+ }
291
+ writeInterruptResolved(context, turnId, resolution) {
292
+ this.writeEvents(context, turnId, (events) => events.writeEvent("interrupt_resolved", {
293
+ interrupt_id: resolution.interruptId,
294
+ kind: resolution.kind,
295
+ approved: resolution.isApproved,
296
+ answered: resolution.isAnswered,
297
+ rejection_message: resolution.rejectionMessage,
298
+ cancellation_reason: resolution.cancellationReason ?? null,
299
+ }));
300
+ }
301
+ /** Writes the `interrupt_resolved` event of an interrupt that steering or `cancel_turn` removed. */
302
+ writeInterruptCancelled(context, turnId, interruptId, kind, reason) {
303
+ this.writeInterruptResolved(context, turnId, {
304
+ interruptId,
305
+ kind,
306
+ isApproved: false,
307
+ isAnswered: false,
308
+ rejectionMessage: toolCallCancellationResults[reason],
309
+ cancellationReason: reason,
310
+ });
311
+ }
312
+ /**
313
+ * Returns why the turn ends at this boundary, or `null`. It reads committed state only: a matching
314
+ * `TurnCancellationRequest`, then any pending steered message.
315
+ */
316
+ readTurnControlReason(context, turnId) {
317
+ if (turnId === null) {
318
+ return null;
319
+ }
320
+ const request = this.persistence.turnCancellationRequest.get(context);
321
+ // Turn IDs never repeat, so a request for an earlier turn never matches.
322
+ if (request !== undefined && request.turnId === turnId) {
323
+ return "turn_cancelled";
324
+ }
325
+ return context.channelSize(this.persistence.steeredUserMessages) > 0 ? "steered" : null;
326
+ }
327
+ /** Builds the runner's view of a turn; every record must be final. */
328
+ createTurnContext(context, state, turnId, toolCalls, events, heartbeat) {
329
+ const runnerToolCalls = toolCalls.map(toRunnerToolCall);
330
+ const toolCallsById = new Map(runnerToolCalls.map((toolCall) => [toolCall.toolCallId, toolCall]));
331
+ const frameworkRunState = this.readFrameworkRunState(context);
332
+ const contextSummary = this.readRunnerContextSummary(context);
333
+ const firstStoredSequence = this.readFirstStoredSequence(context);
334
+ if (contextSummary === null && firstStoredSequence > 1) {
335
+ console.warn(`dex-agents: session ${context.flowId} stores no messages before ${firstStoredSequence}; retention ` +
336
+ "deleted them, and this runner cannot use the context summary that covers them");
337
+ }
338
+ return {
339
+ flowId: context.flowId,
340
+ turnId,
341
+ attempt: context.attempt,
342
+ stepExecutionId: context.stepExecutionId,
343
+ messages: this.readMessages(context, this.readModelInputFirstSequence(context, contextSummary), state.nextMessageSequence),
344
+ frameworkRunState,
345
+ isResuming: frameworkRunState !== null,
346
+ toolCalls: runnerToolCalls,
347
+ events,
348
+ cancellationSignal: context.cancellationSignal,
349
+ deadline: context.deadline,
350
+ enabledBuiltInTools: this.config.builtInTools,
351
+ interactionMode: state.interactionMode ?? "chat",
352
+ plan: this.readOptionalAgentPlan(context),
353
+ contextSummary,
354
+ sessionSettings: this.readTurnSessionSettings(context),
355
+ toolPolicies: this.config.toolPolicies,
356
+ defaultToolEffect: this.config.defaultToolEffect,
357
+ resolveToolPolicy: (toolName, frameworkEffect) => this.resolveToolPolicy(toolName, frameworkEffect),
358
+ recordedToolCall: (toolCallId) => toolCallsById.get(toolCallId),
359
+ heartbeat,
360
+ };
361
+ }
362
+ /**
363
+ * Returns the runner's unfinished-turn messages for an ending turn, or `null` when the runner has no such
364
+ * method or it throws, so the caller stores its note instead.
365
+ */
366
+ buildUnfinishedTurnMessages(context, state, turnId, finalToolCalls) {
367
+ if (this.runner.buildUnfinishedTurnMessages === undefined) {
368
+ return null;
369
+ }
370
+ const events = this.createEventWriter(context, turnId);
371
+ try {
372
+ const turnContext = this.createTurnContext(context, state, turnId, finalToolCalls, events, () => Promise.resolve());
373
+ const messages = this.runner.buildUnfinishedTurnMessages(turnContext);
374
+ if (!Array.isArray(messages) || !messages.every(isFrameworkMessage)) {
375
+ throw new TypeError("buildUnfinishedTurnMessages must return an array of framework messages");
376
+ }
377
+ return messages;
378
+ }
379
+ catch (error) {
380
+ // A runner defect must not block the end of the turn; the caller's note still tells the model.
381
+ console.warn(`dex-agents: no unfinished-turn messages are stored for ${turnId}:`, error);
382
+ return null;
383
+ }
384
+ finally {
385
+ events.close();
386
+ }
387
+ }
388
+ isEnabledBuiltInTool(toolName) {
389
+ return this.config.builtInTools.has(toolName);
390
+ }
391
+ /**
392
+ * Appends messages, writes the advanced AgentState, and returns it.
393
+ *
394
+ * A message goes into the `AgentMessages` window, or, when its own chunk would exceed `messageChunkMaxBytes`,
395
+ * straight into a chunk of its own, so the first store writes the largest value it ever needs. In the same
396
+ * commit it moves the oldest window messages into archive chunks while the window is over its size and
397
+ * applies retention; it writes `AgentMessageIndex` only when one of them changed it. It requires the whole
398
+ * window to be loaded. With `startsTurn`, `TurnReplySequence` records where the turn's replies begin.
399
+ */
400
+ appendMessages(context, state, turnId, frameworkMessages, startsTurn = false) {
401
+ if (frameworkMessages.length === 0 && !startsTurn) {
402
+ this.writeAgentState(context, state);
403
+ return state;
404
+ }
405
+ const activeTurnId = state.activeTurnId;
406
+ if (!startsTurn && activeTurnId !== null && activeTurnId !== undefined) {
407
+ const stored = this.persistence.turnReplySequence.get(context);
408
+ if (stored === undefined || stored.turnId !== activeTurnId) {
409
+ // Recorded before a rollover can move the turn's first messages out of reach.
410
+ this.persistence.turnReplySequence.set(context, {
411
+ turnId: activeTurnId,
412
+ sequence: this.readTurnReplySequence(context, state, activeTurnId),
413
+ });
414
+ }
415
+ }
416
+ const storedIndex = this.readMessageIndex(context);
417
+ let index = storedIndex ?? emptyMessageIndex();
418
+ const directChunks = [];
419
+ let nextMessageSequence = state.nextMessageSequence;
420
+ const createdAt = new Date().toISOString();
421
+ for (const frameworkMessage of frameworkMessages) {
422
+ const message = {
423
+ sequence: nextMessageSequence,
424
+ turnId,
425
+ role: frameworkMessage.role,
426
+ text: frameworkMessage.text,
427
+ toolCallId: frameworkMessage.toolCallId,
428
+ toolName: frameworkMessage.toolName,
429
+ toolArguments: frameworkMessage.toolArguments,
430
+ toolOutput: frameworkMessage.toolOutput,
431
+ frameworkItem: frameworkMessage.frameworkItem,
432
+ createdAt,
433
+ };
434
+ const key = messageSequenceKey(nextMessageSequence);
435
+ if (messageChunkOverheadBytes + agentMessageBytes(message) > this.config.messageChunkMaxBytes) {
436
+ this.persistence.agentMessageChunks.set(context, key, { messages: [message] });
437
+ directChunks.push({ firstSequence: nextMessageSequence, lastSequence: nextMessageSequence });
438
+ }
439
+ else {
440
+ this.persistence.agentMessages.set(context, key, message);
441
+ }
442
+ nextMessageSequence += 1;
443
+ }
444
+ if (directChunks.length > 0) {
445
+ index = { ...index, chunks: [...index.chunks, ...directChunks] };
446
+ }
447
+ if (startsTurn) {
448
+ this.persistence.turnReplySequence.set(context, { turnId, sequence: nextMessageSequence });
449
+ }
450
+ index = this.rollOverMessageWindow(context, index, nextMessageSequence);
451
+ index = this.applyMessageRetention(context, index, nextMessageSequence, this.readContextSummary(context));
452
+ if (!isSameMessageIndex(storedIndex ?? emptyMessageIndex(), index)) {
453
+ this.persistence.agentMessageIndex.set(context, index);
454
+ }
455
+ const advancedState = { ...state, nextMessageSequence };
456
+ this.writeAgentState(context, advancedState);
457
+ return advancedState;
458
+ }
459
+ /**
460
+ * Moves the oldest window messages into immutable chunks while the window holds more than its size. A chunk
461
+ * closes at `messageChunkSize` messages, at `messageChunkMaxBytes` bytes, or before a message stored in its
462
+ * own chunk. One commit writes at most {@link messageRolloverMaxChunksPerCommit} chunks and
463
+ * {@link messageRolloverMaxBytesPerCommit} bytes, so a backlog drains over several commits.
464
+ */
465
+ rollOverMessageWindow(context, index, nextMessageSequence) {
466
+ const chunks = [...index.chunks];
467
+ const directStarts = new Set(chunks.filter((range) => range.firstSequence >= index.windowFirstSequence).map((range) => range.firstSequence));
468
+ let windowFirstSequence = skipDirectChunks(directStarts, index.windowFirstSequence);
469
+ const windowMessageCount = () => nextMessageSequence -
470
+ windowFirstSequence -
471
+ [...directStarts].filter((sequence) => sequence >= windowFirstSequence).length;
472
+ let movedChunks = 0;
473
+ let movedBytes = 0;
474
+ while (windowMessageCount() > this.config.messageWindowSize && movedChunks < messageRolloverMaxChunksPerCommit) {
475
+ const messages = [];
476
+ let chunkBytes = messageChunkOverheadBytes - 1;
477
+ for (let sequence = windowFirstSequence; sequence < nextMessageSequence && messages.length < this.config.messageChunkSize && !directStarts.has(sequence); sequence += 1) {
478
+ const message = this.readWindowMessage(context, sequence);
479
+ const withMessageBytes = chunkBytes + 1 + agentMessageBytes(message);
480
+ if (messages.length > 0 && withMessageBytes > this.config.messageChunkMaxBytes) {
481
+ break;
482
+ }
483
+ messages.push(message);
484
+ chunkBytes = withMessageBytes;
485
+ }
486
+ if (movedChunks > 0 && movedBytes + chunkBytes > messageRolloverMaxBytesPerCommit) {
487
+ break;
488
+ }
489
+ for (const message of messages) {
490
+ this.persistence.agentMessages.delete(context, messageSequenceKey(message.sequence));
491
+ }
492
+ this.persistence.agentMessageChunks.set(context, messageSequenceKey(windowFirstSequence), { messages });
493
+ const lastSequence = windowFirstSequence + messages.length - 1;
494
+ chunks.push({ firstSequence: windowFirstSequence, lastSequence });
495
+ windowFirstSequence = skipDirectChunks(directStarts, lastSequence + 1);
496
+ movedChunks += 1;
497
+ movedBytes += chunkBytes;
498
+ }
499
+ if (windowFirstSequence === index.windowFirstSequence && chunks.length === index.chunks.length) {
500
+ return index;
501
+ }
502
+ chunks.sort((left, right) => left.firstSequence - right.firstSequence);
503
+ return { chunks, firstSequence: firstStoredSequence(chunks, windowFirstSequence), windowFirstSequence };
504
+ }
505
+ /**
506
+ * Deletes the oldest archive chunk while every message in it is summarized and at least `retainedMessageLimit`
507
+ * stored messages come after it, so the newest `retainedMessageLimit` messages always stay. It runs only with
508
+ * compaction and a limit.
509
+ */
510
+ applyMessageRetention(context, index, nextMessageSequence, summary) {
511
+ const limit = this.config.retainedMessageLimit;
512
+ if (this.config.compaction === undefined || limit === null || summary === null) {
513
+ return index;
514
+ }
515
+ let deletedChunks = 0;
516
+ for (const range of index.chunks) {
517
+ // Only archived chunks, before the window; a message stored in its own chunk waits for the rollover.
518
+ if (range.firstSequence >= index.windowFirstSequence ||
519
+ range.lastSequence > summary.lastSequence ||
520
+ nextMessageSequence - (range.lastSequence + 1) < limit) {
521
+ break;
522
+ }
523
+ this.persistence.agentMessageChunks.delete(context, messageSequenceKey(range.firstSequence));
524
+ deletedChunks += 1;
525
+ }
526
+ if (deletedChunks === 0) {
527
+ return index;
528
+ }
529
+ const chunks = index.chunks.slice(deletedChunks);
530
+ return { ...index, chunks, firstSequence: firstStoredSequence(chunks, index.windowFirstSequence) };
531
+ }
532
+ /** Appends the turn's user messages, in order, opens the next turn, and moves to RunModel. */
533
+ beginTurn(context, state, messages, stepType, interactionMode = "chat", executedPlan, wasPlanDraft = false, sessionSettings = this.readSessionSettings(context)) {
534
+ const turnId = `turn-${state.turnCount + 1}`;
535
+ // The turn keeps these until it ends; later updates wait for the next turn.
536
+ if (sessionSettings !== null && Object.keys(sessionSettings).length > 0) {
537
+ this.persistence.turnSessionSettings.set(context, { turnId, sessionSettings });
538
+ }
539
+ else if (this.persistence.turnSessionSettings.get(context) !== undefined) {
540
+ this.persistence.turnSessionSettings.delete(context);
541
+ }
542
+ const sequence = state.nextMessageSequence;
543
+ return this.tracer.traceUserMessageActivation(context, stepType, turnId, messageSequenceKey(sequence), messages.map((message) => message.text).join("\n"), () => {
544
+ const userMessages = messages.map((message) => this.runner.buildUserMessage(message.text));
545
+ if (userMessages.some((userMessage) => userMessage.role !== "user")) {
546
+ throw new TypeError("buildUserMessage must return a message with role user");
547
+ }
548
+ const stateWithMessage = this.appendMessages(context, state, turnId, userMessages, true);
549
+ this.writeAgentState(context, {
550
+ ...stateWithMessage,
551
+ status: "running_model",
552
+ activeTurnId: turnId,
553
+ interactionMode,
554
+ acceptsTurnControl: true,
555
+ });
556
+ this.writeEvents(context, turnId, (events) => {
557
+ events.writeEvent("turn_started", { sequence });
558
+ if (executedPlan !== undefined) {
559
+ events.writeEvent("plan_execution_started", { revision: executedPlan.revision });
560
+ if (wasPlanDraft) {
561
+ writePlanUpdated(events, executedPlan);
562
+ }
563
+ }
564
+ });
565
+ return this.goToRunModel(context);
566
+ });
567
+ }
568
+ /**
569
+ * Returns the mode of a turn that these user messages start: `planning` when plan mode is enabled and a
570
+ * message has `planMode` or a `draft` plan exists, else `chat`.
571
+ */
572
+ decideInteractionMode(context, messages) {
573
+ if (!this.config.planModeEnabled) {
574
+ return "chat";
575
+ }
576
+ const isPlanModeRequested = messages.some((message) => message.planMode === true);
577
+ return isPlanModeRequested || this.readOptionalAgentPlan(context)?.status === "draft" ? "planning" : "chat";
578
+ }
579
+ /** Ends a steered or cancelled turn with its unfinished-turn messages, or with a note; returns the new state. */
580
+ endControlledTurn(context, initialState, turnId, steeredMessageCount) {
581
+ const transitions = this;
582
+ const persistence = this.persistence;
583
+ const toolCalls = transitions.readTurnToolCalls(context, turnId);
584
+ const openToolCall = toolCalls.find((toolCall) => !isFinalToolCallStatus(toolCall.status));
585
+ if (openToolCall !== undefined) {
586
+ throw new Error(`${turnId} ended before tool call ${openToolCall.toolCallId} finished`);
587
+ }
588
+ const isCancelled = transitions.readTurnControlReason(context, turnId) === "turn_cancelled";
589
+ const storedMessageCount = transitions.countStoredTurnReplies(context, initialState, turnId);
590
+ let messages = transitions.buildUnfinishedTurnMessages(context, initialState, turnId, toolCalls);
591
+ if (messages === null) {
592
+ messages = [];
593
+ if (toolCalls.length > 0) {
594
+ // The records are deleted below; the note keeps the next model from repeating them.
595
+ const note = transitions.runner.buildUserMessage(buildToolCallNote(isCancelled ? turnCancelledNoteHeader : turnSteeredNoteHeader, toolCalls));
596
+ if (note.role !== "user") {
597
+ throw new TypeError("buildUserMessage must return a message with role user");
598
+ }
599
+ messages = [note];
600
+ }
601
+ }
602
+ const stateWithMessages = transitions.appendMessages(context, initialState, turnId, messages);
603
+ for (const toolCall of toolCalls) {
604
+ persistence.agentToolCalls.delete(context, toolCall.toolCallId);
605
+ }
606
+ persistence.frameworkRunState.delete(context);
607
+ const endedState = {
608
+ ...stateWithMessages,
609
+ status: "waiting_for_user_message",
610
+ turnCount: stateWithMessages.turnCount + 1,
611
+ activeTurnId: null,
612
+ lastTurnError: null,
613
+ acceptsTurnControl: false,
614
+ };
615
+ transitions.writeAgentState(context, endedState);
616
+ const messageCount = storedMessageCount + messages.length;
617
+ transitions.writeEvents(context, turnId, (events) => isCancelled
618
+ ? events.writeEvent("turn_cancelled", { message_count: messageCount })
619
+ : events.writeEvent("turn_steered", {
620
+ steered_message_count: steeredMessageCount,
621
+ message_count: messageCount,
622
+ }));
623
+ return endedState;
624
+ }
625
+ goToAwaitUserMessage() {
626
+ return goTo(new StepRef(agentSessionStepTypes.awaitUserMessage), undefined);
627
+ }
628
+ /** Reports whether `cancel_turn` stopped this turn; reads committed state only. */
629
+ isTurnCancelled(context, turnId) {
630
+ return this.readTurnControlReason(context, turnId) === "turn_cancelled";
631
+ }
632
+ resolveToolPolicy(toolName, frameworkEffect) {
633
+ return resolveToolPolicy(toolName, this.config.toolPolicies, this.config.defaultToolEffect, frameworkEffect);
634
+ }
635
+ recordUsage(context, usage, events) {
636
+ const sessionUsage = addAgentUsage(this.readAgentUsage(context), usage);
637
+ this.persistence.agentUsage.set(context, sessionUsage);
638
+ events.writeEvent("usage_recorded", {
639
+ recorded: usagePayload(usage),
640
+ session: usagePayload(sessionUsage),
641
+ });
642
+ }
643
+ createEventWriter(context, turnId) {
644
+ return new StreamAgentEventWriter(context, this.persistence.agentEvents, turnId);
645
+ }
646
+ writeEvents(context, turnId, write) {
647
+ const events = this.createEventWriter(context, turnId);
648
+ try {
649
+ write(events);
650
+ }
651
+ finally {
652
+ events.close();
653
+ }
654
+ }
655
+ /** Runs an operation while recording a heartbeat every 20 seconds. */
656
+ async runWithHeartbeats(context, operation, recordHeartbeat = () => this.recordHeartbeat(context)) {
657
+ const heartbeatTimer = setInterval(() => {
658
+ // A rejected heartbeat means the attempt already ended; the operation result decides the outcome.
659
+ recordHeartbeat().catch(() => undefined);
660
+ }, heartbeatIntervalMs);
661
+ try {
662
+ return await operation();
663
+ }
664
+ finally {
665
+ clearInterval(heartbeatTimer);
666
+ }
667
+ }
668
+ recordHeartbeat(context) {
669
+ return context.recordHeartbeat(context.stepExecutionId, stringCodec);
670
+ }
671
+ }
672
+ /**
673
+ * Heartbeats of one tool Step attempt. Each carries the latest checkpoint, else the Step execution ID,
674
+ * which every attempt shares and so never reads as a checkpoint.
675
+ */
676
+ export class ToolStepHeartbeats {
677
+ context;
678
+ previousCheckpoint;
679
+ checkpoint;
680
+ constructor(context) {
681
+ this.context = context;
682
+ const previous = context.hasLastHeartbeatValue() ? context.getLastHeartbeatValue(stringCodec) : undefined;
683
+ this.previousCheckpoint = previous === undefined || previous === context.stepExecutionId ? undefined : previous;
684
+ // Every heartbeat replaces the saved value, so carry the earlier attempt's checkpoint forward.
685
+ this.checkpoint = this.previousCheckpoint;
686
+ }
687
+ record() {
688
+ return this.context.recordHeartbeat(this.checkpoint ?? this.context.stepExecutionId, stringCodec);
689
+ }
690
+ saveCheckpoint(checkpoint) {
691
+ this.checkpoint = checkpoint;
692
+ return this.record();
693
+ }
694
+ }
695
+ /** Returns a record as a runner sees it; only records that runners may see are accepted. */
696
+ export function toRunnerToolCall(record) {
697
+ if (record.status === "awaiting_answer" || record.status === "waiting_for_timer" || record.status === "waiting_for_task") {
698
+ throw new TypeError(`tool call ${record.toolCallId} is still ${record.status}`);
699
+ }
700
+ return record;
701
+ }
702
+ function isFrameworkMessage(value) {
703
+ const message = value;
704
+ const isNullableString = (field) => field === null || typeof field === "string";
705
+ return (typeof message === "object" &&
706
+ message !== null &&
707
+ (message.role === "user" || message.role === "assistant" || message.role === "tool") &&
708
+ typeof message.frameworkItem === "string" &&
709
+ isNullableString(message.text) &&
710
+ isNullableString(message.toolCallId) &&
711
+ isNullableString(message.toolName) &&
712
+ isNullableString(message.toolArguments) &&
713
+ isNullableString(message.toolOutput));
714
+ }
715
+ /** Moves to `FinishTurnCancellation` for one turn; its locks come from the Step's own options. */
716
+ export function finishTurnCancellationMovement(turnId) {
717
+ return StepMovement.of(new StepRef(agentSessionStepTypes.finishTurnCancellation), turnId);
718
+ }
719
+ /** Reports whether `cancel_turn` abandons this call's started task. */
720
+ export function isAbandonedToolTask(toolCall) {
721
+ return ((toolCall.taskId ?? null) !== null &&
722
+ (toolCall.status === "waiting_for_task" || toolCall.status === "awaiting_answer"));
723
+ }
724
+ /** Reports whether steering or `cancel_turn` may abandon a call that waits in a session Step. */
725
+ export function isCancellableByTurnControl(toolCall) {
726
+ // Each of these waits for a person or a Timer; the call has taken no effect. A task's question
727
+ // waits for a person too, but its task has started, and a waiting_for_task record is never listed.
728
+ return (toolCall.status === "awaiting_approval" ||
729
+ (toolCall.status === "awaiting_answer" && (toolCall.taskId ?? null) === null) ||
730
+ toolCall.status === "waiting_for_timer");
731
+ }
732
+ /** Returns a record that steering or `cancel_turn` abandoned before the call started. */
733
+ export function cancelledToolCall(toolCall, reason) {
734
+ return {
735
+ ...toolCall,
736
+ status: "rejected",
737
+ rejectionMessage: toolCallCancellationResults[reason],
738
+ cancellationReason: reason,
739
+ inputQuestion: null,
740
+ inputChoices: [],
741
+ inputAllowsFreeText: true,
742
+ };
743
+ }
744
+ /**
745
+ * Returns the kept chunk that holds a message, found by binary search over the index's ordered ranges, or
746
+ * `undefined` when the message is in the window.
747
+ */
748
+ export function findMessageChunkRange(index, sequence) {
749
+ let low = 0;
750
+ let high = index.chunks.length - 1;
751
+ while (low <= high) {
752
+ const middle = (low + high) >> 1;
753
+ const range = index.chunks[middle];
754
+ if (sequence < range.firstSequence) {
755
+ high = middle - 1;
756
+ }
757
+ else if (sequence > range.lastSequence) {
758
+ low = middle + 1;
759
+ }
760
+ else {
761
+ return range;
762
+ }
763
+ }
764
+ if (sequence < index.windowFirstSequence) {
765
+ throw new Error(`AgentMessageIndex lists no chunk that holds message ${sequence}`);
766
+ }
767
+ return undefined;
768
+ }
769
+ /** The index of a session whose messages are all in the window, as a new session or a 0.1.0 one stores them. */
770
+ function emptyMessageIndex() {
771
+ return { chunks: [], firstSequence: 1, windowFirstSequence: 1 };
772
+ }
773
+ /** The oldest stored message: the first archived chunk's first sequence, else the window's first. */
774
+ function firstStoredSequence(chunks, windowFirstSequence) {
775
+ const first = chunks[0];
776
+ return first !== undefined && first.firstSequence < windowFirstSequence ? first.firstSequence : windowFirstSequence;
777
+ }
778
+ /** Returns the first sequence at or after `sequence` that is not a message stored in its own chunk. */
779
+ function skipDirectChunks(directStarts, sequence) {
780
+ let next = sequence;
781
+ while (directStarts.has(next)) {
782
+ next += 1;
783
+ }
784
+ return next;
785
+ }
786
+ /** Whether a chunk holds exactly the messages of its range, in order. */
787
+ export function isMessageChunkOfRange(chunk, range) {
788
+ return (chunk.messages.length === range.lastSequence - range.firstSequence + 1 &&
789
+ chunk.messages.every((message, offset) => message.sequence === range.firstSequence + offset));
790
+ }
791
+ /** The UTF-8 bytes of a message's JSON as this core stores it. */
792
+ function agentMessageBytes(message) {
793
+ return utf8Encoder.encode(JSON.stringify(encodeAgentMessage(message))).length;
794
+ }
795
+ function isSameMessageIndex(left, right) {
796
+ return (left.firstSequence === right.firstSequence &&
797
+ left.windowFirstSequence === right.windowFirstSequence &&
798
+ left.chunks.length === right.chunks.length &&
799
+ left.chunks.every((range, offset) => range.firstSequence === right.chunks[offset]?.firstSequence &&
800
+ range.lastSequence === right.chunks[offset]?.lastSequence));
801
+ }
802
+ /** Counts a turn's stored messages after the user messages that started it. */
803
+ export function countMessagesAfterUserMessages(messages, turnId) {
804
+ const turnMessages = messages.filter((message) => message.turnId === turnId);
805
+ const firstNonUserIndex = turnMessages.findIndex((message) => message.role !== "user");
806
+ return firstNonUserIndex === -1 ? 0 : turnMessages.length - firstNonUserIndex;
807
+ }
808
+ export function toolStepTypeForEffect(effect) {
809
+ switch (effect) {
810
+ case "read":
811
+ case "idempotent_write":
812
+ return agentSessionStepTypes.callTool;
813
+ case "non_idempotent_write":
814
+ case "unclassified":
815
+ return agentSessionStepTypes.callSingleAttemptTool;
816
+ }
817
+ }
818
+ /** Returns the text of the failure that selected a recovery Step. */
819
+ export function readRecoveryDetail(context) {
820
+ return recoveryErrorDetail(context.recoveryError);
821
+ }
822
+ /** Returns the text of a recovery error: its detail, else its error type, else "unknown failure". */
823
+ export function recoveryErrorDetail(recoveryError) {
824
+ if (recoveryError === undefined) {
825
+ return "unknown failure";
826
+ }
827
+ return recoveryError.detail !== "" ? recoveryError.detail : recoveryError.errorType;
828
+ }
829
+ /** Writes `plan_updated` with the plan's revision, status, and task count. */
830
+ export function writePlanUpdated(events, plan) {
831
+ events.writeEvent("plan_updated", { revision: plan.revision, status: plan.status, task_count: plan.tasks.length });
832
+ }
833
+ function usagePayload(usage) {
834
+ return {
835
+ model_calls: usage.modelCalls,
836
+ input_tokens: usage.inputTokens,
837
+ output_tokens: usage.outputTokens,
838
+ cached_input_tokens: usage.cachedInputTokens,
839
+ reasoning_tokens: usage.reasoningTokens,
840
+ };
841
+ }
842
+ /**
843
+ * Returns the settings of the active turn from its stored copy: the copy's settings while it belongs to that
844
+ * turn, else `{}`. A turn that a Worker without session settings started, such as 0.1.0, leaves an earlier
845
+ * turn's copy in place, which must not apply.
846
+ */
847
+ export function selectTurnSessionSettings(stored, activeTurnId) {
848
+ return stored !== null && activeTurnId !== null && stored.turnId === activeTurnId ? stored.sessionSettings : {};
849
+ }
850
+ //# sourceMappingURL=agent-session-transitions.js.map