fast-agent-mcp 0.2.44__py3-none-any.whl → 0.2.45__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fast-agent-mcp might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fast-agent-mcp
3
- Version: 0.2.44
3
+ Version: 0.2.45
4
4
  Summary: Define, Prompt and Test MCP enabled Agents and Workflows
5
5
  Author-email: Shaun Smith <fastagent@llmindset.co.uk>
6
6
  License: Apache License
@@ -222,10 +222,10 @@ Requires-Dist: mcp==1.12.0
222
222
  Requires-Dist: openai>=1.93.0
223
223
  Requires-Dist: opentelemetry-distro>=0.50b0
224
224
  Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.29.0
225
- Requires-Dist: opentelemetry-instrumentation-anthropic>=0.40.14; python_version >= '3.10' and python_version < '4.0'
225
+ Requires-Dist: opentelemetry-instrumentation-anthropic>=0.42.0; python_version >= '3.10' and python_version < '4.0'
226
226
  Requires-Dist: opentelemetry-instrumentation-google-genai>=0.2b0
227
- Requires-Dist: opentelemetry-instrumentation-mcp>=0.40.14; python_version >= '3.10' and python_version < '4.0'
228
- Requires-Dist: opentelemetry-instrumentation-openai>=0.40.14; python_version >= '3.10' and python_version < '4.0'
227
+ Requires-Dist: opentelemetry-instrumentation-mcp>=0.42.0; python_version >= '3.10' and python_version < '4.0'
228
+ Requires-Dist: opentelemetry-instrumentation-openai>=0.42.0; python_version >= '3.10' and python_version < '4.0'
229
229
  Requires-Dist: prompt-toolkit>=3.0.50
230
230
  Requires-Dist: pydantic-settings>=2.7.0
231
231
  Requires-Dist: pydantic>=2.10.4
@@ -2,7 +2,7 @@ mcp_agent/__init__.py,sha256=18T0AG0W9sJhTY38O9GFFOzliDhxx9p87CvRyti9zbw,1620
2
2
  mcp_agent/app.py,sha256=3mtHP1nRQcRaKhhxgTmCOv00alh70nT7UxNA8bN47QE,5560
3
3
  mcp_agent/config.py,sha256=RjwgvR-Sys4JIzhNyEsaS_NarCe157RenJLOsioUtDk,18980
4
4
  mcp_agent/console.py,sha256=Gjf2QLFumwG1Lav__c07X_kZxxEUSkzV-1_-YbAwcwo,813
5
- mcp_agent/context.py,sha256=9s1F1-UfcI8rz9Yxm6EXHZ4cInuE_cOl_HFu8N8k3yc,7497
5
+ mcp_agent/context.py,sha256=lzz_Fyf9lz9BBAUt1bRVBlyyHjLkyeuyIziAi4qXYUk,7639
6
6
  mcp_agent/context_dependent.py,sha256=QXfhw3RaQCKfscEEBRGuZ3sdMWqkgShz2jJ1ivGGX1I,1455
7
7
  mcp_agent/event_progress.py,sha256=d7T1hQ1D289MYh2Z5bMPB4JqjGqTOzveJuOHE03B_Xo,3720
8
8
  mcp_agent/mcp_server_registry.py,sha256=lmz-aES-l7Gbg4itDF0iCmpso_KD8bVazVKSVzjwNE4,12398
@@ -12,12 +12,12 @@ mcp_agent/agents/agent.py,sha256=EAYlcP1qqI1D0_CS808I806z1048FBjZQxxpcCZPeIU,315
12
12
  mcp_agent/agents/base_agent.py,sha256=VCBWJ-l1zMQiBuONGzqcbqPUQfXK4oq-pBB5lJrMgQ0,32318
13
13
  mcp_agent/agents/workflow/__init__.py,sha256=HloteEW6kalvgR0XewpiFAqaQlMPlPJYg5p3K33IUzI,25
14
14
  mcp_agent/agents/workflow/chain_agent.py,sha256=eIlImirrSXkqBJmPuAJgOKis81Cl6lZEGM0-6IyaUV8,6105
15
- mcp_agent/agents/workflow/evaluator_optimizer.py,sha256=ysUMGM2NzeCIutgr_vXH6kUPpZMw0cX4J_Wl1r8eT84,13296
15
+ mcp_agent/agents/workflow/evaluator_optimizer.py,sha256=LT81m2B7fxgBZY0CorXFOZJbVhM5fnjDjfrcywO5UrM,12210
16
16
  mcp_agent/agents/workflow/orchestrator_agent.py,sha256=lArV7wHwPYepSuxe0ybTGJRJv85iebRI4ZOY_m8kMZQ,21593
17
17
  mcp_agent/agents/workflow/orchestrator_models.py,sha256=5P_aXADVT4Et8qT4e1cb9RelmHX5dCRrzu8j8T41Kdg,7230
18
18
  mcp_agent/agents/workflow/orchestrator_prompts.py,sha256=EXKEI174sshkZyPPEnWbwwNafzSPuA39MXL7iqG9cWc,9106
19
19
  mcp_agent/agents/workflow/parallel_agent.py,sha256=JaQFp35nmAdoBRLAwx8BfnK7kirVq9PMw24LQ3ZEzoc,7705
20
- mcp_agent/agents/workflow/router_agent.py,sha256=6tvI5D_ssKNZ6-tNxYHmw6r6DAQMYgqz3PZKZz2rC44,9466
20
+ mcp_agent/agents/workflow/router_agent.py,sha256=DYxld96C_xy2TXtZDHPB0CaJqyX-7p0LpsfjkZV6-3o,10517
21
21
  mcp_agent/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  mcp_agent/cli/__main__.py,sha256=KyZnfXkml0KsOnfy8T9JDYNVNynKix9cslwuafmKNbc,1089
23
23
  mcp_agent/cli/constants.py,sha256=KawdkaN289nVB02DKPB4IVUJ8-fohIUD0gLfOp0P7B8,551
@@ -32,9 +32,9 @@ mcp_agent/cli/commands/url_parser.py,sha256=5VdtcHRHzi67YignStVbz7u-rcvNNErw9oJL
32
32
  mcp_agent/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
33
  mcp_agent/core/agent_app.py,sha256=SolGwejEmv9XtsTsmiMkNKPia7RN1VcHXm6JoEo4hvQ,16187
34
34
  mcp_agent/core/agent_types.py,sha256=7zVzAFWjvh5dDV3TuDwmO9LAWmDjYnZd3eeLH-wvvIQ,1705
35
- mcp_agent/core/direct_decorators.py,sha256=sYoEA1EmdyAxTpQwUDUjQYWY73VviM-fBnV1Zv7KfeU,19047
35
+ mcp_agent/core/direct_decorators.py,sha256=zWnfs4sxONw_7UOno62ERb0gezMgV_AIOfxb0tRQg3w,18939
36
36
  mcp_agent/core/direct_factory.py,sha256=d_HvbAxyv2WrM07zyCpLXFVn7eArXk1LZmLKS49hzJo,19537
37
- mcp_agent/core/enhanced_prompt.py,sha256=e3FA0_70kw4HRxXCYQs5sO0qzvsM47pc6UG186okXtk,36324
37
+ mcp_agent/core/enhanced_prompt.py,sha256=ZIeJCeW7rcGMBZ2OdEQwqOmRT0wNSp0hO2-dZRSnnLE,36068
38
38
  mcp_agent/core/error_handling.py,sha256=xoyS2kLe0eG0bj2eSJCJ2odIhGUve2SbDR7jP-A-uRw,624
