feat(agent): add provider options passthrough, tool call records, and completion validation retries
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartagent',
|
||||
version: '3.0.3',
|
||||
version: '3.1.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.'
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ export { ToolRegistry } from './smartagent.classes.toolregistry.js';
|
||||
export { truncateOutput } from './smartagent.utils.truncation.js';
|
||||
export type { ITruncateResult } from './smartagent.utils.truncation.js';
|
||||
export { ContextOverflowError } from './smartagent.interfaces.js';
|
||||
export type { IAgentRunOptions, IAgentRunResult } from './smartagent.interfaces.js';
|
||||
export type { IAgentRunOptions, IAgentRunResult, IAgentToolCallRecord, ProviderOptions } from './smartagent.interfaces.js';
|
||||
|
||||
// Re-export tool() and z so consumers can define tools without extra imports
|
||||
export { tool, jsonSchema } from '@push.rocks/smartai';
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ import { tool, jsonSchema } from '@push.rocks/smartai';
|
||||
|
||||
export { tool, jsonSchema };
|
||||
|
||||
export type { LanguageModelV3 } from '@push.rocks/smartai';
|
||||
export type { LanguageModelV3, TSmartAiProviderOptions as ProviderOptions } from '@push.rocks/smartai';
|
||||
|
||||
// zod
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Retry backoff and context overflow logic derived from opencode (MIT) — https://github.com/sst/opencode
|
||||
|
||||
import * as plugins from './plugins.js';
|
||||
import type { IAgentRunOptions, IAgentRunResult } from './smartagent.interfaces.js';
|
||||
import type { IAgentRunOptions, IAgentRunResult, IAgentToolCallRecord } from './smartagent.interfaces.js';
|
||||
import { ContextOverflowError } from './smartagent.interfaces.js';
|
||||
|
||||
// Retry constants
|
||||
@@ -76,11 +76,62 @@ function isContextOverflow(err: unknown): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function parseToolInput(input: unknown): unknown {
|
||||
if (typeof input !== 'string') return input;
|
||||
try {
|
||||
return JSON.parse(input);
|
||||
} catch {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
function errorToString(error: unknown): string {
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
}
|
||||
|
||||
function recordToolCall(
|
||||
toolCalls: IAgentToolCallRecord[],
|
||||
toolCallIndexes: Map<string, number>,
|
||||
toolCall: unknown,
|
||||
update: { output?: unknown; error?: unknown } = {},
|
||||
): void {
|
||||
const call = toolCall as any;
|
||||
const toolCallId = call?.toolCallId;
|
||||
const nextRecord: IAgentToolCallRecord = {
|
||||
toolName: String(call?.toolName ?? ''),
|
||||
input: parseToolInput(call?.input ?? call?.args),
|
||||
};
|
||||
const hasOutput = Object.prototype.hasOwnProperty.call(update, 'output');
|
||||
const hasError = Object.prototype.hasOwnProperty.call(update, 'error');
|
||||
|
||||
if (hasOutput) nextRecord.output = update.output;
|
||||
if (hasError && update.error !== undefined) nextRecord.error = errorToString(update.error);
|
||||
|
||||
const existingIndex = typeof toolCallId === 'string' ? toolCallIndexes.get(toolCallId) : undefined;
|
||||
if (existingIndex !== undefined) {
|
||||
const existingRecord = toolCalls[existingIndex];
|
||||
existingRecord.toolName = nextRecord.toolName || existingRecord.toolName;
|
||||
if (nextRecord.input !== undefined) existingRecord.input = nextRecord.input;
|
||||
if (hasOutput) existingRecord.output = nextRecord.output;
|
||||
if (nextRecord.error !== undefined) existingRecord.error = nextRecord.error;
|
||||
return;
|
||||
}
|
||||
|
||||
toolCalls.push(nextRecord);
|
||||
if (typeof toolCallId === 'string') {
|
||||
toolCallIndexes.set(toolCallId, toolCalls.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
export async function runAgent(options: IAgentRunOptions): Promise<IAgentRunResult> {
|
||||
let stepCount = 0;
|
||||
let attempt = 0;
|
||||
let totalInput = 0;
|
||||
let totalOutput = 0;
|
||||
let validationRetries = 0;
|
||||
const toolCalls: IAgentToolCallRecord[] = [];
|
||||
const toolCallIndexes = new Map<string, number>();
|
||||
|
||||
const tools = options.tools ?? {};
|
||||
|
||||
@@ -110,6 +161,7 @@ export async function runAgent(options: IAgentRunOptions): Promise<IAgentRunResu
|
||||
system: options.system,
|
||||
messages,
|
||||
tools: allTools,
|
||||
providerOptions: options.providerOptions,
|
||||
stopWhen: plugins.stepCountIs(options.maxSteps ?? 20),
|
||||
maxRetries: 0, // handled manually below
|
||||
abortSignal: options.abort,
|
||||
@@ -137,20 +189,48 @@ export async function runAgent(options: IAgentRunOptions): Promise<IAgentRunResu
|
||||
|
||||
experimental_onToolCallStart: options.onToolCall
|
||||
? ({ toolCall }) => {
|
||||
options.onToolCall!(toolCall.toolName, (toolCall as any).input ?? (toolCall as any).args);
|
||||
const input = parseToolInput((toolCall as any).input ?? (toolCall as any).args);
|
||||
recordToolCall(toolCalls, toolCallIndexes, toolCall);
|
||||
options.onToolCall!(toolCall.toolName, input);
|
||||
}
|
||||
: undefined,
|
||||
: ({ toolCall }) => {
|
||||
recordToolCall(toolCalls, toolCallIndexes, toolCall);
|
||||
},
|
||||
|
||||
experimental_onToolCallFinish: options.onToolResult
|
||||
? ({ toolCall, output }) => {
|
||||
options.onToolResult!(toolCall.toolName, output);
|
||||
? (event) => {
|
||||
recordToolCall(
|
||||
toolCalls,
|
||||
toolCallIndexes,
|
||||
event.toolCall,
|
||||
event.success ? { output: event.output } : { error: event.error },
|
||||
);
|
||||
options.onToolResult!(event.toolCall.toolName, event.success ? event.output : undefined);
|
||||
}
|
||||
: undefined,
|
||||
: (event) => {
|
||||
recordToolCall(
|
||||
toolCalls,
|
||||
toolCallIndexes,
|
||||
event.toolCall,
|
||||
event.success ? { output: event.output } : { error: event.error },
|
||||
);
|
||||
},
|
||||
|
||||
onStepFinish: ({ usage }) => {
|
||||
onStepFinish: ({ usage, toolCalls: stepToolCalls, toolResults, content }) => {
|
||||
stepCount++;
|
||||
totalInput += usage?.inputTokens ?? 0;
|
||||
totalOutput += usage?.outputTokens ?? 0;
|
||||
for (const toolCall of stepToolCalls) {
|
||||
recordToolCall(toolCalls, toolCallIndexes, toolCall);
|
||||
}
|
||||
for (const toolResult of toolResults) {
|
||||
recordToolCall(toolCalls, toolCallIndexes, toolResult, { output: (toolResult as any).output });
|
||||
}
|
||||
for (const part of content) {
|
||||
if ((part as any).type === 'tool-error') {
|
||||
recordToolCall(toolCalls, toolCallIndexes, part, { error: (part as any).error });
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -158,12 +238,13 @@ export async function runAgent(options: IAgentRunOptions): Promise<IAgentRunResu
|
||||
const text = await result.text;
|
||||
const finishReason = await result.finishReason;
|
||||
const responseData = await result.response;
|
||||
const responseMessages = responseData.messages as plugins.ModelMessage[];
|
||||
|
||||
attempt = 0; // reset on success
|
||||
|
||||
return {
|
||||
const runResult: IAgentRunResult = {
|
||||
text,
|
||||
messages: responseData.messages as plugins.ModelMessage[],
|
||||
messages: responseMessages,
|
||||
steps: stepCount,
|
||||
finishReason,
|
||||
usage: {
|
||||
@@ -171,7 +252,26 @@ export async function runAgent(options: IAgentRunOptions): Promise<IAgentRunResu
|
||||
outputTokens: totalOutput,
|
||||
totalTokens: totalInput + totalOutput,
|
||||
},
|
||||
toolCalls,
|
||||
};
|
||||
|
||||
if (options.validateCompletion) {
|
||||
const validationPrompt = await options.validateCompletion(runResult);
|
||||
if (typeof validationPrompt === 'string') {
|
||||
if (validationRetries >= (options.maxValidationRetries ?? 0)) {
|
||||
throw new Error(`Agent completion validation failed: ${validationPrompt}`);
|
||||
}
|
||||
validationRetries++;
|
||||
messages = [
|
||||
...messages,
|
||||
...responseMessages,
|
||||
{ role: 'user' as const, content: validationPrompt },
|
||||
];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return runResult;
|
||||
} catch (err: unknown) {
|
||||
// Abort — don't retry
|
||||
if (err instanceof DOMException && err.name === 'AbortError') throw err;
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import type { ToolSet, ModelMessage, LanguageModelV3 } from './plugins.js';
|
||||
import type { ToolSet, ModelMessage, LanguageModelV3, ProviderOptions } from './plugins.js';
|
||||
|
||||
export type { ProviderOptions };
|
||||
|
||||
export interface IAgentToolCallRecord {
|
||||
toolName: string;
|
||||
input: unknown;
|
||||
output?: unknown;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface IAgentRunOptions {
|
||||
/** The LanguageModelV3 to use — from smartai.getModel() */
|
||||
@@ -9,6 +18,8 @@ export interface IAgentRunOptions {
|
||||
system?: string;
|
||||
/** Tools available to the agent */
|
||||
tools?: ToolSet;
|
||||
/** Provider-specific AI SDK request options passed through to streamText() */
|
||||
providerOptions?: ProviderOptions;
|
||||
/**
|
||||
* Maximum number of LLM↔tool round trips.
|
||||
* Each step may execute multiple tools in parallel.
|
||||
@@ -23,6 +34,13 @@ export interface IAgentRunOptions {
|
||||
onToolCall?: (toolName: string, input: unknown) => void;
|
||||
/** Called when a tool call completes */
|
||||
onToolResult?: (toolName: string, result: unknown) => void;
|
||||
/**
|
||||
* Validate the completed run. Return a string to reject the run and reprompt,
|
||||
* or return void to accept the result.
|
||||
*/
|
||||
validateCompletion?: (result: IAgentRunResult) => Promise<string | void> | string | void;
|
||||
/** Number of validation-triggered reprompts allowed. Default: 0 */
|
||||
maxValidationRetries?: number;
|
||||
/**
|
||||
* Called when total token usage approaches the model's context limit.
|
||||
* Receives the full message history and must return a compacted replacement.
|
||||
@@ -44,6 +62,8 @@ export interface IAgentRunResult {
|
||||
finishReason: string;
|
||||
/** Accumulated token usage across all steps */
|
||||
usage: { inputTokens: number; outputTokens: number; totalTokens: number };
|
||||
/** Tool calls observed during the run, including inputs and outputs/errors when available */
|
||||
toolCalls: IAgentToolCallRecord[];
|
||||
}
|
||||
|
||||
export class ContextOverflowError extends Error {
|
||||
|
||||
Reference in New Issue
Block a user