@matt82198/aesop 0.3.2 → 0.4.1
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/CHANGELOG.md +47 -0
- package/README.md +24 -9
- package/aesop.config.example.json +16 -0
- package/bin/cli.js +107 -34
- package/daemons/run-watchdog.sh +6 -2
- package/docs/CONFIGURE.md +31 -20
- package/docs/FIRST-WAVE.md +29 -9
- package/docs/INSTALL.md +181 -26
- package/docs/MICROKERNEL.md +452 -0
- package/docs/PORTING.md +4 -0
- package/docs/README.md +7 -3
- package/docs/THE-AESOP-HYPOTHESIS.md +156 -0
- package/driver/CLAUDE.md +123 -123
- package/driver/README.md +40 -2
- package/driver/adjudication_gate.py +367 -0
- package/driver/aesop.config.example.json +20 -4
- package/driver/backend_config.py +603 -12
- package/driver/claude_code_driver.py +9 -17
- package/driver/codex_driver.py +80 -25
- package/driver/context_pack.py +454 -0
- package/driver/openai_compatible_driver.py +53 -11
- package/driver/openai_transport.py +31 -10
- package/driver/orchestrator_backend.py +332 -0
- package/driver/orchestrator_driver.py +589 -0
- package/driver/proc_util.py +134 -0
- package/driver/wave_loop.py +801 -59
- package/driver/wave_scheduler.py +361 -37
- package/monitor/collect-signals.mjs +83 -0
- package/package.json +1 -1
- package/state_store/coordination.py +65 -5
- package/tools/ci_merge_wait.py +88 -42
- package/tools/ci_shard_runner.py +128 -0
- package/tools/doctor.js +124 -12
- package/tools/reproduce.js +11 -2
- package/tools/seated_shadow_adjudication.py +920 -0
- package/tools/self_stats.py +61 -6
- package/tools/shadow_adjudication.py +1024 -0
- package/tools/verify_test_suite_count.py +250 -0
- package/tools/verify_ui_trio_redaction_proof.py +292 -0
- package/tools/wave_manifest_lint.py +485 -0
- package/tools/wave_scorecard.py +430 -0
- package/ui/config.py +1 -1
- package/ui/cost.py +176 -3
- package/ui/web/dist/assets/{index-CP68RIh3.js → index-4rp6B_ID.js} +1 -1
- package/ui/web/dist/assets/{index-CNQxaiOW.css → index-B2lTtpsH.css} +1 -1
- package/ui/web/dist/index.html +2 -2
|
@@ -35,7 +35,6 @@ Claude agent from plain Python.
|
|
|
35
35
|
stdlib-only, ASCII-only, Windows + Linux safe.
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
|
-
import subprocess
|
|
39
38
|
from typing import Optional
|
|
40
39
|
|
|
41
40
|
from agent_driver import (
|
|
@@ -50,6 +49,7 @@ from agent_driver import (
|
|
|
50
49
|
WorkerStatus,
|
|
51
50
|
WORKER_UNKNOWN,
|
|
52
51
|
)
|
|
52
|
+
from proc_util import run_shell_bounded
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
# Default abstract-role -> Anthropic model mapping. Workers are Haiku by policy
|
|
@@ -75,11 +75,12 @@ class ClaudeCodeDriver(AgentDriver):
|
|
|
75
75
|
|
|
76
76
|
name = "claude-code"
|
|
77
77
|
|
|
78
|
-
def __init__(self, model_map: Optional[dict] = None):
|
|
78
|
+
def __init__(self, model_map: Optional[dict] = None, timeout_s: float = 120.0):
|
|
79
79
|
# Copy so callers cannot mutate our defaults; fall back per-role.
|
|
80
80
|
self._model_map = dict(_DEFAULT_MODEL_MAP)
|
|
81
81
|
if model_map:
|
|
82
82
|
self._model_map.update(model_map)
|
|
83
|
+
self._timeout_s = timeout_s
|
|
83
84
|
|
|
84
85
|
# -- Operation 1: capability probe (concrete) --------------------------
|
|
85
86
|
def probe_capabilities(self) -> DriverCapabilities:
|
|
@@ -149,22 +150,13 @@ class ClaudeCodeDriver(AgentDriver):
|
|
|
149
150
|
real subprocess so tooling/tests get genuine behavior. `shell` is
|
|
150
151
|
advisory -- we always execute through the platform shell so the same
|
|
151
152
|
call works on Windows and Linux.
|
|
153
|
+
|
|
154
|
+
Timeouts truly bound wall-clock (RS-A F1): on expiry the WHOLE process
|
|
155
|
+
tree is killed (taskkill /T on Windows, killpg on POSIX) and we return
|
|
156
|
+
exit 124 promptly, preserving any partial output captured before the
|
|
157
|
+
kill (RS-A F7). See proc_util.run_shell_bounded.
|
|
152
158
|
"""
|
|
153
|
-
|
|
154
|
-
completed = subprocess.run(
|
|
155
|
-
command,
|
|
156
|
-
cwd=cwd,
|
|
157
|
-
shell=True,
|
|
158
|
-
capture_output=True,
|
|
159
|
-
text=True,
|
|
160
|
-
)
|
|
161
|
-
return CommandResult(
|
|
162
|
-
exit_code=completed.returncode,
|
|
163
|
-
stdout=completed.stdout or "",
|
|
164
|
-
stderr=completed.stderr or "",
|
|
165
|
-
)
|
|
166
|
-
except OSError as exc:
|
|
167
|
-
return CommandResult(exit_code=127, stdout="", stderr=str(exc))
|
|
159
|
+
return run_shell_bounded(command, cwd=cwd, timeout_s=self._timeout_s)
|
|
168
160
|
|
|
169
161
|
# -- Operation 5: model selection (concrete) ---------------------------
|
|
170
162
|
def resolve_model(self, role: str) -> str:
|
package/driver/codex_driver.py
CHANGED
|
@@ -34,11 +34,12 @@ stdlib-only, ASCII-only, Windows + Linux safe.
|
|
|
34
34
|
import hashlib
|
|
35
35
|
import json
|
|
36
36
|
import os
|
|
37
|
-
import subprocess
|
|
38
37
|
import time
|
|
39
38
|
from pathlib import Path
|
|
40
39
|
from typing import Dict, Optional
|
|
41
40
|
|
|
41
|
+
from proc_util import run_shell_bounded
|
|
42
|
+
|
|
42
43
|
from agent_driver import (
|
|
43
44
|
AgentDriver,
|
|
44
45
|
CommandResult,
|
|
@@ -176,6 +177,7 @@ class CodexDriver(AgentDriver):
|
|
|
176
177
|
max_retries: int = 2,
|
|
177
178
|
timeout_s: float = 120.0,
|
|
178
179
|
allow_unverified_models: bool = False,
|
|
180
|
+
command_timeout_s: Optional[float] = None,
|
|
179
181
|
):
|
|
180
182
|
"""Initialize the CodexDriver with optional overrides.
|
|
181
183
|
|
|
@@ -185,9 +187,12 @@ class CodexDriver(AgentDriver):
|
|
|
185
187
|
now: callable returning time.time() for testing (default=time.time).
|
|
186
188
|
max_owned_bytes: max total bytes of owned files before pre-dispatch fail (default 200KB).
|
|
187
189
|
max_retries: max in-turn retries on malformed JSON (default 2).
|
|
188
|
-
timeout_s: HTTP timeout
|
|
190
|
+
timeout_s: HTTP transport timeout + worker_status stall threshold, seconds (default 120).
|
|
189
191
|
allow_unverified_models: if True, allow models not known to support json_schema
|
|
190
192
|
(default False). Set to True only for experimental backends.
|
|
193
|
+
command_timeout_s: run_command wall-clock bound, seconds. Defaults to
|
|
194
|
+
timeout_s when unset, but is separately settable so raising the
|
|
195
|
+
HTTP timeout never silently raises the command timeout (RS-A F7).
|
|
191
196
|
|
|
192
197
|
Raises:
|
|
193
198
|
ValueError: if any mapped model is not in JSON_SCHEMA_CAPABLE and
|
|
@@ -207,11 +212,28 @@ class CodexDriver(AgentDriver):
|
|
|
207
212
|
f"Pass allow_unverified_models=True to override (for experimental backends)."
|
|
208
213
|
)
|
|
209
214
|
|
|
210
|
-
self._transport = transport or default_openai_transport
|
|
211
215
|
self._now = now or time.time
|
|
212
216
|
self._max_owned_bytes = max_owned_bytes
|
|
213
217
|
self._max_retries = max_retries
|
|
214
218
|
self._timeout_s = timeout_s
|
|
219
|
+
# run_command gets its OWN knob (falls back to timeout_s) so the HTTP
|
|
220
|
+
# timeout and the command wall-clock bound are independently tunable.
|
|
221
|
+
self._command_timeout_s = (
|
|
222
|
+
command_timeout_s if command_timeout_s is not None else timeout_s
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Transport wiring. When falling back to the default transport, BIND the
|
|
226
|
+
# configured timeout: default_openai_transport has its own timeout_s=120
|
|
227
|
+
# default, so without this wrapper a configured timeout_s was silently
|
|
228
|
+
# ignored for the HTTP call (it only fed worker_status stall math).
|
|
229
|
+
if transport is not None:
|
|
230
|
+
self._transport = transport
|
|
231
|
+
elif default_openai_transport is not None:
|
|
232
|
+
self._transport = lambda payload: default_openai_transport(
|
|
233
|
+
payload, timeout_s=self._timeout_s
|
|
234
|
+
)
|
|
235
|
+
else:
|
|
236
|
+
self._transport = None
|
|
215
237
|
|
|
216
238
|
# In-memory registry of worker status (worker_id -> {start_time, last_output_time, result}).
|
|
217
239
|
self._worker_registry: Dict[str, dict] = {}
|
|
@@ -419,12 +441,21 @@ class CodexDriver(AgentDriver):
|
|
|
419
441
|
structured = None
|
|
420
442
|
last_error = None
|
|
421
443
|
last_content = ""
|
|
444
|
+
last_failure_was_transport = False
|
|
422
445
|
|
|
423
446
|
for attempt in range(self._max_retries + 1):
|
|
447
|
+
# Transport call. Network/auth/HTTP failures are NOT JSON
|
|
448
|
+
# validation errors: the model never produced output, so a
|
|
449
|
+
# schema nudge is meaningless. Retry (covers transients) but
|
|
450
|
+
# without appending nudge messages, and label honestly.
|
|
424
451
|
try:
|
|
425
|
-
# Call transport.
|
|
426
452
|
response = self._transport(payload)
|
|
453
|
+
except Exception as exc:
|
|
454
|
+
last_error = f"transport error: {exc}"
|
|
455
|
+
last_failure_was_transport = True
|
|
456
|
+
continue
|
|
427
457
|
|
|
458
|
+
try:
|
|
428
459
|
# Extract and parse JSON.
|
|
429
460
|
if "choices" not in response or not response["choices"]:
|
|
430
461
|
raise ValueError("no choices in response")
|
|
@@ -437,8 +468,9 @@ class CodexDriver(AgentDriver):
|
|
|
437
468
|
# Success: break out of retry loop.
|
|
438
469
|
break
|
|
439
470
|
|
|
440
|
-
except (json.JSONDecodeError, ValueError, KeyError
|
|
471
|
+
except (json.JSONDecodeError, ValueError, KeyError) as exc:
|
|
441
472
|
last_error = str(exc)
|
|
473
|
+
last_failure_was_transport = False
|
|
442
474
|
# If we have retries left, append error feedback and retry.
|
|
443
475
|
if attempt < self._max_retries:
|
|
444
476
|
# Before appending retry messages, check if total payload would exceed budget.
|
|
@@ -479,21 +511,41 @@ class CodexDriver(AgentDriver):
|
|
|
479
511
|
)
|
|
480
512
|
continue
|
|
481
513
|
|
|
482
|
-
# If validation still failed after all retries.
|
|
514
|
+
# If transport or validation still failed after all retries.
|
|
483
515
|
if structured is None:
|
|
516
|
+
if last_failure_was_transport:
|
|
517
|
+
error_msg = (
|
|
518
|
+
f"transport failed after {self._max_retries + 1} "
|
|
519
|
+
f"attempts: {last_error}"
|
|
520
|
+
)
|
|
521
|
+
else:
|
|
522
|
+
error_msg = (
|
|
523
|
+
f"structured output validation failed after "
|
|
524
|
+
f"{self._max_retries + 1} attempts: {last_error}"
|
|
525
|
+
)
|
|
484
526
|
return WorkerResult(
|
|
485
527
|
worker_id=worker_id,
|
|
486
528
|
status=WORKER_FAILED,
|
|
487
529
|
ok=False,
|
|
488
|
-
error=
|
|
530
|
+
error=error_msg,
|
|
489
531
|
text=last_content,
|
|
490
532
|
)
|
|
491
533
|
|
|
492
534
|
# 8. Ownership enforcement: all returned paths must be in owned_files.
|
|
535
|
+
# Match under the SAME cross-platform policy as the read side
|
|
536
|
+
# (backslashes are separators on every OS): a Windows-authored
|
|
537
|
+
# manifest owning "src\\util.py" must accept a model returning
|
|
538
|
+
# "src/util.py" instead of failing spuriously as out-of-scope.
|
|
539
|
+
# SECURITY: the write always uses the canonical owned entry (which
|
|
540
|
+
# passed containment above), never the model's raw string.
|
|
541
|
+
owned_lookup = {
|
|
542
|
+
p.replace("\\", "/"): p for p in request.owned_files
|
|
543
|
+
}
|
|
493
544
|
files_to_write = []
|
|
494
545
|
for file_entry in structured.get("files", []):
|
|
495
546
|
path_str = file_entry.get("path", "")
|
|
496
|
-
|
|
547
|
+
canonical = owned_lookup.get(path_str.replace("\\", "/"))
|
|
548
|
+
if canonical is None:
|
|
497
549
|
# Distinguish: path not in the owned set (security/isolation violation).
|
|
498
550
|
return WorkerResult(
|
|
499
551
|
worker_id=worker_id,
|
|
@@ -501,21 +553,30 @@ class CodexDriver(AgentDriver):
|
|
|
501
553
|
ok=False,
|
|
502
554
|
error=f"out-of-scope: worker attempted to write {path_str} (not in owned set)",
|
|
503
555
|
)
|
|
504
|
-
files_to_write.append((
|
|
556
|
+
files_to_write.append((canonical, file_entry["contents"]))
|
|
505
557
|
|
|
506
558
|
# 9. Apply (validate ALL before writing ANY).
|
|
507
559
|
written_paths = []
|
|
508
560
|
for path_str, new_contents in files_to_write:
|
|
509
|
-
|
|
561
|
+
# Normalize separators for the write target: on POSIX a backslash
|
|
562
|
+
# is a literal filename char, so an owned path declared "src\c.py"
|
|
563
|
+
# must become "src/c.py" before Path() (mirrors the read path above).
|
|
564
|
+
# written_paths keeps the canonical path_str for record-keeping.
|
|
565
|
+
full_path = Path(request.workdir) / path_str.replace("\\", "/")
|
|
510
566
|
try:
|
|
511
567
|
full_path.write_text(new_contents, encoding="utf-8")
|
|
512
568
|
written_paths.append(path_str)
|
|
513
569
|
except OSError as exc:
|
|
514
570
|
# Distinguish: owned path exists but write failed (OS error).
|
|
571
|
+
# HONESTY: report the files ALREADY written before the
|
|
572
|
+
# failure -- a mid-loop failure leaves the tree partially
|
|
573
|
+
# modified with no rollback, and the orchestrator must know
|
|
574
|
+
# which files are dirty rather than believing none changed.
|
|
515
575
|
return WorkerResult(
|
|
516
576
|
worker_id=worker_id,
|
|
517
577
|
status=WORKER_FAILED,
|
|
518
578
|
ok=False,
|
|
579
|
+
files_written=tuple(written_paths),
|
|
519
580
|
error=f"write_failed: {path_str}: {exc}",
|
|
520
581
|
)
|
|
521
582
|
|
|
@@ -624,22 +685,16 @@ class CodexDriver(AgentDriver):
|
|
|
624
685
|
|
|
625
686
|
Real subprocess execution (not a worker tool). Used for tests, git,
|
|
626
687
|
verification. Mirrors ClaudeCodeDriver.run_command for parity.
|
|
688
|
+
|
|
689
|
+
Timeouts truly bound wall-clock (RS-A F1): on expiry the WHOLE process
|
|
690
|
+
tree is killed (taskkill /T on Windows, killpg on POSIX) and we return
|
|
691
|
+
exit 124 promptly, preserving any partial output captured before the
|
|
692
|
+
kill (RS-A F7). Bounded by command_timeout_s (NOT the HTTP timeout).
|
|
693
|
+
See proc_util.run_shell_bounded.
|
|
627
694
|
"""
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
cwd=cwd,
|
|
632
|
-
shell=True,
|
|
633
|
-
capture_output=True,
|
|
634
|
-
text=True,
|
|
635
|
-
)
|
|
636
|
-
return CommandResult(
|
|
637
|
-
exit_code=completed.returncode,
|
|
638
|
-
stdout=completed.stdout or "",
|
|
639
|
-
stderr=completed.stderr or "",
|
|
640
|
-
)
|
|
641
|
-
except OSError as exc:
|
|
642
|
-
return CommandResult(exit_code=127, stdout="", stderr=str(exc))
|
|
695
|
+
return run_shell_bounded(
|
|
696
|
+
command, cwd=cwd, timeout_s=self._command_timeout_s
|
|
697
|
+
)
|
|
643
698
|
|
|
644
699
|
# -- Operation 5: model selection (concrete) -------------------------
|
|
645
700
|
def resolve_model(self, role: str) -> str:
|