feat(agent): structured task format and workflow improvements

- Replace free-form task description with structured XML format
  (<scan_task><targets><mode>) in StrixAgent for clearer LLM parsing
- Replace verbose <inter_agent_message> with compact <agent_message>
  format to reduce token overhead in inter-agent communication
- Add corrective message when agents respond with plain text instead
  of tool calls, enforcing tool-call-only behavior
- Simplify thinking_blocks type annotation in AgentState
- Add <agent_message> pattern to clean_content() for hidden XML cleanup
This commit is contained in:
0xhis 2026-03-21 00:52:31 -07:00 committed by ST-2
parent 15c95718e6
commit 3a8d319f7f
4 changed files with 62 additions and 47 deletions

View file

@ -97,35 +97,48 @@ class StrixAgent(BaseAgent):
elif target_type == "ip_address":
ip_addresses.append(details["target_ip"])
task_parts = []
target_lines = []
if repositories:
task_parts.append("\n\nRepositories:")
for repo in repositories:
if repo["workspace_path"]:
task_parts.append(f"- {repo['url']} (available at: {repo['workspace_path']})")
target_lines.append(
f' <target type="repository">{repo["url"]} (code at: {repo["workspace_path"]})</target>'
)
else:
task_parts.append(f"- {repo['url']}")
target_lines.append(f' <target type="repository">{repo["url"]}</target>')
if local_code:
task_parts.append("\n\nLocal Codebases:")
task_parts.extend(
f"- {code['path']} (available at: {code['workspace_path']})" for code in local_code
)
for code in local_code:
target_lines.append(
f' <target type="local_code">{code["path"]} (code at: {code["workspace_path"]})</target>'
)
if urls:
task_parts.append("\n\nURLs:")
task_parts.extend(f"- {url}" for url in urls)
for url in urls:
target_lines.append(f' <target type="url">{url}</target>')
if ip_addresses:
task_parts.append("\n\nIP Addresses:")
task_parts.extend(f"- {ip}" for ip in ip_addresses)
for ip in ip_addresses:
target_lines.append(f' <target type="ip">{ip}</target>')
targets_block = "\n".join(target_lines)
has_code = bool(repositories or local_code)
has_urls = bool(urls or ip_addresses)
if has_code and has_urls:
mode = "COMBINED MODE (code + deployed target)"
elif has_code:
mode = "WHITE-BOX (source code provided)"
else:
mode = "BLACK-BOX (URL/domain targets)"
diff_scope_section = ""
if diff_scope.get("active"):
task_parts.append("\n\nScope Constraints:")
task_parts.append(
"- Pull request diff-scope mode is active. Prioritize changed files "
"and use other files only for context."
scope_lines = ["<diff_scope>"]
scope_lines.append(
" <note>Pull request diff-scope mode is active. Prioritize changed files "
"and use other files only for context.</note>"
)
for repo_scope in diff_scope.get("repos", []):
repo_label = (
@ -135,15 +148,23 @@ class StrixAgent(BaseAgent):
)
changed_count = repo_scope.get("analyzable_files_count", 0)
deleted_count = repo_scope.get("deleted_files_count", 0)
task_parts.append(
f"- {repo_label}: {changed_count} changed file(s) in primary scope"
scope_lines.append(
f' <repo name="{repo_label}">{changed_count} changed file(s) in primary scope</repo>'
)
if deleted_count:
task_parts.append(
f"- {repo_label}: {deleted_count} deleted file(s) are context-only"
scope_lines.append(
f' <repo name="{repo_label}">{deleted_count} deleted file(s) are context-only</repo>'
)
scope_lines.append("</diff_scope>")
diff_scope_section = "\n" + "\n".join(scope_lines) + "\n"
task_description = " ".join(task_parts)
task_description = (
f"<scan_task>\n"
f"<targets>\n{targets_block}\n</targets>\n"
f"<mode>{mode}</mode>\n"
f"<action>Begin security assessment NOW. Your first tool call must be create_agent to spawn context-gathering subagents for the targets listed above. Do NOT call wait_for_message — the targets are already specified.</action>\n"
f"{diff_scope_section}</scan_task>"
)
if user_instructions:
task_description += f"\n\nSpecial instructions: {user_instructions}"

View file

@ -411,6 +411,17 @@ class BaseAgent(metaclass=AgentMeta):
if actions:
return await self._execute_actions(actions, tracer)
corrective_message = (
"You responded with plain text instead of a tool call. "
"While the agent loop is running, EVERY response MUST be a tool call. "
"Do NOT send plain text messages. Act via tools:\n"
"- Use the think tool to reason through problems\n"
"- Use create_agent to spawn subagents for testing\n"
"- Use terminal_execute to run commands\n"
"- Use wait_for_message ONLY when waiting for subagent results\n"
"Review your task and take action now."
)
self.state.add_message("user", corrective_message)
return None
async def _execute_actions(self, actions: list[Any], tracer: Optional["Tracer"]) -> bool:
@ -485,33 +496,17 @@ class BaseAgent(metaclass=AgentMeta):
sender_name = "User"
state.add_message("user", message.get("content", ""))
else:
sender_name = sender_id or "Unknown"
if sender_id and sender_id in _agent_graph.get("nodes", {}):
sender_name = _agent_graph["nodes"][sender_id]["name"]
message_content = f"""<inter_agent_message>
<delivery_notice>
<important>You have received a message from another agent. You should acknowledge
this message and respond appropriately based on its content. However, DO NOT echo
back or repeat the entire message structure in your response. Simply process the
content and respond naturally as/if needed.</important>
</delivery_notice>
<sender>
<agent_name>{sender_name}</agent_name>
<agent_id>{sender_id}</agent_id>
</sender>
<message_metadata>
<type>{message.get("message_type", "information")}</type>
<priority>{message.get("priority", "normal")}</priority>
<timestamp>{message.get("timestamp", "")}</timestamp>
</message_metadata>
<content>
message_content = f"""<agent_message
from="{sender_name}"
id="{sender_id}"
type="{message.get("message_type", "information")}"
priority="{message.get("priority", "normal")}">
{message.get("content", "")}
</content>
<delivery_info>
<note>This message was delivered during your task execution.
Please acknowledge and respond if needed.</note>
</delivery_info>
</inter_agent_message>"""
</agent_message>"""
state.add_message("user", message_content.strip())
message["read"] = True

View file

@ -44,9 +44,7 @@ class AgentState(BaseModel):
self.iteration += 1
self.last_updated = datetime.now(UTC).isoformat()
def add_message(
self, role: str, content: Any, thinking_blocks: list[dict[str, Any]] | None = None
) -> None:
def add_message(self, role: str, content: Any, thinking_blocks: list | None = None) -> None:
message = {"role": role, "content": content}
if thinking_blocks:
message["thinking_blocks"] = thinking_blocks

View file

@ -150,6 +150,7 @@ def clean_content(content: str) -> str:
hidden_xml_patterns = [
r"<inter_agent_message>.*?</inter_agent_message>",
r"<agent_message\b[^>]*>.*?</agent_message>",
r"<agent_completion_report>.*?</agent_completion_report>",
]
for pattern in hidden_xml_patterns: