feat: let a turn be silent #913

Merged
coilysiren merged 1 commit from issue-895-silent-turn into main 2026-08-17 18:02:14 +00:00
Member

Lets a turn produce no final text, so an agent that already answered through a tool can decline to answer twice. Implements Kai's decision recorded on the issue.

What changed

  • ParseReply reads an empty reply as silence rather than an error, and a silent turn posts nothing.
  • RequireReply keeps the strict parse for the three callers with no silence to express: the evaluation scorer, board.go, and job content messages. Their behaviour is byte-identical, including the error string.
  • turn.reply.silent records the choice with the executed tool count.

The revisit condition, handled up front

The decision reserved the right to reopen if silence "turns out to hide real failures, because a turn that produces nothing becomes indistinguishable from a turn that broke."

Silence is a choice only once the turn has done something. An empty reply from a turn that ran no tool is not a decision, and it stays the parse failure it was: same check name (parse), same stage, same notice, and the proxy's repair loop still fires for it. That signal comes off the turn rather than out of a sentinel the model has to emit, so nothing depends on the model cooperating.

The evidence that this is the right seam: TestProxyClientRepairsEmptyReplyOnce is untouched and still passes. It pins the old empty-completion repair, and it has no tool calls.

What this does not do

No standing ban on write tools aimed at the reply channel, per the decision. deploy#631 and deploy#632 can come off once this rolls, and that is deploy's call rather than mine.

Tests

silentturn_test.go runs both halves through runTurn: a model that answered through a tool and returned nothing posts nothing, and a model that ran no tool and returned nothing still fails. decision_test.go covers the parse contract on both functions.

just gate PASS.

closes #895

Lets a turn produce no final text, so an agent that already answered through a tool can decline to answer twice. Implements Kai's decision recorded on the issue. ## What changed * `ParseReply` reads an empty reply as silence rather than an error, and a silent turn posts nothing. * `RequireReply` keeps the strict parse for the three callers with no silence to express: the evaluation scorer, `board.go`, and job content messages. Their behaviour is byte-identical, including the error string. * `turn.reply.silent` records the choice with the executed tool count. ## The revisit condition, handled up front The decision reserved the right to reopen if silence "turns out to hide real failures, because a turn that produces nothing becomes indistinguishable from a turn that broke." **Silence is a choice only once the turn has done something.** An empty reply from a turn that ran no tool is not a decision, and it stays the parse failure it was: same check name (`parse`), same stage, same notice, and the proxy's repair loop still fires for it. That signal comes off the turn rather than out of a sentinel the model has to emit, so nothing depends on the model cooperating. The evidence that this is the right seam: `TestProxyClientRepairsEmptyReplyOnce` is untouched and still passes. It pins the old empty-completion repair, and it has no tool calls. ## What this does not do No standing ban on write tools aimed at the reply channel, per the decision. `deploy#631` and `deploy#632` can come off once this rolls, and that is deploy's call rather than mine. ## Tests `silentturn_test.go` runs both halves through `runTurn`: a model that answered through a tool and returned nothing posts nothing, and a model that ran no tool and returned nothing still fails. `decision_test.go` covers the parse contract on both functions. `just gate` PASS. closes #895
feat: let a turn be silent
All checks were successful
ci / image-build (pull_request) Successful in 38s
ci / test (pull_request) Successful in 1m3s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
c0973a56f3
An agent holding a write tool aimed at its own reply channel answered every
question twice. The model wrote through the tool, the turn ended, and the
harness posted the model's final text as well. Nothing in the turn said the
harness was about to speak, and no reply could express "I have already said
what I came to say", so the duplicate was structurally guaranteed.

ParseReply reads an empty reply as silence rather than an error, and a silent
turn posts nothing. Per Kai's decision on the issue.

The revisit condition on that decision is that chosen silence must not become
indistinguishable from a broken turn, so silence is a choice only once the turn
has done something. An empty reply from a turn that ran no tool stays the parse
failure it was, with the same check name, stage, and notice, and the proxy's
repair loop still fires for it. turn.reply.silent records the choice with the
executed tool count.

RequireReply keeps the strict parse for the three callers with no silence to
express: the evaluation scorer, the board, and job content messages.

closes #895

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/sirens-echo!913
No description provided.