@eir-labs/coltrane 0.8.0 → 0.9.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/agents/change-verifier.json +40 -0
- package/agents/code-implementer.json +42 -0
- package/agents/contract-definer.json +31 -0
- package/agents/defect-investigator.json +60 -0
- package/agents/grounder.json +38 -0
- package/agents/pr-publisher.json +48 -0
- package/agents/red-spec-drafter.json +46 -0
- package/agents/spec-publisher.json +48 -0
- package/agents/spec-reviewer.json +32 -0
- package/dist/src/bifrost_invoker.js +53 -52
- package/dist/src/bifrost_invoker.js.map +1 -1
- package/dist/src/change_set_branch.d.ts +131 -0
- package/dist/src/change_set_branch.js +179 -0
- package/dist/src/change_set_branch.js.map +1 -0
- package/dist/src/claude_invoker.d.ts +79 -2
- package/dist/src/claude_invoker.js +377 -36
- package/dist/src/claude_invoker.js.map +1 -1
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +26 -3
- package/dist/src/cli.js.map +1 -1
- package/dist/src/committed_work.d.ts +206 -0
- package/dist/src/committed_work.js +0 -0
- package/dist/src/committed_work.js.map +1 -0
- package/dist/src/composition.d.ts +11 -0
- package/dist/src/composition.js +3 -1
- package/dist/src/composition.js.map +1 -1
- package/dist/src/genome_schema.d.ts +803 -10
- package/dist/src/genome_schema.js +413 -7
- package/dist/src/genome_schema.js.map +1 -1
- package/dist/src/genome_store.d.ts +16 -1
- package/dist/src/genome_store.js +68 -0
- package/dist/src/genome_store.js.map +1 -1
- package/dist/src/gig_tracker.js +4 -0
- package/dist/src/gig_tracker.js.map +1 -1
- package/dist/src/hosted_tools.js +19 -0
- package/dist/src/hosted_tools.js.map +1 -1
- package/dist/src/institution_enforcement.d.ts +77 -0
- package/dist/src/institution_enforcement.js +442 -0
- package/dist/src/institution_enforcement.js.map +1 -0
- package/dist/src/institution_loader.d.ts +63 -0
- package/dist/src/institution_loader.js +239 -0
- package/dist/src/institution_loader.js.map +1 -0
- package/dist/src/loader.d.ts +4 -1
- package/dist/src/loader.js +19 -1
- package/dist/src/loader.js.map +1 -1
- package/dist/src/mcp.js +8 -1
- package/dist/src/mcp.js.map +1 -1
- package/dist/src/runtime.d.ts +66 -0
- package/dist/src/runtime.js +202 -2
- package/dist/src/runtime.js.map +1 -1
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.js +90 -1
- package/dist/src/server.js.map +1 -1
- package/dist/src/venue_realize.d.ts +90 -0
- package/dist/src/venue_realize.js +129 -0
- package/dist/src/venue_realize.js.map +1 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/worker.d.ts +45 -3
- package/dist/src/worker.js +67 -2
- package/dist/src/worker.js.map +1 -1
- package/domain_types/change-request.json +2 -1
- package/domain_types/class-sweep.json +34 -0
- package/domain_types/defect-class.json +25 -0
- package/domain_types/defect-location.json +28 -0
- package/domain_types/defect-report.json +28 -0
- package/domain_types/fix-spec.json +28 -0
- package/domain_types/fix-verification.json +28 -0
- package/domain_types/grounding-dossier.json +95 -0
- package/domain_types/pull-request.json +43 -0
- package/domain_types/red-spec.json +76 -0
- package/domain_types/reproduction.json +28 -0
- package/domain_types/root-cause.json +28 -0
- package/domain_types/subsystem-contract.json +106 -0
- package/institutions/coltrane.json +166 -0
- package/institutions/quartet.json +143 -26
- package/package.json +2 -1
- package/standards/defect-investigation-v1.json +156 -0
- package/standards/software-change-pr-v1.json +151 -0
- package/standards/spec-drafting-v1.json +125 -0
package/dist/src/runtime.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Standard, Agent, Chair } from "./composition.js";
|
|
2
2
|
import { type ToolProviderRegistry } from "./tool_providers.js";
|
|
3
|
+
import { type Realization } from "./venue_realize.js";
|
|
4
|
+
import type { Venue } from "./chart.js";
|
|
3
5
|
export declare const CORE_TO_PRIMITIVE: Record<string, Agent["primitives"][number]>;
|
|
4
6
|
import { type CheckpointStore, type ReuseStore, type PriorBudgetState } from "./reuse.js";
|
|
5
7
|
import type { OutputStore, OutputRecord } from "./outputs.js";
|
|
@@ -9,6 +11,7 @@ import type { SkillRecord, EvalRecord } from "./loader.js";
|
|
|
9
11
|
export interface AgentInvocationContext {
|
|
10
12
|
agent: Agent;
|
|
11
13
|
phase: string;
|
|
14
|
+
gig_id?: string | undefined;
|
|
12
15
|
inputs: readonly OutputRecord[];
|
|
13
16
|
gig_input: Record<string, unknown>;
|
|
14
17
|
output_types?: readonly string[];
|
|
@@ -17,6 +20,10 @@ export interface AgentInvocationContext {
|
|
|
17
20
|
onEvent?: (ev: AgentStreamEvent) => void;
|
|
18
21
|
signal?: AbortSignal | undefined;
|
|
19
22
|
depth?: Depth | undefined;
|
|
23
|
+
realization?: Realization | undefined;
|
|
24
|
+
venue?: Venue | undefined;
|
|
25
|
+
turn_budget?: number | undefined;
|
|
26
|
+
turn_reserve?: number | undefined;
|
|
20
27
|
}
|
|
21
28
|
export interface AgentStreamEvent {
|
|
22
29
|
type: string;
|
|
@@ -65,6 +72,12 @@ export type GigProgressEvent = {
|
|
|
65
72
|
phase: string;
|
|
66
73
|
role: string;
|
|
67
74
|
event: AgentStreamEvent;
|
|
75
|
+
} | {
|
|
76
|
+
type: "budget_state";
|
|
77
|
+
phase: string;
|
|
78
|
+
role: string;
|
|
79
|
+
agent_state: BudgetState["agent_state"];
|
|
80
|
+
pool_remaining: number;
|
|
68
81
|
} | {
|
|
69
82
|
type: "gig_resumed";
|
|
70
83
|
from_gig_id: string;
|
|
@@ -247,6 +260,23 @@ export interface RunDeps {
|
|
|
247
260
|
*/
|
|
248
261
|
toolProviders?: ToolProviderRegistry | undefined;
|
|
249
262
|
mcpServerConfigs?: Readonly<Record<string, unknown>> | undefined;
|
|
263
|
+
/**
|
|
264
|
+
* The slug of the venue that confines this gig's chairs. When set, runGig resolves it against
|
|
265
|
+
* `venues` and calls `resolveAndRealize` BEFORE the first chair is invoked; ANY refusal
|
|
266
|
+
* (unknown-venue, credential-breach, ceiling-empty, wildcard-door, install-digest-mismatch,
|
|
267
|
+
* standing-without-cadence) fails the gig closed — nothing is sealed, no chair runs — exactly as
|
|
268
|
+
* an unresolvable tool grant rejects. The resulting room is threaded onto every chair's
|
|
269
|
+
* `AgentInvocationContext.realization`, so the spawn is confined by construction. Absent = a
|
|
270
|
+
* venue-less dispatch, byte-identical to every run before this wire existed.
|
|
271
|
+
*/
|
|
272
|
+
venue?: string | undefined;
|
|
273
|
+
/** The venues this gig may name, slug → contract. A slug the map does not hold is a dead name and
|
|
274
|
+
* fails closed with `unknown-venue`. Absent = an empty map (any named venue is a dead name). */
|
|
275
|
+
venues?: ReadonlyMap<string, Venue> | undefined;
|
|
276
|
+
/** Credential CLASSES detected present in the ambient environment, handed to the realize gauntlet:
|
|
277
|
+
* any class present but NOT declared by the venue's `credential_surface` is a breach that fails
|
|
278
|
+
* the gig closed. Absent = none present (deny-by-default: an empty surface admits nothing). */
|
|
279
|
+
credentialsPresent?: string[] | undefined;
|
|
250
280
|
}
|
|
251
281
|
/**
|
|
252
282
|
* Per-gig cost-budget input. Honors budget-state.json schema (PR #56). Only
|
|
@@ -280,6 +310,29 @@ export interface BudgetInput {
|
|
|
280
310
|
base_cost?: number;
|
|
281
311
|
/** per-byte multiplier on consumed-input size, in append units. Default 0.1. */
|
|
282
312
|
k?: number;
|
|
313
|
+
/**
|
|
314
|
+
* #turn-budget — the gig-level reserve POOL, in turns: a shared quantity a budget-exhausted chair
|
|
315
|
+
* draws from, capped per chair by its own `turn_reserve`. This dispatch-payload value is the
|
|
316
|
+
* PRIMARY source and OVERRIDES `Standard.reserve_pool` deterministically when both are present
|
|
317
|
+
* (no max, no sum). Orthogonal to `opening`/`base_cost`/`k` — those are append-units, this is
|
|
318
|
+
* turns, and a draw moves `pool_remaining` only, never `spent`/`balance`. Absent → the standard
|
|
319
|
+
* default, then 0 (no pool). Distinct from 0 only in that 0 could equally be an authored empty
|
|
320
|
+
* pool; either way a chair reaching for a reserve finds nothing and is recorded as starved.
|
|
321
|
+
*/
|
|
322
|
+
pool?: number;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* #turn-budget — one attributable draw against the gig reserve pool. `denied` marks a chair that
|
|
326
|
+
* reached for a reserve an empty pool could not give — starvation is a RECORDED state, never a
|
|
327
|
+
* silent no-op. `granted` is 0 on a denied draw; `pool_remaining_after` is the pool level once this
|
|
328
|
+
* draw (or non-draw) settled, so the ledger reads as a monotone draw-down.
|
|
329
|
+
*/
|
|
330
|
+
export interface ReserveDraw {
|
|
331
|
+
/** The drawing chair's role — the attribution the raw invoker event omits, sourced from context. */
|
|
332
|
+
role: string;
|
|
333
|
+
granted: number;
|
|
334
|
+
pool_remaining_after: number;
|
|
335
|
+
denied?: boolean;
|
|
283
336
|
}
|
|
284
337
|
/**
|
|
285
338
|
* Per-gig budget snapshot. Mirrors fields of domain_types/budget-state.json
|
|
@@ -319,6 +372,19 @@ export interface BudgetState {
|
|
|
319
372
|
* 0 when no invoker reported cost (stubbed invokers, skill-only gigs).
|
|
320
373
|
*/
|
|
321
374
|
settled_usd: number;
|
|
375
|
+
/**
|
|
376
|
+
* #turn-budget — turns remaining in the gig reserve pool (Item 2). Seeded from the dispatch
|
|
377
|
+
* `pool` (else `Standard.reserve_pool`, else 0), drawn down as chairs cross into reserve, never
|
|
378
|
+
* negative and never re-increased in v0 (strict draw-down, no preemption). Orthogonal to the
|
|
379
|
+
* append-unit ledger above: a draw moves ONLY this number.
|
|
380
|
+
*/
|
|
381
|
+
pool_remaining: number;
|
|
382
|
+
/**
|
|
383
|
+
* #turn-budget — the attributable draw ledger (Item 2). One record per chair that reached for a
|
|
384
|
+
* reserve, granted or denied, so a post-run reader can see who drew, how much, and what the pool
|
|
385
|
+
* had left — and so a starved chair is visible rather than a silent no-op.
|
|
386
|
+
*/
|
|
387
|
+
draws: ReserveDraw[];
|
|
322
388
|
}
|
|
323
389
|
/** One chair that did not run, and what stood in for it. */
|
|
324
390
|
export interface SkippedChair {
|
package/dist/src/runtime.js
CHANGED
|
@@ -9,6 +9,7 @@ import { executeSkillAsync } from "./skill_subprocess.js";
|
|
|
9
9
|
import { loadSkillPackage } from "./skills.js";
|
|
10
10
|
import { resolveModel } from "./claude_invoker.js";
|
|
11
11
|
import { resolveAgentGrants } from "./tool_providers.js";
|
|
12
|
+
import { resolveAndRealize } from "./venue_realize.js";
|
|
12
13
|
// core type → the process primitive that produces it (reverse of PRIMITIVE_OUTPUT_TYPE).
|
|
13
14
|
// A skill-backed chair seals its output as this primitive/core when its output_contract is
|
|
14
15
|
// a core type.
|
|
@@ -278,6 +279,31 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
278
279
|
// Both halves of reuse need it at t=0 — it is the field that decides whether two runs are
|
|
279
280
|
// the same pipeline, and a gate that fires after the money is spent is not a gate.
|
|
280
281
|
const genome_hash = genomeHash(standard);
|
|
282
|
+
// ── VENUE → DISPATCH WIRE ──────────────────────────────────────────────────────────────────
|
|
283
|
+
// When the gig names a venue, resolve + realize it ONCE, BEFORE any chair is invoked. The room is
|
|
284
|
+
// an ENFORCED performance space: `resolveAndRealize` runs the ordered gauntlet (dead name, wildcard
|
|
285
|
+
// door, standing-without-cadence, install-digest, credential-breach, per-seat ceiling) and returns
|
|
286
|
+
// a fail-closed refusal on the first breach. A refusal ABORTS the gig here — no chair spawns,
|
|
287
|
+
// nothing is sealed, no ledger row is written — exactly as an unresolvable tool grant rejects.
|
|
288
|
+
// The successful room is threaded onto every chair's ctx (below) so the spawn is confined by
|
|
289
|
+
// construction, and torn down at each chair's lifecycle end.
|
|
290
|
+
let gigRealization;
|
|
291
|
+
let gigVenue;
|
|
292
|
+
if (deps.venue !== undefined) {
|
|
293
|
+
const realization = resolveAndRealize(deps.venue, {
|
|
294
|
+
venues: new Map(deps.venues ?? []),
|
|
295
|
+
seats: standard.agents.map((agent) => ({ agent })),
|
|
296
|
+
ambientEnv: {},
|
|
297
|
+
...(deps.credentialsPresent ? { credentialsPresent: deps.credentialsPresent } : {}),
|
|
298
|
+
gigId: gig_id,
|
|
299
|
+
});
|
|
300
|
+
if (!realization.ok) {
|
|
301
|
+
throw new RuntimeError(`venue "${deps.venue}" refused this gig fail-closed: ${realization.refusal.code} — ${realization.refusal.detail}`);
|
|
302
|
+
}
|
|
303
|
+
gigRealization = realization;
|
|
304
|
+
// realize() only returns ok once the slug resolved, so the venue is present in the map.
|
|
305
|
+
gigVenue = deps.venues?.get(deps.venue);
|
|
306
|
+
}
|
|
281
307
|
// Hash the gig input LAZILY. Three callers want it now (#196's provenance backfill, the
|
|
282
308
|
// resume identity, and the reuse key) but a hostile or circular payload must not be
|
|
283
309
|
// canonicalized on a run that never needs it — which is every run that uses none of the
|
|
@@ -483,6 +509,10 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
483
509
|
};
|
|
484
510
|
// Budget state. When deps.budget is undefined, enforcement is OFF (back-compat).
|
|
485
511
|
// When present, we track an in-memory BudgetState mirroring budget-state.json.
|
|
512
|
+
// #turn-budget — the gig reserve pool opens from the dispatch payload FIRST, then the standard's
|
|
513
|
+
// default, then 0. `??` (not max/sum) so the dispatch declaration wins deterministically when both
|
|
514
|
+
// are present and absent stays distinct from a declared 0.
|
|
515
|
+
const poolOpening = deps.budget?.pool ?? standard.reserve_pool ?? 0;
|
|
486
516
|
const budget = deps.budget
|
|
487
517
|
? {
|
|
488
518
|
opening: deps.budget.opening,
|
|
@@ -496,8 +526,16 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
496
526
|
k: deps.budget.k ?? 0.1,
|
|
497
527
|
unit: "append-units",
|
|
498
528
|
settled_usd: 0,
|
|
529
|
+
pool_remaining: poolOpening,
|
|
530
|
+
draws: [],
|
|
499
531
|
}
|
|
500
532
|
: null;
|
|
533
|
+
// #turn-budget — reserve turns HELD by prepared-but-not-yet-settled chairs, the pool's mirror of
|
|
534
|
+
// the append-unit `reserved` above. `prepareChair` runs synchronously for the whole ready batch
|
|
535
|
+
// before any invoke, so an offer computed against `pool_remaining - poolReserved` cannot let two
|
|
536
|
+
// parallel chairs over-lend the same turns. A grant converts the hold to a real draw-down; a
|
|
537
|
+
// no-draw or denial releases it. Conservation therefore holds for every ordering, not by luck.
|
|
538
|
+
let poolReserved = 0;
|
|
501
539
|
// #232 — cost RESERVED by chairs that passed the gate but have not settled. `prepareChair`
|
|
502
540
|
// runs eagerly for the whole ready batch, so the gate must see its batch siblings' holds;
|
|
503
541
|
// but a hold is not spend. It converts to `spent` only when the invocation succeeds, and is
|
|
@@ -978,6 +1016,79 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
978
1016
|
for (const ch of ready)
|
|
979
1017
|
remaining.delete(ch.role);
|
|
980
1018
|
}
|
|
1019
|
+
// ── EXAMINE⇄AMEND ─────────────────────────────────────────────────────────────
|
|
1020
|
+
// The verify seat's verdict is not the last word when the standard budgets examine
|
|
1021
|
+
// rounds. If a VERIFY chair in this phase sealed a FAILING verdict (the one canonical
|
|
1022
|
+
// fail signal every Verdict carries: pass === false), re-run the maker(s) it judged —
|
|
1023
|
+
// the AMEND, with the failing verdict fed back so the fix targets what actually failed —
|
|
1024
|
+
// and re-verify, up to max_examine_rounds, stopping the instant the verdict passes.
|
|
1025
|
+
// Rounds spent without a pass leave the last, failing verdict standing: the loop iterates
|
|
1026
|
+
// to green, it never launders red into green. depends_on chairs read their inputs from
|
|
1027
|
+
// producedByRole, so replacing the maker's record there is what makes the re-verify judge
|
|
1028
|
+
// the amended artifact rather than the original.
|
|
1029
|
+
const examineRounds = standard.max_examine_rounds ?? 0;
|
|
1030
|
+
if (examineRounds > 0) {
|
|
1031
|
+
const allChairs = standard.phases.flatMap((p) => p.chairs);
|
|
1032
|
+
const phaseNameOf = (role) => standard.phases.find((p) => p.chairs.some((c) => c.role === role))?.name ?? phase.name;
|
|
1033
|
+
const primitivesOf = (slug) => standard.agents.find((a) => a.slug === slug)?.primitives ?? [];
|
|
1034
|
+
const dropFromProduced = (recs) => {
|
|
1035
|
+
for (const r of recs) {
|
|
1036
|
+
const idx = produced.indexOf(r);
|
|
1037
|
+
if (idx >= 0)
|
|
1038
|
+
produced.splice(idx, 1);
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
const failingVerdict = (role) => (producedByRole.get(role) ?? []).find((r) => (deps.outputs.coreTypeOf(r.domain_type) ?? "") === "Verdict" &&
|
|
1042
|
+
r.data.pass === false);
|
|
1043
|
+
for (const vch of phase.chairs) {
|
|
1044
|
+
if (!primitivesOf(vch.agent_slug).includes("VERIFY"))
|
|
1045
|
+
continue;
|
|
1046
|
+
let verdict = failingVerdict(vch.role);
|
|
1047
|
+
if (!verdict)
|
|
1048
|
+
continue; // no verdict, or it passed — nothing to amend
|
|
1049
|
+
// The maker(s) to amend are the dependencies that produced the ARTIFACT this verdict
|
|
1050
|
+
// judged — keyed on the chair's Artifact OUTPUT, not the agent's primitive set. A
|
|
1051
|
+
// multi-primitive agent (e.g. one seat plans, another writes) must have only its writing
|
|
1052
|
+
// seat re-run; the plan is settled for the run and is not amended.
|
|
1053
|
+
const producesArtifact = (c) => {
|
|
1054
|
+
const out = c.output_contract[0];
|
|
1055
|
+
return !!out && (deps.outputs.coreTypeOf(out) ?? "") === "Artifact";
|
|
1056
|
+
};
|
|
1057
|
+
const makers = vch.depends_on
|
|
1058
|
+
.map((role) => allChairs.find((c) => c.role === role))
|
|
1059
|
+
.filter((c) => !!c && producesArtifact(c));
|
|
1060
|
+
if (makers.length === 0)
|
|
1061
|
+
continue; // nothing to re-run — a verify with no maker to amend
|
|
1062
|
+
for (let round = 1; round <= examineRounds && verdict; round++) {
|
|
1063
|
+
checkpoint();
|
|
1064
|
+
emit({ type: "phase_start", phase: `${phase.name}:amend#${round}`, roles: [...makers.map((m) => m.role), vch.role] });
|
|
1065
|
+
const feedback = verdict;
|
|
1066
|
+
// AMEND: each maker re-runs with the failing verdict fed in as an extra input, so
|
|
1067
|
+
// the seat that built the change fixes the exact thing the verify caught.
|
|
1068
|
+
for (const mk of makers) {
|
|
1069
|
+
const prep = prepareChair(mk, phaseNameOf(mk.role));
|
|
1070
|
+
if (!prep.inputs.includes(feedback))
|
|
1071
|
+
prep.inputs.push(feedback);
|
|
1072
|
+
const recs = await invokeAndWriteChair(prep);
|
|
1073
|
+
dropFromProduced(producedByRole.get(mk.role) ?? []);
|
|
1074
|
+
producedByRole.set(mk.role, recs);
|
|
1075
|
+
produced.push(...recs);
|
|
1076
|
+
noteCheckpointRole(mk.role, phaseNameOf(mk.role), recs);
|
|
1077
|
+
}
|
|
1078
|
+
// RE-VERIFY the amended artifact.
|
|
1079
|
+
const vprep = prepareChair(vch, phase.name);
|
|
1080
|
+
const vrecs = await invokeAndWriteChair(vprep);
|
|
1081
|
+
dropFromProduced(producedByRole.get(vch.role) ?? []);
|
|
1082
|
+
producedByRole.set(vch.role, vrecs);
|
|
1083
|
+
produced.push(...vrecs);
|
|
1084
|
+
noteCheckpointRole(vch.role, phase.name, vrecs);
|
|
1085
|
+
if (budget)
|
|
1086
|
+
budget.settled_usd = usage.total_cost_usd;
|
|
1087
|
+
saveCheckpoint();
|
|
1088
|
+
verdict = failingVerdict(vch.role); // undefined once it passes → loop ends
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
981
1092
|
}
|
|
982
1093
|
// Resolve a chair's declared output types into seal-specs (type → core → primitive).
|
|
983
1094
|
function outputSpecsFor(domainTypes, fallbackPrimitive) {
|
|
@@ -1294,10 +1405,27 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
1294
1405
|
reserved += cost;
|
|
1295
1406
|
reservedCost = cost;
|
|
1296
1407
|
}
|
|
1408
|
+
// #turn-budget — RESERVE OFFER. Only a chair that DECLARED a `turn_reserve` reaches for the pool;
|
|
1409
|
+
// one that declared none threads no ctx.turn_reserve, so the invoker's own opts-level reserve is
|
|
1410
|
+
// undisturbed (the #329 continuation path). When a budget is enforced the offer is capped to what
|
|
1411
|
+
// the pool can still lend (min(own reserve, pool_remaining - poolReserved)) and HELD; with no
|
|
1412
|
+
// budget there is no pool to cap against, so the declared reserve threads through directly.
|
|
1413
|
+
let reserveOffer;
|
|
1414
|
+
if (chair.turn_reserve !== undefined && !lookup?.hit) {
|
|
1415
|
+
if (budget) {
|
|
1416
|
+
const poolAvailable = Math.max(0, budget.pool_remaining - poolReserved);
|
|
1417
|
+
reserveOffer = Math.min(chair.turn_reserve, poolAvailable);
|
|
1418
|
+
poolReserved += reserveOffer;
|
|
1419
|
+
}
|
|
1420
|
+
else {
|
|
1421
|
+
reserveOffer = chair.turn_reserve;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1297
1424
|
return {
|
|
1298
1425
|
chair, phaseName, agent, primitive, domain_type, output_specs, inputs, skills,
|
|
1299
1426
|
missing_skills: missing, producer_slug: agent.slug, domain,
|
|
1300
1427
|
...(reservedCost !== undefined ? { cost: reservedCost } : {}),
|
|
1428
|
+
...(reserveOffer !== undefined ? { reserve_offer: reserveOffer } : {}),
|
|
1301
1429
|
...(lookup ? { reuse_key: lookup.key } : {}),
|
|
1302
1430
|
...(lookup?.hit ? { reuse_hit: lookup.hit } : {}),
|
|
1303
1431
|
};
|
|
@@ -1332,6 +1460,11 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
1332
1460
|
settleChairCost(p, false);
|
|
1333
1461
|
throw e;
|
|
1334
1462
|
}
|
|
1463
|
+
finally {
|
|
1464
|
+
// Venue → dispatch wire: tear the room down at the chair lifecycle end. `teardown()` is
|
|
1465
|
+
// idempotent and a no-op when no venue was named, so a venue-less gig is unaffected.
|
|
1466
|
+
gigRealization?.teardown();
|
|
1467
|
+
}
|
|
1335
1468
|
}
|
|
1336
1469
|
async function executeChair(p) {
|
|
1337
1470
|
const { chair, phaseName, inputs, skills, output_specs, producer_slug, domain } = p;
|
|
@@ -1428,22 +1561,89 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
1428
1561
|
// try for the same reason: a chair killed by #250's abort still started, and still cost.
|
|
1429
1562
|
const sink = makeUsageSink();
|
|
1430
1563
|
startedInvocations++;
|
|
1564
|
+
// #turn-budget — reserve-draw interception (Items 2 & 3), all at this ONE seat so the
|
|
1565
|
+
// observable cannot drift. `reserveHeld` is what prepareChair offered and held against
|
|
1566
|
+
// `poolReserved`; `drew` records whether the chair actually crossed into its reserve; `settled`
|
|
1567
|
+
// guards the hold so it is converted (grant) or released (no-draw / denial) exactly once.
|
|
1568
|
+
const reserveHeld = p.reserve_offer ?? 0;
|
|
1569
|
+
let drew = false;
|
|
1570
|
+
let reserveSettled = false;
|
|
1571
|
+
const releaseHold = () => {
|
|
1572
|
+
if (reserveSettled)
|
|
1573
|
+
return;
|
|
1574
|
+
poolReserved -= reserveHeld;
|
|
1575
|
+
reserveSettled = true;
|
|
1576
|
+
};
|
|
1431
1577
|
try {
|
|
1432
1578
|
data = await deps.invoke({
|
|
1433
|
-
agent, phase: phaseName, inputs, gig_input: gigInput, skills,
|
|
1579
|
+
agent, phase: phaseName, gig_id, inputs, gig_input: gigInput, skills,
|
|
1434
1580
|
missing_skills: p.missing_skills, // #241 — what did NOT resolve, so the prompt can't assert it
|
|
1435
1581
|
output_types: output_specs.map((s) => s.domain_type), // #174 — the chair's promised subset
|
|
1436
1582
|
// #250 level 2 + #237 — the cancellation signal and the run's depth reach the invocation
|
|
1437
1583
|
// itself, so an invoker can kill its child and shape what it asks the model for.
|
|
1438
1584
|
...(deps.signal ? { signal: deps.signal } : {}),
|
|
1439
1585
|
...(deps.depth ? { depth: deps.depth } : {}),
|
|
1440
|
-
|
|
1586
|
+
// #turn-budget — the chair's own turn budget threads through exactly as `depth` does; the
|
|
1587
|
+
// reserve is the pool-capped OFFER, not the raw declaration, and is present only when the
|
|
1588
|
+
// chair declared a reserve (so a reserve-less chair leaves the invoker's opts-level default
|
|
1589
|
+
// untouched — the #329 continuation path stays byte-identical).
|
|
1590
|
+
...(p.chair.turn_budget !== undefined ? { turn_budget: p.chair.turn_budget } : {}),
|
|
1591
|
+
...(p.reserve_offer !== undefined ? { turn_reserve: p.reserve_offer } : {}),
|
|
1592
|
+
// The venue → dispatch wire: thread the realized room onto the chair's ctx ONLY when a
|
|
1593
|
+
// venue resolved, so the invoker narrows the spawn by construction; both fields stay
|
|
1594
|
+
// absent otherwise (the venue-less path is unchanged).
|
|
1595
|
+
...(gigRealization && gigVenue ? { realization: gigRealization, venue: gigVenue } : {}),
|
|
1596
|
+
onEvent: (ev) => {
|
|
1597
|
+
sink.fold(ev);
|
|
1598
|
+
emit({ type: "agent_event", phase: phaseName, role: chair.role, event: ev });
|
|
1599
|
+
if (!budget)
|
|
1600
|
+
return;
|
|
1601
|
+
// A chair crossed its budget into a granted reserve. Set `yielding` (D1: one condition
|
|
1602
|
+
// at two scales — the gig is yielding IFF a seated chair is drawing reserve), convert the
|
|
1603
|
+
// held offer into a real pool draw-down, and record the attributable draw. `granted` is
|
|
1604
|
+
// the amount we HELD (min(own reserve, pool-at-prep)), not the event's self-report, so
|
|
1605
|
+
// the pool can never be lent past what it holds.
|
|
1606
|
+
if (ev.type === "budget_reserve_granted") {
|
|
1607
|
+
drew = true;
|
|
1608
|
+
const granted = reserveHeld;
|
|
1609
|
+
if (!reserveSettled) {
|
|
1610
|
+
poolReserved -= reserveHeld;
|
|
1611
|
+
reserveSettled = true;
|
|
1612
|
+
}
|
|
1613
|
+
budget.pool_remaining -= granted;
|
|
1614
|
+
budget.agent_state = "yielding";
|
|
1615
|
+
budget.draws.push({ role: chair.role, granted, pool_remaining_after: budget.pool_remaining });
|
|
1616
|
+
emit({ type: "budget_state", phase: phaseName, role: chair.role, agent_state: "yielding", pool_remaining: budget.pool_remaining });
|
|
1617
|
+
}
|
|
1618
|
+
else if (ev.type === "budget_reserve_denied") {
|
|
1619
|
+
// The chair reached for a reserve an empty pool could not give. Record the starvation
|
|
1620
|
+
// (never silent) and release the — necessarily zero — hold. The gig is NOT yielding: a
|
|
1621
|
+
// denied reach is not a draw, so the biconditional stays false.
|
|
1622
|
+
releaseHold();
|
|
1623
|
+
budget.draws.push({ role: chair.role, granted: 0, pool_remaining_after: budget.pool_remaining, denied: true });
|
|
1624
|
+
}
|
|
1625
|
+
},
|
|
1441
1626
|
});
|
|
1442
1627
|
}
|
|
1628
|
+
catch (e) {
|
|
1629
|
+
// The chair spent its reserve (and the pool) WITHOUT landing → depleted (O12/INV16). Only a
|
|
1630
|
+
// chair that actually drew moves to depleted; a plain failure keeps its own error semantics.
|
|
1631
|
+
if (budget && drew)
|
|
1632
|
+
budget.agent_state = "depleted";
|
|
1633
|
+
releaseHold();
|
|
1634
|
+
throw e;
|
|
1635
|
+
}
|
|
1443
1636
|
finally {
|
|
1444
1637
|
if (sink.attributed())
|
|
1445
1638
|
attributedInvocations++;
|
|
1446
1639
|
}
|
|
1640
|
+
// The drawing chair LANDED within its reserve → clear yielding back to `active` (O12/INV16).
|
|
1641
|
+
// The gig-end success path then settles it; an idle chair that held but never drew releases here.
|
|
1642
|
+
if (budget && drew && budget.agent_state === "yielding") {
|
|
1643
|
+
budget.agent_state = "active";
|
|
1644
|
+
emit({ type: "budget_state", phase: phaseName, role: chair.role, agent_state: "active", pool_remaining: budget.pool_remaining });
|
|
1645
|
+
}
|
|
1646
|
+
releaseHold();
|
|
1447
1647
|
// Runtime output_contract check: every type the chair promised must be covered by the
|
|
1448
1648
|
// bound agent's declared output_types (compose-time mirror; a hand-rolled literal could
|
|
1449
1649
|
// still ship a mismatch).
|