@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,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-evals
|
|
3
|
+
description: Write evals for a @statelyai/agent or XState agent machine at the right level — rule checks with no model, decision evals at one decision state, outcome evals over whole runs, and per-state stats from traces — each tied to the states it covers. Use when asked to "write evals", "add evals for the approval logic", "test the agent", "measure the agent", "build an eval suite", or "what should I eval" for an agent built as a state machine.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add evals to an agent machine
|
|
7
|
+
|
|
8
|
+
The machine tells you what to eval and at which level. Every eval names the states it covers, so a failing eval points at a part of the machine.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
read the machine → map the request to states → pick the level per state → write the evals → baseline → hand back
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
This is step 3 of 4. Before: `loop-to-machine`, `add-traces`. Next: `improve-machine`.
|
|
15
|
+
|
|
16
|
+
## Hard rules
|
|
17
|
+
|
|
18
|
+
- **The cheapest level that can catch the failure.** A rule is checked without a model. Only judgment needs a model in the eval.
|
|
19
|
+
- **Ground truth, never a model's opinion of itself.** Outcome evals read terminal states and effects in the world. A model grades text only where no ground truth exists, and that eval says so in its name.
|
|
20
|
+
- **Labels belong to people.** Draft expected outcomes and expected events; ask the user to confirm them before they gate anything. Keep drafts in their own list, marked unconfirmed, and out of every gate.
|
|
21
|
+
- **Never edit the machine to make an eval pass.** A failing eval is a finding. Report it.
|
|
22
|
+
- **Held-out scenarios stay held out.** Evals may run on them; nobody tunes against them.
|
|
23
|
+
|
|
24
|
+
## 1. Read the machine
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { describeMachine } from "@statelyai/agent";
|
|
28
|
+
console.log(describeMachine(machine)); // goal, states, transitions, events
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
List:
|
|
32
|
+
|
|
33
|
+
- the decision states: an `agent.decide` invoke, a `decision` tag, or a state a host answers with `decideNext`;
|
|
34
|
+
- the person's steps: idle states a person answers (`meta.interaction`, the runner's `onIdle`). What the person decides is ground truth, not something to eval; the routing after their answer is a rule;
|
|
35
|
+
- the guards. `describeMachine` shows where a transition function can go and marks it `(conditional)`, but not the guard's name or logic. Read the source for those;
|
|
36
|
+
- the sentinel states and the final states.
|
|
37
|
+
|
|
38
|
+
## 2. Map the request to states
|
|
39
|
+
|
|
40
|
+
Turn the user's words into states. "Evals for the approval logic" means every state and guard that touches approval: the guard that routes a refund over the threshold, the approval step and each answer to it, the sentinel check before the payment, and the final states they lead to. Write the mapping down:
|
|
41
|
+
|
|
42
|
+
| Asked about | States and guards |
|
|
43
|
+
| ----------- | -------------------------------------------------------------------------------------------------- |
|
|
44
|
+
| approval | `deciding` (REFUND: over threshold → `awaitingApproval`), `awaitingApproval` (APPROVE, REJECT), `paying` (sentinel check), `issued`, `closed` |
|
|
45
|
+
|
|
46
|
+
If the request names nothing ("write evals"), cover every decision state, every sentinel, and the run as a whole.
|
|
47
|
+
|
|
48
|
+
## 3. Pick the level
|
|
49
|
+
|
|
50
|
+
| Level | Catches | Runs a model | Tools |
|
|
51
|
+
| ------------ | ---------------------------------------------------- | --------------------- | ------------------------------------------------------------------ |
|
|
52
|
+
| **Rule** | A guard, an ordering, an invariant that must hold | No | Exported guard functions, `canReach`, `explorePaths`, `simulateAgent` |
|
|
53
|
+
| **Decision** | A wrong choice at one decision state | The decider under test | The decider called on recorded or written decision requests |
|
|
54
|
+
| **Request** | A bad text output from one request | That request | `runSeam`, scored on `seamOutput` and the path after it. Not applicable when the machine has no text requests; say so |
|
|
55
|
+
| **Outcome** | A wrong end result for a whole scenario | The whole agent | Scenarios, the runner, an objective over terminal states and effects |
|
|
56
|
+
| **Stats** | Which state the failures cluster in | No (reads traces) | `foldTraces` |
|
|
57
|
+
|
|
58
|
+
A guard or ordering is always a rule eval. Use a decision eval when the guards leave more than one legal option. Use an outcome eval when a person can label the right end result.
|
|
59
|
+
|
|
60
|
+
## 4. Write them
|
|
61
|
+
|
|
62
|
+
Name every eval `<state>: <what it checks>`, so the report reads as a map of the machine. In `test.each` titles, `$name` is a placeholder: write amounts as `100 dollars`, not `$100`.
|
|
63
|
+
|
|
64
|
+
**Rule evals**, from `@statelyai/agent/testing`:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
test("paying: no refund over 100 dollars issues without approval", async () => {
|
|
68
|
+
const result = await canReach(machine, "ruleBroken", { input, payloads, outputs, errors });
|
|
69
|
+
expect(result.complete).toBe(true); // reachable: false is a proof only when the search finished
|
|
70
|
+
expect(result.reachable, JSON.stringify(result.witness)).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("deciding: REFUND over the order amount is refused", () => {
|
|
74
|
+
expect(refundIsLegal({ ...context, order: { ...order, amount: 40 } }, 41)).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Use `explorePaths` to assert the set of reachable final states, and `await simulateAgent(...)` for one scripted path. Cover both sides of every threshold. The two treat a closed choice differently:
|
|
79
|
+
|
|
80
|
+
- `explorePaths` and `canReach` use `options.payloads` **instead of** the decision's own offered options, so they explore values the machine would never offer. That is what a boundary proof needs.
|
|
81
|
+
- `simulateAgent` keeps the offered options: a scripted event the decision did not offer is refused, and the run ends in the decision's `onError`. Script only offered events, or pick an input that offers the value.
|
|
82
|
+
|
|
83
|
+
**Decision evals.** A row is a decision request and the event a person says is right. Write rows by hand, or take them from recorded traces: the first attempt of each decision is a `request.start` event with `request.kind === "decision"` and `request.attempts.length === 0`, and its `request` carries `name`, `options`, and `metadata`. Cast the serialized `request` to `AgentExecutorDecisionRequest`, and compare events without caring about key order. If the decider reads anything outside the request (the ticket, the tool history), store that in the row too.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// Your helper: rows for one decision state, from traces or a fixture file.
|
|
87
|
+
const rows: Array<{ id: string; request: AgentExecutorDecisionRequest; expected: ChosenEvent }> = loadDecisionRows("deciding");
|
|
88
|
+
|
|
89
|
+
test.each(rows)("deciding: $id", async ({ request, expected }) => {
|
|
90
|
+
const { event } = await decide(request); // the policy decider, or the model executor
|
|
91
|
+
expect(event).toEqual(expected);
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Which deciders gate CI: a deterministic reference decider (a scripted or rules-based one) gates. A live model is a measurement, not a unit test: run it outside CI, several times, and report the rate. A seeded random decider is measured over several seeds, never gated.
|
|
96
|
+
|
|
97
|
+
**Request evals.** For a text request (a drafted question, a summary), `runSeam(machine, { input, scripts, seam: { request: "draftQuestion" }, candidate })` runs the whole machine with only that request live. Score `seamOutput`, and the states the run reached after it.
|
|
98
|
+
|
|
99
|
+
**Outcome evals.** One function over a batch of scenario runs, reading ground truth. Score the effects as well as the outcome: a run that ends `issued` with the wrong amount is wrong.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
export function objective(runs: Array<{ scenario: Scenario; outcome: string; refund?: Refund; violations: number; modelCalls: number }>) {
|
|
103
|
+
return {
|
|
104
|
+
accuracy: runs.filter((r) => r.outcome === r.scenario.expected && sameRefund(r.refund, r.scenario.expectedRefund)).length / runs.length,
|
|
105
|
+
violations: runs.reduce((n, r) => n + r.violations, 0), // must be 0
|
|
106
|
+
modelCalls: runs.reduce((n, r) => n + r.modelCalls, 0), // cost
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Read the terminal state from the event logs `add-traces` recorded (`replay(machine, entries).snapshot`, `replay` from `@statelyai/agent/log`). Effects and violations live in the world, not the log: save them next to the thread id when the run ends, so the objective can be recomputed without running anything. Use `matchesTrajectory` only for an ordering a person requires (approval before payment), never to pin the whole path; build its input from the `machine.transition` trace events' `snapshot.value`.
|
|
112
|
+
|
|
113
|
+
**Stats.** `foldTraces(traces, { failed })` (from `@statelyai/agent/testing`) returns `{ state, visits, failures, withheld }` per state. A failed run counts against **every** state it visited, so states after a bad decision rank as high as the decision itself. Use the table to see which states failing runs pass through, then look at the decision states among them, their `withheld` counts, and their decision attempts. `failed` sees only the trace: to judge a run against its label, keep a map from trace to scenario.
|
|
114
|
+
|
|
115
|
+
## 5. Coverage
|
|
116
|
+
|
|
117
|
+
If every decider scores the same on the confirmed scenarios, the scenarios cannot tell a better decider from a worse one. Draft scenarios on the boundaries the evals found (an amount just under and over a threshold, a partial request), label them as drafts, and ask the user to confirm them and add them to the scenario set. They become part of the train or held-out split only once confirmed.
|
|
118
|
+
|
|
119
|
+
## 6. Baseline
|
|
120
|
+
|
|
121
|
+
Run every eval once on the current machine and record the numbers: which pass, the outcome scores per split, the stats table. Failing evals are findings, not bugs in the eval, unless the label was wrong; then ask.
|
|
122
|
+
|
|
123
|
+
## 7. Hand back
|
|
124
|
+
|
|
125
|
+
1. The state mapping from step 2.
|
|
126
|
+
2. The evals by level, each named by state.
|
|
127
|
+
3. The baseline numbers, train and held-out separately.
|
|
128
|
+
4. Every failing eval and the state it points at.
|
|
129
|
+
5. Next step: `improve-machine`, with the objective from step 4 as its scorer.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-traces
|
|
3
|
+
description: Make every run of a @statelyai/agent or XState machine leave a replayable, stamped trace — an event log thread plus trace events — so evals and self-improvement can read what happened and what produced it. Use when asked to "add tracing", "add logging", "persist runs", "stamp traces", "record what the agent did", or "set up observability" for an agent machine, or before writing evals or improving a policy from traces.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add traces to an agent machine
|
|
7
|
+
|
|
8
|
+
Every run should leave two records, both stamped with what produced it:
|
|
9
|
+
|
|
10
|
+
| Record | What it is | Read by |
|
|
11
|
+
| ---------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------- |
|
|
12
|
+
| Event log thread | The external inputs the machine consumed, in order. The source of truth | `replay`, `getDecisionAttempts`, `getUsageFromEvents` |
|
|
13
|
+
| Trace events | Observations: transitions with `from`, decision requests with offered options | `foldTraces`, OpenTelemetry, debugging |
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
version the machines → pick a store → name threads → stamp → collect trace events → prove replay → hand back
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This is step 2 of 4. Before: `loop-to-machine`. Next: `add-evals`, then `improve-machine`.
|
|
20
|
+
|
|
21
|
+
## Hard rules
|
|
22
|
+
|
|
23
|
+
- **Traces never change behavior.** Recording is an observer. No branch reads a trace.
|
|
24
|
+
- **A trace must name what produced it.** A run with no machine version or policy version cannot be used to compare versions, so it is useless to the improver.
|
|
25
|
+
- **Replay is the test.** A log that does not replay to the run's final state with zero model calls is not a trace yet.
|
|
26
|
+
- **No secrets or personal data in metadata or thread ids.** Use ids, not names or emails. The log itself holds the machine's input and every event verbatim (a ticket's text, a customer's reply), and trace snapshots hold the context: in production, redact or encrypt them per your data rules, and ask the user which applies.
|
|
27
|
+
|
|
28
|
+
## 1. Version the machines
|
|
29
|
+
|
|
30
|
+
Declare `version` on every machine whose runs you record: the environment and, if there is one, the policy machine.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
export const deskMachine = deskSetup.createMachine({ id: "refund-desk", version: "1", ... });
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Also name the **decider version**: what answers the decisions. It is the policy machine's `version` when there is one; otherwise a label for the model and prompt that decide (`gpt-6-luna.p1`), or for a stand-in model in tests (`careful`). Runs by different deciders must never share a version label.
|
|
37
|
+
|
|
38
|
+
- Every log entry carries `machineVersion`. Without a declared `version` it falls back to a structural hash that skips functions, and in XState v6 the guards and targets live in transition functions. Changing a guard would not change the hash.
|
|
39
|
+
- Bump `version` on every behavior change. Adding recording is not one: it only observes. `improve-machine` bumps the policy's on promotion.
|
|
40
|
+
|
|
41
|
+
## 2. Pick a store
|
|
42
|
+
|
|
43
|
+
- **Tests and local runs:** `createInMemoryEventLogStore()` from `@statelyai/agent/log`, or write each thread's `result.events` to a JSON file after the run.
|
|
44
|
+
- **Production:** implement `AgentEventLogStore` against your database and check it with `assertEventLogStoreConformance`. Implement the optional `listThreads` so a split can be paged by prefix.
|
|
45
|
+
|
|
46
|
+
Pass it with a `threadId`. The store is write-ahead: no model call starts until the entries before it are durable.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const result = await runAgent(deskMachine, { input, executors, store, threadId, metadata });
|
|
50
|
+
await result.drain(); // late usage entries, before you read the store or exit
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**A thread that already has entries is resumed, not overwritten.** Give every new run a new thread id; reuse one only to continue that run.
|
|
54
|
+
|
|
55
|
+
## 3. Name threads
|
|
56
|
+
|
|
57
|
+
A thread id is how runs are found later. Use a prefix scheme and keep it in one function:
|
|
58
|
+
|
|
59
|
+
| Runs | Thread id |
|
|
60
|
+
| ------------------------ | -------------------------------------------- |
|
|
61
|
+
| Scenario runs for evals | `<split>:<deciderVersion>:<scenarioId>:r<k>`, `k` = the repeat index |
|
|
62
|
+
| Production | `prod:<domain id>` (ticket id, session id) |
|
|
63
|
+
|
|
64
|
+
`store.listThreads?.({ prefix: "train:careful:" })` then pages through one split of one decider. `listThreads` is optional on a custom store.
|
|
65
|
+
|
|
66
|
+
## 4. Stamp each run
|
|
67
|
+
|
|
68
|
+
`metadata` lands on the log's first (`@agent.init`) entry. Put there everything needed to tell two runs' producers apart:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const metadata = {
|
|
72
|
+
environmentVersion: deskMachine.version ?? "unversioned",
|
|
73
|
+
deciderVersion, // see step 1
|
|
74
|
+
models: { decider: "openai/gpt-6-luna" }, // the machine's model refs → what the host resolved them to
|
|
75
|
+
scenario: scenario.id, // eval runs only
|
|
76
|
+
split: scenario.split, // eval runs only
|
|
77
|
+
};
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Read it back with `entries[0].metadata`. A resumed run keeps the stamp of the run that started the log. `executionId` is reserved. The value type is `JsonValue` from `@statelyai/agent/log`.
|
|
81
|
+
|
|
82
|
+
## 5. Collect trace events
|
|
83
|
+
|
|
84
|
+
`onTrace` sees every trace event. Serialize and keep them per thread:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { serializeTraceEvent, type JsonSerializableTraceEvent } from "@statelyai/agent";
|
|
88
|
+
|
|
89
|
+
const trace: JsonSerializableTraceEvent[] = [];
|
|
90
|
+
const result = await runAgent(deskMachine, { ..., onTrace: (event) => trace.push(serializeTraceEvent(event)) });
|
|
91
|
+
await result.drain();
|
|
92
|
+
|
|
93
|
+
// For local batches: one file per thread. Encoding keeps colons and path separators out of the name.
|
|
94
|
+
const file = encodeURIComponent(threadId);
|
|
95
|
+
await mkdir("runs", { recursive: true });
|
|
96
|
+
await writeFile(`runs/${file}.events.json`, JSON.stringify(result.events));
|
|
97
|
+
await writeFile(`runs/${file}.trace.json`, JSON.stringify(trace));
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Decide with the user whether `runs/` is committed or gitignored.
|
|
101
|
+
|
|
102
|
+
- In production, send them to OpenTelemetry instead: `createOtelTraceHandler({ tracer })` from `@statelyai/agent/otel` is an `onTrace` handler.
|
|
103
|
+
- **A policy run inside a decider** is a separate run. Pass it the same `onTrace` (and its own `store` thread, `<threadId>:policy:<n>`) if you want per-state stats for the policy too.
|
|
104
|
+
- **A plain XState machine** (built with `createMachine` or `setup`, decided with `decideNext`): drive it with `createAgentRuntime` and `runToQuiescence`, which accept plain machines, and pass each `decideNext` result as `{ event, attempts }` so refused choices reach the log. Push `{ type: "request.start", request: result.request }` onto the trace before sending the event, since a host-driven decision emits no trace itself.
|
|
105
|
+
- **A plain `createActor` run you cannot change:** attach `recordLog(actor)` from `@statelyai/agent/log` before `actor.start()`, and pass `inspect: traceTransitions((event) => trace.push(serializeTraceEvent(event)))` to `createActor`. `recordLog` cannot record decision attempts.
|
|
106
|
+
|
|
107
|
+
## 6. Prove it
|
|
108
|
+
|
|
109
|
+
Write these tests, with the stand-in model from `loop-to-machine`:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { getDecisionAttempts, replay } from "@statelyai/agent/log";
|
|
113
|
+
import { foldTraces } from "@statelyai/agent/testing";
|
|
114
|
+
|
|
115
|
+
test("a recorded run replays to its final state with no model calls", async () => {
|
|
116
|
+
const result = await runAgent(deskMachine, { input, executors, store, threadId: "test:careful:small-clean:r1", metadata });
|
|
117
|
+
await result.drain();
|
|
118
|
+
const entries = await store.read("test:careful:small-clean:r1");
|
|
119
|
+
const { snapshot } = replay(deskMachine, entries); // a pure fold: nothing runs
|
|
120
|
+
expect(snapshot.value).toEqual(result.snapshot.value);
|
|
121
|
+
expect(entries[0]!.metadata).toMatchObject({ environmentVersion: "1" });
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
- `foldTraces(traces, { failed })` over a batch returns a row for every decision state. `failed` judges one whole trace; start with "the last event is not a `run.end` with status `done`, or the run ended in a failure or sentinel state", and ask the user whether a wrong outcome counts too. `withheld` is above 0 only for a closed choice (`payloads`), where the guards refused an option.
|
|
126
|
+
- `getDecisionAttempts(entries)` lists the refused choices of a run. A stand-in model that picks from the offered options can never propose an illegal one, so write a raw decide executor for this test that proposes a refused event first: `decide: async (request) => ({ event: request.attempts.length ? request.options![0]!.event : { type: "REFUND", amount: 1000 } })`.
|
|
127
|
+
- Running a scenario with and without recording leaves the same world and the same final state.
|
|
128
|
+
|
|
129
|
+
## 7. Hand back
|
|
130
|
+
|
|
131
|
+
1. Where runs are stored and how thread ids are built.
|
|
132
|
+
2. The stamp fields.
|
|
133
|
+
3. Where trace events go.
|
|
134
|
+
4. The replay test passing.
|
|
135
|
+
5. Next step: `add-evals`.
|
|
@@ -1,16 +1,38 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: generate-machine
|
|
3
|
-
description: Author a @statelyai/agent workflow as JSON and check it before it runs. Use when asked to generate or author an agent machine, create a workflow config, write an AgentWorkflowConfig, or produce an LLM-authored state machine for an agent — and when a generated config fails Ajv validation or agent lint and needs repair.
|
|
3
|
+
description: Author a @statelyai/agent workflow as JSON and check it before it runs. Use when asked to generate or author an agent machine, create a workflow config, write an AgentWorkflowConfig, or produce an LLM-authored state machine for an agent — and when a generated config fails verifyAgentConfig, Ajv validation, or agent lint and needs repair.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Generate an agent machine
|
|
7
7
|
|
|
8
|
-
An agent machine is data. You author a JSON `AgentWorkflowConfig`, then run it through gates
|
|
8
|
+
An agent machine is data. You author a JSON `AgentWorkflowConfig`, then run it through `verifyAgentConfig`, whose gates all work with no API key: validate → vocabulary → lower → lint → explore. Do not hand back a config that has not passed every gate.
|
|
9
9
|
|
|
10
10
|
```
|
|
11
|
-
|
|
11
|
+
split environment/policy → author (description = goal) → verifyAgentConfig → repair (≤ 3) → persist traces → hand back
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
To improve an existing policy from its traces, use the `improve-machine` skill instead.
|
|
15
|
+
|
|
16
|
+
## 0. Split environment and policy
|
|
17
|
+
|
|
18
|
+
Decide what is fixed before you write any JSON.
|
|
19
|
+
|
|
20
|
+
- **Environment** (TypeScript, fixed): the rules of the world and the business invariants, as states and guards. People author it and verify it with `canReach` / `explorePaths` from `@statelyai/agent/testing`. You never generate it, and no generated config may change it.
|
|
21
|
+
- **Policy** (JSON, generated): how the agent works toward the goal: which step next, which tool, when to ask a person, cheap judgment vs model call. This is the `AgentWorkflowConfig` you author.
|
|
22
|
+
- The policy plugs into the environment at its decision points and outputs one event; the environment's guards decide whether to accept it.
|
|
23
|
+
- No split needed for a single judgment inside a fixed workflow, or for a workflow where every step is dictated by rules. Author one machine.
|
|
24
|
+
|
|
25
|
+
If the request asks you to generate a business rule (an approval threshold, a payout limit), stop: that belongs in the environment, in TypeScript, reviewed by a person. Say so.
|
|
26
|
+
|
|
27
|
+
Before editing an existing machine, read it as text:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { describeMachine } from "@statelyai/agent";
|
|
31
|
+
const text = describeMachine(machine); // "Machine <id>", then "Goal: …", states, transitions, events
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Pass `{ snapshot }` to mark the active states with `*`. Concept page: [Environment and policy](https://github.com/statelyai/agent/blob/main/docs/environment-and-policy.md).
|
|
35
|
+
|
|
14
36
|
## 1. Read the schema
|
|
15
37
|
|
|
16
38
|
The config shape is fixed by a shipped JSON Schema. Read it first; it is the contract.
|
|
@@ -20,23 +42,32 @@ The config shape is fixed by a shipped JSON Schema. Read it first; it is the con
|
|
|
20
42
|
|
|
21
43
|
## 2. Author the config
|
|
22
44
|
|
|
45
|
+
Start with the root `description`: it is the machine's **goal**, the overall objective plus measurable success criteria. `describeMachine` renders it first as `Goal: …`, and an improver later climbs toward it. Write criteria a scorer can compute from outcomes (terminal states, effects in the world, cost), not "be helpful":
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
"description": "Resolve each support ticket in one session. Success: escalations only for tickets that need a human; every reply approved before sending; at most 2 model calls per ticket."
|
|
49
|
+
```
|
|
50
|
+
|
|
23
51
|
Rules the runtime enforces. Each one maps to a build error or a lint diagnostic:
|
|
24
52
|
|
|
25
53
|
- Every value is a JSON literal or a whole-string `"{{ }}"` expression reading a dot path on `input`, `context`, or `event`. No JavaScript.
|
|
26
54
|
- Guard expressions are truthy-only: no comparisons, operators, or negation.
|
|
27
|
-
- A model call is either a named `requests` entry invoked with `src: "<requestName>"`, or an inline invoke with `src: "agent.decide"` plus an `allowedEvents` list.
|
|
55
|
+
- A model call is either a named `requests` entry invoked with `src: "<requestName>"`, or an inline invoke with `src: "agent.decide"` plus an `allowedEvents` list. A `requests` entry's templates read only its own `input` (`"{{ input.ticket }}"`); the invoke's `input` maps context into it.
|
|
28
56
|
- A state invoking `agent.decide` MUST handle every allowed event in its `on`.
|
|
29
57
|
- An `agent.decide` invoke has no `onDone` (a decision produces no output). Use `onError` for the retries-exhausted path.
|
|
30
58
|
- A request invoke resolves with `{ result, messages }`, so `onDone.assign` reads its result from `"{{ event.output.result.<field> }}"` (`"{{ event.output.result }}"` for a plain-text request). A non-request `actors` invoke reads `"{{ event.output.<field> }}"` directly.
|
|
31
59
|
- Every path must reach a `"type": "final"` state, and each final state needs an `output` when `schemas.output` is declared.
|
|
32
60
|
- Do not invent guard or action names. Only `"{{ }}"` guards, `assign`, and `emit` exist unless the host tells you which named guards/actions it implements.
|
|
33
|
-
- Model refs are strings (`"openai/gpt-
|
|
61
|
+
- Model refs are strings (`"openai/gpt-6-luna"`); the host resolves them.
|
|
62
|
+
- To make a decision a closed choice, add `payloads` to the `agent.decide` input: one payload list per event type, without `type` (`"payloads": { "REFUND": [{ "amount": 20 }, { "amount": 50 }], "ESCALATE": [] }`). The model then picks one listed event instead of writing a payload. A type with no key is offered as `{ type }`; `[]` withholds it.
|
|
63
|
+
- An idle human step can carry `meta.interaction` for the host to render with `getInteraction`: a `label`, `events` (one button per event type), `choices` (one button per full event, `{ "label": "Refund $20", "event": { "type": "REFUND", "amount": 20 } }`, or a context path string holding such a list), and `textEvent` for free text. A bare `"textEvent": "REJECT"` puts the text in a `text` field; `{ "type": "REJECT", "field": "reason" }` names the field. `meta` is not template-evaluated.
|
|
34
64
|
|
|
35
65
|
Reference config — decision, text request, idle human step, one final state:
|
|
36
66
|
|
|
37
67
|
```json
|
|
38
68
|
{
|
|
39
69
|
"id": "support-ticket",
|
|
70
|
+
"description": "Resolve each support ticket in one session. Success: escalations only for tickets that need a human; every reply approved before sending; at most 2 model calls per ticket.",
|
|
40
71
|
"schemas": {
|
|
41
72
|
"input": {
|
|
42
73
|
"type": "object",
|
|
@@ -71,9 +102,9 @@ Reference config — decision, text request, idle human step, one final state:
|
|
|
71
102
|
"context": { "ticket": "{{ input.ticket }}" },
|
|
72
103
|
"requests": {
|
|
73
104
|
"draftReply": {
|
|
74
|
-
"model": "openai/gpt-
|
|
105
|
+
"model": "openai/gpt-6-luna",
|
|
75
106
|
"system": "Draft a short, courteous support reply.",
|
|
76
|
-
"prompt": "{{
|
|
107
|
+
"prompt": "{{ input.ticket }}",
|
|
77
108
|
"input": {
|
|
78
109
|
"type": "object",
|
|
79
110
|
"properties": { "ticket": { "type": "string" } },
|
|
@@ -93,7 +124,7 @@ Reference config — decision, text request, idle human step, one final state:
|
|
|
93
124
|
"id": "triageDecision",
|
|
94
125
|
"src": "agent.decide",
|
|
95
126
|
"input": {
|
|
96
|
-
"model": "openai/gpt-
|
|
127
|
+
"model": "openai/gpt-6-luna",
|
|
97
128
|
"system": "Decide whether this ticket needs escalation or a drafted reply.",
|
|
98
129
|
"prompt": "{{ context.ticket }}",
|
|
99
130
|
"allowedEvents": ["ESCALATE", "REPLY"],
|
|
@@ -132,36 +163,27 @@ Reference config — decision, text request, idle human step, one final state:
|
|
|
132
163
|
}
|
|
133
164
|
```
|
|
134
165
|
|
|
135
|
-
## 3.
|
|
136
|
-
|
|
137
|
-
`validateAgentConfig` from `@statelyai/agent/validate` checks a candidate against the shipped workflow schema with Ajv 2020 built in. It needs `ajv` installed (an optional peer of the package). Run it before anything else touches the config.
|
|
138
|
-
|
|
139
|
-
```ts
|
|
140
|
-
import { validateAgentConfig } from "@statelyai/agent/validate";
|
|
141
|
-
import type { AgentWorkflowConfig } from "@statelyai/agent";
|
|
142
|
-
|
|
143
|
-
function validateGeneratedConfig(candidate: unknown): AgentWorkflowConfig {
|
|
144
|
-
const result = validateAgentConfig(candidate);
|
|
145
|
-
if (result.valid) return candidate as AgentWorkflowConfig;
|
|
146
|
-
throw new Error(
|
|
147
|
-
result.errors.map((error) => `${error.path || "(root)"} ${error.message}`).join("\n"),
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
```
|
|
166
|
+
## 3. Verify with `verifyAgentConfig`
|
|
151
167
|
|
|
152
|
-
|
|
168
|
+
`verifyAgentConfig(config, options)` from `@statelyai/agent/validate` runs every pre-run gate and stops at the first failure. It never throws on a bad config; each gate is a record `{ name, ok, detail? }`. It needs `ajv` installed (an optional peer of the package).
|
|
153
169
|
|
|
154
|
-
|
|
170
|
+
| Gate | Checks with | Fails on |
|
|
171
|
+
| ------------ | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
172
|
+
| `validate` | `validateAgentConfig` (shipped schema) | malformed config shape |
|
|
173
|
+
| `vocabulary` | `allow` lists | an invoke src, named guard, named action, `agent.decide` event, or model ref not listed |
|
|
174
|
+
| `lower` | `setupAgent.fromConfig` | unresolved named guard/action, `onDone` on an `agent.decide` invoke |
|
|
175
|
+
| `lint` | `lintAgentMachine` | error findings (warnings too with `lint: { warnings: true }`) |
|
|
176
|
+
| `explore` | `explorePaths`, once per input | a path ending outside `explore.terminals` (default `['done']`), a loop past `maxDepth`, a `maxPaths`/`maxNodes` cap, untried payloads (`unexpandedEvents`) |
|
|
155
177
|
|
|
156
|
-
`
|
|
178
|
+
`compileSchema` turns the JSON Schemas _inside_ the config into Standard Schema validators for `fromConfig`. The library bundles no JSON Schema engine:
|
|
157
179
|
|
|
158
180
|
```ts
|
|
159
181
|
import Ajv from "ajv";
|
|
160
|
-
import {
|
|
182
|
+
import type { SchemaCompiler, StandardSchemaV1 } from "@statelyai/agent";
|
|
161
183
|
|
|
162
184
|
const ajv = new Ajv({ strict: false });
|
|
163
185
|
|
|
164
|
-
export const
|
|
186
|
+
export const compileSchema: SchemaCompiler = (jsonSchema, name): StandardSchemaV1 => {
|
|
165
187
|
const validate = ajv.compile(jsonSchema);
|
|
166
188
|
return {
|
|
167
189
|
"~standard": {
|
|
@@ -175,87 +197,139 @@ export const ajvCompileSchema: SchemaCompiler = (jsonSchema, name): StandardSche
|
|
|
175
197
|
message: `${name}${e.instancePath} ${e.message}`,
|
|
176
198
|
})),
|
|
177
199
|
},
|
|
178
|
-
// Expose the source JSON Schema so lint
|
|
200
|
+
// Expose the source JSON Schema so lint and describeMachine can read the shape.
|
|
179
201
|
jsonSchema: { input: () => jsonSchema },
|
|
180
202
|
},
|
|
181
203
|
};
|
|
182
204
|
};
|
|
205
|
+
```
|
|
183
206
|
|
|
184
|
-
|
|
207
|
+
Run the gates. List the host's real names in `allow`; an omitted key is unrestricted:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
import { verifyAgentConfig } from "@statelyai/agent/validate";
|
|
211
|
+
|
|
212
|
+
const result = await verifyAgentConfig(candidate, {
|
|
213
|
+
compileSchema,
|
|
214
|
+
allow: {
|
|
215
|
+
actors: [],
|
|
216
|
+
requests: ["draftReply"],
|
|
217
|
+
guards: [],
|
|
218
|
+
actions: [],
|
|
219
|
+
events: ["ESCALATE", "REPLY", "APPROVE", "REJECT"],
|
|
220
|
+
models: ["openai/gpt-6-luna"],
|
|
221
|
+
},
|
|
222
|
+
explore: {
|
|
223
|
+
inputs: [{ ticket: "Export downloads a 0-byte CSV on Safari." }],
|
|
224
|
+
outputs: { draftReply: [{ reply: "" }] },
|
|
225
|
+
// ESCALATE's `reason` is a free-form string: give exploration one to try.
|
|
226
|
+
payloads: () => ({ ESCALATE: [{ reason: "needs a human" }] }),
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
// result.ok, result.gates, result.config (after validate), result.machine (after lower)
|
|
185
230
|
```
|
|
186
231
|
|
|
187
|
-
|
|
232
|
+
- Exploration forks past idle waits on every event the waiting state accepts, so a human step still ends `done`. Add `terminals: ["done", "idle"]` only when a path may legitimately stop at a wait with nothing to accept.
|
|
233
|
+
- `guards` / `actions`: named implementations, passed to `fromConfig`. `actors`: bound with `machine.provide({ actors })` after lowering.
|
|
234
|
+
- `explore` keys other than `inputs` and `terminals` pass through to `explorePaths`: `outputs` (output lists per request or actor src: one element is a canned output, two or more fork one branch each), `errors` (fork a failing branch per src), `payloads` (fork one branch per payload at decisions and idle waits), `dedupeBy` (explore each snapshot key once; the key must cover every field a guard reads), `maxDepth` (default 8), `maxPaths` (default 200), `maxNodes` (default 100,000).
|
|
235
|
+
- Stub each request's output from its declared `output` schema (`""` for string, `0` for number, `[]` for array, recurse on `properties`). Add an `errors` entry for every src whose `onError` path matters.
|
|
236
|
+
- Pick `explore.inputs` that reach every branch the goal cares about. Exploration covers only the inputs you give it.
|
|
237
|
+
- A passing `lint` gate carries its warnings in `detail`. Report them.
|
|
188
238
|
|
|
189
|
-
##
|
|
239
|
+
## 4. Repair loop
|
|
240
|
+
|
|
241
|
+
A failed gate's `detail` names the offending path, state, or name, one problem per line. Feed it back verbatim with the rejected config, and run the gates again:
|
|
190
242
|
|
|
191
243
|
```ts
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
244
|
+
let prompt = originalTask;
|
|
245
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
246
|
+
const candidate = await author({ system, prompt });
|
|
247
|
+
const result = await verifyAgentConfig(candidate, verifyOptions);
|
|
248
|
+
if (result.ok) return result;
|
|
249
|
+
const failed = result.gates.find((gate) => !gate.ok)!;
|
|
250
|
+
prompt = `${originalTask}\n\nYour previous config:\n${JSON.stringify(candidate)}\n\nGate '${failed.name}' failed:\n${failed.detail}\nReturn a corrected config.`;
|
|
251
|
+
}
|
|
195
252
|
```
|
|
196
253
|
|
|
197
|
-
|
|
254
|
+
Keep the cap at 3. A config that fails three repairs is usually asking for something the data form cannot express: author it in TypeScript with `setupAgent` instead, or report that the vocabulary is missing something.
|
|
198
255
|
|
|
199
|
-
|
|
256
|
+
## 5. Manual fallback
|
|
200
257
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
Lint is structural. A dry run proves a path actually settles. No API key needed.
|
|
258
|
+
Without `verifyAgentConfig` (older package, or a gate you need to run alone), run the same gates by hand:
|
|
204
259
|
|
|
205
260
|
```ts
|
|
206
|
-
import {
|
|
261
|
+
import { setupAgent, type AgentWorkflowConfig } from "@statelyai/agent";
|
|
262
|
+
import { validateAgentConfig } from "@statelyai/agent/validate";
|
|
263
|
+
import { explorePaths, lintAgentMachine, simulateAgent } from "@statelyai/agent/testing";
|
|
264
|
+
|
|
265
|
+
const validation = validateAgentConfig(candidate); // { valid, errors: [{ path, message }] }, path is a JSON Pointer
|
|
266
|
+
if (!validation.valid) {
|
|
267
|
+
// Stop here: feed the errors back as the repair prompt. Never lower an invalid config.
|
|
268
|
+
throw new Error(validation.errors.map((e) => `${e.path} ${e.message}`).join("\n"));
|
|
269
|
+
}
|
|
270
|
+
const { machine } = setupAgent.fromConfig(candidate as AgentWorkflowConfig, { compileSchema }); // throws on unresolved names
|
|
271
|
+
lintAgentMachine(machine, { throw: true }); // throws AgentLintError on error findings
|
|
207
272
|
const dryRun = await simulateAgent(machine, {
|
|
208
273
|
input: { ticket: "" },
|
|
209
274
|
script: {
|
|
210
275
|
decisions: { "agent.decide": [{ type: "REPLY" }] },
|
|
211
276
|
text: { draftReply: [{ reply: "" }] },
|
|
212
277
|
},
|
|
213
|
-
});
|
|
214
|
-
|
|
278
|
+
}); // dryRun.status: 'done' | 'idle' | 'exhausted' ('exhausted' = loops, reject)
|
|
279
|
+
const report = await explorePaths(machine, {
|
|
280
|
+
input: { ticket: "" },
|
|
281
|
+
outputs: { draftReply: [{ reply: "" }] },
|
|
282
|
+
payloads: () => ({ ESCALATE: [{ reason: "needs a human" }] }),
|
|
283
|
+
}); // report.unexpandedEvents lists types that still need a payloads entry
|
|
215
284
|
```
|
|
216
285
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
- `'exhausted'` → the machine loops. Reject it.
|
|
220
|
-
- `'idle'` → it stopped at a human step. Expected when the config has one.
|
|
221
|
-
- To cover every branch instead of one path, use `explorePaths` / `canReach` (its result is `{ reachable, witness }`).
|
|
286
|
+
The lint check that bites most often is `decide-without-events`: an `allowedEvents` list with no matching `on` transitions. Do not disable checks.
|
|
222
287
|
|
|
223
|
-
##
|
|
288
|
+
## 6. Persist traces from day one
|
|
224
289
|
|
|
225
|
-
|
|
290
|
+
An improver needs data. Wire an event log store and a thread id per run before the first real run, so every run is a trace:
|
|
226
291
|
|
|
227
292
|
```ts
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
293
|
+
import { runAgent } from "@statelyai/agent";
|
|
294
|
+
import { createInMemoryEventLogStore } from "@statelyai/agent/log";
|
|
295
|
+
|
|
296
|
+
const store = createInMemoryEventLogStore(); // tests only; implement AgentEventLogStore on your database
|
|
297
|
+
const result = await runAgent(machine, {
|
|
298
|
+
input,
|
|
299
|
+
executors,
|
|
300
|
+
store,
|
|
301
|
+
threadId: `tickets:${ticketId}`,
|
|
302
|
+
});
|
|
235
303
|
```
|
|
236
304
|
|
|
237
|
-
|
|
305
|
+
- `threadId` is required with `store`. One thread per run.
|
|
306
|
+
- Give thread ids a prefix an improver can list by, such as `tickets:<ticketId>`. `store.listThreads({ prefix, limit, cursor })` pages through them. It is optional on a custom `AgentEventLogStore`, so with a store that lacks it, record each thread id with its scenario or ticket.
|
|
307
|
+
- Failed decision attempts are journaled on the entry they explain; `getDecisionAttempts(entries)` from `@statelyai/agent/log` reads them back.
|
|
308
|
+
- `replay(machine, await store.read(threadId))` from `@statelyai/agent/log` rebuilds the final state with no model calls. Pass the machine that recorded the thread: with an environment and a policy, the environment; `getUsageFromEvents(entries)` sums token usage.
|
|
238
309
|
|
|
239
|
-
##
|
|
310
|
+
## 7. Hand back
|
|
240
311
|
|
|
241
312
|
Deliver the config JSON plus how to run it:
|
|
242
313
|
|
|
243
314
|
```ts
|
|
244
|
-
import {
|
|
315
|
+
import { runAgent } from "@statelyai/agent";
|
|
245
316
|
import { createAiSdkExecutors } from "@statelyai/agent/ai-sdk";
|
|
246
317
|
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
318
|
+
const run = await runAgent(result.machine!, {
|
|
319
|
+
input: { ticket: "Export downloads a 0-byte CSV on Safari." },
|
|
320
|
+
executors: createAiSdkExecutors({ resolveModel }),
|
|
321
|
+
store,
|
|
322
|
+
threadId,
|
|
323
|
+
});
|
|
251
324
|
```
|
|
252
325
|
|
|
253
|
-
Say which gates passed, list any
|
|
326
|
+
Say which gates passed (list `result.gates`), list any lint warnings, state the goal and its success criteria, and say where traces are written. Model refs are strings, so the host supplies `resolveModel`.
|
|
254
327
|
|
|
255
328
|
## Known limits
|
|
256
329
|
|
|
257
330
|
- **Named guards and actions are host-resolved.** A config carries no functions. `guard: "isReady"` only works if the host passes an `isReady` implementation to `fromConfig`; an unresolved name is a build-time throw. Either list the host's names in the prompt or forbid named guards/actions entirely.
|
|
258
331
|
- **Expressions are dot-path templates only.** `"{{ context.a.b }}"` — no comparisons, no arithmetic, no method calls, no partial interpolation inside a larger string.
|
|
259
|
-
- **Ajv validity is not semantic validity.** A config can validate and still invoke a request that does not exist or route to a state that solves nothing. That is what lint and
|
|
260
|
-
- **
|
|
261
|
-
- **Model refs
|
|
332
|
+
- **Ajv validity is not semantic validity.** A config can validate and still invoke a request that does not exist or route to a state that solves nothing. That is what the `vocabulary`, `lint`, and `explore` gates are for.
|
|
333
|
+
- **Exploration covers the inputs you give it.** A branch no `explore.inputs` entry reaches is unverified.
|
|
334
|
+
- **Model refs are checked only with `allow.models`.** Tool names stay unchecked strings that resolve at run time, in the host.
|
|
335
|
+
- **The gates check structure, not quality.** Whether a config is better than another is measured by replaying scenarios against an objective. See the `improve-machine` skill.
|