Skip to content

Commit 26e29ef

Browse files
ZhiXiao-Linclaude
andcommitted
feat(agent-teams): add TeamRunner with Lead→Worker→Reviewer automation
Integrate AgentTeam with AgentSession execution via new TeamRunner struct. Full multi-agent coordination: Lead decomposes goals into JSON task lists, Workers claim and execute tasks concurrently, Reviewer approves or rejects each result and re-queues rejected tasks for retry. - Add AgentExecutor trait for session-agnostic testability - Add TeamRunner: bind_session, run_until_done, task_board - Add TeamRunResult: done_tasks, rejected_tasks, rounds - Extend TeamConfig with max_rounds (default 10) and poll_interval_ms (default 200) - Modify claim() to also reclaim Rejected tasks (retry support) - Python SDK: PyTeam, PyTeamRunner, PyTeamTaskBoard, PyTeamTask, PyTeamRunResult - Node.js SDK: Team, TeamRunner, TeamTaskBoard, TeamConfig types + index.d.ts - Add example scripts for both SDKs - Update README with full TeamRunner API reference - Bump version to 0.9.1 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e1c6087 commit 26e29ef

16 files changed

Lines changed: 2548 additions & 29 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 159 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -287,27 +287,104 @@ When configured via `AgentConfig::tool_index`, the agent loop extracts the last
287287

288288
### 👥 Agent Teams (Multi-Agent Coordination)
289289

290-
Peer-to-peer multi-agent collaboration through a shared task board and message passing:
290+
Automated Lead → Worker → Reviewer workflows with real LLM execution:
291291

292292
```rust
293-
use a3s_code_core::agent_teams::{AgentTeam, TeamConfig, TeamRole};
293+
use a3s_code_core::{Agent, SessionOptions, agent_teams::{AgentTeam, TeamConfig, TeamRole, TeamRunner}};
294+
295+
let agent = Agent::new("agent.hcl").await?;
294296

295297
let mut team = AgentTeam::new("refactor-auth", TeamConfig::default());
296298
team.add_member("lead", TeamRole::Lead);
297299
team.add_member("worker-1", TeamRole::Worker);
298300
team.add_member("reviewer", TeamRole::Reviewer);
299301

300-
// Post a task to the board
301-
team.task_board().post("Refactor auth module", "lead", None);
302+
let mut runner = TeamRunner::new(team);
303+
runner.bind_session("lead", Arc::new(agent.session(".", None)?))?;
304+
runner.bind_session("worker-1", Arc::new(agent.session(".", None)?))?;
305+
runner.bind_session("reviewer", Arc::new(agent.session(".", None)?))?;
306+
307+
let result = runner.run_until_done("Refactor auth module to use JWT").await?;
308+
println!("Done: {} tasks, {} rejected, {} rounds",
309+
result.done_tasks.len(), result.rejected_tasks.len(), result.rounds);
310+
```
311+
312+
**How it works:**
313+
1. **Lead** decomposes the goal into a JSON task list via LLM
314+
2. **Workers** concurrently claim and execute tasks (each via its own `AgentSession`)
315+
3. **Reviewer** inspects completed work — APPROVED moves the task to Done, REJECTED re-queues it for retry
316+
4. Loop continues until all tasks are Done or `max_rounds` is reached
317+
318+
**Low-level coordination API** (for custom orchestrators):
319+
320+
```rust
321+
// Post, claim, complete manually (no LLM required)
322+
team.task_board().post("Refactor auth", "lead", None);
323+
let task = team.task_board().claim("worker-1").unwrap();
324+
team.task_board().complete(&task.id, "Refactored to JWT");
325+
team.task_board().approve(&task.id);
326+
327+
// Peer messaging
328+
team.send_message("lead", "worker-1", "Focus on the refresh token flow", None).await;
329+
let mut rx = team.take_receiver("worker-1").unwrap();
330+
while let Some(msg) = rx.recv().await { println!("{}: {}", msg.from, msg.content); }
331+
```
332+
333+
<details>
334+
<summary><b>TypeScript</b></summary>
302335

303-
// Worker claims and works on it
304-
let task = team.task_board().claim("worker-1");
336+
```typescript
337+
import { Agent, Team, TeamRunner, TeamConfig } from '@a3s-lab/code';
338+
339+
const agent = await Agent.create('agent.hcl');
305340

306-
// Complete → Review → Approve/Reject workflow
307-
team.task_board().complete("task-1", "Refactored to JWT");
308-
team.task_board().approve("task-1");
341+
const config: TeamConfig = { maxTasks: 50, channelBuffer: 128, maxRounds: 10, pollIntervalMs: 200 };
342+
const team = new Team('refactor-auth', config);
343+
team.addMember('lead', 'lead');
344+
team.addMember('worker-1', 'worker');
345+
team.addMember('reviewer', 'reviewer');
346+
347+
const runner = new TeamRunner(team);
348+
runner.bindSession('lead', agent.session('.'));
349+
runner.bindSession('worker-1', agent.session('.'));
350+
runner.bindSession('reviewer', agent.session('.'));
351+
352+
const result = await runner.runUntilDone('Refactor auth module to use JWT');
353+
console.log(`Done: ${result.doneTasks.length} tasks, ${result.rounds} rounds`);
354+
for (const task of result.doneTasks) {
355+
console.log(` [${task.id}] ${task.description}\n → ${task.result}`);
356+
}
309357
```
310358

359+
</details>
360+
361+
<details>
362+
<summary><b>Python</b></summary>
363+
364+
```python
365+
from a3s_code import Agent, Team, TeamRunner, TeamConfig
366+
367+
agent = Agent.create("agent.hcl")
368+
369+
config = TeamConfig(max_rounds=10, poll_interval_ms=200)
370+
team = Team("refactor-auth", config)
371+
team.add_member("lead", "lead")
372+
team.add_member("worker-1", "worker")
373+
team.add_member("reviewer", "reviewer")
374+
375+
runner = TeamRunner(team)
376+
runner.bind_session("lead", agent.session("."))
377+
runner.bind_session("worker-1", agent.session("."))
378+
runner.bind_session("reviewer", agent.session("."))
379+
380+
result = runner.run_until_done("Refactor auth module to use JWT")
381+
print(f"Done: {len(result.done_tasks)} tasks, {result.rounds} rounds")
382+
for task in result.done_tasks:
383+
print(f" [{task.id}] {task.description}\n{task.result}")
384+
```
385+
386+
</details>
387+
311388
Supports Lead/Worker/Reviewer roles, `mpsc` peer messaging, broadcast, and a full task lifecycle (Open → InProgress → InReview → Done/Rejected).
312389

313390
---
@@ -534,6 +611,11 @@ AgentTeam (multi-agent coordination)
534611
├── TeamTaskBoard (post → claim → complete → review → approve/reject)
535612
├── TeamMember[] (Lead, Worker, Reviewer roles)
536613
└── mpsc channels (peer-to-peer messaging + broadcast)
614+
615+
TeamRunner (LLM-integrated orchestrator)
616+
├── Lead → decomposes goal into JSON task list
617+
├── Workers → concurrently claim + execute tasks via AgentSession
618+
└── Reviewer → approve (Done) or reject (re-queued for retry)
537619
```
538620

539621
---
@@ -705,6 +787,39 @@ SessionOptions::new()
705787
.with_hook_engine(hooks)
706788
```
707789

790+
### Python SDK — Agent Teams
791+
792+
```python
793+
from a3s_code import Agent, Team, TeamRunner, TeamConfig, TeamTaskBoard
794+
795+
# Build team
796+
config = TeamConfig(max_tasks=50, max_rounds=10, poll_interval_ms=200)
797+
team = Team("my-team", config)
798+
team.add_member("lead", "lead") # role: "lead" | "worker" | "reviewer"
799+
team.add_member("worker-1", "worker")
800+
team.add_member("reviewer", "reviewer")
801+
802+
# Bind sessions and run
803+
runner = TeamRunner(team) # consumes the team
804+
runner.bind_session("lead", agent.session("."))
805+
result = runner.run_until_done("Build the feature")
806+
807+
# Inspect results
808+
result.done_tasks # List[TeamTask]
809+
result.rejected_tasks # List[TeamTask] (did not pass review after max_rounds)
810+
result.rounds # int
811+
812+
# Direct board access
813+
board = runner.task_board()
814+
board.post("Fix lint", "lead")
815+
board.claim("worker-1") # → TeamTask | None
816+
board.complete(task_id, "Fixed")
817+
board.approve(task_id)
818+
board.reject(task_id)
819+
tasks = board.by_status("done") # "open"|"in_progress"|"in_review"|"done"|"rejected"
820+
(open, prog, rev, done, rej) = board.stats()
821+
```
822+
708823
### Python SDK
709824

