mirrors-sdk 0.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.
- mirrors_sdk/__init__.py +33 -0
- mirrors_sdk/capture.py +254 -0
- mirrors_sdk/circuit.py +47 -0
- mirrors_sdk/client.py +339 -0
- mirrors_sdk/configuration.py +75 -0
- mirrors_sdk/context.py +254 -0
- mirrors_sdk/delivery.py +96 -0
- mirrors_sdk/models.py +107 -0
- mirrors_sdk/py.typed +0 -0
- mirrors_sdk/resolver.py +69 -0
- mirrors_sdk/snapshot.py +95 -0
- mirrors_sdk/transport.py +184 -0
- mirrors_sdk/types.py +87 -0
- mirrors_sdk/wire.py +310 -0
- mirrors_sdk/worker.py +138 -0
- mirrors_sdk-0.1.0.dist-info/METADATA +180 -0
- mirrors_sdk-0.1.0.dist-info/RECORD +19 -0
- mirrors_sdk-0.1.0.dist-info/WHEEL +5 -0
- mirrors_sdk-0.1.0.dist-info/top_level.txt +1 -0
mirrors_sdk/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Mirrors: optional context and best-effort original trace capture."""
|
|
2
|
+
|
|
3
|
+
from .client import Mirrors
|
|
4
|
+
from .models import (
|
|
5
|
+
ContextFreshness,
|
|
6
|
+
ContextResult,
|
|
7
|
+
DeliveryReceipt,
|
|
8
|
+
DeliveryTicket,
|
|
9
|
+
FlushResult,
|
|
10
|
+
TurnTimer,
|
|
11
|
+
)
|
|
12
|
+
from .types import (
|
|
13
|
+
CanonicalMessage,
|
|
14
|
+
CanonicalTrace,
|
|
15
|
+
EndUser,
|
|
16
|
+
MessageBlock,
|
|
17
|
+
SourceCompleteness,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Mirrors",
|
|
22
|
+
"ContextFreshness",
|
|
23
|
+
"ContextResult",
|
|
24
|
+
"DeliveryReceipt",
|
|
25
|
+
"DeliveryTicket",
|
|
26
|
+
"FlushResult",
|
|
27
|
+
"TurnTimer",
|
|
28
|
+
"CanonicalMessage",
|
|
29
|
+
"CanonicalTrace",
|
|
30
|
+
"EndUser",
|
|
31
|
+
"MessageBlock",
|
|
32
|
+
"SourceCompleteness",
|
|
33
|
+
]
|
mirrors_sdk/capture.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Ordered capture delivery with immutable reconciliation of uncertain writes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
import uuid
|
|
10
|
+
from collections import OrderedDict
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from .models import DeliveryReceipt
|
|
15
|
+
from .transport import TransportError
|
|
16
|
+
from .wire import Wire
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class _Pending:
|
|
21
|
+
digest: bytes
|
|
22
|
+
body: bytes
|
|
23
|
+
request: dict[str, Any]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class _Trace:
|
|
28
|
+
metadata: bytes
|
|
29
|
+
ordinal: int = 0
|
|
30
|
+
pending: _Pending | None = None
|
|
31
|
+
sealed: bool = False
|
|
32
|
+
retired: bool = False
|
|
33
|
+
lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
|
34
|
+
running: asyncio.Task[Any] | None = None
|
|
35
|
+
final_digest: bytes | None = None
|
|
36
|
+
final_owner: asyncio.Task[Any] | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CaptureRuntime:
|
|
40
|
+
"""Loop-owned state; application threads communicate through the SDK worker."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, transport: Any, config: Any) -> None:
|
|
43
|
+
self._transport = transport
|
|
44
|
+
self._config = config
|
|
45
|
+
self._active: dict[str, _Trace] = {}
|
|
46
|
+
self._terminal: dict[str, _Trace] = {}
|
|
47
|
+
self._recent: OrderedDict[str, tuple[bytes, DeliveryReceipt]] = OrderedDict()
|
|
48
|
+
self._closed = False
|
|
49
|
+
|
|
50
|
+
async def send(self, input: dict[str, Any], deadline: float) -> DeliveryReceipt:
|
|
51
|
+
"""Send once; finals may retry once within the same absolute deadline."""
|
|
52
|
+
receipt = await self._send(input, deadline)
|
|
53
|
+
if input.get("final") is True:
|
|
54
|
+
trace_id = input.get("trace", {}).get("id")
|
|
55
|
+
# A rejected final still ends the host execution. Release any prefix state
|
|
56
|
+
# left behind by validation/capacity rejection, preserving admitted finals.
|
|
57
|
+
self.release(trace_id)
|
|
58
|
+
state = self._terminal.get(trace_id)
|
|
59
|
+
if state is not None and state.final_owner is asyncio.current_task():
|
|
60
|
+
# Settled uncertain finals do not consume scarce terminal capacity forever.
|
|
61
|
+
# Their immutable outcome is retained in the same bounded receipt cache.
|
|
62
|
+
self._terminal.pop(trace_id, None)
|
|
63
|
+
if state.final_digest is not None:
|
|
64
|
+
self._remember(trace_id, state.final_digest, receipt)
|
|
65
|
+
self._retire(state)
|
|
66
|
+
return receipt
|
|
67
|
+
|
|
68
|
+
async def _send(self, input: dict[str, Any], deadline: float) -> DeliveryReceipt:
|
|
69
|
+
if self._closed:
|
|
70
|
+
return DeliveryReceipt("dropped", "closed")
|
|
71
|
+
final = input.get("final") is True
|
|
72
|
+
timeout_ms = self._config.timeout_ms
|
|
73
|
+
deadline = min(
|
|
74
|
+
deadline,
|
|
75
|
+
time.monotonic() + (timeout_ms or (6000 if final else 1000)) / 1000,
|
|
76
|
+
)
|
|
77
|
+
try:
|
|
78
|
+
encoded = Wire.encode(input)
|
|
79
|
+
snapshot = json.loads(encoded)
|
|
80
|
+
trace_id = snapshot["trace"]["id"]
|
|
81
|
+
digest = hashlib.sha256(encoded).digest()
|
|
82
|
+
metadata = self._metadata(snapshot["trace"])
|
|
83
|
+
except (TypeError, ValueError, KeyError, RecursionError):
|
|
84
|
+
return DeliveryReceipt("dropped", "invalid_evidence")
|
|
85
|
+
previous = self._recent.get(trace_id)
|
|
86
|
+
if previous is not None:
|
|
87
|
+
return (
|
|
88
|
+
previous[1]
|
|
89
|
+
if previous[0] == digest
|
|
90
|
+
else DeliveryReceipt("dropped", "terminal_conflict")
|
|
91
|
+
)
|
|
92
|
+
state = self._terminal.get(trace_id) or self._active.get(trace_id)
|
|
93
|
+
if state is not None and state.metadata != metadata:
|
|
94
|
+
return DeliveryReceipt("dropped", "invalid_evidence")
|
|
95
|
+
if state is not None and state.sealed and not final:
|
|
96
|
+
return DeliveryReceipt("dropped", "terminal_conflict")
|
|
97
|
+
if state is not None and state.sealed and state.final_digest != digest:
|
|
98
|
+
return DeliveryReceipt("dropped", "terminal_conflict")
|
|
99
|
+
if state is None:
|
|
100
|
+
pool = self._terminal if final else self._active
|
|
101
|
+
if len(pool) >= (16 if final else 64):
|
|
102
|
+
return DeliveryReceipt("dropped", "capacity")
|
|
103
|
+
state = _Trace(metadata, sealed=final)
|
|
104
|
+
pool[trace_id] = state
|
|
105
|
+
if final:
|
|
106
|
+
state.final_digest = digest
|
|
107
|
+
state.final_owner = asyncio.current_task()
|
|
108
|
+
elif final and not state.sealed:
|
|
109
|
+
if len(self._terminal) >= 16:
|
|
110
|
+
return DeliveryReceipt("dropped", "capacity")
|
|
111
|
+
state.sealed = True
|
|
112
|
+
state.final_digest = digest
|
|
113
|
+
state.final_owner = asyncio.current_task()
|
|
114
|
+
self._active.pop(trace_id, None)
|
|
115
|
+
self._terminal[trace_id] = state
|
|
116
|
+
acquired = False
|
|
117
|
+
try:
|
|
118
|
+
remaining = deadline - time.monotonic()
|
|
119
|
+
if remaining <= 0:
|
|
120
|
+
return DeliveryReceipt("unconfirmed", "timeout")
|
|
121
|
+
await asyncio.wait_for(state.lock.acquire(), remaining)
|
|
122
|
+
acquired = True
|
|
123
|
+
state.running = asyncio.current_task()
|
|
124
|
+
if state.retired or self._closed:
|
|
125
|
+
return DeliveryReceipt("dropped", "closed")
|
|
126
|
+
if state.sealed and not final:
|
|
127
|
+
return DeliveryReceipt("dropped", "superseded")
|
|
128
|
+
# Another queued final may have completed while this call waited.
|
|
129
|
+
previous = self._recent.get(trace_id)
|
|
130
|
+
if previous is not None:
|
|
131
|
+
return (
|
|
132
|
+
previous[1]
|
|
133
|
+
if previous[0] == digest
|
|
134
|
+
else DeliveryReceipt("dropped", "terminal_conflict")
|
|
135
|
+
)
|
|
136
|
+
if state.pending is not None and state.pending.digest != digest:
|
|
137
|
+
# An uncertain terminal is immutable. A different final cannot replace it.
|
|
138
|
+
if state.pending.request["final"]:
|
|
139
|
+
return DeliveryReceipt("dropped", "terminal_conflict")
|
|
140
|
+
await self._deliver(state, trace_id, deadline, retry=final)
|
|
141
|
+
if state.pending is None:
|
|
142
|
+
if state.ordinal >= 10_000:
|
|
143
|
+
return DeliveryReceipt("dropped", "capacity")
|
|
144
|
+
request = {
|
|
145
|
+
**snapshot,
|
|
146
|
+
"protocol": 3,
|
|
147
|
+
"agentId": self._config.agent_id,
|
|
148
|
+
"requestId": str(uuid.uuid4()),
|
|
149
|
+
"ordinal": state.ordinal + 1,
|
|
150
|
+
}
|
|
151
|
+
state.pending = _Pending(
|
|
152
|
+
digest, Wire.encode(request, 1_048_576), request
|
|
153
|
+
)
|
|
154
|
+
return await self._deliver(state, trace_id, deadline, retry=final)
|
|
155
|
+
except asyncio.TimeoutError:
|
|
156
|
+
return DeliveryReceipt("unconfirmed", "timeout")
|
|
157
|
+
except asyncio.CancelledError:
|
|
158
|
+
return DeliveryReceipt("unconfirmed", "transport")
|
|
159
|
+
except TransportError as error:
|
|
160
|
+
return DeliveryReceipt(
|
|
161
|
+
"unconfirmed", "timeout" if error.reason == "timeout" else "transport"
|
|
162
|
+
)
|
|
163
|
+
except Exception:
|
|
164
|
+
# Optional capture must not leak protocol or transport failures into the host.
|
|
165
|
+
return DeliveryReceipt("unconfirmed", "transport")
|
|
166
|
+
finally:
|
|
167
|
+
if acquired:
|
|
168
|
+
state.running = None
|
|
169
|
+
state.lock.release()
|
|
170
|
+
|
|
171
|
+
async def _deliver(
|
|
172
|
+
self, state: _Trace, trace_id: str, deadline: float, *, retry: bool
|
|
173
|
+
) -> DeliveryReceipt:
|
|
174
|
+
pending = state.pending
|
|
175
|
+
assert pending is not None
|
|
176
|
+
for attempt in range(2 if retry else 1):
|
|
177
|
+
if time.monotonic() >= deadline:
|
|
178
|
+
raise asyncio.TimeoutError
|
|
179
|
+
if state.retired or self._closed:
|
|
180
|
+
raise asyncio.CancelledError
|
|
181
|
+
try:
|
|
182
|
+
response = await self._transport.request(
|
|
183
|
+
"trace",
|
|
184
|
+
pending.body,
|
|
185
|
+
deadline,
|
|
186
|
+
lambda value: Wire.trace_response(value, pending.request),
|
|
187
|
+
final_recovery=retry,
|
|
188
|
+
)
|
|
189
|
+
break
|
|
190
|
+
except TransportError as error:
|
|
191
|
+
if (
|
|
192
|
+
error.permanent
|
|
193
|
+
or not retry
|
|
194
|
+
or attempt == 1
|
|
195
|
+
or time.monotonic() >= deadline
|
|
196
|
+
):
|
|
197
|
+
raise
|
|
198
|
+
if state.retired or self._closed:
|
|
199
|
+
raise asyncio.CancelledError
|
|
200
|
+
if time.monotonic() >= deadline:
|
|
201
|
+
raise asyncio.TimeoutError
|
|
202
|
+
if not Wire.trace_response(response, pending.request):
|
|
203
|
+
raise ValueError("Invalid capture response")
|
|
204
|
+
state.ordinal = pending.request["ordinal"]
|
|
205
|
+
state.pending = None
|
|
206
|
+
receipt = DeliveryReceipt("accepted", ingestion=response["ingestion"])
|
|
207
|
+
if pending.request["final"]:
|
|
208
|
+
self._active.pop(trace_id, None)
|
|
209
|
+
self._terminal.pop(trace_id, None)
|
|
210
|
+
self._remember(trace_id, pending.digest, receipt)
|
|
211
|
+
return receipt
|
|
212
|
+
|
|
213
|
+
def _remember(self, trace_id: str, digest: bytes, receipt: DeliveryReceipt) -> None:
|
|
214
|
+
self._recent[trace_id] = (digest, receipt)
|
|
215
|
+
if len(self._recent) > 128:
|
|
216
|
+
self._recent.popitem(last=False)
|
|
217
|
+
|
|
218
|
+
def release(self, trace_id: str) -> None:
|
|
219
|
+
"""Retire prefixes locally; an admitted final retains its delivery opportunity."""
|
|
220
|
+
state = self._active.pop(trace_id, None)
|
|
221
|
+
if state is not None:
|
|
222
|
+
self._retire(state)
|
|
223
|
+
|
|
224
|
+
def clear(self) -> None:
|
|
225
|
+
"""Permanently stop capture and abandon all outstanding work."""
|
|
226
|
+
self._closed = True
|
|
227
|
+
for state in (*self._active.values(), *self._terminal.values()):
|
|
228
|
+
self._retire(state)
|
|
229
|
+
self._active.clear()
|
|
230
|
+
self._terminal.clear()
|
|
231
|
+
self._recent.clear()
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def _retire(state: _Trace) -> None:
|
|
235
|
+
state.retired = True
|
|
236
|
+
if state.running is not None:
|
|
237
|
+
state.running.cancel()
|
|
238
|
+
|
|
239
|
+
@staticmethod
|
|
240
|
+
def _metadata(trace: dict[str, Any]) -> bytes:
|
|
241
|
+
# End-user and measured duration intentionally belong to the final snapshot.
|
|
242
|
+
fields = (
|
|
243
|
+
"model",
|
|
244
|
+
"entryPoint",
|
|
245
|
+
"completeness",
|
|
246
|
+
"conversationId",
|
|
247
|
+
"conversationRevision",
|
|
248
|
+
)
|
|
249
|
+
return json.dumps(
|
|
250
|
+
{key: trace[key] for key in fields if key in trace},
|
|
251
|
+
sort_keys=True,
|
|
252
|
+
separators=(",", ":"),
|
|
253
|
+
ensure_ascii=False,
|
|
254
|
+
).encode("utf-8")
|
mirrors_sdk/circuit.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""An endpoint-local circuit, owned exclusively by the background event loop."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Circuit:
|
|
8
|
+
def __init__(self, threshold: int = 3, cooldown_ms: int = 30_000) -> None:
|
|
9
|
+
if type(threshold) is not int or not 1 <= threshold <= 100:
|
|
10
|
+
raise ValueError("Invalid circuit threshold")
|
|
11
|
+
if type(cooldown_ms) is not int or not 1 <= cooldown_ms <= 300_000:
|
|
12
|
+
raise ValueError("Invalid circuit cooldown")
|
|
13
|
+
self._threshold = threshold
|
|
14
|
+
self._cooldown = cooldown_ms / 1000
|
|
15
|
+
self._failures = 0
|
|
16
|
+
self._until = 0.0
|
|
17
|
+
self._probing = False
|
|
18
|
+
self._generation = 0
|
|
19
|
+
|
|
20
|
+
def acquire(self) -> Callable[[bool | None], None] | None:
|
|
21
|
+
if self._until and (time.monotonic() < self._until or self._probing):
|
|
22
|
+
return None
|
|
23
|
+
probe = self._until > 0
|
|
24
|
+
if probe:
|
|
25
|
+
self._probing = True
|
|
26
|
+
generation = self._generation
|
|
27
|
+
finished = False
|
|
28
|
+
|
|
29
|
+
def finish(success: bool | None) -> None:
|
|
30
|
+
nonlocal finished
|
|
31
|
+
if finished or generation != self._generation:
|
|
32
|
+
return
|
|
33
|
+
finished = True
|
|
34
|
+
if probe:
|
|
35
|
+
self._probing = False
|
|
36
|
+
if success is None:
|
|
37
|
+
return
|
|
38
|
+
if success:
|
|
39
|
+
self._failures = 0
|
|
40
|
+
self._until = 0
|
|
41
|
+
else:
|
|
42
|
+
self._failures += 1
|
|
43
|
+
if probe or self._failures >= self._threshold:
|
|
44
|
+
self._until = time.monotonic() + self._cooldown
|
|
45
|
+
self._generation += 1
|
|
46
|
+
|
|
47
|
+
return finish
|
mirrors_sdk/client.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""Synchronous integration surface with optional async waits for async applications."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
from concurrent.futures import TimeoutError
|
|
8
|
+
import hashlib
|
|
9
|
+
import os
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
from .configuration import Config
|
|
14
|
+
from .context import ContextHost
|
|
15
|
+
from .delivery import Deliveries
|
|
16
|
+
from .models import DeliveryReceipt, FlushResult, TurnTimer
|
|
17
|
+
from .wire import Wire
|
|
18
|
+
from .worker import Worker
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Mirrors:
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
api_url: str,
|
|
26
|
+
api_key: str,
|
|
27
|
+
agent_id: str,
|
|
28
|
+
timeout_ms: int | None = None,
|
|
29
|
+
enabled: bool = True,
|
|
30
|
+
failure_threshold: int = 3,
|
|
31
|
+
cooldown_ms: int = 30_000,
|
|
32
|
+
heartbeat_interval_ms: int = 1000,
|
|
33
|
+
mode_lease_ms: int = 3000,
|
|
34
|
+
):
|
|
35
|
+
self._lock = threading.RLock()
|
|
36
|
+
self._pid = os.getpid()
|
|
37
|
+
self._closed = False
|
|
38
|
+
self._failed = False
|
|
39
|
+
self._worker = None
|
|
40
|
+
self._deliveries = Deliveries()
|
|
41
|
+
self._traces = {}
|
|
42
|
+
self._recent = OrderedDict()
|
|
43
|
+
try:
|
|
44
|
+
self._config = Config(
|
|
45
|
+
api_url=api_url,
|
|
46
|
+
api_key=api_key,
|
|
47
|
+
agent_id=agent_id,
|
|
48
|
+
timeout_ms=timeout_ms,
|
|
49
|
+
enabled=enabled,
|
|
50
|
+
failure_threshold=failure_threshold,
|
|
51
|
+
cooldown_ms=cooldown_ms,
|
|
52
|
+
heartbeat_interval_ms=heartbeat_interval_ms,
|
|
53
|
+
mode_lease_ms=mode_lease_ms,
|
|
54
|
+
)
|
|
55
|
+
except Exception:
|
|
56
|
+
self._config = None
|
|
57
|
+
self._enabled = self._config is not None and self._config.enabled
|
|
58
|
+
self._context = ContextHost(self._config)
|
|
59
|
+
if self._config:
|
|
60
|
+
try:
|
|
61
|
+
self._worker = Worker(
|
|
62
|
+
self._config, self._context.observe_mode, self._worker_failed
|
|
63
|
+
)
|
|
64
|
+
self._context.worker = self._worker
|
|
65
|
+
except Exception:
|
|
66
|
+
self._worker_failed()
|
|
67
|
+
|
|
68
|
+
def _unavailable(self):
|
|
69
|
+
if os.getpid() != self._pid:
|
|
70
|
+
return "forked_process"
|
|
71
|
+
if self._config is None:
|
|
72
|
+
return "invalid_options"
|
|
73
|
+
if self._failed:
|
|
74
|
+
return "worker_failed"
|
|
75
|
+
if self._closed:
|
|
76
|
+
return "closed"
|
|
77
|
+
if not self._enabled:
|
|
78
|
+
return "disabled"
|
|
79
|
+
if self._worker is None:
|
|
80
|
+
return "worker_failed"
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def _remember(self, trace_id, value):
|
|
84
|
+
self._recent[trace_id] = value
|
|
85
|
+
self._recent.move_to_end(trace_id)
|
|
86
|
+
if len(self._recent) > 128:
|
|
87
|
+
self._recent.popitem(last=False)
|
|
88
|
+
|
|
89
|
+
def capture_trace(
|
|
90
|
+
self, trace: dict, *, final: bool = False, terminal: str | None = None
|
|
91
|
+
):
|
|
92
|
+
"""Copy and queue original evidence. Never wait for transport or a provider."""
|
|
93
|
+
started = time.monotonic()
|
|
94
|
+
if os.getpid() != self._pid:
|
|
95
|
+
# Never touch a lock inherited from another thread across fork.
|
|
96
|
+
from .models import DeliveryTicket
|
|
97
|
+
|
|
98
|
+
ticket = DeliveryTicket("forked")
|
|
99
|
+
ticket._future.set_result(DeliveryReceipt("dropped", "forked_process"))
|
|
100
|
+
return ticket
|
|
101
|
+
with self._lock:
|
|
102
|
+
reason = self._unavailable()
|
|
103
|
+
if reason:
|
|
104
|
+
return self._deliveries.drop(reason)
|
|
105
|
+
try:
|
|
106
|
+
snapshot = Wire.snapshot(trace, final=final, terminal=terminal)
|
|
107
|
+
encoded_trace = Wire.encode(snapshot)
|
|
108
|
+
value = {"trace": snapshot, "final": final, "captureOnly": True}
|
|
109
|
+
if terminal is not None:
|
|
110
|
+
value["terminal"] = terminal
|
|
111
|
+
digest = hashlib.sha256(Wire.encode(value)).digest()
|
|
112
|
+
except Exception:
|
|
113
|
+
if type(trace) is dict and type(trace.get("id")) is str:
|
|
114
|
+
self.release_trace(trace["id"])
|
|
115
|
+
return self._deliveries.drop("invalid_evidence")
|
|
116
|
+
trace_id = snapshot["id"]
|
|
117
|
+
with self._lock:
|
|
118
|
+
reason = self._unavailable()
|
|
119
|
+
if reason:
|
|
120
|
+
return self._deliveries.drop(reason)
|
|
121
|
+
if trace_id in self._recent:
|
|
122
|
+
recent = self._recent[trace_id]
|
|
123
|
+
return (
|
|
124
|
+
recent[1]
|
|
125
|
+
if recent and recent[0] == digest
|
|
126
|
+
else self._deliveries.drop("terminal_conflict")
|
|
127
|
+
)
|
|
128
|
+
if final:
|
|
129
|
+
self._context.release(trace_id)
|
|
130
|
+
else:
|
|
131
|
+
self._context.observe(trace_id, encoded_trace)
|
|
132
|
+
previous = self._traces.get(trace_id)
|
|
133
|
+
if (
|
|
134
|
+
previous
|
|
135
|
+
and previous[0] == digest
|
|
136
|
+
and previous[1].receipt.status == "pending"
|
|
137
|
+
):
|
|
138
|
+
return previous[1]
|
|
139
|
+
if self._deliveries.full(final) or (
|
|
140
|
+
not final and not previous and len(self._traces) >= 64
|
|
141
|
+
):
|
|
142
|
+
if final:
|
|
143
|
+
self.release_trace(trace_id)
|
|
144
|
+
return self._deliveries.drop("capacity")
|
|
145
|
+
ticket = self._deliveries.create(final, trace_id)
|
|
146
|
+
if final:
|
|
147
|
+
self._traces.pop(trace_id, None)
|
|
148
|
+
self._remember(trace_id, (digest, ticket))
|
|
149
|
+
else:
|
|
150
|
+
self._traces[trace_id] = (digest, ticket)
|
|
151
|
+
budget = self._config.timeout_ms or (6000 if final else 1000)
|
|
152
|
+
try:
|
|
153
|
+
job = self._worker.submit(
|
|
154
|
+
lambda worker: worker.capture.send(value, started + budget / 1000)
|
|
155
|
+
)
|
|
156
|
+
self._deliveries.attach(ticket, job)
|
|
157
|
+
job.add_done_callback(lambda done: self._capture_done(ticket, done))
|
|
158
|
+
except Exception:
|
|
159
|
+
self._deliveries.settle(
|
|
160
|
+
ticket, DeliveryReceipt("unconfirmed", "worker_failed")
|
|
161
|
+
)
|
|
162
|
+
return ticket
|
|
163
|
+
|
|
164
|
+
def _capture_done(self, ticket, job):
|
|
165
|
+
try:
|
|
166
|
+
receipt = job.result()
|
|
167
|
+
except Exception:
|
|
168
|
+
receipt = DeliveryReceipt("unconfirmed", "transport")
|
|
169
|
+
self._deliveries.settle(ticket, receipt)
|
|
170
|
+
|
|
171
|
+
def _begin_context(self, trace, timeout_ms, started, cancel):
|
|
172
|
+
with self._lock:
|
|
173
|
+
reason = self._unavailable()
|
|
174
|
+
if not reason and cancel is not None and cancel.is_set():
|
|
175
|
+
reason = "canceled"
|
|
176
|
+
if (
|
|
177
|
+
not reason
|
|
178
|
+
and type(trace) is dict
|
|
179
|
+
and type(trace.get("id")) is str
|
|
180
|
+
and trace["id"] in self._recent
|
|
181
|
+
):
|
|
182
|
+
reason = "execution_closed"
|
|
183
|
+
if reason:
|
|
184
|
+
return self._context.empty(reason, started), None, started
|
|
185
|
+
future, deadline = self._context.start(trace, timeout_ms, started)
|
|
186
|
+
# Finalization/release may race with bounded preparation outside this lock.
|
|
187
|
+
with self._lock:
|
|
188
|
+
if (
|
|
189
|
+
type(trace) is dict
|
|
190
|
+
and type(trace.get("id")) is str
|
|
191
|
+
and trace["id"] in self._recent
|
|
192
|
+
):
|
|
193
|
+
self._context.release(trace["id"])
|
|
194
|
+
return None, future, deadline
|
|
195
|
+
|
|
196
|
+
def get_context(
|
|
197
|
+
self,
|
|
198
|
+
trace: dict,
|
|
199
|
+
*,
|
|
200
|
+
timeout_ms: int = 1000,
|
|
201
|
+
cancel: threading.Event | None = None,
|
|
202
|
+
):
|
|
203
|
+
"""Return optional text within one total second; failures leave context empty."""
|
|
204
|
+
started = time.monotonic()
|
|
205
|
+
if os.getpid() != self._pid:
|
|
206
|
+
from .models import ContextResult
|
|
207
|
+
|
|
208
|
+
return ContextResult(reason="forked_process")
|
|
209
|
+
result, future, deadline = self._begin_context(
|
|
210
|
+
trace, timeout_ms, started, cancel
|
|
211
|
+
)
|
|
212
|
+
if result:
|
|
213
|
+
return result
|
|
214
|
+
while True:
|
|
215
|
+
if future.done():
|
|
216
|
+
return future.result()
|
|
217
|
+
self._context.tick(future, cancel is not None and cancel.is_set())
|
|
218
|
+
try:
|
|
219
|
+
return future.result(
|
|
220
|
+
timeout=max(0, min(0.005, deadline - time.monotonic()))
|
|
221
|
+
)
|
|
222
|
+
except TimeoutError:
|
|
223
|
+
if time.monotonic() >= deadline:
|
|
224
|
+
self._context.tick(future)
|
|
225
|
+
return (
|
|
226
|
+
future.result()
|
|
227
|
+
if future.done()
|
|
228
|
+
else self._context.empty("timeout", started)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
async def aget_context(
|
|
232
|
+
self,
|
|
233
|
+
trace: dict,
|
|
234
|
+
*,
|
|
235
|
+
timeout_ms: int = 1000,
|
|
236
|
+
cancel: threading.Event | None = None,
|
|
237
|
+
):
|
|
238
|
+
started = time.monotonic()
|
|
239
|
+
if os.getpid() != self._pid:
|
|
240
|
+
from .models import ContextResult
|
|
241
|
+
|
|
242
|
+
return ContextResult(reason="forked_process")
|
|
243
|
+
result, future, deadline = self._begin_context(
|
|
244
|
+
trace, timeout_ms, started, cancel
|
|
245
|
+
)
|
|
246
|
+
if result:
|
|
247
|
+
return result
|
|
248
|
+
try:
|
|
249
|
+
while not future.done():
|
|
250
|
+
self._context.tick(future, cancel is not None and cancel.is_set())
|
|
251
|
+
if future.done():
|
|
252
|
+
break
|
|
253
|
+
await asyncio.sleep(max(0, min(0.005, deadline - time.monotonic())))
|
|
254
|
+
return future.result()
|
|
255
|
+
except asyncio.CancelledError:
|
|
256
|
+
self._context.tick(future, True)
|
|
257
|
+
raise # Preserve application cancellation, never turn it into a model call.
|
|
258
|
+
|
|
259
|
+
def release_trace(self, trace_id: str):
|
|
260
|
+
if os.getpid() != self._pid or type(trace_id) is not str:
|
|
261
|
+
return
|
|
262
|
+
try:
|
|
263
|
+
Wire.id(trace_id)
|
|
264
|
+
except ValueError:
|
|
265
|
+
return
|
|
266
|
+
with self._lock:
|
|
267
|
+
self._context.release(trace_id)
|
|
268
|
+
self._traces.pop(trace_id, None)
|
|
269
|
+
if trace_id not in self._recent:
|
|
270
|
+
self._remember(trace_id, None)
|
|
271
|
+
self._deliveries.cancel("canceled", trace_id)
|
|
272
|
+
if self._worker:
|
|
273
|
+
self._worker.release(trace_id)
|
|
274
|
+
|
|
275
|
+
def set_enabled(self, enabled: bool):
|
|
276
|
+
if os.getpid() != self._pid or type(enabled) is not bool:
|
|
277
|
+
return
|
|
278
|
+
with self._lock:
|
|
279
|
+
if self._closed or self._enabled == enabled:
|
|
280
|
+
return
|
|
281
|
+
self._enabled = enabled
|
|
282
|
+
self._context.set_enabled(enabled)
|
|
283
|
+
if not enabled:
|
|
284
|
+
for trace_id in list(self._traces):
|
|
285
|
+
self.release_trace(trace_id)
|
|
286
|
+
self._deliveries.cancel("disabled")
|
|
287
|
+
if self._worker:
|
|
288
|
+
self._worker.set_enabled(enabled)
|
|
289
|
+
|
|
290
|
+
def flush(self, *, timeout_ms: int = 2000):
|
|
291
|
+
if os.getpid() != self._pid:
|
|
292
|
+
return FlushResult(unconfirmed=1)
|
|
293
|
+
return self._deliveries.flush(timeout_ms)
|
|
294
|
+
|
|
295
|
+
async def aflush(self, *, timeout_ms: int = 2000):
|
|
296
|
+
if os.getpid() != self._pid:
|
|
297
|
+
return FlushResult(unconfirmed=1)
|
|
298
|
+
return await self._deliveries.flush_async(timeout_ms)
|
|
299
|
+
|
|
300
|
+
def close(self, *, timeout_ms: int = 2000):
|
|
301
|
+
if os.getpid() != self._pid:
|
|
302
|
+
return FlushResult(unconfirmed=1)
|
|
303
|
+
with self._lock:
|
|
304
|
+
self._closed = True
|
|
305
|
+
self._context.stop()
|
|
306
|
+
result = self.flush(timeout_ms=timeout_ms)
|
|
307
|
+
self.dispose()
|
|
308
|
+
return result
|
|
309
|
+
|
|
310
|
+
def dispose(self):
|
|
311
|
+
if os.getpid() != self._pid:
|
|
312
|
+
return
|
|
313
|
+
with self._lock:
|
|
314
|
+
self._closed = True
|
|
315
|
+
self._context.stop()
|
|
316
|
+
self._traces.clear()
|
|
317
|
+
self._recent.clear()
|
|
318
|
+
self._deliveries.cancel("closed")
|
|
319
|
+
if self._worker:
|
|
320
|
+
self._worker.dispose()
|
|
321
|
+
|
|
322
|
+
def _worker_failed(self):
|
|
323
|
+
self._failed = True
|
|
324
|
+
self._context.stop("worker_failed")
|
|
325
|
+
self._deliveries.cancel("worker_failed")
|
|
326
|
+
|
|
327
|
+
def diagnostics(self):
|
|
328
|
+
if os.getpid() != self._pid:
|
|
329
|
+
return {"reason": "forked_process"}
|
|
330
|
+
with self._lock:
|
|
331
|
+
return {
|
|
332
|
+
**self._deliveries.diagnostics(),
|
|
333
|
+
"active_executions": len(self._traces),
|
|
334
|
+
"recent_executions": len(self._recent),
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
@staticmethod
|
|
338
|
+
def start_turn() -> TurnTimer:
|
|
339
|
+
return TurnTimer()
|