atakan Claude Fable 5 commited on
Commit
7cdd3d6
·
1 Parent(s): 48ee375

fix: Stop silent empty-answer fallback; surface tool-error reasons

Browse files

- _direct_answer's exception handler now logs instead of silently
swallowing -- the last line of defense before the canned placeholder
was failing invisibly with zero trace.
- run() and run_stream() both retry _direct_answer once before ever
showing the placeholder; run_stream() (what the web UI actually uses)
previously didn't attempt this recovery at all. Verified end-to-end on
a real question that previously produced the placeholder: now returns
a real, correctly-formatted answer instead.
- Same _get_grounded_instruction fix: log on failure instead of silently
dropping RAG grounding.
- prompts.py: tool errors often name the exact concept being tested
("uncontrollable", "not positive semi-definite") -- instruct the model
to state that reason as the headline finding instead of routing around
it with a numeric-looking answer that ignores what the error said.
Verified against the 4 known regression cases from today's ControlBench
run: 2 of 4 fixed for real (not just phrasing), 2 remain (one case
where the rule doesn't reliably take, one case that isn't an error at
all -- an ill-conditioned but successful computation -- and needs a
different rule not yet written).
- Add today's ControlBench-v1 run (v3) for the record: 53.3% overall,
down from 77.1% on Aug 17 against the same adapter. Investigated before
concluding anything: most of the code_and_simulation and
underspecified_and_traps drop is a scorer methodology mismatch (fence-
only code detection, 14-keyword trap matching) against today's
correctly-changed behavior (prefer verified tools over pasted code;
terser refusals). Some of it is real: 4 trap items regressed on
ignoring tool-error text, addressed above.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

