Rebase onto upstream (a4d95fd)
#12
@@ -300,11 +300,11 @@ class AgentLoop:
|
|||||||
message_tool = self.tools.get("message")
|
message_tool = self.tools.get("message")
|
||||||
if isinstance(message_tool, MessageTool):
|
if isinstance(message_tool, MessageTool):
|
||||||
message_tool.set_context(msg.channel, msg.chat_id)
|
message_tool.set_context(msg.channel, msg.chat_id)
|
||||||
|
|
||||||
spawn_tool = self.tools.get("spawn")
|
spawn_tool = self.tools.get("spawn")
|
||||||
if isinstance(spawn_tool, SpawnTool):
|
if isinstance(spawn_tool, SpawnTool):
|
||||||
spawn_tool.set_context(msg.channel, msg.chat_id)
|
spawn_tool.set_context(msg.channel, msg.chat_id, msg.metadata)
|
||||||
|
|
||||||
cron_tool = self.tools.get("cron")
|
cron_tool = self.tools.get("cron")
|
||||||
if isinstance(cron_tool, CronTool):
|
if isinstance(cron_tool, CronTool):
|
||||||
cron_tool.set_context(msg.channel, msg.chat_id)
|
cron_tool.set_context(msg.channel, msg.chat_id)
|
||||||
@@ -505,11 +505,12 @@ class AgentLoop:
|
|||||||
message_tool = self.tools.get("message")
|
message_tool = self.tools.get("message")
|
||||||
if isinstance(message_tool, MessageTool):
|
if isinstance(message_tool, MessageTool):
|
||||||
message_tool.set_context(origin_channel, origin_chat_id)
|
message_tool.set_context(origin_channel, origin_chat_id)
|
||||||
|
|
||||||
|
|
||||||
spawn_tool = self.tools.get("spawn")
|
spawn_tool = self.tools.get("spawn")
|
||||||
if isinstance(spawn_tool, SpawnTool):
|
if isinstance(spawn_tool, SpawnTool):
|
||||||
spawn_tool.set_context(origin_channel, origin_chat_id)
|
spawn_tool.set_context(origin_channel, origin_chat_id, msg.metadata)
|
||||||
|
|
||||||
cron_tool = self.tools.get("cron")
|
cron_tool = self.tools.get("cron")
|
||||||
if isinstance(cron_tool, CronTool):
|
if isinstance(cron_tool, CronTool):
|
||||||
cron_tool.set_context(origin_channel, origin_chat_id)
|
cron_tool.set_context(origin_channel, origin_chat_id)
|
||||||
|
|||||||
+46
-48
@@ -20,7 +20,13 @@ from nanobot.agent.tools.wait import WaitForSubagentsTool
|
|||||||
|
|
||||||
|
|
||||||
class SubagentManager:
|
class SubagentManager:
|
||||||
"""Manages background subagent execution."""
|
"""
|
||||||
|
Manages background subagent execution.
|
||||||
|
|
||||||
|
Subagents are lightweight agent instances that run in the background
|
||||||
|
to handle specific tasks. They share the same LLM provider but have
|
||||||
|
isolated context and a focused system prompt.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -28,8 +34,6 @@ class SubagentManager:
|
|||||||
workspace: Path,
|
workspace: Path,
|
||||||
bus: MessageBus,
|
bus: MessageBus,
|
||||||
model: str | None = None,
|
model: str | None = None,
|
||||||
temperature: float = 0.7,
|
|
||||||
max_tokens: int = 4096,
|
|
||||||
brave_api_key: str | None = None,
|
brave_api_key: str | None = None,
|
||||||
exec_config: "ExecToolConfig | None" = None,
|
exec_config: "ExecToolConfig | None" = None,
|
||||||
restrict_to_workspace: bool = False,
|
restrict_to_workspace: bool = False,
|
||||||
@@ -42,13 +46,10 @@ class SubagentManager:
|
|||||||
# Quota switching only affects the main agent's own requests, not SubagentManager.
|
# Quota switching only affects the main agent's own requests, not SubagentManager.
|
||||||
# Explicit model overrides (e.g. Haiku workers) still take precedence.
|
# Explicit model overrides (e.g. Haiku workers) still take precedence.
|
||||||
self.model = model or "claude-sonnet-4-6"
|
self.model = model or "claude-sonnet-4-6"
|
||||||
self.temperature = temperature
|
|
||||||
self.max_tokens = max_tokens
|
|
||||||
self.brave_api_key = brave_api_key
|
self.brave_api_key = brave_api_key
|
||||||
self.exec_config = exec_config or ExecToolConfig()
|
self.exec_config = exec_config or ExecToolConfig()
|
||||||
self.restrict_to_workspace = restrict_to_workspace
|
self.restrict_to_workspace = restrict_to_workspace
|
||||||
self._running_tasks: dict[str, asyncio.Task[None]] = {}
|
self._running_tasks: dict[str, asyncio.Task[None]] = {}
|
||||||
self._session_tasks: dict[str, set[str]] = {} # session_key -> {task_id, ...}
|
|
||||||
self._task_results: dict[str, str] = {}
|
self._task_results: dict[str, str] = {}
|
||||||
|
|
||||||
async def spawn(
|
async def spawn(
|
||||||
@@ -58,29 +59,39 @@ class SubagentManager:
|
|||||||
model: str | None = None,
|
model: str | None = None,
|
||||||
origin_channel: str = "cli",
|
origin_channel: str = "cli",
|
||||||
origin_chat_id: str = "direct",
|
origin_chat_id: str = "direct",
|
||||||
session_key: str | None = None,
|
origin_metadata: dict[str, Any] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Spawn a subagent to execute a task in the background."""
|
"""
|
||||||
|
Spawn a subagent to execute a task in the background.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task: The task description for the subagent.
|
||||||
|
label: Optional human-readable label for the task.
|
||||||
|
origin_channel: The channel to announce results to.
|
||||||
|
origin_chat_id: The chat ID to announce results to.
|
||||||
|
origin_metadata: Optional metadata to propagate to announcement (e.g. suppress_output).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Status message indicating the subagent was started.
|
||||||
|
"""
|
||||||
task_id = str(uuid.uuid4())[:8]
|
task_id = str(uuid.uuid4())[:8]
|
||||||
display_label = label or task[:30] + ("..." if len(task) > 30 else "")
|
display_label = label or task[:30] + ("..." if len(task) > 30 else "")
|
||||||
origin = {"channel": origin_channel, "chat_id": origin_chat_id}
|
|
||||||
|
|
||||||
|
origin = {
|
||||||
|
"channel": origin_channel,
|
||||||
|
"chat_id": origin_chat_id,
|
||||||
|
"metadata": origin_metadata or {},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create background task
|
||||||
bg_task = asyncio.create_task(
|
bg_task = asyncio.create_task(
|
||||||
self._run_subagent(task_id, task, display_label, origin, model=model)
|
self._run_subagent(task_id, task, display_label, origin, model=model)
|
||||||
)
|
)
|
||||||
self._running_tasks[task_id] = bg_task
|
self._running_tasks[task_id] = bg_task
|
||||||
if session_key:
|
|
||||||
self._session_tasks.setdefault(session_key, set()).add(task_id)
|
# Cleanup when done
|
||||||
|
bg_task.add_done_callback(lambda _: self._running_tasks.pop(task_id, None))
|
||||||
def _cleanup(_: asyncio.Task) -> None:
|
|
||||||
self._running_tasks.pop(task_id, None)
|
|
||||||
if session_key and (ids := self._session_tasks.get(session_key)):
|
|
||||||
ids.discard(task_id)
|
|
||||||
if not ids:
|
|
||||||
del self._session_tasks[session_key]
|
|
||||||
|
|
||||||
bg_task.add_done_callback(_cleanup)
|
|
||||||
|
|
||||||
logger.info(f"Spawned subagent [{task_id}]: {display_label}")
|
logger.info(f"Spawned subagent [{task_id}]: {display_label}")
|
||||||
return f"Subagent [{display_label}] started. Task ID: {task_id}"
|
return f"Subagent [{display_label}] started. Task ID: {task_id}"
|
||||||
|
|
||||||
@@ -93,26 +104,25 @@ class SubagentManager:
|
|||||||
model: str | None = None,
|
model: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Execute the subagent task and announce the result."""
|
"""Execute the subagent task and announce the result."""
|
||||||
logger.info("Subagent [{}] starting task: {}", task_id, label)
|
logger.info(f"Subagent [{task_id}] starting task: {label}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Build subagent tools (no message tool)
|
# Build subagent tools (no message tool)
|
||||||
tools = ToolRegistry()
|
tools = ToolRegistry()
|
||||||
allowed_dir = self.workspace if self.restrict_to_workspace else None
|
allowed_dir = self.workspace if self.restrict_to_workspace else None
|
||||||
tools.register(ReadFileTool(workspace=self.workspace, allowed_dir=allowed_dir))
|
tools.register(ReadFileTool(allowed_dir=allowed_dir))
|
||||||
tools.register(WriteFileTool(workspace=self.workspace, allowed_dir=allowed_dir))
|
tools.register(WriteFileTool(allowed_dir=allowed_dir))
|
||||||
tools.register(EditFileTool(workspace=self.workspace, allowed_dir=allowed_dir))
|
tools.register(EditFileTool(allowed_dir=allowed_dir))
|
||||||
tools.register(ListDirTool(workspace=self.workspace, allowed_dir=allowed_dir))
|
tools.register(ListDirTool(allowed_dir=allowed_dir))
|
||||||
tools.register(ExecTool(
|
tools.register(ExecTool(
|
||||||
working_dir=str(self.workspace),
|
working_dir=str(self.workspace),
|
||||||
timeout=self.exec_config.timeout,
|
timeout=self.exec_config.timeout,
|
||||||
restrict_to_workspace=self.restrict_to_workspace,
|
restrict_to_workspace=self.restrict_to_workspace,
|
||||||
path_append=self.exec_config.path_append,
|
|
||||||
))
|
))
|
||||||
tools.register(WebSearchTool(api_key=self.brave_api_key))
|
tools.register(WebSearchTool(api_key=self.brave_api_key))
|
||||||
tools.register(WebFetchTool())
|
tools.register(WebFetchTool())
|
||||||
spawn_tool = SpawnTool(manager=self)
|
spawn_tool = SpawnTool(manager=self)
|
||||||
spawn_tool.set_context("subagent", origin["chat_id"])
|
spawn_tool.set_context("subagent", origin["chat_id"], origin.get("metadata"))
|
||||||
tools.register(spawn_tool)
|
tools.register(spawn_tool)
|
||||||
tools.register(WaitForSubagentsTool(manager=self))
|
tools.register(WaitForSubagentsTool(manager=self))
|
||||||
|
|
||||||
@@ -135,8 +145,6 @@ class SubagentManager:
|
|||||||
messages=messages,
|
messages=messages,
|
||||||
tools=tools.get_definitions(),
|
tools=tools.get_definitions(),
|
||||||
model=model or self.model,
|
model=model or self.model,
|
||||||
temperature=self.temperature,
|
|
||||||
max_tokens=self.max_tokens,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.has_tool_calls:
|
if response.has_tool_calls:
|
||||||
@@ -147,7 +155,7 @@ class SubagentManager:
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": tc.name,
|
"name": tc.name,
|
||||||
"arguments": json.dumps(tc.arguments, ensure_ascii=False),
|
"arguments": json.dumps(tc.arguments),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for tc in response.tool_calls
|
for tc in response.tool_calls
|
||||||
@@ -160,8 +168,8 @@ class SubagentManager:
|
|||||||
|
|
||||||
# Execute tools
|
# Execute tools
|
||||||
for tool_call in response.tool_calls:
|
for tool_call in response.tool_calls:
|
||||||
args_str = json.dumps(tool_call.arguments, ensure_ascii=False)
|
args_str = json.dumps(tool_call.arguments)
|
||||||
logger.debug("Subagent [{}] executing: {} with arguments: {}", task_id, tool_call.name, args_str)
|
logger.debug(f"Subagent [{task_id}] executing: {tool_call.name} with arguments: {args_str}")
|
||||||
result = await tools.execute(tool_call.name, tool_call.arguments)
|
result = await tools.execute(tool_call.name, tool_call.arguments)
|
||||||
messages.append({
|
messages.append({
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
@@ -176,12 +184,12 @@ class SubagentManager:
|
|||||||
if final_result is None:
|
if final_result is None:
|
||||||
final_result = "Task completed but no final response was generated."
|
final_result = "Task completed but no final response was generated."
|
||||||
|
|
||||||
logger.info("Subagent [{}] completed successfully", task_id)
|
logger.info(f"Subagent [{task_id}] completed successfully")
|
||||||
await self._announce_result(task_id, label, task, final_result, origin, "ok")
|
await self._announce_result(task_id, label, task, final_result, origin, "ok")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"Error: {str(e)}"
|
error_msg = f"Error: {str(e)}"
|
||||||
logger.error("Subagent [{}] failed: {}", task_id, e)
|
logger.error(f"Subagent [{task_id}] failed: {e}")
|
||||||
await self._announce_result(task_id, label, task, error_msg, origin, "error")
|
await self._announce_result(task_id, label, task, error_msg, origin, "error")
|
||||||
|
|
||||||
async def _announce_result(
|
async def _announce_result(
|
||||||
@@ -215,15 +223,17 @@ Result:
|
|||||||
Summarize this naturally for the user. Keep it brief (1-2 sentences). Do not mention technical details like "subagent" or task IDs."""
|
Summarize this naturally for the user. Keep it brief (1-2 sentences). Do not mention technical details like "subagent" or task IDs."""
|
||||||
|
|
||||||
# Inject as system message to trigger main agent
|
# Inject as system message to trigger main agent
|
||||||
|
# Propagate metadata from origin (e.g. suppress_output)
|
||||||
msg = InboundMessage(
|
msg = InboundMessage(
|
||||||
channel="system",
|
channel="system",
|
||||||
sender_id="subagent",
|
sender_id="subagent",
|
||||||
chat_id=f"{origin['channel']}:{origin['chat_id']}",
|
chat_id=f"{origin['channel']}:{origin['chat_id']}",
|
||||||
content=announce_content,
|
content=announce_content,
|
||||||
|
metadata=origin.get("metadata", {}),
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.bus.publish_inbound(msg)
|
await self.bus.publish_inbound(msg)
|
||||||
logger.debug("Subagent [{}] announced result to {}:{}", task_id, origin['channel'], origin['chat_id'])
|
logger.debug(f"Subagent [{task_id}] announced result to {origin['channel']}:{origin['chat_id']}")
|
||||||
|
|
||||||
def _build_subagent_prompt(self, task: str) -> str:
|
def _build_subagent_prompt(self, task: str) -> str:
|
||||||
"""Build a focused system prompt for the subagent."""
|
"""Build a focused system prompt for the subagent."""
|
||||||
@@ -254,17 +264,6 @@ Skills are available at: {self.workspace}/skills/ (read SKILL.md files as needed
|
|||||||
|
|
||||||
When you have completed the task, provide a clear summary of your findings or actions."""
|
When you have completed the task, provide a clear summary of your findings or actions."""
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
async def cancel_by_session(self, session_key: str) -> int:
|
|
||||||
"""Cancel all subagents for the given session. Returns count cancelled."""
|
|
||||||
tasks = [self._running_tasks[tid] for tid in self._session_tasks.get(session_key, [])
|
|
||||||
if tid in self._running_tasks and not self._running_tasks[tid].done()]
|
|
||||||
for t in tasks:
|
|
||||||
t.cancel()
|
|
||||||
if tasks:
|
|
||||||
await asyncio.gather(*tasks, return_exceptions=True)
|
|
||||||
return len(tasks)
|
|
||||||
=======
|
|
||||||
async def wait_for(self, task_ids: list[str]) -> str:
|
async def wait_for(self, task_ids: list[str]) -> str:
|
||||||
"""Wait for specified child subagents to complete and return their results."""
|
"""Wait for specified child subagents to complete and return their results."""
|
||||||
tasks_to_wait = [
|
tasks_to_wait = [
|
||||||
@@ -283,7 +282,6 @@ When you have completed the task, provide a clear summary of your findings or ac
|
|||||||
else:
|
else:
|
||||||
results.append(f"[{tid}]: No result found (invalid ID or task failed before storing)")
|
results.append(f"[{tid}]: No result found (invalid ID or task failed before storing)")
|
||||||
return "\n\n---\n\n".join(results)
|
return "\n\n---\n\n".join(results)
|
||||||
>>>>>>> c606ab9 (Add wait_for_subagents tool and silence child subagent announcements)
|
|
||||||
|
|
||||||
def get_running_count(self) -> int:
|
def get_running_count(self) -> int:
|
||||||
"""Return the number of currently running subagents."""
|
"""Return the number of currently running subagents."""
|
||||||
|
|||||||
@@ -9,19 +9,24 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class SpawnTool(Tool):
|
class SpawnTool(Tool):
|
||||||
"""Tool to spawn a subagent for background task execution."""
|
"""
|
||||||
|
Tool to spawn a subagent for background task execution.
|
||||||
|
|
||||||
|
The subagent runs asynchronously and announces its result back
|
||||||
|
to the main agent when complete.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, manager: "SubagentManager"):
|
def __init__(self, manager: "SubagentManager"):
|
||||||
self._manager = manager
|
self._manager = manager
|
||||||
self._origin_channel = "cli"
|
self._origin_channel = "cli"
|
||||||
self._origin_chat_id = "direct"
|
self._origin_chat_id = "direct"
|
||||||
self._session_key = "cli:direct"
|
self._origin_metadata: dict[str, Any] = {}
|
||||||
|
|
||||||
def set_context(self, channel: str, chat_id: str) -> None:
|
def set_context(self, channel: str, chat_id: str, metadata: dict[str, Any] | None = None) -> None:
|
||||||
"""Set the origin context for subagent announcements."""
|
"""Set the origin context for subagent announcements."""
|
||||||
self._origin_channel = channel
|
self._origin_channel = channel
|
||||||
self._origin_chat_id = chat_id
|
self._origin_chat_id = chat_id
|
||||||
self._session_key = f"{channel}:{chat_id}"
|
self._origin_metadata = metadata or {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
@@ -64,5 +69,5 @@ class SpawnTool(Tool):
|
|||||||
model=model,
|
model=model,
|
||||||
origin_channel=self._origin_channel,
|
origin_channel=self._origin_channel,
|
||||||
origin_chat_id=self._origin_chat_id,
|
origin_chat_id=self._origin_chat_id,
|
||||||
session_key=self._session_key,
|
origin_metadata=self._origin_metadata,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user