@event4u/agent-config 14.2.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 +286 -218
- 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 +3684 -1719
- 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 +16 -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/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/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/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/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
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mid_flight_fallback — the cli→api retry's types, its establish step, and its
|
|
3
|
+
* response stamp.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from `orchestrator.ts` rather than written there, and the ordering
|
|
6
|
+
* is the point (`gate-violation-baselines.json` § check_source_size_budget:
|
|
7
|
+
* "extract, then measure"): the orchestrator sits far over the 1,500-line
|
|
8
|
+
* source ceiling, so the machinery of a new mechanism belongs beside it, not
|
|
9
|
+
* inside it. The extraction also removes a real duplication — the classify →
|
|
10
|
+
* eligibility → ledger → construct sequence ran twice, once in `_run_round`'s
|
|
11
|
+
* non-billable branch and once in its retry block.
|
|
12
|
+
*
|
|
13
|
+
* The full behavioural contract lives in
|
|
14
|
+
* `docs/contracts/ai-council-config.md` § "Mid-flight fallback is
|
|
15
|
+
* failure-class-gated". This file carries the reasons a reader needs AT the
|
|
16
|
+
* code and points at that section for the rest, rather than restating it.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { ExternalAIClient, CouncilResponse } from './clients.js';
|
|
20
|
+
import {
|
|
21
|
+
classifyCliFailure,
|
|
22
|
+
isFallbackEligibleUnder,
|
|
23
|
+
type FallbackPolicy,
|
|
24
|
+
type MidFlightFallback,
|
|
25
|
+
} from './transport_resolver.js';
|
|
26
|
+
|
|
27
|
+
/** One escalation's outcome, for the event sink. */
|
|
28
|
+
export type FallbackOutcome = 'retried' | 'no_twin' | 'cost_budget';
|
|
29
|
+
|
|
30
|
+
export interface FallbackEvent {
|
|
31
|
+
readonly provider: string;
|
|
32
|
+
readonly failure: string;
|
|
33
|
+
readonly outcome: FallbackOutcome;
|
|
34
|
+
readonly api_on_quota: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CliFallbackOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Build the api twin for `provider`, or `null` when it cannot be built —
|
|
40
|
+
* no api constructor, or its strict `api_key_ref` contract refuses.
|
|
41
|
+
* Called lazily, at most once per provider per invocation.
|
|
42
|
+
*/
|
|
43
|
+
readonly construct: (provider: string) => ExternalAIClient | null;
|
|
44
|
+
/** `ai_council.fallback.api_on_quota` — see `FallbackPolicy.apiOnQuota`. */
|
|
45
|
+
readonly api_on_quota: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Called once per ESTABLISHING escalation, never per substituted call.
|
|
48
|
+
*
|
|
49
|
+
* The sink is the caller's: the orchestrator is a pure library with no
|
|
50
|
+
* stdout and no disk, and reaching for `events_log` there would be the
|
|
51
|
+
* first thing to break that. The CLI wires this to `appendEvent`.
|
|
52
|
+
*/
|
|
53
|
+
readonly on_event?: ((e: FallbackEvent) => void) | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A provider's api twin, plus why it replaced the cli transport. */
|
|
57
|
+
export interface EstablishedTwin {
|
|
58
|
+
readonly client: ExternalAIClient;
|
|
59
|
+
readonly reason: string;
|
|
60
|
+
readonly original_error: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Why the twin map exists alongside the ledger.
|
|
65
|
+
*
|
|
66
|
+
* `MidFlightFallback` hands out `'api'` at most ONCE per provider. Without a
|
|
67
|
+
* substitution map an invocation-scoped ledger is therefore strictly WORSE
|
|
68
|
+
* than a per-round one: on round 2 the dead cli member is called again, fails
|
|
69
|
+
* again, and the ledger answers `'stop'` — the seat is lost for the rest of
|
|
70
|
+
* the pass, having fallen back exactly once. With the map, the provider is
|
|
71
|
+
* replaced BEFORE the call from the round after it fell back, so the dead
|
|
72
|
+
* binary is never spawned twice and nothing is constructed twice.
|
|
73
|
+
*
|
|
74
|
+
* The cost, stated rather than hidden: a TRANSIENT single-round failure moves
|
|
75
|
+
* the seat to the metered rung for the rest of the invocation. The mitigation
|
|
76
|
+
* is visibility, not a narrower scope — every fallen-back response carries the
|
|
77
|
+
* stamp `stampFallback` applies.
|
|
78
|
+
*/
|
|
79
|
+
export type TwinMap = Map<string, EstablishedTwin>;
|
|
80
|
+
|
|
81
|
+
export interface EstablishInput {
|
|
82
|
+
readonly member: ExternalAIClient;
|
|
83
|
+
readonly error: string;
|
|
84
|
+
readonly fallback: CliFallbackOptions;
|
|
85
|
+
readonly ledger: MidFlightFallback;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Decide whether this cli failure establishes a twin, and build it if so.
|
|
90
|
+
*
|
|
91
|
+
* Returns `null` for every non-escalating outcome: an ineligible class, a
|
|
92
|
+
* ledger refusal, or no constructible twin.
|
|
93
|
+
*
|
|
94
|
+
* It emits ONLY the `no_twin` event, and that split is deliberate. `no_twin`
|
|
95
|
+
* is final here — the provider has no api rung and nothing downstream can
|
|
96
|
+
* change that. Whether a CONSTRUCTED twin ends as `retried` or `cost_budget`
|
|
97
|
+
* is not knowable at this point, because the caller's projected-spend gate has
|
|
98
|
+
* not run yet; emitting `retried` here would report an escalation that the
|
|
99
|
+
* budget then refused. An ineligible class emits nothing at all: there was no
|
|
100
|
+
* escalation to report.
|
|
101
|
+
*/
|
|
102
|
+
export function establishTwin(input: EstablishInput): EstablishedTwin | null {
|
|
103
|
+
const { member, error, fallback, ledger } = input;
|
|
104
|
+
const failure = classifyCliFailure(error);
|
|
105
|
+
const policy: FallbackPolicy = { apiOnQuota: fallback.api_on_quota };
|
|
106
|
+
if (!isFallbackEligibleUnder(failure, policy)) return null;
|
|
107
|
+
if (ledger.attempt(member.name, failure, policy) !== 'api') return null;
|
|
108
|
+
|
|
109
|
+
const twin = fallback.construct(member.name);
|
|
110
|
+
if (twin === null) {
|
|
111
|
+
fallback.on_event?.({
|
|
112
|
+
provider: member.name,
|
|
113
|
+
failure,
|
|
114
|
+
outcome: 'no_twin',
|
|
115
|
+
api_on_quota: fallback.api_on_quota,
|
|
116
|
+
});
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return { client: twin, reason: failure, original_error: error };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The caller's half of the event: the outcome only it can know. */
|
|
123
|
+
export function emitOutcome(
|
|
124
|
+
fallback: CliFallbackOptions,
|
|
125
|
+
provider: string,
|
|
126
|
+
twin: EstablishedTwin,
|
|
127
|
+
outcome: Exclude<FallbackOutcome, 'no_twin'>,
|
|
128
|
+
): void {
|
|
129
|
+
fallback.on_event?.({
|
|
130
|
+
provider,
|
|
131
|
+
failure: twin.reason,
|
|
132
|
+
outcome,
|
|
133
|
+
api_on_quota: fallback.api_on_quota,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Stamp a response that answered over a fallen-back transport.
|
|
139
|
+
*
|
|
140
|
+
* `sticky` separates the rounds that REUSED an established twin from the one
|
|
141
|
+
* round that established it — a reader counting escalations counts the latter,
|
|
142
|
+
* and two rendered lines must not read as two escalations.
|
|
143
|
+
*/
|
|
144
|
+
export function stampFallback(
|
|
145
|
+
response: CouncilResponse,
|
|
146
|
+
twin: EstablishedTwin,
|
|
147
|
+
sticky: boolean,
|
|
148
|
+
): void {
|
|
149
|
+
response.metadata = {
|
|
150
|
+
...(response.metadata ?? {}),
|
|
151
|
+
fallback_from: 'cli',
|
|
152
|
+
fallback_reason: twin.reason,
|
|
153
|
+
fallback_original_error: twin.original_error,
|
|
154
|
+
...(sticky ? { fallback_sticky: true } : {}),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** True when this member is on the cli rung and its call failed. */
|
|
159
|
+
export function isFailedCliCall(
|
|
160
|
+
member: ExternalAIClient,
|
|
161
|
+
error: string | null,
|
|
162
|
+
transport: string,
|
|
163
|
+
): boolean {
|
|
164
|
+
return error !== null && String(error) !== '' && transport === 'cli';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** What the gated retry needs from the round it runs inside. */
|
|
168
|
+
export interface GatedRetryDeps {
|
|
169
|
+
/** Projected-spend verdict for one call by `client`. `true` → proceed. */
|
|
170
|
+
readonly gate: (client: ExternalAIClient) => boolean;
|
|
171
|
+
/** Perform the call, returning an error-tagged response rather than throwing. */
|
|
172
|
+
readonly call: (client: ExternalAIClient) => CouncilResponse;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface GatedRetryResult {
|
|
176
|
+
readonly response: CouncilResponse;
|
|
177
|
+
/** The member the round should bill and stamp — the twin, or the original. */
|
|
178
|
+
readonly effective: ExternalAIClient;
|
|
179
|
+
readonly retried: boolean;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Run the retry an established twin earns, behind its own spend gate.
|
|
184
|
+
*
|
|
185
|
+
* The gate is separate from the round's because the retry is METERED even when
|
|
186
|
+
* the failed cli call was not: an unmetered seat that falls back has never been
|
|
187
|
+
* priced against the running totals, so reusing the loop-head verdict would let
|
|
188
|
+
* it through unpriced.
|
|
189
|
+
*
|
|
190
|
+
* A refusal returns the ORIGINAL failure with `fallback_skipped: cost_budget`
|
|
191
|
+
* on it — named, so a reader can tell "not retried" from "retry refused".
|
|
192
|
+
*/
|
|
193
|
+
export function runGatedRetry(
|
|
194
|
+
original: CouncilResponse,
|
|
195
|
+
member: ExternalAIClient,
|
|
196
|
+
twin: EstablishedTwin,
|
|
197
|
+
fallback: CliFallbackOptions,
|
|
198
|
+
deps: GatedRetryDeps,
|
|
199
|
+
ledger: MidFlightFallback | null = null,
|
|
200
|
+
): GatedRetryResult {
|
|
201
|
+
if (!deps.gate(twin.client)) {
|
|
202
|
+
original.metadata = { ...(original.metadata ?? {}), fallback_skipped: 'cost_budget' };
|
|
203
|
+
emitOutcome(fallback, member.name, twin, 'cost_budget');
|
|
204
|
+
// The claim goes back, for the same reason it does on the unmetered
|
|
205
|
+
// path: the twin was built and never called, so the provider has not
|
|
206
|
+
// spent its one escalation. R2 round 3, finding 4 — the unmetered
|
|
207
|
+
// refusal released and this one did not, so the two paths this
|
|
208
|
+
// function's own docblock calls parallel behaved differently, and a
|
|
209
|
+
// billable community-cli seat was permanently lost after a single
|
|
210
|
+
// budget-refused escalation.
|
|
211
|
+
ledger?.release(member.name);
|
|
212
|
+
return { response: original, effective: member, retried: false };
|
|
213
|
+
}
|
|
214
|
+
const response = deps.call(twin.client);
|
|
215
|
+
stampFallback(response, twin, false);
|
|
216
|
+
emitOutcome(fallback, member.name, twin, 'retried');
|
|
217
|
+
return { response, effective: twin.client, retried: true };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The unmetered-seat escalation, as one call.
|
|
222
|
+
*
|
|
223
|
+
* A vendor-official CLI member is `billable = false` + `transport = 'cli'`,
|
|
224
|
+
* which is the shape the round's non-billable branch returns early for — so
|
|
225
|
+
* before this existed the retry could only ever fire for the two community
|
|
226
|
+
* subclasses that consume an API key. Returns the established twin, or `null`
|
|
227
|
+
* when nothing escalated.
|
|
228
|
+
*/
|
|
229
|
+
export function escalateUnmetered(
|
|
230
|
+
member: ExternalAIClient,
|
|
231
|
+
response: CouncilResponse,
|
|
232
|
+
transport: string,
|
|
233
|
+
fallback: CliFallbackOptions | null,
|
|
234
|
+
ledger: MidFlightFallback | null,
|
|
235
|
+
): EstablishedTwin | null {
|
|
236
|
+
if (fallback === null || ledger === null) return null;
|
|
237
|
+
if (!isFailedCliCall(member, response.error, transport)) return null;
|
|
238
|
+
const twin = establishTwin({
|
|
239
|
+
member,
|
|
240
|
+
error: String(response.error),
|
|
241
|
+
fallback,
|
|
242
|
+
ledger,
|
|
243
|
+
});
|
|
244
|
+
if (twin === null) return null;
|
|
245
|
+
// NOTHING is emitted here, and the previous comment justifying an emit at
|
|
246
|
+
// establishment is deleted rather than reworded because it stopped being
|
|
247
|
+
// true. It read: "on this path the twin goes through the round's ORDINARY
|
|
248
|
+
// spend gate, whose refusal surfaces as `cost_budget_exceeded` on the
|
|
249
|
+
// response rather than as a fallback event." Round 1's finding-3 fix made
|
|
250
|
+
// a refusal surface as exactly such an event, so an `retried` written here
|
|
251
|
+
// and a `cost_budget` written there put two contradictory lines in the log
|
|
252
|
+
// for one seat — and the first of them claims a seat was saved that was
|
|
253
|
+
// not. R2 round 2, finding 4.
|
|
254
|
+
//
|
|
255
|
+
// The caller emits once, on the outcome it actually observed:
|
|
256
|
+
// `refuseUnmeteredEscalation` for the refusal, `emitOutcome(…, 'retried')`
|
|
257
|
+
// immediately before the metered call for the acceptance.
|
|
258
|
+
return twin;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* A budget-refused escalation of an UNMETERED seat, as one call.
|
|
263
|
+
*
|
|
264
|
+
* The seat degrades to its own original cli failure — never to an abort, and
|
|
265
|
+
* never to the round-wide short-circuit. Before the escalation existed a
|
|
266
|
+
* `billable: false` member returned before the spend gate and could not reach
|
|
267
|
+
* it at all, so a dead free CLI cost its own seat and nothing else; falling
|
|
268
|
+
* through priced as the metered twin put it one branch from aborting EVERY
|
|
269
|
+
* remaining member, which the shipped claim `council-fallback-loses-zero-seats`
|
|
270
|
+
* says cannot happen. R2 round 1, finding 3.
|
|
271
|
+
*
|
|
272
|
+
* The shape matches {@link runGatedRetry}'s refusal path exactly: the original
|
|
273
|
+
* response, `fallback_skipped: cost_budget` so a reader can tell "not retried"
|
|
274
|
+
* from "retry refused", and one outcome on the event log.
|
|
275
|
+
*
|
|
276
|
+
* Rolls the sticky substitution back — the twin was never called, so a later
|
|
277
|
+
* round must re-decide rather than inherit a substitution that never happened.
|
|
278
|
+
* `ledger.release` undoes the once-per-provider claim for the same reason: a
|
|
279
|
+
* twin that never ran must not consume the provider's one escalation.
|
|
280
|
+
*/
|
|
281
|
+
export function refuseUnmeteredEscalation(args: {
|
|
282
|
+
readonly original: CouncilResponse;
|
|
283
|
+
readonly declared: ExternalAIClient;
|
|
284
|
+
readonly twin: EstablishedTwin;
|
|
285
|
+
readonly fallback: CliFallbackOptions | null;
|
|
286
|
+
readonly ledger: MidFlightFallback | null;
|
|
287
|
+
readonly twins: TwinMap | null;
|
|
288
|
+
}): CouncilResponse {
|
|
289
|
+
const { original, declared, twin, fallback, ledger, twins } = args;
|
|
290
|
+
original.metadata = { ...(original.metadata ?? {}), fallback_skipped: 'cost_budget' };
|
|
291
|
+
if (fallback !== null) emitOutcome(fallback, declared.name, twin, 'cost_budget');
|
|
292
|
+
twins?.delete(declared.name);
|
|
293
|
+
ledger?.release(declared.name);
|
|
294
|
+
return original;
|
|
295
|
+
}
|