Skip to content

feat(message): carry a tool result's trusted-verbatim request into the transcript#77

Open
yh928 wants to merge 2 commits into
tinyhumansai:mainfrom
yh928:feat/tool-message-trusted-verbatim
Open

feat(message): carry a tool result's trusted-verbatim request into the transcript#77
yh928 wants to merge 2 commits into
tinyhumansai:mainfrom
yh928:feat/tool-message-trusted-verbatim

Conversation

@yh928

@yh928 yh928 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Summary

A ToolResult can hold structured metadata in raw, but folding it into the transcript goes through Message::tool(call_id, content), which keeps only those two fields. Everything else is dropped at that boundary, so a host has no way to learn anything the tool wanted to say about its own output.

That matters for one property in particular: whether the content may be reshaped. Hosts summarise, truncate, batch, and re-frame tool output, and usually should — but some results are only useful intact. An input schema whose argument names the model must copy exactly, a signature, a diff: rewriting those produces content that still reads well and is wrong. Today a host can only guess from the text, and guessing wrong is silent.

This adds an explicit, opt-in request that survives the fold:

  • ToolResult::mark_trusted_verbatim() / is_trusted_verbatim(), backed by raw under a new TRUSTED_VERBATIM_KEY, so the opt-in costs nothing for results that never use it and round-trips through a host's own serialization.
  • ToolMessage.trusted_verbatim: bool, #[serde(default)].
  • Message::tool_from_result(&ToolResult), which carries the flag across. The agent-loop fold site (harness/agent_loop/tools.rs) now uses it.

The flag is advisory: the crate transports it, honouring it is the host's job.

Why raw-backed rather than a field on ToolResult

ToolResult is constructed all over user code; adding a required field would be a breaking change for every literal, and an Option/Default field would still widen a struct that most results never need. raw already exists for exactly this kind of side-band metadata, and a host that serializes results through its own types round-trips the flag with no schema change.

Concrete motivation

Downstream (OpenHuman) delivers a tool's full input schema to the model as a recoverable tool error, then verifies on the next turn that the model can still see it. The host's own after_tool stages summarise and whitespace-collapse tool output; without a way to say "not this one", the delivered schema arrives reshaped and the verification can never match. That is a host problem to solve, but it needs one bit of information the crate currently discards.

API Or Behavior Changes

Additive only.

  • New: TRUSTED_VERBATIM_KEY, ToolResult::mark_trusted_verbatim, ToolResult::is_trusted_verbatim, ToolMessage.trusted_verbatim, Message::tool_from_result.
  • Message::tool is unchanged and still yields trusted_verbatim: false, so nothing moves for callers that do not opt in.
  • ToolMessage gains a field. Struct-literal construction of ToolMessage outside the crate would need the new field; Message::tool / tool_from_result are unaffected.
  • Wire/serde: #[serde(default)] means transcripts persisted before this field still deserialize, reading as not-trusted.
  • Behavior change in the agent loop: the folded message now reflects the result's flag instead of always false. No effect unless a tool opts in.

Tests

All run locally on this branch:

  • cargo fmt --check
  • cargo clippy --all-targets -- -D warnings
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo build --all-targets
  • cargo build --all-targets --all-features
  • cargo test — 1205 lib tests + suites, 0 failed
  • cargo test --all-features — 1277 lib tests + suites, 0 failed
  • cargo run --example basic_graph

New coverage:

  • trusted_verbatim_is_opt_in_and_round_trips_through_raw — off by default; idempotent marking preserves unrelated raw metadata.
  • unrelated_raw_metadata_never_sets_the_flag — a non-boolean value at the key, and a non-object raw, both read as false, so the opt-in cannot be tripped by accident.
  • tool_from_result_carries_trusted_verbatim_and_tool_does_not — an unmarked result folds identically to Message::tool; a marked one carries the flag.
  • a_transcript_stored_before_the_flag_existed_still_deserializes — the serde(default) path.

Documentation

Rustdoc on every new item, including why the flag is advisory and why it lives in raw. No wiki page seemed warranted for a single additive field — happy to add one if you would prefer it documented there.

Summary by CodeRabbit

  • New Features

    • Tool outputs can be opt-in marked for byte-for-byte preservation back to the model.
    • Transcript entries now retain whether a tool output was marked as trusted verbatim.
    • Legacy transcripts remain compatible, defaulting to untrusted tool outputs.
  • Bug Fixes

    • Completed tool calls now record tool result content and metadata with full fidelity.
  • Tests

    • Added unit tests covering trust-flag behavior, wire/JSON compatibility, serialization rules, and idempotency.

…e transcript

A `ToolResult` can hold structured metadata in `raw`, but folding it into the
transcript goes through `Message::tool(call_id, content)`, which keeps only
those two fields. Everything else is dropped at that boundary, so a host has no
way to learn anything the tool wanted to say about its own output.

