@statelyai/agent 2.0.0-alpha.28 → 2.0.0-alpha.29
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 +149 -26
- package/dist/ai-sdk.d.cts +57 -34
- package/dist/ai-sdk.d.mts +57 -34
- package/dist/ai-sdk.mjs +147 -23
- package/dist/{decision-RQ0iv0oZ.mjs → decision-Bl_rKQnU.mjs} +73 -7
- package/dist/{decision-CmM8-HHz.cjs → decision-oevZnkBe.cjs} +112 -29
- package/dist/{text-logic-f5F5sCdc.cjs → event-log-Dtb16p1p.cjs} +813 -6
- package/dist/{text-logic-CIqHAmBi.mjs → event-log-GPZO9p7_.mjs} +679 -3
- package/dist/{event-log-store-BIPOz-di.d.mts → event-log-store-CN9ygggF.d.cts} +90 -5
- package/dist/{event-log-store-2xzFmgr2.d.cts → event-log-store-DS836wch.d.mts} +90 -5
- package/dist/event-log-store-bCdskPNS.cjs +95 -0
- package/dist/event-log-store-yalq8C77.mjs +84 -0
- package/dist/index.cjs +571 -58
- package/dist/index.d.cts +104 -510
- package/dist/index.d.mts +104 -510
- package/dist/index.mjs +542 -32
- package/dist/log.cjs +77 -20
- package/dist/log.d.cts +5 -5
- package/dist/log.d.mts +5 -5
- package/dist/log.mjs +58 -3
- package/dist/machines.cjs +1 -1
- package/dist/machines.d.cts +15 -15
- package/dist/machines.d.mts +15 -15
- package/dist/machines.mjs +1 -1
- package/dist/model-source-C7FFP2Sj.mjs +39 -0
- package/dist/model-source-DjJPjGSP.cjs +44 -0
- package/dist/openai.cjs +10 -10
- package/dist/openai.d.cts +27 -25
- package/dist/openai.d.mts +27 -25
- package/dist/openai.mjs +2 -2
- package/dist/otel.cjs +5 -4
- package/dist/otel.d.cts +5 -7
- package/dist/otel.d.mts +5 -7
- package/dist/otel.mjs +5 -3
- package/dist/{run-agent-COXXc0RW.d.mts → run-agent-C9ubX8Tx.d.cts} +87 -30
- package/dist/{run-agent-xNTxpt82.mjs → run-agent-CuQT1xSS.mjs} +170 -42
- package/dist/{run-agent-ByO-Bxyd.d.cts → run-agent-CxSyZmIE.d.mts} +87 -30
- package/dist/{run-agent-Dmf8GV2d.cjs → run-agent-cToZ3K42.cjs} +219 -86
- package/dist/{setup-agent-DV4cTou-.cjs → setup-agent-DTF3NQg3.cjs} +31 -20
- package/dist/setup-agent-DlbgwsIJ.d.cts +577 -0
- package/dist/{setup-agent-BuanDXeh.mjs → setup-agent-DniLvDu7.mjs} +15 -9
- package/dist/setup-agent-tsc3vm5I.d.mts +577 -0
- package/dist/tanstack-ai.cjs +627 -0
- package/dist/tanstack-ai.d.cts +204 -0
- package/dist/tanstack-ai.d.mts +204 -0
- package/dist/tanstack-ai.mjs +618 -0
- package/dist/testing.cjs +98 -985
- package/dist/testing.d.cts +51 -347
- package/dist/testing.d.mts +51 -347
- package/dist/testing.mjs +86 -973
- package/dist/{text-logic-IjfYy9DP.d.cts → text-logic-9fv1i9aK.d.cts} +50 -10
- package/dist/{text-logic-D-czVPYp.d.mts → text-logic-BDbGjYMh.d.mts} +50 -10
- package/dist/{types-BgNbNKnJ.d.mts → types-Dp5_Ru4R.d.cts} +2 -2
- package/dist/{types-hR5f5YXa.d.cts → types-Dp5_Ru4R.d.mts} +2 -2
- package/dist/usage-D8UphgEA.d.mts +17 -0
- package/dist/usage-R1NTmLxY.d.cts +17 -0
- package/dist/{utils-_q7F-AZu.d.mts → utils-BnXdWih3.d.mts} +1 -2
- package/dist/{utils-o4YRdHJX.d.cts → utils-ebVpCYoF.d.cts} +1 -2
- package/dist/validate.cjs +239 -8
- package/dist/validate.d.cts +110 -3
- package/dist/validate.d.mts +110 -3
- package/dist/validate.mjs +238 -7
- package/dist/verify-B5qGC5WN.mjs +1181 -0
- package/dist/verify-CBuYIpsa.d.mts +414 -0
- package/dist/verify-Cqghu4gz.cjs +1222 -0
- package/dist/verify-DYH_2VzU.d.cts +414 -0
- package/package.json +44 -31
- package/readme.md +38 -26
- package/skills/generate-machine/SKILL.md +140 -66
- package/skills/improve-machine/SKILL.md +190 -0
- package/skills/improve-machine/reference.md +96 -0
- package/dist/event-log-store-2pAFiWuc.mjs +0 -576
- package/dist/event-log-store-C-COhIXP.cjs +0 -702
- package/dist/usage-BdEjFuIi.mjs +0 -5
- package/dist/usage-efOQPdf_.cjs +0 -10
|
@@ -1,16 +1,38 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: generate-machine
|
|
3
|
-
description: Author a @statelyai/agent workflow as JSON and check it before it runs. Use when asked to generate or author an agent machine, create a workflow config, write an AgentWorkflowConfig, or produce an LLM-authored state machine for an agent — and when a generated config fails Ajv validation or agent lint and needs repair.
|
|
3
|
+
description: Author a @statelyai/agent workflow as JSON and check it before it runs. Use when asked to generate or author an agent machine, create a workflow config, write an AgentWorkflowConfig, or produce an LLM-authored state machine for an agent — and when a generated config fails verifyAgentConfig, Ajv validation, or agent lint and needs repair.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Generate an agent machine
|
|
7
7
|
|
|
8
|
-
An agent machine is data. You author a JSON `AgentWorkflowConfig`, then run it through gates
|
|
8
|
+
An agent machine is data. You author a JSON `AgentWorkflowConfig`, then run it through `verifyAgentConfig`, whose gates all work with no API key: validate → vocabulary → lower → lint → explore. Do not hand back a config that has not passed every gate.
|
|
9
9
|
|
|
10
10
|
```
|
|
11
|
-
|
|
11
|
+
split environment/policy → author (description = goal) → verifyAgentConfig → repair (≤ 3) → persist traces → hand back
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
To improve an existing policy from its traces, use the `improve-machine` skill instead.
|
|
15
|
+
|
|
16
|
+
## 0. Split environment and policy
|
|
17
|
+
|
|
18
|
+
Decide what is fixed before you write any JSON.
|
|
19
|
+
|
|
20
|
+
- **Environment** (TypeScript, fixed): the rules of the world and the business invariants, as states and guards. People author it and verify it with `canReach` / `explorePaths` from `@statelyai/agent/testing`. You never generate it, and no generated config may change it.
|
|
21
|
+
- **Policy** (JSON, generated): how the agent works toward the goal: which step next, which tool, when to ask a person, cheap judgment vs model call. This is the `AgentWorkflowConfig` you author.
|
|
22
|
+
- The policy plugs into the environment at its decision points and outputs one event; the environment's guards decide whether to accept it.
|
|
23
|
+
- No split needed for a single judgment inside a fixed workflow, or for a workflow where every step is dictated by rules. Author one machine.
|
|
24
|
+
|
|
25
|
+
If the request asks you to generate a business rule (an approval threshold, a payout limit), stop: that belongs in the environment, in TypeScript, reviewed by a person. Say so.
|
|
26
|
+
|
|
27
|
+
Before editing an existing machine, read it as text:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { describeMachine } from "@statelyai/agent";
|
|
31
|
+
const text = describeMachine(machine); // "Machine <id>", then "Goal: …", states, transitions, events
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Pass `{ snapshot }` to mark the active states with `*`. Concept page: `docs/environment-and-policy.md`.
|
|
35
|
+
|
|
14
36
|
## 1. Read the schema
|
|
15
37
|
|
|
16
38
|
The config shape is fixed by a shipped JSON Schema. Read it first; it is the contract.
|
|
@@ -20,23 +42,32 @@ The config shape is fixed by a shipped JSON Schema. Read it first; it is the con
|
|
|
20
42
|
|
|
21
43
|
## 2. Author the config
|
|
22
44
|
|
|
45
|
+
Start with the root `description`: it is the machine's **goal**, the overall objective plus measurable success criteria. `describeMachine` renders it first as `Goal: …`, and an improver later climbs toward it. Write criteria a scorer can compute from outcomes (terminal states, effects in the world, cost), not "be helpful":
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
"description": "Resolve each support ticket in one session. Success: escalations only for tickets that need a human; every reply approved before sending; at most 2 model calls per ticket."
|
|
49
|
+
```
|
|
50
|
+
|
|
23
51
|
Rules the runtime enforces. Each one maps to a build error or a lint diagnostic:
|
|
24
52
|
|
|
25
53
|
- Every value is a JSON literal or a whole-string `"{{ }}"` expression reading a dot path on `input`, `context`, or `event`. No JavaScript.
|
|
26
54
|
- Guard expressions are truthy-only: no comparisons, operators, or negation.
|
|
27
|
-
- A model call is either a named `requests` entry invoked with `src: "<requestName>"`, or an inline invoke with `src: "agent.decide"` plus an `allowedEvents` list.
|
|
55
|
+
- A model call is either a named `requests` entry invoked with `src: "<requestName>"`, or an inline invoke with `src: "agent.decide"` plus an `allowedEvents` list. A `requests` entry's templates read only its own `input` (`"{{ input.ticket }}"`); the invoke's `input` maps context into it.
|
|
28
56
|
- A state invoking `agent.decide` MUST handle every allowed event in its `on`.
|
|
29
57
|
- An `agent.decide` invoke has no `onDone` (a decision produces no output). Use `onError` for the retries-exhausted path.
|
|
30
58
|
- A request invoke resolves with `{ result, messages }`, so `onDone.assign` reads its result from `"{{ event.output.result.<field> }}"` (`"{{ event.output.result }}"` for a plain-text request). A non-request `actors` invoke reads `"{{ event.output.<field> }}"` directly.
|
|
31
59
|
- Every path must reach a `"type": "final"` state, and each final state needs an `output` when `schemas.output` is declared.
|
|
32
60
|
- Do not invent guard or action names. Only `"{{ }}"` guards, `assign`, and `emit` exist unless the host tells you which named guards/actions it implements.
|
|
33
61
|
- Model refs are strings (`"openai/gpt-5.4-mini"`); the host resolves them.
|
|
62
|
+
- To make a decision a closed choice, add `payloads` to the `agent.decide` input: one payload list per event type, without `type` (`"payloads": { "REFUND": [{ "amount": 20 }, { "amount": 50 }], "ESCALATE": [] }`). The model then picks one listed event instead of writing a payload. A type with no key is offered as `{ type }`; `[]` withholds it.
|
|
63
|
+
- An idle human step can carry `meta.interaction` for the host to render with `getInteraction`: a `label`, `events` (one button per event type), `choices` (one button per full event, `{ "label": "Refund $20", "event": { "type": "REFUND", "amount": 20 } }`, or a context path string holding such a list), and `textEvent` for free text. A bare `"textEvent": "REJECT"` puts the text in a `text` field; `{ "type": "REJECT", "field": "reason" }` names the field. `meta` is not template-evaluated.
|
|
34
64
|
|
|
35
65
|
Reference config — decision, text request, idle human step, one final state:
|
|
36
66
|
|
|
37
67
|
```json
|
|
38
68
|
{
|
|
39
69
|
"id": "support-ticket",
|
|
70
|
+
"description": "Resolve each support ticket in one session. Success: escalations only for tickets that need a human; every reply approved before sending; at most 2 model calls per ticket.",
|
|
40
71
|
"schemas": {
|
|
41
72
|
"input": {
|
|
42
73
|
"type": "object",
|
|
@@ -73,7 +104,7 @@ Reference config — decision, text request, idle human step, one final state:
|
|
|
73
104
|
"draftReply": {
|
|
74
105
|
"model": "openai/gpt-5.4-mini",
|
|
75
106
|
"system": "Draft a short, courteous support reply.",
|
|
76
|
-
"prompt": "{{
|
|
107
|
+
"prompt": "{{ input.ticket }}",
|
|
77
108
|
"input": {
|
|
78
109
|
"type": "object",
|
|
79
110
|
"properties": { "ticket": { "type": "string" } },
|
|
@@ -132,36 +163,27 @@ Reference config — decision, text request, idle human step, one final state:
|
|
|
132
163
|
}
|
|
133
164
|
```
|
|
134
165
|
|
|
135
|
-
## 3.
|
|
136
|
-
|
|
137
|
-
`validateAgentConfig` from `@statelyai/agent/validate` checks a candidate against the shipped workflow schema with Ajv 2020 built in. It needs `ajv` installed (an optional peer of the package). Run it before anything else touches the config.
|
|
138
|
-
|
|
139
|
-
```ts
|
|
140
|
-
import { validateAgentConfig } from "@statelyai/agent/validate";
|
|
141
|
-
import type { AgentWorkflowConfig } from "@statelyai/agent";
|
|
142
|
-
|
|
143
|
-
function validateGeneratedConfig(candidate: unknown): AgentWorkflowConfig {
|
|
144
|
-
const result = validateAgentConfig(candidate);
|
|
145
|
-
if (result.valid) return candidate as AgentWorkflowConfig;
|
|
146
|
-
throw new Error(
|
|
147
|
-
result.errors.map((error) => `${error.path || "(root)"} ${error.message}`).join("\n"),
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
```
|
|
166
|
+
## 3. Verify with `verifyAgentConfig`
|
|
151
167
|
|
|
152
|
-
|
|
168
|
+
`verifyAgentConfig(config, options)` from `@statelyai/agent/validate` runs every pre-run gate and stops at the first failure. It never throws on a bad config; each gate is a record `{ name, ok, detail? }`. It needs `ajv` installed (an optional peer of the package).
|
|
153
169
|
|
|
154
|
-
|
|
170
|
+
| Gate | Checks with | Fails on |
|
|
171
|
+
| ------------ | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
172
|
+
| `validate` | `validateAgentConfig` (shipped schema) | malformed config shape |
|
|
173
|
+
| `vocabulary` | `allow` lists | an invoke src, named guard, named action, `agent.decide` event, or model ref not listed |
|
|
174
|
+
| `lower` | `setupAgent.fromConfig` | unresolved named guard/action, `onDone` on an `agent.decide` invoke |
|
|
175
|
+
| `lint` | `lintAgentMachine` | error findings (warnings too with `lint: { warnings: true }`) |
|
|
176
|
+
| `explore` | `explorePaths`, once per input | a path ending outside `explore.terminals` (default `['done']`), a loop past `maxDepth`, a `maxPaths`/`maxNodes` cap, untried payloads (`unexpandedEvents`) |
|
|
155
177
|
|
|
156
|
-
`
|
|
178
|
+
`compileSchema` turns the JSON Schemas _inside_ the config into Standard Schema validators for `fromConfig`. The library bundles no JSON Schema engine:
|
|
157
179
|
|
|
158
180
|
```ts
|
|
159
181
|
import Ajv from "ajv";
|
|
160
|
-
import {
|
|
182
|
+
import type { SchemaCompiler, StandardSchemaV1 } from "@statelyai/agent";
|
|
161
183
|
|
|
162
184
|
const ajv = new Ajv({ strict: false });
|
|
163
185
|
|
|
164
|
-
export const
|
|
186
|
+
export const compileSchema: SchemaCompiler = (jsonSchema, name): StandardSchemaV1 => {
|
|
165
187
|
const validate = ajv.compile(jsonSchema);
|
|
166
188
|
return {
|
|
167
189
|
"~standard": {
|
|
@@ -175,87 +197,139 @@ export const ajvCompileSchema: SchemaCompiler = (jsonSchema, name): StandardSche
|
|
|
175
197
|
message: `${name}${e.instancePath} ${e.message}`,
|
|
176
198
|
})),
|
|
177
199
|
},
|
|
178
|
-
// Expose the source JSON Schema so lint
|
|
200
|
+
// Expose the source JSON Schema so lint and describeMachine can read the shape.
|
|
179
201
|
jsonSchema: { input: () => jsonSchema },
|
|
180
202
|
},
|
|
181
203
|
};
|
|
182
204
|
};
|
|
205
|
+
```
|
|
183
206
|
|
|
184
|
-
|
|
207
|
+
Run the gates. List the host's real names in `allow`; an omitted key is unrestricted:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
import { verifyAgentConfig } from "@statelyai/agent/validate";
|
|
211
|
+
|
|
212
|
+
const result = await verifyAgentConfig(candidate, {
|
|
213
|
+
compileSchema,
|
|
214
|
+
allow: {
|
|
215
|
+
actors: [],
|
|
216
|
+
requests: ["draftReply"],
|
|
217
|
+
guards: [],
|
|
218
|
+
actions: [],
|
|
219
|
+
events: ["ESCALATE", "REPLY", "APPROVE", "REJECT"],
|
|
220
|
+
models: ["openai/gpt-5.4-mini"],
|
|
221
|
+
},
|
|
222
|
+
explore: {
|
|
223
|
+
inputs: [{ ticket: "Export downloads a 0-byte CSV on Safari." }],
|
|
224
|
+
outputs: { draftReply: [{ reply: "" }] },
|
|
225
|
+
// ESCALATE's `reason` is a free-form string: give exploration one to try.
|
|
226
|
+
payloads: () => ({ ESCALATE: [{ reason: "needs a human" }] }),
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
// result.ok, result.gates, result.config (after validate), result.machine (after lower)
|
|
185
230
|
```
|
|
186
231
|
|
|
187
|
-
|
|
232
|
+
- Exploration forks past idle waits on every event the waiting state accepts, so a human step still ends `done`. Add `terminals: ["done", "idle"]` only when a path may legitimately stop at a wait with nothing to accept.
|
|
233
|
+
- `guards` / `actions`: named implementations, passed to `fromConfig`. `actors`: bound with `machine.provide({ actors })` after lowering.
|
|
234
|
+
- `explore` keys other than `inputs` and `terminals` pass through to `explorePaths`: `outputs` (output lists per request or actor src: one element is a canned output, two or more fork one branch each), `errors` (fork a failing branch per src), `payloads` (fork one branch per payload at decisions and idle waits), `dedupeBy` (explore each snapshot key once; the key must cover every field a guard reads), `maxDepth` (default 8), `maxPaths` (default 200), `maxNodes` (default 100,000).
|
|
235
|
+
- Stub each request's output from its declared `output` schema (`""` for string, `0` for number, `[]` for array, recurse on `properties`). Add an `errors` entry for every src whose `onError` path matters.
|
|
236
|
+
- Pick `explore.inputs` that reach every branch the goal cares about. Exploration covers only the inputs you give it.
|
|
237
|
+
- A passing `lint` gate carries its warnings in `detail`. Report them.
|
|
188
238
|
|
|
189
|
-
##
|
|
239
|
+
## 4. Repair loop
|
|
240
|
+
|
|
241
|
+
A failed gate's `detail` names the offending path, state, or name, one problem per line. Feed it back verbatim with the rejected config, and run the gates again:
|
|
190
242
|
|
|
191
243
|
```ts
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
244
|
+
let prompt = originalTask;
|
|
245
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
246
|
+
const candidate = await author({ system, prompt });
|
|
247
|
+
const result = await verifyAgentConfig(candidate, verifyOptions);
|
|
248
|
+
if (result.ok) return result;
|
|
249
|
+
const failed = result.gates.find((gate) => !gate.ok)!;
|
|
250
|
+
prompt = `${originalTask}\n\nYour previous config:\n${JSON.stringify(candidate)}\n\nGate '${failed.name}' failed:\n${failed.detail}\nReturn a corrected config.`;
|
|
251
|
+
}
|
|
195
252
|
```
|
|
196
253
|
|
|
197
|
-
|
|
254
|
+
Keep the cap at 3. A config that fails three repairs is usually asking for something the data form cannot express: author it in TypeScript with `setupAgent` instead, or report that the vocabulary is missing something.
|
|
198
255
|
|
|
199
|
-
|
|
256
|
+
## 5. Manual fallback
|
|
200
257
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
Lint is structural. A dry run proves a path actually settles. No API key needed.
|
|
258
|
+
Without `verifyAgentConfig` (older package, or a gate you need to run alone), run the same gates by hand:
|
|
204
259
|
|
|
205
260
|
```ts
|
|
206
|
-
import {
|
|
261
|
+
import { setupAgent, type AgentWorkflowConfig } from "@statelyai/agent";
|
|
262
|
+
import { validateAgentConfig } from "@statelyai/agent/validate";
|
|
263
|
+
import { explorePaths, lintAgentMachine, simulateAgent } from "@statelyai/agent/testing";
|
|
264
|
+
|
|
265
|
+
const validation = validateAgentConfig(candidate); // { valid, errors: [{ path, message }] }, path is a JSON Pointer
|
|
266
|
+
if (!validation.valid) {
|
|
267
|
+
// Stop here: feed the errors back as the repair prompt. Never lower an invalid config.
|
|
268
|
+
throw new Error(validation.errors.map((e) => `${e.path} ${e.message}`).join("\n"));
|
|
269
|
+
}
|
|
270
|
+
const { machine } = setupAgent.fromConfig(candidate as AgentWorkflowConfig, { compileSchema }); // throws on unresolved names
|
|
271
|
+
lintAgentMachine(machine, { throw: true }); // throws AgentLintError on error findings
|
|
207
272
|
const dryRun = await simulateAgent(machine, {
|
|
208
273
|
input: { ticket: "" },
|
|
209
274
|
script: {
|
|
210
275
|
decisions: { "agent.decide": [{ type: "REPLY" }] },
|
|
211
276
|
text: { draftReply: [{ reply: "" }] },
|
|
212
277
|
},
|
|
213
|
-
});
|
|
214
|
-
|
|
278
|
+
}); // dryRun.status: 'done' | 'idle' | 'exhausted' ('exhausted' = loops, reject)
|
|
279
|
+
const report = await explorePaths(machine, {
|
|
280
|
+
input: { ticket: "" },
|
|
281
|
+
outputs: { draftReply: [{ reply: "" }] },
|
|
282
|
+
payloads: () => ({ ESCALATE: [{ reason: "needs a human" }] }),
|
|
283
|
+
}); // report.unexpandedEvents lists types that still need a payloads entry
|
|
215
284
|
```
|
|
216
285
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
- `'exhausted'` → the machine loops. Reject it.
|
|
220
|
-
- `'idle'` → it stopped at a human step. Expected when the config has one.
|
|
221
|
-
- To cover every branch instead of one path, use `explorePaths` / `canReach` (its result is `{ reachable, witness }`).
|
|
286
|
+
The lint check that bites most often is `decide-without-events`: an `allowedEvents` list with no matching `on` transitions. Do not disable checks.
|
|
222
287
|
|
|
223
|
-
##
|
|
288
|
+
## 6. Persist traces from day one
|
|
224
289
|
|
|
225
|
-
|
|
290
|
+
An improver needs data. Wire an event log store and a thread id per run before the first real run, so every run is a trace:
|
|
226
291
|
|
|
227
292
|
```ts
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
293
|
+
import { runAgent } from "@statelyai/agent";
|
|
294
|
+
import { createInMemoryEventLogStore } from "@statelyai/agent/log";
|
|
295
|
+
|
|
296
|
+
const store = createInMemoryEventLogStore(); // tests only; implement AgentEventLogStore on your database
|
|
297
|
+
const result = await runAgent(machine, {
|
|
298
|
+
input,
|
|
299
|
+
executors,
|
|
300
|
+
store,
|
|
301
|
+
threadId: `tickets:${ticketId}`,
|
|
302
|
+
});
|
|
235
303
|
```
|
|
236
304
|
|
|
237
|
-
|
|
305
|
+
- `threadId` is required with `store`. One thread per run.
|
|
306
|
+
- Give thread ids a prefix an improver can list by, such as `tickets:<ticketId>`. `store.listThreads({ prefix, limit, cursor })` pages through them. It is optional on a custom `AgentEventLogStore`, so with a store that lacks it, record each thread id with its scenario or ticket.
|
|
307
|
+
- Failed decision attempts are journaled on the entry they explain; `getDecisionAttempts(entries)` from `@statelyai/agent/log` reads them back.
|
|
308
|
+
- `replay(machine, await store.read(threadId))` from `@statelyai/agent/log` rebuilds the final state with no model calls. Pass the machine that recorded the thread: with an environment and a policy, the environment; `getUsageFromEvents(entries)` sums token usage.
|
|
238
309
|
|
|
239
|
-
##
|
|
310
|
+
## 7. Hand back
|
|
240
311
|
|
|
241
312
|
Deliver the config JSON plus how to run it:
|
|
242
313
|
|
|
243
314
|
```ts
|
|
244
|
-
import {
|
|
315
|
+
import { runAgent } from "@statelyai/agent";
|
|
245
316
|
import { createAiSdkExecutors } from "@statelyai/agent/ai-sdk";
|
|
246
317
|
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
318
|
+
const run = await runAgent(result.machine!, {
|
|
319
|
+
input: { ticket: "Export downloads a 0-byte CSV on Safari." },
|
|
320
|
+
executors: createAiSdkExecutors({ resolveModel }),
|
|
321
|
+
store,
|
|
322
|
+
threadId,
|
|
323
|
+
});
|
|
251
324
|
```
|
|
252
325
|
|
|
253
|
-
Say which gates passed, list any
|
|
326
|
+
Say which gates passed (list `result.gates`), list any lint warnings, state the goal and its success criteria, and say where traces are written. Model refs are strings, so the host supplies `resolveModel`.
|
|
254
327
|
|
|
255
328
|
## Known limits
|
|
256
329
|
|
|
257
330
|
- **Named guards and actions are host-resolved.** A config carries no functions. `guard: "isReady"` only works if the host passes an `isReady` implementation to `fromConfig`; an unresolved name is a build-time throw. Either list the host's names in the prompt or forbid named guards/actions entirely.
|
|
258
331
|
- **Expressions are dot-path templates only.** `"{{ context.a.b }}"` — no comparisons, no arithmetic, no method calls, no partial interpolation inside a larger string.
|
|
259
|
-
- **Ajv validity is not semantic validity.** A config can validate and still invoke a request that does not exist or route to a state that solves nothing. That is what lint and
|
|
260
|
-
- **
|
|
261
|
-
- **Model refs
|
|
332
|
+
- **Ajv validity is not semantic validity.** A config can validate and still invoke a request that does not exist or route to a state that solves nothing. That is what the `vocabulary`, `lint`, and `explore` gates are for.
|
|
333
|
+
- **Exploration covers the inputs you give it.** A branch no `explore.inputs` entry reaches is unverified.
|
|
334
|
+
- **Model refs are checked only with `allow.models`.** Tool names stay unchecked strings that resolve at run time, in the host.
|
|
335
|
+
- **The gates check structure, not quality.** Whether a config is better than another is measured by replaying scenarios against an objective. See the `improve-machine` skill.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: improve-machine
|
|
3
|
+
description: Improve a @statelyai/agent policy machine (a JSON AgentWorkflowConfig) from its run traces toward its goal, inside a fixed environment machine, with gates and a held-out promotion rule. Use when asked to "improve this agent", "self-improve", "optimize the playbook from traces", "tune the policy", "make the agent cheaper or more accurate", or "why is my agent failing" for a machine whose runs are persisted to an event log store.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Improve an agent machine
|
|
7
|
+
|
|
8
|
+
You improve a **policy**: the JSON `AgentWorkflowConfig` that decides how the agent works toward its goal. It runs inside an **environment**: a TypeScript machine whose states and guards encode the rules of the world and the business invariants. One round changes the policy once and measures it:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
locate inputs → read traces → diagnose → propose one diff → verifyAgentConfig (≤ 3 repairs) → replay train → replay held-out → promote or discard → record
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
To author a policy from scratch, use the `generate-machine` skill. Concept page: `docs/environment-and-policy.md`.
|
|
15
|
+
|
|
16
|
+
## Hard rules
|
|
17
|
+
|
|
18
|
+
- **Never edit the environment.** Not its states, guards, invariants, event schemas, or the vocabulary it exposes. When the goal is unreachable without an environment change, write a change request for a person (template in [reference.md](reference.md)) and stop.
|
|
19
|
+
- **Never edit a machine while it runs.** A promoted policy applies to new runs only.
|
|
20
|
+
- **The objective comes from ground truth.** Score outcomes from the traces (terminal states, outputs) or from effects recorded in the world. Never ask a model to grade its own runs.
|
|
21
|
+
- **One cause per round, smallest diff.** A diff that fixes two things cannot tell you which one helped.
|
|
22
|
+
- **Diagnose from training scenarios only.** Held-out scenarios are for the promotion decision. Never read their failures to shape a change, and never move a scenario between splits.
|
|
23
|
+
- **Stop after 3 rounds without a promotion** (or the limit the user gives).
|
|
24
|
+
|
|
25
|
+
## 1. Locate the inputs
|
|
26
|
+
|
|
27
|
+
Find all six before changing anything. If one is missing, stop and ask.
|
|
28
|
+
|
|
29
|
+
| Input | Where to look | If missing |
|
|
30
|
+
| -------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- |
|
|
31
|
+
| Goal | Root `description` of the policy config or the environment machine | Draft one with measurable success criteria; ask the user to confirm it |
|
|
32
|
+
| Objective | A scorer over outcomes (an eval file, a `score*` function, a benchmark) | Write one from the goal's success criteria; **ask the user to confirm it** |
|
|
33
|
+
| Traces | The `store` passed to `createAgentRuntime` / `runAgent`; its `listThreads`, or recorded ids | Wire a store first (see `generate-machine` step 6) and collect runs |
|
|
34
|
+
| Scenarios | A scenario list with expected outcomes, split into train and held-out | Propose a fixed split (about 70/30, every category in both); ask to confirm |
|
|
35
|
+
| Vocabulary | The actors, requests, named guards, actions, decision events, and model refs the host implements | Read them off the host code; never invent names |
|
|
36
|
+
| Policy history | The policy files and a `CHANGELOG.md` next to them | Create the changelog with the current policy as `v0` |
|
|
37
|
+
|
|
38
|
+
The objective defines "better". Do not start a round until the user has confirmed it. A good objective has:
|
|
39
|
+
|
|
40
|
+
- a primary metric from the goal (accuracy against expected outcomes, resolution rate),
|
|
41
|
+
- a hard constraint: invariant violations, which must be 0,
|
|
42
|
+
- a cost term (model calls, tokens, guard rejections) used as a tie-breaker or a stated budget.
|
|
43
|
+
|
|
44
|
+
## 2. Read
|
|
45
|
+
|
|
46
|
+
Read both machines as text. `describeMachine` puts the goal first:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { describeMachine, setupAgent } from "@statelyai/agent";
|
|
50
|
+
|
|
51
|
+
const { machine: policy } = setupAgent.fromConfig(policyConfig, { compileSchema });
|
|
52
|
+
const environmentText = describeMachine(environment);
|
|
53
|
+
const policyText = describeMachine(policy);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Then find the training threads and compute one outcome row per episode:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { getDecisionAttempts, getUsageFromEvents, replay } from "@statelyai/agent/log";
|
|
60
|
+
|
|
61
|
+
// Thread ids name the split and policy version: "train:v<n>:<scenario>:r<k>".
|
|
62
|
+
const prefix = `train:v${currentVersion}:`;
|
|
63
|
+
const threadIds: string[] = [];
|
|
64
|
+
let cursor: string | undefined;
|
|
65
|
+
do {
|
|
66
|
+
const page = await store.listThreads!({ prefix, limit: 100, cursor });
|
|
67
|
+
threadIds.push(...page.threadIds);
|
|
68
|
+
cursor = page.cursor;
|
|
69
|
+
} while (cursor !== undefined);
|
|
70
|
+
|
|
71
|
+
const rows = [];
|
|
72
|
+
for (const threadId of threadIds) {
|
|
73
|
+
const scenario = scenarioOf(threadId);
|
|
74
|
+
const entries = await store.read(threadId);
|
|
75
|
+
const { snapshot } = replay(environment, entries); // the recording machine; pure fold, no model calls
|
|
76
|
+
rows.push({
|
|
77
|
+
threadId,
|
|
78
|
+
expected: scenario.expected,
|
|
79
|
+
state: snapshot.value,
|
|
80
|
+
status: snapshot.status,
|
|
81
|
+
output: snapshot.output,
|
|
82
|
+
steps: entries.length,
|
|
83
|
+
tokens: getUsageFromEvents(entries).totalTokens ?? 0,
|
|
84
|
+
failedAttempts: getDecisionAttempts(entries).flatMap(({ attempts }) => attempts),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- `store.listThreads({ prefix, limit, cursor })` pages through the threads that hold at least one entry. `cursor` is present only while more ids remain. It is optional on a custom `AgentEventLogStore`; `createInMemoryEventLogStore` implements it. Without it, use the thread ids recorded with each scenario.
|
|
90
|
+
- Prefix thread ids with the split and version so a `listThreads` prefix returns one version's training runs and never a held-out thread. `scenarioOf` is your mapping from a thread id back to its scenario.
|
|
91
|
+
- `store.read(threadId)` returns a thread's entries in log order.
|
|
92
|
+
- `replay(machine, entries)` needs the machine, at the version, that recorded the thread. Here that is the environment: its runtime records every thread, and the policy runs only as a decider at its boundary, so never replay with the policy machine. A thread recorded by a different environment version throws `AgentMachineVersionMismatchError`. Score each thread with the version that recorded it.
|
|
93
|
+
- `getDecisionAttempts(entries)` returns `{ index, eventType, attempts }` for each entry whose decision needed retries: the failed tries before the event landed, or every try of an exhausted decision (`eventType` is then `xstate.error.actor.*`). Each attempt has `failure` (`unknown-event`, `invalid-payload`, or `rejected-by-guard`), `reason`, and the attempted `event` when there was one. A first-try decision records nothing.
|
|
94
|
+
- `agent.decide` invokes journal their attempts on their own. With `decideNext` at the boundary, the host must pass them with the event, `runToQuiescence(runtime, { event, attempts })` or `runtime.transition(state, event, { attempts })`, or they never reach the log.
|
|
95
|
+
- Compute the objective over the rows. This is the baseline for the round.
|
|
96
|
+
|
|
97
|
+
## 3. Diagnose
|
|
98
|
+
|
|
99
|
+
Group the failing training episodes by cause. Check these clusters in order:
|
|
100
|
+
|
|
101
|
+
1. **Invariant violations.** Any violation is a stop: the environment is wrong, not the policy. Write a change request.
|
|
102
|
+
2. **Wrong terminal state.** The episode ended in `escalated` when the label says `issued`, or the reverse. Look at which decision led there.
|
|
103
|
+
3. **Guard rejections.** The policy keeps proposing events the environment refuses: `rejected-by-guard` entries in the rows' `failedAttempts`. The policy lacks a check the environment already makes.
|
|
104
|
+
4. **Loops and budget exhaustion.** Repeated state values, or a terminal reached because a turn or retry budget ran out.
|
|
105
|
+
5. **Cost hotspots.** Correct episodes that spend model calls where a `choice` over known facts, or a cheap judgment, would decide.
|
|
106
|
+
|
|
107
|
+
Pick the cluster with the largest effect on the objective. Write the hypothesis down before you edit anything (template in [reference.md](reference.md)): cause, evidence (thread ids), the change, and which metric should move.
|
|
108
|
+
|
|
109
|
+
## 4. Propose
|
|
110
|
+
|
|
111
|
+
Make the smallest policy diff that addresses the one cause.
|
|
112
|
+
|
|
113
|
+
- Typical diffs: add a `choice` branch over facts already in context; replace a model call with a judgment actor from the vocabulary; narrow an `agent.decide` `allowedEvents` list; turn an open decision into a closed choice with `payloads`; tighten a prompt; add an `onError` path; lower a model ref to a cheaper one in the vocabulary.
|
|
114
|
+
- Use only names in the vocabulary. A named guard the host does not implement fails the `lower` gate.
|
|
115
|
+
- Keep the config `id`. Bump `version`.
|
|
116
|
+
- Keep the root `description` (the goal) unchanged. Changing the goal is the user's decision.
|
|
117
|
+
|
|
118
|
+
## 5. Gate
|
|
119
|
+
|
|
120
|
+
Run `verifyAgentConfig` from `@statelyai/agent/validate` with the vocabulary and probe inputs. Probe inputs are one input per training scenario category, chosen to reach every branch the diff touches.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { verifyAgentConfig } from "@statelyai/agent/validate";
|
|
124
|
+
|
|
125
|
+
const verified = await verifyAgentConfig(candidate, {
|
|
126
|
+
compileSchema,
|
|
127
|
+
guards: hostGuards,
|
|
128
|
+
actors: hostActors,
|
|
129
|
+
allow: { actors, requests, guards, actions, events, models },
|
|
130
|
+
explore: {
|
|
131
|
+
inputs: probeInputs,
|
|
132
|
+
terminals: ["done"],
|
|
133
|
+
// Both outcomes, so the branches on either side of the classifier are probed.
|
|
134
|
+
outputs: {
|
|
135
|
+
classifyTicket: [
|
|
136
|
+
{ isRefundRequest: true, confidence: 0.9 },
|
|
137
|
+
{ isRefundRequest: false, confidence: 0.9 },
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
errors: { classifyTicket: new Error("down") },
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
const failed = verified.gates.find((gate) => !gate.ok);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- Gates run in order and stop at the first failure: `validate`, `vocabulary`, `lower`, `lint`, `explore`.
|
|
147
|
+
- A failed gate's `detail` names the offending path, state, or name. Send it back verbatim with the rejected config and regenerate. Cap repairs at 3. After 3, discard the candidate, record it, and move to the next cause.
|
|
148
|
+
- `explore` keys besides `inputs` and `terminals` pass through to `explorePaths`: `outputs` (output lists per src: one element is a canned output, two or more fork one branch each), `errors`, `payloads` (one branch per payload), `dedupeBy` (explore each snapshot key once; the key must cover every field a guard reads), `maxDepth`, `maxPaths`.
|
|
149
|
+
- The environment's own `canReach` proofs cover every policy, because the environment's guards judge every proposed event. You did not change the environment, so you do not re-prove it.
|
|
150
|
+
|
|
151
|
+
## 6. Replay
|
|
152
|
+
|
|
153
|
+
Run the candidate on the training scenarios, then on the held-out scenarios. Write every replay to the store under new thread ids that name the split and version (`train:v3:<scenario>:r1`, `heldout:v3:<scenario>:r1`), so the next round can list them by prefix.
|
|
154
|
+
|
|
155
|
+
- Run the current policy and the candidate on the same scenarios, the same world state, and the same model refs.
|
|
156
|
+
- Model calls are nondeterministic. Run each scenario k times (k ≥ 3 when calls are not cached) and compare means.
|
|
157
|
+
- Compute the objective, invariant violations, and cost per split, for both versions.
|
|
158
|
+
|
|
159
|
+
## 7. Promote or discard
|
|
160
|
+
|
|
161
|
+
State the tolerance before you replay. Promote only when every condition holds:
|
|
162
|
+
|
|
163
|
+
- invariant violations are 0 on both splits,
|
|
164
|
+
- the objective on the held-out split is better than the current policy's,
|
|
165
|
+
- no single scenario regresses beyond the tolerance (for example, a scenario that passed in all k runs now fails in any),
|
|
166
|
+
- cost stays within the stated budget, if there is one.
|
|
167
|
+
|
|
168
|
+
Otherwise discard the candidate and keep the current policy. Either way, append an entry to the `CHANGELOG.md` next to the policies: version, hypothesis, diff summary, gate results, before and after per split, and the decision. Record discarded candidates too; they stop the next round from retrying the same idea.
|
|
169
|
+
|
|
170
|
+
## 8. Stop and report
|
|
171
|
+
|
|
172
|
+
Stop when any of these holds:
|
|
173
|
+
|
|
174
|
+
- 3 rounds in a row without a promotion,
|
|
175
|
+
- the goal's success criteria are met on the held-out split,
|
|
176
|
+
- the remaining failures need an environment change (write a change request),
|
|
177
|
+
- the vocabulary lacks a capability the fix needs (ask for it; name the actor, request, or guard and its input and output).
|
|
178
|
+
|
|
179
|
+
Report to the user: rounds run, each candidate with its hypothesis and decision, the objective before and after on both splits, violations, cost, and any change requests.
|
|
180
|
+
|
|
181
|
+
## Worked example
|
|
182
|
+
|
|
183
|
+
`examples/self-improving-refunds` in the `statelyai/agent` repo runs this loop end to end:
|
|
184
|
+
|
|
185
|
+
- the refund desk is the environment: business rules as guards, and an `illegallyIssued` sentinel proven unreachable,
|
|
186
|
+
- `playbooks/*.json` are policy versions, with the vocabulary listed in `playbooks/README.md`,
|
|
187
|
+
- `improve.ts` is the loop: propose, gate, replay, promote,
|
|
188
|
+
- `benchmark.ts` compares a raw tool loop, the desk with a raw model deciding, and the desk with a playbook deciding.
|
|
189
|
+
|
|
190
|
+
Read the directory before you start. File contents change between releases.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Improve-machine templates
|
|
2
|
+
|
|
3
|
+
Templates for the `improve-machine` skill: a round checklist, the hypothesis, the proposer prompt, the changelog entry, and the environment change request.
|
|
4
|
+
|
|
5
|
+
## Round checklist
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
[ ] Goal confirmed (root description, measurable success criteria)
|
|
9
|
+
[ ] Objective confirmed by the user (primary metric, violations = 0, cost term)
|
|
10
|
+
[ ] Train / held-out split fixed; tolerance stated
|
|
11
|
+
[ ] Vocabulary listed (actors, requests, guards, actions, events, models)
|
|
12
|
+
[ ] Baseline: objective, violations, cost per split for the current version
|
|
13
|
+
[ ] Failures clustered (train only); one cause picked
|
|
14
|
+
[ ] Hypothesis written
|
|
15
|
+
[ ] Candidate: smallest diff, id kept, version bumped, description unchanged
|
|
16
|
+
[ ] verifyAgentConfig passed (repairs used: _/3)
|
|
17
|
+
[ ] Replayed train, then held-out, k runs each, new thread ids
|
|
18
|
+
[ ] Promotion rule applied; CHANGELOG entry written
|
|
19
|
+
[ ] Stop condition checked
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Hypothesis
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
Cause: <one sentence, the mechanism, not the symptom>
|
|
26
|
+
Evidence: <thread ids and what each shows>
|
|
27
|
+
Change: <the one diff, in words>
|
|
28
|
+
Expected effect: <metric> moves from <x> to about <y>; nothing else changes
|
|
29
|
+
Risk: <scenarios that could regress, and why>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Proposer prompt
|
|
33
|
+
|
|
34
|
+
Use this when a model proposes the candidate. Fill every slot from real data.
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
You improve a policy machine. You may change only the policy config below.
|
|
38
|
+
|
|
39
|
+
{describeMachine(environment)}
|
|
40
|
+
|
|
41
|
+
Current policy (version {n}):
|
|
42
|
+
{JSON.stringify(policyConfig, null, 2)}
|
|
43
|
+
|
|
44
|
+
Vocabulary (use no other names):
|
|
45
|
+
- actors: {actors}
|
|
46
|
+
- requests: {requests}
|
|
47
|
+
- named guards: {guards}
|
|
48
|
+
- named actions: {actions}
|
|
49
|
+
- decision events: {events}
|
|
50
|
+
- model refs: {models}
|
|
51
|
+
|
|
52
|
+
Objective on the training scenarios: {objective summary}
|
|
53
|
+
Diagnosed cause: {hypothesis.cause}
|
|
54
|
+
Evidence: {failing rows: scenario, expected, reached state, rejections, tokens}
|
|
55
|
+
|
|
56
|
+
Return one complete AgentWorkflowConfig as JSON. Make the smallest change that
|
|
57
|
+
addresses the diagnosed cause. Keep "id" and "description". Set "version" to {n + 1}.
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
On a failed gate, append:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Your previous config was rejected.
|
|
64
|
+
Gate '{gate.name}' failed:
|
|
65
|
+
{gate.detail}
|
|
66
|
+
Return a corrected config.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Changelog entry
|
|
70
|
+
|
|
71
|
+
```md
|
|
72
|
+
## v{n} — {promoted | discarded}
|
|
73
|
+
|
|
74
|
+
- Hypothesis: {cause} → {change}
|
|
75
|
+
- Diff: {states or transitions touched}
|
|
76
|
+
- Gates: validate ✓ vocabulary ✓ lower ✓ lint ✓ explore ✓ (repairs: {r})
|
|
77
|
+
- Train: objective {a} → {b}, violations {v}, cost {c0} → {c1}
|
|
78
|
+
- Held-out: objective {a} → {b}, violations {v}, cost {c0} → {c1}
|
|
79
|
+
- Regressions: {none | scenario ids}
|
|
80
|
+
- Decision: {why}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Environment change request
|
|
84
|
+
|
|
85
|
+
Write this instead of editing the environment.
|
|
86
|
+
|
|
87
|
+
```md
|
|
88
|
+
# Environment change request: {short title}
|
|
89
|
+
|
|
90
|
+
- Goal criterion blocked: {the success criterion no policy can meet}
|
|
91
|
+
- Evidence: {thread ids; the state and guard that block it}
|
|
92
|
+
- Policies tried: {versions and why each could not get there}
|
|
93
|
+
- Proposed change: {the state, guard, or event to add or change, in words}
|
|
94
|
+
- Invariants affected: {which, and the canReach check to rerun}
|
|
95
|
+
- Owner: a person reviews and merges this; the improver does not.
|
|
96
|
+
```
|