agno 2.2.4__py3-none-any.whl → 2.2.6__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 (41) hide show
  1. agno/agent/agent.py +82 -19
  2. agno/culture/manager.py +3 -4
  3. agno/knowledge/chunking/agentic.py +6 -2
  4. agno/memory/manager.py +9 -4
  5. agno/models/anthropic/claude.py +1 -2
  6. agno/models/azure/ai_foundry.py +31 -14
  7. agno/models/azure/openai_chat.py +12 -4
  8. agno/models/base.py +44 -11
  9. agno/models/cerebras/cerebras.py +11 -6
  10. agno/models/groq/groq.py +7 -4
  11. agno/models/meta/llama.py +12 -6
  12. agno/models/meta/llama_openai.py +5 -1
  13. agno/models/openai/chat.py +20 -12
  14. agno/models/openai/responses.py +10 -5
  15. agno/models/utils.py +254 -8
  16. agno/models/vertexai/claude.py +9 -13
  17. agno/os/app.py +48 -21
  18. agno/os/routers/evals/evals.py +8 -8
  19. agno/os/routers/evals/utils.py +1 -0
  20. agno/os/schema.py +48 -33
  21. agno/os/utils.py +27 -0
  22. agno/run/agent.py +5 -0
  23. agno/run/team.py +2 -0
  24. agno/run/workflow.py +39 -0
  25. agno/session/summary.py +8 -2
  26. agno/session/workflow.py +4 -3
  27. agno/team/team.py +50 -14
  28. agno/tools/file.py +153 -25
  29. agno/tools/function.py +5 -1
  30. agno/tools/notion.py +201 -0
  31. agno/utils/events.py +2 -0
  32. agno/utils/print_response/workflow.py +115 -16
  33. agno/vectordb/milvus/milvus.py +5 -0
  34. agno/workflow/__init__.py +2 -0
  35. agno/workflow/agent.py +298 -0
  36. agno/workflow/workflow.py +929 -64
  37. {agno-2.2.4.dist-info → agno-2.2.6.dist-info}/METADATA +4 -1
  38. {agno-2.2.4.dist-info → agno-2.2.6.dist-info}/RECORD +41 -39
  39. {agno-2.2.4.dist-info → agno-2.2.6.dist-info}/WHEEL +0 -0
  40. {agno-2.2.4.dist-info → agno-2.2.6.dist-info}/licenses/LICENSE +0 -0
  41. {agno-2.2.4.dist-info → agno-2.2.6.dist-info}/top_level.txt +0 -0
agno/team/team.py CHANGED
@@ -45,6 +45,7 @@ from agno.models.base import Model
45
45
  from agno.models.message import Message, MessageReferences
46
46
  from agno.models.metrics import Metrics
47
47
  from agno.models.response import ModelResponse, ModelResponseEvent
48
+ from agno.models.utils import get_model
48
49
  from agno.reasoning.step import NextAction, ReasoningStep, ReasoningSteps
49
50
  from agno.run.agent import RunEvent, RunOutput, RunOutputEvent
50
51
  from agno.run.base import RunStatus
@@ -437,7 +438,7 @@ class Team:
437
438
  self,
438
439
  members: List[Union[Agent, "Team"]],
439
440
  id: Optional[str] = None,
440
- model: Optional[Model] = None,
441
+ model: Optional[Union[Model, str]] = None,
441
442
  name: Optional[str] = None,
442
443
  role: Optional[str] = None,
443
444
  respond_directly: bool = False,
@@ -498,9 +499,9 @@ class Team:
498
499
  post_hooks: Optional[Union[List[Callable[..., Any]], List[BaseGuardrail]]] = None,
499
500
  input_schema: Optional[Type[BaseModel]] = None,
500
501
  output_schema: Optional[Type[BaseModel]] = None,
501
- parser_model: Optional[Model] = None,
502
+ parser_model: Optional[Union[Model, str]] = None,
502
503
  parser_model_prompt: Optional[str] = None,
503
- output_model: Optional[Model] = None,
504
+ output_model: Optional[Union[Model, str]] = None,
504
505
  output_model_prompt: Optional[str] = None,
505
506
  use_json_mode: bool = False,
506
507
  parse_response: bool = True,
@@ -514,7 +515,7 @@ class Team:
514
515
  add_session_summary_to_context: Optional[bool] = None,
515
516
  metadata: Optional[Dict[str, Any]] = None,
516
517
  reasoning: bool = False,
517
- reasoning_model: Optional[Model] = None,
518
+ reasoning_model: Optional[Union[Model, str]] = None,
518
519
  reasoning_agent: Optional[Agent] = None,
519
520
  reasoning_min_steps: int = 1,
520
521
  reasoning_max_steps: int = 10,
@@ -535,7 +536,7 @@ class Team:
535
536
  ):
536
537
  self.members = members
537
538
 
538
- self.model = model
539
+ self.model = model # type: ignore[assignment]
539
540
 
540
541
  self.name = name
541
542
  self.id = id
@@ -618,9 +619,9 @@ class Team:
618
619
 
619
620
  self.input_schema = input_schema
620
621
  self.output_schema = output_schema
621
- self.parser_model = parser_model
622
+ self.parser_model = parser_model # type: ignore[assignment]
622
623
  self.parser_model_prompt = parser_model_prompt
623
- self.output_model = output_model
624
+ self.output_model = output_model # type: ignore[assignment]
624
625
  self.output_model_prompt = output_model_prompt
625
626
  self.use_json_mode = use_json_mode
626
627
  self.parse_response = parse_response
@@ -637,7 +638,7 @@ class Team:
637
638
  self.metadata = metadata
638
639
 
639
640
  self.reasoning = reasoning
640
- self.reasoning_model = reasoning_model
641
+ self.reasoning_model = reasoning_model # type: ignore[assignment]
641
642
  self.reasoning_agent = reasoning_agent
642
643
  self.reasoning_min_steps = reasoning_min_steps
643
644
  self.reasoning_max_steps = reasoning_max_steps
@@ -694,6 +695,8 @@ class Team:
694
695
  # Lazy-initialized shared thread pool executor for background tasks (memory, cultural knowledge, etc.)
695
696
  self._background_executor: Optional[Any] = None
696
697
 
698
+ self._resolve_models()
699
+
697
700
  @property
698
701
  def background_executor(self) -> Any:
