@crewhaus/ir 0.5.7 → 0.6.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/dist/index.d.ts CHANGED
@@ -86,6 +86,61 @@ export type IrSubAgentDefinition = {
86
86
  readonly federation?: {
87
87
  readonly url: string;
88
88
  };
89
+ /** 0.6.0 §4.2 — provenance: the `models:` profile `model` resolved from. */
90
+ readonly modelProfile?: string;
91
+ /**
92
+ * 0.6.0 §4.2 / §7.7 — the `instructions` overlay of the profile `model`
93
+ * resolved from, carried RAW (not folded into `instructions`) so the spawner
94
+ * folds it only for the declared / inherited plans: a Task call that pins one
95
+ * of `allowedProfiles` runs on THAT option's overlay instead. Every other
96
+ * serving slot folds its overlay at lower time; the sub-agent is the one slot
97
+ * whose prompt depends on a per-call choice.
98
+ */
99
+ readonly overlay?: string;
100
+ /** 0.6.0 §7.7 — the child's own request params (spec `thinking` /
101
+ * `max_tokens` / `temperature`, or the referenced profile's). */
102
+ readonly thinking?: IrThinking;
103
+ readonly maxTokens?: number;
104
+ readonly temperature?: number;
105
+ /** 0.6.0 §7.7 — per-sub-agent model routing (the agent block's quartet). */
106
+ readonly modelFallbacks?: readonly string[];
107
+ readonly circuitBreaker?: IrCircuitBreaker;
108
+ readonly modelTiers?: IrModelTiers;
109
+ readonly modelPool?: IrModelPool;
110
+ /** 0.6.0 §7.7 — fraction of the parent's `budget.usd` this child may spend. */
111
+ readonly budgetShare?: number;
112
+ /** 0.6.0 §7.7 — inherit the parent's SERVED arm instead of the declared
113
+ * primary (default false keeps today's declared-primary behaviour). */
114
+ readonly inheritRouting?: boolean;
115
+ /**
116
+ * 0.6.0 §7.7 — the `models:` profiles the Task tool's `profile` argument
117
+ * may name for this child, each RESOLVED at lower time to the model and
118
+ * request params the child runs on when that profile is chosen (the same
119
+ * expansion a `model: $<profile>` slot gets). Resolving here — not at
120
+ * runtime — is what keeps the registry provenance-only downstream of the
121
+ * compiler: nothing at runtime ever sees a `$` or looks a profile up.
122
+ */
123
+ readonly allowedProfiles?: readonly IrSubAgentProfileOption[];
124
+ };
125
+ /**
126
+ * 0.6.0 §7.7 — one entry of {@link IrSubAgentDefinition.allowedProfiles}: the
127
+ * profile NAME (the allowlist the model-filled Task `profile` argument is
128
+ * checked against — it can never name a model outside the spec) plus the
129
+ * profile's resolved serving slot. When the Task call pins this profile the
130
+ * child runs single-model on `model` with these params and this failover
131
+ * chain; its own `model_pool` / `model_tiers` do not route that call. The
132
+ * profile's `instructions` overlay is carried so the spawner can fold it into
133
+ * the child's prompt for that call only.
134
+ */
135
+ export type IrSubAgentProfileOption = {
136
+ readonly profile: string;
137
+ readonly model: string;
138
+ readonly thinking?: IrThinking;
139
+ readonly maxTokens?: number;
140
+ readonly temperature?: number;
141
+ readonly overlay?: string;
142
+ readonly modelFallbacks?: readonly string[];
143
+ readonly circuitBreaker?: IrCircuitBreaker;
89
144
  };
90
145
  /**
91
146
  * Section 14 — per-tool runtime config carried verbatim from the spec to
@@ -104,6 +159,11 @@ export type IrToolConfigs = Readonly<Record<string, unknown>>;
104
159
  */
105
160
  export type IrCompaction = {
106
161
  readonly model?: string;
162
+ /** 0.6.0 §4.2 — provenance: the `models:` profile `model` resolved from. */
163
+ readonly modelProfile?: string;
164
+ /** 0.6.0 §4.2 — the profile's pinned request params (autocompact builds its
165
+ * own request). Present only when the profile declares any. */
166
+ readonly params?: IrModelParams;
107
167
  /** Loop contract 0.4 (Batch A) — context-window fill fraction that
108
168
  * triggers autocompaction (spec `compaction.threshold`, 0.5–0.99).
109
169
  * Runtime default applies when absent. */
@@ -165,23 +225,213 @@ export type IrModelTiers = {
165
225
  readonly priorToolDensityThreshold?: number;
166
226
  };
167
227
  };
