fix(agent): propagate effective session key through subagent pipeline

The previous fix hardcoded session_key_override as channel:chat_id which
broke unified session mode where pending queues use "unified:default".
Propagate the effective key from _set_tool_context through SpawnTool
into the origin dict so _announce_result routes to the correct pending
queue in both normal and unified session modes.
This commit is contained in:
chengyongru
2026-04-20 14:47:14 +08:00
committed by Xubin Ren
parent 2193a64c80
commit 68466b1c2a
4 changed files with 104 additions and 8 deletions
+7 -1
View File
@@ -318,10 +318,16 @@ class AgentLoop:
def _set_tool_context(self, channel: str, chat_id: str, message_id: str | None = None) -> None:
"""Update context for all tools that need routing info."""
# Compute the effective session key (accounts for unified sessions)
# so that subagent results route to the correct pending queue.
effective_key = UNIFIED_SESSION_KEY if self._unified_session else f"{channel}:{chat_id}"
for name in ("message", "spawn", "cron", "my"):
if tool := self.tools.get(name):
if hasattr(tool, "set_context"):
tool.set_context(channel, chat_id, *([message_id] if name == "message" else []))
if name == "spawn":
tool.set_context(channel, chat_id, effective_key=effective_key)
else:
tool.set_context(channel, chat_id, *([message_id] if name == "message" else []))
@staticmethod
def _strip_think(text: str | None) -> str | None:
+7 -5
View File
@@ -107,7 +107,7 @@ class SubagentManager:
"""Spawn a subagent to execute a task in the background."""
task_id = str(uuid.uuid4())[:8]
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, "session_key": session_key}
status = SubagentStatus(
task_id=task_id,
@@ -241,15 +241,17 @@ class SubagentManager:
)
# Inject as system message to trigger main agent.
# Use session_key_override to align with the main agent's session key
# so the result is routed to the pending queue (mid-turn injection)
# instead of being dispatched as a competing independent task.
# Use session_key_override to align with the main agent's effective
# session key (which accounts for unified sessions) so the result is
# routed to the correct pending queue (mid-turn injection) instead of
# being dispatched as a competing independent task.
override = origin.get("session_key") or f"{origin['channel']}:{origin['chat_id']}"
msg = InboundMessage(
channel="system",
sender_id="subagent",
chat_id=f"{origin['channel']}:{origin['chat_id']}",
content=announce_content,
session_key_override=f"{origin['channel']}:{origin['chat_id']}",
session_key_override=override,
metadata={
"injected_event": "subagent_result",
"subagent_task_id": task_id,
+2 -2
View File
@@ -25,11 +25,11 @@ class SpawnTool(Tool):
self._origin_chat_id = "direct"
self._session_key = "cli:direct"
def set_context(self, channel: str, chat_id: str) -> None:
def set_context(self, channel: str, chat_id: str, effective_key: str | None = None) -> None:
"""Set the origin context for subagent announcements."""
self._origin_channel = channel
self._origin_chat_id = chat_id
self._session_key = f"{channel}:{chat_id}"
self._session_key = effective_key or f"{channel}:{chat_id}"
@property
def name(self) -> str:
+88
View File
@@ -412,3 +412,91 @@ class TestSubagentCancellation:
assert cancelled.is_set()
assert task.cancelled()
mgr._announce_result.assert_not_awaited()
class TestSubagentAnnounceSessionKey:
"""Verify _announce_result uses the effective session key for mid-turn routing."""
def _make_mgr(self):
"""Create a SubagentManager with mocked deps and its bus."""
from nanobot.agent.subagent import SubagentManager
from nanobot.bus.queue import MessageBus
bus = MessageBus()
provider = MagicMock()
provider.get_default_model.return_value = "test-model"
mgr = SubagentManager(
provider=provider,
workspace=MagicMock(),
bus=bus,
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
)
return mgr, bus
@pytest.mark.asyncio
async def test_announce_uses_effective_key_in_unified_mode(self):
"""In unified session mode, session_key_override must be 'unified:default'
so the result matches the pending queue key."""
mgr, bus = self._make_mgr()
origin = {"channel": "telegram", "chat_id": "111", "session_key": "unified:default"}
await mgr._announce_result("sub-1", "label", "task", "result", origin, "ok")
msg = await bus.consume_inbound()
assert msg.session_key_override == "unified:default"
assert msg.session_key == "unified:default"
@pytest.mark.asyncio
async def test_announce_uses_raw_key_in_normal_mode(self):
"""Without unified sessions, session_key_override is the raw channel:chat_id."""
mgr, bus = self._make_mgr()
origin = {"channel": "telegram", "chat_id": "222", "session_key": "telegram:222"}
await mgr._announce_result("sub-2", "label", "task", "result", origin, "ok")
msg = await bus.consume_inbound()
assert msg.session_key_override == "telegram:222"
assert msg.session_key == "telegram:222"
@pytest.mark.asyncio
async def test_announce_falls_back_to_origin_when_no_session_key(self):
"""When session_key is None, fallback to f'{channel}:{chat_id}'."""
mgr, bus = self._make_mgr()
origin = {"channel": "discord", "chat_id": "333", "session_key": None}
await mgr._announce_result("sub-3", "label", "task", "result", origin, "ok")
msg = await bus.consume_inbound()
assert msg.session_key_override == "discord:333"
assert msg.channel == "system"
assert msg.chat_id == "discord:333"
@pytest.mark.asyncio
async def test_session_key_flows_through_run_subagent(self):
"""Verify session_key in origin propagates from _run_subagent to _announce_result."""
from nanobot.agent.subagent import SubagentStatus
mgr, bus = self._make_mgr()
async def fake_run(spec):
return SimpleNamespace(
stop_reason="done",
final_content="done",
error=None,
tool_events=[],
)
mgr.runner.run = AsyncMock(side_effect=fake_run)
status = SubagentStatus(
task_id="sub-4", label="label", task_description="task",
started_at=time.monotonic(),
)
await mgr._run_subagent(
"sub-4", "task", "label",
{"channel": "telegram", "chat_id": "444", "session_key": "unified:default"},
status,
)
msg = await bus.consume_inbound()
assert msg.session_key_override == "unified:default"