Skip to content

Record how a verdict was sampled, and stop promising replay - #694

Merged
xmap merged 2 commits into
mainfrom
worktree-sampling-provenance
Aug 20, 2026
Merged

Record how a verdict was sampled, and stop promising replay#694
xmap merged 2 commits into
mainfrom
worktree-sampling-provenance

Conversation

@xmap

@xmap xmap commented Aug 20, 2026

Copy link
Copy Markdown
Owner

The durable inference record has carried request_temperature and request_top_p since it was built, named for the OpenTelemetry GenAI convention, and no producer ever filled them. On the pilot record 583 of 583 calls wrote their max-token bound and 0 of 583 wrote how they were sampled.

What changes

  • LLMChatRequest carries temperature and top_p, both optional, defaulting to None.
  • Both adapters send only a dial the caller actually set. Anthropic uses anthropic.omit; the OpenAI-compatible path builds the field dict conditionally.
  • All three chat-request builders (run_debrief, caution_drafter, _llm_decide_prompt) pin temperature = 0.0.
  • The value reaches entries_decision_inferences through AgentInferenceTrace.
  • ArchivabilityTier's "results stay re-executable" claim is withdrawn in favour of "the identity stays invocable".

Two things worth a reviewer's attention

Identity checks, not truthiness. A deliberate mutation to if request.temperature: makes test_sends_the_sampling_the_caller_set fail. Zero is the only value anything sets, so a truthiness check would have silently dropped every configured value while the code read as wired.

The retraction is an API change. ArchivabilityTier's docstring is emitted verbatim into openapi.json, so the old wording was a promise to API consumers, not a maintainer note. Replay needs sampling settings and a seed; Anthropic's Messages API accepts no seed at any price, so no tier setting can deliver reproducibility. The archaeology sits in a # comment above the class so it does not ship into the public schema.

Scope note

Callers that set nothing still record nothing. The 583 existing rows ran at provider defaults that cannot be recovered, so they stay blank permanently; only future calls carry sampling provenance.

Gates

  • pytest tests/unit tests/architecture: 45068 passed, 630 skipped
  • pytest tests/contract tests/integration: 4663 passed, 1 skipped
  • pyright src/cora tests/unit: 0 errors; ruff clean

🤖 Generated with Claude Code

xmap and others added 2 commits August 20, 2026 10:14
The durable inference record has carried request_temperature and
request_top_p since it was built, named for the OpenTelemetry GenAI
convention, and no producer ever filled them. On the pilot record 583
of 583 calls wrote their max-token bound and 0 of 583 wrote how they
were sampled, so every recorded verdict was one whose sampling nobody
could state.

The port now carries both dials, both optional, and both adapters send
only what a caller actually set. Silence stays silent: inventing a
default would put a sampling claim on the record that no caller made,
and the SDK's real default is not necessarily the documented one.

All three tasks that build a chat request pin temperature to zero.
Each picks from a fixed set under a schema the provider already
enforces, so sampling variance buys nothing and costs consistency. Zero
is not determinism and the comment at each site says so: Anthropic
exposes no seed at all, and batching on a shared GPU perturbs the
in-house path. What pinning buys is lower variance plus a number the
record can state because we chose it.

That makes the catalog's old claim untenable, so it is withdrawn.
ArchivabilityTier said Pinned meant "results stay re-executable", which
promised replay. Replay needs sampling settings and a seed, and no tier
setting supplies the seed. The tier grades whether the identity is
still invocable, which is availability, not reproducibility. That
docstring is published verbatim into openapi.json, so the wording was
an API claim rather than a note, and the correction ships with the
schema.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Re-running the comparison after a code change is not the same request
twice, and each way of getting it wrong yields a full, fast, plausible
result instead of an error, which is what makes them worth writing
down.

Re-used idempotency keys replay the previous walk's stored responses
without reaching the model. A process not restarted after a pull serves
the old build; the check for that is a record one, since nothing wrote
request_temperature before sampling provenance shipped. And the new
decisions have to be separated from the old by event position rather
than by the field being examined, or the measurement agrees with itself.

Also names the join. Inference entries are the record's second tier and
their event_id is a derived dedup key, not a row in events, so the
obvious event_id-to-event_id join matches nothing on a healthy
deployment.

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

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  apps/api/src/cora/agent/adapters
  anthropic_llm.py
  openai_compatible_backend.py
  apps/api/src/cora/agent/aggregates/language_model
  state.py
  apps/api/src/cora/agent/prompts
  caution_drafter.py
  run_debrief.py
  apps/api/src/cora/infrastructure/ports
  inference_recorder.py
  llm.py
  apps/api/src/cora/operation/adapters
  _llm_decide_prompt.py
Project Total  

This report was generated by python-coverage-comment-action

@xmap
xmap merged commit 27c6ff5 into main Aug 20, 2026
19 checks passed
@xmap
xmap deleted the worktree-sampling-provenance branch August 20, 2026 17:16
xmap added a commit that referenced this pull request Aug 20, 2026
…698)

#694 pinned temperature to 0.0 in all three chat-request builders, on
the reasoning that these tasks pick from a fixed verdict set so sampling
variance buys nothing. That reasoning holds. The parameter does not.

Anthropic has deprecated temperature, and newer models reject a request
carrying it. Measured against the Argo gateway on 2026-08-20:

    Sonnet 4.5, Haiku 4.5, Sonnet 4.6, Opus 4.6   accept it
    Opus 4.7, Sonnet 5, Opus 5                    400, "`temperature`
                                                  is deprecated for
                                                  this model"

So the pin does not steady a modern model's answer, it prevents one.
Every agent shipped today declares a model on the accepting side of that
line, so nothing in place broke; what the pin did was silently foreclose
every upgrade, and fail in the least legible way when one was attempted.
The refusal arrives as an invalid-request error, the debrief defers, and
nothing in the deferral names sampling. It was found by pointing a new
RunDebriefer at Sonnet 5 and watching the preflight refuse on call one.

The port keeps both dials and the record still writes whatever a caller
sets, so the provenance work stands. What is withdrawn is the judgement
that these tasks should set one. A caller that sets nothing records
nothing, which is the honest state, and honesty is the point of that
column.

There is a wider consequence worth stating for anyone reasoning about
replay: what can be recorded about how a model was asked is bounded by
what the vendor still permits to be specified, and that surface is
contracting as sampling knobs give way to adaptive reasoning. Recording
sampling faithfully does not arrest that.

Co-authored-by: xmap <16776958+xmap@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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