mirror of
https://github.com/usestrix/strix.git
synced 2026-10-26 06:02:30 +00:00
feat(agents): soft wrap-up nudge for sub-agents whose skills set wrap_up_after_turns
This commit is contained in:
parent
913289d657
commit
d5953670d9
3 changed files with 44 additions and 0 deletions
|
|
@ -24,6 +24,7 @@ from openai import (
|
|||
|
||||
from strix.config import codex
|
||||
from strix.core.hooks import (
|
||||
WRAP_UP_AFTER_TURNS_KEY,
|
||||
BudgetExceededError,
|
||||
BudgetPausedError,
|
||||
SubagentBudgetReservedError,
|
||||
|
|
@ -39,6 +40,7 @@ from strix.core.sessions import (
|
|||
)
|
||||
from strix.llm import request_log
|
||||
from strix.llm.compaction import is_context_overflow, maybe_compact
|
||||
from strix.skills import skill_wrap_up_after_turns
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -434,6 +436,7 @@ async def spawn_child_agent(
|
|||
task=task,
|
||||
parent_history=parent_history,
|
||||
),
|
||||
wrap_up_after_turns=skill_wrap_up_after_turns(skills),
|
||||
event_sink=event_sink,
|
||||
hooks=hooks,
|
||||
)
|
||||
|
|
@ -512,6 +515,7 @@ async def respawn_subagents(
|
|||
task=str(md.get("task", "")),
|
||||
initial_input=[],
|
||||
start_parked=start_parked,
|
||||
wrap_up_after_turns=skill_wrap_up_after_turns(child_skills),
|
||||
event_sink=event_sink,
|
||||
hooks=hooks,
|
||||
)
|
||||
|
|
@ -1163,6 +1167,7 @@ async def _start_child_runner(
|
|||
task: str,
|
||||
initial_input: Any,
|
||||
start_parked: bool = False,
|
||||
wrap_up_after_turns: int | None = None,
|
||||
event_sink: StreamEventSink | None = None,
|
||||
hooks: RunHooks[dict[str, Any]] | None = None,
|
||||
) -> None:
|
||||
|
|
@ -1174,6 +1179,7 @@ async def _start_child_runner(
|
|||
child_ctx["agent_id"] = child_id
|
||||
child_ctx["parent_id"] = parent_id
|
||||
child_ctx["task"] = task
|
||||
child_ctx[WRAP_UP_AFTER_TURNS_KEY] = wrap_up_after_turns
|
||||
|
||||
async def _child_loop() -> None:
|
||||
# A budget stop is a clean scan-wide shutdown, not a child failure: the
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
LLM_TURN_KEY = "llm_turn"
|
||||
# Set on a sub-agent's context from its skills (see ``skill_wrap_up_after_turns``).
|
||||
WRAP_UP_AFTER_TURNS_KEY = "wrap_up_after_turns"
|
||||
_WRAP_UP_REPEAT_TURNS = 5
|
||||
|
||||
# ``BudgetPolicy`` decides what happens when the accumulated LLM cost reaches
|
||||
# ``max_budget_usd``.
|
||||
|
|
@ -35,6 +38,7 @@ LLM_TURN_KEY = "llm_turn"
|
|||
# same call when the operator raises the limit or resumes.
|
||||
__all__ = [
|
||||
"LLM_TURN_KEY",
|
||||
"WRAP_UP_AFTER_TURNS_KEY",
|
||||
"BudgetExceededError",
|
||||
"BudgetPausedError",
|
||||
"BudgetPolicy",
|
||||
|
|
@ -207,6 +211,7 @@ class ReportUsageHooks(RunHooks[dict[str, Any]]):
|
|||
context.context[LLM_TURN_KEY] = int(context.context.get(LLM_TURN_KEY, 0)) + 1
|
||||
try:
|
||||
self._maybe_warn_turns(context, input_items)
|
||||
self._maybe_nudge_wrap_up(context, input_items)
|
||||
self._maybe_warn_budget(context, input_items)
|
||||
except Exception:
|
||||
logger.exception("budget/turn warning injection failed")
|
||||
|
|
@ -270,6 +275,26 @@ class ReportUsageHooks(RunHooks[dict[str, Any]]):
|
|||
)
|
||||
input_items.append({"role": "user", "content": content})
|
||||
|
||||
def _maybe_nudge_wrap_up(
|
||||
self,
|
||||
context: RunContextWrapper[dict[str, Any]],
|
||||
input_items: list[TResponseInputItem],
|
||||
) -> None:
|
||||
"""A soft nudge, never a stop: at the turn a skill asks for, then every few turns."""
|
||||
after = context.context.get(WRAP_UP_AFTER_TURNS_KEY)
|
||||
requests = getattr(getattr(context, "usage", None), "requests", None)
|
||||
if not isinstance(after, int) or not isinstance(requests, int):
|
||||
return
|
||||
turns_used = requests + 1
|
||||
if turns_used < after or (turns_used - after) % _WRAP_UP_REPEAT_TURNS:
|
||||
return
|
||||
content = (
|
||||
f"[NOTICE] You have used {turns_used} turns. Wrap up soon: take only the steps "
|
||||
"you still need to answer your task, then call agent_finish with what you have, "
|
||||
"listing anything unresolved in open_items instead of tracing it further."
|
||||
)
|
||||
input_items.append({"role": "user", "content": content})
|
||||
|
||||
def _maybe_warn_budget(
|
||||
self,
|
||||
context: RunContextWrapper[dict[str, Any]],
|
||||
|
|
|
|||
|
|
@ -270,6 +270,19 @@ def _candidate_skill_files(skill_name: str) -> list[Path]:
|
|||
return _bare_skill_files(skill_name)
|
||||
|
||||
|
||||
def skill_wrap_up_after_turns(skill_names: list[str]) -> int | None:
|
||||
"""Smallest ``wrap_up_after_turns`` set in these skills' frontmatter, if any."""
|
||||
turns = []
|
||||
for skill_name in skill_names:
|
||||
candidates = _candidate_skill_files(skill_name)
|
||||
if len(candidates) != 1:
|
||||
continue
|
||||
value = _read_skill_metadata(candidates[0]).get("wrap_up_after_turns", "")
|
||||
if value.isdigit() and int(value) > 0:
|
||||
turns.append(int(value))
|
||||
return min(turns, default=None)
|
||||
|
||||
|
||||
def load_skills(skill_names: list[str], *, supports_images: bool = True) -> dict[str, str]:
|
||||
"""Load skill markdown bodies (frontmatter stripped) by name.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue