Repository navigation
ACP adapter returns end_turn even when assistant message has internal error #24494
Description
Summary
The ACP adapter appears to return a successful stopReason: "end_turn" even when the underlying assistant message contains an internal error.
This was observed while testing with Fabriqa / Fabriqa.ai acting as the ACP client. Fabriqa finalized the turn from the ACP end_turn result, so provider/session errors that remained internal appeared as a blank assistant response.
This is problematic for ACP clients because end_turn means the agent completed the turn normally. If the model/provider/session processor failed, the client needs a protocol-visible failure, not only an internal session.error event or stderr/log output.
Why this matters
ACP clients communicate with the agent over JSON-RPC. Stderr/logs are diagnostic only and should not be required for correctness.
If a session/prompt turn fails due to provider/model/session error, the ACP adapter should surface that through the ACP/JSON-RPC channel. Returning successful end_turn with no useful assistant content causes clients to finalize blank or misleading assistant messages.
ACP references:
- Prompt turn lifecycle: https://agentclientprotocol.com/protocol/prompt-turn
- Stdio transport / stderr logging: https://agentclientprotocol.com/protocol/transports
Source-level evidence
In packages/opencode/src/session/processor.ts, failures are captured on the assistant message:
ctx.assistantMessage.error = error
yield* bus.publish(Session.Event.Error, {
sessionID: ctx.assistantMessage.sessionID,
error: ctx.assistantMessage.error,
})But in packages/opencode/src/acp/agent.ts, the ACP session/prompt path does not appear to inspect that error:
const response = await this.sdk.session.prompt(...)
const msg = response.data?.info
await sendUsageUpdate(...)
return {
stopReason: "end_turn" as const,
usage: msg ? buildUsage(msg) : undefined,
_meta: {},
}So an assistant message with msg.error can still be mapped to a successful ACP end_turn.
Expected behavior
If response.data?.info.error is present, the ACP adapter should not return a normal successful end_turn.
It should either:
- return a JSON-RPC error response to the
session/promptrequest, or - send a user-visible ACP update explaining the error before ending the turn.
Additionally, the adapter should guarantee that all pending session/update notifications are flushed before returning the session/prompt response. This is related to the existing ordering issue where end_turn can arrive before remaining agent_message_chunk notifications:
Impact
Clients such as Fabriqa, agent-shell, or other ACP hosts may finalize the turn as successful before receiving the actual content or before learning that the turn failed. In the provider-error case, this can produce a blank assistant response with no visible error.
P.S. This report and issue is AI generated but not AI slop. It was manually reviewed by a human over long turns and checked against local logs/source to ensure this is a valid issue.
Activity
github-actions commentedon Apr 26, 2026
Thanks for updating the issue! It now includes a clear description, source-level evidence, reproduction context, and expected behavior. The compliance flag has been removed.
truenorth-lj commentedon May 8, 2026
Hi @hancengiz — thanks for the source-level analysis here, this lined up cleanly with what we needed. The fix is ending up as a two-PR stack:
AgentErrorUpdatesession/updatekind (type-only / additive — no behaviour change yet).acp/agent.ts handleEvent()that convertssession/processor.ts halt()'s recorded error into an ACP frame, so retry-exhaustion failures become a typed frame on the wire instead of a silent stalled turn.The split was so the wire contract could land first before the producer side; happy to combine them if maintainers prefer. Credit to you for catching this —
end_turnfor an internally-failed turn is exactly the bug we were seeing on real ACP clients too.axel7083 commentedon Jun 26, 2026
Looking at https://agentclientprotocol.com/protocol/v1/overview#error-handling opencode should not return
end_turn, this is leading to a pretty weird UI, where the user may think the model just did not responded..