Learn Claude Code
Back to Autonomous Agents

Team ProtocolsAutonomous Agents

s10 (419 LOC) → s11 (499 LOC)

LOC Delta

+80lines

New Tools

2

idleclaim_task
New Classes

0

New Functions

3

scan_unclaimed_tasksclaim_taskmake_identity_block

Team Protocols

Shared Communication Rules

419 LOC

12 tools: bash, read_file, write_file, edit_file, send_message, read_inbox, shutdown_response, plan_approval, spawn_teammate, list_teammates, broadcast, shutdown_request

collaboration

Autonomous Agents

Scan Board, Claim Tasks

499 LOC

14 tools: bash, read_file, write_file, edit_file, send_message, read_inbox, shutdown_response, plan_approval, idle, claim_task, spawn_teammate, list_teammates, broadcast, shutdown_request

collaboration

Source Code Diff

s10 (s10_team_protocols.py) -> s11 (s11_autonomous_agents.py)
11#!/usr/bin/env python3
2-# Harness: protocols -- structured handshakes between models.
2+# Harness: autonomy -- models that find work without being told.
33"""
4-s10_team_protocols.py - Team Protocols
4+s11_autonomous_agents.py - Autonomous Agents
55
6-Shutdown protocol and plan approval protocol, both using the same
7-request_id correlation pattern. Builds on s09's team messaging.
6+Idle cycle with task board polling, auto-claiming unclaimed tasks, and
7+identity re-injection after context compression. Builds on s10's protocols.
88
9- Shutdown FSM: pending -> approved | rejected
9+ Teammate lifecycle:
10+ +-------+
11+ | spawn |
12+ +---+---+
13+ |
14+ v
15+ +-------+ tool_use +-------+
16+ | WORK | <----------- | LLM |
17+ +---+---+ +-------+
18+ |
19+ | stop_reason != tool_use
20+ v
21+ +--------+
22+ | IDLE | poll every 5s for up to 60s
23+ +---+----+
24+ |
25+ +---> check inbox -> message? -> resume WORK
26+ |
27+ +---> scan .tasks/ -> unclaimed? -> claim -> resume WORK
28+ |
29+ +---> timeout (60s) -> shutdown
1030
11- Lead Teammate
12- +---------------------+ +---------------------+
13- | shutdown_request | | |
14- | { | -------> | receives request |
15- | request_id: abc | | decides: approve? |
16- | } | | |
17- +---------------------+ +---------------------+
18- |
19- +---------------------+ +-------v-------------+
20- | shutdown_response | <------- | shutdown_response |
21- | { | | { |
22- | request_id: abc | | request_id: abc |
23- | approve: true | | approve: true |
24- | } | | } |
25- +---------------------+ +---------------------+
26- |
27- v
28- status -> "shutdown", thread stops
31+ Identity re-injection after compression:
32+ messages = [identity_block, ...remaining...]
33+ "You are 'coder', role: backend, team: my-team"
2934
30- Plan approval FSM: pending -> approved | rejected
31-
32- Teammate Lead
33- +---------------------+ +---------------------+
34- | plan_approval | | |
35- | submit: {plan:"..."}| -------> | reviews plan text |
36- +---------------------+ | approve/reject? |
37- +---------------------+
38- |
39- +---------------------+ +-------v-------------+
40- | plan_approval_resp | <------- | plan_approval |
41- | {approve: true} | | review: {req_id, |
42- +---------------------+ | approve: true} |
43- +---------------------+
44-
45- Trackers: {request_id: {"target|from": name, "status": "pending|..."}}
46-
47-Key insight: "Same request_id correlation pattern, two domains."
35+Key insight: "The agent finds work itself."
4836"""
4937
5038import json
5139import os
5240import subprocess
5341import threading
5442import time
5543import uuid
5644from pathlib import Path
5745
5846from anthropic import Anthropic
5947from dotenv import load_dotenv
6048
6149load_dotenv(override=True)
6250if os.getenv("ANTHROPIC_BASE_URL"):
6351 os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
6452
6553WORKDIR = Path.cwd()
6654client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
6755MODEL = os.environ["MODEL_ID"]
6856TEAM_DIR = WORKDIR / ".team"
6957INBOX_DIR = TEAM_DIR / "inbox"
58+TASKS_DIR = WORKDIR / ".tasks"
7059
71-SYSTEM = f"You are a team lead at {WORKDIR}. Manage teammates with shutdown and plan approval protocols."
60+POLL_INTERVAL = 5
61+IDLE_TIMEOUT = 60
7262
63+SYSTEM = f"You are a team lead at {WORKDIR}. Teammates are autonomous -- they find work themselves."
64+
7365VALID_MSG_TYPES = {
7466 "message",
7567 "broadcast",
7668 "shutdown_request",
7769 "shutdown_response",
7870 "plan_approval_response",
7971}
8072
81-# -- Request trackers: correlate by request_id --
73+# -- Request trackers --
8274shutdown_requests = {}
8375plan_requests = {}
8476_tracker_lock = threading.Lock()
77+_claim_lock = threading.Lock()
8578
8679
8780# -- MessageBus: JSONL inbox per teammate --
8881class MessageBus:
8982 def __init__(self, inbox_dir: Path):
9083 self.dir = inbox_dir
9184 self.dir.mkdir(parents=True, exist_ok=True)
9285
9386 def send(self, sender: str, to: str, content: str,
9487 msg_type: str = "message", extra: dict = None) -> str:
9588 if msg_type not in VALID_MSG_TYPES:
9689 return f"Error: Invalid type '{msg_type}'. Valid: {VALID_MSG_TYPES}"
9790 msg = {
9891 "type": msg_type,
9992 "from": sender,
10093 "content": content,
10194 "timestamp": time.time(),
10295 }
10396 if extra:
10497 msg.update(extra)
10598 inbox_path = self.dir / f"{to}.jsonl"
10699 with open(inbox_path, "a") as f:
107100 f.write(json.dumps(msg) + "\n")
108101 return f"Sent {msg_type} to {to}"
109102
110103 def read_inbox(self, name: str) -> list:
111104 inbox_path = self.dir / f"{name}.jsonl"
112105 if not inbox_path.exists():
113106 return []
114107 messages = []
115108 for line in inbox_path.read_text().strip().splitlines():
116109 if line:
117110 messages.append(json.loads(line))
118111 inbox_path.write_text("")
119112 return messages
120113
121114 def broadcast(self, sender: str, content: str, teammates: list) -> str:
122115 count = 0
123116 for name in teammates:
124117 if name != sender:
125118 self.send(sender, name, content, "broadcast")
126119 count += 1
127120 return f"Broadcast to {count} teammates"
128121
129122
130123BUS = MessageBus(INBOX_DIR)
131124
132125
133-# -- TeammateManager with shutdown + plan approval --
126+# -- Task board scanning --
127+def scan_unclaimed_tasks() -> list:
128+ TASKS_DIR.mkdir(exist_ok=True)
129+ unclaimed = []
130+ for f in sorted(TASKS_DIR.glob("task_*.json")):
131+ task = json.loads(f.read_text())
132+ if (task.get("status") == "pending"
133+ and not task.get("owner")
134+ and not task.get("blockedBy")):
135+ unclaimed.append(task)
136+ return unclaimed
137+
138+
139+def claim_task(task_id: int, owner: str) -> str:
140+ with _claim_lock:
141+ path = TASKS_DIR / f"task_{task_id}.json"
142+ if not path.exists():
143+ return f"Error: Task {task_id} not found"
144+ task = json.loads(path.read_text())
145+ task["owner"] = owner
146+ task["status"] = "in_progress"
147+ path.write_text(json.dumps(task, indent=2))
148+ return f"Claimed task #{task_id} for {owner}"
149+
150+
151+# -- Identity re-injection after compression --
152+def make_identity_block(name: str, role: str, team_name: str) -> dict:
153+ return {
154+ "role": "user",
155+ "content": f"<identity>You are '{name}', role: {role}, team: {team_name}. Continue your work.</identity>",
156+ }
157+
158+
159+# -- Autonomous TeammateManager --
134160class TeammateManager:
135161 def __init__(self, team_dir: Path):
136162 self.dir = team_dir
137163 self.dir.mkdir(exist_ok=True)
138164 self.config_path = self.dir / "config.json"
139165 self.config = self._load_config()
140166 self.threads = {}
141167
142168 def _load_config(self) -> dict:
143169 if self.config_path.exists():
144170 return json.loads(self.config_path.read_text())
145171 return {"team_name": "default", "members": []}
146172
147173 def _save_config(self):
148174 self.config_path.write_text(json.dumps(self.config, indent=2))
149175
150176 def _find_member(self, name: str) -> dict:
151177 for m in self.config["members"]:
152178 if m["name"] == name:
153179 return m
154180 return None
155181
182+ def _set_status(self, name: str, status: str):
183+ member = self._find_member(name)
184+ if member:
185+ member["status"] = status
186+ self._save_config()
187+
156188 def spawn(self, name: str, role: str, prompt: str) -> str:
157189 member = self._find_member(name)
158190 if member:
159191 if member["status"] not in ("idle", "shutdown"):
160192 return f"Error: '{name}' is currently {member['status']}"
161193 member["status"] = "working"
162194 member["role"] = role
163195 else:
164196 member = {"name": name, "role": role, "status": "working"}
165197 self.config["members"].append(member)
166198 self._save_config()
167199 thread = threading.Thread(
168- target=self._teammate_loop,
200+ target=self._loop,
169201 args=(name, role, prompt),
170202 daemon=True,
171203 )
172204 self.threads[name] = thread
173205 thread.start()
174206 return f"Spawned '{name}' (role: {role})"
175207
176- def _teammate_loop(self, name: str, role: str, prompt: str):
208+ def _loop(self, name: str, role: str, prompt: str):
209+ team_name = self.config["team_name"]
177210 sys_prompt = (
178- f"You are '{name}', role: {role}, at {WORKDIR}. "
179- f"Submit plans via plan_approval before major work. "
180- f"Respond to shutdown_request with shutdown_response."
211+ f"You are '{name}', role: {role}, team: {team_name}, at {WORKDIR}. "
212+ f"Use idle tool when you have no more work. You will auto-claim new tasks."
181213 )
182214 messages = [{"role": "user", "content": prompt}]
183215 tools = self._teammate_tools()
184- should_exit = False
185- for _ in range(50):
186- inbox = BUS.read_inbox(name)
187- for msg in inbox:
188- messages.append({"role": "user", "content": json.dumps(msg)})
189- if should_exit:
190- break
191- try:
192- response = client.messages.create(
193- model=MODEL,
194- system=sys_prompt,
195- messages=messages,
196- tools=tools,
197- max_tokens=8000,
198- )
199- except Exception:
200- break
201- messages.append({"role": "assistant", "content": response.content})
202- if response.stop_reason != "tool_use":
203- break
204- results = []
205- for block in response.content:
206- if block.type == "tool_use":
207- output = self._exec(name, block.name, block.input)
208- print(f" [{name}] {block.name}: {str(output)[:120]}")
209- results.append({
210- "type": "tool_result",
211- "tool_use_id": block.id,
212- "content": str(output),
213- })
214- if block.name == "shutdown_response" and block.input.get("approve"):
215- should_exit = True
216- messages.append({"role": "user", "content": results})
217- member = self._find_member(name)
218- if member:
219- member["status"] = "shutdown" if should_exit else "idle"
220- self._save_config()
221216
217+ while True:
218+ # -- WORK PHASE: standard agent loop --
219+ for _ in range(50):
220+ inbox = BUS.read_inbox(name)
221+ for msg in inbox:
222+ if msg.get("type") == "shutdown_request":
223+ self._set_status(name, "shutdown")
224+ return
225+ messages.append({"role": "user", "content": json.dumps(msg)})
226+ try:
227+ response = client.messages.create(
228+ model=MODEL,
229+ system=sys_prompt,
230+ messages=messages,
231+ tools=tools,
232+ max_tokens=8000,
233+ )
234+ except Exception:
235+ self._set_status(name, "idle")
236+ return
237+ messages.append({"role": "assistant", "content": response.content})
238+ if response.stop_reason != "tool_use":
239+ break
240+ results = []
241+ idle_requested = False
242+ for block in response.content:
243+ if block.type == "tool_use":
244+ if block.name == "idle":
245+ idle_requested = True
246+ output = "Entering idle phase. Will poll for new tasks."
247+ else:
248+ output = self._exec(name, block.name, block.input)
249+ print(f" [{name}] {block.name}: {str(output)[:120]}")
250+ results.append({
251+ "type": "tool_result",
252+ "tool_use_id": block.id,
253+ "content": str(output),
254+ })
255+ messages.append({"role": "user", "content": results})
256+ if idle_requested:
257+ break
258+
259+ # -- IDLE PHASE: poll for inbox messages and unclaimed tasks --
260+ self._set_status(name, "idle")
261+ resume = False
262+ polls = IDLE_TIMEOUT // max(POLL_INTERVAL, 1)
263+ for _ in range(polls):
264+ time.sleep(POLL_INTERVAL)
265+ inbox = BUS.read_inbox(name)
266+ if inbox:
267+ for msg in inbox:
268+ if msg.get("type") == "shutdown_request":
269+ self._set_status(name, "shutdown")
270+ return
271+ messages.append({"role": "user", "content": json.dumps(msg)})
272+ resume = True
273+ break
274+ unclaimed = scan_unclaimed_tasks()
275+ if unclaimed:
276+ task = unclaimed[0]
277+ claim_task(task["id"], name)
278+ task_prompt = (
279+ f"<auto-claimed>Task #{task['id']}: {task['subject']}\n"
280+ f"{task.get('description', '')}</auto-claimed>"
281+ )
282+ if len(messages) <= 3:
283+ messages.insert(0, make_identity_block(name, role, team_name))
284+ messages.insert(1, {"role": "assistant", "content": f"I am {name}. Continuing."})
285+ messages.append({"role": "user", "content": task_prompt})
286+ messages.append({"role": "assistant", "content": f"Claimed task #{task['id']}. Working on it."})
287+ resume = True
288+ break
289+
290+ if not resume:
291+ self._set_status(name, "shutdown")
292+ return
293+ self._set_status(name, "working")
294+
222295 def _exec(self, sender: str, tool_name: str, args: dict) -> str:
223296 # these base tools are unchanged from s02
224297 if tool_name == "bash":
225298 return _run_bash(args["command"])
226299 if tool_name == "read_file":
227300 return _run_read(args["path"])
228301 if tool_name == "write_file":
229302 return _run_write(args["path"], args["content"])
230303 if tool_name == "edit_file":
231304 return _run_edit(args["path"], args["old_text"], args["new_text"])
232305 if tool_name == "send_message":
233306 return BUS.send(sender, args["to"], args["content"], args.get("msg_type", "message"))
234307 if tool_name == "read_inbox":
235308 return json.dumps(BUS.read_inbox(sender), indent=2)
236309 if tool_name == "shutdown_response":
237310 req_id = args["request_id"]
238- approve = args["approve"]
239311 with _tracker_lock:
240312 if req_id in shutdown_requests:
241- shutdown_requests[req_id]["status"] = "approved" if approve else "rejected"
313+ shutdown_requests[req_id]["status"] = "approved" if args["approve"] else "rejected"
242314 BUS.send(
243315 sender, "lead", args.get("reason", ""),
244- "shutdown_response", {"request_id": req_id, "approve": approve},
316+ "shutdown_response", {"request_id": req_id, "approve": args["approve"]},
245317 )
246- return f"Shutdown {'approved' if approve else 'rejected'}"
318+ return f"Shutdown {'approved' if args['approve'] else 'rejected'}"
247319 if tool_name == "plan_approval":
248320 plan_text = args.get("plan", "")
249321 req_id = str(uuid.uuid4())[:8]
250322 with _tracker_lock:
251323 plan_requests[req_id] = {"from": sender, "plan": plan_text, "status": "pending"}
252324 BUS.send(
253325 sender, "lead", plan_text, "plan_approval_response",
254326 {"request_id": req_id, "plan": plan_text},
255327 )
256- return f"Plan submitted (request_id={req_id}). Waiting for lead approval."
328+ return f"Plan submitted (request_id={req_id}). Waiting for approval."
329+ if tool_name == "claim_task":
330+ return claim_task(args["task_id"], sender)
257331 return f"Unknown tool: {tool_name}"
258332
259333 def _teammate_tools(self) -> list:
260334 # these base tools are unchanged from s02
261335 return [
262336 {"name": "bash", "description": "Run a shell command.",
263337 "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
264338 {"name": "read_file", "description": "Read file contents.",
265339 "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}},
266340 {"name": "write_file", "description": "Write content to file.",
267341 "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
268342 {"name": "edit_file", "description": "Replace exact text in file.",
269343 "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
270344 {"name": "send_message", "description": "Send message to a teammate.",
271345 "input_schema": {"type": "object", "properties": {"to": {"type": "string"}, "content": {"type": "string"}, "msg_type": {"type": "string", "enum": list(VALID_MSG_TYPES)}}, "required": ["to", "content"]}},
272346 {"name": "read_inbox", "description": "Read and drain your inbox.",
273347 "input_schema": {"type": "object", "properties": {}}},
274- {"name": "shutdown_response", "description": "Respond to a shutdown request. Approve to shut down, reject to keep working.",
348+ {"name": "shutdown_response", "description": "Respond to a shutdown request.",
275349 "input_schema": {"type": "object", "properties": {"request_id": {"type": "string"}, "approve": {"type": "boolean"}, "reason": {"type": "string"}}, "required": ["request_id", "approve"]}},
276- {"name": "plan_approval", "description": "Submit a plan for lead approval. Provide plan text.",
350+ {"name": "plan_approval", "description": "Submit a plan for lead approval.",
277351 "input_schema": {"type": "object", "properties": {"plan": {"type": "string"}}, "required": ["plan"]}},
352+ {"name": "idle", "description": "Signal that you have no more work. Enters idle polling phase.",
353+ "input_schema": {"type": "object", "properties": {}}},
354+ {"name": "claim_task", "description": "Claim a task from the task board by ID.",
355+ "input_schema": {"type": "object", "properties": {"task_id": {"type": "integer"}}, "required": ["task_id"]}},
278356 ]
279357
280358 def list_all(self) -> str:
281359 if not self.config["members"]:
282360 return "No teammates."
283361 lines = [f"Team: {self.config['team_name']}"]
284362 for m in self.config["members"]:
285363 lines.append(f" {m['name']} ({m['role']}): {m['status']}")
286364 return "\n".join(lines)
287365
288366 def member_names(self) -> list:
289367 return [m["name"] for m in self.config["members"]]
290368
291369
292370TEAM = TeammateManager(TEAM_DIR)
293371
294372
295373# -- Base tool implementations (these base tools are unchanged from s02) --
296374def _safe_path(p: str) -> Path:
297375 path = (WORKDIR / p).resolve()
298376 if not path.is_relative_to(WORKDIR):
299377 raise ValueError(f"Path escapes workspace: {p}")
300378 return path
301379
302380
303381def _run_bash(command: str) -> str:
304382 dangerous = ["rm -rf /", "sudo", "shutdown", "reboot"]
305383 if any(d in command for d in dangerous):
306384 return "Error: Dangerous command blocked"
307385 try:
308386 r = subprocess.run(
309387 command, shell=True, cwd=WORKDIR,
310388 capture_output=True, text=True, timeout=120,
311389 )
312390 out = (r.stdout + r.stderr).strip()
313391 return out[:50000] if out else "(no output)"
314392 except subprocess.TimeoutExpired:
315393 return "Error: Timeout (120s)"
316394
317395
318396def _run_read(path: str, limit: int = None) -> str:
319397 try:
320398 lines = _safe_path(path).read_text().splitlines()
321399 if limit and limit < len(lines):
322400 lines = lines[:limit] + [f"... ({len(lines) - limit} more)"]
323401 return "\n".join(lines)[:50000]
324402 except Exception as e:
325403 return f"Error: {e}"
326404
327405
328406def _run_write(path: str, content: str) -> str:
329407 try:
330408 fp = _safe_path(path)
331409 fp.parent.mkdir(parents=True, exist_ok=True)
332410 fp.write_text(content)
333411 return f"Wrote {len(content)} bytes"
334412 except Exception as e:
335413 return f"Error: {e}"
336414
337415
338416def _run_edit(path: str, old_text: str, new_text: str) -> str:
339417 try:
340418 fp = _safe_path(path)
341419 c = fp.read_text()
342420 if old_text not in c:
343421 return f"Error: Text not found in {path}"
344422 fp.write_text(c.replace(old_text, new_text, 1))
345423 return f"Edited {path}"
346424 except Exception as e:
347425 return f"Error: {e}"
348426
349427
350428# -- Lead-specific protocol handlers --
351429def handle_shutdown_request(teammate: str) -> str:
352430 req_id = str(uuid.uuid4())[:8]
353431 with _tracker_lock:
354432 shutdown_requests[req_id] = {"target": teammate, "status": "pending"}
355433 BUS.send(
356434 "lead", teammate, "Please shut down gracefully.",
357435 "shutdown_request", {"request_id": req_id},
358436 )
359- return f"Shutdown request {req_id} sent to '{teammate}' (status: pending)"
437+ return f"Shutdown request {req_id} sent to '{teammate}'"
360438
361439
362440def handle_plan_review(request_id: str, approve: bool, feedback: str = "") -> str:
363441 with _tracker_lock:
364442 req = plan_requests.get(request_id)
365443 if not req:
366444 return f"Error: Unknown plan request_id '{request_id}'"
367445 with _tracker_lock:
368446 req["status"] = "approved" if approve else "rejected"
369447 BUS.send(
370448 "lead", req["from"], feedback, "plan_approval_response",
371449 {"request_id": request_id, "approve": approve, "feedback": feedback},
372450 )
373451 return f"Plan {req['status']} for '{req['from']}'"
374452
375453
376454def _check_shutdown_status(request_id: str) -> str:
377455 with _tracker_lock:
378456 return json.dumps(shutdown_requests.get(request_id, {"error": "not found"}))
379457
380458
381-# -- Lead tool dispatch (12 tools) --
459+# -- Lead tool dispatch (14 tools) --
382460TOOL_HANDLERS = {
383461 "bash": lambda **kw: _run_bash(kw["command"]),
384462 "read_file": lambda **kw: _run_read(kw["path"], kw.get("limit")),
385463 "write_file": lambda **kw: _run_write(kw["path"], kw["content"]),
386464 "edit_file": lambda **kw: _run_edit(kw["path"], kw["old_text"], kw["new_text"]),
387465 "spawn_teammate": lambda **kw: TEAM.spawn(kw["name"], kw["role"], kw["prompt"]),
388466 "list_teammates": lambda **kw: TEAM.list_all(),
389467 "send_message": lambda **kw: BUS.send("lead", kw["to"], kw["content"], kw.get("msg_type", "message")),
390468 "read_inbox": lambda **kw: json.dumps(BUS.read_inbox("lead"), indent=2),
391469 "broadcast": lambda **kw: BUS.broadcast("lead", kw["content"], TEAM.member_names()),
392470 "shutdown_request": lambda **kw: handle_shutdown_request(kw["teammate"]),
393471 "shutdown_response": lambda **kw: _check_shutdown_status(kw.get("request_id", "")),
394472 "plan_approval": lambda **kw: handle_plan_review(kw["request_id"], kw["approve"], kw.get("feedback", "")),
473+ "idle": lambda **kw: "Lead does not idle.",
474+ "claim_task": lambda **kw: claim_task(kw["task_id"], "lead"),
395475}
396476
397477# these base tools are unchanged from s02
398478TOOLS = [
399479 {"name": "bash", "description": "Run a shell command.",
400480 "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
401481 {"name": "read_file", "description": "Read file contents.",
402482 "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}},
403483 {"name": "write_file", "description": "Write content to file.",
404484 "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
405485 {"name": "edit_file", "description": "Replace exact text in file.",
406486 "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
407- {"name": "spawn_teammate", "description": "Spawn a persistent teammate.",
487+ {"name": "spawn_teammate", "description": "Spawn an autonomous teammate.",
408488 "input_schema": {"type": "object", "properties": {"name": {"type": "string"}, "role": {"type": "string"}, "prompt": {"type": "string"}}, "required": ["name", "role", "prompt"]}},
409489 {"name": "list_teammates", "description": "List all teammates.",
410490 "input_schema": {"type": "object", "properties": {}}},
411491 {"name": "send_message", "description": "Send a message to a teammate.",
412492 "input_schema": {"type": "object", "properties": {"to": {"type": "string"}, "content": {"type": "string"}, "msg_type": {"type": "string", "enum": list(VALID_MSG_TYPES)}}, "required": ["to", "content"]}},
413493 {"name": "read_inbox", "description": "Read and drain the lead's inbox.",
414494 "input_schema": {"type": "object", "properties": {}}},
415495 {"name": "broadcast", "description": "Send a message to all teammates.",
416496 "input_schema": {"type": "object", "properties": {"content": {"type": "string"}}, "required": ["content"]}},
417- {"name": "shutdown_request", "description": "Request a teammate to shut down gracefully. Returns a request_id for tracking.",
497+ {"name": "shutdown_request", "description": "Request a teammate to shut down.",
418498 "input_schema": {"type": "object", "properties": {"teammate": {"type": "string"}}, "required": ["teammate"]}},
419- {"name": "shutdown_response", "description": "Check the status of a shutdown request by request_id.",
499+ {"name": "shutdown_response", "description": "Check shutdown request status.",
420500 "input_schema": {"type": "object", "properties": {"request_id": {"type": "string"}}, "required": ["request_id"]}},
421- {"name": "plan_approval", "description": "Approve or reject a teammate's plan. Provide request_id + approve + optional feedback.",
501+ {"name": "plan_approval", "description": "Approve or reject a teammate's plan.",
422502 "input_schema": {"type": "object", "properties": {"request_id": {"type": "string"}, "approve": {"type": "boolean"}, "feedback": {"type": "string"}}, "required": ["request_id", "approve"]}},
503+ {"name": "idle", "description": "Enter idle state (for lead -- rarely used).",
504+ "input_schema": {"type": "object", "properties": {}}},
505+ {"name": "claim_task", "description": "Claim a task from the board by ID.",
506+ "input_schema": {"type": "object", "properties": {"task_id": {"type": "integer"}}, "required": ["task_id"]}},
423507]
424508
425509
426510def agent_loop(messages: list):
427511 while True:
428512 inbox = BUS.read_inbox("lead")
429513 if inbox:
430514 messages.append({
431515 "role": "user",
432516 "content": f"<inbox>{json.dumps(inbox, indent=2)}</inbox>",
433517 })
434518 messages.append({
435519 "role": "assistant",
436520 "content": "Noted inbox messages.",
437521 })
438522 response = client.messages.create(
439523 model=MODEL,
440524 system=SYSTEM,
441525 messages=messages,
442526 tools=TOOLS,
443527 max_tokens=8000,
444528 )
445529 messages.append({"role": "assistant", "content": response.content})
446530 if response.stop_reason != "tool_use":
447531 return
448532 results = []
449533 for block in response.content:
450534 if block.type == "tool_use":
451535 handler = TOOL_HANDLERS.get(block.name)
452536 try:
453537 output = handler(**block.input) if handler else f"Unknown tool: {block.name}"
454538 except Exception as e:
455539 output = f"Error: {e}"
456540 print(f"> {block.name}: {str(output)[:200]}")
457541 results.append({
458542 "type": "tool_result",
459543 "tool_use_id": block.id,
460544 "content": str(output),
461545 })
462546 messages.append({"role": "user", "content": results})
463547
464548
465549if __name__ == "__main__":
466550 history = []
467551 while True:
468552 try:
469- query = input("\033[36ms10 >> \033[0m")
553+ query = input("\033[36ms11 >> \033[0m")
470554 except (EOFError, KeyboardInterrupt):
471555 break
472556 if query.strip().lower() in ("q", "exit", ""):
473557 break
474558 if query.strip() == "/team":
475559 print(TEAM.list_all())
476560 continue
477561 if query.strip() == "/inbox":
478562 print(json.dumps(BUS.read_inbox("lead"), indent=2))
479563 continue
564+ if query.strip() == "/tasks":
565+ TASKS_DIR.mkdir(exist_ok=True)
566+ for f in sorted(TASKS_DIR.glob("task_*.json")):
567+ t = json.loads(f.read_text())
568+ marker = {"pending": "[ ]", "in_progress": "[>]", "completed": "[x]"}.get(t["status"], "[?]")
569+ owner = f" @{t['owner']}" if t.get("owner") else ""
570+ print(f" {marker} #{t['id']}: {t['subject']}{owner}")
571+ continue
480572 history.append({"role": "user", "content": query})
481573 agent_loop(history)
482574 response_content = history[-1]["content"]
483575 if isinstance(response_content, list):
484576 for block in response_content:
485577 if hasattr(block, "text"):
486578 print(block.text)
487579 print()