maka-agent 0.2.0-dev.53.20260927 → 0.2.0-dev.62.20260928

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 (55) hide show
  1. package/LICENSE +6 -5
  2. package/dist/workspace-root.js +2 -2
  3. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  4. package/node_modules/@maka/acp-executor-plugin/dist/index.js +333 -28
  5. package/node_modules/@maka/acp-executor-plugin/dist/plugin.mjs +283 -27
  6. package/node_modules/@maka/core/dist/executor-catalog.js +10 -1
  7. package/node_modules/@maka/core/dist/message-queue-order.js +47 -0
  8. package/node_modules/@maka/core/dist/model-metadata.js +2 -1
  9. package/node_modules/@maka/core/dist/redaction.js +42 -3
  10. package/node_modules/@maka/core/dist/relative-time-policy.js +71 -0
  11. package/node_modules/@maka/core/dist/relative-time.js +3 -54
  12. package/node_modules/@maka/core/package.json +1 -0
  13. package/node_modules/@maka/eval/dist/egress-audit.js +127 -0
  14. package/node_modules/@maka/eval/dist/harness-executor.js +16 -96
  15. package/node_modules/@maka/eval/harbor/egress-proxy/entrypoint.sh +4 -4
  16. package/node_modules/@maka/eval/harbor/egress_filter.py +153 -104
  17. package/node_modules/@maka/eval/harbor/eval_framework.py +37 -14
  18. package/node_modules/@maka/eval/harbor/relay_agent.py +4 -6
  19. package/node_modules/@maka/eval/harbor/run_trial.py +11 -4
  20. package/node_modules/@maka/runtime/dist/agent-graph-supervisor-wake.js +33 -6
  21. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +1 -1
  22. package/node_modules/@maka/runtime/dist/plugin-executor-backend.js +26 -5
  23. package/node_modules/@maka/runtime/dist/plugin-executor-service.js +36 -0
  24. package/node_modules/@maka/runtime/dist/provider-error-classification.js +8 -73
  25. package/node_modules/@maka/runtime/dist/provider-error-signals.js +31 -0
  26. package/node_modules/@maka/runtime/dist/provider-retry-policy.js +79 -0
  27. package/node_modules/@maka/runtime/dist/session-manager.js +6 -4
  28. package/node_modules/@maka/runtime/dist/session-projection-helpers.js +2 -0
  29. package/node_modules/@maka/runtime/dist/shell-run-manager.js +8 -8
  30. package/node_modules/@maka/runtime/dist/terminal-run-commit.js +7 -1
  31. package/node_modules/@maka/runtime/dist/tool-runtime.js +5 -4
  32. package/node_modules/@maka/runtime-host/dist/adapter/provider-retry-projector.js +63 -0
  33. package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +7 -51
  34. package/node_modules/@maka/runtime-host/dist/protocol/index.js +9 -3
  35. package/node_modules/@maka/runtime-host/dist/protocol/message-queue-limits.js +21 -0
  36. package/node_modules/@maka/runtime-host/dist/protocol/message.js +6 -104
  37. package/node_modules/@maka/runtime-host/dist/protocol/queue-mutation.js +136 -0
  38. package/node_modules/@maka/runtime-host/dist/protocol/turn-provider-retry.js +69 -0
  39. package/node_modules/@maka/runtime-host/dist/protocol/turn.js +2 -43
  40. package/node_modules/@maka/runtime-host/dist/server/goal-coordinator.js +42 -5
  41. package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +105 -233
  42. package/node_modules/@maka/runtime-host/dist/server/message-queue-state.js +69 -0
  43. package/node_modules/@maka/runtime-host/dist/server/provider-retry-projection.js +71 -0
  44. package/node_modules/@maka/runtime-host/dist/server/queued-mutation-executor.js +94 -0
  45. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +1 -1
  46. package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +9 -58
  47. package/node_modules/@maka/storage/dist/agent-run-store-contract.js +0 -3
  48. package/node_modules/@maka/storage/dist/agent-run-store.js +15 -6
  49. package/node_modules/@maka/storage/dist/execution-persistence-provider.js +1 -0
  50. package/node_modules/@maka/storage/dist/execution-stores.js +1 -0
  51. package/node_modules/@maka/storage/dist/project-catalog.js +41 -42
  52. package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +102 -31
  53. package/node_modules/@maka/storage/dist/sqlite-session-metadata-schema.js +52 -1
  54. package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +34 -2
  55. package/package.json +1 -1
