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

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 (69) hide show
  1. package/CHANGELOG.md +1733 -0
  2. package/dist/ai-sdk.cjs +9 -9
  3. package/dist/ai-sdk.d.cts +5 -5
  4. package/dist/ai-sdk.d.mts +5 -5
  5. package/dist/ai-sdk.mjs +4 -4
  6. package/dist/{decision-Bl_rKQnU.mjs → decision-DqO018Tp.mjs} +23 -10
  7. package/dist/{decision-oevZnkBe.cjs → decision-Iys2hzg3.cjs} +31 -18
  8. package/dist/{event-log-Dtb16p1p.cjs → event-log-Bdww2l1D.cjs} +104 -25
  9. package/dist/{event-log-GPZO9p7_.mjs → event-log-DE430ZCa.mjs} +87 -26
  10. package/dist/{event-log-store-bCdskPNS.cjs → event-log-store-BWMLi6oh.cjs} +1 -1
  11. package/dist/{event-log-store-yalq8C77.mjs → event-log-store-CCWdTPEP.mjs} +1 -1
  12. package/dist/{event-log-store-CN9ygggF.d.cts → event-log-store-DTf_OzB-.d.cts} +1 -1
  13. package/dist/{event-log-store-DS836wch.d.mts → event-log-store-RziIobdW.d.mts} +1 -1
  14. package/dist/index.cjs +136 -23
  15. package/dist/index.d.cts +58 -11
  16. package/dist/index.d.mts +58 -11
  17. package/dist/index.mjs +133 -21
  18. package/dist/log.cjs +2 -2
  19. package/dist/log.d.cts +1 -1
  20. package/dist/log.d.mts +1 -1
  21. package/dist/log.mjs +2 -2
  22. package/dist/machines.cjs +1 -1
  23. package/dist/machines.mjs +1 -1
  24. package/dist/{model-source-C7FFP2Sj.mjs → model-source-CXsSZPq0.mjs} +1 -2
  25. package/dist/{model-source-DjJPjGSP.cjs → model-source-DQ7KgJaT.cjs} +4 -5
  26. package/dist/openai.cjs +3 -3
  27. package/dist/openai.d.cts +2 -2
  28. package/dist/openai.d.mts +2 -2
  29. package/dist/openai.mjs +3 -3
  30. package/dist/otel.d.cts +1 -1
  31. package/dist/otel.d.mts +1 -1
  32. package/dist/{run-agent-C9ubX8Tx.d.cts → run-agent-B5opVUxx.d.cts} +25 -6
  33. package/dist/{run-agent-CxSyZmIE.d.mts → run-agent-D4MwS5GY.d.mts} +25 -6
  34. package/dist/{run-agent-CuQT1xSS.mjs → run-agent-DWZ1uyBJ.mjs} +100 -37
  35. package/dist/{run-agent-cToZ3K42.cjs → run-agent-tmk58M0x.cjs} +100 -37
  36. package/dist/{setup-agent-DniLvDu7.mjs → setup-agent-BaffhCcM.mjs} +38 -3
  37. package/dist/{setup-agent-tsc3vm5I.d.mts → setup-agent-CPyg2ic7.d.mts} +11 -3
  38. package/dist/{setup-agent-DTF3NQg3.cjs → setup-agent-D0L8Anl_.cjs} +38 -3
  39. package/dist/{setup-agent-DlbgwsIJ.d.cts → setup-agent-yFFVXf5s.d.cts} +11 -3
  40. package/dist/tanstack-ai.cjs +3 -3
  41. package/dist/tanstack-ai.d.cts +2 -2
  42. package/dist/tanstack-ai.d.mts +2 -2
  43. package/dist/tanstack-ai.mjs +3 -3
  44. package/dist/testing.cjs +176 -26
  45. package/dist/testing.d.cts +128 -10
  46. package/dist/testing.d.mts +128 -10
  47. package/dist/testing.mjs +168 -20
  48. package/dist/{text-logic-BDbGjYMh.d.mts → text-logic-BwdrDpO9.d.mts} +6 -2
  49. package/dist/{text-logic-9fv1i9aK.d.cts → text-logic-j9hShB7d.d.cts} +6 -2
  50. package/dist/{usage-D8UphgEA.d.mts → usage-BYqv7VBw.d.cts} +1 -1
  51. package/dist/{usage-R1NTmLxY.d.cts → usage-Bl4nK8D-.d.mts} +1 -1
  52. package/dist/validate.cjs +3 -3
  53. package/dist/validate.d.cts +2 -2
  54. package/dist/validate.d.mts +2 -2
  55. package/dist/validate.mjs +3 -3
  56. package/dist/{verify-B5qGC5WN.mjs → verify-BH5e0XLK.mjs} +55 -5
  57. package/dist/{verify-DYH_2VzU.d.cts → verify-CPKDPMwY.d.cts} +14 -6
  58. package/dist/{verify-Cqghu4gz.cjs → verify-CS_xxvVU.cjs} +57 -7
  59. package/dist/{verify-CBuYIpsa.d.mts → verify-tkymWz6X.d.mts} +14 -6
  60. package/package.json +34 -4
  61. package/readme.md +72 -42
  62. package/skills/add-evals/SKILL.md +144 -0
  63. package/skills/add-traces/SKILL.md +147 -0
  64. package/skills/generate-machine/SKILL.md +6 -6
  65. package/skills/graph-engineering/SKILL.md +33 -0
  66. package/skills/improve-machine/SKILL.md +73 -21
  67. package/skills/improve-machine/reference.md +12 -7
  68. package/skills/loop-to-machine/SKILL.md +210 -0
  69. package/skills/loop-to-machine/policy.md +154 -0
