fix(agent): scope subagent reply dedupe to origin message
Made-with: Cursor
This commit is contained in:
+39
-24
@@ -7,7 +7,7 @@ import dataclasses
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from contextlib import AsyncExitStack, nullcontext
|
||||
from contextlib import AsyncExitStack, nullcontext, suppress
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable
|
||||
|
||||
@@ -28,6 +28,7 @@ from nanobot.agent.tools.ask import (
|
||||
pending_ask_user_id,
|
||||
)
|
||||
from nanobot.agent.tools.cron import CronTool
|
||||
from nanobot.agent.tools.file_state import FileStateStore, bind_file_states, reset_file_states
|
||||
from nanobot.agent.tools.filesystem import EditFileTool, ListDirTool, ReadFileTool, WriteFileTool
|
||||
from nanobot.agent.tools.message import MessageTool
|
||||
from nanobot.agent.tools.notebook import NotebookEditTool
|
||||
@@ -247,6 +248,9 @@ class AgentLoop:
|
||||
self.context = ContextBuilder(workspace, timezone=timezone, disabled_skills=disabled_skills)
|
||||
self.sessions = session_manager or SessionManager(workspace)
|
||||
self.tools = ToolRegistry()
|
||||
# One file-read/write tracker per logical session. The tool registry is
|
||||
# shared by this loop, so tools resolve the active state via contextvars.
|
||||
self._file_state_store = FileStateStore()
|
||||
self.runner = AgentRunner(provider)
|
||||
self.subagents = SubagentManager(
|
||||
provider=provider,
|
||||
@@ -351,7 +355,9 @@ class AgentLoop:
|
||||
self.tools.register(AskUserTool())
|
||||
self.tools.register(
|
||||
ReadFileTool(
|
||||
workspace=self.workspace, allowed_dir=allowed_dir, extra_allowed_dirs=extra_read
|
||||
workspace=self.workspace,
|
||||
allowed_dir=allowed_dir,
|
||||
extra_allowed_dirs=extra_read,
|
||||
)
|
||||
)
|
||||
for cls in (WriteFileTool, EditFileTool, ListDirTool):
|
||||
@@ -435,6 +441,8 @@ class AgentLoop:
|
||||
if hasattr(tool, "set_context"):
|
||||
if name == "spawn":
|
||||
tool.set_context(channel, chat_id, effective_key=effective_key)
|
||||
if hasattr(tool, "set_origin_message_id"):
|
||||
tool.set_origin_message_id(message_id)
|
||||
elif name == "cron":
|
||||
tool.set_context(channel, chat_id, metadata=metadata, session_key=session_key)
|
||||
elif name == "message":
|
||||
@@ -486,10 +494,8 @@ class AgentLoop:
|
||||
tasks = self._active_tasks.pop(key, [])
|
||||
cancelled = sum(1 for t in tasks if not t.done() and t.cancel())
|
||||
for t in tasks:
|
||||
try:
|
||||
with suppress(asyncio.CancelledError, Exception):
|
||||
await t
|
||||
except (asyncio.CancelledError, Exception):
|
||||
pass
|
||||
sub_cancelled = await self.subagents.cancel_by_session(key)
|
||||
return cancelled + sub_cancelled
|
||||
|
||||
@@ -618,25 +624,30 @@ class AgentLoop:
|
||||
|
||||
return items
|
||||
|
||||
result = await self.runner.run(AgentRunSpec(
|
||||
initial_messages=initial_messages,
|
||||
tools=self.tools,
|
||||
model=self.model,
|
||||
max_iterations=self.max_iterations,
|
||||
max_tool_result_chars=self.max_tool_result_chars,
|
||||
hook=hook,
|
||||
error_message="Sorry, I encountered an error calling the AI model.",
|
||||
concurrent_tools=True,
|
||||
workspace=self.workspace,
|
||||
session_key=session.key if session else None,
|
||||
context_window_tokens=self.context_window_tokens,
|
||||
context_block_limit=self.context_block_limit,
|
||||
provider_retry_mode=self.provider_retry_mode,
|
||||
progress_callback=on_progress,
|
||||
retry_wait_callback=on_retry_wait,
|
||||
checkpoint_callback=_checkpoint,
|
||||
injection_callback=_drain_pending,
|
||||
))
|
||||
active_session_key = session.key if session else session_key
|
||||
file_state_token = bind_file_states(self._file_state_store.for_session(active_session_key))
|
||||
try:
|
||||
result = await self.runner.run(AgentRunSpec(
|
||||
initial_messages=initial_messages,
|
||||
tools=self.tools,
|
||||
model=self.model,
|
||||
max_iterations=self.max_iterations,
|
||||
max_tool_result_chars=self.max_tool_result_chars,
|
||||
hook=hook,
|
||||
error_message="Sorry, I encountered an error calling the AI model.",
|
||||
concurrent_tools=True,
|
||||
workspace=self.workspace,
|
||||
session_key=session.key if session else None,
|
||||
context_window_tokens=self.context_window_tokens,
|
||||
context_block_limit=self.context_block_limit,
|
||||
provider_retry_mode=self.provider_retry_mode,
|
||||
progress_callback=on_progress,
|
||||
retry_wait_callback=on_retry_wait,
|
||||
checkpoint_callback=_checkpoint,
|
||||
injection_callback=_drain_pending,
|
||||
))
|
||||
finally:
|
||||
reset_file_states(file_state_token)
|
||||
self._last_usage = result.usage
|
||||
if result.stop_reason == "max_iterations":
|
||||
logger.warning("Max iterations ({}) reached", self.max_iterations)
|
||||
@@ -920,6 +931,7 @@ class AgentLoop:
|
||||
chat_id=chat_id,
|
||||
session_summary=pending,
|
||||
current_role=current_role,
|
||||
sender_id=msg.sender_id,
|
||||
)
|
||||
final_content, _, all_msgs, stop_reason, _ = await self._run_agent_loop(
|
||||
messages, session=session, channel=channel, chat_id=chat_id,
|
||||
@@ -947,6 +959,8 @@ class AgentLoop:
|
||||
outbound_metadata: dict[str, Any] = {}
|
||||
if channel == "slack" and key.startswith("slack:") and key.count(":") >= 2:
|
||||
outbound_metadata["slack"] = {"thread_ts": key.split(":", 2)[2]}
|
||||
if origin_message_id := msg.metadata.get("origin_message_id"):
|
||||
outbound_metadata["origin_message_id"] = origin_message_id
|
||||
return OutboundMessage(
|
||||
channel=channel,
|
||||
chat_id=chat_id,
|
||||
@@ -1015,6 +1029,7 @@ class AgentLoop:
|
||||
media=msg.media if msg.media else None,
|
||||
channel=msg.channel,
|
||||
chat_id=self._runtime_chat_id(msg),
|
||||
sender_id=msg.sender_id,
|
||||
)
|
||||
|
||||
async def _bus_progress(
|
||||
|
||||
Reference in New Issue
Block a user