AI SDK と Workflow DevKit を眺め、Agent 抽象の行く末に思いを馳せる

LayerX AI エージェントブログリレー 52日目の記事です。
バクラク事業部 スタッフエンジニアの @izumin5210 です。
Platform Engineering 部 Enabling チームでいろいろなことをしています。
AI SDK 6 Beta で ToolLoopAgent class と Agent interface, Workflow DevKit(vercel/workflow) のサブパッケージである @workflow/ai で DurableAgent と、Agent の抽象を表すための概念が3つ続けて登場しています。 本記事ではこれらの定義・実装や関連するコードを眺め、 AI SDK や TypeScript で同様のレベル感のライブラリにおける Agent 抽象の行く末を妄想して楽しみます。 特にオチはない予定です。
ToolLoopAgent class (AI SDK 6 Beta)
AI SDK v5 で experimental_Agent として導入され、 AI SDK 6 から ToolLoopAgent とリネームされ導入予定の class です。
- メインの API は後述する
Agentinterface が持つgenerate()とstream()の2つ - いずれも
messagesorpromptを引数にとり、constructor 経由で渡した他のオプションとマージしてgenerateText()あるいはstreamText()をそのまま実行する - 従来のオプションに加え、以下2つをコンストラクタから与えることができる
-
callOptionsSchema:generate()およびstream()実行時に追加の引数を要求できる -
prepareCalls:generateText/streamTextに渡すオプションを制御可能
-
messages or prompt 以外のオプションをインスタンス化時に渡し、そのインスタンスを使い回すことで Agent の挙動をカプセル化する… というコンセプトなんでしょうか。
// https://github.com/vercel/ai/blob/bfff278/packages/ai/src/agent/tool-loop-agent.ts#L12-L110 export class ToolLoopAgent< CALL_OPTIONS = never, TOOLS extends ToolSet = {}, OUTPUT extends Output = never, > implements Agent<CALL_OPTIONS, TOOLS, OUTPUT> { // ... private async prepareCall( options: AgentCallParameters<CALL_OPTIONS>, ): Promise</* ... */> { // ... } async generate({ abortSignal, ...options }: AgentCallParameters<CALL_OPTIONS>): Promise< GenerateTextResult<TOOLS, OUTPUT> > { return generateText({ ...(await this.prepareCall(options)), abortSignal, }); } async stream({ abortSignal, experimental_transform, ...options }: AgentStreamParameters<CALL_OPTIONS, TOOLS>): Promise< StreamTextResult<TOOLS, OUTPUT> > { return streamText({ ...(await this.prepareCall(options)), abortSignal, experimental_transform, }); } }
実プロダクト開発では tracing をはじめとして Observability のための設定やフックなど、全ての Agent 処理にもれなく設定したいものがいくつかあるので、それらを設定した BaseAgent abstract class を作ってプロジェクト内で使いまわす… というのはしたくなるような気がします。
Agent class (AI SDK 6 Beta)
vercel/ai#9450 で experimental_Agent が Agent interface と BaseAgent class(後の ToolLoopAgent)に分かれました。Pull Request の description によると拡張性の高い・カスタマイズされた Agent を実装できるようにするのが目的だそうです。
// https://github.com/vercel/ai/blob/bfff278/packages/ai/src/agent/agent.ts#L69-L110 export interface Agent< CALL_OPTIONS = never, TOOLS extends ToolSet = {}, OUTPUT extends Output = never, > { readonly version: 'agent-v1'; readonly id: string | undefined; readonly tools: TOOLS; generate( options: AgentCallParameters<CALL_OPTIONS>, ): PromiseLike<GenerateTextResult<TOOLS, OUTPUT>>; stream( options: AgentStreamParameters<CALL_OPTIONS, TOOLS>, ): PromiseLike<StreamTextResult<TOOLS, OUTPUT>>; }
AI SDK 6 Beta のリリースブログでは OrchsestratorAgent を実装するというユースケース例が紹介されています。
import { Agent } from 'ai'; // Build your own multi-agent orchestrator that delegates to specialists class Orchestrator implements Agent { constructor(private subAgents: Record<string, Agent>) { /* Implementation */ } } const orchestrator = new Orchestrator({ subAgents: { // your subagents }, });https://ai-sdk.dev/docs/announcing-ai-sdk-6-beta#custom-agent-implementations
Agent interface の使い道を考えてみる
この Agent を利用する API は現状以下の3つの関数と1つの utility type です。
createAgentUIStreamcreateAgentUIStreamResponsepipeAgentUIStreamResponseInferAgentTools
上記はいずれも Agent の出力を stream でフロントエンド(useChat)に返すために使うのが主な用途でしょう。 Agent interface を維持していれば、その実態が単一 Agent ではなく複数の Agent を Orchestrate したものであっても、自然に stream に接続できるようになります。
「呼び出し元のための stream 処理」という presentation な知識と「Agent の orchestration」というメインロジックが、 Agent interface を界面にきれいに分離できるようになる… というのが現状 Agent interface を使うモチベーションの1つになりそうです。 Agent の構成を変えるたびに streaming してる API handler まで変更が必要になるのは手間なので、ここに境界を引けることは責務の分解という点でも重要です。
また、 具体の Agent に依存しないような仕組み・フレームワークを自前で作る際にもこの Agent interface は利用できるでしょう。 例えば Agent を Temporal など Durable Workflow の上で動かすための何らかのユーティリティが Agent を要求する… という使い方はありそうです。
…つらつら書いたけど、どちらもわりと一般的な interface の利用用途ですね。
一方で、generate() と stream() はいずれも generateText() と streamText() の返り値の型に依存しています。 特に後者の型 StreamTextResult は自分で作るのはあまり現実的ではないような気がします。 今のところ処理実態は streamText() あるいは ToolLoopAgent.prototype.stream() を使うことが前提となりそうです。 これは後述する DurableAgent の成熟で変わったりするのか… 個人的には注目ポイントの一つです。
DurableAgent (Workflow DevKit beta, @workflow/ai experimental)
DurableAgent は10月下旬に Vercel より公開された Workflow DevKit が提供する実装の1つで、これを利用することで Agent が簡単に Durable に実行できるというものです。
「Agent の Durable な実行」についてはブログリレー14日目で紹介しています。
今回は先にコード例を示します。
async function getWeather({ city }: { city: string }) { "use step"; return `Weather in ${city} is sunny`; } async function myAgent() { "use workflow"; const agent = new DurableAgent({ model: 'anthropic/claude-haiku-4.5', system: 'You are a helpful weather assistant.', tools: { getWeather: { description: 'Get weather for a city', inputSchema: z.object({ city: z.string() }), execute: getWeather, }, }, }); const writable = getWritable<UIMessageChunk>(); await agent.stream({ messages: [{ role: 'user', content: 'How is the weather in San Francisco?' }], writable, }); }https://useworkflow.dev/docs/api-reference/workflow-ai/durable-agent
DurableAgent における Workflow / Step の使い分け
Workflow DevKit では Workflow(ステップの実行制御)と Step(実際の処理)をそれぞれ 'use workflow', 'use step' というディレクティブで制御しますが、DurableAgent を使えば Agent Loop の実装のうち決定論的に処理できない LLM 呼び出しの部分を内部的に Step として扱ってくれます。
// https://github.com/vercel/workflow/blob/71d1757d0fd5f6971dfff6fc1b5601c3cfef6d53/packages/ai/src/agent/do-stream-step.ts#L20-L45
export async function doStreamStep(
// ...
) {
'use step';
// ...
const result = await model.doStream({
prompt: conversationPrompt,
tools,
});
Tool の実行については実装者に委ねられており、先ほどのコード例は 'use step'; が付いているため Step として扱われることになります。
この Tool 実行の扱いが実装者に委ねられている、というのは非常に嬉しいポイントです。 ツール実行も単なる外部 API 呼び出しであれば Step でいいのですが、例えば Agent as Tool 的に別 Agent を呼び出したいケースでは自明ではありません。 Tool 呼び出しを含む Agent Loop のロジックが generateText() / streamText() 内にべったり実装されているため Activity(Step) と Workflow の分離が難しく、 Temporal 上での実現は大変です。しかし、 Workflow DevKit ではなぜかいい感じになってくれています。
DurableAgent の内部実装
ではこれがどう実現されているか、改めて DurableAgent のコードを見てみましょう。
// https://github.com/vercel/workflow/blob/71d1757/packages/ai/src/agent/durable-agent.ts#L94-L261 export class DurableAgent { constructor(options: DurableAgentOptions) { this.model = options.model; this.tools = options.tools ?? {}; this.system = options.system; } generate() { throw new Error('Not implemented'); } async stream(options: DurableAgentStreamOptions) { const prompt = await standardizePrompt({ /* ... */ }); const modelPrompt = await convertToLanguageModelPrompt({ /* ... */ }); const iterator = streamTextIterator({ /* ... */ }); let result = await iterator.next(); while (!result.done) { const toolCalls = result.value; const toolResults = await Promise.all( toolCalls.map( (toolCall): Promise<LanguageModelV2ToolResultPart> => executeTool(toolCall, this.tools) ) ); result = await iterator.next(toolResults); } const sendFinish = options.sendFinish ?? true; const preventClose = options.preventClose ?? false; // Only call closeStream if there's something to do if (sendFinish || !preventClose) { await closeStream(options.writable, preventClose, sendFinish); } // The iterator returns the final conversation prompt (LanguageModelV2Prompt) // which is compatible with ModelMessage[] const messages = result.value as ModelMessage[]; return { messages }; } }
なんと、 streamText() などは使わず独自実装の streamTextIterator() を呼び出していることがわかります。 ここからコードを読み進めていっても基本的には全て独自実装になっており、 AI SDK に依存しているのはモデル呼び出し部分(LanguageModelV2)くらいにです。 Agent Loop はおおむね実装し直しているということになります。びっくり。
この内部実装について DurableAgent が experimental ということもあり、まだまだ変化していくでしょう。 もしかしたら DurableAgent 実装の過程で generateText() / streamText() の内部実装が整理され、 AI SDK 利用者が Agent Loop をより柔軟にカスタマイズできるようなフックや低レベル API が整備されるかもしれません。 そうなると Temporal に乗せるときも便利なんだけどなあ…。
DurableAgent は AI SDK 6 の Agent interface を実装していない
DurableAgent の stream() は async stream(options: DurableAgentStreamOptions): Promise<ModelMessage[]> というシグネチャになっています。 なんと、先ほど紹介した Agent が実装されていません。 まず stream() の責務もちょっと違っています。
-
Agentinterface のstream()- メッセージストリームを
AsyncIterableで返し、その用途は呼び出し側に一任される - ストリームは
UIMessageChunkのほか、stringなどでも取り出し可能
- メッセージストリームを
-
DurableAgentclass のstream()- 引数に
WritableStreamを受け取り、それをUIMessageChunkを書き込んでしまう
- 引数に
ということで、「UIMessageChunk を書き込む」ところまでは規定されてしまっています。 step から workflow に直接 AsyncIterable を返すのが難しいから…か? なので、ある Agent の出力を別の Agent に流すときは Workflow DevKit が提供する stream の機構を通すことになりそうです。 そもそも API の共通化が難しいんですね。
逆に、Agent のポータビリティを高めるために Agent interface が Workflow DevKit でも利用できるかたちに変化したりできればまた楽しいんですけどね。 なんかうまい API ないかなあ。
おわりに
Workflow DevKit の登場で AI SDK 側により低レベルなレイヤの API 化のモチベーションが発生してそうなのが個人的な注目ポイントでした。 現状 Workflow DevKit が Vercel 上での利用が前提になっている中で、AI SDK 側の低レベル API が充実すれば Temporal など別の Durable Workflow 基盤上で AI SDK を扱いやすくなります。 一方で、 Workflow DevKit はここで紹介しなかった Stream をはじめとした AI Agent 時代の Durable Workflow を作りに来てる感もあり、非常に楽しみです。
ただのライブラリ・フレームワーク設計オタクが API を眺めてニヤニヤするだけの記事になってしまいましたが、この3つの class あるいは interface を眺めるだけでもまだまだ Agent は奥が深く、抽象化を推し進めるためにはまだまだ先が見えない状態なんだなあというのが伝わってくる感じがします。 あれだけ AI Agent に投資してる Vercel ですら手探りなので、やはりまだまだ遊びの余地が大きい領域ではありそうです。
Discussion