@event4u/agent-config 14.1.0 → 14.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +291 -181
- package/agents/templates/.ai-council.yml.example +35 -0
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +83 -12
- package/dist/agent-src/rules/roadmap-progress-sync.md +48 -4
- package/dist/agent-src/scripts/resume_probe.ts +154 -9
- package/dist/agent-src/scripts/roadmap_gates.ts +210 -6
- package/dist/agent-src/skills/roadmap-management/SKILL.md +9 -4
- package/dist/agent-src/templates/agent-settings.md +70 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/telemetry/remote.ts +571 -0
- package/dist/agent-src/templates/scripts/telemetry/settings.ts +222 -44
- package/dist/cli/python/workspace_drive.js +8 -3
- package/dist/cli/python/workspace_drive.js.map +1 -1
- package/dist/cli/python/workspace_hosts.js +16 -12
- package/dist/cli/python/workspace_hosts.js.map +1 -1
- package/dist/cli/registry.js +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-F3YUF7SA.js → chunk-3UCJWMMP.js} +1 -1
- package/dist/cli-delegate/chunk-F7DQWYR5.js +69 -0
- package/dist/cli-delegate/{chunk-7PZ4FSZB.js → chunk-FBKL6JWT.js} +61 -2
- package/dist/cli-delegate/{chunk-MNWRDMR5.js → chunk-PKOBQADP.js} +1 -1
- package/dist/cli-delegate/cmd_conformance.js +26 -11
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +3 -3
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +17 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +4 -4
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +3773 -1731
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1070 -408
- package/dist/scripts/_lib/host_launch.js +66 -0
- package/dist/scripts/_lib/host_launch.js.map +1 -0
- package/dist/server/io/yamlIO.js +175 -3
- package/dist/server/io/yamlIO.js.map +1 -1
- package/dist/server/routes/wizard.js +20 -1
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/shared/settingsConsent.js +25 -0
- package/dist/shared/settingsConsent.js.map +1 -1
- package/docs/contracts/settings-classes.md +11 -0
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +2 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/resume_probe.ts +154 -9
- package/src/agent-src/scripts/roadmap_gates.ts +210 -6
- package/src/agent-src/templates/scripts/telemetry/remote.ts +571 -0
- package/src/agent-src/templates/scripts/telemetry/settings.ts +222 -44
- package/src/config/estate-count-budget.json +120 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +32 -4
- package/src/config/gate-violation-baselines.json +63 -63
- package/src/config/gitignore-block.txt +5 -0
- package/src/config/hook-latency-budget.json +40 -1
- package/src/config/hook-token-budget.json +22 -0
- package/src/config/rule-stub-ceilings.json +11 -3
- package/src/scripts/_dispatch.bash +42 -1
- package/src/scripts/_lib/council_fallback_posture.ts +81 -0
- package/src/scripts/_lib/council_fallback_wiring.ts +126 -0
- package/src/scripts/_lib/council_settings_block.ts +152 -0
- package/src/scripts/_lib/headless_invocation.ts +285 -0
- package/src/scripts/_lib/host_launch.ts +72 -0
- package/src/scripts/_lib/map_to_object.ts +16 -0
- package/src/scripts/_lib/roadmap_checkboxes.ts +88 -0
- package/src/scripts/_lib/run_checkpoint.ts +409 -0
- package/src/scripts/_lib/session_register.ts +32 -1
- package/src/scripts/_lib/skill_catalogue_series.ts +91 -0
- package/src/scripts/_lib/stdin.ts +35 -1
- package/src/scripts/_lib/turn_end_refusals.ts +12 -4
- package/src/scripts/_lib/unattended_guard.ts +371 -0
- package/src/scripts/ai_council/config.ts +27 -1
- package/src/scripts/ai_council/events_log.ts +12 -1
- package/src/scripts/ai_council/fallback_config.ts +114 -0
- package/src/scripts/ai_council/mid_flight_fallback.ts +295 -0
- package/src/scripts/ai_council/orchestrator.ts +251 -292
- package/src/scripts/ai_council/py_parity.ts +139 -0
- package/src/scripts/ai_council/response_render.ts +99 -0
- package/src/scripts/ai_council/spend_gate.ts +123 -0
- package/src/scripts/ai_council/transport_resolver.ts +81 -13
- package/src/scripts/bench_hook_injection.ts +100 -6
- package/src/scripts/bench_hook_latency.ts +442 -16
- package/src/scripts/check_estate_count.ts +924 -0
- package/src/scripts/compile_hook_manifest.ts +67 -0
- package/src/scripts/council_cli.ts +100 -125
- package/src/scripts/decision_memo.ts +325 -0
- package/src/scripts/dispatch_r2_reviewer.ts +169 -15
- package/src/scripts/hook_manifest.json +1 -0
- package/src/scripts/hook_manifest.yaml +172 -25
- package/src/scripts/hooks/concern_registry.ts +6 -0
- package/src/scripts/hooks/dispatch_hook.ts +183 -10
- package/src/scripts/hooks/dispatch_issues.ts +26 -0
- package/src/scripts/hooks/end_review_nudge_hook.ts +97 -24
- package/src/scripts/hooks/hook_stdin.ts +111 -5
- package/src/scripts/hooks/injection_budget.ts +595 -0
- package/src/scripts/hooks/payload_stub.ts +446 -0
- package/src/scripts/hooks/run_continuation_hook.ts +627 -0
- package/src/scripts/hooks/session_eol_hook.ts +52 -0
- package/src/scripts/hooks/skill_route_hook.ts +172 -11
- package/src/scripts/hooks/telemetry_usage_hook.ts +339 -0
- package/src/scripts/hooks/tool_result_bytes_hook.ts +27 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +45 -7
- package/src/scripts/hooks_doctor.ts +16 -2
- package/src/scripts/install-hooks.sh +26 -0
- package/src/scripts/interruption_report.ts +330 -13
- package/src/scripts/lint_hook_manifest.ts +167 -0
- package/src/scripts/roadmap_progress_hook.ts +254 -46
- package/src/scripts/run_supervise.ts +623 -0
- package/src/scripts/session_register_hook.ts +108 -8
- package/src/scripts/sessions_cli.ts +18 -6
- package/src/scripts/telemetry_disclosure_hook.ts +244 -0
- package/src/server/io/yamlIO.ts +167 -3
- package/src/shared/settingsConsent.ts +49 -2
- package/dist/cli-delegate/chunk-MMJL4TIX.js +0 -18
|
@@ -70,70 +70,60 @@ import {
|
|
|
70
70
|
} from './prompts.js';
|
|
71
71
|
import { count_dissenters, dissent_quota_met, is_near_duplicate } from './debate_gates.js';
|
|
72
72
|
import { parse_stance_line, render_vote_tally, tally_stances } from './stance_tally.js';
|
|
73
|
+
import { MidFlightFallback } from './transport_resolver.js';
|
|
74
|
+
import {
|
|
75
|
+
emitOutcome,
|
|
76
|
+
escalateUnmetered,
|
|
77
|
+
refuseUnmeteredEscalation,
|
|
78
|
+
establishTwin,
|
|
79
|
+
isFailedCliCall,
|
|
80
|
+
runGatedRetry,
|
|
81
|
+
stampFallback,
|
|
82
|
+
type CliFallbackOptions,
|
|
83
|
+
type EstablishedTwin,
|
|
84
|
+
type TwinMap,
|
|
85
|
+
} from './mid_flight_fallback.js';
|
|
86
|
+
|
|
87
|
+
export type { CliFallbackOptions, EstablishedTwin } from './mid_flight_fallback.js';
|
|
88
|
+
|
|
89
|
+
// The spend gate, extracted whole. Re-exported so every existing importer of
|
|
90
|
+
// `CostBudget` / `OverrunEvent` from this module keeps working unchanged.
|
|
91
|
+
export {
|
|
92
|
+
CostBudget,
|
|
93
|
+
OverrunEvent,
|
|
94
|
+
_breach,
|
|
95
|
+
_total_usd,
|
|
96
|
+
type BreachKind,
|
|
97
|
+
type Spent,
|
|
98
|
+
} from './spend_gate.js';
|
|
99
|
+
import {
|
|
100
|
+
CostBudget,
|
|
101
|
+
OverrunEvent,
|
|
102
|
+
_breach,
|
|
103
|
+
_total_usd,
|
|
104
|
+
type Spent,
|
|
105
|
+
} from './spend_gate.js';
|
|
106
|
+
import { callMember, renderResponseMeta } from './response_render.js';
|
|
107
|
+
import {
|
|
108
|
+
_excTag,
|
|
109
|
+
_metaGet,
|
|
110
|
+
_pyFixed,
|
|
111
|
+
_pyInt,
|
|
112
|
+
_pyLStrip,
|
|
113
|
+
_pyLen,
|
|
114
|
+
_pyReprInt,
|
|
115
|
+
_pyRStrip,
|
|
116
|
+
_pySlice,
|
|
117
|
+
_pySplitWhitespace,
|
|
118
|
+
_pyStrip,
|
|
119
|
+
_setdefault,
|
|
120
|
+
} from './py_parity.js';
|
|
121
|
+
|
|
73
122
|
import { CHAIRMAN_FIELDS_ADDENDUM, render_deanonymization_block } from './blind_review.js';
|
|
74
123
|
import type { AbsentReason } from './transport_resolver.js';
|
|
75
124
|
import { isSoloConcluded, type QuorumResult } from './quorum.js';
|
|
76
125
|
import { isEmptyHandoff, type HandoffEnvelope } from './handoff.js';
|
|
77
126
|
|
|
78
|
-
// ── Python-format / stdlib parity helpers ────────────────────────────────
|
|
79
|
-
//
|
|
80
|
-
// The orchestrator formats USD / scores / strengths via Python f-string
|
|
81
|
-
// specs (`:.4f`, `:.2f`, `:.1f`) which round half-to-even on the decimal
|
|
82
|
-
// representation. JS `toFixed` rounds half away from zero, so the spec
|
|
83
|
-
// formatting is reimplemented to stay byte-exact with the retired Python implementation.
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Format `x` to `ndigits` decimals using round-half-to-even, matching
|
|
87
|
-
* CPython's `format(x, ".<ndigits>f")`.
|
|
88
|
-
*/
|
|
89
|
-
function _pyFixed(x: number, ndigits: number): string {
|
|
90
|
-
if (!Number.isFinite(x)) {
|
|
91
|
-
return String(x);
|
|
92
|
-
}
|
|
93
|
-
const neg = x < 0 || Object.is(x, -0);
|
|
94
|
-
const abs = Math.abs(x);
|
|
95
|
-
const factor = Math.pow(10, ndigits);
|
|
96
|
-
const scaled = abs * factor;
|
|
97
|
-
const floor = Math.floor(scaled);
|
|
98
|
-
const frac = scaled - floor;
|
|
99
|
-
const tol = Math.max(Math.abs(scaled), 1) * 2 ** -40;
|
|
100
|
-
let rounded: number;
|
|
101
|
-
if (Math.abs(frac - 0.5) <= tol) {
|
|
102
|
-
rounded = floor % 2 === 0 ? floor : floor + 1;
|
|
103
|
-
} else {
|
|
104
|
-
rounded = Math.round(scaled);
|
|
105
|
-
}
|
|
106
|
-
let intStr = String(rounded);
|
|
107
|
-
let result: string;
|
|
108
|
-
if (ndigits === 0) {
|
|
109
|
-
result = intStr;
|
|
110
|
-
} else {
|
|
111
|
-
if (intStr.length <= ndigits) {
|
|
112
|
-
intStr = '0'.repeat(ndigits - intStr.length + 1) + intStr;
|
|
113
|
-
}
|
|
114
|
-
const whole = intStr.slice(0, intStr.length - ndigits);
|
|
115
|
-
const dec = intStr.slice(intStr.length - ndigits);
|
|
116
|
-
result = `${whole}.${dec}`;
|
|
117
|
-
}
|
|
118
|
-
return neg ? `-${result}` : result;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Mirror Python `str.strip()` (no-arg). Sibling-twin convention uses trim(). */
|
|
122
|
-
function _pyStrip(s: string): string {
|
|
123
|
-
return s.trim();
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Mirror Python `str.split()` (no separator) — split on runs of whitespace,
|
|
128
|
-
* dropping leading / trailing whitespace (no empty tokens).
|
|
129
|
-
*/
|
|
130
|
-
function _pySplitWhitespace(s: string): string[] {
|
|
131
|
-
const trimmed = s.trim();
|
|
132
|
-
if (trimmed === '') {
|
|
133
|
-
return [];
|
|
134
|
-
}
|
|
135
|
-
return trimmed.split(/\s+/);
|
|
136
|
-
}
|
|
137
127
|
|
|
138
128
|
/**
|
|
139
129
|
* Mirror Python `getattr(obj, name, fallback)` for the duck-typed member
|
|
@@ -160,30 +150,6 @@ function _label(idx: number): string {
|
|
|
160
150
|
|
|
161
151
|
// ── dataclasses ───────────────────────────────────────────────────────
|
|
162
152
|
|
|
163
|
-
export class CostBudget {
|
|
164
|
-
max_input_tokens: number;
|
|
165
|
-
max_output_tokens: number;
|
|
166
|
-
max_calls: number;
|
|
167
|
-
max_total_usd: number; // 0 = USD ceiling disabled (token caps still apply)
|
|
168
|
-
daily_limit_usd: number; // 0 = rolling 24h cap disabled (D3)
|
|
169
|
-
|
|
170
|
-
constructor(
|
|
171
|
-
args: {
|
|
172
|
-
max_input_tokens?: number;
|
|
173
|
-
max_output_tokens?: number;
|
|
174
|
-
max_calls?: number;
|
|
175
|
-
max_total_usd?: number;
|
|
176
|
-
daily_limit_usd?: number;
|
|
177
|
-
} = {},
|
|
178
|
-
) {
|
|
179
|
-
this.max_input_tokens = args.max_input_tokens ?? 50_000;
|
|
180
|
-
this.max_output_tokens = args.max_output_tokens ?? 20_000;
|
|
181
|
-
this.max_calls = args.max_calls ?? 10;
|
|
182
|
-
this.max_total_usd = args.max_total_usd ?? 0.0;
|
|
183
|
-
this.daily_limit_usd = args.daily_limit_usd ?? 0.0;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
153
|
export class CouncilQuestion {
|
|
188
154
|
mode: string; // one of: prompt, roadmap, diff, files
|
|
189
155
|
user_prompt: string; // bundled artefact text
|
|
@@ -196,44 +162,6 @@ export class CouncilQuestion {
|
|
|
196
162
|
}
|
|
197
163
|
}
|
|
198
164
|
|
|
199
|
-
/** Passed to `on_overrun` when projected spend exceeds the budget. */
|
|
200
|
-
export class OverrunEvent {
|
|
201
|
-
member_index: number;
|
|
202
|
-
member: ExternalAIClient;
|
|
203
|
-
next_estimate: CostEstimate; // this member's projected cost
|
|
204
|
-
spent_input_tokens: number; // already-billed totals BEFORE this member
|
|
205
|
-
spent_output_tokens: number;
|
|
206
|
-
spent_usd: number;
|
|
207
|
-
projected_total_usd: number; // spent_usd + next_estimate.total_usd
|
|
208
|
-
daily_spent_usd: number; // rolling 24h spend BEFORE this member (D3)
|
|
209
|
-
daily_limit_usd: number; // the configured daily cap (0 = disabled)
|
|
210
|
-
breach_kind: string; // "session" | "daily" | "tokens"
|
|
211
|
-
|
|
212
|
-
constructor(args: {
|
|
213
|
-
member_index: number;
|
|
214
|
-
member: ExternalAIClient;
|
|
215
|
-
next_estimate: CostEstimate;
|
|
216
|
-
spent_input_tokens: number;
|
|
217
|
-
spent_output_tokens: number;
|
|
218
|
-
spent_usd: number;
|
|
219
|
-
projected_total_usd: number;
|
|
220
|
-
daily_spent_usd?: number;
|
|
221
|
-
daily_limit_usd?: number;
|
|
222
|
-
breach_kind?: string;
|
|
223
|
-
}) {
|
|
224
|
-
this.member_index = args.member_index;
|
|
225
|
-
this.member = args.member;
|
|
226
|
-
this.next_estimate = args.next_estimate;
|
|
227
|
-
this.spent_input_tokens = args.spent_input_tokens;
|
|
228
|
-
this.spent_output_tokens = args.spent_output_tokens;
|
|
229
|
-
this.spent_usd = args.spent_usd;
|
|
230
|
-
this.projected_total_usd = args.projected_total_usd;
|
|
231
|
-
this.daily_spent_usd = args.daily_spent_usd ?? 0.0;
|
|
232
|
-
this.daily_limit_usd = args.daily_limit_usd ?? 0.0;
|
|
233
|
-
this.breach_kind = args.breach_kind ?? 'session';
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
165
|
// Callback signature: receive event → return True (proceed) or False (skip + tag error).
|
|
238
166
|
export type OnOverrunCallback = (event: OverrunEvent) => boolean;
|
|
239
167
|
|
|
@@ -360,10 +288,6 @@ export function estimate_debate_cost(
|
|
|
360
288
|
});
|
|
361
289
|
}
|
|
362
290
|
|
|
363
|
-
/** Mirror the Python `CostEstimate.total_usd` property. */
|
|
364
|
-
function _total_usd(e: CostEstimate): number {
|
|
365
|
-
return e.input_usd + e.output_usd;
|
|
366
|
-
}
|
|
367
291
|
|
|
368
292
|
/**
|
|
369
293
|
* Return a pre-call cost estimate per member, in input order.
|
|
@@ -449,6 +373,8 @@ export interface ConsultOptions {
|
|
|
449
373
|
* context). Default `null` → byte-identical to today.
|
|
450
374
|
*/
|
|
451
375
|
no_project_context_members?: ReadonlySet<string> | null;
|
|
376
|
+
/** Mid-flight cli→api fallback; absent → no retry. See `mid_flight_fallback`. */
|
|
377
|
+
cli_fallback?: CliFallbackOptions | null;
|
|
452
378
|
}
|
|
453
379
|
|
|
454
380
|
/**
|
|
@@ -504,6 +430,11 @@ export function consult(
|
|
|
504
430
|
}
|
|
505
431
|
|
|
506
432
|
const spent: Spent = { input: 0, output: 0, usd: 0.0 };
|
|
433
|
+
// One ledger + one twin map per invocation — the double-spend unit.
|
|
434
|
+
const cli_fallback = opts.cli_fallback ?? null;
|
|
435
|
+
const fallback_ledger = cli_fallback !== null ? new MidFlightFallback() : null;
|
|
436
|
+
const fallback_twins =
|
|
437
|
+
cli_fallback !== null ? new Map<string, EstablishedTwin>() : null;
|
|
507
438
|
let last_results: CouncilResponse[] = [];
|
|
508
439
|
// A3 read unlock: the stable prefix is ALWAYS the original user prompt;
|
|
509
440
|
// per-round critiques + the STANCE contract ride in a volatile suffix so
|
|
@@ -535,6 +466,9 @@ export function consult(
|
|
|
535
466
|
project,
|
|
536
467
|
original_ask,
|
|
537
468
|
advisor_plans,
|
|
469
|
+
cli_fallback,
|
|
470
|
+
fallback_ledger,
|
|
471
|
+
fallback_twins,
|
|
538
472
|
member_prompt_suffix: opts.member_prompt_suffix ?? null,
|
|
539
473
|
no_project_context_members: opts.no_project_context_members ?? null,
|
|
540
474
|
split: suffix_for_round
|
|
@@ -572,12 +506,19 @@ export function consult(
|
|
|
572
506
|
`Reply with ONLY that single line for your final position.\n\n${STANCE_LINE_CONTRACT}`,
|
|
573
507
|
max_tokens: question.max_tokens,
|
|
574
508
|
});
|
|
509
|
+
// Stance repair shares the invocation-wide ledger: transport
|
|
510
|
+
// health and structural validity are INDEPENDENT failure modes,
|
|
511
|
+
// so refusing the retry would discard salvageable work for an
|
|
512
|
+
// unrelated reason. Bounded by the retry's own spend gate.
|
|
575
513
|
const rr = _run_round([member], repairQ, resolvedBudget, spent, {
|
|
576
514
|
table,
|
|
577
515
|
on_overrun,
|
|
578
516
|
project,
|
|
579
517
|
original_ask,
|
|
580
518
|
advisor_plans,
|
|
519
|
+
cli_fallback,
|
|
520
|
+
fallback_ledger,
|
|
521
|
+
fallback_twins,
|
|
581
522
|
});
|
|
582
523
|
const rep = rr[0];
|
|
583
524
|
if (rep !== undefined) {
|
|
@@ -592,12 +533,8 @@ export function consult(
|
|
|
592
533
|
return last_results;
|
|
593
534
|
}
|
|
594
535
|
|
|
595
|
-
interface Spent {
|
|
596
|
-
input: number;
|
|
597
|
-
output: number;
|
|
598
|
-
usd: number;
|
|
599
|
-
}
|
|
600
536
|
|
|
537
|
+
/** Which cap a projected call breaches, or `null` when it fits. */
|
|
601
538
|
interface RunRoundOptions {
|
|
602
539
|
table: PriceTable | null;
|
|
603
540
|
on_overrun: OnOverrunCallback | null;
|
|
@@ -616,6 +553,11 @@ interface RunRoundOptions {
|
|
|
616
553
|
member_prompt_suffix?: Map<string, string> | null;
|
|
617
554
|
/** Ü2 — see `ConsultOptions.no_project_context_members`. */
|
|
618
555
|
no_project_context_members?: ReadonlySet<string> | null;
|
|
556
|
+
/** See `ConsultOptions.cli_fallback`. Both set together, or neither. */
|
|
557
|
+
cli_fallback?: CliFallbackOptions | null;
|
|
558
|
+
fallback_ledger?: MidFlightFallback | null;
|
|
559
|
+
/** Invocation-wide substitution map — see `mid_flight_fallback.TwinMap`. */
|
|
560
|
+
fallback_twins?: TwinMap | null;
|
|
619
561
|
}
|
|
620
562
|
|
|
621
563
|
/** Run a single round; mutate `spent` with cumulative totals. */
|
|
@@ -668,77 +610,111 @@ function _run_round(
|
|
|
668
610
|
})
|
|
669
611
|
: null;
|
|
670
612
|
|
|
613
|
+
const twins = opts.fallback_twins ?? null;
|
|
671
614
|
for (let idx = 0; idx < members.length; idx++) {
|
|
672
|
-
const
|
|
615
|
+
const declared = members[idx] as ExternalAIClient;
|
|
616
|
+
// Sticky substitution: a provider that fell back earlier is
|
|
617
|
+
// replaced BEFORE the call, so the dead binary is spawned once.
|
|
618
|
+
let established = twins?.get(declared.name) ?? null;
|
|
619
|
+
const substituted_on_entry = established !== null;
|
|
620
|
+
let member = established !== null ? established.client : declared;
|
|
621
|
+
// Set only when THIS iteration escalated an unmetered seat; read by
|
|
622
|
+
// the breach handler below so a refusal degrades that one seat instead
|
|
623
|
+
// of the round. See the `unmetered_original !== null` branch there.
|
|
624
|
+
let unmetered_original: CouncilResponse | null = null;
|
|
625
|
+
let unmetered_twin: EstablishedTwin | null = null;
|
|
673
626
|
// ── non-billable members skip the cost gate entirely ─────────
|
|
674
627
|
// ManualClient (and future PlaywrightClient) cost us $0; their
|
|
675
628
|
// token counts are still tracked from the response below for
|
|
676
629
|
// observability, but no projection / budget breach can apply.
|
|
677
630
|
if (!(_getattr(member, 'billable', true) as boolean)) {
|
|
678
|
-
let response
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
631
|
+
let response = callMember(member, question, opts, _system_prompt_for_member, _excTag);
|
|
632
|
+
// ── the fallback's REAL entry point ──────────────────────
|
|
633
|
+
// On an escalation the loop deliberately does NOT `continue`: it
|
|
634
|
+
// falls through to the metered path with `member` swapped for the
|
|
635
|
+
// twin. Why this branch and not the retry block below:
|
|
636
|
+
// `mid_flight_fallback.escalateUnmetered`.
|
|
637
|
+
const nb_twin = escalateUnmetered(
|
|
638
|
+
member,
|
|
639
|
+
response,
|
|
640
|
+
_getattr(member, 'transport', 'api') as string,
|
|
641
|
+
opts.cli_fallback ?? null,
|
|
642
|
+
opts.fallback_ledger ?? null,
|
|
643
|
+
);
|
|
644
|
+
const escalated = nb_twin !== null;
|
|
645
|
+
if (nb_twin !== null) {
|
|
646
|
+
established = nb_twin;
|
|
647
|
+
twins?.set(member.name, nb_twin);
|
|
648
|
+
member = nb_twin.client;
|
|
649
|
+
// Kept so a budget refusal below can return the ORIGINAL cli
|
|
650
|
+
// failure rather than aborting the seat as its twin.
|
|
651
|
+
unmetered_original = response;
|
|
652
|
+
unmetered_twin = nb_twin;
|
|
653
|
+
}
|
|
654
|
+
if (!escalated) {
|
|
655
|
+
_stamp_transport_metadata(response, member);
|
|
656
|
+
results.push(response);
|
|
657
|
+
spent.input += response.input_tokens;
|
|
658
|
+
spent.output += response.output_tokens;
|
|
659
|
+
continue;
|
|
699
660
|
}
|
|
700
|
-
_stamp_transport_metadata(response, member);
|
|
701
|
-
results.push(response);
|
|
702
|
-
spent.input += response.input_tokens;
|
|
703
|
-
spent.output += response.output_tokens;
|
|
704
|
-
continue;
|
|
705
661
|
}
|
|
706
662
|
|
|
707
663
|
// ── projected spend check ────────────────────────────────────
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
if (breaches_tokens || breaches_usd || breaches_daily) {
|
|
724
|
-
const breach_kind = breaches_tokens
|
|
725
|
-
? 'tokens'
|
|
726
|
-
: breaches_daily
|
|
727
|
-
? 'daily'
|
|
728
|
-
: 'session';
|
|
664
|
+
// A substituted twin is priced as ITSELF — the pre-loop estimate row
|
|
665
|
+
// belongs to an unmetered seat and would gate a metered call at $0.
|
|
666
|
+
const est =
|
|
667
|
+
established !== null && table !== null
|
|
668
|
+
? (estimate(question, [member], table, {
|
|
669
|
+
project,
|
|
670
|
+
original_ask,
|
|
671
|
+
advisor_plans: opts.advisor_plans ?? null,
|
|
672
|
+
})[0] as CostEstimate)
|
|
673
|
+
: estimates
|
|
674
|
+
? (estimates[idx] as CostEstimate)
|
|
675
|
+
: null;
|
|
676
|
+
const breach = _breach(est, spent, budget);
|
|
677
|
+
if (breach !== null) {
|
|
678
|
+
const breach_kind = breach;
|
|
729
679
|
const error_tag =
|
|
730
680
|
breach_kind === 'daily'
|
|
731
681
|
? 'daily_budget_exceeded'
|
|
732
682
|
: 'cost_budget_exceeded';
|
|
683
|
+
// A seat that arrived here by escalating from an UNMETERED cli
|
|
684
|
+
// call degrades to its own original failure — never to an abort,
|
|
685
|
+
// and never to the round-wide short-circuit below. The reasoning,
|
|
686
|
+
// and the rollback of both the twin map and the ledger claim, live
|
|
687
|
+
// in `refuseUnmeteredEscalation`.
|
|
688
|
+
if (unmetered_original !== null && unmetered_twin !== null) {
|
|
689
|
+
const refused = refuseUnmeteredEscalation({
|
|
690
|
+
original: unmetered_original,
|
|
691
|
+
declared,
|
|
692
|
+
twin: unmetered_twin,
|
|
693
|
+
fallback: opts.cli_fallback ?? null,
|
|
694
|
+
ledger: opts.fallback_ledger ?? null,
|
|
695
|
+
twins,
|
|
696
|
+
});
|
|
697
|
+
_stamp_transport_metadata(refused, declared);
|
|
698
|
+
results.push(refused);
|
|
699
|
+
spent.input += refused.input_tokens;
|
|
700
|
+
spent.output += refused.output_tokens;
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
733
703
|
if (on_overrun !== null && estimates !== null) {
|
|
734
704
|
const event = new OverrunEvent({
|
|
735
705
|
member_index: idx,
|
|
736
706
|
member,
|
|
737
|
-
|
|
707
|
+
// `est`, not `estimates[idx]`. The breach decision two
|
|
708
|
+
// lines up was already re-priced for a substituted twin;
|
|
709
|
+
// the event that ASKS the operator to approve it showed
|
|
710
|
+
// the pre-loop row for the unmetered DECLARED seat, i.e. a
|
|
711
|
+
// ~$0 next-call estimate for the very call the gate just
|
|
712
|
+
// refused as over budget. R2 review, finding 8.
|
|
713
|
+
next_estimate: (est ?? estimates[idx]) as CostEstimate,
|
|
738
714
|
spent_input_tokens: _pyInt(spent.input),
|
|
739
715
|
spent_output_tokens: _pyInt(spent.output),
|
|
740
716
|
spent_usd: spent.usd,
|
|
741
|
-
projected_total_usd:
|
|
717
|
+
projected_total_usd: spent.usd + (est ? _total_usd(est) : 0.0),
|
|
742
718
|
daily_spent_usd:
|
|
743
719
|
budget.daily_limit_usd > 0 ? _today_spend_usd() : 0.0,
|
|
744
720
|
daily_limit_usd: budget.daily_limit_usd,
|
|
@@ -757,29 +733,62 @@ function _run_round(
|
|
|
757
733
|
}
|
|
758
734
|
}
|
|
759
735
|
|
|
736
|
+
// An escalated unmetered seat clears the gate: NOW the escalation is
|
|
737
|
+
// real, so `retried` is emitted here rather than at establishment.
|
|
738
|
+
// `escalateUnmetered` used to emit it the moment the twin was built,
|
|
739
|
+
// which put a "seat saved" line in the log for escalations the budget
|
|
740
|
+
// then refused — R2 round 2, finding 4.
|
|
741
|
+
if (unmetered_twin !== null) {
|
|
742
|
+
const fb = opts.cli_fallback ?? null;
|
|
743
|
+
if (fb !== null) emitOutcome(fb, declared.name, unmetered_twin, 'retried');
|
|
744
|
+
}
|
|
745
|
+
|
|
760
746
|
// ── actual call ──────────────────────────────────────────────
|
|
761
|
-
let response
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
error: _excTag(exc),
|
|
747
|
+
let response = callMember(member, question, opts, _system_prompt_for_member, _excTag);
|
|
748
|
+
|
|
749
|
+
// ── mid-flight cli→api fallback, billable-cli path ───────────
|
|
750
|
+
// The community CLI wrappers reach the fallback here; the
|
|
751
|
+
// vendor-official ones took the non-billable branch above. Realized
|
|
752
|
+
// cost and the transport stamp below run on `effective`.
|
|
753
|
+
let effective: ExternalAIClient = member;
|
|
754
|
+
const fallback = opts.cli_fallback ?? null;
|
|
755
|
+
const ledger = opts.fallback_ledger ?? null;
|
|
756
|
+
if (
|
|
757
|
+
fallback !== null &&
|
|
758
|
+
ledger !== null &&
|
|
759
|
+
isFailedCliCall(member, response.error, _getattr(member, 'transport', 'api') as string)
|
|
760
|
+
) {
|
|
761
|
+
const twin = establishTwin({
|
|
762
|
+
member,
|
|
763
|
+
error: String(response.error),
|
|
764
|
+
fallback,
|
|
765
|
+
ledger,
|
|
781
766
|
});
|
|
767
|
+
if (twin !== null) {
|
|
768
|
+
const out = runGatedRetry(response, member, twin, fallback, {
|
|
769
|
+
gate: (client) =>
|
|
770
|
+
table === null ||
|
|
771
|
+
_breach(
|
|
772
|
+
estimate(question, [client], table, {
|
|
773
|
+
project,
|
|
774
|
+
original_ask,
|
|
775
|
+
advisor_plans: opts.advisor_plans ?? null,
|
|
776
|
+
})[0] as CostEstimate,
|
|
777
|
+
spent,
|
|
778
|
+
budget,
|
|
779
|
+
) === null,
|
|
780
|
+
call: (client) => callMember(client, question, opts, _system_prompt_for_member, _excTag),
|
|
781
|
+
}, ledger);
|
|
782
|
+
response = out.response;
|
|
783
|
+
effective = out.effective;
|
|
784
|
+
// Established for the rest of the invocation, so later rounds
|
|
785
|
+
// do not re-spawn the dead binary.
|
|
786
|
+
if (out.retried) twins?.set(member.name, twin);
|
|
787
|
+
}
|
|
782
788
|
}
|
|
789
|
+
// No round is silent about which transport answered; `sticky`
|
|
790
|
+
// marks the reuses (see `stampFallback`).
|
|
791
|
+
if (established !== null) stampFallback(response, established, substituted_on_entry);
|
|
783
792
|
results.push(response);
|
|
784
793
|
spent.input += response.input_tokens;
|
|
785
794
|
spent.output += response.output_tokens;
|
|
@@ -792,8 +801,8 @@ function _run_round(
|
|
|
792
801
|
// ttl is '5m': the request builder emits `cache_control:
|
|
793
802
|
// {type:'ephemeral'}` with no explicit ttl, i.e. the 5-min default.
|
|
794
803
|
const actual = reprice_with_cache(
|
|
795
|
-
|
|
796
|
-
|
|
804
|
+
effective.name,
|
|
805
|
+
effective.model,
|
|
797
806
|
{
|
|
798
807
|
input_tokens: response.input_tokens,
|
|
799
808
|
cache_read_input_tokens: response.cache_read_input_tokens,
|
|
@@ -808,14 +817,14 @@ function _run_round(
|
|
|
808
817
|
// Persist to the rolling 24h ledger when the daily cap is
|
|
809
818
|
// active. Errors are swallowed inside record_spend.
|
|
810
819
|
if (budget.daily_limit_usd > 0 && !response.error) {
|
|
811
|
-
_record_daily_spend(_total_usd(actual),
|
|
820
|
+
_record_daily_spend(_total_usd(actual), effective.name, effective.model, {
|
|
812
821
|
cache_read_input_tokens: response.cache_read_input_tokens,
|
|
813
822
|
cache_creation_input_tokens: response.cache_creation_input_tokens,
|
|
814
823
|
cache_ttl: '5m',
|
|
815
824
|
});
|
|
816
825
|
}
|
|
817
826
|
}
|
|
818
|
-
_stamp_transport_metadata(response,
|
|
827
|
+
_stamp_transport_metadata(response, effective, actual_usd);
|
|
819
828
|
}
|
|
820
829
|
|
|
821
830
|
return results;
|
|
@@ -1067,6 +1076,10 @@ function _run_debate_gate_repairs(
|
|
|
1067
1076
|
project: ProjectContext | null;
|
|
1068
1077
|
original_ask: string;
|
|
1069
1078
|
advisor_plans: Map<string, AdvisorPlan> | null;
|
|
1079
|
+
/** Threaded from `run_debate`; ledger and twins are invocation-wide. */
|
|
1080
|
+
cli_fallback?: CliFallbackOptions | null;
|
|
1081
|
+
fallback_ledger?: MidFlightFallback | null;
|
|
1082
|
+
fallback_twins?: Map<string, EstablishedTwin> | null;
|
|
1070
1083
|
},
|
|
1071
1084
|
on_repair: ((member: string, reason: string) => boolean) | null,
|
|
1072
1085
|
): CouncilResponse[] {
|
|
@@ -1182,6 +1195,18 @@ export interface RunDebateOptions {
|
|
|
1182
1195
|
restate?: boolean;
|
|
1183
1196
|
/** Receives the restate responses (rendering, divergence flags, cost). */
|
|
1184
1197
|
on_restate?: ((responses: CouncilResponse[]) => void) | null;
|
|
1198
|
+
/**
|
|
1199
|
+
* Mid-flight cli→api fallback for every call this debate makes — the
|
|
1200
|
+
* restate pass, each round, and the gate-repair re-prompts. Absent →
|
|
1201
|
+
* byte-identical to today.
|
|
1202
|
+
*
|
|
1203
|
+
* ONE ledger and one twin map span the whole invocation, never one per
|
|
1204
|
+
* round: the eligible classes are overwhelmingly DURABLE, so a per-round
|
|
1205
|
+
* ledger would re-spawn and re-fail the dead CLI once per round. Why that
|
|
1206
|
+
* scope needs the twin map to be coherent at all, and what it costs a
|
|
1207
|
+
* transient failure: `mid_flight_fallback.TwinMap`.
|
|
1208
|
+
*/
|
|
1209
|
+
cli_fallback?: CliFallbackOptions | null;
|
|
1185
1210
|
}
|
|
1186
1211
|
|
|
1187
1212
|
/**
|
|
@@ -1241,6 +1266,11 @@ export function run_debate(
|
|
|
1241
1266
|
}
|
|
1242
1267
|
|
|
1243
1268
|
const spent: Spent = { input: 0, output: 0, usd: 0.0 };
|
|
1269
|
+
// One per invocation: restate pass, rounds, and gate repairs share it.
|
|
1270
|
+
const cli_fallback = opts.cli_fallback ?? null;
|
|
1271
|
+
const fallback_ledger = cli_fallback !== null ? new MidFlightFallback() : null;
|
|
1272
|
+
const fallback_twins =
|
|
1273
|
+
cli_fallback !== null ? new Map<string, EstablishedTwin>() : null;
|
|
1244
1274
|
const all_rounds: CouncilResponse[][] = [];
|
|
1245
1275
|
// A3 read unlock (debate twin): stable prefix = the original prompt; the
|
|
1246
1276
|
// per-round positions block rides in a volatile suffix (see consult loop).
|
|
@@ -1267,6 +1297,9 @@ export function run_debate(
|
|
|
1267
1297
|
project,
|
|
1268
1298
|
original_ask,
|
|
1269
1299
|
advisor_plans,
|
|
1300
|
+
cli_fallback,
|
|
1301
|
+
fallback_ledger,
|
|
1302
|
+
fallback_twins,
|
|
1270
1303
|
});
|
|
1271
1304
|
opts.on_restate?.(restated);
|
|
1272
1305
|
}
|
|
@@ -1296,6 +1329,9 @@ export function run_debate(
|
|
|
1296
1329
|
project,
|
|
1297
1330
|
original_ask,
|
|
1298
1331
|
advisor_plans,
|
|
1332
|
+
cli_fallback,
|
|
1333
|
+
fallback_ledger,
|
|
1334
|
+
fallback_twins,
|
|
1299
1335
|
split: current_suffix
|
|
1300
1336
|
? { stable: question.user_prompt, suffix: current_suffix }
|
|
1301
1337
|
: null,
|
|
@@ -1315,7 +1351,16 @@ export function run_debate(
|
|
|
1315
1351
|
}),
|
|
1316
1352
|
budget,
|
|
1317
1353
|
spent,
|
|
1318
|
-
{
|
|
1354
|
+
{
|
|
1355
|
+
table,
|
|
1356
|
+
on_overrun,
|
|
1357
|
+
project,
|
|
1358
|
+
original_ask,
|
|
1359
|
+
advisor_plans,
|
|
1360
|
+
cli_fallback,
|
|
1361
|
+
fallback_ledger,
|
|
1362
|
+
fallback_twins,
|
|
1363
|
+
},
|
|
1319
1364
|
opts.on_repair ?? null,
|
|
1320
1365
|
);
|
|
1321
1366
|
}
|
|
@@ -1731,28 +1776,6 @@ export function run_consensus_scoring(
|
|
|
1731
1776
|
* Tokens marked `estimated=True` get a `~` prefix so the audit
|
|
1732
1777
|
* trail flags heuristic counts.
|
|
1733
1778
|
*/
|
|
1734
|
-
function _render_response_meta(r: CouncilResponse): string {
|
|
1735
|
-
const meta_dict: Record<string, unknown> = r.metadata ?? {};
|
|
1736
|
-
const billable = Boolean(_metaGet(meta_dict, 'billable', true));
|
|
1737
|
-
const estimated = Boolean(_metaGet(meta_dict, 'tokens_estimated', false));
|
|
1738
|
-
const parts: string[] = [];
|
|
1739
|
-
if (!billable) {
|
|
1740
|
-
const label = (_metaGet(meta_dict, 'subscription_label', null) ||
|
|
1741
|
-
'flat-rate') as string;
|
|
1742
|
-
parts.push(`cost: subscription (${label})`);
|
|
1743
|
-
} else {
|
|
1744
|
-
const cost_usd = _metaGet(meta_dict, 'cost_usd', undefined);
|
|
1745
|
-
if (typeof cost_usd === 'number' && !(typeof cost_usd === 'boolean')) {
|
|
1746
|
-
parts.push(`cost: $${_pyFixed(cost_usd, 4)}`);
|
|
1747
|
-
}
|
|
1748
|
-
const prefix = estimated ? '~' : '';
|
|
1749
|
-
parts.push(
|
|
1750
|
-
`tokens: ${prefix}${r.input_tokens} in / ${prefix}${r.output_tokens} out`,
|
|
1751
|
-
);
|
|
1752
|
-
}
|
|
1753
|
-
parts.push(`${r.latency_ms} ms`);
|
|
1754
|
-
return `*${parts.join(' · ')}*`;
|
|
1755
|
-
}
|
|
1756
1779
|
|
|
1757
1780
|
// Lens defaults for the Phase 9 confidence-explanation badge. The PR
|
|
1758
1781
|
// lens stays terse so the existing "Must-fix / Nice-to-have" structure
|
|
@@ -1906,7 +1929,7 @@ export function render(responses: CouncilResponse[], opts: RenderOptions = {}):
|
|
|
1906
1929
|
blocks.push(`${header}\n\n*ERROR:* \`${r.error}\``);
|
|
1907
1930
|
continue;
|
|
1908
1931
|
}
|
|
1909
|
-
const meta =
|
|
1932
|
+
const meta = renderResponseMeta(r, { pyFixed: _pyFixed });
|
|
1910
1933
|
blocks.push(`${header}\n\n${meta}\n\n${r.text}`);
|
|
1911
1934
|
}
|
|
1912
1935
|
if (peer_review !== null && peer_review.responses.length > 0) {
|
|
@@ -2205,67 +2228,3 @@ function _render_bucket(
|
|
|
2205
2228
|
}
|
|
2206
2229
|
return lines.join('\n');
|
|
2207
2230
|
}
|
|
2208
|
-
|
|
2209
|
-
// ── small stdlib parity helpers ───────────────────────────────────────
|
|
2210
|
-
|
|
2211
|
-
/** Mirror Python `int(x)` truncation-toward-zero on a float total. */
|
|
2212
|
-
function _pyInt(x: number): number {
|
|
2213
|
-
return Math.trunc(x);
|
|
2214
|
-
}
|
|
2215
|
-
|
|
2216
|
-
/** Mirror Python `repr(int)` for the error message in estimate_debate_cost. */
|
|
2217
|
-
function _pyReprInt(x: number): string {
|
|
2218
|
-
return String(x);
|
|
2219
|
-
}
|
|
2220
|
-
|
|
2221
|
-
/** Mirror `type(exc).__name__ + ": " + str(exc)`. */
|
|
2222
|
-
function _excTag(exc: unknown): string {
|
|
2223
|
-
if (exc instanceof Error) {
|
|
2224
|
-
return `${exc.name}: ${exc.message}`;
|
|
2225
|
-
}
|
|
2226
|
-
return `Error: ${String(exc)}`;
|
|
2227
|
-
}
|
|
2228
|
-
|
|
2229
|
-
/** dict.setdefault(key, value) — only set when key absent. */
|
|
2230
|
-
function _setdefault(
|
|
2231
|
-
obj: Record<string, unknown>,
|
|
2232
|
-
key: string,
|
|
2233
|
-
value: unknown,
|
|
2234
|
-
): void {
|
|
2235
|
-
if (!(key in obj)) {
|
|
2236
|
-
obj[key] = value;
|
|
2237
|
-
}
|
|
2238
|
-
}
|
|
2239
|
-
|
|
2240
|
-
/** dict.get(key, default). */
|
|
2241
|
-
function _metaGet(
|
|
2242
|
-
obj: Record<string, unknown>,
|
|
2243
|
-
key: string,
|
|
2244
|
-
fallback: unknown,
|
|
2245
|
-
): unknown {
|
|
2246
|
-
return key in obj ? obj[key] : fallback;
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
/** Python `str.lstrip()` (no-arg) — strip leading whitespace. */
|
|
2250
|
-
function _pyLStrip(s: string): string {
|
|
2251
|
-
return s.replace(/^\s+/, '');
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
|
-
/** Python `str.rstrip()` (no-arg) — strip trailing whitespace. */
|
|
2255
|
-
function _pyRStrip(s: string): string {
|
|
2256
|
-
return s.replace(/\s+$/, '');
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
/** Python `len(str)` — code-point count, not UTF-16 unit count. */
|
|
2260
|
-
function _pyLen(s: string): number {
|
|
2261
|
-
let n = 0;
|
|
2262
|
-
for (const _ of s) {
|
|
2263
|
-
n += 1;
|
|
2264
|
-
}
|
|
2265
|
-
return n;
|
|
2266
|
-
}
|
|
2267
|
-
|
|
2268
|
-
/** Python `s[start:end]` — code-point slicing. */
|
|
2269
|
-
function _pySlice(s: string, start: number, end: number): string {
|
|
2270
|
-
return [...s].slice(start, end).join('');
|
|
2271
|
-
}
|