benchmarks/results/controlai_agent_v3_controlbench_v1.json ADDED
@@ -0,0 +1,511 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "overall_score": 53.3,
3
+ "pillar_scores": {
4
+ "theory_and_concepts": 61.7,
5
+ "numerical_synthesis": 100.0,
6
+ "code_and_simulation": 6.0,
7
+ "underspecified_and_traps": 20.0,
8
+ "real_world_case_studies": 73.8
9
+ },
10
+ "total_items": 50,
11
+ "evaluated_items": 50,
12
+ "item_results": [
13
+ {
14
+ "id": "cb_theory_001",
15
+ "pillar": "theory_and_concepts",
16
+ "score": 100.0,
17
+ "details": {
18
+ "score": 100.0,
19
+ "concepts_found": 4,
20
+ "total_concepts": 4,
21
+ "missing": []
22
+ }
23
+ },
24
+ {
25
+ "id": "cb_theory_002",
26
+ "pillar": "theory_and_concepts",
27
+ "score": 75.0,
28
+ "details": {
29
+ "score": 75.0,
30
+ "concepts_found": 3,
31
+ "total_concepts": 4,
32
+ "missing": [
33
+ "PBH immediately determines stabilizability (if uncontrollable modes have Re(lambda) < 0)"
34
+ ]
35
+ }
36
+ },
37
+ {
38
+ "id": "cb_theory_003",
39
+ "pillar": "theory_and_concepts",
40
+ "score": 25.0,
41
+ "details": {
42
+ "score": 25.0,
43
+ "concepts_found": 1,
44
+ "total_concepts": 4,
45
+ "missing": [
46
+ "RHP zeros cause initial undershoot in step response",
47
+ "RHP zero at z requires closed-loop bandwidth to be bounded below approximately z/2",
48
+ "Cannot invert RHP zeros without creating unstable closed-loop pole-zero cancelations"
49
+ ]
50
+ }
51
+ },
52
+ {
53
+ "id": "cb_theory_004",
54
+ "pillar": "theory_and_concepts",
55
+ "score": 50.0,
56
+ "details": {
57
+ "score": 50.0,
58
+ "concepts_found": 2,
59
+ "total_concepts": 4,
60
+ "missing": [
61
+ "Indirect method fails / is inconclusive if any eigenvalue has Re(lambda) = 0 (critical cases)",
62
+ "Direct method: constructs positive-definite V(x) with negative semi-definite or negative definite V_dot; valid globally or over an estimated invariant set"
63
+ ]
64
+ }
65
+ },
66
+ {
67
+ "id": "cb_theory_005",
68
+ "pillar": "theory_and_concepts",
69
+ "score": 25.0,
70
+ "details": {
71
+ "score": 25.0,
72
+ "concepts_found": 1,
73
+ "total_concepts": 4,
74
+ "missing": [
75
+ "Closed-loop eigenvalues are exactly the union of LQR poles and observer poles",
76
+ "Doyle (1978) showed that LQG has no guaranteed gain or phase margins (can have arbitrarily small gain margin)",
77
+ "Observer loop recovery (LTR) is required to restore LQR robustness"
78
+ ]
79
+ }
80
+ },
81
+ {
82
+ "id": "cb_theory_006",
83
+ "pillar": "theory_and_concepts",
84
+ "score": 100.0,
85
+ "details": {
86
+ "score": 100.0,
87
+ "concepts_found": 4,
88
+ "total_concepts": 4,
89
+ "missing": []
90
+ }
91
+ },
92
+ {
93
+ "id": "cb_theory_007",
94
+ "pillar": "theory_and_concepts",
95
+ "score": 75.0,
96
+ "details": {
97
+ "score": 75.0,
98
+ "concepts_found": 3,
99
+ "total_concepts": 4,
100
+ "missing": [
101
+ "Provides structural robustness against plant parameter variations"
102
+ ]
103
+ }
104
+ },
105
+ {
106
+ "id": "cb_theory_008",
107
+ "pillar": "theory_and_concepts",
108
+ "score": 0.0,
109
+ "details": {
110
+ "score": 0.0,
111
+ "concepts_found": 0,
112
+ "total_concepts": 3,
113
+ "missing": [
114
+ "Guaranteed Gain Margin: GM in [1/2, infinity) (or -6 dB to +infinity dB)",
115
+ "Guaranteed Phase Margin: PM >= 60 degrees (at least +/- 60 deg)",
116
+ "Result stems from Kalman inequality (return difference inequality): ||I + R^(1/2) K (sI-A)^(-1) B R^(-1/2)|| >= 1 for s=jw"
117
+ ]
118
+ }
119
+ },
120
+ {
121
+ "id": "cb_theory_009",
122
+ "pillar": "theory_and_concepts",
123
+ "score": 66.7,
124
+ "details": {
125
+ "score": 66.7,
126
+ "concepts_found": 2,
127
+ "total_concepts": 3,
128
+ "missing": [
129
+ "Passivity is preferred in physical systems (mechanical, electrical, robotics) with energy storage and collocated actuators/sensors where infinite gain margin is achievable"
130
+ ]
131
+ }
132
+ },
133
+ {
134
+ "id": "cb_theory_010",
135
+ "pillar": "theory_and_concepts",
136
+ "score": 100.0,
137
+ "details": {
138
+ "score": 100.0,
139
+ "concepts_found": 4,
140
+ "total_concepts": 4,
141
+ "missing": []
142
+ }
143
+ },
144
+ {
145
+ "id": "cb_num_001",
146
+ "pillar": "numerical_synthesis",
147
+ "score": 100.0,
148
+ "details": {
149
+ "score": 100.0,
150
+ "tool_matched": true,
151
+ "numeric_verified": true
152
+ }
153
+ },
154
+ {
155
+ "id": "cb_num_002",
156
+ "pillar": "numerical_synthesis",
157
+ "score": 100.0,
158
+ "details": {
159
+ "score": 100.0,
160
+ "tool_matched": true,
161
+ "numeric_verified": true
162
+ }
163
+ },
164
+ {
165
+ "id": "cb_num_003",
166
+ "pillar": "numerical_synthesis",
167
+ "score": 100.0,
168
+ "details": {
169
+ "score": 100.0,
170
+ "tool_matched": true,
171
+ "numeric_verified": true
172
+ }
173
+ },
174
+ {
175
+ "id": "cb_num_004",
176
+ "pillar": "numerical_synthesis",
177
+ "score": 100.0,
178
+ "details": {
179
+ "score": 100.0,
180
+ "tool_matched": true,
181
+ "numeric_verified": true
182
+ }
183
+ },
184
+ {
185
+ "id": "cb_num_005",
186
+ "pillar": "numerical_synthesis",
187
+ "score": 100.0,
188
+ "details": {
189
+ "score": 100.0,
190
+ "tool_matched": true,
191
+ "numeric_verified": true
192
+ }
193
+ },
194
+ {
195
+ "id": "cb_num_006",
196
+ "pillar": "numerical_synthesis",
197
+ "score": 100.0,
198
+ "details": {
199
+ "score": 100.0,
200
+ "tool_matched": true,
201
+ "numeric_verified": true
202
+ }
203
+ },
204
+ {
205
+ "id": "cb_num_007",
206
+ "pillar": "numerical_synthesis",
207
+ "score": 100.0,
208
+ "details": {
209
+ "score": 100.0,
210
+ "tool_matched": true,
211
+ "numeric_verified": true
212
+ }
213
+ },
214
+ {
215
+ "id": "cb_num_008",
216
+ "pillar": "numerical_synthesis",
217
+ "score": 100.0,
218
+ "details": {
219
+ "score": 100.0,
220
+ "tool_matched": true,
221
+ "numeric_verified": true
222
+ }
223
+ },
224
+ {
225
+ "id": "cb_num_009",
226
+ "pillar": "numerical_synthesis",
227
+ "score": 100.0,
228
+ "details": {
229
+ "score": 100.0,
230
+ "tool_matched": true,
231
+ "numeric_verified": true
232
+ }
233
+ },
234
+ {
235
+ "id": "cb_num_010",
236
+ "pillar": "numerical_synthesis",
237
+ "score": 100.0,
238
+ "details": {
239
+ "score": 100.0,
240
+ "tool_matched": true,
241
+ "numeric_verified": true
242
+ }
243
+ },
244
+ {
245
+ "id": "cb_num_011",
246
+ "pillar": "numerical_synthesis",
247
+ "score": 100.0,
248
+ "details": {
249
+ "score": 100.0,
250
+ "tool_matched": true,
251
+ "numeric_verified": true
252
+ }
253
+ },
254
+ {
255
+ "id": "cb_num_012",
256
+ "pillar": "numerical_synthesis",
257
+ "score": 100.0,
258
+ "details": {
259
+ "score": 100.0,
260
+ "tool_matched": true,
261
+ "numeric_verified": true
262
+ }
263
+ },
264
+ {
265
+ "id": "cb_code_001",
266
+ "pillar": "code_and_simulation",
267
+ "score": 0.0,
268
+ "details": {
269
+ "score": 0.0,
270
+ "status": "No code block found"
271
+ }
272
+ },
273
+ {
274
+ "id": "cb_code_002",
275
+ "pillar": "code_and_simulation",
276
+ "score": 0.0,
277
+ "details": {
278
+ "score": 0.0,
279
+ "status": "No code block found"
280
+ }
281
+ },
282
+ {
283
+ "id": "cb_code_003",
284
+ "pillar": "code_and_simulation",
285
+ "score": 30.0,
286
+ "details": {
287
+ "score": 30.0,
288
+ "sandbox_executed": false,
289
+ "message": "AttributeError: Line2D.set() got an unexpected keyword argument 'plotstyle'"
290
+ }
291
+ },
292
+ {
293
+ "id": "cb_code_004",
294
+ "pillar": "code_and_simulation",
295
+ "score": 30.0,
296
+ "details": {
297
+ "score": 30.0,
298
+ "sandbox_executed": false,
299
+ "message": "ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 2 is different from 1)"
300
+ }
301
+ },
302
+ {
303
+ "id": "cb_code_005",
304
+ "pillar": "code_and_simulation",
305
+ "score": 0.0,
306
+ "details": {
307
+ "score": 0.0,
308
+ "status": "No code block found"
309
+ }
310
+ },
311
+ {
312
+ "id": "cb_code_006",
313
+ "pillar": "code_and_simulation",
314
+ "score": 0.0,
315
+ "details": {
316
+ "score": 0.0,
317
+ "status": "No code block found"
318
+ }
319
+ },
320
+ {
321
+ "id": "cb_code_007",
322
+ "pillar": "code_and_simulation",
323
+ "score": 0.0,
324
+ "details": {
325
+ "score": 0.0,
326
+ "matlab_syntax_score": 0.0
327
+ }
328
+ },
329
+ {
330
+ "id": "cb_code_008",
331
+ "pillar": "code_and_simulation",
332
+ "score": 0.0,
333
+ "details": {
334
+ "score": 0.0,
335
+ "status": "No code block found"
336
+ }
337
+ },
338
+ {
339
+ "id": "cb_code_009",
340
+ "pillar": "code_and_simulation",
341
+ "score": 0.0,
342
+ "details": {
343
+ "score": 0.0,
344
+ "status": "No code block found"
345
+ }
346
+ },
347
+ {
348
+ "id": "cb_code_010",
349
+ "pillar": "code_and_simulation",
350
+ "score": 0.0,
351
+ "details": {
352
+ "score": 0.0,
353
+ "status": "No code block found"
354
+ }
355
+ },
356
+ {
357
+ "id": "cb_trap_001",
358
+ "pillar": "underspecified_and_traps",
359
+ "score": 0.0,
360
+ "details": {
361
+ "score": 0.0,
362
+ "refusal_or_trap_detected": false
363
+ }
364
+ },
365
+ {
366
+ "id": "cb_trap_002",
367
+ "pillar": "underspecified_and_traps",
368
+ "score": 0.0,
369
+ "details": {
370
+ "score": 0.0,
371
+ "refusal_or_trap_detected": false
372
+ }
373
+ },
374
+ {
375
+ "id": "cb_trap_003",
376
+ "pillar": "underspecified_and_traps",
377
+ "score": 0.0,
378
+ "details": {
379
+ "score": 0.0,
380
+ "refusal_or_trap_detected": false
381
+ }
382
+ },
383
+ {
384
+ "id": "cb_trap_004",
385
+ "pillar": "underspecified_and_traps",
386
+ "score": 0.0,
387
+ "details": {
388
+ "score": 0.0,
389
+ "refusal_or_trap_detected": false
390
+ }
391
+ },
392
+ {
393
+ "id": "cb_trap_005",
394
+ "pillar": "underspecified_and_traps",
395
+ "score": 0.0,
396
+ "details": {
397
+ "score": 0.0,
398
+ "refusal_or_trap_detected": false
399
+ }
400
+ },
401
+ {
402
+ "id": "cb_trap_006",
403
+ "pillar": "underspecified_and_traps",
404
+ "score": 100.0,
405
+ "details": {
406
+ "score": 100.0,
407
+ "refusal_or_trap_detected": true
408
+ }
409
+ },
410
+ {
411
+ "id": "cb_trap_007",
412
+ "pillar": "underspecified_and_traps",
413
+ "score": 0.0,
414
+ "details": {
415
+ "score": 0.0,
416
+ "refusal_or_trap_detected": false
417
+ }
418
+ },
419
+ {
420
+ "id": "cb_trap_008",
421
+ "pillar": "underspecified_and_traps",
422
+ "score": 0.0,
423
+ "details": {
424
+ "score": 0.0,
425
+ "refusal_or_trap_detected": false
426
+ }
427
+ },
428
+ {
429
+ "id": "cb_trap_009",
430
+ "pillar": "underspecified_and_traps",
431
+ "score": 100.0,
432
+ "details": {
433
+ "score": 100.0,
434
+ "refusal_or_trap_detected": true
435
+ }
436
+ },
437
+ {
438
+ "id": "cb_trap_010",
439
+ "pillar": "underspecified_and_traps",
440
+ "score": 0.0,
441
+ "details": {
442
+ "score": 0.0,
443
+ "refusal_or_trap_detected": false
444
+ }
445
+ },
446
+ {
447
+ "id": "cb_case_001",
448
+ "pillar": "real_world_case_studies",
449
+ "score": 80.0,
450
+ "details": {
451
+ "score": 80.0
452
+ }
453
+ },
454
+ {
455
+ "id": "cb_case_002",
456
+ "pillar": "real_world_case_studies",
457
+ "score": 80.0,
458
+ "details": {
459
+ "score": 80.0
460
+ }
461
+ },
462
+ {
463
+ "id": "cb_case_003",
464
+ "pillar": "real_world_case_studies",
465
+ "score": 80.0,
466
+ "details": {
467
+ "score": 80.0
468
+ }
469
+ },
470
+ {
471
+ "id": "cb_case_004",
472
+ "pillar": "real_world_case_studies",
473
+ "score": 80.0,
474
+ "details": {
475
+ "score": 80.0
476
+ }
477
+ },
478
+ {
479
+ "id": "cb_case_005",
480
+ "pillar": "real_world_case_studies",
481
+ "score": 80.0,
482
+ "details": {
483
+ "score": 80.0
484
+ }
485
+ },
486
+ {
487
+ "id": "cb_case_006",
488
+ "pillar": "real_world_case_studies",
489
+ "score": 80.0,
490
+ "details": {
491
+ "score": 80.0
492
+ }
493
+ },
494
+ {
495
+ "id": "cb_case_007",
496
+ "pillar": "real_world_case_studies",
497
+ "score": 80.0,
498
+ "details": {
499
+ "score": 80.0
500
+ }
501
+ },
502
+ {
503
+ "id": "cb_case_008",
504
+ "pillar": "real_world_case_studies",
505
+ "score": 30.0,
506
+ "details": {
507
+ "score": 30.0
508
+ }
509
+ }
510
+ ]
511
+ }
controlai_agent/orchestrator.py CHANGED
@@ -626,7 +626,8 @@ class ControlAIAgent:
626
  "[Nise, Control Systems Engineering, p. 583]); never invent a citation and never print a "
627
  "raw filename, file extension, or course code."
628
  )
629
- except Exception:
 
630
  return base_instruction
631
 
632
  def _direct_answer(
@@ -664,7 +665,11 @@ class ControlAIAgent:
664
  )
665
  _, cleaned = _extract_tool_calls(self._generate(rendered, max_tokens=max_tokens))
666
  return cleaned
667
- except Exception:
 
 
 
 
668
  return ""
669
 
670
  def _execute_with_provenance(
@@ -820,6 +825,13 @@ class ControlAIAgent:
820
  # is stripped down to "" and showing final_output would leak a raw
821
  # <tool_call>{...}</tool_call> JSON blob straight into the chat.
822
  final_response = clean_final or self._direct_answer(user_prompt, effective_sys, history)
 
 
 
 
 
 
 
823
  if not final_response:
824
  final_response = "The computational analysis has been completed as detailed above."
825
  return AgentResult(
@@ -1011,6 +1023,13 @@ class ControlAIAgent:
1011
  final_text = clean_synth
1012
  # Strip any lingering raw json
1013
  final_text = re.sub(r"\{\s*[\"']name[\"']\s*:[\s\S]*?\}\s*\}", "", final_text).strip()
 
 
 
 
 
 
 
1014
  if not final_text:
1015
  final_text = "The computational analysis and simulation have been executed successfully as detailed above."
1016
 
 
626
  "[Nise, Control Systems Engineering, p. 583]); never invent a citation and never print a "
627
  "raw filename, file extension, or course code."
628
  )
629
+ except Exception as exc:
630
+ print(f"[_get_grounded_instruction] FAILED, continuing ungrounded: {type(exc).__name__}: {exc}")
631
  return base_instruction
632
 
633
  def _direct_answer(
 
665
  )
666
  _, cleaned = _extract_tool_calls(self._generate(rendered, max_tokens=max_tokens))
667
  return cleaned
668
+ except Exception as exc:
669
+ # This is the last line of defense before the user sees the canned
670
+ # placeholder -- if it fails, that failure must leave a trace
671
+ # instead of vanishing, or a real bug here is undiagnosable.
672
+ print(f"[_direct_answer] FAILED for prompt {user_prompt[:80]!r}: {type(exc).__name__}: {exc}")
673
  return ""
674
 
675
  def _execute_with_provenance(
 
825
  # is stripped down to "" and showing final_output would leak a raw
826
  # <tool_call>{...}</tool_call> JSON blob straight into the chat.
827
  final_response = clean_final or self._direct_answer(user_prompt, effective_sys, history)
828
+ if not final_response:
829
+ # _direct_answer reliably produces real text when tested in
830
+ # isolation, so an empty result here is most likely a transient
831
+ # failure (resource contention, a stray exception) rather than a
832
+ # repeatable one -- one retry clears most of those before giving
833
+ # up and showing the placeholder.
834
+ final_response = self._direct_answer(user_prompt, effective_sys, history)
835
  if not final_response:
836
  final_response = "The computational analysis has been completed as detailed above."
837
  return AgentResult(
 
1023
  final_text = clean_synth
1024
  # Strip any lingering raw json
1025
  final_text = re.sub(r"\{\s*[\"']name[\"']\s*:[\s\S]*?\}\s*\}", "", final_text).strip()
1026
+ if not final_text:
1027
+ # A fresh, tool-free, minimal-context generation reliably produces
1028
+ # real text even when the tool-heavy synthesis turn degenerated --
1029
+ # try it (twice, for a transient failure) before the placeholder.
1030
+ final_text = self._direct_answer(user_prompt, effective_sys, history)
1031
+ if not final_text:
1032
+ final_text = self._direct_answer(user_prompt, effective_sys, history)
1033
  if not final_text:
1034
  final_text = "The computational analysis and simulation have been executed successfully as detailed above."
1035
 
controlai_agent/prompts.py CHANGED
@@ -37,6 +37,6 @@ When answering theoretical principles, derivations, proofs, comparisons, or limi
37
  3. **NO RAW LATEX DOCUMENT TAGS**: NEVER output `\begin{figure}`, `\includegraphics`, `\caption`, `\centering`, `\section*`, or `\end{figure}`. Use standard markdown headers (e.g. `### Section Title`).
38
  4. **MANDATORY TOOL CALL FOR PLOTS**: When asked to plot, visualize, or simulate (Nyquist plot, Bode diagram, Root Locus, Step Response, Phase Portrait), NEVER hallucinate a fake image filename. You MUST explicitly call `execute_python_code` (using `control as ct` or `matplotlib.pyplot`) or `simulate_step_response` to generate and display the real plot!
39
  5. **Deterministic Grounding**: When a tool executes successfully, present the exact numerical results and generated plot.
40
- 6. **No Infinite Retries**: If a tool returns an error, correct the parameters or synthesize the analytical answer directly.
41
  7. **No Forced Citations**: Do NOT cite arbitrary random file paths unless the user explicitly requests literature references.
42
  """
 
37
  3. **NO RAW LATEX DOCUMENT TAGS**: NEVER output `\begin{figure}`, `\includegraphics`, `\caption`, `\centering`, `\section*`, or `\end{figure}`. Use standard markdown headers (e.g. `### Section Title`).
38
  4. **MANDATORY TOOL CALL FOR PLOTS**: When asked to plot, visualize, or simulate (Nyquist plot, Bode diagram, Root Locus, Step Response, Phase Portrait), NEVER hallucinate a fake image filename. You MUST explicitly call `execute_python_code` (using `control as ct` or `matplotlib.pyplot`) or `simulate_step_response` to generate and display the real plot!
39
  5. **Deterministic Grounding**: When a tool executes successfully, present the exact numerical results and generated plot.
40
+ 6. **No Infinite Retries, but READ the error first**: If a tool returns an error, the error text itself is often the entire answer to what's actually being asked -- "uncontrollable", "singular", "poles can't be placed", "not positive semi-definite" name the exact concept the question is testing. Before doing anything else: quote or restate that reason in your final answer as the headline finding, in plain engineering language (e.g. "this system is uncontrollable because..."). Only after stating it should you correct parameters and retry, or move on. NEVER silently produce a numeric-looking final answer (a bare $$K = ...$$, a computed matrix) that routes around an error without ever mentioning what it said -- an ignored error is a worse answer than no answer.
41
  7. **No Forced Citations**: Do NOT cite arbitrary random file paths unless the user explicitly requests literature references.
42
  """