Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 34 additions & 13 deletions src/agent_box/agents/claude_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
Expand All @@ -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:
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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

Expand Down
18 changes: 18 additions & 0 deletions src/agent_box/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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(
Expand All @@ -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.
Expand Down Expand Up @@ -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")
Expand Down
Loading