diff --git a/strix/agents/StrixAgent/system_prompt.jinja b/strix/agents/StrixAgent/system_prompt.jinja index bde3157..bcc1359 100644 --- a/strix/agents/StrixAgent/system_prompt.jinja +++ b/strix/agents/StrixAgent/system_prompt.jinja @@ -314,13 +314,29 @@ CRITICAL RULES: 4. Use ONLY the exact format shown above. NEVER use JSON/YAML/INI or any other syntax for tools or parameters. 5. When sending ANY multi-line content in tool parameters, use real newlines (actual line breaks). Do NOT emit literal "\n" sequences. Literal "\n" instead of real line breaks will cause tools to fail. 6. Tool names must match exactly the tool "name" defined (no module prefixes, dots, or variants). - - Correct: ... - - Incorrect: ... - - Incorrect: ... - - Incorrect: {"think": {...}} 7. Parameters must use value exactly. Do NOT pass parameters as JSON or key:value lines. Do NOT add quotes/braces around values. 8. Do NOT wrap tool calls in markdown/code fences or add any text before or after the tool block. +CORRECT format — use this EXACTLY: + +value + + +WRONG formats — NEVER use these: +- value +- ... +- ... +- {"tool_name": {"param_name": "value"}} +- ```...``` + +Do NOT emit any extra XML tags in your output. In particular: +- NO ... or ... blocks +- NO ... or ... blocks +- NO ... or ... wrappers +If you need to reason, use the think tool. Your raw output must contain ONLY the tool call — no surrounding XML tags. + +Notice: use NOT , use NOT , use NOT . + Example (agent creation tool): Perform targeted XSS testing on the search endpoint diff --git a/strix/interface/streaming_parser.py b/strix/interface/streaming_parser.py index 95e9523..2ea69fa 100644 --- a/strix/interface/streaming_parser.py +++ b/strix/interface/streaming_parser.py @@ -3,8 +3,11 @@ import re from dataclasses import dataclass from typing import Literal +from strix.llm.utils import normalize_tool_format + _FUNCTION_TAG_PREFIX = "]+)>") _FUNC_END_PATTERN = re.compile(r"") @@ -21,9 +24,8 @@ def _get_safe_content(content: str) -> tuple[str, str]: return content, "" suffix = content[last_lt:] - target = _FUNCTION_TAG_PREFIX # " list[StreamSegment]: if not content: return [] + content = normalize_tool_format(content) + segments: list[StreamSegment] = [] func_matches = list(_FUNC_PATTERN.finditer(content)) diff --git a/strix/llm/llm.py b/strix/llm/llm.py index 50501aa..0cace0e 100644 --- a/strix/llm/llm.py +++ b/strix/llm/llm.py @@ -14,6 +14,7 @@ from strix.llm.memory_compressor import MemoryCompressor from strix.llm.utils import ( _truncate_to_first_function, fix_incomplete_tool_call, + normalize_tool_format, parse_tool_invocations, ) from strix.skills import load_skills @@ -143,10 +144,12 @@ class LLM: delta = self._get_chunk_content(chunk) if delta: accumulated += delta - if "" in accumulated: - accumulated = accumulated[ - : accumulated.find("") + len("") - ] + if "" in accumulated or "" in accumulated: + for end_tag in ("", ""): + pos = accumulated.find(end_tag) + if pos != -1: + accumulated = accumulated[: pos + len(end_tag)] + break yield LLMResponse(content=accumulated) done_streaming = 1 continue @@ -155,6 +158,7 @@ class LLM: if chunks: self._update_usage_stats(stream_chunk_builder(chunks)) + accumulated = normalize_tool_format(accumulated) accumulated = fix_incomplete_tool_call(_truncate_to_first_function(accumulated)) yield LLMResponse( content=accumulated, diff --git a/strix/llm/utils.py b/strix/llm/utils.py index bef04ce..56caa34 100644 --- a/strix/llm/utils.py +++ b/strix/llm/utils.py @@ -3,6 +3,29 @@ import re from typing import Any +_INVOKE_OPEN = re.compile(r'') +_PARAM_NAME_ATTR = re.compile(r'') +_FUNCTION_CALLS_TAG = re.compile(r"") + + +def normalize_tool_format(content: str) -> str: + """Convert alternative tool-call XML format to the expected one. + + Handles: + ... → stripped + + + → + """ + if "", content) + content = _PARAM_NAME_ATTR.sub(r"", content) + return content.replace("", "") + + STRIX_MODEL_MAP: dict[str, str] = { "claude-sonnet-4.6": "anthropic/claude-sonnet-4-6", "claude-opus-4.6": "anthropic/claude-opus-4-6", @@ -41,7 +64,9 @@ def _truncate_to_first_function(content: str) -> str: if not content: return content - function_starts = [match.start() for match in re.finditer(r"= 2: second_function_start = function_starts[1] @@ -52,6 +77,7 @@ def _truncate_to_first_function(content: str) -> str: def parse_tool_invocations(content: str) -> list[dict[str, Any]] | None: + content = normalize_tool_format(content) content = fix_incomplete_tool_call(content) tool_invocations: list[dict[str, Any]] = [] @@ -81,12 +107,14 @@ def parse_tool_invocations(content: str) -> list[dict[str, Any]] | None: def fix_incomplete_tool_call(content: str) -> str: - """Fix incomplete tool calls by adding missing tag.""" - if ( - "" not in content - ): + """Fix incomplete tool calls by adding missing closing tag. + + Handles both ```` and ```` formats. + """ + has_open = "" in content + if has_open and count_open == 1 and not has_close: content = content.rstrip() content = content + "function>" if content.endswith("" return content @@ -107,6 +135,7 @@ def clean_content(content: str) -> str: if not content: return "" + content = normalize_tool_format(content) content = fix_incomplete_tool_call(content) tool_pattern = r"]+>.*?"