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
36 changes: 18 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ jobs:
PY
- name: Distributions bundle the exact event ABI corpus (#144)
run: |
for f in trace-v6-execution.ndjson trace-v6-lifecycle.ndjson runner-v10-refusal.ndjson; do
for f in trace-v7-execution.ndjson trace-v7-lifecycle.ndjson runner-v10-refusal.ndjson; do
unzip -l dist/*.whl | grep -q "droste/testing/fixtures/$f" || {
echo "wheel is missing droste/testing/fixtures/$f"; exit 1;
}
Expand All @@ -154,21 +154,21 @@ jobs:

from droste.testing import (
runner_v10_refusal_ndjson,
trace_v6_execution_ndjson,
trace_v6_lifecycle_ndjson,
trace_v7_execution_ndjson,
trace_v7_lifecycle_ndjson,
)

source = Path(sys.argv[1])
assert trace_v6_execution_ndjson() == (source / "trace-v6-execution.ndjson").read_bytes()
assert trace_v6_lifecycle_ndjson() == (source / "trace-v6-lifecycle.ndjson").read_bytes()
assert trace_v7_execution_ndjson() == (source / "trace-v7-execution.ndjson").read_bytes()
assert trace_v7_lifecycle_ndjson() == (source / "trace-v7-lifecycle.ndjson").read_bytes()
assert runner_v10_refusal_ndjson() == (source / "runner-v10-refusal.ndjson").read_bytes()
PY
sdist_root="$(tar tzf dist/droste-*.tar.gz | sed -n '1s#/.*##p')"
tar xzf dist/droste-*.tar.gz -C "$tmp"
cmp src/droste/testing/fixtures/trace-v6-lifecycle.ndjson \
"$tmp/$sdist_root/src/droste/testing/fixtures/trace-v6-lifecycle.ndjson"
cmp src/droste/testing/fixtures/trace-v6-execution.ndjson \
"$tmp/$sdist_root/src/droste/testing/fixtures/trace-v6-execution.ndjson"
cmp src/droste/testing/fixtures/trace-v7-lifecycle.ndjson \
"$tmp/$sdist_root/src/droste/testing/fixtures/trace-v7-lifecycle.ndjson"
cmp src/droste/testing/fixtures/trace-v7-execution.ndjson \
"$tmp/$sdist_root/src/droste/testing/fixtures/trace-v7-execution.ndjson"
cmp src/droste/testing/fixtures/runner-v10-refusal.ndjson \
"$tmp/$sdist_root/src/droste/testing/fixtures/runner-v10-refusal.ndjson"

Expand Down Expand Up @@ -319,7 +319,7 @@ jobs:
PY
- name: Distributions bundle the exact event ABI corpus (#144)
run: |
for f in trace-v6-execution.ndjson trace-v6-lifecycle.ndjson runner-v10-refusal.ndjson; do
for f in trace-v7-execution.ndjson trace-v7-lifecycle.ndjson runner-v10-refusal.ndjson; do
unzip -l dist/*.whl | grep -q "droste/testing/fixtures/$f" || {
echo "wheel is missing droste/testing/fixtures/$f"; exit 1;
}
Expand All @@ -333,21 +333,21 @@ jobs:

from droste.testing import (
runner_v10_refusal_ndjson,
trace_v6_execution_ndjson,
trace_v6_lifecycle_ndjson,
trace_v7_execution_ndjson,
trace_v7_lifecycle_ndjson,
)

source = Path(sys.argv[1])
assert trace_v6_execution_ndjson() == (source / "trace-v6-execution.ndjson").read_bytes()
assert trace_v6_lifecycle_ndjson() == (source / "trace-v6-lifecycle.ndjson").read_bytes()
assert trace_v7_execution_ndjson() == (source / "trace-v7-execution.ndjson").read_bytes()
assert trace_v7_lifecycle_ndjson() == (source / "trace-v7-lifecycle.ndjson").read_bytes()
assert runner_v10_refusal_ndjson() == (source / "runner-v10-refusal.ndjson").read_bytes()
PY
sdist_root="$(tar tzf dist/droste-*.tar.gz | sed -n '1s#/.*##p')"
tar xzf dist/droste-*.tar.gz -C "$tmp"
cmp src/droste/testing/fixtures/trace-v6-lifecycle.ndjson \
"$tmp/$sdist_root/src/droste/testing/fixtures/trace-v6-lifecycle.ndjson"
cmp src/droste/testing/fixtures/trace-v6-execution.ndjson \
"$tmp/$sdist_root/src/droste/testing/fixtures/trace-v6-execution.ndjson"
cmp src/droste/testing/fixtures/trace-v7-lifecycle.ndjson \
"$tmp/$sdist_root/src/droste/testing/fixtures/trace-v7-lifecycle.ndjson"
cmp src/droste/testing/fixtures/trace-v7-execution.ndjson \
"$tmp/$sdist_root/src/droste/testing/fixtures/trace-v7-execution.ndjson"
cmp src/droste/testing/fixtures/runner-v10-refusal.ndjson \
"$tmp/$sdist_root/src/droste/testing/fixtures/runner-v10-refusal.ndjson"

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ jobs:
staging="droste-relay-$GITHUB_REF_NAME"
mkdir -p "$staging/conformance" relay-dist
cp src/droste/substrates/_relay/*.ts pyodide/README.md "$staging/"
cp src/droste/testing/fixtures/trace-v6-execution.ndjson \
src/droste/testing/fixtures/trace-v6-lifecycle.ndjson \
cp src/droste/testing/fixtures/trace-v7-execution.ndjson \
src/droste/testing/fixtures/trace-v7-lifecycle.ndjson \
src/droste/testing/fixtures/runner-v10-refusal.ndjson \
"$staging/conformance/"
printf '%s %s\n' "$GITHUB_REF_NAME" "$GITHUB_SHA" > "$staging/DROSTE_VERSION"
Expand Down
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ evidence with that status rather than leaving the model to interpret prefixes.

## Trace ABI

- Every structured event is a strict Trace ABI v6 value. Stamp it exactly once
- Every structured event is a strict Trace ABI v7 value. Stamp it exactly once
through `ExecutionContext`; do not emit raw or partially enveloped event
dictionaries at host boundaries.
- Treat every envelope/body, classification, and ordering change as an ABI
Expand Down Expand Up @@ -189,7 +189,7 @@ evidence with that status rather than leaving the model to interpret prefixes.
- A strict published event vocabulary/body change requires a Trace ABI bump and,
when embedded in runner output, an atomic runner-protocol bump. Do not expand
an old strict version in place or add a compatibility decoder in the engine.
- Trace ABI v6 usage is `resolved` only when both root and subcall scopes have
- Trace ABI v7 usage is `resolved` only when both root and subcall scopes have
complete provider usage. Missing or malformed usage preserves any known
counts, marks that scope `complete=false`, and makes terminal usage
`kind="partial"`; never report conservative reservations as provider usage.
Expand Down Expand Up @@ -239,7 +239,7 @@ evidence with that status rather than leaving the model to interpret prefixes.
partial evidence rather than invalidating an otherwise valid envelope.
- `reasoning_tokens` is a non-negative breakdown inside `completion_tokens`.
Preserve it through internal usage copies, folds, and root/subcall
`ExecutionStats`, but do not add it to totals or Trace ABI v6 events.
`ExecutionStats`, but do not add it to totals or Trace ABI v7 events.
Observation basis and reasoning usage are
internal callback/accounting facts; do not add them to the public usage
projections.
Expand Down Expand Up @@ -272,7 +272,7 @@ evidence with that status rather than leaving the model to interpret prefixes.
partial and settle conservatively rather than falling back to the start
estimate.
- `ExecutionStats` folds those same cache classes separately for root and
subcall scopes. Trace ABI v6 exposes them in every durable usage breakdown;
subcall scopes. Trace ABI v7 exposes them in every durable usage breakdown;
complete scopes require the disjoint cache classes to fit inside inclusive
input tokens, while partial scopes preserve independently validated counts.
ModelRelay names the classes `cache_read_input_tokens` and
Expand Down
24 changes: 24 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,30 @@ consumers, and Pyodide-substrate integrations staging the Deno relay.

## Unreleased (post-0.21.1)

### Trace ABI v7 publishes answer-state checkpoints

The new configurable `checkpoint` event carries the draft the engine currently
holds — `checkpoint_seq`, `draft`, `draft_chars`, `ready`, and an opaque
`payload` — after every executed step whose draft moved. A host that loses the
process (a watchdog kill, a crashed substrate) can render the last checkpoint
instead of nothing. It is retention-gated like every other content-bearing
event, so it reaches a terminal record only when named in
`TraceRetentionPolicy.retain`.

`RLMConfig.checkpoint_payload_provider` is the optional callable that fills
`payload`. It returns any JSON object or `None`; the engine never inspects the
value and never schema-checks it. A provider that raises is reported through a
`RuntimeWarning` and the checkpoint carries `payload: null` — a checkpoint can
never fail a run.

Hosts must accept Trace ABI 7 and update to the `trace_v7_*` conformance
fixtures (`droste.testing.trace_v7_lifecycle_ndjson` /
`trace_v7_execution_ndjson`, backing `trace-v7-lifecycle.ndjson` /
`trace-v7-execution.ndjson`). Scaffold manifests report `abis.trace: 7`, so
every manifest id changes; pinned ids must be re-derived. Strict v6 readers
reject the new event and the new version, so this is an atomic consumer
migration.

## 0.21.1 (from 0.21.0)

### Streamed Responses preserve their terminal stop reason
Expand Down
6 changes: 3 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,14 +293,14 @@ exceptions use the same closed five-field shape as other preflight responses;
run exceptions use the ordinary run response shape.

Completed responses also carry the policy-resolved
[Trace ABI v6](trace-abi.md) `run_record`. Live events and terminal records use
[Trace ABI v7](trace-abi.md) `run_record`. Live events and terminal records use
the same strict envelope and projection. Persistence remains a host I/O
decision; the engine never opens a trace store.

The Deno/Pyodide relay keeps its three process output concerns physically
separate. fd1 carries exactly one unary response JSON line. A required
`DROSTE_RELAY_EVENT_FD` names one inherited writable descriptor (fd3 by
convention) that carries canonical Trace ABI v6 NDJSON only. An external
convention) that carries canonical Trace ABI v7 NDJSON only. An external
launcher also includes that number in `DENO_EXTRA_STDIO_FDS`, which Deno
consumes at startup to register inherited descriptors above fd2. Passing an
OS-level descriptor without this Deno marker leaves it unavailable to relay
Expand Down Expand Up @@ -376,7 +376,7 @@ are versioned, each by a single integer:
Protocol v9 embeds Trace ABI v5 and its transient cumulative
`usage_progress` event at settled root/subcall boundaries. The bump prevents
a v8 host from silently dropping the live accounting signal.
Protocol v10 embeds Trace ABI v6 and scaffold manifest v3, adding the
Protocol v10 embeds Trace ABI v7 and scaffold manifest v3, adding the
required seventh `max_iterations` authorization field. The exact seven-field
budget prevents a v9 host from silently submitting an unbounded loop
contract.
Expand Down
2 changes: 1 addition & 1 deletion docs/budgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ reservations fails loudly.

## Trace facts

Every mutation is a durable Trace ABI v6 `budget` event from
Every mutation is a durable Trace ABI v7 `budget` event from
`source="budget_ledger"`: `reserve`, `commit`, `refund`, or `exhaust`.
Mutation events carry `resource`, non-negative `amount`, and `call_id`. The
terminal snapshot records the configured, consumed, and remaining vectors.
Expand Down
2 changes: 1 addition & 1 deletion docs/scaffold-manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ inference facts.
"schema_version": 3,
"engine": {"version": "0.10.6", "source_revision": null},
"abis": {
"kernel": 1, "capability": 1, "trace": 6,
"kernel": 1, "capability": 1, "trace": 7,
"prompt_pack": 2, "provider": 4, "runner": 10
},
"prompt_pack": {
Expand Down
39 changes: 29 additions & 10 deletions docs/trace-abi.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Trace ABI v6
# Trace ABI v7

Droste exposes one append-only event stream and one policy-resolved terminal
`RunRecord`. The engine creates values; it does not choose a database or write
Expand All @@ -15,15 +15,15 @@ does not change merely because a released fixture is added.

## Event envelope

Every event is a strict v6 value with these fields:
Every event is a strict v7 value with these fields:

```json
{
"run_id": "e0f7...",
"seq": 1,
"timestamp": "2026-07-14T05:00:00Z",
"type": "progress",
"version": 6,
"version": 7,
"persistence_class": "transient",
"parent_run_id": "optional-parent",
"depth": 0,
Expand All @@ -48,15 +48,15 @@ The persistence class is exhaustive and fixed by event type:
| Class | Event types | Rule |
| --- | --- | --- |
| `durable` | `usage`, `budget`, `policy`, `capability`, `done` | Always in the terminal record |
| `configurable` | `iteration_start`, `llm_response`, `code`, `output`, `execution_error`, `subcall`, `repair`, `extract`, `result`, `replay` | Included only when named by `TraceRetentionPolicy.retain` |
| `configurable` | `iteration_start`, `llm_response`, `code`, `output`, `execution_error`, `subcall`, `repair`, `extract`, `result`, `replay`, `checkpoint` | Included only when named by `TraceRetentionPolicy.retain` |
| `transient` | `startup`, `progress`, `reasoning_delta`, `usage_progress` | Live delivery only; never in the terminal record |

Retention governs the terminal record, not the live channel. `result` is
always delivered once before `done`, even when it is not retained. `replay` is
different: it is emitted only when the host explicitly selects replay
retention.

## Exhaustive v6 bodies
## Exhaustive v7 bodies

Every event body has a fixed top-level schema. Optional fields are marked `?`.
Objects named below are JSON objects; all other types are primitive.
Expand All @@ -75,6 +75,7 @@ Objects named below are JSON objects; all other types are primitive.
| `repair` | `phase: "start"|"completion"|"failure"`, `kind: "missing_code"|"execution_error"|"terminal"`, `iteration`, `error?` only on failure |
| `extract` | `phase: "start"|"completion"|"failure"`, `iteration`, `extract_error?` only on failure |
| `result`, `replay` | `result: object` |
| `checkpoint` | `iteration: integer`, `checkpoint_seq: integer`, `draft: string`, `draft_chars: integer`, `ready: boolean`, `payload: object|null` |
| `usage_progress` | `boundary: "root"|"subcall"`, `kind`, cumulative `root`, `subcall`, `unattributed`, and `total_tokens` |
| `usage` | `kind: "resolved"|"partial"`, `root: object`, `subcall: object`, `unattributed: object`, `total_tokens: integer`, `wall_time_ms: integer` |
| `budget` | `kind: "snapshot"|"mutation"`, `source: string`, plus the kind-specific fields below |
Expand Down Expand Up @@ -118,7 +119,7 @@ partial observation preserves its reported counters and marks the affected
scope incomplete. The last progress snapshot reconciles with terminal `usage`
unless a later boundary has no numeric usage observation.

The budget body remains a discriminated event in Trace ABI v6. The terminal snapshot uses
The budget body remains a discriminated event in Trace ABI v7. The terminal snapshot uses
`kind="snapshot"`, `source="budget_ledger"`, and `configured`, `consumed`,
and `remaining` objects. The configured object includes the structural
`max_iterations` ceiling; terminal `iterations` records how many iterations
Expand Down Expand Up @@ -193,6 +194,24 @@ start and exactly one completion or failure once entered. Extract fallback does
the same; a failure carries the typed `extract_error`. The canonical `result`
still carries the unary-equivalent answer and `done` remains content-free.

## Answer-state checkpoints

`checkpoint` publishes the answer state the engine holds right now, so a host
that loses the process still has the last draft it saw. The engine emits one
after every executed step whose draft moved, or whose host had something of its
own to add. `checkpoint_seq` starts at one and strictly increases within a
`run_id`; `draft_chars` always equals `len(draft)`.

`payload` is opaque. Hosts fill it through `RLMConfig.checkpoint_payload_provider`,
a callable returning any JSON object or `None`. The engine carries the value
across without inspecting it and never schema-checks its contents, exactly as
the relay carries adapter `meta`. A provider that raises is reported and the
checkpoint carries `payload: null`: a checkpoint can never fail a run.

The event is `configurable`, not `durable`. It carries draft content, so it
reaches a terminal record only when a host names it in
`TraceRetentionPolicy.retain`.

## Terminal reconciliation

Finalization emits resolved `usage`, `budget`, and `policy`; always delivers the
Expand All @@ -211,12 +230,12 @@ and sdist. Python consumers load them through package resources:
```python
from droste.testing import (
runner_v10_refusal_ndjson,
trace_v6_execution_ndjson,
trace_v6_lifecycle_ndjson,
trace_v7_execution_ndjson,
trace_v7_lifecycle_ndjson,
)

execution_lines = trace_v6_execution_ndjson().splitlines()
event_lines = trace_v6_lifecycle_ndjson().splitlines()
execution_lines = trace_v7_execution_ndjson().splitlines()
event_lines = trace_v7_lifecycle_ndjson().splitlines()
pre_admission_refusal = runner_v10_refusal_ndjson()
```

Expand Down
2 changes: 1 addition & 1 deletion examples/pyodide-host/e2e_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const RUNNER_REFUSAL_FIXTURE = new URL(
import.meta.url,
);
const TRACE_LIFECYCLE_FIXTURE = new URL(
"../../src/droste/testing/fixtures/trace-v6-lifecycle.ndjson",
"../../src/droste/testing/fixtures/trace-v7-lifecycle.ndjson",
import.meta.url,
);
const TEST_BUDGET = {
Expand Down
4 changes: 2 additions & 2 deletions pyodide/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ The process has three independent output lanes:
| Descriptor | Contract |
|------------|----------|
| fd1 | Exactly one unary response JSON line. Adapter-owned responses use its HostResponse schema; pre-adapter event-channel failures use the relay-level error above. |
| configured event descriptor (fd3 by convention) | Canonical Trace ABI v6 NDJSON only. |
| configured event descriptor (fd3 by convention) | Canonical Trace ABI v7 NDJSON only. |
| fd2 | Diagnostics only; never parse or promote these bytes as events. |

Drain fd2 and the event descriptor concurrently. A hard cancellation or
Expand Down Expand Up @@ -466,7 +466,7 @@ Roughly three tiers of coverage:
- **Extract-fallback failure rate is unknown.** When `max_iterations` is
exhausted without `answer["ready"]`, one more LLM call tries to synthesize a
best-effort answer; a failure there now surfaces as a structured
`extract_error` (result field + the Trace ABI v6 `extract` failure event) instead of
`extract_error` (result field + the Trace ABI v7 `extract` failure event) instead of
silently falling back to raw loop output, but there's no data yet on how
often that call actually fails or why. No retry has been added — that's a
decision for once real failure data exists, not before.
Expand Down
2 changes: 1 addition & 1 deletion pyodide/event_channel_probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const mode = Deno.args[0];
const channel = eventChannelFromEnvironment();
const fixture = await Deno.readTextFile(
new URL(
"../src/droste/testing/fixtures/trace-v6-lifecycle.ndjson",
"../src/droste/testing/fixtures/trace-v7-lifecycle.ndjson",
import.meta.url,
),
);
Expand Down
2 changes: 1 addition & 1 deletion pyodide/event_channel_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import { isRlmEvent } from "../src/droste/substrates/_relay/events.ts";

const TRACE_LIFECYCLE_FIXTURE = new URL(
"../src/droste/testing/fixtures/trace-v6-lifecycle.ndjson",
"../src/droste/testing/fixtures/trace-v7-lifecycle.ndjson",
import.meta.url,
);

Expand Down
Loading