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