Skip to content

fix(harness): bound generic function result content - #1016

Closed
mfpiccolo wants to merge 1 commit into
mainfrom
fix/bound-function-results
Closed

fix(harness): bound generic function result content#1016
mfpiccolo wants to merge 1 commit into
mainfrom
fix/bound-function-results

Conversation

@mfpiccolo

@mfpiccolo mfpiccolo commented Aug 30, 2026

Copy link
Copy Markdown

Summary

Prevent oversized generic function results from being duplicated verbatim into both model-facing content and structured details.

This mitigation is function-agnostic. It does not special-case Compose or any other worker.

Previously, when a function returned an arbitrary JSON value without explicit content blocks, Harness serialized the complete value into model-facing text while also retaining the same value in details. Large function results were therefore duplicated in the transcript.

Harness now:

  • Preserves the complete structured result in details.
  • Keeps existing rendering for ordinary-sized results.
  • Replaces oversized implicit JSON rendering with a bounded structural summary.
  • Preserves explicit function-provided content blocks unchanged.

Oversized result summary

When implicit serialized content exceeds 16 KiB, the model-facing result contains metadata similar to:

{
  "result_omitted_from_content": true,
  "reason": "result exceeds model-facing inline limit",
  "type": "object",
  "serialized_bytes": 68432,
  "inline_limit_bytes": 16384,
  "details_available": true,
  "fields": 2,
  "field_names": ["containers", "operation_id"]
}

The original result remains available losslessly in details.

Behavior

Result type Behavior
Ordinary generic result Existing compact JSON rendering is preserved
Oversized generic result Model-facing content becomes a bounded summary
Explicit content blocks Preserved unchanged, regardless of size
Structured details Always preserve the original result

Testing

Regression coverage confirms that:

  • Oversized generic results are not duplicated into model-facing content.
  • The complete value remains in details.
  • Explicit content blocks are not rewritten.
  • Ordinary-sized generic results retain their existing rendering.

Commands run:

  • cargo test -p harness --lib — 453 tests passed
  • cargo clippy -p harness --lib -- -D warnings

Related PR

Companion Compose source fix: iii-hq/iii#<2107>

@vercel

vercel Bot commented Aug 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview Aug 30, 2026 3:14pm
workers-tech-spec Ready Ready Preview Aug 30, 2026 3:14pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

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

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 69 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

@andersonleal

Copy link
Copy Markdown
Collaborator

Closing: this behaviour already exists, one layer down, and this change would regress it.

The cap is already there

context::assemble runs an unconditional per-result cap as Step 0, before prune and before compaction:

  • context-manager/src/core/prune.rs:589cap_results_with_sizes()
  • context-manager/src/functions/assemble.rs:344-359 — the wiring
  • Default max_result_tokens: 20_000 (~80 KB at the chars/4 estimator), configurable per worker config and per call, 0 disables it

It landed as #801 / MOT-4014, "fix(context-manager): bound context growth from function results" — the same intent as this PR's title.

It is not optional: assemble_context() (harness/src/turn_loop.rs:2711) has no fallback, an assemble error fails the turn. Every result that reaches a model has passed the cap.

details is not model-facing

The premise — that an oversized result is "duplicated verbatim into both model-facing content and structured details" — doesn't hold. Providers serialize only content; the sole exception is the status: "denied" envelope (provider-anthropic/src/wire/messages.rs:74, same in openai / openrouter / deepseek / xai). And the estimator explicitly nulls details before counting (context-manager/src/core/estimate.rs:55-60).

So there is no token duplication. details costs storage and wire bytes, not context — and that is exactly the half this PR preserves losslessly.

What the change would break

  1. 16 KiB vs ~80 KB, and destructive where the existing pass is not. Between the two thresholds the model today gets head + marker + tail of the real content; with this PR it gets a list of field names. 16 KiB is small for a file read or a search result.
  2. The summary points somewhere the model cannot reach. It emits "details_available": true, but the model has no way to read details. The existing marker says re-call <function_id> with narrower arguments if the omitted middle is needed, which is actionable.
  3. It disarms the details bounding that exists today. When the cap fires, details over 2 KiB is replaced with {"context_capped": {...}} (prune.rs:611-623). With this PR the content lands under the cap threshold, cap_results_with_sizes never fires, and details stays full-size in the transcript forever.
  4. It doesn't fix the actual failure. append_function_result() (turn_loop.rs:2502) persists details verbatim through session::append, so a >1 MiB result still exceeds the engine WS message limit and wedges the harness. This PR shrinks only the half that was already bounded.

Suggested path

If 20k tokens is too generous for the Compose case, lower max_result_tokens in the context-manager config, or pass options.max_result_tokens on the assemble call — a line of YAML instead of a hard-coded 16 KiB constant in normalize().

The real fix is the companion iii-hq/iii#2107 (the Compose source). If the >1 MiB wedge is the concern, the place to bound it is the append path, not normalize() — and that would be a separate, welcome PR.

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.

2 participants