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,234 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Constraint Graph — DAG-based resource access control.
|
|
4
|
+
|
|
5
|
+
The constraint graph is the **only** path through which agents may access
|
|
6
|
+
resources (tools, APIs, data). Every resource access request is resolved
|
|
7
|
+
by traversing a directed acyclic graph of constraint edges that encode
|
|
8
|
+
allow/deny rules with optional conditions.
|
|
9
|
+
|
|
10
|
+
Architecture:
|
|
11
|
+
Agent ──▶ ConstraintGraph.resolve() ──▶ allow / deny
|
|
12
|
+
│
|
|
13
|
+
├─ match agent_pattern against agent_id
|
|
14
|
+
├─ match resource name
|
|
15
|
+
└─ evaluate conditions (time, role, etc.)
|
|
16
|
+
|
|
17
|
+
Integration:
|
|
18
|
+
``ConstraintGraphEnforcer`` implements the same ``intercept()`` protocol
|
|
19
|
+
as ``PolicyInterceptor`` so it can be composed via ``CompositeInterceptor``.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import fnmatch
|
|
25
|
+
import logging
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from enum import Enum
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Data models
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
class Permission(Enum):
|
|
38
|
+
"""Permission type for a constraint edge."""
|
|
39
|
+
ALLOW = "allow"
|
|
40
|
+
DENY = "deny"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ResourceType(Enum):
|
|
44
|
+
"""Classification of a governed resource."""
|
|
45
|
+
TOOL = "tool"
|
|
46
|
+
API = "api"
|
|
47
|
+
DATA = "data"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class ResourceNode:
|
|
52
|
+
"""A resource (tool, API, data) in the constraint graph.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
name: Unique resource identifier (e.g. ``"database_query"``).
|
|
56
|
+
resource_type: Classification of the resource.
|
|
57
|
+
metadata: Arbitrary key/value metadata for condition evaluation.
|
|
58
|
+
"""
|
|
59
|
+
name: str
|
|
60
|
+
resource_type: ResourceType = ResourceType.TOOL
|
|
61
|
+
metadata: dict[str, Any] = field(default_factory=dict, hash=False)
|
|
62
|
+
|
|
63
|
+
def __hash__(self) -> int:
|
|
64
|
+
return hash((self.name, self.resource_type))
|
|
65
|
+
|
|
66
|
+
def __eq__(self, other: object) -> bool:
|
|
67
|
+
if not isinstance(other, ResourceNode):
|
|
68
|
+
return NotImplemented
|
|
69
|
+
return self.name == other.name and self.resource_type == other.resource_type
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class ConstraintEdge:
|
|
74
|
+
"""A constraint linking an agent pattern to a resource permission.
|
|
75
|
+
|
|
76
|
+
Attributes:
|
|
77
|
+
agent_pattern: Glob pattern matched against agent IDs.
|
|
78
|
+
resource: The resource this constraint governs.
|
|
79
|
+
permission: Whether to allow or deny access.
|
|
80
|
+
conditions: Optional key/value conditions that must all be satisfied
|
|
81
|
+
for this edge to apply (e.g. ``{"role": "admin"}``).
|
|
82
|
+
priority: Higher-priority edges take precedence during resolution.
|
|
83
|
+
"""
|
|
84
|
+
agent_pattern: str
|
|
85
|
+
resource: str
|
|
86
|
+
permission: Permission
|
|
87
|
+
conditions: dict[str, Any] = field(default_factory=dict)
|
|
88
|
+
priority: int = 0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# Constraint Graph
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
class ConstraintGraph:
|
|
96
|
+
"""DAG of resource constraints.
|
|
97
|
+
|
|
98
|
+
Edges are evaluated in priority order (highest first). The first matching
|
|
99
|
+
edge determines the outcome. If no edge matches, access is **denied** by
|
|
100
|
+
default (deny-by-default posture).
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(self) -> None:
|
|
104
|
+
self._nodes: dict[str, ResourceNode] = {}
|
|
105
|
+
self._edges: list[ConstraintEdge] = []
|
|
106
|
+
|
|
107
|
+
# -- mutators -----------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
def add_resource(self, node: ResourceNode) -> None:
|
|
110
|
+
"""Register a resource node."""
|
|
111
|
+
self._nodes[node.name] = node
|
|
112
|
+
|
|
113
|
+
def add_constraint(self, edge: ConstraintEdge) -> None:
|
|
114
|
+
"""Add a constraint edge and re-sort by descending priority."""
|
|
115
|
+
self._edges.append(edge)
|
|
116
|
+
self._edges.sort(key=lambda e: e.priority, reverse=True)
|
|
117
|
+
|
|
118
|
+
# -- query --------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def resources(self) -> dict[str, ResourceNode]:
|
|
122
|
+
"""Read-only view of registered resources."""
|
|
123
|
+
return dict(self._nodes)
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def edges(self) -> list[ConstraintEdge]:
|
|
127
|
+
"""Read-only copy of constraint edges (sorted by priority)."""
|
|
128
|
+
return list(self._edges)
|
|
129
|
+
|
|
130
|
+
# -- resolution ---------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
def resolve(
|
|
133
|
+
self,
|
|
134
|
+
agent_id: str,
|
|
135
|
+
resource: str,
|
|
136
|
+
context: dict[str, Any] | None = None,
|
|
137
|
+
) -> bool:
|
|
138
|
+
"""Check whether *agent_id* may access *resource*.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
agent_id: The requesting agent's identifier.
|
|
142
|
+
resource: Name of the target resource.
|
|
143
|
+
context: Runtime context used for condition evaluation.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
``True`` if access is allowed, ``False`` otherwise.
|
|
147
|
+
"""
|
|
148
|
+
context = context or {}
|
|
149
|
+
|
|
150
|
+
for edge in self._edges:
|
|
151
|
+
if not fnmatch.fnmatch(agent_id, edge.agent_pattern):
|
|
152
|
+
continue
|
|
153
|
+
if not fnmatch.fnmatch(resource, edge.resource):
|
|
154
|
+
continue
|
|
155
|
+
if not self._conditions_met(edge.conditions, context):
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
allowed = edge.permission == Permission.ALLOW
|
|
159
|
+
logger.debug(
|
|
160
|
+
"constraint resolved: agent=%s resource=%s -> %s (priority=%d)",
|
|
161
|
+
agent_id,
|
|
162
|
+
resource,
|
|
163
|
+
edge.permission.value,
|
|
164
|
+
edge.priority,
|
|
165
|
+
)
|
|
166
|
+
return allowed
|
|
167
|
+
|
|
168
|
+
# Deny by default
|
|
169
|
+
logger.debug(
|
|
170
|
+
"constraint resolved: agent=%s resource=%s -> deny (no matching edge)",
|
|
171
|
+
agent_id,
|
|
172
|
+
resource,
|
|
173
|
+
)
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
# -- internals ----------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def _conditions_met(
|
|
180
|
+
conditions: dict[str, Any],
|
|
181
|
+
context: dict[str, Any],
|
|
182
|
+
) -> bool:
|
|
183
|
+
"""Return ``True`` if every condition key/value is present in *context*."""
|
|
184
|
+
return all(context.get(k) == v for k, v in conditions.items())
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
# Enforcer (PolicyInterceptor-compatible)
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
class ConstraintGraphEnforcer:
|
|
192
|
+
"""Intercepts tool calls and enforces the constraint graph.
|
|
193
|
+
|
|
194
|
+
Implements the same ``intercept(request) -> result`` protocol used by
|
|
195
|
+
``PolicyInterceptor`` and ``CompositeInterceptor`` so it can be plugged
|
|
196
|
+
into the existing governance pipeline.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
def __init__(
|
|
200
|
+
self,
|
|
201
|
+
graph: ConstraintGraph,
|
|
202
|
+
context: dict[str, Any] | None = None,
|
|
203
|
+
) -> None:
|
|
204
|
+
self.graph = graph
|
|
205
|
+
self.context = context or {}
|
|
206
|
+
|
|
207
|
+
def intercept(self, request: Any) -> Any:
|
|
208
|
+
"""Enforce constraint graph for a ``ToolCallRequest``.
|
|
209
|
+
|
|
210
|
+
Imports are deferred to avoid circular dependency with
|
|
211
|
+
``integrations.base``.
|
|
212
|
+
"""
|
|
213
|
+
from agent_os.integrations.base import ToolCallResult
|
|
214
|
+
|
|
215
|
+
agent_id = getattr(request, "agent_id", "") or ""
|
|
216
|
+
tool_name = getattr(request, "tool_name", "") or ""
|
|
217
|
+
|
|
218
|
+
if not agent_id:
|
|
219
|
+
return ToolCallResult(
|
|
220
|
+
allowed=False,
|
|
221
|
+
reason="Constraint graph requires agent_id on the request",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
allowed = self.graph.resolve(agent_id, tool_name, self.context)
|
|
225
|
+
if not allowed:
|
|
226
|
+
return ToolCallResult(
|
|
227
|
+
allowed=False,
|
|
228
|
+
reason=(
|
|
229
|
+
f"Constraint graph denied agent '{agent_id}' "
|
|
230
|
+
f"access to resource '{tool_name}'"
|
|
231
|
+
),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return ToolCallResult(allowed=True)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Content and knowledge quality governance for AI agents.
|
|
4
|
+
|
|
5
|
+
Runtime governance answers 'is the agent behavior safe?'
|
|
6
|
+
Content governance answers 'is the agent output accurate, well-structured,
|
|
7
|
+
and meeting quality standards?'
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ContentDimension(str, Enum):
|
|
16
|
+
"""Dimensions for content quality evaluation."""
|
|
17
|
+
ACCURACY = "accuracy"
|
|
18
|
+
COMPLETENESS = "completeness"
|
|
19
|
+
FRESHNESS = "freshness"
|
|
20
|
+
STRUCTURE = "structure"
|
|
21
|
+
RELEVANCE = "relevance"
|
|
22
|
+
CONSISTENCY = "consistency"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class QualityGate(str, Enum):
|
|
26
|
+
"""Content quality gate decisions."""
|
|
27
|
+
PASS = "pass"
|
|
28
|
+
WARN = "warn"
|
|
29
|
+
FAIL = "fail"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ContentQualityRule:
|
|
34
|
+
"""A rule for evaluating content quality."""
|
|
35
|
+
name: str
|
|
36
|
+
dimension: ContentDimension
|
|
37
|
+
threshold: float # 0.0 to 1.0
|
|
38
|
+
gate: QualityGate = QualityGate.WARN
|
|
39
|
+
description: str = ""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class ContentEvaluation:
|
|
44
|
+
"""Result of evaluating content against quality rules."""
|
|
45
|
+
dimension: ContentDimension
|
|
46
|
+
score: float
|
|
47
|
+
gate_result: QualityGate
|
|
48
|
+
rule_name: str
|
|
49
|
+
details: str = ""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class ContentQualityReport:
|
|
54
|
+
"""Aggregated quality report for agent output."""
|
|
55
|
+
agent_id: str
|
|
56
|
+
content_id: str
|
|
57
|
+
evaluations: list[ContentEvaluation] = field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def passed(self) -> bool:
|
|
61
|
+
return all(e.gate_result != QualityGate.FAIL for e in self.evaluations)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def overall_score(self) -> float:
|
|
65
|
+
if not self.evaluations:
|
|
66
|
+
return 0.0
|
|
67
|
+
return sum(e.score for e in self.evaluations) / len(self.evaluations)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def warnings(self) -> list[ContentEvaluation]:
|
|
71
|
+
return [e for e in self.evaluations if e.gate_result == QualityGate.WARN]
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def failures(self) -> list[ContentEvaluation]:
|
|
75
|
+
return [e for e in self.evaluations if e.gate_result == QualityGate.FAIL]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ContentQualityEvaluator:
|
|
79
|
+
"""Evaluates agent output against content quality rules.
|
|
80
|
+
|
|
81
|
+
Usage:
|
|
82
|
+
evaluator = ContentQualityEvaluator()
|
|
83
|
+
evaluator.add_rule(ContentQualityRule(
|
|
84
|
+
name="min-accuracy",
|
|
85
|
+
dimension=ContentDimension.ACCURACY,
|
|
86
|
+
threshold=0.8,
|
|
87
|
+
gate=QualityGate.FAIL,
|
|
88
|
+
))
|
|
89
|
+
|
|
90
|
+
report = evaluator.evaluate(
|
|
91
|
+
agent_id="agent-1",
|
|
92
|
+
content_id="response-123",
|
|
93
|
+
scores={ContentDimension.ACCURACY: 0.75},
|
|
94
|
+
)
|
|
95
|
+
assert not report.passed # Below 0.8 threshold
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(self) -> None:
|
|
99
|
+
self._rules: list[ContentQualityRule] = []
|
|
100
|
+
|
|
101
|
+
def add_rule(self, rule: ContentQualityRule) -> None:
|
|
102
|
+
self._rules.append(rule)
|
|
103
|
+
|
|
104
|
+
def load_rules(self, rules: list[dict[str, Any]]) -> None:
|
|
105
|
+
"""Load rules from a list of dicts (e.g., from YAML config)."""
|
|
106
|
+
for r in rules:
|
|
107
|
+
self._rules.append(ContentQualityRule(
|
|
108
|
+
name=r["name"],
|
|
109
|
+
dimension=ContentDimension(r["dimension"]),
|
|
110
|
+
threshold=float(r["threshold"]),
|
|
111
|
+
gate=QualityGate(r.get("gate", "warn")),
|
|
112
|
+
description=r.get("description", ""),
|
|
113
|
+
))
|
|
114
|
+
|
|
115
|
+
def evaluate(
|
|
116
|
+
self,
|
|
117
|
+
agent_id: str,
|
|
118
|
+
content_id: str,
|
|
119
|
+
scores: dict[ContentDimension, float],
|
|
120
|
+
) -> ContentQualityReport:
|
|
121
|
+
"""Evaluate content scores against configured rules."""
|
|
122
|
+
evaluations = []
|
|
123
|
+
for rule in self._rules:
|
|
124
|
+
score = scores.get(rule.dimension, 0.0)
|
|
125
|
+
if score >= rule.threshold:
|
|
126
|
+
gate_result = QualityGate.PASS
|
|
127
|
+
else:
|
|
128
|
+
gate_result = rule.gate
|
|
129
|
+
evaluations.append(ContentEvaluation(
|
|
130
|
+
dimension=rule.dimension,
|
|
131
|
+
score=score,
|
|
132
|
+
gate_result=gate_result,
|
|
133
|
+
rule_name=rule.name,
|
|
134
|
+
details=f"Score {score:.2f} vs threshold {rule.threshold:.2f}",
|
|
135
|
+
))
|
|
136
|
+
return ContentQualityReport(
|
|
137
|
+
agent_id=agent_id,
|
|
138
|
+
content_id=content_id,
|
|
139
|
+
evaluations=evaluations,
|
|
140
|
+
)
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""
|
|
4
|
+
Context Budget Scheduler — token budget as a kernel primitive.
|
|
5
|
+
|
|
6
|
+
Makes the "Scale by Subtraction" philosophy (90 % lookup, 10 % reasoning)
|
|
7
|
+
concrete and enforced. The kernel owns the budget; agents cannot exceed it.
|
|
8
|
+
|
|
9
|
+
Closes #207.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from enum import Enum, auto
|
|
17
|
+
from typing import Any, Callable
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Signals
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
class AgentSignal(Enum):
|
|
24
|
+
"""Kernel signals for context budget enforcement."""
|
|
25
|
+
|
|
26
|
+
SIGSTOP = auto() # Budget exceeded — halt the agent
|
|
27
|
+
SIGWARN = auto() # Budget nearing limit
|
|
28
|
+
SIGRESUME = auto() # Budget replenished
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Data structures
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class ContextWindow:
|
|
37
|
+
"""An allocated context window for an agent task."""
|
|
38
|
+
|
|
39
|
+
agent_id: str
|
|
40
|
+
task: str
|
|
41
|
+
lookup_budget: int # tokens for retrieval / facts
|
|
42
|
+
reasoning_budget: int # tokens for LLM reasoning
|
|
43
|
+
total: int # lookup + reasoning
|
|
44
|
+
created_at: float = field(default_factory=time.time)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def lookup_ratio(self) -> float:
|
|
48
|
+
return self.lookup_budget / self.total if self.total else 0.0
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def reasoning_ratio(self) -> float:
|
|
52
|
+
return self.reasoning_budget / self.total if self.total else 0.0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ContextPriority(Enum):
|
|
56
|
+
"""Task priority levels for context allocation."""
|
|
57
|
+
|
|
58
|
+
CRITICAL = 3 # Gets full allocation even if pool is tight
|
|
59
|
+
HIGH = 2
|
|
60
|
+
NORMAL = 1
|
|
61
|
+
LOW = 0 # Smallest possible allocation
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class UsageRecord:
|
|
66
|
+
"""Tracks actual token usage by an agent."""
|
|
67
|
+
|
|
68
|
+
agent_id: str
|
|
69
|
+
window: ContextWindow
|
|
70
|
+
lookup_used: int = 0
|
|
71
|
+
reasoning_used: int = 0
|
|
72
|
+
started_at: float = field(default_factory=time.time)
|
|
73
|
+
stopped: bool = False
|
|
74
|
+
stop_reason: str | None = None
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def total_used(self) -> int:
|
|
78
|
+
return self.lookup_used + self.reasoning_used
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def remaining(self) -> int:
|
|
82
|
+
return max(0, self.window.total - self.total_used)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def utilization(self) -> float:
|
|
86
|
+
return self.total_used / self.window.total if self.window.total else 0.0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
# Budget Exceeded Error
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
class BudgetExceeded(Exception):
|
|
94
|
+
"""Raised when an agent exceeds its context budget."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, agent_id: str, budget: int, used: int) -> None:
|
|
97
|
+
self.agent_id = agent_id
|
|
98
|
+
self.budget = budget
|
|
99
|
+
self.used = used
|
|
100
|
+
super().__init__(
|
|
101
|
+
f"Agent {agent_id} exceeded context budget: {used}/{budget} tokens"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# Context Budget Scheduler
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
# Default minimum context sizes per priority (tokens)
|
|
110
|
+
_MIN_CONTEXT: dict[ContextPriority, int] = {
|
|
111
|
+
ContextPriority.CRITICAL: 4000,
|
|
112
|
+
ContextPriority.HIGH: 2000,
|
|
113
|
+
ContextPriority.NORMAL: 1000,
|
|
114
|
+
ContextPriority.LOW: 500,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ContextScheduler:
|
|
119
|
+
"""
|
|
120
|
+
Kernel primitive that governs how context budget is allocated.
|
|
121
|
+
|
|
122
|
+
Like CPU scheduling but for token budgets. Enforces the 90/10
|
|
123
|
+
(lookup/reasoning) split and emits SIGSTOP when an agent goes
|
|
124
|
+
over budget.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
total_budget : int
|
|
129
|
+
Global token pool (shared across all active agents).
|
|
130
|
+
lookup_ratio : float
|
|
131
|
+
Fraction of each allocation devoted to lookup (default 0.90).
|
|
132
|
+
warn_threshold : float
|
|
133
|
+
Fraction of budget at which SIGWARN fires (default 0.85).
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
total_budget: int = 8000,
|
|
139
|
+
lookup_ratio: float = 0.90,
|
|
140
|
+
warn_threshold: float = 0.85,
|
|
141
|
+
) -> None:
|
|
142
|
+
if not 0.0 < lookup_ratio < 1.0:
|
|
143
|
+
raise ValueError("lookup_ratio must be between 0 and 1 exclusive")
|
|
144
|
+
if total_budget < 1:
|
|
145
|
+
raise ValueError("total_budget must be positive")
|
|
146
|
+
|
|
147
|
+
self.total_budget = total_budget
|
|
148
|
+
self.lookup_ratio = lookup_ratio
|
|
149
|
+
self.warn_threshold = warn_threshold
|
|
150
|
+
|
|
151
|
+
self._active: dict[str, UsageRecord] = {}
|
|
152
|
+
self._history: list[UsageRecord] = []
|
|
153
|
+
self._signal_handlers: dict[AgentSignal, list[Callable]] = {
|
|
154
|
+
s: [] for s in AgentSignal
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# -- Allocation -----------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
def allocate(
|
|
160
|
+
self,
|
|
161
|
+
agent_id: str,
|
|
162
|
+
task: str,
|
|
163
|
+
priority: ContextPriority = ContextPriority.NORMAL,
|
|
164
|
+
max_tokens: int | None = None,
|
|
165
|
+
) -> ContextWindow:
|
|
166
|
+
"""
|
|
167
|
+
Allocate a context window for *agent_id*.
|
|
168
|
+
|
|
169
|
+
The scheduler decides the actual size based on remaining pool
|
|
170
|
+
capacity and the task priority.
|
|
171
|
+
"""
|
|
172
|
+
available = self._available_tokens()
|
|
173
|
+
minimum = _MIN_CONTEXT[priority]
|
|
174
|
+
|
|
175
|
+
if max_tokens is not None:
|
|
176
|
+
desired = min(max_tokens, available)
|
|
177
|
+
else:
|
|
178
|
+
# Scale by priority
|
|
179
|
+
desired = min(
|
|
180
|
+
int(self.total_budget * (0.25 + 0.25 * priority.value)),
|
|
181
|
+
available,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Ensure at least the minimum (or whatever is left),
|
|
185
|
+
# but never exceed an explicit max_tokens cap.
|
|
186
|
+
if max_tokens is not None:
|
|
187
|
+
allocated = min(desired, max_tokens)
|
|
188
|
+
else:
|
|
189
|
+
allocated = max(minimum, desired) if available >= minimum else available
|
|
190
|
+
|
|
191
|
+
lookup = int(allocated * self.lookup_ratio)
|
|
192
|
+
reasoning = allocated - lookup
|
|
193
|
+
|
|
194
|
+
window = ContextWindow(
|
|
195
|
+
agent_id=agent_id,
|
|
196
|
+
task=task,
|
|
197
|
+
lookup_budget=lookup,
|
|
198
|
+
reasoning_budget=reasoning,
|
|
199
|
+
total=allocated,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
self._active[agent_id] = UsageRecord(agent_id=agent_id, window=window)
|
|
203
|
+
return window
|
|
204
|
+
|
|
205
|
+
# -- Usage tracking -------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
def record_usage(
|
|
208
|
+
self,
|
|
209
|
+
agent_id: str,
|
|
210
|
+
lookup_tokens: int = 0,
|
|
211
|
+
reasoning_tokens: int = 0,
|
|
212
|
+
) -> UsageRecord:
|
|
213
|
+
"""
|
|
214
|
+
Record token usage for an active allocation.
|
|
215
|
+
|
|
216
|
+
Emits SIGWARN or SIGSTOP as thresholds are crossed.
|
|
217
|
+
"""
|
|
218
|
+
rec = self._active.get(agent_id)
|
|
219
|
+
if rec is None:
|
|
220
|
+
raise KeyError(f"No active allocation for agent {agent_id}")
|
|
221
|
+
if rec.stopped:
|
|
222
|
+
raise BudgetExceeded(agent_id, rec.window.total, rec.total_used)
|
|
223
|
+
|
|
224
|
+
rec.lookup_used += lookup_tokens
|
|
225
|
+
rec.reasoning_used += reasoning_tokens
|
|
226
|
+
|
|
227
|
+
# Check thresholds
|
|
228
|
+
utilization = rec.utilization
|
|
229
|
+
if utilization >= 1.0:
|
|
230
|
+
rec.stopped = True
|
|
231
|
+
rec.stop_reason = "budget_exceeded"
|
|
232
|
+
self._emit(AgentSignal.SIGSTOP, agent_id)
|
|
233
|
+
raise BudgetExceeded(agent_id, rec.window.total, rec.total_used)
|
|
234
|
+
elif utilization >= self.warn_threshold:
|
|
235
|
+
self._emit(AgentSignal.SIGWARN, agent_id)
|
|
236
|
+
|
|
237
|
+
return rec
|
|
238
|
+
|
|
239
|
+
def release(self, agent_id: str) -> UsageRecord | None:
|
|
240
|
+
"""Release an allocation and move it to history."""
|
|
241
|
+
rec = self._active.pop(agent_id, None)
|
|
242
|
+
if rec is not None:
|
|
243
|
+
self._history.append(rec)
|
|
244
|
+
return rec
|
|
245
|
+
|
|
246
|
+
# -- Queries --------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
def get_usage(self, agent_id: str) -> UsageRecord | None:
|
|
249
|
+
return self._active.get(agent_id)
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def active_agents(self) -> list[str]:
|
|
253
|
+
return list(self._active.keys())
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def active_count(self) -> int:
|
|
257
|
+
return len(self._active)
|
|
258
|
+
|
|
259
|
+
def _available_tokens(self) -> int:
|
|
260
|
+
used = sum(r.window.total for r in self._active.values())
|
|
261
|
+
return max(0, self.total_budget - used)
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def available_tokens(self) -> int:
|
|
265
|
+
return self._available_tokens()
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def utilization(self) -> float:
|
|
269
|
+
"""Global pool utilization (0.0 – 1.0)."""
|
|
270
|
+
allocated = sum(r.window.total for r in self._active.values())
|
|
271
|
+
return allocated / self.total_budget if self.total_budget else 0.0
|
|
272
|
+
|
|
273
|
+
def get_health_report(self) -> dict[str, Any]:
|
|
274
|
+
"""Return a summary of scheduler state."""
|
|
275
|
+
return {
|
|
276
|
+
"total_budget": self.total_budget,
|
|
277
|
+
"available": self._available_tokens(),
|
|
278
|
+
"utilization": round(self.utilization, 3),
|
|
279
|
+
"active_agents": self.active_count,
|
|
280
|
+
"lookup_ratio": self.lookup_ratio,
|
|
281
|
+
"agents": {
|
|
282
|
+
aid: {
|
|
283
|
+
"task": r.window.task,
|
|
284
|
+
"allocated": r.window.total,
|
|
285
|
+
"used": r.total_used,
|
|
286
|
+
"remaining": r.remaining,
|
|
287
|
+
"stopped": r.stopped,
|
|
288
|
+
}
|
|
289
|
+
for aid, r in self._active.items()
|
|
290
|
+
},
|
|
291
|
+
"history_count": len(self._history),
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
# -- Signal system --------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
def on_signal(self, signal: AgentSignal, handler: Callable) -> None:
|
|
297
|
+
"""Register a handler for *signal*."""
|
|
298
|
+
self._signal_handlers[signal].append(handler)
|
|
299
|
+
|
|
300
|
+
def _emit(self, signal: AgentSignal, agent_id: str) -> None:
|
|
301
|
+
for handler in self._signal_handlers[signal]:
|
|
302
|
+
try:
|
|
303
|
+
handler(agent_id, signal)
|
|
304
|
+
except Exception:
|
|
305
|
+
pass
|