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.
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/PKG-INFO +1 -1
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/pyproject.toml +1 -1
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/wrappers/__init__.py +7 -0
- chuk_tool_processor-0.19/src/chuk_tool_processor/execution/wrappers/observable.py +417 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/__init__.py +7 -0
- chuk_tool_processor-0.19/src/chuk_tool_processor/guards/contract_guard.py +250 -0
- chuk_tool_processor-0.19/src/chuk_tool_processor/models/__init__.py +94 -0
- chuk_tool_processor-0.19/src/chuk_tool_processor/models/execution_span.py +651 -0
- chuk_tool_processor-0.19/src/chuk_tool_processor/models/execution_trace.py +571 -0
- chuk_tool_processor-0.19/src/chuk_tool_processor/models/sandbox_policy.py +552 -0
- chuk_tool_processor-0.19/src/chuk_tool_processor/models/tool_contract.py +552 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/models/tool_spec.py +80 -1
- chuk_tool_processor-0.19/src/chuk_tool_processor/observability/__init__.py +68 -0
- chuk_tool_processor-0.19/src/chuk_tool_processor/observability/trace_sink.py +677 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor.egg-info/PKG-INFO +1 -1
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor.egg-info/SOURCES.txt +7 -0
- chuk_tool_processor-0.18/src/chuk_tool_processor/models/__init__.py +0 -21
- chuk_tool_processor-0.18/src/chuk_tool_processor/observability/__init__.py +0 -30
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/README.md +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/setup.cfg +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/__init__.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/config.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/core/__init__.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/core/context.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/core/exceptions.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/core/processor.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/discovery/__init__.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/discovery/dynamic_provider.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/discovery/search.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/discovery/searchable.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/discovery/synonyms.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/__init__.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/bulkhead.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/code_sandbox.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/strategies/__init__.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/strategies/inprocess_strategy.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/strategies/subprocess_strategy.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/tool_executor.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/wrappers/caching.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/wrappers/circuit_breaker.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/wrappers/factory.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/wrappers/rate_limiting.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/wrappers/redis_circuit_breaker.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/wrappers/redis_rate_limiting.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/execution/wrappers/retry.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/assumption_trace.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/base.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/budget.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/chain.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/concurrency.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/models.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/network_policy.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/output_size.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/per_tool.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/plan_shape.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/precondition.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/provenance.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/retry_safety.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/runaway.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/saturation.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/schema_strictness.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/sensitive_data.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/side_effect.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/timeout_budget.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/unresolved.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/logging/__init__.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/logging/context.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/logging/formatter.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/logging/helpers.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/logging/metrics.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/__init__.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/mcp_tool.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/middleware.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/models.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/register_mcp_tools.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/setup_mcp_http_streamable.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/setup_mcp_sse.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/setup_mcp_stdio.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/stream_manager.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/transport/__init__.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/transport/base_transport.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/transport/http_streamable_transport.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/transport/models.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/transport/sse_transport.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/mcp/transport/stdio_transport.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/models/execution_strategy.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/models/return_order.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/models/streaming_tool.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/models/tool_call.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/models/tool_export_mixin.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/models/tool_result.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/models/validated_tool.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/observability/metrics.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/observability/setup.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/observability/tracing.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/plugins/__init__.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/plugins/discovery.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/plugins/parsers/__init__.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/plugins/parsers/base.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/plugins/parsers/function_call_tool.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/plugins/parsers/json_tool.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/plugins/parsers/openai_tool.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/plugins/parsers/xml_tool.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/py.typed +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/registry/__init__.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/registry/auto_register.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/registry/decorators.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/registry/interface.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/registry/metadata.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/registry/provider.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/registry/providers/__init__.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/registry/providers/memory.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/registry/providers/redis.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/registry/tool_export.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/scheduling/__init__.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/scheduling/greedy_dag.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/scheduling/policy.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/scheduling/types.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/utils/__init__.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/utils/fast_json.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/utils/validation.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor.egg-info/dependency_links.txt +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor.egg-info/requires.txt +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor.egg-info/top_level.txt +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/tests/test_bulkhead.py +0 -0
- {chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/tests/test_execution_context.py +0 -0
- {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.
|
|
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.
|
|
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)
|
{chuk_tool_processor-0.18 → chuk_tool_processor-0.19}/src/chuk_tool_processor/guards/__init__.py
RENAMED
|
@@ -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
|
]
|