diff --git a/agents/s02_tool_use.py b/agents/s02_tool_use.py index a05ac4bf9..93abd39c9 100644 --- a/agents/s02_tool_use.py +++ b/agents/s02_tool_use.py @@ -49,20 +49,28 @@ def run_bash(command: str) -> str: if any(d in command for d in dangerous): return "Error: Dangerous command blocked" try: - r = subprocess.run(command, shell=True, cwd=WORKDIR, - capture_output=True, text=True, timeout=120) + r = subprocess.run( + command, + shell=True, + cwd=WORKDIR, + capture_output=True, + text=True, + timeout=120, + ) out = (r.stdout + r.stderr).strip() return out[:50000] if out else "(no output)" except subprocess.TimeoutExpired: return "Error: Timeout (120s)" -def run_read(path: str, limit: int = None) -> str: +def run_read(path: str, limit: int | None = None) -> str: try: text = safe_path(path).read_text() lines = text.splitlines() - if limit and limit < len(lines): - lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"] + if limit is not None: + limit = max(0, int(limit)) + if limit < len(lines): + lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"] return "\n".join(lines)[:50000] except Exception as e: return f"Error: {e}" @@ -92,29 +100,64 @@ def run_edit(path: str, old_text: str, new_text: str) -> str: # -- The dispatch map: {tool_name: handler} -- TOOL_HANDLERS = { - "bash": lambda **kw: run_bash(kw["command"]), - "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")), + "bash": lambda **kw: run_bash(kw["command"]), + "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")), "write_file": lambda **kw: run_write(kw["path"], kw["content"]), - "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]), + "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]), } TOOLS = [ - {"name": "bash", "description": "Run a shell command.", - "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}, - {"name": "read_file", "description": "Read file contents.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}}, - {"name": "write_file", "description": "Write content to file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, - {"name": "edit_file", "description": "Replace exact text in file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, + { + "name": "bash", + "description": "Run a shell command.", + "input_schema": { + "type": "object", + "properties": {"command": {"type": "string"}}, + "required": ["command"], + }, + }, + { + "name": "read_file", + "description": "Read file contents.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, + "required": ["path"], + }, + }, + { + "name": "write_file", + "description": "Write content to file.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, + "required": ["path", "content"], + }, + }, + { + "name": "edit_file", + "description": "Replace exact text in file.", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "old_text": {"type": "string"}, + "new_text": {"type": "string"}, + }, + "required": ["path", "old_text", "new_text"], + }, + }, ] def agent_loop(messages: list): while True: response = client.messages.create( - model=MODEL, system=SYSTEM, messages=messages, - tools=TOOLS, max_tokens=8000, + model=MODEL, + system=SYSTEM, + messages=messages, + tools=TOOLS, + max_tokens=8000, ) messages.append({"role": "assistant", "content": response.content}) if response.stop_reason != "tool_use": @@ -123,9 +166,13 @@ def agent_loop(messages: list): for block in response.content: if block.type == "tool_use": handler = TOOL_HANDLERS.get(block.name) - output = handler(**block.input) if handler else f"Unknown tool: {block.name}" + output = ( + handler(**block.input) if handler else f"Unknown tool: {block.name}" + ) print(f"> {block.name}: {output[:200]}") - results.append({"type": "tool_result", "tool_use_id": block.id, "content": output}) + results.append( + {"type": "tool_result", "tool_use_id": block.id, "content": output} + ) messages.append({"role": "user", "content": results}) diff --git a/agents/s03_todo_write.py b/agents/s03_todo_write.py index 9ca805c05..9bbb6f083 100644 --- a/agents/s03_todo_write.py +++ b/agents/s03_todo_write.py @@ -78,7 +78,9 @@ def render(self) -> str: return "No todos." lines = [] for item in self.items: - marker = {"pending": "[ ]", "in_progress": "[>]", "completed": "[x]"}[item["status"]] + marker = {"pending": "[ ]", "in_progress": "[>]", "completed": "[x]"}[ + item["status"] + ] lines.append(f"{marker} #{item['id']}: {item['text']}") done = sum(1 for t in self.items if t["status"] == "completed") lines.append(f"\n({done}/{len(self.items)} completed)") @@ -95,27 +97,38 @@ def safe_path(p: str) -> Path: raise ValueError(f"Path escapes workspace: {p}") return path + def run_bash(command: str) -> str: dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"] if any(d in command for d in dangerous): return "Error: Dangerous command blocked" try: - r = subprocess.run(command, shell=True, cwd=WORKDIR, - capture_output=True, text=True, timeout=120) + r = subprocess.run( + command, + shell=True, + cwd=WORKDIR, + capture_output=True, + text=True, + timeout=120, + ) out = (r.stdout + r.stderr).strip() return out[:50000] if out else "(no output)" except subprocess.TimeoutExpired: return "Error: Timeout (120s)" -def run_read(path: str, limit: int = None) -> str: + +def run_read(path: str, limit: int | None = None) -> str: try: lines = safe_path(path).read_text().splitlines() - if limit and limit < len(lines): - lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] + if limit is not None: + limit = max(0, int(limit)) + if limit < len(lines): + lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] return "\n".join(lines)[:50000] except Exception as e: return f"Error: {e}" + def run_write(path: str, content: str) -> str: try: fp = safe_path(path) @@ -125,6 +138,7 @@ def run_write(path: str, content: str) -> str: except Exception as e: return f"Error: {e}" + def run_edit(path: str, old_text: str, new_text: str) -> str: try: fp = safe_path(path) @@ -138,24 +152,79 @@ def run_edit(path: str, old_text: str, new_text: str) -> str: TOOL_HANDLERS = { - "bash": lambda **kw: run_bash(kw["command"]), - "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")), + "bash": lambda **kw: run_bash(kw["command"]), + "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")), "write_file": lambda **kw: run_write(kw["path"], kw["content"]), - "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]), - "todo": lambda **kw: TODO.update(kw["items"]), + "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]), + "todo": lambda **kw: TODO.update(kw["items"]), } TOOLS = [ - {"name": "bash", "description": "Run a shell command.", - "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}, - {"name": "read_file", "description": "Read file contents.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}}, - {"name": "write_file", "description": "Write content to file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, - {"name": "edit_file", "description": "Replace exact text in file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "todo", "description": "Update task list. Track progress on multi-step tasks.", - "input_schema": {"type": "object", "properties": {"items": {"type": "array", "items": {"type": "object", "properties": {"id": {"type": "string"}, "text": {"type": "string"}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed"]}}, "required": ["id", "text", "status"]}}}, "required": ["items"]}}, + { + "name": "bash", + "description": "Run a shell command.", + "input_schema": { + "type": "object", + "properties": {"command": {"type": "string"}}, + "required": ["command"], + }, + }, + { + "name": "read_file", + "description": "Read file contents.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, + "required": ["path"], + }, + }, + { + "name": "write_file", + "description": "Write content to file.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, + "required": ["path", "content"], + }, + }, + { + "name": "edit_file", + "description": "Replace exact text in file.", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "old_text": {"type": "string"}, + "new_text": {"type": "string"}, + }, + "required": ["path", "old_text", "new_text"], + }, + }, + { + "name": "todo", + "description": "Update task list. Track progress on multi-step tasks.", + "input_schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "text": {"type": "string"}, + "status": { + "type": "string", + "enum": ["pending", "in_progress", "completed"], + }, + }, + "required": ["id", "text", "status"], + }, + } + }, + "required": ["items"], + }, + }, ] @@ -165,8 +234,11 @@ def agent_loop(messages: list): while True: # Nag reminder is injected below, alongside tool results response = client.messages.create( - model=MODEL, system=SYSTEM, messages=messages, - tools=TOOLS, max_tokens=8000, + model=MODEL, + system=SYSTEM, + messages=messages, + tools=TOOLS, + max_tokens=8000, ) messages.append({"role": "assistant", "content": response.content}) if response.stop_reason != "tool_use": @@ -177,16 +249,28 @@ def agent_loop(messages: list): if block.type == "tool_use": handler = TOOL_HANDLERS.get(block.name) try: - output = handler(**block.input) if handler else f"Unknown tool: {block.name}" + output = ( + handler(**block.input) + if handler + else f"Unknown tool: {block.name}" + ) except Exception as e: output = f"Error: {e}" print(f"> {block.name}: {str(output)[:200]}") - results.append({"type": "tool_result", "tool_use_id": block.id, "content": str(output)}) + results.append( + { + "type": "tool_result", + "tool_use_id": block.id, + "content": str(output), + } + ) if block.name == "todo": used_todo = True rounds_since_todo = 0 if used_todo else rounds_since_todo + 1 if rounds_since_todo >= 3: - results.insert(0, {"type": "text", "text": "Update your todos."}) + results.insert( + 0, {"type": "text", "text": "Update your todos."} + ) messages.append({"role": "user", "content": results}) diff --git a/agents/s04_subagent.py b/agents/s04_subagent.py index 9de087b1f..e1a8d69f8 100644 --- a/agents/s04_subagent.py +++ b/agents/s04_subagent.py @@ -49,27 +49,38 @@ def safe_path(p: str) -> Path: raise ValueError(f"Path escapes workspace: {p}") return path + def run_bash(command: str) -> str: dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"] if any(d in command for d in dangerous): return "Error: Dangerous command blocked" try: - r = subprocess.run(command, shell=True, cwd=WORKDIR, - capture_output=True, text=True, timeout=120) + r = subprocess.run( + command, + shell=True, + cwd=WORKDIR, + capture_output=True, + text=True, + timeout=120, + ) out = (r.stdout + r.stderr).strip() return out[:50000] if out else "(no output)" except subprocess.TimeoutExpired: return "Error: Timeout (120s)" -def run_read(path: str, limit: int = None) -> str: + +def run_read(path: str, limit: int | None = None) -> str: try: lines = safe_path(path).read_text().splitlines() - if limit and limit < len(lines): - lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] + if limit is not None: + limit = max(0, int(limit)) + if limit < len(lines): + lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] return "\n".join(lines)[:50000] except Exception as e: return f"Error: {e}" + def run_write(path: str, content: str) -> str: try: fp = safe_path(path) @@ -79,6 +90,7 @@ def run_write(path: str, content: str) -> str: except Exception as e: return f"Error: {e}" + def run_edit(path: str, old_text: str, new_text: str) -> str: try: fp = safe_path(path) @@ -92,22 +104,54 @@ def run_edit(path: str, old_text: str, new_text: str) -> str: TOOL_HANDLERS = { - "bash": lambda **kw: run_bash(kw["command"]), - "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")), + "bash": lambda **kw: run_bash(kw["command"]), + "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")), "write_file": lambda **kw: run_write(kw["path"], kw["content"]), - "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]), + "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]), } # Child gets all base tools except task (no recursive spawning) CHILD_TOOLS = [ - {"name": "bash", "description": "Run a shell command.", - "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}, - {"name": "read_file", "description": "Read file contents.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}}, - {"name": "write_file", "description": "Write content to file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, - {"name": "edit_file", "description": "Replace exact text in file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, + { + "name": "bash", + "description": "Run a shell command.", + "input_schema": { + "type": "object", + "properties": {"command": {"type": "string"}}, + "required": ["command"], + }, + }, + { + "name": "read_file", + "description": "Read file contents.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, + "required": ["path"], + }, + }, + { + "name": "write_file", + "description": "Write content to file.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, + "required": ["path", "content"], + }, + }, + { + "name": "edit_file", + "description": "Replace exact text in file.", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "old_text": {"type": "string"}, + "new_text": {"type": "string"}, + }, + "required": ["path", "old_text", "new_text"], + }, + }, ] @@ -116,8 +160,11 @@ def run_subagent(prompt: str) -> str: sub_messages = [{"role": "user", "content": prompt}] # fresh context for _ in range(30): # safety limit response = client.messages.create( - model=MODEL, system=SUBAGENT_SYSTEM, messages=sub_messages, - tools=CHILD_TOOLS, max_tokens=8000, + model=MODEL, + system=SUBAGENT_SYSTEM, + messages=sub_messages, + tools=CHILD_TOOLS, + max_tokens=8000, ) sub_messages.append({"role": "assistant", "content": response.content}) if response.stop_reason != "tool_use": @@ -126,25 +173,52 @@ def run_subagent(prompt: str) -> str: for block in response.content: if block.type == "tool_use": handler = TOOL_HANDLERS.get(block.name) - output = handler(**block.input) if handler else f"Unknown tool: {block.name}" - results.append({"type": "tool_result", "tool_use_id": block.id, "content": str(output)[:50000]}) + output = ( + handler(**block.input) if handler else f"Unknown tool: {block.name}" + ) + results.append( + { + "type": "tool_result", + "tool_use_id": block.id, + "content": str(output)[:50000], + } + ) sub_messages.append({"role": "user", "content": results}) # Only the final text returns to the parent -- child context is discarded - return "".join(b.text for b in response.content if hasattr(b, "text")) or "(no summary)" + return ( + "".join(b.text for b in response.content if hasattr(b, "text")) + or "(no summary)" + ) # -- Parent tools: base tools + task dispatcher -- PARENT_TOOLS = CHILD_TOOLS + [ - {"name": "task", "description": "Spawn a subagent with fresh context. It shares the filesystem but not conversation history.", - "input_schema": {"type": "object", "properties": {"prompt": {"type": "string"}, "description": {"type": "string", "description": "Short description of the task"}}, "required": ["prompt"]}}, + { + "name": "task", + "description": "Spawn a subagent with fresh context. It shares the filesystem but not conversation history.", + "input_schema": { + "type": "object", + "properties": { + "prompt": {"type": "string"}, + "description": { + "type": "string", + "description": "Short description of the task", + }, + }, + "required": ["prompt"], + }, + }, ] def agent_loop(messages: list): while True: response = client.messages.create( - model=MODEL, system=SYSTEM, messages=messages, - tools=PARENT_TOOLS, max_tokens=8000, + model=MODEL, + system=SYSTEM, + messages=messages, + tools=PARENT_TOOLS, + max_tokens=8000, ) messages.append({"role": "assistant", "content": response.content}) if response.stop_reason != "tool_use": @@ -158,9 +232,19 @@ def agent_loop(messages: list): output = run_subagent(block.input["prompt"]) else: handler = TOOL_HANDLERS.get(block.name) - output = handler(**block.input) if handler else f"Unknown tool: {block.name}" + output = ( + handler(**block.input) + if handler + else f"Unknown tool: {block.name}" + ) print(f" {str(output)[:200]}") - results.append({"type": "tool_result", "tool_use_id": block.id, "content": str(output)}) + results.append( + { + "type": "tool_result", + "tool_use_id": block.id, + "content": str(output), + } + ) messages.append({"role": "user", "content": results}) diff --git a/agents/s05_skill_loading.py b/agents/s05_skill_loading.py index ee8ffc157..c21e5f321 100644 --- a/agents/s05_skill_loading.py +++ b/agents/s05_skill_loading.py @@ -100,7 +100,7 @@ def get_content(self, name: str) -> str: skill = self.skills.get(name) if not skill: return f"Error: Unknown skill '{name}'. Available: {', '.join(self.skills.keys())}" - return f"\n{skill['body']}\n" + return f'\n{skill["body"]}\n' SKILL_LOADER = SkillLoader(SKILLS_DIR) @@ -120,27 +120,38 @@ def safe_path(p: str) -> Path: raise ValueError(f"Path escapes workspace: {p}") return path + def run_bash(command: str) -> str: dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"] if any(d in command for d in dangerous): return "Error: Dangerous command blocked" try: - r = subprocess.run(command, shell=True, cwd=WORKDIR, - capture_output=True, text=True, timeout=120) + r = subprocess.run( + command, + shell=True, + cwd=WORKDIR, + capture_output=True, + text=True, + timeout=120, + ) out = (r.stdout + r.stderr).strip() return out[:50000] if out else "(no output)" except subprocess.TimeoutExpired: return "Error: Timeout (120s)" -def run_read(path: str, limit: int = None) -> str: + +def run_read(path: str, limit: int | None = None) -> str: try: lines = safe_path(path).read_text().splitlines() - if limit and limit < len(lines): - lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] + if limit is not None: + limit = max(0, int(limit)) + if limit < len(lines): + lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] return "\n".join(lines)[:50000] except Exception as e: return f"Error: {e}" + def run_write(path: str, content: str) -> str: try: fp = safe_path(path) @@ -150,6 +161,7 @@ def run_write(path: str, content: str) -> str: except Exception as e: return f"Error: {e}" + def run_edit(path: str, old_text: str, new_text: str) -> str: try: fp = safe_path(path) @@ -163,32 +175,76 @@ def run_edit(path: str, old_text: str, new_text: str) -> str: TOOL_HANDLERS = { - "bash": lambda **kw: run_bash(kw["command"]), - "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")), + "bash": lambda **kw: run_bash(kw["command"]), + "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")), "write_file": lambda **kw: run_write(kw["path"], kw["content"]), - "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]), + "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]), "load_skill": lambda **kw: SKILL_LOADER.get_content(kw["name"]), } TOOLS = [ - {"name": "bash", "description": "Run a shell command.", - "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}, - {"name": "read_file", "description": "Read file contents.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}}, - {"name": "write_file", "description": "Write content to file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, - {"name": "edit_file", "description": "Replace exact text in file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "load_skill", "description": "Load specialized knowledge by name.", - "input_schema": {"type": "object", "properties": {"name": {"type": "string", "description": "Skill name to load"}}, "required": ["name"]}}, + { + "name": "bash", + "description": "Run a shell command.", + "input_schema": { + "type": "object", + "properties": {"command": {"type": "string"}}, + "required": ["command"], + }, + }, + { + "name": "read_file", + "description": "Read file contents.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, + "required": ["path"], + }, + }, + { + "name": "write_file", + "description": "Write content to file.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, + "required": ["path", "content"], + }, + }, + { + "name": "edit_file", + "description": "Replace exact text in file.", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "old_text": {"type": "string"}, + "new_text": {"type": "string"}, + }, + "required": ["path", "old_text", "new_text"], + }, + }, + { + "name": "load_skill", + "description": "Load specialized knowledge by name.", + "input_schema": { + "type": "object", + "properties": { + "name": {"type": "string", "description": "Skill name to load"} + }, + "required": ["name"], + }, + }, ] def agent_loop(messages: list): while True: response = client.messages.create( - model=MODEL, system=SYSTEM, messages=messages, - tools=TOOLS, max_tokens=8000, + model=MODEL, + system=SYSTEM, + messages=messages, + tools=TOOLS, + max_tokens=8000, ) messages.append({"role": "assistant", "content": response.content}) if response.stop_reason != "tool_use": @@ -198,11 +254,21 @@ def agent_loop(messages: list): if block.type == "tool_use": handler = TOOL_HANDLERS.get(block.name) try: - output = handler(**block.input) if handler else f"Unknown tool: {block.name}" + output = ( + handler(**block.input) + if handler + else f"Unknown tool: {block.name}" + ) except Exception as e: output = f"Error: {e}" print(f"> {block.name}: {str(output)[:200]}") - results.append({"type": "tool_result", "tool_use_id": block.id, "content": str(output)}) + results.append( + { + "type": "tool_result", + "tool_use_id": block.id, + "content": str(output), + } + ) messages.append({"role": "user", "content": results}) diff --git a/agents/s06_context_compact.py b/agents/s06_context_compact.py index b9c6aa8d2..0ac5631b9 100644 --- a/agents/s06_context_compact.py +++ b/agents/s06_context_compact.py @@ -106,17 +106,27 @@ def auto_compact(messages: list) -> list: conversation_text = json.dumps(messages, default=str)[:80000] response = client.messages.create( model=MODEL, - messages=[{"role": "user", "content": - "Summarize this conversation for continuity. Include: " - "1) What was accomplished, 2) Current state, 3) Key decisions made. " - "Be concise but preserve critical details.\n\n" + conversation_text}], + messages=[ + { + "role": "user", + "content": "Summarize this conversation for continuity. Include: " + "1) What was accomplished, 2) Current state, 3) Key decisions made. " + "Be concise but preserve critical details.\n\n" + conversation_text, + } + ], max_tokens=2000, ) summary = response.content[0].text # Replace all messages with compressed summary return [ - {"role": "user", "content": f"[Conversation compressed. Transcript: {transcript_path}]\n\n{summary}"}, - {"role": "assistant", "content": "Understood. I have the context from the summary. Continuing."}, + { + "role": "user", + "content": f"[Conversation compressed. Transcript: {transcript_path}]\n\n{summary}", + }, + { + "role": "assistant", + "content": "Understood. I have the context from the summary. Continuing.", + }, ] @@ -127,27 +137,38 @@ def safe_path(p: str) -> Path: raise ValueError(f"Path escapes workspace: {p}") return path + def run_bash(command: str) -> str: dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"] if any(d in command for d in dangerous): return "Error: Dangerous command blocked" try: - r = subprocess.run(command, shell=True, cwd=WORKDIR, - capture_output=True, text=True, timeout=120) + r = subprocess.run( + command, + shell=True, + cwd=WORKDIR, + capture_output=True, + text=True, + timeout=120, + ) out = (r.stdout + r.stderr).strip() return out[:50000] if out else "(no output)" except subprocess.TimeoutExpired: return "Error: Timeout (120s)" -def run_read(path: str, limit: int = None) -> str: + +def run_read(path: str, limit: int | None = None) -> str: try: lines = safe_path(path).read_text().splitlines() - if limit and limit < len(lines): - lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] + if limit is not None: + limit = max(0, int(limit)) + if limit < len(lines): + lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] return "\n".join(lines)[:50000] except Exception as e: return f"Error: {e}" + def run_write(path: str, content: str) -> str: try: fp = safe_path(path) @@ -157,6 +178,7 @@ def run_write(path: str, content: str) -> str: except Exception as e: return f"Error: {e}" + def run_edit(path: str, old_text: str, new_text: str) -> str: try: fp = safe_path(path) @@ -170,24 +192,67 @@ def run_edit(path: str, old_text: str, new_text: str) -> str: TOOL_HANDLERS = { - "bash": lambda **kw: run_bash(kw["command"]), - "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")), + "bash": lambda **kw: run_bash(kw["command"]), + "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")), "write_file": lambda **kw: run_write(kw["path"], kw["content"]), - "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]), - "compact": lambda **kw: "Manual compression requested.", + "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]), + "compact": lambda **kw: "Manual compression requested.", } TOOLS = [ - {"name": "bash", "description": "Run a shell command.", - "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}, - {"name": "read_file", "description": "Read file contents.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}}, - {"name": "write_file", "description": "Write content to file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, - {"name": "edit_file", "description": "Replace exact text in file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "compact", "description": "Trigger manual conversation compression.", - "input_schema": {"type": "object", "properties": {"focus": {"type": "string", "description": "What to preserve in the summary"}}}}, + { + "name": "bash", + "description": "Run a shell command.", + "input_schema": { + "type": "object", + "properties": {"command": {"type": "string"}}, + "required": ["command"], + }, + }, + { + "name": "read_file", + "description": "Read file contents.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, + "required": ["path"], + }, + }, + { + "name": "write_file", + "description": "Write content to file.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, + "required": ["path", "content"], + }, + }, + { + "name": "edit_file", + "description": "Replace exact text in file.", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "old_text": {"type": "string"}, + "new_text": {"type": "string"}, + }, + "required": ["path", "old_text", "new_text"], + }, + }, + { + "name": "compact", + "description": "Trigger manual conversation compression.", + "input_schema": { + "type": "object", + "properties": { + "focus": { + "type": "string", + "description": "What to preserve in the summary", + } + }, + }, + }, ] @@ -200,8 +265,11 @@ def agent_loop(messages: list): print("[auto_compact triggered]") messages[:] = auto_compact(messages) response = client.messages.create( - model=MODEL, system=SYSTEM, messages=messages, - tools=TOOLS, max_tokens=8000, + model=MODEL, + system=SYSTEM, + messages=messages, + tools=TOOLS, + max_tokens=8000, ) messages.append({"role": "assistant", "content": response.content}) if response.stop_reason != "tool_use": @@ -216,11 +284,21 @@ def agent_loop(messages: list): else: handler = TOOL_HANDLERS.get(block.name) try: - output = handler(**block.input) if handler else f"Unknown tool: {block.name}" + output = ( + handler(**block.input) + if handler + else f"Unknown tool: {block.name}" + ) except Exception as e: output = f"Error: {e}" print(f"> {block.name}: {str(output)[:200]}") - results.append({"type": "tool_result", "tool_use_id": block.id, "content": str(output)}) + results.append( + { + "type": "tool_result", + "tool_use_id": block.id, + "content": str(output), + } + ) messages.append({"role": "user", "content": results}) # Layer 3: manual compact triggered by the compact tool if manual_compact: diff --git a/agents/s07_task_system.py b/agents/s07_task_system.py index 82b16af62..5c459dcdb 100644 --- a/agents/s07_task_system.py +++ b/agents/s07_task_system.py @@ -65,8 +65,13 @@ def _save(self, task: dict): def create(self, subject: str, description: str = "") -> str: task = { - "id": self._next_id, "subject": subject, "description": description, - "status": "pending", "blockedBy": [], "blocks": [], "owner": "", + "id": self._next_id, + "subject": subject, + "description": description, + "status": "pending", + "blockedBy": [], + "blocks": [], + "owner": "", } self._save(task) self._next_id += 1 @@ -75,8 +80,13 @@ def create(self, subject: str, description: str = "") -> str: def get(self, task_id: int) -> str: return json.dumps(self._load(task_id), indent=2) - def update(self, task_id: int, status: str = None, - add_blocked_by: list = None, add_blocks: list = None) -> str: + def update( + self, + task_id: int, + status: str = None, + add_blocked_by: list = None, + add_blocks: list = None, + ) -> str: task = self._load(task_id) if status: if status not in ("pending", "in_progress", "completed"): @@ -117,7 +127,9 @@ def list_all(self) -> str: return "No tasks." lines = [] for t in tasks: - marker = {"pending": "[ ]", "in_progress": "[>]", "completed": "[x]"}.get(t["status"], "[?]") + marker = {"pending": "[ ]", "in_progress": "[>]", "completed": "[x]"}.get( + t["status"], "[?]" + ) blocked = f" (blocked by: {t['blockedBy']})" if t.get("blockedBy") else "" lines.append(f"{marker} #{t['id']}: {t['subject']}{blocked}") return "\n".join(lines) @@ -133,27 +145,38 @@ def safe_path(p: str) -> Path: raise ValueError(f"Path escapes workspace: {p}") return path + def run_bash(command: str) -> str: dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"] if any(d in command for d in dangerous): return "Error: Dangerous command blocked" try: - r = subprocess.run(command, shell=True, cwd=WORKDIR, - capture_output=True, text=True, timeout=120) + r = subprocess.run( + command, + shell=True, + cwd=WORKDIR, + capture_output=True, + text=True, + timeout=120, + ) out = (r.stdout + r.stderr).strip() return out[:50000] if out else "(no output)" except subprocess.TimeoutExpired: return "Error: Timeout (120s)" -def run_read(path: str, limit: int = None) -> str: + +def run_read(path: str, limit: int | None = None) -> str: try: lines = safe_path(path).read_text().splitlines() - if limit and limit < len(lines): - lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] + if limit is not None: + limit = max(0, int(limit)) + if limit < len(lines): + lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] return "\n".join(lines)[:50000] except Exception as e: return f"Error: {e}" + def run_write(path: str, content: str) -> str: try: fp = safe_path(path) @@ -163,6 +186,7 @@ def run_write(path: str, content: str) -> str: except Exception as e: return f"Error: {e}" + def run_edit(path: str, old_text: str, new_text: str) -> str: try: fp = safe_path(path) @@ -176,41 +200,113 @@ def run_edit(path: str, old_text: str, new_text: str) -> str: TOOL_HANDLERS = { - "bash": lambda **kw: run_bash(kw["command"]), - "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")), - "write_file": lambda **kw: run_write(kw["path"], kw["content"]), - "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]), + "bash": lambda **kw: run_bash(kw["command"]), + "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")), + "write_file": lambda **kw: run_write(kw["path"], kw["content"]), + "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]), "task_create": lambda **kw: TASKS.create(kw["subject"], kw.get("description", "")), - "task_update": lambda **kw: TASKS.update(kw["task_id"], kw.get("status"), kw.get("addBlockedBy"), kw.get("addBlocks")), - "task_list": lambda **kw: TASKS.list_all(), - "task_get": lambda **kw: TASKS.get(kw["task_id"]), + "task_update": lambda **kw: TASKS.update( + kw["task_id"], kw.get("status"), kw.get("addBlockedBy"), kw.get("addBlocks") + ), + "task_list": lambda **kw: TASKS.list_all(), + "task_get": lambda **kw: TASKS.get(kw["task_id"]), } TOOLS = [ - {"name": "bash", "description": "Run a shell command.", - "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}, - {"name": "read_file", "description": "Read file contents.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}}, - {"name": "write_file", "description": "Write content to file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, - {"name": "edit_file", "description": "Replace exact text in file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "task_create", "description": "Create a new task.", - "input_schema": {"type": "object", "properties": {"subject": {"type": "string"}, "description": {"type": "string"}}, "required": ["subject"]}}, - {"name": "task_update", "description": "Update a task's status or dependencies.", - "input_schema": {"type": "object", "properties": {"task_id": {"type": "integer"}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed"]}, "addBlockedBy": {"type": "array", "items": {"type": "integer"}}, "addBlocks": {"type": "array", "items": {"type": "integer"}}}, "required": ["task_id"]}}, - {"name": "task_list", "description": "List all tasks with status summary.", - "input_schema": {"type": "object", "properties": {}}}, - {"name": "task_get", "description": "Get full details of a task by ID.", - "input_schema": {"type": "object", "properties": {"task_id": {"type": "integer"}}, "required": ["task_id"]}}, + { + "name": "bash", + "description": "Run a shell command.", + "input_schema": { + "type": "object", + "properties": {"command": {"type": "string"}}, + "required": ["command"], + }, + }, + { + "name": "read_file", + "description": "Read file contents.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, + "required": ["path"], + }, + }, + { + "name": "write_file", + "description": "Write content to file.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, + "required": ["path", "content"], + }, + }, + { + "name": "edit_file", + "description": "Replace exact text in file.", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "old_text": {"type": "string"}, + "new_text": {"type": "string"}, + }, + "required": ["path", "old_text", "new_text"], + }, + }, + { + "name": "task_create", + "description": "Create a new task.", + "input_schema": { + "type": "object", + "properties": { + "subject": {"type": "string"}, + "description": {"type": "string"}, + }, + "required": ["subject"], + }, + }, + { + "name": "task_update", + "description": "Update a task's status or dependencies.", + "input_schema": { + "type": "object", + "properties": { + "task_id": {"type": "integer"}, + "status": { + "type": "string", + "enum": ["pending", "in_progress", "completed"], + }, + "addBlockedBy": {"type": "array", "items": {"type": "integer"}}, + "addBlocks": {"type": "array", "items": {"type": "integer"}}, + }, + "required": ["task_id"], + }, + }, + { + "name": "task_list", + "description": "List all tasks with status summary.", + "input_schema": {"type": "object", "properties": {}}, + }, + { + "name": "task_get", + "description": "Get full details of a task by ID.", + "input_schema": { + "type": "object", + "properties": {"task_id": {"type": "integer"}}, + "required": ["task_id"], + }, + }, ] def agent_loop(messages: list): while True: response = client.messages.create( - model=MODEL, system=SYSTEM, messages=messages, - tools=TOOLS, max_tokens=8000, + model=MODEL, + system=SYSTEM, + messages=messages, + tools=TOOLS, + max_tokens=8000, ) messages.append({"role": "assistant", "content": response.content}) if response.stop_reason != "tool_use": @@ -220,11 +316,21 @@ def agent_loop(messages: list): if block.type == "tool_use": handler = TOOL_HANDLERS.get(block.name) try: - output = handler(**block.input) if handler else f"Unknown tool: {block.name}" + output = ( + handler(**block.input) + if handler + else f"Unknown tool: {block.name}" + ) except Exception as e: output = f"Error: {e}" print(f"> {block.name}: {str(output)[:200]}") - results.append({"type": "tool_result", "tool_use_id": block.id, "content": str(output)}) + results.append( + { + "type": "tool_result", + "tool_use_id": block.id, + "content": str(output), + } + ) messages.append({"role": "user", "content": results}) diff --git a/agents/s08_background_tasks.py b/agents/s08_background_tasks.py index 77a992eaf..b80db0230 100644 --- a/agents/s08_background_tasks.py +++ b/agents/s08_background_tasks.py @@ -66,8 +66,12 @@ def _execute(self, task_id: str, command: str): """Thread target: run subprocess, capture output, push to queue.""" try: r = subprocess.run( - command, shell=True, cwd=WORKDIR, - capture_output=True, text=True, timeout=300 + command, + shell=True, + cwd=WORKDIR, + capture_output=True, + text=True, + timeout=300, ) output = (r.stdout + r.stderr).strip()[:50000] status = "completed" @@ -80,12 +84,14 @@ def _execute(self, task_id: str, command: str): self.tasks[task_id]["status"] = status self.tasks[task_id]["result"] = output or "(no output)" with self._lock: - self._notification_queue.append({ - "task_id": task_id, - "status": status, - "command": command[:80], - "result": (output or "(no output)")[:500], - }) + self._notification_queue.append( + { + "task_id": task_id, + "status": status, + "command": command[:80], + "result": (output or "(no output)")[:500], + } + ) def check(self, task_id: str = None) -> str: """Check status of one task or list all.""" @@ -93,7 +99,9 @@ def check(self, task_id: str = None) -> str: t = self.tasks.get(task_id) if not t: return f"Error: Unknown task {task_id}" - return f"[{t['status']}] {t['command'][:60]}\n{t.get('result') or '(running)'}" + return ( + f"[{t['status']}] {t['command'][:60]}\n{t.get('result') or '(running)'}" + ) lines = [] for tid, t in self.tasks.items(): lines.append(f"{tid}: [{t['status']}] {t['command'][:60]}") @@ -117,27 +125,38 @@ def safe_path(p: str) -> Path: raise ValueError(f"Path escapes workspace: {p}") return path + def run_bash(command: str) -> str: dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"] if any(d in command for d in dangerous): return "Error: Dangerous command blocked" try: - r = subprocess.run(command, shell=True, cwd=WORKDIR, - capture_output=True, text=True, timeout=120) + r = subprocess.run( + command, + shell=True, + cwd=WORKDIR, + capture_output=True, + text=True, + timeout=120, + ) out = (r.stdout + r.stderr).strip() return out[:50000] if out else "(no output)" except subprocess.TimeoutExpired: return "Error: Timeout (120s)" -def run_read(path: str, limit: int = None) -> str: + +def run_read(path: str, limit: int | None = None) -> str: try: lines = safe_path(path).read_text().splitlines() - if limit and limit < len(lines): - lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] + if limit is not None: + limit = max(0, int(limit)) + if limit < len(lines): + lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] return "\n".join(lines)[:50000] except Exception as e: return f"Error: {e}" + def run_write(path: str, content: str) -> str: try: fp = safe_path(path) @@ -147,6 +166,7 @@ def run_write(path: str, content: str) -> str: except Exception as e: return f"Error: {e}" + def run_edit(path: str, old_text: str, new_text: str) -> str: try: fp = safe_path(path) @@ -160,27 +180,72 @@ def run_edit(path: str, old_text: str, new_text: str) -> str: TOOL_HANDLERS = { - "bash": lambda **kw: run_bash(kw["command"]), - "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")), - "write_file": lambda **kw: run_write(kw["path"], kw["content"]), - "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]), - "background_run": lambda **kw: BG.run(kw["command"]), + "bash": lambda **kw: run_bash(kw["command"]), + "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")), + "write_file": lambda **kw: run_write(kw["path"], kw["content"]), + "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]), + "background_run": lambda **kw: BG.run(kw["command"]), "check_background": lambda **kw: BG.check(kw.get("task_id")), } TOOLS = [ - {"name": "bash", "description": "Run a shell command (blocking).", - "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}, - {"name": "read_file", "description": "Read file contents.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}}, - {"name": "write_file", "description": "Write content to file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, - {"name": "edit_file", "description": "Replace exact text in file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "background_run", "description": "Run command in background thread. Returns task_id immediately.", - "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}, - {"name": "check_background", "description": "Check background task status. Omit task_id to list all.", - "input_schema": {"type": "object", "properties": {"task_id": {"type": "string"}}}}, + { + "name": "bash", + "description": "Run a shell command (blocking).", + "input_schema": { + "type": "object", + "properties": {"command": {"type": "string"}}, + "required": ["command"], + }, + }, + { + "name": "read_file", + "description": "Read file contents.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, + "required": ["path"], + }, + }, + { + "name": "write_file", + "description": "Write content to file.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, + "required": ["path", "content"], + }, + }, + { + "name": "edit_file", + "description": "Replace exact text in file.", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "old_text": {"type": "string"}, + "new_text": {"type": "string"}, + }, + "required": ["path", "old_text", "new_text"], + }, + }, + { + "name": "background_run", + "description": "Run command in background thread. Returns task_id immediately.", + "input_schema": { + "type": "object", + "properties": {"command": {"type": "string"}}, + "required": ["command"], + }, + }, + { + "name": "check_background", + "description": "Check background task status. Omit task_id to list all.", + "input_schema": { + "type": "object", + "properties": {"task_id": {"type": "string"}}, + }, + }, ] @@ -192,11 +257,21 @@ def agent_loop(messages: list): notif_text = "\n".join( f"[bg:{n['task_id']}] {n['status']}: {n['result']}" for n in notifs ) - messages.append({"role": "user", "content": f"\n{notif_text}\n"}) - messages.append({"role": "assistant", "content": "Noted background results."}) + messages.append( + { + "role": "user", + "content": f"\n{notif_text}\n", + } + ) + messages.append( + {"role": "assistant", "content": "Noted background results."} + ) response = client.messages.create( - model=MODEL, system=SYSTEM, messages=messages, - tools=TOOLS, max_tokens=8000, + model=MODEL, + system=SYSTEM, + messages=messages, + tools=TOOLS, + max_tokens=8000, ) messages.append({"role": "assistant", "content": response.content}) if response.stop_reason != "tool_use": @@ -206,11 +281,21 @@ def agent_loop(messages: list): if block.type == "tool_use": handler = TOOL_HANDLERS.get(block.name) try: - output = handler(**block.input) if handler else f"Unknown tool: {block.name}" + output = ( + handler(**block.input) + if handler + else f"Unknown tool: {block.name}" + ) except Exception as e: output = f"Error: {e}" print(f"> {block.name}: {str(output)[:200]}") - results.append({"type": "tool_result", "tool_use_id": block.id, "content": str(output)}) + results.append( + { + "type": "tool_result", + "tool_use_id": block.id, + "content": str(output), + } + ) messages.append({"role": "user", "content": results}) diff --git a/agents/s09_agent_teams.py b/agents/s09_agent_teams.py index 284a1ac19..b6df2b4ed 100644 --- a/agents/s09_agent_teams.py +++ b/agents/s09_agent_teams.py @@ -62,7 +62,9 @@ TEAM_DIR = WORKDIR / ".team" INBOX_DIR = TEAM_DIR / "inbox" -SYSTEM = f"You are a team lead at {WORKDIR}. Spawn teammates and communicate via inboxes." +SYSTEM = ( + f"You are a team lead at {WORKDIR}. Spawn teammates and communicate via inboxes." +) VALID_MSG_TYPES = { "message", @@ -79,8 +81,14 @@ def __init__(self, inbox_dir: Path): self.dir = inbox_dir self.dir.mkdir(parents=True, exist_ok=True) - def send(self, sender: str, to: str, content: str, - msg_type: str = "message", extra: dict = None) -> str: + def send( + self, + sender: str, + to: str, + content: str, + msg_type: str = "message", + extra: dict = None, + ) -> str: if msg_type not in VALID_MSG_TYPES: return f"Error: Invalid type '{msg_type}'. Valid: {VALID_MSG_TYPES}" msg = { @@ -191,11 +199,13 @@ def _teammate_loop(self, name: str, role: str, prompt: str): if block.type == "tool_use": output = self._exec(name, block.name, block.input) print(f" [{name}] {block.name}: {str(output)[:120]}") - results.append({ - "type": "tool_result", - "tool_use_id": block.id, - "content": str(output), - }) + results.append( + { + "type": "tool_result", + "tool_use_id": block.id, + "content": str(output), + } + ) messages.append({"role": "user", "content": results}) member = self._find_member(name) if member and member["status"] != "shutdown": @@ -213,7 +223,9 @@ def _exec(self, sender: str, tool_name: str, args: dict) -> str: if tool_name == "edit_file": return _run_edit(args["path"], args["old_text"], args["new_text"]) if tool_name == "send_message": - return BUS.send(sender, args["to"], args["content"], args.get("msg_type", "message")) + return BUS.send( + sender, args["to"], args["content"], args.get("msg_type", "message") + ) if tool_name == "read_inbox": return json.dumps(BUS.read_inbox(sender), indent=2) return f"Unknown tool: {tool_name}" @@ -221,18 +233,67 @@ def _exec(self, sender: str, tool_name: str, args: dict) -> str: def _teammate_tools(self) -> list: # these base tools are unchanged from s02 return [ - {"name": "bash", "description": "Run a shell command.", - "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}, - {"name": "read_file", "description": "Read file contents.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}}, - {"name": "write_file", "description": "Write content to file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, - {"name": "edit_file", "description": "Replace exact text in file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "send_message", "description": "Send message to a teammate.", - "input_schema": {"type": "object", "properties": {"to": {"type": "string"}, "content": {"type": "string"}, "msg_type": {"type": "string", "enum": list(VALID_MSG_TYPES)}}, "required": ["to", "content"]}}, - {"name": "read_inbox", "description": "Read and drain your inbox.", - "input_schema": {"type": "object", "properties": {}}}, + { + "name": "bash", + "description": "Run a shell command.", + "input_schema": { + "type": "object", + "properties": {"command": {"type": "string"}}, + "required": ["command"], + }, + }, + { + "name": "read_file", + "description": "Read file contents.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}}, + "required": ["path"], + }, + }, + { + "name": "write_file", + "description": "Write content to file.", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "content": {"type": "string"}, + }, + "required": ["path", "content"], + }, + }, + { + "name": "edit_file", + "description": "Replace exact text in file.", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "old_text": {"type": "string"}, + "new_text": {"type": "string"}, + }, + "required": ["path", "old_text", "new_text"], + }, + }, + { + "name": "send_message", + "description": "Send message to a teammate.", + "input_schema": { + "type": "object", + "properties": { + "to": {"type": "string"}, + "content": {"type": "string"}, + "msg_type": {"type": "string", "enum": list(VALID_MSG_TYPES)}, + }, + "required": ["to", "content"], + }, + }, + { + "name": "read_inbox", + "description": "Read and drain your inbox.", + "input_schema": {"type": "object", "properties": {}}, + }, ] def list_all(self) -> str: @@ -264,8 +325,12 @@ def _run_bash(command: str) -> str: return "Error: Dangerous command blocked" try: r = subprocess.run( - command, shell=True, cwd=WORKDIR, - capture_output=True, text=True, timeout=120, + command, + shell=True, + cwd=WORKDIR, + capture_output=True, + text=True, + timeout=120, ) out = (r.stdout + r.stderr).strip() return out[:50000] if out else "(no output)" @@ -273,11 +338,13 @@ def _run_bash(command: str) -> str: return "Error: Timeout (120s)" -def _run_read(path: str, limit: int = None) -> str: +def _run_read(path: str, limit: int | None = None) -> str: try: lines = _safe_path(path).read_text().splitlines() - if limit and limit < len(lines): - lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] + if limit is not None: + limit = max(0, int(limit)) + if limit < len(lines): + lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] return "\n".join(lines)[:50000] except Exception as e: return f"Error: {e}" @@ -307,37 +374,106 @@ def _run_edit(path: str, old_text: str, new_text: str) -> str: # -- Lead tool dispatch (9 tools) -- TOOL_HANDLERS = { - "bash": lambda **kw: _run_bash(kw["command"]), - "read_file": lambda **kw: _run_read(kw["path"], kw.get("limit")), - "write_file": lambda **kw: _run_write(kw["path"], kw["content"]), - "edit_file": lambda **kw: _run_edit(kw["path"], kw["old_text"], kw["new_text"]), - "spawn_teammate": lambda **kw: TEAM.spawn(kw["name"], kw["role"], kw["prompt"]), - "list_teammates": lambda **kw: TEAM.list_all(), - "send_message": lambda **kw: BUS.send("lead", kw["to"], kw["content"], kw.get("msg_type", "message")), - "read_inbox": lambda **kw: json.dumps(BUS.read_inbox("lead"), indent=2), - "broadcast": lambda **kw: BUS.broadcast("lead", kw["content"], TEAM.member_names()), + "bash": lambda **kw: _run_bash(kw["command"]), + "read_file": lambda **kw: _run_read(kw["path"], kw.get("limit")), + "write_file": lambda **kw: _run_write(kw["path"], kw["content"]), + "edit_file": lambda **kw: _run_edit(kw["path"], kw["old_text"], kw["new_text"]), + "spawn_teammate": lambda **kw: TEAM.spawn(kw["name"], kw["role"], kw["prompt"]), + "list_teammates": lambda **kw: TEAM.list_all(), + "send_message": lambda **kw: BUS.send( + "lead", kw["to"], kw["content"], kw.get("msg_type", "message") + ), + "read_inbox": lambda **kw: json.dumps(BUS.read_inbox("lead"), indent=2), + "broadcast": lambda **kw: BUS.broadcast("lead", kw["content"], TEAM.member_names()), } # these base tools are unchanged from s02 TOOLS = [ - {"name": "bash", "description": "Run a shell command.", - "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}, - {"name": "read_file", "description": "Read file contents.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}}, - {"name": "write_file", "description": "Write content to file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, - {"name": "edit_file", "description": "Replace exact text in file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "spawn_teammate", "description": "Spawn a persistent teammate that runs in its own thread.", - "input_schema": {"type": "object", "properties": {"name": {"type": "string"}, "role": {"type": "string"}, "prompt": {"type": "string"}}, "required": ["name", "role", "prompt"]}}, - {"name": "list_teammates", "description": "List all teammates with name, role, status.", - "input_schema": {"type": "object", "properties": {}}}, - {"name": "send_message", "description": "Send a message to a teammate's inbox.", - "input_schema": {"type": "object", "properties": {"to": {"type": "string"}, "content": {"type": "string"}, "msg_type": {"type": "string", "enum": list(VALID_MSG_TYPES)}}, "required": ["to", "content"]}}, - {"name": "read_inbox", "description": "Read and drain the lead's inbox.", - "input_schema": {"type": "object", "properties": {}}}, - {"name": "broadcast", "description": "Send a message to all teammates.", - "input_schema": {"type": "object", "properties": {"content": {"type": "string"}}, "required": ["content"]}}, + { + "name": "bash", + "description": "Run a shell command.", + "input_schema": { + "type": "object", + "properties": {"command": {"type": "string"}}, + "required": ["command"], + }, + }, + { + "name": "read_file", + "description": "Read file contents.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, + "required": ["path"], + }, + }, + { + "name": "write_file", + "description": "Write content to file.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, + "required": ["path", "content"], + }, + }, + { + "name": "edit_file", + "description": "Replace exact text in file.", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "old_text": {"type": "string"}, + "new_text": {"type": "string"}, + }, + "required": ["path", "old_text", "new_text"], + }, + }, + { + "name": "spawn_teammate", + "description": "Spawn a persistent teammate that runs in its own thread.", + "input_schema": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "role": {"type": "string"}, + "prompt": {"type": "string"}, + }, + "required": ["name", "role", "prompt"], + }, + }, + { + "name": "list_teammates", + "description": "List all teammates with name, role, status.", + "input_schema": {"type": "object", "properties": {}}, + }, + { + "name": "send_message", + "description": "Send a message to a teammate's inbox.", + "input_schema": { + "type": "object", + "properties": { + "to": {"type": "string"}, + "content": {"type": "string"}, + "msg_type": {"type": "string", "enum": list(VALID_MSG_TYPES)}, + }, + "required": ["to", "content"], + }, + }, + { + "name": "read_inbox", + "description": "Read and drain the lead's inbox.", + "input_schema": {"type": "object", "properties": {}}, + }, + { + "name": "broadcast", + "description": "Send a message to all teammates.", + "input_schema": { + "type": "object", + "properties": {"content": {"type": "string"}}, + "required": ["content"], + }, + }, ] @@ -345,14 +481,18 @@ def agent_loop(messages: list): while True: inbox = BUS.read_inbox("lead") if inbox: - messages.append({ - "role": "user", - "content": f"{json.dumps(inbox, indent=2)}", - }) - messages.append({ - "role": "assistant", - "content": "Noted inbox messages.", - }) + messages.append( + { + "role": "user", + "content": f"{json.dumps(inbox, indent=2)}", + } + ) + messages.append( + { + "role": "assistant", + "content": "Noted inbox messages.", + } + ) response = client.messages.create( model=MODEL, system=SYSTEM, @@ -368,15 +508,21 @@ def agent_loop(messages: list): if block.type == "tool_use": handler = TOOL_HANDLERS.get(block.name) try: - output = handler(**block.input) if handler else f"Unknown tool: {block.name}" + output = ( + handler(**block.input) + if handler + else f"Unknown tool: {block.name}" + ) except Exception as e: output = f"Error: {e}" print(f"> {block.name}: {str(output)[:200]}") - results.append({ - "type": "tool_result", - "tool_use_id": block.id, - "content": str(output), - }) + results.append( + { + "type": "tool_result", + "tool_use_id": block.id, + "content": str(output), + } + ) messages.append({"role": "user", "content": results}) diff --git a/agents/s10_team_protocols.py b/agents/s10_team_protocols.py index 21f936df3..b4b696102 100644 --- a/agents/s10_team_protocols.py +++ b/agents/s10_team_protocols.py @@ -89,8 +89,14 @@ def __init__(self, inbox_dir: Path): self.dir = inbox_dir self.dir.mkdir(parents=True, exist_ok=True) - def send(self, sender: str, to: str, content: str, - msg_type: str = "message", extra: dict = None) -> str: + def send( + self, + sender: str, + to: str, + content: str, + msg_type: str = "message", + extra: dict = None, + ) -> str: if msg_type not in VALID_MSG_TYPES: return f"Error: Invalid type '{msg_type}'. Valid: {VALID_MSG_TYPES}" msg = { @@ -205,11 +211,13 @@ def _teammate_loop(self, name: str, role: str, prompt: str): if block.type == "tool_use": output = self._exec(name, block.name, block.input) print(f" [{name}] {block.name}: {str(output)[:120]}") - results.append({ - "type": "tool_result", - "tool_use_id": block.id, - "content": str(output), - }) + results.append( + { + "type": "tool_result", + "tool_use_id": block.id, + "content": str(output), + } + ) if block.name == "shutdown_response" and block.input.get("approve"): should_exit = True messages.append({"role": "user", "content": results}) @@ -229,7 +237,9 @@ def _exec(self, sender: str, tool_name: str, args: dict) -> str: if tool_name == "edit_file": return _run_edit(args["path"], args["old_text"], args["new_text"]) if tool_name == "send_message": - return BUS.send(sender, args["to"], args["content"], args.get("msg_type", "message")) + return BUS.send( + sender, args["to"], args["content"], args.get("msg_type", "message") + ) if tool_name == "read_inbox": return json.dumps(BUS.read_inbox(sender), indent=2) if tool_name == "shutdown_response": @@ -237,19 +247,31 @@ def _exec(self, sender: str, tool_name: str, args: dict) -> str: approve = args["approve"] with _tracker_lock: if req_id in shutdown_requests: - shutdown_requests[req_id]["status"] = "approved" if approve else "rejected" + shutdown_requests[req_id]["status"] = ( + "approved" if approve else "rejected" + ) BUS.send( - sender, "lead", args.get("reason", ""), - "shutdown_response", {"request_id": req_id, "approve": approve}, + sender, + "lead", + args.get("reason", ""), + "shutdown_response", + {"request_id": req_id, "approve": approve}, ) return f"Shutdown {'approved' if approve else 'rejected'}" if tool_name == "plan_approval": plan_text = args.get("plan", "") req_id = str(uuid.uuid4())[:8] with _tracker_lock: - plan_requests[req_id] = {"from": sender, "plan": plan_text, "status": "pending"} + plan_requests[req_id] = { + "from": sender, + "plan": plan_text, + "status": "pending", + } BUS.send( - sender, "lead", plan_text, "plan_approval_response", + sender, + "lead", + plan_text, + "plan_approval_response", {"request_id": req_id, "plan": plan_text}, ) return f"Plan submitted (request_id={req_id}). Waiting for lead approval." @@ -258,22 +280,89 @@ def _exec(self, sender: str, tool_name: str, args: dict) -> str: def _teammate_tools(self) -> list: # these base tools are unchanged from s02 return [ - {"name": "bash", "description": "Run a shell command.", - "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}, - {"name": "read_file", "description": "Read file contents.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}}, - {"name": "write_file", "description": "Write content to file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, - {"name": "edit_file", "description": "Replace exact text in file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "send_message", "description": "Send message to a teammate.", - "input_schema": {"type": "object", "properties": {"to": {"type": "string"}, "content": {"type": "string"}, "msg_type": {"type": "string", "enum": list(VALID_MSG_TYPES)}}, "required": ["to", "content"]}}, - {"name": "read_inbox", "description": "Read and drain your inbox.", - "input_schema": {"type": "object", "properties": {}}}, - {"name": "shutdown_response", "description": "Respond to a shutdown request. Approve to shut down, reject to keep working.", - "input_schema": {"type": "object", "properties": {"request_id": {"type": "string"}, "approve": {"type": "boolean"}, "reason": {"type": "string"}}, "required": ["request_id", "approve"]}}, - {"name": "plan_approval", "description": "Submit a plan for lead approval. Provide plan text.", - "input_schema": {"type": "object", "properties": {"plan": {"type": "string"}}, "required": ["plan"]}}, + { + "name": "bash", + "description": "Run a shell command.", + "input_schema": { + "type": "object", + "properties": {"command": {"type": "string"}}, + "required": ["command"], + }, + }, + { + "name": "read_file", + "description": "Read file contents.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}}, + "required": ["path"], + }, + }, + { + "name": "write_file", + "description": "Write content to file.", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "content": {"type": "string"}, + }, + "required": ["path", "content"], + }, + }, + { + "name": "edit_file", + "description": "Replace exact text in file.", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "old_text": {"type": "string"}, + "new_text": {"type": "string"}, + }, + "required": ["path", "old_text", "new_text"], + }, + }, + { + "name": "send_message", + "description": "Send message to a teammate.", + "input_schema": { + "type": "object", + "properties": { + "to": {"type": "string"}, + "content": {"type": "string"}, + "msg_type": {"type": "string", "enum": list(VALID_MSG_TYPES)}, + }, + "required": ["to", "content"], + }, + }, + { + "name": "read_inbox", + "description": "Read and drain your inbox.", + "input_schema": {"type": "object", "properties": {}}, + }, + { + "name": "shutdown_response", + "description": "Respond to a shutdown request. Approve to shut down, reject to keep working.", + "input_schema": { + "type": "object", + "properties": { + "request_id": {"type": "string"}, + "approve": {"type": "boolean"}, + "reason": {"type": "string"}, + }, + "required": ["request_id", "approve"], + }, + }, + { + "name": "plan_approval", + "description": "Submit a plan for lead approval. Provide plan text.", + "input_schema": { + "type": "object", + "properties": {"plan": {"type": "string"}}, + "required": ["plan"], + }, + }, ] def list_all(self) -> str: @@ -305,8 +394,12 @@ def _run_bash(command: str) -> str: return "Error: Dangerous command blocked" try: r = subprocess.run( - command, shell=True, cwd=WORKDIR, - capture_output=True, text=True, timeout=120, + command, + shell=True, + cwd=WORKDIR, + capture_output=True, + text=True, + timeout=120, ) out = (r.stdout + r.stderr).strip() return out[:50000] if out else "(no output)" @@ -314,11 +407,13 @@ def _run_bash(command: str) -> str: return "Error: Timeout (120s)" -def _run_read(path: str, limit: int = None) -> str: +def _run_read(path: str, limit: int | None = None) -> str: try: lines = _safe_path(path).read_text().splitlines() - if limit and limit < len(lines): - lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] + if limit is not None: + limit = max(0, int(limit)) + if limit < len(lines): + lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] return "\n".join(lines)[:50000] except Exception as e: return f"Error: {e}" @@ -352,8 +447,11 @@ def handle_shutdown_request(teammate: str) -> str: with _tracker_lock: shutdown_requests[req_id] = {"target": teammate, "status": "pending"} BUS.send( - "lead", teammate, "Please shut down gracefully.", - "shutdown_request", {"request_id": req_id}, + "lead", + teammate, + "Please shut down gracefully.", + "shutdown_request", + {"request_id": req_id}, ) return f"Shutdown request {req_id} sent to '{teammate}' (status: pending)" @@ -366,7 +464,10 @@ def handle_plan_review(request_id: str, approve: bool, feedback: str = "") -> st with _tracker_lock: req["status"] = "approved" if approve else "rejected" BUS.send( - "lead", req["from"], feedback, "plan_approval_response", + "lead", + req["from"], + feedback, + "plan_approval_response", {"request_id": request_id, "approve": approve, "feedback": feedback}, ) return f"Plan {req['status']} for '{req['from']}'" @@ -379,46 +480,142 @@ def _check_shutdown_status(request_id: str) -> str: # -- Lead tool dispatch (12 tools) -- TOOL_HANDLERS = { - "bash": lambda **kw: _run_bash(kw["command"]), - "read_file": lambda **kw: _run_read(kw["path"], kw.get("limit")), - "write_file": lambda **kw: _run_write(kw["path"], kw["content"]), - "edit_file": lambda **kw: _run_edit(kw["path"], kw["old_text"], kw["new_text"]), - "spawn_teammate": lambda **kw: TEAM.spawn(kw["name"], kw["role"], kw["prompt"]), - "list_teammates": lambda **kw: TEAM.list_all(), - "send_message": lambda **kw: BUS.send("lead", kw["to"], kw["content"], kw.get("msg_type", "message")), - "read_inbox": lambda **kw: json.dumps(BUS.read_inbox("lead"), indent=2), - "broadcast": lambda **kw: BUS.broadcast("lead", kw["content"], TEAM.member_names()), - "shutdown_request": lambda **kw: handle_shutdown_request(kw["teammate"]), + "bash": lambda **kw: _run_bash(kw["command"]), + "read_file": lambda **kw: _run_read(kw["path"], kw.get("limit")), + "write_file": lambda **kw: _run_write(kw["path"], kw["content"]), + "edit_file": lambda **kw: _run_edit(kw["path"], kw["old_text"], kw["new_text"]), + "spawn_teammate": lambda **kw: TEAM.spawn(kw["name"], kw["role"], kw["prompt"]), + "list_teammates": lambda **kw: TEAM.list_all(), + "send_message": lambda **kw: BUS.send( + "lead", kw["to"], kw["content"], kw.get("msg_type", "message") + ), + "read_inbox": lambda **kw: json.dumps(BUS.read_inbox("lead"), indent=2), + "broadcast": lambda **kw: BUS.broadcast("lead", kw["content"], TEAM.member_names()), + "shutdown_request": lambda **kw: handle_shutdown_request(kw["teammate"]), "shutdown_response": lambda **kw: _check_shutdown_status(kw.get("request_id", "")), - "plan_approval": lambda **kw: handle_plan_review(kw["request_id"], kw["approve"], kw.get("feedback", "")), + "plan_approval": lambda **kw: handle_plan_review( + kw["request_id"], kw["approve"], kw.get("feedback", "") + ), } # these base tools are unchanged from s02 TOOLS = [ - {"name": "bash", "description": "Run a shell command.", - "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}, - {"name": "read_file", "description": "Read file contents.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}}, - {"name": "write_file", "description": "Write content to file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, - {"name": "edit_file", "description": "Replace exact text in file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "spawn_teammate", "description": "Spawn a persistent teammate.", - "input_schema": {"type": "object", "properties": {"name": {"type": "string"}, "role": {"type": "string"}, "prompt": {"type": "string"}}, "required": ["name", "role", "prompt"]}}, - {"name": "list_teammates", "description": "List all teammates.", - "input_schema": {"type": "object", "properties": {}}}, - {"name": "send_message", "description": "Send a message to a teammate.", - "input_schema": {"type": "object", "properties": {"to": {"type": "string"}, "content": {"type": "string"}, "msg_type": {"type": "string", "enum": list(VALID_MSG_TYPES)}}, "required": ["to", "content"]}}, - {"name": "read_inbox", "description": "Read and drain the lead's inbox.", - "input_schema": {"type": "object", "properties": {}}}, - {"name": "broadcast", "description": "Send a message to all teammates.", - "input_schema": {"type": "object", "properties": {"content": {"type": "string"}}, "required": ["content"]}}, - {"name": "shutdown_request", "description": "Request a teammate to shut down gracefully. Returns a request_id for tracking.", - "input_schema": {"type": "object", "properties": {"teammate": {"type": "string"}}, "required": ["teammate"]}}, - {"name": "shutdown_response", "description": "Check the status of a shutdown request by request_id.", - "input_schema": {"type": "object", "properties": {"request_id": {"type": "string"}}, "required": ["request_id"]}}, - {"name": "plan_approval", "description": "Approve or reject a teammate's plan. Provide request_id + approve + optional feedback.", - "input_schema": {"type": "object", "properties": {"request_id": {"type": "string"}, "approve": {"type": "boolean"}, "feedback": {"type": "string"}}, "required": ["request_id", "approve"]}}, + { + "name": "bash", + "description": "Run a shell command.", + "input_schema": { + "type": "object", + "properties": {"command": {"type": "string"}}, + "required": ["command"], + }, + }, + { + "name": "read_file", + "description": "Read file contents.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, + "required": ["path"], + }, + }, + { + "name": "write_file", + "description": "Write content to file.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, + "required": ["path", "content"], + }, + }, + { + "name": "edit_file", + "description": "Replace exact text in file.", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "old_text": {"type": "string"}, + "new_text": {"type": "string"}, + }, + "required": ["path", "old_text", "new_text"], + }, + }, + { + "name": "spawn_teammate", + "description": "Spawn a persistent teammate.", + "input_schema": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "role": {"type": "string"}, + "prompt": {"type": "string"}, + }, + "required": ["name", "role", "prompt"], + }, + }, + { + "name": "list_teammates", + "description": "List all teammates.", + "input_schema": {"type": "object", "properties": {}}, + }, + { + "name": "send_message", + "description": "Send a message to a teammate.", + "input_schema": { + "type": "object", + "properties": { + "to": {"type": "string"}, + "content": {"type": "string"}, + "msg_type": {"type": "string", "enum": list(VALID_MSG_TYPES)}, + }, + "required": ["to", "content"], + }, + }, + { + "name": "read_inbox", + "description": "Read and drain the lead's inbox.", + "input_schema": {"type": "object", "properties": {}}, + }, + { + "name": "broadcast", + "description": "Send a message to all teammates.", + "input_schema": { + "type": "object", + "properties": {"content": {"type": "string"}}, + "required": ["content"], + }, + }, + { + "name": "shutdown_request", + "description": "Request a teammate to shut down gracefully. Returns a request_id for tracking.", + "input_schema": { + "type": "object", + "properties": {"teammate": {"type": "string"}}, + "required": ["teammate"], + }, + }, + { + "name": "shutdown_response", + "description": "Check the status of a shutdown request by request_id.", + "input_schema": { + "type": "object", + "properties": {"request_id": {"type": "string"}}, + "required": ["request_id"], + }, + }, + { + "name": "plan_approval", + "description": "Approve or reject a teammate's plan. Provide request_id + approve + optional feedback.", + "input_schema": { + "type": "object", + "properties": { + "request_id": {"type": "string"}, + "approve": {"type": "boolean"}, + "feedback": {"type": "string"}, + }, + "required": ["request_id", "approve"], + }, + }, ] @@ -426,14 +623,18 @@ def agent_loop(messages: list): while True: inbox = BUS.read_inbox("lead") if inbox: - messages.append({ - "role": "user", - "content": f"{json.dumps(inbox, indent=2)}", - }) - messages.append({ - "role": "assistant", - "content": "Noted inbox messages.", - }) + messages.append( + { + "role": "user", + "content": f"{json.dumps(inbox, indent=2)}", + } + ) + messages.append( + { + "role": "assistant", + "content": "Noted inbox messages.", + } + ) response = client.messages.create( model=MODEL, system=SYSTEM, @@ -449,15 +650,21 @@ def agent_loop(messages: list): if block.type == "tool_use": handler = TOOL_HANDLERS.get(block.name) try: - output = handler(**block.input) if handler else f"Unknown tool: {block.name}" + output = ( + handler(**block.input) + if handler + else f"Unknown tool: {block.name}" + ) except Exception as e: output = f"Error: {e}" print(f"> {block.name}: {str(output)[:200]}") - results.append({ - "type": "tool_result", - "tool_use_id": block.id, - "content": str(output), - }) + results.append( + { + "type": "tool_result", + "tool_use_id": block.id, + "content": str(output), + } + ) messages.append({"role": "user", "content": results}) diff --git a/agents/s11_autonomous_agents.py b/agents/s11_autonomous_agents.py index 856bc92c3..f4d57d95f 100644 --- a/agents/s11_autonomous_agents.py +++ b/agents/s11_autonomous_agents.py @@ -82,8 +82,14 @@ def __init__(self, inbox_dir: Path): self.dir = inbox_dir self.dir.mkdir(parents=True, exist_ok=True) - def send(self, sender: str, to: str, content: str, - msg_type: str = "message", extra: dict = None) -> str: + def send( + self, + sender: str, + to: str, + content: str, + msg_type: str = "message", + extra: dict = None, + ) -> str: if msg_type not in VALID_MSG_TYPES: return f"Error: Invalid type '{msg_type}'. Valid: {VALID_MSG_TYPES}" msg = { @@ -128,9 +134,11 @@ def scan_unclaimed_tasks() -> list: unclaimed = [] for f in sorted(TASKS_DIR.glob("task_*.json")): task = json.loads(f.read_text()) - if (task.get("status") == "pending" - and not task.get("owner") - and not task.get("blockedBy")): + if ( + task.get("status") == "pending" + and not task.get("owner") + and not task.get("blockedBy") + ): unclaimed.append(task) return unclaimed @@ -246,11 +254,13 @@ def _loop(self, name: str, role: str, prompt: str): else: output = self._exec(name, block.name, block.input) print(f" [{name}] {block.name}: {str(output)[:120]}") - results.append({ - "type": "tool_result", - "tool_use_id": block.id, - "content": str(output), - }) + results.append( + { + "type": "tool_result", + "tool_use_id": block.id, + "content": str(output), + } + ) messages.append({"role": "user", "content": results}) if idle_requested: break @@ -280,9 +290,20 @@ def _loop(self, name: str, role: str, prompt: str): ) if len(messages) <= 3: messages.insert(0, make_identity_block(name, role, team_name)) - messages.insert(1, {"role": "assistant", "content": f"I am {name}. Continuing."}) + messages.insert( + 1, + { + "role": "assistant", + "content": f"I am {name}. Continuing.", + }, + ) messages.append({"role": "user", "content": task_prompt}) - messages.append({"role": "assistant", "content": f"Claimed task #{task['id']}. Working on it."}) + messages.append( + { + "role": "assistant", + "content": f"Claimed task #{task['id']}. Working on it.", + } + ) resume = True break @@ -302,26 +323,40 @@ def _exec(self, sender: str, tool_name: str, args: dict) -> str: if tool_name == "edit_file": return _run_edit(args["path"], args["old_text"], args["new_text"]) if tool_name == "send_message": - return BUS.send(sender, args["to"], args["content"], args.get("msg_type", "message")) + return BUS.send( + sender, args["to"], args["content"], args.get("msg_type", "message") + ) if tool_name == "read_inbox": return json.dumps(BUS.read_inbox(sender), indent=2) if tool_name == "shutdown_response": req_id = args["request_id"] with _tracker_lock: if req_id in shutdown_requests: - shutdown_requests[req_id]["status"] = "approved" if args["approve"] else "rejected" + shutdown_requests[req_id]["status"] = ( + "approved" if args["approve"] else "rejected" + ) BUS.send( - sender, "lead", args.get("reason", ""), - "shutdown_response", {"request_id": req_id, "approve": args["approve"]}, + sender, + "lead", + args.get("reason", ""), + "shutdown_response", + {"request_id": req_id, "approve": args["approve"]}, ) return f"Shutdown {'approved' if args['approve'] else 'rejected'}" if tool_name == "plan_approval": plan_text = args.get("plan", "") req_id = str(uuid.uuid4())[:8] with _tracker_lock: - plan_requests[req_id] = {"from": sender, "plan": plan_text, "status": "pending"} + plan_requests[req_id] = { + "from": sender, + "plan": plan_text, + "status": "pending", + } BUS.send( - sender, "lead", plan_text, "plan_approval_response", + sender, + "lead", + plan_text, + "plan_approval_response", {"request_id": req_id, "plan": plan_text}, ) return f"Plan submitted (request_id={req_id}). Waiting for approval." @@ -332,26 +367,103 @@ def _exec(self, sender: str, tool_name: str, args: dict) -> str: def _teammate_tools(self) -> list: # these base tools are unchanged from s02 return [ - {"name": "bash", "description": "Run a shell command.", - "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}, - {"name": "read_file", "description": "Read file contents.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}}, - {"name": "write_file", "description": "Write content to file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, - {"name": "edit_file", "description": "Replace exact text in file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "send_message", "description": "Send message to a teammate.", - "input_schema": {"type": "object", "properties": {"to": {"type": "string"}, "content": {"type": "string"}, "msg_type": {"type": "string", "enum": list(VALID_MSG_TYPES)}}, "required": ["to", "content"]}}, - {"name": "read_inbox", "description": "Read and drain your inbox.", - "input_schema": {"type": "object", "properties": {}}}, - {"name": "shutdown_response", "description": "Respond to a shutdown request.", - "input_schema": {"type": "object", "properties": {"request_id": {"type": "string"}, "approve": {"type": "boolean"}, "reason": {"type": "string"}}, "required": ["request_id", "approve"]}}, - {"name": "plan_approval", "description": "Submit a plan for lead approval.", - "input_schema": {"type": "object", "properties": {"plan": {"type": "string"}}, "required": ["plan"]}}, - {"name": "idle", "description": "Signal that you have no more work. Enters idle polling phase.", - "input_schema": {"type": "object", "properties": {}}}, - {"name": "claim_task", "description": "Claim a task from the task board by ID.", - "input_schema": {"type": "object", "properties": {"task_id": {"type": "integer"}}, "required": ["task_id"]}}, + { + "name": "bash", + "description": "Run a shell command.", + "input_schema": { + "type": "object", + "properties": {"command": {"type": "string"}}, + "required": ["command"], + }, + }, + { + "name": "read_file", + "description": "Read file contents.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}}, + "required": ["path"], + }, + }, + { + "name": "write_file", + "description": "Write content to file.", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "content": {"type": "string"}, + }, + "required": ["path", "content"], + }, + }, + { + "name": "edit_file", + "description": "Replace exact text in file.", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "old_text": {"type": "string"}, + "new_text": {"type": "string"}, + }, + "required": ["path", "old_text", "new_text"], + }, + }, + { + "name": "send_message", + "description": "Send message to a teammate.", + "input_schema": { + "type": "object", + "properties": { + "to": {"type": "string"}, + "content": {"type": "string"}, + "msg_type": {"type": "string", "enum": list(VALID_MSG_TYPES)}, + }, + "required": ["to", "content"], + }, + }, + { + "name": "read_inbox", + "description": "Read and drain your inbox.", + "input_schema": {"type": "object", "properties": {}}, + }, + { + "name": "shutdown_response", + "description": "Respond to a shutdown request.", + "input_schema": { + "type": "object", + "properties": { + "request_id": {"type": "string"}, + "approve": {"type": "boolean"}, + "reason": {"type": "string"}, + }, + "required": ["request_id", "approve"], + }, + }, + { + "name": "plan_approval", + "description": "Submit a plan for lead approval.", + "input_schema": { + "type": "object", + "properties": {"plan": {"type": "string"}}, + "required": ["plan"], + }, + }, + { + "name": "idle", + "description": "Signal that you have no more work. Enters idle polling phase.", + "input_schema": {"type": "object", "properties": {}}, + }, + { + "name": "claim_task", + "description": "Claim a task from the task board by ID.", + "input_schema": { + "type": "object", + "properties": {"task_id": {"type": "integer"}}, + "required": ["task_id"], + }, + }, ] def list_all(self) -> str: @@ -383,8 +495,12 @@ def _run_bash(command: str) -> str: return "Error: Dangerous command blocked" try: r = subprocess.run( - command, shell=True, cwd=WORKDIR, - capture_output=True, text=True, timeout=120, + command, + shell=True, + cwd=WORKDIR, + capture_output=True, + text=True, + timeout=120, ) out = (r.stdout + r.stderr).strip() return out[:50000] if out else "(no output)" @@ -392,11 +508,13 @@ def _run_bash(command: str) -> str: return "Error: Timeout (120s)" -def _run_read(path: str, limit: int = None) -> str: +def _run_read(path: str, limit: int | None = None) -> str: try: lines = _safe_path(path).read_text().splitlines() - if limit and limit < len(lines): - lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] + if limit is not None: + limit = max(0, int(limit)) + if limit < len(lines): + lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] return "\n".join(lines)[:50000] except Exception as e: return f"Error: {e}" @@ -430,8 +548,11 @@ def handle_shutdown_request(teammate: str) -> str: with _tracker_lock: shutdown_requests[req_id] = {"target": teammate, "status": "pending"} BUS.send( - "lead", teammate, "Please shut down gracefully.", - "shutdown_request", {"request_id": req_id}, + "lead", + teammate, + "Please shut down gracefully.", + "shutdown_request", + {"request_id": req_id}, ) return f"Shutdown request {req_id} sent to '{teammate}'" @@ -444,7 +565,10 @@ def handle_plan_review(request_id: str, approve: bool, feedback: str = "") -> st with _tracker_lock: req["status"] = "approved" if approve else "rejected" BUS.send( - "lead", req["from"], feedback, "plan_approval_response", + "lead", + req["from"], + feedback, + "plan_approval_response", {"request_id": request_id, "approve": approve, "feedback": feedback}, ) return f"Plan {req['status']} for '{req['from']}'" @@ -457,52 +581,158 @@ def _check_shutdown_status(request_id: str) -> str: # -- Lead tool dispatch (14 tools) -- TOOL_HANDLERS = { - "bash": lambda **kw: _run_bash(kw["command"]), - "read_file": lambda **kw: _run_read(kw["path"], kw.get("limit")), - "write_file": lambda **kw: _run_write(kw["path"], kw["content"]), - "edit_file": lambda **kw: _run_edit(kw["path"], kw["old_text"], kw["new_text"]), - "spawn_teammate": lambda **kw: TEAM.spawn(kw["name"], kw["role"], kw["prompt"]), - "list_teammates": lambda **kw: TEAM.list_all(), - "send_message": lambda **kw: BUS.send("lead", kw["to"], kw["content"], kw.get("msg_type", "message")), - "read_inbox": lambda **kw: json.dumps(BUS.read_inbox("lead"), indent=2), - "broadcast": lambda **kw: BUS.broadcast("lead", kw["content"], TEAM.member_names()), - "shutdown_request": lambda **kw: handle_shutdown_request(kw["teammate"]), + "bash": lambda **kw: _run_bash(kw["command"]), + "read_file": lambda **kw: _run_read(kw["path"], kw.get("limit")), + "write_file": lambda **kw: _run_write(kw["path"], kw["content"]), + "edit_file": lambda **kw: _run_edit(kw["path"], kw["old_text"], kw["new_text"]), + "spawn_teammate": lambda **kw: TEAM.spawn(kw["name"], kw["role"], kw["prompt"]), + "list_teammates": lambda **kw: TEAM.list_all(), + "send_message": lambda **kw: BUS.send( + "lead", kw["to"], kw["content"], kw.get("msg_type", "message") + ), + "read_inbox": lambda **kw: json.dumps(BUS.read_inbox("lead"), indent=2), + "broadcast": lambda **kw: BUS.broadcast("lead", kw["content"], TEAM.member_names()), + "shutdown_request": lambda **kw: handle_shutdown_request(kw["teammate"]), "shutdown_response": lambda **kw: _check_shutdown_status(kw.get("request_id", "")), - "plan_approval": lambda **kw: handle_plan_review(kw["request_id"], kw["approve"], kw.get("feedback", "")), - "idle": lambda **kw: "Lead does not idle.", - "claim_task": lambda **kw: claim_task(kw["task_id"], "lead"), + "plan_approval": lambda **kw: handle_plan_review( + kw["request_id"], kw["approve"], kw.get("feedback", "") + ), + "idle": lambda **kw: "Lead does not idle.", + "claim_task": lambda **kw: claim_task(kw["task_id"], "lead"), } # these base tools are unchanged from s02 TOOLS = [ - {"name": "bash", "description": "Run a shell command.", - "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}, - {"name": "read_file", "description": "Read file contents.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}}, - {"name": "write_file", "description": "Write content to file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, - {"name": "edit_file", "description": "Replace exact text in file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "spawn_teammate", "description": "Spawn an autonomous teammate.", - "input_schema": {"type": "object", "properties": {"name": {"type": "string"}, "role": {"type": "string"}, "prompt": {"type": "string"}}, "required": ["name", "role", "prompt"]}}, - {"name": "list_teammates", "description": "List all teammates.", - "input_schema": {"type": "object", "properties": {}}}, - {"name": "send_message", "description": "Send a message to a teammate.", - "input_schema": {"type": "object", "properties": {"to": {"type": "string"}, "content": {"type": "string"}, "msg_type": {"type": "string", "enum": list(VALID_MSG_TYPES)}}, "required": ["to", "content"]}}, - {"name": "read_inbox", "description": "Read and drain the lead's inbox.", - "input_schema": {"type": "object", "properties": {}}}, - {"name": "broadcast", "description": "Send a message to all teammates.", - "input_schema": {"type": "object", "properties": {"content": {"type": "string"}}, "required": ["content"]}}, - {"name": "shutdown_request", "description": "Request a teammate to shut down.", - "input_schema": {"type": "object", "properties": {"teammate": {"type": "string"}}, "required": ["teammate"]}}, - {"name": "shutdown_response", "description": "Check shutdown request status.", - "input_schema": {"type": "object", "properties": {"request_id": {"type": "string"}}, "required": ["request_id"]}}, - {"name": "plan_approval", "description": "Approve or reject a teammate's plan.", - "input_schema": {"type": "object", "properties": {"request_id": {"type": "string"}, "approve": {"type": "boolean"}, "feedback": {"type": "string"}}, "required": ["request_id", "approve"]}}, - {"name": "idle", "description": "Enter idle state (for lead -- rarely used).", - "input_schema": {"type": "object", "properties": {}}}, - {"name": "claim_task", "description": "Claim a task from the board by ID.", - "input_schema": {"type": "object", "properties": {"task_id": {"type": "integer"}}, "required": ["task_id"]}}, + { + "name": "bash", + "description": "Run a shell command.", + "input_schema": { + "type": "object", + "properties": {"command": {"type": "string"}}, + "required": ["command"], + }, + }, + { + "name": "read_file", + "description": "Read file contents.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, + "required": ["path"], + }, + }, + { + "name": "write_file", + "description": "Write content to file.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, + "required": ["path", "content"], + }, + }, + { + "name": "edit_file", + "description": "Replace exact text in file.", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "old_text": {"type": "string"}, + "new_text": {"type": "string"}, + }, + "required": ["path", "old_text", "new_text"], + }, + }, + { + "name": "spawn_teammate", + "description": "Spawn an autonomous teammate.", + "input_schema": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "role": {"type": "string"}, + "prompt": {"type": "string"}, + }, + "required": ["name", "role", "prompt"], + }, + }, + { + "name": "list_teammates", + "description": "List all teammates.", + "input_schema": {"type": "object", "properties": {}}, + }, + { + "name": "send_message", + "description": "Send a message to a teammate.", + "input_schema": { + "type": "object", + "properties": { + "to": {"type": "string"}, + "content": {"type": "string"}, + "msg_type": {"type": "string", "enum": list(VALID_MSG_TYPES)}, + }, + "required": ["to", "content"], + }, + }, + { + "name": "read_inbox", + "description": "Read and drain the lead's inbox.", + "input_schema": {"type": "object", "properties": {}}, + }, + { + "name": "broadcast", + "description": "Send a message to all teammates.", + "input_schema": { + "type": "object", + "properties": {"content": {"type": "string"}}, + "required": ["content"], + }, + }, + { + "name": "shutdown_request", + "description": "Request a teammate to shut down.", + "input_schema": { + "type": "object", + "properties": {"teammate": {"type": "string"}}, + "required": ["teammate"], + }, + }, + { + "name": "shutdown_response", + "description": "Check shutdown request status.", + "input_schema": { + "type": "object", + "properties": {"request_id": {"type": "string"}}, + "required": ["request_id"], + }, + }, + { + "name": "plan_approval", + "description": "Approve or reject a teammate's plan.", + "input_schema": { + "type": "object", + "properties": { + "request_id": {"type": "string"}, + "approve": {"type": "boolean"}, + "feedback": {"type": "string"}, + }, + "required": ["request_id", "approve"], + }, + }, + { + "name": "idle", + "description": "Enter idle state (for lead -- rarely used).", + "input_schema": {"type": "object", "properties": {}}, + }, + { + "name": "claim_task", + "description": "Claim a task from the board by ID.", + "input_schema": { + "type": "object", + "properties": {"task_id": {"type": "integer"}}, + "required": ["task_id"], + }, + }, ] @@ -510,14 +740,18 @@ def agent_loop(messages: list): while True: inbox = BUS.read_inbox("lead") if inbox: - messages.append({ - "role": "user", - "content": f"{json.dumps(inbox, indent=2)}", - }) - messages.append({ - "role": "assistant", - "content": "Noted inbox messages.", - }) + messages.append( + { + "role": "user", + "content": f"{json.dumps(inbox, indent=2)}", + } + ) + messages.append( + { + "role": "assistant", + "content": "Noted inbox messages.", + } + ) response = client.messages.create( model=MODEL, system=SYSTEM, @@ -533,15 +767,21 @@ def agent_loop(messages: list): if block.type == "tool_use": handler = TOOL_HANDLERS.get(block.name) try: - output = handler(**block.input) if handler else f"Unknown tool: {block.name}" + output = ( + handler(**block.input) + if handler + else f"Unknown tool: {block.name}" + ) except Exception as e: output = f"Error: {e}" print(f"> {block.name}: {str(output)[:200]}") - results.append({ - "type": "tool_result", - "tool_use_id": block.id, - "content": str(output), - }) + results.append( + { + "type": "tool_result", + "tool_use_id": block.id, + "content": str(output), + } + ) messages.append({"role": "user", "content": results}) @@ -564,7 +804,11 @@ def agent_loop(messages: list): TASKS_DIR.mkdir(exist_ok=True) for f in sorted(TASKS_DIR.glob("task_*.json")): t = json.loads(f.read_text()) - marker = {"pending": "[ ]", "in_progress": "[>]", "completed": "[x]"}.get(t["status"], "[?]") + marker = { + "pending": "[ ]", + "in_progress": "[>]", + "completed": "[x]", + }.get(t["status"], "[?]") owner = f" @{t['owner']}" if t.get("owner") else "" print(f" {marker} #{t['id']}: {t['subject']}{owner}") continue diff --git a/agents/s12_worktree_task_isolation.py b/agents/s12_worktree_task_isolation.py index 0162de58f..05fc56f77 100644 --- a/agents/s12_worktree_task_isolation.py +++ b/agents/s12_worktree_task_isolation.py @@ -390,7 +390,9 @@ def run(self, name: str, command: str) -> str: except subprocess.TimeoutExpired: return "Error: Timeout (300s)" - def remove(self, name: str, force: bool = False, complete_task: bool = False) -> str: + def remove( + self, name: str, force: bool = False, complete_task: bool = False + ) -> str: wt = self._find(name) if not wt: return f"Error: Unknown worktree '{name}'" @@ -467,7 +469,9 @@ def keep(self, name: str) -> str: "status": "kept", }, ) - return json.dumps(kept, indent=2) if kept else f"Error: Unknown worktree '{name}'" + return ( + json.dumps(kept, indent=2) if kept else f"Error: Unknown worktree '{name}'" + ) WORKTREES = WorktreeManager(REPO_ROOT, TASKS, EVENTS) @@ -500,11 +504,13 @@ def run_bash(command: str) -> str: return "Error: Timeout (120s)" -def run_read(path: str, limit: int = None) -> str: +def run_read(path: str, limit: int | None = None) -> str: try: lines = safe_path(path).read_text().splitlines() - if limit and limit < len(lines): - lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] + if limit is not None: + limit = max(0, int(limit)) + if limit < len(lines): + lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] return "\n".join(lines)[:50000] except Exception as e: return f"Error: {e}" @@ -540,14 +546,22 @@ def run_edit(path: str, old_text: str, new_text: str) -> str: "task_create": lambda **kw: TASKS.create(kw["subject"], kw.get("description", "")), "task_list": lambda **kw: TASKS.list_all(), "task_get": lambda **kw: TASKS.get(kw["task_id"]), - "task_update": lambda **kw: TASKS.update(kw["task_id"], kw.get("status"), kw.get("owner")), - "task_bind_worktree": lambda **kw: TASKS.bind_worktree(kw["task_id"], kw["worktree"], kw.get("owner", "")), - "worktree_create": lambda **kw: WORKTREES.create(kw["name"], kw.get("task_id"), kw.get("base_ref", "HEAD")), + "task_update": lambda **kw: TASKS.update( + kw["task_id"], kw.get("status"), kw.get("owner") + ), + "task_bind_worktree": lambda **kw: TASKS.bind_worktree( + kw["task_id"], kw["worktree"], kw.get("owner", "") + ), + "worktree_create": lambda **kw: WORKTREES.create( + kw["name"], kw.get("task_id"), kw.get("base_ref", "HEAD") + ), "worktree_list": lambda **kw: WORKTREES.list_all(), "worktree_status": lambda **kw: WORKTREES.status(kw["name"]), "worktree_run": lambda **kw: WORKTREES.run(kw["name"], kw["command"]), "worktree_keep": lambda **kw: WORKTREES.keep(kw["name"]), - "worktree_remove": lambda **kw: WORKTREES.remove(kw["name"], kw.get("force", False), kw.get("complete_task", False)), + "worktree_remove": lambda **kw: WORKTREES.remove( + kw["name"], kw.get("force", False), kw.get("complete_task", False) + ), "worktree_events": lambda **kw: EVENTS.list_recent(kw.get("limit", 20)), } @@ -743,7 +757,11 @@ def agent_loop(messages: list): if block.type == "tool_use": handler = TOOL_HANDLERS.get(block.name) try: - output = handler(**block.input) if handler else f"Unknown tool: {block.name}" + output = ( + handler(**block.input) + if handler + else f"Unknown tool: {block.name}" + ) except Exception as e: output = f"Error: {e}" print(f"> {block.name}: {str(output)[:200]}") diff --git a/agents/s_full.py b/agents/s_full.py index d4dcfd3c6..d3910da4d 100644 --- a/agents/s_full.py +++ b/agents/s_full.py @@ -65,8 +65,13 @@ POLL_INTERVAL = 5 IDLE_TIMEOUT = 60 -VALID_MSG_TYPES = {"message", "broadcast", "shutdown_request", - "shutdown_response", "plan_approval_response"} +VALID_MSG_TYPES = { + "message", + "broadcast", + "shutdown_request", + "shutdown_response", + "plan_approval_response", +} # === SECTION: base_tools === @@ -76,27 +81,38 @@ def safe_path(p: str) -> Path: raise ValueError(f"Path escapes workspace: {p}") return path + def run_bash(command: str) -> str: dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"] if any(d in command for d in dangerous): return "Error: Dangerous command blocked" try: - r = subprocess.run(command, shell=True, cwd=WORKDIR, - capture_output=True, text=True, timeout=120) + r = subprocess.run( + command, + shell=True, + cwd=WORKDIR, + capture_output=True, + text=True, + timeout=120, + ) out = (r.stdout + r.stderr).strip() return out[:50000] if out else "(no output)" except subprocess.TimeoutExpired: return "Error: Timeout (120s)" -def run_read(path: str, limit: int = None) -> str: + +def run_read(path: str, limit: int | None = None) -> str: try: lines = safe_path(path).read_text().splitlines() - if limit and limit < len(lines): - lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] + if limit is not None: + limit = max(0, int(limit)) + if limit < len(lines): + lines = lines[:limit] + [f"... ({len(lines) - limit} more)"] return "\n".join(lines)[:50000] except Exception as e: return f"Error: {e}" + def run_write(path: str, content: str) -> str: try: fp = safe_path(path) @@ -106,6 +122,7 @@ def run_write(path: str, content: str) -> str: except Exception as e: return f"Error: {e}" + def run_edit(path: str, old_text: str, new_text: str) -> str: try: fp = safe_path(path) @@ -129,23 +146,33 @@ def update(self, items: list) -> str: content = str(item.get("content", "")).strip() status = str(item.get("status", "pending")).lower() af = str(item.get("activeForm", "")).strip() - if not content: raise ValueError(f"Item {i}: content required") + if not content: + raise ValueError(f"Item {i}: content required") if status not in ("pending", "in_progress", "completed"): raise ValueError(f"Item {i}: invalid status '{status}'") - if not af: raise ValueError(f"Item {i}: activeForm required") - if status == "in_progress": ip += 1 + if not af: + raise ValueError(f"Item {i}: activeForm required") + if status == "in_progress": + ip += 1 validated.append({"content": content, "status": status, "activeForm": af}) - if len(validated) > 20: raise ValueError("Max 20 todos") - if ip > 1: raise ValueError("Only one in_progress allowed") + if len(validated) > 20: + raise ValueError("Max 20 todos") + if ip > 1: + raise ValueError("Only one in_progress allowed") self.items = validated return self.render() def render(self) -> str: - if not self.items: return "No todos." + if not self.items: + return "No todos." lines = [] for item in self.items: - m = {"completed": "[x]", "in_progress": "[>]", "pending": "[ ]"}.get(item["status"], "[?]") - suffix = f" <- {item['activeForm']}" if item["status"] == "in_progress" else "" + m = {"completed": "[x]", "in_progress": "[>]", "pending": "[ ]"}.get( + item["status"], "[?]" + ) + suffix = ( + f" <- {item['activeForm']}" if item["status"] == "in_progress" else "" + ) lines.append(f"{m} {item['content']}{suffix}") done = sum(1 for t in self.items if t["status"] == "completed") lines.append(f"\n({done}/{len(self.items)} completed)") @@ -158,17 +185,52 @@ def has_open_items(self) -> bool: # === SECTION: subagent (s04) === def run_subagent(prompt: str, agent_type: str = "Explore") -> str: sub_tools = [ - {"name": "bash", "description": "Run command.", - "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}, - {"name": "read_file", "description": "Read file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}}, + { + "name": "bash", + "description": "Run command.", + "input_schema": { + "type": "object", + "properties": {"command": {"type": "string"}}, + "required": ["command"], + }, + }, + { + "name": "read_file", + "description": "Read file.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}}, + "required": ["path"], + }, + }, ] if agent_type != "Explore": sub_tools += [ - {"name": "write_file", "description": "Write file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, - {"name": "edit_file", "description": "Edit file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, + { + "name": "write_file", + "description": "Write file.", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "content": {"type": "string"}, + }, + "required": ["path", "content"], + }, + }, + { + "name": "edit_file", + "description": "Edit file.", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "old_text": {"type": "string"}, + "new_text": {"type": "string"}, + }, + "required": ["path", "old_text", "new_text"], + }, + }, ] sub_handlers = { "bash": lambda **kw: run_bash(kw["command"]), @@ -179,7 +241,9 @@ def run_subagent(prompt: str, agent_type: str = "Explore") -> str: sub_msgs = [{"role": "user", "content": prompt}] resp = None for _ in range(30): - resp = client.messages.create(model=MODEL, messages=sub_msgs, tools=sub_tools, max_tokens=8000) + resp = client.messages.create( + model=MODEL, messages=sub_msgs, tools=sub_tools, max_tokens=8000 + ) sub_msgs.append({"role": "assistant", "content": resp.content}) if resp.stop_reason != "tool_use": break @@ -187,10 +251,19 @@ def run_subagent(prompt: str, agent_type: str = "Explore") -> str: for b in resp.content: if b.type == "tool_use": h = sub_handlers.get(b.name, lambda **kw: "Unknown tool") - results.append({"type": "tool_result", "tool_use_id": b.id, "content": str(h(**b.input))[:50000]}) + results.append( + { + "type": "tool_result", + "tool_use_id": b.id, + "content": str(h(**b.input))[:50000], + } + ) sub_msgs.append({"role": "user", "content": results}) if resp: - return "".join(b.text for b in resp.content if hasattr(b, "text")) or "(no summary)" + return ( + "".join(b.text for b in resp.content if hasattr(b, "text")) + or "(no summary)" + ) return "(subagent failed)" @@ -213,19 +286,25 @@ def __init__(self, skills_dir: Path): self.skills[name] = {"meta": meta, "body": body} def descriptions(self) -> str: - if not self.skills: return "(no skills)" - return "\n".join(f" - {n}: {s['meta'].get('description', '-')}" for n, s in self.skills.items()) + if not self.skills: + return "(no skills)" + return "\n".join( + f" - {n}: {s['meta'].get('description', '-')}" + for n, s in self.skills.items() + ) def load(self, name: str) -> str: s = self.skills.get(name) - if not s: return f"Error: Unknown skill '{name}'. Available: {', '.join(self.skills.keys())}" - return f"\n{s['body']}\n" + if not s: + return f"Error: Unknown skill '{name}'. Available: {', '.join(self.skills.keys())}" + return f'\n{s["body"]}\n' # === SECTION: compression (s06) === def estimate_tokens(messages: list) -> int: return len(json.dumps(messages, default=str)) // 4 + def microcompact(messages: list): indices = [] for i, msg in enumerate(messages): @@ -239,6 +318,7 @@ def microcompact(messages: list): if isinstance(part.get("content"), str) and len(part["content"]) > 100: part["content"] = "[cleared]" + def auto_compact(messages: list) -> list: TRANSCRIPT_DIR.mkdir(exist_ok=True) path = TRANSCRIPT_DIR / f"transcript_{int(time.time())}.jsonl" @@ -248,13 +328,18 @@ def auto_compact(messages: list) -> list: conv_text = json.dumps(messages, default=str)[:80000] resp = client.messages.create( model=MODEL, - messages=[{"role": "user", "content": f"Summarize for continuity:\n{conv_text}"}], + messages=[ + {"role": "user", "content": f"Summarize for continuity:\n{conv_text}"} + ], max_tokens=2000, ) summary = resp.content[0].text return [ {"role": "user", "content": f"[Compressed. Transcript: {path}]\n{summary}"}, - {"role": "assistant", "content": "Understood. Continuing with summary context."}, + { + "role": "assistant", + "content": "Understood. Continuing with summary context.", + }, ] @@ -269,23 +354,36 @@ def _next_id(self) -> int: def _load(self, tid: int) -> dict: p = TASKS_DIR / f"task_{tid}.json" - if not p.exists(): raise ValueError(f"Task {tid} not found") + if not p.exists(): + raise ValueError(f"Task {tid} not found") return json.loads(p.read_text()) def _save(self, task: dict): (TASKS_DIR / f"task_{task['id']}.json").write_text(json.dumps(task, indent=2)) def create(self, subject: str, description: str = "") -> str: - task = {"id": self._next_id(), "subject": subject, "description": description, - "status": "pending", "owner": None, "blockedBy": [], "blocks": []} + task = { + "id": self._next_id(), + "subject": subject, + "description": description, + "status": "pending", + "owner": None, + "blockedBy": [], + "blocks": [], + } self._save(task) return json.dumps(task, indent=2) def get(self, tid: int) -> str: return json.dumps(self._load(tid), indent=2) - def update(self, tid: int, status: str = None, - add_blocked_by: list = None, add_blocks: list = None) -> str: + def update( + self, + tid: int, + status: str = None, + add_blocked_by: list = None, + add_blocks: list = None, + ) -> str: task = self._load(tid) if status: task["status"] = status @@ -306,11 +404,16 @@ def update(self, tid: int, status: str = None, return json.dumps(task, indent=2) def list_all(self) -> str: - tasks = [json.loads(f.read_text()) for f in sorted(TASKS_DIR.glob("task_*.json"))] - if not tasks: return "No tasks." + tasks = [ + json.loads(f.read_text()) for f in sorted(TASKS_DIR.glob("task_*.json")) + ] + if not tasks: + return "No tasks." lines = [] for t in tasks: - m = {"pending": "[ ]", "in_progress": "[>]", "completed": "[x]"}.get(t["status"], "[?]") + m = {"pending": "[ ]", "in_progress": "[>]", "completed": "[x]"}.get( + t["status"], "[?]" + ) owner = f" @{t['owner']}" if t.get("owner") else "" blocked = f" (blocked by: {t['blockedBy']})" if t.get("blockedBy") else "" lines.append(f"{m} #{t['id']}: {t['subject']}{owner}{blocked}") @@ -333,25 +436,50 @@ def __init__(self): def run(self, command: str, timeout: int = 120) -> str: tid = str(uuid.uuid4())[:8] self.tasks[tid] = {"status": "running", "command": command, "result": None} - threading.Thread(target=self._exec, args=(tid, command, timeout), daemon=True).start() + threading.Thread( + target=self._exec, args=(tid, command, timeout), daemon=True + ).start() return f"Background task {tid} started: {command[:80]}" def _exec(self, tid: str, command: str, timeout: int): try: - r = subprocess.run(command, shell=True, cwd=WORKDIR, - capture_output=True, text=True, timeout=timeout) + r = subprocess.run( + command, + shell=True, + cwd=WORKDIR, + capture_output=True, + text=True, + timeout=timeout, + ) output = (r.stdout + r.stderr).strip()[:50000] - self.tasks[tid].update({"status": "completed", "result": output or "(no output)"}) + self.tasks[tid].update( + {"status": "completed", "result": output or "(no output)"} + ) except Exception as e: self.tasks[tid].update({"status": "error", "result": str(e)}) - self.notifications.put({"task_id": tid, "status": self.tasks[tid]["status"], - "result": self.tasks[tid]["result"][:500]}) + self.notifications.put( + { + "task_id": tid, + "status": self.tasks[tid]["status"], + "result": self.tasks[tid]["result"][:500], + } + ) def check(self, tid: str = None) -> str: if tid: t = self.tasks.get(tid) - return f"[{t['status']}] {t.get('result', '(running)')}" if t else f"Unknown: {tid}" - return "\n".join(f"{k}: [{v['status']}] {v['command'][:60]}" for k, v in self.tasks.items()) or "No bg tasks." + return ( + f"[{t['status']}] {t.get('result', '(running)')}" + if t + else f"Unknown: {tid}" + ) + return ( + "\n".join( + f"{k}: [{v['status']}] {v['command'][:60]}" + for k, v in self.tasks.items() + ) + or "No bg tasks." + ) def drain(self) -> list: notifs = [] @@ -365,18 +493,30 @@ class MessageBus: def __init__(self): INBOX_DIR.mkdir(parents=True, exist_ok=True) - def send(self, sender: str, to: str, content: str, - msg_type: str = "message", extra: dict = None) -> str: - msg = {"type": msg_type, "from": sender, "content": content, - "timestamp": time.time()} - if extra: msg.update(extra) + def send( + self, + sender: str, + to: str, + content: str, + msg_type: str = "message", + extra: dict = None, + ) -> str: + msg = { + "type": msg_type, + "from": sender, + "content": content, + "timestamp": time.time(), + } + if extra: + msg.update(extra) with open(INBOX_DIR / f"{to}.jsonl", "a") as f: f.write(json.dumps(msg) + "\n") return f"Sent {msg_type} to {to}" def read_inbox(self, name: str) -> list: path = INBOX_DIR / f"{name}.jsonl" - if not path.exists(): return [] + if not path.exists(): + return [] msgs = [json.loads(l) for l in path.read_text().strip().splitlines() if l] path.write_text("") return msgs @@ -415,7 +555,8 @@ def _save(self): def _find(self, name: str) -> dict: for m in self.config["members"]: - if m["name"] == name: return m + if m["name"] == name: + return m return None def spawn(self, name: str, role: str, prompt: str) -> str: @@ -429,7 +570,9 @@ def spawn(self, name: str, role: str, prompt: str) -> str: member = {"name": name, "role": role, "status": "working"} self.config["members"].append(member) self._save() - threading.Thread(target=self._loop, args=(name, role, prompt), daemon=True).start() + threading.Thread( + target=self._loop, args=(name, role, prompt), daemon=True + ).start() return f"Spawned '{name}' (role: {role})" def _set_status(self, name: str, status: str): @@ -440,17 +583,81 @@ def _set_status(self, name: str, status: str): def _loop(self, name: str, role: str, prompt: str): team_name = self.config["team_name"] - sys_prompt = (f"You are '{name}', role: {role}, team: {team_name}, at {WORKDIR}. " - f"Use idle when done with current work. You may auto-claim tasks.") + sys_prompt = ( + f"You are '{name}', role: {role}, team: {team_name}, at {WORKDIR}. " + f"Use idle when done with current work. You may auto-claim tasks." + ) messages = [{"role": "user", "content": prompt}] tools = [ - {"name": "bash", "description": "Run command.", "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}, - {"name": "read_file", "description": "Read file.", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}}, - {"name": "write_file", "description": "Write file.", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, - {"name": "edit_file", "description": "Edit file.", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "send_message", "description": "Send message.", "input_schema": {"type": "object", "properties": {"to": {"type": "string"}, "content": {"type": "string"}}, "required": ["to", "content"]}}, - {"name": "idle", "description": "Signal no more work.", "input_schema": {"type": "object", "properties": {}}}, - {"name": "claim_task", "description": "Claim task by ID.", "input_schema": {"type": "object", "properties": {"task_id": {"type": "integer"}}, "required": ["task_id"]}}, + { + "name": "bash", + "description": "Run command.", + "input_schema": { + "type": "object", + "properties": {"command": {"type": "string"}}, + "required": ["command"], + }, + }, + { + "name": "read_file", + "description": "Read file.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}}, + "required": ["path"], + }, + }, + { + "name": "write_file", + "description": "Write file.", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "content": {"type": "string"}, + }, + "required": ["path", "content"], + }, + }, + { + "name": "edit_file", + "description": "Edit file.", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "old_text": {"type": "string"}, + "new_text": {"type": "string"}, + }, + "required": ["path", "old_text", "new_text"], + }, + }, + { + "name": "send_message", + "description": "Send message.", + "input_schema": { + "type": "object", + "properties": { + "to": {"type": "string"}, + "content": {"type": "string"}, + }, + "required": ["to", "content"], + }, + }, + { + "name": "idle", + "description": "Signal no more work.", + "input_schema": {"type": "object", "properties": {}}, + }, + { + "name": "claim_task", + "description": "Claim task by ID.", + "input_schema": { + "type": "object", + "properties": {"task_id": {"type": "integer"}}, + "required": ["task_id"], + }, + }, ] while True: # -- WORK PHASE -- @@ -463,8 +670,12 @@ def _loop(self, name: str, role: str, prompt: str): messages.append({"role": "user", "content": json.dumps(msg)}) try: response = client.messages.create( - model=MODEL, system=sys_prompt, messages=messages, - tools=tools, max_tokens=8000) + model=MODEL, + system=sys_prompt, + messages=messages, + tools=tools, + max_tokens=8000, + ) except Exception: self._set_status(name, "shutdown") return @@ -481,15 +692,31 @@ def _loop(self, name: str, role: str, prompt: str): elif block.name == "claim_task": output = self.task_mgr.claim(block.input["task_id"], name) elif block.name == "send_message": - output = self.bus.send(name, block.input["to"], block.input["content"]) + output = self.bus.send( + name, block.input["to"], block.input["content"] + ) else: - dispatch = {"bash": lambda **kw: run_bash(kw["command"]), - "read_file": lambda **kw: run_read(kw["path"]), - "write_file": lambda **kw: run_write(kw["path"], kw["content"]), - "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"])} - output = dispatch.get(block.name, lambda **kw: "Unknown")(**block.input) + dispatch = { + "bash": lambda **kw: run_bash(kw["command"]), + "read_file": lambda **kw: run_read(kw["path"]), + "write_file": lambda **kw: run_write( + kw["path"], kw["content"] + ), + "edit_file": lambda **kw: run_edit( + kw["path"], kw["old_text"], kw["new_text"] + ), + } + output = dispatch.get(block.name, lambda **kw: "Unknown")( + **block.input + ) print(f" [{name}] {block.name}: {str(output)[:120]}") - results.append({"type": "tool_result", "tool_use_id": block.id, "content": str(output)}) + results.append( + { + "type": "tool_result", + "tool_use_id": block.id, + "content": str(output), + } + ) messages.append({"role": "user", "content": results}) if idle_requested: break @@ -510,19 +737,43 @@ def _loop(self, name: str, role: str, prompt: str): unclaimed = [] for f in sorted(TASKS_DIR.glob("task_*.json")): t = json.loads(f.read_text()) - if t.get("status") == "pending" and not t.get("owner") and not t.get("blockedBy"): + if ( + t.get("status") == "pending" + and not t.get("owner") + and not t.get("blockedBy") + ): unclaimed.append(t) if unclaimed: task = unclaimed[0] self.task_mgr.claim(task["id"], name) # Identity re-injection for compressed contexts if len(messages) <= 3: - messages.insert(0, {"role": "user", "content": - f"You are '{name}', role: {role}, team: {team_name}."}) - messages.insert(1, {"role": "assistant", "content": f"I am {name}. Continuing."}) - messages.append({"role": "user", "content": - f"Task #{task['id']}: {task['subject']}\n{task.get('description', '')}"}) - messages.append({"role": "assistant", "content": f"Claimed task #{task['id']}. Working on it."}) + messages.insert( + 0, + { + "role": "user", + "content": f"You are '{name}', role: {role}, team: {team_name}.", + }, + ) + messages.insert( + 1, + { + "role": "assistant", + "content": f"I am {name}. Continuing.", + }, + ) + messages.append( + { + "role": "user", + "content": f"Task #{task['id']}: {task['subject']}\n{task.get('description', '')}", + } + ) + messages.append( + { + "role": "assistant", + "content": f"Claimed task #{task['id']}. Working on it.", + } + ) resume = True break if not resume: @@ -531,7 +782,8 @@ def _loop(self, name: str, role: str, prompt: str): self._set_status(name, "working") def list_all(self) -> str: - if not self.config["members"]: return "No teammates." + if not self.config["members"]: + return "No teammates." lines = [f"Team: {self.config['team_name']}"] for m in self.config["members"]: lines.append(f" {m['name']} ({m['role']}): {m['status']}") @@ -560,93 +812,306 @@ def member_names(self) -> list: def handle_shutdown_request(teammate: str) -> str: req_id = str(uuid.uuid4())[:8] shutdown_requests[req_id] = {"target": teammate, "status": "pending"} - BUS.send("lead", teammate, "Please shut down.", "shutdown_request", {"request_id": req_id}) + BUS.send( + "lead", + teammate, + "Please shut down.", + "shutdown_request", + {"request_id": req_id}, + ) return f"Shutdown request {req_id} sent to '{teammate}'" + # === SECTION: plan_approval (s10) === def handle_plan_review(request_id: str, approve: bool, feedback: str = "") -> str: req = plan_requests.get(request_id) - if not req: return f"Error: Unknown plan request_id '{request_id}'" + if not req: + return f"Error: Unknown plan request_id '{request_id}'" req["status"] = "approved" if approve else "rejected" - BUS.send("lead", req["from"], feedback, "plan_approval_response", - {"request_id": request_id, "approve": approve, "feedback": feedback}) + BUS.send( + "lead", + req["from"], + feedback, + "plan_approval_response", + {"request_id": request_id, "approve": approve, "feedback": feedback}, + ) return f"Plan {req['status']} for '{req['from']}'" # === SECTION: tool_dispatch (s02) === TOOL_HANDLERS = { - "bash": lambda **kw: run_bash(kw["command"]), - "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")), - "write_file": lambda **kw: run_write(kw["path"], kw["content"]), - "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]), - "TodoWrite": lambda **kw: TODO.update(kw["items"]), - "task": lambda **kw: run_subagent(kw["prompt"], kw.get("agent_type", "Explore")), - "load_skill": lambda **kw: SKILLS.load(kw["name"]), - "compress": lambda **kw: "Compressing...", - "background_run": lambda **kw: BG.run(kw["command"], kw.get("timeout", 120)), + "bash": lambda **kw: run_bash(kw["command"]), + "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")), + "write_file": lambda **kw: run_write(kw["path"], kw["content"]), + "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]), + "TodoWrite": lambda **kw: TODO.update(kw["items"]), + "task": lambda **kw: run_subagent(kw["prompt"], kw.get("agent_type", "Explore")), + "load_skill": lambda **kw: SKILLS.load(kw["name"]), + "compress": lambda **kw: "Compressing...", + "background_run": lambda **kw: BG.run(kw["command"], kw.get("timeout", 120)), "check_background": lambda **kw: BG.check(kw.get("task_id")), - "task_create": lambda **kw: TASK_MGR.create(kw["subject"], kw.get("description", "")), - "task_get": lambda **kw: TASK_MGR.get(kw["task_id"]), - "task_update": lambda **kw: TASK_MGR.update(kw["task_id"], kw.get("status"), kw.get("add_blocked_by"), kw.get("add_blocks")), - "task_list": lambda **kw: TASK_MGR.list_all(), - "spawn_teammate": lambda **kw: TEAM.spawn(kw["name"], kw["role"], kw["prompt"]), - "list_teammates": lambda **kw: TEAM.list_all(), - "send_message": lambda **kw: BUS.send("lead", kw["to"], kw["content"], kw.get("msg_type", "message")), - "read_inbox": lambda **kw: json.dumps(BUS.read_inbox("lead"), indent=2), - "broadcast": lambda **kw: BUS.broadcast("lead", kw["content"], TEAM.member_names()), + "task_create": lambda **kw: TASK_MGR.create( + kw["subject"], kw.get("description", "") + ), + "task_get": lambda **kw: TASK_MGR.get(kw["task_id"]), + "task_update": lambda **kw: TASK_MGR.update( + kw["task_id"], kw.get("status"), kw.get("add_blocked_by"), kw.get("add_blocks") + ), + "task_list": lambda **kw: TASK_MGR.list_all(), + "spawn_teammate": lambda **kw: TEAM.spawn(kw["name"], kw["role"], kw["prompt"]), + "list_teammates": lambda **kw: TEAM.list_all(), + "send_message": lambda **kw: BUS.send( + "lead", kw["to"], kw["content"], kw.get("msg_type", "message") + ), + "read_inbox": lambda **kw: json.dumps(BUS.read_inbox("lead"), indent=2), + "broadcast": lambda **kw: BUS.broadcast("lead", kw["content"], TEAM.member_names()), "shutdown_request": lambda **kw: handle_shutdown_request(kw["teammate"]), - "plan_approval": lambda **kw: handle_plan_review(kw["request_id"], kw["approve"], kw.get("feedback", "")), - "idle": lambda **kw: "Lead does not idle.", - "claim_task": lambda **kw: TASK_MGR.claim(kw["task_id"], "lead"), + "plan_approval": lambda **kw: handle_plan_review( + kw["request_id"], kw["approve"], kw.get("feedback", "") + ), + "idle": lambda **kw: "Lead does not idle.", + "claim_task": lambda **kw: TASK_MGR.claim(kw["task_id"], "lead"), } TOOLS = [ - {"name": "bash", "description": "Run a shell command.", - "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}, - {"name": "read_file", "description": "Read file contents.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}}, - {"name": "write_file", "description": "Write content to file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, - {"name": "edit_file", "description": "Replace exact text in file.", - "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "TodoWrite", "description": "Update task tracking list.", - "input_schema": {"type": "object", "properties": {"items": {"type": "array", "items": {"type": "object", "properties": {"content": {"type": "string"}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed"]}, "activeForm": {"type": "string"}}, "required": ["content", "status", "activeForm"]}}}, "required": ["items"]}}, - {"name": "task", "description": "Spawn a subagent for isolated exploration or work.", - "input_schema": {"type": "object", "properties": {"prompt": {"type": "string"}, "agent_type": {"type": "string", "enum": ["Explore", "general-purpose"]}}, "required": ["prompt"]}}, - {"name": "load_skill", "description": "Load specialized knowledge by name.", - "input_schema": {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}}, - {"name": "compress", "description": "Manually compress conversation context.", - "input_schema": {"type": "object", "properties": {}}}, - {"name": "background_run", "description": "Run command in background thread.", - "input_schema": {"type": "object", "properties": {"command": {"type": "string"}, "timeout": {"type": "integer"}}, "required": ["command"]}}, - {"name": "check_background", "description": "Check background task status.", - "input_schema": {"type": "object", "properties": {"task_id": {"type": "string"}}}}, - {"name": "task_create", "description": "Create a persistent file task.", - "input_schema": {"type": "object", "properties": {"subject": {"type": "string"}, "description": {"type": "string"}}, "required": ["subject"]}}, - {"name": "task_get", "description": "Get task details by ID.", - "input_schema": {"type": "object", "properties": {"task_id": {"type": "integer"}}, "required": ["task_id"]}}, - {"name": "task_update", "description": "Update task status or dependencies.", - "input_schema": {"type": "object", "properties": {"task_id": {"type": "integer"}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed", "deleted"]}, "add_blocked_by": {"type": "array", "items": {"type": "integer"}}, "add_blocks": {"type": "array", "items": {"type": "integer"}}}, "required": ["task_id"]}}, - {"name": "task_list", "description": "List all tasks.", - "input_schema": {"type": "object", "properties": {}}}, - {"name": "spawn_teammate", "description": "Spawn a persistent autonomous teammate.", - "input_schema": {"type": "object", "properties": {"name": {"type": "string"}, "role": {"type": "string"}, "prompt": {"type": "string"}}, "required": ["name", "role", "prompt"]}}, - {"name": "list_teammates", "description": "List all teammates.", - "input_schema": {"type": "object", "properties": {}}}, - {"name": "send_message", "description": "Send a message to a teammate.", - "input_schema": {"type": "object", "properties": {"to": {"type": "string"}, "content": {"type": "string"}, "msg_type": {"type": "string", "enum": list(VALID_MSG_TYPES)}}, "required": ["to", "content"]}}, - {"name": "read_inbox", "description": "Read and drain the lead's inbox.", - "input_schema": {"type": "object", "properties": {}}}, - {"name": "broadcast", "description": "Send message to all teammates.", - "input_schema": {"type": "object", "properties": {"content": {"type": "string"}}, "required": ["content"]}}, - {"name": "shutdown_request", "description": "Request a teammate to shut down.", - "input_schema": {"type": "object", "properties": {"teammate": {"type": "string"}}, "required": ["teammate"]}}, - {"name": "plan_approval", "description": "Approve or reject a teammate's plan.", - "input_schema": {"type": "object", "properties": {"request_id": {"type": "string"}, "approve": {"type": "boolean"}, "feedback": {"type": "string"}}, "required": ["request_id", "approve"]}}, - {"name": "idle", "description": "Enter idle state.", - "input_schema": {"type": "object", "properties": {}}}, - {"name": "claim_task", "description": "Claim a task from the board.", - "input_schema": {"type": "object", "properties": {"task_id": {"type": "integer"}}, "required": ["task_id"]}}, + { + "name": "bash", + "description": "Run a shell command.", + "input_schema": { + "type": "object", + "properties": {"command": {"type": "string"}}, + "required": ["command"], + }, + }, + { + "name": "read_file", + "description": "Read file contents.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, + "required": ["path"], + }, + }, + { + "name": "write_file", + "description": "Write content to file.", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, + "required": ["path", "content"], + }, + }, + { + "name": "edit_file", + "description": "Replace exact text in file.", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "old_text": {"type": "string"}, + "new_text": {"type": "string"}, + }, + "required": ["path", "old_text", "new_text"], + }, + }, + { + "name": "TodoWrite", + "description": "Update task tracking list.", + "input_schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "content": {"type": "string"}, + "status": { + "type": "string", + "enum": ["pending", "in_progress", "completed"], + }, + "activeForm": {"type": "string"}, + }, + "required": ["content", "status", "activeForm"], + }, + } + }, + "required": ["items"], + }, + }, + { + "name": "task", + "description": "Spawn a subagent for isolated exploration or work.", + "input_schema": { + "type": "object", + "properties": { + "prompt": {"type": "string"}, + "agent_type": { + "type": "string", + "enum": ["Explore", "general-purpose"], + }, + }, + "required": ["prompt"], + }, + }, + { + "name": "load_skill", + "description": "Load specialized knowledge by name.", + "input_schema": { + "type": "object", + "properties": {"name": {"type": "string"}}, + "required": ["name"], + }, + }, + { + "name": "compress", + "description": "Manually compress conversation context.", + "input_schema": {"type": "object", "properties": {}}, + }, + { + "name": "background_run", + "description": "Run command in background thread.", + "input_schema": { + "type": "object", + "properties": { + "command": {"type": "string"}, + "timeout": {"type": "integer"}, + }, + "required": ["command"], + }, + }, + { + "name": "check_background", + "description": "Check background task status.", + "input_schema": { + "type": "object", + "properties": {"task_id": {"type": "string"}}, + }, + }, + { + "name": "task_create", + "description": "Create a persistent file task.", + "input_schema": { + "type": "object", + "properties": { + "subject": {"type": "string"}, + "description": {"type": "string"}, + }, + "required": ["subject"], + }, + }, + { + "name": "task_get", + "description": "Get task details by ID.", + "input_schema": { + "type": "object", + "properties": {"task_id": {"type": "integer"}}, + "required": ["task_id"], + }, + }, + { + "name": "task_update", + "description": "Update task status or dependencies.", + "input_schema": { + "type": "object", + "properties": { + "task_id": {"type": "integer"}, + "status": { + "type": "string", + "enum": ["pending", "in_progress", "completed", "deleted"], + }, + "add_blocked_by": {"type": "array", "items": {"type": "integer"}}, + "add_blocks": {"type": "array", "items": {"type": "integer"}}, + }, + "required": ["task_id"], + }, + }, + { + "name": "task_list", + "description": "List all tasks.", + "input_schema": {"type": "object", "properties": {}}, + }, + { + "name": "spawn_teammate", + "description": "Spawn a persistent autonomous teammate.", + "input_schema": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "role": {"type": "string"}, + "prompt": {"type": "string"}, + }, + "required": ["name", "role", "prompt"], + }, + }, + { + "name": "list_teammates", + "description": "List all teammates.", + "input_schema": {"type": "object", "properties": {}}, + }, + { + "name": "send_message", + "description": "Send a message to a teammate.", + "input_schema": { + "type": "object", + "properties": { + "to": {"type": "string"}, + "content": {"type": "string"}, + "msg_type": {"type": "string", "enum": list(VALID_MSG_TYPES)}, + }, + "required": ["to", "content"], + }, + }, + { + "name": "read_inbox", + "description": "Read and drain the lead's inbox.", + "input_schema": {"type": "object", "properties": {}}, + }, + { + "name": "broadcast", + "description": "Send message to all teammates.", + "input_schema": { + "type": "object", + "properties": {"content": {"type": "string"}}, + "required": ["content"], + }, + }, + { + "name": "shutdown_request", + "description": "Request a teammate to shut down.", + "input_schema": { + "type": "object", + "properties": {"teammate": {"type": "string"}}, + "required": ["teammate"], + }, + }, + { + "name": "plan_approval", + "description": "Approve or reject a teammate's plan.", + "input_schema": { + "type": "object", + "properties": { + "request_id": {"type": "string"}, + "approve": {"type": "boolean"}, + "feedback": {"type": "string"}, + }, + "required": ["request_id", "approve"], + }, + }, + { + "name": "idle", + "description": "Enter idle state.", + "input_schema": {"type": "object", "properties": {}}, + }, + { + "name": "claim_task", + "description": "Claim a task from the board.", + "input_schema": { + "type": "object", + "properties": {"task_id": {"type": "integer"}}, + "required": ["task_id"], + }, + }, ] @@ -662,18 +1127,35 @@ def agent_loop(messages: list): # s08: drain background notifications notifs = BG.drain() if notifs: - txt = "\n".join(f"[bg:{n['task_id']}] {n['status']}: {n['result']}" for n in notifs) - messages.append({"role": "user", "content": f"\n{txt}\n"}) - messages.append({"role": "assistant", "content": "Noted background results."}) + txt = "\n".join( + f"[bg:{n['task_id']}] {n['status']}: {n['result']}" for n in notifs + ) + messages.append( + { + "role": "user", + "content": f"\n{txt}\n", + } + ) + messages.append( + {"role": "assistant", "content": "Noted background results."} + ) # s10: check lead inbox inbox = BUS.read_inbox("lead") if inbox: - messages.append({"role": "user", "content": f"{json.dumps(inbox, indent=2)}"}) + messages.append( + { + "role": "user", + "content": f"{json.dumps(inbox, indent=2)}", + } + ) messages.append({"role": "assistant", "content": "Noted inbox messages."}) # LLM call response = client.messages.create( - model=MODEL, system=SYSTEM, messages=messages, - tools=TOOLS, max_tokens=8000, + model=MODEL, + system=SYSTEM, + messages=messages, + tools=TOOLS, + max_tokens=8000, ) messages.append({"role": "assistant", "content": response.content}) if response.stop_reason != "tool_use": @@ -688,17 +1170,29 @@ def agent_loop(messages: list): manual_compress = True handler = TOOL_HANDLERS.get(block.name) try: - output = handler(**block.input) if handler else f"Unknown tool: {block.name}" + output = ( + handler(**block.input) + if handler + else f"Unknown tool: {block.name}" + ) except Exception as e: output = f"Error: {e}" print(f"> {block.name}: {str(output)[:200]}") - results.append({"type": "tool_result", "tool_use_id": block.id, "content": str(output)}) + results.append( + { + "type": "tool_result", + "tool_use_id": block.id, + "content": str(output), + } + ) if block.name == "TodoWrite": used_todo = True # s03: nag reminder (only when todo workflow is active) rounds_without_todo = 0 if used_todo else rounds_without_todo + 1 if TODO.has_open_items() and rounds_without_todo >= 3: - results.insert(0, {"type": "text", "text": "Update your todos."}) + results.insert( + 0, {"type": "text", "text": "Update your todos."} + ) messages.append({"role": "user", "content": results}) # s06: manual compress if manual_compress: