@matt82198/aesop 0.2.0 → 0.3.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +50 -260
  3. package/bin/cli.js +7 -3
  4. package/docs/HOOK-INSTALL.md +15 -56
  5. package/docs/INSTALL.md +4 -3
  6. package/docs/PORTING.md +166 -0
  7. package/docs/TEAM-STATE.md +16 -184
  8. package/docs/reproduce.md +33 -3
  9. package/driver/CLAUDE.md +50 -48
  10. package/driver/codex_driver.py +59 -12
  11. package/driver/openai_transport.py +33 -0
  12. package/driver/wave_bridge.py +9 -1
  13. package/driver/wave_loop.py +437 -70
  14. package/driver/wave_scheduler.py +890 -0
  15. package/hooks/pre-push-policy.sh +22 -5
  16. package/monitor/collect-signals.mjs +69 -0
  17. package/package.json +4 -4
  18. package/state_store/__init__.py +2 -1
  19. package/state_store/projections.py +63 -0
  20. package/state_store/read_api.py +156 -0
  21. package/state_store/write_api.py +462 -0
  22. package/tools/cost_ceiling.py +106 -15
  23. package/tools/cost_projection.py +559 -0
  24. package/tools/crossos_drift.py +394 -0
  25. package/tools/eod_sweep.py +137 -33
  26. package/tools/mutation_test.py +130 -8
  27. package/tools/proposals.mjs +47 -2
  28. package/tools/reproduce.js +405 -0
  29. package/tools/secret_scan.py +73 -18
  30. package/tools/stall_check.py +247 -16
  31. package/tools/stateapi_lint.py +325 -0
  32. package/tools/test_battery.py +173 -0
  33. package/tools/verify_cost_panel.py +345 -0
  34. package/tools/wave_preflight.py +296 -29
  35. package/tools/wave_templates.py +170 -45
  36. package/ui/collectors.py +81 -0
  37. package/ui/handler.py +230 -85
  38. package/ui/sse.py +3 -3
  39. package/ui/wave_telemetry.py +24 -18
  40. package/ui/web/dist/assets/{index-DqZLgwNg.css → index-CNQxaiOW.css} +1 -1
  41. package/ui/web/dist/assets/index-CP68RIh3.js +9 -0
  42. package/ui/web/dist/index.html +2 -2
  43. package/docs/QUICKSTART.md +0 -80
  44. package/ui/web/dist/assets/index-BGbgw2Nh.js +0 -9
@@ -63,15 +63,25 @@ except ImportError:
63
63
  default_openai_transport = None
64
64
 
65
65
 
66
- # Abstract-role -> OpenAI model mapping. Workers map to gpt-3.5-turbo (cheap);
66
+ # Abstract-role -> OpenAI model mapping. Workers map to gpt-4o-mini (supports json_schema);
67
67
  # setup/verify to gpt-4-turbo (stronger). User decision #1 (plan Section 7)
68
- # allows upgrading to gpt-4o-mini/gpt-4o; this is the conservative default.
68
+ # allows upgrading to gpt-4o; gpt-3.5-turbo does NOT support response_format json_schema.
69
69
  _DEFAULT_MODEL_MAP = {
70
- ROLE_WORKER: "gpt-3.5-turbo",
70
+ ROLE_WORKER: "gpt-4o-mini",
71
71
  ROLE_SETUP: "gpt-4-turbo",
72
72
  ROLE_VERIFY: "gpt-4-turbo",
73
73
  }
74
74
 
75
+ # Models that support response_format with type json_schema.
76
+ # Raise ValueError at __init__ if a mapped model is not in this set.
77
+ JSON_SCHEMA_CAPABLE = {
78
+ "gpt-4o",
79
+ "gpt-4o-mini",
80
+ "gpt-4-turbo",
81
+ "gpt-4.1",
82
+ "gpt-4.1-preview",
83
+ }
84
+
75
85
  # Default schema for structured worker output: full-file replacements.
76
86
  # See plan Section 2.1.
