simplicio-prompt 0.1.0
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.
- package/LICENSE +15 -0
- package/README.md +173 -0
- package/YOOL_TUPLE_HAMT.md +1149 -0
- package/adopters.md +24 -0
- package/benchmarks/generate_prompt_benchmark_pdf.py +355 -0
- package/benchmarks/generate_v2_benchmark_pdf.py +302 -0
- package/benchmarks/prompt_vs_normal.py +431 -0
- package/benchmarks/prompt_vs_normal_benchmark.pdf +124 -0
- package/benchmarks/prompt_vs_normal_results.md +148 -0
- package/benchmarks/v2_safe_speed_benchmark.pdf +118 -0
- package/benchmarks/v2_safe_speed_benchmark.py +626 -0
- package/benchmarks/v2_safe_speed_results.json +446 -0
- package/benchmarks/v2_safe_speed_results.md +96 -0
- package/docs/assets/simplicio-prompt-hero.png +0 -0
- package/docs/assets/yool-v2-safe-speed-infographic-en.png +0 -0
- package/docs/assets/yool-v2-safe-speed-infographic-pt.png +0 -0
- package/examples/node/build-catalog.mjs +70 -0
- package/examples/python/minimal_bus.py +134 -0
- package/examples/python/receipts.py +152 -0
- package/guardrails/cpu_throttle.py +119 -0
- package/guardrails/disk_gc.py +212 -0
- package/kernel/README.md +82 -0
- package/kernel/yool_tuple_kernel.py +1109 -0
- package/kernel-implementation-request.md +38 -0
- package/package.json +40 -0
- package/prompts/agent-runtime-execution-prompt.md +119 -0
- package/prompts/legacy-tuple-space-engine-prompt.md +36 -0
|
@@ -0,0 +1,1109 @@
|
|
|
1
|
+
"""Reference Tuple-Space + Yool kernel.
|
|
2
|
+
|
|
3
|
+
The kernel keeps the repository intentionally dependency-free while showing the
|
|
4
|
+
production shape expected by the spec:
|
|
5
|
+
|
|
6
|
+
- Linda-style tuple-space primitives: out_tuple, in_tuple, rd_tuple.
|
|
7
|
+
- Capability-addressed yools with Hilbert-like tuple map positions.
|
|
8
|
+
- Hookwall capability checks.
|
|
9
|
+
- Hierarchical batch_spawn for 1,000,000+ virtual subagents without a flat list.
|
|
10
|
+
- compress_token/pruning for inactive materialized agents.
|
|
11
|
+
- Lane worker fan-out controlled by environment-driven runtime policy.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import random
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
import weakref
|
|
23
|
+
from collections import OrderedDict, defaultdict
|
|
24
|
+
from concurrent.futures import Future, ThreadPoolExecutor, as_completed
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import Any, Callable, DefaultDict, Dict, Iterable, List, Optional, Tuple
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
DEFAULT_LANE_CONCURRENCY = 32
|
|
30
|
+
DEFAULT_MAX_LANE_CONCURRENCY = 64
|
|
31
|
+
DEFAULT_CPU_QUOTA_PCT = 95
|
|
32
|
+
DEFAULT_QUEUE_MAXSIZE = 8192
|
|
33
|
+
DEFAULT_COMPRESSION_THRESHOLD = 1024
|
|
34
|
+
DEFAULT_CACHE_MAX_ENTRIES = 16384
|
|
35
|
+
DEFAULT_CACHE_TTL_S = 3600
|
|
36
|
+
DEFAULT_API_MAX_RETRIES = 3
|
|
37
|
+
DEFAULT_API_BACKOFF_BASE_MS = 100
|
|
38
|
+
DEFAULT_API_BACKOFF_MAX_MS = 5000
|
|
39
|
+
DEFAULT_CIRCUIT_FAILURE_THRESHOLD = 5
|
|
40
|
+
DEFAULT_CIRCUIT_COOLDOWN_S = 30
|
|
41
|
+
DEFAULT_BATCH_SMALL_TASK_SIZE = 32
|
|
42
|
+
DEFAULT_CONTEXT_COMPRESSION_CHARS = 6000
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _positive_int_from_env(names: Iterable[str], default: int) -> int:
|
|
46
|
+
for name in names:
|
|
47
|
+
value = os.getenv(name)
|
|
48
|
+
if value is None or not value.strip():
|
|
49
|
+
continue
|
|
50
|
+
try:
|
|
51
|
+
parsed = int(value)
|
|
52
|
+
except ValueError:
|
|
53
|
+
return default
|
|
54
|
+
return parsed if parsed > 0 else default
|
|
55
|
+
return default
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _positive_float_from_env(names: Iterable[str], default: float) -> float:
|
|
59
|
+
for name in names:
|
|
60
|
+
value = os.getenv(name)
|
|
61
|
+
if value is None or not value.strip():
|
|
62
|
+
continue
|
|
63
|
+
try:
|
|
64
|
+
parsed = float(value)
|
|
65
|
+
except ValueError:
|
|
66
|
+
return default
|
|
67
|
+
return parsed if parsed > 0 else default
|
|
68
|
+
return default
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class RuntimePolicy:
|
|
73
|
+
"""Host-aware runtime policy, with env aliases for adoption in agents/IDEs."""
|
|
74
|
+
|
|
75
|
+
lane_concurrency: int = DEFAULT_LANE_CONCURRENCY
|
|
76
|
+
max_lane_concurrency: int = DEFAULT_MAX_LANE_CONCURRENCY
|
|
77
|
+
cpu_quota_pct: int = DEFAULT_CPU_QUOTA_PCT
|
|
78
|
+
queue_maxsize: int = DEFAULT_QUEUE_MAXSIZE
|
|
79
|
+
compression_threshold: int = DEFAULT_COMPRESSION_THRESHOLD
|
|
80
|
+
cache_max_entries: int = DEFAULT_CACHE_MAX_ENTRIES
|
|
81
|
+
cache_ttl_s: float = DEFAULT_CACHE_TTL_S
|
|
82
|
+
api_max_retries: int = DEFAULT_API_MAX_RETRIES
|
|
83
|
+
api_backoff_base_ms: int = DEFAULT_API_BACKOFF_BASE_MS
|
|
84
|
+
api_backoff_max_ms: int = DEFAULT_API_BACKOFF_MAX_MS
|
|
85
|
+
circuit_failure_threshold: int = DEFAULT_CIRCUIT_FAILURE_THRESHOLD
|
|
86
|
+
circuit_cooldown_s: float = DEFAULT_CIRCUIT_COOLDOWN_S
|
|
87
|
+
batch_small_task_size: int = DEFAULT_BATCH_SMALL_TASK_SIZE
|
|
88
|
+
context_compression_chars: int = DEFAULT_CONTEXT_COMPRESSION_CHARS
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_env(cls) -> "RuntimePolicy":
|
|
92
|
+
requested = _positive_int_from_env(
|
|
93
|
+
("YOOL_TUPLE_LANE_CONCURRENCY", "YOOL_LANE_CONCURRENCY"),
|
|
94
|
+
DEFAULT_LANE_CONCURRENCY,
|
|
95
|
+
)
|
|
96
|
+
max_lane = _positive_int_from_env(
|
|
97
|
+
("YOOL_TUPLE_MAX_LANE_CONCURRENCY", "YOOL_MAX_LANE_CONCURRENCY"),
|
|
98
|
+
DEFAULT_MAX_LANE_CONCURRENCY,
|
|
99
|
+
)
|
|
100
|
+
cpu_quota = _positive_int_from_env(
|
|
101
|
+
("YOOL_TUPLE_CPU_QUOTA_PCT", "YOOL_CPU_QUOTA_PCT"),
|
|
102
|
+
DEFAULT_CPU_QUOTA_PCT,
|
|
103
|
+
)
|
|
104
|
+
queue_size = _positive_int_from_env(
|
|
105
|
+
("YOOL_TUPLE_QUEUE_MAXSIZE", "YOOL_QUEUE_MAXSIZE"),
|
|
106
|
+
DEFAULT_QUEUE_MAXSIZE,
|
|
107
|
+
)
|
|
108
|
+
compression = _positive_int_from_env(
|
|
109
|
+
("YOOL_TUPLE_COMPRESSION_THRESHOLD", "YOOL_COMPRESSION_THRESHOLD"),
|
|
110
|
+
DEFAULT_COMPRESSION_THRESHOLD,
|
|
111
|
+
)
|
|
112
|
+
cache_max_entries = _positive_int_from_env(
|
|
113
|
+
("YOOL_TUPLE_CACHE_MAX_ENTRIES", "YOOL_CACHE_MAX_ENTRIES"),
|
|
114
|
+
DEFAULT_CACHE_MAX_ENTRIES,
|
|
115
|
+
)
|
|
116
|
+
cache_ttl = _positive_float_from_env(
|
|
117
|
+
("YOOL_TUPLE_CACHE_TTL_S", "YOOL_CACHE_TTL_S"),
|
|
118
|
+
DEFAULT_CACHE_TTL_S,
|
|
119
|
+
)
|
|
120
|
+
api_max_retries = _positive_int_from_env(
|
|
121
|
+
("YOOL_TUPLE_API_MAX_RETRIES", "YOOL_API_MAX_RETRIES"),
|
|
122
|
+
DEFAULT_API_MAX_RETRIES,
|
|
123
|
+
)
|
|
124
|
+
backoff_base = _positive_int_from_env(
|
|
125
|
+
("YOOL_TUPLE_API_BACKOFF_BASE_MS", "YOOL_API_BACKOFF_BASE_MS"),
|
|
126
|
+
DEFAULT_API_BACKOFF_BASE_MS,
|
|
127
|
+
)
|
|
128
|
+
backoff_max = _positive_int_from_env(
|
|
129
|
+
("YOOL_TUPLE_API_BACKOFF_MAX_MS", "YOOL_API_BACKOFF_MAX_MS"),
|
|
130
|
+
DEFAULT_API_BACKOFF_MAX_MS,
|
|
131
|
+
)
|
|
132
|
+
circuit_threshold = _positive_int_from_env(
|
|
133
|
+
("YOOL_TUPLE_CIRCUIT_FAILURE_THRESHOLD", "YOOL_CIRCUIT_FAILURE_THRESHOLD"),
|
|
134
|
+
DEFAULT_CIRCUIT_FAILURE_THRESHOLD,
|
|
135
|
+
)
|
|
136
|
+
circuit_cooldown = _positive_float_from_env(
|
|
137
|
+
("YOOL_TUPLE_CIRCUIT_COOLDOWN_S", "YOOL_CIRCUIT_COOLDOWN_S"),
|
|
138
|
+
DEFAULT_CIRCUIT_COOLDOWN_S,
|
|
139
|
+
)
|
|
140
|
+
batch_size = _positive_int_from_env(
|
|
141
|
+
("YOOL_TUPLE_BATCH_SMALL_TASK_SIZE", "YOOL_BATCH_SMALL_TASK_SIZE"),
|
|
142
|
+
DEFAULT_BATCH_SMALL_TASK_SIZE,
|
|
143
|
+
)
|
|
144
|
+
context_chars = _positive_int_from_env(
|
|
145
|
+
("YOOL_TUPLE_CONTEXT_COMPRESSION_CHARS", "YOOL_CONTEXT_COMPRESSION_CHARS"),
|
|
146
|
+
DEFAULT_CONTEXT_COMPRESSION_CHARS,
|
|
147
|
+
)
|
|
148
|
+
return cls(
|
|
149
|
+
lane_concurrency=requested,
|
|
150
|
+
max_lane_concurrency=max(1, max_lane),
|
|
151
|
+
cpu_quota_pct=max(1, min(100, cpu_quota)),
|
|
152
|
+
queue_maxsize=max(1, queue_size),
|
|
153
|
+
compression_threshold=max(1, compression),
|
|
154
|
+
cache_max_entries=max(1, cache_max_entries),
|
|
155
|
+
cache_ttl_s=max(0.001, cache_ttl),
|
|
156
|
+
api_max_retries=max(0, api_max_retries),
|
|
157
|
+
api_backoff_base_ms=max(1, backoff_base),
|
|
158
|
+
api_backoff_max_ms=max(1, backoff_max),
|
|
159
|
+
circuit_failure_threshold=max(1, circuit_threshold),
|
|
160
|
+
circuit_cooldown_s=max(0.001, circuit_cooldown),
|
|
161
|
+
batch_small_task_size=max(1, batch_size),
|
|
162
|
+
context_compression_chars=max(64, context_chars),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def concurrency_for(
|
|
166
|
+
self,
|
|
167
|
+
queued_roots: int,
|
|
168
|
+
*,
|
|
169
|
+
ewma_latency_ms: float | None = None,
|
|
170
|
+
error_rate: float = 0.0,
|
|
171
|
+
) -> int:
|
|
172
|
+
requested = self.lane_concurrency
|
|
173
|
+
if requested <= 0:
|
|
174
|
+
requested = min(max(1, queued_roots), max(1, os.cpu_count() or 1))
|
|
175
|
+
ceiling = max(1, min(self.max_lane_concurrency, queued_roots or 1))
|
|
176
|
+
concurrency = max(1, min(requested, ceiling))
|
|
177
|
+
if queued_roots > requested * 4:
|
|
178
|
+
concurrency = min(ceiling, max(concurrency, requested * 2))
|
|
179
|
+
if ewma_latency_ms is not None and ewma_latency_ms > 250:
|
|
180
|
+
concurrency = min(ceiling, max(concurrency, concurrency * 2))
|
|
181
|
+
if error_rate >= 0.2:
|
|
182
|
+
concurrency = max(1, concurrency // 2)
|
|
183
|
+
return max(1, concurrency)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class HilbertIndex:
|
|
187
|
+
"""Simplified Hilbert-style index: tuple multi-dim -> stable hashable path."""
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def compute(keys: Tuple[Any, ...]) -> Tuple[Any, ...]:
|
|
191
|
+
return keys
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class YoolTuple:
|
|
195
|
+
"""Tuple envelope: yool + map + authority + lane + source pointers + receipts."""
|
|
196
|
+
|
|
197
|
+
def __init__(
|
|
198
|
+
self,
|
|
199
|
+
yool: str,
|
|
200
|
+
map_index: Tuple[Any, ...],
|
|
201
|
+
authority: str,
|
|
202
|
+
lane: str,
|
|
203
|
+
source: str,
|
|
204
|
+
data: Optional[Dict[str, Any]] = None,
|
|
205
|
+
*,
|
|
206
|
+
parent_id: int | None = None,
|
|
207
|
+
agent_id: int | None = None,
|
|
208
|
+
) -> None:
|
|
209
|
+
self.id = agent_id
|
|
210
|
+
self.yool = yool
|
|
211
|
+
self.map = map_index
|
|
212
|
+
self.authority = authority
|
|
213
|
+
self.lane = lane
|
|
214
|
+
self.source = source
|
|
215
|
+
self.parent_id = parent_id
|
|
216
|
+
self.receipts: List[str] = []
|
|
217
|
+
self.data = data or {}
|
|
218
|
+
self.last_active = time.monotonic()
|
|
219
|
+
|
|
220
|
+
def touch(self) -> None:
|
|
221
|
+
self.last_active = time.monotonic()
|
|
222
|
+
|
|
223
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
224
|
+
return {
|
|
225
|
+
"id": self.id,
|
|
226
|
+
"yool": self.yool,
|
|
227
|
+
"map": list(self.map),
|
|
228
|
+
"authority": self.authority,
|
|
229
|
+
"lane": self.lane,
|
|
230
|
+
"source": self.source,
|
|
231
|
+
"parent_id": self.parent_id,
|
|
232
|
+
"receipts": list(self.receipts),
|
|
233
|
+
"data": dict(self.data),
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@dataclass
|
|
238
|
+
class CompressToken:
|
|
239
|
+
"""Compact inactive agent state; enough to inspect or lazily restore later."""
|
|
240
|
+
|
|
241
|
+
agent_id: int
|
|
242
|
+
yool: str
|
|
243
|
+
map_index: Tuple[Any, ...]
|
|
244
|
+
authority: str
|
|
245
|
+
lane: str
|
|
246
|
+
source: str
|
|
247
|
+
parent_id: int | None
|
|
248
|
+
receipts: List[str] = field(default_factory=list)
|
|
249
|
+
data_digest: str = ""
|
|
250
|
+
compressed_at: float = field(default_factory=time.monotonic)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@dataclass(frozen=True)
|
|
254
|
+
class BatchSpawnReceipt:
|
|
255
|
+
root_agent_id: int
|
|
256
|
+
depth: int
|
|
257
|
+
branching: int
|
|
258
|
+
virtual_agents: int
|
|
259
|
+
compression_threshold: int
|
|
260
|
+
receipt_id: str
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class CircuitOpenError(RuntimeError):
|
|
264
|
+
"""Raised when a provider is cooling down after repeated transient failures."""
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
Executor = Callable[[YoolTuple], Any]
|
|
268
|
+
BatchExecutor = Callable[[List[YoolTuple]], Any]
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _stable_digest(value: Any) -> str:
|
|
272
|
+
raw = json.dumps(value, sort_keys=True, separators=(",", ":"), default=repr)
|
|
273
|
+
return hashlib.blake2b(raw.encode("utf-8"), digest_size=16).hexdigest()
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@dataclass
|
|
277
|
+
class CacheEntry:
|
|
278
|
+
value: Any
|
|
279
|
+
created_at: float = field(default_factory=time.monotonic)
|
|
280
|
+
hits: int = 0
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class ReceiptCache:
|
|
284
|
+
"""Small LRU+TTL cache keyed by tuple receipt and deterministic input hashes."""
|
|
285
|
+
|
|
286
|
+
def __init__(self, *, max_entries: int, ttl_s: float) -> None:
|
|
287
|
+
self.max_entries = max(1, max_entries)
|
|
288
|
+
self.ttl_s = max(0.001, ttl_s)
|
|
289
|
+
self._items: OrderedDict[str, CacheEntry] = OrderedDict()
|
|
290
|
+
self._lock = threading.RLock()
|
|
291
|
+
|
|
292
|
+
def get(self, keys: Iterable[str]) -> tuple[bool, Any, str | None]:
|
|
293
|
+
now = time.monotonic()
|
|
294
|
+
with self._lock:
|
|
295
|
+
for key in keys:
|
|
296
|
+
item = self._items.get(key)
|
|
297
|
+
if item is None:
|
|
298
|
+
continue
|
|
299
|
+
if now - item.created_at > self.ttl_s:
|
|
300
|
+
self._items.pop(key, None)
|
|
301
|
+
continue
|
|
302
|
+
item.hits += 1
|
|
303
|
+
self._items.move_to_end(key)
|
|
304
|
+
return True, item.value, key
|
|
305
|
+
return False, None, None
|
|
306
|
+
|
|
307
|
+
def set(self, keys: Iterable[str], value: Any) -> None:
|
|
308
|
+
now = time.monotonic()
|
|
309
|
+
with self._lock:
|
|
310
|
+
for key in keys:
|
|
311
|
+
self._items[key] = CacheEntry(value=value, created_at=now)
|
|
312
|
+
self._items.move_to_end(key)
|
|
313
|
+
while len(self._items) > self.max_entries:
|
|
314
|
+
self._items.popitem(last=False)
|
|
315
|
+
|
|
316
|
+
def snapshot(self) -> Dict[str, Any]:
|
|
317
|
+
with self._lock:
|
|
318
|
+
return {
|
|
319
|
+
"entries": len(self._items),
|
|
320
|
+
"max_entries": self.max_entries,
|
|
321
|
+
"ttl_s": self.ttl_s,
|
|
322
|
+
"hits": sum(item.hits for item in self._items.values()),
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@dataclass(frozen=True)
|
|
327
|
+
class BackoffPolicy:
|
|
328
|
+
max_retries: int
|
|
329
|
+
base_ms: int
|
|
330
|
+
max_ms: int
|
|
331
|
+
jitter_ratio: float = 0.25
|
|
332
|
+
|
|
333
|
+
@classmethod
|
|
334
|
+
def from_runtime(cls, policy: RuntimePolicy) -> "BackoffPolicy":
|
|
335
|
+
return cls(
|
|
336
|
+
max_retries=policy.api_max_retries,
|
|
337
|
+
base_ms=policy.api_backoff_base_ms,
|
|
338
|
+
max_ms=policy.api_backoff_max_ms,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def delay_s(self, attempt: int) -> float:
|
|
342
|
+
capped_ms = min(self.max_ms, self.base_ms * (2**attempt))
|
|
343
|
+
jitter = 1 + random.uniform(-self.jitter_ratio, self.jitter_ratio)
|
|
344
|
+
return max(0.0, capped_ms * jitter / 1000)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@dataclass
|
|
348
|
+
class CircuitState:
|
|
349
|
+
failures: int = 0
|
|
350
|
+
opened_until: float = 0.0
|
|
351
|
+
successes: int = 0
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class ProviderCircuitBreaker:
|
|
355
|
+
"""Provider-level circuit breaker to avoid hammering APIs/LLMs during faults."""
|
|
356
|
+
|
|
357
|
+
def __init__(self, *, failure_threshold: int, cooldown_s: float) -> None:
|
|
358
|
+
self.failure_threshold = max(1, failure_threshold)
|
|
359
|
+
self.cooldown_s = max(0.001, cooldown_s)
|
|
360
|
+
self._states: DefaultDict[str, CircuitState] = defaultdict(CircuitState)
|
|
361
|
+
self._lock = threading.RLock()
|
|
362
|
+
|
|
363
|
+
def before_call(self, provider: str) -> None:
|
|
364
|
+
with self._lock:
|
|
365
|
+
state = self._states[provider]
|
|
366
|
+
if state.opened_until > time.monotonic():
|
|
367
|
+
remaining = state.opened_until - time.monotonic()
|
|
368
|
+
raise CircuitOpenError(
|
|
369
|
+
f"circuit open for provider {provider!r}; retry after {remaining:.2f}s"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def record_success(self, provider: str) -> None:
|
|
373
|
+
with self._lock:
|
|
374
|
+
state = self._states[provider]
|
|
375
|
+
state.failures = 0
|
|
376
|
+
state.opened_until = 0.0
|
|
377
|
+
state.successes += 1
|
|
378
|
+
|
|
379
|
+
def record_failure(self, provider: str) -> None:
|
|
380
|
+
with self._lock:
|
|
381
|
+
state = self._states[provider]
|
|
382
|
+
state.failures += 1
|
|
383
|
+
if state.failures >= self.failure_threshold:
|
|
384
|
+
state.opened_until = time.monotonic() + self.cooldown_s
|
|
385
|
+
|
|
386
|
+
def snapshot(self) -> Dict[str, Any]:
|
|
387
|
+
with self._lock:
|
|
388
|
+
return {
|
|
389
|
+
provider: {
|
|
390
|
+
"failures": state.failures,
|
|
391
|
+
"successes": state.successes,
|
|
392
|
+
"open": state.opened_until > time.monotonic(),
|
|
393
|
+
}
|
|
394
|
+
for provider, state in sorted(self._states.items())
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@dataclass
|
|
399
|
+
class LaneMetrics:
|
|
400
|
+
successes: int = 0
|
|
401
|
+
failures: int = 0
|
|
402
|
+
ewma_latency_ms: float = 0.0
|
|
403
|
+
|
|
404
|
+
def record(self, elapsed_ms: float, *, ok: bool) -> None:
|
|
405
|
+
if self.ewma_latency_ms <= 0:
|
|
406
|
+
self.ewma_latency_ms = elapsed_ms
|
|
407
|
+
else:
|
|
408
|
+
self.ewma_latency_ms = (self.ewma_latency_ms * 0.8) + (elapsed_ms * 0.2)
|
|
409
|
+
if ok:
|
|
410
|
+
self.successes += 1
|
|
411
|
+
else:
|
|
412
|
+
self.failures += 1
|
|
413
|
+
|
|
414
|
+
@property
|
|
415
|
+
def error_rate(self) -> float:
|
|
416
|
+
total = self.successes + self.failures
|
|
417
|
+
return 0.0 if total == 0 else self.failures / total
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
class ContextCompressor:
|
|
421
|
+
"""Digest-preserving compression for large LLM prompt/context payloads."""
|
|
422
|
+
|
|
423
|
+
def __init__(self, *, threshold_chars: int) -> None:
|
|
424
|
+
self.threshold_chars = max(64, threshold_chars)
|
|
425
|
+
|
|
426
|
+
def compress(self, data: Dict[str, Any]) -> tuple[Dict[str, Any], bool]:
|
|
427
|
+
changed = False
|
|
428
|
+
compressed: Dict[str, Any] = {}
|
|
429
|
+
for key, value in data.items():
|
|
430
|
+
new_value, item_changed = self._compress_value(value)
|
|
431
|
+
compressed[key] = new_value
|
|
432
|
+
changed = changed or item_changed
|
|
433
|
+
return compressed, changed
|
|
434
|
+
|
|
435
|
+
def _compress_value(self, value: Any) -> tuple[Any, bool]:
|
|
436
|
+
if isinstance(value, str) and len(value) > self.threshold_chars:
|
|
437
|
+
head = value[: self.threshold_chars // 2]
|
|
438
|
+
tail = value[-self.threshold_chars // 4 :]
|
|
439
|
+
return (
|
|
440
|
+
{
|
|
441
|
+
"compressed": True,
|
|
442
|
+
"encoding": "sha256+head_tail",
|
|
443
|
+
"digest": hashlib.sha256(value.encode("utf-8")).hexdigest(),
|
|
444
|
+
"original_chars": len(value),
|
|
445
|
+
"preview": f"{head}\n...[compressed]...\n{tail}",
|
|
446
|
+
},
|
|
447
|
+
True,
|
|
448
|
+
)
|
|
449
|
+
if isinstance(value, list):
|
|
450
|
+
changed = False
|
|
451
|
+
out = []
|
|
452
|
+
for item in value:
|
|
453
|
+
new_item, item_changed = self._compress_value(item)
|
|
454
|
+
out.append(new_item)
|
|
455
|
+
changed = changed or item_changed
|
|
456
|
+
return out, changed
|
|
457
|
+
if isinstance(value, dict):
|
|
458
|
+
compressor = ContextCompressor(threshold_chars=self.threshold_chars)
|
|
459
|
+
out, changed = compressor.compress(value)
|
|
460
|
+
return out, changed
|
|
461
|
+
return value, False
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
class TupleSpace:
|
|
465
|
+
"""Thread-safe tuple space with indexed lanes and lazy hierarchical agents."""
|
|
466
|
+
|
|
467
|
+
def __init__(self, *, policy: RuntimePolicy | None = None) -> None:
|
|
468
|
+
self.policy = policy or RuntimePolicy.from_env()
|
|
469
|
+
self.space: Dict[Tuple[Any, ...], List[YoolTuple]] = defaultdict(list)
|
|
470
|
+
self.lane_index: DefaultDict[str, List[YoolTuple]] = defaultdict(list)
|
|
471
|
+
self.agents: weakref.WeakValueDictionary[int, YoolTuple] = (
|
|
472
|
+
weakref.WeakValueDictionary()
|
|
473
|
+
)
|
|
474
|
+
self.walls: DefaultDict[str, List[str]] = defaultdict(list)
|
|
475
|
+
self.compressed_agents: Dict[int, CompressToken] = {}
|
|
476
|
+
self.receipt_cache = ReceiptCache(
|
|
477
|
+
max_entries=self.policy.cache_max_entries,
|
|
478
|
+
ttl_s=self.policy.cache_ttl_s,
|
|
479
|
+
)
|
|
480
|
+
self.circuit_breaker = ProviderCircuitBreaker(
|
|
481
|
+
failure_threshold=self.policy.circuit_failure_threshold,
|
|
482
|
+
cooldown_s=self.policy.circuit_cooldown_s,
|
|
483
|
+
)
|
|
484
|
+
self.context_compressor = ContextCompressor(
|
|
485
|
+
threshold_chars=self.policy.context_compression_chars
|
|
486
|
+
)
|
|
487
|
+
self.local_yools: Dict[str, Executor] = {}
|
|
488
|
+
self.virtual_agent_count = 0
|
|
489
|
+
self._next_agent_id = 0
|
|
490
|
+
self._lock = threading.RLock()
|
|
491
|
+
self._cond = threading.Condition(self._lock)
|
|
492
|
+
|
|
493
|
+
def out_tuple(self, t: YoolTuple) -> None:
|
|
494
|
+
with self._cond:
|
|
495
|
+
self.space[t.map].append(t)
|
|
496
|
+
self.lane_index[t.lane].append(t)
|
|
497
|
+
t.receipts.append(
|
|
498
|
+
f"out@{self._receipt_hash(t.map, t.yool, len(t.receipts))}"
|
|
499
|
+
)
|
|
500
|
+
t.touch()
|
|
501
|
+
self._cond.notify_all()
|
|
502
|
+
|
|
503
|
+
def in_tuple(
|
|
504
|
+
self,
|
|
505
|
+
template: Dict[str, Any],
|
|
506
|
+
*,
|
|
507
|
+
timeout_s: float | None = None,
|
|
508
|
+
) -> Optional[YoolTuple]:
|
|
509
|
+
deadline = time.monotonic() + timeout_s if timeout_s is not None else None
|
|
510
|
+
with self._cond:
|
|
511
|
+
while True:
|
|
512
|
+
match = self._find_match(template)
|
|
513
|
+
if match is not None:
|
|
514
|
+
self._remove_tuple(match)
|
|
515
|
+
match.touch()
|
|
516
|
+
return match
|
|
517
|
+
if timeout_s is None:
|
|
518
|
+
return None
|
|
519
|
+
remaining = deadline - time.monotonic() if deadline is not None else 0
|
|
520
|
+
if remaining <= 0:
|
|
521
|
+
return None
|
|
522
|
+
self._cond.wait(timeout=remaining)
|
|
523
|
+
|
|
524
|
+
def rd_tuple(self, template: Dict[str, Any]) -> Optional[YoolTuple]:
|
|
525
|
+
with self._lock:
|
|
526
|
+
match = self._find_match(template)
|
|
527
|
+
if match is not None:
|
|
528
|
+
match.touch()
|
|
529
|
+
return match
|
|
530
|
+
|
|
531
|
+
def register_local_yool(self, yool: str, executor: Executor) -> None:
|
|
532
|
+
"""Register deterministic local work to avoid unnecessary LLM/API calls."""
|
|
533
|
+
with self._lock:
|
|
534
|
+
self.local_yools[yool] = executor
|
|
535
|
+
|
|
536
|
+
def execute_tuple(
|
|
537
|
+
self,
|
|
538
|
+
tup: YoolTuple,
|
|
539
|
+
executor: Executor,
|
|
540
|
+
*,
|
|
541
|
+
provider: str | None = None,
|
|
542
|
+
use_cache: bool = True,
|
|
543
|
+
retryable: Callable[[BaseException], bool] | None = None,
|
|
544
|
+
sleep_fn: Callable[[float], None] = time.sleep,
|
|
545
|
+
) -> Any:
|
|
546
|
+
"""Execute one tuple through local routing, compression, cache, and guardrails."""
|
|
547
|
+
local_executor = self.local_yools.get(tup.yool)
|
|
548
|
+
if local_executor is not None:
|
|
549
|
+
tup.receipts.append(f"local_route@{self._receipt_hash(tup.yool, tup.map)}")
|
|
550
|
+
return local_executor(tup)
|
|
551
|
+
|
|
552
|
+
if self._should_compress_context(tup):
|
|
553
|
+
self.compress_context(tup)
|
|
554
|
+
|
|
555
|
+
cache_keys = self.cache_keys_for_tuple(tup)
|
|
556
|
+
if use_cache:
|
|
557
|
+
hit, cached, key = self.receipt_cache.get(cache_keys)
|
|
558
|
+
if hit:
|
|
559
|
+
tup.receipts.append(f"cache_hit@{key}")
|
|
560
|
+
return cached
|
|
561
|
+
|
|
562
|
+
provider_name = provider or str(tup.data.get("provider") or "local")
|
|
563
|
+
|
|
564
|
+
def run() -> Any:
|
|
565
|
+
return executor(tup)
|
|
566
|
+
|
|
567
|
+
if provider_name == "local":
|
|
568
|
+
result = run()
|
|
569
|
+
else:
|
|
570
|
+
result = self.call_with_backoff(
|
|
571
|
+
provider_name,
|
|
572
|
+
run,
|
|
573
|
+
retryable=retryable,
|
|
574
|
+
sleep_fn=sleep_fn,
|
|
575
|
+
)
|
|
576
|
+
if use_cache:
|
|
577
|
+
self.receipt_cache.set(cache_keys, result)
|
|
578
|
+
tup.receipts.append(
|
|
579
|
+
f"cache_store@{self._receipt_hash(provider_name, cache_keys)}"
|
|
580
|
+
)
|
|
581
|
+
return result
|
|
582
|
+
|
|
583
|
+
def call_with_backoff(
|
|
584
|
+
self,
|
|
585
|
+
provider: str,
|
|
586
|
+
fn: Callable[[], Any],
|
|
587
|
+
*,
|
|
588
|
+
retryable: Callable[[BaseException], bool] | None = None,
|
|
589
|
+
sleep_fn: Callable[[float], None] = time.sleep,
|
|
590
|
+
) -> Any:
|
|
591
|
+
"""Call APIs/LLMs with jittered exponential backoff and provider breaker."""
|
|
592
|
+
policy = BackoffPolicy.from_runtime(self.policy)
|
|
593
|
+
retryable = retryable or self._default_retryable
|
|
594
|
+
attempt = 0
|
|
595
|
+
while True:
|
|
596
|
+
self.circuit_breaker.before_call(provider)
|
|
597
|
+
try:
|
|
598
|
+
result = fn()
|
|
599
|
+
except BaseException as exc:
|
|
600
|
+
self.circuit_breaker.record_failure(provider)
|
|
601
|
+
if attempt >= policy.max_retries or not retryable(exc):
|
|
602
|
+
raise
|
|
603
|
+
sleep_fn(policy.delay_s(attempt))
|
|
604
|
+
attempt += 1
|
|
605
|
+
continue
|
|
606
|
+
self.circuit_breaker.record_success(provider)
|
|
607
|
+
return result
|
|
608
|
+
|
|
609
|
+
def compress_context(
|
|
610
|
+
self, tup: YoolTuple, *, threshold_chars: int | None = None
|
|
611
|
+
) -> bool:
|
|
612
|
+
compressor = (
|
|
613
|
+
self.context_compressor
|
|
614
|
+
if threshold_chars is None
|
|
615
|
+
else ContextCompressor(threshold_chars=threshold_chars)
|
|
616
|
+
)
|
|
617
|
+
compressed, changed = compressor.compress(tup.data)
|
|
618
|
+
if changed:
|
|
619
|
+
tup.data = compressed
|
|
620
|
+
tup.receipts.append(
|
|
621
|
+
f"compress_context@{self._receipt_hash(tup.yool, tup.map, tup.data)}"
|
|
622
|
+
)
|
|
623
|
+
tup.touch()
|
|
624
|
+
return changed
|
|
625
|
+
|
|
626
|
+
def cache_keys_for_tuple(self, tup: YoolTuple) -> List[str]:
|
|
627
|
+
input_key = _stable_digest(
|
|
628
|
+
{
|
|
629
|
+
"yool": tup.yool,
|
|
630
|
+
"data": tup.data,
|
|
631
|
+
}
|
|
632
|
+
)
|
|
633
|
+
receipt_key = _stable_digest({"receipts": tup.receipts, "input": input_key})
|
|
634
|
+
return [f"input:{input_key}", f"receipt:{receipt_key}"]
|
|
635
|
+
|
|
636
|
+
def scan_index(
|
|
637
|
+
self,
|
|
638
|
+
*,
|
|
639
|
+
lane: str | None = None,
|
|
640
|
+
yool: str | None = None,
|
|
641
|
+
limit: int = 100,
|
|
642
|
+
) -> List[YoolTuple]:
|
|
643
|
+
with self._lock:
|
|
644
|
+
candidates = (
|
|
645
|
+
self.lane_index.get(lane, [])
|
|
646
|
+
if lane
|
|
647
|
+
else [tup for values in self.space.values() for tup in values]
|
|
648
|
+
)
|
|
649
|
+
out: List[YoolTuple] = []
|
|
650
|
+
for tup in candidates:
|
|
651
|
+
if yool is None or tup.yool == yool:
|
|
652
|
+
out.append(tup)
|
|
653
|
+
if len(out) >= limit:
|
|
654
|
+
break
|
|
655
|
+
return out
|
|
656
|
+
|
|
657
|
+
def spawn_agent(
|
|
658
|
+
self, parent: YoolTuple, agent_yool: str, agent_data: Dict[str, Any]
|
|
659
|
+
) -> int:
|
|
660
|
+
with self._lock:
|
|
661
|
+
agent_id = self._allocate_agent_id()
|
|
662
|
+
map_idx = HilbertIndex.compute((*parent.map, agent_id))
|
|
663
|
+
new_tuple = YoolTuple(
|
|
664
|
+
yool=agent_yool,
|
|
665
|
+
map_index=map_idx,
|
|
666
|
+
authority=f"subagent_{agent_id}",
|
|
667
|
+
lane=str(agent_data.get("lane") or parent.lane),
|
|
668
|
+
source=f"spawned_from_{parent.authority}",
|
|
669
|
+
data=agent_data,
|
|
670
|
+
parent_id=parent.id,
|
|
671
|
+
agent_id=agent_id,
|
|
672
|
+
)
|
|
673
|
+
self.agents[agent_id] = new_tuple
|
|
674
|
+
self.out_tuple(new_tuple)
|
|
675
|
+
self.prune_idle(self.policy.compression_threshold)
|
|
676
|
+
return agent_id
|
|
677
|
+
|
|
678
|
+
def batch_spawn(
|
|
679
|
+
self,
|
|
680
|
+
parent: YoolTuple,
|
|
681
|
+
agent_yool: str,
|
|
682
|
+
*,
|
|
683
|
+
depth: int,
|
|
684
|
+
branching: int,
|
|
685
|
+
compression_threshold: int | None = None,
|
|
686
|
+
agent_data: Dict[str, Any] | None = None,
|
|
687
|
+
) -> BatchSpawnReceipt:
|
|
688
|
+
"""Create a lazy deep hierarchy without enumerating every leaf.
|
|
689
|
+
|
|
690
|
+
Example: depth=4, branching=32 represents 1,048,576 leaf subagents while
|
|
691
|
+
materializing one controller tuple plus a compressed virtual subtree.
|
|
692
|
+
"""
|
|
693
|
+
if depth < 1:
|
|
694
|
+
raise ValueError("depth must be >= 1")
|
|
695
|
+
if branching < 1:
|
|
696
|
+
raise ValueError("branching must be >= 1")
|
|
697
|
+
threshold = compression_threshold or self.policy.compression_threshold
|
|
698
|
+
virtual_agents = branching**depth
|
|
699
|
+
controller_id = self.spawn_agent(
|
|
700
|
+
parent,
|
|
701
|
+
agent_yool,
|
|
702
|
+
{
|
|
703
|
+
**(agent_data or {}),
|
|
704
|
+
"lazy_batch": True,
|
|
705
|
+
"depth": depth,
|
|
706
|
+
"branching": branching,
|
|
707
|
+
"virtual_agents": virtual_agents,
|
|
708
|
+
"compression_threshold": threshold,
|
|
709
|
+
},
|
|
710
|
+
)
|
|
711
|
+
with self._lock:
|
|
712
|
+
self.virtual_agent_count += virtual_agents
|
|
713
|
+
receipt_id = self._receipt_hash(
|
|
714
|
+
(controller_id, depth, branching), agent_yool, virtual_agents
|
|
715
|
+
)
|
|
716
|
+
controller = self.agents.get(controller_id)
|
|
717
|
+
if controller is not None:
|
|
718
|
+
controller.receipts.append(f"batch_spawn@{receipt_id}")
|
|
719
|
+
if len(self.agents) > threshold:
|
|
720
|
+
self.compress_token(controller_id)
|
|
721
|
+
return BatchSpawnReceipt(
|
|
722
|
+
root_agent_id=controller_id,
|
|
723
|
+
depth=depth,
|
|
724
|
+
branching=branching,
|
|
725
|
+
virtual_agents=virtual_agents,
|
|
726
|
+
compression_threshold=threshold,
|
|
727
|
+
receipt_id=receipt_id,
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
def compress_token(self, agent_id: int) -> CompressToken | None:
|
|
731
|
+
with self._lock:
|
|
732
|
+
tup = self.agents.get(agent_id)
|
|
733
|
+
if tup is None:
|
|
734
|
+
return self.compressed_agents.get(agent_id)
|
|
735
|
+
digest = hashlib.sha256(
|
|
736
|
+
repr(sorted(tup.data.items())).encode("utf-8")
|
|
737
|
+
).hexdigest()
|
|
738
|
+
token = CompressToken(
|
|
739
|
+
agent_id=agent_id,
|
|
740
|
+
yool=tup.yool,
|
|
741
|
+
map_index=tup.map,
|
|
742
|
+
authority=tup.authority,
|
|
743
|
+
lane=tup.lane,
|
|
744
|
+
source=tup.source,
|
|
745
|
+
parent_id=tup.parent_id,
|
|
746
|
+
receipts=list(tup.receipts),
|
|
747
|
+
data_digest=digest,
|
|
748
|
+
)
|
|
749
|
+
self._remove_tuple(tup)
|
|
750
|
+
self.compressed_agents[agent_id] = token
|
|
751
|
+
self.agents.pop(agent_id, None)
|
|
752
|
+
return token
|
|
753
|
+
|
|
754
|
+
def prune_idle(self, max_active: int | None = None) -> int:
|
|
755
|
+
max_active = max_active or self.policy.compression_threshold
|
|
756
|
+
with self._lock:
|
|
757
|
+
active = [
|
|
758
|
+
(agent_id, tup.last_active) for agent_id, tup in self.agents.items()
|
|
759
|
+
]
|
|
760
|
+
if len(active) <= max_active:
|
|
761
|
+
return 0
|
|
762
|
+
active.sort(key=lambda item: item[1])
|
|
763
|
+
to_compress = len(active) - max_active
|
|
764
|
+
for agent_id, _ in active[:to_compress]:
|
|
765
|
+
self.compress_token(agent_id)
|
|
766
|
+
return to_compress
|
|
767
|
+
|
|
768
|
+
def route_packet(self, packet: Dict[str, Any], target_lane: str) -> bool:
|
|
769
|
+
target = self.rd_tuple({"lane": target_lane})
|
|
770
|
+
if target is None:
|
|
771
|
+
return False
|
|
772
|
+
target.data.update(packet)
|
|
773
|
+
target.receipts.append(
|
|
774
|
+
f"route@{self._receipt_hash(target.map, target_lane, len(packet))}"
|
|
775
|
+
)
|
|
776
|
+
target.touch()
|
|
777
|
+
return True
|
|
778
|
+
|
|
779
|
+
def hookwall(self, wall_id: str, capability: str, action: str = "hook") -> bool:
|
|
780
|
+
with self._lock:
|
|
781
|
+
if action == "hook":
|
|
782
|
+
if capability not in self.walls[wall_id]:
|
|
783
|
+
self.walls[wall_id].append(capability)
|
|
784
|
+
return True
|
|
785
|
+
if action == "check":
|
|
786
|
+
return capability in self.walls.get(wall_id, [])
|
|
787
|
+
if action == "unhook":
|
|
788
|
+
if capability in self.walls.get(wall_id, []):
|
|
789
|
+
self.walls[wall_id].remove(capability)
|
|
790
|
+
return True
|
|
791
|
+
return False
|
|
792
|
+
|
|
793
|
+
def snapshot(self) -> Dict[str, Any]:
|
|
794
|
+
with self._lock:
|
|
795
|
+
return {
|
|
796
|
+
"tuples": sum(len(items) for items in self.space.values()),
|
|
797
|
+
"lanes": {
|
|
798
|
+
lane: len(items) for lane, items in sorted(self.lane_index.items())
|
|
799
|
+
},
|
|
800
|
+
"active_agents": len(self.agents),
|
|
801
|
+
"compressed_agents": len(self.compressed_agents),
|
|
802
|
+
"virtual_agents": self.virtual_agent_count,
|
|
803
|
+
"total_agents": len(self.agents)
|
|
804
|
+
+ len(self.compressed_agents)
|
|
805
|
+
+ self.virtual_agent_count,
|
|
806
|
+
"policy": {
|
|
807
|
+
"lane_concurrency": self.policy.lane_concurrency,
|
|
808
|
+
"max_lane_concurrency": self.policy.max_lane_concurrency,
|
|
809
|
+
"cpu_quota_pct": self.policy.cpu_quota_pct,
|
|
810
|
+
"queue_maxsize": self.policy.queue_maxsize,
|
|
811
|
+
"compression_threshold": self.policy.compression_threshold,
|
|
812
|
+
"cache_max_entries": self.policy.cache_max_entries,
|
|
813
|
+
"cache_ttl_s": self.policy.cache_ttl_s,
|
|
814
|
+
"api_max_retries": self.policy.api_max_retries,
|
|
815
|
+
"api_backoff_base_ms": self.policy.api_backoff_base_ms,
|
|
816
|
+
"api_backoff_max_ms": self.policy.api_backoff_max_ms,
|
|
817
|
+
"circuit_failure_threshold": self.policy.circuit_failure_threshold,
|
|
818
|
+
"circuit_cooldown_s": self.policy.circuit_cooldown_s,
|
|
819
|
+
"batch_small_task_size": self.policy.batch_small_task_size,
|
|
820
|
+
"context_compression_chars": self.policy.context_compression_chars,
|
|
821
|
+
},
|
|
822
|
+
"cache": self.receipt_cache.snapshot(),
|
|
823
|
+
"circuit_breakers": self.circuit_breaker.snapshot(),
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
def _allocate_agent_id(self) -> int:
|
|
827
|
+
agent_id = self._next_agent_id
|
|
828
|
+
self._next_agent_id += 1
|
|
829
|
+
return agent_id
|
|
830
|
+
|
|
831
|
+
def _find_match(self, template: Dict[str, Any]) -> YoolTuple | None:
|
|
832
|
+
if "lane" in template:
|
|
833
|
+
candidates = list(self.lane_index.get(str(template["lane"]), []))
|
|
834
|
+
else:
|
|
835
|
+
candidates = [
|
|
836
|
+
tup for tuples_list in self.space.values() for tup in tuples_list
|
|
837
|
+
]
|
|
838
|
+
for tup in candidates:
|
|
839
|
+
if self._matches(tup, template):
|
|
840
|
+
return tup
|
|
841
|
+
return None
|
|
842
|
+
|
|
843
|
+
def _matches(self, t: YoolTuple, template: Dict[str, Any]) -> bool:
|
|
844
|
+
for key, expected in template.items():
|
|
845
|
+
if hasattr(t, key):
|
|
846
|
+
value = getattr(t, key)
|
|
847
|
+
else:
|
|
848
|
+
value = t.data.get(key)
|
|
849
|
+
if value != expected:
|
|
850
|
+
return False
|
|
851
|
+
return True
|
|
852
|
+
|
|
853
|
+
def _remove_tuple(self, t: YoolTuple) -> None:
|
|
854
|
+
tuples_list = self.space.get(t.map)
|
|
855
|
+
if tuples_list:
|
|
856
|
+
self.space[t.map] = [item for item in tuples_list if item is not t]
|
|
857
|
+
if not self.space[t.map]:
|
|
858
|
+
del self.space[t.map]
|
|
859
|
+
lane_items = self.lane_index.get(t.lane)
|
|
860
|
+
if lane_items:
|
|
861
|
+
self.lane_index[t.lane] = [item for item in lane_items if item is not t]
|
|
862
|
+
if not self.lane_index[t.lane]:
|
|
863
|
+
del self.lane_index[t.lane]
|
|
864
|
+
|
|
865
|
+
@staticmethod
|
|
866
|
+
def _receipt_hash(*parts: Any) -> str:
|
|
867
|
+
raw = repr(parts).encode("utf-8")
|
|
868
|
+
return hashlib.blake2b(raw, digest_size=8).hexdigest()
|
|
869
|
+
|
|
870
|
+
@staticmethod
|
|
871
|
+
def _default_retryable(exc: BaseException) -> bool:
|
|
872
|
+
return isinstance(exc, (TimeoutError, ConnectionError))
|
|
873
|
+
|
|
874
|
+
@staticmethod
|
|
875
|
+
def _should_compress_context(tup: YoolTuple) -> bool:
|
|
876
|
+
if tup.data.get("compress_context") is False:
|
|
877
|
+
return False
|
|
878
|
+
if tup.data.get("compress_context") is True:
|
|
879
|
+
return True
|
|
880
|
+
return bool(tup.data.get("provider") or tup.data.get("llm_context"))
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
class LaneWorkerPool:
|
|
884
|
+
"""Bounded per-lane worker fan-out for tuple execution."""
|
|
885
|
+
|
|
886
|
+
def __init__(
|
|
887
|
+
self,
|
|
888
|
+
space: TupleSpace,
|
|
889
|
+
*,
|
|
890
|
+
policy: RuntimePolicy | None = None,
|
|
891
|
+
lane_concurrency: Dict[str, int] | None = None,
|
|
892
|
+
) -> None:
|
|
893
|
+
self.space = space
|
|
894
|
+
self.policy = policy or space.policy
|
|
895
|
+
self.lane_concurrency = dict(lane_concurrency or {})
|
|
896
|
+
self.lane_metrics: DefaultDict[str, LaneMetrics] = defaultdict(LaneMetrics)
|
|
897
|
+
|
|
898
|
+
def concurrency_for(self, lane: str) -> int:
|
|
899
|
+
queued = len(self.space.scan_index(lane=lane, limit=self.policy.queue_maxsize))
|
|
900
|
+
configured = self.lane_concurrency.get(lane)
|
|
901
|
+
metrics = self.lane_metrics[lane]
|
|
902
|
+
if configured is not None:
|
|
903
|
+
baseline = RuntimePolicy(
|
|
904
|
+
lane_concurrency=configured,
|
|
905
|
+
max_lane_concurrency=self.policy.max_lane_concurrency,
|
|
906
|
+
)
|
|
907
|
+
return baseline.concurrency_for(
|
|
908
|
+
queued,
|
|
909
|
+
ewma_latency_ms=metrics.ewma_latency_ms or None,
|
|
910
|
+
error_rate=metrics.error_rate,
|
|
911
|
+
)
|
|
912
|
+
return self.policy.concurrency_for(
|
|
913
|
+
queued,
|
|
914
|
+
ewma_latency_ms=metrics.ewma_latency_ms or None,
|
|
915
|
+
error_rate=metrics.error_rate,
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
def run_lane(
|
|
919
|
+
self,
|
|
920
|
+
lane: str,
|
|
921
|
+
executor: Executor,
|
|
922
|
+
*,
|
|
923
|
+
provider: str | None = None,
|
|
924
|
+
use_cache: bool = True,
|
|
925
|
+
retryable: Callable[[BaseException], bool] | None = None,
|
|
926
|
+
sleep_fn: Callable[[float], None] = time.sleep,
|
|
927
|
+
speculative_executor: Executor | None = None,
|
|
928
|
+
) -> List[Any]:
|
|
929
|
+
tuples = self.space.scan_index(lane=lane, limit=self.policy.queue_maxsize)
|
|
930
|
+
if not tuples:
|
|
931
|
+
return []
|
|
932
|
+
concurrency = self.concurrency_for(lane)
|
|
933
|
+
semaphore = threading.Semaphore(concurrency)
|
|
934
|
+
results: List[Any] = []
|
|
935
|
+
with ThreadPoolExecutor(max_workers=concurrency) as pool:
|
|
936
|
+
futures: List[Future[Any]] = [
|
|
937
|
+
pool.submit(
|
|
938
|
+
self._execute_one,
|
|
939
|
+
lane,
|
|
940
|
+
executor,
|
|
941
|
+
semaphore,
|
|
942
|
+
provider,
|
|
943
|
+
use_cache,
|
|
944
|
+
retryable,
|
|
945
|
+
sleep_fn,
|
|
946
|
+
speculative_executor,
|
|
947
|
+
)
|
|
948
|
+
for _ in tuples
|
|
949
|
+
]
|
|
950
|
+
for future in as_completed(futures):
|
|
951
|
+
results.append(future.result())
|
|
952
|
+
return results
|
|
953
|
+
|
|
954
|
+
def run_lane_batched(
|
|
955
|
+
self,
|
|
956
|
+
lane: str,
|
|
957
|
+
batch_executor: BatchExecutor,
|
|
958
|
+
*,
|
|
959
|
+
batch_size: int | None = None,
|
|
960
|
+
) -> List[Any]:
|
|
961
|
+
"""Drain small lane tasks in batches to reduce scheduler/API overhead."""
|
|
962
|
+
concurrency_hint = self.concurrency_for(lane)
|
|
963
|
+
tuples = self._drain_lane(lane)
|
|
964
|
+
if not tuples:
|
|
965
|
+
return []
|
|
966
|
+
size = max(1, batch_size or self.policy.batch_small_task_size)
|
|
967
|
+
batches = [
|
|
968
|
+
tuples[index : index + size] for index in range(0, len(tuples), size)
|
|
969
|
+
]
|
|
970
|
+
concurrency = min(concurrency_hint, len(batches))
|
|
971
|
+
results: List[Any] = []
|
|
972
|
+
with ThreadPoolExecutor(max_workers=max(1, concurrency)) as pool:
|
|
973
|
+
futures = [
|
|
974
|
+
pool.submit(self._execute_batch, lane, batch, batch_executor)
|
|
975
|
+
for batch in batches
|
|
976
|
+
]
|
|
977
|
+
for future in as_completed(futures):
|
|
978
|
+
item = future.result()
|
|
979
|
+
if isinstance(item, list):
|
|
980
|
+
results.extend(item)
|
|
981
|
+
else:
|
|
982
|
+
results.append(item)
|
|
983
|
+
return results
|
|
984
|
+
|
|
985
|
+
def _execute_one(
|
|
986
|
+
self,
|
|
987
|
+
lane: str,
|
|
988
|
+
executor: Executor,
|
|
989
|
+
semaphore: threading.Semaphore,
|
|
990
|
+
provider: str | None,
|
|
991
|
+
use_cache: bool,
|
|
992
|
+
retryable: Callable[[BaseException], bool] | None,
|
|
993
|
+
sleep_fn: Callable[[float], None],
|
|
994
|
+
speculative_executor: Executor | None,
|
|
995
|
+
) -> Any:
|
|
996
|
+
with semaphore:
|
|
997
|
+
tup = self.space.in_tuple({"lane": lane})
|
|
998
|
+
if tup is None:
|
|
999
|
+
return None
|
|
1000
|
+
return self._execute_tuple(
|
|
1001
|
+
lane,
|
|
1002
|
+
tup,
|
|
1003
|
+
executor,
|
|
1004
|
+
provider=provider,
|
|
1005
|
+
use_cache=use_cache,
|
|
1006
|
+
retryable=retryable,
|
|
1007
|
+
sleep_fn=sleep_fn,
|
|
1008
|
+
speculative_executor=speculative_executor,
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
def _execute_tuple(
|
|
1012
|
+
self,
|
|
1013
|
+
lane: str,
|
|
1014
|
+
tup: YoolTuple,
|
|
1015
|
+
executor: Executor,
|
|
1016
|
+
*,
|
|
1017
|
+
provider: str | None,
|
|
1018
|
+
use_cache: bool,
|
|
1019
|
+
retryable: Callable[[BaseException], bool] | None,
|
|
1020
|
+
sleep_fn: Callable[[float], None],
|
|
1021
|
+
speculative_executor: Executor | None,
|
|
1022
|
+
) -> Any:
|
|
1023
|
+
start = time.perf_counter()
|
|
1024
|
+
ok = False
|
|
1025
|
+
try:
|
|
1026
|
+
if speculative_executor is not None and tup.data.get("idempotent") is True:
|
|
1027
|
+
result = self._speculative_execute(tup, executor, speculative_executor)
|
|
1028
|
+
else:
|
|
1029
|
+
result = self.space.execute_tuple(
|
|
1030
|
+
tup,
|
|
1031
|
+
executor,
|
|
1032
|
+
provider=provider,
|
|
1033
|
+
use_cache=use_cache,
|
|
1034
|
+
retryable=retryable,
|
|
1035
|
+
sleep_fn=sleep_fn,
|
|
1036
|
+
)
|
|
1037
|
+
ok = True
|
|
1038
|
+
return result
|
|
1039
|
+
finally:
|
|
1040
|
+
elapsed_ms = (time.perf_counter() - start) * 1000
|
|
1041
|
+
self.lane_metrics[lane].record(elapsed_ms, ok=ok)
|
|
1042
|
+
|
|
1043
|
+
def _speculative_execute(
|
|
1044
|
+
self, tup: YoolTuple, primary: Executor, speculative: Executor
|
|
1045
|
+
) -> Any:
|
|
1046
|
+
errors: List[BaseException] = []
|
|
1047
|
+
with ThreadPoolExecutor(max_workers=2) as pool:
|
|
1048
|
+
futures = [
|
|
1049
|
+
pool.submit(primary, tup),
|
|
1050
|
+
pool.submit(speculative, tup),
|
|
1051
|
+
]
|
|
1052
|
+
for future in as_completed(futures):
|
|
1053
|
+
try:
|
|
1054
|
+
result = future.result()
|
|
1055
|
+
except BaseException as exc:
|
|
1056
|
+
errors.append(exc)
|
|
1057
|
+
continue
|
|
1058
|
+
for pending in futures:
|
|
1059
|
+
if pending is not future:
|
|
1060
|
+
pending.cancel()
|
|
1061
|
+
tup.receipts.append(
|
|
1062
|
+
f"speculative_win@{self.space._receipt_hash(tup.yool, tup.map)}"
|
|
1063
|
+
)
|
|
1064
|
+
return result
|
|
1065
|
+
raise errors[0]
|
|
1066
|
+
|
|
1067
|
+
def _drain_lane(self, lane: str) -> List[YoolTuple]:
|
|
1068
|
+
tuples = self.space.scan_index(lane=lane, limit=self.policy.queue_maxsize)
|
|
1069
|
+
drained: List[YoolTuple] = []
|
|
1070
|
+
for _ in tuples:
|
|
1071
|
+
tup = self.space.in_tuple({"lane": lane})
|
|
1072
|
+
if tup is not None:
|
|
1073
|
+
drained.append(tup)
|
|
1074
|
+
return drained
|
|
1075
|
+
|
|
1076
|
+
def _execute_batch(
|
|
1077
|
+
self, lane: str, batch: List[YoolTuple], batch_executor: BatchExecutor
|
|
1078
|
+
) -> Any:
|
|
1079
|
+
start = time.perf_counter()
|
|
1080
|
+
ok = False
|
|
1081
|
+
try:
|
|
1082
|
+
receipt = self.space._receipt_hash(
|
|
1083
|
+
lane, [tup.map for tup in batch], len(batch)
|
|
1084
|
+
)
|
|
1085
|
+
for tup in batch:
|
|
1086
|
+
tup.receipts.append(f"batch@{receipt}")
|
|
1087
|
+
result = batch_executor(batch)
|
|
1088
|
+
ok = True
|
|
1089
|
+
return result
|
|
1090
|
+
finally:
|
|
1091
|
+
elapsed_ms = (time.perf_counter() - start) * 1000
|
|
1092
|
+
self.lane_metrics[lane].record(elapsed_ms, ok=ok)
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
def build_default_space() -> tuple[TupleSpace, YoolTuple]:
|
|
1096
|
+
ts = TupleSpace()
|
|
1097
|
+
root = YoolTuple("kernel_root", HilbertIndex.compute((0,)), "root", "main", "user")
|
|
1098
|
+
ts.out_tuple(root)
|
|
1099
|
+
return ts, root
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
if __name__ == "__main__":
|
|
1103
|
+
ts, root = build_default_space()
|
|
1104
|
+
ts.spawn_agent(root, "hamt_builder", {"status": "ready"})
|
|
1105
|
+
ts.batch_spawn(
|
|
1106
|
+
root, "codex_worker", depth=4, branching=32, compression_threshold=128
|
|
1107
|
+
)
|
|
1108
|
+
ts.hookwall("main_wall", "capability_root", "hook")
|
|
1109
|
+
print(ts.snapshot())
|