chuk-tool-processor 0.18__tar.gz → 0.19__tar.gz

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 (127) hide show
  1. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/PKG-INFO +1 -1
  2. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/pyproject.toml +1 -1
  3. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/wrappers/__init__.py +7 -0
  4. chuk_tool_processor-0.19/src/chuk_tool_processor/execution/wrappers/observable.py +417 -0
  5. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/__init__.py +7 -0
  6. chuk_tool_processor-0.19/src/chuk_tool_processor/guards/contract_guard.py +250 -0
  7. chuk_tool_processor-0.19/src/chuk_tool_processor/models/__init__.py +94 -0
  8. chuk_tool_processor-0.19/src/chuk_tool_processor/models/execution_span.py +651 -0
  9. chuk_tool_processor-0.19/src/chuk_tool_processor/models/execution_trace.py +571 -0
  10. chuk_tool_processor-0.19/src/chuk_tool_processor/models/sandbox_policy.py +552 -0
  11. chuk_tool_processor-0.19/src/chuk_tool_processor/models/tool_contract.py +552 -0
  12. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/models/tool_spec.py +80 -1
  13. chuk_tool_processor-0.19/src/chuk_tool_processor/observability/__init__.py +68 -0
  14. chuk_tool_processor-0.19/src/chuk_tool_processor/observability/trace_sink.py +677 -0
  15. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor.egg-info/PKG-INFO +1 -1
  16. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor.egg-info/SOURCES.txt +7 -0
  17. chuk_tool_processor-0.18/src/chuk_tool_processor/models/__init__.py +0 -21
  18. chuk_tool_processor-0.18/src/chuk_tool_processor/observability/__init__.py +0 -30
  19. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/README.md +0 -0
  20. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/setup.cfg +0 -0
  21. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/__init__.py +0 -0
  22. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/config.py +0 -0
  23. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/core/__init__.py +0 -0
  24. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/core/context.py +0 -0
  25. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/core/exceptions.py +0 -0
  26. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/core/processor.py +0 -0
  27. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/discovery/__init__.py +0 -0
  28. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/discovery/dynamic_provider.py +0 -0
  29. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/discovery/search.py +0 -0
  30. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/discovery/searchable.py +0 -0
  31. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/discovery/synonyms.py +0 -0
  32. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/__init__.py +0 -0
  33. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/bulkhead.py +0 -0
  34. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/code_sandbox.py +0 -0
  35. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/strategies/__init__.py +0 -0
  36. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/strategies/inprocess_strategy.py +0 -0
  37. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/strategies/subprocess_strategy.py +0 -0
  38. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/tool_executor.py +0 -0
  39. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/wrappers/caching.py +0 -0
  40. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/wrappers/circuit_breaker.py +0 -0
  41. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/wrappers/factory.py +0 -0
  42. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/wrappers/rate_limiting.py +0 -0
  43. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/wrappers/redis_circuit_breaker.py +0 -0
  44. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/wrappers/redis_rate_limiting.py +0 -0
  45. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/wrappers/retry.py +0 -0
  46. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/assumption_trace.py +0 -0
  47. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/base.py +0 -0
  48. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/budget.py +0 -0
  49. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/chain.py +0 -0
  50. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/concurrency.py +0 -0
  51. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/models.py +0 -0
  52. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/network_policy.py +0 -0
  53. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/output_size.py +0 -0
  54. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/per_tool.py +0 -0
  55. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/plan_shape.py +0 -0
  56. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/precondition.py +0 -0
  57. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/provenance.py +0 -0
  58. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/retry_safety.py +0 -0
  59. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/runaway.py +0 -0
  60. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/saturation.py +0 -0
  61. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/schema_strictness.py +0 -0
  62. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/sensitive_data.py +0 -0
  63. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/side_effect.py +0 -0
  64. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/timeout_budget.py +0 -0
  65. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/unresolved.py +0 -0
  66. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/logging/__init__.py +0 -0
  67. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/logging/context.py +0 -0
  68. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/logging/formatter.py +0 -0
  69. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/logging/helpers.py +0 -0
  70. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/logging/metrics.py +0 -0
  71. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/__init__.py +0 -0
  72. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/mcp_tool.py +0 -0
  73. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/middleware.py +0 -0
  74. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/models.py +0 -0
  75. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/register_mcp_tools.py +0 -0
  76. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/setup_mcp_http_streamable.py +0 -0
  77. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/setup_mcp_sse.py +0 -0
  78. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/setup_mcp_stdio.py +0 -0
  79. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/stream_manager.py +0 -0
  80. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/transport/__init__.py +0 -0
  81. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/transport/base_transport.py +0 -0
  82. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/transport/http_streamable_transport.py +0 -0
  83. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/transport/models.py +0 -0
  84. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/transport/sse_transport.py +0 -0
  85. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/transport/stdio_transport.py +0 -0
  86. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/models/execution_strategy.py +0 -0
  87. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/models/return_order.py +0 -0
  88. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/models/streaming_tool.py +0 -0
  89. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/models/tool_call.py +0 -0
  90. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/models/tool_export_mixin.py +0 -0
  91. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/models/tool_result.py +0 -0
  92. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/models/validated_tool.py +0 -0
  93. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/observability/metrics.py +0 -0
  94. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/observability/setup.py +0 -0
  95. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/observability/tracing.py +0 -0
  96. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/plugins/__init__.py +0 -0
  97. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/plugins/discovery.py +0 -0
  98. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/plugins/parsers/__init__.py +0 -0
  99. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/plugins/parsers/base.py +0 -0
  100. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/plugins/parsers/function_call_tool.py +0 -0
  101. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/plugins/parsers/json_tool.py +0 -0
  102. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/plugins/parsers/openai_tool.py +0 -0
  103. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/plugins/parsers/xml_tool.py +0 -0
  104. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/py.typed +0 -0
  105. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/registry/__init__.py +0 -0
  106. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/registry/auto_register.py +0 -0
  107. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/registry/decorators.py +0 -0
  108. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/registry/interface.py +0 -0
  109. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/registry/metadata.py +0 -0
  110. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/registry/provider.py +0 -0
  111. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/registry/providers/__init__.py +0 -0
  112. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/registry/providers/memory.py +0 -0
  113. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/registry/providers/redis.py +0 -0
  114. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/registry/tool_export.py +0 -0
  115. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/scheduling/__init__.py +0 -0
  116. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/scheduling/greedy_dag.py +0 -0
  117. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/scheduling/policy.py +0 -0
  118. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/scheduling/types.py +0 -0
  119. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/utils/__init__.py +0 -0
  120. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/utils/fast_json.py +0 -0
  121. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/utils/validation.py +0 -0
  122. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor.egg-info/dependency_links.txt +0 -0
  123. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor.egg-info/requires.txt +0 -0
  124. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor.egg-info/top_level.txt +0 -0
  125. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/tests/test_bulkhead.py +0 -0
  126. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/tests/test_execution_context.py +0 -0
  127. {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/tests/test_scoped_registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chuk-tool-processor
3
- Version: 0.18
3
+ Version: 0.19
4
4
  Summary: Async-native framework for registering, discovering, and executing tools referenced in LLM responses
5
5
  Author-email: CHUK Team <chrishayuk@somejunkmailbox.com>
6
6
  Maintainer-email: CHUK Team <chrishayuk@somejunkmailbox.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "chuk-tool-processor"
7
- version = "0.18"
7
+ version = "0.19"
8
8
  description = "Async-native framework for registering, discovering, and executing tools referenced in LLM responses"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -24,6 +24,10 @@ from chuk_tool_processor.execution.wrappers.factory import (
24
24
  create_production_executor,
25
25
  create_rate_limiter,
26
26
  )
27
+ from chuk_tool_processor.execution.wrappers.observable import (
28
+ ObservableExecutor,
29
+ TracingExecutorMixin,
30
+ )
27
31
  from chuk_tool_processor.execution.wrappers.rate_limiting import (
28
32
  RateLimitedToolExecutor,
29
33
  RateLimiter,
@@ -88,6 +92,9 @@ __all__ = [
88
92
  "create_circuit_breaker",
89
93
  "create_rate_limiter",
90
94
  "create_production_executor",
95
+ # Observable execution
96
+ "ObservableExecutor",
97
+ "TracingExecutorMixin",
91
98
  ]
92
99
 
93
100
  # Add Redis exports if available
@@ -0,0 +1,417 @@
1
+ # chuk_tool_processor/execution/wrappers/observable.py
2
+ """
3
+ ObservableExecutor: Execution wrapper that produces ExecutionSpans.
4
+
5
+ This wrapper intercepts tool execution to:
6
+ - Build ExecutionSpan records
7
+ - Record to TraceSink
8
+ - Capture guard decisions
9
+ - Track timing and outcomes
10
+
11
+ This is the integration point between execution and observability.
12
+
13
+ Example:
14
+ >>> sink = InMemoryTraceSink()
15
+ >>> executor = ObservableExecutor(
16
+ ... strategy=InProcessStrategy(registry),
17
+ ... sink=sink,
18
+ ... )
19
+ >>> results = await executor.run([tool_call])
20
+ >>> # Span automatically recorded to sink
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from datetime import UTC, datetime
26
+ from typing import TYPE_CHECKING
27
+
28
+ from chuk_tool_processor.core.context import ExecutionContext, get_current_context
29
+ from chuk_tool_processor.guards.base import GuardResult
30
+ from chuk_tool_processor.models.execution_span import (
31
+ ErrorInfo,
32
+ ExecutionStrategy,
33
+ GuardDecision,
34
+ SandboxType,
35
+ SpanBuilder,
36
+ )
37
+ from chuk_tool_processor.models.execution_trace import ExecutionTrace, TraceBuilder
38
+ from chuk_tool_processor.models.tool_call import ToolCall
39
+ from chuk_tool_processor.models.tool_result import ToolResult
40
+ from chuk_tool_processor.observability.trace_sink import BaseTraceSink, get_trace_sink
41
+
42
+ if TYPE_CHECKING:
43
+ from chuk_tool_processor.execution.strategies.interface import ExecutionStrategy as StrategyInterface
44
+ from chuk_tool_processor.guards.chain import GuardChain
45
+
46
+
47
+ class ObservableExecutor:
48
+ """
49
+ Execution wrapper that produces ExecutionSpans for observability.
50
+
51
+ This wrapper sits between the caller and the underlying execution
52
+ strategy, intercepting calls to:
53
+
54
+ 1. Build span data before execution
55
+ 2. Record guard decisions
56
+ 3. Capture timing
57
+ 4. Record outcome (success/failure/blocked)
58
+ 5. Write spans to TraceSink
59
+
60
+ Example:
61
+ >>> from chuk_tool_processor.observability.trace_sink import InMemoryTraceSink
62
+ >>> sink = InMemoryTraceSink()
63
+ >>> executor = ObservableExecutor(
64
+ ... strategy=InProcessStrategy(registry),
65
+ ... sink=sink,
66
+ ... guard_chain=guard_chain,
67
+ ... )
68
+ >>>
69
+ >>> results = await executor.run([tool_call])
70
+ >>>
71
+ >>> # Query recorded spans
72
+ >>> async for span in sink.query_spans():
73
+ ... print(f"{span.tool_name}: {span.outcome}")
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ strategy: StrategyInterface,
79
+ sink: BaseTraceSink | None = None,
80
+ guard_chain: GuardChain | None = None,
81
+ trace_id: str | None = None,
82
+ record_results: bool = True,
83
+ record_arguments: bool = True,
84
+ ):
85
+ """
86
+ Initialize observable executor.
87
+
88
+ Args:
89
+ strategy: Underlying execution strategy
90
+ sink: TraceSink to record spans to (uses global if None)
91
+ guard_chain: Optional guard chain for recording guard decisions
92
+ trace_id: Trace ID to use for all spans (generates new if None)
93
+ record_results: Whether to record result values in spans
94
+ record_arguments: Whether to record argument values in spans
95
+ """
96
+ self._strategy = strategy
97
+ self._sink = sink
98
+ self._guard_chain = guard_chain
99
+ self._trace_id = trace_id
100
+ self._record_results = record_results
101
+ self._record_arguments = record_arguments
102
+
103
+ @property
104
+ def sink(self) -> BaseTraceSink:
105
+ """Get the trace sink (global if not explicitly set)."""
106
+ return self._sink or get_trace_sink()
107
+
108
+ def _get_sandbox_type(self) -> SandboxType:
109
+ """Determine sandbox type from strategy."""
110
+ strategy_name = type(self._strategy).__name__.lower()
111
+
112
+ if "subprocess" in strategy_name:
113
+ return SandboxType.PROCESS
114
+ elif "mcp" in strategy_name:
115
+ return SandboxType.MCP
116
+ elif "container" in strategy_name:
117
+ return SandboxType.CONTAINER
118
+ else:
119
+ return SandboxType.NONE
120
+
121
+ def _get_execution_strategy(self) -> ExecutionStrategy:
122
+ """Determine execution strategy enum from strategy instance."""
123
+ strategy_name = type(self._strategy).__name__.lower()
124
+
125
+ if "subprocess" in strategy_name:
126
+ return ExecutionStrategy.SUBPROCESS
127
+ elif "mcp" in strategy_name:
128
+ if "sse" in strategy_name:
129
+ return ExecutionStrategy.MCP_SSE
130
+ elif "http" in strategy_name:
131
+ return ExecutionStrategy.MCP_HTTP
132
+ else:
133
+ return ExecutionStrategy.MCP_STDIO
134
+ elif "sandbox" in strategy_name:
135
+ return ExecutionStrategy.CODE_SANDBOX
136
+ else:
137
+ return ExecutionStrategy.INPROCESS
138
+
139
+ def _create_span_builder(
140
+ self,
141
+ tool_call: ToolCall,
142
+ context: ExecutionContext | None,
143
+ ) -> SpanBuilder:
144
+ """Create a span builder for a tool call."""
145
+ return SpanBuilder(
146
+ tool_name=tool_call.tool,
147
+ arguments=tool_call.arguments if self._record_arguments else {},
148
+ namespace=tool_call.namespace,
149
+ trace_id=self._trace_id,
150
+ request_id=context.request_id if context else None,
151
+ tool_call_id=tool_call.id,
152
+ )
153
+
154
+ def _record_guard_decision(
155
+ self,
156
+ builder: SpanBuilder,
157
+ guard_result: GuardResult,
158
+ guard_name: str,
159
+ duration_ms: float = 0.0,
160
+ ) -> None:
161
+ """Record a guard decision in the span builder."""
162
+ decision = GuardDecision(
163
+ guard_name=guard_name,
164
+ guard_class=type(guard_result).__module__,
165
+ verdict=guard_result.verdict.value.upper(),
166
+ reason=guard_result.reason,
167
+ details=guard_result.details,
168
+ duration_ms=duration_ms,
169
+ repaired_args=guard_result.repaired_args,
170
+ )
171
+ builder.add_guard_decision(decision)
172
+
173
+ async def run(
174
+ self,
175
+ tool_calls: list[ToolCall],
176
+ context: ExecutionContext | None = None,
177
+ ) -> list[ToolResult]:
178
+ """
179
+ Execute tool calls with span recording.
180
+
181
+ Args:
182
+ tool_calls: List of tool calls to execute
183
+ context: Execution context (uses current if None)
184
+
185
+ Returns:
186
+ List of tool results
187
+ """
188
+ context = context or get_current_context()
189
+
190
+ # Create span builders for each call
191
+ builders: dict[str, SpanBuilder] = {}
192
+ for call in tool_calls:
193
+ builder = self._create_span_builder(call, context)
194
+ builder.set_sandbox(self._get_sandbox_type())
195
+ builder.set_strategy(self._get_execution_strategy())
196
+ builders[call.id] = builder
197
+
198
+ # Run guard checks if we have a guard chain
199
+ blocked_calls: set[str] = set()
200
+ if self._guard_chain:
201
+ for call in tool_calls:
202
+ builder = builders[call.id]
203
+ builder.start_guard_phase()
204
+
205
+ start = datetime.now(UTC)
206
+ result = self._guard_chain.check(call.tool, call.arguments)
207
+ duration = (datetime.now(UTC) - start).total_seconds() * 1000
208
+
209
+ # Record each guard's decision
210
+ self._record_guard_decision(builder, result, "GuardChain", duration)
211
+ builder.end_guard_phase()
212
+
213
+ if result.blocked:
214
+ blocked_calls.add(call.id)
215
+ builder.set_blocked()
216
+
217
+ # Filter out blocked calls
218
+ executable_calls = [c for c in tool_calls if c.id not in blocked_calls]
219
+ results: list[ToolResult] = []
220
+
221
+ # Execute non-blocked calls
222
+ if executable_calls:
223
+ for call in executable_calls:
224
+ builders[call.id].set_started()
225
+
226
+ try:
227
+ strategy_results = await self._strategy.run(executable_calls)
228
+
229
+ # Match results to calls and update builders
230
+ for call, result in zip(executable_calls, strategy_results, strict=False):
231
+ builder = builders[call.id]
232
+
233
+ if result.error:
234
+ # Handle both string errors and structured error_info
235
+ if result.error_info:
236
+ builder.set_error(
237
+ ErrorInfo(
238
+ error_type=result.error_info.code.value if result.error_info.code else "Error",
239
+ message=result.error_info.message,
240
+ retryable=result.error_info.retryable,
241
+ )
242
+ )
243
+ else:
244
+ builder.set_error(
245
+ ErrorInfo(
246
+ error_type="Error",
247
+ message=str(result.error),
248
+ retryable=False,
249
+ )
250
+ )
251
+ else:
252
+ builder.set_result(result.result if self._record_results else None)
253
+
254
+ results.extend(strategy_results)
255
+
256
+ except Exception as e:
257
+ # Execution failed entirely
258
+ for call in executable_calls:
259
+ builders[call.id].set_error(e)
260
+
261
+ raise
262
+
263
+ # Build and record all spans
264
+ for call in tool_calls:
265
+ span = builders[call.id].build()
266
+ await self.sink.record_span(span)
267
+
268
+ # Add blocked results
269
+ for call in tool_calls:
270
+ if call.id in blocked_calls:
271
+ blocking_guard = builders[call.id]._guard_decisions[-1] if builders[call.id]._guard_decisions else None
272
+ reason = blocking_guard.reason if blocking_guard else "Blocked by guard"
273
+ results.append(
274
+ ToolResult(
275
+ tool=call.tool,
276
+ result=None,
277
+ error=f"GuardBlocked: {reason}",
278
+ )
279
+ )
280
+
281
+ return results
282
+
283
+ async def run_with_trace(
284
+ self,
285
+ tool_calls: list[ToolCall],
286
+ context: ExecutionContext | None = None,
287
+ trace_name: str = "",
288
+ trace_tags: list[str] | None = None,
289
+ ) -> tuple[list[ToolResult], ExecutionTrace]:
290
+ """
291
+ Execute tool calls and return both results and complete trace.
292
+
293
+ This is useful for:
294
+ - Capturing traces for replay
295
+ - Debugging complex executions
296
+ - Training data generation
297
+
298
+ Args:
299
+ tool_calls: List of tool calls to execute
300
+ context: Execution context
301
+ trace_name: Name for the trace
302
+ trace_tags: Tags for the trace
303
+
304
+ Returns:
305
+ Tuple of (results, trace)
306
+ """
307
+ context = context or get_current_context()
308
+
309
+ # Build trace
310
+ trace_builder = TraceBuilder(name=trace_name, context=context)
311
+ trace_builder.start()
312
+ trace_builder.capture_environment()
313
+
314
+ for tag in trace_tags or []:
315
+ trace_builder.with_tag(tag)
316
+
317
+ for call in tool_calls:
318
+ trace_builder.add_tool_call(call)
319
+
320
+ # Execute
321
+ results = await self.run(tool_calls, context)
322
+
323
+ # Collect spans for the trace
324
+ async for span in self.sink.query_spans():
325
+ if any(span.tool_call_id == call.id for call in tool_calls):
326
+ trace_builder.add_span(span)
327
+
328
+ trace = trace_builder.build()
329
+
330
+ # Record complete trace
331
+ await self.sink.record_trace(trace)
332
+
333
+ return results, trace
334
+
335
+
336
+ class TracingExecutorMixin:
337
+ """
338
+ Mixin that adds tracing to any execution strategy.
339
+
340
+ Use this to add observability to custom execution strategies.
341
+
342
+ Example:
343
+ >>> class MyStrategy(TracingExecutorMixin, BaseStrategy):
344
+ ... async def run(self, tool_calls):
345
+ ... async with self.trace_execution(tool_calls) as builders:
346
+ ... results = await self._execute(tool_calls)
347
+ ... for call, result in zip(tool_calls, results):
348
+ ... builders[call.id].set_result(result.result)
349
+ ... return results
350
+ """
351
+
352
+ _sink: BaseTraceSink | None = None
353
+ _trace_id: str | None = None
354
+
355
+ @property
356
+ def trace_sink(self) -> BaseTraceSink:
357
+ """Get trace sink."""
358
+ return self._sink or get_trace_sink()
359
+
360
+ async def __aenter__(self):
361
+ """Enter async context."""
362
+ return self
363
+
364
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
365
+ """Exit async context."""
366
+ pass
367
+
368
+ class TraceContext:
369
+ """Context manager for tracing a batch of executions."""
370
+
371
+ def __init__(
372
+ self,
373
+ mixin: TracingExecutorMixin,
374
+ tool_calls: list[ToolCall],
375
+ ):
376
+ self._mixin = mixin
377
+ self._tool_calls = tool_calls
378
+ self._builders: dict[str, SpanBuilder] = {}
379
+
380
+ async def __aenter__(self) -> dict[str, SpanBuilder]:
381
+ """Start tracing."""
382
+ context = get_current_context()
383
+
384
+ for call in self._tool_calls:
385
+ builder = SpanBuilder(
386
+ tool_name=call.tool,
387
+ arguments=call.arguments,
388
+ namespace=call.namespace,
389
+ trace_id=self._mixin._trace_id,
390
+ request_id=context.request_id if context else None,
391
+ tool_call_id=call.id,
392
+ )
393
+ builder.set_started()
394
+ self._builders[call.id] = builder
395
+
396
+ return self._builders
397
+
398
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
399
+ """End tracing and record spans."""
400
+ for _call_id, builder in self._builders.items():
401
+ if exc_type is not None:
402
+ builder.set_error(exc_val)
403
+
404
+ span = builder.build()
405
+ await self._mixin.trace_sink.record_span(span)
406
+
407
+ def trace_execution(self, tool_calls: list[ToolCall]) -> TraceContext:
408
+ """
409
+ Create a trace context for a batch of executions.
410
+
411
+ Example:
412
+ >>> async with self.trace_execution(calls) as builders:
413
+ ... results = await self._do_execute(calls)
414
+ ... for call, result in zip(calls, results):
415
+ ... builders[call.id].set_result(result)
416
+ """
417
+ return self.TraceContext(self, tool_calls)
@@ -53,6 +53,10 @@ from chuk_tool_processor.guards.concurrency import (
53
53
  ConcurrencyLimitExceeded,
54
54
  ConcurrencyState,
55
55
  )
56
+ from chuk_tool_processor.guards.contract_guard import (
57
+ ContractAwareGuardChain,
58
+ ContractGuard,
59
+ )
56
60
  from chuk_tool_processor.guards.models import (
57
61
  EnforcementLevel,
58
62
  ToolClassification,
@@ -236,4 +240,7 @@ __all__ = [
236
240
  "ToolCall",
237
241
  "TraceViolation",
238
242
  "inventory_sigma_constraints",
243
+ # Contract guard
244
+ "ContractGuard",
245
+ "ContractAwareGuardChain",
239
246
  ]