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
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""
|
|
4
|
+
OpenAI Assistants Integration
|
|
5
|
+
|
|
6
|
+
Wraps OpenAI Assistants API with Agent OS governance.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from agent_os.integrations import OpenAIKernel
|
|
10
|
+
from openai import OpenAI
|
|
11
|
+
|
|
12
|
+
client = OpenAI()
|
|
13
|
+
kernel = OpenAIKernel(policy="strict")
|
|
14
|
+
|
|
15
|
+
# Create assistant as normal
|
|
16
|
+
assistant = client.beta.assistants.create(
|
|
17
|
+
name="Trading Bot",
|
|
18
|
+
instructions="You analyze market data",
|
|
19
|
+
model="gpt-4-turbo"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Wrap for governance
|
|
23
|
+
governed_assistant = kernel.wrap(assistant, client)
|
|
24
|
+
|
|
25
|
+
# All runs are now governed!
|
|
26
|
+
thread = governed_assistant.create_thread()
|
|
27
|
+
governed_assistant.add_message(thread.id, "Analyze AAPL")
|
|
28
|
+
run = governed_assistant.run(thread.id) # Governed execution
|
|
29
|
+
|
|
30
|
+
Features:
|
|
31
|
+
- Pre-execution policy checks
|
|
32
|
+
- Tool call interception and validation
|
|
33
|
+
- Real-time run monitoring
|
|
34
|
+
- SIGKILL support (cancel run on violation)
|
|
35
|
+
- Full audit trail
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
import json
|
|
39
|
+
import logging
|
|
40
|
+
import random
|
|
41
|
+
import time
|
|
42
|
+
from collections.abc import Generator
|
|
43
|
+
from dataclasses import dataclass, field
|
|
44
|
+
from datetime import datetime
|
|
45
|
+
from typing import Any, Callable, Optional
|
|
46
|
+
|
|
47
|
+
from .base import BaseIntegration, ExecutionContext, GovernancePolicy
|
|
48
|
+
|
|
49
|
+
logger = logging.getLogger("agent_os.openai")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class AssistantContext(ExecutionContext):
|
|
54
|
+
"""Extended execution context for OpenAI Assistants.
|
|
55
|
+
|
|
56
|
+
Tracks assistant-specific state including thread IDs, run IDs,
|
|
57
|
+
function call history, and cumulative token usage for governance
|
|
58
|
+
enforcement.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
assistant_id: The OpenAI assistant identifier.
|
|
62
|
+
thread_ids: List of thread IDs created during this session.
|
|
63
|
+
run_ids: List of run IDs executed during this session.
|
|
64
|
+
function_calls: History of function/tool calls made by the assistant.
|
|
65
|
+
prompt_tokens: Cumulative prompt tokens consumed across all runs.
|
|
66
|
+
completion_tokens: Cumulative completion tokens consumed across all runs.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
assistant_id: str = ""
|
|
70
|
+
thread_ids: list[str] = field(default_factory=list)
|
|
71
|
+
run_ids: list[str] = field(default_factory=list)
|
|
72
|
+
function_calls: list[dict] = field(default_factory=list)
|
|
73
|
+
|
|
74
|
+
# Token tracking
|
|
75
|
+
prompt_tokens: int = 0
|
|
76
|
+
completion_tokens: int = 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Transient error base classes for retry detection
|
|
80
|
+
_TRANSIENT_ERROR_NAMES = ("RateLimitError", "APIConnectionError", "Timeout", "APITimeoutError")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _is_transient(exc: Exception) -> bool:
|
|
84
|
+
"""Return True if the exception is a transient OpenAI error."""
|
|
85
|
+
return type(exc).__name__ in _TRANSIENT_ERROR_NAMES
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def retry_with_backoff(
|
|
89
|
+
fn: Callable[..., Any],
|
|
90
|
+
*args: Any,
|
|
91
|
+
max_retries: int = 3,
|
|
92
|
+
base_delay: float = 1.0,
|
|
93
|
+
max_delay: float = 30.0,
|
|
94
|
+
**kwargs: Any,
|
|
95
|
+
) -> Any:
|
|
96
|
+
"""Call *fn* with exponential backoff + jitter on transient errors.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
fn: Callable to invoke.
|
|
100
|
+
max_retries: Number of retry attempts after the initial call.
|
|
101
|
+
base_delay: Base delay in seconds for backoff calculation.
|
|
102
|
+
max_delay: Upper bound for the computed delay.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
The return value of *fn*.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
The last caught exception if all retries are exhausted.
|
|
109
|
+
"""
|
|
110
|
+
last_exc: Optional[Exception] = None
|
|
111
|
+
for attempt in range(max_retries + 1):
|
|
112
|
+
try:
|
|
113
|
+
return fn(*args, **kwargs)
|
|
114
|
+
except Exception as exc:
|
|
115
|
+
if not _is_transient(exc) or attempt == max_retries:
|
|
116
|
+
raise
|
|
117
|
+
last_exc = exc
|
|
118
|
+
delay = min(base_delay * (2 ** attempt) + random.uniform(0, 1), max_delay)
|
|
119
|
+
logger.warning(
|
|
120
|
+
"Retry %d/%d for %s after %s (delay=%.2fs)",
|
|
121
|
+
attempt + 1,
|
|
122
|
+
max_retries,
|
|
123
|
+
fn.__name__ if hasattr(fn, "__name__") else str(fn),
|
|
124
|
+
type(exc).__name__,
|
|
125
|
+
delay,
|
|
126
|
+
)
|
|
127
|
+
time.sleep(delay)
|
|
128
|
+
raise last_exc # type: ignore[misc]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class OpenAIKernel(BaseIntegration):
|
|
132
|
+
"""
|
|
133
|
+
OpenAI Assistants adapter for Agent OS.
|
|
134
|
+
|
|
135
|
+
Provides governance for:
|
|
136
|
+
- Assistant creation/modification
|
|
137
|
+
- Thread management
|
|
138
|
+
- Run execution
|
|
139
|
+
- Tool/function calls
|
|
140
|
+
- File operations
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
kernel = OpenAIKernel(policy=GovernancePolicy(
|
|
144
|
+
max_tokens=10000,
|
|
145
|
+
allowed_tools=["code_interpreter", "retrieval"],
|
|
146
|
+
blocked_patterns=["password", "api_key", "secret"]
|
|
147
|
+
))
|
|
148
|
+
|
|
149
|
+
governed = kernel.wrap(assistant, client)
|
|
150
|
+
result = governed.run(thread_id)
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(
|
|
154
|
+
self,
|
|
155
|
+
policy: Optional[GovernancePolicy] = None,
|
|
156
|
+
max_retries: int = 3,
|
|
157
|
+
timeout_seconds: float = 300.0,
|
|
158
|
+
):
|
|
159
|
+
"""Initialise the OpenAI governance kernel.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
policy: Governance policy to enforce. When ``None`` the default
|
|
163
|
+
``GovernancePolicy`` is used.
|
|
164
|
+
max_retries: Maximum number of retry attempts for transient
|
|
165
|
+
OpenAI errors (default 3).
|
|
166
|
+
timeout_seconds: Default timeout in seconds for operations
|
|
167
|
+
(default 300).
|
|
168
|
+
"""
|
|
169
|
+
super().__init__(policy)
|
|
170
|
+
self.max_retries = max_retries
|
|
171
|
+
self.timeout_seconds = timeout_seconds
|
|
172
|
+
self._wrapped_assistants: dict[str, Any] = {} # assistant_id -> original
|
|
173
|
+
self._clients: dict[str, Any] = {} # assistant_id -> client
|
|
174
|
+
self._cancelled_runs: set[str] = set()
|
|
175
|
+
self._start_time = time.monotonic()
|
|
176
|
+
self._last_error: Optional[str] = None
|
|
177
|
+
|
|
178
|
+
def wrap(self, agent: Any, client: Any = None) -> "GovernedAssistant":
|
|
179
|
+
"""Wrap an OpenAI Assistant with governance.
|
|
180
|
+
|
|
181
|
+
This is the primary wrapping method, consistent with all other
|
|
182
|
+
adapters. OpenAI Assistants require both an assistant object
|
|
183
|
+
**and** a client, so ``client`` must be provided.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
agent: OpenAI Assistant object.
|
|
187
|
+
client: OpenAI client instance (required).
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
GovernedAssistant with full governance.
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
TypeError: If *client* is not provided.
|
|
194
|
+
"""
|
|
195
|
+
if client is None:
|
|
196
|
+
raise TypeError(
|
|
197
|
+
"OpenAIKernel.wrap() requires a 'client' argument: "
|
|
198
|
+
"kernel.wrap(assistant, client)"
|
|
199
|
+
)
|
|
200
|
+
assistant_id = agent.id
|
|
201
|
+
ctx = AssistantContext(
|
|
202
|
+
agent_id=assistant_id,
|
|
203
|
+
session_id=f"oai-{int(time.time())}",
|
|
204
|
+
policy=self.policy,
|
|
205
|
+
assistant_id=assistant_id
|
|
206
|
+
)
|
|
207
|
+
self.contexts[assistant_id] = ctx
|
|
208
|
+
self._wrapped_assistants[assistant_id] = agent
|
|
209
|
+
self._clients[assistant_id] = client
|
|
210
|
+
|
|
211
|
+
return GovernedAssistant(
|
|
212
|
+
assistant=agent,
|
|
213
|
+
client=client,
|
|
214
|
+
kernel=self,
|
|
215
|
+
ctx=ctx
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def wrap_assistant(self, assistant: Any, client: Any) -> "GovernedAssistant":
|
|
219
|
+
"""Wrap an OpenAI Assistant with governance.
|
|
220
|
+
|
|
221
|
+
.. deprecated::
|
|
222
|
+
Use :meth:`wrap` instead::
|
|
223
|
+
|
|
224
|
+
governed = kernel.wrap(assistant, client)
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
assistant: OpenAI Assistant object.
|
|
228
|
+
client: OpenAI client instance.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
GovernedAssistant with full governance.
|
|
232
|
+
"""
|
|
233
|
+
import warnings
|
|
234
|
+
warnings.warn(
|
|
235
|
+
"wrap_assistant() is deprecated, use wrap(assistant, client) instead.",
|
|
236
|
+
DeprecationWarning,
|
|
237
|
+
stacklevel=2,
|
|
238
|
+
)
|
|
239
|
+
return self.wrap(assistant, client)
|
|
240
|
+
|
|
241
|
+
def unwrap(self, governed_agent: Any) -> Any:
|
|
242
|
+
"""Retrieve the original unwrapped assistant.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
governed_agent: A ``GovernedAssistant`` or any object.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
The original OpenAI assistant object if *governed_agent* is a
|
|
249
|
+
``GovernedAssistant``; otherwise returns *governed_agent* as-is.
|
|
250
|
+
"""
|
|
251
|
+
if isinstance(governed_agent, GovernedAssistant):
|
|
252
|
+
return governed_agent._assistant
|
|
253
|
+
return governed_agent
|
|
254
|
+
|
|
255
|
+
def cancel_run(self, thread_id: str, run_id: str, client: Any):
|
|
256
|
+
"""Cancel a run (SIGKILL equivalent).
|
|
257
|
+
|
|
258
|
+
Immediately marks the run as cancelled locally and issues a cancel
|
|
259
|
+
request to the OpenAI API. If the API call fails (e.g. the run
|
|
260
|
+
has already completed), the error is silently ignored.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
thread_id: The thread the run belongs to.
|
|
264
|
+
run_id: The run to cancel.
|
|
265
|
+
client: OpenAI client used to issue the cancellation.
|
|
266
|
+
"""
|
|
267
|
+
self._cancelled_runs.add(run_id)
|
|
268
|
+
try:
|
|
269
|
+
client.beta.threads.runs.cancel(
|
|
270
|
+
thread_id=thread_id,
|
|
271
|
+
run_id=run_id
|
|
272
|
+
)
|
|
273
|
+
except Exception: # noqa: BLE001 — best-effort cancel, run may already be complete
|
|
274
|
+
logger.warning("Run cancel failed (may already be complete): thread=%s run=%s", thread_id, run_id, exc_info=True)
|
|
275
|
+
|
|
276
|
+
def is_cancelled(self, run_id: str) -> bool:
|
|
277
|
+
"""Check whether a run has been cancelled via :meth:`cancel_run`.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
run_id: The run identifier to check.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
``True`` if the run was previously cancelled.
|
|
284
|
+
"""
|
|
285
|
+
return run_id in self._cancelled_runs
|
|
286
|
+
|
|
287
|
+
def health_check(self) -> dict[str, Any]:
|
|
288
|
+
"""Return adapter health status.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
A dict with ``status``, ``backend``, ``last_error``, and
|
|
292
|
+
``uptime_seconds`` keys.
|
|
293
|
+
"""
|
|
294
|
+
uptime = time.monotonic() - self._start_time
|
|
295
|
+
has_clients = bool(self._clients)
|
|
296
|
+
if self._last_error:
|
|
297
|
+
status = "degraded"
|
|
298
|
+
elif not has_clients:
|
|
299
|
+
status = "healthy"
|
|
300
|
+
else:
|
|
301
|
+
status = "healthy"
|
|
302
|
+
return {
|
|
303
|
+
"status": status,
|
|
304
|
+
"backend": "openai",
|
|
305
|
+
"backend_connected": has_clients,
|
|
306
|
+
"last_error": self._last_error,
|
|
307
|
+
"uptime_seconds": round(uptime, 2),
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class GovernedAssistant:
|
|
312
|
+
"""
|
|
313
|
+
OpenAI Assistant wrapped with Agent OS governance.
|
|
314
|
+
|
|
315
|
+
All API calls are intercepted for policy enforcement.
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
def __init__(
|
|
319
|
+
self,
|
|
320
|
+
assistant: Any,
|
|
321
|
+
client: Any,
|
|
322
|
+
kernel: OpenAIKernel,
|
|
323
|
+
ctx: AssistantContext
|
|
324
|
+
):
|
|
325
|
+
self._assistant = assistant
|
|
326
|
+
self._client = client
|
|
327
|
+
self._kernel = kernel
|
|
328
|
+
self._ctx = ctx
|
|
329
|
+
self._tool_registry: dict[str, Callable] = {}
|
|
330
|
+
|
|
331
|
+
def register_tool(self, name: str, func: Callable) -> None:
|
|
332
|
+
"""Register a tool function for automatic execution."""
|
|
333
|
+
self._tool_registry[name] = func
|
|
334
|
+
|
|
335
|
+
@property
|
|
336
|
+
def id(self) -> str:
|
|
337
|
+
"""Assistant ID"""
|
|
338
|
+
return self._assistant.id
|
|
339
|
+
|
|
340
|
+
@property
|
|
341
|
+
def name(self) -> str:
|
|
342
|
+
"""Assistant name"""
|
|
343
|
+
return self._assistant.name
|
|
344
|
+
|
|
345
|
+
# =========================================================================
|
|
346
|
+
# Thread Management
|
|
347
|
+
# =========================================================================
|
|
348
|
+
|
|
349
|
+
def create_thread(self, **kwargs) -> Any:
|
|
350
|
+
"""Create a new conversation thread.
|
|
351
|
+
|
|
352
|
+
The thread ID is automatically recorded in the execution context
|
|
353
|
+
for audit purposes.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
**kwargs: Forwarded to ``client.beta.threads.create()``.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
The newly created OpenAI thread object.
|
|
360
|
+
"""
|
|
361
|
+
thread = self._client.beta.threads.create(**kwargs)
|
|
362
|
+
self._ctx.thread_ids.append(thread.id)
|
|
363
|
+
return thread
|
|
364
|
+
|
|
365
|
+
def get_thread(self, thread_id: str) -> Any:
|
|
366
|
+
"""Retrieve an existing thread by ID.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
thread_id: The thread to retrieve.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
The OpenAI thread object.
|
|
373
|
+
"""
|
|
374
|
+
return self._client.beta.threads.retrieve(thread_id)
|
|
375
|
+
|
|
376
|
+
def delete_thread(self, thread_id: str) -> bool:
|
|
377
|
+
"""Delete a thread and remove it from the execution context.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
thread_id: The thread to delete.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
``True`` if the thread was successfully deleted.
|
|
384
|
+
"""
|
|
385
|
+
result = self._client.beta.threads.delete(thread_id)
|
|
386
|
+
if thread_id in self._ctx.thread_ids:
|
|
387
|
+
self._ctx.thread_ids.remove(thread_id)
|
|
388
|
+
return result.deleted
|
|
389
|
+
|
|
390
|
+
# =========================================================================
|
|
391
|
+
# Message Management
|
|
392
|
+
# =========================================================================
|
|
393
|
+
|
|
394
|
+
def add_message(
|
|
395
|
+
self,
|
|
396
|
+
thread_id: str,
|
|
397
|
+
content: str,
|
|
398
|
+
role: str = "user",
|
|
399
|
+
**kwargs
|
|
400
|
+
) -> Any:
|
|
401
|
+
"""Add a message to a thread with pre-execution policy checks.
|
|
402
|
+
|
|
403
|
+
The message content is validated against ``blocked_patterns`` before
|
|
404
|
+
being sent to the OpenAI API.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
thread_id: Target thread.
|
|
408
|
+
content: Message text.
|
|
409
|
+
role: Message role (default ``"user"``).
|
|
410
|
+
**kwargs: Additional parameters forwarded to the API.
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
The created OpenAI message object.
|
|
414
|
+
|
|
415
|
+
Raises:
|
|
416
|
+
PolicyViolationError: If the content matches a blocked pattern.
|
|
417
|
+
"""
|
|
418
|
+
# Pre-check: blocked patterns
|
|
419
|
+
allowed, reason = self._kernel.pre_execute(self._ctx, content)
|
|
420
|
+
if not allowed:
|
|
421
|
+
raise PolicyViolationError(f"Message blocked: {reason}")
|
|
422
|
+
|
|
423
|
+
message = self._client.beta.threads.messages.create(
|
|
424
|
+
thread_id=thread_id,
|
|
425
|
+
role=role,
|
|
426
|
+
content=content,
|
|
427
|
+
**kwargs
|
|
428
|
+
)
|
|
429
|
+
return message
|
|
430
|
+
|
|
431
|
+
def list_messages(self, thread_id: str, **kwargs) -> list:
|
|
432
|
+
"""List messages in a thread.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
thread_id: The thread whose messages to list.
|
|
436
|
+
**kwargs: Additional parameters (e.g. ``limit``, ``order``).
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
A list of message objects in the thread.
|
|
440
|
+
"""
|
|
441
|
+
return self._client.beta.threads.messages.list(
|
|
442
|
+
thread_id=thread_id,
|
|
443
|
+
**kwargs
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
# =========================================================================
|
|
447
|
+
# Run Execution (Core Governance)
|
|
448
|
+
# =========================================================================
|
|
449
|
+
|
|
450
|
+
def run(
|
|
451
|
+
self,
|
|
452
|
+
thread_id: str,
|
|
453
|
+
instructions: Optional[str] = None,
|
|
454
|
+
tools: Optional[list] = None,
|
|
455
|
+
poll_interval: float = 1.0,
|
|
456
|
+
**kwargs
|
|
457
|
+
) -> Any:
|
|
458
|
+
"""
|
|
459
|
+
Execute a governed run.
|
|
460
|
+
|
|
461
|
+
This is the primary method for executing the assistant.
|
|
462
|
+
All tool calls and outputs are validated against policy.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
thread_id: Thread to run on
|
|
466
|
+
instructions: Optional override instructions
|
|
467
|
+
tools: Optional tools to enable
|
|
468
|
+
poll_interval: How often to check run status
|
|
469
|
+
**kwargs: Additional run parameters
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
Completed run object
|
|
473
|
+
|
|
474
|
+
Raises:
|
|
475
|
+
PolicyViolationError: If policy is violated
|
|
476
|
+
RunCancelledException: If run was SIGKILL'd
|
|
477
|
+
"""
|
|
478
|
+
# Pre-check
|
|
479
|
+
if instructions:
|
|
480
|
+
allowed, reason = self._kernel.pre_execute(self._ctx, instructions)
|
|
481
|
+
if not allowed:
|
|
482
|
+
raise PolicyViolationError(f"Instructions blocked: {reason}")
|
|
483
|
+
|
|
484
|
+
# Validate tools against policy
|
|
485
|
+
if tools:
|
|
486
|
+
self._validate_tools(tools)
|
|
487
|
+
|
|
488
|
+
# Create run
|
|
489
|
+
run_kwargs = {
|
|
490
|
+
"thread_id": thread_id,
|
|
491
|
+
"assistant_id": self._assistant.id,
|
|
492
|
+
**kwargs
|
|
493
|
+
}
|
|
494
|
+
if instructions:
|
|
495
|
+
run_kwargs["instructions"] = instructions
|
|
496
|
+
if tools:
|
|
497
|
+
run_kwargs["tools"] = tools
|
|
498
|
+
|
|
499
|
+
run = self._client.beta.threads.runs.create(**run_kwargs)
|
|
500
|
+
self._ctx.run_ids.append(run.id)
|
|
501
|
+
|
|
502
|
+
# Poll until complete (with governance checks)
|
|
503
|
+
return self._poll_run(thread_id, run.id, poll_interval)
|
|
504
|
+
|
|
505
|
+
def run_stream(
|
|
506
|
+
self,
|
|
507
|
+
thread_id: str,
|
|
508
|
+
instructions: Optional[str] = None,
|
|
509
|
+
**kwargs
|
|
510
|
+
) -> Generator:
|
|
511
|
+
"""
|
|
512
|
+
Stream a governed run.
|
|
513
|
+
|
|
514
|
+
Yields events as they arrive, with real-time policy checks.
|
|
515
|
+
"""
|
|
516
|
+
# Pre-check
|
|
517
|
+
if instructions:
|
|
518
|
+
allowed, reason = self._kernel.pre_execute(self._ctx, instructions)
|
|
519
|
+
if not allowed:
|
|
520
|
+
raise PolicyViolationError(f"Instructions blocked: {reason}")
|
|
521
|
+
|
|
522
|
+
# Create streaming run
|
|
523
|
+
with self._client.beta.threads.runs.stream(
|
|
524
|
+
thread_id=thread_id,
|
|
525
|
+
assistant_id=self._assistant.id,
|
|
526
|
+
instructions=instructions,
|
|
527
|
+
**kwargs
|
|
528
|
+
) as stream:
|
|
529
|
+
for event in stream:
|
|
530
|
+
# Check for cancellation
|
|
531
|
+
if hasattr(event, 'data') and hasattr(event.data, 'id'):
|
|
532
|
+
if self._kernel.is_cancelled(event.data.id):
|
|
533
|
+
raise RunCancelledException("Run was cancelled (SIGKILL)")
|
|
534
|
+
|
|
535
|
+
# Yield event
|
|
536
|
+
yield event
|
|
537
|
+
|
|
538
|
+
def _poll_run(
|
|
539
|
+
self,
|
|
540
|
+
thread_id: str,
|
|
541
|
+
run_id: str,
|
|
542
|
+
poll_interval: float
|
|
543
|
+
) -> Any:
|
|
544
|
+
"""
|
|
545
|
+
Poll run status with governance checks.
|
|
546
|
+
"""
|
|
547
|
+
while True:
|
|
548
|
+
# Check for SIGKILL
|
|
549
|
+
if self._kernel.is_cancelled(run_id):
|
|
550
|
+
raise RunCancelledException("Run was cancelled (SIGKILL)")
|
|
551
|
+
|
|
552
|
+
run = self._client.beta.threads.runs.retrieve(
|
|
553
|
+
thread_id=thread_id,
|
|
554
|
+
run_id=run_id
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
# Update token counts
|
|
558
|
+
if hasattr(run, 'usage') and run.usage:
|
|
559
|
+
self._ctx.prompt_tokens += run.usage.prompt_tokens or 0
|
|
560
|
+
self._ctx.completion_tokens += run.usage.completion_tokens or 0
|
|
561
|
+
|
|
562
|
+
# Check token limit
|
|
563
|
+
total = self._ctx.prompt_tokens + self._ctx.completion_tokens
|
|
564
|
+
if total > self._kernel.policy.max_tokens:
|
|
565
|
+
self._kernel.cancel_run(thread_id, run_id, self._client)
|
|
566
|
+
raise PolicyViolationError(
|
|
567
|
+
f"Token limit exceeded: {total} > {self._kernel.policy.max_tokens}"
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
# Handle different statuses
|
|
571
|
+
if run.status == "completed":
|
|
572
|
+
self._kernel.post_execute(self._ctx, run)
|
|
573
|
+
return run
|
|
574
|
+
|
|
575
|
+
elif run.status == "requires_action":
|
|
576
|
+
# Tool calls need approval
|
|
577
|
+
run = self._handle_tool_calls(thread_id, run)
|
|
578
|
+
|
|
579
|
+
elif run.status in ["failed", "cancelled", "expired"]:
|
|
580
|
+
return run
|
|
581
|
+
|
|
582
|
+
elif run.status in ["queued", "in_progress"]:
|
|
583
|
+
time.sleep(poll_interval)
|
|
584
|
+
|
|
585
|
+
else:
|
|
586
|
+
# Unknown status
|
|
587
|
+
time.sleep(poll_interval)
|
|
588
|
+
|
|
589
|
+
def _handle_tool_calls(self, thread_id: str, run: Any) -> Any:
|
|
590
|
+
"""
|
|
591
|
+
Handle tool calls with policy validation.
|
|
592
|
+
"""
|
|
593
|
+
tool_calls = run.required_action.submit_tool_outputs.tool_calls
|
|
594
|
+
tool_outputs = []
|
|
595
|
+
|
|
596
|
+
for tool_call in tool_calls:
|
|
597
|
+
# Record tool call
|
|
598
|
+
call_info = {
|
|
599
|
+
"id": tool_call.id,
|
|
600
|
+
"type": tool_call.type,
|
|
601
|
+
"function": tool_call.function.name if hasattr(tool_call, 'function') else None,
|
|
602
|
+
"arguments": tool_call.function.arguments if hasattr(tool_call, 'function') else None,
|
|
603
|
+
"timestamp": datetime.now().isoformat()
|
|
604
|
+
}
|
|
605
|
+
self._ctx.function_calls.append(call_info)
|
|
606
|
+
self._ctx.tool_calls.append(call_info)
|
|
607
|
+
|
|
608
|
+
# Check tool call count
|
|
609
|
+
if len(self._ctx.tool_calls) > self._kernel.policy.max_tool_calls:
|
|
610
|
+
self._kernel.cancel_run(thread_id, run.id, self._client)
|
|
611
|
+
raise PolicyViolationError(
|
|
612
|
+
f"Tool call limit exceeded: {len(self._ctx.tool_calls)} > {self._kernel.policy.max_tool_calls}"
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
# Validate function name
|
|
616
|
+
if hasattr(tool_call, 'function'):
|
|
617
|
+
func_name = tool_call.function.name
|
|
618
|
+
if self._kernel.policy.allowed_tools:
|
|
619
|
+
if func_name not in self._kernel.policy.allowed_tools:
|
|
620
|
+
self._kernel.cancel_run(thread_id, run.id, self._client)
|
|
621
|
+
raise PolicyViolationError(
|
|
622
|
+
f"Tool not allowed: {func_name}"
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# Check human approval requirement
|
|
626
|
+
if self._kernel.policy.require_human_approval:
|
|
627
|
+
tool_outputs.append({
|
|
628
|
+
"tool_call_id": tool_call.id,
|
|
629
|
+
"output": json.dumps({
|
|
630
|
+
"status": "requires_approval",
|
|
631
|
+
"function": func_name if hasattr(tool_call, 'function') else "unknown",
|
|
632
|
+
"message": "Tool execution requires human approval per governance policy"
|
|
633
|
+
})
|
|
634
|
+
})
|
|
635
|
+
continue
|
|
636
|
+
|
|
637
|
+
# Execute via tool registry if available
|
|
638
|
+
output = None
|
|
639
|
+
if hasattr(self, '_tool_registry') and self._tool_registry:
|
|
640
|
+
func_name_exec = tool_call.function.name if hasattr(tool_call, 'function') else None
|
|
641
|
+
if func_name_exec and func_name_exec in self._tool_registry:
|
|
642
|
+
try:
|
|
643
|
+
args = json.loads(tool_call.function.arguments) if hasattr(tool_call, 'function') else {}
|
|
644
|
+
result = self._tool_registry[func_name_exec](**args)
|
|
645
|
+
output = json.dumps(result) if not isinstance(result, str) else result
|
|
646
|
+
except Exception as e:
|
|
647
|
+
logger.warning("Tool execution failed for %s", func_name_exec, exc_info=True)
|
|
648
|
+
output = json.dumps({"status": "error", "message": str(e)})
|
|
649
|
+
|
|
650
|
+
if output is None:
|
|
651
|
+
output = json.dumps({
|
|
652
|
+
"status": "no_executor",
|
|
653
|
+
"function": tool_call.function.name if hasattr(tool_call, 'function') else "unknown",
|
|
654
|
+
"message": "No tool executor registered for this function"
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
tool_outputs.append({
|
|
658
|
+
"tool_call_id": tool_call.id,
|
|
659
|
+
"output": output
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
# Submit outputs
|
|
663
|
+
return self._client.beta.threads.runs.submit_tool_outputs(
|
|
664
|
+
thread_id=thread_id,
|
|
665
|
+
run_id=run.id,
|
|
666
|
+
tool_outputs=tool_outputs
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
def _validate_tools(self, tools: list):
|
|
670
|
+
"""Validate tools against policy"""
|
|
671
|
+
if not self._kernel.policy.allowed_tools:
|
|
672
|
+
return # No restrictions
|
|
673
|
+
|
|
674
|
+
for tool in tools:
|
|
675
|
+
tool_type = tool.get("type") if isinstance(tool, dict) else getattr(tool, "type", None)
|
|
676
|
+
if tool_type and tool_type not in self._kernel.policy.allowed_tools:
|
|
677
|
+
raise PolicyViolationError(f"Tool type not allowed: {tool_type}")
|
|
678
|
+
|
|
679
|
+
# =========================================================================
|
|
680
|
+
# Signal Handling
|
|
681
|
+
# =========================================================================
|
|
682
|
+
|
|
683
|
+
def sigkill(self, thread_id: str, run_id: str):
|
|
684
|
+
"""Send SIGKILL to a running assistant — immediately cancels the run.
|
|
685
|
+
|
|
686
|
+
This is the primary mechanism for forcibly stopping a governed
|
|
687
|
+
assistant that has violated policy or needs emergency termination.
|
|
688
|
+
|
|
689
|
+
Args:
|
|
690
|
+
thread_id: Thread containing the run.
|
|
691
|
+
run_id: The run to kill.
|
|
692
|
+
|
|
693
|
+
Example:
|
|
694
|
+
>>> governed.sigkill(thread_id="thread_abc", run_id="run_xyz")
|
|
695
|
+
"""
|
|
696
|
+
self._kernel.cancel_run(thread_id, run_id, self._client)
|
|
697
|
+
|
|
698
|
+
def sigstop(self, thread_id: str, run_id: str):
|
|
699
|
+
"""Send SIGSTOP to a running assistant.
|
|
700
|
+
|
|
701
|
+
.. note::
|
|
702
|
+
|
|
703
|
+
The OpenAI Assistants API does not support pausing a run, so
|
|
704
|
+
this behaves identically to :meth:`sigkill` (cancels the run).
|
|
705
|
+
|
|
706
|
+
Args:
|
|
707
|
+
thread_id: Thread containing the run.
|
|
708
|
+
run_id: The run to stop.
|
|
709
|
+
"""
|
|
710
|
+
self._kernel.cancel_run(thread_id, run_id, self._client)
|
|
711
|
+
|
|
712
|
+
# =========================================================================
|
|
713
|
+
# Utility
|
|
714
|
+
# =========================================================================
|
|
715
|
+
|
|
716
|
+
def get_context(self) -> AssistantContext:
|
|
717
|
+
"""Return the execution context containing the full audit trail.
|
|
718
|
+
|
|
719
|
+
Returns:
|
|
720
|
+
The ``AssistantContext`` for this governed assistant, including
|
|
721
|
+
thread IDs, run IDs, function call history, and token usage.
|
|
722
|
+
"""
|
|
723
|
+
return self._ctx
|
|
724
|
+
|
|
725
|
+
def get_token_usage(self) -> dict:
|
|
726
|
+
"""Return cumulative token usage statistics.
|
|
727
|
+
|
|
728
|
+
Returns:
|
|
729
|
+
A dict with keys ``prompt_tokens``, ``completion_tokens``,
|
|
730
|
+
``total_tokens``, and ``limit``.
|
|
731
|
+
|
|
732
|
+
Example:
|
|
733
|
+
>>> governed.get_token_usage()
|
|
734
|
+
{'prompt_tokens': 120, 'completion_tokens': 80, 'total_tokens': 200, 'limit': 10000}
|
|
735
|
+
"""
|
|
736
|
+
return {
|
|
737
|
+
"prompt_tokens": self._ctx.prompt_tokens,
|
|
738
|
+
"completion_tokens": self._ctx.completion_tokens,
|
|
739
|
+
"total_tokens": self._ctx.prompt_tokens + self._ctx.completion_tokens,
|
|
740
|
+
"limit": self._kernel.policy.max_tokens
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
def __getattr__(self, name):
|
|
744
|
+
"""Proxy attribute access to the underlying OpenAI assistant.
|
|
745
|
+
|
|
746
|
+
Allows transparent access to assistant properties (e.g. ``model``,
|
|
747
|
+
``instructions``) that are not explicitly overridden by this wrapper.
|
|
748
|
+
"""
|
|
749
|
+
return getattr(self._assistant, name)
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
class PolicyViolationError(Exception):
|
|
753
|
+
"""Raised when an assistant action violates governance policy.
|
|
754
|
+
|
|
755
|
+
Contains a human-readable reason describing which policy was violated.
|
|
756
|
+
"""
|
|
757
|
+
|
|
758
|
+
pass
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
class RunCancelledException(Exception):
|
|
762
|
+
"""Raised when a run is forcibly cancelled via SIGKILL.
|
|
763
|
+
|
|
764
|
+
Indicates that ``cancel_run`` (or ``sigkill``) was invoked, either
|
|
765
|
+
directly or automatically by the governance layer (e.g. token limit
|
|
766
|
+
exceeded, disallowed tool call).
|
|
767
|
+
"""
|
|
768
|
+
|
|
769
|
+
pass
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
# ============================================================================
|
|
773
|
+
# Convenience Functions
|
|
774
|
+
# ============================================================================
|
|
775
|
+
|
|
776
|
+
def wrap(
|
|
777
|
+
assistant: Any,
|
|
778
|
+
client: Any,
|
|
779
|
+
policy: Optional[GovernancePolicy] = None,
|
|
780
|
+
max_retries: int = 3,
|
|
781
|
+
timeout_seconds: float = 300.0,
|
|
782
|
+
) -> GovernedAssistant:
|
|
783
|
+
"""Quick wrapper for OpenAI Assistants.
|
|
784
|
+
|
|
785
|
+
Example::
|
|
786
|
+
|
|
787
|
+
from agent_os.integrations.openai_adapter import wrap
|
|
788
|
+
|
|
789
|
+
governed = wrap(my_assistant, openai_client)
|
|
790
|
+
result = governed.run(thread_id)
|
|
791
|
+
"""
|
|
792
|
+
return OpenAIKernel(
|
|
793
|
+
policy, max_retries=max_retries, timeout_seconds=timeout_seconds
|
|
794
|
+
).wrap(assistant, client)
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def wrap_assistant(
|
|
798
|
+
assistant: Any,
|
|
799
|
+
client: Any,
|
|
800
|
+
policy: Optional[GovernancePolicy] = None,
|
|
801
|
+
max_retries: int = 3,
|
|
802
|
+
timeout_seconds: float = 300.0,
|
|
803
|
+
) -> GovernedAssistant:
|
|
804
|
+
"""Quick wrapper for OpenAI Assistants.
|
|
805
|
+
|
|
806
|
+
.. deprecated::
|
|
807
|
+
Use :func:`wrap` instead.
|
|
808
|
+
"""
|
|
809
|
+
import warnings
|
|
810
|
+
warnings.warn(
|
|
811
|
+
"wrap_assistant() is deprecated, use wrap() instead.",
|
|
812
|
+
DeprecationWarning,
|
|
813
|
+
stacklevel=2,
|
|
814
|
+
)
|
|
815
|
+
return wrap(assistant, client, policy=policy, max_retries=max_retries,
|
|
816
|
+
timeout_seconds=timeout_seconds)
|