agno 2.3.2__py3-none-any.whl → 2.3.3__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.
Files changed (73) hide show
  1. agno/agent/agent.py +513 -185
  2. agno/compression/__init__.py +3 -0
  3. agno/compression/manager.py +176 -0
  4. agno/db/dynamo/dynamo.py +11 -0
  5. agno/db/firestore/firestore.py +5 -1
  6. agno/db/gcs_json/gcs_json_db.py +5 -2
  7. agno/db/in_memory/in_memory_db.py +5 -2
  8. agno/db/json/json_db.py +5 -1
  9. agno/db/migrations/manager.py +4 -4
  10. agno/db/mongo/async_mongo.py +158 -34
  11. agno/db/mongo/mongo.py +6 -2
  12. agno/db/mysql/mysql.py +48 -54
  13. agno/db/postgres/async_postgres.py +61 -51
  14. agno/db/postgres/postgres.py +42 -50
  15. agno/db/redis/redis.py +5 -0
  16. agno/db/redis/utils.py +5 -5
  17. agno/db/singlestore/singlestore.py +99 -108
  18. agno/db/sqlite/async_sqlite.py +29 -27
  19. agno/db/sqlite/sqlite.py +30 -26
  20. agno/knowledge/reader/pdf_reader.py +2 -2
  21. agno/knowledge/reader/tavily_reader.py +0 -1
  22. agno/memory/__init__.py +14 -1
  23. agno/memory/manager.py +217 -4
  24. agno/memory/strategies/__init__.py +15 -0
  25. agno/memory/strategies/base.py +67 -0
  26. agno/memory/strategies/summarize.py +196 -0
  27. agno/memory/strategies/types.py +37 -0
  28. agno/models/anthropic/claude.py +84 -80
  29. agno/models/aws/bedrock.py +38 -16
  30. agno/models/aws/claude.py +97 -277
  31. agno/models/azure/ai_foundry.py +8 -4
  32. agno/models/base.py +101 -14
  33. agno/models/cerebras/cerebras.py +18 -7
  34. agno/models/cerebras/cerebras_openai.py +4 -2
  35. agno/models/cohere/chat.py +8 -4
  36. agno/models/google/gemini.py +578 -20
  37. agno/models/groq/groq.py +18 -5
  38. agno/models/huggingface/huggingface.py +17 -6
  39. agno/models/ibm/watsonx.py +16 -6
  40. agno/models/litellm/chat.py +17 -7
  41. agno/models/message.py +19 -5
  42. agno/models/meta/llama.py +20 -4
  43. agno/models/mistral/mistral.py +8 -4
  44. agno/models/ollama/chat.py +17 -6
  45. agno/models/openai/chat.py +17 -6
  46. agno/models/openai/responses.py +23 -9
  47. agno/models/vertexai/claude.py +99 -5
  48. agno/os/interfaces/agui/router.py +1 -0
  49. agno/os/interfaces/agui/utils.py +97 -57
  50. agno/os/router.py +16 -0
  51. agno/os/routers/memory/memory.py +143 -0
  52. agno/os/routers/memory/schemas.py +26 -0
  53. agno/os/schema.py +21 -6
  54. agno/os/utils.py +134 -10
  55. agno/run/base.py +2 -1
  56. agno/run/workflow.py +1 -1
  57. agno/team/team.py +565 -219
  58. agno/tools/mcp/mcp.py +1 -1
  59. agno/utils/agent.py +119 -1
  60. agno/utils/models/ai_foundry.py +9 -2
  61. agno/utils/models/claude.py +12 -5
  62. agno/utils/models/cohere.py +9 -2
  63. agno/utils/models/llama.py +9 -2
  64. agno/utils/models/mistral.py +4 -2
  65. agno/utils/print_response/agent.py +37 -2
  66. agno/utils/print_response/team.py +52 -0
  67. agno/utils/tokens.py +41 -0
  68. agno/workflow/types.py +2 -2
  69. {agno-2.3.2.dist-info → agno-2.3.3.dist-info}/METADATA +45 -40
  70. {agno-2.3.2.dist-info → agno-2.3.3.dist-info}/RECORD +73 -66
  71. {agno-2.3.2.dist-info → agno-2.3.3.dist-info}/WHEEL +0 -0
  72. {agno-2.3.2.dist-info → agno-2.3.3.dist-info}/licenses/LICENSE +0 -0
  73. {agno-2.3.2.dist-info → agno-2.3.3.dist-info}/top_level.txt +0 -0
