openhands commited on
Commit
1189510
·
1 Parent(s): 019190c

Update template: replace qwen3-5_6-template_v1.1.jinja with v1.1.2 version

Browse files
qwen3-5_6-template_v1.1.jinja → qwen3_5-6-template_v1.1.2.jinja RENAMED
@@ -1,12 +1,72 @@
1
- {#- ===== SECTION 1: MACRO render_content =====
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  Handles string, list (image/video/text items), or None/undefined.
3
  count_vision=true: increments ns.image_count / ns.video_count.
 
 
 
4
  -#}
5
- {%- macro render_content(content, count_vision=false) -%}
6
- {%- if content is string -%}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  {{- content -}}
8
- {%- elif content is iterable and content is not mapping -%}
 
9
  {%- for item in content -%}
 
10
  {%- if item.type == 'image' or 'image' in item or 'image_url' in item -%}
11
  {%- if count_vision -%}{%- set ns.image_count = ns.image_count + 1 -%}{%- endif -%}
12
  {%- if add_vision_id is defined and add_vision_id -%}
@@ -21,22 +81,70 @@
21
  {{- '<|vision_start|><|video_pad|><|vision_end|>' -}}
22
  {%- elif item.type == 'text' or 'text' in item -%}
23
  {{- item.text -}}
 
 
 
24
  {%- endif -%}
25
  {%- endfor -%}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  {%- endif -%}
27
  {%- endmacro -%}
28
 
29
  {#- ===== SECTION 2: NAMESPACE INITIALISATION =====
30
  Single ns object for all mutable state.
31
- enable_thinking default=true (BUG-003 fix)
32
- preserve_thinking default=true: when false, suppresses think-block output in
33
- generation prompt and overrides enable_thinking to false.
34
- Passed via --chat-template-kwargs {"preserve_thinking":false}.
 
 
 
 
 
35
  -#}
36
  {%- set ns = namespace(
37
  enable_thinking=true,
 
38
  image_count=0,
39
- video_count=0
 
 
40
  ) -%}
41
 
42
  {#- Resolve enable_thinking kwarg -#}
@@ -48,13 +156,18 @@
48
  {%- endif -%}
49
  {%- endif -%}
50
 
51
- {#- Resolve preserve_thinking kwarg.
52
  preserve_thinking=false => force non-thinking mode (same as enable_thinking=false).
53
  preserve_thinking=true => default, no override (thinking controlled by enable_thinking).
54
  When not defined => default, no override.
55
  -#}
56
- {%- if preserve_thinking is defined and not preserve_thinking -%}
57
- {%- set ns.enable_thinking = false -%}
 
 
 
 
 
58
  {%- endif -%}
59
 
60
  {#- ===== SECTION 3: PRE-SCAN =====
@@ -82,14 +195,26 @@
82
  {%- endif -%}
83
  {%- endfor -%}
84
 
85
- {#- ===== SECTION 4: COLLECT SYSTEM CONTENT =====
86
- Merge all system/developer messages with \n\n separator (BUG-004 fix).
 
 
 
 
 
 
 
 
87
  <|think_off|> / <|think_on|> markers are stripped from output.
 
 
 
88
  -#}
89
  {%- set ns_sys = namespace(content='') -%}
90
  {%- for msg in messages -%}
91
  {%- if msg.role == 'system' or msg.role == 'developer' -%}
92
- {%- set _c = render_content(msg.content | default('')) | trim -%}
 
93
  {%- set _c = _c | replace('<|think_off|>', '') | replace('<|think_on|>', '') | trim -%}
94
  {%- if _c -%}
95
  {%- if ns_sys.content == '' -%}
@@ -101,7 +226,7 @@
101
  {%- endif -%}
102
  {%- endfor -%}
103
 
104
- {#- ===== SECTION 5: BUILD TOOLS LIST =====
105
  Normalise each tool to {"type":"function","function":{...}} format.
106
  Serialisation happens later at output time (avoids Markup + str escaping bugs).
107
  -#}
@@ -117,7 +242,7 @@
117
  {%- endfor -%}
118
  {%- endif -%}
119
 
120
- {#- ===== SECTION 6: OUTPUT SYSTEM TURN =====
121
  Each fragment output via its own {{ }} block so tojson Markup objects are
122
  never Python-concatenated with plain strings (would trigger HTML-escaping).
123
  User system content appears BEFORE the tools block (correct ordering).
@@ -140,26 +265,32 @@
140
  {{- '<|im_end|>\n' -}}
141
  {%- endif -%}
142
 
143
- {#- ===== SECTION 7: MAIN MESSAGE LOOP ===== -#}
 
 
 
 
 
144
  {%- for message in messages -%}
145
 
146
- {#- 7a: System / Developer — already rendered above, skip -#}
147
  {%- if message.role == 'system' or message.role == 'developer' -%}
148
 
149
- {#- 7b: User messages -#}
150
  {%- elif message.role == 'user' -%}
151
- {%- set _uc = render_content(message.content | default(''), true) -%}
152
  {{- '<|im_start|>user\n' + _uc + '<|im_end|>\n' -}}
153
 
154
- {#- 7c: Assistant messages -#}
155
  {%- elif message.role == 'assistant' -%}
156
- {#- Safely extract content as string — guard against absent key (BUG-002 fix).
157
  Also support message.reasoning_content as an explicit think-block source
158
  (used by some frameworks that store thinking separately from content). -#}
159
  {%- if message.content is defined and message.content is string -%}
160
  {%- set _ac = message.content -%}
161
- {%- elif message.content is defined and message.content is iterable and message.content is not mapping -%}
162
- {%- set _ac = render_content(message.content) -%}
 
163
  {%- else -%}
164
  {%- set _ac = '' -%}
165
  {%- endif -%}
@@ -174,7 +305,8 @@
174
  {%- endif -%}
175
 
176
  {#- Collect tool_calls if present -#}
177
- {%- set _tc = message.tool_calls if message.tool_calls is defined and message.tool_calls else [] -%}
 
178
 
179
  {#- Strip <tool_call> prefix from content when tool_calls also present
180
  (some frameworks duplicate the data in both fields) -#}
@@ -182,38 +314,50 @@
182
  {%- set _ac = _ac.split('<tool_call>')[0] | trim -%}
183
  {%- endif -%}
184
 
185
- {#- Determine if this is the last-in-history assistant turn.
186
- When add_generation_prompt=False and this is the last message, think blocks
187
- must be preserved (and non-thinking prefill applied if needed).
188
- All other turns have their think blocks stripped. -#}
189
- {%- set _is_last_hist = loop.last and not (add_generation_prompt | default(false)) -%}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
- {#- Think-block handling (BUG-001 fix + last-turn preservation):
192
- - Tool-call turns : never strip (think block is part of the tool-call format)
193
- - Last-history turn : preserve; inject non-thinking prefill when absent
194
- - Historical turns : strip using fuzzy end-tag matching to handle
195
- </think>, </thinking>, </ think>, </think > variants -#}
196
- {%- if not _tc -%}
197
- {%- if _is_last_hist -%}
198
- {%- if '<think>' not in _ac and not ns.enable_thinking -%}
199
- {%- set _ac = '<think>\n\n</think>\n\n' + _ac -%}
200
- {%- endif -%}
201
- {%- else -%}
202
- {#- Fuzzy end-tag detection for historical turn stripping -#}
203
- {%- set _think_end = '' -%}
204
- {%- if '</think>' in _ac -%}
205
- {%- set _think_end = '</think>' -%}
206
- {%- elif '</thinking>' in _ac -%}
207
- {%- set _think_end = '</thinking>' -%}
208
- {%- elif '</ think>' in _ac -%}
209
- {%- set _think_end = '</ think>' -%}
210
- {%- elif '</think >' in _ac -%}
211
- {%- set _think_end = '</think >' -%}
212
- {%- endif -%}
213
- {%- if _think_end -%}
214
- {%- set _ac = _ac.split(_think_end)[-1].lstrip('\n') -%}
215
- {%- endif -%}
216
  {%- endif -%}
 
 
 
217
  {%- endif -%}
218
 
219
  {#- Emit the assistant turn -#}
@@ -223,9 +367,9 @@
223
  {%- if _tc -%}{{- '\n' -}}{%- endif -%}
224
  {%- endif -%}
225
 
226
- {#- Render tool calls in Hermes format (BUG-006 fix: arguments as-is or tojson).
227
  Each value output via its own {{ }} block — never concatenated with plain strings
228
- in Python, which would trigger Markup HTML-escaping (BUG-003/markup fix). -#}
229
  {%- if _tc -%}
230
  {%- for tc in _tc -%}
231
  {{- '<tool_call>\n' -}}
@@ -245,15 +389,31 @@
245
  {%- endif -%}
246
  {{- '<|im_end|>\n' -}}
247
 
248
- {#- 7d: Tool results — group consecutive tool messages into one user turn -#}
249
  {%- elif message.role == 'tool' -%}
250
  {%- set _prev_role = messages[loop.index0 - 1].role if loop.index0 > 0 else '' -%}
251
  {%- set _next_role = messages[loop.index0 + 1].role if not loop.last else '' -%}
 
 
 
 
 
252
  {%- if _prev_role != 'tool' -%}
253
  {{- '<|im_start|>user\n' -}}
254
  {%- endif -%}
255
  {{- '<tool_response>\n' -}}
256
- {{- message.content | default('') -}}
 
 
 
 
 
 
 
 
 
 
 
257
  {%- if _next_role == 'tool' -%}
258
  {{- '\n</tool_response>\n' -}}
259
  {%- else -%}
@@ -261,24 +421,25 @@
261
  {{- '<|im_end|>\n' -}}
262
  {%- endif -%}
263
 
264
- {#- 7e: Unknown role -#}
265
  {%- else -%}
266
  {{- raise_exception('Unexpected message role: ' + message.role) -}}
267
  {%- endif -%}
268
 
269
  {%- endfor -%}
270
 
271
- {#- ===== SECTION 8: GENERATION PROMPT =====
 
 
 
272
  enable_thinking=True → open <think>\n prefill so llama.cpp reasoning-budget
273
  and other inference engines can hook into the think-stream.
274
  The model continues generating inside the open block.
275
- enable_thinking=False → exact non-thinking prefill: <think>\n\n</think>\n\n
276
- (19-char closed block, BUG-005 fix)
277
-
278
  NOTE: The <think>\n opener is EPHEMERAL — it lives only in the generation
279
- prompt, never in chat history. Historical think-block stripping (BUG-001)
280
- is handled in Section 7c and is entirely unaffected by this change.
281
- No context poisoning risk.
282
  -#}
283
  {%- if add_generation_prompt -%}
284
  {{- '<|im_start|>assistant\n' -}}
@@ -287,4 +448,4 @@
287
  {%- else -%}
288
  {{- '<think>\n\n</think>\n\n' -}}
289
  {%- endif -%}
290
- {%- endif -%}
 
1
+ {#- ============================================================================
2
+ Qwen Chat Template v0.8
3
+ Based on v0.7 with additional compatibility fixes for llama.cpp
4
+
5
+ FIXES APPLIED IN v0.8:
6
+ 1. Type guard in detect_tool_error macro (prevents errors on non-string input)
7
+ 2. Type check for tool_calls (ensures list type before iteration)
8
+ 3. Replaced emoji in tool error warnings with text-only (tokenization safety)
9
+
10
+ FIXES APPLIED IN v0.7:
11
+ 1. Tool call error handling with consecutive_failures tracking
12
+ 2. System message media validation (raises exception for images/videos)
13
+ 3. Empty messages validation (raises exception if no messages)
14
+ 4. Unknown content type handling (raises exception for unexpected types)
15
+ 5. Think-block display logic (preserve_thinking controls ALL assistant messages, not just generation prompt)
16
+
17
+ Sections:
18
+ - 1A: MACRO render_content (with media validation for system content)
19
+ - 1B: MACRO detect_tool_error (error detection for tool responses)
20
+ - 2: NAMESPACE INITIALISATION (with error tracking)
21
+ - 3: PRE-SCAN (thinking mode detection)
22
+ - 4: VALIDATE MESSAGES (empty messages check)
23
+ - 5: COLLECT SYSTEM CONTENT (with media validation for system messages)
24
+ - 6: BUILD TOOLS LIST
25
+ - 7: OUTPUT SYSTEM TURN
26
+ - 8: MAIN MESSAGE LOOP (with error detection in tool responses)
27
+ - 9: GENERATION PROMPT (fixed preserve_thinking logic)
28
+ ============================================================================ -#}
29
+
30
+ {#- ===== HELPER: raise_exception macro =====
31
+ Jinja2 doesn't have a built-in raise_exception.
32
+ This macro outputs an error marker in the rendered output.
33
+ Callers should check output for "ERROR:" pattern to detect validation failures.
34
+ -#}
35
+ {%- macro raise_exception(message) -%}
36
+ {{- '\n[ERROR: ' ~ message ~ ']' -}}
37
+ {%- endmacro -%}
38
+
39
+ {#- ===== SECTION 1A: MACRO render_content =====
40
  Handles string, list (image/video/text items), or None/undefined.
41
  count_vision=true: increments ns.image_count / ns.video_count.
42
+ is_system_content=false: Set true when rendering system/developer content
43
+ to enable media validation (raises exception).
44
+ count_vision=true: increments vision counters.
45
  -#}
46
+ {%- macro render_content(content, count_vision=false, is_system_content=false) -%}
47
+ {#- VALIDATION: System messages cannot contain images or videos (from v18) -#}
48
+ {#- FIX: also exclude strings and handle None - llama.cpp treats strings as non-iterable in for loops -#}
49
+ {%- if is_system_content and content is iterable and content is not mapping and content is not string and content is not none -%}
50
+ {%- for item in content -%}
51
+ {%- if item.type == 'image' or 'image' in item or 'image_url' in item -%}
52
+ {{- raise_exception('System message cannot contain images.') -}}
53
+ {%- endif -%}
54
+ {%- if item.type == 'video' or 'video' in item -%}
55
+ {{- raise_exception('System message cannot contain videos.') -}}
56
+ {%- endif -%}
57
+ {%- endfor -%}
58
+ {%- endif -%}
59
+
60
+ {#- Main content rendering -#}
61
+ {#- Handle None/undefined content -#}
62
+ {%- if content is none or content is defined == false -%}
63
+ {{- '' -}}
64
+ {%- elif content is string -%}
65
  {{- content -}}
66
+ {#- FIX: also exclude strings - llama.cpp treats strings as non-iterable in for loops -#}
67
+ {%- elif content is iterable and content is not mapping and content is not string -%}
68
  {%- for item in content -%}
69
+ {#- Handle different item types -#}
70
  {%- if item.type == 'image' or 'image' in item or 'image_url' in item -%}
71
  {%- if count_vision -%}{%- set ns.image_count = ns.image_count + 1 -%}{%- endif -%}
72
  {%- if add_vision_id is defined and add_vision_id -%}
 
81
  {{- '<|vision_start|><|video_pad|><|vision_end|>' -}}
82
  {%- elif item.type == 'text' or 'text' in item -%}
83
  {{- item.text -}}
84
+ {#- ERROR: Unknown content type - raise explicit exception (from v18) -#}
85
+ {%- else -%}
86
+ {{- raise_exception('Unexpected content type in message content.') -}}
87
  {%- endif -%}
88
  {%- endfor -%}
89
+ {#- ERROR: Unknown content type - raise explicit exception (from v18) -#}
90
+ {%- elif content is not none and content is defined -%}
91
+ {{- raise_exception('Unexpected content type.') -}}
92
+ {%- endif -%}
93
+ {%- endmacro -%}
94
+
95
+ {#- ===== SECTION 1B: MACRO detect_tool_error (NEW in v0.7) =====
96
+ Detects if a tool response contains error indicators.
97
+ Uses heuristics from v18:
98
+ - Checks for error keywords (error, exception, traceback, failed to)
99
+ - Ignores responses with '$ ' (shell output prefix) or 'took ' (timing info)
100
+ - Ignores responses > 500 chars (likely valid output, not error)
101
+
102
+ Returns: ns.last_tool_failed (true/false)
103
+ Side effect: Updates ns.consecutive_failures counter
104
+ -#}
105
+ {%- macro detect_tool_error(content) -%}
106
+ {#- Type guard: ensure content is string (llama.cpp compatibility) -#}
107
+ {%- set content = content if content is string else '' -%}
108
+ {%- set content_lower = content | lower -%}
109
+ {%- set content_length = content | length -%}
110
+
111
+ {#- Error detection heuristics: short response + no shell prefix + has error keywords -#}
112
+ {%- if content_length < 500
113
+ and '$ ' not in content
114
+ and 'took ' not in content_lower
115
+ and ('"error":' in content_lower or 'error:' in content_lower
116
+ or 'exception:' in content_lower or 'traceback' in content_lower
117
+ or 'command not found' in content_lower or 'invalid syntax' in content_lower
118
+ or 'failed to' in content_lower or 'permission denied' in content_lower) -%}
119
+ {#- Error detected - update failure tracking -#}
120
+ {%- set ns.last_tool_failed = true -%}
121
+ {%- set ns.consecutive_failures = ns.consecutive_failures + 1 -%}
122
+ {%- else -%}
123
+ {#- No error - reset failure tracking -#}
124
+ {%- set ns.last_tool_failed = false -%}
125
+ {%- set ns.consecutive_failures = 0 -%}
126
  {%- endif -%}
127
  {%- endmacro -%}
128
 
129
  {#- ===== SECTION 2: NAMESPACE INITIALISATION =====
130
  Single ns object for all mutable state.
131
+
132
+ enable_thinking: default=true (controls think-block in generation prompt)
133
+ preserve_thinking: default=true (controls think-block display in conversation history)
134
+ image_count: Vision counter for images
135
+ video_count: Vision counter for videos
136
+
137
+ NEW in v0.7:
138
+ - consecutive_failures: Tracks consecutive tool call failures (from v18)
139
+ - last_tool_failed: Boolean flag for current tool response (from v18)
140
  -#}
141
  {%- set ns = namespace(
142
  enable_thinking=true,
143
+ preserve_thinking=true,
144
  image_count=0,
145
+ video_count=0,
146
+ consecutive_failures=0,
147
+ last_tool_failed=false
148
  ) -%}
149
 
150
  {#- Resolve enable_thinking kwarg -#}
 
156
  {%- endif -%}
157
  {%- endif -%}
158
 
159
+ {#- Resolve preserve_thinking kwarg (FIXED in v0.7: now also affects conversation history, not just generation prompt).
160
  preserve_thinking=false => force non-thinking mode (same as enable_thinking=false).
161
  preserve_thinking=true => default, no override (thinking controlled by enable_thinking).
162
  When not defined => default, no override.
163
  -#}
164
+ {%- if preserve_thinking is defined -%}
165
+ {%- if not preserve_thinking -%}
166
+ {%- set ns.enable_thinking = false -%}
167
+ {%- set ns.preserve_thinking = false -%}
168
+ {%- else -%}
169
+ {%- set ns.preserve_thinking = true -%}
170
+ {%- endif -%}
171
  {%- endif -%}
172
 
173
  {#- ===== SECTION 3: PRE-SCAN =====
 
195
  {%- endif -%}
196
  {%- endfor -%}
197
 
198
+ {#- ===== SECTION 4: VALIDATE MESSAGES (NEW in v0.7) =====
199
+ Validate that messages is provided and not empty.
200
+ From v18: raises exception if no messages provided.
201
+ -#}
202
+ {%- if not messages -%}
203
+ {{- raise_exception('No messages provided.') -}}
204
+ {%- endif -%}
205
+
206
+ {#- ===== SECTION 5: COLLECT SYSTEM CONTENT =====
207
+ Merge all system/developer messages with \n\n separator.
208
  <|think_off|> / <|think_on|> markers are stripped from output.
209
+
210
+ FIXED in v0.7: Pass is_system_content=true to render_content to trigger
211
+ media validation (raises exception if system contains images/videos).
212
  -#}
213
  {%- set ns_sys = namespace(content='') -%}
214
  {%- for msg in messages -%}
215
  {%- if msg.role == 'system' or msg.role == 'developer' -%}
216
+ {#- Pass is_system_content=true for media validation -#}
217
+ {%- set _c = render_content(msg.content | default(''), false, true) | trim -%}
218
  {%- set _c = _c | replace('<|think_off|>', '') | replace('<|think_on|>', '') | trim -%}
219
  {%- if _c -%}
220
  {%- if ns_sys.content == '' -%}
 
226
  {%- endif -%}
227
  {%- endfor -%}
228
 
229
+ {#- ===== SECTION 6: BUILD TOOLS LIST =====
230
  Normalise each tool to {"type":"function","function":{...}} format.
231
  Serialisation happens later at output time (avoids Markup + str escaping bugs).
232
  -#}
 
242
  {%- endfor -%}
243
  {%- endif -%}
244
 
245
+ {#- ===== SECTION 7: OUTPUT SYSTEM TURN =====
246
  Each fragment output via its own {{ }} block so tojson Markup objects are
247
  never Python-concatenated with plain strings (would trigger HTML-escaping).
248
  User system content appears BEFORE the tools block (correct ordering).
 
265
  {{- '<|im_end|>\n' -}}
266
  {%- endif -%}
267
 
268
+ {#- ===== SECTION 8: MAIN MESSAGE LOOP =====
269
+ FIXED in v0.7:
270
+ - Tool responses now have error detection via detect_tool_error macro
271
+ - Warning messages injected for failed tool calls
272
+ - consecutive_failures tracking for escalating warnings
273
+ -#}
274
  {%- for message in messages -%}
275
 
276
+ {#- 8a: System / Developer — already rendered above, skip -#}
277
  {%- if message.role == 'system' or message.role == 'developer' -%}
278
 
279
+ {#- 8b: User messages -#}
280
  {%- elif message.role == 'user' -%}
281
+ {%- set _uc = render_content(message.content | default(''), true, false) -%}
282
  {{- '<|im_start|>user\n' + _uc + '<|im_end|>\n' -}}
283
 
284
+ {#- 8c: Assistant messages -#}
285
  {%- elif message.role == 'assistant' -%}
286
+ {#- Safely extract content as string — guard against absent key.
287
  Also support message.reasoning_content as an explicit think-block source
288
  (used by some frameworks that store thinking separately from content). -#}
289
  {%- if message.content is defined and message.content is string -%}
290
  {%- set _ac = message.content -%}
291
+ {#- FIX: also exclude strings - llama.cpp treats strings as non-iterable in for loops -#}
292
+ {%- elif message.content is defined and message.content is iterable and message.content is not mapping and message.content is not string -%}
293
+ {%- set _ac = render_content(message.content, false, false) -%}
294
  {%- else -%}
295
  {%- set _ac = '' -%}
296
  {%- endif -%}
 
305
  {%- endif -%}
306
 
307
  {#- Collect tool_calls if present -#}
308
+ {#- Type check: ensure tool_calls is a list, not string (llama.cpp compatibility) -#}
309
+ {%- set _tc = message.tool_calls if message.tool_calls is defined and message.tool_calls is iterable and message.tool_calls is not string else [] -%}
310
 
311
  {#- Strip <tool_call> prefix from content when tool_calls also present
312
  (some frameworks duplicate the data in both fields) -#}
 
314
  {%- set _ac = _ac.split('<tool_call>')[0] | trim -%}
315
  {%- endif -%}
316
 
317
+ {#- FIXED in v0.7: Think-block handling with preserve_thinking support
318
+
319
+ New logic (from v18): preserve_thinking controls think-block display on ALL
320
+ assistant messages, not just generation prompt:
321
+
322
+ - Tool-call turns : never strip (think block is part of the tool-call format)
323
+ - preserve_thinking : if true, show think blocks on ALL messages
324
+ - Last-history turn : if preserve_thinking false, apply last-turn handling
325
+ - Historical turns : if preserve_thinking false, strip think blocks
326
+
327
+ The old behavior (strip unless add_generation_prompt) is now controlled
328
+ by preserve_thinking parameter.
329
+ -#}
330
+ {%- set _show_think = false -%}
331
+ {%- if _tc -%}
332
+ {#- Tool calls: always show think block -#}
333
+ {%- set _show_think = true -%}
334
+ {%- elif ns.preserve_thinking -%}
335
+ {#- preserve_thinking=true: show think blocks on all messages -#}
336
+ {%- set _show_think = true -%}
337
+ {%- elif loop.last -%}
338
+ {#- Last message without preserve_thinking: show if thinking enabled -#}
339
+ {%- set _show_think = ns.enable_thinking -%}
340
+ {%- endif -%}
341
 
342
+ {#- Apply think-block stripping based on _show_think flag -#}
343
+ {%- if not _show_think -%}
344
+ {#- Fuzzy end-tag detection for stripping -#}
345
+ {%- set _think_end = '' -%}
346
+ {%- if '</think>' in _ac -%}
347
+ {%- set _think_end = '</think>' -%}
348
+ {%- elif '</thinking>' in _ac -%}
349
+ {%- set _think_end = '</thinking>' -%}
350
+ {%- elif '</ think>' in _ac -%}
351
+ {%- set _think_end = '</ think>' -%}
352
+ {%- elif '</think >' in _ac -%}
353
+ {%- set _think_end = '</think >' -%}
354
+ {%- endif -%}
355
+ {%- if _think_end -%}
356
+ {%- set _ac = _ac.split(_think_end)[-1].lstrip('\n') -%}
 
 
 
 
 
 
 
 
 
 
357
  {%- endif -%}
358
+ {%- elif not _tc and loop.last and '<think>' not in _ac and not ns.enable_thinking -%}
359
+ {#- Last turn, non-thinking: inject empty think block if missing -#}
360
+ {%- set _ac = '<think>\n\n</think>\n\n' + _ac -%}
361
  {%- endif -%}
362
 
363
  {#- Emit the assistant turn -#}
 
367
  {%- if _tc -%}{{- '\n' -}}{%- endif -%}
368
  {%- endif -%}
369
 
370
+ {#- Render tool calls in Hermes format.
371
  Each value output via its own {{ }} block — never concatenated with plain strings
372
+ in Python, which would trigger Markup HTML-escaping. -#}
373
  {%- if _tc -%}
374
  {%- for tc in _tc -%}
375
  {{- '<tool_call>\n' -}}
 
389
  {%- endif -%}
390
  {{- '<|im_end|>\n' -}}
391
 
392
+ {#- 8d: Tool results — with error detection (NEW in v0.7) -#}
393
  {%- elif message.role == 'tool' -%}
394
  {%- set _prev_role = messages[loop.index0 - 1].role if loop.index0 > 0 else '' -%}
395
  {%- set _next_role = messages[loop.index0 + 1].role if not loop.last else '' -%}
396
+
397
+ {#- NEW in v0.7: Detect errors in tool response -#}
398
+ {%- set _tool_content = message.content | default('') -%}
399
+ {{- detect_tool_error(_tool_content) -}}
400
+
401
  {%- if _prev_role != 'tool' -%}
402
  {{- '<|im_start|>user\n' -}}
403
  {%- endif -%}
404
  {{- '<tool_response>\n' -}}
405
+ {{- _tool_content -}}
406
+
407
+ {#- NEW in v0.7: Inject warning if tool error detected -#}
408
+ {#- v0.8: Replaced emoji with text-only for tokenization safety -#}
409
+ {%- if ns.last_tool_failed -%}
410
+ {%- if ns.consecutive_failures >= 2 -%}
411
+ {{- '\n\n[SYSTEM WARNING: ' ~ ns.consecutive_failures ~ ' consecutive tool errors detected. Your previous approach is incorrect.]' -}}
412
+ {%- else -%}
413
+ {{- '\n\n[SYSTEM WARNING: The previous tool call returned an error. Diagnose the failure and retry with corrected arguments.]' -}}
414
+ {%- endif -%}
415
+ {%- endif -%}
416
+
417
  {%- if _next_role == 'tool' -%}
418
  {{- '\n</tool_response>\n' -}}
419
  {%- else -%}
 
421
  {{- '<|im_end|>\n' -}}
422
  {%- endif -%}
423
 
424
+ {#- 8e: Unknown role - explicit error (from v18) -#}
425
  {%- else -%}
426
  {{- raise_exception('Unexpected message role: ' + message.role) -}}
427
  {%- endif -%}
428
 
429
  {%- endfor -%}
430
 
431
+ {#- ===== SECTION 9: GENERATION PROMPT =====
432
+ FIXED in v0.7: preserve_thinking now affects conversation history (Section 8),
433
+ so generation prompt logic is simplified.
434
+
435
  enable_thinking=True → open <think>\n prefill so llama.cpp reasoning-budget
436
  and other inference engines can hook into the think-stream.
437
  The model continues generating inside the open block.
438
+ enable_thinking=False → exact non-thinking prefill: </think>\n\n
439
+
 
440
  NOTE: The <think>\n opener is EPHEMERAL — it lives only in the generation
441
+ prompt, never in chat history. Historical think-block stripping is handled
442
+ in Section 8 based on preserve_thinking setting.
 
443
  -#}
444
  {%- if add_generation_prompt -%}
445
  {{- '<|im_start|>assistant\n' -}}
 
448
  {%- else -%}
449
  {{- '<think>\n\n</think>\n\n' -}}
450
  {%- endif -%}
451
+ {%- endif -%}