699
702
  """Lazy initialization of shared thread pool executor for background tasks.
@@ -902,6 +905,17 @@ class Team:
902
905
  """Return True if the db the team is equipped with is an Async implementation"""
903
906
  return self.db is not None and isinstance(self.db, AsyncBaseDb)
904
907
 
908
+ def _resolve_models(self) -> None:
909
+ """Resolve model strings to Model instances."""
910
+ if self.model is not None:
911
+ self.model = get_model(self.model)
912
+ if self.reasoning_model is not None:
913
+ self.reasoning_model = get_model(self.reasoning_model)
914
+ if self.parser_model is not None:
915
+ self.parser_model = get_model(self.parser_model)
916
+ if self.output_model is not None:
917
+ self.output_model = get_model(self.output_model)
918
+
905
919
  def initialize_team(self, debug_mode: Optional[bool] = None) -> None:
906
920
  # Make sure for the team, we are using the team logger
907
921
  use_team_logger()
@@ -1593,6 +1607,7 @@ class Team:
1593
1607
  tools=_tools,
1594
1608
  response_format=response_format,
1595
1609
  stream_events=stream_events,
1610
+ session_state=session_state,
1596
1611
  ):
1597
1612
  raise_if_cancelled(run_response.run_id) # type: ignore
1598
1613
  yield event
@@ -1604,6 +1619,7 @@ class Team:
1604
1619
  tools=_tools,
1605
1620
  response_format=response_format,
1606
1621
  stream_events=stream_events,
1622
+ session_state=session_state,
1607
1623
  ):
1608
1624
  raise_if_cancelled(run_response.run_id) # type: ignore
1609
1625
  from agno.run.team import IntermediateRunContentEvent, RunContentEvent
@@ -1932,6 +1948,7 @@ class Team:
1932
1948
  team_id=self.id,
1933
1949
  team_name=self.name,
1934
1950
  metadata=metadata,
1951
+ session_state=session_state,
1935
1952
  input=run_input,
1936
1953
  )
1937
1954
 
@@ -2439,6 +2456,7 @@ class Team:
2439
2456
  tools=_tools,
2440
2457
  response_format=response_format,
2441
2458
  stream_events=stream_events,
2459
+ session_state=session_state,
2442
2460
  ):
2443
2461
  raise_if_cancelled(run_response.run_id) # type: ignore
2444
2462
  yield event
@@ -2450,6 +2468,7 @@ class Team:
2450
2468
  tools=_tools,
2451
2469
  response_format=response_format,
2452
2470
  stream_events=stream_events,
2471
+ session_state=session_state,
2453
2472
  ):
2454
2473
  raise_if_cancelled(run_response.run_id) # type: ignore
2455
2474
  from agno.run.team import IntermediateRunContentEvent, RunContentEvent
@@ -2771,6 +2790,7 @@ class Team:
2771
2790
  team_id=self.id,
2772
2791
  team_name=self.name,
2773
2792
  metadata=metadata,
2793
+ session_state=session_state,
2774
2794
  input=run_input,
2775
2795
  )
2776
2796
 
@@ -2930,6 +2950,12 @@ class Team:
2930
2950
  if model_response.audio is not None:
2931
2951
  run_response.response_audio = model_response.audio
2932
2952
 
2953
+ # Update session_state with changes from model response
2954
+ if model_response.updated_session_state is not None and run_response.session_state is not None:
2955
+ from agno.utils.merge_dict import merge_dictionaries
2956
+
2957
+ merge_dictionaries(run_response.session_state, model_response.updated_session_state)
2958
+
2933
2959
  # Build a list of messages that should be added to the RunOutput
2934
2960
  messages_for_run_response = [m for m in run_messages.messages if m.add_to_agent_memory]
2935
2961
 
@@ -2954,6 +2980,7 @@ class Team:
2954
2980
  tools: Optional[List[Union[Function, dict]]] = None,
2955
2981
  response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
2956
2982
  stream_events: bool = False,
2983
+ session_state: Optional[Dict[str, Any]] = None,
2957
2984
  ) -> Iterator[Union[TeamRunOutputEvent, RunOutputEvent]]:
2958
2985
  self.model = cast(Model, self.model)
2959
2986
 
@@ -2985,6 +3012,7 @@ class Team:
2985
3012
  reasoning_state=reasoning_state,
2986
3013
  stream_events=stream_events,
2987
3014
  parse_structured_output=self.should_parse_structured_output,
3015
+ session_state=session_state,
2988
3016
  )
2989
3017
 
2990
3018
  # 3. Update TeamRunOutput
@@ -3036,6 +3064,7 @@ class Team:
3036
3064
  tools: Optional[List[Union[Function, dict]]] = None,
3037
3065
  response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
3038
3066
  stream_events: bool = False,
3067
+ session_state: Optional[Dict[str, Any]] = None,
3039
3068
  ) -> AsyncIterator[Union[TeamRunOutputEvent, RunOutputEvent]]:
3040
3069
  self.model = cast(Model, self.model)
3041
3070
 
@@ -3068,6 +3097,7 @@ class Team:
3068
3097
  reasoning_state=reasoning_state,
3069
3098
  stream_events=stream_events,
3070
3099
  parse_structured_output=self.should_parse_structured_output,
3100
+ session_state=session_state,
3071
3101
  ):
3072
3102
  yield event
3073
3103
 
@@ -3122,6 +3152,7 @@ class Team:
3122
3152
  reasoning_state: Optional[Dict[str, Any]] = None,
3123
3153
  stream_events: bool = False,
3124
3154
  parse_structured_output: bool = False,
3155
+ session_state: Optional[Dict[str, Any]] = None,
3125
3156
  ) -> Iterator[Union[TeamRunOutputEvent, RunOutputEvent]]:
3126
3157
  if isinstance(model_response_event, tuple(get_args(RunOutputEvent))) or isinstance(
3127
3158
  model_response_event, tuple(get_args(TeamRunOutputEvent))
@@ -3299,10 +3330,15 @@ class Team:
3299
3330
 
3300
3331
  # If the model response is a tool_call_completed, update the existing tool call in the run_response
3301
3332
  elif model_response_event.event == ModelResponseEvent.tool_call_completed.value:
3302
- if model_response_event.updated_session_state is not None and session.session_data is not None:
3303
- merge_dictionaries(
3304
- session.session_data["session_state"], model_response_event.updated_session_state
3305
- )
3333
+ if model_response_event.updated_session_state is not None:
3334
+ # Update the session_state variable that TeamRunOutput references
3335
+ if session_state is not None:
3336
+ merge_dictionaries(session_state, model_response_event.updated_session_state)
3337
+ # Also update the DB session object
3338
+ if session.session_data is not None:
3339
+ merge_dictionaries(
3340
+ session.session_data["session_state"], model_response_event.updated_session_state
3341
+ )
3306
3342
 
3307
3343
  if model_response_event.images is not None:
3308
3344
  for image in model_response_event.images:
@@ -4498,7 +4534,7 @@ class Team:
4498
4534
  store_events=self.store_events,
4499
4535
  )
4500
4536
  else:
4501
- log_warning(
4537
+ log_info(
4502
4538
  f"Reasoning model: {reasoning_model.__class__.__name__} is not a native reasoning model, defaulting to manual Chain-of-Thought reasoning"
4503
4539
  )
4504
4540
  use_default_reasoning = True
@@ -4779,7 +4815,7 @@ class Team:
4779
4815
  store_events=self.store_events,
4780
4816
  )
4781
4817
  else:
4782
- log_warning(
4818
+ log_info(
4783
4819
  f"Reasoning model: {reasoning_model.__class__.__name__} is not a native reasoning model, defaulting to manual Chain-of-Thought reasoning"
4784
4820
  )
4785
4821
  use_default_reasoning = True
agno/tools/file.py CHANGED
@@ -1,9 +1,9 @@
1
1
  import json
2
2
  from pathlib import Path
3
- from typing import Any, List, Optional
3
+ from typing import Any, List, Optional, Tuple
4
4
 
5
5
  from agno.tools import Toolkit
6
- from agno.utils.log import log_debug, log_error, log_info
6
+ from agno.utils.log import log_debug, log_error
7
7
 
8
8
 
9
9
  class FileTools(Toolkit):
@@ -12,14 +12,26 @@ class FileTools(Toolkit):
12
12
  base_dir: Optional[Path] = None,
13
13
  enable_save_file: bool = True,
14
14
  enable_read_file: bool = True,
15
+ enable_delete_file: bool = False,
15
16
  enable_list_files: bool = True,
16
17
  enable_search_files: bool = True,
18
+ enable_read_file_chunk: bool = True,
19
+ enable_replace_file_chunk: bool = True,
20
+ expose_base_directory: bool = False,
21
+ max_file_length: int = 10000000,
22
+ max_file_lines: int = 100000,
23
+ line_separator: str = "\n",
17
24
  all: bool = False,
18
25
  **kwargs,
19
26
  ):
20
27
  self.base_dir: Path = base_dir or Path.cwd()
28
+ self.base_dir = self.base_dir.resolve()
21
29
 
22
30
  tools: List[Any] = []
31
+ self.max_file_length = max_file_length
32
+ self.max_file_lines = max_file_lines
33
+ self.line_separator = line_separator
34
+ self.expose_base_directory = expose_base_directory
23
35
  if all or enable_save_file:
24
36
  tools.append(self.save_file)
25
37
  if all or enable_read_file:
@@ -28,10 +40,16 @@ class FileTools(Toolkit):
28
40
  tools.append(self.list_files)
29
41
  if all or enable_search_files:
30
42
  tools.append(self.search_files)
43
+ if all or enable_delete_file:
44
+ tools.append(self.delete_file)
45
+ if all or enable_read_file_chunk:
46
+ tools.append(self.read_file_chunk)
47
+ if all or enable_replace_file_chunk:
48
+ tools.append(self.replace_file_chunk)
31
49
 
32
50
  super().__init__(name="file_tools", tools=tools, **kwargs)
33
51
 
34
- def save_file(self, contents: str, file_name: str, overwrite: bool = True) -> str:
52
+ def save_file(self, contents: str, file_name: str, overwrite: bool = True, encoding: str = "utf-8") -> str:
35
53
  """Saves the contents to a file called `file_name` and returns the file name if successful.
