@statelyai/agent 2.0.0-alpha.24 → 2.0.0-alpha.26
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 +30 -34
- package/dist/ai-sdk.d.cts +27 -31
- package/dist/ai-sdk.d.mts +27 -31
- package/dist/ai-sdk.mjs +27 -31
- package/dist/decision-CM9CYZ6t.cjs +519 -0
- package/dist/decision-DzSv1NpD.mjs +453 -0
- package/dist/event-log-store-Ba2fsc4E.mjs +576 -0
- package/dist/event-log-store-DQmImLYk.cjs +702 -0
- package/dist/event-log-store-DvOcs10J.d.cts +338 -0
- package/dist/event-log-store-DxyVgnas.d.mts +338 -0
- package/dist/index.cjs +429 -4272
- package/dist/index.d.cts +104 -1005
- package/dist/index.d.mts +104 -1005
- package/dist/index.mjs +389 -4202
- package/dist/log.cjs +392 -0
- package/dist/log.d.cts +32 -0
- package/dist/log.d.mts +32 -0
- package/dist/log.mjs +371 -0
- package/dist/machines.cjs +27 -16
- package/dist/machines.d.cts +1 -1
- package/dist/machines.d.mts +1 -1
- package/dist/machines.mjs +27 -16
- package/dist/openai.cjs +16 -15
- package/dist/openai.d.cts +9 -9
- package/dist/openai.d.mts +9 -9
- package/dist/openai.mjs +10 -9
- package/dist/otel.d.cts +1 -1
- package/dist/otel.d.mts +1 -1
- package/dist/{run-agent-Ck7Sa3Tb.d.mts → run-agent-BOfoJg5B.d.cts} +86 -387
- package/dist/{run-agent-DWqA8KiI.d.cts → run-agent-CG343kFs.d.mts} +86 -387
- package/dist/run-agent-Cb1-LS5P.cjs +1783 -0
- package/dist/run-agent-DvDQd4If.mjs +1669 -0
- package/dist/{setup-agent-CeO2fARv.mjs → setup-agent-CFW-jPI8.mjs} +9 -20
- package/dist/{setup-agent-Dl2VELLi.cjs → setup-agent-CPf7MxnG.cjs} +20 -37
- package/dist/testing.cjs +1088 -0
- package/dist/testing.d.cts +617 -0
- package/dist/testing.d.mts +617 -0
- package/dist/testing.mjs +1079 -0
- package/dist/{text-logic-DJBK3LbG.d.mts → text-logic-BFEOxyxv.d.cts} +71 -147
- package/dist/text-logic-Bl_ZRxDi.mjs +833 -0
- package/dist/text-logic-C8xyr6Ag.cjs +1091 -0
- package/dist/{text-logic-C7bSnH_B.d.cts → text-logic-hZSiRUo1.d.mts} +71 -147
- package/dist/{types-CjHk5OdH.d.cts → types-B1Qs2q3q.d.cts} +2 -1
- package/dist/{types-DWA6X9R-.d.mts → types-tA3SdczG.d.mts} +2 -1
- package/dist/usage-BdEjFuIi.mjs +5 -0
- package/dist/usage-efOQPdf_.cjs +10 -0
- package/dist/utils-C2DPqomn.d.mts +109 -0
- package/dist/utils-ceStNCOh.d.cts +109 -0
- package/package.json +26 -5
- package/readme.md +5 -3
- package/skills/generate-machine/SKILL.md +4 -6
- package/dist/decision-BBZ2jMO1.mjs +0 -1490
- package/dist/decision-Cv6SfNWl.cjs +0 -1856
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
const require_text_logic = require("./text-logic-C8xyr6Ag.cjs");
|
|
2
|
+
require("./validate.cjs");
|
|
3
|
+
let xstate = require("xstate");
|
|
4
|
+
//#region src/internal/is-record.ts
|
|
5
|
+
/** True for a plain object-like value (arrays excluded). @internal */
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/events.ts
|
|
11
|
+
/**
|
|
12
|
+
* Thrown by {@link parseAgentEvent} (and {@link eventFromInteraction}) when a
|
|
13
|
+
* wire payload is not a usable event: it is not an object with a string
|
|
14
|
+
* `type`, it names a reserved `@agent.*` type, or its fields fail the schema
|
|
15
|
+
* the machine registered for that type. A host turns it into a 400.
|
|
16
|
+
*
|
|
17
|
+
* Not handling an event is NOT this error — a state machine ignores events it
|
|
18
|
+
* has no transition for, so an event the current state does not handle parses
|
|
19
|
+
* fine and settles the run with `result.ignored` set.
|
|
20
|
+
*/
|
|
21
|
+
var AgentInvalidEventPayloadError = class extends require_text_logic.AgentError {
|
|
22
|
+
eventType;
|
|
23
|
+
constructor(eventType, detail) {
|
|
24
|
+
super("invalid-event-payload", `Invalid event payload for '${eventType}': ${detail}`);
|
|
25
|
+
this.name = "AgentInvalidEventPayloadError";
|
|
26
|
+
this.eventType = eventType;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
/** Prefix for the synthetic tool name generated per candidate event (e.g. `send_event_ASK`). @internal */
|
|
30
|
+
const EVENT_TOOL_PREFIX = "send_event_";
|
|
31
|
+
function hashString(value) {
|
|
32
|
+
let hash = 5381;
|
|
33
|
+
for (let i = 0; i < value.length; i++) hash = hash * 33 ^ value.charCodeAt(i);
|
|
34
|
+
return (hash >>> 0).toString(36);
|
|
35
|
+
}
|
|
36
|
+
function sanitizeEventToolName(eventType) {
|
|
37
|
+
const base = `${EVENT_TOOL_PREFIX}${eventType.replace(/[^a-zA-Z0-9_-]/g, "_") || "event"}`;
|
|
38
|
+
if (base.length <= 64) return base;
|
|
39
|
+
const hash = hashString(eventType);
|
|
40
|
+
const prefixLength = 64 - hash.length - 1;
|
|
41
|
+
return `${base.slice(0, prefixLength)}_${hash}`;
|
|
42
|
+
}
|
|
43
|
+
function disambiguateEventToolName(toolName, eventType, usedToolNames) {
|
|
44
|
+
if (!usedToolNames.has(toolName)) {
|
|
45
|
+
usedToolNames.add(toolName);
|
|
46
|
+
return toolName;
|
|
47
|
+
}
|
|
48
|
+
const suffix = `_${hashString(eventType)}`;
|
|
49
|
+
const uniqueToolName = `${toolName.slice(0, 64 - suffix.length)}${suffix}`;
|
|
50
|
+
usedToolNames.add(uniqueToolName);
|
|
51
|
+
return uniqueToolName;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* True when an event type matches an `allowedEvents` entry: an exact type,
|
|
55
|
+
* `'*'` (every event), or a `'prefix.*'` wildcard matching any deeper
|
|
56
|
+
* segment (`'todo.*'` matches `'todo.add'` and `'todo.list.clear'`, not
|
|
57
|
+
* `'todo'` itself — mirroring xstate's partial wildcard events).
|
|
58
|
+
*/
|
|
59
|
+
function matchesEventPattern(eventType, pattern) {
|
|
60
|
+
if (pattern === "*") return true;
|
|
61
|
+
if (pattern.endsWith(".*")) return eventType.startsWith(`${pattern.slice(0, -1)}`);
|
|
62
|
+
return eventType === pattern;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Event types reserved for delivery by the library (`@agent.*`). A machine
|
|
66
|
+
* may declare transitions on them,
|
|
67
|
+
* but they are never model-facing: {@link getAcceptedEvents} drops them before
|
|
68
|
+
* any `allowedEvents` matching, so they cannot be offered as a decision
|
|
69
|
+
* candidate (not even under a `'*'` wildcard) and {@link parseAgentEvent}
|
|
70
|
+
* rejects them — a model or a wire message must not be able to forge one.
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
const RESERVED_AGENT_EVENT_PREFIX = "@agent.";
|
|
74
|
+
function isReservedAgentEvent(eventType) {
|
|
75
|
+
return eventType.startsWith(RESERVED_AGENT_EVENT_PREFIX);
|
|
76
|
+
}
|
|
77
|
+
/** True when an `allowedEvents` entry is a wildcard pattern rather than a concrete event type. @internal */
|
|
78
|
+
function isEventPattern(entry) {
|
|
79
|
+
return entry === "*" || entry.endsWith(".*");
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Parses a wire payload (a request body, a socket frame) into an
|
|
83
|
+
* event typed as the machine's event union, so it can go straight to
|
|
84
|
+
* `runAgent({ event })` / `actor.send(...)` without a cast. Pass the machine
|
|
85
|
+
* itself, or any snapshot of it.
|
|
86
|
+
*
|
|
87
|
+
* It checks exactly what a schema can check: the payload is an object with a
|
|
88
|
+
* string `type`, that type is not one of the library-reserved `@agent.*` types,
|
|
89
|
+
* and — when the machine registered a schema for it — the fields satisfy that
|
|
90
|
+
* schema (defaults filled, transforms applied). Anything else throws
|
|
91
|
+
* {@link AgentInvalidEventPayloadError}, which a host answers with a 400.
|
|
92
|
+
*
|
|
93
|
+
* It deliberately does NOT check whether the current state handles the event.
|
|
94
|
+
* State machines ignore events they have no transition for; that is normal
|
|
95
|
+
* behavior, not a validation failure. Send the parsed event and read
|
|
96
|
+
* `result.ignored` if the host wants to tell the client nothing happened.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* const event = parseAgentEvent(machine, await request.json());
|
|
101
|
+
* const result = await runAgent(machine, { store, threadId, event, executors });
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
function parseAgentEvent(source, payload, options = {}) {
|
|
105
|
+
if (!isRecord(payload) || typeof payload.type !== "string") throw new AgentInvalidEventPayloadError("(unknown)", "expected an object with a string `type`.");
|
|
106
|
+
const event = payload;
|
|
107
|
+
if (isReservedAgentEvent(event.type)) throw new AgentInvalidEventPayloadError(event.type, "this event type is reserved for the library and cannot be sent from outside.");
|
|
108
|
+
const registered = require_text_logic.getAgentExecutionOptions(typeof source.provide === "function" ? source : source.machine)?.schemas;
|
|
109
|
+
const inputSchema = (options.events ?? options.schemas?.events ?? registered?.events)?.[event.type];
|
|
110
|
+
if (!inputSchema) return event;
|
|
111
|
+
const { type, ...fields } = event;
|
|
112
|
+
try {
|
|
113
|
+
return {
|
|
114
|
+
...require_text_logic.validateSchemaSync(inputSchema, fields),
|
|
115
|
+
type
|
|
116
|
+
};
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw new AgentInvalidEventPayloadError(event.type, error instanceof Error ? error.message : String(error));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Lists the events a snapshot can currently accept, as {@link AgentEventDescriptor}s
|
|
123
|
+
* a model can be offered (via `resolveDecision`/an adapter's tool-per-event
|
|
124
|
+
* mapping). **Filters by event TYPE only** — it does not evaluate guards, so
|
|
125
|
+
* a type-legal-but-guard-rejected event can still appear here. Guard
|
|
126
|
+
* legality is checked separately, at decision-resolution time, via
|
|
127
|
+
* `snapshot.can(event)` (the `canTake` option of {@link resolveDecision} /
|
|
128
|
+
* {@link ResolveDecisionOptions}). Pass `eventTypes` to further narrow to a
|
|
129
|
+
* declared `allowedEvents` set — entries may be exact types or wildcard
|
|
130
|
+
* patterns (`'*'`, `'todo.*'`; see {@link matchesEventPattern}).
|
|
131
|
+
*
|
|
132
|
+
* XState-internal (`xstate.*`) and library-reserved
|
|
133
|
+
* ({@link RESERVED_AGENT_EVENT_PREFIX}) event types are always excluded, before
|
|
134
|
+
* any `allowedEvents` matching — a machine that handles `'@agent.usage'` still
|
|
135
|
+
* never offers it to a model.
|
|
136
|
+
*/
|
|
137
|
+
function getAcceptedEvents(snapshot, options = {}) {
|
|
138
|
+
const eventTypes = options.eventTypes;
|
|
139
|
+
const seen = /* @__PURE__ */ new Set();
|
|
140
|
+
const usedToolNames = /* @__PURE__ */ new Set();
|
|
141
|
+
return (0, xstate.getNextTransitions)(snapshot).flatMap((transitionDefinition) => {
|
|
142
|
+
const eventType = transitionDefinition.eventType;
|
|
143
|
+
if (!eventType || eventType === "*" || eventType.startsWith("xstate.") || isReservedAgentEvent(eventType) || eventTypes && !eventTypes.some((pattern) => matchesEventPattern(eventType, pattern)) || seen.has(eventType)) return [];
|
|
144
|
+
seen.add(eventType);
|
|
145
|
+
const toolName = disambiguateEventToolName(sanitizeEventToolName(eventType), eventType, usedToolNames);
|
|
146
|
+
const inputSchema = (options.events ?? options.schemas?.events)?.[eventType];
|
|
147
|
+
return [{
|
|
148
|
+
type: eventType,
|
|
149
|
+
toolName,
|
|
150
|
+
...inputSchema ? { inputSchema } : {}
|
|
151
|
+
}];
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/decision.ts
|
|
156
|
+
function decideRequestFromInput(input) {
|
|
157
|
+
const allowedEventTypes = resolveAllowedEventTypes(input.allowedEvents, input) ?? [];
|
|
158
|
+
return {
|
|
159
|
+
kind: "decision",
|
|
160
|
+
id: "",
|
|
161
|
+
name: input.name,
|
|
162
|
+
input,
|
|
163
|
+
model: input.model,
|
|
164
|
+
system: input.system,
|
|
165
|
+
prompt: input.prompt,
|
|
166
|
+
messages: input.messages,
|
|
167
|
+
events: allowedEventTypes.filter((type) => !isEventPattern(type)).map((type) => ({
|
|
168
|
+
type,
|
|
169
|
+
toolName: sanitizeEventToolName(type)
|
|
170
|
+
})),
|
|
171
|
+
attempts: [],
|
|
172
|
+
temperature: input.temperature,
|
|
173
|
+
maxOutputTokens: input.maxOutputTokens,
|
|
174
|
+
topP: input.topP,
|
|
175
|
+
topK: input.topK,
|
|
176
|
+
seed: input.seed,
|
|
177
|
+
stopSequences: input.stopSequences,
|
|
178
|
+
metadata: input.metadata
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function decideActorWithExecutor(execute) {
|
|
182
|
+
const logic = (0, xstate.createAsyncLogic)({ run: async ({ input, signal }) => {
|
|
183
|
+
if (!execute) throw new Error(`'${require_text_logic.DECIDE_ACTOR}' has no host execution. Provide an implementation with machine.provide({ actors: { '${require_text_logic.DECIDE_ACTOR}': ... } }) or resolve the returned agent request with resolveDecision(...).`);
|
|
184
|
+
const resolvedEventTypes = resolveAllowedEventTypes(input.allowedEvents, input);
|
|
185
|
+
if (resolvedEventTypes === void 0) throw new Error(`'${require_text_logic.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.`);
|
|
186
|
+
if (resolvedEventTypes.some(isEventPattern)) throw new Error(`'${require_text_logic.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.`);
|
|
187
|
+
return resolveDecision(decideRequestFromInput(input), { decide: execute }, {
|
|
188
|
+
maxRetries: input.maxRetries ?? 2,
|
|
189
|
+
signal
|
|
190
|
+
});
|
|
191
|
+
} });
|
|
192
|
+
return Object.assign(logic, {
|
|
193
|
+
kind: "statelyai.decisionLogic",
|
|
194
|
+
maxRetries: 2,
|
|
195
|
+
request: decideRequestFromInput,
|
|
196
|
+
allowedEventTypes: (input) => resolveAllowedEventTypes(input.allowedEvents, input),
|
|
197
|
+
withExecutor: (nextExecute) => decideActorWithExecutor(nextExecute)
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function createDecideActor() {
|
|
201
|
+
return decideActorWithExecutor();
|
|
202
|
+
}
|
|
203
|
+
function resolveAllowedEventTypes(allowedEvents, input) {
|
|
204
|
+
if (allowedEvents === void 0) return;
|
|
205
|
+
const resolved = typeof allowedEvents === "function" ? allowedEvents({ input }) : allowedEvents;
|
|
206
|
+
return typeof resolved === "string" ? [resolved] : resolved;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Creates reusable, standalone {@link DecisionLogic}: an actor that, when
|
|
210
|
+
* run, resolves to exactly one currently-legal {@link ChosenEvent} by
|
|
211
|
+
* calling the host `decide` executor (passed here as `execute`, or supplied
|
|
212
|
+
* later via {@link DecisionLogic.withExecutor}, `machine.provide(...)`, or
|
|
213
|
+
* `runAgent`'s `decide` option). Register the result under `actors:` and
|
|
214
|
+
* invoke it by name; for a one-off, state-local decision, prefer the
|
|
215
|
+
* `agent.decide` builtin invoke instead — it needs no separate declaration
|
|
216
|
+
* and types `allowedEvents` against the machine's own event schemas.
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```ts
|
|
220
|
+
* import { createDecisionLogic } from '@statelyai/agent';
|
|
221
|
+
* import { z } from 'zod';
|
|
222
|
+
*
|
|
223
|
+
* export const chooseMove = createDecisionLogic({
|
|
224
|
+
* schemas: { input: z.object({ playerHp: z.number(), enemyHp: z.number() }) },
|
|
225
|
+
* model: 'openai/gpt-5.4-mini',
|
|
226
|
+
* system: 'You are playing a turn-based game. Choose exactly one legal move.',
|
|
227
|
+
* prompt: ({ input }) => `Player HP: ${input.playerHp}\nEnemy HP: ${input.enemyHp}`,
|
|
228
|
+
* allowedEvents: ({ input }) =>
|
|
229
|
+
* input.playerHp <= 6
|
|
230
|
+
* ? ['ATTACK', 'DEFEND', 'HEAL', 'FLEE']
|
|
231
|
+
* : ['ATTACK', 'DEFEND', 'FLEE'],
|
|
232
|
+
* });
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
235
|
+
function createDecisionLogic(config, execute) {
|
|
236
|
+
const maxRetries = config.maxRetries ?? 2;
|
|
237
|
+
const request = (input) => {
|
|
238
|
+
const parsedInput = config.schemas ? require_text_logic.validateSchemaSync(config.schemas.input, input) : input;
|
|
239
|
+
const args = { input: parsedInput };
|
|
240
|
+
const allowedEventTypes = resolveAllowedEventTypes(config.allowedEvents, parsedInput);
|
|
241
|
+
return {
|
|
242
|
+
kind: "decision",
|
|
243
|
+
id: "",
|
|
244
|
+
name: require_text_logic.resolveTextLogicValue(config.name, args),
|
|
245
|
+
input: parsedInput,
|
|
246
|
+
model: require_text_logic.resolveTextLogicValue(config.model, args),
|
|
247
|
+
system: require_text_logic.resolveTextLogicValue(config.system, args),
|
|
248
|
+
prompt: require_text_logic.resolveTextLogicValue(config.prompt, args),
|
|
249
|
+
messages: require_text_logic.resolveTextLogicValue(config.messages, args),
|
|
250
|
+
events: (allowedEventTypes ?? []).map((type) => ({
|
|
251
|
+
type,
|
|
252
|
+
toolName: sanitizeEventToolName(type)
|
|
253
|
+
})),
|
|
254
|
+
attempts: [],
|
|
255
|
+
temperature: require_text_logic.resolveTextLogicValue(config.temperature, args),
|
|
256
|
+
maxOutputTokens: require_text_logic.resolveTextLogicValue(config.maxOutputTokens, args),
|
|
257
|
+
topP: require_text_logic.resolveTextLogicValue(config.topP, args),
|
|
258
|
+
topK: require_text_logic.resolveTextLogicValue(config.topK, args),
|
|
259
|
+
seed: require_text_logic.resolveTextLogicValue(config.seed, args),
|
|
260
|
+
stopSequences: require_text_logic.resolveTextLogicValue(config.stopSequences, args),
|
|
261
|
+
metadata: require_text_logic.resolveTextLogicValue(config.metadata, args)
|
|
262
|
+
};
|
|
263
|
+
};
|
|
264
|
+
const logic = (0, xstate.createAsyncLogic)({ run: async ({ input, signal }) => {
|
|
265
|
+
if (!execute) throw new Error("Decision logic has no host execution. Pass an executor as the second argument to createDecisionLogic(...), provide a runtime adapter, or bind it through runAgent/provideExecutors, or call resolveDecision from your host.");
|
|
266
|
+
const allowedEventTypes = resolveAllowedEventTypes(config.allowedEvents, input);
|
|
267
|
+
if (allowedEventTypes === void 0) throw new Error("Decision logic 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 on this logic to use it here.");
|
|
268
|
+
if (allowedEventTypes.some(isEventPattern)) throw new Error("Decision logic 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.");
|
|
269
|
+
return resolveDecision(request(input), { decide: execute }, {
|
|
270
|
+
maxRetries,
|
|
271
|
+
signal
|
|
272
|
+
});
|
|
273
|
+
} });
|
|
274
|
+
const decisionLogic = Object.assign(logic, {
|
|
275
|
+
kind: "statelyai.decisionLogic",
|
|
276
|
+
maxRetries,
|
|
277
|
+
request,
|
|
278
|
+
allowedEventTypes: (input) => resolveAllowedEventTypes(config.allowedEvents, input),
|
|
279
|
+
withExecutor(nextExecute) {
|
|
280
|
+
return createDecisionLogic(config, nextExecute);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
if (execute) require_text_logic.executorBoundLogics.add(decisionLogic);
|
|
284
|
+
return decisionLogic;
|
|
285
|
+
}
|
|
286
|
+
/** Type guard: true for any actor logic built by createDecisionLogic/createDecideActor (checks the `kind` marker). @internal */
|
|
287
|
+
function isDecisionLogic(value) {
|
|
288
|
+
return !!value && typeof value === "object" && value.kind === "statelyai.decisionLogic" && typeof value.request === "function";
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Builds an {@link AgentDecisionRequest} for {@link resolveDecision}, filling
|
|
292
|
+
* in the fields a hand-written literal has to restate every time: `kind`, an
|
|
293
|
+
* `id`, an empty `attempts` list, and each candidate event's `toolName`.
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* ```ts
|
|
297
|
+
* const chosen = await resolveDecision(
|
|
298
|
+
* createDecisionRequest({
|
|
299
|
+
* name: 'judge',
|
|
300
|
+
* model: 'reviewer',
|
|
301
|
+
* prompt: `Judge this draft.\n\n${snapshot.context.draft}`,
|
|
302
|
+
* events: getAcceptedEvents(snapshot),
|
|
303
|
+
* }),
|
|
304
|
+
* { decide },
|
|
305
|
+
* { canTake: (event) => snapshot.can(event) },
|
|
306
|
+
* );
|
|
307
|
+
* ```
|
|
308
|
+
*/
|
|
309
|
+
function createDecisionRequest(options) {
|
|
310
|
+
const { id, name, events, attempts, ...rest } = options;
|
|
311
|
+
return {
|
|
312
|
+
...rest,
|
|
313
|
+
kind: "decision",
|
|
314
|
+
id: id ?? name ?? "decision",
|
|
315
|
+
...name === void 0 ? {} : { name },
|
|
316
|
+
events: dedupeToolNames(events.map((event) => typeof event === "string" ? {
|
|
317
|
+
type: event,
|
|
318
|
+
toolName: sanitizeEventToolName(event)
|
|
319
|
+
} : event)),
|
|
320
|
+
attempts: attempts ?? []
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function dedupeToolNames(candidates) {
|
|
324
|
+
const taken = /* @__PURE__ */ new Set();
|
|
325
|
+
return candidates.map((candidate) => {
|
|
326
|
+
if (candidate.toolName === void 0) return candidate;
|
|
327
|
+
let toolName = candidate.toolName;
|
|
328
|
+
for (let n = 2; taken.has(toolName); n++) {
|
|
329
|
+
const suffix = `_${n}`;
|
|
330
|
+
toolName = `${candidate.toolName.slice(0, 64 - suffix.length)}${suffix}`;
|
|
331
|
+
}
|
|
332
|
+
taken.add(toolName);
|
|
333
|
+
return toolName === candidate.toolName ? candidate : {
|
|
334
|
+
...candidate,
|
|
335
|
+
toolName
|
|
336
|
+
};
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Thrown by {@link resolveDecision} when every attempt (up to
|
|
341
|
+
* `maxRetries + 1` of them) fails one of the three checks recorded in
|
|
342
|
+
* {@link DecisionAttempt.failure}. Carries the full `attempts` list for
|
|
343
|
+
* diagnostics; a machine typically routes this via the decision invoke's
|
|
344
|
+
* `onError`.
|
|
345
|
+
*/
|
|
346
|
+
var AgentDecisionExhaustedError = class extends require_text_logic.AgentError {
|
|
347
|
+
attempts;
|
|
348
|
+
constructor(attempts) {
|
|
349
|
+
super("decision-exhausted", `Decision exhausted after ${attempts.length} attempt${attempts.length === 1 ? "" : "s"}: ` + attempts.map((attempt) => attempt.reason).join("; "));
|
|
350
|
+
this.name = "AgentDecisionExhaustedError";
|
|
351
|
+
this.attempts = attempts;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
/**
|
|
355
|
+
* Renders a decision request's prior failed `attempts` into feedback messages
|
|
356
|
+
* a host appends to the model call so retries converge — the transport-agnostic
|
|
357
|
+
* "your last choice failed because X, choose again from Y" logic every adapter
|
|
358
|
+
* and raw-SDK host repeats. Returns one `user`-role {@link AgentMessage} per
|
|
359
|
+
* attempt (empty when there are none); adapters map each onto their wire
|
|
360
|
+
* message shape (`attempt.content` is always a string). Core never rewrites the
|
|
361
|
+
* request itself — this only turns the recorded attempts into messages.
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* ```ts
|
|
365
|
+
* const messages = [...baseMessages, ...renderDecisionAttempts(request)];
|
|
366
|
+
* ```
|
|
367
|
+
*/
|
|
368
|
+
function renderDecisionAttempts(request) {
|
|
369
|
+
const types = request.events.map((event) => event.type).join(", ") || "(none)";
|
|
370
|
+
return request.attempts.map((attempt) => require_text_logic.userMessage(`Your previous choice failed: ${attempt.reason}. Choose again from: ${types}`));
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Validation + retry core for decisions. No provider mechanics — the
|
|
374
|
+
* `executors.decide` is responsible for making the model choose an event; this
|
|
375
|
+
* function only validates the choice and retries on failure, up to
|
|
376
|
+
* `options.maxRetries` (default 2, i.e. up to 3 attempts total).
|
|
377
|
+
*
|
|
378
|
+
* Each attempt is checked in order and can fail one of three ways (recorded
|
|
379
|
+
* as a {@link DecisionAttempt}): `'unknown-event'` (the chosen `type` is not
|
|
380
|
+
* among `request.events`), `'invalid-payload'` (the payload fails that
|
|
381
|
+
* event's schema), or `'rejected-by-guard'` (passes both checks but
|
|
382
|
+
* `options.canTake` returns `false` — a type/payload-legal event the
|
|
383
|
+
* machine's guard rejects right now; omit `canTake` to skip this check).
|
|
384
|
+
* Every prior failed attempt for this call is fed back to the executor on
|
|
385
|
+
* the next attempt via `request.attempts`, so an adapter can render "your
|
|
386
|
+
* last choice failed because X — try again" into the next model call; core
|
|
387
|
+
* never rewrites the request itself. Exhausting all attempts throws
|
|
388
|
+
* {@link AgentDecisionExhaustedError} with the full attempts list.
|
|
389
|
+
*
|
|
390
|
+
* @example
|
|
391
|
+
* ```ts
|
|
392
|
+
* const event = await resolveDecision(request, executors, {
|
|
393
|
+
* canTake: (e) => snapshot.can(e),
|
|
394
|
+
* });
|
|
395
|
+
* ```
|
|
396
|
+
*/
|
|
397
|
+
async function resolveDecision(request, executors, options = {}) {
|
|
398
|
+
const executor = executors?.decide;
|
|
399
|
+
if (typeof executor !== "function") throw new require_text_logic.AgentError("missing-decide-executor", "resolveDecision(request, executors, ...) takes the EXECUTOR SET (the same object executeAgentRequest takes), and this one has no 'decide' function. Build one with createAiSdkExecutors({ models }), or pass { decide }.");
|
|
400
|
+
const maxRetries = options.maxRetries ?? 2;
|
|
401
|
+
const attempts = [];
|
|
402
|
+
const eventsByType = new Map(request.events.map((event) => [event.type, event]));
|
|
403
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
404
|
+
options.signal?.throwIfAborted();
|
|
405
|
+
const result = await executor({
|
|
406
|
+
...request,
|
|
407
|
+
name: request.name ?? (request.id || "agent.decide"),
|
|
408
|
+
attempts: [...attempts]
|
|
409
|
+
}, {
|
|
410
|
+
...options.signal !== void 0 ? { signal: options.signal } : {},
|
|
411
|
+
...request.id ? { requestId: request.id } : {}
|
|
412
|
+
});
|
|
413
|
+
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' } }.`);
|
|
414
|
+
const { event } = result;
|
|
415
|
+
const descriptor = eventsByType.get(event.type);
|
|
416
|
+
if (!descriptor) {
|
|
417
|
+
attempts.push({
|
|
418
|
+
event,
|
|
419
|
+
failure: "unknown-event",
|
|
420
|
+
reason: `'${event.type}' is not among the currently allowed events: ${request.events.map((candidate) => candidate.type).join(", ") || "(none)"}.`
|
|
421
|
+
});
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
let validatedEvent = event;
|
|
425
|
+
if (descriptor.inputSchema) {
|
|
426
|
+
const { type, ...payload } = event;
|
|
427
|
+
try {
|
|
428
|
+
validatedEvent = {
|
|
429
|
+
...require_text_logic.validateSchemaSync(descriptor.inputSchema, payload),
|
|
430
|
+
type
|
|
431
|
+
};
|
|
432
|
+
} catch (error) {
|
|
433
|
+
attempts.push({
|
|
434
|
+
event,
|
|
435
|
+
failure: "invalid-payload",
|
|
436
|
+
reason: `'${event.type}' payload failed validation: ${error instanceof Error ? error.message : String(error)}`
|
|
437
|
+
});
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (options.canTake?.(validatedEvent) === false) {
|
|
442
|
+
attempts.push({
|
|
443
|
+
event: validatedEvent,
|
|
444
|
+
failure: "rejected-by-guard",
|
|
445
|
+
reason: `'${validatedEvent.type}' is not currently takeable (guard rejected it).`
|
|
446
|
+
});
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
return validatedEvent;
|
|
450
|
+
}
|
|
451
|
+
throw new AgentDecisionExhaustedError(attempts);
|
|
452
|
+
}
|
|
453
|
+
//#endregion
|
|
454
|
+
Object.defineProperty(exports, "AgentDecisionExhaustedError", {
|
|
455
|
+
enumerable: true,
|
|
456
|
+
get: function() {
|
|
457
|
+
return AgentDecisionExhaustedError;
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
Object.defineProperty(exports, "AgentInvalidEventPayloadError", {
|
|
461
|
+
enumerable: true,
|
|
462
|
+
get: function() {
|
|
463
|
+
return AgentInvalidEventPayloadError;
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
Object.defineProperty(exports, "createDecideActor", {
|
|
467
|
+
enumerable: true,
|
|
468
|
+
get: function() {
|
|
469
|
+
return createDecideActor;
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
Object.defineProperty(exports, "createDecisionLogic", {
|
|
473
|
+
enumerable: true,
|
|
474
|
+
get: function() {
|
|
475
|
+
return createDecisionLogic;
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
Object.defineProperty(exports, "createDecisionRequest", {
|
|
479
|
+
enumerable: true,
|
|
480
|
+
get: function() {
|
|
481
|
+
return createDecisionRequest;
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
Object.defineProperty(exports, "getAcceptedEvents", {
|
|
485
|
+
enumerable: true,
|
|
486
|
+
get: function() {
|
|
487
|
+
return getAcceptedEvents;
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
Object.defineProperty(exports, "isDecisionLogic", {
|
|
491
|
+
enumerable: true,
|
|
492
|
+
get: function() {
|
|
493
|
+
return isDecisionLogic;
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
Object.defineProperty(exports, "isRecord", {
|
|
497
|
+
enumerable: true,
|
|
498
|
+
get: function() {
|
|
499
|
+
return isRecord;
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
Object.defineProperty(exports, "parseAgentEvent", {
|
|
503
|
+
enumerable: true,
|
|
504
|
+
get: function() {
|
|
505
|
+
return parseAgentEvent;
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
Object.defineProperty(exports, "renderDecisionAttempts", {
|
|
509
|
+
enumerable: true,
|
|
510
|
+
get: function() {
|
|
511
|
+
return renderDecisionAttempts;
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
Object.defineProperty(exports, "resolveDecision", {
|
|
515
|
+
enumerable: true,
|
|
516
|
+
get: function() {
|
|
517
|
+
return resolveDecision;
|
|
518
|
+
}
|
|
519
|
+
});
|