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-v8-execution.ndjson trace-v8-lifecycle.ndjson runner-v10-refusal.ndjson; do
for f in trace-v9-execution.ndjson trace-v9-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_v8_execution_ndjson,
trace_v8_lifecycle_ndjson,
trace_v9_execution_ndjson,
trace_v9_lifecycle_ndjson,
)

source = Path(sys.argv[1])
assert trace_v8_execution_ndjson() == (source / "trace-v8-execution.ndjson").read_bytes()
assert trace_v8_lifecycle_ndjson() == (source / "trace-v8-lifecycle.ndjson").read_bytes()
assert trace_v9_execution_ndjson() == (source / "trace-v9-execution.ndjson").read_bytes()
assert trace_v9_lifecycle_ndjson() == (source / "trace-v9-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-v8-lifecycle.ndjson \
"$tmp/$sdist_root/src/droste/testing/fixtures/trace-v8-lifecycle.ndjson"
cmp src/droste/testing/fixtures/trace-v8-execution.ndjson \
"$tmp/$sdist_root/src/droste/testing/fixtures/trace-v8-execution.ndjson"
cmp src/droste/testing/fixtures/trace-v9-lifecycle.ndjson \
"$tmp/$sdist_root/src/droste/testing/fixtures/trace-v9-lifecycle.ndjson"
cmp src/droste/testing/fixtures/trace-v9-execution.ndjson \
"$tmp/$sdist_root/src/droste/testing/fixtures/trace-v9-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-v8-execution.ndjson trace-v8-lifecycle.ndjson runner-v10-refusal.ndjson; do
for f in trace-v9-execution.ndjson trace-v9-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_v8_execution_ndjson,
trace_v8_lifecycle_ndjson,
trace_v9_execution_ndjson,
trace_v9_lifecycle_ndjson,
)

