feat(agent): add provider options passthrough, tool call records, and completion validation retries

This commit is contained in:
2026-05-07 10:26:45 +00:00
parent 0dde716109
commit b08cb3689e
10 changed files with 901 additions and 603 deletions
+109 -9
View File
@@ -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;