168
- /** One declared candidate in a `model_pool`. */
169
- export type IrModelPoolCandidate = {
228
+ /**
229
+ * 0.6.0 §4.2 — the per-model request parameters a `models:` profile can pin
230
+ * on an AUXILIARY slot (judge / compaction / degrade / security / watchme),
231
+ * where the consumer builds its own provider request. Carried ONLY when the
232
+ * slot resolved through a profile that declares them; absent otherwise.
233
+ */
234
+ export type IrModelParams = {
235
+ readonly thinking?: IrThinking;
236
+ readonly maxTokens?: number;
237
+ readonly temperature?: number;
238
+ };
239
+ /**
240
+ * 0.6.0 §5.4 — the RESTRICTED per-profile permission schema: `deny` and
241
+ * `ask` ONLY. A profile can narrow the shape's permissions (decision-level
242
+ * meet: deny < ask < allow), never widen them — the spec rejects
243
+ * `alwaysAllow` / `mode` / `ask_mode`, and `modelPlanIntegrity` re-pins the
244
+ * shape for direct-IR builders.
245
+ */
246
+ export type IrProfilePermissions = {
247
+ readonly deny?: readonly string[];
248
+ readonly ask?: readonly string[];
249
+ };
250
+ /**
251
+ * 0.6.0 §7.11 (N1) — what a profile REQUIRES of its own model. The four
252
+ * feature flags mirror `ProviderFeatures`; the two size floors are spelled
253
+ * as floors (`…Gte`). Structurally `@crewhaus/cost-tracker`'s
254
+ * `CapabilityRequirement` and `@crewhaus/model-plan`'s `FeatureRequirement`,
255
+ * so one value serves the compile-time twin and the runtime gate.
256
+ */
257
+ export type IrModelRequires = {
258
+ readonly tool_use?: boolean;
259
+ readonly vision?: boolean;
260
+ readonly thinking?: boolean;
261
+ readonly web_search?: boolean;
262
+ readonly contextWindowGte?: number;
263
+ readonly maxOutputTokensGte?: number;
264
+ };
265
+ /**
266
+ * 0.6.0 §4.1 — a DECLARED capability override for a model the offline
267
+ * capability table does not know (local / azure / named hosts). Flat, every
268
+ * field optional: an unknown fact never satisfies a requirement on it.
269
+ */
270
+ export type IrModelCapabilities = {
271
+ readonly tool_use?: boolean;
272
+ readonly vision?: boolean;
273
+ readonly thinking?: boolean;
274
+ readonly web_search?: boolean;
275
+ readonly caching?: "explicit" | "automatic" | false;
276
+ readonly contextWindow?: number;
277
+ readonly maxOutputTokens?: number;
278
+ };
279
+ /**
280
+ * 0.6.0 §4.2 — one resolved model profile: the lower-time expansion of a
281
+ * `models:` entry, and the settings a `model_pool` candidate carries inline.
282
+ * Every field but `model` is optional, and a profile-less candidate is a bare
283
+ * `{ model, tags }` with every key here absent — which is exactly why
284
+ * existing pools lower and emit byte-identically. `profile` is the registry
285
+ * name (provenance AND the scoreboard arm identity, §7.9); `overlay` is the
286
+ * per-model instructions overlay appended in the volatile prompt region when
287
+ * this candidate serves; `tools` is SUBSET-ONLY (builtin keys as the spec
288
+ * spells them, `mcp__<server>__*` globs, `Consult` / `Escalate`; `[]` means
289
+ * zero shape tools); `costCapUsdMicros` is the per-profile cap inside a run.
290
+ * `$` never appears in an IR model string — references are resolved here.
291
+ */
292
+ export type IrModelProfile = {
293
+ readonly profile?: string;
170
294
  readonly model: string;
295
+ readonly tags?: readonly string[];
296
+ readonly thinking?: IrThinking;
297
+ readonly maxTokens?: number;
298
+ readonly temperature?: number;
299
+ readonly modelCallTimeoutMs?: number;
300
+ readonly overlay?: string;
301
+ readonly tools?: readonly string[];
302
+ readonly toolConfigs?: IrToolConfigs;
303
+ readonly permissions?: IrProfilePermissions;
304
+ readonly rateLimits?: IrRateLimits;
305
+ readonly caching?: "prefer" | "off";
306
+ readonly costCapUsdMicros?: number;
307
+ readonly requires?: IrModelRequires;
308
+ readonly capabilities?: IrModelCapabilities;
309
+ readonly fallbacks?: readonly string[];
310
+ readonly circuitBreaker?: IrCircuitBreaker;
311
+ };
312
+ /**
313
+ * 0.6.0 §4.2 — the `models:` registry as lowered: profile name → resolved
314
+ * profile (sentinels resolved by price rank, every snake_case key renamed).
315
+ * Present on a variant ONLY when the spec declares `models:`; emitters never
316
+ * read it (README / loop projection / `models explain` do), so an absent
317
+ * registry keeps every bundle byte-identical.
318
+ */
319
+ export type IrModelProfiles = Readonly<Record<string, IrModelProfile>>;
320
+ /**
321
+ * One declared candidate in a `model_pool`. 0.6.0 §4.2: a profile plus the
322
+ * routing identity `tags` (always present — the spec defaults them; a
323
+ * `$profile` candidate inherits the profile's tags when it declares none)
324
+ * and `enabled: false`, which withdraws the candidate from routing without
325
+ * deleting its learned history. The lowering inserts `model` then `tags`
326
+ * FIRST and spreads every 0.6.0 key after them, so `JSON.stringify` of a
327
+ * plain candidate is byte-identical to 0.5.x (the key-order guard).
328
+ */
329
+ export type IrModelPoolCandidate = IrModelProfile & {
171
330
  readonly tags: readonly string[];
331
+ readonly enabled?: false;
332
+ };
333
+ /** 0.6.0 §7.2.2 — the `when:` clause of a route rule (spec keys verbatim — the
334
+ * runtime's `evaluateRules` in `@crewhaus/model-plan` reads this shape). */
335
+ export type IrModelPoolRuleWhen = {
336
+ readonly has_images?: boolean;
337
+ readonly message_matches?: string;
338
+ readonly user_text_chars_gt?: number;
339
+ readonly context_tokens_gt?: number;
340
+ readonly tool_in_play?: boolean;
341
+ readonly channel?: string;
342
+ readonly budget_spent_ratio_gt?: number;
343
+ readonly turn_index_lt?: number;
344
+ };
345
+ /**
346
+ * 0.6.0 §7.2.2 — one rule-directed routing rule, evaluated first-match in
347
+ * `preRoute` before the policy. `use` names a candidate tag, a roster arm id
348
+ * (a `$profile` reference lowers to its profile name — the arm identity), or
349
+ * a capability requirement the turn's candidates must satisfy.
350
+ */
351
+ export type IrModelPoolRule = {
352
+ readonly id: string;
353
+ readonly when: IrModelPoolRuleWhen;
354
+ readonly use: string | {
355
+ readonly requires: IrModelRequires;
356
+ };
357
+ readonly enabled?: boolean;
358
+ };
359
+ /** 0.6.0 §7.2.3 — `policy: classifier`: a forced-tool single call whose
360
+ * verdict is constrained to the roster's tags. */
361
+ export type IrModelPoolClassifier = {
362
+ readonly model: string;
363
+ readonly modelProfile?: string;
364
+ readonly labels: Readonly<Record<string, string>>;
365
+ readonly maxTokens?: number;
366
+ };
367
+ /**
368
+ * 0.6.0 §7.3–§7.8 — the hybrid strategy block. Role slots (`draft`,
369
+ * `escalateTo`, `members`, `escalateOnDisagreement`) name a candidate tag or
370
+ * a roster arm id; model slots (`guide.model`, `shadow.candidate`,
371
+ * `shadow.gradeWith`, `committee.judge`) are resolved model strings with
372
+ * their profile provenance beside them.
373
+ */
374
+ export type IrModelPoolStrategy = {
375
+ readonly cascade?: {
376
+ readonly draft: string;
377
+ readonly escalateTo: string;
378
+ readonly cleanPrompt?: boolean;
379
+ };
380
+ readonly guide?: {
381
+ readonly model: string;
382
+ readonly modelProfile?: string;
383
+ readonly every?: "first_turn" | "turn";
384
+ readonly maxTokens?: number;
385
+ readonly budgetUsd?: number;
386
+ };
387
+ readonly shadow?: {
388
+ readonly candidate: string;
389
+ readonly candidateProfile?: string;
390
+ readonly sampleRate?: number;
391
+ readonly gradeWith?: string;
392
+ readonly gradeWithProfile?: string;
393
+ };
394
+ readonly committee?: {
395
+ readonly members: readonly string[];
396
+ readonly judge?: string;
397
+ readonly judgeProfile?: string;
398
+ readonly escalateOnDisagreement?: string;
399
+ };
400
+ readonly modelDirected?: boolean;
401
+ readonly maxEscalations?: number;
402
+ };
403
+ /** 0.6.0 §6.3, §7.10 — the reward block (a sibling of `learning`; nothing
404
+ * here is optimizer-tunable). */
405
+ export type IrModelPoolReward = {
406
+ readonly qualitySource?: "none" | "in_loop" | "shadow" | "promoted";
407
+ readonly priors?: "none" | "eval";
408
+ readonly floor?: {
409
+ readonly arm?: string;
410
+ readonly confidence?: number;
411
+ readonly tolerance?: number;
412
+ };
413
+ readonly resetOnProfileChange?: boolean;
172
414
  };
173
415
  /**
174
416
  * Adaptive model routing — the N-candidate `model_pool` with a per-turn
175
- * selection `policy` (`static` | `heuristic` | `learned`). Carried on the
176
- * agent blocks of the routing-capable shapes (cli, channel, managed), mutually
177
- * exclusive with `modelTiers`/`modelFallbacks` (enforced in the spec). Absent
178
- * when the spec omits `model_pool` — codegen gates on presence so an unset
179
- * block leaves bundles byte-identical. `policy` and each candidate's `tags` are
180
- * always present (spec defaults them); every other knob is carried verbatim.
417
+ * selection `policy` (`static` | `heuristic` | `learned` | `classifier`).
418
+ * Carried on the agent blocks of the routing-capable shapes (cli, channel,
419
+ * managed), on workflow steps, graph nodes (0.6.0), crew roles and
420
+ * sub-agents (0.6.0), mutually exclusive with `modelTiers`/`modelFallbacks`
421
+ * (enforced in the spec). Absent when the spec omits `model_pool` — codegen
422
+ * gates on presence so an unset block leaves bundles byte-identical. `policy`
423
+ * and each candidate's `tags` are always present (spec defaults them); every
424
+ * other knob is carried verbatim.
425
+ *
426
+ * 0.6.0 §7.1 — the pool IS the hybrid container: `rules` / `directives` /
427
+ * `classifier` / `strategy` / `reward` / `scope` are siblings of `routing`
428
+ * and `learning`, each present only when declared. The whole block travels
429
+ * to runtime inside the one `JSON.stringify(modelPool)` every emitter
430
+ * already writes, so every new key rides that blob without an emitter edit.
181
431
  */
182
432
  export type IrModelPool = {
183
433
  readonly candidates: readonly IrModelPoolCandidate[];
184
- readonly policy: "static" | "heuristic" | "learned";
434
+ readonly policy: "static" | "heuristic" | "learned" | "classifier";
185
435
  readonly objective?: {
186
436
  readonly quality?: number;
187
437
  readonly cost?: number;
@@ -203,6 +453,18 @@ export type IrModelPool = {
203
453
  readonly seed?: string;
204
454
  readonly bandit?: "epsilon-greedy" | "thompson";
205
455
  };
456
+ /** 0.6.0 §7.2.1 — per-message `/model` steering (default off everywhere). */
457
+ readonly directives?: boolean;
458
+ readonly rules?: readonly IrModelPoolRule[];
459
+ readonly classifier?: IrModelPoolClassifier;
460
+ readonly strategy?: IrModelPoolStrategy;
461
+ readonly reward?: IrModelPoolReward;
462
+ /** 0.6.0 §7.9 — scoped routeKey prefix, carried verbatim when declared.
463
+ * Never stamped at lower time (that would change every pre-0.6.0 pooled
464
+ * step/role blob); the host that knows the scope defaults it at runtime —
465
+ * the crew orchestrator to the role name (PR 7b), the composition root to
466
+ * the caller's toolset scope (PR 10). */
467
+ readonly scope?: string;
206
468
  };
207
469
  /**
208
470
  * Loop contract 0.4 (Batch A) — extended-thinking selector, lowered from
@@ -325,9 +587,21 @@ export type IrFailureTaxonomy = readonly IrFailureTaxonomyEntry[];
325
587
  * spec's `budget` block. `usdMicros` is the dollar ceiling in USD-micros
326
588
  * (1 USD = 1_000_000) — the unit the runtime meters in. `onExceed` decides
327
589
  * the behaviour when accrued spend reaches the cap: `stop` ends the run
328
- * before the next turn; `degrade` re-resolves the primary model to `model`
329
- * (one cheaper rung) and continues. Carried on the interactive shapes that
330
- * loop (cli, channel, managed); absent when the spec omits the block.
590
+ * (0.6.0 §7.12: checked before every model call, tool iterations included);
591
+ * `degrade` re-resolves the primary model to `model` (one cheaper rung) and
592
+ * continues the rung serves the rest of the turn the degrade fired in and
593
+ * the run ends at the next turn boundary; under a `modelPool` the rung is
594
+ * the forced candidate rather than an adapter swap. `scope` (0.6.0) is
595
+ * carried ONLY when the spec
596
+ * declares it (absent ⇒ the runtime's `run` default, byte-identical for
597
+ * older specs): `session` seeds the meter on resume from the session log's
598
+ * persisted `cost_accrual` lines so the cap bounds the conversation.
599
+ * `judgeShare` (0.6.0 §6.2, spec `judge_share`) is likewise carried only
600
+ * when declared (absent ⇒ the runtime's 0.3 default): the fraction of
601
+ * `usdMicros` the auxiliary roles (judge, compaction, guide, classifier,
602
+ * consult, committee, shadow) may spend before the runtime raises the
603
+ * `judge_share_exhausted` signal. Carried on the shapes that run the shared
604
+ * loop; absent when the spec omits the block.
331
605
  */
332
606
  export type IrBudget = {
333
607
  readonly usdMicros: number;
@@ -336,7 +610,12 @@ export type IrBudget = {
336
610
  } | {
337
611
  readonly kind: "degrade";
338
612
  readonly model: string;
613
+ /** 0.6.0 §4.2 — provenance + the degrade profile's request params. */
614
+ readonly modelProfile?: string;
615
+ readonly params?: IrModelParams;
339
616
  };
617
+ readonly scope?: "run" | "session";
618
+ readonly judgeShare?: number;
340
619
  };
341
620
  /**
342
621
  * Loop contract 0.4 (Batch B, G02) — the grader selector inside
@@ -346,7 +625,8 @@ export type IrBudget = {
346
625
  * `criteria`. `model` is the judge model id; when ABSENT the runtime
347
626
  * uses the shape's primary model (the `cheapest` sentinel was already
348
627
  * resolved at lower time, like `compaction.model`). Judge calls are
349
- * METERED into the run budget.
628
+ * METERED into the run budget (0.6.0: on the run bus with
629
+ * `role: "judge"`, bounded by `IrBudget.judgeShare`).
350
630
  * - `contains` / `regex` — deterministic pass/fail text checks (score 1
351
631
  * on pass, 0 on fail; no model spend).
352
632
  */
@@ -354,6 +634,18 @@ export type IrEvaluationGrader = {
354
634
  readonly type: "llm_judge";
355
635
  readonly criteria: string;
356
636
  readonly model?: string;
637
+ /** 0.6.0 §4.2 — provenance: the `models:` profile `model` resolved from. */
638
+ readonly modelProfile?: string;
639
+ /** 0.6.0 §6.2 — a judge PANEL (resolved model strings); exclusive with `model`. */
640
+ readonly judges?: readonly string[];
641
+ /** 0.6.0 §6.2 — repeat verdicts per judge, folded by median. */
642
+ readonly repeats?: number;
643
+ /** 0.6.0 §6.2 — pinned judge sampling temperature. */
644
+ readonly temperature?: number;
645
+ /** 0.6.0 §6.2 — what the judge grades. */
646
+ readonly target?: "output" | "transcript";
647
+ /** 0.6.0 §4.2 — the judge profile's other pinned params (thinking / maxTokens). */
648
+ readonly params?: IrModelParams;
357
649
  } | {
358
650
  readonly type: "contains";
359
651
  readonly value: string;
@@ -384,10 +676,25 @@ export type IrEvaluation = {
384
676
  readonly grader: IrEvaluationGrader;
385
677
  /** Present iff `grader.type === "llm_judge"` (resolved default 0.7). */
386
678
  readonly threshold?: number;
387
- /** Resolved below-threshold behaviour (spec `on_fail`, default "retry"). */
388
- readonly onFail: "retry" | "halt" | "note";
679
+ /** Resolved below-threshold behaviour (spec `on_fail`, default "retry").
680
+ * 0.6.0 §7.3 adds `escalate`: re-run the turn on the pool's
681
+ * `strategy.cascade.escalateTo` candidate (else the strongest). */
682
+ readonly onFail: "retry" | "halt" | "note" | "escalate";
389
683
  /** Resolved retry hard-cap (spec `max_retries`, default 1). */
390
684
  readonly maxRetries: number;
685
+ /**
686
+ * 0.6.0 §7.3 — present iff `onFail === "escalate"`: the roster arm the
687
+ * failing turn is re-run on, RESOLVED at lower time from
688
+ * `model_pool.strategy.cascade.escalate_to` (a candidate tag, or a
689
+ * `$profile` lowered to its arm id), else the pool's strongest candidate —
690
+ * its `routing.strongTag` (default `strong`) when a candidate carries it,
691
+ * else the last declared candidate's arm id (the router's `escalation()`
692
+ * convention). The runtime resolves it against the booted roster; it never
693
+ * names a model outside the spec.
694
+ */
695
+ readonly escalateTo?: string;
696
+ /** 0.6.0 §4.3 — the judge-independence lint waiver; carried only when true. */
697
+ readonly allowSelfJudge?: true;
391
698
  };
392
699
  /**
393
700
  * Loop contract 0.4 (Batch B, G02) — the judge gate carried by
@@ -416,6 +723,22 @@ export type IrJudge = {
416
723
  readonly onFail: "retry_previous" | "halt" | "continue";
417
724
  /** Resolved re-run hard-cap (spec `max_retries`, default 1). */
418
725
  readonly maxRetries: number;
726
+ /** 0.6.0 §4.2 — provenance of the judge model folded into the step's/node's
727
+ * `model` slot, when it resolved through a `models:` profile. */
728
+ readonly modelProfile?: string;
729
+ /** 0.6.0 §6.2 — a judge PANEL (resolved model strings); exclusive with the
730
+ * single judge model. */
731
+ readonly judges?: readonly string[];
732
+ /** 0.6.0 §6.2 — repeat verdicts per judge, folded by median. */
733
+ readonly repeats?: number;
734
+ /** 0.6.0 §6.2 — pinned judge sampling temperature. */
735
+ readonly temperature?: number;
736
+ /** 0.6.0 §6.2 — what the judge grades. */
737
+ readonly target?: "output" | "transcript";
738
+ /** 0.6.0 §7.3 — the pool tag / arm id a `retry_previous` re-run is forced onto. */
739
+ readonly escalateTo?: string;
740
+ /** 0.6.0 §4.2 — the judge profile's other pinned params (thinking / maxTokens). */
741
+ readonly params?: IrModelParams;
419
742
  };
420
743
  /**
421
744
  * Pillar 3 (FR-004) — per-target security fabric configuration the
@@ -436,6 +759,9 @@ export type IrSecurity = {
436
759
  readonly justification?: {
437
760
  readonly judge: "rule-based" | "claude";
438
761
  readonly model?: string;
762
+ /** 0.6.0 §4.2 — provenance + the judge profile's request params. */
763
+ readonly modelProfile?: string;
764
+ readonly params?: IrModelParams;
439
765
  };
440
766
  /**
441
767
  * Pillar 3 sink-side fabric (FR-006) — the egress-matching strategy.
@@ -780,6 +1106,12 @@ export type IrSlo = {
780
1106
  readonly ttftMs?: number;
781
1107
  readonly costPerHourUsd?: number;
782
1108
  readonly egressBlockRate?: number;
1109
+ /** Escalations per turn (spec `escalation_rate`). */
1110
+ readonly escalationRate?: number;
1111
+ /** Failing in-loop grades + judge-gate verdicts over all of them (spec `judge_fail_rate`). */
1112
+ readonly judgeFailRate?: number;
1113
+ /** Route decisions the quality floor forced onto the floor arm, over all decisions (spec `floor_block_rate`). */
1114
+ readonly floorBlockRate?: number;
783
1115
  readonly windowMs?: number;
784
1116
  readonly mitigation: ReadonlyArray<IrSloMitigation>;
785
1117
  };