source = Path(sys.argv[1])
assert trace_v8_execution_ndjson() == (source / "trace-v8-execution.ndjson").read_bytes()
assert trace_v8_lifecycle_ndjson() == (source / "trace-v8-lifecycle.ndjson").read_bytes()
assert trace_v9_execution_ndjson() == (source / "trace-v9-execution.ndjson").read_bytes()
assert trace_v9_lifecycle_ndjson() == (source / "trace-v9-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-v8-lifecycle.ndjson \
"$tmp/$sdist_root/src/droste/testing/fixtures/trace-v8-lifecycle.ndjson"
cmp src/droste/testing/fixtures/trace-v8-execution.ndjson \
"$tmp/$sdist_root/src/droste/testing/fixtures/trace-v8-execution.ndjson"
cmp src/droste/testing/fixtures/trace-v9-lifecycle.ndjson \
"$tmp/$sdist_root/src/droste/testing/fixtures/trace-v9-lifecycle.ndjson"
cmp src/droste/testing/fixtures/trace-v9-execution.ndjson \
"$tmp/$sdist_root/src/droste/testing/fixtures/trace-v9-execution.ndjson"
cmp src/droste/testing/fixtures/runner-v10-refusal.ndjson \
"$tmp/$sdist_root/src/droste/testing/fixtures/runner-v10-refusal.ndjson"

Expand Down
32 changes: 32 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,38 @@ Ordered newest first. "Embedder" means anything that builds on the engine
beyond the `droste` CLI: hosts calling `run_rlm` in-process, `droste_runner`
consumers, and Pyodide-substrate integrations staging the Deno relay.

## 0.24.0

### Trace ABI v9 reports which ready-time gates a run armed

The `startup` event gains an optional `ready_gates` array naming the gates the
run actually enabled: any of `policy_hints`, `ready_metadata_validator`, and
`ready_answer_validator`. An empty array means the run enforced nothing.

It exists because enforcement here is opt-in and defaults to off, so a host that
never passes `policy_hints` runs with `ready_violations` and
`contract_violations` permanently inert -- and until now emitted a trace
identical to a fully gated run. That is the silent degradation this file is for.
One embedder shipped for months in exactly that state while its benchmark suite
passed hints and measured the enforced configuration; no test could fail,
because nothing recorded which system was running.

**Embedders should assert on `ready_gates` in whatever checks their production
configuration**, and treat an unexpectedly empty array as a misconfiguration
rather than a default.

### `ready_answer_validator` gates on observed run state

`run_rlm` accepts a new optional `ready_answer_validator`, called with a
`ReadyAnswerState` (metadata, answer content, iteration, calls made, successful
calls) instead of metadata alone.

`ready_metadata_validator` sees only model-written metadata, so it can verify
the model's *claims* about its work but not the work. Behavioural rules -- "do
not answer without consulting the data" -- need what the engine observed. The
older hook is unchanged and still supported; both run when both are supplied,
and violations from either revoke readiness identically.

## 0.23.0

### Trace ABI v8 reports liveness during provider calls
Expand Down
8 changes: 4 additions & 4 deletions docs/trace-abi.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,12 +230,12 @@ and sdist. Python consumers load them through package resources:
```python
from droste.testing import (
runner_v10_refusal_ndjson,
trace_v8_execution_ndjson,
trace_v8_lifecycle_ndjson,
trace_v9_execution_ndjson,
trace_v9_lifecycle_ndjson,
)

execution_lines = trace_v8_execution_ndjson().splitlines()
event_lines = trace_v8_lifecycle_ndjson().splitlines()
execution_lines = trace_v9_execution_ndjson().splitlines()
event_lines = trace_v9_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-v8-lifecycle.ndjson",
"../../src/droste/testing/fixtures/trace-v9-lifecycle.ndjson",
import.meta.url,
);
const TEST_BUDGET = {
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-v8-lifecycle.ndjson",
"../src/droste/testing/fixtures/trace-v9-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-v8-lifecycle.ndjson",
"../src/droste/testing/fixtures/trace-v9-lifecycle.ndjson",
import.meta.url,
);

Expand Down
6 changes: 3 additions & 3 deletions pyodide/events_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ function wire(
run_id: "run-1",
seq: 1,
timestamp: "2026-07-14T00:00:00Z",
version: 8,
version: 9,
persistence_class: persistence ?? PERSISTENCE_BY_TYPE[type],
depth: 0,
...body,
Expand Down Expand Up @@ -384,7 +384,7 @@ Deno.test("successful output beginning ERROR remains an output event", () => {

Deno.test("Python and relay accept the same execution golden NDJSON", async () => {
const fixture = new URL(
"../src/droste/testing/fixtures/trace-v8-execution.ndjson",
"../src/droste/testing/fixtures/trace-v9-execution.ndjson",
import.meta.url,
);
const lines = (await Deno.readTextFile(fixture)).trim().split("\n");
Expand Down Expand Up @@ -428,7 +428,7 @@ Deno.test("Python and relay accept the same execution golden NDJSON", async () =

Deno.test("Python and relay accept the same lifecycle golden NDJSON", async () => {
const fixture = new URL(
"../src/droste/testing/fixtures/trace-v8-lifecycle.ndjson",
"../src/droste/testing/fixtures/trace-v9-lifecycle.ndjson",
import.meta.url,
);
const lines = (await Deno.readTextFile(fixture)).trim().split("\n");
Expand Down
4 changes: 2 additions & 2 deletions pyodide/heartbeat_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const { isRlmEvent, PERSISTENCE_BY_TYPE, RLM_EVENT_TYPES } = await import(
function wire(body: Record<string, unknown>): string {
return JSON.stringify({
type: "heartbeat",
version: 8,
version: 9,
run_id: "run-1",
seq: 4,
timestamp: "2026-08-03T00:00:00Z",
Expand Down Expand Up @@ -97,7 +97,7 @@ Deno.test("a heartbeat a live subcall produced is forwarded", () => {
depth: 1,
seq: 2,
timestamp: "2026-08-03T21:46:54.805Z",
version: 8,
version: 9,
persistence_class: "transient",
};

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "droste"
version = "0.23.0"
version = "0.24.0"
description = "Recursive analysis engine for data too large for a context window, built with Recursive Language Models (RLMs)"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
4 changes: 4 additions & 0 deletions src/droste/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@
CheckpointPayloadProvider,
ExtractableWorkProbe,
ExtractContextProvider,
ReadyAnswerState,
ReadyAnswerValidator,
ReadyMetadataValidator,
ReadyMetadataValidatorError,
)
Expand Down Expand Up @@ -172,6 +174,8 @@
"CheckpointPayloadProvider",
"ExtractableWorkProbe",
"ExtractContextProvider",
"ReadyAnswerState",
"ReadyAnswerValidator",
"ReadyMetadataValidator",
"ReadyMetadataValidatorError",
"extract_code_block",
Expand Down
3 changes: 2 additions & 1 deletion src/droste/execution/trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from typing import Any, Callable, Mapping
from uuid import uuid4

TRACE_ABI_VERSION = 8
TRACE_ABI_VERSION = 9


class PersistenceClass(str, Enum):
Expand Down Expand Up @@ -71,6 +71,7 @@ class PersistenceClass(str, Enum):
"provider_protocol": (int, _NONE_TYPE),
"scaffold_manifest_id": str,
"scaffold_manifest_version": int,
"ready_gates": (list, tuple),
},
),
"progress": ({"status": str}, {}),
Expand Down
4 changes: 4 additions & 0 deletions src/droste/loop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
CheckpointPayloadProvider,
ExtractableWorkProbe,
ExtractContextProvider,
ReadyAnswerState,
ReadyAnswerValidator,
ReadyMetadataValidator,
ReadyMetadataValidatorError,
)
Expand All @@ -20,6 +22,8 @@
"CheckpointPayloadProvider",
"ExtractableWorkProbe",
"ExtractContextProvider",
"ReadyAnswerState",
"ReadyAnswerValidator",
"ReadyMetadataValidator",
"ReadyMetadataValidatorError",
]
20 changes: 20 additions & 0 deletions src/droste/loop/rlm.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from .code_extractor import extract_code_block
from .step import (
EMPTY_OUTPUT_NUDGE,
ReadyAnswerValidator,
ReadyMetadataValidator,
RLMConfig,
RLMResult,
Expand Down Expand Up @@ -767,6 +768,7 @@ def run_rlm(
prompt_pack: PromptPack | None = None,
consumer_prompt_catalog: PromptPackCatalog | None = None,
ready_metadata_validator: ReadyMetadataValidator | None = None,
ready_answer_validator: ReadyAnswerValidator | None = None,
) -> RLMResult:
try:
cfg = config or RLMConfig()
Expand Down Expand Up @@ -927,6 +929,7 @@ def step_kwargs() -> dict[str, Any]:
namespaced_accessor_pairs=namespaced_accessor_pairs,
semantic_evidence=semantic_evidence,
ready_metadata_validator=ready_metadata_validator,
ready_answer_validator=ready_answer_validator,
)

def emit_checkpoint() -> None:
Expand Down Expand Up @@ -995,6 +998,23 @@ def call_live_root(live_messages: list[dict[str, str]]) -> tuple[str, Any, RLMEr
"provider_protocol": scaffold_manifest.body["abis"]["provider"],
"scaffold_manifest_id": scaffold_manifest.manifest_id,
"scaffold_manifest_version": scaffold_manifest.schema_version,
# Which ready-time gates this run actually armed. Enforcement
# that defaults to off is indistinguishable from a caller who
# meant to disable it, so a run that gates nothing has to say
# so rather than look identical to a fully gated one. Cozy
# shipped for months with policy_hints unset -- every
# ready_violations/contract_violations check inert -- while its
# benchmarks passed hints and measured an enforced system.
# Nothing in either trace recorded the difference.
"ready_gates": sorted(
name
for name, active in (
("policy_hints", cfg.policy_hints is not None),
("ready_metadata_validator", ready_metadata_validator is not None),
("ready_answer_validator", ready_answer_validator is not None),
)
if active
),
}
)
while not answer.get("ready"):
Expand Down
Loading