@superdurable/dex-agents 0.2.0 → 0.4.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 (92) hide show
  1. package/README.md +143 -6
  2. package/dist/src/agent-context-compaction.d.ts +150 -4
  3. package/dist/src/agent-context-compaction.js +253 -5
  4. package/dist/src/agent-context-compaction.js.map +1 -1
  5. package/dist/src/agent-framework-runner.d.ts +89 -0
  6. package/dist/src/agent-framework-runner.js +26 -0
  7. package/dist/src/agent-framework-runner.js.map +1 -1
  8. package/dist/src/agent-session-codecs.js +9 -0
  9. package/dist/src/agent-session-codecs.js.map +1 -1
  10. package/dist/src/agent-session-failure.d.ts +58 -0
  11. package/dist/src/agent-session-failure.js +126 -0
  12. package/dist/src/agent-session-failure.js.map +1 -0
  13. package/dist/src/agent-session-flow-graph-cli.d.ts +2 -0
  14. package/dist/src/agent-session-flow-graph-cli.js +47 -0
  15. package/dist/src/agent-session-flow-graph-cli.js.map +1 -0
  16. package/dist/src/agent-session-flow-graph.d.ts +38 -0
  17. package/dist/src/agent-session-flow-graph.js +54 -0
  18. package/dist/src/agent-session-flow-graph.js.map +1 -0
  19. package/dist/src/agent-session-flow-graph.json +2204 -0
  20. package/dist/src/agent-session-flow.d.ts +23 -2
  21. package/dist/src/agent-session-flow.js +5 -0
  22. package/dist/src/agent-session-flow.js.map +1 -1
  23. package/dist/src/agent-session-spend-limit.d.ts +156 -0
  24. package/dist/src/agent-session-spend-limit.js +42 -0
  25. package/dist/src/agent-session-spend-limit.js.map +1 -0
  26. package/dist/src/agent-session-start.d.ts +49 -0
  27. package/dist/src/agent-session-start.js +67 -0
  28. package/dist/src/agent-session-start.js.map +1 -0
  29. package/dist/src/agent-session-step-options.d.ts +52 -1
  30. package/dist/src/agent-session-step-options.js +34 -2
  31. package/dist/src/agent-session-step-options.js.map +1 -1
  32. package/dist/src/agent-session-steps.d.ts +9 -0
  33. package/dist/src/agent-session-steps.js +247 -50
  34. package/dist/src/agent-session-steps.js.map +1 -1
  35. package/dist/src/agent-session-transitions.d.ts +14 -1
  36. package/dist/src/agent-session-transitions.js +61 -1
  37. package/dist/src/agent-session-transitions.js.map +1 -1
  38. package/dist/src/agent-session-values.d.ts +43 -2
  39. package/dist/src/agent-session-values.js +6 -0
  40. package/dist/src/agent-session-values.js.map +1 -1
  41. package/dist/src/agent-session-wait-steps.js +3 -3
  42. package/dist/src/agent-session-wait-steps.js.map +1 -1
  43. package/dist/src/connectors/connector-input-schema.d.ts +19 -0
  44. package/dist/src/connectors/connector-input-schema.js +228 -0
  45. package/dist/src/connectors/connector-input-schema.js.map +1 -0
  46. package/dist/src/connectors/connector-step-context.d.ts +77 -0
  47. package/dist/src/connectors/connector-step-context.js +124 -0
  48. package/dist/src/connectors/connector-step-context.js.map +1 -0
  49. package/dist/src/connectors/connector-tool-source.d.ts +353 -0
  50. package/dist/src/connectors/connector-tool-source.js +541 -0
  51. package/dist/src/connectors/connector-tool-source.js.map +1 -0
  52. package/dist/src/connectors/index.d.ts +3 -0
  53. package/dist/src/connectors/index.js +11 -0
  54. package/dist/src/connectors/index.js.map +1 -0
  55. package/dist/src/index.d.ts +10 -3
  56. package/dist/src/index.js +8 -2
  57. package/dist/src/index.js.map +1 -1
  58. package/dist/src/stepwise/agent-definition.d.ts +8 -0
  59. package/dist/src/stepwise/index.d.ts +2 -1
  60. package/dist/src/stepwise/index.js +2 -1
  61. package/dist/src/stepwise/index.js.map +1 -1
  62. package/dist/src/stepwise/stepwise-agent-runner.d.ts +15 -32
  63. package/dist/src/stepwise/stepwise-agent-runner.js +37 -200
  64. package/dist/src/stepwise/stepwise-agent-runner.js.map +1 -1
  65. package/dist/src/stepwise/stepwise-agent-session-flow.js +1 -0
  66. package/dist/src/stepwise/stepwise-agent-session-flow.js.map +1 -1
  67. package/dist/src/stepwise/tool-source.d.ts +11 -1
  68. package/dist/src/testing/agent-session-test-client.js +2 -5
  69. package/dist/src/testing/agent-session-test-client.js.map +1 -1
  70. package/dist/src/testing/context-window.d.ts +194 -0
  71. package/dist/src/testing/context-window.js +360 -0
  72. package/dist/src/testing/context-window.js.map +1 -0
  73. package/dist/src/testing/index.d.ts +1 -0
  74. package/dist/src/testing/index.js +1 -0
  75. package/dist/src/testing/index.js.map +1 -1
  76. package/dist/src/tool-source-router.d.ts +211 -0
  77. package/dist/src/tool-source-router.js +380 -0
  78. package/dist/src/tool-source-router.js.map +1 -0
  79. package/dist/src/ui-stream/agent-event-sse.d.ts +3 -2
  80. package/dist/src/ui-stream/agent-event-sse.js +11 -3
  81. package/dist/src/ui-stream/agent-event-sse.js.map +1 -1
  82. package/dist/src/ui-stream/agent-session-record-reader.d.ts +2 -0
  83. package/dist/src/ui-stream/agent-session-record-reader.js +5 -0
  84. package/dist/src/ui-stream/agent-session-record-reader.js.map +1 -1
  85. package/dist/src/ui-stream/agent-ui-message-stream-responses.js +3 -2
  86. package/dist/src/ui-stream/agent-ui-message-stream-responses.js.map +1 -1
  87. package/dist/src/ui-stream/agent-ui-message-stream.js +17 -3
  88. package/dist/src/ui-stream/agent-ui-message-stream.js.map +1 -1
  89. package/dist/src/ui-stream/agent-ui-messages.d.ts +2 -0
  90. package/dist/src/ui-stream/agent-ui-messages.js +5 -3
  91. package/dist/src/ui-stream/agent-ui-messages.js.map +1 -1
  92. package/package.json +15 -3
@@ -1 +1 @@
1
- {"version":3,"file":"agent-session-step-options.js","sourceRoot":"","sources":["../../src/agent-session-step-options.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,EAAE;AACF,kDAAkD;AAClD,mEAAmE;AACnE,+CAA+C;AAC/C,EAAE;AACF,0DAA0D;AAE1D,OAAO,EAAE,cAAc,EAAE,OAAO,EAA6D,MAAM,mBAAmB,CAAC;AAGvH,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAEtE,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC;AAC/B,qFAAqF;AACrF,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,sBAAsB,EAAE,EAAE,GAAG,QAAQ;IACrC,kBAAkB,EAAE,CAAC,GAAG,QAAQ;IAChC,YAAY,EAAE,EAAE,eAAe,EAAE,CAAC,EAAE,eAAe,EAAE,EAAE,GAAG,QAAQ,EAAE;IACpE,cAAc,EAAE,cAAc,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,qBAAqB,CAAC,iBAAiB,CAAC,CAAC;CAChE,CAAC;AACjC,qGAAqG;AACrG,MAAM,CAAC,MAAM,aAAa,GAAgB,EAAE,iBAAiB,EAAE,KAAK,EAAE,CAAC;AACvE,mGAAmG;AACnG,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,iBAAiB,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAW,CAAC;AAWhG,sGAAsG;AACtG,MAAM,UAAU,kBAAkB,CAChC,WAAoC,EACpC,QAAgB,EAChB,UAA6B;IAE7B,MAAM,KAAK,GAAG;QACZ,wBAAwB,EAAE,UAAU,CAAC,UAAU;YAC7C,CAAC,CAAC,CAAC,WAAW,CAAC,aAAa,EAAE,WAAW,CAAC,cAAc,EAAE,WAAW,CAAC,kBAAkB,CAAC;YACzF,CAAC,CAAC,CAAC,WAAW,CAAC,aAAa,EAAE,WAAW,CAAC,cAAc,CAAC;QAC3D,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,gCAAgC,EAAE,UAAU,CAAC,SAAS,EAAE,CAAC;KACzG,CAAC;IACF,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,qBAAqB,CAAC,QAAQ;YACjC,OAAO,EAAE,GAAG,cAAc,EAAE,GAAG,oBAAoB,EAAE,GAAG,KAAK,EAAE,CAAC;QAClE,KAAK,qBAAqB,CAAC,gBAAgB;YACzC,OAAO,EAAE,GAAG,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,GAAG,KAAK,EAAE,CAAC;QACtE;YACE,MAAM,IAAI,SAAS,CAAC,aAAa,QAAQ,gCAAgC,CAAC,CAAC;IAC/E,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"agent-session-step-options.js","sourceRoot":"","sources":["../../src/agent-session-step-options.ts"],"names":[],"mappings":"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"}
@@ -34,6 +34,8 @@ export declare class RunModelStep implements Step<void> {
34
34
  getStepType(): string;
35
35
  getStepOptions(): StepOptions;
36
36
  execute(context: AsyncContext): Promise<StepDecision>;
37
+ /** Starts the next queued write or skips the rest; `null` lets a cancelled turn end here. */
38
+ private continueOrderedWrites;
37
39
  /** Returns RunModel's move instead of a model call, or `null` to call the model. */
38
40
  private decideBeforeModelCall;
39
41
  private completeTurn;
@@ -140,6 +142,8 @@ export declare class RecordTurnFailureStep implements Step<CompactContextInput |
140
142
  getStepOptions(): StepOptions;
141
143
  execute(context: Context): StepDecision;
142
144
  }
145
+ /** Reports whether a Step failed because its commit would exceed the Flow's state limit. */
146
+ export declare function isStateLimitFailure(recoveryError: RecoveryErrorInfo | undefined): boolean;
143
147
  /** `last_turn_error` of a turn whose runner stored messages incrementally without the builder. */
144
148
  export declare const missingUnfinishedTurnMessageBuilder = "a runner that sets ToolCallsRequested.newMessages must implement buildUnfinishedTurnMessages";
145
149
  /**
@@ -148,6 +152,11 @@ export declare const missingUnfinishedTurnMessageBuilder = "a runner that sets T
148
152
  */
149
153
  export declare function toolStepOptions(persistence: AgentSessionPersistence, stepType: string, toolCallId?: string): StepOptions;
150
154
  export declare function toolStepMovement(persistence: AgentSessionPersistence, stepType: string, toolCallId: string): StepMovement<string>;
155
+ /**
156
+ * Returns the movement that starts an approved call's tool Step, with the runner's timeout and retry policy.
157
+ * Only `CallTool` takes the retry policy; `CallSingleAttemptTool` keeps one attempt.
158
+ */
159
+ export declare function toolCallMovement(transitions: AgentSessionTransitions, toolCall: AgentToolCallRecord): StepMovement<string>;
151
160
  /** Moves to `PollToolTask`; a poll that keeps failing leaves the task running, so the call is recorded by effect. */
152
161
  export declare function toolTaskPollMovement(persistence: AgentSessionPersistence, toolCall: AgentToolCallRecord): StepMovement<string>;
153
162
  /** Returns the record of a tool's question: `awaiting_answer`, or `failed` when it is blank. */
@@ -8,14 +8,14 @@
8
8
  import { ExecuteFailure, StepMovement, int64Codec, StepRef, StepAttemptProgress, Timer, Wait, deadEnd, forceFail, goTo, goToMany, stringCodec, voidCodec, } from "@superdurable/dex";
9
9
  import { buildPlanClearedOutput, buildPlanUpdatedOutput, isBlankText, validateAskUserArguments, validateDurableWaitArguments, validateWriteTodosArguments, writeTodosCalledMoreThanOnce, } from "./agent-built-in-tools.js";
10
10
  import { isCompactionTriggered, isContextCompactionRunner, readRetainedTokenBudget, } from "./agent-context-compaction.js";
11
- import { ToolExecutionError, } from "./agent-framework-runner.js";
11
+ import { ToolExecutionError, ToolOutcomeUnknownError, } from "./agent-framework-runner.js";
12
12
  import { buildPlanExecutionMessage, planningTurnToolRejection, planStatusAfterWrite } from "./agent-plans.js";
13
13
  import { agentSessionStartInputCodec, turnFailureRouteInputCodec } from "./agent-session-codecs.js";
14
14
  import { messageStepOptions, minuteMs, modelCallStepOptions, recoveryRetry, syncDurability, } from "./agent-session-step-options.js";
15
15
  import { agentSessionStepTypes, allPendingMessages, cancelledToolCall, isAbandonedToolTask, isCancellableByTurnControl, readRecoveryDetail, recoveryErrorDetail, toRunnerToolCall, toolStepTypeForEffect, ToolStepHeartbeats, writePlanUpdated, } from "./agent-session-transitions.js";
16
16
  import { messageSequenceKey, planExecutionKey, } from "./agent-session-persistence.js";
17
17
  import { checkStoredSessionSettings } from "./agent-session-settings.js";
18
- import { approvalDeadlinePassed, blankToolTaskId, buildTurnFailureNote, emptyAgentUsage, invalidToolTaskPollInterval, isFinalToolCallStatus, missingToolTaskPoller, toolCallNotRunErrorPrefix, toolTaskAbandonedError, } from "./agent-session-values.js";
18
+ import { approvalDeadlinePassed, blankToolTaskId, buildTurnFailureNote, emptyAgentUsage, invalidToolTaskPollInterval, isFinalToolCallStatus, missingToolTaskPoller, toolCallNotRunErrorPrefix, toolCallSkippedAfterWriteDetail, toolTaskAbandonedError, } from "./agent-session-values.js";
19
19
  /** Longest wait between two polls of a tool task, as in the Python core; a longer interval is shortened. */
20
20
  const maximumToolTaskPollIntervalSeconds = 86_400;
21
21
  // An int64 Value, as the Python core's Step[int] encodes it.
@@ -159,7 +159,7 @@ export class RunModelStep {
159
159
  }
160
160
  getStepOptions() {
161
161
  // Each movement into RunModel adds the archive chunks of the model input.
162
- return messageStepOptions(this.transitions.persistence, this.getStepType(), staticMessageChunkLoads(this.transitions));
162
+ return messageStepOptions(this.transitions.persistence, this.getStepType(), staticMessageChunkLoads(this.transitions), this.transitions.config.runModelLimit);
163
163
  }
164
164
  async execute(context) {
165
165
  const transitions = this.transitions;
@@ -168,7 +168,14 @@ export class RunModelStep {
168
168
  if (turnId === null) {
169
169
  throw new Error(`the agent session ${context.flowId} has no active turn`);
170
170
  }
171
- const toolCalls = transitions.readTurnToolCalls(context, turnId);
171
+ let toolCalls = transitions.readTurnToolCalls(context, turnId);
172
+ if (toolCalls.some((toolCall) => toolCall.writeQueued === true)) {
173
+ const writeDecision = this.continueOrderedWrites(context, state, turnId, toolCalls);
174
+ if (writeDecision !== null) {
175
+ return writeDecision;
176
+ }
177
+ toolCalls = transitions.readTurnToolCalls(context, turnId);
178
+ }
172
179
  const openToolCall = toolCalls.find((toolCall) => !isFinalToolCallStatus(toolCall.status));
173
180
  if (openToolCall !== undefined) {
174
181
  throw new Error(`RunModel for ${turnId} started before tool call ${openToolCall.toolCallId} finished`);
@@ -184,8 +191,18 @@ export class RunModelStep {
184
191
  if (throughSequence !== null) {
185
192
  return this.startCompaction(context, state, turnContext.contextSummary ?? null, throughSequence);
186
193
  }
194
+ const refusal = await transitions.checkSpendLimit(context, state, turnId, this.getStepType());
195
+ if (refusal !== null) {
196
+ return refusal;
197
+ }
187
198
  return await transitions.tracer.traceModelCall(context, this.getStepType(), turnContext, async (modelCallSpan) => {
188
- const outcome = await transitions.runWithHeartbeats(context, () => transitions.runner.runModelStep(turnContext));
199
+ const outcome = await transitions.runWithHeartbeats(context, async () => {
200
+ const result = await transitions.runner.runModelStep(turnContext);
201
+ if (result?.kind === "completed" || result?.kind === "tool_calls_requested") {
202
+ await transitions.reportModelUsage(context, turnId, this.getStepType(), result.usage);
203
+ }
204
+ return result;
205
+ });
189
206
  switch (outcome?.kind) {
190
207
  case "completed": {
191
208
  modelCallSpan.recordOutcome(outcome, appendedMessageKeys(state, outcome.newMessages));
@@ -211,6 +228,61 @@ export class RunModelStep {
211
228
  events.close();
212
229
  }
213
230
  }
231
+ /** Starts the next queued write or skips the rest; `null` lets a cancelled turn end here. */
232
+ continueOrderedWrites(context, state, turnId, toolCalls) {
233
+ const transitions = this.transitions;
234
+ const queued = toolCalls.filter((toolCall) => toolCall.writeQueued === true);
235
+ const reason = transitions.readTurnControlReason(context, turnId);
236
+ if (reason !== null) {
237
+ transitions.writeEvents(context, turnId, (events) => {
238
+ for (const toolCall of queued) {
239
+ transitions.finishUnscheduledToolCall(context, { ...cancelledToolCall(toolCall, reason), writeQueued: false }, events);
240
+ }
241
+ });
242
+ return null;
243
+ }
244
+ const openToolCall = toolCalls.find((toolCall) => toolCall.writeQueued !== true && !isFinalToolCallStatus(toolCall.status));
245
+ if (openToolCall !== undefined) {
246
+ throw new Error(`RunModel for ${turnId} started before tool call ${openToolCall.toolCallId} finished`);
247
+ }
248
+ const nextWrite = queued[0];
249
+ const previousWrite = toolCalls
250
+ .filter((toolCall) => toolCall.orderedWrite === true &&
251
+ toolCall.writeQueued !== true &&
252
+ toolCall.deferralOrder < nextWrite.deferralOrder)
253
+ .at(-1);
254
+ if (previousWrite !== undefined && previousWrite.status !== "succeeded") {
255
+ transitions.writeEvents(context, turnId, (events) => {
256
+ for (const toolCall of queued) {
257
+ transitions.finishUnscheduledToolCall(context, skippedToolCall(toolCall), events);
258
+ }
259
+ });
260
+ // The skips commit before the model call, so readers see them while the model runs.
261
+ return transitions.goToRunModel(context);
262
+ }
263
+ const createdAt = new Date();
264
+ const traceContext = transitions.tracer.readActivationTraceparent(context);
265
+ const { traceContext: _queuedTraceContext, ...queuedWrite } = nextWrite;
266
+ const started = {
267
+ ...queuedWrite,
268
+ writeQueued: false,
269
+ status: nextWrite.requiresApproval ? "awaiting_approval" : "approved",
270
+ ...(traceContext === null ? {} : { traceContext }),
271
+ };
272
+ transitions.writeToolCall(context, started);
273
+ let movement;
274
+ if (started.requiresApproval) {
275
+ const interrupt = toolApprovalInterrupt(started, createdAt.toISOString(), interruptDeadline(transitions, createdAt));
276
+ transitions.persistence.pendingInterrupts.set(context, [...transitions.readPendingInterrupts(context), interrupt]);
277
+ transitions.writeEvents(context, turnId, (events) => transitions.writeInterruptRaised(events, interrupt));
278
+ movement = toolStepMovement(transitions.persistence, agentSessionStepTypes.awaitToolApproval, started.toolCallId);
279
+ }
280
+ else {
281
+ movement = toolCallMovement(transitions, started);
282
+ }
283
+ transitions.writeAgentState(context, { ...state, status: "running_tools" });
284
+ return goToMany(movement, StepMovement.of(new StepRef(agentSessionStepTypes.resumeTurn), 1));
285
+ }
214
286
  /** Returns RunModel's move instead of a model call, or `null` to call the model. */
215
287
  decideBeforeModelCall(context, turnId) {
216
288
  // Execute state is fixed at the first attempt, so this and the compaction check are RunModel's decision points.
@@ -290,15 +362,15 @@ export class RunModelStep {
290
362
  context,
291
363
  turnId,
292
364
  createdAt: createdAt.toISOString(),
293
- interruptDeadline: transitions.config.interruptDeadlineMs === undefined
294
- ? null
295
- : new Date(createdAt.getTime() + transitions.config.interruptDeadlineMs).toISOString(),
365
+ interruptDeadline: interruptDeadline(transitions, createdAt),
296
366
  interrupts: [],
297
367
  traceContext: transitions.tracer.readActivationTraceparent(context),
298
368
  movements: [],
299
369
  events,
300
370
  interactionMode: state.interactionMode ?? "chat",
301
371
  writeTodosCallCount: outcome.toolCalls.filter((toolCall) => toolCall.toolName === "write_todos" && transitions.isEnabledBuiltInTool(toolCall.toolName)).length,
372
+ hasStartedOrderedWrite: false,
373
+ queuedCount: 0,
302
374
  };
303
375
  for (const [index, requestedToolCall] of outcome.toolCalls.entries()) {
304
376
  const deferralOrder = toolCalls.length + index;
@@ -316,7 +388,7 @@ export class RunModelStep {
316
388
  transitions.persistence.pendingInterrupts.set(context, batch.interrupts);
317
389
  transitions.recordUsage(context, outcome.usage, events);
318
390
  transitions.writeAgentState(context, { ...stateWithMessages, status: "running_tools" });
319
- batch.movements.push(StepMovement.of(new StepRef(agentSessionStepTypes.resumeTurn), outcome.toolCalls.length));
391
+ batch.movements.push(StepMovement.of(new StepRef(agentSessionStepTypes.resumeTurn), outcome.toolCalls.length - batch.queuedCount));
320
392
  return goToMany(...batch.movements);
321
393
  }
322
394
  /** A planning turn hides executable tools; a call to one never runs. */
@@ -330,31 +402,27 @@ export class RunModelStep {
330
402
  const transitions = this.transitions;
331
403
  const requiresApproval = requestedToolCall.policy.requiresApproval;
332
404
  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
- });
405
+ const isOrderedWrite = requestedToolCall.ordered === true && effect !== "read";
406
+ const record = {
407
+ ...createToolCallRecord(batch, requestedToolCall, effect, requiresApproval, deferralOrder),
408
+ orderedWrite: isOrderedWrite,
409
+ };
338
410
  writeToolCallDeferred(batch.events, record);
339
- const nextStepType = requiresApproval
340
- ? agentSessionStepTypes.awaitToolApproval
341
- : toolStepTypeForEffect(effect);
342
- batch.movements.push(toolStepMovement(transitions.persistence, nextStepType, requestedToolCall.toolCallId));
411
+ if (isOrderedWrite && batch.hasStartedOrderedWrite) {
412
+ // Started by RunModel once every earlier ordered write of the turn succeeded.
413
+ transitions.writeToolCall(batch.context, { ...record, status: "approved", writeQueued: true });
414
+ batch.queuedCount += 1;
415
+ return;
416
+ }
417
+ batch.hasStartedOrderedWrite ||= isOrderedWrite;
418
+ const deferred = { ...record, status: requiresApproval ? "awaiting_approval" : "approved" };
419
+ transitions.writeToolCall(batch.context, deferred);
343
420
  if (!requiresApproval) {
421
+ batch.movements.push(toolCallMovement(transitions, deferred));
344
422
  return;
345
423
  }
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
- };
424
+ batch.movements.push(toolStepMovement(transitions.persistence, agentSessionStepTypes.awaitToolApproval, requestedToolCall.toolCallId));
425
+ const interrupt = toolApprovalInterrupt(record, batch.createdAt, batch.interruptDeadline);
358
426
  batch.interrupts.push(interrupt);
359
427
  transitions.writeInterruptRaised(batch.events, interrupt);
360
428
  }
@@ -477,6 +545,10 @@ export class CompactContextStep {
477
545
  if (!isContextCompactionRunner(runner)) {
478
546
  throw new TypeError("CompactContext needs a runner that implements the compaction methods");
479
547
  }
548
+ const refusal = await transitions.checkSpendLimit(context, state, turnId, this.getStepType());
549
+ if (refusal !== null) {
550
+ return refusal;
551
+ }
480
552
  const events = transitions.createEventWriter(context, turnId);
481
553
  try {
482
554
  events.writeEvent("compaction_started", { through_sequence: throughSequence });
@@ -511,21 +583,27 @@ export class CompactContextStep {
511
583
  const messages = transitions.readMessages(context, firstSequence, throughSequence + 1);
512
584
  const policy = transitions.config.compaction;
513
585
  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
- }));
586
+ const result = await transitions.runWithHeartbeats(context, async () => {
587
+ const summaryResult = await runner.summarizeContext({
588
+ flowId: context.flowId,
589
+ turnId,
590
+ attempt: context.attempt,
591
+ stepExecutionId: context.stepExecutionId,
592
+ previousSummary,
593
+ messages,
594
+ throughSequence,
595
+ events,
596
+ cancellationSignal: context.cancellationSignal,
597
+ deadline: context.deadline,
598
+ ...(policy === undefined ? {} : { inputTokenBudget: policy.contextWindowTokens }),
599
+ sessionSettings: transitions.readTurnSessionSettings(context),
600
+ heartbeat: () => transitions.recordHeartbeat(context),
601
+ });
602
+ if (typeof summaryResult?.usage === "object") {
603
+ await transitions.reportModelUsage(context, turnId, this.getStepType(), summaryResult.usage);
604
+ }
605
+ return summaryResult;
606
+ });
529
607
  if (typeof result?.text !== "string" || result.text.trim() === "" || typeof result.usage !== "object") {
530
608
  throw new TypeError("summarizeContext must return summary text and usage");
531
609
  }
@@ -610,7 +688,7 @@ export class AwaitToolApprovalStep {
610
688
  isAnswered: false,
611
689
  rejectionMessage: null,
612
690
  });
613
- return goToMany(toolStepMovement(this.transitions.persistence, toolStepTypeForEffect(toolCall.effect), toolCall.toolCallId));
691
+ return goToMany(toolCallMovement(this.transitions, { ...toolCall, status: "approved" }));
614
692
  });
615
693
  }
616
694
  rejectToolCall(context, toolCall, rejectionMessage) {
@@ -680,12 +758,19 @@ export class ToolCallStep {
680
758
  heartbeat: () => heartbeats.record(),
681
759
  ...(heartbeats.previousCheckpoint === undefined ? {} : { heartbeatCheckpoint: heartbeats.previousCheckpoint }),
682
760
  saveHeartbeatCheckpoint: (checkpoint) => heartbeats.saveCheckpoint(checkpoint),
761
+ context,
683
762
  };
684
763
  let outcome;
685
764
  try {
686
765
  outcome = await transitions.runWithHeartbeats(context, () => transitions.runner.executeDeferredToolCall(execution), () => heartbeats.record());
687
766
  }
688
767
  catch (error) {
768
+ if (error instanceof ToolOutcomeUnknownError) {
769
+ const unknown = { ...toolCall, status: "outcome_unknown", error: error.message };
770
+ toolCallSpan.recordFinished(unknown);
771
+ transitions.finishToolCall(context, unknown, events);
772
+ return deadEnd();
773
+ }
689
774
  if (!(error instanceof ToolExecutionError)) {
690
775
  throw error;
691
776
  }
@@ -703,7 +788,12 @@ export class ToolCallStep {
703
788
  if (typeof outcome?.output !== "string") {
704
789
  throw new TypeError(`executeDeferredToolCall returned no output for ${toolCallId}`);
705
790
  }
706
- const succeeded = { ...toolCall, status: "succeeded", output: outcome.output };
791
+ const succeeded = {
792
+ ...toolCall,
793
+ status: "succeeded",
794
+ output: outcome.output,
795
+ truncatedOutputBytes: outcome.truncatedOutputBytes ?? null,
796
+ };
707
797
  toolCallSpan.recordFinished(succeeded);
708
798
  transitions.finishToolCall(context, succeeded, events);
709
799
  return deadEnd();
@@ -786,6 +876,9 @@ export class ToolCallFailureRecordingStep {
786
876
  }
787
877
  execute(context, toolCallId) {
788
878
  const transitions = this.transitions;
879
+ if (isStateLimitFailure(context.recoveryError)) {
880
+ return recordFailedSession(transitions, context, readRecoveryDetail(context));
881
+ }
789
882
  const toolCall = transitions.readToolCall(context, toolCallId);
790
883
  if (isFinalToolCallStatus(toolCall.status)) {
791
884
  return deadEnd();
@@ -855,12 +948,17 @@ export class ResumeTurnStep {
855
948
  // The results did not all arrive, so wait again for the same count.
856
949
  return goTo(new StepRef(agentSessionStepTypes.resumeTurn), expectedResultCount);
857
950
  }
858
- const unfinishedCount = toolCalls.filter((toolCall) => !isFinalToolCallStatus(toolCall.status)).length;
951
+ const unfinishedCount = toolCalls.filter((toolCall) => !isFinalToolCallStatus(toolCall.status) && toolCall.writeQueued !== true).length;
859
952
  if (unfinishedCount > 0) {
860
953
  // A consumed result from an earlier turn must not resume this one early.
861
954
  return goTo(new StepRef(agentSessionStepTypes.resumeTurn), unfinishedCount);
862
955
  }
863
956
  if (reason !== null) {
957
+ transitions.writeEvents(context, turnId, (events) => {
958
+ for (const toolCall of toolCalls.filter((queued) => queued.writeQueued === true)) {
959
+ transitions.finishUnscheduledToolCall(context, { ...cancelledToolCall(toolCall, reason), writeQueued: false }, events);
960
+ }
961
+ });
864
962
  return transitions.goToApplyTurnControl(context);
865
963
  }
866
964
  transitions.writeAgentState(context, { ...state, status: "running_model" });
@@ -912,7 +1010,7 @@ export class ApplyTurnControlStep {
912
1010
  // Each movement into ApplyTurnControl adds the archive chunks of the model input.
913
1011
  // The lock keeps a turn from starting while cancel_turn decides about the last one.
914
1012
  return {
915
- ...messageStepOptions(this.transitions.persistence, this.getStepType(), staticMessageChunkLoads(this.transitions)),
1013
+ ...messageStepOptions(this.transitions.persistence, this.getStepType(), staticMessageChunkLoads(this.transitions), this.transitions.config.runModelLimit),
916
1014
  executeLockAttributes: [this.transitions.persistence.turnCancellationRequest.lock()],
917
1015
  };
918
1016
  }
@@ -935,6 +1033,11 @@ export class ApplyTurnControlStep {
935
1033
  return transitions.beginTurn(context, state, steeredMessages, this.getStepType(), transitions.decideInteractionMode(context, steeredMessages));
936
1034
  }
937
1035
  }
1036
+ // server-zd reports a Step whose commit would pass limits.maxFlowStateBytes with this type and text:
1037
+ // "run state of <X> bytes exceeds the maximum of <Y> bytes", or "active run state of ..." since the limit
1038
+ // counts only active state.
1039
+ const stateLimitErrorType = "ValueTooLarge";
1040
+ const stateLimitErrorText = "run state of ";
938
1041
  /** Ends a failed turn; it is the failure route of `RunModel` and `CompactContext`, so it takes either's input. */
939
1042
  export class RecordTurnFailureStep {
940
1043
  transitions;
@@ -959,15 +1062,48 @@ export class RecordTurnFailureStep {
959
1062
  }
960
1063
  execute(context) {
961
1064
  const transitions = this.transitions;
1065
+ if (isStateLimitFailure(context.recoveryError)) {
1066
+ return recordFailedSession(transitions, context, readRecoveryDetail(context));
1067
+ }
962
1068
  const state = transitions.readAgentState(context);
963
1069
  const toolCalls = transitions.readTurnToolCalls(context, state.activeTurnId);
964
1070
  const finalToolCalls = toolCalls.filter((toolCall) => isFinalToolCallStatus(toolCall.status));
965
1071
  const unfinishedTurnMessages = state.activeTurnId === null
966
1072
  ? null
967
1073
  : transitions.buildUnfinishedTurnMessages(context, state, state.activeTurnId, finalToolCalls);
968
- return recordFailedTurn(transitions, context, state, toolCalls, unfinishedTurnMessages ?? [], readRecoveryDetail(context));
1074
+ // A refused model call moves here on purpose, with its reason already in the state.
1075
+ const error = context.recoveryError === undefined && state.lastTurnError ? state.lastTurnError : readRecoveryDetail(context);
1076
+ return recordFailedTurn(transitions, context, state, toolCalls, unfinishedTurnMessages ?? [], error);
969
1077
  }
970
1078
  }
1079
+ /** Reports whether a Step failed because its commit would exceed the Flow's state limit. */
1080
+ export function isStateLimitFailure(recoveryError) {
1081
+ // The Server gives this failure no code of its own; a value over its own limit shares the type.
1082
+ return recoveryError?.errorType === stateLimitErrorType && recoveryError.detail.includes(stateLimitErrorText);
1083
+ }
1084
+ /**
1085
+ * Ends a session whose Flow reached the state limit, in one commit with the Flow's close. This commit is checked
1086
+ * against the same limit, so it stores no turn messages and keeps the turn's tool call records. When it does not
1087
+ * fit, Dex fails the Flow with the same text and `AgentState` stays as it was; see `agent-session-failure.ts`.
1088
+ */
1089
+ function recordFailedSession(transitions, context, error) {
1090
+ const persistence = transitions.persistence;
1091
+ const state = transitions.readAgentState(context);
1092
+ persistence.frameworkRunState.delete(context);
1093
+ if (transitions.readPendingInterrupts(context).length > 0) {
1094
+ persistence.pendingInterrupts.set(context, []);
1095
+ }
1096
+ transitions.writeAgentState(context, {
1097
+ ...state,
1098
+ status: "failed",
1099
+ turnCount: state.turnCount + (state.activeTurnId === null ? 0 : 1),
1100
+ activeTurnId: null,
1101
+ lastTurnError: error,
1102
+ acceptsTurnControl: false,
1103
+ });
1104
+ transitions.writeEvents(context, state.activeTurnId, (events) => events.writeEvent("turn_failed", { error }));
1105
+ return forceFail(error);
1106
+ }
971
1107
  /**
972
1108
  * Ends the active turn as failed in the current commit: appends the unfinished-turn messages, then the
973
1109
  * turn-failure note when the turn has final records, and clears the turn.
@@ -1106,6 +1242,63 @@ export function toolStepOptions(persistence, stepType, toolCallId) {
1106
1242
  export function toolStepMovement(persistence, stepType, toolCallId) {
1107
1243
  return StepMovement.of(new StepRef(stepType), toolCallId, toolStepOptions(persistence, stepType, toolCallId));
1108
1244
  }
1245
+ /**
1246
+ * Returns the movement that starts an approved call's tool Step, with the runner's timeout and retry policy.
1247
+ * Only `CallTool` takes the retry policy; `CallSingleAttemptTool` keeps one attempt.
1248
+ */
1249
+ export function toolCallMovement(transitions, toolCall) {
1250
+ const stepType = toolStepTypeForEffect(toolCall.effect);
1251
+ const options = toolStepOptions(transitions.persistence, stepType, toolCall.toolCallId);
1252
+ const overrides = readToolCallStepOptions(transitions, toolCall);
1253
+ return StepMovement.of(new StepRef(stepType), toolCall.toolCallId, {
1254
+ ...options,
1255
+ ...(overrides?.executeMethodTimeout === undefined ? {} : { executeMethodTimeoutMs: overrides.executeMethodTimeout }),
1256
+ ...(overrides?.executeRetry === undefined || stepType !== agentSessionStepTypes.callTool
1257
+ ? {}
1258
+ : { executeRetry: overrides.executeRetry }),
1259
+ });
1260
+ }
1261
+ function readToolCallStepOptions(transitions, toolCall) {
1262
+ const provide = transitions.runner.toolCallStepOptions;
1263
+ if (provide === undefined) {
1264
+ return undefined;
1265
+ }
1266
+ try {
1267
+ return provide.call(transitions.runner, toRunnerToolCall(toolCall));
1268
+ }
1269
+ catch (error) {
1270
+ // The call then runs with the defaults; a missing tool fails by its effect class.
1271
+ console.warn(`dex-agents: toolCallStepOptions failed for ${toolCall.toolName}:`, error);
1272
+ return undefined;
1273
+ }
1274
+ }
1275
+ function toolApprovalInterrupt(toolCall, createdAt, deadline) {
1276
+ return {
1277
+ interruptId: toolCall.toolCallId,
1278
+ kind: "tool_approval",
1279
+ toolCallId: toolCall.toolCallId,
1280
+ toolName: toolCall.toolName,
1281
+ arguments: toolCall.arguments,
1282
+ createdAt,
1283
+ deadline,
1284
+ question: null,
1285
+ choices: [],
1286
+ allowsFreeText: true,
1287
+ };
1288
+ }
1289
+ function interruptDeadline(transitions, createdAt) {
1290
+ const deadlineMs = transitions.config.interruptDeadlineMs;
1291
+ return deadlineMs === undefined ? null : new Date(createdAt.getTime() + deadlineMs).toISOString();
1292
+ }
1293
+ function skippedToolCall(toolCall) {
1294
+ return {
1295
+ ...toolCall,
1296
+ status: "failed",
1297
+ writeQueued: false,
1298
+ skipped: true,
1299
+ error: `${toolCallNotRunErrorPrefix}: ${toolCallSkippedAfterWriteDetail}`,
1300
+ };
1301
+ }
1109
1302
  /**
1110
1303
  * Returns the options of `PollToolTask`. It takes no lock and never waits on a cancellation: it owns the
1111
1304
  * record, and a started task runs on.
@@ -1193,6 +1386,10 @@ function createToolCallRecord(batch, requestedToolCall, effect, requiresApproval
1193
1386
  taskId: null,
1194
1387
  taskPollIntervalSeconds: null,
1195
1388
  taskAnswerPending: false,
1389
+ orderedWrite: false,
1390
+ writeQueued: false,
1391
+ skipped: false,
1392
+ truncatedOutputBytes: null,
1196
1393
  };
1197
1394
  }
1198
1395
  function writeToolCallDeferred(events, record) {