Files
collection-claude-code-sour…/nano-claude-code/multi_agent/tools.py
chauncygu 1d4ffa964d Update README.MD and add nano-claude-code v3.0 + original-source-code/src
- README.MD: add original-source-code and nano-claude-code sections, update
  overview table (4 subprojects), add v3.0 news entry, expand comparison table
  with memory/multi-agent/skills dimensions
- nano-claude-code v3.0: multi-agent package (multi_agent/), memory package
  (memory/), skill package (skill/) with built-in /commit and /review skills,
  context compression (compaction.py), tool registry plugin system, diff view,
  17 slash commands, 18 built-in tools, 101 tests (~5000 lines total)
- original-source-code/src: add raw TypeScript source tree (1884 files)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 10:26:29 -07:00

296 lines
11 KiB
Python

"""Multi-agent tool registrations.
Registers the following tools into the central tool_registry:
Agent — spawn a sub-agent (sync or background)
SendMessage — send a message to a named background agent
CheckAgentResult — check status/result of a background agent
ListAgentTasks — list all active/finished agent tasks
ListAgentTypes — list available agent type definitions
"""
from __future__ import annotations
from tool_registry import ToolDef, register_tool
from .subagent import SubAgentManager, get_agent_definition, load_agent_definitions
# ── Singleton manager ──────────────────────────────────────────────────────
_agent_manager: SubAgentManager | None = None
def get_agent_manager() -> SubAgentManager:
"""Return (and lazily create) the process-wide SubAgentManager."""
global _agent_manager
if _agent_manager is None:
_agent_manager = SubAgentManager()
return _agent_manager
# ── Tool implementations ───────────────────────────────────────────────────
def _agent_tool(params: dict, config: dict) -> str:
"""Spawn a sub-agent.
Reads from config:
_system_prompt — injected by agent.py run(), used as base system prompt
_depth — current nesting depth (prevents infinite recursion)
"""
mgr = get_agent_manager()
prompt = params["prompt"]
wait = params.get("wait", True)
isolation = params.get("isolation", "")
name = params.get("name", "")
model_override = params.get("model", "")
subagent_type = params.get("subagent_type", "")
system_prompt = config.get("_system_prompt", "You are a helpful assistant.")
depth = config.get("_depth", 0)
# Strip private keys before passing to sub-agent
eff_config = {k: v for k, v in config.items() if not k.startswith("_")}
if model_override:
eff_config["model"] = model_override
# Resolve agent definition
agent_def = None
if subagent_type:
agent_def = get_agent_definition(subagent_type)
if agent_def is None:
return (
f"Error: unknown subagent_type '{subagent_type}'. "
"Use ListAgentTypes to see available types."
)
task = mgr.spawn(
prompt, eff_config, system_prompt,
depth=depth,
agent_def=agent_def,
isolation=isolation,
name=name,
)
if task.status == "failed":
return f"Error spawning agent: {task.result}"
if wait:
mgr.wait(task.id, timeout=300)
result = task.result or f"(no output — status: {task.status})"
header = f"[Agent: {task.name}"
if subagent_type:
header += f" ({subagent_type})"
if task.worktree_branch:
header += f", branch: {task.worktree_branch}"
header += "]"
return f"{header}\n\n{result}"
else:
info_parts = [f"Task ID: {task.id}", f"Name: {task.name}", f"Status: {task.status}"]
if subagent_type:
info_parts.append(f"Type: {subagent_type}")
if task.worktree_branch:
info_parts.append(f"Worktree branch: {task.worktree_branch}")
info_parts.append("Use CheckAgentResult or SendMessage to interact with this agent.")
return "\n".join(info_parts)
def _send_message(params: dict, config: dict) -> str:
mgr = get_agent_manager()
target = params["to"]
message = params["message"]
ok = mgr.send_message(target, message)
if ok:
return f"Message queued for agent '{target}'. It will be processed after current work completes."
task_id = mgr._by_name.get(target, target)
task = mgr.tasks.get(task_id)
if task is None:
return f"Error: no agent found with id or name '{target}'"
return f"Error: agent '{target}' is not running (status: {task.status}). Cannot send message."
def _check_agent_result(params: dict, config: dict) -> str:
mgr = get_agent_manager()
task_id = params["task_id"]
task = mgr.tasks.get(task_id)
if task is None:
return f"Error: no task with id '{task_id}'"
lines = [f"Status: {task.status}", f"Name: {task.name}"]
if task.worktree_branch:
lines.append(f"Worktree branch: {task.worktree_branch}")
if task.result:
lines.append(f"\nResult:\n{task.result}")
return "\n".join(lines)
def _list_agent_tasks(params: dict, config: dict) -> str:
mgr = get_agent_manager()
tasks = mgr.list_tasks()
if not tasks:
return "No sub-agent tasks."
lines = ["ID | Name | Status | Worktree branch | Prompt"]
lines.append("-------------|----------|-----------|-----------------|------")
for t in tasks:
prompt_short = t.prompt[:50] + ("..." if len(t.prompt) > 50 else "")
wt = t.worktree_branch[:15] if t.worktree_branch else "-"
lines.append(f"{t.id} | {t.name[:8]:8s} | {t.status:9s} | {wt:15s} | {prompt_short}")
return "\n".join(lines)
def _list_agent_types(params: dict, config: dict) -> str:
defs = load_agent_definitions()
if not defs:
return "No agent types available."
lines = ["Available agent types:", ""]
for aname, d in sorted(defs.items()):
model_info = f" model: {d.model}" if d.model else ""
tools_info = f" tools: {', '.join(d.tools)}" if d.tools else ""
lines.append(f" {aname:20s} [{d.source:8s}] {d.description}")
if model_info:
lines.append(f" {model_info}")
if tools_info:
lines.append(f" {tools_info}")
lines.append("")
lines.append(
"Create custom agents: place .md files in ~/.nano-claude/agents/ or .nano-claude/agents/"
)
return "\n".join(lines)
# ── Tool registrations ─────────────────────────────────────────────────────
register_tool(ToolDef(
name="Agent",
schema={
"name": "Agent",
"description": (
"Spawn a sub-agent to handle a task autonomously. The sub-agent runs in a "
"separate thread with its own conversation history. Supports specialized agent "
"types (coder, reviewer, researcher, tester, or custom from .nano-claude/agents/), "
"isolated git worktrees for parallel work, and background execution.\n\n"
"When using isolation='worktree', the agent gets its own git branch and "
"working copy — ideal for parallel coding tasks that shouldn't interfere."
),
"input_schema": {
"type": "object",
"properties": {
"prompt": {
"type": "string",
"description": "Task description for the sub-agent",
},
"subagent_type": {
"type": "string",
"description": (
"Specialized agent type: 'general-purpose', 'coder', 'reviewer', "
"'researcher', 'tester', or any custom type. "
"Use ListAgentTypes to see all available types."
),
},
"name": {
"type": "string",
"description": (
"Human-readable name for this agent instance. "
"Makes it addressable via SendMessage while running in background."
),
},
"model": {
"type": "string",
"description": "Model override for this specific agent (optional)",
},
"wait": {
"type": "boolean",
"description": (
"Block until complete (default: true). "
"Set false to run in background."
),
},
"isolation": {
"type": "string",
"enum": ["worktree"],
"description": (
"'worktree' creates a temporary git worktree so the agent works "
"on an isolated copy of the repo. Changes stay on a separate branch "
"and can be reviewed/merged after completion."
),
},
},
"required": ["prompt"],
},
},
func=_agent_tool,
read_only=False,
concurrent_safe=False,
))
register_tool(ToolDef(
name="SendMessage",
schema={
"name": "SendMessage",
"description": (
"Send a follow-up message to a running background agent. "
"The message is queued and processed after the agent finishes its current work. "
"Reference agents by the name set via Agent(name=...) or by task ID."
),
"input_schema": {
"type": "object",
"properties": {
"to": {"type": "string", "description": "Agent name or task ID"},
"message": {"type": "string", "description": "Message to send to the agent"},
},
"required": ["to", "message"],
},
},
func=_send_message,
read_only=False,
concurrent_safe=True,
))
register_tool(ToolDef(
name="CheckAgentResult",
schema={
"name": "CheckAgentResult",
"description": "Check the status and result of a spawned sub-agent task.",
"input_schema": {
"type": "object",
"properties": {
"task_id": {"type": "string", "description": "Task ID returned by Agent tool"},
},
"required": ["task_id"],
},
},
func=_check_agent_result,
read_only=True,
concurrent_safe=True,
))
register_tool(ToolDef(
name="ListAgentTasks",
schema={
"name": "ListAgentTasks",
"description": "List all sub-agent tasks and their statuses.",
"input_schema": {
"type": "object",
"properties": {},
},
},
func=_list_agent_tasks,
read_only=True,
concurrent_safe=True,
))
register_tool(ToolDef(
name="ListAgentTypes",
schema={
"name": "ListAgentTypes",
"description": (
"List all available agent types (built-in and custom). "
"Use the type names as subagent_type when calling Agent."
),
"input_schema": {
"type": "object",
"properties": {},
},
},
func=_list_agent_types,
read_only=True,
concurrent_safe=True,
))