@statelyai/agent 1.1.6 → 2.0.0-alpha.5
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/LICENSE +21 -0
- package/dist/ai-sdk.cjs +249 -0
- package/dist/ai-sdk.d.cts +168 -0
- package/dist/ai-sdk.d.mts +168 -0
- package/dist/ai-sdk.mjs +241 -0
- package/dist/cli.cjs +63 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +64 -0
- package/dist/decision-FTmbqSEe.mjs +938 -0
- package/dist/decision-pC-bY2DE.cjs +1231 -0
- package/dist/index.cjs +54 -0
- package/dist/index.d.cts +1217 -0
- package/dist/index.d.mts +1194 -405
- package/dist/index.mjs +3 -588
- package/dist/openai-compat.cjs +319 -0
- package/dist/openai-compat.d.cts +98 -0
- package/dist/openai-compat.d.mts +98 -0
- package/dist/openai-compat.mjs +312 -0
- package/dist/src-CjpHDU8F.mjs +2445 -0
- package/dist/src-DcRsWPfV.cjs +2564 -0
- package/dist/text-logic-1ZQkO3zr.d.cts +682 -0
- package/dist/text-logic-2EMJIS-n.d.mts +682 -0
- package/dist/types-BHjeDdch.d.cts +208 -0
- package/dist/types-Cq1YlAQ6.d.mts +208 -0
- package/dist/utils-CWUCa3pF.d.mts +108 -0
- package/dist/utils-lK1wnL2i.d.cts +108 -0
- package/dist/zod.cjs +31 -0
- package/dist/zod.d.cts +30 -0
- package/dist/zod.d.mts +30 -0
- package/dist/zod.mjs +30 -0
- package/package.json +109 -28
- package/readme.md +144 -6
- package/schemas/agent-workflow.json +527 -0
- package/.changeset/README.md +0 -8
- package/.changeset/config.json +0 -11
- package/.env.template +0 -3
- package/.github/actions/ci-setup/action.yml +0 -24
- package/.github/workflows/release.yml +0 -46
- package/.vscode/launch.json +0 -28
- package/CHANGELOG.md +0 -222
- package/dist/index.d.ts +0 -428
- package/dist/index.js +0 -621
- package/examples/chatbot.ts +0 -71
- package/examples/cot.ts +0 -89
- package/examples/email.ts +0 -118
- package/examples/example.ts +0 -81
- package/examples/goal.ts +0 -94
- package/examples/helpers/helpers.ts +0 -17
- package/examples/helpers/loader.ts +0 -32
- package/examples/helpers/runner.ts +0 -27
- package/examples/joke.ts +0 -225
- package/examples/multi.ts +0 -103
- package/examples/newspaper.ts +0 -324
- package/examples/number.ts +0 -102
- package/examples/raffle.ts +0 -105
- package/examples/sandbox.ts +0 -28
- package/examples/simple.ts +0 -39
- package/examples/support.ts +0 -147
- package/examples/ticTacToe.ts +0 -224
- package/examples/todo.ts +0 -137
- package/examples/tutor.ts +0 -100
- package/examples/verify.ts +0 -120
- package/examples/weather.ts +0 -178
- package/examples/wiki.ts +0 -30
- package/examples/word.ts +0 -171
- package/src/adapters/vercel.ts +0 -7
- package/src/agent-experimental.ts +0 -221
- package/src/agent.test.ts +0 -506
- package/src/agent.ts +0 -300
- package/src/decision.test.ts +0 -179
- package/src/decision.ts +0 -84
- package/src/index.ts +0 -4
- package/src/memory.ts +0 -25
- package/src/planners/shortestPathPlanner.ts +0 -22
- package/src/planners/simplePlanner.ts +0 -139
- package/src/schemas.ts +0 -11
- package/src/strategies/chain-of-note.ts +0 -155
- package/src/templates/defaultText.ts +0 -18
- package/src/text.ts +0 -241
- package/src/types.ts +0 -499
- package/src/utils.ts +0 -72
- package/tsconfig.json +0 -109
- package/vitest.config.ts +0 -9
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
import { createAsyncLogic, createLogic, getNextTransitions } from "xstate";
|
|
2
|
+
//#region src/utils.ts
|
|
3
|
+
/**
|
|
4
|
+
* Deep-clones a snapshot to a plain-JSON value via a `JSON` round-trip, the
|
|
5
|
+
* shape you persist and later feed back to `runAgent({ snapshot })`. Asserts
|
|
6
|
+
* JSON-serializability: functions, `undefined`, and other non-JSON values are
|
|
7
|
+
* dropped or throw exactly as `JSON.stringify`/`JSON.parse` would. Returns a
|
|
8
|
+
* plain-JSON deep clone, not a live snapshot.
|
|
9
|
+
*/
|
|
10
|
+
function persistSnapshot(snapshot) {
|
|
11
|
+
return JSON.parse(JSON.stringify(snapshot));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Walks a context value and returns the dot-paths of the first few values that
|
|
15
|
+
* would NOT survive a JSON persist/resume round-trip (see {@link persistSnapshot}):
|
|
16
|
+
* `Date`, `Map`, `Set`, `RegExp`, functions, `undefined`, `bigint`, class
|
|
17
|
+
* instances (non-plain objects), and circular references. Plain objects,
|
|
18
|
+
* arrays, and JSON primitives are walked/allowed. Returns `[]` for a
|
|
19
|
+
* fully-JSON-safe value. Cheap and bounded (stops after `limit` findings) —
|
|
20
|
+
* intended for a dev-only warning at the moment persistence matters.
|
|
21
|
+
*/
|
|
22
|
+
function findNonSerializableContextPaths(context, limit = 5) {
|
|
23
|
+
const paths = [];
|
|
24
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
25
|
+
const walk = (value, path) => {
|
|
26
|
+
if (paths.length >= limit) return;
|
|
27
|
+
if (value === null) return;
|
|
28
|
+
const type = typeof value;
|
|
29
|
+
if (type === "string" || type === "number" || type === "boolean") return;
|
|
30
|
+
if (type === "undefined" || type === "bigint" || type === "function" || type === "symbol") {
|
|
31
|
+
paths.push(`${path} (${type})`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const obj = value;
|
|
35
|
+
if (seen.has(obj)) {
|
|
36
|
+
paths.push(`${path} (circular)`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
seen.add(obj);
|
|
41
|
+
value.forEach((item, index) => walk(item, `${path}[${index}]`));
|
|
42
|
+
seen.delete(obj);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const proto = Object.getPrototypeOf(value);
|
|
46
|
+
if (proto === Object.prototype || proto === null) {
|
|
47
|
+
seen.add(obj);
|
|
48
|
+
for (const [key, item] of Object.entries(value)) walk(item, `${path}.${key}`);
|
|
49
|
+
seen.delete(obj);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const ctorName = value.constructor?.name ?? "object";
|
|
53
|
+
paths.push(`${path} (${ctorName})`);
|
|
54
|
+
};
|
|
55
|
+
walk(context, "context");
|
|
56
|
+
return paths;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A stable, dependency-free structural fingerprint of a machine — a short hex
|
|
60
|
+
* `djb2` hash over its **structural** config only: state ids/nesting, transition
|
|
61
|
+
* event types and targets, invoke `src`s, `initial`, and any other serializable
|
|
62
|
+
* config fields. Function values (context/output builders, prompts, inline
|
|
63
|
+
* guards/actions) are excluded entirely, so two machines that differ only in
|
|
64
|
+
* their prompts or executors hash identically; adding/removing/retargeting a
|
|
65
|
+
* state or transition changes the hash.
|
|
66
|
+
*
|
|
67
|
+
* Used by {@link runAgent} to stamp settled snapshots with a `version` and to
|
|
68
|
+
* detect a structurally-edited machine on resume. It is a change detector, not
|
|
69
|
+
* a cryptographic digest — collisions are possible but unlikely for real
|
|
70
|
+
* configs. Pass an explicit `machineVersion` to `runAgent` to override it.
|
|
71
|
+
*/
|
|
72
|
+
function getMachineStructuralHash(machine) {
|
|
73
|
+
return djb2Hex(stableStructuralString(machine.config));
|
|
74
|
+
}
|
|
75
|
+
function stableStructuralString(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
76
|
+
if (value === null) return "null";
|
|
77
|
+
const type = typeof value;
|
|
78
|
+
if (type === "function" || type === "undefined" || type === "symbol") return "";
|
|
79
|
+
if (type === "string") return JSON.stringify(value);
|
|
80
|
+
if (type === "number" || type === "boolean") return String(value);
|
|
81
|
+
if (type === "bigint") return `${value}n`;
|
|
82
|
+
const obj = value;
|
|
83
|
+
if (seen.has(obj)) return "\"[circular]\"";
|
|
84
|
+
seen.add(obj);
|
|
85
|
+
let out;
|
|
86
|
+
if (Array.isArray(value)) out = `[${value.map((item) => stableStructuralString(item, seen)).join(",")}]`;
|
|
87
|
+
else {
|
|
88
|
+
const parts = [];
|
|
89
|
+
for (const key of Object.keys(obj).sort()) {
|
|
90
|
+
const child = obj[key];
|
|
91
|
+
const childType = typeof child;
|
|
92
|
+
if (childType === "function" || childType === "undefined" || childType === "symbol") continue;
|
|
93
|
+
parts.push(`${JSON.stringify(key)}:${stableStructuralString(child, seen)}`);
|
|
94
|
+
}
|
|
95
|
+
out = `{${parts.join(",")}}`;
|
|
96
|
+
}
|
|
97
|
+
seen.delete(obj);
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
function djb2Hex(input) {
|
|
101
|
+
let hash = 5381;
|
|
102
|
+
for (let i = 0; i < input.length; i++) hash = (hash << 5) + hash + input.charCodeAt(i) | 0;
|
|
103
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
104
|
+
}
|
|
105
|
+
/** Builds a {@link UserMessage} from a string or multimodal content parts. */
|
|
106
|
+
function userMessage(content) {
|
|
107
|
+
return {
|
|
108
|
+
role: "user",
|
|
109
|
+
content
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** Builds an {@link AssistantMessage} from a string or content parts (text, files, tool calls/results). */
|
|
113
|
+
function assistantMessage(content) {
|
|
114
|
+
return {
|
|
115
|
+
role: "assistant",
|
|
116
|
+
content
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** Builds a {@link SystemMessage}. */
|
|
120
|
+
function systemMessage(content) {
|
|
121
|
+
return {
|
|
122
|
+
role: "system",
|
|
123
|
+
content
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Builds a {@link ToolMessage} from one or more tool-result parts. */
|
|
127
|
+
function toolMessage(content) {
|
|
128
|
+
return {
|
|
129
|
+
role: "tool",
|
|
130
|
+
content
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Returns the merged `meta` of a snapshot's active state(s) — the typed
|
|
135
|
+
* replacement for the `Object.values(snapshot.getMeta())[0]` dance.
|
|
136
|
+
*
|
|
137
|
+
* `snapshot.getMeta()` is keyed by state id; a leaf machine has one active
|
|
138
|
+
* state, but parallel/nested machines can have several. This shallow-merges
|
|
139
|
+
* every active state's meta into one object (later/deeper entries win) and
|
|
140
|
+
* returns `{}` when no active state declares meta.
|
|
141
|
+
*
|
|
142
|
+
* The return type is recovered from the snapshot's own `getMeta()` type, so a
|
|
143
|
+
* schema-typed machine (`setupAgent({ meta })`) yields the meta schema's
|
|
144
|
+
* output type. Pass an explicit `TMeta` to override when the snapshot is
|
|
145
|
+
* untyped (e.g. `AnyMachineSnapshot`).
|
|
146
|
+
*
|
|
147
|
+
* @example HITL: read the current state's interaction protocol off an idle
|
|
148
|
+
* snapshot to render for a human.
|
|
149
|
+
* ```ts
|
|
150
|
+
* const { interaction } = getStateMeta(result.snapshot);
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
function getStateMeta(snapshot) {
|
|
154
|
+
return Object.assign({}, ...Object.values(snapshot.getMeta()).filter((meta) => meta != null));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Reads the run-owned message log off a snapshot settled by a `runAgent` call
|
|
158
|
+
* that used `getRequests` (or `options.messages`) — the typed replacement for
|
|
159
|
+
* the `(snapshot as { messages?: AgentMessage[] }).messages` cast. runAgent
|
|
160
|
+
* stamps the log as a plain enumerable `messages` property (like `agentMeta`),
|
|
161
|
+
* so it survives a JSON persist/resume round-trip; this accessor works on the
|
|
162
|
+
* live settled snapshot and on a JSON-parsed persisted one alike. Returns `[]`
|
|
163
|
+
* when no log was stamped (e.g. a default invoke-driven run).
|
|
164
|
+
*
|
|
165
|
+
* The write path is `runAgent(..., { messages })`: an explicit seed that
|
|
166
|
+
* overrides the resume snapshot's stamped log (fold in a user reply on
|
|
167
|
+
* resume, or start a run with prior history).
|
|
168
|
+
*/
|
|
169
|
+
function getAgentMessages(snapshot) {
|
|
170
|
+
const stamped = snapshot?.messages;
|
|
171
|
+
return Array.isArray(stamped) ? stamped : [];
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Structural guard for a {@link StandardSchemaV1}: `true` when `value` carries
|
|
175
|
+
* the `~standard` marker. Used to tell an already-schema'd tool `inputSchema`
|
|
176
|
+
* (a Zod/Valibot/… schema) apart from an SDK-specific schema wrapper that core
|
|
177
|
+
* can't read directly — see the `ai-sdk` tool pass-through and `openai-compat`
|
|
178
|
+
* tool serialization.
|
|
179
|
+
*/
|
|
180
|
+
function isStandardSchema(value) {
|
|
181
|
+
return typeof value === "object" && value !== null && "~standard" in value;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Pulls the JSON Schema off a {@link StandardSchemaV1} via its optional
|
|
185
|
+
* `~standard.jsonSchema.input()` extension (implemented by e.g. Zod v4's
|
|
186
|
+
* `z.toJSONSchema`), awaiting it when the producer is async. Returns
|
|
187
|
+
* `undefined` when the schema doesn't expose the extension. Use this to build
|
|
188
|
+
* a provider request's `response_format`/tool `parameters` from a schema.
|
|
189
|
+
*/
|
|
190
|
+
async function getJsonSchema(schema) {
|
|
191
|
+
const jsonSchemaFn = schema?.["~standard"].jsonSchema?.input;
|
|
192
|
+
if (!jsonSchemaFn) return;
|
|
193
|
+
const result = jsonSchemaFn();
|
|
194
|
+
return result instanceof Promise ? await result : result;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Synchronous variant of {@link getJsonSchema}, for call sites that can't
|
|
198
|
+
* await (building tool/event descriptors inline). An async JSON Schema
|
|
199
|
+
* producer is treated as absent (returns `undefined`) — in practice Zod's
|
|
200
|
+
* `z.toJSONSchema` resolves synchronously.
|
|
201
|
+
*/
|
|
202
|
+
function getJsonSchemaSync(schema) {
|
|
203
|
+
const jsonSchemaFn = schema?.["~standard"].jsonSchema?.input;
|
|
204
|
+
if (!jsonSchemaFn) return;
|
|
205
|
+
const result = jsonSchemaFn();
|
|
206
|
+
return result instanceof Promise ? void 0 : result;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Validates `value` against a {@link StandardSchemaV1}, synchronously.
|
|
210
|
+
* Throws if the schema's `validate` returns a `Promise` (async validation is
|
|
211
|
+
* not supported anywhere in this library) or if validation reports issues —
|
|
212
|
+
* in which case the thrown `Error.message` joins every issue message with
|
|
213
|
+
* `', '`.
|
|
214
|
+
*/
|
|
215
|
+
function validateSchemaSync(schema, value) {
|
|
216
|
+
const result = schema["~standard"].validate(value);
|
|
217
|
+
if (result instanceof Promise) throw new Error("Async schema validation is not supported.");
|
|
218
|
+
if (result.issues) throw new Error(result.issues.map((issue) => issue.message).join(", "));
|
|
219
|
+
return result.value;
|
|
220
|
+
}
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region src/internal/registry.ts
|
|
223
|
+
const agentExecutionOptions = /* @__PURE__ */ new WeakMap();
|
|
224
|
+
/**
|
|
225
|
+
* Machine-carried wait-state predicates, keyed on the machine's root `config`
|
|
226
|
+
* object. `config` is shared by reference across `machine.provide(...)` (unlike
|
|
227
|
+
* the machine object itself), so a predicate registered here travels with the
|
|
228
|
+
* machine through `.provide` — which is why it is keyed on `config`, not the
|
|
229
|
+
* machine. Set by `setupAgent({ isSuspended })` in `createMachine`, read by
|
|
230
|
+
* `runAgent` (below the host `options.isSuspended` override, above the timing
|
|
231
|
+
* heuristic).
|
|
232
|
+
*/
|
|
233
|
+
const machineSuspensionPredicates = /* @__PURE__ */ new WeakMap();
|
|
234
|
+
/** Reads the {@link machineSuspensionPredicates} predicate carried by `machine` (via its root `config`), if any. */
|
|
235
|
+
function getMachineSuspensionPredicate(machine) {
|
|
236
|
+
const config = machine.config;
|
|
237
|
+
return config ? machineSuspensionPredicates.get(config) : void 0;
|
|
238
|
+
}
|
|
239
|
+
const unboundPlaceholderLogics = /* @__PURE__ */ new WeakSet();
|
|
240
|
+
/** Text/decision logics created WITH their own executor (withExecutor or the
|
|
241
|
+
* factory's second arg) — these are runnable as-is, so runAgent's bind check
|
|
242
|
+
* must not reject them as direct-object invoke srcs. */
|
|
243
|
+
const executorBoundLogics = /* @__PURE__ */ new WeakSet();
|
|
244
|
+
function missingActor(src) {
|
|
245
|
+
const logic = createAsyncLogic({ run: async () => {
|
|
246
|
+
throw new Error(`'${src}' has no host execution. Provide an implementation with machine.provide({ actorSources: { '${src}': ... } }).`);
|
|
247
|
+
} });
|
|
248
|
+
unboundPlaceholderLogics.add(logic);
|
|
249
|
+
return logic;
|
|
250
|
+
}
|
|
251
|
+
function isUnboundPlaceholder(logic) {
|
|
252
|
+
return !!logic && typeof logic === "object" && unboundPlaceholderLogics.has(logic);
|
|
253
|
+
}
|
|
254
|
+
function getRegisteredAgentExecutionOptions(machine, options) {
|
|
255
|
+
return {
|
|
256
|
+
...agentExecutionOptions.get(machine),
|
|
257
|
+
...options
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region src/text-logic.ts
|
|
262
|
+
const USER_INPUT_ACTOR = "agent.userInput";
|
|
263
|
+
const GENERATE_TEXT_ACTOR = "agent.generateText";
|
|
264
|
+
const STREAM_TEXT_ACTOR = "agent.streamText";
|
|
265
|
+
const DECIDE_ACTOR = "agent.decide";
|
|
266
|
+
const PLAN_ACTOR = "agent.plan";
|
|
267
|
+
const agentTextInputSchema = { "~standard": {
|
|
268
|
+
version: 1,
|
|
269
|
+
vendor: "statelyai-agent",
|
|
270
|
+
validate(value) {
|
|
271
|
+
return !!value && typeof value === "object" && typeof value.model === "string" ? { value } : { issues: [{ message: "Expected agent text input with a model" }] };
|
|
272
|
+
}
|
|
273
|
+
} };
|
|
274
|
+
const unknownOutputSchema = { "~standard": {
|
|
275
|
+
version: 1,
|
|
276
|
+
vendor: "statelyai-agent",
|
|
277
|
+
validate(value) {
|
|
278
|
+
return { value };
|
|
279
|
+
}
|
|
280
|
+
} };
|
|
281
|
+
const stringOutputSchema = { "~standard": {
|
|
282
|
+
version: 1,
|
|
283
|
+
vendor: "statelyai-agent",
|
|
284
|
+
validate(value) {
|
|
285
|
+
return typeof value === "string" ? { value } : { issues: [{ message: "Expected string output" }] };
|
|
286
|
+
}
|
|
287
|
+
} };
|
|
288
|
+
function createBuiltinTextActor(src, mode, outputSchema) {
|
|
289
|
+
const logic = createAsyncLogic({ run: async () => {
|
|
290
|
+
throw new Error(`'${src}' has no host execution. Provide an implementation with machine.provide({ actorSources: { '${src}': ... } }) or execute the returned agent request with executeAgentRequest(...).`);
|
|
291
|
+
} });
|
|
292
|
+
return Object.assign(logic, {
|
|
293
|
+
kind: "statelyai.textLogic",
|
|
294
|
+
mode,
|
|
295
|
+
schemas: {
|
|
296
|
+
input: agentTextInputSchema,
|
|
297
|
+
output: outputSchema
|
|
298
|
+
},
|
|
299
|
+
request(input) {
|
|
300
|
+
return validateSchemaSync(agentTextInputSchema, input);
|
|
301
|
+
},
|
|
302
|
+
async execute(input, executors) {
|
|
303
|
+
const { output } = await executeAgentTextRequest(mode, src, validateSchemaSync(agentTextInputSchema, input), executors);
|
|
304
|
+
return validateSchemaSync(outputSchema, output);
|
|
305
|
+
},
|
|
306
|
+
withExecutor(execute) {
|
|
307
|
+
return Object.assign(createTextLogic({
|
|
308
|
+
mode,
|
|
309
|
+
schemas: {
|
|
310
|
+
input: agentTextInputSchema,
|
|
311
|
+
output: outputSchema
|
|
312
|
+
},
|
|
313
|
+
name: ({ input }) => input.name,
|
|
314
|
+
model: ({ input }) => input.model,
|
|
315
|
+
system: ({ input }) => input.system,
|
|
316
|
+
prompt: ({ input }) => input.prompt,
|
|
317
|
+
messages: ({ input }) => input.messages,
|
|
318
|
+
tools: ({ input }) => input.tools,
|
|
319
|
+
toolChoice: ({ input }) => input.toolChoice,
|
|
320
|
+
reasoning: ({ input }) => input.reasoning,
|
|
321
|
+
temperature: ({ input }) => input.temperature,
|
|
322
|
+
maxOutputTokens: ({ input }) => input.maxOutputTokens,
|
|
323
|
+
topP: ({ input }) => input.topP,
|
|
324
|
+
topK: ({ input }) => input.topK,
|
|
325
|
+
seed: ({ input }) => input.seed,
|
|
326
|
+
stopSequences: ({ input }) => input.stopSequences,
|
|
327
|
+
metadata: ({ input }) => input.metadata
|
|
328
|
+
}, execute));
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
/** The unbound `agent.generateText`/`agent.streamText` builtins registered by setupAgent. @internal */
|
|
333
|
+
const builtinTextActors = {
|
|
334
|
+
[GENERATE_TEXT_ACTOR]: createBuiltinTextActor(GENERATE_TEXT_ACTOR, "generate", unknownOutputSchema),
|
|
335
|
+
[STREAM_TEXT_ACTOR]: createBuiltinTextActor(STREAM_TEXT_ACTOR, "stream", stringOutputSchema)
|
|
336
|
+
};
|
|
337
|
+
/** The unbound `agent.userInput` builtin registered by setupAgent (an unbound-placeholder logic — see internal/registry.ts). @internal */
|
|
338
|
+
const userInputActor = createAsyncLogic({ run: async () => {
|
|
339
|
+
throw new Error(`'${USER_INPUT_ACTOR}' has no host execution. Provide an implementation with machine.provide({ actorSources: { '${USER_INPUT_ACTOR}': ... } }).`);
|
|
340
|
+
} });
|
|
341
|
+
unboundPlaceholderLogics.add(userInputActor);
|
|
342
|
+
/**
|
|
343
|
+
* Validates a raw model/executor output against `schema`, returning the
|
|
344
|
+
* parsed value. Thin wrapper over {@link validateSchemaSync} for parsing a
|
|
345
|
+
* text request's structured output outside of `TextLogic.execute`/
|
|
346
|
+
* `executeAgentRequest` (e.g. a custom host loop).
|
|
347
|
+
*/
|
|
348
|
+
function parseOutput(schema, output) {
|
|
349
|
+
return validateSchemaSync(schema, output);
|
|
350
|
+
}
|
|
351
|
+
/** Resolves a `ResolveTextLogicValue` (calls it if it's a function, else returns it as-is). @internal */
|
|
352
|
+
function resolveTextLogicValue(value, args) {
|
|
353
|
+
return typeof value === "function" ? value(args) : value;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Creates reusable, standalone {@link TextLogic}: an actor that, when run,
|
|
357
|
+
* resolves typed input to typed output via a model call. Register the
|
|
358
|
+
* result under `actorSources:` and invoke it by name (equivalent to what
|
|
359
|
+
* `setupAgent({ requests })` builds internally for each request entry). Pass
|
|
360
|
+
* `execute` here, or bind it later with `.withExecutor(...)`, a runtime
|
|
361
|
+
* adapter's `machine.provide(...)`, or `runAgent`'s `generateText`/
|
|
362
|
+
* `streamText` options.
|
|
363
|
+
*
|
|
364
|
+
* @example
|
|
365
|
+
* ```ts
|
|
366
|
+
* export const tellJoke = createTextLogic({
|
|
367
|
+
* mode: 'stream',
|
|
368
|
+
* schemas: { input: z.object({ topic: z.string() }), output: z.string() },
|
|
369
|
+
* model: 'openai/gpt-5.4-mini',
|
|
370
|
+
* system: 'You tell short, punchy jokes.',
|
|
371
|
+
* prompt: ({ input }) => `Tell a joke about ${input.topic}.`,
|
|
372
|
+
* });
|
|
373
|
+
* ```
|
|
374
|
+
*/
|
|
375
|
+
function createTextLogic(config, execute) {
|
|
376
|
+
const request = (input) => {
|
|
377
|
+
const args = { input: validateSchemaSync(config.schemas.input, input) };
|
|
378
|
+
return {
|
|
379
|
+
name: resolveTextLogicValue(config.name, args),
|
|
380
|
+
model: resolveTextLogicValue(config.model, args),
|
|
381
|
+
system: resolveTextLogicValue(config.system, args),
|
|
382
|
+
prompt: resolveTextLogicValue(config.prompt, args),
|
|
383
|
+
messages: resolveTextLogicValue(config.messages, args),
|
|
384
|
+
tools: resolveTextLogicValue(config.tools, args),
|
|
385
|
+
toolChoice: resolveTextLogicValue(config.toolChoice, args),
|
|
386
|
+
outputSchema: config.schemas.output,
|
|
387
|
+
reasoning: resolveTextLogicValue(config.reasoning, args),
|
|
388
|
+
temperature: resolveTextLogicValue(config.temperature, args),
|
|
389
|
+
maxOutputTokens: resolveTextLogicValue(config.maxOutputTokens, args),
|
|
390
|
+
topP: resolveTextLogicValue(config.topP, args),
|
|
391
|
+
topK: resolveTextLogicValue(config.topK, args),
|
|
392
|
+
seed: resolveTextLogicValue(config.seed, args),
|
|
393
|
+
stopSequences: resolveTextLogicValue(config.stopSequences, args),
|
|
394
|
+
metadata: resolveTextLogicValue(config.metadata, args)
|
|
395
|
+
};
|
|
396
|
+
};
|
|
397
|
+
const logic = createAsyncLogic({ run: async ({ input, signal, system, self }, enq) => {
|
|
398
|
+
const resolvedRequest = request(input);
|
|
399
|
+
if (!execute) throw new Error("Text logic has no host execution. Pass an executor as the second argument to createTextLogic(...), provide a runtime adapter, or extract it with getAgentRequests(..., { actorSources }).");
|
|
400
|
+
const result = await execute({
|
|
401
|
+
input,
|
|
402
|
+
request: resolvedRequest,
|
|
403
|
+
signal,
|
|
404
|
+
system,
|
|
405
|
+
self,
|
|
406
|
+
emit: enq.emit
|
|
407
|
+
});
|
|
408
|
+
const selfId = self?.id;
|
|
409
|
+
const output = await normalizeGeneratorResult(result, typeof selfId === "string" ? selfId : "text logic", { request: resolvedRequest });
|
|
410
|
+
return validateSchemaSync(config.schemas.output, output);
|
|
411
|
+
} });
|
|
412
|
+
const textLogic = Object.assign(logic, {
|
|
413
|
+
kind: "statelyai.textLogic",
|
|
414
|
+
mode: config.mode ?? "generate",
|
|
415
|
+
schemas: config.schemas,
|
|
416
|
+
request,
|
|
417
|
+
async execute(input, executors) {
|
|
418
|
+
const { output } = await executeAgentTextRequest(config.mode ?? "generate", "textLogic", request(input), executors);
|
|
419
|
+
return validateSchemaSync(config.schemas.output, output);
|
|
420
|
+
},
|
|
421
|
+
withExecutor(nextExecute) {
|
|
422
|
+
return createTextLogic(config, nextExecute);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
if (execute) executorBoundLogics.add(textLogic);
|
|
426
|
+
return textLogic;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Binds a child machine's {@link TextLogic} to a raw
|
|
430
|
+
* {@link AgentRequestExecutor} (the `generateText`/`streamText` shape hosts
|
|
431
|
+
* implement). Encapsulates the `withExecutor` idiom child agents repeat:
|
|
432
|
+
* default the request's `tools` to `{}`, forward the actor `signal`, call the
|
|
433
|
+
* executor, and return its `{ output }` envelope. Use this to share ONE
|
|
434
|
+
* executor across a parent and its nested children.
|
|
435
|
+
*
|
|
436
|
+
* @example
|
|
437
|
+
* ```ts
|
|
438
|
+
* childMachine.provide({
|
|
439
|
+
* actorSources: {
|
|
440
|
+
* researchTopic: bindRequestExecutor(setup.requests.researchTopic, generateText),
|
|
441
|
+
* },
|
|
442
|
+
* });
|
|
443
|
+
* ```
|
|
444
|
+
*/
|
|
445
|
+
function bindRequestExecutor(logic, executor) {
|
|
446
|
+
return logic.withExecutor(async ({ request, signal }) => {
|
|
447
|
+
const { output } = await executor({
|
|
448
|
+
...request,
|
|
449
|
+
tools: request.tools ?? {}
|
|
450
|
+
}, { signal });
|
|
451
|
+
return { output };
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
/** Type guard: true for any actor logic built by createTextLogic (checks the `kind` marker). @internal */
|
|
455
|
+
function isTextLogic(value) {
|
|
456
|
+
return !!value && typeof value === "object" && value.kind === "statelyai.textLogic" && typeof value.request === "function";
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Classifies a text request's output schema as `'structured'` (its JSON
|
|
460
|
+
* Schema is `type: 'object'`, `type: 'array'`, or a top-level union/
|
|
461
|
+
* composition — `anyOf`/`oneOf`/`allOf`, which a bare `z.union`/
|
|
462
|
+
* `z.discriminatedUnion` emits with no top-level `type`) or `'text'`
|
|
463
|
+
* (anything else, including no schema). Reads the schema's
|
|
464
|
+
* `~standard.jsonSchema.input()` extension — schemas without it are treated
|
|
465
|
+
* as `'text'`.
|
|
466
|
+
*/
|
|
467
|
+
function getAgentOutputMode(schema) {
|
|
468
|
+
const jsonSchema = getStandardSchemaJson(schema);
|
|
469
|
+
if (!jsonSchema) return "text";
|
|
470
|
+
if (jsonSchema.type === "object" || jsonSchema.type === "array") return "structured";
|
|
471
|
+
if (jsonSchema.type === void 0 && ("anyOf" in jsonSchema || "oneOf" in jsonSchema || "allOf" in jsonSchema)) return "structured";
|
|
472
|
+
return "text";
|
|
473
|
+
}
|
|
474
|
+
/** True when {@link getAgentOutputMode} classifies `schema` as `'structured'`. */
|
|
475
|
+
function isStructuredOutputSchema(schema) {
|
|
476
|
+
return getAgentOutputMode(schema) === "structured";
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Builds the uniform structured-output envelope schema every structured request
|
|
480
|
+
* is sent to the provider as: a root object `{ result: <inner> }`, plus — when
|
|
481
|
+
* `options.reasoning` is `true` — an optional string `reasoning` property listed
|
|
482
|
+
* BEFORE `result` (property order nudges the model to reason first). This is THE
|
|
483
|
+
* wire contract for structured output: a root object is universally accepted as
|
|
484
|
+
* a provider response schema, unlike a bare union/array root that many providers
|
|
485
|
+
* reject.
|
|
486
|
+
*
|
|
487
|
+
* The returned {@link StandardSchemaV1} validates the `{ reasoning?, result }`
|
|
488
|
+
* envelope (unwrapping `result` through the original schema, capturing a string
|
|
489
|
+
* `reasoning` when present) and exposes the enveloped JSON Schema. Adapters read
|
|
490
|
+
* `.result` off the provider output before the machine validates it — so this is
|
|
491
|
+
* transparent: user-facing output types stay the declared (un-enveloped) schema,
|
|
492
|
+
* and `reasoning` is surfaced only on the raw executor result, never in machine
|
|
493
|
+
* context/output.
|
|
494
|
+
*/
|
|
495
|
+
function buildEnvelopeSchema(inner, options = {}) {
|
|
496
|
+
const includeReasoning = options.reasoning === true;
|
|
497
|
+
const buildJson = (innerJson) => ({
|
|
498
|
+
type: "object",
|
|
499
|
+
properties: {
|
|
500
|
+
...includeReasoning ? { reasoning: { type: "string" } } : {},
|
|
501
|
+
result: innerJson ?? {}
|
|
502
|
+
},
|
|
503
|
+
required: ["result"],
|
|
504
|
+
additionalProperties: false
|
|
505
|
+
});
|
|
506
|
+
return { "~standard": {
|
|
507
|
+
version: 1,
|
|
508
|
+
vendor: "statelyai-agent",
|
|
509
|
+
validate(value) {
|
|
510
|
+
if (!value || typeof value !== "object" || !("result" in value)) return { issues: [{ message: "Expected a { result } envelope object" }] };
|
|
511
|
+
const innerResult = inner["~standard"].validate(value.result);
|
|
512
|
+
if (innerResult instanceof Promise) throw new Error("Async schema validation is not supported.");
|
|
513
|
+
if (innerResult.issues) return innerResult;
|
|
514
|
+
const envelope = { result: innerResult.value };
|
|
515
|
+
const reasoning = value.reasoning;
|
|
516
|
+
if (typeof reasoning === "string") envelope.reasoning = reasoning;
|
|
517
|
+
return { value: envelope };
|
|
518
|
+
},
|
|
519
|
+
jsonSchema: { input: () => {
|
|
520
|
+
const innerJson = inner["~standard"].jsonSchema?.input?.();
|
|
521
|
+
return innerJson instanceof Promise ? innerJson.then(buildJson) : buildJson(innerJson);
|
|
522
|
+
} }
|
|
523
|
+
} };
|
|
524
|
+
}
|
|
525
|
+
function getStandardSchemaJson(schema) {
|
|
526
|
+
const jsonSchema = (schema?.["~standard"])?.jsonSchema?.input?.();
|
|
527
|
+
return jsonSchema && !(jsonSchema instanceof Promise) ? jsonSchema : void 0;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Merges request-declared and call-site `tools`, dispatches to the
|
|
531
|
+
* `mode`-appropriate executor (`generateText`/`streamText`), and normalizes
|
|
532
|
+
* the raw result via {@link normalizeGeneratorResult}. Shared by
|
|
533
|
+
* `TextLogic.execute`, `executeAgentRequest`, and the `agent.generateText`/
|
|
534
|
+
* `agent.streamText` builtins. Throws if no executor is registered for
|
|
535
|
+
* `mode`.
|
|
536
|
+
*
|
|
537
|
+
* @internal
|
|
538
|
+
*/
|
|
539
|
+
async function executeAgentTextRequest(mode, id, input, executors, tools = {}, info) {
|
|
540
|
+
const request = {
|
|
541
|
+
...input,
|
|
542
|
+
tools: {
|
|
543
|
+
...input.tools,
|
|
544
|
+
...tools
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
const executor = mode === "stream" ? executors.streamText : executors.generateText;
|
|
548
|
+
if (!executor) throw new Error(`No executor provided for ${mode === "stream" ? "stream" : "generate"} request '${id}'.`);
|
|
549
|
+
const raw = await executor(request, info);
|
|
550
|
+
return {
|
|
551
|
+
output: await normalizeGeneratorResult(raw, id, {
|
|
552
|
+
request,
|
|
553
|
+
onChunk: info?.onChunk
|
|
554
|
+
}),
|
|
555
|
+
raw
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function hasTextStream(value) {
|
|
559
|
+
return "textStream" in value && typeof value.textStream === "object" && !!value.textStream?.[Symbol.asyncIterator];
|
|
560
|
+
}
|
|
561
|
+
function parseRawAiSdkText(text, request, id) {
|
|
562
|
+
if (!request?.outputSchema) return text;
|
|
563
|
+
let parsed = text;
|
|
564
|
+
try {
|
|
565
|
+
parsed = JSON.parse(text);
|
|
566
|
+
} catch {}
|
|
567
|
+
try {
|
|
568
|
+
return parseOutput(request.outputSchema, parsed);
|
|
569
|
+
} catch (error) {
|
|
570
|
+
throw new Error(`Executor for '${id}' returned a raw AI SDK result whose text could not be parsed against the request's outputSchema. Structured-output requests through raw AI SDK generateText/streamText functions are best-effort — for reliable structured output, use createAiSdkExecutors from '@statelyai/agent/ai-sdk'. Cause: ${error instanceof Error ? error.message : String(error)}`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Unwraps an executor result into the request's final output. Accepts three
|
|
575
|
+
* shapes:
|
|
576
|
+
* - `{ output }` (the {@link AgentRequestExecutorResult} envelope) — awaits and
|
|
577
|
+
* returns `output` (the fast path; unchanged).
|
|
578
|
+
* - a raw AI SDK `streamText` result (`{ textStream }` async iterable) — iterates
|
|
579
|
+
* `textStream`, forwarding each string chunk to `info.onChunk`, then resolves
|
|
580
|
+
* the final text from `await result.text` if present else the accumulated chunks.
|
|
581
|
+
* - a raw AI SDK `generateText` result (`{ text }` string or promise) — awaits `text`.
|
|
582
|
+
*
|
|
583
|
+
* For the two raw AI SDK shapes, if `info.request?.outputSchema` is set the final
|
|
584
|
+
* text is parsed through {@link parseOutput} (best-effort); a parse failure throws
|
|
585
|
+
* an error recommending `createAiSdkExecutors` from '@statelyai/agent/ai-sdk'.
|
|
586
|
+
*
|
|
587
|
+
* A value matching none of these is a runtime error naming `id`. This is
|
|
588
|
+
* generator-result unwrapping only — decision results are extracted separately
|
|
589
|
+
* by `resolveDecision`.
|
|
590
|
+
*
|
|
591
|
+
* @internal
|
|
592
|
+
*/
|
|
593
|
+
async function normalizeGeneratorResult(result, id = "text request", info) {
|
|
594
|
+
const resolved = await result;
|
|
595
|
+
if (!resolved || typeof resolved !== "object") throw invalidGeneratorResult(id);
|
|
596
|
+
if ("output" in resolved) return await resolved.output;
|
|
597
|
+
if (hasTextStream(resolved)) {
|
|
598
|
+
let accumulated = "";
|
|
599
|
+
for await (const chunk of resolved.textStream) {
|
|
600
|
+
accumulated += chunk;
|
|
601
|
+
info?.onChunk?.(chunk);
|
|
602
|
+
}
|
|
603
|
+
const finalText = "text" in resolved && resolved.text !== void 0 ? await resolved.text : accumulated;
|
|
604
|
+
return parseRawAiSdkText(String(finalText), info?.request, id);
|
|
605
|
+
}
|
|
606
|
+
if ("text" in resolved) {
|
|
607
|
+
const finalText = await resolved.text;
|
|
608
|
+
return parseRawAiSdkText(String(finalText), info?.request, id);
|
|
609
|
+
}
|
|
610
|
+
throw invalidGeneratorResult(id);
|
|
611
|
+
}
|
|
612
|
+
function invalidGeneratorResult(id) {
|
|
613
|
+
return /* @__PURE__ */ new Error(`Executor for '${id}' returned an invalid result: executors must return { output } (an envelope with the text string or structured object as \`output\`, plus optional passthrough fields). Raw Vercel AI SDK generateText/streamText results ({ text } or { textStream }) are also accepted.`);
|
|
614
|
+
}
|
|
615
|
+
//#endregion
|
|
616
|
+
//#region src/events.ts
|
|
617
|
+
/** Default prefix for the synthetic tool name generated per candidate event (e.g. `send_event_ASK`). Override per-request with {@link AgentEventToolNameResolver}. */
|
|
618
|
+
const EVENT_TOOL_PREFIX = "send_event_";
|
|
619
|
+
function hashString(value) {
|
|
620
|
+
let hash = 5381;
|
|
621
|
+
for (let i = 0; i < value.length; i++) hash = hash * 33 ^ value.charCodeAt(i);
|
|
622
|
+
return (hash >>> 0).toString(36);
|
|
623
|
+
}
|
|
624
|
+
function sanitizeEventToolName(eventType) {
|
|
625
|
+
const base = `${EVENT_TOOL_PREFIX}${eventType.replace(/[^a-zA-Z0-9_-]/g, "_") || "event"}`;
|
|
626
|
+
if (base.length <= 64) return base;
|
|
627
|
+
const hash = hashString(eventType);
|
|
628
|
+
const prefixLength = 64 - hash.length - 1;
|
|
629
|
+
return `${base.slice(0, prefixLength)}_${hash}`;
|
|
630
|
+
}
|
|
631
|
+
function disambiguateEventToolName(toolName, eventType, usedToolNames) {
|
|
632
|
+
if (!usedToolNames.has(toolName)) {
|
|
633
|
+
usedToolNames.add(toolName);
|
|
634
|
+
return toolName;
|
|
635
|
+
}
|
|
636
|
+
const suffix = `_${hashString(eventType)}`;
|
|
637
|
+
const uniqueToolName = `${toolName.slice(0, 64 - suffix.length)}${suffix}`;
|
|
638
|
+
usedToolNames.add(uniqueToolName);
|
|
639
|
+
return uniqueToolName;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* True when an event type matches an `allowedEvents` entry: an exact type,
|
|
643
|
+
* `'*'` (every event), or a `'prefix.*'` wildcard matching any deeper
|
|
644
|
+
* segment (`'todo.*'` matches `'todo.add'` and `'todo.list.clear'`, not
|
|
645
|
+
* `'todo'` itself — mirroring xstate's partial wildcard events).
|
|
646
|
+
*/
|
|
647
|
+
function matchesEventPattern(eventType, pattern) {
|
|
648
|
+
if (pattern === "*") return true;
|
|
649
|
+
if (pattern.endsWith(".*")) return eventType.startsWith(`${pattern.slice(0, -1)}`);
|
|
650
|
+
return eventType === pattern;
|
|
651
|
+
}
|
|
652
|
+
/** True when an `allowedEvents` entry is a wildcard pattern rather than a concrete event type. @internal */
|
|
653
|
+
function isEventPattern(entry) {
|
|
654
|
+
return entry === "*" || entry.endsWith(".*");
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Runtime-validates a dynamically-built `{ type, ...payload }` event against a
|
|
658
|
+
* snapshot's currently-accepted events (via {@link getAcceptedEvents}) and,
|
|
659
|
+
* when one is registered, the event type's payload schema — returning the
|
|
660
|
+
* event typed as the machine's event union (recovered from the snapshot type)
|
|
661
|
+
* so it can be sent to `runAgent({ event })` / `actor.send(...)` without an
|
|
662
|
+
* `as never` cast. For generic, meta-driven hosts that assemble events from
|
|
663
|
+
* user input or a wire message.
|
|
664
|
+
*
|
|
665
|
+
* Throws a descriptive error when `event.type` is not currently accepted
|
|
666
|
+
* (listing the accepted types) or when its payload fails the registered schema.
|
|
667
|
+
* Pass event payload schemas via `options.events`/`options.schemas` (the same
|
|
668
|
+
* shape {@link getAcceptedEvents} takes) — the accepted TYPES always come from
|
|
669
|
+
* the live snapshot; the schemas only add payload validation.
|
|
670
|
+
*
|
|
671
|
+
* @example
|
|
672
|
+
* ```ts
|
|
673
|
+
* const event = parseAgentEvent(result.snapshot, rawEvent, { events: schemas.events });
|
|
674
|
+
* result = await runAgent(machine, { snapshot: result.snapshot, event, executors });
|
|
675
|
+
* ```
|
|
676
|
+
*/
|
|
677
|
+
function parseAgentEvent(snapshot, event, options = {}) {
|
|
678
|
+
const accepted = getAcceptedEvents(snapshot, options);
|
|
679
|
+
const descriptor = accepted.find((candidate) => candidate.type === event.type);
|
|
680
|
+
if (!descriptor) throw new Error(`parseAgentEvent: '${event.type}' is not an accepted event in the current state. Accepted: ${accepted.map((candidate) => candidate.type).join(", ") || "(none)"}.`);
|
|
681
|
+
if (descriptor.inputSchema) {
|
|
682
|
+
const { type, ...payload } = event;
|
|
683
|
+
try {
|
|
684
|
+
return {
|
|
685
|
+
...validateSchemaSync(descriptor.inputSchema, payload),
|
|
686
|
+
type
|
|
687
|
+
};
|
|
688
|
+
} catch (error) {
|
|
689
|
+
throw new Error(`parseAgentEvent: '${event.type}' payload failed validation: ${error instanceof Error ? error.message : String(error)}`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return event;
|
|
693
|
+
}
|
|
694
|
+
function getAcceptedEvents(snapshot, options = {}) {
|
|
695
|
+
const eventTypes = options.eventTypes;
|
|
696
|
+
const seen = /* @__PURE__ */ new Set();
|
|
697
|
+
const usedToolNames = /* @__PURE__ */ new Set();
|
|
698
|
+
return getNextTransitions(snapshot).flatMap((transitionDefinition) => {
|
|
699
|
+
const eventType = transitionDefinition.eventType;
|
|
700
|
+
if (!eventType || eventType === "*" || eventType.startsWith("xstate.") || eventTypes && !eventTypes.some((pattern) => matchesEventPattern(eventType, pattern)) || seen.has(eventType)) return [];
|
|
701
|
+
seen.add(eventType);
|
|
702
|
+
const defaultToolName = sanitizeEventToolName(eventType);
|
|
703
|
+
const toolName = options.eventToolName ? options.eventToolName({
|
|
704
|
+
eventType,
|
|
705
|
+
defaultToolName
|
|
706
|
+
}) : disambiguateEventToolName(defaultToolName, eventType, usedToolNames);
|
|
707
|
+
const inputSchema = (options.events ?? options.schemas?.events)?.[eventType];
|
|
708
|
+
return [{
|
|
709
|
+
type: eventType,
|
|
710
|
+
toolName,
|
|
711
|
+
...inputSchema ? { inputSchema } : {}
|
|
712
|
+
}];
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
//#endregion
|
|
716
|
+
//#region src/decision.ts
|
|
717
|
+
function decideRequestFromInput(input) {
|
|
718
|
+
const allowedEventTypes = resolveAllowedEventTypes(input.allowedEvents, input) ?? [];
|
|
719
|
+
return {
|
|
720
|
+
kind: "decision",
|
|
721
|
+
id: "",
|
|
722
|
+
model: input.model,
|
|
723
|
+
system: input.system,
|
|
724
|
+
prompt: input.prompt,
|
|
725
|
+
messages: input.messages,
|
|
726
|
+
events: allowedEventTypes.filter((type) => !isEventPattern(type)).map((type) => ({
|
|
727
|
+
type,
|
|
728
|
+
toolName: sanitizeEventToolName(type)
|
|
729
|
+
})),
|
|
730
|
+
attempts: [],
|
|
731
|
+
temperature: input.temperature,
|
|
732
|
+
maxOutputTokens: input.maxOutputTokens,
|
|
733
|
+
topP: input.topP,
|
|
734
|
+
topK: input.topK,
|
|
735
|
+
seed: input.seed,
|
|
736
|
+
stopSequences: input.stopSequences,
|
|
737
|
+
metadata: input.metadata
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
function decideActorWithExecutor(execute) {
|
|
741
|
+
const logic = createAsyncLogic({ run: async ({ input, signal }) => {
|
|
742
|
+
if (!execute) throw new Error(`'${DECIDE_ACTOR}' has no host execution. Provide an implementation with machine.provide({ actorSources: { '${DECIDE_ACTOR}': ... } }) or resolve the returned agent request with resolveDecision(...).`);
|
|
743
|
+
const resolvedEventTypes = resolveAllowedEventTypes(input.allowedEvents, input);
|
|
744
|
+
if (resolvedEventTypes === void 0) throw new Error(`'${DECIDE_ACTOR}' input has omitted \`allowedEvents\`, which means "all currently-legal events" — but that requires a snapshot-aware host (runAgent or the step path) to resolve. Under a bare createActor(...), declare \`allowedEvents\` explicitly to use this actor here.`);
|
|
745
|
+
if (resolvedEventTypes.some(isEventPattern)) throw new Error(`'${DECIDE_ACTOR}' input uses wildcard \`allowedEvents\` patterns, which expand against the live snapshot — that requires a snapshot-aware host (runAgent or the step path). Under a bare createActor(...), list event types explicitly.`);
|
|
746
|
+
return resolveDecision(decideRequestFromInput(input), execute, {
|
|
747
|
+
maxRetries: input.maxRetries ?? 2,
|
|
748
|
+
signal
|
|
749
|
+
});
|
|
750
|
+
} });
|
|
751
|
+
return Object.assign(logic, {
|
|
752
|
+
kind: "statelyai.decisionLogic",
|
|
753
|
+
maxRetries: 2,
|
|
754
|
+
request: decideRequestFromInput,
|
|
755
|
+
allowedEventTypes: (input) => resolveAllowedEventTypes(input.allowedEvents, input),
|
|
756
|
+
withExecutor: (nextExecute) => decideActorWithExecutor(nextExecute)
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
function createDecideActor() {
|
|
760
|
+
return decideActorWithExecutor();
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Reserved event type the `agent.plan` builtin adds to every step's
|
|
764
|
+
* candidates as the explicit "no further action needed" move. Choosing it
|
|
765
|
+
* ends the plan (`stopped: 'done'`); it is never sent to the machine, so
|
|
766
|
+
* machines need no no-op sentinel event of their own.
|
|
767
|
+
*/
|
|
768
|
+
const PLAN_DONE_EVENT_TYPE = "agent.plan.done";
|
|
769
|
+
function createPlanActor() {
|
|
770
|
+
const logic = createLogic({
|
|
771
|
+
context: ({ input }) => ({
|
|
772
|
+
applied: [],
|
|
773
|
+
stepsRemaining: input.maxSteps ?? 8,
|
|
774
|
+
stopped: null
|
|
775
|
+
}),
|
|
776
|
+
run: ({ context, event }) => event.type === "plan.applied" ? { context: {
|
|
777
|
+
...context,
|
|
778
|
+
applied: [...context.applied, event.event],
|
|
779
|
+
stepsRemaining: context.stepsRemaining - 1
|
|
780
|
+
} } : event.type === "plan.ended" ? {
|
|
781
|
+
context: {
|
|
782
|
+
...context,
|
|
783
|
+
stopped: event.stopped
|
|
784
|
+
},
|
|
785
|
+
status: "done",
|
|
786
|
+
output: {
|
|
787
|
+
steps: context.applied,
|
|
788
|
+
stopped: event.stopped
|
|
789
|
+
}
|
|
790
|
+
} : void 0
|
|
791
|
+
});
|
|
792
|
+
return Object.assign(logic, {
|
|
793
|
+
kind: "statelyai.planLogic",
|
|
794
|
+
maxRetries: 2,
|
|
795
|
+
request: decideRequestFromInput,
|
|
796
|
+
allowedEventTypes: (input) => resolveAllowedEventTypes(input.allowedEvents, input)
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
const PLAN_LEDGER_SCOPE = { emit: () => {} };
|
|
800
|
+
/**
|
|
801
|
+
* Builds a fresh plan ledger snapshot from resolved plan input — the shared
|
|
802
|
+
* starting point for BOTH hosts (the step path reads the invoke child's own
|
|
803
|
+
* initial snapshot; runAgent seeds a local ledger with this). @internal
|
|
804
|
+
*/
|
|
805
|
+
function initialPlanLedger(logic, input) {
|
|
806
|
+
return logic.getInitialSnapshot(PLAN_LEDGER_SCOPE, input);
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Advances a plan ledger by one {@link PlanLedgerEvent}, returning the next
|
|
810
|
+
* snapshot (unwrapping `createLogic`'s `[snapshot, effects]` tuple). Pure — no
|
|
811
|
+
* mutation of the input snapshot. @internal
|
|
812
|
+
*/
|
|
813
|
+
function advancePlanLedger(logic, snapshot, event) {
|
|
814
|
+
const result = logic.transition(snapshot, event, PLAN_LEDGER_SCOPE);
|
|
815
|
+
return Array.isArray(result) ? result[0] : result;
|
|
816
|
+
}
|
|
817
|
+
/** Type guard: true for the `agent.plan` builtin logic (checks the `kind` marker). @internal */
|
|
818
|
+
function isPlanLogic(logic) {
|
|
819
|
+
return !!logic && logic.kind === "statelyai.planLogic";
|
|
820
|
+
}
|
|
821
|
+
function resolveAllowedEventTypes(allowedEvents, input) {
|
|
822
|
+
if (allowedEvents === void 0) return;
|
|
823
|
+
const resolved = typeof allowedEvents === "function" ? allowedEvents({ input }) : allowedEvents;
|
|
824
|
+
return typeof resolved === "string" ? [resolved] : resolved;
|
|
825
|
+
}
|
|
826
|
+
/** Type guard: true for any actor logic built by createDecisionLogic/createDecideActor (checks the `kind` marker). @internal */
|
|
827
|
+
function isDecisionLogic(value) {
|
|
828
|
+
return !!value && typeof value === "object" && value.kind === "statelyai.decisionLogic" && typeof value.request === "function";
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Thrown by {@link resolveDecision} when every attempt (up to
|
|
832
|
+
* `maxRetries + 1` of them) fails one of the three checks recorded in
|
|
833
|
+
* {@link DecisionAttempt.failure}. Carries the full `attempts` list for
|
|
834
|
+
* diagnostics; a machine typically routes this via the decision invoke's
|
|
835
|
+
* `onError`.
|
|
836
|
+
*/
|
|
837
|
+
var DecisionExhaustedError = class extends Error {
|
|
838
|
+
attempts;
|
|
839
|
+
constructor(attempts) {
|
|
840
|
+
super(`Decision exhausted after ${attempts.length} attempt${attempts.length === 1 ? "" : "s"}: ` + attempts.map((attempt) => attempt.reason).join("; "));
|
|
841
|
+
this.name = "DecisionExhaustedError";
|
|
842
|
+
this.attempts = attempts;
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
/**
|
|
846
|
+
* Renders a decision request's prior failed `attempts` into feedback messages
|
|
847
|
+
* a host appends to the model call so retries converge — the transport-agnostic
|
|
848
|
+
* "your last choice failed because X, choose again from Y" logic every adapter
|
|
849
|
+
* and raw-SDK host repeats. Returns one `user`-role {@link AgentMessage} per
|
|
850
|
+
* attempt (empty when there are none); adapters map each onto their wire
|
|
851
|
+
* message shape (`attempt.content` is always a string). Core never rewrites the
|
|
852
|
+
* request itself — this only turns the recorded attempts into messages.
|
|
853
|
+
*
|
|
854
|
+
* @example
|
|
855
|
+
* ```ts
|
|
856
|
+
* const messages = [...baseMessages, ...renderDecisionAttempts(request)];
|
|
857
|
+
* ```
|
|
858
|
+
*/
|
|
859
|
+
function renderDecisionAttempts(request) {
|
|
860
|
+
const types = request.events.map((event) => event.type).join(", ") || "(none)";
|
|
861
|
+
return request.attempts.map((attempt) => userMessage(`Your previous choice failed: ${attempt.reason}. Choose again from: ${types}`));
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Validation + retry core for decisions. No provider mechanics — the
|
|
865
|
+
* `executor` is responsible for making the model choose an event; this
|
|
866
|
+
* function only validates the choice and retries on failure, up to
|
|
867
|
+
* `options.maxRetries` (default 2, i.e. up to 3 attempts total).
|
|
868
|
+
*
|
|
869
|
+
* Each attempt is checked in order and can fail one of three ways (recorded
|
|
870
|
+
* as a {@link DecisionAttempt}): `'unknown-event'` (the chosen `type` is not
|
|
871
|
+
* among `request.events`), `'invalid-payload'` (the payload fails that
|
|
872
|
+
* event's schema), or `'rejected-by-guard'` (passes both checks but
|
|
873
|
+
* `options.canTake` returns `false` — a type/payload-legal event the
|
|
874
|
+
* machine's guard rejects right now; omit `canTake` to skip this check).
|
|
875
|
+
* Every prior failed attempt for this call is fed back to the executor on
|
|
876
|
+
* the next attempt via `request.attempts`, so an adapter can render "your
|
|
877
|
+
* last choice failed because X — try again" into the next model call; core
|
|
878
|
+
* never rewrites the request itself. Exhausting all attempts throws
|
|
879
|
+
* {@link DecisionExhaustedError} with the full attempts list.
|
|
880
|
+
*
|
|
881
|
+
* @example
|
|
882
|
+
* ```ts
|
|
883
|
+
* const event = await resolveDecision(request, decide, {
|
|
884
|
+
* canTake: (e) => snapshot.can(e),
|
|
885
|
+
* });
|
|
886
|
+
* ```
|
|
887
|
+
*/
|
|
888
|
+
async function resolveDecision(request, executor, options = {}) {
|
|
889
|
+
const maxRetries = options.maxRetries ?? 2;
|
|
890
|
+
const attempts = [];
|
|
891
|
+
const eventsByType = new Map(request.events.map((event) => [event.type, event]));
|
|
892
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
893
|
+
options.signal?.throwIfAborted();
|
|
894
|
+
const { event } = await executor({
|
|
895
|
+
...request,
|
|
896
|
+
attempts: [...attempts],
|
|
897
|
+
signal: options.signal
|
|
898
|
+
});
|
|
899
|
+
const descriptor = eventsByType.get(event.type);
|
|
900
|
+
if (!descriptor) {
|
|
901
|
+
attempts.push({
|
|
902
|
+
event,
|
|
903
|
+
failure: "unknown-event",
|
|
904
|
+
reason: `'${event.type}' is not among the currently allowed events: ${request.events.map((candidate) => candidate.type).join(", ") || "(none)"}.`
|
|
905
|
+
});
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
let validatedEvent = event;
|
|
909
|
+
if (descriptor.inputSchema) {
|
|
910
|
+
const { type, ...payload } = event;
|
|
911
|
+
try {
|
|
912
|
+
validatedEvent = {
|
|
913
|
+
...validateSchemaSync(descriptor.inputSchema, payload),
|
|
914
|
+
type
|
|
915
|
+
};
|
|
916
|
+
} catch (error) {
|
|
917
|
+
attempts.push({
|
|
918
|
+
event,
|
|
919
|
+
failure: "invalid-payload",
|
|
920
|
+
reason: `'${event.type}' payload failed validation: ${error instanceof Error ? error.message : String(error)}`
|
|
921
|
+
});
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (options.canTake?.(validatedEvent) === false) {
|
|
926
|
+
attempts.push({
|
|
927
|
+
event: validatedEvent,
|
|
928
|
+
failure: "rejected-by-guard",
|
|
929
|
+
reason: `'${validatedEvent.type}' is not currently takeable (guard rejected it).`
|
|
930
|
+
});
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
return validatedEvent;
|
|
934
|
+
}
|
|
935
|
+
throw new DecisionExhaustedError(attempts);
|
|
936
|
+
}
|
|
937
|
+
//#endregion
|
|
938
|
+
export { agentExecutionOptions as A, getJsonSchema as B, executeAgentTextRequest as C, normalizeGeneratorResult as D, isTextLogic as E, machineSuspensionPredicates as F, persistSnapshot as G, getMachineStructuralHash as H, missingActor as I, userMessage as J, systemMessage as K, assistantMessage as L, getMachineSuspensionPredicate as M, getRegisteredAgentExecutionOptions as N, parseOutput as O, isUnboundPlaceholder as P, findNonSerializableContextPaths as R, createTextLogic as S, isStructuredOutputSchema as T, getStateMeta as U, getJsonSchemaSync as V, isStandardSchema as W, validateSchemaSync as Y, PLAN_ACTOR as _, createPlanActor as a, buildEnvelopeSchema as b, isPlanLogic as c, EVENT_TOOL_PREFIX as d, getAcceptedEvents as f, DECIDE_ACTOR as g, sanitizeEventToolName as h, createDecideActor as i, executorBoundLogics as j, userInputActor as k, renderDecisionAttempts as l, parseAgentEvent as m, PLAN_DONE_EVENT_TYPE as n, initialPlanLedger as o, matchesEventPattern as p, toolMessage as q, advancePlanLedger as r, isDecisionLogic as s, DecisionExhaustedError as t, resolveDecision as u, USER_INPUT_ACTOR as v, getAgentOutputMode as w, builtinTextActors as x, bindRequestExecutor as y, getAgentMessages as z };
|