agno/tools/mcp/mcp.py CHANGED
@@ -43,7 +43,7 @@ class MCPTools(Toolkit):
43
43
  include_tools: Optional[list[str]] = None,
44
44
  exclude_tools: Optional[list[str]] = None,
45
45
  refresh_connection: bool = False,
46
- tool_name_prefix: Optional[str] = "",
46
+ tool_name_prefix: Optional[str] = None,
47
47
  **kwargs,
48
48
  ):
49
49
  """
agno/utils/agent.py CHANGED
@@ -1,10 +1,11 @@
1
1
  from asyncio import Future, Task
2
- from typing import TYPE_CHECKING, Any, AsyncIterator, Dict, Iterator, List, Optional, Sequence, Union
2
+ from typing import TYPE_CHECKING, Any, AsyncIterator, Callable, Dict, Iterator, List, Optional, Sequence, Union
3
3
 
4
4
  from agno.media import Audio, File, Image, Video
5
5
  from agno.models.message import Message
6
6
  from agno.models.metrics import Metrics
7
7
  from agno.models.response import ModelResponse
8
+ from agno.run import RunContext
8
9
  from agno.run.agent import RunEvent, RunInput, RunOutput, RunOutputEvent
9
10
  from agno.run.team import RunOutputEvent as TeamRunOutputEvent
10
11
  from agno.run.team import TeamRunOutput
@@ -818,3 +819,120 @@ async def aget_chat_history_util(entity: Union["Agent", "Team"], session_id: str
818
819
  raise Exception("Session not found")
819
820
 
820
821
  return session.get_chat_history() # type: ignore
822
+
823
+
824
+ def execute_instructions(
825
+ instructions: Callable,
826
+ agent: Optional[Union["Agent", "Team"]] = None,
827
+ team: Optional["Team"] = None,
828
+ session_state: Optional[Dict[str, Any]] = None,
829
+ run_context: Optional[RunContext] = None,
830
+ ) -> Union[str, List[str]]:
831
+ """Execute the instructions function."""
832
+ import inspect
833
+
834
+ signature = inspect.signature(instructions)
835
+ instruction_args: Dict[str, Any] = {}
836
+
837
+ # Check for agent parameter
838
+ if "agent" in signature.parameters:
839
+ instruction_args["agent"] = agent
840
+
841
+ if "team" in signature.parameters:
842
+ instruction_args["team"] = team
843
+
844
+ # Check for session_state parameter
845
+ if "session_state" in signature.parameters:
846
+ instruction_args["session_state"] = session_state or {}
847
+
848
+ # Check for run_context parameter
849
+ if "run_context" in signature.parameters:
850
+ instruction_args["run_context"] = run_context or None
851
+
852
+ # Run the instructions function, await if it's awaitable, otherwise run directly (in thread)
853
+ if inspect.iscoroutinefunction(instructions):
854
+ raise Exception("Instructions function is async, use `agent.arun()` instead")
855
+
856
+ # Run the instructions function
857
+ return instructions(**instruction_args)
858
+
859
+
860
+ def execute_system_message(
861
+ system_message: Callable,
862
+ agent: Optional[Union["Agent", "Team"]] = None,
863
+ team: Optional["Team"] = None,
864
+ session_state: Optional[Dict[str, Any]] = None,
865
+ run_context: Optional[RunContext] = None,
866
+ ) -> str:
867
+ """Execute the system message function."""
868
+ import inspect
869
+
870
+ signature = inspect.signature(system_message)
871
+ system_message_args: Dict[str, Any] = {}
872
+
873
+ # Check for agent parameter
874
+ if "agent" in signature.parameters:
875
+ system_message_args["agent"] = agent
876
+ if "team" in signature.parameters:
877
+ system_message_args["team"] = team
878
+ if inspect.iscoroutinefunction(system_message):
879
+ raise ValueError("System message function is async, use `agent.arun()` instead")
880
+
881
+ return system_message(**system_message_args)
882
+
883
+
884
+ async def aexecute_instructions(
885
+ instructions: Callable,
886
+ agent: Optional[Union["Agent", "Team"]] = None,
887
+ team: Optional["Team"] = None,
888
+ session_state: Optional[Dict[str, Any]] = None,
889
+ run_context: Optional[RunContext] = None,
890
+ ) -> Union[str, List[str]]:
891
+ """Execute the instructions function."""
892
+ import inspect
893
+
894
+ signature = inspect.signature(instructions)
895
+ instruction_args: Dict[str, Any] = {}
896
+
897
+ # Check for agent parameter
898
+ if "agent" in signature.parameters:
899
+ instruction_args["agent"] = agent
900
+ if "team" in signature.parameters:
901
+ instruction_args["team"] = team
902
+
903
+ # Check for session_state parameter
904
+ if "session_state" in signature.parameters:
905
+ instruction_args["session_state"] = session_state or {}
906
+
907
+ # Check for run_context parameter
908
+ if "run_context" in signature.parameters:
909
+ instruction_args["run_context"] = run_context or None
910
+
911
+ if inspect.iscoroutinefunction(instructions):
912
+ return await instructions(**instruction_args)
913
+ else:
914
+ return instructions(**instruction_args)
915
+
916
+
917
+ async def aexecute_system_message(
918
+ system_message: Callable,
919
+ agent: Optional[Union["Agent", "Team"]] = None,
920
+ team: Optional["Team"] = None,
921
+ session_state: Optional[Dict[str, Any]] = None,
922
+ run_context: Optional[RunContext] = None,
923
+ ) -> str:
924
+ import inspect
925
+
926
+ signature = inspect.signature(system_message)
927
+ system_message_args: Dict[str, Any] = {}
928
+
929
+ # Check for agent parameter
930
+ if "agent" in signature.parameters:
931
+ system_message_args["agent"] = agent
932
+ if "team" in signature.parameters:
933
+ system_message_args["team"] = team
934
+
935
+ if inspect.iscoroutinefunction(system_message):
936
+ return await system_message(**system_message_args)
937
+ else:
938
+ return system_message(**system_message_args)
@@ -5,19 +5,26 @@ from agno.utils.log import log_warning
5
5
  from agno.utils.openai import images_to_message
6
6
 
7
7
 
8
- def format_message(message: Message) -> Dict[str, Any]:
8
+ def format_message(message: Message, compress_tool_results: bool = False) -> Dict[str, Any]:
9
9
  """
