@statelyai/agent 2.0.0-alpha.11 → 2.0.0-alpha.13
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 +4 -5
- package/dist/ai-sdk.d.cts +7 -4
- package/dist/ai-sdk.d.mts +7 -4
- package/dist/ai-sdk.mjs +1 -2
- package/dist/{events-JiVPYrct.mjs → decision-BezSD_YC.mjs} +327 -20
- package/dist/{events-CRQj3VtP.cjs → decision-dWGhBh0P.cjs} +401 -28
- package/dist/errors-BQRk9eiZ.d.cts +19 -0
- package/dist/errors-C9rxnWbX.d.mts +19 -0
- package/dist/errors-CeSXQx0v.mjs +23 -0
- package/dist/errors-DUBBzRLP.cjs +28 -0
- package/dist/event-log-store-CNT_7F0V.cjs +452 -0
- package/dist/event-log-store-CriMgX1D.d.mts +144 -0
- package/dist/event-log-store-D7pWtIhb.mjs +411 -0
- package/dist/event-log-store-Ruq18mGp.d.cts +144 -0
- package/dist/index.cjs +1050 -705
- package/dist/index.d.cts +538 -565
- package/dist/index.d.mts +538 -565
- package/dist/index.mjs +950 -644
- package/dist/machines.cjs +752 -0
- package/dist/machines.d.cts +372 -0
- package/dist/machines.d.mts +372 -0
- package/dist/machines.mjs +741 -0
- package/dist/otel.cjs +268 -0
- package/dist/otel.d.cts +67 -0
- package/dist/otel.d.mts +67 -0
- package/dist/otel.mjs +267 -0
- package/dist/run-agent-C3mFDGTf.d.mts +1111 -0
- package/dist/run-agent-DnvtcnTZ.d.cts +1111 -0
- package/dist/setup-agent-DAZZSjDS.mjs +1711 -0
- package/dist/setup-agent-DP95MFrI.cjs +1836 -0
- package/dist/sqlite.cjs +135 -0
- package/dist/sqlite.d.cts +57 -0
- package/dist/sqlite.d.mts +57 -0
- package/dist/sqlite.mjs +133 -0
- package/dist/{text-logic-CaKqgX4Y.d.mts → text-logic-BDxwQNsD.d.cts} +155 -72
- package/dist/{text-logic-Ckhr2kKC.d.cts → text-logic-TkKPw8Aq.d.mts} +155 -72
- package/dist/{types-qm00QF91.d.mts → types-QbEfCVny.d.cts} +1 -1
- package/dist/{types-C9QiMjre.d.cts → types-_FXoFBGO.d.mts} +1 -1
- package/package.json +47 -39
- package/readme.md +49 -12
- package/schemas/agent-workflow.json +40 -21
- package/skills/generate-machine/SKILL.md +267 -0
- package/dist/adapter.cjs +0 -15
- package/dist/adapter.d.cts +0 -4
- package/dist/adapter.d.mts +0 -4
- package/dist/adapter.mjs +0 -2
- package/dist/decision-C3k4ve51.mjs +0 -227
- package/dist/decision-D8wJrM8W.cjs +0 -286
- package/dist/openai-compat.cjs +0 -309
- package/dist/openai-compat.d.cts +0 -59
- package/dist/openai-compat.d.mts +0 -59
- package/dist/openai-compat.mjs +0 -308
- package/dist/steps-BALp1eZo.d.mts +0 -198
- package/dist/steps-CVe54GPP.cjs +0 -420
- package/dist/steps-CkyyyuHd.mjs +0 -379
- package/dist/steps-MjnQI4aB.d.cts +0 -198
- package/dist/steps.cjs +0 -12
- package/dist/steps.d.cts +0 -3
- package/dist/steps.d.mts +0 -3
- package/dist/steps.mjs +0 -3
- package/dist/utils-BYqT_Dyv.d.cts +0 -108
- package/dist/utils-Do5wIJrh.d.mts +0 -108
- package/dist/zod.cjs +0 -31
- package/dist/zod.d.cts +0 -30
- package/dist/zod.d.mts +0 -30
- package/dist/zod.mjs +0 -30
package/dist/index.mjs
CHANGED
|
@@ -1,565 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
function addMessages(resolve) {
|
|
7
|
-
return (args) => {
|
|
8
|
-
const resolved = typeof resolve === "function" ? resolve(args) : resolve;
|
|
9
|
-
return [...args.context.messages, ...Array.isArray(resolved) ? resolved : [resolved]];
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Builds a transition-function result that appends one or more
|
|
14
|
-
* {@link AgentMessage}s to a context's `messages` array. `resolve` is either
|
|
15
|
-
* a message (or array of messages) or a function of `{ context, event }`
|
|
16
|
-
* returning them; the returned function is meant to be used directly as (or
|
|
17
|
-
* composed into) a transition's result, e.g. `on: { USER_REPLIED:
|
|
18
|
-
* agent.appendMessages(({ event }) => userMessage(event.text)) }`. Requires
|
|
19
|
-
* `messages: AgentMessage[]` on context — see {@link messagesSchema} for a
|
|
20
|
-
* ready-made schema for that field.
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* ```ts
|
|
24
|
-
* on: {
|
|
25
|
-
* USER_REPLIED: appendMessages(({ event }) => userMessage(event.text)),
|
|
26
|
-
* }
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
function appendMessages(resolve) {
|
|
30
|
-
return (args) => ({ context: { messages: addMessages(resolve)(args) } });
|
|
31
|
-
}
|
|
32
|
-
const KNOWN_PART_TYPES = new Set([
|
|
33
|
-
"text",
|
|
34
|
-
"image",
|
|
35
|
-
"file",
|
|
36
|
-
"tool-call",
|
|
37
|
-
"tool-result"
|
|
38
|
-
]);
|
|
39
|
-
function isKnownPart(part) {
|
|
40
|
-
return !!part && typeof part === "object" && KNOWN_PART_TYPES.has(part.type);
|
|
41
|
-
}
|
|
42
|
-
function validatePartsArray(content) {
|
|
43
|
-
if (!Array.isArray(content)) return "Expected content to be a string or an array of parts";
|
|
44
|
-
for (const part of content) if (!isKnownPart(part)) {
|
|
45
|
-
const type = part && typeof part === "object" ? part.type : void 0;
|
|
46
|
-
return `Unknown message part type: ${JSON.stringify(type)}`;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* A {@link StandardSchemaV1} validating an `AgentMessage[]` context field —
|
|
51
|
-
* checks that every message has a known `role` (`system`/`user`/`assistant`/
|
|
52
|
-
* `tool`) and that `content` is either a string (where the role allows it) or
|
|
53
|
-
* an array of parts with a known `type`. Use it directly as a context
|
|
54
|
-
* schema's `messages` field when authoring with `createAgentSchemas`.
|
|
55
|
-
*/
|
|
56
|
-
const messagesSchema = { "~standard": {
|
|
57
|
-
version: 1,
|
|
58
|
-
vendor: "statelyai-agent",
|
|
59
|
-
validate(value) {
|
|
60
|
-
if (!Array.isArray(value)) return { issues: [{ message: "Expected an array of agent messages" }] };
|
|
61
|
-
for (const message of value) {
|
|
62
|
-
if (!message || typeof message !== "object") return { issues: [{ message: "Expected an array of agent messages" }] };
|
|
63
|
-
const role = message.role;
|
|
64
|
-
const content = message.content;
|
|
65
|
-
if (role !== "system" && role !== "user" && role !== "assistant" && role !== "tool") return { issues: [{ message: `Unknown message role: ${JSON.stringify(role)}` }] };
|
|
66
|
-
if (role === "system") {
|
|
67
|
-
if (typeof content !== "string") return { issues: [{ message: "system message content must be a string" }] };
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
if (role === "tool") {
|
|
71
|
-
const error = validatePartsArray(content) ?? (content.some((part) => part.type !== "tool-result") ? "tool message content must contain only tool-result parts" : void 0);
|
|
72
|
-
if (error) return { issues: [{ message: error }] };
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
if (typeof content === "string") continue;
|
|
76
|
-
const error = validatePartsArray(content);
|
|
77
|
-
if (error) return { issues: [{ message: error }] };
|
|
78
|
-
}
|
|
79
|
-
return { value };
|
|
80
|
-
}
|
|
81
|
-
} };
|
|
82
|
-
//#endregion
|
|
83
|
-
//#region src/workflow-config.ts
|
|
84
|
-
const DECIDE_SRC$1 = "agent.decide";
|
|
85
|
-
const workflowConfigWholeExpressionPattern = /^\{\{\s*([\s\S]*?)\s*\}\}$/;
|
|
86
|
-
const workflowConfigTemplateExpressionPattern = /\{\{\s*([\s\S]*?)\s*\}\}/g;
|
|
87
|
-
function evaluateWorkflowConfigPath(expression, scope) {
|
|
88
|
-
const parts = expression.trim().split(".").filter(Boolean);
|
|
89
|
-
let current = scope;
|
|
90
|
-
for (const part of parts) {
|
|
91
|
-
if (!current || typeof current !== "object") return;
|
|
92
|
-
current = current[part];
|
|
93
|
-
}
|
|
94
|
-
return current;
|
|
95
|
-
}
|
|
96
|
-
function evaluateWorkflowConfigValue(value, scope) {
|
|
97
|
-
if (typeof value === "string") {
|
|
98
|
-
const wholeMatch = value.match(workflowConfigWholeExpressionPattern);
|
|
99
|
-
if (wholeMatch?.[1]) return evaluateWorkflowConfigPath(wholeMatch[1], scope);
|
|
100
|
-
return value.replace(workflowConfigTemplateExpressionPattern, (_match, expression) => {
|
|
101
|
-
const resolved = evaluateWorkflowConfigPath(expression, scope);
|
|
102
|
-
return resolved === void 0 || resolved === null ? "" : String(resolved);
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
if (Array.isArray(value)) return value.map((item) => evaluateWorkflowConfigValue(item, scope));
|
|
106
|
-
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, evaluateWorkflowConfigValue(item, scope)]));
|
|
107
|
-
return value;
|
|
108
|
-
}
|
|
109
|
-
function createSchemasFromWorkflowConfig(config, compileSchema) {
|
|
110
|
-
return createAgentSchemas({
|
|
111
|
-
context: compileSchema(config.schemas?.context ?? { type: "object" }, "context"),
|
|
112
|
-
events: Object.fromEntries(Object.entries(config.schemas?.events ?? {}).map(([key, schema]) => [key, compileSchema(schema, `event.${key}`)])),
|
|
113
|
-
emitted: Object.fromEntries(Object.entries(config.schemas?.emitted ?? {}).map(([key, schema]) => [key, compileSchema(schema, `emitted.${key}`)])),
|
|
114
|
-
input: compileSchema(config.schemas?.input ?? {}, "input"),
|
|
115
|
-
output: compileSchema(config.schemas?.output ?? {}, "output"),
|
|
116
|
-
meta: compileSchema(config.schemas?.meta ?? {}, "meta")
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
function createRequestsFromWorkflowConfig(config, compileSchema) {
|
|
120
|
-
return Object.fromEntries(Object.entries(config.requests ?? {}).map(([key, request]) => [key, {
|
|
121
|
-
mode: request.mode,
|
|
122
|
-
description: request.description,
|
|
123
|
-
schemas: {
|
|
124
|
-
input: compileSchema(request.input, `${key}.input`),
|
|
125
|
-
output: compileSchema(request.output, `${key}.output`)
|
|
126
|
-
},
|
|
127
|
-
model: ({ input }) => String(evaluateWorkflowConfigValue(request.model, { input }) ?? ""),
|
|
128
|
-
system: request.system === void 0 ? void 0 : ({ input }) => evaluateWorkflowConfigValue(request.system, { input }),
|
|
129
|
-
prompt: request.prompt === void 0 ? void 0 : ({ input }) => evaluateWorkflowConfigValue(request.prompt, { input }),
|
|
130
|
-
messages: request.messages === void 0 ? void 0 : ({ input }) => evaluateWorkflowConfigValue(request.messages, { input }),
|
|
131
|
-
tools: request.tools,
|
|
132
|
-
toolChoice: request.toolChoice,
|
|
133
|
-
reasoning: request.reasoning,
|
|
134
|
-
temperature: request.temperature === void 0 ? void 0 : ({ input }) => evaluateWorkflowConfigValue(request.temperature, { input }),
|
|
135
|
-
maxOutputTokens: request.maxOutputTokens === void 0 ? void 0 : ({ input }) => evaluateWorkflowConfigValue(request.maxOutputTokens, { input }),
|
|
136
|
-
topP: request.topP === void 0 ? void 0 : ({ input }) => evaluateWorkflowConfigValue(request.topP, { input }),
|
|
137
|
-
topK: request.topK === void 0 ? void 0 : ({ input }) => evaluateWorkflowConfigValue(request.topK, { input }),
|
|
138
|
-
seed: request.seed === void 0 ? void 0 : ({ input }) => evaluateWorkflowConfigValue(request.seed, { input }),
|
|
139
|
-
stopSequences: request.stopSequences === void 0 ? void 0 : ({ input }) => evaluateWorkflowConfigValue(request.stopSequences, { input }),
|
|
140
|
-
metadata: request.metadata === void 0 ? void 0 : ({ input }) => evaluateWorkflowConfigValue(request.metadata, { input })
|
|
141
|
-
}]));
|
|
142
|
-
}
|
|
143
|
-
function createActorPlaceholdersFromWorkflowConfig(config) {
|
|
144
|
-
return Object.fromEntries(Object.keys(config.actors ?? {}).map((key) => [key, missingActor(key)]));
|
|
145
|
-
}
|
|
146
|
-
function createAssignAction(assignConfig) {
|
|
147
|
-
return ({ context, event }) => ({ context: Object.fromEntries(Object.entries(assignConfig).map(([key, value]) => [key, evaluateWorkflowConfigValue(value, {
|
|
148
|
-
context,
|
|
149
|
-
event
|
|
150
|
-
})])) });
|
|
151
|
-
}
|
|
152
|
-
function lowerWorkflowActions(actionConfig) {
|
|
153
|
-
if (!actionConfig) return;
|
|
154
|
-
const lowered = (Array.isArray(actionConfig) ? actionConfig : [actionConfig]).map((action) => {
|
|
155
|
-
if (action.assign !== void 0) return createAssignAction(action.assign);
|
|
156
|
-
if (action.emit !== void 0) return ({ context, event }, enq) => {
|
|
157
|
-
enq.emit(evaluateWorkflowConfigValue(action.emit, {
|
|
158
|
-
context,
|
|
159
|
-
event
|
|
160
|
-
}));
|
|
161
|
-
};
|
|
162
|
-
if (!action.type) throw new Error("setupAgent.fromConfig: action must declare 'type', 'assign', or 'emit'.");
|
|
163
|
-
return {
|
|
164
|
-
type: action.type,
|
|
165
|
-
params: ({ context, event }) => evaluateWorkflowConfigValue(action.params, {
|
|
166
|
-
context,
|
|
167
|
-
event
|
|
168
|
-
})
|
|
169
|
-
};
|
|
170
|
-
});
|
|
171
|
-
return Array.isArray(actionConfig) ? lowered : lowered[0];
|
|
172
|
-
}
|
|
173
|
-
function workflowTransitionMatches(transitionConfig, scope) {
|
|
174
|
-
if (transitionConfig.guard === void 0) return true;
|
|
175
|
-
if (typeof transitionConfig.guard === "string") return Boolean(evaluateWorkflowConfigValue(transitionConfig.guard, scope));
|
|
176
|
-
return typeof transitionConfig.guard === "function" ? transitionConfig.guard(scope) : false;
|
|
177
|
-
}
|
|
178
|
-
function applyWorkflowTransitionActions(actionConfig, scope, enq) {
|
|
179
|
-
if (!actionConfig) return;
|
|
180
|
-
const actions = Array.isArray(actionConfig) ? actionConfig : [actionConfig];
|
|
181
|
-
let assigned;
|
|
182
|
-
for (const action of actions) if (action.assign !== void 0) assigned = {
|
|
183
|
-
...assigned ?? {},
|
|
184
|
-
...Object.fromEntries(Object.entries(action.assign).map(([key, value]) => [key, evaluateWorkflowConfigValue(value, scope)]))
|
|
185
|
-
};
|
|
186
|
-
else if (action.emit !== void 0) enq.emit(evaluateWorkflowConfigValue(action.emit, scope));
|
|
187
|
-
else if (action.type) throw new Error(`setupAgent.fromConfig: named action type '${action.type}' is not supported in a transition 'actions' entry (config lowering wires no action implementations). Use 'emit' or 'assign', or move it to a state 'entry'/'exit'.`);
|
|
188
|
-
else throw new Error("setupAgent.fromConfig: a transition action must declare 'emit', 'assign', or 'type'.");
|
|
189
|
-
return assigned;
|
|
190
|
-
}
|
|
191
|
-
function lowerWorkflowTransitionResult(transitionConfig, scope, enq) {
|
|
192
|
-
const actionAssign = applyWorkflowTransitionActions(transitionConfig.actions, scope, enq);
|
|
193
|
-
const configAssign = transitionConfig.assign ? Object.fromEntries(Object.entries(transitionConfig.assign).map(([key, value]) => [key, evaluateWorkflowConfigValue(value, scope)])) : void 0;
|
|
194
|
-
const context = configAssign || actionAssign ? {
|
|
195
|
-
...configAssign ?? {},
|
|
196
|
-
...actionAssign ?? {}
|
|
197
|
-
} : void 0;
|
|
198
|
-
return {
|
|
199
|
-
...transitionConfig.target !== void 0 ? { target: transitionConfig.target } : {},
|
|
200
|
-
...context ? { context } : {},
|
|
201
|
-
...transitionConfig.description !== void 0 ? { description: transitionConfig.description } : {},
|
|
202
|
-
...transitionConfig.reenter !== void 0 ? { reenter: transitionConfig.reenter } : {},
|
|
203
|
-
...transitionConfig.meta !== void 0 ? { meta: transitionConfig.meta } : {}
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
function lowerWorkflowTransition(transitionConfig) {
|
|
207
|
-
return ({ context, event }, enq) => {
|
|
208
|
-
const scope = {
|
|
209
|
-
context,
|
|
210
|
-
event
|
|
211
|
-
};
|
|
212
|
-
if (!workflowTransitionMatches(transitionConfig, scope)) return;
|
|
213
|
-
return lowerWorkflowTransitionResult(transitionConfig, scope, enq);
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
function lowerWorkflowTransitionOrArray(transitionConfig) {
|
|
217
|
-
if (!transitionConfig) return;
|
|
218
|
-
return Array.isArray(transitionConfig) ? ({ context, event }, enq) => {
|
|
219
|
-
const scope = {
|
|
220
|
-
context,
|
|
221
|
-
event
|
|
222
|
-
};
|
|
223
|
-
const transition = transitionConfig.find((candidate) => workflowTransitionMatches(candidate, scope));
|
|
224
|
-
if (!transition) return;
|
|
225
|
-
return lowerWorkflowTransitionResult(transition, scope, enq);
|
|
226
|
-
} : lowerWorkflowTransition(transitionConfig);
|
|
227
|
-
}
|
|
228
|
-
function lowerWorkflowInvoke(invokeConfig, stateKey) {
|
|
229
|
-
if (invokeConfig.src === DECIDE_SRC$1 && invokeConfig.onDone !== void 0) throw new Error(`setupAgent.fromConfig: state '${stateKey}' declares an 'onDone' on its 'agent.decide' invoke, which is always a config error. A decision has no output of its own — the chosen event is delivered automatically and handled by the state's 'on' transitions. Remove the 'onDone' (use 'on' for the chosen event; 'onError' still handles retries exhausted).`);
|
|
230
|
-
return {
|
|
231
|
-
...invokeConfig.id !== void 0 ? { id: invokeConfig.id } : {},
|
|
232
|
-
src: invokeConfig.src,
|
|
233
|
-
...invokeConfig.input !== void 0 ? { input: ({ context, event }) => evaluateWorkflowConfigValue(invokeConfig.input, {
|
|
234
|
-
context,
|
|
235
|
-
event
|
|
236
|
-
}) } : {},
|
|
237
|
-
...invokeConfig.onDone !== void 0 ? { onDone: lowerWorkflowTransitionOrArray(invokeConfig.onDone) } : {},
|
|
238
|
-
...invokeConfig.onError !== void 0 ? { onError: lowerWorkflowTransitionOrArray(invokeConfig.onError) } : {},
|
|
239
|
-
...invokeConfig.meta !== void 0 ? { meta: invokeConfig.meta } : {}
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
function assertValidTransitionTargets(stateKey, location, transitionConfig, siblingKeys) {
|
|
243
|
-
if (!transitionConfig) return;
|
|
244
|
-
const transitions = Array.isArray(transitionConfig) ? transitionConfig : [transitionConfig];
|
|
245
|
-
for (const transition of transitions) {
|
|
246
|
-
const targets = transition.target === void 0 ? [] : Array.isArray(transition.target) ? transition.target : [transition.target];
|
|
247
|
-
for (const target of targets) {
|
|
248
|
-
if (typeof target !== "string" || target.startsWith("#") || target.startsWith(".")) continue;
|
|
249
|
-
const head = target.split(".")[0];
|
|
250
|
-
if (!siblingKeys.has(head)) throw new Error(`setupAgent.fromConfig: state '${stateKey}' has a ${location} transition targeting '${target}', which is not a state at that level. Valid targets: ${[...siblingKeys].join(", ") || "(none)"}.`);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
function assertStateTransitionTargets(stateKey, stateConfig, siblingKeys) {
|
|
255
|
-
for (const [eventType, transition] of Object.entries(stateConfig.on ?? {})) assertValidTransitionTargets(stateKey, `'on.${eventType}'`, transition, siblingKeys);
|
|
256
|
-
for (const [delay, transition] of Object.entries(stateConfig.after ?? {})) assertValidTransitionTargets(stateKey, `'after.${delay}'`, transition, siblingKeys);
|
|
257
|
-
assertValidTransitionTargets(stateKey, "'always'", stateConfig.always, siblingKeys);
|
|
258
|
-
assertValidTransitionTargets(stateKey, "'onDone'", stateConfig.onDone, siblingKeys);
|
|
259
|
-
assertValidTransitionTargets(stateKey, "'choice'", stateConfig.choice, siblingKeys);
|
|
260
|
-
const invokes = stateConfig.invoke ? Array.isArray(stateConfig.invoke) ? stateConfig.invoke : [stateConfig.invoke] : [];
|
|
261
|
-
for (const invoke of invokes) {
|
|
262
|
-
assertValidTransitionTargets(stateKey, "invoke 'onDone'", invoke.onDone, siblingKeys);
|
|
263
|
-
assertValidTransitionTargets(stateKey, "invoke 'onError'", invoke.onError, siblingKeys);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
function lowerWorkflowState(stateKey, stateConfig, siblingKeys) {
|
|
267
|
-
assertStateTransitionTargets(stateKey, stateConfig, siblingKeys);
|
|
268
|
-
const childKeys = new Set(Object.keys(stateConfig.states ?? {}));
|
|
269
|
-
return {
|
|
270
|
-
...stateConfig.description !== void 0 ? { description: stateConfig.description } : {},
|
|
271
|
-
...stateConfig.type !== void 0 ? { type: stateConfig.type } : {},
|
|
272
|
-
...stateConfig.initial !== void 0 ? { initial: stateConfig.initial } : {},
|
|
273
|
-
...stateConfig.states !== void 0 ? { states: Object.fromEntries(Object.entries(stateConfig.states).map(([key, child]) => [key, lowerWorkflowState(key, child, childKeys)])) } : {},
|
|
274
|
-
...stateConfig.choice !== void 0 ? { choice: lowerWorkflowTransitionOrArray(stateConfig.choice) } : {},
|
|
275
|
-
...stateConfig.invoke !== void 0 ? { invoke: Array.isArray(stateConfig.invoke) ? stateConfig.invoke.map((invoke) => lowerWorkflowInvoke(invoke, stateKey)) : lowerWorkflowInvoke(stateConfig.invoke, stateKey) } : {},
|
|
276
|
-
...stateConfig.on !== void 0 ? { on: Object.fromEntries(Object.entries(stateConfig.on).map(([eventType, transitionConfig]) => [eventType, lowerWorkflowTransitionOrArray(transitionConfig)])) } : {},
|
|
277
|
-
...stateConfig.always !== void 0 ? { always: lowerWorkflowTransitionOrArray(stateConfig.always) } : {},
|
|
278
|
-
...stateConfig.onDone !== void 0 ? { onDone: lowerWorkflowTransitionOrArray(stateConfig.onDone) } : {},
|
|
279
|
-
...stateConfig.after !== void 0 ? { after: Object.fromEntries(Object.entries(stateConfig.after).map(([delay, transitionConfig]) => [delay, lowerWorkflowTransitionOrArray(transitionConfig)])) } : {},
|
|
280
|
-
...stateConfig.entry !== void 0 ? { entry: lowerWorkflowActions(stateConfig.entry) } : {},
|
|
281
|
-
...stateConfig.exit !== void 0 ? { exit: lowerWorkflowActions(stateConfig.exit) } : {},
|
|
282
|
-
...stateConfig.tags !== void 0 ? { tags: stateConfig.tags } : {},
|
|
283
|
-
...stateConfig.output !== void 0 ? { output: ({ context, event }) => evaluateWorkflowConfigValue(stateConfig.output, {
|
|
284
|
-
context,
|
|
285
|
-
event
|
|
286
|
-
}) } : {},
|
|
287
|
-
...stateConfig.meta !== void 0 ? { meta: stateConfig.meta } : {}
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
function setupAgentFromConfig(config, options) {
|
|
291
|
-
if (!options || typeof options.compileSchema !== "function") throw new Error("setupAgent.fromConfig(...) requires a 'compileSchema' option: { compileSchema: (jsonSchema, name) => StandardSchemaV1 }. Bring your own JSON Schema engine (Ajv, @cfworker/json-schema, a compiled-Zod-from-JSON-Schema pipeline, ...). Core intentionally ships no JSON Schema engine.");
|
|
292
|
-
const { compileSchema } = options;
|
|
293
|
-
const schemas = createSchemasFromWorkflowConfig(config, compileSchema);
|
|
294
|
-
const requestActors = createRequestActors(createRequestsFromWorkflowConfig(config, compileSchema));
|
|
295
|
-
return setupAgent({
|
|
296
|
-
schemas,
|
|
297
|
-
actorSources: {
|
|
298
|
-
...createActorPlaceholdersFromWorkflowConfig(config),
|
|
299
|
-
...requestActors
|
|
300
|
-
}
|
|
301
|
-
}).createMachine({
|
|
302
|
-
...config.id !== void 0 ? { id: config.id } : {},
|
|
303
|
-
...config.description !== void 0 ? { description: config.description } : {},
|
|
304
|
-
...config.context !== void 0 ? { context: ({ input }) => validateSchemaSync(schemas.context, evaluateWorkflowConfigValue(config.context, { input })) } : {},
|
|
305
|
-
initial: config.initial,
|
|
306
|
-
states: (() => {
|
|
307
|
-
const rootKeys = new Set(Object.keys(config.states));
|
|
308
|
-
return Object.fromEntries(Object.entries(config.states).map(([key, state]) => [key, lowerWorkflowState(key, state, rootKeys)]));
|
|
309
|
-
})(),
|
|
310
|
-
...config.meta !== void 0 ? { meta: config.meta } : {}
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
//#endregion
|
|
314
|
-
//#region src/setup-agent.ts
|
|
315
|
-
const emptyEventSchema = { "~standard": {
|
|
316
|
-
version: 1,
|
|
317
|
-
vendor: "statelyai-agent",
|
|
318
|
-
validate(value) {
|
|
319
|
-
return value !== null && typeof value === "object" && Object.keys(value).length === 0 ? { value: {} } : { issues: [{ message: "Expected an empty event payload" }] };
|
|
320
|
-
}
|
|
321
|
-
} };
|
|
322
|
-
function normalizeEventSchemas(events) {
|
|
323
|
-
return Object.fromEntries(Object.entries(events).map(([type, schema]) => [type, schema && typeof schema === "object" && "~standard" in schema ? schema : emptyEventSchema]));
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Builds a machine's {@link AgentSchemaPack} from a partial schema
|
|
327
|
-
* declaration — only `context` is required; `events`/`input`/`output`/`meta`
|
|
328
|
-
* default to empty/unknown schemas when omitted. Pass the result as
|
|
329
|
-
* `setupAgent({ schemas })`'s `schemas` (or spread the individual fields
|
|
330
|
-
* directly into `setupAgent({ context, events, ... })` — both forms are
|
|
331
|
-
* accepted).
|
|
332
|
-
*/
|
|
333
|
-
function createAgentSchemas(schemas) {
|
|
334
|
-
return {
|
|
335
|
-
context: schemas.context,
|
|
336
|
-
events: normalizeEventSchemas(schemas.events ?? {}),
|
|
337
|
-
input: schemas.input,
|
|
338
|
-
output: schemas.output,
|
|
339
|
-
meta: schemas.meta,
|
|
340
|
-
emitted: schemas.emitted
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
function mergeContextSchema(base, fields) {
|
|
344
|
-
return { "~standard": {
|
|
345
|
-
version: 1,
|
|
346
|
-
vendor: "statelyai-agent",
|
|
347
|
-
validate(value) {
|
|
348
|
-
const baseResult = base["~standard"].validate(value);
|
|
349
|
-
if (baseResult instanceof Promise) throw new Error("setupAgent: async context schemas are not supported.");
|
|
350
|
-
if (baseResult.issues) return baseResult;
|
|
351
|
-
const merged = { ...baseResult.value };
|
|
352
|
-
const issues = [];
|
|
353
|
-
for (const [key, fieldSchema] of Object.entries(fields)) {
|
|
354
|
-
const fieldResult = fieldSchema["~standard"].validate(value[key]);
|
|
355
|
-
if (fieldResult instanceof Promise) throw new Error("setupAgent: async context schemas are not supported.");
|
|
356
|
-
if (fieldResult.issues) issues.push(...fieldResult.issues.map((issue) => ({
|
|
357
|
-
...issue,
|
|
358
|
-
path: [key, ...issue.path ?? []]
|
|
359
|
-
})));
|
|
360
|
-
else merged[key] = fieldResult.value;
|
|
361
|
-
}
|
|
362
|
-
return issues.length > 0 ? { issues } : { value: merged };
|
|
363
|
-
}
|
|
364
|
-
} };
|
|
365
|
-
}
|
|
366
|
-
function resolveAgentStateSchemas(contextSchema, states) {
|
|
367
|
-
return Object.fromEntries(Object.entries(states).map(([key, state]) => {
|
|
368
|
-
if (!state || typeof state !== "object") return [key, state];
|
|
369
|
-
const children = "states" in state && state.states ? resolveAgentStateSchemas(contextSchema, state.states) : void 0;
|
|
370
|
-
if ("context" in state && state.context) return [key, {
|
|
371
|
-
schemas: { context: mergeContextSchema(contextSchema, state.context) },
|
|
372
|
-
...children ? { states: children } : {}
|
|
373
|
-
}];
|
|
374
|
-
return [key, children ? {
|
|
375
|
-
...state,
|
|
376
|
-
states: children
|
|
377
|
-
} : state];
|
|
378
|
-
}));
|
|
379
|
-
}
|
|
380
|
-
/**
|
|
381
|
-
* Schema-first `setup(...)` for agent machines — the standard entry point
|
|
382
|
-
* for authoring a machine (the blueprint) that this library then runs (via
|
|
383
|
-
* {@link runAgent} or the step helpers) against host-supplied model/decision
|
|
384
|
-
* executors. Context, events, machine input, machine output, and
|
|
385
|
-
* state/transition meta are all standard schemas — no `{} as Type` casts —
|
|
386
|
-
* and are retained on `result.schemas` for runtime validation. Also
|
|
387
|
-
* registers the `agent.generateText`/`agent.streamText`/`agent.userInput`/
|
|
388
|
-
* `agent.decide`/`agent.plan` builtin actors and lowers `requests`/`actorSources` into the
|
|
389
|
-
* machine's actor sources. The result is the xstate `setup(...)` object with
|
|
390
|
-
* a wrapped `result.createMachine(...)` plus `result.schemas`/`models`/
|
|
391
|
-
* `requests`/`appendMessages` attached. Also has a
|
|
392
|
-
* `setupAgent.fromConfig(...)` namespace member for building a machine from
|
|
393
|
-
* a serializable {@link AgentWorkflowConfig} instead of this TS API.
|
|
394
|
-
*
|
|
395
|
-
* @example
|
|
396
|
-
* ```ts
|
|
397
|
-
* const schemas = createAgentSchemas({
|
|
398
|
-
* context: z.object({ topic: z.string(), joke: z.string().nullable() }),
|
|
399
|
-
* input: z.object({ topic: z.string() }),
|
|
400
|
-
* output: z.object({ joke: z.string() }),
|
|
401
|
-
* });
|
|
402
|
-
*
|
|
403
|
-
* const agent = setupAgent({
|
|
404
|
-
* schemas,
|
|
405
|
-
* actorSources: { tellJoke },
|
|
406
|
-
* });
|
|
407
|
-
*
|
|
408
|
-
* const jokeMachine = agent.createMachine({
|
|
409
|
-
* context: ({ input }) => ({ topic: input.topic, joke: null }),
|
|
410
|
-
* initial: 'telling',
|
|
411
|
-
* states: {
|
|
412
|
-
* telling: {
|
|
413
|
-
* invoke: {
|
|
414
|
-
* id: 'joke',
|
|
415
|
-
* src: 'tellJoke',
|
|
416
|
-
* input: ({ context }) => ({ topic: context.topic }),
|
|
417
|
-
* onDone: ({ output }) => ({ target: 'done', context: { joke: output } }),
|
|
418
|
-
* },
|
|
419
|
-
* },
|
|
420
|
-
* done: { type: 'final', output: ({ context }) => ({ joke: context.joke ?? '' }) },
|
|
421
|
-
* },
|
|
422
|
-
* });
|
|
423
|
-
* ```
|
|
424
|
-
*/
|
|
425
|
-
function setupAgent(config) {
|
|
426
|
-
return createSetupAgent(config);
|
|
427
|
-
}
|
|
428
|
-
function collectFinalStateOutputs(states, outputs = []) {
|
|
429
|
-
for (const state of Object.values(states ?? {})) {
|
|
430
|
-
if (state?.type === "final" && state.output !== void 0) outputs.push(state.output);
|
|
431
|
-
collectFinalStateOutputs(state?.states, outputs);
|
|
432
|
-
}
|
|
433
|
-
return outputs;
|
|
434
|
-
}
|
|
435
|
-
function withRootOutputFromSingleFinal(config) {
|
|
436
|
-
if (!config || typeof config !== "object" || "output" in config || !("states" in config)) return config;
|
|
437
|
-
const outputs = collectFinalStateOutputs(config.states);
|
|
438
|
-
return outputs.length === 1 ? {
|
|
439
|
-
...config,
|
|
440
|
-
output: outputs[0]
|
|
441
|
-
} : config;
|
|
442
|
-
}
|
|
443
|
-
(function(_setupAgent) {
|
|
444
|
-
function fromConfig(config, options) {
|
|
445
|
-
return setupAgentFromConfig(config, options);
|
|
446
|
-
}
|
|
447
|
-
_setupAgent.fromConfig = fromConfig;
|
|
448
|
-
})(setupAgent || (setupAgent = {}));
|
|
449
|
-
/** Builds one TextLogic actor per `setupAgent({ requests })` entry. @internal */
|
|
450
|
-
function createRequestActors(requests) {
|
|
451
|
-
return Object.fromEntries(Object.entries(requests).map(([key, request]) => {
|
|
452
|
-
return [key, createTextLogic({
|
|
453
|
-
...request,
|
|
454
|
-
name: key,
|
|
455
|
-
mode: request.mode ?? "generate"
|
|
456
|
-
})];
|
|
457
|
-
}));
|
|
458
|
-
}
|
|
459
|
-
function normalizeAgentSchemas(config) {
|
|
460
|
-
if ("schemas" in config && config.schemas) return config.schemas;
|
|
461
|
-
const loose = config;
|
|
462
|
-
return createAgentSchemas({
|
|
463
|
-
...loose,
|
|
464
|
-
context: loose.context
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
function normalizeAgentRequestInput(requests) {
|
|
468
|
-
return requests ?? {};
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* Runtime guard: a key appearing in both `actorSources`/`requests` is almost
|
|
472
|
-
* certainly a mistake (whichever spread applies last would silently win) —
|
|
473
|
-
* fail fast with a clear message rather than let one implementation shadow
|
|
474
|
-
* another.
|
|
475
|
-
*/
|
|
476
|
-
function assertNoActorKeyCollisions(actorSources, requests) {
|
|
477
|
-
const seenIn = /* @__PURE__ */ new Map();
|
|
478
|
-
const groups = [["actorSources", actorSources], ["requests", requests]];
|
|
479
|
-
for (const [groupName, group] of groups) for (const key of Object.keys(group ?? {})) {
|
|
480
|
-
const existingGroup = seenIn.get(key);
|
|
481
|
-
if (existingGroup) throw new Error(`setupAgent: key '${key}' is defined in both '${existingGroup}' and '${groupName}'. Each actor source key must be unique across 'actorSources' and 'requests'.`);
|
|
482
|
-
seenIn.set(key, groupName);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
const RESERVED_AGENT_ACTOR_KEYS = [
|
|
486
|
-
...Object.keys(builtinTextActors),
|
|
487
|
-
USER_INPUT_ACTOR,
|
|
488
|
-
DECIDE_ACTOR,
|
|
489
|
-
PLAN_ACTOR
|
|
490
|
-
];
|
|
491
|
-
/**
|
|
492
|
-
* Rejects a user-supplied `actorSources`/`requests` key in the reserved
|
|
493
|
-
* `agent.*` builtin namespace. Without this, the builtins-first spread in
|
|
494
|
-
* {@link createAgentActorSources} lets such a key overwrite the builtin
|
|
495
|
-
* (`agent.decide`, `agent.plan`, …) silently. Deliberate override of a builtin
|
|
496
|
-
* is still possible after the machine is created, via
|
|
497
|
-
* `machine.provide({ actorSources: { 'agent.decide': ... } })`.
|
|
498
|
-
*/
|
|
499
|
-
function assertNoReservedAgentKeys(actorSources, requests) {
|
|
500
|
-
const groups = [["actorSources", actorSources], ["requests", requests]];
|
|
501
|
-
for (const [groupName, group] of groups) for (const key of Object.keys(group ?? {})) if (RESERVED_AGENT_ACTOR_KEYS.includes(key)) throw new Error(`setupAgent: '${groupName}' key '${key}' is a reserved builtin agent actor and cannot be redefined here (it would silently clobber the builtin). Reserved keys: ${RESERVED_AGENT_ACTOR_KEYS.join(", ")}. To deliberately override a builtin, do it on the created machine instead: machine.provide({ actorSources: { '${key}': ... } }).`);
|
|
502
|
-
}
|
|
503
|
-
function createAgentActorSources(actorSources, requestActors) {
|
|
504
|
-
assertNoActorKeyCollisions(actorSources, requestActors);
|
|
505
|
-
assertNoReservedAgentKeys(actorSources, requestActors);
|
|
506
|
-
return {
|
|
507
|
-
...builtinTextActors,
|
|
508
|
-
[USER_INPUT_ACTOR]: userInputActor,
|
|
509
|
-
[DECIDE_ACTOR]: createDecideActor(),
|
|
510
|
-
[PLAN_ACTOR]: createPlanActor(),
|
|
511
|
-
...actorSources,
|
|
512
|
-
...requestActors
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
function createAgentSetupConfig(schemas, actorSources, config) {
|
|
516
|
-
return {
|
|
517
|
-
schemas: {
|
|
518
|
-
context: schemas.context,
|
|
519
|
-
events: schemas.events,
|
|
520
|
-
input: schemas.input,
|
|
521
|
-
output: schemas.output,
|
|
522
|
-
meta: schemas.meta,
|
|
523
|
-
...schemas.emitted && Object.keys(schemas.emitted).length > 0 ? { emitted: schemas.emitted } : {}
|
|
524
|
-
},
|
|
525
|
-
...config.states ? { states: resolveAgentStateSchemas(schemas.context, config.states) } : {},
|
|
526
|
-
actorSources,
|
|
527
|
-
actions: config.actions,
|
|
528
|
-
guards: config.guards,
|
|
529
|
-
delays: config.delays
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
function createSetupAgent(config) {
|
|
533
|
-
const schemas = normalizeAgentSchemas(config);
|
|
534
|
-
const requestActors = createRequestActors(normalizeAgentRequestInput(config.requests));
|
|
535
|
-
const actorSources = createAgentActorSources(config.actorSources, requestActors);
|
|
536
|
-
const base = setup(createAgentSetupConfig(schemas, actorSources, config));
|
|
537
|
-
const createBaseMachine = base.createMachine.bind(base);
|
|
538
|
-
const models = config.models ?? {};
|
|
539
|
-
const machineOptions = {
|
|
540
|
-
schemas,
|
|
541
|
-
actorSources,
|
|
542
|
-
models
|
|
543
|
-
};
|
|
544
|
-
return Object.assign(base, {
|
|
545
|
-
createMachine(machineConfig) {
|
|
546
|
-
const machine = createBaseMachine(withRootOutputFromSingleFinal(machineConfig));
|
|
547
|
-
agentExecutionOptions.set(machine, machineOptions);
|
|
548
|
-
if (config.isSuspended) {
|
|
549
|
-
const rootConfig = machine.config;
|
|
550
|
-
if (rootConfig) machineSuspensionPredicates.set(rootConfig, config.isSuspended);
|
|
551
|
-
}
|
|
552
|
-
return machine;
|
|
553
|
-
},
|
|
554
|
-
schemas,
|
|
555
|
-
models,
|
|
556
|
-
requests: requestActors,
|
|
557
|
-
appendMessages(resolve) {
|
|
558
|
-
return appendMessages(resolve);
|
|
559
|
-
}
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
//#endregion
|
|
1
|
+
import { t as AgentError } from "./errors-CeSXQx0v.mjs";
|
|
2
|
+
import { _ as resolveAgentRequests, a as AgentReplayDivergenceError, b as appendMessages, c as diffEventLogs, d as initEntry, f as replay, g as initialAgentStep, h as getInvokeEffectMetadata, i as AGENT_USAGE_EVENT_TYPE, l as getAgentEffects, m as executeAgentRequest, n as setupAgent, o as AgentReplayMachineMismatchError, p as verifyReplay, r as AGENT_INIT_EVENT_TYPE, s as createReplayEntry, t as createAgentSchemas, u as getCallUsage, v as resolveAgentStep, x as messagesSchema, y as transitionAgentStep } from "./setup-agent-DAZZSjDS.mjs";
|
|
3
|
+
import { A as parseStructuredEnvelope, D as normalizeGeneratorResult, E as isTextLogic, F as getMachineSuspensionPredicate, G as getJsonSchemaSync, H as findNonSerializableContextPaths, I as getRegisteredAgentExecutionOptions, J as isStandardSchema, K as getMachineStructuralHash, L as isUnboundPlaceholder, N as executorBoundLogics, O as parseModelRef, P as getMachineStaticTransitionTargets, Q as userMessage, S as createTextLogic, T as getAgentOutputMode, U as getAgentMessages, V as assistantMessage, W as getJsonSchema, X as systemMessage, Y as persistSnapshot, Z as toolMessage, b as buildEnvelopeSchema, c as isPlanLogic, d as getAcceptedEvents, f as parseAgentEvent, g as INTERPRET_SOURCE, k as parseOutput, l as renderDecisionAttempts, m as AGENT_USAGE_TOKEN_FIELDS, n as PLAN_DONE_EVENT_TYPE, o as initialPlanLedger, p as sanitizeEventToolName, q as getStateMeta, r as advancePlanLedger, s as isDecisionLogic, t as AgentDecisionExhaustedError, u as resolveDecision, w as extractCallUsage, y as bindRequestExecutor } from "./decision-BezSD_YC.mjs";
|
|
4
|
+
import { a as assertEventLogStoreConformance, i as assertAgentLogEntry, n as AgentEventLogConflictError, o as assertJsonSerializable, r as NonSerializableAgentEventError, s as createInMemoryEventLogStore, t as AGENT_EVENT_SCHEMA_VERSION } from "./event-log-store-D7pWtIhb.mjs";
|
|
5
|
+
import { createActor, createAsyncLogic, getNextTransitions } from "xstate";
|
|
563
6
|
//#region src/internal/state-request-pass.ts
|
|
564
7
|
async function runTextPhase(stateRequest, baseMessages, deps) {
|
|
565
8
|
const { model, system } = stateRequest;
|
|
@@ -598,6 +41,8 @@ async function runTextPhase(stateRequest, baseMessages, deps) {
|
|
|
598
41
|
output = await normalizeGeneratorResult(raw, id, { request });
|
|
599
42
|
const rawReasoning = raw?.reasoning;
|
|
600
43
|
const reasoning = typeof rawReasoning === "string" ? rawReasoning : void 0;
|
|
44
|
+
const usage = extractCallUsage(raw);
|
|
45
|
+
if (usage) deps.recordUsage?.(usage);
|
|
601
46
|
deps.onResult?.(agentRequest, {
|
|
602
47
|
output,
|
|
603
48
|
raw
|
|
@@ -607,7 +52,8 @@ async function runTextPhase(stateRequest, baseMessages, deps) {
|
|
|
607
52
|
request: agentRequest,
|
|
608
53
|
output,
|
|
609
54
|
raw,
|
|
610
|
-
...reasoning !== void 0 ? { reasoning } : {}
|
|
55
|
+
...reasoning !== void 0 ? { reasoning } : {},
|
|
56
|
+
...usage !== void 0 ? { usage } : {}
|
|
611
57
|
});
|
|
612
58
|
} catch (error) {
|
|
613
59
|
deps.onTrace?.({
|
|
@@ -695,12 +141,12 @@ async function runStateRequestPass(requests, deps) {
|
|
|
695
141
|
* machine simply takes no transition). Opt out with
|
|
696
142
|
* {@link RunAgentOptions.onIllegalResumeEvent} `'ignore'`.
|
|
697
143
|
*/
|
|
698
|
-
var
|
|
144
|
+
var AgentIllegalResumeEventError = class extends AgentError {
|
|
699
145
|
eventType;
|
|
700
146
|
acceptedTypes;
|
|
701
147
|
constructor(eventType, acceptedTypes) {
|
|
702
|
-
super(`runAgent: cannot resume with event '${eventType}' — the restored state does not accept it. Accepted event types: ${acceptedTypes.length > 0 ? acceptedTypes.join(", ") : "(none)"}.`);
|
|
703
|
-
this.name = "
|
|
148
|
+
super("illegal-resume-event", `runAgent: cannot resume with event '${eventType}' — the restored state does not accept it. Accepted event types: ${acceptedTypes.length > 0 ? acceptedTypes.join(", ") : "(none)"}.`);
|
|
149
|
+
this.name = "AgentIllegalResumeEventError";
|
|
704
150
|
this.eventType = eventType;
|
|
705
151
|
this.acceptedTypes = acceptedTypes;
|
|
706
152
|
}
|
|
@@ -714,30 +160,30 @@ var IllegalResumeEventError = class extends Error {
|
|
|
714
160
|
* the snapshot may no longer resume cleanly. `from` is the snapshot's version,
|
|
715
161
|
* `to` the current machine's.
|
|
716
162
|
*/
|
|
717
|
-
var
|
|
163
|
+
var AgentSnapshotVersionMismatchError = class extends AgentError {
|
|
718
164
|
from;
|
|
719
165
|
to;
|
|
720
166
|
machineId;
|
|
721
167
|
constructor(from, to, machineId) {
|
|
722
|
-
super(`runAgent: cannot resume snapshot stamped with machine version '${from}' against machine '${machineId}' at version '${to}' — the machine's structure changed since the snapshot was persisted. Provide options.migrateSnapshot to adapt it, or set options.onVersionMismatch to 'warn'/'ignore' to proceed anyway.`);
|
|
723
|
-
this.name = "
|
|
168
|
+
super("snapshot-version-mismatch", `runAgent: cannot resume snapshot stamped with machine version '${from}' against machine '${machineId}' at version '${to}' — the machine's structure changed since the snapshot was persisted. Provide options.migrateSnapshot to adapt it, or set options.onVersionMismatch to 'warn'/'ignore' to proceed anyway.`);
|
|
169
|
+
this.name = "AgentSnapshotVersionMismatchError";
|
|
724
170
|
this.from = from;
|
|
725
171
|
this.to = to;
|
|
726
172
|
this.machineId = machineId;
|
|
727
173
|
}
|
|
728
174
|
};
|
|
729
175
|
/**
|
|
730
|
-
* Thrown by {@link
|
|
176
|
+
* Thrown by {@link generateResult} when the run settles `idle` instead of
|
|
731
177
|
* `done`: the machine paused for external input. Carries the idle `snapshot`
|
|
732
178
|
* and `acceptedTypes` (the event types that could resume it, via
|
|
733
179
|
* {@link getAcceptedEvents}). Use {@link runAgent} directly when idle is an
|
|
734
180
|
* expected outcome you handle.
|
|
735
181
|
*/
|
|
736
|
-
var AgentIdleError = class extends
|
|
182
|
+
var AgentIdleError = class extends AgentError {
|
|
737
183
|
snapshot;
|
|
738
184
|
acceptedTypes;
|
|
739
185
|
constructor(snapshot, acceptedTypes) {
|
|
740
|
-
super(`
|
|
186
|
+
super("agent-idle", `generateResult: the machine paused (idle) instead of completing. Resume it by calling runAgent with one of these events: ${acceptedTypes.length > 0 ? acceptedTypes.join(", ") : "(none)"}.`);
|
|
741
187
|
this.name = "AgentIdleError";
|
|
742
188
|
this.snapshot = snapshot;
|
|
743
189
|
this.acceptedTypes = acceptedTypes;
|
|
@@ -751,17 +197,104 @@ var AgentIdleError = class extends Error {
|
|
|
751
197
|
* {@link traceTransitions}.
|
|
752
198
|
*/
|
|
753
199
|
const AGENT_TRACE_SCHEMA_VERSION = 1;
|
|
200
|
+
const TRACE_ENVELOPE_KEYS = [
|
|
201
|
+
"schemaVersion",
|
|
202
|
+
"runId",
|
|
203
|
+
"seq",
|
|
204
|
+
"timestamp",
|
|
205
|
+
"machineId",
|
|
206
|
+
"machineVersion",
|
|
207
|
+
"type",
|
|
208
|
+
"status",
|
|
209
|
+
"cause",
|
|
210
|
+
"eventId",
|
|
211
|
+
"reasoning",
|
|
212
|
+
"chunk"
|
|
213
|
+
];
|
|
214
|
+
/**
|
|
215
|
+
* Best-effort JSON projection of an arbitrary value. Never throws: functions,
|
|
216
|
+
* symbols, `undefined`, and cyclic back-references are DROPPED (array holes
|
|
217
|
+
* become `null`), non-finite numbers become `null`, `bigint`s become strings,
|
|
218
|
+
* `Error`s become `{ name, message, stack?, code? }`, and anything with a
|
|
219
|
+
* `toJSON()` (e.g. `Date`) is projected through it — the same losses a
|
|
220
|
+
* `JSON.parse(JSON.stringify(...))` round-trip incurs, minus the throws.
|
|
221
|
+
*/
|
|
222
|
+
function toJsonValue(value, ancestors) {
|
|
223
|
+
if (value === null) return null;
|
|
224
|
+
const type = typeof value;
|
|
225
|
+
if (type === "string" || type === "boolean") return value;
|
|
226
|
+
if (type === "number") return Number.isFinite(value) ? value : null;
|
|
227
|
+
if (type === "bigint") return value.toString();
|
|
228
|
+
if (type !== "object") return;
|
|
229
|
+
const object = value;
|
|
230
|
+
if (ancestors.includes(object)) return;
|
|
231
|
+
const nextAncestors = [...ancestors, object];
|
|
232
|
+
if (object instanceof Error) {
|
|
233
|
+
const serialized = {
|
|
234
|
+
name: object.name,
|
|
235
|
+
message: object.message
|
|
236
|
+
};
|
|
237
|
+
if (typeof object.stack === "string") serialized.stack = object.stack;
|
|
238
|
+
const code = object.code;
|
|
239
|
+
if (typeof code === "string") serialized.code = code;
|
|
240
|
+
const cause = toJsonValue(object.cause, nextAncestors);
|
|
241
|
+
if (cause !== void 0) serialized.cause = cause;
|
|
242
|
+
return serialized;
|
|
243
|
+
}
|
|
244
|
+
const toJSON = object.toJSON;
|
|
245
|
+
if (typeof toJSON === "function") return toJsonValue(toJSON.call(object), nextAncestors);
|
|
246
|
+
if (Array.isArray(object)) return object.map((item) => toJsonValue(item, nextAncestors) ?? null);
|
|
247
|
+
const out = {};
|
|
248
|
+
for (const [key, item] of Object.entries(object)) {
|
|
249
|
+
const serializedItem = toJsonValue(item, nextAncestors);
|
|
250
|
+
if (serializedItem !== void 0) out[key] = serializedItem;
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Projects an {@link AgentTraceEvent} into a guaranteed JSON-safe envelope —
|
|
256
|
+
* the form the trace stream is actually sold for (one `JSON.stringify` per line
|
|
257
|
+
* in a JSONL file). Live values are sanitized rather than trusted:
|
|
258
|
+
*
|
|
259
|
+
* - Snapshots (`run.start`, `machine.transition`, `run.end`) go through the
|
|
260
|
+
* same JSON round-trip as {@link persistSnapshot}, so what lands on disk is
|
|
261
|
+
* what a resume would see.
|
|
262
|
+
* - `request.end`'s `raw` (a provider SDK object, frequently cyclic) is DROPPED
|
|
263
|
+
* unless `includeRaw` is set, in which case it is sanitized like everything
|
|
264
|
+
* else.
|
|
265
|
+
* - Non-serializable values anywhere (functions, symbols, `undefined`, cyclic
|
|
266
|
+
* back-references) are dropped; `Error`s become `{ name, message, stack?,
|
|
267
|
+
* code? }` instead of `{}`. Nothing throws.
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```ts
|
|
271
|
+
* await appendFile('trace.jsonl', JSON.stringify(serializeTraceEvent(event)) + '\n');
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
274
|
+
function serializeTraceEvent(event, options = {}) {
|
|
275
|
+
const out = {};
|
|
276
|
+
for (const [key, value] of Object.entries(event)) {
|
|
277
|
+
if (key === "raw" && !options.includeRaw) continue;
|
|
278
|
+
if (TRACE_ENVELOPE_KEYS.includes(key)) {
|
|
279
|
+
if (value !== void 0) out[key] = value;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const serialized = toJsonValue(value, []);
|
|
283
|
+
if (serialized !== void 0) out[key] = serialized;
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
754
287
|
let nextRunAgentTraceId = 1;
|
|
755
|
-
var
|
|
288
|
+
var AgentMaxModelCallsExceededError = class extends AgentError {
|
|
756
289
|
constructor() {
|
|
757
|
-
super("runAgent exceeded maxModelCalls.");
|
|
758
|
-
this.name = "
|
|
290
|
+
super("max-model-calls-exceeded", "runAgent exceeded maxModelCalls.");
|
|
291
|
+
this.name = "AgentMaxModelCallsExceededError";
|
|
759
292
|
}
|
|
760
293
|
};
|
|
761
294
|
function wrapsDecisionExhausted(error) {
|
|
762
295
|
let current = error;
|
|
763
296
|
for (let depth = 0; depth < 10 && current != null; depth++) {
|
|
764
|
-
if (current instanceof
|
|
297
|
+
if (current instanceof AgentDecisionExhaustedError) return true;
|
|
765
298
|
current = current.cause;
|
|
766
299
|
}
|
|
767
300
|
return false;
|
|
@@ -789,14 +322,14 @@ function collectConfiguredInvokeSrcs(stateConfig, stateName, out) {
|
|
|
789
322
|
/**
|
|
790
323
|
* Duck-types a state machine actor logic (an invoked child machine) vs. any
|
|
791
324
|
* other actor logic. xstate's `StateMachine` carries `.config`, `.root`, and
|
|
792
|
-
* a `.provide(...)` method plus
|
|
325
|
+
* a `.provide(...)` method plus a `sources.actors` map — this
|
|
793
326
|
* combination is unique to machines and survives the dual-package/version
|
|
794
327
|
* boundary an `instanceof` check would not. Used to descend the bind-time
|
|
795
328
|
* walk into invoked child machines (their internal agent requests are opaque
|
|
796
329
|
* to the parent-level source walk otherwise).
|
|
797
330
|
*/
|
|
798
331
|
function isStateMachine(logic) {
|
|
799
|
-
return !!logic && typeof logic === "object" && "config" in logic && "root" in logic && typeof logic.provide === "function" && typeof logic.
|
|
332
|
+
return !!logic && typeof logic === "object" && "config" in logic && "root" in logic && typeof logic.provide === "function" && typeof logic.sources === "object" && !!logic.sources?.actors;
|
|
800
333
|
}
|
|
801
334
|
/**
|
|
802
335
|
* Fails fast (throws) at bind time — before any actor runs — when the
|
|
@@ -831,11 +364,11 @@ function assertMachineBindable(machine, effectiveSources, options, ctx) {
|
|
|
831
364
|
assertChildMachineBindable(src, src, stateName, options, ctx);
|
|
832
365
|
continue;
|
|
833
366
|
}
|
|
834
|
-
if ((isTextLogic(src) || isDecisionLogic(src) || isPlanLogic(src)) && !executorBoundLogics.has(src)) throw new Error(`runAgent: ${where} '${stateName}' invokes a direct-object actor logic (kind: '${src.kind}'). Direct-object invoke srcs cannot be rebound by runAgent — either call '.withExecutor(...)' on the logic before invoking it, or register it as a string-keyed actor source instead (machine.provide({
|
|
367
|
+
if ((isTextLogic(src) || isDecisionLogic(src) || isPlanLogic(src)) && !executorBoundLogics.has(src)) throw new Error(`runAgent: ${where} '${stateName}' invokes a direct-object actor logic (kind: '${src.kind}'). Direct-object invoke srcs cannot be rebound by runAgent — either call '.withExecutor(...)' on the logic before invoking it, or register it as a string-keyed actor source instead (machine.provide({ actors: { name: logic } })) and invoke it by name.`);
|
|
835
368
|
continue;
|
|
836
369
|
}
|
|
837
370
|
const logic = effectiveSources[src];
|
|
838
|
-
if (logic === void 0) throw new Error(`runAgent: ${where} '${stateName}' invokes unregistered actor source '${src}'. Provide it via machine.provide({
|
|
371
|
+
if (logic === void 0) throw new Error(`runAgent: ${where} '${stateName}' invokes unregistered actor source '${src}'. Provide it via machine.provide({ actors: { '${src}': ... } }) or runAgent(machine, { actors: { '${src}': ... } }).`);
|
|
839
372
|
if (isStateMachine(logic)) {
|
|
840
373
|
assertChildMachineBindable(logic, src, stateName, options, ctx);
|
|
841
374
|
continue;
|
|
@@ -859,7 +392,7 @@ function assertMachineBindable(machine, effectiveSources, options, ctx) {
|
|
|
859
392
|
if (logic.mode !== "stream" && !options.hasGenerateText) throw new Error(`runAgent: ${where} '${stateName}' invokes text source '${src}' but no 'generateText' executor was provided to runAgent(...).`);
|
|
860
393
|
continue;
|
|
861
394
|
}
|
|
862
|
-
if (isUnboundPlaceholder(logic)) throw new Error(`runAgent: ${where} '${stateName}' invokes actor source '${src}', which has no host execution. Provide it via machine.provide({
|
|
395
|
+
if (isUnboundPlaceholder(logic)) throw new Error(`runAgent: ${where} '${stateName}' invokes actor source '${src}', which has no host execution. Provide it via machine.provide({ actors: { '${src}': ... } }) or runAgent(machine, { actors: { '${src}': ... } }).`);
|
|
863
396
|
}
|
|
864
397
|
}
|
|
865
398
|
/** Descends the bind-time walk into an invoked child state machine, guarding
|
|
@@ -868,7 +401,7 @@ function assertChildMachineBindable(childMachine, childSrc, stateName, options,
|
|
|
868
401
|
if (ctx.visited.has(childMachine)) return;
|
|
869
402
|
const childName = typeof childSrc === "string" ? childSrc : childMachine.config.id ?? "(child machine)";
|
|
870
403
|
const childPath = ctx.childPath ? `${ctx.childPath} > ${childName}` : childName;
|
|
871
|
-
const childSources = childMachine.
|
|
404
|
+
const childSources = childMachine.sources.actors;
|
|
872
405
|
assertMachineBindable(childMachine, childSources, options, {
|
|
873
406
|
isChild: true,
|
|
874
407
|
childPath,
|
|
@@ -884,7 +417,21 @@ function assertChildMachineBindable(childMachine, childSrc, stateName, options,
|
|
|
884
417
|
* runAgent's executors — this error is only for the unrebindable direct-object
|
|
885
418
|
* case. */
|
|
886
419
|
function unrebindableChildRequestError(childPath, stateName, requestSrc, kind) {
|
|
887
|
-
return /* @__PURE__ */ new Error(`runAgent: child machine '${childPath}' (state '${stateName}') invokes ${kind} source '${requestSrc}', which has no host execution and is reached through a direct-object invoke src that runAgent cannot rebind. Requests reached through string-keyed actor sources inherit runAgent's generateText/streamText/decide executors automatically; a direct-object child machine does not. Either bind the request with its own executor (requestLogic.withExecutor(...)), or register the child as a string-keyed actor source (machine.provide({
|
|
420
|
+
return /* @__PURE__ */ new Error(`runAgent: child machine '${childPath}' (state '${stateName}') invokes ${kind} source '${requestSrc}', which has no host execution and is reached through a direct-object invoke src that runAgent cannot rebind. Requests reached through string-keyed actor sources inherit runAgent's generateText/streamText/decide executors automatically; a direct-object child machine does not. Either bind the request with its own executor (requestLogic.withExecutor(...)), or register the child as a string-keyed actor source (machine.provide({ actors: { <child>: childMachine } })) and invoke it by name.`);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* True when the snapshot's active states declare a transition for the reserved
|
|
424
|
+
* `'@agent.usage'` type EXPLICITLY. A catch-all `on: { '*': … }` deliberately
|
|
425
|
+
* does not count: a wildcard is a machine's own event vocabulary, not an
|
|
426
|
+
* opt-in to a library-reserved event, and `snapshot.can(event)` alone cannot
|
|
427
|
+
* tell the two apart (it answers "would this event be taken?", which a
|
|
428
|
+
* wildcard makes true for everything). Gating delivery on the explicit
|
|
429
|
+
* declaration is what keeps `@agent.usage` opt-in by construction — and keeps
|
|
430
|
+
* a wildcard machine's context and event log byte-identical to a run without
|
|
431
|
+
* the feature. @internal
|
|
432
|
+
*/
|
|
433
|
+
function declaresUsageTransition(snapshot) {
|
|
434
|
+
return getNextTransitions(snapshot).some((transition) => transition.eventType === AGENT_USAGE_EVENT_TYPE);
|
|
888
435
|
}
|
|
889
436
|
/** Reads the durable invoke id/src off the async actor's own ref (`self`). */
|
|
890
437
|
function selfIdAndSrc(self) {
|
|
@@ -946,7 +493,9 @@ function wrapTextLogicForRunAgent(logic, runCtx) {
|
|
|
946
493
|
}, self);
|
|
947
494
|
runCtx.onChunk?.(chunk, { request: agentRequest });
|
|
948
495
|
},
|
|
949
|
-
signal
|
|
496
|
+
signal,
|
|
497
|
+
...runCtx.runId !== void 0 ? { runId: runCtx.runId } : {},
|
|
498
|
+
...id !== "" ? { requestId: id } : {}
|
|
950
499
|
});
|
|
951
500
|
const output = await normalizeGeneratorResult(raw, id, {
|
|
952
501
|
request,
|
|
@@ -961,6 +510,14 @@ function wrapTextLogicForRunAgent(logic, runCtx) {
|
|
|
961
510
|
});
|
|
962
511
|
const rawReasoning = raw?.reasoning;
|
|
963
512
|
const reasoning = typeof rawReasoning === "string" ? rawReasoning : void 0;
|
|
513
|
+
const usage = extractCallUsage(raw);
|
|
514
|
+
if (usage) runCtx.recordUsage?.(usage, {
|
|
515
|
+
kind: "text",
|
|
516
|
+
...id !== "" ? { id } : {},
|
|
517
|
+
...src !== "" ? { src } : {},
|
|
518
|
+
model: request.model,
|
|
519
|
+
...request.name !== void 0 ? { name: request.name } : {}
|
|
520
|
+
}, self);
|
|
964
521
|
runCtx.onResult?.(agentRequest, {
|
|
965
522
|
output,
|
|
966
523
|
raw
|
|
@@ -970,7 +527,8 @@ function wrapTextLogicForRunAgent(logic, runCtx) {
|
|
|
970
527
|
request: agentRequest,
|
|
971
528
|
output,
|
|
972
529
|
raw,
|
|
973
|
-
...reasoning !== void 0 ? { reasoning } : {}
|
|
530
|
+
...reasoning !== void 0 ? { reasoning } : {},
|
|
531
|
+
...usage !== void 0 ? { usage } : {}
|
|
974
532
|
}, self);
|
|
975
533
|
return { output };
|
|
976
534
|
} catch (error) {
|
|
@@ -983,7 +541,7 @@ function wrapTextLogicForRunAgent(logic, runCtx) {
|
|
|
983
541
|
}
|
|
984
542
|
});
|
|
985
543
|
}
|
|
986
|
-
function createCountingDecide(runCtx, self) {
|
|
544
|
+
function createCountingDecide(runCtx, self, kind = "decision") {
|
|
987
545
|
return async (attemptRequest) => {
|
|
988
546
|
runCtx.consumeModelCall();
|
|
989
547
|
runCtx.onTrace?.({
|
|
@@ -991,7 +549,20 @@ function createCountingDecide(runCtx, self) {
|
|
|
991
549
|
request: attemptRequest
|
|
992
550
|
}, self);
|
|
993
551
|
try {
|
|
994
|
-
const result = await runCtx.decide(
|
|
552
|
+
const result = await runCtx.decide(runCtx.runId !== void 0 ? {
|
|
553
|
+
...attemptRequest,
|
|
554
|
+
runId: runCtx.runId
|
|
555
|
+
} : attemptRequest);
|
|
556
|
+
const usage = extractCallUsage(result);
|
|
557
|
+
if (usage) {
|
|
558
|
+
const { src } = selfIdAndSrc(self);
|
|
559
|
+
runCtx.recordUsage?.(usage, {
|
|
560
|
+
kind,
|
|
561
|
+
...attemptRequest.id ? { id: attemptRequest.id } : {},
|
|
562
|
+
...src !== "" ? { src } : {},
|
|
563
|
+
model: attemptRequest.model
|
|
564
|
+
}, self);
|
|
565
|
+
}
|
|
995
566
|
runCtx.onResult?.(attemptRequest, {
|
|
996
567
|
output: result.event,
|
|
997
568
|
raw: result
|
|
@@ -1000,7 +571,8 @@ function createCountingDecide(runCtx, self) {
|
|
|
1000
571
|
type: "request.end",
|
|
1001
572
|
request: attemptRequest,
|
|
1002
573
|
output: result.event,
|
|
1003
|
-
raw: result
|
|
574
|
+
raw: result,
|
|
575
|
+
...usage !== void 0 ? { usage } : {}
|
|
1004
576
|
}, self);
|
|
1005
577
|
return result;
|
|
1006
578
|
} catch (error) {
|
|
@@ -1078,7 +650,7 @@ function createRunAgentPlanLogic(logic, runCtx) {
|
|
|
1078
650
|
if (!runCtx.decide) throw new Error("runAgent: no 'decide' executor provided.");
|
|
1079
651
|
const { id } = selfIdAndSrc(self);
|
|
1080
652
|
const stopOn = new Set(input.stopOn ?? []);
|
|
1081
|
-
const countingDecide = createCountingDecide(runCtx, self);
|
|
653
|
+
const countingDecide = createCountingDecide(runCtx, self, "plan");
|
|
1082
654
|
const base = logic.request(input);
|
|
1083
655
|
let ledger = initialPlanLedger(logic, input);
|
|
1084
656
|
const end = (stopped) => {
|
|
@@ -1208,6 +780,10 @@ function provideTraceSink(onTrace) {
|
|
|
1208
780
|
* actor off `self._parent`, always present under a live `createActor` tree.
|
|
1209
781
|
* `onTrace` (when given) mints a per-root-actor envelope. `schemas` come from
|
|
1210
782
|
* the machine's registered `setupAgent` execution options.
|
|
783
|
+
*
|
|
784
|
+
* `recordUsage` has no run-level aggregate to fold into here (there is no
|
|
785
|
+
* run), so it does one thing: deliver the reserved `@agent.usage` event, gated
|
|
786
|
+
* exactly like runAgent's — see {@link deliverUsageToInvokingActor}.
|
|
1211
787
|
*/
|
|
1212
788
|
function provideBindContext(machine, executors, options) {
|
|
1213
789
|
return {
|
|
@@ -1217,11 +793,45 @@ function provideBindContext(machine, executors, options) {
|
|
|
1217
793
|
onChunk: options.onChunk ? (chunk) => options.onChunk(chunk) : void 0,
|
|
1218
794
|
onTrace: provideTraceSink(options.onTrace),
|
|
1219
795
|
consumeModelCall: () => {},
|
|
796
|
+
recordUsage: (usage, source, self) => {
|
|
797
|
+
deliverUsageToInvokingActor(usage, source ?? {}, self);
|
|
798
|
+
},
|
|
1220
799
|
actorHolder: { actorRef: void 0 },
|
|
1221
800
|
schemas: getRegisteredAgentExecutionOptions(machine).schemas
|
|
1222
801
|
};
|
|
1223
802
|
}
|
|
1224
803
|
/**
|
|
804
|
+
* `provideExecutors`' counterpart to runAgent's `deliverUsageEvent`: after a
|
|
805
|
+
* bound call settles with reported usage, send the reserved
|
|
806
|
+
* `@agent.usage` event to the machine actor that INVOKED the request — read
|
|
807
|
+
* off the settling request actor's `self._parent`, which under a live
|
|
808
|
+
* `createActor` tree is always the invoking machine (there is no run-scoped
|
|
809
|
+
* root actor on this path).
|
|
810
|
+
*
|
|
811
|
+
* Gated identically to runAgent: the invoking snapshot must be active, must
|
|
812
|
+
* declare an `'@agent.usage'` transition EXPLICITLY (see
|
|
813
|
+
* {@link declaresUsageTransition} — a catch-all `on: { '*' }` is not an opt-in),
|
|
814
|
+
* and must be able to take the event. There is no cycle to settle in
|
|
815
|
+
* uncontrolled mode, so there are no dropped stragglers.
|
|
816
|
+
*
|
|
817
|
+
* Delivery follows `provideExecutors`' binding boundary: only sources IT bound
|
|
818
|
+
* report here, so an invoked child machine that was not itself passed through
|
|
819
|
+
* `provideExecutors` reports nothing. @internal
|
|
820
|
+
*/
|
|
821
|
+
function deliverUsageToInvokingActor(usage, source, self) {
|
|
822
|
+
const actorRef = self?._parent;
|
|
823
|
+
if (!actorRef) return;
|
|
824
|
+
const snapshot = actorRef.getSnapshot();
|
|
825
|
+
if (snapshot?.status !== "active" || !declaresUsageTransition(snapshot)) return;
|
|
826
|
+
const event = {
|
|
827
|
+
type: AGENT_USAGE_EVENT_TYPE,
|
|
828
|
+
...source,
|
|
829
|
+
usage
|
|
830
|
+
};
|
|
831
|
+
if (!snapshot.can(event)) return;
|
|
832
|
+
actorRef.send(event);
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
1225
835
|
* Host-binds one text/stream source for {@link provideExecutors} using the SAME
|
|
1226
836
|
* emission helper as `runAgent` ({@link wrapTextLogicForRunAgent}), so a bound
|
|
1227
837
|
* text request emits request.start/stream.chunk/request.end/request.error with
|
|
@@ -1264,7 +874,7 @@ function bindPlanForProvide(machine, logic, executors, options) {
|
|
|
1264
874
|
function rebindChildMachine(childMachine, runCtx, visited) {
|
|
1265
875
|
if (visited.has(childMachine)) return childMachine;
|
|
1266
876
|
const childVisited = new Set([...visited, childMachine]);
|
|
1267
|
-
const sources = childMachine.
|
|
877
|
+
const sources = childMachine.sources.actors;
|
|
1268
878
|
const wrapped = {};
|
|
1269
879
|
for (const [key, logic] of Object.entries(sources)) {
|
|
1270
880
|
if (isDecisionLogic(logic)) {
|
|
@@ -1285,7 +895,7 @@ function rebindChildMachine(childMachine, runCtx, visited) {
|
|
|
1285
895
|
continue;
|
|
1286
896
|
}
|
|
1287
897
|
}
|
|
1288
|
-
return Object.keys(wrapped).length > 0 ? childMachine.provide({
|
|
898
|
+
return Object.keys(wrapped).length > 0 ? childMachine.provide({ actors: wrapped }) : childMachine;
|
|
1289
899
|
}
|
|
1290
900
|
/**
|
|
1291
901
|
* Runs an agent machine to completion or idle: a `createActor` host that
|
|
@@ -1302,7 +912,7 @@ function rebindChildMachine(childMachine, runCtx, visited) {
|
|
|
1302
912
|
*
|
|
1303
913
|
* Binding happens **before** the actor starts: every invoke the machine
|
|
1304
914
|
* could reach is walked and checked against the effective actor sources
|
|
1305
|
-
* (`options.
|
|
915
|
+
* (`options.actors` merged onto the machine), so a missing
|
|
1306
916
|
* `streamText`/`decide` executor or any other unbound actor source throws
|
|
1307
917
|
* immediately — a bind-time error, not a mid-run failure. The one exception
|
|
1308
918
|
* is `agent.userInput`: unhandled, it binds as a pending placeholder that
|
|
@@ -1327,14 +937,42 @@ function rebindChildMachine(childMachine, runCtx, visited) {
|
|
|
1327
937
|
* lives in an adapter — use `createAiSdkExecutors` from '@statelyai/agent/ai-sdk'.
|
|
1328
938
|
*/
|
|
1329
939
|
async function runAgent(machine, options) {
|
|
940
|
+
return createAgentSession(machine, options, { oneShot: true }).settled();
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Session mode: {@link runAgent}'s engine with a long-lived actor. Use it when
|
|
944
|
+
* the agent is a *session* fed by external events (chat turns, device or
|
|
945
|
+
* timer events, a socket) rather than a one-shot job — you keep the log,
|
|
946
|
+
* budget, traces, and idle semantics that bare `provideExecutors` +
|
|
947
|
+
* `createActor` would forfeit.
|
|
948
|
+
*
|
|
949
|
+
* ```ts
|
|
950
|
+
* const session = createAgentActor(machine, { input, executors });
|
|
951
|
+
* let result = await session.settled(); // first quiescence
|
|
952
|
+
* while (result.status === "idle") {
|
|
953
|
+
* session.actor.send(await nextUserEvent(result.snapshot));
|
|
954
|
+
* result = await session.settled(); // next quiescence, same log
|
|
955
|
+
* }
|
|
956
|
+
* session.stop();
|
|
957
|
+
* ```
|
|
958
|
+
*
|
|
959
|
+
* Accepts the same options as {@link runAgent} (including `snapshot`/`events`
|
|
960
|
+
* resume). Not yet supported in session mode: `getRequests` re-interpretation
|
|
961
|
+
* across cycles behaves per-cycle exactly as in `runAgent`.
|
|
962
|
+
*/
|
|
963
|
+
function createAgentActor(machine, options) {
|
|
964
|
+
return createAgentSession(machine, options, { oneShot: false });
|
|
965
|
+
}
|
|
966
|
+
function createAgentSession(machine, options, lifecycle) {
|
|
1330
967
|
const maxModelCalls = options.maxModelCalls ?? 100;
|
|
1331
968
|
let modelCallCount = 0;
|
|
1332
969
|
let budgetExceeded = false;
|
|
1333
970
|
let warnedNonSerializable = false;
|
|
971
|
+
let warnedHeuristicIdle = false;
|
|
1334
972
|
const runId = `run_${nextRunAgentTraceId++}`;
|
|
1335
973
|
let traceSeq = 0;
|
|
1336
974
|
const machineId = machine.config.id ?? machine.id ?? "(machine)";
|
|
1337
|
-
const machineVersion = options.machineVersion ?? getMachineStructuralHash(machine);
|
|
975
|
+
const machineVersion = options.machineVersion ?? machine.version ?? getMachineStructuralHash(machine);
|
|
1338
976
|
const agentMeta = {
|
|
1339
977
|
machineId,
|
|
1340
978
|
version: machineVersion
|
|
@@ -1354,13 +992,47 @@ async function runAgent(machine, options) {
|
|
|
1354
992
|
});
|
|
1355
993
|
};
|
|
1356
994
|
const consumeModelCall = () => {
|
|
1357
|
-
if (budgetExceeded) throw new
|
|
1358
|
-
modelCallCount
|
|
1359
|
-
if (modelCallCount > maxModelCalls) {
|
|
995
|
+
if (budgetExceeded) throw new AgentMaxModelCallsExceededError();
|
|
996
|
+
if (modelCallCount + 1 > maxModelCalls) {
|
|
1360
997
|
budgetExceeded = true;
|
|
1361
|
-
throw new
|
|
998
|
+
throw new AgentMaxModelCallsExceededError();
|
|
1362
999
|
}
|
|
1000
|
+
modelCallCount += 1;
|
|
1001
|
+
};
|
|
1002
|
+
const tokenTotals = {};
|
|
1003
|
+
const cycleGate = { isResolved: () => false };
|
|
1004
|
+
const deliverUsageEvent = (usage, source) => {
|
|
1005
|
+
const actorRef = actorHolder.actorRef;
|
|
1006
|
+
if (!actorRef) return;
|
|
1007
|
+
const event = {
|
|
1008
|
+
type: AGENT_USAGE_EVENT_TYPE,
|
|
1009
|
+
...source,
|
|
1010
|
+
usage
|
|
1011
|
+
};
|
|
1012
|
+
const snapshot = actorRef.getSnapshot();
|
|
1013
|
+
if (snapshot?.status !== "active" || !declaresUsageTransition(snapshot)) return;
|
|
1014
|
+
if (cycleGate.isResolved()) {
|
|
1015
|
+
onTrace({
|
|
1016
|
+
type: "usage.dropped",
|
|
1017
|
+
event,
|
|
1018
|
+
reason: "settled"
|
|
1019
|
+
});
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
if (!snapshot.can(event)) return;
|
|
1023
|
+
actorRef.send(event);
|
|
1363
1024
|
};
|
|
1025
|
+
const recordUsage = (usage, source = {}) => {
|
|
1026
|
+
for (const field of AGENT_USAGE_TOKEN_FIELDS) {
|
|
1027
|
+
const value = usage[field];
|
|
1028
|
+
if (typeof value === "number" && Number.isFinite(value)) tokenTotals[field] = (tokenTotals[field] ?? 0) + value;
|
|
1029
|
+
}
|
|
1030
|
+
deliverUsageEvent(usage, source);
|
|
1031
|
+
};
|
|
1032
|
+
const runUsage = () => ({
|
|
1033
|
+
...tokenTotals,
|
|
1034
|
+
modelCalls: modelCallCount
|
|
1035
|
+
});
|
|
1364
1036
|
const warnNonSerializableContext = (snapshot) => {
|
|
1365
1037
|
if (warnedNonSerializable || process.env.NODE_ENV === "production") return;
|
|
1366
1038
|
let offending = [];
|
|
@@ -1373,8 +1045,8 @@ async function runAgent(machine, options) {
|
|
|
1373
1045
|
warnedNonSerializable = true;
|
|
1374
1046
|
console.warn(`runAgent: context holds value(s) that will not survive snapshot persist/resume (JSON round-trip): ${offending.join(", ")}. Persist only JSON-serializable context, or convert these before the run settles.`);
|
|
1375
1047
|
};
|
|
1376
|
-
const provided = machine.provide({
|
|
1377
|
-
const effectiveSources = provided.
|
|
1048
|
+
const provided = machine.provide({ actors: options.actors });
|
|
1049
|
+
const effectiveSources = provided.sources.actors;
|
|
1378
1050
|
assertBindable(provided, effectiveSources, {
|
|
1379
1051
|
hasGenerateText: !!options.executors?.generateText,
|
|
1380
1052
|
hasDecide: !!options.executors?.decide,
|
|
@@ -1390,7 +1062,9 @@ async function runAgent(machine, options) {
|
|
|
1390
1062
|
onResult: options.onResult,
|
|
1391
1063
|
onTrace,
|
|
1392
1064
|
consumeModelCall,
|
|
1065
|
+
recordUsage,
|
|
1393
1066
|
actorHolder,
|
|
1067
|
+
runId,
|
|
1394
1068
|
schemas: getRegisteredAgentExecutionOptions(machine).schemas
|
|
1395
1069
|
};
|
|
1396
1070
|
let userInputIsPlaceholder = false;
|
|
@@ -1424,11 +1098,12 @@ async function runAgent(machine, options) {
|
|
|
1424
1098
|
continue;
|
|
1425
1099
|
}
|
|
1426
1100
|
}
|
|
1427
|
-
const boundMachine = provided.provide({
|
|
1428
|
-
const
|
|
1101
|
+
const boundMachine = provided.provide({ actors: wrappedSources });
|
|
1102
|
+
const declaredSuspensionPredicate = options.isSuspended ?? getMachineSuspensionPredicate(machine);
|
|
1103
|
+
const isSuspended = declaredSuspensionPredicate ?? (() => false);
|
|
1429
1104
|
let effectiveSnapshot = options.snapshot;
|
|
1430
1105
|
if (effectiveSnapshot !== void 0) {
|
|
1431
|
-
const from = effectiveSnapshot.agentMeta?.version;
|
|
1106
|
+
const from = effectiveSnapshot.agentMeta?.version ?? effectiveSnapshot.version;
|
|
1432
1107
|
if (from !== void 0 && from !== machineVersion) {
|
|
1433
1108
|
const info = {
|
|
1434
1109
|
from,
|
|
@@ -1437,11 +1112,23 @@ async function runAgent(machine, options) {
|
|
|
1437
1112
|
if (options.migrateSnapshot) effectiveSnapshot = options.migrateSnapshot(effectiveSnapshot, info);
|
|
1438
1113
|
else {
|
|
1439
1114
|
const mode = options.onVersionMismatch ?? "throw";
|
|
1440
|
-
if (mode === "throw") throw new
|
|
1115
|
+
if (mode === "throw") throw new AgentSnapshotVersionMismatchError(from, machineVersion, machineId);
|
|
1441
1116
|
if (mode === "warn") console.warn(`runAgent: resuming a snapshot stamped with machine version '${from}' against machine '${machineId}' at version '${machineVersion}'. Structural changes may not resume cleanly.`);
|
|
1442
1117
|
}
|
|
1443
1118
|
}
|
|
1444
1119
|
}
|
|
1120
|
+
const resumeEvents = options.events;
|
|
1121
|
+
if (effectiveSnapshot === void 0 && options.events !== void 0 && options.events[0]?.event.type === "@agent.init") {
|
|
1122
|
+
const { snapshot: replayedSnapshot } = replay(machine, resumeEvents, { machineVersion });
|
|
1123
|
+
effectiveSnapshot = machine.getPersistedSnapshot(replayedSnapshot);
|
|
1124
|
+
}
|
|
1125
|
+
const machineOwnVersion = machine.version;
|
|
1126
|
+
if (effectiveSnapshot !== void 0 && effectiveSnapshot.version !== machineOwnVersion) {
|
|
1127
|
+
const aligned = Object.assign(Object.create(Object.getPrototypeOf(effectiveSnapshot)), effectiveSnapshot);
|
|
1128
|
+
if (machineOwnVersion === void 0) delete aligned.version;
|
|
1129
|
+
else aligned.version = machineOwnVersion;
|
|
1130
|
+
effectiveSnapshot = aligned;
|
|
1131
|
+
}
|
|
1445
1132
|
const priorMessages = getAgentMessages(effectiveSnapshot);
|
|
1446
1133
|
const messages = typeof options.messages === "function" ? [...options.messages([...priorMessages])] : [...priorMessages, ...options.messages ?? []];
|
|
1447
1134
|
const stampMessages = (snapshot) => {
|
|
@@ -1451,18 +1138,60 @@ async function runAgent(machine, options) {
|
|
|
1451
1138
|
if (effectiveSnapshot !== void 0 && options.event !== void 0 && (options.onIllegalResumeEvent ?? "throw") === "throw") {
|
|
1452
1139
|
const acceptedTypes = getAcceptedEvents(createActor(boundMachine, { snapshot: effectiveSnapshot }).getSnapshot(), { schemas: runCtx.schemas }).map((descriptor) => descriptor.type);
|
|
1453
1140
|
const eventType = options.event.type;
|
|
1454
|
-
if (!acceptedTypes.includes(eventType)) throw new
|
|
1141
|
+
if (!acceptedTypes.includes(eventType)) throw new AgentIllegalResumeEventError(eventType, acceptedTypes);
|
|
1142
|
+
}
|
|
1143
|
+
const replayEvents = [...resumeEvents ?? []];
|
|
1144
|
+
const replayEventIds = /* @__PURE__ */ new Set();
|
|
1145
|
+
for (let index = 0; index < replayEvents.length; index++) {
|
|
1146
|
+
const entry = replayEvents[index];
|
|
1147
|
+
assertAgentLogEntry(entry);
|
|
1148
|
+
if (entry.index !== index) throw new Error(`runAgent events must be contiguous from index 0; found entry.index ${entry.index} at position ${index}.`);
|
|
1149
|
+
if (entry.machineId !== machineId || entry.machineVersion !== machineVersion) throw new AgentReplayMachineMismatchError(entry.id, entry.index, {
|
|
1150
|
+
machineId,
|
|
1151
|
+
machineVersion
|
|
1152
|
+
}, {
|
|
1153
|
+
machineId: entry.machineId,
|
|
1154
|
+
machineVersion: entry.machineVersion
|
|
1155
|
+
});
|
|
1156
|
+
if (replayEventIds.has(entry.id)) throw new Error(`runAgent events contain duplicate event id '${entry.id}'.`);
|
|
1157
|
+
replayEventIds.add(entry.id);
|
|
1455
1158
|
}
|
|
1456
|
-
|
|
1159
|
+
const hasCompleteReplayHistory = replayEvents[0]?.event.type === "@agent.init" || effectiveSnapshot === void 0 && replayEvents.length === 0;
|
|
1160
|
+
const appendReplayEvent = (event) => {
|
|
1161
|
+
const entry = createReplayEntry(machine, replayEvents, event, {
|
|
1162
|
+
machineVersion,
|
|
1163
|
+
verification: hasCompleteReplayHistory
|
|
1164
|
+
});
|
|
1165
|
+
if (replayEventIds.has(entry.id)) throw new Error(`runAgent generated duplicate event id '${entry.id}'.`);
|
|
1166
|
+
replayEventIds.add(entry.id);
|
|
1167
|
+
replayEvents.push(entry);
|
|
1168
|
+
options.onEvent?.(entry);
|
|
1169
|
+
return entry;
|
|
1170
|
+
};
|
|
1171
|
+
if (replayEvents.length === 0 && effectiveSnapshot === void 0) {
|
|
1172
|
+
const entry = initEntry(machine, options.input, { machineVersion });
|
|
1173
|
+
replayEventIds.add(entry.id);
|
|
1174
|
+
replayEvents.push(entry);
|
|
1175
|
+
options.onEvent?.(entry);
|
|
1176
|
+
}
|
|
1177
|
+
return (() => {
|
|
1457
1178
|
let settled = false;
|
|
1179
|
+
let finalized = false;
|
|
1180
|
+
cycleGate.isResolved = () => settled;
|
|
1181
|
+
let lastResult;
|
|
1182
|
+
const waiters = [];
|
|
1458
1183
|
let idleTimer;
|
|
1459
1184
|
let actor;
|
|
1460
1185
|
let deliveringResumeEvent = options.event !== void 0;
|
|
1461
|
-
const settle = (
|
|
1186
|
+
const settle = (outcome) => {
|
|
1462
1187
|
if (settled) return;
|
|
1463
1188
|
settled = true;
|
|
1189
|
+
const result = {
|
|
1190
|
+
...outcome,
|
|
1191
|
+
events: [...replayEvents],
|
|
1192
|
+
usage: runUsage()
|
|
1193
|
+
};
|
|
1464
1194
|
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
1465
|
-
if (options.signal) options.signal.removeEventListener("abort", onAbort);
|
|
1466
1195
|
stampAgentMeta(result.snapshot);
|
|
1467
1196
|
stampMessages(result.snapshot);
|
|
1468
1197
|
if ("persistedSnapshot" in result) {
|
|
@@ -1471,10 +1200,15 @@ async function runAgent(machine, options) {
|
|
|
1471
1200
|
}
|
|
1472
1201
|
onTrace({
|
|
1473
1202
|
type: "run.end",
|
|
1474
|
-
...
|
|
1203
|
+
...outcome
|
|
1475
1204
|
});
|
|
1476
|
-
|
|
1477
|
-
|
|
1205
|
+
if (lifecycle.oneShot || result.status !== "idle") {
|
|
1206
|
+
finalized = true;
|
|
1207
|
+
if (options.signal) options.signal.removeEventListener("abort", onAbort);
|
|
1208
|
+
actor.stop();
|
|
1209
|
+
}
|
|
1210
|
+
lastResult = result;
|
|
1211
|
+
for (const resolve of waiters.splice(0)) resolve(result);
|
|
1478
1212
|
};
|
|
1479
1213
|
const onAbort = () => {
|
|
1480
1214
|
settle({
|
|
@@ -1527,6 +1261,7 @@ async function runAgent(machine, options) {
|
|
|
1527
1261
|
generateText: runCtx.generateText,
|
|
1528
1262
|
decide: runCtx.decide ? createCountingDecide(runCtx, void 0) : void 0,
|
|
1529
1263
|
consumeModelCall,
|
|
1264
|
+
recordUsage,
|
|
1530
1265
|
nextRequestId: () => `interpret_${++interpretSeq}`,
|
|
1531
1266
|
onTrace,
|
|
1532
1267
|
onResult: runCtx.onResult,
|
|
@@ -1563,7 +1298,13 @@ async function runAgent(machine, options) {
|
|
|
1563
1298
|
if (settled) return;
|
|
1564
1299
|
const current = actor.getSnapshot();
|
|
1565
1300
|
if (isIdleSnapshot(current, { ignoreUserInputChildren: userInputIsPlaceholder })) {
|
|
1566
|
-
if (!maybeInterpret(current))
|
|
1301
|
+
if (!maybeInterpret(current)) {
|
|
1302
|
+
if (!declaredSuspensionPredicate && current.status === "active" && !warnedHeuristicIdle && process.env.NODE_ENV !== "production") {
|
|
1303
|
+
warnedHeuristicIdle = true;
|
|
1304
|
+
console.warn("[@statelyai/agent] runAgent settled idle via the timing heuristic (no suspension predicate declared). This is best-effort; for deterministic idle detection, declare setupAgent({ isSuspended }) or pass runAgent(machine, { isSuspended }), e.g. (s) => s.hasTag('waiting').");
|
|
1305
|
+
}
|
|
1306
|
+
settleIdle(current);
|
|
1307
|
+
}
|
|
1567
1308
|
}
|
|
1568
1309
|
}, 0);
|
|
1569
1310
|
};
|
|
@@ -1573,12 +1314,21 @@ async function runAgent(machine, options) {
|
|
|
1573
1314
|
inspect: (event) => {
|
|
1574
1315
|
if (typeof options.inspect === "function") options.inspect(event);
|
|
1575
1316
|
else options.inspect?.next?.(event);
|
|
1576
|
-
if (
|
|
1317
|
+
if (event.type !== "@xstate.transition" || event.actorRef !== actor.ref) return;
|
|
1318
|
+
if (settled) {
|
|
1319
|
+
if (finalized) return;
|
|
1320
|
+
settled = false;
|
|
1321
|
+
lastResult = void 0;
|
|
1322
|
+
}
|
|
1577
1323
|
const snapshot = event.snapshot;
|
|
1324
|
+
let eventId;
|
|
1325
|
+
if (event.event.type !== "@xstate.init" && (event.sourceRef !== event.actorRef || event.event.type === "xstate.timer")) eventId = appendReplayEvent(event.event).id;
|
|
1326
|
+
else if (event.event.type === "@xstate.init") eventId = replayEvents[0]?.event.type === "@agent.init" ? replayEvents[0].id : void 0;
|
|
1578
1327
|
onTrace({
|
|
1579
1328
|
type: "machine.transition",
|
|
1580
1329
|
snapshot,
|
|
1581
|
-
event: event.event
|
|
1330
|
+
event: event.event,
|
|
1331
|
+
...eventId !== void 0 ? { eventId } : {}
|
|
1582
1332
|
});
|
|
1583
1333
|
options.onTransition?.(snapshot, event.event);
|
|
1584
1334
|
if (snapshot.status === "done") {
|
|
@@ -1623,6 +1373,19 @@ async function runAgent(machine, options) {
|
|
|
1623
1373
|
});
|
|
1624
1374
|
});
|
|
1625
1375
|
for (const [type, handler] of Object.entries(options.on ?? {})) if (typeof handler === "function") actor.on(type, handler);
|
|
1376
|
+
const sessionApi = {
|
|
1377
|
+
actor,
|
|
1378
|
+
get events() {
|
|
1379
|
+
return replayEvents;
|
|
1380
|
+
},
|
|
1381
|
+
usage: runUsage,
|
|
1382
|
+
settled: () => settled && lastResult !== void 0 ? Promise.resolve(lastResult) : new Promise((resolve) => {
|
|
1383
|
+
waiters.push(resolve);
|
|
1384
|
+
}),
|
|
1385
|
+
stop: () => {
|
|
1386
|
+
actor.stop();
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1626
1389
|
if (options.signal) {
|
|
1627
1390
|
if (options.signal.aborted) {
|
|
1628
1391
|
settle({
|
|
@@ -1631,7 +1394,7 @@ async function runAgent(machine, options) {
|
|
|
1631
1394
|
error: options.signal.reason ?? /* @__PURE__ */ new Error("Aborted"),
|
|
1632
1395
|
snapshot: actor.getSnapshot()
|
|
1633
1396
|
});
|
|
1634
|
-
return;
|
|
1397
|
+
return sessionApi;
|
|
1635
1398
|
}
|
|
1636
1399
|
options.signal.addEventListener("abort", onAbort);
|
|
1637
1400
|
}
|
|
@@ -1646,13 +1409,17 @@ async function runAgent(machine, options) {
|
|
|
1646
1409
|
deliveringResumeEvent = false;
|
|
1647
1410
|
actor.send(options.event);
|
|
1648
1411
|
}
|
|
1649
|
-
|
|
1412
|
+
return sessionApi;
|
|
1413
|
+
})();
|
|
1650
1414
|
}
|
|
1651
1415
|
/**
|
|
1652
|
-
* Runs an agent machine to a **final state
|
|
1653
|
-
*
|
|
1416
|
+
* Runs an agent machine to a **final state**, for run-to-done flows where an
|
|
1417
|
+
* idle pause is unexpected. Wraps {@link runAgent}:
|
|
1654
1418
|
*
|
|
1655
|
-
* - `done` → resolves with `result.output` (the machine's
|
|
1419
|
+
* - `done` → resolves with the done result: `result.output` (the machine's
|
|
1420
|
+
* `OutputFrom`) plus metadata — `result.snapshot`, the replayable
|
|
1421
|
+
* `result.events`, and the aggregated `result.usage` — the same shape
|
|
1422
|
+
* `generateText` users expect (`text` + call metadata).
|
|
1656
1423
|
* - `idle` → throws {@link AgentIdleError} carrying the idle snapshot and the
|
|
1657
1424
|
* event types that could resume it.
|
|
1658
1425
|
* - `error` → throws `result.error` when it is an `Error`; otherwise wraps it
|
|
@@ -1660,18 +1427,18 @@ async function runAgent(machine, options) {
|
|
|
1660
1427
|
* `.error` is the raw thrown value.
|
|
1661
1428
|
*
|
|
1662
1429
|
* Use {@link runAgent} directly when idle is an expected outcome you handle
|
|
1663
|
-
* (human-in-the-loop, resumable flows); use `
|
|
1430
|
+
* (human-in-the-loop, resumable flows); use `generateResult` when the
|
|
1664
1431
|
* machine is meant to run straight through to a final state.
|
|
1665
1432
|
*/
|
|
1666
|
-
async function
|
|
1433
|
+
async function generateResult(machine, options) {
|
|
1667
1434
|
const result = await runAgent(machine, options);
|
|
1668
|
-
if (result.status === "done") return result
|
|
1435
|
+
if (result.status === "done") return result;
|
|
1669
1436
|
if (result.status === "idle") {
|
|
1670
1437
|
const acceptedTypes = getAcceptedEvents(result.snapshot, { schemas: getRegisteredAgentExecutionOptions(machine).schemas }).map((descriptor) => descriptor.type);
|
|
1671
1438
|
throw new AgentIdleError(result.snapshot, acceptedTypes);
|
|
1672
1439
|
}
|
|
1673
1440
|
if (result.error instanceof Error) throw result.error;
|
|
1674
|
-
const wrapped = /* @__PURE__ */ new Error(`
|
|
1441
|
+
const wrapped = /* @__PURE__ */ new Error(`generateResult: run failed with cause '${result.cause}'.`);
|
|
1675
1442
|
wrapped.cause = result.cause;
|
|
1676
1443
|
wrapped.error = result.error;
|
|
1677
1444
|
throw wrapped;
|
|
@@ -1750,6 +1517,105 @@ function collectPendingUserInputs(snapshot) {
|
|
|
1750
1517
|
return pending;
|
|
1751
1518
|
}
|
|
1752
1519
|
//#endregion
|
|
1520
|
+
//#region src/agent-run.ts
|
|
1521
|
+
/**
|
|
1522
|
+
* Wraps {@link runAgent} in a canonical run-stream handle: the run's trace
|
|
1523
|
+
* events as a pull-based {@link AgentRun.events} async iterator, alongside the
|
|
1524
|
+
* unchanged {@link AgentRun.result} promise.
|
|
1525
|
+
*
|
|
1526
|
+
* Semantics:
|
|
1527
|
+
*
|
|
1528
|
+
* 1. **Starts immediately.** The underlying `runAgent` is invoked on this call,
|
|
1529
|
+
* not on the first `events` iteration — work is already in flight when
|
|
1530
|
+
* `createAgentRun` returns.
|
|
1531
|
+
* 2. **Buffer, don't stall.** Events are queued in an unbounded in-memory
|
|
1532
|
+
* buffer as the run emits them; a slow or absent consumer never applies
|
|
1533
|
+
* backpressure to the run. Once `run.end` is delivered, the iterator
|
|
1534
|
+
* completes.
|
|
1535
|
+
* 3. **Composes `onTrace`.** If `options.onTrace` is passed, it still fires for
|
|
1536
|
+
* every event — the wrapper adds queue delivery, it does not replace the
|
|
1537
|
+
* caller's sink.
|
|
1538
|
+
* 4. **`result` mirrors `runAgent`.** Same resolution and rejection behavior
|
|
1539
|
+
* (see {@link AgentRun.result}). A no-op rejection handler is attached
|
|
1540
|
+
* internally so a caller who reads only `events` never trips an unhandled
|
|
1541
|
+
* rejection on a bind-time throw; the returned promise still rejects for a
|
|
1542
|
+
* caller who awaits it.
|
|
1543
|
+
* 5. **Resume is identical.** Options pass straight through, so starting from a
|
|
1544
|
+
* persisted `snapshot` (+ resume `event`) streams that run's events from its
|
|
1545
|
+
* own `run.start`, exactly as `runAgent` would run it.
|
|
1546
|
+
* 6. **Early termination does not cancel the run.** Breaking out of a
|
|
1547
|
+
* `for await` (or calling `events.return()`) stops delivery, but the run
|
|
1548
|
+
* keeps going and `result` still settles. Cancelling the run itself is
|
|
1549
|
+
* future work — pass `options.signal` to abort it.
|
|
1550
|
+
* 7. **`events` is single-consumer.** See {@link AgentRun.events}.
|
|
1551
|
+
*/
|
|
1552
|
+
function createAgentRun(machine, options) {
|
|
1553
|
+
const buffer = [];
|
|
1554
|
+
const resolvers = [];
|
|
1555
|
+
let closed = false;
|
|
1556
|
+
let stopped = false;
|
|
1557
|
+
const push = (event) => {
|
|
1558
|
+
if (closed || stopped) return;
|
|
1559
|
+
const resolve = resolvers.shift();
|
|
1560
|
+
if (resolve) resolve({
|
|
1561
|
+
value: event,
|
|
1562
|
+
done: false
|
|
1563
|
+
});
|
|
1564
|
+
else buffer.push(event);
|
|
1565
|
+
};
|
|
1566
|
+
const close = () => {
|
|
1567
|
+
if (closed) return;
|
|
1568
|
+
closed = true;
|
|
1569
|
+
while (resolvers.length > 0) resolvers.shift()({
|
|
1570
|
+
value: void 0,
|
|
1571
|
+
done: true
|
|
1572
|
+
});
|
|
1573
|
+
};
|
|
1574
|
+
const userOnTrace = options.onTrace;
|
|
1575
|
+
const result = runAgent(machine, {
|
|
1576
|
+
...options,
|
|
1577
|
+
onTrace: (event) => {
|
|
1578
|
+
push(event);
|
|
1579
|
+
userOnTrace?.(event);
|
|
1580
|
+
if (event.type === "run.end") close();
|
|
1581
|
+
}
|
|
1582
|
+
});
|
|
1583
|
+
result.then(close, close);
|
|
1584
|
+
return {
|
|
1585
|
+
events: {
|
|
1586
|
+
next() {
|
|
1587
|
+
if (buffer.length > 0) return Promise.resolve({
|
|
1588
|
+
value: buffer.shift(),
|
|
1589
|
+
done: false
|
|
1590
|
+
});
|
|
1591
|
+
if (closed || stopped) return Promise.resolve({
|
|
1592
|
+
value: void 0,
|
|
1593
|
+
done: true
|
|
1594
|
+
});
|
|
1595
|
+
return new Promise((resolve) => {
|
|
1596
|
+
resolvers.push(resolve);
|
|
1597
|
+
});
|
|
1598
|
+
},
|
|
1599
|
+
return() {
|
|
1600
|
+
stopped = true;
|
|
1601
|
+
buffer.length = 0;
|
|
1602
|
+
while (resolvers.length > 0) resolvers.shift()({
|
|
1603
|
+
value: void 0,
|
|
1604
|
+
done: true
|
|
1605
|
+
});
|
|
1606
|
+
return Promise.resolve({
|
|
1607
|
+
value: void 0,
|
|
1608
|
+
done: true
|
|
1609
|
+
});
|
|
1610
|
+
},
|
|
1611
|
+
[Symbol.asyncIterator]() {
|
|
1612
|
+
return this;
|
|
1613
|
+
}
|
|
1614
|
+
},
|
|
1615
|
+
result
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
//#endregion
|
|
1753
1619
|
//#region src/provide-executors.ts
|
|
1754
1620
|
/**
|
|
1755
1621
|
* Binds a machine's agent actor sources to a set of host `executors` in one
|
|
@@ -1764,8 +1630,8 @@ function collectPendingUserInputs(snapshot) {
|
|
|
1764
1630
|
*
|
|
1765
1631
|
* behaves like a normal XState actor whose agent invokes now reach real models.
|
|
1766
1632
|
*
|
|
1767
|
-
* Binding pass over `machine.
|
|
1768
|
-
* `options.
|
|
1633
|
+
* Binding pass over `machine.sources.actors` (after merging
|
|
1634
|
+
* `options.actors`):
|
|
1769
1635
|
* - `mode: 'generate'` text source → `executors.generateText`
|
|
1770
1636
|
* - `mode: 'stream'` text source → `executors.streamText`
|
|
1771
1637
|
* - decision / `agent.decide` source → `executors.decide` (snapshot-driven
|
|
@@ -1779,7 +1645,7 @@ function collectPendingUserInputs(snapshot) {
|
|
|
1779
1645
|
*
|
|
1780
1646
|
* A source that already carries its own executor (`.withExecutor(...)`) is left
|
|
1781
1647
|
* as-is. `agent.userInput` is left UNBOUND — an uncontrolled host handles idle
|
|
1782
|
-
* itself, so supply a handler via `options.
|
|
1648
|
+
* itself, so supply a handler via `options.actors` if the machine uses it.
|
|
1783
1649
|
* Non-agent actors are untouched.
|
|
1784
1650
|
*
|
|
1785
1651
|
* Throws at bind time if a source needs an executor kind that `executors` does
|
|
@@ -1794,8 +1660,8 @@ function provideExecutors(machine, executors, options = {}) {
|
|
|
1794
1660
|
onChunk: options.onChunk,
|
|
1795
1661
|
onTrace: options.onTrace
|
|
1796
1662
|
};
|
|
1797
|
-
const provided = options.
|
|
1798
|
-
const effectiveSources = provided.
|
|
1663
|
+
const provided = options.actors ? machine.provide({ actors: options.actors }) : machine;
|
|
1664
|
+
const effectiveSources = provided.sources.actors;
|
|
1799
1665
|
const wrappedSources = {};
|
|
1800
1666
|
const invokedSrcs = getConfiguredInvokeSrcs(provided);
|
|
1801
1667
|
for (const [key, logic] of Object.entries(effectiveSources)) {
|
|
@@ -1827,7 +1693,7 @@ function provideExecutors(machine, executors, options = {}) {
|
|
|
1827
1693
|
continue;
|
|
1828
1694
|
}
|
|
1829
1695
|
}
|
|
1830
|
-
return provided.provide({
|
|
1696
|
+
return provided.provide({ actors: wrappedSources });
|
|
1831
1697
|
}
|
|
1832
1698
|
function missingExecutorError(src, kind, executor) {
|
|
1833
1699
|
return /* @__PURE__ */ new Error(`provideExecutors: actor source '${src}' is a ${kind} source but no '${executor}' executor was provided. Add it to the executors object, or bind the source with its own executor (logic.withExecutor(...)) before calling provideExecutors.`);
|
|
@@ -1883,8 +1749,15 @@ function collectTransitionTargets(value, fromNode, index, out) {
|
|
|
1883
1749
|
return;
|
|
1884
1750
|
}
|
|
1885
1751
|
if (typeof value === "object") {
|
|
1886
|
-
const target = value
|
|
1887
|
-
if (target !== void 0)
|
|
1752
|
+
const { target, to } = value;
|
|
1753
|
+
if (target !== void 0) {
|
|
1754
|
+
collectTransitionTargets(target, fromNode, index, out);
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
if (to !== void 0) {
|
|
1758
|
+
collectTransitionTargets(to, fromNode, index, out);
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1888
1761
|
}
|
|
1889
1762
|
}
|
|
1890
1763
|
function resolveTargetString(target, fromNode, index, out) {
|
|
@@ -1896,12 +1769,16 @@ function resolveTargetString(target, fromNode, index, out) {
|
|
|
1896
1769
|
if (index.has(resolved)) out.targets.push(resolved);
|
|
1897
1770
|
else out.opaque = true;
|
|
1898
1771
|
}
|
|
1899
|
-
function outgoingTargets(node, index) {
|
|
1772
|
+
function outgoingTargets(node, index, staticTargets) {
|
|
1900
1773
|
const out = {
|
|
1901
1774
|
targets: [],
|
|
1902
1775
|
opaque: false
|
|
1903
1776
|
};
|
|
1904
1777
|
const { config } = node;
|
|
1778
|
+
if (staticTargets) {
|
|
1779
|
+
for (const target of staticTargets[node.path] ?? []) resolveTargetString(target, node, index, out);
|
|
1780
|
+
return out;
|
|
1781
|
+
}
|
|
1905
1782
|
for (const value of Object.values(config.on ?? {})) collectTransitionTargets(value, node, index, out);
|
|
1906
1783
|
collectTransitionTargets(config.always, node, index, out);
|
|
1907
1784
|
collectTransitionTargets(config.choice, node, index, out);
|
|
@@ -1913,7 +1790,7 @@ function outgoingTargets(node, index) {
|
|
|
1913
1790
|
}
|
|
1914
1791
|
return out;
|
|
1915
1792
|
}
|
|
1916
|
-
function computeReachable(rootConfig, index) {
|
|
1793
|
+
function computeReachable(rootConfig, index, staticTargets) {
|
|
1917
1794
|
const reachable = /* @__PURE__ */ new Set();
|
|
1918
1795
|
const queue = [];
|
|
1919
1796
|
const markAncestors = (path) => {
|
|
@@ -1938,7 +1815,7 @@ function computeReachable(rootConfig, index) {
|
|
|
1938
1815
|
while (queue.length > 0) {
|
|
1939
1816
|
const node = index.get(queue.shift());
|
|
1940
1817
|
if (!node) continue;
|
|
1941
|
-
const { targets, opaque } = outgoingTargets(node, index);
|
|
1818
|
+
const { targets, opaque } = outgoingTargets(node, index, staticTargets);
|
|
1942
1819
|
for (const target of targets) enter(target);
|
|
1943
1820
|
if (opaque) for (const sibling of childrenOf(index, node.parentPath)) enter(sibling.path);
|
|
1944
1821
|
}
|
|
@@ -1958,11 +1835,11 @@ function ancestorChain(node, index) {
|
|
|
1958
1835
|
function hasNonEmptyOn(config) {
|
|
1959
1836
|
return !!config.on && Object.keys(config.on).length > 0;
|
|
1960
1837
|
}
|
|
1961
|
-
function decisionKindOf(src,
|
|
1838
|
+
function decisionKindOf(src, actors) {
|
|
1962
1839
|
if (typeof src === "string") {
|
|
1963
1840
|
if (src === DECIDE_SRC) return "decision";
|
|
1964
1841
|
if (src === PLAN_SRC) return "plan";
|
|
1965
|
-
const logic =
|
|
1842
|
+
const logic = actors[src];
|
|
1966
1843
|
if (isDecisionLogic(logic)) return "decision";
|
|
1967
1844
|
if (isPlanLogic(logic)) return "plan";
|
|
1968
1845
|
return;
|
|
@@ -2007,7 +1884,7 @@ function checkUnreachableStates(ctx) {
|
|
|
2007
1884
|
function checkDecideWithoutEvents(ctx) {
|
|
2008
1885
|
const out = [];
|
|
2009
1886
|
for (const node of ctx.index.values()) for (const invoke of node.invokes) {
|
|
2010
|
-
const kind = decisionKindOf(invoke.src, ctx.
|
|
1887
|
+
const kind = decisionKindOf(invoke.src, ctx.actors);
|
|
2011
1888
|
if (!kind) continue;
|
|
2012
1889
|
const selfHandles = hasNonEmptyOn(node.config);
|
|
2013
1890
|
const ancestorHandles = ancestorChain(node, ctx.index).some((ancestor) => hasNonEmptyOn(ancestor.config));
|
|
@@ -2044,7 +1921,7 @@ function checkDirectObjectSrc(ctx) {
|
|
|
2044
1921
|
code: "direct-object-src",
|
|
2045
1922
|
severity: "warning",
|
|
2046
1923
|
path: node.path,
|
|
2047
|
-
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({
|
|
1924
|
+
message: `State '${node.path}' invokes a direct-object agent logic. Direct-object invoke srcs cannot be rebound by runAgent, so they inherit no host executors — call '.withExecutor(...)' on the logic, or register it as a string-keyed actor source (machine.provide({ actors: { name: logic } })) and invoke it by name.`
|
|
2048
1925
|
});
|
|
2049
1926
|
}
|
|
2050
1927
|
return out;
|
|
@@ -2088,8 +1965,8 @@ function isBuiltinOrWildcardEvent(eventType) {
|
|
|
2088
1965
|
}
|
|
2089
1966
|
function checkUndeclaredEvents(ctx) {
|
|
2090
1967
|
const declared = ctx.schemas?.events;
|
|
2091
|
-
|
|
2092
|
-
|
|
1968
|
+
const declaredTypes = new Set(Object.keys(declared ?? {}));
|
|
1969
|
+
if ([...declaredTypes].every((type) => type.startsWith("@agent."))) return [];
|
|
2093
1970
|
const out = [];
|
|
2094
1971
|
for (const node of ctx.index.values()) for (const eventType of Object.keys(node.config.on ?? {})) {
|
|
2095
1972
|
if (declaredTypes.has(eventType) || isBuiltinOrWildcardEvent(eventType)) continue;
|
|
@@ -2141,7 +2018,7 @@ const LINT_CHECKS = [
|
|
|
2141
2018
|
function lintAgentMachine(machine, options = {}) {
|
|
2142
2019
|
const config = machine.config ?? {};
|
|
2143
2020
|
const index = buildStateIndex(config);
|
|
2144
|
-
const reachable = computeReachable(config, index);
|
|
2021
|
+
const reachable = computeReachable(config, index, getMachineStaticTransitionTargets(machine));
|
|
2145
2022
|
const registered = getRegisteredAgentExecutionOptions(machine);
|
|
2146
2023
|
const ctx = {
|
|
2147
2024
|
machine,
|
|
@@ -2149,7 +2026,7 @@ function lintAgentMachine(machine, options = {}) {
|
|
|
2149
2026
|
index,
|
|
2150
2027
|
reachable,
|
|
2151
2028
|
schemas: registered.schemas,
|
|
2152
|
-
|
|
2029
|
+
actors: registered.actors ?? machine.sources?.actors ?? {}
|
|
2153
2030
|
};
|
|
2154
2031
|
const disabled = new Set(options.disable ?? []);
|
|
2155
2032
|
return LINT_CHECKS.flatMap((check) => check(ctx)).filter((d) => !disabled.has(d.code));
|
|
@@ -2159,11 +2036,11 @@ function lintAgentMachine(machine, options = {}) {
|
|
|
2159
2036
|
* `diagnostics` holds the findings; the message lists them one per finding,
|
|
2160
2037
|
* so a test runner's failure output reads like the CLI's lint report.
|
|
2161
2038
|
*/
|
|
2162
|
-
var AgentLintError = class extends
|
|
2039
|
+
var AgentLintError = class extends AgentError {
|
|
2163
2040
|
diagnostics;
|
|
2164
2041
|
constructor(machineId, diagnostics) {
|
|
2165
2042
|
const lines = diagnostics.map((d) => ` ${d.severity === "error" ? "error" : "warn "} ${d.code} ${d.path}\n ${d.message}`);
|
|
2166
|
-
super(`Agent machine '${machineId}' failed lint (${diagnostics.length} finding(s)):\n${lines.join("\n")}`);
|
|
2043
|
+
super("lint-failed", `Agent machine '${machineId}' failed lint (${diagnostics.length} finding(s)):\n${lines.join("\n")}`);
|
|
2167
2044
|
this.name = "AgentLintError";
|
|
2168
2045
|
this.diagnostics = diagnostics;
|
|
2169
2046
|
}
|
|
@@ -2490,4 +2367,433 @@ async function canReach(machine, statePath, options = {}) {
|
|
|
2490
2367
|
} : { canReach: false };
|
|
2491
2368
|
}
|
|
2492
2369
|
//#endregion
|
|
2493
|
-
|
|
2370
|
+
//#region src/trajectory.ts
|
|
2371
|
+
function isRecord$2(value) {
|
|
2372
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2373
|
+
}
|
|
2374
|
+
/**
|
|
2375
|
+
* Classifies one item. Event-shaped things (log entries, events) become events;
|
|
2376
|
+
* everything else is a state value. A snapshot is unwrapped to its `value`.
|
|
2377
|
+
* @internal
|
|
2378
|
+
*/
|
|
2379
|
+
function normalize(item) {
|
|
2380
|
+
if (isRecord$2(item)) {
|
|
2381
|
+
const nested = item["event"];
|
|
2382
|
+
if (isRecord$2(nested) && typeof nested["type"] === "string") return {
|
|
2383
|
+
kind: "event",
|
|
2384
|
+
event: nested
|
|
2385
|
+
};
|
|
2386
|
+
if (typeof item["type"] === "string") return {
|
|
2387
|
+
kind: "event",
|
|
2388
|
+
event: item
|
|
2389
|
+
};
|
|
2390
|
+
if ("value" in item && "status" in item) return {
|
|
2391
|
+
kind: "state",
|
|
2392
|
+
value: item["value"]
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
return {
|
|
2396
|
+
kind: "state",
|
|
2397
|
+
value: item
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
/** Every leaf of a state value as a dot path (`{ review: 'editing' }` → `review.editing`). @internal */
|
|
2401
|
+
function statePaths(value) {
|
|
2402
|
+
if (typeof value === "string") return [value];
|
|
2403
|
+
if (!isRecord$2(value)) return [];
|
|
2404
|
+
return Object.entries(value).flatMap(([key, child]) => statePaths(child).map((path) => `${key}.${path}`));
|
|
2405
|
+
}
|
|
2406
|
+
/**
|
|
2407
|
+
* A string expectation matches a state value when it names one of its leaves or
|
|
2408
|
+
* an ancestor of one, so `'review'` matches `{ review: 'editing' }`. @internal
|
|
2409
|
+
*/
|
|
2410
|
+
function stateMatchesPath(path, value) {
|
|
2411
|
+
return statePaths(value).some((leaf) => leaf === path || leaf.startsWith(`${path}.`));
|
|
2412
|
+
}
|
|
2413
|
+
function deepEqual(a, b) {
|
|
2414
|
+
if (Object.is(a, b)) return true;
|
|
2415
|
+
if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => deepEqual(item, b[index]));
|
|
2416
|
+
if (!isRecord$2(a) || !isRecord$2(b)) return false;
|
|
2417
|
+
const keys = Object.keys(a);
|
|
2418
|
+
return keys.length === Object.keys(b).length && keys.every((key) => deepEqual(a[key], b[key]));
|
|
2419
|
+
}
|
|
2420
|
+
/**
|
|
2421
|
+
* Does one actual item satisfy one expected item?
|
|
2422
|
+
*
|
|
2423
|
+
* - A string expectation matches an event's `type`, or a state value it names.
|
|
2424
|
+
* - An event expectation matches an event with the same `type` whose other
|
|
2425
|
+
* declared keys deep-equal, so payload details are opt-in.
|
|
2426
|
+
* - A state-value expectation matches structurally.
|
|
2427
|
+
* @internal
|
|
2428
|
+
*/
|
|
2429
|
+
function matchesItem(expected, actual) {
|
|
2430
|
+
const actualItem = normalize(actual);
|
|
2431
|
+
if (typeof expected === "string") return actualItem.kind === "event" ? actualItem.event.type === expected : stateMatchesPath(expected, actualItem.value);
|
|
2432
|
+
const expectedItem = normalize(expected);
|
|
2433
|
+
if (expectedItem.kind !== actualItem.kind) return false;
|
|
2434
|
+
if (expectedItem.kind === "event" && actualItem.kind === "event") return Object.keys(expectedItem.event).every((key) => deepEqual(expectedItem.event[key], actualItem.event[key]));
|
|
2435
|
+
return deepEqual(expectedItem.value, actualItem.value);
|
|
2436
|
+
}
|
|
2437
|
+
/**
|
|
2438
|
+
* Matches a run's trajectory against an expected one.
|
|
2439
|
+
*
|
|
2440
|
+
* Both trajectories may be state values (collected from `onTransition`) or
|
|
2441
|
+
* events (`result.events`, whose {@link AgentLogEntry} envelopes are unwrapped);
|
|
2442
|
+
* items are compared by shape, so `['prompting', 'drafting']` scores a state
|
|
2443
|
+
* path and `['PROMPT_SUBMITTED', 'SEND']` scores an event log with the same
|
|
2444
|
+
* call.
|
|
2445
|
+
*
|
|
2446
|
+
* Semantics are an ordered subsequence: every expected item must appear in
|
|
2447
|
+
* `actual`, in order, with gaps allowed. Pass `{ exact: true }` to require
|
|
2448
|
+
* equality instead.
|
|
2449
|
+
*
|
|
2450
|
+
* @example Trajectory assertions in a test
|
|
2451
|
+
* ```ts
|
|
2452
|
+
* const statePath: unknown[] = [];
|
|
2453
|
+
* const result = await runAgent(machine, {
|
|
2454
|
+
* input,
|
|
2455
|
+
* executors,
|
|
2456
|
+
* onTransition: (snapshot) => statePath.push(snapshot.value),
|
|
2457
|
+
* });
|
|
2458
|
+
*
|
|
2459
|
+
* const path = matchesTrajectory(statePath, ['prompting', 'drafting', 'sent']);
|
|
2460
|
+
* expect(path.matched, JSON.stringify(path.firstMiss)).toBe(true);
|
|
2461
|
+
* expect(matchesTrajectory(result.events, ['PROMPT_SUBMITTED', 'SEND']).matched).toBe(true);
|
|
2462
|
+
* ```
|
|
2463
|
+
*
|
|
2464
|
+
* @example Partial credit in an eval scorer
|
|
2465
|
+
* ```ts
|
|
2466
|
+
* const match = matchesTrajectory(output.statePath, expected.statePath);
|
|
2467
|
+
* return { name: 'state_path', score: match.score, metadata: { ...match.firstMiss } };
|
|
2468
|
+
* ```
|
|
2469
|
+
*/
|
|
2470
|
+
function matchesTrajectory(actual, expected, options = {}) {
|
|
2471
|
+
const expectedCount = expected.length;
|
|
2472
|
+
if (options.exact) {
|
|
2473
|
+
let matchedCount = 0;
|
|
2474
|
+
let firstMiss;
|
|
2475
|
+
for (let index = 0; index < expectedCount; index++) {
|
|
2476
|
+
if (index < actual.length && matchesItem(expected[index], actual[index])) {
|
|
2477
|
+
matchedCount++;
|
|
2478
|
+
continue;
|
|
2479
|
+
}
|
|
2480
|
+
firstMiss = {
|
|
2481
|
+
index,
|
|
2482
|
+
expected: expected[index],
|
|
2483
|
+
searchedFrom: index
|
|
2484
|
+
};
|
|
2485
|
+
break;
|
|
2486
|
+
}
|
|
2487
|
+
const matched = !firstMiss && actual.length === expectedCount;
|
|
2488
|
+
const denominator = Math.max(expectedCount, actual.length);
|
|
2489
|
+
return {
|
|
2490
|
+
matched,
|
|
2491
|
+
matchedCount,
|
|
2492
|
+
expectedCount,
|
|
2493
|
+
score: matched ? 1 : denominator === 0 ? 1 : matchedCount / denominator,
|
|
2494
|
+
...firstMiss ? { firstMiss } : {}
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
let cursor = 0;
|
|
2498
|
+
let matchedCount = 0;
|
|
2499
|
+
for (let index = 0; index < expectedCount; index++) {
|
|
2500
|
+
const want = expected[index];
|
|
2501
|
+
let found = -1;
|
|
2502
|
+
for (let at = cursor; at < actual.length; at++) if (matchesItem(want, actual[at])) {
|
|
2503
|
+
found = at;
|
|
2504
|
+
break;
|
|
2505
|
+
}
|
|
2506
|
+
if (found === -1) return {
|
|
2507
|
+
matched: false,
|
|
2508
|
+
matchedCount,
|
|
2509
|
+
expectedCount,
|
|
2510
|
+
score: matchedCount / expectedCount,
|
|
2511
|
+
firstMiss: {
|
|
2512
|
+
index,
|
|
2513
|
+
expected: want,
|
|
2514
|
+
searchedFrom: cursor
|
|
2515
|
+
}
|
|
2516
|
+
};
|
|
2517
|
+
matchedCount++;
|
|
2518
|
+
cursor = found + 1;
|
|
2519
|
+
}
|
|
2520
|
+
return {
|
|
2521
|
+
matched: true,
|
|
2522
|
+
matchedCount,
|
|
2523
|
+
expectedCount,
|
|
2524
|
+
score: 1
|
|
2525
|
+
};
|
|
2526
|
+
}
|
|
2527
|
+
//#endregion
|
|
2528
|
+
//#region src/scripted-executors.ts
|
|
2529
|
+
/**
|
|
2530
|
+
* Scripted executors — a keyless, deterministic stand-in for a model host.
|
|
2531
|
+
*
|
|
2532
|
+
* `createScriptedExecutors` builds a full `{ generateText, streamText, decide }`
|
|
2533
|
+
* set that plays back canned answers from FIFO queues, so `runAgent` (or
|
|
2534
|
+
* `provideExecutors`, or a bare `TextLogic.execute`) runs with no API key and no
|
|
2535
|
+
* network. It is the fastest way to see a machine run, and the least ceremonial
|
|
2536
|
+
* way to test one: same machine, same executor contract, scripted answers.
|
|
2537
|
+
*/
|
|
2538
|
+
/** Thrown when a scripted queue runs dry on a pending request. */
|
|
2539
|
+
var ScriptedExecutorsError = class extends AgentError {
|
|
2540
|
+
constructor(message) {
|
|
2541
|
+
super("scripted-executors-exhausted", message);
|
|
2542
|
+
this.name = "ScriptedExecutorsError";
|
|
2543
|
+
}
|
|
2544
|
+
};
|
|
2545
|
+
function isRecord$1(value) {
|
|
2546
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2547
|
+
}
|
|
2548
|
+
/** The only own keys an executor-result envelope carries. @internal */
|
|
2549
|
+
const TEXT_ENVELOPE_KEYS = new Set([
|
|
2550
|
+
"output",
|
|
2551
|
+
"usage",
|
|
2552
|
+
"raw"
|
|
2553
|
+
]);
|
|
2554
|
+
/**
|
|
2555
|
+
* True when a scripted entry is the executor envelope rather than the output
|
|
2556
|
+
* value: it owns an `output` key and owns NOTHING outside the envelope's own
|
|
2557
|
+
* vocabulary. Bare `'output' in value` would swallow an output object's
|
|
2558
|
+
* siblings (`{ output: 'draft', confidence: 0.9 }` would lose `confidence`)
|
|
2559
|
+
* and would also match an inherited `output`. @internal
|
|
2560
|
+
*/
|
|
2561
|
+
function isTextEnvelope(value) {
|
|
2562
|
+
return Object.hasOwn(value, "output") && Object.keys(value).every((key) => TEXT_ENVELOPE_KEYS.has(key));
|
|
2563
|
+
}
|
|
2564
|
+
/**
|
|
2565
|
+
* Resolves ONE scripted text entry to an executor result: a function entry is
|
|
2566
|
+
* called with the request, and the value is taken as the raw envelope only when
|
|
2567
|
+
* it is one (see {@link isTextEnvelope}). Shared with `runSeam`, whose routed
|
|
2568
|
+
* queues follow the same entry conventions. @internal
|
|
2569
|
+
*/
|
|
2570
|
+
async function resolveScriptedTextEntry(entry, request, info) {
|
|
2571
|
+
const value = typeof entry === "function" ? await entry(request, info) : entry;
|
|
2572
|
+
return isRecord$1(value) && isTextEnvelope(value) ? value : { output: value };
|
|
2573
|
+
}
|
|
2574
|
+
/**
|
|
2575
|
+
* Keyless executors that replay a script instead of calling a model. Every
|
|
2576
|
+
* slot is provided, so any machine binds; a request with no entry left throws a
|
|
2577
|
+
* descriptive error naming what was pending.
|
|
2578
|
+
*
|
|
2579
|
+
* Queues are consumed FIFO and are copied on creation, so the caller's arrays
|
|
2580
|
+
* are never mutated and each call builds a fresh, independent playthrough.
|
|
2581
|
+
* Entries may be plain values or functions of the request, which is how one
|
|
2582
|
+
* script serves a machine that loops or branches: route on `request.name` (the
|
|
2583
|
+
* `setupAgent({ requests })` key) or on the decision's candidate `events`.
|
|
2584
|
+
*
|
|
2585
|
+
* @example
|
|
2586
|
+
* ```ts
|
|
2587
|
+
* const result = await runAgent(moderationMachine, {
|
|
2588
|
+
* input: { comment: 'honestly this update is terrible', trust: 20 },
|
|
2589
|
+
* executors: createScriptedExecutors({
|
|
2590
|
+
* decisions: [{ type: 'FLAG', reason: 'Borderline tone.' }],
|
|
2591
|
+
* }),
|
|
2592
|
+
* });
|
|
2593
|
+
* ```
|
|
2594
|
+
*
|
|
2595
|
+
* @example Dynamic entries
|
|
2596
|
+
* ```ts
|
|
2597
|
+
* createScriptedExecutors({
|
|
2598
|
+
* text: [(request) => `Draft about ${request.prompt}`],
|
|
2599
|
+
* decisions: [(request) => ({ type: request.events[0]!.type })],
|
|
2600
|
+
* });
|
|
2601
|
+
* ```
|
|
2602
|
+
*/
|
|
2603
|
+
function createScriptedExecutors(script = {}) {
|
|
2604
|
+
const decisions = [...script.decisions ?? []];
|
|
2605
|
+
const text = [...script.text ?? []];
|
|
2606
|
+
const nextText = async (request, info) => {
|
|
2607
|
+
if (text.length === 0) throw new ScriptedExecutorsError(`createScriptedExecutors: script ran dry on a pending text request ${describeText(request)}. Add another entry to the script's \`text\` queue.`);
|
|
2608
|
+
return resolveScriptedTextEntry(text.shift(), request, info);
|
|
2609
|
+
};
|
|
2610
|
+
return {
|
|
2611
|
+
generateText: nextText,
|
|
2612
|
+
streamText: async (request, info) => {
|
|
2613
|
+
const result = await nextText(request, info);
|
|
2614
|
+
if (typeof result.output === "string") info?.onChunk?.(result.output);
|
|
2615
|
+
return result;
|
|
2616
|
+
},
|
|
2617
|
+
decide: async (request) => {
|
|
2618
|
+
if (decisions.length === 0) throw new ScriptedExecutorsError(`createScriptedExecutors: script ran dry on a pending decision request (id '${request.id}'). Add another entry to the script's \`decisions\` queue. Candidate events: ${request.events.map((event) => event.type).join(", ") || "(none)"}.`);
|
|
2619
|
+
const entry = decisions.shift();
|
|
2620
|
+
const value = typeof entry === "function" ? await entry(request) : entry;
|
|
2621
|
+
return typeof value.type === "string" ? { event: value } : value;
|
|
2622
|
+
}
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
function describeText(request) {
|
|
2626
|
+
return request.name ? `'${request.name}'` : `(model '${request.model}')`;
|
|
2627
|
+
}
|
|
2628
|
+
//#endregion
|
|
2629
|
+
//#region src/seam.ts
|
|
2630
|
+
/** Thrown when the call plan runs dry on a request that is not the live seam. */
|
|
2631
|
+
var SeamScriptError = class extends AgentError {
|
|
2632
|
+
constructor(message) {
|
|
2633
|
+
super("seam-script-exhausted", message);
|
|
2634
|
+
this.name = "SeamScriptError";
|
|
2635
|
+
}
|
|
2636
|
+
};
|
|
2637
|
+
function seamKeyOf(seam) {
|
|
2638
|
+
return seam.request ?? seam.model;
|
|
2639
|
+
}
|
|
2640
|
+
function describeRequest(request) {
|
|
2641
|
+
return request.name ? `'${request.name}' (model '${request.model}')` : `(model '${request.model}')`;
|
|
2642
|
+
}
|
|
2643
|
+
function isRecord(value) {
|
|
2644
|
+
return typeof value === "object" && value !== null;
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* The seam's own answer, for scoring. Our `{ output }` envelope is read
|
|
2648
|
+
* directly; a raw AI SDK `generateText` result contributes its `text`. A raw
|
|
2649
|
+
* STREAM result is left alone — the machine consumes that stream, and reading
|
|
2650
|
+
* it here would steal the chunks — so `seamOutput` is `undefined` for a
|
|
2651
|
+
* streaming candidate that returns one. Score its trajectory instead.
|
|
2652
|
+
* @internal
|
|
2653
|
+
*/
|
|
2654
|
+
async function seamOutputOf(result, request) {
|
|
2655
|
+
if (!isRecord(result)) return;
|
|
2656
|
+
if ("output" in result) return await result["output"];
|
|
2657
|
+
if ("textStream" in result) return;
|
|
2658
|
+
return await normalizeGeneratorResult(result, `seam '${request.name ?? request.model}'`, { request });
|
|
2659
|
+
}
|
|
2660
|
+
/**
|
|
2661
|
+
* Runs a machine end to end with one model call under test, and slices the run
|
|
2662
|
+
* at that call.
|
|
2663
|
+
*
|
|
2664
|
+
* The `before`/`after` slices are the point: `after.statePath` is the branch
|
|
2665
|
+
* the seam chose and `after.events` is the same question against the durable
|
|
2666
|
+
* log, both ready for `matchesTrajectory`. The state slice splits where the
|
|
2667
|
+
* state path stood when the seam answered; the event slice splits at the
|
|
2668
|
+
* seam's own effect completion (the first `xstate.done.*`/`xstate.error.*`
|
|
2669
|
+
* entry appended after the call was made).
|
|
2670
|
+
*
|
|
2671
|
+
* @example Keyless: the seam is scripted too, so the whole thing runs offline.
|
|
2672
|
+
* ```ts
|
|
2673
|
+
* const run = await runSeam(emailDrafter, {
|
|
2674
|
+
* scripts: { promptEvaluator: [vague, complete], emailDrafter: [draft] },
|
|
2675
|
+
* seam: { request: 'evaluatePrompt' },
|
|
2676
|
+
* respond: ({ state }) => (state === 'prompting' ? { type: 'PROMPT_SUBMITTED', prompt } : null),
|
|
2677
|
+
* });
|
|
2678
|
+
*
|
|
2679
|
+
* matchesTrajectory(run.after.statePath, ['needsMoreInfo', 'drafting']);
|
|
2680
|
+
* ```
|
|
2681
|
+
*
|
|
2682
|
+
* @example A candidate prompt at the seam, scored against the same rows.
|
|
2683
|
+
* ```ts
|
|
2684
|
+
* const { generateText } = createAiSdkExecutors({ models });
|
|
2685
|
+
* const run = await runSeam(emailDrafter, { ...row, candidate: generateText });
|
|
2686
|
+
* ```
|
|
2687
|
+
*/
|
|
2688
|
+
async function runSeam(machine, options) {
|
|
2689
|
+
const { seam, candidate } = options;
|
|
2690
|
+
const queues = new Map(Object.entries(options.scripts ?? {}).map(([key, answers]) => [key, [...answers]]));
|
|
2691
|
+
const statePath = [];
|
|
2692
|
+
let calls = 0;
|
|
2693
|
+
let seamMatches = 0;
|
|
2694
|
+
let seamOutput;
|
|
2695
|
+
let seamReached = false;
|
|
2696
|
+
let callsBeforeSeam = -1;
|
|
2697
|
+
let seamStateAt = 0;
|
|
2698
|
+
let seamEventAt = 0;
|
|
2699
|
+
let liveEvents = 0;
|
|
2700
|
+
const scriptedAnswer = async (request, info, optional) => {
|
|
2701
|
+
const key = request.name !== void 0 && queues.has(request.name) ? request.name : request.model;
|
|
2702
|
+
const queue = queues.get(key);
|
|
2703
|
+
if (!queue?.length) {
|
|
2704
|
+
if (optional) return;
|
|
2705
|
+
throw new SeamScriptError(`runSeam: no scripted answer left for request ${describeRequest(request)}. Add an entry to \`scripts.${key}\` — its last entry repeats, so one extra answer covers a longer branch.`);
|
|
2706
|
+
}
|
|
2707
|
+
return resolveScriptedTextEntry(queue.length === 1 ? queue[0] : queue.shift(), request, info);
|
|
2708
|
+
};
|
|
2709
|
+
const route = async (request, info) => {
|
|
2710
|
+
const callIndex = calls++;
|
|
2711
|
+
const isSeam = (seam.request !== void 0 ? request.name === seam.request : request.model === seam.model) && seamMatches++ === (seam.occurrence ?? 0);
|
|
2712
|
+
const scripted = await scriptedAnswer(request, info, isSeam && candidate !== void 0);
|
|
2713
|
+
if (!isSeam) return scripted;
|
|
2714
|
+
seamReached = true;
|
|
2715
|
+
callsBeforeSeam = callIndex;
|
|
2716
|
+
seamStateAt = statePath.length;
|
|
2717
|
+
seamEventAt = liveEvents;
|
|
2718
|
+
if (!candidate) {
|
|
2719
|
+
if (!scripted) throw new SeamScriptError(`runSeam: the seam '${seamKeyOf(seam)}' has no candidate and no scripted answer. Pass \`candidate\`, or script the seam's key.`);
|
|
2720
|
+
seamOutput = scripted.output;
|
|
2721
|
+
return scripted;
|
|
2722
|
+
}
|
|
2723
|
+
const result = await candidate(request, info);
|
|
2724
|
+
seamOutput = await seamOutputOf(result, request);
|
|
2725
|
+
return result;
|
|
2726
|
+
};
|
|
2727
|
+
const executors = {
|
|
2728
|
+
...options.executors,
|
|
2729
|
+
generateText: route,
|
|
2730
|
+
streamText: async (request, info) => {
|
|
2731
|
+
const result = await route(request, info);
|
|
2732
|
+
const output = isRecord(result) ? result["output"] : void 0;
|
|
2733
|
+
if (typeof output === "string") info?.onChunk?.(output);
|
|
2734
|
+
return result;
|
|
2735
|
+
}
|
|
2736
|
+
};
|
|
2737
|
+
let snapshot;
|
|
2738
|
+
let event;
|
|
2739
|
+
let events = [];
|
|
2740
|
+
let result;
|
|
2741
|
+
const maxTurns = options.maxTurns ?? 12;
|
|
2742
|
+
for (let turn = 0; turn <= maxTurns; turn++) {
|
|
2743
|
+
result = await runAgent(machine, {
|
|
2744
|
+
...snapshot ? { snapshot } : { input: options.input },
|
|
2745
|
+
...event ? { event } : {},
|
|
2746
|
+
...options.isSuspended ? { isSuspended: options.isSuspended } : {},
|
|
2747
|
+
...options.actors ? { actors: options.actors } : {},
|
|
2748
|
+
events,
|
|
2749
|
+
executors,
|
|
2750
|
+
onEvent: () => {
|
|
2751
|
+
liveEvents++;
|
|
2752
|
+
},
|
|
2753
|
+
onTransition: (next, causedBy) => {
|
|
2754
|
+
if (turn > 0 && causedBy.type === "@xstate.init") return;
|
|
2755
|
+
statePath.push(next.value);
|
|
2756
|
+
}
|
|
2757
|
+
});
|
|
2758
|
+
events = result.events;
|
|
2759
|
+
liveEvents = events.length;
|
|
2760
|
+
if (result.status !== "idle" || turn === maxTurns) break;
|
|
2761
|
+
const next = options.respond?.({
|
|
2762
|
+
snapshot: result.snapshot,
|
|
2763
|
+
state: result.snapshot.value,
|
|
2764
|
+
meta: getStateMeta(result.snapshot),
|
|
2765
|
+
turn,
|
|
2766
|
+
result
|
|
2767
|
+
});
|
|
2768
|
+
if (!next) break;
|
|
2769
|
+
event = next;
|
|
2770
|
+
snapshot = result.snapshot;
|
|
2771
|
+
}
|
|
2772
|
+
let splitAt = events.length;
|
|
2773
|
+
if (seamReached) {
|
|
2774
|
+
splitAt = seamEventAt;
|
|
2775
|
+
for (let index = seamEventAt; index < events.length; index++) {
|
|
2776
|
+
const type = events[index].event.type;
|
|
2777
|
+
if (type.startsWith("xstate.done") || type.startsWith("xstate.error")) {
|
|
2778
|
+
splitAt = index;
|
|
2779
|
+
break;
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
const splitStateAt = seamReached ? seamStateAt : statePath.length;
|
|
2784
|
+
return {
|
|
2785
|
+
result,
|
|
2786
|
+
seamOutput,
|
|
2787
|
+
callsBeforeSeam,
|
|
2788
|
+
before: {
|
|
2789
|
+
statePath: statePath.slice(0, splitStateAt),
|
|
2790
|
+
events: events.slice(0, splitAt)
|
|
2791
|
+
},
|
|
2792
|
+
after: {
|
|
2793
|
+
statePath: statePath.slice(splitStateAt),
|
|
2794
|
+
events: events.slice(splitAt)
|
|
2795
|
+
}
|
|
2796
|
+
};
|
|
2797
|
+
}
|
|
2798
|
+
//#endregion
|
|
2799
|
+
export { AGENT_EVENT_SCHEMA_VERSION, AGENT_INIT_EVENT_TYPE, AGENT_TRACE_SCHEMA_VERSION, AGENT_USAGE_EVENT_TYPE, AgentDecisionExhaustedError, AgentError, AgentEventLogConflictError, AgentIdleError, AgentIllegalResumeEventError, AgentLintError, AgentReplayDivergenceError, AgentReplayMachineMismatchError, AgentSnapshotVersionMismatchError, NonSerializableAgentEventError, PLAN_DONE_EVENT_TYPE, appendMessages, assertAgentLogEntry, assertAgentMachine, assertEventLogStoreConformance, assertJsonSerializable, assistantMessage, bindRequestExecutor, buildEnvelopeSchema, canReach, createAgentActor, createAgentRun, createAgentSchemas, createInMemoryEventLogStore, createReplayEntry, createScriptedExecutors, createTextLogic, diffEventLogs, executeAgentRequest, explorePaths, generateResult, getAcceptedEvents, getAgentEffects, getAgentMessages, getAgentOutputMode, getCallUsage, getJsonSchema, getJsonSchemaSync, getMachineStructuralHash, getStateMeta, initEntry, inspectTransitions, isStandardSchema, lintAgentMachine, matchesTrajectory, messagesSchema, parseAgentEvent, parseModelRef, parseOutput, parseStructuredEnvelope, persistSnapshot, provideExecutors, renderDecisionAttempts, replay, resolveDecision, runAgent, runSeam, serializeTraceEvent, setupAgent, simulateAgent, systemMessage, toolMessage, traceTransitions, userMessage, verifyReplay };
|