feat(agent): add streamed reasoning summary callbacks to runAgent
This commit is contained in:
@@ -5,6 +5,14 @@
|
||||
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
- add streamed reasoning summary callbacks to runAgent (agent)
|
||||
- Introduces onReasoningStart, onReasoningDelta, and onReasoningEnd callbacks in the agent options interface
|
||||
- Handles reasoning-start, reasoning-delta, and reasoning-end stream chunks while accumulating reasoning text by id
|
||||
- Ensures incomplete reasoning streams are finalized after the response completes
|
||||
- Adds tests for reasoning summary streaming and updates the README API documentation
|
||||
|
||||
## 2026-05-14 - 3.3.0
|
||||
|
||||
### Features
|
||||
|
||||
@@ -76,7 +76,7 @@ console.log(result.usage); // { inputTokens, outputTokens, totalTokens, cacheR
|
||||
- ⚡ **Parallel tool execution** — multiple tool calls in a single step are executed concurrently
|
||||
- 🔧 **Auto-retry with backoff** — handles 429/529/503 errors with header-aware retry delays
|
||||
- 🩹 **Tool call repair** — case-insensitive name matching + invalid tool sink prevents crashes
|
||||
- 📊 **Token streaming** — `onToken` and `onToolCall` callbacks for real-time progress
|
||||
- 📊 **Token and reasoning streaming** — `onToken`, `onReasoning*`, and `onToolCall` callbacks for real-time progress
|
||||
- 💥 **Context overflow handling** — detects overflow and invokes your `onContextOverflow` callback
|
||||
|
||||
## Core API
|
||||
@@ -98,6 +98,9 @@ The single entry point. Options:
|
||||
| `messages` | `ModelMessage[]` | `[]` | Conversation history (for multi-turn) |
|
||||
| `maxRetries` | `number` | `5` | Max retries on rate-limit/server errors |
|
||||
| `onToken` | `(delta: string) => void` | — | Streaming token callback |
|
||||
| `onReasoningStart` | `(id: string) => void` | — | Called when a reasoning summary starts |
|
||||
| `onReasoningDelta` | `(id: string, delta: string) => void` | — | Called for streamed reasoning summary text |
|
||||
| `onReasoningEnd` | `(id: string, text: string) => void` | — | Called when a reasoning summary completes |
|
||||
| `onToolCall` | `(name: string) => void` | — | Called when a tool is invoked |
|
||||
| `onToolResult` | `(name: string, result: unknown) => void` | — | Called when a tool finishes |
|
||||
| `validateCompletion` | `(result) => string \| void` | — | Return a string to reject and reprompt an incomplete run |
|
||||
|
||||
@@ -33,6 +33,25 @@ const createTextStreamResult = (text: string) => ({
|
||||
] as any[]),
|
||||
});
|
||||
|
||||
const createReasoningStreamResult = (reasoning: string, text: string) => ({
|
||||
stream: convertArrayToReadableStream([
|
||||
{ type: 'stream-start', warnings: [] },
|
||||
{ type: 'response-metadata', id: 'response-1', timestamp: new Date(0), modelId: 'mock-model' },
|
||||
{ type: 'reasoning-start', id: 'reasoning-1' },
|
||||
{ type: 'reasoning-delta', id: 'reasoning-1', delta: reasoning.slice(0, 7) },
|
||||
{ type: 'reasoning-delta', id: 'reasoning-1', delta: reasoning.slice(7) },
|
||||
{ type: 'reasoning-end', id: 'reasoning-1' },
|
||||
{ type: 'text-start', id: 'text-1' },
|
||||
{ type: 'text-delta', id: 'text-1', delta: text },
|
||||
{ type: 'text-end', id: 'text-1' },
|
||||
{
|
||||
type: 'finish',
|
||||
finishReason: { unified: 'stop', raw: 'stop' },
|
||||
usage: createUsage(2, 2),
|
||||
},
|
||||
] as any[]),
|
||||
});
|
||||
|
||||
const createToolCallStreamResult = (toolName: string, input: unknown) => ({
|
||||
stream: convertArrayToReadableStream([
|
||||
{ type: 'stream-start', warnings: [] },
|
||||
@@ -131,6 +150,32 @@ tap.test('runAgent should add OpenAI cache defaults when sessionId is provided',
|
||||
expect(openaiOptions.reasoningEffort).toEqual('high');
|
||||
});
|
||||
|
||||
tap.test('runAgent should stream reasoning summary callbacks', async () => {
|
||||
const reasoningEvents: string[] = [];
|
||||
const tokenDeltas: string[] = [];
|
||||
const model = new MockLanguageModelV3({
|
||||
doStream: async () => createReasoningStreamResult('thinking through it', 'done') as any,
|
||||
});
|
||||
|
||||
const result = await smartagent.runAgent({
|
||||
model,
|
||||
prompt: 'hello',
|
||||
onToken: (delta) => tokenDeltas.push(delta),
|
||||
onReasoningStart: (id) => reasoningEvents.push('start:' + id),
|
||||
onReasoningDelta: (id, delta) => reasoningEvents.push('delta:' + id + ':' + delta),
|
||||
onReasoningEnd: (id, text) => reasoningEvents.push('end:' + id + ':' + text),
|
||||
});
|
||||
|
||||
expect(result.text).toEqual('done');
|
||||
expect(tokenDeltas.join('')).toEqual('done');
|
||||
expect(reasoningEvents).toEqual([
|
||||
'start:reasoning-1',
|
||||
'delta:reasoning-1:thinkin',
|
||||
'delta:reasoning-1:g through it',
|
||||
'end:reasoning-1:thinking through it',
|
||||
]);
|
||||
});
|
||||
|
||||
tap.test('runAgent should mark Anthropic prompt cache breakpoints by default', async () => {
|
||||
const model = new MockLanguageModelV3({
|
||||
provider: 'anthropic',
|
||||
|
||||
@@ -156,6 +156,7 @@ export async function runAgent(options: IAgentRunOptions): Promise<IAgentRunResu
|
||||
let validationRetries = 0;
|
||||
const toolCalls: IAgentToolCallRecord[] = [];
|
||||
const toolCallIndexes = new Map<string, number>();
|
||||
const reasoningTextById = new Map<string, string>();
|
||||
|
||||
const tools = options.tools ?? {};
|
||||
const cache = options.cache ?? 'auto';
|
||||
@@ -227,8 +228,33 @@ export async function runAgent(options: IAgentRunOptions): Promise<IAgentRunResu
|
||||
},
|
||||
|
||||
onChunk: ({ chunk }) => {
|
||||
if (chunk.type === 'text-delta' && options.onToken) {
|
||||
options.onToken((chunk as any).textDelta ?? (chunk as any).text ?? '');
|
||||
const chunkType = String((chunk as any).type || '');
|
||||
if (chunkType === 'text-delta' && options.onToken) {
|
||||
options.onToken((chunk as any).delta ?? (chunk as any).textDelta ?? (chunk as any).text ?? '');
|
||||
return;
|
||||
}
|
||||
if (chunkType === 'reasoning-start') {
|
||||
const id = (chunk as any).id || 'reasoning';
|
||||
reasoningTextById.set(id, '');
|
||||
options.onReasoningStart?.(id, (chunk as any).providerMetadata);
|
||||
return;
|
||||
}
|
||||
if (chunkType === 'reasoning-delta') {
|
||||
const id = (chunk as any).id || 'reasoning';
|
||||
const delta = (chunk as any).delta ?? (chunk as any).textDelta ?? (chunk as any).text ?? '';
|
||||
if (!reasoningTextById.has(id)) {
|
||||
reasoningTextById.set(id, '');
|
||||
options.onReasoningStart?.(id, (chunk as any).providerMetadata);
|
||||
}
|
||||
reasoningTextById.set(id, (reasoningTextById.get(id) ?? '') + delta);
|
||||
options.onReasoningDelta?.(id, delta, (chunk as any).providerMetadata);
|
||||
return;
|
||||
}
|
||||
if (chunkType === 'reasoning-end') {
|
||||
const id = (chunk as any).id || 'reasoning';
|
||||
const text = reasoningTextById.get(id) ?? '';
|
||||
reasoningTextById.delete(id);
|
||||
options.onReasoningEnd?.(id, text, (chunk as any).providerMetadata);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -286,6 +312,10 @@ export async function runAgent(options: IAgentRunOptions): Promise<IAgentRunResu
|
||||
const finishReason = await result.finishReason;
|
||||
const responseData = await result.response;
|
||||
const responseMessages = responseData.messages as plugins.ModelMessage[];
|
||||
for (const [id, reasoningText] of reasoningTextById) {
|
||||
options.onReasoningEnd?.(id, reasoningText);
|
||||
reasoningTextById.delete(id);
|
||||
}
|
||||
|
||||
attempt = 0; // reset on success
|
||||
|
||||
|
||||
@@ -45,6 +45,12 @@ export interface IAgentRunOptions {
|
||||
messages?: ModelMessage[];
|
||||
/** Called for each streamed text delta */
|
||||
onToken?: (delta: string) => void;
|
||||
/** Called when the model starts a streamed reasoning summary */
|
||||
onReasoningStart?: (id: string, providerMetadata?: unknown) => void;
|
||||
/** Called for each streamed reasoning summary delta */
|
||||
onReasoningDelta?: (id: string, delta: string, providerMetadata?: unknown) => void;
|
||||
/** Called when a streamed reasoning summary completes */
|
||||
onReasoningEnd?: (id: string, text: string, providerMetadata?: unknown) => void;
|
||||
/** Called when a tool call starts */
|
||||
onToolCall?: (toolName: string, input: unknown) => void;
|
||||
/** Called when a tool call completes */
|
||||
|
||||
Reference in New Issue
Block a user