@statelyai/agent 2.0.0-alpha.29 → 2.0.0-alpha.30

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 (68) hide show
  1. package/dist/ai-sdk.cjs +4 -4
  2. package/dist/ai-sdk.d.cts +5 -5
  3. package/dist/ai-sdk.d.mts +5 -5
  4. package/dist/ai-sdk.mjs +4 -4
  5. package/dist/{decision-Bl_rKQnU.mjs → decision-BHkvuImD.mjs} +2 -2
  6. package/dist/{decision-oevZnkBe.cjs → decision-CIb7ddIv.cjs} +2 -2
  7. package/dist/{event-log-Dtb16p1p.cjs → event-log-BZRfBRd7.cjs} +1 -1
  8. package/dist/{event-log-GPZO9p7_.mjs → event-log-DJCMHek_.mjs} +1 -1
  9. package/dist/{event-log-store-CN9ygggF.d.cts → event-log-store-CIIJohjj.d.mts} +1 -1
  10. package/dist/{event-log-store-DS836wch.d.mts → event-log-store-CLAC9q2Y.d.cts} +1 -1
  11. package/dist/{event-log-store-bCdskPNS.cjs → event-log-store-DZR4IIWe.cjs} +1 -1
  12. package/dist/{event-log-store-yalq8C77.mjs → event-log-store-DsP6GCRh.mjs} +1 -1
  13. package/dist/index.cjs +12 -7
  14. package/dist/index.d.cts +5 -5
  15. package/dist/index.d.mts +5 -5
  16. package/dist/index.mjs +12 -7
  17. package/dist/log.cjs +2 -2
  18. package/dist/log.d.cts +1 -1
  19. package/dist/log.d.mts +1 -1
  20. package/dist/log.mjs +2 -2
  21. package/dist/machines.cjs +1 -1
  22. package/dist/machines.mjs +1 -1
  23. package/dist/{model-source-DjJPjGSP.cjs → model-source-BCKg4qJ1.cjs} +2 -2
  24. package/dist/{model-source-C7FFP2Sj.mjs → model-source-BaHwJ1wK.mjs} +2 -2
  25. package/dist/openai.cjs +3 -3
  26. package/dist/openai.d.cts +2 -2
  27. package/dist/openai.d.mts +2 -2
  28. package/dist/openai.mjs +3 -3
  29. package/dist/otel.d.cts +1 -1
  30. package/dist/otel.d.mts +1 -1
  31. package/dist/{run-agent-CuQT1xSS.mjs → run-agent-BpHqkaZw.mjs} +14 -5
  32. package/dist/{run-agent-CxSyZmIE.d.mts → run-agent-CAVt5R-r.d.mts} +11 -3
  33. package/dist/{run-agent-C9ubX8Tx.d.cts → run-agent-CTlMc3Jb.d.cts} +11 -3
  34. package/dist/{run-agent-cToZ3K42.cjs → run-agent-ctLCvNgL.cjs} +14 -5
  35. package/dist/{setup-agent-DniLvDu7.mjs → setup-agent-7rV_cujV.mjs} +2 -2
  36. package/dist/{setup-agent-tsc3vm5I.d.mts → setup-agent-CGELn1qZ.d.mts} +2 -2
  37. package/dist/{setup-agent-DlbgwsIJ.d.cts → setup-agent-CLPCkXsu.d.cts} +2 -2
  38. package/dist/{setup-agent-DTF3NQg3.cjs → setup-agent-DoUk7EdC.cjs} +2 -2
  39. package/dist/tanstack-ai.cjs +3 -3
  40. package/dist/tanstack-ai.d.cts +2 -2
  41. package/dist/tanstack-ai.d.mts +2 -2
  42. package/dist/tanstack-ai.mjs +3 -3
  43. package/dist/testing.cjs +110 -4
  44. package/dist/testing.d.cts +85 -4
  45. package/dist/testing.d.mts +85 -4
  46. package/dist/testing.mjs +110 -5
  47. package/dist/{text-logic-BDbGjYMh.d.mts → text-logic-BRYOPWvY.d.mts} +2 -2
  48. package/dist/{text-logic-9fv1i9aK.d.cts → text-logic-DmciuVUQ.d.cts} +2 -2
  49. package/dist/{usage-D8UphgEA.d.mts → usage-BkM3z02Y.d.mts} +1 -1
  50. package/dist/{usage-R1NTmLxY.d.cts → usage-Cm_ovz9V.d.cts} +1 -1
  51. package/dist/validate.cjs +3 -3
  52. package/dist/validate.d.cts +2 -2
  53. package/dist/validate.d.mts +2 -2
  54. package/dist/validate.mjs +3 -3
  55. package/dist/{verify-B5qGC5WN.mjs → verify-COFWyprJ.mjs} +2 -2
  56. package/dist/{verify-Cqghu4gz.cjs → verify-CVlsTZ-V.cjs} +2 -2
  57. package/dist/{verify-CBuYIpsa.d.mts → verify-Cn86rT8l.d.mts} +1 -1
  58. package/dist/{verify-DYH_2VzU.d.cts → verify-Dzwd4Mvk.d.cts} +1 -1
  59. package/package.json +1 -1
  60. package/readme.md +3 -3
  61. package/skills/add-evals/SKILL.md +129 -0
  62. package/skills/add-traces/SKILL.md +135 -0
  63. package/skills/generate-machine/SKILL.md +5 -5
  64. package/skills/graph-engineering/SKILL.md +33 -0
  65. package/skills/improve-machine/SKILL.md +70 -19
  66. package/skills/improve-machine/reference.md +12 -7
  67. package/skills/loop-to-machine/SKILL.md +183 -0
  68. package/skills/loop-to-machine/policy.md +158 -0
