temporal-agent-rs
Status:
0.1.0— early; APIs may break before 1.0.
Durable AI agent execution on Temporal using AutoAgents for LLM provider and tool abstractions.
The headline export is AgentWorkflow: a Temporal workflow that runs a
ReAct-style agent loop where every LLM call and every tool invocation is
checkpointed as a Temporal activity. If the worker crashes mid-loop, the
workflow resumes from the last completed activity without re-paying for prior
LLM tokens.
Inspired by Temporal's blog post, Of Course You Can Build Dynamic AI Agents with Temporal.
Architecture
┌──────────────────────── AgentWorkflow (deterministic) ────────────────────────┐
│ │
│ while not done: │
│ ┌──────────┐ ┌─────────────┐ │
│ │ history │ ───────▶│ llm_chat │ ── LlmResponse ─┐ │
│ └──────────┘ │ (activity) │ │ │
│ └─────────────┘ ▼ │
│ ┌────────────────┐ │
│ │ Final? Tools? │ │
│ └────────────────┘ │
│ │ │ │
│ return ▼ │
│ ┌─────────────┐ │
│ │ execute_tool│ │
│ │ (activity) │ × N │
│ └─────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────────────┘
- Workflow = orchestration. Deterministic, replayable, holds the conversation history.
- Activities = the only place LLM providers and tool implementations are called. Non-deterministic, retryable, observable in the Temporal UI.
Features
AgentWorkflowwith a ReAct loop, signals, queries, andcontinue_as_newhistory compaction.AgentActivitieswithllm_chatandexecute_tool.ToolRegistrythat accepts any AutoAgentsArc<dyn ToolT>(use the#[tool]derive macro).AgentWorkerBuilderfor one-line worker setup.- Provider-agnostic: bring your own
Arc<dyn LLMProvider>(OpenAI, Anthropic, Ollama, etc. — anything supported byautoagents_llm). - Human-in-the-loop as a regular tool — the library does not special-case any tool name. See Human-in-the-loop tools.
Prerequisites
- Rust ≥ 1.95 (install via rustup).
- Temporal CLI — for running the examples against a local dev server.
Install with
brew install temporalor follow the official install guide. - Docker — only required to run the integration test suite, which
spins up Temporal and Ollama containers automatically via
testcontainers. - An OpenAI-compatible API key for the examples (set
OPENAI_API_KEY; override the endpoint withOPENAI_BASE_URLto point at Ollama or any other compatible server).
Quick start
use Arc;
use *;
# async
Starting a workflow from a client:
use *;
use ;
let handle = client.start_workflow.await?;
let out: AgentOutput = handle.get_result.await?;
println!;
Running the examples
Two examples ship with the crate:
simple_math_agent— minimal autonomous loop with a singleaddtool.interactive_math_agent— adds anask_usertool so the agent can pause for human input on the worker's stdin.
# Terminal 1: local Temporal dev server (install via `brew install temporal` or temporal.io)
# Simple autonomous agent — single `add` tool, no human-in-the-loop.
# Terminal 2:
OPENAI_API_KEY=sk-...
# Terminal 3:
# Same workflow, but the agent can pause to ask the user for missing info.
# The worker terminal also accepts typed answers on stdin.
OPENAI_API_KEY=sk-...
The Temporal Web UI is at http://localhost:8233. Click into the workflow to
see every llm_chat and execute_tool as a separate activity event.
To witness durability: kill the worker mid-loop (Ctrl-C in terminal 2),
restart it, and the workflow picks up from the last completed activity.
Human-in-the-loop tools
The library treats every tool uniformly — there is no built-in "ask the user"
primitive, no AskUser response variant, no awaiting_user flag baked into
the workflow state. Pause-and-wait semantics are implemented inside the
user's tool, not inside the agent loop.
Why this works without library special-casing
When the LLM emits a tool call, the workflow dispatches it as an
execute_tool activity. If that activity's execute() blocks on a channel
waiting for an external answer, Temporal happily keeps it in-flight up to the
configured start_to_close_timeout (the library default is 1 hour;
override per-deployment if you need longer). When the answer arrives, the
tool returns it as a normal serde_json::Value. The LLM observes it on the
next llm_chat turn as a standard tool result. No special workflow code
needed; the diagram above already covers it.
The pattern
Define a ToolT whose execute() publishes the question to an out-of-band
channel and awaits an answer. Three concrete delivery mechanisms, in order
of increasing production-readiness:
| Mechanism | When to use | Crash-durable? |
|---|---|---|
Stdin → in-process channel (used in examples/interactive_math_agent) |
Local dev, single-user demos | No — pending question lost on worker restart |
| HTTP / Unix socket sidecar | Multi-user UIs, multi-process clients | No — pending question lost unless persisted externally |
Temporal async activity completion (task token + client.complete_activity_…) |
Production | Yes — survives worker restarts |
The example uses the stdin variant for brevity. Production deployments should
use Temporal async activity completion: the tool persists (task_token, question) to a queue/UI, returns ActivityError::WillCompleteAsync, and an
external client completes the activity with the answer later. (This requires
the tool to access the ActivityContext, which today means writing the
activity directly rather than going through our execute_tool dispatcher — a
future library enhancement.)
Tool-side snippet (from the example)
use broadcast;
use ;
use ;
Register it like any other tool:
let = ;
// spawn a stdin reader (or HTTP listener, etc.) that publishes to answer_tx
new
.llm
.tool
.tool
.queue
.build_worker?;
Activity timeout
The default start_to_close_timeout for tool activities is set generously
(1 hour) so that human-in-the-loop tools don't trip the timeout. Tools that
complete quickly are unaffected. See
src/workflow.rs (tool_opts) to tweak it.
Trade-off note
With the in-process answer mechanisms (stdin, local socket), if the worker
process crashes while a question is pending, the answer channel state is
lost. Temporal will retry the execute_tool activity on the new worker; the
tool will reprint the question and ask again. For full crash durability,
use the Temporal async activity completion approach.
Determinism contract for users
When you write tools and provider configs:
- Tools must be side-effect-safe-on-retry by default. Tool errors are reported back to the LLM, not retried by Temporal, but infrastructure errors do retry up to 3 times.
- The LLM provider must be
Send + Sync + 'static.Arc<dyn LLMProvider>already satisfies this for AutoAgents' built-in providers. - Never call your
LLMProvideror yourToolTfrom inside workflow code. The workflow holds tools by name; the only path to invocation is theexecute_toolactivity.
Version compatibility
| Crate | Version |
|---|---|
temporalio-sdk |
0.4.x (prerelease) |
autoagents |
0.3.x |
| Rust edition | 2024 |
| MSRV | 1.95 |
The Temporal Rust SDK is prerelease; API breaks are expected on minor
version bumps. This crate pins to 0.4.x for now.
Contributing
See CONTRIBUTING.md for setup, build/test commands, and PR conventions. By participating you agree to abide by our Code of Conduct.
Changelog
See the Releases page for per-version notes auto-generated from merged PRs.
Security
Please report vulnerabilities privately — see SECURITY.md.
License
MIT