WorkflowAgent
WorkflowAgent
@ai-sdk/workflow 의 WorkflowAgent 는 workflow 안에서 실행되는 내구성 있고 재개 가능한(durable, resumable) 에이전트를 만들기 위해 설계됐어요. ToolLoopAgent 와 동일한 에이전트 루프를 제공하지만, 자동 상태 지속, tool 스키마 직렬화, workflow 단계 경계를 넘어 살아남는 내장 tool 승인 흐름을 추가해요.
출처: 문서
본문
왜 내구성 있는 에이전트인가? (Why Durable Agents?)
표준 ToolLoopAgent 는 전적으로 메모리에서 실행돼요. 프로세스가 크래시하면 모든 진행이 손실돼요. 여러 tool 호출을 하는 프로덕션 에이전트에서 이는 문제를 만듭니다:
- 상태성 (Statefulness) — 장기 실행 에이전트 루프는 프로세스 경계를 넘어 상태를 지속해야 해요.
- 재개 가능성 (Resumability) — 단계가 실패하면 처음부터 다시 시작하지 말고 마지막 체크포인트에서 재시도하고 싶어요.
- 사람 개입 (Human-in-the-loop) — 사용자 승인이 필요한 tools는 에이전트를 일시 중지하고 나중에 재개해야 해요.
- 관측성 (Observability) — 각 tool 호출이 별개의 workflow 단계로 실행되어 대시보드에서 보여요.
WorkflowAgent 는 workflow 안에서 실행되어 각 tool 실행이 자동 재시도가 있는 내구성 있는 단계가 됨으로써 이를 해결해요.
WorkflowAgent vs ToolLoopAgent 언제 쓰나
| ToolLoopAgent | WorkflowAgent | |
|---|---|---|
| 패키지 (Package) | ai |
@ai-sdk/workflow |
| 런타임 (Runtime) | In-memory | Workflow |
| 내구성 (Durability) | 크래시 시 손실 | 재시작에도 유지 |
| Tool 재시도 | 수동 | 자동 (workflow 단계를 통해) |
| 사람 승인 | 내장 | 내장 + 일시 중단에도 유지 |
generate() 메서드 |
사용 가능 | 사용 불가 |
stream() 메서드 |
사용 가능 | 기본 API |
| 스트림 출력 | streamText 반환값 |
ModelCallStreamPart 가 있는 writable 매개변수 |
내구성이 필요 없는 더 단순한 사용 사례에는 ai 패키지의 ToolLoopAgent 를 사용하세요.
설치 (Installation)
npm install @ai-sdk/workflow workflow@beta
@ai-sdk/workflow 는 현재 beta 태그로 제공되는 Workflow 5를 필요로 하며, ai 패키지와 zod peer 의존성도 필요해요. workflow 패키지는 Workflow DevKit 런타임(getWritable, 'use workflow', 'use step')을 제공해요.
WorkflowAgent 만들기 (Creating a WorkflowAgent)
모델, 지시, tools로 WorkflowAgent 클래스를 인스턴스화해 에이전트를 정의하세요:
import { WorkflowAgent } from '@ai-sdk/workflow';
import { tool } from 'ai';
import { z } from 'zod';
const agent = new WorkflowAgent({
model: 'anthropic/claude-sonnet-5',
instructions: 'You are a helpful assistant.',
tools: {
weather: tool({
description: 'Get weather for a location',
inputSchema: z.object({
location: z.string(),
}),
execute: async ({ location }) => ({
location,
temperature: 72,
}),
}),
},
});
모델 해석 (Model Resolution)
model 매개변수는 두 가지 형태를 받아요:
// String — AI Gateway model ID
new WorkflowAgent({ model: 'anthropic/claude-sonnet-5' });
// Provider instance
import { openai } from '@ai-sdk/openai';
new WorkflowAgent({ model: openai('gpt-6-astra') });
Workflow에서 에이전트 사용하기 (Using the Agent in a Workflow)
WorkflowAgent 는 workflow 함수 안에서 실행되도록 설계됐어요. 핵심 통합 지점은:
- 함수를
'use workflow'로 표시 - 에이전트의
stream()메서드에getWritable()전달 - API 라우트에서 workflow 시작
End-to-End 예제
import { WorkflowAgent, type ModelCallStreamPart } from '@ai-sdk/workflow';
import { convertToModelMessages, tool, type UIMessage } from 'ai';
import { getWritable } from 'workflow';
import { z } from 'zod';
export async function chat(messages: UIMessage[]) {
'use workflow';
const modelMessages = await convertToModelMessages(messages);
const agent = new WorkflowAgent({
model: 'anthropic/claude-sonnet-5',
instructions: 'You are a flight booking assistant.',
tools: {
searchFlights: tool({
description: 'Search for available flights',
inputSchema: z.object({
origin: z.string(),
destination: z.string(),
date: z.string(),
}),
execute: searchFlightsStep,
}),
bookFlight: tool({
description: 'Book a specific flight',
inputSchema: z.object({
flightId: z.string(),
passengerName: z.string(),
}),
execute: bookFlightStep,
}),
},
});
const result = await agent.stream({
messages: modelMessages,
writable: getWritable<ModelCallStreamPart>(),
});
return { messages: result.messages };
}
import { createModelCallToUIChunkTransform } from '@ai-sdk/workflow';
import { createUIMessageStreamResponse, type UIMessage } from 'ai';
import { start } from 'workflow/api';
import { chat } from '@/workflow/agent-chat';
export async function POST(request: Request) {
const { messages }: { messages: UIMessage[] } = await request.json();
const run = await start(chat, [messages]);
return createUIMessageStreamResponse({
stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()),
});
}
메시지 변환 (Message Conversion)
WorkflowAgent.stream() 은 UIMessage[] 가 아니라 ModelMessage[] 를 기대해요. 클라이언트(useChat 를 통해)에서 메시지를 받으면 먼저 변환하세요:
import { convertToModelMessages, type UIMessage } from 'ai';
export async function chat(messages: UIMessage[]) {
'use workflow';
const modelMessages = await convertToModelMessages(messages);
const result = await agent.stream({
messages: modelMessages,
// ...
});
}
쓰기 가능한 스트림 (Writable Streams)
반환된 스트림을 소비하는 ToolLoopAgent 와 달리, WorkflowAgent 는 workflow 런타임이 getWritable() 로 제공하는 writable 스트림에 원시 ModelCallStreamPart 청크를 써요. 응답 경계에서는 createModelCallToUIChunkTransform() 을 사용해 이것들을 클라이언트용 UIMessageChunk 객체로 변환하세요:
import { createModelCallToUIChunkTransform } from '@ai-sdk/workflow';
import { createUIMessageStreamResponse } from 'ai';
// Convert raw model stream parts → UI message chunks
return createUIMessageStreamResponse({
stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()),
});
변환은 재시도 시 WorkflowAgent 가 내보내는 reset-step 이벤트도 전달해요.
클라이언트는 실패한 model-call 단계의 부분 파트를 제거한 후 재시도 출력을 처리해요.
WorkflowChatTransport로 재개 가능한 스트리밍
Workflow 함수는 타임아웃되거나 네트워크 실패로 중단될 수 있어요. WorkflowChatTransport 는 이러한 중단을 자동으로 처리하는 ChatTransport 구현이에요. 스트림이 finish 이벤트 없이 끝나는 것을 감지하고 끊긴 지점에서 재개하도록 재연결해요.
'use client';
import { useChat } from '@ai-sdk/react';
import { WorkflowChatTransport } from '@ai-sdk/workflow/client';
import { useMemo } from 'react';
export default function Chat() {
const transport = useMemo(
() =>
new WorkflowChatTransport({
api: '/api/chat',
maxConsecutiveErrors: 5,
}),
[],
);
const { messages, sendMessage } = useChat({ transport });
// ... render chat UI
}
transport는 POST 엔드포인트가 x-workflow-run-id 응답 헤더를 반환하고, 재연결을 위해 {api}/{runId}/stream 에 GET 엔드포인트가 있어야 해요:
import { createModelCallToUIChunkTransform } from '@ai-sdk/workflow';
import { createUIMessageStreamResponse, type UIMessage } from 'ai';
import { start } from 'workflow/api';
import { chat } from '@/workflow/agent-chat';
export async function POST(request: Request) {
const { messages }: { messages: UIMessage[] } = await request.json();
const run = await start(chat, [messages]);
return createUIMessageStreamResponse({
stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()),
headers: {
'x-workflow-run-id': run.runId,
},
});
}
import { createModelCallToUIChunkTransform } from '@ai-sdk/workflow';
import { createUIMessageStreamResponse } from 'ai';
import type { NextRequest } from 'next/server';
import { getRun } from 'workflow/api';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ runId: string }> },
) {
const { runId } = await params;
const startIndex = Number(
new URL(request.url).searchParams.get('startIndex') ?? '0',
);
if (!Number.isSafeInteger(startIndex) || startIndex < 0) {
return Response.json(
{ error: 'startIndex must be a non-negative safe integer' },
{ status: 400 },
);
}
const run = await getRun(runId);
const readable = run
.getReadable({ startIndex: 0 })
.pipeThrough(
createModelCallToUIChunkTransform({ uiStartIndex: startIndex }),
);
return createUIMessageStreamResponse({
stream: readable,
headers: {
'x-workflow-run-id': runId,
},
});
}
WorkflowChatTransport 는 UIMessageChunk 객체를 세지만, 내구성 있는 WorkflowAgent 스트림은 원시 ModelCallStreamPart 객체를 저장해요. 위와 같이 인덱스 0 에서 원시 스트림을 재생하고 createModelCallToUIChunkTransform() 에서 음수가 아닌 UI 커서를 적용하세요. 음수 시작 인덱스는 이미 UIMessageChunk 객체를 저장하는 내구성 있는 스트림이 필요하며 이 원시→UI 변환과는 함께 쓸 수 없어요.
전체 API 참조는 WorkflowChatTransport 를 보세요.
Workflow 단계로서의 Tools (Tools as Workflow Steps)
tool execute 함수를 'use step' 로 표시해 내구성 있는 workflow 단계로 만들면 각 tool 호출에 다음이 제공돼요:
- 자동 재시도 (Automatic retries) — 실패한 tool 호출은 자동으로 재시도돼요(기본: 3회)
- 지속 (Persistence) — 결과가 프로세스 재시작에도 유지
- 관측성 (Observability) — 각 tool 호출이 workflow 대시보드에서 별개의 단계로 보임
async function searchFlightsStep(input: {
origin: string;
destination: string;
date: string;
}) {
'use step';
const response = await fetch(`https://api.flights.example/search?...`);
return response.json();
}
async function bookFlightStep(input: {
flightId: string;
passengerName: string;
}) {
'use step';
const response = await fetch('https://api.flights.example/book', {
method: 'POST',
body: JSON.stringify(input),
});
return response.json();
}
'use step' 가 없는 tools는 여전히 동작하지만 내구성 보장 없이 일반 인메모리 함수로 실행돼요.
Tool 승인 (Tool Approval)
WorkflowAgent 의 경우 사람 승인은 tool 정의에서 needsApproval 로 설정돼요. 이는 WorkflowAgent 에 특화된 것이에요. generateText, streamText, ToolLoopAgent 에서는 toolApproval 을 대신 사용하세요. workflow tool에 needsApproval 이 설정되면 에이전트는 멈추고 writable 스트림에 승인 요청을 내보내요. workflow는 사용자가 승인하거나 거부할 때까지 일시 중단됩니다:
const agent = new WorkflowAgent({
model: 'anthropic/claude-sonnet-5',
tools: {
bookFlight: tool({
description: 'Book a flight',
inputSchema: z.object({
flightId: z.string(),
passengerName: z.string(),
}),
needsApproval: true, // Always require approval
execute: bookFlightStep,
}),
cancelBooking: tool({
description: 'Cancel a booking',
inputSchema: z.object({ bookingId: z.string() }),
// Conditional approval based on input
needsApproval: async input => {
return input.bookingId.startsWith('VIP-');
},
execute: cancelBookingStep,
}),
},
});
workflow가 내구성이 있으므로 승인 요청은 프로세스 재시작에도 유지돼요. 사용자가 몇 시간 후에 승인해도 에이전트는 재개해요.
서명된 Tool 승인 (Signed Tool Approvals)
클라이언트가 제출한 메시지 기록은 workflow로 재생되기 전에 수정될 수 있어요. 민감한 작업을 수행하는 tools의 경우 experimental_toolApprovalSecret 을 설정해 승인 요청을 인증하세요:
const agent = new WorkflowAgent({
model: 'anthropic/claude-sonnet-5',
experimental_toolApprovalSecret: {
environmentVariable: 'TOOL_APPROVAL_SECRET',
},
tools: {
bookFlight: tool({
description: 'Book a flight',
inputSchema: z.object({ flightId: z.string() }),
needsApproval: true,
execute: bookFlightStep,
}),
},
});
에이전트는 승인 ID, tool 호출 ID, tool 이름, 검증된 입력을 HMAC 서명해요. 승인된 메시지 기록이 재생될 때 서명이 없거나 유효하지 않으면 tool이 실행되지 않아요. 서명은 내구성 있는 model-call 스트림, createModelCallToUIChunkTransform(), addToolApprovalResponse(), convertToModelMessages() 를 통해 보존돼요.
최소 32바이트의 높은 엔트로피 시크릿을 사용하고, 승인을 발급하거나 재개할 수 있는 모든 워커가 같은 시크릿을 사용하게 하세요. 그 시크릿으로 서명된 승인이 처리되는 동안 이전 키를 유지하세요. 시크릿을 바꾸면 그 보류 중인 승인이 무효화돼요.
WorkflowAgent 는 서명 및 검증 단계에 환경 변수 이름만 전달해요. 각 단계는 로컬 환경에서 시크릿을 읽고, 원시 값은 단계 인자, 내구성 있는 스트림 파트, 콜백, 텔레메트리 이벤트에 절대 포함되지 않아요. 모든 워커에 환경 변수를 설정하고, 시크릿 값을 runtimeContext 나 toolsContext 에 넣지 마세요.
experimental_toolApprovalSecret 을 agent.stream() 에도 제공할 수 있어요. 스트림 수준 값이 생성자 기본값을 오버라이드해요.
루프 제어 (Loop Control)
ToolLoopAgent 와 달리 WorkflowAgent 는 기본 단계 제한을 적용하지 않아요. stopWhen 을 생략하면 모델이 tool 호출을 멈추거나 다른 자연 종료 조건이 충족될 때까지 계속돼요.
에이전트가 취할 수 있는 단계 수를 제어하세요:
import { isStepCount } from 'ai';
const result = await agent.stream({
messages,
stopWhen: isStepCount(10), // Stop after 10 LLM calls
});
stopWhen 을 생략하면 WorkflowAgent 는 tool 호출을 끝낼 때까지 계속돼요. isLoopFinished() 로 그 의도를 명시적으로 만들 수 있어요:
import { isLoopFinished } from 'ai';
const result = await agent.stream({
messages,
stopWhen: isLoopFinished(),
});
isLoopFinished() 는 WorkflowAgent 의 stopWhen 생략과 동등해요. tool을 계속 호출하는 모델이 무한정 실행되고 상당한 비용이 들 수 있으므로 주의해서 사용하세요. isLoopFinished() 참조.
구조화된 출력 (Structured Output)
Output 을 사용해 에이전트 응답을 타입화된 객체로 파싱하세요:
import { Output } from '@ai-sdk/workflow';
import { z } from 'zod';
const result = await agent.stream({
messages,
output: Output.object({
schema: z.object({
sentiment: z.enum(['positive', 'neutral', 'negative']),
summary: z.string(),
}),
}),
});
console.log(result.output); // { sentiment: 'positive', summary: '...' }
설정 옵션 (Configuration Options)
WorkflowAgent 는 ToolLoopAgent(temperature, maxOutputTokens, topP 등)와 동일한 생성 설정과 workflow 특화 옵션을 받아요.
runtimeContext 및 toolsContext
프롬프트에 넣지 않고 에이전트 루프를 통해 서버 측 상태를 전달하세요. 이전 experimental_context 옵션 대신 이것들을 사용하세요.
runtimeContext는prepareStep, 수명 주기 콜백,onEnd를 흐르는 공유 에이전트 상태예요. 불변으로 취급하세요. 현재 및 이후 단계에 업데이트하려면prepareStep에서 새 값을 반환하세요.toolsContext는 tool 이름 키로 된 tool별 맵이에요. 각 tool의execute는 자체 검증된 항목만context로 봐요.contextSchema를 선언한 tools는 실행 전에 그 스키마에 대해 항목을 검증해요.
import { WorkflowAgent } from '@ai-sdk/workflow';
import { tool } from 'ai';
import { z } from 'zod';
const agent = new WorkflowAgent({
model: 'anthropic/claude-sonnet-4-6',
tools: {
weather: tool({
description: 'Get the weather for a city.',
inputSchema: z.object({ city: z.string() }),
contextSchema: z.object({
defaultUnit: z.enum(['celsius', 'fahrenheit']),
}),
execute: async ({ city }, { context }) => ({
city,
unit: context.defaultUnit,
}),
}),
},
// Shared agent state — available in `prepareStep`, lifecycle callbacks, and `onEnd`.
runtimeContext: {
tenantId: 'tenant_123',
requestId: 'req_abc',
plan: 'enterprise',
},
// Per-tool context — each tool sees only its own validated entry.
toolsContext: {
weather: { defaultUnit: 'celsius' },
},
prepareStep: ({ runtimeContext }) => {
if (runtimeContext.plan === 'enterprise') {
return { temperature: 0.2 };
}
return {};
},
});
runtimeContext 와 toolsContext 는 호출별로 stream() 에 전달해 생성자 수준 기본값을 오버라이드할 수도 있어요.
WorkflowAgent 는 Workflow 런타임 안에서 실행되므로 컨텍스트 값은 workflow와 단계 경계를 넘어 지속되고 재생될 수 있어요. runtimeContext, toolsContext, prepareStep 에서 반환된 컨텍스트 값을 직렬화 가능하게 유지하세요. 문자열, 숫자, 부울, 배열, 평범한 객체, 날짜, URL, 맵, 셋 및 기타 Workflow가 지원하는 구조화된 데이터 같은 평범한 데이터를 사용하세요. 컨텍스트에 함수, 클래스 인스턴스, 심볼, WeakMap, WeakSet, 데이터베이스 클라이언트, SDK 클라이언트를 넣지 마세요. 식별자나 설정 데이터를 대신 전달하고, 직렬화할 수 없는 리소스는 단계 함수 안에서 다시 생성하세요.
이것은 단일 프로세스 수명 동안 더 풍부한 JavaScript 값을 운반할 수 있는 인메모리 실행 ToolLoopAgent 와 다릅니다. WorkflowAgent 에서 컨텍스트를 내구성 있는 데이터로 취급하면 workflow 재생과 단계 실행이 안정적이에요.
experimental_sandbox
tools가 실행 환경을 필요로 할 때 sandbox 세션을 전달하세요. sandbox는 experimental_sandbox 로 tool 설명과 execute 함수에, 그리고 현재 단계에 대해 오버라이드할 수 있는 prepareStep 에 사용할 수 있어요:
const agent = new WorkflowAgent({
model: 'anthropic/claude-sonnet-5',
tools: {
shell: tool({
description: 'Run a shell command in the sandbox.',
inputSchema: z.object({ command: z.string() }),
execute: async ({ command }, { experimental_sandbox }) => {
if (!experimental_sandbox) {
throw new Error('Sandbox is not available');
}
return experimental_sandbox.run({ command });
},
}),
},
experimental_sandbox: sandbox,
});
await agent.stream({
messages,
writable: getWritable(),
experimental_sandbox: requestSandbox, // Overrides the constructor default.
});
experimental_sandbox 는 내구성 있는 컨텍스트가 아니라 라이브 런타임 핸들이에요. runtimeContext 나 toolsContext 에 저장하지 마세요. tool이 별도의 workflow 단계로 실행된다면 직렬화 가능한 sandbox 식별자나 설정을 전달하고 그 단계 안에서 다시 부착하세요.
prepareCall
에이전트 루프가 시작되기 전에 한 번 호출돼요. 런타임 컨텍스트에 기반해 모델, 지시 또는 다른 설정을 변환하는 데 사용하세요:
const agent = new WorkflowAgent({
model: 'anthropic/claude-sonnet-5',
prepareCall: async ({ model, tools, messages }) => {
return {
instructions: `Current time: ${new Date().toISOString()}`,
};
},
});
prepareStep
각 단계(LLM 호출) 전에 호출돼요. 설정 수정, 컨텍스트 관리, 메시지 동적 주입에 사용하세요:
const agent = new WorkflowAgent({
model: 'anthropic/claude-sonnet-4-6',
prepareStep: async ({ stepNumber, experimental_sandbox }) => {
if (stepNumber > 5) {
return { toolChoice: 'none' }; // Force text response after 5 steps
}
if (experimental_sandbox) {
return { temperature: 0.2 };
}
return {};
},
});
prepareCall 과 prepareStep 모두 stream() 에서 호출별로 전달할 수도 있어요.
수명 주기 콜백 (Lifecycle Callbacks)
에이전트는 로깅, 관측성, 커스텀 텔레메트리를 위한 수명 주기 콜백을 제공해요. 모든 콜백은 생성자(에이전트 전체) 또는 stream()(호출별)에서 정의할 수 있어요. 둘 다 제공되면 둘 다 발화해요(생성자 우선):
const agent = new WorkflowAgent({
model: 'anthropic/claude-sonnet-5',
onStart({ messages }) {
console.log(`Agent started with ${messages.length} messages`);
},
onStepStart({ stepNumber }) {
console.log(`Step ${stepNumber} starting`);
},
onToolExecutionStart({ toolCall }) {
console.log(`Calling tool: ${toolCall.toolName}`);
},
onToolExecutionEnd({ toolCall, success, durationMs }) {
console.log(`Tool finished: ${toolCall.toolName}`, {
success,
durationMs,
});
},
onStepEnd({ usage, finishReason }) {
console.log('Step done:', { finishReason });
},
onEnd({ steps, totalUsage }) {
console.log(`Completed in ${steps.length} steps`);
},
});
구체적인 tool 세트의 경우 WorkflowAgentToolExecutionStartEvent 와 WorkflowAgentToolExecutionEndEvent 는 각 tool 이름과 그 입력, 컨텍스트, 출력 유형 사이의 관계를 보존해요. TypeScript는 중첩된 toolCall 을 직접 좁히지만, tool 이름으로 상관된 toolContext 또는 output 필드를 좁혀야 할 때는 Extract 또는 사용자 정의 타입 가드를 사용하세요.
tool 입력 콜백(onInputStart, onInputDelta, onInputAvailable)도 WorkflowAgent 에 의해 보존돼요. 모델 호출은 내구성 있는 단계 안에서 실행되는 반면 콜백 함수는 workflow 컨텍스트에 남는데, 임의의 함수는 단계 경계를 넘을 수 없기 때문이에요. 그 결과 WorkflowAgent 는 모델 단계 동안 콜백 이벤트를 기록하고 그 단계가 완료된 직후, tool 실행과 단계 수명 주기 콜백 이전에 순서대로 재생해요. 이들은 모델 생성과 동시에 실행되지 않으며 진행 중인 취소나 백프레셔를 제공할 수 없어요. 각 콜백은 contextSchema 검증 후 자체 tool의 toolsContext 항목을 받아요.
매우 조각난 tool 입력의 경우 onInputDelta 재생 데이터는 내구성 있는 model-step 결과의 일부예요. 생성된 각 델타가 필요할 때만 onInputDelta 를 설정하세요. 델타 재생 데이터를 보유하지 않으려면 생략하세요.
폐기된 experimental_onStart 와 experimental_onStepStart 이름은 백워드 호환성을 위해 계속 사용할 수 있어요. 같은 생성자나 stream() 호출에 안정 버전과 실험 버전 이름이 모두 제공되면 안정 콜백이 사용돼요.
타입 추론 (Type Inference)
타입 안전한 클라이언트 컴포넌트를 위해 UI 메시지 유형을 추론하세요:
import { WorkflowAgent, InferWorkflowAgentUIMessage } from '@ai-sdk/workflow';
const myAgent = new WorkflowAgent({
// ... configuration
});
export type MyAgentUIMessage = InferWorkflowAgentUIMessage<typeof myAgent>;
DurableAgent 에서 마이그레이션
WorkflowAgent 는 Workflow DevKit의 DurableAgent 를 대체해요. 둘은 같은 핵심 아이디어(workflow 안에서 실행되는 내구성 있는 에이전트 루프)를 공유하지만, WorkflowAgent 는 클래스를 AI SDK로 옮기고, 타입을 강화하고, 일등석(frist-class) tool 승인을 도입해요. 현재 DurableAgent 를 사용 중이라면 아래 단계를 따라 전환하세요.
import와 클래스 이름 변경
DurableAgent 는 workflow/ai 에서 내보내졌어요. WorkflowAgent 는 헬퍼들과 함께 @ai-sdk/workflow 에서 내보내져요.
- import { DurableAgent } from 'workflow/ai';
+ import { WorkflowAgent, type ModelCallStreamPart } from '@ai-sdk/workflow';
- const agent = new DurableAgent({
+ const agent = new WorkflowAgent({
model: 'anthropic/claude-sonnet-5',
instructions: 'You are a helpful assistant.',
tools: { /* ... */ },
});
workflow 옆에 새 패키지를 설치하세요:
npm install @ai-sdk/workflow workflow@beta
UIMessageChunk 가 아니라 ModelCallStreamPart 작성
DurableAgent 는 getWritable() 이 반환한 writable에 UIMessageChunk 객체를 직접 썼어요. WorkflowAgent 는 더 저수준의 ModelCallStreamPart 형태를 쓰고 변환은 응답 경계의 트랜스폼에 맡겨요. 이렇게 하면 내구성 있는 스트림이 프로바이더 형태로 유지되고 workflow 페이로드에 UI 프로토콜을 굽지 않아요.
// Inside the workflow
await agent.stream({
messages,
- writable: getWritable<UIMessageChunk>(),
+ writable: getWritable<ModelCallStreamPart>(),
});
// Inside the route handler
+ import { createModelCallToUIChunkTransform } from '@ai-sdk/workflow';
return createUIMessageStreamResponse({
- stream: run.readable,
+ stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()),
});
maxSteps 를 stopWhen 으로 교체
DurableAgent 는 maxSteps 를 직접 받았어요. WorkflowAgent 는 AI SDK의 공유 stopWhen 조건을 사용하므로 같은 중지 로직이 ToolLoopAgent, generateText, streamText 에서 동작해요.
+ import { isStepCount } from 'ai';
await agent.stream({
messages,
- maxSteps: 10,
+ stopWhen: isStepCount(10),
});
중지 조건 전체 목록은 Loop Control 을 보세요.
experimental_output 를 output 으로 교체
+ import { Output } from '@ai-sdk/workflow';
await agent.stream({
messages,
- experimental_output: Output.object({ schema }),
+ output: Output.object({ schema }),
});
반환값은 이제 result.output 에 있어요(이전에는 result.experimental_output).
WorkflowAgent: human-in-the-loop tools에 needsApproval 사용
DurableAgent 에서는 tool 승인이 tool의 execute 함수 안에서 Hook을 호출해 구현됐어요. WorkflowAgent 는 승인을 일등석 tool 속성으로 만듭니다. 에이전트가 승인 요청을 내보내고 workflow를 일시 중단하며, 사용자가 응답하면 자동으로 재개해요.
bookFlight: tool({
description: 'Book a flight',
inputSchema: z.object({ flightId: z.string() }),
+ needsApproval: true,
- execute: async (input) => {
- const approved = await waitForApprovalHook(input);
- if (!approved) throw new Error('Denied');
- return bookFlightStep(input);
- },
+ execute: bookFlightStep,
}),
needsApproval 은 비동기 함수도 받아 입력별로 승인이 필요한지 결정할 수 있어요(위의 Tool 승인 참조).
uiMessages / collectUIMessages 제거됨
DurableAgent.stream() 은 collectUIMessages: true 가 설정되면 누적된 uiMessages 를 반환했어요. WorkflowAgent.stream() 은 대신 result.messages 에 ModelMessage[] 를 반환해요.
지속을 위해 UIMessage[] 를 진실의 원천으로 저장하고 에이전트에 전달하기 전에 convertToModelMessages 를 호출하세요. 이것은 Chatbot Message Persistence 에 설명된 패턴이에요. 내장된 ModelMessage → UIMessage 변환은 없으므로, 나중에 UI에서 대화를 렌더링해야 한다면 result.messages 를 유일한 사본으로 지속하지 마세요.
const result = await agent.stream({
messages,
writable: getWritable<ModelCallStreamPart>(),
- collectUIMessages: true,
});
- return { uiMessages: result.uiMessages };
+ return { messages: result.messages };
generate() 메서드 없음
WorkflowAgent 는 stream() 만 노출해요. agent.generate() 를 호출하고 있었다면 stream() 으로 전환하고 프로미스가 해결되면 result.messages / result.output 을 읽으세요.
experimental_context 를 runtimeContext 와 toolsContext 로 교체
WorkflowAgent 는 더 이상 experimental_context 를 받지 않아요. 값을 공유 에이전트 상태(runtimeContext)와 tool별 상태(toolsContext)로 나누세요. 그러면 각 tool의 execute 는 자체 검증된 항목만 context 로 받아요. 전체 형태는 runtimeContext 및 toolsContext 를 보세요.
const agent = new WorkflowAgent({
model: 'anthropic/claude-sonnet-5',
tools: { weather: weatherTool },
- experimental_context: { tenantId: 'tenant_123', apiKey: *** },
+ runtimeContext: { tenantId: 'tenant_123' },
+ toolsContext: { weather: { apiKey: *** } },
});
그 외 모든 것
다른 옵션은 같은 이름으로 이어져요: prepareStep, onStart, onStepStart, onStepEnd, onEnd, onError, toolChoice, activeTools, timeout, repairToolCall, experimental_sandbox, 그리고 일반 생성 설정(temperature, maxOutputTokens, topP, …). WorkflowAgent 는 추가로 prepareCall(루프 전 한 번 실행)과 위에서 문서화한 onToolExecutionStart / onToolExecutionEnd 수명 주기 콜백을 추가해요. 폐기된 experimental_onStart 와 experimental_onStepStart 별칭은 백워드 호환성을 위해 계속 사용할 수 있어요.
다음 단계 (Next Steps)
- 상세 매개변수 문서는 WorkflowAgent API Reference
- 스트림 재연결 옵션은 WorkflowChatTransport API Reference
- 인메모리
ToolLoopAgent대안은 Building Agents - 고급 중지 조건은 Loop Control