package/dist/validate.cjs CHANGED
@@ -21,9 +21,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
- const require_event_log = require("./event-log-Dtb16p1p.cjs");
25
- const require_setup_agent = require("./setup-agent-DTF3NQg3.cjs");
26
- const require_verify = require("./verify-Cqghu4gz.cjs");
24
+ const require_event_log = require("./event-log-BZRfBRd7.cjs");
25
+ const require_setup_agent = require("./setup-agent-DoUk7EdC.cjs");
26
+ const require_verify = require("./verify-CVlsTZ-V.cjs");
27
27
  let ajv_dist_2020_js = require("ajv/dist/2020.js");
28
28
  ajv_dist_2020_js = __toESM(ajv_dist_2020_js, 1);
29
29
  //#region schemas/agent-workflow.json
@@ -1,5 +1,5 @@
1
- import { d as AgentWorkflowConfig, g as FromConfigOptions, v as SchemaCompiler } from "./setup-agent-DlbgwsIJ.cjs";
2
- import { l as ExplorePathsOptions } from "./verify-DYH_2VzU.cjs";
1
+ import { d as AgentWorkflowConfig, g as FromConfigOptions, v as SchemaCompiler } from "./setup-agent-CLPCkXsu.cjs";
2
+ import { l as ExplorePathsOptions } from "./verify-Dzwd4Mvk.cjs";
3
3
  import { AnyActorLogic, AnyStateMachine } from "xstate";
4
4
  //#region src/validate/validate-config.d.ts
5
5
  /**
@@ -1,5 +1,5 @@
1
- import { d as AgentWorkflowConfig, g as FromConfigOptions, v as SchemaCompiler } from "./setup-agent-tsc3vm5I.mjs";
2
- import { l as ExplorePathsOptions } from "./verify-CBuYIpsa.mjs";
1
+ import { d as AgentWorkflowConfig, g as FromConfigOptions, v as SchemaCompiler } from "./setup-agent-CGELn1qZ.mjs";
2
+ import { l as ExplorePathsOptions } from "./verify-Cn86rT8l.mjs";
3
3
  import { AnyActorLogic, AnyStateMachine } from "xstate";
4
4
  //#region src/validate/validate-config.d.ts
5
5
  /**
package/dist/validate.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { D as STREAM_TEXT_ACTOR, E as GENERATE_TEXT_ACTOR, T as DECIDE_ACTOR } from "./event-log-GPZO9p7_.mjs";
2
- import { i as setupAgentFromConfig } from "./setup-agent-DniLvDu7.mjs";
3
- import { a as explorePaths, o as lintAgentMachine } from "./verify-B5qGC5WN.mjs";
1
+ import { D as STREAM_TEXT_ACTOR, E as GENERATE_TEXT_ACTOR, T as DECIDE_ACTOR } from "./event-log-DJCMHek_.mjs";
2
+ import { i as setupAgentFromConfig } from "./setup-agent-7rV_cujV.mjs";
3
+ import { a as explorePaths, o as lintAgentMachine } from "./verify-COFWyprJ.mjs";
4
4
  import Ajv2020 from "ajv/dist/2020.js";
5
5
  //#region schemas/agent-workflow.json
6
6
  var agent_workflow_default = {
@@ -1,5 +1,5 @@
1
- import { P as isTextLogic, U as getRegisteredAgentExecutionOptions, V as executorBoundLogics, X as getJsonSchemaSync, st as AgentError } from "./event-log-GPZO9p7_.mjs";
2
- import { d as getAcceptedEvents, l as resolveDecision, m as isRecord, n as acceptedDecisionOptions, s as isDecisionLogic, t as AgentDecisionExhaustedError } from "./decision-Bl_rKQnU.mjs";
1
+ import { P as isTextLogic, U as getRegisteredAgentExecutionOptions, V as executorBoundLogics, X as getJsonSchemaSync, st as AgentError } from "./event-log-DJCMHek_.mjs";
2
+ import { d as getAcceptedEvents, l as resolveDecision, m as isRecord, n as acceptedDecisionOptions, s as isDecisionLogic, t as AgentDecisionExhaustedError } from "./decision-BHkvuImD.mjs";
3
3
  import { getNextTransitions, initialTransition, transition } from "xstate";
4
4
  //#region src/steps.ts
5
5
  /**
@@ -1,5 +1,5 @@
1
- const require_event_log = require("./event-log-Dtb16p1p.cjs");
2
- const require_decision = require("./decision-oevZnkBe.cjs");
1
+ const require_event_log = require("./event-log-BZRfBRd7.cjs");
2
+ const require_decision = require("./decision-CIb7ddIv.cjs");
3
3
  let xstate = require("xstate");
4
4
  //#region src/steps.ts
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import { u as ChosenEvent } from "./types-Dp5_Ru4R.mjs";
2
- import { Q as AgentError } from "./text-logic-BDbGjYMh.mjs";
2
+ import { Q as AgentError } from "./text-logic-BRYOPWvY.mjs";
3
3
  import { AnyMachineSnapshot, AnyStateMachine } from "xstate";
4
4
  //#region src/verify.d.ts
5
5
  /** Severity of an {@link AgentLintDiagnostic}. `error` findings fail CI/the CLI; `warning`s are advisory. */
