agnt5 0.2.8a6__cp310-abi3-macosx_11_0_arm64.whl → 0.2.8a8__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/_core.abi3.so +0 -0
- agnt5/_telemetry.py +7 -2
- agnt5/agent.py +744 -171
- agnt5/client.py +18 -1
- agnt5/context.py +94 -0
- agnt5/exceptions.py +13 -0
- agnt5/function.py +18 -11
- agnt5/lm.py +124 -16
- agnt5/tool.py +110 -29
- agnt5/worker.py +421 -28
- agnt5/workflow.py +367 -72
- {agnt5-0.2.8a6.dist-info → agnt5-0.2.8a8.dist-info}/METADATA +1 -1
- agnt5-0.2.8a8.dist-info/RECORD +22 -0
- agnt5-0.2.8a6.dist-info/RECORD +0 -22
- {agnt5-0.2.8a6.dist-info → agnt5-0.2.8a8.dist-info}/WHEEL +0 -0
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
|
-
|
|
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,
|
|
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
|
-
#
|
|
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
|
-
#
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
352
|
-
|
|
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__(
|
|
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
|
-
#
|
|
363
|
-
|
|
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
|
-
"""
|
|
377
|
-
return self.
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
687
|
-
|
|
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
|
-
|
|
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
|
-
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
agnt5-0.2.8a8.dist-info/METADATA,sha256=VglBS6V3Yk7vYqxeKySrvVDmYH9kVfLlpEadlR_02OY,996
|
|
2
|
+
agnt5-0.2.8a8.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=03uz6uMOrguB0JG4le4aHrSjh6efMC7tdyY9NQR0oHE,12548240
|
|
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.8a8.dist-info/RECORD,,
|