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