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
39 changes: 37 additions & 2 deletions packages/core/src/agents/agent-transcript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,43 @@ describe('agent-transcript', () => {
cleanup();
});

it('drops usage-only ROUND_TEXT to keep the canonical view valid', () => {
it('writes usage-only ROUND_TEXT with a model message for exporters', () => {
const jsonlPath = path.join(tempDir, 's', 'agent-x.jsonl');
const { emitter, cleanup } = makeWriter(jsonlPath);

emitter.emit(AgentEventType.ROUND_TEXT, {
subagentId: 'agent-x',
runId: 'run-1',
round: 1,
text: '',
thoughtText: '',
usageMetadata: {
promptTokenCount: 100,
candidatesTokenCount: 20,
cachedContentTokenCount: 40,
totalTokenCount: 120,
},
timestamp: Date.now(),
});
cleanup();

const records = readJsonl(jsonlPath);
expect(records).toHaveLength(1);
expect(records[0]).toMatchObject({
type: 'assistant',
message: { role: 'model', parts: [] },
usageMetadata: {
promptTokenCount: 100,
candidatesTokenCount: 20,
cachedContentTokenCount: 40,
totalTokenCount: 120,
},
agentRunId: 'run-1',
agentRound: 1,
});
});

it('drops ROUND_TEXT with no text, thought, or usage', () => {
const jsonlPath = path.join(tempDir, 's', 'agent-x.jsonl');
const { emitter, cleanup } = makeWriter(jsonlPath);

Expand All @@ -418,7 +454,6 @@ describe('agent-transcript', () => {
round: 1,
text: '',
thoughtText: '',
usageMetadata: { totalTokenCount: 42 },
timestamp: Date.now(),
});
cleanup();
Expand Down
18 changes: 8 additions & 10 deletions packages/core/src/agents/agent-transcript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,18 +458,16 @@ export function attachJsonlTranscriptWriter(
};

const onRoundText = (event: AgentRoundTextEvent) => {
if (!event.text && !event.thoughtText) return;
const parts = [
...(event.thoughtText
? [{ text: event.thoughtText, thought: true }]
: []),
...(event.text ? [{ text: event.text }] : []),
];
if (parts.length === 0 && !event.usageMetadata) return;
Comment thread
DragonnZhang marked this conversation as resolved.
append({
...baseFields('assistant'),
message: {
role: 'model',
parts: [
...(event.thoughtText
? [{ text: event.thoughtText, thought: true }]
: []),
...(event.text ? [{ text: event.text }] : []),
],
},
message: { role: 'model', parts },
usageMetadata: event.usageMetadata,
agentRunId: event.runId ?? streamRunId,
agentRound: event.round,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/agents/runtime/agent-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1002,7 +1002,7 @@ export class AgentCore {
break;
}

if (roundText || roundThoughtText) {
if (roundText || roundThoughtText || lastUsage) {
this.eventEmitter?.emit(AgentEventType.ROUND_TEXT, {
subagentId: this.subagentId,
runId,
Expand Down
54 changes: 54 additions & 0 deletions packages/core/src/agents/runtime/agent-headless.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
import {
AgentEventEmitter,
AgentEventType,
type AgentRoundTextEvent,
type AgentStreamTextEvent,
type AgentToolCallEvent,
type AgentToolResultEvent,
Expand Down Expand Up @@ -2090,6 +2091,59 @@ describe('subagent.ts', () => {
expect(events[1]!.thought).toBe(false);
});

it('should emit usage for a tool-call-only model round', async () => {
const { config } = await createMockConfig();
const usageMetadata = {
promptTokenCount: 100,
candidatesTokenCount: 10,
cachedContentTokenCount: 5,
totalTokenCount: 110,
};
mockSendMessageStream.mockImplementation(async () =>
(async function* () {
yield {
type: 'chunk',
value: {
functionCalls: [
{
id: 'call-1',
name: 'missing_tool',
args: {},
},
],
usageMetadata,
},
};
})(),
);

const eventEmitter = new AgentEventEmitter();
const events: AgentRoundTextEvent[] = [];
eventEmitter.on(AgentEventType.ROUND_TEXT, (event) => {
events.push(event);
});
const scope = await AgentHeadless.create(
'test-agent',
config,
promptConfig,
defaultModelConfig,
{ ...defaultRunConfig, max_turns: 1 },
undefined,
eventEmitter,
);

await scope.execute(new ContextState());

expect(events).toEqual([
expect.objectContaining({
round: 1,
text: '',
thoughtText: '',
usageMetadata,
}),
]);
});

it('should exclude thought text from finalText', async () => {
const { config } = await createMockConfig();

Expand Down
Loading