Learn Claude Code
Back to Team Protocols

Agent TeamsTeam Protocols

s09 (348 LOC) → s10 (419 LOC)

LOC Delta

+71lines

New Tools

3

shutdown_responseplan_approvalshutdown_request
New Classes

0

New Functions

3

handle_shutdown_requesthandle_plan_review_check_shutdown_status

Agent Teams

Teammates + Mailboxes

348 LOC

9 tools: bash, read_file, write_file, edit_file, send_message, read_inbox, spawn_teammate, list_teammates, broadcast

collaboration

Team Protocols

Shared Communication Rules

419 LOC

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

collaboration

Source Code Diff

s09 (s09_agent_teams.py) -> s10 (s10_team_protocols.py)
11#!/usr/bin/env python3
2-# Harness: team mailboxes -- multiple models, coordinated through files.
2+# Harness: protocols -- structured handshakes between models.
33"""
4-s09_agent_teams.py - Agent Teams
4+s10_team_protocols.py - Team Protocols
55
6-Persistent named agents with file-based JSONL inboxes. Each teammate runs
7-its own agent loop in a separate thread. Communication via append-only inboxes.
6+Shutdown protocol and plan approval protocol, both using the same
7+request_id correlation pattern. Builds on s09's team messaging.
88
9- Subagent (s04): spawn -> execute -> return summary -> destroyed
10- Teammate (s09): spawn -> work -> idle -> work -> ... -> shutdown
9+ Shutdown FSM: pending -> approved | rejected
1110
12- .team/config.json .team/inbox/
13- +----------------------------+ +------------------+
14- | {"team_name": "default", | | alice.jsonl |
15- | "members": [ | | bob.jsonl |
16- | {"name":"alice", | | lead.jsonl |
17- | "role":"coder", | +------------------+
18- | "status":"idle"} |
19- | ]} | send_message("alice", "fix bug"):
20- +----------------------------+ open("alice.jsonl", "a").write(msg)
11+ Lead Teammate
12+ +---------------------+ +---------------------+
13+ | shutdown_request | | |
14+ | { | -------> | receives request |
15+ | request_id: abc | | decides: approve? |
16+ | } | | |
17+ +---------------------+ +---------------------+
18+ |
19+ +---------------------+ +-------v-------------+
20+ | shutdown_response | <------- | shutdown_response |
21+ | { | | { |
22+ | request_id: abc | | request_id: abc |
23+ | approve: true | | approve: true |
24+ | } | | } |
25+ +---------------------+ +---------------------+
26+ |
27+ v
28+ status -> "shutdown", thread stops
2129
22- read_inbox("alice"):
23- spawn_teammate("alice","coder",...) msgs = [json.loads(l) for l in ...]
24- | open("alice.jsonl", "w").close()
25- v return msgs # drain
26- Thread: alice Thread: bob
27- +------------------+ +------------------+
28- | agent_loop | | agent_loop |
29- | status: working | | status: idle |
30- | ... runs tools | | ... waits ... |
31- | status -> idle | | |
32- +------------------+ +------------------+
30+ Plan approval FSM: pending -> approved | rejected
3331
34- 5 message types (all declared, not all handled here):
35- +-------------------------+-----------------------------------+
36- | message | Normal text message |
37- | broadcast | Sent to all teammates |
38- | shutdown_request | Request graceful shutdown (s10) |
39- | shutdown_response | Approve/reject shutdown (s10) |
40- | plan_approval_response | Approve/reject plan (s10) |
41- +-------------------------+-----------------------------------+
32+ Teammate Lead
33+ +---------------------+ +---------------------+
34+ | plan_approval | | |
35+ | submit: {plan:"..."}| -------> | reviews plan text |
36+ +---------------------+ | approve/reject? |
37+ +---------------------+
38+ |
39+ +---------------------+ +-------v-------------+
40+ | plan_approval_resp | <------- | plan_approval |
41+ | {approve: true} | | review: {req_id, |
42+ +---------------------+ | approve: true} |
43+ +---------------------+
4244
43-Key insight: "Teammates that can talk to each other."
45+ Trackers: {request_id: {"target|from": name, "status": "pending|..."}}
46+
47+Key insight: "Same request_id correlation pattern, two domains."
4448"""
4549
4650import json
4751import os
4852import subprocess
4953import threading
5054import time
55+import uuid
5156from pathlib import Path
5257
5358from anthropic import Anthropic
5459from dotenv import load_dotenv
5560
5661load_dotenv(override=True)
5762if os.getenv("ANTHROPIC_BASE_URL"):
5863 os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
5964
6065WORKDIR = Path.cwd()
6166client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
6267MODEL = os.environ["MODEL_ID"]
6368TEAM_DIR = WORKDIR / ".team"
6469INBOX_DIR = TEAM_DIR / "inbox"
6570
66-SYSTEM = f"You are a team lead at {WORKDIR}. Spawn teammates and communicate via inboxes."
71+SYSTEM = f"You are a team lead at {WORKDIR}. Manage teammates with shutdown and plan approval protocols."
6772
6873VALID_MSG_TYPES = {
6974 "message",
7075 "broadcast",
7176 "shutdown_request",
7277 "shutdown_response",
7378 "plan_approval_response",
7479}
7580
81+# -- Request trackers: correlate by request_id --
82+shutdown_requests = {}
83+plan_requests = {}
84+_tracker_lock = threading.Lock()
7685
86+
7787# -- MessageBus: JSONL inbox per teammate --
7888class MessageBus:
7989 def __init__(self, inbox_dir: Path):
8090 self.dir = inbox_dir
8191 self.dir.mkdir(parents=True, exist_ok=True)
8292
8393 def send(self, sender: str, to: str, content: str,
8494 msg_type: str = "message", extra: dict = None) -> str:
8595 if msg_type not in VALID_MSG_TYPES:
8696 return f"Error: Invalid type '{msg_type}'. Valid: {VALID_MSG_TYPES}"
8797 msg = {
8898 "type": msg_type,
8999 "from": sender,
90100 "content": content,
91101 "timestamp": time.time(),
92102 }
93103 if extra:
94104 msg.update(extra)
95105 inbox_path = self.dir / f"{to}.jsonl"
96106 with open(inbox_path, "a") as f:
97107 f.write(json.dumps(msg) + "\n")
98108 return f"Sent {msg_type} to {to}"
99109
100110 def read_inbox(self, name: str) -> list:
101111 inbox_path = self.dir / f"{name}.jsonl"
102112 if not inbox_path.exists():
103113 return []
104114 messages = []
105115 for line in inbox_path.read_text().strip().splitlines():
106116 if line:
107117 messages.append(json.loads(line))
108118 inbox_path.write_text("")
109119 return messages
110120
111121 def broadcast(self, sender: str, content: str, teammates: list) -> str:
112122 count = 0
113123 for name in teammates:
114124 if name != sender:
115125 self.send(sender, name, content, "broadcast")
116126 count += 1
117127 return f"Broadcast to {count} teammates"
118128
119129
120130BUS = MessageBus(INBOX_DIR)
121131
122132
123-# -- TeammateManager: persistent named agents with config.json --
133+# -- TeammateManager with shutdown + plan approval --
124134class TeammateManager:
125135 def __init__(self, team_dir: Path):
126136 self.dir = team_dir
127137 self.dir.mkdir(exist_ok=True)
128138 self.config_path = self.dir / "config.json"
129139 self.config = self._load_config()
130140 self.threads = {}
131141
132142 def _load_config(self) -> dict:
133143 if self.config_path.exists():
134144 return json.loads(self.config_path.read_text())
135145 return {"team_name": "default", "members": []}
136146
137147 def _save_config(self):
138148 self.config_path.write_text(json.dumps(self.config, indent=2))
139149
140150 def _find_member(self, name: str) -> dict:
141151 for m in self.config["members"]:
142152 if m["name"] == name:
143153 return m
144154 return None
145155
146156 def spawn(self, name: str, role: str, prompt: str) -> str:
147157 member = self._find_member(name)
148158 if member:
149159 if member["status"] not in ("idle", "shutdown"):
150160 return f"Error: '{name}' is currently {member['status']}"
151161 member["status"] = "working"
152162 member["role"] = role
153163 else:
154164 member = {"name": name, "role": role, "status": "working"}
155165 self.config["members"].append(member)
156166 self._save_config()
157167 thread = threading.Thread(
158168 target=self._teammate_loop,
159169 args=(name, role, prompt),
160170 daemon=True,
161171 )
162172 self.threads[name] = thread
163173 thread.start()
164174 return f"Spawned '{name}' (role: {role})"
165175
166176 def _teammate_loop(self, name: str, role: str, prompt: str):
167177 sys_prompt = (
168178 f"You are '{name}', role: {role}, at {WORKDIR}. "
169- f"Use send_message to communicate. Complete your task."
179+ f"Submit plans via plan_approval before major work. "
180+ f"Respond to shutdown_request with shutdown_response."
170181 )
171182 messages = [{"role": "user", "content": prompt}]
172183 tools = self._teammate_tools()
184+ should_exit = False
173185 for _ in range(50):
174186 inbox = BUS.read_inbox(name)
175187 for msg in inbox:
176188 messages.append({"role": "user", "content": json.dumps(msg)})
189+ if should_exit:
190+ break
177191 try:
178192 response = client.messages.create(
179193 model=MODEL,
180194 system=sys_prompt,
181195 messages=messages,
182196 tools=tools,
183197 max_tokens=8000,
184198 )
185199 except Exception:
186200 break
187201 messages.append({"role": "assistant", "content": response.content})
188202 if response.stop_reason != "tool_use":
189203 break
190204 results = []
191205 for block in response.content:
192206 if block.type == "tool_use":
193207 output = self._exec(name, block.name, block.input)
194208 print(f" [{name}] {block.name}: {str(output)[:120]}")
195209 results.append({
196210 "type": "tool_result",
197211 "tool_use_id": block.id,
198212 "content": str(output),
199213 })
214+ if block.name == "shutdown_response" and block.input.get("approve"):
215+ should_exit = True
200216 messages.append({"role": "user", "content": results})
201217 member = self._find_member(name)
202- if member and member["status"] != "shutdown":
203- member["status"] = "idle"
218+ if member:
219+ member["status"] = "shutdown" if should_exit else "idle"
204220 self._save_config()
205221
206222 def _exec(self, sender: str, tool_name: str, args: dict) -> str:
207223 # these base tools are unchanged from s02
208224 if tool_name == "bash":
209225 return _run_bash(args["command"])
210226 if tool_name == "read_file":
211227 return _run_read(args["path"])
212228 if tool_name == "write_file":
213229 return _run_write(args["path"], args["content"])
214230 if tool_name == "edit_file":
215231 return _run_edit(args["path"], args["old_text"], args["new_text"])
216232 if tool_name == "send_message":
217233 return BUS.send(sender, args["to"], args["content"], args.get("msg_type", "message"))
218234 if tool_name == "read_inbox":
219235 return json.dumps(BUS.read_inbox(sender), indent=2)
236+ if tool_name == "shutdown_response":
237+ req_id = args["request_id"]
238+ approve = args["approve"]
239+ with _tracker_lock:
240+ if req_id in shutdown_requests:
241+ shutdown_requests[req_id]["status"] = "approved" if approve else "rejected"
242+ BUS.send(
243+ sender, "lead", args.get("reason", ""),
244+ "shutdown_response", {"request_id": req_id, "approve": approve},
245+ )
246+ return f"Shutdown {'approved' if approve else 'rejected'}"
247+ if tool_name == "plan_approval":
248+ plan_text = args.get("plan", "")
249+ req_id = str(uuid.uuid4())[:8]
250+ with _tracker_lock:
251+ plan_requests[req_id] = {"from": sender, "plan": plan_text, "status": "pending"}
252+ BUS.send(
253+ sender, "lead", plan_text, "plan_approval_response",
254+ {"request_id": req_id, "plan": plan_text},
255+ )
256+ return f"Plan submitted (request_id={req_id}). Waiting for lead approval."
220257 return f"Unknown tool: {tool_name}"
221258
222259 def _teammate_tools(self) -> list:
223260 # these base tools are unchanged from s02
224261 return [
225262 {"name": "bash", "description": "Run a shell command.",
226263 "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
227264 {"name": "read_file", "description": "Read file contents.",
228265 "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}},
229266 {"name": "write_file", "description": "Write content to file.",
230267 "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
231268 {"name": "edit_file", "description": "Replace exact text in file.",
232269 "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
233270 {"name": "send_message", "description": "Send message to a teammate.",
234271 "input_schema": {"type": "object", "properties": {"to": {"type": "string"}, "content": {"type": "string"}, "msg_type": {"type": "string", "enum": list(VALID_MSG_TYPES)}}, "required": ["to", "content"]}},
235272 {"name": "read_inbox", "description": "Read and drain your inbox.",
236273 "input_schema": {"type": "object", "properties": {}}},
274+ {"name": "shutdown_response", "description": "Respond to a shutdown request. Approve to shut down, reject to keep working.",
275+ "input_schema": {"type": "object", "properties": {"request_id": {"type": "string"}, "approve": {"type": "boolean"}, "reason": {"type": "string"}}, "required": ["request_id", "approve"]}},
276+ {"name": "plan_approval", "description": "Submit a plan for lead approval. Provide plan text.",
277+ "input_schema": {"type": "object", "properties": {"plan": {"type": "string"}}, "required": ["plan"]}},
237278 ]
238279
239280 def list_all(self) -> str:
240281 if not self.config["members"]:
241282 return "No teammates."
242283 lines = [f"Team: {self.config['team_name']}"]
243284 for m in self.config["members"]:
244285 lines.append(f" {m['name']} ({m['role']}): {m['status']}")
245286 return "\n".join(lines)
246287
247288 def member_names(self) -> list:
248289 return [m["name"] for m in self.config["members"]]
249290
250291
251292TEAM = TeammateManager(TEAM_DIR)
252293
253294
254295# -- Base tool implementations (these base tools are unchanged from s02) --
255296def _safe_path(p: str) -> Path:
256297 path = (WORKDIR / p).resolve()
257298 if not path.is_relative_to(WORKDIR):
258299 raise ValueError(f"Path escapes workspace: {p}")
259300 return path
260301
261302
262303def _run_bash(command: str) -> str:
263304 dangerous = ["rm -rf /", "sudo", "shutdown", "reboot"]
264305 if any(d in command for d in dangerous):
265306 return "Error: Dangerous command blocked"
266307 try:
267308 r = subprocess.run(
268309 command, shell=True, cwd=WORKDIR,
269310 capture_output=True, text=True, timeout=120,
270311 )
271312 out = (r.stdout + r.stderr).strip()
272313 return out[:50000] if out else "(no output)"
273314 except subprocess.TimeoutExpired:
274315 return "Error: Timeout (120s)"
275316
276317
277318def _run_read(path: str, limit: int = None) -> str:
278319 try:
279320 lines = _safe_path(path).read_text().splitlines()
280321 if limit and limit < len(lines):
281322 lines = lines[:limit] + [f"... ({len(lines) - limit} more)"]
282323 return "\n".join(lines)[:50000]
283324 except Exception as e:
284325 return f"Error: {e}"
285326
286327
287328def _run_write(path: str, content: str) -> str:
288329 try:
289330 fp = _safe_path(path)
290331 fp.parent.mkdir(parents=True, exist_ok=True)
291332 fp.write_text(content)
292333 return f"Wrote {len(content)} bytes"
293334 except Exception as e:
294335 return f"Error: {e}"
295336
296337
297338def _run_edit(path: str, old_text: str, new_text: str) -> str:
298339 try:
299340 fp = _safe_path(path)
300341 c = fp.read_text()
301342 if old_text not in c:
302343 return f"Error: Text not found in {path}"
303344 fp.write_text(c.replace(old_text, new_text, 1))
304345 return f"Edited {path}"
305346 except Exception as e:
306347 return f"Error: {e}"
307348
308349
309-# -- Lead tool dispatch (9 tools) --
350+# -- Lead-specific protocol handlers --
351+def handle_shutdown_request(teammate: str) -> str:
352+ req_id = str(uuid.uuid4())[:8]
353+ with _tracker_lock:
354+ shutdown_requests[req_id] = {"target": teammate, "status": "pending"}
355+ BUS.send(
356+ "lead", teammate, "Please shut down gracefully.",
357+ "shutdown_request", {"request_id": req_id},
358+ )
359+ return f"Shutdown request {req_id} sent to '{teammate}' (status: pending)"
360+
361+
362+def handle_plan_review(request_id: str, approve: bool, feedback: str = "") -> str:
363+ with _tracker_lock:
364+ req = plan_requests.get(request_id)
365+ if not req:
366+ return f"Error: Unknown plan request_id '{request_id}'"
367+ with _tracker_lock:
368+ req["status"] = "approved" if approve else "rejected"
369+ BUS.send(
370+ "lead", req["from"], feedback, "plan_approval_response",
371+ {"request_id": request_id, "approve": approve, "feedback": feedback},
372+ )
373+ return f"Plan {req['status']} for '{req['from']}'"
374+
375+
376+def _check_shutdown_status(request_id: str) -> str:
377+ with _tracker_lock:
378+ return json.dumps(shutdown_requests.get(request_id, {"error": "not found"}))
379+
380+
381+# -- Lead tool dispatch (12 tools) --
310382TOOL_HANDLERS = {
311- "bash": lambda **kw: _run_bash(kw["command"]),
312- "read_file": lambda **kw: _run_read(kw["path"], kw.get("limit")),
313- "write_file": lambda **kw: _run_write(kw["path"], kw["content"]),
314- "edit_file": lambda **kw: _run_edit(kw["path"], kw["old_text"], kw["new_text"]),
315- "spawn_teammate": lambda **kw: TEAM.spawn(kw["name"], kw["role"], kw["prompt"]),
316- "list_teammates": lambda **kw: TEAM.list_all(),
317- "send_message": lambda **kw: BUS.send("lead", kw["to"], kw["content"], kw.get("msg_type", "message")),
318- "read_inbox": lambda **kw: json.dumps(BUS.read_inbox("lead"), indent=2),
319- "broadcast": lambda **kw: BUS.broadcast("lead", kw["content"], TEAM.member_names()),
383+ "bash": lambda **kw: _run_bash(kw["command"]),
384+ "read_file": lambda **kw: _run_read(kw["path"], kw.get("limit")),
385+ "write_file": lambda **kw: _run_write(kw["path"], kw["content"]),
386+ "edit_file": lambda **kw: _run_edit(kw["path"], kw["old_text"], kw["new_text"]),
387+ "spawn_teammate": lambda **kw: TEAM.spawn(kw["name"], kw["role"], kw["prompt"]),
388+ "list_teammates": lambda **kw: TEAM.list_all(),
389+ "send_message": lambda **kw: BUS.send("lead", kw["to"], kw["content"], kw.get("msg_type", "message")),
390+ "read_inbox": lambda **kw: json.dumps(BUS.read_inbox("lead"), indent=2),
391+ "broadcast": lambda **kw: BUS.broadcast("lead", kw["content"], TEAM.member_names()),
392+ "shutdown_request": lambda **kw: handle_shutdown_request(kw["teammate"]),
393+ "shutdown_response": lambda **kw: _check_shutdown_status(kw.get("request_id", "")),
394+ "plan_approval": lambda **kw: handle_plan_review(kw["request_id"], kw["approve"], kw.get("feedback", "")),
320395}
321396
322397# these base tools are unchanged from s02
323398TOOLS = [
324399 {"name": "bash", "description": "Run a shell command.",
325400 "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
326401 {"name": "read_file", "description": "Read file contents.",
327402 "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}},
328403 {"name": "write_file", "description": "Write content to file.",
329404 "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
330405 {"name": "edit_file", "description": "Replace exact text in file.",
331406 "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
332- {"name": "spawn_teammate", "description": "Spawn a persistent teammate that runs in its own thread.",
407+ {"name": "spawn_teammate", "description": "Spawn a persistent teammate.",
333408 "input_schema": {"type": "object", "properties": {"name": {"type": "string"}, "role": {"type": "string"}, "prompt": {"type": "string"}}, "required": ["name", "role", "prompt"]}},
334- {"name": "list_teammates", "description": "List all teammates with name, role, status.",
409+ {"name": "list_teammates", "description": "List all teammates.",
335410 "input_schema": {"type": "object", "properties": {}}},
336- {"name": "send_message", "description": "Send a message to a teammate's inbox.",
411+ {"name": "send_message", "description": "Send a message to a teammate.",
337412 "input_schema": {"type": "object", "properties": {"to": {"type": "string"}, "content": {"type": "string"}, "msg_type": {"type": "string", "enum": list(VALID_MSG_TYPES)}}, "required": ["to", "content"]}},
338413 {"name": "read_inbox", "description": "Read and drain the lead's inbox.",
339414 "input_schema": {"type": "object", "properties": {}}},
340415 {"name": "broadcast", "description": "Send a message to all teammates.",
341416 "input_schema": {"type": "object", "properties": {"content": {"type": "string"}}, "required": ["content"]}},
417+ {"name": "shutdown_request", "description": "Request a teammate to shut down gracefully. Returns a request_id for tracking.",
418+ "input_schema": {"type": "object", "properties": {"teammate": {"type": "string"}}, "required": ["teammate"]}},
419+ {"name": "shutdown_response", "description": "Check the status of a shutdown request by request_id.",
420+ "input_schema": {"type": "object", "properties": {"request_id": {"type": "string"}}, "required": ["request_id"]}},
421+ {"name": "plan_approval", "description": "Approve or reject a teammate's plan. Provide request_id + approve + optional feedback.",
422+ "input_schema": {"type": "object", "properties": {"request_id": {"type": "string"}, "approve": {"type": "boolean"}, "feedback": {"type": "string"}}, "required": ["request_id", "approve"]}},
342423]
343424
344425
345426def agent_loop(messages: list):
346427 while True:
347428 inbox = BUS.read_inbox("lead")
348429 if inbox:
349430 messages.append({
350431 "role": "user",
351432 "content": f"<inbox>{json.dumps(inbox, indent=2)}</inbox>",
352433 })
353434 messages.append({
354435 "role": "assistant",
355436 "content": "Noted inbox messages.",
356437 })
357438 response = client.messages.create(
358439 model=MODEL,
359440 system=SYSTEM,
360441 messages=messages,
361442 tools=TOOLS,
362443 max_tokens=8000,
363444 )
364445 messages.append({"role": "assistant", "content": response.content})
365446 if response.stop_reason != "tool_use":
366447 return
367448 results = []
368449 for block in response.content:
369450 if block.type == "tool_use":
370451 handler = TOOL_HANDLERS.get(block.name)
371452 try:
372453 output = handler(**block.input) if handler else f"Unknown tool: {block.name}"
373454 except Exception as e:
374455 output = f"Error: {e}"
375456 print(f"> {block.name}: {str(output)[:200]}")
376457 results.append({
377458 "type": "tool_result",
378459 "tool_use_id": block.id,
379460 "content": str(output),
380461 })
381462 messages.append({"role": "user", "content": results})
382463
383464
384465if __name__ == "__main__":
385466 history = []
386467 while True:
387468 try:
388- query = input("\033[36ms09 >> \033[0m")
469+ query = input("\033[36ms10 >> \033[0m")
389470 except (EOFError, KeyboardInterrupt):
390471 break
391472 if query.strip().lower() in ("q", "exit", ""):
392473 break
393474 if query.strip() == "/team":
394475 print(TEAM.list_all())
395476 continue
396477 if query.strip() == "/inbox":
397478 print(json.dumps(BUS.read_inbox("lead"), indent=2))
398479 continue
399480 history.append({"role": "user", "content": query})
400481 agent_loop(history)
401482 response_content = history[-1]["content"]
402483 if isinstance(response_content, list):
403484 for block in response_content:
404485 if hasattr(block, "text"):
405486 print(block.text)
406487 print()