39
39
  mcp_agent/core/exceptions.py,sha256=ENAD_qGG67foxy6vDkIvc-lgopIUQy6O7zvNPpPXaQg,2289
40
40
  mcp_agent/core/fastagent.py,sha256=qA64fwmJ6TVFEvmj6l-oTWPD28Js5zJvdDnPjH-agzQ,25117
@@ -49,15 +49,15 @@ mcp_agent/executor/executor.py,sha256=E44p6d-o3OMRoP_dNs_cDnyti91LQ3P9eNU88mSi1k
49
49
  mcp_agent/executor/task_registry.py,sha256=PCALFeYtkQrPBg4RBJnlA0aDI8nHclrNkHGUS4kV3W8,1242
50
50
  mcp_agent/executor/workflow_signal.py,sha256=Cg1uZBk3fn8kXhPOg-wINNuVaf3v9pvLD6NbqWy5Z6E,11142
51
51
  mcp_agent/human_input/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
- mcp_agent/human_input/elicitation_form.py,sha256=s9rrX-qKxKZ91Uf-wjmrRIKgqDP-gdn6_1xOwizyJno,28158
52
+ mcp_agent/human_input/elicitation_form.py,sha256=VgS-DXlwYTU4qDntok4Pqt8qfl1w_-Xby5PTlfNerug,28324
53
53
  mcp_agent/human_input/elicitation_forms.py,sha256=w8XQ1GfZX8Jw-VB4jnDI0Im4mF-T9Ts8mT2zRZBtL6M,3824
54
54
  mcp_agent/human_input/elicitation_handler.py,sha256=YfVhIhSBc9wuszPS4zoHho4n1pwmIoq13huN4MSRkIs,3305
55
55
  mcp_agent/human_input/elicitation_state.py,sha256=Unl9uhEybUqACCUimnETdfUprJNpYDMq3DdbbHw5oAw,1175
56
56
  mcp_agent/human_input/handler.py,sha256=s712Z5ssTCwjL9-VKoIdP5CtgMh43YvepynYisiWTTA,3144
57
57
  mcp_agent/human_input/types.py,sha256=RtWBOVzy8vnYoQrc36jRLn8z8N3C4pDPMBN5vF6qM5Y,1476
58
58
  mcp_agent/llm/__init__.py,sha256=d8zgwG-bRFuwiMNMYkywg_qytk4P8lawyld_meuUmHI,68
59
- mcp_agent/llm/augmented_llm.py,sha256=FwH3nFwtqX9JJiwsHWvBCwDRooAMUhVM258-rUMheRI,27568
60
- mcp_agent/llm/augmented_llm_passthrough.py,sha256=-Pu_cW5p7gKyc6E7OT7lG642RyAFkv-uj6U-JwKpOPE,9421
59
+ mcp_agent/llm/augmented_llm.py,sha256=UiOTOAaNVnc03yuLBbBZstnaOG6Q8XLwkyiHPA3yxEk,27434
60
+ mcp_agent/llm/augmented_llm_passthrough.py,sha256=bu0DJkjyFPzBZEU7f6MHnOp__9BCYl56tFd5nZVhSeY,8808
61
61
  mcp_agent/llm/augmented_llm_playback.py,sha256=BQeBXRpO-xGAY9wIJxyde6xpHmZEdQPLd32frF8t3QQ,4916
62
62
  mcp_agent/llm/augmented_llm_silent.py,sha256=IUnK_1Byy4D9TG0Pj46LFeNezgSTQ8d6MQIHWAImBwE,1846
63
63
  mcp_agent/llm/augmented_llm_slow.py,sha256=DDSD8bL2flmQrVHZm-UDs7sR8aHRWkDOcOW-mX_GPok,2067
@@ -73,7 +73,7 @@ mcp_agent/llm/usage_tracking.py,sha256=rF6v8QQDam8QbvlP4jzHljKqvuNHExeYDLkUMI86c
73
73
  mcp_agent/llm/providers/__init__.py,sha256=heVxtmuqFJOnjjxHz4bWSqTAxXoN1E8twC_gQ_yJpHk,265
74
74
  mcp_agent/llm/providers/anthropic_utils.py,sha256=vYDN5G5jKMhD2CQg8veJYab7tvvzYkDMq8M1g_hUAQg,3275
75
75
  mcp_agent/llm/providers/augmented_llm_aliyun.py,sha256=XylkJKZ9theSVUxJKOZkf1244hgzng4Ng4Dr209Qb-w,1101
76
- mcp_agent/llm/providers/augmented_llm_anthropic.py,sha256=kmzJ_45eMerUkGZ7Mzy544SLpxcUbfOnAr0h_mHktcs,23980
76
+ mcp_agent/llm/providers/augmented_llm_anthropic.py,sha256=l32pJ3yo0oVv7ELaoi1aCSV2TDGhBuYf0AKwW0lgjPs,30359
77
77
  mcp_agent/llm/providers/augmented_llm_azure.py,sha256=sBVWgY88F4OsdRSHl71BAT2p3XPvuZp844z1ubwcV7U,6098
78
78
  mcp_agent/llm/providers/augmented_llm_bedrock.py,sha256=nfby1udL07zPTOLlN_tFxd1h0JRioo2oIW7v4iP4Bnk,82267
79
79
  mcp_agent/llm/providers/augmented_llm_deepseek.py,sha256=zI9a90dwT4r6E1f_xp4K50Cj9sD7y7kNRgjo0s1pd5w,3804
@@ -150,20 +150,20 @@ mcp_agent/resources/examples/researcher/researcher-eval.py,sha256=CR9m4lyoXijS1w
150
150
  mcp_agent/resources/examples/researcher/researcher-imp.py,sha256=oJxSVnLbZfIn71QbQR1E6j_m_UBrOOGP4SVljXErHLQ,7879
151
151
  mcp_agent/resources/examples/researcher/researcher.py,sha256=SZfExi-FfwYETzGt2O3caS3L5E6EemV3IUrJHyzZqHI,1333
152
152
  mcp_agent/resources/examples/workflows/chaining.py,sha256=tY0kA0U8s2rceAO4ogZFtpQEkiUWcrYnYDgHu_-4G50,889
153
- mcp_agent/resources/examples/workflows/evaluator.py,sha256=pZckGkulwZguSkEaQFejXyZQm143LU_sHqUtCB_m_dA,3096
153
+ mcp_agent/resources/examples/workflows/evaluator.py,sha256=XJXrk5r1hrJzfZAMtQ7WIggy6qPttMJG1yqxYELO7C4,3101
154
154
  mcp_agent/resources/examples/workflows/fastagent.config.yaml,sha256=qaxk-p7Pl7JepdL3a7BTl0CIp4LHCXies7pFdVWS9xk,783
155
155
  mcp_agent/resources/examples/workflows/graded_report.md,sha256=QVF38xEtDIO1a2P-xv2hlBEG6KKYughtFkzDhY2NpzE,2726
156
156
  mcp_agent/resources/examples/workflows/human_input.py,sha256=_I6nS6xYo8IHAmvzsUYOxqVGb4G6BTyJXPAmS3fNcBU,621
157
157
  mcp_agent/resources/examples/workflows/orchestrator.py,sha256=5Jxfe0s3ai4qRpVXm3DaqtkDvjeWD2CcysW36J6JA-M,2521
158
158
  mcp_agent/resources/examples/workflows/parallel.py,sha256=OTWhX33uum_rspAzrSf3uCTj7b67Kvw3UUU8nsosz4g,1839
159
- mcp_agent/resources/examples/workflows/router.py,sha256=56FX7JhZ6ERafWGoVhspoalT1kkfkfbb5P-6MTFDamA,2032
159
+ mcp_agent/resources/examples/workflows/router.py,sha256=vmw8aBitByi5PRFIvjYWWn2GUtAPiwzl7juN3kmRqvw,2033
160
160
  mcp_agent/resources/examples/workflows/short_story.md,sha256=XN9I2kzCcMmke3dE5F2lyRH5iFUZUQ8Sy-hS3rm_Wlc,1153
161
161
  mcp_agent/resources/examples/workflows/short_story.txt,sha256=X3y_1AyhLFN2AKzCKvucJtDgAFIJfnlbsbGZO5bBWu0,1187
162
162
  mcp_agent/tools/tool_definition.py,sha256=L3Pxl-uLEXqlVoo-bYuFTFALeI-2pIU44YgFhsTKEtM,398
163
- mcp_agent/ui/console_display.py,sha256=2G8hv4Ig70IHCHvw7FK-ksDcp9UHqbUYU-Y64_j0Nuk,25826
163
+ mcp_agent/ui/console_display.py,sha256=cv-dvpJT7-zd7d8nqlcDLKfYg9_pDEQQOt7rGnDvj54,26057
164
164
  mcp_agent/ui/console_display_legacy.py,sha256=sm2v61-IPVafbF7uUaOyhO2tW_zgFWOjNS83IEWqGgI,14931
165
- fast_agent_mcp-0.2.44.dist-info/METADATA,sha256=lvZvAbFvs2tbNpHi35Cbh_PgeyNtWFTnI8Fa8RHpXyQ,31044
166
- fast_agent_mcp-0.2.44.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
167
- fast_agent_mcp-0.2.44.dist-info/entry_points.txt,sha256=QaX5kLdI0VdMPRdPUF1nkG_WdLUTNjp_icW6e3EhNYU,232
168
- fast_agent_mcp-0.2.44.dist-info/licenses/LICENSE,sha256=Gx1L3axA4PnuK4FxsbX87jQ1opoOkSFfHHSytW6wLUU,10935
169
- fast_agent_mcp-0.2.44.dist-info/RECORD,,
165
+ fast_agent_mcp-0.2.45.dist-info/METADATA,sha256=xdue1zGI50YjprDUaZUnOlO4vW-S02iGdK6dKh5Cq74,31041
166
+ fast_agent_mcp-0.2.45.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
167
+ fast_agent_mcp-0.2.45.dist-info/entry_points.txt,sha256=QaX5kLdI0VdMPRdPUF1nkG_WdLUTNjp_icW6e3EhNYU,232
168
+ fast_agent_mcp-0.2.45.dist-info/licenses/LICENSE,sha256=Gx1L3axA4PnuK4FxsbX87jQ1opoOkSFfHHSytW6wLUU,10935
169
+ fast_agent_mcp-0.2.45.dist-info/RECORD,,
@@ -33,16 +33,14 @@ class QualityRating(str, Enum):
33
33
  GOOD = "GOOD" # Minor improvements possible
34
34
  EXCELLENT = "EXCELLENT" # No improvements needed
35
35
 
36
- # Map string values to integer values for comparisons
37
- @property
38
- def value(self) -> int:
39
- """Convert string enum values to integers for comparison."""
40
- return {
41
- "POOR": 0,
42
- "FAIR": 1,
43
- "GOOD": 2,
44
- "EXCELLENT": 3,
45
- }[self._value_]
36
+
37
+ # Separate mapping for quality ratings to numerical values
38
+ QUALITY_RATING_VALUES = {
39
+ QualityRating.POOR: 0,
40
+ QualityRating.FAIR: 1,
41
+ QualityRating.GOOD: 2,
42
+ QualityRating.EXCELLENT: 3,
43
+ }
46
44
 
47
45
 
48
46
  class EvaluationResult(BaseModel):
@@ -140,7 +138,7 @@ class EvaluatorOptimizerAgent(BaseAgent):
140
138
 
141
139
  # Evaluate current response
142
140
  eval_prompt = self._build_eval_prompt(
143
- request=request, response=response.all_text(), iteration=refinement_count
141
+ request=request, response=response.last_text(), iteration=refinement_count
144
142
  )
145
143
 
146
144
  # Create evaluation message and get structured evaluation result
@@ -171,7 +169,7 @@ class EvaluatorOptimizerAgent(BaseAgent):
171
169
  logger.debug(f"Evaluation result: {evaluation_result.rating}")
172
170
 
173
171
  # Track best response based on rating
174
- if evaluation_result.rating.value > best_rating.value:
172
+ if QUALITY_RATING_VALUES[evaluation_result.rating] > QUALITY_RATING_VALUES[best_rating]:
175
173
  best_rating = evaluation_result.rating
176
174
  best_response = response
177
175
  logger.debug(f"New best response (rating: {best_rating})")
@@ -183,14 +181,17 @@ class EvaluatorOptimizerAgent(BaseAgent):
183
181
  best_response = response
184
182
  break
185
183
 
186
- if evaluation_result.rating.value >= self.min_rating.value:
184
+ if (
185
+ QUALITY_RATING_VALUES[evaluation_result.rating]
186
+ >= QUALITY_RATING_VALUES[self.min_rating]
187
+ ):
187
188
  logger.debug(f"Acceptable quality reached ({evaluation_result.rating})")
188
189
  break
189
190
 
190
191
  # Generate refined response
191
192
  refinement_prompt = self._build_refinement_prompt(
192
193
  request=request,
193
- response=response.all_text(),
194
+ response=response.last_text(), ## only if there is no history?
194
195
  feedback=evaluation_result,
195
196
  iteration=refinement_count,
196
197
  )
@@ -270,48 +271,21 @@ class EvaluatorOptimizerAgent(BaseAgent):
270
271
  return f"""
271
272
  You are an expert evaluator for content quality. Your task is to evaluate a response against the user's original request.
272
273
 
273
- Evaluate the response for iteration {iteration + 1} and provide structured feedback on its quality and areas for improvement.
274
+ Evaluate the response for iteration {iteration + 1} and provide feedback on its quality and areas for improvement.
274
275
 
276
+ ```
275
277
  <fastagent:data>
276
- <fastagent:request>
278
+ <fastagent:request>
277
279
  {request}
278
- </fastagent:request>
280
+ </fastagent:request>
279
281
 
280
- <fastagent:response>
282
+ <fastagent:response>
281
283
  {response}
282
- </fastagent:response>
284
+ </fastagent:response>
283
285
  </fastagent:data>
284
286
 
285
- <fastagent:instruction>
286
- Your response MUST be valid JSON matching this exact format (no other text, markdown, or explanation):
287
-
288
- {{
289
- "rating": "RATING",
290
- "feedback": "DETAILED FEEDBACK",
291
- "needs_improvement": BOOLEAN,
292
- "focus_areas": ["FOCUS_AREA_1", "FOCUS_AREA_2", "FOCUS_AREA_3"]
293
- }}
294
-
295
- Where:
296
- - RATING: Must be one of: "EXCELLENT", "GOOD", "FAIR", or "POOR"
297
- - EXCELLENT: No improvements needed
298
- - GOOD: Only minor improvements possible
299
- - FAIR: Several improvements needed
300
- - POOR: Major improvements needed
301
- - DETAILED FEEDBACK: Specific, actionable feedback (as a single string)
302
- - BOOLEAN: true or false (lowercase, no quotes) indicating if further improvement is needed
303
- - FOCUS_AREAS: Array of 1-3 specific areas to focus on (empty array if no improvement needed)
304
-
305
- Example of valid response (DO NOT include the triple backticks in your response):
306
- {{
307
- "rating": "GOOD",
308
- "feedback": "The response is clear but could use more supporting evidence.",
309
- "needs_improvement": true,
310
- "focus_areas": ["Add more examples", "Include data points"]
311
- }}
312
-
313
- IMPORTANT: Your response should be ONLY the JSON object without any code fences, explanations, or other text.
314
- </fastagent:instruction>
287
+ ```
288
+
315
289
  """
316
290
 
317
291
  def _build_refinement_prompt(
@@ -333,28 +307,27 @@ IMPORTANT: Your response should be ONLY the JSON object without any code fences,
333
307
  Returns:
334
308
  Formatted refinement prompt
335
309
  """
336
- focus_areas = ", ".join(feedback.focus_areas) if feedback.focus_areas else "None specified"
310
+
311
+ # Format focus areas as bulleted list with each item on a separate line
312
+ if feedback.focus_areas:
313
+ focus_areas = "\n".join(f" * {area}" for area in feedback.focus_areas)
314
+ else:
315
+ focus_areas = "None specified"
337
316
 
338
317
  return f"""
339
- You are tasked with improving a response based on expert feedback. This is iteration {iteration + 1} of the refinement process.
318
+ You are tasked with improving your previous response based on expert feedback. This is iteration {iteration + 1} of the refinement process.
340
319
 
341
320
  Your goal is to address all feedback points while maintaining accuracy and relevance to the original request.
342
321
 
343
- <fastagent:data>
344
- <fastagent:request>
345
- {request}
346
- </fastagent:request>
347
-
348
- <fastagent:previous-response>
349
- {response}
350
- </fastagent:previous-response>
322
+ ```
351
323
 
352
324
  <fastagent:feedback>
353
- <rating>{feedback.rating}</rating>
354
- <details>{feedback.feedback}</details>
355
- <focus-areas>{focus_areas}</focus-areas>
325
+ <rating>{feedback.rating.name}</rating>
326
+ <details>{feedback.feedback}</details>
327
+ <focus-areas>
328
+ {focus_areas}
329
+ </focus-areas>
356
330
  </fastagent:feedback>
357
- </fastagent:data>
358
331
 
359
332
  <fastagent:instruction>
360
333
  Create an improved version of the response that:
@@ -365,4 +338,7 @@ Create an improved version of the response that:
365
338
 
366
339
  Provide your complete improved response without explanations or commentary.
367
340
  </fastagent:instruction>
341
+
342
+ ```
343
+
368
344
  """
@@ -39,7 +39,7 @@ Follow these guidelines:
39
39
  """
40
40
 
41
41
  # Default routing instruction with placeholders for context (AgentCard JSON)
42
- DEFAULT_ROUTING_INSTRUCTION = """
42
+ ROUTING_AGENT_INSTRUCTION = """
43
43
  Select from the following agents to handle the request:
44
44
  <fastagent:agents>
45
45
  [
@@ -100,7 +100,7 @@ class RouterAgent(BaseAgent):
100
100
  self.routing_instruction = routing_instruction
101
101
  self.agent_map = {agent.name: agent for agent in agents}
102
102
 
103
- # Set up base router request parameters
103
+ # Set up base router request parameters with just the base instruction for now
104
104
  base_params = {"systemPrompt": ROUTING_SYSTEM_INSTRUCTION, "use_history": False}
105
105
 
106
106
  if default_request_params:
@@ -120,6 +120,18 @@ class RouterAgent(BaseAgent):
120
120
  if not getattr(agent, "initialized", False):
121
121
  await agent.initialize()
122
122
 
123
+ complete_routing_instruction = await self._generate_routing_instruction(
124
+ self.agents, self.routing_instruction
125
+ )
126
+
127
+ # Update the system prompt to include the routing instruction with agent cards
128
+ combined_system_prompt = (
129
+ ROUTING_SYSTEM_INSTRUCTION + "\n\n" + complete_routing_instruction
130
+ )
131
+ self._default_request_params.systemPrompt = combined_system_prompt
132
+ self.instruction = combined_system_prompt
133
+ self._routing_instruction_generated = True
134
+
123
135
  self.initialized = True
124
136
 
125
137
  async def shutdown(self) -> None:
@@ -133,6 +145,36 @@ class RouterAgent(BaseAgent):
133
145
  except Exception as e:
134
146
  logger.warning(f"Error shutting down agent: {str(e)}")
135
147
 
148
+ @staticmethod
149
+ async def _generate_routing_instruction(
150
+ agents: List[Agent], routing_instruction: Optional[str] = None
151
+ ) -> str:
152
+ """
153
+ Generate the complete routing instruction with agent cards.
154
+
155
+ Args:
156
+ agents: List of agents to include in routing instruction
157
+ routing_instruction: Optional custom routing instruction template
158
+
159
+ Returns:
160
+ Complete routing instruction with agent cards formatted
161
+ """
162
+ # Generate agent descriptions
163
+ agent_descriptions = []
164
+ for agent in agents:
165
+ agent_card: AgentCard = await agent.agent_card()
166
+ agent_descriptions.append(
167
+ agent_card.model_dump_json(
168
+ include={"name", "description", "skills"}, exclude_none=True
169
+ )
170
+ )
171
+
172
+ context = ",\n".join(agent_descriptions)
173
+
174
+ # Format the routing instruction
175
+ instruction_template = routing_instruction or ROUTING_AGENT_INSTRUCTION
176
+ return instruction_template.format(context=context)
177
+
136
178
  async def attach_llm(
137
179
  self,
138
180
  llm_factory: type[AugmentedLLMProtocol] | Callable[..., AugmentedLLMProtocol],
@@ -227,27 +269,10 @@ class RouterAgent(BaseAgent):
227
269
  agent=self.agents[0].name, confidence="high", reasoning="Only one agent available"
228
270
  ), None
229
271
 
230
- # Generate agent descriptions for the context
231
- agent_descriptions = []
232
- for agent in self.agents:
233
- agent_card: AgentCard = await agent.agent_card()
234
- agent_descriptions.append(
235
- agent_card.model_dump_json(
236
- include={"name", "description", "skills"}, exclude_none=True
237
- )
238
- )
239
-
240
- context = ",\n".join(agent_descriptions)
241
-
242
- # Format the routing prompt
243
- routing_instruction = self.routing_instruction or DEFAULT_ROUTING_INSTRUCTION
244
- routing_instruction = routing_instruction.format(context=context)
245
-
246
272
  assert self._llm
247
- mutated = message.model_copy(deep=True)
248
- mutated.add_text(routing_instruction)
273
+ # No need to add routing instruction here - it's already in the system prompt
249
274
  response, _ = await self._llm.structured(
250
- [mutated],
275
+ [message],
251
276
  RoutingResponse,
252
277
  self._default_request_params,
253
278
  )
mcp_agent/context.py CHANGED
@@ -10,7 +10,11 @@ from typing import TYPE_CHECKING, Any, Optional, Union
10
10
  from mcp import ServerSession
11
11
  from opentelemetry import trace
12
12
  from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
13
+
14
+ # from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
13
15
  from opentelemetry.instrumentation.google_genai import GoogleGenAiSdkInstrumentor
16
+
17
+ # from opentelemetry.instrumentation.mcp import McpInstrumentor
14
18
  from opentelemetry.instrumentation.openai import OpenAIInstrumentor
15
19
  from opentelemetry.propagate import set_global_textmap
16
20
  from opentelemetry.sdk.resources import Resource
@@ -23,6 +23,9 @@ from typing import (
23
23
  from mcp.client.session import ElicitationFnT
24
24
 
25
25
  from mcp_agent.agents.agent import AgentConfig
26
+ from mcp_agent.agents.workflow.router_agent import (
27
+ ROUTING_SYSTEM_INSTRUCTION,
28
+ )
26
29
  from mcp_agent.core.agent_types import AgentType
27
30
  from mcp_agent.core.request_params import RequestParams
28
31
 
@@ -397,10 +400,6 @@ def router(
397
400
  Returns:
398
401
  A decorator that registers the router with proper type annotations
399
402
  """
400
- default_instruction = """
401
- You are a router that determines which specialized agent should handle a given query.
402
- Analyze the query and select the most appropriate agent to handle it.
403
- """
404
403
 
405
404
  return cast(
406
405
  "Callable[[AgentCallable[P, R]], DecoratedRouterProtocol[P, R]]",
@@ -408,7 +407,7 @@ def router(
408
407
  self,
409
408
  AgentType.ROUTER,
410
409
  name=name,
411
- instruction=instruction or default_instruction,
410
+ instruction=instruction or ROUTING_SYSTEM_INSTRUCTION,
412
411
  servers=servers,
413
412
  model=model,
414
413
  use_history=use_history,
@@ -511,13 +511,12 @@ def create_keybindings(on_toggle_multiline=None, app=None, agent_provider=None,
511
511
  rich_print("\n[green]✓ Copied to clipboard[/green]")
512
512
  return
513
513
 
514
- rich_print("\n[yellow]No assistant messages found[/yellow]")
515
514
  else:
516
- rich_print("\n[yellow]No message history available[/yellow]")
517
- except Exception as e:
518
- rich_print(f"\n[red]Error copying: {e}[/red]")
515
+ pass
516
+ except Exception:
517
+ pass
519
518
  else:
520
- rich_print("[yellow]Clipboard copy not available in this context[/yellow]")
519
+ pass
521
520
 
522
521
  return kb
523
522
 
@@ -5,6 +5,7 @@ from typing import Any, Dict, Optional
5
5
 
6
6
  from mcp.types import ElicitRequestedSchema
7
7
  from prompt_toolkit import Application
8
+ from prompt_toolkit.application.current import get_app
8
9
  from prompt_toolkit.buffer import Buffer
9
10
  from prompt_toolkit.filters import Condition
10
11
  from prompt_toolkit.formatted_text import FormattedText
@@ -272,29 +273,32 @@ class ElicitationForm:
272
273
  keep_focused_window_visible=True,
273
274
  )
274
275
 
275
- # Create title bar manually
276
- title_bar = Window(
277
- FormattedTextControl(FormattedText([("class:title", "Elicitation Request")])),
278
- height=1,
279
- style="class:dialog.title",
280
- )
281
-
282
- # Combine title, sticky headers, and scrollable content
276
+ # Combine sticky headers and scrollable content (no separate title bar needed)
283
277
  full_content = HSplit(
284
278
  [
285
- title_bar,
286
- Window(height=1), # Spacing after title
279
+ Window(height=1), # Top spacing
287
280
  sticky_headers, # Headers stay fixed at top
288
281
  scrollable_content, # Form fields can scroll
289
282
  ]
290
283
  )
291
284
 
292
- # Create dialog frame manually to avoid Dialog's internal scrolling
285
+ # Create dialog frame with title
293
286
  dialog = Frame(
294
287
  body=full_content,
288
+ title="Elicitation Request",
295
289
  style="class:dialog",
296
290
  )
297
291
 
292
+ # Apply width constraints by putting Frame in VSplit with flexible spacers
293
+ # This prevents console display interference and constrains the Frame border
294
+ constrained_dialog = VSplit(
295
+ [
296
+ Window(width=10), # Smaller left spacer
297
+ dialog,
298
+ Window(width=10), # Smaller right spacer
299
+ ]
300
+ )
301
+
298
302
  # Key bindings
299
303
  kb = KeyBindings()
300
304
 
@@ -370,7 +374,7 @@ class ElicitationForm:
370
374
  # Add toolbar to the layout
371
375
  root_layout = HSplit(
372
376
  [
373
- dialog, # The main dialog
377
+ constrained_dialog, # The width-constrained dialog
374
378
  self._toolbar_window,
375
379
  ]
376
380
  )
@@ -588,7 +592,6 @@ class ElicitationForm:
588
592
 
589
593
  def _is_in_multiline_field(self) -> bool:
590
594
  """Check if currently focused field is a multiline field."""
591
- from prompt_toolkit.application.current import get_app
592
595
 
593
596
  focused = get_app().layout.current_control
594
597
 
@@ -212,8 +212,6 @@ class AugmentedLLM(ContextDependent, AugmentedLLMProtocol, Generic[MessageParamT
212
212
  # note - check changes here are mirrored in structured(). i've thought hard about
213
213
  # a strategy to reduce duplication etc, but aiming for simple but imperfect for the moment
214
214
 
215
- # We never expect this for structured() calls - this is for interactive use - developers
216
- # can do this programatically
217
215
  # TODO -- create a "fast-agent" control role rather than magic strings
218
216
 
219
217
  if multipart_messages[-1].first_text().startswith("***SAVE_HISTORY"):
@@ -235,6 +233,7 @@ class AugmentedLLM(ContextDependent, AugmentedLLMProtocol, Generic[MessageParamT
235
233
 
236
234
  # add generic error and termination reason handling/rollback
237
235
  self._message_history.append(assistant_response)
236
+
238
237
  return assistant_response
239
238
 
240
239
  @abstractmethod
@@ -164,15 +164,10 @@ class PassthroughLLM(AugmentedLLM):
164
164
  request_params: RequestParams | None = None,
165
165
  is_template: bool = False,
166
166
  ) -> PromptMessageMultipart:
167
- print(
168
- f"DEBUG: PassthroughLLM _apply_prompt_provider_specific called with {len(multipart_messages)} messages, is_template={is_template}"
169
- )
170
-
171
167
  # Add messages to history with proper is_prompt flag
172
168
  self.history.extend(multipart_messages, is_prompt=is_template)
173
169
 
174
170
  last_message = multipart_messages[-1]
175
- print(f"DEBUG: Last message role: {last_message.role}, text: '{last_message.first_text()}'")
176
171
 
177
172
  if self.is_tool_call(last_message):
178
173
  result = Prompt.assistant(await self.generate_str(last_message.first_text()))
@@ -209,14 +204,8 @@ class PassthroughLLM(AugmentedLLM):
209
204
  else:
210
205
  # TODO -- improve when we support Audio/Multimodal gen models e.g. gemini . This should really just return the input as "assistant"...
211
206
  concatenated: str = "\n".join(message.all_text() for message in multipart_messages)
212
- print(
213
- f"DEBUG: PassthroughLLM generating response: '{concatenated}' (is_template={is_template})"
214
- )
215
207
  await self.show_assistant_message(concatenated)
216
208
  result = Prompt.assistant(concatenated)
217
- print(f"DEBUG: PassthroughLLM created result: {result}")
218
- print(f"DEBUG: Result first_text(): {result.first_text()}")
219
- print(f"DEBUG: Result content: {result.content}")
220
209
 
221
210
  # Track usage for this passthrough "turn"
222
211
  try:
@@ -1,4 +1,5 @@
1
- from typing import TYPE_CHECKING, List, Tuple, Type
1
+ import json
2
+ from typing import TYPE_CHECKING, Any, List, Tuple, Type
2
3
 
3
4
  from mcp.types import TextContent
4
5
 
@@ -33,6 +34,7 @@ from anthropic.types import (
33
34
  from mcp.types import (
34
35
  CallToolRequest,
35
36
  CallToolRequestParams,
37
+ CallToolResult,
36
38
  ContentBlock,
37
39
  )
38
40
  from rich.text import Text
@@ -99,6 +101,184 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
99
101
  cache_mode = self.context.config.anthropic.cache_mode
100
102
  return cache_mode
101
103
 
104
+ async def _prepare_tools(self, structured_model: Type[ModelT] | None = None) -> List[ToolParam]:
105
+ """Prepare tools based on whether we're in structured output mode."""
106
+ if structured_model:
107
+ # JSON mode - create a single tool for structured output
108
+ return [
109
+ ToolParam(
110
+ name="return_structured_output",
111
+ description="Return the response in the required JSON format",
112
+ input_schema=structured_model.model_json_schema(),
113
+ )
114
+ ]
115
+ else:
116
+ # Regular mode - use tools from aggregator
117
+ tool_list: ListToolsResult = await self.aggregator.list_tools()
118
+ return [
119
+ ToolParam(
120
+ name=tool.name,
121
+ description=tool.description or "",
122
+ input_schema=tool.inputSchema,
123
+ )
124
+ for tool in tool_list.tools
125
+ ]
126
+
127
+ def _apply_system_cache(self, base_args: dict, cache_mode: str) -> None:
128
+ """Apply cache control to system prompt if cache mode allows it."""
129
+ if cache_mode != "off" and base_args["system"]:
130
+ if isinstance(base_args["system"], str):
131
+ base_args["system"] = [
132
+ {
133
+ "type": "text",
134
+ "text": base_args["system"],
135
+ "cache_control": {"type": "ephemeral"},
136
+ }
137
+ ]
138
+ self.logger.debug(
139
+ "Applied cache_control to system prompt (caches tools+system in one block)"
140
+ )
141
+ else:
142
+ self.logger.debug(f"System prompt is not a string: {type(base_args['system'])}")
143
+
144
+ async def _apply_conversation_cache(self, messages: List[MessageParam], cache_mode: str) -> int:
145
+ """Apply conversation caching if in auto mode. Returns number of cache blocks applied."""
146
+ applied_count = 0
147
+ if cache_mode == "auto" and self.history.should_apply_conversation_cache():
148
+ cache_updates = self.history.get_conversation_cache_updates()
149
+
150
+ # Remove cache control from old positions
151
+ if cache_updates["remove"]:
152
+ self.history.remove_cache_control_from_messages(messages, cache_updates["remove"])
153
+ self.logger.debug(
154
+ f"Removed conversation cache_control from positions {cache_updates['remove']}"
155
+ )
156
+
157
+ # Add cache control to new positions
158
+ if cache_updates["add"]:
159
+ applied_count = self.history.add_cache_control_to_messages(
160
+ messages, cache_updates["add"]
161
+ )
162
+ if applied_count > 0:
163
+ self.history.apply_conversation_cache_updates(cache_updates)
164
+ self.logger.debug(
165
+ f"Applied conversation cache_control to positions {cache_updates['add']} ({applied_count} blocks)"
166
+ )
167
+ else:
168
+ self.logger.debug(
169
+ f"Failed to apply conversation cache_control to positions {cache_updates['add']}"
170
+ )
171
+
172
+ return applied_count
173
+
174
+ async def _process_structured_output(
175
+ self,
176
+ content_block: Any,
177
+ ) -> Tuple[str, CallToolResult, TextContent]:
178
+ """
179
+ Process a structured output tool call from Anthropic.
180
+
181
+ This handles the special case where Anthropic's model was forced to use
182
+ a 'return_structured_output' tool via tool_choice. The tool input contains
183
+ the JSON data we want, so we extract it and format it for display.
184
+
185
+ Even though we don't call an external tool, we must create a CallToolResult
186
+ to satisfy Anthropic's API requirement that every tool_use has a corresponding
187
+ tool_result in the next message.
188
+
189
+ Returns:
190
+ Tuple of (tool_use_id, tool_result, content_block) for the structured data
191
+ """
192
+ tool_args = content_block.input
193
+ tool_use_id = content_block.id
194
+
195
+ # Show the formatted JSON response to the user
196
+ json_response = json.dumps(tool_args, indent=2)
197
+ await self.show_assistant_message(json_response)
198
+
199
+ # Create the content for responses
200
+ structured_content = TextContent(type="text", text=json.dumps(tool_args))
201
+
202
+ # Create a CallToolResult to satisfy Anthropic's API requirements
203
+ # This represents the "result" of our structured output "tool"
204
+ tool_result = CallToolResult(isError=False, content=[structured_content])
205
+
206
+ return tool_use_id, tool_result, structured_content
207
+
208
+ async def _process_regular_tool_call(
209
+ self,
210
+ content_block: Any,
211
+ available_tools: List[ToolParam],
212
+ is_first_tool: bool,
213
+ message_text: str | Text,
214
+ ) -> Tuple[str, CallToolResult]:
215
+ """
216
+ Process a regular MCP tool call.
217
+
218
+ This handles actual tool execution via the MCP aggregator.
219
+ """
220
+ tool_name = content_block.name
221
+ tool_args = content_block.input
222
+ tool_use_id = content_block.id
223
+
224
+ if is_first_tool:
225
+ await self.show_assistant_message(message_text, tool_name)
226
+
227
+ self.show_tool_call(available_tools, tool_name, tool_args)
228
+ tool_call_request = CallToolRequest(
229
+ method="tools/call",
230
+ params=CallToolRequestParams(name=tool_name, arguments=tool_args),
231
+ )
232
+ result = await self.call_tool(request=tool_call_request, tool_call_id=tool_use_id)
233
+ self.show_tool_result(result)
234
+ return tool_use_id, result
235
+
236
+ async def _process_tool_calls(
237
+ self,
238
+ tool_uses: List[Any],
239
+ available_tools: List[ToolParam],
240
+ message_text: str | Text,
241
+ structured_model: Type[ModelT] | None = None,
242
+ ) -> Tuple[List[Tuple[str, CallToolResult]], List[ContentBlock]]:
243
+ """
244
+ Process tool calls, handling both structured output and regular MCP tools.
245
+
246
+ For structured output mode:
247
+ - Extracts JSON data from the forced 'return_structured_output' tool
248
+ - Does NOT create fake CallToolResults
249
+ - Returns the JSON content directly
250
+
251
+ For regular tools:
252
+ - Calls actual MCP tools via the aggregator
253
+ - Returns real CallToolResults
254
+ """
255
+ tool_results = []
256
+ responses = []
257
+
258
+ for tool_idx, content_block in enumerate(tool_uses):
259
+ tool_name = content_block.name
260
+ is_first_tool = tool_idx == 0
261
+
262
+ if tool_name == "return_structured_output" and structured_model:
263
+ # Structured output: extract JSON, don't call external tools
264
+ (
265
+ tool_use_id,
266
+ tool_result,
267
+ structured_content,
268
+ ) = await self._process_structured_output(content_block)
269
+ responses.append(structured_content)
270
+ # Add to tool_results to satisfy Anthropic's API requirement for tool_result messages
271
+ tool_results.append((tool_use_id, tool_result))
272
+ else:
273
+ # Regular tool: call external MCP tool
274
+ tool_use_id, tool_result = await self._process_regular_tool_call(
275
+ content_block, available_tools, is_first_tool, message_text
276
+ )
277
+ tool_results.append((tool_use_id, tool_result))
278
+ responses.extend(tool_result.content)
279
+
280
+ return tool_results, responses
281
+
102
282
  async def _process_stream(self, stream: AsyncMessageStream, model: str) -> Message:
103
283
  """Process the streaming response and display real-time token usage."""
104
284
  # Track estimated output tokens by counting text chunks
@@ -150,6 +330,7 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
150
330
  self,
151
331
  message_param,
152
332
  request_params: RequestParams | None = None,
333
+ structured_model: Type[ModelT] | None = None,
153
334
  ) -> list[ContentBlock]:
154
335
  """
155
336
  Process a query using an LLM and available tools.
@@ -181,15 +362,7 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
181
362
  cache_mode = self._get_cache_mode()
182
363
  self.logger.debug(f"Anthropic cache_mode: {cache_mode}")
183
364
 
184
- tool_list: ListToolsResult = await self.aggregator.list_tools()
185
- available_tools: List[ToolParam] = [
186
- ToolParam(
187
- name=tool.name,
188
- description=tool.description or "",
189
- input_schema=tool.inputSchema,
190
- )
191
- for tool in tool_list.tools
192
- ]
365
+ available_tools = await self._prepare_tools(structured_model)
193
366
 
194
367
  responses: List[ContentBlock] = []
195
368
 
@@ -209,59 +382,25 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
209
382
  "tools": available_tools,
210
383
  }
211
384
 
212
- # Apply cache_control to system prompt if cache_mode is not "off"
213
- # This caches both tools and system prompt together in one cache block
214
- if cache_mode != "off" and base_args["system"]:
215
- if isinstance(base_args["system"], str):
216
- base_args["system"] = [
217
- {
218
- "type": "text",
219
- "text": base_args["system"],
220
- "cache_control": {"type": "ephemeral"},
221
- }
222
- ]
223
- self.logger.debug(
224
- "Applied cache_control to system prompt (caches tools+system in one block)"
225
- )
226
- else:
227
- self.logger.debug(f"System prompt is not a string: {type(base_args['system'])}")
385
+ # Add tool_choice for structured output mode
386
+ if structured_model:
387
+ base_args["tool_choice"] = {"type": "tool", "name": "return_structured_output"}
228
388
 
229
- # Apply conversation caching using walking algorithm if in auto mode
230
- if cache_mode == "auto" and self.history.should_apply_conversation_cache():
231
- cache_updates = self.history.get_conversation_cache_updates()
389
+ # Apply cache control to system prompt
390
+ self._apply_system_cache(base_args, cache_mode)
232
391
 
233
- # Remove cache control from old positions
234
- if cache_updates["remove"]:
235
- self.history.remove_cache_control_from_messages(
236
- messages, cache_updates["remove"]
237
- )
238
- self.logger.debug(
239
- f"Removed conversation cache_control from positions {cache_updates['remove']}"
240
- )
392
+ # Apply conversation caching
393
+ applied_count = await self._apply_conversation_cache(messages, cache_mode)
241
394
 
242
- # Add cache control to new positions
243
- if cache_updates["add"]:
244
- applied_count = self.history.add_cache_control_to_messages(
245
- messages, cache_updates["add"]
395
+ # Verify we don't exceed Anthropic's 4 cache block limit
396
+ if applied_count > 0:
397
+ total_cache_blocks = applied_count
398
+ if cache_mode != "off" and base_args["system"]:
399
+ total_cache_blocks += 1 # tools+system cache block
400
+ if total_cache_blocks > 4:
401
+ self.logger.warning(
402
+ f"Total cache blocks ({total_cache_blocks}) exceeds Anthropic limit of 4"
246
403
  )
247
- if applied_count > 0:
248
- self.history.apply_conversation_cache_updates(cache_updates)
249
- self.logger.debug(
250
- f"Applied conversation cache_control to positions {cache_updates['add']} ({applied_count} blocks)"
251
- )
252
-
253
- # Verify we don't exceed Anthropic's 4 cache block limit
254
- total_cache_blocks = applied_count
255
- if cache_mode != "off" and base_args["system"]:
256
- total_cache_blocks += 1 # tools+system cache block
257
- if total_cache_blocks > 4:
258
- self.logger.warning(
259
- f"Total cache blocks ({total_cache_blocks}) exceeds Anthropic limit of 4"
260
- )
261
- else:
262
- self.logger.debug(
263
- f"Failed to apply conversation cache_control to positions {cache_updates['add']}"
264
- )
265
404
 
266
405
  if params.maxTokens is not None:
267
406
  base_args["max_tokens"] = params.maxTokens
@@ -387,34 +526,22 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
387
526
  style="dim green italic",
388
527
  )
389
528
 
390
- # Process all tool calls and collect results
391
- tool_results = []
392
- # Use a different loop variable for tool enumeration if 'i' is outer loop counter
393
- for tool_idx, content_block in enumerate(tool_uses):
394
- tool_name = content_block.name
395
- tool_args = content_block.input
396
- tool_use_id = content_block.id
397
-
398
- if tool_idx == 0: # Only show message for first tool use
399
- await self.show_assistant_message(message_text, tool_name)
400
-
401
- self.show_tool_call(available_tools, tool_name, tool_args)
402
- tool_call_request = CallToolRequest(
403
- method="tools/call",
404
- params=CallToolRequestParams(name=tool_name, arguments=tool_args),
405
- )
406
- # TODO -- support MCP isError etc.
407
- result = await self.call_tool(
408
- request=tool_call_request, tool_call_id=tool_use_id
409
- )
410
- self.show_tool_result(result)
411
-
412
- # Add each result to our collection
413
- tool_results.append((tool_use_id, result))
414
- responses.extend(result.content)
529
+ # Process all tool calls using the helper method
530
+ tool_results, tool_responses = await self._process_tool_calls(
531
+ tool_uses, available_tools, message_text, structured_model
532
+ )
533
+ responses.extend(tool_responses)
415
534
 
535
+ # Always add tool_results_message first (required by Anthropic API)
416
536
  messages.append(AnthropicConverter.create_tool_results_message(tool_results))
417
537
 
538
+ # For structured output, we have our result and should exit after sending tool_result
539
+ if structured_model and any(
540
+ tool.name == "return_structured_output" for tool in tool_uses
541
+ ):
542
+ self.logger.debug("Structured output received, breaking iteration loop")
543
+ break
544
+
418
545
  # Only save the new conversation messages to history if use_history is true
419
546
  # Keep the prompt messages separate
420
547
  if params.use_history:
@@ -501,19 +628,51 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
501
628
  ) -> Tuple[ModelT | None, PromptMessageMultipart]: # noqa: F821
502
629
  request_params = self.get_request_params(request_params)
503
630
 
504
- # TODO - convert this to use Tool Calling convention for Anthropic Structured outputs
505
- multipart_messages[-1].add_text(
506
- """YOU MUST RESPOND IN THE FOLLOWING FORMAT:
507
- {schema}
508
- RESPOND ONLY WITH THE JSON, NO PREAMBLE, CODE FENCES OR 'properties' ARE PERMISSABLE """.format(
509
- schema=model.model_json_schema()
510
- )
511
- )
631
+ # Check the last message role
632
+ last_message = multipart_messages[-1]
512
633
 
513
- result: PromptMessageMultipart = await self._apply_prompt_provider_specific(
514
- multipart_messages, request_params
634
+ # Add all previous messages to history (or all messages if last is from assistant)
635
+ messages_to_add = (
636
+ multipart_messages[:-1] if last_message.role == "user" else multipart_messages
515
637
  )
516
- return self._structured_from_multipart(result, model)
638
+ converted = []
639
+
640
+ for msg in messages_to_add:
641
+ anthropic_msg = AnthropicConverter.convert_to_anthropic(msg)
642
+ converted.append(anthropic_msg)
643
+
644
+ self.history.extend(converted, is_prompt=False)
645
+
646
+ if last_message.role == "user":
647
+ self.logger.debug("Last message in prompt is from user, generating structured response")
648
+ message_param = AnthropicConverter.convert_to_anthropic(last_message)
649
+
650
+ # Call _anthropic_completion with the structured model
651
+ response_content = await self._anthropic_completion(
652
+ message_param, request_params, structured_model=model
653
+ )
654
+
655
+ # Extract the structured data from the response
656
+ for content in response_content:
657
+ if content.type == "text":
658
+ try:
659
+ # Parse the JSON response from the tool
660
+ data = json.loads(content.text)
661
+ parsed_model = model(**data)
662
+ # Create assistant response
663
+ assistant_response = Prompt.assistant(content)
664
+ return parsed_model, assistant_response
665
+ except (json.JSONDecodeError, ValueError) as e:
666
+ self.logger.error(f"Failed to parse structured output: {e}")
667
+ assistant_response = Prompt.assistant(content)
668
+ return None, assistant_response
669
+
670
+ # If no valid response found
671
+ return None, Prompt.assistant()
672
+ else:
673
+ # For assistant messages: Return the last message content
674
+ self.logger.debug("Last message in prompt is from assistant, returning it directly")
675
+ return None, last_message
517
676
 
518
677
  def _show_usage(self, raw_usage: Usage, turn_usage: TurnUsage) -> None:
519
678
  # Print raw usage for debugging
@@ -18,7 +18,7 @@ fast = FastAgent("Evaluator-Optimizer")
18
18
  candidate details, and company information. Tailor the response to the company and job requirements.
19
19
  """,
20
20
  servers=["fetch"],
21
- model="haiku3",
21
+ model="gpt-4.1-nano",
22
22
  use_history=True,
23
23
  )
24
24
  # Define evaluator agent
@@ -40,7 +40,7 @@ fast = FastAgent("Evaluator-Optimizer")
40
40
  Summarize your evaluation as a structured response with:
41
41
  - Overall quality rating.
42
42
  - Specific feedback and areas for improvement.""",
43
- model="gpt-4.1",
43
+ model="sonnet",
44
44
  )
45
45
  # Define the evaluator-optimizer workflow
46
46
  @fast.evaluator_optimizer(
@@ -43,7 +43,7 @@ SAMPLE_REQUESTS = [
43
43
  )
44
44
  @fast.router(
45
45
  name="route",
46
- model="sonnet",
46
+ model="gpt-4.1",
47
47
  agents=["code_expert", "general_assistant", "fetcher"],
48
48
  )
49
49
  async def main() -> None:
@@ -1,6 +1,8 @@
1
+ from json import JSONDecodeError
1
2
  from typing import Optional, Union
2
3
 
3
4
  from mcp.types import CallToolResult
5
+ from rich.json import JSON
4
6
  from rich.panel import Panel
5
7
  from rich.text import Text
6
8
 
@@ -52,13 +54,15 @@ class ConsoleDisplay:
52
54
  elif len(content) == 1 and is_text_content(content[0]):
53
55
  text_content = get_text(content[0])
54
56
  char_count = len(text_content) if text_content else 0
55
- status = f"Text Only ({char_count} chars)"
57
+ status = f"Text Only {char_count} chars"
56
58
  else:
57
59
  text_count = sum(1 for item in content if is_text_content(item))
58
60
  if text_count == len(content):
59
61
  status = f"{len(content)} Text Blocks" if len(content) > 1 else "1 Text Block"
60
62
  else:
61
- status = f"{len(content)} Content Blocks"
63
+ status = (
64
+ f"{len(content)} Content Blocks" if len(content) > 1 else "1 Content Block"
65
+ )
62
66
 
63
67
  # Combined separator and status line
64
68
  left = f"[{block_color}]▎[/{block_color}][{text_color}]▶[/{text_color}]{f' [{block_color}]{name}[/{block_color}]' if name else ''}"
@@ -357,16 +361,22 @@ class ConsoleDisplay:
357
361
  right = f"[dim]{model}[/dim]" if model else ""
358
362
  self._create_combined_separator_status(left, right)
359
363
 
360
- # Display content as markdown if it looks like markdown, otherwise as text
361
364
  if isinstance(message_text, str):
362
365
  content = message_text
363
- # if any(marker in content for marker in ["##", "**", "*", "`", "---", "###"]):
364
- md = Markdown(content, code_theme=CODE_STYLE)
365
- console.console.print(md, markup=self._markup)
366
- # else:
367
- # console.console.print(content, markup=self._markup)
366
+
367
+ # Try to detect and pretty print JSON
368
+ try:
369
+ import json
370
+
371
+ json.loads(content)
372
+ json = JSON(message_text)
373
+ console.console.print(json, markup=self._markup)
374
+ except (JSONDecodeError, TypeError, ValueError):
375
+ # Not JSON, treat as markdown
376
+ md = Markdown(content, code_theme=CODE_STYLE)
377
+ console.console.print(md, markup=self._markup)
368
378
  else:
369
- # Handle Text objects directly
379
+ # Handle Rich Text objects directly
370
380
  console.console.print(message_text, markup=self._markup)
371
381
 
372
382
  # Bottom separator with server list: ─ [server1] [server2] ────────