@matt82198/aesop 0.3.2 → 0.4.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/CHANGELOG.md +23 -0
- package/README.md +14 -6
- package/aesop.config.example.json +16 -0
- package/bin/cli.js +5 -2
- package/docs/INSTALL.md +104 -14
- package/docs/MICROKERNEL.md +452 -0
- package/docs/README.md +4 -0
- package/docs/THE-AESOP-HYPOTHESIS.md +140 -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/seated_shadow_adjudication.py +920 -0
- package/tools/shadow_adjudication.py +1024 -0
- package/tools/verify_ui_trio_redaction_proof.py +292 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Two-tier adjudication gate: challenger + incumbent escalation (swap increment 3).
|
|
3
|
+
|
|
4
|
+
CONSERVATIVE design: a cheaper challenger model handles adjudication decisions,
|
|
5
|
+
but escalates every doubtful call to the incumbent (frontier model) for safety.
|
|
6
|
+
|
|
7
|
+
Flow:
|
|
8
|
+
1. Call challenger.decide()
|
|
9
|
+
2. If verdict=='DECISION_FAILED', escalate (source: escalated-failed)
|
|
10
|
+
3. If verdict=='undetermined', escalate (source: escalated-undetermined)
|
|
11
|
+
4. If confidence < threshold, escalate (source: escalated-lowconf)
|
|
12
|
+
5. If decision_type not in allowed_decision_types, escalate (source: escalated-disallowed-type)
|
|
13
|
+
6. Otherwise, with probability spot_check_frac, escalate for audit (source: escalated-spotcheck)
|
|
14
|
+
7. Else accept challenger verdict as final
|
|
15
|
+
|
|
16
|
+
SAFETY INVARIANT:
|
|
17
|
+
The gate's output verdict on any given item is EITHER the challenger's confident
|
|
18
|
+
non-undetermined verdict OR the incumbent's resolvable verdict (not DECISION_FAILED/undetermined).
|
|
19
|
+
It NEVER emits an undetermined/DECISION_FAILED/low-confidence challenger verdict as final.
|
|
20
|
+
|
|
21
|
+
DEFENSIVE CORRECTNESS (incumbent validation):
|
|
22
|
+
When escalating to the incumbent, the gate validates the incumbent's returned verdict.
|
|
23
|
+
If the incumbent ALSO returned DECISION_FAILED or undetermined (both failed to decide),
|
|
24
|
+
the gate marks this as explicitly unresolved (escalation_unresolved=True) rather than
|
|
25
|
+
silently presenting the incumbent's failure as a confident verdict. The gate passes
|
|
26
|
+
through the incumbent's verdict (it is the ground truth) but flags it as unresolved.
|
|
27
|
+
The gate is only as safe as its incumbent — if both fail, the gate surfaces an
|
|
28
|
+
explicit unresolved terminal, never a fabricated verdict.
|
|
29
|
+
|
|
30
|
+
DETERMINISM:
|
|
31
|
+
Spot-check decisions are deterministic (seeded) so tests are reproducible.
|
|
32
|
+
The caller can supply a fixed seed or a per-call nonce.
|
|
33
|
+
|
|
34
|
+
stdlib-only, ASCII-only, Windows + Linux safe.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
import hashlib
|
|
38
|
+
import json
|
|
39
|
+
from dataclasses import dataclass, field
|
|
40
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class AdjudicationGate:
|
|
45
|
+
"""Two-tier gate: challenger decides, incumbent escalates on doubt.
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
challenger: An OrchestratorDriver instance (cheap backend).
|
|
49
|
+
incumbent_fn: A callable(decision_type, context_pack, schema) -> dict
|
|
50
|
+
that returns the incumbent's verdict (frontier model).
|
|
51
|
+
escalate_on_undetermined: If True (default), escalate undetermined verdicts
|
|
52
|
+
to incumbent. If False (not recommended), accept
|
|
53
|
+
undetermined as final (violates safety invariant).
|
|
54
|
+
escalate_confidence_below: Float threshold (0.0-1.0, default 0.70).
|
|
55
|
+
If challenger's confidence < threshold, escalate.
|
|
56
|
+
spot_check_frac: Float probability (0.0-1.0, default 0.10) of escalating
|
|
57
|
+
a confident challenger verdict for audit sampling.
|
|
58
|
+
allowed_decision_types: List of decision types the challenger may decide
|
|
59
|
+
without escalation (default: all). If empty, all
|
|
60
|
+
types are allowed. Narrow this to exclude narrative
|
|
61
|
+
mechanisms the ladder showed weaker models struggle with.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
challenger: Any # OrchestratorDriver
|
|
65
|
+
incumbent_fn: Callable[[str, Any, Optional[Dict[str, Any]]], Dict[str, Any]]
|
|
66
|
+
escalate_on_undetermined: bool = True
|
|
67
|
+
escalate_confidence_below: float = 0.70
|
|
68
|
+
spot_check_frac: float = 0.10
|
|
69
|
+
allowed_decision_types: List[str] = field(default_factory=list)
|
|
70
|
+
|
|
71
|
+
def adjudicate(
|
|
72
|
+
self,
|
|
73
|
+
decision_type: str,
|
|
74
|
+
context_pack: Any, # ContextPack
|
|
75
|
+
schema: Optional[Dict[str, Any]] = None,
|
|
76
|
+
) -> Dict[str, Any]:
|
|
77
|
+
"""Adjudicate using challenger, escalate on doubt, return safe verdict.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
decision_type: Name of the decision class (e.g., 'rank_backlog').
|
|
81
|
+
context_pack: ContextPack with file-brain snapshot.
|
|
82
|
+
schema: Optional JSON schema for decision validation.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Dict with keys:
|
|
86
|
+
- verdict: The final verdict (from challenger or incumbent).
|
|
87
|
+
- evidence: Reasoning for the verdict.
|
|
88
|
+
- confidence: Confidence score (from challenger or incumbent).
|
|
89
|
+
- source: Where the verdict came from:
|
|
90
|
+
* 'challenger' = challenger confident, allowed, not spot-checked
|
|
91
|
+
* 'escalated-undetermined' = challenger returned undetermined
|
|
92
|
+
* 'escalated-lowconf' = challenger confidence below threshold
|
|
93
|
+
* 'escalated-failed' = challenger returned DECISION_FAILED
|
|
94
|
+
* 'escalated-disallowed-type' = decision_type not in allowed set
|
|
95
|
+
* 'escalated-spotcheck' = challenger confident but sampled for audit
|
|
96
|
+
- challenger_verdict: The raw challenger output (always retained for audit).
|
|
97
|
+
- incumbent_verdict: Present only if escalated (the incumbent's verdict).
|
|
98
|
+
|
|
99
|
+
SAFETY INVARIANT (enforced by construction):
|
|
100
|
+
The returned verdict is EITHER:
|
|
101
|
+
(a) The challenger's confident (confidence >= threshold) non-undetermined
|
|
102
|
+
verdict for an allowed decision type (not spot-checked), OR
|
|
103
|
+
(b) The incumbent's verdict (escalated for safety).
|
|
104
|
+
It is NEVER an undetermined/DECISION_FAILED/low-confidence challenger
|
|
105
|
+
verdict as final. Tests assert this holds for every case.
|
|
106
|
+
"""
|
|
107
|
+
# Call challenger.
|
|
108
|
+
challenger_result = self.challenger.decide(decision_type, context_pack, schema)
|
|
109
|
+
|
|
110
|
+
# Extract confidence from challenger (default to 0.0 if absent).
|
|
111
|
+
# Fail-closed coercions: bool (True == 1 would fake full confidence)
|
|
112
|
+
# and NaN (every comparison is False, silently bypassing the low-conf
|
|
113
|
+
# escalation) are NOT valid confidence values.
|
|
114
|
+
challenger_confidence = challenger_result.get("confidence", 0.0)
|
|
115
|
+
if (
|
|
116
|
+
isinstance(challenger_confidence, bool)
|
|
117
|
+
or not isinstance(challenger_confidence, (int, float))
|
|
118
|
+
or challenger_confidence != challenger_confidence # NaN
|
|
119
|
+
):
|
|
120
|
+
challenger_confidence = 0.0
|
|
121
|
+
|
|
122
|
+
# Extract verdict (DECISION_FAILED means failure). Normalize for the
|
|
123
|
+
# reserved-terminal checks: case variants ("decision_failed",
|
|
124
|
+
# "UNDETERMINED") and non-string verdicts must not slip past the
|
|
125
|
+
# escalation rules (fail-closed: non-string == failure).
|
|
126
|
+
challenger_verdict = challenger_result.get("verdict")
|
|
127
|
+
if isinstance(challenger_verdict, str):
|
|
128
|
+
_verdict_norm = challenger_verdict.strip().upper()
|
|
129
|
+
else:
|
|
130
|
+
_verdict_norm = "DECISION_FAILED"
|
|
131
|
+
|
|
132
|
+
# Rule 1: Challenger failed.
|
|
133
|
+
if _verdict_norm == "DECISION_FAILED":
|
|
134
|
+
incumbent_result = self.incumbent_fn(decision_type, context_pack, schema)
|
|
135
|
+
result_dict = {
|
|
136
|
+
"verdict": incumbent_result.get("verdict"),
|
|
137
|
+
"evidence": incumbent_result.get("evidence", []),
|
|
138
|
+
"confidence": incumbent_result.get("confidence", 0.0),
|
|
139
|
+
"source": "escalated-failed",
|
|
140
|
+
"challenger_verdict": challenger_result,
|
|
141
|
+
"incumbent_verdict": incumbent_result,
|
|
142
|
+
}
|
|
143
|
+
# Defensive correctness: mark as unresolved if incumbent also failed.
|
|
144
|
+
if self._is_verdict_unresolved(incumbent_result):
|
|
145
|
+
result_dict["escalation_unresolved"] = True
|
|
146
|
+
return result_dict
|
|
147
|
+
|
|
148
|
+
# Rule 2: Challenger returned undetermined.
|
|
149
|
+
if self.escalate_on_undetermined and _verdict_norm == "UNDETERMINED":
|
|
150
|
+
incumbent_result = self.incumbent_fn(decision_type, context_pack, schema)
|
|
151
|
+
result_dict = {
|
|
152
|
+
"verdict": incumbent_result.get("verdict"),
|
|
153
|
+
"evidence": incumbent_result.get("evidence", []),
|
|
154
|
+
"confidence": incumbent_result.get("confidence", 0.0),
|
|
155
|
+
"source": "escalated-undetermined",
|
|
156
|
+
"challenger_verdict": challenger_result,
|
|
157
|
+
"incumbent_verdict": incumbent_result,
|
|
158
|
+
}
|
|
159
|
+
# Defensive correctness: mark as unresolved if incumbent also failed.
|
|
160
|
+
if self._is_verdict_unresolved(incumbent_result):
|
|
161
|
+
result_dict["escalation_unresolved"] = True
|
|
162
|
+
return result_dict
|
|
163
|
+
|
|
164
|
+
# Rule 3: Challenger confidence below threshold.
|
|
165
|
+
if challenger_confidence < self.escalate_confidence_below:
|
|
166
|
+
incumbent_result = self.incumbent_fn(decision_type, context_pack, schema)
|
|
167
|
+
result_dict = {
|
|
168
|
+
"verdict": incumbent_result.get("verdict"),
|
|
169
|
+
"evidence": incumbent_result.get("evidence", []),
|
|
170
|
+
"confidence": incumbent_result.get("confidence", 0.0),
|
|
171
|
+
"source": "escalated-lowconf",
|
|
172
|
+
"challenger_verdict": challenger_result,
|
|
173
|
+
"incumbent_verdict": incumbent_result,
|
|
174
|
+
}
|
|
175
|
+
# Defensive correctness: mark as unresolved if incumbent also failed.
|
|
176
|
+
if self._is_verdict_unresolved(incumbent_result):
|
|
177
|
+
result_dict["escalation_unresolved"] = True
|
|
178
|
+
return result_dict
|
|
179
|
+
|
|
180
|
+
# Rule 4: Decision type not allowed (if allowed list is non-empty).
|
|
181
|
+
if (
|
|
182
|
+
self.allowed_decision_types
|
|
183
|
+
and decision_type not in self.allowed_decision_types
|
|
184
|
+
):
|
|
185
|
+
incumbent_result = self.incumbent_fn(decision_type, context_pack, schema)
|
|
186
|
+
result_dict = {
|
|
187
|
+
"verdict": incumbent_result.get("verdict"),
|
|
188
|
+
"evidence": incumbent_result.get("evidence", []),
|
|
189
|
+
"confidence": incumbent_result.get("confidence", 0.0),
|
|
190
|
+
"source": "escalated-disallowed-type",
|
|
191
|
+
"challenger_verdict": challenger_result,
|
|
192
|
+
"incumbent_verdict": incumbent_result,
|
|
193
|
+
}
|
|
194
|
+
# Defensive correctness: mark as unresolved if incumbent also failed.
|
|
195
|
+
if self._is_verdict_unresolved(incumbent_result):
|
|
196
|
+
result_dict["escalation_unresolved"] = True
|
|
197
|
+
return result_dict
|
|
198
|
+
|
|
199
|
+
# Rule 5: Spot-check sample (deterministic, not random).
|
|
200
|
+
if self._should_spot_check(decision_type, context_pack):
|
|
201
|
+
incumbent_result = self.incumbent_fn(decision_type, context_pack, schema)
|
|
202
|
+
result_dict = {
|
|
203
|
+
"verdict": incumbent_result.get("verdict"),
|
|
204
|
+
"evidence": incumbent_result.get("evidence", []),
|
|
205
|
+
"confidence": incumbent_result.get("confidence", 0.0),
|
|
206
|
+
"source": "escalated-spotcheck",
|
|
207
|
+
"challenger_verdict": challenger_result,
|
|
208
|
+
"incumbent_verdict": incumbent_result,
|
|
209
|
+
}
|
|
210
|
+
# Defensive correctness: mark as unresolved if incumbent also failed.
|
|
211
|
+
if self._is_verdict_unresolved(incumbent_result):
|
|
212
|
+
result_dict["escalation_unresolved"] = True
|
|
213
|
+
return result_dict
|
|
214
|
+
|
|
215
|
+
# Accept challenger verdict.
|
|
216
|
+
return {
|
|
217
|
+
"verdict": challenger_verdict,
|
|
218
|
+
"evidence": challenger_result.get("evidence", []),
|
|
219
|
+
"confidence": challenger_confidence,
|
|
220
|
+
"source": "challenger",
|
|
221
|
+
"challenger_verdict": challenger_result,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
def _is_verdict_unresolved(self, result: Dict[str, Any]) -> bool:
|
|
225
|
+
"""Check if a verdict result is unresolved.
|
|
226
|
+
|
|
227
|
+
Unresolved = DECISION_FAILED, undetermined (any case variant), or a
|
|
228
|
+
missing/None/non-string verdict (a malformed incumbent result without
|
|
229
|
+
a 'verdict' key must never be presented as a confident final verdict).
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
result: A verdict dict with a 'verdict' key.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
True if the verdict is DECISION_FAILED, undetermined, or absent.
|
|
236
|
+
"""
|
|
237
|
+
verdict = result.get("verdict")
|
|
238
|
+
if not isinstance(verdict, str):
|
|
239
|
+
# Missing/None/non-string verdict is unresolved (fail-closed);
|
|
240
|
+
# never present it as a resolved incumbent verdict.
|
|
241
|
+
return True
|
|
242
|
+
return verdict.strip().upper() in ("DECISION_FAILED", "UNDETERMINED")
|
|
243
|
+
|
|
244
|
+
def _should_spot_check(self, decision_type: str, context_pack: Any) -> bool:
|
|
245
|
+
"""Deterministically decide if this call should be spot-checked.
|
|
246
|
+
|
|
247
|
+
Spot-check is seeded per ITEM (not per decision_type). Each distinct item
|
|
248
|
+
(context_pack with different content AND different evidence) gets an independent
|
|
249
|
+
draw at the configured fraction. The same item (same content + same evidence)
|
|
250
|
+
always produces the same decision (deterministic, reproducible across runs).
|
|
251
|
+
Different items produce independent decisions, so approximately spot_check_frac
|
|
252
|
+
of all items get sampled.
|
|
253
|
+
|
|
254
|
+
FIX (BL2 Finding 1): Fold evidence into the canonical digest. In production,
|
|
255
|
+
content is the SHARED file-brain (identical across all findings in a wave);
|
|
256
|
+
the per-item distinguisher is pack.evidence. Without folding evidence, every
|
|
257
|
+
item in a wave hashes identically → spot-check is 0% or 100% of the WAVE.
|
|
258
|
+
With evidence, spot-check is ~spot_check_frac per ITEM (independent draws).
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
decision_type: The decision type string.
|
|
262
|
+
context_pack: The context pack, used to build a stable content+evidence digest.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
True if this call should be escalated for audit, False otherwise.
|
|
266
|
+
"""
|
|
267
|
+
# Build a stable per-item key from (decision_type + context_pack content + evidence).
|
|
268
|
+
# Extract content digest from the pack (try .content attr, fall back to str).
|
|
269
|
+
canonical = decision_type + "|"
|
|
270
|
+
if hasattr(context_pack, "content") and isinstance(context_pack.content, dict):
|
|
271
|
+
# Sorted JSON of content items for stable serialization.
|
|
272
|
+
try:
|
|
273
|
+
content_text = json.dumps(
|
|
274
|
+
sorted(context_pack.content.items()), separators=(",", ":")
|
|
275
|
+
)
|
|
276
|
+
canonical += content_text
|
|
277
|
+
except (TypeError, ValueError):
|
|
278
|
+
# If JSON encoding fails, fall back to str.
|
|
279
|
+
canonical += str(context_pack)
|
|
280
|
+
else:
|
|
281
|
+
# No .content attr; use string representation.
|
|
282
|
+
canonical += str(context_pack)
|
|
283
|
+
|
|
284
|
+
# FIX (BL2): Fold evidence into the canonical digest so spot-check is per-item.
|
|
285
|
+
canonical += "|"
|
|
286
|
+
if hasattr(context_pack, "evidence"):
|
|
287
|
+
evidence = context_pack.evidence
|
|
288
|
+
if isinstance(evidence, dict):
|
|
289
|
+
# Sorted JSON of evidence for stable serialization.
|
|
290
|
+
try:
|
|
291
|
+
evidence_text = json.dumps(
|
|
292
|
+
sorted(evidence.items()), separators=(",", ":")
|
|
293
|
+
)
|
|
294
|
+
canonical += evidence_text
|
|
295
|
+
except (TypeError, ValueError):
|
|
296
|
+
# F10 FIX: If sorting fails (unsortable keys), use repr on the dict itself
|
|
297
|
+
# (not sorted items). This fallback CANNOT raise from sorted() again.
|
|
298
|
+
canonical += repr(evidence)
|
|
299
|
+
else:
|
|
300
|
+
# Non-dict evidence: convert to stable string.
|
|
301
|
+
canonical += repr(evidence)
|
|
302
|
+
# If no evidence attr, leave it blank (canonical += "")
|
|
303
|
+
|
|
304
|
+
# Hash the canonical key to get a deterministic integer.
|
|
305
|
+
hash_val = int(hashlib.md5(canonical.encode()).hexdigest(), 16)
|
|
306
|
+
# Sample at the configured fraction (0.0-1.0).
|
|
307
|
+
# round() before int(): int(0.29 * 100) == 28 due to float representation,
|
|
308
|
+
# which would silently under-sample the configured fraction.
|
|
309
|
+
return (hash_val % 100) < int(round(self.spot_check_frac * 100))
|
|
310
|
+
|
|
311
|
+
def summarize_run(self, results: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
312
|
+
"""Summarize adjudication run statistics.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
results: List of dicts returned by adjudicate() calls.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Dict with:
|
|
319
|
+
- n: Total number of adjudications.
|
|
320
|
+
- accepted_challenger: Count of verdicts from challenger (source=='challenger').
|
|
321
|
+
- escalated_by_reason: Dict mapping reason strings to counts.
|
|
322
|
+
- spot_check_agreements: Count of escalated-spotcheck where incumbent
|
|
323
|
+
agreed with challenger.
|
|
324
|
+
- spot_check_disagreements: Count of escalated-spotcheck where they disagreed.
|
|
325
|
+
- effective_escalation_rate: Fraction of calls that were escalated.
|
|
326
|
+
"""
|
|
327
|
+
n = len(results)
|
|
328
|
+
accepted_challenger = 0
|
|
329
|
+
escalated_by_reason = {}
|
|
330
|
+
spot_check_agreements = 0
|
|
331
|
+
spot_check_disagreements = 0
|
|
332
|
+
|
|
333
|
+
for result in results:
|
|
334
|
+
source = result.get("source", "unknown")
|
|
335
|
+
|
|
336
|
+
if source == "challenger":
|
|
337
|
+
accepted_challenger += 1
|
|
338
|
+
else:
|
|
339
|
+
# Escalated for some reason.
|
|
340
|
+
escalated_by_reason[source] = escalated_by_reason.get(source, 0) + 1
|
|
341
|
+
|
|
342
|
+
# Track spot-check agreement.
|
|
343
|
+
if source == "escalated-spotcheck":
|
|
344
|
+
challenger_verdict = (
|
|
345
|
+
result.get("challenger_verdict", {}).get("verdict")
|
|
346
|
+
)
|
|
347
|
+
incumbent_verdict = (
|
|
348
|
+
result.get("incumbent_verdict", {}).get("verdict")
|
|
349
|
+
)
|
|
350
|
+
if challenger_verdict == incumbent_verdict:
|
|
351
|
+
spot_check_agreements += 1
|
|
352
|
+
else:
|
|
353
|
+
spot_check_disagreements += 1
|
|
354
|
+
|
|
355
|
+
escalated_count = n - accepted_challenger
|
|
356
|
+
effective_escalation_rate = (
|
|
357
|
+
escalated_count / n if n > 0 else 0.0
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
"n": n,
|
|
362
|
+
"accepted_challenger": accepted_challenger,
|
|
363
|
+
"escalated_by_reason": escalated_by_reason,
|
|
364
|
+
"spot_check_agreements": spot_check_agreements,
|
|
365
|
+
"spot_check_disagreements": spot_check_disagreements,
|
|
366
|
+
"effective_escalation_rate": effective_escalation_rate,
|
|
367
|
+
}
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
{
|
|
10
10
|
"name": "OpenAI Codex / Chat Completions",
|
|
11
11
|
"backend": "codex",
|
|
12
|
-
"model": "gpt-
|
|
13
|
-
"description": "Uses OpenAI's Chat Completions API. Set OPENAI_API_KEY environment variable before running. Tier 2 verification (validate all JSON, spot-check, repair budget).",
|
|
12
|
+
"model": "gpt-4o-mini",
|
|
13
|
+
"description": "Uses OpenAI's Chat Completions API. Set OPENAI_API_KEY environment variable before running. Tier 2 verification (validate all JSON, spot-check, repair budget). Model must support json_schema structured output (gpt-4o-mini does; gpt-3.5-class models are rejected at init).",
|
|
14
14
|
"optional_fields": {
|
|
15
15
|
"model": "gpt-4-turbo for stronger verification",
|
|
16
16
|
"max_owned_bytes": 200000,
|
|
@@ -65,10 +65,26 @@
|
|
|
65
65
|
"backend": "claude",
|
|
66
66
|
"comment": "Recommended production configuration: Claude Code with Haiku workers, Tier 1 verification. Drop this into aesop.config.json (removing comments) to use."
|
|
67
67
|
},
|
|
68
|
+
"unified_seats_config": {
|
|
69
|
+
"comment": "HS-1 (0.4.0): ONE namespaced block selects BOTH seats. seats.worker takes the same fields as the legacy flat block above (and wins over it when both are present); seats.orchestrator swaps the DECISION seat (backend 'harness' = the live Claude Code session, the default; 'openai-compatible' routes OrchestratorDriver.decide() to an API model). NO seats block = today's behavior exactly: Claude worker + harness orchestrator, no key required. Example: local Ollama worker + hosted gpt-4o-mini orchestrator:",
|
|
70
|
+
"seats": {
|
|
71
|
+
"worker": {
|
|
72
|
+
"backend": "openai-compatible",
|
|
73
|
+
"base_url": "http://localhost:11434/v1",
|
|
74
|
+
"model": "mistral",
|
|
75
|
+
"is_local": true
|
|
76
|
+
},
|
|
77
|
+
"orchestrator": {
|
|
78
|
+
"backend": "openai-compatible",
|
|
79
|
+
"model": "gpt-4o-mini",
|
|
80
|
+
"api_key_env": "OPENAI_API_KEY"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
68
84
|
"experimental_codex": {
|
|
69
85
|
"backend": "codex",
|
|
70
|
-
"model": "gpt-
|
|
71
|
-
"comment": "Experimental Codex backend. Requires OPENAI_API_KEY set. Tier 2 verification."
|
|
86
|
+
"model": "gpt-4o-mini",
|
|
87
|
+
"comment": "Experimental Codex backend (json_schema-capable model required). Requires OPENAI_API_KEY set. Tier 2 verification. NOTE: as a bare legacy flat block this is INERT in the wave scheduler's default path -- move it under seats.worker to activate it."
|
|
72
88
|
},
|
|
73
89
|
"experimental_ollama": {
|
|
74
90
|
"backend": "openai-compatible",
|