agnt5 0.2.2__cp39-abi3-macosx_11_0_arm64.whl → 0.2.4__cp39-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/worker.py CHANGED
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import contextvars
6
7
  import logging
7
8
  from typing import Any, Dict, Optional
8
9
 
@@ -12,6 +13,12 @@ from ._telemetry import setup_module_logger
12
13
 
13
14
  logger = setup_module_logger(__name__)
14
15
 
16
+ # Context variable to store trace metadata for propagation to LM calls
17
+ # This allows Rust LM layer to access traceparent without explicit parameter passing
18
+ _trace_metadata: contextvars.ContextVar[Dict[str, str]] = contextvars.ContextVar(
19
+ '_trace_metadata', default={}
20
+ )
21
+
15
22
 
16
23
  class Worker:
17
24
  """AGNT5 Worker for registering and running functions/workflows with the coordinator.
@@ -88,6 +95,10 @@ class Worker:
88
95
  # Create Rust worker instance
89
96
  self._rust_worker = self._PyWorker(self._rust_config)
90
97
 
98
+ # Create worker-scoped entity state manager
99
+ from .entity import EntityStateManager
100
+ self._entity_state_manager = EntityStateManager()
101
+
91
102
  logger.info(
92
103
  f"Worker initialized: {service_name} v{service_version} (runtime: {runtime})"
93
104
  )
@@ -191,7 +202,7 @@ class Worker:
191
202
 
192
203
  # Build metadata dict with methods list and schemas
193
204
  metadata_dict = {
194
- "methods": json.dumps(list(entity_type._methods.keys())),
205
+ "methods": json.dumps(list(entity_type._method_schemas.keys())),
195
206
  "method_schemas": json.dumps(method_schemas)
196
207
  }
197
208
 
@@ -205,7 +216,7 @@ class Worker:
205
216
  definition=None,
206
217
  )
207
218
  components.append(component_info)
208
- logger.debug(f"Discovered entity: {name} with methods: {list(entity_type._methods.keys())}")
219
+ logger.debug(f"Discovered entity: {name} with methods: {list(entity_type._method_schemas.keys())}")
209
220
 
210
221
  # Discover agents
211
222
  for name, agent in AgentRegistry.all().items():
@@ -236,68 +247,6 @@ class Worker:
236
247
  components.append(component_info)
237
248
  logger.debug(f"Discovered agent: {name}")
238
249
 
239
- # Discover tools
240
- for name, tool in ToolRegistry.all().items():
241
- # Serialize schemas to JSON strings
242
- input_schema_str = None
243
- if hasattr(tool, 'input_schema') and tool.input_schema:
244
- input_schema_str = json.dumps(tool.input_schema)
245
-
246
- output_schema_str = None
247
- if hasattr(tool, 'output_schema') and tool.output_schema:
248
- output_schema_str = json.dumps(tool.output_schema)
249
-
250
- component_info = self._PyComponentInfo(
251
- name=name,
252
- component_type="tool",
253
- metadata={},
254
- config={},
255
- input_schema=input_schema_str,
256
- output_schema=output_schema_str,
257
- definition=None,
258
- )
259
- components.append(component_info)
260
- logger.debug(f"Discovered tool: {name}")
261
-
262
- # Discover entities
263
- for name, entity_type in EntityRegistry.all().items():
264
- # Build metadata dict with methods list as JSON string
265
- metadata_dict = {
266
- "methods": json.dumps(list(entity_type._methods.keys()))
267
- }
268
-
269
- component_info = self._PyComponentInfo(
270
- name=name,
271
- component_type="entity",
272
- metadata=metadata_dict,
273
- config={},
274
- input_schema=None,
275
- output_schema=None,
276
- definition=None,
277
- )
278
- components.append(component_info)
279
- logger.debug(f"Discovered entity: {name} with methods: {list(entity_type._methods.keys())}")
280
-
281
- # Discover agents
282
- for name, agent in AgentRegistry.all().items():
283
- # Build metadata dict with agent info
284
- metadata_dict = {
285
- "model": agent.model_name,
286
- "tools": json.dumps(list(agent.tools.keys()) if hasattr(agent, 'tools') else [])
287
- }
288
-
289
- component_info = self._PyComponentInfo(
290
- name=name,
291
- component_type="agent",
292
- metadata=metadata_dict,
293
- config={},
294
- input_schema=None,
295
- output_schema=None,
296
- definition=None,
297
- )
298
- components.append(component_info)
299
- logger.debug(f"Discovered agent: {name}")
300
-
301
250
  logger.info(f"Discovered {len(components)} components")
302
251
  return components
303
252
 
@@ -305,67 +254,66 @@ class Worker:
305
254
  """Create the message handler that will be called by Rust worker."""
306
255
 
307
256
  def handle_message(request):
308
- """Handle incoming execution requests."""
309
- try:
310
- # Extract request details
311
- component_name = request.component_name
312
- component_type = request.component_type
313
- input_data = request.input_data
314
-
315
- logger.debug(
316
- f"Handling {component_type} request: {component_name}, input size: {len(input_data)} bytes"
317
- )
257
+ """Handle incoming execution requests - returns coroutine for Rust to await."""
258
+ # Extract request details
259
+ component_name = request.component_name
260
+ component_type = request.component_type
261
+ input_data = request.input_data
262
+
263
+ logger.debug(
264
+ f"Handling {component_type} request: {component_name}, input size: {len(input_data)} bytes"
265
+ )
318
266
 
319
- # Import all registries
320
- from .tool import ToolRegistry
321
- from .entity import EntityRegistry
322
- from .agent import AgentRegistry
323
-
324
- # Route based on component type
325
- if component_type == "tool":
326
- tool = ToolRegistry.get(component_name)
327
- if tool:
328
- logger.debug(f"Found tool: {component_name}")
329
- result = asyncio.run(self._execute_tool(tool, input_data, request))
330
- return result
331
-
332
- elif component_type == "entity":
333
- entity_type = EntityRegistry.get(component_name)
334
- if entity_type:
335
- logger.debug(f"Found entity: {component_name}")
336
- result = asyncio.run(self._execute_entity(entity_type, input_data, request))
337
- return result
338
-
339
- elif component_type == "agent":
340
- agent = AgentRegistry.get(component_name)
341
- if agent:
342
- logger.debug(f"Found agent: {component_name}")
343
- result = asyncio.run(self._execute_agent(agent, input_data, request))
344
- return result
345
-
346
- elif component_type == "workflow":
347
- workflow_config = WorkflowRegistry.get(component_name)
348
- if workflow_config:
349
- logger.debug(f"Found workflow: {component_name}")
350
- result = asyncio.run(self._execute_workflow(workflow_config, input_data, request))
351
- return result
352
-
353
- elif component_type == "function":
354
- function_config = FunctionRegistry.get(component_name)
355
- if function_config:
356
- logger.info(f"🔥 WORKER: Received request for function: {component_name}")
357
- result = asyncio.run(self._execute_function(function_config, input_data, request))
358
- return result
359
-
360
- # Not found
361
- error_msg = f"Component '{component_name}' of type '{component_type}' not found"
362
- logger.error(error_msg)
267
+ # Import all registries
268
+ from .tool import ToolRegistry
269
+ from .entity import EntityRegistry
270
+ from .agent import AgentRegistry
271
+
272
+ # Route based on component type and return coroutines
273
+ if component_type == "tool":
274
+ tool = ToolRegistry.get(component_name)
275
+ if tool:
276
+ logger.debug(f"Found tool: {component_name}")
277
+ # Return coroutine, don't await it
278
+ return self._execute_tool(tool, input_data, request)
279
+
280
+ elif component_type == "entity":
281
+ entity_type = EntityRegistry.get(component_name)
282
+ if entity_type:
283
+ logger.debug(f"Found entity: {component_name}")
284
+ # Return coroutine, don't await it
285
+ return self._execute_entity(entity_type, input_data, request)
286
+
287
+ elif component_type == "agent":
288
+ agent = AgentRegistry.get(component_name)
289
+ if agent:
290
+ logger.debug(f"Found agent: {component_name}")
291
+ # Return coroutine, don't await it
292
+ return self._execute_agent(agent, input_data, request)
293
+
294
+ elif component_type == "workflow":
295
+ workflow_config = WorkflowRegistry.get(component_name)
296
+ if workflow_config:
297
+ logger.debug(f"Found workflow: {component_name}")
298
+ # Return coroutine, don't await it
299
+ return self._execute_workflow(workflow_config, input_data, request)
300
+
301
+ elif component_type == "function":
302
+ function_config = FunctionRegistry.get(component_name)
303
+ if function_config:
304
+ logger.info(f"🔥 WORKER: Received request for function: {component_name}")
305
+ # Return coroutine, don't await it
306
+ return self._execute_function(function_config, input_data, request)
307
+
308
+ # Not found - need to return an async error response
309
+ error_msg = f"Component '{component_name}' of type '{component_type}' not found"
310
+ logger.error(error_msg)
311
+
312
+ # Create async wrapper for error response
313
+ async def error_response():
363
314
  return self._create_error_response(request, error_msg)
364
315
 
365
- except Exception as e:
366
- error_msg = f"Handler error: {e}"
367
- logger.error(error_msg, exc_info=True)
368
- return self._create_error_response(request, error_msg)
316
+ return error_response()
369
317
 
370
318
  return handle_message
371
319
 
@@ -382,20 +330,45 @@ class Worker:
382
330
  # Parse input data
383
331
  input_dict = json.loads(input_data.decode("utf-8")) if input_data else {}
384
332
 
385
- # Create context
333
+ # Store trace metadata in contextvar for LM calls to access
334
+ # The Rust worker injects traceparent into request.metadata for trace propagation
335
+ if hasattr(request, 'metadata') and request.metadata:
336
+ _trace_metadata.set(dict(request.metadata))
337
+ logger.debug(f"Trace metadata stored: traceparent={request.metadata.get('traceparent', 'N/A')}")
338
+
339
+ # Create context with runtime_context for trace correlation
386
340
  ctx = Context(
387
341
  run_id=f"{self.service_name}:{config.name}",
388
- component_type="function",
342
+ runtime_context=request.runtime_context,
389
343
  )
390
344
 
391
- # Execute function
392
- if input_dict:
393
- result = config.handler(ctx, **input_dict)
394
- else:
395
- result = config.handler(ctx)
396
-
397
- # Debug: Log what type result is
398
- logger.info(f"🔥 WORKER: Function result type: {type(result).__name__}, isasyncgen: {inspect.isasyncgen(result)}, iscoroutine: {inspect.iscoroutine(result)}")
345
+ # Create span for function execution with trace linking
346
+ from ._core import create_span, flush_telemetry_py
347
+
348
+ with create_span(
349
+ config.name,
350
+ "function",
351
+ request.runtime_context,
352
+ {
353
+ "function.name": config.name,
354
+ "service.name": self.service_name,
355
+ },
356
+ ) as span:
357
+ # Execute function
358
+ if input_dict:
359
+ result = config.handler(ctx, **input_dict)
360
+ else:
361
+ result = config.handler(ctx)
362
+
363
+ # Debug: Log what type result is
364
+ logger.info(f"🔥 WORKER: Function result type: {type(result).__name__}, isasyncgen: {inspect.isasyncgen(result)}, iscoroutine: {inspect.iscoroutine(result)}")
365
+
366
+ # Flush telemetry after span ends to ensure it's exported
367
+ try:
368
+ flush_telemetry_py()
369
+ logger.debug("Telemetry flushed after function execution")
370
+ except Exception as e:
371
+ logger.warning(f"Failed to flush telemetry: {e}")
399
372
 
400
373
  # Check if result is an async generator (streaming function)
401
374
  if inspect.isasyncgen(result):
@@ -473,16 +446,17 @@ class Worker:
473
446
  )
474
447
 
475
448
  async def _execute_workflow(self, config, input_data: bytes, request):
476
- """Execute a workflow handler with replay support (Phase 6B)."""
449
+ """Execute a workflow handler with automatic replay support."""
477
450
  import json
478
- from .context import Context
451
+ from .workflow import WorkflowEntity, WorkflowContext
452
+ from .entity import _get_state_manager
479
453
  from ._core import PyExecuteComponentResponse
480
454
 
481
455
  try:
482
456
  # Parse input data
483
457
  input_dict = json.loads(input_data.decode("utf-8")) if input_data else {}
484
458
 
485
- # Phase 6B: Parse replay data from request metadata
459
+ # Parse replay data from request metadata for crash recovery
486
460
  completed_steps = {}
487
461
  initial_state = {}
488
462
 
@@ -507,38 +481,74 @@ class Worker:
507
481
  except json.JSONDecodeError:
508
482
  logger.warning("Failed to parse workflow_state from metadata")
509
483
 
510
- # Create context with replay data
511
- ctx = Context(
484
+ # Create WorkflowEntity for state management
485
+ workflow_entity = WorkflowEntity(run_id=f"{self.service_name}:{config.name}")
486
+
487
+ # Load replay data into entity if provided
488
+ if completed_steps:
489
+ workflow_entity._completed_steps = completed_steps
490
+ logger.debug(f"Loaded {len(completed_steps)} completed steps into workflow entity")
491
+
492
+ if initial_state:
493
+ # Load initial state into entity's state manager
494
+ state_manager = _get_state_manager()
495
+ state_manager._states[workflow_entity._state_key] = initial_state
496
+ logger.debug(f"Loaded initial state with {len(initial_state)} keys into workflow entity")
497
+
498
+ # Create WorkflowContext with entity and runtime_context for trace correlation
499
+ ctx = WorkflowContext(
500
+ workflow_entity=workflow_entity,
512
501
  run_id=f"{self.service_name}:{config.name}",
513
- component_type="workflow",
514
- completed_steps=completed_steps if completed_steps else None,
515
- initial_state=initial_state if initial_state else None,
502
+ runtime_context=request.runtime_context,
516
503
  )
517
504
 
518
- # Execute workflow
519
- if input_dict:
520
- result = await config.handler(ctx, **input_dict)
521
- else:
522
- result = await config.handler(ctx)
505
+ # Create span for workflow execution with trace linking
506
+ from ._core import create_span, flush_telemetry_py
507
+
508
+ with create_span(
509
+ config.name,
510
+ "workflow",
511
+ request.runtime_context,
512
+ {
513
+ "workflow.name": config.name,
514
+ "service.name": self.service_name,
515
+ },
516
+ ) as span:
517
+ # Execute workflow
518
+ if input_dict:
519
+ result = await config.handler(ctx, **input_dict)
520
+ else:
521
+ result = await config.handler(ctx)
522
+
523
+ # Flush telemetry after span ends to ensure it's exported
524
+ try:
525
+ flush_telemetry_py()
526
+ logger.debug("Telemetry flushed after workflow execution")
527
+ except Exception as e:
528
+ logger.warning(f"Failed to flush telemetry: {e}")
523
529
 
524
530
  # Serialize result
525
531
  output_data = json.dumps(result).encode("utf-8")
526
532
 
527
- # Phase 6: Collect workflow execution metadata (similar to entity pattern)
533
+ # Collect workflow execution metadata for durability
528
534
  metadata = {}
529
535
 
530
536
  # Add step events to metadata (for workflow durability)
531
- if ctx._step_events:
532
- metadata["step_events"] = json.dumps(ctx._step_events)
533
- logger.debug(f"Workflow has {len(ctx._step_events)} recorded steps")
537
+ # Access _step_events from the workflow entity, not the context
538
+ step_events = ctx._workflow_entity._step_events
539
+ if step_events:
540
+ metadata["step_events"] = json.dumps(step_events)
541
+ logger.debug(f"Workflow has {len(step_events)} recorded steps")
534
542
 
535
543
  # Add final state snapshot to metadata (if state was used)
536
- if hasattr(ctx, '_state_client') and ctx.state.has_changes():
537
- state_snapshot = ctx.state.get_state_snapshot()
538
- metadata["workflow_state"] = json.dumps(state_snapshot)
539
- logger.debug(f"Workflow state snapshot: {state_snapshot}")
544
+ # Check if _state was initialized without triggering property getter
545
+ if hasattr(ctx, '_workflow_entity') and ctx._workflow_entity._state is not None:
546
+ if ctx._workflow_entity._state.has_changes():
547
+ state_snapshot = ctx._workflow_entity._state.get_state_snapshot()
548
+ metadata["workflow_state"] = json.dumps(state_snapshot)
549
+ logger.debug(f"Workflow state snapshot: {state_snapshot}")
540
550
 
541
- logger.info(f"Workflow completed successfully with {len(ctx._step_events)} steps")
551
+ logger.info(f"Workflow completed successfully with {len(step_events)} steps")
542
552
 
543
553
  return PyExecuteComponentResponse(
544
554
  invocation_id=request.invocation_id,
@@ -578,10 +588,10 @@ class Worker:
578
588
  # Parse input data
579
589
  input_dict = json.loads(input_data.decode("utf-8")) if input_data else {}
580
590
 
581
- # Create context
591
+ # Create context with runtime_context for trace correlation
582
592
  ctx = Context(
583
593
  run_id=f"{self.service_name}:{tool.name}",
584
- component_type="tool",
594
+ runtime_context=request.runtime_context,
585
595
  )
586
596
 
587
597
  # Execute tool
@@ -622,9 +632,12 @@ class Worker:
622
632
  """Execute an entity method."""
623
633
  import json
624
634
  from .context import Context
625
- from .entity import EntityType, DurableEntity, _entity_states
635
+ from .entity import EntityType, Entity, _entity_state_manager_ctx
626
636
  from ._core import PyExecuteComponentResponse
627
637
 
638
+ # Set entity state manager in context for Entity instances to access
639
+ _entity_state_manager_ctx.set(self._entity_state_manager)
640
+
628
641
  try:
629
642
  # Parse input data
630
643
  input_dict = json.loads(input_data.decode("utf-8")) if input_data else {}
@@ -638,13 +651,26 @@ class Worker:
638
651
  if not method_name:
639
652
  raise ValueError("Entity invocation requires 'method' parameter")
640
653
 
641
- # Check if this is a class-based entity (DurableEntity subclass) or method-based (EntityType)
642
- if entity_type.entity_class is not None:
643
- # Class-based entity: use the stored class reference
644
- entity_instance = entity_type.entity_class(key=entity_key)
645
- else:
646
- # Method-based entity: create EntityInstance
647
- entity_instance = entity_type(entity_key)
654
+ # Load state from platform if provided in request metadata
655
+ state_key = (entity_type.name, entity_key)
656
+ if hasattr(request, 'metadata') and request.metadata:
657
+ if "entity_state" in request.metadata:
658
+ platform_state_json = request.metadata["entity_state"]
659
+ platform_version = int(request.metadata.get("state_version", "0"))
660
+
661
+ # Load platform state into state manager
662
+ self._entity_state_manager.load_state_from_platform(
663
+ state_key,
664
+ platform_state_json,
665
+ platform_version
666
+ )
667
+ logger.info(
668
+ f"Loaded entity state from platform: {entity_type.name}/{entity_key} "
669
+ f"(version {platform_version})"
670
+ )
671
+
672
+ # Create entity instance using the stored class reference
673
+ entity_instance = entity_type.entity_class(key=entity_key)
648
674
 
649
675
  # Get method
650
676
  if not hasattr(entity_instance, method_name):
@@ -658,26 +684,32 @@ class Worker:
658
684
  # Serialize result
659
685
  output_data = json.dumps(result).encode("utf-8")
660
686
 
661
- # Phase 5B: Capture entity state after execution for persistence
662
- state_key = (entity_type.name, entity_key)
687
+ # Capture entity state after execution with version tracking
688
+ state_dict, expected_version, new_version = \
689
+ self._entity_state_manager.get_state_for_persistence(state_key)
690
+
663
691
  metadata = {}
664
- if state_key in _entity_states:
665
- entity_state = _entity_states[state_key]
692
+ if state_dict:
666
693
  # Serialize state as JSON string for platform persistence
667
- state_json = json.dumps(entity_state)
694
+ state_json = json.dumps(state_dict)
668
695
  # Pass in metadata for Worker Coordinator to publish
669
696
  metadata = {
670
697
  "entity_state": state_json,
671
698
  "entity_type": entity_type.name,
672
699
  "entity_key": entity_key,
700
+ "expected_version": str(expected_version),
701
+ "new_version": str(new_version),
673
702
  }
674
- logger.debug(f"Entity state update: {entity_type.name}:{entity_key}, state: {state_json}")
703
+ logger.info(
704
+ f"Captured entity state: {entity_type.name}/{entity_key} "
705
+ f"(version {expected_version} → {new_version})"
706
+ )
675
707
 
676
708
  return PyExecuteComponentResponse(
677
709
  invocation_id=request.invocation_id,
678
710
  success=True,
679
711
  output_data=output_data,
680
- state_update=None, # TODO: Phase 6 - Use structured StateUpdate object
712
+ state_update=None, # TODO: Use structured StateUpdate object
681
713
  error_message=None,
682
714
  metadata=metadata, # Include state in metadata for Worker Coordinator
683
715
  is_chunk=False,
@@ -716,10 +748,10 @@ class Worker:
716
748
  if not user_message:
717
749
  raise ValueError("Agent invocation requires 'message' parameter")
718
750
 
719
- # Create context
751
+ # Create context with runtime_context for trace correlation
720
752
  ctx = Context(
721
753
  run_id=f"{self.service_name}:{agent.name}",
722
- component_type="agent",
754
+ runtime_context=request.runtime_context,
723
755
  )
724
756
 
725
757
  # Execute agent