diff --git a/.changeset/durable-agent-stream-result-totalusage.md b/.changeset/durable-agent-stream-result-totalusage.md new file mode 100644 index 0000000000..e72d4d9014 --- /dev/null +++ b/.changeset/durable-agent-stream-result-totalusage.md @@ -0,0 +1,5 @@ +--- +"@workflow/ai": minor +--- + +Expose `totalUsage` and `finishReason` on the `DurableAgent.stream()` result, mirroring the AI SDK's `GenerateTextResult`/`StreamTextResult` and the existing `onFinish` event payload. diff --git a/packages/ai/src/agent/durable-agent-compat.test.ts b/packages/ai/src/agent/durable-agent-compat.test.ts index 9c9b95c2a0..a5207a7ff8 100644 --- a/packages/ai/src/agent/durable-agent-compat.test.ts +++ b/packages/ai/src/agent/durable-agent-compat.test.ts @@ -1348,6 +1348,30 @@ describe('DurableAgent (ToolLoopAgent compat)', () => { } `); }); + + it('should expose finishReason and totalUsage on the stream result', async () => { + const agent = new DurableAgent({ + model: asModelFactory(mockModel), + }); + + const { writable } = createMockWritable(); + const result = await agent.stream({ + messages: [{ role: 'user' as const, content: 'test' }], + writable, + }); + + expect({ + finishReason: result.finishReason, + inputTokens: result.totalUsage.inputTokens, + outputTokens: result.totalUsage.outputTokens, + }).toMatchInlineSnapshot(` + { + "finishReason": "stop", + "inputTokens": 3, + "outputTokens": 10, + } + `); + }); }); }); diff --git a/packages/ai/src/agent/durable-agent.ts b/packages/ai/src/agent/durable-agent.ts index a84d7214dc..0727a7d8fc 100644 --- a/packages/ai/src/agent/durable-agent.ts +++ b/packages/ai/src/agent/durable-agent.ts @@ -735,6 +735,16 @@ export interface DurableAgentStreamResult< */ toolResults: ToolResult[]; + /** + * The finish reason from the last step. + */ + finishReason: FinishReason; + + /** + * The total token usage across all steps. + */ + totalUsage: LanguageModelUsage; + /** * The generated structured output. It uses the `experimental_output` specification. * Only available when `experimental_output` is specified. @@ -980,6 +990,8 @@ export class DurableAgent { steps, toolCalls: [], toolResults: [], + finishReason: 'other', + totalUsage: aggregateUsage(steps), experimental_output: undefined as OUTPUT, uiMessages: undefined, }; @@ -1168,15 +1180,17 @@ export class DurableAgent { // resolved tool results), so callers can resume the conversation. // Cast matches the existing pattern used at the end of stream(). const messages = iterMessages as unknown as ModelMessage[]; + const lastStep = steps[steps.length - 1]; + const totalUsage = aggregateUsage(steps); + const finishReason = lastStep?.finishReason ?? 'other'; if (mergedOnFinish && !wasAborted) { - const lastStep = steps[steps.length - 1]; await mergedOnFinish({ steps, messages, text: lastStep?.text ?? '', - finishReason: lastStep?.finishReason ?? 'other', - totalUsage: aggregateUsage(steps), + finishReason, + totalUsage, experimental_context: experimentalContext, experimental_output: undefined as OUTPUT, }); @@ -1191,6 +1205,8 @@ export class DurableAgent { steps, toolCalls: allToolCalls, toolResults: allToolResults, + finishReason, + totalUsage, experimental_output: undefined as OUTPUT, uiMessages, }; @@ -1328,15 +1344,18 @@ export class DurableAgent { } } + const lastStep = steps[steps.length - 1]; + const totalUsage = aggregateUsage(steps); + const finishReason = lastStep?.finishReason ?? 'other'; + // Call onFinish callback if provided (always call, even on errors, but not on abort) if (mergedOnFinish && !wasAborted) { - const lastStep = steps[steps.length - 1]; await mergedOnFinish({ steps, messages: messages as ModelMessage[], text: lastStep?.text ?? '', - finishReason: lastStep?.finishReason ?? 'other', - totalUsage: aggregateUsage(steps), + finishReason, + totalUsage, experimental_context: experimentalContext, experimental_output: experimentalOutput, }); @@ -1358,6 +1377,8 @@ export class DurableAgent { steps, toolCalls: lastStepToolCalls, toolResults: lastStepToolResults, + finishReason, + totalUsage, experimental_output: experimentalOutput, uiMessages, };