Skip to content

Commit 8c80f76

Browse files
hikejsclaude
andcommitted
feat: add Orchestrator bindings to Node.js SDK
- Add Orchestrator, SubAgentConfig, SubAgentHandle classes - Add TypeScript definitions with JSDoc examples - Add orchestrator.mjs example - Update version to 1.0.4 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3aea7a3 commit 8c80f76

5 files changed

Lines changed: 336 additions & 2 deletions

File tree

sdk/node/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-code-node"
3-
version = "1.0.3"
3+
version = "1.0.4"
44
edition = "2021"
55
authors = ["A3S Lab Team"]
66
license = "MIT"

sdk/node/examples/orchestrator.mjs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* Agent Orchestrator Example for Node.js
3+
*
4+
* Demonstrates main-sub agent coordination with real-time monitoring
5+
* and dynamic control.
6+
*/
7+
8+
const { Orchestrator } = require('@a3s-lab/code');
9+
10+
async function main() {
11+
console.log('=== Agent Orchestrator Node.js Example ===\n');
12+
13+
// Create orchestrator
14+
console.log('1. Creating orchestrator...');
15+
const orch = Orchestrator.create();
16+
console.log(` Active SubAgents: ${orch.activeCount()}\n`);
17+
18+
// Spawn SubAgent 1
19+
console.log('2. Spawning SubAgent 1...');
20+
const config1 = {
21+
agentType: 'general',
22+
description: 'Code analysis task',
23+
prompt: 'Use glob to find all JavaScript files',
24+
permissive: true,
25+
maxSteps: 10
26+
};
27+
const handle1 = orch.spawnSubagent(config1);
28+
console.log(` SubAgent 1 ID: ${handle1.id}\n`);
29+
30+
// Spawn SubAgent 2
31+
console.log('3. Spawning SubAgent 2...');
32+
const config2 = {
33+
agentType: 'explore',
34+
description: 'Documentation check',
35+
prompt: 'Check if README.md exists',
36+
permissive: true,
37+
maxSteps: 5
38+
};
39+
const handle2 = orch.spawnSubagent(config2);
40+
console.log(` SubAgent 2 ID: ${handle2.id}\n`);
41+
42+
// Monitor execution
43+
console.log('4. Monitoring execution...');
44+
for (let i = 0; i < 15; i++) {
45+
await sleep(200);
46+
const active = orch.activeCount();
47+
const state1 = handle1.state();
48+
const state2 = handle2.state();
49+
50+
console.log(` [${(i * 0.2).toFixed(1)}s] Active: ${active}, State1: ${state1.substring(0, 20)}, State2: ${state2.substring(0, 20)}`);
51+
52+
// Test control operations
53+
if (i === 3) {
54+
console.log('\n -> Pausing SubAgent 1...');
55+
handle1.pause();
56+
}
57+
58+
if (i === 6) {
59+
console.log(' -> Resuming SubAgent 1...\n');
60+
handle1.resume();
61+
}
62+
63+
// Check if all done
64+
if (active === 0) {
65+
console.log(`\n All SubAgents completed at ${(i * 0.2).toFixed(1)}s`);
66+
break;
67+
}
68+
}
69+
70+
// Collect results
71+
console.log('\n5. Collecting results...');
72+
try {
73+
const result1 = handle1.wait();
74+
console.log(` SubAgent 1: ${result1.substring(0, 80)}...`);
75+
} catch (e) {
76+
console.log(` SubAgent 1 error: ${e.message}`);
77+
}
78+
79+
try {
80+
const result2 = handle2.wait();
81+
console.log(` SubAgent 2: ${result2.substring(0, 80)}...`);
82+
} catch (e) {
83+
console.log(` SubAgent 2 error: ${e.message}`);
84+
}
85+
86+
// Final status
87+
console.log('\n6. Final status:');
88+
console.log(` Active SubAgents: ${orch.activeCount()}`);
89+
console.log(` SubAgent 1 state: ${handle1.state()}`);
90+
console.log(` SubAgent 2 state: ${handle2.state()}`);
91+
92+
console.log('\n=== Example completed ===');
93+
console.log('\nNote: This example uses placeholder execution.');
94+
console.log('For real LLM execution, integrate AgentLoop in SubAgentWrapper.');
95+
}
96+
97+
function sleep(ms) {
98+
return new Promise(resolve => setTimeout(resolve, ms));
99+
}
100+
101+
main().catch(console.error);

sdk/node/index.d.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,87 @@ export declare class TeamRunner {
569569
runUntilDone(goal: string): Promise<TeamRunResult>;
570570
}
571571

572+
// ============================================================================
573+
// Agent Orchestrator - Main-Sub Agent Coordination
574+
// ============================================================================
575+
576+
/**
577+
* SubAgent configuration for orchestrator.
578+
*/
579+
export interface SubAgentConfig {
580+
/** Agent type (general, explore, plan, etc.) */
581+
agentType: string;
582+
/** Task description */
583+
description: string;
584+
/** Execution prompt */
585+
prompt: string;
586+
/** Enable permissive mode (bypass HITL) */
587+
permissive: boolean;
588+
/** Maximum execution steps */
589+
maxSteps?: number;
590+
/** Execution timeout (milliseconds) */
591+
timeoutMs?: number;
592+
/** Parent SubAgent ID (for nesting) */
593+
parentId?: string;
594+
}
595+
596+
/**
597+
* SubAgent handle for control and monitoring.
598+
*/
599+
export declare class SubAgentHandle {
600+
/** Get SubAgent ID */
601+
readonly id: string;
602+
/** Get current state (non-blocking) */
603+
state(): string;
604+
/** Pause execution */
605+
pause(): void;
606+
/** Resume execution */
607+
resume(): void;
608+
/** Cancel execution */
609+
cancel(): void;
610+
/** Wait for completion and get result */
611+
wait(): string;
612+
}
613+
614+
/**
615+
* Agent Orchestrator for main-sub agent coordination.
616+
*
617+
* Provides real-time monitoring and dynamic control of multiple SubAgents
618+
* through an event-driven architecture.
619+
*
620+
* @example
621+
* ```typescript
622+
* const orch = Orchestrator.create();
623+
*
624+
* const config: SubAgentConfig = {
625+
* agentType: 'general',
626+
* description: 'Find Python files',
627+
* prompt: 'Use glob to find Python files',
628+
* permissive: true,
629+
* maxSteps: 10
630+
* };
631+
*
632+
* const handle = orch.spawnSubagent(config);
633+
* console.log('SubAgent ID:', handle.id);
634+
*
635+
* // Control operations
636+
* handle.pause();
637+
* handle.resume();
638+
*
639+
* // Wait for result
640+
* const result = handle.wait();
641+
* console.log('Result:', result);
642+
* ```
643+
*/
644+
export declare class Orchestrator {
645+
/** Create a new orchestrator with memory-based event communication */
646+
static create(): Orchestrator;
647+
/** Spawn a new SubAgent */
648+
spawnSubagent(config: SubAgentConfig): SubAgentHandle;
649+
/** Get active SubAgent count */
650+
activeCount(): number;
651+
}
652+
572653
// ============================================================================
573654
// Standalone functions
574655
// ============================================================================

sdk/node/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@a3s-lab/code",
3-
"version": "1.0.3",
3+
"version": "1.0.4",
44
"description": "A3S Code - Native AI coding agent library for Node.js",
55
"main": "index.js",
66
"types": "index.d.ts",

sdk/node/src/lib.rs

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ use a3s_code_core::agent_teams::{
2323
TeamRole as RustTeamRole, TeamRunner as RustTeamRunner, TeamTask as RustTeamTask,
2424
TeamTaskBoard as RustTeamTaskBoard,
2525
};
26+
use a3s_code_core::orchestrator::{
27+
AgentOrchestrator as RustOrchestrator, ControlSignal as RustControlSignal,
28+
OrchestratorEvent as RustOrchestratorEvent, SubAgentConfig as RustSubAgentConfig,
29+
SubAgentHandle as RustSubAgentHandle, SubAgentState as RustSubAgentState,
30+
};
2631
use a3s_code_core::config::{
2732
SearchConfig as RustSearchConfig, SearchEngineConfig as RustSearchEngineConfig,
2833
SearchHealthConfig as RustSearchHealthConfig,
@@ -2245,3 +2250,150 @@ impl From<SearchConfig> for RustSearchConfig {
22452250
}
22462251
}
22472252
}
2253+
2254+
// ============================================================================
2255+
// Agent Orchestrator - Main-Sub Agent Coordination
2256+
// ============================================================================
2257+
2258+
/// SubAgent configuration for orchestrator.
2259+
#[napi(object)]
2260+
#[derive(Clone)]
2261+
pub struct SubAgentConfig {
2262+
/// Agent type (general, explore, plan, etc.)
2263+
pub agent_type: String,
2264+
/// Task description
2265+
pub description: String,
2266+
/// Execution prompt
2267+
pub prompt: String,
2268+
/// Enable permissive mode (bypass HITL)
2269+
pub permissive: bool,
2270+
/// Maximum execution steps
2271+
pub max_steps: Option<u32>,
2272+
/// Execution timeout (milliseconds)
2273+
pub timeout_ms: Option<u32>,
2274+
/// Parent SubAgent ID (for nesting)
2275+
pub parent_id: Option<String>,
2276+
}
2277+
2278+
impl From<SubAgentConfig> for RustSubAgentConfig {
2279+
fn from(c: SubAgentConfig) -> Self {
2280+
let mut config = RustSubAgentConfig::new(c.agent_type, c.prompt);
2281+
if !c.description.is_empty() {
2282+
config = config.with_description(c.description);
2283+
}
2284+
config = config.with_permissive(c.permissive);
2285+
if let Some(steps) = c.max_steps {
2286+
config = config.with_max_steps(steps as usize);
2287+
}
2288+
if let Some(timeout) = c.timeout_ms {
2289+
config = config.with_timeout_ms(timeout as u64);
2290+
}
2291+
if let Some(parent) = c.parent_id {
2292+
config = config.with_parent_id(parent);
2293+
}
2294+
config
2295+
}
2296+
}
2297+
2298+
/// SubAgent handle for control and monitoring.
2299+
#[napi]
2300+
pub struct SubAgentHandle {
2301+
inner: Arc<tokio::sync::Mutex<RustSubAgentHandle>>,
2302+
}
2303+
2304+
#[napi]
2305+
impl SubAgentHandle {
2306+
/// Get SubAgent ID
2307+
#[napi(getter)]
2308+
pub fn id(&self) -> napi::Result<String> {
2309+
let handle = self.inner.clone();
2310+
Ok(get_runtime().block_on(async move {
2311+
let h = handle.lock().await;
2312+
h.id.clone()
2313+
}))
2314+
}
2315+
2316+
/// Get current state (non-blocking)
2317+
#[napi]
2318+
pub fn state(&self) -> napi::Result<String> {
2319+
let handle = self.inner.clone();
2320+
Ok(get_runtime().block_on(async move {
2321+
let h = handle.lock().await;
2322+
let state = h.state_async().await;
2323+
format!("{:?}", state)
2324+
}))
2325+
}
2326+
2327+
/// Pause execution
2328+
#[napi]
2329+
pub fn pause(&self) -> napi::Result<()> {
2330+
let handle = self.inner.clone();
2331+
get_runtime()
2332+
.block_on(async move { handle.lock().await.pause().await })
2333+
.map_err(|e| napi::Error::from_reason(format!("Pause failed: {}", e)))
2334+
}
2335+
2336+
/// Resume execution
2337+
#[napi]
2338+
pub fn resume(&self) -> napi::Result<()> {
2339+
let handle = self.inner.clone();
2340+
get_runtime()
2341+
.block_on(async move { handle.lock().await.resume().await })
2342+
.map_err(|e| napi::Error::from_reason(format!("Resume failed: {}", e)))
2343+
}
2344+
2345+
/// Cancel execution
2346+
#[napi]
2347+
pub fn cancel(&self) -> napi::Result<()> {
2348+
let handle = self.inner.clone();
2349+
get_runtime()
2350+
.block_on(async move { handle.lock().await.cancel().await })
2351+
.map_err(|e| napi::Error::from_reason(format!("Cancel failed: {}", e)))
2352+
}
2353+
2354+
/// Wait for completion and get result
2355+
#[napi]
2356+
pub fn wait(&self) -> napi::Result<String> {
2357+
let handle = self.inner.clone();
2358+
get_runtime()
2359+
.block_on(async move { handle.lock().await.wait().await })
2360+
.map_err(|e| napi::Error::from_reason(format!("Wait failed: {}", e)))
2361+
}
2362+
}
2363+
2364+
/// Agent Orchestrator for main-sub agent coordination.
2365+
#[napi]
2366+
pub struct Orchestrator {
2367+
inner: Arc<tokio::sync::Mutex<RustOrchestrator>>,
2368+
}
2369+
2370+
#[napi]
2371+
impl Orchestrator {
2372+
/// Create a new orchestrator with memory-based event communication
2373+
#[napi(factory)]
2374+
pub fn create() -> Self {
2375+
Self {
2376+
inner: Arc::new(tokio::sync::Mutex::new(RustOrchestrator::new_memory())),
2377+
}
2378+
}
2379+
2380+
/// Spawn a new SubAgent
2381+
#[napi]
2382+
pub fn spawn_subagent(&self, config: SubAgentConfig) -> napi::Result<SubAgentHandle> {
2383+
let orch = self.inner.clone();
2384+
let cfg: RustSubAgentConfig = config.into();
2385+
let handle = get_runtime()
2386+
.block_on(async move { orch.lock().await.spawn_subagent(cfg).await })
2387+
.map_err(|e| napi::Error::from_reason(format!("Spawn failed: {}", e)))?;
2388+
Ok(SubAgentHandle {
2389+
inner: Arc::new(tokio::sync::Mutex::new(handle)),
2390+
})
2391+
}
2392+
2393+
/// Get active SubAgent count
2394+
#[napi]
2395+
pub fn active_count(&self) -> napi::Result<u32> {
2396+
let orch = self.inner.clone();
2397+
Ok(get_runtime().block_on(async move { orch.lock().await.active_count().await }) as u32)
2398+
}
2399+
}

0 commit comments

Comments
 (0)