Repository navigation
Description
Problem
When a non-tool LLM call in an ACP session exhausts retries (AI_RetryError: maxRetriesExceeded or any unhandled error from the model stream), session/processor.ts halt() writes the failure to the local logfile and updates the assistant message's internal error field, but it never emits an ACP frame back to the client.
On the wire this looks like silence — the assistant message persists with parts: [] and no stopReason, and a connected ACP client has no way to distinguish "still streaming" from "the call failed."
Tool-call failures don't have this problem because they ride tool_call / tool_call_update shapes. But turn-level errors (rate limits, provider 5xx, context-overflow rejections, budget gates, auth failures, etc.) currently have no typed channel back to the client.
Reproduction
- Connect an ACP client and start a session.
- Trigger a turn that exhausts retries against the model provider (e.g. force a 429 or a bad API key).
- Observe: the client never receives any frame indicating the turn ended with an error. The watchdog (or a long human wait) is the only way to notice the turn is dead.
Proposal
Add a new session/update kind, agent_error, carrying a small closed vocabulary of error categories so clients can render a typed banner / retry affordance instead of guessing from a watchdog timeout.
interface AgentErrorUpdate {
sessionUpdate: "agent_error"
error: {
type: "budget" | "rate_limit" | "provider_unavailable" | "context_overflow" | "auth" | "unknown"
message: string
retryable: boolean
detail?: Record<string, unknown>
retry_after_seconds?: number | null
reset_at_epoch_ms?: number | null
source?: string | null
}
stopReason?: "error"
}retryable is on-the-wire explicit (rather than derived from type) so consumers don't have to maintain a parallel classification table.
The contract can land first as a type-only addition (no behaviour change, nothing imports the new module yet); the actual emit at halt() would be a follow-up so the wire contract is frozen before the producer change rolls out.
Why a new local type rather than extending @agentclientprotocol/sdk
The SDK's SessionUpdate is a closed type alias (discriminated union), not an interface. TypeScript declaration merging only works on interfaces, so the union can't be extended via an ambient .d.ts patch. A local extended union SessionUpdateWithAgentError = SessionUpdate | AgentErrorUpdate is the clean TS-native approach. If the SDK ever adopts agent_error as a first-class kind, the local alias can be deleted and the SDK literal imported directly — no other code needs to change.
Out of scope
- The actual emit site change at
halt()— kept as a follow-up so the contract can stabilise first. authis in the vocabulary for completeness but should not be emitted onagent_error: auth failures occur before a session exists, so they belong on a connection-scoped error channel, not a session/update.
Activity
github-actions commentedon May 8, 2026
This issue doesn't fully meet our contributing guidelines.
What needs to be fixed:
[FEATURE]:(current title usesfeat(acp):instead)Please edit this issue to address the above within 2 hours, or it will be automatically closed.
This issue might be a duplicate of existing issues. Please check:
end_turneven when assistant message has internal error — covers the same root problem (no typed ACP error channel for turn-level LLM failures) and references the same code paths insession/processor.tsandacp/agent.tsIf you believe this was flagged incorrectly, please let a maintainer know.
github-actions commentedon May 8, 2026
This issue has been automatically closed because it was not updated to meet our contributing guidelines within the 2-hour window.
Feel free to open a new issue that follows our issue templates.