Problem
When using --format quiet, token usage information (inputTokens, outputTokens, cachedReadTokens, cachedWriteTokens, totalTokens) and cost are completely discarded — they appear neither in stdout nor stderr.
This makes it impossible for callers (CI runners, orchestrators, billing dashboards) to track API consumption without switching to --format json, which changes the stdout contract entirely.
Steps to Reproduce
acpx --cwd /tmp --approve-all --format quiet --verbose \
claude exec "Reply with exactly: OK"
stdout:
stderr:
[acpx] spawning installed built-in agent @agentclientprotocol/[email protected] ...
[acpx] initialized protocol version 1
Token usage and cost: nowhere to be found.
Compare with --format json, where the final JSON-RPC response contains full usage:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"stopReason": "end_turn",
"usage": {
"inputTokens": 17030,
"outputTokens": 4,
"cachedReadTokens": 0,
"cachedWriteTokens": 0,
"totalTokens": 17034
}
}
}
Root Cause
QuietOutputFormatter.onAcpMessage() in src/output.ts only handles two cases and silently ignores everything else:
onAcpMessage(message) {
// Case 1: collect text chunks
const update = extractSessionUpdateNotification(message);
if (update?.update.sessionUpdate === "agent_message_chunk" && ...) {
this.chunks.push(update.update.content.text);
return;
}
// Case 2: flush text on stop
if (parsePromptStopReason(message)) this.flushBufferedOutput();
// ← result.usage is right inside `message.result` here, but is never read
}
onError(_params) {} // errors also silently dropped
flush() {}
parsePromptStopReason() already parses message.result to extract stopReason — message.result.usage is sitting right there at the same time but is never consumed.
Proposed Fix
Emit usage and cost to stderr when the final result arrives. This follows standard CLI conventions: stdout stays clean for piping, stderr carries diagnostic/meta information.
onAcpMessage(message) {
const update = extractSessionUpdateNotification(message);
if (
update?.update.sessionUpdate === "agent_message_chunk" &&
update.update.content.type === "text"
) {
this.chunks.push(update.update.content.text);
return;
}
const stopReason = parsePromptStopReason(message);
if (stopReason) {
this.flushBufferedOutput();
// emit token usage to stderr — keeps stdout clean for piping
const result = (message as { result?: Record<string, unknown> }).result;
const usage = result?.usage as Record<string, number> | undefined;
if (usage) {
process.stderr.write(
`[acpx] tokens: input=${usage.inputTokens} output=${usage.outputTokens}` +
` cache_read=${usage.cachedReadTokens} cache_write=${usage.cachedWriteTokens}` +
` total=${usage.totalTokens}\n`
);
}
const cost = result?.cost as { amount?: number; currency?: string } | undefined;
if (cost?.amount != null) {
process.stderr.write(`[acpx] cost: ${cost.amount} ${cost.currency ?? "USD"}\n`);
}
}
}
After the fix, stderr would look like:
[acpx] spawning installed built-in agent @agentclientprotocol/[email protected] ...
[acpx] initialized protocol version 1
[acpx] tokens: input=17030 output=4 cache_read=0 cache_write=0 total=17034
[acpx] cost: 0.051276 USD
stdout remains unchanged (OK), so existing callers that pipe or parse stdout are completely unaffected.
Versions
acpx: 0.5.3
@agentclientprotocol/claude-agent-acp: 0.29.2
Related
Problem
When using
--format quiet, token usage information (inputTokens,outputTokens,cachedReadTokens,cachedWriteTokens,totalTokens) and cost are completely discarded — they appear neither in stdout nor stderr.This makes it impossible for callers (CI runners, orchestrators, billing dashboards) to track API consumption without switching to
--format json, which changes the stdout contract entirely.Steps to Reproduce
stdout:
stderr:
Token usage and cost: nowhere to be found.
Compare with
--format json, where the final JSON-RPC response contains full usage:{ "jsonrpc": "2.0", "id": 2, "result": { "stopReason": "end_turn", "usage": { "inputTokens": 17030, "outputTokens": 4, "cachedReadTokens": 0, "cachedWriteTokens": 0, "totalTokens": 17034 } } }Root Cause
QuietOutputFormatter.onAcpMessage()insrc/output.tsonly handles two cases and silently ignores everything else:parsePromptStopReason()already parsesmessage.resultto extractstopReason—message.result.usageis sitting right there at the same time but is never consumed.Proposed Fix
Emit usage and cost to stderr when the final result arrives. This follows standard CLI conventions: stdout stays clean for piping, stderr carries diagnostic/meta information.
After the fix, stderr would look like:
stdout remains unchanged (
OK), so existing callers that pipe or parse stdout are completely unaffected.Versions
acpx: 0.5.3@agentclientprotocol/claude-agent-acp: 0.29.2Related
QuietOutputFormatter(onError(_params) {})