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