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 함수 안에서 실행되도록 설계됐어요. 핵심 통합 지점은:

  1. 함수를 'use workflow' 로 표시
  2. 에이전트의 stream() 메서드에 getWritable() 전달
  3. 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 호출을 멈추거나 다른 자연 종료 조건이 충족될 때까지 계속돼요.

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)

더 알아보기 (Learn more)

전체 사이트맵