agnt5 0.2.8a7__cp310-abi3-macosx_11_0_arm64.whl → 0.2.8a9__cp310-abi3-macosx_11_0_arm64.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 agnt5 might be problematic. Click here for more details.

agnt5/workflow.py CHANGED
@@ -7,10 +7,10 @@ import functools
7
7
  import inspect
8
8
  import logging
9
9
  import uuid
10
- from typing import Any, Callable, Dict, List, Optional, TypeVar, cast
10
+ from typing import Any, Awaitable, Callable, Dict, List, Optional, TypeVar, Union, cast
11
11
 
12
12
  from ._schema_utils import extract_function_metadata, extract_function_schemas
13
- from .context import Context
13
+ from .context import Context, set_current_context
14
14
  from .entity import Entity, EntityState, _get_state_adapter
15
15
  from .function import FunctionContext
16
16
  from .types import HandlerFunc, WorkflowConfig
@@ -23,6 +23,7 @@ T = TypeVar("T")
23
23
  # Global workflow registry
24
24
  _WORKFLOW_REGISTRY: Dict[str, WorkflowConfig] = {}
25
25
 
26
+
26
27
  class WorkflowContext(Context):
27
28
  """
28
29
  Context for durable workflows.
@@ -32,17 +33,28 @@ class WorkflowContext(Context):
32
33
  - Step tracking and replay
33
34
  - Orchestration (task, parallel, gather)
34
35
  - Checkpointing (step)
36
+ - Memory scoping (session_id, user_id for multi-level memory)
35
37
 
36
38
  WorkflowContext delegates state to the underlying WorkflowEntity,
37
39
  which provides durability and state change tracking for AI workflows.
40
+
41
+ Memory Scoping:
42
+ - run_id: Unique workflow run identifier
43
+ - session_id: For multi-turn conversations (optional)
44
+ - user_id: For user-scoped long-term memory (optional)
45
+ These identifiers enable agents to automatically select the appropriate
46
+ memory scope (run/session/user) via context propagation.
38
47
  """
39
48
 
40
49
  def __init__(
41
50
  self,
42
51
  workflow_entity: "WorkflowEntity", # Forward reference
43
52
  run_id: str,
53
+ session_id: Optional[str] = None,
54
+ user_id: Optional[str] = None,
44
55
  attempt: int = 0,
45
56
  runtime_context: Optional[Any] = None,
57
+ checkpoint_callback: Optional[Callable[[dict], None]] = None,
46
58
  ) -> None:
47
59
  """
48
60
  Initialize workflow context.
@@ -50,15 +62,41 @@ class WorkflowContext(Context):
50
62
  Args:
51
63
  workflow_entity: WorkflowEntity instance managing workflow state
52
64
  run_id: Unique workflow run identifier
65
+ session_id: Session identifier for multi-turn conversations (default: run_id)
66
+ user_id: User identifier for user-scoped memory (optional)
53
67
  attempt: Retry attempt number (0-indexed)
54
68
  runtime_context: RuntimeContext for trace correlation
69
+ checkpoint_callback: Optional callback for sending real-time checkpoints
55
70
  """
56
71
  super().__init__(run_id, attempt, runtime_context)
57
72
  self._workflow_entity = workflow_entity
58
73
  self._step_counter: int = 0 # Track step sequence
74
+ self._sequence_number: int = 0 # Global sequence for checkpoints
75
+ self._checkpoint_callback = checkpoint_callback
76
+
77
+ # Memory scoping identifiers
78
+ self.session_id = session_id or run_id # Default: session = run (ephemeral)
79
+ self.user_id = user_id # Optional: user-scoped memory
59
80
 
60
81
  # === State Management ===
61
82
 
83
+ def _send_checkpoint(self, checkpoint_type: str, checkpoint_data: dict) -> None:
84
+ """
85
+ Send a checkpoint via the checkpoint callback.
86
+
87
+ Args:
88
+ checkpoint_type: Type of checkpoint (e.g., "workflow.state.changed")
89
+ checkpoint_data: Checkpoint payload
90
+ """
91
+ if self._checkpoint_callback:
92
+ self._sequence_number += 1
93
+ checkpoint = {
94
+ "checkpoint_type": checkpoint_type,
95
+ "checkpoint_data": checkpoint_data,
96
+ "sequence_number": self._sequence_number,
97
+ }
98
+ self._checkpoint_callback(checkpoint)
99
+
62
100
  @property
