agno 2.3.21__py3-none-any.whl → 2.3.23__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 (74) hide show
  1. agno/agent/agent.py +48 -2
  2. agno/agent/remote.py +234 -73
  3. agno/client/a2a/__init__.py +10 -0
  4. agno/client/a2a/client.py +554 -0
  5. agno/client/a2a/schemas.py +112 -0
  6. agno/client/a2a/utils.py +369 -0
  7. agno/db/migrations/utils.py +19 -0
  8. agno/db/migrations/v1_to_v2.py +54 -16
  9. agno/db/migrations/versions/v2_3_0.py +92 -53
  10. agno/db/mysql/async_mysql.py +5 -7
  11. agno/db/mysql/mysql.py +5 -7
  12. agno/db/mysql/schemas.py +39 -21
  13. agno/db/postgres/async_postgres.py +172 -42
  14. agno/db/postgres/postgres.py +186 -38
  15. agno/db/postgres/schemas.py +39 -21
  16. agno/db/postgres/utils.py +6 -2
  17. agno/db/singlestore/schemas.py +41 -21
  18. agno/db/singlestore/singlestore.py +14 -3
  19. agno/db/sqlite/async_sqlite.py +7 -2
  20. agno/db/sqlite/schemas.py +36 -21
  21. agno/db/sqlite/sqlite.py +3 -7
  22. agno/knowledge/chunking/document.py +3 -2
  23. agno/knowledge/chunking/markdown.py +8 -3
  24. agno/knowledge/chunking/recursive.py +2 -2
  25. agno/models/base.py +4 -0
  26. agno/models/google/gemini.py +27 -4
  27. agno/models/openai/chat.py +1 -1
  28. agno/models/openai/responses.py +14 -7
  29. agno/os/middleware/jwt.py +66 -27
  30. agno/os/routers/agents/router.py +3 -3
  31. agno/os/routers/evals/evals.py +2 -2
  32. agno/os/routers/knowledge/knowledge.py +5 -5
  33. agno/os/routers/knowledge/schemas.py +1 -1
  34. agno/os/routers/memory/memory.py +4 -4
  35. agno/os/routers/session/session.py +2 -2
  36. agno/os/routers/teams/router.py +4 -4
  37. agno/os/routers/traces/traces.py +3 -3
  38. agno/os/routers/workflows/router.py +3 -3
  39. agno/os/schema.py +1 -1
  40. agno/reasoning/deepseek.py +11 -1
  41. agno/reasoning/gemini.py +6 -2
  42. agno/reasoning/groq.py +8 -3
  43. agno/reasoning/openai.py +2 -0
  44. agno/remote/base.py +106 -9
  45. agno/skills/__init__.py +17 -0
  46. agno/skills/agent_skills.py +370 -0
  47. agno/skills/errors.py +32 -0
  48. agno/skills/loaders/__init__.py +4 -0
  49. agno/skills/loaders/base.py +27 -0
  50. agno/skills/loaders/local.py +216 -0
  51. agno/skills/skill.py +65 -0
  52. agno/skills/utils.py +107 -0
  53. agno/skills/validator.py +277 -0
  54. agno/team/remote.py +220 -60
  55. agno/team/team.py +41 -3
  56. agno/tools/brandfetch.py +27 -18
  57. agno/tools/browserbase.py +150 -13
  58. agno/tools/function.py +6 -1
  59. agno/tools/mcp/mcp.py +300 -17
  60. agno/tools/mcp/multi_mcp.py +269 -14
  61. agno/tools/toolkit.py +89 -21
  62. agno/utils/mcp.py +49 -8
  63. agno/utils/string.py +43 -1
  64. agno/workflow/condition.py +4 -2
  65. agno/workflow/loop.py +20 -1
  66. agno/workflow/remote.py +173 -33
  67. agno/workflow/router.py +4 -1
  68. agno/workflow/steps.py +4 -0
  69. agno/workflow/workflow.py +14 -0
  70. {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/METADATA +13 -14
  71. {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/RECORD +74 -60
  72. {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/WHEEL +0 -0
  73. {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/licenses/LICENSE +0 -0
  74. {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/top_level.txt +0 -0
agno/utils/string.py CHANGED
@@ -2,13 +2,15 @@ import hashlib
2
2
  import json
3
3
  import re
4
4
  import uuid
5
- from typing import Optional, Type
5
+ from typing import Any, Optional, Type, Union
6
6
  from uuid import uuid4
7
7
 
8
8
  from pydantic import BaseModel, ValidationError
9
9
 
10
10
  from agno.utils.log import logger
11
11
 
12
+ POSTGRES_INVALID_CHARS_REGEX = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\ufffe\uffff]")
13
+
12
14
 
13
15
  def is_valid_uuid(uuid_str: str) -> bool:
14
16
  """
@@ -275,3 +277,43 @@ def generate_id_from_name(name: Optional[str] = None) -> str:
275
277
  return name.lower().replace(" ", "-").replace("_", "-")
276
278
  else:
277
279
  return str(uuid4())
280
+
281
+
282
+ def sanitize_postgres_string(value: Optional[str]) -> Optional[str]:
283
+ """Remove illegal chars from string values to prevent PostgreSQL encoding errors.
284
+
285
+ This function all chars illegal in Postgres UTF-8 text fields.
286
+ Useful to prevent CharacterNotInRepertoireError when storing strings.
287
+
288
+ Args:
289
+ value: The string value to sanitize.
290
+
291
+ Returns:
292
+ The sanitized string with illegal chars removed, or None if input was None.
293
+ """
294
+ if value is None:
295
+ return None
296
+ if isinstance(value, str):
297
+ return POSTGRES_INVALID_CHARS_REGEX.sub("", value)
298
+
299
+
300
+ def sanitize_postgres_strings(data: Union[dict, list, str, Any]) -> Union[dict, list, str, Any]:
301
+ """Recursively sanitize all string values in a dictionary or JSON structure.
302
+
303
+ This function traverses dictionaries, lists, and nested structures to find
304
+ and sanitize all string values, removing null bytes that PostgreSQL cannot handle.
305
+
306
+ Args:
307
+ data: The data structure to sanitize (dict, list, str or any other type).
308
+
309
+ Returns:
310
+ The sanitized data structure with all strings cleaned of null bytes.
311
+ """
312
+ if isinstance(data, dict):
313
+ return {key: sanitize_postgres_strings(value) for key, value in data.items()}
314
+ elif isinstance(data, list):
315
+ return [sanitize_postgres_strings(item) for item in data]
316
+ elif isinstance(data, str):
317
+ return sanitize_postgres_string(data)
318
+ else:
319
+ return data
@@ -279,7 +279,7 @@ class Condition:
279
279
  content=f"Condition {self.name} completed with {len(all_results)} results",
280
280
  success=all(result.success for result in all_results) if all_results else True,
281
281
  error=None,
282
- stop=False,
282
+ stop=any(result.stop for result in all_results) if all_results else False,
283
283
  steps=all_results,
284
284
  )
285
285
 
@@ -460,6 +460,7 @@ class Condition:
460
460
  step_type=StepType.CONDITION,
461
461
  content=f"Condition {self.name} completed with {len(all_results)} results",
462
462
  success=all(result.success for result in all_results) if all_results else True,
463
+ stop=any(result.stop for result in all_results) if all_results else False,
463
464
  steps=all_results,
464
465
  )
465
466
 
@@ -571,7 +572,7 @@ class Condition:
571
572
  content=f"Condition {self.name} completed with {len(all_results)} results",
572
573
  success=all(result.success for result in all_results) if all_results else True,
573
574
  error=None,
574
- stop=False,
575
+ stop=any(result.stop for result in all_results) if all_results else False,
575
576
  steps=all_results,
576
577
  )
577
578
 
@@ -755,5 +756,6 @@ class Condition:
755
756
  step_type=StepType.CONDITION,
756
757
  content=f"Condition {self.name} completed with {len(all_results)} results",
757
758
  success=all(result.success for result in all_results) if all_results else True,
759
+ stop=any(result.stop for result in all_results) if all_results else False,
758
760
  steps=all_results,
759
761
  )
agno/workflow/loop.py CHANGED
@@ -150,6 +150,7 @@ class Loop:
150
150
 
151
151
  all_results = []
152
152
  iteration = 0
153
+ early_termination = False
153
154
 
154
155
  while iteration < self.max_iterations:
155
156
  # Execute all steps in this iteration - mirroring workflow logic
@@ -182,6 +183,7 @@ class Loop:
182
183
 
183
184
  if any(output.stop for output in step_output):
184
185
  logger.info(f"Early termination requested by step {step_name}")
186
+ early_termination = True
185
187
  break
186
188
  else:
187
189
  # Single StepOutput
@@ -191,6 +193,7 @@ class Loop:
191
193
 
192
194
  if step_output.stop:
193
195
  logger.info(f"Early termination requested by step {step_name}")
196
+ early_termination = True
194
197
  break
195
198
 
196
199
  # Update step input for next step
@@ -209,7 +212,11 @@ class Loop:
209
212
  break
210
213
  except Exception as e:
211
214
  logger.warning(f"End condition evaluation failed: {e}")
212
- # Continue with loop if end condition fails
215
+
216
+ # Break out of iteration loop if early termination was requested
217
+ if early_termination:
218
+ log_debug(f"Loop ending early due to step termination request at iteration {iteration}")
219
+ break
213
220
 
214
221
  log_debug(f"Loop End: {self.name} ({iteration} iterations)", center=True, symbol="=")
215
222
 
@@ -224,6 +231,7 @@ class Loop:
224
231
  step_type=StepType.LOOP,
225
232
  content=f"Loop {self.name} completed {iteration} iterations with {len(flattened_results)} total steps",
226
233
  success=all(result.success for result in flattened_results) if flattened_results else True,
234
+ stop=any(result.stop for result in flattened_results) if flattened_results else False,
227
235
  steps=flattened_results,
228
236
  )
229
237
 
@@ -434,6 +442,7 @@ class Loop:
434
442
  step_type=StepType.LOOP,
435
443
  content=f"Loop {self.name} completed {iteration} iterations with {len(flattened_results)} total steps",
436
444
  success=all(result.success for result in flattened_results) if flattened_results else True,
445
+ stop=any(result.stop for result in flattened_results) if flattened_results else False,
437
446
  steps=flattened_results,
438
447
  )
439
448
 
@@ -462,6 +471,7 @@ class Loop:
462
471
 
463
472
  all_results = []
464
473
  iteration = 0
474
+ early_termination = False
465
475
 
466
476
  while iteration < self.max_iterations:
467
477
  # Execute all steps in this iteration - mirroring workflow logic
@@ -494,6 +504,7 @@ class Loop:
494
504
 
495
505
  if any(output.stop for output in step_output):
496
506
  logger.info(f"Early termination requested by step {step_name}")
507
+ early_termination = True
497
508
  break
498
509
  else:
499
510
  # Single StepOutput
@@ -503,6 +514,7 @@ class Loop:
503
514
 
504
515
  if step_output.stop:
505
516
  logger.info(f"Early termination requested by step {step_name}")
517
+ early_termination = True
506
518
  break
507
519
 
508
520
  # Update step input for next step
@@ -525,6 +537,11 @@ class Loop:
525
537
  except Exception as e:
526
538
  logger.warning(f"End condition evaluation failed: {e}")
527
539
 
540
+ # Break out of iteration loop if early termination was requested
541
+ if early_termination:
542
+ log_debug(f"Loop ending early due to step termination request at iteration {iteration}")
543
+ break
544
+
528
545
  # Use workflow logger for async loop completion
529
546
  log_debug(f"Async Loop End: {self.name} ({iteration} iterations)", center=True, symbol="=")
530
547
 
@@ -539,6 +556,7 @@ class Loop:
539
556
  step_type=StepType.LOOP,
540
557
  content=f"Loop {self.name} completed {iteration} iterations with {len(flattened_results)} total steps",
541
558
  success=all(result.success for result in flattened_results) if flattened_results else True,
559
+ stop=any(result.stop for result in flattened_results) if flattened_results else False,
542
560
  steps=flattened_results,
543
561
  )
544
562
 
@@ -752,5 +770,6 @@ class Loop:
752
770
  step_type=StepType.LOOP,
753
771
  content=f"Loop {self.name} completed {iteration} iterations with {len(flattened_results)} total steps",
754
772
  success=all(result.success for result in flattened_results) if flattened_results else True,
773
+ stop=any(result.stop for result in flattened_results) if flattened_results else False,
755
774
  steps=flattened_results,
756
775
  )
agno/workflow/remote.py CHANGED
@@ -24,21 +24,28 @@ class RemoteWorkflow(BaseRemote):
24
24
  base_url: str,
25
25
  workflow_id: str,
26
26
  timeout: float = 300.0,
27
+ protocol: Literal["agentos", "a2a"] = "agentos",
28
+ a2a_protocol: Literal["json-rpc", "rest"] = "rest",
27
29
  config_ttl: float = 300.0,
28
30
  ):
29
- """Initialize AgentOSRunner for local or remote execution.
31
+ """Initialize RemoteWorkflow for remote execution.
30
32
 
31
- For remote execution, provide base_url and workflow_id.
33
+ Supports two protocols:
34
+ - "agentos": Agno's proprietary AgentOS REST API (default)
35
+ - "a2a": A2A (Agent-to-Agent) protocol for cross-framework communication
32
36
 
33
37
  Args:
34
- base_url: Base URL for remote AgentOS instance (e.g., "http://localhost:7777")
35
- workflow_id: ID of remote workflow
38
+ base_url: Base URL for remote instance (e.g., "http://localhost:7777")
39
+ workflow_id: ID of remote workflow on the remote server
36
40
  timeout: Request timeout in seconds (default: 300)
41
+ protocol: Communication protocol - "agentos" (default) or "a2a"
42
+ a2a_protocol: For A2A protocol only - Whether to use JSON-RPC or REST protocol.
37
43
  config_ttl: Time-to-live for cached config in seconds (default: 300)
38
44
  """
39
- super().__init__(base_url, timeout, config_ttl)
45
+ super().__init__(base_url, timeout, protocol, a2a_protocol, config_ttl)
40
46
  self.workflow_id = workflow_id
41
47
  self._cached_workflow_config = None
48
+ self._config_ttl = config_ttl
42
49
 
43
50
  @property
44
51
  def id(self) -> str:
@@ -46,13 +53,38 @@ class RemoteWorkflow(BaseRemote):
46
53
 
47
54
  async def get_workflow_config(self) -> "WorkflowResponse":
48
55
  """Get the workflow config from remote (always fetches fresh)."""
49
- return await self.client.aget_workflow(self.workflow_id)
56
+ from agno.os.routers.workflows.schema import WorkflowResponse
57
+
58
+ if self.protocol == "a2a":
59
+ from agno.client.a2a.schemas import AgentCard
60
+
61
+ agent_card: Optional[AgentCard] = await self.a2a_client.aget_agent_card() # type: ignore
62
+
63
+ return WorkflowResponse(
64
+ id=self.workflow_id,
65
+ name=agent_card.name if agent_card else self.workflow_id,
66
+ description=agent_card.description if agent_card else f"A2A workflow: {self.workflow_id}",
67
+ )
68
+
69
+ # AgentOS protocol: fetch fresh config from remote
70
+ return await self.agentos_client.aget_workflow(self.workflow_id) # type: ignore
50
71
 
51
72
  @property
52
73
  def _workflow_config(self) -> "WorkflowResponse":
53
74
  """Get the workflow config from remote, cached with TTL."""
54
75
  from agno.os.routers.workflows.schema import WorkflowResponse
55
76
 
77
+ if self.protocol == "a2a":
78
+ from agno.client.a2a.schemas import AgentCard
79
+
80
+ agent_card: Optional[AgentCard] = self.a2a_client.get_agent_card() # type: ignore
81
+
82
+ return WorkflowResponse(
83
+ id=self.workflow_id,
84
+ name=agent_card.name if agent_card else self.workflow_id,
85
+ description=agent_card.description if agent_card else f"A2A workflow: {self.workflow_id}",
86
+ )
87
+
56
88
  current_time = time.time()
57
89
 
58
90
  # Check if cache is valid
@@ -62,15 +94,15 @@ class RemoteWorkflow(BaseRemote):
62
94
  return config
63
95
 
64
96
  # Fetch fresh config
65
- config: WorkflowResponse = self.client.get_workflow(self.workflow_id) # type: ignore
97
+ config: WorkflowResponse = self.agentos_client.get_workflow(self.workflow_id) # type: ignore
66
98
  self._cached_workflow_config = (config, current_time)
67
99
  return config
68
100
 
69
- def refresh_config(self) -> "WorkflowResponse":
101
+ async def refresh_config(self) -> "WorkflowResponse":
70
102
  """Force refresh the cached workflow config."""
71
103
  from agno.os.routers.workflows.schema import WorkflowResponse
72
104
 
73
- config: WorkflowResponse = self.client.get_workflow(self.workflow_id)
105
+ config: WorkflowResponse = await self.agentos_client.aget_workflow(self.workflow_id) # type: ignore
74
106
  self._cached_workflow_config = (config, time.time())
75
107
  return config
76
108
 
@@ -88,10 +120,15 @@ class RemoteWorkflow(BaseRemote):
88
120
 
89
121
  @property
90
122
  def db(self) -> Optional[RemoteDb]:
91
- if self._workflow_config is not None and self._workflow_config.db_id is not None:
123
+ if (
124
+ self.agentos_client
125
+ and self._config
126
+ and self._workflow_config is not None
127
+ and self._workflow_config.db_id is not None
128
+ ):
92
129
  return RemoteDb.from_config(
93
130
  db_id=self._workflow_config.db_id,
94
- client=self.client,
131
+ client=self.agentos_client,
95
132
  config=self._config,
96
133
  )
97
134
  return None
@@ -165,42 +202,145 @@ class RemoteWorkflow(BaseRemote):
165
202
  serialized_input = serialize_input(validated_input)
166
203
  headers = self._get_auth_headers(auth_token)
167
204
 
168
- if stream:
169
- # Handle streaming response
170
- return self.get_client().run_workflow_stream(
171
- workflow_id=self.workflow_id,
205
+ # A2A protocol path
206
+ if self.a2a_client:
207
+ return self._arun_a2a( # type: ignore[return-value]
172
208
  message=serialized_input,
173
- additional_data=additional_data,
174
- run_id=run_id,
175
- session_id=session_id,
209
+ stream=stream or False,
176
210
  user_id=user_id,
177
- audio=audio,
211
+ context_id=session_id, # Map session_id → context_id for A2A
178
212
  images=images,
179
213
  videos=videos,
214
+ audio=audio,
180
215
  files=files,
181
- session_state=session_state,
182
- stream_events=stream_events,
183
216
  headers=headers,
184
- **kwargs,
185
217
  )
218
+
219
+ # AgentOS protocol path (default)
220
+ if self.agentos_client:
221
+ if stream:
222
+ # Handle streaming response
223
+ return self.agentos_client.run_workflow_stream(
224
+ workflow_id=self.workflow_id,
225
+ message=serialized_input,
226
+ additional_data=additional_data,
227
+ run_id=run_id,
228
+ session_id=session_id,
229
+ user_id=user_id,
230
+ audio=audio,
231
+ images=images,
232
+ videos=videos,
233
+ files=files,
234
+ session_state=session_state,
235
+ stream_events=stream_events,
236
+ headers=headers,
237
+ **kwargs,
238
+ )
239
+ else:
240
+ return self.agentos_client.run_workflow( # type: ignore
241
+ workflow_id=self.workflow_id,
242
+ message=serialized_input,
243
+ additional_data=additional_data,
244
+ run_id=run_id,
245
+ session_id=session_id,
246
+ user_id=user_id,
247
+ audio=audio,
248
+ images=images,
249
+ videos=videos,
250
+ files=files,
251
+ session_state=session_state,
252
+ headers=headers,
253
+ **kwargs,
254
+ )
186
255
  else:
187
- return self.get_client().run_workflow( # type: ignore
188
- workflow_id=self.workflow_id,
189
- message=serialized_input,
190
- additional_data=additional_data,
191
- run_id=run_id,
192
- session_id=session_id,
256
+ raise ValueError("No client available")
257
+
258
+ def _arun_a2a(
259
+ self,
260
+ message: str,
261
+ stream: bool,
262
+ user_id: Optional[str],
263
+ context_id: Optional[str],
264
+ images: Optional[List[Image]],
265
+ videos: Optional[List[Video]],
266
+ audio: Optional[List[Audio]],
267
+ files: Optional[List[File]],
268
+ headers: Optional[Dict[str, str]],
269
+ ) -> Union[WorkflowRunOutput, AsyncIterator[WorkflowRunOutputEvent]]:
270
+ """Execute via A2A protocol.
271
+
272
+ Args:
273
+ message: Serialized message string
274
+ stream: Whether to stream the response
275
+ user_id: User identifier
276
+ context_id: Session/context ID (maps to session_id)
277
+ images: Images to include
278
+ videos: Videos to include
279
+ audio: Audio files to include
280
+ files: Files to include
281
+ headers: HTTP headers to include in the request (optional)
282
+ Returns:
283
+ WorkflowRunOutput for non-streaming, AsyncIterator[WorkflowRunOutputEvent] for streaming
284
+ """
285
+ if not self.a2a_client:
286
+ raise ValueError("A2A client not available")
287
+ from agno.client.a2a.utils import map_stream_events_to_workflow_run_events
288
+
289
+ if stream:
290
+ # Return async generator for streaming
291
+ event_stream = self.a2a_client.stream_message(
292
+ message=message,
293
+ context_id=context_id,
193
294
  user_id=user_id,
194
- audio=audio,
295
+ images=list(images) if images else None,
296
+ audio=list(audio) if audio else None,
297
+ videos=list(videos) if videos else None,
298
+ files=list(files) if files else None,
299
+ headers=headers,
300
+ )
301
+ return map_stream_events_to_workflow_run_events(event_stream, workflow_id=self.workflow_id) # type: ignore
302
+ else:
303
+ # Return coroutine for non-streaming
304
+ return self._arun_a2a_send( # type: ignore[return-value]
305
+ message=message,
306
+ user_id=user_id,
307
+ context_id=context_id,
195
308
  images=images,
309
+ audio=audio,
196
310
  videos=videos,
197
311
  files=files,
198
- session_state=session_state,
199
312
  headers=headers,
200
- **kwargs,
201
313
  )
202
314
 
203
- async def cancel_run(self, run_id: str, auth_token: Optional[str] = None) -> bool:
315
+ async def _arun_a2a_send(
316
+ self,
317
+ message: str,
318
+ user_id: Optional[str],
319
+ context_id: Optional[str],
320
+ images: Optional[List[Image]],
321
+ videos: Optional[List[Video]],
322
+ audio: Optional[List[Audio]],
323
+ files: Optional[List[File]],
324
+ headers: Optional[Dict[str, str]],
325
+ ) -> WorkflowRunOutput:
326
+ """Send a non-streaming A2A message and convert response to WorkflowRunOutput."""
327
+ if not self.a2a_client:
328
+ raise ValueError("A2A client not available")
329
+ from agno.client.a2a.utils import map_task_result_to_workflow_run_output
330
+
331
+ task_result = await self.a2a_client.send_message(
332
+ message=message,
333
+ context_id=context_id,
334
+ user_id=user_id,
335
+ images=list(images) if images else None,
336
+ audio=list(audio) if audio else None,
337
+ videos=list(videos) if videos else None,
338
+ files=list(files) if files else None,
339
+ headers=headers,
340
+ )
341
+ return map_task_result_to_workflow_run_output(task_result, workflow_id=self.workflow_id, user_id=user_id)
342
+
343
+ async def acancel_run(self, run_id: str, auth_token: Optional[str] = None) -> bool:
204
344
  """Cancel a running workflow execution.
205
345
 
206
346
  Args:
@@ -212,7 +352,7 @@ class RemoteWorkflow(BaseRemote):
212
352
  """
213
353
  headers = self._get_auth_headers(auth_token)
214
354
  try:
215
- await self.get_client().cancel_workflow_run(
355
+ await self.get_os_client().cancel_workflow_run(
216
356
  workflow_id=self.workflow_id,
217
357
  run_id=run_id,
218
358
  headers=headers,
agno/workflow/router.py CHANGED
@@ -267,6 +267,7 @@ class Router:
267
267
  step_type=StepType.ROUTER,
268
268
  content=f"Router {self.name} completed with {len(all_results)} results",
269
269
  success=all(result.success for result in all_results) if all_results else True,
270
+ stop=any(result.stop for result in all_results) if all_results else False,
270
271
  steps=all_results,
271
272
  )
272
273
 
@@ -438,6 +439,7 @@ class Router:
438
439
  step_type=StepType.ROUTER,
439
440
  content=f"Router {self.name} completed with {len(all_results)} results",
440
441
  success=all(result.success for result in all_results) if all_results else True,
442
+ stop=any(result.stop for result in all_results) if all_results else False,
441
443
  steps=all_results,
442
444
  )
443
445
 
@@ -544,6 +546,7 @@ class Router:
544
546
  step_type=StepType.ROUTER,
545
547
  content=f"Router {self.name} completed with {len(all_results)} results",
546
548
  success=all(result.success for result in all_results) if all_results else True,
549
+ stop=any(result.stop for result in all_results) if all_results else False,
547
550
  steps=all_results,
548
551
  )
549
552
 
@@ -718,6 +721,6 @@ class Router:
718
721
  content=f"Router {self.name} completed with {len(all_results)} results",
719
722
  success=all(result.success for result in all_results) if all_results else True,
720
723
  error=None,
721
- stop=False,
724
+ stop=any(result.stop for result in all_results) if all_results else False,
722
725
  steps=all_results,
723
726
  )
agno/workflow/steps.py CHANGED
@@ -194,6 +194,7 @@ class Steps:
194
194
  step_type=StepType.STEPS,
195
195
  content=f"Steps {self.name} completed with {len(all_results)} results",
196
196
  success=all(result.success for result in all_results) if all_results else True,
197
+ stop=any(result.stop for result in all_results) if all_results else False,
197
198
  steps=all_results,
198
199
  )
199
200
 
@@ -351,6 +352,7 @@ class Steps:
351
352
  step_type=StepType.STEPS,
352
353
  content=f"Steps {self.name} completed with {len(all_results)} results",
353
354
  success=all(result.success for result in all_results) if all_results else True,
355
+ stop=any(result.stop for result in all_results) if all_results else False,
354
356
  steps=all_results,
355
357
  )
356
358
 
@@ -443,6 +445,7 @@ class Steps:
443
445
  step_type=StepType.STEPS,
444
446
  content=f"Steps {self.name} completed with {len(all_results)} results",
445
447
  success=all(result.success for result in all_results) if all_results else True,
448
+ stop=any(result.stop for result in all_results) if all_results else False,
446
449
  steps=all_results,
447
450
  )
448
451
 
@@ -599,6 +602,7 @@ class Steps:
599
602
  step_type=StepType.STEPS,
600
603
  content=f"Steps {self.name} completed with {len(all_results)} results",
601
604
  success=all(result.success for result in all_results) if all_results else True,
605
+ stop=any(result.stop for result in all_results) if all_results else False,
602
606
  steps=all_results,
603
607
  )
604
608
 
agno/workflow/workflow.py CHANGED
@@ -36,6 +36,9 @@ from agno.models.message import Message
36
36
  from agno.models.metrics import Metrics
37
37
  from agno.run import RunContext, RunStatus
38
38
  from agno.run.agent import RunContentEvent, RunEvent, RunOutput
39
+ from agno.run.cancel import (
40
+ acancel_run as acancel_run_global,
41
+ )
39
42
  from agno.run.cancel import (
40
43
  acleanup_run,
41
44
  araise_if_cancelled,
@@ -3496,6 +3499,17 @@ class Workflow:
3496
3499
  """
3497
3500
  return cancel_run_global(run_id)
3498
3501
 
3502
+ async def acancel_run(self, run_id: str) -> bool:
3503
+ """Cancel a running workflow execution (async version).
3504
+
3505
+ Args:
3506
+ run_id (str): The run_id to cancel.
3507
+
3508
+ Returns:
3509
+ bool: True if the run was found and marked for cancellation, False otherwise.
3510
+ """
3511
+ return await acancel_run_global(run_id)
3512
+
3499
3513
  @overload
3500
3514
  def run(
3501
3515
  self,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agno
3
- Version: 2.3.21
3
+ Version: 2.3.23
4
4
  Summary: Agno: a lightweight library for building Multi-Agent Systems
5
5
  Author-email: Ashpreet Bedi <ashpreet@agno.com>
6
6
  Project-URL: homepage, https://agno.com
@@ -157,7 +157,7 @@ Requires-Dist: mcp>=1.9.2; extra == "mcp"
157
157
  Provides-Extra: mem0
158
158
  Requires-Dist: mem0ai; extra == "mem0"
159
159
  Provides-Extra: memori
160
- Requires-Dist: memorisdk==3.0.5; extra == "memori"
160
+ Requires-Dist: memori>=3.0.5; extra == "memori"
161
161
  Provides-Extra: newspaper
162
162
  Requires-Dist: newspaper4k; extra == "newspaper"
163
163
  Requires-Dist: lxml_html_clean; extra == "newspaper"
@@ -425,23 +425,22 @@ Dynamic: license-file
425
425
 
426
426
  Agno is a multi-agent framework, runtime, and control plane. Use it to build private and secure AI products that run in your cloud.
427
427
 
428
- - **Build** agents, teams, and workflows with memory, knowledge, guardrails, and 100+ toolkits.
428
+ - **Build** agents, teams, and workflows with memory, knowledge, guardrails and 100+ integrations.
429
429
  - **Run** in production with a stateless FastAPI runtime. Horizontally scalable.
430
430
  - **Manage** with a control plane that connects directly to your runtime — no data leaves your environment.
431
431
 
432
432
  ## Why Agno?
433
433
 
434
- - **Your cloud, your data:** AgentOS runs entirely in your infrastructure. Zero data leaves your environment.
435
- - **Production-ready from day one:** Pre-built FastAPI runtime with SSE endpoints, ready to deploy.
436
- - **Actually fast:** 529× faster than LangGraph, 24× lower memory. Matters at scale.
434
+ - **Your cloud, your data:** Runs entirely in your infrastructure. Nothing leaves your environment.
435
+ - **Ready for production on day one:** Pre-built FastAPI runtime with SSE endpoints, ready to deploy.
436
+ - **Incredibly fast:** 529× faster than LangGraph, 24× lower memory.
437
437
 
438
438
  ## Getting Started
439
439
 
440
- New to Agno? Start with the [getting started guide](https://github.com/agno-agi/agno/tree/main/cookbook/00_getting_started).
440
+ Start with the [getting started guide](https://github.com/agno-agi/agno/tree/main/cookbook/00_getting_started), then:
441
441
 
442
- Then:
443
442
  - Browse the [cookbooks](https://github.com/agno-agi/agno/tree/main/cookbook) for real-world examples
444
- - Read the [docs](https://docs.agno.com) to learn more.
443
+ - Read the [docs](https://docs.agno.com) to learn more
445
444
 
446
445
  ## Resources
447
446
 
@@ -509,26 +508,26 @@ This isn't a privacy mode or enterprise add-on. It's how Agno works.
509
508
 
510
509
  ## Features
511
510
 
512
- **Core**
511
+ ### Core:
513
512
  - Model agnostic — works with OpenAI, Anthropic, Google, local models, whatever
514
513
  - Type-safe I/O with `input_schema` and `output_schema`
515
514
  - Async-first, built for long-running tasks
516
515
  - Natively multimodal (text, images, audio, video, files)
517
516
 
518
- **Memory & Knowledge**
517
+ ### Memory & Knowledge:
519
518
  - Persistent storage for session history and state
520
519
  - User memory that persists across sessions
521
520
  - Agentic RAG with 20+ vector stores, hybrid search, reranking
522
521
  - Culture — shared long-term memory across agents
523
522
 
524
- **Execution**
523
+ ### Execution:
525
524
  - Human-in-the-loop (confirmations, approvals, overrides)
526
525
  - Guardrails for validation and security
527
526
  - Pre/post hooks for the agent lifecycle
528
527
  - First-class MCP and A2A support
529
528
  - 100+ built-in toolkits
530
529
 
531
- **Production**
530
+ ### Production:
532
531
  - Ready-to-use FastAPI runtime
533
532
  - Integrated control plane UI
534
533
  - Evals for accuracy, performance, latency
@@ -546,7 +545,7 @@ We're obsessive about performance because agent workloads spawn hundreds of inst
546
545
  | Instantiation | **3μs** | 1,587μs (529× slower) | 170μs (57× slower) | 210μs (70× slower) |
547
546
  | Memory | **6.6 KiB** | 161 KiB (24× higher) | 29 KiB (4× higher) | 66 KiB (10× higher) |
548
547
 
549
- Run the benchmarks yourself: [`cookbook/evals/performance`](https://github.com/agno-agi/agno/tree/main/cookbook/evals/performance)
548
+ Run the benchmarks yourself: [`cookbook/12_evals/performance`](https://github.com/agno-agi/agno/tree/main/cookbook/12_evals/performance)
550
549
 
551
550
  https://github.com/user-attachments/assets/54b98576-1859-4880-9f2d-15e1a426719d
552
551