77
87
  WORKER_PATCH_SCHEMA = {
@@ -165,6 +175,7 @@ class CodexDriver(AgentDriver):
165
175
  max_owned_bytes: int = 200_000,
166
176
  max_retries: int = 2,
167
177
  timeout_s: float = 120.0,
178
+ allow_unverified_models: bool = False,
168
179
  ):
169
180
  """Initialize the CodexDriver with optional overrides.
170
181
 
@@ -175,11 +186,27 @@ class CodexDriver(AgentDriver):
175
186
  max_owned_bytes: max total bytes of owned files before pre-dispatch fail (default 200KB).
176
187
  max_retries: max in-turn retries on malformed JSON (default 2).
177
188
  timeout_s: HTTP timeout in seconds (default 120).
189
+ allow_unverified_models: if True, allow models not known to support json_schema
190
+ (default False). Set to True only for experimental backends.
191
+
192
+ Raises:
193
+ ValueError: if any mapped model is not in JSON_SCHEMA_CAPABLE and
194
+ allow_unverified_models is False.
178
195
  """
179
196
  self._model_map = dict(_DEFAULT_MODEL_MAP)
180
197
  if model_map:
181
198
  self._model_map.update(model_map)
182
199
 
200
+ # Validate that all mapped models support json_schema response_format.
201
+ if not allow_unverified_models:
202
+ for role, model in self._model_map.items():
203
+ if model not in JSON_SCHEMA_CAPABLE:
204
+ raise ValueError(
205
+ f"Model '{model}' mapped to role '{role}' does not support "
206
+ f"response_format json_schema. Known capable models: {sorted(JSON_SCHEMA_CAPABLE)}. "
207
+ f"Pass allow_unverified_models=True to override (for experimental backends)."
208
+ )
209
+
183
210
  self._transport = transport or default_openai_transport
184
211
  self._now = now or time.time
185
212
  self._max_owned_bytes = max_owned_bytes
@@ -414,18 +441,36 @@ class CodexDriver(AgentDriver):
414
441
  last_error = str(exc)
415
442
  # If we have retries left, append error feedback and retry.
416
443
  if attempt < self._max_retries:
444
+ # Before appending retry messages, check if total payload would exceed budget.
445
+ # Serialize the current payload + proposed new messages to estimate size.
446
+ error_msg = f"(attempt {attempt+1} failed: {last_error})"
447
+ nudge_msg = "Previous response was not valid JSON per the schema; return ONLY the JSON object."
448
+
449
+ # Estimate size of new messages to be added
450
+ test_payload = json.dumps(payload)
451
+ new_messages = [
452
+ {"role": "assistant", "content": error_msg},
453
+ {"role": "user", "content": nudge_msg},
454
+ ]
455
+ test_payload_with_retry = json.dumps({
456
+ **payload,
457
+ "messages": payload["messages"] + new_messages
458
+ })
459
+
460
+ if len(test_payload_with_retry.encode("utf-8")) > self._max_owned_bytes:
461
+ return WorkerResult(
462
+ worker_id=worker_id,
463
+ status=WORKER_FAILED,
464
+ ok=False,
465
+ error=f"budget_exceeded_on_retry: retry would exceed context budget ({len(test_payload_with_retry)} > {self._max_owned_bytes})",
466
+ )
467
+
417
468
  payload["messages"].append(
418
469
  {
419
470
  "role": "assistant",
420
- "content": (
421
- f"(attempt {attempt+1} failed: {last_error})"
422
- ),
471
+ "content": error_msg,
423
472
  }
424
473
  )
