@statelyai/agent 2.0.0-alpha.23 → 2.0.0-alpha.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/ai-sdk.cjs +30 -34
  2. package/dist/ai-sdk.d.cts +27 -31
  3. package/dist/ai-sdk.d.mts +27 -31
  4. package/dist/ai-sdk.mjs +27 -31
  5. package/dist/decision-CM9CYZ6t.cjs +519 -0
  6. package/dist/decision-DzSv1NpD.mjs +453 -0
  7. package/dist/event-log-store-Ba2fsc4E.mjs +576 -0
  8. package/dist/event-log-store-DQmImLYk.cjs +702 -0
  9. package/dist/event-log-store-DvOcs10J.d.cts +338 -0
  10. package/dist/event-log-store-DxyVgnas.d.mts +338 -0
  11. package/dist/index.cjs +429 -4272
  12. package/dist/index.d.cts +104 -1005
  13. package/dist/index.d.mts +104 -1005
  14. package/dist/index.mjs +389 -4202
  15. package/dist/log.cjs +392 -0
  16. package/dist/log.d.cts +32 -0
  17. package/dist/log.d.mts +32 -0
  18. package/dist/log.mjs +371 -0
  19. package/dist/machines.cjs +27 -16
  20. package/dist/machines.d.cts +1 -1
  21. package/dist/machines.d.mts +1 -1
  22. package/dist/machines.mjs +27 -16
  23. package/dist/openai.cjs +16 -15
  24. package/dist/openai.d.cts +9 -9
  25. package/dist/openai.d.mts +9 -9
  26. package/dist/openai.mjs +10 -9
  27. package/dist/otel.d.cts +1 -1
  28. package/dist/otel.d.mts +1 -1
  29. package/dist/{run-agent-Ck7Sa3Tb.d.mts → run-agent-BOfoJg5B.d.cts} +86 -387
  30. package/dist/{run-agent-DWqA8KiI.d.cts → run-agent-CG343kFs.d.mts} +86 -387
  31. package/dist/run-agent-Cb1-LS5P.cjs +1783 -0
  32. package/dist/run-agent-DvDQd4If.mjs +1669 -0
  33. package/dist/{setup-agent-CeO2fARv.mjs → setup-agent-CFW-jPI8.mjs} +9 -20
  34. package/dist/{setup-agent-Dl2VELLi.cjs → setup-agent-CPf7MxnG.cjs} +20 -37
  35. package/dist/testing.cjs +1222 -0
  36. package/dist/testing.d.cts +602 -0
  37. package/dist/testing.d.mts +602 -0
  38. package/dist/testing.mjs +1212 -0
  39. package/dist/{text-logic-DJBK3LbG.d.mts → text-logic-BFEOxyxv.d.cts} +71 -147
  40. package/dist/text-logic-Bl_ZRxDi.mjs +833 -0
  41. package/dist/text-logic-C8xyr6Ag.cjs +1091 -0
  42. package/dist/{text-logic-C7bSnH_B.d.cts → text-logic-hZSiRUo1.d.mts} +71 -147
  43. package/dist/{types-CjHk5OdH.d.cts → types-B1Qs2q3q.d.cts} +2 -1
  44. package/dist/{types-DWA6X9R-.d.mts → types-tA3SdczG.d.mts} +2 -1
  45. package/dist/usage-BdEjFuIi.mjs +5 -0
  46. package/dist/usage-efOQPdf_.cjs +10 -0
  47. package/dist/utils-C1p37X_c.d.mts +209 -0
  48. package/dist/utils-RalRn9sk.d.cts +209 -0
  49. package/package.json +24 -4
  50. package/readme.md +2 -2
  51. package/skills/generate-machine/SKILL.md +4 -6
  52. package/dist/decision-BBZ2jMO1.mjs +0 -1490
  53. package/dist/decision-Cv6SfNWl.cjs +0 -1856
package/dist/index.mjs CHANGED
@@ -1,6 +1,139 @@
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";
1
+ import { A as getJsonSchema, B as userMessage, D as assistantMessage, F as getStatePath, H as AgentError, I as isStandardSchema, M as getMachineStructuralHash, N as getMessageText, P as getStateMeta, R as systemMessage, U as AgentTruncatedError, a as bindRequestExecutor, b as getAgentExecutionOptions, d as getCallUsage, f as isTextLogic, g as providerOutputSchema, h as parseProviderOutput, j as getJsonSchemaSync, l as getAgentOutputMode, m as parseOutput, s as createTextLogic, u as getCallFinishReason, y as executorBoundLogics, z as toolMessage } from "./text-logic-Bl_ZRxDi.mjs";
2
+ import { a as isDecisionLogic, c as AgentInvalidEventPayloadError, i as createDecisionRequest, l as getAcceptedEvents, o as renderDecisionAttempts, r as createDecisionLogic, s as resolveDecision, t as AgentDecisionExhaustedError, u as parseAgentEvent } from "./decision-DzSv1NpD.mjs";
3
+ import { t as AGENT_USAGE_EVENT_TYPE } from "./usage-BdEjFuIi.mjs";
4
+ import { n as getAgentSchemas, r as setupAgent, t as createAgentSchemas } from "./setup-agent-CFW-jPI8.mjs";
5
+ import { _ as rejectAgentStep, a as bindDecisionForProvide, c as inspectTransitions, d as runAgent, f as serializeTraceEvent, g as initialAgentStep, i as bindChildMachineForProvide, l as isAgentIdle, m as executeAgentRequest, n as AgentMaxModelCallsExceededError, o as bindTextForProvide, p as traceTransitions, r as AgentSnapshotDivergedError, s as getConfiguredInvokeSrcs, t as AGENT_TRACE_SCHEMA_VERSION, u as isStateMachineLogic, v as resolveAgentStep, y as transitionAgentStep } from "./run-agent-DvDQd4If.mjs";
6
+ import { o as AgentMachineVersionMismatchError, t as AgentEventLogConflictError } from "./event-log-store-Ba2fsc4E.mjs";
7
+ //#region src/messages.ts
8
+ const KNOWN_PART_TYPES = new Set([
9
+ "text",
10
+ "image",
11
+ "file",
12
+ "tool-call",
13
+ "tool-result"
14
+ ]);
15
+ const USER_PART_TYPES = new Set([
16
+ "text",
17
+ "image",
18
+ "file"
19
+ ]);
20
+ const ASSISTANT_PART_TYPES = new Set([
21
+ "text",
22
+ "file",
23
+ "tool-call",
24
+ "tool-result"
25
+ ]);
26
+ const TOOL_PART_TYPES = new Set(["tool-result"]);
27
+ const TOOL_RESULT_CONTENT_PART_TYPES = new Set(["text", "image"]);
28
+ function isKnownPart(part) {
29
+ const type = part && typeof part === "object" ? part.type : void 0;
30
+ return typeof type === "string" && KNOWN_PART_TYPES.has(type);
31
+ }
32
+ function requireString(part, type, field) {
33
+ return typeof part[field] === "string" ? void 0 : `${type} part requires a string "${field}"`;
34
+ }
35
+ function isMediaData(value) {
36
+ return typeof value === "string" || value instanceof Uint8Array || value instanceof ArrayBuffer || value instanceof URL;
37
+ }
38
+ function requireMediaData(part, type, field) {
39
+ return isMediaData(part[field]) ? void 0 : `${type} part requires "${field}" to be a string, Uint8Array, ArrayBuffer, or URL`;
40
+ }
41
+ const TOOL_RESULT_OUTPUT_TYPES = new Set([
42
+ "text",
43
+ "json",
44
+ "error-text",
45
+ "error-json",
46
+ "content"
47
+ ]);
48
+ function validateToolResultOutput(output) {
49
+ if (!output || typeof output !== "object" || Array.isArray(output)) return "tool-result part requires an \"output\" object";
50
+ const record = output;
51
+ const outputType = record.type;
52
+ if (typeof outputType !== "string" || !TOOL_RESULT_OUTPUT_TYPES.has(outputType)) return `Unknown tool-result output type: ${JSON.stringify(outputType)}`;
53
+ if (outputType === "text" || outputType === "error-text") {
54
+ if (typeof record.value !== "string") return `tool-result output of type "${outputType}" requires a string "value"`;
55
+ return;
56
+ }
57
+ if (outputType === "content") {
58
+ if (!Array.isArray(record.value)) return "tool-result output of type \"content\" requires an array \"value\"";
59
+ for (const contentPart of record.value) {
60
+ const error = validatePart(contentPart, TOOL_RESULT_CONTENT_PART_TYPES, "tool-result output content");
61
+ if (error) return error;
62
+ }
63
+ return;
64
+ }
65
+ return record.value !== void 0 ? void 0 : `tool-result output of type "${outputType}" requires a "value"`;
66
+ }
67
+ function validatePart(part, allowedTypes, location) {
68
+ if (!isKnownPart(part)) {
69
+ const type = part && typeof part === "object" ? part.type : void 0;
70
+ return `Unknown message part type: ${JSON.stringify(type)}`;
71
+ }
72
+ if (!allowedTypes.has(part.type)) return `${location} does not allow "${part.type}" parts`;
73
+ const record = part;
74
+ switch (record.type) {
75
+ case "text": return requireString(record, "text", "text");
76
+ case "image": return requireMediaData(record, "image", "image");
77
+ case "file": return requireMediaData(record, "file", "data") ?? requireString(record, "file", "mediaType");
78
+ case "tool-call": return requireString(record, "tool-call", "toolCallId") ?? requireString(record, "tool-call", "toolName") ?? ("input" in record ? void 0 : "tool-call part requires an \"input\" value");
79
+ case "tool-result": return requireString(record, "tool-result", "toolCallId") ?? requireString(record, "tool-result", "toolName") ?? validateToolResultOutput(record.output);
80
+ default: return;
81
+ }
82
+ }
83
+ function validatePartsArray(content, allowedTypes, location) {
84
+ if (!Array.isArray(content)) return "Expected content to be a string or an array of parts";
85
+ for (const part of content) {
86
+ const error = validatePart(part, allowedTypes, location);
87
+ if (error) return error;
88
+ }
89
+ }
90
+ /**
91
+ * A {@link StandardSchemaV1} validating an `AgentMessage[]` context field —
92
+ * checks that every message has a known `role` (`system`/`user`/`assistant`/
93
+ * `tool`) and that `content` is either a string (where the role allows it) or
94
+ * an array of role-appropriate parts whose required fields and media payloads
95
+ * have the right runtime types (extra fields are allowed). Use it directly as
96
+ * a context schema's `messages` field when authoring with `createAgentSchemas`
97
+ * or `fromConfig`. A zod object cannot nest a Standard Schema, so in a zod
98
+ * context schema write `z.custom<AgentMessage[]>(isAgentMessages)`, which runs
99
+ * this same validator (see {@link isAgentMessages}).
100
+ */
101
+ const messagesSchema = { "~standard": {
102
+ version: 1,
103
+ vendor: "statelyai-agent",
104
+ validate(value) {
105
+ if (!Array.isArray(value)) return { issues: [{ message: "Expected an array of agent messages" }] };
106
+ for (const message of value) {
107
+ if (!message || typeof message !== "object") return { issues: [{ message: "Expected an array of agent messages" }] };
108
+ const role = message.role;
109
+ const content = message.content;
110
+ if (role !== "system" && role !== "user" && role !== "assistant" && role !== "tool") return { issues: [{ message: `Unknown message role: ${JSON.stringify(role)}` }] };
111
+ if (role === "system") {
112
+ if (typeof content !== "string") return { issues: [{ message: "system message content must be a string" }] };
113
+ continue;
114
+ }
115
+ if (role === "tool") {
116
+ const error = validatePartsArray(content, TOOL_PART_TYPES, "tool message content");
117
+ if (error) return { issues: [{ message: error }] };
118
+ continue;
119
+ }
120
+ if (typeof content === "string") continue;
121
+ const error = validatePartsArray(content, role === "user" ? USER_PART_TYPES : ASSISTANT_PART_TYPES, `${role} message content`);
122
+ if (error) return { issues: [{ message: error }] };
123
+ }
124
+ return { value };
125
+ }
126
+ } };
127
+ /**
128
+ * Type guard over {@link messagesSchema}: `true` when `value` is a valid
129
+ * `AgentMessage[]`. The one-liner for zod context schemas:
130
+ * `messages: z.custom<AgentMessage[]>(isAgentMessages)`.
131
+ */
132
+ function isAgentMessages(value) {
133
+ const result = messagesSchema["~standard"].validate(value);
134
+ return !(result instanceof Promise) && !("issues" in result && result.issues);
135
+ }
136
+ //#endregion
4
137
  //#region src/interaction.ts
5
138
  function issue(message, path) {
6
139
  return {
@@ -23,9 +156,10 @@ function validateEventMeta(value, path) {
23
156
  * A {@link StandardSchemaV1} for the state metadata {@link getInteraction}
24
157
  * reads: an object whose optional `interaction` declares a `label` (a string
25
158
  * 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.
159
+ * `events` map of choices, and a `textEvent` for free-text answers. This is
160
+ * the default `meta` type `setupAgent` gives a machine; pass it to
161
+ * `createAgentSchemas({ meta: interactionMetaSchema })` only when building a
162
+ * schema pack by hand.
29
163
  */
30
164
  const interactionMetaSchema = { "~standard": {
31
165
  version: 1,
@@ -72,7 +206,7 @@ function getInteraction(snapshot, options = {}) {
72
206
  const schemas = getAgentExecutionOptions(snapshot.machine)?.schemas;
73
207
  const accepted = new Set(getAcceptedEvents(snapshot).map((event) => event.type));
74
208
  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]) => {
209
+ const events = Object.entries(interaction.events ?? {}).filter((entry) => entry[1] !== void 0 && accepted.has(entry[0])).map(([type, config]) => {
76
210
  return [type, typeof config === "string" ? { label: config } : config];
77
211
  }).filter(([type, descriptor]) => {
78
212
  try {
@@ -122,4241 +256,294 @@ function eventFromInteraction(snapshot, choice) {
122
256
  return parseAgentEvent(snapshot, event);
123
257
  }
124
258
  //#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
- }
259
+ //#region src/agent-run.ts
148
260
  /**
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.
261
+ * Wraps {@link runAgent} in a canonical run-stream handle: the run's trace
262
+ * events as a pull-based {@link AgentRun.events} async iterator, alongside the
263
+ * unchanged {@link AgentRun.result} promise.
159
264
  *
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.
265
+ * Semantics:
186
266
  *
187
- * @internal
267
+ * 1. **Starts immediately.** The underlying `runAgent` is invoked on this call,
268
+ * not on the first `events` iteration — work is already in flight when
269
+ * `createAgentRun` returns.
270
+ * 2. **Buffer, don't stall.** Events are queued in an unbounded in-memory
271
+ * buffer as the run emits them; a slow or absent consumer never applies
272
+ * backpressure to the run. Once `run.end` is delivered, the iterator
273
+ * completes.
274
+ * 3. **Composes `onTrace`.** If `options.onTrace` is passed, it still fires for
275
+ * every event — the wrapper adds queue delivery, it does not replace the
276
+ * caller's sink.
277
+ * 4. **`result` mirrors `runAgent`.** Same resolution and rejection behavior
278
+ * (see {@link AgentRun.result}). A no-op rejection handler is attached
279
+ * internally so a caller who reads only `events` never trips an unhandled
280
+ * rejection on a bind-time throw; the returned promise still rejects for a
281
+ * caller who awaits it.
282
+ * 5. **Resume is identical.** Options pass straight through, so starting from a
283
+ * persisted `snapshot` (+ resume `event`) streams that run's events from its
284
+ * own `run.start`, exactly as `runAgent` would run it.
285
+ * 6. **Early termination does not cancel the run.** Breaking out of a
286
+ * `for await` (or calling `events.return()`) stops delivery, but the run
287
+ * keeps going and `result` still settles. Cancelling the run itself is
288
+ * future work — pass `options.signal` to abort it.
289
+ * 7. **`events` is single-consumer.** See {@link AgentRun.events}.
188
290
  */
189
- function getAgentRequests(actions, { machine, ...options } = {}) {
190
- if (machine) options = {
191
- ...getRegisteredAgentExecutionOptions(machine),
192
- ...options
291
+ function createAgentRun(machine, options) {
292
+ const buffer = [];
293
+ const resolvers = [];
294
+ let closed = false;
295
+ let stopped = false;
296
+ const push = (event) => {
297
+ if (closed || stopped) return;
298
+ const resolve = resolvers.shift();
299
+ if (resolve) resolve({
300
+ value: event,
301
+ done: false
302
+ });
303
+ else buffer.push(event);
304
+ };
305
+ const drain = () => {
306
+ while (resolvers.length > 0) resolvers.shift()({
307
+ value: void 0,
308
+ done: true
309
+ });
310
+ };
311
+ const close = () => {
312
+ if (closed) return;
313
+ closed = true;
314
+ drain();
193
315
  };
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
- }];
316
+ const userOnTrace = options.onTrace;
317
+ const result = runAgent(machine, {
318
+ ...options,
319
+ onTrace: (event) => {
320
+ push(event);
321
+ userOnTrace?.(event);
322
+ if (event.type === "run.end") close();
213
323
  }
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
324
  });
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];
325
+ result.then(close, close);
238
326
  return {
239
- type: "xstate.done.actor",
240
- actorId: id,
241
- ...typeof child?.sessionId === "string" ? { sessionId: child.sessionId } : {},
242
- output
327
+ events: {
328
+ next() {
329
+ if (buffer.length > 0) return Promise.resolve({
330
+ value: buffer.shift(),
331
+ done: false
332
+ });
333
+ if (closed || stopped) return Promise.resolve({
334
+ value: void 0,
335
+ done: true
336
+ });
337
+ return new Promise((resolve) => {
338
+ resolvers.push(resolve);
339
+ });
340
+ },
341
+ return() {
342
+ stopped = true;
343
+ buffer.length = 0;
344
+ drain();
345
+ return Promise.resolve({
346
+ value: void 0,
347
+ done: true
348
+ });
349
+ },
350
+ [Symbol.asyncIterator]() {
351
+ return this;
352
+ }
353
+ },
354
+ result
243
355
  };
244
356
  }
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];
357
+ function requestIdentity(request) {
358
+ const candidate = request;
359
+ const id = candidate.id ?? candidate.name ?? "agent.request";
255
360
  return {
256
- type: "xstate.error.actor",
257
- actorId: id,
258
- ...typeof child?.sessionId === "string" ? { sessionId: child.sessionId } : {},
259
- error
361
+ name: candidate.kind === "text" ? candidate.input?.name ?? id : candidate.name ?? id,
362
+ id
260
363
  };
261
364
  }
262
365
  /**
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.
366
+ * Streams one {@link runAgent} call as agent-level events. The machine remains
367
+ * the single artifact; this function only projects its requests, chunks,
368
+ * transitions, emitted events, and terminal result into an async iterable.
328
369
  */
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 ? {
336
- kind: "text",
337
- id: requestOrEffect.requestId,
338
- src: "",
339
- mode: requestOrEffect.mode,
340
- input: requestOrEffect.request,
341
- tools: requestOrEffect.request.tools ?? {},
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
370
+ async function* runAgentStream(machine, options) {
371
+ const run = createAgentRun(machine, options);
372
+ for await (const event of run.events) switch (event.type) {
373
+ case "request.start":
374
+ yield {
375
+ kind: "request",
376
+ phase: "start",
377
+ ...requestIdentity(event.request),
378
+ request: event.request
379
+ };
380
+ break;
381
+ case "request.end":
382
+ yield {
383
+ kind: "request",
384
+ phase: "end",
385
+ ...requestIdentity(event.request),
386
+ request: event.request,
387
+ output: event.output,
388
+ raw: event.raw
389
+ };
390
+ break;
391
+ case "request.error":
392
+ yield {
393
+ kind: "request",
394
+ phase: "error",
395
+ ...requestIdentity(event.request),
396
+ request: event.request,
397
+ error: event.error
398
+ };
399
+ break;
400
+ case "stream.chunk":
401
+ yield {
402
+ kind: "chunk",
403
+ ...requestIdentity(event.request),
404
+ delta: event.chunk
405
+ };
406
+ break;
407
+ case "machine.transition":
408
+ yield {
409
+ kind: "transition",
410
+ value: event.snapshot.value,
411
+ event: event.event
412
+ };
413
+ break;
414
+ case "emit":
415
+ yield {
416
+ kind: "emit",
417
+ event: event.event
418
+ };
419
+ break;
420
+ }
421
+ const result = await run.result;
422
+ if (result.status === "done") yield {
423
+ kind: "done",
424
+ result
349
425
  };
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"
426
+ else if (result.status === "idle") yield {
427
+ kind: "idle",
428
+ result
429
+ };
430
+ else yield {
431
+ kind: "error",
432
+ result
366
433
  };
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
434
  }
394
435
  //#endregion
395
- //#region src/event-log.ts
436
+ //#region src/provide-executors.ts
396
437
  /**
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.
438
+ * Binds a machine's agent actor sources to a set of host `executors` in one
439
+ * call, returning a `machine.provide(...)`-ed copy ready for a plain
440
+ * `createActor(...)` — the uncontrolled-mode counterpart to {@link runAgent}.
441
+ * No run loop, no idle settling: the returned machine drives itself, so
400
442
  *
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.
443
+ * ```ts
444
+ * const actor = createActor(provideExecutors(machine, { generateText, decide }), { input });
445
+ * actor.start();
446
+ * ```
406
447
  *
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);
489
- try {
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:
448
+ * behaves like a normal XState actor whose agent invokes now reach real models.
694
449
  *
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.
450
+ * Binding pass over `machine.sources.actors` (after merging
451
+ * `options.actors`):
452
+ * - `mode: 'generate'` text source → `executors.generateText`
453
+ * - `mode: 'stream'` text source → `executors.streamText`
454
+ * - decision / `agent.decide` source → `executors.decide` (snapshot-driven
455
+ * candidate events, guard `canTake`, and auto-delivery of the chosen event,
456
+ * mirroring `runAgent` but without its model-call counting)
699
457
  *
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`.
458
+ * Pass `options.onTrace` to observe request-level trace events (identical in
459
+ * shape to `runAgent`'s); pair it with {@link traceTransitions} on the actor's
460
+ * `inspect` to also capture `machine.transition` events in the same stream.
722
461
  *
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.
462
+ * A source that already carries its own executor (`.withExecutor(...)`) is left
463
+ * as-is.
464
+ * itself, so supply a handler via `options.actors` if the machine uses it.
465
+ * Non-agent actors are untouched.
728
466
  *
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.
467
+ * Throws at bind time if a source needs an executor kind that `executors` does
468
+ * not provide.
740
469
  *
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.
470
+ * Executor inheritance is RECURSIVE, exactly as in `runAgent`: a string-keyed
471
+ * invoked child machine is rebound too, so its own text/decision requests — at
472
+ * any depth — reach the same host executors. A direct-object invoke `src`
473
+ * cannot be swapped via `.provide`, so nothing under one inherits; bind those
474
+ * with `.withExecutor(...)` or register the child as a string-keyed source. A
475
+ * source that already carries its own executor is never overwritten.
745
476
  */
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
477
+ function provideExecutors(machine, executors, options = {}) {
478
+ const bindOptions = {
479
+ onChunk: options.onChunk,
480
+ onTrace: options.onTrace
755
481
  };
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
769
- });
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;
482
+ const withActors = (target, actors) => target.provide({ actors });
483
+ const provided = options.actors ? withActors(machine, options.actors) : machine;
484
+ const effectiveSources = provided.sources.actors;
485
+ const wrappedSources = {};
486
+ const invokedSrcs = getConfiguredInvokeSrcs(provided);
487
+ for (const [key, logic] of Object.entries(effectiveSources)) {
488
+ if (isStateMachineLogic(logic)) {
489
+ if (invokedSrcs.has(key)) assertChildBindable(logic, executors, key, new Set([provided]));
490
+ const rebound = bindChildMachineForProvide(logic, executors, bindOptions, new Set([provided]));
491
+ if (rebound !== logic) wrappedSources[key] = rebound;
492
+ continue;
822
493
  }
494
+ const requirement = executorRequirementOf(logic);
495
+ if (!requirement) continue;
496
+ const binding = {
497
+ ...requirement,
498
+ bind: () => isDecisionLogic(logic) ? bindDecisionForProvide(provided, logic, executors, bindOptions) : bindTextForProvide(provided, logic, executors, bindOptions)
499
+ };
500
+ if (executorBoundLogics.has(logic)) continue;
501
+ if (!executors[binding.executorKey]) {
502
+ if (invokedSrcs.has(key)) throw missingExecutorError(key, binding.kind, binding.executorKey);
503
+ continue;
504
+ }
505
+ wrappedSources[key] = binding.bind();
823
506
  }
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);
846
- }
847
- return {
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;
857
- }
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)
876
- });
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);
889
- }
890
- return {
891
- snapshot,
892
- persistedSnapshot: persisted
893
- };
507
+ return withActors(provided, wrappedSources);
894
508
  }
895
- //#endregion
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
- */
903
- /**
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;
918
- }
919
- };
920
- /**
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
- }
509
+ /** The executor slot + label an agent logic needs, or `undefined` for a non-agent actor. */
510
+ function executorRequirementOf(logic) {
511
+ if (isDecisionLogic(logic)) return {
512
+ executorKey: "decide",
513
+ kind: "decision"
964
514
  };
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 } : {}
515
+ if (isTextLogic(logic)) return logic.mode === "stream" ? {
516
+ executorKey: "streamText",
517
+ kind: "streaming text"
518
+ } : {
519
+ executorKey: "generateText",
520
+ kind: "text"
994
521
  };
995
522
  }
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;
1332
- }
1333
- for (const testCase of cases) await testCase.run(create);
1334
- }
1335
- //#endregion
1336
- //#region src/run-agent.ts
1337
- /**
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";
1352
- }
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
- }
1366
- /**
1367
- * The version of the {@link AgentTraceEvent} envelope every trace event carries
1368
- * as `schemaVersion`. Bumped only on a breaking change to the envelope or any
1369
- * payload shape, so a consumer can gate on it. Emitted identically by
1370
- * {@link runAgent}, {@link provideExecutors}' `onTrace`, and
1371
- * {@link traceTransitions}.
1372
- */
1373
- const AGENT_TRACE_SCHEMA_VERSION = 1;
1374
- const TRACE_ENVELOPE_KEYS = [
1375
- "schemaVersion",
1376
- "runId",
1377
- "seq",
1378
- "timestamp",
1379
- "machineId",
1380
- "machineVersion",
1381
- "type",
1382
- "status",
1383
- "cause",
1384
- "reasoning",
1385
- "finishReason",
1386
- "chunk"
1387
- ];
1388
- /**
1389
- * Best-effort JSON projection of an arbitrary value. Never throws: functions,
1390
- * symbols, `undefined`, and cyclic back-references are DROPPED (array holes
1391
- * become `null`), non-finite numbers become `null`, `bigint`s become strings,
1392
- * `Error`s become `{ name, message, stack?, code? }`, and anything with a
1393
- * `toJSON()` (e.g. `Date`) is projected through it — the same losses a
1394
- * `JSON.parse(JSON.stringify(...))` round-trip incurs, minus the throws.
1395
- */
1396
- function toJsonValue(value, ancestors) {
1397
- if (value === null) return null;
1398
- const type = typeof value;
1399
- if (type === "string" || type === "boolean") return value;
1400
- if (type === "number") return Number.isFinite(value) ? value : null;
1401
- if (type === "bigint") return value.toString();
1402
- if (type !== "object") return;
1403
- const object = value;
1404
- if (ancestors.includes(object)) return;
1405
- const nextAncestors = [...ancestors, object];
1406
- if (object instanceof Error) {
1407
- const serialized = {
1408
- name: object.name,
1409
- message: object.message
1410
- };
1411
- if (typeof object.stack === "string") serialized.stack = object.stack;
1412
- const code = object.code;
1413
- if (typeof code === "string") serialized.code = code;
1414
- const cause = toJsonValue(object.cause, nextAncestors);
1415
- if (cause !== void 0) serialized.cause = cause;
1416
- return serialized;
1417
- }
1418
- const toJSON = object.toJSON;
1419
- if (typeof toJSON === "function") return toJsonValue(toJSON.call(object), nextAncestors);
1420
- if (Array.isArray(object)) return object.map((item) => toJsonValue(item, nextAncestors) ?? null);
1421
- const out = {};
1422
- for (const [key, item] of Object.entries(object)) {
1423
- const serializedItem = toJsonValue(item, nextAncestors);
1424
- if (serializedItem !== void 0) out[key] = serializedItem;
1425
- }
1426
- return out;
1427
- }
1428
523
  /**
1429
- * Projects an {@link AgentTraceEvent} into a guaranteed JSON-safe envelope —
1430
- * the form the trace stream is actually sold for (one `JSON.stringify` per line
1431
- * in a JSONL file). Live values are sanitized rather than trusted:
1432
- *
1433
- * - Snapshots (`run.start`, `machine.transition`, `run.end`) go through the
1434
- * same JSON round-trip as `machine.getPersistedSnapshot(...)`, so what lands on disk is
1435
- * what a resume would see.
1436
- * - `request.end`'s `raw` (a provider SDK object, frequently cyclic) is DROPPED
1437
- * unless `includeRaw` is set, in which case it is sanitized like everything
1438
- * else.
1439
- * - Non-serializable values anywhere (functions, symbols, `undefined`, cyclic
1440
- * back-references) are dropped; `Error`s become `{ name, message, stack?,
1441
- * code? }` instead of `{}`. Nothing throws.
1442
- *
1443
- * @example
1444
- * ```ts
1445
- * await appendFile('trace.jsonl', JSON.stringify(serializeTraceEvent(event)) + '\n');
1446
- * ```
524
+ * Walks an invoked child machine's own invoked sources (recursively, at any
525
+ * depth) and throws the same missing-executor error `provideExecutors` throws
526
+ * for the top-level machine — before any actor starts. Mirrors runAgent's
527
+ * `assertBindable` for the uncontrolled path. Cycle-safe via `visited`.
1447
528
  */
1448
- function serializeTraceEvent(event, options = {}) {
1449
- const out = {};
1450
- for (const [key, value] of Object.entries(event)) {
1451
- if (key === "raw" && !options.includeRaw) continue;
1452
- if (TRACE_ENVELOPE_KEYS.includes(key)) {
1453
- if (value !== void 0) out[key] = value;
529
+ function assertChildBindable(childMachine, executors, path, visited) {
530
+ if (visited.has(childMachine)) return;
531
+ const nextVisited = new Set([...visited, childMachine]);
532
+ const sources = childMachine.sources.actors;
533
+ for (const src of getConfiguredInvokeSrcs(childMachine)) {
534
+ const logic = sources[src];
535
+ if (!logic) continue;
536
+ if (isStateMachineLogic(logic)) {
537
+ assertChildBindable(logic, executors, `${path} > ${src}`, nextVisited);
1454
538
  continue;
1455
539
  }
1456
- const serialized = toJsonValue(value, []);
1457
- if (serialized !== void 0) out[key] = serialized;
1458
- }
1459
- return out;
1460
- }
1461
- let nextRunAgentTraceId = 1;
1462
- /**
1463
- * Thrown into the invoke that would have made the call once
1464
- * {@link RunAgentOptions.maxModelCalls} is spent. It reaches the machine
1465
- * through the normal error channel, so an invoke's `onError` can branch on it
1466
- * (`error.code === 'max-model-calls'`, the same string the settled result's
1467
- * `cause` uses) and route to a degraded/finish state instead of failing the
1468
- * run. Unhandled, it settles `{ status: 'error', cause: 'max-model-calls' }`.
1469
- *
1470
- * ```ts
1471
- * onError: [
1472
- * { guard: ({ event }) => event.error?.code === 'max-model-calls', target: 'budgetSpent' },
1473
- * { target: 'failed' },
1474
- * ]
1475
- * ```
1476
- */
1477
- var AgentMaxModelCallsExceededError = class extends AgentError {
1478
- /** The budget that was exceeded (`options.maxModelCalls`). */
1479
- maxModelCalls;
1480
- constructor(maxModelCalls) {
1481
- super("max-model-calls", `runAgent exceeded maxModelCalls (${maxModelCalls}). Raise the budget, or handle it in the invoke's onError (error.code === 'max-model-calls').`);
1482
- this.name = "AgentMaxModelCallsExceededError";
1483
- this.maxModelCalls = maxModelCalls;
1484
- }
1485
- };
1486
- function wrapsDecisionExhausted(error) {
1487
- let current = error;
1488
- for (let depth = 0; depth < 10 && current != null; depth++) {
1489
- if (current instanceof AgentDecisionExhaustedError) return true;
1490
- current = current.cause;
1491
- }
1492
- return false;
1493
- }
1494
- /**
1495
- * Recursively collects every invoke's `src` from raw machine config (spike
1496
- * S6: `machine.config` preserves authored srcs; the built `machine.root`
1497
- * normalizes object srcs to synthetic string ids and loses the distinction
1498
- * this walk needs). Function-valued `src` resolvers are dynamic and are not
1499
- * statically analyzable, so they are skipped (pass-through, like any other
1500
- * non-agent actor).
1501
- */
1502
- function collectConfiguredInvokeSrcs(stateConfig, stateName, out) {
1503
- if (!stateConfig) return;
1504
- const invokes = stateConfig.invoke === void 0 ? [] : Array.isArray(stateConfig.invoke) ? stateConfig.invoke : [stateConfig.invoke];
1505
- for (const invokeConfig of invokes) {
1506
- const src = invokeConfig?.src;
1507
- if (typeof src === "string" || src && typeof src === "object") out.push({
1508
- stateName,
1509
- src
1510
- });
540
+ const requirement = executorRequirementOf(logic);
541
+ if (!requirement || executorBoundLogics.has(logic)) continue;
542
+ if (!executors[requirement.executorKey]) throw missingExecutorError(`${path} > ${src}`, requirement.kind, requirement.executorKey);
1511
543
  }
1512
- for (const [childName, childConfig] of Object.entries(stateConfig.states ?? {})) collectConfiguredInvokeSrcs(childConfig, `${stateName}.${childName}`, out);
1513
- }
1514
- /**
1515
- * Duck-types a state machine actor logic (an invoked child machine) vs. any
1516
- * other actor logic. xstate's `StateMachine` carries `.config`, `.root`, and
1517
- * a `.provide(...)` method plus a `sources.actors` map — this
1518
- * combination is unique to machines and survives the dual-package/version
1519
- * boundary an `instanceof` check would not. Used to descend the bind-time
1520
- * walk into invoked child machines (their internal agent requests are opaque
1521
- * to the parent-level source walk otherwise).
1522
- */
1523
- function isStateMachineLogic(logic) {
1524
- return !!logic && typeof logic === "object" && "config" in logic && "root" in logic && typeof logic.provide === "function" && typeof logic.sources === "object" && !!logic.sources?.actors;
1525
544
  }
1526
- /**
1527
- * Fails fast (throws) at bind time — before any actor runs — when the
1528
- * machine invokes an agent actor `runAgent` cannot execute. See §3.2 point 2.
1529
- *
1530
- * Recurses into invoked child state machines (arbitrarily deep). A child
1531
- * machine's agent requests reached through string-keyed invoke srcs DO inherit
1532
- * the parent runAgent's `generateText`/`streamText`/`decide` executors —
1533
- * runAgent rebinds them with the same host-backed wrappers (see
1534
- * {@link rebindChildMachine}) — so the only remaining bind-time errors are: a
1535
- * required executor kind missing entirely (naming the invoke chain and src),
1536
- * and an unbound request reached through a direct-object invoke src that can't
1537
- * be rebound ({@link unrebindableChildRequestError}). A request that carries
1538
- * its own executor (`.withExecutor(...)`, tracked in `executorBoundLogics`)
1539
- * always runs itself; explicit binding shadows inheritance.
1540
- */
1541
- function assertBindable(machine, effectiveSources, executors) {
1542
- assertMachineBindable(machine, effectiveSources, executors, {
1543
- isChild: false,
1544
- childPath: "",
1545
- rebindable: true,
1546
- visited: new Set([machine])
1547
- });
1548
- }
1549
- function assertMachineBindable(machine, effectiveSources, executors, ctx) {
1550
- const invokes = [];
1551
- collectConfiguredInvokeSrcs(machine.config, machine.config.id ?? "(root)", invokes);
1552
- const where = ctx.isChild ? `child machine '${ctx.childPath}' state` : "state";
1553
- for (const { stateName, src } of invokes) {
1554
- if (typeof src !== "string") {
1555
- if (isStateMachineLogic(src)) {
1556
- assertChildMachineBindable(src, src, stateName, executors, ctx);
1557
- continue;
1558
- }
1559
- if ((isTextLogic(src) || isDecisionLogic(src)) && !executorBoundLogics.has(src)) throw new Error(`runAgent: ${where} '${stateName}' invokes a direct-object actor logic (kind: '${src.kind}'). Direct-object invoke srcs cannot be rebound by runAgent — either call '.withExecutor(...)' on the logic before invoking it, or register it as a string-keyed actor source instead (machine.provide({ actors: { name: logic } })) and invoke it by name.`);
1560
- continue;
1561
- }
1562
- const logic = effectiveSources[src];
1563
- if (logic === void 0) throw new Error(`runAgent: ${where} '${stateName}' invokes unregistered actor source '${src}'. Provide it via machine.provide({ actors: { '${src}': ... } }) or runAgent(machine, { actors: { '${src}': ... } }).`);
1564
- if (isStateMachineLogic(logic)) {
1565
- assertChildMachineBindable(logic, src, stateName, executors, ctx);
1566
- continue;
1567
- }
1568
- if (src === "agent.userInput") continue;
1569
- if (isDecisionLogic(logic)) {
1570
- if (executorBoundLogics.has(logic)) continue;
1571
- if (!ctx.rebindable) throw unrebindableChildRequestError(ctx.childPath, stateName, src, "decision");
1572
- if (!executors.decide) throw new Error(`runAgent: ${where} '${stateName}' invokes decision source '${src}' but no 'decide' executor was provided to runAgent(...).`);
1573
- continue;
1574
- }
1575
- if (isTextLogic(logic)) {
1576
- if (executorBoundLogics.has(logic)) continue;
1577
- if (!ctx.rebindable) throw unrebindableChildRequestError(ctx.childPath, stateName, src, logic.mode === "stream" ? "streaming text" : "text");
1578
- if (logic.mode === "stream" && !executors.streamText) throw new Error(`runAgent: ${where} '${stateName}' invokes streaming text source '${src}' but no 'streamText' executor was provided to runAgent(...).`);
1579
- if (logic.mode !== "stream" && !executors.generateText) throw new Error(`runAgent: ${where} '${stateName}' invokes text source '${src}' but no 'generateText' executor was provided to runAgent(...).`);
1580
- continue;
1581
- }
1582
- if (isUnboundPlaceholder(logic)) throw new Error(`runAgent: ${where} '${stateName}' invokes actor source '${src}', which has no host execution. Provide it via machine.provide({ actors: { '${src}': ... } }) or runAgent(machine, { actors: { '${src}': ... } }).`);
1583
- }
1584
- }
1585
- /** Descends the bind-time walk into an invoked child state machine, guarding
1586
- * against a machine that (transitively) invokes itself. */
1587
- function assertChildMachineBindable(childMachine, childSrc, stateName, executors, ctx) {
1588
- if (ctx.visited.has(childMachine)) return;
1589
- const childName = typeof childSrc === "string" ? childSrc : childMachine.config.id ?? "(child machine)";
1590
- const childPath = ctx.childPath ? `${ctx.childPath} > ${childName}` : childName;
1591
- const childSources = childMachine.sources.actors;
1592
- assertMachineBindable(childMachine, childSources, executors, {
1593
- isChild: true,
1594
- childPath,
1595
- rebindable: ctx.rebindable && typeof childSrc === "string",
1596
- visited: new Set([...ctx.visited, childMachine])
1597
- });
1598
- }
1599
- /** The loud bind-time error for an unbound agent request reached under a
1600
- * direct-object invoke src, which runAgent cannot rebind (only string-keyed
1601
- * sources can be swapped via `.provide`). Names the invoke chain AND the
1602
- * request src, and spells out the `.withExecutor`/string-keyed remedy. Note:
1603
- * requests reachable through string-keyed srcs at any depth DO inherit
1604
- * runAgent's executors — this error is only for the unrebindable direct-object
1605
- * case. */
1606
- function unrebindableChildRequestError(childPath, stateName, requestSrc, kind) {
1607
- return /* @__PURE__ */ new Error(`runAgent: child machine '${childPath}' (state '${stateName}') invokes ${kind} source '${requestSrc}', which has no host execution and is reached through a direct-object invoke src that runAgent cannot rebind. Requests reached through string-keyed actor sources inherit runAgent's generateText/streamText/decide executors automatically; a direct-object child machine does not. Either bind the request with its own executor (requestLogic.withExecutor(...)), or register the child as a string-keyed actor source (machine.provide({ actors: { <child>: childMachine } })) and invoke it by name.`);
1608
- }
1609
- /**
1610
- * True when the snapshot's active states declare a transition that would
1611
- * receive the reserved `'@agent.usage'` type — an explicit `on: { '@agent.usage'
1612
- * }` OR a catch-all `on: { '*': … }`. Plain XState semantics apply unmodified:
1613
- * a wildcard matches every event delivered to the machine, reserved ones
1614
- * included. (The MODEL-facing side stays closed: `getAcceptedEvents` drops
1615
- * `@agent.*` before any `allowedEvents` matching, so a wildcard never offers
1616
- * the reserved event as a decision candidate.) @internal
1617
- */
1618
- function declaresUsageTransition(snapshot) {
1619
- return getNextTransitions(snapshot).some((transition) => transition.eventType === "@agent.usage" || transition.eventType === "*");
1620
- }
1621
- /**
1622
- * Builds the ONE place a trace payload is emitted: it hands the payload to the
1623
- * envelope-stamping trace sink and, from that same payload, invokes the sugar
1624
- * callbacks that are projections of it — {@link RunAgentOptions.onChunk},
1625
- * {@link RunAgentOptions.onResult}, {@link RunAgentOptions.onTransition}. Each
1626
- * keeps its historical position relative to the trace: `onResult` fires just
1627
- * BEFORE its `request.end`, `onChunk`/`onTransition` just AFTER their
1628
- * `stream.chunk`/`machine.transition`. Sugar dispatch never depends on whether
1629
- * a trace sink is present, and the trace sink is never called when it is
1630
- * absent — so an `onTrace`-less run still mints no envelope (and advances no
1631
- * `seq`). @internal
1632
- */
1633
- function createTraceDispatch(sinks) {
1634
- return (payload, self) => {
1635
- switch (payload.type) {
1636
- case "stream.chunk":
1637
- sinks.onTrace?.(payload, self);
1638
- sinks.onChunk?.(payload.chunk, { request: payload.request });
1639
- return;
1640
- case "request.end":
1641
- sinks.onResult?.(payload.request, {
1642
- output: payload.output,
1643
- raw: payload.raw
1644
- });
1645
- sinks.onTrace?.(payload, self);
1646
- return;
1647
- case "machine.transition":
1648
- sinks.onTrace?.(payload, self);
1649
- sinks.onTransition?.(payload.snapshot, payload.event);
1650
- return;
1651
- default: sinks.onTrace?.(payload, self);
1652
- }
1653
- };
1654
- }
1655
- /** Reads the durable invoke id/src off the async actor's own ref (`self`). */
1656
- function selfIdAndSrc(self) {
1657
- return {
1658
- id: typeof self?.id === "string" ? self.id : "",
1659
- src: typeof self?.src === "string" ? self.src : ""
1660
- };
1661
- }
1662
- /**
1663
- * The machine actor that INVOKED a decision request — the actor whose
1664
- * live snapshot supplies the candidate events and drives `canTake`/`send`.
1665
- * For a top-level request this is the root actor (identity-equal to
1666
- * `runCtx.actorHolder.actorRef`); for a request inside an invoked child
1667
- * machine it is that child's actor, so a child decision reads and drives
1668
- * the CHILD's snapshot — not the root's. Read off `self._parent`, with the
1669
- * root actor as a fallback.
1670
- */
1671
- function invokingActorOf(self, runCtx) {
1672
- return self?._parent ?? runCtx.actorHolder.actorRef;
1673
- }
1674
- /**
1675
- * The shared text/stream emission helper: binds a {@link TextLogic} to
1676
- * `runCtx`'s executor and constructs the `request.start` / `stream.chunk` /
1677
- * `request.end` (incl. the lifted `reasoning`) / `request.error` trace payloads.
1678
- * Used by both `runAgent` and `provideExecutors` so the two paths produce
1679
- * identical event shapes by construction. @internal
1680
- */
1681
- function bindTextLogic(logic, runCtx) {
1682
- return logic.withExecutor(async ({ request: rawRequest, self: selfArg, signal }) => {
1683
- const self = selfArg;
1684
- const { id, src } = selfIdAndSrc(self);
1685
- const request = {
1686
- ...rawRequest,
1687
- name: rawRequest.name ?? (src !== "" && src !== "agent.generateText" && src !== "agent.streamText" ? src : id || src)
1688
- };
1689
- const executor = logic.mode === "stream" ? runCtx.streamText : runCtx.generateText;
1690
- if (!executor) throw new Error(`No '${logic.mode === "stream" ? "streamText" : "generateText"}' executor provided.`);
1691
- const requestWithTools = {
1692
- ...request,
1693
- tools: request.tools ?? {}
1694
- };
1695
- const agentRequest = {
1696
- kind: "text",
1697
- id,
1698
- src,
1699
- mode: logic.mode,
1700
- input: request,
1701
- tools: requestWithTools.tools,
1702
- events: []
1703
- };
1704
- runCtx.consumeModelCall();
1705
- runCtx.emitTrace?.({
1706
- type: "request.start",
1707
- request: agentRequest
1708
- }, self);
1709
- try {
1710
- const callKey = id !== "" ? runCtx.callKey?.(id, self) : void 0;
1711
- await runCtx.awaitJournal?.();
1712
- const raw = await executor(requestWithTools, {
1713
- onChunk: (chunk) => {
1714
- runCtx.emitTrace?.({
1715
- type: "stream.chunk",
1716
- request: agentRequest,
1717
- chunk
1718
- }, self);
1719
- },
1720
- signal,
1721
- ...runCtx.runId !== void 0 ? { runId: runCtx.runId } : {},
1722
- ...id !== "" ? { requestId: id } : {},
1723
- ...callKey !== void 0 ? { callKey } : {}
1724
- });
1725
- const output = await normalizeGeneratorResult(raw, id, {
1726
- request,
1727
- onChunk: (chunk) => {
1728
- runCtx.emitTrace?.({
1729
- type: "stream.chunk",
1730
- request: agentRequest,
1731
- chunk
1732
- }, self);
1733
- }
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
- }
1746
- const rawReasoning = raw?.reasoning;
1747
- const reasoning = typeof rawReasoning === "string" ? rawReasoning : void 0;
1748
- const usage = getCallUsage(raw);
1749
- const finishReason = getCallFinishReason(raw);
1750
- if (usage) runCtx.recordUsage?.(usage, {
1751
- kind: "text",
1752
- ...id !== "" ? { id } : {},
1753
- ...src !== "" ? { src } : {},
1754
- model: request.model,
1755
- ...request.name !== void 0 ? { name: request.name } : {}
1756
- }, self);
1757
- runCtx.emitTrace?.({
1758
- type: "request.end",
1759
- request: agentRequest,
1760
- output,
1761
- raw,
1762
- ...reasoning !== void 0 ? { reasoning } : {},
1763
- ...usage !== void 0 ? { usage } : {},
1764
- ...finishReason !== void 0 ? { finishReason } : {}
1765
- }, self);
1766
- return { output };
1767
- } catch (error) {
1768
- runCtx.emitTrace?.({
1769
- type: "request.error",
1770
- request: agentRequest,
1771
- error
1772
- }, self);
1773
- throw error;
1774
- }
1775
- });
1776
- }
1777
- function createCountingDecide(runCtx, self) {
1778
- return async (attemptRequest, info) => {
1779
- runCtx.consumeModelCall();
1780
- runCtx.emitTrace?.({
1781
- type: "request.start",
1782
- request: attemptRequest
1783
- }, self);
1784
- try {
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?.();
1789
- const result = await runCtx.decide(runCtx.runId !== void 0 ? {
1790
- ...attemptRequest,
1791
- runId: runCtx.runId
1792
- } : attemptRequest, {
1793
- ...info,
1794
- ...runCtx.runId !== void 0 ? { runId: runCtx.runId } : {},
1795
- ...info?.requestId === void 0 && id !== "" ? { requestId: id } : {},
1796
- ...info?.callKey === void 0 && callKey !== void 0 ? { callKey } : {}
1797
- });
1798
- const usage = getCallUsage(result);
1799
- const finishReason = getCallFinishReason(result);
1800
- if (usage) {
1801
- const { src } = selfIdAndSrc(self);
1802
- runCtx.recordUsage?.(usage, {
1803
- kind: "decision",
1804
- ...attemptRequest.id ? { id: attemptRequest.id } : {},
1805
- ...src !== "" ? { src } : {},
1806
- model: attemptRequest.model
1807
- }, self);
1808
- }
1809
- runCtx.emitTrace?.({
1810
- type: "request.end",
1811
- request: attemptRequest,
1812
- output: result.event,
1813
- raw: result,
1814
- ...usage !== void 0 ? { usage } : {},
1815
- ...finishReason !== void 0 ? { finishReason } : {}
1816
- }, self);
1817
- return result;
1818
- } catch (error) {
1819
- runCtx.emitTrace?.({
1820
- type: "request.error",
1821
- request: attemptRequest,
1822
- error
1823
- }, self);
1824
- throw error;
1825
- }
1826
- };
1827
- }
1828
- /**
1829
- * Builds the decision actor logic runAgent installs in place of a
1830
- * `DecisionLogic`/`agent.decide` source. `DecisionLogic.withExecutor(...)`
1831
- * can only swap the innermost per-attempt executor — the `resolveDecision(...)`
1832
- * call (and its `canTake`) is hardwired inside the original logic's `run`.
1833
- * To supply `canTake` (mode-3, §2.6), runAgent instead builds a fresh async
1834
- * logic here that calls `resolveDecision` itself, reusing `logic.request(...)`
1835
- * to build the request the same way the original logic would have.
1836
- *
1837
- * On success it SENDS the chosen event to the invoking actor (auto-delivery)
1838
- * and then completes with that event
1839
- * as its output — so callers never wire an `onDone` to deliver it. See the
1840
- * send-then-complete note inside `run` for how exit-cancels-invoke interacts
1841
- * with `onDone`.
1842
- */
1843
- function bindDecisionLogic(logic, runCtx) {
1844
- const decisionLogic = createAsyncLogic({ run: async ({ input, signal, self: selfArg }) => {
1845
- if (!runCtx.decide) throw new Error("No 'decide' executor provided.");
1846
- const self = selfArg;
1847
- const { id } = selfIdAndSrc(self);
1848
- const declaredEventTypes = logic.allowedEventTypes?.(input);
1849
- await Promise.resolve();
1850
- const actorRef = invokingActorOf(self, runCtx);
1851
- const events = actorRef ? getAcceptedEvents(actorRef.getSnapshot(), {
1852
- schemas: runCtx.schemas,
1853
- eventTypes: declaredEventTypes
1854
- }) : [];
1855
- const lowered = logic.request(input);
1856
- const chosen = await resolveDecision({
1857
- ...lowered,
1858
- id,
1859
- name: lowered.name ?? id,
1860
- input,
1861
- events
1862
- }, { decide: createCountingDecide(runCtx, self) }, {
1863
- maxRetries: logic.maxRetries,
1864
- signal,
1865
- canTake: (event) => actorRef ? actorRef.getSnapshot().can(event) : true
1866
- });
1867
- actorRef?.send(chosen);
1868
- await Promise.resolve();
1869
- return chosen;
1870
- } });
1871
- return Object.assign(decisionLogic, {
1872
- kind: "statelyai.decisionLogic",
1873
- maxRetries: logic.maxRetries,
1874
- request: logic.request,
1875
- withExecutor: (nextExecute) => bindDecisionLogic(logic.withExecutor(nextExecute), runCtx)
1876
- });
1877
- }
1878
- /**
1879
- * The set of string-keyed actor `src`s the machine's own config invokes
1880
- * (top-level, recursing into child STATES but not into invoked child
1881
- * machines). {@link provideExecutors} uses it to require an executor only for a
1882
- * source the machine actually invokes — the always-registered `agent.*`
1883
- * builtins that go unused must not force their executors to be supplied.
1884
- * @internal
1885
- */
1886
- function getConfiguredInvokeSrcs(machine) {
1887
- const invokes = [];
1888
- collectConfiguredInvokeSrcs(machine.config, machine.config.id ?? "(root)", invokes);
1889
- const srcs = /* @__PURE__ */ new Set();
1890
- for (const { src } of invokes) if (typeof src === "string") srcs.add(src);
1891
- return srcs;
1892
- }
1893
- const rootTraceRegistry = /* @__PURE__ */ new WeakMap();
1894
- let nextProvideRunId = 1;
1895
- /** Walks `self._parent` from an invoked async leaf actor up to its root actor. */
1896
- function rootActorOf(self) {
1897
- let ref = self;
1898
- while (ref?._parent) ref = ref._parent;
1899
- return ref;
1900
- }
1901
- /** The per-root envelope state, minted on first use (runId `run_<n>`, matching runAgent). */
1902
- function rootTraceState(root) {
1903
- let state = rootTraceRegistry.get(root);
1904
- if (!state) {
1905
- const logic = root.logic;
1906
- const machineId = (logic?.config)?.id ?? logic?.id ?? "(machine)";
1907
- const machineVersion = logic ? resolveMachineVersion(logic) : "";
1908
- state = {
1909
- runId: `run_${nextProvideRunId++}`,
1910
- seq: 0,
1911
- machineId,
1912
- machineVersion
1913
- };
1914
- rootTraceRegistry.set(root, state);
1915
- }
1916
- return state;
1917
- }
1918
- /** Stamps a per-root-actor envelope onto a bare trace payload. */
1919
- function stampRootTrace(root, payload) {
1920
- const state = rootTraceState(root);
1921
- return {
1922
- schemaVersion: 1,
1923
- runId: state.runId,
1924
- seq: ++state.seq,
1925
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1926
- machineId: state.machineId,
1927
- machineVersion: state.machineVersion,
1928
- ...payload
1929
- };
1930
- }
1931
- /** Adapts a public `onTrace` into the payload-level trace sink a {@link TraceDispatch} fans out to. */
1932
- function provideTraceSink(onTrace) {
1933
- if (!onTrace) return;
1934
- return (payload, self) => {
1935
- const root = rootActorOf(self);
1936
- if (root) onTrace(stampRootTrace(root, payload));
1937
- };
1938
- }
1939
- /**
1940
- * A minimal {@link RunAgentBindContext} for `provideExecutors` (uncontrolled
1941
- * `createActor`): the same wrappers runAgent installs, MINUS the run-scoped
1942
- * model-call counter. `consumeModelCall` is a no-op (no budget), and
1943
- * `actorHolder.actorRef` is left undefined — the wrappers read the invoking
1944
- * actor off `self._parent`, always present under a live `createActor` tree.
1945
- * `onTrace` (when given) mints a per-root-actor envelope. `schemas` come from
1946
- * the machine's registered `setupAgent` execution options.
1947
- *
1948
- * `recordUsage` has no run-level aggregate to fold into here (there is no
1949
- * run), so it does one thing: deliver the reserved `@agent.usage` event, gated
1950
- * exactly like runAgent's — see {@link deliverUsageEvent}.
1951
- */
1952
- function provideBindContext(machine, executors, options) {
1953
- const traceSink = provideTraceSink(options.onTrace);
1954
- const onChunk = options.onChunk;
1955
- return {
1956
- generateText: executors.generateText,
1957
- streamText: executors.streamText,
1958
- decide: executors.decide,
1959
- emitTrace: traceSink || onChunk ? createTraceDispatch({
1960
- onTrace: traceSink,
1961
- onChunk: onChunk ? (chunk) => onChunk(chunk) : void 0
1962
- }) : void 0,
1963
- consumeModelCall: () => {},
1964
- recordUsage: (usage, source, self) => {
1965
- deliverUsageEvent({
1966
- type: AGENT_USAGE_EVENT_TYPE,
1967
- ...source,
1968
- usage
1969
- }, () => self?._parent);
1970
- },
1971
- actorHolder: { actorRef: void 0 },
1972
- schemas: getRegisteredAgentExecutionOptions(machine).schemas
1973
- };
1974
- }
1975
- /**
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,
1979
- * the settling request actor's `self._parent` (always the invoking machine
1980
- * under a live `createActor` tree) on the `provideExecutors` path.
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
- *
1985
- * Gating is identical on both: the target snapshot must be active, must declare
1986
- * an `'@agent.usage'` transition — explicitly, or through a catch-all
1987
- * `on: { '*' }` (see {@link declaresUsageTransition}) — and must be able to
1988
- * take the event. `onDropped` is the run path's straggler gate: it returns `true` for a
1989
- * call that settled after the cycle resolved, which drops the event (traced as
1990
- * `usage.dropped`) rather than delivering it. Uncontrolled mode has no cycle to
1991
- * settle, so it passes no gate and has no dropped stragglers.
1992
- *
1993
- * Delivery follows each path's binding boundary: only sources IT bound report
1994
- * here, so an invoked child machine that was not itself passed through
1995
- * `provideExecutors` reports nothing. @internal
1996
- */
1997
- function deliverUsageEvent(event, resolveActorRef, onDropped) {
1998
- const actorRef = resolveActorRef();
1999
- if (!actorRef) return;
2000
- const snapshot = actorRef.getSnapshot();
2001
- if (snapshot?.status !== "active" || !declaresUsageTransition(snapshot)) return;
2002
- if (onDropped?.(event)) return;
2003
- if (!snapshot.can(event)) return;
2004
- actorRef.send(event);
2005
- }
2006
- /**
2007
- * Host-binds one text/stream source for {@link provideExecutors} using the SAME
2008
- * emission helper as `runAgent` ({@link bindTextLogic}), so a bound
2009
- * text request emits request.start/stream.chunk/request.end/request.error with
2010
- * identical shapes. @internal
2011
- */
2012
- function bindTextForProvide(machine, logic, executors, options) {
2013
- return bindTextLogic(logic, provideBindContext(machine, executors, options));
2014
- }
2015
- /**
2016
- * Host-binds one `DecisionLogic`/`agent.decide` source for
2017
- * {@link provideExecutors}: runAgent's decision wrapper (snapshot-driven
2018
- * candidate events, `canTake`, auto-delivery of the chosen event) with the same
2019
- * request-level tracing runAgent emits, minus run-scoped counting. @internal
2020
- */
2021
- function bindDecisionForProvide(machine, logic, executors, options) {
2022
- return bindDecisionLogic(logic, provideBindContext(machine, executors, options));
2023
- }
2024
- /**
2025
- * Recursively binds an invoked child state machine for {@link provideExecutors},
2026
- * with the same semantics `runAgent` applies ({@link rebindChildMachine}):
2027
- * string-keyed text/decision sources at any depth inherit the host executors,
2028
- * a source that carries its own executor is left alone, and a cycle is
2029
- * returned as-is. Each machine in the tree is bound with its own registered
2030
- * `setupAgent` schemas. Returns the original machine when nothing needed
2031
- * wrapping. @internal
2032
- */
2033
- function bindChildMachineForProvide(childMachine, executors, options, visited) {
2034
- const ctxFor = (target) => provideBindContext(target, executors, options);
2035
- return rebindChildMachine(childMachine, ctxFor(childMachine), visited, ctxFor);
2036
- }
2037
- /**
2038
- * Validates `input` against the machine's registered input schema, returning
2039
- * the schema's output — so defaults are filled and transforms applied before
2040
- * the value reaches `createActor` or the replayable event log.
2041
- *
2042
- * Standard Schema only (no validation library is referenced), so this works for
2043
- * whatever the machine was declared with. Omitted input stays omitted rather
2044
- * than being validated as `{}`: "started with no input" keeps meaning what it
2045
- * has always meant, instead of newly failing schemas with required fields.
2046
- */
2047
- function resolveMachineInput(machine, input) {
2048
- if (input === void 0) return input;
2049
- const schema = getRegisteredAgentExecutionOptions(machine).schemas?.input;
2050
- if (!isStandardSchema(schema)) return input;
2051
- try {
2052
- return validateSchemaSync(schema, input);
2053
- } catch (error) {
2054
- throw new AgentError("invalid-machine-input", `runAgent: machine input failed validation against the declared input schema: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
2055
- }
2056
- }
2057
- /**
2058
- * Recursively rebinds an invoked child machine's own agent sources with the
2059
- * SAME host-backed wrappers runAgent applies to the top-level machine, so a
2060
- * child's text/stream/decision requests inherit runAgent's executors and
2061
- * participate in maxModelCalls counting, onTrace/onChunk/onResult exactly like
2062
- * parent requests. Returns the child machine to invoke: a `.provide`-rebound
2063
- * copy when any inner source needed wrapping, else the original untouched.
2064
- *
2065
- * Only string-keyed sources are visited — a direct-object invoke src cannot be
2066
- * swapped via `.provide` (assertBindable already rejected an unbound request
2067
- * under one). A source that already carries its own executor
2068
- * (`executorBoundLogics`) is left as-is: explicit binding shadows inheritance.
2069
- * Cycle-safe via `visited` (a machine that invokes itself is returned as-is).
2070
- */
2071
- function rebindChildMachine(childMachine, runCtx, visited, ctxFor) {
2072
- if (visited.has(childMachine)) return childMachine;
2073
- const childVisited = new Set([...visited, childMachine]);
2074
- runCtx = ctxFor ? ctxFor(childMachine) : runCtx;
2075
- const sources = childMachine.sources.actors;
2076
- const wrapped = {};
2077
- for (const [key, logic] of Object.entries(sources)) {
2078
- if (isDecisionLogic(logic)) {
2079
- if (!executorBoundLogics.has(logic)) wrapped[key] = bindDecisionLogic(logic, runCtx);
2080
- continue;
2081
- }
2082
- if (isTextLogic(logic)) {
2083
- if (!executorBoundLogics.has(logic)) wrapped[key] = bindTextLogic(logic, runCtx);
2084
- continue;
2085
- }
2086
- if (isStateMachineLogic(logic)) {
2087
- const rebound = rebindChildMachine(logic, runCtx, childVisited, ctxFor);
2088
- if (rebound !== logic) wrapped[key] = rebound;
2089
- continue;
2090
- }
2091
- }
2092
- return Object.keys(wrapped).length > 0 ? childMachine.provide({ actors: wrapped }) : childMachine;
2093
- }
2094
- /**
2095
- * Runs an agent machine to completion or idle: a `createActor` host that
2096
- * binds `options`' host executors onto the machine's `agent.*`/`TextLogic`/
2097
- * `DecisionLogic` actor sources, starts (or resumes) the actor, and drives
2098
- * it until it settles — {@link RunAgentResult} `done | idle | error`. Unlike
2099
- * the step helpers ({@link initialAgentStep} etc — a pure
2100
- * transition-at-a-time path for durable hosts), `runAgent` owns a live actor
2101
- * internally; there is no continuation callback, so **idle always settles**
2102
- * and the caller resumes explicitly by passing the settled `{ snapshot,
2103
- * event }` back in. The actor is stopped on every settle path (`done`,
2104
- * `idle`, and `error` alike) — resume is always by snapshot, never by
2105
- * holding a reference to a live actor.
2106
- *
2107
- * Binding happens **before** the actor starts: every invoke the machine
2108
- * could reach is walked and checked against the effective actor sources
2109
- * (`options.actors` merged onto the machine), so a missing
2110
- * `streamText`/`decide` executor or any other unbound actor source throws
2111
- * immediately — a bind-time error, not a mid-run failure. The one exception
2112
- * is `agent.userInput`: unhandled, it binds as a pending placeholder that
2113
- * settles the run idle (with `pendingUserInputs`) instead of erroring.
2114
- *
2115
- * @example
2116
- * ```ts
2117
- * const executors = createAiSdkExecutors({ models });
2118
- * let r = await runAgent(machine, { input, executors });
2119
- * while (r.status === 'idle') {
2120
- * const event = await promptUser(getAcceptedEvents(r.snapshot));
2121
- * r = await runAgent(machine, { snapshot: r.snapshot, event, executors });
2122
- * }
2123
- * if (r.status !== 'done') throw new Error(`Run did not complete: ${r.status}`);
2124
- * console.log(r.output);
2125
- * ```
2126
- *
2127
- * The `executors`' `generateText`/`streamText` accept the raw Vercel AI SDK
2128
- * functions directly (`executors: { generateText, streamText }` with them
2129
- * imported from `ai`) — their `{ text }`/`{ textStream }` results are unwrapped
2130
- * natively. `decide` cannot be a raw AI SDK function: the tool-per-event mapping
2131
- * lives in an adapter — use `createAiSdkExecutors` from '@statelyai/agent/ai-sdk'.
2132
- */
2133
- async function runAgent(machine, options) {
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();
2149
- }
2150
- /**
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.
2155
- */
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
- }
2164
- }
2165
- function createAgentSession(machine, options) {
2166
- const maxModelCalls = options.maxModelCalls ?? 100;
2167
- let modelCallCount = 0;
2168
- let budgetExceeded = false;
2169
- let warnedNonSerializable = false;
2170
- const runId = `run_${nextRunAgentTraceId++}`;
2171
- let traceSeq = 0;
2172
- const resolvedInput = resolveMachineInput(machine, options.input);
2173
- const machineId = machine.config.id ?? machine.id ?? "(machine)";
2174
- const machineVersion = resolveMachineVersion(machine);
2175
- const onTrace = createTraceDispatch({
2176
- onTrace: (payload) => {
2177
- options.onTrace?.({
2178
- schemaVersion: 1,
2179
- runId,
2180
- seq: ++traceSeq,
2181
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2182
- machineId,
2183
- machineVersion,
2184
- ...payload
2185
- });
2186
- },
2187
- onChunk: options.onChunk,
2188
- onResult: options.onResult,
2189
- onTransition: options.onTransition
2190
- });
2191
- const consumeModelCall = () => {
2192
- if (budgetExceeded) throw new AgentMaxModelCallsExceededError(maxModelCalls);
2193
- if (modelCallCount + 1 > maxModelCalls) {
2194
- budgetExceeded = true;
2195
- throw new AgentMaxModelCallsExceededError(maxModelCalls);
2196
- }
2197
- modelCallCount += 1;
2198
- };
2199
- const cycleGate = { isResolved: () => false };
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]
2216
- });
2217
- }).catch((error) => {
2218
- journalError ??= error;
2219
- failRunOnJournalError(error);
2220
- }).finally(() => {
2221
- journalPending--;
2222
- });
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;
2300
- const runUsage = () => ({
2301
- ...getUsageFromEvents(logEntries.slice(resumedLogLength)),
2302
- modelCalls: modelCallCount
2303
- });
2304
- const warnNonSerializableContext = (snapshot) => {
2305
- if (warnedNonSerializable || process.env.NODE_ENV === "production") return;
2306
- let offending = [];
2307
- try {
2308
- offending = findNonSerializableContextPaths(snapshot.context);
2309
- } catch {
2310
- return;
2311
- }
2312
- if (offending.length === 0) return;
2313
- warnedNonSerializable = true;
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.`);
2315
- };
2316
- const provided = machine.provide({ actors: options.actors });
2317
- logMachine = provided;
2318
- const effectiveSources = provided.sources.actors;
2319
- const executors = {
2320
- ...getRegisteredAgentExecutionOptions(machine).models?.[DEFAULT_AGENT_EXECUTORS]?.(),
2321
- ...options.executors
2322
- };
2323
- assertBindable(provided, effectiveSources, executors);
2324
- const actorHolder = { actorRef: void 0 };
2325
- const runCtx = {
2326
- generateText: executors.generateText,
2327
- streamText: executors.streamText,
2328
- decide: executors.decide,
2329
- emitTrace: onTrace,
2330
- consumeModelCall,
2331
- recordUsage,
2332
- actorHolder,
2333
- awaitJournal,
2334
- runId,
2335
- schemas: getRegisteredAgentExecutionOptions(machine).schemas
2336
- };
2337
- let userInputIsPlaceholder = false;
2338
- const wrappedSources = {};
2339
- for (const [key, logic] of Object.entries(effectiveSources)) {
2340
- if (key === "agent.userInput") {
2341
- if (options.userInput) {
2342
- const userInput = options.userInput;
2343
- wrappedSources[key] = createAsyncLogic({ run: async ({ input }) => await userInput(input) });
2344
- } else if (isUnboundPlaceholder(logic)) {
2345
- userInputIsPlaceholder = true;
2346
- wrappedSources[key] = createAsyncLogic({ run: () => new Promise(() => {}) });
2347
- }
2348
- continue;
2349
- }
2350
- if (isDecisionLogic(logic)) {
2351
- wrappedSources[key] = bindDecisionLogic(logic, runCtx);
2352
- continue;
2353
- }
2354
- if (isTextLogic(logic)) {
2355
- if (!executorBoundLogics.has(logic)) wrappedSources[key] = bindTextLogic(logic, runCtx);
2356
- continue;
2357
- }
2358
- if (isStateMachineLogic(logic)) {
2359
- const rebound = rebindChildMachine(logic, runCtx, new Set([machine]));
2360
- if (rebound !== logic) wrappedSources[key] = rebound;
2361
- continue;
2362
- }
2363
- }
2364
- const boundMachine = provided.provide({ actors: wrappedSources });
2365
- const isIdle = getMachineIdlePredicate(machine) ?? isAgentIdle;
2366
- const isIntentionalIdle = (snapshot) => isIdle(snapshot) || collectPendingUserInputs(snapshot).length > 0;
2367
- const resumeEvents = options.events;
2368
- let effectiveSnapshot = options.snapshot;
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;
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
- });
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 });
2473
- }
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;
2486
- };
2487
- const resumeEventToSend = options.event;
2488
- let ignoredEvent;
2489
- seedInitEntry?.();
2490
- let settled = false;
2491
- let journalAtSettle;
2492
- cycleGate.isResolved = () => settled;
2493
- let lastResult;
2494
- const waiters = [];
2495
- let idleTimer;
2496
- let actor;
2497
- let actorStarted = false;
2498
- let deliveringResumeEvent = resumeEventToSend !== void 0;
2499
- const settle = (outcome) => {
2500
- if (settled) return;
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
- };
2522
- const result = {
2523
- ...outcome,
2524
- events: [...logEntries],
2525
- persist,
2526
- drain: drainJournal,
2527
- usage: runUsage(),
2528
- ...ignoredEvent !== void 0 ? { ignored: ignoredEvent } : {}
2529
- };
2530
- if (idleTimer !== void 0) clearTimeout(idleTimer);
2531
- onTrace({
2532
- type: "run.end",
2533
- ...outcome
2534
- });
2535
- if (options.signal) options.signal.removeEventListener("abort", onAbort);
2536
- actor.stop();
2537
- lastResult = result;
2538
- for (const resolve of waiters.splice(0)) resolve(result);
2539
- };
2540
- const onAbort = () => {
2541
- settle({
2542
- status: "error",
2543
- cause: "aborted",
2544
- error: options.signal?.reason ?? /* @__PURE__ */ new Error("Aborted"),
2545
- snapshot: actor.getSnapshot()
2546
- });
2547
- };
2548
- const settleIdle = (current) => {
2549
- warnNonSerializableContext(current);
2550
- const pendingUserInputs = userInputIsPlaceholder ? collectPendingUserInputs(current) : [];
2551
- settle({
2552
- status: "idle",
2553
- snapshot: current,
2554
- ...pendingUserInputs.length > 0 ? { pendingUserInputs } : {}
2555
- });
2556
- };
2557
- const runErrorCause = (error) => budgetExceeded ? "max-model-calls" : wrapsDecisionExhausted(error) ? "decision-exhausted" : "machine";
2558
- const scheduleIdleCheck = () => {
2559
- if (idleTimer !== void 0) clearTimeout(idleTimer);
2560
- idleTimer = setTimeout(() => {
2561
- idleTimer = void 0;
2562
- if (settled) return;
2563
- const current = actor.getSnapshot();
2564
- if (isIntentionalIdle(current) && isIdleSnapshot(current, { ignoreUserInputChildren: userInputIsPlaceholder })) settleIdle(current);
2565
- }, 0);
2566
- };
2567
- actor = createActor(boundMachine, {
2568
- input: resolvedInput,
2569
- snapshot: effectiveSnapshot,
2570
- inspect: (event) => {
2571
- if (typeof options.inspect === "function") options.inspect(event);
2572
- else options.inspect?.next?.(event);
2573
- if (event.type !== "@xstate.transition" || event.actorRef !== actor.ref) return;
2574
- if (settled) return;
2575
- const snapshot = event.snapshot;
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);
2580
- onTrace({
2581
- type: "machine.transition",
2582
- snapshot,
2583
- event: event.event
2584
- });
2585
- if (snapshot.status === "done") {
2586
- settle({
2587
- status: "done",
2588
- output: snapshot.output,
2589
- snapshot
2590
- });
2591
- return;
2592
- }
2593
- if (snapshot.status === "error") {
2594
- settle({
2595
- status: "error",
2596
- cause: runErrorCause(snapshot.error),
2597
- error: snapshot.error,
2598
- snapshot
2599
- });
2600
- return;
2601
- }
2602
- if (snapshot.status === "stopped") {
2603
- settle({
2604
- status: "error",
2605
- cause: "stopped",
2606
- error: /* @__PURE__ */ new Error("Actor stopped externally."),
2607
- snapshot
2608
- });
2609
- return;
2610
- }
2611
- if (!deliveringResumeEvent && isIntentionalIdle(snapshot) && isIdleSnapshot(snapshot, { ignoreUserInputChildren: userInputIsPlaceholder })) {
2612
- queueMicrotask(() => {
2613
- if (settled) return;
2614
- const current = actor.getSnapshot();
2615
- if (isIntentionalIdle(current) && isIdleSnapshot(current, { ignoreUserInputChildren: userInputIsPlaceholder })) settleIdle(current);
2616
- else scheduleIdleCheck();
2617
- });
2618
- return;
2619
- }
2620
- scheduleIdleCheck();
2621
- }
2622
- });
2623
- actorHolder.actorRef = actor;
2624
- actor.subscribe({ error: () => {} });
2625
- actor.on("*", (event) => {
2626
- onTrace({
2627
- type: "emit",
2628
- event
2629
- });
2630
- });
2631
- for (const [type, handler] of Object.entries(options.on ?? {})) if (typeof handler === "function") actor.on(type, handler);
2632
- const sessionApi = { settled: async () => {
2633
- const result = settled && lastResult !== void 0 ? lastResult : await new Promise((resolve) => {
2634
- waiters.push(resolve);
2635
- });
2636
- await journalAtSettle;
2637
- return result;
2638
- } };
2639
- if (options.signal) {
2640
- if (options.signal.aborted) {
2641
- settle({
2642
- status: "error",
2643
- cause: "aborted",
2644
- error: options.signal.reason ?? /* @__PURE__ */ new Error("Aborted"),
2645
- snapshot: actor.getSnapshot()
2646
- });
2647
- return sessionApi;
2648
- }
2649
- options.signal.addEventListener("abort", onAbort);
2650
- }
2651
- onTrace({
2652
- type: "run.start",
2653
- ...resolvedInput !== void 0 ? { input: resolvedInput } : {},
2654
- ...effectiveSnapshot !== void 0 ? { snapshot: effectiveSnapshot } : {},
2655
- ...resumeEventToSend !== void 0 ? { event: resumeEventToSend } : {}
2656
- });
2657
- actorStarted = true;
2658
- actor.start();
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) {
2681
- deliveringResumeEvent = false;
2682
- actor.send(resumeEventToSend);
2683
- }
2684
- return sessionApi;
2685
- }
2686
- /**
2687
- * Wraps a `(snapshot, actorRef) => void` handler into a function usable as
2688
- * {@link RunAgentOptions.inspect}: it filters the raw inspection stream to
2689
- * `@xstate.transition` events and hands the handler the typed
2690
- * {@link AnyMachineSnapshot} and the {@link InspectedActorRef} that
2691
- * transitioned. Attribute a child actor via `actorRef.id`/`actorRef.src`. Saves
2692
- * the manual `event.type === '@xstate.transition'` filtering and the snapshot/
2693
- * actorRef casts.
2694
- */
2695
- function inspectTransitions(handler) {
2696
- return (inspectionEvent) => {
2697
- if (inspectionEvent.type !== "@xstate.transition") return;
2698
- handler(inspectionEvent.snapshot, inspectionEvent.actorRef);
2699
- };
2700
- }
2701
- /**
2702
- * An xstate `inspect` handler that emits `machine.transition` trace events onto
2703
- * `onTrace`, sharing the SAME versioned envelope and per-root-actor `seq`
2704
- * registry as {@link provideExecutors}' `onTrace`. Pair the two on one actor to
2705
- * get a single ordered trace stream (request + transition events) for the
2706
- * uncontrolled path:
2707
- *
2708
- * ```ts
2709
- * const bound = provideExecutors(machine, executors, { onTrace });
2710
- * const actor = createActor(bound, { inspect: traceTransitions(onTrace) });
2711
- * ```
2712
- *
2713
- * Only ROOT-actor transitions are traced (matching `runAgent`'s
2714
- * `machine.transition`); child-actor transitions are ignored. Attribute the
2715
- * event via its envelope `runId`.
2716
- *
2717
- * By design this path has NO `run.start`/`run.end` events: `createActor` has no
2718
- * run boundary the way `runAgent` does, so the stream starts at the actor's
2719
- * first transition. It also does NOT emit `emit` trace events: in this xstate
2720
- * build emitted events are delivered through `actor.on(...)`, not the inspection
2721
- * protocol, so they are not observable from an `inspect` handler — subscribe
2722
- * with `actor.on('*', ...)` if you need them.
2723
- */
2724
- function traceTransitions(onTrace) {
2725
- return (inspectionEvent) => {
2726
- if (inspectionEvent.type !== "@xstate.transition") return;
2727
- const actorRef = inspectionEvent.actorRef;
2728
- if (actorRef?._parent) return;
2729
- onTrace(stampRootTrace(actorRef, {
2730
- type: "machine.transition",
2731
- snapshot: inspectionEvent.snapshot,
2732
- event: inspectionEvent.event
2733
- }));
2734
- };
2735
- }
2736
- function isIdleSnapshot(snapshot, { ignoreUserInputChildren }) {
2737
- if (snapshot.status !== "active") return false;
2738
- if (Object.values(snapshot.children ?? {}).some((child) => {
2739
- const ref = child;
2740
- if (ignoreUserInputChildren && ref?.src === "agent.userInput") return false;
2741
- const childSnapshot = ref?.getSnapshot?.();
2742
- if (childSnapshot?.status !== "active") return false;
2743
- if (isMachineSnapshot(childSnapshot)) return !isIdleSnapshot(childSnapshot, { ignoreUserInputChildren });
2744
- return true;
2745
- })) return false;
2746
- return !getNextTransitions(snapshot).some((transitionDef) => transitionDef.eventType === "" || transitionDef.eventType.startsWith("xstate.after"));
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
- }
2766
- function collectPendingUserInputs(snapshot) {
2767
- const pending = [];
2768
- for (const [id, child] of Object.entries(snapshot.children ?? {})) {
2769
- const ref = child;
2770
- if (ref?.src !== "agent.userInput") continue;
2771
- const childSnapshot = ref.getSnapshot?.();
2772
- if (childSnapshot?.status !== "active") continue;
2773
- pending.push({
2774
- id,
2775
- input: childSnapshot.input
2776
- });
2777
- }
2778
- return pending;
2779
- }
2780
- //#endregion
2781
- //#region src/agent-run.ts
2782
- /**
2783
- * Wraps {@link runAgent} in a canonical run-stream handle: the run's trace
2784
- * events as a pull-based {@link AgentRun.events} async iterator, alongside the
2785
- * unchanged {@link AgentRun.result} promise.
2786
- *
2787
- * Semantics:
2788
- *
2789
- * 1. **Starts immediately.** The underlying `runAgent` is invoked on this call,
2790
- * not on the first `events` iteration — work is already in flight when
2791
- * `createAgentRun` returns.
2792
- * 2. **Buffer, don't stall.** Events are queued in an unbounded in-memory
2793
- * buffer as the run emits them; a slow or absent consumer never applies
2794
- * backpressure to the run. Once `run.end` is delivered, the iterator
2795
- * completes.
2796
- * 3. **Composes `onTrace`.** If `options.onTrace` is passed, it still fires for
2797
- * every event — the wrapper adds queue delivery, it does not replace the
2798
- * caller's sink.
2799
- * 4. **`result` mirrors `runAgent`.** Same resolution and rejection behavior
2800
- * (see {@link AgentRun.result}). A no-op rejection handler is attached
2801
- * internally so a caller who reads only `events` never trips an unhandled
2802
- * rejection on a bind-time throw; the returned promise still rejects for a
2803
- * caller who awaits it.
2804
- * 5. **Resume is identical.** Options pass straight through, so starting from a
2805
- * persisted `snapshot` (+ resume `event`) streams that run's events from its
2806
- * own `run.start`, exactly as `runAgent` would run it.
2807
- * 6. **Early termination does not cancel the run.** Breaking out of a
2808
- * `for await` (or calling `events.return()`) stops delivery, but the run
2809
- * keeps going and `result` still settles. Cancelling the run itself is
2810
- * future work — pass `options.signal` to abort it.
2811
- * 7. **`events` is single-consumer.** See {@link AgentRun.events}.
2812
- */
2813
- function createAgentRun(machine, options) {
2814
- const buffer = [];
2815
- const resolvers = [];
2816
- let closed = false;
2817
- let stopped = false;
2818
- const push = (event) => {
2819
- if (closed || stopped) return;
2820
- const resolve = resolvers.shift();
2821
- if (resolve) resolve({
2822
- value: event,
2823
- done: false
2824
- });
2825
- else buffer.push(event);
2826
- };
2827
- const drain = () => {
2828
- while (resolvers.length > 0) resolvers.shift()({
2829
- value: void 0,
2830
- done: true
2831
- });
2832
- };
2833
- const close = () => {
2834
- if (closed) return;
2835
- closed = true;
2836
- drain();
2837
- };
2838
- const userOnTrace = options.onTrace;
2839
- const result = runAgent(machine, {
2840
- ...options,
2841
- onTrace: (event) => {
2842
- push(event);
2843
- userOnTrace?.(event);
2844
- if (event.type === "run.end") close();
2845
- }
2846
- });
2847
- result.then(close, close);
2848
- return {
2849
- events: {
2850
- next() {
2851
- if (buffer.length > 0) return Promise.resolve({
2852
- value: buffer.shift(),
2853
- done: false
2854
- });
2855
- if (closed || stopped) return Promise.resolve({
2856
- value: void 0,
2857
- done: true
2858
- });
2859
- return new Promise((resolve) => {
2860
- resolvers.push(resolve);
2861
- });
2862
- },
2863
- return() {
2864
- stopped = true;
2865
- buffer.length = 0;
2866
- drain();
2867
- return Promise.resolve({
2868
- value: void 0,
2869
- done: true
2870
- });
2871
- },
2872
- [Symbol.asyncIterator]() {
2873
- return this;
2874
- }
2875
- },
2876
- result
2877
- };
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
- }
2997
- //#endregion
2998
- //#region src/provide-executors.ts
2999
- /**
3000
- * Binds a machine's agent actor sources to a set of host `executors` in one
3001
- * call, returning a `machine.provide(...)`-ed copy ready for a plain
3002
- * `createActor(...)` — the uncontrolled-mode counterpart to {@link runAgent}.
3003
- * No run loop, no idle settling: the returned machine drives itself, so
3004
- *
3005
- * ```ts
3006
- * const actor = createActor(provideExecutors(machine, { generateText, decide }), { input });
3007
- * actor.start();
3008
- * ```
3009
- *
3010
- * behaves like a normal XState actor whose agent invokes now reach real models.
3011
- *
3012
- * Binding pass over `machine.sources.actors` (after merging
3013
- * `options.actors`):
3014
- * - `mode: 'generate'` text source → `executors.generateText`
3015
- * - `mode: 'stream'` text source → `executors.streamText`
3016
- * - decision / `agent.decide` source → `executors.decide` (snapshot-driven
3017
- * candidate events, guard `canTake`, and auto-delivery of the chosen event,
3018
- * mirroring `runAgent` but without its model-call counting)
3019
- *
3020
- * Pass `options.onTrace` to observe request-level trace events (identical in
3021
- * shape to `runAgent`'s); pair it with {@link traceTransitions} on the actor's
3022
- * `inspect` to also capture `machine.transition` events in the same stream.
3023
- *
3024
- * A source that already carries its own executor (`.withExecutor(...)`) is left
3025
- * as-is. `agent.userInput` is left UNBOUND — an uncontrolled host handles idle
3026
- * itself, so supply a handler via `options.actors` if the machine uses it.
3027
- * Non-agent actors are untouched.
3028
- *
3029
- * Throws at bind time if a source needs an executor kind that `executors` does
3030
- * not provide.
3031
- *
3032
- * Executor inheritance is RECURSIVE, exactly as in `runAgent`: a string-keyed
3033
- * invoked child machine is rebound too, so its own text/decision requests — at
3034
- * any depth — reach the same host executors. A direct-object invoke `src`
3035
- * cannot be swapped via `.provide`, so nothing under one inherits; bind those
3036
- * with `.withExecutor(...)` or register the child as a string-keyed source. A
3037
- * source that already carries its own executor is never overwritten.
3038
- */
3039
- function provideExecutors(machine, executors, options = {}) {
3040
- const bindOptions = {
3041
- onChunk: options.onChunk,
3042
- onTrace: options.onTrace
3043
- };
3044
- const withActors = (target, actors) => target.provide({ actors });
3045
- const provided = options.actors ? withActors(machine, options.actors) : machine;
3046
- const effectiveSources = provided.sources.actors;
3047
- const wrappedSources = {};
3048
- const invokedSrcs = getConfiguredInvokeSrcs(provided);
3049
- for (const [key, logic] of Object.entries(effectiveSources)) {
3050
- if (key === "agent.userInput") continue;
3051
- if (isStateMachineLogic(logic)) {
3052
- if (invokedSrcs.has(key)) assertChildBindable(logic, executors, key, new Set([provided]));
3053
- const rebound = bindChildMachineForProvide(logic, executors, bindOptions, new Set([provided]));
3054
- if (rebound !== logic) wrappedSources[key] = rebound;
3055
- continue;
3056
- }
3057
- const requirement = executorRequirementOf(logic);
3058
- if (!requirement) continue;
3059
- const binding = {
3060
- ...requirement,
3061
- bind: () => isDecisionLogic(logic) ? bindDecisionForProvide(provided, logic, executors, bindOptions) : bindTextForProvide(provided, logic, executors, bindOptions)
3062
- };
3063
- if (executorBoundLogics.has(logic)) continue;
3064
- if (!executors[binding.executorKey]) {
3065
- if (invokedSrcs.has(key)) throw missingExecutorError(key, binding.kind, binding.executorKey);
3066
- continue;
3067
- }
3068
- wrappedSources[key] = binding.bind();
3069
- }
3070
- return withActors(provided, wrappedSources);
3071
- }
3072
- /** The executor slot + label an agent logic needs, or `undefined` for a non-agent actor. */
3073
- function executorRequirementOf(logic) {
3074
- if (isDecisionLogic(logic)) return {
3075
- executorKey: "decide",
3076
- kind: "decision"
3077
- };
3078
- if (isTextLogic(logic)) return logic.mode === "stream" ? {
3079
- executorKey: "streamText",
3080
- kind: "streaming text"
3081
- } : {
3082
- executorKey: "generateText",
3083
- kind: "text"
3084
- };
3085
- }
3086
- /**
3087
- * Walks an invoked child machine's own invoked sources (recursively, at any
3088
- * depth) and throws the same missing-executor error `provideExecutors` throws
3089
- * for the top-level machine — before any actor starts. Mirrors runAgent's
3090
- * `assertBindable` for the uncontrolled path. Cycle-safe via `visited`.
3091
- */
3092
- function assertChildBindable(childMachine, executors, path, visited) {
3093
- if (visited.has(childMachine)) return;
3094
- const nextVisited = new Set([...visited, childMachine]);
3095
- const sources = childMachine.sources.actors;
3096
- for (const src of getConfiguredInvokeSrcs(childMachine)) {
3097
- if (src === "agent.userInput") continue;
3098
- const logic = sources[src];
3099
- if (!logic) continue;
3100
- if (isStateMachineLogic(logic)) {
3101
- assertChildBindable(logic, executors, `${path} > ${src}`, nextVisited);
3102
- continue;
3103
- }
3104
- const requirement = executorRequirementOf(logic);
3105
- if (!requirement || executorBoundLogics.has(logic)) continue;
3106
- if (!executors[requirement.executorKey]) throw missingExecutorError(`${path} > ${src}`, requirement.kind, requirement.executorKey);
3107
- }
3108
- }
3109
- function missingExecutorError(src, kind, executor) {
3110
- return /* @__PURE__ */ new Error(`provideExecutors: actor source '${src}' is a ${kind} source but no '${executor}' executor was provided. Add it to the executors object, or bind the source with its own executor (logic.withExecutor(...)) before calling provideExecutors.`);
3111
- }
3112
- //#endregion
3113
- //#region src/verify.ts
3114
- const DECIDE_SRC = "agent.decide";
3115
- const USER_INPUT_SRC = "agent.userInput";
3116
- function normalizeInvokes(invoke) {
3117
- if (invoke === void 0 || invoke === null) return [];
3118
- return Array.isArray(invoke) ? invoke : [invoke];
3119
- }
3120
- function buildStateIndex(rootConfig) {
3121
- const index = /* @__PURE__ */ new Map();
3122
- const walk = (states, parentPath) => {
3123
- for (const [name, config] of Object.entries(states ?? {})) {
3124
- const path = parentPath ? `${parentPath}.${name}` : name;
3125
- const hasChildren = !!config.states && Object.keys(config.states).length > 0;
3126
- index.set(path, {
3127
- path,
3128
- config,
3129
- parentPath,
3130
- isFinal: config.type === "final",
3131
- isParallel: config.type === "parallel",
3132
- isCompound: hasChildren && config.type !== "parallel",
3133
- invokes: normalizeInvokes(config.invoke)
3134
- });
3135
- if (hasChildren) walk(config.states, path);
3136
- }
3137
- };
3138
- walk(rootConfig.states, "");
3139
- return index;
3140
- }
3141
- function ancestorChain(node, index) {
3142
- const chain = [];
3143
- let parent = node.parentPath;
3144
- while (parent) {
3145
- const parentNode = index.get(parent);
3146
- if (!parentNode) break;
3147
- chain.push(parentNode);
3148
- parent = parentNode.parentPath;
3149
- }
3150
- return chain;
3151
- }
3152
- function hasNonEmptyOn(config) {
3153
- return !!config.on && Object.keys(config.on).length > 0;
3154
- }
3155
- function isDecisionInvoke(src, actors) {
3156
- if (typeof src === "string") return src === DECIDE_SRC || isDecisionLogic(actors[src]);
3157
- return isDecisionLogic(src);
3158
- }
3159
- function isAgentLogicNeedingBinding(src) {
3160
- return (isTextLogic(src) || isDecisionLogic(src)) && !executorBoundLogics.has(src);
3161
- }
3162
- function checkDecideWithoutEvents(ctx) {
3163
- const out = [];
3164
- for (const node of ctx.index.values()) for (const invoke of node.invokes) {
3165
- if (!isDecisionInvoke(invoke.src, ctx.actors)) continue;
3166
- const selfHandles = hasNonEmptyOn(node.config);
3167
- const ancestorHandles = ancestorChain(node, ctx.index).some((ancestor) => hasNonEmptyOn(ancestor.config));
3168
- const invokeObserves = invoke.onDone !== void 0;
3169
- if (selfHandles || ancestorHandles || invokeObserves) continue;
3170
- const srcName = typeof invoke.src === "string" ? invoke.src : "(inline logic)";
3171
- out.push({
3172
- code: "decide-without-events",
3173
- severity: "error",
3174
- path: node.path,
3175
- message: `State '${node.path}' invokes decision source '${srcName}', but neither it nor any ancestor handles any event (no 'on:'), so the decision's chosen event can never be delivered. Add an 'on:' handler for the candidate events.`
3176
- });
3177
- }
3178
- return out;
3179
- }
3180
- function checkDirectObjectSrc(ctx) {
3181
- const out = [];
3182
- for (const node of ctx.index.values()) for (const invoke of node.invokes) {
3183
- const src = invoke.src;
3184
- if (typeof src === "string" || !src || typeof src !== "object") continue;
3185
- if (!isAgentLogicNeedingBinding(src)) continue;
3186
- out.push({
3187
- code: "direct-object-src",
3188
- severity: "warning",
3189
- path: node.path,
3190
- message: `State '${node.path}' invokes a direct-object agent logic. Direct-object invoke srcs cannot be rebound by runAgent, so they inherit no host executors — call '.withExecutor(...)' on the logic, or register it as a string-keyed actor source (machine.provide({ actors: { name: logic } })) and invoke it by name.`
3191
- });
3192
- }
3193
- return out;
3194
- }
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"));
3198
- }
3199
- function checkInvokeWithoutOnError(ctx) {
3200
- const out = [];
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)";
3205
- out.push({
3206
- code: "invoke-without-on-error",
3207
- severity: "warning",
3208
- path: 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).`
3210
- });
3211
- }
3212
- return out;
3213
- }
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",
3227
- severity: "warning",
3228
- path: "(root)",
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
- }] : [];
3231
- }
3232
- const LINT_CHECKS = [
3233
- checkDecideWithoutEvents,
3234
- checkDirectObjectSrc,
3235
- checkInvokeWithoutOnError,
3236
- checkUnhandledAgentMessages
3237
- ];
3238
- /**
3239
- * Runs static structural checks over a built agent machine and returns the
3240
- * findings ({@link AgentLintDiagnostic}[], empty when clean). Works for
3241
- * TS-authored (`setupAgent(...).createMachine(...)`) and
3242
- * `setupAgent.fromConfig(...)`-compiled machines alike, reading `machine.config`
3243
- * plus the schemas/actor sources the library already retains per machine.
3244
- *
3245
- * No model calls, no API keys — a coding agent that emits an agent machine can
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.
3249
- *
3250
- * Pass `{ throw: true }` for the one-liner form used in tests and generation
3251
- * loops: it returns silently when the machine is clean and throws
3252
- * {@link AgentLintError} (findings on `.diagnostics`) on error-severity
3253
- * findings, or on warnings too with `{ throw: true, warnings: true }`.
3254
- *
3255
- * @example
3256
- * ```ts
3257
- * const errors = lintAgentMachine(machine).filter((d) => d.severity === 'error');
3258
- * if (errors.length) throw new Error(errors.map((e) => `${e.path}: ${e.message}`).join('\n'));
3259
- * ```
3260
- *
3261
- * @example Throwing form
3262
- * ```ts
3263
- * test('agent machine is structurally sound', () => {
3264
- * lintAgentMachine(machine, { throw: true });
3265
- * });
3266
- * ```
3267
- */
3268
- function lintAgentMachine(machine, options = {}) {
3269
- const config = machine.config ?? {};
3270
- const index = buildStateIndex(config);
3271
- const registered = getRegisteredAgentExecutionOptions(machine);
3272
- const ctx = {
3273
- config,
3274
- index,
3275
- schemas: registered.schemas,
3276
- actors: registered.actors ?? machine.sources?.actors ?? {}
3277
- };
3278
- const disabled = new Set(options.disable ?? []);
3279
- const diagnostics = LINT_CHECKS.flatMap((check) => check(ctx)).filter((d) => !disabled.has(d.code));
3280
- if (options.throw) {
3281
- const failing = options.warnings ? diagnostics : diagnostics.filter((d) => d.severity === "error");
3282
- if (failing.length > 0) throw new AgentLintError(machine.id ?? "(machine)", failing);
3283
- }
3284
- return diagnostics;
3285
- }
3286
- /**
3287
- * Thrown by `lintAgentMachine(machine, { throw: true })` (and its
3288
- * {@link assertAgentMachine} alias) when lint finds failing diagnostics.
3289
- * `diagnostics` holds the findings; the message lists them one per finding,
3290
- * so a test runner's failure output reads like the CLI's lint report.
3291
- */
3292
- var AgentLintError = class extends AgentError {
3293
- diagnostics;
3294
- constructor(machineId, diagnostics) {
3295
- const lines = diagnostics.map((d) => ` ${d.severity === "error" ? "error" : "warn "} ${d.code} ${d.path}\n ${d.message}`);
3296
- super("lint-failed", `Agent machine '${machineId}' failed lint (${diagnostics.length} finding(s)):\n${lines.join("\n")}`);
3297
- this.name = "AgentLintError";
3298
- this.diagnostics = diagnostics;
3299
- }
3300
- };
3301
- /**
3302
- * Asserts a machine passes {@link lintAgentMachine}: returns silently when
3303
- * clean, throws {@link AgentLintError} (with the findings on `.diagnostics`)
3304
- * otherwise — sugar for `lintAgentMachine(machine, { ...options, throw: true })`.
3305
- * Fails on error-severity findings; set `warnings: true` to fail on warnings
3306
- * too. The one-liner for tests and generation loops:
3307
- *
3308
- * @example
3309
- * ```ts
3310
- * test('agent machine is structurally sound', () => {
3311
- * assertAgentMachine(machine);
3312
- * });
3313
- * ```
3314
- */
3315
- function assertAgentMachine(machine, options = {}) {
3316
- lintAgentMachine(machine, {
3317
- ...options,
3318
- throw: true
3319
- });
3320
- }
3321
- function pendingInvokes(step) {
3322
- const out = [];
3323
- for (const metadata of getPendingInvokes(step.snapshot)) if (typeof metadata.src === "string") out.push({
3324
- id: metadata.id,
3325
- src: metadata.src
3326
- });
3327
- return out;
3328
- }
3329
- function takeFromQueue(channel, src) {
3330
- const queue = channel?.[src];
3331
- if (queue && queue.length > 0) return {
3332
- found: true,
3333
- value: queue.shift()
3334
- };
3335
- return { found: false };
3336
- }
3337
- /**
3338
- * Deterministically plays a machine through, resolving each request from a
3339
- * {@link SimulationScript} instead of a model — no API keys, no model calls.
3340
- * Runs on the pure step path ({@link initialAgentStep} etc.), so it exercises
3341
- * the real transition logic. Returns the terminal `status`, final `snapshot`,
3342
- * and a `trail` of every step taken.
3343
- *
3344
- * Decisions run through the live run's own validation/retry core,
3345
- * {@link resolveDecision}, with the script standing in for the model: each
3346
- * attempt consumes the next queued {@link ChosenEvent} for that src, so an
3347
- * unknown, payload-invalid, or guard-rejected event is NOT silently swallowed
3348
- * — the next queued decision is tried, exactly as a live run re-asks the
3349
- * model. The decision logic's `maxRetries` caps attempts as it would live;
3350
- * when retries continue past the end of the queue, the last queued decision
3351
- * repeats (a scripted model that insists). The repeat applies only within one
3352
- * decision request's retries — each new decision request must have its own
3353
- * queued entry, or the dry-script error throws as usual. Exhausting all
3354
- * attempts delivers
3355
- * the resulting {@link AgentDecisionExhaustedError} to the machine as the
3356
- * decision invoke's error (so an `onError` transition observes it, as it
3357
- * would live); with no `onError` to catch it, it is thrown.
3358
- *
3359
- * Throws a descriptive error when the script runs dry mid-request, naming the
3360
- * pending request's kind, src, and id so the missing scripted response is
3361
- * obvious.
3362
- *
3363
- * @example
3364
- * ```ts
3365
- * const { status, snapshot } = simulateAgent(machine, {
3366
- * input: { topic: 'state machines' },
3367
- * script: {
3368
- * decisions: { 'agent.decide': [{ type: 'ESCALATE' }] },
3369
- * events: [{ type: 'APPROVE' }], // crosses the human gate
3370
- * },
3371
- * });
3372
- * ```
3373
- */
3374
- async function simulateAgent(machine, options) {
3375
- const maxSteps = options.maxSteps ?? 100;
3376
- const script = {
3377
- text: mapValues(options.script.text ?? {}, (arr) => [...arr]),
3378
- decisions: mapValues(options.script.decisions ?? {}, (arr) => [...arr]),
3379
- invokes: mapValues(options.script.invokes ?? {}, (arr) => [...arr]),
3380
- errors: mapValues(options.script.errors ?? {}, (arr) => [...arr]),
3381
- events: [...options.script.events ?? []]
3382
- };
3383
- if (options.script.userInput?.length) script.invokes[USER_INPUT_SRC] = [...options.script.userInput, ...script.invokes[USER_INPUT_SRC] ?? []];
3384
- let step = initialAgentStep(machine, options.input);
3385
- const trail = [{ state: step.snapshot.value }];
3386
- for (let i = 0; i < maxSteps; i++) {
3387
- if (step.done) return {
3388
- status: "done",
3389
- snapshot: step.snapshot,
3390
- trail
3391
- };
3392
- const request = step.requests[0];
3393
- if (request) {
3394
- if (request.kind === "decision") {
3395
- const invokeMeta = findInvokeMetadata(step, request.id);
3396
- const src = invokeMeta?.src ?? request.id;
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
- }
3408
- step = await applyScriptedDecision(machine, step, request, decisionSrc, invokeMeta?.logic, script, trail);
3409
- continue;
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
- }
3420
- const taken = takeFromQueue(script.text, request.src);
3421
- if (!taken.found) throw scriptDryError("text", request.src, request.id);
3422
- step = resolveAgentStep(machine, step, request, taken.value);
3423
- trail.push({
3424
- state: step.snapshot.value,
3425
- resolvedRequest: {
3426
- kind: "text",
3427
- src: request.src,
3428
- id: request.id,
3429
- outcome: "output"
3430
- }
3431
- });
3432
- continue;
3433
- }
3434
- const [invoke] = pendingInvokes(step);
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
- }
3445
- const taken = takeFromQueue(script.invokes, invoke.src);
3446
- if (!taken.found) throw scriptDryError("userInput", invoke.src, invoke.id);
3447
- step = resolveAgentStep(machine, step, invoke.id, taken.value);
3448
- trail.push({
3449
- state: step.snapshot.value,
3450
- resolvedRequest: {
3451
- kind: "userInput",
3452
- src: invoke.src,
3453
- id: invoke.id,
3454
- outcome: "output"
3455
- }
3456
- });
3457
- continue;
3458
- }
3459
- const external = script.events;
3460
- if (external.length > 0) {
3461
- const event = external.shift();
3462
- if (!step.snapshot.can(event)) throw new Error(`simulateAgent: scripted external event '${event.type}' cannot be taken in state ${JSON.stringify(step.snapshot.value)} (no handler, or its guard rejected it). A live run's send would be silently dropped here; fix the script's \`events\` queue.`);
3463
- step = transitionAgentStep(machine, step, event);
3464
- trail.push({
3465
- state: step.snapshot.value,
3466
- appliedEvent: event,
3467
- external: true
3468
- });
3469
- continue;
3470
- }
3471
- return {
3472
- status: "idle",
3473
- snapshot: step.snapshot,
3474
- trail
3475
- };
3476
- }
3477
- return {
3478
- status: "exhausted",
3479
- snapshot: step.snapshot,
3480
- trail
3481
- };
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
- }
3506
- async function applyScriptedDecision(machine, step, request, decisionSrc, invokeLogic, script, trail) {
3507
- const dequeued = [];
3508
- const decide = async () => {
3509
- const taken = takeFromQueue(script.decisions, decisionSrc);
3510
- if (taken.found) {
3511
- dequeued.push(taken.value);
3512
- return { event: taken.value };
3513
- }
3514
- if (dequeued.length === 0) throw scriptDryError("decision", decisionSrc, request.id, request);
3515
- return { event: dequeued[dequeued.length - 1] };
3516
- };
3517
- const maxRetries = (isDecisionLogic(invokeLogic) ? invokeLogic : resolveRegisteredDecisionLogic(machine, decisionSrc))?.maxRetries;
3518
- let chosen;
3519
- try {
3520
- chosen = await resolveDecision(request, { decide }, {
3521
- maxRetries,
3522
- canTake: (event) => step.snapshot.can(event)
3523
- });
3524
- } catch (error) {
3525
- if (!(error instanceof AgentDecisionExhaustedError)) throw error;
3526
- const next = rejectAgentStep(machine, step, request.id, error);
3527
- if (next.snapshot.status === "error") throw error;
3528
- trail.push({
3529
- state: next.snapshot.value,
3530
- rejectedEvents: dequeued
3531
- });
3532
- return next;
3533
- }
3534
- const next = transitionAgentStep(machine, step, chosen);
3535
- const rejected = dequeued.slice(0, -1);
3536
- trail.push({
3537
- state: next.snapshot.value,
3538
- appliedEvent: chosen,
3539
- ...rejected.length > 0 ? { rejectedEvents: rejected } : {}
3540
- });
3541
- return next;
3542
- }
3543
- function findInvokeMetadata(step, id) {
3544
- for (const metadata of getPendingInvokes(step.snapshot)) {
3545
- if (metadata.id !== id) continue;
3546
- return {
3547
- ...typeof metadata.src === "string" ? { src: metadata.src } : {},
3548
- logic: metadata.logic ?? (typeof metadata.src === "object" ? metadata.src : void 0)
3549
- };
3550
- }
3551
- }
3552
- function resolveRegisteredDecisionLogic(machine, src) {
3553
- const candidate = getRegisteredAgentExecutionOptions(machine).actors?.[src] ?? machine.sources?.actors?.[src];
3554
- return isDecisionLogic(candidate) ? candidate : void 0;
3555
- }
3556
- function mapValues(obj, fn) {
3557
- return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, fn(value)]));
3558
- }
3559
- function scriptDryError(kind, src, id, request) {
3560
- const events = request?.kind === "decision" ? ` Candidate events: ${request.events.map((e) => e.type).join(", ") || "(none)"}.` : "";
3561
- const key = kind === "text" ? `text['${src}']` : kind === "decision" ? `decisions['${src}']` : src === USER_INPUT_SRC ? "userInput" : `invokes['${src}']`;
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}`);
3563
- }
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
- }
3577
- async function explore(machine, options, stopWhen) {
3578
- const maxDepth = options.maxDepth ?? 8;
3579
- const maxPaths = options.maxPaths ?? 200;
3580
- const textScript = options.text ?? {};
3581
- const invokeOutputs = { ...options.invokes ?? {} };
3582
- if ("userInput" in options) invokeOutputs[USER_INPUT_SRC] = options.userInput;
3583
- const errorScript = options.errors ?? {};
3584
- const reachedStates = /* @__PURE__ */ new Set();
3585
- const reachedValues = [];
3586
- const terminals = [];
3587
- const unexplored = [];
3588
- let prunedByGuard = 0;
3589
- let pathsExplored = 0;
3590
- let hitPathCap = false;
3591
- let witness;
3592
- const recordState = (snapshot) => {
3593
- const key = JSON.stringify(snapshot.value);
3594
- if (!reachedStates.has(key)) {
3595
- reachedStates.add(key);
3596
- reachedValues.push(snapshot.value);
3597
- }
3598
- };
3599
- const initial = initialAgentStep(machine, options.input);
3600
- recordState(initial.snapshot);
3601
- if (stopWhen?.(initial.snapshot)) witness = [];
3602
- const advance = (step) => {
3603
- let current = step;
3604
- for (let i = 0; i < MAX_ADVANCE_STEPS; i++) {
3605
- if (stopWhen?.(current.snapshot)) return {
3606
- step: current,
3607
- hit: true
3608
- };
3609
- if (current.done) return { step: current };
3610
- const request = current.requests[0];
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
- };
3616
- if (!(request.src in textScript)) return {
3617
- step: current,
3618
- blockedSrc: request.src
3619
- };
3620
- current = resolveAgentStep(machine, current, request, textScript[request.src]);
3621
- recordState(current.snapshot);
3622
- continue;
3623
- }
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
- }
3637
- const [invoke] = pendingInvokes(current);
3638
- if (invoke) {
3639
- if (invoke.src in errorScript) return {
3640
- step: current,
3641
- fork: invokeFork(invoke.id, invoke.src, invokeOutputs)
3642
- };
3643
- if (!(invoke.src in invokeOutputs)) return {
3644
- step: current,
3645
- blockedSrc: invoke.src
3646
- };
3647
- current = resolveAgentStep(machine, current, invoke.id, invokeOutputs[invoke.src]);
3648
- recordState(current.snapshot);
3649
- continue;
3650
- }
3651
- return { step: current };
3652
- }
3653
- return { step: current };
3654
- };
3655
- const visit = async (step, path, startDepth) => {
3656
- let depth = startDepth;
3657
- if (witness !== void 0) return;
3658
- if (pathsExplored >= maxPaths) {
3659
- hitPathCap = true;
3660
- return;
3661
- }
3662
- const { step: settled, blockedSrc, hit, fork } = advance(step);
3663
- if (hit) {
3664
- witness = path;
3665
- return;
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
- }
3692
- if (blockedSrc) {
3693
- pathsExplored++;
3694
- terminals.push({
3695
- status: "needs-output",
3696
- path,
3697
- state: settled.snapshot.value,
3698
- missingSrc: blockedSrc
3699
- });
3700
- unexplored.push(`needs-output: no canned output for src '${blockedSrc}' at path [${path.map((e) => e.type).join(", ")}]`);
3701
- return;
3702
- }
3703
- if (settled.done) {
3704
- pathsExplored++;
3705
- terminals.push({
3706
- status: "done",
3707
- path,
3708
- state: settled.snapshot.value
3709
- });
3710
- return;
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
- }
3722
- const request = settled.requests[0];
3723
- const branchEvents = request?.kind === "decision" ? request.events.map((descriptor) => ({ type: descriptor.type })) : getAcceptedEvents(settled.snapshot).map((descriptor) => ({ type: descriptor.type }));
3724
- if (branchEvents.length === 0) {
3725
- pathsExplored++;
3726
- terminals.push({
3727
- status: "idle",
3728
- path,
3729
- state: settled.snapshot.value
3730
- });
3731
- return;
3732
- }
3733
- if (depth >= maxDepth) {
3734
- pathsExplored++;
3735
- terminals.push({
3736
- status: "max-depth",
3737
- path,
3738
- state: settled.snapshot.value
3739
- });
3740
- unexplored.push(`max-depth: stopped at path [${path.map((e) => e.type).join(", ")}]`);
3741
- return;
3742
- }
3743
- for (const event of branchEvents) {
3744
- if (witness !== void 0 || pathsExplored >= maxPaths) {
3745
- if (pathsExplored >= maxPaths) hitPathCap = true;
3746
- return;
3747
- }
3748
- if (!settled.snapshot.can(event)) {
3749
- prunedByGuard++;
3750
- continue;
3751
- }
3752
- const next = transitionAgentStep(machine, settled, event);
3753
- recordState(next.snapshot);
3754
- await visit(next, [...path, event], depth + 1);
3755
- }
3756
- };
3757
- if (witness === void 0) await visit(initial, [], 0);
3758
- return {
3759
- report: {
3760
- reachedStates: reachedValues,
3761
- terminals,
3762
- prunedByGuard,
3763
- unexplored,
3764
- pathsExplored,
3765
- hitPathCap
3766
- },
3767
- witness
3768
- };
3769
- }
3770
- /**
3771
- * Enumerates a machine's decision and external-event branches to a bounded
3772
- * depth, model-free, and reports which states are reached and how each path
3773
- * terminates. At each decision request it forks one branch per candidate event
3774
- * (guard-rejected candidates are counted in `prunedByGuard`, not explored); at
3775
- * an idle wait it forks per externally-accepted event. Text requests resolve
3776
- * from `text`, other invokes from `invokes` (or `userInput` for
3777
- * `agent.userInput`) — all by-src canned-output maps, and a missing src halts
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.
3781
- *
3782
- * Combinatorics are bounded by `maxDepth` (default 8) and `maxPaths` (default
3783
- * 200, reported via `hitPathCap`).
3784
- *
3785
- * @example
3786
- * ```ts
3787
- * const report = await explorePaths(refundMachine, { input: { request: 'x', amount: 5000 } });
3788
- * // report.terminals → both 'refunded' and 'denied'; report.prunedByGuard → 1
3789
- * ```
3790
- */
3791
- async function explorePaths(machine, options = {}) {
3792
- return (await explore(machine, options)).report;
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
- }
3812
- /**
3813
- * Answers "can the machine reach this?" by exploring its branches (a thin
3814
- * wrapper over {@link explorePaths}). The target is either a state path string
3815
- * (`snapshot.matches(...)` semantics) or a snapshot predicate — the predicate
3816
- * form checks any property (a context invariant, a tag, a state+context
3817
- * combination) without reifying a sentinel state for it. Returns
3818
- * `{ reachable: true, witness }` with the event sequence that reaches it, or
3819
- * `{ reachable: false }`.
3820
- *
3821
- * @example
3822
- * ```ts
3823
- * const { reachable, witness } = await canReach(refundMachine, 'denied', { input: { request: 'x', amount: 5000 } });
3824
- * // reachable → true; witness → [{ type: 'NEEDS_REVIEW' }, { type: 'DENY' }]
3825
- * ```
3826
- *
3827
- * @example Predicate target — a violation property, no sentinel state needed
3828
- * ```ts
3829
- * const violation = await canReach(
3830
- * refundMachine,
3831
- * (snapshot) => snapshot.matches('issued') && !snapshot.context.approved,
3832
- * { input: { amount: 5000 } },
3833
- * );
3834
- * // violation.reachable → false is the safety proof
3835
- * ```
3836
- */
3837
- async function canReach(machine, target, options = {}) {
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));
3840
- return witness !== void 0 ? {
3841
- reachable: true,
3842
- witness,
3843
- ...resolvedTarget ? { target: resolvedTarget } : {}
3844
- } : {
3845
- reachable: false,
3846
- ...resolvedTarget ? { target: resolvedTarget } : {}
3847
- };
3848
- }
3849
- //#endregion
3850
- //#region src/trajectory.ts
3851
- /**
3852
- * Classifies one item. Event-shaped things (log entries, events) become events;
3853
- * everything else is a state value. A snapshot is unwrapped to its `value`.
3854
- * @internal
3855
- */
3856
- function normalize(item) {
3857
- if (isRecord(item)) {
3858
- const nested = item["event"];
3859
- if (isRecord(nested) && typeof nested["type"] === "string") return {
3860
- kind: "event",
3861
- event: nested
3862
- };
3863
- if (typeof item["type"] === "string") return {
3864
- kind: "event",
3865
- event: item
3866
- };
3867
- if ("value" in item) return {
3868
- kind: "state",
3869
- value: item["value"]
3870
- };
3871
- }
3872
- return {
3873
- kind: "state",
3874
- value: item
3875
- };
3876
- }
3877
- /** Every leaf of a state value as a dot path (`{ review: 'editing' }` → `review.editing`). @internal */
3878
- function statePaths(value) {
3879
- if (typeof value === "string") return [value];
3880
- if (!isRecord(value)) return [];
3881
- return Object.entries(value).flatMap(([key, child]) => statePaths(child).map((path) => `${key}.${path}`));
3882
- }
3883
- /**
3884
- * A string expectation matches a state value when it names one of its leaves or
3885
- * an ancestor of one, so `'review'` matches `{ review: 'editing' }`. @internal
3886
- */
3887
- function stateMatchesPath(path, value) {
3888
- return statePaths(value).some((leaf) => leaf === path || leaf.startsWith(`${path}.`));
3889
- }
3890
- function deepEqual(a, b) {
3891
- if (Object.is(a, b)) return true;
3892
- if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => deepEqual(item, b[index]));
3893
- if (!isRecord(a) || !isRecord(b)) return false;
3894
- const keys = Object.keys(a);
3895
- return keys.length === Object.keys(b).length && keys.every((key) => deepEqual(a[key], b[key]));
3896
- }
3897
- /**
3898
- * Normalizes ONE expected item into a predicate over actual items, so the
3899
- * expectation is classified once instead of once per candidate:
3900
- *
3901
- * - A string expectation matches an event's `type`, or a state value it names.
3902
- * - An event expectation matches an event with the same `type` whose other
3903
- * declared keys deep-equal, so payload details are opt-in.
3904
- * - A state-value expectation matches structurally.
3905
- * @internal
3906
- */
3907
- function matcherFor(expected) {
3908
- if (typeof expected === "string") return (actual) => {
3909
- const actualItem = normalize(actual);
3910
- return actualItem.kind === "event" ? actualItem.event.type === expected : stateMatchesPath(expected, actualItem.value);
3911
- };
3912
- const expectedItem = normalize(expected);
3913
- switch (expectedItem.kind) {
3914
- case "event": return (actual) => {
3915
- const actualItem = normalize(actual);
3916
- return actualItem.kind === "event" && Object.keys(expectedItem.event).every((key) => deepEqual(expectedItem.event[key], actualItem.event[key]));
3917
- };
3918
- case "state": return (actual) => {
3919
- const actualItem = normalize(actual);
3920
- return actualItem.kind === "state" && deepEqual(expectedItem.value, actualItem.value);
3921
- };
3922
- }
3923
- }
3924
- /**
3925
- * Matches a run's trajectory against an expected one.
3926
- *
3927
- * Both trajectories may be state values or events collected from
3928
- * `onTransition`;
3929
- * items are compared by shape, so `['prompting', 'drafting']` scores a state
3930
- * path and `['PROMPT_SUBMITTED', 'SEND']` scores an event log with the same
3931
- * call.
3932
- *
3933
- * Semantics are an ordered subsequence: every expected item must appear in
3934
- * `actual`, in order, with gaps allowed. Pass `{ exact: true }` to require
3935
- * equality instead.
3936
- *
3937
- * @example Trajectory assertions in a test
3938
- * ```ts
3939
- * const statePath: unknown[] = [];
3940
- * const result = await runAgent(machine, {
3941
- * input,
3942
- * executors,
3943
- * onTransition: (snapshot) => statePath.push(snapshot.value),
3944
- * });
3945
- *
3946
- * const path = matchesTrajectory(statePath, ['prompting', 'drafting', 'sent']);
3947
- * expect(path.matched, JSON.stringify(path.firstMiss)).toBe(true);
3948
- * expect(matchesTrajectory(events, ['PROMPT_SUBMITTED', 'SEND']).matched).toBe(true);
3949
- * ```
3950
- *
3951
- * @example Partial credit in an eval scorer
3952
- * ```ts
3953
- * const match = matchesTrajectory(output.statePath, expected.statePath);
3954
- * return { name: 'state_path', score: match.score, metadata: { ...match.firstMiss } };
3955
- * ```
3956
- */
3957
- function matchesTrajectory(actual, expected, options = {}) {
3958
- const expectedCount = expected.length;
3959
- if (expectedCount === 0) throw new AgentError("empty-trajectory", "matchesTrajectory: expected trajectory must contain at least one item.");
3960
- const exact = options.exact === true;
3961
- let cursor = 0;
3962
- let matchedCount = 0;
3963
- let firstMiss;
3964
- for (let index = 0; index < expectedCount; index++) {
3965
- const want = expected[index];
3966
- const matches = matcherFor(want);
3967
- const searchedFrom = exact ? index : cursor;
3968
- let found = -1;
3969
- for (let at = searchedFrom; at < actual.length; at++) {
3970
- if (matches(actual[at])) {
3971
- found = at;
3972
- break;
3973
- }
3974
- if (exact) break;
3975
- }
3976
- if (found === -1) {
3977
- firstMiss = {
3978
- index,
3979
- expected: want,
3980
- searchedFrom
3981
- };
3982
- break;
3983
- }
3984
- matchedCount++;
3985
- cursor = found + 1;
3986
- }
3987
- const matched = !firstMiss && (!exact || actual.length === expectedCount);
3988
- const denominator = exact ? Math.max(expectedCount, actual.length) : expectedCount;
3989
- return {
3990
- matched,
3991
- matchedCount,
3992
- expectedCount,
3993
- score: matched ? 1 : denominator === 0 ? 1 : matchedCount / denominator,
3994
- ...firstMiss ? { firstMiss } : {}
3995
- };
3996
- }
3997
- //#endregion
3998
- //#region src/scripted-executors.ts
3999
- /**
4000
- * Scripted executors — a deterministic stand-in for a model host.
4001
- *
4002
- * `createScriptedExecutors` builds a full `{ generateText, streamText, decide }`
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.
4009
- *
4010
- * @module
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
- }
4035
- /** The only own keys an executor-result envelope carries. @internal */
4036
- const TEXT_ENVELOPE_KEYS = new Set([
4037
- "output",
4038
- "usage",
4039
- "raw"
4040
- ]);
4041
- /**
4042
- * True when a scripted entry is the executor envelope rather than the output
4043
- * value: it owns an `output` key and owns NOTHING outside the envelope's own
4044
- * vocabulary. Bare `'output' in value` would swallow an output object's
4045
- * siblings (`{ output: 'draft', confidence: 0.9 }` would lose `confidence`)
4046
- * and would also match an inherited `output`. @internal
4047
- */
4048
- function isTextEnvelope(value) {
4049
- return Object.hasOwn(value, "output") && Object.keys(value).every((key) => TEXT_ENVELOPE_KEYS.has(key));
4050
- }
4051
- /**
4052
- * Resolves ONE scripted text entry to an executor result: a function entry is
4053
- * called with the request, and the value is taken as the raw envelope only when
4054
- * it is one (see {@link isTextEnvelope}). Shared with `runSeam`, whose routed
4055
- * queues follow the same entry conventions. @internal
4056
- */
4057
- async function resolveScriptedTextEntry(entry, request, info) {
4058
- const value = typeof entry === "function" ? await entry(request, info) : entry;
4059
- return isRecord(value) && isTextEnvelope(value) ? value : { output: value };
4060
- }
4061
- /**
4062
- * Names a pending text request in an error message. Shared with `runSeam`.
4063
- * @internal
4064
- */
4065
- function describeText(request) {
4066
- return request.name ? `'${request.name}' (model '${request.model}')` : `(model '${request.model}')`;
4067
- }
4068
- /**
4069
- * Stream semantics with no model: the whole text lands as one chunk. Shared
4070
- * with `runSeam`, whose scripted answers stream the same way. @internal
4071
- */
4072
- function emitScriptedChunk(result, info) {
4073
- const output = isRecord(result) ? result["output"] : void 0;
4074
- if (typeof output === "string") info?.onChunk?.(output);
4075
- }
4076
- /**
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.
4086
- *
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.
4090
- *
4091
- * @example
4092
- * ```ts
4093
- * const result = await runAgent(moderationMachine, {
4094
- * input: { comment: 'honestly this update is terrible', trust: 20 },
4095
- * executors: createScriptedExecutors({
4096
- * decisions: { moderate: [{ type: 'FLAG', reason: 'Borderline tone.' }] },
4097
- * }),
4098
- * });
4099
- * ```
4100
- *
4101
- * @example Dynamic entries
4102
- * ```ts
4103
- * createScriptedExecutors({
4104
- * text: { draft: (request) => `Draft about ${request.prompt}` },
4105
- * decisions: { route: (request) => ({ type: request.events[0]!.type }) },
4106
- * });
4107
- * ```
4108
- *
4109
- * @example Scripted human input
4110
- * ```ts
4111
- * const scripted = createScriptedExecutors({ userInput: ['ship it'] });
4112
- * await runAgent(machine, { executors: scripted, userInput: scripted.userInput });
4113
- * ```
4114
- */
4115
- function createScriptedExecutors(script = {}) {
4116
- const decisions = toQueues(script.decisions);
4117
- const text = toQueues(script.text);
4118
- const userInput = [...script.userInput ?? []];
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));
4136
- };
4137
- return {
4138
- calls,
4139
- userInput: async (input) => {
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)];
4148
- return typeof entry === "function" ? await entry(input) : entry;
4149
- },
4150
- generateText: (request, info) => nextText("generateText", request, info),
4151
- streamText: async (request, info) => {
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);
4167
- emitScriptedChunk(result, info);
4168
- return result;
4169
- },
4170
- decide: async (request) => {
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);
4180
- const value = typeof entry === "function" ? await entry(request) : entry;
4181
- return withDefaultUsage(isRecord(value) && typeof value["type"] !== "string" && "event" in value ? value : { event: value });
4182
- }
4183
- };
4184
- }
4185
- //#endregion
4186
- //#region src/seam.ts
4187
- /**
4188
- * The seam's own answer, for scoring. Our `{ output }` envelope is read
4189
- * directly; a raw AI SDK `generateText` result contributes its `text`. A raw
4190
- * STREAM result is left alone — the machine consumes that stream, and reading
4191
- * it here would steal the chunks — so `seamOutput` is `undefined` for a
4192
- * streaming candidate that returns one. Score its trajectory instead.
4193
- * @internal
4194
- */
4195
- async function seamOutputOf(result, request) {
4196
- if (!isRecord(result)) return;
4197
- if ("output" in result) return await result["output"];
4198
- if ("textStream" in result) return;
4199
- return await normalizeGeneratorResult(result, `seam '${request.name ?? request.model}'`, { request });
4200
- }
4201
- /**
4202
- * Runs a machine end to end with one model call under test, and slices the run
4203
- * at that call.
4204
- *
4205
- * The `before`/`after` slices are the point: `after.statePath` is the branch
4206
- * the seam chose and `after.events` is the same question against the durable
4207
- * log, both ready for `matchesTrajectory`. The state slice splits where the
4208
- * state path stood when the seam answered; the event slice splits at the
4209
- * seam's own effect completion (the first `xstate.done.*`/`xstate.error.*`
4210
- * entry appended after the call was made).
4211
- *
4212
- * @example No API key: the seam is scripted too, so the whole thing runs offline.
4213
- * ```ts
4214
- * const run = await runSeam(emailDrafter, {
4215
- * scripts: { promptEvaluator: [vague, complete], emailDrafter: [draft] },
4216
- * seam: { request: 'evaluatePrompt' },
4217
- * respond: ({ state }) => (state === 'prompting' ? { type: 'PROMPT_SUBMITTED', prompt } : null),
4218
- * });
4219
- *
4220
- * matchesTrajectory(run.after.statePath, ['needsMoreInfo', 'drafting']);
4221
- * ```
4222
- *
4223
- * @example A candidate prompt at the seam, scored against the same rows.
4224
- * ```ts
4225
- * const { generateText } = createAiSdkExecutors({ models });
4226
- * const run = await runSeam(emailDrafter, { ...row, candidate: generateText });
4227
- * ```
4228
- */
4229
- async function runSeam(machine, options) {
4230
- const { seam, candidate } = options;
4231
- const queues = new Map(Object.entries(options.scripts ?? {}).map(([key, answers]) => [key, [...answers]]));
4232
- const statePath = [];
4233
- let calls = 0;
4234
- let seamMatches = 0;
4235
- let seamOutput;
4236
- let seamUsage;
4237
- const callLog = [];
4238
- let seamReached = false;
4239
- let callsBeforeSeam = -1;
4240
- let seamStateAt = 0;
4241
- let seamEventAt = 0;
4242
- let liveEvents = 0;
4243
- const queueKeyOf = (request) => request.name !== void 0 && queues.has(request.name) ? request.name : request.model;
4244
- /**
4245
- * Consumes this request's slot in the call plan, or resolves `undefined` when
4246
- * its queue is dry. With `repeatLast`, the last entry is replayed instead of
4247
- * running dry.
4248
- */
4249
- const takeScriptedSlot = async (request, info) => {
4250
- const queue = queues.get(queueKeyOf(request));
4251
- if (!queue?.length) return;
4252
- return resolveScriptedTextEntry(options.repeatLast && queue.length === 1 ? queue[0] : queue.shift(), request, info);
4253
- };
4254
- const scriptedAnswer = async (request, info) => {
4255
- const scripted = await takeScriptedSlot(request, info);
4256
- if (!scripted) throw new AgentError("seam-script-exhausted", `runSeam: no scripted answer left for request ${describeText(request)}. Add an entry to \`scripts.${queueKeyOf(request)}\`, or pass \`repeatLast: true\` to replay its last entry down a longer branch.`);
4257
- return scripted;
4258
- };
4259
- const route = async (request, info) => {
4260
- const callIndex = calls++;
4261
- const isSeam = request.name === seam.request && seamMatches++ === (seam.occurrence ?? 0);
4262
- const ledger = (source) => {
4263
- callLog.push({
4264
- ...request.name !== void 0 ? { name: request.name } : {},
4265
- model: request.model,
4266
- key: queueKeyOf(request),
4267
- source,
4268
- seam: isSeam
4269
- });
4270
- };
4271
- if (isSeam && candidate) {
4272
- await takeScriptedSlot(request, info);
4273
- seamReached = true;
4274
- callsBeforeSeam = callIndex;
4275
- seamStateAt = statePath.length;
4276
- seamEventAt = liveEvents;
4277
- const result = await candidate(request, info);
4278
- seamOutput = await seamOutputOf(result, request);
4279
- seamUsage = getCallUsage(result);
4280
- ledger("candidate");
4281
- return result;
4282
- }
4283
- const scripted = await scriptedAnswer(request, info);
4284
- ledger("script");
4285
- if (!isSeam) return scripted;
4286
- seamReached = true;
4287
- callsBeforeSeam = callIndex;
4288
- seamStateAt = statePath.length;
4289
- seamEventAt = liveEvents;
4290
- seamOutput = scripted.output;
4291
- seamUsage = scripted.usage;
4292
- return scripted;
4293
- };
4294
- const executors = {
4295
- ...options.executors,
4296
- generateText: route,
4297
- streamText: async (request, info) => {
4298
- const result = await route(request, info);
4299
- emitScriptedChunk(result, info);
4300
- return result;
4301
- }
4302
- };
4303
- let snapshot;
4304
- let event;
4305
- const events = [];
4306
- let result;
4307
- const maxTurns = options.maxTurns ?? 12;
4308
- for (let turn = 0; turn <= maxTurns; turn++) {
4309
- result = await runAgent(machine, {
4310
- ...snapshot ? { snapshot } : { input: options.input },
4311
- ...event ? { event } : {},
4312
- ...options.actors ? { actors: options.actors } : {},
4313
- executors,
4314
- onTransition: (next, causedBy) => {
4315
- if (turn > 0 && causedBy.type === "@xstate.init") return;
4316
- events.push(causedBy);
4317
- liveEvents = events.length;
4318
- statePath.push(next.value);
4319
- }
4320
- });
4321
- if (result.status !== "idle" || turn === maxTurns) break;
4322
- const next = options.respond?.({
4323
- snapshot: result.snapshot,
4324
- state: result.snapshot.value,
4325
- meta: getStateMeta(result.snapshot),
4326
- turn,
4327
- result
4328
- });
4329
- if (!next) break;
4330
- event = next;
4331
- snapshot = result.persist();
4332
- }
4333
- let splitAt = events.length;
4334
- if (seamReached) {
4335
- splitAt = seamEventAt;
4336
- for (let index = seamEventAt; index < events.length; index++) {
4337
- const type = events[index].type;
4338
- if (type.startsWith("xstate.done") || type.startsWith("xstate.error")) {
4339
- splitAt = index;
4340
- break;
4341
- }
4342
- }
4343
- }
4344
- const splitStateAt = seamReached ? seamStateAt : statePath.length;
4345
- return {
4346
- result,
4347
- seamOutput,
4348
- ...seamUsage !== void 0 ? { seamUsage } : {},
4349
- callsBeforeSeam,
4350
- calls: callLog,
4351
- before: {
4352
- statePath: statePath.slice(0, splitStateAt),
4353
- events: events.slice(0, splitAt)
4354
- },
4355
- after: {
4356
- statePath: statePath.slice(splitStateAt),
4357
- events: events.slice(splitAt)
4358
- }
4359
- };
545
+ function missingExecutorError(src, kind, executor) {
546
+ return /* @__PURE__ */ new Error(`provideExecutors: actor source '${src}' is a ${kind} source but no '${executor}' executor was provided. Add it to the executors object, or bind the source with its own executor (logic.withExecutor(...)) before calling provideExecutors.`);
4360
547
  }
