Skip to content

Instantly share code, notes, and snippets.

@eersnington
Created November 22, 2025 07:24
Show Gist options
  • Select an option

  • Save eersnington/5834afe0488a781349e5eb67c2996b98 to your computer and use it in GitHub Desktop.

Select an option

Save eersnington/5834afe0488a781349e5eb67c2996b98 to your computer and use it in GitHub Desktop.
DurableAgent Feature Completion Specification

DurableAgent Feature Completion Specification

1. Objective

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.

2. Reference Architecture

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.

3. Proposed Interface Changes

3.1. DurableAgentOptions

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
}

3.2. DurableAgentStreamOptions

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;
}

4. Implementation Details

4.1. DurableAgent Class Update

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 ...
  }
}

4.2. streamTextIterator Update

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 });
    }
  }
}

4.3. doStreamStep Update

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 ...
}

5. Implementing generate() (Non-streaming)

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
  };
}

6. Implementation Steps

  1. Update packages/ai/src/agent/durable-agent.ts:
    • Rename and expand interfaces.
    • Update constructor to store options.
    • Update stream to pass new options.
  2. Update packages/ai/src/agent/stream-text-iterator.ts:
    • Accept new options in arguments.
    • Pass them to doStreamStep.
    • Implement onStepFinish.
  3. Update packages/ai/src/agent/do-stream-step.ts:
    • Accept new options.
    • Map DurableAgentOptions -> LanguageModelV2CallOptions (e.g., maxOutputTokens -> maxTokens).
    • Handle providerOptions extraction logic.
  4. Verification:
    • Verify providerOptions are passed as headers.
    • Verify temperature affects output (via mock or test).
    • Verify onStepFinish is called.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment