mirror of
https://github.com/usestrix/strix.git
synced 2026-09-12 23:01:05 +00:00
feat: add graceful wrap-up command for agents
Add a new "wrap up" feature that allows users to gracefully request an agent to finish its current work and report findings, instead of hard-stopping with ESC. Changes: - Add wrap_up_agent() function in agents_graph_actions.py that sends an urgent message instructing the agent to conclude within 3-5 iterations - Add WrapUpAgentScreen modal dialog in tui.py with amber/warning styling - Add 'W' keyboard binding to trigger wrap-up on selected agent - Update help screen to show new keybinding - Add CSS styles for the wrap-up dialog This provides a middle ground between letting an agent run indefinitely and hard-stopping it - the agent can summarize findings and properly report back to its parent via agent_finish or finish_scan.
This commit is contained in:
parent
706bb193c0
commit
39c94bfeec
3 changed files with 265 additions and 1 deletions
|
|
@ -596,6 +596,76 @@ StopAgentScreen {
|
|||
border: none;
|
||||
}
|
||||
|
||||
WrapUpAgentScreen {
|
||||
align: center middle;
|
||||
background: $background 0%;
|
||||
}
|
||||
|
||||
#wrap_up_agent_dialog {
|
||||
grid-size: 1;
|
||||
grid-gutter: 1;
|
||||
grid-rows: auto auto auto;
|
||||
padding: 1;
|
||||
width: 40;
|
||||
height: auto;
|
||||
border: round #f59e0b;
|
||||
background: #1a1a1a 98%;
|
||||
}
|
||||
|
||||
#wrap_up_agent_title {
|
||||
color: #f59e0b;
|
||||
text-style: bold;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#wrap_up_agent_description {
|
||||
color: #a3a3a3;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#wrap_up_agent_buttons {
|
||||
grid-size: 2;
|
||||
grid-gutter: 1;
|
||||
grid-columns: 1fr 1fr;
|
||||
width: 100%;
|
||||
height: 1;
|
||||
}
|
||||
|
||||
#wrap_up_agent_buttons Button {
|
||||
height: 1;
|
||||
min-height: 1;
|
||||
border: none;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
#wrap_up_agent {
|
||||
background: transparent;
|
||||
color: #f59e0b;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#wrap_up_agent:hover, #wrap_up_agent:focus {
|
||||
background: #f59e0b;
|
||||
color: #1a1a1a;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#cancel_wrap_up {
|
||||
background: transparent;
|
||||
color: #737373;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#cancel_wrap_up:hover, #cancel_wrap_up:focus {
|
||||
background: rgb(54, 54, 54);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
QuitScreen {
|
||||
align: center middle;
|
||||
background: $background 0%;
|
||||
|
|
|
|||
|
|
@ -157,7 +157,8 @@ class HelpScreen(ModalScreen): # type: ignore[misc]
|
|||
yield Grid(
|
||||
Label("🦉 Strix Help", id="help_title"),
|
||||
Label(
|
||||
"F1 Help\nCtrl+Q/C Quit\nESC Stop Agent\n"
|
||||
"F1 Help\nCtrl+Q/C Quit\nESC Stop Agent (hard stop)\n"
|
||||
"W Wrap Up Agent (graceful)\n"
|
||||
"Enter Send message to agent\nTab Switch panels\n↑/↓ Navigate tree",
|
||||
id="help_content",
|
||||
),
|
||||
|
|
@ -217,6 +218,59 @@ class StopAgentScreen(ModalScreen): # type: ignore[misc]
|
|||
self.app.pop_screen()
|
||||
|
||||
|
||||
class WrapUpAgentScreen(ModalScreen): # type: ignore[misc]
|
||||
def __init__(self, agent_name: str, agent_id: str):
|
||||
super().__init__()
|
||||
self.agent_name = agent_name
|
||||
self.agent_id = agent_id
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Grid(
|
||||
Label(f"📝 Wrap up '{self.agent_name}'?", id="wrap_up_agent_title"),
|
||||
Label(
|
||||
"Agent will finish current work and report findings.",
|
||||
id="wrap_up_agent_description",
|
||||
),
|
||||
Grid(
|
||||
Button("Yes", variant="warning", id="wrap_up_agent"),
|
||||
Button("No", variant="default", id="cancel_wrap_up"),
|
||||
id="wrap_up_agent_buttons",
|
||||
),
|
||||
id="wrap_up_agent_dialog",
|
||||
)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
cancel_button = self.query_one("#cancel_wrap_up", Button)
|
||||
cancel_button.focus()
|
||||
|
||||
def on_key(self, event: events.Key) -> None:
|
||||
if event.key in ("left", "right", "up", "down"):
|
||||
focused = self.focused
|
||||
|
||||
if focused and focused.id == "wrap_up_agent":
|
||||
cancel_button = self.query_one("#cancel_wrap_up", Button)
|
||||
cancel_button.focus()
|
||||
else:
|
||||
wrap_up_button = self.query_one("#wrap_up_agent", Button)
|
||||
wrap_up_button.focus()
|
||||
|
||||
event.prevent_default()
|
||||
elif event.key == "enter":
|
||||
focused = self.focused
|
||||
if focused and isinstance(focused, Button):
|
||||
focused.press()
|
||||
event.prevent_default()
|
||||
elif event.key == "escape":
|
||||
self.app.pop_screen()
|
||||
event.prevent_default()
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "wrap_up_agent":
|
||||
self.app.action_confirm_wrap_up_agent(self.agent_id)
|
||||
else:
|
||||
self.app.pop_screen()
|
||||
|
||||
|
||||
class QuitScreen(ModalScreen): # type: ignore[misc]
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Grid(
|
||||
|
|
@ -272,6 +326,7 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||
Binding("ctrl+q", "request_quit", "Quit", priority=True),
|
||||
Binding("ctrl+c", "request_quit", "Quit", priority=True),
|
||||
Binding("escape", "stop_selected_agent", "Stop Agent", priority=True),
|
||||
Binding("w", "wrap_up_selected_agent", "Wrap Up Agent", priority=True),
|
||||
]
|
||||
|
||||
def __init__(self, args: argparse.Namespace):
|
||||
|
|
@ -1227,6 +1282,69 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||
|
||||
logging.exception(f"Failed to stop agent {agent_id}")
|
||||
|
||||
def action_wrap_up_selected_agent(self) -> None:
|
||||
if (
|
||||
self.show_splash
|
||||
or not self.is_mounted
|
||||
or len(self.screen_stack) > 1
|
||||
or not self.selected_agent_id
|
||||
):
|
||||
return
|
||||
|
||||
agent_name, should_wrap_up = self._validate_agent_for_wrap_up()
|
||||
if not should_wrap_up:
|
||||
return
|
||||
|
||||
try:
|
||||
self.query_one("#main_container")
|
||||
except (ValueError, Exception):
|
||||
return
|
||||
|
||||
self.push_screen(WrapUpAgentScreen(agent_name, self.selected_agent_id))
|
||||
|
||||
def _validate_agent_for_wrap_up(self) -> tuple[str, bool]:
|
||||
agent_name = "Unknown Agent"
|
||||
|
||||
try:
|
||||
if self.tracer and self.selected_agent_id in self.tracer.agents:
|
||||
agent_data = self.tracer.agents[self.selected_agent_id]
|
||||
agent_name = agent_data.get("name", "Unknown Agent")
|
||||
|
||||
agent_status = agent_data.get("status", "running")
|
||||
if agent_status not in ["running", "waiting"]:
|
||||
return agent_name, False
|
||||
|
||||
return agent_name, True
|
||||
|
||||
except (KeyError, AttributeError, ValueError) as e:
|
||||
import logging
|
||||
|
||||
logging.warning(f"Failed to validate agent for wrap-up: {e}")
|
||||
|
||||
return agent_name, False
|
||||
|
||||
def action_confirm_wrap_up_agent(self, agent_id: str) -> None:
|
||||
self.pop_screen()
|
||||
|
||||
try:
|
||||
from strix.tools.agents_graph.agents_graph_actions import wrap_up_agent
|
||||
|
||||
result = wrap_up_agent(agent_id)
|
||||
|
||||
import logging
|
||||
|
||||
if result.get("success"):
|
||||
logging.info(f"Wrap-up request sent to agent: {result.get('message', 'Unknown')}")
|
||||
else:
|
||||
logging.warning(
|
||||
f"Failed to send wrap-up request: {result.get('error', 'Unknown error')}"
|
||||
)
|
||||
|
||||
except Exception:
|
||||
import logging
|
||||
|
||||
logging.exception(f"Failed to wrap up agent {agent_id}")
|
||||
|
||||
def action_custom_quit(self) -> None:
|
||||
for agent_id in list(self._agent_verb_timers.keys()):
|
||||
self._stop_agent_verb_timer(agent_id)
|
||||
|
|
|
|||
|
|
@ -530,6 +530,82 @@ def stop_agent(agent_id: str) -> dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
def wrap_up_agent(agent_id: str) -> dict[str, Any]:
|
||||
"""Request an agent to gracefully wrap up its work and report findings."""
|
||||
try:
|
||||
if agent_id not in _agent_graph["nodes"]:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Agent '{agent_id}' not found in graph",
|
||||
"agent_id": agent_id,
|
||||
}
|
||||
|
||||
agent_node = _agent_graph["nodes"][agent_id]
|
||||
|
||||
if agent_node["status"] in ["completed", "error", "failed", "stopped"]:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Agent '{agent_node['name']}' has already finished",
|
||||
"agent_id": agent_id,
|
||||
"previous_status": agent_node["status"],
|
||||
}
|
||||
|
||||
# Send a wrap-up message to the agent
|
||||
wrap_up_message = """<wrap_up_request>
|
||||
<priority>URGENT</priority>
|
||||
<instruction>
|
||||
The user has requested that you wrap up your current work.
|
||||
You have approximately 3-5 iterations to:
|
||||
1. Complete or pause any critical in-progress work
|
||||
2. Summarize your findings and progress so far
|
||||
3. Call agent_finish (if you are a subagent) or finish_scan (if you are the root agent)
|
||||
to properly report your results
|
||||
|
||||
Do NOT start any new major tasks. Focus on concluding your work gracefully.
|
||||
</instruction>
|
||||
</wrap_up_request>"""
|
||||
|
||||
if agent_id not in _agent_messages:
|
||||
_agent_messages[agent_id] = []
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
_agent_messages[agent_id].append(
|
||||
{
|
||||
"id": f"wrapup_{uuid4().hex[:8]}",
|
||||
"from": "user",
|
||||
"to": agent_id,
|
||||
"content": wrap_up_message,
|
||||
"message_type": "instruction",
|
||||
"priority": "urgent",
|
||||
"timestamp": datetime.now(UTC).isoformat(),
|
||||
"delivered": True,
|
||||
"read": False,
|
||||
}
|
||||
)
|
||||
|
||||
# If agent is waiting, resume it so it can process the wrap-up request
|
||||
if agent_id in _agent_states:
|
||||
agent_state = _agent_states[agent_id]
|
||||
if agent_state.is_waiting_for_input():
|
||||
agent_state.resume_from_waiting()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Wrap-up request sent to agent '{agent_node['name']}'",
|
||||
"agent_id": agent_id,
|
||||
"agent_name": agent_node["name"],
|
||||
"note": "Agent will gracefully finish and report its findings",
|
||||
}
|
||||
|
||||
except Exception as e: # noqa: BLE001
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Failed to send wrap-up request: {e}",
|
||||
"agent_id": agent_id,
|
||||
}
|
||||
|
||||
|
||||
def send_user_message_to_agent(agent_id: str, message: str) -> dict[str, Any]:
|
||||
try:
|
||||
if agent_id not in _agent_graph["nodes"]:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue