Skip to content

Commit e06b663

Browse files
authored
feat(agent): support experimental stream transforms (#10504)
## Background Stream transformation such as stream smoothing were not supported when using the agent abstraction. ## Summary Add `experimental_transform` support to `Agent.stream` and related functions. Not implemented as a property on `Agent` because transformations specific to streaming and not available for generate. ## Manual Verification Ran `examples/next-openai` with http://localhost:3000/chat-openai-smooth-stream
1 parent fba51ce commit e06b663

File tree

11 files changed

+139
-9
lines changed

11 files changed

+139
-9
lines changed

.changeset/little-gifts-drive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'ai': patch
3+
---
4+
5+
feat(agent): support experimental stream transforms

content/docs/07-reference/01-ai-sdk-core/16-tool-loop-agent.mdx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,13 @@ for await (const chunk of stream.textStream) {
272272
description:
273273
'An optional abort signal that can be used to cancel the call.',
274274
},
275+
{
276+
name: 'experimental_transform',
277+
type: 'StreamTextTransform | Array<StreamTextTransform>',
278+
isOptional: true,
279+
description:
280+
'Optional stream transformation(s). They are applied in the order provided and must maintain the stream structure. See `streamText` docs for details.',
281+
},
275282
]}
276283
/>
277284

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { openai, OpenAIResponsesProviderOptions } from '@ai-sdk/openai';
2+
import { ToolLoopAgent, InferAgentUIMessage } from 'ai';
3+
4+
export const openaiBasicAgent = new ToolLoopAgent({
5+
model: openai('gpt-5-mini'),
6+
providerOptions: {
7+
openai: {
8+
reasoningEffort: 'medium',
9+
reasoningSummary: 'detailed',
10+
// store: false,
11+
} satisfies OpenAIResponsesProviderOptions,
12+
},
13+
onStepFinish: ({ request }) => {
14+
console.dir(request.body, { depth: Infinity });
15+
},
16+
});
17+
18+
export type OpenAIBasicMessage = InferAgentUIMessage<typeof openaiBasicAgent>;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { openaiBasicAgent } from '@/agent/openai-basic-agent';
2+
import { createAgentUIStreamResponse, smoothStream } from 'ai';
3+
4+
export async function POST(req: Request) {
5+
const { messages } = await req.json();
6+
7+
return createAgentUIStreamResponse({
8+
agent: openaiBasicAgent,
9+
messages,
10+
experimental_transform: smoothStream(),
11+
});
12+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use client';
2+
3+
import { OpenAIBasicMessage } from '@/agent/openai-basic-agent';
4+
import { Response } from '@/components/ai-elements/response';
5+
import ChatInput from '@/components/chat-input';
6+
import { ReasoningView } from '@/components/reasoning-view';
7+
import { useChat } from '@ai-sdk/react';
8+
import { DefaultChatTransport } from 'ai';
9+
10+
export default function TestOpenAISmoothStream() {
11+
const { error, status, sendMessage, messages, regenerate } =
12+
useChat<OpenAIBasicMessage>({
13+
transport: new DefaultChatTransport({
14+
api: '/api/chat-openai-smooth-stream',
15+
}),
16+
});
17+
18+
return (
19+
<div className="flex flex-col py-24 mx-auto w-full max-w-md stretch">
20+
<h1 className="mb-4 text-xl font-bold">OpenAI Smooth Stream</h1>
21+
22+
{messages.map(message => (
23+
<div key={message.id} className="whitespace-pre-wrap">
24+
{message.role === 'user' ? 'User: ' : 'AI: '}
25+
{message.parts.map((part, index) => {
26+
switch (part.type) {
27+
case 'text': {
28+
return <Response key={index}>{part.text}</Response>;
29+
}
30+
case 'reasoning': {
31+
return <ReasoningView part={part} key={index} />;
32+
}
33+
}
34+
})}
35+
</div>
36+
))}
37+
38+
{error && (
39+
<div className="mt-4">
40+
<div className="text-red-500">An error occurred.</div>
41+
<button
42+
type="button"
43+
className="px-4 py-2 mt-4 text-blue-500 rounded-md border border-blue-500"
44+
onClick={() => regenerate()}
45+
>
46+
Retry
47+
</button>
48+
</div>
49+
)}
50+
51+
<ChatInput status={status} onSubmit={text => sendMessage({ text })} />
52+
</div>
53+
);
54+
}

packages/ai/src/agent/agent.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import { ModelMessage } from '@ai-sdk/provider-utils';
22
import { GenerateTextResult } from '../generate-text/generate-text-result';
33
import { Output } from '../generate-text/output';
4+
import { StreamTextTransform } from '../generate-text/stream-text';
45
import { StreamTextResult } from '../generate-text/stream-text-result';
56
import { ToolSet } from '../generate-text/tool-set';
67

8+
/**
9+
* Parameters for calling an agent.
10+
*/
711
export type AgentCallParameters<CALL_OPTIONS> = ([CALL_OPTIONS] extends [never]
812
? { options?: never }
913
: { options: CALL_OPTIONS }) &
@@ -45,6 +49,23 @@ export type AgentCallParameters<CALL_OPTIONS> = ([CALL_OPTIONS] extends [never]
4549
abortSignal?: AbortSignal;
4650
};
4751

52+
/**
53+
* Parameters for streaming an output from an agent.
54+
*/
55+
export type AgentStreamParameters<
56+
CALL_OPTIONS,
57+
TOOLS extends ToolSet,
58+
> = AgentCallParameters<CALL_OPTIONS> & {
59+
/**
60+
* Optional stream transformations.
61+
* They are applied in the order they are provided.
62+
* The stream transformations must maintain the stream structure for streamText to work correctly.
63+
*/
64+
experimental_transform?:
65+
| StreamTextTransform<TOOLS>
66+
| Array<StreamTextTransform<TOOLS>>;
67+
};
68+
4869
/**
4970
* An Agent receives a prompt (text or messages) and generates or streams an output
5071
* that consists of steps, tool calls, data parts, etc.
@@ -84,6 +105,6 @@ export interface Agent<
84105
* Streams an output from the agent (streaming).
85106
*/
86107
stream(
87-
options: AgentCallParameters<CALL_OPTIONS>,
108+
options: AgentStreamParameters<CALL_OPTIONS, TOOLS>,
88109
): PromiseLike<StreamTextResult<TOOLS, OUTPUT>>;
89110
}

