@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.
Files changed (75) hide show
  1. package/dist/ai-sdk.cjs +149 -26
  2. package/dist/ai-sdk.d.cts +57 -34
  3. package/dist/ai-sdk.d.mts +57 -34
  4. package/dist/ai-sdk.mjs +147 -23
  5. package/dist/{decision-RQ0iv0oZ.mjs → decision-Bl_rKQnU.mjs} +73 -7
  6. package/dist/{decision-CmM8-HHz.cjs → decision-oevZnkBe.cjs} +112 -29
  7. package/dist/{text-logic-f5F5sCdc.cjs → event-log-Dtb16p1p.cjs} +813 -6
  8. package/dist/{text-logic-CIqHAmBi.mjs → event-log-GPZO9p7_.mjs} +679 -3
  9. package/dist/{event-log-store-BIPOz-di.d.mts → event-log-store-CN9ygggF.d.cts} +90 -5
  10. package/dist/{event-log-store-2xzFmgr2.d.cts → event-log-store-DS836wch.d.mts} +90 -5
  11. package/dist/event-log-store-bCdskPNS.cjs +95 -0
  12. package/dist/event-log-store-yalq8C77.mjs +84 -0
  13. package/dist/index.cjs +571 -58
  14. package/dist/index.d.cts +104 -510
  15. package/dist/index.d.mts +104 -510
  16. package/dist/index.mjs +542 -32
  17. package/dist/log.cjs +77 -20
  18. package/dist/log.d.cts +5 -5
  19. package/dist/log.d.mts +5 -5
  20. package/dist/log.mjs +58 -3
  21. package/dist/machines.cjs +1 -1
  22. package/dist/machines.d.cts +15 -15
  23. package/dist/machines.d.mts +15 -15
  24. package/dist/machines.mjs +1 -1
  25. package/dist/model-source-C7FFP2Sj.mjs +39 -0
  26. package/dist/model-source-DjJPjGSP.cjs +44 -0
  27. package/dist/openai.cjs +10 -10
  28. package/dist/openai.d.cts +27 -25
  29. package/dist/openai.d.mts +27 -25
  30. package/dist/openai.mjs +2 -2
  31. package/dist/otel.cjs +5 -4
  32. package/dist/otel.d.cts +5 -7
  33. package/dist/otel.d.mts +5 -7
  34. package/dist/otel.mjs +5 -3
  35. package/dist/{run-agent-COXXc0RW.d.mts → run-agent-C9ubX8Tx.d.cts} +87 -30
  36. package/dist/{run-agent-xNTxpt82.mjs → run-agent-CuQT1xSS.mjs} +170 -42
  37. package/dist/{run-agent-ByO-Bxyd.d.cts → run-agent-CxSyZmIE.d.mts} +87 -30
  38. package/dist/{run-agent-Dmf8GV2d.cjs → run-agent-cToZ3K42.cjs} +219 -86
  39. package/dist/{setup-agent-DV4cTou-.cjs → setup-agent-DTF3NQg3.cjs} +31 -20
  40. package/dist/setup-agent-DlbgwsIJ.d.cts +577 -0
  41. package/dist/{setup-agent-BuanDXeh.mjs → setup-agent-DniLvDu7.mjs} +15 -9
  42. package/dist/setup-agent-tsc3vm5I.d.mts +577 -0
  43. package/dist/tanstack-ai.cjs +627 -0
  44. package/dist/tanstack-ai.d.cts +204 -0
  45. package/dist/tanstack-ai.d.mts +204 -0
  46. package/dist/tanstack-ai.mjs +618 -0
  47. package/dist/testing.cjs +98 -985
  48. package/dist/testing.d.cts +51 -347
  49. package/dist/testing.d.mts +51 -347
  50. package/dist/testing.mjs +86 -973
  51. package/dist/{text-logic-IjfYy9DP.d.cts → text-logic-9fv1i9aK.d.cts} +50 -10
  52. package/dist/{text-logic-D-czVPYp.d.mts → text-logic-BDbGjYMh.d.mts} +50 -10
  53. package/dist/{types-BgNbNKnJ.d.mts → types-Dp5_Ru4R.d.cts} +2 -2
  54. package/dist/{types-hR5f5YXa.d.cts → types-Dp5_Ru4R.d.mts} +2 -2
  55. package/dist/usage-D8UphgEA.d.mts +17 -0
  56. package/dist/usage-R1NTmLxY.d.cts +17 -0
  57. package/dist/{utils-_q7F-AZu.d.mts → utils-BnXdWih3.d.mts} +1 -2
  58. package/dist/{utils-o4YRdHJX.d.cts → utils-ebVpCYoF.d.cts} +1 -2
  59. package/dist/validate.cjs +239 -8
  60. package/dist/validate.d.cts +110 -3
  61. package/dist/validate.d.mts +110 -3
  62. package/dist/validate.mjs +238 -7
  63. package/dist/verify-B5qGC5WN.mjs +1181 -0
  64. package/dist/verify-CBuYIpsa.d.mts +414 -0
  65. package/dist/verify-Cqghu4gz.cjs +1222 -0
  66. package/dist/verify-DYH_2VzU.d.cts +414 -0
  67. package/package.json +44 -31
  68. package/readme.md +38 -26
  69. package/skills/generate-machine/SKILL.md +140 -66
  70. package/skills/improve-machine/SKILL.md +190 -0
  71. package/skills/improve-machine/reference.md +96 -0
  72. package/dist/event-log-store-2pAFiWuc.mjs +0 -576
  73. package/dist/event-log-store-C-COhIXP.cjs +0 -702
  74. package/dist/usage-BdEjFuIi.mjs +0 -5
  75. 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 that all work with no API key: Ajv → `fromConfig` → lint → simulate. Do not hand back a config that has not passed all four.
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
- author → validate (validateAgentConfig) → lower (fromConfig) → lint (lintAgentMachine) → simulate → hand back
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": "{{ context.ticket }}",
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. Validate with `validateAgentConfig`
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
- Each error carries a JSON Pointer `path`, so a repair prompt can name the exact bad field.
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
- ## 4. Lower with `fromConfig`
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
- `fromConfig` needs a `compileSchema` that turns the JSON Schemas _inside_ the config into Standard Schema validators. Two engines, two jobs: the 2020 build above checks the config document; this one compiles the per-field schemas. The library bundles no JSON Schema engine, by design.
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 { setupAgent, type SchemaCompiler, type StandardSchemaV1 } from "@statelyai/agent";
182
+ import type { SchemaCompiler, StandardSchemaV1 } from "@statelyai/agent";
161
183
 
