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,410 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""In-memory broker adapter for testing and simple use cases."""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import heapq
|
|
7
|
+
import uuid
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
10
|
+
|
|
11
|
+
from amb_core.broker import BrokerAdapter, MessageHandler
|
|
12
|
+
from amb_core.models import Message, MessagePriority
|
|
13
|
+
|
|
14
|
+
# Priority mapping for heapq (lower number = higher priority)
|
|
15
|
+
PRIORITY_ORDER = {
|
|
16
|
+
MessagePriority.CRITICAL: 0,
|
|
17
|
+
MessagePriority.URGENT: 1,
|
|
18
|
+
MessagePriority.HIGH: 2,
|
|
19
|
+
MessagePriority.NORMAL: 3,
|
|
20
|
+
MessagePriority.LOW: 4,
|
|
21
|
+
MessagePriority.BACKGROUND: 5,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InMemoryBroker(BrokerAdapter):
|
|
26
|
+
"""
|
|
27
|
+
In-memory broker implementation for testing and simple use cases.
|
|
28
|
+
|
|
29
|
+
This broker stores messages in memory and uses anyio for async handling.
|
|
30
|
+
It's suitable for testing, development, and single-process applications.
|
|
31
|
+
|
|
32
|
+
Features:
|
|
33
|
+
- Backpressure: Automatically slows down producers when consumers are overwhelmed
|
|
34
|
+
- Priority lanes: CRITICAL messages jump ahead of BACKGROUND messages
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
max_queue_size: int = 1000,
|
|
40
|
+
backpressure_threshold: float = 0.8,
|
|
41
|
+
backpressure_delay: float = 0.01,
|
|
42
|
+
use_priority_delivery: bool = True
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
Initialize the in-memory broker.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
max_queue_size: Maximum messages per topic before backpressure kicks in
|
|
49
|
+
backpressure_threshold: Queue fill percentage (0.0-1.0) that triggers backpressure
|
|
50
|
+
backpressure_delay: Delay in seconds when backpressure is active
|
|
51
|
+
use_priority_delivery: If True, deliver messages in priority order (slower but respects priority lanes)
|
|
52
|
+
"""
|
|
53
|
+
self._connected = False
|
|
54
|
+
self._subscriptions: Dict[str, Dict[str, MessageHandler]] = defaultdict(dict)
|
|
55
|
+
|
|
56
|
+
# Priority queue: List of tuples (priority_value, counter, message)
|
|
57
|
+
self._message_queues: Dict[str, List[Tuple[int, int, Message]]] = defaultdict(list)
|
|
58
|
+
self._message_counter = 0 # For stable sorting within same priority
|
|
59
|
+
|
|
60
|
+
self._response_queues: Dict[str, asyncio.Queue] = {}
|
|
61
|
+
self._request_message_ids: Set[str] = set() # Track request message IDs to avoid self-capture
|
|
62
|
+
self._tasks: Set[asyncio.Task] = set()
|
|
63
|
+
|
|
64
|
+
# Backpressure configuration
|
|
65
|
+
self._max_queue_size = max_queue_size
|
|
66
|
+
self._backpressure_threshold = backpressure_threshold
|
|
67
|
+
self._backpressure_delay = backpressure_delay
|
|
68
|
+
self._use_priority_delivery = use_priority_delivery
|
|
69
|
+
|
|
70
|
+
# Statistics for monitoring
|
|
71
|
+
self._backpressure_events: Dict[str, int] = defaultdict(int)
|
|
72
|
+
|
|
73
|
+
# Background processing tasks for priority delivery
|
|
74
|
+
self._delivery_tasks: Dict[str, asyncio.Task] = {}
|
|
75
|
+
|
|
76
|
+
async def connect(self) -> None:
|
|
77
|
+
"""Establish connection (no-op for in-memory broker)."""
|
|
78
|
+
self._connected = True
|
|
79
|
+
|
|
80
|
+
async def disconnect(self) -> None:
|
|
81
|
+
"""Close connection and cancel all tasks."""
|
|
82
|
+
self._connected = False
|
|
83
|
+
|
|
84
|
+
# Cancel all delivery tasks
|
|
85
|
+
for task in self._delivery_tasks.values():
|
|
86
|
+
if not task.done():
|
|
87
|
+
task.cancel()
|
|
88
|
+
|
|
89
|
+
if self._delivery_tasks:
|
|
90
|
+
await asyncio.gather(*self._delivery_tasks.values(), return_exceptions=True)
|
|
91
|
+
|
|
92
|
+
# Cancel all running tasks
|
|
93
|
+
for task in self._tasks:
|
|
94
|
+
if not task.done():
|
|
95
|
+
task.cancel()
|
|
96
|
+
|
|
97
|
+
if self._tasks:
|
|
98
|
+
await asyncio.gather(*self._tasks, return_exceptions=True)
|
|
99
|
+
|
|
100
|
+
self._tasks.clear()
|
|
101
|
+
self._delivery_tasks.clear()
|
|
102
|
+
self._subscriptions.clear()
|
|
103
|
+
self._message_queues.clear()
|
|
104
|
+
self._response_queues.clear()
|
|
105
|
+
self._request_message_ids.clear()
|
|
106
|
+
self._backpressure_events.clear()
|
|
107
|
+
|
|
108
|
+
def _check_backpressure(self, topic: str) -> bool:
|
|
109
|
+
"""
|
|
110
|
+
Check if backpressure should be applied for a topic.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
topic: The topic to check
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
True if backpressure should be applied
|
|
117
|
+
"""
|
|
118
|
+
queue_size = len(self._message_queues[topic])
|
|
119
|
+
threshold = int(self._max_queue_size * self._backpressure_threshold)
|
|
120
|
+
return queue_size >= threshold
|
|
121
|
+
|
|
122
|
+
async def _apply_backpressure(self, topic: str) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Apply backpressure delay to slow down the producer.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
topic: The topic experiencing backpressure
|
|
128
|
+
"""
|
|
129
|
+
self._backpressure_events[topic] += 1
|
|
130
|
+
await asyncio.sleep(self._backpressure_delay)
|
|
131
|
+
|
|
132
|
+
async def _priority_delivery_worker(self, topic: str) -> None:
|
|
133
|
+
"""
|
|
134
|
+
Background worker that delivers messages in priority order.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
topic: The topic to process
|
|
138
|
+
"""
|
|
139
|
+
try:
|
|
140
|
+
while self._connected:
|
|
141
|
+
# Check if there are messages to deliver
|
|
142
|
+
if not self._message_queues[topic]:
|
|
143
|
+
await asyncio.sleep(0.001) # Small delay to avoid busy waiting
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
# Get highest priority message
|
|
147
|
+
_, _, message = heapq.heappop(self._message_queues[topic])
|
|
148
|
+
|
|
149
|
+
# Check if this is a response message
|
|
150
|
+
is_response = (
|
|
151
|
+
message.correlation_id
|
|
152
|
+
and message.correlation_id in self._response_queues
|
|
153
|
+
and message.id not in self._request_message_ids
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Deliver to handlers (unless it's a response message)
|
|
157
|
+
if not is_response:
|
|
158
|
+
handlers = self._subscriptions.get(topic, {})
|
|
159
|
+
for handler in handlers.values():
|
|
160
|
+
task = asyncio.create_task(handler(message))
|
|
161
|
+
self._tasks.add(task)
|
|
162
|
+
task.add_done_callback(self._tasks.discard)
|
|
163
|
+
|
|
164
|
+
# Handle response messages
|
|
165
|
+
if is_response:
|
|
166
|
+
await self._response_queues[message.correlation_id].put(message)
|
|
167
|
+
|
|
168
|
+
except asyncio.CancelledError:
|
|
169
|
+
# Worker cancelled during shutdown
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
async def publish(self, message: Message, wait_for_confirmation: bool = False) -> Optional[str]:
|
|
173
|
+
"""
|
|
174
|
+
Publish a message to all subscribers of the topic.
|
|
175
|
+
|
|
176
|
+
Implements backpressure: If the queue is near capacity, the publisher
|
|
177
|
+
is automatically slowed down to prevent overwhelming consumers.
|
|
178
|
+
|
|
179
|
+
Implements priority lanes: CRITICAL messages are processed before
|
|
180
|
+
BACKGROUND messages, allowing urgent tasks to jump the queue.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
message: The message to publish
|
|
184
|
+
wait_for_confirmation: If True, wait for handlers to process
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Message ID
|
|
188
|
+
"""
|
|
189
|
+
if not self._connected:
|
|
190
|
+
raise ConnectionError("Broker not connected")
|
|
191
|
+
|
|
192
|
+
topic = message.topic
|
|
193
|
+
|
|
194
|
+
# Apply backpressure if queue is getting full (reactive streams flow control)
|
|
195
|
+
if self._check_backpressure(topic):
|
|
196
|
+
await self._apply_backpressure(topic)
|
|
197
|
+
|
|
198
|
+
# Check if queue is at max capacity
|
|
199
|
+
if len(self._message_queues[topic]) >= self._max_queue_size:
|
|
200
|
+
# Drop oldest BACKGROUND message, or raise error if no BACKGROUND messages
|
|
201
|
+
dropped = self._drop_background_message(topic)
|
|
202
|
+
if not dropped:
|
|
203
|
+
raise RuntimeError(
|
|
204
|
+
f"Queue for topic '{topic}' is full ({self._max_queue_size} messages). "
|
|
205
|
+
"Producer is overwhelmed. Consider increasing queue size or adding more consumers."
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Add message to priority queue
|
|
209
|
+
priority_value = PRIORITY_ORDER.get(message.priority, 3)
|
|
210
|
+
self._message_counter += 1
|
|
211
|
+
heapq.heappush(
|
|
212
|
+
self._message_queues[topic],
|
|
213
|
+
(priority_value, self._message_counter, message)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Start priority delivery worker if not already running
|
|
217
|
+
if self._use_priority_delivery and topic not in self._delivery_tasks:
|
|
218
|
+
task = asyncio.create_task(self._priority_delivery_worker(topic))
|
|
219
|
+
self._delivery_tasks[topic] = task
|
|
220
|
+
|
|
221
|
+
# Check if this is a response message for request-response pattern
|
|
222
|
+
is_response = (
|
|
223
|
+
message.correlation_id
|
|
224
|
+
and message.correlation_id in self._response_queues
|
|
225
|
+
and message.id not in self._request_message_ids
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# For non-priority mode or when waiting for confirmation, deliver immediately
|
|
229
|
+
if not self._use_priority_delivery:
|
|
230
|
+
# Deliver to subscribers (skip if this is a response message)
|
|
231
|
+
if not is_response:
|
|
232
|
+
handlers = self._subscriptions.get(topic, {})
|
|
233
|
+
|
|
234
|
+
if wait_for_confirmation:
|
|
235
|
+
# Wait for all handlers to complete
|
|
236
|
+
tasks = []
|
|
237
|
+
for handler in handlers.values():
|
|
238
|
+
tasks.append(handler(message))
|
|
239
|
+
|
|
240
|
+
if tasks:
|
|
241
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
242
|
+
else:
|
|
243
|
+
# Fire and forget - schedule handlers without waiting
|
|
244
|
+
for handler in handlers.values():
|
|
245
|
+
task = asyncio.create_task(handler(message))
|
|
246
|
+
self._tasks.add(task)
|
|
247
|
+
task.add_done_callback(self._tasks.discard)
|
|
248
|
+
|
|
249
|
+
# Handle request-response pattern
|
|
250
|
+
# Capture response messages in the response queue
|
|
251
|
+
if is_response:
|
|
252
|
+
await self._response_queues[message.correlation_id].put(message)
|
|
253
|
+
|
|
254
|
+
return message.id
|
|
255
|
+
|
|
256
|
+
def _drop_background_message(self, topic: str) -> bool:
|
|
257
|
+
"""
|
|
258
|
+
Drop the oldest BACKGROUND priority message from the queue.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
topic: The topic to drop from
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
True if a message was dropped, False if no BACKGROUND messages found
|
|
265
|
+
"""
|
|
266
|
+
queue = self._message_queues[topic]
|
|
267
|
+
background_priority = PRIORITY_ORDER[MessagePriority.BACKGROUND]
|
|
268
|
+
|
|
269
|
+
# Find index of oldest BACKGROUND message (largest counter value for background priority)
|
|
270
|
+
background_idx = None
|
|
271
|
+
max_counter = -1
|
|
272
|
+
|
|
273
|
+
for i, (priority, counter, _msg) in enumerate(queue):
|
|
274
|
+
if priority == background_priority and counter > max_counter:
|
|
275
|
+
background_idx = i
|
|
276
|
+
max_counter = counter
|
|
277
|
+
|
|
278
|
+
if background_idx is not None:
|
|
279
|
+
# Remove the item and re-heapify
|
|
280
|
+
queue[background_idx] = queue[-1]
|
|
281
|
+
queue.pop()
|
|
282
|
+
if background_idx < len(queue):
|
|
283
|
+
heapq.heapify(queue)
|
|
284
|
+
return True
|
|
285
|
+
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
async def subscribe(self, topic: str, handler: MessageHandler) -> str:
|
|
289
|
+
"""
|
|
290
|
+
Subscribe to a topic.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
topic: Topic to subscribe to
|
|
294
|
+
handler: Message handler function
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Subscription ID
|
|
298
|
+
"""
|
|
299
|
+
if not self._connected:
|
|
300
|
+
raise ConnectionError("Broker not connected")
|
|
301
|
+
|
|
302
|
+
subscription_id = str(uuid.uuid4())
|
|
303
|
+
self._subscriptions[topic][subscription_id] = handler
|
|
304
|
+
return subscription_id
|
|
305
|
+
|
|
306
|
+
async def unsubscribe(self, subscription_id: str) -> None:
|
|
307
|
+
"""
|
|
308
|
+
Unsubscribe from a topic.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
subscription_id: The subscription ID
|
|
312
|
+
"""
|
|
313
|
+
for topic_handlers in self._subscriptions.values():
|
|
314
|
+
if subscription_id in topic_handlers:
|
|
315
|
+
del topic_handlers[subscription_id]
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
async def request(self, message: Message, timeout: float = 30.0) -> Message:
|
|
319
|
+
"""
|
|
320
|
+
Send a request and wait for response.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
message: Request message
|
|
324
|
+
timeout: Timeout in seconds
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Response message
|
|
328
|
+
|
|
329
|
+
Raises:
|
|
330
|
+
TimeoutError: If timeout exceeded
|
|
331
|
+
"""
|
|
332
|
+
if not self._connected:
|
|
333
|
+
raise ConnectionError("Broker not connected")
|
|
334
|
+
|
|
335
|
+
# Generate correlation ID if not present
|
|
336
|
+
if not message.correlation_id:
|
|
337
|
+
message.correlation_id = str(uuid.uuid4())
|
|
338
|
+
|
|
339
|
+
# Set up reply queue
|
|
340
|
+
reply_queue: asyncio.Queue = asyncio.Queue()
|
|
341
|
+
self._response_queues[message.correlation_id] = reply_queue
|
|
342
|
+
|
|
343
|
+
# Mark this message ID as a request to avoid self-capture
|
|
344
|
+
self._request_message_ids.add(message.id)
|
|
345
|
+
|
|
346
|
+
try:
|
|
347
|
+
# Publish the request
|
|
348
|
+
await self.publish(message, wait_for_confirmation=False)
|
|
349
|
+
|
|
350
|
+
# Wait for response
|
|
351
|
+
try:
|
|
352
|
+
response = await asyncio.wait_for(reply_queue.get(), timeout=timeout)
|
|
353
|
+
return response
|
|
354
|
+
except asyncio.TimeoutError:
|
|
355
|
+
raise TimeoutError(f"No response received within {timeout} seconds")
|
|
356
|
+
|
|
357
|
+
finally:
|
|
358
|
+
# Clean up
|
|
359
|
+
if message.correlation_id in self._response_queues:
|
|
360
|
+
del self._response_queues[message.correlation_id]
|
|
361
|
+
if message.id in self._request_message_ids:
|
|
362
|
+
self._request_message_ids.discard(message.id)
|
|
363
|
+
|
|
364
|
+
async def get_pending_messages(self, topic: str, limit: int = 10) -> List[Message]:
|
|
365
|
+
"""
|
|
366
|
+
Get pending messages from a topic (in priority order).
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
topic: Topic to get messages from
|
|
370
|
+
limit: Maximum number of messages
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
List of messages (highest priority first)
|
|
374
|
+
"""
|
|
375
|
+
if not self._connected:
|
|
376
|
+
raise ConnectionError("Broker not connected")
|
|
377
|
+
|
|
378
|
+
queue = self._message_queues.get(topic, [])
|
|
379
|
+
|
|
380
|
+
# Sort by priority and return top N messages
|
|
381
|
+
sorted_messages = sorted(queue, key=lambda x: (x[0], x[1]))
|
|
382
|
+
messages = [item[2] for item in sorted_messages[:limit]]
|
|
383
|
+
|
|
384
|
+
return messages
|
|
385
|
+
|
|
386
|
+
def get_backpressure_stats(self, topic: Optional[str] = None) -> Dict[str, int]:
|
|
387
|
+
"""
|
|
388
|
+
Get backpressure statistics.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
topic: Optional topic to get stats for. If None, returns all topics.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Dictionary mapping topics to backpressure event counts
|
|
395
|
+
"""
|
|
396
|
+
if topic:
|
|
397
|
+
return {topic: self._backpressure_events.get(topic, 0)}
|
|
398
|
+
return dict(self._backpressure_events)
|
|
399
|
+
|
|
400
|
+
def get_queue_size(self, topic: str) -> int:
|
|
401
|
+
"""
|
|
402
|
+
Get current queue size for a topic.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
topic: The topic to check
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
Number of messages in the queue
|
|
409
|
+
"""
|
|
410
|
+
return len(self._message_queues.get(topic, []))
|
amb_core/models.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Core message models for AMB."""
|
|
4
|
+
|
|
5
|
+
from enum import Enum, IntEnum
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pydantic import BaseModel, Field, ConfigDict, field_validator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MessagePriority(IntEnum):
|
|
12
|
+
"""
|
|
13
|
+
Message priority levels.
|
|
14
|
+
|
|
15
|
+
Higher values indicate higher priority. Messages with higher priority
|
|
16
|
+
are processed before lower priority messages when queued.
|
|
17
|
+
"""
|
|
18
|
+
BACKGROUND = 0 # Lowest priority for background tasks
|
|
19
|
+
LOW = 1
|
|
20
|
+
NORMAL = 5
|
|
21
|
+
HIGH = 8
|
|
22
|
+
URGENT = 10
|
|
23
|
+
CRITICAL = 15 # For system-critical messages like fraud alerts
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Priority:
|
|
27
|
+
"""
|
|
28
|
+
Convenience class for accessing priority levels.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
message = Message(payload=data, priority=Priority.HIGH)
|
|
32
|
+
"""
|
|
33
|
+
BACKGROUND = MessagePriority.BACKGROUND
|
|
34
|
+
LOW = MessagePriority.LOW
|
|
35
|
+
NORMAL = MessagePriority.NORMAL
|
|
36
|
+
HIGH = MessagePriority.HIGH
|
|
37
|
+
URGENT = MessagePriority.URGENT
|
|
38
|
+
CRITICAL = MessagePriority.CRITICAL
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class MessageStatus(str, Enum):
|
|
42
|
+
"""Status of a message in its lifecycle."""
|
|
43
|
+
PENDING = "pending"
|
|
44
|
+
DELIVERED = "delivered"
|
|
45
|
+
ACKNOWLEDGED = "acknowledged"
|
|
46
|
+
FAILED = "failed"
|
|
47
|
+
EXPIRED = "expired"
|
|
48
|
+
DLQ = "dlq" # Moved to dead letter queue
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Message(BaseModel):
|
|
52
|
+
"""
|
|
53
|
+
Core message model for the Agent Message Bus.
|
|
54
|
+
|
|
55
|
+
This model represents a message that can be sent through the bus.
|
|
56
|
+
It includes metadata for routing, tracking, handling, and distributed tracing.
|
|
57
|
+
|
|
58
|
+
New in v0.2.0:
|
|
59
|
+
- trace_id: For distributed tracing across agents
|
|
60
|
+
- ttl_seconds: Alias for ttl for clearer API
|
|
61
|
+
- is_expired: Property to check if message has expired
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
id: str = Field(..., description="Unique message identifier")
|
|
65
|
+
topic: str = Field(..., description="Message topic/channel")
|
|
66
|
+
payload: Dict[str, Any] = Field(default_factory=dict, description="Message payload")
|
|
67
|
+
priority: MessagePriority = Field(default=MessagePriority.NORMAL, description="Message priority")
|
|
68
|
+
|
|
69
|
+
# Metadata
|
|
70
|
+
sender: Optional[str] = Field(None, description="Sender identifier")
|
|
71
|
+
correlation_id: Optional[str] = Field(None, description="Correlation ID for request-response patterns")
|
|
72
|
+
reply_to: Optional[str] = Field(None, description="Topic to reply to")
|
|
73
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), description="Message timestamp")
|
|
74
|
+
|
|
75
|
+
# TTL and expiration
|
|
76
|
+
ttl: Optional[int] = Field(None, description="Time to live in seconds", alias="ttl_seconds")
|
|
77
|
+
|
|
78
|
+
# Distributed tracing (AMB-004)
|
|
79
|
+
trace_id: Optional[str] = Field(None, description="Distributed trace ID for tracking message flow")
|
|
80
|
+
span_id: Optional[str] = Field(None, description="Span ID within the trace")
|
|
81
|
+
parent_span_id: Optional[str] = Field(None, description="Parent span ID for nested operations")
|
|
82
|
+
|
|
83
|
+
# Additional metadata
|
|
84
|
+
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
|
|
85
|
+
|
|
86
|
+
model_config = ConfigDict(
|
|
87
|
+
json_encoders={
|
|
88
|
+
datetime: lambda v: v.isoformat()
|
|
89
|
+
},
|
|
90
|
+
populate_by_name=True, # Allow both 'ttl' and 'ttl_seconds'
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@field_validator('priority', mode='before')
|
|
94
|
+
@classmethod
|
|
95
|
+
def validate_priority(cls, v):
|
|
96
|
+
"""Accept both int and MessagePriority."""
|
|
97
|
+
if isinstance(v, int):
|
|
98
|
+
return MessagePriority(v)
|
|
99
|
+
if isinstance(v, str):
|
|
100
|
+
return MessagePriority[v.upper()]
|
|
101
|
+
return v
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def ttl_seconds(self) -> Optional[int]:
|
|
105
|
+
"""Alias for ttl for clearer API."""
|
|
106
|
+
return self.ttl
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def is_expired(self) -> bool:
|
|
110
|
+
"""
|
|
111
|
+
Check if the message has expired based on TTL.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
True if the message has exceeded its TTL, False otherwise
|
|
115
|
+
"""
|
|
116
|
+
if self.ttl is None:
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
now = datetime.now(timezone.utc)
|
|
120
|
+
age_seconds = (now - self.timestamp).total_seconds()
|
|
121
|
+
return age_seconds > self.ttl
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def age_seconds(self) -> float:
|
|
125
|
+
"""Get the age of the message in seconds."""
|
|
126
|
+
now = datetime.now(timezone.utc)
|
|
127
|
+
return (now - self.timestamp).total_seconds()
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def remaining_ttl(self) -> Optional[float]:
|
|
131
|
+
"""
|
|
132
|
+
Get remaining TTL in seconds.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Remaining TTL, 0 if expired, None if no TTL set
|
|
136
|
+
"""
|
|
137
|
+
if self.ttl is None:
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
remaining = self.ttl - self.age_seconds
|
|
141
|
+
return max(0, remaining)
|