36
54
 
37
55
  :param contents: The contents to save.
@@ -40,44 +58,146 @@ class FileTools(Toolkit):
40
58
  :return: The file name if successful, otherwise returns an error message.
41
59
  """
42
60
  try:
43
- file_path = self.base_dir.joinpath(file_name)
61
+ safe, file_path = self.check_escape(file_name)
62
+ if not (safe):
63
+ log_error(f"Attempted to save file: {file_name}")
64
+ return "Error saving file"
44
65
  log_debug(f"Saving contents to {file_path}")
45
66
  if not file_path.parent.exists():
46
67
  file_path.parent.mkdir(parents=True, exist_ok=True)
47
68
  if file_path.exists() and not overwrite:
48
69
  return f"File {file_name} already exists"
49
- file_path.write_text(contents)
50
- log_info(f"Saved: {file_path}")
70
+ file_path.write_text(contents, encoding=encoding)
71
+ log_debug(f"Saved: {file_path}")
51
72
  return str(file_name)
52
73
  except Exception as e:
53
74
  log_error(f"Error saving to file: {e}")
54
75
  return f"Error saving to file: {e}"
55
76
 
56
- def read_file(self, file_name: str) -> str:
77
+ def read_file_chunk(self, file_name: str, start_line: int, end_line: int, encoding: str = "utf-8") -> str:
78
+ """Reads the contents of the file `file_name` and returns lines from start_line to end_line.
79
+
80
+ :param file_name: The name of the file to read.
81
+ :param start_line: Number of first line in the returned chunk
82
+ :param end_line: Number of the last line in the returned chunk
83
+ :param encoding: Encoding to use, default - utf-8
84
+
85
+ :return: The contents of the selected chunk
86
+ """
87
+ try:
88
+ log_debug(f"Reading file: {file_name}")
89
+ safe, file_path = self.check_escape(file_name)
90
+ if not (safe):
91
+ log_error(f"Attempted to read file: {file_name}")
92
+ return "Error reading file"
93
+ contents = file_path.read_text(encoding=encoding)
94
+ lines = contents.split(self.line_separator)
95
+ return self.line_separator.join(lines[start_line : end_line + 1])
96
+ except Exception as e:
97
+ log_error(f"Error reading file: {e}")
98
+ return f"Error reading file: {e}"
99
+
100
+ def replace_file_chunk(
101
+ self, file_name: str, start_line: int, end_line: int, chunk: str, encoding: str = "utf-8"
102
+ ) -> str:
103
+ """Reads the contents of the file, replaces lines
104
+ between start_line and end_line with chunk and writes the file
105
+
106
+ :param file_name: The name of the file to process.
107
+ :param start_line: Number of first line in the replaced chunk
108
+ :param end_line: Number of the last line in the replaced chunk
109
+ :param chunk: String to be inserted instead of lines from start_line to end_line. Can have multiple lines.
110
+ :param encoding: Encoding to use, default - utf-8
111
+
112
+ :return: file name if successfull, error message otherwise
113
+ """
114
+ try:
115
+ log_debug(f"Patching file: {file_name}")
116
+ safe, file_path = self.check_escape(file_name)
117
+ if not (safe):
118
+ log_error(f"Attempted to read file: {file_name}")
119
+ return "Error reading file"
120
+ contents = file_path.read_text(encoding=encoding)
121
+ lines = contents.split(self.line_separator)
122
+ start = lines[0:start_line]
123
+ end = lines[end_line + 1 :]
124
+ return self.save_file(
125
+ file_name=file_name, contents=self.line_separator.join(start + [chunk] + end), encoding=encoding
126
+ )
127
+ except Exception as e:
128
+ log_error(f"Error patching file: {e}")
129
+ return f"Error patching file: {e}"
130
+
131
+ def read_file(self, file_name: str, encoding: str = "utf-8") -> str:
57
132
  """Reads the contents of the file `file_name` and returns the contents if successful.