10
10
  Format a message into the format expected by OpenAI.
11
11
 
12
12
  Args:
13
13
  message (Message): The message to format.
14
+ compress_tool_results: Whether to compress tool results.
14
15
 
15
16
  Returns:
16
17
  Dict[str, Any]: The formatted message.
17
18
  """
19
+ # Use compressed content for tool messages if compression is active
20
+ content = message.content
21
+
22
+ if message.role == "tool":
23
+ content = message.get_content(use_compressed_content=compress_tool_results)
24
+
18
25
  message_dict: Dict[str, Any] = {
19
26
  "role": message.role,
20
- "content": message.content,
27
+ "content": content,
21
28
  "name": message.name,
22
29
  "tool_call_id": message.tool_call_id,
23
30
  "tool_calls": message.tool_calls,
@@ -1,6 +1,6 @@
1
1
  import json
2
2
  from dataclasses import dataclass, field
3
- from typing import Any, Dict, List, Optional, Tuple
3
+ from typing import Any, Dict, List, Optional, Tuple, Union
4
4
 
5
5
  from agno.media import File, Image
6
6
  from agno.models.message import Message
@@ -221,17 +221,20 @@ def _format_file_for_message(file: File) -> Optional[Dict[str, Any]]:
221
221
  return None
222
222
 
223
223
 
224
- def format_messages(messages: List[Message]) -> Tuple[List[Dict[str, str]], str]:
224
+ def format_messages(
225
+ messages: List[Message], compress_tool_results: bool = False
226
+ ) -> Tuple[List[Dict[str, Union[str, list]]], str]:
225
227
  """
