temporal_agent_rs/builder.rs
1//! Convenience builder for assembling an agent-aware Temporal worker.
2//!
3//! ```ignore
4//! let mut worker = AgentWorkerBuilder::new(client)
5//! .llm(my_llm_provider)
6//! .tool(Arc::new(MyTool::default()))
7//! .queue("agents")
8//! .build_worker(&runtime)?;
9//! worker.run().await?;
10//! ```
11
12use std::sync::Arc;
13
14use autoagents_core::tool::ToolT;
15use autoagents_llm::LLMProvider;
16use temporalio_client::Client;
17use temporalio_sdk::{Worker, WorkerOptions};
18use temporalio_sdk_core::CoreRuntime;
19use tokio::sync::OnceCell;
20
21use crate::activities::AgentActivities;
22use crate::error::AgentError;
23use crate::state::ToolSchema;
24use crate::tool::{ToolRegistry, ToolRegistryBuilder};
25use crate::workflow::AgentWorkflow;
26
27/// Tool catalog made available to the workflow side.
28///
29/// The workflow is read-only with respect to tools — it only needs their
30/// schemas (name, description, args_schema) to send to the LLM each turn.
31/// Stored in a process-wide `OnceCell` so the workflow's `#[run]` body can
32/// access it deterministically (the catalog is set once at worker start, so
33/// reads return the same value on every replay).
34pub(crate) static WORKER_TOOL_CATALOG: OnceCell<Vec<ToolSchema>> = OnceCell::const_new();
35
36/// Fluent builder for an agent-aware Temporal [`Worker`].
37///
38/// Required calls: [`AgentWorkerBuilder::new`] → [`AgentWorkerBuilder::llm`]
39/// → [`AgentWorkerBuilder::build_worker`]. Tools and queue name are
40/// optional.
41pub struct AgentWorkerBuilder {
42 client: Client,
43 llm: Option<Arc<dyn LLMProvider>>,
44 tools: ToolRegistryBuilder,
45 queue: String,
46}
47
48impl AgentWorkerBuilder {
49 /// Start a new builder bound to an already-connected Temporal [`Client`].
50 #[must_use]
51 pub fn new(client: Client) -> Self {
52 Self {
53 client,
54 llm: None,
55 tools: ToolRegistry::builder(),
56 queue: "agents".to_string(),
57 }
58 }
59
60 /// Required. The LLM provider used by the `llm_chat` activity.
61 #[must_use]
62 pub fn llm(mut self, llm: Arc<dyn LLMProvider>) -> Self {
63 self.llm = Some(llm);
64 self
65 }
66
67 /// Register a tool. Call once per tool the agent should be allowed to use.
68 #[must_use]
69 pub fn tool(mut self, tool: Arc<dyn ToolT>) -> Self {
70 self.tools = self.tools.add(tool);
71 self
72 }
73
74 /// Override the default task queue name (`"agents"`).
75 #[must_use]
76 pub fn queue(mut self, queue: impl Into<String>) -> Self {
77 self.queue = queue.into();
78 self
79 }
80
81 /// Construct the Temporal worker with `AgentWorkflow` + `AgentActivities`
82 /// registered.
83 ///
84 /// Panics if [`Self::llm`] was not called.
85 pub fn build_worker(self, runtime: &CoreRuntime) -> Result<Worker, AgentError> {
86 let llm = self
87 .llm
88 .expect("AgentWorkerBuilder::llm(...) must be called before build_worker()");
89 let registry = self.tools.build();
90
91 // Publish the tool catalog so the workflow can include it in each
92 // llm_chat payload. The library does NOT inject any synthetic
93 // tools — including question-asking. Users who want human-in-the-
94 // loop register their own `ask_user`-style tool whose `execute()`
95 // blocks until an answer is delivered (see the math_agent example).
96 let catalog = registry.to_schemas();
97 // OnceCell::set is fallible if already set; in long-running test
98 // processes we tolerate re-initialization with the same data.
99 let _ = WORKER_TOOL_CATALOG.set(catalog);
100
101 let activities = AgentActivities::new(llm, registry);
102
103 let opts = WorkerOptions::new(&self.queue)
104 .register_workflow::<AgentWorkflow>()
105 .register_activities(activities)
106 .build();
107
108 Worker::new(runtime, self.client, opts)
109 .map_err(|e| AgentError::Other(format!("worker init: {e}")))
110 }
111}