feat(agent): add provider options passthrough, tool call records, and completion validation retries
This commit is contained in:
+153
@@ -1,8 +1,56 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { MockLanguageModelV3, convertArrayToReadableStream } from 'ai/test';
|
||||
import * as smartagent from '../ts/index.js';
|
||||
import { filesystemTool, shellTool, httpTool, jsonTool, truncateOutput } from '../ts_tools/index.js';
|
||||
import { compactMessages } from '../ts_compaction/index.js';
|
||||
|
||||
const createUsage = (inputTokens: number, outputTokens: number) => ({
|
||||
inputTokens: {
|
||||
total: inputTokens,
|
||||
noCache: inputTokens,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
outputTokens: {
|
||||
total: outputTokens,
|
||||
text: outputTokens,
|
||||
reasoning: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const createTextStreamResult = (text: string) => ({
|
||||
stream: convertArrayToReadableStream([
|
||||
{ type: 'stream-start', warnings: [] },
|
||||
{ type: 'response-metadata', id: 'response-1', timestamp: new Date(0), modelId: 'mock-model' },
|
||||
{ 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(1, 1),
|
||||
},
|
||||
] as any[]),
|
||||
});
|
||||
|
||||
const createToolCallStreamResult = (toolName: string, input: unknown) => ({
|
||||
stream: convertArrayToReadableStream([
|
||||
{ type: 'stream-start', warnings: [] },
|
||||
{ type: 'response-metadata', id: 'response-1', timestamp: new Date(0), modelId: 'mock-model' },
|
||||
{
|
||||
type: 'tool-call',
|
||||
toolCallId: 'tool-call-1',
|
||||
toolName,
|
||||
input: JSON.stringify(input),
|
||||
},
|
||||
{
|
||||
type: 'finish',
|
||||
finishReason: { unified: 'tool-calls', raw: 'tool-calls' },
|
||||
usage: createUsage(2, 1),
|
||||
},
|
||||
] as any[]),
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Core exports
|
||||
// ============================================================
|
||||
@@ -35,6 +83,111 @@ tap.test('should re-export stepCountIs', async () => {
|
||||
expect(smartagent.stepCountIs).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
tap.test('runAgent should forward providerOptions to streamText', async () => {
|
||||
const model = new MockLanguageModelV3({
|
||||
doStream: async () => createTextStreamResult('ok') as any,
|
||||
});
|
||||
|
||||
const providerOptions = {
|
||||
openai: {
|
||||
reasoningEffort: 'xhigh',
|
||||
},
|
||||
} as const;
|
||||
|
||||
const result = await smartagent.runAgent({
|
||||
model,
|
||||
prompt: 'hello',
|
||||
providerOptions,
|
||||
});
|
||||
|
||||
expect(result.text).toEqual('ok');
|
||||
expect((model.doStreamCalls[0].providerOptions as any).openai.reasoningEffort).toEqual('xhigh');
|
||||
});
|
||||
|
||||
tap.test('runAgent should return final tool call records', async () => {
|
||||
let streamCallCount = 0;
|
||||
const callbackToolCalls: Array<{ name: string; input: unknown }> = [];
|
||||
const callbackToolResults: Array<{ name: string; result: unknown }> = [];
|
||||
const model = new MockLanguageModelV3({
|
||||
doStream: async () => {
|
||||
streamCallCount++;
|
||||
return streamCallCount === 1
|
||||
? createToolCallStreamResult('echo', { text: 'hello' }) as any
|
||||
: createTextStreamResult('saved') as any;
|
||||
},
|
||||
});
|
||||
|
||||
const result = await smartagent.runAgent({
|
||||
model,
|
||||
prompt: 'echo hello',
|
||||
tools: {
|
||||
echo: smartagent.tool({
|
||||
description: 'Echo text',
|
||||
inputSchema: smartagent.z.object({ text: smartagent.z.string() }),
|
||||
execute: async ({ text }: { text: string }) => `saved:${text}`,
|
||||
}),
|
||||
},
|
||||
maxSteps: 5,
|
||||
onToolCall: (name, input) => callbackToolCalls.push({ name, input }),
|
||||
onToolResult: (name, result) => callbackToolResults.push({ name, result }),
|
||||
});
|
||||
|
||||
const echoCall = result.toolCalls.find((toolCall) => toolCall.toolName === 'echo');
|
||||
|
||||
expect(result.text).toEqual('saved');
|
||||
expect(echoCall).toBeTruthy();
|
||||
expect(echoCall!.input).toEqual({ text: 'hello' });
|
||||
expect(echoCall!.output).toEqual('saved:hello');
|
||||
expect(callbackToolCalls[0]).toEqual({ name: 'echo', input: { text: 'hello' } });
|
||||
expect(callbackToolResults[0]).toEqual({ name: 'echo', result: 'saved:hello' });
|
||||
});
|
||||
|
||||
tap.test('runAgent should reprompt when validateCompletion returns a string', async () => {
|
||||
let streamCallCount = 0;
|
||||
let validationCallCount = 0;
|
||||
const model = new MockLanguageModelV3({
|
||||
doStream: async () => {
|
||||
streamCallCount++;
|
||||
return createTextStreamResult(streamCallCount === 1 ? 'incomplete' : 'complete') as any;
|
||||
},
|
||||
});
|
||||
|
||||
const result = await smartagent.runAgent({
|
||||
model,
|
||||
prompt: 'process document',
|
||||
maxValidationRetries: 1,
|
||||
validateCompletion: (runResult) => {
|
||||
validationCallCount++;
|
||||
return runResult.text === 'complete' ? undefined : 'Call a save tool before finalizing.';
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.text).toEqual('complete');
|
||||
expect(validationCallCount).toEqual(2);
|
||||
expect(model.doStreamCalls.length).toEqual(2);
|
||||
expect(JSON.stringify(model.doStreamCalls[1].prompt)).toInclude('Call a save tool before finalizing.');
|
||||
});
|
||||
|
||||
tap.test('runAgent should reject when validation retries are exhausted', async () => {
|
||||
let threw = false;
|
||||
const model = new MockLanguageModelV3({
|
||||
doStream: async () => createTextStreamResult('incomplete') as any,
|
||||
});
|
||||
|
||||
try {
|
||||
await smartagent.runAgent({
|
||||
model,
|
||||
prompt: 'process document',
|
||||
validateCompletion: () => 'Missing required save tool call.',
|
||||
});
|
||||
} catch (error) {
|
||||
threw = true;
|
||||
expect((error as Error).message).toInclude('Missing required save tool call.');
|
||||
}
|
||||
|
||||
expect(threw).toBeTrue();
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// ToolRegistry
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user