1use 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#[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#[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
111struct 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 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 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#[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 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
597fn 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 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}