Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .changeset/studio-dryrun-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@statelyai/agent": patch
---

Fixes from installing the package in an existing app for the first time.

- **Plain objects from another realm are JSON.** A jest test's vm context gives objects another `Object.prototype`, so `metadata`, store reads, and errors from there were rejected and a run could silently lose its log. The log's checks now accept them.
- **A run with a `store` fails rather than run unlogged** when its first entry cannot be written: `{ status: "error", cause: "journal" }`.
- **Journaled errors keep their `code`** (a string or a number), and `replay` turns them back into `Error` objects. They carry an `"@error": true` marker, so a thrown plain `{ name, message }` replays as the plain object it was.
- **A throwing `onTrace`, `onTransition`, `onChunk`, `onResult`, `onEvent`, or `on` handler no longer fails the run**; it is reported with `console.error`.
- **Reusing a thread id with different input throws `thread-input-mismatch`** instead of returning the other run's result.
- **`setupAgent` throws `invalid-schema` for a schema without `~standard`** (zod 3.23 and earlier) instead of skipping its validation.
- **`describeMachine` prints `?`** for a transition whose target comes from a helper call, instead of `(stays)`.
- **Subpath types resolve under `moduleResolution: "node"`** (`typesVersions`), and `engines` allows Node 22.
- **`ai` peer floor is 7.0.128,** the first release with `experimental_decide`, which `@statelyai/agent/ai-sdk` imports.
- **Undefined fields are dropped from input and events** before they are journaled, as `JSON.stringify` drops them. Any other value the log cannot hold fails a run that has a `store`, and stops the log with a `console.warn` when it does not, instead of failing one way for input and another for events.
- **`setupAgent` throws `invalid-request`** for a request with neither `prompt` nor `messages`, instead of every call failing inside the invoke.
- **`ExploreSnapshot` and `CanReachTarget` types** from `@statelyai/agent/testing`, for exploration callbacks in an app whose own `xstate` is another version.
- **`CHANGELOG.md` ships in the package,** and a new "Upgrading between alphas" page lists the breaking changes since earlier alphas.
- **`runSeam`'s `respond` may return a promise,** so a seam eval can answer idle pauses with a real async client.
2 changes: 1 addition & 1 deletion docs/environment-and-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ The split pays off when there is a policy worth learning. Skip it in these cases
- [Machines as data](machines-as-data.md): the config format and `describeMachine`.
- [Testing and verification](verify.md): `canReach`, `explorePaths`, and payload variants.
- [Event log](event-log.md) and [Persistence](persistence.md): stores, threads, and replay.
- The [`graph-engineering`](https://github.com/statelyai/agent/blob/main/skills/graph-engineering/SKILL.md) skill routes existing code to [`loop-to-machine`](https://github.com/statelyai/agent/blob/main/skills/loop-to-machine/SKILL.md), which converts the loop and checks parity with `compareRuns`. It routes new policies to [`generate-machine`](https://github.com/statelyai/agent/blob/main/skills/generate-machine/SKILL.md). Then [`add-traces`](https://github.com/statelyai/agent/blob/main/skills/add-traces/SKILL.md) persists runs, [`add-evals`](https://github.com/statelyai/agent/blob/main/skills/add-evals/SKILL.md) writes evaluations for each state, and [`improve-machine`](https://github.com/statelyai/agent/blob/main/skills/improve-machine/SKILL.md) proposes a pull request from traces, or reports that no candidate beat the current policy.
- The [`graph-engineering`](https://github.com/statelyai/agent/blob/next/skills/graph-engineering/SKILL.md) skill routes existing code to [`loop-to-machine`](https://github.com/statelyai/agent/blob/next/skills/loop-to-machine/SKILL.md), which converts the loop and checks parity with `compareRuns`. It routes new policies to [`generate-machine`](https://github.com/statelyai/agent/blob/next/skills/generate-machine/SKILL.md). Then [`add-traces`](https://github.com/statelyai/agent/blob/next/skills/add-traces/SKILL.md) persists runs, [`add-evals`](https://github.com/statelyai/agent/blob/next/skills/add-evals/SKILL.md) writes evaluations for each state, and [`improve-machine`](https://github.com/statelyai/agent/blob/next/skills/improve-machine/SKILL.md) proposes a pull request from traces, or reports that no candidate beat the current policy.
- [examples/self-improving-refunds](../examples/self-improving-refunds): a refund desk as the environment, JSON playbooks as the policy, an improvement loop, and a benchmark against a raw tool loop.
- [examples/code-review-loop](../examples/code-review-loop): review gates as the environment, a triage policy improved from traces against hidden tests, run with Claude Code in headless mode.
- [examples/tic-tac-toe](../examples/tic-tac-toe): a game as the environment, a policy over board features, and a `canReach` proof that it never loses as the bar for promotion.
6 changes: 4 additions & 2 deletions docs/event-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ interface AgentLogEntry {

- `recordedAt` is acceptance metadata, not machine time. A transition that needs time takes it from the event.
- `machineVersion` is the machine's declared `version`, or its structural hash when none is declared.
- `verification.stateHash` is the hash of the persisted snapshot `replay` folds to after the entry is applied. A run records replay's fold, not its live snapshot: replay never starts children, so a live child that already settled while its completion waits to be delivered is not part of the hash.
- `verification.stateHash` is the hash of the persisted snapshot `replay` folds to after the entry is applied. A run records replay's fold, not its live snapshot: replay never starts children, so a live child that already settled while its completion waits to be delivered is not part of the hash. So the hash checks each entry against the fold of the entries before it, not that the log is complete or that the fold matches the live run. A dropped last entry leaves the remaining hashes intact, an altered event that folds to the same state still matches, and a machine that reads something the log does not keep can replay to a different state. Compare the replayed context and output with the live result in a test.
- `metadata` is host-owned and stored verbatim. The init entry's `metadata.executionId` names the lineage; read it with `getLogExecutionId(entries)`.
- To stamp a run with what produced it (a policy version, model ids, a scenario), pass `metadata` to `createAgentRuntime` or `runAgent`. It lands on the init entry next to `executionId`, which is reserved. A resume keeps the stamp of the run that started the log; a version bridge opens a new segment stamped with the bridging run's `metadata`. A value that is not JSON throws before the run starts.
- `attempts` holds the decision attempts the machine did not accept before this entry's event landed. Replay ignores it. See [Decision attempts](#decision-attempts).

Entries are strict JSON. `createReplayEntry`, `initEntry`, and every store append reject values JSON would drop or coerce (`undefined`, functions, symbols, bigint, non-finite numbers, `Date`, `Map`, `Set`, class instances, cycles) with `NonSerializableAgentEventError`, which carries the offending `path`. `Error` payloads on `xstate.error.actor` are normalized to `{ name, message, cause? }`; an `AgentDecisionExhaustedError` also keeps its `attempts`. Use `assertJsonSerializable` and `assertAgentLogEntry` at custom transport boundaries.
Entries are strict JSON, and plain objects and arrays from another realm (a jest test's vm context) count as JSON. Object fields set to `undefined` in an input or event are dropped, as `JSON.stringify` drops them. `createReplayEntry`, `initEntry`, and every store append reject the other values JSON would drop or coerce (functions, symbols, bigint, non-finite numbers, `Date`, `Map`, `Set`, class instances, cycles, `undefined` array items) with `NonSerializableAgentEventError`, which carries the offending `path`. `Error` payloads on `xstate.error.actor` are normalized to `{ name, message, code?, cause?, "@error": true }`, keeping `code` when it is a string or a number; an `AgentDecisionExhaustedError` also keeps its `attempts`. `replay` turns objects marked `"@error"` back into `Error` objects, so an `onError` that checks `instanceof Error` takes the same branch on replay. A thrown plain object stays a plain object. Other properties of the thrown value are not kept. Use `assertJsonSerializable` and `assertAgentLogEntry` at custom transport boundaries.

## Record a log

Expand Down Expand Up @@ -72,6 +72,8 @@ const result = await runToQuiescence(
- A rejected write (an `AgentEventLogConflictError`, or any storage failure) aborts in-flight work and settles the run `{ status: "error", cause: "journal" }`. No further calls run.
- A `@agent.usage` entry that arrives after the run settled is still written, but the result does not wait for it. Await `result.drain()` before terminating the process if you care about those straggler entries; it resolves once every write issued so far has landed, and never rejects (a failed write already settled the run with `cause: "journal"`).
- `threadId` is required with a `store`; without it `runToQuiescence` throws `AgentError` with code `missing-thread-id`.
- A thread that already has entries is resumed. `input` that differs from the input the thread started with throws `AgentError` with code `thread-input-mismatch`, since two runs sharing a thread id would otherwise get one result.
- If an entry cannot be written (an `input` or event that is not JSON), the run settles `{ status: "error", cause: "journal" }` instead of running without a log. Without a store, the log stops at that entry with a `console.warn`, and the run goes on.
- Passing `events` as well makes that log the resume, and it must BE the thread's log: a different length throws `AgentEventLogConflictError`, and an entry-for-entry mismatch at the same length throws `AgentError` with code `event-log-conflict`.

### `onEvent`: observer
Expand Down
4 changes: 2 additions & 2 deletions docs/from-a-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ pnpm exec tsx examples/code-review-loop/run.ts --improve --policy v0
4. Replay the train and held-out tasks with the candidate.
5. Promote only with zero violations on both splits, a held-out objective no lower than before, and a combined objective strictly higher. `run.ts` writes a promoted policy to `policies/current.json`.

Inside Claude Code, the [`improve-machine` skill](https://github.com/statelyai/agent/blob/main/skills/improve-machine/SKILL.md) runs the same round with Claude Code as the improver:
Inside Claude Code, the [`improve-machine` skill](https://github.com/statelyai/agent/blob/next/skills/improve-machine/SKILL.md) runs the same round with Claude Code as the improver:

```bash
mkdir -p .claude/skills && cp -R skills/improve-machine .claude/skills/
Expand All @@ -512,4 +512,4 @@ The full prompt and its allowed tools are in the [example's README](../examples/
- [Human in the loop](human-in-the-loop.md): idle states, `meta.interaction`, and resuming.
- [Environment and policy](environment-and-policy.md): the fixed environment, the generated policy, the objective, and the improvement loop.
- [code-review-loop](../examples/code-review-loop): Part 2's factory: `before.ts`, the machine, the policies, and the improvement round on Claude Code.
- [`skills/improve-machine`](https://github.com/statelyai/agent/blob/main/skills/improve-machine/SKILL.md): one improvement round from traces, run by an agent.
- [`skills/improve-machine`](https://github.com/statelyai/agent/blob/next/skills/improve-machine/SKILL.md): one improvement round from traces, run by an agent.
4 changes: 2 additions & 2 deletions docs/generate-machines.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@ A failed gate's `detail` is the repair prompt. Send it back to the model with th

The full authoring procedure ships as an agent skill. It covers the environment and policy split, the schema location, the rules a config must follow, a few-shot config, the Ajv adapter, `verifyAgentConfig`, the repair loop, and persisting traces.

**[`skills/generate-machine/SKILL.md`](https://github.com/statelyai/agent/blob/main/skills/generate-machine/SKILL.md)**
**[`skills/generate-machine/SKILL.md`](https://github.com/statelyai/agent/blob/next/skills/generate-machine/SKILL.md)**

Copy that directory into your coding agent's skills directory. For Claude Code this is `.claude/skills/`. Other harnesses use their own location. The agent then loads the skill on requests such as "author an agent machine for X" and runs the gates itself.

To improve a generated policy from its run traces, use [`skills/improve-machine`](https://github.com/statelyai/agent/blob/main/skills/improve-machine/SKILL.md). See [Environment and policy](environment-and-policy.md).
To improve a generated policy from its run traces, use [`skills/improve-machine`](https://github.com/statelyai/agent/blob/next/skills/improve-machine/SKILL.md). See [Environment and policy](environment-and-policy.md).

## Limits

Expand Down
1 change: 1 addition & 0 deletions docs/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"hosts",
"any-stack",
"xstate-4-and-5-apps",
"upgrading",
"steps",
"advanced",
"---State & durability---",
Expand Down
2 changes: 1 addition & 1 deletion docs/models-and-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export const hostAgent = new Agent({
});
```

Any framework that exposes a `LanguageModel` works the same way. The machine and the Mastra agent share one model definition. See [`examples/mastra-host`](https://github.com/statelyai/agent/tree/main/examples/mastra-host) for the full bridge, including pause and resume across tool calls.
Any framework that exposes a `LanguageModel` works the same way. The machine and the Mastra agent share one model definition. See [`examples/mastra-host`](https://github.com/statelyai/agent/tree/next/examples/mastra-host) for the full bridge, including pause and resume across tool calls.

## LangChain models

Expand Down
2 changes: 2 additions & 0 deletions docs/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ A `machine.transition` event carries `from.value`, the state value the event arr

A `request.end` event carries the call's `output` and `raw`, plus what the executor reported about the call itself: `usage` when it reported tokens, and `finishReason` when it reported why the call stopped, normalized to `'stop'`, `'length'`, `'tool-calls'`, `'content-filter'`, or `'other'`. See [Finish reason and truncation](text-requests.md#finish-reason-and-truncation).

Trace handlers are observers: an `onTrace`, `onTransition`, `onChunk`, `onResult`, `onEvent`, or `on` handler that throws is reported with `console.error`, and the run continues.

`serializeTraceEvent` creates a JSON-safe projection. Traces are observations, not a persistence protocol; the [event log](event-log.md) is.

`usage.dropped` means the `@agent.usage` event was not delivered to the machine, because no active state accepted it or the leg had already settled. The spend is still journaled and still counted: the entry is in the event log, and `getUsageFromEvents` folds it into the totals. Only the machine event is dropped.
Expand Down
6 changes: 3 additions & 3 deletions docs/scope.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ The host decides how it happens, binding `searchWeb` to the search client, cache

The repository examples show the orchestration shape. They are not replacement implementations of the systems listed above.

- [Deep research](https://github.com/statelyai/agent/blob/main/examples/deep-research/index.ts) models planning, concurrent research, reflection, and synthesis. The host still supplies the search implementation.
- [Hierarchical teams](https://github.com/statelyai/agent/blob/main/examples/hierarchical-teams/index.ts) models specialized child agents. External data and side effects remain host-owned.
- [Deep research](https://github.com/statelyai/agent/blob/next/examples/deep-research/index.ts) models planning, concurrent research, reflection, and synthesis. The host still supplies the search implementation.
- [Hierarchical teams](https://github.com/statelyai/agent/blob/next/examples/hierarchical-teams/index.ts) models specialized child agents. External data and side effects remain host-owned.

## Core criteria

The rules for what belongs in core live in [CONTRIBUTING.md](https://github.com/statelyai/agent/blob/main/CONTRIBUTING.md).
The rules for what belongs in core live in [CONTRIBUTING.md](https://github.com/statelyai/agent/blob/next/CONTRIBUTING.md).

## Non-goals

Expand Down
Loading
Loading