agent_hypervisor 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.
Files changed (60) hide show
  1. agent_hypervisor-3.1.0.dist-info/METADATA +824 -0
  2. agent_hypervisor-3.1.0.dist-info/RECORD +60 -0
  3. agent_hypervisor-3.1.0.dist-info/WHEEL +4 -0
  4. agent_hypervisor-3.1.0.dist-info/entry_points.txt +2 -0
  5. agent_hypervisor-3.1.0.dist-info/licenses/LICENSE +21 -0
  6. hypervisor/__init__.py +160 -0
  7. hypervisor/api/__init__.py +7 -0
  8. hypervisor/api/models.py +285 -0
  9. hypervisor/api/server.py +742 -0
  10. hypervisor/audit/__init__.py +4 -0
  11. hypervisor/audit/commitment.py +76 -0
  12. hypervisor/audit/delta.py +135 -0
  13. hypervisor/audit/gc.py +99 -0
  14. hypervisor/cli/__init__.py +3 -0
  15. hypervisor/cli/formatters.py +99 -0
  16. hypervisor/cli/session_commands.py +200 -0
  17. hypervisor/constants.py +106 -0
  18. hypervisor/core.py +352 -0
  19. hypervisor/integrations/__init__.py +10 -0
  20. hypervisor/integrations/iatp_adapter.py +142 -0
  21. hypervisor/integrations/nexus_adapter.py +108 -0
  22. hypervisor/integrations/verification_adapter.py +122 -0
  23. hypervisor/liability/__init__.py +142 -0
  24. hypervisor/liability/attribution.py +86 -0
  25. hypervisor/liability/ledger.py +121 -0
  26. hypervisor/liability/quarantine.py +119 -0
  27. hypervisor/liability/slashing.py +80 -0
  28. hypervisor/liability/vouching.py +134 -0
  29. hypervisor/models.py +277 -0
  30. hypervisor/observability/__init__.py +27 -0
  31. hypervisor/observability/causal_trace.py +70 -0
  32. hypervisor/observability/event_bus.py +222 -0
  33. hypervisor/observability/prometheus_collector.py +248 -0
  34. hypervisor/observability/saga_span_exporter.py +341 -0
  35. hypervisor/providers.py +121 -0
  36. hypervisor/py.typed +0 -0
  37. hypervisor/reversibility/__init__.py +3 -0
  38. hypervisor/reversibility/registry.py +108 -0
  39. hypervisor/rings/__init__.py +21 -0
  40. hypervisor/rings/breach_detector.py +200 -0
  41. hypervisor/rings/classifier.py +78 -0
  42. hypervisor/rings/elevation.py +219 -0
  43. hypervisor/rings/enforcer.py +97 -0
  44. hypervisor/saga/__init__.py +22 -0
  45. hypervisor/saga/checkpoint.py +110 -0
  46. hypervisor/saga/dsl.py +190 -0
  47. hypervisor/saga/fan_out.py +126 -0
  48. hypervisor/saga/orchestrator.py +229 -0
  49. hypervisor/saga/schema.py +244 -0
  50. hypervisor/saga/state_machine.py +157 -0
  51. hypervisor/security/__init__.py +13 -0
  52. hypervisor/security/kill_switch.py +200 -0
  53. hypervisor/security/rate_limiter.py +190 -0
  54. hypervisor/session/__init__.py +194 -0
  55. hypervisor/session/intent_locks.py +118 -0
  56. hypervisor/session/isolation.py +37 -0
  57. hypervisor/session/sso.py +169 -0
  58. hypervisor/session/vector_clock.py +118 -0
  59. hypervisor/verification/__init__.py +3 -0
  60. hypervisor/verification/history.py +173 -0
@@ -0,0 +1,80 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ # Public Preview — basic implementation
4
+ """
5
+ Collateral Penalty Engine — stub implementation.
6
+
7
+ Public Preview: penalty is not enforced. Penalty calls are logged only.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import uuid
13
+ from dataclasses import dataclass, field
14
+ from datetime import UTC, datetime
15
+
16
+
17
+ @dataclass
18
+ class SlashResult:
19
+ """Result of a penalty operation."""
20
+
21
+ slash_id: str
22
+ vouchee_did: str
23
+ vouchee_sigma_before: float
24
+ vouchee_sigma_after: float
25
+ voucher_clips: list[VoucherClip]
26
+ reason: str
27
+ session_id: str
28
+ timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
29
+ cascade_depth: int = 0
30
+
31
+
32
+ @dataclass
33
+ class VoucherClip:
34
+ """A collateral clip applied to a sponsor."""
35
+
36
+ voucher_did: str
37
+ sigma_before: float
38
+ sigma_after: float
39
+ risk_weight: float
40
+ vouch_id: str
41
+
42
+
43
+ class SlashingEngine:
44
+ """
45
+ Penalty stub (Public Preview: logs penalty events, no penalties applied).
46
+ """
47
+
48
+ MAX_CASCADE_DEPTH = 2
49
+ SIGMA_FLOOR = 0.05
50
+
51
+ def __init__(self, vouching_engine: object) -> None:
52
+ self._slash_history: list[SlashResult] = []
53
+
54
+ def slash(
55
+ self,
56
+ vouchee_did: str,
57
+ session_id: str,
58
+ vouchee_sigma: float,
59
+ risk_weight: float,
60
+ reason: str,
61
+ agent_scores: dict[str, float],
62
+ cascade_depth: int = 0,
63
+ ) -> SlashResult:
64
+ """Log a penalty event (Public Preview: no penalties applied)."""
65
+ result = SlashResult(
66
+ slash_id=f"penalize:{uuid.uuid4()}",
67
+ vouchee_did=vouchee_did,
68
+ vouchee_sigma_before=vouchee_sigma,
69
+ vouchee_sigma_after=vouchee_sigma,
70
+ voucher_clips=[],
71
+ reason=reason,
72
+ session_id=session_id,
73
+ cascade_depth=0,
74
+ )
75
+ self._slash_history.append(result)
76
+ return result
77
+
78
+ @property
79
+ def history(self) -> list[SlashResult]:
80
+ return list(self._slash_history)
@@ -0,0 +1,134 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ # Public Preview — basic implementation
4
+ """
5
+ Sponsorship Protocol — stub implementation.
6
+
7
+ Public Preview: sponsorship is not enforced. All requests are approved.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import uuid
13
+ from dataclasses import dataclass, field
14
+ from datetime import UTC, datetime
15
+
16
+ from hypervisor.constants import (
17
+ VOUCHING_DEFAULT_BOND_PCT,
18
+ VOUCHING_DEFAULT_MAX_EXPOSURE,
19
+ VOUCHING_MIN_VOUCHER_SCORE,
20
+ VOUCHING_SCORE_SCALE,
21
+ )
22
+
23
+
24
+ @dataclass
25
+ class VouchRecord:
26
+ """A record of one agent sponsorship for another within a session."""
27
+
28
+ vouch_id: str
29
+ voucher_did: str
30
+ vouchee_did: str
31
+ session_id: str
32
+ bonded_sigma_pct: float
33
+ bonded_amount: float
34
+ created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
35
+ expiry: datetime | None = None
36
+ is_active: bool = True
37
+ released_at: datetime | None = None
38
+
39
+ @property
40
+ def is_expired(self) -> bool:
41
+ if self.expiry is None:
42
+ return False
43
+ return datetime.now(UTC) > self.expiry
44
+
45
+
46
+ class VouchingEngine:
47
+ """
48
+ Sponsorship stub (Public Preview: approves all, no bonding).
49
+ """
50
+
51
+ SCORE_SCALE = VOUCHING_SCORE_SCALE
52
+ MIN_VOUCHER_SCORE = VOUCHING_MIN_VOUCHER_SCORE
53
+ DEFAULT_BOND_PCT = VOUCHING_DEFAULT_BOND_PCT
54
+ DEFAULT_MAX_EXPOSURE = VOUCHING_DEFAULT_MAX_EXPOSURE
55
+
56
+ def __init__(self, max_exposure: float | None = None) -> None:
57
+ self._vouches: dict[str, VouchRecord] = {}
58
+ self.max_exposure = max_exposure or self.DEFAULT_MAX_EXPOSURE
59
+
60
+ def vouch(
61
+ self,
62
+ voucher_did: str,
63
+ vouchee_did: str,
64
+ session_id: str,
65
+ voucher_sigma: float,
66
+ bond_pct: float | None = None,
67
+ expiry: datetime | None = None,
68
+ ) -> VouchRecord:
69
+ """Create a sponsorship record (Public Preview: always succeeds, no bonding)."""
70
+ record = VouchRecord(
71
+ vouch_id=f"sponsor:{uuid.uuid4()}",
72
+ voucher_did=voucher_did,
73
+ vouchee_did=vouchee_did,
74
+ session_id=session_id,
75
+ bonded_sigma_pct=0.0,
76
+ bonded_amount=0.0,
77
+ )
78
+ self._vouches[record.vouch_id] = record
79
+ return record
80
+
81
+ def compute_eff_score(
82
+ self,
83
+ vouchee_did: str,
84
+ session_id: str,
85
+ vouchee_sigma: float,
86
+ risk_weight: float,
87
+ ) -> float:
88
+ """Return sponsored agent's own score (Public Preview: no sponsor boost)."""
89
+ return vouchee_sigma
90
+
91
+ def get_vouchers_for(self, agent_did: str, session_id: str) -> list[VouchRecord]:
92
+ """Get all sponsors for an agent in a session."""
93
+ return [
94
+ v for v in self._vouches.values()
95
+ if v.vouchee_did == agent_did
96
+ and v.session_id == session_id
97
+ and v.is_active
98
+ ]
99
+
100
+ def get_total_exposure(self, voucher_did: str, session_id: str) -> float:
101
+ """Always zero in Public Preview."""
102
+ return 0.0
103
+
104
+ def release_bond(self, vouch_id: str) -> None:
105
+ """Release a sponsorship bond."""
106
+ if vouch_id not in self._vouches:
107
+ raise VouchingError(f"Sponsor {vouch_id} not found")
108
+ record = self._vouches[vouch_id]
109
+ record.is_active = False
110
+ record.released_at = datetime.now(UTC)
111
+
112
+ def release_session_bonds(self, session_id: str) -> int:
113
+ """Release all bonds for a session."""
114
+ count = 0
115
+ for v in self._vouches.values():
116
+ if v.session_id == session_id and v.is_active:
117
+ v.is_active = False
118
+ v.released_at = datetime.now(UTC)
119
+ count += 1
120
+ return count
121
+
122
+ def _active_vouches_for(
123
+ self, agent_did: str, session_id: str
124
+ ) -> list[VouchRecord]:
125
+ return self.get_vouchers_for(agent_did, session_id)
126
+
127
+ def _creates_cycle(
128
+ self, voucher_did: str, vouchee_did: str, session_id: str
129
+ ) -> bool:
130
+ return False
131
+
132
+
133
+ class VouchingError(Exception):
134
+ """Raised for sponsorship protocol violations."""
hypervisor/models.py ADDED
@@ -0,0 +1,277 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ """Core data models for the Agent Hypervisor."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import re
8
+ from dataclasses import dataclass, field
9
+ from datetime import UTC, datetime
10
+ from enum import Enum
11
+
12
+ from hypervisor.constants import (
13
+ MAX_AGENT_ID_LENGTH,
14
+ MAX_API_PATH_LENGTH,
15
+ MAX_DURATION_LIMIT,
16
+ MAX_NAME_LENGTH,
17
+ MAX_PARTICIPANTS_LIMIT,
18
+ MAX_UNDO_WINDOW,
19
+ RING_1_TRUST_THRESHOLD,
20
+ RING_2_TRUST_THRESHOLD,
21
+ RISK_WEIGHT_FULL,
22
+ RISK_WEIGHT_NONE,
23
+ RISK_WEIGHT_PARTIAL,
24
+ SESSION_DEFAULT_MIN_EFF_SCORE,
25
+ )
26
+
27
+ # Agent ID: DID format (did:method:id) or simple alphanumeric identifiers.
28
+ # Restrict to safe characters — no @, no consecutive special chars.
29
+ _AGENT_ID_PATTERN = re.compile(r"^[a-zA-Z0-9](?:[a-zA-Z0-9._:-]*[a-zA-Z0-9])?$")
30
+ # Aliases for backward compatibility
31
+ _MAX_AGENT_ID_LENGTH = MAX_AGENT_ID_LENGTH
32
+ _MAX_NAME_LENGTH = MAX_NAME_LENGTH
33
+ _MAX_API_PATH_LENGTH = MAX_API_PATH_LENGTH
34
+ _MAX_PARTICIPANTS_LIMIT = MAX_PARTICIPANTS_LIMIT
35
+ _MAX_DURATION_LIMIT = MAX_DURATION_LIMIT
36
+ _MAX_UNDO_WINDOW = MAX_UNDO_WINDOW
37
+
38
+
39
+ class ConsistencyMode(str, Enum):
40
+ """Session consistency mode. Strong requires consensus; Eventual uses gossip."""
41
+
42
+ STRONG = "strong"
43
+ EVENTUAL = "eventual"
44
+
45
+
46
+ class ExecutionRing(int, Enum):
47
+ """
48
+ Hardware-inspired execution privilege rings.
49
+
50
+ Ring 0 (Root): Hypervisor config & penalty — requires SRE Witness.
51
+ Ring 1 (Privileged): Non-reversible actions — requires eff_score > 0.95 + consensus.
52
+ Ring 2 (Standard): Reversible actions — requires eff_score > 0.60.
53
+ Ring 3 (Sandbox): Read-only / research — default for unknown agents.
54
+ """
55
+
56
+ RING_0_ROOT = 0
57
+ RING_1_PRIVILEGED = 1
58
+ RING_2_STANDARD = 2
59
+ RING_3_SANDBOX = 3
60
+
61
+ @classmethod
62
+ def from_eff_score(cls, eff_score: float, has_consensus: bool = False) -> ExecutionRing:
63
+ """Derive ring level from effective reputation score."""
64
+ if eff_score > RING_1_TRUST_THRESHOLD and has_consensus:
65
+ return cls.RING_1_PRIVILEGED
66
+ elif eff_score > RING_2_TRUST_THRESHOLD:
67
+ return cls.RING_2_STANDARD
68
+ else:
69
+ return cls.RING_3_SANDBOX
70
+
71
+
72
+ class ReversibilityLevel(str, Enum):
73
+ """How reversible an action is."""
74
+
75
+ FULL = "full"
76
+ PARTIAL = "partial"
77
+ NONE = "none"
78
+
79
+ @property
80
+ def risk_weight_range(self) -> tuple[float, float]:
81
+ """Return the (min, max) risk weight ω for this reversibility level."""
82
+ if self == ReversibilityLevel.FULL:
83
+ return RISK_WEIGHT_FULL
84
+ elif self == ReversibilityLevel.PARTIAL:
85
+ return RISK_WEIGHT_PARTIAL
86
+ else:
87
+ return RISK_WEIGHT_NONE
88
+
89
+ @property
90
+ def default_risk_weight(self) -> float:
91
+ """Return the default ω for this level."""
92
+ lo, hi = self.risk_weight_range
93
+ return (lo + hi) / 2
94
+
95
+
96
+ class SessionState(str, Enum):
97
+ """Lifecycle state of a Shared Session."""
98
+
99
+ CREATED = "created"
100
+ HANDSHAKING = "handshaking"
101
+ ACTIVE = "active"
102
+ TERMINATING = "terminating"
103
+ ARCHIVED = "archived"
104
+
105
+
106
+ def _validate_identifier(value: str, field_name: str) -> None:
107
+ """Validate an identifier string (agent DID, action ID, etc.)."""
108
+ if not isinstance(value, str):
109
+ raise TypeError(f"{field_name} must be a string, got {type(value).__name__}")
110
+ if not value or not value.strip():
111
+ raise ValueError(f"{field_name} must not be empty")
112
+ if len(value) > _MAX_AGENT_ID_LENGTH:
113
+ raise ValueError(
114
+ f"{field_name} exceeds maximum length of {_MAX_AGENT_ID_LENGTH} characters"
115
+ )
116
+ if not _AGENT_ID_PATTERN.match(value):
117
+ raise ValueError(
118
+ f"{field_name} contains invalid characters: {value!r}. "
119
+ f"Only alphanumeric, hyphens, underscores, colons, and dots are allowed."
120
+ )
121
+
122
+
123
+ def _validate_api_path(value: str, field_name: str) -> None:
124
+ """Validate an API path string."""
125
+ if not isinstance(value, str):
126
+ raise TypeError(f"{field_name} must be a string, got {type(value).__name__}")
127
+ if not value or not value.strip():
128
+ raise ValueError(f"{field_name} must not be empty")
129
+ if len(value) > _MAX_API_PATH_LENGTH:
130
+ raise ValueError(
131
+ f"{field_name} exceeds maximum length of {_MAX_API_PATH_LENGTH} characters"
132
+ )
133
+
134
+
135
+ @dataclass
136
+ class SessionConfig:
137
+ """Configuration for a new Shared Session."""
138
+
139
+ consistency_mode: ConsistencyMode = ConsistencyMode.EVENTUAL
140
+ max_participants: int = 10
141
+ max_duration_seconds: int = 3600
142
+ min_eff_score: float = SESSION_DEFAULT_MIN_EFF_SCORE
143
+ enable_audit: bool = True
144
+ enable_blockchain_commitment: bool = False
145
+
146
+ def __post_init__(self) -> None:
147
+ if not isinstance(self.max_participants, int):
148
+ raise TypeError(
149
+ f"max_participants must be an integer, got {type(self.max_participants).__name__}"
150
+ )
151
+ if self.max_participants < 1:
152
+ raise ValueError(
153
+ f"max_participants must be at least 1, got {self.max_participants}"
154
+ )
155
+ if self.max_participants > _MAX_PARTICIPANTS_LIMIT:
156
+ raise ValueError(
157
+ f"max_participants must not exceed {_MAX_PARTICIPANTS_LIMIT}, "
158
+ f"got {self.max_participants}"
159
+ )
160
+ if not isinstance(self.max_duration_seconds, int):
161
+ raise TypeError(
162
+ f"max_duration_seconds must be an integer, "
163
+ f"got {type(self.max_duration_seconds).__name__}"
164
+ )
165
+ if self.max_duration_seconds < 1:
166
+ raise ValueError(
167
+ f"max_duration_seconds must be at least 1, got {self.max_duration_seconds}"
168
+ )
169
+ if self.max_duration_seconds > _MAX_DURATION_LIMIT:
170
+ raise ValueError(
171
+ f"max_duration_seconds must not exceed {_MAX_DURATION_LIMIT} (7 days), "
172
+ f"got {self.max_duration_seconds}"
173
+ )
174
+ if not isinstance(self.min_eff_score, (int, float)):
175
+ raise TypeError(
176
+ f"min_eff_score must be a number, got {type(self.min_eff_score).__name__}"
177
+ )
178
+ if not (0.0 <= self.min_eff_score <= 1.0):
179
+ raise ValueError(
180
+ f"min_eff_score must be between 0.0 and 1.0, got {self.min_eff_score}"
181
+ )
182
+
183
+
184
+ @dataclass
185
+ class SessionParticipant:
186
+ """An agent participating in a session."""
187
+
188
+ agent_did: str
189
+ ring: ExecutionRing = ExecutionRing.RING_3_SANDBOX
190
+ sigma_raw: float = 0.0
191
+ eff_score: float = 0.0
192
+ joined_at: datetime = field(default_factory=lambda: datetime.now(UTC))
193
+ is_active: bool = True
194
+
195
+ def __post_init__(self) -> None:
196
+ _validate_identifier(self.agent_did, "agent_did")
197
+ if not isinstance(self.ring, ExecutionRing):
198
+ try:
199
+ self.ring = ExecutionRing(self.ring)
200
+ except (ValueError, KeyError):
201
+ raise ValueError(
202
+ f"ring must be a valid ExecutionRing (0-3), got {self.ring!r}"
203
+ )
204
+ if not isinstance(self.sigma_raw, (int, float)):
205
+ raise TypeError(
206
+ f"sigma_raw must be a number, got {type(self.sigma_raw).__name__}"
207
+ )
208
+ if not (0.0 <= self.sigma_raw <= 1.0):
209
+ raise ValueError(
210
+ f"sigma_raw must be between 0.0 and 1.0, got {self.sigma_raw}"
211
+ )
212
+ if not isinstance(self.eff_score, (int, float)):
213
+ raise TypeError(
214
+ f"eff_score must be a number, got {type(self.eff_score).__name__}"
215
+ )
216
+ if not (0.0 <= self.eff_score <= 1.0):
217
+ raise ValueError(
218
+ f"eff_score must be between 0.0 and 1.0, got {self.eff_score}"
219
+ )
220
+
221
+
222
+ @dataclass
223
+ class ActionDescriptor:
224
+ """Describes an action from an IATP Capability Manifest."""
225
+
226
+ action_id: str
227
+ name: str
228
+ execute_api: str
229
+ undo_api: str | None = None
230
+ reversibility: ReversibilityLevel = ReversibilityLevel.NONE
231
+ undo_window_seconds: int = 0
232
+ compensation_method: str | None = None
233
+ is_read_only: bool = False
234
+ is_admin: bool = False
235
+
236
+ def __post_init__(self) -> None:
237
+ _validate_identifier(self.action_id, "action_id")
238
+ if not isinstance(self.name, str) or not self.name.strip():
239
+ raise ValueError("name must be a non-empty string")
240
+ if len(self.name) > _MAX_NAME_LENGTH:
241
+ raise ValueError(
242
+ f"name exceeds maximum length of {_MAX_NAME_LENGTH} characters"
243
+ )
244
+ _validate_api_path(self.execute_api, "execute_api")
245
+ if self.undo_api is not None:
246
+ _validate_api_path(self.undo_api, "undo_api")
247
+ if not isinstance(self.undo_window_seconds, int):
248
+ raise TypeError(
249
+ f"undo_window_seconds must be an integer, "
250
+ f"got {type(self.undo_window_seconds).__name__}"
251
+ )
252
+ if self.undo_window_seconds < 0:
253
+ raise ValueError(
254
+ f"undo_window_seconds must not be negative, got {self.undo_window_seconds}"
255
+ )
256
+ if self.undo_window_seconds > _MAX_UNDO_WINDOW:
257
+ raise ValueError(
258
+ f"undo_window_seconds must not exceed {_MAX_UNDO_WINDOW} (24 hours), "
259
+ f"got {self.undo_window_seconds}"
260
+ )
261
+
262
+ @property
263
+ def risk_weight(self) -> float:
264
+ """Compute ω from reversibility level."""
265
+ return self.reversibility.default_risk_weight
266
+
267
+ @property
268
+ def required_ring(self) -> ExecutionRing:
269
+ """Determine minimum ring required for this action."""
270
+ if self.is_admin:
271
+ return ExecutionRing.RING_0_ROOT
272
+ elif self.reversibility == ReversibilityLevel.NONE and not self.is_read_only:
273
+ return ExecutionRing.RING_1_PRIVILEGED
274
+ elif self.is_read_only:
275
+ return ExecutionRing.RING_3_SANDBOX
276
+ else:
277
+ return ExecutionRing.RING_2_STANDARD
@@ -0,0 +1,27 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ """Observability module — structured event bus, causal tracing, and metrics."""
4
+
5
+ from hypervisor.observability.causal_trace import CausalTraceId
6
+ from hypervisor.observability.event_bus import (
7
+ EventType,
8
+ HypervisorEvent,
9
+ HypervisorEventBus,
10
+ )
11
+ from hypervisor.observability.prometheus_collector import RingMetricsCollector
12
+ from hypervisor.observability.saga_span_exporter import (
13
+ SagaSpanExporter,
14
+ SagaSpanRecord,
15
+ SpanSink,
16
+ )
17
+
18
+ __all__ = [
19
+ "EventType",
20
+ "HypervisorEvent",
21
+ "HypervisorEventBus",
22
+ "CausalTraceId",
23
+ "RingMetricsCollector",
24
+ "SagaSpanExporter",
25
+ "SagaSpanRecord",
26
+ "SpanSink",
27
+ ]
@@ -0,0 +1,70 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ """
4
+ Causal trace IDs for cross-agent distributed tracing.
5
+
6
+ Unlike simple correlation IDs, causal trace IDs encode the full
7
+ spawn/delegation tree, making distributed traces genuinely readable.
8
+
9
+ Format: {trace_id}/{span_id}[/{parent_span_id}]
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import uuid
15
+ from dataclasses import dataclass, field
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class CausalTraceId:
20
+ """Encodes the full spawn/delegation tree for a trace."""
21
+
22
+ trace_id: str = field(default_factory=lambda: uuid.uuid4().hex[:12])
23
+ span_id: str = field(default_factory=lambda: uuid.uuid4().hex[:8])
24
+ parent_span_id: str | None = None
25
+ depth: int = 0
26
+
27
+ def child(self) -> CausalTraceId:
28
+ """Create a child span (e.g., when spawning a sub-agent or delegating)."""
29
+ return CausalTraceId(
30
+ trace_id=self.trace_id,
31
+ span_id=uuid.uuid4().hex[:8],
32
+ parent_span_id=self.span_id,
33
+ depth=self.depth + 1,
34
+ )
35
+
36
+ def sibling(self) -> CausalTraceId:
37
+ """Create a sibling span (same parent, different operation)."""
38
+ return CausalTraceId(
39
+ trace_id=self.trace_id,
40
+ span_id=uuid.uuid4().hex[:8],
41
+ parent_span_id=self.parent_span_id,
42
+ depth=self.depth,
43
+ )
44
+
45
+ @property
46
+ def full_id(self) -> str:
47
+ """Full trace path: trace_id/span_id[/parent_span_id]."""
48
+ parts = [self.trace_id, self.span_id]
49
+ if self.parent_span_id:
50
+ parts.append(self.parent_span_id)
51
+ return "/".join(parts)
52
+
53
+ @classmethod
54
+ def from_string(cls, s: str) -> CausalTraceId:
55
+ """Parse a CausalTraceId from its string representation."""
56
+ parts = s.split("/")
57
+ if len(parts) < 2:
58
+ raise ValueError(f"Invalid causal trace ID: {s}")
59
+ return cls(
60
+ trace_id=parts[0],
61
+ span_id=parts[1],
62
+ parent_span_id=parts[2] if len(parts) > 2 else None,
63
+ )
64
+
65
+ def is_ancestor_of(self, other: CausalTraceId) -> bool:
66
+ """Check if this trace is an ancestor of another (same trace tree)."""
67
+ return self.trace_id == other.trace_id and other.depth > self.depth
68
+
69
+ def __str__(self) -> str:
70
+ return self.full_id