58
133
 
59
134
  :param file_name: The name of the file to read.
135
+ :param encoding: Encoding to use, default - utf-8
60
136
  :return: The contents of the file if successful, otherwise returns an error message.
61
137
  """
62
138
  try:
63
- log_info(f"Reading file: {file_name}")
64
- file_path = self.base_dir.joinpath(file_name)
65
- contents = file_path.read_text(encoding="utf-8")
139
+ log_debug(f"Reading file: {file_name}")
140
+ safe, file_path = self.check_escape(file_name)
141
+ if not (safe):
142
+ log_error(f"Attempted to read file: {file_name}")
143
+ return "Error reading file"
144
+ contents = file_path.read_text(encoding=encoding)
145
+ if len(contents) > self.max_file_length:
146
+ return "Error reading file: file too long. Use read_file_chunk instead"
147
+ if len(contents.split(self.line_separator)) > self.max_file_lines:
148
+ return "Error reading file: file too long. Use read_file_chunk instead"
149
+
66
150
  return str(contents)
67
151
  except Exception as e:
68
152
  log_error(f"Error reading file: {e}")
69
153
  return f"Error reading file: {e}"
70
154
 
71
- def list_files(self) -> str:
72
- """Returns a list of files in the base directory
155
+ def delete_file(self, file_name: str) -> str:
156
+ """Deletes a file
157
+ :param file_name: Name of the file to delete
158
+
159
+ :return: Empty string, if operation succeeded, otherwise returns an error message
160
+ """
161
+ safe, path = self.check_escape(file_name)
162
+ try:
163
+ if safe:
164
+ if path.is_dir():
165
+ path.rmdir()
166
+ return ""
167
+ path.unlink()
168
+ return ""
169
+ else:
170
+ log_error(f"Attempt to delete file outside {self.base_dir}: {file_name}")
171
+ return "Incorrect file_name"
172
+ except Exception as e:
173
+ log_error(f"Error removing {file_name}: {e}")
174
+ return f"Error removing file: {e}"
175
+
176
+ def check_escape(self, relative_path: str) -> Tuple[bool, Path]:
177
+ d = self.base_dir.joinpath(Path(relative_path)).resolve()
178
+ if self.base_dir == d:
179
+ return True, d
180
+ try:
181
+ d.relative_to(self.base_dir)
182
+ except ValueError:
183
+ log_error("Attempted to escape base_dir")
184
+ return False, self.base_dir
185
+ return True, d
186
+
187
+ def list_files(self, **kwargs) -> str:
188
+ """Returns a list of files in directory
189
+ :param directory: (Optional) name of directory to list.
73
190
 
74
191
  :return: The contents of the file if successful, otherwise returns an error message.
75
192
  """