@@ -23,7 +23,10 @@ import json
23
23
  import os
24
24
  import re
25
25
  import time
26
+ from contextlib import suppress
27
+ from ipaddress import IPv6Address
26
28
  from pathlib import Path
29
+ from typing import NamedTuple
27
30
  from urllib.parse import unquote, urlsplit
28
31
 
29
32
  PINNED_REVISION = "d49e28f1e4ddd13d289e85a5f312a66750951932"
@@ -36,6 +39,14 @@ PERCENT_ESCAPE = re.compile(r"%(?![0-9a-fA-F]{2})")
36
39
  TERMINAL_BENCH = re.compile(r"terminal[-_.%/+\s]*bench", re.IGNORECASE)
37
40
 
38
41
 
42
+ class ConnectTarget(NamedTuple):
43
+ """Validated CONNECT authority adapted into the URL policy's input shape."""
44
+
45
+ host: str
46
+ port: int | None
47
+ url: str
48
+
49
+
39
50
  def contamination_rule(raw_url: str) -> tuple[str, str, str] | None:
40
51
  normalized = normalize_url(raw_url)
41
52
  url = urlsplit(normalized)
@@ -150,12 +161,12 @@ def configure(updated: object) -> None:
150
161
 
151
162
 
152
163
  def request(flow: object) -> None:
153
- apply_http_policy(flow, flow.request.pretty_url)
164
+ enforce_url_policy(flow, flow.request.pretty_url)
154
165
 
155
166
 
156
167
  def response(flow: object) -> None:
157
- response = getattr(flow, "response", None)
158
- if getattr(response, "status_code", None) != 101:
168
+ reply = getattr(flow, "response", None)
169
+ if getattr(reply, "status_code", None) != 101:
159
170
  return
160
171
  # mitmproxy 12.2.3 sets flow.websocket before HttpResponseHook only when
161
172
  # the 101 is a real WebSocket upgrade (Upgrade + version 13 + option on).
@@ -163,36 +174,34 @@ def response(flow: object) -> None:
163
174
  # under rawtcp=false and must be audited.
164
175
  if getattr(flow, "websocket", None) is not None:
165
176
  return
166
- record_raw_tunnel(flow)
177
+ reject_raw_transport(flow, close=False)
167
178
 
168
179
 
169
180
  def http_connect(flow: object) -> None:
181
+ request = getattr(flow, "request", None)
170
182
  try:
171
- raw_url = connect_target_url(flow)
172
- except Exception:
173
- raw_url = ""
174
- apply_http_policy(flow, raw_url)
183
+ target = parse_connect_target(
184
+ getattr(request, "host", None),
185
+ getattr(request, "port", None),
186
+ ).url
187
+ except (TypeError, ValueError):
188
+ target = ""
189
+ enforce_url_policy(flow, target)
175
190
 
176
191
 
177
192
  def tcp_start(flow: object) -> None:
178
- # Last resort if a TCPLayer is still admitted (tcp_hosts / ignore).
179
- # CONNECT raw is closed by next_layer → CloseRawLayer; HTTP 101 raw is
180
- # closed by rawtcp=false. Neither of those paths starts a TCPLayer.
181
- record_raw_tunnel(flow)
182
- kill_flow(flow)
193
+ # This is a last-resort hook for a raw layer admitted by an override.
194
+ reject_raw_transport(flow)
183
195
 
184
196
 
185
197
  def tcp_message(flow: object) -> None:
186
198
  messages = getattr(flow, "messages", None)
187
- if messages:
199
+ if messages and hasattr(messages[-1], "content"):
188
200
  messages[-1].content = b""
189
- kill_flow(flow)
201
+ close_flow(flow)
190
202
 
191
203
 
192
204
  def next_layer(nextlayer: object) -> None:
193
- # Script addons run before the built-in classifier assigns layer. If we
194
- # set CloseRawLayer here, NextLayer leaves it in place. Waiting for
195
- # isinstance(..., TCPLayer) never fires on the production CONNECT path.
196
205
  current = getattr(nextlayer, "layer", None)
197
206
  context = getattr(nextlayer, "context", None)
198
207
  if current is None:
@@ -204,20 +213,20 @@ def next_layer(nextlayer: object) -> None:
204
213
  server_tls.child_layer = ClientTLSLayer(context)
205
214
  nextlayer.layer = server_tls
206
215
  return
207
- if not looks_like_raw_tcp(nextlayer):
216
+ if not initial_stream_is_raw(nextlayer):
208
217
  return
209
- record_raw_tunnel(context)
210
- nextlayer.layer = CloseRawLayer(context)
218
+ reject_raw_transport(context, close=False)
219
+ nextlayer.layer = RejectRawTransport(context, proxy_commands)
211
220
  return
212
221
  if TCPLayer is None or not isinstance(current, TCPLayer):
213
222
  return
214
- record_raw_tunnel(context)
215
- closer = CloseRawLayer(context)
223
+ reject_raw_transport(context, close=False)
224
+ closer = RejectRawTransport(context, proxy_commands)
216
225
  replace_layer(context, current, closer)
217
226
  nextlayer.layer = closer
218
227
 
219
228
 
220
- def apply_http_policy(flow: object, raw_url: str) -> None:
229
+ def enforce_url_policy(flow: object, raw_url: str) -> None:
221
230
  if http is None:
222
231
  raise RuntimeError("mitmproxy is required to run the Eval egress filter")
223
232
  try:
@@ -225,7 +234,7 @@ def apply_http_policy(flow: object, raw_url: str) -> None:
225
234
  if not matched:
226
235
  return
227
236
  rule_id, host, normalized_path = matched
228
- append_audit(rule_id, host, normalized_path)
237
+ audit_event(rule_id, host, normalized_path)
229
238
  flow.response = blocked_response(rule_id)
230
239
  except Exception as error:
231
240
  flow.response = http.Response.make(
@@ -237,27 +246,50 @@ def apply_http_policy(flow: object, raw_url: str) -> None:
237
246
  },
238
247
  )
239
248
  try:
240
- append_audit("policy_error", "", type(error).__name__)
249
+ audit_event("policy_error", "", type(error).__name__)
241
250
  except Exception:
242
251
  pass
243
252
 
244
253
 
245
- def connect_target_url(flow: object) -> str:
246
- request = flow.request
247
- host = (getattr(request, "pretty_host", None) or getattr(request, "host", "") or "").strip()
248
- if not host:
249
- raise ValueError("empty CONNECT host")
250
- if ":" in host and not host.startswith("["):
251
- host = f"[{host}]"
252
- port = getattr(request, "port", None)
253
- if port in (None, 443):
254
- return f"https://{host}/"
255
- if port == 80:
256
- return f"http://{host}/"
257
- return f"https://{host}:{port}/"
254
+ def connect_url(host: object, port: object) -> str:
255
+ return parse_connect_target(host, port).url
258
256
 
259
257
 
