Durable agents with Vercel Workflow
Long-running assistants and chat backends often need durable execution: if a function restarts, the same logical run should resume, tool calls should retry safely, and each step should be observable. @github-tools/sdk supports this through the Vercel Workflow SDK and the @github-tools/sdk/workflow entry point.
Integrate a durable GitHub assistant
What “durable” means here
"use workflow": Your orchestration function is a workflow: the platform can pause, resume, and replay it across failures and deploys."use step": Each GitHub tool implementation runs inside a named, module-level step. Every tool call is a durable step with retries and full Node.js access in the workflow runtime.createDurableGithubAgent: Wraps the same GitHub tools in aWorkflowAgentfrom@ai-sdk/workflowso LLM turns and tool invocations participate in that durable model (not just rawgenerateTextin a plain serverless handler).
Together, this is the recommended pattern when you build Nuxt, Next.js, or other apps that expose a chat or agent API and must not lose progress on timeout or cold start.
Install optional workflow dependencies
Durable agents are optional. Add them only when you use @github-tools/sdk/workflow:
pnpm add workflow @ai-sdk/workflow
npm install workflow @ai-sdk/workflow
yarn add workflow @ai-sdk/workflow
bun add workflow @ai-sdk/workflow
You still need ai, zod, and @github-tools/sdk as documented in Installation.
Minimal durable workflow
createDurableGithubAgent returns a DurableGithubAgent with two methods:
.stream(): real-time output to aWritableStream(for chat UIs).generate(): non-streaming, returns the full text response (for bots, background jobs, webhooks)
Both methods execute each tool call as a durable workflow step with automatic retries.
Streaming (chat UI)
import { createDurableGithubAgent } from '@github-tools/sdk/workflow'
import { getWritable } from 'workflow'
import type { ModelCallStreamPart, ModelMessage } from 'ai'
export async function durableGithubChat(
messages: ModelMessage[],
token: string,
model: string
) {
'use workflow'
const writable = getWritable<ModelCallStreamPart>()
const agent = createDurableGithubAgent({
model,
token,
preset: 'code-review',
requireApproval: true,
})
await agent.stream({ messages, writable })
}
Non-streaming (bot / background job)
For non-streaming use cases (bots, webhooks, background jobs), use createGithubAgent inside a "use step" function. This gives you the full tool loop while keeping the step durable:
import { createGithubAgent } from '@github-tools/sdk'
async function runAgentTurn(prompt: string) {
'use step'
const agent = createGithubAgent({
model: 'anthropic/claude-sonnet-4.6',
preset: 'code-review',
requireApproval: false,
})
const { text } = await agent.generate({ prompt })
return text
}
export async function reviewWorkflow(prompt: string) {
'use workflow'
await runAgentTurn(prompt)
}
Wire it into your framework
Workflows are plain exported functions. Your framework's Workflow integration starts the run from an API route and streams results to the client.
Place workflows under server/workflows/ and start them from a Nitro server route:
import { start } from 'workflow/api'
import { durableGithubChat } from '../workflows/durable-chat.workflow'
export default defineEventHandler(async (event) => {
const { messages, model } = await readBody(event)
const session = await requireUserSession(event)
const run = await start(durableGithubChat, [messages, session.secure.githubToken, model])
setHeader(event, 'x-workflow-run-id', run.runId)
return run.readable
})
Export workflows from any module and start them from an App Router route handler:
import { start } from 'workflow/api'
import { durableGithubChat } from '@/workflows/durable-chat.workflow'
export async function POST(req: Request) {
const { messages, model } = await req.json()
const run = await start(durableGithubChat, [messages, process.env.GITHUB_TOKEN!, model])
return new Response(run.readable, {
headers: { 'x-workflow-run-id': run.runId },
})
}
examples/pr-review-agent/ starter: a ~60-line PR review agent with multi-turn durable sessions and evlog AI observability. Full walkthrough: Chat SDK.Presets and options
All presets (code-review, issue-triage, repo-explorer, ci-ops, security-audit, release-manager, maintainer) work with createDurableGithubAgent. Options mirror createGithubAgent for model, token, preset, instructions, and temperature, with additional pass-through for WorkflowAgentOptions fields like experimental_telemetry, onStepEnd, onEnd, and prepareStep. Use stopWhen (for example stepCountIs(n)) instead of the deprecated maxSteps parameter.
Approval control and durable agents
requireApproval maps to needsApproval on write tools. When a tool needs approval, WorkflowAgent pauses the workflow, emits an approval request to the stream, and resumes when the user approves or denies: the same UX as createGithubAgent, but durable across restarts.
Wire the client with WorkflowChatTransport and a GET reconnect route ({api}/{runId}/stream) so long runs survive timeouts. The demo chat app toggles this with the shield icon.
For richer approval policies ('once' per session, input-dependent predicates), use the eve extension.
Standard agents vs durable agents
createGithubAgent | createDurableGithubAgent | |
|---|---|---|
| Import | @github-tools/sdk | @github-tools/sdk/workflow |
| Runtime | In-process ToolLoopAgent | DurableGithubAgent inside "use workflow" |
| Methods | .generate(), .stream() | .generate(), .stream() |
| Retries / resume | You handle | Workflow-managed durable steps |
requireApproval | Supported | Supported (via needsApproval on tools) |
Durable steps on every tool
Even if you do not use createDurableGithubAgent, spreading createGithubTools() inside a workflow still benefits from per-tool "use step" boundaries when the AI SDK executes tools. Each GitHub operation remains a proper workflow step. The durable agent entry point additionally durably wraps the LLM loop itself.