193
+ directory = kwargs.get("directory", ".")
76
194
  try:
77
- log_info(f"Reading files in : {self.base_dir}")
78
- return json.dumps(
79
- [str(file_path.relative_to(self.base_dir)) for file_path in self.base_dir.iterdir()], indent=4
80
- )
195
+ log_debug(f"Reading files in : {self.base_dir}/{directory}")
196
+ safe, d = self.check_escape(directory)
197
+ if safe:
198
+ return json.dumps([str(file_path.relative_to(self.base_dir)) for file_path in d.iterdir()], indent=4)
199
+ else:
200
+ return "{}"
81
201
  except Exception as e:
82
202
  log_error(f"Error reading files: {e}")
83
203
  return f"Error reading files: {e}"
@@ -94,15 +214,23 @@ class FileTools(Toolkit):
94
214
 
95
215
  log_debug(f"Searching files in {self.base_dir} with pattern {pattern}")
96
216
  matching_files = list(self.base_dir.glob(pattern))
97
-
98
- file_paths = [str(file_path.relative_to(self.base_dir)) for file_path in matching_files]
99
-
100
- result = {
101
- "pattern": pattern,
102
- "base_directory": str(self.base_dir),
103
- "matches_found": len(file_paths),
104
- "files": file_paths,
105
- }
217
+ result = None
218
+ if self.expose_base_directory:
219
+ file_paths = [str(file_path) for file_path in matching_files]
220
+ result = {
221
+ "pattern": pattern,
222
+ "matches_found": len(file_paths),
223
+ "base_directory": str(self.base_dir),
224
+ "files": file_paths,
225
+ }
226
+ else:
227
+ file_paths = [str(file_path.relative_to(self.base_dir)) for file_path in matching_files]
228
+
229
+ result = {
230
+ "pattern": pattern,
231
+ "matches_found": len(file_paths),
232
+ "files": file_paths,
233
+ }
106
234
  log_debug(f"Found {len(file_paths)} files matching pattern {pattern}")
107
235
  return json.dumps(result, indent=2)
108
236
 
agno/tools/function.py CHANGED
@@ -440,7 +440,7 @@ class Function(BaseModel):
440
440
  @staticmethod
441
441
  def _wrap_callable(func: Callable) -> Callable:
442
442
  """Wrap a callable with Pydantic's validate_call decorator, if relevant"""
443
- from inspect import isasyncgenfunction, iscoroutinefunction
443
+ from inspect import isasyncgenfunction, iscoroutinefunction, signature
444
444
 
445
445
  pydantic_version = Version(version("pydantic"))
446
446
 
@@ -458,6 +458,10 @@ class Function(BaseModel):
458
458
  # Don't wrap callables that are already wrapped with validate_call
459
459
  elif getattr(func, "_wrapped_for_validation", False):
460
460
  return func
461
+ # Don't wrap functions with session_state parameter
462
+ # session_state needs to be passed by reference, not copied by pydantic's validation
463
+ elif "session_state" in signature(func).parameters:
464
+ return func
461
465
  # Wrap the callable with validate_call
462
466
  else:
463
467
  wrapped = validate_call(func, config=dict(arbitrary_types_allowed=True)) # type: ignore