425
- # Deterministic nudge: instructs model to return valid JSON only.
426
- # Temperature remains 0 (no sampling change). This is reproducible:
427
- # same input + same nudge + temp=0 -> same output (idempotent retry).
428
- nudge_msg = "Previous response was not valid JSON per the schema; return ONLY the JSON object."
429
474
  payload["messages"].append(
430
475
  {
431
476
  "role": "user",
@@ -449,11 +494,12 @@ class CodexDriver(AgentDriver):
449
494
  for file_entry in structured.get("files", []):
450
495
  path_str = file_entry.get("path", "")
451
496
  if path_str not in request.owned_files:
497
+ # Distinguish: path not in the owned set (security/isolation violation).
452
498
  return WorkerResult(
453
499
  worker_id=worker_id,
454
500
  status=WORKER_FAILED,
455
501
  ok=False,
456
- error=f"worker attempted to write out-of-scope path: {path_str}",
502
+ error=f"out-of-scope: worker attempted to write {path_str} (not in owned set)",
457
503
  )
458
504
  files_to_write.append((path_str, file_entry["contents"]))
459
505
 
@@ -465,11 +511,12 @@ class CodexDriver(AgentDriver):
465
511
  full_path.write_text(new_contents, encoding="utf-8")
466
512
  written_paths.append(path_str)
467
513
  except OSError as exc:
514
+ # Distinguish: owned path exists but write failed (OS error).
468
515
  return WorkerResult(
469
516
  worker_id=worker_id,
470
517
  status=WORKER_FAILED,
471
518
  ok=False,
472
- error=f"failed to write file {path_str}: {exc}",
519
+ error=f"write_failed: {path_str}: {exc}",
473
520
  )
474
521
 
475
522
  # 10. Cost tracking: read usage.total_tokens (fail-closed-honest).
@@ -140,6 +140,39 @@ def default_openai_transport(
140
140
 
141
141
  return json.loads(body)
142
142
 
143
+ except urllib.error.HTTPError as exc:
144
+ # Extract error details from response body if available.
145
+ # HTTPError.fp contains the response body with potentially actionable
146
+ # error codes (e.g., insufficient_quota vs rate_limit_exceeded).
147
+ error_code = None
148
+ error_message = None
149
+ if exc.fp:
150
+ try:
151
+ # Read bounded response body (~500 bytes) to avoid memory issues.
152
+ error_body = exc.fp.read(500).decode("utf-8", errors="replace")
153
+ error_data = json.loads(error_body)
154
+ if isinstance(error_data, dict) and "error" in error_data:
155
+ error_obj = error_data["error"]
156
+ if isinstance(error_obj, dict):
157
+ error_code = error_obj.get("code")
158
+ error_message = error_obj.get("message")
159
+ except (json.JSONDecodeError, ValueError, AttributeError):
160
+ # If we can't parse the body, fall back to plain message.
161
+ pass
162
+
163
+ # Build exception message with extracted details if available.
164
+ # Never include request headers or API key material.
165
+ if error_code or error_message:
166
+ details = []
167
+ if error_code:
168
+ details.append(error_code)
169
+ if error_message:
170
+ details.append(error_message)
171
+ exc_msg = f"OpenAI API request failed: HTTP Error {exc.code}: {exc.msg} [{', '.join(details)}]"
172
+ else:
173
+ exc_msg = f"OpenAI API request failed: HTTP Error {exc.code}: {exc.msg}"
174
+ raise RuntimeError(exc_msg) from exc
175
+
143
176
  except urllib.error.URLError as exc:
144
177
  raise RuntimeError(f"OpenAI API request failed: {exc}") from exc
145
178
  except json.JSONDecodeError as exc:
@@ -94,9 +94,17 @@ def build_manifest_item(driver: AgentDriver, item: Dict[str, Any]) -> Dict[str,
94
94
  model = driver.resolve_model(ROLE_WORKER)
95
95
 
96
96
  # Resolve all four policy knobs from the backend's verification tier.
97
- policy = verification_policy(caps)
97
+ # Wrap to provide driver name in error.
98
+ try:
99
+ policy = verification_policy(caps)
100
+ except ValueError as e:
101
+ raise ValueError(
102
+ f"Unsupported verification tier {caps.recommended_verification_tier} "
103
+ f"from driver '{driver.name}': {e}"
104
+ )
98
105
 
99
106
  # Copy the input item and enrich with model, tier, and all four policy knobs.
107
+ # Preserve all original fields including the optional `repo` field.
100
108
  result = dict(item)
101
109
  result["model"] = model
102
110
  result["verificationTier"] = caps.recommended_verification_tier