diff --git a/src/agent_box/agents/claude_code.py b/src/agent_box/agents/claude_code.py index e348594..fd3f147 100644 --- a/src/agent_box/agents/claude_code.py +++ b/src/agent_box/agents/claude_code.py @@ -544,7 +544,13 @@ async def _send_tool_result( } # Use the transport directly — ``client.query()`` only sends plain # text user messages, but we need to attach a tool_result payload. - await client._transport.write(json.dumps(message) + "\n") + payload = json.dumps(message) + "\n" + log.info( + "sending tool_result to CLI: tool_use_id=%s session_id=%s content_preview=%r", + tool_use_id, session_id or "(none)", answer[:200], + ) + await client._transport.write(payload) + log.info("tool_result written to CLI stdin for tool_use_id=%s", tool_use_id) @property def has_pending_question(self) -> bool: @@ -558,6 +564,15 @@ def has_pending_question(self) -> bool: async def run(self, prompt: str, user_id: str = "", channel: str = "") -> AsyncIterator[OutgoingMessage]: client = await self._ensure_client() + # Diagnostic: trace every run() entry — what prompt, what pending state. + pending_kind = self._pending_ask.get("kind") if self._pending_ask else None + pending_tool = self._pending_ask.get("tool_use_id") if self._pending_ask else None + log.info( + "run() entry: project=%s user=%s channel=%s prompt_preview=%r pending_kind=%s pending_tool_use_id=%s", + self.project.name, user_id, channel, + (prompt or "")[:200], pending_kind, pending_tool, + ) + # If a previous run() paused with a pending AskUserQuestion or # ExitPlanMode, the current prompt is the user's answer — send it # as a tool_result so the blocked CLI can continue. @@ -566,13 +581,14 @@ async def run(self, prompt: str, user_id: str = "", channel: str = "") -> AsyncI self._pending_ask = None kind = ask.get("kind", "question") log.info( - "resuming pending %s tool_use_id=%s", - kind, ask["tool_use_id"], + "consuming pending ask: kind=%s tool_use_id=%s session_id=%s user_reply_preview=%r", + kind, ask["tool_use_id"], ask.get("session_id") or "(none)", + (prompt or "")[:200], ) if kind == "exit_plan_mode": content = _build_plan_approval_result(prompt) - log.info("ExitPlanMode reply parsed: %s", content) + log.info("ExitPlanMode reply parsed → tool_result content: %r", content[:200]) else: questions = ask.get("questions", []) try: @@ -597,6 +613,7 @@ async def run(self, prompt: str, user_id: str = "", channel: str = "") -> AsyncI session_id=ask.get("session_id") or "", ) else: + log.info("no pending ask — sending prompt as fresh query to CLI") await client.query(prompt) # Build prefixes to strip from file paths in tool summaries. @@ -624,8 +641,9 @@ async def run(self, prompt: str, user_id: str = "", channel: str = "") -> AsyncI body = "(agent 请求退出 plan 模式,但没有提供计划内容)" text = body + _PLAN_APPROVAL_HINT log.info( - "ExitPlanMode detected, tool_use_id=%s", - block.id, + "ExitPlanMode detected — setting pending ask: " + "tool_use_id=%s session_id=%s", + block.id, msg.session_id, ) yield OutgoingMessage( text=text, @@ -639,13 +657,17 @@ async def run(self, prompt: str, user_id: str = "", channel: str = "") -> AsyncI "session_id": msg.session_id, "kind": "exit_plan_mode", } + log.info( + "ExitPlanMode pending ask saved, returning from run() — " + "waiting for user reply to approve/reject" + ) return # Stop yielding; next run() will resume # AskUserQuestion log.info( - "AskUserQuestion detected, tool_use_id=%s, input=%s", - block.id, - block.input, + "AskUserQuestion detected — setting pending ask: " + "tool_use_id=%s session_id=%s input=%s", + block.id, msg.session_id, block.input, ) question_text = self._format_question(block) yield OutgoingMessage( @@ -661,10 +683,9 @@ async def run(self, prompt: str, user_id: str = "", channel: str = "") -> AsyncI "kind": "question", } log.info( - "AskUserQuestion intercepted, pausing run() " - "tool_use_id=%s, question_text=%s", - block.id, - question_text, + "AskUserQuestion pending ask saved, returning from run() — " + "waiting for user reply (question_preview=%r)", + question_text[:200], ) return # Stop yielding; next run() will resume diff --git a/src/agent_box/main.py b/src/agent_box/main.py index 46f4fde..2d9cac4 100644 --- a/src/agent_box/main.py +++ b/src/agent_box/main.py @@ -46,9 +46,14 @@ def _create_channel(self, channel_type: str, send_in: anyio.abc.ObjectSendStream async def handle_message( self, msg: IncomingMessage, reply: anyio.abc.ObjectSendStream[OutgoingMessage] ) -> None: + log.info( + "handle_message entry: user=%s channel=%s text_preview=%r", + msg.user_id, msg.channel, (msg.text or "")[:200], + ) result = await self.router.route(msg) if result.reply is not None: + log.info("router replied directly (no agent call): %r", (result.reply or "")[:200]) await reply.send(OutgoingMessage(text=result.reply, user_id=msg.user_id, channel=msg.channel)) if result.reset_agent: current = self.sessions.get_current() @@ -60,6 +65,14 @@ async def handle_message( project_name = result.project or self.sessions.get_current() self.sessions.ensure_default() # always available as a fallback agent = self._get_or_create_agent(project_name) + # Surface pending-question state so we can see whether the user's + # reply is about to be consumed as a tool_result or treated as a + # fresh query. + has_pending = getattr(agent, "has_pending_question", False) + log.info( + "dispatching to agent: project=%s agent_type=%s has_pending_question=%s", + project_name, type(agent).__name__, has_pending, + ) async for out_msg in agent.run(msg.text, user_id=msg.user_id, channel=msg.channel): if out_msg.text and out_msg.type.value == "text" and self.sessions.get_current() != project_name: out_msg = OutgoingMessage( @@ -71,6 +84,7 @@ async def handle_message( ) await reply.send(out_msg) self.sessions.update_session_id(project_name, agent.project.session_id or "") + log.info("handle_message done: project=%s", project_name) async def run(self, channel_types: list[str] | None = None) -> None: """Run the app with one or more channels simultaneously. @@ -150,6 +164,10 @@ async def _safe_handle(msg: IncomingMessage, reply: anyio.abc.ObjectSendStream[O try: async with anyio.create_task_group() as tg: async for msg in recv_in: + log.info( + "dispatch_loop received message: user=%s channel=%s text_preview=%r", + msg.user_id, msg.channel, (msg.text or "")[:200], + ) tg.start_soon(_safe_handle, msg, send_out.clone()) except Exception: log.exception("dispatch loop crashed")