durable_tool_call/
durable_tool_call.rs1use std::{future::Future, pin::Pin};
21
22use claudius::{
23 Anthropic, ContentBlock, KnownModel, Message, MessageCreateParams, MessageParam, MessageRole,
24 ToolResultBlock, ToolUnionParam, ToolUseBlock,
25};
26use langcontinuation::{
27 Tool, ToolCallId, ToolDispatch, Trampoline, Workflow, dispatch_tool_uses, from_env,
28 generate_goto, live::Executor, push_env,
29};
30use serde::{Deserialize, Serialize};
31
32struct CalculatorTool;
35
36impl Tool for CalculatorTool {
37 fn name(&self) -> String {
38 "add".to_string()
39 }
40
41 fn to_param(&self) -> ToolUnionParam {
42 ToolUnionParam::new_custom_tool(
43 "add".to_string(),
44 serde_json::json!({
45 "type": "object",
46 "properties": {
47 "a": { "type": "number" },
48 "b": { "type": "number" }
49 },
50 "required": ["a", "b"]
51 }),
52 )
53 }
54
55 fn call<'a>(
56 &'a self,
57 id: ToolCallId,
58 tool_use: &'a ToolUseBlock,
59 ) -> Pin<Box<dyn Future<Output = ToolResultBlock> + Send + 'a>> {
60 let tool_use_id = tool_use.id.clone();
61 let input = tool_use.input.clone();
62 Box::pin(async move {
63 let _ = id;
67 #[derive(Deserialize)]
68 struct Args {
69 a: f64,
70 b: f64,
71 }
72 match serde_json::from_value::<Args>(input) {
73 Ok(args) => ToolResultBlock::new(tool_use_id)
74 .with_string_content((args.a + args.b).to_string()),
75 Err(err) => ToolResultBlock::new(tool_use_id)
76 .with_string_content(err.to_string())
77 .with_error(true),
78 }
79 })
80 }
81}
82
83#[derive(Clone, Debug, Default, Deserialize, Serialize)]
85struct Conversation {
86 messages: Vec<MessageParam>,
87}
88
89fn request(conversation: &Conversation) -> MessageCreateParams {
90 MessageCreateParams::new(
91 256,
92 conversation.messages.clone(),
93 KnownModel::ClaudeHaiku45.into(),
94 )
95 .with_system(
96 "You can call the `add` tool to add two numbers. Use it for any arithmetic, \
97 then answer the user in plain text."
98 .to_string(),
99 )
100 .with_tools(vec![CalculatorTool.to_param()])
101}
102
103generate_goto! {
104 fn entry(workflow: &mut Workflow, question: String, continuation: Continuation) -> Result<ContinuationChoice, handled::SError> {
105 let mut conversation = Conversation::default();
106 conversation
107 .messages
108 .push(MessageParam::new_with_string(question, MessageRole::User));
109 let req = request(&conversation);
110 push_env!(workflow.conversation: Conversation = conversation);
111 Ok(continuation.anthropic("anthropic", req, "response: Message", "receive"))
112 }
113}
114
115generate_goto! {
116 async fn receive(
117 workflow: &mut Workflow,
118 conversation: Conversation,
119 response: Message,
120 continuation: Continuation
121 ) -> Result<ContinuationChoice, handled::SError> {
122 let mut conversation = conversation;
124 conversation.messages.push(MessageParam::from(response.clone()));
125 push_env!(workflow.conversation: Conversation = conversation);
126
127 match dispatch_tool_uses(continuation, &response, "tool_results: ToolResults", "after_tools") {
129 ToolDispatch::Tools(choice) => Ok(choice),
130 ToolDispatch::Done(continuation) => {
131 let text = response
132 .content
133 .iter()
134 .filter_map(|block| match block {
135 ContentBlock::Text(text) => Some(text.text.clone()),
136 _ => None,
137 })
138 .collect::<Vec<_>>()
139 .join("");
140 push_env!(workflow.answer: String = text);
141 Ok(continuation.halt())
142 }
143 }
144 }
145}
146
147#[derive(Clone, Debug, Deserialize, Serialize)]
150struct ToolResults(Vec<ToolResultBlock>);
151
152generate_goto! {
153 fn after_tools(
154 workflow: &mut Workflow,
155 conversation: Conversation,
156 tool_results: ToolResults,
157 continuation: Continuation
158 ) -> Result<ContinuationChoice, handled::SError> {
159 let mut conversation = conversation;
161 let blocks: Vec<ContentBlock> = tool_results
162 .0
163 .into_iter()
164 .map(ContentBlock::ToolResult)
165 .collect();
166 conversation
167 .messages
168 .push(MessageParam::new_with_blocks(blocks, MessageRole::User));
169 let req = request(&conversation);
170 push_env!(workflow.conversation: Conversation = conversation);
171 Ok(continuation.anthropic("anthropic", req, "response: Message", "receive"))
172 }
173}
174
175#[tokio::main]
176async fn main() -> Result<(), Box<dyn std::error::Error>> {
177 let mut workflow = Workflow::new("durable-tool-call", "entry");
178 push_env!(workflow.question: String = "What is 21 plus 21?".to_string());
179
180 let mut trampoline = Trampoline::default();
181 trampoline.register("entry", entry);
182 trampoline.register("receive", receive);
183 trampoline.register("after_tools", after_tools);
184 trampoline.register_tool(CalculatorTool);
185
186 let anthropic = Anthropic::new(None)?;
187 let executor = Executor::new(trampoline).with_anthropic("anthropic", anthropic);
188 let workflow = executor.run_workflow(workflow).await?;
189
190 from_env!(let answer: String = workflow.lookup());
191 println!("{answer}");
192 Ok(())
193}