226
228
  Process the list of messages and separate them into API messages and system messages.
227
229
 
228
230
  Args:
229
231
  messages (List[Message]): The list of messages to process.
232
+ compress_tool_results: Whether to compress tool results.
230
233
 
231
234
  Returns:
232
- Tuple[List[Dict[str, str]], str]: A tuple containing the list of API messages and the concatenated system messages.
235
+ Tuple[List[Dict[str, Union[str, list]]], str]: A tuple containing the list of API messages and the concatenated system messages.
233
236
  """
234
- chat_messages: List[Dict[str, str]] = []
237
+ chat_messages: List[Dict[str, Union[str, list]]] = []
235
238
  system_messages: List[str] = []
236
239
 
237
240
  for message in messages:
@@ -301,11 +304,15 @@ def format_messages(messages: List[Message]) -> Tuple[List[Dict[str, str]], str]
301
304
  )
302
305
  elif message.role == "tool":
303
306
  content = []
307
+
308
+ # Use compressed content for tool messages if compression is active
309
+ tool_result = message.get_content(use_compressed_content=compress_tool_results)
310
+
304
311
  content.append(
305
312
  {
306
313
  "type": "tool_result",
307
314
  "tool_use_id": message.tool_call_id,
308
- "content": str(message.content),
315
+ "content": str(tool_result),
309
316
  }
310
317
  )
311
318
 
@@ -46,21 +46,28 @@ def _format_images_for_message(message: Message, images: Sequence[Image]) -> Lis
46
46
  return message_content_with_image
47
47
 
48
48
 
49
- def format_messages(messages: List[Message]) -> List[Dict[str, Any]]:
49
+ def format_messages(messages: List[Message], compress_tool_results: bool = False) -> List[Dict[str, Any]]:
50
50
  """
51
51
  Format messages for the Cohere API.
52
52
 
53
53
  Args:
54
54
  messages (List[Message]): The list of messages.
55
+ compress_tool_results: Whether to compress tool results.
55
56
 
56
57
  Returns:
57
58
  List[Dict[str, Any]]: The formatted messages.
58
59
  """
59
60
  formatted_messages = []
60
61
  for message in messages:
62
+ # Use compressed content for tool messages if compression is active
63
+ content = message.content
64
+
65
+ if message.role == "tool":
66
+ content = message.get_content(use_compressed_content=compress_tool_results)
67
+
61
68
  message_dict = {
62
69
  "role": message.role,
63
- "content": message.content,
70
+ "content": content,
64
71
  "name": message.name,
65
72
  "tool_call_id": message.tool_call_id,
66
73
  "tool_calls": message.tool_calls,
@@ -19,13 +19,17 @@ TOOL_CALL_ROLE_MAP = {
19
19
  }
20
20
 
21
21
 
22
- def format_message(message: Message, openai_like: bool = False, tool_calls: bool = False) -> Dict[str, Any]:
22
+ def format_message(
23
+ message: Message, openai_like: bool = False, tool_calls: bool = False, compress_tool_results: bool = False
24
+ ) -> Dict[str, Any]:
23
25
  """
24
26
  Format a message into the format expected by Llama API.
25
27
 
26
28
  Args:
