@statelyai/agent 2.0.0-alpha.10 → 2.0.0-alpha.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-sdk.cjs +62 -99
- package/dist/ai-sdk.d.cts +10 -117
- package/dist/ai-sdk.d.mts +10 -117
- package/dist/ai-sdk.mjs +63 -91
- package/dist/{decision-CQdrKc8k.mjs → decision-BezSD_YC.mjs} +120 -32
- package/dist/{decision-b-lkcs4L.cjs → decision-dWGhBh0P.cjs} +141 -53
- package/dist/errors-BQRk9eiZ.d.cts +19 -0
- package/dist/errors-C9rxnWbX.d.mts +19 -0
- package/dist/errors-CeSXQx0v.mjs +23 -0
- package/dist/errors-DUBBzRLP.cjs +28 -0
- package/dist/event-log-store-CNT_7F0V.cjs +452 -0
- package/dist/event-log-store-CriMgX1D.d.mts +144 -0
- package/dist/event-log-store-D7pWtIhb.mjs +411 -0
- package/dist/event-log-store-Ruq18mGp.d.cts +144 -0
- package/dist/index.cjs +2844 -30
- package/dist/index.d.cts +1005 -12
- package/dist/index.d.mts +1005 -12
- package/dist/index.mjs +2799 -4
- package/dist/machines.cjs +752 -0
- package/dist/machines.d.cts +372 -0
- package/dist/machines.d.mts +372 -0
- package/dist/machines.mjs +741 -0
- package/dist/otel.cjs +268 -0
- package/dist/otel.d.cts +67 -0
- package/dist/otel.d.mts +67 -0
- package/dist/otel.mjs +267 -0
- package/dist/run-agent-B1nBa7aV.d.mts +1111 -0
- package/dist/run-agent-qkg0evvW.d.cts +1111 -0
- package/dist/setup-agent-DAZZSjDS.mjs +1711 -0
- package/dist/setup-agent-DP95MFrI.cjs +1836 -0
- package/dist/sqlite.cjs +135 -0
- package/dist/sqlite.d.cts +57 -0
- package/dist/sqlite.d.mts +57 -0
- package/dist/sqlite.mjs +133 -0
- package/dist/{text-logic-C7WJpCIc.d.mts → text-logic-CorGzucG.d.cts} +110 -66
- package/dist/{text-logic-CZjyACzQ.d.cts → text-logic-DjR5PUVz.d.mts} +110 -66
- package/dist/{types-C9QiMjre.d.cts → types-QbEfCVny.d.cts} +1 -1
- package/dist/{types-qm00QF91.d.mts → types-_FXoFBGO.d.mts} +1 -1
- package/package.json +55 -27
- package/readme.md +40 -9
- package/schemas/agent-workflow.json +44 -26
- package/skills/generate-machine/SKILL.md +267 -0
- package/dist/cli.cjs +0 -64
- package/dist/cli.d.cts +0 -1
- package/dist/cli.d.mts +0 -1
- package/dist/cli.mjs +0 -65
- package/dist/openai-compat.cjs +0 -319
- package/dist/openai-compat.d.cts +0 -98
- package/dist/openai-compat.d.mts +0 -98
- package/dist/openai-compat.mjs +0 -312
- package/dist/run-agent-BFMkuI1F.cjs +0 -1599
- package/dist/run-agent-BVMd--1l.d.cts +0 -1103
- package/dist/run-agent-BoAOq_0Z.mjs +0 -1540
- package/dist/run-agent-DzYJb3CK.d.mts +0 -1103
- package/dist/src-D-77Ha5p.cjs +0 -1087
- package/dist/src-DUeIFjv-.mjs +0 -1016
- package/dist/utils-Dri7aeEG.d.cts +0 -108
- package/dist/utils-Y6GDRGGE.d.mts +0 -108
- package/dist/zod.cjs +0 -31
- package/dist/zod.d.cts +0 -30
- package/dist/zod.d.mts +0 -30
- package/dist/zod.mjs +0 -30
package/dist/index.cjs
CHANGED
|
@@ -1,59 +1,2873 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
const require_errors = require("./errors-DUBBzRLP.cjs");
|
|
3
|
+
const require_setup_agent = require("./setup-agent-DP95MFrI.cjs");
|
|
4
|
+
const require_decision = require("./decision-dWGhBh0P.cjs");
|
|
5
|
+
const require_event_log_store = require("./event-log-store-CNT_7F0V.cjs");
|
|
6
|
+
let xstate = require("xstate");
|
|
7
|
+
//#region src/internal/state-request-pass.ts
|
|
8
|
+
async function runTextPhase(stateRequest, baseMessages, deps) {
|
|
9
|
+
const { model, system } = stateRequest;
|
|
10
|
+
const id = stateRequest.id ?? deps.nextRequestId();
|
|
11
|
+
const promptMessage = require_decision.userMessage(stateRequest.prompt);
|
|
12
|
+
if (stateRequest.kind === "decision") return {
|
|
13
|
+
stateRequest,
|
|
14
|
+
id,
|
|
15
|
+
output: void 0,
|
|
16
|
+
appended: [promptMessage]
|
|
17
|
+
};
|
|
18
|
+
if (!deps.generateText) throw new Error("runAgent: a getRequests request needs a 'generateText' executor (or use kind: 'decision').");
|
|
19
|
+
const request = {
|
|
20
|
+
model,
|
|
21
|
+
...system !== void 0 ? { system } : {},
|
|
22
|
+
messages: [...baseMessages, promptMessage],
|
|
23
|
+
tools: {}
|
|
24
|
+
};
|
|
25
|
+
const agentRequest = {
|
|
26
|
+
kind: "text",
|
|
27
|
+
id,
|
|
28
|
+
src: require_decision.INTERPRET_SOURCE,
|
|
29
|
+
mode: "generate",
|
|
30
|
+
input: request,
|
|
31
|
+
tools: {},
|
|
32
|
+
events: []
|
|
33
|
+
};
|
|
34
|
+
deps.consumeModelCall();
|
|
35
|
+
deps.onTrace?.({
|
|
36
|
+
type: "request.start",
|
|
37
|
+
request: agentRequest
|
|
38
|
+
});
|
|
39
|
+
let output;
|
|
40
|
+
try {
|
|
41
|
+
const raw = await deps.generateText(request, { signal: deps.signal });
|
|
42
|
+
output = await require_decision.normalizeGeneratorResult(raw, id, { request });
|
|
43
|
+
const rawReasoning = raw?.reasoning;
|
|
44
|
+
const reasoning = typeof rawReasoning === "string" ? rawReasoning : void 0;
|
|
45
|
+
const usage = require_decision.extractCallUsage(raw);
|
|
46
|
+
if (usage) deps.recordUsage?.(usage);
|
|
47
|
+
deps.onResult?.(agentRequest, {
|
|
48
|
+
output,
|
|
49
|
+
raw
|
|
50
|
+
});
|
|
51
|
+
deps.onTrace?.({
|
|
52
|
+
type: "request.end",
|
|
53
|
+
request: agentRequest,
|
|
54
|
+
output,
|
|
55
|
+
raw,
|
|
56
|
+
...reasoning !== void 0 ? { reasoning } : {},
|
|
57
|
+
...usage !== void 0 ? { usage } : {}
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
deps.onTrace?.({
|
|
61
|
+
type: "request.error",
|
|
62
|
+
request: agentRequest,
|
|
63
|
+
error
|
|
64
|
+
});
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
stateRequest,
|
|
69
|
+
id,
|
|
70
|
+
output,
|
|
71
|
+
appended: [promptMessage, require_decision.assistantMessage(typeof output === "string" ? output : JSON.stringify(output))]
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async function runAdvancePhase(plan, deps) {
|
|
75
|
+
const { stateRequest, id, output } = plan;
|
|
76
|
+
const { model, system } = stateRequest;
|
|
77
|
+
deps.appendToLog(...plan.appended);
|
|
78
|
+
if (stateRequest.kind !== "decision" && stateRequest.onDone !== void 0) {
|
|
79
|
+
const snapshot = deps.getSnapshot();
|
|
80
|
+
const resolved = typeof stateRequest.onDone === "function" ? stateRequest.onDone({
|
|
81
|
+
output,
|
|
82
|
+
snapshot,
|
|
83
|
+
messages: deps.messages
|
|
84
|
+
}) : stateRequest.onDone;
|
|
85
|
+
if (!resolved) return false;
|
|
86
|
+
const acceptedTypes = require_decision.getAcceptedEvents(snapshot, { schemas: deps.schemas }).map((descriptor) => descriptor.type);
|
|
87
|
+
if (!acceptedTypes.includes(resolved.type)) throw new Error(`runAgent: getRequests request '${id}' resolved onDone to event '${resolved.type}', which the current state does not accept. Accepted: ${acceptedTypes.join(", ") || "(none)"}.`);
|
|
88
|
+
if (!snapshot.can(resolved)) return false;
|
|
89
|
+
deps.send(resolved);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
const events = require_decision.getAcceptedEvents(deps.getSnapshot(), {
|
|
93
|
+
schemas: deps.schemas,
|
|
94
|
+
...stateRequest.allowedEvents ? { eventTypes: stateRequest.allowedEvents } : {}
|
|
95
|
+
});
|
|
96
|
+
if (events.length === 0) return false;
|
|
97
|
+
if (!deps.decide) throw new Error(`runAgent: a getRequests request without 'onDone' needs a 'decide' executor to choose between events ${events.map((descriptor) => `'${descriptor.type}'`).join(", ")}. Provide request.onDone for deterministic advancement, or a 'decide' executor.`);
|
|
98
|
+
const chosen = await require_decision.resolveDecision({
|
|
99
|
+
kind: "decision",
|
|
100
|
+
id,
|
|
101
|
+
model,
|
|
102
|
+
...system !== void 0 ? { system } : {},
|
|
103
|
+
messages: [...deps.messages],
|
|
104
|
+
events,
|
|
105
|
+
attempts: []
|
|
106
|
+
}, deps.decide, {
|
|
107
|
+
signal: deps.signal,
|
|
108
|
+
canTake: (event) => deps.getSnapshot().can(event)
|
|
109
|
+
});
|
|
110
|
+
if (deps.isSettled()) return false;
|
|
111
|
+
deps.appendToLog(require_decision.assistantMessage(`[chose: ${chosen.type}]`));
|
|
112
|
+
deps.send(chosen);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Executes one getRequests pass: all text calls concurrently against the
|
|
117
|
+
* pass-start log, then per-request advancement sequentially in request
|
|
118
|
+
* order. Returns whether ANY request sent an event — the host settles idle
|
|
119
|
+
* when none did (otherwise an unchanged snapshot would re-produce the same
|
|
120
|
+
* pass forever). Stops early (skipping remaining appends/sends) once
|
|
121
|
+
* {@link StateRequestPassDeps.isSettled} reports the run is over. Throws on
|
|
122
|
+
* executor/`onDone` errors — the host maps them to its error settle.
|
|
123
|
+
*/
|
|
124
|
+
async function runStateRequestPass(requests, deps) {
|
|
125
|
+
const baseMessages = [...deps.messages];
|
|
126
|
+
const plans = await Promise.all(requests.map((stateRequest) => runTextPhase(stateRequest, baseMessages, deps)));
|
|
127
|
+
let sentAny = false;
|
|
128
|
+
for (const plan of plans) {
|
|
129
|
+
if (deps.isSettled()) break;
|
|
130
|
+
sentAny = await runAdvancePhase(plan, deps) || sentAny;
|
|
131
|
+
}
|
|
132
|
+
return { sentAny };
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/run-agent.ts
|
|
136
|
+
/**
|
|
137
|
+
* Thrown by {@link runAgent} when resuming with a `snapshot` + `event` whose
|
|
138
|
+
* `type` the restored state cannot accept (a type-level check via
|
|
139
|
+
* {@link getAcceptedEvents}). A programmer/integration error, in the same
|
|
140
|
+
* class as runAgent's bind-time throws — it throws rather than settling an
|
|
141
|
+
* `error` result. A type-legal event a guard rejects is NOT this error (the
|
|
142
|
+
* machine simply takes no transition). Opt out with
|
|
143
|
+
* {@link RunAgentOptions.onIllegalResumeEvent} `'ignore'`.
|
|
144
|
+
*/
|
|
145
|
+
var AgentIllegalResumeEventError = class extends require_errors.AgentError {
|
|
146
|
+
eventType;
|
|
147
|
+
acceptedTypes;
|
|
148
|
+
constructor(eventType, acceptedTypes) {
|
|
149
|
+
super("illegal-resume-event", `runAgent: cannot resume with event '${eventType}' — the restored state does not accept it. Accepted event types: ${acceptedTypes.length > 0 ? acceptedTypes.join(", ") : "(none)"}.`);
|
|
150
|
+
this.name = "AgentIllegalResumeEventError";
|
|
151
|
+
this.eventType = eventType;
|
|
152
|
+
this.acceptedTypes = acceptedTypes;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Thrown by {@link runAgent} when resuming from a `snapshot` whose stamped
|
|
157
|
+
* `agentMeta.version` differs from the current machine's version, under the
|
|
158
|
+
* default `onVersionMismatch: 'throw'` and with no `migrateSnapshot` hook. The
|
|
159
|
+
* structural fingerprint of the machine changed since the snapshot was
|
|
160
|
+
* persisted (a state/transition/invoke was added, removed, or retargeted), so
|
|
161
|
+
* the snapshot may no longer resume cleanly. `from` is the snapshot's version,
|
|
162
|
+
* `to` the current machine's.
|
|
163
|
+
*/
|
|
164
|
+
var AgentSnapshotVersionMismatchError = class extends require_errors.AgentError {
|
|
165
|
+
from;
|
|
166
|
+
to;
|
|
167
|
+
machineId;
|
|
168
|
+
constructor(from, to, machineId) {
|
|
169
|
+
super("snapshot-version-mismatch", `runAgent: cannot resume snapshot stamped with machine version '${from}' against machine '${machineId}' at version '${to}' — the machine's structure changed since the snapshot was persisted. Provide options.migrateSnapshot to adapt it, or set options.onVersionMismatch to 'warn'/'ignore' to proceed anyway.`);
|
|
170
|
+
this.name = "AgentSnapshotVersionMismatchError";
|
|
171
|
+
this.from = from;
|
|
172
|
+
this.to = to;
|
|
173
|
+
this.machineId = machineId;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Thrown by {@link generateResult} when the run settles `idle` instead of
|
|
178
|
+
* `done`: the machine paused for external input. Carries the idle `snapshot`
|
|
179
|
+
* and `acceptedTypes` (the event types that could resume it, via
|
|
180
|
+
* {@link getAcceptedEvents}). Use {@link runAgent} directly when idle is an
|
|
181
|
+
* expected outcome you handle.
|
|
182
|
+
*/
|
|
183
|
+
var AgentIdleError = class extends require_errors.AgentError {
|
|
184
|
+
snapshot;
|
|
185
|
+
acceptedTypes;
|
|
186
|
+
constructor(snapshot, acceptedTypes) {
|
|
187
|
+
super("agent-idle", `generateResult: the machine paused (idle) instead of completing. Resume it by calling runAgent with one of these events: ${acceptedTypes.length > 0 ? acceptedTypes.join(", ") : "(none)"}.`);
|
|
188
|
+
this.name = "AgentIdleError";
|
|
189
|
+
this.snapshot = snapshot;
|
|
190
|
+
this.acceptedTypes = acceptedTypes;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* The version of the {@link AgentTraceEvent} envelope every trace event carries
|
|
195
|
+
* as `schemaVersion`. Bumped only on a breaking change to the envelope or any
|
|
196
|
+
* payload shape, so a consumer can gate on it. Emitted identically by
|
|
197
|
+
* {@link runAgent}, {@link provideExecutors}' `onTrace`, and
|
|
198
|
+
* {@link traceTransitions}.
|
|
199
|
+
*/
|
|
200
|
+
const AGENT_TRACE_SCHEMA_VERSION = 1;
|
|
201
|
+
const TRACE_ENVELOPE_KEYS = [
|
|
202
|
+
"schemaVersion",
|
|
203
|
+
"runId",
|
|
204
|
+
"seq",
|
|
205
|
+
"timestamp",
|
|
206
|
+
"machineId",
|
|
207
|
+
"machineVersion",
|
|
208
|
+
"type",
|
|
209
|
+
"status",
|
|
210
|
+
"cause",
|
|
211
|
+
"eventId",
|
|
212
|
+
"reasoning",
|
|
213
|
+
"chunk"
|
|
214
|
+
];
|
|
215
|
+
/**
|
|
216
|
+
* Best-effort JSON projection of an arbitrary value. Never throws: functions,
|
|
217
|
+
* symbols, `undefined`, and cyclic back-references are DROPPED (array holes
|
|
218
|
+
* become `null`), non-finite numbers become `null`, `bigint`s become strings,
|
|
219
|
+
* `Error`s become `{ name, message, stack?, code? }`, and anything with a
|
|
220
|
+
* `toJSON()` (e.g. `Date`) is projected through it — the same losses a
|
|
221
|
+
* `JSON.parse(JSON.stringify(...))` round-trip incurs, minus the throws.
|
|
222
|
+
*/
|
|
223
|
+
function toJsonValue(value, ancestors) {
|
|
224
|
+
if (value === null) return null;
|
|
225
|
+
const type = typeof value;
|
|
226
|
+
if (type === "string" || type === "boolean") return value;
|
|
227
|
+
if (type === "number") return Number.isFinite(value) ? value : null;
|
|
228
|
+
if (type === "bigint") return value.toString();
|
|
229
|
+
if (type !== "object") return;
|
|
230
|
+
const object = value;
|
|
231
|
+
if (ancestors.includes(object)) return;
|
|
232
|
+
const nextAncestors = [...ancestors, object];
|
|
233
|
+
if (object instanceof Error) {
|
|
234
|
+
const serialized = {
|
|
235
|
+
name: object.name,
|
|
236
|
+
message: object.message
|
|
237
|
+
};
|
|
238
|
+
if (typeof object.stack === "string") serialized.stack = object.stack;
|
|
239
|
+
const code = object.code;
|
|
240
|
+
if (typeof code === "string") serialized.code = code;
|
|
241
|
+
const cause = toJsonValue(object.cause, nextAncestors);
|
|
242
|
+
if (cause !== void 0) serialized.cause = cause;
|
|
243
|
+
return serialized;
|
|
244
|
+
}
|
|
245
|
+
const toJSON = object.toJSON;
|
|
246
|
+
if (typeof toJSON === "function") return toJsonValue(toJSON.call(object), nextAncestors);
|
|
247
|
+
if (Array.isArray(object)) return object.map((item) => toJsonValue(item, nextAncestors) ?? null);
|
|
248
|
+
const out = {};
|
|
249
|
+
for (const [key, item] of Object.entries(object)) {
|
|
250
|
+
const serializedItem = toJsonValue(item, nextAncestors);
|
|
251
|
+
if (serializedItem !== void 0) out[key] = serializedItem;
|
|
252
|
+
}
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Projects an {@link AgentTraceEvent} into a guaranteed JSON-safe envelope —
|
|
257
|
+
* the form the trace stream is actually sold for (one `JSON.stringify` per line
|
|
258
|
+
* in a JSONL file). Live values are sanitized rather than trusted:
|
|
259
|
+
*
|
|
260
|
+
* - Snapshots (`run.start`, `machine.transition`, `run.end`) go through the
|
|
261
|
+
* same JSON round-trip as {@link persistSnapshot}, so what lands on disk is
|
|
262
|
+
* what a resume would see.
|
|
263
|
+
* - `request.end`'s `raw` (a provider SDK object, frequently cyclic) is DROPPED
|
|
264
|
+
* unless `includeRaw` is set, in which case it is sanitized like everything
|
|
265
|
+
* else.
|
|
266
|
+
* - Non-serializable values anywhere (functions, symbols, `undefined`, cyclic
|
|
267
|
+
* back-references) are dropped; `Error`s become `{ name, message, stack?,
|
|
268
|
+
* code? }` instead of `{}`. Nothing throws.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```ts
|
|
272
|
+
* await appendFile('trace.jsonl', JSON.stringify(serializeTraceEvent(event)) + '\n');
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
function serializeTraceEvent(event, options = {}) {
|
|
276
|
+
const out = {};
|
|
277
|
+
for (const [key, value] of Object.entries(event)) {
|
|
278
|
+
if (key === "raw" && !options.includeRaw) continue;
|
|
279
|
+
if (TRACE_ENVELOPE_KEYS.includes(key)) {
|
|
280
|
+
if (value !== void 0) out[key] = value;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const serialized = toJsonValue(value, []);
|
|
284
|
+
if (serialized !== void 0) out[key] = serialized;
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
let nextRunAgentTraceId = 1;
|
|
289
|
+
var AgentMaxModelCallsExceededError = class extends require_errors.AgentError {
|
|
290
|
+
constructor() {
|
|
291
|
+
super("max-model-calls-exceeded", "runAgent exceeded maxModelCalls.");
|
|
292
|
+
this.name = "AgentMaxModelCallsExceededError";
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
function wrapsDecisionExhausted(error) {
|
|
296
|
+
let current = error;
|
|
297
|
+
for (let depth = 0; depth < 10 && current != null; depth++) {
|
|
298
|
+
if (current instanceof require_decision.AgentDecisionExhaustedError) return true;
|
|
299
|
+
current = current.cause;
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Recursively collects every invoke's `src` from raw machine config (spike
|
|
305
|
+
* S6: `machine.config` preserves authored srcs; the built `machine.root`
|
|
306
|
+
* normalizes object srcs to synthetic string ids and loses the distinction
|
|
307
|
+
* this walk needs). Function-valued `src` resolvers are dynamic and are not
|
|
308
|
+
* statically analyzable, so they are skipped (pass-through, like any other
|
|
309
|
+
* non-agent actor).
|
|
310
|
+
*/
|
|
311
|
+
function collectConfiguredInvokeSrcs(stateConfig, stateName, out) {
|
|
312
|
+
if (!stateConfig) return;
|
|
313
|
+
const invokes = stateConfig.invoke === void 0 ? [] : Array.isArray(stateConfig.invoke) ? stateConfig.invoke : [stateConfig.invoke];
|
|
314
|
+
for (const invokeConfig of invokes) {
|
|
315
|
+
const src = invokeConfig?.src;
|
|
316
|
+
if (typeof src === "string" || src && typeof src === "object") out.push({
|
|
317
|
+
stateName,
|
|
318
|
+
src
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
for (const [childName, childConfig] of Object.entries(stateConfig.states ?? {})) collectConfiguredInvokeSrcs(childConfig, `${stateName}.${childName}`, out);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Duck-types a state machine actor logic (an invoked child machine) vs. any
|
|
325
|
+
* other actor logic. xstate's `StateMachine` carries `.config`, `.root`, and
|
|
326
|
+
* a `.provide(...)` method plus a `sources.actors` map — this
|
|
327
|
+
* combination is unique to machines and survives the dual-package/version
|
|
328
|
+
* boundary an `instanceof` check would not. Used to descend the bind-time
|
|
329
|
+
* walk into invoked child machines (their internal agent requests are opaque
|
|
330
|
+
* to the parent-level source walk otherwise).
|
|
331
|
+
*/
|
|
332
|
+
function isStateMachine(logic) {
|
|
333
|
+
return !!logic && typeof logic === "object" && "config" in logic && "root" in logic && typeof logic.provide === "function" && typeof logic.sources === "object" && !!logic.sources?.actors;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Fails fast (throws) at bind time — before any actor runs — when the
|
|
337
|
+
* machine invokes an agent actor `runAgent` cannot execute. See §3.2 point 2.
|
|
338
|
+
*
|
|
339
|
+
* Recurses into invoked child state machines (arbitrarily deep). A child
|
|
340
|
+
* machine's agent requests reached through string-keyed invoke srcs DO inherit
|
|
341
|
+
* the parent runAgent's `generateText`/`streamText`/`decide` executors —
|
|
342
|
+
* runAgent rebinds them with the same host-backed wrappers (see
|
|
343
|
+
* {@link rebindChildMachine}) — so the only remaining bind-time errors are: a
|
|
344
|
+
* required executor kind missing entirely (naming the invoke chain and src),
|
|
345
|
+
* and an unbound request reached through a direct-object invoke src that can't
|
|
346
|
+
* be rebound ({@link unrebindableChildRequestError}). A request that carries
|
|
347
|
+
* its own executor (`.withExecutor(...)`, tracked in `executorBoundLogics`)
|
|
348
|
+
* always runs itself; explicit binding shadows inheritance.
|
|
349
|
+
*/
|
|
350
|
+
function assertBindable(machine, effectiveSources, options) {
|
|
351
|
+
assertMachineBindable(machine, effectiveSources, options, {
|
|
352
|
+
isChild: false,
|
|
353
|
+
childPath: "",
|
|
354
|
+
rebindable: true,
|
|
355
|
+
visited: new Set([machine])
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
function assertMachineBindable(machine, effectiveSources, options, ctx) {
|
|
359
|
+
const invokes = [];
|
|
360
|
+
collectConfiguredInvokeSrcs(machine.config, machine.config.id ?? "(root)", invokes);
|
|
361
|
+
const where = ctx.isChild ? `child machine '${ctx.childPath}' state` : "state";
|
|
362
|
+
for (const { stateName, src } of invokes) {
|
|
363
|
+
if (typeof src !== "string") {
|
|
364
|
+
if (isStateMachine(src)) {
|
|
365
|
+
assertChildMachineBindable(src, src, stateName, options, ctx);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if ((require_decision.isTextLogic(src) || require_decision.isDecisionLogic(src) || require_decision.isPlanLogic(src)) && !require_decision.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.`);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const logic = effectiveSources[src];
|
|
372
|
+
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}': ... } }).`);
|
|
373
|
+
if (isStateMachine(logic)) {
|
|
374
|
+
assertChildMachineBindable(logic, src, stateName, options, ctx);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (src === "agent.userInput") continue;
|
|
378
|
+
if (require_decision.isDecisionLogic(logic)) {
|
|
379
|
+
if (require_decision.executorBoundLogics.has(logic)) continue;
|
|
380
|
+
if (!ctx.rebindable) throw unrebindableChildRequestError(ctx.childPath, stateName, src, "decision");
|
|
381
|
+
if (!options.hasDecide) throw new Error(`runAgent: ${where} '${stateName}' invokes decision source '${src}' but no 'decide' executor was provided to runAgent(...).`);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (require_decision.isPlanLogic(logic)) {
|
|
385
|
+
if (!ctx.rebindable) throw unrebindableChildRequestError(ctx.childPath, stateName, src, "plan");
|
|
386
|
+
if (!options.hasDecide) throw new Error(`runAgent: ${where} '${stateName}' invokes plan source '${src}' but no 'decide' executor was provided to runAgent(...).`);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (require_decision.isTextLogic(logic)) {
|
|
390
|
+
if (require_decision.executorBoundLogics.has(logic)) continue;
|
|
391
|
+
if (!ctx.rebindable) throw unrebindableChildRequestError(ctx.childPath, stateName, src, logic.mode === "stream" ? "streaming text" : "text");
|
|
392
|
+
if (logic.mode === "stream" && !options.hasStreamText) throw new Error(`runAgent: ${where} '${stateName}' invokes streaming text source '${src}' but no 'streamText' executor was provided to runAgent(...).`);
|
|
393
|
+
if (logic.mode !== "stream" && !options.hasGenerateText) throw new Error(`runAgent: ${where} '${stateName}' invokes text source '${src}' but no 'generateText' executor was provided to runAgent(...).`);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (require_decision.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}': ... } }).`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/** Descends the bind-time walk into an invoked child state machine, guarding
|
|
400
|
+
* against a machine that (transitively) invokes itself. */
|
|
401
|
+
function assertChildMachineBindable(childMachine, childSrc, stateName, options, ctx) {
|
|
402
|
+
if (ctx.visited.has(childMachine)) return;
|
|
403
|
+
const childName = typeof childSrc === "string" ? childSrc : childMachine.config.id ?? "(child machine)";
|
|
404
|
+
const childPath = ctx.childPath ? `${ctx.childPath} > ${childName}` : childName;
|
|
405
|
+
const childSources = childMachine.sources.actors;
|
|
406
|
+
assertMachineBindable(childMachine, childSources, options, {
|
|
407
|
+
isChild: true,
|
|
408
|
+
childPath,
|
|
409
|
+
rebindable: ctx.rebindable && typeof childSrc === "string",
|
|
410
|
+
visited: new Set([...ctx.visited, childMachine])
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
/** The loud bind-time error for an unbound agent request reached under a
|
|
414
|
+
* direct-object invoke src, which runAgent cannot rebind (only string-keyed
|
|
415
|
+
* sources can be swapped via `.provide`). Names the invoke chain AND the
|
|
416
|
+
* request src, and spells out the `.withExecutor`/string-keyed remedy. Note:
|
|
417
|
+
* requests reachable through string-keyed srcs at any depth DO inherit
|
|
418
|
+
* runAgent's executors — this error is only for the unrebindable direct-object
|
|
419
|
+
* case. */
|
|
420
|
+
function unrebindableChildRequestError(childPath, stateName, requestSrc, kind) {
|
|
421
|
+
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.`);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* True when the snapshot's active states declare a transition for the reserved
|
|
425
|
+
* `'@agent.usage'` type EXPLICITLY. A catch-all `on: { '*': … }` deliberately
|
|
426
|
+
* does not count: a wildcard is a machine's own event vocabulary, not an
|
|
427
|
+
* opt-in to a library-reserved event, and `snapshot.can(event)` alone cannot
|
|
428
|
+
* tell the two apart (it answers "would this event be taken?", which a
|
|
429
|
+
* wildcard makes true for everything). Gating delivery on the explicit
|
|
430
|
+
* declaration is what keeps `@agent.usage` opt-in by construction — and keeps
|
|
431
|
+
* a wildcard machine's context and event log byte-identical to a run without
|
|
432
|
+
* the feature. @internal
|
|
433
|
+
*/
|
|
434
|
+
function declaresUsageTransition(snapshot) {
|
|
435
|
+
return (0, xstate.getNextTransitions)(snapshot).some((transition) => transition.eventType === require_setup_agent.AGENT_USAGE_EVENT_TYPE);
|
|
436
|
+
}
|
|
437
|
+
/** Reads the durable invoke id/src off the async actor's own ref (`self`). */
|
|
438
|
+
function selfIdAndSrc(self) {
|
|
439
|
+
const ref = self;
|
|
440
|
+
return {
|
|
441
|
+
id: typeof ref?.id === "string" ? ref.id : "",
|
|
442
|
+
src: typeof ref?.src === "string" ? ref.src : ""
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* The machine actor that INVOKED a decision/plan request — the actor whose
|
|
447
|
+
* live snapshot supplies the candidate events and drives `canTake`/`send`.
|
|
448
|
+
* For a top-level request this is the root actor (identity-equal to
|
|
449
|
+
* `runCtx.actorHolder.actorRef`); for a request inside an invoked child
|
|
450
|
+
* machine it is that child's actor, so a child decision/plan reads and drives
|
|
451
|
+
* the CHILD's snapshot — not the root's. Read off `self._parent`, with the
|
|
452
|
+
* root actor as a fallback.
|
|
453
|
+
*/
|
|
454
|
+
function invokingActorOf(self, runCtx) {
|
|
455
|
+
return self?._parent ?? runCtx.actorHolder.actorRef;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* The shared text/stream emission helper: binds a {@link TextLogic} to
|
|
459
|
+
* `runCtx`'s executor and constructs the `request.start` / `stream.chunk` /
|
|
460
|
+
* `request.end` (incl. the lifted `reasoning`) / `request.error` trace payloads.
|
|
461
|
+
* Used by both `runAgent` and `provideExecutors` so the two paths produce
|
|
462
|
+
* identical event shapes by construction. @internal
|
|
463
|
+
*/
|
|
464
|
+
function wrapTextLogicForRunAgent(logic, runCtx) {
|
|
465
|
+
return logic.withExecutor(async ({ request, self, signal }) => {
|
|
466
|
+
const { id, src } = selfIdAndSrc(self);
|
|
467
|
+
const executor = logic.mode === "stream" ? runCtx.streamText : runCtx.generateText;
|
|
468
|
+
if (!executor) throw new Error(`runAgent: no '${logic.mode === "stream" ? "streamText" : "generateText"}' executor provided.`);
|
|
469
|
+
const requestWithTools = {
|
|
470
|
+
...request,
|
|
471
|
+
tools: request.tools ?? {}
|
|
472
|
+
};
|
|
473
|
+
const agentRequest = {
|
|
474
|
+
kind: "text",
|
|
475
|
+
id,
|
|
476
|
+
src,
|
|
477
|
+
mode: logic.mode,
|
|
478
|
+
input: request,
|
|
479
|
+
tools: requestWithTools.tools,
|
|
480
|
+
events: []
|
|
481
|
+
};
|
|
482
|
+
runCtx.consumeModelCall();
|
|
483
|
+
runCtx.onTrace?.({
|
|
484
|
+
type: "request.start",
|
|
485
|
+
request: agentRequest
|
|
486
|
+
}, self);
|
|
487
|
+
try {
|
|
488
|
+
const raw = await executor(requestWithTools, {
|
|
489
|
+
onChunk: (chunk) => {
|
|
490
|
+
runCtx.onTrace?.({
|
|
491
|
+
type: "stream.chunk",
|
|
492
|
+
request: agentRequest,
|
|
493
|
+
chunk
|
|
494
|
+
}, self);
|
|
495
|
+
runCtx.onChunk?.(chunk, { request: agentRequest });
|
|
496
|
+
},
|
|
497
|
+
signal,
|
|
498
|
+
...runCtx.runId !== void 0 ? { runId: runCtx.runId } : {},
|
|
499
|
+
...id !== "" ? { requestId: id } : {}
|
|
500
|
+
});
|
|
501
|
+
const output = await require_decision.normalizeGeneratorResult(raw, id, {
|
|
502
|
+
request,
|
|
503
|
+
onChunk: (chunk) => {
|
|
504
|
+
runCtx.onTrace?.({
|
|
505
|
+
type: "stream.chunk",
|
|
506
|
+
request: agentRequest,
|
|
507
|
+
chunk
|
|
508
|
+
}, self);
|
|
509
|
+
runCtx.onChunk?.(chunk, { request: agentRequest });
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
const rawReasoning = raw?.reasoning;
|
|
513
|
+
const reasoning = typeof rawReasoning === "string" ? rawReasoning : void 0;
|
|
514
|
+
const usage = require_decision.extractCallUsage(raw);
|
|
515
|
+
if (usage) runCtx.recordUsage?.(usage, {
|
|
516
|
+
kind: "text",
|
|
517
|
+
...id !== "" ? { id } : {},
|
|
518
|
+
...src !== "" ? { src } : {},
|
|
519
|
+
model: request.model,
|
|
520
|
+
...request.name !== void 0 ? { name: request.name } : {}
|
|
521
|
+
}, self);
|
|
522
|
+
runCtx.onResult?.(agentRequest, {
|
|
523
|
+
output,
|
|
524
|
+
raw
|
|
525
|
+
});
|
|
526
|
+
runCtx.onTrace?.({
|
|
527
|
+
type: "request.end",
|
|
528
|
+
request: agentRequest,
|
|
529
|
+
output,
|
|
530
|
+
raw,
|
|
531
|
+
...reasoning !== void 0 ? { reasoning } : {},
|
|
532
|
+
...usage !== void 0 ? { usage } : {}
|
|
533
|
+
}, self);
|
|
534
|
+
return { output };
|
|
535
|
+
} catch (error) {
|
|
536
|
+
runCtx.onTrace?.({
|
|
537
|
+
type: "request.error",
|
|
538
|
+
request: agentRequest,
|
|
539
|
+
error
|
|
540
|
+
}, self);
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
function createCountingDecide(runCtx, self, kind = "decision") {
|
|
546
|
+
return async (attemptRequest) => {
|
|
547
|
+
runCtx.consumeModelCall();
|
|
548
|
+
runCtx.onTrace?.({
|
|
549
|
+
type: "request.start",
|
|
550
|
+
request: attemptRequest
|
|
551
|
+
}, self);
|
|
552
|
+
try {
|
|
553
|
+
const result = await runCtx.decide(runCtx.runId !== void 0 ? {
|
|
554
|
+
...attemptRequest,
|
|
555
|
+
runId: runCtx.runId
|
|
556
|
+
} : attemptRequest);
|
|
557
|
+
const usage = require_decision.extractCallUsage(result);
|
|
558
|
+
if (usage) {
|
|
559
|
+
const { src } = selfIdAndSrc(self);
|
|
560
|
+
runCtx.recordUsage?.(usage, {
|
|
561
|
+
kind,
|
|
562
|
+
...attemptRequest.id ? { id: attemptRequest.id } : {},
|
|
563
|
+
...src !== "" ? { src } : {},
|
|
564
|
+
model: attemptRequest.model
|
|
565
|
+
}, self);
|
|
566
|
+
}
|
|
567
|
+
runCtx.onResult?.(attemptRequest, {
|
|
568
|
+
output: result.event,
|
|
569
|
+
raw: result
|
|
570
|
+
});
|
|
571
|
+
runCtx.onTrace?.({
|
|
572
|
+
type: "request.end",
|
|
573
|
+
request: attemptRequest,
|
|
574
|
+
output: result.event,
|
|
575
|
+
raw: result,
|
|
576
|
+
...usage !== void 0 ? { usage } : {}
|
|
577
|
+
}, self);
|
|
578
|
+
return result;
|
|
579
|
+
} catch (error) {
|
|
580
|
+
runCtx.onTrace?.({
|
|
581
|
+
type: "request.error",
|
|
582
|
+
request: attemptRequest,
|
|
583
|
+
error
|
|
584
|
+
}, self);
|
|
585
|
+
throw error;
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Builds the decision actor logic runAgent installs in place of a
|
|
591
|
+
* `DecisionLogic`/`agent.decide` source. `DecisionLogic.withExecutor(...)`
|
|
592
|
+
* can only swap the innermost per-attempt executor — the `resolveDecision(...)`
|
|
593
|
+
* call (and its `canTake`) is hardwired inside the original logic's `run`.
|
|
594
|
+
* To supply `canTake` (mode-3, §2.6), runAgent instead builds a fresh async
|
|
595
|
+
* logic here that calls `resolveDecision` itself, reusing `logic.request(...)`
|
|
596
|
+
* to build the request the same way the original logic would have.
|
|
597
|
+
*
|
|
598
|
+
* On success it SENDS the chosen event to the invoking actor (auto-delivery,
|
|
599
|
+
* mirroring {@link createRunAgentPlanLogic}) and then completes with that event
|
|
600
|
+
* as its output — so callers never wire an `onDone` to deliver it. See the
|
|
601
|
+
* send-then-complete note inside `run` for how exit-cancels-invoke interacts
|
|
602
|
+
* with `onDone`.
|
|
603
|
+
*/
|
|
604
|
+
function createRunAgentDecisionLogic(logic, runCtx) {
|
|
605
|
+
const decisionLogic = (0, xstate.createAsyncLogic)({ run: async ({ input, signal, self }) => {
|
|
606
|
+
if (!runCtx.decide) throw new Error("runAgent: no 'decide' executor provided.");
|
|
607
|
+
const { id } = selfIdAndSrc(self);
|
|
608
|
+
const declaredEventTypes = logic.allowedEventTypes?.(input);
|
|
609
|
+
await Promise.resolve();
|
|
610
|
+
const actorRef = invokingActorOf(self, runCtx);
|
|
611
|
+
const events = actorRef ? require_decision.getAcceptedEvents(actorRef.getSnapshot(), {
|
|
612
|
+
schemas: runCtx.schemas,
|
|
613
|
+
eventTypes: declaredEventTypes
|
|
614
|
+
}) : [];
|
|
615
|
+
const chosen = await require_decision.resolveDecision({
|
|
616
|
+
...logic.request(input),
|
|
617
|
+
id,
|
|
618
|
+
events
|
|
619
|
+
}, createCountingDecide(runCtx, self), {
|
|
620
|
+
maxRetries: logic.maxRetries,
|
|
621
|
+
signal,
|
|
622
|
+
canTake: (event) => actorRef ? actorRef.getSnapshot().can(event) : true
|
|
623
|
+
});
|
|
624
|
+
actorRef?.send(chosen);
|
|
625
|
+
await Promise.resolve();
|
|
626
|
+
return chosen;
|
|
627
|
+
} });
|
|
628
|
+
return Object.assign(decisionLogic, {
|
|
629
|
+
kind: "statelyai.decisionLogic",
|
|
630
|
+
maxRetries: logic.maxRetries,
|
|
631
|
+
request: logic.request,
|
|
632
|
+
withExecutor: (nextExecute) => createRunAgentDecisionLogic(logic.withExecutor(nextExecute), runCtx)
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Builds the plan actor logic runAgent installs in place of the `agent.plan`
|
|
637
|
+
* builtin: iterated {@link resolveDecision}. Each step re-reads the live
|
|
638
|
+
* snapshot (so the candidate set reflects everything applied so far), asks
|
|
639
|
+
* the `decide` executor for one legal event with the same validation/retry
|
|
640
|
+
* loop a decision gets, and sends it to the machine. The loop ends on a
|
|
641
|
+
* `stopOn` event, at `maxSteps`, when no legal candidate remains, or when an
|
|
642
|
+
* applied event exits the invoking state (xstate cancels this invoke — the
|
|
643
|
+
* machine simply moves on and the pending output is discarded).
|
|
644
|
+
*
|
|
645
|
+
* The invoke's `input` is resolved once, so the prompt cannot re-render
|
|
646
|
+
* context between steps; instead the applied trail is appended to the prompt
|
|
647
|
+
* each step so the model can see plan progress.
|
|
648
|
+
*/
|
|
649
|
+
function createRunAgentPlanLogic(logic, runCtx) {
|
|
650
|
+
const planLogic = (0, xstate.createAsyncLogic)({ run: async ({ input, signal, self }) => {
|
|
651
|
+
if (!runCtx.decide) throw new Error("runAgent: no 'decide' executor provided.");
|
|
652
|
+
const { id } = selfIdAndSrc(self);
|
|
653
|
+
const stopOn = new Set(input.stopOn ?? []);
|
|
654
|
+
const countingDecide = createCountingDecide(runCtx, self, "plan");
|
|
655
|
+
const base = logic.request(input);
|
|
656
|
+
let ledger = require_decision.initialPlanLedger(logic, input);
|
|
657
|
+
const end = (stopped) => {
|
|
658
|
+
ledger = require_decision.advancePlanLedger(logic, ledger, {
|
|
659
|
+
type: "plan.ended",
|
|
660
|
+
stopped
|
|
661
|
+
});
|
|
662
|
+
return ledger.output;
|
|
663
|
+
};
|
|
664
|
+
await Promise.resolve();
|
|
665
|
+
const invokingActor = invokingActorOf(self, runCtx);
|
|
666
|
+
while (ledger.context.stepsRemaining > 0) {
|
|
667
|
+
const actorRef = invokingActor;
|
|
668
|
+
if (!actorRef || signal.aborted) break;
|
|
669
|
+
const machineEvents = require_decision.getAcceptedEvents(actorRef.getSnapshot(), {
|
|
670
|
+
schemas: runCtx.schemas,
|
|
671
|
+
eventTypes: logic.allowedEventTypes(input)
|
|
672
|
+
});
|
|
673
|
+
if (machineEvents.length === 0) return end("no-legal-events");
|
|
674
|
+
const doneDescriptor = {
|
|
675
|
+
type: require_decision.PLAN_DONE_EVENT_TYPE,
|
|
676
|
+
toolName: require_decision.sanitizeEventToolName(require_decision.PLAN_DONE_EVENT_TYPE)
|
|
677
|
+
};
|
|
678
|
+
const events = machineEvents.some((event) => event.type === "agent.plan.done") ? machineEvents : [...machineEvents, doneDescriptor];
|
|
679
|
+
const applied = ledger.context.applied;
|
|
680
|
+
const trail = applied.length === 0 ? "" : `\n\nEvents already applied in this plan, in order:\n${applied.map((step) => JSON.stringify(step)).join("\n")}\nContinue from here; do not repeat applied events.`;
|
|
681
|
+
const doneHint = `\n\nWhen the request is fully handled (or no action is needed), choose '${require_decision.PLAN_DONE_EVENT_TYPE}'.`;
|
|
682
|
+
const chosen = await require_decision.resolveDecision({
|
|
683
|
+
...base,
|
|
684
|
+
id: `${id}[${applied.length}]`,
|
|
685
|
+
events,
|
|
686
|
+
prompt: `${base.prompt ?? ""}${trail}${doneHint}`,
|
|
687
|
+
attempts: []
|
|
688
|
+
}, countingDecide, {
|
|
689
|
+
maxRetries: input.maxRetries ?? logic.maxRetries,
|
|
690
|
+
signal,
|
|
691
|
+
canTake: (event) => {
|
|
692
|
+
if (event.type === "agent.plan.done" || stopOn.has(event.type)) return true;
|
|
693
|
+
return invokingActor ? invokingActor.getSnapshot().can(event) : true;
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
if (chosen.type === "agent.plan.done") return end("done");
|
|
697
|
+
actorRef.send(chosen);
|
|
698
|
+
ledger = require_decision.advancePlanLedger(logic, ledger, {
|
|
699
|
+
type: "plan.applied",
|
|
700
|
+
event: chosen
|
|
701
|
+
});
|
|
702
|
+
await Promise.resolve();
|
|
703
|
+
if (stopOn.has(chosen.type)) return end("stop-event");
|
|
704
|
+
}
|
|
705
|
+
return end("max-steps");
|
|
706
|
+
} });
|
|
707
|
+
return Object.assign(planLogic, {
|
|
708
|
+
kind: "statelyai.planLogic",
|
|
709
|
+
maxRetries: logic.maxRetries,
|
|
710
|
+
request: logic.request,
|
|
711
|
+
allowedEventTypes: logic.allowedEventTypes
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* The set of string-keyed actor `src`s the machine's own config invokes
|
|
716
|
+
* (top-level, recursing into child STATES but not into invoked child
|
|
717
|
+
* machines). {@link provideExecutors} uses it to require an executor only for a
|
|
718
|
+
* source the machine actually invokes — the always-registered `agent.*`
|
|
719
|
+
* builtins that go unused must not force their executors to be supplied.
|
|
720
|
+
* @internal
|
|
721
|
+
*/
|
|
722
|
+
function getConfiguredInvokeSrcs(machine) {
|
|
723
|
+
const invokes = [];
|
|
724
|
+
collectConfiguredInvokeSrcs(machine.config, machine.config.id ?? "(root)", invokes);
|
|
725
|
+
const srcs = /* @__PURE__ */ new Set();
|
|
726
|
+
for (const { src } of invokes) if (typeof src === "string") srcs.add(src);
|
|
727
|
+
return srcs;
|
|
728
|
+
}
|
|
729
|
+
const rootTraceRegistry = /* @__PURE__ */ new WeakMap();
|
|
730
|
+
let nextProvideRunId = 1;
|
|
731
|
+
/** Walks `self._parent` from an invoked async leaf actor up to its root actor. */
|
|
732
|
+
function rootActorOf(self) {
|
|
733
|
+
let ref = self;
|
|
734
|
+
if (!ref) return;
|
|
735
|
+
while (ref._parent) ref = ref._parent;
|
|
736
|
+
return ref;
|
|
737
|
+
}
|
|
738
|
+
/** The per-root envelope state, minted on first use (runId `run_<n>`, matching runAgent). */
|
|
739
|
+
function rootTraceState(root) {
|
|
740
|
+
let state = rootTraceRegistry.get(root);
|
|
741
|
+
if (!state) {
|
|
742
|
+
const logic = root.logic;
|
|
743
|
+
const machineId = (logic?.config)?.id ?? logic?.id ?? "(machine)";
|
|
744
|
+
const machineVersion = logic ? require_decision.getMachineStructuralHash(logic) : "";
|
|
745
|
+
state = {
|
|
746
|
+
runId: `run_${nextProvideRunId++}`,
|
|
747
|
+
seq: 0,
|
|
748
|
+
machineId,
|
|
749
|
+
machineVersion
|
|
750
|
+
};
|
|
751
|
+
rootTraceRegistry.set(root, state);
|
|
752
|
+
}
|
|
753
|
+
return state;
|
|
754
|
+
}
|
|
755
|
+
/** Stamps a per-root-actor envelope onto a bare trace payload. */
|
|
756
|
+
function stampRootTrace(root, payload) {
|
|
757
|
+
const state = rootTraceState(root);
|
|
758
|
+
return {
|
|
759
|
+
schemaVersion: 1,
|
|
760
|
+
runId: state.runId,
|
|
761
|
+
seq: ++state.seq,
|
|
762
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
763
|
+
machineId: state.machineId,
|
|
764
|
+
machineVersion: state.machineVersion,
|
|
765
|
+
...payload
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
/** Adapts a public `onTrace` into the payload-level {@link RunAgentBindContext.onTrace} sink used by the shared emission helpers. */
|
|
769
|
+
function provideTraceSink(onTrace) {
|
|
770
|
+
if (!onTrace) return;
|
|
771
|
+
return (payload, self) => {
|
|
772
|
+
const root = rootActorOf(self);
|
|
773
|
+
if (root) onTrace(stampRootTrace(root, payload));
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* A minimal {@link RunAgentBindContext} for `provideExecutors` (uncontrolled
|
|
778
|
+
* `createActor`): the same wrappers runAgent installs, MINUS the run-scoped
|
|
779
|
+
* model-call counter. `consumeModelCall` is a no-op (no budget), and
|
|
780
|
+
* `actorHolder.actorRef` is left undefined — the wrappers read the invoking
|
|
781
|
+
* actor off `self._parent`, always present under a live `createActor` tree.
|
|
782
|
+
* `onTrace` (when given) mints a per-root-actor envelope. `schemas` come from
|
|
783
|
+
* the machine's registered `setupAgent` execution options.
|
|
784
|
+
*
|
|
785
|
+
* `recordUsage` has no run-level aggregate to fold into here (there is no
|
|
786
|
+
* run), so it does one thing: deliver the reserved `@agent.usage` event, gated
|
|
787
|
+
* exactly like runAgent's — see {@link deliverUsageToInvokingActor}.
|
|
788
|
+
*/
|
|
789
|
+
function provideBindContext(machine, executors, options) {
|
|
790
|
+
return {
|
|
791
|
+
generateText: executors.generateText,
|
|
792
|
+
streamText: executors.streamText,
|
|
793
|
+
decide: executors.decide,
|
|
794
|
+
onChunk: options.onChunk ? (chunk) => options.onChunk(chunk) : void 0,
|
|
795
|
+
onTrace: provideTraceSink(options.onTrace),
|
|
796
|
+
consumeModelCall: () => {},
|
|
797
|
+
recordUsage: (usage, source, self) => {
|
|
798
|
+
deliverUsageToInvokingActor(usage, source ?? {}, self);
|
|
799
|
+
},
|
|
800
|
+
actorHolder: { actorRef: void 0 },
|
|
801
|
+
schemas: require_decision.getRegisteredAgentExecutionOptions(machine).schemas
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* `provideExecutors`' counterpart to runAgent's `deliverUsageEvent`: after a
|
|
806
|
+
* bound call settles with reported usage, send the reserved
|
|
807
|
+
* `@agent.usage` event to the machine actor that INVOKED the request — read
|
|
808
|
+
* off the settling request actor's `self._parent`, which under a live
|
|
809
|
+
* `createActor` tree is always the invoking machine (there is no run-scoped
|
|
810
|
+
* root actor on this path).
|
|
811
|
+
*
|
|
812
|
+
* Gated identically to runAgent: the invoking snapshot must be active, must
|
|
813
|
+
* declare an `'@agent.usage'` transition EXPLICITLY (see
|
|
814
|
+
* {@link declaresUsageTransition} — a catch-all `on: { '*' }` is not an opt-in),
|
|
815
|
+
* and must be able to take the event. There is no cycle to settle in
|
|
816
|
+
* uncontrolled mode, so there are no dropped stragglers.
|
|
817
|
+
*
|
|
818
|
+
* Delivery follows `provideExecutors`' binding boundary: only sources IT bound
|
|
819
|
+
* report here, so an invoked child machine that was not itself passed through
|
|
820
|
+
* `provideExecutors` reports nothing. @internal
|
|
821
|
+
*/
|
|
822
|
+
function deliverUsageToInvokingActor(usage, source, self) {
|
|
823
|
+
const actorRef = self?._parent;
|
|
824
|
+
if (!actorRef) return;
|
|
825
|
+
const snapshot = actorRef.getSnapshot();
|
|
826
|
+
if (snapshot?.status !== "active" || !declaresUsageTransition(snapshot)) return;
|
|
827
|
+
const event = {
|
|
828
|
+
type: require_setup_agent.AGENT_USAGE_EVENT_TYPE,
|
|
829
|
+
...source,
|
|
830
|
+
usage
|
|
831
|
+
};
|
|
832
|
+
if (!snapshot.can(event)) return;
|
|
833
|
+
actorRef.send(event);
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Host-binds one text/stream source for {@link provideExecutors} using the SAME
|
|
837
|
+
* emission helper as `runAgent` ({@link wrapTextLogicForRunAgent}), so a bound
|
|
838
|
+
* text request emits request.start/stream.chunk/request.end/request.error with
|
|
839
|
+
* identical shapes. @internal
|
|
840
|
+
*/
|
|
841
|
+
function bindTextForProvide(machine, logic, executors, options) {
|
|
842
|
+
return wrapTextLogicForRunAgent(logic, provideBindContext(machine, executors, options));
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Host-binds one `DecisionLogic`/`agent.decide` source for
|
|
846
|
+
* {@link provideExecutors}: runAgent's decision wrapper (snapshot-driven
|
|
847
|
+
* candidate events, `canTake`, auto-delivery of the chosen event) with the same
|
|
848
|
+
* request-level tracing runAgent emits, minus run-scoped counting. @internal
|
|
849
|
+
*/
|
|
850
|
+
function bindDecisionForProvide(machine, logic, executors, options) {
|
|
851
|
+
return createRunAgentDecisionLogic(logic, provideBindContext(machine, executors, options));
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Host-binds one `agent.plan` source for {@link provideExecutors}: runAgent's
|
|
855
|
+
* plan wrapper (iterated snapshot-driven decisions, auto-delivery) with the same
|
|
856
|
+
* request-level tracing runAgent emits, minus run-scoped counting. @internal
|
|
857
|
+
*/
|
|
858
|
+
function bindPlanForProvide(machine, logic, executors, options) {
|
|
859
|
+
return createRunAgentPlanLogic(logic, provideBindContext(machine, executors, options));
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Recursively rebinds an invoked child machine's own agent sources with the
|
|
863
|
+
* SAME host-backed wrappers runAgent applies to the top-level machine, so a
|
|
864
|
+
* child's text/stream/decision/plan requests inherit runAgent's executors and
|
|
865
|
+
* participate in maxModelCalls counting, onTrace/onChunk/onResult exactly like
|
|
866
|
+
* parent requests. Returns the child machine to invoke: a `.provide`-rebound
|
|
867
|
+
* copy when any inner source needed wrapping, else the original untouched.
|
|
868
|
+
*
|
|
869
|
+
* Only string-keyed sources are visited — a direct-object invoke src cannot be
|
|
870
|
+
* swapped via `.provide` (assertBindable already rejected an unbound request
|
|
871
|
+
* under one). A source that already carries its own executor
|
|
872
|
+
* (`executorBoundLogics`) is left as-is: explicit binding shadows inheritance.
|
|
873
|
+
* Cycle-safe via `visited` (a machine that invokes itself is returned as-is).
|
|
874
|
+
*/
|
|
875
|
+
function rebindChildMachine(childMachine, runCtx, visited) {
|
|
876
|
+
if (visited.has(childMachine)) return childMachine;
|
|
877
|
+
const childVisited = new Set([...visited, childMachine]);
|
|
878
|
+
const sources = childMachine.sources.actors;
|
|
879
|
+
const wrapped = {};
|
|
880
|
+
for (const [key, logic] of Object.entries(sources)) {
|
|
881
|
+
if (require_decision.isDecisionLogic(logic)) {
|
|
882
|
+
if (!require_decision.executorBoundLogics.has(logic)) wrapped[key] = createRunAgentDecisionLogic(logic, runCtx);
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
if (require_decision.isPlanLogic(logic)) {
|
|
886
|
+
wrapped[key] = createRunAgentPlanLogic(logic, runCtx);
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
if (require_decision.isTextLogic(logic)) {
|
|
890
|
+
if (!require_decision.executorBoundLogics.has(logic)) wrapped[key] = wrapTextLogicForRunAgent(logic, runCtx);
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
if (isStateMachine(logic)) {
|
|
894
|
+
const rebound = rebindChildMachine(logic, runCtx, childVisited);
|
|
895
|
+
if (rebound !== logic) wrapped[key] = rebound;
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return Object.keys(wrapped).length > 0 ? childMachine.provide({ actors: wrapped }) : childMachine;
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Runs an agent machine to completion or idle: a `createActor` host that
|
|
903
|
+
* binds `options`' host executors onto the machine's `agent.*`/`TextLogic`/
|
|
904
|
+
* `DecisionLogic` actor sources, starts (or resumes) the actor, and drives
|
|
905
|
+
* it until it settles — {@link RunAgentResult} `done | idle | error`. Unlike
|
|
906
|
+
* the step helpers ({@link initialAgentStep} etc — a pure
|
|
907
|
+
* transition-at-a-time path for durable hosts), `runAgent` owns a live actor
|
|
908
|
+
* internally; there is no continuation callback, so **idle always settles**
|
|
909
|
+
* and the caller resumes explicitly by passing the settled `{ snapshot,
|
|
910
|
+
* event }` back in. The actor is stopped on every settle path (`done`,
|
|
911
|
+
* `idle`, and `error` alike) — resume is always by snapshot, never by
|
|
912
|
+
* holding a reference to a live actor.
|
|
913
|
+
*
|
|
914
|
+
* Binding happens **before** the actor starts: every invoke the machine
|
|
915
|
+
* could reach is walked and checked against the effective actor sources
|
|
916
|
+
* (`options.actors` merged onto the machine), so a missing
|
|
917
|
+
* `streamText`/`decide` executor or any other unbound actor source throws
|
|
918
|
+
* immediately — a bind-time error, not a mid-run failure. The one exception
|
|
919
|
+
* is `agent.userInput`: unhandled, it binds as a pending placeholder that
|
|
920
|
+
* settles the run idle (with `pendingUserInputs`) instead of erroring.
|
|
921
|
+
*
|
|
922
|
+
* @example
|
|
923
|
+
* ```ts
|
|
924
|
+
* const executors = createAiSdkExecutors({ models });
|
|
925
|
+
* let r = await runAgent(machine, { input, executors });
|
|
926
|
+
* while (r.status === 'idle') {
|
|
927
|
+
* const event = await promptUser(getAcceptedEvents(r.snapshot));
|
|
928
|
+
* r = await runAgent(machine, { snapshot: r.snapshot, event, executors });
|
|
929
|
+
* }
|
|
930
|
+
* if (r.status !== 'done') throw new Error(`Run did not complete: ${r.status}`);
|
|
931
|
+
* console.log(r.output);
|
|
932
|
+
* ```
|
|
933
|
+
*
|
|
934
|
+
* The `executors`' `generateText`/`streamText` accept the raw Vercel AI SDK
|
|
935
|
+
* functions directly (`executors: { generateText, streamText }` with them
|
|
936
|
+
* imported from `ai`) — their `{ text }`/`{ textStream }` results are unwrapped
|
|
937
|
+
* natively. `decide` cannot be a raw AI SDK function: the tool-per-event mapping
|
|
938
|
+
* lives in an adapter — use `createAiSdkExecutors` from '@statelyai/agent/ai-sdk'.
|
|
939
|
+
*/
|
|
940
|
+
async function runAgent(machine, options) {
|
|
941
|
+
return createAgentSession(machine, options, { oneShot: true }).settled();
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Session mode: {@link runAgent}'s engine with a long-lived actor. Use it when
|
|
945
|
+
* the agent is a *session* fed by external events (chat turns, device or
|
|
946
|
+
* timer events, a socket) rather than a one-shot job — you keep the log,
|
|
947
|
+
* budget, traces, and idle semantics that bare `provideExecutors` +
|
|
948
|
+
* `createActor` would forfeit.
|
|
949
|
+
*
|
|
950
|
+
* ```ts
|
|
951
|
+
* const session = createAgentActor(machine, { input, executors });
|
|
952
|
+
* let result = await session.settled(); // first quiescence
|
|
953
|
+
* while (result.status === "idle") {
|
|
954
|
+
* session.actor.send(await nextUserEvent(result.snapshot));
|
|
955
|
+
* result = await session.settled(); // next quiescence, same log
|
|
956
|
+
* }
|
|
957
|
+
* session.stop();
|
|
958
|
+
* ```
|
|
959
|
+
*
|
|
960
|
+
* Accepts the same options as {@link runAgent} (including `snapshot`/`events`
|
|
961
|
+
* resume). Not yet supported in session mode: `getRequests` re-interpretation
|
|
962
|
+
* across cycles behaves per-cycle exactly as in `runAgent`.
|
|
963
|
+
*/
|
|
964
|
+
function createAgentActor(machine, options) {
|
|
965
|
+
return createAgentSession(machine, options, { oneShot: false });
|
|
966
|
+
}
|
|
967
|
+
function createAgentSession(machine, options, lifecycle) {
|
|
968
|
+
const maxModelCalls = options.maxModelCalls ?? 100;
|
|
969
|
+
let modelCallCount = 0;
|
|
970
|
+
let budgetExceeded = false;
|
|
971
|
+
let warnedNonSerializable = false;
|
|
972
|
+
let warnedHeuristicIdle = false;
|
|
973
|
+
const runId = `run_${nextRunAgentTraceId++}`;
|
|
974
|
+
let traceSeq = 0;
|
|
975
|
+
const machineId = machine.config.id ?? machine.id ?? "(machine)";
|
|
976
|
+
const machineVersion = options.machineVersion ?? machine.version ?? require_decision.getMachineStructuralHash(machine);
|
|
977
|
+
const agentMeta = {
|
|
978
|
+
machineId,
|
|
979
|
+
version: machineVersion
|
|
980
|
+
};
|
|
981
|
+
const stampAgentMeta = (snapshot) => {
|
|
982
|
+
if (snapshot && typeof snapshot === "object") snapshot.agentMeta = agentMeta;
|
|
983
|
+
};
|
|
984
|
+
const onTrace = (event) => {
|
|
985
|
+
options.onTrace?.({
|
|
986
|
+
schemaVersion: 1,
|
|
987
|
+
runId,
|
|
988
|
+
seq: ++traceSeq,
|
|
989
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
990
|
+
machineId,
|
|
991
|
+
machineVersion,
|
|
992
|
+
...event
|
|
993
|
+
});
|
|
994
|
+
};
|
|
995
|
+
const consumeModelCall = () => {
|
|
996
|
+
if (budgetExceeded) throw new AgentMaxModelCallsExceededError();
|
|
997
|
+
if (modelCallCount + 1 > maxModelCalls) {
|
|
998
|
+
budgetExceeded = true;
|
|
999
|
+
throw new AgentMaxModelCallsExceededError();
|
|
1000
|
+
}
|
|
1001
|
+
modelCallCount += 1;
|
|
1002
|
+
};
|
|
1003
|
+
const tokenTotals = {};
|
|
1004
|
+
const cycleGate = { isResolved: () => false };
|
|
1005
|
+
const deliverUsageEvent = (usage, source) => {
|
|
1006
|
+
const actorRef = actorHolder.actorRef;
|
|
1007
|
+
if (!actorRef) return;
|
|
1008
|
+
const event = {
|
|
1009
|
+
type: require_setup_agent.AGENT_USAGE_EVENT_TYPE,
|
|
1010
|
+
...source,
|
|
1011
|
+
usage
|
|
1012
|
+
};
|
|
1013
|
+
const snapshot = actorRef.getSnapshot();
|
|
1014
|
+
if (snapshot?.status !== "active" || !declaresUsageTransition(snapshot)) return;
|
|
1015
|
+
if (cycleGate.isResolved()) {
|
|
1016
|
+
onTrace({
|
|
1017
|
+
type: "usage.dropped",
|
|
1018
|
+
event,
|
|
1019
|
+
reason: "settled"
|
|
1020
|
+
});
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
if (!snapshot.can(event)) return;
|
|
1024
|
+
actorRef.send(event);
|
|
1025
|
+
};
|
|
1026
|
+
const recordUsage = (usage, source = {}) => {
|
|
1027
|
+
for (const field of require_decision.AGENT_USAGE_TOKEN_FIELDS) {
|
|
1028
|
+
const value = usage[field];
|
|
1029
|
+
if (typeof value === "number" && Number.isFinite(value)) tokenTotals[field] = (tokenTotals[field] ?? 0) + value;
|
|
1030
|
+
}
|
|
1031
|
+
deliverUsageEvent(usage, source);
|
|
1032
|
+
};
|
|
1033
|
+
const runUsage = () => ({
|
|
1034
|
+
...tokenTotals,
|
|
1035
|
+
modelCalls: modelCallCount
|
|
1036
|
+
});
|
|
1037
|
+
const warnNonSerializableContext = (snapshot) => {
|
|
1038
|
+
if (warnedNonSerializable || process.env.NODE_ENV === "production") return;
|
|
1039
|
+
let offending = [];
|
|
1040
|
+
try {
|
|
1041
|
+
offending = require_decision.findNonSerializableContextPaths(snapshot.context);
|
|
1042
|
+
} catch {
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
if (offending.length === 0) return;
|
|
1046
|
+
warnedNonSerializable = true;
|
|
1047
|
+
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.`);
|
|
1048
|
+
};
|
|
1049
|
+
const provided = machine.provide({ actors: options.actors });
|
|
1050
|
+
const effectiveSources = provided.sources.actors;
|
|
1051
|
+
assertBindable(provided, effectiveSources, {
|
|
1052
|
+
hasGenerateText: !!options.executors?.generateText,
|
|
1053
|
+
hasDecide: !!options.executors?.decide,
|
|
1054
|
+
hasStreamText: !!options.executors?.streamText
|
|
1055
|
+
});
|
|
1056
|
+
if (options.getRequests && !options.executors?.generateText && !options.executors?.decide) throw new Error("runAgent: 'getRequests' requires a 'generateText' and/or 'decide' executor — the returned requests run through them.");
|
|
1057
|
+
const actorHolder = { actorRef: void 0 };
|
|
1058
|
+
const runCtx = {
|
|
1059
|
+
generateText: options.executors?.generateText,
|
|
1060
|
+
streamText: options.executors?.streamText,
|
|
1061
|
+
decide: options.executors?.decide,
|
|
1062
|
+
onChunk: options.onChunk,
|
|
1063
|
+
onResult: options.onResult,
|
|
1064
|
+
onTrace,
|
|
1065
|
+
consumeModelCall,
|
|
1066
|
+
recordUsage,
|
|
1067
|
+
actorHolder,
|
|
1068
|
+
runId,
|
|
1069
|
+
schemas: require_decision.getRegisteredAgentExecutionOptions(machine).schemas
|
|
1070
|
+
};
|
|
1071
|
+
let userInputIsPlaceholder = false;
|
|
1072
|
+
const wrappedSources = {};
|
|
1073
|
+
for (const [key, logic] of Object.entries(effectiveSources)) {
|
|
1074
|
+
if (key === "agent.userInput") {
|
|
1075
|
+
if (options.userInput) {
|
|
1076
|
+
const userInput = options.userInput;
|
|
1077
|
+
wrappedSources[key] = (0, xstate.createAsyncLogic)({ run: async ({ input }) => await userInput(input) });
|
|
1078
|
+
} else if (require_decision.isUnboundPlaceholder(logic)) {
|
|
1079
|
+
userInputIsPlaceholder = true;
|
|
1080
|
+
wrappedSources[key] = (0, xstate.createAsyncLogic)({ run: () => new Promise(() => {}) });
|
|
1081
|
+
}
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
if (require_decision.isDecisionLogic(logic)) {
|
|
1085
|
+
wrappedSources[key] = createRunAgentDecisionLogic(logic, runCtx);
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
if (require_decision.isPlanLogic(logic)) {
|
|
1089
|
+
wrappedSources[key] = createRunAgentPlanLogic(logic, runCtx);
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
if (require_decision.isTextLogic(logic)) {
|
|
1093
|
+
if (!require_decision.executorBoundLogics.has(logic)) wrappedSources[key] = wrapTextLogicForRunAgent(logic, runCtx);
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
if (isStateMachine(logic)) {
|
|
1097
|
+
const rebound = rebindChildMachine(logic, runCtx, new Set([machine]));
|
|
1098
|
+
if (rebound !== logic) wrappedSources[key] = rebound;
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
const boundMachine = provided.provide({ actors: wrappedSources });
|
|
1103
|
+
const declaredSuspensionPredicate = options.isSuspended ?? require_decision.getMachineSuspensionPredicate(machine);
|
|
1104
|
+
const isSuspended = declaredSuspensionPredicate ?? (() => false);
|
|
1105
|
+
let effectiveSnapshot = options.snapshot;
|
|
1106
|
+
if (effectiveSnapshot !== void 0) {
|
|
1107
|
+
const from = effectiveSnapshot.agentMeta?.version ?? effectiveSnapshot.version;
|
|
1108
|
+
if (from !== void 0 && from !== machineVersion) {
|
|
1109
|
+
const info = {
|
|
1110
|
+
from,
|
|
1111
|
+
to: machineVersion
|
|
1112
|
+
};
|
|
1113
|
+
if (options.migrateSnapshot) effectiveSnapshot = options.migrateSnapshot(effectiveSnapshot, info);
|
|
1114
|
+
else {
|
|
1115
|
+
const mode = options.onVersionMismatch ?? "throw";
|
|
1116
|
+
if (mode === "throw") throw new AgentSnapshotVersionMismatchError(from, machineVersion, machineId);
|
|
1117
|
+
if (mode === "warn") console.warn(`runAgent: resuming a snapshot stamped with machine version '${from}' against machine '${machineId}' at version '${machineVersion}'. Structural changes may not resume cleanly.`);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
const resumeEvents = options.events;
|
|
1122
|
+
if (effectiveSnapshot === void 0 && options.events !== void 0 && options.events[0]?.event.type === "@agent.init") {
|
|
1123
|
+
const { snapshot: replayedSnapshot } = require_setup_agent.replay(machine, resumeEvents, { machineVersion });
|
|
1124
|
+
effectiveSnapshot = machine.getPersistedSnapshot(replayedSnapshot);
|
|
1125
|
+
}
|
|
1126
|
+
const machineOwnVersion = machine.version;
|
|
1127
|
+
if (effectiveSnapshot !== void 0 && effectiveSnapshot.version !== machineOwnVersion) {
|
|
1128
|
+
const aligned = Object.assign(Object.create(Object.getPrototypeOf(effectiveSnapshot)), effectiveSnapshot);
|
|
1129
|
+
if (machineOwnVersion === void 0) delete aligned.version;
|
|
1130
|
+
else aligned.version = machineOwnVersion;
|
|
1131
|
+
effectiveSnapshot = aligned;
|
|
1132
|
+
}
|
|
1133
|
+
const priorMessages = require_decision.getAgentMessages(effectiveSnapshot);
|
|
1134
|
+
const messages = typeof options.messages === "function" ? [...options.messages([...priorMessages])] : [...priorMessages, ...options.messages ?? []];
|
|
1135
|
+
const stampMessages = (snapshot) => {
|
|
1136
|
+
if (!options.getRequests && !options.messages && messages.length === 0) return;
|
|
1137
|
+
if (snapshot && typeof snapshot === "object") snapshot.messages = [...messages];
|
|
1138
|
+
};
|
|
1139
|
+
if (effectiveSnapshot !== void 0 && options.event !== void 0 && (options.onIllegalResumeEvent ?? "throw") === "throw") {
|
|
1140
|
+
const acceptedTypes = require_decision.getAcceptedEvents((0, xstate.createActor)(boundMachine, { snapshot: effectiveSnapshot }).getSnapshot(), { schemas: runCtx.schemas }).map((descriptor) => descriptor.type);
|
|
1141
|
+
const eventType = options.event.type;
|
|
1142
|
+
if (!acceptedTypes.includes(eventType)) throw new AgentIllegalResumeEventError(eventType, acceptedTypes);
|
|
1143
|
+
}
|
|
1144
|
+
const replayEvents = [...resumeEvents ?? []];
|
|
1145
|
+
const replayEventIds = /* @__PURE__ */ new Set();
|
|
1146
|
+
for (let index = 0; index < replayEvents.length; index++) {
|
|
1147
|
+
const entry = replayEvents[index];
|
|
1148
|
+
require_event_log_store.assertAgentLogEntry(entry);
|
|
1149
|
+
if (entry.index !== index) throw new Error(`runAgent events must be contiguous from index 0; found entry.index ${entry.index} at position ${index}.`);
|
|
1150
|
+
if (entry.machineId !== machineId || entry.machineVersion !== machineVersion) throw new require_setup_agent.AgentReplayMachineMismatchError(entry.id, entry.index, {
|
|
1151
|
+
machineId,
|
|
1152
|
+
machineVersion
|
|
1153
|
+
}, {
|
|
1154
|
+
machineId: entry.machineId,
|
|
1155
|
+
machineVersion: entry.machineVersion
|
|
1156
|
+
});
|
|
1157
|
+
if (replayEventIds.has(entry.id)) throw new Error(`runAgent events contain duplicate event id '${entry.id}'.`);
|
|
1158
|
+
replayEventIds.add(entry.id);
|
|
1159
|
+
}
|
|
1160
|
+
const hasCompleteReplayHistory = replayEvents[0]?.event.type === "@agent.init" || effectiveSnapshot === void 0 && replayEvents.length === 0;
|
|
1161
|
+
const appendReplayEvent = (event) => {
|
|
1162
|
+
const entry = require_setup_agent.createReplayEntry(machine, replayEvents, event, {
|
|
1163
|
+
machineVersion,
|
|
1164
|
+
verification: hasCompleteReplayHistory
|
|
1165
|
+
});
|
|
1166
|
+
if (replayEventIds.has(entry.id)) throw new Error(`runAgent generated duplicate event id '${entry.id}'.`);
|
|
1167
|
+
replayEventIds.add(entry.id);
|
|
1168
|
+
replayEvents.push(entry);
|
|
1169
|
+
options.onEvent?.(entry);
|
|
1170
|
+
return entry;
|
|
1171
|
+
};
|
|
1172
|
+
if (replayEvents.length === 0 && effectiveSnapshot === void 0) {
|
|
1173
|
+
const entry = require_setup_agent.initEntry(machine, options.input, { machineVersion });
|
|
1174
|
+
replayEventIds.add(entry.id);
|
|
1175
|
+
replayEvents.push(entry);
|
|
1176
|
+
options.onEvent?.(entry);
|
|
1177
|
+
}
|
|
1178
|
+
return (() => {
|
|
1179
|
+
let settled = false;
|
|
1180
|
+
let finalized = false;
|
|
1181
|
+
cycleGate.isResolved = () => settled;
|
|
1182
|
+
let lastResult;
|
|
1183
|
+
const waiters = [];
|
|
1184
|
+
let idleTimer;
|
|
1185
|
+
let actor;
|
|
1186
|
+
let deliveringResumeEvent = options.event !== void 0;
|
|
1187
|
+
const settle = (outcome) => {
|
|
1188
|
+
if (settled) return;
|
|
1189
|
+
settled = true;
|
|
1190
|
+
const result = {
|
|
1191
|
+
...outcome,
|
|
1192
|
+
events: [...replayEvents],
|
|
1193
|
+
usage: runUsage()
|
|
1194
|
+
};
|
|
1195
|
+
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
1196
|
+
stampAgentMeta(result.snapshot);
|
|
1197
|
+
stampMessages(result.snapshot);
|
|
1198
|
+
if ("persistedSnapshot" in result) {
|
|
1199
|
+
stampAgentMeta(result.persistedSnapshot);
|
|
1200
|
+
stampMessages(result.persistedSnapshot);
|
|
1201
|
+
}
|
|
1202
|
+
onTrace({
|
|
1203
|
+
type: "run.end",
|
|
1204
|
+
...outcome
|
|
1205
|
+
});
|
|
1206
|
+
if (lifecycle.oneShot || result.status !== "idle") {
|
|
1207
|
+
finalized = true;
|
|
1208
|
+
if (options.signal) options.signal.removeEventListener("abort", onAbort);
|
|
1209
|
+
actor.stop();
|
|
1210
|
+
}
|
|
1211
|
+
lastResult = result;
|
|
1212
|
+
for (const resolve of waiters.splice(0)) resolve(result);
|
|
1213
|
+
};
|
|
1214
|
+
const onAbort = () => {
|
|
1215
|
+
settle({
|
|
1216
|
+
status: "error",
|
|
1217
|
+
cause: "aborted",
|
|
1218
|
+
error: options.signal?.reason ?? /* @__PURE__ */ new Error("Aborted"),
|
|
1219
|
+
snapshot: actor.getSnapshot()
|
|
1220
|
+
});
|
|
1221
|
+
};
|
|
1222
|
+
const settleIdle = (current) => {
|
|
1223
|
+
warnNonSerializableContext(current);
|
|
1224
|
+
const pendingUserInputs = userInputIsPlaceholder ? collectPendingUserInputs(current) : [];
|
|
1225
|
+
settle({
|
|
1226
|
+
status: "idle",
|
|
1227
|
+
snapshot: current,
|
|
1228
|
+
...pendingUserInputs.length > 0 ? {
|
|
1229
|
+
pendingUserInputs,
|
|
1230
|
+
persistedSnapshot: actor.getPersistedSnapshot()
|
|
1231
|
+
} : {}
|
|
1232
|
+
});
|
|
1233
|
+
};
|
|
1234
|
+
let interpreting = false;
|
|
1235
|
+
let interpretSeq = 0;
|
|
1236
|
+
const appendToLog = (...items) => {
|
|
1237
|
+
messages.push(...items);
|
|
1238
|
+
if (options.onMessage) {
|
|
1239
|
+
const info = {
|
|
1240
|
+
runId,
|
|
1241
|
+
machineId,
|
|
1242
|
+
machineVersion
|
|
1243
|
+
};
|
|
1244
|
+
for (const item of items) options.onMessage(item, info);
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
const runErrorCause = (error) => budgetExceeded ? "max-model-calls" : wrapsDecisionExhausted(error) ? "decision-exhausted" : "machine";
|
|
1248
|
+
const settleInterpretError = (error) => {
|
|
1249
|
+
settle({
|
|
1250
|
+
status: "error",
|
|
1251
|
+
cause: runErrorCause(error),
|
|
1252
|
+
error,
|
|
1253
|
+
snapshot: actor.getSnapshot()
|
|
1254
|
+
});
|
|
1255
|
+
};
|
|
1256
|
+
const passDeps = {
|
|
1257
|
+
getSnapshot: () => actor.getSnapshot(),
|
|
1258
|
+
send: (event) => actor.send(event),
|
|
1259
|
+
isSettled: () => settled,
|
|
1260
|
+
messages,
|
|
1261
|
+
appendToLog,
|
|
1262
|
+
generateText: runCtx.generateText,
|
|
1263
|
+
decide: runCtx.decide ? createCountingDecide(runCtx, void 0) : void 0,
|
|
1264
|
+
consumeModelCall,
|
|
1265
|
+
recordUsage,
|
|
1266
|
+
nextRequestId: () => `interpret_${++interpretSeq}`,
|
|
1267
|
+
onTrace,
|
|
1268
|
+
onResult: runCtx.onResult,
|
|
1269
|
+
schemas: runCtx.schemas,
|
|
1270
|
+
signal: options.signal
|
|
1271
|
+
};
|
|
1272
|
+
const maybeInterpret = (snapshot) => {
|
|
1273
|
+
if (!options.getRequests || settled) return false;
|
|
1274
|
+
if (interpreting) return true;
|
|
1275
|
+
let requested;
|
|
1276
|
+
try {
|
|
1277
|
+
requested = options.getRequests(snapshot, { messages });
|
|
1278
|
+
} catch (error) {
|
|
1279
|
+
settleInterpretError(error);
|
|
1280
|
+
return true;
|
|
1281
|
+
}
|
|
1282
|
+
const requests = (Array.isArray(requested) ? requested : requested ? [requested] : []).filter((stateRequest) => Boolean(stateRequest));
|
|
1283
|
+
if (requests.length === 0) return false;
|
|
1284
|
+
interpreting = true;
|
|
1285
|
+
runStateRequestPass(requests, passDeps).then(({ sentAny }) => {
|
|
1286
|
+
if (settled || sentAny) return;
|
|
1287
|
+
const current = actor.getSnapshot();
|
|
1288
|
+
if (isIdleSnapshot(current, { ignoreUserInputChildren: userInputIsPlaceholder })) settleIdle(current);
|
|
1289
|
+
}).catch((error) => settleInterpretError(error)).finally(() => {
|
|
1290
|
+
interpreting = false;
|
|
1291
|
+
if (!settled) scheduleIdleCheck();
|
|
1292
|
+
});
|
|
1293
|
+
return true;
|
|
1294
|
+
};
|
|
1295
|
+
const scheduleIdleCheck = () => {
|
|
1296
|
+
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
1297
|
+
idleTimer = setTimeout(() => {
|
|
1298
|
+
idleTimer = void 0;
|
|
1299
|
+
if (settled) return;
|
|
1300
|
+
const current = actor.getSnapshot();
|
|
1301
|
+
if (isIdleSnapshot(current, { ignoreUserInputChildren: userInputIsPlaceholder })) {
|
|
1302
|
+
if (!maybeInterpret(current)) {
|
|
1303
|
+
if (!declaredSuspensionPredicate && current.status === "active" && !warnedHeuristicIdle && process.env.NODE_ENV !== "production") {
|
|
1304
|
+
warnedHeuristicIdle = true;
|
|
1305
|
+
console.warn("[@statelyai/agent] runAgent settled idle via the timing heuristic (no suspension predicate declared). This is best-effort; for deterministic idle detection, declare setupAgent({ isSuspended }) or pass runAgent(machine, { isSuspended }), e.g. (s) => s.hasTag('waiting').");
|
|
1306
|
+
}
|
|
1307
|
+
settleIdle(current);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}, 0);
|
|
1311
|
+
};
|
|
1312
|
+
actor = (0, xstate.createActor)(boundMachine, {
|
|
1313
|
+
input: options.input,
|
|
1314
|
+
snapshot: effectiveSnapshot,
|
|
1315
|
+
inspect: (event) => {
|
|
1316
|
+
if (typeof options.inspect === "function") options.inspect(event);
|
|
1317
|
+
else options.inspect?.next?.(event);
|
|
1318
|
+
if (event.type !== "@xstate.transition" || event.actorRef !== actor.ref) return;
|
|
1319
|
+
if (settled) {
|
|
1320
|
+
if (finalized) return;
|
|
1321
|
+
settled = false;
|
|
1322
|
+
lastResult = void 0;
|
|
1323
|
+
}
|
|
1324
|
+
const snapshot = event.snapshot;
|
|
1325
|
+
let eventId;
|
|
1326
|
+
if (event.event.type !== "@xstate.init" && (event.sourceRef !== event.actorRef || event.event.type === "xstate.timer")) eventId = appendReplayEvent(event.event).id;
|
|
1327
|
+
else if (event.event.type === "@xstate.init") eventId = replayEvents[0]?.event.type === "@agent.init" ? replayEvents[0].id : void 0;
|
|
1328
|
+
onTrace({
|
|
1329
|
+
type: "machine.transition",
|
|
1330
|
+
snapshot,
|
|
1331
|
+
event: event.event,
|
|
1332
|
+
...eventId !== void 0 ? { eventId } : {}
|
|
1333
|
+
});
|
|
1334
|
+
options.onTransition?.(snapshot, event.event);
|
|
1335
|
+
if (snapshot.status === "done") {
|
|
1336
|
+
settle({
|
|
1337
|
+
status: "done",
|
|
1338
|
+
output: snapshot.output,
|
|
1339
|
+
snapshot
|
|
1340
|
+
});
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
if (snapshot.status === "error") {
|
|
1344
|
+
settle({
|
|
1345
|
+
status: "error",
|
|
1346
|
+
cause: runErrorCause(snapshot.error),
|
|
1347
|
+
error: snapshot.error,
|
|
1348
|
+
snapshot
|
|
1349
|
+
});
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
if (snapshot.status === "stopped") {
|
|
1353
|
+
settle({
|
|
1354
|
+
status: "error",
|
|
1355
|
+
cause: "stopped",
|
|
1356
|
+
error: /* @__PURE__ */ new Error("Actor stopped externally."),
|
|
1357
|
+
snapshot
|
|
1358
|
+
});
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
if (!deliveringResumeEvent && isSuspended(snapshot) && isIdleSnapshot(snapshot, { ignoreUserInputChildren: userInputIsPlaceholder })) {
|
|
1362
|
+
if (!maybeInterpret(snapshot)) settleIdle(snapshot);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
scheduleIdleCheck();
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
actorHolder.actorRef = actor;
|
|
1369
|
+
actor.subscribe({ error: () => {} });
|
|
1370
|
+
actor.on("*", (event) => {
|
|
1371
|
+
onTrace({
|
|
1372
|
+
type: "emit",
|
|
1373
|
+
event
|
|
1374
|
+
});
|
|
1375
|
+
});
|
|
1376
|
+
for (const [type, handler] of Object.entries(options.on ?? {})) if (typeof handler === "function") actor.on(type, handler);
|
|
1377
|
+
const sessionApi = {
|
|
1378
|
+
actor,
|
|
1379
|
+
get events() {
|
|
1380
|
+
return replayEvents;
|
|
1381
|
+
},
|
|
1382
|
+
usage: runUsage,
|
|
1383
|
+
settled: () => settled && lastResult !== void 0 ? Promise.resolve(lastResult) : new Promise((resolve) => {
|
|
1384
|
+
waiters.push(resolve);
|
|
1385
|
+
}),
|
|
1386
|
+
stop: () => {
|
|
1387
|
+
actor.stop();
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
if (options.signal) {
|
|
1391
|
+
if (options.signal.aborted) {
|
|
1392
|
+
settle({
|
|
1393
|
+
status: "error",
|
|
1394
|
+
cause: "aborted",
|
|
1395
|
+
error: options.signal.reason ?? /* @__PURE__ */ new Error("Aborted"),
|
|
1396
|
+
snapshot: actor.getSnapshot()
|
|
1397
|
+
});
|
|
1398
|
+
return sessionApi;
|
|
1399
|
+
}
|
|
1400
|
+
options.signal.addEventListener("abort", onAbort);
|
|
1401
|
+
}
|
|
1402
|
+
onTrace({
|
|
1403
|
+
type: "run.start",
|
|
1404
|
+
...options.input !== void 0 ? { input: options.input } : {},
|
|
1405
|
+
...effectiveSnapshot !== void 0 ? { snapshot: effectiveSnapshot } : {},
|
|
1406
|
+
...options.event !== void 0 ? { event: options.event } : {}
|
|
1407
|
+
});
|
|
1408
|
+
actor.start();
|
|
1409
|
+
if (options.event) {
|
|
1410
|
+
deliveringResumeEvent = false;
|
|
1411
|
+
actor.send(options.event);
|
|
1412
|
+
}
|
|
1413
|
+
return sessionApi;
|
|
1414
|
+
})();
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Runs an agent machine to a **final state**, for run-to-done flows where an
|
|
1418
|
+
* idle pause is unexpected. Wraps {@link runAgent}:
|
|
1419
|
+
*
|
|
1420
|
+
* - `done` → resolves with the done result: `result.output` (the machine's
|
|
1421
|
+
* `OutputFrom`) plus metadata — `result.snapshot`, the replayable
|
|
1422
|
+
* `result.events`, and the aggregated `result.usage` — the same shape
|
|
1423
|
+
* `generateText` users expect (`text` + call metadata).
|
|
1424
|
+
* - `idle` → throws {@link AgentIdleError} carrying the idle snapshot and the
|
|
1425
|
+
* event types that could resume it.
|
|
1426
|
+
* - `error` → throws `result.error` when it is an `Error`; otherwise wraps it
|
|
1427
|
+
* in an `Error` whose `.cause` is the {@link RunAgentErrorCause} and whose
|
|
1428
|
+
* `.error` is the raw thrown value.
|
|
1429
|
+
*
|
|
1430
|
+
* Use {@link runAgent} directly when idle is an expected outcome you handle
|
|
1431
|
+
* (human-in-the-loop, resumable flows); use `generateResult` when the
|
|
1432
|
+
* machine is meant to run straight through to a final state.
|
|
1433
|
+
*/
|
|
1434
|
+
async function generateResult(machine, options) {
|
|
1435
|
+
const result = await runAgent(machine, options);
|
|
1436
|
+
if (result.status === "done") return result;
|
|
1437
|
+
if (result.status === "idle") {
|
|
1438
|
+
const acceptedTypes = require_decision.getAcceptedEvents(result.snapshot, { schemas: require_decision.getRegisteredAgentExecutionOptions(machine).schemas }).map((descriptor) => descriptor.type);
|
|
1439
|
+
throw new AgentIdleError(result.snapshot, acceptedTypes);
|
|
1440
|
+
}
|
|
1441
|
+
if (result.error instanceof Error) throw result.error;
|
|
1442
|
+
const wrapped = /* @__PURE__ */ new Error(`generateResult: run failed with cause '${result.cause}'.`);
|
|
1443
|
+
wrapped.cause = result.cause;
|
|
1444
|
+
wrapped.error = result.error;
|
|
1445
|
+
throw wrapped;
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Wraps a `(snapshot, actorRef) => void` handler into a function usable as
|
|
1449
|
+
* {@link RunAgentOptions.inspect}: it filters the raw inspection stream to
|
|
1450
|
+
* `@xstate.transition` events and hands the handler the typed
|
|
1451
|
+
* {@link AnyMachineSnapshot} and the {@link InspectedActorRef} that
|
|
1452
|
+
* transitioned. Attribute a child actor via `actorRef.id`/`actorRef.src`. Saves
|
|
1453
|
+
* the manual `event.type === '@xstate.transition'` filtering and the snapshot/
|
|
1454
|
+
* actorRef casts.
|
|
1455
|
+
*/
|
|
1456
|
+
function inspectTransitions(handler) {
|
|
1457
|
+
return (inspectionEvent) => {
|
|
1458
|
+
if (inspectionEvent.type !== "@xstate.transition") return;
|
|
1459
|
+
handler(inspectionEvent.snapshot, inspectionEvent.actorRef);
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* An xstate `inspect` handler that emits `machine.transition` trace events onto
|
|
1464
|
+
* `onTrace`, sharing the SAME versioned envelope and per-root-actor `seq`
|
|
1465
|
+
* registry as {@link provideExecutors}' `onTrace`. Pair the two on one actor to
|
|
1466
|
+
* get a single ordered trace stream (request + transition events) for the
|
|
1467
|
+
* uncontrolled path:
|
|
1468
|
+
*
|
|
1469
|
+
* ```ts
|
|
1470
|
+
* const bound = provideExecutors(machine, executors, { onTrace });
|
|
1471
|
+
* const actor = createActor(bound, { inspect: traceTransitions(onTrace) });
|
|
1472
|
+
* ```
|
|
1473
|
+
*
|
|
1474
|
+
* Only ROOT-actor transitions are traced (matching `runAgent`'s
|
|
1475
|
+
* `machine.transition`); child-actor transitions are ignored. Attribute the
|
|
1476
|
+
* event via its envelope `runId`.
|
|
1477
|
+
*
|
|
1478
|
+
* By design this path has NO `run.start`/`run.end` events: `createActor` has no
|
|
1479
|
+
* run boundary the way `runAgent` does, so the stream starts at the actor's
|
|
1480
|
+
* first transition. It also does NOT emit `emit` trace events: in this xstate
|
|
1481
|
+
* build emitted events are delivered through `actor.on(...)`, not the inspection
|
|
1482
|
+
* protocol, so they are not observable from an `inspect` handler — subscribe
|
|
1483
|
+
* with `actor.on('*', ...)` if you need them.
|
|
1484
|
+
*/
|
|
1485
|
+
function traceTransitions(onTrace) {
|
|
1486
|
+
return (inspectionEvent) => {
|
|
1487
|
+
if (inspectionEvent.type !== "@xstate.transition") return;
|
|
1488
|
+
const actorRef = inspectionEvent.actorRef;
|
|
1489
|
+
if (actorRef?._parent) return;
|
|
1490
|
+
onTrace(stampRootTrace(actorRef, {
|
|
1491
|
+
type: "machine.transition",
|
|
1492
|
+
snapshot: inspectionEvent.snapshot,
|
|
1493
|
+
event: inspectionEvent.event
|
|
1494
|
+
}));
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
function isIdleSnapshot(snapshot, { ignoreUserInputChildren }) {
|
|
1498
|
+
if (snapshot.status !== "active") return false;
|
|
1499
|
+
if (Object.values(snapshot.children ?? {}).some((child) => {
|
|
1500
|
+
const ref = child;
|
|
1501
|
+
if (ignoreUserInputChildren && ref?.src === "agent.userInput") return false;
|
|
1502
|
+
return ref?.getSnapshot?.()?.status === "active";
|
|
1503
|
+
})) return false;
|
|
1504
|
+
return !(0, xstate.getNextTransitions)(snapshot).some((transitionDef) => transitionDef.eventType === "" || transitionDef.eventType.startsWith("xstate.after"));
|
|
1505
|
+
}
|
|
1506
|
+
function collectPendingUserInputs(snapshot) {
|
|
1507
|
+
const pending = [];
|
|
1508
|
+
for (const [id, child] of Object.entries(snapshot.children ?? {})) {
|
|
1509
|
+
const ref = child;
|
|
1510
|
+
if (ref?.src !== "agent.userInput") continue;
|
|
1511
|
+
const childSnapshot = ref.getSnapshot?.();
|
|
1512
|
+
if (childSnapshot?.status !== "active") continue;
|
|
1513
|
+
pending.push({
|
|
1514
|
+
id,
|
|
1515
|
+
input: childSnapshot.input
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
return pending;
|
|
1519
|
+
}
|
|
1520
|
+
//#endregion
|
|
1521
|
+
//#region src/agent-run.ts
|
|
1522
|
+
/**
|
|
1523
|
+
* Wraps {@link runAgent} in a canonical run-stream handle: the run's trace
|
|
1524
|
+
* events as a pull-based {@link AgentRun.events} async iterator, alongside the
|
|
1525
|
+
* unchanged {@link AgentRun.result} promise.
|
|
1526
|
+
*
|
|
1527
|
+
* Semantics:
|
|
1528
|
+
*
|
|
1529
|
+
* 1. **Starts immediately.** The underlying `runAgent` is invoked on this call,
|
|
1530
|
+
* not on the first `events` iteration — work is already in flight when
|
|
1531
|
+
* `createAgentRun` returns.
|
|
1532
|
+
* 2. **Buffer, don't stall.** Events are queued in an unbounded in-memory
|
|
1533
|
+
* buffer as the run emits them; a slow or absent consumer never applies
|
|
1534
|
+
* backpressure to the run. Once `run.end` is delivered, the iterator
|
|
1535
|
+
* completes.
|
|
1536
|
+
* 3. **Composes `onTrace`.** If `options.onTrace` is passed, it still fires for
|
|
1537
|
+
* every event — the wrapper adds queue delivery, it does not replace the
|
|
1538
|
+
* caller's sink.
|
|
1539
|
+
* 4. **`result` mirrors `runAgent`.** Same resolution and rejection behavior
|
|
1540
|
+
* (see {@link AgentRun.result}). A no-op rejection handler is attached
|
|
1541
|
+
* internally so a caller who reads only `events` never trips an unhandled
|
|
1542
|
+
* rejection on a bind-time throw; the returned promise still rejects for a
|
|
1543
|
+
* caller who awaits it.
|
|
1544
|
+
* 5. **Resume is identical.** Options pass straight through, so starting from a
|
|
1545
|
+
* persisted `snapshot` (+ resume `event`) streams that run's events from its
|
|
1546
|
+
* own `run.start`, exactly as `runAgent` would run it.
|
|
1547
|
+
* 6. **Early termination does not cancel the run.** Breaking out of a
|
|
1548
|
+
* `for await` (or calling `events.return()`) stops delivery, but the run
|
|
1549
|
+
* keeps going and `result` still settles. Cancelling the run itself is
|
|
1550
|
+
* future work — pass `options.signal` to abort it.
|
|
1551
|
+
* 7. **`events` is single-consumer.** See {@link AgentRun.events}.
|
|
1552
|
+
*/
|
|
1553
|
+
function createAgentRun(machine, options) {
|
|
1554
|
+
const buffer = [];
|
|
1555
|
+
const resolvers = [];
|
|
1556
|
+
let closed = false;
|
|
1557
|
+
let stopped = false;
|
|
1558
|
+
const push = (event) => {
|
|
1559
|
+
if (closed || stopped) return;
|
|
1560
|
+
const resolve = resolvers.shift();
|
|
1561
|
+
if (resolve) resolve({
|
|
1562
|
+
value: event,
|
|
1563
|
+
done: false
|
|
1564
|
+
});
|
|
1565
|
+
else buffer.push(event);
|
|
1566
|
+
};
|
|
1567
|
+
const close = () => {
|
|
1568
|
+
if (closed) return;
|
|
1569
|
+
closed = true;
|
|
1570
|
+
while (resolvers.length > 0) resolvers.shift()({
|
|
1571
|
+
value: void 0,
|
|
1572
|
+
done: true
|
|
1573
|
+
});
|
|
1574
|
+
};
|
|
1575
|
+
const userOnTrace = options.onTrace;
|
|
1576
|
+
const result = runAgent(machine, {
|
|
1577
|
+
...options,
|
|
1578
|
+
onTrace: (event) => {
|
|
1579
|
+
push(event);
|
|
1580
|
+
userOnTrace?.(event);
|
|
1581
|
+
if (event.type === "run.end") close();
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
result.then(close, close);
|
|
1585
|
+
return {
|
|
1586
|
+
events: {
|
|
1587
|
+
next() {
|
|
1588
|
+
if (buffer.length > 0) return Promise.resolve({
|
|
1589
|
+
value: buffer.shift(),
|
|
1590
|
+
done: false
|
|
1591
|
+
});
|
|
1592
|
+
if (closed || stopped) return Promise.resolve({
|
|
1593
|
+
value: void 0,
|
|
1594
|
+
done: true
|
|
1595
|
+
});
|
|
1596
|
+
return new Promise((resolve) => {
|
|
1597
|
+
resolvers.push(resolve);
|
|
1598
|
+
});
|
|
1599
|
+
},
|
|
1600
|
+
return() {
|
|
1601
|
+
stopped = true;
|
|
1602
|
+
buffer.length = 0;
|
|
1603
|
+
while (resolvers.length > 0) resolvers.shift()({
|
|
1604
|
+
value: void 0,
|
|
1605
|
+
done: true
|
|
1606
|
+
});
|
|
1607
|
+
return Promise.resolve({
|
|
1608
|
+
value: void 0,
|
|
1609
|
+
done: true
|
|
1610
|
+
});
|
|
1611
|
+
},
|
|
1612
|
+
[Symbol.asyncIterator]() {
|
|
1613
|
+
return this;
|
|
1614
|
+
}
|
|
1615
|
+
},
|
|
1616
|
+
result
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
//#endregion
|
|
1620
|
+
//#region src/provide-executors.ts
|
|
1621
|
+
/**
|
|
1622
|
+
* Binds a machine's agent actor sources to a set of host `executors` in one
|
|
1623
|
+
* call, returning a `machine.provide(...)`-ed copy ready for a plain
|
|
1624
|
+
* `createActor(...)` — the uncontrolled-mode counterpart to {@link runAgent}.
|
|
1625
|
+
* No run loop, no idle settling: the returned machine drives itself, so
|
|
1626
|
+
*
|
|
1627
|
+
* ```ts
|
|
1628
|
+
* const actor = createActor(provideExecutors(machine, { generateText, decide }), { input });
|
|
1629
|
+
* actor.start();
|
|
1630
|
+
* ```
|
|
1631
|
+
*
|
|
1632
|
+
* behaves like a normal XState actor whose agent invokes now reach real models.
|
|
1633
|
+
*
|
|
1634
|
+
* Binding pass over `machine.sources.actors` (after merging
|
|
1635
|
+
* `options.actors`):
|
|
1636
|
+
* - `mode: 'generate'` text source → `executors.generateText`
|
|
1637
|
+
* - `mode: 'stream'` text source → `executors.streamText`
|
|
1638
|
+
* - decision / `agent.decide` source → `executors.decide` (snapshot-driven
|
|
1639
|
+
* candidate events, guard `canTake`, and auto-delivery of the chosen event,
|
|
1640
|
+
* mirroring `runAgent` but without its model-call counting)
|
|
1641
|
+
* - `agent.plan` source → `executors.decide` (iterated, same semantics)
|
|
1642
|
+
*
|
|
1643
|
+
* Pass `options.onTrace` to observe request-level trace events (identical in
|
|
1644
|
+
* shape to `runAgent`'s); pair it with {@link traceTransitions} on the actor's
|
|
1645
|
+
* `inspect` to also capture `machine.transition` events in the same stream.
|
|
1646
|
+
*
|
|
1647
|
+
* A source that already carries its own executor (`.withExecutor(...)`) is left
|
|
1648
|
+
* as-is. `agent.userInput` is left UNBOUND — an uncontrolled host handles idle
|
|
1649
|
+
* itself, so supply a handler via `options.actors` if the machine uses it.
|
|
1650
|
+
* Non-agent actors are untouched.
|
|
1651
|
+
*
|
|
1652
|
+
* Throws at bind time if a source needs an executor kind that `executors` does
|
|
1653
|
+
* not provide.
|
|
1654
|
+
*
|
|
1655
|
+
* v1 does NOT descend into invoked child state machines: a string-keyed child
|
|
1656
|
+
* machine source is left untouched, so a child with its own agent invokes needs
|
|
1657
|
+
* its own `provideExecutors(...)` (or `runAgent`, which does rebind children).
|
|
1658
|
+
*/
|
|
1659
|
+
function provideExecutors(machine, executors, options = {}) {
|
|
1660
|
+
const bindOptions = {
|
|
1661
|
+
onChunk: options.onChunk,
|
|
1662
|
+
onTrace: options.onTrace
|
|
1663
|
+
};
|
|
1664
|
+
const provided = options.actors ? machine.provide({ actors: options.actors }) : machine;
|
|
1665
|
+
const effectiveSources = provided.sources.actors;
|
|
1666
|
+
const wrappedSources = {};
|
|
1667
|
+
const invokedSrcs = getConfiguredInvokeSrcs(provided);
|
|
1668
|
+
for (const [key, logic] of Object.entries(effectiveSources)) {
|
|
1669
|
+
if (key === "agent.userInput") continue;
|
|
1670
|
+
if (require_decision.isDecisionLogic(logic)) {
|
|
1671
|
+
if (require_decision.executorBoundLogics.has(logic)) continue;
|
|
1672
|
+
if (!executors.decide) {
|
|
1673
|
+
if (invokedSrcs.has(key)) throw missingExecutorError(key, "decision", "decide");
|
|
1674
|
+
continue;
|
|
1675
|
+
}
|
|
1676
|
+
wrappedSources[key] = bindDecisionForProvide(provided, logic, executors, bindOptions);
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1679
|
+
if (require_decision.isPlanLogic(logic)) {
|
|
1680
|
+
if (!executors.decide) {
|
|
1681
|
+
if (invokedSrcs.has(key)) throw missingExecutorError(key, "plan", "decide");
|
|
1682
|
+
continue;
|
|
1683
|
+
}
|
|
1684
|
+
wrappedSources[key] = bindPlanForProvide(provided, logic, executors, bindOptions);
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
if (require_decision.isTextLogic(logic)) {
|
|
1688
|
+
if (require_decision.executorBoundLogics.has(logic)) continue;
|
|
1689
|
+
if (!(logic.mode === "stream" ? executors.streamText : executors.generateText)) {
|
|
1690
|
+
if (invokedSrcs.has(key)) throw logic.mode === "stream" ? missingExecutorError(key, "streaming text", "streamText") : missingExecutorError(key, "text", "generateText");
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
wrappedSources[key] = bindTextForProvide(provided, logic, executors, bindOptions);
|
|
1694
|
+
continue;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
return provided.provide({ actors: wrappedSources });
|
|
1698
|
+
}
|
|
1699
|
+
function missingExecutorError(src, kind, executor) {
|
|
1700
|
+
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.`);
|
|
1701
|
+
}
|
|
1702
|
+
//#endregion
|
|
1703
|
+
//#region src/verify.ts
|
|
1704
|
+
const DECIDE_SRC = "agent.decide";
|
|
1705
|
+
const PLAN_SRC = "agent.plan";
|
|
1706
|
+
function normalizeInvokes(invoke) {
|
|
1707
|
+
if (invoke === void 0 || invoke === null) return [];
|
|
1708
|
+
return Array.isArray(invoke) ? invoke : [invoke];
|
|
1709
|
+
}
|
|
1710
|
+
function buildStateIndex(rootConfig) {
|
|
1711
|
+
const index = /* @__PURE__ */ new Map();
|
|
1712
|
+
const walk = (states, parentPath) => {
|
|
1713
|
+
for (const [name, config] of Object.entries(states ?? {})) {
|
|
1714
|
+
const path = parentPath ? `${parentPath}.${name}` : name;
|
|
1715
|
+
const hasChildren = !!config.states && Object.keys(config.states).length > 0;
|
|
1716
|
+
index.set(path, {
|
|
1717
|
+
path,
|
|
1718
|
+
name,
|
|
1719
|
+
config,
|
|
1720
|
+
parentPath,
|
|
1721
|
+
type: config.type,
|
|
1722
|
+
isFinal: config.type === "final",
|
|
1723
|
+
isParallel: config.type === "parallel",
|
|
1724
|
+
isCompound: hasChildren && config.type !== "parallel",
|
|
1725
|
+
invokes: normalizeInvokes(config.invoke)
|
|
1726
|
+
});
|
|
1727
|
+
if (hasChildren) walk(config.states, path);
|
|
1728
|
+
}
|
|
1729
|
+
};
|
|
1730
|
+
walk(rootConfig.states, "");
|
|
1731
|
+
return index;
|
|
1732
|
+
}
|
|
1733
|
+
function childrenOf(index, parentPath) {
|
|
1734
|
+
const out = [];
|
|
1735
|
+
for (const node of index.values()) if (node.parentPath === parentPath) out.push(node);
|
|
1736
|
+
return out;
|
|
1737
|
+
}
|
|
1738
|
+
function collectTransitionTargets(value, fromNode, index, out) {
|
|
1739
|
+
if (value === void 0 || value === null) return;
|
|
1740
|
+
if (Array.isArray(value)) {
|
|
1741
|
+
for (const item of value) collectTransitionTargets(item, fromNode, index, out);
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
if (typeof value === "function") {
|
|
1745
|
+
out.opaque = true;
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
if (typeof value === "string") {
|
|
1749
|
+
resolveTargetString(value, fromNode, index, out);
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
if (typeof value === "object") {
|
|
1753
|
+
const { target, to } = value;
|
|
1754
|
+
if (target !== void 0) {
|
|
1755
|
+
collectTransitionTargets(target, fromNode, index, out);
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
if (to !== void 0) {
|
|
1759
|
+
collectTransitionTargets(to, fromNode, index, out);
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
function resolveTargetString(target, fromNode, index, out) {
|
|
1765
|
+
if (target.startsWith("#")) {
|
|
1766
|
+
out.opaque = true;
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
const resolved = target.startsWith(".") ? `${fromNode.path}.${target.slice(1)}` : fromNode.parentPath ? `${fromNode.parentPath}.${target}` : target;
|
|
1770
|
+
if (index.has(resolved)) out.targets.push(resolved);
|
|
1771
|
+
else out.opaque = true;
|
|
1772
|
+
}
|
|
1773
|
+
function outgoingTargets(node, index, staticTargets) {
|
|
1774
|
+
const out = {
|
|
1775
|
+
targets: [],
|
|
1776
|
+
opaque: false
|
|
1777
|
+
};
|
|
1778
|
+
const { config } = node;
|
|
1779
|
+
if (staticTargets) {
|
|
1780
|
+
for (const target of staticTargets[node.path] ?? []) resolveTargetString(target, node, index, out);
|
|
1781
|
+
return out;
|
|
1782
|
+
}
|
|
1783
|
+
for (const value of Object.values(config.on ?? {})) collectTransitionTargets(value, node, index, out);
|
|
1784
|
+
collectTransitionTargets(config.always, node, index, out);
|
|
1785
|
+
collectTransitionTargets(config.choice, node, index, out);
|
|
1786
|
+
for (const value of Object.values(config.after ?? {})) collectTransitionTargets(value, node, index, out);
|
|
1787
|
+
collectTransitionTargets(config.onDone, node, index, out);
|
|
1788
|
+
for (const invoke of node.invokes) {
|
|
1789
|
+
collectTransitionTargets(invoke.onDone, node, index, out);
|
|
1790
|
+
collectTransitionTargets(invoke.onError, node, index, out);
|
|
1791
|
+
}
|
|
1792
|
+
return out;
|
|
1793
|
+
}
|
|
1794
|
+
function computeReachable(rootConfig, index, staticTargets) {
|
|
1795
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
1796
|
+
const queue = [];
|
|
1797
|
+
const markAncestors = (path) => {
|
|
1798
|
+
let parent = index.get(path)?.parentPath ?? "";
|
|
1799
|
+
while (parent) {
|
|
1800
|
+
reachable.add(parent);
|
|
1801
|
+
parent = index.get(parent)?.parentPath ?? "";
|
|
1802
|
+
}
|
|
1803
|
+
};
|
|
1804
|
+
const enter = (path) => {
|
|
1805
|
+
if (reachable.has(path)) return;
|
|
1806
|
+
const node = index.get(path);
|
|
1807
|
+
if (!node) return;
|
|
1808
|
+
reachable.add(path);
|
|
1809
|
+
queue.push(path);
|
|
1810
|
+
markAncestors(path);
|
|
1811
|
+
if (node.isParallel) for (const child of childrenOf(index, path)) enter(child.path);
|
|
1812
|
+
else if (node.isCompound && node.config.initial) enter(`${path}.${node.config.initial}`);
|
|
1813
|
+
};
|
|
1814
|
+
if (rootConfig.type === "parallel") for (const child of childrenOf(index, "")) enter(child.path);
|
|
1815
|
+
else if (rootConfig.initial) enter(rootConfig.initial);
|
|
1816
|
+
while (queue.length > 0) {
|
|
1817
|
+
const node = index.get(queue.shift());
|
|
1818
|
+
if (!node) continue;
|
|
1819
|
+
const { targets, opaque } = outgoingTargets(node, index, staticTargets);
|
|
1820
|
+
for (const target of targets) enter(target);
|
|
1821
|
+
if (opaque) for (const sibling of childrenOf(index, node.parentPath)) enter(sibling.path);
|
|
1822
|
+
}
|
|
1823
|
+
return reachable;
|
|
1824
|
+
}
|
|
1825
|
+
function ancestorChain(node, index) {
|
|
1826
|
+
const chain = [];
|
|
1827
|
+
let parent = node.parentPath;
|
|
1828
|
+
while (parent) {
|
|
1829
|
+
const parentNode = index.get(parent);
|
|
1830
|
+
if (!parentNode) break;
|
|
1831
|
+
chain.push(parentNode);
|
|
1832
|
+
parent = parentNode.parentPath;
|
|
1833
|
+
}
|
|
1834
|
+
return chain;
|
|
1835
|
+
}
|
|
1836
|
+
function hasNonEmptyOn(config) {
|
|
1837
|
+
return !!config.on && Object.keys(config.on).length > 0;
|
|
1838
|
+
}
|
|
1839
|
+
function decisionKindOf(src, actors) {
|
|
1840
|
+
if (typeof src === "string") {
|
|
1841
|
+
if (src === DECIDE_SRC) return "decision";
|
|
1842
|
+
if (src === PLAN_SRC) return "plan";
|
|
1843
|
+
const logic = actors[src];
|
|
1844
|
+
if (require_decision.isDecisionLogic(logic)) return "decision";
|
|
1845
|
+
if (require_decision.isPlanLogic(logic)) return "plan";
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
if (require_decision.isDecisionLogic(src)) return "decision";
|
|
1849
|
+
if (require_decision.isPlanLogic(src)) return "plan";
|
|
1850
|
+
}
|
|
1851
|
+
function isAgentLogicNeedingBinding(src) {
|
|
1852
|
+
return (require_decision.isTextLogic(src) || require_decision.isDecisionLogic(src) || require_decision.isPlanLogic(src)) && !require_decision.executorBoundLogics.has(src);
|
|
1853
|
+
}
|
|
1854
|
+
function schemaExposesJson(schema) {
|
|
1855
|
+
if (!schema) return false;
|
|
1856
|
+
try {
|
|
1857
|
+
return require_decision.getJsonSchemaSync(schema) !== void 0;
|
|
1858
|
+
} catch {
|
|
1859
|
+
return false;
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
function isDeclaredOutputSchema(schema) {
|
|
1863
|
+
if (!schema) return false;
|
|
1864
|
+
let json;
|
|
1865
|
+
try {
|
|
1866
|
+
json = require_decision.getJsonSchemaSync(schema);
|
|
1867
|
+
} catch {
|
|
1868
|
+
return false;
|
|
1869
|
+
}
|
|
1870
|
+
if (!json) return false;
|
|
1871
|
+
const properties = json.properties;
|
|
1872
|
+
const required = json.required;
|
|
1873
|
+
return json.type === "object" && !!properties && Object.keys(properties).length > 0 || Array.isArray(required) && required.length > 0;
|
|
1874
|
+
}
|
|
1875
|
+
function checkUnreachableStates(ctx) {
|
|
1876
|
+
const out = [];
|
|
1877
|
+
for (const node of ctx.index.values()) if (!ctx.reachable.has(node.path)) out.push({
|
|
1878
|
+
code: "unreachable-state",
|
|
1879
|
+
severity: "error",
|
|
1880
|
+
path: node.path,
|
|
1881
|
+
message: `State '${node.path}' is unreachable: no transition, always, onDone, or onError target (from any reachable state) leads to it. Remove it or add a transition.`
|
|
1882
|
+
});
|
|
1883
|
+
return out;
|
|
1884
|
+
}
|
|
1885
|
+
function checkDecideWithoutEvents(ctx) {
|
|
1886
|
+
const out = [];
|
|
1887
|
+
for (const node of ctx.index.values()) for (const invoke of node.invokes) {
|
|
1888
|
+
const kind = decisionKindOf(invoke.src, ctx.actors);
|
|
1889
|
+
if (!kind) continue;
|
|
1890
|
+
const selfHandles = hasNonEmptyOn(node.config);
|
|
1891
|
+
const ancestorHandles = ancestorChain(node, ctx.index).some((ancestor) => hasNonEmptyOn(ancestor.config));
|
|
1892
|
+
const invokeObserves = invoke.onDone !== void 0;
|
|
1893
|
+
if (selfHandles || ancestorHandles || invokeObserves) continue;
|
|
1894
|
+
const srcName = typeof invoke.src === "string" ? invoke.src : "(inline logic)";
|
|
1895
|
+
out.push({
|
|
1896
|
+
code: "decide-without-events",
|
|
1897
|
+
severity: "error",
|
|
1898
|
+
path: node.path,
|
|
1899
|
+
message: `State '${node.path}' invokes ${kind} source '${srcName}', but neither it nor any ancestor handles any event (no 'on:'), so the ${kind}'s chosen event can never be delivered. Add an 'on:' handler for the candidate events.`
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
return out;
|
|
1903
|
+
}
|
|
1904
|
+
function checkUnserializableContext(ctx) {
|
|
1905
|
+
const contextSchema = ctx.schemas?.context;
|
|
1906
|
+
if (!contextSchema) return [];
|
|
1907
|
+
if (schemaExposesJson(contextSchema)) return [];
|
|
1908
|
+
return [{
|
|
1909
|
+
code: "unserializable-context",
|
|
1910
|
+
severity: "warning",
|
|
1911
|
+
path: "context",
|
|
1912
|
+
message: "The context schema does not expose a JSON schema (e.g. a `z.custom` without a `jsonSchema` extension, such as a messages array), so its fields cannot be statically checked for JSON persist/resume round-tripping. This is expected for message transcripts; verify any other custom-typed context is JSON-serializable."
|
|
1913
|
+
}];
|
|
1914
|
+
}
|
|
1915
|
+
function checkDirectObjectSrc(ctx) {
|
|
1916
|
+
const out = [];
|
|
1917
|
+
for (const node of ctx.index.values()) for (const invoke of node.invokes) {
|
|
1918
|
+
const src = invoke.src;
|
|
1919
|
+
if (typeof src === "string" || !src || typeof src !== "object") continue;
|
|
1920
|
+
if (!isAgentLogicNeedingBinding(src)) continue;
|
|
1921
|
+
out.push({
|
|
1922
|
+
code: "direct-object-src",
|
|
1923
|
+
severity: "warning",
|
|
1924
|
+
path: node.path,
|
|
1925
|
+
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.`
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
return out;
|
|
1929
|
+
}
|
|
1930
|
+
function checkFinalWithoutOutput(ctx) {
|
|
1931
|
+
if (!isDeclaredOutputSchema(ctx.schemas?.output)) return [];
|
|
1932
|
+
if (ctx.config.output !== void 0) return [];
|
|
1933
|
+
const out = [];
|
|
1934
|
+
for (const node of ctx.index.values()) {
|
|
1935
|
+
if (node.parentPath !== "" || !node.isFinal) continue;
|
|
1936
|
+
if (node.config.output === void 0) out.push({
|
|
1937
|
+
code: "final-without-output",
|
|
1938
|
+
severity: "error",
|
|
1939
|
+
path: node.path,
|
|
1940
|
+
message: `The machine declares an output schema, but top-level final state '${node.path}' has no 'output'. Its snapshot output will be undefined and fail the schema. Add an 'output' to this final state (or a root 'output').`
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
return out;
|
|
1944
|
+
}
|
|
1945
|
+
function outputFnReadsEvent(fn) {
|
|
1946
|
+
return /\bevent\s*(?:\.|\?\.|\[)/.test(fn.toString());
|
|
1947
|
+
}
|
|
1948
|
+
function checkFinalOutputReadsEvent(ctx) {
|
|
1949
|
+
const out = [];
|
|
1950
|
+
for (const node of ctx.index.values()) {
|
|
1951
|
+
if (node.parentPath !== "" || !node.isFinal) continue;
|
|
1952
|
+
const output = node.config.output;
|
|
1953
|
+
if (typeof output !== "function") continue;
|
|
1954
|
+
if (!outputFnReadsEvent(output)) continue;
|
|
1955
|
+
out.push({
|
|
1956
|
+
code: "final-output-reads-event",
|
|
1957
|
+
severity: "warning",
|
|
1958
|
+
path: node.path,
|
|
1959
|
+
message: `Final state '${node.path}' has an 'output' function that reads 'event'. Final-state 'output' functions are evaluated more than once with different events (the entering event, then the machine-done computation) under current xstate behavior, so 'event' is unreliable here. Read 'context' only; capture what you need from the entering event into context in the transition that targets this state. (This guard can relax if xstate guarantees a stable entering event across evaluations.)`
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
return out;
|
|
1963
|
+
}
|
|
1964
|
+
function isBuiltinOrWildcardEvent(eventType) {
|
|
1965
|
+
return eventType === "*" || eventType.includes("*") || eventType.startsWith("xstate.") || eventType.startsWith("done.") || eventType.startsWith("error.");
|
|
1966
|
+
}
|
|
1967
|
+
function checkUndeclaredEvents(ctx) {
|
|
1968
|
+
const declared = ctx.schemas?.events;
|
|
1969
|
+
const declaredTypes = new Set(Object.keys(declared ?? {}));
|
|
1970
|
+
if ([...declaredTypes].every((type) => type.startsWith("@agent."))) return [];
|
|
1971
|
+
const out = [];
|
|
1972
|
+
for (const node of ctx.index.values()) for (const eventType of Object.keys(node.config.on ?? {})) {
|
|
1973
|
+
if (declaredTypes.has(eventType) || isBuiltinOrWildcardEvent(eventType)) continue;
|
|
1974
|
+
out.push({
|
|
1975
|
+
code: "undeclared-event",
|
|
1976
|
+
severity: "warning",
|
|
1977
|
+
path: node.path,
|
|
1978
|
+
message: `State '${node.path}' handles event '${eventType}' in 'on:', but it is not declared in schemas.events and is not a builtin/wildcard pattern. If intentional its payload stays unvalidated; otherwise this is likely a typo.`
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
return out;
|
|
1982
|
+
}
|
|
1983
|
+
function checkMissingFinal(ctx) {
|
|
1984
|
+
for (const node of ctx.index.values()) if (node.isFinal && ctx.reachable.has(node.path)) return [];
|
|
1985
|
+
return [{
|
|
1986
|
+
code: "missing-final",
|
|
1987
|
+
severity: "warning",
|
|
1988
|
+
path: "(root)",
|
|
1989
|
+
message: "The machine has no reachable final state, so a run can never settle 'done' (only idle/looping). This is legal for agents that only idle, but verify it is intended."
|
|
1990
|
+
}];
|
|
1991
|
+
}
|
|
1992
|
+
const LINT_CHECKS = [
|
|
1993
|
+
checkUnreachableStates,
|
|
1994
|
+
checkDecideWithoutEvents,
|
|
1995
|
+
checkUnserializableContext,
|
|
1996
|
+
checkDirectObjectSrc,
|
|
1997
|
+
checkFinalWithoutOutput,
|
|
1998
|
+
checkFinalOutputReadsEvent,
|
|
1999
|
+
checkUndeclaredEvents,
|
|
2000
|
+
checkMissingFinal
|
|
2001
|
+
];
|
|
2002
|
+
/**
|
|
2003
|
+
* Runs static structural checks over a built agent machine and returns the
|
|
2004
|
+
* findings ({@link AgentLintDiagnostic}[], empty when clean). Works for
|
|
2005
|
+
* TS-authored (`setupAgent(...).createMachine(...)`) and
|
|
2006
|
+
* `setupAgent.fromConfig(...)`-compiled machines alike, reading `machine.config`
|
|
2007
|
+
* plus the schemas/actor sources the library already retains per machine.
|
|
2008
|
+
*
|
|
2009
|
+
* No model calls, no API keys — a coding agent that emits an agent machine can
|
|
2010
|
+
* call this to catch dead states, undeliverable decisions, un-rebindable
|
|
2011
|
+
* invoke srcs, and output-contract gaps before ever running it.
|
|
2012
|
+
*
|
|
2013
|
+
* @example
|
|
2014
|
+
* ```ts
|
|
2015
|
+
* const errors = lintAgentMachine(machine).filter((d) => d.severity === 'error');
|
|
2016
|
+
* if (errors.length) throw new Error(errors.map((e) => `${e.path}: ${e.message}`).join('\n'));
|
|
2017
|
+
* ```
|
|
2018
|
+
*/
|
|
2019
|
+
function lintAgentMachine(machine, options = {}) {
|
|
2020
|
+
const config = machine.config ?? {};
|
|
2021
|
+
const index = buildStateIndex(config);
|
|
2022
|
+
const reachable = computeReachable(config, index, require_decision.getMachineStaticTransitionTargets(machine));
|
|
2023
|
+
const registered = require_decision.getRegisteredAgentExecutionOptions(machine);
|
|
2024
|
+
const ctx = {
|
|
2025
|
+
machine,
|
|
2026
|
+
config,
|
|
2027
|
+
index,
|
|
2028
|
+
reachable,
|
|
2029
|
+
schemas: registered.schemas,
|
|
2030
|
+
actors: registered.actors ?? machine.sources?.actors ?? {}
|
|
2031
|
+
};
|
|
2032
|
+
const disabled = new Set(options.disable ?? []);
|
|
2033
|
+
return LINT_CHECKS.flatMap((check) => check(ctx)).filter((d) => !disabled.has(d.code));
|
|
2034
|
+
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Thrown by {@link assertAgentMachine} when lint finds failing diagnostics.
|
|
2037
|
+
* `diagnostics` holds the findings; the message lists them one per finding,
|
|
2038
|
+
* so a test runner's failure output reads like the CLI's lint report.
|
|
2039
|
+
*/
|
|
2040
|
+
var AgentLintError = class extends require_errors.AgentError {
|
|
2041
|
+
diagnostics;
|
|
2042
|
+
constructor(machineId, diagnostics) {
|
|
2043
|
+
const lines = diagnostics.map((d) => ` ${d.severity === "error" ? "error" : "warn "} ${d.code} ${d.path}\n ${d.message}`);
|
|
2044
|
+
super("lint-failed", `Agent machine '${machineId}' failed lint (${diagnostics.length} finding(s)):\n${lines.join("\n")}`);
|
|
2045
|
+
this.name = "AgentLintError";
|
|
2046
|
+
this.diagnostics = diagnostics;
|
|
2047
|
+
}
|
|
2048
|
+
};
|
|
2049
|
+
/**
|
|
2050
|
+
* Asserts a machine passes {@link lintAgentMachine}: returns silently when
|
|
2051
|
+
* clean, throws {@link AgentLintError} (with the findings on `.diagnostics`)
|
|
2052
|
+
* otherwise. Fails on error-severity findings; set `warnings: true` to fail on
|
|
2053
|
+
* warnings too. The one-liner for tests and generation loops:
|
|
2054
|
+
*
|
|
2055
|
+
* @example
|
|
2056
|
+
* ```ts
|
|
2057
|
+
* test('agent machine is structurally sound', () => {
|
|
2058
|
+
* assertAgentMachine(machine);
|
|
2059
|
+
* });
|
|
2060
|
+
* ```
|
|
2061
|
+
*/
|
|
2062
|
+
function assertAgentMachine(machine, options = {}) {
|
|
2063
|
+
const diagnostics = lintAgentMachine(machine, options);
|
|
2064
|
+
const failing = options.warnings ? diagnostics : diagnostics.filter((d) => d.severity === "error");
|
|
2065
|
+
if (failing.length > 0) throw new AgentLintError(machine.id ?? "(machine)", failing);
|
|
2066
|
+
}
|
|
2067
|
+
function pendingInvokes(step) {
|
|
2068
|
+
const out = [];
|
|
2069
|
+
for (const action of step.actions) {
|
|
2070
|
+
const metadata = require_setup_agent.getInvokeEffectMetadata(action);
|
|
2071
|
+
if (typeof metadata?.src === "string" && typeof metadata.id === "string") out.push({
|
|
2072
|
+
id: metadata.id,
|
|
2073
|
+
src: metadata.src
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
return out;
|
|
2077
|
+
}
|
|
2078
|
+
function takeFromQueue(channel, src) {
|
|
2079
|
+
const queue = channel?.[src];
|
|
2080
|
+
if (queue && queue.length > 0) return {
|
|
2081
|
+
found: true,
|
|
2082
|
+
value: queue.shift()
|
|
2083
|
+
};
|
|
2084
|
+
return { found: false };
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* Deterministically plays a machine through, resolving each request from a
|
|
2088
|
+
* {@link SimulationScript} instead of a model — no API keys, no model calls.
|
|
2089
|
+
* Runs on the pure step path ({@link initialAgentStep} etc.), so it exercises
|
|
2090
|
+
* the real transition logic. Returns the terminal `status`, final `snapshot`,
|
|
2091
|
+
* and a `trail` of every step taken.
|
|
2092
|
+
*
|
|
2093
|
+
* Throws a descriptive error when the script runs dry mid-request, naming the
|
|
2094
|
+
* pending request's kind, src, and id so the missing scripted response is
|
|
2095
|
+
* obvious.
|
|
2096
|
+
*
|
|
2097
|
+
* @example
|
|
2098
|
+
* ```ts
|
|
2099
|
+
* const { status, snapshot } = simulateAgent(machine, {
|
|
2100
|
+
* input: { topic: 'state machines' },
|
|
2101
|
+
* script: { decisions: { 'agent.decide': [{ type: 'END' }] } },
|
|
2102
|
+
* });
|
|
2103
|
+
* ```
|
|
2104
|
+
*/
|
|
2105
|
+
async function simulateAgent(machine, options) {
|
|
2106
|
+
const maxSteps = options.maxSteps ?? 100;
|
|
2107
|
+
const script = {
|
|
2108
|
+
text: { ...options.script.text },
|
|
2109
|
+
decisions: mapValues(options.script.decisions ?? {}, (arr) => [...arr]),
|
|
2110
|
+
invokes: mapValues(options.script.invokes ?? {}, (arr) => [...arr])
|
|
2111
|
+
};
|
|
2112
|
+
let step = require_setup_agent.initialAgentStep(machine, options.input);
|
|
2113
|
+
const trail = [];
|
|
2114
|
+
for (let i = 0; i < maxSteps; i++) {
|
|
2115
|
+
if (step.done) return {
|
|
2116
|
+
status: "done",
|
|
2117
|
+
snapshot: step.snapshot,
|
|
2118
|
+
trail
|
|
2119
|
+
};
|
|
2120
|
+
const request = step.requests[0];
|
|
2121
|
+
if (request) {
|
|
2122
|
+
if (request.kind === "decision") {
|
|
2123
|
+
const decisionSrc = new Map(pendingInvokes(step).map((invoke) => [invoke.id, invoke.src])).get(request.id) ?? request.id;
|
|
2124
|
+
const taken = takeFromQueue(script.decisions, decisionSrc);
|
|
2125
|
+
if (!taken.found) throw scriptDryError("decision", decisionSrc, request.id, request);
|
|
2126
|
+
step = require_setup_agent.transitionAgentStep(machine, step, taken.value);
|
|
2127
|
+
trail.push({
|
|
2128
|
+
state: step.snapshot.value,
|
|
2129
|
+
appliedEvent: taken.value
|
|
2130
|
+
});
|
|
2131
|
+
continue;
|
|
2132
|
+
}
|
|
2133
|
+
if (request.kind === "plan") {
|
|
2134
|
+
const taken = takeFromQueue(script.decisions, request.src);
|
|
2135
|
+
if (!taken.found) throw scriptDryError("decision", request.src, request.id, request);
|
|
2136
|
+
const event = taken.value;
|
|
2137
|
+
step = await require_setup_agent.resolveAgentRequests(machine, step, { decide: async () => ({ event }) });
|
|
2138
|
+
trail.push({
|
|
2139
|
+
state: step.snapshot.value,
|
|
2140
|
+
appliedEvent: taken.value
|
|
2141
|
+
});
|
|
2142
|
+
continue;
|
|
2143
|
+
}
|
|
2144
|
+
const taken = takeFromQueue(script.text, request.src);
|
|
2145
|
+
if (!taken.found) throw scriptDryError("text", request.src, request.id);
|
|
2146
|
+
step = require_setup_agent.resolveAgentStep(machine, step, request, taken.value);
|
|
2147
|
+
trail.push({
|
|
2148
|
+
state: step.snapshot.value,
|
|
2149
|
+
resolvedRequest: {
|
|
2150
|
+
kind: "text",
|
|
2151
|
+
src: request.src,
|
|
2152
|
+
id: request.id
|
|
2153
|
+
}
|
|
2154
|
+
});
|
|
2155
|
+
continue;
|
|
2156
|
+
}
|
|
2157
|
+
const [invoke] = pendingInvokes(step);
|
|
2158
|
+
if (invoke) {
|
|
2159
|
+
const taken = takeFromQueue(script.invokes, invoke.src);
|
|
2160
|
+
if (!taken.found) throw scriptDryError("userInput", invoke.src, invoke.id);
|
|
2161
|
+
step = require_setup_agent.resolveAgentStep(machine, step, invoke.id, taken.value);
|
|
2162
|
+
trail.push({
|
|
2163
|
+
state: step.snapshot.value,
|
|
2164
|
+
resolvedRequest: {
|
|
2165
|
+
kind: "userInput",
|
|
2166
|
+
src: invoke.src,
|
|
2167
|
+
id: invoke.id
|
|
2168
|
+
}
|
|
2169
|
+
});
|
|
2170
|
+
continue;
|
|
2171
|
+
}
|
|
2172
|
+
return {
|
|
2173
|
+
status: "idle",
|
|
2174
|
+
snapshot: step.snapshot,
|
|
2175
|
+
trail
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
return {
|
|
2179
|
+
status: "exhausted",
|
|
2180
|
+
snapshot: step.snapshot,
|
|
2181
|
+
trail
|
|
2182
|
+
};
|
|
2183
|
+
}
|
|
2184
|
+
function mapValues(obj, fn) {
|
|
2185
|
+
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, fn(value)]));
|
|
2186
|
+
}
|
|
2187
|
+
function scriptDryError(kind, src, id, request) {
|
|
2188
|
+
const events = request?.kind === "decision" ? ` Candidate events: ${request.events.map((e) => e.type).join(", ") || "(none)"}.` : "";
|
|
2189
|
+
return /* @__PURE__ */ new Error(`simulateAgent: script ran dry on a pending ${kind} request for src '${src}' (id '${id}'). Add a '${kind}' entry for '${src}' to the script.${events}`);
|
|
2190
|
+
}
|
|
2191
|
+
async function explore(machine, options, stopWhen) {
|
|
2192
|
+
const maxDepth = options.maxDepth ?? 8;
|
|
2193
|
+
const maxPaths = options.maxPaths ?? 200;
|
|
2194
|
+
const textOutputs = options.textOutputs ?? {};
|
|
2195
|
+
const reachedStates = /* @__PURE__ */ new Set();
|
|
2196
|
+
const reachedValues = [];
|
|
2197
|
+
const terminals = [];
|
|
2198
|
+
const unexplored = [];
|
|
2199
|
+
let prunedByGuard = 0;
|
|
2200
|
+
let pathsExplored = 0;
|
|
2201
|
+
let hitPathCap = false;
|
|
2202
|
+
let witness;
|
|
2203
|
+
const recordState = (snapshot) => {
|
|
2204
|
+
const key = JSON.stringify(snapshot.value);
|
|
2205
|
+
if (!reachedStates.has(key)) {
|
|
2206
|
+
reachedStates.add(key);
|
|
2207
|
+
reachedValues.push(snapshot.value);
|
|
2208
|
+
}
|
|
2209
|
+
};
|
|
2210
|
+
const initial = require_setup_agent.initialAgentStep(machine, options.input);
|
|
2211
|
+
recordState(initial.snapshot);
|
|
2212
|
+
if (stopWhen?.(initial.snapshot)) witness = [];
|
|
2213
|
+
const advance = (step) => {
|
|
2214
|
+
let current = step;
|
|
2215
|
+
for (let i = 0; i < 1e3; i++) {
|
|
2216
|
+
if (current.done) return { step: current };
|
|
2217
|
+
const request = current.requests[0];
|
|
2218
|
+
if (request && request.kind === "text") {
|
|
2219
|
+
if (!(request.src in textOutputs)) return {
|
|
2220
|
+
step: current,
|
|
2221
|
+
blockedSrc: request.src
|
|
2222
|
+
};
|
|
2223
|
+
current = require_setup_agent.resolveAgentStep(machine, current, request, textOutputs[request.src]);
|
|
2224
|
+
recordState(current.snapshot);
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
if (request && (request.kind === "decision" || request.kind === "plan")) return { step: current };
|
|
2228
|
+
const [invoke] = pendingInvokes(current);
|
|
2229
|
+
if (invoke) {
|
|
2230
|
+
if (!(invoke.src in textOutputs)) return {
|
|
2231
|
+
step: current,
|
|
2232
|
+
blockedSrc: invoke.src
|
|
2233
|
+
};
|
|
2234
|
+
current = require_setup_agent.resolveAgentStep(machine, current, invoke.id, textOutputs[invoke.src]);
|
|
2235
|
+
recordState(current.snapshot);
|
|
2236
|
+
continue;
|
|
2237
|
+
}
|
|
2238
|
+
return { step: current };
|
|
2239
|
+
}
|
|
2240
|
+
return { step: current };
|
|
2241
|
+
};
|
|
2242
|
+
const visit = async (step, path, depth) => {
|
|
2243
|
+
if (witness !== void 0) return;
|
|
2244
|
+
if (pathsExplored >= maxPaths) {
|
|
2245
|
+
hitPathCap = true;
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
const { step: settled, blockedSrc } = advance(step);
|
|
2249
|
+
if (stopWhen?.(settled.snapshot)) {
|
|
2250
|
+
witness = path;
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
if (blockedSrc) {
|
|
2254
|
+
pathsExplored++;
|
|
2255
|
+
terminals.push({
|
|
2256
|
+
status: "needs-output",
|
|
2257
|
+
path,
|
|
2258
|
+
state: settled.snapshot.value,
|
|
2259
|
+
missingSrc: blockedSrc
|
|
2260
|
+
});
|
|
2261
|
+
unexplored.push(`needs-output: no canned output for src '${blockedSrc}' at path [${path.map((e) => e.type).join(", ")}]`);
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
if (settled.done) {
|
|
2265
|
+
pathsExplored++;
|
|
2266
|
+
terminals.push({
|
|
2267
|
+
status: "done",
|
|
2268
|
+
path,
|
|
2269
|
+
state: settled.snapshot.value
|
|
2270
|
+
});
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
const request = settled.requests[0];
|
|
2274
|
+
const isPlan = request?.kind === "plan";
|
|
2275
|
+
const branchEvents = request?.kind === "decision" || request?.kind === "plan" ? request.events.map((descriptor) => ({ type: descriptor.type })) : require_decision.getAcceptedEvents(settled.snapshot).map((descriptor) => ({ type: descriptor.type }));
|
|
2276
|
+
if (branchEvents.length === 0) {
|
|
2277
|
+
pathsExplored++;
|
|
2278
|
+
terminals.push({
|
|
2279
|
+
status: "idle",
|
|
2280
|
+
path,
|
|
2281
|
+
state: settled.snapshot.value
|
|
2282
|
+
});
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
if (depth >= maxDepth) {
|
|
2286
|
+
pathsExplored++;
|
|
2287
|
+
terminals.push({
|
|
2288
|
+
status: "max-depth",
|
|
2289
|
+
path,
|
|
2290
|
+
state: settled.snapshot.value
|
|
2291
|
+
});
|
|
2292
|
+
unexplored.push(`max-depth: stopped at path [${path.map((e) => e.type).join(", ")}]`);
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
for (const event of branchEvents) {
|
|
2296
|
+
if (witness !== void 0 || pathsExplored >= maxPaths) {
|
|
2297
|
+
if (pathsExplored >= maxPaths) hitPathCap = true;
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
if (!(isPlan && event.type === "agent.plan.done") && !settled.snapshot.can(event)) {
|
|
2301
|
+
prunedByGuard++;
|
|
2302
|
+
continue;
|
|
2303
|
+
}
|
|
2304
|
+
const next = isPlan ? await require_setup_agent.resolveAgentRequests(machine, settled, { decide: async () => ({ event }) }) : require_setup_agent.transitionAgentStep(machine, settled, event);
|
|
2305
|
+
recordState(next.snapshot);
|
|
2306
|
+
await visit(next, [...path, event], depth + 1);
|
|
2307
|
+
}
|
|
2308
|
+
};
|
|
2309
|
+
if (witness === void 0) await visit(initial, [], 0);
|
|
2310
|
+
return {
|
|
2311
|
+
report: {
|
|
2312
|
+
reachedStates: reachedValues,
|
|
2313
|
+
terminals,
|
|
2314
|
+
prunedByGuard,
|
|
2315
|
+
unexplored,
|
|
2316
|
+
pathsExplored,
|
|
2317
|
+
hitPathCap
|
|
2318
|
+
},
|
|
2319
|
+
witness
|
|
2320
|
+
};
|
|
2321
|
+
}
|
|
2322
|
+
/**
|
|
2323
|
+
* Enumerates a machine's decision and external-event branches to a bounded
|
|
2324
|
+
* depth, model-free, and reports which states are reached and how each path
|
|
2325
|
+
* terminates. At each decision request it forks one branch per candidate event
|
|
2326
|
+
* (guard-rejected candidates are counted in `prunedByGuard`, not explored); at
|
|
2327
|
+
* an idle wait it forks per externally-accepted event. A `agent.plan` request
|
|
2328
|
+
* forks the same way — one branch per candidate, including the reserved
|
|
2329
|
+
* `agent.plan.done` move — advancing each branch through the real plan protocol
|
|
2330
|
+
* (`resolveAgentRequests`), so a plan can consume several depth units. Text/`userInput` invokes
|
|
2331
|
+
* are resolved from `textOutputs` (a by-src canned-output map) — a missing src
|
|
2332
|
+
* halts that branch with a `needs-output` terminal rather than throwing.
|
|
2333
|
+
*
|
|
2334
|
+
* Combinatorics are bounded by `maxDepth` (default 8) and `maxPaths` (default
|
|
2335
|
+
* 200, reported via `hitPathCap`).
|
|
2336
|
+
*
|
|
2337
|
+
* @example
|
|
2338
|
+
* ```ts
|
|
2339
|
+
* const report = await explorePaths(refundMachine, { input: { request: 'x', amount: 5000 } });
|
|
2340
|
+
* // report.terminals → both 'refunded' and 'denied'; report.prunedByGuard → 1
|
|
2341
|
+
* ```
|
|
2342
|
+
*/
|
|
2343
|
+
async function explorePaths(machine, options = {}) {
|
|
2344
|
+
return (await explore(machine, options)).report;
|
|
2345
|
+
}
|
|
2346
|
+
/**
|
|
2347
|
+
* Answers "can the machine reach `statePath`?" by exploring its branches (a
|
|
2348
|
+
* thin wrapper over {@link explorePaths}). Returns `{ canReach: true, witness }`
|
|
2349
|
+
* with the event sequence that reaches it, or `{ canReach: false }`.
|
|
2350
|
+
*
|
|
2351
|
+
* @example
|
|
2352
|
+
* ```ts
|
|
2353
|
+
* const { canReach, witness } = await canReach(refundMachine, 'denied', { input: { request: 'x', amount: 5000 } });
|
|
2354
|
+
* // canReach → true; witness → [{ type: 'NEEDS_REVIEW' }, { type: 'DENY' }]
|
|
2355
|
+
* ```
|
|
2356
|
+
*/
|
|
2357
|
+
async function canReach(machine, statePath, options = {}) {
|
|
2358
|
+
const { witness } = await explore(machine, options, (snapshot) => {
|
|
2359
|
+
try {
|
|
2360
|
+
return snapshot.matches(statePath);
|
|
2361
|
+
} catch {
|
|
2362
|
+
return false;
|
|
2363
|
+
}
|
|
2364
|
+
});
|
|
2365
|
+
return witness !== void 0 ? {
|
|
2366
|
+
canReach: true,
|
|
2367
|
+
witness
|
|
2368
|
+
} : { canReach: false };
|
|
2369
|
+
}
|
|
2370
|
+
//#endregion
|
|
2371
|
+
//#region src/trajectory.ts
|
|
2372
|
+
function isRecord$2(value) {
|
|
2373
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2374
|
+
}
|
|
2375
|
+
/**
|
|
2376
|
+
* Classifies one item. Event-shaped things (log entries, events) become events;
|
|
2377
|
+
* everything else is a state value. A snapshot is unwrapped to its `value`.
|
|
2378
|
+
* @internal
|
|
2379
|
+
*/
|
|
2380
|
+
function normalize(item) {
|
|
2381
|
+
if (isRecord$2(item)) {
|
|
2382
|
+
const nested = item["event"];
|
|
2383
|
+
if (isRecord$2(nested) && typeof nested["type"] === "string") return {
|
|
2384
|
+
kind: "event",
|
|
2385
|
+
event: nested
|
|
2386
|
+
};
|
|
2387
|
+
if (typeof item["type"] === "string") return {
|
|
2388
|
+
kind: "event",
|
|
2389
|
+
event: item
|
|
2390
|
+
};
|
|
2391
|
+
if ("value" in item && "status" in item) return {
|
|
2392
|
+
kind: "state",
|
|
2393
|
+
value: item["value"]
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
return {
|
|
2397
|
+
kind: "state",
|
|
2398
|
+
value: item
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
/** Every leaf of a state value as a dot path (`{ review: 'editing' }` → `review.editing`). @internal */
|
|
2402
|
+
function statePaths(value) {
|
|
2403
|
+
if (typeof value === "string") return [value];
|
|
2404
|
+
if (!isRecord$2(value)) return [];
|
|
2405
|
+
return Object.entries(value).flatMap(([key, child]) => statePaths(child).map((path) => `${key}.${path}`));
|
|
2406
|
+
}
|
|
2407
|
+
/**
|
|
2408
|
+
* A string expectation matches a state value when it names one of its leaves or
|
|
2409
|
+
* an ancestor of one, so `'review'` matches `{ review: 'editing' }`. @internal
|
|
2410
|
+
*/
|
|
2411
|
+
function stateMatchesPath(path, value) {
|
|
2412
|
+
return statePaths(value).some((leaf) => leaf === path || leaf.startsWith(`${path}.`));
|
|
2413
|
+
}
|
|
2414
|
+
function deepEqual(a, b) {
|
|
2415
|
+
if (Object.is(a, b)) return true;
|
|
2416
|
+
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]));
|
|
2417
|
+
if (!isRecord$2(a) || !isRecord$2(b)) return false;
|
|
2418
|
+
const keys = Object.keys(a);
|
|
2419
|
+
return keys.length === Object.keys(b).length && keys.every((key) => deepEqual(a[key], b[key]));
|
|
2420
|
+
}
|
|
2421
|
+
/**
|
|
2422
|
+
* Does one actual item satisfy one expected item?
|
|
2423
|
+
*
|
|
2424
|
+
* - A string expectation matches an event's `type`, or a state value it names.
|
|
2425
|
+
* - An event expectation matches an event with the same `type` whose other
|
|
2426
|
+
* declared keys deep-equal, so payload details are opt-in.
|
|
2427
|
+
* - A state-value expectation matches structurally.
|
|
2428
|
+
* @internal
|
|
2429
|
+
*/
|
|
2430
|
+
function matchesItem(expected, actual) {
|
|
2431
|
+
const actualItem = normalize(actual);
|
|
2432
|
+
if (typeof expected === "string") return actualItem.kind === "event" ? actualItem.event.type === expected : stateMatchesPath(expected, actualItem.value);
|
|
2433
|
+
const expectedItem = normalize(expected);
|
|
2434
|
+
if (expectedItem.kind !== actualItem.kind) return false;
|
|
2435
|
+
if (expectedItem.kind === "event" && actualItem.kind === "event") return Object.keys(expectedItem.event).every((key) => deepEqual(expectedItem.event[key], actualItem.event[key]));
|
|
2436
|
+
return deepEqual(expectedItem.value, actualItem.value);
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
2439
|
+
* Matches a run's trajectory against an expected one.
|
|
2440
|
+
*
|
|
2441
|
+
* Both trajectories may be state values (collected from `onTransition`) or
|
|
2442
|
+
* events (`result.events`, whose {@link AgentLogEntry} envelopes are unwrapped);
|
|
2443
|
+
* items are compared by shape, so `['prompting', 'drafting']` scores a state
|
|
2444
|
+
* path and `['PROMPT_SUBMITTED', 'SEND']` scores an event log with the same
|
|
2445
|
+
* call.
|
|
2446
|
+
*
|
|
2447
|
+
* Semantics are an ordered subsequence: every expected item must appear in
|
|
2448
|
+
* `actual`, in order, with gaps allowed. Pass `{ exact: true }` to require
|
|
2449
|
+
* equality instead.
|
|
2450
|
+
*
|
|
2451
|
+
* @example Trajectory assertions in a test
|
|
2452
|
+
* ```ts
|
|
2453
|
+
* const statePath: unknown[] = [];
|
|
2454
|
+
* const result = await runAgent(machine, {
|
|
2455
|
+
* input,
|
|
2456
|
+
* executors,
|
|
2457
|
+
* onTransition: (snapshot) => statePath.push(snapshot.value),
|
|
2458
|
+
* });
|
|
2459
|
+
*
|
|
2460
|
+
* const path = matchesTrajectory(statePath, ['prompting', 'drafting', 'sent']);
|
|
2461
|
+
* expect(path.matched, JSON.stringify(path.firstMiss)).toBe(true);
|
|
2462
|
+
* expect(matchesTrajectory(result.events, ['PROMPT_SUBMITTED', 'SEND']).matched).toBe(true);
|
|
2463
|
+
* ```
|
|
2464
|
+
*
|
|
2465
|
+
* @example Partial credit in an eval scorer
|
|
2466
|
+
* ```ts
|
|
2467
|
+
* const match = matchesTrajectory(output.statePath, expected.statePath);
|
|
2468
|
+
* return { name: 'state_path', score: match.score, metadata: { ...match.firstMiss } };
|
|
2469
|
+
* ```
|
|
2470
|
+
*/
|
|
2471
|
+
function matchesTrajectory(actual, expected, options = {}) {
|
|
2472
|
+
const expectedCount = expected.length;
|
|
2473
|
+
if (options.exact) {
|
|
2474
|
+
let matchedCount = 0;
|
|
2475
|
+
let firstMiss;
|
|
2476
|
+
for (let index = 0; index < expectedCount; index++) {
|
|
2477
|
+
if (index < actual.length && matchesItem(expected[index], actual[index])) {
|
|
2478
|
+
matchedCount++;
|
|
2479
|
+
continue;
|
|
2480
|
+
}
|
|
2481
|
+
firstMiss = {
|
|
2482
|
+
index,
|
|
2483
|
+
expected: expected[index],
|
|
2484
|
+
searchedFrom: index
|
|
2485
|
+
};
|
|
2486
|
+
break;
|
|
2487
|
+
}
|
|
2488
|
+
const matched = !firstMiss && actual.length === expectedCount;
|
|
2489
|
+
const denominator = Math.max(expectedCount, actual.length);
|
|
2490
|
+
return {
|
|
2491
|
+
matched,
|
|
2492
|
+
matchedCount,
|
|
2493
|
+
expectedCount,
|
|
2494
|
+
score: matched ? 1 : denominator === 0 ? 1 : matchedCount / denominator,
|
|
2495
|
+
...firstMiss ? { firstMiss } : {}
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
let cursor = 0;
|
|
2499
|
+
let matchedCount = 0;
|
|
2500
|
+
for (let index = 0; index < expectedCount; index++) {
|
|
2501
|
+
const want = expected[index];
|
|
2502
|
+
let found = -1;
|
|
2503
|
+
for (let at = cursor; at < actual.length; at++) if (matchesItem(want, actual[at])) {
|
|
2504
|
+
found = at;
|
|
2505
|
+
break;
|
|
2506
|
+
}
|
|
2507
|
+
if (found === -1) return {
|
|
2508
|
+
matched: false,
|
|
2509
|
+
matchedCount,
|
|
2510
|
+
expectedCount,
|
|
2511
|
+
score: matchedCount / expectedCount,
|
|
2512
|
+
firstMiss: {
|
|
2513
|
+
index,
|
|
2514
|
+
expected: want,
|
|
2515
|
+
searchedFrom: cursor
|
|
2516
|
+
}
|
|
2517
|
+
};
|
|
2518
|
+
matchedCount++;
|
|
2519
|
+
cursor = found + 1;
|
|
2520
|
+
}
|
|
2521
|
+
return {
|
|
2522
|
+
matched: true,
|
|
2523
|
+
matchedCount,
|
|
2524
|
+
expectedCount,
|
|
2525
|
+
score: 1
|
|
2526
|
+
};
|
|
2527
|
+
}
|
|
2528
|
+
//#endregion
|
|
2529
|
+
//#region src/scripted-executors.ts
|
|
2530
|
+
/**
|
|
2531
|
+
* Scripted executors — a keyless, deterministic stand-in for a model host.
|
|
2532
|
+
*
|
|
2533
|
+
* `createScriptedExecutors` builds a full `{ generateText, streamText, decide }`
|
|
2534
|
+
* set that plays back canned answers from FIFO queues, so `runAgent` (or
|
|
2535
|
+
* `provideExecutors`, or a bare `TextLogic.execute`) runs with no API key and no
|
|
2536
|
+
* network. It is the fastest way to see a machine run, and the least ceremonial
|
|
2537
|
+
* way to test one: same machine, same executor contract, scripted answers.
|
|
2538
|
+
*/
|
|
2539
|
+
/** Thrown when a scripted queue runs dry on a pending request. */
|
|
2540
|
+
var ScriptedExecutorsError = class extends require_errors.AgentError {
|
|
2541
|
+
constructor(message) {
|
|
2542
|
+
super("scripted-executors-exhausted", message);
|
|
2543
|
+
this.name = "ScriptedExecutorsError";
|
|
2544
|
+
}
|
|
2545
|
+
};
|
|
2546
|
+
function isRecord$1(value) {
|
|
2547
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2548
|
+
}
|
|
2549
|
+
/** The only own keys an executor-result envelope carries. @internal */
|
|
2550
|
+
const TEXT_ENVELOPE_KEYS = new Set([
|
|
2551
|
+
"output",
|
|
2552
|
+
"usage",
|
|
2553
|
+
"raw"
|
|
2554
|
+
]);
|
|
2555
|
+
/**
|
|
2556
|
+
* True when a scripted entry is the executor envelope rather than the output
|
|
2557
|
+
* value: it owns an `output` key and owns NOTHING outside the envelope's own
|
|
2558
|
+
* vocabulary. Bare `'output' in value` would swallow an output object's
|
|
2559
|
+
* siblings (`{ output: 'draft', confidence: 0.9 }` would lose `confidence`)
|
|
2560
|
+
* and would also match an inherited `output`. @internal
|
|
2561
|
+
*/
|
|
2562
|
+
function isTextEnvelope(value) {
|
|
2563
|
+
return Object.hasOwn(value, "output") && Object.keys(value).every((key) => TEXT_ENVELOPE_KEYS.has(key));
|
|
2564
|
+
}
|
|
2565
|
+
/**
|
|
2566
|
+
* Resolves ONE scripted text entry to an executor result: a function entry is
|
|
2567
|
+
* called with the request, and the value is taken as the raw envelope only when
|
|
2568
|
+
* it is one (see {@link isTextEnvelope}). Shared with `runSeam`, whose routed
|
|
2569
|
+
* queues follow the same entry conventions. @internal
|
|
2570
|
+
*/
|
|
2571
|
+
async function resolveScriptedTextEntry(entry, request, info) {
|
|
2572
|
+
const value = typeof entry === "function" ? await entry(request, info) : entry;
|
|
2573
|
+
return isRecord$1(value) && isTextEnvelope(value) ? value : { output: value };
|
|
2574
|
+
}
|
|
2575
|
+
/**
|
|
2576
|
+
* Keyless executors that replay a script instead of calling a model. Every
|
|
2577
|
+
* slot is provided, so any machine binds; a request with no entry left throws a
|
|
2578
|
+
* descriptive error naming what was pending.
|
|
2579
|
+
*
|
|
2580
|
+
* Queues are consumed FIFO and are copied on creation, so the caller's arrays
|
|
2581
|
+
* are never mutated and each call builds a fresh, independent playthrough.
|
|
2582
|
+
* Entries may be plain values or functions of the request, which is how one
|
|
2583
|
+
* script serves a machine that loops or branches: route on `request.name` (the
|
|
2584
|
+
* `setupAgent({ requests })` key) or on the decision's candidate `events`.
|
|
2585
|
+
*
|
|
2586
|
+
* @example
|
|
2587
|
+
* ```ts
|
|
2588
|
+
* const result = await runAgent(moderationMachine, {
|
|
2589
|
+
* input: { comment: 'honestly this update is terrible', trust: 20 },
|
|
2590
|
+
* executors: createScriptedExecutors({
|
|
2591
|
+
* decisions: [{ type: 'FLAG', reason: 'Borderline tone.' }],
|
|
2592
|
+
* }),
|
|
2593
|
+
* });
|
|
2594
|
+
* ```
|
|
2595
|
+
*
|
|
2596
|
+
* @example Dynamic entries
|
|
2597
|
+
* ```ts
|
|
2598
|
+
* createScriptedExecutors({
|
|
2599
|
+
* text: [(request) => `Draft about ${request.prompt}`],
|
|
2600
|
+
* decisions: [(request) => ({ type: request.events[0]!.type })],
|
|
2601
|
+
* });
|
|
2602
|
+
* ```
|
|
2603
|
+
*/
|
|
2604
|
+
function createScriptedExecutors(script = {}) {
|
|
2605
|
+
const decisions = [...script.decisions ?? []];
|
|
2606
|
+
const text = [...script.text ?? []];
|
|
2607
|
+
const nextText = async (request, info) => {
|
|
2608
|
+
if (text.length === 0) throw new ScriptedExecutorsError(`createScriptedExecutors: script ran dry on a pending text request ${describeText(request)}. Add another entry to the script's \`text\` queue.`);
|
|
2609
|
+
return resolveScriptedTextEntry(text.shift(), request, info);
|
|
2610
|
+
};
|
|
2611
|
+
return {
|
|
2612
|
+
generateText: nextText,
|
|
2613
|
+
streamText: async (request, info) => {
|
|
2614
|
+
const result = await nextText(request, info);
|
|
2615
|
+
if (typeof result.output === "string") info?.onChunk?.(result.output);
|
|
2616
|
+
return result;
|
|
2617
|
+
},
|
|
2618
|
+
decide: async (request) => {
|
|
2619
|
+
if (decisions.length === 0) throw new ScriptedExecutorsError(`createScriptedExecutors: script ran dry on a pending decision request (id '${request.id}'). Add another entry to the script's \`decisions\` queue. Candidate events: ${request.events.map((event) => event.type).join(", ") || "(none)"}.`);
|
|
2620
|
+
const entry = decisions.shift();
|
|
2621
|
+
const value = typeof entry === "function" ? await entry(request) : entry;
|
|
2622
|
+
return typeof value.type === "string" ? { event: value } : value;
|
|
2623
|
+
}
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
2626
|
+
function describeText(request) {
|
|
2627
|
+
return request.name ? `'${request.name}'` : `(model '${request.model}')`;
|
|
2628
|
+
}
|
|
2629
|
+
//#endregion
|
|
2630
|
+
//#region src/seam.ts
|
|
2631
|
+
/** Thrown when the call plan runs dry on a request that is not the live seam. */
|
|
2632
|
+
var SeamScriptError = class extends require_errors.AgentError {
|
|
2633
|
+
constructor(message) {
|
|
2634
|
+
super("seam-script-exhausted", message);
|
|
2635
|
+
this.name = "SeamScriptError";
|
|
2636
|
+
}
|
|
2637
|
+
};
|
|
2638
|
+
function seamKeyOf(seam) {
|
|
2639
|
+
return seam.request ?? seam.model;
|
|
2640
|
+
}
|
|
2641
|
+
function describeRequest(request) {
|
|
2642
|
+
return request.name ? `'${request.name}' (model '${request.model}')` : `(model '${request.model}')`;
|
|
2643
|
+
}
|
|
2644
|
+
function isRecord(value) {
|
|
2645
|
+
return typeof value === "object" && value !== null;
|
|
2646
|
+
}
|
|
2647
|
+
/**
|
|
2648
|
+
* The seam's own answer, for scoring. Our `{ output }` envelope is read
|
|
2649
|
+
* directly; a raw AI SDK `generateText` result contributes its `text`. A raw
|
|
2650
|
+
* STREAM result is left alone — the machine consumes that stream, and reading
|
|
2651
|
+
* it here would steal the chunks — so `seamOutput` is `undefined` for a
|
|
2652
|
+
* streaming candidate that returns one. Score its trajectory instead.
|
|
2653
|
+
* @internal
|
|
2654
|
+
*/
|
|
2655
|
+
async function seamOutputOf(result, request) {
|
|
2656
|
+
if (!isRecord(result)) return;
|
|
2657
|
+
if ("output" in result) return await result["output"];
|
|
2658
|
+
if ("textStream" in result) return;
|
|
2659
|
+
return await require_decision.normalizeGeneratorResult(result, `seam '${request.name ?? request.model}'`, { request });
|
|
2660
|
+
}
|
|
2661
|
+
/**
|
|
2662
|
+
* Runs a machine end to end with one model call under test, and slices the run
|
|
2663
|
+
* at that call.
|
|
2664
|
+
*
|
|
2665
|
+
* The `before`/`after` slices are the point: `after.statePath` is the branch
|
|
2666
|
+
* the seam chose and `after.events` is the same question against the durable
|
|
2667
|
+
* log, both ready for `matchesTrajectory`. The state slice splits where the
|
|
2668
|
+
* state path stood when the seam answered; the event slice splits at the
|
|
2669
|
+
* seam's own effect completion (the first `xstate.done.*`/`xstate.error.*`
|
|
2670
|
+
* entry appended after the call was made).
|
|
2671
|
+
*
|
|
2672
|
+
* @example Keyless: the seam is scripted too, so the whole thing runs offline.
|
|
2673
|
+
* ```ts
|
|
2674
|
+
* const run = await runSeam(emailDrafter, {
|
|
2675
|
+
* scripts: { promptEvaluator: [vague, complete], emailDrafter: [draft] },
|
|
2676
|
+
* seam: { request: 'evaluatePrompt' },
|
|
2677
|
+
* respond: ({ state }) => (state === 'prompting' ? { type: 'PROMPT_SUBMITTED', prompt } : null),
|
|
2678
|
+
* });
|
|
2679
|
+
*
|
|
2680
|
+
* matchesTrajectory(run.after.statePath, ['needsMoreInfo', 'drafting']);
|
|
2681
|
+
* ```
|
|
2682
|
+
*
|
|
2683
|
+
* @example A candidate prompt at the seam, scored against the same rows.
|
|
2684
|
+
* ```ts
|
|
2685
|
+
* const { generateText } = createAiSdkExecutors({ models });
|
|
2686
|
+
* const run = await runSeam(emailDrafter, { ...row, candidate: generateText });
|
|
2687
|
+
* ```
|
|
2688
|
+
*/
|
|
2689
|
+
async function runSeam(machine, options) {
|
|
2690
|
+
const { seam, candidate } = options;
|
|
2691
|
+
const queues = new Map(Object.entries(options.scripts ?? {}).map(([key, answers]) => [key, [...answers]]));
|
|
2692
|
+
const statePath = [];
|
|
2693
|
+
let calls = 0;
|
|
2694
|
+
let seamMatches = 0;
|
|
2695
|
+
let seamOutput;
|
|
2696
|
+
let seamReached = false;
|
|
2697
|
+
let callsBeforeSeam = -1;
|
|
2698
|
+
let seamStateAt = 0;
|
|
2699
|
+
let seamEventAt = 0;
|
|
2700
|
+
let liveEvents = 0;
|
|
2701
|
+
const scriptedAnswer = async (request, info, optional) => {
|
|
2702
|
+
const key = request.name !== void 0 && queues.has(request.name) ? request.name : request.model;
|
|
2703
|
+
const queue = queues.get(key);
|
|
2704
|
+
if (!queue?.length) {
|
|
2705
|
+
if (optional) return;
|
|
2706
|
+
throw new SeamScriptError(`runSeam: no scripted answer left for request ${describeRequest(request)}. Add an entry to \`scripts.${key}\` — its last entry repeats, so one extra answer covers a longer branch.`);
|
|
2707
|
+
}
|
|
2708
|
+
return resolveScriptedTextEntry(queue.length === 1 ? queue[0] : queue.shift(), request, info);
|
|
2709
|
+
};
|
|
2710
|
+
const route = async (request, info) => {
|
|
2711
|
+
const callIndex = calls++;
|
|
2712
|
+
const isSeam = (seam.request !== void 0 ? request.name === seam.request : request.model === seam.model) && seamMatches++ === (seam.occurrence ?? 0);
|
|
2713
|
+
const scripted = await scriptedAnswer(request, info, isSeam && candidate !== void 0);
|
|
2714
|
+
if (!isSeam) return scripted;
|
|
2715
|
+
seamReached = true;
|
|
2716
|
+
callsBeforeSeam = callIndex;
|
|
2717
|
+
seamStateAt = statePath.length;
|
|
2718
|
+
seamEventAt = liveEvents;
|
|
2719
|
+
if (!candidate) {
|
|
2720
|
+
if (!scripted) throw new SeamScriptError(`runSeam: the seam '${seamKeyOf(seam)}' has no candidate and no scripted answer. Pass \`candidate\`, or script the seam's key.`);
|
|
2721
|
+
seamOutput = scripted.output;
|
|
2722
|
+
return scripted;
|
|
2723
|
+
}
|
|
2724
|
+
const result = await candidate(request, info);
|
|
2725
|
+
seamOutput = await seamOutputOf(result, request);
|
|
2726
|
+
return result;
|
|
2727
|
+
};
|
|
2728
|
+
const executors = {
|
|
2729
|
+
...options.executors,
|
|
2730
|
+
generateText: route,
|
|
2731
|
+
streamText: async (request, info) => {
|
|
2732
|
+
const result = await route(request, info);
|
|
2733
|
+
const output = isRecord(result) ? result["output"] : void 0;
|
|
2734
|
+
if (typeof output === "string") info?.onChunk?.(output);
|
|
2735
|
+
return result;
|
|
2736
|
+
}
|
|
2737
|
+
};
|
|
2738
|
+
let snapshot;
|
|
2739
|
+
let event;
|
|
2740
|
+
let events = [];
|
|
2741
|
+
let result;
|
|
2742
|
+
const maxTurns = options.maxTurns ?? 12;
|
|
2743
|
+
for (let turn = 0; turn <= maxTurns; turn++) {
|
|
2744
|
+
result = await runAgent(machine, {
|
|
2745
|
+
...snapshot ? { snapshot } : { input: options.input },
|
|
2746
|
+
...event ? { event } : {},
|
|
2747
|
+
...options.isSuspended ? { isSuspended: options.isSuspended } : {},
|
|
2748
|
+
...options.actors ? { actors: options.actors } : {},
|
|
2749
|
+
events,
|
|
2750
|
+
executors,
|
|
2751
|
+
onEvent: () => {
|
|
2752
|
+
liveEvents++;
|
|
2753
|
+
},
|
|
2754
|
+
onTransition: (next, causedBy) => {
|
|
2755
|
+
if (turn > 0 && causedBy.type === "@xstate.init") return;
|
|
2756
|
+
statePath.push(next.value);
|
|
2757
|
+
}
|
|
2758
|
+
});
|
|
2759
|
+
events = result.events;
|
|
2760
|
+
liveEvents = events.length;
|
|
2761
|
+
if (result.status !== "idle" || turn === maxTurns) break;
|
|
2762
|
+
const next = options.respond?.({
|
|
2763
|
+
snapshot: result.snapshot,
|
|
2764
|
+
state: result.snapshot.value,
|
|
2765
|
+
meta: require_decision.getStateMeta(result.snapshot),
|
|
2766
|
+
turn,
|
|
2767
|
+
result
|
|
2768
|
+
});
|
|
2769
|
+
if (!next) break;
|
|
2770
|
+
event = next;
|
|
2771
|
+
snapshot = result.snapshot;
|
|
2772
|
+
}
|
|
2773
|
+
let splitAt = events.length;
|
|
2774
|
+
if (seamReached) {
|
|
2775
|
+
splitAt = seamEventAt;
|
|
2776
|
+
for (let index = seamEventAt; index < events.length; index++) {
|
|
2777
|
+
const type = events[index].event.type;
|
|
2778
|
+
if (type.startsWith("xstate.done") || type.startsWith("xstate.error")) {
|
|
2779
|
+
splitAt = index;
|
|
2780
|
+
break;
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
const splitStateAt = seamReached ? seamStateAt : statePath.length;
|
|
2785
|
+
return {
|
|
2786
|
+
result,
|
|
2787
|
+
seamOutput,
|
|
2788
|
+
callsBeforeSeam,
|
|
2789
|
+
before: {
|
|
2790
|
+
statePath: statePath.slice(0, splitStateAt),
|
|
2791
|
+
events: events.slice(0, splitAt)
|
|
2792
|
+
},
|
|
2793
|
+
after: {
|
|
2794
|
+
statePath: statePath.slice(splitStateAt),
|
|
2795
|
+
events: events.slice(splitAt)
|
|
2796
|
+
}
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
//#endregion
|
|
2800
|
+
exports.AGENT_EVENT_SCHEMA_VERSION = require_event_log_store.AGENT_EVENT_SCHEMA_VERSION;
|
|
2801
|
+
exports.AGENT_INIT_EVENT_TYPE = require_setup_agent.AGENT_INIT_EVENT_TYPE;
|
|
2802
|
+
exports.AGENT_TRACE_SCHEMA_VERSION = AGENT_TRACE_SCHEMA_VERSION;
|
|
2803
|
+
exports.AGENT_USAGE_EVENT_TYPE = require_setup_agent.AGENT_USAGE_EVENT_TYPE;
|
|
2804
|
+
exports.AgentDecisionExhaustedError = require_decision.AgentDecisionExhaustedError;
|
|
2805
|
+
exports.AgentError = require_errors.AgentError;
|
|
2806
|
+
exports.AgentEventLogConflictError = require_event_log_store.AgentEventLogConflictError;
|
|
2807
|
+
exports.AgentIdleError = AgentIdleError;
|
|
2808
|
+
exports.AgentIllegalResumeEventError = AgentIllegalResumeEventError;
|
|
2809
|
+
exports.AgentLintError = AgentLintError;
|
|
2810
|
+
exports.AgentReplayDivergenceError = require_setup_agent.AgentReplayDivergenceError;
|
|
2811
|
+
exports.AgentReplayMachineMismatchError = require_setup_agent.AgentReplayMachineMismatchError;
|
|
2812
|
+
exports.AgentSnapshotVersionMismatchError = AgentSnapshotVersionMismatchError;
|
|
2813
|
+
exports.NonSerializableAgentEventError = require_event_log_store.NonSerializableAgentEventError;
|
|
10
2814
|
exports.PLAN_DONE_EVENT_TYPE = require_decision.PLAN_DONE_EVENT_TYPE;
|
|
11
|
-
exports.
|
|
12
|
-
exports.
|
|
13
|
-
exports.assertAgentMachine =
|
|
2815
|
+
exports.appendMessages = require_setup_agent.appendMessages;
|
|
2816
|
+
exports.assertAgentLogEntry = require_event_log_store.assertAgentLogEntry;
|
|
2817
|
+
exports.assertAgentMachine = assertAgentMachine;
|
|
2818
|
+
exports.assertEventLogStoreConformance = require_event_log_store.assertEventLogStoreConformance;
|
|
2819
|
+
exports.assertJsonSerializable = require_event_log_store.assertJsonSerializable;
|
|
14
2820
|
exports.assistantMessage = require_decision.assistantMessage;
|
|
15
2821
|
exports.bindRequestExecutor = require_decision.bindRequestExecutor;
|
|
16
2822
|
exports.buildEnvelopeSchema = require_decision.buildEnvelopeSchema;
|
|
17
|
-
exports.canReach =
|
|
18
|
-
exports.
|
|
2823
|
+
exports.canReach = canReach;
|
|
2824
|
+
exports.createAgentActor = createAgentActor;
|
|
2825
|
+
exports.createAgentRun = createAgentRun;
|
|
2826
|
+
exports.createAgentSchemas = require_setup_agent.createAgentSchemas;
|
|
2827
|
+
exports.createInMemoryEventLogStore = require_event_log_store.createInMemoryEventLogStore;
|
|
2828
|
+
exports.createReplayEntry = require_setup_agent.createReplayEntry;
|
|
2829
|
+
exports.createScriptedExecutors = createScriptedExecutors;
|
|
19
2830
|
exports.createTextLogic = require_decision.createTextLogic;
|
|
20
|
-
exports.
|
|
21
|
-
exports.
|
|
2831
|
+
exports.diffEventLogs = require_setup_agent.diffEventLogs;
|
|
2832
|
+
exports.executeAgentRequest = require_setup_agent.executeAgentRequest;
|
|
2833
|
+
exports.explorePaths = explorePaths;
|
|
2834
|
+
exports.generateResult = generateResult;
|
|
22
2835
|
exports.getAcceptedEvents = require_decision.getAcceptedEvents;
|
|
2836
|
+
exports.getAgentEffects = require_setup_agent.getAgentEffects;
|
|
23
2837
|
exports.getAgentMessages = require_decision.getAgentMessages;
|
|
24
2838
|
exports.getAgentOutputMode = require_decision.getAgentOutputMode;
|
|
25
|
-
exports.
|
|
2839
|
+
exports.getCallUsage = require_setup_agent.getCallUsage;
|
|
26
2840
|
exports.getJsonSchema = require_decision.getJsonSchema;
|
|
27
2841
|
exports.getJsonSchemaSync = require_decision.getJsonSchemaSync;
|
|
28
2842
|
exports.getMachineStructuralHash = require_decision.getMachineStructuralHash;
|
|
29
2843
|
exports.getStateMeta = require_decision.getStateMeta;
|
|
30
|
-
exports.
|
|
31
|
-
exports.inspectTransitions =
|
|
2844
|
+
exports.initEntry = require_setup_agent.initEntry;
|
|
2845
|
+
exports.inspectTransitions = inspectTransitions;
|
|
32
2846
|
exports.isStandardSchema = require_decision.isStandardSchema;
|
|
33
|
-
exports.
|
|
34
|
-
exports.
|
|
35
|
-
exports.
|
|
36
|
-
exports.messagesSchema = require_run_agent.messagesSchema;
|
|
2847
|
+
exports.lintAgentMachine = lintAgentMachine;
|
|
2848
|
+
exports.matchesTrajectory = matchesTrajectory;
|
|
2849
|
+
exports.messagesSchema = require_setup_agent.messagesSchema;
|
|
37
2850
|
exports.parseAgentEvent = require_decision.parseAgentEvent;
|
|
38
2851
|
exports.parseModelRef = require_decision.parseModelRef;
|
|
39
2852
|
exports.parseOutput = require_decision.parseOutput;
|
|
40
2853
|
exports.parseStructuredEnvelope = require_decision.parseStructuredEnvelope;
|
|
41
2854
|
exports.persistSnapshot = require_decision.persistSnapshot;
|
|
2855
|
+
exports.provideExecutors = provideExecutors;
|
|
42
2856
|
exports.renderDecisionAttempts = require_decision.renderDecisionAttempts;
|
|
43
|
-
exports.
|
|
44
|
-
exports.resolveAgentStep = require_src.resolveAgentStep;
|
|
2857
|
+
exports.replay = require_setup_agent.replay;
|
|
45
2858
|
exports.resolveDecision = require_decision.resolveDecision;
|
|
46
|
-
exports.runAgent =
|
|
47
|
-
exports.
|
|
2859
|
+
exports.runAgent = runAgent;
|
|
2860
|
+
exports.runSeam = runSeam;
|
|
2861
|
+
exports.serializeTraceEvent = serializeTraceEvent;
|
|
48
2862
|
Object.defineProperty(exports, "setupAgent", {
|
|
49
2863
|
enumerable: true,
|
|
50
2864
|
get: function() {
|
|
51
|
-
return
|
|
2865
|
+
return require_setup_agent.setupAgent;
|
|
52
2866
|
}
|
|
53
2867
|
});
|
|
54
|
-
exports.simulateAgent =
|
|
2868
|
+
exports.simulateAgent = simulateAgent;
|
|
55
2869
|
exports.systemMessage = require_decision.systemMessage;
|
|
56
2870
|
exports.toolMessage = require_decision.toolMessage;
|
|
57
|
-
exports.
|
|
2871
|
+
exports.traceTransitions = traceTransitions;
|
|
58
2872
|
exports.userMessage = require_decision.userMessage;
|
|
59
|
-
exports.
|
|
2873
|
+
exports.verifyReplay = require_setup_agent.verifyReplay;
|