@statelyai/agent 2.0.0-alpha.28 → 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.
- package/dist/ai-sdk.cjs +150 -27
- package/dist/ai-sdk.d.cts +59 -36
- package/dist/ai-sdk.d.mts +59 -36
- package/dist/ai-sdk.mjs +148 -24
- package/dist/{decision-RQ0iv0oZ.mjs → decision-BHkvuImD.mjs} +74 -8
- package/dist/{decision-CmM8-HHz.cjs → decision-CIb7ddIv.cjs} +113 -30
- package/dist/{text-logic-f5F5sCdc.cjs → event-log-BZRfBRd7.cjs} +814 -7
- package/dist/{text-logic-CIqHAmBi.mjs → event-log-DJCMHek_.mjs} +680 -4
- package/dist/{event-log-store-BIPOz-di.d.mts → event-log-store-CIIJohjj.d.mts} +90 -5
- package/dist/{event-log-store-2xzFmgr2.d.cts → event-log-store-CLAC9q2Y.d.cts} +90 -5
- package/dist/event-log-store-DZR4IIWe.cjs +95 -0
- package/dist/event-log-store-DsP6GCRh.mjs +84 -0
- package/dist/index.cjs +576 -58
- package/dist/index.d.cts +104 -510
- package/dist/index.d.mts +104 -510
- package/dist/index.mjs +547 -32
- package/dist/log.cjs +77 -20
- package/dist/log.d.cts +5 -5
- package/dist/log.d.mts +5 -5
- package/dist/log.mjs +58 -3
- package/dist/machines.cjs +1 -1
- package/dist/machines.d.cts +15 -15
- package/dist/machines.d.mts +15 -15
- package/dist/machines.mjs +1 -1
- package/dist/model-source-BCKg4qJ1.cjs +44 -0
- package/dist/model-source-BaHwJ1wK.mjs +39 -0
- package/dist/openai.cjs +11 -11
- package/dist/openai.d.cts +28 -26
- package/dist/openai.d.mts +28 -26
- package/dist/openai.mjs +3 -3
- package/dist/otel.cjs +5 -4
- package/dist/otel.d.cts +5 -7
- package/dist/otel.d.mts +5 -7
- package/dist/otel.mjs +5 -3
- package/dist/{run-agent-xNTxpt82.mjs → run-agent-BpHqkaZw.mjs} +181 -44
- package/dist/{run-agent-COXXc0RW.d.mts → run-agent-CAVt5R-r.d.mts} +95 -30
- package/dist/{run-agent-ByO-Bxyd.d.cts → run-agent-CTlMc3Jb.d.cts} +95 -30
- package/dist/{run-agent-Dmf8GV2d.cjs → run-agent-ctLCvNgL.cjs} +230 -88
- package/dist/{setup-agent-BuanDXeh.mjs → setup-agent-7rV_cujV.mjs} +15 -9
- package/dist/setup-agent-CGELn1qZ.d.mts +577 -0
- package/dist/setup-agent-CLPCkXsu.d.cts +577 -0
- package/dist/{setup-agent-DV4cTou-.cjs → setup-agent-DoUk7EdC.cjs} +31 -20
- package/dist/tanstack-ai.cjs +627 -0
- package/dist/tanstack-ai.d.cts +204 -0
- package/dist/tanstack-ai.d.mts +204 -0
- package/dist/tanstack-ai.mjs +618 -0
- package/dist/testing.cjs +204 -985
- package/dist/testing.d.cts +132 -347
- package/dist/testing.d.mts +132 -347
- package/dist/testing.mjs +191 -973
- package/dist/{text-logic-IjfYy9DP.d.cts → text-logic-BRYOPWvY.d.mts} +52 -12
- package/dist/{text-logic-D-czVPYp.d.mts → text-logic-DmciuVUQ.d.cts} +52 -12
- package/dist/{types-BgNbNKnJ.d.mts → types-Dp5_Ru4R.d.cts} +2 -2
- package/dist/{types-hR5f5YXa.d.cts → types-Dp5_Ru4R.d.mts} +2 -2
- package/dist/usage-BkM3z02Y.d.mts +17 -0
- package/dist/usage-Cm_ovz9V.d.cts +17 -0
- package/dist/{utils-_q7F-AZu.d.mts → utils-BnXdWih3.d.mts} +1 -2
- package/dist/{utils-o4YRdHJX.d.cts → utils-ebVpCYoF.d.cts} +1 -2
- package/dist/validate.cjs +239 -8
- package/dist/validate.d.cts +110 -3
- package/dist/validate.d.mts +110 -3
- package/dist/validate.mjs +238 -7
- package/dist/verify-COFWyprJ.mjs +1181 -0
- package/dist/verify-CVlsTZ-V.cjs +1222 -0
- package/dist/verify-Cn86rT8l.d.mts +414 -0
- package/dist/verify-Dzwd4Mvk.d.cts +414 -0
- package/package.json +44 -31
- package/readme.md +40 -28
- package/skills/add-evals/SKILL.md +129 -0
- package/skills/add-traces/SKILL.md +135 -0
- package/skills/generate-machine/SKILL.md +143 -69
- package/skills/graph-engineering/SKILL.md +33 -0
- package/skills/improve-machine/SKILL.md +241 -0
- package/skills/improve-machine/reference.md +101 -0
- package/skills/loop-to-machine/SKILL.md +183 -0
- package/skills/loop-to-machine/policy.md +158 -0
- package/dist/event-log-store-2pAFiWuc.mjs +0 -576
- package/dist/event-log-store-C-COhIXP.cjs +0 -702
- package/dist/usage-BdEjFuIi.mjs +0 -5
- package/dist/usage-efOQPdf_.cjs +0 -10
|
@@ -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.
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: improve-machine
|
|
3
|
+
description: Improve a @statelyai/agent policy machine (a TypeScript setupAgent machine or a JSON AgentWorkflowConfig) from its run traces toward its goal, inside a fixed environment machine, with gates and a held-out promotion rule, and hand the result to a person as a pull request. 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 machine that decides how the agent works toward its goal, written in TypeScript or as a JSON `AgentWorkflowConfig`. 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 → gate (≤ 3 repairs) → replay train → replay held-out → recommend or discard → record → pull request
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
This is step 4 of 4, after `loop-to-machine`, `add-traces`, and `add-evals`. To author a JSON policy from scratch, use the `generate-machine` skill. Concept page: [Environment and policy](https://github.com/statelyai/agent/blob/main/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. Eval files may list baseline failures for every split; read only the training rows.
|
|
23
|
+
- **Stop after 3 rounds without a promotion** (or the limit the user gives).
|
|
24
|
+
- **Propose, never promote.** A round ends in a recommendation. A recommended candidate goes to a person as a pull request with its scorecard; merging it is the promotion. Never merge, deploy, or switch the running policy yourself.
|
|
25
|
+
|
|
26
|
+
## Policy format
|
|
27
|
+
|
|
28
|
+
The rounds are the same for both formats. What differs is what the diff may touch and how it is gated.
|
|
29
|
+
|
|
30
|
+
| | TypeScript policy | JSON policy |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| The policy | A `setupAgent` machine in its own file(s), with a declared `version` | An `AgentWorkflowConfig` file |
|
|
33
|
+
| May change | The policy file(s) and their `CHANGELOG.md`; once, the line in the runner that routes the decision to the policy; tests that pin measured numbers (see the pull request) | The config's `states` and `context` |
|
|
34
|
+
| Gates | `tsc`, `lintAgentMachine`, `explorePaths` on the policy, the environment check, the overfit check | `verifyAgentConfig` |
|
|
35
|
+
| Vocabulary | What the policy file imports and the events the environment offers | The `allow` lists |
|
|
36
|
+
|
|
37
|
+
**No policy yet.** `loop-to-machine` leaves a single model decision inside the environment when the old code had no policy logic. Then:
|
|
38
|
+
|
|
39
|
+
- Record `v0` in a new `CHANGELOG.md` next to the environment (in a `policy/` directory): "the model decides directly". This entry goes in the working tree.
|
|
40
|
+
- The first recommended candidate introduces the policy as `v1`: a policy file that reproduces the current behavior (it passes the environment's prompt and facts through to the model) plus the one rule the diagnosis found, and the routing line in the runner (`withPolicy(modelExecutors)`). The pattern is in `../loop-to-machine/policy.md`.
|
|
41
|
+
- Its `description` is the environment's goal with measurable success criteria. If the environment's goal has none, draft them from the confirmed objective and list the draft under questions for the user.
|
|
42
|
+
- If the policy needs a fact the environment does not pass in the decision's `metadata.facts`, that is an environment change request.
|
|
43
|
+
|
|
44
|
+
For a TypeScript policy, before the first round:
|
|
45
|
+
|
|
46
|
+
- **List the environment files** (the environment machine, its guards, the world and its tools) and record a hash of each. Any change to them fails the round. The runner is not one of them, but its diff may contain only the routing line.
|
|
47
|
+
- **Keep the policy a machine.** A diff may add states, `choice` branches, guards over facts, helpers in the policy file, or change a prompt or a model ref. It may not turn the policy into a free function, call tools the environment owns, or import from the environment anything but its schemas, types, constants, and exported guard functions.
|
|
48
|
+
|
|
49
|
+
## 1. Locate the inputs
|
|
50
|
+
|
|
51
|
+
Find all six before changing anything. If one is missing, stop and ask.
|
|
52
|
+
|
|
53
|
+
| Input | Where to look | If missing |
|
|
54
|
+
| -------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- |
|
|
55
|
+
| Goal | Root `description` of the policy config or the environment machine | Draft one with measurable success criteria; ask the user to confirm it |
|
|
56
|
+
| 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** |
|
|
57
|
+
| Traces | The `store` passed to `createAgentRuntime` / `runAgent`; its `listThreads`, or recorded ids | Wire a store first (the `add-traces` skill) and collect runs |
|
|
58
|
+
| 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 |
|
|
59
|
+
| Vocabulary | The actors, requests, named guards, actions, decision events, and model refs the host implements | Read them off the host code; never invent names |
|
|
60
|
+
| Policy history | The policy files and a `CHANGELOG.md` next to them | Create the changelog with the current policy as `v0` |
|
|
61
|
+
|
|
62
|
+
The objective defines "better". Do not start a round until the user has confirmed it, including how two versions compare when it has more than one field. A good objective has:
|
|
63
|
+
|
|
64
|
+
- a primary metric from the goal (accuracy against expected outcomes and effects, resolution rate). With several, state the rule: one primary, or none worse and one better,
|
|
65
|
+
- a hard constraint: invariant violations, which must be 0,
|
|
66
|
+
- a cost term (model calls, tokens, guard rejections) used as a tie-breaker or a stated budget. Count the calls that reach a model executor, not the environment's decision requests: a policy that answers with a rule makes no model call, and a cost term that counts requests cannot see the saving.
|
|
67
|
+
|
|
68
|
+
## 2. Read
|
|
69
|
+
|
|
70
|
+
Read both machines as text. `describeMachine` puts the goal first:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { describeMachine, setupAgent } from "@statelyai/agent";
|
|
74
|
+
|
|
75
|
+
// A TypeScript policy: import it. A JSON policy: lower it.
|
|
76
|
+
const { machine: policy } = setupAgent.fromConfig(policyConfig, { compileSchema });
|
|
77
|
+
const environmentText = describeMachine(environment);
|
|
78
|
+
const policyText = describeMachine(policy);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Then find the training threads and compute one outcome row per episode:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { getDecisionAttempts, getUsageFromEvents, replay } from "@statelyai/agent/log";
|
|
85
|
+
|
|
86
|
+
// Thread ids name the split and policy version: "train:v<n>:<scenario>:r<k>".
|
|
87
|
+
const prefix = `train:v${currentVersion}:`;
|
|
88
|
+
const threadIds: string[] = [];
|
|
89
|
+
let cursor: string | undefined;
|
|
90
|
+
do {
|
|
91
|
+
const page = await store.listThreads!({ prefix, limit: 100, cursor });
|
|
92
|
+
threadIds.push(...page.threadIds);
|
|
93
|
+
cursor = page.cursor;
|
|
94
|
+
} while (cursor !== undefined);
|
|
95
|
+
|
|
96
|
+
const rows = [];
|
|
97
|
+
for (const threadId of threadIds) {
|
|
98
|
+
const scenario = scenarioOf(threadId);
|
|
99
|
+
const entries = await store.read(threadId);
|
|
100
|
+
const { snapshot } = replay(environment, entries); // the recording machine; pure fold, no model calls
|
|
101
|
+
rows.push({
|
|
102
|
+
threadId,
|
|
103
|
+
expected: scenario.expected,
|
|
104
|
+
state: snapshot.value,
|
|
105
|
+
status: snapshot.status,
|
|
106
|
+
output: snapshot.output,
|
|
107
|
+
steps: entries.length,
|
|
108
|
+
tokens: getUsageFromEvents(entries).totalTokens ?? 0,
|
|
109
|
+
failedAttempts: getDecisionAttempts(entries).flatMap(({ attempts }) => attempts),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- `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.
|
|
115
|
+
- 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.
|
|
116
|
+
- `store.read(threadId)` returns a thread's entries in log order.
|
|
117
|
+
- `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.
|
|
118
|
+
- `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.
|
|
119
|
+
- `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.
|
|
120
|
+
- Compute the objective over the rows. This is the baseline for the round.
|
|
121
|
+
|
|
122
|
+
## 3. Diagnose
|
|
123
|
+
|
|
124
|
+
Group the failing training episodes by cause. Check these clusters in order:
|
|
125
|
+
|
|
126
|
+
1. **Invariant violations.** Any violation is a stop: the environment is wrong, not the policy. Write a change request.
|
|
127
|
+
2. **Wrong terminal state.** The episode ended in `escalated` when the label says `issued`, or the reverse. Look at which decision led there.
|
|
128
|
+
3. **Right terminal state, wrong effect.** The episode ended `issued` with the wrong amount, or reached the right state by a costlier path. Compare the chosen event with the offered options at each decision.
|
|
129
|
+
4. **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.
|
|
130
|
+
5. **Loops and budget exhaustion.** Repeated state values, or a terminal reached because a turn or retry budget ran out.
|
|
131
|
+
6. **Cost hotspots.** Correct episodes that spend model calls where a `choice` over known facts, or a cheap judgment, would decide.
|
|
132
|
+
|
|
133
|
+
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.
|
|
134
|
+
|
|
135
|
+
## 4. Propose
|
|
136
|
+
|
|
137
|
+
Make the smallest policy diff that addresses the one cause.
|
|
138
|
+
|
|
139
|
+
- 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.
|
|
140
|
+
- TypeScript policy: compare facts directly in a guard or `choice` instead of adding a named guard; keep helpers in the policy file.
|
|
141
|
+
- JSON policy: use only names in the vocabulary. A named guard the host does not implement fails the `lower` gate.
|
|
142
|
+
- Keep the machine `id`. Bump `version`.
|
|
143
|
+
- Keep the root `description` (the goal) unchanged. Changing the goal is the user's decision.
|
|
144
|
+
|
|
145
|
+
## 5. Gate
|
|
146
|
+
|
|
147
|
+
### TypeScript policy
|
|
148
|
+
|
|
149
|
+
Run every check; a failure's output is the repair prompt. Cap repairs at 3.
|
|
150
|
+
|
|
151
|
+
1. **Environment unchanged:** every environment file hashes as recorded. A change is never repaired; discard the candidate.
|
|
152
|
+
2. **Types:** `tsc --noEmit` passes.
|
|
153
|
+
3. **Lint:** `lintAgentMachine(policy)` from `@statelyai/agent/testing` reports no errors.
|
|
154
|
+
4. **Paths:** for probe inputs built from recorded decision requests (one per training scenario that reaches the decision: its facts and offered options), `canReach(policy, (snapshot) => snapshot.status === "done" && !isOfferedEvent(snapshot.output), { input })` is `{ reachable: false, complete: true }`: every final output is one of the offered events. Pass `outputs` for any actor the policy invokes, and `errors: { "agent.decide": new Error("down") }` so the fallback path is checked too.
|
|
155
|
+
5. **Source rules:** the policy file is still one `setupAgent` machine; its imports from the environment are only schemas, types, constants, and exported guard functions; it names no actor the environment owns. These are source checks: read the diff, or grep its imports and actor names.
|
|
156
|
+
6. **Overfit:** the diff contains no literal from the scenarios (ids, ticket text, customer ids, order ids). A branch that matches one scenario's data is memorization; discard it.
|
|
157
|
+
7. **Existing evals:** the rule and decision evals from `add-evals` still pass. Tests that pin measured numbers of the current version (a baseline failure list, a call count) are expected to change; update them in the same pull request and list each change in its body.
|
|
158
|
+
|
|
159
|
+
The environment's own `canReach` proofs still cover every policy, because the environment's guards judge every proposed event.
|
|
160
|
+
|
|
161
|
+
### JSON policy
|
|
162
|
+
|
|
163
|
+
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.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { verifyAgentConfig } from "@statelyai/agent/validate";
|
|
167
|
+
|
|
168
|
+
const verified = await verifyAgentConfig(candidate, {
|
|
169
|
+
compileSchema,
|
|
170
|
+
guards: hostGuards,
|
|
171
|
+
actors: hostActors,
|
|
172
|
+
allow: { actors, requests, guards, actions, events, models },
|
|
173
|
+
explore: {
|
|
174
|
+
inputs: probeInputs,
|
|
175
|
+
terminals: ["done"],
|
|
176
|
+
// Both outcomes, so the branches on either side of the classifier are probed.
|
|
177
|
+
outputs: {
|
|
178
|
+
classifyTicket: [
|
|
179
|
+
{ isRefundRequest: true, confidence: 0.9 },
|
|
180
|
+
{ isRefundRequest: false, confidence: 0.9 },
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
errors: { classifyTicket: new Error("down") },
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
const failed = verified.gates.find((gate) => !gate.ok);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
- Gates run in order and stop at the first failure: `validate`, `vocabulary`, `lower`, `lint`, `explore`.
|
|
190
|
+
- 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.
|
|
191
|
+
- `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`.
|
|
192
|
+
- 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.
|
|
193
|
+
|
|
194
|
+
## 6. Replay
|
|
195
|
+
|
|
196
|
+
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, in the scheme `add-traces` set up (`train:v3:<scenario>:r1`, `held-out:v3:<scenario>:r1`), so the next round can list them by prefix. With no persistent store, write each thread's `result.events` to a file.
|
|
197
|
+
|
|
198
|
+
- Run the current policy and the candidate on the same scenarios, the same world state, and the same model refs.
|
|
199
|
+
- Model calls are nondeterministic. Run each scenario k times (k ≥ 3 when calls are not cached) and compare means.
|
|
200
|
+
- Compute the objective, invariant violations, and cost per split, for both versions.
|
|
201
|
+
|
|
202
|
+
## 7. Recommend or discard
|
|
203
|
+
|
|
204
|
+
State the tolerance before you replay. Recommend only when every condition holds:
|
|
205
|
+
|
|
206
|
+
- invariant violations are 0 on both splits,
|
|
207
|
+
- the objective on the held-out split is better than the current policy's,
|
|
208
|
+
- no single scenario regresses beyond the tolerance (for example, a scenario that passed in all k runs now fails in any),
|
|
209
|
+
- cost stays within the stated budget, if there is one.
|
|
210
|
+
|
|
211
|
+
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.
|
|
212
|
+
|
|
213
|
+
### The pull request
|
|
214
|
+
|
|
215
|
+
A recommended candidate becomes one pull request (or, with no remote, one branch and a diff file), opened against the repository's integration branch:
|
|
216
|
+
|
|
217
|
+
- **The diff:** the policy file(s) and the new `CHANGELOG.md` entry; the runner's routing line, when this candidate introduces the policy; and updated pinned measurements, each listed in the body. Nothing else.
|
|
218
|
+
- **The body:** the hypothesis, the scorecard (objective, violations, and cost before and after, train and held-out separately, k runs each), every scenario that changed outcome, the gate results, and the thread ids a reviewer can replay.
|
|
219
|
+
- **One candidate per pull request.** Leave your working tree on the current policy. Later rounds build on the current policy, not on an unmerged candidate.
|
|
220
|
+
|
|
221
|
+
## 8. Stop and report
|
|
222
|
+
|
|
223
|
+
Stop when any of these holds:
|
|
224
|
+
|
|
225
|
+
- 3 rounds in a row without a promotion,
|
|
226
|
+
- the goal's success criteria are met on the held-out split,
|
|
227
|
+
- the remaining failures need an environment change (write a change request),
|
|
228
|
+
- the vocabulary lacks a capability the fix needs (ask for it; name the actor, request, or guard and its input and output).
|
|
229
|
+
|
|
230
|
+
Report to the user: rounds run, each candidate with its hypothesis and decision, the pull request link (or, with no remote, the branch and diff file), the objective before and after on both splits, violations, cost, and any change requests.
|
|
231
|
+
|
|
232
|
+
## Worked example
|
|
233
|
+
|
|
234
|
+
[`examples/self-improving-refunds`](https://github.com/statelyai/agent/blob/main/examples/self-improving-refunds) in the `statelyai/agent` repo (not in the installed package) runs this loop end to end for a JSON policy:
|
|
235
|
+
|
|
236
|
+
- the refund desk is the environment: business rules as guards, and an `illegallyIssued` sentinel proven unreachable,
|
|
237
|
+
- `playbooks/*.json` are policy versions, with the vocabulary listed in `playbooks/README.md`,
|
|
238
|
+
- `improve.ts` is the loop: propose, gate, replay, promote,
|
|
239
|
+
- `benchmark.ts` compares a raw tool loop, the desk with a raw model deciding, and the desk with a playbook deciding.
|
|
240
|
+
|
|
241
|
+
Read the directory before you start. File contents change between releases.
|
|
@@ -0,0 +1,101 @@
|
|
|
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 (or, for a new policy: v0 recorded, v1 reproduces current behavior plus one rule)
|
|
16
|
+
[ ] Gates passed: TS (env unchanged, tsc, lint, paths, source rules, overfit, evals) or JSON (verifyAgentConfig) (repairs used: _/3)
|
|
17
|
+
[ ] Replayed train, then held-out, k runs each, new thread ids
|
|
18
|
+
[ ] Promotion rule applied; CHANGELOG entry written
|
|
19
|
+
[ ] Recommended candidate opened as one pull request with its scorecard; not merged
|
|
20
|
+
[ ] Stop condition checked
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Hypothesis
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Cause: <one sentence, the mechanism, not the symptom>
|
|
27
|
+
Evidence: <thread ids and what each shows>
|
|
28
|
+
Change: <the one diff, in words>
|
|
29
|
+
Expected effect: <metric> moves from <x> to about <y>; nothing else changes
|
|
30
|
+
Risk: <scenarios that could regress, and why>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Proposer prompt
|
|
34
|
+
|
|
35
|
+
Use this when a model proposes the candidate. Fill every slot from real data.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
You improve a policy machine. You may change only the policy config below.
|
|
39
|
+
|
|
40
|
+
{describeMachine(environment)}
|
|
41
|
+
|
|
42
|
+
Current policy (version {n}):
|
|
43
|
+
{the policy file's source, or JSON.stringify(policyConfig, null, 2)}
|
|
44
|
+
|
|
45
|
+
Vocabulary (use no other names):
|
|
46
|
+
- actors: {actors}
|
|
47
|
+
- requests: {requests}
|
|
48
|
+
- named guards: {guards}
|
|
49
|
+
- named actions: {actions}
|
|
50
|
+
- decision events: {events}
|
|
51
|
+
- model refs: {models}
|
|
52
|
+
|
|
53
|
+
Objective on the training scenarios: {objective summary}
|
|
54
|
+
Diagnosed cause: {hypothesis.cause}
|
|
55
|
+
Evidence: {failing rows: scenario, expected, reached state, rejections, tokens}
|
|
56
|
+
|
|
57
|
+
Return the complete policy: the TypeScript file, or one AgentWorkflowConfig as
|
|
58
|
+
JSON. Make the smallest change that addresses the diagnosed cause. Keep "id" and
|
|
59
|
+
"description". Set "version" to {n + 1}. Never match on a scenario's data.
|
|
60
|
+
For a TypeScript policy, keep the result one `setupAgent` machine. Import from
|
|
61
|
+
the environment only schemas, types, constants, and exported guard functions.
|
|
62
|
+
Do not invoke tools the environment owns.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
On a failed gate, append:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
Your previous config was rejected.
|
|
69
|
+
Gate '{gate.name}' failed:
|
|
70
|
+
{gate.detail}
|
|
71
|
+
Return a corrected config.
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Changelog entry
|
|
75
|
+
|
|
76
|
+
```md
|
|
77
|
+
## v{n} — {recommended | discarded}
|
|
78
|
+
|
|
79
|
+
- Hypothesis: {cause} → {change}
|
|
80
|
+
- Diff: {states or transitions touched}
|
|
81
|
+
- Gates: {TS: environment ✓ tsc ✓ lint ✓ paths ✓ overfit ✓ evals ✓ | JSON: validate ✓ vocabulary ✓ lower ✓ lint ✓ explore ✓} (repairs: {r})
|
|
82
|
+
- Train: objective {a} → {b}, violations {v}, cost {c0} → {c1}
|
|
83
|
+
- Held-out: objective {a} → {b}, violations {v}, cost {c0} → {c1}
|
|
84
|
+
- Regressions: {none | scenario ids}
|
|
85
|
+
- Decision: {why}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Environment change request
|
|
89
|
+
|
|
90
|
+
Write this instead of editing the environment.
|
|
91
|
+
|
|
92
|
+
```md
|
|
93
|
+
# Environment change request: {short title}
|
|
94
|
+
|
|
95
|
+
- Goal criterion blocked: {the success criterion no policy can meet}
|
|
96
|
+
- Evidence: {thread ids; the state and guard that block it}
|
|
97
|
+
- Policies tried: {versions and why each could not get there}
|
|
98
|
+
- Proposed change: {the state, guard, or event to add or change, in words}
|
|
99
|
+
- Invariants affected: {which, and the canReach check to rerun}
|
|
100
|
+
- Owner: a person reviews and merges this; the improver does not.
|
|
101
|
+
```
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: loop-to-machine
|
|
3
|
+
description: Convert existing agent code (a while-loop around a model, a tool loop, ad-hoc if/else control flow) into an XState v6 state machine with @statelyai/agent, and prove with a parity report that it behaves like the code it replaces. Use when asked to "turn this agent into a state machine", "port my loop to xstate", "make this a statechart", "refactor this agent into a machine", or "split environment and policy" for code that already runs.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Convert agent code to a machine
|
|
7
|
+
|
|
8
|
+
You replace the control flow of working agent code with a machine, and show where the machine behaves like the old code and where it differs on purpose. The model calls, tools, and retry code move across unchanged.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
inventory → pin the baseline → environment → (policy) → runner → proofs → parity → hand back
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
This is step 1 of 4. Next: `add-traces`, then `add-evals`, then `improve-machine`. To author a machine with no existing code, use `generate-machine`.
|
|
15
|
+
|
|
16
|
+
## Hard rules
|
|
17
|
+
|
|
18
|
+
- **Pin the baseline before you change anything.** The parity test runs against the old code, which stays in the repo, untouched, until a person deletes it.
|
|
19
|
+
- **Never promise "behaves exactly like".** Promise what the parity report shows: same outcome and effects on these scenarios, with every difference labeled.
|
|
20
|
+
- **Never copy a rule break.** Where the old code broke a rule, the machine blocks it. That difference is `intended`, not a regression.
|
|
21
|
+
- **Rules are code, not prompt text.** A rule that lived only in a system prompt becomes a guard. Keep the prompt line only as context for the model.
|
|
22
|
+
- **Business rules belong to people.** If a rule is ambiguous (which threshold, which order of checks), ask. Never pick one silently.
|
|
23
|
+
|
|
24
|
+
## 1. Inventory the code
|
|
25
|
+
|
|
26
|
+
Read the loop and fill this table before writing anything. Each row maps to a machine element.
|
|
27
|
+
|
|
28
|
+
| In the code | Becomes |
|
|
29
|
+
| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
30
|
+
| A tool with a side effect someone must gate (refund, send) | An actor invoked by one state, entered only through a guarded transition |
|
|
31
|
+
| A read-only tool (search, lookup) | An actor the machine invokes itself, or a tool on a text request |
|
|
32
|
+
| A rule: an `if`, a prompt line, a check after the fact | A guard on the transition it protects |
|
|
33
|
+
| A point where the model picks what to do | A decision state: `agent.decide`, usually a closed choice |
|
|
34
|
+
| A value the model pulled out of the input (an order id, an amount) | Code when the format is fixed (a regex); a text request when it is not |
|
|
35
|
+
| A step every correct run takes in the same order (verify, then look up) | A state the machine enters itself; the model no longer chooses it |
|
|
36
|
+
| A person in the loop: `await approve()`, a returned `pending`, or a tool that asks a person (`askCustomer`, `requestApproval`), even a synchronous one | An idle state with `meta.interaction`, answered by `onIdle` in the runner |
|
|
37
|
+
| A cap (`turns < 8`, a retry counter) on a loop the machine keeps | A `choice` state against an exported constant |
|
|
38
|
+
| A cap on a loop the machine removed | Nothing. Record that it is gone and why |
|
|
39
|
+
| Each distinct return | A final state |
|
|
40
|
+
| A terminal tool with a side effect (`escalate`, `close`) | A state that invokes it, then the final state; its `onError` goes to a `failed` final |
|
|
41
|
+
|
|
42
|
+
List the rules as a numbered list in your notes. Every one must end as a guard, a state ordering, or a written reason it was dropped.
|
|
43
|
+
|
|
44
|
+
## 2. Pin the baseline
|
|
45
|
+
|
|
46
|
+
The parity test exists before the machine does.
|
|
47
|
+
|
|
48
|
+
1. **Scenarios.** Reuse the code's tests, fixtures, or benchmark cases. Each has an `id` and, where people know it, the `expected` outcome. If there are none, draft 6–10 that cover every terminal and every rule, and ask the user to confirm them.
|
|
49
|
+
2. **Deterministic stand-in models.** Parity with a live model drifts between runs. Use the code's scripted or mock models if it has them; otherwise write one keyed on the input (see [`examples/mock-model.ts`](https://github.com/statelyai/agent/blob/main/examples/mock-model.ts) in the `statelyai/agent` repo). You want two: one that follows the rules, and one that breaks them. A seeded random model needs its seed derived from the scenario id and a fresh instance per run, or every scenario replays the same draws.
|
|
50
|
+
3. **Ground truth.** Read outcomes and effects from the world, not from the agent's own report. `effects` holds **every write the world records**: the refunds, and also the approvals, the verifications, the questions asked, the escalations. A refund issued without its approval looks correct if `effects` holds only refunds. Text a model wrote (an escalation reason) differs on every run: compare its count or presence, not the text. If the world can check rules after the fact, return those violations. If it cannot, write the check as a plain function over the effects.
|
|
51
|
+
4. **The harness check.** Run the old code on both sides with each stand-in model. Each row's baseline must equal its candidate (the runs are deterministic), and with the rule-following model every row must be `same`. With a rule-breaking model, rows are `regression` by design: the candidate breaks rules.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { compareRuns, type ParityResult, type ParityRun } from "@statelyai/agent/testing";
|
|
55
|
+
|
|
56
|
+
function toParityRun(run: { outcome: string; world: World }): ParityRun {
|
|
57
|
+
return {
|
|
58
|
+
outcome: run.outcome,
|
|
59
|
+
// Every write the world records; model-written text as a count.
|
|
60
|
+
effects: { refunds: run.world.refunds, approvals: run.world.approvals, verified: [...run.world.verified], escalations: run.world.escalations.length },
|
|
61
|
+
violations: run.world.violations, // rules broken, from ground truth
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A model factory, called once per scenario and per side. Construct a stateful or seeded
|
|
66
|
+
// model inside it (seeded from s.id); returning a shared stateless object is fine.
|
|
67
|
+
const runWith = (runner: typeof runLoop, model: (s: Scenario) => Model) => async (s: Scenario) =>
|
|
68
|
+
toParityRun(await runner({ ...s, model: model(s), world: createWorld(s) }));
|
|
69
|
+
|
|
70
|
+
const MODELS: Array<[string, (s: Scenario) => Model]> = [
|
|
71
|
+
["careful", () => carefulModel],
|
|
72
|
+
["sloppy", () => sloppyModel],
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
test.each(MODELS)("harness: the loop matches itself (%s)", async (_, model) => {
|
|
76
|
+
const report = await compareRuns({ scenarios: SCENARIOS, baseline: runWith(runLoop, model), candidate: runWith(runLoop, model) });
|
|
77
|
+
for (const row of report.scenarios) expect(row.candidate, row.id).toEqual(row.baseline);
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 3. Author the environment
|
|
82
|
+
|
|
83
|
+
The environment is the machine: states, guards that encode the rules, side-effecting actors, an invariant sentinel. Use `setupAgent` (from `@statelyai/agent`) with zod schemas for `input`, `context`, `output`, and every event. Export the event schemas as one object, so a policy can reuse them later.
|
|
84
|
+
|
|
85
|
+
A gated action:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
deciding: {
|
|
89
|
+
tags: ["decision"],
|
|
90
|
+
invoke: {
|
|
91
|
+
src: "agent.decide",
|
|
92
|
+
input: ({ context }) => ({
|
|
93
|
+
model: "decider", // a key the host's executors resolve
|
|
94
|
+
name: "chooseResolution", // tests, traces, and a later policy find it by this name
|
|
95
|
+
prompt: `Ticket: ${context.ticket}\nOrder: ${JSON.stringify(context.order)}`,
|
|
96
|
+
allowedEvents: ["REFUND", "ESCALATE", "CLOSE"],
|
|
97
|
+
// A closed choice: the decider picks one of these. The runtime drops the ones the guards refuse.
|
|
98
|
+
payloads: { REFUND: refundAmounts(context).map((amount) => ({ amount })) },
|
|
99
|
+
// The facts a decider reads. Arrives as `request.metadata.facts`.
|
|
100
|
+
metadata: { facts: { order: context.order, requestedAmount: context.requestedAmount } },
|
|
101
|
+
}),
|
|
102
|
+
onError: { target: "escalated" }, // retries exhausted, or no option legal
|
|
103
|
+
},
|
|
104
|
+
on: {
|
|
105
|
+
REFUND: ({ context, event }) =>
|
|
106
|
+
refundIsLegal(context, event.amount)
|
|
107
|
+
? { target: event.amount > APPROVAL_THRESHOLD ? "awaitingApproval" : "paying", context: { amount: event.amount } }
|
|
108
|
+
: undefined, // undefined rejects: the decision retries with feedback
|
|
109
|
+
ESCALATE: { target: "escalated" },
|
|
110
|
+
CLOSE: { target: "closed" },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- **Do the deterministic steps yourself.** Identity, lookup, the ownership check: states the machine runs in order. A model only answers what genuinely needs judgment.
|
|
116
|
+
- **Export the guards** as plain functions (`refundIsLegal`) so tests can call them, and build the machine in a factory (`createDeskMachine({ refundIsLegal })`) so a test can pass a loosened guard.
|
|
117
|
+
- **Add an invariant sentinel** for the rule that must never break: a final state with a description starting `SENTINEL:`, entered from an `always` on the state that invokes the side effect (`paying`). An `always` is taken before the state's invoke starts, so the effect never runs. Write the check as its own function, not a call to the guard, so loosening the guard does not loosen the check.
|
|
118
|
+
- **A person's step** is an idle state whose `meta.interaction` tells a host what to render: `{ label: "Approve a refund of ${amount}?", events: { APPROVE: { label: "Approve" }, REJECT: { label: "Reject" } } }`, or `textEvent: { type: "CUSTOMER_REPLIED", field: "reply" }` for free text.
|
|
119
|
+
- **Actors are placeholders.** Declare each tool as `createAsyncLogic<Output, Input>({ run })` from `xstate` in `setupAgent({ actors })` (XState v6 has no `fromPromise`). Give the type parameters, or `onDone`'s `output` is untyped. Pass the real implementations at run time with `runAgent(machine, { actors })`.
|
|
120
|
+
- **Final outputs carry the machine's result** (`{ outcome, orderId, amount }`), JSON only. The runner rebuilds the old return type from it and from the world (step 5).
|
|
121
|
+
- **Declare `version`** on `createMachine` (`version: "1"`). Behavior lives in transition functions, which the structural hash cannot see; traces stamp this string.
|
|
122
|
+
|
|
123
|
+
## 4. A policy, only if the code already has one
|
|
124
|
+
|
|
125
|
+
Keep the decision in the environment as one `agent.decide`, and do not write a policy machine, unless the old code already has decision logic beyond one model call: a rule ladder, heuristics that pick a strategy, a cheap check before the model. A policy that only passes the options through to a model is boilerplate. `improve-machine` introduces a policy when the traces show a rule worth adding.
|
|
126
|
+
|
|
127
|
+
If the code does have that logic, port it as a policy machine that answers `chooseResolution`. The pattern, with code that compiles, is in [policy.md](policy.md).
|
|
128
|
+
|
|
129
|
+
## 5. The runner
|
|
130
|
+
|
|
131
|
+
Write `runMachine` with the old function's signature and result type, so callers and the parity test change one line.
|
|
132
|
+
|
|
133
|
+
- `runAgent(machine, { input, executors, actors, onIdle })`.
|
|
134
|
+
- `onIdle` answers each idle state with the old code's call (`tools.askCustomer(...)`, `tools.requestApproval(...)`). Write the returned events with `as const` on `type`, or the event union does not narrow.
|
|
135
|
+
- `executors.decide` adapts the code's model to the decision: the closed choice arrives as `request.options` (each `{ event }`), and the facts as `request.metadata?.facts`, typed `unknown`: parse it with the exported facts schema. Every executor slot is optional in the type; check or assert the ones you call.
|
|
136
|
+
- Rebuild the old result from `result.output` and the world. A field that has no meaning anymore (a turn count with no loop) keeps its type and gets a comment saying what it now holds.
|
|
137
|
+
|
|
138
|
+
## 6. Proofs that need no model
|
|
139
|
+
|
|
140
|
+
From `@statelyai/agent/testing`:
|
|
141
|
+
|
|
142
|
+
- `lintAgentMachine(machine)`: no errors.
|
|
143
|
+
- `canReach(machine, "<sentinel>", options)` returns `reachable: false` **and** `complete: true`, for each distinct input shape (an input with an order id and one without lead to different paths; loop over them).
|
|
144
|
+
- `options.payloads` lists values on both sides of every guard boundary (`0, 40, 41, 100, 101, 240, 241, 1000`). Without it, a closed choice is explored only with its own `payloads`, and `complete: true` then proves nothing about the boundaries.
|
|
145
|
+
- `options.outputs` lists every output each actor can return (found and not found, owned and not, refunded and not); `options.errors` makes each actor fail once.
|
|
146
|
+
- A loosened guard makes `canReach` true. Write this test too, or the proof proves nothing.
|
|
147
|
+
- Each normal final state is reachable, so the search is not vacuous.
|
|
148
|
+
|
|
149
|
+
Type the options with `ExplorePathsOptions` from `@statelyai/agent/testing`. `errors` takes one error per actor src, and `"agent.decide"` is a valid key.
|
|
150
|
+
|
|
151
|
+
## 7. Parity
|
|
152
|
+
|
|
153
|
+
Switch the parity test's candidate to `runMachine`, and run it with each stand-in model:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
test.each(MODELS)("parity with the loop: %s", async (_, model) => {
|
|
157
|
+
const report = await compareRuns({ scenarios: SCENARIOS, baseline: runWith(runLoop, model), candidate: runWith(runMachine, model) });
|
|
158
|
+
const unreviewed = report.scenarios.filter((row) => row.label === "regression" && !REVIEWED[row.id]);
|
|
159
|
+
expect(unreviewed).toEqual([]);
|
|
160
|
+
// A reviewed difference still has to be exactly the difference a person accepted.
|
|
161
|
+
for (const row of report.scenarios.filter((row) => REVIEWED[row.id])) REVIEWED[row.id]!(row);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Effect-only differences a person accepted: who, and the exact difference.
|
|
165
|
+
const REVIEWED: Record<string, (row: ParityResult<Scenario>) => void> = {
|
|
166
|
+
// "no-order-id-no-reply": (row) => { /* accepted by <name>: asks once, not three times */ expect(row.candidate.outcome).toBe(row.baseline.outcome); ... },
|
|
167
|
+
};
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
- Every `regression` row: fix the machine, or, when the old behavior was wrong but broke no rule, ask the user. If the outcomes differ, the answer may become the scenario's `expected`. If only effects differ (the loop asked three times, the machine once), list the row in a reviewed-differences list in the test, with the exact difference asserted and the person who accepted it. Never edit a scenario to make a row pass.
|
|
171
|
+
- Every `intended` row: check that its reason is a real rule. A candidate that fixed a rule break with the same outcome is `intended` too.
|
|
172
|
+
- Run once with a live model if a key is available. Report it as one sample, not a guarantee.
|
|
173
|
+
|
|
174
|
+
## 8. Hand back
|
|
175
|
+
|
|
176
|
+
Report, in this order:
|
|
177
|
+
|
|
178
|
+
1. The files: environment, runner, tests (and a policy, if the code had one). The old code is untouched.
|
|
179
|
+
2. The rule list from step 1, each mapped to its guard, its state, or the reason it was dropped.
|
|
180
|
+
3. The parity table per stand-in model: `same / intended / regression`, and one line per `intended` row with its reason.
|
|
181
|
+
4. The `canReach` results and the loosened-guard test.
|
|
182
|
+
5. Anything you had to decide that the user should confirm.
|
|
183
|
+
6. Next step: `add-traces`.
|