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.
- package/LICENSE +6 -5
- package/dist/workspace-root.js +2 -2
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/acp-executor-plugin/dist/index.js +333 -28
- package/node_modules/@maka/acp-executor-plugin/dist/plugin.mjs +283 -27
- package/node_modules/@maka/core/dist/executor-catalog.js +10 -1
- package/node_modules/@maka/core/dist/message-queue-order.js +47 -0
- package/node_modules/@maka/core/dist/model-metadata.js +2 -1
- package/node_modules/@maka/core/dist/redaction.js +42 -3
- package/node_modules/@maka/core/dist/relative-time-policy.js +71 -0
- package/node_modules/@maka/core/dist/relative-time.js +3 -54
- package/node_modules/@maka/core/package.json +1 -0
- package/node_modules/@maka/eval/dist/egress-audit.js +127 -0
- package/node_modules/@maka/eval/dist/harness-executor.js +16 -96
- package/node_modules/@maka/eval/harbor/egress-proxy/entrypoint.sh +4 -4
- package/node_modules/@maka/eval/harbor/egress_filter.py +153 -104
- package/node_modules/@maka/eval/harbor/eval_framework.py +37 -14
- package/node_modules/@maka/eval/harbor/relay_agent.py +4 -6
- package/node_modules/@maka/eval/harbor/run_trial.py +11 -4
- package/node_modules/@maka/runtime/dist/agent-graph-supervisor-wake.js +33 -6
- package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +1 -1
- package/node_modules/@maka/runtime/dist/plugin-executor-backend.js +26 -5
- package/node_modules/@maka/runtime/dist/plugin-executor-service.js +36 -0
- package/node_modules/@maka/runtime/dist/provider-error-classification.js +8 -73
- package/node_modules/@maka/runtime/dist/provider-error-signals.js +31 -0
- package/node_modules/@maka/runtime/dist/provider-retry-policy.js +79 -0
- package/node_modules/@maka/runtime/dist/session-manager.js +6 -4
- package/node_modules/@maka/runtime/dist/session-projection-helpers.js +2 -0
- package/node_modules/@maka/runtime/dist/shell-run-manager.js +8 -8
- package/node_modules/@maka/runtime/dist/terminal-run-commit.js +7 -1
- package/node_modules/@maka/runtime/dist/tool-runtime.js +5 -4
- package/node_modules/@maka/runtime-host/dist/adapter/provider-retry-projector.js +63 -0
- package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +7 -51
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +9 -3
- package/node_modules/@maka/runtime-host/dist/protocol/message-queue-limits.js +21 -0
- package/node_modules/@maka/runtime-host/dist/protocol/message.js +6 -104
- package/node_modules/@maka/runtime-host/dist/protocol/queue-mutation.js +136 -0
- package/node_modules/@maka/runtime-host/dist/protocol/turn-provider-retry.js +69 -0
- package/node_modules/@maka/runtime-host/dist/protocol/turn.js +2 -43
- package/node_modules/@maka/runtime-host/dist/server/goal-coordinator.js +42 -5
- package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +105 -233
- package/node_modules/@maka/runtime-host/dist/server/message-queue-state.js +69 -0
- package/node_modules/@maka/runtime-host/dist/server/provider-retry-projection.js +71 -0
- package/node_modules/@maka/runtime-host/dist/server/queued-mutation-executor.js +94 -0
- package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +1 -1
- package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +9 -58
- package/node_modules/@maka/storage/dist/agent-run-store-contract.js +0 -3
- package/node_modules/@maka/storage/dist/agent-run-store.js +15 -6
- package/node_modules/@maka/storage/dist/execution-persistence-provider.js +1 -0
- package/node_modules/@maka/storage/dist/execution-stores.js +1 -0
- package/node_modules/@maka/storage/dist/project-catalog.js +41 -42
- package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +102 -31
- package/node_modules/@maka/storage/dist/sqlite-session-metadata-schema.js +52 -1
- package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +34 -2
- 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
|
-
|
|
164
|
+
enforce_url_policy(flow, flow.request.pretty_url)
|
|
154
165
|
|
|
155
166
|
|
|
156
167
|
def response(flow: object) -> None:
|
|
157
|
-
|
|
158
|
-
if getattr(
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
#
|
|
179
|
-
|
|
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
|
-
|
|
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
|
|
216
|
+
if not initial_stream_is_raw(nextlayer):
|
|
208
217
|
return
|
|
209
|
-
|
|
210
|
-
nextlayer.layer =
|
|
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
|
-
|
|
215
|
-
closer =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
249
|
+
audit_event("policy_error", "", type(error).__name__)
|
|
241
250
|
except Exception:
|
|
242
251
|
pass
|
|
243
252
|
|
|
244
253
|
|
|
245
|
-
def
|
|
246
|
-
|
|
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
|
|
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
|
|
313
|
-
server = getattr(
|
|
314
|
-
address = getattr(server, "address", 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
|
|
323
|
-
host,
|
|
354
|
+
def reject_raw_transport(owner: object, *, close: bool = True) -> None:
|
|
355
|
+
host, port = peer_label(owner)
|
|
324
356
|
try:
|
|
325
|
-
|
|
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
|
|
331
|
-
|
|
332
|
-
if callable(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
record
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
def
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
|
38
|
-
|
|
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
|
|
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
|
|
35
|
+
from eval_framework import current_framework, framework_agent_module
|
|
35
36
|
|
|
36
|
-
framework =
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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(() =>
|
|
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;
|