Skip to main content

tool_calling_and_web_searching/
tool_calling_and_web_searching.rs

1//! Parallel tool-calling agents on the durable tool-call primitive.
2//!
3//! This example shows the agent pattern the crate is meant to support, built on
4//! [`langcontinuation::Continuation::tool_call`] and a registered
5//! [`langcontinuation::Tool`]:
6//!
7//! - The entrypoint forks into two independent research branches.
8//! - One branch researches LangChain with Anthropic web search.
9//! - The other branch inspects this repository through a client-side text editor
10//!   tool.
11//! - Both branches write required artifacts into `/work`.
12//! - The join step receives both reports as typed environment values and asks
13//!   the model to synthesize a final recommendation.
14//!
15//! The text editor is a single [`Tool`] implementation registered once with
16//! [`Trampoline::register_tool`]. The workflow never mounts a filesystem, parses
17//! tool JSON, or dispatches tool commands: that all lives in the tool. Each
18//! receiver just appends the model turn to its conversation and, when the model
19//! calls tools, returns a [`Continuation::tool_call`] suspension that the runtime
20//! resolves through the registry. Filesystem policy is explicit in the tool:
21//! `/repo` is read-only, `/work` is writable scratch space.
22//!
23//! Run it live:
24//!
25//! ```sh
26//! cargo run --example tool_calling_and_web_searching
27//! ```
28//!
29//! Or, with `DATABASE_URL` configured, through the Postgres-backed batch
30//! executor:
31//!
32//! ```sh
33//! cargo run --features batch --example tool_calling_and_web_searching -- --batch
34//! ```
35
36use std::{
37    ffi::OsStr,
38    future::Future,
39    path::{Path, PathBuf},
40    pin::Pin,
41};
42
43use claudius::{
44    Anthropic, ContentBlock, DocumentBlock, FileSystem, KnownModel, Message, MessageCreateParams,
45    MessageParam, MessageRole, Model, MountHierarchy, Permissions, PlainTextSource, StopReason,
46    TextBlock, ToolResultBlock, ToolTextEditor20250728, ToolUnionParam, ToolUseBlock,
47    WebSearchTool20250305,
48};
49use langcontinuation::{
50    __print_llm_output, Continuation, ContinuationChoice, ForkBranch, Tool, ToolCallId, Trampoline,
51    Workflow, client_tool_uses, from_env, generate_goto, live::Executor, push_env,
52};
53use serde::{Deserialize, Serialize};
54use utf8path::Path as Utf8Path;
55
56const CONFIG_KEY: &str = "config: RunConfig";
57const LANGCHAIN_REPORT_KEY: &str = "langchain_report: BranchOutput";
58const LANGCONTINUATION_REPORT_KEY: &str = "langcontinuation_report: BranchOutput";
59const FINAL_RECOMMENDATION_KEY: &str = "final_recommendation: BranchOutput";
60const BRANCH_TOOL_CALLS: u32 = 8;
61const JOIN_TOOL_CALLS: u32 = 4;
62
63#[derive(Clone, Debug, Deserialize, Serialize)]
64struct RunConfig {
65    user_ask: String,
66    repo_root: String,
67    root_mount: String,
68    work_root: String,
69}
70
71/// Durable per-loop state. The mount roots are deliberately absent: the
72/// registered [`TextEditorTool`] owns filesystem configuration, so only the
73/// conversation, the required artifact, and the tool budget are durable here.
74#[derive(Clone, Debug, Deserialize, Serialize)]
75struct ToolLoopState {
76    messages: Vec<MessageParam>,
77    work_root: String,
78    required_file: String,
79    max_tool_calls: u32,
80    tool_calls_used: u32,
81    system_prompt: String,
82}
83
84#[derive(Clone, Debug, Deserialize, Serialize)]
85struct LangChainLoop {
86    state: ToolLoopState,
87}
88
89#[derive(Clone, Debug, Deserialize, Serialize)]
90struct LangContinuationLoop {
91    state: ToolLoopState,
92}
93
94#[derive(Clone, Debug, Deserialize, Serialize)]
95struct JoinLoop {
96    state: ToolLoopState,
97}
98
99/// Names a `Vec<ToolResultBlock>` for the macro key convention. Distinct
100/// parameter names keep each branch's results under a distinct environment key,
101/// so fork/join never merges conflicting writes.
102#[derive(Clone, Debug, Deserialize, Serialize)]
103struct ToolResults(Vec<ToolResultBlock>);
104
105#[derive(Clone, Debug, Deserialize, Serialize)]
106struct BranchOutput {
107    path: String,
108    text: String,
109}
110
111/// The single client-side tool for this demo: a mount-backed text editor.
112///
113/// All filesystem policy lives here. The tool holds the run's mount roots,
114/// rebuilds a [`MountHierarchy`] per call, parses the text-editor command, and
115/// dispatches it. The workflow code below never touches a filesystem.
116struct TextEditorTool {
117    repo_root: String,
118    root_mount: String,
119    work_root: String,
120}
121
122impl TextEditorTool {
123    fn new(config: &RunConfig) -> Self {
124        Self {
125            repo_root: config.repo_root.clone(),
126            root_mount: config.root_mount.clone(),
127            work_root: config.work_root.clone(),
128        }
129    }
130
131    fn mount(&self) -> Result<MountHierarchy, std::io::Error> {
132        let mut hierarchy = MountHierarchy::default();
133        hierarchy
134            .mount(
135                "/".into(),
136                Permissions::ReadOnly,
137                Utf8Path::from(self.root_mount.clone()).into_owned(),
138            )
139            .map_err(std::io::Error::other)?;
140        hierarchy
141            .mount(
142                "/repo".into(),
143                Permissions::ReadOnly,
144                Utf8Path::from(self.repo_root.clone()).into_owned(),
145            )
146            .map_err(std::io::Error::other)?;
147        hierarchy
148            .mount(
149                "/work".into(),
150                Permissions::ReadWrite,
151                Utf8Path::from(self.work_root.clone()).into_owned(),
152            )
153            .map_err(std::io::Error::other)?;
154        Ok(hierarchy)
155    }
156}
157
158impl Tool for TextEditorTool {
159    fn name(&self) -> String {
160        "str_replace_based_edit_tool".to_string()
161    }
162
163    fn to_param(&self) -> ToolUnionParam {
164        ToolUnionParam::TextEditor20250728(
165            ToolTextEditor20250728::new().with_max_characters(12_000),
166        )
167    }
168
169    fn call<'a>(
170        &'a self,
171        id: ToolCallId,
172        tool_use: &'a ToolUseBlock,
173    ) -> Pin<Box<dyn Future<Output = ToolResultBlock> + Send + 'a>> {
174        let tool_use_id = tool_use.id.clone();
175        let input = tool_use.input.clone();
176        Box::pin(async move {
177            // `id` is the durable dedupe key; filesystem edits here are
178            // effectively idempotent, so the demo only logs it.
179            let _ = id;
180            let block = ToolResultBlock::new(tool_use_id);
181            let result = match self.mount() {
182                Ok(filesystem) => dispatch_text_editor(&filesystem, &input).await,
183                Err(err) => Err(err),
184            };
185
186            match result {
187                Ok(output) => block.with_string_content(output),
188                Err(err) => block.with_string_content(err.to_string()).with_error(true),
189            }
190        })
191    }
192}
193
194async fn dispatch_text_editor(
195    filesystem: &MountHierarchy,
196    input: &serde_json::Value,
197) -> Result<String, std::io::Error> {
198    async {
199        #[derive(Deserialize)]
200        struct Command {
201            command: String,
202        }
203        let command: Command = serde_json::from_value(input.clone())?;
204        match command.command.as_str() {
205            "view" => {
206                #[derive(Deserialize)]
207                struct ViewArgs {
208                    path: String,
209                    view_range: Option<(u32, u32)>,
210                }
211                let args: ViewArgs = serde_json::from_value(input.clone())?;
212                filesystem.view(&args.path, args.view_range).await
213            }
214            "str_replace" => {
215                #[derive(Deserialize)]
216                struct StrReplaceArgs {
217                    path: String,
218                    old_str: String,
219                    new_str: Option<String>,
220                }
221                let args: StrReplaceArgs = serde_json::from_value(input.clone())?;
222                filesystem
223                    .str_replace(
224                        &args.path,
225                        &args.old_str,
226                        args.new_str.as_deref().unwrap_or(""),
227                    )
228                    .await
229            }
230            "insert" => {
231                #[derive(Deserialize)]
232                struct InsertArgs {
233                    path: String,
234                    insert_line: u32,
235                    insert_text: Option<String>,
236                    new_str: Option<String>,
237                }
238                let args: InsertArgs = serde_json::from_value(input.clone())?;
239                let insert_text = args.insert_text.or(args.new_str).ok_or_else(|| {
240                    std::io::Error::new(
241                        std::io::ErrorKind::InvalidInput,
242                        "insert requires insert_text or new_str",
243                    )
244                })?;
245                filesystem
246                    .insert(&args.path, args.insert_line, &insert_text)
247                    .await
248            }
249            "create" => {
250                #[derive(Deserialize)]
251                struct CreateArgs {
252                    path: String,
253                    file_text: String,
254                }
255                let args: CreateArgs = serde_json::from_value(input.clone())?;
256                filesystem.create(&args.path, &args.file_text).await
257            }
258            other => Err(std::io::Error::new(
259                std::io::ErrorKind::Unsupported,
260                format!("{other} is not supported by the text editor demo"),
261            )),
262        }
263    }
264    .await
265}
266
267generate_goto! {
268    fn entrypoint(workflow: &mut Workflow, config: RunConfig, continuation: Continuation) -> Result<ContinuationChoice, handled::SError> {
269        let run_id = workflow.run_id().to_string();
270        let _ = config;
271        Ok(continuation.fork_join(
272            ForkBranch::new(format!("{run_id}:langchain"), "start_langchain_branch"),
273            ForkBranch::new(format!("{run_id}:langcontinuation"), "start_langcontinuation_branch"),
274            "start_join",
275        ))
276    }
277}
278
279generate_goto! {
280    fn start_langchain_branch(workflow: &mut Workflow, config: RunConfig, continuation: Continuation) -> Result<ContinuationChoice, handled::SError> {
281        let state = branch_state(
282            &config,
283            "/work/langchain.md",
284            "LangChain research branch",
285            format!(
286                "Research LangChain for this user request: `{}`.\n\nUse web_search for current public information about LangChain. Use the text editor to create `/work/langchain.md` with a concise report covering what LangChain is, why teams use it, its strengths, and its practical limits.\n\nYou may make at most {BRANCH_TOOL_CALLS} text_editor tool calls in this branch. The web_search tool is available for web research. Do not stop until `/work/langchain.md` exists.",
287                config.user_ask
288            ),
289        );
290        push_env!(workflow.langchain_loop: LangChainLoop = LangChainLoop { state });
291        Ok(continuation.call("ask_langchain"))
292    }
293}
294
295generate_goto! {
296    fn start_langcontinuation_branch(workflow: &mut Workflow, config: RunConfig, continuation: Continuation) -> Result<ContinuationChoice, handled::SError> {
297        let state = branch_state(
298            &config,
299            "/work/langcontinuation.md",
300            "langcontinuation repository branch",
301            format!(
302                "Inspect the langcontinuation repository for this user request: `{}`.\n\nUse text_editor view/search calls against `/repo` to understand the crate. You may also use web_search if external context is useful, but the repository is the source of truth. Create `/work/langcontinuation.md` with a concise report covering what langcontinuation is, how its continuation model works, what is implemented now, and what promise the tool-calling example should demonstrate.\n\nYou may make at most {BRANCH_TOOL_CALLS} text_editor tool calls in this branch. Do not stop until `/work/langcontinuation.md` exists.",
303                config.user_ask
304            ),
305        );
306        push_env!(workflow.langcontinuation_loop: LangContinuationLoop = LangContinuationLoop { state });
307        Ok(continuation.call("ask_langcontinuation"))
308    }
309}
310
311generate_goto! {
312    fn ask_langchain(workflow: &mut Workflow, langchain_loop: LangChainLoop, continuation: Continuation) -> Result<ContinuationChoice, handled::SError> {
313        let _ = workflow;
314        Ok(continuation.anthropic(
315            "anthropic",
316            anthropic_request(&langchain_loop.state),
317            "langchain_response: Message",
318            "handle_langchain_response",
319        ))
320    }
321}
322
323generate_goto! {
324    fn ask_langcontinuation(workflow: &mut Workflow, langcontinuation_loop: LangContinuationLoop, continuation: Continuation) -> Result<ContinuationChoice, handled::SError> {
325        let _ = workflow;
326        Ok(continuation.anthropic(
327            "anthropic",
328            anthropic_request(&langcontinuation_loop.state),
329            "langcontinuation_response: Message",
330            "handle_langcontinuation_response",
331        ))
332    }
333}
334
335generate_goto! {
336    fn handle_langchain_response(
337        workflow: &mut Workflow,
338        langchain_loop: LangChainLoop,
339        langchain_response: Message,
340        continuation: Continuation
341    ) -> Result<ContinuationChoice, handled::SError> {
342        let (state, choice) = receive_response(
343            workflow,
344            langchain_loop.state,
345            langchain_response,
346            continuation,
347            LANGCHAIN_REPORT_KEY,
348            "langchain_results: ToolResults",
349            "after_langchain_tools",
350        )?;
351        push_env!(workflow.langchain_loop: LangChainLoop = LangChainLoop { state });
352        Ok(choice)
353    }
354}
355
356generate_goto! {
357    fn handle_langcontinuation_response(
358        workflow: &mut Workflow,
359        langcontinuation_loop: LangContinuationLoop,
360        langcontinuation_response: Message,
361        continuation: Continuation
362    ) -> Result<ContinuationChoice, handled::SError> {
363        let (state, choice) = receive_response(
364            workflow,
365            langcontinuation_loop.state,
366            langcontinuation_response,
367            continuation,
368            LANGCONTINUATION_REPORT_KEY,
369            "langcontinuation_results: ToolResults",
370            "after_langcontinuation_tools",
371        )?;
372        push_env!(workflow.langcontinuation_loop: LangContinuationLoop = LangContinuationLoop { state });
373        Ok(choice)
374    }
375}
376
377generate_goto! {
378    fn after_langchain_tools(
379        workflow: &mut Workflow,
380        langchain_loop: LangChainLoop,
381        langchain_results: ToolResults,
382        continuation: Continuation
383    ) -> Result<ContinuationChoice, handled::SError> {
384        let mut state = langchain_loop.state;
385        thread_tool_results(&mut state, langchain_results);
386        push_env!(workflow.langchain_loop: LangChainLoop = LangChainLoop { state });
387        Ok(continuation.call("ask_langchain"))
388    }
389}
390
391generate_goto! {
392    fn after_langcontinuation_tools(
393        workflow: &mut Workflow,
394        langcontinuation_loop: LangContinuationLoop,
395        langcontinuation_results: ToolResults,
396        continuation: Continuation
397    ) -> Result<ContinuationChoice, handled::SError> {
398        let mut state = langcontinuation_loop.state;
399        thread_tool_results(&mut state, langcontinuation_results);
400        push_env!(workflow.langcontinuation_loop: LangContinuationLoop = LangContinuationLoop { state });
401        Ok(continuation.call("ask_langcontinuation"))
402    }
403}
404
405generate_goto! {
406    fn start_join(
407        workflow: &mut Workflow,
408        langchain_report: BranchOutput,
409        langcontinuation_report: BranchOutput,
410        continuation: Continuation
411    ) -> Result<ContinuationChoice, handled::SError> {
412        let config: RunConfig = workflow
413            .from_env(CONFIG_KEY)
414            .map_err(|err| langcontinuation::env_decode_error(CONFIG_KEY, err))?
415            .ok_or_else(|| langcontinuation::missing_env_error(CONFIG_KEY))?;
416
417        let instruction = format!(
418            "Synthesize these two branch reports for the user request: `{}`.\n\nUse the text editor to create `/work/recommendation.md`. Recommend the highest-impact improvement to make the repository's promise land better, and explain why this live fork/join tool-calling demo is the right next investment. You may make at most {JOIN_TOOL_CALLS} text_editor tool calls in this join step. Do not stop until `/work/recommendation.md` exists.",
419            config.user_ask
420        );
421        let messages = vec![MessageParam::new_with_blocks(
422            vec![
423                ContentBlock::Text(TextBlock::new(instruction)),
424                ContentBlock::Document(
425                    DocumentBlock::new_with_plain_text(PlainTextSource::new(langchain_report.text))
426                        .with_title("LangChain branch report".to_string()),
427                ),
428                ContentBlock::Document(
429                    DocumentBlock::new_with_plain_text(PlainTextSource::new(
430                        langcontinuation_report.text,
431                    ))
432                    .with_title("langcontinuation branch report".to_string()),
433                ),
434            ],
435            MessageRole::User,
436        )];
437        let state = ToolLoopState {
438            messages,
439            work_root: config.work_root,
440            required_file: "/work/recommendation.md".to_string(),
441            max_tool_calls: JOIN_TOOL_CALLS,
442            tool_calls_used: 0,
443            system_prompt: system_prompt("join synthesis"),
444        };
445
446        push_env!(workflow.join_loop: JoinLoop = JoinLoop { state });
447        Ok(continuation.call("ask_join"))
448    }
449}
450
451generate_goto! {
452    fn ask_join(workflow: &mut Workflow, join_loop: JoinLoop, continuation: Continuation) -> Result<ContinuationChoice, handled::SError> {
453        let _ = workflow;
454        Ok(continuation.anthropic(
455            "anthropic",
456            anthropic_request(&join_loop.state),
457            "join_response: Message",
458            "handle_join_response",
459        ))
460    }
461}
462
463generate_goto! {
464    fn handle_join_response(
465        workflow: &mut Workflow,
466        join_loop: JoinLoop,
467        join_response: Message,
468        continuation: Continuation
469    ) -> Result<ContinuationChoice, handled::SError> {
470        let (state, choice) = receive_response(
471            workflow,
472            join_loop.state,
473            join_response,
474            continuation,
475            FINAL_RECOMMENDATION_KEY,
476            "join_results: ToolResults",
477            "after_join_tools",
478        )?;
479        push_env!(workflow.join_loop: JoinLoop = JoinLoop { state });
480        Ok(choice)
481    }
482}
483
484generate_goto! {
485    fn after_join_tools(
486        workflow: &mut Workflow,
487        join_loop: JoinLoop,
488        join_results: ToolResults,
489        continuation: Continuation
490    ) -> Result<ContinuationChoice, handled::SError> {
491        let mut state = join_loop.state;
492        thread_tool_results(&mut state, join_results);
493        push_env!(workflow.join_loop: JoinLoop = JoinLoop { state });
494        Ok(continuation.call("ask_join"))
495    }
496}
497
498fn branch_state(
499    config: &RunConfig,
500    required_file: &str,
501    branch_name: &str,
502    task: String,
503) -> ToolLoopState {
504    ToolLoopState {
505        messages: vec![MessageParam::user(task)],
506        work_root: config.work_root.clone(),
507        required_file: required_file.to_string(),
508        max_tool_calls: BRANCH_TOOL_CALLS,
509        tool_calls_used: 0,
510        system_prompt: system_prompt(branch_name),
511    }
512}
513
514fn system_prompt(branch_name: &str) -> String {
515    let prompt = format!(
516        "You are running inside the langcontinuation tool-calling demo, in the {branch_name} step.\n\nAvailable filesystem mounts:\n- `/repo` is the langcontinuation repository mounted read-only.\n- `/work` is writable scratch space for report artifacts.\n\nUse web_search for external facts and the text editor for filesystem reads/writes. Text editor calls are executed by the Rust workflow; web_search is Anthropic server-side. Respect the explicit tool-call budget in the user message. When you are done, stop only after the required `/work/...` file exists."
517    );
518    prompt
519}
520
521fn anthropic_request(state: &ToolLoopState) -> MessageCreateParams {
522    // The text editor is a registered client-side tool; web search is server
523    // side. Both are advertised to the model here.
524
525    MessageCreateParams::new(
526        4096,
527        state.messages.clone(),
528        Model::Known(KnownModel::ClaudeSonnet45),
529    )
530    .with_system_string(state.system_prompt.clone())
531    .with_tools(vec![
532        ToolUnionParam::TextEditor20250728(
533            ToolTextEditor20250728::new().with_max_characters(12_000),
534        ),
535        ToolUnionParam::WebSearch20250305(WebSearchTool20250305::new().with_max_uses(4)),
536    ])
537}
538
539/// Receive a model response and choose the next durable step.
540///
541/// This is the whole agent loop body now: append the assistant turn, and either
542/// suspend to run the called tools (the runtime resolves them through the
543/// registry) or, when the model is done, read the required artifact and halt.
544/// There is no mounting, JSON parsing, or tool dispatch here — that lives in
545/// [`TextEditorTool`].
546#[allow(clippy::too_many_arguments)]
547fn receive_response(
548    workflow: &mut Workflow,
549    mut state: ToolLoopState,
550    response: Message,
551    continuation: Continuation,
552    output_key: &str,
553    results_key: &str,
554    after_function: &str,
555) -> Result<(ToolLoopState, ContinuationChoice), handled::SError> {
556    (|| {
557        state.messages.push(MessageParam::from(response.clone()));
558
559        let tool_uses = client_tool_uses(&response);
560        if !tool_uses.is_empty() {
561            let requested = tool_uses.len() as u32;
562            if state.tool_calls_used.saturating_add(requested) > state.max_tool_calls {
563                return Err(demo_error(
564                    "tool-call-budget-exhausted",
565                    "model exceeded the text_editor tool-call budget",
566                )
567                .with_string_field("required_file", &state.required_file)
568                .with_atom_field("max_tool_calls", state.max_tool_calls)
569                .with_atom_field("tool_calls_used", state.tool_calls_used)
570                .with_atom_field("requested_tool_calls", requested));
571            }
572            state.tool_calls_used += requested;
573            // Suspend: the runtime runs each tool_use through the registered
574            // tool and resumes at `after_function` with the results.
575            return Ok((
576                state,
577                continuation.tool_call(tool_uses, results_key, after_function),
578            ));
579        }
580
581        if matches!(response.stop_reason, Some(StopReason::ToolUse)) {
582            return Err(demo_error(
583                "unsupported-tool-use",
584                "model requested tool use but no client-side text editor tool call was present",
585            )
586            .with_string_field("required_file", &state.required_file));
587        }
588
589        let output = read_required_output(&state)?;
590        workflow
591            .into_env(output_key, output)
592            .map_err(|err| langcontinuation::env_encode_error(output_key, err))?;
593        Ok((state, continuation.halt()))
594    })()
595}
596
597/// Thread tool results back into the conversation as the next user message.
598fn thread_tool_results(state: &mut ToolLoopState, results: ToolResults) {
599    let blocks: Vec<ContentBlock> = results
600        .0
601        .into_iter()
602        .map(ContentBlock::ToolResult)
603        .collect();
604    state
605        .messages
606        .push(MessageParam::new_with_blocks(blocks, MessageRole::User));
607}
608
609fn read_required_output(state: &ToolLoopState) -> Result<BranchOutput, handled::SError> {
610    (|| {
611        let path = virtual_work_path(&state.work_root, &state.required_file)?;
612        let text = std::fs::read_to_string(&path).map_err(|err| {
613            demo_error(
614                "required-output-missing",
615                "model stopped before creating the required output file",
616            )
617            .with_string_field("required_file", &state.required_file)
618            .with_string_field("source", &err.to_string())
619        })?;
620        if text.trim().is_empty() {
621            return Err(demo_error(
622                "required-output-empty",
623                "model created the required output file but left it empty",
624            )
625            .with_string_field("required_file", &state.required_file));
626        }
627        Ok(BranchOutput {
628            path: path.display().to_string(),
629            text,
630        })
631    })()
632}
633
634fn virtual_work_path(work_root: &str, virtual_path: &str) -> Result<PathBuf, handled::SError> {
635    (|| {
636        let relative = virtual_path.strip_prefix("/work/").ok_or_else(|| {
637            demo_error(
638                "invalid-required-file",
639                "required output path must be inside the /work mount",
640            )
641            .with_string_field("required_file", virtual_path)
642        })?;
643        Ok(Path::new(work_root).join(relative))
644    })()
645}
646
647fn demo_error(code: &str, message: &str) -> handled::SError {
648    handled::SError::new("tool-calling-and-web-searching")
649        .with_code(code)
650        .with_message(message)
651}
652
653fn register_workflow(config: &RunConfig) -> Trampoline {
654    let mut trampoline = Trampoline::default();
655    trampoline.register("entrypoint", entrypoint);
656    trampoline.register("start_langchain_branch", start_langchain_branch);
657    trampoline.register(
658        "start_langcontinuation_branch",
659        start_langcontinuation_branch,
660    );
661    trampoline.register("ask_langchain", ask_langchain);
662    trampoline.register("ask_langcontinuation", ask_langcontinuation);
663    trampoline.register("handle_langchain_response", handle_langchain_response);
664    trampoline.register(
665        "handle_langcontinuation_response",
666        handle_langcontinuation_response,
667    );
668    trampoline.register("after_langchain_tools", after_langchain_tools);
669    trampoline.register("after_langcontinuation_tools", after_langcontinuation_tools);
670    trampoline.register("start_join", start_join);
671    trampoline.register("ask_join", ask_join);
672    trampoline.register("handle_join_response", handle_join_response);
673    trampoline.register("after_join_tools", after_join_tools);
674    // One registered tool serves every branch and the join.
675    trampoline.register_tool(TextEditorTool::new(config));
676    trampoline
677}
678
679#[cfg(feature = "batch")]
680async fn run_batch_executor(
681    trampoline: Trampoline,
682    workflow: Workflow,
683    anthropic: Anthropic,
684    database_url: &str,
685) -> Result<Workflow, Box<dyn std::error::Error>> {
686    let result = async {
687        let run_id = workflow.run_id().to_string();
688        let pool = sqlx::PgPool::connect(database_url).await?;
689        langcontinuation::batch::migrate(&pool).await?;
690        let executor = langcontinuation::batch::Executor::with_default_config(trampoline, pool)
691            .with_anthropic("anthropic", anthropic);
692        executor.enqueue_workflow(workflow).await?;
693        let _summary = executor.run().await?;
694        let record = executor.load_workflow(&run_id).await?.ok_or_else(|| {
695            demo_error(
696                "batch-workflow-missing",
697                "batch executor did not persist the workflow row",
698            )
699            .with_string_field("run_id", &run_id)
700        })?;
701        match record.status {
702            langcontinuation::batch::WorkflowStatus::Halted => Ok(record.workflow),
703            langcontinuation::batch::WorkflowStatus::Failed => {
704                Err(demo_error("batch-workflow-failed", "batch workflow failed")
705                    .with_string_field("run_id", &run_id)
706                    .with_string_field("source", record.error_sexpr.as_deref().unwrap_or("unknown"))
707                    .into())
708            }
709            status => Err(demo_error(
710                "batch-workflow-incomplete",
711                "batch executor stopped before the workflow halted",
712            )
713            .with_string_field("run_id", &run_id)
714            .with_atom_field("status", status.as_str())
715            .into()),
716        }
717    }
718    .await;
719    result
720}
721
722#[cfg(not(feature = "batch"))]
723async fn run_batch_executor(
724    _trampoline: Trampoline,
725    _workflow: Workflow,
726    _anthropic: Anthropic,
727    _database_url: &str,
728) -> Result<Workflow, Box<dyn std::error::Error>> {
729    Err(demo_error(
730        "batch-feature-disabled",
731        "rebuild the example with `--features batch` to use --batch with DATABASE_URL",
732    )
733    .into())
734}
735
736#[tokio::main]
737async fn main() -> Result<(), Box<dyn std::error::Error>> {
738    async {
739        let batch_requested = std::env::args_os()
740            .skip(1)
741            .any(|arg| arg == OsStr::new("--batch"));
742        let database_url = std::env::var("DATABASE_URL").ok();
743        if batch_requested && database_url.is_some() && !cfg!(feature = "batch") {
744            return Err(demo_error(
745                "batch-feature-disabled",
746                "rebuild the example with `--features batch` to use --batch with DATABASE_URL",
747            )
748            .into());
749        }
750        let run_nanos = std::time::SystemTime::now()
751            .duration_since(std::time::UNIX_EPOCH)?
752            .as_nanos();
753        let run_id = format!("tool-demo-{}-{run_nanos}", std::process::id());
754        let repo_root = std::env::current_dir()?;
755        let demo_root = repo_root
756            .join("target")
757            .join("langcontinuation-tool-demo")
758            .join(&run_id);
759        let root_mount = demo_root.join("root");
760        let work_root = demo_root.join("work");
761        std::fs::create_dir_all(&root_mount)?;
762        std::fs::create_dir_all(&work_root)?;
763
764        let config = RunConfig {
765            user_ask: "Tell me about langchain and langcontinuation?".to_string(),
766            repo_root: repo_root.display().to_string(),
767            root_mount: root_mount.display().to_string(),
768            work_root: work_root.display().to_string(),
769        };
770
771        let mut workflow = Workflow::new(run_id, "entrypoint");
772        push_env!(workflow.config: RunConfig = config.clone());
773
774        let trampoline = register_workflow(&config);
775
776        let anthropic = Anthropic::new(None)?;
777        let workflow = match (batch_requested, database_url.as_deref()) {
778            (true, Some(database_url)) => {
779                run_batch_executor(trampoline, workflow, anthropic, database_url).await?
780            }
781            _ => {
782                let executor = Executor::new(trampoline).with_anthropic("anthropic", anthropic);
783                executor.run_workflow(workflow).await?
784            }
785        };
786
787        from_env!(let final_recommendation: BranchOutput = workflow.lookup());
788        __print_llm_output(&final_recommendation.text);
789        println!("\nWrote {}", final_recommendation.path);
790        Ok(())
791    }
792    .await
793}