162
184
  const ajv = new Ajv({ strict: false });
163
185
 
164
- export const ajvCompileSchema: SchemaCompiler = (jsonSchema, name): StandardSchemaV1 => {
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's serializability checks can read the shape.
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
- const { machine, schemas } = setupAgent.fromConfig(config, { compileSchema: ajvCompileSchema });
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
- Lowering is itself a gate: it throws on an unresolved named guard/action and on an `onDone` attached to an `agent.decide` invoke.
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
- ## 5. Lint
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
- import { lintAgentMachine } from "@statelyai/agent/testing";
193
- const diagnostics = lintAgentMachine(machine);
194
- lintAgentMachine(machine, { throw: true }); // throws AgentLintError on error-severity findings
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
- Every check applies to config-built machines, reachability included — the lowering keeps the config's transition targets, so `unreachable-state` and `missing-final` read the real graph. Do not disable checks.
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
- The one that bites most often is `decide-without-events`: an `allowedEvents` list with no matching `on` transitions produces a decision the machine can never deliver.
256
+ ## 5. Manual fallback
200
257
 
201
- ## 6. Simulate a dry run
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 { simulateAgent } from "@statelyai/agent/testing";
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
- // dryRun.status: 'done' | 'idle' | 'exhausted'
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
- Derive the script from the config rather than guessing: take the first entry of each decision's `allowedEvents`, and stub each request's output from its declared `output` schema (`""` for string, `0` for number, `[]` for array, recurse on `properties`).
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
- ## 7. Repair loop
288
+ ## 6. Persist traces from day one
224
289
 
225
- Every gate throws with a message naming the offending field, state, or path. Feed that message back verbatim and regenerate:
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
- for (let attempt = 0; attempt < 3; attempt++) {
229
- try {
230
- return buildAndVet(await author({ system, prompt }));
231
- } catch (error) {
232
- prompt = `${originalTask}\n\nYour previous config was rejected:\n${(error as Error).message}\nReturn a corrected config.`;
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
- Keep the cap at ~3. A config that fails three schema-shaped repairs is usually asking for something the data form cannot express — author it in TypeScript with `setupAgent` instead.
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
- ## 8. Hand back
310
+ ## 7. Hand back
240
311
 
241
312
  Deliver the config JSON plus how to run it:
242
313
 
243
314
  ```ts
244
- import { runToQuiescence } from "@statelyai/agent";
315
+ import { runAgent } from "@statelyai/agent";
245
316
  import { createAiSdkExecutors } from "@statelyai/agent/ai-sdk";
246
317
 
247
- const result = await runToQuiescence(
248
- createAgentRuntime(machine, { executors: createAiSdkExecutors({ resolveModel }) }),
249
- { input: { ticket: "Export downloads a 0-byte CSV on Safari." } },
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 warning-severity lint diagnostics, and report the dry-run status. Model refs are strings, so the host supplies `resolveModel`.
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 simulate are for.
260
- - **A dry run covers one path.** Use `explorePaths` when the branch structure matters.
261
- - **Model refs and tool names are unchecked strings.** They resolve at run time, in the host.
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
+ ```