Feat/a2a expose usage metadata (#27288)

This commit is contained in:
jvargassanchez-dot
2026-05-22 11:51:39 -06:00
committed by GitHub
parent ba04e99bea
commit d1fa323cfb
2 changed files with 81 additions and 0 deletions
@@ -294,6 +294,66 @@ describe('Task', () => {
]); ]);
}); });
it('should capture usageMetadata on Finished event and include it in final status update', async () => {
const mockConfig = createMockConfig();
const mockEventBus: ExecutionEventBus = {
publish: vi.fn(),
on: vi.fn(),
off: vi.fn(),
once: vi.fn(),
removeAllListeners: vi.fn(),
finished: vi.fn(),
};
// @ts-expect-error - Calling private constructor for test purposes.
const task = new Task(
'task-id',
'context-id',
mockConfig as Config,
mockEventBus,
);
const finishedEvent = {
type: GeminiEventType.Finished,
value: {
reason: 'STOP',
usageMetadata: {
promptTokenCount: 100,
candidatesTokenCount: 50,
totalTokenCount: 150,
},
},
};
await task.acceptAgentMessage(finishedEvent);
expect(task.usageMetadata).toEqual({
promptTokenCount: 100,
candidatesTokenCount: 50,
totalTokenCount: 150,
});
task.setTaskStateAndPublishUpdate(
'input-required',
{ kind: CoderAgentEvent.StateChangeEvent },
undefined,
undefined,
true, // final
);
expect(mockEventBus.publish).toHaveBeenCalledWith(
expect.objectContaining({
final: true,
metadata: expect.objectContaining({
usageMetadata: {
promptTokenCount: 100,
candidatesTokenCount: 50,
totalTokenCount: 150,
},
}),
}),
);
});
it('should update modelInfo and reflect it in metadata and status updates', async () => { it('should update modelInfo and reflect it in metadata and status updates', async () => {
const mockConfig = createMockConfig(); const mockConfig = createMockConfig();
const mockEventBus: ExecutionEventBus = { const mockEventBus: ExecutionEventBus = {
+21
View File
@@ -89,6 +89,12 @@ export class Task {
currentAgentMessageId = uuidv4(); currentAgentMessageId = uuidv4();
promptCount = 0; promptCount = 0;
autoExecute: boolean; autoExecute: boolean;
usageMetadata?: {
promptTokenCount?: number;
candidatesTokenCount?: number;
totalTokenCount?: number;
cachedContentTokenCount?: number;
};
private get isYoloMatch(): boolean { private get isYoloMatch(): boolean {
return ( return (
this.autoExecute || this.config.getApprovalMode() === ApprovalMode.YOLO this.autoExecute || this.config.getApprovalMode() === ApprovalMode.YOLO
@@ -274,6 +280,7 @@ export class Task {
userTier?: UserTierId; userTier?: UserTierId;
error?: string; error?: string;
traceId?: string; traceId?: string;
usageMetadata?: Task['usageMetadata'];
} = { } = {
coderAgent: coderAgentMessage, coderAgent: coderAgentMessage,
model: this.modelInfo || this.config.getModel(), model: this.modelInfo || this.config.getModel(),
@@ -288,6 +295,10 @@ export class Task {
metadata.traceId = traceId; metadata.traceId = traceId;
} }
if (final && this.usageMetadata) {
metadata.usageMetadata = this.usageMetadata;
}
return { return {
kind: 'status-update', kind: 'status-update',
taskId: this.id, taskId: this.id,
@@ -857,8 +868,18 @@ export class Task {
break; break;
case GeminiEventType.Finished: case GeminiEventType.Finished:
logger.info(`[Task ${this.id}] Agent finished its turn.`); logger.info(`[Task ${this.id}] Agent finished its turn.`);
// Capture the usage metadata when the stream finishes
if (
event.value &&
typeof event.value === 'object' &&
'usageMetadata' in event.value
) {
this.usageMetadata = event.value
.usageMetadata as typeof this.usageMetadata;
}
break; break;
case GeminiEventType.ModelInfo: case GeminiEventType.ModelInfo:
this.usageMetadata = undefined;
this.modelInfo = event.value; this.modelInfo = event.value;
break; break;
case GeminiEventType.Retry: case GeminiEventType.Retry: