File size: 5,295 Bytes
9d0fd45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
"""stop_subagent β€” cooperatively ask a running sub-agent to stop."""

from __future__ import annotations

import logging
from typing import Any

from frontier_agent.components.agent_bus import AgentBus
from frontier_agent.components.agent_bus.stop_signal import get_stop_registry
from frontier_agent.core.execution_context import get_current_execution_scope
from frontier_agent.core.runtime import registry
from frontier_agent.core.tool import tool
from plugins.tools._bus_scope import resolve_bus_task_id

logger = logging.getLogger(__name__)


def _current_metadata(bus: AgentBus, session_id: str) -> dict[str, Any]:
    """Metadata of the session's running job, or ``{}`` if it cannot be read.

    The existence check is ``hasattr`` and the call is made on ``bus`` directly.
    Binding the attribute first β€” ``reader = getattr(bus, ..., None)`` then
    ``callable(reader)`` β€” narrows it to a bare ``(...) -> object``, which makes
    the ``dict()`` around the result untypeable; going through ``bus`` keeps the
    declared signature. ``hasattr`` also beats catching ``AttributeError``, which
    would swallow one raised *inside* a real implementation.

    A bus-shaped stand-in that predates the accessor is the case being tolerated
    (the tests use one). Any other failure to read metadata degrades to "not a
    publish job" rather than turning a stop request into an exception, and says
    so in the log.
    """
    if not hasattr(bus, "current_job_metadata"):
        return {}
    try:
        return dict(bus.current_job_metadata(session_id) or {})
    except Exception:
        logger.warning(
            "stop_subagent: could not read running-job metadata for %s",
            session_id, exc_info=True,
        )
        return {}


@tool
async def stop_subagent(agent_name: str, force: bool = False) -> str:
    """Ask a running sub-agent to stop exploring as soon as possible.

    Use this when a sub-agent is clearly going off-track, looping on the same
    searches, no longer needed, or burning budget. The sub-agent is told to
    stop and MAY submit a brief report if it already has valuable findings β€”
    otherwise it just stops. The sub-agent is NOT destroyed: you can assign it
    new work later. Pick up whatever it returns with ``collect_reports``.

    A sub-agent running a PUBLISH task is refused unless ``force`` is set: that
    task writes the run's deliverable, and stopping it throws the deliverable
    away.

    Args:
        agent_name: Name of the sub-agent to stop (the name you created /
            assigned it with).
        force: Stop even a publish task. Only when the publisher is genuinely
            wedged and you accept losing what it was writing.

    Returns:
        A short status line describing what happened.
    """
    scope = get_current_execution_scope()
    if scope is None:
        return (
            "Error: stop_subagent can only be called inside an active "
            "ReAct execution."
        )
    name = (agent_name or "").strip()
    if not name:
        return "Error: `agent_name` was empty."

    bus = registry.get(AgentBus)
    task_id = resolve_bus_task_id(scope)
    session_id = f"{task_id}::{name}"
    session = bus.get_session(session_id)
    if session is None:
        return f"Error: sub-agent {name!r} not found (was it created?)."
    if session.current_job_id is None:
        return (
            f"{name} has no task running right now β€” nothing to stop. "
            "(It may have already finished; call collect_reports.)"
        )

    # A publish task is the one job whose whole value is the file it writes, and
    # ``StopSignalObserver`` stops by POPPING the LLM turn it just produced β€”
    # so a stop landing between "decided to write" and "wrote" discards the
    # deliverable and leaves nothing behind. Seen for real: a coordinator
    # dispatched a publish task and stopped it ~5 s later, four times running,
    # burning its publisher's whole task budget while its finished answer never
    # reached /outputs. Publish tasks are short by construction β€” waiting is
    # almost always right, so make the override explicit.
    if force is not True and _current_metadata(bus, session_id).get("can_publish") is True:
        return (
            f"Refusing to stop {name}: it is running the PUBLISH task, which "
            "writes the final deliverable, and stopping it now would throw "
            "that write away. Publish tasks are short β€” call collect_reports "
            "to wait for it. Pass force=true only if the publisher is truly "
            "wedged and you accept losing what it was writing."
        )

    # Non-blocking: just register the signal and return. We do NOT wait for
    # the sub-agent to actually stop β€” it reacts on its own next LLM response.
    # Tag the request with the running job so a stop that outlives its job
    # can't roll back a later task on this (reusable) session.
    get_stop_registry().request_stop(session_id, session.current_job_id or "")
    logger.info("stop_subagent: stop signal queued for %s", session_id)
    return (
        f"Stop signal sent to {name}; it will stop exploring shortly and "
        "may submit a brief report if it has valuable findings."
    )


__all__ = ["stop_subagent"]