agent_os_kernel 3.1.0__py3-none-any.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.
- agent_control_plane/__init__.py +662 -0
- agent_control_plane/a2a_adapter.py +543 -0
- agent_control_plane/adapter.py +417 -0
- agent_control_plane/agent_hibernation.py +394 -0
- agent_control_plane/agent_kernel.py +470 -0
- agent_control_plane/compliance.py +720 -0
- agent_control_plane/constraint_graphs.py +478 -0
- agent_control_plane/control_plane.py +854 -0
- agent_control_plane/example_executors.py +195 -0
- agent_control_plane/execution_engine.py +231 -0
- agent_control_plane/flight_recorder.py +846 -0
- agent_control_plane/governance_layer.py +435 -0
- agent_control_plane/hf_utils.py +563 -0
- agent_control_plane/interfaces/__init__.py +55 -0
- agent_control_plane/interfaces/kernel_interface.py +361 -0
- agent_control_plane/interfaces/plugin_interface.py +497 -0
- agent_control_plane/interfaces/protocol_interfaces.py +387 -0
- agent_control_plane/kernel_space.py +1009 -0
- agent_control_plane/langchain_adapter.py +424 -0
- agent_control_plane/lifecycle.py +3113 -0
- agent_control_plane/mcp_adapter.py +653 -0
- agent_control_plane/ml_safety.py +563 -0
- agent_control_plane/multimodal.py +727 -0
- agent_control_plane/mute_agent.py +422 -0
- agent_control_plane/observability.py +787 -0
- agent_control_plane/orchestrator.py +482 -0
- agent_control_plane/plugin_registry.py +750 -0
- agent_control_plane/policy_engine.py +954 -0
- agent_control_plane/process_isolation.py +777 -0
- agent_control_plane/shadow_mode.py +310 -0
- agent_control_plane/signals.py +493 -0
- agent_control_plane/supervisor_agents.py +430 -0
- agent_control_plane/time_travel_debugger.py +557 -0
- agent_control_plane/tool_registry.py +452 -0
- agent_control_plane/vfs.py +697 -0
- agent_kernel/__init__.py +69 -0
- agent_kernel/analyzer.py +435 -0
- agent_kernel/auditor.py +36 -0
- agent_kernel/completeness_auditor.py +237 -0
- agent_kernel/detector.py +203 -0
- agent_kernel/kernel.py +744 -0
- agent_kernel/memory_manager.py +85 -0
- agent_kernel/models.py +374 -0
- agent_kernel/nudge_mechanism.py +263 -0
- agent_kernel/outcome_analyzer.py +338 -0
- agent_kernel/patcher.py +582 -0
- agent_kernel/semantic_analyzer.py +316 -0
- agent_kernel/semantic_purge.py +349 -0
- agent_kernel/simulator.py +449 -0
- agent_kernel/teacher.py +85 -0
- agent_kernel/triage.py +152 -0
- agent_os/__init__.py +409 -0
- agent_os/_adversarial_impl.py +200 -0
- agent_os/_circuit_breaker_impl.py +232 -0
- agent_os/_mcp_metrics.py +193 -0
- agent_os/adversarial.py +20 -0
- agent_os/agents_compat.py +490 -0
- agent_os/audit_logger.py +135 -0
- agent_os/base_agent.py +651 -0
- agent_os/circuit_breaker.py +34 -0
- agent_os/cli/__init__.py +659 -0
- agent_os/cli/cmd_audit.py +128 -0
- agent_os/cli/cmd_init.py +152 -0
- agent_os/cli/cmd_policy.py +41 -0
- agent_os/cli/cmd_policy_gen.py +180 -0
- agent_os/cli/cmd_validate.py +258 -0
- agent_os/cli/mcp_scan.py +265 -0
- agent_os/cli/output.py +192 -0
- agent_os/cli/policy_checker.py +330 -0
- agent_os/compat.py +74 -0
- agent_os/constraint_graph.py +234 -0
- agent_os/content_governance.py +140 -0
- agent_os/context_budget.py +305 -0
- agent_os/credential_redactor.py +224 -0
- agent_os/diff_policy.py +89 -0
- agent_os/egress_policy.py +159 -0
- agent_os/escalation.py +276 -0
- agent_os/event_bus.py +124 -0
- agent_os/exceptions.py +180 -0
- agent_os/execution_context_policy.py +141 -0
- agent_os/github_enterprise.py +96 -0
- agent_os/health.py +20 -0
- agent_os/integrations/__init__.py +279 -0
- agent_os/integrations/a2a_adapter.py +279 -0
- agent_os/integrations/agent_lightning/__init__.py +30 -0
- agent_os/integrations/anthropic_adapter.py +420 -0
- agent_os/integrations/autogen_adapter.py +620 -0
- agent_os/integrations/base.py +1137 -0
- agent_os/integrations/compat.py +229 -0
- agent_os/integrations/config.py +98 -0
- agent_os/integrations/conversation_guardian.py +957 -0
- agent_os/integrations/crewai_adapter.py +467 -0
- agent_os/integrations/drift_detector.py +425 -0
- agent_os/integrations/dry_run.py +124 -0
- agent_os/integrations/escalation.py +582 -0
- agent_os/integrations/gemini_adapter.py +364 -0
- agent_os/integrations/google_adk_adapter.py +633 -0
- agent_os/integrations/guardrails_adapter.py +394 -0
- agent_os/integrations/health.py +197 -0
- agent_os/integrations/langchain_adapter.py +654 -0
- agent_os/integrations/llamafirewall.py +343 -0
- agent_os/integrations/llamaindex_adapter.py +188 -0
- agent_os/integrations/logging.py +191 -0
- agent_os/integrations/maf_adapter.py +631 -0
- agent_os/integrations/mistral_adapter.py +365 -0
- agent_os/integrations/openai_adapter.py +816 -0
- agent_os/integrations/openai_agents_sdk.py +406 -0
- agent_os/integrations/policy_compose.py +171 -0
- agent_os/integrations/profiling.py +144 -0
- agent_os/integrations/pydantic_ai_adapter.py +420 -0
- agent_os/integrations/rate_limiter.py +130 -0
- agent_os/integrations/rbac.py +143 -0
- agent_os/integrations/registry.py +113 -0
- agent_os/integrations/scope_guard.py +303 -0
- agent_os/integrations/semantic_kernel_adapter.py +769 -0
- agent_os/integrations/smolagents_adapter.py +629 -0
- agent_os/integrations/templates.py +178 -0
- agent_os/integrations/token_budget.py +134 -0
- agent_os/integrations/tool_aliases.py +190 -0
- agent_os/integrations/webhooks.py +177 -0
- agent_os/lite.py +208 -0
- agent_os/mcp_gateway.py +385 -0
- agent_os/mcp_message_signer.py +273 -0
- agent_os/mcp_protocols.py +161 -0
- agent_os/mcp_response_scanner.py +232 -0
- agent_os/mcp_security.py +924 -0
- agent_os/mcp_session_auth.py +231 -0
- agent_os/mcp_sliding_rate_limiter.py +184 -0
- agent_os/memory_guard.py +409 -0
- agent_os/metrics.py +134 -0
- agent_os/mute.py +428 -0
- agent_os/mute_agent.py +209 -0
- agent_os/policies/__init__.py +77 -0
- agent_os/policies/async_evaluator.py +275 -0
- agent_os/policies/backends.py +670 -0
- agent_os/policies/bridge.py +169 -0
- agent_os/policies/budget.py +85 -0
- agent_os/policies/cli.py +294 -0
- agent_os/policies/conflict_resolution.py +270 -0
- agent_os/policies/data_classification.py +252 -0
- agent_os/policies/evaluator.py +239 -0
- agent_os/policies/policy_schema.json +228 -0
- agent_os/policies/rate_limiting.py +145 -0
- agent_os/policies/schema.py +115 -0
- agent_os/policies/shared.py +331 -0
- agent_os/prompt_injection.py +694 -0
- agent_os/providers.py +182 -0
- agent_os/py.typed +0 -0
- agent_os/retry.py +81 -0
- agent_os/reversibility.py +251 -0
- agent_os/sandbox.py +432 -0
- agent_os/sandbox_provider.py +140 -0
- agent_os/secure_codegen.py +525 -0
- agent_os/security_skills.py +538 -0
- agent_os/semantic_policy.py +422 -0
- agent_os/server/__init__.py +15 -0
- agent_os/server/__main__.py +25 -0
- agent_os/server/app.py +277 -0
- agent_os/server/models.py +104 -0
- agent_os/shift_left_metrics.py +130 -0
- agent_os/stateless.py +742 -0
- agent_os/supervisor.py +148 -0
- agent_os/task_outcome.py +148 -0
- agent_os/transparency.py +181 -0
- agent_os/trust_root.py +128 -0
- agent_os_kernel-3.1.0.dist-info/METADATA +1269 -0
- agent_os_kernel-3.1.0.dist-info/RECORD +337 -0
- agent_os_kernel-3.1.0.dist-info/WHEEL +4 -0
- agent_os_kernel-3.1.0.dist-info/entry_points.txt +2 -0
- agent_os_kernel-3.1.0.dist-info/licenses/LICENSE +21 -0
- agent_os_observability/__init__.py +27 -0
- agent_os_observability/dashboards.py +898 -0
- agent_os_observability/metrics.py +398 -0
- agent_os_observability/server.py +223 -0
- agent_os_observability/tracer.py +232 -0
- agent_primitives/__init__.py +24 -0
- agent_primitives/failures.py +84 -0
- agent_primitives/py.typed +0 -0
- amb_core/__init__.py +177 -0
- amb_core/adapters/__init__.py +57 -0
- amb_core/adapters/aws_sqs_broker.py +376 -0
- amb_core/adapters/azure_servicebus_broker.py +340 -0
- amb_core/adapters/kafka_broker.py +260 -0
- amb_core/adapters/nats_broker.py +285 -0
- amb_core/adapters/rabbitmq_broker.py +235 -0
- amb_core/adapters/redis_broker.py +262 -0
- amb_core/broker.py +145 -0
- amb_core/bus.py +481 -0
- amb_core/cloudevents.py +509 -0
- amb_core/dlq.py +345 -0
- amb_core/hf_utils.py +536 -0
- amb_core/memory_broker.py +410 -0
- amb_core/models.py +141 -0
- amb_core/persistence.py +529 -0
- amb_core/schema.py +294 -0
- amb_core/tracing.py +358 -0
- atr/__init__.py +640 -0
- atr/access.py +348 -0
- atr/composition.py +645 -0
- atr/decorator.py +357 -0
- atr/executor.py +384 -0
- atr/health.py +557 -0
- atr/hf_utils.py +449 -0
- atr/injection.py +422 -0
- atr/metrics.py +440 -0
- atr/policies.py +403 -0
- atr/py.typed +2 -0
- atr/registry.py +452 -0
- atr/schema.py +480 -0
- atr/tools/safe/__init__.py +75 -0
- atr/tools/safe/calculator.py +467 -0
- atr/tools/safe/datetime_tool.py +443 -0
- atr/tools/safe/file_reader.py +402 -0
- atr/tools/safe/http_client.py +316 -0
- atr/tools/safe/json_parser.py +374 -0
- atr/tools/safe/text_tool.py +537 -0
- atr/tools/safe/toolkit.py +175 -0
- caas/__init__.py +162 -0
- caas/api/__init__.py +7 -0
- caas/api/server.py +1328 -0
- caas/caching.py +834 -0
- caas/cli.py +210 -0
- caas/conversation.py +223 -0
- caas/decay.py +72 -0
- caas/detection/__init__.py +9 -0
- caas/detection/detector.py +238 -0
- caas/enrichment.py +130 -0
- caas/gateway/__init__.py +27 -0
- caas/gateway/trust_gateway.py +474 -0
- caas/hf_utils.py +479 -0
- caas/ingestion/__init__.py +23 -0
- caas/ingestion/processors.py +253 -0
- caas/ingestion/structure_parser.py +188 -0
- caas/models.py +356 -0
- caas/pragmatic_truth.py +444 -0
- caas/routing/__init__.py +10 -0
- caas/routing/heuristic_router.py +58 -0
- caas/storage/__init__.py +9 -0
- caas/storage/store.py +389 -0
- caas/triad.py +213 -0
- caas/tuning/__init__.py +9 -0
- caas/tuning/tuner.py +329 -0
- caas/vfs/__init__.py +14 -0
- caas/vfs/filesystem.py +452 -0
- cmvk/__init__.py +218 -0
- cmvk/audit.py +402 -0
- cmvk/benchmarks.py +478 -0
- cmvk/constitutional.py +904 -0
- cmvk/hf_utils.py +301 -0
- cmvk/metrics.py +473 -0
- cmvk/profiles.py +300 -0
- cmvk/py.typed +0 -0
- cmvk/types.py +12 -0
- cmvk/verification.py +956 -0
- emk/__init__.py +89 -0
- emk/causal.py +352 -0
- emk/hf_utils.py +421 -0
- emk/indexer.py +83 -0
- emk/py.typed +0 -0
- emk/schema.py +204 -0
- emk/sleep_cycle.py +347 -0
- emk/store.py +281 -0
- iatp/__init__.py +166 -0
- iatp/attestation.py +461 -0
- iatp/cli.py +317 -0
- iatp/hf_utils.py +472 -0
- iatp/ipc_pipes.py +580 -0
- iatp/main.py +412 -0
- iatp/models/__init__.py +447 -0
- iatp/policy_engine.py +337 -0
- iatp/py.typed +2 -0
- iatp/recovery.py +321 -0
- iatp/security/__init__.py +270 -0
- iatp/sidecar/__init__.py +519 -0
- iatp/telemetry/__init__.py +164 -0
- iatp/tests/__init__.py +1 -0
- iatp/tests/test_attestation.py +370 -0
- iatp/tests/test_cli.py +131 -0
- iatp/tests/test_ed25519_attestation.py +211 -0
- iatp/tests/test_models.py +130 -0
- iatp/tests/test_policy_engine.py +347 -0
- iatp/tests/test_recovery.py +281 -0
- iatp/tests/test_security.py +222 -0
- iatp/tests/test_sidecar.py +167 -0
- iatp/tests/test_telemetry.py +175 -0
- mcp_kernel_server/__init__.py +28 -0
- mcp_kernel_server/cli.py +274 -0
- mcp_kernel_server/resources.py +217 -0
- mcp_kernel_server/server.py +564 -0
- mcp_kernel_server/tools.py +1174 -0
- mute_agent/__init__.py +68 -0
- mute_agent/core/__init__.py +1 -0
- mute_agent/core/execution_agent.py +166 -0
- mute_agent/core/handshake_protocol.py +201 -0
- mute_agent/core/reasoning_agent.py +238 -0
- mute_agent/knowledge_graph/__init__.py +1 -0
- mute_agent/knowledge_graph/graph_elements.py +65 -0
- mute_agent/knowledge_graph/multidimensional_graph.py +170 -0
- mute_agent/knowledge_graph/subgraph.py +224 -0
- mute_agent/listener/__init__.py +43 -0
- mute_agent/listener/adapters/__init__.py +31 -0
- mute_agent/listener/adapters/base_adapter.py +189 -0
- mute_agent/listener/adapters/caas_adapter.py +344 -0
- mute_agent/listener/adapters/control_plane_adapter.py +436 -0
- mute_agent/listener/adapters/iatp_adapter.py +332 -0
- mute_agent/listener/adapters/scak_adapter.py +251 -0
- mute_agent/listener/listener.py +610 -0
- mute_agent/listener/state_observer.py +436 -0
- mute_agent/listener/threshold_config.py +313 -0
- mute_agent/super_system/__init__.py +1 -0
- mute_agent/super_system/router.py +204 -0
- mute_agent/visualization/__init__.py +10 -0
- mute_agent/visualization/graph_debugger.py +502 -0
- nexus/README.md +60 -0
- nexus/__init__.py +51 -0
- nexus/arbiter.py +359 -0
- nexus/client.py +466 -0
- nexus/dmz.py +444 -0
- nexus/escrow.py +430 -0
- nexus/exceptions.py +286 -0
- nexus/pyproject.toml +36 -0
- nexus/registry.py +393 -0
- nexus/reputation.py +425 -0
- nexus/schemas/__init__.py +51 -0
- nexus/schemas/compliance.py +276 -0
- nexus/schemas/escrow.py +251 -0
- nexus/schemas/manifest.py +225 -0
- nexus/schemas/receipt.py +208 -0
- nexus/tests/__init__.py +0 -0
- nexus/tests/conftest.py +146 -0
- nexus/tests/test_arbiter.py +192 -0
- nexus/tests/test_dmz.py +194 -0
- nexus/tests/test_escrow.py +276 -0
- nexus/tests/test_exceptions.py +225 -0
- nexus/tests/test_registry.py +232 -0
- nexus/tests/test_reputation.py +328 -0
- nexus/tests/test_schemas.py +295 -0
agent_os/stateless.py
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Stateless Kernel — June 2026 MCP-compliant design.
|
|
4
|
+
|
|
5
|
+
This module implements a fully stateless execution kernel that complies with
|
|
6
|
+
the Model Context Protocol (MCP) specification targeted for June 2026. The
|
|
7
|
+
stateless architecture enables horizontal scaling: any kernel instance can
|
|
8
|
+
handle any request because no session state is stored in-process.
|
|
9
|
+
|
|
10
|
+
Architecture overview:
|
|
11
|
+
┌──────────────┐ ┌────────────────┐ ┌──────────────┐
|
|
12
|
+
│ Client / │────▶│ StatelessKernel │────▶│ StateBackend │
|
|
13
|
+
│ MCP Host │◀────│ (any instance) │◀────│ (Redis, etc) │
|
|
14
|
+
└──────────────┘ └────────────────┘ └──────────────┘
|
|
15
|
+
|
|
16
|
+
Key design principles:
|
|
17
|
+
- **No session state in kernel**: Every request carries its own
|
|
18
|
+
``ExecutionContext`` with agent identity, policy list, and history.
|
|
19
|
+
- **All context passed per request**: The kernel never looks up prior
|
|
20
|
+
requests; the caller is responsible for threading context.
|
|
21
|
+
- **Pluggable state backends**: State that must persist (e.g. agent
|
|
22
|
+
working memory) is stored in an external backend implementing the
|
|
23
|
+
``StateBackend`` protocol. Built-in backends:
|
|
24
|
+
|
|
25
|
+
- ``MemoryBackend``: In-memory dict with TTL support (dev/test only).
|
|
26
|
+
- ``RedisBackend``: Production-grade backend with connection pooling,
|
|
27
|
+
configurable timeouts, and optional ``RedisConfig``.
|
|
28
|
+
|
|
29
|
+
- **Horizontally scalable**: Because kernels are stateless, you can
|
|
30
|
+
run N replicas behind a load balancer with no sticky sessions.
|
|
31
|
+
|
|
32
|
+
State serialization format:
|
|
33
|
+
All state values are serialized as JSON via ``json.dumps`` / ``json.loads``.
|
|
34
|
+
Keys are prefixed with a configurable namespace (default ``"agent-os:"``)
|
|
35
|
+
to avoid collisions in shared Redis instances. A ``SerializationError``
|
|
36
|
+
is raised if a value cannot be round-tripped through JSON.
|
|
37
|
+
|
|
38
|
+
Resilience:
|
|
39
|
+
Backend calls are wrapped in a circuit breaker (see ``CircuitBreaker``)
|
|
40
|
+
that opens after repeated failures, preventing cascade failures when
|
|
41
|
+
the backend is unavailable.
|
|
42
|
+
|
|
43
|
+
Observability:
|
|
44
|
+
When OpenTelemetry is installed, the kernel emits spans for every
|
|
45
|
+
``execute()`` call and backend operation, annotated with action name,
|
|
46
|
+
agent ID, and backend type.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
>>> from agent_os.stateless import StatelessKernel, ExecutionContext
|
|
50
|
+
>>> kernel = StatelessKernel()
|
|
51
|
+
>>> ctx = ExecutionContext(agent_id="a1", policies=["read_only"])
|
|
52
|
+
>>> result = await kernel.execute("database_query", {"query": "SELECT 1"}, ctx)
|
|
53
|
+
>>> assert result.success
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
from __future__ import annotations
|
|
57
|
+
|
|
58
|
+
import hashlib
|
|
59
|
+
import json
|
|
60
|
+
import logging
|
|
61
|
+
import time
|
|
62
|
+
from dataclasses import dataclass, field
|
|
63
|
+
from datetime import datetime, timezone
|
|
64
|
+
from typing import Any, Protocol
|
|
65
|
+
|
|
66
|
+
from agent_os.circuit_breaker import CircuitBreaker, CircuitBreakerConfig, CircuitBreakerOpen
|
|
67
|
+
from agent_os.exceptions import SerializationError
|
|
68
|
+
|
|
69
|
+
logger = logging.getLogger(__name__)
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Optional OpenTelemetry support
|
|
73
|
+
# Design decision: OTel is opt-in to avoid adding a hard dependency.
|
|
74
|
+
# When present, every kernel.execute() and backend call emits a trace span
|
|
75
|
+
# so operators can correlate latency across services.
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
try:
|
|
78
|
+
from opentelemetry import context as _otel_context
|
|
79
|
+
from opentelemetry import trace as _otel_trace
|
|
80
|
+
|
|
81
|
+
_HAS_OTEL = True
|
|
82
|
+
except ImportError: # pragma: no cover
|
|
83
|
+
_otel_trace = None # type: ignore[assignment]
|
|
84
|
+
_otel_context = None # type: ignore[assignment]
|
|
85
|
+
_HAS_OTEL = False
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# =============================================================================
|
|
89
|
+
# State Backend Protocol
|
|
90
|
+
# Design decision: Using typing.Protocol (structural subtyping) instead of
|
|
91
|
+
# an ABC so that any object with get/set/delete methods satisfies the
|
|
92
|
+
# contract without explicit inheritance. This makes it easy to adapt
|
|
93
|
+
# third-party clients (e.g. DynamoDB, Cosmos DB) as backends.
|
|
94
|
+
# =============================================================================
|
|
95
|
+
|
|
96
|
+
class StateBackend(Protocol):
|
|
97
|
+
"""Protocol for external state storage.
|
|
98
|
+
|
|
99
|
+
Any object implementing ``get``, ``set``, and ``delete`` as async
|
|
100
|
+
methods satisfies this protocol via structural subtyping — no
|
|
101
|
+
explicit inheritance required.
|
|
102
|
+
|
|
103
|
+
All values are JSON-serializable dictionaries. Keys are plain strings
|
|
104
|
+
(the backend may add its own prefix for namespacing).
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
key: A unique string identifying the state entry.
|
|
108
|
+
value: A JSON-serializable dictionary to store.
|
|
109
|
+
ttl: Optional time-to-live in seconds. After expiry the entry
|
|
110
|
+
should be treated as deleted.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
async def get(self, key: str) -> dict[str, Any] | None:
|
|
114
|
+
"""Get state by key."""
|
|
115
|
+
...
|
|
116
|
+
|
|
117
|
+
async def set(self, key: str, value: dict[str, Any], ttl: int | None = None) -> None:
|
|
118
|
+
"""Set state with optional TTL."""
|
|
119
|
+
...
|
|
120
|
+
|
|
121
|
+
async def delete(self, key: str) -> None:
|
|
122
|
+
"""Delete state."""
|
|
123
|
+
...
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class MemoryBackend:
|
|
127
|
+
"""In-memory state backend for testing and development.
|
|
128
|
+
|
|
129
|
+
Stores state as ``{key: (value_dict, expires_at)}`` tuples in a plain
|
|
130
|
+
Python dictionary. TTL expiry is checked lazily on ``get()``; expired
|
|
131
|
+
entries are removed on access rather than via a background sweep.
|
|
132
|
+
|
|
133
|
+
Warning:
|
|
134
|
+
Not suitable for production. State is lost on process restart and
|
|
135
|
+
is not shared across kernel replicas. Use ``RedisBackend`` for
|
|
136
|
+
production deployments.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def __init__(self) -> None:
|
|
140
|
+
# Store maps key -> (value_dict, optional_expiry_monotonic_time).
|
|
141
|
+
# Using monotonic clock for TTL avoids issues with wall-clock jumps.
|
|
142
|
+
self._store: dict[str, tuple[dict[str, Any], float | None]] = {}
|
|
143
|
+
self._debug = False
|
|
144
|
+
|
|
145
|
+
async def get(self, key: str) -> dict[str, Any] | None:
|
|
146
|
+
entry = self._store.get(key)
|
|
147
|
+
if entry is None:
|
|
148
|
+
return None
|
|
149
|
+
value, expires_at = entry
|
|
150
|
+
if expires_at is not None and time.monotonic() >= expires_at:
|
|
151
|
+
del self._store[key]
|
|
152
|
+
return None
|
|
153
|
+
return value
|
|
154
|
+
|
|
155
|
+
async def set(self, key: str, value: dict[str, Any], ttl: int | None = None) -> None:
|
|
156
|
+
expires_at = (time.monotonic() + ttl) if ttl is not None else None
|
|
157
|
+
self._store[key] = (value, expires_at)
|
|
158
|
+
|
|
159
|
+
async def delete(self, key: str) -> None:
|
|
160
|
+
self._store.pop(key, None)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class RedisConfig:
|
|
165
|
+
"""Configuration for Redis connection pooling and timeouts.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
host: Redis server hostname.
|
|
169
|
+
port: Redis server port.
|
|
170
|
+
db: Redis database number.
|
|
171
|
+
password: Optional authentication password.
|
|
172
|
+
pool_size: Maximum number of connections in the pool.
|
|
173
|
+
connect_timeout: Timeout in seconds for establishing a connection.
|
|
174
|
+
read_timeout: Timeout in seconds for reading a response.
|
|
175
|
+
retry_on_timeout: Whether to retry commands that time out.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
host: str = "localhost"
|
|
179
|
+
port: int = 6379
|
|
180
|
+
db: int = 0
|
|
181
|
+
password: str | None = None
|
|
182
|
+
pool_size: int = 10
|
|
183
|
+
connect_timeout: float = 5.0
|
|
184
|
+
read_timeout: float = 10.0
|
|
185
|
+
retry_on_timeout: bool = True
|
|
186
|
+
|
|
187
|
+
def to_url(self) -> str:
|
|
188
|
+
"""Build a Redis URL from host/port/db."""
|
|
189
|
+
auth = f":{self.password}@" if self.password else ""
|
|
190
|
+
return f"redis://{auth}{self.host}:{self.port}/{self.db}"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class RedisBackend:
|
|
194
|
+
"""Redis state backend (for production).
|
|
195
|
+
|
|
196
|
+
Supports connection pooling and configurable timeouts via ``RedisConfig``.
|
|
197
|
+
When no config is provided the legacy ``url`` parameter is used with
|
|
198
|
+
default timeout/pool behaviour for backward compatibility.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
def __init__(
|
|
202
|
+
self,
|
|
203
|
+
url: str = "redis://localhost:6379",
|
|
204
|
+
key_prefix: str = "agent-os:",
|
|
205
|
+
config: RedisConfig | None = None,
|
|
206
|
+
):
|
|
207
|
+
if not isinstance(key_prefix, str):
|
|
208
|
+
raise TypeError(f"key_prefix must be str, got {type(key_prefix).__name__}")
|
|
209
|
+
self._config = config
|
|
210
|
+
self.url = config.to_url() if config else url
|
|
211
|
+
self._client = None
|
|
212
|
+
self._pool = None
|
|
213
|
+
self._prefix = key_prefix
|
|
214
|
+
|
|
215
|
+
async def _get_client(self):
|
|
216
|
+
if self._client is None:
|
|
217
|
+
import redis.asyncio as aioredis
|
|
218
|
+
|
|
219
|
+
if self._config is not None:
|
|
220
|
+
self._pool = aioredis.ConnectionPool.from_url(
|
|
221
|
+
self.url,
|
|
222
|
+
max_connections=self._config.pool_size,
|
|
223
|
+
socket_connect_timeout=self._config.connect_timeout,
|
|
224
|
+
socket_timeout=self._config.read_timeout,
|
|
225
|
+
retry_on_timeout=self._config.retry_on_timeout,
|
|
226
|
+
)
|
|
227
|
+
self._client = aioredis.Redis(connection_pool=self._pool)
|
|
228
|
+
else:
|
|
229
|
+
self._client = aioredis.from_url(self.url)
|
|
230
|
+
return self._client
|
|
231
|
+
|
|
232
|
+
async def get(self, key: str) -> dict[str, Any] | None:
|
|
233
|
+
client = await self._get_client()
|
|
234
|
+
data = await client.get(f"{self._prefix}{key}")
|
|
235
|
+
if not data:
|
|
236
|
+
return None
|
|
237
|
+
try:
|
|
238
|
+
return json.loads(data)
|
|
239
|
+
except (json.JSONDecodeError, TypeError) as exc:
|
|
240
|
+
logger.error(
|
|
241
|
+
"Deserialization failed: key=%s error=%s",
|
|
242
|
+
key,
|
|
243
|
+
str(exc),
|
|
244
|
+
)
|
|
245
|
+
raise SerializationError(
|
|
246
|
+
f"Failed to deserialize state for key '{key}': {exc}",
|
|
247
|
+
details={"key": key, "original_error": str(exc)},
|
|
248
|
+
) from exc
|
|
249
|
+
|
|
250
|
+
async def set(self, key: str, value: dict[str, Any], ttl: int | None = None) -> None:
|
|
251
|
+
client = await self._get_client()
|
|
252
|
+
try:
|
|
253
|
+
serialized = json.dumps(value)
|
|
254
|
+
except (TypeError, ValueError) as exc:
|
|
255
|
+
logger.error(
|
|
256
|
+
"Serialization failed: key=%s value_type=%s error=%s",
|
|
257
|
+
key,
|
|
258
|
+
type(value).__name__,
|
|
259
|
+
str(exc),
|
|
260
|
+
)
|
|
261
|
+
raise SerializationError(
|
|
262
|
+
f"Failed to serialize state for key '{key}': {exc}",
|
|
263
|
+
details={
|
|
264
|
+
"key": key,
|
|
265
|
+
"value_type": type(value).__name__,
|
|
266
|
+
"original_error": str(exc),
|
|
267
|
+
},
|
|
268
|
+
) from exc
|
|
269
|
+
await client.set(f"{self._prefix}{key}", serialized, ex=ttl)
|
|
270
|
+
|
|
271
|
+
async def delete(self, key: str) -> None:
|
|
272
|
+
client = await self._get_client()
|
|
273
|
+
await client.delete(f"{self._prefix}{key}")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# =============================================================================
|
|
277
|
+
# Stateless Request/Response Types
|
|
278
|
+
# Design decision: Using dataclasses (not Pydantic) for request/response
|
|
279
|
+
# types to keep the core kernel dependency-free. Pydantic is used in the
|
|
280
|
+
# integrations layer where richer validation is needed.
|
|
281
|
+
# =============================================================================
|
|
282
|
+
|
|
283
|
+
@dataclass
|
|
284
|
+
class ExecutionContext:
|
|
285
|
+
"""Complete context for a stateless execution request.
|
|
286
|
+
|
|
287
|
+
All state needed for a request is passed here — the kernel never
|
|
288
|
+
maintains session state internally. Callers are responsible for
|
|
289
|
+
threading the ``updated_context`` from one ``ExecutionResult`` into
|
|
290
|
+
the next request to maintain conversational continuity.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
agent_id: Unique identifier of the requesting agent.
|
|
294
|
+
policies: List of policy names to enforce (e.g. ``["read_only"]``).
|
|
295
|
+
Policy definitions are resolved from ``StatelessKernel.policies``.
|
|
296
|
+
history: Chronological list of previous actions in this session,
|
|
297
|
+
each a dict with ``action``, ``timestamp``, and ``success`` keys.
|
|
298
|
+
state_ref: Optional key referencing externalized state in the
|
|
299
|
+
backend. When present, the kernel loads this state before
|
|
300
|
+
execution and persists updates afterward.
|
|
301
|
+
metadata: Arbitrary metadata passed through to the result.
|
|
302
|
+
"""
|
|
303
|
+
agent_id: str
|
|
304
|
+
policies: list[str] = field(default_factory=list)
|
|
305
|
+
history: list[dict[str, Any]] = field(default_factory=list)
|
|
306
|
+
state_ref: str | None = None # Reference to external state
|
|
307
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
308
|
+
|
|
309
|
+
def to_dict(self) -> dict[str, Any]:
|
|
310
|
+
return {
|
|
311
|
+
"agent_id": self.agent_id,
|
|
312
|
+
"policies": self.policies,
|
|
313
|
+
"history": self.history,
|
|
314
|
+
"state_ref": self.state_ref,
|
|
315
|
+
"metadata": self.metadata
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@dataclass
|
|
320
|
+
class ExecutionRequest:
|
|
321
|
+
"""Internal representation of a stateless execution request.
|
|
322
|
+
|
|
323
|
+
Created by ``StatelessKernel.execute()`` from the caller-supplied
|
|
324
|
+
action, params, and context. The ``request_id`` is auto-generated as
|
|
325
|
+
a truncated SHA-256 hash to enable correlation in logs without
|
|
326
|
+
requiring the caller to supply an ID.
|
|
327
|
+
"""
|
|
328
|
+
action: str
|
|
329
|
+
params: dict[str, Any]
|
|
330
|
+
context: ExecutionContext
|
|
331
|
+
request_id: str | None = None
|
|
332
|
+
|
|
333
|
+
def __post_init__(self):
|
|
334
|
+
if self.request_id is None:
|
|
335
|
+
self.request_id = hashlib.sha256(
|
|
336
|
+
f"{self.context.agent_id}:{self.action}:{datetime.now(timezone.utc).isoformat()}".encode()
|
|
337
|
+
).hexdigest()[:16]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@dataclass
|
|
341
|
+
class ExecutionResult:
|
|
342
|
+
"""Result of a stateless kernel execution.
|
|
343
|
+
|
|
344
|
+
Attributes:
|
|
345
|
+
success: ``True`` if the action completed without policy violation
|
|
346
|
+
or execution error.
|
|
347
|
+
data: The action's return value (arbitrary type). ``None`` on
|
|
348
|
+
failure.
|
|
349
|
+
error: Human-readable error message when ``success`` is ``False``.
|
|
350
|
+
signal: Kernel signal emitted on failure — ``"SIGKILL"`` for policy
|
|
351
|
+
violations, ``"SIGTERM"`` for execution errors.
|
|
352
|
+
updated_context: A new ``ExecutionContext`` reflecting the latest
|
|
353
|
+
history and state reference. Callers should use this as the
|
|
354
|
+
context for subsequent requests.
|
|
355
|
+
metadata: Request metadata including ``request_id`` and timestamp.
|
|
356
|
+
"""
|
|
357
|
+
success: bool
|
|
358
|
+
data: Any
|
|
359
|
+
error: str | None = None
|
|
360
|
+
signal: str | None = None
|
|
361
|
+
updated_context: ExecutionContext | None = None
|
|
362
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# =============================================================================
|
|
366
|
+
# Stateless Kernel
|
|
367
|
+
# Design decision: The kernel is intentionally thin — it delegates policy
|
|
368
|
+
# checking, state persistence, and action execution to composable
|
|
369
|
+
# components. This keeps the kernel testable and allows swapping backends
|
|
370
|
+
# or policy engines without changing core logic.
|
|
371
|
+
# =============================================================================
|
|
372
|
+
|
|
373
|
+
class StatelessKernel:
|
|
374
|
+
"""
|
|
375
|
+
Stateless kernel for MCP June 2026 compliance.
|
|
376
|
+
|
|
377
|
+
Design principles:
|
|
378
|
+
- Every request is self-contained
|
|
379
|
+
- State stored in external backend
|
|
380
|
+
- Kernel can run on any instance (horizontal scaling)
|
|
381
|
+
- No agent registration required
|
|
382
|
+
|
|
383
|
+
Usage:
|
|
384
|
+
kernel = StatelessKernel(backend=RedisBackend())
|
|
385
|
+
|
|
386
|
+
result = await kernel.execute(
|
|
387
|
+
action="database_query",
|
|
388
|
+
params={"query": "SELECT * FROM users"},
|
|
389
|
+
context=ExecutionContext(
|
|
390
|
+
agent_id="analyst-001",
|
|
391
|
+
policies=["read_only", "no_pii"]
|
|
392
|
+
)
|
|
393
|
+
)
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
# Default policy rules
|
|
397
|
+
DEFAULT_POLICIES = {
|
|
398
|
+
"read_only": {
|
|
399
|
+
"blocked_actions": ["file_write", "database_write", "send_email"],
|
|
400
|
+
"constraints": {"database_query": {"mode": "read"}}
|
|
401
|
+
},
|
|
402
|
+
"no_pii": {
|
|
403
|
+
"blocked_patterns": ["ssn", "social_security", "credit_card", "password"]
|
|
404
|
+
},
|
|
405
|
+
"strict": {
|
|
406
|
+
"require_approval": ["send_email", "file_write", "code_execution"]
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
def __init__(
|
|
411
|
+
self,
|
|
412
|
+
backend: StateBackend | None = None,
|
|
413
|
+
policies: dict[str, Any] | None = None,
|
|
414
|
+
enable_tracing: bool = False,
|
|
415
|
+
circuit_breaker_config: CircuitBreakerConfig | None = None,
|
|
416
|
+
):
|
|
417
|
+
self.backend = backend or MemoryBackend()
|
|
418
|
+
self.policies = {**self.DEFAULT_POLICIES, **(policies or {})}
|
|
419
|
+
self.enable_tracing = enable_tracing and _HAS_OTEL
|
|
420
|
+
self._tracer = (
|
|
421
|
+
_otel_trace.get_tracer("agent_os.stateless") if self.enable_tracing else None
|
|
422
|
+
)
|
|
423
|
+
self._backend_type = type(self.backend).__name__
|
|
424
|
+
self.circuit_breaker = CircuitBreaker(circuit_breaker_config)
|
|
425
|
+
|
|
426
|
+
async def execute(
|
|
427
|
+
self,
|
|
428
|
+
action: str,
|
|
429
|
+
params: dict[str, Any],
|
|
430
|
+
context: ExecutionContext
|
|
431
|
+
) -> ExecutionResult:
|
|
432
|
+
"""
|
|
433
|
+
Execute an action statelessly with full policy governance.
|
|
434
|
+
|
|
435
|
+
This is the main entry point. Every request is self-contained:
|
|
436
|
+
policies are checked, the action is executed, state is updated
|
|
437
|
+
externally, and an updated context is returned.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
action: Action to execute (e.g., "database_query", "file_write", "chat")
|
|
441
|
+
params: Action parameters (passed to handler and checked against policies)
|
|
442
|
+
context: Complete execution context including agent_id, policies, and history
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
ExecutionResult with:
|
|
446
|
+
- success=True, data=result, updated_context (on success)
|
|
447
|
+
- success=False, error=reason, signal="SIGKILL" (on policy violation)
|
|
448
|
+
- success=False, error=str(e), signal="SIGTERM" (on execution error)
|
|
449
|
+
|
|
450
|
+
Example:
|
|
451
|
+
>>> result = await kernel.execute(
|
|
452
|
+
... action="database_query",
|
|
453
|
+
... params={"query": "SELECT * FROM users"},
|
|
454
|
+
... context=ExecutionContext(agent_id="a1", policies=["read_only"])
|
|
455
|
+
... )
|
|
456
|
+
>>> if result.success:
|
|
457
|
+
... print(result.data)
|
|
458
|
+
... else:
|
|
459
|
+
... print(f"Blocked: {result.error}")
|
|
460
|
+
"""
|
|
461
|
+
request = ExecutionRequest(action=action, params=params, context=context)
|
|
462
|
+
|
|
463
|
+
span_ctx = self._start_span("kernel.execute", {
|
|
464
|
+
"operation": "execute",
|
|
465
|
+
"action": action,
|
|
466
|
+
"agent_id": context.agent_id,
|
|
467
|
+
"backend_type": self._backend_type,
|
|
468
|
+
})
|
|
469
|
+
try:
|
|
470
|
+
return await self._execute_inner(request, action, params, context)
|
|
471
|
+
finally:
|
|
472
|
+
self._end_span(span_ctx)
|
|
473
|
+
|
|
474
|
+
async def _execute_inner(
|
|
475
|
+
self,
|
|
476
|
+
request: ExecutionRequest,
|
|
477
|
+
action: str,
|
|
478
|
+
params: dict[str, Any],
|
|
479
|
+
context: ExecutionContext,
|
|
480
|
+
) -> ExecutionResult:
|
|
481
|
+
"""Core execute logic, called inside an optional tracing span."""
|
|
482
|
+
# 1. Load external state if referenced
|
|
483
|
+
external_state: dict[str, Any] = {}
|
|
484
|
+
if context.state_ref:
|
|
485
|
+
external_state = await self._backend_get(context.state_ref) or {}
|
|
486
|
+
|
|
487
|
+
# 2. Check policies
|
|
488
|
+
policy_result = self._check_policies(action, params, context.policies)
|
|
489
|
+
if not policy_result["allowed"]:
|
|
490
|
+
return ExecutionResult(
|
|
491
|
+
success=False,
|
|
492
|
+
data=None,
|
|
493
|
+
error=policy_result["reason"],
|
|
494
|
+
signal="SIGKILL",
|
|
495
|
+
metadata={
|
|
496
|
+
"request_id": request.request_id,
|
|
497
|
+
"violation": policy_result["reason"],
|
|
498
|
+
"timestamp": datetime.now(timezone.utc).isoformat()
|
|
499
|
+
}
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
# 3. Execute action
|
|
503
|
+
try:
|
|
504
|
+
result = await self._execute_action(action, params, external_state)
|
|
505
|
+
except Exception as e:
|
|
506
|
+
return ExecutionResult(
|
|
507
|
+
success=False,
|
|
508
|
+
data=None,
|
|
509
|
+
error=str(e),
|
|
510
|
+
signal="SIGTERM",
|
|
511
|
+
metadata={"request_id": request.request_id}
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
# 4. Update external state if needed
|
|
515
|
+
new_state_ref = context.state_ref
|
|
516
|
+
if result.get("state_update"):
|
|
517
|
+
new_state = {**external_state, **result["state_update"]}
|
|
518
|
+
new_state_ref = new_state_ref or f"state:{context.agent_id}"
|
|
519
|
+
await self._backend_set(new_state_ref, new_state)
|
|
520
|
+
|
|
521
|
+
# 5. Build updated context
|
|
522
|
+
updated_context = ExecutionContext(
|
|
523
|
+
agent_id=context.agent_id,
|
|
524
|
+
policies=context.policies,
|
|
525
|
+
history=context.history + [{
|
|
526
|
+
"action": action,
|
|
527
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
528
|
+
"success": True
|
|
529
|
+
}],
|
|
530
|
+
state_ref=new_state_ref,
|
|
531
|
+
metadata=context.metadata
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
return ExecutionResult(
|
|
535
|
+
success=True,
|
|
536
|
+
data=result.get("data"),
|
|
537
|
+
updated_context=updated_context,
|
|
538
|
+
metadata={
|
|
539
|
+
"request_id": request.request_id,
|
|
540
|
+
"timestamp": datetime.now(timezone.utc).isoformat()
|
|
541
|
+
}
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
def _check_policies(
|
|
545
|
+
self,
|
|
546
|
+
action: str,
|
|
547
|
+
params: dict[str, Any],
|
|
548
|
+
policy_names: list[str]
|
|
549
|
+
) -> dict[str, Any]:
|
|
550
|
+
"""Check if action is allowed under policies.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
action: The action being attempted (e.g., "database_query", "file_write")
|
|
554
|
+
params: Parameters for the action
|
|
555
|
+
policy_names: List of policy names to check against
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
Dict with 'allowed' (bool) and 'reason' (str) keys.
|
|
559
|
+
When blocked, includes 'suggestion' with actionable fix.
|
|
560
|
+
"""
|
|
561
|
+
for policy_name in policy_names:
|
|
562
|
+
policy = self.policies.get(policy_name)
|
|
563
|
+
if not policy:
|
|
564
|
+
continue
|
|
565
|
+
|
|
566
|
+
# Check blocked actions
|
|
567
|
+
if action in policy.get("blocked_actions", []):
|
|
568
|
+
allowed_actions = [a for a in ["read", "query", "list"]
|
|
569
|
+
if a not in policy.get("blocked_actions", [])]
|
|
570
|
+
suggestion = (f"Try a read-only action instead (e.g., {', '.join(allowed_actions[:3])})"
|
|
571
|
+
if allowed_actions else "Request policy exception from administrator")
|
|
572
|
+
return {
|
|
573
|
+
"allowed": False,
|
|
574
|
+
"reason": f"Action '{action}' blocked by '{policy_name}' policy. {suggestion}."
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
# Check blocked patterns in params
|
|
578
|
+
params_str = json.dumps(params).lower()
|
|
579
|
+
for pattern in policy.get("blocked_patterns", []):
|
|
580
|
+
if pattern.lower() in params_str:
|
|
581
|
+
return {
|
|
582
|
+
"allowed": False,
|
|
583
|
+
"reason": (
|
|
584
|
+
f"Content blocked: '{pattern}' detected in request parameters. "
|
|
585
|
+
f"Policy '{policy_name}' prohibits this pattern. "
|
|
586
|
+
f"Remove the sensitive content and retry."
|
|
587
|
+
)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
# Check requires approval
|
|
591
|
+
if action in policy.get("require_approval", []):
|
|
592
|
+
if not params.get("approved"):
|
|
593
|
+
return {
|
|
594
|
+
"allowed": False,
|
|
595
|
+
"reason": (
|
|
596
|
+
f"Action '{action}' requires approval. "
|
|
597
|
+
f"Add approved=True to params after getting authorization, "
|
|
598
|
+
f"or use a non-restricted action instead."
|
|
599
|
+
)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return {"allowed": True, "reason": None}
|
|
603
|
+
|
|
604
|
+
async def _execute_action(
|
|
605
|
+
self,
|
|
606
|
+
action: str,
|
|
607
|
+
params: dict[str, Any],
|
|
608
|
+
state: dict[str, Any]
|
|
609
|
+
) -> dict[str, Any]:
|
|
610
|
+
"""Execute action (stub - real impl dispatches to handlers)."""
|
|
611
|
+
return {
|
|
612
|
+
"data": {
|
|
613
|
+
"status": "executed",
|
|
614
|
+
"action": action,
|
|
615
|
+
"result": f"Action '{action}' executed successfully"
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
# -----------------------------------------------------------------
|
|
620
|
+
# Backend wrappers (circuit breaker + tracing)
|
|
621
|
+
# Design decision: All backend calls go through the circuit breaker
|
|
622
|
+
# to prevent cascading failures when the backend (e.g. Redis) is
|
|
623
|
+
# down. The breaker opens after repeated failures and returns
|
|
624
|
+
# CircuitBreakerOpen without hitting the backend, giving it time
|
|
625
|
+
# to recover.
|
|
626
|
+
# -----------------------------------------------------------------
|
|
627
|
+
|
|
628
|
+
async def _backend_get(self, key: str) -> dict[str, Any] | None:
|
|
629
|
+
"""Get from backend through circuit breaker with tracing."""
|
|
630
|
+
span_ctx = self._start_span("kernel.backend.get", {
|
|
631
|
+
"operation": "get",
|
|
632
|
+
"key": key,
|
|
633
|
+
"backend_type": self._backend_type,
|
|
634
|
+
})
|
|
635
|
+
try:
|
|
636
|
+
return await self.circuit_breaker.call(self.backend.get, key)
|
|
637
|
+
except CircuitBreakerOpen:
|
|
638
|
+
raise
|
|
639
|
+
finally:
|
|
640
|
+
self._end_span(span_ctx)
|
|
641
|
+
|
|
642
|
+
async def _backend_set(
|
|
643
|
+
self, key: str, value: dict[str, Any], ttl: int | None = None
|
|
644
|
+
) -> None:
|
|
645
|
+
"""Set in backend through circuit breaker with tracing."""
|
|
646
|
+
span_ctx = self._start_span("kernel.backend.set", {
|
|
647
|
+
"operation": "set",
|
|
648
|
+
"key": key,
|
|
649
|
+
"backend_type": self._backend_type,
|
|
650
|
+
})
|
|
651
|
+
try:
|
|
652
|
+
await self.circuit_breaker.call(self.backend.set, key, value, ttl)
|
|
653
|
+
except CircuitBreakerOpen:
|
|
654
|
+
raise
|
|
655
|
+
finally:
|
|
656
|
+
self._end_span(span_ctx)
|
|
657
|
+
|
|
658
|
+
async def _backend_delete(self, key: str) -> None:
|
|
659
|
+
"""Delete from backend through circuit breaker with tracing."""
|
|
660
|
+
span_ctx = self._start_span("kernel.backend.delete", {
|
|
661
|
+
"operation": "delete",
|
|
662
|
+
"key": key,
|
|
663
|
+
"backend_type": self._backend_type,
|
|
664
|
+
})
|
|
665
|
+
try:
|
|
666
|
+
await self.circuit_breaker.call(self.backend.delete, key)
|
|
667
|
+
except CircuitBreakerOpen:
|
|
668
|
+
raise
|
|
669
|
+
finally:
|
|
670
|
+
self._end_span(span_ctx)
|
|
671
|
+
|
|
672
|
+
# -----------------------------------------------------------------
|
|
673
|
+
# OpenTelemetry helpers
|
|
674
|
+
# -----------------------------------------------------------------
|
|
675
|
+
|
|
676
|
+
def _start_span(
|
|
677
|
+
self, name: str, attributes: dict[str, str]
|
|
678
|
+
) -> Any | None:
|
|
679
|
+
"""Start an OTel span if tracing is enabled. Returns a context token."""
|
|
680
|
+
if not self._tracer:
|
|
681
|
+
return None
|
|
682
|
+
span = self._tracer.start_span(name, attributes=attributes)
|
|
683
|
+
ctx = _otel_trace.set_span_in_context(span)
|
|
684
|
+
token = _otel_context.attach(ctx)
|
|
685
|
+
return (span, token)
|
|
686
|
+
|
|
687
|
+
@staticmethod
|
|
688
|
+
def _end_span(span_ctx: Any | None) -> None:
|
|
689
|
+
"""End the OTel span if present."""
|
|
690
|
+
if span_ctx is None:
|
|
691
|
+
return
|
|
692
|
+
span, token = span_ctx
|
|
693
|
+
span.end()
|
|
694
|
+
_otel_context.detach(token)
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
# =============================================================================
|
|
698
|
+
# Helper Functions
|
|
699
|
+
# =============================================================================
|
|
700
|
+
|
|
701
|
+
async def stateless_execute(
|
|
702
|
+
action: str,
|
|
703
|
+
params: dict,
|
|
704
|
+
agent_id: str,
|
|
705
|
+
policies: list[str] | None = None,
|
|
706
|
+
history: list[dict] | None = None,
|
|
707
|
+
backend: StateBackend | None = None
|
|
708
|
+
) -> ExecutionResult:
|
|
709
|
+
"""Convenience function for one-shot stateless execution.
|
|
710
|
+
|
|
711
|
+
Creates an ephemeral ``StatelessKernel`` and ``ExecutionContext``,
|
|
712
|
+
executes the action, and returns the result. Useful for simple
|
|
713
|
+
scripts and tests where managing a kernel instance is unnecessary.
|
|
714
|
+
|
|
715
|
+
Args:
|
|
716
|
+
action: Action to execute (e.g. ``"database_query"``).
|
|
717
|
+
params: Action parameters.
|
|
718
|
+
agent_id: Identifier of the requesting agent.
|
|
719
|
+
policies: Policy names to enforce. Defaults to ``[]``.
|
|
720
|
+
history: Prior action history. Defaults to ``[]``.
|
|
721
|
+
backend: Optional ``StateBackend``. Defaults to ``MemoryBackend``.
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
An ``ExecutionResult`` with the outcome of the action.
|
|
725
|
+
|
|
726
|
+
Example:
|
|
727
|
+
>>> result = await stateless_execute(
|
|
728
|
+
... action="database_query",
|
|
729
|
+
... params={"query": "SELECT * FROM users"},
|
|
730
|
+
... agent_id="analyst-001",
|
|
731
|
+
... policies=["read_only"],
|
|
732
|
+
... )
|
|
733
|
+
>>> print(result.success)
|
|
734
|
+
True
|
|
735
|
+
"""
|
|
736
|
+
kernel = StatelessKernel(backend=backend)
|
|
737
|
+
context = ExecutionContext(
|
|
738
|
+
agent_id=agent_id,
|
|
739
|
+
policies=policies or [],
|
|
740
|
+
history=history or []
|
|
741
|
+
)
|
|
742
|
+
return await kernel.execute(action, params, context)
|