subagent-pattern.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. """
  2. Subagent Pattern - How to implement Task tool for context isolation.
  3. The key insight: spawn child agents with ISOLATED context to prevent
  4. "context pollution" where exploration details fill up the main conversation.
  5. """
  6. import time
  7. import sys
  8. # Assuming client, MODEL, execute_tool are defined elsewhere
  9. # =============================================================================
  10. # AGENT TYPE REGISTRY
  11. # =============================================================================
  12. AGENT_TYPES = {
  13. # Explore: Read-only, for searching and analyzing
  14. "explore": {
  15. "description": "Read-only agent for exploring code, finding files, searching",
  16. "tools": ["bash", "read_file"], # No write access!
  17. "prompt": "You are an exploration agent. Search and analyze, but NEVER modify files. Return a concise summary of what you found.",
  18. },
  19. # Code: Full-powered, for implementation
  20. "code": {
  21. "description": "Full agent for implementing features and fixing bugs",
  22. "tools": "*", # All tools
  23. "prompt": "You are a coding agent. Implement the requested changes efficiently. Return a summary of what you changed.",
  24. },
  25. # Plan: Read-only, for design work
  26. "plan": {
  27. "description": "Planning agent for designing implementation strategies",
  28. "tools": ["bash", "read_file"], # Read-only
  29. "prompt": "You are a planning agent. Analyze the codebase and output a numbered implementation plan. Do NOT make any changes.",
  30. },
  31. # Add your own types here...
  32. # "test": {
  33. # "description": "Testing agent for running and analyzing tests",
  34. # "tools": ["bash", "read_file"],
  35. # "prompt": "Run tests and report results. Don't modify code.",
  36. # },
  37. }
  38. def get_agent_descriptions() -> str:
  39. """Generate descriptions for Task tool schema."""
  40. return "\n".join(
  41. f"- {name}: {cfg['description']}"
  42. for name, cfg in AGENT_TYPES.items()
  43. )
  44. def get_tools_for_agent(agent_type: str, base_tools: list) -> list:
  45. """
  46. Filter tools based on agent type.
  47. '*' means all base tools.
  48. Otherwise, whitelist specific tool names.
  49. Note: Subagents don't get Task tool to prevent infinite recursion.
  50. """
  51. allowed = AGENT_TYPES.get(agent_type, {}).get("tools", "*")
  52. if allowed == "*":
  53. return base_tools # All base tools, but NOT Task
  54. return [t for t in base_tools if t["name"] in allowed]
  55. # =============================================================================
  56. # TASK TOOL DEFINITION
  57. # =============================================================================
  58. TASK_TOOL = {
  59. "name": "Task",
  60. "description": f"""Spawn a subagent for a focused subtask.
  61. Subagents run in ISOLATED context - they don't see parent's history.
  62. Use this to keep the main conversation clean.
  63. Agent types:
  64. {get_agent_descriptions()}
  65. Example uses:
  66. - Task(explore): "Find all files using the auth module"
  67. - Task(plan): "Design a migration strategy for the database"
  68. - Task(code): "Implement the user registration form"
  69. """,
  70. "input_schema": {
  71. "type": "object",
  72. "properties": {
  73. "description": {
  74. "type": "string",
  75. "description": "Short task name (3-5 words) for progress display"
  76. },
  77. "prompt": {
  78. "type": "string",
  79. "description": "Detailed instructions for the subagent"
  80. },
  81. "agent_type": {
  82. "type": "string",
  83. "enum": list(AGENT_TYPES.keys()),
  84. "description": "Type of agent to spawn"
  85. },
  86. },
  87. "required": ["description", "prompt", "agent_type"],
  88. },
  89. }
  90. # =============================================================================
  91. # SUBAGENT EXECUTION
  92. # =============================================================================
  93. def run_task(description: str, prompt: str, agent_type: str,
  94. client, model: str, workdir, base_tools: list, execute_tool) -> str:
  95. """
  96. Execute a subagent task with isolated context.
  97. Key concepts:
  98. 1. ISOLATED HISTORY - subagent starts fresh, no parent context
  99. 2. FILTERED TOOLS - based on agent type permissions
  100. 3. AGENT-SPECIFIC PROMPT - specialized behavior
  101. 4. RETURNS SUMMARY ONLY - parent sees just the final result
  102. Args:
  103. description: Short name for progress display
  104. prompt: Detailed instructions for subagent
  105. agent_type: Key from AGENT_TYPES
  106. client: Anthropic client
  107. model: Model to use
  108. workdir: Working directory
  109. base_tools: List of tool definitions
  110. execute_tool: Function to execute tools
  111. Returns:
  112. Final text output from subagent
  113. """
  114. if agent_type not in AGENT_TYPES:
  115. return f"Error: Unknown agent type '{agent_type}'"
  116. config = AGENT_TYPES[agent_type]
  117. # Agent-specific system prompt
  118. sub_system = f"""You are a {agent_type} subagent at {workdir}.
  119. {config["prompt"]}
  120. Complete the task and return a clear, concise summary."""
  121. # Filtered tools for this agent type
  122. sub_tools = get_tools_for_agent(agent_type, base_tools)
  123. # KEY: ISOLATED message history!
  124. # The subagent starts fresh, doesn't see parent's conversation
  125. sub_messages = [{"role": "user", "content": prompt}]
  126. # Progress display
  127. print(f" [{agent_type}] {description}")
  128. start = time.time()
  129. tool_count = 0
  130. # Run the same agent loop (but silently)
  131. while True:
  132. response = client.messages.create(
  133. model=model,
  134. system=sub_system,
  135. messages=sub_messages,
  136. tools=sub_tools,
  137. max_tokens=8000,
  138. )
  139. # Check if done
  140. if response.stop_reason != "tool_use":
  141. break
  142. # Execute tools
  143. tool_calls = [b for b in response.content if b.type == "tool_use"]
  144. results = []
  145. for tc in tool_calls:
  146. tool_count += 1
  147. output = execute_tool(tc.name, tc.input)
  148. results.append({
  149. "type": "tool_result",
  150. "tool_use_id": tc.id,
  151. "content": output
  152. })
  153. # Update progress (in-place on same line)
  154. elapsed = time.time() - start
  155. sys.stdout.write(
  156. f"\r [{agent_type}] {description} ... {tool_count} tools, {elapsed:.1f}s"
  157. )
  158. sys.stdout.flush()
  159. sub_messages.append({"role": "assistant", "content": response.content})
  160. sub_messages.append({"role": "user", "content": results})
  161. # Final progress update
  162. elapsed = time.time() - start
  163. sys.stdout.write(
  164. f"\r [{agent_type}] {description} - done ({tool_count} tools, {elapsed:.1f}s)\n"
  165. )
  166. # Extract and return ONLY the final text
  167. # This is what the parent agent sees - a clean summary
  168. for block in response.content:
  169. if hasattr(block, "text"):
  170. return block.text
  171. return "(subagent returned no text)"
  172. # =============================================================================
  173. # USAGE EXAMPLE
  174. # =============================================================================
  175. """
  176. # In your main agent's execute_tool function:
  177. def execute_tool(name: str, args: dict) -> str:
  178. if name == "Task":
  179. return run_task(
  180. description=args["description"],
  181. prompt=args["prompt"],
  182. agent_type=args["agent_type"],
  183. client=client,
  184. model=MODEL,
  185. workdir=WORKDIR,
  186. base_tools=BASE_TOOLS,
  187. execute_tool=execute_tool # Pass self for recursion
  188. )
  189. # ... other tools ...
  190. # In your TOOLS list:
  191. TOOLS = BASE_TOOLS + [TASK_TOOL]
  192. """