chore(orchestration): drop XML wrappers + close remaining audit gaps
Final pass after re-audit. Three sub-specs landed:
**XML simplification** — the legacy XML envelopes were prompt-engineering
ceremony, not parser primitives (the SDK uses native tool-calling). Drop
the verbose wrappers in favor of one-liner labeled headers. Side benefit:
fixes the unescaped-content XML-injection bug the audit caught (peer
content containing ``</content>`` no longer breaks the wrapper).
- ``_format_inter_agent_message``: ``<inter_agent_message><sender>...
<content>...`` 9-line XML → ``[Message from {name} ({id}) | type=... |
priority=...]\n{content}``.
- ``_render_completion_report``: ``<agent_completion_report><agent_info>
...<results>...`` XML → human-readable structured text with section
headers and bulleted lists.
- ``inherited_context``: ``<inherited_context_from_parent>...`` →
``== Inherited context from parent (background only) ==``.
**MG1: TUI stop-agent uses graceful cancel.** ``tui.py`` was calling
``bus.cancel_descendants`` (hard, ``task.cancel()`` mid-stream) for the
stop-agent button. Switched to ``bus.cancel_descendants_graceful``, which
uses ``RunResultStreaming.cancel(mode="after_turn")`` to let each agent
finish its current turn (and save to session) before honoring the cancel.
The hard path remains in ``entry.py`` for KeyboardInterrupt where
graceful isn't possible.
**MG2: Document hook lock-free stats mutation.** Added a comment in
``hooks.on_llm_start`` explaining why ``warned_85`` / ``warned_final``
are mutated lock-free: SDK serializes ``on_llm_start`` per agent, so this
hook is the sole writer to those keys; ``record_usage`` only writes
disjoint keys (in/out/cached/calls).
**AG3: Auto-load ``coordination/root_agent`` skill for the root.**
Legacy auto-loaded the orchestration-guidance skill for root agents
only. Threaded ``is_root`` through ``render_system_prompt`` →
``_resolve_skills``; root agents now get the skill, children don't.
Skipped (per user direction): whitebox-wiki integration (CG2-4) — the
auto-injection / auto-update of the shared repo wiki was a pre-migration
feature; user opted not to restore it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -151,6 +151,7 @@ def build_strix_agent(
|
||||
skills=skills,
|
||||
scan_mode=scan_mode,
|
||||
is_whitebox=is_whitebox,
|
||||
is_root=is_root,
|
||||
interactive=interactive,
|
||||
system_prompt_context=system_prompt_context,
|
||||
)
|
||||
|
||||
+10
-1
@@ -27,6 +27,7 @@ def _resolve_skills(
|
||||
requested: list[str] | None,
|
||||
scan_mode: str = "deep",
|
||||
is_whitebox: bool = False,
|
||||
is_root: bool = False,
|
||||
) -> list[str]:
|
||||
"""Build the deduped, ordered skills list for the prompt render.
|
||||
|
||||
@@ -36,11 +37,15 @@ def _resolve_skills(
|
||||
2. ``scan_modes/<mode>`` (always).
|
||||
3. ``tooling/agent_browser`` (always — every agent has shell + the
|
||||
agent-browser CLI).
|
||||
4. Whitebox-specific skills if applicable.
|
||||
4. ``coordination/root_agent`` for the root agent only — orchestration
|
||||
guidance for delegating to specialist subagents.
|
||||
5. Whitebox-specific skills if applicable.
|
||||
"""
|
||||
ordered: list[str] = list(requested or [])
|
||||
ordered.append(f"scan_modes/{scan_mode}")
|
||||
ordered.append("tooling/agent_browser")
|
||||
if is_root:
|
||||
ordered.append("coordination/root_agent")
|
||||
if is_whitebox:
|
||||
ordered.append("coordination/source_aware_whitebox")
|
||||
ordered.append("custom/source_aware_sast")
|
||||
@@ -59,6 +64,7 @@ def render_system_prompt(
|
||||
skills: list[str] | None = None,
|
||||
scan_mode: str = "deep",
|
||||
is_whitebox: bool = False,
|
||||
is_root: bool = False,
|
||||
interactive: bool = False,
|
||||
system_prompt_context: dict[str, Any] | None = None,
|
||||
) -> str:
|
||||
@@ -70,6 +76,8 @@ def render_system_prompt(
|
||||
skill.
|
||||
is_whitebox: When True, the source-aware whitebox skill stack
|
||||
is loaded too.
|
||||
is_root: When True, ``coordination/root_agent`` orchestration
|
||||
guidance is auto-loaded.
|
||||
interactive: When True, the prompt renders the interactive-mode
|
||||
communication rules block.
|
||||
system_prompt_context: Free-form dict that the template's
|
||||
@@ -96,6 +104,7 @@ def render_system_prompt(
|
||||
requested=skills,
|
||||
scan_mode=scan_mode,
|
||||
is_whitebox=is_whitebox,
|
||||
is_root=is_root,
|
||||
)
|
||||
skill_content = load_skills(skills_to_load)
|
||||
env.globals["get_skill"] = lambda name: skill_content.get(name, "")
|
||||
|
||||
@@ -1843,11 +1843,16 @@ class StrixTUIApp(App): # type: ignore[misc]
|
||||
return agent_name, False
|
||||
|
||||
def action_confirm_stop_agent(self, agent_id: str) -> None:
|
||||
# Graceful stop: each agent's current turn finishes (and is saved to
|
||||
# session) before the run loop honors the cancel. The interactive
|
||||
# outer loop sees ``stopping`` and exits with status="stopped".
|
||||
# The hard ``cancel_descendants`` path remains for KeyboardInterrupt
|
||||
# in entry.py where graceful isn't possible.
|
||||
if self._scan_loop is None or self._scan_loop.is_closed():
|
||||
logging.warning("No active scan loop; cannot stop agent %s", agent_id)
|
||||
return
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.bus.cancel_descendants(agent_id),
|
||||
self.bus.cancel_descendants_graceful(agent_id),
|
||||
self._scan_loop,
|
||||
)
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ retries — so a single drain per turn doesn't lose messages on retry.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from agents.run_config import CallModelData, ModelInputData
|
||||
@@ -26,11 +25,8 @@ logger = logging.getLogger(__name__)
|
||||
async def inject_messages_filter(data: CallModelData) -> ModelInputData:
|
||||
"""Drain bus inbox and append messages as user-role items before the LLM call.
|
||||
|
||||
Peer-agent messages are wrapped in the legacy ``<inter_agent_message>``
|
||||
XML envelope (sender / metadata / content / delivery_info) so the
|
||||
receiving model gets the same prompt-shape as pre-migration — including
|
||||
the explicit "DO NOT echo back" instruction. Direct user messages
|
||||
(``from="user"``) are passed plain.
|
||||
Peer-agent messages get a one-line labeled header followed by the
|
||||
body. Direct user messages (``from="user"``) are passed plain.
|
||||
|
||||
Any exception inside the filter — malformed message dict, bug in
|
||||
``bus.drain``, etc. — is caught and the original ``data.model_data``
|
||||
@@ -72,31 +68,24 @@ async def inject_messages_filter(data: CallModelData) -> ModelInputData:
|
||||
|
||||
|
||||
def _format_inter_agent_message(bus: AgentMessageBus, msg: dict[str, Any]) -> str:
|
||||
"""Render a peer-agent message in the legacy XML envelope.
|
||||
"""Render a peer-agent message as a labeled header + body.
|
||||
|
||||
The wrapper carries an explicit "do not echo back" instruction that
|
||||
the legacy harness used to keep models from quoting the entire
|
||||
received message dict in their own next turn.
|
||||
Format:
|
||||
[Message from {name} ({id}) | type={type} | priority={priority}]
|
||||
{content}
|
||||
|
||||
Plain text by design — no XML wrapping, no escaping concerns. The
|
||||
label line tells the receiver who sent this and why so it doesn't
|
||||
confuse a peer message with its own work; the rest of the body is
|
||||
delivered as-is.
|
||||
"""
|
||||
sender_id = str(msg.get("from", "unknown"))
|
||||
sender_name = bus.names.get(sender_id, sender_id)
|
||||
msg_type = msg.get("type", "information")
|
||||
priority = msg.get("priority", "normal")
|
||||
timestamp = msg.get("timestamp") or datetime.now(UTC).isoformat()
|
||||
content = str(msg.get("content", ""))
|
||||
return (
|
||||
"<inter_agent_message>\n"
|
||||
" <delivery_notice><important>You have received a message from another "
|
||||
"agent. Acknowledge and respond to the sender if needed; DO NOT echo "
|
||||
"back this entire message block.</important></delivery_notice>\n"
|
||||
f" <sender><agent_name>{sender_name}</agent_name>"
|
||||
f"<agent_id>{sender_id}</agent_id></sender>\n"
|
||||
f" <message_metadata><type>{msg_type}</type>"
|
||||
f"<priority>{priority}</priority>"
|
||||
f"<timestamp>{timestamp}</timestamp></message_metadata>\n"
|
||||
f" <content>{content}</content>\n"
|
||||
" <delivery_info><note>This message was delivered during your task "
|
||||
"execution. Please acknowledge and respond if needed.</note>"
|
||||
"</delivery_info>\n"
|
||||
"</inter_agent_message>"
|
||||
f"[Message from {sender_name} ({sender_id}) "
|
||||
f"| type={msg_type} | priority={priority}]\n"
|
||||
f"{content}"
|
||||
)
|
||||
|
||||
@@ -56,6 +56,13 @@ class StrixOrchestrationHooks(RunHooks[Any]):
|
||||
# warnings fire exactly once per agent lifetime — surviving
|
||||
# ``run_with_continuation`` cycles, mirroring legacy
|
||||
# ``state.max_iterations_warning_sent``.
|
||||
#
|
||||
# The flags are mutated lock-free below. Safe because the SDK
|
||||
# serializes ``on_llm_start`` per agent (one in-flight LLM call
|
||||
# per ``Runner.run`` instance), so this hook is the sole writer
|
||||
# to ``warned_85`` / ``warned_final`` for this agent_id.
|
||||
# ``record_usage`` (which acquires the lock) only writes
|
||||
# ``in`` / ``out`` / ``cached`` / ``calls`` — disjoint keys.
|
||||
if cur >= int(max_turns * 0.85) and not stats.get("warned_85"):
|
||||
stats["warned_85"] = True
|
||||
input_items.append(
|
||||
|
||||
@@ -19,6 +19,7 @@ import asyncio
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
from agents import RunContextWrapper, function_tool
|
||||
@@ -52,26 +53,34 @@ def _render_completion_report(
|
||||
findings: list[str],
|
||||
recommendations: list[str],
|
||||
) -> str:
|
||||
"""Render an ``<agent_completion_report>`` XML payload (legacy parity)."""
|
||||
from datetime import UTC, datetime
|
||||
from html import escape
|
||||
"""Render a child's completion report as plain structured text.
|
||||
|
||||
Goes into the parent's bus inbox; the inject filter prepends a
|
||||
``[Message from ...]`` header on top, so this body just carries the
|
||||
contents. No XML — no escaping concerns, no parser ambiguity.
|
||||
"""
|
||||
status = "SUCCESS" if success else "FAILED"
|
||||
completion_time = datetime.now(UTC).isoformat()
|
||||
findings_xml = "".join(f"<finding>{escape(f)}</finding>" for f in findings)
|
||||
recs_xml = "".join(f"<recommendation>{escape(r)}</recommendation>" for r in recommendations)
|
||||
return (
|
||||
"<agent_completion_report>\n"
|
||||
f" <agent_info><agent_name>{escape(agent_name)}</agent_name>"
|
||||
f"<agent_id>{escape(agent_id)}</agent_id>"
|
||||
f"<task>{escape(task)}</task>"
|
||||
f"<status>{status}</status>"
|
||||
f"<completion_time>{completion_time}</completion_time></agent_info>\n"
|
||||
f" <results><summary>{escape(result_summary)}</summary>"
|
||||
f"<findings>{findings_xml}</findings>"
|
||||
f"<recommendations>{recs_xml}</recommendations></results>\n"
|
||||
"</agent_completion_report>"
|
||||
)
|
||||
|
||||
lines: list[str] = [
|
||||
f"== Completion report from {agent_name} ({agent_id}) ==",
|
||||
f"Status: {status}",
|
||||
f"Time: {completion_time}",
|
||||
]
|
||||
if task:
|
||||
lines.append(f"Task: {task}")
|
||||
lines.append("")
|
||||
lines.append("Summary:")
|
||||
lines.append(result_summary or "(none)")
|
||||
if findings:
|
||||
lines.append("")
|
||||
lines.append("Findings:")
|
||||
lines.extend(f"- {f}" for f in findings)
|
||||
if recommendations:
|
||||
lines.append("")
|
||||
lines.append("Recommendations:")
|
||||
lines.extend(f"- {r}" for r in recommendations)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@function_tool(timeout=30)
|
||||
@@ -456,9 +465,9 @@ async def create_agent(
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"<inherited_context_from_parent>\n"
|
||||
"== Inherited context from parent (background only) ==\n"
|
||||
f"{rendered}\n"
|
||||
"</inherited_context_from_parent>\n"
|
||||
"== End of inherited context ==\n"
|
||||
"Use the above as background only; do not continue the "
|
||||
"parent's work. Your task follows."
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user