63
101
  def state(self):
64
102
  """
@@ -71,7 +109,11 @@ class WorkflowContext(Context):
71
109
  ctx.state.set("status", "processing")
72
110
  status = ctx.state.get("status")
73
111
  """
74
- return self._workflow_entity.state
112
+ state = self._workflow_entity.state
113
+ # Pass checkpoint callback to state for real-time streaming
114
+ if hasattr(state, "_set_checkpoint_callback"):
115
+ state._set_checkpoint_callback(self._send_checkpoint)
116
+ return state
75
117
 
76
118
  # === Orchestration ===
77
119
 
@@ -129,7 +171,7 @@ class WorkflowContext(Context):
129
171
  # Extract handler name from function reference or use string
130
172
  if callable(handler):
131
173
  handler_name = handler.__name__
132
- if not hasattr(handler, '_agnt5_config'):
174
+ if not hasattr(handler, "_agnt5_config"):
133
175
  raise ValueError(
134
176
  f"Function '{handler_name}' is not a registered @function. "
135
177
  f"Did you forget to add the @function decorator?"
@@ -147,32 +189,104 @@ class WorkflowContext(Context):
147
189
  self._logger.info(f"🔄 Replaying cached step: {step_name}")
148
190
  return result
149
191
 
150
- # Execute function
192
+ # Emit workflow.step.started checkpoint
193
+ self._send_checkpoint(
194
+ "workflow.step.started",
195
+ {
196
+ "step_name": step_name,
197
+ "handler_name": handler_name,
198
+ "input": args or kwargs,
199
+ },
200
+ )
201
+
202
+ # Execute function with OpenTelemetry span
151
203
  self._logger.info(f"▶️ Executing new step: {step_name}")
152
204
  func_config = FunctionRegistry.get(handler_name)
153
205
  if func_config is None:
154
206
  raise ValueError(f"Function '{handler_name}' not found in registry")
155
207
 
156
- # Create FunctionContext for the function execution
157
- func_ctx = FunctionContext(
158
- run_id=f"{self.run_id}:task:{handler_name}",
159
- runtime_context=self._runtime_context,
160
- )
208
+ # Import span creation utility and JSON serialization
209
+ from ._core import create_span
210
+ import json
211
+
212
+ # Serialize input data for span attributes
213
+ input_repr = json.dumps({"args": args, "kwargs": kwargs}) if args or kwargs else "{}"
214
+
215
+ # Create span for task execution
216
+ with create_span(
217
+ f"workflow.task.{handler_name}",
218
+ "function",
219
+ self._runtime_context,
220
+ {
221
+ "step_name": step_name,
222
+ "handler_name": handler_name,
223
+ "run_id": self.run_id,
224
+ "input.data": input_repr,
225
+ },
226
+ ) as span:
227
+ # Create FunctionContext for the function execution
228
+ func_ctx = FunctionContext(
229
+ run_id=f"{self.run_id}:task:{handler_name}",
230
+ runtime_context=self._runtime_context,
231
+ )
161
232
 
162
- # Execute function with arguments
163
- # Support legacy pattern: ctx.task("func_name", input=data) or ctx.task(func_ref, input=data)
164
- if len(args) == 0 and "input" in kwargs:
165
- # Legacy pattern - single input parameter
166
- input_data = kwargs.pop("input") # Remove from kwargs
167
- result = await func_config.handler(func_ctx, input_data, **kwargs)
168
- else:
169
- # Type-safe pattern - pass all args/kwargs
170
- result = await func_config.handler(func_ctx, *args, **kwargs)
233
+ try:
234
+ # Execute function with arguments
235
+ # Support legacy pattern: ctx.task("func_name", input=data) or ctx.task(func_ref, input=data)
236
+ if len(args) == 0 and "input" in kwargs:
237
+ # Legacy pattern - single input parameter
238
+ input_data = kwargs.pop("input") # Remove from kwargs
239
+ result = await func_config.handler(func_ctx, input_data, **kwargs)
240
+ else:
241
+ # Type-safe pattern - pass all args/kwargs
242
+ result = await func_config.handler(func_ctx, *args, **kwargs)
243
+
244
+ # Add output data to span
245
+ try:
246
+ output_repr = json.dumps(result)
247
+ span.set_attribute("output.data", output_repr)
248
+ except (TypeError, ValueError):
249
+ # If result is not JSON serializable, use repr
250
+ span.set_attribute("output.data", repr(result))
251
+
252
+ # Record step completion in WorkflowEntity
253
+ self._workflow_entity.record_step_completion(
254
+ step_name, handler_name, args or kwargs, result
255
+ )
171
256
 
172
- # Record step completion in WorkflowEntity
173
- self._workflow_entity.record_step_completion(step_name, handler_name, args or kwargs, result)
257
+ # Emit workflow.step.completed checkpoint
258
+ self._send_checkpoint(
259
+ "workflow.step.completed",
260
+ {
261
+ "step_name": step_name,
262
+ "handler_name": handler_name,
263
+ "input": args or kwargs,
264
+ "result": result,
265
+ },
266
+ )
174
267
 
175
- return result
268
+ return result
269
+
270
+ except Exception as e:
271
+ # Emit workflow.step.error checkpoint
272
+ self._send_checkpoint(
273
+ "workflow.step.error",
274
+ {
275
+ "step_name": step_name,
276
+ "handler_name": handler_name,
277
+ "input": args or kwargs,
278
+ "error": str(e),
279
+ "error_type": type(e).__name__,
280
+ },
281
+ )
282
+
283
+ # Record error in span
284
+ span.set_attribute("error", "true")
285
+ span.set_attribute("error.message", str(e))
286
+ span.set_attribute("error.type", type(e).__name__)
287
+
288
+ # Re-raise to propagate failure
289
+ raise
176
290
 
177
291
  async def parallel(self, *tasks: Awaitable[T]) -> List[T]:
178
292
  """
@@ -191,6 +305,7 @@ class WorkflowContext(Context):
191
305
  )
192
306
  """
193
307
  import asyncio
308
+
194
309
  return list(await asyncio.gather(*tasks))
195
310
 
196
311
  async def gather(self, **tasks: Awaitable[T]) -> Dict[str, T]:
@@ -210,15 +325,14 @@ class WorkflowContext(Context):
210
325
  )
211
326
  """
212
327
  import asyncio
328
+
213
329
  keys = list(tasks.keys())
214
330
  values = list(tasks.values())
215
331
  results = await asyncio.gather(*values)
216
332
  return dict(zip(keys, results))
217
333
 
218
334
  async def step(
219
- self,
220
- name: str,
221
- func_or_awaitable: Union[Callable[[], Awaitable[T]], Awaitable[T]]
335
+ self, name: str, func_or_awaitable: Union[Callable[[], Awaitable[T]], Awaitable[T]]
222
336
  ) -> T:
223
337
  """
224
338
  Checkpoint expensive operations for durability.
@@ -255,10 +369,7 @@ class WorkflowContext(Context):
255
369
  return result
256
370
 
257
371
  async def wait_for_user(
258
- self,
259
- question: str,
260
- input_type: str = "text",
261
- options: Optional[List[Dict]] = None
372
+ self, question: str, input_type: str = "text", options: Optional[List[Dict]] = None
262
373
  ) -> str:
263
374
  """
264
375
  Pause workflow execution and wait for user input.
@@ -322,7 +433,7 @@ class WorkflowContext(Context):
322
433
  # No response yet - pause execution
323
434
  # Collect current workflow state for checkpoint
324
435
  checkpoint_state = {}
325
- if hasattr(self._workflow_entity, '_state') and self._workflow_entity._state is not None:
436
+ if hasattr(self._workflow_entity, "_state") and self._workflow_entity._state is not None:
326
437
  checkpoint_state = self._workflow_entity._state.get_state_snapshot()
327
438
 
328
439
  self._logger.info(f"⏸️ Pausing workflow for user input: {question}")
@@ -331,7 +442,7 @@ class WorkflowContext(Context):
331
442
  question=question,
332
443
  input_type=input_type,
333
444
  options=options,
334
- checkpoint_state=checkpoint_state
445
+ checkpoint_state=checkpoint_state,
335
446
  )
336
447
 
337
448
 
@@ -339,6 +450,7 @@ class WorkflowContext(Context):
339
450
  # WorkflowEntity: Entity specialized for workflow execution state
340
451
  # ============================================================================
341
452
 
453
+
342
454
  class WorkflowEntity(Entity):
343
455
  """
344
456
  Entity specialized for workflow execution state.
@@ -347,20 +459,50 @@ class WorkflowEntity(Entity):
347
459
  - Step tracking for replay and crash recovery
348
460
  - State change tracking for debugging and audit (AI workflows)
349
461
  - Completed step cache for efficient replay
462
+ - Automatic state persistence after workflow execution
350
463
 
351
- Workflows are temporary entities - they exist for the duration of
352
- execution and their state is used for coordination between steps.
464
+ Workflow state is persisted to the database after successful execution,
465
+ enabling crash recovery, replay, and cross-invocation state management.
466
+ The workflow decorator automatically calls _persist_state() to ensure
467
+ durability.
353
468
  """
354
469
 
355
- def __init__(self, run_id: str):
470
+ def __init__(
471
+ self,
472
+ run_id: str,
473
+ session_id: Optional[str] = None,
474
+ user_id: Optional[str] = None,
475
+ ):
356
476
  """
357
- Initialize workflow entity.
477
+ Initialize workflow entity with memory scope.
358
478
 
359
479
  Args:
360
480
  run_id: Unique workflow run identifier
481
+ session_id: Session identifier for multi-turn conversations (optional)
482
+ user_id: User identifier for user-scoped memory (optional)
483
+
484
+ Memory Scope Priority:
485
+ - user_id present → key: user:{user_id}
486
+ - session_id present (and != run_id) → key: session:{session_id}
487
+ - else → key: run:{run_id}
361
488
  """
362
- # Initialize as entity with workflow key pattern
363
- super().__init__(key=f"workflow:{run_id}")
489
+ # Determine entity key based on memory scope priority
490
+ if user_id:
491
+ entity_key = f"user:{user_id}"
492
+ memory_scope = "user"
493
+ elif session_id and session_id != run_id:
494
+ entity_key = f"session:{session_id}"
495
+ memory_scope = "session"
496
+ else:
497
+ entity_key = f"run:{run_id}"
498
+ memory_scope = "run"
499
+
500
+ # Initialize as entity with scoped key pattern
501
+ super().__init__(key=entity_key)
502
+
503
+ # Store run_id separately for tracking (even if key is session/user scoped)
504
+ self._run_id = run_id
505
+ self._memory_scope = memory_scope
364
506
 
365
507
  # Step tracking for replay and recovery
366
508
  self._step_events: list[Dict[str, Any]] = []
@@ -369,19 +511,15 @@ class WorkflowEntity(Entity):
369
511
  # State change tracking for debugging/audit (AI workflows)
370
512
  self._state_changes: list[Dict[str, Any]] = []
371
513
 
372
- logger.debug(f"Created WorkflowEntity: {run_id}")
514
+ logger.debug(f"Created WorkflowEntity: run={run_id}, scope={memory_scope}, key={entity_key}")
373
515
 
374
516
  @property
375
517
  def run_id(self) -> str:
376
- """Extract run_id from workflow key."""
377
- return self._key.split(":", 1)[1]
518
+ """Get run_id for this workflow execution."""
519
+ return self._run_id
378
520
 
379
521
  def record_step_completion(
380
- self,
381
- step_name: str,
382
- handler_name: str,
383
- input_data: Any,
384
- result: Any
522
+ self, step_name: str, handler_name: str, input_data: Any, result: Any
385
523
  ) -> None:
386
524
  """
387
525
  Record completed step for replay and recovery.
@@ -392,12 +530,14 @@ class WorkflowEntity(Entity):
392
530
  input_data: Input data passed to function
393
531
  result: Function result