packages/ai/src/agent/create-agent-ui-stream-response.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { UIMessageStreamOptions } from '../generate-text';
1+
import { StreamTextTransform, UIMessageStreamOptions } from '../generate-text';
22
import { Output } from '../generate-text/output';
33
import { ToolSet } from '../generate-text/tool-set';
44
import { createUIMessageStreamResponse } from '../ui-message-stream';
@@ -30,6 +30,9 @@ export async function createAgentUIStreamResponse<
3030
agent: Agent<CALL_OPTIONS, TOOLS, OUTPUT>;
3131
messages: unknown[];
3232
options?: CALL_OPTIONS;
33+
experimental_transform?:
34+
| StreamTextTransform<TOOLS>
35+
| Array<StreamTextTransform<TOOLS>>;
3336
} & UIMessageStreamResponseInit &
3437
UIMessageStreamOptions<
3538
UIMessage<MESSAGE_METADATA, never, InferUITools<TOOLS>>

packages/ai/src/agent/create-agent-ui-stream.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { UIMessageStreamOptions } from '../generate-text';
1+
import { StreamTextTransform, UIMessageStreamOptions } from '../generate-text';
22
import { Output } from '../generate-text/output';
33
import { ToolSet } from '../generate-text/tool-set';
44
import { InferUIMessageChunk } from '../ui-message-stream';
@@ -25,11 +25,15 @@ export async function createAgentUIStream<
2525
agent,
2626
messages,
2727
options,
28+
experimental_transform,
2829
...uiMessageStreamOptions
2930
}: {
3031
agent: Agent<CALL_OPTIONS, TOOLS, OUTPUT>;
3132
messages: unknown[];
3233
options?: CALL_OPTIONS;
34+
experimental_transform?:
35+
| StreamTextTransform<TOOLS>
36+
| Array<StreamTextTransform<TOOLS>>;
3337
} & UIMessageStreamOptions<
3438
UIMessage<MESSAGE_METADATA, never, InferUITools<TOOLS>>
3539
>): Promise<
@@ -51,6 +55,7 @@ export async function createAgentUIStream<
5155
const result = await agent.stream({
5256
prompt: modelMessages,
5357
options: options as CALL_OPTIONS,
58+
experimental_transform,
5459
});
5560

5661
return result.toUIMessageStream(uiMessageStreamOptions);

packages/ai/src/agent/pipe-agent-ui-stream-to-response.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ServerResponse } from 'node:http';
2-
import { UIMessageStreamOptions } from '../generate-text';
2+
import { StreamTextTransform, UIMessageStreamOptions } from '../generate-text';
33
import { Output } from '../generate-text/output';
44
import { ToolSet } from '../generate-text/tool-set';
55
import { pipeUIMessageStreamToResponse } from '../ui-message-stream';
@@ -31,6 +31,9 @@ export async function pipeAgentUIStreamToResponse<
3131
agent: Agent<CALL_OPTIONS, TOOLS, OUTPUT>;
3232
messages: unknown[];
3333
options?: CALL_OPTIONS;
34+
experimental_transform?:
35+
| StreamTextTransform<TOOLS>
36+
| Array<StreamTextTransform<TOOLS>>;
3437
} & UIMessageStreamResponseInit &
3538
UIMessageStreamOptions<
3639
UIMessage<MESSAGE_METADATA, never, InferUITools<TOOLS>>

packages/ai/src/agent/tool-loop-agent.test-d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Output } from '../generate-text';
44
import { MockLanguageModelV3 } from '../test/mock-language-model-v3';
55
import { AsyncIterableStream } from '../util/async-iterable-stream';
66
import { DeepPartial } from '../util/deep-partial';
7-
import { AgentCallParameters } from './agent';
7+
import { AgentCallParameters, AgentStreamParameters } from './agent';
88
import { ToolLoopAgent } from './tool-loop-agent';
99

1010
describe('ToolLoopAgent', () => {
@@ -78,7 +78,7 @@ describe('ToolLoopAgent', () => {
7878
});
7979

8080
expectTypeOf<Parameters<typeof agent.stream>[0]>().toEqualTypeOf<
81-
AgentCallParameters<{ callOption: string }>
81+
AgentStreamParameters<{ callOption: string }, {}>
8282
>();
8383
});
8484

@@ -88,7 +88,7 @@ describe('ToolLoopAgent', () => {
8888
});
8989

9090
expectTypeOf<Parameters<typeof agent.stream>[0]>().toEqualTypeOf<
91-
AgentCallParameters<never>
91+
AgentStreamParameters<never, {}>
9292
>();
9393
});
9494

0 commit comments

Comments
 (0)