710825
```python
@@ -750,6 +865,39 @@ session.save()
750865
resumed = agent.resume_session(session.session_id, opts)
751866
```
752867

868+
### Node.js SDK — Agent Teams
869+
870+
```typescript
871+
import { Agent, Team, TeamRunner, TeamConfig, TeamTaskBoard } from '@a3s-lab/code';
872+
873+
// Build team
874+
const config: TeamConfig = { maxTasks: 50, channelBuffer: 128, maxRounds: 10, pollIntervalMs: 200 };
875+
const team = new Team('my-team', config);
876+
team.addMember('lead', 'lead'); // role: "lead" | "worker" | "reviewer"
877+
team.addMember('worker-1', 'worker');
878+
team.addMember('reviewer', 'reviewer');
879+
880+
// Bind sessions and run
881+
const runner = new TeamRunner(team); // consumes the team
882+
runner.bindSession('lead', agent.session('.'));
883+
const result = await runner.runUntilDone('Build the feature');
884+
885+
// Inspect results
886+
result.doneTasks // TeamTask[]
887+
result.rejectedTasks // TeamTask[] (did not pass review after maxRounds)
888+
result.rounds // number
889+
890+
// Direct board access
891+
const board = runner.taskBoard();
892+
board.post('Fix lint', 'lead');
893+
board.claim('worker-1'); // → TeamTask | null
894+
board.complete(taskId, 'Fixed');
895+
board.approve(taskId);
896+
board.reject(taskId);
897+
const tasks = await board.byStatus('done'); // "open"|"in_progress"|"in_review"|"done"|"rejected"
898+
const stats = board.stats(); // { open, inProgress, inReview, done, rejected, total }
899+
```
900+
753901
### Node.js SDK
754902

755903
```typescript
@@ -833,10 +981,12 @@ cargo run --example 02_streaming
833981
| Python | `sdk/python/examples/advanced_features_demo.py` | Direct tools, hooks, queue/lanes, security, resilience, memory |
834982
| Python | `sdk/python/examples/test_git_worktree.py` | Git worktree tool: direct calls + LLM-driven |
835983
| Python | `sdk/python/examples/test_prompt_slots.py` | Prompt slots: role, guidelines, response style, extra |
984+
| Python | `sdk/python/examples/test_agent_teams.py` | Multi-agent teams: TeamRunner, Lead/Worker/Reviewer workflow |
836985
| Node.js | `sdk/node/examples/agentic_loop_demo.js` | Basic send, streaming, multi-turn, planning, skills, security |
837986
| Node.js | `sdk/node/examples/advanced_features_demo.js` | Direct tools, hooks, queue/lanes, security, resilience, memory |
838987
| Node.js | `sdk/node/examples/test_git_worktree.js` | Git worktree tool: direct calls + LLM-driven |
839988
| Node.js | `sdk/node/examples/test_prompt_slots.js` | Prompt slots: role, guidelines, response style, extra |
989+
| Node.js | `sdk/node/examples/test_agent_teams.js` | Multi-agent teams: TeamRunner, Lead/Worker/Reviewer workflow |
840990

841991
### Integration & Feature Tests
842992

cli/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-cli"
3-
version = "0.9.0"
3+
version = "0.9.1"
44
edition = "2021"
55
authors = ["A3S Lab Team"]
66
license = "MIT"

core/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-core"
3-
version = "0.9.0"
3+
version = "0.9.1"
44
edition = "2021"
55
authors = ["A3S Lab Team"]
66
license = "MIT"

core/src/agent.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2103,8 +2103,12 @@ impl AgentLoop {
21032103
tx.send(AgentEvent::ConfirmationTimeout {
21042104
tool_id: tool_call.id.clone(),
21052105
action_taken: match timeout_action {
2106-
crate::hitl::TimeoutAction::Reject => "rejected".to_string(),
2107-
crate::hitl::TimeoutAction::AutoApprove => "auto_approved".to_string(),
2106+
crate::hitl::TimeoutAction::Reject => {
2107+
"rejected".to_string()
2108+
}
2109+
crate::hitl::TimeoutAction::AutoApprove => {
2110+
"auto_approved".to_string()
2111+
}
21082112
},
21092113
})
21102114
.await

0 commit comments

Comments
 (0)