Compare commits

...

2 Commits

Author SHA1 Message Date
jkunz f183bf19ac v3.4.0
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-05-14 22:44:10 +00:00
jkunz 6fb2b3a61f feat(agent): add streamed reasoning summary callbacks to runAgent 2026-05-14 22:44:08 +00:00
7 changed files with 100 additions and 5 deletions
+11
View File
@@ -5,6 +5,17 @@
## 2026-05-14 - 3.4.0
### 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartagent",
"version": "3.3.0",
"version": "3.4.0",
"private": false,
"description": "Agentic loop for ai-sdk (Vercel AI SDK). Wraps streamText with stopWhen for parallel multi-step tool execution. Built on @push.rocks/smartai.",
"main": "dist_ts/index.js",
+4 -1
View File
@@ -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 |
+45
View File
@@ -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',
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartagent',
version: '3.3.0',
version: '3.4.0',
description: 'Agentic loop for ai-sdk (Vercel AI SDK). Wraps streamText with stopWhen for parallel multi-step tool execution. Built on @push.rocks/smartai.'
}
+32 -2
View File
@@ -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
+6
View File
@@ -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 */