@statelyai/agent 2.0.0-alpha.28 → 2.0.0-alpha.30
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 +150 -27
- package/dist/ai-sdk.d.cts +59 -36
- package/dist/ai-sdk.d.mts +59 -36
- package/dist/ai-sdk.mjs +148 -24
- package/dist/{decision-RQ0iv0oZ.mjs → decision-BHkvuImD.mjs} +74 -8
- package/dist/{decision-CmM8-HHz.cjs → decision-CIb7ddIv.cjs} +113 -30
- package/dist/{text-logic-f5F5sCdc.cjs → event-log-BZRfBRd7.cjs} +814 -7
- package/dist/{text-logic-CIqHAmBi.mjs → event-log-DJCMHek_.mjs} +680 -4
- package/dist/{event-log-store-BIPOz-di.d.mts → event-log-store-CIIJohjj.d.mts} +90 -5
- package/dist/{event-log-store-2xzFmgr2.d.cts → event-log-store-CLAC9q2Y.d.cts} +90 -5
- package/dist/event-log-store-DZR4IIWe.cjs +95 -0
- package/dist/event-log-store-DsP6GCRh.mjs +84 -0
- package/dist/index.cjs +576 -58
- package/dist/index.d.cts +104 -510
- package/dist/index.d.mts +104 -510
- package/dist/index.mjs +547 -32
- package/dist/log.cjs +77 -20
- package/dist/log.d.cts +5 -5
- package/dist/log.d.mts +5 -5
- package/dist/log.mjs +58 -3
- package/dist/machines.cjs +1 -1
- package/dist/machines.d.cts +15 -15
- package/dist/machines.d.mts +15 -15
- package/dist/machines.mjs +1 -1
- package/dist/model-source-BCKg4qJ1.cjs +44 -0
- package/dist/model-source-BaHwJ1wK.mjs +39 -0
- package/dist/openai.cjs +11 -11
- package/dist/openai.d.cts +28 -26
- package/dist/openai.d.mts +28 -26
- package/dist/openai.mjs +3 -3
- package/dist/otel.cjs +5 -4
- package/dist/otel.d.cts +5 -7
- package/dist/otel.d.mts +5 -7
- package/dist/otel.mjs +5 -3
- package/dist/{run-agent-xNTxpt82.mjs → run-agent-BpHqkaZw.mjs} +181 -44
- package/dist/{run-agent-COXXc0RW.d.mts → run-agent-CAVt5R-r.d.mts} +95 -30
- package/dist/{run-agent-ByO-Bxyd.d.cts → run-agent-CTlMc3Jb.d.cts} +95 -30
- package/dist/{run-agent-Dmf8GV2d.cjs → run-agent-ctLCvNgL.cjs} +230 -88
- package/dist/{setup-agent-BuanDXeh.mjs → setup-agent-7rV_cujV.mjs} +15 -9
- package/dist/setup-agent-CGELn1qZ.d.mts +577 -0
- package/dist/setup-agent-CLPCkXsu.d.cts +577 -0
- package/dist/{setup-agent-DV4cTou-.cjs → setup-agent-DoUk7EdC.cjs} +31 -20
- package/dist/tanstack-ai.cjs +627 -0
- package/dist/tanstack-ai.d.cts +204 -0
- package/dist/tanstack-ai.d.mts +204 -0
- package/dist/tanstack-ai.mjs +618 -0
- package/dist/testing.cjs +204 -985
- package/dist/testing.d.cts +132 -347
- package/dist/testing.d.mts +132 -347
- package/dist/testing.mjs +191 -973
- package/dist/{text-logic-IjfYy9DP.d.cts → text-logic-BRYOPWvY.d.mts} +52 -12
- package/dist/{text-logic-D-czVPYp.d.mts → text-logic-DmciuVUQ.d.cts} +52 -12
- package/dist/{types-BgNbNKnJ.d.mts → types-Dp5_Ru4R.d.cts} +2 -2
- package/dist/{types-hR5f5YXa.d.cts → types-Dp5_Ru4R.d.mts} +2 -2
- package/dist/usage-BkM3z02Y.d.mts +17 -0
- package/dist/usage-Cm_ovz9V.d.cts +17 -0
- package/dist/{utils-_q7F-AZu.d.mts → utils-BnXdWih3.d.mts} +1 -2
- package/dist/{utils-o4YRdHJX.d.cts → utils-ebVpCYoF.d.cts} +1 -2
- package/dist/validate.cjs +239 -8
- package/dist/validate.d.cts +110 -3
- package/dist/validate.d.mts +110 -3
- package/dist/validate.mjs +238 -7
- package/dist/verify-COFWyprJ.mjs +1181 -0
- package/dist/verify-CVlsTZ-V.cjs +1222 -0
- package/dist/verify-Cn86rT8l.d.mts +414 -0
- package/dist/verify-Dzwd4Mvk.d.cts +414 -0
- package/package.json +44 -31
- package/readme.md +40 -28
- package/skills/add-evals/SKILL.md +129 -0
- package/skills/add-traces/SKILL.md +135 -0
- package/skills/generate-machine/SKILL.md +143 -69
- package/skills/graph-engineering/SKILL.md +33 -0
- package/skills/improve-machine/SKILL.md +241 -0
- package/skills/improve-machine/reference.md +101 -0
- package/skills/loop-to-machine/SKILL.md +183 -0
- package/skills/loop-to-machine/policy.md +158 -0
- package/dist/event-log-store-2pAFiWuc.mjs +0 -576
- package/dist/event-log-store-C-COhIXP.cjs +0 -702
- package/dist/usage-BdEjFuIi.mjs +0 -5
- package/dist/usage-efOQPdf_.cjs +0 -10
package/dist/testing.mjs
CHANGED
|
@@ -1,971 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
//#region src/steps.ts
|
|
6
|
-
/**
|
|
7
|
-
* Internal: the pure `(state, event) => (nextState, requests)` view of an
|
|
8
|
-
* agent machine, used by model-free simulation (`simulateAgent`) and tests.
|
|
9
|
-
* An {@link AgentStep} is a snapshot plus the model requests the machine is
|
|
10
|
-
* now waiting on; nothing here executes anything. Hosts use
|
|
11
|
-
* `createAgentRuntime`.
|
|
12
|
-
* @module
|
|
13
|
-
*/
|
|
14
|
-
/** @internal Normalizes current and legacy XState invoke effect shapes. */
|
|
15
|
-
function getInvokeEffectMetadata(action) {
|
|
16
|
-
if (action.type === "@xstate.spawn") return action;
|
|
17
|
-
if (action.type === "xstate.spawnChild") {
|
|
18
|
-
const params = action.params;
|
|
19
|
-
return params ? {
|
|
20
|
-
...params,
|
|
21
|
-
logic: action.logic
|
|
22
|
-
} : void 0;
|
|
23
|
-
}
|
|
24
|
-
if (action.type === "@xstate.start" && typeof action.src === "string") return action;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* The invokes a snapshot is still waiting on, lowered to the same spawn-effect
|
|
28
|
-
* shape {@link getAgentRequests} reads. Derived from the snapshot's live
|
|
29
|
-
* children rather than the last transition's actions, so a state that invokes
|
|
30
|
-
* several actors at once (an `invoke: [...]` array, or parallel regions each
|
|
31
|
-
* invoking) keeps reporting the siblings after the first one settles —
|
|
32
|
-
* including when the settled invoke's `onDone` is targetless and starts
|
|
33
|
-
* nothing new.
|
|
34
|
-
*
|
|
35
|
-
* Entries are ordered by invoke id, so a walker that resolves one invoke per
|
|
36
|
-
* step is deterministic.
|
|
37
|
-
*
|
|
38
|
-
* @internal
|
|
39
|
-
*/
|
|
40
|
-
function getPendingInvokes(snapshot) {
|
|
41
|
-
const children = snapshot.children ?? {};
|
|
42
|
-
return Object.keys(children).sort().flatMap((id) => {
|
|
43
|
-
const child = children[id];
|
|
44
|
-
const childSnapshot = child?.getSnapshot?.();
|
|
45
|
-
if (!child || childSnapshot?.status !== "active") return [];
|
|
46
|
-
return [{
|
|
47
|
-
type: "@xstate.spawn",
|
|
48
|
-
id,
|
|
49
|
-
src: child.src,
|
|
50
|
-
input: childSnapshot.input,
|
|
51
|
-
logic: child.logic
|
|
52
|
-
}];
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Scans a set of executable actions (as returned by xstate's `transition`/
|
|
57
|
-
* `initialTransition`) for spawned `TextLogic`/`DecisionLogic` invokes and
|
|
58
|
-
* lowers each into an {@link AgentStepRequest}. Pass `options.machine` to
|
|
59
|
-
* pre-fill `schemas`/`actors` from the machine's registered `setupAgent`
|
|
60
|
-
* options instead of passing them by hand.
|
|
61
|
-
* `options.snapshot` is required to resolve a decision's candidate events
|
|
62
|
-
* (intersecting declared `allowedEvents` with what's currently legal) — omit
|
|
63
|
-
* it and decision requests report an empty `events` list.
|
|
64
|
-
*
|
|
65
|
-
* @internal
|
|
66
|
-
*/
|
|
67
|
-
function getAgentRequests(actions, { machine, ...options } = {}) {
|
|
68
|
-
if (machine) options = {
|
|
69
|
-
...getRegisteredAgentExecutionOptions(machine),
|
|
70
|
-
...options
|
|
71
|
-
};
|
|
72
|
-
return actions.flatMap((action) => {
|
|
73
|
-
const params = getInvokeEffectMetadata(action);
|
|
74
|
-
if (!params || typeof params.src !== "string") return [];
|
|
75
|
-
if (typeof params.id !== "string" || params.id.length === 0) throw new Error(`Agent invoke '${params.src}' must define a durable string id.`);
|
|
76
|
-
const registeredLogic = isTextLogic(params.logic) || isDecisionLogic(params.logic) ? params.logic : options.actors?.[params.src];
|
|
77
|
-
if (isDecisionLogic(registeredLogic)) {
|
|
78
|
-
const decisionRequest = registeredLogic.request(params.input);
|
|
79
|
-
const allowedEventTypes = registeredLogic.allowedEventTypes?.(params.input);
|
|
80
|
-
const events = options.snapshot ? getAcceptedEvents(options.snapshot, {
|
|
81
|
-
events: options.events,
|
|
82
|
-
schemas: options.schemas,
|
|
83
|
-
eventTypes: allowedEventTypes
|
|
84
|
-
}) : [];
|
|
85
|
-
return [{
|
|
86
|
-
...decisionRequest,
|
|
87
|
-
id: params.id,
|
|
88
|
-
events
|
|
89
|
-
}];
|
|
90
|
-
}
|
|
91
|
-
const textLogic = isTextLogic(registeredLogic) ? registeredLogic : void 0;
|
|
92
|
-
const input = textLogic ? textLogic.request(params.input) : void 0;
|
|
93
|
-
if (!input) return [];
|
|
94
|
-
return [{
|
|
95
|
-
kind: "text",
|
|
96
|
-
id: params.id,
|
|
97
|
-
src: params.src,
|
|
98
|
-
...textLogic ? { mode: textLogic.mode } : {},
|
|
99
|
-
input,
|
|
100
|
-
tools: input.tools ?? {},
|
|
101
|
-
events: []
|
|
102
|
-
}];
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Builds the canonical `xstate.done.actor` event xstate's `transition()`
|
|
107
|
-
* expects to resolve a spawned invoke — including the child session identity
|
|
108
|
-
* that rejects a stale completion from an earlier incarnation.
|
|
109
|
-
*
|
|
110
|
-
* @internal
|
|
111
|
-
*/
|
|
112
|
-
function doneEvent(snapshot, request, output) {
|
|
113
|
-
const id = typeof request === "string" ? request : request.id;
|
|
114
|
-
const child = snapshot.children[id];
|
|
115
|
-
return {
|
|
116
|
-
type: "xstate.done.actor",
|
|
117
|
-
actorId: id,
|
|
118
|
-
...typeof child?.sessionId === "string" ? { sessionId: child.sessionId } : {},
|
|
119
|
-
output
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* The `xstate.error.actor` counterpart of {@link doneEvent} — the event a
|
|
124
|
-
* failed invoke delivers, which `onError` transitions observe. Carries the
|
|
125
|
-
* same child session identity, so xstate does not discard it as stale.
|
|
126
|
-
*
|
|
127
|
-
* @internal
|
|
128
|
-
*/
|
|
129
|
-
function errorEvent(snapshot, request, error) {
|
|
130
|
-
const id = typeof request === "string" ? request : request.id;
|
|
131
|
-
const child = snapshot.children[id];
|
|
132
|
-
return {
|
|
133
|
-
type: "xstate.error.actor",
|
|
134
|
-
actorId: id,
|
|
135
|
-
...typeof child?.sessionId === "string" ? { sessionId: child.sessionId } : {},
|
|
136
|
-
error
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Applies a request's `output` as a done event (or, with `outcome: 'error'`,
|
|
141
|
-
* its `output` as the invoke's error) via `transition(...)`, returning the raw
|
|
142
|
-
* `[snapshot, actions]` tuple. Lower-level than {@link resolveAgentStep} /
|
|
143
|
-
* {@link rejectAgentStep} — those helpers wrap this and also run
|
|
144
|
-
* {@link getAgentRequests} to produce the next {@link AgentStep}.
|
|
145
|
-
*
|
|
146
|
-
* @internal
|
|
147
|
-
*/
|
|
148
|
-
function transitionResult(logic, snapshot, request, output, outcome = "output") {
|
|
149
|
-
const event = outcome === "error" ? errorEvent(snapshot, request, output) : doneEvent(snapshot, request, output);
|
|
150
|
-
const result = transition(logic, snapshot, event);
|
|
151
|
-
applyFinalStateOutput(logic, result[0], event);
|
|
152
|
-
return result;
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Starts a machine and returns its first {@link AgentStep} — the step-path
|
|
156
|
-
* equivalent of `initialTransition` plus request discovery. Begins the
|
|
157
|
-
* durable/per-model-call-checkpoint loop: resolve each `step.requests` entry
|
|
158
|
-
* (via {@link executeAgentRequest} for `kind: 'text'`, or
|
|
159
|
-
* {@link resolveDecision} for `kind: 'decision'`), then advance with
|
|
160
|
-
* {@link resolveAgentStep} or {@link transitionAgentStep}.
|
|
161
|
-
*/
|
|
162
|
-
function initialAgentStep(machine, input, options) {
|
|
163
|
-
const [snapshot, actions] = initialTransition(machine, input);
|
|
164
|
-
return createAgentStep(machine, snapshot, actions, getRegisteredAgentExecutionOptions(machine, options));
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Applies an externally-sent event (e.g. a decision's chosen event, or a
|
|
168
|
-
* human's reply) and returns the next {@link AgentStep}. Accepts **either**
|
|
169
|
-
* a raw snapshot **or** a prior `AgentStep` as the second argument —
|
|
170
|
-
* `.snapshot` is unwrapped automatically, so callers can thread the whole
|
|
171
|
-
* step object through without manually plucking the snapshot out.
|
|
172
|
-
*/
|
|
173
|
-
function transitionAgentStep(machine, snapshotOrStep, event, options) {
|
|
174
|
-
const [nextSnapshot, actions] = transition(machine, isAgentStep(snapshotOrStep) ? snapshotOrStep.snapshot : snapshotOrStep, event);
|
|
175
|
-
return createAgentStep(machine, nextSnapshot, actions, getRegisteredAgentExecutionOptions(machine, options));
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Applies a resolved text request's output (a `kind: 'text'`
|
|
179
|
-
* {@link AgentRequest} — not a decision) as a done event and returns the
|
|
180
|
-
* next {@link AgentStep}. For decisions, resolve with `resolveDecision`
|
|
181
|
-
* (which returns a {@link ChosenEvent}) and apply it with
|
|
182
|
-
* {@link transitionAgentStep} instead — a decision has no output value of
|
|
183
|
-
* its own to feed here.
|
|
184
|
-
*/
|
|
185
|
-
function resolveAgentStep(machine, step, request, output, options) {
|
|
186
|
-
const [snapshot, actions] = transitionResult(machine, step.snapshot, request, output);
|
|
187
|
-
return createAgentStep(machine, snapshot, actions, getRegisteredAgentExecutionOptions(machine, options));
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* Rejects a pending invoke — the failure counterpart of
|
|
191
|
-
* {@link resolveAgentStep}. Delivers `error` as the invoke's
|
|
192
|
-
* `xstate.error.actor` event, so the machine takes that invoke's `onError`
|
|
193
|
-
* transition and returns the next {@link AgentStep}. With no `onError` in
|
|
194
|
-
* scope the returned step's snapshot has `status: 'error'`, exactly as a live
|
|
195
|
-
* run's failed invoke does.
|
|
196
|
-
*
|
|
197
|
-
* A durable host replays a failed call with this: the log says the call
|
|
198
|
-
* failed, so the replay rejects the invoke instead of resolving it. Scripted
|
|
199
|
-
* playthroughs use it too — see `simulateAgent`'s `script.errors`.
|
|
200
|
-
*
|
|
201
|
-
* `error` can be any value. An `Error` instance is the usual one, but a plain
|
|
202
|
-
* object (`{ code: 'truncated' }`) reaches the `onError` transition as
|
|
203
|
-
* `event.error` untouched, so a machine that branches on a failure code is
|
|
204
|
-
* testable without constructing a provider's error class.
|
|
205
|
-
*/
|
|
206
|
-
function rejectAgentStep(machine, step, request, error, options) {
|
|
207
|
-
const [snapshot, actions] = transitionResult(machine, step.snapshot, request, error, "error");
|
|
208
|
-
return createAgentStep(machine, snapshot, actions, getRegisteredAgentExecutionOptions(machine, options));
|
|
209
|
-
}
|
|
210
|
-
function createAgentStep(machine, snapshot, actions, options) {
|
|
211
|
-
applyFinalStateOutput(machine, snapshot);
|
|
212
|
-
return {
|
|
213
|
-
snapshot,
|
|
214
|
-
actions,
|
|
215
|
-
requests: getAgentRequests(getPendingInvokes(snapshot), {
|
|
216
|
-
...options,
|
|
217
|
-
snapshot
|
|
218
|
-
}),
|
|
219
|
-
done: snapshot.status === "done"
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
function resolveStateValueConfig(config, value) {
|
|
223
|
-
if (typeof value === "string") return config.states?.[value];
|
|
224
|
-
if (!value || typeof value !== "object") return;
|
|
225
|
-
for (const [key, childValue] of Object.entries(value)) {
|
|
226
|
-
const childConfig = config.states?.[key];
|
|
227
|
-
if (!childConfig) continue;
|
|
228
|
-
if (childConfig.type === "final") return childConfig;
|
|
229
|
-
const nested = resolveStateValueConfig(childConfig, childValue);
|
|
230
|
-
if (nested) return nested;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
function applyFinalStateOutput(logic, snapshot, event) {
|
|
234
|
-
const machineSnapshot = snapshot;
|
|
235
|
-
if (machineSnapshot.status !== "done" || machineSnapshot.output !== void 0 || !("config" in logic)) return;
|
|
236
|
-
const config = logic.config;
|
|
237
|
-
if (!config) return;
|
|
238
|
-
const output = resolveStateValueConfig(config, machineSnapshot.value)?.output;
|
|
239
|
-
if (output === void 0) return;
|
|
240
|
-
machineSnapshot.output = typeof output === "function" ? output({
|
|
241
|
-
context: machineSnapshot.context,
|
|
242
|
-
event
|
|
243
|
-
}) : output;
|
|
244
|
-
}
|
|
245
|
-
function isAgentStep(value) {
|
|
246
|
-
return !!value && typeof value === "object" && "snapshot" in value && "actions" in value && "requests" in value;
|
|
247
|
-
}
|
|
248
|
-
//#endregion
|
|
249
|
-
//#region src/verify.ts
|
|
250
|
-
const DECIDE_SRC = "agent.decide";
|
|
251
|
-
function normalizeInvokes(invoke) {
|
|
252
|
-
if (invoke === void 0 || invoke === null) return [];
|
|
253
|
-
return Array.isArray(invoke) ? invoke : [invoke];
|
|
254
|
-
}
|
|
255
|
-
function buildStateIndex(rootConfig) {
|
|
256
|
-
const index = /* @__PURE__ */ new Map();
|
|
257
|
-
const walk = (states, parentPath) => {
|
|
258
|
-
for (const [name, config] of Object.entries(states ?? {})) {
|
|
259
|
-
const path = parentPath ? `${parentPath}.${name}` : name;
|
|
260
|
-
const hasChildren = !!config.states && Object.keys(config.states).length > 0;
|
|
261
|
-
index.set(path, {
|
|
262
|
-
path,
|
|
263
|
-
config,
|
|
264
|
-
parentPath,
|
|
265
|
-
isFinal: config.type === "final",
|
|
266
|
-
isParallel: config.type === "parallel",
|
|
267
|
-
isCompound: hasChildren && config.type !== "parallel",
|
|
268
|
-
invokes: normalizeInvokes(config.invoke)
|
|
269
|
-
});
|
|
270
|
-
if (hasChildren) walk(config.states, path);
|
|
271
|
-
}
|
|
272
|
-
};
|
|
273
|
-
walk(rootConfig.states, "");
|
|
274
|
-
return index;
|
|
275
|
-
}
|
|
276
|
-
function ancestorChain(node, index) {
|
|
277
|
-
const chain = [];
|
|
278
|
-
let parent = node.parentPath;
|
|
279
|
-
while (parent) {
|
|
280
|
-
const parentNode = index.get(parent);
|
|
281
|
-
if (!parentNode) break;
|
|
282
|
-
chain.push(parentNode);
|
|
283
|
-
parent = parentNode.parentPath;
|
|
284
|
-
}
|
|
285
|
-
return chain;
|
|
286
|
-
}
|
|
287
|
-
function hasNonEmptyOn(config) {
|
|
288
|
-
return !!config.on && Object.keys(config.on).length > 0;
|
|
289
|
-
}
|
|
290
|
-
function isDecisionInvoke(src, actors) {
|
|
291
|
-
if (typeof src === "string") return src === DECIDE_SRC || isDecisionLogic(actors[src]);
|
|
292
|
-
return isDecisionLogic(src);
|
|
293
|
-
}
|
|
294
|
-
function isAgentLogicNeedingBinding(src) {
|
|
295
|
-
return (isTextLogic(src) || isDecisionLogic(src)) && !executorBoundLogics.has(src);
|
|
296
|
-
}
|
|
297
|
-
function checkDecideWithoutEvents(ctx) {
|
|
298
|
-
const out = [];
|
|
299
|
-
for (const node of ctx.index.values()) for (const invoke of node.invokes) {
|
|
300
|
-
if (!isDecisionInvoke(invoke.src, ctx.actors)) continue;
|
|
301
|
-
const selfHandles = hasNonEmptyOn(node.config);
|
|
302
|
-
const ancestorHandles = ancestorChain(node, ctx.index).some((ancestor) => hasNonEmptyOn(ancestor.config));
|
|
303
|
-
const invokeObserves = invoke.onDone !== void 0;
|
|
304
|
-
if (selfHandles || ancestorHandles || invokeObserves) continue;
|
|
305
|
-
const srcName = typeof invoke.src === "string" ? invoke.src : "(inline logic)";
|
|
306
|
-
out.push({
|
|
307
|
-
code: "decide-without-events",
|
|
308
|
-
severity: "error",
|
|
309
|
-
path: node.path,
|
|
310
|
-
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.`
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
return out;
|
|
314
|
-
}
|
|
315
|
-
function checkDirectObjectSrc(ctx) {
|
|
316
|
-
const out = [];
|
|
317
|
-
for (const node of ctx.index.values()) for (const invoke of node.invokes) {
|
|
318
|
-
const src = invoke.src;
|
|
319
|
-
if (typeof src === "string" || !src || typeof src !== "object") continue;
|
|
320
|
-
if (!isAgentLogicNeedingBinding(src)) continue;
|
|
321
|
-
out.push({
|
|
322
|
-
code: "direct-object-src",
|
|
323
|
-
severity: "warning",
|
|
324
|
-
path: node.path,
|
|
325
|
-
message: `State '${node.path}' invokes a direct-object agent logic. Direct-object invoke srcs cannot be rebound by the agent runtime, 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.`
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
return out;
|
|
329
|
-
}
|
|
330
|
-
function handlesActorError(config) {
|
|
331
|
-
if (config["onError"] !== void 0) return true;
|
|
332
|
-
return Object.keys(config.on ?? {}).some((type) => type === "*" || type.startsWith("xstate.error"));
|
|
333
|
-
}
|
|
334
|
-
function checkInvokeWithoutOnError(ctx) {
|
|
335
|
-
const out = [];
|
|
336
|
-
for (const node of ctx.index.values()) for (const invoke of node.invokes) {
|
|
337
|
-
if (invoke.onError !== void 0 || handlesActorError(node.config)) continue;
|
|
338
|
-
if (handlesActorError(ctx.config) || ancestorChain(node, ctx.index).some((ancestor) => handlesActorError(ancestor.config))) continue;
|
|
339
|
-
const srcName = typeof invoke.src === "string" ? invoke.src : "(inline logic)";
|
|
340
|
-
out.push({
|
|
341
|
-
code: "invoke-without-on-error",
|
|
342
|
-
severity: "warning",
|
|
343
|
-
path: node.path,
|
|
344
|
-
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).`
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
return out;
|
|
348
|
-
}
|
|
349
|
-
const LINT_CHECKS = [
|
|
350
|
-
checkDecideWithoutEvents,
|
|
351
|
-
checkDirectObjectSrc,
|
|
352
|
-
checkInvokeWithoutOnError
|
|
353
|
-
];
|
|
354
|
-
/**
|
|
355
|
-
* Runs static structural checks over a built agent machine and returns the
|
|
356
|
-
* findings ({@link AgentLintDiagnostic}[], empty when clean). Works for
|
|
357
|
-
* TS-authored (`setupAgent(...).createMachine(...)`) and
|
|
358
|
-
* `setupAgent.fromConfig(...)`-compiled machines alike, reading `machine.config`
|
|
359
|
-
* plus the schemas/actor sources the library already retains per machine.
|
|
360
|
-
*
|
|
361
|
-
* No model calls, no API keys — a coding agent that emits an agent machine can
|
|
362
|
-
* call this to catch agent-specific mistakes such as undeliverable decisions,
|
|
363
|
-
* un-rebindable request sources, and an unhandled transcript event. General
|
|
364
|
-
* state-machine lint belongs to XState tooling.
|
|
365
|
-
*
|
|
366
|
-
* Pass `{ throw: true }` for the one-liner form used in tests and generation
|
|
367
|
-
* loops: it returns silently when the machine is clean and throws
|
|
368
|
-
* {@link AgentLintError} (findings on `.diagnostics`) on error-severity
|
|
369
|
-
* findings, or on warnings too with `{ throw: true, warnings: true }`.
|
|
370
|
-
*
|
|
371
|
-
* @example
|
|
372
|
-
* ```ts
|
|
373
|
-
* const errors = lintAgentMachine(machine).filter((d) => d.severity === 'error');
|
|
374
|
-
* if (errors.length) throw new Error(errors.map((e) => `${e.path}: ${e.message}`).join('\n'));
|
|
375
|
-
* ```
|
|
376
|
-
*
|
|
377
|
-
* @example Throwing form
|
|
378
|
-
* ```ts
|
|
379
|
-
* test('agent machine is structurally sound', () => {
|
|
380
|
-
* lintAgentMachine(machine, { throw: true });
|
|
381
|
-
* });
|
|
382
|
-
* ```
|
|
383
|
-
*/
|
|
384
|
-
function lintAgentMachine(machine, options = {}) {
|
|
385
|
-
const config = machine.config ?? {};
|
|
386
|
-
const index = buildStateIndex(config);
|
|
387
|
-
const registered = getRegisteredAgentExecutionOptions(machine);
|
|
388
|
-
const ctx = {
|
|
389
|
-
config,
|
|
390
|
-
index,
|
|
391
|
-
schemas: registered.schemas,
|
|
392
|
-
actors: registered.actors ?? machine.sources?.actors ?? {}
|
|
393
|
-
};
|
|
394
|
-
const disabled = new Set(options.disable ?? []);
|
|
395
|
-
const diagnostics = LINT_CHECKS.flatMap((check) => check(ctx)).filter((d) => !disabled.has(d.code));
|
|
396
|
-
if (options.throw) {
|
|
397
|
-
const failing = options.warnings ? diagnostics : diagnostics.filter((d) => d.severity === "error");
|
|
398
|
-
if (failing.length > 0) throw new AgentLintError(machine.id ?? "(machine)", failing);
|
|
399
|
-
}
|
|
400
|
-
return diagnostics;
|
|
401
|
-
}
|
|
402
|
-
/**
|
|
403
|
-
* Thrown by `lintAgentMachine(machine, { throw: true })` (and its
|
|
404
|
-
* {@link assertAgentMachine} alias) when lint finds failing diagnostics.
|
|
405
|
-
* `diagnostics` holds the findings; the message lists them one per finding,
|
|
406
|
-
* so a test runner's failure output reads like the CLI's lint report.
|
|
407
|
-
*/
|
|
408
|
-
var AgentLintError = class extends AgentError {
|
|
409
|
-
diagnostics;
|
|
410
|
-
constructor(machineId, diagnostics) {
|
|
411
|
-
const lines = diagnostics.map((d) => ` ${d.severity === "error" ? "error" : "warn "} ${d.code} ${d.path}\n ${d.message}`);
|
|
412
|
-
super("lint-failed", `Agent machine '${machineId}' failed lint (${diagnostics.length} finding(s)):\n${lines.join("\n")}`);
|
|
413
|
-
this.name = "AgentLintError";
|
|
414
|
-
this.diagnostics = diagnostics;
|
|
415
|
-
}
|
|
416
|
-
};
|
|
417
|
-
/**
|
|
418
|
-
* Asserts a machine passes {@link lintAgentMachine}: returns silently when
|
|
419
|
-
* clean, throws {@link AgentLintError} (with the findings on `.diagnostics`)
|
|
420
|
-
* otherwise — sugar for `lintAgentMachine(machine, { ...options, throw: true })`.
|
|
421
|
-
* Fails on error-severity findings; set `warnings: true` to fail on warnings
|
|
422
|
-
* too. The one-liner for tests and generation loops:
|
|
423
|
-
*
|
|
424
|
-
* @example
|
|
425
|
-
* ```ts
|
|
426
|
-
* test('agent machine is structurally sound', () => {
|
|
427
|
-
* assertAgentMachine(machine);
|
|
428
|
-
* });
|
|
429
|
-
* ```
|
|
430
|
-
*/
|
|
431
|
-
function assertAgentMachine(machine, options = {}) {
|
|
432
|
-
lintAgentMachine(machine, {
|
|
433
|
-
...options,
|
|
434
|
-
throw: true
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
function pendingInvokes(step) {
|
|
438
|
-
const out = [];
|
|
439
|
-
for (const metadata of getPendingInvokes(step.snapshot)) if (typeof metadata.src === "string") out.push({
|
|
440
|
-
id: metadata.id,
|
|
441
|
-
src: metadata.src
|
|
442
|
-
});
|
|
443
|
-
return out;
|
|
444
|
-
}
|
|
445
|
-
function takeFromQueue(channel, src) {
|
|
446
|
-
const queue = channel?.[src];
|
|
447
|
-
if (queue && queue.length > 0) return {
|
|
448
|
-
found: true,
|
|
449
|
-
value: queue.shift()
|
|
450
|
-
};
|
|
451
|
-
return { found: false };
|
|
452
|
-
}
|
|
453
|
-
/**
|
|
454
|
-
* Deterministically plays a machine through, resolving each request from a
|
|
455
|
-
* {@link SimulationScript} instead of a model — no API keys, no model calls.
|
|
456
|
-
* Runs on the pure step path ({@link initialAgentStep} etc.), so it exercises
|
|
457
|
-
* the real transition logic. Returns the terminal `status`, final `snapshot`,
|
|
458
|
-
* and a `trail` of every step taken.
|
|
459
|
-
*
|
|
460
|
-
* Decisions run through the live run's own validation/retry core,
|
|
461
|
-
* {@link resolveDecision}, with the script standing in for the model: each
|
|
462
|
-
* attempt consumes the next queued {@link ChosenEvent} for that src, so an
|
|
463
|
-
* unknown, payload-invalid, or guard-rejected event is NOT silently swallowed
|
|
464
|
-
* — the next queued decision is tried, exactly as a live run re-asks the
|
|
465
|
-
* model. The decision logic's `maxRetries` caps attempts as it would live;
|
|
466
|
-
* when retries continue past the end of the queue, the last queued decision
|
|
467
|
-
* repeats (a scripted model that insists). The repeat applies only within one
|
|
468
|
-
* decision request's retries — each new decision request must have its own
|
|
469
|
-
* queued entry, or the dry-script error throws as usual. Exhausting all
|
|
470
|
-
* attempts delivers
|
|
471
|
-
* the resulting {@link AgentDecisionExhaustedError} to the machine as the
|
|
472
|
-
* decision invoke's error (so an `onError` transition observes it, as it
|
|
473
|
-
* would live); with no `onError` to catch it, it is thrown.
|
|
474
|
-
*
|
|
475
|
-
* Throws a descriptive error when the script runs dry mid-request, naming the
|
|
476
|
-
* pending request's kind, src, and id so the missing scripted response is
|
|
477
|
-
* obvious.
|
|
478
|
-
*
|
|
479
|
-
* @example
|
|
480
|
-
* ```ts
|
|
481
|
-
* const { status, snapshot } = simulateAgent(machine, {
|
|
482
|
-
* input: { topic: 'state machines' },
|
|
483
|
-
* script: {
|
|
484
|
-
* decisions: { 'agent.decide': [{ type: 'ESCALATE' }] },
|
|
485
|
-
* events: [{ type: 'APPROVE' }], // crosses the human gate
|
|
486
|
-
* },
|
|
487
|
-
* });
|
|
488
|
-
* ```
|
|
489
|
-
*/
|
|
490
|
-
async function simulateAgent(machine, options) {
|
|
491
|
-
const maxSteps = options.maxSteps ?? 100;
|
|
492
|
-
const script = {
|
|
493
|
-
text: mapValues(options.script.text ?? {}, (arr) => [...arr]),
|
|
494
|
-
decisions: mapValues(options.script.decisions ?? {}, (arr) => [...arr]),
|
|
495
|
-
invokes: mapValues(options.script.invokes ?? {}, (arr) => [...arr]),
|
|
496
|
-
errors: mapValues(options.script.errors ?? {}, (arr) => [...arr]),
|
|
497
|
-
events: [...options.script.events ?? []]
|
|
498
|
-
};
|
|
499
|
-
let step = initialAgentStep(machine, options.input);
|
|
500
|
-
const trail = [{ state: step.snapshot.value }];
|
|
501
|
-
for (let i = 0; i < maxSteps; i++) {
|
|
502
|
-
if (step.done) return {
|
|
503
|
-
status: "done",
|
|
504
|
-
snapshot: step.snapshot,
|
|
505
|
-
trail
|
|
506
|
-
};
|
|
507
|
-
const request = step.requests[0];
|
|
508
|
-
if (request) {
|
|
509
|
-
if (request.kind === "decision") {
|
|
510
|
-
const invokeMeta = findInvokeMetadata(step, request.id);
|
|
511
|
-
const src = invokeMeta?.src ?? request.id;
|
|
512
|
-
const decisionSrc = src in (script.decisions ?? {}) ? src : request.id in (script.decisions ?? {}) ? request.id : src;
|
|
513
|
-
const errorSrc = queueKey(script.errors, src, request.id);
|
|
514
|
-
const failure = takeFromQueue(script.errors, errorSrc);
|
|
515
|
-
if (failure.found) {
|
|
516
|
-
step = rejectScriptedRequest(machine, step, request.id, failure.value, {
|
|
517
|
-
kind: "decision",
|
|
518
|
-
src: errorSrc,
|
|
519
|
-
trail
|
|
520
|
-
});
|
|
521
|
-
continue;
|
|
522
|
-
}
|
|
523
|
-
step = await applyScriptedDecision(machine, step, request, decisionSrc, invokeMeta?.logic, script, trail);
|
|
524
|
-
continue;
|
|
525
|
-
}
|
|
526
|
-
const failure = takeFromQueue(script.errors, request.src);
|
|
527
|
-
if (failure.found) {
|
|
528
|
-
step = rejectScriptedRequest(machine, step, request, failure.value, {
|
|
529
|
-
kind: "text",
|
|
530
|
-
src: request.src,
|
|
531
|
-
trail
|
|
532
|
-
});
|
|
533
|
-
continue;
|
|
534
|
-
}
|
|
535
|
-
const taken = takeFromQueue(script.text, request.src);
|
|
536
|
-
if (!taken.found) throw scriptDryError("text", request.src, request.id);
|
|
537
|
-
step = resolveAgentStep(machine, step, request, asTextResult(taken.value));
|
|
538
|
-
trail.push({
|
|
539
|
-
state: step.snapshot.value,
|
|
540
|
-
resolvedRequest: {
|
|
541
|
-
kind: "text",
|
|
542
|
-
src: request.src,
|
|
543
|
-
id: request.id,
|
|
544
|
-
outcome: "output"
|
|
545
|
-
}
|
|
546
|
-
});
|
|
547
|
-
continue;
|
|
548
|
-
}
|
|
549
|
-
const [invoke] = pendingInvokes(step);
|
|
550
|
-
if (invoke) {
|
|
551
|
-
const failure = takeFromQueue(script.errors, invoke.src);
|
|
552
|
-
if (failure.found) {
|
|
553
|
-
step = rejectScriptedRequest(machine, step, invoke.id, failure.value, {
|
|
554
|
-
kind: "invoke",
|
|
555
|
-
src: invoke.src,
|
|
556
|
-
trail
|
|
557
|
-
});
|
|
558
|
-
continue;
|
|
559
|
-
}
|
|
560
|
-
const taken = takeFromQueue(script.invokes, invoke.src);
|
|
561
|
-
if (!taken.found) throw scriptDryError("invoke", invoke.src, invoke.id);
|
|
562
|
-
step = resolveAgentStep(machine, step, invoke.id, taken.value);
|
|
563
|
-
trail.push({
|
|
564
|
-
state: step.snapshot.value,
|
|
565
|
-
resolvedRequest: {
|
|
566
|
-
kind: "invoke",
|
|
567
|
-
src: invoke.src,
|
|
568
|
-
id: invoke.id,
|
|
569
|
-
outcome: "output"
|
|
570
|
-
}
|
|
571
|
-
});
|
|
572
|
-
continue;
|
|
573
|
-
}
|
|
574
|
-
const external = script.events;
|
|
575
|
-
if (external.length > 0) {
|
|
576
|
-
const event = external.shift();
|
|
577
|
-
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.`);
|
|
578
|
-
step = transitionAgentStep(machine, step, event);
|
|
579
|
-
trail.push({
|
|
580
|
-
state: step.snapshot.value,
|
|
581
|
-
appliedEvent: event,
|
|
582
|
-
external: true
|
|
583
|
-
});
|
|
584
|
-
continue;
|
|
585
|
-
}
|
|
586
|
-
return {
|
|
587
|
-
status: "idle",
|
|
588
|
-
snapshot: step.snapshot,
|
|
589
|
-
trail
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
return {
|
|
593
|
-
status: "exhausted",
|
|
594
|
-
snapshot: step.snapshot,
|
|
595
|
-
trail
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
function queueKey(channel, src, id) {
|
|
599
|
-
return channel && !(src in channel) && id in channel ? id : src;
|
|
600
|
-
}
|
|
601
|
-
function rejectScriptedRequest(machine, step, request, error, entry) {
|
|
602
|
-
const id = typeof request === "string" ? request : request.id;
|
|
603
|
-
const next = rejectAgentStep(machine, step, request, error);
|
|
604
|
-
if (next.snapshot.status === "error") throw unhandledScriptedError(error, entry.kind, entry.src, id);
|
|
605
|
-
entry.trail.push({
|
|
606
|
-
state: next.snapshot.value,
|
|
607
|
-
resolvedRequest: {
|
|
608
|
-
kind: entry.kind,
|
|
609
|
-
src: entry.src,
|
|
610
|
-
id,
|
|
611
|
-
outcome: "error",
|
|
612
|
-
error
|
|
613
|
-
}
|
|
614
|
-
});
|
|
615
|
-
return next;
|
|
616
|
-
}
|
|
617
|
-
function unhandledScriptedError(error, kind, src, id) {
|
|
618
|
-
if (error instanceof Error) return error;
|
|
619
|
-
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 });
|
|
620
|
-
}
|
|
621
|
-
async function applyScriptedDecision(machine, step, request, decisionSrc, invokeLogic, script, trail) {
|
|
622
|
-
const dequeued = [];
|
|
623
|
-
const decide = async () => {
|
|
624
|
-
const taken = takeFromQueue(script.decisions, decisionSrc);
|
|
625
|
-
if (taken.found) {
|
|
626
|
-
dequeued.push(taken.value);
|
|
627
|
-
return { event: taken.value };
|
|
628
|
-
}
|
|
629
|
-
if (dequeued.length === 0) throw scriptDryError("decision", decisionSrc, request.id, request);
|
|
630
|
-
return { event: dequeued[dequeued.length - 1] };
|
|
631
|
-
};
|
|
632
|
-
const maxRetries = (isDecisionLogic(invokeLogic) ? invokeLogic : resolveRegisteredDecisionLogic(machine, decisionSrc))?.maxRetries;
|
|
633
|
-
let chosen;
|
|
634
|
-
try {
|
|
635
|
-
chosen = await resolveDecision(request, { decide }, {
|
|
636
|
-
maxRetries,
|
|
637
|
-
canTake: (event) => step.snapshot.can(event)
|
|
638
|
-
});
|
|
639
|
-
} catch (error) {
|
|
640
|
-
if (!(error instanceof AgentDecisionExhaustedError)) throw error;
|
|
641
|
-
const next = rejectAgentStep(machine, step, request.id, error);
|
|
642
|
-
if (next.snapshot.status === "error") throw error;
|
|
643
|
-
trail.push({
|
|
644
|
-
state: next.snapshot.value,
|
|
645
|
-
rejectedEvents: dequeued
|
|
646
|
-
});
|
|
647
|
-
return next;
|
|
648
|
-
}
|
|
649
|
-
const next = transitionAgentStep(machine, step, chosen);
|
|
650
|
-
const rejected = dequeued.slice(0, -1);
|
|
651
|
-
trail.push({
|
|
652
|
-
state: next.snapshot.value,
|
|
653
|
-
appliedEvent: chosen,
|
|
654
|
-
...rejected.length > 0 ? { rejectedEvents: rejected } : {}
|
|
655
|
-
});
|
|
656
|
-
return next;
|
|
657
|
-
}
|
|
658
|
-
function findInvokeMetadata(step, id) {
|
|
659
|
-
for (const metadata of getPendingInvokes(step.snapshot)) {
|
|
660
|
-
if (metadata.id !== id) continue;
|
|
661
|
-
return {
|
|
662
|
-
...typeof metadata.src === "string" ? { src: metadata.src } : {},
|
|
663
|
-
logic: metadata.logic ?? (typeof metadata.src === "object" ? metadata.src : void 0)
|
|
664
|
-
};
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
function resolveRegisteredDecisionLogic(machine, src) {
|
|
668
|
-
const candidate = getRegisteredAgentExecutionOptions(machine).actors?.[src] ?? machine.sources?.actors?.[src];
|
|
669
|
-
return isDecisionLogic(candidate) ? candidate : void 0;
|
|
670
|
-
}
|
|
671
|
-
function asTextResult(value) {
|
|
672
|
-
return {
|
|
673
|
-
result: value,
|
|
674
|
-
messages: []
|
|
675
|
-
};
|
|
676
|
-
}
|
|
677
|
-
function mapValues(obj, fn) {
|
|
678
|
-
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, fn(value)]));
|
|
679
|
-
}
|
|
680
|
-
function scriptDryError(kind, src, id, request) {
|
|
681
|
-
const events = request?.kind === "decision" ? ` Candidate events: ${request.events.map((e) => e.type).join(", ") || "(none)"}.` : "";
|
|
682
|
-
const key = kind === "text" ? `text['${src}']` : kind === "decision" ? `decisions['${src}']` : `invokes['${src}']`;
|
|
683
|
-
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}`);
|
|
684
|
-
}
|
|
685
|
-
const MAX_ADVANCE_STEPS = 1e3;
|
|
686
|
-
function invokeFork(id, src, outputs) {
|
|
687
|
-
return src in outputs ? {
|
|
688
|
-
id,
|
|
689
|
-
src,
|
|
690
|
-
hasOutput: true,
|
|
691
|
-
output: outputs[src]
|
|
692
|
-
} : {
|
|
693
|
-
id,
|
|
694
|
-
src,
|
|
695
|
-
hasOutput: false
|
|
696
|
-
};
|
|
697
|
-
}
|
|
698
|
-
async function explore(machine, options, stopWhen) {
|
|
699
|
-
const maxDepth = options.maxDepth ?? 8;
|
|
700
|
-
const maxPaths = options.maxPaths ?? 200;
|
|
701
|
-
const textScript = options.text ?? {};
|
|
702
|
-
const invokeOutputs = { ...options.invokes };
|
|
703
|
-
const errorScript = options.errors ?? {};
|
|
704
|
-
const reachedStates = /* @__PURE__ */ new Set();
|
|
705
|
-
const reachedValues = [];
|
|
706
|
-
const terminals = [];
|
|
707
|
-
const unexplored = [];
|
|
708
|
-
let prunedByGuard = 0;
|
|
709
|
-
let pathsExplored = 0;
|
|
710
|
-
let hitPathCap = false;
|
|
711
|
-
let witness;
|
|
712
|
-
const recordState = (snapshot) => {
|
|
713
|
-
const key = JSON.stringify(snapshot.value);
|
|
714
|
-
if (!reachedStates.has(key)) {
|
|
715
|
-
reachedStates.add(key);
|
|
716
|
-
reachedValues.push(snapshot.value);
|
|
717
|
-
}
|
|
718
|
-
};
|
|
719
|
-
const initial = initialAgentStep(machine, options.input);
|
|
720
|
-
recordState(initial.snapshot);
|
|
721
|
-
if (stopWhen?.(initial.snapshot)) witness = [];
|
|
722
|
-
const advance = (step) => {
|
|
723
|
-
let current = step;
|
|
724
|
-
for (let i = 0; i < MAX_ADVANCE_STEPS; i++) {
|
|
725
|
-
if (stopWhen?.(current.snapshot)) return {
|
|
726
|
-
step: current,
|
|
727
|
-
hit: true
|
|
728
|
-
};
|
|
729
|
-
if (current.done) return { step: current };
|
|
730
|
-
const request = current.requests[0];
|
|
731
|
-
if (request && request.kind === "text") {
|
|
732
|
-
if (request.src in errorScript) return {
|
|
733
|
-
step: current,
|
|
734
|
-
fork: invokeFork(request.id, request.src, textScript)
|
|
735
|
-
};
|
|
736
|
-
if (!(request.src in textScript)) return {
|
|
737
|
-
step: current,
|
|
738
|
-
blockedSrc: request.src
|
|
739
|
-
};
|
|
740
|
-
current = resolveAgentStep(machine, current, request, asTextResult(textScript[request.src]));
|
|
741
|
-
recordState(current.snapshot);
|
|
742
|
-
continue;
|
|
743
|
-
}
|
|
744
|
-
if (request && request.kind === "decision") {
|
|
745
|
-
const src = findInvokeMetadata(current, request.id)?.src;
|
|
746
|
-
const errorSrc = src !== void 0 && src in errorScript ? src : request.id in errorScript ? request.id : void 0;
|
|
747
|
-
return errorSrc !== void 0 ? {
|
|
748
|
-
step: current,
|
|
749
|
-
fork: {
|
|
750
|
-
id: request.id,
|
|
751
|
-
src: errorSrc,
|
|
752
|
-
hasOutput: false,
|
|
753
|
-
isDecision: true
|
|
754
|
-
}
|
|
755
|
-
} : { step: current };
|
|
756
|
-
}
|
|
757
|
-
const [invoke] = pendingInvokes(current);
|
|
758
|
-
if (invoke) {
|
|
759
|
-
if (invoke.src in errorScript) return {
|
|
760
|
-
step: current,
|
|
761
|
-
fork: invokeFork(invoke.id, invoke.src, invokeOutputs)
|
|
762
|
-
};
|
|
763
|
-
if (!(invoke.src in invokeOutputs)) return {
|
|
764
|
-
step: current,
|
|
765
|
-
blockedSrc: invoke.src
|
|
766
|
-
};
|
|
767
|
-
current = resolveAgentStep(machine, current, invoke.id, invokeOutputs[invoke.src]);
|
|
768
|
-
recordState(current.snapshot);
|
|
769
|
-
continue;
|
|
770
|
-
}
|
|
771
|
-
return { step: current };
|
|
772
|
-
}
|
|
773
|
-
return { step: current };
|
|
774
|
-
};
|
|
775
|
-
const visit = async (step, path, startDepth) => {
|
|
776
|
-
let depth = startDepth;
|
|
777
|
-
if (witness !== void 0) return;
|
|
778
|
-
if (pathsExplored >= maxPaths) {
|
|
779
|
-
hitPathCap = true;
|
|
780
|
-
return;
|
|
781
|
-
}
|
|
782
|
-
const { step: settled, blockedSrc, hit, fork } = advance(step);
|
|
783
|
-
if (hit) {
|
|
784
|
-
witness = path;
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
if (fork) {
|
|
788
|
-
if (depth >= maxDepth) {
|
|
789
|
-
pathsExplored++;
|
|
790
|
-
terminals.push({
|
|
791
|
-
status: "max-depth",
|
|
792
|
-
path,
|
|
793
|
-
state: settled.snapshot.value
|
|
794
|
-
});
|
|
795
|
-
unexplored.push(`max-depth: stopped at path [${path.map((e) => e.type).join(", ")}]`);
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
const failed = rejectAgentStep(machine, settled, fork.id, errorScript[fork.src]);
|
|
799
|
-
recordState(failed.snapshot);
|
|
800
|
-
await visit(failed, [...path, { type: `xstate.error.actor.${fork.id}` }], depth + 1);
|
|
801
|
-
if (!fork.isDecision) {
|
|
802
|
-
if (fork.hasOutput) {
|
|
803
|
-
const resolved = resolveAgentStep(machine, settled, fork.id, fork.output);
|
|
804
|
-
recordState(resolved.snapshot);
|
|
805
|
-
await visit(resolved, path, depth + 1);
|
|
806
|
-
}
|
|
807
|
-
return;
|
|
808
|
-
}
|
|
809
|
-
depth += 1;
|
|
810
|
-
if (witness !== void 0) return;
|
|
811
|
-
}
|
|
812
|
-
if (blockedSrc) {
|
|
813
|
-
pathsExplored++;
|
|
814
|
-
terminals.push({
|
|
815
|
-
status: "needs-output",
|
|
816
|
-
path,
|
|
817
|
-
state: settled.snapshot.value,
|
|
818
|
-
missingSrc: blockedSrc
|
|
819
|
-
});
|
|
820
|
-
unexplored.push(`needs-output: no canned output for src '${blockedSrc}' at path [${path.map((e) => e.type).join(", ")}]`);
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
if (settled.done) {
|
|
824
|
-
pathsExplored++;
|
|
825
|
-
terminals.push({
|
|
826
|
-
status: "done",
|
|
827
|
-
path,
|
|
828
|
-
state: settled.snapshot.value
|
|
829
|
-
});
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
if (settled.snapshot.status === "error") {
|
|
833
|
-
pathsExplored++;
|
|
834
|
-
terminals.push({
|
|
835
|
-
status: "error",
|
|
836
|
-
path,
|
|
837
|
-
state: settled.snapshot.value,
|
|
838
|
-
error: settled.snapshot.error
|
|
839
|
-
});
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
|
-
const request = settled.requests[0];
|
|
843
|
-
const branchEvents = request?.kind === "decision" ? request.events.map((descriptor) => ({ type: descriptor.type })) : getAcceptedEvents(settled.snapshot).map((descriptor) => ({ type: descriptor.type }));
|
|
844
|
-
if (branchEvents.length === 0) {
|
|
845
|
-
pathsExplored++;
|
|
846
|
-
terminals.push({
|
|
847
|
-
status: "idle",
|
|
848
|
-
path,
|
|
849
|
-
state: settled.snapshot.value
|
|
850
|
-
});
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
if (depth >= maxDepth) {
|
|
854
|
-
pathsExplored++;
|
|
855
|
-
terminals.push({
|
|
856
|
-
status: "max-depth",
|
|
857
|
-
path,
|
|
858
|
-
state: settled.snapshot.value
|
|
859
|
-
});
|
|
860
|
-
unexplored.push(`max-depth: stopped at path [${path.map((e) => e.type).join(", ")}]`);
|
|
861
|
-
return;
|
|
862
|
-
}
|
|
863
|
-
for (const event of branchEvents) {
|
|
864
|
-
if (witness !== void 0 || pathsExplored >= maxPaths) {
|
|
865
|
-
if (pathsExplored >= maxPaths) hitPathCap = true;
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
if (!settled.snapshot.can(event)) {
|
|
869
|
-
prunedByGuard++;
|
|
870
|
-
continue;
|
|
871
|
-
}
|
|
872
|
-
const next = transitionAgentStep(machine, settled, event);
|
|
873
|
-
recordState(next.snapshot);
|
|
874
|
-
await visit(next, [...path, event], depth + 1);
|
|
875
|
-
}
|
|
876
|
-
};
|
|
877
|
-
if (witness === void 0) await visit(initial, [], 0);
|
|
878
|
-
return {
|
|
879
|
-
report: {
|
|
880
|
-
reachedStates: reachedValues,
|
|
881
|
-
terminals,
|
|
882
|
-
prunedByGuard,
|
|
883
|
-
unexplored,
|
|
884
|
-
pathsExplored,
|
|
885
|
-
hitPathCap
|
|
886
|
-
},
|
|
887
|
-
witness
|
|
888
|
-
};
|
|
889
|
-
}
|
|
890
|
-
/**
|
|
891
|
-
* Enumerates a machine's decision and external-event branches to a bounded
|
|
892
|
-
* depth, model-free, and reports which states are reached and how each path
|
|
893
|
-
* terminates. At each decision request it forks one branch per candidate event
|
|
894
|
-
* (guard-rejected candidates are counted in `prunedByGuard`, not explored); at
|
|
895
|
-
* an idle wait it forks per externally-accepted event. Text requests resolve
|
|
896
|
-
* from `text`, other invokes from `invokes` — all by-src canned-output maps, and a missing src halts
|
|
897
|
-
* that branch with a `needs-output` terminal rather than throwing. A src in
|
|
898
|
-
* `errors` forks an extra branch where that invoke is rejected, so states
|
|
899
|
-
* behind an `onError` are explored too.
|
|
900
|
-
*
|
|
901
|
-
* Combinatorics are bounded by `maxDepth` (default 8) and `maxPaths` (default
|
|
902
|
-
* 200, reported via `hitPathCap`).
|
|
903
|
-
*
|
|
904
|
-
* @example
|
|
905
|
-
* ```ts
|
|
906
|
-
* const report = await explorePaths(refundMachine, { input: { request: 'x', amount: 5000 } });
|
|
907
|
-
* // report.terminals → both 'refunded' and 'denied'; report.prunedByGuard → 1
|
|
908
|
-
* ```
|
|
909
|
-
*/
|
|
910
|
-
async function explorePaths(machine, options = {}) {
|
|
911
|
-
return (await explore(machine, options)).report;
|
|
912
|
-
}
|
|
913
|
-
/** Thrown when a string reachability target is not a state in the machine. */
|
|
914
|
-
var AgentUnknownStateError = class extends AgentError {
|
|
915
|
-
target;
|
|
916
|
-
constructor(machineId, target) {
|
|
917
|
-
super("unknown-state", `canReach: machine '${machineId}' has no state matching '${target}'.`);
|
|
918
|
-
this.name = "AgentUnknownStateError";
|
|
919
|
-
this.target = target;
|
|
920
|
-
}
|
|
921
|
-
};
|
|
922
|
-
function resolveStateTarget(machine, target) {
|
|
923
|
-
const stack = [machine.root];
|
|
924
|
-
while (stack.length > 0) {
|
|
925
|
-
const node = stack.pop();
|
|
926
|
-
if (node.id === target.replace(/^#/, "") || node.path.join(".") === target) return node.id;
|
|
927
|
-
stack.push(...Object.values(node.states));
|
|
928
|
-
}
|
|
929
|
-
throw new AgentUnknownStateError(machine.id, target);
|
|
930
|
-
}
|
|
931
|
-
/**
|
|
932
|
-
* Answers "can the machine reach this?" by exploring its branches (a thin
|
|
933
|
-
* wrapper over {@link explorePaths}). The target is either a state path string
|
|
934
|
-
* (`snapshot.matches(...)` semantics) or a snapshot predicate — the predicate
|
|
935
|
-
* form checks any property (a context invariant, a tag, a state+context
|
|
936
|
-
* combination) without reifying a sentinel state for it. Returns
|
|
937
|
-
* `{ reachable: true, witness }` with the event sequence that reaches it, or
|
|
938
|
-
* `{ reachable: false }`.
|
|
939
|
-
*
|
|
940
|
-
* @example
|
|
941
|
-
* ```ts
|
|
942
|
-
* const { reachable, witness } = await canReach(refundMachine, 'denied', { input: { request: 'x', amount: 5000 } });
|
|
943
|
-
* // reachable → true; witness → [{ type: 'NEEDS_REVIEW' }, { type: 'DENY' }]
|
|
944
|
-
* ```
|
|
945
|
-
*
|
|
946
|
-
* @example Predicate target — a violation property, no sentinel state needed
|
|
947
|
-
* ```ts
|
|
948
|
-
* const violation = await canReach(
|
|
949
|
-
* refundMachine,
|
|
950
|
-
* (snapshot) => snapshot.matches('issued') && !snapshot.context.approved,
|
|
951
|
-
* { input: { amount: 5000 } },
|
|
952
|
-
* );
|
|
953
|
-
* // violation.reachable → false is the safety proof
|
|
954
|
-
* ```
|
|
955
|
-
*/
|
|
956
|
-
async function canReach(machine, target, options = {}) {
|
|
957
|
-
const resolvedTarget = typeof target === "string" ? resolveStateTarget(machine, target) : void 0;
|
|
958
|
-
const { witness } = await explore(machine, options, typeof target === "function" ? target : (snapshot) => snapshot.nodes.some((node) => node.id === resolvedTarget));
|
|
959
|
-
return witness !== void 0 ? {
|
|
960
|
-
reachable: true,
|
|
961
|
-
witness,
|
|
962
|
-
...resolvedTarget ? { target: resolvedTarget } : {}
|
|
963
|
-
} : {
|
|
964
|
-
reachable: false,
|
|
965
|
-
...resolvedTarget ? { target: resolvedTarget } : {}
|
|
966
|
-
};
|
|
967
|
-
}
|
|
968
|
-
//#endregion
|
|
1
|
+
import { $ as getStateMeta, F as normalizeGeneratorResult, N as getCallUsage, st as AgentError } from "./event-log-DJCMHek_.mjs";
|
|
2
|
+
import { m as isRecord } from "./decision-BHkvuImD.mjs";
|
|
3
|
+
import { p as runToQuiescence, s as createAgentRuntime } from "./run-agent-BpHqkaZw.mjs";
|
|
4
|
+
import { a as explorePaths, i as canReach, n as AgentUnknownStateError, o as lintAgentMachine, r as assertAgentMachine, s as simulateAgent, t as AgentLintError } from "./verify-COFWyprJ.mjs";
|
|
969
5
|
//#region src/trajectory.ts
|
|
970
6
|
/**
|
|
971
7
|
* Classifies one item. Event-shaped things (log entries, events) become events;
|
|
@@ -994,17 +30,17 @@ function normalize(item) {
|
|
|
994
30
|
};
|
|
995
31
|
}
|
|
996
32
|
/** Every leaf of a state value as a dot path (`{ review: 'editing' }` → `review.editing`). @internal */
|
|
997
|
-
function statePaths(value) {
|
|
33
|
+
function statePaths$1(value) {
|
|
998
34
|
if (typeof value === "string") return [value];
|
|
999
35
|
if (!isRecord(value)) return [];
|
|
1000
|
-
return Object.entries(value).flatMap(([key, child]) => statePaths(child).map((path) => `${key}.${path}`));
|
|
36
|
+
return Object.entries(value).flatMap(([key, child]) => statePaths$1(child).map((path) => `${key}.${path}`));
|
|
1001
37
|
}
|
|
1002
38
|
/**
|
|
1003
39
|
* A string expectation matches a state value when it names one of its leaves or
|
|
1004
40
|
* an ancestor of one, so `'review'` matches `{ review: 'editing' }`. @internal
|
|
1005
41
|
*/
|
|
1006
42
|
function stateMatchesPath(path, value) {
|
|
1007
|
-
return statePaths(value).some((leaf) => leaf === path || leaf.startsWith(`${path}.`));
|
|
43
|
+
return statePaths$1(value).some((leaf) => leaf === path || leaf.startsWith(`${path}.`));
|
|
1008
44
|
}
|
|
1009
45
|
function deepEqual(a, b) {
|
|
1010
46
|
if (Object.is(a, b)) return true;
|
|
@@ -1118,7 +154,7 @@ function matchesTrajectory(actual, expected, options = {}) {
|
|
|
1118
154
|
//#endregion
|
|
1119
155
|
//#region src/seam.ts
|
|
1120
156
|
/** The only own keys an executor result carries. @internal */
|
|
1121
|
-
const TEXT_RESULT_KEYS = new Set([
|
|
157
|
+
const TEXT_RESULT_KEYS = /* @__PURE__ */ new Set([
|
|
1122
158
|
"result",
|
|
1123
159
|
"messages",
|
|
1124
160
|
"usage",
|
|
@@ -1324,4 +360,186 @@ async function runSeam(machine, options) {
|
|
|
1324
360
|
};
|
|
1325
361
|
}
|
|
1326
362
|
//#endregion
|
|
1327
|
-
|
|
363
|
+
//#region src/fold-traces.ts
|
|
364
|
+
/**
|
|
365
|
+
* Folds many runs' traces into per-state stats: how many runs visited each
|
|
366
|
+
* state, how many of those failed, and how many options the guards withheld
|
|
367
|
+
* at decisions made there. Pure; reads only:
|
|
368
|
+
*
|
|
369
|
+
* - `machine.transition`: the run visited `from.value` and `snapshot.value`.
|
|
370
|
+
* - `request.start` of a decision, first attempt only: its `request.withheld`
|
|
371
|
+
* counts toward the state the run was in, the `from.value` of the
|
|
372
|
+
* transition its choice causes.
|
|
373
|
+
*
|
|
374
|
+
* A trace is one run's events, live (`AgentTraceEvent`) or serialized
|
|
375
|
+
* (`serializeTraceEvent`). A host-driven `decideNext` traces nothing; push
|
|
376
|
+
* `{ type: 'request.start', request: result.request }` onto the trace before
|
|
377
|
+
* sending its event. Parallel states count once per active leaf.
|
|
378
|
+
*
|
|
379
|
+
* @example
|
|
380
|
+
* ```ts no-check
|
|
381
|
+
* const stats = foldTraces(traces, {
|
|
382
|
+
* failed: (trace) => trace.at(-1)?.type === 'run.end' && trace.at(-1).status === 'error',
|
|
383
|
+
* });
|
|
384
|
+
* ```
|
|
385
|
+
*/
|
|
386
|
+
function foldTraces(traces, options) {
|
|
387
|
+
const stats = /* @__PURE__ */ new Map();
|
|
388
|
+
const statsFor = (state) => {
|
|
389
|
+
let entry = stats.get(state);
|
|
390
|
+
if (!entry) {
|
|
391
|
+
entry = {
|
|
392
|
+
state,
|
|
393
|
+
visits: 0,
|
|
394
|
+
failures: 0,
|
|
395
|
+
withheld: 0
|
|
396
|
+
};
|
|
397
|
+
stats.set(state, entry);
|
|
398
|
+
}
|
|
399
|
+
return entry;
|
|
400
|
+
};
|
|
401
|
+
for (const trace of traces) {
|
|
402
|
+
const visited = /* @__PURE__ */ new Set();
|
|
403
|
+
let current = [];
|
|
404
|
+
const visit = (state) => {
|
|
405
|
+
statsFor(state);
|
|
406
|
+
visited.add(state);
|
|
407
|
+
};
|
|
408
|
+
for (const event of trace) if (event.type === "machine.transition") {
|
|
409
|
+
for (const state of statePaths(valueOf(event.from))) visit(state);
|
|
410
|
+
current = statePaths(valueOf(event.snapshot));
|
|
411
|
+
for (const state of current) visit(state);
|
|
412
|
+
} else if (event.type === "request.start" && isFirstDecision(event.request)) {
|
|
413
|
+
const withheld = event.request.withheld;
|
|
414
|
+
if (!Array.isArray(withheld)) continue;
|
|
415
|
+
for (const state of current) statsFor(state).withheld += withheld.length;
|
|
416
|
+
}
|
|
417
|
+
const failed = options.failed(trace);
|
|
418
|
+
for (const state of visited) {
|
|
419
|
+
const entry = statsFor(state);
|
|
420
|
+
entry.visits++;
|
|
421
|
+
if (failed) entry.failures++;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return [...stats.values()];
|
|
425
|
+
}
|
|
426
|
+
function valueOf(holder) {
|
|
427
|
+
return isRecord(holder) ? holder.value : void 0;
|
|
428
|
+
}
|
|
429
|
+
function isFirstDecision(request) {
|
|
430
|
+
return isRecord(request) && request.kind === "decision" && (!Array.isArray(request.attempts) || request.attempts.length === 0);
|
|
431
|
+
}
|
|
432
|
+
function statePaths(value, prefix = "") {
|
|
433
|
+
if (typeof value === "string") return [prefix + value];
|
|
434
|
+
if (!isRecord(value)) return [];
|
|
435
|
+
const entries = Object.entries(value);
|
|
436
|
+
if (entries.length === 0) return prefix ? [prefix.slice(0, -1)] : [];
|
|
437
|
+
return entries.flatMap(([key, child]) => statePaths(child, `${prefix}${key}.`));
|
|
438
|
+
}
|
|
439
|
+
//#endregion
|
|
440
|
+
//#region src/compare-runs.ts
|
|
441
|
+
/**
|
|
442
|
+
* Runs every scenario through a baseline and a candidate implementation and
|
|
443
|
+
* labels each difference, so a port can show where it behaves like the code it
|
|
444
|
+
* replaces and where it differs on purpose.
|
|
445
|
+
*
|
|
446
|
+
* Parity is outcome-level: the terminal outcome and the effects in the world,
|
|
447
|
+
* not the sequence of tool calls, since a machine usually reorders steps. A
|
|
448
|
+
* candidate that broke no rule where the baseline broke one is intended, even
|
|
449
|
+
* when the outcome matches. A difference also counts as intended when only the
|
|
450
|
+
* candidate reached the scenario's `expected` outcome. Every other difference,
|
|
451
|
+
* and every candidate rule break, is a regression for a person to review.
|
|
452
|
+
*
|
|
453
|
+
* Runs are sequential, baseline first. A run that throws is recorded with
|
|
454
|
+
* outcome `'threw'` and the error message as its `detail`. Parity holds for
|
|
455
|
+
* these scenarios and these runs only; with a live model, run it more than once.
|
|
456
|
+
*
|
|
457
|
+
* @example
|
|
458
|
+
* ```ts no-check
|
|
459
|
+
* const report = await compareRuns({
|
|
460
|
+
* scenarios,
|
|
461
|
+
* baseline: async (s) => toParityRun(await runRawLoop(s)),
|
|
462
|
+
* candidate: async (s) => toParityRun(await runDesk(s)),
|
|
463
|
+
* });
|
|
464
|
+
* expect(report.ok).toBe(true);
|
|
465
|
+
* ```
|
|
466
|
+
*/
|
|
467
|
+
async function compareRuns(options) {
|
|
468
|
+
const scenarios = [];
|
|
469
|
+
const counts = {
|
|
470
|
+
same: 0,
|
|
471
|
+
intended: 0,
|
|
472
|
+
regression: 0
|
|
473
|
+
};
|
|
474
|
+
for (const scenario of options.scenarios) {
|
|
475
|
+
const baseline = await settle(() => options.baseline(scenario));
|
|
476
|
+
const candidate = await settle(() => options.candidate(scenario));
|
|
477
|
+
const { label, reason } = labelRun(scenario.expected, baseline, candidate);
|
|
478
|
+
counts[label]++;
|
|
479
|
+
scenarios.push({
|
|
480
|
+
id: scenario.id,
|
|
481
|
+
scenario,
|
|
482
|
+
label,
|
|
483
|
+
reason,
|
|
484
|
+
baseline,
|
|
485
|
+
candidate
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
scenarios,
|
|
490
|
+
counts,
|
|
491
|
+
ok: counts.regression === 0
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
async function settle(run) {
|
|
495
|
+
try {
|
|
496
|
+
return await run();
|
|
497
|
+
} catch (error) {
|
|
498
|
+
return {
|
|
499
|
+
outcome: "threw",
|
|
500
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function labelRun(expected, baseline, candidate) {
|
|
505
|
+
if (candidate.violations?.length) return {
|
|
506
|
+
label: "regression",
|
|
507
|
+
reason: `candidate broke a rule: ${list(candidate.violations)}`
|
|
508
|
+
};
|
|
509
|
+
if (candidate.outcome === "threw") return {
|
|
510
|
+
label: "regression",
|
|
511
|
+
reason: `candidate threw: ${String(candidate.detail)}`
|
|
512
|
+
};
|
|
513
|
+
if (baseline.violations?.length) return {
|
|
514
|
+
label: "intended",
|
|
515
|
+
reason: `baseline broke a rule: ${list(baseline.violations)}`
|
|
516
|
+
};
|
|
517
|
+
const sameOutcome = baseline.outcome === candidate.outcome;
|
|
518
|
+
if (sameOutcome && structurallyEqual(baseline.effects, candidate.effects)) return {
|
|
519
|
+
label: "same",
|
|
520
|
+
reason: ""
|
|
521
|
+
};
|
|
522
|
+
if (expected !== void 0 && candidate.outcome === expected && baseline.outcome !== expected) return {
|
|
523
|
+
label: "intended",
|
|
524
|
+
reason: `only the candidate reached the expected '${expected}'`
|
|
525
|
+
};
|
|
526
|
+
return {
|
|
527
|
+
label: "regression",
|
|
528
|
+
reason: sameOutcome ? "effects differ" : `outcome differs: ${baseline.outcome} → ${candidate.outcome}`
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function list(items) {
|
|
532
|
+
return items.map((item) => typeof item === "string" ? item : JSON.stringify(item)).join(", ");
|
|
533
|
+
}
|
|
534
|
+
function structurallyEqual(a, b) {
|
|
535
|
+
if (Object.is(a, b)) return true;
|
|
536
|
+
if (a instanceof Date || b instanceof Date) return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
|
|
537
|
+
if (a instanceof Map || b instanceof Map) return a instanceof Map && b instanceof Map && structurallyEqual([...a], [...b]);
|
|
538
|
+
if (a instanceof Set || b instanceof Set) return a instanceof Set && b instanceof Set && structurallyEqual([...a], [...b]);
|
|
539
|
+
if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => structurallyEqual(item, b[index]));
|
|
540
|
+
if (!isRecord(a) || !isRecord(b)) return false;
|
|
541
|
+
const keys = Object.keys(a);
|
|
542
|
+
return keys.length === Object.keys(b).length && keys.every((key) => Object.hasOwn(b, key) && structurallyEqual(a[key], b[key]));
|
|
543
|
+
}
|
|
544
|
+
//#endregion
|
|
545
|
+
export { AgentLintError, AgentUnknownStateError, assertAgentMachine, canReach, compareRuns, explorePaths, foldTraces, lintAgentMachine, matchesTrajectory, runSeam, simulateAgent };
|