@statelyai/agent 2.0.0-alpha.7 → 2.0.0-alpha.8
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 +34 -3
- package/dist/ai-sdk.d.cts +34 -5
- package/dist/ai-sdk.d.mts +34 -5
- package/dist/ai-sdk.mjs +33 -4
- package/dist/cli.cjs +3 -2
- package/dist/cli.mjs +2 -1
- package/dist/{decision-mPR_YQd8.mjs → decision-CQdrKc8k.mjs} +4 -1
- package/dist/{decision-BnATHy0W.cjs → decision-b-lkcs4L.cjs} +9 -0
- package/dist/index.cjs +13 -12
- package/dist/index.d.cts +6 -1072
- package/dist/index.d.mts +6 -1072
- package/dist/index.mjs +3 -2
- package/dist/openai-compat.cjs +1 -1
- package/dist/openai-compat.d.cts +3 -3
- package/dist/openai-compat.d.mts +3 -3
- package/dist/openai-compat.mjs +1 -1
- package/dist/{src-BpQdxsKc.mjs → run-agent-7OaHM7SB.mjs} +23 -987
- package/dist/{src-MysDmqwT.cjs → run-agent-BQ3vV7UI.cjs} +21 -1045
- package/dist/run-agent-BzW4emV_.d.cts +1078 -0
- package/dist/run-agent-XYjmBxHi.d.mts +1078 -0
- package/dist/src-CFtSqm-c.cjs +1043 -0
- package/dist/src-d-jhiOgP.mjs +984 -0
- package/dist/{text-logic-2wFNEznm.d.mts → text-logic-C7WJpCIc.d.mts} +1 -1
- package/dist/{text-logic-4Q2F9kyr.d.cts → text-logic-CZjyACzQ.d.cts} +1 -1
- package/dist/{types-BHjeDdch.d.cts → types-C9QiMjre.d.cts} +16 -5
- package/dist/{types-Cq1YlAQ6.d.mts → types-qm00QF91.d.mts} +16 -5
- package/dist/{utils-CWUCa3pF.d.mts → utils-Dri7aeEG.d.cts} +1 -1
- package/dist/{utils-lK1wnL2i.d.cts → utils-Y6GDRGGE.d.mts} +1 -1
- package/dist/zod.d.cts +1 -1
- package/dist/zod.d.mts +1 -1
- package/package.json +1 -1
- package/readme.md +21 -23
|
@@ -0,0 +1,1043 @@
|
|
|
1
|
+
require("./run-agent-BQ3vV7UI.cjs");
|
|
2
|
+
const require_decision = require("./decision-b-lkcs4L.cjs");
|
|
3
|
+
let xstate = require("xstate");
|
|
4
|
+
//#region src/steps.ts
|
|
5
|
+
/**
|
|
6
|
+
* The step path: durable, per-model-call-checkpoint hosting of an agent
|
|
7
|
+
* machine. Public vocabulary — `initialAgentStep`, `transitionAgentStep`,
|
|
8
|
+
* `resolveAgentStep`, `getAgentRequests`, `executeAgentRequest`,
|
|
9
|
+
* `resolveAgentRequests`.
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
/** @internal Normalizes current and legacy XState invoke effect shapes. */
|
|
13
|
+
function getInvokeEffectMetadata(action) {
|
|
14
|
+
if (action.type === "@xstate.spawn") return action;
|
|
15
|
+
if (action.type === "xstate.spawnChild") {
|
|
16
|
+
const params = action.params;
|
|
17
|
+
return params ? {
|
|
18
|
+
...params,
|
|
19
|
+
logic: action.logic
|
|
20
|
+
} : void 0;
|
|
21
|
+
}
|
|
22
|
+
if (action.type === "@xstate.start" && typeof action.src === "string") return action;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Scans a set of executable actions (as returned by xstate's `transition`/
|
|
26
|
+
* `initialTransition`) for spawned `TextLogic`/`DecisionLogic` invokes and
|
|
27
|
+
* lowers each into an {@link AgentStepRequest}. The hand-passed-schemas
|
|
28
|
+
* implementation detail behind the public {@link getAgentRequests} — it needs
|
|
29
|
+
* `schemas`/`actorSources` passed explicitly, whereas `getAgentRequests`
|
|
30
|
+
* pre-fills them from the machine's registered `setupAgent` options.
|
|
31
|
+
* `options.snapshot` is required to resolve a decision's candidate events
|
|
32
|
+
* (intersecting declared `allowedEvents` with what's currently legal) — omit
|
|
33
|
+
* it and decision requests report an empty `events` list.
|
|
34
|
+
*
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
function getAgentRequestsWith(actions, options = {}) {
|
|
38
|
+
return [...actions.flatMap((action) => {
|
|
39
|
+
const params = getInvokeEffectMetadata(action);
|
|
40
|
+
if (!params || typeof params.src !== "string") return [];
|
|
41
|
+
if (typeof params.id !== "string" || params.id.length === 0) throw new Error(`Agent invoke '${params.src}' must define a durable string id.`);
|
|
42
|
+
const registeredLogic = require_decision.isTextLogic(params.logic) || require_decision.isDecisionLogic(params.logic) ? params.logic : options.actorSources?.[params.src];
|
|
43
|
+
if (require_decision.isDecisionLogic(registeredLogic)) {
|
|
44
|
+
const decisionRequest = registeredLogic.request(params.input);
|
|
45
|
+
const allowedEventTypes = registeredLogic.allowedEventTypes?.(params.input);
|
|
46
|
+
const events = options.snapshot ? require_decision.getAcceptedEvents(options.snapshot, {
|
|
47
|
+
events: options.events,
|
|
48
|
+
schemas: options.schemas,
|
|
49
|
+
eventTypes: allowedEventTypes,
|
|
50
|
+
eventToolName: options.eventToolName
|
|
51
|
+
}) : [];
|
|
52
|
+
return [{
|
|
53
|
+
...decisionRequest,
|
|
54
|
+
id: params.id,
|
|
55
|
+
events
|
|
56
|
+
}];
|
|
57
|
+
}
|
|
58
|
+
const textLogic = require_decision.isTextLogic(registeredLogic) ? registeredLogic : void 0;
|
|
59
|
+
const input = textLogic ? textLogic.request(params.input) : void 0;
|
|
60
|
+
if (!input) return [];
|
|
61
|
+
return [{
|
|
62
|
+
kind: "text",
|
|
63
|
+
id: params.id,
|
|
64
|
+
src: params.src,
|
|
65
|
+
...textLogic ? { mode: textLogic.mode } : {},
|
|
66
|
+
input,
|
|
67
|
+
tools: input.tools ?? {},
|
|
68
|
+
events: []
|
|
69
|
+
}];
|
|
70
|
+
}), ...getActivePlanRequests(options)];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Scans the live snapshot's children for active `agent.plan` (plan-logic)
|
|
74
|
+
* invokes and lowers each into an {@link AgentPlanRequest} — the re-surfacing
|
|
75
|
+
* half of plan discovery. Reads the applied-event trail and remaining budget
|
|
76
|
+
* from the child's own ledger `context` ({@link PlanLedgerContext}), recomputes
|
|
77
|
+
* the currently-legal candidates (∩ declared `allowedEvents`) plus the reserved
|
|
78
|
+
* `agent.plan.done` move, and takes `stepsRemaining` from the ledger (falling
|
|
79
|
+
* back to `maxSteps - applied.length` for a snapshot with no context yet).
|
|
80
|
+
* Returns `[]` when no snapshot is available (candidates need a live snapshot).
|
|
81
|
+
*
|
|
82
|
+
* @internal
|
|
83
|
+
*/
|
|
84
|
+
function getActivePlanRequests(options) {
|
|
85
|
+
const snapshot = options.snapshot;
|
|
86
|
+
if (!snapshot) return [];
|
|
87
|
+
const children = snapshot.children;
|
|
88
|
+
if (!children) return [];
|
|
89
|
+
const requests = [];
|
|
90
|
+
for (const [id, child] of Object.entries(children)) {
|
|
91
|
+
const ref = child;
|
|
92
|
+
if (typeof ref?.getSnapshot !== "function") continue;
|
|
93
|
+
const src = typeof ref.src === "string" ? ref.src : void 0;
|
|
94
|
+
const logic = (src ? options.actorSources?.[src] : void 0) ?? ref.logic;
|
|
95
|
+
if (!require_decision.isPlanLogic(logic)) continue;
|
|
96
|
+
const childSnapshot = ref.getSnapshot();
|
|
97
|
+
if (childSnapshot?.status !== "active") continue;
|
|
98
|
+
const input = childSnapshot.input ?? {};
|
|
99
|
+
const maxSteps = input.maxSteps ?? 8;
|
|
100
|
+
const ledger = childSnapshot.context ?? {};
|
|
101
|
+
const applied = ledger.applied ?? [];
|
|
102
|
+
const stepsRemaining = ledger.stepsRemaining ?? maxSteps - applied.length;
|
|
103
|
+
const machineEvents = require_decision.getAcceptedEvents(snapshot, {
|
|
104
|
+
events: options.events,
|
|
105
|
+
schemas: options.schemas,
|
|
106
|
+
eventTypes: logic.allowedEventTypes(input) ?? void 0,
|
|
107
|
+
eventToolName: options.eventToolName
|
|
108
|
+
});
|
|
109
|
+
const events = machineEvents.some((event) => event.type === "agent.plan.done") ? machineEvents : [...machineEvents, {
|
|
110
|
+
type: require_decision.PLAN_DONE_EVENT_TYPE,
|
|
111
|
+
toolName: require_decision.sanitizeEventToolName(require_decision.PLAN_DONE_EVENT_TYPE)
|
|
112
|
+
}];
|
|
113
|
+
requests.push({
|
|
114
|
+
kind: "plan",
|
|
115
|
+
id,
|
|
116
|
+
src: src ?? "",
|
|
117
|
+
input,
|
|
118
|
+
events,
|
|
119
|
+
applied,
|
|
120
|
+
stepsRemaining: Math.max(0, stepsRemaining)
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return requests;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Builds the synthetic `xstate.done.actor.<id>` event xstate's `transition()`
|
|
127
|
+
* expects to resolve a spawned invoke — the event {@link resolveAgentStep}
|
|
128
|
+
* applies internally.
|
|
129
|
+
*
|
|
130
|
+
* @internal
|
|
131
|
+
*/
|
|
132
|
+
function doneEvent(request, output) {
|
|
133
|
+
return {
|
|
134
|
+
type: `xstate.done.actor.${typeof request === "string" ? request : request.id}`,
|
|
135
|
+
output
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Applies a request's `output` as a done event via `transition(...)`,
|
|
140
|
+
* returning the raw `[snapshot, actions]` tuple. Lower-level than
|
|
141
|
+
* {@link resolveAgentStep} — that helper wraps this and also runs
|
|
142
|
+
* {@link getAgentRequests} to produce the next {@link AgentStep}.
|
|
143
|
+
*
|
|
144
|
+
* @internal
|
|
145
|
+
*/
|
|
146
|
+
function transitionResult(logic, snapshot, request, output) {
|
|
147
|
+
const event = doneEvent(request, output);
|
|
148
|
+
const result = (0, xstate.transition)(logic, snapshot, event);
|
|
149
|
+
applyFinalStateOutput(logic, result[0], event);
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Starts a machine and returns its first {@link AgentStep} — the step-path
|
|
154
|
+
* equivalent of `initialTransition` plus request discovery. Begins the
|
|
155
|
+
* durable/per-model-call-checkpoint loop: resolve each `step.requests` entry
|
|
156
|
+
* (via {@link executeAgentRequest} for `kind: 'text'`, or
|
|
157
|
+
* {@link resolveDecision} for `kind: 'decision'`), then advance with
|
|
158
|
+
* {@link resolveAgentStep} or {@link transitionAgentStep}.
|
|
159
|
+
*/
|
|
160
|
+
function initialAgentStep(machine, input, options) {
|
|
161
|
+
const [snapshot, actions] = (0, xstate.initialTransition)(machine, input);
|
|
162
|
+
return createAgentStep(machine, snapshot, actions, require_decision.getRegisteredAgentExecutionOptions(machine, options));
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Applies an externally-sent event (e.g. a decision's chosen event, or a
|
|
166
|
+
* human's reply) and returns the next {@link AgentStep}. Accepts **either**
|
|
167
|
+
* a raw snapshot **or** a prior `AgentStep` as the second argument —
|
|
168
|
+
* `.snapshot` is unwrapped automatically, so callers can thread the whole
|
|
169
|
+
* step object through without manually plucking the snapshot out.
|
|
170
|
+
*/
|
|
171
|
+
function transitionAgentStep(machine, snapshotOrStep, event, options) {
|
|
172
|
+
const [nextSnapshot, actions] = (0, xstate.transition)(machine, isAgentStep(snapshotOrStep) ? snapshotOrStep.snapshot : snapshotOrStep, event);
|
|
173
|
+
return createAgentStep(machine, nextSnapshot, actions, require_decision.getRegisteredAgentExecutionOptions(machine, options));
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Applies a resolved text request's output (a `kind: 'text'`
|
|
177
|
+
* {@link AgentRequest} — not a decision) as a done event and returns the
|
|
178
|
+
* next {@link AgentStep}. For decisions, resolve with `resolveDecision`
|
|
179
|
+
* (which returns a {@link ChosenEvent}) and apply it with
|
|
180
|
+
* {@link transitionAgentStep} instead — a decision has no output value of
|
|
181
|
+
* its own to feed here.
|
|
182
|
+
*/
|
|
183
|
+
function resolveAgentStep(machine, step, request, output, options) {
|
|
184
|
+
const [snapshot, actions] = transitionResult(machine, step.snapshot, request, output);
|
|
185
|
+
return createAgentStep(machine, snapshot, actions, require_decision.getRegisteredAgentExecutionOptions(machine, options));
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Snapshot in, requests out: scans executable actions for spawned agent
|
|
189
|
+
* invokes and lowers each into an {@link AgentStepRequest}, pre-filled with
|
|
190
|
+
* the machine's registered `setupAgent` schemas/actorSources (so callers
|
|
191
|
+
* don't pass them by hand each call) — merged with any `options` passed here,
|
|
192
|
+
* which take precedence. The step path's public discovery primitive;
|
|
193
|
+
* `initialAgentStep`/`transitionAgentStep`/`resolveAgentStep` call it
|
|
194
|
+
* internally to populate `AgentStep.requests`.
|
|
195
|
+
*/
|
|
196
|
+
function getAgentRequests(machine, actions, snapshot, options = {}) {
|
|
197
|
+
return getAgentRequestsWith(actions, {
|
|
198
|
+
...require_decision.getRegisteredAgentExecutionOptions(machine, options),
|
|
199
|
+
...options,
|
|
200
|
+
snapshot
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
async function executeAgentRequest(request, executors, options) {
|
|
204
|
+
if (request.kind === "decision") throw new Error("executeAgentRequest(...) is text-only. Resolve a 'decision' request with resolveDecision(request, executors.decide, ...) instead.");
|
|
205
|
+
assertTextExecutor(request, executors);
|
|
206
|
+
const { output, raw } = await require_decision.executeAgentTextRequest(request.mode ?? "generate", request.id, request.input, executors, request.tools);
|
|
207
|
+
const normalizedOutput = request.input.outputSchema ? require_decision.validateSchemaSync(request.input.outputSchema, output) : output;
|
|
208
|
+
return options?.verbose ? {
|
|
209
|
+
output: normalizedOutput,
|
|
210
|
+
raw
|
|
211
|
+
} : normalizedOutput;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Resolves the current step's pending requests and returns the next
|
|
215
|
+
* {@link AgentStep} — one iteration of the durable step loop, collapsing the
|
|
216
|
+
* manual `request.kind` dispatch a host would otherwise write by hand.
|
|
217
|
+
*
|
|
218
|
+
* For each pending request, in order: a `kind: 'text'` request is run with
|
|
219
|
+
* {@link executeAgentRequest} then fed back via {@link resolveAgentStep}; a
|
|
220
|
+
* `kind: 'decision'` request is resolved with `resolveDecision` (wiring
|
|
221
|
+
* `canTake` to `step.snapshot.can` so guard-rejected choices retry) then
|
|
222
|
+
* applied with {@link transitionAgentStep}. The **current** step is re-read
|
|
223
|
+
* after each application — the machine may advance and its `requests` change —
|
|
224
|
+
* so this always resolves against the live step, never a stale list.
|
|
225
|
+
*
|
|
226
|
+
* A `kind: 'plan'` request (`agent.plan`) is resolved natively too: one plan
|
|
227
|
+
* step per call. It resolves a single decision from `request.events` (wiring
|
|
228
|
+
* `canTake` to `step.snapshot.can`, exempting the reserved `agent.plan.done`
|
|
229
|
+
* move and `stopOn` events), then either applies the chosen machine event and
|
|
230
|
+
* lets the next step re-surface the plan, or completes the plan (feeding its
|
|
231
|
+
* `{ steps, stopped }` output back) on the done move / a `stopOn` event / an
|
|
232
|
+
* exhausted budget / no legal events. The plan's applied trail is carried in
|
|
233
|
+
* the invoke child's snapshot, so persisting the step between calls resumes the
|
|
234
|
+
* plan identically.
|
|
235
|
+
*
|
|
236
|
+
* Missing the executor a request needs throws a clear error
|
|
237
|
+
* (`generateText`/`streamText` for text, `decide` for decisions and plans).
|
|
238
|
+
*
|
|
239
|
+
* A complete durable host is two lines:
|
|
240
|
+
*
|
|
241
|
+
* ```ts
|
|
242
|
+
* let step = initialAgentStep(machine, input);
|
|
243
|
+
* while (!step.done) step = await resolveAgentRequests(machine, step, executors);
|
|
244
|
+
* ```
|
|
245
|
+
*
|
|
246
|
+
* All pending **text** requests of a step are resolved in parallel
|
|
247
|
+
* (`Promise.all`) — parallel statechart regions are genuinely concurrent, so
|
|
248
|
+
* their model calls run concurrently — then their outputs apply in
|
|
249
|
+
* **request-array order** (deterministic for durable replay regardless of which
|
|
250
|
+
* call finishes first). Decisions and plans stay **one at a time**: applying
|
|
251
|
+
* either changes the set of legal candidates for what follows, so they cannot be
|
|
252
|
+
* resolved against a stale snapshot. A host that instead wants strictly
|
|
253
|
+
* sequential text resolution loops the manual per-request helpers
|
|
254
|
+
* ({@link executeAgentRequest} + {@link resolveAgentStep}) one at a time.
|
|
255
|
+
*/
|
|
256
|
+
async function resolveAgentRequests(machine, step, executors, options) {
|
|
257
|
+
const [request] = step.requests;
|
|
258
|
+
if (!request) return step;
|
|
259
|
+
if (request.kind === "decision") {
|
|
260
|
+
if (!executors.decide) throw new Error(`this step's decision request '${request.id}' needs a 'decide' executor but none was provided.`);
|
|
261
|
+
return transitionAgentStep(machine, step, await require_decision.resolveDecision(request, executors.decide, {
|
|
262
|
+
canTake: (event) => step.snapshot.can(event),
|
|
263
|
+
maxRetries: options?.maxRetries
|
|
264
|
+
}), options);
|
|
265
|
+
}
|
|
266
|
+
if (request.kind === "plan") return resolvePlanRequest(machine, step, request, executors, options);
|
|
267
|
+
const textRequests = step.requests.filter((candidate) => candidate.kind === "text");
|
|
268
|
+
for (const textRequest of textRequests) assertTextExecutor(textRequest, executors);
|
|
269
|
+
const outputs = await Promise.all(textRequests.map((textRequest) => executeAgentRequest(textRequest, executors)));
|
|
270
|
+
let next = step;
|
|
271
|
+
for (let index = 0; index < textRequests.length; index++) next = resolveAgentStep(machine, next, textRequests[index], outputs[index], options);
|
|
272
|
+
return next;
|
|
273
|
+
}
|
|
274
|
+
function assertTextExecutor(request, executors) {
|
|
275
|
+
const mode = request.mode ?? "generate";
|
|
276
|
+
const kind = mode === "stream" ? "streamText" : "generateText";
|
|
277
|
+
if (!(mode === "stream" ? executors.streamText : executors.generateText)) throw new Error(`this step's text request '${request.src}' needs a '${kind}' executor but none was provided.`);
|
|
278
|
+
}
|
|
279
|
+
async function resolvePlanRequest(machine, step, request, executors, options) {
|
|
280
|
+
if (!executors.decide) throw new Error(`this step's plan request '${request.src}' needs a 'decide' executor but none was provided.`);
|
|
281
|
+
const stopOn = new Set(request.input.stopOn ?? []);
|
|
282
|
+
if (request.stepsRemaining <= 0) return completePlan(machine, step, request.id, request.applied, "max-steps", options);
|
|
283
|
+
if (request.events.filter((event) => event.type !== "agent.plan.done").length === 0) return completePlan(machine, step, request.id, request.applied, "no-legal-events", options);
|
|
284
|
+
const chosen = await require_decision.resolveDecision(planStepDecisionRequest(request), executors.decide, {
|
|
285
|
+
maxRetries: options?.maxRetries,
|
|
286
|
+
canTake: (event) => {
|
|
287
|
+
if (event.type === "agent.plan.done" || stopOn.has(event.type)) return true;
|
|
288
|
+
return step.snapshot.can(event);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
if (chosen.type === "agent.plan.done") return completePlan(machine, step, request.id, request.applied, "done", options);
|
|
292
|
+
const applied = [...request.applied, chosen];
|
|
293
|
+
advancePlanChildLedger(step.snapshot, request.id, {
|
|
294
|
+
type: "plan.applied",
|
|
295
|
+
event: chosen
|
|
296
|
+
});
|
|
297
|
+
const next = transitionAgentStep(machine, step, chosen, options);
|
|
298
|
+
if (stopOn.has(chosen.type)) {
|
|
299
|
+
if (isPlanActive(next.snapshot, request.id)) return completePlan(machine, next, request.id, applied, "stop-event", options);
|
|
300
|
+
}
|
|
301
|
+
return next;
|
|
302
|
+
}
|
|
303
|
+
function completePlan(machine, step, id, steps, stopped, options) {
|
|
304
|
+
return resolveAgentStep(machine, step, { id }, {
|
|
305
|
+
steps,
|
|
306
|
+
stopped
|
|
307
|
+
}, options);
|
|
308
|
+
}
|
|
309
|
+
function planStepDecisionRequest(request) {
|
|
310
|
+
const { input, applied, events, id } = request;
|
|
311
|
+
const trail = applied.length === 0 ? "" : `\n\nEvents already applied in this plan, in order:\n${applied.map((step) => JSON.stringify(step)).join("\n")}\nContinue from here; do not repeat applied events.`;
|
|
312
|
+
const doneHint = `\n\nWhen the request is fully handled (or no action is needed), choose '${require_decision.PLAN_DONE_EVENT_TYPE}'.`;
|
|
313
|
+
return {
|
|
314
|
+
kind: "decision",
|
|
315
|
+
id: `${id}[${applied.length}]`,
|
|
316
|
+
model: input.model,
|
|
317
|
+
system: input.system,
|
|
318
|
+
prompt: `${input.prompt ?? ""}${trail}${doneHint}`,
|
|
319
|
+
messages: input.messages,
|
|
320
|
+
events,
|
|
321
|
+
attempts: [],
|
|
322
|
+
temperature: input.temperature,
|
|
323
|
+
maxOutputTokens: input.maxOutputTokens,
|
|
324
|
+
topP: input.topP,
|
|
325
|
+
topK: input.topK,
|
|
326
|
+
seed: input.seed,
|
|
327
|
+
stopSequences: input.stopSequences,
|
|
328
|
+
metadata: input.metadata
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function advancePlanChildLedger(snapshot, id, event) {
|
|
332
|
+
const child = snapshot.children?.[id];
|
|
333
|
+
const childSnapshot = child?.getSnapshot?.();
|
|
334
|
+
if (!require_decision.isPlanLogic(child?.logic) || !childSnapshot || typeof childSnapshot !== "object") return;
|
|
335
|
+
Object.assign(childSnapshot, require_decision.advancePlanLedger(child.logic, childSnapshot, event));
|
|
336
|
+
}
|
|
337
|
+
function isPlanActive(snapshot, id) {
|
|
338
|
+
return (snapshot.children?.[id])?.getSnapshot?.()?.status === "active";
|
|
339
|
+
}
|
|
340
|
+
function createAgentStep(machine, snapshot, actions, options) {
|
|
341
|
+
applyFinalStateOutput(machine, snapshot);
|
|
342
|
+
return {
|
|
343
|
+
snapshot,
|
|
344
|
+
actions,
|
|
345
|
+
requests: getAgentRequestsWith(actions, {
|
|
346
|
+
...options,
|
|
347
|
+
snapshot
|
|
348
|
+
}),
|
|
349
|
+
done: snapshot.status === "done"
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function resolveStateValueConfig(config, value) {
|
|
353
|
+
if (typeof value === "string") return config.states?.[value];
|
|
354
|
+
if (!value || typeof value !== "object") return;
|
|
355
|
+
for (const [key, childValue] of Object.entries(value)) {
|
|
356
|
+
const childConfig = config.states?.[key];
|
|
357
|
+
if (!childConfig) continue;
|
|
358
|
+
if (childConfig.type === "final") return childConfig;
|
|
359
|
+
const nested = resolveStateValueConfig(childConfig, childValue);
|
|
360
|
+
if (nested) return nested;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function applyFinalStateOutput(logic, snapshot, event) {
|
|
364
|
+
const machineSnapshot = snapshot;
|
|
365
|
+
if (machineSnapshot.status !== "done" || machineSnapshot.output !== void 0 || !("config" in logic)) return;
|
|
366
|
+
const config = logic.config;
|
|
367
|
+
if (!config) return;
|
|
368
|
+
const output = resolveStateValueConfig(config, machineSnapshot.value)?.output;
|
|
369
|
+
if (output === void 0) return;
|
|
370
|
+
machineSnapshot.output = typeof output === "function" ? output({
|
|
371
|
+
context: machineSnapshot.context,
|
|
372
|
+
event
|
|
373
|
+
}) : output;
|
|
374
|
+
}
|
|
375
|
+
function isAgentStep(value) {
|
|
376
|
+
return !!value && typeof value === "object" && "snapshot" in value && "actions" in value && "requests" in value;
|
|
377
|
+
}
|
|
378
|
+
//#endregion
|
|
379
|
+
//#region src/verify.ts
|
|
380
|
+
const DECIDE_SRC = "agent.decide";
|
|
381
|
+
const PLAN_SRC = "agent.plan";
|
|
382
|
+
function normalizeInvokes(invoke) {
|
|
383
|
+
if (invoke === void 0 || invoke === null) return [];
|
|
384
|
+
return Array.isArray(invoke) ? invoke : [invoke];
|
|
385
|
+
}
|
|
386
|
+
function buildStateIndex(rootConfig) {
|
|
387
|
+
const index = /* @__PURE__ */ new Map();
|
|
388
|
+
const walk = (states, parentPath) => {
|
|
389
|
+
for (const [name, config] of Object.entries(states ?? {})) {
|
|
390
|
+
const path = parentPath ? `${parentPath}.${name}` : name;
|
|
391
|
+
const hasChildren = !!config.states && Object.keys(config.states).length > 0;
|
|
392
|
+
index.set(path, {
|
|
393
|
+
path,
|
|
394
|
+
name,
|
|
395
|
+
config,
|
|
396
|
+
parentPath,
|
|
397
|
+
type: config.type,
|
|
398
|
+
isFinal: config.type === "final",
|
|
399
|
+
isParallel: config.type === "parallel",
|
|
400
|
+
isCompound: hasChildren && config.type !== "parallel",
|
|
401
|
+
invokes: normalizeInvokes(config.invoke)
|
|
402
|
+
});
|
|
403
|
+
if (hasChildren) walk(config.states, path);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
walk(rootConfig.states, "");
|
|
407
|
+
return index;
|
|
408
|
+
}
|
|
409
|
+
function childrenOf(index, parentPath) {
|
|
410
|
+
const out = [];
|
|
411
|
+
for (const node of index.values()) if (node.parentPath === parentPath) out.push(node);
|
|
412
|
+
return out;
|
|
413
|
+
}
|
|
414
|
+
function collectTransitionTargets(value, fromNode, index, out) {
|
|
415
|
+
if (value === void 0 || value === null) return;
|
|
416
|
+
if (Array.isArray(value)) {
|
|
417
|
+
for (const item of value) collectTransitionTargets(item, fromNode, index, out);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (typeof value === "function") {
|
|
421
|
+
out.opaque = true;
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (typeof value === "string") {
|
|
425
|
+
resolveTargetString(value, fromNode, index, out);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (typeof value === "object") {
|
|
429
|
+
const target = value.target;
|
|
430
|
+
if (target !== void 0) collectTransitionTargets(target, fromNode, index, out);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function resolveTargetString(target, fromNode, index, out) {
|
|
434
|
+
if (target.startsWith("#")) {
|
|
435
|
+
out.opaque = true;
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const resolved = target.startsWith(".") ? `${fromNode.path}.${target.slice(1)}` : fromNode.parentPath ? `${fromNode.parentPath}.${target}` : target;
|
|
439
|
+
if (index.has(resolved)) out.targets.push(resolved);
|
|
440
|
+
else out.opaque = true;
|
|
441
|
+
}
|
|
442
|
+
function outgoingTargets(node, index) {
|
|
443
|
+
const out = {
|
|
444
|
+
targets: [],
|
|
445
|
+
opaque: false
|
|
446
|
+
};
|
|
447
|
+
const { config } = node;
|
|
448
|
+
for (const value of Object.values(config.on ?? {})) collectTransitionTargets(value, node, index, out);
|
|
449
|
+
collectTransitionTargets(config.always, node, index, out);
|
|
450
|
+
collectTransitionTargets(config.choice, node, index, out);
|
|
451
|
+
for (const value of Object.values(config.after ?? {})) collectTransitionTargets(value, node, index, out);
|
|
452
|
+
collectTransitionTargets(config.onDone, node, index, out);
|
|
453
|
+
for (const invoke of node.invokes) {
|
|
454
|
+
collectTransitionTargets(invoke.onDone, node, index, out);
|
|
455
|
+
collectTransitionTargets(invoke.onError, node, index, out);
|
|
456
|
+
}
|
|
457
|
+
return out;
|
|
458
|
+
}
|
|
459
|
+
function computeReachable(rootConfig, index) {
|
|
460
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
461
|
+
const queue = [];
|
|
462
|
+
const markAncestors = (path) => {
|
|
463
|
+
let parent = index.get(path)?.parentPath ?? "";
|
|
464
|
+
while (parent) {
|
|
465
|
+
reachable.add(parent);
|
|
466
|
+
parent = index.get(parent)?.parentPath ?? "";
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
const enter = (path) => {
|
|
470
|
+
if (reachable.has(path)) return;
|
|
471
|
+
const node = index.get(path);
|
|
472
|
+
if (!node) return;
|
|
473
|
+
reachable.add(path);
|
|
474
|
+
queue.push(path);
|
|
475
|
+
markAncestors(path);
|
|
476
|
+
if (node.isParallel) for (const child of childrenOf(index, path)) enter(child.path);
|
|
477
|
+
else if (node.isCompound && node.config.initial) enter(`${path}.${node.config.initial}`);
|
|
478
|
+
};
|
|
479
|
+
if (rootConfig.type === "parallel") for (const child of childrenOf(index, "")) enter(child.path);
|
|
480
|
+
else if (rootConfig.initial) enter(rootConfig.initial);
|
|
481
|
+
while (queue.length > 0) {
|
|
482
|
+
const node = index.get(queue.shift());
|
|
483
|
+
if (!node) continue;
|
|
484
|
+
const { targets, opaque } = outgoingTargets(node, index);
|
|
485
|
+
for (const target of targets) enter(target);
|
|
486
|
+
if (opaque) for (const sibling of childrenOf(index, node.parentPath)) enter(sibling.path);
|
|
487
|
+
}
|
|
488
|
+
return reachable;
|
|
489
|
+
}
|
|
490
|
+
function ancestorChain(node, index) {
|
|
491
|
+
const chain = [];
|
|
492
|
+
let parent = node.parentPath;
|
|
493
|
+
while (parent) {
|
|
494
|
+
const parentNode = index.get(parent);
|
|
495
|
+
if (!parentNode) break;
|
|
496
|
+
chain.push(parentNode);
|
|
497
|
+
parent = parentNode.parentPath;
|
|
498
|
+
}
|
|
499
|
+
return chain;
|
|
500
|
+
}
|
|
501
|
+
function hasNonEmptyOn(config) {
|
|
502
|
+
return !!config.on && Object.keys(config.on).length > 0;
|
|
503
|
+
}
|
|
504
|
+
function decisionKindOf(src, actorSources) {
|
|
505
|
+
if (typeof src === "string") {
|
|
506
|
+
if (src === DECIDE_SRC) return "decision";
|
|
507
|
+
if (src === PLAN_SRC) return "plan";
|
|
508
|
+
const logic = actorSources[src];
|
|
509
|
+
if (require_decision.isDecisionLogic(logic)) return "decision";
|
|
510
|
+
if (require_decision.isPlanLogic(logic)) return "plan";
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (require_decision.isDecisionLogic(src)) return "decision";
|
|
514
|
+
if (require_decision.isPlanLogic(src)) return "plan";
|
|
515
|
+
}
|
|
516
|
+
function isAgentLogicNeedingBinding(src) {
|
|
517
|
+
return (require_decision.isTextLogic(src) || require_decision.isDecisionLogic(src) || require_decision.isPlanLogic(src)) && !require_decision.executorBoundLogics.has(src);
|
|
518
|
+
}
|
|
519
|
+
function schemaExposesJson(schema) {
|
|
520
|
+
if (!schema) return false;
|
|
521
|
+
try {
|
|
522
|
+
return require_decision.getJsonSchemaSync(schema) !== void 0;
|
|
523
|
+
} catch {
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function isDeclaredOutputSchema(schema) {
|
|
528
|
+
if (!schema) return false;
|
|
529
|
+
let json;
|
|
530
|
+
try {
|
|
531
|
+
json = require_decision.getJsonSchemaSync(schema);
|
|
532
|
+
} catch {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
if (!json) return false;
|
|
536
|
+
const properties = json.properties;
|
|
537
|
+
const required = json.required;
|
|
538
|
+
return json.type === "object" && !!properties && Object.keys(properties).length > 0 || Array.isArray(required) && required.length > 0;
|
|
539
|
+
}
|
|
540
|
+
function checkUnreachableStates(ctx) {
|
|
541
|
+
const out = [];
|
|
542
|
+
for (const node of ctx.index.values()) if (!ctx.reachable.has(node.path)) out.push({
|
|
543
|
+
code: "unreachable-state",
|
|
544
|
+
severity: "error",
|
|
545
|
+
path: node.path,
|
|
546
|
+
message: `State '${node.path}' is unreachable: no transition, always, onDone, or onError target (from any reachable state) leads to it. Remove it or add a transition.`
|
|
547
|
+
});
|
|
548
|
+
return out;
|
|
549
|
+
}
|
|
550
|
+
function checkDecideWithoutEvents(ctx) {
|
|
551
|
+
const out = [];
|
|
552
|
+
for (const node of ctx.index.values()) for (const invoke of node.invokes) {
|
|
553
|
+
const kind = decisionKindOf(invoke.src, ctx.actorSources);
|
|
554
|
+
if (!kind) continue;
|
|
555
|
+
const selfHandles = hasNonEmptyOn(node.config);
|
|
556
|
+
const ancestorHandles = ancestorChain(node, ctx.index).some((ancestor) => hasNonEmptyOn(ancestor.config));
|
|
557
|
+
const invokeObserves = invoke.onDone !== void 0;
|
|
558
|
+
if (selfHandles || ancestorHandles || invokeObserves) continue;
|
|
559
|
+
const srcName = typeof invoke.src === "string" ? invoke.src : "(inline logic)";
|
|
560
|
+
out.push({
|
|
561
|
+
code: "decide-without-events",
|
|
562
|
+
severity: "error",
|
|
563
|
+
path: node.path,
|
|
564
|
+
message: `State '${node.path}' invokes ${kind} source '${srcName}', but neither it nor any ancestor handles any event (no 'on:'), so the ${kind}'s chosen event can never be delivered. Add an 'on:' handler for the candidate events.`
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
return out;
|
|
568
|
+
}
|
|
569
|
+
function checkUnserializableContext(ctx) {
|
|
570
|
+
const contextSchema = ctx.schemas?.context;
|
|
571
|
+
if (!contextSchema) return [];
|
|
572
|
+
if (schemaExposesJson(contextSchema)) return [];
|
|
573
|
+
return [{
|
|
574
|
+
code: "unserializable-context",
|
|
575
|
+
severity: "warning",
|
|
576
|
+
path: "context",
|
|
577
|
+
message: "The context schema does not expose a JSON schema (e.g. a `z.custom` without a `jsonSchema` extension, such as a messages array), so its fields cannot be statically checked for JSON persist/resume round-tripping. This is expected for message transcripts; verify any other custom-typed context is JSON-serializable."
|
|
578
|
+
}];
|
|
579
|
+
}
|
|
580
|
+
function checkDirectObjectSrc(ctx) {
|
|
581
|
+
const out = [];
|
|
582
|
+
for (const node of ctx.index.values()) for (const invoke of node.invokes) {
|
|
583
|
+
const src = invoke.src;
|
|
584
|
+
if (typeof src === "string" || !src || typeof src !== "object") continue;
|
|
585
|
+
if (!isAgentLogicNeedingBinding(src)) continue;
|
|
586
|
+
out.push({
|
|
587
|
+
code: "direct-object-src",
|
|
588
|
+
severity: "warning",
|
|
589
|
+
path: node.path,
|
|
590
|
+
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({ actorSources: { name: logic } })) and invoke it by name.`
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
return out;
|
|
594
|
+
}
|
|
595
|
+
function checkFinalWithoutOutput(ctx) {
|
|
596
|
+
if (!isDeclaredOutputSchema(ctx.schemas?.output)) return [];
|
|
597
|
+
if (ctx.config.output !== void 0) return [];
|
|
598
|
+
const out = [];
|
|
599
|
+
for (const node of ctx.index.values()) {
|
|
600
|
+
if (node.parentPath !== "" || !node.isFinal) continue;
|
|
601
|
+
if (node.config.output === void 0) out.push({
|
|
602
|
+
code: "final-without-output",
|
|
603
|
+
severity: "error",
|
|
604
|
+
path: node.path,
|
|
605
|
+
message: `The machine declares an output schema, but top-level final state '${node.path}' has no 'output'. Its snapshot output will be undefined and fail the schema. Add an 'output' to this final state (or a root 'output').`
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
return out;
|
|
609
|
+
}
|
|
610
|
+
function outputFnReadsEvent(fn) {
|
|
611
|
+
return /\bevent\s*(?:\.|\?\.|\[)/.test(fn.toString());
|
|
612
|
+
}
|
|
613
|
+
function checkFinalOutputReadsEvent(ctx) {
|
|
614
|
+
const out = [];
|
|
615
|
+
for (const node of ctx.index.values()) {
|
|
616
|
+
if (node.parentPath !== "" || !node.isFinal) continue;
|
|
617
|
+
const output = node.config.output;
|
|
618
|
+
if (typeof output !== "function") continue;
|
|
619
|
+
if (!outputFnReadsEvent(output)) continue;
|
|
620
|
+
out.push({
|
|
621
|
+
code: "final-output-reads-event",
|
|
622
|
+
severity: "warning",
|
|
623
|
+
path: node.path,
|
|
624
|
+
message: `Final state '${node.path}' has an 'output' function that reads 'event'. Final-state 'output' functions are evaluated more than once with different events (the entering event, then the machine-done computation) under current xstate behavior, so 'event' is unreliable here. Read 'context' only; capture what you need from the entering event into context in the transition that targets this state. (This guard can relax if xstate guarantees a stable entering event across evaluations.)`
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
return out;
|
|
628
|
+
}
|
|
629
|
+
function checkMissingFinal(ctx) {
|
|
630
|
+
for (const node of ctx.index.values()) if (node.isFinal && ctx.reachable.has(node.path)) return [];
|
|
631
|
+
return [{
|
|
632
|
+
code: "missing-final",
|
|
633
|
+
severity: "warning",
|
|
634
|
+
path: "(root)",
|
|
635
|
+
message: "The machine has no reachable final state, so a run can never settle 'done' (only idle/looping). This is legal for agents that only idle, but verify it is intended."
|
|
636
|
+
}];
|
|
637
|
+
}
|
|
638
|
+
const LINT_CHECKS = [
|
|
639
|
+
checkUnreachableStates,
|
|
640
|
+
checkDecideWithoutEvents,
|
|
641
|
+
checkUnserializableContext,
|
|
642
|
+
checkDirectObjectSrc,
|
|
643
|
+
checkFinalWithoutOutput,
|
|
644
|
+
checkFinalOutputReadsEvent,
|
|
645
|
+
checkMissingFinal
|
|
646
|
+
];
|
|
647
|
+
/**
|
|
648
|
+
* Runs static structural checks over a built agent machine and returns the
|
|
649
|
+
* findings ({@link AgentLintDiagnostic}[], empty when clean). Works for
|
|
650
|
+
* TS-authored (`setupAgent(...).createMachine(...)`) and
|
|
651
|
+
* `setupAgent.fromConfig(...)`-compiled machines alike, reading `machine.config`
|
|
652
|
+
* plus the schemas/actor sources the library already retains per machine.
|
|
653
|
+
*
|
|
654
|
+
* No model calls, no API keys — a coding agent that emits an agent machine can
|
|
655
|
+
* call this to catch dead states, undeliverable decisions, un-rebindable
|
|
656
|
+
* invoke srcs, and output-contract gaps before ever running it.
|
|
657
|
+
*
|
|
658
|
+
* @example
|
|
659
|
+
* ```ts
|
|
660
|
+
* const errors = lintAgentMachine(machine).filter((d) => d.severity === 'error');
|
|
661
|
+
* if (errors.length) throw new Error(errors.map((e) => `${e.path}: ${e.message}`).join('\n'));
|
|
662
|
+
* ```
|
|
663
|
+
*/
|
|
664
|
+
function lintAgentMachine(machine, options = {}) {
|
|
665
|
+
const config = machine.config ?? {};
|
|
666
|
+
const index = buildStateIndex(config);
|
|
667
|
+
const reachable = computeReachable(config, index);
|
|
668
|
+
const registered = require_decision.getRegisteredAgentExecutionOptions(machine);
|
|
669
|
+
const ctx = {
|
|
670
|
+
machine,
|
|
671
|
+
config,
|
|
672
|
+
index,
|
|
673
|
+
reachable,
|
|
674
|
+
schemas: registered.schemas,
|
|
675
|
+
actorSources: registered.actorSources ?? machine.implementations?.actorSources ?? {}
|
|
676
|
+
};
|
|
677
|
+
const disabled = new Set(options.disable ?? []);
|
|
678
|
+
return LINT_CHECKS.flatMap((check) => check(ctx)).filter((d) => !disabled.has(d.code));
|
|
679
|
+
}
|
|
680
|
+
function pendingInvokes(step) {
|
|
681
|
+
const out = [];
|
|
682
|
+
for (const action of step.actions) {
|
|
683
|
+
const metadata = getInvokeEffectMetadata(action);
|
|
684
|
+
if (typeof metadata?.src === "string" && typeof metadata.id === "string") out.push({
|
|
685
|
+
id: metadata.id,
|
|
686
|
+
src: metadata.src
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
return out;
|
|
690
|
+
}
|
|
691
|
+
function takeFromQueue(channel, src) {
|
|
692
|
+
const queue = channel?.[src];
|
|
693
|
+
if (queue && queue.length > 0) return {
|
|
694
|
+
found: true,
|
|
695
|
+
value: queue.shift()
|
|
696
|
+
};
|
|
697
|
+
return { found: false };
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Deterministically plays a machine through, resolving each request from a
|
|
701
|
+
* {@link SimulationScript} instead of a model — no API keys, no model calls.
|
|
702
|
+
* Runs on the pure step path ({@link initialAgentStep} etc.), so it exercises
|
|
703
|
+
* the real transition logic. Returns the terminal `status`, final `snapshot`,
|
|
704
|
+
* and a `trail` of every step taken.
|
|
705
|
+
*
|
|
706
|
+
* Throws a descriptive error when the script runs dry mid-request, naming the
|
|
707
|
+
* pending request's kind, src, and id so the missing scripted response is
|
|
708
|
+
* obvious.
|
|
709
|
+
*
|
|
710
|
+
* @example
|
|
711
|
+
* ```ts
|
|
712
|
+
* const { status, snapshot } = simulateAgent(machine, {
|
|
713
|
+
* input: { topic: 'state machines' },
|
|
714
|
+
* script: { decisions: { 'agent.decide': [{ type: 'END' }] } },
|
|
715
|
+
* });
|
|
716
|
+
* ```
|
|
717
|
+
*/
|
|
718
|
+
async function simulateAgent(machine, options) {
|
|
719
|
+
const maxSteps = options.maxSteps ?? 100;
|
|
720
|
+
const script = {
|
|
721
|
+
text: { ...options.script.text },
|
|
722
|
+
decisions: mapValues(options.script.decisions ?? {}, (arr) => [...arr]),
|
|
723
|
+
userInput: mapValues(options.script.userInput ?? {}, (arr) => [...arr])
|
|
724
|
+
};
|
|
725
|
+
let step = initialAgentStep(machine, options.input);
|
|
726
|
+
const trail = [];
|
|
727
|
+
for (let i = 0; i < maxSteps; i++) {
|
|
728
|
+
if (step.done) return {
|
|
729
|
+
status: "done",
|
|
730
|
+
snapshot: step.snapshot,
|
|
731
|
+
trail
|
|
732
|
+
};
|
|
733
|
+
const request = step.requests[0];
|
|
734
|
+
if (request) {
|
|
735
|
+
if (request.kind === "decision") {
|
|
736
|
+
const decisionSrc = new Map(pendingInvokes(step).map((invoke) => [invoke.id, invoke.src])).get(request.id) ?? request.id;
|
|
737
|
+
const taken = takeFromQueue(script.decisions, decisionSrc);
|
|
738
|
+
if (!taken.found) throw scriptDryError("decision", decisionSrc, request.id, request);
|
|
739
|
+
step = transitionAgentStep(machine, step, taken.value);
|
|
740
|
+
trail.push({
|
|
741
|
+
state: step.snapshot.value,
|
|
742
|
+
appliedEvent: taken.value
|
|
743
|
+
});
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
if (request.kind === "plan") {
|
|
747
|
+
const taken = takeFromQueue(script.decisions, request.src);
|
|
748
|
+
if (!taken.found) throw scriptDryError("decision", request.src, request.id, request);
|
|
749
|
+
const event = taken.value;
|
|
750
|
+
step = await resolveAgentRequests(machine, step, { decide: async () => ({ event }) });
|
|
751
|
+
trail.push({
|
|
752
|
+
state: step.snapshot.value,
|
|
753
|
+
appliedEvent: taken.value
|
|
754
|
+
});
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
const taken = takeFromQueue(script.text, request.src);
|
|
758
|
+
if (!taken.found) throw scriptDryError("text", request.src, request.id);
|
|
759
|
+
step = resolveAgentStep(machine, step, request, taken.value);
|
|
760
|
+
trail.push({
|
|
761
|
+
state: step.snapshot.value,
|
|
762
|
+
resolvedRequest: {
|
|
763
|
+
kind: "text",
|
|
764
|
+
src: request.src,
|
|
765
|
+
id: request.id
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
const [invoke] = pendingInvokes(step);
|
|
771
|
+
if (invoke) {
|
|
772
|
+
const taken = takeFromQueue(script.userInput, invoke.src);
|
|
773
|
+
if (!taken.found) throw scriptDryError("userInput", invoke.src, invoke.id);
|
|
774
|
+
step = resolveAgentStep(machine, step, invoke.id, taken.value);
|
|
775
|
+
trail.push({
|
|
776
|
+
state: step.snapshot.value,
|
|
777
|
+
resolvedRequest: {
|
|
778
|
+
kind: "userInput",
|
|
779
|
+
src: invoke.src,
|
|
780
|
+
id: invoke.id
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
status: "idle",
|
|
787
|
+
snapshot: step.snapshot,
|
|
788
|
+
trail
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
return {
|
|
792
|
+
status: "exhausted",
|
|
793
|
+
snapshot: step.snapshot,
|
|
794
|
+
trail
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
function mapValues(obj, fn) {
|
|
798
|
+
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, fn(value)]));
|
|
799
|
+
}
|
|
800
|
+
function scriptDryError(kind, src, id, request) {
|
|
801
|
+
const events = request?.kind === "decision" ? ` Candidate events: ${request.events.map((e) => e.type).join(", ") || "(none)"}.` : "";
|
|
802
|
+
return /* @__PURE__ */ new Error(`simulateAgent: script ran dry on a pending ${kind} request for src '${src}' (id '${id}'). Add a '${kind}' entry for '${src}' to the script.${events}`);
|
|
803
|
+
}
|
|
804
|
+
async function explore(machine, options, stopWhen) {
|
|
805
|
+
const maxDepth = options.maxDepth ?? 8;
|
|
806
|
+
const maxPaths = options.maxPaths ?? 200;
|
|
807
|
+
const textOutputs = options.textOutputs ?? {};
|
|
808
|
+
const reachedStates = /* @__PURE__ */ new Set();
|
|
809
|
+
const reachedValues = [];
|
|
810
|
+
const terminals = [];
|
|
811
|
+
const unexplored = [];
|
|
812
|
+
let prunedByGuard = 0;
|
|
813
|
+
let pathsExplored = 0;
|
|
814
|
+
let hitPathCap = false;
|
|
815
|
+
let witness;
|
|
816
|
+
const recordState = (snapshot) => {
|
|
817
|
+
const key = JSON.stringify(snapshot.value);
|
|
818
|
+
if (!reachedStates.has(key)) {
|
|
819
|
+
reachedStates.add(key);
|
|
820
|
+
reachedValues.push(snapshot.value);
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
const initial = initialAgentStep(machine, options.input);
|
|
824
|
+
recordState(initial.snapshot);
|
|
825
|
+
if (stopWhen?.(initial.snapshot)) witness = [];
|
|
826
|
+
const advance = (step) => {
|
|
827
|
+
let current = step;
|
|
828
|
+
for (let i = 0; i < 1e3; i++) {
|
|
829
|
+
if (current.done) return { step: current };
|
|
830
|
+
const request = current.requests[0];
|
|
831
|
+
if (request && request.kind === "text") {
|
|
832
|
+
if (!(request.src in textOutputs)) return {
|
|
833
|
+
step: current,
|
|
834
|
+
blockedSrc: request.src
|
|
835
|
+
};
|
|
836
|
+
current = resolveAgentStep(machine, current, request, textOutputs[request.src]);
|
|
837
|
+
recordState(current.snapshot);
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
if (request && (request.kind === "decision" || request.kind === "plan")) return { step: current };
|
|
841
|
+
const [invoke] = pendingInvokes(current);
|
|
842
|
+
if (invoke) {
|
|
843
|
+
if (!(invoke.src in textOutputs)) return {
|
|
844
|
+
step: current,
|
|
845
|
+
blockedSrc: invoke.src
|
|
846
|
+
};
|
|
847
|
+
current = resolveAgentStep(machine, current, invoke.id, textOutputs[invoke.src]);
|
|
848
|
+
recordState(current.snapshot);
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
return { step: current };
|
|
852
|
+
}
|
|
853
|
+
return { step: current };
|
|
854
|
+
};
|
|
855
|
+
const visit = async (step, path, depth) => {
|
|
856
|
+
if (witness !== void 0) return;
|
|
857
|
+
if (pathsExplored >= maxPaths) {
|
|
858
|
+
hitPathCap = true;
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const { step: settled, blockedSrc } = advance(step);
|
|
862
|
+
if (stopWhen?.(settled.snapshot)) {
|
|
863
|
+
witness = path;
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (blockedSrc) {
|
|
867
|
+
pathsExplored++;
|
|
868
|
+
terminals.push({
|
|
869
|
+
status: "needs-output",
|
|
870
|
+
path,
|
|
871
|
+
state: settled.snapshot.value,
|
|
872
|
+
missingSrc: blockedSrc
|
|
873
|
+
});
|
|
874
|
+
unexplored.push(`needs-output: no canned output for src '${blockedSrc}' at path [${path.map((e) => e.type).join(", ")}]`);
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (settled.done) {
|
|
878
|
+
pathsExplored++;
|
|
879
|
+
terminals.push({
|
|
880
|
+
status: "done",
|
|
881
|
+
path,
|
|
882
|
+
state: settled.snapshot.value
|
|
883
|
+
});
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
const request = settled.requests[0];
|
|
887
|
+
const isPlan = request?.kind === "plan";
|
|
888
|
+
const branchEvents = request?.kind === "decision" || request?.kind === "plan" ? request.events.map((descriptor) => ({ type: descriptor.type })) : require_decision.getAcceptedEvents(settled.snapshot).map((descriptor) => ({ type: descriptor.type }));
|
|
889
|
+
if (branchEvents.length === 0) {
|
|
890
|
+
pathsExplored++;
|
|
891
|
+
terminals.push({
|
|
892
|
+
status: "idle",
|
|
893
|
+
path,
|
|
894
|
+
state: settled.snapshot.value
|
|
895
|
+
});
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (depth >= maxDepth) {
|
|
899
|
+
pathsExplored++;
|
|
900
|
+
terminals.push({
|
|
901
|
+
status: "max-depth",
|
|
902
|
+
path,
|
|
903
|
+
state: settled.snapshot.value
|
|
904
|
+
});
|
|
905
|
+
unexplored.push(`max-depth: stopped at path [${path.map((e) => e.type).join(", ")}]`);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
for (const event of branchEvents) {
|
|
909
|
+
if (witness !== void 0 || pathsExplored >= maxPaths) {
|
|
910
|
+
if (pathsExplored >= maxPaths) hitPathCap = true;
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
if (!(isPlan && event.type === "agent.plan.done") && !settled.snapshot.can(event)) {
|
|
914
|
+
prunedByGuard++;
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
const next = isPlan ? await resolveAgentRequests(machine, settled, { decide: async () => ({ event }) }) : transitionAgentStep(machine, settled, event);
|
|
918
|
+
recordState(next.snapshot);
|
|
919
|
+
await visit(next, [...path, event], depth + 1);
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
if (witness === void 0) await visit(initial, [], 0);
|
|
923
|
+
return {
|
|
924
|
+
report: {
|
|
925
|
+
reachedStates: reachedValues,
|
|
926
|
+
terminals,
|
|
927
|
+
prunedByGuard,
|
|
928
|
+
unexplored,
|
|
929
|
+
pathsExplored,
|
|
930
|
+
hitPathCap
|
|
931
|
+
},
|
|
932
|
+
witness
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Enumerates a machine's decision and external-event branches to a bounded
|
|
937
|
+
* depth, model-free, and reports which states are reached and how each path
|
|
938
|
+
* terminates. At each decision request it forks one branch per candidate event
|
|
939
|
+
* (guard-rejected candidates are counted in `prunedByGuard`, not explored); at
|
|
940
|
+
* an idle wait it forks per externally-accepted event. A `agent.plan` request
|
|
941
|
+
* forks the same way — one branch per candidate, including the reserved
|
|
942
|
+
* `agent.plan.done` move — advancing each branch through the real plan protocol
|
|
943
|
+
* (`resolveAgentRequests`), so a plan can consume several depth units. Text/`userInput` invokes
|
|
944
|
+
* are resolved from `textOutputs` (a by-src canned-output map) — a missing src
|
|
945
|
+
* halts that branch with a `needs-output` terminal rather than throwing.
|
|
946
|
+
*
|
|
947
|
+
* Combinatorics are bounded by `maxDepth` (default 8) and `maxPaths` (default
|
|
948
|
+
* 200, reported via `hitPathCap`).
|
|
949
|
+
*
|
|
950
|
+
* @example
|
|
951
|
+
* ```ts
|
|
952
|
+
* const report = await explorePaths(refundMachine, { input: { request: 'x', amount: 5000 } });
|
|
953
|
+
* // report.terminals → both 'refunded' and 'denied'; report.prunedByGuard → 1
|
|
954
|
+
* ```
|
|
955
|
+
*/
|
|
956
|
+
async function explorePaths(machine, options = {}) {
|
|
957
|
+
return (await explore(machine, options)).report;
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Answers "can the machine reach `statePath`?" by exploring its branches (a
|
|
961
|
+
* thin wrapper over {@link explorePaths}). Returns `{ canReach: true, witness }`
|
|
962
|
+
* with the event sequence that reaches it, or `{ canReach: false }`.
|
|
963
|
+
*
|
|
964
|
+
* @example
|
|
965
|
+
* ```ts
|
|
966
|
+
* const { canReach, witness } = await canReach(refundMachine, 'denied', { input: { request: 'x', amount: 5000 } });
|
|
967
|
+
* // canReach → true; witness → [{ type: 'NEEDS_REVIEW' }, { type: 'DENY' }]
|
|
968
|
+
* ```
|
|
969
|
+
*/
|
|
970
|
+
async function canReach(machine, statePath, options = {}) {
|
|
971
|
+
const { witness } = await explore(machine, options, (snapshot) => {
|
|
972
|
+
try {
|
|
973
|
+
return snapshot.matches(statePath);
|
|
974
|
+
} catch {
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
return witness !== void 0 ? {
|
|
979
|
+
canReach: true,
|
|
980
|
+
witness
|
|
981
|
+
} : { canReach: false };
|
|
982
|
+
}
|
|
983
|
+
//#endregion
|
|
984
|
+
Object.defineProperty(exports, "canReach", {
|
|
985
|
+
enumerable: true,
|
|
986
|
+
get: function() {
|
|
987
|
+
return canReach;
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
Object.defineProperty(exports, "executeAgentRequest", {
|
|
991
|
+
enumerable: true,
|
|
992
|
+
get: function() {
|
|
993
|
+
return executeAgentRequest;
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
Object.defineProperty(exports, "explorePaths", {
|
|
997
|
+
enumerable: true,
|
|
998
|
+
get: function() {
|
|
999
|
+
return explorePaths;
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
Object.defineProperty(exports, "getAgentRequests", {
|
|
1003
|
+
enumerable: true,
|
|
1004
|
+
get: function() {
|
|
1005
|
+
return getAgentRequests;
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
Object.defineProperty(exports, "initialAgentStep", {
|
|
1009
|
+
enumerable: true,
|
|
1010
|
+
get: function() {
|
|
1011
|
+
return initialAgentStep;
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
Object.defineProperty(exports, "lintAgentMachine", {
|
|
1015
|
+
enumerable: true,
|
|
1016
|
+
get: function() {
|
|
1017
|
+
return lintAgentMachine;
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
Object.defineProperty(exports, "resolveAgentRequests", {
|
|
1021
|
+
enumerable: true,
|
|
1022
|
+
get: function() {
|
|
1023
|
+
return resolveAgentRequests;
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
Object.defineProperty(exports, "resolveAgentStep", {
|
|
1027
|
+
enumerable: true,
|
|
1028
|
+
get: function() {
|
|
1029
|
+
return resolveAgentStep;
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
Object.defineProperty(exports, "simulateAgent", {
|
|
1033
|
+
enumerable: true,
|
|
1034
|
+
get: function() {
|
|
1035
|
+
return simulateAgent;
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
Object.defineProperty(exports, "transitionAgentStep", {
|
|
1039
|
+
enumerable: true,
|
|
1040
|
+
get: function() {
|
|
1041
|
+
return transitionAgentStep;
|
|
1042
|
+
}
|
|
1043
|
+
});
|