Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion examples/fastapi-vite/backend/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions examples/temporal-durable/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Python
__pycache__/
*.py[cod]
*.egg-info/
.venv/
dist/

# Environment
.env
.env*.local
1 change: 1 addition & 0 deletions examples/temporal-durable/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
61 changes: 61 additions & 0 deletions examples/temporal-durable/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Durable Agent Execution with Temporal

An agent (weather + population tools) running as a Temporal workflow.
Every LLM call and tool call is a durable activity — the agent survives
crashes and restarts because Temporal replays activity results from its
event history.

## How it works

The framework and Temporal compose via plain async/await:

- **Tools**: `@ai.tool` with `execute_activity()` in the body — each tool
is its own activity with a matching signature
- **LLM**: `DurableModel` wraps an activity call into a `LanguageModel`;
the activity uses `llm.buffer()` and `ToolSchema`
- **Loop**: `ai.stream_loop()` runs the agent loop unchanged
- **Bus**: `ai.run()` provides the unified message bus for streaming

The agent function is identical to the non-Temporal version.
Temporal doesn't know about the framework; the framework doesn't know
about Temporal.

**3 files:** `activities.py` (I/O), `workflow.py` (agent + wrappers), `main.py`

```
Workflow (deterministic) Activities (real I/O)
┌─────────────────────────┐ ┌──────────────────────┐
│ while True: │ │ │
│ response = activity───┼─────────>│ llm_call(messages) │
│ │<─────────┼ → Anthropic API │
│ if no tool_calls: │ │ │
│ return text │ │ │
│ │ │ │
│ gather( │ │ │
│ activity(tool1) ────┼─────────>│ get_weather(city) │
│ activity(tool2) ────┼─────────>│ get_population(city)│
│ ) │<─────────┼ → plain functions │
└─────────────────────────┘ └──────────────────────┘
```

On crash/restart, Temporal replays activity results from its event history.
The workflow re-executes deterministically — each `execute_activity()` call
returns the cached result instead of re-running the I/O.

## Setup

```bash
# 1. Install & start Temporal
brew install temporal
temporal server start-dev

# 2. Install deps
cd examples/temporal-durable
uv sync

# 3. Set API key
export AI_GATEWAY_API_KEY=...

# 4. Run
uv run python main.py
```
62 changes: 62 additions & 0 deletions examples/temporal-durable/activities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Temporal activities — all real I/O lives here.

Each tool is its own activity with a plain function signature.
The LLM activity uses ToolSchema (no dummy fn) and llm.buffer()
(no manual drain loop).
"""

from __future__ import annotations

import os
from dataclasses import dataclass
from typing import Any

from temporalio import activity

import vercel_ai_sdk as ai
from vercel_ai_sdk.anthropic import AnthropicModel


# ── Tool activities (one per tool, plain functions) ───────────────


@activity.defn(name="get_weather")
async def get_weather_activity(city: str) -> str:
return f"Sunny, 72F in {city}"


@activity.defn(name="get_population")
async def get_population_activity(city: str) -> int:
return {"new york": 8_336_817, "los angeles": 3_979_576}.get(
city.lower(), 1_000_000
)


# ── LLM activity ─────────────────────────────────────────────────


@dataclass
class LLMCallParams:
messages: list[dict[str, Any]]
tool_schemas: list[dict[str, Any]]


@dataclass
class LLMCallResult:
message: dict[str, Any] # serialized ai.Message


@activity.defn(name="llm_call")
async def llm_call_activity(params: LLMCallParams) -> LLMCallResult:
"""Call the LLM, drain the stream, return the final message."""
llm = AnthropicModel(
model="anthropic/claude-sonnet-4",
base_url="https://ai-gateway.vercel.sh",
api_key=os.environ.get("AI_GATEWAY_API_KEY"),
)

messages = [ai.Message.model_validate(m) for m in params.messages]
tools = [ai.ToolSchema.model_validate(t) for t in params.tool_schemas]

result = await llm.buffer(messages, tools)
return LLMCallResult(message=result.model_dump())
55 changes: 55 additions & 0 deletions examples/temporal-durable/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Entry point — starts a Temporal worker and executes the agent workflow.

Prerequisites:
1. Temporal dev server: temporal server start-dev
2. AI_GATEWAY_API_KEY environment variable set

Usage:
uv run python main.py
uv run python main.py "What is the weather in Tokyo?"
"""

from __future__ import annotations

import asyncio
import sys
import uuid

from temporalio.client import Client
from temporalio.worker import Worker

from activities import get_weather_activity, get_population_activity, llm_call_activity
from workflow import AgentWorkflow

TASK_QUEUE = "agent-durable"


async def main(user_query: str) -> None:
client = await Client.connect("localhost:7233")

async with Worker(
client,
task_queue=TASK_QUEUE,
workflows=[AgentWorkflow],
activities=[llm_call_activity, get_weather_activity, get_population_activity],
):
workflow_id = f"agent-durable-{uuid.uuid4().hex[:8]}"
print(f"Workflow {workflow_id}")
print(f"Query: {user_query}\n")

result = await client.execute_workflow(
AgentWorkflow.run,
user_query,
id=workflow_id,
task_queue=TASK_QUEUE,
)
print(result)


if __name__ == "__main__":
query = (
sys.argv[1]
if len(sys.argv) > 1
else ("What's the weather and population of New York and Los Angeles?")
)
asyncio.run(main(query))
12 changes: 12 additions & 0 deletions examples/temporal-durable/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[project]
name = "temporal-durable"
version = "0.1.0"
description = "Durable agent execution with Temporal"
requires-python = ">=3.12"
dependencies = [
"vercel-ai-sdk",
"temporalio>=1.9.0",
]

[tool.uv.sources]
vercel-ai-sdk = { path = "../..", editable = true }
Loading