@@ -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-BwdrDpO9.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. */
@@ -10,7 +10,7 @@ type AgentLintSeverity = "error" | "warning";
10
10
  * or config location, and `message` explains the problem and its remedy.
11
11
  */
12
12
  interface AgentLintDiagnostic {
13
- code: "decide-without-events" | "direct-object-src" | "invoke-without-on-error";
13
+ code: "decide-without-events" | "decide-allows-human-event" | "direct-object-src" | "invoke-without-on-error";
14
14
  severity: AgentLintSeverity;
15
15
  /** State path (`parent.child`) or config pointer (e.g. `(root)`, `context`) the finding is about. */
16
16
  path: string;
@@ -212,6 +212,14 @@ interface SimulateAgentResult {
212
212
  */
213
213
  declare function simulateAgent(machine: AnyStateMachine, options: SimulateAgentOptions): Promise<SimulateAgentResult>;
214
214
  /** Options for {@link explorePaths}. */
215
+ /**
216
+ * The snapshot type the exploration callbacks (`payloads`, `dedupeBy`, a
217
+ * `canReach` predicate) receive. Import it from here rather than from your
218
+ * own `xstate`, which can be another version than the one this package uses.
219
+ */
220
+ type ExploreSnapshot = AnyMachineSnapshot;
221
+ /** What {@link canReach} looks for: a state id or path, or a snapshot predicate. */
222
+ type CanReachTarget = string | ((snapshot: ExploreSnapshot) => boolean);
215
223
  interface ExplorePathsOptions {
216
224
  input?: unknown;
217
225
  /** Max branch points (decisions + external-event forks) along any path. Default 8. */
@@ -274,7 +282,7 @@ interface ExplorePathsOptions {
274
282
  * were not all tried, so it is listed in `unexpandedEvents`. A type with no
275
283
  * schema at all is applied as `{ type }`.
276
284
  */
277
- payloads?: (snapshot: AnyMachineSnapshot) => Partial<Record<string, Record<string, unknown>[]>>;
285
+ payloads?: (snapshot: ExploreSnapshot) => Partial<Record<string, Record<string, unknown>[]>>;
278
286
  /**
279
287
  * A projection of the snapshot that determines its future. Settled
280
288
  * snapshots with an equal key are explored once (again only when reached
@@ -283,7 +291,7 @@ interface ExplorePathsOptions {
283
291
  * a key that drops a field the machine reads makes the search unsound.
284
292
  * Without it, every path is explored in full.
285
293
  */
286
- dedupeBy?: (snapshot: AnyMachineSnapshot) => string;
294
+ dedupeBy?: (snapshot: ExploreSnapshot) => string;
287
295
  }
288
296
  /** A single explored path's terminal outcome. */
289
297
  interface AgentPathTerminal {
@@ -409,6 +417,6 @@ declare class AgentUnknownStateError extends AgentError {
409
417
  * // violation.reachable → false is the safety proof
410
418
  * ```
411
419
  */
412
- declare function canReach(machine: AnyStateMachine, target: string | ((snapshot: AnyMachineSnapshot) => boolean), options?: ExplorePathsOptions): Promise<CanReachResult>;
420
+ declare function canReach(machine: AnyStateMachine, target: CanReachTarget, options?: ExplorePathsOptions): Promise<CanReachResult>;
413
421
  //#endregion
414
- export { explorePaths as _, AgentPathTerminal as a, CanReachResult as c, SimulateAgentOptions as d, SimulateAgentResult as f, canReach as g, assertAgentMachine as h, AgentPathReport as i, ExplorePathsOptions as l, SimulationTrailEntry as m, AgentLintError as n, AgentUnknownStateError as o, SimulationScript as p, AgentLintSeverity as r, AssertAgentMachineOptions as s, AgentLintDiagnostic as t, LintAgentMachineOptions as u, lintAgentMachine as v, simulateAgent as y };
422
+ export { assertAgentMachine as _, AgentPathTerminal as a, lintAgentMachine as b, CanReachResult as c, ExploreSnapshot as d, LintAgentMachineOptions as f, SimulationTrailEntry as g, SimulationScript as h, AgentPathReport as i, CanReachTarget as l, SimulateAgentResult as m, AgentLintError as n, AgentUnknownStateError as o, SimulateAgentOptions as p, AgentLintSeverity as r, AssertAgentMachineOptions as s, AgentLintDiagnostic as t, ExplorePathsOptions as u, canReach as v, simulateAgent as x, explorePaths as y };
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.31",
4
4
  "description": "Make invalid agent actions impossible. Agent logic as state machines: deterministic, inspectable, resumable, runs anywhere.",
5
5
  "keywords": [
6
6
  "agent",
@@ -21,12 +21,41 @@
21
21
  "files": [
22
22
  "dist",
23
23
  "schemas",
24
- "skills"
24
+ "skills",
25
+ "CHANGELOG.md"
25
26
  ],
26
27
  "type": "module",
27
28
  "main": "dist/index.cjs",
28
29
  "module": "dist/index.mjs",
29
30
  "types": "dist/index.d.mts",
31
+ "typesVersions": {
32
+ "*": {
33
+ "ai-sdk": [
34
+ "./dist/ai-sdk.d.mts"
35
+ ],
36
+ "log": [
37
+ "./dist/log.d.mts"
38
+ ],
39
+ "machines": [
40
+ "./dist/machines.d.mts"
41
+ ],
42
+ "openai": [
43
+ "./dist/openai.d.mts"
44
+ ],
45
+ "otel": [
46
+ "./dist/otel.d.mts"
47
+ ],
48
+ "tanstack-ai": [
49
+ "./dist/tanstack-ai.d.mts"
50
+ ],
51
+ "testing": [
52
+ "./dist/testing.d.mts"
53
+ ],
54
+ "validate": [
55
+ "./dist/validate.d.mts"
56
+ ]
57
+ }
58
+ },
30
59
  "exports": {
31
60
  ".": {
32
61
  "import": {
@@ -132,6 +161,7 @@
132
161
  "@langchain/core": "^1.2.17",
133
162
  "@langchain/openai": "^1.6.2",
134
163
  "@mastra/core": "^1.75.0",
164
+ "@modelcontextprotocol/sdk": "^1.29.0",
135
165
  "@opentelemetry/api": "^1.9.1",
136
166
  "@opentelemetry/context-async-hooks": "^2.12.0",
137
167
  "@opentelemetry/sdk-trace-base": "^2.12.0",
@@ -159,7 +189,7 @@
159
189
  "peerDependencies": {
160
190
  "@opentelemetry/api": "^1",
161
191
  "@tanstack/ai": ">=0.66.0 <1",
162
- "ai": "^7.0.0",
192
+ "ai": "^7.0.128",
163
193
  "ajv": "^8.20.0",
164
194
  "openai": ">=5.0.0 <8",
165
195
  "xstate": "6.0.0-alpha.69"
@@ -182,7 +212,7 @@
182
212
  }
183
213
  },
184
214
  "engines": {
185
- "node": ">=22.18.0"
215
+ "node": ">=22"
186
216
  },
187
217
  "scripts": {
188
218
  "build": "tsdown",
package/readme.md CHANGED
@@ -14,25 +14,25 @@ Environment and policy:
14
14
 
15
15
  - The environment is code you own and can verify; the policy is something a model can write inside it.
16
16
  - The environment holds the rules and invariants as TypeScript states and guards, fixed and checked with `canReach`.
17
- - The policy is a JSON machine a model can author and improve from traces, gated before it runs and promoted only on a measured gain. See [Environment and policy](docs/environment-and-policy.md).
17
+ - The policy is a JSON machine a model can author and improve from traces, gated before it runs and promoted only on a measured gain. See [Environment and policy](https://github.com/statelyai/agent/blob/next/docs/environment-and-policy.md).
18
18
 
19
19
  Stately Agent 2 is in alpha. APIs may change before the stable release.
20
20
 
21
- [Documentation](https://stately.ai/docs/packages/agent) · [Examples](examples/README.md) · [XState](https://github.com/statelyai/xstate)
21
+ [Documentation](https://stately.ai/docs/packages/agent) · [Examples](https://github.com/statelyai/agent/blob/next/examples/README.md) · [XState](https://github.com/statelyai/xstate)
22
22
 
23
23
  ## Four starting points
24
24
 
25
- - **Author a new agent.** Build a machine from states, decisions, and typed requests; run it locally with `runAgent`, test it with no API key, then use it in any framework or runtime with zero machine changes. See the [Quickstart](docs/quickstart.md) and [Use in any stack](docs/any-stack.md).
26
- - **Retrofit an existing agent.** Turn a `while` loop into a machine: your SDK calls, tools, and retry code become the executors; the machine replaces only the control flow. See [Migrating from a hand-rolled loop](docs/from-a-loop.md).
27
- - **Copy a known pattern.** ReAct, reflection, plan-and-execute, RAG, supervisor, and more, each a single runnable file you lift in 60 seconds. See [Agent patterns](docs/patterns.md).
28
- - **Let an agent improve its own logic.** Keep the rules in a fixed TypeScript environment and let a model rewrite the JSON policy inside it, gated by `verifyAgentConfig` and promoted only when held-out scenarios improve. See [Environment and policy](docs/environment-and-policy.md) and the [`improve-machine` skill](skills/improve-machine/SKILL.md).
25
+ - **Author a new agent.** Build a machine from states, decisions, and typed requests; run it locally with `runAgent`, test it with no API key, then use it in any framework or runtime with zero machine changes. See the [Quickstart](https://github.com/statelyai/agent/blob/next/docs/quickstart.md) and [Use in any stack](https://github.com/statelyai/agent/blob/next/docs/any-stack.md).
26
+ - **Retrofit an existing agent.** Turn a `while` loop into a machine: your SDK calls, tools, and retry code become the executors; the machine replaces only the control flow. See [Migrating from a hand-rolled loop](https://github.com/statelyai/agent/blob/next/docs/from-a-loop.md).
27
+ - **Copy a known pattern.** ReAct, reflection, plan-and-execute, RAG, supervisor, and more, each a single runnable file you lift in 60 seconds. See [Agent patterns](https://github.com/statelyai/agent/blob/next/docs/patterns.md).
28
+ - **Let an agent improve its own logic.** Keep the rules in a fixed TypeScript environment and let a model rewrite the JSON policy inside it, gated by `verifyAgentConfig` and promoted only when held-out scenarios improve. See [Environment and policy](https://github.com/statelyai/agent/blob/next/docs/environment-and-policy.md) and the [`improve-machine` skill](https://github.com/statelyai/agent/blob/next/skills/improve-machine/SKILL.md).
29
29
 
30
30
  ## Install
31
31
 
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.
@@ -40,7 +40,7 @@ XState is pinned while its experimental durable restoration API evolves.
40
40
  For the optional Vercel AI SDK executor:
41
41
 
42
42
  ```sh
43
- pnpm add ai@^7 @ai-sdk/openai@^4
43
+ pnpm add ai@^7.0.128 @ai-sdk/openai@^4
44
44
  ```
45
45
 
46
46
  For the optional raw OpenAI SDK executor (`@statelyai/agent/openai`):
@@ -55,13 +55,43 @@ For the optional TanStack AI executor (`@statelyai/agent/tanstack-ai`):
55
55
  pnpm add @tanstack/ai @tanstack/ai-openai
56
56
  ```
57
57
 
58
- Classification, grading, relevance, and yes/no checks are judgments: call the AI SDK's `experimental_decide` with `@ai-sdk/typesafe-ai` (`pnpm add ai @ai-sdk/typesafe-ai`) from an ordinary actor; see [Judgments with the AI SDK](docs/judgments.md).
58
+ Classification, grading, relevance, and yes/no checks are judgments: call the AI SDK's `experimental_decide` with `@ai-sdk/typesafe-ai` (`pnpm add ai @ai-sdk/typesafe-ai`) from an ordinary actor; see [Judgments with the AI SDK](https://github.com/statelyai/agent/blob/next/docs/judgments.md).
59
+
60
+ Upgrading from an earlier alpha: see [Upgrading between alphas](https://github.com/statelyai/agent/blob/next/docs/upgrading.md). Every release's notes are in `CHANGELOG.md`, which ships in the package.
59
61
 
60
62
  Requirements:
61
63
 
62
- - Node 22.18 or newer, and XState v6 alpha.46 or newer.
63
- - The package is ESM-first. CommonJS builds are published too, so `require()` works.
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.
64
+ - Node 22 or newer, and XState exactly `6.0.0-alpha.69`.
65
+ - TypeScript 5.4 or newer. Older versions accept the code, but callback parameters in `createMachine` silently become `any`.
66
+ - A [Standard Schema](https://standardschema.dev) library: zod 4, zod 3.24 or newer, Valibot, or ArkType. `setupAgent` throws `invalid-schema` for zod 3.23 and earlier.
67
+ - The package is ESM-first. CommonJS builds are published too, so `require()` works. Subpath imports (`@statelyai/agent/testing`) resolve under every `moduleResolution`, including `node`.
68
+ - `@statelyai/agent/ai-sdk` needs `ai` 7.0.128 or newer. 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. `@statelyai/agent/openai` needs `openai` 5 or newer.
69
+
70
+ An app that already uses XState 4 or 5 keeps it: put the agent in its own workspace package. See [Use in an XState 4 or 5 app](https://github.com/statelyai/agent/blob/next/docs/xstate-4-and-5-apps.md). If both versions land in one `node_modules`, the package fails with errors that do not mention a version:
71
+
72
+ | Error | Cause |
73
+ | --- | --- |
74
+ | `createAsyncLogic is not a function` | `xstate` resolved to 4 or 5 from CommonJS |
75
+ | `Cannot find module '…/node_modules/xstate/durable'` | `xstate` resolved to 4 or 5 from ESM |
76
+ | `unmet peer dependency "xstate@6.0.0-alpha.69"` (pnpm, npm) or `has incorrect peer dependency "xstate@6.0.0-alpha.69"` (yarn 1) | The install's only warning of the two above |
77
+
78
+ ## Agent skills
79
+
80
+ The package ships agent skills in `node_modules/@statelyai/agent/skills/`. Copy them into your coding agent's skills directory (for Claude Code, `.claude/skills/`):
81
+
82
+ ```sh
83
+ mkdir -p .claude/skills
84
+ cp -R node_modules/@statelyai/agent/skills/* .claude/skills/
85
+ ```
86
+
87
+ | Skill | What it does |
88
+ | --- | --- |
89
+ | [`graph-engineering`](https://github.com/statelyai/agent/blob/next/skills/graph-engineering/SKILL.md) | Finds which step a project is at and routes to the next skill |
90
+ | [`loop-to-machine`](https://github.com/statelyai/agent/blob/next/skills/loop-to-machine/SKILL.md) | Converts existing agent code into a machine and checks parity with `compareRuns` |
91
+ | [`generate-machine`](https://github.com/statelyai/agent/blob/next/skills/generate-machine/SKILL.md) | Authors a JSON policy machine from scratch and runs `verifyAgentConfig` |
92
+ | [`add-traces`](https://github.com/statelyai/agent/blob/next/skills/add-traces/SKILL.md) | Stamps and persists every run so it replays |
93
+ | [`add-evals`](https://github.com/statelyai/agent/blob/next/skills/add-evals/SKILL.md) | Writes evals per state, at the cheapest level that catches the failure |
94
+ | [`improve-machine`](https://github.com/statelyai/agent/blob/next/skills/improve-machine/SKILL.md) | Improves a policy from traces and hands the change to a person as a pull request |
65
95
 
66
96
  ## Quick start
67
97
 
@@ -151,7 +181,7 @@ import { openai } from "@ai-sdk/openai";
151
181
  import { createAiSdkExecutors } from "@statelyai/agent/ai-sdk";
152
182
 
153
183
  // In the setupAgent({ ... }) call above:
154
- // models: { fast: openai("gpt-5.4-mini") },
184
+ // models: { fast: openai("gpt-6-luna") },
155
185
 
156
186
  const liveResult = await runAgent(refundMachine, {
157
187
  input: { request: "I was charged twice for the same order.", amount: 75 },
@@ -159,11 +189,11 @@ const liveResult = await runAgent(refundMachine, {
159
189
  });
160
190
  ```
161
191
 
162
- `createAiSdkExecutors(refundMachine)` reads the `models` declared in `setupAgent({ models })`, which also type the machine's model refs. `createAiSdkExecutors({ models })` still works when the host owns the map. Executors are always explicit, and core does not import the AI SDK. See [Models and providers](docs/models-and-providers.md) and [Hosts and executors](docs/hosts.md).
192
+ `createAiSdkExecutors(refundMachine)` reads the `models` declared in `setupAgent({ models })`, which also type the machine's model refs. `createAiSdkExecutors({ models })` still works when the host owns the map. Executors are always explicit, and core does not import the AI SDK. See [Models and providers](https://github.com/statelyai/agent/blob/next/docs/models-and-providers.md) and [Hosts and executors](https://github.com/statelyai/agent/blob/next/docs/hosts.md).
163
193
 
164
- For a machine with several requests, the executor routes on `request.name`, or `MockLanguageModelV3` from `ai/test` stands in for the provider behind `createAiSdkExecutors`. See [Evals](docs/evals.md#testing-without-a-provider).
194
+ For a machine with several requests, the executor routes on `request.name`, or `MockLanguageModelV3` from `ai/test` stands in for the provider behind `createAiSdkExecutors`. See [Evals](https://github.com/statelyai/agent/blob/next/docs/evals.md#testing-without-a-provider).
165
195
 
166
- A host that needs the transition loop itself (a durable object, events pushed mid-run, a custom checkpoint policy) uses `createAgentRuntime` directly. See [Write your own host loop](docs/hosts.md#write-your-own-host-loop).
196
+ A host that needs the transition loop itself (a durable object, events pushed mid-run, a custom checkpoint policy) uses `createAgentRuntime` directly. See [Write your own host loop](https://github.com/statelyai/agent/blob/next/docs/hosts.md#write-your-own-host-loop).
167
197
 
168
198
  ## Architecture
169
199
 
@@ -181,38 +211,38 @@ The machine never talks to a model directly, so swapping `createAiSdkExecutors`
181
211
 
182
212
  The example above has one model decision and two final outcomes. Real machines add approval states, retries, parallel work, child agents, and long-running waits without changing how control flow is represented.
183
213
 
184
- The core concepts (machines owning control flow, bounded model decisions, typed requests, host-run executors, and native XState snapshots) are in the [documentation overview](docs/index.md).
214
+ The core concepts (machines owning control flow, bounded model decisions, typed requests, host-run executors, and native XState snapshots) are in the [documentation overview](https://github.com/statelyai/agent/blob/next/docs/index.md).
185
215
 
186
216
  ## Examples
187
217
 
188
218
  <!-- starter examples derived from examples/*/metadata.json and examples/index.ts -->
189
219
 
190
- - [Twenty Questions](examples/twenty-questions) shows a model choosing legal events in a loop.
191
- - [Chameleon](examples/chameleon) combines hidden information, sequential turns, and a human vote.
192
- - [Snapshot migration](examples/snapshot-migration) uses XState's native `version` and `migrate` contract.
193
- - [Portable XState loop](examples/portable-xstate-loop) runs the artifact through native transitions and effects.
194
- - [Application-owned storage](examples/file-snapshot-store) persists native XState snapshots in application code.
195
- - [Tool calling](examples/review-tool-calls) keeps tools and message history in the AI SDK's native format.
196
- - [Ticket triage](examples/triage) returns structured data from a model request.
197
- - [JSON agent](examples/json-agent) runs a machine defined as data.
220
+ - [Twenty Questions](https://github.com/statelyai/agent/tree/next/examples/twenty-questions) shows a model choosing legal events in a loop.
221
+ - [Chameleon](https://github.com/statelyai/agent/tree/next/examples/chameleon) combines hidden information, sequential turns, and a human vote.
222
+ - [Snapshot migration](https://github.com/statelyai/agent/tree/next/examples/snapshot-migration) uses XState's native `version` and `migrate` contract.
223
+ - [Portable XState loop](https://github.com/statelyai/agent/tree/next/examples/portable-xstate-loop) runs the artifact through native transitions and effects.
224
+ - [Application-owned storage](https://github.com/statelyai/agent/tree/next/examples/file-snapshot-store) persists native XState snapshots in application code.
225
+ - [Tool calling](https://github.com/statelyai/agent/tree/next/examples/review-tool-calls) keeps tools and message history in the AI SDK's native format.
226
+ - [Ticket triage](https://github.com/statelyai/agent/tree/next/examples/triage) returns structured data from a model request.
227
+ - [JSON agent](https://github.com/statelyai/agent/tree/next/examples/json-agent) runs a machine defined as data.
198
228
 
199
- See [all examples](examples/README.md).
229
+ See [all examples](https://github.com/statelyai/agent/blob/next/examples/README.md).
200
230
 
201
231
  ## Related
202
232
 
203
- - [Machines](docs/machines.md)
204
- - [Text requests](docs/text-requests.md)
205
- - [Decisions](docs/decisions.md)
206
- - [Human in the loop](docs/human-in-the-loop.md)
207
- - [Testing and verification](docs/verify.md)
208
- - [Evals](docs/evals.md)
209
- - [Generating machines with an LLM](docs/generate-machines.md)
210
- - [Hosts and executors](docs/hosts.md)
211
- - [Models and providers](docs/models-and-providers.md)
212
- - [Use in any stack](docs/any-stack.md)
213
- - [Persistence](docs/persistence.md)
214
- - [Observability](docs/observability.md)
215
- - [Usage and budgets](docs/usage-and-budgets.md)
216
- - [Agent patterns](docs/patterns.md)
217
- - [Migrating from a hand-rolled loop](docs/from-a-loop.md)
218
- - [Coming from LangGraph](docs/langgraph-comparison.md)
233
+ - [Machines](https://github.com/statelyai/agent/blob/next/docs/machines.md)
234
+ - [Text requests](https://github.com/statelyai/agent/blob/next/docs/text-requests.md)
235
+ - [Decisions](https://github.com/statelyai/agent/blob/next/docs/decisions.md)
236
+ - [Human in the loop](https://github.com/statelyai/agent/blob/next/docs/human-in-the-loop.md)
237
+ - [Testing and verification](https://github.com/statelyai/agent/blob/next/docs/verify.md)
238
+ - [Evals](https://github.com/statelyai/agent/blob/next/docs/evals.md)
239
+ - [Generating machines with an LLM](https://github.com/statelyai/agent/blob/next/docs/generate-machines.md)
240
+ - [Hosts and executors](https://github.com/statelyai/agent/blob/next/docs/hosts.md)
241
+ - [Models and providers](https://github.com/statelyai/agent/blob/next/docs/models-and-providers.md)
242
+ - [Use in any stack](https://github.com/statelyai/agent/blob/next/docs/any-stack.md)
243
+ - [Persistence](https://github.com/statelyai/agent/blob/next/docs/persistence.md)
244
+ - [Observability](https://github.com/statelyai/agent/blob/next/docs/observability.md)
245
+ - [Usage and budgets](https://github.com/statelyai/agent/blob/next/docs/usage-and-budgets.md)
246
+ - [Agent patterns](https://github.com/statelyai/agent/blob/next/docs/patterns.md)
247
+ - [Migrating from a hand-rolled loop](https://github.com/statelyai/agent/blob/next/docs/from-a-loop.md)
248
+ - [Coming from LangGraph](https://github.com/statelyai/agent/blob/next/docs/langgraph-comparison.md)
@@ -0,0 +1,144 @@
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`. A machine whose model choice is the output of a text request (a tool call it then checks) has no decision states: eval that choice at the **request** level with `runSeam`;
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
+ Draft labels gate nothing, but rule evals need no labels, so they gate CI from day one. The label-based evals start gating as the user confirms labels (`draft: false`). Say so in the hand-back, so nobody reads a green CI as "the outcome evals pass".
61
+
62
+ ## 4. Write them
63
+
64
+ Name every eval `<state>: <what it checks>`, so the report reads as a map of the machine. An outcome eval over a whole run takes the final state it expects (`issued: partial-refund refunds 50 dollars`). In `test.each` titles, `$name` is a placeholder: write amounts as `100 dollars`, not `$100`.
65
+
66
+ **Rule evals**, from `@statelyai/agent/testing`:
67
+
68
+ ```ts
69
+ test("paying: no refund over 100 dollars issues without approval", async () => {
70
+ const result = await canReach(machine, "<sentinel>", { input, payloads, outputs, errors });
71
+ expect(result.complete).toBe(true); // reachable: false is a proof only when the search finished
72
+ expect(result.reachable, JSON.stringify(result.witness)).toBe(false);
73
+ });
74
+
75
+ test("deciding: REFUND over the order amount never reaches paying", async () => {
76
+ const result = await canReach(machine, (s) => s.matches("paying") && s.context.amount > s.context.order.amount, { input, payloads, outputs });
77
+ expect(result.complete).toBe(true);
78
+ expect(result.reachable).toBe(false);
79
+ });
80
+ ```
81
+
82
+ Check guards through the machine, not by importing the guard and calling it: an imported `refundIsLegal` is always the one in the source, so it passes on a machine built with a loosened guard. A unit test of the guard function is fine as well, never instead.
83
+
84
+ 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:
85
+
86
+ - `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.
87
+ - `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.
88
+
89
+ **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 with `getDecisionRequests(trace, { name })` from `@statelyai/agent/testing`: each decision's first attempt, with the states it was asked in, its `options`, and its `metadata`. 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.
90
+
91
+ ```ts
92
+ import { getDecisionRequests } from "@statelyai/agent/testing";
93
+
94
+ // One row per recorded decision; `expected` is the label a person confirmed for that decision.
95
+ // A scenario can decide more than once, so label each decision by its position, not the scenario alone.
96
+ const rows = traces.flatMap(({ scenario, trace }) =>
97
+ getDecisionRequests(trace, { name: "chooseResolution" }).map(({ request }, i) => ({
98
+ id: `${scenario.id}#${i}`,
99
+ request,
100
+ expected: labels[`${scenario.id}#${i}`],
101
+ })),
102
+ );
103
+
104
+ test.each(rows)("deciding: $id", async ({ request, expected }) => {
105
+ const { event } = await decide(request); // the policy decider, or the model executor
106
+ expect(event).toEqual(expected);
107
+ });
108
+ ```
109
+
110
+ 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.
111
+
112
+ **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.
113
+
114
+ **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.
115
+
116
+ ```ts
117
+ export function objective(runs: Array<{ scenario: Scenario; outcome: string; refund?: Refund; violations: number; modelCalls: number }>) {
118
+ return {
119
+ accuracy: runs.filter((r) => r.outcome === r.scenario.expected && sameRefund(r.refund, r.scenario.expectedRefund)).length / runs.length,
120
+ violations: runs.reduce((n, r) => n + r.violations, 0), // must be 0
121
+ modelCalls: runs.reduce((n, r) => n + r.modelCalls, 0), // cost
122
+ };
123
+ }
124
+ ```
125
+
126
+ 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`.
127
+
128
+ **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.
129
+
130
+ ## 5. Coverage
131
+
132
+ 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.
133
+
134
+ ## 6. Baseline
135
+
136
+ 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.
137
+
138
+ ## 7. Hand back
139
+
140
+ 1. The state mapping from step 2.
141
+ 2. The evals by level, each named by state.
142
+ 3. The baseline numbers, train and held-out separately.
143
+ 4. Every failing eval and the state it points at.
144
+ 5. Next step: `improve-machine`, with the objective from step 4 as its scorer.
@@ -0,0 +1,147 @@
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. An `onTrace` or `onTransition` handler that throws is reported with `console.error` and skipped. The store is different: it is write-ahead, so a failed write stops the run with `cause: "journal"`. Ask the user whether a run should fail when its log cannot be written (the store) or carry on unrecorded (record from `onEvent` instead).
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**: whatever produces the model's answers. It is the policy machine's `version` when there is one. Otherwise it is a label for the model, its settings, and its prompts (`gpt-6-luna.xhigh.p<hash of the prompts>`); this holds for a machine with only text requests too. When the prompts are private to another module (a route file), export a prompt version from that module rather than guessing; a release or commit id is the fallback. In tests it is the stand-in model's name (`careful`). Runs by different deciders must never share a 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
+ - A test-only variant built from the same factory (a loosened guard) gets its own `version` (`"1-loose"`), so its runs are never mistaken for the real machine's.
41
+
42
+ ## 2. Pick a store
43
+
44
+ - **Tests and local runs:** `createInMemoryEventLogStore()` from `@statelyai/agent/log`, or write each thread's `result.events` to a JSON file after the run.
45
+ - **Production:** implement `AgentEventLogStore` against your database and check it with `assertEventLogStoreConformance`. Implement the optional `listThreads` so a split can be paged by prefix.
46
+
47
+ Pass it with a `threadId`. The store is write-ahead: no model call starts until the entries before it are durable.
48
+
49
+ ```ts
50
+ const result = await runAgent(deskMachine, { input, executors, store, threadId, metadata });
51
+ await result.drain(); // late usage entries, before you read the store or exit
52
+ ```
53
+
54
+ **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. Passing different `input` to a thread that already has entries throws `thread-input-mismatch` instead of handing back another run's result.
55
+
56
+ For a production store, use a table of its own (`threadId`, `index`, the entry as JSON, a unique key on both), not a table that already means something else, such as a usage counter.
57
+
58
+ ## 3. Name threads
59
+
60
+ A thread id is how runs are found later. Use a prefix scheme and keep it in one function:
61
+
62
+ | Runs | Thread id |
63
+ | ------------------------ | -------------------------------------------- |
64
+ | Scenario runs for evals | `<split>:<deciderVersion>:<scenarioId>:r<k>`, `k` = the repeat index |
65
+ | Production | `prod:<domain id>` (ticket id, session id) |
66
+
67
+ `store.listThreads?.({ prefix: "train:careful:" })` then pages through one split of one decider. `listThreads` is optional on a custom store.
68
+
69
+ ## 4. Stamp each run
70
+
71
+ `metadata` lands on the log's first (`@agent.init`) entry. Put there everything needed to tell two runs' producers apart:
72
+
73
+ ```ts
74
+ const metadata = {
75
+ environmentVersion: deskMachine.version ?? "unversioned",
76
+ deciderVersion, // see step 1
77
+ models: { decider: "openai/gpt-6-luna" }, // the machine's model refs → what the host resolved them to
78
+ scenario: scenario.id, // eval runs only
79
+ split: scenario.split, // eval runs only
80
+ };
81
+ ```
82
+
83
+ Read it back with `entries[0].metadata`. It also holds `executionId` (and `migratedFrom` after a version bridge), which the library adds and reserves, so compare with `toMatchObject`, not `toEqual`. A resumed run keeps the stamp of the run that started the log. Type the stamp as `Record<string, JsonValue>` (`JsonValue` from `@statelyai/agent/log`); an interface without an index signature does not fit.
84
+
85
+ ## 5. Collect trace events
86
+
87
+ `onTrace` sees every trace event. Serialize and keep them per thread:
88
+
89
+ ```ts
90
+ import { serializeTraceEvent, type JsonSerializableTraceEvent } from "@statelyai/agent";
91
+
92
+ const trace: JsonSerializableTraceEvent[] = [];
93
+ const result = await runAgent(deskMachine, { ..., onTrace: (event) => trace.push(serializeTraceEvent(event)) });
94
+ await result.drain();
95
+
96
+ // For local batches: one file per thread. Encoding keeps colons and path separators out of the name.
97
+ const file = encodeURIComponent(threadId);
98
+ await mkdir("runs", { recursive: true });
99
+ await writeFile(`runs/${file}.events.json`, JSON.stringify(result.events));
100
+ await writeFile(`runs/${file}.trace.json`, JSON.stringify(trace));
101
+ ```
102
+
103
+ Decide with the user whether `runs/` is committed or gitignored.
104
+
105
+ - In production, send them to OpenTelemetry: `createOtelTraceHandler({ tracer })` from `@statelyai/agent/otel` is an `onTrace` handler. It needs the optional peer `@opentelemetry/api`; without OpenTelemetry in the app, write the serialized events next to the log instead.
106
+ - **A policy run inside a decider** is a separate run. Pass it the same `onTrace` (and a `store`) if you want per-state stats for the policy too. `createPolicyDecider` gives each decision's policy run its own thread, `policy:<callKey>:<SHA-256 of its input>`; never give it one fixed `threadId`, or the second decision resumes the first.
107
+ - **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.
108
+ - **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.
109
+
110
+ ## 6. Prove it
111
+
112
+ Write these tests, with the stand-in model from `loop-to-machine`:
113
+
114
+ ```ts
115
+ import { getDecisionAttempts, replay } from "@statelyai/agent/log";
116
+ import { foldTraces } from "@statelyai/agent/testing";
117
+
118
+ test("a recorded run replays to its final state with no model calls", async () => {
119
+ const result = await runAgent(deskMachine, { input, executors, store, threadId: "test:careful:small-clean:r1", metadata });
120
+ await result.drain();
121
+ const entries = await store.read("test:careful:small-clean:r1");
122
+ expect(entries[0]?.event.type).toBe("@agent.init"); // a log was written at all; a run with no outside input has only this entry
123
+ const { snapshot } = replay(deskMachine, entries, { verify: "strict" }); // a pure fold: nothing runs
124
+ // Compare more than the state value: context and output catch a field the log does not keep.
125
+ expect({ status: snapshot.status, value: snapshot.value, context: snapshot.context, output: snapshot.output }).toEqual({
126
+ status: result.snapshot.status,
127
+ value: result.snapshot.value,
128
+ context: result.snapshot.context,
129
+ output: result.snapshot.output,
130
+ });
131
+ expect(entries[0]!.metadata).toMatchObject({ environmentVersion: "1" });
132
+ });
133
+ ```
134
+
135
+ `verify: "strict"` checks that every entry carries a hash and that each matches the fold. It does not show the log is complete (a dropped last entry leaves the rest intact), nor that the fold matches the live run, which is why the test compares the snapshots too. A journaled `Error` replays as an `Error` with its `name`, `message`, and a string or number `code`; any other property of the thrown value is not kept. A thrown plain object replays as a plain object.
136
+
137
+ - `foldTraces(traces, { failed })` over a batch returns a row for every state a run rested in. A `choice` state the run passes through in the same step gets no row. `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. A serialized `run.end` is `{ type: "run.end", status, snapshot: { value, context, … } }`: `failed: (t) => { const end = t.at(-1); return end?.type !== "run.end" || end.status !== "done" || ["failed", "invariantBroken"].includes(String(end.snapshot?.value)); }`. `withheld` is above 0 only for a closed choice (`payloads`), where the guards refused an option.
138
+ - `getDecisionAttempts(entries)` lists the refused choices of a run; for a machine with no decisions (`agent.decide` or `decideNext`) it is always empty, so skip this test. 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 } })`.
139
+ - Running a scenario with and without recording leaves the same world, the same model calls, and the same final state, and the recorded run's log is not empty.
140
+
141
+ ## 7. Hand back
142
+
143
+ 1. Where runs are stored and how thread ids are built.
144
+ 2. The stamp fields.
145
+ 3. Where trace events go.
146
+ 4. The replay test passing.
147
+ 5. Next step: `add-evals`.