394
532
  """
395
- self._step_events.append({
396
- "step_name": step_name,
397
- "handler_name": handler_name,
398
- "input": input_data,
399
- "result": result
400
- })
533
+ self._step_events.append(
534
+ {
535
+ "step_name": step_name,
536
+ "handler_name": handler_name,
537
+ "input": input_data,
538
+ "result": result,
539
+ }
540
+ )
401
541
  self._completed_steps[step_name] = result
402
542
  logger.debug(f"Recorded step completion: {step_name}")
403
543
 
@@ -437,6 +577,109 @@ class WorkflowEntity(Entity):
437
577
  self._completed_steps[response_key] = response
438
578
  logger.info(f"Injected user response for {self.run_id}: {response}")
439
579
 
580
+ def get_agent_data(self, agent_name: str) -> Dict[str, Any]:
581
+ """
582
+ Get agent conversation data from workflow state.
583
+
584
+ Args:
585
+ agent_name: Name of the agent
586
+
587
+ Returns:
588
+ Dictionary containing agent conversation data (messages, metadata)
589
+ or empty dict if agent has no data yet
590
+
591
+ Example:
592
+ ```python
593
+ agent_data = workflow_entity.get_agent_data("ResearchAgent")
594
+ messages = agent_data.get("messages", [])
595
+ ```
596
+ """
597
+ return self.state.get(f"agent.{agent_name}", {})
598
+
599
+ def get_agent_messages(self, agent_name: str) -> list[Dict[str, Any]]:
600
+ """
601
+ Get agent messages from workflow state.
602
+
603
+ Args:
604
+ agent_name: Name of the agent
605
+
606
+ Returns:
607
+ List of message dictionaries
608
+
609
+ Example:
610
+ ```python
611
+ messages = workflow_entity.get_agent_messages("ResearchAgent")
612
+ for msg in messages:
613
+ print(f"{msg['role']}: {msg['content']}")
614
+ ```
615
+ """
616
+ agent_data = self.get_agent_data(agent_name)
617
+ return agent_data.get("messages", [])
618
+
619
+ def list_agents(self) -> list[str]:
620
+ """
621
+ List all agents with data in this workflow.
622
+
623
+ Returns:
624
+ List of agent names that have stored conversation data
625
+
626
+ Example:
627
+ ```python
628
+ agents = workflow_entity.list_agents()
629
+ # ['ResearchAgent', 'AnalysisAgent', 'SynthesisAgent']
630
+ ```
631
+ """
632
+ agents = []
633
+ for key in self.state._state.keys():
634
+ if key.startswith("agent."):
635
+ agents.append(key.replace("agent.", "", 1))
636
+ return agents
637
+
638
+ async def _persist_state(self) -> None:
639
+ """
640
+ Internal method to persist workflow state to entity storage.
641
+
642
+ This is prefixed with _ so it won't be wrapped by the entity method wrapper.
643
+ Called after workflow execution completes to ensure state is durable.
644
+ """
645
+ logger.info(f"🔍 DEBUG: _persist_state() CALLED for workflow {self.run_id}")
646
+
647
+ try:
648
+ from .entity import _get_state_adapter
649
+
650
+ logger.info(f"🔍 DEBUG: Getting state adapter...")
651
+ # Get the state adapter (must be in Worker context)
652
+ adapter = _get_state_adapter()
653
+ logger.info(f"🔍 DEBUG: Got state adapter: {type(adapter).__name__}")
654
+
655
+ logger.info(f"🔍 DEBUG: Getting state snapshot...")
656
+ # Get current state snapshot
657
+ state_dict = self.state.get_state_snapshot()
658
+ logger.info(f"🔍 DEBUG: State snapshot has {len(state_dict)} keys: {list(state_dict.keys())}")
659
+
660
+ logger.info(f"🔍 DEBUG: Loading current version for optimistic locking...")
661
+ # Load current version (for optimistic locking)
662
+ _, current_version = await adapter.load_with_version(self._entity_type, self._key)
663
+ logger.info(f"🔍 DEBUG: Current version: {current_version}")
664
+
665
+ logger.info(f"🔍 DEBUG: Saving state to database...")
666
+ # Save state with version check
667
+ new_version = await adapter.save_state(
668
+ self._entity_type, self._key, state_dict, current_version
669
+ )
670
+
671
+ logger.info(
672
+ f"✅ SUCCESS: Persisted WorkflowEntity state for {self.run_id} "
673
+ f"(version {current_version} -> {new_version}, {len(state_dict)} keys)"
674
+ )
675
+ except Exception as e:
676
+ logger.error(
677
+ f"❌ ERROR: Failed to persist workflow state for {self.run_id}: {e}",
678
+ exc_info=True
679
+ )
680
+ # Re-raise to let caller handle
681
+ raise
682
+
440
683
  @property
441
684
  def state(self) -> "WorkflowState":
442
685
  """
@@ -471,42 +714,62 @@ class WorkflowState(EntityState):
471
714
  """
472
715
  super().__init__(state_dict)
473
716
  self._workflow_entity = workflow_entity
717
+ self._checkpoint_callback: Optional[Callable[[str, dict], None]] = None
718
+
719
+ def _set_checkpoint_callback(self, callback: Callable[[str, dict], None]) -> None:
720
+ """
721
+ Set the checkpoint callback for real-time state change streaming.
722
+
723
+ Args:
724
+ callback: Function to call when state changes
725
+ """
726
+ self._checkpoint_callback = callback
474
727
 
475
728
  def set(self, key: str, value: Any) -> None:
476
729
  """Set value and track change."""
477
730
  super().set(key, value)
478
731
  # Track change for debugging/audit
479
732
  import time
480
- self._workflow_entity._state_changes.append({
481
- "key": key,
482
- "value": value,
483
- "timestamp": time.time(),
484
- "deleted": False
485
- })
733
+
734
+ change_record = {"key": key, "value": value, "timestamp": time.time(), "deleted": False}
735
+ self._workflow_entity._state_changes.append(change_record)
736
+
737
+ # Emit checkpoint for real-time state streaming
738
+ if self._checkpoint_callback:
739
+ self._checkpoint_callback(
740
+ "workflow.state.changed", {"key": key, "value": value, "operation": "set"}
741
+ )
486
742
 
487
743
  def delete(self, key: str) -> None:
488
744
  """Delete key and track change."""
489
745
  super().delete(key)
490
746
  # Track deletion
491
747
  import time
492
- self._workflow_entity._state_changes.append({
493
- "key": key,
494
- "value": None,
495
- "timestamp": time.time(),
496
- "deleted": True
497
- })
748
+
749
+ change_record = {"key": key, "value": None, "timestamp": time.time(), "deleted": True}
750
+ self._workflow_entity._state_changes.append(change_record)
751
+
752
+ # Emit checkpoint for real-time state streaming
753
+ if self._checkpoint_callback:
754
+ self._checkpoint_callback("workflow.state.changed", {"key": key, "operation": "delete"})
498
755
 
499
756
  def clear(self) -> None:
500
757
  """Clear all state and track change."""
501
758
  super().clear()
502
759
  # Track clear operation
503
760
  import time
504
- self._workflow_entity._state_changes.append({
761
+
762
+ change_record = {
505
763
  "key": "__clear__",
506
764
  "value": None,
507
765
  "timestamp": time.time(),
508
- "deleted": True
509
- })
766
+ "deleted": True,
767
+ }
768
+ self._workflow_entity._state_changes.append(change_record)
769
+
770
+ # Emit checkpoint for real-time state streaming
771
+ if self._checkpoint_callback:
772
+ self._checkpoint_callback("workflow.state.changed", {"operation": "clear"})
510
773
 
511
774
  def has_changes(self) -> bool:
512
775
  """Check if any state changes have been tracked."""
@@ -683,11 +946,45 @@ def workflow(
683
946
  run_id=run_id,
684
947
  )
685
948
 
686
- # Execute workflow
687
- return await handler_func(ctx, *args, **kwargs)
949
+ # Set context in task-local storage for automatic propagation
950
+ token = set_current_context(ctx)
951
+ try:
952
+ # Execute workflow
953
+ result = await handler_func(ctx, *args, **kwargs)
954
+
955
+ # Persist workflow state after successful execution
956
+ try:
957
+ await workflow_entity._persist_state()
958
+ except Exception as e:
959
+ logger.error(f"Failed to persist workflow state (non-fatal): {e}", exc_info=True)
960
+ # Don't fail the workflow - persistence failure shouldn't break execution
961
+
962
+ return result
963
+ finally:
964
+ # Always reset context to prevent leakage
965
+ from .context import _current_context
966
+
967
+ _current_context.reset(token)
688
968
  else:
689
- # WorkflowContext provided - use it
690
- return await handler_func(*args, **kwargs)
969
+ # WorkflowContext provided - use it and set in contextvar
970
+ ctx = args[0]
971
+ token = set_current_context(ctx)
972
+ try:
973
+ result = await handler_func(*args, **kwargs)
974
+
975
+ # Persist workflow state after successful execution
976
+ try:
977
+ await ctx._workflow_entity._persist_state()
978
+ except Exception as e:
979
+ logger.error(f"Failed to persist workflow state (non-fatal): {e}", exc_info=True)
980
+ # Don't fail the workflow - persistence failure shouldn't break execution
981
+
982
+ return result
983
+ finally:
984
+ # Always reset context to prevent leakage
985
+ from .context import _current_context
986
+
987
+ _current_context.reset(token)
691
988
 
692
989
  # Store config on wrapper for introspection
693
990
  wrapper._agnt5_config = config # type: ignore
@@ -698,5 +995,3 @@ def workflow(
698
995
  return decorator
699
996
  else:
700
997
  return decorator(_func)
701
-
702
-
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agnt5
3
- Version: 0.2.8a7
3
+ Version: 0.2.8a9
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Programming Language :: Python :: 3
@@ -0,0 +1,22 @@
1
+ agnt5-0.2.8a9.dist-info/METADATA,sha256=fCSo0PhnFkA9J1zcqNdmLiMYfTPHpYVVtwQLRGdhDDw,996
2
+ agnt5-0.2.8a9.dist-info/WHEEL,sha256=-lwEpi49KOTCcgx48T3fLSP8Dxynwa-iRMZNo-JZaqc,103
3
+ agnt5/__init__.py,sha256=liMb9egh56qvgY4Xvs9s7grOzF3lXSE8-nIksJLNAy4,2195
4
+ agnt5/_compat.py,sha256=BGuy3v5VDOHVa5f3Z-C22iMN19lAt0mPmXwF3qSSWxI,369
5
+ agnt5/_core.abi3.so,sha256=jvQNwEx-YaPcHYz6pmn8ETlN9n0ZCvLqq2MSYjPe-JQ,12645584
6
+ agnt5/_retry_utils.py,sha256=loHsWY5BR4wZy57IzcDEjQAy88DHVwVIr25Cn1d9GPA,5801
7
+ agnt5/_schema_utils.py,sha256=MR67RW757T4Oq2Jqf4kB61H_b51zwaf3CLWELnkngRo,9572
8
+ agnt5/_telemetry.py,sha256=Oafa1uC4IXIf12mhj5kBLwmVtVcLQweay5Nrr7-xRDg,6781
9
+ agnt5/agent.py,sha256=b8xOxmtBxdzbK-2G1oWiRZRtetjDaAc3CWCx_zW0ep4,70600
10
+ agnt5/client.py,sha256=aHQvxlH4_CP93YkS9lSKUrIwPS1maCiuYNwhuFqRElE,23928
11
+ agnt5/context.py,sha256=qbJUtZE7LpU1VJx2I2DJO6OCZHoYWkEJBGIfycBawfU,5695
12
+ agnt5/entity.py,sha256=AlHmSHVxQD5EYBvkmERKUkwv0ERrKaT8rvRK611hv_I,28941
13
+ agnt5/exceptions.py,sha256=Nqg7CeKRu-oIj34A-2Y-QZ4YQ_TLPuin-yg8Mw7-76o,3277
14
+ agnt5/function.py,sha256=vPNKhFCgFz4mIPVJqt0WvIWJoDgC7jXE3miEHT4LVKI,11417
15
+ agnt5/lm.py,sha256=UKAnBTTYvJEkjqu23GEtdZ4RZiKNjDgGX5YiaOVfbq4,27348
16
+ agnt5/tool.py,sha256=EG9ZvFbRKnJvSVcgVxtOliDMmPmUeCsdm4hiLewuvr8,21629
17
+ agnt5/tracing.py,sha256=Mh2-OfnQM61lM_P8gxJstafdsUA8Gxoo1lP-Joxhub8,5980
18
+ agnt5/types.py,sha256=Zb71ZMwvrt1p4SH18cAKunp2y5tao_W5_jGYaPDejQo,2840
19
+ agnt5/version.py,sha256=rOq1mObLihnnKgKqBrwZA0zwOPudEKVFcW1a48ynkqc,573
20
+ agnt5/worker.py,sha256=Qd71KveMyFIFVI8tpF2_8CKPXG7WwJSYNnKeNPUkn94,70555
21
+ agnt5/workflow.py,sha256=SKsdP8fhljJq-zdwS0Q63Fmo2TEEFZduYsssXugVgiU,35632
22
+ agnt5-0.2.8a9.dist-info/RECORD,,