Skip to content

fix: contain whole-response tool wrappers emitted as prose - #142

Merged
antra-tess merged 3 commits into
mainfrom
mica/tool-wrapper-prose-guard-main
Sep 7, 2026
Merged

fix: contain whole-response tool wrappers emitted as prose#142
antra-tess merged 3 commits into
mainfrom
mica/tool-wrapper-prose-guard-main

Conversation

@antra-tess

Copy link
Copy Markdown
Collaborator

Summary

Adds default-off toolWrapperProseGuard for the failure mode where a provider emits a complete textual rendering of an available tool invocation instead of structured tool_use.

On an exact whole-visible-response match, the framework executes nothing, publishes nothing, omits the wrapper from assistant continuity, preserves raw provider tracing, and records only [tool-boundary] No tool was called. as a system boundary. Unknown tools, quotation/code/mixed prose, genuine structured calls, and ordinary prose remain unchanged.

Lifecycle safety

  • successful ephemeral completion closes typing and outgoing completion after caller disposal
  • transient creation failure releases conversation/ephemeral reservation
  • watchdog-disposed ephemerals close every started outgoing stream without authorizing a stale successor frame
  • late disposed-generation tool events cannot execute
  • logical tool-call state survives retry/context-budget restart
  • EventGate and KV receipt ownership remain generation/frame scoped

Evidence

Exact head: dce7eacced743ea17409f9b48f0503c26db3048d

  • typecheck/build clean
  • focused wrapper/lifecycle 25/25
  • full sequential compiled suite 721 total / 717 pass / 0 fail / 4 skip
  • hostile Opus review: ACCEPT, 0 blockers
  • all three review-cycle lifecycle blockers have red mutation controls
  • direct timeout probe emits chunk -> stop typing -> outgoing complete

Boundaries

Default off. This PR does not enable the guard for any resident, alter any resident store, or authorize deployment/restart. Guarded turns deliberately do not expose speculative outgoing preview/TTS before whole-response classification; authoritative ordinary delivery occurs at turn completion.

@Anarchid Anarchid left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟢 CLEAR

Reviewer: Codex (GPT-5.6 Sol)

Reviewed head: dce7eacced743ea17409f9b48f0503c26db3048d

No material findings. The guard is fail-closed at the whole-response boundary, limits authority to the exact tool set sent on the inference, preserves genuine structured-tool history across retries/restarts, and prevents wrapper prose from reaching continuity, module speech, authoritative channel delivery, or speculative outgoing streaming. The generation-ownership changes also close the reviewed ephemeral disposal seams without letting late tool events act on a removed or replacement agent.

Tooling results:

  • ./node_modules/.bin/tsc --noEmit — passed.
  • npm run build — passed.
  • node --import tsx --test test/framework.test.ts — passed (1/1 test file).
  • node --import tsx --test test/tool-wrapper-prose-guard.test.ts test/tool-wrapper-prose-guard-integration.test.ts — passed (2/2 test files).
  • node --import tsx --test --test-concurrency=1 test/ephemeral-settle.test.ts test/deferred-wake-integrity.test.ts test/channel-lifecycle-provenance.test.ts test/event-gate-passthrough.test.ts test/event-gate-policy-mutation.test.ts test/tool-wrapper-prose-guard-integration.test.ts — passed (6/6 test files).
  • git diff --check 635f540c9fc3f2206d7369e23f8da656c42cb24b...HEAD — passed.
  • npm ci could not be used because the repository has no lockfile. A normal-sandbox npm install --ignore-scripts --no-package-lock --no-audit --no-fund could not complete with restricted registry access, so checks used the existing workspace dependency installation plus the mapped local Chronicle, Context Manager, and Membrane packages.
  • The repository-wide compiled test command was attempted both normally and with --test-concurrency=1; in this sandbox it stalled when reaching the unrelated code-execution.test.js after the first 9 files passed, and was interrupted. The PR's GitHub CI matrix is green; the changed guard and its immediate lifecycle/EventGate dependencies were exercised locally as listed above.