@@ -1,5 +1,5 @@
1
1
  import { u as ChosenEvent } from "./types-Dp5_Ru4R.cjs";
2
- import { Q as AgentError } from "./text-logic-9fv1i9aK.cjs";
2
+ import { Q as AgentError } from "./text-logic-DmciuVUQ.cjs";
3
3
  import { AnyMachineSnapshot, AnyStateMachine } from "xstate";
4
4
  //#region src/verify.d.ts
5
5
  /** Severity of an {@link AgentLintDiagnostic}. `error` findings fail CI/the CLI; `warning`s are advisory. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statelyai/agent",
3
- "version": "2.0.0-alpha.29",
3
+ "version": "2.0.0-alpha.30",
4
4
  "description": "Make invalid agent actions impossible. Agent logic as state machines: deterministic, inspectable, resumable, runs anywhere.",
5
5
  "keywords": [
6
6
  "agent",
package/readme.md CHANGED
@@ -32,7 +32,7 @@ Stately Agent 2 is in alpha. APIs may change before the stable release.
32
32
  <!-- install command matching the package prerelease channel and package.json peers -->
33
33
 
34
34
  ```sh
35
- pnpm add @statelyai/agent@alpha xstate@6.0.0-alpha.64 zod
35
+ pnpm add @statelyai/agent@alpha xstate@6.0.0-alpha.69 zod
36
36
  ```
37
37
 
38
38
  XState is pinned while its experimental durable restoration API evolves.
@@ -59,7 +59,7 @@ Classification, grading, relevance, and yes/no checks are judgments: call the AI
59
59
 
60
60
  Requirements:
61
61
 
62
- - Node 22.18 or newer, and XState v6 alpha.46 or newer.
62
+ - Node 22.18 or newer, and XState `6.0.0-alpha.69`.
63
63
  - The package is ESM-first. CommonJS builds are published too, so `require()` works.
64
64
  - Provider packages must match your `ai` major: `@ai-sdk/openai@^4` pairs with `ai@^7`. A bare `@ai-sdk/openai` resolves to `@latest`, which can mismatch the `ai` peer.
65
65
 
@@ -151,7 +151,7 @@ import { openai } from "@ai-sdk/openai";
151
151
  import { createAiSdkExecutors } from "@statelyai/agent/ai-sdk";
152
152
 
153
153
  // In the setupAgent({ ... }) call above:
154
- // models: { fast: openai("gpt-5.4-mini") },
154
+ // models: { fast: openai("gpt-6-luna") },
155
155
 
156
156
  const liveResult = await runAgent(refundMachine, {
157
157
  input: { request: "I was charged twice for the same order.", amount: 75 },
@@ -0,0 +1,129 @@
1
+ ---
2
+ name: add-evals
3
+ description: Write evals for a @statelyai/agent or XState agent machine at the right level — rule checks with no model, decision evals at one decision state, outcome evals over whole runs, and per-state stats from traces — each tied to the states it covers. Use when asked to "write evals", "add evals for the approval logic", "test the agent", "measure the agent", "build an eval suite", or "what should I eval" for an agent built as a state machine.
4
+ ---
5
+
6
+ # Add evals to an agent machine
7
+
8
+ The machine tells you what to eval and at which level. Every eval names the states it covers, so a failing eval points at a part of the machine.
9
+
10
+ ```
11
+ read the machine → map the request to states → pick the level per state → write the evals → baseline → hand back
12
+ ```
13
+
14
+ This is step 3 of 4. Before: `loop-to-machine`, `add-traces`. Next: `improve-machine`.
15
+
16
+ ## Hard rules
17
+
18
+ - **The cheapest level that can catch the failure.** A rule is checked without a model. Only judgment needs a model in the eval.
19
+ - **Ground truth, never a model's opinion of itself.** Outcome evals read terminal states and effects in the world. A model grades text only where no ground truth exists, and that eval says so in its name.
20
+ - **Labels belong to people.** Draft expected outcomes and expected events; ask the user to confirm them before they gate anything. Keep drafts in their own list, marked unconfirmed, and out of every gate.
21
+ - **Never edit the machine to make an eval pass.** A failing eval is a finding. Report it.
22
+ - **Held-out scenarios stay held out.** Evals may run on them; nobody tunes against them.
23
+
24
+ ## 1. Read the machine
25
+
26
+ ```ts
27
+ import { describeMachine } from "@statelyai/agent";
28
+ console.log(describeMachine(machine)); // goal, states, transitions, events
29
+ ```
30
+
31
+ List:
32
+
33
+ - the decision states: an `agent.decide` invoke, a `decision` tag, or a state a host answers with `decideNext`;
34
+ - the person's steps: idle states a person answers (`meta.interaction`, the runner's `onIdle`). What the person decides is ground truth, not something to eval; the routing after their answer is a rule;
35
+ - the guards. `describeMachine` shows where a transition function can go and marks it `(conditional)`, but not the guard's name or logic. Read the source for those;
36
+ - the sentinel states and the final states.
37
+
38
+ ## 2. Map the request to states
39
+
40
+ Turn the user's words into states. "Evals for the approval logic" means every state and guard that touches approval: the guard that routes a refund over the threshold, the approval step and each answer to it, the sentinel check before the payment, and the final states they lead to. Write the mapping down:
41
+
42
+ | Asked about | States and guards |
43
+ | ----------- | -------------------------------------------------------------------------------------------------- |
44
+ | approval | `deciding` (REFUND: over threshold → `awaitingApproval`), `awaitingApproval` (APPROVE, REJECT), `paying` (sentinel check), `issued`, `closed` |
45
+
46
+ If the request names nothing ("write evals"), cover every decision state, every sentinel, and the run as a whole.
47
+
48
+ ## 3. Pick the level
49
+
50
+ | Level | Catches | Runs a model | Tools |
51
+ | ------------ | ---------------------------------------------------- | --------------------- | ------------------------------------------------------------------ |
52
+ | **Rule** | A guard, an ordering, an invariant that must hold | No | Exported guard functions, `canReach`, `explorePaths`, `simulateAgent` |
53
+ | **Decision** | A wrong choice at one decision state | The decider under test | The decider called on recorded or written decision requests |
54
+ | **Request** | A bad text output from one request | That request | `runSeam`, scored on `seamOutput` and the path after it. Not applicable when the machine has no text requests; say so |
55
+ | **Outcome** | A wrong end result for a whole scenario | The whole agent | Scenarios, the runner, an objective over terminal states and effects |
56
+ | **Stats** | Which state the failures cluster in | No (reads traces) | `foldTraces` |
57
+
58
+ A guard or ordering is always a rule eval. Use a decision eval when the guards leave more than one legal option. Use an outcome eval when a person can label the right end result.
59
+
60
+ ## 4. Write them
61
+
62
+ Name every eval `<state>: <what it checks>`, so the report reads as a map of the machine. In `test.each` titles, `$name` is a placeholder: write amounts as `100 dollars`, not `$100`.
63
+
64
+ **Rule evals**, from `@statelyai/agent/testing`:
65
+
66
+ ```ts
67
+ test("paying: no refund over 100 dollars issues without approval", async () => {
68
+ const result = await canReach(machine, "ruleBroken", { input, payloads, outputs, errors });
69
+ expect(result.complete).toBe(true); // reachable: false is a proof only when the search finished
70
+ expect(result.reachable, JSON.stringify(result.witness)).toBe(false);
71
+ });
72
+
73
+ test("deciding: REFUND over the order amount is refused", () => {
74
+ expect(refundIsLegal({ ...context, order: { ...order, amount: 40 } }, 41)).toBe(false);
75
+ });
76
+ ```
77
+
78
+ Use `explorePaths` to assert the set of reachable final states, and `await simulateAgent(...)` for one scripted path. Cover both sides of every threshold. The two treat a closed choice differently:
79
+
80
+ - `explorePaths` and `canReach` use `options.payloads` **instead of** the decision's own offered options, so they explore values the machine would never offer. That is what a boundary proof needs.
81
+ - `simulateAgent` keeps the offered options: a scripted event the decision did not offer is refused, and the run ends in the decision's `onError`. Script only offered events, or pick an input that offers the value.
82
+
83
+ **Decision evals.** A row is a decision request and the event a person says is right. Write rows by hand, or take them from recorded traces: the first attempt of each decision is a `request.start` event with `request.kind === "decision"` and `request.attempts.length === 0`, and its `request` carries `name`, `options`, and `metadata`. Cast the serialized `request` to `AgentExecutorDecisionRequest`, and compare events without caring about key order. If the decider reads anything outside the request (the ticket, the tool history), store that in the row too.
84
+
85
+ ```ts
86
+ // Your helper: rows for one decision state, from traces or a fixture file.
87
+ const rows: Array<{ id: string; request: AgentExecutorDecisionRequest; expected: ChosenEvent }> = loadDecisionRows("deciding");
88
+
89
+ test.each(rows)("deciding: $id", async ({ request, expected }) => {
90
+ const { event } = await decide(request); // the policy decider, or the model executor
91
+ expect(event).toEqual(expected);
92
+ });
93
+ ```
94
+
95
+ Which deciders gate CI: a deterministic reference decider (a scripted or rules-based one) gates. A live model is a measurement, not a unit test: run it outside CI, several times, and report the rate. A seeded random decider is measured over several seeds, never gated.
96
+
97
+ **Request evals.** For a text request (a drafted question, a summary), `runSeam(machine, { input, scripts, seam: { request: "draftQuestion" }, candidate })` runs the whole machine with only that request live. Score `seamOutput`, and the states the run reached after it.
98
+
99
+ **Outcome evals.** One function over a batch of scenario runs, reading ground truth. Score the effects as well as the outcome: a run that ends `issued` with the wrong amount is wrong.
100
+
101
+ ```ts
102
+ export function objective(runs: Array<{ scenario: Scenario; outcome: string; refund?: Refund; violations: number; modelCalls: number }>) {
103
+ return {
104
+ accuracy: runs.filter((r) => r.outcome === r.scenario.expected && sameRefund(r.refund, r.scenario.expectedRefund)).length / runs.length,
105
+ violations: runs.reduce((n, r) => n + r.violations, 0), // must be 0
106
+ modelCalls: runs.reduce((n, r) => n + r.modelCalls, 0), // cost
107
+ };
108
+ }
109
+ ```
110
+
111
+ Read the terminal state from the event logs `add-traces` recorded (`replay(machine, entries).snapshot`, `replay` from `@statelyai/agent/log`). Effects and violations live in the world, not the log: save them next to the thread id when the run ends, so the objective can be recomputed without running anything. Use `matchesTrajectory` only for an ordering a person requires (approval before payment), never to pin the whole path; build its input from the `machine.transition` trace events' `snapshot.value`.
112
+
113
+ **Stats.** `foldTraces(traces, { failed })` (from `@statelyai/agent/testing`) returns `{ state, visits, failures, withheld }` per state. A failed run counts against **every** state it visited, so states after a bad decision rank as high as the decision itself. Use the table to see which states failing runs pass through, then look at the decision states among them, their `withheld` counts, and their decision attempts. `failed` sees only the trace: to judge a run against its label, keep a map from trace to scenario.
114
+
115
+ ## 5. Coverage
116
+
117
+ If every decider scores the same on the confirmed scenarios, the scenarios cannot tell a better decider from a worse one. Draft scenarios on the boundaries the evals found (an amount just under and over a threshold, a partial request), label them as drafts, and ask the user to confirm them and add them to the scenario set. They become part of the train or held-out split only once confirmed.
118
+
119
+ ## 6. Baseline
120
+
121
+ Run every eval once on the current machine and record the numbers: which pass, the outcome scores per split, the stats table. Failing evals are findings, not bugs in the eval, unless the label was wrong; then ask.
122
+
123
+ ## 7. Hand back
124
+
125
+ 1. The state mapping from step 2.
126
+ 2. The evals by level, each named by state.
127
+ 3. The baseline numbers, train and held-out separately.
128
+ 4. Every failing eval and the state it points at.
129
+ 5. Next step: `improve-machine`, with the objective from step 4 as its scorer.
@@ -0,0 +1,135 @@
1
+ ---
2
+ name: add-traces
3
+ description: Make every run of a @statelyai/agent or XState machine leave a replayable, stamped trace — an event log thread plus trace events — so evals and self-improvement can read what happened and what produced it. Use when asked to "add tracing", "add logging", "persist runs", "stamp traces", "record what the agent did", or "set up observability" for an agent machine, or before writing evals or improving a policy from traces.
4
+ ---
5
+
6
+ # Add traces to an agent machine
7
+
8
+ Every run should leave two records, both stamped with what produced it:
9
+
10
+ | Record | What it is | Read by |
11
+ | ---------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------- |
12
+ | Event log thread | The external inputs the machine consumed, in order. The source of truth | `replay`, `getDecisionAttempts`, `getUsageFromEvents` |
13
+ | Trace events | Observations: transitions with `from`, decision requests with offered options | `foldTraces`, OpenTelemetry, debugging |
14
+
15
+ ```
16
+ version the machines → pick a store → name threads → stamp → collect trace events → prove replay → hand back
17
+ ```
18
+
19
+ This is step 2 of 4. Before: `loop-to-machine`. Next: `add-evals`, then `improve-machine`.
20
+
21
+ ## Hard rules
22
+
23
+ - **Traces never change behavior.** Recording is an observer. No branch reads a trace.
24
+ - **A trace must name what produced it.** A run with no machine version or policy version cannot be used to compare versions, so it is useless to the improver.
25
+ - **Replay is the test.** A log that does not replay to the run's final state with zero model calls is not a trace yet.
26
+ - **No secrets or personal data in metadata or thread ids.** Use ids, not names or emails. The log itself holds the machine's input and every event verbatim (a ticket's text, a customer's reply), and trace snapshots hold the context: in production, redact or encrypt them per your data rules, and ask the user which applies.
27
+
28
+ ## 1. Version the machines
29
+
30
+ Declare `version` on every machine whose runs you record: the environment and, if there is one, the policy machine.
31
+
32
+ ```ts
33
+ export const deskMachine = deskSetup.createMachine({ id: "refund-desk", version: "1", ... });
34
+ ```
35
+
36
+ Also name the **decider version**: what answers the decisions. It is the policy machine's `version` when there is one; otherwise a label for the model and prompt that decide (`gpt-6-luna.p1`), or for a stand-in model in tests (`careful`). Runs by different deciders must never share a version label.
37
+
38
+ - Every log entry carries `machineVersion`. Without a declared `version` it falls back to a structural hash that skips functions, and in XState v6 the guards and targets live in transition functions. Changing a guard would not change the hash.
39
+ - Bump `version` on every behavior change. Adding recording is not one: it only observes. `improve-machine` bumps the policy's on promotion.
40
+
41
+ ## 2. Pick a store
42
+
43
+ - **Tests and local runs:** `createInMemoryEventLogStore()` from `@statelyai/agent/log`, or write each thread's `result.events` to a JSON file after the run.
44
+ - **Production:** implement `AgentEventLogStore` against your database and check it with `assertEventLogStoreConformance`. Implement the optional `listThreads` so a split can be paged by prefix.
45
+
46
+ Pass it with a `threadId`. The store is write-ahead: no model call starts until the entries before it are durable.
47
+
48
+ ```ts
49
+ const result = await runAgent(deskMachine, { input, executors, store, threadId, metadata });
50
+ await result.drain(); // late usage entries, before you read the store or exit
51
+ ```
52
+
53
+ **A thread that already has entries is resumed, not overwritten.** Give every new run a new thread id; reuse one only to continue that run.
54
+
55
+ ## 3. Name threads
56
+
57
+ A thread id is how runs are found later. Use a prefix scheme and keep it in one function:
58
+
59
+ | Runs | Thread id |
60
+ | ------------------------ | -------------------------------------------- |
61
+ | Scenario runs for evals | `<split>:<deciderVersion>:<scenarioId>:r<k>`, `k` = the repeat index |
62
+ | Production | `prod:<domain id>` (ticket id, session id) |
63
+
64
+ `store.listThreads?.({ prefix: "train:careful:" })` then pages through one split of one decider. `listThreads` is optional on a custom store.
65
+
66
+ ## 4. Stamp each run
67
+
68
+ `metadata` lands on the log's first (`@agent.init`) entry. Put there everything needed to tell two runs' producers apart:
69
+
70
+ ```ts
71
+ const metadata = {
72
+ environmentVersion: deskMachine.version ?? "unversioned",
73
+ deciderVersion, // see step 1
74
+ models: { decider: "openai/gpt-6-luna" }, // the machine's model refs → what the host resolved them to
75
+ scenario: scenario.id, // eval runs only
76
+ split: scenario.split, // eval runs only
77
+ };
78
+ ```
79
+
80
+ Read it back with `entries[0].metadata`. A resumed run keeps the stamp of the run that started the log. `executionId` is reserved. The value type is `JsonValue` from `@statelyai/agent/log`.
81
+
82
+ ## 5. Collect trace events
83
+
84
+ `onTrace` sees every trace event. Serialize and keep them per thread:
85
+
86
+ ```ts
87
+ import { serializeTraceEvent, type JsonSerializableTraceEvent } from "@statelyai/agent";
88
+
89
+ const trace: JsonSerializableTraceEvent[] = [];
90
+ const result = await runAgent(deskMachine, { ..., onTrace: (event) => trace.push(serializeTraceEvent(event)) });
91
+ await result.drain();
92
+
93
+ // For local batches: one file per thread. Encoding keeps colons and path separators out of the name.
94
+ const file = encodeURIComponent(threadId);
95
+ await mkdir("runs", { recursive: true });
96
+ await writeFile(`runs/${file}.events.json`, JSON.stringify(result.events));
97
+ await writeFile(`runs/${file}.trace.json`, JSON.stringify(trace));
98
+ ```
99
+
100
+ Decide with the user whether `runs/` is committed or gitignored.
101
+
102
+ - In production, send them to OpenTelemetry instead: `createOtelTraceHandler({ tracer })` from `@statelyai/agent/otel` is an `onTrace` handler.
103
+ - **A policy run inside a decider** is a separate run. Pass it the same `onTrace` (and its own `store` thread, `<threadId>:policy:<n>`) if you want per-state stats for the policy too.
104
+ - **A plain XState machine** (built with `createMachine` or `setup`, decided with `decideNext`): drive it with `createAgentRuntime` and `runToQuiescence`, which accept plain machines, and pass each `decideNext` result as `{ event, attempts }` so refused choices reach the log. Push `{ type: "request.start", request: result.request }` onto the trace before sending the event, since a host-driven decision emits no trace itself.
105
+ - **A plain `createActor` run you cannot change:** attach `recordLog(actor)` from `@statelyai/agent/log` before `actor.start()`, and pass `inspect: traceTransitions((event) => trace.push(serializeTraceEvent(event)))` to `createActor`. `recordLog` cannot record decision attempts.
106
+
107
+ ## 6. Prove it
108
+
109
+ Write these tests, with the stand-in model from `loop-to-machine`:
110
+
111
+ ```ts
112
+ import { getDecisionAttempts, replay } from "@statelyai/agent/log";
113
+ import { foldTraces } from "@statelyai/agent/testing";
114
+
115
+ test("a recorded run replays to its final state with no model calls", async () => {
116
+ const result = await runAgent(deskMachine, { input, executors, store, threadId: "test:careful:small-clean:r1", metadata });
117
+ await result.drain();
118
+ const entries = await store.read("test:careful:small-clean:r1");
119
+ const { snapshot } = replay(deskMachine, entries); // a pure fold: nothing runs
120
+ expect(snapshot.value).toEqual(result.snapshot.value);
121
+ expect(entries[0]!.metadata).toMatchObject({ environmentVersion: "1" });
122
+ });
123
+ ```
124
+
125
+ - `foldTraces(traces, { failed })` over a batch returns a row for every decision state. `failed` judges one whole trace; start with "the last event is not a `run.end` with status `done`, or the run ended in a failure or sentinel state", and ask the user whether a wrong outcome counts too. `withheld` is above 0 only for a closed choice (`payloads`), where the guards refused an option.
126
+ - `getDecisionAttempts(entries)` lists the refused choices of a run. A stand-in model that picks from the offered options can never propose an illegal one, so write a raw decide executor for this test that proposes a refused event first: `decide: async (request) => ({ event: request.attempts.length ? request.options![0]!.event : { type: "REFUND", amount: 1000 } })`.
127
+ - Running a scenario with and without recording leaves the same world and the same final state.
128
+
129
+ ## 7. Hand back
130
+
131
+ 1. Where runs are stored and how thread ids are built.
132
+ 2. The stamp fields.
133
+ 3. Where trace events go.
134
+ 4. The replay test passing.
135
+ 5. Next step: `add-evals`.
@@ -31,7 +31,7 @@ import { describeMachine } from "@statelyai/agent";
31
31
  const text = describeMachine(machine); // "Machine <id>", then "Goal: …", states, transitions, events
32
32
  ```
33
33
 
34
- Pass `{ snapshot }` to mark the active states with `*`. Concept page: `docs/environment-and-policy.md`.
34
+ Pass `{ snapshot }` to mark the active states with `*`. Concept page: [Environment and policy](https://github.com/statelyai/agent/blob/main/docs/environment-and-policy.md).
35
35
 
36
36
  ## 1. Read the schema
37
37
 
@@ -58,7 +58,7 @@ Rules the runtime enforces. Each one maps to a build error or a lint diagnostic:
58
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.
59
59
  - Every path must reach a `"type": "final"` state, and each final state needs an `output` when `schemas.output` is declared.
60
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.
61
- - Model refs are strings (`"openai/gpt-5.4-mini"`); the host resolves them.
61
+ - Model refs are strings (`"openai/gpt-6-luna"`); the host resolves them.
62
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
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.
64
64
 
@@ -102,7 +102,7 @@ Reference config — decision, text request, idle human step, one final state:
102
102
  "context": { "ticket": "{{ input.ticket }}" },
103
103
  "requests": {
104
104
  "draftReply": {
105
- "model": "openai/gpt-5.4-mini",
105
+ "model": "openai/gpt-6-luna",
106
106
  "system": "Draft a short, courteous support reply.",
107
107
  "prompt": "{{ input.ticket }}",
108
108
  "input": {
@@ -124,7 +124,7 @@ Reference config — decision, text request, idle human step, one final state:
124
124
  "id": "triageDecision",
125
125
  "src": "agent.decide",
126
126
  "input": {
127
- "model": "openai/gpt-5.4-mini",
127
+ "model": "openai/gpt-6-luna",
128
128
  "system": "Decide whether this ticket needs escalation or a drafted reply.",
129
129
  "prompt": "{{ context.ticket }}",
130
130
  "allowedEvents": ["ESCALATE", "REPLY"],
@@ -217,7 +217,7 @@ const result = await verifyAgentConfig(candidate, {
217
217
  guards: [],
218
218
  actions: [],
219
219
  events: ["ESCALATE", "REPLY", "APPROVE", "REJECT"],
220
- models: ["openai/gpt-5.4-mini"],
220
+ models: ["openai/gpt-6-luna"],
221
221
  },
222
222
  explore: {
223
223
  inputs: [{ ticket: "Export downloads a 0-byte CSV on Safari." }],
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: graph-engineering
3
+ description: Route an agent-reliability request to the right @statelyai/agent skill and keep the four steps in order — make it a state machine, add traces, add evals, self-improve. Use when asked to "make my agent reliable", "graph engineering", "turn my agent into a state machine and improve it", "where do I start with this agent", or any request that spans more than one of those steps.
4
+ ---
5
+
6
+ # Graph engineering
7
+
8
+ Four steps, in order. Each one needs the one before it.
9
+
10
+ | Step | Skill | Done when |
11
+ | --- | --- | --- |
12
+ | 1. Make it a state machine | `loop-to-machine` (existing code) or `generate-machine` (from scratch) | Rules are guards and the sentinel is unreachable (`canReach`). From existing code: the parity report has no unreviewed regressions. From scratch: every `verifyAgentConfig` gate passes |
13
+ | 2. Add traces | `add-traces` | Every run leaves a stamped log thread that replays to its final state |
14
+ | 3. Add evals | `add-evals` | Evals per state at the cheapest level, an objective, baseline numbers per split |
15
+ | 4. Self-improve | `improve-machine` | A pull request with one policy change and its held-out scorecard, or a report that none helped |
16
+
17
+ ## Find where the project is
18
+
19
+ Check in this order and start at the first step that is not done:
20
+
21
+ 1. Is the agent's control flow a machine (`setupAgent`, `createMachine`, an `AgentWorkflowConfig`)? If it is a loop or ad-hoc code, step 1.
22
+ 2. Do runs persist to an event log store with a declared machine `version` and a `metadata` stamp? If not, step 2.
23
+ 3. Is there an objective over ground truth, and evals that name the states they cover? If not, step 3.
24
+ 4. Otherwise, step 4.
25
+
26
+ Tell the user which step you are starting at and why, in one line, then load that skill.
27
+
28
+ ## Across all steps
29
+
30
+ - **The environment is people's code.** Rules, guards, invariants, and the sentinel change only by human review. No step rewrites them on its own.
31
+ - **Ground truth scores everything.** Outcomes and effects in the world, labeled by people. A model never grades its own runs.
32
+ - **Ask for labels and rules; decide the mechanics.** Expected outcomes, thresholds, the objective, and the train/held-out split are the user's. File layout, test names, and helper code are yours.
33
+ - **Hand back between steps.** Finish one step's hand-back before starting the next, unless the user asked for several in one go.