mirror of
https://github.com/usestrix/strix.git
synced 2026-11-01 07:02:32 +00:00
feat(core): opt-in parallel tool calls via STRIX_PARALLEL_TOOL_CALLS
This commit is contained in:
parent
f46bed6740
commit
dca9482c82
13 changed files with 48 additions and 4 deletions
|
|
@ -717,6 +717,7 @@ def build_strix_agent(
|
|||
is_whitebox: bool = False,
|
||||
is_diff_scoped: bool = False,
|
||||
interactive: bool = False,
|
||||
parallel_tool_calls: bool = False,
|
||||
chat_completions_tools: bool = False,
|
||||
strict_tool_schemas: bool = True,
|
||||
system_prompt_context: dict[str, Any] | None = None,
|
||||
|
|
@ -751,6 +752,7 @@ def build_strix_agent(
|
|||
is_root=is_root,
|
||||
is_diff_scoped=is_diff_scoped,
|
||||
interactive=interactive,
|
||||
parallel_tool_calls=parallel_tool_calls,
|
||||
system_prompt_context=system_prompt_context,
|
||||
supports_images=supports_images,
|
||||
report_tool=report_tool.name,
|
||||
|
|
@ -813,6 +815,7 @@ def make_child_factory(
|
|||
is_whitebox: bool = False,
|
||||
is_diff_scoped: bool = False,
|
||||
interactive: bool = False,
|
||||
parallel_tool_calls: bool = False,
|
||||
chat_completions_tools: bool = False,
|
||||
strict_tool_schemas: bool = True,
|
||||
system_prompt_context: dict[str, Any] | None = None,
|
||||
|
|
@ -835,6 +838,7 @@ def make_child_factory(
|
|||
is_whitebox=is_whitebox,
|
||||
is_diff_scoped=is_diff_scoped,
|
||||
interactive=interactive,
|
||||
parallel_tool_calls=parallel_tool_calls,
|
||||
chat_completions_tools=chat_completions_tools,
|
||||
strict_tool_schemas=strict_tool_schemas,
|
||||
system_prompt_context=system_prompt_context,
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ def render_system_prompt(
|
|||
is_root: bool = False,
|
||||
is_diff_scoped: bool = False,
|
||||
interactive: bool = False,
|
||||
parallel_tool_calls: bool = False,
|
||||
system_prompt_context: dict[str, Any] | None = None,
|
||||
include_scope: bool = True,
|
||||
supports_images: bool = True,
|
||||
|
|
@ -131,6 +132,7 @@ def render_system_prompt(
|
|||
requested_skill_names=[name for name in skill_content if name not in shared],
|
||||
available_skills=get_available_skills(),
|
||||
interactive=interactive,
|
||||
parallel_tool_calls=parallel_tool_calls,
|
||||
is_root=is_root,
|
||||
system_prompt_context=system_prompt_context or {},
|
||||
include_scope=include_scope,
|
||||
|
|
|
|||
|
|
@ -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 {% if parallel_tool_calls %}a tool call (independent calls can go in the same turn){% else %}exactly one tool call{% endif %} — 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,9 @@ 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.
|
||||
{%- if parallel_tool_calls %}
|
||||
- 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 %}
|
||||
{% endif %}
|
||||
</communication_rules>
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,10 @@ class LlmSettings(BaseSettings):
|
|||
default=False,
|
||||
alias="STRIX_FORCE_REQUIRED_TOOL_CHOICE",
|
||||
)
|
||||
parallel_tool_calls: bool = Field(
|
||||
default=False,
|
||||
alias="STRIX_PARALLEL_TOOL_CALLS",
|
||||
)
|
||||
prompt_cache: bool = Field(
|
||||
default=True,
|
||||
alias="STRIX_PROMPT_CACHE",
|
||||
|
|
|
|||
|
|
@ -1009,7 +1009,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. "
|
||||
|
|
@ -1021,7 +1021,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. "
|
||||
|
|
|
|||
|
|
@ -247,6 +247,7 @@ def make_model_settings(
|
|||
*,
|
||||
model_name: str,
|
||||
force_required_tool_choice: bool = False,
|
||||
parallel_tool_calls: bool = False,
|
||||
request_timeout: float | None = None,
|
||||
prompt_cache: bool = True,
|
||||
extra_headers: dict[str, str] | None = None,
|
||||
|
|
@ -255,7 +256,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=parallel_tool_calls if has_tools else None,
|
||||
retry=DEFAULT_MODEL_RETRY,
|
||||
include_usage=True,
|
||||
extra_args=request_timeout_extra_args(request_timeout),
|
||||
|
|
|
|||
|
|
@ -176,6 +176,7 @@ def _compose_root_instructions_override(
|
|||
is_whitebox: bool,
|
||||
is_diff_scoped: bool,
|
||||
interactive: bool,
|
||||
parallel_tool_calls: bool,
|
||||
system_prompt_context: dict[str, Any],
|
||||
supports_images: bool,
|
||||
report_tool: str,
|
||||
|
|
@ -190,6 +191,7 @@ def _compose_root_instructions_override(
|
|||
is_root=True,
|
||||
is_diff_scoped=is_diff_scoped,
|
||||
interactive=interactive,
|
||||
parallel_tool_calls=parallel_tool_calls,
|
||||
system_prompt_context=system_prompt_context,
|
||||
include_scope=False,
|
||||
supports_images=supports_images,
|
||||
|
|
@ -413,6 +415,7 @@ async def run_strix_scan(
|
|||
settings.llm.reasoning_effort,
|
||||
model_name=resolved_model,
|
||||
force_required_tool_choice=settings.llm.force_required_tool_choice,
|
||||
parallel_tool_calls=settings.llm.parallel_tool_calls,
|
||||
request_timeout=settings.llm.timeout,
|
||||
prompt_cache=settings.llm.prompt_cache,
|
||||
extra_headers=settings.llm.extra_headers,
|
||||
|
|
@ -508,6 +511,7 @@ async def run_strix_scan(
|
|||
is_whitebox=is_whitebox,
|
||||
is_diff_scoped=is_diff_scoped,
|
||||
interactive=interactive,
|
||||
parallel_tool_calls=settings.llm.parallel_tool_calls,
|
||||
system_prompt_context=root_context,
|
||||
supports_images=supports_images,
|
||||
report_tool=report_tool.name,
|
||||
|
|
@ -521,6 +525,7 @@ async def run_strix_scan(
|
|||
is_whitebox=is_whitebox,
|
||||
is_diff_scoped=is_diff_scoped,
|
||||
interactive=interactive,
|
||||
parallel_tool_calls=settings.llm.parallel_tool_calls,
|
||||
chat_completions_tools=chat_completions_tools,
|
||||
strict_tool_schemas=strict_tool_schemas,
|
||||
system_prompt_context=root_context,
|
||||
|
|
@ -544,6 +549,7 @@ async def run_strix_scan(
|
|||
is_whitebox=is_whitebox,
|
||||
is_diff_scoped=is_diff_scoped,
|
||||
interactive=interactive,
|
||||
parallel_tool_calls=settings.llm.parallel_tool_calls,
|
||||
chat_completions_tools=chat_completions_tools,
|
||||
strict_tool_schemas=strict_tool_schemas,
|
||||
system_prompt_context=scope_context,
|
||||
|
|
|
|||
|
|
@ -314,6 +314,12 @@ 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_when_requested() -> None:
|
||||
settings = make_model_settings("none", model_name="gpt-4o", parallel_tool_calls=True)
|
||||
|
||||
assert settings.parallel_tool_calls is True
|
||||
|
||||
|
||||
def test_make_model_settings_omits_parallel_tool_calls_without_tools() -> None:
|
||||
settings = make_model_settings("none", model_name="gpt-4o", has_tools=False)
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ def _wire_runner(monkeypatch: pytest.MonkeyPatch, tmp_path: Any) -> None:
|
|||
model="openai/gpt-4o",
|
||||
reasoning_effort="high",
|
||||
force_required_tool_choice=False,
|
||||
parallel_tool_calls=False,
|
||||
timeout=300,
|
||||
prompt_cache=True,
|
||||
extra_headers=None,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ def _settings() -> Any:
|
|||
model="openai/gpt-4o",
|
||||
reasoning_effort="high",
|
||||
force_required_tool_choice=False,
|
||||
parallel_tool_calls=False,
|
||||
timeout=300,
|
||||
prompt_cache=True,
|
||||
extra_headers=None,
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ async def test_persistent_rate_limit_stops_gracefully(
|
|||
model="openai/gpt-4o",
|
||||
reasoning_effort="high",
|
||||
force_required_tool_choice=False,
|
||||
parallel_tool_calls=False,
|
||||
timeout=300,
|
||||
prompt_cache=True,
|
||||
extra_headers=None,
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ def _patch_engine_scaffold(
|
|||
model="openai/gpt-4o",
|
||||
reasoning_effort="high",
|
||||
force_required_tool_choice=False,
|
||||
parallel_tool_calls=False,
|
||||
timeout=300,
|
||||
prompt_cache=True,
|
||||
extra_headers=None,
|
||||
|
|
@ -285,6 +286,19 @@ async def test_unknown_tool_calls_are_returned_to_the_model(
|
|||
assert captured["run_config"].tool_not_found_behavior == "return_error_to_model"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("interactive", [False, True])
|
||||
def test_parallel_tool_calls_prompt_is_opt_in(interactive: bool) -> None:
|
||||
default = render_system_prompt(include_scope=False, interactive=interactive)
|
||||
parallel = render_system_prompt(
|
||||
include_scope=False, interactive=interactive, parallel_tool_calls=True
|
||||
)
|
||||
|
||||
assert "Read-only calls can go together" not in default
|
||||
assert ("Read-only calls can go together" in parallel) is not interactive
|
||||
assert ("exactly one tool call" in default) is interactive
|
||||
assert "exactly one tool call" not in parallel
|
||||
|
||||
|
||||
def test_scope_is_rendered_once_at_the_end_of_the_prompt() -> None:
|
||||
prompt = render_system_prompt(
|
||||
system_prompt_context={
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ def _settings() -> Any:
|
|||
model="openai/gpt-4o",
|
||||
reasoning_effort="high",
|
||||
force_required_tool_choice=False,
|
||||
parallel_tool_calls=False,
|
||||
timeout=300,
|
||||
prompt_cache=True,
|
||||
extra_headers=None,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue