mirror of
https://github.com/usestrix/strix.git
synced 2026-11-09 13:31:41 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5953670d9 | ||
|
|
913289d657 | ||
|
|
6369287997 |
No files matched your search
@@ -44,7 +44,7 @@ INTERACTIVE BEHAVIOR:
|
||||
- If you end a turn on plain text and the nudge arrives, your words already reached the user. Do not repeat them: call wait_for_user.
|
||||
- You can narrate while you work — plain text is shown to the user as you go. Narrating is free; wait_for_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_user only when you genuinely need the user.
|
||||
- While actively working on a task, every turn should carry a tool call (independent calls can go in the same turn) — use think to plan, the appropriate tool to act, and wait_for_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, write it and call wait_for_user.
|
||||
{% else %}
|
||||
AUTONOMOUS BEHAVIOR:
|
||||
@@ -55,6 +55,7 @@ AUTONOMOUS BEHAVIOR:
|
||||
- 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 does nothing: it neither ends the run nor yields — it just wastes a turn and forces a retry. The lifecycle tools (``finish_scan`` for root, ``agent_finish`` for subagents) are the ONLY way to terminate, and the report flows through them. If you find yourself wanting to say "Done!" or "Scan complete" without a tool call, call the lifecycle tool instead.
|
||||
- Read-only calls can go together in one turn: when several file reads or searches don't depend on each other's output, issue them as separate tool calls in the same turn rather than one per turn. Anything else — writing files, running commands that change state, sending requests, creating or messaging agents, reporting, or finishing — goes alone in its own turn.
|
||||
{% endif %}
|
||||
</communication_rules>
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -1004,7 +1008,7 @@ async def _append_tool_required_message(
|
||||
message = (
|
||||
"Your previous message ended a turn without a tool call. Plain text never ends "
|
||||
"execution and never hands control to the user: it is shown to the user, and the "
|
||||
"run continues. Continue immediately and call exactly one tool. "
|
||||
"run continues. Continue immediately and call a tool. "
|
||||
"If you have nothing to do until the user replies, call wait_for_user; your "
|
||||
"text already reached them, so do not repeat it. "
|
||||
"If you are blocked waiting for another agent, call wait_for_agents. "
|
||||
@@ -1016,7 +1020,7 @@ async def _append_tool_required_message(
|
||||
message = (
|
||||
"Your previous response ended the autonomous run without a lifecycle tool "
|
||||
"call. That is invalid in non-interactive mode; plain text final answers are "
|
||||
"ignored. Continue immediately and call exactly one tool. "
|
||||
"ignored. Continue immediately and call a tool. "
|
||||
f"If your work is complete, call {finish_tool}. "
|
||||
"If you are blocked waiting for another agent, call wait_for_agents. "
|
||||
"Otherwise use the appropriate execution or planning tool. "
|
||||
@@ -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]],
|
||||
|
||||
@@ -255,7 +255,7 @@ def make_model_settings(
|
||||
) -> ModelSettings:
|
||||
headers = _request_headers(model_name, extra_headers)
|
||||
model_settings = ModelSettings(
|
||||
parallel_tool_calls=False if has_tools else None,
|
||||
parallel_tool_calls=True if has_tools else None,
|
||||
retry=DEFAULT_MODEL_RETRY,
|
||||
include_usage=True,
|
||||
extra_args=request_timeout_extra_args(request_timeout),
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -310,8 +310,8 @@ def test_make_model_settings_forces_required_for_anyllm_routed_openai_model() ->
|
||||
assert settings.tool_choice == "required"
|
||||
|
||||
|
||||
def test_make_model_settings_disables_parallel_tool_calls_by_default() -> None:
|
||||
assert make_model_settings("none", model_name="gpt-4o").parallel_tool_calls is False
|
||||
def test_make_model_settings_enables_parallel_tool_calls_by_default() -> None:
|
||||
assert make_model_settings("none", model_name="gpt-4o").parallel_tool_calls is True
|
||||
|
||||
|
||||
def test_make_model_settings_omits_parallel_tool_calls_without_tools() -> None:
|
||||
|
||||
Reference in new issue
Block a user