fix(prompts): strip JSON fences at render time

Keep fenced examples readable in prompt Markdown while removing only JSON fence markers from the final assembled system prompt. Cover all bundled profiles and preserve non-JSON fences.

Alessandro committed Jul 25, 2026 at 16:01 UTC 9f1e24a2dbc6fdb1f80f4997028ff1f3f9fc2a9e
6 files changed +59 -7
AGENTS.md
+1
@@ -29,6 +29,7 @@
29 - Use Linux paths and commands in examples.
30 - When a live Dockerized Agent Zero target is explicitly named, verify that exact runtime instead of assuming a fixed localhost port.
31 - Message-loop completion flows through a response tool with `break_loop`; plain or malformed Chat Completions text enters repair, and native Responses output text is normalized through the same response-tool path.
32 +- Prompt Markdown may retain fenced JSON examples for readability; final system-prompt rendering removes only their JSON fence markers before model calls and preserves non-JSON fences.
33 - Copy live core-plugin changes back into tracked source under `plugins/`.
34 - Develop new custom plugins under ignored `usr/plugins/`; tracked bundled plugins live under `plugins/`.
35 - Use the framework runtime for backend and plugin-hook verification, not the separate agent execution runtime.
agent.py
+4 -2
@@ -568,8 +568,10 @@ class Agent:
568 "message_loop_prompts_after", self, loop_data=loop_data
569 )
570
571 - # concatenate system prompt
572 - system_text = "\n\n".join(loop_data.system)
571 + # concatenate system prompt and remove JSON fence markers from examples
572 + system_text = files.remove_code_fences(
573 + "\n\n".join(loop_data.system), language="json"
574 + )
575
576 # join protocol and extras
577 protocol = self._build_context_message(
helpers/files.py
+8 -1
@@ -433,7 +433,14 @@ def find_existing_paths_by_pattern(pattern: str):
433 return matches
434
435
436 -def remove_code_fences(text):
436 +def remove_code_fences(text, language: str | None = None):
437 + if language:
438 + pattern = (
439 + rf"(?ims)^[ \t]*(```|~~~)[ \t]*{re.escape(language)}[ \t]*\r?\n"
440 + r"(.*?)^[ \t]*\1[ \t]*\r?$"
441 + )
442 + return re.sub(pattern, lambda match: match.group(2), text)
443 +
444 # Pattern to match code fences with optional language specifier
445 pattern = r"(```|~~~)(.*?\n)(.*?)(\1)"
446
helpers/files.py.dox.md
+2 -2
@@ -33,7 +33,7 @@
33 - `find_file_in_dirs(_filename: str, _directories: list[str])`: This function searches for a filename in a list of directories in order.
34 - `get_unique_filenames_in_dirs(dir_paths: list[str], pattern: str=..., type: Literal['file', 'dir', 'any']=...)`
35 - `find_existing_paths_by_pattern(pattern: str)`
36 -- `remove_code_fences(text)`
36 +- `remove_code_fences(text, language: str | None=...)`: Remove every code fence, or only fences for one language while preserving their contents.
37 - `is_full_json_template(text)`
38 - `write_file(relative_path: str, content: str, encoding: str=...)`
39 - `delete_file(relative_path: str)`
@@ -55,7 +55,7 @@
55
56 ## Key Concepts
57
58 -- Important called helpers/classes observed in the source: `os.path.dirname`, `os.path.abspath`, `find_file_in_dirs`, `is_full_json_template`, `remove_code_fences`, `evaluate_text_conditions`, `replace_placeholders_text`, `process_includes`, `re.compile`, `_process`, `get_abs_path`, `is_probably_binary_bytes`, `replace_value`, `re.sub`, `os.path.normpath`, `FileNotFoundError`, `result.sort`, `glob.glob`, `matches.sort`, `re.fullmatch`.
58 +- Important called helpers/classes observed in the source: `os.path.dirname`, `os.path.abspath`, `find_file_in_dirs`, `is_full_json_template`, `remove_code_fences`, `evaluate_text_conditions`, `replace_placeholders_text`, `process_includes`, `re.compile`, `re.escape`, `_process`, `get_abs_path`, `is_probably_binary_bytes`, `replace_value`, `re.sub`, `os.path.normpath`, `FileNotFoundError`, `result.sort`, `glob.glob`, `matches.sort`, `re.fullmatch`.
59 - Keep request/response, tool, or helper semantics documented here at the same time as source changes.
60
61 ## Work Guidance
prompts/agent.system.main.communication.md
+2
@@ -18,6 +18,7 @@
18 Fences in the examples below are documentation formatting only. Your actual output starts with `{` and ends with `}` — no fences, no language tag, no prose.
19
20 ### Response example
21 +~~~json
22 {
23 "thoughts": [
24 "instructions?",
@@ -32,5 +33,6 @@ Fences in the examples below are documentation formatting only. Your actual outp
33 "arg2": "val2"
34 }
35 }
36 +~~~
37
38 {{ include "agent.system.main.communication_additions.md" }}
tests/test_default_prompt_budget.py
+42 -2
@@ -19,7 +19,7 @@ def _iter_prompt_files():
19 yield from prompts_dir.rglob("*.md")
20
21
22 -async def _build_system_text(profile: str = "agent0") -> str:
22 +async def _build_system_text(profile: str = "agent0", rendered: bool = False) -> str:
23 old_args = dict(runtime.args)
24 runtime.args.clear()
25 runtime.args["dockerized"] = "true"
@@ -34,6 +34,9 @@ async def _build_system_text(profile: str = "agent0") -> str:
34 set_current=False,
35 )
36 try:
37 + if rendered:
38 + prompt = await ctx.agent0.prepare_prompt(ctx.agent0.loop_data)
39 + return str(prompt[0].content)
40 system = await ctx.agent0.get_system_prompt(ctx.agent0.loop_data)
41 return "\n\n".join(system)
42 finally:
@@ -45,6 +48,7 @@ async def _build_system_text(profile: str = "agent0") -> str:
48 @pytest.mark.asyncio
49 async def test_default_agent0_prompt_budget_and_guardrails():
50 system_text = await _build_system_text()
51 + rendered_system_text = await _build_system_text(rendered=True)
52 communication_prompt = (
53 PROJECT_ROOT / "prompts" / "agent.system.main.communication.md"
54 ).read_text(encoding="utf-8")
@@ -66,7 +70,9 @@ async def test_default_agent0_prompt_budget_and_guardrails():
70 assert '"tool_name": "memory_load"' in system_text
71 assert "informative but tight" in system_text
72 assert "Your actual output starts with `{` and ends with `}`" in system_text
69 - assert "~~~json" not in communication_prompt
73 + assert "~~~json" in communication_prompt
74 + assert "~~~json" not in rendered_system_text
75 + assert "```json" not in rendered_system_text
76 assert "# code_execution_remote tool" not in system_text
77 assert "# text_editor_remote tool" not in system_text
78 assert "### computer_use_remote" not in system_text
@@ -76,6 +82,40 @@ async def test_default_agent0_prompt_budget_and_guardrails():
82 assert "Computer Use enablement is scoped to the current CLI session" not in system_text
83
84
85 +@pytest.mark.asyncio
86 +@pytest.mark.parametrize(
87 + "profile", ["agent0", "default", "developer", "researcher", "tiny-local"]
88 +)
89 +async def test_rendered_profiles_strip_json_fences(profile: str):
90 + system_text = await _build_system_text(profile, rendered=True)
91 +
92 + assert "~~~json" not in system_text
93 + assert "```json" not in system_text
94 +
95 + if profile == "researcher":
96 + assert "~~~python" in system_text
97 +
98 +
99 +def test_remove_code_fences_can_target_json_only():
100 + from helpers import files
101 +
102 + prompt = """Before
103 +~~~json
104 +{"tool_name":"response","tool_args":{"text":"done"}}
105 +~~~
106 +~~~python
107 +print("keep me fenced")
108 +~~~
109 +After
110 +"""
111 +
112 + rendered = files.remove_code_fences(prompt, language="json")
113 +
114 + assert "~~~json" not in rendered
115 + assert '{"tool_name":"response"' in rendered
116 + assert '~~~python\nprint("keep me fenced")\n~~~' in rendered
117 +
118 +
119 @pytest.mark.asyncio
120 async def test_tiny_local_profile_prompt_is_action_first_json_contract():
121 system_text = await _build_system_text("tiny-local")