Verdict: clear to proceed on the inspected head. No blockers or non-blocking follow-up from this review; confidence is limited only by the sandbox-specific full-suite stall described above.

— Reviewed by GPT-5.6 Sol via OpenAI Codex.

@antra-tess
antra-tess merged commit f59cc22 into main Sep 7, 2026
5 checks passed

@slimepriestess slimepriestess left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Read against head dce7eac; CI green, Anarchid's Codex pass already clear. Approving, with three notes for the record rather than asks.

The guard core is right and narrow. detectKnownToolWrapperProse matches only when every visible block is text (signed thinking allowed), the joined body is exactly one wrapper shape, and the tag names a tool from the set sent on that inference. The unit test's negative table covers the cases I'd have worried about (quoted, fenced, prefixed, mixed, unclosed, mismatched close, structured tool_use, empty tool set). Integration test 913 pins the request-disallowed case, which is the one that matters for authority. The containment path stores only the fixed receipt with the tool name in metadata, and allText becomes empty so nothing reaches speech or the outgoing surfaces. Good.

Note 1, operator-facing cost worth stating where the switch lives. With toolWrapperProseGuard: true, proseStream is never constructed for that resident, so speak-while-acting, typing preview, and any streamed outgoing delivery are off for every turn, not just wrapper turns; ordinary prose arrives at turn completion (test 976). The docs page says so; the recipe-side field comment in host #115 says only "default-off containment". Since the recipe is where an operator flips it, a clause there ("disables streamed prose delivery for this agent") would save someone a puzzled hour. Not blocking.

Note 2, single-generation names verified safe for current callers. usedEphemeralAgentNames is never released on successful disposal (only on creation failure), so a name can't recur for the framework's lifetime. I checked the two things that could bite: conversation forks carry a generation counter in the name (forkAgentName(channelId, generation)), so an idle-TTL close followed by a fresh message spawns a new name, no collision; and the host's subagent module names spawns spawn-<name>-<Date.now()> and forks <name>-d<depth>[-retry<n>]-<Date.now()>. The spawn site's retry loop recomputes only the timestamp, so a retry landing in the same millisecond as a failed run would now throw where it previously overwrote; that's a host-side nit for its own retry naming (the fork site already carries retry${attempt}), not this PR's problem.

Note 3, scope. The title is the guard; roughly two thirds of the framework diff is ephemeral/conversation generation ownership (generation tickets, single-generation names, late-event discard by Agent identity, disposal owning terminal typing/outgoing close). I spot-read the disposal path and the generationLost check and they hang together; I did not re-run the red mutation controls the body cites. Anarchid's pass covered that half, so I'm counting on it rather than duplicating it.

antra-tess added a commit to Meganeuridae/agent-framework that referenced this pull request Sep 7, 2026
Two conflicts against the 7 commits main gained since 7324521 (anima-research#141
turn-provenance batched wakes, anima-research#142, anima-research#143, 0.12.0), both keep-both:

- driveStream pre-stream: main's ephemeral-disposal ownership check runs
  first, then this branch's dispositions injection for the subconscious.
- driveStream finally: main's `frameReachedTerminal` guard on the deferred
  flush, over this branch's per-agent `drainDeferredFor`.

The auto-merged channel-incoming wake loop keeps anima-research#141's `counterparty` /
`addressed` provenance under this branch's `targetAgents` routing.

Bug fixed while reviewing: `getAllTools()` pushed TUNE_OUT_TOOL onto the
array `getChannelTools()` returns, which is the registry's module constant
CHANNEL_TOOL_DEFINITIONS — every call appended another `tune_out` to the
shared list. The array is now copied before the push; tune-out-e2e asserts
the resident's board carries `tune_out` exactly once across repeated calls.

Verified against a build of context-manager#54 (main 92f5a65 + ef11162):
tsc clean, suite 725 / 0 fail / 4 skipped, tune-out suites 22/22.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bm7wvGLvxF6RGAKCRB6MH7
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.

3 participants