mirror of
https://github.com/usestrix/strix.git
synced 2026-08-24 03:42:37 +02:00
refactor(tools): split wait_for_message into respond_to_user + wait_for_agents
One tool was doing three jobs (wait on the user, wait on other agents, and - wrongly - wait for a long-running command), so the driver had to guess which one an agent meant and used parent_id as the proxy: the root waits for a human, everyone else waits for agents. That proxy is wrong, since the user can message any agent from the TUI's agent tree. Tool identity now carries the intent, and the coordinator records it as a wait_kind that survives snapshot/restore: respond_to_user -> wait_kind="user", never auto-resumed (root or not) wait_for_agents -> wait_kind="agents", auto-resumed on a 300s timer recovery exhaust -> wait_kind="stalled" respond_to_user fuses the message and the yield into one call, so there is no way to answer and then forget to stop - the two-step that gpt-4o-mini skipped 2/2 in live testing. Plain text still renders as before. Auto-resume is also bounded now: an agent that re-parks after every timeout burned a model turn every 300s for the rest of the scan (and, since parked children notify their parent, spammed the parent's inbox on the same cycle). After _MAX_IDLE_AUTO_RESUMES it stays parked until a real message arrives.
This commit is contained in:
+11
-3
@@ -23,7 +23,7 @@ from strix.tools.agents_graph.tools import (
|
||||
send_message_to_agent,
|
||||
stop_agent,
|
||||
view_agent_graph,
|
||||
wait_for_message,
|
||||
wait_for_agents,
|
||||
)
|
||||
from strix.tools.finish.tool import finish_scan
|
||||
from strix.tools.load_skill.tool import load_skill
|
||||
@@ -49,6 +49,7 @@ from strix.tools.reporting.tool import (
|
||||
get_report,
|
||||
list_reports,
|
||||
)
|
||||
from strix.tools.respond.tool import respond_to_user
|
||||
from strix.tools.thinking.tool import think
|
||||
from strix.tools.todo.tools import (
|
||||
create_todo,
|
||||
@@ -345,6 +346,10 @@ def _make_shell_configurator(*, chat_completions: bool) -> Any:
|
||||
return configure
|
||||
|
||||
|
||||
# Tools that hand control away by parking the agent rather than ending the scan.
|
||||
_PARKING_TOOLS: frozenset[str] = frozenset({"respond_to_user", "wait_for_agents"})
|
||||
|
||||
|
||||
def _lifecycle_tool_completed(tool_name: str, output: Any) -> bool:
|
||||
if tool_name == "agent_finish":
|
||||
completion_key = "agent_completed"
|
||||
@@ -363,7 +368,7 @@ def _lifecycle_tool_completed(tool_name: str, output: Any) -> bool:
|
||||
|
||||
|
||||
def _wait_tool_parked(tool_name: str, output: Any) -> bool:
|
||||
if tool_name != "wait_for_message" or not isinstance(output, str):
|
||||
if tool_name not in _PARKING_TOOLS or not isinstance(output, str):
|
||||
return False
|
||||
try:
|
||||
parsed = json.loads(output)
|
||||
@@ -425,7 +430,7 @@ _BASE_TOOLS: tuple[Tool, ...] = (
|
||||
scope_rules,
|
||||
view_agent_graph,
|
||||
send_message_to_agent,
|
||||
wait_for_message,
|
||||
wait_for_agents,
|
||||
create_agent,
|
||||
stop_agent,
|
||||
)
|
||||
@@ -509,6 +514,9 @@ def build_strix_agent(
|
||||
)
|
||||
|
||||
agent_tools = [*_EXTRA_TOOLS, *(extra_tools or [])]
|
||||
if interactive:
|
||||
# Yielding to the user is only meaningful when one is attached.
|
||||
agent_tools.append(respond_to_user)
|
||||
if is_root:
|
||||
tools: list[Tool] = [*_BASE_TOOLS, *agent_tools, finish_scan]
|
||||
else:
|
||||
|
||||
@@ -33,22 +33,23 @@ INTER-AGENT MESSAGES:
|
||||
INTERACTIVE BEHAVIOR:
|
||||
- You are in an interactive conversation with a user.
|
||||
- HOW EXECUTION ENDS: your turn ends ONLY when you make an explicit lifecycle tool call. Plain text NEVER ends your turn and NEVER hands control to the user — text is shown to the user, and then execution continues.
|
||||
- To hand control back to the user (you answered them, or you need their input before continuing), call the wait_for_message tool. This is the ONLY sanctioned way to yield to the user; it parks you until the user's next message arrives.
|
||||
- To answer the user and hand control back, call respond_to_user. It delivers your message AND parks you for their reply in one call, so there is no way to answer and then forget to stop. This is the ONLY way to yield to the user.
|
||||
- To wait on another AGENT (a child's report, a peer's reply), call wait_for_agents. That is not a way to reach the user.
|
||||
- To end the whole engagement, call the lifecycle tool: finish_scan (root) or agent_finish (subagent).
|
||||
- A turn that ends with plain text and no tool call does NOT stop you: the system nudges you to continue and will re-run you. Do not rely on going silent to pause — it will not pause you.
|
||||
- Answering a user question: put your answer in text, then call wait_for_message in the SAME turn (the text is delivered to the user and you park for their reply). Do not answer and then fall silent — that just triggers a continuation nudge.
|
||||
- You may include brief explanatory text before a tool call.
|
||||
- Answering a user question: put the answer in respond_to_user's message. Do not write the answer as plain text and then fall silent — that does not reach a stopping point, it just triggers a continuation nudge.
|
||||
- You may include brief explanatory text before a tool call, and you can narrate while you work — plain text is shown to the user as you go. Narrating is free; respond_to_user is specifically the act of WAITING for the user, so do not call it just to give a status update.
|
||||
- Respond naturally when the user asks questions or gives instructions.
|
||||
- While actively working on a task, every turn should carry exactly one tool call — use think to plan, the appropriate tool to act, and wait_for_message only when you genuinely need the user.
|
||||
- Never loop through think or other tools just to prepare, polish, confirm, or announce an answer. Once you know the answer, say it (then wait_for_message).
|
||||
- While actively working on a task, every turn should carry exactly one tool call — use think to plan, the appropriate tool to act, and respond_to_user only when you genuinely need the user.
|
||||
- Never loop through think or other tools just to prepare, polish, confirm, or announce an answer. Once you know the answer, send it with respond_to_user.
|
||||
{% else %}
|
||||
AUTONOMOUS BEHAVIOR:
|
||||
- Work autonomously by default
|
||||
- You should NOT ask for user input or confirmation - you should always proceed with your task autonomously.
|
||||
- Minimize user messaging: avoid redundancy and repetition; consolidate updates into a single concise message
|
||||
- NEVER send an empty or blank message. If you have no content to output or need to wait (for user input, subagent results, or any other reason), you MUST call the wait_for_message tool (or another appropriate tool) instead of emitting an empty response.
|
||||
- If there is nothing to execute and no user query to answer any more: do NOT send filler/repetitive text — either call wait_for_message or finish your work (subagents: agent_finish; root: finish_scan)
|
||||
- While the agent loop is running, almost every output MUST be a tool call. Do NOT send plain text messages; act via tools. If idle, use wait_for_message; when done, use agent_finish (subagents) or finish_scan (root)
|
||||
- NEVER send an empty or blank message. If you have no content to output or need to wait for subagent results, you MUST call the wait_for_agents tool (or another appropriate tool) instead of emitting an empty response.
|
||||
- There is no user attached to this run, so there is nobody to ask and nothing to yield to. If there is nothing left to execute: do NOT send filler/repetitive text — either call wait_for_agents (only if you are genuinely expecting another agent to message you) or finish your work (subagents: agent_finish; root: finish_scan)
|
||||
- While the agent loop is running, almost every output MUST be a tool call. Do NOT send plain text messages; act via tools. If waiting on another agent, use wait_for_agents; when done, use agent_finish (subagents) or finish_scan (root)
|
||||
- A text-only turn — even one — IMMEDIATELY ends the scan/run with no report written. The lifecycle tools (``finish_scan`` for root, ``agent_finish`` for subagents) are the ONLY valid way to terminate. If you find yourself wanting to say "Done!" or "Scan complete" without a tool call, call the lifecycle tool instead — the report and termination signal both flow through it.
|
||||
{% endif %}
|
||||
</communication_rules>
|
||||
|
||||
Reference in New Issue
Block a user