Repository navigation
fix(mcp): chat_with_agent carries the caller's turn so delegated work reports back (#3232) - #3297
Conversation
…lt, opt-out, kill switch (#3232) §15.1h gains the MCP caller contract: async dispatches carry the platform turn as parent by default, a typed "manual" opts out, sync stays opt-in, sequential /chat carries nothing, MCP_REPORT_BACK_ENABLED stops it all. Known limits drop the stale pull-sink entry (#3114) and add the shutdown and cleanup-sweep terminals, the cross-turn replay and plain sequential /chat. §45 FR-5b states execution_id parity for dedicated tools; the mcp-server architecture row names resolveReportBack and the kill-switch plumbing. Refs #3232
…oute (#3232) — red Real createServer + real MCP client over streamable HTTP + the real reconciler, with a fake backend recording each /task and /chat body, the forwarded X-Trinity-Execution-Id and the Idempotency-Key. Covers the async default, the typed opt-in and the manual opt-out, header-first, the dedicated tool, the report_back result fields, the kill switch and its env wiring, and a table test of resolveReportBack. Every absent assertion is paired with a forwarding sibling so the file is red on dev. Red on its own by design; the next commits turn it green. Refs #3232
…ry /task route (#3232) resolveReportBack decides once per call whether parent_execution_id goes on the /task body and which report_back fields the result gets: async dispatches (parallel+async, #946 pull-routed) default to the platform header turn; a typed "manual" opts out; a self-task with inject_result is excluded; sync parallel is opt-in by a typed id; the header turn wins over a typed id, which must pass the header's own format check (isWellFormedExecutionId); sequential /chat never carries one. One [Report-Back #3232] log line per call. callerTurn and the idempotency key are unchanged. createChatTools / runAgentChat take reportBackEnabled (default on), plumbed from createServer. Refs #3232
…ution_id (#3232) zod drops an undeclared key before execute, so the dedicated tools lost the typed id (and the manual opt-out) silently. They now declare it and pass it to runAgentChat, with the same report-back rule as chat_with_agent. reportBackEnabled threads through makeDedicatedChatTool (trailing optional) and ReconcilerOptions (required, so tsc fails if a start site forgets it); index.ts hands createServer's value to the reconciler. Refs #3232
…ery chat_with_<agent> (#3232) EXECUTION_ID_PARAM_DESCRIPTION states the async default, the "manual" opt-out, how a sync call opts in, that requested is not a guarantee, that a plain sequential call does not report back, and that a replay reports where the first run was asked to. Parameter text is not cut at the 2,048-char tool-description cap; the tool descriptions and DELEGATION_CONTRACT are unchanged. Refs #3232
…rt-back (#3232) createServer reads MCP_REPORT_BACK_ENABLED (default on; only "false" turns it off) and logs the mode at startup beside the #946 line. Wired into the mcp-server service of all three compose files and documented in .env.example. Turning it off stops every parent_execution_id, default and typed, without an image revert. Refs #3232
#3232) channel-completion-report gains an Entry Points row, the per-route table, a copy-paste example, the opt-out, the chain behaviour and a 'no note arrived' runbook; its pull-sink row is corrected (#3114) and the shutdown / cleanup-sweep terminals are listed. agent-to-agent-collaboration: the pull branch forwards the parent, and async delegation reports back by default. User docs replace the unconditional 'yes, you'll hear back' with the async vs sequential split. feature-flows.md index row. Refs #3232
…sation (#3232) POST /api/agents/{name}/voice-reply checked only that the execution was the caller's own, then delivered to its source_channel* — so a delegated child that inherited a Slack or Telegram context could post a voice note into the user's thread, with no proactive-consent check on Slack. The async report-back default makes such children common. The route now answers {delivered: false, reason: "delegated_turn"} when source_channel_agent is set (non-NULL means inherited, turn_audience._is_delegated), before any channel branch. §48.1 FR-9. Refs #3232
…independent oracle (#3232)
…gation in known limits (#3232)
…artup line names every off value (#3232)
…inherited context is for the report only (#3232)
…FAQ qualifies report-back (#3232)
/review ReportBranch:
The backend voice-reply refusal is additive scope, but it follows directly from the default: without it, a delegated child with voice enabled could speak into the user's thread with no proactive-consent check. It is justified in the PR body, so it is not drift. Execution coverage (Step 2.5)
Source-text grep ( Fix mutation (run locally, then restored):
Local runs on the PR head: Critical Findings (block merge)None. Informational Findings (review required)[I1] Product Quality (4.15): the default turns on unprompted posts into customer threads, once per async hop (Confidence: 8/10) [I2] Product Quality (4.15): the kill switch exists only as an env var (Confidence: 6/10) [I3] Observability: Low confidence (appendix)
Clean Categories
Summary
🤖 Generated with Claude Code |
dolho
left a comment
There was a problem hiding this comment.
Approved — /review found no blocking findings; see review comment above.
|
merge-train (2026-10-07): changed the body's |
…ep-both on append-only files
Four user-docs files conflicted with this train's siblings (#3294, #3297, #3299, #3303), which documented their own changes on the same lines. Each hunk keeps both sides' facts once: dev's new text, plus this sync's additions (wired-boundaries list, receipt contract, new FAQ entries, bound-widget field rules). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
merge-train (2026-10-07): merged as part of train #3329 (green). Before the squash, |
Fixes #3232
chat_with_agentdeclared anexecution_idargument for report-back (ent#224) but never read it, and the dedicatedchat_with_<agent>tools never declared it, so work delegated from a Slack, Telegram or Workspace turn never reported back into that conversation. This PR makes the MCP server send the caller's turn asparent_execution_idon every/taskroute. It also turns report-back on by default for async delegation and adds a kill switch. The backend inheritance guard is unchanged. This isRefs, notFixes: a plain sequential call still cannot report back (see below).Where the parent id comes from, and why
X-Trinity-Execution-Idturn the agent is actually serving. A model-typedexecution_idis used only when there is no header. Reason: a resumed session can copy a stale id out of its history, and the header names the live turn. It is the same rule the other effect tools already use (resolveExecutionId).manualis never forwarded.parallel=true, async=true, and the experiment: pull-coordination pilot — route MCP chat_with_agent through the agent queue #946 pull-routed sequential call, which also answers with a receipt). The caller's turn ends with a receipt, so the child's note is usually the person's only news. Passexecution_id="manual"to turn it off for one call. A self-task withinject_result=trueis excluded, because it already names its destination.execution_id): the caller answers inline, so a default would double-post on almost every call.report_back: "requested"in its result (plus a short note on the default path); an opted-in call that cannot send one getsreport_back: "off"with a reason. Gate results, depth refusals and thrown errors are unchanged.Idempotency-Key, and the turn header forwarded to the backend is computed exactly as before; a typedmanualnever clears it.resolveReportBack(src/mcp-server/src/tools/chat.ts). It logs one[Report-Back #3232]line per call, so the default's uptake can be measured after deploy.Behaviour change in customer conversations
parallel=true, async=true) and replies "on it". The job finishes, and nobody in the thread hears about it.allow_proactive, Telegram groupallow_proactive(Telegram DMs are consent by construction), and the Workspace, where the note goes only to the client thread that started the work. Delivery is best-effort: no binding, no consent or a failed send means no note.MCP_REPORT_BACK_ENABLED=falseon the mcp-server and restart it (false,0,nooroff, any case). No parent is sent anywhere, default or typed, and receipts are byte-identical to today. No image revert is needed. The knob is indocker-compose.yml,docker-compose.prod.yml,docker-compose.hosted.ymland.env.example.One backend change: delegated children no longer speak into the conversation
POST /api/agents/{name}/voice-reply(src/backend/routers/agents.py) now refuses a delegated child, i.e. a row that inherited its parent's conversation (source_channel_agentset), withreason="delegated_turn", before any channel branch. Inherited context exists for the consent-gated completion report. The voice route has no proactive-consent check, so the default above would otherwise let a delegated child with voice enabled speak into the user's thread. A direct channel turn still delivers.chat_execution_service.pyis untouched, and_inherited_channel_context(ent#265) is still the only gate on who may inherit a conversation; its documented limits apply unchanged.What still cannot report back, and why
parallel=false,/chat):ChatMessageRequesthas no parent field, and/chatcreates its row without inheritance. This needs backend work; follow-up bug: a sequential chat_with_agent call cannot report back to the caller's conversation #3295. The result saysreport_back: "off", reasonsequential_chat, when an id was passed.failedwith no report (pre-existing).fan_out: its backend request has no parent field.Note for abilityai/trinity-enterprise#566
Its body says "
chat.tsforwardsexecution_id". That was false before this PR. It is now true on every/taskroute, and by default for async delegation. When its intent key is designed, it should prefer the platform turn (X-Trinity-Execution-Id, which the backend already receives on/chatand/task), with the typed parent as a fallback, as the skill-gate requester already does.Tests
src/mcp-server/src/chat-parent-execution.test.ts(new). It uses the realcreateServer, a real MCP client over streamable HTTP, the real reconciler for a dedicated tool, and a fake backend that records the/taskand/chatrequest bodies, the forwarded turn header and theIdempotency-Key. Every acceptance criterion is asserted on what leaves the MCP server. Each "nothing sent" assertion is paired in the same test with a forwarding sibling, so the file is red ondev.resolveReportBackis also checked over its full input product against an independent oracle.tests/unit/test_3232_voice_reply_delegated_child.py: a delegated child is refused on Slack and Telegram; a direct turn still delivers. Red ondev.test(mcp): …) is red on its own by design (failing tests first). Squash-merge is assumed.Local proof (targeted, as agreed for this PR; CI runs the full suites):
src/mcp-server:npm run buildclean;npm test775/775.routers.agents, the ent#224/ent#265/ent#457 report paths, compose and.env.exampleparity, and the registry guard: 780 passed./taskbranch, 18 of the 55 tests fail; with the original bug restored (executedropsexecution_id), 16 fail; with the dedicated tool not forwarding, 3 fail; with the kill-switch env read ignored, 1 fails.MCP_REPORT_BACK_ENABLEDon themcp-serverservice only, andfalseoverrides it.Follow-ups: #3295 (sequential
/chatreport-back) and #3296 (cross-turn replay).The
uilabel is on because the compose files put this PR in the Lane C merge gate, which runsfrontend-e2eon the PR (it passed). No frontend code changed.Reviewers: this PR turns on a behaviour that posts into customer conversations without an argument (async delegation report-back), so the review is also the product sign-off for that default.