Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 7 additions & 63 deletions cli/src/agent/backends/acp/AcpSdkBackend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -645,59 +645,6 @@ describe('AcpSdkBackend', () => {
expect(turnCompleteIdx).toBeGreaterThan(stragglerIdx);
});

it('emits a context-only usage mid-turn so the status bar updates live', async () => {
backendStatics.UPDATE_QUIET_PERIOD_MS = 25;
backendStatics.UPDATE_DRAIN_TIMEOUT_MS = 200;
backendStatics.PRE_PROMPT_UPDATE_QUIET_PERIOD_MS = 1;
backendStatics.PRE_PROMPT_UPDATE_DRAIN_TIMEOUT_MS = 50;
backendStatics.LATE_FLUSH_INTERVAL_MS = 5;
backendStatics.LATE_FLUSH_QUIET_PERIOD_MS = 10;
backendStatics.LATE_FLUSH_WINDOW_MS = 50;

const backend = new AcpSdkBackend({ command: 'opencode' });
const backendInternal = backend as unknown as {
transport: {
sendRequest: (...args: unknown[]) => Promise<unknown>;
close: () => Promise<void>;
} | null;
handleSessionUpdate: (params: unknown) => void;
};

const messages: AgentMessage[] = [];
backendInternal.transport = {
sendRequest: async () => {
backendInternal.handleSessionUpdate({
sessionId: 'session-1',
update: { sessionUpdate: 'usage_update', used: 1_000, size: 200_000 }
});
backendInternal.handleSessionUpdate({
sessionId: 'session-1',
update: { sessionUpdate: 'usage_update', used: 1_000, size: 200_000 }
});
backendInternal.handleSessionUpdate({
sessionId: 'session-1',
update: { sessionUpdate: 'usage_update', used: 2_500, size: 200_000 }
});
await sleep(5);
return {
stopReason: 'end_turn',
usage: { inputTokens: 100, outputTokens: 50 }
};
},
close: async () => {}
};

await backend.prompt('session-1', [{ type: 'text', text: 'hello' }], (m) => messages.push(m));

const usageMessages = messages.filter((m): m is Extract<AgentMessage, { type: 'usage' }> => m.type === 'usage');
// Two mid-turn ticks (deduped second 1_000) + one final emit with the
// prompt-level input/output totals.
expect(usageMessages.length).toBe(3);
expect(usageMessages[0]).toMatchObject({ inputTokens: 0, outputTokens: 0, contextTokens: 1_000, contextWindow: 200_000 });
expect(usageMessages[1]).toMatchObject({ inputTokens: 0, outputTokens: 0, contextTokens: 2_500, contextWindow: 200_000 });
expect(usageMessages[2]).toMatchObject({ inputTokens: 100, outputTokens: 50, contextTokens: 2_500, contextWindow: 200_000 });
});

it('emits a context-only usage on finalize when the prompt response carries no usage', async () => {
backendStatics.UPDATE_QUIET_PERIOD_MS = 25;
backendStatics.UPDATE_DRAIN_TIMEOUT_MS = 200;
Expand Down Expand Up @@ -734,15 +681,12 @@ describe('AcpSdkBackend', () => {
await backend.prompt('session-1', [{ type: 'text', text: 'hi' }], (m) => messages.push(m));

const usageMessages = messages.filter((m): m is Extract<AgentMessage, { type: 'usage' }> => m.type === 'usage');
// One mid-turn emit and one finalize fallback — both context-only.
expect(usageMessages.length).toBe(2);
for (const usage of usageMessages) {
expect(usage).toMatchObject({
inputTokens: 0,
outputTokens: 0,
contextTokens: 4_200,
contextWindow: 200_000
});
}
expect(usageMessages.length).toBe(1);
expect(usageMessages[0]).toMatchObject({
inputTokens: 0,
outputTokens: 0,
contextTokens: 4_200,
contextWindow: 200_000
});
});
});
36 changes: 6 additions & 30 deletions cli/src/agent/backends/acp/AcpSdkBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ export class AcpSdkBackend implements AgentBackend {
private responseCompleteResolvers: Array<() => void> = [];
private lastSessionUpdateAt = 0;
private latestUsageUpdate: AcpUsageUpdate | null = null;
private activeOnUpdate: ((msg: AgentMessage) => void) | null = null;

/** Retry configuration for ACP initialization */
private static readonly INIT_RETRY_OPTIONS = {
Expand Down Expand Up @@ -284,7 +283,6 @@ export class AcpSdkBackend implements AgentBackend {
);
this.messageHandler?.drainBuffers();
this.messageHandler = new AcpMessageHandler(onUpdate);
this.activeOnUpdate = onUpdate;
this.isProcessingMessage = true;
this.lastSessionUpdateAt = Date.now();
this.latestUsageUpdate = null;
Expand Down Expand Up @@ -345,7 +343,6 @@ export class AcpSdkBackend implements AgentBackend {
onUpdate({ type: 'turn_complete', stopReason });
}
} finally {
this.activeOnUpdate = null;
this.isProcessingMessage = false;
this.notifyResponseComplete();
}
Expand Down Expand Up @@ -425,7 +422,6 @@ export class AcpSdkBackend implements AgentBackend {
if (!this.transport) return;
this.messageHandler?.drainBuffers();
this.messageHandler = null;
this.activeOnUpdate = null;
this.activeSessionId = null;
this.isProcessingMessage = false;
this.sessionModelsMetadata.clear();
Expand All @@ -450,32 +446,12 @@ export class AcpSdkBackend implements AgentBackend {
if (!isObject(update)) return;
if (asString(update.sessionUpdate) !== ACP_SESSION_UPDATE_TYPES.usageUpdate) return;

const contextTokens = this.asFiniteNumber(update.used) ?? undefined;
const contextWindow = this.asFiniteNumber(update.size) ?? undefined;
const prev = this.latestUsageUpdate;
const changed = !prev
|| prev.contextTokens !== contextTokens
|| prev.contextWindow !== contextWindow;
this.latestUsageUpdate = { contextTokens, contextWindow };

// Surface context updates mid-turn so the web status bar shows live
// ctx N/M (X%) instead of staying blank until the final prompt usage
// arrives. ACP usage_update only carries context tokens, so I/O is
// sent as 0; the final prompt-finalize emit overwrites with the real
// input/output totals.
if (
changed
&& this.activeOnUpdate
&& (contextTokens !== undefined || contextWindow !== undefined)
) {
this.activeOnUpdate({
type: 'usage',
inputTokens: 0,
outputTokens: 0,
contextTokens,
contextWindow
});
}
const contextTokens = this.asFiniteNumber(update.used);
const contextWindow = this.asFiniteNumber(update.size);
this.latestUsageUpdate = {
contextTokens: contextTokens ?? undefined,
contextWindow: contextWindow ?? undefined
};
}

private readLatestUsageUpdate(): AcpUsageUpdate | null {
Expand Down
Loading