feat(reply): a reply that called tools shows which ones, and how each went #403

Merged
coilysiren merged 4 commits from feat/tool-call-footer into main 2026-08-13 12:23:02 +00:00
Member

The receipt a reader can see, where the grounding check is a guard only the
service can see. No hammer, no tool call, so a member can tell a lookup from an
answer from memory.

Three states rather than two. A failed lookup, an empty result, and a full one
read differently, which is the conflation 195 is about applied to disclosure.
The outcome is recorded where the call completes rather than derived at the
reply, because an empty result is recoverable from the result text afterwards
and a failure is not. That was the real work here: ExecutedTool carried no
status, so the state the feature exists for was the one nothing could see.

Aggregation is consecutive and never crosses a status. A A B A stays three
lines so the order survives, and a failure is never counted inside a run of
successes.

Appended where AppendIssueReferences already appends, after the checks. A
footer of backticked tool names is exactly the shape ValidateNoToolCallMarkup
rejects, so passing it through the model-reply checks would fail every
tool-using reply. The existing line already carries the reason: service
authored, so it runs after the checks rather than through them.

Names only. Arguments can carry member text and echoing it back would build a
surface into the data-borne vector in 177, so a test asserts an injected
argument does not survive into the reply.

Both lanes, because the reply path is shared and the verification problem is
identical. Making it Echo only would be the change requiring work, so it is
recorded on the issue as my call.

Three questions stay open and undecided: multi-message placement, whether very
large counts deserve surfacing rather than compression, and one I found while
wiring it, which is what happens when a reply near the Discord limit is
truncated with a footer on it.

closes #385

Co-authored-by: Kai Siren coilysiren@gmail.com
Co-authored-by: Claude noreply@anthropic.com

The receipt a reader can see, where the grounding check is a guard only the service can see. No hammer, no tool call, so a member can tell a lookup from an answer from memory. Three states rather than two. A failed lookup, an empty result, and a full one read differently, which is the conflation 195 is about applied to disclosure. The outcome is recorded where the call completes rather than derived at the reply, because an empty result is recoverable from the result text afterwards and a failure is not. That was the real work here: ExecutedTool carried no status, so the state the feature exists for was the one nothing could see. Aggregation is consecutive and never crosses a status. A A B A stays three lines so the order survives, and a failure is never counted inside a run of successes. Appended where AppendIssueReferences already appends, after the checks. A footer of backticked tool names is exactly the shape ValidateNoToolCallMarkup rejects, so passing it through the model-reply checks would fail every tool-using reply. The existing line already carries the reason: service authored, so it runs after the checks rather than through them. Names only. Arguments can carry member text and echoing it back would build a surface into the data-borne vector in 177, so a test asserts an injected argument does not survive into the reply. Both lanes, because the reply path is shared and the verification problem is identical. Making it Echo only would be the change requiring work, so it is recorded on the issue as my call. Three questions stay open and undecided: multi-message placement, whether very large counts deserve surfacing rather than compression, and one I found while wiring it, which is what happens when a reply near the Discord limit is truncated with a footer on it. closes #385 Co-authored-by: Kai Siren <coilysiren@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
feat(reply): a reply that called tools shows which ones, and how each went
All checks were successful
ci / test (pull_request) Successful in 30s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
ci / image-build (pull_request) Successful in 18s
02fcd7e5cc
The receipt a reader can see, where the grounding check is a guard only the
service can see. No hammer, no tool call, so a member can tell a lookup from an
answer from memory.

Three states rather than two. A failed lookup, an empty result, and a full one
read differently, which is the conflation 195 is about applied to disclosure.
The outcome is recorded where the call completes rather than derived at the
reply, because an empty result is recoverable from the result text afterwards
and a failure is not. That was the real work here: ExecutedTool carried no
status, so the state the feature exists for was the one nothing could see.

Aggregation is consecutive and never crosses a status. A A B A stays three
lines so the order survives, and a failure is never counted inside a run of
successes.

Appended where AppendIssueReferences already appends, after the checks. A
footer of backticked tool names is exactly the shape ValidateNoToolCallMarkup
rejects, so passing it through the model-reply checks would fail every
tool-using reply. The existing line already carries the reason: service
authored, so it runs after the checks rather than through them.

Names only. Arguments can carry member text and echoing it back would build a
surface into the data-borne vector in 177, so a test asserts an injected
argument does not survive into the reply.

Both lanes, because the reply path is shared and the verification problem is
identical. Making it Echo only would be the change requiring work, so it is
recorded on the issue as my call.

Three questions stay open and undecided: multi-message placement, whether very
large counts deserve surfacing rather than compression, and one I found while
wiring it, which is what happens when a reply near the Discord limit is
truncated with a footer on it.

closes #385

Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Merge branch 'main' into feat/tool-call-footer
All checks were successful
ci / test (pull_request) Successful in 34s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
ci / image-build (pull_request) Successful in 22s
83dd9b904b
test(reply): pin the footer's own truncation gap rather than ship it silently
All checks were successful
ci / test (pull_request) Successful in 33s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
ci / image-build (pull_request) Successful in 17s
5e28d75077
The footer is appended before the Discord send budget is applied, so a reply
near the ceiling loses it. Absence reads as no tools ran, which is the one
belief this feature exists to make impossible.

Pinned rather than fixed here, and pinned rather than filed and forgotten. The
fix needs the transport's budget to reach the place the footer is appended, and
that is an interface question rather than an arithmetic one: agent.go is
transport agnostic, the Discord turn owns the limit, and the HTTP turn has no
limit at all. Working that out badly would be worse than working it out later.

The second test records that this predates the footer. AppendIssueReferences
sits in the same position and has always been truncatable, so the fix belongs
to service-authored suffixes generally rather than to this feature.

Both follow the repository's characterization pattern: they assert today's
wrong behaviour and their failure message says the fix landed and the assertion
should follow it.

Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
fix(reply): keep the disclosure footer inside the transport's send budget
All checks were successful
ci / test (pull_request) Successful in 29s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
ci / image-build (pull_request) Successful in 22s
caf4345866
The footer was appended before the Discord ceiling applied, so a long reply
lost it, and absence reads as no tools ran. It failed exactly on the long
tool-heavy replies where the receipt matters most.

I framed this as needing a turnIO widening and it does not. replyBudget is an
optional capability asserted at the call site, the same pattern reactor and
spanTagger already use, so the Discord turn declares its ceiling and the HTTP
turn declares nothing and is unbounded. No interface every transport implements
had to change.

The answer yields rather than the footer, because the footer is short and
bounded and the answer is neither. A reply that already fits is untouched.

The two characterization tests I added an hour ago are replaced rather than
kept. They exercised the unbounded path, so they would have passed against this
fix without proving anything about it, which is the shape of test this battery
keeps finding. The replacements assert what a member receives.

Mutation checked by disabling the budget, where the reply comes back 2014 runes
against a 1990 ceiling.

The sibling hazard is unchanged and still real: AppendIssueReferences sits in
the same position and is still truncatable. It now has a budget to reach for.

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!403
No description provided.