Skip to content

WorkflowAgent.stream(): Output.object responseFormat is dropped in doStreamStep — structured output never enforced #16565

Description

@SPMTCH

Package: @ai-sdk/workflow (packages/workflow in this repo)
Versions affected: @ai-sdk/workflow 1.0.4–1.0.11, with ai 7.0.x. Confirmed on 1.0.11 + ai@7.0.11; the bug has been present since at least 1.0.4 based on reading the history of do-stream-step.ts.

Symptom

WorkflowAgent.stream() called with output: Output.object({ schema }) never actually enforces structured output. The model call that goes out has responseFormat: null regardless of the output spec passed to the agent, and there is no schema-in-prompt fallback either. In practice, with real providers this surfaces as AI_NoObjectGeneratedError: could not parse the response once the model returns prose or a shape that doesn't match the schema, because the model was never told what schema to produce.

For comparison, the same output/schema config used through the non-workflow Experimental_Agent / ToolLoopAgent path in ai correctly receives responseFormat on the model call — so this looks specific to the WorkflowAgent step-boundary plumbing, not a schema-conversion bug.

Minimal repro

import { WorkflowAgent, Output } from '@ai-sdk/workflow';
import { MockLanguageModelV4 } from 'ai/test';
import { z } from 'zod';

const model = new MockLanguageModelV4({
  doStream: async () => ({
    stream: new ReadableStream({
      start(c) {
        c.enqueue({ type: 'text-start', id: '1' });
        c.enqueue({ type: 'text-delta', id: '1', delta: '{"ok":true}' });
        c.enqueue({ type: 'text-end', id: '1' });
        c.enqueue({ type: 'finish', finishReason: 'stop', usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 } });
        c.close();
      },
    }),
  }),
});

const agent = new WorkflowAgent({
  model,
  output: Output.object({ schema: z.object({ ok: z.boolean() }) }),
});

await agent.stream({ messages: [{ role: 'user', content: 'go' }] });

console.log(model.doStreamCalls[0]?.responseFormat);
// Actual:   null
// Expected: { type: 'json', schema: <json-schema for z.object({ ok: z.boolean() })> }

Root cause

responseFormat is computed correctly and threaded most of the way through, then dropped at the last hop:

  1. WorkflowAgent's stream path computes it from the output spec: packages/workflow/src/workflow-agent.ts:2093
    responseFormat: await (options.output ?? this.output)?.responseFormat,
  2. It's forwarded correctly into the options passed to doStreamStep: packages/workflow/src/stream-text-iterator.ts:358 (inside the options object built for the doStreamStep(...) call at line 348).
  3. doStreamStep's own option type even declares it: packages/workflow/src/do-stream-step.ts:59
    responseFormat?: LanguageModelV4CallOptions['responseFormat'];
  4. But the streamModelCall({...}) call inside doStreamStep (packages/workflow/src/do-stream-step.ts:125-148) never includes it. toolChoice, headers, and reasoning are all forwarded from options, responseFormat is not:
    const { stream: modelStream } = await streamModelCall({
      model,
      messages: conversationPrompt as unknown as ModelMessage[],
      allowSystemInMessages: true,
      tools,
      toolChoice: options?.toolChoice,       // forwarded
      includeRawChunks: options?.includeRawChunks,
      providerOptions: options?.providerOptions,
      abortSignal: options?.abortSignal,
      headers: options?.headers,             // forwarded
      reasoning: options?.reasoning,         // forwarded
      maxOutputTokens: options?.maxOutputTokens,
      ...
      // responseFormat: options?.responseFormat,  <-- missing
    });

Notably, reasoning forwarding on this exact call was added in 1.0.6 for what looks like the same class of gap (changelog: "Forward reasoning generation settings from WorkflowAgent to model calls"). responseFormat needs the same one-line fix.

Suggested fix

In packages/workflow/src/do-stream-step.ts, add responseFormat: options?.responseFormat, to the streamModelCall({...}) call around line 137-138, alongside the existing toolChoice/headers/reasoning forwarding.

Context: found while integrating WorkflowAgent in a production app. Happy to send a one-line PR if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions