@statelyai/agent 1.1.6 → 2.0.0-alpha.11
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/adapter.cjs +15 -0
- package/dist/adapter.d.cts +4 -0
- package/dist/adapter.d.mts +4 -0
- package/dist/adapter.mjs +2 -0
- package/dist/ai-sdk.cjs +306 -0
- package/dist/ai-sdk.d.cts +96 -0
- package/dist/ai-sdk.d.mts +96 -0
- package/dist/ai-sdk.mjs +304 -0
- package/dist/decision-C3k4ve51.mjs +227 -0
- package/dist/decision-D8wJrM8W.cjs +286 -0
- package/dist/events-CRQj3VtP.cjs +1010 -0
- package/dist/events-JiVPYrct.mjs +759 -0
- package/dist/index.cjs +2528 -0
- package/dist/index.d.cts +1232 -0
- package/dist/index.d.mts +1217 -413
- package/dist/index.mjs +2489 -584
- package/dist/openai-compat.cjs +309 -0
- package/dist/openai-compat.d.cts +59 -0
- package/dist/openai-compat.d.mts +59 -0
- package/dist/openai-compat.mjs +308 -0
- package/dist/steps-BALp1eZo.d.mts +198 -0
- package/dist/steps-CVe54GPP.cjs +420 -0
- package/dist/steps-CkyyyuHd.mjs +379 -0
- package/dist/steps-MjnQI4aB.d.cts +198 -0
- package/dist/steps.cjs +12 -0
- package/dist/steps.d.cts +3 -0
- package/dist/steps.d.mts +3 -0
- package/dist/steps.mjs +3 -0
- package/dist/text-logic-CaKqgX4Y.d.mts +710 -0
- package/dist/text-logic-Ckhr2kKC.d.cts +710 -0
- package/dist/types-C9QiMjre.d.cts +219 -0
- package/dist/types-qm00QF91.d.mts +219 -0
- package/dist/utils-BYqT_Dyv.d.cts +108 -0
- package/dist/utils-Do5wIJrh.d.mts +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 +132 -28
- package/readme.md +153 -6
- package/schemas/agent-workflow.json +526 -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
package/dist/ai-sdk.mjs
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { L as isStandardSchema, d as buildEnvelopeSchema, h as getAgentOutputMode } from "./events-JiVPYrct.mjs";
|
|
2
|
+
import { l as renderDecisionAttempts } from "./decision-C3k4ve51.mjs";
|
|
3
|
+
import { NoObjectGeneratedError, Output, generateText, stepCountIs, streamText, tool } from "ai";
|
|
4
|
+
//#region src/ai-sdk/mappers.ts
|
|
5
|
+
/**
|
|
6
|
+
* Maps an {@link AgentTools} map onto AI SDK `tool()` definitions. A tool that
|
|
7
|
+
* already carries its own Standard Schema `inputSchema` — an AI SDK
|
|
8
|
+
* `tool({...})`, or any equivalent descriptor — is passed through **unchanged**,
|
|
9
|
+
* so the SDK applies its own validation, calls `execute(input, options)`, and
|
|
10
|
+
* every extra property (`providerOptions`, `toModelOutput`, …) survives. A bare
|
|
11
|
+
* `AgentToolExecute` function becomes a tool with an unconstrained input schema;
|
|
12
|
+
* a minimal descriptor with no readable schema is converted with a permissive
|
|
13
|
+
* fallback. Used by {@link toAiSdkCallSettings}.
|
|
14
|
+
*/
|
|
15
|
+
function toAiSdkTools(tools) {
|
|
16
|
+
const entries = [];
|
|
17
|
+
for (const [name, descriptor] of Object.entries(tools)) {
|
|
18
|
+
if (!descriptor) continue;
|
|
19
|
+
if (typeof descriptor === "function") {
|
|
20
|
+
entries.push([name, tool({
|
|
21
|
+
inputSchema: unknownSchema,
|
|
22
|
+
execute: (input) => descriptor(input)
|
|
23
|
+
})]);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (isStandardSchema(descriptor.inputSchema)) {
|
|
27
|
+
entries.push([name, descriptor]);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const inputSchema = descriptor.inputSchema ?? descriptor.schemas?.input;
|
|
31
|
+
const toolOptions = {
|
|
32
|
+
description: descriptor.description,
|
|
33
|
+
inputSchema: inputSchema ? inputSchema : unknownSchema
|
|
34
|
+
};
|
|
35
|
+
if (descriptor.execute) {
|
|
36
|
+
entries.push([name, tool({
|
|
37
|
+
...toolOptions,
|
|
38
|
+
execute: (input) => descriptor.execute?.(input)
|
|
39
|
+
})]);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
entries.push([name, tool(toolOptions)]);
|
|
43
|
+
}
|
|
44
|
+
return Object.fromEntries(entries);
|
|
45
|
+
}
|
|
46
|
+
const unknownSchema = { "~standard": {
|
|
47
|
+
version: 1,
|
|
48
|
+
vendor: "statelyai-agent",
|
|
49
|
+
validate: (value) => ({ value }),
|
|
50
|
+
jsonSchema: { input: () => ({}) }
|
|
51
|
+
} };
|
|
52
|
+
/**
|
|
53
|
+
* AI SDK request-mapping settings shared by `generateText`/`streamText`.
|
|
54
|
+
* `AgentTextRequest.messages` (`AgentMessage[]`) and AI SDK's `ModelMessage[]`
|
|
55
|
+
* are structurally compatible by design (§1 of .scratch/p0-design.md) — the cast
|
|
56
|
+
* below is a typed identity mapping, not a semantic conversion.
|
|
57
|
+
*/
|
|
58
|
+
function toAiSdkCallSettings(request) {
|
|
59
|
+
const messages = request.messages;
|
|
60
|
+
return {
|
|
61
|
+
system: request.system,
|
|
62
|
+
...messages ? { messages } : { prompt: request.prompt ?? "" },
|
|
63
|
+
temperature: request.temperature,
|
|
64
|
+
maxOutputTokens: request.maxOutputTokens,
|
|
65
|
+
topP: request.topP,
|
|
66
|
+
topK: request.topK,
|
|
67
|
+
seed: request.seed,
|
|
68
|
+
stopSequences: request.stopSequences,
|
|
69
|
+
tools: request.tools ? toAiSdkTools(request.tools) : void 0,
|
|
70
|
+
toolChoice: toAiSdkToolChoice(request.toolChoice)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** Maps an {@link AgentToolChoice} to AI SDK's tool-choice shape — `{ type: 'tool'; name }` becomes `{ type: 'tool'; toolName }`; `'auto'`/`'none'`/`'required'`/`undefined` pass through unchanged. */
|
|
74
|
+
function toAiSdkToolChoice(toolChoice) {
|
|
75
|
+
return typeof toolChoice === "object" ? {
|
|
76
|
+
type: "tool",
|
|
77
|
+
toolName: toolChoice.name
|
|
78
|
+
} : toolChoice;
|
|
79
|
+
}
|
|
80
|
+
/** `true` when the request should use AI SDK structured `Output.object`. */
|
|
81
|
+
function isStructuredOutputRequest(request) {
|
|
82
|
+
return getAgentOutputMode(request.outputSchema) === "structured";
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Extracts the first complete top-level JSON value from `text`, or returns
|
|
86
|
+
* `undefined` when there is nothing to repair (no complete value, or the value
|
|
87
|
+
* already spans the whole text). Models occasionally emit two structured-output
|
|
88
|
+
* envelopes back to back (`{"result":{…}}{"result":{…}}`), which fails JSON
|
|
89
|
+
* parsing wholesale; the balanced scan below recovers the first value and
|
|
90
|
+
* drops the rest.
|
|
91
|
+
*/
|
|
92
|
+
function extractFirstJsonValue(text) {
|
|
93
|
+
const start = text.search(/[{[]/);
|
|
94
|
+
if (start === -1) return;
|
|
95
|
+
let depth = 0;
|
|
96
|
+
let inString = false;
|
|
97
|
+
let escaped = false;
|
|
98
|
+
for (let i = start; i < text.length; i++) {
|
|
99
|
+
const char = text[i];
|
|
100
|
+
if (inString) {
|
|
101
|
+
if (escaped) escaped = false;
|
|
102
|
+
else if (char === "\\") escaped = true;
|
|
103
|
+
else if (char === "\"") inString = false;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (char === "\"") inString = true;
|
|
107
|
+
else if (char === "{" || char === "[") depth++;
|
|
108
|
+
else if (char === "}" || char === "]") {
|
|
109
|
+
depth--;
|
|
110
|
+
if (depth === 0) {
|
|
111
|
+
const value = text.slice(start, i + 1);
|
|
112
|
+
return value === text.trim() ? void 0 : value;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** One AI SDK `tool()` per candidate event — the "tool-per-event +
|
|
118
|
+
* toolChoice: 'required'" recipe from .scratch/p0-design.md §2.6. */
|
|
119
|
+
function toAiSdkEventTools(events) {
|
|
120
|
+
return Object.fromEntries(events.map((event) => [event.toolName, tool({
|
|
121
|
+
description: `Choose the '${event.type}' move.`,
|
|
122
|
+
inputSchema: event.inputSchema ? event.inputSchema : unknownSchema
|
|
123
|
+
})]));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Messages for a decision request, with prior failed `attempts` (§2.6)
|
|
127
|
+
* rendered as appended user messages (via core's {@link renderDecisionAttempts})
|
|
128
|
+
* so retries converge. Core never rewrites prompts — this is adapter business.
|
|
129
|
+
*/
|
|
130
|
+
function toDecisionMessages(request) {
|
|
131
|
+
if (!request.messages && request.attempts.length === 0) return;
|
|
132
|
+
const messages = [...request.messages ?? (request.prompt !== void 0 ? [{
|
|
133
|
+
role: "user",
|
|
134
|
+
content: request.prompt
|
|
135
|
+
}] : [])];
|
|
136
|
+
for (const attempt of renderDecisionAttempts(request)) messages.push({
|
|
137
|
+
role: "user",
|
|
138
|
+
content: attempt.content
|
|
139
|
+
});
|
|
140
|
+
return messages;
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/ai-sdk/index.ts
|
|
144
|
+
function maxStepsSetting(request) {
|
|
145
|
+
return typeof request.metadata?.maxSteps === "number" ? { stopWhen: stepCountIs(request.metadata.maxSteps) } : {};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Identity helper for a `models` map whose value is exported. Returns the map
|
|
149
|
+
* unchanged, but types it as {@link AiSdkModelMap}`<keyof T & string>` — a
|
|
150
|
+
* portable, nameable type — so an exported `const models = defineModels({...})`
|
|
151
|
+
* needs no `Record<'a' | 'b', LanguageModel>` annotation and never triggers
|
|
152
|
+
* TS2742 ("inferred type cannot be named without a reference to …"). The exact
|
|
153
|
+
* ref keys survive, so `createAiSdkExecutors({ models })` and
|
|
154
|
+
* `setupAgent({ models })` still infer/autocomplete them.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```ts
|
|
158
|
+
* export const models = defineModels({
|
|
159
|
+
* quick: openai('gpt-5.4-mini'),
|
|
160
|
+
* deep: openai('gpt-5.4'),
|
|
161
|
+
* });
|
|
162
|
+
* // typeof models === AiSdkModelMap<'quick' | 'deep'>
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
function defineModels(models) {
|
|
166
|
+
return models;
|
|
167
|
+
}
|
|
168
|
+
function resolveAiSdkModel(options, modelRef) {
|
|
169
|
+
if (options.resolveModel) return options.resolveModel(modelRef);
|
|
170
|
+
const models = options.models;
|
|
171
|
+
if (!models) throw new Error(`createAiSdkExecutors: no model resolver configured for '${modelRef}'.`);
|
|
172
|
+
const model = models[modelRef];
|
|
173
|
+
if (!model) throw new Error(`createAiSdkExecutors: unknown model '${modelRef}'.`);
|
|
174
|
+
return model;
|
|
175
|
+
}
|
|
176
|
+
function withJsonRepair(output) {
|
|
177
|
+
return {
|
|
178
|
+
...output,
|
|
179
|
+
parseCompleteOutput: async (options, context) => {
|
|
180
|
+
try {
|
|
181
|
+
return await output.parseCompleteOutput(options, context);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
const repaired = extractFirstJsonValue(options.text);
|
|
184
|
+
if (repaired === void 0) throw error;
|
|
185
|
+
return await output.parseCompleteOutput({
|
|
186
|
+
...options,
|
|
187
|
+
text: repaired
|
|
188
|
+
}, context);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The canonical Vercel AI SDK adapter: builds the `{ generateText, streamText,
|
|
195
|
+
* decide }` executor set consumed by `runAgent`/`executeAgentRequest`. `ai`
|
|
196
|
+
* must not become a dependency of core `src/` files — this subpath is the one
|
|
197
|
+
* place it's imported, and callers must supply their own model resolver so no
|
|
198
|
+
* concrete provider package (e.g. `@ai-sdk/openai`) becomes a dependency here
|
|
199
|
+
* either.
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```ts
|
|
203
|
+
* const executors = createAiSdkExecutors({ models: { quick: openai('gpt-5.4-mini') } });
|
|
204
|
+
* const result = await runAgent(machine, { input, executors });
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
function createAiSdkExecutors(options) {
|
|
208
|
+
const generateText$1 = async (request, info) => {
|
|
209
|
+
const common = {
|
|
210
|
+
model: resolveAiSdkModel(options, request.model),
|
|
211
|
+
abortSignal: info?.signal,
|
|
212
|
+
...toAiSdkCallSettings(request),
|
|
213
|
+
...maxStepsSetting(request)
|
|
214
|
+
};
|
|
215
|
+
if (isStructuredOutputRequest(request)) {
|
|
216
|
+
const envelope = buildEnvelopeSchema(request.outputSchema, { reasoning: request.reasoning });
|
|
217
|
+
const structuredOutput = withJsonRepair(Output.object({ schema: envelope }));
|
|
218
|
+
const canRetry = !request.tools || Object.keys(request.tools).length === 0;
|
|
219
|
+
let result;
|
|
220
|
+
try {
|
|
221
|
+
result = await generateText({
|
|
222
|
+
...common,
|
|
223
|
+
output: structuredOutput
|
|
224
|
+
});
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (!canRetry || !NoObjectGeneratedError.isInstance(error) || info?.signal?.aborted) throw error;
|
|
227
|
+
result = await generateText({
|
|
228
|
+
...common,
|
|
229
|
+
output: structuredOutput
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const { result: output, reasoning } = result.output;
|
|
233
|
+
return {
|
|
234
|
+
output,
|
|
235
|
+
...reasoning !== void 0 ? { reasoning } : {},
|
|
236
|
+
usage: result.usage,
|
|
237
|
+
finishReason: result.finishReason,
|
|
238
|
+
toolCalls: result.toolCalls,
|
|
239
|
+
toolResults: result.toolResults
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const result = await generateText(common);
|
|
243
|
+
return {
|
|
244
|
+
output: result.text,
|
|
245
|
+
usage: result.usage,
|
|
246
|
+
finishReason: result.finishReason,
|
|
247
|
+
toolCalls: result.toolCalls,
|
|
248
|
+
toolResults: result.toolResults
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
const streamText$1 = async (request, info) => {
|
|
252
|
+
const result = streamText({
|
|
253
|
+
model: resolveAiSdkModel(options, request.model),
|
|
254
|
+
abortSignal: info?.signal,
|
|
255
|
+
...toAiSdkCallSettings(request),
|
|
256
|
+
...maxStepsSetting(request)
|
|
257
|
+
});
|
|
258
|
+
for await (const chunk of result.textStream) info?.onChunk?.(chunk);
|
|
259
|
+
return {
|
|
260
|
+
output: await result.text,
|
|
261
|
+
usage: await result.usage,
|
|
262
|
+
finishReason: await result.finishReason
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
const decide = async (request) => {
|
|
266
|
+
const model = resolveAiSdkModel(options, request.model);
|
|
267
|
+
const tools = toAiSdkEventTools(request.events);
|
|
268
|
+
const messages = toDecisionMessages(request);
|
|
269
|
+
const result = await generateText({
|
|
270
|
+
model,
|
|
271
|
+
abortSignal: request.signal,
|
|
272
|
+
system: request.system,
|
|
273
|
+
...messages ? { messages } : { prompt: request.prompt ?? "" },
|
|
274
|
+
tools,
|
|
275
|
+
toolChoice: "required",
|
|
276
|
+
stopWhen: stepCountIs(1),
|
|
277
|
+
temperature: request.temperature,
|
|
278
|
+
maxOutputTokens: request.maxOutputTokens,
|
|
279
|
+
topP: request.topP,
|
|
280
|
+
topK: request.topK,
|
|
281
|
+
seed: request.seed,
|
|
282
|
+
stopSequences: request.stopSequences
|
|
283
|
+
});
|
|
284
|
+
const toolCall = result.toolCalls[0];
|
|
285
|
+
if (!toolCall) throw new Error("createAiSdkExecutors: decide — model did not call an event tool.");
|
|
286
|
+
const chosenEvent = request.events.find((event) => event.toolName === toolCall.toolName);
|
|
287
|
+
if (!chosenEvent) throw new Error(`createAiSdkExecutors: decide — model called unknown tool '${toolCall.toolName}'.`);
|
|
288
|
+
return {
|
|
289
|
+
event: {
|
|
290
|
+
...toolCall.input && typeof toolCall.input === "object" ? toolCall.input : {},
|
|
291
|
+
type: chosenEvent.type
|
|
292
|
+
},
|
|
293
|
+
usage: result.usage,
|
|
294
|
+
finishReason: result.finishReason
|
|
295
|
+
};
|
|
296
|
+
};
|
|
297
|
+
return {
|
|
298
|
+
generateText: generateText$1,
|
|
299
|
+
streamText: streamText$1,
|
|
300
|
+
decide
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
//#endregion
|
|
304
|
+
export { createAiSdkExecutors, defineModels };
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { H as validateSchemaSync, V as userMessage, a as sanitizeEventToolName, n as isEventPattern, o as DECIDE_ACTOR } from "./events-JiVPYrct.mjs";
|
|
2
|
+
import { createAsyncLogic, createLogic } from "xstate";
|
|
3
|
+
//#region src/decision.ts
|
|
4
|
+
function decideRequestFromInput(input) {
|
|
5
|
+
const allowedEventTypes = resolveAllowedEventTypes(input.allowedEvents, input) ?? [];
|
|
6
|
+
return {
|
|
7
|
+
kind: "decision",
|
|
8
|
+
id: "",
|
|
9
|
+
model: input.model,
|
|
10
|
+
system: input.system,
|
|
11
|
+
prompt: input.prompt,
|
|
12
|
+
messages: input.messages,
|
|
13
|
+
events: allowedEventTypes.filter((type) => !isEventPattern(type)).map((type) => ({
|
|
14
|
+
type,
|
|
15
|
+
toolName: sanitizeEventToolName(type)
|
|
16
|
+
})),
|
|
17
|
+
attempts: [],
|
|
18
|
+
temperature: input.temperature,
|
|
19
|
+
maxOutputTokens: input.maxOutputTokens,
|
|
20
|
+
topP: input.topP,
|
|
21
|
+
topK: input.topK,
|
|
22
|
+
seed: input.seed,
|
|
23
|
+
stopSequences: input.stopSequences,
|
|
24
|
+
metadata: input.metadata
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function decideActorWithExecutor(execute) {
|
|
28
|
+
const logic = createAsyncLogic({ run: async ({ input, signal }) => {
|
|
29
|
+
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(...).`);
|
|
30
|
+
const resolvedEventTypes = resolveAllowedEventTypes(input.allowedEvents, input);
|
|
31
|
+
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.`);
|
|
32
|
+
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.`);
|
|
33
|
+
return resolveDecision(decideRequestFromInput(input), execute, {
|
|
34
|
+
maxRetries: input.maxRetries ?? 2,
|
|
35
|
+
signal
|
|
36
|
+
});
|
|
37
|
+
} });
|
|
38
|
+
return Object.assign(logic, {
|
|
39
|
+
kind: "statelyai.decisionLogic",
|
|
40
|
+
maxRetries: 2,
|
|
41
|
+
request: decideRequestFromInput,
|
|
42
|
+
allowedEventTypes: (input) => resolveAllowedEventTypes(input.allowedEvents, input),
|
|
43
|
+
withExecutor: (nextExecute) => decideActorWithExecutor(nextExecute)
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function createDecideActor() {
|
|
47
|
+
return decideActorWithExecutor();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Reserved event type the `agent.plan` builtin adds to every step's
|
|
51
|
+
* candidates as the explicit "no further action needed" move. Choosing it
|
|
52
|
+
* ends the plan (`stopped: 'done'`); it is never sent to the machine, so
|
|
53
|
+
* machines need no no-op sentinel event of their own.
|
|
54
|
+
*/
|
|
55
|
+
const PLAN_DONE_EVENT_TYPE = "agent.plan.done";
|
|
56
|
+
function createPlanActor() {
|
|
57
|
+
const logic = createLogic({
|
|
58
|
+
context: ({ input }) => ({
|
|
59
|
+
applied: [],
|
|
60
|
+
stepsRemaining: input.maxSteps ?? 8,
|
|
61
|
+
stopped: null
|
|
62
|
+
}),
|
|
63
|
+
run: ({ context, event }) => event.type === "plan.applied" ? { context: {
|
|
64
|
+
...context,
|
|
65
|
+
applied: [...context.applied, event.event],
|
|
66
|
+
stepsRemaining: context.stepsRemaining - 1
|
|
67
|
+
} } : event.type === "plan.ended" ? {
|
|
68
|
+
context: {
|
|
69
|
+
...context,
|
|
70
|
+
stopped: event.stopped
|
|
71
|
+
},
|
|
72
|
+
status: "done",
|
|
73
|
+
output: {
|
|
74
|
+
steps: context.applied,
|
|
75
|
+
stopped: event.stopped
|
|
76
|
+
}
|
|
77
|
+
} : void 0
|
|
78
|
+
});
|
|
79
|
+
return Object.assign(logic, {
|
|
80
|
+
kind: "statelyai.planLogic",
|
|
81
|
+
maxRetries: 2,
|
|
82
|
+
request: decideRequestFromInput,
|
|
83
|
+
allowedEventTypes: (input) => resolveAllowedEventTypes(input.allowedEvents, input)
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const PLAN_LEDGER_SCOPE = { emit: () => {} };
|
|
87
|
+
/**
|
|
88
|
+
* Builds a fresh plan ledger snapshot from resolved plan input — the shared
|
|
89
|
+
* starting point for BOTH hosts (the step path reads the invoke child's own
|
|
90
|
+
* initial snapshot; runAgent seeds a local ledger with this). @internal
|
|
91
|
+
*/
|
|
92
|
+
function initialPlanLedger(logic, input) {
|
|
93
|
+
return logic.getInitialSnapshot(PLAN_LEDGER_SCOPE, input);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Advances a plan ledger by one {@link PlanLedgerEvent}, returning the next
|
|
97
|
+
* snapshot (unwrapping `createLogic`'s `[snapshot, effects]` tuple). Pure — no
|
|
98
|
+
* mutation of the input snapshot. @internal
|
|
99
|
+
*/
|
|
100
|
+
function advancePlanLedger(logic, snapshot, event) {
|
|
101
|
+
const result = logic.transition(snapshot, event, PLAN_LEDGER_SCOPE);
|
|
102
|
+
return Array.isArray(result) ? result[0] : result;
|
|
103
|
+
}
|
|
104
|
+
/** Type guard: true for the `agent.plan` builtin logic (checks the `kind` marker). @internal */
|
|
105
|
+
function isPlanLogic(logic) {
|
|
106
|
+
return !!logic && logic.kind === "statelyai.planLogic";
|
|
107
|
+
}
|
|
108
|
+
function resolveAllowedEventTypes(allowedEvents, input) {
|
|
109
|
+
if (allowedEvents === void 0) return;
|
|
110
|
+
const resolved = typeof allowedEvents === "function" ? allowedEvents({ input }) : allowedEvents;
|
|
111
|
+
return typeof resolved === "string" ? [resolved] : resolved;
|
|
112
|
+
}
|
|
113
|
+
/** Type guard: true for any actor logic built by createDecisionLogic/createDecideActor (checks the `kind` marker). @internal */
|
|
114
|
+
function isDecisionLogic(value) {
|
|
115
|
+
return !!value && typeof value === "object" && value.kind === "statelyai.decisionLogic" && typeof value.request === "function";
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Thrown by {@link resolveDecision} when every attempt (up to
|
|
119
|
+
* `maxRetries + 1` of them) fails one of the three checks recorded in
|
|
120
|
+
* {@link DecisionAttempt.failure}. Carries the full `attempts` list for
|
|
121
|
+
* diagnostics; a machine typically routes this via the decision invoke's
|
|
122
|
+
* `onError`.
|
|
123
|
+
*/
|
|
124
|
+
var DecisionExhaustedError = class extends Error {
|
|
125
|
+
attempts;
|
|
126
|
+
constructor(attempts) {
|
|
127
|
+
super(`Decision exhausted after ${attempts.length} attempt${attempts.length === 1 ? "" : "s"}: ` + attempts.map((attempt) => attempt.reason).join("; "));
|
|
128
|
+
this.name = "DecisionExhaustedError";
|
|
129
|
+
this.attempts = attempts;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Renders a decision request's prior failed `attempts` into feedback messages
|
|
134
|
+
* a host appends to the model call so retries converge — the transport-agnostic
|
|
135
|
+
* "your last choice failed because X, choose again from Y" logic every adapter
|
|
136
|
+
* and raw-SDK host repeats. Returns one `user`-role {@link AgentMessage} per
|
|
137
|
+
* attempt (empty when there are none); adapters map each onto their wire
|
|
138
|
+
* message shape (`attempt.content` is always a string). Core never rewrites the
|
|
139
|
+
* request itself — this only turns the recorded attempts into messages.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* const messages = [...baseMessages, ...renderDecisionAttempts(request)];
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
function renderDecisionAttempts(request) {
|
|
147
|
+
const types = request.events.map((event) => event.type).join(", ") || "(none)";
|
|
148
|
+
return request.attempts.map((attempt) => userMessage(`Your previous choice failed: ${attempt.reason}. Choose again from: ${types}`));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Validation + retry core for decisions. No provider mechanics — the
|
|
152
|
+
* `executor` is responsible for making the model choose an event; this
|
|
153
|
+
* function only validates the choice and retries on failure, up to
|
|
154
|
+
* `options.maxRetries` (default 2, i.e. up to 3 attempts total).
|
|
155
|
+
*
|
|
156
|
+
* Each attempt is checked in order and can fail one of three ways (recorded
|
|
157
|
+
* as a {@link DecisionAttempt}): `'unknown-event'` (the chosen `type` is not
|
|
158
|
+
* among `request.events`), `'invalid-payload'` (the payload fails that
|
|
159
|
+
* event's schema), or `'rejected-by-guard'` (passes both checks but
|
|
160
|
+
* `options.canTake` returns `false` — a type/payload-legal event the
|
|
161
|
+
* machine's guard rejects right now; omit `canTake` to skip this check).
|
|
162
|
+
* Every prior failed attempt for this call is fed back to the executor on
|
|
163
|
+
* the next attempt via `request.attempts`, so an adapter can render "your
|
|
164
|
+
* last choice failed because X — try again" into the next model call; core
|
|
165
|
+
* never rewrites the request itself. Exhausting all attempts throws
|
|
166
|
+
* {@link DecisionExhaustedError} with the full attempts list.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```ts
|
|
170
|
+
* const event = await resolveDecision(request, decide, {
|
|
171
|
+
* canTake: (e) => snapshot.can(e),
|
|
172
|
+
* });
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
async function resolveDecision(request, executor, options = {}) {
|
|
176
|
+
const maxRetries = options.maxRetries ?? 2;
|
|
177
|
+
const attempts = [];
|
|
178
|
+
const eventsByType = new Map(request.events.map((event) => [event.type, event]));
|
|
179
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
180
|
+
options.signal?.throwIfAborted();
|
|
181
|
+
const result = await executor({
|
|
182
|
+
...request,
|
|
183
|
+
attempts: [...attempts],
|
|
184
|
+
signal: options.signal
|
|
185
|
+
});
|
|
186
|
+
if (!result || typeof result !== "object" || typeof result.event !== "object" || result.event === null || typeof result.event.type !== "string") throw new Error(`decide executor must return { event: { type: string, ... } }; got ${JSON.stringify(result)}. Wrap the chosen event: return { event: { type: 'SAFE' } }.`);
|
|
187
|
+
const { event } = result;
|
|
188
|
+
const descriptor = eventsByType.get(event.type);
|
|
189
|
+
if (!descriptor) {
|
|
190
|
+
attempts.push({
|
|
191
|
+
event,
|
|
192
|
+
failure: "unknown-event",
|
|
193
|
+
reason: `'${event.type}' is not among the currently allowed events: ${request.events.map((candidate) => candidate.type).join(", ") || "(none)"}.`
|
|
194
|
+
});
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
let validatedEvent = event;
|
|
198
|
+
if (descriptor.inputSchema) {
|
|
199
|
+
const { type, ...payload } = event;
|
|
200
|
+
try {
|
|
201
|
+
validatedEvent = {
|
|
202
|
+
...validateSchemaSync(descriptor.inputSchema, payload),
|
|
203
|
+
type
|
|
204
|
+
};
|
|
205
|
+
} catch (error) {
|
|
206
|
+
attempts.push({
|
|
207
|
+
event,
|
|
208
|
+
failure: "invalid-payload",
|
|
209
|
+
reason: `'${event.type}' payload failed validation: ${error instanceof Error ? error.message : String(error)}`
|
|
210
|
+
});
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (options.canTake?.(validatedEvent) === false) {
|
|
215
|
+
attempts.push({
|
|
216
|
+
event: validatedEvent,
|
|
217
|
+
failure: "rejected-by-guard",
|
|
218
|
+
reason: `'${validatedEvent.type}' is not currently takeable (guard rejected it).`
|
|
219
|
+
});
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
return validatedEvent;
|
|
223
|
+
}
|
|
224
|
+
throw new DecisionExhaustedError(attempts);
|
|
225
|
+
}
|
|
226
|
+
//#endregion
|
|
227
|
+
export { createPlanActor as a, isPlanLogic as c, createDecideActor as i, renderDecisionAttempts as l, PLAN_DONE_EVENT_TYPE as n, initialPlanLedger as o, advancePlanLedger as r, isDecisionLogic as s, DecisionExhaustedError as t, resolveDecision as u };
|