Skip to content

Worlds fixes (realized after Seahaven was built) - #1773

Merged
sfierro merged 8 commits into
sfierro/synthetic-worlds-ebfrom
sfierro/world-builder-phase0
Sep 18, 2026
Merged

sfierro merged 8 commits into
sfierro/synthetic-worlds-ebfrom
sfierro/world-builder-phase0

Conversation

@sfierro

@sfierro sfierro commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Draft, for reading the diff. Based on sfierro/synthetic-worlds-eb (itself draft PR #1761).

Net effect on Kiln is much smaller than the commit list suggests. Two of the seven commits on this branch add the runtime shim and the validity harness; a later commit removes both. Read as a unit, what actually lands in Kiln is the wire fixes plus one settle change.

What survives

  • 90d9aa8ee, a415afce2, f2c5dfe8b — the OpenEnv client fixes a shim-shaped world needs: error code and details to the model, reset facts from observation.result, settle after state, keepalive, and generic protocol tolerance.
  • 119452d8eDEFAULT_SETTLE_CALLS drops the ("state_digest", "controller_digest") pair.

What lands and leaves again

  • 466072e0e adds kiln_ai.worlds.shim, cabc3a57b adds kiln_ai.worlds.validity.
  • 1b4b59dda removes both. They now live in the integration-tests repo as the world-shim and world-validity distributions.

The reasoning: Kiln's job with worlds is to call them. It speaks OpenEnv over a socket and stays a black box about what serves the world, so the machinery for building one does not belong here. What stays is the client and its proof — session_manager.py, datamodel/world.py, tools/world_tool.py, and testing.py, a reference OpenEnv server that exists to show Kiln can drive a world at all.

Nothing in Kiln imported either package: no pyproject changes, no rewritten imports, and a grep for both module paths across the tree comes back empty.

On the settle change

controller_digest is served by no world framework Kiln is pointed at — Seahaven's control set is controller_changes and controller_run_sql, and nothing else — so against a Seahaven world the call missed on every episode. It missed quietly, because _settle reads unknown_tool as "this environment does not serve the tool". A grep across libs/ and app/ found one writer (the entry itself) and no reader outside tests.

What makes an absent control tool safe is _settle's tolerance, not the contents of that tuple. That tolerance is untouched, so a deployment whose world does serve a digest tool adds the pair back.

Checks

uv run ./checks.sh --agent-mode green.

Worth deciding before review

If this is read commit-by-commit, a reviewer spends most of it on ~6,300 lines of shim that leave again. Squashing or reordering before review would make the actual Kiln change legible. Left as-is because the history is honest and that is your call.

🤖 Generated with Claude Code

sfierro and others added 7 commits September 13, 2026 15:39
World Builder phase 0 needs a runtime before it can have a world. Seahaven is
specified but unwritten, so `kiln_ai.worlds.shim` copies its authoring surface
closely enough that a world moves to Seahaven by changing one dependency. It is
deliberately throwaway, and it is self-contained: nothing outside the package
changed, and Kiln's session manager stays a client of the wire, never an import.

What a world gets:

- `World`, `@world.tool`, `@world.middleware`, `@world.instance_startup`, `Ctx`,
  and `Tool.from_function`, which refuses at registration everything it can see
  going wrong later: an `async def`, a missing annotation, a `datetime` argument,
  a mutable default, a name that only exists under `TYPE_CHECKING`.
- A per-instance SQLite runtime on the stdlib module: a clock frozen at the
  fixture's timestamp that SQLite's own date functions read, seeded ids, nesting
  transactions, and a read-only inspection connection behind a write-denying
  authorizer.
- Fixtures that freeze through `VACUUM INTO` into a pending directory and one
  rename, verify their own bytes, seal build artifacts beside themselves, and
  fork.
- One instance per session, and a net-changes diff plus a state digest computed
  by comparing the instance against a read-only baseline, which is what a grader
  reads after an episode.
- The three control tools, the OpenEnv wire server Kiln's `OpenEnvSessionManager`
  talks to, content-composed versions, and a three-verb CLI.

Tests: one module per runtime module, 281 in all, including determinism, 25
concurrent sessions with no cross-talk, and a conformance test that drives the
shim's app through Kiln's real session manager end to end and asserts the whole
`final_state` shape — the contract the wire fixes in phase 2 build on.

Deviations from the component spec, accepted in review:

- The spec puts the session-capacity counter at module level; the code keeps one
  per app. A module-level counter is only correct when a process serves one
  world, and the fault runs and the tests both serve several, where one world's
  sessions would otherwise eat another's budget.
- The spec lists `fastapi` and `uvicorn` as dependencies of `openenv.py` without
  adding them to `kiln-ai`'s own dependency list, and the code does the same:
  both are already in the workspace and already imported by
  `worlds/testing.py`, and `__init__.py` imports neither `openenv.py` nor
  `cli.py`, so importing `kiln_ai.worlds.shim` pulls in neither. Serving a world
  is an explicit opt-in, so it is the right place for the cost.

Five checks go beyond the spec, each closing a hole review found, and
`components/shim.md` has been updated to describe them:

- Forbidden argument annotations are matched anywhere inside the annotation, not
  only at its top level: with `strict=True`, `Optional[datetime]` builds a model
  no wire value satisfies, so accepting one would hand the agent a tool it can
  never call.
- A tracked table's primary key columns must be unable to hold NULL. The diff
  joins on the key with `IS`, so two NULL-keyed rows would match each other and
  `changes()` would report edits nobody made.
- Every `untracked_tables` name must be a real table, so a typo cannot silently
  leave a table tracked.
- The clock's SQL overrides refuse the `'localtime'` and `'utc'` modifiers, which
  read the host machine's timezone and would make a world answer differently on
  every machine.
- The wire serialises with `ensure_ascii=False`, and a failed reset clears the
  destroyed episode's id and step count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QZFUkuNKL5kaBWVz9Sk6sE
…client

Five changes to the OpenEnv session manager and the world tool proxy, so a world
that models a real product can be driven and graded without Kiln learning
anything about that product. Every one is backwards compatible with the
reference server in `worlds/testing.py`: an environment that knows nothing of
control tools or coded errors ends its episodes exactly as before.

1. Error code and details reach the model. `ToolCallOutcome` carries
   `error_code` and `error_details`, read from a dict error's `code`, `message`
   and `details` only. The proxy renders the error through the new
   `render_tool_error` -- `{"error": {code, message, details}}`, all three keys
   always present -- and splits on the code: `WORLD_FAILURE_CODES` (`internal`,
   `unknown_tool`, `invalid_arguments`, `world_gap`) keep `is_error=True`, while
   any other code is the modelled product answering with an error of its own and
   is delivered as an ordinary result, which is what the real system's tool would
   return for the same refusal. The reference server's uncoded `type` key is
   deliberately not mapped to a code, so no existing assertion moves.

2. Reset facts merge `observation.result`, `observation.metadata` and top-level
   `metadata`, lowest to highest precedence, rather than reading one or the
   other. Seahaven-shaped environments report their facts in the result.

3. `end_episode` settles. After `state` and before `close`, under the popped
   session's lock, it steps each of `DEFAULT_SETTLE_CALLS` and writes
   `final_state["changes"]` and `final_state["state_digest"]`. After, not
   before, so `step_count` stays the agent's own on an environment that counts
   tool calls. An `unknown_tool` or uncoded error means the environment has no
   such snapshot and the key is simply absent; any other coded error is recorded
   as `final_state["settle_error"] = {tool, code, message}`, settling stops
   there, and the episode still ends so the generation is saved with the
   evidence in it rather than scoring as "the agent wrote nothing".
   `settle_calls=()` disables it.

4. `call_control_tool(episode, tool_name, arguments)` on the session manager and
   its Protocol. The wire step is factored into `_step_call_tool(..., record=)`;
   control calls pass `record=False`, so Kiln's own probes never count as the
   agent's reward or end its episode.

5. `render_tool_result(None)` renders `null`, matching what a tool that
   serializes its own empty body shows.

`worlds/testing.py` gains `ControlledCounterEnv` -- coded errors and unlisted
control tools -- and an `env_factory` on `create_app`/`serve_in_thread`,
defaulting to the plain `CounterEnv`.

Deviations from the spec (components/kiln_wire_fixes.md):

- The spec said the shim's conformance test belongs to phase 1 and is untouched
  here; the code edits it, because phase 1's `serve_in_thread` defaults to
  `include_control_tools=True`, so the shim's episodes now settle for real and
  the test's exact-equality assertion on `final_state` no longer held. The edit
  tightens rather than loosens it: `changes` and `state_digest` are popped (so
  their absence is a `KeyError`), exact equality holds on the remaining five
  keys, and both popped values are asserted on content -- the one insert the
  episode made, and a lowercase-hex digest.

- The spec put the reversed-settle-order case inside
  `test_settle_records_a_coded_failure`; the code splits it into
  `test_settle_stops_at_the_first_failure`, because two setups and two teardowns
  in one test blur which arrangement failed.

- The spec said the judge's `KeyError` on a missing `changes` is "recorded as
  the eval item's error"; the code asserts the runner's last `Progress` is
  `(complete=0, errors=1)` alongside no persisted score, because the runner
  keeps no per-item error record, and the score assertion alone would also pass
  if the item had been silently skipped or the failure swallowed -- which is the
  failure mode that test exists to rule out.

Spec error corrected: the spec's `ControlledCounterEnv` reports `"tools": 4` in
its reset result, but the env lists five tools; the code reports
`len(self.TOOLS)` and the tests assert against it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QZFUkuNKL5kaBWVz9Sk6sE
`kiln_ai.worlds.validity`: the measurement that answers phase 0's question — does a
hand-written world reproduce, per input and per configuration, the eval outcome the
system it replicates produces, and does it rank agent configurations the same way.

Nine modules. `scrub` (noise rules, declared-divergence expansion, JSON comparison),
`idmap` (created ids by creation order), `traces` (recorded episodes out of a Kiln
project, tool-step extraction, the symmetric invalidity rule), `replay` (tool-level
replay against an in-process shim instance), `decision` (teacher-forced decision replay
against a control arm), `metrics` (pairing, bootstraps, the paired and rank statistics,
the gates), `faults` (world-server lifecycle, trigger counting, the fault gate), `cost`
(cost model and budget meter), `report` (`validity.json` and `validity.md`).

The package holds nothing about any particular world. Scrub rules, create tools, served
tools, declared divergences, the world-server command, the fault table and pricing all
arrive as arguments from a driver that knows its own world; phase 8 supplies them. The
boundary is enforced rather than promised: `test_package` greps every source for the
driver's vocabulary — the product, its fixture, its config file — and a second grep bans
model ids and providers in the package's own modules, but not in tests, since a Kiln run
config has to name a real `ModelProviderName`. That grep found a real leak: `cost.py`
defaulted `pricing_source` to the driver's own filename.

Three defects worth recording, all of which made a gate easier to pass.

The cluster bootstrap did not resample clusters. `_clustered` drew with replacement, but
both statistics it feeds re-group their input by the clustering key — `delta_value` by
(configuration, input) and `_difference` through a dict keyed by (run_id, decision_index)
— so a cluster drawn twice collapsed back into one, or overwrote itself. What was
computed was a ~63% unweighted subset of the clusters, under-dispersed: on a realistic
6x20x3 paired set the interval came out about 28% too narrow. Both gates that read an
interval are one-sided in the permissive direction (`paired` tests `interval.high`,
`decision` tests `interval.low`), so every instance made a gate easier to pass. Each draw
is now relabelled into its own cluster identity. The first regression tests for this were
wrong: they exercised the statistics on hand-relabelled input and passed against the
reintroduced bug. They now count groups and pairs inside the statistic, so they reach
`_clustered`'s `relabel` argument, and were mutation-verified against a reverted fix.

The replay gate passed on zero replayed steps. `replay_steps` was an optional keyword, so
a clean mismatch list with no step count read as "no undeclared classes". It is now
required and `not replay_steps` covers both None and 0 — the spec's rule is that nothing
raises on emptiness and nothing passes on it.

Four of the six seeded faults could never report a moved gate. A paired-targeted fault run
is ten input pairs x six configurations x two repeats: 20 valid episodes per configuration,
judged against a `min_valid_per_config` of 40. The paired and rank gates therefore came
back `inconclusive`, `moved_gates` counts only pass-to-fail, and the report's `moved`
column would have read empty however broken the faulted world was — which a reader parses
as "the gate did not move" when it means "the gate could not move". `fault_thresholds`
rescales the clean thresholds to the fault run's own volume, and `detect` now takes those
thresholds and an `evaluable` check that marks the fault inconclusive *with a reason* when
no gate could have moved. An inconclusive fault fails the fault gate's zero-inconclusive
clause exactly like an untriggered one.

Anything the report did not verify, it says so. A step count nobody supplied stays null
rather than becoming `len(mismatches)`; `build_report` cross-checks its gates against
`executed_steps` and against the replay count the gate was judged on, because it is the
only place both halves are visible; a world server that publishes no `max_concurrent_envs`
becomes a caveat; a model the pricing table does not name raises `UnpricedModel` whenever
a budget is set, because a budget that cannot bind is not a budget.

Deviations from components/validity_harness.md:

- The spec puts the world-server lifecycle in `faults.py` and defines `cost.py`; the
  implementation plan named a `server.py` and omitted `cost.py`. Followed the component
  spec, which is authoritative, and corrected the plan line.
- The spec has `start_world_server` refuse a server whose `max_concurrent_envs` is below
  the requested concurrency; the shim's `/metadata` reported no such key, so the check
  could never fire. Added it to the shim (one line, plus an assertion in the existing
  test) and kept the skip-with-caveat path for any other OpenEnv server.
- The spec's `build_prefix` distinguishes a skipped step by `results[i] is None`; the code
  takes an explicit `raw_steps`, because a world result that is genuinely null — a delete —
  is not the same thing as a step the world was never asked to answer.
- `ReplayedEpisode` gains `records`, because `replay.jsonl` needs an "ok" line per matching
  step and a matching step leaves no `Mismatch` to build one from.
- `decision._substituted` sets `is_error` only for `WORLD_FAILURE_CODES`, where the spec's
  prose says every error envelope, because it reuses `world_tool.py`'s own constant so the
  world arm sees exactly what the world lane would have shown the model. A coded product
  error is an ordinary result on both arms; the spec text is wrong here.
- Several signatures gained arguments the spec left no room for: `known_ids` on the replay
  entry points, `reset_kwargs`/`world_version` on `replay_decision` (the spec's example
  hardcodes a fixture name the generic half cannot know), `replay_steps` and the bootstrap
  seed on the gates, `scores` on `Cell` so the per-configuration component rates are
  computable at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QZFUkuNKL5kaBWVz9Sk6sE
Three client fixes found after Seahaven shipped, all in the session manager's
wire handling and the episode model it fills.

websockets keepalive now states ping_interval=20 / ping_timeout=120. The
library's default 120s -> 20s timeout loses every episode on the box when a
world server stalls at the event loop doing synchronous work: it cannot answer
a ping, and the keepalive closes the session out from under a run. Seahaven
ships its own client at 120s and this matches it. Both values are stated
rather than defaulted, so a library default that moves cannot silently change
what a run measured. STEP_TIMEOUT_S gains a comment recording the consequence:
the keepalive, not the 600s step timeout, is the real ceiling for a stalled
event loop.

_settle() tolerates "tool_not_found" beside None and "unknown_tool".
MCPErrorType.TOOL_NOT_FOUND is a real OpenEnv constant; nothing maps error_type
onto the code today, so this is defensive rather than currently reachable, but
if anyone later does, the tolerance already covers it.

WorldEpisode.reset_metadata is renamed to reset_facts and its description
corrected. Both were fossils: the name and the description claimed the facts
came from the observation's metadata, but start_episode reads
observation.result first and Seahaven reports there exclusively. No alias and
no migration -- nothing has shipped and the model is not on main.

The generated api_schema.d.ts was regenerated with generate_schema.sh and is
verified by check_schema.sh inside the check suite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QZFUkuNKL5kaBWVz9Sk6sE
…und one environment

The transport layer should tolerate what the OpenEnv protocol actually permits
and nothing that was fitted to a particular world. Reviewing the settle and
reset paths against openenv 0.4.2 on the wire, one entry in each was shaped
around environments we wrote ourselves; both are gone, and each thing that
stayed is justified here as generic rather than accommodation.

Removed: `"tool_not_found"` from `_settle`'s tolerated codes. It was
unreachable for every OpenEnv environment. `ToolError` is `extra="forbid"`
with only `error_type` and `message`, so no environment can send a `code` at
all, and `tool_not_found` is a value of `error_type`, which Kiln never reads.
The only thing the entry could ever have matched is a world that deliberately
repeated the literal as a code. It also never belonged: `functional_spec.md:259`
and `[A-13]` both state the rule as "`unknown_tool` or an error with no code at
all", so this restores the code to the spec rather than changing it.

Removed: the top-level `data["metadata"]` reset-facts source, dead by
construction. OpenEnv's `serialization.py` copies `observation.metadata` to the
top level as a sibling, so that value is always either absent or an exact copy
and can never say anything the observation did not. Pinned by a new test that
fails if it is re-added.

Kept, both observation slots. This is generic rather than accommodation: base
`Observation` is `extra="forbid"` and `metadata` is its only free-form field,
so a stock observation has nowhere else to put facts, while `result` belongs to
`CallToolObservation` and is reachable only by subclassing. Reading only
`result` would mean Kiln takes facts solely from environments shaped like one
framework. A real environment proves it -- the UpKeep mock returns a bare
`Observation(metadata={...})` and would have gone factless.

Kept, `None` as the generic settle arm, whose value is narrower than it looks:
for an environment serving no control tools `changes` is absent either way, so
what the arm buys is the difference between an absent optional key and a
recorded fault. It keeps `validity/traces.py` from stamping
`invalid="settle_error"` on every such episode and inflating the
`settle_errors` count, and it keeps worlds that serve only some settle tools
working.

Kept, non-dict `error` handling, as type discipline on a field declared
`str | None` rather than accommodation of any one environment: it stops a
foreign server's list or number reaching the tool proxy as an "error string".
Its test is now parametrized over a string and a list, so the coercion itself
is pinned and not only the tolerance.

Kept, `"unknown_tool"`, with the tension documented rather than resolved. It is
the same class of thing `tool_not_found` was -- one framework's vocabulary, not
a protocol constant -- and survives only because it is reachable and
load-bearing: a Seahaven-shaped world started without `include_control_tools`
answers exactly that, and dropping it would write `settle_error` into every one
of its episodes. Resolving it properly means a design decision (a
`settle_tolerated_codes` argument beside `settle_calls`, or moving the
vocabulary to where `WORLD_FAILURE_CODES` lives) and belongs with the Seahaven
swap, so `_settle`'s docstring now says so in code rather than only in the spec
repo.

The UpKeep verifier was run before and after and is byte-identical in
behaviour; both reset slots and non-dict tolerance are preserved, so nothing
another engineer depends on moved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QZFUkuNKL5kaBWVz9Sk6sE
Kiln's job with worlds is to call them. It speaks OpenEnv over a socket and
stays a black box about what serves the world, so the machinery for *building*
one does not belong here.

`kiln_ai.worlds.shim` and `kiln_ai.worlds.validity` move to the integration-tests
repo as two standalone distributions, `world-shim` and `world-validity`, under
`world-builder/resources/`. What stays is the client and its proof:
`session_manager.py`, `datamodel/world.py`, `tools/world_tool.py`, and
`testing.py` — a reference OpenEnv server that exists to show Kiln can drive a
world, which is the client under test, not world-building.

Nothing in Kiln imported either package: no pyproject changes, no rewritten
imports, and a grep for both module paths across the tree comes back empty.
The shim was under-declared here anyway — it imports fastapi and uvicorn, which
`libs/core/pyproject.toml` never listed, so they only ever arrived through
kiln-server inside the workspace and a plain `pip install kiln-ai` could not
have served a world. `world-shim` declares them.

Git history does not follow: this is a `git rm` here and fresh files there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`DEFAULT_SETTLE_CALLS` carried a `("state_digest", "controller_digest")` pair.
No world framework we point Kiln at serves that tool — Seahaven's control set is
`controller_changes` and `controller_run_sql`, and nothing else — so against a
Seahaven world the call missed on every single episode.

It missed quietly, which is why it went unnoticed: `_settle` reads `unknown_tool`
as "this environment does not serve the tool", logs at debug and leaves the key
absent. So the cost was a wasted round trip per episode writing a key nothing
opened. A grep across `libs/` and `app/` found one writer — the entry itself —
and no reader outside tests: no scorer, judge, API route or serializer.

The docstring now says what settle asks for and why, and makes the important
distinction explicit: what makes an absent control tool safe is `_settle`'s
tolerance, not the contents of this tuple. That tolerance is untouched, so a
deployment whose world does serve a digest tool adds the pair back and it
settles like any other entry.

Tests narrowed rather than deleted. Two that used `controller_digest` through
`call_control_tool` rather than through settle are deliberately left: they are
now what pins the tolerance this docstring claims.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 989e6361-ab37-473d-b774-201924bab1b7

📥 Commits

Reviewing files that changed from the base of the PR and between 8009e57 and af83ef4.

📒 Files selected for processing (6)
  • libs/core/kiln_ai/adapters/eval/test_eval_runner_worlds.py
  • libs/core/kiln_ai/tools/test_world_tool.py
  • libs/core/kiln_ai/tools/world_tool.py
  • libs/core/kiln_ai/worlds/session_manager.py
  • libs/core/kiln_ai/worlds/test_session_manager.py
  • libs/core/kiln_ai/worlds/testing.py
 _____________________________________________________________
< Return early? Yes. Return *something meaningful*? Also yes. >
 -------------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 15, 2026

Copy link
Copy Markdown

📊 Coverage Report

Overall Coverage: 93%

Diff: origin/sfierro/synthetic-worlds-eb...HEAD

  • libs/core/kiln_ai/tools/world_tool.py (100%)
  • libs/core/kiln_ai/worlds/session_manager.py (100%)

Summary

  • Total: 21 lines
  • Missing: 0 lines
  • Coverage: 100%

Seahaven now answers `reset` with its facts in the observation's `metadata`,
publishes the change log through the ordinary `state` call, and sends an error
as OpenEnv's own `{error_type, message}` with its `{code, message, details}`
triple moved to `metadata["seahaven_error"]`. A `WorldBug` raises and stops the
session rather than arriving as data. Kiln is a plain OpenEnv client again, and
three things it grew to cope with the old shapes have nothing left to do.

The settle machinery goes. `call_control_tool`, `DEFAULT_SETTLE_CALLS` and
`_settle` existed to reach a control tool for the record of what an episode
changed. That record is in the state document now, and `end_episode` already
copies every key of `state` into `final_state`, so the replacement is no code at
all. `_live_session` and `_step_call_tool` were split out only to share a step
with the control path and fold back into `call_tool`.

The classification goes with it. `WORLD_FAILURE_CODES` decided whether an error
was the world failing or the product answering by matching a code against a
hardcoded set -- a denylist of one framework's spelling, wrong for `db_error`,
and needing a patch for every new backend. No code arrives any more, and a
broken world stops being data, so the question it answered is not Kiln's. Every
error on an observation is a failed call: `error_type`'s whole vocabulary,
`execution_error` included, means the call did not work, and an environment that
wants an error read as an ordinary answer returns it as a result.

`reset_facts` reverts to `reset_metadata`, reading the field the protocol
declares and nothing else.

What stays, because none of it was about Seahaven. The keepalive: a world server
doing synchronous work cannot answer a ping, and the library's 20s default drops
the session long before a long tool call finishes or Kiln's own 600s ceiling is
near. The error read, repointed at `error_type` and pulled out as
`read_observation_error` so its tolerance is testable on its own -- a code is
still read for an environment that sends one, and no shape is rejected.
`render_tool_result(None)` answering `"null"` rather than an empty string.

The test environment sent `{"type": ...}` on an error, which is not a shape any
OpenEnv environment sends: `ToolError` declares `error_type`. Conformed, so the
fake is worth testing against.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sfierro sfierro changed the title Worlds: the shim and the validity harness leave Kiln Worlds fixes (realized after Seahaven was built) Sep 18, 2026
@sfierro
sfierro marked this pull request as ready for review September 18, 2026 18:33
@sfierro
sfierro merged commit 3e8f108 into sfierro/synthetic-worlds-eb Sep 18, 2026
10 of 13 checks passed
@sfierro
sfierro deleted the sfierro/world-builder-phase0 branch September 18, 2026 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant