Skip to content

Commit 7377392

Browse files
committed
feat(examples): add external task handler tests for all three SDKs
Demonstrates the Multi-Machine External Task pattern: - Coordinator creates session with Execute lane → External mode - Agent streams prompt, bash/write/edit → ExternalTask objects - Coordinator polls pending_external_tasks(), processes locally - Coordinator completes via complete_external_task() Three test scenarios per SDK: 1. External mode (Execute lane tasks routed to external handler) 2. Hybrid mode (local execution + external notification) 3. Dynamic lane switching (Internal ↔ External ↔ Hybrid) Files: - core/examples/test_external_task_handler.rs (Rust) - sdk/python/examples/test_external_task_handler.py (Python) - sdk/node/examples/test_external_task_handler.js (Node.js) - docs/parallelization.md (updated with external task test references)
1 parent 04e49f7 commit 7377392

4 files changed

Lines changed: 697 additions & 193 deletions

File tree

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
//! External Task Handler Integration Test
2+
//!
3+
//! Demonstrates the Multi-Machine External Task pattern:
4+
//! 1. Coordinator creates a session with Execute lane set to External mode
5+
//! 2. Agent streams a prompt that triggers bash/write/edit tool calls
6+
//! 3. ExternalTaskPending events fire — coordinator polls pending tasks
7+
//! 4. Coordinator processes tasks locally (simulating a remote worker)
8+
//! 5. Coordinator completes tasks via complete_external_task()
9+
//!
10+
//! Run with: cargo run --example test_external_task_handler
11+
12+
use a3s_code_core::agent::AgentEvent;
13+
use a3s_code_core::permissions::PermissionPolicy;
14+
use a3s_code_core::queue::{
15+
ExternalTaskResult, LaneHandlerConfig, SessionLane, TaskHandlerMode,
16+
};
17+
use a3s_code_core::{Agent, SessionOptions, SessionQueueConfig};
18+
use anyhow::Result;
19+
use std::path::PathBuf;
20+
use std::process::Command;
21+
use std::sync::Arc;
22+
use std::time::Instant;
23+
24+
fn find_config() -> Result<PathBuf> {
25+
let home_config = dirs::home_dir()
26+
.map(|h| h.join(".a3s/config.hcl"))
27+
.filter(|p| p.exists());
28+
29+
if let Some(config) = home_config {
30+
return Ok(config);
31+
}
32+
33+
let project_config = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
34+
.parent()
35+
.and_then(|p| p.parent())
36+
.and_then(|p| p.parent())
37+
.map(|p| p.join(".a3s/config.hcl"))
38+
.filter(|p| p.exists());
39+
40+
project_config.ok_or_else(|| anyhow::anyhow!("Config file not found"))
41+
}
42+
43+
/// Simulate a remote worker executing a bash command
44+
fn worker_execute_bash(command: &str, working_dir: &str) -> (bool, String, i32, Option<String>) {
45+
let output = Command::new("sh")
46+
.arg("-c")
47+
.arg(command)
48+
.current_dir(working_dir)
49+
.output();
50+
51+
match output {
52+
Ok(out) => {
53+
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
54+
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
55+
let success = out.status.success();
56+
let exit_code = out.status.code().unwrap_or(1);
57+
let error = if success { None } else { Some(stderr) };
58+
(success, stdout, exit_code, error)
59+
}
60+
Err(e) => (false, String::new(), 1, Some(e.to_string())),
61+
}
62+
}
63+
64+
#[tokio::main]
65+
async fn main() -> Result<()> {
66+
tracing_subscriber::fmt()
67+
.with_env_filter("info,a3s_code_core=info")
68+
.init();
69+
70+
println!("🚀 A3S Code - External Task Handler Integration Test\n");
71+
println!("{}", "=".repeat(80));
72+
73+
let config_path = find_config()?;
74+
println!("📄 Using config: {}", config_path.display());
75+
println!("{}", "=".repeat(80));
76+
77+
let agent = Agent::new(config_path.to_str().unwrap()).await?;
78+
79+
// =========================================================================
80+
// Test 1: External mode — Execute lane tasks routed to external handler
81+
// =========================================================================
82+
println!("\n📦 Test 1: External Task Handler (Execute Lane → External)");
83+
println!("{}", "-".repeat(80));
84+
85+
// 1. Create session with queue enabled
86+
let queue_config = SessionQueueConfig::default()
87+
.with_lane_features()
88+
.with_timeout(60_000);
89+
90+
let mut opts = SessionOptions::default().with_queue_config(queue_config);
91+
opts.permission_checker = Some(Arc::new(PermissionPolicy::permissive()));
92+
let session = agent.session(".", Some(opts))?;
93+
94+
// 2. Route Execute lane to External mode
95+
session
96+
.set_lane_handler(
97+
SessionLane::Execute,
98+
LaneHandlerConfig {
99+
mode: TaskHandlerMode::External,
100+
timeout_ms: 60_000,
101+
},
102+
)
103+
.await;
104+
105+
println!("✓ Session created with Execute lane → External mode");
106+
println!(" Query lane: Internal (read, glob, grep run locally)");
107+
println!(" Execute lane: External (bash, write, edit → ExternalTask)");
108+
println!();
109+
110+
// 3. Stream a prompt that will trigger Execute-lane tools (bash)
111+
let start = Instant::now();
112+
let session_clone = session.clone();
113+
114+
let (mut rx, _handle) = session
115+
.stream(
116+
"Run these bash commands and tell me the results:\n\
117+
1. echo 'Hello from external worker'\n\
118+
2. date '+%Y-%m-%d %H:%M:%S'\n\
119+
3. uname -s",
120+
None,
121+
)
122+
.await?;
123+
124+
// 4. Event loop: handle external tasks as they arrive
125+
let mut external_tasks_processed = 0u32;
126+
let mut text_output = String::new();
127+
128+
while let Some(event) = rx.recv().await {
129+
match event {
130+
AgentEvent::ExternalTaskPending { task_id, command_type, .. } => {
131+
println!(" 📥 ExternalTaskPending: {} ({})", &task_id[..8], command_type);
132+
133+
// Poll all pending tasks
134+
let tasks = session_clone.pending_external_tasks().await;
135+
for task in tasks {
136+
println!(" 🔧 Worker processing: {} → {}", task.command_type, &task.task_id[..8]);
137+
138+
// Execute the task (simulating a remote worker)
139+
let (success, output, exit_code, error) = if task.command_type == "bash" {
140+
let cmd = task.payload["command"].as_str().unwrap_or("echo 'no command'");
141+
let dir = task.payload["working_dir"].as_str().unwrap_or(".");
142+
worker_execute_bash(cmd, dir)
143+
} else {
144+
// For non-bash tasks, return a placeholder
145+
(true, format!("External handler processed: {}", task.command_type), 0, None)
146+
};
147+
148+
println!(" ✅ Worker result: success={}, exit_code={}", success, exit_code);
149+
if !output.is_empty() {
150+
let preview = output.trim();
151+
let preview = if preview.len() > 60 { &preview[..60] } else { preview };
152+
println!(" Output: {}", preview);
153+
}
154+
155+
// Complete the external task
156+
let completed = session_clone
157+
.complete_external_task(
158+
&task.task_id,
159+
ExternalTaskResult {
160+
success,
161+
result: serde_json::json!({
162+
"output": output,
163+
"exit_code": exit_code,
164+
}),
165+
error,
166+
},
167+
)
168+
.await;
169+
170+
if completed {
171+
external_tasks_processed += 1;
172+
println!(" 📤 Task {} completed and returned to agent", &task.task_id[..8]);
173+
}
174+
}
175+
}
176+
AgentEvent::TextDelta { text } => {
177+
text_output.push_str(&text);
178+
print!("{text}");
179+
}
180+
AgentEvent::ToolStart { name, .. } => {
181+
println!("\n 🔨 Tool: {name}");
182+
}
183+
AgentEvent::End { .. } => {
184+
println!();
185+
break;
186+
}
187+
AgentEvent::Error { message } => {
188+
println!("\n ❌ Error: {message}");
189+
break;
190+
}
191+
_ => {}
192+
}
193+
}
194+
195+
let duration = start.elapsed();
196+
197+
println!("\n{}", "-".repeat(80));
198+
println!("📊 Results:");
199+
println!(" Duration: {:.2}s", duration.as_secs_f64());
200+
println!(" External tasks processed: {}", external_tasks_processed);
201+
println!(" Response length: {} chars", text_output.len());
202+
203+
// =========================================================================
204+
// Test 2: Hybrid mode — local execution + external notification
205+
// =========================================================================
206+
println!("\n\n📦 Test 2: Hybrid Mode (Execute Lane → Hybrid)");
207+
println!("{}", "-".repeat(80));
208+
209+
// Switch to Hybrid mode
210+
session
211+
.set_lane_handler(
212+
SessionLane::Execute,
213+
LaneHandlerConfig {
214+
mode: TaskHandlerMode::Hybrid,
215+
timeout_ms: 60_000,
216+
},
217+
)
218+
.await;
219+
220+
println!("✓ Execute lane switched to Hybrid mode");
221+
println!(" Tools execute locally AND emit ExternalTaskPending events");
222+
println!();
223+
224+
let start = Instant::now();
225+
let result = session
226+
.send("Run: echo 'hybrid mode test'", None)
227+
.await?;
228+
229+
let duration = start.elapsed();
230+
println!("✓ Completed in {:.2}s", duration.as_secs_f64());
231+
println!(" Response: {}", &result.text[..result.text.len().min(120)]);
232+
println!(" Tool calls: {}", result.tool_calls_count);
233+
234+
// =========================================================================
235+
// Test 3: Dynamic lane switching
236+
// =========================================================================
237+
println!("\n\n📦 Test 3: Dynamic Lane Switching");
238+
println!("{}", "-".repeat(80));
239+
240+
// Switch back to Internal
241+
session
242+
.set_lane_handler(
243+
SessionLane::Execute,
244+
LaneHandlerConfig {
245+
mode: TaskHandlerMode::Internal,
246+
timeout_ms: 60_000,
247+
},
248+
)
249+
.await;
250+
251+
println!("✓ Execute lane switched back to Internal mode");
252+
253+
let start = Instant::now();
254+
let result = session
255+
.send("Run: echo 'back to internal mode'", None)
256+
.await?;
257+
258+
let duration = start.elapsed();
259+
println!("✓ Completed in {:.2}s", duration.as_secs_f64());
260+
println!(" Response: {}", &result.text[..result.text.len().min(120)]);
261+
262+
println!("\n{}", "=".repeat(80));
263+
println!("✅ All external task handler tests completed!");
264+
println!("{}", "=".repeat(80));
265+
266+
Ok(())
267+
}

docs/parallelization.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,3 +227,26 @@ node examples/test_internal_parallel.js
227227
- Serial: ~17s tool execution
228228
- Parallel: ~1.8s tool execution
229229
- Speedup: ~9x for tool execution
230+
231+
## External Task Handler Tests
232+
233+
For Multi-Machine External Task processing (coordinator/worker pattern), see:
234+
235+
```bash
236+
# Rust
237+
cd crates/code
238+
cargo run --example test_external_task_handler
239+
240+
# Python
241+
cd crates/code/sdk/python
242+
python3 examples/test_external_task_handler.py
243+
244+
# Node.js
245+
cd crates/code/sdk/node
246+
node examples/test_external_task_handler.js
247+
```
248+
249+
These tests demonstrate:
250+
1. Execute lane → External mode (tasks routed to external handler)
251+
2. Hybrid mode (local execution + external notification)
252+
3. Dynamic lane switching at runtime

0 commit comments

Comments
 (0)