@statelyai/agent 2.0.0-alpha.22 → 2.0.0-alpha.24
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/dist/ai-sdk.cjs +53 -25
- package/dist/ai-sdk.d.cts +20 -12
- package/dist/ai-sdk.d.mts +20 -12
- package/dist/ai-sdk.mjs +53 -25
- package/dist/{decision-DxSTqzgZ.mjs → decision-BBZ2jMO1.mjs} +444 -79
- package/dist/{decision-1o45_ZrS.cjs → decision-Cv6SfNWl.cjs} +525 -94
- package/dist/index.cjs +2224 -1032
- package/dist/index.d.cts +413 -275
- package/dist/index.d.mts +413 -275
- package/dist/index.mjs +2163 -983
- package/dist/machines.cjs +30 -2
- package/dist/machines.d.cts +1 -1
- package/dist/machines.d.mts +1 -1
- package/dist/machines.mjs +30 -2
- package/dist/openai.cjs +447 -0
- package/dist/openai.d.cts +177 -0
- package/dist/openai.d.mts +177 -0
- package/dist/openai.mjs +437 -0
- package/dist/otel.cjs +3 -6
- package/dist/otel.d.cts +1 -1
- package/dist/otel.d.mts +1 -1
- package/dist/otel.mjs +3 -6
- package/dist/run-agent-Ck7Sa3Tb.d.mts +968 -0
- package/dist/run-agent-DWqA8KiI.d.cts +968 -0
- package/dist/setup-agent-CeO2fARv.mjs +639 -0
- package/dist/setup-agent-Dl2VELLi.cjs +663 -0
- package/dist/{text-logic-BaxPrcLk.d.cts → text-logic-C7bSnH_B.d.cts} +195 -44
- package/dist/{text-logic-DhfFWzu9.d.mts → text-logic-DJBK3LbG.d.mts} +195 -44
- package/dist/{types-DSdj2tGs.d.cts → types-CjHk5OdH.d.cts} +4 -19
- package/dist/{types-CvWRGFxP.d.mts → types-DWA6X9R-.d.mts} +4 -19
- package/package.json +17 -21
- package/readme.md +32 -17
- package/dist/errors-BQRk9eiZ.d.cts +0 -19
- package/dist/errors-C9rxnWbX.d.mts +0 -19
- package/dist/errors-CeSXQx0v.mjs +0 -23
- package/dist/errors-DUBBzRLP.cjs +0 -28
- package/dist/event-log-store-Bz7HDBkE.d.cts +0 -145
- package/dist/event-log-store-DmIDosD6.mjs +0 -427
- package/dist/event-log-store-a_TKy1gk.cjs +0 -468
- package/dist/event-log-store-hrA1vqtN.d.mts +0 -145
- package/dist/run-agent-0fpEZ1wJ.d.mts +0 -1098
- package/dist/run-agent-Zbkx7XP8.d.cts +0 -1098
- package/dist/setup-agent-DkCTy5Eu.cjs +0 -1707
- package/dist/setup-agent-nSFYrNTP.mjs +0 -1587
- package/dist/sqlite.cjs +0 -129
- package/dist/sqlite.d.cts +0 -57
- package/dist/sqlite.d.mts +0 -57
- package/dist/sqlite.mjs +0 -127
package/dist/index.mjs
CHANGED
|
@@ -1,194 +1,1368 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
import { $ as userMessage, B as assistantMessage, C as getCallFinishReason, D as parseModelRef, E as normalizeGeneratorResult, F as getRegisteredAgentExecutionOptions, G as getMachineStructuralHash, H as findNonSerializableContextPaths, I as isUnboundPlaceholder, J as getStatePath, K as getMessageText, M as executorBoundLogics, N as getAgentExecutionOptions, O as parseOutput, P as getMachineIdlePredicate, Q as toolMessage, S as getAgentOutputMode, T as isTextLogic, U as getJsonSchema, V as djb2Hex, W as getJsonSchemaSync, X as resolveMachineVersion, Y as isStandardSchema, Z as systemMessage, _ as bindRequestExecutor, a as isDecisionLogic, at as messagesSchema, b as createTextLogic, c as AgentInvalidEventPayloadError, d as isRecord, et as validateSchemaSync, f as AGENT_USAGE_TOKEN_FIELDS, i as createDecisionRequest, it as isAgentMessages, j as DEFAULT_AGENT_EXECUTORS, k as parseStructuredEnvelope, l as getAcceptedEvents, o as renderDecisionAttempts, ot as AgentError, q as getStateMeta, r as createDecisionLogic, rt as appendMessages, s as resolveDecision, st as AgentTruncatedError, t as AgentDecisionExhaustedError, tt as AGENT_MESSAGES_EVENT_TYPE, u as parseAgentEvent, v as buildEnvelopeSchema, w as getCallUsage, x as executeAgentTextRequest } from "./decision-BBZ2jMO1.mjs";
|
|
2
|
+
import { i as AGENT_USAGE_EVENT_TYPE, n as getAgentSchemas, r as setupAgent, t as createAgentSchemas } from "./setup-agent-CeO2fARv.mjs";
|
|
3
|
+
import { createActor, createAsyncLogic, getNextTransitions, initialTransition, isMachineSnapshot, transition } from "xstate";
|
|
4
|
+
//#region src/interaction.ts
|
|
5
|
+
function issue(message, path) {
|
|
6
|
+
return {
|
|
7
|
+
message,
|
|
8
|
+
path
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function validateEventMeta(value, path) {
|
|
12
|
+
if (typeof value === "string") return [];
|
|
13
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return [issue("Expected a label string or an interaction event descriptor", path)];
|
|
14
|
+
const descriptor = value;
|
|
15
|
+
const issues = [];
|
|
16
|
+
if (descriptor["label"] !== void 0 && typeof descriptor["label"] !== "string") issues.push(issue("Expected a string", [...path, "label"]));
|
|
17
|
+
if (descriptor["style"] !== void 0 && typeof descriptor["style"] !== "string") issues.push(issue("Expected a string", [...path, "style"]));
|
|
18
|
+
const event = descriptor["event"];
|
|
19
|
+
if (event !== void 0 && (!event || typeof event !== "object" || Array.isArray(event))) issues.push(issue("Expected an object of fixed event fields", [...path, "event"]));
|
|
20
|
+
return issues;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A {@link StandardSchemaV1} for the state metadata {@link getInteraction}
|
|
24
|
+
* reads: an object whose optional `interaction` declares a `label` (a string
|
|
25
|
+
* with `{context.path}` interpolation, or a function of the context), an
|
|
26
|
+
* `events` map of choices, and a `textEvent` for free-text answers. Pass it
|
|
27
|
+
* straight to `createAgentSchemas({ meta: interactionMetaSchema })` instead of
|
|
28
|
+
* restating the shape per machine.
|
|
29
|
+
*/
|
|
30
|
+
const interactionMetaSchema = { "~standard": {
|
|
31
|
+
version: 1,
|
|
32
|
+
vendor: "statelyai-agent",
|
|
33
|
+
validate(value) {
|
|
34
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { issues: [{ message: "Expected a state meta object" }] };
|
|
35
|
+
const interaction = value["interaction"];
|
|
36
|
+
if (interaction === void 0) return { value };
|
|
37
|
+
if (!interaction || typeof interaction !== "object" || Array.isArray(interaction)) return { issues: [issue("Expected an interaction descriptor object", ["interaction"])] };
|
|
38
|
+
const descriptor = interaction;
|
|
39
|
+
const issues = [];
|
|
40
|
+
const label = descriptor["label"];
|
|
41
|
+
if (label !== void 0 && typeof label !== "string" && typeof label !== "function") issues.push(issue("Expected a string or a (context) => string", ["interaction", "label"]));
|
|
42
|
+
const events = descriptor["events"];
|
|
43
|
+
if (events !== void 0) if (!events || typeof events !== "object" || Array.isArray(events)) issues.push(issue("Expected an object keyed by event type", ["interaction", "events"]));
|
|
44
|
+
else for (const [type, config] of Object.entries(events)) issues.push(...validateEventMeta(config, [
|
|
45
|
+
"interaction",
|
|
46
|
+
"events",
|
|
47
|
+
type
|
|
48
|
+
]));
|
|
49
|
+
if (descriptor["textEvent"] !== void 0 && typeof descriptor["textEvent"] !== "string") issues.push(issue("Expected a string", ["interaction", "textEvent"]));
|
|
50
|
+
return issues.length > 0 ? { issues } : { value };
|
|
51
|
+
}
|
|
52
|
+
} };
|
|
53
|
+
function readPath(value, path) {
|
|
54
|
+
return path.split(".").reduce((current, key) => {
|
|
55
|
+
if (!current || typeof current !== "object") return void 0;
|
|
56
|
+
return current[key];
|
|
57
|
+
}, value);
|
|
58
|
+
}
|
|
59
|
+
function interpolate(label, context, preserveWhitespace) {
|
|
60
|
+
const interpolated = label.replace(/\{([^{}]+)\}/g, (_, path) => {
|
|
61
|
+
const value = readPath(context, path.trim());
|
|
62
|
+
return value === void 0 || value === null ? "" : String(value);
|
|
63
|
+
});
|
|
64
|
+
return preserveWhitespace ? interpolated : interpolated.replace(/\s+/g, " ").trim();
|
|
65
|
+
}
|
|
66
|
+
/** Reads the active state's interaction metadata and filters it through the
|
|
67
|
+
* events XState currently accepts. Returns `undefined` when none is declared. */
|
|
68
|
+
function getInteraction(snapshot, options = {}) {
|
|
69
|
+
const interaction = getStateMeta(snapshot).interaction;
|
|
70
|
+
if (!interaction) return void 0;
|
|
71
|
+
const preserveWhitespace = options.preserveWhitespace ?? false;
|
|
72
|
+
const schemas = getAgentExecutionOptions(snapshot.machine)?.schemas;
|
|
73
|
+
const accepted = new Set(getAcceptedEvents(snapshot).map((event) => event.type));
|
|
74
|
+
const label = typeof interaction.label === "function" ? interaction.label({ context: snapshot.context }) : interaction.label ?? "";
|
|
75
|
+
const events = Object.entries(interaction.events ?? {}).filter(([type]) => accepted.has(type)).map(([type, config]) => {
|
|
76
|
+
return [type, typeof config === "string" ? { label: config } : config];
|
|
77
|
+
}).filter(([type, descriptor]) => {
|
|
78
|
+
try {
|
|
79
|
+
const complete = parseAgentEvent(snapshot, {
|
|
80
|
+
...descriptor.event,
|
|
81
|
+
type
|
|
82
|
+
}, { schemas });
|
|
83
|
+
return snapshot.can(complete);
|
|
84
|
+
} catch {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
}).map(([type, descriptor]) => {
|
|
88
|
+
return {
|
|
89
|
+
type,
|
|
90
|
+
label: interpolate(descriptor.label ?? type, snapshot.context, preserveWhitespace),
|
|
91
|
+
...descriptor.style ? { style: descriptor.style } : {},
|
|
92
|
+
...descriptor.event ? { event: descriptor.event } : {}
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
label: interpolate(label, snapshot.context, preserveWhitespace),
|
|
97
|
+
events,
|
|
98
|
+
...interaction.textEvent && accepted.has(interaction.textEvent) ? { textEvent: interaction.textEvent } : {}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Converts a rendered interaction choice back into a schema-validated machine
|
|
102
|
+
* event. Fixed fields declared in metadata are merged before user fields. */
|
|
103
|
+
function eventFromInteraction(snapshot, choice) {
|
|
104
|
+
const interaction = getInteraction(snapshot);
|
|
105
|
+
if (!interaction) throw new AgentInvalidEventPayloadError("(interaction)", "this state renders no interaction, so there is no choice to convert.");
|
|
106
|
+
let event;
|
|
107
|
+
if ("text" in choice) {
|
|
108
|
+
if (!interaction.textEvent) throw new AgentInvalidEventPayloadError("(text)", `this interaction takes no free text. Choices: ${interaction.events.map(({ type }) => type).join(", ") || "(none)"}.`);
|
|
109
|
+
event = {
|
|
110
|
+
type: interaction.textEvent,
|
|
111
|
+
text: choice.text
|
|
112
|
+
};
|
|
113
|
+
} else {
|
|
114
|
+
const descriptor = interaction.events.find(({ type }) => type === choice.type);
|
|
115
|
+
if (!descriptor) throw new AgentInvalidEventPayloadError(choice.type, `this interaction does not offer that choice. Choices: ${interaction.events.map(({ type }) => type).join(", ") || "(none)"}.`);
|
|
116
|
+
event = {
|
|
117
|
+
...choice,
|
|
118
|
+
...descriptor.event,
|
|
119
|
+
type: choice.type
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return parseAgentEvent(snapshot, event);
|
|
123
|
+
}
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/steps.ts
|
|
126
|
+
/**
|
|
127
|
+
* The step envelope: an INTERNAL, per-model-call-checkpoint view of an agent
|
|
128
|
+
* machine (`AgentStep` = snapshot + actions + discovered requests + done). It
|
|
129
|
+
* once backed a public step API; the public step path is now the thin
|
|
130
|
+
* effect/replay loop in `./effects.ts` (re-exported from `@statelyai/agent`).
|
|
131
|
+
* These helpers survive as internals that `runAgent`, `verify.ts`, and the
|
|
132
|
+
* effect path build on — the only symbol still on the public surface is
|
|
133
|
+
* {@link executeAgentRequest} (the `text`-effect resolver).
|
|
134
|
+
* @module
|
|
135
|
+
*/
|
|
136
|
+
/** @internal Normalizes current and legacy XState invoke effect shapes. */
|
|
137
|
+
function getInvokeEffectMetadata(action) {
|
|
138
|
+
if (action.type === "@xstate.spawn") return action;
|
|
139
|
+
if (action.type === "xstate.spawnChild") {
|
|
140
|
+
const params = action.params;
|
|
141
|
+
return params ? {
|
|
142
|
+
...params,
|
|
143
|
+
logic: action.logic
|
|
144
|
+
} : void 0;
|
|
145
|
+
}
|
|
146
|
+
if (action.type === "@xstate.start" && typeof action.src === "string") return action;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The invokes a snapshot is still waiting on, lowered to the same spawn-effect
|
|
150
|
+
* shape {@link getAgentRequests} reads. Derived from the snapshot's live
|
|
151
|
+
* children rather than the last transition's actions, so a state that invokes
|
|
152
|
+
* several actors at once (an `invoke: [...]` array, or parallel regions each
|
|
153
|
+
* invoking) keeps reporting the siblings after the first one settles —
|
|
154
|
+
* including when the settled invoke's `onDone` is targetless and starts
|
|
155
|
+
* nothing new.
|
|
156
|
+
*
|
|
157
|
+
* Entries are ordered by invoke id, so a walker that resolves one invoke per
|
|
158
|
+
* step is deterministic.
|
|
159
|
+
*
|
|
160
|
+
* @internal
|
|
161
|
+
*/
|
|
162
|
+
function getPendingInvokes(snapshot) {
|
|
163
|
+
const children = snapshot.children ?? {};
|
|
164
|
+
return Object.keys(children).sort().flatMap((id) => {
|
|
165
|
+
const child = children[id];
|
|
166
|
+
const childSnapshot = child?.getSnapshot?.();
|
|
167
|
+
if (!child || childSnapshot?.status !== "active") return [];
|
|
168
|
+
return [{
|
|
169
|
+
type: "@xstate.spawn",
|
|
170
|
+
id,
|
|
171
|
+
src: child.src,
|
|
172
|
+
input: childSnapshot.input,
|
|
173
|
+
logic: child.logic
|
|
174
|
+
}];
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Scans a set of executable actions (as returned by xstate's `transition`/
|
|
179
|
+
* `initialTransition`) for spawned `TextLogic`/`DecisionLogic` invokes and
|
|
180
|
+
* lowers each into an {@link AgentStepRequest}. Pass `options.machine` to
|
|
181
|
+
* pre-fill `schemas`/`actors` from the machine's registered `setupAgent`
|
|
182
|
+
* options instead of passing them by hand.
|
|
183
|
+
* `options.snapshot` is required to resolve a decision's candidate events
|
|
184
|
+
* (intersecting declared `allowedEvents` with what's currently legal) — omit
|
|
185
|
+
* it and decision requests report an empty `events` list.
|
|
186
|
+
*
|
|
187
|
+
* @internal
|
|
188
|
+
*/
|
|
189
|
+
function getAgentRequests(actions, { machine, ...options } = {}) {
|
|
190
|
+
if (machine) options = {
|
|
191
|
+
...getRegisteredAgentExecutionOptions(machine),
|
|
192
|
+
...options
|
|
193
|
+
};
|
|
194
|
+
return actions.flatMap((action) => {
|
|
195
|
+
const params = getInvokeEffectMetadata(action);
|
|
196
|
+
if (!params || typeof params.src !== "string") return [];
|
|
197
|
+
if (typeof params.id !== "string" || params.id.length === 0) throw new Error(`Agent invoke '${params.src}' must define a durable string id.`);
|
|
198
|
+
const registeredLogic = isTextLogic(params.logic) || isDecisionLogic(params.logic) ? params.logic : options.actors?.[params.src];
|
|
199
|
+
if (isDecisionLogic(registeredLogic)) {
|
|
200
|
+
const decisionRequest = registeredLogic.request(params.input);
|
|
201
|
+
const allowedEventTypes = registeredLogic.allowedEventTypes?.(params.input);
|
|
202
|
+
const events = options.snapshot ? getAcceptedEvents(options.snapshot, {
|
|
203
|
+
events: options.events,
|
|
204
|
+
schemas: options.schemas,
|
|
205
|
+
eventTypes: allowedEventTypes,
|
|
206
|
+
eventToolName: options.eventToolName
|
|
207
|
+
}) : [];
|
|
208
|
+
return [{
|
|
209
|
+
...decisionRequest,
|
|
210
|
+
id: params.id,
|
|
211
|
+
events
|
|
212
|
+
}];
|
|
213
|
+
}
|
|
214
|
+
const textLogic = isTextLogic(registeredLogic) ? registeredLogic : void 0;
|
|
215
|
+
const input = textLogic ? textLogic.request(params.input) : void 0;
|
|
216
|
+
if (!input) return [];
|
|
217
|
+
return [{
|
|
218
|
+
kind: "text",
|
|
219
|
+
id: params.id,
|
|
220
|
+
src: params.src,
|
|
221
|
+
...textLogic ? { mode: textLogic.mode } : {},
|
|
222
|
+
input,
|
|
223
|
+
tools: input.tools ?? {},
|
|
224
|
+
events: []
|
|
225
|
+
}];
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Builds the canonical `xstate.done.actor` event xstate's `transition()`
|
|
230
|
+
* expects to resolve a spawned invoke — including the child session identity
|
|
231
|
+
* that rejects a stale completion from an earlier incarnation.
|
|
232
|
+
*
|
|
233
|
+
* @internal
|
|
234
|
+
*/
|
|
235
|
+
function doneEvent(snapshot, request, output) {
|
|
236
|
+
const id = typeof request === "string" ? request : request.id;
|
|
237
|
+
const child = snapshot.children[id];
|
|
238
|
+
return {
|
|
239
|
+
type: "xstate.done.actor",
|
|
240
|
+
actorId: id,
|
|
241
|
+
...typeof child?.sessionId === "string" ? { sessionId: child.sessionId } : {},
|
|
242
|
+
output
|
|
17
243
|
};
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* The `xstate.error.actor` counterpart of {@link doneEvent} — the event a
|
|
247
|
+
* failed invoke delivers, which `onError` transitions observe. Carries the
|
|
248
|
+
* same child session identity, so xstate does not discard it as stale.
|
|
249
|
+
*
|
|
250
|
+
* @internal
|
|
251
|
+
*/
|
|
252
|
+
function errorEvent(snapshot, request, error) {
|
|
253
|
+
const id = typeof request === "string" ? request : request.id;
|
|
254
|
+
const child = snapshot.children[id];
|
|
255
|
+
return {
|
|
256
|
+
type: "xstate.error.actor",
|
|
257
|
+
actorId: id,
|
|
258
|
+
...typeof child?.sessionId === "string" ? { sessionId: child.sessionId } : {},
|
|
259
|
+
error
|
|
24
260
|
};
|
|
25
|
-
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Applies a request's `output` as a done event (or, with `outcome: 'error'`,
|
|
264
|
+
* its `output` as the invoke's error) via `transition(...)`, returning the raw
|
|
265
|
+
* `[snapshot, actions]` tuple. Lower-level than {@link resolveAgentStep} /
|
|
266
|
+
* {@link rejectAgentStep} — those helpers wrap this and also run
|
|
267
|
+
* {@link getAgentRequests} to produce the next {@link AgentStep}.
|
|
268
|
+
*
|
|
269
|
+
* @internal
|
|
270
|
+
*/
|
|
271
|
+
function transitionResult(logic, snapshot, request, output, outcome = "output") {
|
|
272
|
+
const event = outcome === "error" ? errorEvent(snapshot, request, output) : doneEvent(snapshot, request, output);
|
|
273
|
+
const result = transition(logic, snapshot, event);
|
|
274
|
+
applyFinalStateOutput(logic, result[0], event);
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Starts a machine and returns its first {@link AgentStep} — the step-path
|
|
279
|
+
* equivalent of `initialTransition` plus request discovery. Begins the
|
|
280
|
+
* durable/per-model-call-checkpoint loop: resolve each `step.requests` entry
|
|
281
|
+
* (via {@link executeAgentRequest} for `kind: 'text'`, or
|
|
282
|
+
* {@link resolveDecision} for `kind: 'decision'`), then advance with
|
|
283
|
+
* {@link resolveAgentStep} or {@link transitionAgentStep}.
|
|
284
|
+
*/
|
|
285
|
+
function initialAgentStep(machine, input, options) {
|
|
286
|
+
const [snapshot, actions] = initialTransition(machine, input);
|
|
287
|
+
return createAgentStep(machine, snapshot, actions, getRegisteredAgentExecutionOptions(machine, options));
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Applies an externally-sent event (e.g. a decision's chosen event, or a
|
|
291
|
+
* human's reply) and returns the next {@link AgentStep}. Accepts **either**
|
|
292
|
+
* a raw snapshot **or** a prior `AgentStep` as the second argument —
|
|
293
|
+
* `.snapshot` is unwrapped automatically, so callers can thread the whole
|
|
294
|
+
* step object through without manually plucking the snapshot out.
|
|
295
|
+
*/
|
|
296
|
+
function transitionAgentStep(machine, snapshotOrStep, event, options) {
|
|
297
|
+
const [nextSnapshot, actions] = transition(machine, isAgentStep(snapshotOrStep) ? snapshotOrStep.snapshot : snapshotOrStep, event);
|
|
298
|
+
return createAgentStep(machine, nextSnapshot, actions, getRegisteredAgentExecutionOptions(machine, options));
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Applies a resolved text request's output (a `kind: 'text'`
|
|
302
|
+
* {@link AgentRequest} — not a decision) as a done event and returns the
|
|
303
|
+
* next {@link AgentStep}. For decisions, resolve with `resolveDecision`
|
|
304
|
+
* (which returns a {@link ChosenEvent}) and apply it with
|
|
305
|
+
* {@link transitionAgentStep} instead — a decision has no output value of
|
|
306
|
+
* its own to feed here.
|
|
307
|
+
*/
|
|
308
|
+
function resolveAgentStep(machine, step, request, output, options) {
|
|
309
|
+
const [snapshot, actions] = transitionResult(machine, step.snapshot, request, output);
|
|
310
|
+
return createAgentStep(machine, snapshot, actions, getRegisteredAgentExecutionOptions(machine, options));
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Rejects a pending invoke — the failure counterpart of
|
|
314
|
+
* {@link resolveAgentStep}. Delivers `error` as the invoke's
|
|
315
|
+
* `xstate.error.actor` event, so the machine takes that invoke's `onError`
|
|
316
|
+
* transition and returns the next {@link AgentStep}. With no `onError` in
|
|
317
|
+
* scope the returned step's snapshot has `status: 'error'`, exactly as a live
|
|
318
|
+
* run's failed invoke does.
|
|
319
|
+
*
|
|
320
|
+
* A durable host replays a failed call with this: the log says the call
|
|
321
|
+
* failed, so the replay rejects the invoke instead of resolving it. Scripted
|
|
322
|
+
* playthroughs use it too — see `simulateAgent`'s `script.errors`.
|
|
323
|
+
*
|
|
324
|
+
* `error` can be any value. An `Error` instance is the usual one, but a plain
|
|
325
|
+
* object (`{ code: 'truncated' }`) reaches the `onError` transition as
|
|
326
|
+
* `event.error` untouched, so a machine that branches on a failure code is
|
|
327
|
+
* testable without constructing a provider's error class.
|
|
328
|
+
*/
|
|
329
|
+
function rejectAgentStep(machine, step, request, error, options) {
|
|
330
|
+
const [snapshot, actions] = transitionResult(machine, step.snapshot, request, error, "error");
|
|
331
|
+
return createAgentStep(machine, snapshot, actions, getRegisteredAgentExecutionOptions(machine, options));
|
|
332
|
+
}
|
|
333
|
+
async function executeAgentRequest(requestOrEffect, executors) {
|
|
334
|
+
if (requestOrEffect.kind === "decision") throw new Error("executeAgentRequest(...) is text-only. Resolve a 'decision' request with resolveDecision(request, executors, ...) instead.");
|
|
335
|
+
const request = "requestId" in requestOrEffect ? {
|
|
26
336
|
kind: "text",
|
|
27
|
-
id,
|
|
28
|
-
src:
|
|
29
|
-
mode:
|
|
30
|
-
input: request,
|
|
31
|
-
tools: {},
|
|
337
|
+
id: requestOrEffect.requestId,
|
|
338
|
+
src: "",
|
|
339
|
+
mode: requestOrEffect.mode,
|
|
340
|
+
input: requestOrEffect.request,
|
|
341
|
+
tools: requestOrEffect.request.tools ?? {},
|
|
32
342
|
events: []
|
|
343
|
+
} : requestOrEffect;
|
|
344
|
+
assertTextExecutor(request, executors);
|
|
345
|
+
const { output, raw } = await executeAgentTextRequest(request.mode ?? "generate", request.id, request.input, executors, request.tools);
|
|
346
|
+
return {
|
|
347
|
+
output: request.input.outputSchema ? validateSchemaSync(request.input.outputSchema, output) : output,
|
|
348
|
+
raw
|
|
33
349
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
});
|
|
39
|
-
|
|
350
|
+
}
|
|
351
|
+
function assertTextExecutor(request, executors) {
|
|
352
|
+
const mode = request.mode ?? "generate";
|
|
353
|
+
const kind = mode === "stream" ? "streamText" : "generateText";
|
|
354
|
+
if (!(mode === "stream" ? executors.streamText : executors.generateText)) throw new Error(`this step's text request '${request.src}' needs a '${kind}' executor but none was provided.`);
|
|
355
|
+
}
|
|
356
|
+
function createAgentStep(machine, snapshot, actions, options) {
|
|
357
|
+
applyFinalStateOutput(machine, snapshot);
|
|
358
|
+
return {
|
|
359
|
+
snapshot,
|
|
360
|
+
actions,
|
|
361
|
+
requests: getAgentRequests(getPendingInvokes(snapshot), {
|
|
362
|
+
...options,
|
|
363
|
+
snapshot
|
|
364
|
+
}),
|
|
365
|
+
done: snapshot.status === "done"
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function resolveStateValueConfig(config, value) {
|
|
369
|
+
if (typeof value === "string") return config.states?.[value];
|
|
370
|
+
if (!value || typeof value !== "object") return;
|
|
371
|
+
for (const [key, childValue] of Object.entries(value)) {
|
|
372
|
+
const childConfig = config.states?.[key];
|
|
373
|
+
if (!childConfig) continue;
|
|
374
|
+
if (childConfig.type === "final") return childConfig;
|
|
375
|
+
const nested = resolveStateValueConfig(childConfig, childValue);
|
|
376
|
+
if (nested) return nested;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function applyFinalStateOutput(logic, snapshot, event) {
|
|
380
|
+
const machineSnapshot = snapshot;
|
|
381
|
+
if (machineSnapshot.status !== "done" || machineSnapshot.output !== void 0 || !("config" in logic)) return;
|
|
382
|
+
const config = logic.config;
|
|
383
|
+
if (!config) return;
|
|
384
|
+
const output = resolveStateValueConfig(config, machineSnapshot.value)?.output;
|
|
385
|
+
if (output === void 0) return;
|
|
386
|
+
machineSnapshot.output = typeof output === "function" ? output({
|
|
387
|
+
context: machineSnapshot.context,
|
|
388
|
+
event
|
|
389
|
+
}) : output;
|
|
390
|
+
}
|
|
391
|
+
function isAgentStep(value) {
|
|
392
|
+
return !!value && typeof value === "object" && "snapshot" in value && "actions" in value && "requests" in value;
|
|
393
|
+
}
|
|
394
|
+
//#endregion
|
|
395
|
+
//#region src/event-log.ts
|
|
396
|
+
/**
|
|
397
|
+
* The event log: an append-only journal of the EXTERNAL inputs a machine
|
|
398
|
+
* consumed, and the pure fold ({@link replay}) that turns that journal back
|
|
399
|
+
* into a snapshot without executing anything.
|
|
400
|
+
*
|
|
401
|
+
* A journaled entry is always an external input — an invoke completion
|
|
402
|
+
* (`xstate.done.actor`/`xstate.error.actor` carrying its output inline), a
|
|
403
|
+
* user-sent event, a timer firing, a reserved `@agent.usage` record. Never a
|
|
404
|
+
* raised/internal event: replay re-derives those from the machine's own logic,
|
|
405
|
+
* so journaling them would double-apply them.
|
|
406
|
+
*
|
|
407
|
+
* A log is self-contained: its reserved first entry ({@link initEntry}) carries
|
|
408
|
+
* either the machine `input` (a fresh start) or a persisted snapshot (a
|
|
409
|
+
* continuation across a machine version, or from a log-less legacy snapshot),
|
|
410
|
+
* so {@link replay} needs no side channel.
|
|
411
|
+
* @module
|
|
412
|
+
*/
|
|
413
|
+
/** The durable entry envelope version. */
|
|
414
|
+
const AGENT_EVENT_SCHEMA_VERSION = 1;
|
|
415
|
+
/**
|
|
416
|
+
* The reserved event type of the first entry of every log. It carries the
|
|
417
|
+
* machine `input` (fresh start) or a persisted `snapshot` (continuation), so a
|
|
418
|
+
* log replays with no side channel. Lives in the reserved `@agent.*` namespace
|
|
419
|
+
* so it can never collide with a machine's own vocabulary; it is consumed by
|
|
420
|
+
* {@link replay} and never fed to `transition`.
|
|
421
|
+
*/
|
|
422
|
+
const AGENT_INIT_EVENT_TYPE = "@agent.init";
|
|
423
|
+
/** A malformed or internally inconsistent log. */
|
|
424
|
+
var AgentEventLogError = class extends AgentError {
|
|
425
|
+
constructor(message) {
|
|
426
|
+
super("invalid-event-log", message);
|
|
427
|
+
this.name = "AgentEventLogError";
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
/** A precise failure when an entry would not survive a JSON round-trip. */
|
|
431
|
+
var NonSerializableAgentEventError = class extends AgentError {
|
|
432
|
+
path;
|
|
433
|
+
valueType;
|
|
434
|
+
constructor(path, valueType) {
|
|
435
|
+
super("non-serializable-event", `Agent event field '${path}' is not JSON-serializable (${valueType}).`);
|
|
436
|
+
this.name = "NonSerializableAgentEventError";
|
|
437
|
+
this.path = path;
|
|
438
|
+
this.valueType = valueType;
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
/** Replay reached a state that does not match the entry's recorded hash. */
|
|
442
|
+
var AgentReplayDivergenceError = class extends AgentError {
|
|
443
|
+
constructor(eventId, index, kind, expected, actual) {
|
|
444
|
+
super("replay-diverged", kind === "missing-verification" ? `Replay entry '${eventId}' at index ${index} has no recorded verification hash.` : `Replay diverged after '${eventId}' at index ${index}: expected state hash '${expected}', got '${actual}'.`);
|
|
445
|
+
this.eventId = eventId;
|
|
446
|
+
this.index = index;
|
|
447
|
+
this.kind = kind;
|
|
448
|
+
this.expected = expected;
|
|
449
|
+
this.actual = actual;
|
|
450
|
+
this.name = "AgentReplayDivergenceError";
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
/**
|
|
454
|
+
* An entry was recorded against a different machine (id) or a different
|
|
455
|
+
* machine version than the one replaying it. An init-with-snapshot entry is
|
|
456
|
+
* exempt: that entry IS the bridge from the old version to the new one.
|
|
457
|
+
*/
|
|
458
|
+
var AgentMachineVersionMismatchError = class extends AgentError {
|
|
459
|
+
constructor(eventId, index, expected, actual) {
|
|
460
|
+
super("machine-version-mismatch", `Event log entry '${eventId}' at index ${index} targets machine '${actual.machineId}'@'${actual.machineVersion}', expected '${expected.machineId}'@'${expected.machineVersion}'.`);
|
|
461
|
+
this.eventId = eventId;
|
|
462
|
+
this.index = index;
|
|
463
|
+
this.expected = expected;
|
|
464
|
+
this.actual = actual;
|
|
465
|
+
this.name = "AgentMachineVersionMismatchError";
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
function assertNoHiddenKeys(value, path, isOwnKey) {
|
|
469
|
+
const symbols = Object.getOwnPropertySymbols(value);
|
|
470
|
+
if (symbols.length > 0) throw new NonSerializableAgentEventError(`${path}.[${String(symbols[0])}]`, "symbol key");
|
|
471
|
+
const hiddenKey = Object.getOwnPropertyNames(value).find((key) => !isOwnKey?.(key) && !Object.getOwnPropertyDescriptor(value, key)?.enumerable);
|
|
472
|
+
if (hiddenKey !== void 0) throw new NonSerializableAgentEventError(`${path}.${hiddenKey}`, "non-enumerable property");
|
|
473
|
+
}
|
|
474
|
+
const arrayIndexPattern = /^(?:0|[1-9]\d*)$/;
|
|
475
|
+
/**
|
|
476
|
+
* Rejects values JSON would drop or coerce. Unlike `JSON.stringify`, this does
|
|
477
|
+
* not silently erase `undefined`/functions or turn non-finite numbers into
|
|
478
|
+
* `null`; durable entries contain only plain JSON values.
|
|
479
|
+
*/
|
|
480
|
+
function assertJsonSerializable(value, path = "entry", ancestors = /* @__PURE__ */ new WeakSet()) {
|
|
481
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
482
|
+
if (typeof value === "number") {
|
|
483
|
+
if (!Number.isFinite(value) || Object.is(value, -0)) throw new NonSerializableAgentEventError(path, Object.is(value, -0) ? "-0" : String(value));
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (typeof value !== "object") throw new NonSerializableAgentEventError(path, typeof value);
|
|
487
|
+
if (ancestors.has(value)) throw new NonSerializableAgentEventError(path, "circular reference");
|
|
488
|
+
ancestors.add(value);
|
|
40
489
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
490
|
+
if (Array.isArray(value)) {
|
|
491
|
+
for (let index = 0; index < value.length; index++) {
|
|
492
|
+
if (!Object.hasOwn(value, index)) throw new NonSerializableAgentEventError(`${path}[${index}]`, "array hole");
|
|
493
|
+
assertJsonSerializable(value[index], `${path}[${index}]`, ancestors);
|
|
494
|
+
}
|
|
495
|
+
const extraKey = Object.keys(value).find((key) => !arrayIndexPattern.test(key) || Number(key) >= value.length);
|
|
496
|
+
if (extraKey !== void 0) throw new NonSerializableAgentEventError(`${path}.${extraKey}`, "array property");
|
|
497
|
+
assertNoHiddenKeys(value, path, (key) => key === "length" || arrayIndexPattern.test(key));
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const prototype = Object.getPrototypeOf(value);
|
|
501
|
+
if (prototype !== Object.prototype && prototype !== null) throw new NonSerializableAgentEventError(path, value.constructor?.name ?? "object");
|
|
502
|
+
assertNoHiddenKeys(value, path);
|
|
503
|
+
for (const [key, child] of Object.entries(value)) assertJsonSerializable(child, `${path}.${key}`, ancestors);
|
|
504
|
+
} finally {
|
|
505
|
+
ancestors.delete(value);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function requireNonEmptyString(value, label, qualifier = "") {
|
|
509
|
+
if (typeof value !== "string" || !value) throw new AgentEventLogError(`Agent event ${label} must be a non-empty string${qualifier}.`);
|
|
510
|
+
}
|
|
511
|
+
/** Validates the complete durable envelope before append/export/replay. */
|
|
512
|
+
function assertAgentLogEntry(entry) {
|
|
513
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) throw new AgentEventLogError("Agent event entry must be an object.");
|
|
514
|
+
assertJsonSerializable(entry);
|
|
515
|
+
const candidate = entry;
|
|
516
|
+
if (candidate.schemaVersion !== 1) throw new AgentEventLogError(`Unsupported agent event schema version '${String(candidate.schemaVersion)}'; expected '1'.`);
|
|
517
|
+
if (!Number.isInteger(candidate.index) || candidate.index < 0) throw new AgentEventLogError(`Agent event entry.index must be a non-negative integer; got ${String(candidate.index)}.`);
|
|
518
|
+
for (const field of [
|
|
519
|
+
"id",
|
|
520
|
+
"machineId",
|
|
521
|
+
"machineVersion"
|
|
522
|
+
]) requireNonEmptyString(candidate[field], `entry.${field}`);
|
|
523
|
+
if (typeof candidate.recordedAt !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(candidate.recordedAt) || Number.isNaN(Date.parse(candidate.recordedAt))) throw new AgentEventLogError(`Agent event entry.recordedAt is not RFC 3339: '${String(candidate.recordedAt)}'.`);
|
|
524
|
+
if (candidate.event === null || typeof candidate.event !== "object" || Array.isArray(candidate.event) || typeof candidate.event.type !== "string" || !candidate.event.type) throw new AgentEventLogError("Agent event entry.event requires a non-empty string type.");
|
|
525
|
+
if (candidate.causationId !== void 0) requireNonEmptyString(candidate.causationId, "entry.causationId", " when present");
|
|
526
|
+
if (candidate.metadata !== void 0 && (candidate.metadata === null || typeof candidate.metadata !== "object" || Array.isArray(candidate.metadata))) throw new AgentEventLogError("Agent event entry.metadata must be an object when present.");
|
|
527
|
+
if (candidate.verification !== void 0 && (candidate.verification === null || typeof candidate.verification !== "object" || typeof candidate.verification.stateHash !== "string" || !candidate.verification.stateHash)) throw new AgentEventLogError("Agent event entry.verification requires a non-empty stateHash string.");
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Replaces `Error` instances with plain `{ name, message, cause? }` objects so
|
|
531
|
+
* an `xstate.error.actor` payload survives the log's JSON round-trip. Cycles
|
|
532
|
+
* are preserved by identity; non-plain objects are left alone (and are then
|
|
533
|
+
* rejected by {@link assertJsonSerializable}).
|
|
534
|
+
*/
|
|
535
|
+
function normalizeEventErrors(value, seen = /* @__PURE__ */ new WeakMap()) {
|
|
536
|
+
if (value instanceof Error) {
|
|
537
|
+
const normalized = {
|
|
538
|
+
name: value.name,
|
|
539
|
+
message: value.message
|
|
540
|
+
};
|
|
541
|
+
seen.set(value, normalized);
|
|
542
|
+
if (value.cause !== void 0) normalized.cause = normalizeEventErrors(value.cause, seen);
|
|
543
|
+
return normalized;
|
|
544
|
+
}
|
|
545
|
+
if (value === null || typeof value !== "object") return value;
|
|
546
|
+
const existing = seen.get(value);
|
|
547
|
+
if (existing !== void 0) return existing;
|
|
548
|
+
if (Array.isArray(value)) {
|
|
549
|
+
const result = [];
|
|
550
|
+
seen.set(value, result);
|
|
551
|
+
for (const item of value) result.push(normalizeEventErrors(item, seen));
|
|
552
|
+
return result;
|
|
553
|
+
}
|
|
554
|
+
const prototype = Object.getPrototypeOf(value);
|
|
555
|
+
if (prototype !== Object.prototype && prototype !== null) return value;
|
|
556
|
+
const result = {};
|
|
557
|
+
seen.set(value, result);
|
|
558
|
+
for (const [key, item] of Object.entries(value)) result[key] = normalizeEventErrors(item, seen);
|
|
559
|
+
return result;
|
|
560
|
+
}
|
|
561
|
+
const SNAPSHOT_VOLATILE_KEYS = new Set([
|
|
562
|
+
"sessionId",
|
|
563
|
+
"_nextTimerId",
|
|
564
|
+
"_nextActorIds"
|
|
565
|
+
]);
|
|
566
|
+
const CHILD_VOLATILE_KEYS = new Set(["sessionId"]);
|
|
567
|
+
function positionOf(parent, key) {
|
|
568
|
+
if (parent === "snapshot") return key === "children" ? "children" : "data";
|
|
569
|
+
if (parent === "children") return "child";
|
|
570
|
+
if (parent === "child") return key === "snapshot" ? "snapshot" : "data";
|
|
571
|
+
return "data";
|
|
572
|
+
}
|
|
573
|
+
function canonicalizeForHash(value, seen = /* @__PURE__ */ new WeakSet(), position = "snapshot") {
|
|
574
|
+
if (value === void 0) return "[undefined]";
|
|
575
|
+
if (typeof value === "function") return "[function]";
|
|
576
|
+
if (typeof value === "bigint") return `[bigint:${String(value)}]`;
|
|
577
|
+
if (typeof value === "symbol") return `[symbol:${String(value)}]`;
|
|
578
|
+
if (value === null || typeof value !== "object") return value;
|
|
579
|
+
if (value instanceof Date) return `[Date:${value.toISOString()}]`;
|
|
580
|
+
if (value instanceof Error) {
|
|
581
|
+
if (seen.has(value)) return "[circular]";
|
|
582
|
+
seen.add(value);
|
|
583
|
+
try {
|
|
584
|
+
return {
|
|
585
|
+
errorName: value.name,
|
|
586
|
+
message: value.message,
|
|
587
|
+
...value.cause !== void 0 ? { cause: canonicalizeForHash(value.cause, seen, "data") } : {}
|
|
588
|
+
};
|
|
589
|
+
} finally {
|
|
590
|
+
seen.delete(value);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (seen.has(value)) return "[circular]";
|
|
594
|
+
seen.add(value);
|
|
595
|
+
try {
|
|
596
|
+
if (value instanceof Set) return [...value].map((item) => canonicalizeForHash(item, seen, "data")).sort((a, b) => stableJson(a).localeCompare(stableJson(b)));
|
|
597
|
+
if (value instanceof Map) return [...value.entries()].map(([key, item]) => [canonicalizeForHash(key, seen, "data"), canonicalizeForHash(item, seen, "data")]).sort(([a], [b]) => stableJson(a).localeCompare(stableJson(b)));
|
|
598
|
+
if (Array.isArray(value)) return value.map((item) => canonicalizeForHash(item, seen, "data"));
|
|
599
|
+
const volatileKeys = position === "snapshot" ? SNAPSHOT_VOLATILE_KEYS : position === "child" ? CHILD_VOLATILE_KEYS : void 0;
|
|
600
|
+
const result = {};
|
|
601
|
+
for (const key of Object.keys(value).sort()) {
|
|
602
|
+
if (volatileKeys?.has(key)) continue;
|
|
603
|
+
const child = value[key];
|
|
604
|
+
if (child === void 0) continue;
|
|
605
|
+
result[key] = canonicalizeForHash(child, seen, positionOf(position, key));
|
|
606
|
+
}
|
|
607
|
+
return result;
|
|
608
|
+
} finally {
|
|
609
|
+
seen.delete(value);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Key-sorted JSON for comparing two events by content: key insertion order
|
|
614
|
+
* does not count, so an entry read back from storage equals the one that was
|
|
615
|
+
* appended.
|
|
616
|
+
* @internal
|
|
617
|
+
*/
|
|
618
|
+
function canonicalEventJson(event) {
|
|
619
|
+
return stableJson(canonicalizeForHash(event, /* @__PURE__ */ new Set(), "data"));
|
|
620
|
+
}
|
|
621
|
+
function stableJson(value) {
|
|
622
|
+
return JSON.stringify(sortJson(value));
|
|
623
|
+
}
|
|
624
|
+
function sortJson(value) {
|
|
625
|
+
if (Array.isArray(value)) return value.map(sortJson);
|
|
626
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortJson(value[key])]));
|
|
627
|
+
return value;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* The canonical hash of a PERSISTED snapshot (`machine.getPersistedSnapshot()`,
|
|
631
|
+
* or {@link replay}'s `persistedSnapshot`) — the value recorded in an entry's
|
|
632
|
+
* `verification.stateHash`.
|
|
633
|
+
*
|
|
634
|
+
* Stripped before hashing, because they are re-minted on every fold and would
|
|
635
|
+
* otherwise make two identical runs hash differently: `sessionId`,
|
|
636
|
+
* `_nextTimerId` and `_nextActorIds` at the snapshot root, `sessionId` on each
|
|
637
|
+
* `children[*]` entry (recursively, through nested child snapshots), and
|
|
638
|
+
* `undefined`-valued properties (so a snapshot hashes the same before and
|
|
639
|
+
* after a JSON round-trip). Stripping is confined to those structural
|
|
640
|
+
* positions: a `sessionId` inside `context`, `output`, `error` or an event
|
|
641
|
+
* payload is application data and is hashed. Object keys are sorted, so key
|
|
642
|
+
* order is irrelevant.
|
|
643
|
+
*
|
|
644
|
+
* A change detector, not a cryptographic digest.
|
|
645
|
+
*/
|
|
646
|
+
function getSnapshotStateHash(snapshot) {
|
|
647
|
+
return djb2Hex(stableJson(canonicalizeForHash(snapshot)));
|
|
648
|
+
}
|
|
649
|
+
function machineIdOf(machine) {
|
|
650
|
+
return machine.config.id ?? machine.id ?? "(machine)";
|
|
651
|
+
}
|
|
652
|
+
function toPersisted(machine, snapshot) {
|
|
653
|
+
return typeof snapshot.matches === "function" ? machine.getPersistedSnapshot(snapshot) : snapshot;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Creates a JSON-safe, self-describing entry for `event` appended after
|
|
657
|
+
* `entries` (which must be the complete prefix, so `index` and the recorded
|
|
658
|
+
* state hash line up).
|
|
659
|
+
*/
|
|
660
|
+
function createReplayEntry(machine, entries, event, options = {}) {
|
|
661
|
+
const index = entries.length;
|
|
662
|
+
const machineVersion = options.machineVersion ?? resolveMachineVersion(machine);
|
|
663
|
+
const entry = {
|
|
664
|
+
schemaVersion: 1,
|
|
665
|
+
id: options.id ?? `evt_${String(index).padStart(8, "0")}`,
|
|
666
|
+
index,
|
|
667
|
+
recordedAt: options.recordedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
668
|
+
machineId: machineIdOf(machine),
|
|
669
|
+
machineVersion,
|
|
670
|
+
event: normalizeEventErrors(event),
|
|
671
|
+
...options.causationId !== void 0 ? { causationId: options.causationId } : {},
|
|
672
|
+
...options.metadata !== void 0 ? { metadata: options.metadata } : {}
|
|
673
|
+
};
|
|
674
|
+
assertAgentLogEntry(entry);
|
|
675
|
+
if (options.verification !== false) entry.verification = { stateHash: getSnapshotStateHash(options.snapshot ? toPersisted(machine, options.snapshot) : replay(machine, [...entries, entry], {
|
|
676
|
+
machineVersion,
|
|
677
|
+
verify: false
|
|
678
|
+
}).persistedSnapshot) };
|
|
679
|
+
return entry;
|
|
680
|
+
}
|
|
681
|
+
function pruneUndefined(value) {
|
|
682
|
+
if (Array.isArray(value)) return value.map(pruneUndefined);
|
|
683
|
+
if (value === null || typeof value !== "object") return value;
|
|
684
|
+
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return value;
|
|
685
|
+
const result = {};
|
|
686
|
+
for (const [key, child] of Object.entries(value)) {
|
|
687
|
+
if (child === void 0) continue;
|
|
688
|
+
result[key] = pruneUndefined(child);
|
|
689
|
+
}
|
|
690
|
+
return result;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* The reserved first entry of a log. Pass EXACTLY one of:
|
|
694
|
+
*
|
|
695
|
+
* - `{ input }` — a fresh start; replay folds from `initialTransition`.
|
|
696
|
+
* - `{ snapshot }` — a continuation from a persisted snapshot: a version
|
|
697
|
+
* bridge, or a legacy snapshot that predates the log. Replay restores it
|
|
698
|
+
* purely (no actors started) and folds the rest of the log onto it.
|
|
699
|
+
*
|
|
700
|
+
* `options.metadata.executionId` names the lineage; this library always sets
|
|
701
|
+
* one, and readers must tolerate its absence in logs written elsewhere.
|
|
702
|
+
*/
|
|
703
|
+
function initEntry(machine, init = {}, options = {}) {
|
|
704
|
+
const hasSnapshot = init.snapshot !== void 0;
|
|
705
|
+
if (hasSnapshot && init.input !== void 0) throw new AgentEventLogError("initEntry accepts exactly one of `input` (fresh start) or `snapshot` (continuation).");
|
|
706
|
+
return createReplayEntry(machine, [], {
|
|
707
|
+
type: AGENT_INIT_EVENT_TYPE,
|
|
708
|
+
...hasSnapshot ? { snapshot: pruneUndefined(init.snapshot) } : init.input !== void 0 ? { input: init.input } : {}
|
|
709
|
+
}, options);
|
|
710
|
+
}
|
|
711
|
+
/** Whether `entry` is the reserved init entry. */
|
|
712
|
+
function isInitEntry(entry) {
|
|
713
|
+
return entry?.event?.type === AGENT_INIT_EVENT_TYPE;
|
|
714
|
+
}
|
|
715
|
+
/** Whether `entry` is the version-bridging init-with-snapshot entry. */
|
|
716
|
+
function isSnapshotInitEntry(entry) {
|
|
717
|
+
return isInitEntry(entry) && entry.event.snapshot !== void 0;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* The log's execution id: the lineage identity pinned in the init entry's
|
|
721
|
+
* `metadata.executionId`.
|
|
722
|
+
*
|
|
723
|
+
* Minted once per run and inherited verbatim by every resume of that log, so
|
|
724
|
+
* it names one lineage rather than one process. Entry ids cannot serve this
|
|
725
|
+
* role — the built-in ids are index-derived, so every log's init entry is
|
|
726
|
+
* `evt_00000000`. A fork copies the init entry, so it shares its parent's
|
|
727
|
+
* execution id by construction.
|
|
728
|
+
*
|
|
729
|
+
* Returns `undefined` for a log with no init entry and for one written without
|
|
730
|
+
* the field — callers must treat the id as optional.
|
|
731
|
+
*/
|
|
732
|
+
function getLogExecutionId(entries) {
|
|
733
|
+
const executionId = (entries.find((entry) => isInitEntry(entry))?.metadata)?.executionId;
|
|
734
|
+
return typeof executionId === "string" ? executionId : void 0;
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Validates a log before replay or append: every envelope well-formed, indices
|
|
738
|
+
* contiguous from 0, ids unique, the first entry the reserved init entry, and
|
|
739
|
+
* one consistent machine identity throughout.
|
|
740
|
+
*
|
|
741
|
+
* With `machine`, entries are also checked against that machine's id and
|
|
742
|
+
* resolved version — a mismatch throws {@link AgentMachineVersionMismatchError}.
|
|
743
|
+
* An init-with-snapshot entry is exempt from the version check: that entry is
|
|
744
|
+
* itself the bridge from whatever version produced the snapshot.
|
|
745
|
+
*/
|
|
746
|
+
function validateReplayEntries(entries, options = {}) {
|
|
747
|
+
if (entries.length === 0) return;
|
|
748
|
+
const reference = entries.find((entry) => !isSnapshotInitEntry(entry)) ?? entries[0];
|
|
749
|
+
const expected = options.machine ? {
|
|
750
|
+
machineId: machineIdOf(options.machine),
|
|
751
|
+
machineVersion: options.machineVersion ?? resolveMachineVersion(options.machine)
|
|
752
|
+
} : {
|
|
753
|
+
machineId: reference.machineId,
|
|
754
|
+
machineVersion: options.machineVersion ?? reference.machineVersion
|
|
755
|
+
};
|
|
756
|
+
const ids = /* @__PURE__ */ new Set();
|
|
757
|
+
for (let index = 0; index < entries.length; index++) {
|
|
758
|
+
const entry = entries[index];
|
|
759
|
+
assertAgentLogEntry(entry);
|
|
760
|
+
if (entry.index !== index) throw new AgentEventLogError(`Event log must be contiguous from index 0; found entry.index ${entry.index} at position ${index}.`);
|
|
761
|
+
if (ids.has(entry.id)) throw new AgentEventLogError(`Event log contains duplicate entry id '${entry.id}'.`);
|
|
762
|
+
ids.add(entry.id);
|
|
763
|
+
if (index === 0 && !isInitEntry(entry)) throw new AgentEventLogError(`Event log must start with the reserved '${AGENT_INIT_EVENT_TYPE}' entry; found '${entry.event.type}'.`);
|
|
764
|
+
if (index > 0 && isInitEntry(entry)) throw new AgentEventLogError(`Event log has a second '${AGENT_INIT_EVENT_TYPE}' entry at index ${index}.`);
|
|
765
|
+
const versionMatches = entry.machineVersion === expected.machineVersion;
|
|
766
|
+
if (entry.machineId !== expected.machineId || !versionMatches && !isSnapshotInitEntry(entry)) throw new AgentMachineVersionMismatchError(entry.id, entry.index, expected, {
|
|
767
|
+
machineId: entry.machineId,
|
|
768
|
+
machineVersion: entry.machineVersion
|
|
64
769
|
});
|
|
65
|
-
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* The prefix `[0, upToIndex)` of a log, as a new log. `upToIndex` is exclusive
|
|
774
|
+
* and must leave the init entry in place, so the fork is still self-contained.
|
|
775
|
+
*/
|
|
776
|
+
function forkEventLog(entries, upToIndex) {
|
|
777
|
+
if (!Number.isInteger(upToIndex) || upToIndex < 1 || upToIndex > entries.length) throw new AgentEventLogError(`forkEventLog: upToIndex must be an integer in [1, ${entries.length}]; got ${String(upToIndex)}.`);
|
|
778
|
+
const forked = entries.slice(0, upToIndex);
|
|
779
|
+
if (!isInitEntry(forked[0])) throw new AgentEventLogError(`forkEventLog: the forked prefix must start with the reserved '${AGENT_INIT_EVENT_TYPE}' entry.`);
|
|
780
|
+
return forked;
|
|
781
|
+
}
|
|
782
|
+
const DONE_ACTOR_EVENT_TYPE = "xstate.done.actor";
|
|
783
|
+
const ERROR_ACTOR_EVENT_TYPE = "xstate.error.actor";
|
|
784
|
+
/** Normalizes a mixed `EventObject | AgentLogEntry` history into bare events. */
|
|
785
|
+
function toEvents(history) {
|
|
786
|
+
if (!history) return [];
|
|
787
|
+
return history.map((item) => {
|
|
788
|
+
const candidate = item;
|
|
789
|
+
return candidate?.schemaVersion === 1 && typeof candidate.index === "number" && candidate.event !== null && typeof candidate.event === "object" ? candidate.event : item;
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* The 1-based occurrence `n` for the NEXT call at invoke site `siteId`:
|
|
794
|
+
* `1 + completions`, where a completion is a journaled `xstate.done.actor` OR
|
|
795
|
+
* `xstate.error.actor` for that `actorId` (an error is a semantic completion).
|
|
796
|
+
* The one counting rule behind every `${siteId}#${n}` call key, so a live run
|
|
797
|
+
* and a replay derive the same `n` by construction.
|
|
798
|
+
*/
|
|
799
|
+
function agentCallOccurrence(history, siteId) {
|
|
800
|
+
let count = 0;
|
|
801
|
+
for (const event of toEvents(history)) if ((event.type === DONE_ACTOR_EVENT_TYPE || event.type === ERROR_ACTOR_EVENT_TYPE) && event.actorId === siteId) count++;
|
|
802
|
+
return count + 1;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Folds the reserved `@agent.usage` entries of a log into one cumulative usage
|
|
806
|
+
* total. The log is the source of truth, so the totals are a pure projection of
|
|
807
|
+
* it: replay a log, fold it, and you get the numbers the run reported with no
|
|
808
|
+
* host-side accumulator in between.
|
|
809
|
+
*
|
|
810
|
+
* Accepts entries or bare events. Token fields are partial sums: a field stays
|
|
811
|
+
* `undefined` until some entry reports it, and non-finite values are ignored.
|
|
812
|
+
*/
|
|
813
|
+
function getUsageFromEvents(entries) {
|
|
814
|
+
const totals = {};
|
|
815
|
+
for (const event of toEvents(entries)) {
|
|
816
|
+
if (event.type !== "@agent.usage") continue;
|
|
817
|
+
const usage = event.usage;
|
|
818
|
+
if (!usage || typeof usage !== "object") continue;
|
|
819
|
+
for (const field of AGENT_USAGE_TOKEN_FIELDS) {
|
|
820
|
+
const value = usage[field];
|
|
821
|
+
if (typeof value === "number" && Number.isFinite(value)) totals[field] = (totals[field] ?? 0) + value;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return totals;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Rewrites a journaled `xstate.done.actor`/`xstate.error.actor` event's
|
|
828
|
+
* `sessionId` to the session the CURRENT fold minted for that `actorId`.
|
|
829
|
+
*
|
|
830
|
+
* Session ids are re-minted on every fold, and XState drops a completion event
|
|
831
|
+
* whose `sessionId` does not match the live child — silently, as a no-op
|
|
832
|
+
* transition. Rebinding is what makes a journaled completion replayable at all.
|
|
833
|
+
* `sessions` caches the mapping per fold, so a later event referring to the
|
|
834
|
+
* recorded session of an already-stopped child still resolves.
|
|
835
|
+
*/
|
|
836
|
+
function rebindActorSession(event, snapshot, sessions) {
|
|
837
|
+
const actorEvent = event;
|
|
838
|
+
if (typeof actorEvent.actorId !== "string" || typeof actorEvent.sessionId !== "string") return event;
|
|
839
|
+
const key = `${actorEvent.actorId}\0${actorEvent.sessionId}`;
|
|
840
|
+
let sessionId = sessions.get(key);
|
|
841
|
+
if (!sessionId) {
|
|
842
|
+
const child = snapshot.children[actorEvent.actorId];
|
|
843
|
+
if (typeof child?.sessionId !== "string") return event;
|
|
844
|
+
sessionId = child.sessionId;
|
|
845
|
+
sessions.set(key, sessionId);
|
|
66
846
|
}
|
|
67
847
|
return {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
deps.appendToLog(...plan.appended);
|
|
78
|
-
if (stateRequest.kind !== "decision" && stateRequest.onDone !== void 0) {
|
|
79
|
-
const snapshot = deps.getSnapshot();
|
|
80
|
-
const resolved = typeof stateRequest.onDone === "function" ? stateRequest.onDone({
|
|
81
|
-
output,
|
|
82
|
-
snapshot,
|
|
83
|
-
messages: deps.messages
|
|
84
|
-
}) : stateRequest.onDone;
|
|
85
|
-
if (!resolved) return false;
|
|
86
|
-
const acceptedTypes = getAcceptedEvents(snapshot, { schemas: deps.schemas }).map((descriptor) => descriptor.type);
|
|
87
|
-
if (!acceptedTypes.includes(resolved.type)) throw new Error(`runAgent: getRequests request '${id}' resolved onDone to event '${resolved.type}', which the current state does not accept. Accepted: ${acceptedTypes.join(", ") || "(none)"}.`);
|
|
88
|
-
if (!snapshot.can(resolved)) return false;
|
|
89
|
-
deps.send(resolved);
|
|
90
|
-
return true;
|
|
848
|
+
...event,
|
|
849
|
+
sessionId
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
function verifyEntry(entry, persisted, mode) {
|
|
853
|
+
if (mode === false) return;
|
|
854
|
+
if (!entry.verification) {
|
|
855
|
+
if (mode === "strict") throw new AgentReplayDivergenceError(entry.id, entry.index, "missing-verification");
|
|
856
|
+
return;
|
|
91
857
|
}
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
858
|
+
const actual = getSnapshotStateHash(persisted);
|
|
859
|
+
if (actual !== entry.verification.stateHash) throw new AgentReplayDivergenceError(entry.id, entry.index, "state", entry.verification.stateHash, actual);
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Folds a log through XState's pure `initialTransition`/`transition` WITHOUT
|
|
863
|
+
* executing anything: no actor is started, no action runs, no model is called.
|
|
864
|
+
* A journaled `xstate.done.actor` entry carries the completed actor's output
|
|
865
|
+
* inline, so the invoke it belongs to is never re-run. Crash recovery, fork
|
|
866
|
+
* resume, and time travel in one call.
|
|
867
|
+
*
|
|
868
|
+
* The first entry is the reserved {@link initEntry}: `input` folds from
|
|
869
|
+
* `initialTransition`, `snapshot` restores the persisted snapshot purely
|
|
870
|
+
* (`machine.restoreSnapshot`) and folds the rest of the log onto it.
|
|
871
|
+
*/
|
|
872
|
+
function replay(machine, entries, options = {}) {
|
|
873
|
+
validateReplayEntries(entries, {
|
|
874
|
+
machine,
|
|
875
|
+
machineVersion: options.machineVersion ?? resolveMachineVersion(machine)
|
|
109
876
|
});
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
* executor/`onDone` errors — the host maps them to its error settle.
|
|
123
|
-
*/
|
|
124
|
-
async function runStateRequestPass(requests, deps) {
|
|
125
|
-
const baseMessages = [...deps.messages];
|
|
126
|
-
const plans = await Promise.all(requests.map((stateRequest) => runTextPhase(stateRequest, baseMessages, deps)));
|
|
127
|
-
let sentAny = false;
|
|
128
|
-
for (const plan of plans) {
|
|
129
|
-
if (deps.isSettled()) break;
|
|
130
|
-
sentAny = await runAdvancePhase(plan, deps) || sentAny;
|
|
877
|
+
if (entries.length === 0) throw new AgentEventLogError(`Cannot replay an empty event log; it must start with a '${AGENT_INIT_EVENT_TYPE}' entry.`);
|
|
878
|
+
const initEvent = entries[0].event;
|
|
879
|
+
let snapshot = initEvent.snapshot !== void 0 ? machine.restoreSnapshot(initEvent.snapshot) : initialTransition(machine, initEvent.input)[0];
|
|
880
|
+
let persisted = machine.getPersistedSnapshot(snapshot);
|
|
881
|
+
verifyEntry(entries[0], persisted, options.verify);
|
|
882
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
883
|
+
for (let index = 1; index < entries.length; index++) {
|
|
884
|
+
const entry = entries[index];
|
|
885
|
+
const event = rebindActorSession(entry.event, snapshot, sessions);
|
|
886
|
+
[snapshot] = transition(machine, snapshot, event);
|
|
887
|
+
persisted = machine.getPersistedSnapshot(snapshot);
|
|
888
|
+
verifyEntry(entry, persisted, options.verify);
|
|
131
889
|
}
|
|
132
|
-
return {
|
|
890
|
+
return {
|
|
891
|
+
snapshot,
|
|
892
|
+
persistedSnapshot: persisted
|
|
893
|
+
};
|
|
133
894
|
}
|
|
134
895
|
//#endregion
|
|
135
|
-
//#region src/
|
|
896
|
+
//#region src/event-log-store.ts
|
|
897
|
+
/**
|
|
898
|
+
* Durable storage for {@link AgentLogEntry} logs: the one interface a host
|
|
899
|
+
* implements, plus the in-memory reference implementation the conformance
|
|
900
|
+
* suite is written against.
|
|
901
|
+
* @module
|
|
902
|
+
*/
|
|
136
903
|
/**
|
|
137
|
-
*
|
|
138
|
-
* `
|
|
139
|
-
*
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
this.
|
|
150
|
-
this.
|
|
151
|
-
this.acceptedTypes = acceptedTypes;
|
|
904
|
+
* Rejection from {@link AgentEventLogStore.append} when the thread's stored
|
|
905
|
+
* length is not the `expectedIndex` the writer held — a concurrent writer
|
|
906
|
+
* appended first. `actualIndex` is the next index the store would accept.
|
|
907
|
+
*/
|
|
908
|
+
var AgentEventLogConflictError = class extends AgentError {
|
|
909
|
+
threadId;
|
|
910
|
+
expectedIndex;
|
|
911
|
+
actualIndex;
|
|
912
|
+
constructor(threadId, expectedIndex, actualIndex) {
|
|
913
|
+
super("event-log-conflict", `AgentEventLogStore.append: index conflict on thread "${threadId}": expected index ${expectedIndex} but found ${actualIndex} — a concurrent writer won.`);
|
|
914
|
+
this.name = "AgentEventLogConflictError";
|
|
915
|
+
this.threadId = threadId;
|
|
916
|
+
this.expectedIndex = expectedIndex;
|
|
917
|
+
this.actualIndex = actualIndex;
|
|
152
918
|
}
|
|
153
919
|
};
|
|
154
920
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*/
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
921
|
+
* In-memory reference store, and the baseline the conformance suite runs
|
|
922
|
+
* against. Per-thread entries are held in a contiguous, index-ordered list;
|
|
923
|
+
* append's check-and-push runs synchronously (no `await` between reading the
|
|
924
|
+
* length and writing), so two appends racing on the same `expectedIndex`
|
|
925
|
+
* resolve to exactly one winner. Every stored and returned entry is
|
|
926
|
+
* `structuredClone`d, so a caller can neither mutate stored state through a
|
|
927
|
+
* value it appended nor through a value it read.
|
|
928
|
+
*/
|
|
929
|
+
function createInMemoryEventLogStore() {
|
|
930
|
+
const threads = /* @__PURE__ */ new Map();
|
|
931
|
+
const clone = (value) => structuredClone(value);
|
|
932
|
+
return {
|
|
933
|
+
async append({ threadId, expectedIndex, entries }) {
|
|
934
|
+
for (const entry of entries) assertAgentLogEntry(entry);
|
|
935
|
+
for (let i = 0; i < entries.length; i++) if (entries[i].index !== expectedIndex + i) throw new Error(`AgentEventLogStore.append: entry.index (${entries[i].index}) must be contiguous from expectedIndex (${expectedIndex}); expected ${expectedIndex + i} at position ${i} on thread "${threadId}".`);
|
|
936
|
+
const list = threads.get(threadId);
|
|
937
|
+
const length = list ? list.length : 0;
|
|
938
|
+
if (length !== expectedIndex) throw new AgentEventLogConflictError(threadId, expectedIndex, length);
|
|
939
|
+
const ids = new Set((list ?? []).map((entry) => entry.id));
|
|
940
|
+
for (const entry of entries) {
|
|
941
|
+
if (ids.has(entry.id)) throw new Error(`AgentEventLogStore.append: duplicate event id "${entry.id}" in thread "${threadId}".`);
|
|
942
|
+
ids.add(entry.id);
|
|
943
|
+
}
|
|
944
|
+
const cloned = entries.map((entry) => clone(entry));
|
|
945
|
+
if (list) for (const entry of cloned) list.push(entry);
|
|
946
|
+
else threads.set(threadId, cloned);
|
|
947
|
+
},
|
|
948
|
+
async read(threadId, options) {
|
|
949
|
+
const list = threads.get(threadId) ?? [];
|
|
950
|
+
const from = options?.from ?? 0;
|
|
951
|
+
return list.slice(from).map((entry) => clone(entry));
|
|
952
|
+
},
|
|
953
|
+
async length(threadId) {
|
|
954
|
+
return threads.get(threadId)?.length ?? 0;
|
|
955
|
+
},
|
|
956
|
+
async fork({ threadId, newThreadId, upToIndex }) {
|
|
957
|
+
if ((threads.get(newThreadId)?.length ?? 0) > 0) throw new Error(`AgentEventLogStore.fork: newThreadId "${newThreadId}" already has entries.`);
|
|
958
|
+
const source = threads.get(threadId);
|
|
959
|
+
if (!source) throw new Error(`AgentEventLogStore.fork: unknown source thread "${threadId}".`);
|
|
960
|
+
const upTo = upToIndex ?? source.length;
|
|
961
|
+
if (!Number.isInteger(upTo) || upTo < 1 || upTo > source.length) throw new Error(`AgentEventLogStore.fork: upToIndex must be an integer in [1, ${source.length}] on thread "${threadId}"; got ${String(upTo)}.`);
|
|
962
|
+
threads.set(newThreadId, source.slice(0, upTo).map((entry) => clone(entry)));
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
//#endregion
|
|
967
|
+
//#region src/event-log-store-conformance.ts
|
|
968
|
+
/**
|
|
969
|
+
* The runner-agnostic conformance suite for {@link AgentEventLogStore}
|
|
970
|
+
* implementations. Split out of `event-log-store.ts` so the store contract and
|
|
971
|
+
* the reference implementation stay readable.
|
|
972
|
+
*
|
|
973
|
+
* Every case throws a descriptive `Error` on the first violation, so any test
|
|
974
|
+
* runner (or a plain script) can drive it against its own store. Pass a
|
|
975
|
+
* `{ describe, it }` harness to register one test per case instead.
|
|
976
|
+
* @module
|
|
977
|
+
*/
|
|
978
|
+
function fail(message) {
|
|
979
|
+
throw new Error(`event-log-store conformance: ${message}`);
|
|
980
|
+
}
|
|
981
|
+
function entry(index, type, metadata) {
|
|
982
|
+
return {
|
|
983
|
+
schemaVersion: 1,
|
|
984
|
+
id: `evt_${index}`,
|
|
985
|
+
index,
|
|
986
|
+
recordedAt: "2026-01-01T00:00:00.000Z",
|
|
987
|
+
machineId: "conformance",
|
|
988
|
+
machineVersion: "v1",
|
|
989
|
+
event: {
|
|
990
|
+
type,
|
|
991
|
+
seq: index
|
|
992
|
+
},
|
|
993
|
+
...metadata !== void 0 ? { metadata } : {}
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
function entriesFrom(start, count) {
|
|
997
|
+
return Array.from({ length: count }, (_unused, i) => entry(start + i, `e${start + i}`));
|
|
998
|
+
}
|
|
999
|
+
function assertJsonEqual(actual, expected, message) {
|
|
1000
|
+
const a = JSON.stringify(actual);
|
|
1001
|
+
const b = JSON.stringify(expected);
|
|
1002
|
+
if (a !== b) fail(`${message} (expected ${b}, got ${a})`);
|
|
1003
|
+
}
|
|
1004
|
+
const cases = [
|
|
1005
|
+
{
|
|
1006
|
+
name: "an unknown thread reads empty and has length 0",
|
|
1007
|
+
async run(create) {
|
|
1008
|
+
const store = await create();
|
|
1009
|
+
assertJsonEqual(await store.read("missing"), [], "read of an unknown thread must be empty");
|
|
1010
|
+
if (await store.length("missing") !== 0) fail("length of an unknown thread must be 0");
|
|
1011
|
+
}
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
name: "a single append reads back verbatim",
|
|
1015
|
+
async run(create) {
|
|
1016
|
+
const store = await create();
|
|
1017
|
+
const e = entry(0, "start", { label: "first" });
|
|
1018
|
+
await store.append({
|
|
1019
|
+
threadId: "t",
|
|
1020
|
+
expectedIndex: 0,
|
|
1021
|
+
entries: [e]
|
|
1022
|
+
});
|
|
1023
|
+
assertJsonEqual(await store.read("t"), [e], "read must return the appended entry");
|
|
1024
|
+
if (await store.length("t") !== 1) fail("length after a single append must be 1");
|
|
1025
|
+
}
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
name: "a second append extends the log in order",
|
|
1029
|
+
async run(create) {
|
|
1030
|
+
const store = await create();
|
|
1031
|
+
await store.append({
|
|
1032
|
+
threadId: "t",
|
|
1033
|
+
expectedIndex: 0,
|
|
1034
|
+
entries: entriesFrom(0, 3)
|
|
1035
|
+
});
|
|
1036
|
+
await store.append({
|
|
1037
|
+
threadId: "t",
|
|
1038
|
+
expectedIndex: 3,
|
|
1039
|
+
entries: entriesFrom(3, 2)
|
|
1040
|
+
});
|
|
1041
|
+
if (await store.length("t") !== 5) fail("length after appending 3 then 2 entries must be 5");
|
|
1042
|
+
assertJsonEqual((await store.read("t")).map((e) => e.index), [
|
|
1043
|
+
0,
|
|
1044
|
+
1,
|
|
1045
|
+
2,
|
|
1046
|
+
3,
|
|
1047
|
+
4
|
|
1048
|
+
], "reads must return entries in contiguous log order");
|
|
1049
|
+
}
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
name: "a non-contiguous entry.index is misuse, not a race",
|
|
1053
|
+
async run(create) {
|
|
1054
|
+
const store = await create();
|
|
1055
|
+
let caught;
|
|
1056
|
+
try {
|
|
1057
|
+
await store.append({
|
|
1058
|
+
threadId: "t",
|
|
1059
|
+
expectedIndex: 0,
|
|
1060
|
+
entries: [entry(1, "gap")]
|
|
1061
|
+
});
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
caught = error;
|
|
1064
|
+
}
|
|
1065
|
+
if (!(caught instanceof Error) || caught instanceof AgentEventLogConflictError) fail("a non-contiguous entry.index must throw a plain Error, not a conflict");
|
|
1066
|
+
}
|
|
1067
|
+
},
|
|
1068
|
+
{
|
|
1069
|
+
name: "a stale expectedIndex conflicts with correct fields",
|
|
1070
|
+
async run(create) {
|
|
1071
|
+
const store = await create();
|
|
1072
|
+
await store.append({
|
|
1073
|
+
threadId: "t",
|
|
1074
|
+
expectedIndex: 0,
|
|
1075
|
+
entries: [entry(0, "a")]
|
|
1076
|
+
});
|
|
1077
|
+
let caught;
|
|
1078
|
+
try {
|
|
1079
|
+
await store.append({
|
|
1080
|
+
threadId: "t",
|
|
1081
|
+
expectedIndex: 0,
|
|
1082
|
+
entries: [entry(0, "b")]
|
|
1083
|
+
});
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
caught = error;
|
|
1086
|
+
}
|
|
1087
|
+
if (!(caught instanceof AgentEventLogConflictError)) fail("a stale expectedIndex must throw AgentEventLogConflictError");
|
|
1088
|
+
if (caught.threadId !== "t" || caught.expectedIndex !== 0 || caught.actualIndex !== 1) fail("conflict error must carry threadId, expectedIndex, and the actual index");
|
|
1089
|
+
}
|
|
1090
|
+
},
|
|
1091
|
+
{
|
|
1092
|
+
name: "entry ids are unique within a thread",
|
|
1093
|
+
async run(create) {
|
|
1094
|
+
const store = await create();
|
|
1095
|
+
await store.append({
|
|
1096
|
+
threadId: "t",
|
|
1097
|
+
expectedIndex: 0,
|
|
1098
|
+
entries: [entry(0, "a")]
|
|
1099
|
+
});
|
|
1100
|
+
let caught;
|
|
1101
|
+
try {
|
|
1102
|
+
await store.append({
|
|
1103
|
+
threadId: "t",
|
|
1104
|
+
expectedIndex: 1,
|
|
1105
|
+
entries: [{
|
|
1106
|
+
...entry(1, "b"),
|
|
1107
|
+
id: "evt_0"
|
|
1108
|
+
}]
|
|
1109
|
+
});
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
caught = error;
|
|
1112
|
+
}
|
|
1113
|
+
if (!(caught instanceof Error) || caught instanceof AgentEventLogConflictError) fail("a duplicate event id within a thread must throw a plain Error");
|
|
1114
|
+
}
|
|
1115
|
+
},
|
|
1116
|
+
{
|
|
1117
|
+
name: "concurrent appends at the same index have exactly one winner",
|
|
1118
|
+
async run(create) {
|
|
1119
|
+
const store = await create();
|
|
1120
|
+
await store.append({
|
|
1121
|
+
threadId: "t",
|
|
1122
|
+
expectedIndex: 0,
|
|
1123
|
+
entries: [entry(0, "a")]
|
|
1124
|
+
});
|
|
1125
|
+
const results = await Promise.allSettled([store.append({
|
|
1126
|
+
threadId: "t",
|
|
1127
|
+
expectedIndex: 1,
|
|
1128
|
+
entries: [entry(1, "w-a")]
|
|
1129
|
+
}), store.append({
|
|
1130
|
+
threadId: "t",
|
|
1131
|
+
expectedIndex: 1,
|
|
1132
|
+
entries: [entry(1, "w-b")]
|
|
1133
|
+
})]);
|
|
1134
|
+
const winners = results.filter((r) => r.status === "fulfilled");
|
|
1135
|
+
const conflicts = results.filter((r) => r.status === "rejected" && r.reason instanceof AgentEventLogConflictError);
|
|
1136
|
+
if (winners.length !== 1) fail(`exactly one racing append must win, got ${winners.length}`);
|
|
1137
|
+
if (conflicts.length !== 1) fail(`exactly one racing append must reject with a conflict, got ${conflicts.length}`);
|
|
1138
|
+
if (await store.length("t") !== 2) fail("after the race the log length must be 2");
|
|
1139
|
+
}
|
|
1140
|
+
},
|
|
1141
|
+
{
|
|
1142
|
+
name: "read({ from }) is incrementally correct",
|
|
1143
|
+
async run(create) {
|
|
1144
|
+
const store = await create();
|
|
1145
|
+
await store.append({
|
|
1146
|
+
threadId: "t",
|
|
1147
|
+
expectedIndex: 0,
|
|
1148
|
+
entries: entriesFrom(0, 5)
|
|
1149
|
+
});
|
|
1150
|
+
assertJsonEqual((await store.read("t", { from: 2 })).map((e) => e.index), [
|
|
1151
|
+
2,
|
|
1152
|
+
3,
|
|
1153
|
+
4
|
|
1154
|
+
], "read({ from }) must skip entries below `from`");
|
|
1155
|
+
assertJsonEqual(await store.read("t", { from: 5 }), [], "read({ from }) at the log length must be empty");
|
|
1156
|
+
}
|
|
1157
|
+
},
|
|
1158
|
+
{
|
|
1159
|
+
name: "threads are isolated and grow independently",
|
|
1160
|
+
async run(create) {
|
|
1161
|
+
const store = await create();
|
|
1162
|
+
await store.append({
|
|
1163
|
+
threadId: "a",
|
|
1164
|
+
expectedIndex: 0,
|
|
1165
|
+
entries: [entry(0, "x")]
|
|
1166
|
+
});
|
|
1167
|
+
if (await store.length("b") !== 0) fail("an append to one thread must not create another");
|
|
1168
|
+
await store.append({
|
|
1169
|
+
threadId: "b",
|
|
1170
|
+
expectedIndex: 0,
|
|
1171
|
+
entries: entriesFrom(0, 2)
|
|
1172
|
+
});
|
|
1173
|
+
if (await store.length("a") !== 1 || await store.length("b") !== 2) fail("threads must grow independently");
|
|
1174
|
+
}
|
|
1175
|
+
},
|
|
1176
|
+
{
|
|
1177
|
+
name: "metadata round-trips verbatim",
|
|
1178
|
+
async run(create) {
|
|
1179
|
+
const store = await create();
|
|
1180
|
+
const metadata = {
|
|
1181
|
+
source: "user",
|
|
1182
|
+
nested: { attempt: 2 },
|
|
1183
|
+
tags: ["x", "y"]
|
|
1184
|
+
};
|
|
1185
|
+
await store.append({
|
|
1186
|
+
threadId: "t",
|
|
1187
|
+
expectedIndex: 0,
|
|
1188
|
+
entries: [entry(0, "e", metadata)]
|
|
1189
|
+
});
|
|
1190
|
+
assertJsonEqual((await store.read("t"))[0].metadata, metadata, "metadata must round-trip verbatim");
|
|
1191
|
+
}
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
name: "stored entries are isolated from caller mutation",
|
|
1195
|
+
async run(create) {
|
|
1196
|
+
const store = await create();
|
|
1197
|
+
const e = entry(0, "e", { tags: ["a"] });
|
|
1198
|
+
await store.append({
|
|
1199
|
+
threadId: "t",
|
|
1200
|
+
expectedIndex: 0,
|
|
1201
|
+
entries: [e]
|
|
1202
|
+
});
|
|
1203
|
+
e.metadata.tags.push("mutated-after-append");
|
|
1204
|
+
e.event.seq = 999;
|
|
1205
|
+
const read = await store.read("t");
|
|
1206
|
+
read[0].metadata.tags.push("mutated-after-read");
|
|
1207
|
+
read[0].event.seq = 888;
|
|
1208
|
+
const reread = await store.read("t");
|
|
1209
|
+
assertJsonEqual(reread[0].metadata, { tags: ["a"] }, "stored entry must be isolated from post-append and post-read mutation");
|
|
1210
|
+
assertJsonEqual(reread[0].event, {
|
|
1211
|
+
type: "e",
|
|
1212
|
+
seq: 0
|
|
1213
|
+
}, "stored event must be isolated from mutation of an appended or read copy");
|
|
1214
|
+
}
|
|
1215
|
+
},
|
|
1216
|
+
{
|
|
1217
|
+
name: "fork copies a prefix and then diverges independently",
|
|
1218
|
+
async run(create) {
|
|
1219
|
+
const store = await create();
|
|
1220
|
+
await store.append({
|
|
1221
|
+
threadId: "src",
|
|
1222
|
+
expectedIndex: 0,
|
|
1223
|
+
entries: entriesFrom(0, 3)
|
|
1224
|
+
});
|
|
1225
|
+
await store.fork({
|
|
1226
|
+
threadId: "src",
|
|
1227
|
+
newThreadId: "fork-full"
|
|
1228
|
+
});
|
|
1229
|
+
if (await store.length("fork-full") !== 3) fail("fork with the default upToIndex must copy the full log");
|
|
1230
|
+
await store.fork({
|
|
1231
|
+
threadId: "src",
|
|
1232
|
+
newThreadId: "fork-1",
|
|
1233
|
+
upToIndex: 1
|
|
1234
|
+
});
|
|
1235
|
+
assertJsonEqual((await store.read("fork-1")).map((e) => e.index), [0], "fork with upToIndex 1 must copy only entry 0");
|
|
1236
|
+
await store.fork({
|
|
1237
|
+
threadId: "src",
|
|
1238
|
+
newThreadId: "fork-2",
|
|
1239
|
+
upToIndex: 2
|
|
1240
|
+
});
|
|
1241
|
+
assertJsonEqual((await store.read("fork-2")).map((e) => e.id), ["evt_0", "evt_1"], "fork with upToIndex 2 must copy entries 0 and 1 only");
|
|
1242
|
+
await store.append({
|
|
1243
|
+
threadId: "fork-1",
|
|
1244
|
+
expectedIndex: 1,
|
|
1245
|
+
entries: [entry(1, "branch")]
|
|
1246
|
+
});
|
|
1247
|
+
assertJsonEqual((await store.read("fork-1")).map((e) => e.event.type), ["e0", "branch"], "a forked thread must append independently from its copied length");
|
|
1248
|
+
if (await store.length("src") !== 3) fail("appending to a fork must not touch the source thread");
|
|
1249
|
+
}
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
name: "fork rejects a non-empty target, an unknown source, and an out-of-range cutoff (0 included)",
|
|
1253
|
+
async run(create) {
|
|
1254
|
+
const store = await create();
|
|
1255
|
+
await store.append({
|
|
1256
|
+
threadId: "src",
|
|
1257
|
+
expectedIndex: 0,
|
|
1258
|
+
entries: entriesFrom(0, 3)
|
|
1259
|
+
});
|
|
1260
|
+
await store.fork({
|
|
1261
|
+
threadId: "src",
|
|
1262
|
+
newThreadId: "fork-full"
|
|
1263
|
+
});
|
|
1264
|
+
let caughtExisting;
|
|
1265
|
+
try {
|
|
1266
|
+
await store.fork({
|
|
1267
|
+
threadId: "src",
|
|
1268
|
+
newThreadId: "fork-full"
|
|
1269
|
+
});
|
|
1270
|
+
} catch (error) {
|
|
1271
|
+
caughtExisting = error;
|
|
1272
|
+
}
|
|
1273
|
+
if (!(caughtExisting instanceof Error) || caughtExisting instanceof AgentEventLogConflictError) fail("forking onto a non-empty thread must reject with a plain Error");
|
|
1274
|
+
let caughtUnknown;
|
|
1275
|
+
try {
|
|
1276
|
+
await store.fork({
|
|
1277
|
+
threadId: "nope",
|
|
1278
|
+
newThreadId: "fork-nope"
|
|
1279
|
+
});
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
caughtUnknown = error;
|
|
1282
|
+
}
|
|
1283
|
+
if (!(caughtUnknown instanceof Error) || caughtUnknown instanceof AgentEventLogConflictError) fail("forking an unknown source thread must reject with a plain Error");
|
|
1284
|
+
let caughtRange;
|
|
1285
|
+
try {
|
|
1286
|
+
await store.fork({
|
|
1287
|
+
threadId: "src",
|
|
1288
|
+
newThreadId: "fork-range",
|
|
1289
|
+
upToIndex: 99
|
|
1290
|
+
});
|
|
1291
|
+
} catch (error) {
|
|
1292
|
+
caughtRange = error;
|
|
1293
|
+
}
|
|
1294
|
+
if (!(caughtRange instanceof Error) || caughtRange instanceof AgentEventLogConflictError) fail("forking past the source length must reject with a plain Error");
|
|
1295
|
+
let caughtZero;
|
|
1296
|
+
try {
|
|
1297
|
+
await store.fork({
|
|
1298
|
+
threadId: "src",
|
|
1299
|
+
newThreadId: "fork-zero",
|
|
1300
|
+
upToIndex: 0
|
|
1301
|
+
});
|
|
1302
|
+
} catch (error) {
|
|
1303
|
+
caughtZero = error;
|
|
1304
|
+
}
|
|
1305
|
+
if (!(caughtZero instanceof Error) || caughtZero instanceof AgentEventLogConflictError) fail("forking with upToIndex 0 must reject with a plain Error");
|
|
1306
|
+
if (await store.length("fork-zero") !== 0) fail("a rejected fork must not create the target thread");
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
];
|
|
1310
|
+
/**
|
|
1311
|
+
* Validates a store against the reference's semantics: empty read + zero length
|
|
1312
|
+
* for unknown threads; single and multi-entry append; contiguity misuse guard;
|
|
1313
|
+
* stale-`expectedIndex` conflict with correct fields; an interleaved concurrent
|
|
1314
|
+
* append race (exactly one winner); entry-id uniqueness; `read({ from })`
|
|
1315
|
+
* incremental correctness; thread isolation; metadata round-trip; deep-copy
|
|
1316
|
+
* isolation on append and read; and the full fork contract.
|
|
1317
|
+
*
|
|
1318
|
+
* Without a `harness`, every case runs in sequence and the first violation
|
|
1319
|
+
* throws. With one, each case is registered as its own test:
|
|
1320
|
+
*
|
|
1321
|
+
* ```ts
|
|
1322
|
+
* import { describe, it } from 'vitest';
|
|
1323
|
+
* assertEventLogStoreConformance(createMyStore, { describe, it });
|
|
1324
|
+
* ```
|
|
1325
|
+
*/
|
|
1326
|
+
async function assertEventLogStoreConformance(create, harness) {
|
|
1327
|
+
if (harness) {
|
|
1328
|
+
harness.describe("AgentEventLogStore conformance", () => {
|
|
1329
|
+
for (const testCase of cases) harness.it(testCase.name, () => testCase.run(create));
|
|
1330
|
+
});
|
|
1331
|
+
return;
|
|
173
1332
|
}
|
|
174
|
-
|
|
1333
|
+
for (const testCase of cases) await testCase.run(create);
|
|
1334
|
+
}
|
|
1335
|
+
//#endregion
|
|
1336
|
+
//#region src/run-agent.ts
|
|
175
1337
|
/**
|
|
176
|
-
* Thrown by {@link
|
|
177
|
-
* `
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
this.
|
|
188
|
-
this.
|
|
189
|
-
this.
|
|
1338
|
+
* Thrown by {@link runAgent} when a resume is given BOTH an `events` log and a
|
|
1339
|
+
* `snapshot` that claims a position in that log (`agentMeta.logIndex`), and the
|
|
1340
|
+
* two disagree about the state at that position. The log is the source of
|
|
1341
|
+
* truth, so this is a host bug (a snapshot cached from a different lineage, or
|
|
1342
|
+
* a snapshot mutated out of band), not a recoverable resume: pass the log
|
|
1343
|
+
* alone, or the matching snapshot.
|
|
1344
|
+
*/
|
|
1345
|
+
var AgentSnapshotDivergedError = class extends AgentError {
|
|
1346
|
+
constructor(expected, actual, logIndex) {
|
|
1347
|
+
super("snapshot-diverged", `runAgent: the resume snapshot disagrees with the event log at index ${logIndex}: the log replays to state hash '${expected}', the snapshot hashes '${actual}'.`);
|
|
1348
|
+
this.expected = expected;
|
|
1349
|
+
this.actual = actual;
|
|
1350
|
+
this.logIndex = logIndex;
|
|
1351
|
+
this.name = "AgentSnapshotDivergedError";
|
|
190
1352
|
}
|
|
191
1353
|
};
|
|
1354
|
+
function hashResumeSnapshot(snapshot) {
|
|
1355
|
+
if (snapshot !== null && typeof snapshot === "object" && "agentMeta" in snapshot) {
|
|
1356
|
+
const rest = {};
|
|
1357
|
+
for (const [key, value] of Object.entries(snapshot)) if (key !== "agentMeta") rest[key] = value;
|
|
1358
|
+
return getSnapshotStateHash(rest);
|
|
1359
|
+
}
|
|
1360
|
+
return getSnapshotStateHash(snapshot);
|
|
1361
|
+
}
|
|
1362
|
+
function readAgentMeta(snapshot) {
|
|
1363
|
+
const meta = snapshot?.agentMeta;
|
|
1364
|
+
return meta !== null && typeof meta === "object" ? meta : void 0;
|
|
1365
|
+
}
|
|
192
1366
|
/**
|
|
193
1367
|
* The version of the {@link AgentTraceEvent} envelope every trace event carries
|
|
194
1368
|
* as `schemaVersion`. Bumped only on a breaking change to the envelope or any
|
|
@@ -207,8 +1381,8 @@ const TRACE_ENVELOPE_KEYS = [
|
|
|
207
1381
|
"type",
|
|
208
1382
|
"status",
|
|
209
1383
|
"cause",
|
|
210
|
-
"eventId",
|
|
211
1384
|
"reasoning",
|
|
1385
|
+
"finishReason",
|
|
212
1386
|
"chunk"
|
|
213
1387
|
];
|
|
214
1388
|
/**
|
|
@@ -284,69 +1458,6 @@ function serializeTraceEvent(event, options = {}) {
|
|
|
284
1458
|
}
|
|
285
1459
|
return out;
|
|
286
1460
|
}
|
|
287
|
-
function snapshotNodes(snapshot) {
|
|
288
|
-
return (snapshot.nodes ?? []).map((raw) => {
|
|
289
|
-
const node = raw;
|
|
290
|
-
return {
|
|
291
|
-
id: node.id ?? "",
|
|
292
|
-
key: node.key ?? "",
|
|
293
|
-
...node.description !== void 0 ? { description: node.description } : {},
|
|
294
|
-
tags: [...node.tags ?? []],
|
|
295
|
-
...node.meta !== void 0 ? { meta: node.meta } : {},
|
|
296
|
-
ownEvents: [...node.ownEvents ?? []],
|
|
297
|
-
leaf: Object.keys(node.states ?? {}).length === 0
|
|
298
|
-
};
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
/**
|
|
302
|
-
* The active state nodes of a snapshot, as plain {@link AgentSnapshotNode}
|
|
303
|
-
* descriptors. The escape hatch under {@link getSnapshotRequests}: use it when
|
|
304
|
-
* your `getRequests` hook needs to build requests some other way, so host code
|
|
305
|
-
* never touches xstate's private `snapshot._nodes`.
|
|
306
|
-
*/
|
|
307
|
-
function getSnapshotNodes(snapshot) {
|
|
308
|
-
return snapshotNodes(snapshot);
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Builds the {@link AgentStateRequest}s for a snapshot straight from its active
|
|
312
|
-
* state nodes — the prompts-in-descriptions recipe as a function, so a
|
|
313
|
-
* {@link RunAgentOptions.getRequests} hook is one line and never reaches into
|
|
314
|
-
* xstate's private `snapshot._nodes`:
|
|
315
|
-
*
|
|
316
|
-
* ```ts
|
|
317
|
-
* runAgent(machine, {
|
|
318
|
-
* executors,
|
|
319
|
-
* getRequests: (snapshot) => getSnapshotRequests(snapshot, { model: 'writer' }),
|
|
320
|
-
* });
|
|
321
|
-
* ```
|
|
322
|
-
*
|
|
323
|
-
* Each described active node becomes one request: `prompt` from the node's
|
|
324
|
-
* `description`, `kind: 'decision'` when it is tagged `'decision'`, `system`
|
|
325
|
-
* from `meta.role` when present, `allowedEvents` scoped to the node's own
|
|
326
|
-
* events, and an explicit `onDone` when the node has exactly one own event
|
|
327
|
-
* (single-outcome states advance deterministically; anything else falls
|
|
328
|
-
* through to a `decide` call). Nodes tagged `'waiting'` produce nothing, so the
|
|
329
|
-
* run settles idle for a human. Override any of it with `filter`/`map`.
|
|
330
|
-
*/
|
|
331
|
-
function getSnapshotRequests(snapshot, options) {
|
|
332
|
-
const filter = options.filter ?? ((node) => !!node.description && !node.tags.includes("waiting"));
|
|
333
|
-
const requests = [];
|
|
334
|
-
for (const node of snapshotNodes(snapshot)) {
|
|
335
|
-
if (!filter(node)) continue;
|
|
336
|
-
const system = node.meta?.role;
|
|
337
|
-
const request = {
|
|
338
|
-
model: options.model,
|
|
339
|
-
prompt: node.description ?? "",
|
|
340
|
-
kind: node.tags.includes("decision") ? "decision" : "text",
|
|
341
|
-
...system !== void 0 ? { system } : {},
|
|
342
|
-
...node.ownEvents.length > 0 ? { allowedEvents: node.ownEvents } : {},
|
|
343
|
-
...node.ownEvents.length === 1 ? { onDone: { type: node.ownEvents[0] } } : {}
|
|
344
|
-
};
|
|
345
|
-
const mapped = options.map ? options.map(request, node) : request;
|
|
346
|
-
if (mapped) requests.push(mapped);
|
|
347
|
-
}
|
|
348
|
-
return requests;
|
|
349
|
-
}
|
|
350
1461
|
let nextRunAgentTraceId = 1;
|
|
351
1462
|
/**
|
|
352
1463
|
* Thrown into the invoke that would have made the call once
|
|
@@ -571,10 +1682,10 @@ function bindTextLogic(logic, runCtx) {
|
|
|
571
1682
|
return logic.withExecutor(async ({ request: rawRequest, self: selfArg, signal }) => {
|
|
572
1683
|
const self = selfArg;
|
|
573
1684
|
const { id, src } = selfIdAndSrc(self);
|
|
574
|
-
const request =
|
|
1685
|
+
const request = {
|
|
575
1686
|
...rawRequest,
|
|
576
|
-
name: src
|
|
577
|
-
}
|
|
1687
|
+
name: rawRequest.name ?? (src !== "" && src !== "agent.generateText" && src !== "agent.streamText" ? src : id || src)
|
|
1688
|
+
};
|
|
578
1689
|
const executor = logic.mode === "stream" ? runCtx.streamText : runCtx.generateText;
|
|
579
1690
|
if (!executor) throw new Error(`No '${logic.mode === "stream" ? "streamText" : "generateText"}' executor provided.`);
|
|
580
1691
|
const requestWithTools = {
|
|
@@ -596,6 +1707,8 @@ function bindTextLogic(logic, runCtx) {
|
|
|
596
1707
|
request: agentRequest
|
|
597
1708
|
}, self);
|
|
598
1709
|
try {
|
|
1710
|
+
const callKey = id !== "" ? runCtx.callKey?.(id, self) : void 0;
|
|
1711
|
+
await runCtx.awaitJournal?.();
|
|
599
1712
|
const raw = await executor(requestWithTools, {
|
|
600
1713
|
onChunk: (chunk) => {
|
|
601
1714
|
runCtx.emitTrace?.({
|
|
@@ -606,7 +1719,8 @@ function bindTextLogic(logic, runCtx) {
|
|
|
606
1719
|
},
|
|
607
1720
|
signal,
|
|
608
1721
|
...runCtx.runId !== void 0 ? { runId: runCtx.runId } : {},
|
|
609
|
-
...id !== "" ? { requestId: id } : {}
|
|
1722
|
+
...id !== "" ? { requestId: id } : {},
|
|
1723
|
+
...callKey !== void 0 ? { callKey } : {}
|
|
610
1724
|
});
|
|
611
1725
|
const output = await normalizeGeneratorResult(raw, id, {
|
|
612
1726
|
request,
|
|
@@ -618,9 +1732,21 @@ function bindTextLogic(logic, runCtx) {
|
|
|
618
1732
|
}, self);
|
|
619
1733
|
}
|
|
620
1734
|
});
|
|
1735
|
+
const responseMessages = raw?.messages;
|
|
1736
|
+
if (Array.isArray(responseMessages) && responseMessages.length > 0) {
|
|
1737
|
+
const parent = invokingActorOf(self, runCtx);
|
|
1738
|
+
const event = {
|
|
1739
|
+
type: AGENT_MESSAGES_EVENT_TYPE,
|
|
1740
|
+
request: request.name,
|
|
1741
|
+
actorId: id,
|
|
1742
|
+
messages: responseMessages
|
|
1743
|
+
};
|
|
1744
|
+
if ((parent?.getSnapshot())?.can(event)) parent?.send(event);
|
|
1745
|
+
}
|
|
621
1746
|
const rawReasoning = raw?.reasoning;
|
|
622
1747
|
const reasoning = typeof rawReasoning === "string" ? rawReasoning : void 0;
|
|
623
1748
|
const usage = getCallUsage(raw);
|
|
1749
|
+
const finishReason = getCallFinishReason(raw);
|
|
624
1750
|
if (usage) runCtx.recordUsage?.(usage, {
|
|
625
1751
|
kind: "text",
|
|
626
1752
|
...id !== "" ? { id } : {},
|
|
@@ -634,7 +1760,8 @@ function bindTextLogic(logic, runCtx) {
|
|
|
634
1760
|
output,
|
|
635
1761
|
raw,
|
|
636
1762
|
...reasoning !== void 0 ? { reasoning } : {},
|
|
637
|
-
...usage !== void 0 ? { usage } : {}
|
|
1763
|
+
...usage !== void 0 ? { usage } : {},
|
|
1764
|
+
...finishReason !== void 0 ? { finishReason } : {}
|
|
638
1765
|
}, self);
|
|
639
1766
|
return { output };
|
|
640
1767
|
} catch (error) {
|
|
@@ -656,15 +1783,20 @@ function createCountingDecide(runCtx, self) {
|
|
|
656
1783
|
}, self);
|
|
657
1784
|
try {
|
|
658
1785
|
const { id } = selfIdAndSrc(self);
|
|
1786
|
+
const invokeCallKey = id !== "" ? runCtx.callKey?.(id, self) : void 0;
|
|
1787
|
+
const callKey = invokeCallKey === void 0 ? void 0 : `${invokeCallKey}.${attemptRequest.attempts?.length ?? 0}`;
|
|
1788
|
+
await runCtx.awaitJournal?.();
|
|
659
1789
|
const result = await runCtx.decide(runCtx.runId !== void 0 ? {
|
|
660
1790
|
...attemptRequest,
|
|
661
1791
|
runId: runCtx.runId
|
|
662
1792
|
} : attemptRequest, {
|
|
663
1793
|
...info,
|
|
664
1794
|
...runCtx.runId !== void 0 ? { runId: runCtx.runId } : {},
|
|
665
|
-
...info?.requestId === void 0 && id !== "" ? { requestId: id } : {}
|
|
1795
|
+
...info?.requestId === void 0 && id !== "" ? { requestId: id } : {},
|
|
1796
|
+
...info?.callKey === void 0 && callKey !== void 0 ? { callKey } : {}
|
|
666
1797
|
});
|
|
667
1798
|
const usage = getCallUsage(result);
|
|
1799
|
+
const finishReason = getCallFinishReason(result);
|
|
668
1800
|
if (usage) {
|
|
669
1801
|
const { src } = selfIdAndSrc(self);
|
|
670
1802
|
runCtx.recordUsage?.(usage, {
|
|
@@ -679,7 +1811,8 @@ function createCountingDecide(runCtx, self) {
|
|
|
679
1811
|
request: attemptRequest,
|
|
680
1812
|
output: result.event,
|
|
681
1813
|
raw: result,
|
|
682
|
-
...usage !== void 0 ? { usage } : {}
|
|
1814
|
+
...usage !== void 0 ? { usage } : {},
|
|
1815
|
+
...finishReason !== void 0 ? { finishReason } : {}
|
|
683
1816
|
}, self);
|
|
684
1817
|
return result;
|
|
685
1818
|
} catch (error) {
|
|
@@ -719,9 +1852,12 @@ function bindDecisionLogic(logic, runCtx) {
|
|
|
719
1852
|
schemas: runCtx.schemas,
|
|
720
1853
|
eventTypes: declaredEventTypes
|
|
721
1854
|
}) : [];
|
|
1855
|
+
const lowered = logic.request(input);
|
|
722
1856
|
const chosen = await resolveDecision({
|
|
723
|
-
...
|
|
1857
|
+
...lowered,
|
|
724
1858
|
id,
|
|
1859
|
+
name: lowered.name ?? id,
|
|
1860
|
+
input,
|
|
725
1861
|
events
|
|
726
1862
|
}, { decide: createCountingDecide(runCtx, self) }, {
|
|
727
1863
|
maxRetries: logic.maxRetries,
|
|
@@ -826,19 +1962,26 @@ function provideBindContext(machine, executors, options) {
|
|
|
826
1962
|
}) : void 0,
|
|
827
1963
|
consumeModelCall: () => {},
|
|
828
1964
|
recordUsage: (usage, source, self) => {
|
|
829
|
-
deliverUsageEvent(
|
|
1965
|
+
deliverUsageEvent({
|
|
1966
|
+
type: AGENT_USAGE_EVENT_TYPE,
|
|
1967
|
+
...source,
|
|
1968
|
+
usage
|
|
1969
|
+
}, () => self?._parent);
|
|
830
1970
|
},
|
|
831
1971
|
actorHolder: { actorRef: void 0 },
|
|
832
1972
|
schemas: getRegisteredAgentExecutionOptions(machine).schemas
|
|
833
1973
|
};
|
|
834
1974
|
}
|
|
835
1975
|
/**
|
|
836
|
-
* The single reserved-`@agent.usage`
|
|
837
|
-
* after a bound call settles with reported usage, send the event to
|
|
838
|
-
* actor `resolveActorRef` names — the run's root actor on the `runAgent` path,
|
|
1976
|
+
* The single reserved-`@agent.usage` DELIVERY seam, shared by both bind paths:
|
|
1977
|
+
* after a bound call settles with reported usage, send the prebuilt `event` to
|
|
1978
|
+
* the machine actor `resolveActorRef` names — the run's root actor on the `runAgent` path,
|
|
839
1979
|
* the settling request actor's `self._parent` (always the invoking machine
|
|
840
1980
|
* under a live `createActor` tree) on the `provideExecutors` path.
|
|
841
1981
|
*
|
|
1982
|
+
* Journaling is separate and unconditional (see `recordUsage`): a call's tokens
|
|
1983
|
+
* are recorded in the event log whether or not the machine reacts to them.
|
|
1984
|
+
*
|
|
842
1985
|
* Gating is identical on both: the target snapshot must be active, must declare
|
|
843
1986
|
* an `'@agent.usage'` transition — explicitly, or through a catch-all
|
|
844
1987
|
* `on: { '*' }` (see {@link declaresUsageTransition}) — and must be able to
|
|
@@ -851,16 +1994,11 @@ function provideBindContext(machine, executors, options) {
|
|
|
851
1994
|
* here, so an invoked child machine that was not itself passed through
|
|
852
1995
|
* `provideExecutors` reports nothing. @internal
|
|
853
1996
|
*/
|
|
854
|
-
function deliverUsageEvent(
|
|
1997
|
+
function deliverUsageEvent(event, resolveActorRef, onDropped) {
|
|
855
1998
|
const actorRef = resolveActorRef();
|
|
856
1999
|
if (!actorRef) return;
|
|
857
2000
|
const snapshot = actorRef.getSnapshot();
|
|
858
2001
|
if (snapshot?.status !== "active" || !declaresUsageTransition(snapshot)) return;
|
|
859
|
-
const event = {
|
|
860
|
-
type: AGENT_USAGE_EVENT_TYPE,
|
|
861
|
-
...source,
|
|
862
|
-
usage
|
|
863
|
-
};
|
|
864
2002
|
if (onDropped?.(event)) return;
|
|
865
2003
|
if (!snapshot.can(event)) return;
|
|
866
2004
|
actorRef.send(event);
|
|
@@ -993,47 +2131,47 @@ function rebindChildMachine(childMachine, runCtx, visited, ctxFor) {
|
|
|
993
2131
|
* lives in an adapter — use `createAiSdkExecutors` from '@statelyai/agent/ai-sdk'.
|
|
994
2132
|
*/
|
|
995
2133
|
async function runAgent(machine, options) {
|
|
996
|
-
|
|
2134
|
+
const store = options.store;
|
|
2135
|
+
if (store === void 0) return createAgentSession(machine, options).settled();
|
|
2136
|
+
const threadId = options.threadId;
|
|
2137
|
+
if (threadId === void 0 || threadId === "") throw new AgentError("missing-thread-id", "runAgent: `threadId` is required when `store` is given — it names the log thread to read and append to.");
|
|
2138
|
+
if (options.events !== void 0) {
|
|
2139
|
+
const storedThread = await store.read(threadId);
|
|
2140
|
+
if (storedThread.length !== options.events.length) throw new AgentEventLogConflictError(threadId, options.events.length, storedThread.length);
|
|
2141
|
+
assertThreadMatchesEvents(threadId, storedThread, options.events);
|
|
2142
|
+
return createAgentSession(machine, options).settled();
|
|
2143
|
+
}
|
|
2144
|
+
const stored = await store.read(threadId);
|
|
2145
|
+
return createAgentSession(machine, stored.length > 0 ? {
|
|
2146
|
+
...options,
|
|
2147
|
+
events: stored
|
|
2148
|
+
} : options).settled();
|
|
997
2149
|
}
|
|
998
2150
|
/**
|
|
999
|
-
*
|
|
1000
|
-
*
|
|
1001
|
-
*
|
|
1002
|
-
*
|
|
1003
|
-
* `createActor` would forfeit.
|
|
1004
|
-
*
|
|
1005
|
-
* ```ts
|
|
1006
|
-
* const session = createAgentActor(machine, { input, executors });
|
|
1007
|
-
* let result = await session.settled(); // first quiescence
|
|
1008
|
-
* while (result.status === "idle") {
|
|
1009
|
-
* session.actor.send(await nextUserEvent(result.snapshot));
|
|
1010
|
-
* result = await session.settled(); // next quiescence, same log
|
|
1011
|
-
* }
|
|
1012
|
-
* session.stop();
|
|
1013
|
-
* ```
|
|
1014
|
-
*
|
|
1015
|
-
* Accepts the same options as {@link runAgent} (including `snapshot`/`events`
|
|
1016
|
-
* resume). Not yet supported in session mode: `getRequests` re-interpretation
|
|
1017
|
-
* across cycles behaves per-cycle exactly as in `runAgent`.
|
|
2151
|
+
* Entry-by-entry check that the caller's `events` IS the store's thread. Equal
|
|
2152
|
+
* lengths prove nothing: a fork, a rolled-back replay, or a concurrent writer
|
|
2153
|
+
* that appended and truncated all produce a divergent log of the same size,
|
|
2154
|
+
* and appending this run's entries onto it would splice two lineages together.
|
|
1018
2155
|
*/
|
|
1019
|
-
function
|
|
1020
|
-
|
|
2156
|
+
function assertThreadMatchesEvents(threadId, stored, events) {
|
|
2157
|
+
for (let index = 0; index < stored.length; index++) {
|
|
2158
|
+
const storedEntry = stored[index];
|
|
2159
|
+
const givenEntry = events[index];
|
|
2160
|
+
const storedHash = storedEntry.verification?.stateHash;
|
|
2161
|
+
const givenHash = givenEntry.verification?.stateHash;
|
|
2162
|
+
if (!(storedEntry.id === givenEntry.id && storedEntry.index === givenEntry.index && canonicalEventJson(storedEntry.event) === canonicalEventJson(givenEntry.event) && (storedHash === void 0 || givenHash === void 0 || storedHash === givenHash))) throw new AgentError("event-log-conflict", `runAgent: the given \`events\` diverge from thread "${threadId}" at index ${index} (stored entry '${storedEntry.id}', given '${givenEntry.id}') — the log passed in is not this thread's log.`);
|
|
2163
|
+
}
|
|
1021
2164
|
}
|
|
1022
|
-
function createAgentSession(machine, options
|
|
2165
|
+
function createAgentSession(machine, options) {
|
|
1023
2166
|
const maxModelCalls = options.maxModelCalls ?? 100;
|
|
1024
2167
|
let modelCallCount = 0;
|
|
1025
2168
|
let budgetExceeded = false;
|
|
1026
2169
|
let warnedNonSerializable = false;
|
|
1027
|
-
let warnedHeuristicIdle = false;
|
|
1028
2170
|
const runId = `run_${nextRunAgentTraceId++}`;
|
|
1029
2171
|
let traceSeq = 0;
|
|
1030
2172
|
const resolvedInput = resolveMachineInput(machine, options.input);
|
|
1031
2173
|
const machineId = machine.config.id ?? machine.id ?? "(machine)";
|
|
1032
2174
|
const machineVersion = resolveMachineVersion(machine);
|
|
1033
|
-
const agentMeta = {
|
|
1034
|
-
machineId,
|
|
1035
|
-
version: machineVersion
|
|
1036
|
-
};
|
|
1037
2175
|
const onTrace = createTraceDispatch({
|
|
1038
2176
|
onTrace: (payload) => {
|
|
1039
2177
|
options.onTrace?.({
|
|
@@ -1058,25 +2196,109 @@ function createAgentSession(machine, options, lifecycle) {
|
|
|
1058
2196
|
}
|
|
1059
2197
|
modelCallCount += 1;
|
|
1060
2198
|
};
|
|
1061
|
-
const tokenTotals = {};
|
|
1062
2199
|
const cycleGate = { isResolved: () => false };
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
2200
|
+
const logEntries = [];
|
|
2201
|
+
const store = options.store;
|
|
2202
|
+
const threadId = options.threadId;
|
|
2203
|
+
if (store !== void 0 && (threadId === void 0 || threadId === "")) throw new AgentError("missing-thread-id", "runAgent: `threadId` is required when `store` is given — it names the log thread to read and append to.");
|
|
2204
|
+
let journalChain = Promise.resolve();
|
|
2205
|
+
let journalPending = 0;
|
|
2206
|
+
let journalError;
|
|
2207
|
+
const journalEntry = (entry) => {
|
|
2208
|
+
if (store === void 0) return;
|
|
2209
|
+
journalPending++;
|
|
2210
|
+
journalChain = journalChain.then(async () => {
|
|
2211
|
+
if (journalError !== void 0) return;
|
|
2212
|
+
await store.append({
|
|
2213
|
+
threadId,
|
|
2214
|
+
expectedIndex: entry.index,
|
|
2215
|
+
entries: [entry]
|
|
1074
2216
|
});
|
|
1075
|
-
|
|
2217
|
+
}).catch((error) => {
|
|
2218
|
+
journalError ??= error;
|
|
2219
|
+
failRunOnJournalError(error);
|
|
2220
|
+
}).finally(() => {
|
|
2221
|
+
journalPending--;
|
|
1076
2222
|
});
|
|
1077
2223
|
};
|
|
2224
|
+
/**
|
|
2225
|
+
* Every journal write ISSUED SO FAR, including the stragglers appended after
|
|
2226
|
+
* the run settled. Re-reads the chain tail until it stops moving, so a write
|
|
2227
|
+
* queued while an earlier one was in flight is covered too. Never rejects: a
|
|
2228
|
+
* failed write already settled the run with `cause: 'journal'`.
|
|
2229
|
+
*/
|
|
2230
|
+
const drainJournal = async () => {
|
|
2231
|
+
if (store === void 0) return;
|
|
2232
|
+
let tail = journalChain;
|
|
2233
|
+
for (;;) {
|
|
2234
|
+
await tail;
|
|
2235
|
+
if (journalPending === 0 && journalChain === tail) return;
|
|
2236
|
+
tail = journalChain;
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
/** The barrier: settle the pending chain, then surface any failure. */
|
|
2240
|
+
const awaitJournal = async () => {
|
|
2241
|
+
if (store === void 0) return;
|
|
2242
|
+
while (journalPending > 0) await journalChain;
|
|
2243
|
+
if (journalError !== void 0) throw journalError;
|
|
2244
|
+
};
|
|
2245
|
+
const failRunOnJournalError = (error) => {
|
|
2246
|
+
if (settled) return;
|
|
2247
|
+
settle({
|
|
2248
|
+
status: "error",
|
|
2249
|
+
cause: "journal",
|
|
2250
|
+
error,
|
|
2251
|
+
snapshot: actor.getSnapshot()
|
|
2252
|
+
});
|
|
2253
|
+
};
|
|
2254
|
+
let resumedLogLength = 0;
|
|
2255
|
+
let logExecutionId;
|
|
2256
|
+
const verificationEnabled = options.verification !== false;
|
|
2257
|
+
let lastRootSnapshot;
|
|
2258
|
+
let logMachine = machine;
|
|
2259
|
+
let loggingEnabled = true;
|
|
2260
|
+
const appendLogEntry = (event, snapshot) => {
|
|
2261
|
+
if (!loggingEnabled) return;
|
|
2262
|
+
const entry = createReplayEntry(logMachine, logEntries, event, {
|
|
2263
|
+
machineVersion,
|
|
2264
|
+
verification: verificationEnabled,
|
|
2265
|
+
...verificationEnabled && snapshot !== void 0 ? { snapshot } : {}
|
|
2266
|
+
});
|
|
2267
|
+
logEntries.push(entry);
|
|
2268
|
+
journalEntry(entry);
|
|
2269
|
+
options.onEvent?.(entry);
|
|
2270
|
+
return entry;
|
|
2271
|
+
};
|
|
2272
|
+
const usageEntrySnapshot = () => {
|
|
2273
|
+
const current = lastRootSnapshot;
|
|
2274
|
+
if (current && (current.status !== "active" || !declaresUsageTransition(current))) return current;
|
|
2275
|
+
};
|
|
2276
|
+
const recordUsage = (usage, source = {}) => {
|
|
2277
|
+
const event = {
|
|
2278
|
+
type: AGENT_USAGE_EVENT_TYPE,
|
|
2279
|
+
...source,
|
|
2280
|
+
usage
|
|
2281
|
+
};
|
|
2282
|
+
const journal = { event };
|
|
2283
|
+
pendingUsageJournal = journal;
|
|
2284
|
+
try {
|
|
2285
|
+
deliverUsageEvent(event, () => actorHolder.actorRef, (dropped) => {
|
|
2286
|
+
if (!cycleGate.isResolved()) return false;
|
|
2287
|
+
onTrace({
|
|
2288
|
+
type: "usage.dropped",
|
|
2289
|
+
event: dropped,
|
|
2290
|
+
reason: "settled"
|
|
2291
|
+
});
|
|
2292
|
+
return true;
|
|
2293
|
+
});
|
|
2294
|
+
} finally {
|
|
2295
|
+
pendingUsageJournal = void 0;
|
|
2296
|
+
}
|
|
2297
|
+
if (journal.entry === void 0) appendLogEntry(event, usageEntrySnapshot());
|
|
2298
|
+
};
|
|
2299
|
+
let pendingUsageJournal;
|
|
1078
2300
|
const runUsage = () => ({
|
|
1079
|
-
...
|
|
2301
|
+
...getUsageFromEvents(logEntries.slice(resumedLogLength)),
|
|
1080
2302
|
modelCalls: modelCallCount
|
|
1081
2303
|
});
|
|
1082
2304
|
const warnNonSerializableContext = (snapshot) => {
|
|
@@ -1092,18 +2314,23 @@ function createAgentSession(machine, options, lifecycle) {
|
|
|
1092
2314
|
console.warn(`runAgent: context holds value(s) that will not survive snapshot persist/resume (JSON round-trip): ${offending.join(", ")}. Persist only JSON-serializable context, or convert these before the run settles.`);
|
|
1093
2315
|
};
|
|
1094
2316
|
const provided = machine.provide({ actors: options.actors });
|
|
2317
|
+
logMachine = provided;
|
|
1095
2318
|
const effectiveSources = provided.sources.actors;
|
|
1096
|
-
|
|
1097
|
-
|
|
2319
|
+
const executors = {
|
|
2320
|
+
...getRegisteredAgentExecutionOptions(machine).models?.[DEFAULT_AGENT_EXECUTORS]?.(),
|
|
2321
|
+
...options.executors
|
|
2322
|
+
};
|
|
2323
|
+
assertBindable(provided, effectiveSources, executors);
|
|
1098
2324
|
const actorHolder = { actorRef: void 0 };
|
|
1099
2325
|
const runCtx = {
|
|
1100
|
-
generateText:
|
|
1101
|
-
streamText:
|
|
1102
|
-
decide:
|
|
2326
|
+
generateText: executors.generateText,
|
|
2327
|
+
streamText: executors.streamText,
|
|
2328
|
+
decide: executors.decide,
|
|
1103
2329
|
emitTrace: onTrace,
|
|
1104
2330
|
consumeModelCall,
|
|
1105
2331
|
recordUsage,
|
|
1106
2332
|
actorHolder,
|
|
2333
|
+
awaitJournal,
|
|
1107
2334
|
runId,
|
|
1108
2335
|
schemas: getRegisteredAgentExecutionOptions(machine).schemas
|
|
1109
2336
|
};
|
|
@@ -1135,103 +2362,178 @@ function createAgentSession(machine, options, lifecycle) {
|
|
|
1135
2362
|
}
|
|
1136
2363
|
}
|
|
1137
2364
|
const boundMachine = provided.provide({ actors: wrappedSources });
|
|
1138
|
-
const
|
|
1139
|
-
const
|
|
1140
|
-
const
|
|
2365
|
+
const isIdle = getMachineIdlePredicate(machine) ?? isAgentIdle;
|
|
2366
|
+
const isIntentionalIdle = (snapshot) => isIdle(snapshot) || collectPendingUserInputs(snapshot).length > 0;
|
|
2367
|
+
const resumeEvents = options.events;
|
|
1141
2368
|
let effectiveSnapshot = options.snapshot;
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
2369
|
+
/** Deferred until after the illegal-resume check, so a rejected resume emits no entries. */
|
|
2370
|
+
let seedInitEntry;
|
|
2371
|
+
const restoredPersistedSnapshot = () => {
|
|
2372
|
+
try {
|
|
2373
|
+
const restored = logMachine.restoreSnapshot(options.snapshot);
|
|
2374
|
+
return logMachine.getPersistedSnapshot(restored);
|
|
2375
|
+
} catch {
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
};
|
|
2379
|
+
/**
|
|
2380
|
+
* Appends the log's reserved first entry, trying each candidate `init` in
|
|
2381
|
+
* turn and, for each, an unverified entry if the hash cannot be computed.
|
|
2382
|
+
* A machine or snapshot that defeats every attempt leaves the run WITHOUT a
|
|
2383
|
+
* log rather than failing it: `runAgent` predates the log, and an exotic
|
|
2384
|
+
* machine that cannot be folded must still run.
|
|
2385
|
+
*/
|
|
2386
|
+
const appendInitEntry = (candidates, metadata) => {
|
|
2387
|
+
for (const init of candidates) for (const verification of verificationEnabled ? [true, false] : [false]) try {
|
|
2388
|
+
const entry = initEntry(logMachine, init, {
|
|
2389
|
+
machineVersion,
|
|
2390
|
+
verification,
|
|
2391
|
+
metadata
|
|
2392
|
+
});
|
|
2393
|
+
logEntries.push(entry);
|
|
2394
|
+
journalEntry(entry);
|
|
2395
|
+
options.onEvent?.(entry);
|
|
2396
|
+
return;
|
|
2397
|
+
} catch {}
|
|
2398
|
+
loggingEnabled = false;
|
|
2399
|
+
};
|
|
2400
|
+
/** Init forms for a snapshot resume, most faithful first. */
|
|
2401
|
+
const snapshotInitCandidates = () => {
|
|
2402
|
+
const restored = restoredPersistedSnapshot();
|
|
2403
|
+
return [...restored !== void 0 ? [{ snapshot: restored }] : [], { snapshot: options.snapshot }];
|
|
2404
|
+
};
|
|
2405
|
+
if (resumeEvents !== void 0 && resumeEvents.length > 0) {
|
|
2406
|
+
validateReplayEntries(resumeEvents);
|
|
2407
|
+
const tail = resumeEvents[resumeEvents.length - 1];
|
|
2408
|
+
const inheritedExecutionId = getLogExecutionId(resumeEvents);
|
|
2409
|
+
if (resumeEvents[0].machineId !== machineId) throw new AgentMachineVersionMismatchError(tail.id, tail.index, {
|
|
2410
|
+
machineId,
|
|
2411
|
+
machineVersion
|
|
2412
|
+
}, {
|
|
2413
|
+
machineId: resumeEvents[0].machineId,
|
|
2414
|
+
machineVersion: tail.machineVersion
|
|
2415
|
+
});
|
|
2416
|
+
if (tail.machineVersion === machineVersion) {
|
|
2417
|
+
logEntries.push(...resumeEvents);
|
|
2418
|
+
resumedLogLength = logEntries.length;
|
|
2419
|
+
logExecutionId = inheritedExecutionId;
|
|
2420
|
+
const cachedMeta = readAgentMeta(options.snapshot);
|
|
2421
|
+
const tailStateHash = tail.verification?.stateHash;
|
|
2422
|
+
if (!(options.snapshot !== void 0 && inheritedExecutionId !== void 0 && cachedMeta?.logId === inheritedExecutionId && cachedMeta.logIndex === resumeEvents.length && tailStateHash !== void 0 && hashResumeSnapshot(options.snapshot) === tailStateHash)) {
|
|
2423
|
+
const cachedIndex = cachedMeta?.logIndex;
|
|
2424
|
+
if (options.snapshot !== void 0 && typeof cachedIndex === "number" && cachedIndex >= 1 && cachedIndex <= resumeEvents.length && resumeEvents[cachedIndex - 1].verification?.stateHash !== void 0) {
|
|
2425
|
+
const expected = getSnapshotStateHash(replay(logMachine, resumeEvents.slice(0, cachedIndex), {
|
|
2426
|
+
machineVersion,
|
|
2427
|
+
verify: false
|
|
2428
|
+
}).persistedSnapshot);
|
|
2429
|
+
const actual = hashResumeSnapshot(options.snapshot);
|
|
2430
|
+
if (expected !== actual) throw new AgentSnapshotDivergedError(expected, actual, cachedIndex);
|
|
2431
|
+
}
|
|
2432
|
+
effectiveSnapshot = replay(logMachine, resumeEvents, {
|
|
2433
|
+
machineVersion,
|
|
2434
|
+
verify: true
|
|
2435
|
+
}).persistedSnapshot;
|
|
1154
2436
|
}
|
|
2437
|
+
} else {
|
|
2438
|
+
if (options.snapshot === void 0) throw new AgentMachineVersionMismatchError(tail.id, tail.index, {
|
|
2439
|
+
machineId,
|
|
2440
|
+
machineVersion
|
|
2441
|
+
}, {
|
|
2442
|
+
machineId: tail.machineId,
|
|
2443
|
+
machineVersion: tail.machineVersion
|
|
2444
|
+
});
|
|
2445
|
+
const bridgeMeta = readAgentMeta(options.snapshot);
|
|
2446
|
+
if (inheritedExecutionId === void 0 || bridgeMeta?.logId !== inheritedExecutionId) throw new AgentMachineVersionMismatchError(tail.id, tail.index, {
|
|
2447
|
+
machineId,
|
|
2448
|
+
machineVersion
|
|
2449
|
+
}, {
|
|
2450
|
+
machineId: tail.machineId,
|
|
2451
|
+
machineVersion: tail.machineVersion
|
|
2452
|
+
});
|
|
2453
|
+
const bridgeTailHash = tail.verification?.stateHash;
|
|
2454
|
+
const bridgeSnapshotHash = hashResumeSnapshot(options.snapshot);
|
|
2455
|
+
if (bridgeMeta.logIndex !== resumeEvents.length) throw new AgentSnapshotDivergedError(bridgeTailHash ?? "(unrecorded)", bridgeSnapshotHash, resumeEvents.length);
|
|
2456
|
+
if (bridgeTailHash !== void 0 && bridgeSnapshotHash !== bridgeTailHash) throw new AgentSnapshotDivergedError(bridgeTailHash, bridgeSnapshotHash, resumeEvents.length);
|
|
2457
|
+
logExecutionId = inheritedExecutionId;
|
|
2458
|
+
const migratedFrom = {
|
|
2459
|
+
machineVersion: tail.machineVersion,
|
|
2460
|
+
logIndex: resumeEvents.length
|
|
2461
|
+
};
|
|
2462
|
+
seedInitEntry = () => appendInitEntry(snapshotInitCandidates(), {
|
|
2463
|
+
...logExecutionId !== void 0 ? { executionId: logExecutionId } : {},
|
|
2464
|
+
migratedFrom
|
|
2465
|
+
});
|
|
1155
2466
|
}
|
|
2467
|
+
} else if (options.snapshot !== void 0) {
|
|
2468
|
+
logExecutionId = crypto.randomUUID();
|
|
2469
|
+
seedInitEntry = () => appendInitEntry(snapshotInitCandidates(), { executionId: logExecutionId });
|
|
2470
|
+
} else {
|
|
2471
|
+
logExecutionId = crypto.randomUUID();
|
|
2472
|
+
seedInitEntry = () => appendInitEntry([resolvedInput !== void 0 ? { input: resolvedInput } : {}], { executionId: logExecutionId });
|
|
1156
2473
|
}
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
const
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
}
|
|
1170
|
-
const priorMessages = getAgentMessages(effectiveSnapshot);
|
|
1171
|
-
const messages = typeof options.messages === "function" ? [...options.messages([...priorMessages])] : [...priorMessages, ...options.messages ?? []];
|
|
1172
|
-
const stampSettledSnapshot = (snapshot) => {
|
|
1173
|
-
if (!snapshot || typeof snapshot !== "object") return;
|
|
1174
|
-
snapshot.agentMeta = agentMeta;
|
|
1175
|
-
if (!options.getRequests && !options.messages && messages.length === 0) return;
|
|
1176
|
-
snapshot.messages = [...messages];
|
|
1177
|
-
};
|
|
1178
|
-
if (effectiveSnapshot !== void 0 && options.event !== void 0) {
|
|
1179
|
-
const acceptedTypes = getAcceptedEvents(createActor(boundMachine, { snapshot: effectiveSnapshot }).getSnapshot(), { schemas: runCtx.schemas }).map((descriptor) => descriptor.type);
|
|
1180
|
-
const eventType = options.event.type;
|
|
1181
|
-
if (!acceptedTypes.includes(eventType)) throw new AgentIllegalResumeEventError(eventType, acceptedTypes);
|
|
1182
|
-
}
|
|
1183
|
-
const replayEvents = [...resumeEvents ?? []];
|
|
1184
|
-
validateReplayEntries(replayEvents, {
|
|
1185
|
-
machineId,
|
|
1186
|
-
machineVersion
|
|
1187
|
-
}, "runAgent events");
|
|
1188
|
-
const replayEventIds = new Set(replayEvents.map((entry) => entry.id));
|
|
1189
|
-
const hasCompleteReplayHistory = replayEvents[0]?.event.type === "@agent.init" || effectiveSnapshot === void 0 && replayEvents.length === 0;
|
|
1190
|
-
const appendReplayEvent = (event) => {
|
|
1191
|
-
const entry = createReplayEntry(machine, replayEvents, event, {
|
|
1192
|
-
machineVersion,
|
|
1193
|
-
verification: hasCompleteReplayHistory
|
|
1194
|
-
});
|
|
1195
|
-
if (replayEventIds.has(entry.id)) throw new Error(`runAgent generated duplicate event id '${entry.id}'.`);
|
|
1196
|
-
replayEventIds.add(entry.id);
|
|
1197
|
-
replayEvents.push(entry);
|
|
1198
|
-
options.onEvent?.(entry);
|
|
1199
|
-
return entry;
|
|
2474
|
+
const resumedHistory = [...logEntries];
|
|
2475
|
+
const siteCallCounts = /* @__PURE__ */ new Map();
|
|
2476
|
+
const mintedCallKeys = /* @__PURE__ */ new WeakMap();
|
|
2477
|
+
runCtx.callKey = (siteId, self) => {
|
|
2478
|
+
if (logExecutionId === void 0) return;
|
|
2479
|
+
const memoized = self === void 0 ? void 0 : mintedCallKeys.get(self);
|
|
2480
|
+
if (memoized !== void 0) return memoized;
|
|
2481
|
+
const startedHere = siteCallCounts.get(siteId) ?? 0;
|
|
2482
|
+
siteCallCounts.set(siteId, startedHere + 1);
|
|
2483
|
+
const key = `${logExecutionId}:${siteId}#${agentCallOccurrence(resumedHistory, siteId) + startedHere}`;
|
|
2484
|
+
if (self !== void 0) mintedCallKeys.set(self, key);
|
|
2485
|
+
return key;
|
|
1200
2486
|
};
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
replayEvents.push(entry);
|
|
1205
|
-
options.onEvent?.(entry);
|
|
1206
|
-
}
|
|
2487
|
+
const resumeEventToSend = options.event;
|
|
2488
|
+
let ignoredEvent;
|
|
2489
|
+
seedInitEntry?.();
|
|
1207
2490
|
let settled = false;
|
|
1208
|
-
let
|
|
2491
|
+
let journalAtSettle;
|
|
1209
2492
|
cycleGate.isResolved = () => settled;
|
|
1210
2493
|
let lastResult;
|
|
1211
2494
|
const waiters = [];
|
|
1212
2495
|
let idleTimer;
|
|
1213
2496
|
let actor;
|
|
1214
|
-
let
|
|
2497
|
+
let actorStarted = false;
|
|
2498
|
+
let deliveringResumeEvent = resumeEventToSend !== void 0;
|
|
1215
2499
|
const settle = (outcome) => {
|
|
1216
2500
|
if (settled) return;
|
|
1217
2501
|
settled = true;
|
|
2502
|
+
let persistedSnapshot;
|
|
2503
|
+
let persistenceError;
|
|
2504
|
+
if (actorStarted && outcome.status !== "done") try {
|
|
2505
|
+
persistedSnapshot = actor.getPersistedSnapshot();
|
|
2506
|
+
} catch (error) {
|
|
2507
|
+
persistenceError = error;
|
|
2508
|
+
}
|
|
2509
|
+
const logIndexAtSettle = logEntries.length;
|
|
2510
|
+
journalAtSettle = journalChain;
|
|
2511
|
+
const persist = () => {
|
|
2512
|
+
if (persistenceError !== void 0) throw persistenceError;
|
|
2513
|
+
persistedSnapshot ??= actor.getPersistedSnapshot();
|
|
2514
|
+
persistedSnapshot.agentMeta = {
|
|
2515
|
+
machineId,
|
|
2516
|
+
version: machineVersion,
|
|
2517
|
+
...logExecutionId !== void 0 ? { logId: logExecutionId } : {},
|
|
2518
|
+
logIndex: logIndexAtSettle
|
|
2519
|
+
};
|
|
2520
|
+
return persistedSnapshot;
|
|
2521
|
+
};
|
|
1218
2522
|
const result = {
|
|
1219
2523
|
...outcome,
|
|
1220
|
-
events: [...
|
|
1221
|
-
|
|
2524
|
+
events: [...logEntries],
|
|
2525
|
+
persist,
|
|
2526
|
+
drain: drainJournal,
|
|
2527
|
+
usage: runUsage(),
|
|
2528
|
+
...ignoredEvent !== void 0 ? { ignored: ignoredEvent } : {}
|
|
1222
2529
|
};
|
|
1223
2530
|
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
1224
|
-
stampSettledSnapshot(result.snapshot);
|
|
1225
|
-
if ("persistedSnapshot" in result) stampSettledSnapshot(result.persistedSnapshot);
|
|
1226
2531
|
onTrace({
|
|
1227
2532
|
type: "run.end",
|
|
1228
2533
|
...outcome
|
|
1229
2534
|
});
|
|
1230
|
-
if (
|
|
1231
|
-
|
|
1232
|
-
if (options.signal) options.signal.removeEventListener("abort", onAbort);
|
|
1233
|
-
actor.stop();
|
|
1234
|
-
}
|
|
2535
|
+
if (options.signal) options.signal.removeEventListener("abort", onAbort);
|
|
2536
|
+
actor.stop();
|
|
1235
2537
|
lastResult = result;
|
|
1236
2538
|
for (const resolve of waiters.splice(0)) resolve(result);
|
|
1237
2539
|
};
|
|
@@ -1245,89 +2547,21 @@ function createAgentSession(machine, options, lifecycle) {
|
|
|
1245
2547
|
};
|
|
1246
2548
|
const settleIdle = (current) => {
|
|
1247
2549
|
warnNonSerializableContext(current);
|
|
1248
|
-
const pendingUserInputs = userInputIsPlaceholder ? collectPendingUserInputs(current) : [];
|
|
1249
|
-
settle({
|
|
1250
|
-
status: "idle",
|
|
1251
|
-
snapshot: current,
|
|
1252
|
-
|
|
1253
|
-
...pendingUserInputs.length > 0 ? { pendingUserInputs } : {}
|
|
1254
|
-
});
|
|
1255
|
-
};
|
|
1256
|
-
let interpreting = false;
|
|
1257
|
-
let interpretSeq = 0;
|
|
1258
|
-
const appendToLog = (...items) => {
|
|
1259
|
-
messages.push(...items);
|
|
1260
|
-
if (options.onMessage) {
|
|
1261
|
-
const info = {
|
|
1262
|
-
runId,
|
|
1263
|
-
machineId,
|
|
1264
|
-
machineVersion
|
|
1265
|
-
};
|
|
1266
|
-
for (const item of items) options.onMessage(item, info);
|
|
1267
|
-
}
|
|
1268
|
-
};
|
|
1269
|
-
const runErrorCause = (error) => budgetExceeded ? "max-model-calls" : wrapsDecisionExhausted(error) ? "decision-exhausted" : "machine";
|
|
1270
|
-
const settleInterpretError = (error) => {
|
|
1271
|
-
settle({
|
|
1272
|
-
status: "error",
|
|
1273
|
-
cause: runErrorCause(error),
|
|
1274
|
-
error,
|
|
1275
|
-
snapshot: actor.getSnapshot()
|
|
1276
|
-
});
|
|
1277
|
-
};
|
|
1278
|
-
const passDeps = {
|
|
1279
|
-
getSnapshot: () => actor.getSnapshot(),
|
|
1280
|
-
send: (event) => actor.send(event),
|
|
1281
|
-
isSettled: () => settled,
|
|
1282
|
-
messages,
|
|
1283
|
-
appendToLog,
|
|
1284
|
-
generateText: runCtx.generateText,
|
|
1285
|
-
decide: runCtx.decide ? createCountingDecide(runCtx, void 0) : void 0,
|
|
1286
|
-
consumeModelCall,
|
|
1287
|
-
recordUsage,
|
|
1288
|
-
nextRequestId: () => `interpret_${++interpretSeq}`,
|
|
1289
|
-
onTrace,
|
|
1290
|
-
schemas: runCtx.schemas,
|
|
1291
|
-
signal: options.signal
|
|
1292
|
-
};
|
|
1293
|
-
const maybeInterpret = (snapshot) => {
|
|
1294
|
-
if (!options.getRequests || settled) return false;
|
|
1295
|
-
if (interpreting) return true;
|
|
1296
|
-
let requested;
|
|
1297
|
-
try {
|
|
1298
|
-
requested = options.getRequests(snapshot, { messages });
|
|
1299
|
-
} catch (error) {
|
|
1300
|
-
settleInterpretError(error);
|
|
1301
|
-
return true;
|
|
1302
|
-
}
|
|
1303
|
-
const requests = (Array.isArray(requested) ? requested : requested ? [requested] : []).filter((stateRequest) => Boolean(stateRequest));
|
|
1304
|
-
if (requests.length === 0) return false;
|
|
1305
|
-
interpreting = true;
|
|
1306
|
-
runStateRequestPass(requests, passDeps).then(({ sentAny }) => {
|
|
1307
|
-
if (settled || sentAny) return;
|
|
1308
|
-
const current = actor.getSnapshot();
|
|
1309
|
-
if (isIdleSnapshot(current, { ignoreUserInputChildren: userInputIsPlaceholder })) settleIdle(current);
|
|
1310
|
-
}).catch((error) => settleInterpretError(error)).finally(() => {
|
|
1311
|
-
interpreting = false;
|
|
1312
|
-
if (!settled) scheduleIdleCheck();
|
|
2550
|
+
const pendingUserInputs = userInputIsPlaceholder ? collectPendingUserInputs(current) : [];
|
|
2551
|
+
settle({
|
|
2552
|
+
status: "idle",
|
|
2553
|
+
snapshot: current,
|
|
2554
|
+
...pendingUserInputs.length > 0 ? { pendingUserInputs } : {}
|
|
1313
2555
|
});
|
|
1314
|
-
return true;
|
|
1315
2556
|
};
|
|
2557
|
+
const runErrorCause = (error) => budgetExceeded ? "max-model-calls" : wrapsDecisionExhausted(error) ? "decision-exhausted" : "machine";
|
|
1316
2558
|
const scheduleIdleCheck = () => {
|
|
1317
2559
|
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
1318
2560
|
idleTimer = setTimeout(() => {
|
|
1319
2561
|
idleTimer = void 0;
|
|
1320
2562
|
if (settled) return;
|
|
1321
2563
|
const current = actor.getSnapshot();
|
|
1322
|
-
if (isIdleSnapshot(current, { ignoreUserInputChildren: userInputIsPlaceholder }))
|
|
1323
|
-
if (!maybeInterpret(current)) {
|
|
1324
|
-
if (!declaredIdlePredicate && current.status === "active" && !warnedHeuristicIdle && process.env.NODE_ENV !== "production") {
|
|
1325
|
-
warnedHeuristicIdle = true;
|
|
1326
|
-
console.warn("[@statelyai/agent] runAgent settled idle via the timing heuristic (no idle predicate declared). This is best-effort; for deterministic idle detection, declare setupAgent({ isIdle }) or pass runAgent(machine, { isIdle }), e.g. (s) => s.hasTag('waiting').");
|
|
1327
|
-
}
|
|
1328
|
-
settleIdle(current);
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
2564
|
+
if (isIntentionalIdle(current) && isIdleSnapshot(current, { ignoreUserInputChildren: userInputIsPlaceholder })) settleIdle(current);
|
|
1331
2565
|
}, 0);
|
|
1332
2566
|
};
|
|
1333
2567
|
actor = createActor(boundMachine, {
|
|
@@ -1337,20 +2571,16 @@ function createAgentSession(machine, options, lifecycle) {
|
|
|
1337
2571
|
if (typeof options.inspect === "function") options.inspect(event);
|
|
1338
2572
|
else options.inspect?.next?.(event);
|
|
1339
2573
|
if (event.type !== "@xstate.transition" || event.actorRef !== actor.ref) return;
|
|
1340
|
-
if (settled)
|
|
1341
|
-
if (finalized) return;
|
|
1342
|
-
settled = false;
|
|
1343
|
-
lastResult = void 0;
|
|
1344
|
-
}
|
|
2574
|
+
if (settled) return;
|
|
1345
2575
|
const snapshot = event.snapshot;
|
|
1346
|
-
|
|
1347
|
-
if (event.event
|
|
1348
|
-
|
|
2576
|
+
lastRootSnapshot = snapshot;
|
|
2577
|
+
if (resumeEventToSend !== void 0 && event.event === resumeEventToSend && event.microsteps.length === 0 && event.actions.length === 0 && event.sent.length === 0) ignoredEvent = resumeEventToSend;
|
|
2578
|
+
if (pendingUsageJournal !== void 0 && event.event === pendingUsageJournal.event) pendingUsageJournal.entry = appendLogEntry(event.event, snapshot);
|
|
2579
|
+
else if (event.event.type !== "@xstate.init" && (event.sourceRef !== event.actorRef || event.event.type === "xstate.timer")) appendLogEntry(event.event, snapshot);
|
|
1349
2580
|
onTrace({
|
|
1350
2581
|
type: "machine.transition",
|
|
1351
2582
|
snapshot,
|
|
1352
|
-
event: event.event
|
|
1353
|
-
...eventId !== void 0 ? { eventId } : {}
|
|
2583
|
+
event: event.event
|
|
1354
2584
|
});
|
|
1355
2585
|
if (snapshot.status === "done") {
|
|
1356
2586
|
settle({
|
|
@@ -1378,13 +2608,12 @@ function createAgentSession(machine, options, lifecycle) {
|
|
|
1378
2608
|
});
|
|
1379
2609
|
return;
|
|
1380
2610
|
}
|
|
1381
|
-
if (!deliveringResumeEvent &&
|
|
2611
|
+
if (!deliveringResumeEvent && isIntentionalIdle(snapshot) && isIdleSnapshot(snapshot, { ignoreUserInputChildren: userInputIsPlaceholder })) {
|
|
1382
2612
|
queueMicrotask(() => {
|
|
1383
2613
|
if (settled) return;
|
|
1384
2614
|
const current = actor.getSnapshot();
|
|
1385
|
-
if (
|
|
1386
|
-
|
|
1387
|
-
} else scheduleIdleCheck();
|
|
2615
|
+
if (isIntentionalIdle(current) && isIdleSnapshot(current, { ignoreUserInputChildren: userInputIsPlaceholder })) settleIdle(current);
|
|
2616
|
+
else scheduleIdleCheck();
|
|
1388
2617
|
});
|
|
1389
2618
|
return;
|
|
1390
2619
|
}
|
|
@@ -1400,19 +2629,13 @@ function createAgentSession(machine, options, lifecycle) {
|
|
|
1400
2629
|
});
|
|
1401
2630
|
});
|
|
1402
2631
|
for (const [type, handler] of Object.entries(options.on ?? {})) if (typeof handler === "function") actor.on(type, handler);
|
|
1403
|
-
const sessionApi = {
|
|
1404
|
-
|
|
1405
|
-
get events() {
|
|
1406
|
-
return replayEvents;
|
|
1407
|
-
},
|
|
1408
|
-
usage: runUsage,
|
|
1409
|
-
settled: () => settled && lastResult !== void 0 ? Promise.resolve(lastResult) : new Promise((resolve) => {
|
|
2632
|
+
const sessionApi = { settled: async () => {
|
|
2633
|
+
const result = settled && lastResult !== void 0 ? lastResult : await new Promise((resolve) => {
|
|
1410
2634
|
waiters.push(resolve);
|
|
1411
|
-
})
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
};
|
|
2635
|
+
});
|
|
2636
|
+
await journalAtSettle;
|
|
2637
|
+
return result;
|
|
2638
|
+
} };
|
|
1416
2639
|
if (options.signal) {
|
|
1417
2640
|
if (options.signal.aborted) {
|
|
1418
2641
|
settle({
|
|
@@ -1429,47 +2652,38 @@ function createAgentSession(machine, options, lifecycle) {
|
|
|
1429
2652
|
type: "run.start",
|
|
1430
2653
|
...resolvedInput !== void 0 ? { input: resolvedInput } : {},
|
|
1431
2654
|
...effectiveSnapshot !== void 0 ? { snapshot: effectiveSnapshot } : {},
|
|
1432
|
-
...
|
|
2655
|
+
...resumeEventToSend !== void 0 ? { event: resumeEventToSend } : {}
|
|
1433
2656
|
});
|
|
2657
|
+
actorStarted = true;
|
|
1434
2658
|
actor.start();
|
|
1435
|
-
if (
|
|
2659
|
+
if (!settled) {
|
|
2660
|
+
const startedSnapshot = actor.getSnapshot();
|
|
2661
|
+
lastRootSnapshot ??= startedSnapshot;
|
|
2662
|
+
if (startedSnapshot.status === "done") settle({
|
|
2663
|
+
status: "done",
|
|
2664
|
+
output: startedSnapshot.output,
|
|
2665
|
+
snapshot: startedSnapshot
|
|
2666
|
+
});
|
|
2667
|
+
else if (startedSnapshot.status === "error") settle({
|
|
2668
|
+
status: "error",
|
|
2669
|
+
cause: runErrorCause(startedSnapshot.error),
|
|
2670
|
+
error: startedSnapshot.error,
|
|
2671
|
+
snapshot: startedSnapshot
|
|
2672
|
+
});
|
|
2673
|
+
else if (startedSnapshot.status === "stopped") settle({
|
|
2674
|
+
status: "error",
|
|
2675
|
+
cause: "stopped",
|
|
2676
|
+
error: /* @__PURE__ */ new Error("Actor stopped externally."),
|
|
2677
|
+
snapshot: startedSnapshot
|
|
2678
|
+
});
|
|
2679
|
+
}
|
|
2680
|
+
if (resumeEventToSend && !settled) {
|
|
1436
2681
|
deliveringResumeEvent = false;
|
|
1437
|
-
actor.send(
|
|
2682
|
+
actor.send(resumeEventToSend);
|
|
1438
2683
|
}
|
|
1439
2684
|
return sessionApi;
|
|
1440
2685
|
}
|
|
1441
2686
|
/**
|
|
1442
|
-
* Runs an agent machine to a **final state**, for run-to-done flows where an
|
|
1443
|
-
* idle pause is unexpected. Wraps {@link runAgent}:
|
|
1444
|
-
*
|
|
1445
|
-
* - `done` → resolves with the done result: `result.output` (the machine's
|
|
1446
|
-
* `OutputFrom`) plus metadata — `result.snapshot`, the replayable
|
|
1447
|
-
* `result.events`, and the aggregated `result.usage` — the same shape
|
|
1448
|
-
* `generateText` users expect (`text` + call metadata).
|
|
1449
|
-
* - `idle` → throws {@link AgentIdleError} carrying the idle snapshot and the
|
|
1450
|
-
* event types that could resume it.
|
|
1451
|
-
* - `error` → throws `result.error` when it is an `Error`; otherwise wraps it
|
|
1452
|
-
* in an `Error` whose `.cause` is the {@link RunAgentErrorCause} and whose
|
|
1453
|
-
* `.error` is the raw thrown value.
|
|
1454
|
-
*
|
|
1455
|
-
* Use {@link runAgent} directly when idle is an expected outcome you handle
|
|
1456
|
-
* (human-in-the-loop, resumable flows); use `generateResult` when the
|
|
1457
|
-
* machine is meant to run straight through to a final state.
|
|
1458
|
-
*/
|
|
1459
|
-
async function generateResult(machine, options) {
|
|
1460
|
-
const result = await runAgent(machine, options);
|
|
1461
|
-
if (result.status === "done") return result;
|
|
1462
|
-
if (result.status === "idle") {
|
|
1463
|
-
const acceptedTypes = getAcceptedEvents(result.snapshot, { schemas: getRegisteredAgentExecutionOptions(machine).schemas }).map((descriptor) => descriptor.type);
|
|
1464
|
-
throw new AgentIdleError(result.snapshot, acceptedTypes);
|
|
1465
|
-
}
|
|
1466
|
-
if (result.error instanceof Error) throw result.error;
|
|
1467
|
-
const wrapped = /* @__PURE__ */ new Error(`generateResult: run failed with cause '${result.cause}'.`);
|
|
1468
|
-
wrapped.cause = result.cause;
|
|
1469
|
-
wrapped.error = result.error;
|
|
1470
|
-
throw wrapped;
|
|
1471
|
-
}
|
|
1472
|
-
/**
|
|
1473
2687
|
* Wraps a `(snapshot, actorRef) => void` handler into a function usable as
|
|
1474
2688
|
* {@link RunAgentOptions.inspect}: it filters the raw inspection stream to
|
|
1475
2689
|
* `@xstate.transition` events and hands the handler the typed
|
|
@@ -1531,6 +2745,24 @@ function isIdleSnapshot(snapshot, { ignoreUserInputChildren }) {
|
|
|
1531
2745
|
})) return false;
|
|
1532
2746
|
return !getNextTransitions(snapshot).some((transitionDef) => transitionDef.eventType === "" || transitionDef.eventType.startsWith("xstate.after"));
|
|
1533
2747
|
}
|
|
2748
|
+
/**
|
|
2749
|
+
* Default machine-owned idle signal used by {@link runAgent}: an active
|
|
2750
|
+
* snapshot that accepts an external event anywhere in its active hierarchy,
|
|
2751
|
+
* or declares interaction metadata on an active state.
|
|
2752
|
+
*
|
|
2753
|
+
* Compose this in `setupAgent({ isIdle })` when the application has additional
|
|
2754
|
+
* wait states: `isIdle: (snapshot) => isAgentIdle(snapshot) || snapshot.hasTag('waiting')`.
|
|
2755
|
+
* Pending work and active invoked children are checked separately by the
|
|
2756
|
+
* runner, so this function only answers whether the state is an intentional
|
|
2757
|
+
* external wait.
|
|
2758
|
+
*/
|
|
2759
|
+
function isAgentIdle(snapshot) {
|
|
2760
|
+
if (snapshot.status !== "active") return false;
|
|
2761
|
+
return snapshot.nodes.some((node) => {
|
|
2762
|
+
const meta = node.meta;
|
|
2763
|
+
return typeof meta === "object" && meta !== null && "interaction" in meta;
|
|
2764
|
+
}) || getAcceptedEvents(snapshot).length > 0;
|
|
2765
|
+
}
|
|
1534
2766
|
function collectPendingUserInputs(snapshot) {
|
|
1535
2767
|
const pending = [];
|
|
1536
2768
|
for (const [id, child] of Object.entries(snapshot.children ?? {})) {
|
|
@@ -1644,6 +2876,124 @@ function createAgentRun(machine, options) {
|
|
|
1644
2876
|
result
|
|
1645
2877
|
};
|
|
1646
2878
|
}
|
|
2879
|
+
function requestIdentity(request) {
|
|
2880
|
+
const candidate = request;
|
|
2881
|
+
const id = candidate.id ?? candidate.name ?? "agent.request";
|
|
2882
|
+
return {
|
|
2883
|
+
name: candidate.kind === "text" ? candidate.input?.name ?? id : candidate.name ?? id,
|
|
2884
|
+
id
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* Streams one {@link runAgent} call as agent-level events. The machine remains
|
|
2889
|
+
* the single artifact; this function only projects its requests, chunks,
|
|
2890
|
+
* transitions, emitted events, and terminal result into an async iterable.
|
|
2891
|
+
*/
|
|
2892
|
+
async function* runAgentStream(machine, options) {
|
|
2893
|
+
const run = createAgentRun(machine, options);
|
|
2894
|
+
for await (const event of run.events) switch (event.type) {
|
|
2895
|
+
case "request.start":
|
|
2896
|
+
yield {
|
|
2897
|
+
kind: "request",
|
|
2898
|
+
phase: "start",
|
|
2899
|
+
...requestIdentity(event.request),
|
|
2900
|
+
request: event.request
|
|
2901
|
+
};
|
|
2902
|
+
break;
|
|
2903
|
+
case "request.end":
|
|
2904
|
+
yield {
|
|
2905
|
+
kind: "request",
|
|
2906
|
+
phase: "end",
|
|
2907
|
+
...requestIdentity(event.request),
|
|
2908
|
+
request: event.request,
|
|
2909
|
+
output: event.output,
|
|
2910
|
+
raw: event.raw
|
|
2911
|
+
};
|
|
2912
|
+
break;
|
|
2913
|
+
case "request.error":
|
|
2914
|
+
yield {
|
|
2915
|
+
kind: "request",
|
|
2916
|
+
phase: "error",
|
|
2917
|
+
...requestIdentity(event.request),
|
|
2918
|
+
request: event.request,
|
|
2919
|
+
error: event.error
|
|
2920
|
+
};
|
|
2921
|
+
break;
|
|
2922
|
+
case "stream.chunk":
|
|
2923
|
+
yield {
|
|
2924
|
+
kind: "chunk",
|
|
2925
|
+
...requestIdentity(event.request),
|
|
2926
|
+
delta: event.chunk
|
|
2927
|
+
};
|
|
2928
|
+
break;
|
|
2929
|
+
case "machine.transition":
|
|
2930
|
+
yield {
|
|
2931
|
+
kind: "transition",
|
|
2932
|
+
value: event.snapshot.value,
|
|
2933
|
+
event: event.event
|
|
2934
|
+
};
|
|
2935
|
+
break;
|
|
2936
|
+
case "emit":
|
|
2937
|
+
yield {
|
|
2938
|
+
kind: "emit",
|
|
2939
|
+
event: event.event
|
|
2940
|
+
};
|
|
2941
|
+
break;
|
|
2942
|
+
}
|
|
2943
|
+
const result = await run.result;
|
|
2944
|
+
if (result.status === "done") yield {
|
|
2945
|
+
kind: "done",
|
|
2946
|
+
result
|
|
2947
|
+
};
|
|
2948
|
+
else if (result.status === "idle") yield {
|
|
2949
|
+
kind: "idle",
|
|
2950
|
+
result
|
|
2951
|
+
};
|
|
2952
|
+
else yield {
|
|
2953
|
+
kind: "error",
|
|
2954
|
+
result
|
|
2955
|
+
};
|
|
2956
|
+
}
|
|
2957
|
+
//#endregion
|
|
2958
|
+
//#region src/run-loop.ts
|
|
2959
|
+
function addUsage(total, next) {
|
|
2960
|
+
const merged = { ...total };
|
|
2961
|
+
for (const [key, value] of Object.entries(next)) if (typeof value === "number") merged[key] = (merged[key] ?? 0) + value;
|
|
2962
|
+
return merged;
|
|
2963
|
+
}
|
|
2964
|
+
/** Drives one machine artifact across as many idle/resume turns as the host
|
|
2965
|
+
* supplies. XState snapshots remain the continuation; this helper owns no
|
|
2966
|
+
* persistence, retry, tool-loop, or storage policy. */
|
|
2967
|
+
async function runAgentLoop(machine, options) {
|
|
2968
|
+
const { onIdle, persist, maxTurns = 100, events: initialEvents, ...runOptions } = options;
|
|
2969
|
+
if (!Number.isInteger(maxTurns) || maxTurns < 0) throw new Error("runAgentLoop: maxTurns must be a non-negative integer.");
|
|
2970
|
+
let usage = { modelCalls: 0 };
|
|
2971
|
+
let snapshot;
|
|
2972
|
+
let event;
|
|
2973
|
+
const stored = options.store !== void 0;
|
|
2974
|
+
let events = initialEvents;
|
|
2975
|
+
for (let turn = 0;; turn++) {
|
|
2976
|
+
const result = await runAgent(machine, {
|
|
2977
|
+
...runOptions,
|
|
2978
|
+
...events ? { events } : {},
|
|
2979
|
+
...snapshot ? {
|
|
2980
|
+
snapshot,
|
|
2981
|
+
event
|
|
2982
|
+
} : {}
|
|
2983
|
+
});
|
|
2984
|
+
events = stored ? void 0 : result.events;
|
|
2985
|
+
usage = addUsage(usage, result.usage);
|
|
2986
|
+
const cumulative = {
|
|
2987
|
+
...result,
|
|
2988
|
+
usage
|
|
2989
|
+
};
|
|
2990
|
+
if (result.status !== "idle" || !onIdle || turn >= maxTurns) return cumulative;
|
|
2991
|
+
snapshot = result.persist();
|
|
2992
|
+
await persist?.(snapshot, result);
|
|
2993
|
+
event = await onIdle(result) ?? void 0;
|
|
2994
|
+
if (!event) return cumulative;
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
1647
2997
|
//#endregion
|
|
1648
2998
|
//#region src/provide-executors.ts
|
|
1649
2999
|
/**
|
|
@@ -1788,98 +3138,6 @@ function buildStateIndex(rootConfig) {
|
|
|
1788
3138
|
walk(rootConfig.states, "");
|
|
1789
3139
|
return index;
|
|
1790
3140
|
}
|
|
1791
|
-
function childrenOf(index, parentPath) {
|
|
1792
|
-
const out = [];
|
|
1793
|
-
for (const node of index.values()) if (node.parentPath === parentPath) out.push(node);
|
|
1794
|
-
return out;
|
|
1795
|
-
}
|
|
1796
|
-
function collectTransitionTargets(value, fromNode, index, out) {
|
|
1797
|
-
if (value === void 0 || value === null) return;
|
|
1798
|
-
if (Array.isArray(value)) {
|
|
1799
|
-
for (const item of value) collectTransitionTargets(item, fromNode, index, out);
|
|
1800
|
-
return;
|
|
1801
|
-
}
|
|
1802
|
-
if (typeof value === "function") {
|
|
1803
|
-
out.opaque = true;
|
|
1804
|
-
return;
|
|
1805
|
-
}
|
|
1806
|
-
if (typeof value === "string") {
|
|
1807
|
-
resolveTargetString(value, fromNode, index, out);
|
|
1808
|
-
return;
|
|
1809
|
-
}
|
|
1810
|
-
if (typeof value === "object") {
|
|
1811
|
-
const { target, to } = value;
|
|
1812
|
-
if (target !== void 0) {
|
|
1813
|
-
collectTransitionTargets(target, fromNode, index, out);
|
|
1814
|
-
return;
|
|
1815
|
-
}
|
|
1816
|
-
if (to !== void 0) {
|
|
1817
|
-
collectTransitionTargets(to, fromNode, index, out);
|
|
1818
|
-
return;
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
function resolveTargetString(target, fromNode, index, out) {
|
|
1823
|
-
if (target.startsWith("#")) {
|
|
1824
|
-
out.opaque = true;
|
|
1825
|
-
return;
|
|
1826
|
-
}
|
|
1827
|
-
const resolved = target.startsWith(".") ? `${fromNode.path}.${target.slice(1)}` : fromNode.parentPath ? `${fromNode.parentPath}.${target}` : target;
|
|
1828
|
-
if (index.has(resolved)) out.targets.push(resolved);
|
|
1829
|
-
else out.opaque = true;
|
|
1830
|
-
}
|
|
1831
|
-
function outgoingTargets(node, index, staticTargets) {
|
|
1832
|
-
const out = {
|
|
1833
|
-
targets: [],
|
|
1834
|
-
opaque: false
|
|
1835
|
-
};
|
|
1836
|
-
const { config } = node;
|
|
1837
|
-
if (staticTargets) {
|
|
1838
|
-
for (const target of staticTargets[node.path] ?? []) resolveTargetString(target, node, index, out);
|
|
1839
|
-
return out;
|
|
1840
|
-
}
|
|
1841
|
-
for (const value of Object.values(config.on ?? {})) collectTransitionTargets(value, node, index, out);
|
|
1842
|
-
collectTransitionTargets(config.always, node, index, out);
|
|
1843
|
-
collectTransitionTargets(config.choice, node, index, out);
|
|
1844
|
-
for (const value of Object.values(config.after ?? {})) collectTransitionTargets(value, node, index, out);
|
|
1845
|
-
collectTransitionTargets(config.onDone, node, index, out);
|
|
1846
|
-
for (const invoke of node.invokes) {
|
|
1847
|
-
collectTransitionTargets(invoke.onDone, node, index, out);
|
|
1848
|
-
collectTransitionTargets(invoke.onError, node, index, out);
|
|
1849
|
-
}
|
|
1850
|
-
return out;
|
|
1851
|
-
}
|
|
1852
|
-
function computeReachable(rootConfig, index, staticTargets) {
|
|
1853
|
-
const reachable = /* @__PURE__ */ new Set();
|
|
1854
|
-
const queue = [];
|
|
1855
|
-
const markAncestors = (path) => {
|
|
1856
|
-
let parent = index.get(path)?.parentPath ?? "";
|
|
1857
|
-
while (parent) {
|
|
1858
|
-
reachable.add(parent);
|
|
1859
|
-
parent = index.get(parent)?.parentPath ?? "";
|
|
1860
|
-
}
|
|
1861
|
-
};
|
|
1862
|
-
const enter = (path) => {
|
|
1863
|
-
if (reachable.has(path)) return;
|
|
1864
|
-
const node = index.get(path);
|
|
1865
|
-
if (!node) return;
|
|
1866
|
-
reachable.add(path);
|
|
1867
|
-
queue.push(path);
|
|
1868
|
-
markAncestors(path);
|
|
1869
|
-
if (node.isParallel) for (const child of childrenOf(index, path)) enter(child.path);
|
|
1870
|
-
else if (node.isCompound && node.config.initial) enter(`${path}.${node.config.initial}`);
|
|
1871
|
-
};
|
|
1872
|
-
if (rootConfig.type === "parallel") for (const child of childrenOf(index, "")) enter(child.path);
|
|
1873
|
-
else if (rootConfig.initial) enter(rootConfig.initial);
|
|
1874
|
-
while (queue.length > 0) {
|
|
1875
|
-
const node = index.get(queue.shift());
|
|
1876
|
-
if (!node) continue;
|
|
1877
|
-
const { targets, opaque } = outgoingTargets(node, index, staticTargets);
|
|
1878
|
-
for (const target of targets) enter(target);
|
|
1879
|
-
if (opaque) for (const sibling of childrenOf(index, node.parentPath)) enter(sibling.path);
|
|
1880
|
-
}
|
|
1881
|
-
return reachable;
|
|
1882
|
-
}
|
|
1883
3141
|
function ancestorChain(node, index) {
|
|
1884
3142
|
const chain = [];
|
|
1885
3143
|
let parent = node.parentPath;
|
|
@@ -1901,24 +3159,6 @@ function isDecisionInvoke(src, actors) {
|
|
|
1901
3159
|
function isAgentLogicNeedingBinding(src) {
|
|
1902
3160
|
return (isTextLogic(src) || isDecisionLogic(src)) && !executorBoundLogics.has(src);
|
|
1903
3161
|
}
|
|
1904
|
-
function readJsonSchema(schema) {
|
|
1905
|
-
if (!schema) return;
|
|
1906
|
-
try {
|
|
1907
|
-
return getJsonSchemaSync(schema);
|
|
1908
|
-
} catch {
|
|
1909
|
-
return;
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1912
|
-
function checkUnreachableStates(ctx) {
|
|
1913
|
-
const out = [];
|
|
1914
|
-
for (const node of ctx.index.values()) if (!ctx.reachable.has(node.path)) out.push({
|
|
1915
|
-
code: "unreachable-state",
|
|
1916
|
-
severity: "error",
|
|
1917
|
-
path: node.path,
|
|
1918
|
-
message: `State '${node.path}' is unreachable: no transition, always, onDone, or onError target (from any reachable state) leads to it. Remove it or add a transition.`
|
|
1919
|
-
});
|
|
1920
|
-
return out;
|
|
1921
|
-
}
|
|
1922
3162
|
function checkDecideWithoutEvents(ctx) {
|
|
1923
3163
|
const out = [];
|
|
1924
3164
|
for (const node of ctx.index.values()) for (const invoke of node.invokes) {
|
|
@@ -1937,17 +3177,6 @@ function checkDecideWithoutEvents(ctx) {
|
|
|
1937
3177
|
}
|
|
1938
3178
|
return out;
|
|
1939
3179
|
}
|
|
1940
|
-
function checkUnserializableContext(ctx) {
|
|
1941
|
-
const contextSchema = ctx.schemas?.context;
|
|
1942
|
-
if (!contextSchema) return [];
|
|
1943
|
-
if (readJsonSchema(contextSchema)) return [];
|
|
1944
|
-
return [{
|
|
1945
|
-
code: "unserializable-context",
|
|
1946
|
-
severity: "warning",
|
|
1947
|
-
path: "context",
|
|
1948
|
-
message: "The context schema does not expose a JSON schema (e.g. a `z.custom` without a `jsonSchema` extension, such as a messages array), so its fields cannot be statically checked for JSON persist/resume round-tripping. This is expected for message transcripts; verify any other custom-typed context is JSON-serializable."
|
|
1949
|
-
}];
|
|
1950
|
-
}
|
|
1951
3180
|
function checkDirectObjectSrc(ctx) {
|
|
1952
3181
|
const out = [];
|
|
1953
3182
|
for (const node of ctx.index.values()) for (const invoke of node.invokes) {
|
|
@@ -1963,80 +3192,48 @@ function checkDirectObjectSrc(ctx) {
|
|
|
1963
3192
|
}
|
|
1964
3193
|
return out;
|
|
1965
3194
|
}
|
|
1966
|
-
function
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
const required = outputJson?.required;
|
|
1970
|
-
if (!(outputJson?.type === "object" && !!properties && Object.keys(properties).length > 0 || Array.isArray(required) && required.length > 0)) return [];
|
|
1971
|
-
if (ctx.config.output !== void 0) return [];
|
|
1972
|
-
const out = [];
|
|
1973
|
-
for (const node of ctx.index.values()) {
|
|
1974
|
-
if (node.parentPath !== "" || !node.isFinal) continue;
|
|
1975
|
-
if (node.config.output === void 0) out.push({
|
|
1976
|
-
code: "final-without-output",
|
|
1977
|
-
severity: "error",
|
|
1978
|
-
path: node.path,
|
|
1979
|
-
message: `The machine declares an output schema, but top-level final state '${node.path}' has no 'output'. Its snapshot output will be undefined and fail the schema. Add an 'output' to this final state (or a root 'output').`
|
|
1980
|
-
});
|
|
1981
|
-
}
|
|
1982
|
-
return out;
|
|
1983
|
-
}
|
|
1984
|
-
function outputFnReadsEvent(fn) {
|
|
1985
|
-
return /\bevent\s*(?:\.|\?\.|\[)/.test(fn.toString());
|
|
1986
|
-
}
|
|
1987
|
-
function checkFinalOutputReadsEvent(ctx) {
|
|
1988
|
-
const out = [];
|
|
1989
|
-
for (const node of ctx.index.values()) {
|
|
1990
|
-
if (node.parentPath !== "" || !node.isFinal) continue;
|
|
1991
|
-
const output = node.config.output;
|
|
1992
|
-
if (typeof output !== "function") continue;
|
|
1993
|
-
if (!outputFnReadsEvent(output)) continue;
|
|
1994
|
-
out.push({
|
|
1995
|
-
code: "final-output-reads-event",
|
|
1996
|
-
severity: "warning",
|
|
1997
|
-
path: node.path,
|
|
1998
|
-
message: `Final state '${node.path}' has an 'output' function that reads 'event'. Final-state 'output' functions are evaluated more than once with different events (the entering event, then the machine-done computation) under current xstate behavior, so 'event' is unreliable here. Read 'context' only; capture what you need from the entering event into context in the transition that targets this state. (This guard can relax if xstate guarantees a stable entering event across evaluations.)`
|
|
1999
|
-
});
|
|
2000
|
-
}
|
|
2001
|
-
return out;
|
|
2002
|
-
}
|
|
2003
|
-
function isBuiltinOrWildcardEvent(eventType) {
|
|
2004
|
-
return eventType === "*" || eventType.includes("*") || eventType.startsWith("xstate.") || eventType.startsWith("done.") || eventType.startsWith("error.");
|
|
3195
|
+
function handlesActorError(config) {
|
|
3196
|
+
if (config["onError"] !== void 0) return true;
|
|
3197
|
+
return Object.keys(config.on ?? {}).some((type) => type === "*" || type.startsWith("xstate.error"));
|
|
2005
3198
|
}
|
|
2006
|
-
function
|
|
2007
|
-
const declared = ctx.schemas?.events;
|
|
2008
|
-
const declaredTypes = new Set(Object.keys(declared ?? {}));
|
|
2009
|
-
if ([...declaredTypes].every((type) => type.startsWith("@agent."))) return [];
|
|
3199
|
+
function checkInvokeWithoutOnError(ctx) {
|
|
2010
3200
|
const out = [];
|
|
2011
|
-
for (const node of ctx.index.values()) for (const
|
|
2012
|
-
if (
|
|
3201
|
+
for (const node of ctx.index.values()) for (const invoke of node.invokes) {
|
|
3202
|
+
if (invoke.onError !== void 0 || handlesActorError(node.config)) continue;
|
|
3203
|
+
if (handlesActorError(ctx.config) || ancestorChain(node, ctx.index).some((ancestor) => handlesActorError(ancestor.config))) continue;
|
|
3204
|
+
const srcName = typeof invoke.src === "string" ? invoke.src : "(inline logic)";
|
|
2013
3205
|
out.push({
|
|
2014
|
-
code: "
|
|
3206
|
+
code: "invoke-without-on-error",
|
|
2015
3207
|
severity: "warning",
|
|
2016
3208
|
path: node.path,
|
|
2017
|
-
message: `State '${node.path}'
|
|
3209
|
+
message: `State '${node.path}' invokes '${srcName}' with no 'onError' transition, and no ancestor handles an actor error. A rejected request or actor puts the machine in an error state with no modeled recovery. Add 'onError' (a 'failed' final state, or a bounded retry).`
|
|
2018
3210
|
});
|
|
2019
3211
|
}
|
|
2020
3212
|
return out;
|
|
2021
3213
|
}
|
|
2022
|
-
function
|
|
2023
|
-
|
|
2024
|
-
return [
|
|
2025
|
-
|
|
3214
|
+
function checkUnhandledAgentMessages(ctx) {
|
|
3215
|
+
const handlesMessages = (config) => config.on?.["agent.messages"] !== void 0 || config.on?.["*"] !== void 0;
|
|
3216
|
+
if (handlesMessages(ctx.config) || [...ctx.index.values()].some((node) => handlesMessages(node.config))) return [];
|
|
3217
|
+
let contextJsonSchema;
|
|
3218
|
+
try {
|
|
3219
|
+
contextJsonSchema = getJsonSchemaSync(ctx.schemas?.context);
|
|
3220
|
+
} catch {}
|
|
3221
|
+
if (!contextJsonSchema?.properties?.messages) return [];
|
|
3222
|
+
return [...ctx.index.values()].some((node) => node.invokes.some((invoke) => {
|
|
3223
|
+
if (typeof invoke.src !== "string") return false;
|
|
3224
|
+
return invoke.src === "agent.generateText" || invoke.src === "agent.streamText" || isTextLogic(ctx.actors[invoke.src]);
|
|
3225
|
+
})) ? [{
|
|
3226
|
+
code: "unhandled-agent-messages",
|
|
2026
3227
|
severity: "warning",
|
|
2027
3228
|
path: "(root)",
|
|
2028
|
-
message:
|
|
2029
|
-
}];
|
|
3229
|
+
message: `Text requests may return framework messages, but the machine does not handle '${AGENT_MESSAGES_EVENT_TYPE}'. Add on: { '${AGENT_MESSAGES_EVENT_TYPE}': appendMessages() } when transcript retention is intended, or disable this warning when messages are intentionally ignored.`
|
|
3230
|
+
}] : [];
|
|
2030
3231
|
}
|
|
2031
3232
|
const LINT_CHECKS = [
|
|
2032
|
-
checkUnreachableStates,
|
|
2033
3233
|
checkDecideWithoutEvents,
|
|
2034
|
-
checkUnserializableContext,
|
|
2035
3234
|
checkDirectObjectSrc,
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
checkUndeclaredEvents,
|
|
2039
|
-
checkMissingFinal
|
|
3235
|
+
checkInvokeWithoutOnError,
|
|
3236
|
+
checkUnhandledAgentMessages
|
|
2040
3237
|
];
|
|
2041
3238
|
/**
|
|
2042
3239
|
* Runs static structural checks over a built agent machine and returns the
|
|
@@ -2046,8 +3243,9 @@ const LINT_CHECKS = [
|
|
|
2046
3243
|
* plus the schemas/actor sources the library already retains per machine.
|
|
2047
3244
|
*
|
|
2048
3245
|
* No model calls, no API keys — a coding agent that emits an agent machine can
|
|
2049
|
-
* call this to catch
|
|
2050
|
-
*
|
|
3246
|
+
* call this to catch agent-specific mistakes such as undeliverable decisions,
|
|
3247
|
+
* un-rebindable request sources, and an unhandled transcript event. General
|
|
3248
|
+
* state-machine lint belongs to XState tooling.
|
|
2051
3249
|
*
|
|
2052
3250
|
* Pass `{ throw: true }` for the one-liner form used in tests and generation
|
|
2053
3251
|
* loops: it returns silently when the machine is clean and throws
|
|
@@ -2070,12 +3268,10 @@ const LINT_CHECKS = [
|
|
|
2070
3268
|
function lintAgentMachine(machine, options = {}) {
|
|
2071
3269
|
const config = machine.config ?? {};
|
|
2072
3270
|
const index = buildStateIndex(config);
|
|
2073
|
-
const reachable = computeReachable(config, index, getMachineStaticTransitionTargets(machine));
|
|
2074
3271
|
const registered = getRegisteredAgentExecutionOptions(machine);
|
|
2075
3272
|
const ctx = {
|
|
2076
3273
|
config,
|
|
2077
3274
|
index,
|
|
2078
|
-
reachable,
|
|
2079
3275
|
schemas: registered.schemas,
|
|
2080
3276
|
actors: registered.actors ?? machine.sources?.actors ?? {}
|
|
2081
3277
|
};
|
|
@@ -2124,13 +3320,10 @@ function assertAgentMachine(machine, options = {}) {
|
|
|
2124
3320
|
}
|
|
2125
3321
|
function pendingInvokes(step) {
|
|
2126
3322
|
const out = [];
|
|
2127
|
-
for (const
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
src: metadata.src
|
|
2132
|
-
});
|
|
2133
|
-
}
|
|
3323
|
+
for (const metadata of getPendingInvokes(step.snapshot)) if (typeof metadata.src === "string") out.push({
|
|
3324
|
+
id: metadata.id,
|
|
3325
|
+
src: metadata.src
|
|
3326
|
+
});
|
|
2134
3327
|
return out;
|
|
2135
3328
|
}
|
|
2136
3329
|
function takeFromQueue(channel, src) {
|
|
@@ -2184,6 +3377,7 @@ async function simulateAgent(machine, options) {
|
|
|
2184
3377
|
text: mapValues(options.script.text ?? {}, (arr) => [...arr]),
|
|
2185
3378
|
decisions: mapValues(options.script.decisions ?? {}, (arr) => [...arr]),
|
|
2186
3379
|
invokes: mapValues(options.script.invokes ?? {}, (arr) => [...arr]),
|
|
3380
|
+
errors: mapValues(options.script.errors ?? {}, (arr) => [...arr]),
|
|
2187
3381
|
events: [...options.script.events ?? []]
|
|
2188
3382
|
};
|
|
2189
3383
|
if (options.script.userInput?.length) script.invokes[USER_INPUT_SRC] = [...options.script.userInput, ...script.invokes[USER_INPUT_SRC] ?? []];
|
|
@@ -2201,9 +3395,28 @@ async function simulateAgent(machine, options) {
|
|
|
2201
3395
|
const invokeMeta = findInvokeMetadata(step, request.id);
|
|
2202
3396
|
const src = invokeMeta?.src ?? request.id;
|
|
2203
3397
|
const decisionSrc = src in (script.decisions ?? {}) ? src : request.id in (script.decisions ?? {}) ? request.id : src;
|
|
3398
|
+
const errorSrc = queueKey(script.errors, src, request.id);
|
|
3399
|
+
const failure = takeFromQueue(script.errors, errorSrc);
|
|
3400
|
+
if (failure.found) {
|
|
3401
|
+
step = rejectScriptedRequest(machine, step, request.id, failure.value, {
|
|
3402
|
+
kind: "decision",
|
|
3403
|
+
src: errorSrc,
|
|
3404
|
+
trail
|
|
3405
|
+
});
|
|
3406
|
+
continue;
|
|
3407
|
+
}
|
|
2204
3408
|
step = await applyScriptedDecision(machine, step, request, decisionSrc, invokeMeta?.logic, script, trail);
|
|
2205
3409
|
continue;
|
|
2206
3410
|
}
|
|
3411
|
+
const failure = takeFromQueue(script.errors, request.src);
|
|
3412
|
+
if (failure.found) {
|
|
3413
|
+
step = rejectScriptedRequest(machine, step, request, failure.value, {
|
|
3414
|
+
kind: "text",
|
|
3415
|
+
src: request.src,
|
|
3416
|
+
trail
|
|
3417
|
+
});
|
|
3418
|
+
continue;
|
|
3419
|
+
}
|
|
2207
3420
|
const taken = takeFromQueue(script.text, request.src);
|
|
2208
3421
|
if (!taken.found) throw scriptDryError("text", request.src, request.id);
|
|
2209
3422
|
step = resolveAgentStep(machine, step, request, taken.value);
|
|
@@ -2212,13 +3425,23 @@ async function simulateAgent(machine, options) {
|
|
|
2212
3425
|
resolvedRequest: {
|
|
2213
3426
|
kind: "text",
|
|
2214
3427
|
src: request.src,
|
|
2215
|
-
id: request.id
|
|
3428
|
+
id: request.id,
|
|
3429
|
+
outcome: "output"
|
|
2216
3430
|
}
|
|
2217
3431
|
});
|
|
2218
3432
|
continue;
|
|
2219
3433
|
}
|
|
2220
3434
|
const [invoke] = pendingInvokes(step);
|
|
2221
3435
|
if (invoke) {
|
|
3436
|
+
const failure = takeFromQueue(script.errors, invoke.src);
|
|
3437
|
+
if (failure.found) {
|
|
3438
|
+
step = rejectScriptedRequest(machine, step, invoke.id, failure.value, {
|
|
3439
|
+
kind: "userInput",
|
|
3440
|
+
src: invoke.src,
|
|
3441
|
+
trail
|
|
3442
|
+
});
|
|
3443
|
+
continue;
|
|
3444
|
+
}
|
|
2222
3445
|
const taken = takeFromQueue(script.invokes, invoke.src);
|
|
2223
3446
|
if (!taken.found) throw scriptDryError("userInput", invoke.src, invoke.id);
|
|
2224
3447
|
step = resolveAgentStep(machine, step, invoke.id, taken.value);
|
|
@@ -2227,7 +3450,8 @@ async function simulateAgent(machine, options) {
|
|
|
2227
3450
|
resolvedRequest: {
|
|
2228
3451
|
kind: "userInput",
|
|
2229
3452
|
src: invoke.src,
|
|
2230
|
-
id: invoke.id
|
|
3453
|
+
id: invoke.id,
|
|
3454
|
+
outcome: "output"
|
|
2231
3455
|
}
|
|
2232
3456
|
});
|
|
2233
3457
|
continue;
|
|
@@ -2256,6 +3480,29 @@ async function simulateAgent(machine, options) {
|
|
|
2256
3480
|
trail
|
|
2257
3481
|
};
|
|
2258
3482
|
}
|
|
3483
|
+
function queueKey(channel, src, id) {
|
|
3484
|
+
return channel && !(src in channel) && id in channel ? id : src;
|
|
3485
|
+
}
|
|
3486
|
+
function rejectScriptedRequest(machine, step, request, error, entry) {
|
|
3487
|
+
const id = typeof request === "string" ? request : request.id;
|
|
3488
|
+
const next = rejectAgentStep(machine, step, request, error);
|
|
3489
|
+
if (next.snapshot.status === "error") throw unhandledScriptedError(error, entry.kind, entry.src, id);
|
|
3490
|
+
entry.trail.push({
|
|
3491
|
+
state: next.snapshot.value,
|
|
3492
|
+
resolvedRequest: {
|
|
3493
|
+
kind: entry.kind,
|
|
3494
|
+
src: entry.src,
|
|
3495
|
+
id,
|
|
3496
|
+
outcome: "error",
|
|
3497
|
+
error
|
|
3498
|
+
}
|
|
3499
|
+
});
|
|
3500
|
+
return next;
|
|
3501
|
+
}
|
|
3502
|
+
function unhandledScriptedError(error, kind, src, id) {
|
|
3503
|
+
if (error instanceof Error) return error;
|
|
3504
|
+
return new Error(`simulateAgent: the scripted error for the ${kind} request '${src}' (id '${id}') reached no onError transition, so the machine errored. Add an onError to that invoke, or drop the errors['${src}'] entry.`, { cause: error });
|
|
3505
|
+
}
|
|
2259
3506
|
async function applyScriptedDecision(machine, step, request, decisionSrc, invokeLogic, script, trail) {
|
|
2260
3507
|
const dequeued = [];
|
|
2261
3508
|
const decide = async () => {
|
|
@@ -2276,12 +3523,7 @@ async function applyScriptedDecision(machine, step, request, decisionSrc, invoke
|
|
|
2276
3523
|
});
|
|
2277
3524
|
} catch (error) {
|
|
2278
3525
|
if (!(error instanceof AgentDecisionExhaustedError)) throw error;
|
|
2279
|
-
const next =
|
|
2280
|
-
type: "xstate.error.actor",
|
|
2281
|
-
actorId: request.id,
|
|
2282
|
-
...sessionIdOf(step.snapshot, request.id),
|
|
2283
|
-
error
|
|
2284
|
-
});
|
|
3526
|
+
const next = rejectAgentStep(machine, step, request.id, error);
|
|
2285
3527
|
if (next.snapshot.status === "error") throw error;
|
|
2286
3528
|
trail.push({
|
|
2287
3529
|
state: next.snapshot.value,
|
|
@@ -2299,9 +3541,8 @@ async function applyScriptedDecision(machine, step, request, decisionSrc, invoke
|
|
|
2299
3541
|
return next;
|
|
2300
3542
|
}
|
|
2301
3543
|
function findInvokeMetadata(step, id) {
|
|
2302
|
-
for (const
|
|
2303
|
-
|
|
2304
|
-
if (metadata?.id !== id) continue;
|
|
3544
|
+
for (const metadata of getPendingInvokes(step.snapshot)) {
|
|
3545
|
+
if (metadata.id !== id) continue;
|
|
2305
3546
|
return {
|
|
2306
3547
|
...typeof metadata.src === "string" ? { src: metadata.src } : {},
|
|
2307
3548
|
logic: metadata.logic ?? (typeof metadata.src === "object" ? metadata.src : void 0)
|
|
@@ -2312,10 +3553,6 @@ function resolveRegisteredDecisionLogic(machine, src) {
|
|
|
2312
3553
|
const candidate = getRegisteredAgentExecutionOptions(machine).actors?.[src] ?? machine.sources?.actors?.[src];
|
|
2313
3554
|
return isDecisionLogic(candidate) ? candidate : void 0;
|
|
2314
3555
|
}
|
|
2315
|
-
function sessionIdOf(snapshot, id) {
|
|
2316
|
-
const child = snapshot.children[id];
|
|
2317
|
-
return typeof child?.sessionId === "string" ? { sessionId: child.sessionId } : {};
|
|
2318
|
-
}
|
|
2319
3556
|
function mapValues(obj, fn) {
|
|
2320
3557
|
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, fn(value)]));
|
|
2321
3558
|
}
|
|
@@ -2325,12 +3562,25 @@ function scriptDryError(kind, src, id, request) {
|
|
|
2325
3562
|
return /* @__PURE__ */ new Error(`simulateAgent: script ran dry on a pending ${kind} request for src '${src}' (id '${id}'). Add an entry to the script's \`${key}\` queue.${events}`);
|
|
2326
3563
|
}
|
|
2327
3564
|
const MAX_ADVANCE_STEPS = 1e3;
|
|
3565
|
+
function invokeFork(id, src, outputs) {
|
|
3566
|
+
return src in outputs ? {
|
|
3567
|
+
id,
|
|
3568
|
+
src,
|
|
3569
|
+
hasOutput: true,
|
|
3570
|
+
output: outputs[src]
|
|
3571
|
+
} : {
|
|
3572
|
+
id,
|
|
3573
|
+
src,
|
|
3574
|
+
hasOutput: false
|
|
3575
|
+
};
|
|
3576
|
+
}
|
|
2328
3577
|
async function explore(machine, options, stopWhen) {
|
|
2329
3578
|
const maxDepth = options.maxDepth ?? 8;
|
|
2330
3579
|
const maxPaths = options.maxPaths ?? 200;
|
|
2331
3580
|
const textScript = options.text ?? {};
|
|
2332
3581
|
const invokeOutputs = { ...options.invokes ?? {} };
|
|
2333
3582
|
if ("userInput" in options) invokeOutputs[USER_INPUT_SRC] = options.userInput;
|
|
3583
|
+
const errorScript = options.errors ?? {};
|
|
2334
3584
|
const reachedStates = /* @__PURE__ */ new Set();
|
|
2335
3585
|
const reachedValues = [];
|
|
2336
3586
|
const terminals = [];
|
|
@@ -2359,6 +3609,10 @@ async function explore(machine, options, stopWhen) {
|
|
|
2359
3609
|
if (current.done) return { step: current };
|
|
2360
3610
|
const request = current.requests[0];
|
|
2361
3611
|
if (request && request.kind === "text") {
|
|
3612
|
+
if (request.src in errorScript) return {
|
|
3613
|
+
step: current,
|
|
3614
|
+
fork: invokeFork(request.id, request.src, textScript)
|
|
3615
|
+
};
|
|
2362
3616
|
if (!(request.src in textScript)) return {
|
|
2363
3617
|
step: current,
|
|
2364
3618
|
blockedSrc: request.src
|
|
@@ -2367,9 +3621,25 @@ async function explore(machine, options, stopWhen) {
|
|
|
2367
3621
|
recordState(current.snapshot);
|
|
2368
3622
|
continue;
|
|
2369
3623
|
}
|
|
2370
|
-
if (request && request.kind === "decision")
|
|
3624
|
+
if (request && request.kind === "decision") {
|
|
3625
|
+
const src = findInvokeMetadata(current, request.id)?.src;
|
|
3626
|
+
const errorSrc = src !== void 0 && src in errorScript ? src : request.id in errorScript ? request.id : void 0;
|
|
3627
|
+
return errorSrc !== void 0 ? {
|
|
3628
|
+
step: current,
|
|
3629
|
+
fork: {
|
|
3630
|
+
id: request.id,
|
|
3631
|
+
src: errorSrc,
|
|
3632
|
+
hasOutput: false,
|
|
3633
|
+
isDecision: true
|
|
3634
|
+
}
|
|
3635
|
+
} : { step: current };
|
|
3636
|
+
}
|
|
2371
3637
|
const [invoke] = pendingInvokes(current);
|
|
2372
3638
|
if (invoke) {
|
|
3639
|
+
if (invoke.src in errorScript) return {
|
|
3640
|
+
step: current,
|
|
3641
|
+
fork: invokeFork(invoke.id, invoke.src, invokeOutputs)
|
|
3642
|
+
};
|
|
2373
3643
|
if (!(invoke.src in invokeOutputs)) return {
|
|
2374
3644
|
step: current,
|
|
2375
3645
|
blockedSrc: invoke.src
|
|
@@ -2382,17 +3652,43 @@ async function explore(machine, options, stopWhen) {
|
|
|
2382
3652
|
}
|
|
2383
3653
|
return { step: current };
|
|
2384
3654
|
};
|
|
2385
|
-
const visit = async (step, path,
|
|
3655
|
+
const visit = async (step, path, startDepth) => {
|
|
3656
|
+
let depth = startDepth;
|
|
2386
3657
|
if (witness !== void 0) return;
|
|
2387
3658
|
if (pathsExplored >= maxPaths) {
|
|
2388
3659
|
hitPathCap = true;
|
|
2389
3660
|
return;
|
|
2390
3661
|
}
|
|
2391
|
-
const { step: settled, blockedSrc, hit } = advance(step);
|
|
3662
|
+
const { step: settled, blockedSrc, hit, fork } = advance(step);
|
|
2392
3663
|
if (hit) {
|
|
2393
3664
|
witness = path;
|
|
2394
3665
|
return;
|
|
2395
3666
|
}
|
|
3667
|
+
if (fork) {
|
|
3668
|
+
if (depth >= maxDepth) {
|
|
3669
|
+
pathsExplored++;
|
|
3670
|
+
terminals.push({
|
|
3671
|
+
status: "max-depth",
|
|
3672
|
+
path,
|
|
3673
|
+
state: settled.snapshot.value
|
|
3674
|
+
});
|
|
3675
|
+
unexplored.push(`max-depth: stopped at path [${path.map((e) => e.type).join(", ")}]`);
|
|
3676
|
+
return;
|
|
3677
|
+
}
|
|
3678
|
+
const failed = rejectAgentStep(machine, settled, fork.id, errorScript[fork.src]);
|
|
3679
|
+
recordState(failed.snapshot);
|
|
3680
|
+
await visit(failed, [...path, { type: `xstate.error.actor.${fork.id}` }], depth + 1);
|
|
3681
|
+
if (!fork.isDecision) {
|
|
3682
|
+
if (fork.hasOutput) {
|
|
3683
|
+
const resolved = resolveAgentStep(machine, settled, fork.id, fork.output);
|
|
3684
|
+
recordState(resolved.snapshot);
|
|
3685
|
+
await visit(resolved, path, depth + 1);
|
|
3686
|
+
}
|
|
3687
|
+
return;
|
|
3688
|
+
}
|
|
3689
|
+
depth += 1;
|
|
3690
|
+
if (witness !== void 0) return;
|
|
3691
|
+
}
|
|
2396
3692
|
if (blockedSrc) {
|
|
2397
3693
|
pathsExplored++;
|
|
2398
3694
|
terminals.push({
|
|
@@ -2413,6 +3709,16 @@ async function explore(machine, options, stopWhen) {
|
|
|
2413
3709
|
});
|
|
2414
3710
|
return;
|
|
2415
3711
|
}
|
|
3712
|
+
if (settled.snapshot.status === "error") {
|
|
3713
|
+
pathsExplored++;
|
|
3714
|
+
terminals.push({
|
|
3715
|
+
status: "error",
|
|
3716
|
+
path,
|
|
3717
|
+
state: settled.snapshot.value,
|
|
3718
|
+
error: settled.snapshot.error
|
|
3719
|
+
});
|
|
3720
|
+
return;
|
|
3721
|
+
}
|
|
2416
3722
|
const request = settled.requests[0];
|
|
2417
3723
|
const branchEvents = request?.kind === "decision" ? request.events.map((descriptor) => ({ type: descriptor.type })) : getAcceptedEvents(settled.snapshot).map((descriptor) => ({ type: descriptor.type }));
|
|
2418
3724
|
if (branchEvents.length === 0) {
|
|
@@ -2469,7 +3775,9 @@ async function explore(machine, options, stopWhen) {
|
|
|
2469
3775
|
* an idle wait it forks per externally-accepted event. Text requests resolve
|
|
2470
3776
|
* from `text`, other invokes from `invokes` (or `userInput` for
|
|
2471
3777
|
* `agent.userInput`) — all by-src canned-output maps, and a missing src halts
|
|
2472
|
-
* that branch with a `needs-output` terminal rather than throwing.
|
|
3778
|
+
* that branch with a `needs-output` terminal rather than throwing. A src in
|
|
3779
|
+
* `errors` forks an extra branch where that invoke is rejected, so states
|
|
3780
|
+
* behind an `onError` are explored too.
|
|
2473
3781
|
*
|
|
2474
3782
|
* Combinatorics are bounded by `maxDepth` (default 8) and `maxPaths` (default
|
|
2475
3783
|
* 200, reported via `hitPathCap`).
|
|
@@ -2483,6 +3791,24 @@ async function explore(machine, options, stopWhen) {
|
|
|
2483
3791
|
async function explorePaths(machine, options = {}) {
|
|
2484
3792
|
return (await explore(machine, options)).report;
|
|
2485
3793
|
}
|
|
3794
|
+
/** Thrown when a string reachability target is not a state in the machine. */
|
|
3795
|
+
var AgentUnknownStateError = class extends AgentError {
|
|
3796
|
+
target;
|
|
3797
|
+
constructor(machineId, target) {
|
|
3798
|
+
super("unknown-state", `canReach: machine '${machineId}' has no state matching '${target}'.`);
|
|
3799
|
+
this.name = "AgentUnknownStateError";
|
|
3800
|
+
this.target = target;
|
|
3801
|
+
}
|
|
3802
|
+
};
|
|
3803
|
+
function resolveStateTarget(machine, target) {
|
|
3804
|
+
const stack = [machine.root];
|
|
3805
|
+
while (stack.length > 0) {
|
|
3806
|
+
const node = stack.pop();
|
|
3807
|
+
if (node.id === target.replace(/^#/, "") || node.path.join(".") === target) return node.id;
|
|
3808
|
+
stack.push(...Object.values(node.states));
|
|
3809
|
+
}
|
|
3810
|
+
throw new AgentUnknownStateError(machine.id, target);
|
|
3811
|
+
}
|
|
2486
3812
|
/**
|
|
2487
3813
|
* Answers "can the machine reach this?" by exploring its branches (a thin
|
|
2488
3814
|
* wrapper over {@link explorePaths}). The target is either a state path string
|
|
@@ -2509,23 +3835,16 @@ async function explorePaths(machine, options = {}) {
|
|
|
2509
3835
|
* ```
|
|
2510
3836
|
*/
|
|
2511
3837
|
async function canReach(machine, target, options = {}) {
|
|
2512
|
-
const
|
|
2513
|
-
|
|
2514
|
-
return snapshot.matches(target);
|
|
2515
|
-
} catch {
|
|
2516
|
-
return false;
|
|
2517
|
-
}
|
|
2518
|
-
});
|
|
3838
|
+
const resolvedTarget = typeof target === "string" ? resolveStateTarget(machine, target) : void 0;
|
|
3839
|
+
const { witness } = await explore(machine, options, typeof target === "function" ? target : (snapshot) => snapshot.nodes.some((node) => node.id === resolvedTarget));
|
|
2519
3840
|
return witness !== void 0 ? {
|
|
2520
3841
|
reachable: true,
|
|
2521
|
-
witness
|
|
2522
|
-
|
|
2523
|
-
}
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
function isRecord(value) {
|
|
2528
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3842
|
+
witness,
|
|
3843
|
+
...resolvedTarget ? { target: resolvedTarget } : {}
|
|
3844
|
+
} : {
|
|
3845
|
+
reachable: false,
|
|
3846
|
+
...resolvedTarget ? { target: resolvedTarget } : {}
|
|
3847
|
+
};
|
|
2529
3848
|
}
|
|
2530
3849
|
//#endregion
|
|
2531
3850
|
//#region src/trajectory.ts
|
|
@@ -2545,7 +3864,7 @@ function normalize(item) {
|
|
|
2545
3864
|
kind: "event",
|
|
2546
3865
|
event: item
|
|
2547
3866
|
};
|
|
2548
|
-
if ("value" in item
|
|
3867
|
+
if ("value" in item) return {
|
|
2549
3868
|
kind: "state",
|
|
2550
3869
|
value: item["value"]
|
|
2551
3870
|
};
|
|
@@ -2605,8 +3924,8 @@ function matcherFor(expected) {
|
|
|
2605
3924
|
/**
|
|
2606
3925
|
* Matches a run's trajectory against an expected one.
|
|
2607
3926
|
*
|
|
2608
|
-
* Both trajectories may be state values
|
|
2609
|
-
*
|
|
3927
|
+
* Both trajectories may be state values or events collected from
|
|
3928
|
+
* `onTransition`;
|
|
2610
3929
|
* items are compared by shape, so `['prompting', 'drafting']` scores a state
|
|
2611
3930
|
* path and `['PROMPT_SUBMITTED', 'SEND']` scores an event log with the same
|
|
2612
3931
|
* call.
|
|
@@ -2626,7 +3945,7 @@ function matcherFor(expected) {
|
|
|
2626
3945
|
*
|
|
2627
3946
|
* const path = matchesTrajectory(statePath, ['prompting', 'drafting', 'sent']);
|
|
2628
3947
|
* expect(path.matched, JSON.stringify(path.firstMiss)).toBe(true);
|
|
2629
|
-
* expect(matchesTrajectory(
|
|
3948
|
+
* expect(matchesTrajectory(events, ['PROMPT_SUBMITTED', 'SEND']).matched).toBe(true);
|
|
2630
3949
|
* ```
|
|
2631
3950
|
*
|
|
2632
3951
|
* @example Partial credit in an eval scorer
|
|
@@ -2637,6 +3956,7 @@ function matcherFor(expected) {
|
|
|
2637
3956
|
*/
|
|
2638
3957
|
function matchesTrajectory(actual, expected, options = {}) {
|
|
2639
3958
|
const expectedCount = expected.length;
|
|
3959
|
+
if (expectedCount === 0) throw new AgentError("empty-trajectory", "matchesTrajectory: expected trajectory must contain at least one item.");
|
|
2640
3960
|
const exact = options.exact === true;
|
|
2641
3961
|
let cursor = 0;
|
|
2642
3962
|
let matchedCount = 0;
|
|
@@ -2677,17 +3997,41 @@ function matchesTrajectory(actual, expected, options = {}) {
|
|
|
2677
3997
|
//#endregion
|
|
2678
3998
|
//#region src/scripted-executors.ts
|
|
2679
3999
|
/**
|
|
2680
|
-
* Scripted executors — a
|
|
4000
|
+
* Scripted executors — a deterministic stand-in for a model host.
|
|
2681
4001
|
*
|
|
2682
4002
|
* `createScriptedExecutors` builds a full `{ generateText, streamText, decide }`
|
|
2683
|
-
* set (plus a `userInput` handler) that plays back canned answers
|
|
2684
|
-
*
|
|
2685
|
-
* `
|
|
2686
|
-
*
|
|
2687
|
-
*
|
|
4003
|
+
* set (plus a `userInput` handler) that plays back canned answers keyed by
|
|
4004
|
+
* request name, so `runAgent` (or `provideExecutors`, or a bare
|
|
4005
|
+
* `TextLogic.execute`) runs with no API key and no network.
|
|
4006
|
+
*
|
|
4007
|
+
* One rule: entries for a name are consumed in order, and the last entry
|
|
4008
|
+
* repeats forever.
|
|
2688
4009
|
*
|
|
2689
4010
|
* @module
|
|
2690
4011
|
*/
|
|
4012
|
+
function toQueues(value) {
|
|
4013
|
+
return new Map(Object.entries(value ?? {}).map(([name, entries]) => [name, {
|
|
4014
|
+
entries: Array.isArray(entries) ? [...entries] : [entries],
|
|
4015
|
+
index: 0
|
|
4016
|
+
}]));
|
|
4017
|
+
}
|
|
4018
|
+
/**
|
|
4019
|
+
* Takes the next entry for `name`, holding on the last entry once the list is
|
|
4020
|
+
* spent. `undefined` means the script has no route for this name at all.
|
|
4021
|
+
* @internal
|
|
4022
|
+
*/
|
|
4023
|
+
function takeEntry(queues, name) {
|
|
4024
|
+
const queue = queues.get(name) ?? queues.get("*");
|
|
4025
|
+
if (!queue || queue.entries.length === 0) return;
|
|
4026
|
+
const entry = queue.entries[Math.min(queue.index, queue.entries.length - 1)];
|
|
4027
|
+
queue.index++;
|
|
4028
|
+
return entry;
|
|
4029
|
+
}
|
|
4030
|
+
/** @internal */
|
|
4031
|
+
function noAnswer(name, queues) {
|
|
4032
|
+
const known = [...queues.keys()].join(", ") || "(none)";
|
|
4033
|
+
return /* @__PURE__ */ new Error(`No scripted answer for request '${name}'. Known: ${known}`);
|
|
4034
|
+
}
|
|
2691
4035
|
/** The only own keys an executor-result envelope carries. @internal */
|
|
2692
4036
|
const TEXT_ENVELOPE_KEYS = new Set([
|
|
2693
4037
|
"output",
|
|
@@ -2730,22 +4074,26 @@ function emitScriptedChunk(result, info) {
|
|
|
2730
4074
|
if (typeof output === "string") info?.onChunk?.(output);
|
|
2731
4075
|
}
|
|
2732
4076
|
/**
|
|
2733
|
-
*
|
|
2734
|
-
*
|
|
2735
|
-
*
|
|
4077
|
+
* Executors that replay a script instead of calling a model. Every slot is
|
|
4078
|
+
* provided, so any machine binds.
|
|
4079
|
+
*
|
|
4080
|
+
* Answers are keyed by the request's resolved name — its authored `name`, else
|
|
4081
|
+
* the invoke `id` — and `"*"` serves anything unmatched.
|
|
4082
|
+
* Entries for a name are consumed in order and the last one repeats forever, so
|
|
4083
|
+
* a looping machine needs no extra option. A request the script has no route
|
|
4084
|
+
* for throws an ordinary `Error` from inside the executor, which reaches the
|
|
4085
|
+
* machine as an actor error.
|
|
2736
4086
|
*
|
|
2737
|
-
* Queues are
|
|
2738
|
-
*
|
|
2739
|
-
*
|
|
2740
|
-
* script serves a machine that loops or branches: route on `request.name` (the
|
|
2741
|
-
* `setupAgent({ requests })` key) or on the decision's candidate `events`.
|
|
4087
|
+
* Queues are copied on creation, so the caller's arrays are never mutated and
|
|
4088
|
+
* each call builds a fresh, independent playthrough. Entries may be plain
|
|
4089
|
+
* values or functions of the request.
|
|
2742
4090
|
*
|
|
2743
4091
|
* @example
|
|
2744
4092
|
* ```ts
|
|
2745
4093
|
* const result = await runAgent(moderationMachine, {
|
|
2746
4094
|
* input: { comment: 'honestly this update is terrible', trust: 20 },
|
|
2747
4095
|
* executors: createScriptedExecutors({
|
|
2748
|
-
* decisions: [{ type: 'FLAG', reason: 'Borderline tone.' }],
|
|
4096
|
+
* decisions: { moderate: [{ type: 'FLAG', reason: 'Borderline tone.' }] },
|
|
2749
4097
|
* }),
|
|
2750
4098
|
* });
|
|
2751
4099
|
* ```
|
|
@@ -2753,8 +4101,8 @@ function emitScriptedChunk(result, info) {
|
|
|
2753
4101
|
* @example Dynamic entries
|
|
2754
4102
|
* ```ts
|
|
2755
4103
|
* createScriptedExecutors({
|
|
2756
|
-
* text:
|
|
2757
|
-
* decisions:
|
|
4104
|
+
* text: { draft: (request) => `Draft about ${request.prompt}` },
|
|
4105
|
+
* decisions: { route: (request) => ({ type: request.events[0]!.type }) },
|
|
2758
4106
|
* });
|
|
2759
4107
|
* ```
|
|
2760
4108
|
*
|
|
@@ -2765,30 +4113,72 @@ function emitScriptedChunk(result, info) {
|
|
|
2765
4113
|
* ```
|
|
2766
4114
|
*/
|
|
2767
4115
|
function createScriptedExecutors(script = {}) {
|
|
2768
|
-
const decisions =
|
|
2769
|
-
const text =
|
|
4116
|
+
const decisions = toQueues(script.decisions);
|
|
4117
|
+
const text = toQueues(script.text);
|
|
2770
4118
|
const userInput = [...script.userInput ?? []];
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
4119
|
+
let userInputIndex = 0;
|
|
4120
|
+
const calls = [];
|
|
4121
|
+
const withDefaultUsage = (result) => result.usage || !script.usage ? result : {
|
|
4122
|
+
...result,
|
|
4123
|
+
usage: script.usage
|
|
4124
|
+
};
|
|
4125
|
+
const nextText = async (kind, request, info) => {
|
|
4126
|
+
const name = request.name ?? "*";
|
|
4127
|
+
calls.push({
|
|
4128
|
+
kind,
|
|
4129
|
+
name,
|
|
4130
|
+
input: request.input,
|
|
4131
|
+
request
|
|
4132
|
+
});
|
|
4133
|
+
const entry = takeEntry(text, name);
|
|
4134
|
+
if (entry === void 0) throw noAnswer(name, text);
|
|
4135
|
+
return withDefaultUsage(await resolveScriptedTextEntry(entry, request, info));
|
|
2774
4136
|
};
|
|
2775
4137
|
return {
|
|
4138
|
+
calls,
|
|
2776
4139
|
userInput: async (input) => {
|
|
2777
|
-
|
|
2778
|
-
|
|
4140
|
+
calls.push({
|
|
4141
|
+
kind: "userInput",
|
|
4142
|
+
name: "agent.userInput",
|
|
4143
|
+
input,
|
|
4144
|
+
request: input
|
|
4145
|
+
});
|
|
4146
|
+
if (userInput.length === 0) throw new Error("No scripted answer for request 'agent.userInput'. Add an entry to the script's `userInput` array.");
|
|
4147
|
+
const entry = userInput[Math.min(userInputIndex++, userInput.length - 1)];
|
|
2779
4148
|
return typeof entry === "function" ? await entry(input) : entry;
|
|
2780
4149
|
},
|
|
2781
|
-
generateText: nextText,
|
|
4150
|
+
generateText: (request, info) => nextText("generateText", request, info),
|
|
2782
4151
|
streamText: async (request, info) => {
|
|
2783
|
-
const
|
|
4152
|
+
const name = request.name ?? "*";
|
|
4153
|
+
const streamEntry = script.stream?.[name] ?? script.stream?.["*"];
|
|
4154
|
+
if (streamEntry !== void 0) {
|
|
4155
|
+
calls.push({
|
|
4156
|
+
kind: "streamText",
|
|
4157
|
+
name,
|
|
4158
|
+
input: request.input,
|
|
4159
|
+
request
|
|
4160
|
+
});
|
|
4161
|
+
const resolved = typeof streamEntry === "function" ? await streamEntry(request, info) : streamEntry;
|
|
4162
|
+
const chunks = typeof resolved === "string" ? [resolved] : [...resolved];
|
|
4163
|
+
for (const chunk of chunks) info?.onChunk?.(chunk);
|
|
4164
|
+
return withDefaultUsage({ output: chunks.join("") });
|
|
4165
|
+
}
|
|
4166
|
+
const result = await nextText("streamText", request, info);
|
|
2784
4167
|
emitScriptedChunk(result, info);
|
|
2785
4168
|
return result;
|
|
2786
4169
|
},
|
|
2787
4170
|
decide: async (request) => {
|
|
2788
|
-
|
|
2789
|
-
|
|
4171
|
+
const name = request.name ?? request.id;
|
|
4172
|
+
calls.push({
|
|
4173
|
+
kind: "decide",
|
|
4174
|
+
name,
|
|
4175
|
+
input: request.input,
|
|
4176
|
+
request
|
|
4177
|
+
});
|
|
4178
|
+
const entry = takeEntry(decisions, name);
|
|
4179
|
+
if (entry === void 0) throw noAnswer(name, decisions);
|
|
2790
4180
|
const value = typeof entry === "function" ? await entry(request) : entry;
|
|
2791
|
-
return isRecord(value) && typeof value["type"] !== "string" && "event" in value ? value : { event: value };
|
|
4181
|
+
return withDefaultUsage(isRecord(value) && typeof value["type"] !== "string" && "event" in value ? value : { event: value });
|
|
2792
4182
|
}
|
|
2793
4183
|
};
|
|
2794
4184
|
}
|
|
@@ -2819,7 +4209,7 @@ async function seamOutputOf(result, request) {
|
|
|
2819
4209
|
* seam's own effect completion (the first `xstate.done.*`/`xstate.error.*`
|
|
2820
4210
|
* entry appended after the call was made).
|
|
2821
4211
|
*
|
|
2822
|
-
* @example
|
|
4212
|
+
* @example No API key: the seam is scripted too, so the whole thing runs offline.
|
|
2823
4213
|
* ```ts
|
|
2824
4214
|
* const run = await runSeam(emailDrafter, {
|
|
2825
4215
|
* scripts: { promptEvaluator: [vague, complete], emailDrafter: [draft] },
|
|
@@ -2912,27 +4302,22 @@ async function runSeam(machine, options) {
|
|
|
2912
4302
|
};
|
|
2913
4303
|
let snapshot;
|
|
2914
4304
|
let event;
|
|
2915
|
-
|
|
4305
|
+
const events = [];
|
|
2916
4306
|
let result;
|
|
2917
4307
|
const maxTurns = options.maxTurns ?? 12;
|
|
2918
4308
|
for (let turn = 0; turn <= maxTurns; turn++) {
|
|
2919
4309
|
result = await runAgent(machine, {
|
|
2920
4310
|
...snapshot ? { snapshot } : { input: options.input },
|
|
2921
4311
|
...event ? { event } : {},
|
|
2922
|
-
...options.isIdle ? { isIdle: options.isIdle } : {},
|
|
2923
4312
|
...options.actors ? { actors: options.actors } : {},
|
|
2924
|
-
events,
|
|
2925
4313
|
executors,
|
|
2926
|
-
onEvent: () => {
|
|
2927
|
-
liveEvents++;
|
|
2928
|
-
},
|
|
2929
4314
|
onTransition: (next, causedBy) => {
|
|
2930
4315
|
if (turn > 0 && causedBy.type === "@xstate.init") return;
|
|
4316
|
+
events.push(causedBy);
|
|
4317
|
+
liveEvents = events.length;
|
|
2931
4318
|
statePath.push(next.value);
|
|
2932
4319
|
}
|
|
2933
4320
|
});
|
|
2934
|
-
events = result.events;
|
|
2935
|
-
liveEvents = events.length;
|
|
2936
4321
|
if (result.status !== "idle" || turn === maxTurns) break;
|
|
2937
4322
|
const next = options.respond?.({
|
|
2938
4323
|
snapshot: result.snapshot,
|
|
@@ -2943,13 +4328,13 @@ async function runSeam(machine, options) {
|
|
|
2943
4328
|
});
|
|
2944
4329
|
if (!next) break;
|
|
2945
4330
|
event = next;
|
|
2946
|
-
snapshot = result.
|
|
4331
|
+
snapshot = result.persist();
|
|
2947
4332
|
}
|
|
2948
4333
|
let splitAt = events.length;
|
|
2949
4334
|
if (seamReached) {
|
|
2950
4335
|
splitAt = seamEventAt;
|
|
2951
4336
|
for (let index = seamEventAt; index < events.length; index++) {
|
|
2952
|
-
const type = events[index].
|
|
4337
|
+
const type = events[index].type;
|
|
2953
4338
|
if (type.startsWith("xstate.done") || type.startsWith("xstate.error")) {
|
|
2954
4339
|
splitAt = index;
|
|
2955
4340
|
break;
|
|
@@ -2974,209 +4359,4 @@ async function runSeam(machine, options) {
|
|
|
2974
4359
|
};
|
|
2975
4360
|
}
|
|
2976
4361
|
//#endregion
|
|
2977
|
-
|
|
2978
|
-
/**
|
|
2979
|
-
* The durable host runner: {@link runDurableAgent} drives an executor-bound
|
|
2980
|
-
* agent machine on xstate's `createDurable` execution (`xstate/durable`),
|
|
2981
|
-
* with the agent event log as the journal.
|
|
2982
|
-
*
|
|
2983
|
-
* Where {@link replay} + `getAgentEffects` hand a host an effect list to run
|
|
2984
|
-
* itself, `runDurableAgent` owns the whole loop on the durable runtime:
|
|
2985
|
-
* invoked actors execute live through xstate's own runtime, every EXTERNAL
|
|
2986
|
-
* event (invoke completions included) is appended to the log, and a resume
|
|
2987
|
-
* folds the log back through pure transitions — an invoke whose completion is
|
|
2988
|
-
* already journaled is never re-started, so recorded model calls are never
|
|
2989
|
-
* re-executed. Crash recovery re-runs only the work that was still in flight.
|
|
2990
|
-
*
|
|
2991
|
-
* @module
|
|
2992
|
-
*/
|
|
2993
|
-
const DONE_ACTOR_EVENT_TYPE = "xstate.done.actor";
|
|
2994
|
-
const ERROR_ACTOR_EVENT_TYPE = "xstate.error.actor";
|
|
2995
|
-
function completionActorId(event) {
|
|
2996
|
-
if (event.type !== DONE_ACTOR_EVENT_TYPE && event.type !== ERROR_ACTOR_EVENT_TYPE) return;
|
|
2997
|
-
const actorId = event.actorId;
|
|
2998
|
-
return typeof actorId === "string" ? actorId : void 0;
|
|
2999
|
-
}
|
|
3000
|
-
/** Thrown by the adapter's `waitForEvent` when nothing can produce an event. */
|
|
3001
|
-
const IDLE = Symbol("agent.durable.idle");
|
|
3002
|
-
function createMailbox() {
|
|
3003
|
-
const queue = [];
|
|
3004
|
-
const waiters = [];
|
|
3005
|
-
return {
|
|
3006
|
-
push(event) {
|
|
3007
|
-
const waiter = waiters.shift();
|
|
3008
|
-
if (waiter) waiter(event);
|
|
3009
|
-
else queue.push(event);
|
|
3010
|
-
},
|
|
3011
|
-
take() {
|
|
3012
|
-
const next = queue.shift();
|
|
3013
|
-
if (next !== void 0) return Promise.resolve(next);
|
|
3014
|
-
return new Promise((resolve) => waiters.push(resolve));
|
|
3015
|
-
},
|
|
3016
|
-
size: () => queue.length
|
|
3017
|
-
};
|
|
3018
|
-
}
|
|
3019
|
-
/**
|
|
3020
|
-
* Runs an agent machine as a durable execution: journal in, journal out.
|
|
3021
|
-
*
|
|
3022
|
-
* A fresh call starts from `input` and appends a reserved init entry; a
|
|
3023
|
-
* resume call folds `entries` through pure transitions first — invokes whose
|
|
3024
|
-
* completions are journaled are suppressed (their recorded results replay
|
|
3025
|
-
* instead of re-executing), while work that was in flight at the crash
|
|
3026
|
-
* re-executes live. After the journal, an optional `options.event` is
|
|
3027
|
-
* delivered. The call settles:
|
|
3028
|
-
*
|
|
3029
|
-
* - `done` when the machine reaches a final state, with `output`;
|
|
3030
|
-
* - `idle` when the frontier needs an external event the host has not
|
|
3031
|
-
* supplied (no live work pending, or `isIdle` says the pending work is a
|
|
3032
|
-
* human wait). Persist `entries` and call again with them later.
|
|
3033
|
-
*
|
|
3034
|
-
* ```ts
|
|
3035
|
-
* const first = await runDurableAgent(machine, { input, executors });
|
|
3036
|
-
* // ... persist first.entries; later, in a new process:
|
|
3037
|
-
* const next = await runDurableAgent(machine, {
|
|
3038
|
-
* entries: first.entries,
|
|
3039
|
-
* event: { type: "APPROVE" },
|
|
3040
|
-
* executors,
|
|
3041
|
-
* });
|
|
3042
|
-
* ```
|
|
3043
|
-
*
|
|
3044
|
-
* @experimental Built on xstate's experimental `xstate/durable` entrypoint.
|
|
3045
|
-
*/
|
|
3046
|
-
async function runDurableAgent(machine, options = {}) {
|
|
3047
|
-
const bound = options.executors ? provideExecutors(machine, options.executors, {
|
|
3048
|
-
actors: options.actors,
|
|
3049
|
-
onChunk: options.onChunk,
|
|
3050
|
-
onTrace: options.onTrace
|
|
3051
|
-
}) : options.actors ? machine.provide({ actors: options.actors }) : machine;
|
|
3052
|
-
const machineId = machine.config.id ?? machine.id ?? "(machine)";
|
|
3053
|
-
const machineVersion = options.machineVersion ?? resolveMachineVersion(machine);
|
|
3054
|
-
const priorEntries = options.entries ?? [];
|
|
3055
|
-
if (priorEntries.length > 0) validateReplayEntries(priorEntries, {
|
|
3056
|
-
machineId,
|
|
3057
|
-
machineVersion
|
|
3058
|
-
}, "Durable journal entries");
|
|
3059
|
-
const hasInit = priorEntries[0]?.event.type === AGENT_INIT_EVENT_TYPE;
|
|
3060
|
-
const input = hasInit ? priorEntries[0].event.input : options.input;
|
|
3061
|
-
const journal = priorEntries.slice(hasInit ? 1 : 0).map((entry) => entry.event);
|
|
3062
|
-
const journaledCompletions = /* @__PURE__ */ new Map();
|
|
3063
|
-
for (const event of journal) {
|
|
3064
|
-
const actorId = completionActorId(event);
|
|
3065
|
-
if (actorId !== void 0) journaledCompletions.set(actorId, (journaledCompletions.get(actorId) ?? 0) + 1);
|
|
3066
|
-
}
|
|
3067
|
-
const storedExecutionId = hasInit ? priorEntries[0].metadata?.executionId : void 0;
|
|
3068
|
-
const executionId = typeof storedExecutionId === "string" ? storedExecutionId : hasInit ? priorEntries[0].id : crypto.randomUUID();
|
|
3069
|
-
const mailbox = createMailbox();
|
|
3070
|
-
const rootAddress = machineId;
|
|
3071
|
-
const suppressedChildren = /* @__PURE__ */ new WeakSet();
|
|
3072
|
-
const startsSeen = /* @__PURE__ */ new Map();
|
|
3073
|
-
const liveInFlight = /* @__PURE__ */ new Set();
|
|
3074
|
-
const findChildRef = (effect) => {
|
|
3075
|
-
const raw = effect;
|
|
3076
|
-
const candidates = [raw.actor, ...Array.isArray(raw.args) ? raw.args : []];
|
|
3077
|
-
for (const candidate of candidates) {
|
|
3078
|
-
const ref = candidate;
|
|
3079
|
-
if (ref && typeof ref.sessionId === "string" && typeof ref.id === "string") return ref;
|
|
3080
|
-
}
|
|
3081
|
-
};
|
|
3082
|
-
let replaying = journal.length > 0;
|
|
3083
|
-
let liveEventConsumed = false;
|
|
3084
|
-
let latestSnapshot;
|
|
3085
|
-
const execution = createDurable(bound, {
|
|
3086
|
-
executionId,
|
|
3087
|
-
sendEvent(source, target, event) {
|
|
3088
|
-
if (target.address === rootAddress) {
|
|
3089
|
-
mailbox.push(event);
|
|
3090
|
-
return;
|
|
3091
|
-
}
|
|
3092
|
-
deliverEvent(source, target, event);
|
|
3093
|
-
},
|
|
3094
|
-
runtime(_metadata, effect) {
|
|
3095
|
-
const type = effect.type;
|
|
3096
|
-
if (type === "@xstate.spawn" || type === "@xstate.start") {
|
|
3097
|
-
const child = findChildRef(effect);
|
|
3098
|
-
if (!child) return {};
|
|
3099
|
-
if (type === "@xstate.spawn") {
|
|
3100
|
-
const seen = (startsSeen.get(child.id) ?? 0) + 1;
|
|
3101
|
-
startsSeen.set(child.id, seen);
|
|
3102
|
-
if (seen <= (journaledCompletions.get(child.id) ?? 0)) suppressedChildren.add(child);
|
|
3103
|
-
else liveInFlight.add(child.id);
|
|
3104
|
-
}
|
|
3105
|
-
if (suppressedChildren.has(child)) return {
|
|
3106
|
-
spawnActor() {},
|
|
3107
|
-
startActor() {}
|
|
3108
|
-
};
|
|
3109
|
-
}
|
|
3110
|
-
return {};
|
|
3111
|
-
},
|
|
3112
|
-
executeAction(action) {
|
|
3113
|
-
if (replaying) return;
|
|
3114
|
-
action.exec?.();
|
|
3115
|
-
},
|
|
3116
|
-
async waitForEvent() {
|
|
3117
|
-
if (mailbox.size() > 0) return await mailbox.take();
|
|
3118
|
-
if (liveInFlight.size > 0 && !(options.isIdle?.(latestSnapshot) ?? false)) return await mailbox.take();
|
|
3119
|
-
if (!liveEventConsumed && options.event !== void 0) {
|
|
3120
|
-
liveEventConsumed = true;
|
|
3121
|
-
return options.event;
|
|
3122
|
-
}
|
|
3123
|
-
throw IDLE;
|
|
3124
|
-
}
|
|
3125
|
-
});
|
|
3126
|
-
const entries = [...priorEntries];
|
|
3127
|
-
const entryOptions = {
|
|
3128
|
-
machineVersion,
|
|
3129
|
-
verification: options.verification ?? false
|
|
3130
|
-
};
|
|
3131
|
-
const appendEntry = (event) => {
|
|
3132
|
-
const entry = createReplayEntry(machine, entries, event, entryOptions);
|
|
3133
|
-
entries.push(entry);
|
|
3134
|
-
options.onEntry?.(entry);
|
|
3135
|
-
};
|
|
3136
|
-
if (!hasInit) {
|
|
3137
|
-
const entry = initEntry(machine, input, {
|
|
3138
|
-
...entryOptions,
|
|
3139
|
-
metadata: { executionId }
|
|
3140
|
-
});
|
|
3141
|
-
entries.push(entry);
|
|
3142
|
-
options.onEntry?.(entry);
|
|
3143
|
-
}
|
|
3144
|
-
let journalIndex = 0;
|
|
3145
|
-
let [snapshot, effects] = execution.initialTransition(input);
|
|
3146
|
-
latestSnapshot = snapshot;
|
|
3147
|
-
for (;;) {
|
|
3148
|
-
await execution.executeEffects(effects);
|
|
3149
|
-
const machineSnapshot = snapshot;
|
|
3150
|
-
if (machineSnapshot.status === "done") return {
|
|
3151
|
-
status: "done",
|
|
3152
|
-
output: machineSnapshot.output,
|
|
3153
|
-
snapshot,
|
|
3154
|
-
entries
|
|
3155
|
-
};
|
|
3156
|
-
if (machineSnapshot.status === "error") throw machineSnapshot.error;
|
|
3157
|
-
let event;
|
|
3158
|
-
let fromJournal = false;
|
|
3159
|
-
if (journalIndex < journal.length) {
|
|
3160
|
-
event = journal[journalIndex];
|
|
3161
|
-
journalIndex++;
|
|
3162
|
-
fromJournal = true;
|
|
3163
|
-
replaying = journalIndex < journal.length;
|
|
3164
|
-
} else try {
|
|
3165
|
-
event = await execution.waitForEvent();
|
|
3166
|
-
} catch (error) {
|
|
3167
|
-
if (error === IDLE) return {
|
|
3168
|
-
status: "idle",
|
|
3169
|
-
snapshot,
|
|
3170
|
-
entries
|
|
3171
|
-
};
|
|
3172
|
-
throw error;
|
|
3173
|
-
}
|
|
3174
|
-
const completedId = completionActorId(event);
|
|
3175
|
-
if (completedId !== void 0) liveInFlight.delete(completedId);
|
|
3176
|
-
if (!fromJournal) appendEntry(event);
|
|
3177
|
-
[snapshot, effects] = execution.transition(snapshot, event);
|
|
3178
|
-
latestSnapshot = snapshot;
|
|
3179
|
-
}
|
|
3180
|
-
}
|
|
3181
|
-
//#endregion
|
|
3182
|
-
export { AGENT_EVENT_SCHEMA_VERSION, AGENT_INIT_EVENT_TYPE, AGENT_TRACE_SCHEMA_VERSION, AGENT_USAGE_EVENT_TYPE, AgentDecisionExhaustedError, AgentError, AgentEventLogConflictError, AgentIdleError, AgentIllegalResumeEventError, AgentLintError, AgentMaxModelCallsExceededError, AgentReplayDivergenceError, AgentReplayMachineMismatchError, AgentSnapshotVersionMismatchError, NonSerializableAgentEventError, appendMessages, assertAgentLogEntry, assertAgentMachine, assertEventLogStoreConformance, assertJsonSerializable, assistantMessage, bindRequestExecutor, buildEnvelopeSchema, canReach, createAgentActor, createAgentRun, createAgentSchemas, createDecisionLogic, createInMemoryEventLogStore, createReplayEntry, createScriptedExecutors, createTextLogic, diffEventLogs, executeAgentRequest, explorePaths, generateResult, getAcceptedEvents, getAgentEffects, getAgentMessages, getAgentOutputMode, getAgentSchemas, getCallUsage, getJsonSchema, getJsonSchemaSync, getMachineStructuralHash, getSnapshotNodes, getSnapshotRequests, getStateMeta, initEntry, inspectTransitions, isStandardSchema, lintAgentMachine, matchesTrajectory, messagesSchema, parseAgentEvent, parseModelRef, parseOutput, parseStructuredEnvelope, provideExecutors, renderDecisionAttempts, replay, resolveDecision, runAgent, runDurableAgent, runSeam, serializeTraceEvent, setupAgent, simulateAgent, systemMessage, toolMessage, traceTransitions, userMessage };
|
|
4362
|
+
export { AGENT_EVENT_SCHEMA_VERSION, AGENT_INIT_EVENT_TYPE, AGENT_MESSAGES_EVENT_TYPE, AGENT_TRACE_SCHEMA_VERSION, AGENT_USAGE_EVENT_TYPE, AgentDecisionExhaustedError, AgentError, AgentEventLogConflictError, AgentEventLogError, AgentInvalidEventPayloadError, AgentLintError, AgentMachineVersionMismatchError, AgentMaxModelCallsExceededError, AgentReplayDivergenceError, AgentSnapshotDivergedError, AgentTruncatedError, AgentUnknownStateError, NonSerializableAgentEventError, agentCallOccurrence, appendMessages, assertAgentLogEntry, assertAgentMachine, assertEventLogStoreConformance, assertJsonSerializable, assistantMessage, bindRequestExecutor, buildEnvelopeSchema, canReach, createAgentSchemas, createDecisionLogic, createDecisionRequest, createInMemoryEventLogStore, createReplayEntry, createScriptedExecutors, createTextLogic, eventFromInteraction, executeAgentRequest, explorePaths, forkEventLog, getAcceptedEvents, getAgentOutputMode, getAgentSchemas, getCallFinishReason, getCallUsage, getInteraction, getJsonSchema, getJsonSchemaSync, getLogExecutionId, getMachineStructuralHash, getMessageText, getSnapshotStateHash, getStateMeta, getStatePath, getUsageFromEvents, initEntry, inspectTransitions, interactionMetaSchema, isAgentIdle, isAgentMessages, isStandardSchema, lintAgentMachine, matchesTrajectory, messagesSchema, parseAgentEvent, parseModelRef, parseOutput, parseStructuredEnvelope, provideExecutors, rebindActorSession, renderDecisionAttempts, replay, resolveDecision, runAgent, runAgentLoop, runAgentStream, runSeam, serializeTraceEvent, setupAgent, simulateAgent, systemMessage, toolMessage, traceTransitions, userMessage, validateReplayEntries };
|