4361
548
  //#endregion
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 };
549
+ export { AGENT_TRACE_SCHEMA_VERSION, AGENT_USAGE_EVENT_TYPE, AgentDecisionExhaustedError, AgentError, AgentEventLogConflictError, AgentInvalidEventPayloadError, AgentMachineVersionMismatchError, AgentMaxModelCallsExceededError, AgentSnapshotDivergedError, AgentTruncatedError, assistantMessage, bindRequestExecutor, createAgentSchemas, createDecisionLogic, createDecisionRequest, createTextLogic, eventFromInteraction, executeAgentRequest, getAcceptedEvents, getAgentOutputMode, getAgentSchemas, getCallFinishReason, getCallUsage, getInteraction, getJsonSchema, getJsonSchemaSync, getMachineStructuralHash, getMessageText, getStateMeta, getStatePath, initialAgentStep, inspectTransitions, interactionMetaSchema, isAgentIdle, isAgentMessages, isStandardSchema, messagesSchema, parseAgentEvent, parseOutput, parseProviderOutput, provideExecutors, providerOutputSchema, rejectAgentStep, renderDecisionAttempts, resolveAgentStep, resolveDecision, runAgent, runAgentStream, serializeTraceEvent, setupAgent, systemMessage, toolMessage, traceTransitions, transitionAgentStep, userMessage };