@@ -845,6 +1177,9 @@ export type IrWatchme = {
845
1177
  readonly capture: "full" | "mirrors";
846
1178
  /** Phase-2 judge model — default resolved at lower time. */
847
1179
  readonly judgeModel: string;
1180
+ /** 0.6.0 §4.2 — provenance + the judge profile's request params. */
1181
+ readonly judgeProfile?: string;
1182
+ readonly judgeParams?: IrModelParams;
848
1183
  readonly judgeSampleRate: number;
849
1184
  readonly judgeBudgetUsd: number;
850
1185
  readonly scope: "harness" | "user";
@@ -896,6 +1231,9 @@ export type IrV0 = {
896
1231
  readonly version: 0;
897
1232
  readonly name: string;
898
1233
  readonly target: "cli";
1234
+ /** 0.6.0 §4.2 — the lowered `models:` registry. Present only when the spec
1235
+ * declares one; emitters never read it. */
1236
+ readonly models?: IrModelProfiles;
899
1237
  readonly agent: {
900
1238
  readonly model: string;
901
1239
  readonly instructions: string;
@@ -922,6 +1260,13 @@ export type IrV0 = {
922
1260
  readonly modelTiers?: IrModelTiers;
923
1261
  /** Adaptive model routing — N-candidate pool. Absent → single-model. */
924
1262
  readonly modelPool?: IrModelPool;
1263
+ /** 0.6.0 §4.2 — provenance: the `models:` profile `model` resolved from.
1264
+ * Absent unless the slot was declared as a `$profile` reference. */
1265
+ readonly modelProfile?: string;
1266
+ /** 0.6.0 §4.1 — sampling temperature (spec `temperature`, or the
1267
+ * referenced profile's). Exclusive with `thinking`. Absent when neither
1268
+ * declares it. */
1269
+ readonly temperature?: number;
925
1270
  };
926
1271
  readonly tools: readonly string[];
927
1272
  readonly toolConfigs: IrToolConfigs;
@@ -1018,6 +1363,11 @@ export type IrWorkflowStep = {
1018
1363
  * PolicyRouter decides per step against the shared routing-store
1019
1364
  * scoreboard). Absent → single-model. */
1020
1365
  readonly modelPool?: IrModelPool;
1366
+ /** 0.6.0 §4.2 — provenance: the `models:` profile `model` resolved from. */
1367
+ readonly modelProfile?: string;
1368
+ /** 0.6.0 §4.1 — sampling temperature (spec `steps[].temperature`, or the
1369
+ * referenced profile's). Exclusive with `thinking`. */
1370
+ readonly temperature?: number;
1021
1371
  /** Loop contract 0.4 (Batch B, G02) — `"judge"` marks a gate step over
1022
1372
  * the previous step's output. ABSENT on regular agent steps. */
1023
1373
  readonly kind?: "judge";
@@ -1033,6 +1383,9 @@ export type IrWorkflowV0 = {
1033
1383
  readonly version: 0;
1034
1384
  readonly name: string;
1035
1385
  readonly target: "workflow";
1386
+ /** 0.6.0 §4.2 — the lowered `models:` registry. Present only when the spec
1387
+ * declares one; emitters never read it. */
1388
+ readonly models?: IrModelProfiles;
1036
1389
  readonly steps: readonly IrWorkflowStep[];
1037
1390
  readonly mcp_servers: IrMcpServers;
1038
1391
  readonly permissions: IrPermissions;
@@ -1278,6 +1631,9 @@ export type IrChannelV0 = {
1278
1631
  readonly version: 0;
1279
1632
  readonly name: string;
1280
1633
  readonly target: "channel";
1634
+ /** 0.6.0 §4.2 — the lowered `models:` registry. Present only when the spec
1635
+ * declares one; emitters never read it. */
1636
+ readonly models?: IrModelProfiles;
1281
1637
  readonly agent: {
1282
1638
  readonly model: string;
1283
1639
  readonly instructions: string;
@@ -1298,6 +1654,13 @@ export type IrChannelV0 = {
1298
1654
  readonly modelTiers?: IrModelTiers;
1299
1655
  /** Adaptive model routing — N-candidate pool. Absent → single-model. */
1300
1656
  readonly modelPool?: IrModelPool;
1657
+ /** 0.6.0 §4.2 — provenance: the `models:` profile `model` resolved from.
1658
+ * Absent unless the slot was declared as a `$profile` reference. */
1659
+ readonly modelProfile?: string;
1660
+ /** 0.6.0 §4.1 — sampling temperature (spec `temperature`, or the
1661
+ * referenced profile's). Exclusive with `thinking`. Absent when neither
1662
+ * declares it. */
1663
+ readonly temperature?: number;
1301
1664
  };
1302
1665
  readonly tools: readonly string[];
1303
1666
  readonly toolConfigs: IrToolConfigs;
@@ -1377,6 +1740,9 @@ export type IrManagedV0 = {
1377
1740
  readonly version: 0;
1378
1741
  readonly name: string;
1379
1742
  readonly target: "managed";
1743
+ /** 0.6.0 §4.2 — the lowered `models:` registry. Present only when the spec
1744
+ * declares one; emitters never read it. */
1745
+ readonly models?: IrModelProfiles;
1380
1746
  readonly agent: {
1381
1747
  readonly model: string;
1382
1748
  readonly instructions: string;
@@ -1397,6 +1763,13 @@ export type IrManagedV0 = {
1397
1763
  readonly modelTiers?: IrModelTiers;
1398
1764
  /** Adaptive model routing — N-candidate pool. Absent → single-model. */
1399
1765
  readonly modelPool?: IrModelPool;
1766
+ /** 0.6.0 §4.2 — provenance: the `models:` profile `model` resolved from.
1767
+ * Absent unless the slot was declared as a `$profile` reference. */
1768
+ readonly modelProfile?: string;
1769
+ /** 0.6.0 §4.1 — sampling temperature (spec `temperature`, or the
1770
+ * referenced profile's). Exclusive with `thinking`. Absent when neither
1771
+ * declares it. */
1772
+ readonly temperature?: number;
1400
1773
  };
1401
1774
  readonly tenants: readonly IrManagedTenant[];
1402
1775
  /** Loop contract 0.4 (Batch F, G81) — tool catalog for the managed daemon.
@@ -1473,6 +1846,19 @@ export type IrGraphNode = {
1473
1846
  readonly thinking?: IrThinking;
1474
1847
  readonly tools: readonly string[];
1475
1848
  readonly toolConfigs: IrToolConfigs;
1849
+ /** 0.6.0 §7.7 — per-node model routing (graph nodes carried NONE before
1850
+ * 0.6.0): the agent block's quartet, lowered by the same
1851
+ * `lowerModelFailover` and rendered onto the node's `runChatLoop` call
1852
+ * exactly like a workflow step. Absent → the node's single resolved model. */
1853
+ readonly modelFallbacks?: readonly string[];
1854
+ readonly circuitBreaker?: IrCircuitBreaker;
1855
+ readonly modelTiers?: IrModelTiers;
1856
+ readonly modelPool?: IrModelPool;
1857
+ /** 0.6.0 §4.2 — provenance: the `models:` profile `model` resolved from. */
1858
+ readonly modelProfile?: string;
1859
+ /** 0.6.0 §4.1 — sampling temperature (spec `nodes.<n>.temperature`, or the
1860
+ * referenced profile's). Exclusive with `thinking`. */
1861
+ readonly temperature?: number;
1476
1862
  /**
1477
1863
  * When set, the node calls `ctx.requestApproval(prompt)` BEFORE its LLM
1478
1864
  * turn and pauses the graph until `resume(checkpointId, decision)`. The
@@ -1521,6 +1907,9 @@ export type IrGraphV0 = {
1521
1907
  readonly version: 0;
1522
1908
  readonly name: string;
1523
1909
  readonly target: "graph";
1910
+ /** 0.6.0 §4.2 — the lowered `models:` registry. Present only when the spec
1911
+ * declares one; emitters never read it. */
1912
+ readonly models?: IrModelProfiles;
1524
1913
  readonly entry: string;
1525
1914
  readonly nodes: readonly IrGraphNode[];
1526
1915
  readonly edges: readonly IrGraphEdge[];
@@ -1573,11 +1962,21 @@ export type IrPipelineV0 = {
1573
1962
  readonly version: 0;
1574
1963
  readonly name: string;
1575
1964
  readonly target: "pipeline";
1965
+ /** 0.6.0 §4.2 — the lowered `models:` registry. Present only when the spec
1966
+ * declares one; emitters never read it. */
1967
+ readonly models?: IrModelProfiles;
1576
1968
  readonly agent: {
1577
1969
  readonly model: string;
1578
1970
  readonly instructions: string;
1579
1971
  /** Adaptive model routing — N-candidate pool. Absent → single-model. */
1580
1972
  readonly modelPool?: IrModelPool;
1973
+ /** 0.6.0 §4.2 — provenance: the `models:` profile `model` resolved from.
1974
+ * Absent unless the slot was declared as a `$profile` reference. */
1975
+ readonly modelProfile?: string;
1976
+ /** 0.6.0 §4.1 — sampling temperature (spec `temperature`, or the
1977
+ * referenced profile's). Exclusive with `thinking`. Absent when neither
1978
+ * declares it. */
1979
+ readonly temperature?: number;
1581
1980
  };
1582
1981
  readonly retrieve: {
1583
1982
  readonly embedderModel: string;
@@ -1639,8 +2038,19 @@ export type IrCrewRole = {
1639
2038
  readonly modelTiers?: IrModelTiers;
1640
2039
  /** Item 9 (G37) — per-role N-candidate pool with a selection policy (a
1641
2040
  * PolicyRouter decides per role against the shared routing-store
1642
- * scoreboard). Absent → single-model. */
2041
+ * scoreboard). Absent → single-model. 0.6.0 §7.7 (PR 7b): the WIDENED
2042
+ * pool — each candidate carries its per-candidate profile settings
2043
+ * (`IrModelPoolCandidate`) — is emitted verbatim onto the role literal and
2044
+ * forwarded intact by `@crewhaus/crew-orchestrator` into the role's turns,
2045
+ * which stamp `scope` with the role name when the spec pinned none (the
2046
+ * compiler leaves `scope` unstamped so pre-0.6.0 pooled roles stay
2047
+ * byte-identical). */
1643
2048
  readonly modelPool?: IrModelPool;
2049
+ /** 0.6.0 §4.2 — provenance: the `models:` profile `model` resolved from. */
2050
+ readonly modelProfile?: string;
2051
+ /** 0.6.0 §4.1 — sampling temperature (spec `roles.<r>.temperature`, or the
2052
+ * referenced profile's). Exclusive with `thinking`. */
2053
+ readonly temperature?: number;
1644
2054
  /** 0.5.0 — this role's RESOLVED Thredz config: the crew-wide defaults with
1645
2055
  * the role's own `thredz.roles.<name>` overrides merged in, shorthands
1646
2056
  * expanded and the credential lowered. Absent → this role has no hosted
@@ -1666,11 +2076,21 @@ export type IrCrewRouting = {
1666
2076
  readonly contains: string;
1667
2077
  readonly to: string;
1668
2078
  }>>>;
2079
+ /**
2080
+ * 0.6.0 §7.7 — the model the `kind: "llm"` router runs on (resolved; a
2081
+ * `$profile` reference lowers here with its provenance beside it). Absent →
2082
+ * the emitter keeps today's default, the entry role's model.
2083
+ */
2084
+ readonly model?: string;
2085
+ readonly modelProfile?: string;
1669
2086
  };
1670
2087
  export type IrCrewV0 = {
1671
2088
  readonly version: 0;
1672
2089
  readonly name: string;
1673
2090
  readonly target: "crew";
2091
+ /** 0.6.0 §4.2 — the lowered `models:` registry. Present only when the spec
2092
+ * declares one; emitters never read it. */
2093
+ readonly models?: IrModelProfiles;
1674
2094
  readonly entry: string;
1675
2095
  readonly roles: readonly IrCrewRole[];
1676
2096
  readonly routing?: IrCrewRouting;
@@ -1726,6 +2146,9 @@ export type IrResearchV0 = {
1726
2146
  readonly version: 0;
1727
2147
  readonly name: string;
1728
2148
  readonly target: "research";
2149
+ /** 0.6.0 §4.2 — the lowered `models:` registry. Present only when the spec
2150
+ * declares one; emitters never read it. */
2151
+ readonly models?: IrModelProfiles;
1729
2152
  readonly agent: {
1730
2153
  readonly model: string;
1731
2154
  readonly instructions: string;
@@ -1734,6 +2157,13 @@ export type IrResearchV0 = {
1734
2157
  readonly maxTokens?: number;
1735
2158
  /** Adaptive model routing — N-candidate pool. Absent → single-model. */
1736
2159
  readonly modelPool?: IrModelPool;
2160
+ /** 0.6.0 §4.2 — provenance: the `models:` profile `model` resolved from.
2161
+ * Absent unless the slot was declared as a `$profile` reference. */
2162
+ readonly modelProfile?: string;
2163
+ /** 0.6.0 §4.1 — sampling temperature (spec `temperature`, or the
2164
+ * referenced profile's). Exclusive with `thinking`. Absent when neither
2165
+ * declares it. */
2166
+ readonly temperature?: number;
1737
2167
  };
1738
2168
  /** Default research goal. The daemon's `--goal "..."` flag overrides. */
1739
2169
  readonly goal: string;
@@ -1792,6 +2222,9 @@ export type IrBatchV0 = {
1792
2222
  readonly version: 0;
1793
2223
  readonly name: string;
1794
2224
  readonly target: "batch";
2225
+ /** 0.6.0 §4.2 — the lowered `models:` registry. Present only when the spec
2226
+ * declares one; emitters never read it. */
2227
+ readonly models?: IrModelProfiles;
1795
2228
  readonly agent: {
1796
2229
  readonly model: string;
1797
2230
  readonly instructions: string;
@@ -1800,6 +2233,13 @@ export type IrBatchV0 = {
1800
2233
  readonly maxTokens?: number;
1801
2234
  /** Adaptive model routing — N-candidate pool. Absent → single-model. */
1802
2235
  readonly modelPool?: IrModelPool;
2236
+ /** 0.6.0 §4.2 — provenance: the `models:` profile `model` resolved from.
2237
+ * Absent unless the slot was declared as a `$profile` reference. */
2238
+ readonly modelProfile?: string;
2239
+ /** 0.6.0 §4.1 — sampling temperature (spec `temperature`, or the
2240
+ * referenced profile's). Exclusive with `thinking`. Absent when neither
2241
+ * declares it. */
2242
+ readonly temperature?: number;
1803
2243
  };
1804
2244
  readonly queue: {
1805
2245
  readonly adapter: IrBatchQueueAdapter;
@@ -1852,9 +2292,15 @@ export type IrVoiceV0 = {
1852
2292
  readonly version: 0;
1853
2293
  readonly name: string;
1854
2294
  readonly target: "voice";
2295
+ /** 0.6.0 §4.2 — the lowered `models:` registry. Present only when the spec
2296
+ * declares one; emitters never read it. */
2297
+ readonly models?: IrModelProfiles;
1855
2298
  readonly agent: {
1856
2299
  readonly model: string;
1857
2300
  readonly instructions: string;
2301
+ /** 0.6.0 §4.2 — provenance: the `models:` profile `model` resolved from
2302
+ * (profile → model only on this shape; every other field is warned). */
2303
+ readonly modelProfile?: string;
1858
2304
  };
1859
2305
  readonly voice: {
1860
2306
  readonly provider: IrVoiceProvider;
@@ -1893,6 +2339,9 @@ export type IrBrowserV0 = {
1893
2339
  readonly version: 0;
1894
2340
  readonly name: string;
1895
2341
  readonly target: "browser";
2342
+ /** 0.6.0 §4.2 — the lowered `models:` registry. Present only when the spec
2343
+ * declares one; emitters never read it. */
2344
+ readonly models?: IrModelProfiles;
1896
2345
  readonly agent: {
1897
2346
  readonly model: string;
1898
2347
  readonly instructions: string;
@@ -1901,6 +2350,13 @@ export type IrBrowserV0 = {
1901
2350
  readonly maxTokens?: number;
1902
2351
  /** Adaptive model routing — N-candidate pool. Absent → single-model. */
1903
2352
  readonly modelPool?: IrModelPool;
2353
+ /** 0.6.0 §4.2 — provenance: the `models:` profile `model` resolved from.
2354
+ * Absent unless the slot was declared as a `$profile` reference. */
2355
+ readonly modelProfile?: string;
2356
+ /** 0.6.0 §4.1 — sampling temperature (spec `temperature`, or the
2357
+ * referenced profile's). Exclusive with `thinking`. Absent when neither
2358
+ * declares it. */
2359
+ readonly temperature?: number;
1904
2360
  };
1905
2361
  readonly driver: {
1906
2362
  readonly backend: IrBrowserBackend;
@@ -1921,6 +2377,11 @@ export type IrBrowserV0 = {
1921
2377
  };
1922
2378
  /** Vision-grounding model. Defaults at lower-time to agent.model. */
1923
2379
  readonly groundingModel: string;
2380
+ /** 0.6.0 §4.2 — provenance when `groundingModel` resolved through a profile. */
2381
+ readonly groundingModelProfile?: string;
2382
+ /** 0.6.0 §4.2 — the grounding profile's pinned request params, honoured by
2383
+ * `@crewhaus/tool-vision-grounding`'s `FindElement` call. */
2384
+ readonly groundingParams?: IrModelParams;
1924
2385
  readonly tools: readonly string[];
1925
2386
  readonly toolConfigs: IrToolConfigs;
1926
2387
  readonly mcp_servers: IrMcpServers;
@@ -1949,10 +2410,15 @@ export type IrEvalV0 = {
1949
2410
  readonly version: 0;
1950
2411
  readonly name: string;
1951
2412
  readonly target: "eval";
2413
+ /** 0.6.0 §4.2 — the lowered `models:` registry. Present only when the spec
2414
+ * declares one; emitters never read it. */
2415
+ readonly models?: IrModelProfiles;
1952
2416
  readonly agent: {
1953
2417
  readonly model: string;
1954
2418
  readonly instructions: string;
1955
2419
  readonly tools: readonly string[];
2420
+ /** 0.6.0 §4.2 — provenance: the `models:` profile `model` resolved from. */
2421
+ readonly modelProfile?: string;
1956
2422
  };
1957
2423
  readonly dataset: {
1958
2424
  readonly name: string;
@@ -2012,9 +2478,14 @@ export type IrChainV0 = {
2012
2478
  readonly version: 0;
2013
2479
  readonly name: string;
2014
2480
  readonly target: "onchain";
2481
+ /** 0.6.0 §4.2 — the lowered `models:` registry. Present only when the spec
2482
+ * declares one; emitters never read it. */
2483
+ readonly models?: IrModelProfiles;
2015
2484
  readonly agent: {
2016
2485
  readonly model: string;
2017
2486
  readonly instructions: string;
2487
+ /** 0.6.0 §4.2 — provenance: the `models:` profile `model` resolved from. */
2488
+ readonly modelProfile?: string;
2018
2489
  };
2019
2490
  readonly chains: readonly IrChainBinding[];
2020
2491
  readonly wallets: readonly IrWalletBinding[];
@@ -2046,9 +2517,14 @@ export type IrChainGameV0 = {
2046
2517
  readonly version: 0;
2047
2518
  readonly name: string;
2048
2519
  readonly target: "onchain-game";
2520
+ /** 0.6.0 §4.2 — the lowered `models:` registry. Present only when the spec
2521
+ * declares one; emitters never read it. */
2522
+ readonly models?: IrModelProfiles;
2049
2523
  readonly agent: {
2050
2524
  readonly model: string;
2051
2525
  readonly instructions: string;
2526
+ /** 0.6.0 §4.2 — provenance: the `models:` profile `model` resolved from. */
2527
+ readonly modelProfile?: string;
2052
2528
  };
2053
2529
  /** Games are bound to one chain at a time; multi-chain games are rare. */
2054
2530
  readonly chain: IrChainBinding;
package/dist/loop.js CHANGED
@@ -73,6 +73,12 @@ function describeJudge(j) {
73
73
  function describeModelPool(pool) {
74
74
  return `adaptive model pool (${pool.candidates.length} candidates, policy: ${pool.policy})`;
75
75
  }
76
+ /** 0.6.0 §4.3 — "2 model profiles (fast, strong) · primary profile: fast". */
77
+ function describeModelProfiles(models, primaryProfile) {
78
+ const names = Object.keys(models);
79
+ const summary = countList(names.length, "model profile", names);
80
+ return primaryProfile !== undefined ? `${summary} · primary profile: ${primaryProfile}` : summary;
81
+ }
76
82
  function ringSegmentsFromView(view) {
77
83
  const tools = view.tools ?? [];
78
84
  const toolsKey = view.toolsKey ?? "tools";
@@ -106,6 +112,14 @@ function ringSegmentsFromView(view) {
106
112
  reasonKeys.push("agent.model_pool");
107
113
  reasonParts.push(describeModelPool(view.modelPool));
108
114
  }
115
+ // 0.6.0 §4.3 — the `models:` registry surfaces as `models.<name>` keys (the
116
+ // YAML the operator jumps to), and the primary's profile provenance names
117
+ // the profile it resolved from.
118
+ if (view.models !== undefined) {
119
+ for (const name of Object.keys(view.models))
120
+ reasonKeys.push(`models.${name}`);
121
+ reasonParts.push(describeModelProfiles(view.models, view.modelProfile));
122
+ }
109
123
  const reason = segment("reason", reasonKeys, `${reasonParts.join(" · ")} on ${view.model}`, `single fixed model (${view.model}) — no thinking, tiers, or pool`);
110
124
  // act ← tools / mcp_servers / sub_agents.
111
125
  const actKeys = [];
@@ -176,9 +190,12 @@ function ringSegmentsFromView(view) {
176
190
  stopKeys.push("budget");
177
191
  const usd = view.budget.usdMicros / 1_000_000;
178
192
  const onExceed = view.budget.onExceed.kind === "degrade"
179
- ? ` (on exceed: degrade → ${view.budget.onExceed.model})`
180
- : " (on exceed: stop)";
181
- stopParts.push(`budget $${usd}${onExceed}`);
193
+ ? `on exceed: degrade → ${view.budget.onExceed.model}`
194
+ : "on exceed: stop";
195
+ // 0.6.0 — `scope` is only carried when declared, so the rendered summary
196
+ // is byte-identical for every pre-0.6.0 IR.
197
+ const scope = view.budget.scope !== undefined ? `; scope: ${view.budget.scope}` : "";
198
+ stopParts.push(`budget $${usd} (${onExceed}${scope})`);
182
199
  }
183
200
  if (view.limits !== undefined) {
184
201
  stopKeys.push("limits");
@@ -228,6 +245,10 @@ function miniSegments(view) {
228
245
  reasonKeys.push("model");
229
246
  reasonParts.push(`model: ${view.model}`);
230
247
  }
248
+ if (view.modelProfile !== undefined) {
249
+ reasonKeys.push(`models.${view.modelProfile}`);
250
+ reasonParts.push(`profile: ${view.modelProfile}`);
251
+ }
231
252
  if (view.thinking !== undefined) {
232
253
  reasonKeys.push("thinking");
233
254
  reasonParts.push("extended thinking");
@@ -283,6 +304,7 @@ function workflowCanvas(ir, warnings) {
283
304
  ? { model: step.model, ...(step.judge !== undefined ? { judge: step.judge } : {}) }
284
305
  : {
285
306
  model: step.model,
307
+ ...(step.modelProfile !== undefined ? { modelProfile: step.modelProfile } : {}),
286
308
  ...(step.thinking !== undefined ? { thinking: step.thinking } : {}),
287
309
  // Item 9 (G37) — per-step model pool surfaces in the reason segment.
288
310
  ...(step.modelPool !== undefined ? { modelPool: step.modelPool } : {}),
@@ -337,7 +359,10 @@ function graphCanvas(ir, warnings) {
337
359
  ? { model: node.model, ...(node.judge !== undefined ? { judge: node.judge } : {}) }
338
360
  : {
339
361
  model: node.model,
362
+ ...(node.modelProfile !== undefined ? { modelProfile: node.modelProfile } : {}),
340
363
  ...(node.thinking !== undefined ? { thinking: node.thinking } : {}),
364
+ // 0.6.0 §7.7 — per-node model pool surfaces in the reason segment.
365
+ ...(node.modelPool !== undefined ? { modelPool: node.modelPool } : {}),
341
366
  tools: node.tools,
342
367
  ...(node.hitlPrompt !== undefined ? { hitlPrompt: node.hitlPrompt } : {}),
343
368
  }),
@@ -380,6 +405,7 @@ function crewCanvas(ir, warnings) {
380
405
  kind: "role",
381
406
  mini: miniSegments({
382
407
  model: role.model,
408
+ ...(role.modelProfile !== undefined ? { modelProfile: role.modelProfile } : {}),
383
409
  ...(role.thinking !== undefined ? { thinking: role.thinking } : {}),
384
410
  // Item 9 (G37) — per-role model pool surfaces in the reason segment.
385
411
  ...(role.modelPool !== undefined ? { modelPool: role.modelPool } : {}),
@@ -496,6 +522,8 @@ function cliRingView(ir) {
496
522
  ...(ir.agent.thinking !== undefined ? { thinking: ir.agent.thinking } : {}),
497
523
  ...(ir.agent.modelTiers !== undefined ? { modelTiers: ir.agent.modelTiers } : {}),
498
524
  ...(ir.agent.modelPool !== undefined ? { modelPool: ir.agent.modelPool } : {}),
525
+ ...(ir.models !== undefined ? { models: ir.models } : {}),
526
+ ...(ir.agent.modelProfile !== undefined ? { modelProfile: ir.agent.modelProfile } : {}),
499
527
  ...(ir.evaluation !== undefined ? { evaluation: ir.evaluation } : {}),
500
528
  ...(ir.learning !== undefined ? { learning: ir.learning } : {}),
501
529
  ...(ir.security !== undefined ? { security: ir.security } : {}),
@@ -524,6 +552,8 @@ function channelRingView(ir) {
524
552
  ...(ir.agent.thinking !== undefined ? { thinking: ir.agent.thinking } : {}),
525
553
  ...(ir.agent.modelTiers !== undefined ? { modelTiers: ir.agent.modelTiers } : {}),
526
554
  ...(ir.agent.modelPool !== undefined ? { modelPool: ir.agent.modelPool } : {}),
555
+ ...(ir.models !== undefined ? { models: ir.models } : {}),
556
+ ...(ir.agent.modelProfile !== undefined ? { modelProfile: ir.agent.modelProfile } : {}),
527
557
  ...(ir.evaluation !== undefined ? { evaluation: ir.evaluation } : {}),
528
558
  ...(ir.learning !== undefined ? { learning: ir.learning } : {}),
529
559
  ...(ir.memory !== undefined ? { memory: ir.memory } : {}),
@@ -544,6 +574,8 @@ function managedRingView(ir) {
544
574
  ...(ir.agent.thinking !== undefined ? { thinking: ir.agent.thinking } : {}),
545
575
  ...(ir.agent.modelTiers !== undefined ? { modelTiers: ir.agent.modelTiers } : {}),
546
576
  ...(ir.agent.modelPool !== undefined ? { modelPool: ir.agent.modelPool } : {}),
577
+ ...(ir.models !== undefined ? { models: ir.models } : {}),
578
+ ...(ir.agent.modelProfile !== undefined ? { modelProfile: ir.agent.modelProfile } : {}),
547
579
  ...(ir.evaluation !== undefined ? { evaluation: ir.evaluation } : {}),
548
580
  ...(ir.learning !== undefined ? { learning: ir.learning } : {}),
549
581
  ...(ir.memory !== undefined ? { memory: ir.memory } : {}),
package/dist/readme.js CHANGED
@@ -179,18 +179,101 @@ function collectConfiguredToolNames(ir) {
179
179
  visit(ir);
180
180
  return configured;
181
181
  }
182
- /** Deduped models across variant shapes (agent / steps / nodes / roles). */
182
+ /**
183
+ * Deduped models across variant shapes (agent / steps / nodes / roles), in
184
+ * declaration order: the primary slot(s) first, then — 0.6.0 §4.3 — every
185
+ * other model a run can route a call to: pool candidates, the two tiers,
186
+ * failover chains, and the auxiliary slots (compaction, the in-loop judge or
187
+ * judge panel, the budget degrade rung, the security judge, the watchme
188
+ * judge). The README used to list only the primary slot(s), so a pooled or
189
+ * judged harness under-reported the credentials it needs. A registry
190
+ * profile's model appears once like any other string; the profile names
191
+ * themselves render in the "Model profiles" section.
192
+ */
183
193
  function collectModels(ir) {
194
+ const out = [];
195
+ const add = (model) => {
196
+ if (model !== undefined && model.length > 0 && !out.includes(model))
197
+ out.push(model);
198
+ };
199
+ const addRouted = (block) => {
200
+ add(block.model);
201
+ for (const c of block.modelPool?.candidates ?? []) {
202
+ add(c.model);
203
+ for (const f of c.fallbacks ?? [])
204
+ add(f);
205
+ }
206
+ if (block.modelTiers !== undefined) {
207
+ add(block.modelTiers.fast);
208
+ add(block.modelTiers.default);
209
+ }
210
+ for (const f of block.modelFallbacks ?? [])
211
+ add(f);
212
+ };
184
213
  switch (ir.target) {
185
214
  case "workflow":
186
- return [...new Set(ir.steps.map((s) => s.model))];
215
+ for (const s of ir.steps) {
216
+ addRouted(s);
217
+ for (const j of s.judge?.judges ?? [])
218
+ add(j);
219
+ }
220
+ break;
187
221
  case "graph":
188
- return [...new Set(ir.nodes.map((n) => n.model))];
222
+ for (const n of ir.nodes) {
223
+ addRouted(n);
224
+ for (const j of n.judge?.judges ?? [])
225
+ add(j);
226
+ }
227
+ break;
189
228
  case "crew":
190
- return [...new Set(ir.roles.map((r) => r.model))];
229
+ for (const r of ir.roles) {
230
+ addRouted(r);
231
+ for (const sa of r.subAgents)
232
+ if (sa.model !== undefined)
233
+ addRouted({ ...sa, model: sa.model });
234
+ }
235
+ add(ir.routing?.model);
236
+ break;
191
237
  default:
192
- return [ir.agent.model];
238
+ addRouted(ir.agent);
239
+ break;
240
+ }
241
+ const aux = ir;
242
+ for (const sa of aux.subAgents ?? []) {
243
+ if (sa.model !== undefined)
244
+ addRouted({ ...sa, model: sa.model });
193
245
  }
246
+ add(aux.compaction?.model);
247
+ add(aux.evaluation?.grader.model);
248
+ for (const j of aux.evaluation?.grader.judges ?? [])
249
+ add(j);
250
+ add(aux.budget?.onExceed.model);
251
+ add(aux.security?.justification?.model);
252
+ add(aux.watchme?.judgeModel);
253
+ add(aux.groundingModel);
254
+ return out;
255
+ }
256
+ /**
257
+ * 0.6.0 §4.3 — the "Model profiles" section: one row per `models:` profile
258
+ * with its model, tags and the settings it pins. Rendered only when the IR
259
+ * carries a registry, so profile-less bundles keep the same README.
260
+ */
261
+ function renderModelProfilesSection(ir) {
262
+ const registry = ir.models;
263
+ if (registry === undefined)
264
+ return undefined;
265
+ const names = Object.keys(registry).sort();
266
+ if (names.length === 0)
267
+ return undefined;
268
+ const rows = names.map((name) => {
269
+ const profile = registry[name];
270
+ const tags = (profile.tags ?? []).map((t) => `\`${escapeCell(t)}\``).join(", ") || "—";
271
+ const pinned = Object.keys(profile)
272
+ .filter((k) => k !== "profile" && k !== "model" && k !== "tags")
273
+ .sort();
274
+ return `| \`${escapeCell(name)}\` | \`${escapeCell(profile.model)}\` | ${tags} | ${pinned.length > 0 ? pinned.map((k) => `\`${k}\``).join(", ") : "—"} |`;
275
+ });
276
+ return ["| Profile | Model | Tags | Pins |", "| --- | --- | --- | --- |", ...rows].join("\n");
194
277
  }
195
278
  /**
196
279
  * Adversarial-review F5 — escape an interpolated value for a GFM table
@@ -375,6 +458,10 @@ export function renderBundleReadme(ir, opts = {}) {
375
458
  .join(", ")} |`,
376
459
  ].join("\n");
377
460
  const sections = [{ heading: "Harness", body: harnessRows }];
461
+ // 0.6.0 §4.3 — the `models:` registry, when the spec declared one.
462
+ const profiles = renderModelProfilesSection(ir);
463
+ if (profiles !== undefined)
464
+ sections.push({ heading: "Model profiles", body: profiles });
378
465
  const tools = renderToolsSection(ir);
379
466
  if (tools !== undefined)
380
467
  sections.push({ heading: "Tools", body: tools });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewhaus/ir",
3
- "version": "0.5.7",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Canonical typed intermediate representation",
6
6
  "main": "dist/index.js",