27
29
  message (Message): The message to format.
28
30
  openai_like (bool): Whether to format the message as an OpenAI-like message.
31
+ tool_calls (bool): Whether tool calls are present.
32
+ compress_tool_results: Whether to compress tool results.
29
33
 
30
34
  Returns:
31
35
  Dict[str, Any]: The formatted message.
@@ -52,10 +56,13 @@ def format_message(message: Message, openai_like: bool = False, tool_calls: bool
52
56
  log_warning("Audio input is currently unsupported.")
53
57
 
54
58
  if message.role == "tool":
59
+ # Use compressed content if compression is active
60
+ content = message.get_content(use_compressed_content=compress_tool_results)
61
+
55
62
  message_dict = {
56
63
  "role": "tool",
57
64
  "tool_call_id": message.tool_call_id,
58
- "content": message.content,
65
+ "content": content,
59
66
  }
60
67
 
61
68
  if message.role == "assistant":
@@ -48,7 +48,7 @@ def _format_image_for_message(image: Image) -> Optional[ImageURLChunk]:
48
48
  return None
49
49
 
50
50
 
51
- def format_messages(messages: List[Message]) -> List[MistralMessage]:
51
+ def format_messages(messages: List[Message], compress_tool_results: bool = False) -> List[MistralMessage]:
52
52
  mistral_messages: List[MistralMessage] = []
53
53
 
54
54
  for message in messages:
@@ -84,7 +84,9 @@ def format_messages(messages: List[Message]) -> List[MistralMessage]:
84
84
  elif message.role == "system":
85
85
  mistral_message = SystemMessage(role="system", content=message.content)
86
86
  elif message.role == "tool":
87
- mistral_message = ToolMessage(name="tool", content=message.content, tool_call_id=message.tool_call_id)
87
+ # Get compressed content if compression is active
88
+ tool_content = message.get_content(use_compressed_content=compress_tool_results)
89
+ mistral_message = ToolMessage(name="tool", content=tool_content, tool_call_id=message.tool_call_id)
88
90
  else:
89
91
  raise ValueError(f"Unknown role: {message.role}")
90
92
 
@@ -179,6 +179,7 @@ def print_response_stream(
179
179
  show_reasoning=show_reasoning,
180
180
  show_full_reasoning=show_full_reasoning,
181
181
  accumulated_tool_calls=accumulated_tool_calls,
182
+ compression_manager=agent.compression_manager,
182
183
  )
183
184
  panels.extend(additional_panels)
184
185
  if panels:
@@ -204,6 +205,10 @@ def print_response_stream(
204
205
  live_log.update(Group(*panels))
205
206
  agent.session_summary_manager.summaries_updated = False
206
207
 
208
+ # Clear compression stats after final display
209
+ if agent.compression_manager is not None:
210
+ agent.compression_manager.stats.clear()
211
+
207
212
  response_timer.stop()
208
213
 
209
214
  # Final update to remove the "Thinking..." status
@@ -366,6 +371,7 @@ async def aprint_response_stream(
366
371
  show_reasoning=show_reasoning,
367
372
  show_full_reasoning=show_full_reasoning,
368
373
  accumulated_tool_calls=accumulated_tool_calls,
374
+ compression_manager=agent.compression_manager,
369
375
  )
370
376
  panels.extend(additional_panels)
371
377
  if panels:
@@ -391,6 +397,10 @@ async def aprint_response_stream(
391
397
  live_log.update(Group(*panels))
392
398
  agent.session_summary_manager.summaries_updated = False
393
399
 
400
+ # Clear compression stats after final display
401
+ if agent.compression_manager is not None:
402
+ agent.compression_manager.stats.clear()
403
+
394
404
  response_timer.stop()
395
405
 
396
406
  # Final update to remove the "Thinking..." status
@@ -407,6 +417,7 @@ def build_panels_stream(
407
417
  show_reasoning: bool = True,
408
418
  show_full_reasoning: bool = False,
409
419
  accumulated_tool_calls: Optional[List] = None,
420
+ compression_manager: Optional[Any] = None,
410
421
  ):
411
422
  panels = []
412
423
 
@@ -447,8 +458,18 @@ def build_panels_stream(
447
458
  for formatted_tool_call in formatted_tool_calls:
448
459
  tool_calls_content.append(f"• {formatted_tool_call}\n")
449
460
 
461
+ tool_calls_text = tool_calls_content.plain.rstrip()
462
+
463
+ # Add compression stats if available (don't clear - caller will clear after final display)
464
+ if compression_manager is not None and compression_manager.stats:
465
+ stats = compression_manager.stats
466
+ saved = stats.get("original_size", 0) - stats.get("compressed_size", 0)
467
+ orig = stats.get("original_size", 1)
468
+ if stats.get("messages_compressed", 0) > 0:
469
+ tool_calls_text += f"\n\nTool results compressed: {stats.get('messages_compressed', 0)} | Saved: {saved:,} chars ({saved / orig * 100:.0f}%)"
470
+
450
471
  tool_calls_panel = create_panel(
451
- content=tool_calls_content.plain.rstrip(),
472
+ content=tool_calls_text,
452
473
  title="Tool Calls",
453
474
  border_style="yellow",
454
475
  )
@@ -589,6 +610,7 @@ def print_response(
589
610
  show_full_reasoning=show_full_reasoning,
590
611
  tags_to_include_in_markdown=tags_to_include_in_markdown,
591
612
  markdown=markdown,
613
+ compression_manager=agent.compression_manager,
592
614
  )
593
615
  panels.extend(additional_panels)
594
616
 
@@ -721,6 +743,7 @@ async def aprint_response(
721
743
  show_full_reasoning=show_full_reasoning,
722
744
  tags_to_include_in_markdown=tags_to_include_in_markdown,
723
745
  markdown=markdown,
746
+ compression_manager=agent.compression_manager,
724
747
  )
725
748
  panels.extend(additional_panels)
726
749
 
@@ -757,6 +780,7 @@ def build_panels(
757
780
  show_full_reasoning: bool = False,
758
781
  tags_to_include_in_markdown: Optional[Set[str]] = None,
759
782
  markdown: bool = False,
783
+ compression_manager: Optional[Any] = None,
760
784
  ):
761
785
  panels = []
762
786
 
@@ -808,8 +832,19 @@ def build_panels(
808
832
  for formatted_tool_call in formatted_tool_calls:
809
833
  tool_calls_content.append(f"• {formatted_tool_call}\n")
810
834
 
835
+ tool_calls_text = tool_calls_content.plain.rstrip()
836
+
837
+ # Add compression stats if available
838
+ if compression_manager is not None and compression_manager.stats:
839
+ stats = compression_manager.stats
840
+ saved = stats.get("original_size", 0) - stats.get("compressed_size", 0)
841
+ orig = stats.get("original_size", 1)
842
+ if stats.get("messages_compressed", 0) > 0:
843
+ tool_calls_text += f"\n\nTool results compressed: {stats.get('messages_compressed', 0)} | Saved: {saved:,} chars ({saved / orig * 100:.0f}%)"
844
+ compression_manager.stats.clear()
845
+
811
846
  tool_calls_panel = create_panel(
812
- content=tool_calls_content.plain.rstrip(),
847
+ content=tool_calls_text,
813
848
  title="Tool Calls",
814
849
  border_style="yellow",
815
850
  )
@@ -260,6 +260,15 @@ def print_response(
260
260
  # Join with blank lines between items
261
261
  tool_calls_text = "\n\n".join(lines)
262
262
 
263
+ # Add compression stats at end of tool calls
264
+ if team.compression_manager is not None and team.compression_manager.stats:
265
+ stats = team.compression_manager.stats
266
+ saved = stats.get("original_size", 0) - stats.get("compressed_size", 0)
267
+ orig = stats.get("original_size", 1)
268
+ if stats.get("messages_compressed", 0) > 0:
269
+ tool_calls_text += f"\n\nTool results compressed: {stats.get('messages_compressed', 0)} | Saved: {saved:,} chars ({saved / orig * 100:.0f}%)"
270
+ team.compression_manager.stats.clear()
271
+
263
272
  team_tool_calls_panel = create_panel(
264
273
  content=tool_calls_text,
265
274
  title="Team Tool Calls",
@@ -613,6 +622,14 @@ def print_response_stream(
613
622
  # Join with blank lines between items
614
623
  tool_calls_text = "\n\n".join(lines)
615
624
 
625
+ # Add compression stats if available (don't clear - will be cleared in final_panels)
626
+ if team.compression_manager is not None and team.compression_manager.stats:
627
+ stats = team.compression_manager.stats
628
+ saved = stats.get("original_size", 0) - stats.get("compressed_size", 0)
629
+ orig = stats.get("original_size", 1)
630
+ if stats.get("messages_compressed", 0) > 0:
631
+ tool_calls_text += f"\n\nTool results compressed: {stats.get('messages_compressed', 0)} | Saved: {saved:,} chars ({saved / orig * 100:.0f}%)"
632
+
616
633
  team_tool_calls_panel = create_panel(
617
634
  content=tool_calls_text,
618
635
  title="Team Tool Calls",
@@ -815,6 +832,15 @@ def print_response_stream(
815
832
 
816
833
  tool_calls_text = "\n\n".join(lines)
817
834
 
835
+ # Add compression stats at end of tool calls
836
+ if team.compression_manager is not None and team.compression_manager.stats:
837
+ stats = team.compression_manager.stats
838
+ saved = stats.get("original_size", 0) - stats.get("compressed_size", 0)
839
+ orig = stats.get("original_size", 1)
840
+ if stats.get("messages_compressed", 0) > 0:
841
+ tool_calls_text += f"\n\nTool results compressed: {stats.get('messages_compressed', 0)} | Saved: {saved:,} chars ({saved / orig * 100:.0f}%)"
842
+ team.compression_manager.stats.clear()
843
+
818
844
  team_tool_calls_panel = create_panel(
819
845
  content=tool_calls_text,
820
846
  title="Team Tool Calls",
@@ -1095,6 +1121,15 @@ async def aprint_response(
1095
1121
 
1096
1122
  tool_calls_text = "\n\n".join(lines)
1097
1123
 
1124
+ # Add compression stats at end of tool calls
1125
+ if team.compression_manager is not None and team.compression_manager.stats:
1126
+ stats = team.compression_manager.stats
1127
+ saved = stats.get("original_size", 0) - stats.get("compressed_size", 0)
1128
+ orig = stats.get("original_size", 1)
1129
+ if stats.get("messages_compressed", 0) > 0:
1130
+ tool_calls_text += f"\n\nTool results compressed: {stats.get('messages_compressed', 0)} | Saved: {saved:,} chars ({saved / orig * 100:.0f}%)"
1131
+ team.compression_manager.stats.clear()
1132
+
1098
1133
  team_tool_calls_panel = create_panel(
1099
1134
  content=tool_calls_text,
1100
1135
  title="Team Tool Calls",
@@ -1446,6 +1481,14 @@ async def aprint_response_stream(
1446
1481
  # Join with blank lines between items
1447
1482
  tool_calls_text = "\n\n".join(lines)
1448
1483
 
1484
+ # Add compression stats if available (don't clear - will be cleared in final_panels)
1485
+ if team.compression_manager is not None and team.compression_manager.stats:
1486
+ stats = team.compression_manager.stats
1487
+ saved = stats.get("original_size", 0) - stats.get("compressed_size", 0)
1488
+ orig = stats.get("original_size", 1)
1489
+ if stats.get("messages_compressed", 0) > 0:
1490
+ tool_calls_text += f"\n\nTool results compressed: {stats.get('messages_compressed', 0)} | Saved: {saved:,} chars ({saved / orig * 100:.0f}%)"
1491
+
1449
1492
  team_tool_calls_panel = create_panel(
1450
1493
  content=tool_calls_text,
1451
1494
  title="Team Tool Calls",
@@ -1666,6 +1709,15 @@ async def aprint_response_stream(
1666
1709
 
1667
1710
  tool_calls_text = "\n\n".join(lines)
1668
1711
 
1712
+ # Add compression stats at end of tool calls
1713
+ if team.compression_manager is not None and team.compression_manager.stats:
1714
+ stats = team.compression_manager.stats
1715
+ saved = stats.get("original_size", 0) - stats.get("compressed_size", 0)
1716
+ orig = stats.get("original_size", 1)
1717
+ if stats.get("messages_compressed", 0) > 0:
1718
+ tool_calls_text += f"\n\nTool results compressed: {stats.get('messages_compressed', 0)} | Saved: {saved:,} chars ({saved / orig * 100:.0f}%)"
1719
+ team.compression_manager.stats.clear()
1720
+
1669
1721
  team_tool_calls_panel = create_panel(
1670
1722
  content=tool_calls_text,
1671
1723
  title="Team Tool Calls",
agno/utils/tokens.py ADDED
@@ -0,0 +1,41 @@
1
+ """Token counting utilities for text processing."""
2
+
3
+
4
+ def count_tokens(text: str) -> int:
5
+ """Count tokens in text using tiktoken.
6
+
7
+ Uses cl100k_base encoding (compatible with GPT-4, GPT-4o, GPT-3.5-turbo).
8
+ Falls back to character-based estimation if tiktoken is not available.
9
+
10
+ Args:
11
+ text: The text string to count tokens for.
12
+
13
+ Returns:
14
+ Total token count for the text.
15
+
16
+ Examples:
17
+ >>> count_tokens("Hello world")
18
+ 2
19
+ >>> count_tokens("")
20
+ 0
21
+ """
22
+ try:
23
+ import tiktoken
24
+
25
+ # Use cl100k_base encoding (GPT-4, GPT-4o, GPT-3.5-turbo)
26
+ encoding = tiktoken.get_encoding("cl100k_base")
27
+ tokens = encoding.encode(text)
28
+ return len(tokens)
29
+ except ImportError:
30
+ from agno.utils.log import log_warning
31
+
32
+ log_warning(
33
+ "tiktoken not installed. You can install with `pip install -U tiktoken`. Using character-based estimation."
34
+ )
35
+ # Fallback: rough estimation (1 token H 4 characters)
36
+ return len(text) // 4
37
+ except Exception as e:
38
+ from agno.utils.log import log_warning
39
+
40
+ log_warning(f"Error counting tokens: {e}. Using character-based estimation.")
41
+ return len(text) // 4
agno/workflow/types.py CHANGED
@@ -209,7 +209,7 @@ class StepInput:
209
209
  input_dict: Optional[Union[str, Dict[str, Any], List[Any]]] = None
210
210
  if self.input is not None:
211
211
  if isinstance(self.input, BaseModel):
212
- input_dict = self.input.model_dump(exclude_none=True)
212
+ input_dict = self.input.model_dump(exclude_none=True, mode="json")
213
213
  elif isinstance(self.input, (dict, list)):
214
214
  input_dict = self.input
215
215
  else:
@@ -281,7 +281,7 @@ class StepOutput:
281
281
  content_dict: Optional[Union[str, Dict[str, Any], List[Any]]] = None
282
282
  if self.content is not None:
283
283
  if isinstance(self.content, BaseModel):
284
- content_dict = self.content.model_dump(exclude_none=True)
284
+ content_dict = self.content.model_dump(exclude_none=True, mode="json")
285
285
  elif isinstance(self.content, (dict, list)):
286
286
  content_dict = self.content
287
287
  else: