Bring @workflow/ai's DurableAgent to feature parity with Vercel AI SDK's ToolLoopAgent. This involves adding configuration for generation parameters, provider-specific options, observability hooks, and implementing the missing generate() method.
We are modeling this after ToolLoopAgent from ai.
Reference Key Features:
- Generation Control:
temperature,maxOutputTokens,topP,topK,seed, etc. - Tool Control:
toolChoice,activeTools,maxRetries. - Observability:
onStepFinish,experimental_telemetry. - Infrastructure:
providerOptions(headers/api-keys),abortSignal.
Location: packages/ai/src/agent/durable-agent.ts
We will expand DurableAgentOptions to include all ToolLoopAgent settings.
import type {
LanguageModelV2,
LogLevel,
TelemetrySettings, // from 'ai'
Output, // from 'ai'
} from 'ai';
export interface DurableAgentOptions {
// --- Core ---
model: string | (() => Promise<LanguageModelV2>);
tools?: ToolSet;
system?: string;
// --- Generation Control ---
maxOutputTokens?: number;
temperature?: number;
topP?: number;
topK?: number;
presencePenalty?: number;
frequencyPenalty?: number;
stopSequences?: string[];
seed?: number;
// --- Tool & Loop Control ---
maxRetries?: number; // Default: 2
toolChoice?: 'auto' | 'none' | 'required' | { type: 'tool'; toolName: string };
activeTools?: Array<string>;
stopWhen?: StopCondition<NoInfer<ToolSet>> | Array<StopCondition<NoInfer<ToolSet>>>;
// --- Infrastructure ---
abortSignal?: AbortSignal;
providerOptions?: Record<string, Record<string, any>>; // e.g. { openai: { headers: ... } }
// --- Observability ---
experimental_telemetry?: TelemetrySettings;
onStepFinish?: (event: {
step: any; // Will map to StepResult
// Add other relevant context if needed
}) => void | Promise<void>;
// --- Output (Future Support) ---
// output?: Output; // To be implemented for structured output support
}Location: packages/ai/src/agent/durable-agent.ts
Allow runtime overrides for specific parameters, consistent with AI SDK patterns.
export interface DurableAgentStreamOptions {
messages: ModelMessage[];
writable: WritableStream<UIMessageChunk>;
// Runtime Overrides
system?: string;
abortSignal?: AbortSignal;
stopWhen?: StopCondition<NoInfer<ToolSet>> | Array<StopCondition<NoInfer<ToolSet>>>;
// Durable Specifics
preventClose?: boolean;
sendStart?: boolean;
sendFinish?: boolean;
}The class must hold these options and pass them down.
// packages/ai/src/agent/durable-agent.ts
export class DurableAgent {
private options: DurableAgentOptions;
constructor(options: DurableAgentOptions) {
this.options = options;
// ... validation if needed
}
async stream(options: DurableAgentStreamOptions) {
// ... prepare prompt ...
const iterator = streamTextIterator({
// Core
model: this.options.model,
tools: this.options.tools || {},
writable: options.writable,
prompt: modelPrompt,
// Control & Overrides
stopConditions: options.stopWhen ?? this.options.stopWhen,
abortSignal: options.abortSignal ?? this.options.abortSignal,
// Pass all generation options from this.options
maxOutputTokens: this.options.maxOutputTokens,
temperature: this.options.temperature,
topP: this.options.topP,
topK: this.options.topK,
providerOptions: this.options.providerOptions,
toolChoice: this.options.toolChoice,
// ... etc
});
// ... existing loop logic ...
}
}Location: packages/ai/src/agent/stream-text-iterator.ts
This function acts as the orchestrator. It needs to receive the configuration and pass it to doStreamStep.
export async function* streamTextIterator({
// ... existing args
maxOutputTokens,
temperature,
providerOptions,
toolChoice,
// ... new args
}: {
// ... types
}) {
// ... setup ...
while (!done) {
const { toolCalls, finish, step } = await doStreamStep(
conversationPrompt,
model,
writable,
toolsToModelTools(tools), // Note: need to handle activeTools here if implemented
{
sendStart: sendStart && isFirstIteration,
// Pass Generation Config
maxOutputTokens,
temperature,
providerOptions,
toolChoice,
}
);
// ... handle tool execution ...
// Handle onStepFinish
if (onStepFinish) {
await onStepFinish({ step });
}
}
}Location: packages/ai/src/agent/do-stream-step.ts
This is the low-level bridge to LanguageModelV2. It must map our options to the LanguageModelV2.doStream call.
export async function doStreamStep(
// ... args
options?: {
// ... existing
maxOutputTokens?: number;
temperature?: number;
providerOptions?: Record<string, any>;
toolChoice?: any;
}
) {
// ... model init ...
// Map our options to LanguageModelV2CallOptions
const result = await model.doStream({
prompt: conversationPrompt,
tools,
// Generation Settings
maxTokens: options?.maxOutputTokens, // Note name mapping: maxOutputTokens -> maxTokens
temperature: options?.temperature,
topP: options?.topP,
topK: options?.topK,
seed: options?.seed,
// Provider Settings
headers: options?.providerOptions?.[model.provider]?.headers, // Extract headers for specific provider
// Tool Choice
toolChoice: options?.toolChoice,
});
// ... existing transform streams ...
}The generate() method is currently missing. It should buffer the stream into a single result object.
// packages/ai/src/agent/durable-agent.ts
async generate(options: DurableAgentGenerateOptions): Promise<GenerateTextResult> {
// 1. Create a fake writable stream to capture chunks
const chunks: UIMessageChunk[] = [];
const writable = new WritableStream<UIMessageChunk>({
write(chunk) {
chunks.push(chunk);
}
});
// 2. Re-use the stream logic
await this.stream({
...options,
writable,
preventClose: false
});
// 3. Aggregate chunks into a result object
// (Similar to how AI SDK's generateText aggregates stream output)
const text = chunks
.filter(c => c.type === 'text-delta')
.map(c => c.delta)
.join('');
const toolCalls = chunks
.filter(c => c.type === 'tool-call')
// ... map structure
return {
text,
toolCalls,
// ... full result structure
};
}- Update
packages/ai/src/agent/durable-agent.ts:- Rename and expand interfaces.
- Update constructor to store
options. - Update
streamto pass new options.
- Update
packages/ai/src/agent/stream-text-iterator.ts:- Accept new options in arguments.
- Pass them to
doStreamStep. - Implement
onStepFinish.
- Update
packages/ai/src/agent/do-stream-step.ts:- Accept new options.
- Map
DurableAgentOptions->LanguageModelV2CallOptions(e.g.,maxOutputTokens->maxTokens). - Handle
providerOptionsextraction logic.
- Verification:
- Verify
providerOptionsare passed as headers. - Verify
temperatureaffects output (via mock or test). - Verify
onStepFinishis called.
- Verify