260
- def looks_like_raw_tcp(nextlayer: object) -> bool:
258
+ def parse_connect_target(host: object, port: object) -> ConnectTarget:
259
+ normalized_host = _normalize_connect_host(host)
260
+ normalized_port = _normalize_connect_port(port)
261
+ scheme = "http" if normalized_port == 80 else "https"
262
+ authority = normalized_host if normalized_port in (None, 443, 80) else f"{normalized_host}:{normalized_port}"
263
+ return ConnectTarget(
264
+ host=normalized_host,
265
+ port=normalized_port,
266
+ url=f"{scheme}://{authority}/",
267
+ )
268
+
269
+
270
+ def _normalize_connect_host(host: object) -> str:
271
+ if not isinstance(host, str) or not host or host != host.strip():
272
+ raise ValueError("empty CONNECT host")
273
+ if host.startswith("[") and host.endswith("]"):
274
+ IPv6Address(host[1:-1])
275
+ return host
276
+ if ":" in host:
277
+ IPv6Address(host)
278
+ return f"[{host}]"
279
+ if re.search(r"[\s/@?#\\\[\]]", host):
280
+ raise ValueError("invalid CONNECT host")
281
+ return host
282
+
283
+
284
+ def _normalize_connect_port(port: object) -> int | None:
285
+ if port is None:
286
+ return None
287
+ if type(port) is not int or not 1 <= port <= 65535:
288
+ raise ValueError("invalid CONNECT port")
289
+ return port
290
+
291
+
292
+ def initial_stream_is_raw(nextlayer: object) -> bool:
261
293
  """Close only when the bytes cannot still become HTTP or TLS.
262
294
 
263
295
  Script next_layer runs before mitmproxy 12.2.3's classifier. Copying its
@@ -309,9 +341,9 @@ def _next_layer_bytes(nextlayer: object, name: str) -> bytes:
309
341
  return bytes(data) if isinstance(data, (bytes, bytearray)) else b""
310
342
 
311
343
 
312
- def tcp_peer(flow: object) -> tuple[str, str]:
313
- server = getattr(flow, "server_conn", None) or getattr(flow, "server", None)
314
- address = getattr(server, "address", None) if server is not None else None
344
+ def peer_label(owner: object) -> tuple[str, str]:
345
+ server = getattr(owner, "server_conn", None) or getattr(owner, "server", None)
346
+ address = getattr(server, "address", None)
315
347
  if isinstance(address, (tuple, list)) and address:
316
348
  host = str(address[0])[:255]
317
349
  port = address[1] if len(address) > 1 else ""
@@ -319,19 +351,21 @@ def tcp_peer(flow: object) -> tuple[str, str]:
319
351
  return "", ""
320
352
 
321
353
 
322
- def record_raw_tunnel(flow: object) -> None:
323
- host, path = tcp_peer(flow)
354
+ def reject_raw_transport(owner: object, *, close: bool = True) -> None:
355
+ host, port = peer_label(owner)
324
356
  try:
325
- append_audit("raw_tunnel", host, path)
357
+ audit_event("raw_tunnel", host, port)
326
358
  except Exception:
327
359
  pass
360
+ if close:
361
+ close_flow(owner)
328
362
 
329
363
 
330
- def kill_flow(flow: object) -> None:
331
- kill = getattr(flow, "kill", None)
332
- if callable(kill) and getattr(flow, "killable", True):
364
+ def close_flow(flow: object) -> None:
365
+ terminate = getattr(flow, "kill", None)
366
+ if callable(terminate) and getattr(flow, "killable", True):
333
367
  try:
334
- kill()
368
+ terminate()
335
369
  except Exception:
336
370
  pass
337
371
 
@@ -351,8 +385,9 @@ def replace_layer(context: object, current: object, closer: object) -> None:
351
385
  layers.insert(min(index, len(layers)), closer)
352
386
 
353
387
 
354
- class CloseRawLayer(Layer):
355
- def __init__(self, context: object) -> None:
388
+ class RejectRawTransport(Layer):
389
+ def __init__(self, context: object, commands: object) -> None:
390
+ self._close_connection = getattr(commands, "CloseConnection", None)
356
391
  if Layer is object:
357
392
  self.context = context
358
393
  return
@@ -363,13 +398,13 @@ class CloseRawLayer(Layer):
363
398
  super().__init__(context)
364
399
 
365
400
  def handle_event(self, event: object):
366
- if proxy_commands is None:
401
+ if self._close_connection is None:
367
402
  return
368
403
  yield
369
404
  for name in ("client", "server"):
370
405
  connection = getattr(self.context, name, None)
371
406
  if connection is not None:
372
- yield proxy_commands.CloseConnection(connection)
407
+ yield self._close_connection(connection)
373
408
 
374
409
 
375
410
  def blocked_response(rule_id: str):
@@ -383,53 +418,67 @@ def blocked_response(rule_id: str):
383
418
  )
384
419
 
385
420
 
386
- def append_audit(rule_id: str, host: str, normalized_path: str) -> None:
387
- AUDIT_PATH.parent.mkdir(parents=True, exist_ok=True)
388
- if AUDIT_PATH.exists() and AUDIT_PATH.stat().st_size >= MAX_AUDIT_BYTES:
389
- write_truncation_marker()
390
- return
391
- record = {
392
- "ts": int(time.time() * 1000),
393
- "ruleId": rule_id,
394
- "host": host[:255],
395
- "normalizedPath": normalized_path[:4096],
396
- }
397
- with AUDIT_PATH.open("a", encoding="utf-8") as stream:
398
- stream.write(json.dumps(record, ensure_ascii=True, separators=(",", ":")) + "\n")
399
-
400
-
401
- def write_truncation_marker() -> None:
402
- if audit_already_truncated():
403
- return
404
- record = {
405
- "ts": int(time.time() * 1000),
406
- "ruleId": "audit_truncated",
407
- "host": "",
408
- "normalizedPath": "",
409
- }
410
- prefix = ""
411
- if AUDIT_PATH.exists() and AUDIT_PATH.stat().st_size > 0:
412
- with AUDIT_PATH.open("rb") as stream:
413
- stream.seek(-1, os.SEEK_END)
414
- if stream.read(1) != b"\n":
415
- prefix = "\n"
416
- with AUDIT_PATH.open("a", encoding="utf-8") as stream:
417
- stream.write(prefix + json.dumps(record, ensure_ascii=True, separators=(",", ":")) + "\n")
418
-
419
-
420
- def audit_already_truncated() -> bool:
421
- if not AUDIT_PATH.exists():
422
- return False
423
- with AUDIT_PATH.open("rb") as stream:
424
- stream.seek(0, os.SEEK_END)
425
- size = stream.tell()
426
- stream.seek(max(0, size - 4096))
427
- tail = stream.read().decode("utf-8", errors="ignore")
428
- lines = [line for line in tail.splitlines() if line.strip()]
429
- if not lines:
430
- return False
431
- try:
432
- parsed = json.loads(lines[-1])
433
- except json.JSONDecodeError:
434
- return False
435
- return isinstance(parsed, dict) and parsed.get("ruleId") == "audit_truncated"
421
+ class AuditJournal:
422
+ def __init__(self, path: Path, byte_limit: int) -> None:
423
+ self.path = path
424
+ self.byte_limit = byte_limit
425
+
426
+ def record(self, rule_id: str, host: str, normalized_path: str) -> None:
427
+ self.path.parent.mkdir(parents=True, exist_ok=True)
428
+ entry = self._encode(rule_id, host[:255], normalized_path[:4096])
429
+ existing_bytes = self.path.stat().st_size if self.path.exists() else 0
430
+ if existing_bytes + len(entry) <= self.byte_limit:
431
+ with self.path.open("ab") as stream:
432
+ stream.write(entry)
433
+ return
434
+ self.mark_full()
435
+
436
+ def mark_full(self) -> None:
437
+ if self.has_full_marker():
438
+ return
439
+ with self.path.open("ab") as stream:
440
+ stream.write(self._separator() + self._encode("audit_truncated", "", ""))
441
+
442
+ def has_full_marker(self) -> bool:
443
+ last = self._last_record()
444
+ return last is not None and last.get("ruleId") == "audit_truncated"
445
+
446
+ def _separator(self) -> bytes:
447
+ if not self.path.exists() or self.path.stat().st_size == 0:
448
+ return b""
449
+ final_offset = self.path.stat().st_size - 1
450
+ with self.path.open("rb") as stream:
451
+ stream.seek(final_offset)
452
+ return b"" if stream.read(1) == b"\n" else b"\n"
453
+
454
+ def _last_record(self) -> dict[str, object] | None:
455
+ if not self.path.exists() or self.path.stat().st_size == 0:
456
+ return None
457
+ size = self.path.stat().st_size
458
+ with self.path.open("rb") as stream:
459
+ stream.seek(max(0, size - 4096))
460
+ lines = stream.read().decode("utf-8", errors="ignore").splitlines()
461
+ for line in reversed(lines):
462
+ if not line.strip():
463
+ continue
464
+ decoded = None
465
+ with suppress(json.JSONDecodeError):
466
+ decoded = json.loads(line)
467
+ return decoded if isinstance(decoded, dict) else None
468
+ return None
469
+
470
+ @staticmethod
471
+ def _encode(rule_id: str, host: str, normalized_path: str) -> bytes:
472
+ record = {
473
+ "ts": int(time.time() * 1000),
474
+ "ruleId": rule_id,
475
+ "host": host,
476
+ "normalizedPath": normalized_path,
477
+ }
478
+ return (json.dumps(record, ensure_ascii=True, separators=(",", ":")) + "\n").encode(
479
+ "utf-8"
480
+ )
481
+
482
+
483
+ def audit_event(rule_id: str, host: str, normalized_path: str) -> None:
484
+ AuditJournal(AUDIT_PATH, MAX_AUDIT_BYTES).record(rule_id, host, normalized_path)
@@ -15,26 +15,49 @@
15
15
  # specific language governing permissions and limitations
16
16
  # under the License.
17
17
 
18
- """Authoritative Eval harness framework selection.
19
-
20
- `run_trial.py` validates the argv framework and installs it here before Harbor,
21
- Pier, or the shared relay can import. The relay must not read the environment.
22
- """
18
+ """Process-local framework authority shared by the trial runner and relay."""
23
19
 
24
20
  from __future__ import annotations
25
21
 
26
- _FRAMEWORKS = frozenset({"harbor", "pier"})
27
- _framework: str | None = None
22
+ from typing import NamedTuple
23
+
24
+
25
+ class FrameworkSpec(NamedTuple):
26
+ name: str
27
+ distribution: str
28
+ agent_module: str
28
29
 
29
30
 
30
- def install(framework: str) -> None:
31
- if framework not in _FRAMEWORKS:
31
+ _FRAMEWORKS = {
32
+ "harbor": FrameworkSpec("harbor", "harbor", "harbor.agents.base"),
33
+ "pier": FrameworkSpec("pier", "datacurve-pier", "pier.agents.base"),
34
+ }
35
+ _active: FrameworkSpec | None = None
36
+
37
+
38
+ def framework_spec(name: str) -> FrameworkSpec:
39
+ try:
40
+ return _FRAMEWORKS[name]
41
+ except KeyError:
32
42
  raise RuntimeError("framework must be harbor or pier")
33
- global _framework
34
- _framework = framework
35
43
 
36
44
 
37
- def selected() -> str:
38
- if _framework not in _FRAMEWORKS:
45
+ def install(name: str) -> None:
46
+ """Select the framework before importing its process-wide relay module."""
47
+
48
+ global _active
49
+ _active = framework_spec(name)
50
+
51
+
52
+ def current_framework() -> str:
53
+ if _active is None:
39
54
  raise RuntimeError("Eval framework selection is not installed")
40
- return _framework
55
+ return _active.name
56
+
57
+
58
+ def framework_distribution(name: str) -> str:
59
+ return framework_spec(name).distribution
60
+
61
+
62
+ def framework_agent_module(name: str) -> str:
63
+ return framework_spec(name).agent_module
@@ -23,6 +23,7 @@ import asyncio
23
23
  import base64
24
24
  import contextlib
25
25
  import hashlib
26
+ import importlib
26
27
  import json
27
28
  import os
28
29
  import re
@@ -31,13 +32,10 @@ import tempfile
31
32
  from pathlib import Path
32
33
  from typing import Any
33
34
 
34
- from eval_framework import selected
35
+ from eval_framework import current_framework, framework_agent_module
35
36
 
36
- framework = selected()
37
- if framework == "harbor":
38
- from harbor.agents.base import BaseAgent
39
- else:
40
- from pier.agents.base import BaseAgent
37
+ framework = current_framework()
38
+ BaseAgent = importlib.import_module(framework_agent_module(framework)).BaseAgent
41
39
 
42
40
 
43
41
  class RelayTransportClosed(RuntimeError):
@@ -120,7 +120,13 @@ async def run_trial(framework: str, expected_version: str, config_file: Path) ->
120
120
  from eval_framework import install
121
121
 
122
122
  install(framework)
123
- distribution = {"harbor": "harbor", "pier": "datacurve-pier"}[framework]
123
+ await _run_selected_trial(framework, expected_version, config_file)
124
+
125
+
126
+ async def _run_selected_trial(framework: str, expected_version: str, config_file: Path) -> None:
127
+ from eval_framework import framework_distribution
128
+
129
+ distribution = framework_distribution(framework)
124
130
  if importlib.metadata.version(distribution) != expected_version:
125
131
  raise FrameworkVersionMismatch
126
132
  try:
@@ -184,10 +190,11 @@ async def create_harbor_trial(trial_type: type, config: object) -> object:
184
190
 
185
191
 
186
192
  async def main() -> None:
187
- from eval_framework import install
188
-
189
193
  framework, expected_version, config_path = sys.argv[1:]
190
- install(framework)
194
+ await _run_main(framework, expected_version, config_path)
195
+
196
+
197
+ async def _run_main(framework: str, expected_version: str, config_path: str) -> None:
191
198
  task = asyncio.current_task()
192
199
  assert task is not None
193
200
  loop = asyncio.get_running_loop()
@@ -217,7 +217,9 @@ export class AgentGraphSupervisorWakeCoordinator {
217
217
  snapshotVersion: snapshot.snapshotVersion,
218
218
  rootSessionId,
219
219
  });
220
- if (this.#closed || claimed.wake.status === 'delivered')
220
+ if (this.#closed ||
221
+ claimed.wake.status === 'delivered' ||
222
+ claimed.wake.status === 'exhausted')
221
223
  return;
222
224
  abortSignal.throwIfAborted();
223
225
  await this.#deliverWake(claimed.wake, snapshot, abortSignal, result);
@@ -279,6 +281,14 @@ export class AgentGraphSupervisorWakeCoordinator {
279
281
  await this.#deliverWake(wake, snapshot, abortSignal);
280
282
  }
281
283
  async #deliverWake(wake, snapshot, abortSignal, result) {
284
+ if (wake.status === 'exhausted')
285
+ return;
286
+ if (wake.attemptCount >= this.#maxDeliveryAttempts) {
287
+ if (wake.status === 'retryable_failed') {
288
+ await this.#input.wakeStore.exhaustAgentGraphSupervisorWake(wake.graphId, wake.wakeId, wake.failureReason?.slice(0, 4_000) || 'delivery_attempts_exhausted');
289
+ }
290
+ return;
291
+ }
282
292
  const presentation = (await this.#input.renderWake?.(wake.rootSessionId, snapshot, result)) ?? {
283
293
  text: renderAgentGraphSupervisorWakePrompt(snapshot, result),
284
294
  displayText: 'Agent graph reached a supervisor checkpoint.',
@@ -286,7 +296,7 @@ export class AgentGraphSupervisorWakeCoordinator {
286
296
  };
287
297
  let lastFailure;
288
298
  let overflowRecoveryAttempted = false;
289
- for (let index = 0; index < this.#maxDeliveryAttempts; index += 1) {
299
+ for (let index = wake.attemptCount; index < this.#maxDeliveryAttempts; index += 1) {
290
300
  abortSignal.throwIfAborted();
291
301
  if (!(await this.#isSessionDeliverable(wake.rootSessionId))) {
292
302
  await this.#supersedeSession(wake.rootSessionId, 'session_unavailable');
@@ -304,9 +314,15 @@ export class AgentGraphSupervisorWakeCoordinator {
304
314
  wakeId: wake.wakeId,
305
315
  attemptId,
306
316
  turnId,
317
+ maxAttempts: this.#maxDeliveryAttempts,
307
318
  });
308
- if (!admission.acquired)
319
+ if (!admission.acquired) {
320
+ if (admission.wake.status === 'retryable_failed' &&
321
+ admission.wake.attemptCount >= this.#maxDeliveryAttempts) {
322
+ await this.#input.wakeStore.exhaustAgentGraphSupervisorWake(wake.graphId, wake.wakeId, admission.wake.failureReason?.slice(0, 4_000) || 'delivery_attempts_exhausted');
323
+ }
309
324
  return;
325
+ }
310
326
  if (this.#sessionWakesSuppressed(wake.rootSessionId))
311
327
  return;
312
328
  if (this.#closed) {
@@ -396,7 +412,8 @@ export class AgentGraphSupervisorWakeCoordinator {
396
412
  recoveryAttempted: overflowRecoveryAttempted,
397
413
  });
398
414
  await emitWakeDiagnostic(this.#input.onDiagnostic, exhaustedDiagnostic(wake, overflowError));
399
- throw overflowError;
415
+ await this.#input.wakeStore.exhaustAgentGraphSupervisorWake(wake.graphId, wake.wakeId, overflowError.message.slice(0, 4_000));
416
+ return;
400
417
  }
401
418
  overflowRecoveryAttempted = true;
402
419
  try {
@@ -413,6 +430,15 @@ export class AgentGraphSupervisorWakeCoordinator {
413
430
  attemptId: overflowAttempt.attemptId,
414
431
  ...(recovery ? { recovery } : {}),
415
432
  });
433
+ if (recovery?.outcome && recovery.outcome.kind !== 'compacted') {
434
+ const overflowError = await this.#contextOverflowError(wake.rootSessionId, snapshot, {
435
+ recoveryAttempted: true,
436
+ recoveryFailure: recovery.outcome.reason,
437
+ });
438
+ await emitWakeDiagnostic(this.#input.onDiagnostic, exhaustedDiagnostic(wake, overflowError));
439
+ await this.#input.wakeStore.exhaustAgentGraphSupervisorWake(wake.graphId, wake.wakeId, overflowError.message.slice(0, 4_000));
440
+ return;
441
+ }
416
442
  }
417
443
  catch (error) {
418
444
  if (this.#closed || isAbortError(error))
@@ -430,13 +456,14 @@ export class AgentGraphSupervisorWakeCoordinator {
430
456
  recoveryFailure: failureReason,
431
457
  });
432
458
  await emitWakeDiagnostic(this.#input.onDiagnostic, exhaustedDiagnostic(wake, overflowError));
433
- throw overflowError;
459
+ await this.#input.wakeStore.exhaustAgentGraphSupervisorWake(wake.graphId, wake.wakeId, overflowError.message.slice(0, 4_000));
460
+ return;
434
461
  }
435
462
  }
436
463
  if (this.#closed)
437
464
  return;
438
465
  }
439
- throw new Error(`Agent graph supervisor wake was not delivered after ${this.#maxDeliveryAttempts} attempts: ${lastFailure ?? 'unknown failure'}`);
466
+ await this.#input.wakeStore.exhaustAgentGraphSupervisorWake(wake.graphId, wake.wakeId, lastFailure?.slice(0, 4_000) || 'delivery_attempts_exhausted');
440
467
  }
441
468
  async #contextOverflowError(rootSessionId, fallbackSnapshot, input) {
442
469
  let currentSnapshot = fallbackSnapshot;
@@ -20,7 +20,7 @@ import { DEFAULT_TOOL_MODE, isToolMode } from '@maka/core/tool-mode';
20
20
  import { resolveEffectiveOrchestration, } from '@maka/core/orchestration';
21
21
  import { stripUndefinedDeep } from '@maka/core/tool-args-identity';
22
22
  import { YIELD_AGENT_GRAPH_TOOL_NAME, } from './stream-graph-supervisor-tools.js';
23
- import { providerRetryReason } from './provider-error-classification.js';
23
+ import { providerRetryReason } from './provider-retry-policy.js';
24
24
  import Ajv from 'ajv';
25
25
  import Ajv2019 from 'ajv/dist/2019.js';
26
26
  import Ajv2020 from 'ajv/dist/2020.js';
@@ -51,19 +51,39 @@ export class PluginExecutorBackend {
51
51
  const queue = new AsyncEventQueue();
52
52
  const messageId = this.#newId();
53
53
  const producer = this.#produce(input, messageId, abort.signal, queue).finally(() => queue.close());
54
- const active = { abort, settled: producer };
54
+ const active = { abort, settled: producer.then(() => undefined) };
55
55
  this.#active.add(active);
56
+ let acknowledged = false;
56
57
  try {
57
58
  for await (const event of queue) {
58
59
  yield event;
59
60
  queue.ackConsumed();
60
61
  }
61
- await producer;
62
+ const returnedResult = await producer;
63
+ if (returnedResult) {
64
+ // The Runtime Kernel requests the next item only after onSessionEvent
65
+ // resolves. Reaching this point means its terminal event was accepted.
66
+ // The Plugin decides whether its external execution actually settled;
67
+ // uncertain execution or a failed checkpoint keeps the pending marker.
68
+ if (this.#binding.acknowledgeExecution)
69
+ try {
70
+ await this.#binding.acknowledgeExecution(this.sessionId, input.turnId);
71
+ acknowledged = true;
72
+ }
73
+ catch {
74
+ /* The provider abandonment below makes the uncertainty explicit. */
75
+ }
76
+ }
62
77
  }
63
78
  finally {
64
79
  queue.noteConsumerDetached();
65
80
  abort.abort(new Error('Plugin executor event consumer detached'));
66
- await producer.catch(() => undefined);
81
+ const returnedResult = await producer.catch(() => false);
82
+ if (returnedResult &&
83
+ this.#binding.acknowledgeExecution &&
84
+ !acknowledged &&
85
+ this.#binding.abandonExecution)
86
+ await this.#binding.abandonExecution(this.sessionId, input.turnId).catch(() => undefined);
67
87
  this.#active.delete(active);
68
88
  }
69
89
  }
@@ -128,13 +148,14 @@ export class PluginExecutorBackend {
128
148
  this.#publishCancellation(turnId, { status: 'cancelled', source: 'caller' }, queue);
129
149
  else
130
150
  this.#publishFailure(turnId, failure instanceof Error ? failure.message : 'External executor failed', undefined, false, queue);
131
- return;
151
+ return false;
132
152
  }
133
153
  if (result === undefined) {
134
154
  this.#publishFailure(turnId, 'External executor returned no terminal result', undefined, false, queue);
135
- return;
155
+ return false;
136
156
  }
137
157
  this.#publishResult(turnId, messageId, result, queue);
158
+ return true;
138
159
  }
139
160
  async #requestPermission(input, request, signal, toolUseIds, queue) {
140
161
  const hosted = input.hostedInteraction;