That matters for one property in particular: whether the content may be
reshaped. Hosts summarise, truncate, batch, and re-frame tool output, and
usually should — but some results are only useful intact. An input schema whose
argument names the model must copy exactly, a signature, a diff: rewriting those
produces content that still reads well and is wrong. Today a host can only guess
from the text, and guessing wrong is silent.

Add an explicit, opt-in request:

- `ToolResult::mark_trusted_verbatim()` / `is_trusted_verbatim()`, backed by
  `raw` under `TRUSTED_VERBATIM_KEY`, so the opt-in costs nothing for results
  that never use it and round-trips through a host's own serialization.
- `ToolMessage.trusted_verbatim: bool`, `#[serde(default)]` so transcripts
  persisted before this field still deserialize (and read as not-trusted).
- `Message::tool_from_result(&ToolResult)`, which carries the flag across; the
  agent-loop fold site now uses it.

The flag is advisory: the crate transports it, honouring it is the host's job.
`Message::tool` is unchanged and still sets `false`, so nothing moves for
callers that do not opt in.

Tests cover the opt-in being off by default, idempotent marking that preserves
unrelated `raw` metadata, non-boolean or non-object `raw` never tripping it,
`tool_from_result` matching `tool` for an unmarked result, and a pre-field
transcript still loading.

Verified: `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`,
`cargo build --all-targets`, `cargo test` (1205 lib tests + suites), and
`cargo run --example basic_graph`.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a873289-c765-41fc-8193-d11e206a0e75

📥 Commits

Reviewing files that changed from the base of the PR and between 0d93012 and 0e1358f.

📒 Files selected for processing (4)
  • src/harness/message/test.rs
  • src/harness/message/types.rs
  • src/harness/tool/test.rs
  • src/harness/tool/types.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/harness/message/types.rs
  • src/harness/tool/types.rs

📝 Walkthrough

Walkthrough

ToolResult gains an opt-in trusted-verbatim marker stored in raw. Tool-message construction preserves that marker, completed tool calls use the full result when appending transcript messages, and legacy deserialization defaults the new field to false.

Changes

Trusted verbatim propagation

Layer / File(s) Summary
ToolResult trusted-verbatim contract
src/harness/tool/types.rs, src/harness/tool/test.rs
Adds the trusted-verbatim key and methods for recording and checking it, with tests covering defaults, malformed metadata, idempotency, and preservation.
Transcript trust propagation
src/harness/message/types.rs, src/harness/message/mod.rs, src/harness/agent_loop/tools.rs, src/harness/message/test.rs
Adds ToolMessage.trusted_verbatim, maps it from ToolResult, keeps direct tool messages untrusted, and appends completed results through Message::tool_from_result; tests cover propagation and legacy deserialization.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ToolResult
  participant finish_tool_call
  participant Message_tool_from_result
  participant Transcript
  ToolResult->>finish_tool_call: completed result
  finish_tool_call->>Message_tool_from_result: full ToolResult
  Message_tool_from_result->>Message_tool_from_result: read is_trusted_verbatim()
  Message_tool_from_result->>Transcript: append ToolMessage with trust metadata
Loading

Possibly related PRs

Suggested reviewers: senamakel

Poem

I’m a rabbit with a transcript to weave,
Trusting the bytes we choose to believe.
Tool results hop in, metadata bright,
Legacy fields rest easy at night.
Into the message, the true flags go—
A tidy burrow for outputs to flow.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: propagating a tool result’s trusted-verbatim request into transcript messages.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/harness/tool/types.rs`:
- Around line 132-139: The mark_trusted_verbatim method must explicitly handle
non-object raw values instead of silently leaving them unmarked. Choose and
implement a clear behavior, such as adding a fallible try_mark_trusted_verbatim
API or encoding the trust marker while preserving the existing value, update
Message::tool_from_result to propagate that outcome correctly, and add a
regression test covering string/array raw values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 33ac1689-5708-4b0b-8b61-dcf0e12be50f

📥 Commits

Reviewing files that changed from the base of the PR and between 29769ad and 0d93012.

📒 Files selected for processing (6)
  • src/harness/agent_loop/tools.rs
  • src/harness/message/mod.rs
  • src/harness/message/test.rs
  • src/harness/message/types.rs
  • src/harness/tool/test.rs
  • src/harness/tool/types.rs

Comment thread src/harness/tool/types.rs Outdated
@greptile-apps

greptile-apps Bot commented Jul 26, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds an opt-in trusted_verbatim flag that travels from a ToolResult through to the transcript ToolMessage, letting a tool signal that its content must not be summarised, truncated, or re-framed by the host. The design is additive and backward-compatible: serde default/skip_serializing_if keep existing transcripts loadable and wire-identical for messages that never opt in.

  • ToolResult::mark_trusted_verbatim() / is_trusted_verbatim() store the flag under TRUSTED_VERBATIM_KEY in raw; the method returns false (rather than silently no-oping) when raw is a non-object value, and #[must_use] makes that failure visible.
  • ToolMessage.trusted_verbatim carries the flag in the transcript; Message::tool_from_result is the new fold path that transfers it, while Message::tool is unchanged.
  • The agent-loop fold site now calls tool_from_result, wiring the feature end-to-end; error/invalid-call paths correctly keep Message::tool.

Confidence Score: 4/5

Safe to merge for the flag-transport machinery itself; one gap in the crate's own compaction middleware should be addressed.

The new flag, its serde round-trip, and the agent-loop wiring are all well-implemented and covered by thorough tests. However, the crate's own MicrocompactMiddleware neither skips nor preserves trusted-verbatim messages — it replaces their content with a placeholder and resets the flag to false on the replacement. Any agent run that uses this middleware alongside mark_trusted_verbatim() will have the protection silently defeated, and downstream hosts inspecting the compacted transcript will observe an incorrect false.

Files Needing Attention: src/harness/middleware/library/context.rs — the MicrocompactMiddleware compaction loop needs a trusted_verbatim guard before it replaces message bodies.

Important Files Changed

Filename Overview
src/harness/tool/types.rs Adds TRUSTED_VERBATIM_KEY constant and mark_trusted_verbatim/is_trusted_verbatim on ToolResult; handles the non-object raw case by returning false (addresses the previous thread concern).
src/harness/message/types.rs Adds trusted_verbatim bool to ToolMessage with correct serde(default, skip_serializing_if) attributes and a private is_false helper; backward-compatible and wire-efficient.
src/harness/message/mod.rs Adds Message::tool_from_result that carries trusted_verbatim across from ToolResult; Message::tool is unchanged and continues to yield trusted_verbatim: false.
src/harness/agent_loop/tools.rs Agent loop now uses tool_from_result instead of Message::tool at the finish_tool_call fold site, correctly carrying the flag; error/invalid paths still use Message::tool (appropriate, as those have no ToolResult).
src/harness/middleware/library/context.rs MicrocompactMiddleware replaces tool message bodies with a placeholder without checking trusted_verbatim, silently defeating the new flag and stripping it from the replacement message.
src/harness/tool/test.rs Adds four targeted tests: opt-in idempotency, non-boolean key guards, refused marking on non-object raw, and the legacy-transcript serde path.
src/harness/message/test.rs Adds tests covering tool_from_result flag carry-through, legacy deserialization, and wire format omission for unset flag.

Sequence Diagram

sequenceDiagram
    participant Tool
    participant AgentLoop as "Agent Loop"
    participant MsgFactory as "Message factory"
    participant Transcript as "Transcript"
    participant Middleware as "MicrocompactMiddleware"
    participant Host

    Tool->>Tool: "mark_trusted_verbatim() sets raw key"
    Tool-->>AgentLoop: "ToolResult { trusted_verbatim in raw }"
    AgentLoop->>MsgFactory: "tool_from_result(&result)"
    MsgFactory->>MsgFactory: "is_trusted_verbatim() -> true"
    MsgFactory-->>Transcript: "ToolMessage { trusted_verbatim: true }"
    Transcript-->>Middleware: "ModelRequest with ToolMessage"
    Note over Middleware: "Does NOT check trusted_verbatim - replaces body with placeholder and strips flag to false"
    Middleware-->>Host: "ToolMessage { trusted_verbatim: false }"
    Host->>Host: "Sees false, may reshape content"
Loading

Reviews (2): Last reviewed commit: "fix(tool): report a refused trusted-verb..." | Re-trigger Greptile

Comment thread src/harness/tool/types.rs Outdated
Comment thread src/harness/message/types.rs Outdated
…flag

Both review bots flagged the same defect, and they were right: when `raw`
already held a non-object value, `mark_trusted_verbatim` did nothing at all.
`get_or_insert_with` returns the existing scalar, `as_object_mut()` gives
`None`, and the insert was skipped — with no return value, no panic, and no
diagnostic. `is_trusted_verbatim()` then kept answering `false` while the tool
author believed the opt-in had taken effect.

The flag lives under a key in `raw`, so a scalar genuinely has nowhere to put
it, and clobbering a value the tool deliberately set would be worse. So the
request is now *refused and reported* rather than silently lost:
`mark_trusted_verbatim` returns `bool`, and is `#[must_use]` precisely because
the failure is otherwise invisible. A tool that wants both can move its value
under a key of its own and call again.

Second finding: the field serialized as `"trusted_verbatim":false` on every
tool message, which contradicts this change's own "costs nothing unless you opt
in" claim and diverges from the rest of the crate. Added
`skip_serializing_if`, reusing the `is_false` helper convention already used by
`graph::export::types` rather than an inline `std::ops::Not::not`.

Tests: a new regression case walks a string, an array, and a number `raw`,
asserting each refuses, leaves the tool's own value untouched, and that the
call succeeds once room is made; a new wire test pins that an unset flag is
omitted and a set one is present. The existing tests now assert the return
value instead of discarding it — `#[must_use]` caught all three call sites.

Verified: fmt, clippy (both feature sets), build (both), `cargo test` (1207),
`cargo test --all-features`, and `cargo run --example basic_graph`.
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