fix(acp): parent tool calls to an assistant message for projection - #81
Merged
Conversation
ACP tool.call.updated payloads carried no parentMessageId/messageId, unlike the Claude and OpenAI runtimes. The platform session event projection drops TOOL_CALL_START when both are missing, so every OpenCode tool call lost its name and rendered as an orphaned generic "tool" card in the chat UI. Start an anonymous assistant message when a tool call opens without an active one (mirroring the Claude runtime's ensureMessageStarted) and stamp the sticky parentMessageId on every tool.call.updated payload.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the OpenCode (ACP) runtime rendering every tool call as a generic
toolcard in the platform chat UI (reported in YEF-891 with OpenCode + DeepSeek V4 Pro + an MCP server).Root cause. ACP
tool.call.updatedpayloads carried noparentMessageId/messageId, unlike the Claude runtime (agent-sdk-event-writer.tssetsparentMessageId) and the OpenAI runtime (app-server-item-events.tssetsmessageId). The platform projection (mosoo/pkgs/runtime-events/src/session-event-projection.tstoolCallStartEvent) dropsTOOL_CALL_START— the only AG-UI event carryingtoolCallName— when both ids are missing. The tool result still lands, soag-ui-session'sappendToolResultcreates an orphaned segment with the hardcoded fallback name"tool". Every OpenCode tool call (MCP and built-in, any model) therefore rendered astool, losing its real name. Theitem.startedevent does carry the title, but it is not projected to AG-UI at all.Changes
AcpAssistantTranscriptState: when a tool call opens without an active assistant message, start an anonymous assistant message first (mirroring the Claude runtime'sensureMessageStarted), so tools are parented to an assistant message rather than the user's prompt message. Applied to both thetool_call/tool_call_updatepath and the permission-request path.AcpToolEventState.patch: stamp a stickyparentMessageId(first observed parent wins for the call's lifetime) on everytool.call.updatedpayload. The field is already part of the wire contract (runtime-events.tsvalidatesparentMessageIdas an optional string ontool.call.updated).message.started/message.completedenvelope.Verification
Fed identical tool events through the real platform pipeline (
appRuntimeEventToAgUiSessionEvents→applyAgUiEventsToSessionLiveStatefrom the mosoo repo):Also reproduced the full platform path locally (opencode-ai 1.18.4
opencode acp --pure,OPENCODE_CONFIG_CONTENTwithdeepseek/deepseek-v4-pro, MCP servers passed via ACPsession/new, including through a faithful replica of the platform MCP proxy): OpenCode emitstitle/kindon every tool update, tool calls execute, turns end withend_turn— confirming the name loss happened in the event pipeline, not in OpenCode/DeepSeek/MCP transport.bun test: 1074 pass / 1 pre-existing failure (acp-file-system.test.ts/tmpvs/private/tmprealpath on macOS, fails on a clean checkout too)tsc --noEmit: cleanvp lint: clean