Files
hermes-agent/tests/tools/test_delegate_interrupted_partial_output.py
teknium1 b71359066c fix: /stop halts background subagents and returns their partial results as interrupted completions
A turn's hard interrupt fans out only to `_active_children`; background
delegate_task units are detached from the parent at dispatch
(`_dispatch_background` / honor_parent_interrupt=False), so /stop left them
running to completion and their result arrived minutes later as a parked
wake. Every stop surface now also calls
`tools.async_delegation.interrupt_for_session` for the session's units:

- gateway: `_interrupt_and_clear_session` (busy /stop, /new share it — /new
  already did this in `_handle_reset_command`, the second request is
  idempotent) and the idle `_handle_stop_command` tail, which replied
  "No active task to stop." while a background child was running; it now
  stops them and replies "Stopped".
- tui_gateway `session.interrupt` (Desktop Stop / TUI stop) — own UI sid +
  spawner id only, so a viewer tab never kills gateway work.
- acp_adapter `cancel`.
- CLI `/stop` already used `interrupt_all` (process-wide); unchanged.

The stop recurses: depth>0 delegations are always synchronous
(`_model_background_value`), so the child's hard interrupt reaches its
workers through its own `_active_children` fan-out, and each level's
interrupted partial result rolls up as that child's completion.

An interrupted child's entry now carries what it actually had: the loop's
`final_response` is the "Operation interrupted." placeholder (also appended
as the closing assistant row), so `_build_result_entry` takes the child's
last real assistant text as `summary` and keeps the placeholder as `error`.
The unit finalizes normally and re-enters at once as its completion notice
(status=interrupted, "Partial output: ...", "Subagent Task Interrupted" on
TUI/Desktop) instead of the chat waiting for the child's budget to run out.

Docs: delegate_task description, tools/AGENTS.md, delegation.md,
gateway-session-lifecycle.md.

Part of #114456
2026-09-18 20:34:42 -07:00

29 lines
1.7 KiB
Python

"""Invariant: a child stopped mid-work reports what it HAD so far. The loop's ``final_response`` for an
interrupted turn is the placeholder ``"Operation interrupted."`` (also appended as the closing assistant row);
the parent-visible entry must carry the child's last real assistant text as ``summary`` and keep the
placeholder as ``error`` — never a partial result that reads as an empty stop. See #114456.
"""
from types import SimpleNamespace
from tools.delegate_tool_child_run import _SchemaOutcome, _build_result_entry
def test_interrupted_child_entry_carries_its_partial_output():
messages = [
{"role": "user", "content": "kickoff"},
{"role": "assistant", "content": [{"type": "text", "text": "Audited 3 of 7 modules; two findings so far."}],
"tool_calls": [{"id": "c1", "type": "function", "function": {"name": "terminal", "arguments": "{}"}}]},
{"role": "tool", "tool_call_id": "c1", "content": "[Command interrupted]"},
{"role": "assistant", "content": "Operation interrupted."},
]
result = {"final_response": "Operation interrupted.", "messages": messages, "api_calls": 2,
"completed": False, "interrupted": True}
child = SimpleNamespace(model="m", session_estimated_cost_usd=0.0, session_cost_status="unknown",
session_prompt_tokens=1, session_completion_tokens=1, _delegate_role="leaf")
entry = _build_result_entry(child, result, 0, 12.5, _SchemaOutcome(None, None, [], 0))
assert entry["status"] == entry["exit_reason"] == "interrupted"
assert entry["summary"] == "Audited 3 of 7 modules; two findings so far."
assert entry["error"] == "Operation interrupted."