@statelyai/agent 2.0.0-alpha.24 → 2.0.0-alpha.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-sdk.cjs +30 -34
- package/dist/ai-sdk.d.cts +27 -31
- package/dist/ai-sdk.d.mts +27 -31
- package/dist/ai-sdk.mjs +27 -31
- package/dist/decision-CM9CYZ6t.cjs +519 -0
- package/dist/decision-DzSv1NpD.mjs +453 -0
- package/dist/event-log-store-Ba2fsc4E.mjs +576 -0
- package/dist/event-log-store-DQmImLYk.cjs +702 -0
- package/dist/event-log-store-DvOcs10J.d.cts +338 -0
- package/dist/event-log-store-DxyVgnas.d.mts +338 -0
- package/dist/index.cjs +429 -4272
- package/dist/index.d.cts +104 -1005
- package/dist/index.d.mts +104 -1005
- package/dist/index.mjs +389 -4202
- package/dist/log.cjs +392 -0
- package/dist/log.d.cts +32 -0
- package/dist/log.d.mts +32 -0
- package/dist/log.mjs +371 -0
- package/dist/machines.cjs +27 -16
- package/dist/machines.d.cts +1 -1
- package/dist/machines.d.mts +1 -1
- package/dist/machines.mjs +27 -16
- package/dist/openai.cjs +16 -15
- package/dist/openai.d.cts +9 -9
- package/dist/openai.d.mts +9 -9
- package/dist/openai.mjs +10 -9
- package/dist/otel.d.cts +1 -1
- package/dist/otel.d.mts +1 -1
- package/dist/{run-agent-Ck7Sa3Tb.d.mts → run-agent-BOfoJg5B.d.cts} +86 -387
- package/dist/{run-agent-DWqA8KiI.d.cts → run-agent-CG343kFs.d.mts} +86 -387
- package/dist/run-agent-Cb1-LS5P.cjs +1783 -0
- package/dist/run-agent-DvDQd4If.mjs +1669 -0
- package/dist/{setup-agent-CeO2fARv.mjs → setup-agent-CFW-jPI8.mjs} +9 -20
- package/dist/{setup-agent-Dl2VELLi.cjs → setup-agent-CPf7MxnG.cjs} +20 -37
- package/dist/testing.cjs +1222 -0
- package/dist/testing.d.cts +602 -0
- package/dist/testing.d.mts +602 -0
- package/dist/testing.mjs +1212 -0
- package/dist/{text-logic-DJBK3LbG.d.mts → text-logic-BFEOxyxv.d.cts} +71 -147
- package/dist/text-logic-Bl_ZRxDi.mjs +833 -0
- package/dist/text-logic-C8xyr6Ag.cjs +1091 -0
- package/dist/{text-logic-C7bSnH_B.d.cts → text-logic-hZSiRUo1.d.mts} +71 -147
- package/dist/{types-CjHk5OdH.d.cts → types-B1Qs2q3q.d.cts} +2 -1
- package/dist/{types-DWA6X9R-.d.mts → types-tA3SdczG.d.mts} +2 -1
- package/dist/usage-BdEjFuIi.mjs +5 -0
- package/dist/usage-efOQPdf_.cjs +10 -0
- package/dist/utils-C1p37X_c.d.mts +209 -0
- package/dist/utils-RalRn9sk.d.cts +209 -0
- package/package.json +24 -4
- package/readme.md +2 -2
- package/skills/generate-machine/SKILL.md +4 -6
- package/dist/decision-BBZ2jMO1.mjs +0 -1490
- package/dist/decision-Cv6SfNWl.cjs +0 -1856
package/dist/testing.mjs
ADDED
|
@@ -0,0 +1,1212 @@
|
|
|
1
|
+
import { H as AgentError, P as getStateMeta, S as getRegisteredAgentExecutionOptions, d as getCallUsage, f as isTextLogic, p as normalizeGeneratorResult, y as executorBoundLogics } from "./text-logic-Bl_ZRxDi.mjs";
|
|
2
|
+
import { a as isDecisionLogic, d as isRecord, l as getAcceptedEvents, s as resolveDecision, t as AgentDecisionExhaustedError } from "./decision-DzSv1NpD.mjs";
|
|
3
|
+
import { _ as rejectAgentStep, d as runAgent, g as initialAgentStep, h as getPendingInvokes, v as resolveAgentStep, y as transitionAgentStep } from "./run-agent-DvDQd4If.mjs";
|
|
4
|
+
//#region src/verify.ts
|
|
5
|
+
const DECIDE_SRC = "agent.decide";
|
|
6
|
+
function normalizeInvokes(invoke) {
|
|
7
|
+
if (invoke === void 0 || invoke === null) return [];
|
|
8
|
+
return Array.isArray(invoke) ? invoke : [invoke];
|
|
9
|
+
}
|
|
10
|
+
function buildStateIndex(rootConfig) {
|
|
11
|
+
const index = /* @__PURE__ */ new Map();
|
|
12
|
+
const walk = (states, parentPath) => {
|
|
13
|
+
for (const [name, config] of Object.entries(states ?? {})) {
|
|
14
|
+
const path = parentPath ? `${parentPath}.${name}` : name;
|
|
15
|
+
const hasChildren = !!config.states && Object.keys(config.states).length > 0;
|
|
16
|
+
index.set(path, {
|
|
17
|
+
path,
|
|
18
|
+
config,
|
|
19
|
+
parentPath,
|
|
20
|
+
isFinal: config.type === "final",
|
|
21
|
+
isParallel: config.type === "parallel",
|
|
22
|
+
isCompound: hasChildren && config.type !== "parallel",
|
|
23
|
+
invokes: normalizeInvokes(config.invoke)
|
|
24
|
+
});
|
|
25
|
+
if (hasChildren) walk(config.states, path);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
walk(rootConfig.states, "");
|
|
29
|
+
return index;
|
|
30
|
+
}
|
|
31
|
+
function ancestorChain(node, index) {
|
|
32
|
+
const chain = [];
|
|
33
|
+
let parent = node.parentPath;
|
|
34
|
+
while (parent) {
|
|
35
|
+
const parentNode = index.get(parent);
|
|
36
|
+
if (!parentNode) break;
|
|
37
|
+
chain.push(parentNode);
|
|
38
|
+
parent = parentNode.parentPath;
|
|
39
|
+
}
|
|
40
|
+
return chain;
|
|
41
|
+
}
|
|
42
|
+
function hasNonEmptyOn(config) {
|
|
43
|
+
return !!config.on && Object.keys(config.on).length > 0;
|
|
44
|
+
}
|
|
45
|
+
function isDecisionInvoke(src, actors) {
|
|
46
|
+
if (typeof src === "string") return src === DECIDE_SRC || isDecisionLogic(actors[src]);
|
|
47
|
+
return isDecisionLogic(src);
|
|
48
|
+
}
|
|
49
|
+
function isAgentLogicNeedingBinding(src) {
|
|
50
|
+
return (isTextLogic(src) || isDecisionLogic(src)) && !executorBoundLogics.has(src);
|
|
51
|
+
}
|
|
52
|
+
function checkDecideWithoutEvents(ctx) {
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const node of ctx.index.values()) for (const invoke of node.invokes) {
|
|
55
|
+
if (!isDecisionInvoke(invoke.src, ctx.actors)) continue;
|
|
56
|
+
const selfHandles = hasNonEmptyOn(node.config);
|
|
57
|
+
const ancestorHandles = ancestorChain(node, ctx.index).some((ancestor) => hasNonEmptyOn(ancestor.config));
|
|
58
|
+
const invokeObserves = invoke.onDone !== void 0;
|
|
59
|
+
if (selfHandles || ancestorHandles || invokeObserves) continue;
|
|
60
|
+
const srcName = typeof invoke.src === "string" ? invoke.src : "(inline logic)";
|
|
61
|
+
out.push({
|
|
62
|
+
code: "decide-without-events",
|
|
63
|
+
severity: "error",
|
|
64
|
+
path: node.path,
|
|
65
|
+
message: `State '${node.path}' invokes decision source '${srcName}', but neither it nor any ancestor handles any event (no 'on:'), so the decision's chosen event can never be delivered. Add an 'on:' handler for the candidate events.`
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function checkDirectObjectSrc(ctx) {
|
|
71
|
+
const out = [];
|
|
72
|
+
for (const node of ctx.index.values()) for (const invoke of node.invokes) {
|
|
73
|
+
const src = invoke.src;
|
|
74
|
+
if (typeof src === "string" || !src || typeof src !== "object") continue;
|
|
75
|
+
if (!isAgentLogicNeedingBinding(src)) continue;
|
|
76
|
+
out.push({
|
|
77
|
+
code: "direct-object-src",
|
|
78
|
+
severity: "warning",
|
|
79
|
+
path: node.path,
|
|
80
|
+
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.`
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
function handlesActorError(config) {
|
|
86
|
+
if (config["onError"] !== void 0) return true;
|
|
87
|
+
return Object.keys(config.on ?? {}).some((type) => type === "*" || type.startsWith("xstate.error"));
|
|
88
|
+
}
|
|
89
|
+
function checkInvokeWithoutOnError(ctx) {
|
|
90
|
+
const out = [];
|
|
91
|
+
for (const node of ctx.index.values()) for (const invoke of node.invokes) {
|
|
92
|
+
if (invoke.onError !== void 0 || handlesActorError(node.config)) continue;
|
|
93
|
+
if (handlesActorError(ctx.config) || ancestorChain(node, ctx.index).some((ancestor) => handlesActorError(ancestor.config))) continue;
|
|
94
|
+
const srcName = typeof invoke.src === "string" ? invoke.src : "(inline logic)";
|
|
95
|
+
out.push({
|
|
96
|
+
code: "invoke-without-on-error",
|
|
97
|
+
severity: "warning",
|
|
98
|
+
path: node.path,
|
|
99
|
+
message: `State '${node.path}' invokes '${srcName}' with no 'onError' transition, and no ancestor handles an actor error. A rejected request or actor puts the machine in an error state with no modeled recovery. Add 'onError' (a 'failed' final state, or a bounded retry).`
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
const LINT_CHECKS = [
|
|
105
|
+
checkDecideWithoutEvents,
|
|
106
|
+
checkDirectObjectSrc,
|
|
107
|
+
checkInvokeWithoutOnError
|
|
108
|
+
];
|
|
109
|
+
/**
|
|
110
|
+
* Runs static structural checks over a built agent machine and returns the
|
|
111
|
+
* findings ({@link AgentLintDiagnostic}[], empty when clean). Works for
|
|
112
|
+
* TS-authored (`setupAgent(...).createMachine(...)`) and
|
|
113
|
+
* `setupAgent.fromConfig(...)`-compiled machines alike, reading `machine.config`
|
|
114
|
+
* plus the schemas/actor sources the library already retains per machine.
|
|
115
|
+
*
|
|
116
|
+
* No model calls, no API keys — a coding agent that emits an agent machine can
|
|
117
|
+
* call this to catch agent-specific mistakes such as undeliverable decisions,
|
|
118
|
+
* un-rebindable request sources, and an unhandled transcript event. General
|
|
119
|
+
* state-machine lint belongs to XState tooling.
|
|
120
|
+
*
|
|
121
|
+
* Pass `{ throw: true }` for the one-liner form used in tests and generation
|
|
122
|
+
* loops: it returns silently when the machine is clean and throws
|
|
123
|
+
* {@link AgentLintError} (findings on `.diagnostics`) on error-severity
|
|
124
|
+
* findings, or on warnings too with `{ throw: true, warnings: true }`.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```ts
|
|
128
|
+
* const errors = lintAgentMachine(machine).filter((d) => d.severity === 'error');
|
|
129
|
+
* if (errors.length) throw new Error(errors.map((e) => `${e.path}: ${e.message}`).join('\n'));
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* @example Throwing form
|
|
133
|
+
* ```ts
|
|
134
|
+
* test('agent machine is structurally sound', () => {
|
|
135
|
+
* lintAgentMachine(machine, { throw: true });
|
|
136
|
+
* });
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
function lintAgentMachine(machine, options = {}) {
|
|
140
|
+
const config = machine.config ?? {};
|
|
141
|
+
const index = buildStateIndex(config);
|
|
142
|
+
const registered = getRegisteredAgentExecutionOptions(machine);
|
|
143
|
+
const ctx = {
|
|
144
|
+
config,
|
|
145
|
+
index,
|
|
146
|
+
schemas: registered.schemas,
|
|
147
|
+
actors: registered.actors ?? machine.sources?.actors ?? {}
|
|
148
|
+
};
|
|
149
|
+
const disabled = new Set(options.disable ?? []);
|
|
150
|
+
const diagnostics = LINT_CHECKS.flatMap((check) => check(ctx)).filter((d) => !disabled.has(d.code));
|
|
151
|
+
if (options.throw) {
|
|
152
|
+
const failing = options.warnings ? diagnostics : diagnostics.filter((d) => d.severity === "error");
|
|
153
|
+
if (failing.length > 0) throw new AgentLintError(machine.id ?? "(machine)", failing);
|
|
154
|
+
}
|
|
155
|
+
return diagnostics;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Thrown by `lintAgentMachine(machine, { throw: true })` (and its
|
|
159
|
+
* {@link assertAgentMachine} alias) when lint finds failing diagnostics.
|
|
160
|
+
* `diagnostics` holds the findings; the message lists them one per finding,
|
|
161
|
+
* so a test runner's failure output reads like the CLI's lint report.
|
|
162
|
+
*/
|
|
163
|
+
var AgentLintError = class extends AgentError {
|
|
164
|
+
diagnostics;
|
|
165
|
+
constructor(machineId, diagnostics) {
|
|
166
|
+
const lines = diagnostics.map((d) => ` ${d.severity === "error" ? "error" : "warn "} ${d.code} ${d.path}\n ${d.message}`);
|
|
167
|
+
super("lint-failed", `Agent machine '${machineId}' failed lint (${diagnostics.length} finding(s)):\n${lines.join("\n")}`);
|
|
168
|
+
this.name = "AgentLintError";
|
|
169
|
+
this.diagnostics = diagnostics;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Asserts a machine passes {@link lintAgentMachine}: returns silently when
|
|
174
|
+
* clean, throws {@link AgentLintError} (with the findings on `.diagnostics`)
|
|
175
|
+
* otherwise — sugar for `lintAgentMachine(machine, { ...options, throw: true })`.
|
|
176
|
+
* Fails on error-severity findings; set `warnings: true` to fail on warnings
|
|
177
|
+
* too. The one-liner for tests and generation loops:
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```ts
|
|
181
|
+
* test('agent machine is structurally sound', () => {
|
|
182
|
+
* assertAgentMachine(machine);
|
|
183
|
+
* });
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
function assertAgentMachine(machine, options = {}) {
|
|
187
|
+
lintAgentMachine(machine, {
|
|
188
|
+
...options,
|
|
189
|
+
throw: true
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function pendingInvokes(step) {
|
|
193
|
+
const out = [];
|
|
194
|
+
for (const metadata of getPendingInvokes(step.snapshot)) if (typeof metadata.src === "string") out.push({
|
|
195
|
+
id: metadata.id,
|
|
196
|
+
src: metadata.src
|
|
197
|
+
});
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
function takeFromQueue(channel, src) {
|
|
201
|
+
const queue = channel?.[src];
|
|
202
|
+
if (queue && queue.length > 0) return {
|
|
203
|
+
found: true,
|
|
204
|
+
value: queue.shift()
|
|
205
|
+
};
|
|
206
|
+
return { found: false };
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Deterministically plays a machine through, resolving each request from a
|
|
210
|
+
* {@link SimulationScript} instead of a model — no API keys, no model calls.
|
|
211
|
+
* Runs on the pure step path ({@link initialAgentStep} etc.), so it exercises
|
|
212
|
+
* the real transition logic. Returns the terminal `status`, final `snapshot`,
|
|
213
|
+
* and a `trail` of every step taken.
|
|
214
|
+
*
|
|
215
|
+
* Decisions run through the live run's own validation/retry core,
|
|
216
|
+
* {@link resolveDecision}, with the script standing in for the model: each
|
|
217
|
+
* attempt consumes the next queued {@link ChosenEvent} for that src, so an
|
|
218
|
+
* unknown, payload-invalid, or guard-rejected event is NOT silently swallowed
|
|
219
|
+
* — the next queued decision is tried, exactly as a live run re-asks the
|
|
220
|
+
* model. The decision logic's `maxRetries` caps attempts as it would live;
|
|
221
|
+
* when retries continue past the end of the queue, the last queued decision
|
|
222
|
+
* repeats (a scripted model that insists). The repeat applies only within one
|
|
223
|
+
* decision request's retries — each new decision request must have its own
|
|
224
|
+
* queued entry, or the dry-script error throws as usual. Exhausting all
|
|
225
|
+
* attempts delivers
|
|
226
|
+
* the resulting {@link AgentDecisionExhaustedError} to the machine as the
|
|
227
|
+
* decision invoke's error (so an `onError` transition observes it, as it
|
|
228
|
+
* would live); with no `onError` to catch it, it is thrown.
|
|
229
|
+
*
|
|
230
|
+
* Throws a descriptive error when the script runs dry mid-request, naming the
|
|
231
|
+
* pending request's kind, src, and id so the missing scripted response is
|
|
232
|
+
* obvious.
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```ts
|
|
236
|
+
* const { status, snapshot } = simulateAgent(machine, {
|
|
237
|
+
* input: { topic: 'state machines' },
|
|
238
|
+
* script: {
|
|
239
|
+
* decisions: { 'agent.decide': [{ type: 'ESCALATE' }] },
|
|
240
|
+
* events: [{ type: 'APPROVE' }], // crosses the human gate
|
|
241
|
+
* },
|
|
242
|
+
* });
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
async function simulateAgent(machine, options) {
|
|
246
|
+
const maxSteps = options.maxSteps ?? 100;
|
|
247
|
+
const script = {
|
|
248
|
+
text: mapValues(options.script.text ?? {}, (arr) => [...arr]),
|
|
249
|
+
decisions: mapValues(options.script.decisions ?? {}, (arr) => [...arr]),
|
|
250
|
+
invokes: mapValues(options.script.invokes ?? {}, (arr) => [...arr]),
|
|
251
|
+
errors: mapValues(options.script.errors ?? {}, (arr) => [...arr]),
|
|
252
|
+
events: [...options.script.events ?? []]
|
|
253
|
+
};
|
|
254
|
+
let step = initialAgentStep(machine, options.input);
|
|
255
|
+
const trail = [{ state: step.snapshot.value }];
|
|
256
|
+
for (let i = 0; i < maxSteps; i++) {
|
|
257
|
+
if (step.done) return {
|
|
258
|
+
status: "done",
|
|
259
|
+
snapshot: step.snapshot,
|
|
260
|
+
trail
|
|
261
|
+
};
|
|
262
|
+
const request = step.requests[0];
|
|
263
|
+
if (request) {
|
|
264
|
+
if (request.kind === "decision") {
|
|
265
|
+
const invokeMeta = findInvokeMetadata(step, request.id);
|
|
266
|
+
const src = invokeMeta?.src ?? request.id;
|
|
267
|
+
const decisionSrc = src in (script.decisions ?? {}) ? src : request.id in (script.decisions ?? {}) ? request.id : src;
|
|
268
|
+
const errorSrc = queueKey(script.errors, src, request.id);
|
|
269
|
+
const failure = takeFromQueue(script.errors, errorSrc);
|
|
270
|
+
if (failure.found) {
|
|
271
|
+
step = rejectScriptedRequest(machine, step, request.id, failure.value, {
|
|
272
|
+
kind: "decision",
|
|
273
|
+
src: errorSrc,
|
|
274
|
+
trail
|
|
275
|
+
});
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
step = await applyScriptedDecision(machine, step, request, decisionSrc, invokeMeta?.logic, script, trail);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const failure = takeFromQueue(script.errors, request.src);
|
|
282
|
+
if (failure.found) {
|
|
283
|
+
step = rejectScriptedRequest(machine, step, request, failure.value, {
|
|
284
|
+
kind: "text",
|
|
285
|
+
src: request.src,
|
|
286
|
+
trail
|
|
287
|
+
});
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const taken = takeFromQueue(script.text, request.src);
|
|
291
|
+
if (!taken.found) throw scriptDryError("text", request.src, request.id);
|
|
292
|
+
step = resolveAgentStep(machine, step, request, asTextResult(taken.value));
|
|
293
|
+
trail.push({
|
|
294
|
+
state: step.snapshot.value,
|
|
295
|
+
resolvedRequest: {
|
|
296
|
+
kind: "text",
|
|
297
|
+
src: request.src,
|
|
298
|
+
id: request.id,
|
|
299
|
+
outcome: "output"
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const [invoke] = pendingInvokes(step);
|
|
305
|
+
if (invoke) {
|
|
306
|
+
const failure = takeFromQueue(script.errors, invoke.src);
|
|
307
|
+
if (failure.found) {
|
|
308
|
+
step = rejectScriptedRequest(machine, step, invoke.id, failure.value, {
|
|
309
|
+
kind: "invoke",
|
|
310
|
+
src: invoke.src,
|
|
311
|
+
trail
|
|
312
|
+
});
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const taken = takeFromQueue(script.invokes, invoke.src);
|
|
316
|
+
if (!taken.found) throw scriptDryError("invoke", invoke.src, invoke.id);
|
|
317
|
+
step = resolveAgentStep(machine, step, invoke.id, taken.value);
|
|
318
|
+
trail.push({
|
|
319
|
+
state: step.snapshot.value,
|
|
320
|
+
resolvedRequest: {
|
|
321
|
+
kind: "invoke",
|
|
322
|
+
src: invoke.src,
|
|
323
|
+
id: invoke.id,
|
|
324
|
+
outcome: "output"
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const external = script.events;
|
|
330
|
+
if (external.length > 0) {
|
|
331
|
+
const event = external.shift();
|
|
332
|
+
if (!step.snapshot.can(event)) throw new Error(`simulateAgent: scripted external event '${event.type}' cannot be taken in state ${JSON.stringify(step.snapshot.value)} (no handler, or its guard rejected it). A live run's send would be silently dropped here; fix the script's \`events\` queue.`);
|
|
333
|
+
step = transitionAgentStep(machine, step, event);
|
|
334
|
+
trail.push({
|
|
335
|
+
state: step.snapshot.value,
|
|
336
|
+
appliedEvent: event,
|
|
337
|
+
external: true
|
|
338
|
+
});
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
status: "idle",
|
|
343
|
+
snapshot: step.snapshot,
|
|
344
|
+
trail
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
status: "exhausted",
|
|
349
|
+
snapshot: step.snapshot,
|
|
350
|
+
trail
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function queueKey(channel, src, id) {
|
|
354
|
+
return channel && !(src in channel) && id in channel ? id : src;
|
|
355
|
+
}
|
|
356
|
+
function rejectScriptedRequest(machine, step, request, error, entry) {
|
|
357
|
+
const id = typeof request === "string" ? request : request.id;
|
|
358
|
+
const next = rejectAgentStep(machine, step, request, error);
|
|
359
|
+
if (next.snapshot.status === "error") throw unhandledScriptedError(error, entry.kind, entry.src, id);
|
|
360
|
+
entry.trail.push({
|
|
361
|
+
state: next.snapshot.value,
|
|
362
|
+
resolvedRequest: {
|
|
363
|
+
kind: entry.kind,
|
|
364
|
+
src: entry.src,
|
|
365
|
+
id,
|
|
366
|
+
outcome: "error",
|
|
367
|
+
error
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
return next;
|
|
371
|
+
}
|
|
372
|
+
function unhandledScriptedError(error, kind, src, id) {
|
|
373
|
+
if (error instanceof Error) return error;
|
|
374
|
+
return new Error(`simulateAgent: the scripted error for the ${kind} request '${src}' (id '${id}') reached no onError transition, so the machine errored. Add an onError to that invoke, or drop the errors['${src}'] entry.`, { cause: error });
|
|
375
|
+
}
|
|
376
|
+
async function applyScriptedDecision(machine, step, request, decisionSrc, invokeLogic, script, trail) {
|
|
377
|
+
const dequeued = [];
|
|
378
|
+
const decide = async () => {
|
|
379
|
+
const taken = takeFromQueue(script.decisions, decisionSrc);
|
|
380
|
+
if (taken.found) {
|
|
381
|
+
dequeued.push(taken.value);
|
|
382
|
+
return { event: taken.value };
|
|
383
|
+
}
|
|
384
|
+
if (dequeued.length === 0) throw scriptDryError("decision", decisionSrc, request.id, request);
|
|
385
|
+
return { event: dequeued[dequeued.length - 1] };
|
|
386
|
+
};
|
|
387
|
+
const maxRetries = (isDecisionLogic(invokeLogic) ? invokeLogic : resolveRegisteredDecisionLogic(machine, decisionSrc))?.maxRetries;
|
|
388
|
+
let chosen;
|
|
389
|
+
try {
|
|
390
|
+
chosen = await resolveDecision(request, { decide }, {
|
|
391
|
+
maxRetries,
|
|
392
|
+
canTake: (event) => step.snapshot.can(event)
|
|
393
|
+
});
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (!(error instanceof AgentDecisionExhaustedError)) throw error;
|
|
396
|
+
const next = rejectAgentStep(machine, step, request.id, error);
|
|
397
|
+
if (next.snapshot.status === "error") throw error;
|
|
398
|
+
trail.push({
|
|
399
|
+
state: next.snapshot.value,
|
|
400
|
+
rejectedEvents: dequeued
|
|
401
|
+
});
|
|
402
|
+
return next;
|
|
403
|
+
}
|
|
404
|
+
const next = transitionAgentStep(machine, step, chosen);
|
|
405
|
+
const rejected = dequeued.slice(0, -1);
|
|
406
|
+
trail.push({
|
|
407
|
+
state: next.snapshot.value,
|
|
408
|
+
appliedEvent: chosen,
|
|
409
|
+
...rejected.length > 0 ? { rejectedEvents: rejected } : {}
|
|
410
|
+
});
|
|
411
|
+
return next;
|
|
412
|
+
}
|
|
413
|
+
function findInvokeMetadata(step, id) {
|
|
414
|
+
for (const metadata of getPendingInvokes(step.snapshot)) {
|
|
415
|
+
if (metadata.id !== id) continue;
|
|
416
|
+
return {
|
|
417
|
+
...typeof metadata.src === "string" ? { src: metadata.src } : {},
|
|
418
|
+
logic: metadata.logic ?? (typeof metadata.src === "object" ? metadata.src : void 0)
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function resolveRegisteredDecisionLogic(machine, src) {
|
|
423
|
+
const candidate = getRegisteredAgentExecutionOptions(machine).actors?.[src] ?? machine.sources?.actors?.[src];
|
|
424
|
+
return isDecisionLogic(candidate) ? candidate : void 0;
|
|
425
|
+
}
|
|
426
|
+
function asTextResult(value) {
|
|
427
|
+
return {
|
|
428
|
+
result: value,
|
|
429
|
+
messages: []
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function mapValues(obj, fn) {
|
|
433
|
+
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, fn(value)]));
|
|
434
|
+
}
|
|
435
|
+
function scriptDryError(kind, src, id, request) {
|
|
436
|
+
const events = request?.kind === "decision" ? ` Candidate events: ${request.events.map((e) => e.type).join(", ") || "(none)"}.` : "";
|
|
437
|
+
const key = kind === "text" ? `text['${src}']` : kind === "decision" ? `decisions['${src}']` : `invokes['${src}']`;
|
|
438
|
+
return /* @__PURE__ */ new Error(`simulateAgent: script ran dry on a pending ${kind} request for src '${src}' (id '${id}'). Add an entry to the script's \`${key}\` queue.${events}`);
|
|
439
|
+
}
|
|
440
|
+
const MAX_ADVANCE_STEPS = 1e3;
|
|
441
|
+
function invokeFork(id, src, outputs) {
|
|
442
|
+
return src in outputs ? {
|
|
443
|
+
id,
|
|
444
|
+
src,
|
|
445
|
+
hasOutput: true,
|
|
446
|
+
output: outputs[src]
|
|
447
|
+
} : {
|
|
448
|
+
id,
|
|
449
|
+
src,
|
|
450
|
+
hasOutput: false
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
async function explore(machine, options, stopWhen) {
|
|
454
|
+
const maxDepth = options.maxDepth ?? 8;
|
|
455
|
+
const maxPaths = options.maxPaths ?? 200;
|
|
456
|
+
const textScript = options.text ?? {};
|
|
457
|
+
const invokeOutputs = { ...options.invokes };
|
|
458
|
+
const errorScript = options.errors ?? {};
|
|
459
|
+
const reachedStates = /* @__PURE__ */ new Set();
|
|
460
|
+
const reachedValues = [];
|
|
461
|
+
const terminals = [];
|
|
462
|
+
const unexplored = [];
|
|
463
|
+
let prunedByGuard = 0;
|
|
464
|
+
let pathsExplored = 0;
|
|
465
|
+
let hitPathCap = false;
|
|
466
|
+
let witness;
|
|
467
|
+
const recordState = (snapshot) => {
|
|
468
|
+
const key = JSON.stringify(snapshot.value);
|
|
469
|
+
if (!reachedStates.has(key)) {
|
|
470
|
+
reachedStates.add(key);
|
|
471
|
+
reachedValues.push(snapshot.value);
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
const initial = initialAgentStep(machine, options.input);
|
|
475
|
+
recordState(initial.snapshot);
|
|
476
|
+
if (stopWhen?.(initial.snapshot)) witness = [];
|
|
477
|
+
const advance = (step) => {
|
|
478
|
+
let current = step;
|
|
479
|
+
for (let i = 0; i < MAX_ADVANCE_STEPS; i++) {
|
|
480
|
+
if (stopWhen?.(current.snapshot)) return {
|
|
481
|
+
step: current,
|
|
482
|
+
hit: true
|
|
483
|
+
};
|
|
484
|
+
if (current.done) return { step: current };
|
|
485
|
+
const request = current.requests[0];
|
|
486
|
+
if (request && request.kind === "text") {
|
|
487
|
+
if (request.src in errorScript) return {
|
|
488
|
+
step: current,
|
|
489
|
+
fork: invokeFork(request.id, request.src, textScript)
|
|
490
|
+
};
|
|
491
|
+
if (!(request.src in textScript)) return {
|
|
492
|
+
step: current,
|
|
493
|
+
blockedSrc: request.src
|
|
494
|
+
};
|
|
495
|
+
current = resolveAgentStep(machine, current, request, asTextResult(textScript[request.src]));
|
|
496
|
+
recordState(current.snapshot);
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (request && request.kind === "decision") {
|
|
500
|
+
const src = findInvokeMetadata(current, request.id)?.src;
|
|
501
|
+
const errorSrc = src !== void 0 && src in errorScript ? src : request.id in errorScript ? request.id : void 0;
|
|
502
|
+
return errorSrc !== void 0 ? {
|
|
503
|
+
step: current,
|
|
504
|
+
fork: {
|
|
505
|
+
id: request.id,
|
|
506
|
+
src: errorSrc,
|
|
507
|
+
hasOutput: false,
|
|
508
|
+
isDecision: true
|
|
509
|
+
}
|
|
510
|
+
} : { step: current };
|
|
511
|
+
}
|
|
512
|
+
const [invoke] = pendingInvokes(current);
|
|
513
|
+
if (invoke) {
|
|
514
|
+
if (invoke.src in errorScript) return {
|
|
515
|
+
step: current,
|
|
516
|
+
fork: invokeFork(invoke.id, invoke.src, invokeOutputs)
|
|
517
|
+
};
|
|
518
|
+
if (!(invoke.src in invokeOutputs)) return {
|
|
519
|
+
step: current,
|
|
520
|
+
blockedSrc: invoke.src
|
|
521
|
+
};
|
|
522
|
+
current = resolveAgentStep(machine, current, invoke.id, invokeOutputs[invoke.src]);
|
|
523
|
+
recordState(current.snapshot);
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
return { step: current };
|
|
527
|
+
}
|
|
528
|
+
return { step: current };
|
|
529
|
+
};
|
|
530
|
+
const visit = async (step, path, startDepth) => {
|
|
531
|
+
let depth = startDepth;
|
|
532
|
+
if (witness !== void 0) return;
|
|
533
|
+
if (pathsExplored >= maxPaths) {
|
|
534
|
+
hitPathCap = true;
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const { step: settled, blockedSrc, hit, fork } = advance(step);
|
|
538
|
+
if (hit) {
|
|
539
|
+
witness = path;
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (fork) {
|
|
543
|
+
if (depth >= maxDepth) {
|
|
544
|
+
pathsExplored++;
|
|
545
|
+
terminals.push({
|
|
546
|
+
status: "max-depth",
|
|
547
|
+
path,
|
|
548
|
+
state: settled.snapshot.value
|
|
549
|
+
});
|
|
550
|
+
unexplored.push(`max-depth: stopped at path [${path.map((e) => e.type).join(", ")}]`);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const failed = rejectAgentStep(machine, settled, fork.id, errorScript[fork.src]);
|
|
554
|
+
recordState(failed.snapshot);
|
|
555
|
+
await visit(failed, [...path, { type: `xstate.error.actor.${fork.id}` }], depth + 1);
|
|
556
|
+
if (!fork.isDecision) {
|
|
557
|
+
if (fork.hasOutput) {
|
|
558
|
+
const resolved = resolveAgentStep(machine, settled, fork.id, fork.output);
|
|
559
|
+
recordState(resolved.snapshot);
|
|
560
|
+
await visit(resolved, path, depth + 1);
|
|
561
|
+
}
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
depth += 1;
|
|
565
|
+
if (witness !== void 0) return;
|
|
566
|
+
}
|
|
567
|
+
if (blockedSrc) {
|
|
568
|
+
pathsExplored++;
|
|
569
|
+
terminals.push({
|
|
570
|
+
status: "needs-output",
|
|
571
|
+
path,
|
|
572
|
+
state: settled.snapshot.value,
|
|
573
|
+
missingSrc: blockedSrc
|
|
574
|
+
});
|
|
575
|
+
unexplored.push(`needs-output: no canned output for src '${blockedSrc}' at path [${path.map((e) => e.type).join(", ")}]`);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (settled.done) {
|
|
579
|
+
pathsExplored++;
|
|
580
|
+
terminals.push({
|
|
581
|
+
status: "done",
|
|
582
|
+
path,
|
|
583
|
+
state: settled.snapshot.value
|
|
584
|
+
});
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (settled.snapshot.status === "error") {
|
|
588
|
+
pathsExplored++;
|
|
589
|
+
terminals.push({
|
|
590
|
+
status: "error",
|
|
591
|
+
path,
|
|
592
|
+
state: settled.snapshot.value,
|
|
593
|
+
error: settled.snapshot.error
|
|
594
|
+
});
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const request = settled.requests[0];
|
|
598
|
+
const branchEvents = request?.kind === "decision" ? request.events.map((descriptor) => ({ type: descriptor.type })) : getAcceptedEvents(settled.snapshot).map((descriptor) => ({ type: descriptor.type }));
|
|
599
|
+
if (branchEvents.length === 0) {
|
|
600
|
+
pathsExplored++;
|
|
601
|
+
terminals.push({
|
|
602
|
+
status: "idle",
|
|
603
|
+
path,
|
|
604
|
+
state: settled.snapshot.value
|
|
605
|
+
});
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (depth >= maxDepth) {
|
|
609
|
+
pathsExplored++;
|
|
610
|
+
terminals.push({
|
|
611
|
+
status: "max-depth",
|
|
612
|
+
path,
|
|
613
|
+
state: settled.snapshot.value
|
|
614
|
+
});
|
|
615
|
+
unexplored.push(`max-depth: stopped at path [${path.map((e) => e.type).join(", ")}]`);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
for (const event of branchEvents) {
|
|
619
|
+
if (witness !== void 0 || pathsExplored >= maxPaths) {
|
|
620
|
+
if (pathsExplored >= maxPaths) hitPathCap = true;
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (!settled.snapshot.can(event)) {
|
|
624
|
+
prunedByGuard++;
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
const next = transitionAgentStep(machine, settled, event);
|
|
628
|
+
recordState(next.snapshot);
|
|
629
|
+
await visit(next, [...path, event], depth + 1);
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
if (witness === void 0) await visit(initial, [], 0);
|
|
633
|
+
return {
|
|
634
|
+
report: {
|
|
635
|
+
reachedStates: reachedValues,
|
|
636
|
+
terminals,
|
|
637
|
+
prunedByGuard,
|
|
638
|
+
unexplored,
|
|
639
|
+
pathsExplored,
|
|
640
|
+
hitPathCap
|
|
641
|
+
},
|
|
642
|
+
witness
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Enumerates a machine's decision and external-event branches to a bounded
|
|
647
|
+
* depth, model-free, and reports which states are reached and how each path
|
|
648
|
+
* terminates. At each decision request it forks one branch per candidate event
|
|
649
|
+
* (guard-rejected candidates are counted in `prunedByGuard`, not explored); at
|
|
650
|
+
* an idle wait it forks per externally-accepted event. Text requests resolve
|
|
651
|
+
* from `text`, other invokes from `invokes` — all by-src canned-output maps, and a missing src halts
|
|
652
|
+
* that branch with a `needs-output` terminal rather than throwing. A src in
|
|
653
|
+
* `errors` forks an extra branch where that invoke is rejected, so states
|
|
654
|
+
* behind an `onError` are explored too.
|
|
655
|
+
*
|
|
656
|
+
* Combinatorics are bounded by `maxDepth` (default 8) and `maxPaths` (default
|
|
657
|
+
* 200, reported via `hitPathCap`).
|
|
658
|
+
*
|
|
659
|
+
* @example
|
|
660
|
+
* ```ts
|
|
661
|
+
* const report = await explorePaths(refundMachine, { input: { request: 'x', amount: 5000 } });
|
|
662
|
+
* // report.terminals → both 'refunded' and 'denied'; report.prunedByGuard → 1
|
|
663
|
+
* ```
|
|
664
|
+
*/
|
|
665
|
+
async function explorePaths(machine, options = {}) {
|
|
666
|
+
return (await explore(machine, options)).report;
|
|
667
|
+
}
|
|
668
|
+
/** Thrown when a string reachability target is not a state in the machine. */
|
|
669
|
+
var AgentUnknownStateError = class extends AgentError {
|
|
670
|
+
target;
|
|
671
|
+
constructor(machineId, target) {
|
|
672
|
+
super("unknown-state", `canReach: machine '${machineId}' has no state matching '${target}'.`);
|
|
673
|
+
this.name = "AgentUnknownStateError";
|
|
674
|
+
this.target = target;
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
function resolveStateTarget(machine, target) {
|
|
678
|
+
const stack = [machine.root];
|
|
679
|
+
while (stack.length > 0) {
|
|
680
|
+
const node = stack.pop();
|
|
681
|
+
if (node.id === target.replace(/^#/, "") || node.path.join(".") === target) return node.id;
|
|
682
|
+
stack.push(...Object.values(node.states));
|
|
683
|
+
}
|
|
684
|
+
throw new AgentUnknownStateError(machine.id, target);
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Answers "can the machine reach this?" by exploring its branches (a thin
|
|
688
|
+
* wrapper over {@link explorePaths}). The target is either a state path string
|
|
689
|
+
* (`snapshot.matches(...)` semantics) or a snapshot predicate — the predicate
|
|
690
|
+
* form checks any property (a context invariant, a tag, a state+context
|
|
691
|
+
* combination) without reifying a sentinel state for it. Returns
|
|
692
|
+
* `{ reachable: true, witness }` with the event sequence that reaches it, or
|
|
693
|
+
* `{ reachable: false }`.
|
|
694
|
+
*
|
|
695
|
+
* @example
|
|
696
|
+
* ```ts
|
|
697
|
+
* const { reachable, witness } = await canReach(refundMachine, 'denied', { input: { request: 'x', amount: 5000 } });
|
|
698
|
+
* // reachable → true; witness → [{ type: 'NEEDS_REVIEW' }, { type: 'DENY' }]
|
|
699
|
+
* ```
|
|
700
|
+
*
|
|
701
|
+
* @example Predicate target — a violation property, no sentinel state needed
|
|
702
|
+
* ```ts
|
|
703
|
+
* const violation = await canReach(
|
|
704
|
+
* refundMachine,
|
|
705
|
+
* (snapshot) => snapshot.matches('issued') && !snapshot.context.approved,
|
|
706
|
+
* { input: { amount: 5000 } },
|
|
707
|
+
* );
|
|
708
|
+
* // violation.reachable → false is the safety proof
|
|
709
|
+
* ```
|
|
710
|
+
*/
|
|
711
|
+
async function canReach(machine, target, options = {}) {
|
|
712
|
+
const resolvedTarget = typeof target === "string" ? resolveStateTarget(machine, target) : void 0;
|
|
713
|
+
const { witness } = await explore(machine, options, typeof target === "function" ? target : (snapshot) => snapshot.nodes.some((node) => node.id === resolvedTarget));
|
|
714
|
+
return witness !== void 0 ? {
|
|
715
|
+
reachable: true,
|
|
716
|
+
witness,
|
|
717
|
+
...resolvedTarget ? { target: resolvedTarget } : {}
|
|
718
|
+
} : {
|
|
719
|
+
reachable: false,
|
|
720
|
+
...resolvedTarget ? { target: resolvedTarget } : {}
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
//#endregion
|
|
724
|
+
//#region src/trajectory.ts
|
|
725
|
+
/**
|
|
726
|
+
* Classifies one item. Event-shaped things (log entries, events) become events;
|
|
727
|
+
* everything else is a state value. A snapshot is unwrapped to its `value`.
|
|
728
|
+
* @internal
|
|
729
|
+
*/
|
|
730
|
+
function normalize(item) {
|
|
731
|
+
if (isRecord(item)) {
|
|
732
|
+
const nested = item["event"];
|
|
733
|
+
if (isRecord(nested) && typeof nested["type"] === "string") return {
|
|
734
|
+
kind: "event",
|
|
735
|
+
event: nested
|
|
736
|
+
};
|
|
737
|
+
if (typeof item["type"] === "string") return {
|
|
738
|
+
kind: "event",
|
|
739
|
+
event: item
|
|
740
|
+
};
|
|
741
|
+
if ("value" in item) return {
|
|
742
|
+
kind: "state",
|
|
743
|
+
value: item["value"]
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
return {
|
|
747
|
+
kind: "state",
|
|
748
|
+
value: item
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
/** Every leaf of a state value as a dot path (`{ review: 'editing' }` → `review.editing`). @internal */
|
|
752
|
+
function statePaths(value) {
|
|
753
|
+
if (typeof value === "string") return [value];
|
|
754
|
+
if (!isRecord(value)) return [];
|
|
755
|
+
return Object.entries(value).flatMap(([key, child]) => statePaths(child).map((path) => `${key}.${path}`));
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* A string expectation matches a state value when it names one of its leaves or
|
|
759
|
+
* an ancestor of one, so `'review'` matches `{ review: 'editing' }`. @internal
|
|
760
|
+
*/
|
|
761
|
+
function stateMatchesPath(path, value) {
|
|
762
|
+
return statePaths(value).some((leaf) => leaf === path || leaf.startsWith(`${path}.`));
|
|
763
|
+
}
|
|
764
|
+
function deepEqual(a, b) {
|
|
765
|
+
if (Object.is(a, b)) return true;
|
|
766
|
+
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]));
|
|
767
|
+
if (!isRecord(a) || !isRecord(b)) return false;
|
|
768
|
+
const keys = Object.keys(a);
|
|
769
|
+
return keys.length === Object.keys(b).length && keys.every((key) => deepEqual(a[key], b[key]));
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Normalizes ONE expected item into a predicate over actual items, so the
|
|
773
|
+
* expectation is classified once instead of once per candidate:
|
|
774
|
+
*
|
|
775
|
+
* - A string expectation matches an event's `type`, or a state value it names.
|
|
776
|
+
* - An event expectation matches an event with the same `type` whose other
|
|
777
|
+
* declared keys deep-equal, so payload details are opt-in.
|
|
778
|
+
* - A state-value expectation matches structurally.
|
|
779
|
+
* @internal
|
|
780
|
+
*/
|
|
781
|
+
function matcherFor(expected) {
|
|
782
|
+
if (typeof expected === "string") return (actual) => {
|
|
783
|
+
const actualItem = normalize(actual);
|
|
784
|
+
return actualItem.kind === "event" ? actualItem.event.type === expected : stateMatchesPath(expected, actualItem.value);
|
|
785
|
+
};
|
|
786
|
+
const expectedItem = normalize(expected);
|
|
787
|
+
switch (expectedItem.kind) {
|
|
788
|
+
case "event": return (actual) => {
|
|
789
|
+
const actualItem = normalize(actual);
|
|
790
|
+
return actualItem.kind === "event" && Object.keys(expectedItem.event).every((key) => deepEqual(expectedItem.event[key], actualItem.event[key]));
|
|
791
|
+
};
|
|
792
|
+
case "state": return (actual) => {
|
|
793
|
+
const actualItem = normalize(actual);
|
|
794
|
+
return actualItem.kind === "state" && deepEqual(expectedItem.value, actualItem.value);
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Matches a run's trajectory against an expected one.
|
|
800
|
+
*
|
|
801
|
+
* Both trajectories may be state values or events collected from
|
|
802
|
+
* `onTransition`;
|
|
803
|
+
* items are compared by shape, so `['prompting', 'drafting']` scores a state
|
|
804
|
+
* path and `['PROMPT_SUBMITTED', 'SEND']` scores an event log with the same
|
|
805
|
+
* call.
|
|
806
|
+
*
|
|
807
|
+
* Semantics are an ordered subsequence: every expected item must appear in
|
|
808
|
+
* `actual`, in order, with gaps allowed. Pass `{ exact: true }` to require
|
|
809
|
+
* equality instead.
|
|
810
|
+
*
|
|
811
|
+
* @example Trajectory assertions in a test
|
|
812
|
+
* ```ts
|
|
813
|
+
* const statePath: unknown[] = [];
|
|
814
|
+
* const result = await runAgent(machine, {
|
|
815
|
+
* input,
|
|
816
|
+
* executors,
|
|
817
|
+
* onTransition: (snapshot) => statePath.push(snapshot.value),
|
|
818
|
+
* });
|
|
819
|
+
*
|
|
820
|
+
* const path = matchesTrajectory(statePath, ['prompting', 'drafting', 'sent']);
|
|
821
|
+
* expect(path.matched, JSON.stringify(path.firstMiss)).toBe(true);
|
|
822
|
+
* expect(matchesTrajectory(events, ['PROMPT_SUBMITTED', 'SEND']).matched).toBe(true);
|
|
823
|
+
* ```
|
|
824
|
+
*
|
|
825
|
+
* @example Partial credit in an eval scorer
|
|
826
|
+
* ```ts
|
|
827
|
+
* const match = matchesTrajectory(output.statePath, expected.statePath);
|
|
828
|
+
* return { name: 'state_path', score: match.score, metadata: { ...match.firstMiss } };
|
|
829
|
+
* ```
|
|
830
|
+
*/
|
|
831
|
+
function matchesTrajectory(actual, expected, options = {}) {
|
|
832
|
+
const expectedCount = expected.length;
|
|
833
|
+
if (expectedCount === 0) throw new AgentError("empty-trajectory", "matchesTrajectory: expected trajectory must contain at least one item.");
|
|
834
|
+
const exact = options.exact === true;
|
|
835
|
+
let cursor = 0;
|
|
836
|
+
let matchedCount = 0;
|
|
837
|
+
let firstMiss;
|
|
838
|
+
for (let index = 0; index < expectedCount; index++) {
|
|
839
|
+
const want = expected[index];
|
|
840
|
+
const matches = matcherFor(want);
|
|
841
|
+
const searchedFrom = exact ? index : cursor;
|
|
842
|
+
let found = -1;
|
|
843
|
+
for (let at = searchedFrom; at < actual.length; at++) {
|
|
844
|
+
if (matches(actual[at])) {
|
|
845
|
+
found = at;
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
848
|
+
if (exact) break;
|
|
849
|
+
}
|
|
850
|
+
if (found === -1) {
|
|
851
|
+
firstMiss = {
|
|
852
|
+
index,
|
|
853
|
+
expected: want,
|
|
854
|
+
searchedFrom
|
|
855
|
+
};
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
matchedCount++;
|
|
859
|
+
cursor = found + 1;
|
|
860
|
+
}
|
|
861
|
+
const matched = !firstMiss && (!exact || actual.length === expectedCount);
|
|
862
|
+
const denominator = exact ? Math.max(expectedCount, actual.length) : expectedCount;
|
|
863
|
+
return {
|
|
864
|
+
matched,
|
|
865
|
+
matchedCount,
|
|
866
|
+
expectedCount,
|
|
867
|
+
score: matched ? 1 : denominator === 0 ? 1 : matchedCount / denominator,
|
|
868
|
+
...firstMiss ? { firstMiss } : {}
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
//#endregion
|
|
872
|
+
//#region src/scripted-executors.ts
|
|
873
|
+
/**
|
|
874
|
+
* Scripted executors — a deterministic stand-in for a model host.
|
|
875
|
+
*
|
|
876
|
+
* `createScriptedExecutors` builds a full `{ generateText, streamText, decide }`
|
|
877
|
+
* set that plays back canned answers keyed by
|
|
878
|
+
* request name, so `runAgent` (or `provideExecutors`, or a bare
|
|
879
|
+
* `TextLogic.execute`) runs with no API key and no network.
|
|
880
|
+
*
|
|
881
|
+
* One rule: entries for a name are consumed in order, and the last entry
|
|
882
|
+
* repeats forever.
|
|
883
|
+
*
|
|
884
|
+
* @module
|
|
885
|
+
*/
|
|
886
|
+
function toQueues(value) {
|
|
887
|
+
return new Map(Object.entries(value ?? {}).map(([name, entries]) => [name, {
|
|
888
|
+
entries: Array.isArray(entries) ? [...entries] : [entries],
|
|
889
|
+
index: 0
|
|
890
|
+
}]));
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Takes the next entry for `name`, holding on the last entry once the list is
|
|
894
|
+
* spent. `undefined` means the script has no route for this name at all.
|
|
895
|
+
* @internal
|
|
896
|
+
*/
|
|
897
|
+
function takeEntry(queues, name) {
|
|
898
|
+
const queue = queues.get(name) ?? queues.get("*");
|
|
899
|
+
if (!queue || queue.entries.length === 0) return;
|
|
900
|
+
const entry = queue.entries[Math.min(queue.index, queue.entries.length - 1)];
|
|
901
|
+
queue.index++;
|
|
902
|
+
return entry;
|
|
903
|
+
}
|
|
904
|
+
/** @internal */
|
|
905
|
+
function noAnswer(name, queues) {
|
|
906
|
+
const known = [...queues.keys()].join(", ") || "(none)";
|
|
907
|
+
return /* @__PURE__ */ new Error(`No scripted answer for request '${name}'. Known: ${known}`);
|
|
908
|
+
}
|
|
909
|
+
/** The only own keys an executor result carries. @internal */
|
|
910
|
+
const TEXT_RESULT_KEYS = new Set([
|
|
911
|
+
"result",
|
|
912
|
+
"messages",
|
|
913
|
+
"usage",
|
|
914
|
+
"raw"
|
|
915
|
+
]);
|
|
916
|
+
/**
|
|
917
|
+
* True when a scripted entry is the executor result rather than the output
|
|
918
|
+
* value: it owns a `result` key and owns NOTHING outside the result's own
|
|
919
|
+
* vocabulary. Bare `'result' in value` would swallow an output object's
|
|
920
|
+
* siblings (`{ result: 'draft', confidence: 0.9 }` would lose `confidence`)
|
|
921
|
+
* and would also match an inherited `result`. @internal
|
|
922
|
+
*/
|
|
923
|
+
function isTextResult(value) {
|
|
924
|
+
return Object.hasOwn(value, "result") && Object.keys(value).every((key) => TEXT_RESULT_KEYS.has(key));
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Resolves ONE scripted text entry to an executor result: a function entry is
|
|
928
|
+
* called with the request, and the value is taken as the executor result only
|
|
929
|
+
* when it is one (see {@link isTextResult}). Shared with `runSeam`, whose routed
|
|
930
|
+
* queues follow the same entry conventions. @internal
|
|
931
|
+
*/
|
|
932
|
+
async function resolveScriptedTextEntry(entry, request, info) {
|
|
933
|
+
const value = typeof entry === "function" ? await entry(request, info) : entry;
|
|
934
|
+
return isRecord(value) && isTextResult(value) ? value : { result: value };
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Names a pending text request in an error message. Shared with `runSeam`.
|
|
938
|
+
* @internal
|
|
939
|
+
*/
|
|
940
|
+
function describeText(request) {
|
|
941
|
+
return request.name ? `'${request.name}' (model '${request.model}')` : `(model '${request.model}')`;
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Stream semantics with no model: the whole text lands as one chunk. Shared
|
|
945
|
+
* with `runSeam`, whose scripted answers stream the same way. @internal
|
|
946
|
+
*/
|
|
947
|
+
function emitScriptedChunk(result, info) {
|
|
948
|
+
const text = isRecord(result) ? result["result"] : void 0;
|
|
949
|
+
if (typeof text === "string") info?.onChunk?.(text);
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Executors that replay a script instead of calling a model. Every slot is
|
|
953
|
+
* provided, so any machine binds.
|
|
954
|
+
*
|
|
955
|
+
* Answers are keyed by the request's resolved name — its authored `name`, else
|
|
956
|
+
* the invoke `id` — and `"*"` serves anything unmatched.
|
|
957
|
+
* Entries for a name are consumed in order and the last one repeats forever, so
|
|
958
|
+
* a looping machine needs no extra option. A request the script has no route
|
|
959
|
+
* for throws an ordinary `Error` from inside the executor, which reaches the
|
|
960
|
+
* machine as an actor error.
|
|
961
|
+
*
|
|
962
|
+
* Queues are copied on creation, so the caller's arrays are never mutated and
|
|
963
|
+
* each call builds a fresh, independent playthrough. Entries may be plain
|
|
964
|
+
* values or functions of the request.
|
|
965
|
+
*
|
|
966
|
+
* @example
|
|
967
|
+
* ```ts
|
|
968
|
+
* const result = await runAgent(moderationMachine, {
|
|
969
|
+
* input: { comment: 'honestly this update is terrible', trust: 20 },
|
|
970
|
+
* executors: createScriptedExecutors({
|
|
971
|
+
* decisions: { moderate: [{ type: 'FLAG', reason: 'Borderline tone.' }] },
|
|
972
|
+
* }),
|
|
973
|
+
* });
|
|
974
|
+
* ```
|
|
975
|
+
*
|
|
976
|
+
* @example Dynamic entries
|
|
977
|
+
* ```ts
|
|
978
|
+
* createScriptedExecutors({
|
|
979
|
+
* text: { draft: (request) => `Draft about ${request.prompt}` },
|
|
980
|
+
* decisions: { route: (request) => ({ type: request.events[0]!.type }) },
|
|
981
|
+
* });
|
|
982
|
+
* ```
|
|
983
|
+
*/
|
|
984
|
+
function createScriptedExecutors(script = {}) {
|
|
985
|
+
const decisions = toQueues(script.decisions);
|
|
986
|
+
const text = toQueues(script.text);
|
|
987
|
+
const calls = [];
|
|
988
|
+
const withDefaultUsage = (result) => result.usage || !script.usage ? result : {
|
|
989
|
+
...result,
|
|
990
|
+
usage: script.usage
|
|
991
|
+
};
|
|
992
|
+
const nextText = async (kind, request, info) => {
|
|
993
|
+
const name = request.name ?? "*";
|
|
994
|
+
calls.push({
|
|
995
|
+
kind,
|
|
996
|
+
name,
|
|
997
|
+
input: request.input,
|
|
998
|
+
request
|
|
999
|
+
});
|
|
1000
|
+
const entry = takeEntry(text, name);
|
|
1001
|
+
if (entry === void 0) throw noAnswer(name, text);
|
|
1002
|
+
return withDefaultUsage(await resolveScriptedTextEntry(entry, request, info));
|
|
1003
|
+
};
|
|
1004
|
+
return {
|
|
1005
|
+
calls,
|
|
1006
|
+
generateText: (request, info) => nextText("generateText", request, info),
|
|
1007
|
+
streamText: async (request, info) => {
|
|
1008
|
+
const name = request.name ?? "*";
|
|
1009
|
+
const streamEntry = script.stream?.[name] ?? script.stream?.["*"];
|
|
1010
|
+
if (streamEntry !== void 0) {
|
|
1011
|
+
calls.push({
|
|
1012
|
+
kind: "streamText",
|
|
1013
|
+
name,
|
|
1014
|
+
input: request.input,
|
|
1015
|
+
request
|
|
1016
|
+
});
|
|
1017
|
+
const resolved = typeof streamEntry === "function" ? await streamEntry(request, info) : streamEntry;
|
|
1018
|
+
const chunks = typeof resolved === "string" ? [resolved] : [...resolved];
|
|
1019
|
+
for (const chunk of chunks) info?.onChunk?.(chunk);
|
|
1020
|
+
return withDefaultUsage({ result: chunks.join("") });
|
|
1021
|
+
}
|
|
1022
|
+
const result = await nextText("streamText", request, info);
|
|
1023
|
+
emitScriptedChunk(result, info);
|
|
1024
|
+
return result;
|
|
1025
|
+
},
|
|
1026
|
+
decide: async (request) => {
|
|
1027
|
+
const name = request.name ?? request.id;
|
|
1028
|
+
calls.push({
|
|
1029
|
+
kind: "decide",
|
|
1030
|
+
name,
|
|
1031
|
+
input: request.input,
|
|
1032
|
+
request
|
|
1033
|
+
});
|
|
1034
|
+
const entry = takeEntry(decisions, name);
|
|
1035
|
+
if (entry === void 0) throw noAnswer(name, decisions);
|
|
1036
|
+
const value = typeof entry === "function" ? await entry(request) : entry;
|
|
1037
|
+
return withDefaultUsage(isRecord(value) && typeof value["type"] !== "string" && "event" in value ? value : { event: value });
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
//#endregion
|
|
1042
|
+
//#region src/seam.ts
|
|
1043
|
+
/**
|
|
1044
|
+
* The seam's own answer, for scoring: the executor result's `result`.
|
|
1045
|
+
* @internal
|
|
1046
|
+
*/
|
|
1047
|
+
async function seamOutputOf(result, request) {
|
|
1048
|
+
if (!isRecord(result)) return;
|
|
1049
|
+
return await normalizeGeneratorResult(result, `seam '${request.name ?? request.model}'`);
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Runs a machine end to end with one model call under test, and slices the run
|
|
1053
|
+
* at that call.
|
|
1054
|
+
*
|
|
1055
|
+
* The `before`/`after` slices are the point: `after.statePath` is the branch
|
|
1056
|
+
* the seam chose and `after.events` is the same question against the durable
|
|
1057
|
+
* log, both ready for `matchesTrajectory`. The state slice splits where the
|
|
1058
|
+
* state path stood when the seam answered; the event slice splits at the
|
|
1059
|
+
* seam's own effect completion (the first `xstate.done.*`/`xstate.error.*`
|
|
1060
|
+
* entry appended after the call was made).
|
|
1061
|
+
*
|
|
1062
|
+
* @example No API key: the seam is scripted too, so the whole thing runs offline.
|
|
1063
|
+
* ```ts
|
|
1064
|
+
* const run = await runSeam(emailDrafter, {
|
|
1065
|
+
* scripts: { promptEvaluator: [vague, complete], emailDrafter: [draft] },
|
|
1066
|
+
* seam: { request: 'evaluatePrompt' },
|
|
1067
|
+
* respond: ({ state }) => (state === 'prompting' ? { type: 'PROMPT_SUBMITTED', prompt } : null),
|
|
1068
|
+
* });
|
|
1069
|
+
*
|
|
1070
|
+
* matchesTrajectory(run.after.statePath, ['needsMoreInfo', 'drafting']);
|
|
1071
|
+
* ```
|
|
1072
|
+
*
|
|
1073
|
+
* @example A candidate prompt at the seam, scored against the same rows.
|
|
1074
|
+
* ```ts
|
|
1075
|
+
* const { generateText } = createAiSdkExecutors({ models });
|
|
1076
|
+
* const run = await runSeam(emailDrafter, { ...row, candidate: generateText });
|
|
1077
|
+
* ```
|
|
1078
|
+
*/
|
|
1079
|
+
async function runSeam(machine, options) {
|
|
1080
|
+
const { seam, candidate } = options;
|
|
1081
|
+
const queues = new Map(Object.entries(options.scripts ?? {}).map(([key, answers]) => [key, [...answers]]));
|
|
1082
|
+
const statePath = [];
|
|
1083
|
+
let calls = 0;
|
|
1084
|
+
let seamMatches = 0;
|
|
1085
|
+
let seamOutput;
|
|
1086
|
+
let seamUsage;
|
|
1087
|
+
const callLog = [];
|
|
1088
|
+
let seamReached = false;
|
|
1089
|
+
let callsBeforeSeam = -1;
|
|
1090
|
+
let seamStateAt = 0;
|
|
1091
|
+
let seamEventAt = 0;
|
|
1092
|
+
let liveEvents = 0;
|
|
1093
|
+
const queueKeyOf = (request) => request.name !== void 0 && queues.has(request.name) ? request.name : request.model;
|
|
1094
|
+
/**
|
|
1095
|
+
* Consumes this request's slot in the call plan, or resolves `undefined` when
|
|
1096
|
+
* its queue is dry. With `repeatLast`, the last entry is replayed instead of
|
|
1097
|
+
* running dry.
|
|
1098
|
+
*/
|
|
1099
|
+
const takeScriptedSlot = async (request, info) => {
|
|
1100
|
+
const queue = queues.get(queueKeyOf(request));
|
|
1101
|
+
if (!queue?.length) return;
|
|
1102
|
+
return resolveScriptedTextEntry(options.repeatLast && queue.length === 1 ? queue[0] : queue.shift(), request, info);
|
|
1103
|
+
};
|
|
1104
|
+
const scriptedAnswer = async (request, info) => {
|
|
1105
|
+
const scripted = await takeScriptedSlot(request, info);
|
|
1106
|
+
if (!scripted) throw new AgentError("seam-script-exhausted", `runSeam: no scripted answer left for request ${describeText(request)}. Add an entry to \`scripts.${queueKeyOf(request)}\`, or pass \`repeatLast: true\` to replay its last entry down a longer branch.`);
|
|
1107
|
+
return scripted;
|
|
1108
|
+
};
|
|
1109
|
+
const route = async (request, info) => {
|
|
1110
|
+
const callIndex = calls++;
|
|
1111
|
+
const isSeam = request.name === seam.request && seamMatches++ === (seam.occurrence ?? 0);
|
|
1112
|
+
const ledger = (source) => {
|
|
1113
|
+
callLog.push({
|
|
1114
|
+
...request.name !== void 0 ? { name: request.name } : {},
|
|
1115
|
+
model: request.model,
|
|
1116
|
+
key: queueKeyOf(request),
|
|
1117
|
+
source,
|
|
1118
|
+
seam: isSeam
|
|
1119
|
+
});
|
|
1120
|
+
};
|
|
1121
|
+
if (isSeam && candidate) {
|
|
1122
|
+
await takeScriptedSlot(request, info);
|
|
1123
|
+
seamReached = true;
|
|
1124
|
+
callsBeforeSeam = callIndex;
|
|
1125
|
+
seamStateAt = statePath.length;
|
|
1126
|
+
seamEventAt = liveEvents;
|
|
1127
|
+
const result = await candidate(request, info);
|
|
1128
|
+
seamOutput = await seamOutputOf(result, request);
|
|
1129
|
+
seamUsage = getCallUsage(result);
|
|
1130
|
+
ledger("candidate");
|
|
1131
|
+
return result;
|
|
1132
|
+
}
|
|
1133
|
+
const scripted = await scriptedAnswer(request, info);
|
|
1134
|
+
ledger("script");
|
|
1135
|
+
if (!isSeam) return scripted;
|
|
1136
|
+
seamReached = true;
|
|
1137
|
+
callsBeforeSeam = callIndex;
|
|
1138
|
+
seamStateAt = statePath.length;
|
|
1139
|
+
seamEventAt = liveEvents;
|
|
1140
|
+
seamOutput = scripted.result;
|
|
1141
|
+
seamUsage = scripted.usage;
|
|
1142
|
+
return scripted;
|
|
1143
|
+
};
|
|
1144
|
+
const executors = {
|
|
1145
|
+
...options.executors,
|
|
1146
|
+
generateText: route,
|
|
1147
|
+
streamText: async (request, info) => {
|
|
1148
|
+
const result = await route(request, info);
|
|
1149
|
+
emitScriptedChunk(result, info);
|
|
1150
|
+
return result;
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
let snapshot;
|
|
1154
|
+
let event;
|
|
1155
|
+
const events = [];
|
|
1156
|
+
let result;
|
|
1157
|
+
const maxTurns = options.maxTurns ?? 12;
|
|
1158
|
+
for (let turn = 0; turn <= maxTurns; turn++) {
|
|
1159
|
+
result = await runAgent(machine, {
|
|
1160
|
+
...snapshot ? { snapshot } : { input: options.input },
|
|
1161
|
+
...event ? { event } : {},
|
|
1162
|
+
...options.actors ? { actors: options.actors } : {},
|
|
1163
|
+
executors,
|
|
1164
|
+
onTransition: (next, causedBy) => {
|
|
1165
|
+
if (turn > 0 && causedBy.type === "@xstate.init") return;
|
|
1166
|
+
events.push(causedBy);
|
|
1167
|
+
liveEvents = events.length;
|
|
1168
|
+
statePath.push(next.value);
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
if (result.status !== "idle" || turn === maxTurns) break;
|
|
1172
|
+
const next = options.respond?.({
|
|
1173
|
+
snapshot: result.snapshot,
|
|
1174
|
+
state: result.snapshot.value,
|
|
1175
|
+
meta: getStateMeta(result.snapshot),
|
|
1176
|
+
turn,
|
|
1177
|
+
result
|
|
1178
|
+
});
|
|
1179
|
+
if (!next) break;
|
|
1180
|
+
event = next;
|
|
1181
|
+
snapshot = result.persist();
|
|
1182
|
+
}
|
|
1183
|
+
let splitAt = events.length;
|
|
1184
|
+
if (seamReached) {
|
|
1185
|
+
splitAt = seamEventAt;
|
|
1186
|
+
for (let index = seamEventAt; index < events.length; index++) {
|
|
1187
|
+
const type = events[index].type;
|
|
1188
|
+
if (type.startsWith("xstate.done") || type.startsWith("xstate.error")) {
|
|
1189
|
+
splitAt = index;
|
|
1190
|
+
break;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
const splitStateAt = seamReached ? seamStateAt : statePath.length;
|
|
1195
|
+
return {
|
|
1196
|
+
result,
|
|
1197
|
+
seamOutput,
|
|
1198
|
+
...seamUsage !== void 0 ? { seamUsage } : {},
|
|
1199
|
+
callsBeforeSeam,
|
|
1200
|
+
calls: callLog,
|
|
1201
|
+
before: {
|
|
1202
|
+
statePath: statePath.slice(0, splitStateAt),
|
|
1203
|
+
events: events.slice(0, splitAt)
|
|
1204
|
+
},
|
|
1205
|
+
after: {
|
|
1206
|
+
statePath: statePath.slice(splitStateAt),
|
|
1207
|
+
events: events.slice(splitAt)
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
//#endregion
|
|
1212
|
+
export { AgentLintError, AgentUnknownStateError, assertAgentMachine, canReach, createScriptedExecutors, explorePaths, lintAgentMachine, matchesTrajectory, runSeam, simulateAgent };
|