Repository navigation
Replies: 1 comment
|
The useful distinction here is presentation versus control-plane facts. Omitting plans, modes, titles, and render-oriented cards keeps the automation contract small; correlated tool lifecycle and per-turn usage answer different machine questions: whether policy/cancellation/audit can identify the active action, and whether tenant accounting can reconcile the turn. A minimal projection could preserve the existing callId from tool/call through exactly one terminal tool_call_update, then normalize assistant-message usage into PromptResponse.usage or usage_update. Raw arguments and results need an explicit redaction and retention policy before crossing the process boundary. We added a source-pinned capability ledger, safe workaround guidance, and acceptance gates here: https://sandbaseai.github.io/deepseek-harness-handbook/acp-editor-boundary.html Disclosure: I contribute to this independent community handbook. |
Uh oh!
There was an error while loading. Please reload this page.
Summary
We built a multi-tenant agent platform that runs DSH as its inference backend over ACP, and we have real inference working end to end. This post is about the one gap that cost us the most: the ACP bridge emits exactly one
SessionUpdatevariant, so an automation client cannot reconstruct what happened during a turn — only the final text.We read
2026-07-23-acp-automation-only-protocol.mdfirst, and we are not asking to relitigate it. Keeping editor cards, plans, titles, modes and human elicitation out of an automation transport is the right call, andpackages/acp/acp/README.md:76-81registers the consequences honestly.Our ask is narrower, and we think it is consistent with that decision rather than a reversal of it: tool activity and token usage are not presentation. They are machine-readable control-flow and accounting facts that an automated controller needs, and they are the two things the note's "inspect durable logs or another API" escape hatch cannot deliver for an out-of-process client.
The current surface
One
sessionUpdateliteral exists in the whole package:The event handler at
packages/acp/acp/src/index.ts:222-252subscribes tosession/eventand filters toassistant/messageonly.What makes this cheap: the SDK already models all of it
This is the part we want to highlight — none of the following needs a protocol extension or a
_metaconvention. The pinned@agentclientprotocol/sdk0.25.1 (packages/acp/acp/package.json:35) already defines the full union atdist/schema/types.gen.d.ts:4623:And the harness's own internal event map already carries the data, in
packages/core/session/src/types.ts:tool/call—{ turn, step, callId, name, arguments }tool_calltool/result—{ turn, step, message, error? }tool_call_updateuser/message—UserMessageuser_message_chunkassistant/message—{ …, usage?: TokenUsage }usage/usage_updateSo the gap is purely projection at the transport boundary. The producers, the schema, and the SDK helpers all exist already.
1. Tool calls are invisible
Tools execute inside DSH and the ACP client sees nothing until the final assistant text. A concrete minimal shape, all fields already in
ToolCall(types.gen.d.ts:5231forToolCallStatus,:5292forToolKind):title/rawInputalone are enough for automation. Deciding not to synthesizekindorlocations(the render-intent concern that2026-07-04-trim-acp-bridge-unreachable-surface.mddeliberately removed) would be entirely consistent — we are asking for the correlation facts, not the cards.Our workaround: we route every tool through our own MCP facade so we can account for tool calls on our side, and we synthesize the timeline there. That only works because we own the tool surface; it does nothing for DSH-internal tools.
2. No token usage at the ACP boundary
session/promptreturns only{ stopReason }(packages/acp/acp/src/index.ts:405,:421-422). The harness has@deepseek-ai/dsh-token-meterandassistant/messagealready carriesusage?: TokenUsage(packages/core/session/src/types.ts:277), but nothing reaches the wire — so metering and billing integrations get zero.PromptResponsealready has a first-class optionalusagefield (types.gen.d.ts:3730), and the shapes line up almost 1:1:TokenUsage(packages/llm/llm/src/types.ts:135)Usage(types.gen.d.ts:5334)inputTokensinputTokensoutputTokensoutputTokenscacheReadTokens?cachedReadTokens?cacheWriteTokens?cachedWriteTokens?reasoningTokens?thoughtTokens?Populating
PromptResponse.usageon the turn-end frame looks like the smallest possible change with the highest value, and it adds no new event type at all.Our workaround: none available in-band. We currently reconcile token cost from our model gateway's billing records instead of from the agent runtime, which means our per-turn attribution is coarser than for our other backends.
3.
agent_message_chunkcarries nomessageIdAt
packages/acp/acp/src/index.ts:233-236the notification omitsmessageId, so a client receiving several chunks cannot group them per assistant message.ContentChunk.messageIdis optional in the schema (types.gen.d.ts:903), so this is spec-legal — butMessage.idis described as "Stable identity preserved across every representation boundary" (packages/llm/llm/src/message.ts:129-131) and is already in hand at the emit site, so passing it through looks close to free.Is the omission intentional, given the bridge emits whole committed messages rather than deltas?
4. No turn-state signal
There is no
running/idleframe, so clients synthesize turn boundaries from thesession/promptcall/return pair. That works for one in-flight prompt per session and we are not asking for a new event type here — noting it only becausesession_info_updateandcurrent_mode_updateexist in the union and might already be the intended home.Why the durable log isn't a substitute
The automation-only note suggests clients "inspect durable logs or another API" for tool traces and usage. For an out-of-process client that boundary is hard to use: the JSONL log is an internal format with no compatibility promise, it requires shared-filesystem access to the agent's
persistenceRoot(fine for a subagent on the same host, not for a service consuming DSH over a process boundary), and there is no ACP method to read it —session/load,listandresumeare all unsupported. So in practice ACP is the only surface available, and it is the one place the data is missing.Offer
We are happy to contribute PRs. The projection in
ctx.on('session/event', …)is where all four of these land, and we have equivalent mappings working in our own client already. If you would prefer these gated behind an opt-in config so the default automation contract stays minimal, that works for us too — say the word on shape and we will follow it.Happy to be told we have misread the design intent on any of these.
All reactions