@sema-agent/core 7.0.0 → 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 7.0.1 — 2026-08-30
4
+
5
+ ### Added
6
+ - **`task_progress.model` + `BackgroundChildEvent.model`** (#508①, cli [5725] item-B): every spawn-progress tick and background child event now carries the SERVED model id (PREPARED semantics — the id the child's prepare actually resolved, not the word the caller wrote). The fleet row publishes the same served id, so row/tick/card lanes state one fact; a replayed workflow leg reports the cached model on every face.
7
+ - **`apiFailure` on the completed-agent details card and both notification lanes** (#506 residual): terminal API-failure evidence now reaches every reader of a settled child, not only the host-face `TaskResult`.
8
+
9
+ ### Changed — narrowing (named)
10
+ - **Unknown/empty delegation `model` argument: soft note → typed reject** (#508②, CC $L closed-set anchored): `model.unknown` (echoes the accepted catalog) and `model.empty` replace the old accept-with-note behavior. A caller that previously leaned on the note-and-serve-default path now gets a refusal naming the legal values. Positive control: every cataloged id serves exactly as before. **Two lanes are exempt** (pre-ship rescan, the caller-census duty): the tier-3 revival lane feeds the durable row's MACHINE-minted recorded word back through this gate, so it keeps the soft-degrade (loud `modelNote` + inherited model — a catalog rotation must not permanently strand a named teammate and its queued mail), and its fallback chain skips string candidates the live catalog cannot resolve (a degrade that re-selects the retired word would revive a child dead on arrival); `subagent_type: "fork"` keeps the schema's own "model is ignored" promise for the empty string exactly as for any other word. Membership itself hardened: the catalog read is own-property (`Object.hasOwn`), so `Object.prototype` words (`constructor`, `toString`, …) refuse as `model.unknown` instead of passing the gate and crashing the child.
11
+ - **Per-call model binding is resolved AT the gate** (rescan): a judged per-call word now rides the child spec as the resolved `Model` object, so the judged roster is the authoritative binding — an explicit `opts.models` table naming a key the runner's own catalog lacks now serves on the judged binding instead of passing the gate and dying inside the child's prepare. `BackgroundChildEvent.model` / roster-row docs restated to the served-id-first rule (display label is the unresolvable-ref fallback only); the residual one-generation lag window for definition/tool-level string refs after `swapModels` is documented, not claimed away. The background roster record gains optional `modelFallback` (additive; fork lane already carried it).
12
+ - **File-history mode domain, both doors** (security): the import door refuses a version-row mode that is not an integer in [0, 0o7777] and refuses set-ID modes (setuid/setgid) BY NAME; on replay, `applyRecordedMode` STRIPS the set-ID bits and applies the remainder (`mode & 0o1777`, sticky preserved) under the loud `modeNotPreserved` account — set-ID bits never reach chmod (capture may legitimately record one; replay is where the escalation lives). A file already at exactly the recorded mode answers `unchanged` with no account (the equality fast path precedes the escalation arm), and `previewFileDelta` compares against what replay would actually apply, so a set-ID-recorded drift converges instead of being promised forever.
13
+
14
+ ### Changed — widening (named)
15
+ - **The delegation tool's model roster is read at JUDGEMENT time, not frozen at mount** (#508 residual): after `Runner.swapModels`, a long-lived mount no longer hard-rejects the models the deployment just started serving (the old refusal echoed the RETIRED generation as the accepted values). Explicit `opts.models` keeps snapshot semantics; only the runner-derived roster re-reads live. The schema/announce projection remains a mount-time snapshot — the refusal therefore carries its own accepted-values echo.
16
+
17
+ ### Fixed
18
+ - Flaky-guard ledger: the three new pin files registered as bounded effect-polling (gate was red since the #508 batch landed).
19
+
20
+ ### Docs
21
+ - #509 core half: the subagent transcript LANDING law (three store shapes + the CC 2.1.250 unconditional-landing anchor) spelled out at `SubagentToolOptions.runner`, the background placement mint gate, and `resolveSubagentTranscriptTier`; reproduced-and-located, wiring fix owned by the delegation runner's store choice.
22
+ - 6.0.0 release-criteria acceptance corrections (test [5719]/[5721]): G1.a added-count 65→77, F7 absent-seat claim restated.
23
+
3
24
  ## 7.0.0 — 2026-08-30
4
25
 
5
26
  > Ships as its own major immediately after 6.0.0 (ruled: the three-dimensional rule scope travels alone so downstream adapts to it in isolation). The 6.0.0 artifact is this same tree with the design/382 P1 surface excised whole (type-driven surgery in an isolated worktree, tag v6.0.0); `npm run handoff:diff v6.0.0 v7.0.0` reproduces exactly this surface.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The HANDLING contract shared by every tool that launches background work and hands the model a
3
+ * receipt: the Agent tool's async-launch card and `run_workflow`'s backgrounded launch receipt.
4
+ *
5
+ * Why a module rather than two copies. Both receipts state the same two things — "these bytes are
6
+ * plumbing, do not paste them at the user" and "say what you launched in your own words" — and they
7
+ * had drifted into two wordings of it (`; ` vs ` — `, "this result" vs "this tool result"). Neither
8
+ * difference carried meaning; each was just the copy that happened to be typed second. A model reading
9
+ * one receipt and a model reading the other should be reading the same instruction, and a later edit
10
+ * to one of them should not be able to leave the other behind.
11
+ *
12
+ * What deliberately stays PER-LANE (the earlier ruling, narrowed rather than reversed): each receipt
13
+ * hands back different artifacts, so each names its own. The Agent card names `task_id`; the workflow
14
+ * receipt names the ids plus the host filesystem `scriptPath` it may have written. That enumeration is
15
+ * a fact about the lane and cannot be centralized without one of them lying. So the SCOPE clause is the
16
+ * caller's, and the sentence around it is this module's. Likewise the envelopes stay as they were —
17
+ * the Agent lane renders prose, the workflow lane a JSON field — which is why these are string parts
18
+ * rather than a rendered receipt.
19
+ */
20
+ /**
21
+ * The no-quote clause for one lane. `artifactScope` is the caller's own enumeration of what "any part
22
+ * of it" covers, spliced in verbatim INCLUDING its leading separator — a comma clause
23
+ * (`", including the task_id below,"`) and a parenthetical (`" (the ids above, and scriptPath when
24
+ * present)"`) punctuate differently and the caller owns which it uses. Pass `""` for a lane with
25
+ * nothing to enumerate; the sentence still reads.
26
+ */
27
+ export declare function launchReceiptNoQuoteClause(artifactScope: string): string;
28
+ /**
29
+ * The closing instruction, whole and identical on every launch lane: report the launch in the model's
30
+ * own words instead of echoing the receipt. Callers append their own lane-specific continuation (how
31
+ * the result will or will not arrive) after a single space; this clause is a complete sentence on its
32
+ * own so a lane with nothing to add can use it unchanged.
33
+ */
34
+ export declare const LAUNCH_RECEIPT_OWN_WORDS_CLAUSE = "In your own words, briefly tell the user what you launched \u2014 do not echo this tool result.";
@@ -0,0 +1,5 @@
1
+ const NO_QUOTE_HEAD = "This tool result is internal metadata — never quote or paste any part of it";
2
+ export function launchReceiptNoQuoteClause(artifactScope) {
3
+ return `${NO_QUOTE_HEAD}${artifactScope} into a user-facing reply.`;
4
+ }
5
+ export const LAUNCH_RECEIPT_OWN_WORDS_CLAUSE = "In your own words, briefly tell the user what you launched — do not echo this tool result.";
@@ -4,6 +4,60 @@ import { type ExecutionEnv } from "../internal/harness.js";
4
4
  import type { RunInternals } from "../core/runner/prepare-task.js";
5
5
  import type { TaskNotificationPayload } from "../core/task-notification.js";
6
6
  import { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX } from "../config/defaults.js";
7
+ /**
8
+ * #505 residual — the two GRANULARITY facts a delegation's terminal `usage` was missing beside
9
+ * tokens/turns/cost, closing a real reporting asymmetry rather than adding decoration: the workflow
10
+ * lane's completion notification has published `tool_uses` (summed `stats.toolCalls`) and `duration_ms`
11
+ * (wall clock) since it was written, and the Agent tool's own SYNC report publishes both in its
12
+ * `<usage>` footer — only the delegation TERMINAL faces (the background/fork/revive notification and its
13
+ * observer twin) reported neither, so the same child answered "how much work, how long" on one lane and
14
+ * refused on the other.
15
+ *
16
+ * PROVENANCE is the workflow lane's — `stats.toolCalls`, the RUN's own count — because that is the
17
+ * quantity the other side of the comparison publishes, and because it is available on every terminal
18
+ * lane, including the revive lane, which has no parent-side forwarding observer at all.
19
+ *
20
+ * WHAT THIS DOES NOT CLOSE, stated because the honest scope is narrower than "one name, one meaning":
21
+ * the Agent tool's SYNC `<usage>` footer keeps its own `tool_uses`, counted by the parent-side
22
+ * `toolStatsCounter`, and the two numbers differ in BOTH directions for the same child — that counter
23
+ * EXCLUDES the delegation tool's own calls and INCLUDES descendants' (CC's nested fold), while
24
+ * `stats.toolCalls` counts the direct child's calls and counts its delegations among them. A child that
25
+ * itself delegates therefore reports two different `tool_uses` depending on whether it was run
26
+ * synchronously or in the background. That footer is a byte-pinned CC-anchored face; reconciling the
27
+ * two is a separate ruling, and until it is made this comment is the place the divergence is written
28
+ * down rather than a claim that it is gone.
29
+ *
30
+ * Knownness follows `delegatedCostField`'s rule exactly: `toolCalls` is optional on the stats face
31
+ * ("undefined means the gateway reported no usage", and a checkpointed resume folds it per-leg), so the
32
+ * key is ABSENT rather than zeroed — a fabricated 0 reads as "ran no tools", which is a claim, not a
33
+ * gap. Duration is always knowable (the caller holds the lane's launch instant), so it is unconditional.
34
+ *
35
+ * TWO SPELLINGS, one meaning, matching each face's established vocabulary: the observer
36
+ * ({@link import("../core/types.js").BackgroundChildEvent}) `usage` face is camelCase and already
37
+ * DECLARES `toolUses`/`durationMs`; the model-facing `TaskNotificationPayload.usage` is rendered into
38
+ * `<usage>` as JSON where the workflow lane and the Agent sync footer both spell them snake_case. No new
39
+ * word is minted on either side.
40
+ *
41
+ * Exported at MODULE level (not from the package index — the same posture as `completedAgentCard` and
42
+ * `classifySubagentError`, and for the same reason): the ABSENT arm is the point of the knownness rule
43
+ * and it is not reachable through a real child. This engine's runner initializes `stats.toolCalls` to a
44
+ * number on every path, so only a `TaskResult` from elsewhere — a foreign runner, an older checkpoint —
45
+ * can carry the optional-and-missing shape the stats face declares. A pin that only ever sees a live
46
+ * child cannot tell "absent" apart from "zero", which is precisely the confusion being prevented.
47
+ */
48
+ export declare function delegatedGranularityFields(stats: {
49
+ toolCalls?: number;
50
+ }, elapsedMs: number): {
51
+ toolUses?: number;
52
+ durationMs: number;
53
+ };
54
+ /** {@link delegatedGranularityFields}'s snake_case twin for the model-facing notification `usage`. */
55
+ export declare function delegatedGranularityFieldsWire(stats: {
56
+ toolCalls?: number;
57
+ }, elapsedMs: number): {
58
+ tool_uses?: number;
59
+ duration_ms: number;
60
+ };
7
61
  export { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX };
8
62
  import { SubagentRetainLedger } from "./retain-ledger.js";
9
63
  import { type PeerSelfRef } from "./peer-admission.js";
@@ -268,6 +322,17 @@ export declare function completedAgentCard(child: {
268
322
  blockedReason?: string;
269
323
  errorMessage?: string;
270
324
  errorCode?: string;
325
+ /** The child run's provider-boundary fault assertion, verbatim from its `TaskResult.apiFailure`.
326
+ * PRESENCE is the claim ("this child's terminal failure came from the transport/provider, not from
327
+ * this deployment refusing to send, a limit, or unusable model output"); the members are whatever
328
+ * the failing attempt stated about itself (`status` when the provider answered with one, `requestId`
329
+ * when it named one). An EMPTY object is therefore meaningful and must not be normalized away: it
330
+ * says "provider fault, unlabelled", which is a different statement from absence ("not a provider
331
+ * fault"). Absent on every non-provider terminal and on every completed child. */
332
+ apiFailure?: {
333
+ status?: number;
334
+ requestId?: string;
335
+ };
271
336
  retryAfterMs?: number;
272
337
  degraded?: unknown;
273
338
  structuredOutput?: unknown;
@@ -514,9 +579,61 @@ export declare function delegationEntryLedgerFootprint(registry: object): {
514
579
  * catalog) after mounting must REBUILD the ToolSpec — call `createSubagentTool` again with the fresh
515
580
  * roster and serve the new tool — for the change to take effect; already-running tasks keep the roster
516
581
  * they were prepared with.
582
+ *
583
+ * The MODEL roster ({@link SubagentToolOptions.models}) deliberately does NOT follow that rule when it
584
+ * is left to the runner: a hot-swapped catalog is a first-class runner verb (`Runner.swapModels`) with
585
+ * no rebuild step, so the tool re-reads it at each judgement instead of requiring a remount. Only the
586
+ * schema/announce projection of it stays a mount-time snapshot (cache-prefix stability). Passing
587
+ * `models` explicitly opts back into snapshot semantics for that table — see the field.
517
588
  */
518
589
  export interface SubagentToolOptions {
519
- /** Runner used to execute child tasks. */
590
+ /**
591
+ * Runner used to execute child tasks.
592
+ *
593
+ * **This runner's session store decides where a child TRANSCRIPT lives — and therefore whether one
594
+ * exists at all after the fact.** A SYNC or BACKGROUND child mints its session through
595
+ * `runner.sessions`, never through the parent leg's store, so a deployment that mounts the Agent
596
+ * tool on a DIFFERENT runner than the one serving root tasks has split the transcript lane away
597
+ * from the session lane. (The FORK lane is the deliberate exception and shows the split is a known
598
+ * shape: a fork branches the CALLER's session through `ctx.hostSessionFork` — the host Runner's
599
+ * own store — precisely because `runner` may be a split child-execution runner whose store never
600
+ * held the source. A fork child therefore follows the HOST store's fate, an ordinary child this
601
+ * runner's, and the same deployment can land one on disk and lose the other.) Three shapes for the
602
+ * ordinary lanes, all reachable and all silent:
603
+ * - durable store on THIS runner whose `release` PRESERVES history (`evict:"forget"`, the
604
+ * `FileStorageBackend` default), no {@link SubagentToolOptions.background}.`agentStore` — the
605
+ * child transcript lands UNPLACED and survives the settle leg's eager release (a cache drop),
606
+ * so post-hoc forensics work even though nothing can ADDRESS it through an `a*` handle. The
607
+ * qualifier is load-bearing, not decorative: durability and release semantics are INDEPENDENT
608
+ * declarations, and a store that declares itself durable while deleting on release
609
+ * (`new FileStorageBackend({ evict: "delete" })`, or any custom store with the same policy)
610
+ * destroys the transcript at settle — the settle leg calls `release`, and there is no placement
611
+ * to make the deletion refuse. "Durable" is a statement about restarts, never about release;
612
+ * - durable store + `agentStore` — the placed partition (see the mint gate in the background
613
+ * lane): addressable across a restart, list-excluded, joint-reaped with its row. This arm is
614
+ * also the one immune to the evict policy above (the settle leg keeps a confirmed row's
615
+ * transcript instead of releasing it);
616
+ * - transient/process-local store on THIS runner (e.g. a throwaway sub-task store, or a routing
617
+ * store that sends ordinary child acquires to a private cache) — the child transcript NEVER
618
+ * reaches disk whatever the root leg's store is. Nothing in the engine compensates: the
619
+ * completion notification's recent steps and the in-memory registry are the only readers, and
620
+ * both die with the process.
621
+ * The asymmetry this produces is worth naming, because it reads as a bug from the outside: the
622
+ * `run_workflow` tool is mounted by the ENGINE on the run's own runner (prepare-task's
623
+ * `runnerSelf`), so a workflow node's child session always follows the ROOT store, while this tool
624
+ * is mounted by the DEPLOYMENT and follows whatever runner it was handed. One data root can
625
+ * therefore hold every workflow node's transcript and none of the Agent tool's.
626
+ * There is deliberately NO manifest field that answers "does a child transcript reach disk here":
627
+ * `WiringManifest.fleet.subagentTranscripts` is the ADDRESSABILITY tier and folds to `"none"` on
628
+ * the absent-row-store arm before it ever looks at a session store, so it cannot separate the
629
+ * first shape above from the third (the landing pins carry that as an explicit arm). The facts
630
+ * that DO answer it are this runner's store's own declarations — `sessions.durability` and its
631
+ * release/evict policy — read together, which is why they are named here rather than folded into
632
+ * a word. Upstream anchor (CC 2.1.250): a sub-agent transcript is written unconditionally to
633
+ * its own file beside the parent session (`projects/<projectKey>/<sessionId>/subagents/agent-<id>
634
+ * .jsonl` + a `.meta.json` sidecar), so "the child conversation is readable afterwards" is a
635
+ * property CC never makes conditional on a store tier.
636
+ */
520
637
  runner: Runner;
521
638
  /**
522
639
  * design/115 P3 — background sub-agents (CC `run_in_background`). When set, the tool exposes the
@@ -599,7 +716,22 @@ export interface SubagentToolOptions {
599
716
  * wins — fork-shadowing precedent).
600
717
  */
601
718
  builtinAgents?: boolean;
602
- /** Model catalog, used ONLY to validate an agent's string `model` ref at assembly time (fail-fast). */
719
+ /**
720
+ * Model catalog the tool validates model words against — an agent definition's string `model` ref at
721
+ * assembly time (fail-fast), and the per-call `model` argument at call time.
722
+ *
723
+ * OMITTED is the normal deployment shape: the tool then reads the runner's CURRENT catalog
724
+ * (`Runner.agentCatalog.models`) at each of those moments, so a catalog hot-swapped after mount is
725
+ * the one a delegation is judged against. Passing this explicitly is a deliberate NARROWING and
726
+ * pins the tool to exactly this table for the tool's whole life — the runner's generation changes
727
+ * no longer reach it. And this table is AUTHORITATIVE for the per-call word, not merely advisory:
728
+ * a word judged against it leaves the gate as this table's resolved `Model` object and the child
729
+ * runs that binding — including a key the runner's own catalog does not carry, and including a
730
+ * same-name key the runner binds differently. (The judged-then-re-resolved split this replaced
731
+ * admitted such words and then killed the child inside its own prepare, or ran a binding the spawn
732
+ * frame never named.) A definition's string `model` ref is different: it is validated here at
733
+ * assembly time but still resolves at the child's own prepare through the runner's live table.
734
+ */
603
735
  models?: Record<string, Model>;
604
736
  /** Tools available to the child (a deliberately narrowed subset). */
605
737
  tools?: ToolSpec[];
@@ -37,10 +37,27 @@ function delegatedCostField(stats) {
37
37
  const total = rollupDelegatedCost(stats, stats.nested);
38
38
  return total !== undefined ? { costMicroUsd: total } : {};
39
39
  }
40
+ export function delegatedGranularityFields(stats, elapsedMs) {
41
+ return { ...(stats.toolCalls !== undefined ? { toolUses: stats.toolCalls } : {}), durationMs: elapsedMs };
42
+ }
43
+ function providerFaultField(child) {
44
+ if (child.apiFailure === undefined)
45
+ return {};
46
+ return {
47
+ apiFailure: {
48
+ ...(typeof child.apiFailure.status === "number" ? { status: child.apiFailure.status } : {}),
49
+ ...(typeof child.apiFailure.requestId === "string" ? { requestId: child.apiFailure.requestId } : {}),
50
+ },
51
+ };
52
+ }
53
+ export function delegatedGranularityFieldsWire(stats, elapsedMs) {
54
+ return { ...(stats.toolCalls !== undefined ? { tool_uses: stats.toolCalls } : {}), duration_ms: elapsedMs };
55
+ }
40
56
  export { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX };
41
57
  import { SUBAGENT_RESUME_CAP, SubagentRetainLedger, getOrCreateSessionRetainLedger, ensureSessionReapHook, createResumePrompt, } from "./retain-ledger.js";
42
58
  import { createPeerInboundChainRef, createPeerSelfRef } from "./peer-admission.js";
43
59
  import { recordRosterSpawn } from "./roster-store.js";
60
+ import { LAUNCH_RECEIPT_OWN_WORDS_CLAUSE, launchReceiptNoQuoteClause } from "./launch-receipt-contract.js";
44
61
  import { ObserverDigestTap, ObserverPairing, createObserverReportToolSpec, markObserverTaskId, unmarkObserverTaskId, isObserverTaskId, ObserverResumeStateError, ObserverStoppedByUserError, observerFramingPrompt, observerSlug, resolveObserverDeclaration, } from "./observer.js";
45
62
  import { SubagentStepRecorder } from "./subagent-steps.js";
46
63
  const BG_AGENT_RESULT_MAX = 4_000;
@@ -326,6 +343,7 @@ function resumabilityClaim(f) {
326
343
  const BG_AGENT_COLLATERAL_REAP_REASON = "its parent run ended";
327
344
  const RESERVED_AGENT_NAMES = new Set([OUTPUT_TOOL_NAME, TOOL_SEARCH_NAME, OFFLOAD_TOOL_NAME, REPORT_BLOCKED_TOOL_NAME, DEFAULT_SUBAGENT_TOOL_NAME]);
328
345
  export const REPORT_FIELD_MAX = 300;
346
+ const ROSTER_ECHO_MAX = 400;
329
347
  function configError(message, code) {
330
348
  const e = new Error(message);
331
349
  e.code = code;
@@ -364,6 +382,7 @@ export function completedAgentCard(child, extras) {
364
382
  ...(child.blockedReason !== undefined ? { blockedReason: child.blockedReason } : {}),
365
383
  ...(child.errorMessage !== undefined ? { errorMessage: child.errorMessage } : {}),
366
384
  ...(child.errorCode !== undefined ? { errorCode: child.errorCode } : {}),
385
+ ...providerFaultField(child),
367
386
  ...(child.retryAfterMs !== undefined ? { retryAfterMs: child.retryAfterMs } : {}),
368
387
  ...(child.degraded !== undefined ? { degraded: child.degraded } : {}),
369
388
  ...(extras.handbackWarning !== undefined ? { handbackWarning: extras.handbackWarning } : {}),
@@ -711,9 +730,10 @@ export function createSubagentResume(deps) {
711
730
  const completionIdRevive = deps.registry?.getCompletionId(deps.taskId ?? "");
712
731
  const reviveName = deps.rowDescription ?? `sub-agent ${marker}`;
713
732
  const failReasonRevive = status === "failed" ? child.errorMessage : undefined;
733
+ const reviveElapsedMs = Date.now() - reviveStartedAt;
714
734
  const reviveTerminalSummary = failReasonRevive !== undefined
715
- ? `Agent "${reviveName}" (resumed) failed: ${failReasonRevive}${ccElapsedTag(Date.now() - reviveStartedAt)}`.slice(0, 300) + errorKindClause(errClassRevive)
716
- : `Agent "${reviveName}" (resumed) ${status === "killed" ? "stopped" : child.status === "completed" ? "finished" : String(child.status)}${ccElapsedTag(Date.now() - reviveStartedAt)}`;
735
+ ? `Agent "${reviveName}" (resumed) failed: ${failReasonRevive}${ccElapsedTag(reviveElapsedMs)}`.slice(0, 300) + errorKindClause(errClassRevive)
736
+ : `Agent "${reviveName}" (resumed) ${status === "killed" ? "stopped" : child.status === "completed" ? "finished" : String(child.status)}${ccElapsedTag(reviveElapsedMs)}`;
717
737
  const reviveDurableProbe = deps.registry !== undefined && deps.taskId !== undefined ? deps.registry.durableAgentRowProbe(deps.taskId) : undefined;
718
738
  const reviveDurableRow = reviveDurableProbe !== undefined ? await reviveDurableProbe().catch(() => false) : false;
719
739
  const resumableRevive = resumabilityClaim({
@@ -739,7 +759,7 @@ export function createSubagentResume(deps) {
739
759
  summary: reviveTerminalSummary,
740
760
  ...resumeResidual(),
741
761
  resumable: resumableRevive,
742
- usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
762
+ usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats), ...delegatedGranularityFields(child.stats, reviveElapsedMs) },
743
763
  ...(completionIdRevive !== undefined ? { completionId: completionIdRevive } : {}),
744
764
  });
745
765
  const resumeFrame = {
@@ -753,11 +773,12 @@ export function createSubagentResume(deps) {
753
773
  summary: reviveTerminalSummary,
754
774
  ...(failReasonRevive !== undefined ? { error: failReasonRevive.slice(0, REPORT_FIELD_MAX) } : {}),
755
775
  ...(status === "failed" && errCodeRevive !== undefined ? { errorCode: errCodeRevive } : {}),
776
+ ...providerFaultField(child),
756
777
  ...(resumeHandbackResult ? { result: notifyResultField(resumeHandbackResult) } : {}),
757
778
  ...(status === "killed" && child.result ? { partial: true } : {}),
758
779
  ...resumeResidual(),
759
780
  resumable: resumableRevive,
760
- usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
781
+ usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats), ...delegatedGranularityFieldsWire(child.stats, reviveElapsedMs) },
761
782
  ...(completionIdRevive !== undefined ? { completionId: completionIdRevive } : {}),
762
783
  };
763
784
  const gateOwner = ownsTerminalFacesRevive && status === "completed" && deps.notify !== undefined && deps.registry !== undefined && ledger.get(deps.parentToolCallId) === entry
@@ -1206,7 +1227,7 @@ export function forkWorktreeTranslationNote(parentCwd, worktreeDir) {
1206
1227
  export function asyncLaunchedReceipt(p) {
1207
1228
  const noteLines = (p.notes ?? []).filter((n) => n !== undefined && n !== "");
1208
1229
  const oneShotBlocked = p.notify && p.oneShot === true;
1209
- return (`Async agent launched successfully. (This tool result is internal metadata — never quote or paste any part of it, including the task_id below, into a user-facing reply.)\n` +
1230
+ return (`Async agent launched successfully. (${launchReceiptNoQuoteClause(", including the task_id below,")})\n` +
1210
1231
  `task_id: ${p.taskId} (internal ID - do not mention to user. Use SendMessage with to: '${p.taskId}', summary: '<5-10 word recap>' to continue this agent.)\n` +
1211
1232
  (oneShotBlocked
1212
1233
  ? `${p.workingLine} This is a ONE-SHOT submission — there is no later turn for a background notification to land in, so do NOT end your turn expecting one. Actively wait instead: TaskOutput(task_id: "${p.taskId}", block: true). If it is still running after the wait, wait again (bounded) rather than ending the turn, or write out your best available answer now if you are near your own time budget.\n`
@@ -1215,10 +1236,18 @@ export function asyncLaunchedReceipt(p) {
1215
1236
  : `${p.workingLine} Its result is NOT pushed automatically — retrieve progress and results with TaskOutput(task_id) where mounted.\n`) +
1216
1237
  noteLines.map((n) => `${n}\n`).join("") +
1217
1238
  (oneShotBlocked
1218
- ? `In your own words, briefly tell the user what you launched — do not echo this tool result. Do not assume a later message will deliver the result: this submission ends after this turn, so retrieve it now with the blocking TaskOutput wait above before writing your final answer.`
1239
+ ? `${LAUNCH_RECEIPT_OWN_WORDS_CLAUSE} Do not assume a later message will deliver the result: this submission ends after this turn, so retrieve it now with the blocking TaskOutput wait above before writing your final answer.`
1219
1240
  : p.notify
1220
- ? `In your own words, briefly tell the user what you launched — do not echo this tool result. Agent results will arrive in a subsequent message. If the user asks for progress, say the agent is still running.`
1221
- : `In your own words, briefly tell the user what you launched — do not echo this tool result. The result will NOT arrive on its own — retrieve it with TaskOutput(task_id) where mounted before relying on it.`));
1241
+ ? `${LAUNCH_RECEIPT_OWN_WORDS_CLAUSE} Agent results will arrive in a subsequent message. If the user asks for progress, say the agent is still running.`
1242
+ : `${LAUNCH_RECEIPT_OWN_WORDS_CLAUSE} The result will NOT arrive on its own — retrieve it with TaskOutput(task_id) where mounted before relying on it.`));
1243
+ }
1244
+ function runnerModelCatalog(runner) {
1245
+ try {
1246
+ return runner?.agentCatalog?.models;
1247
+ }
1248
+ catch {
1249
+ return undefined;
1250
+ }
1222
1251
  }
1223
1252
  export function createSubagentTool(opts) {
1224
1253
  const catalog = opts.runner.agentCatalog;
@@ -1227,8 +1256,7 @@ export function createSubagentTool(opts) {
1227
1256
  if (opts.builtinAgents === undefined && catalog?.builtinAgents !== undefined) {
1228
1257
  opts = { ...opts, builtinAgents: catalog.builtinAgents };
1229
1258
  }
1230
- if (opts.models === undefined && catalog?.models)
1231
- opts = { ...opts, models: catalog.models };
1259
+ const assemblyModels = opts.models ?? catalog?.models;
1232
1260
  if (opts.agents?.length) {
1233
1261
  opts = {
1234
1262
  ...opts,
@@ -1252,7 +1280,7 @@ export function createSubagentTool(opts) {
1252
1280
  }
1253
1281
  if (typeof a.model === "string") {
1254
1282
  try {
1255
- resolveModel(a.model, opts.models);
1283
+ resolveModel(a.model, assemblyModels);
1256
1284
  }
1257
1285
  catch {
1258
1286
  throw configError(`createSubagentTool: agent "${a.name}" references unknown model "${a.model}" (pass \`models\` to resolve string refs).`, "config.agent.unknown_model");
@@ -1308,7 +1336,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1308
1336
  ...(forkOffered ? [FORK_SUBAGENT_TYPE] : []),
1309
1337
  ...(generalPurposeShadowed ? [] : [GENERAL_PURPOSE_SUBAGENT_TYPE]),
1310
1338
  ];
1311
- const rosterNames = opts.models ? Object.keys(opts.models) : undefined;
1339
+ const activeModels = () => opts.models ?? runnerModelCatalog(opts.runner);
1340
+ const mountModels = activeModels();
1341
+ const rosterNames = mountModels ? Object.keys(mountModels) : undefined;
1312
1342
  const agentListing = [
1313
1343
  ...(generalPurposeShadowed
1314
1344
  ? []
@@ -1424,7 +1454,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1424
1454
  })),
1425
1455
  model: Type.Optional(Type.String({
1426
1456
  description: rosterNames !== undefined && rosterNames.length > 0
1427
- ? `Optional model override for this agent. Takes precedence over the agent definition's model. If omitted, uses the agent definition's model, or inherits from the parent. Ignored for subagent_type: "${FORK_SUBAGENT_TYPE}" — forks always inherit the parent model.`
1457
+ ? `Optional model override for this agent. Takes precedence over the agent definition's model. If omitted, uses the agent definition's model, or inherits from the parent. A value outside this deployment's model catalog is REFUSED — the call does not fall back silently. Ignored for subagent_type: "${FORK_SUBAGENT_TYPE}" — forks always inherit the parent model.`
1428
1458
  : `Optional model override request. No model catalog is configured here, so a requested name cannot be resolved — the agent runs its definition's model (or inherits from the parent) and the result notes the override was not applied. Ignored for subagent_type: "${FORK_SUBAGENT_TYPE}".`,
1429
1459
  })),
1430
1460
  isolation: Type.Optional(Type.Literal("worktree", {
@@ -1560,23 +1590,54 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1560
1590
  };
1561
1591
  }
1562
1592
  }
1593
+ if (typeof a.model === "string" && a.model.trim() === "" && !wantsFork && reviveClaim === undefined) {
1594
+ return {
1595
+ isError: true,
1596
+ content: "Sub-agent not started: `model` was empty. Name a model from this deployment's catalog, or omit the parameter to run the agent type's own model.",
1597
+ details: { error: "model.empty" },
1598
+ };
1599
+ }
1563
1600
  const requestedModel = typeof a.model === "string" && a.model.trim() !== "" ? a.model.trim() : undefined;
1564
1601
  let perCallModel;
1565
1602
  let modelNote;
1566
- if (requestedModel !== undefined) {
1603
+ const judgedModels = activeModels();
1604
+ const rosterKeys = judgedModels !== undefined ? Object.keys(judgedModels) : [];
1605
+ if (typeof a.model === "string" && requestedModel === undefined) {
1606
+ modelNote = wantsFork
1607
+ ? `note: model "" was ignored — a fork always runs on the caller's model.`
1608
+ : `note: the durable record's model was blank and was NOT applied — the revived agent ran on its default (inherited) model.`;
1609
+ }
1610
+ else if (requestedModel !== undefined) {
1567
1611
  if (wantsFork) {
1568
1612
  modelNote = `note: model "${requestedModel}" was ignored — a fork always runs on the caller's model.`;
1569
1613
  }
1570
- else if (!opts.models) {
1614
+ else if (rosterKeys.length === 0) {
1571
1615
  modelNote = `note: model "${requestedModel}" was NOT applied — this delegation tool has no model roster configured; the sub-agent ran on its default (inherited) model.`;
1572
1616
  }
1573
1617
  else {
1574
1618
  try {
1575
- resolveModel(requestedModel, opts.models);
1576
- perCallModel = requestedModel;
1619
+ perCallModel = resolveModel(requestedModel, judgedModels);
1577
1620
  }
1578
1621
  catch {
1579
- modelNote = `note: model "${requestedModel}" was NOT applied — unknown model (roster: ${Object.keys(opts.models).join(", ")}); the sub-agent ran on its default (inherited) model.`;
1622
+ if (reviveClaim !== undefined) {
1623
+ modelNote = `note: recorded model "${inlineUntrusted(requestedModel, 80)}" is not on the current model roster and was NOT applied — the revived agent ran on its default (inherited) model.`;
1624
+ }
1625
+ else {
1626
+ const shown = [];
1627
+ let budget = ROSTER_ECHO_MAX;
1628
+ for (const k of rosterKeys) {
1629
+ if (budget - k.length < 0)
1630
+ break;
1631
+ shown.push(k);
1632
+ budget -= k.length + 2;
1633
+ }
1634
+ const rosterEcho = shown.length === rosterKeys.length ? shown.join(", ") : `${shown.join(", ")}, … (${rosterKeys.length - shown.length} more)`;
1635
+ return {
1636
+ isError: true,
1637
+ content: `Sub-agent not started: unknown model "${inlineUntrusted(requestedModel, 80)}". Available model values: ${inlineUntrusted(rosterEcho, ROSTER_ECHO_MAX + 32)}. Omit \`model\` to run the agent type's own model, or inherit the caller's.`,
1638
+ details: { error: "model.unknown" },
1639
+ };
1640
+ }
1580
1641
  }
1581
1642
  }
1582
1643
  }
@@ -1690,9 +1751,39 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1690
1751
  const childTools = def
1691
1752
  ? resolveToolSubset(childPool, def.allowTools, def.denyTools)
1692
1753
  : resolveToolSubset(childPool, opts.allowTools, opts.denyTools);
1693
- const childModel = wantsFork
1754
+ let childModel = wantsFork
1694
1755
  ? ctx.model ?? opts.model
1695
1756
  : perCallModel ?? (def ? (def.model ?? ctx.model ?? opts.model) : opts.model);
1757
+ if (reviveClaim !== undefined && typeof childModel === "string") {
1758
+ const resolvable = (ref) => {
1759
+ try {
1760
+ resolveModel(ref, runnerModelCatalog(opts.runner));
1761
+ return true;
1762
+ }
1763
+ catch {
1764
+ return false;
1765
+ }
1766
+ };
1767
+ if (!resolvable(childModel)) {
1768
+ const dead = childModel;
1769
+ const rest = def !== undefined ? [ctx.model, opts.model] : [opts.model];
1770
+ childModel = rest.find((c) => c !== undefined && (typeof c !== "string" || resolvable(c)));
1771
+ if (modelNote === undefined) {
1772
+ modelNote = `note: model "${inlineUntrusted(dead, 80)}" from this agent type's configuration is not on the current model roster and was NOT applied — the revived agent ran on its default (inherited) model.`;
1773
+ }
1774
+ }
1775
+ }
1776
+ const spawnModel = typeof childModel === "string"
1777
+ ? (() => {
1778
+ try {
1779
+ return resolveModel(childModel, judgedModels).id;
1780
+ }
1781
+ catch {
1782
+ return resolveModelDisplayLabel(childModel);
1783
+ }
1784
+ })()
1785
+ : childModel?.id;
1786
+ const rowModelKey = perCallModel !== undefined ? requestedModel : typeof childModel === "string" ? childModel : childModel?.id;
1696
1787
  const childSystemPrompt = def?.systemPrompt ?? opts.systemPrompt;
1697
1788
  const childDefaultPersona = childSystemPrompt === undefined && !wantsFork ? SUBAGENT_PROMPT : undefined;
1698
1789
  const defMaxTurns = typeof def?.maxTurns === "number" && Number.isFinite(def.maxTurns) && def.maxTurns > 0 ? def.maxTurns : undefined;
@@ -2342,7 +2433,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2342
2433
  ...(ctx.taskId !== undefined && ctx.taskId !== ctx.sessionId ? { parentTaskId: ctx.taskId } : {}),
2343
2434
  ...(ctx.sessionId !== undefined ? { parentSessionId: ctx.sessionId } : {}),
2344
2435
  ...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}),
2345
- ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}),
2436
+ ...(rowModelKey !== undefined ? { model: rowModelKey } : {}),
2346
2437
  deliveryChannel: "attaching",
2347
2438
  toolUseId: ctx.toolCallId,
2348
2439
  ...(bg.agentStore !== undefined ? { store: bg.agentStore } : {}),
@@ -2403,7 +2494,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2403
2494
  }, { priority: "next" }), "subagent.reapTerminalNotify");
2404
2495
  });
2405
2496
  if (agentName !== undefined) {
2406
- recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, sessionId: forkedId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}), ...(modelFallback !== undefined ? { modelFallback } : {}), ...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
2497
+ recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, sessionId: forkedId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...(rowModelKey !== undefined ? { model: rowModelKey } : {}), ...(modelFallback !== undefined ? { modelFallback } : {}), ...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
2407
2498
  }
2408
2499
  const bgSink = ctx.onBackgroundChildEvent;
2409
2500
  const sinkEmit = (event) => {
@@ -2422,6 +2513,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2422
2513
  ...(bgScope !== undefined ? { scope: bgScope } : {}),
2423
2514
  description: shortDesc,
2424
2515
  agentType: spawnAgentType,
2516
+ ...(spawnModel !== undefined ? { model: spawnModel } : {}),
2425
2517
  ...(bgForkCycleSeq !== undefined ? { seq: bgForkCycleSeq } : {}),
2426
2518
  ...(agentName !== undefined ? { name: agentName } : {}),
2427
2519
  sessionId: forkedId,
@@ -2451,6 +2543,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2451
2543
  parentToolCallId: ctx.toolCallId,
2452
2544
  progressTaskId: forkedId,
2453
2545
  agentType: spawnAgentType,
2546
+ ...(spawnModel !== undefined ? { model: spawnModel } : {}),
2454
2547
  ...(bgForkCycleSeq !== undefined ? { seq: bgForkCycleSeq } : {}),
2455
2548
  ...(currentAction !== undefined ? { currentAction } : {}),
2456
2549
  ...(currentTool !== undefined ? { currentTool } : {}),
@@ -2485,6 +2578,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2485
2578
  progressTaskId: e.taskId,
2486
2579
  ...(e.parentTaskId !== undefined ? { progressParentTaskId: e.parentTaskId } : {}),
2487
2580
  agentType: spawnAgentType,
2581
+ ...(spawnModel !== undefined ? { model: spawnModel } : {}),
2488
2582
  ...(e.name !== undefined ? { name: e.name } : {}),
2489
2583
  ...(currentAction !== undefined ? { currentAction } : {}),
2490
2584
  ...(currentTool !== undefined ? { currentTool } : {}),
@@ -2562,9 +2656,10 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2562
2656
  const resumableFork = false;
2563
2657
  const forkTranscriptId = child.sessionId ?? forkedId;
2564
2658
  const failReasonFork = settledBg === "failed" ? child.errorMessage : undefined;
2659
+ const forkElapsedMs = Date.now() - forkBgStartedAt;
2565
2660
  const forkTerminalSummary = failReasonFork !== undefined
2566
- ? `Agent "${shortDesc}" failed: ${failReasonFork}${ccElapsedTag(Date.now() - forkBgStartedAt)}`.slice(0, 300) + errorKindClause(errClassFork)
2567
- : `${ccCompletionText(shortDesc, settledBg, String(child.status), Date.now() - forkBgStartedAt)}`;
2661
+ ? `Agent "${shortDesc}" failed: ${failReasonFork}${ccElapsedTag(forkElapsedMs)}`.slice(0, 300) + errorKindClause(errClassFork)
2662
+ : `${ccCompletionText(shortDesc, settledBg, String(child.status), forkElapsedMs)}`;
2568
2663
  const ownsTerminalFacesFork = bg.registry.claimAgentTerminalNotify(taskId);
2569
2664
  if (ownsTerminalFacesFork)
2570
2665
  sinkEmit({
@@ -2580,7 +2675,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2580
2675
  summary: forkTerminalSummary,
2581
2676
  ...residualFork,
2582
2677
  resumable: resumableFork,
2583
- usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
2678
+ usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats), ...delegatedGranularityFields(child.stats, forkElapsedMs) },
2584
2679
  ...(completionIdFork !== undefined ? { completionId: completionIdFork } : {}),
2585
2680
  });
2586
2681
  try {
@@ -2594,12 +2689,13 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2594
2689
  summary: forkTerminalSummary,
2595
2690
  ...(failReasonFork !== undefined ? { error: failReasonFork.slice(0, REPORT_FIELD_MAX) } : {}),
2596
2691
  ...(settledBg === "failed" && errCodeFork !== undefined ? { errorCode: errCodeFork } : {}),
2692
+ ...providerFaultField(child),
2597
2693
  ...(child.sessionId ? { sessionId: child.sessionId } : {}),
2598
2694
  ...(forkBgResult ? { result: notifyResultField(forkBgResult) } : {}),
2599
2695
  ...(settledBg === "killed" && child.result ? { partial: true } : {}),
2600
2696
  ...residualFork,
2601
2697
  resumable: resumableFork,
2602
- usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
2698
+ usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats), ...delegatedGranularityFieldsWire(child.stats, forkElapsedMs) },
2603
2699
  ...(completionIdFork !== undefined ? { completionId: completionIdFork } : {}),
2604
2700
  }, { priority: "next" });
2605
2701
  }
@@ -2912,7 +3008,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2912
3008
  ...(ctx.taskId !== undefined && ctx.taskId !== ctx.sessionId ? { parentTaskId: ctx.taskId } : {}),
2913
3009
  ...(ctx.sessionId !== undefined ? { parentSessionId: ctx.sessionId } : {}),
2914
3010
  ...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}),
2915
- ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}),
3011
+ ...(rowModelKey !== undefined ? { model: rowModelKey } : {}),
2916
3012
  deliveryChannel: "attaching",
2917
3013
  toolUseId: ctx.toolCallId,
2918
3014
  ...(bg.agentStore !== undefined ? { store: bg.agentStore } : {}),
@@ -2936,7 +3032,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2936
3032
  chargeDelegationEntryHandle(bg.registry, capLedgerKey, bg.agentStore, reviveRow === undefined ? taskId : undefined);
2937
3033
  childInternals.peerSelfRef?.addAxis("h", taskId);
2938
3034
  if (agentName !== undefined) {
2939
- recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}), ...(reviveRow !== undefined ? ((reviveRow.rootSessionId ?? reviveRow.parentSessionId) !== undefined ? { rootSessionId: reviveRow.rootSessionId ?? reviveRow.parentSessionId } : {}) : (ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: reviveRow?.spawnedAt ?? Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
3035
+ recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...(reviveRow !== undefined ? (reviveRow.model !== undefined ? { model: reviveRow.model } : {}) : rowModelKey !== undefined ? { model: rowModelKey } : {}), ...(modelFallback !== undefined ? { modelFallback } : {}), ...(reviveRow !== undefined ? ((reviveRow.rootSessionId ?? reviveRow.parentSessionId) !== undefined ? { rootSessionId: reviveRow.rootSessionId ?? reviveRow.parentSessionId } : {}) : (ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: reviveRow?.spawnedAt ?? Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
2940
3036
  }
2941
3037
  const bgSink = ctx.onBackgroundChildEvent;
2942
3038
  const sinkEmit = (event) => {
@@ -2992,6 +3088,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2992
3088
  ...(bgScope !== undefined ? { scope: bgScope } : {}),
2993
3089
  description: reviveRow !== undefined ? `${shortDesc} (revived)` : shortDesc,
2994
3090
  agentType: spawnAgentType,
3091
+ ...(spawnModel !== undefined ? { model: spawnModel } : {}),
2995
3092
  ...(bgCycleSeq !== undefined ? { seq: bgCycleSeq } : {}),
2996
3093
  ...(agentName !== undefined ? { name: agentName } : {}),
2997
3094
  sessionId: bgChildSessionId,
@@ -3065,6 +3162,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
3065
3162
  parentToolCallId: ctx.toolCallId,
3066
3163
  progressTaskId: bgChildSessionId,
3067
3164
  agentType: spawnAgentType,
3165
+ ...(spawnModel !== undefined ? { model: spawnModel } : {}),
3068
3166
  ...(bgCycleSeq !== undefined ? { seq: bgCycleSeq } : {}),
3069
3167
  ...(currentAction !== undefined ? { currentAction } : {}),
3070
3168
  ...(currentTool !== undefined ? { currentTool } : {}),
@@ -3111,6 +3209,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
3111
3209
  progressTaskId: e.taskId,
3112
3210
  ...(e.parentTaskId !== undefined ? { progressParentTaskId: e.parentTaskId } : {}),
3113
3211
  agentType: spawnAgentType,
3212
+ ...(spawnModel !== undefined ? { model: spawnModel } : {}),
3114
3213
  ...(e.name !== undefined ? { name: e.name } : {}),
3115
3214
  ...(currentAction !== undefined ? { currentAction } : {}),
3116
3215
  ...(currentTool !== undefined ? { currentTool } : {}),
@@ -3399,9 +3498,10 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
3399
3498
  named: agentName !== undefined,
3400
3499
  });
3401
3500
  const failReasonBg = settled === "failed" ? unparkedPauseReason ?? child.errorMessage : undefined;
3501
+ const bgElapsedMs = Date.now() - bgStartedAt;
3402
3502
  const bgTerminalSummary = failReasonBg !== undefined
3403
- ? `Agent "${shortDesc}" failed: ${failReasonBg}${ccElapsedTag(Date.now() - bgStartedAt)}${observerNote}`.slice(0, 300) + errorKindClause(errClassBg)
3404
- : `${ccCompletionText(shortDesc, settled, String(child.status), Date.now() - bgStartedAt)}${observerNote}`;
3503
+ ? `Agent "${shortDesc}" failed: ${failReasonBg}${ccElapsedTag(bgElapsedMs)}${observerNote}`.slice(0, 300) + errorKindClause(errClassBg)
3504
+ : `${ccCompletionText(shortDesc, settled, String(child.status), bgElapsedMs)}${observerNote}`;
3405
3505
  const ownsTerminalFaces = bg.registry.claimAgentTerminalNotify(taskId);
3406
3506
  if (ownsTerminalFaces)
3407
3507
  sinkEmit({
@@ -3416,7 +3516,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
3416
3516
  summary: bgTerminalSummary,
3417
3517
  ...residual,
3418
3518
  resumable: resumableBg,
3419
- usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
3519
+ usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats), ...delegatedGranularityFields(child.stats, bgElapsedMs) },
3420
3520
  ...(completionIdBg !== undefined ? { completionId: completionIdBg } : {}),
3421
3521
  });
3422
3522
  const completionFrame = {
@@ -3429,12 +3529,13 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
3429
3529
  summary: bgTerminalSummary,
3430
3530
  ...(failReasonBg !== undefined ? { error: failReasonBg.slice(0, REPORT_FIELD_MAX) } : {}),
3431
3531
  ...(settled === "failed" && errCodeBg !== undefined ? { errorCode: errCodeBg } : {}),
3532
+ ...providerFaultField(child),
3432
3533
  ...(child.sessionId ? { sessionId: child.sessionId } : {}),
3433
3534
  ...(bgHandbackResult ? { result: notifyResultField(bgHandbackResult) } : {}),
3434
3535
  ...(settled === "killed" && child.result ? { partial: true } : {}),
3435
3536
  ...residual,
3436
3537
  resumable: resumableBg,
3437
- usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
3538
+ usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats), ...delegatedGranularityFieldsWire(child.stats, bgElapsedMs) },
3438
3539
  ...(completionIdBg !== undefined ? { completionId: completionIdBg } : {}),
3439
3540
  };
3440
3541
  const gateOwner = ownsTerminalFaces && settled === "completed" && notify !== undefined ? bgRetain?.childSessionId : undefined;
@@ -166,6 +166,14 @@ export function validateFileHistoryExport(data) {
166
166
  return bad(`tracked row ${t.relOrAbsPath} carries a non-integer version number`);
167
167
  if (v.blobHash !== null && (typeof v.blobHash !== "string" || !/^[0-9a-f]{64}$/.test(v.blobHash)))
168
168
  return bad(`tracked row ${t.relOrAbsPath} carries a blob reference that is not a sha256 digest`);
169
+ if (v.mode !== undefined) {
170
+ if (typeof v.mode !== "number" || !Number.isInteger(v.mode) || v.mode < 0 || v.mode > 0o7777) {
171
+ return bad(`tracked row ${t.relOrAbsPath} carries a mode outside the permission-bit domain [0, 0o7777] — refused, never coerced`);
172
+ }
173
+ if ((v.mode & 0o6000) !== 0) {
174
+ return bad(`tracked row ${t.relOrAbsPath} carries a set-ID mode (0o${v.mode.toString(8)}) — a restore replaying setuid/setgid bits would mint a privilege-escalation primitive; refused`);
175
+ }
176
+ }
169
177
  }
170
178
  if (new Set(t.versions.map((v) => v.version)).size !== t.versions.length)
171
179
  return bad(`tracked row ${t.relOrAbsPath} carries the same version number twice — a chain whose versions are ambiguous cannot be published`);
@@ -284,13 +292,15 @@ export async function applyFileRestoreViaEnv(env, absPath, target, opts) {
284
292
  return { outcome: "identical" };
285
293
  if (modeOnly === "applied")
286
294
  return { outcome: "applied" };
295
+ if (modeOnly === "applied_unpreserved")
296
+ return { outcome: "applied", modeNotPreserved: true };
287
297
  return { outcome: "identical", modeNotPreserved: true };
288
298
  }
289
299
  const w = await writeRestoredBytes(env, absPath, target.bytes, signal);
290
300
  if (!w.ok)
291
301
  return { outcome: w.refused ? "refused" : "failed", reason: `write: ${w.code}` };
292
302
  const modeAfter = await applyRecordedMode(env, absPath, opts, undefined);
293
- return { outcome: "applied", ...(modeAfter === "unpreserved" ? { modeNotPreserved: true } : {}) };
303
+ return { outcome: "applied", ...(modeAfter === "unpreserved" || modeAfter === "applied_unpreserved" ? { modeNotPreserved: true } : {}) };
294
304
  }
295
305
  async function restorePreflight(env, absPath, opts) {
296
306
  const info = await env.fileInfo(absPath, opts.signal);
@@ -337,7 +347,10 @@ export async function previewFileDelta(env, absPath, targetHash, targetBytes, ta
337
347
  return undefined;
338
348
  const info = await env.fileInfo(absPath);
339
349
  const currentMode = info.ok ? info.value.mode : undefined;
340
- return modeProvenDifferent(targetMode, currentMode) ? { insertions: 0, deletions: 0 } : undefined;
350
+ const replayMode = (targetMode & 0o6000) !== 0 ? targetMode & 0o1777 : targetMode;
351
+ return modeProvenDifferent(targetMode, currentMode) && modeProvenDifferent(replayMode, currentMode)
352
+ ? { insertions: 0, deletions: 0 }
353
+ : undefined;
341
354
  }
342
355
  return countRestoreLineDiff(curBytes, targetBytes);
343
356
  }
@@ -347,6 +360,15 @@ async function applyRecordedMode(env, absPath, opts, currentMode) {
347
360
  return "unchanged";
348
361
  if (currentMode === (want & 0o7777))
349
362
  return "unchanged";
363
+ if ((want & 0o6000) !== 0) {
364
+ const stripped = want & 0o1777;
365
+ if (currentMode === stripped)
366
+ return "unpreserved";
367
+ if (env.setFileMode === undefined)
368
+ return "unpreserved";
369
+ const r = await env.setFileMode(absPath, stripped, opts.signal);
370
+ return r.ok ? "applied_unpreserved" : "unpreserved";
371
+ }
350
372
  if (env.setFileMode === undefined)
351
373
  return "unpreserved";
352
374
  const r = await env.setFileMode(absPath, want & 0o7777, opts.signal);
@@ -3,7 +3,7 @@ export function resolveModel(ref, models) {
3
3
  if (typeof ref !== "string") {
4
4
  return ref;
5
5
  }
6
- const m = models?.[ref];
6
+ const m = models !== undefined && Object.hasOwn(models, ref) ? models[ref] : undefined;
7
7
  if (!m) {
8
8
  throw new Error(`Unknown model ref "${ref}". Provide it in RunnerDeps.models or pass a Model object.`);
9
9
  }
@@ -1410,6 +1410,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1410
1410
  ...(internals?.cycleSeq !== undefined ? { seq: internals.cycleSeq } : {}),
1411
1411
  ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
1412
1412
  ...(subagentName ? { name: subagentName } : {}),
1413
+ model: prepared.model.id,
1413
1414
  usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
1414
1415
  status: "running",
1415
1416
  ...ident(),
@@ -4223,6 +4224,7 @@ export class Runner {
4223
4224
  ...(internals?.cycleSeq !== undefined ? { seq: internals.cycleSeq } : {}),
4224
4225
  ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
4225
4226
  ...(subagentName ? { name: subagentName } : {}),
4227
+ model: prepared.model.id,
4226
4228
  usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
4227
4229
  status: result.status === "completed" ? "completed" : "failed",
4228
4230
  ...ident(),
@@ -66,6 +66,19 @@ export interface TaskNotificationPayload {
66
66
  * `TaskResult.errorCode` taxonomy (brain `[code]` prefixes via extractErrorCode, `limit.*`,
67
67
  * `budget.*`, …). Engine-minted, never model text. Absent when the failure carried no code. */
68
68
  errorCode?: string;
69
+ /** Terminal, delegation lanes: the settling run's PROVIDER-BOUNDARY fault assertion, mirrored from
70
+ * its `TaskResult.apiFailure`. Its PRESENCE is the claim — "this failure came from the transport or
71
+ * the provider, not from this deployment refusing to send, a limit, or unusable model output" — and
72
+ * `errorCode` beside it names WHICH terminal; the two answer different questions and neither implies
73
+ * the other. Members are whatever the failing attempt stated about itself (`status` when the provider
74
+ * answered with one, `requestId` when it named one), so an EMPTY object is meaningful: "provider
75
+ * fault, unlabelled", which is not the same statement as absence. Absent on every non-provider
76
+ * terminal, on every completed run, and on the non-delegation lanes (bash/monitor), whose failures do
77
+ * not pass through a model provider at all. */
78
+ apiFailure?: {
79
+ status?: number;
80
+ requestId?: string;
81
+ };
69
82
  /** Structured exit code of a background command's terminal notification (bash lane): the process
70
83
  * exited on its own with this code. Lets consumers branch on success/failure without parsing the
71
84
  * summary wording. Absent when the process never exited by itself (killed / spawn-failed lanes). */
@@ -77,6 +90,13 @@ export interface TaskNotificationPayload {
77
90
  * every other notification (a completed result is never flagged). */
78
91
  partial?: boolean;
79
92
  output_file?: string;
93
+ /** Whatever the settling lane knows about its own spend, JSON-rendered verbatim into one `<usage>`
94
+ * tag. Deliberately untyped — the lanes report different quantities and nothing here should force
95
+ * one to fabricate a figure it does not have. What IS contracted is the VOCABULARY: a name that two
96
+ * lanes both publish must mean the same thing on both. The delegation lanes
97
+ * (`background_agent`, its fork/revive cycles) and the workflow lane therefore agree on
98
+ * `tokens`/`turns`/`costMicroUsd`/`tool_uses`/`duration_ms`; the workflow lane's fan-out counters
99
+ * (`agent_count`, `agents_done`, …) and resume counters have no delegation analog and are its own. */
80
100
  usage?: unknown;
81
101
  /** CC `<diagnostics>` parity: ENGINE-MINTED teaching text for the "result is empty/unexpected —
82
102
  * now what" moment (per-agent read route / journal coordinate / resume command). Producers must mint it
@@ -4646,6 +4646,31 @@ export type TaskEvent = ({
4646
4646
  * nor an agent-type — the child then keeps its taskId (NOT the raw objective, which could leak a delegated
4647
4647
  * secret to a progress-scoped consumer — dual-review Q2). Sanitized (control-char-stripped, length-capped). */
4648
4648
  name?: string;
4649
+ /**
4650
+ * The model this sub-run was PREPARED with — the resolved id, read off the leg's prepared model
4651
+ * at the mint, so it is the same value `TaskResult.model` and the `task.start` trace frame carry.
4652
+ * (`task.end` reports the run's totals and has never named a model; there is nothing to join to
4653
+ * there.)
4654
+ *
4655
+ * Why it rides the live lane at all: a delegation may name a tier word (`"sonnet"`), an agent
4656
+ * definition's model, or NOTHING (inherit the caller's current model, or fall to the `subagent`
4657
+ * role) — every one of those resolves somewhere the consumer cannot see, so a shell badging a
4658
+ * running child previously had only the REQUESTED word (or nothing) to render, which is a
4659
+ * different claim from what the child runs on. This is the resolved answer, not the request.
4660
+ *
4661
+ * PREPARED, deliberately, and NOT "whatever is serving this turn" — stated in the first sentence
4662
+ * because the difference is observable. A mid-run DEGRADE switch (`spec.limits.degrade`) or a
4663
+ * gateway re-route moves the serving model without rewriting this field, exactly as
4664
+ * `TaskResult.model` behaves; the switch is announced on its own channel (`TaskResult.degraded`),
4665
+ * and the per-call served id lives on the brain-call telemetry. One name, one meaning, across the
4666
+ * three faces that use it — at the cost of being the leg's declared model rather than a live one.
4667
+ *
4668
+ * Present on every tick this build mints (the frame family is subagent-only by construction, so
4669
+ * there is no lane where a leg has no prepared model). Declared optional for the ordinary reason:
4670
+ * a consumer folding frames from a pre-key producer must keep compiling, and absence there means
4671
+ * "this producer did not state it", never "no model".
4672
+ */
4673
+ model?: string;
4649
4674
  /** The child's most recent tool intent as one human line ("Bash npm test",
4650
4675
  * "Edit src/x.ts") — the SAME source/value as the registry sink tick's `currentAction` (residual
4651
4676
  * observability, lane B), attached to the FORWARDED frame because every client wire projects this
@@ -5246,6 +5271,40 @@ export interface BackgroundChildEvent {
5246
5271
  * waiting for spawn-frame forwarding; `name` stays the design/99 DISPLAY label (description-backed)
5247
5272
  * and was never a type field. */
5248
5273
  agentType?: string;
5274
+ /**
5275
+ * spawn + tick: the model the ROW runs on — the resolved `ModelRef` the spawner selected for this
5276
+ * child (a per-call `model`, an agent definition's, or the caller's own current model, inherited).
5277
+ * It answers with the SERVED catalog model id wherever that is knowable, because this value sits
5278
+ * beside the child's own `task_progress.model` (the leg's prepared id) and a consumer joins the
5279
+ * two: a row saying "sonnet" next to ticks naming the catalog id it routed to is indistinguishable
5280
+ * from two different children. A `Model` object carries its id — and a per-call word that passed
5281
+ * the spawn gate arrives as exactly that resolved object, so for it the row and the ticks are one
5282
+ * value by construction. A definition/tool-level STRING ref resolves through the catalog in force
5283
+ * at the spawn judgement; the child's own prepare re-resolves such a ref against the runner's live
5284
+ * table, so for those refs alone a catalog hot-swapped between the two reads can lag the row one
5285
+ * generation behind the ticks. The DISPLAY rule survives only as the FALLBACK, for a string the
5286
+ * judgement-time catalog cannot resolve (a CC tier alias then shows its sema tier, never the alias
5287
+ * verbatim). It rides ticks as well as spawn for the same reason
5288
+ * {@link agentType} does: a consumer that only forwards ticks must not have to wait for spawn-frame
5289
+ * forwarding to fill its row. Like `agentType` it is the ROW's own fact and is NEVER copied off a
5290
+ * forwarded frame — a nested descendant's `task_progress` names ITS model, not this row's.
5291
+ *
5292
+ * ABSENT is a fact, not a gap, and there are exactly three ways to get there:
5293
+ * · the delegation named no model anywhere in the chain, so the child runs on the `subagent` ROLE
5294
+ * and its concrete id is only decided at the child's own prepare;
5295
+ * · the RETAIN-LEDGER wake lane (a `SendMessage` resume of a completed child), whose frames are
5296
+ * projected from an `AccessibleTaskRow` — and that row has no model column, so this lane has
5297
+ * nothing to state and will not invent one. Stated precisely because the sibling wake lane does
5298
+ * NOT share the limitation: a tier-3 DURABLE revive re-enters through the ordinary background
5299
+ * spawn, re-derives the model like a first spawn, and its `(revived)` frames carry it whenever
5300
+ * that derivation lands on a model (the durable record keeps a `model` of its own; a recorded
5301
+ * key the current catalog no longer resolves degrades the revival to the inherited model, and a
5302
+ * mount with nothing to inherit then leaves these frames honestly silent);
5303
+ * · a pre-key producer.
5304
+ * In every case the child's own `task_progress` frames still carry the resolved answer, which is the
5305
+ * authority; this field is the row-level convenience beside it.
5306
+ */
5307
+ model?: string;
5249
5308
  /** spawn: the HOST task's DECLARED task id (parent attribution). Omitted when the host
5250
5309
  * run declared no task id (the `spec.taskId ?? sessionId` fallback would launder a session id into
5251
5310
  * a task-id field — the orphan-pointer shape); {@link parentSessionId} is the always-on linkage. */
@@ -5359,7 +5418,21 @@ export interface BackgroundChildEvent {
5359
5418
  stoppedBy?: "user" | "parent" | "system" | (string & {});
5360
5419
  /** terminal: bounded human summary (same text the task_notification carries). */
5361
5420
  summary?: string;
5362
- /** tick: live rollup (`task_progress.usage`) · terminal: final `{tokens, turns, costMicroUsd}`. */
5421
+ /** tick: live rollup (`task_progress.usage`) · terminal: the settled
5422
+ * `{tokens, turns, costMicroUsd?, toolUses?, durationMs}`. `costMicroUsd` is own+nested and present
5423
+ * only when that total is KNOWN (RB-368); `toolUses` is the run's own `stats.toolCalls` and is
5424
+ * absent — never zeroed — when the gateway reported no usage. The last two joined the terminal face
5425
+ * to close a reporting asymmetry: the workflow lane's completion notification and the Agent tool's
5426
+ * own sync `<usage>` footer had published both all along, so one child answered "how much work, how
5427
+ * long" on one lane and refused on the other.
5428
+ *
5429
+ * `durationMs` MEASURES DIFFERENT SPANS on the two kinds, which is worth knowing before plotting it:
5430
+ * on a TICK it is the child RUN's own elapsed (the forwarded frame's value, from the run's start);
5431
+ * on the TERMINAL it is the delegation LEG's, from the lane's launch instant — which is earlier, so
5432
+ * a consumer graphing one series sees a step up at settle. Both are true of what they name; neither
5433
+ * can be computed from the other without the spawn/prepare interval, which is why they are not
5434
+ * reconciled into one. The intra-turn activity beat carries a third, narrower shape (`{toolUses}`
5435
+ * alone — there is no honest live token figure on that lane). */
5363
5436
  usage?: {
5364
5437
  totalTokens?: number;
5365
5438
  toolUses?: number;
@@ -133,7 +133,12 @@ export interface WiringManifest {
133
133
  * a completed/failed/killed(non-user) subagent minted through the deps-visible assembly is
134
134
  * continuable across a process restart. A statement about the DEPS-VISIBLE assembly only: a
135
135
  * caller-mounted Agent tool over its own runner is outside this manifest's sight (per-read
136
- * honest degrade + the integrity notice own that case, never a fabricated tier). */
136
+ * honest degrade + the integrity notice own that case, never a fabricated tier) — and that
137
+ * blind spot is load-bearing here, because the store this field is derived from is the ROOT
138
+ * leg's while the store an ordinary child transcript is actually minted through is the AGENT
139
+ * TOOL's runner's ({@link resolveSubagentTranscriptTier} names the rule and the fork-lane
140
+ * exception). The word names ADDRESSABILITY: `none`/`rows` are silent on whether a child
141
+ * transcript reaches disk at all, and only `full` promises the a* handle survives a restart. */
137
142
  fleet: {
138
143
  backgroundAgentStore: boolean;
139
144
  hostChildEventSink: boolean;
@@ -254,6 +259,18 @@ export type SubagentTranscriptTier = "none" | "rows" | "full";
254
259
  * fold picked). This is the DEPLOYMENT-level judgment only; the per-handle "did this row's
255
260
  * transcript actually land" question belongs to `durableAgentRowProbe` (the S1b release-flip gate)
256
261
  * — two different questions, deliberately two named faces (do not merge them back into one).
262
+ *
263
+ * WHICH session store to pass: the one on the runner the Agent tool was mounted with
264
+ * (`SubagentToolOptions.runner.sessions`), because that is the store an ordinary (sync/background)
265
+ * child transcript is minted through — the fork lane deliberately branches through the HOST store
266
+ * instead, see the law on `SubagentToolOptions.runner`. A deployment that serves root tasks from one
267
+ * runner and delegated children from another (a throwaway sub-task store, a routing store) has two
268
+ * different stores here, and passing the ROOT deps' store reports the root lane's fate under the
269
+ * child lane's name. Note what the three words do and do not promise about DISK: this tier names
270
+ * ADDRESSABILITY, not persistence. `none` is returned before any store is inspected, so it covers
271
+ * both "the transcript is on disk but nothing can address it" and "there is no transcript" — an
272
+ * operator asking whether a child conversation is readable afterwards must read the delegation
273
+ * store's own durability + release/evict semantics, not this word.
257
274
  */
258
275
  export declare function resolveSubagentTranscriptTier(agentStoreWired: boolean, sessionStore: {
259
276
  readonly placements?: {
@@ -3,6 +3,7 @@ import { defineTool, errorResult } from "../core/tools.js";
3
3
  import { governanceBaselineError, governanceBaselineProblem } from "./governance-baseline-validity.js";
4
4
  import { redactSecrets, redactHostLeaks, boundedRedactedSummary } from "../core/untrusted-egress.js";
5
5
  import { withDelegationProvenance } from "../core/tool-policy.js";
6
+ import { LAUNCH_RECEIPT_OWN_WORDS_CLAUSE, launchReceiptNoQuoteClause } from "../agents/launch-receipt-contract.js";
6
7
  import { startWorkflow } from "./workflow.js";
7
8
  import { buildWorkflowPrimitives } from "./workflow-primitives.js";
8
9
  import { parseWorkflowMeta, splitWorkflowMeta, workflowScriptReadsClockOrRandom } from "./workflow-meta.js";
@@ -630,8 +631,7 @@ export async function createRunWorkflowTool(d) {
630
631
  task_id: runId,
631
632
  status: "started",
632
633
  ...(persistedScriptPath !== undefined ? { scriptPath: persistedScriptPath } : {}),
633
- handling: "This tool result is internal metadata — never quote or paste any part of it (the ids above, and scriptPath when present) into a user-facing reply. " +
634
- "In your own words, briefly tell the user what you launched; do not echo this result.",
634
+ handling: `${launchReceiptNoQuoteClause(" (the ids above, and scriptPath when present)")} ${LAUNCH_RECEIPT_OWN_WORDS_CLAUSE}`,
635
635
  note: (() => {
636
636
  const pollExpr = d.taskRegistry ? `TaskOutput({ task_id: "${runId}" })` : undefined;
637
637
  const blockingPollExpr = d.taskRegistry ? `TaskOutput({ task_id: "${runId}", block: true })` : undefined;
@@ -54,11 +54,14 @@ function forReview(text) {
54
54
  return text;
55
55
  return `${text.slice(0, MAX_REVIEWED_PROMPT_CHARS)}\n[…TRUNCATED FOR REVIEW: ${text.length - MAX_REVIEWED_PROMPT_CHARS} further characters follow that the child WILL receive and this review did NOT see]`;
56
56
  }
57
- function workflowModelLabel(spec) {
57
+ function workflowModelLabel(spec, catalog) {
58
58
  const model = spec.model;
59
59
  if (model === undefined)
60
60
  return undefined;
61
- return typeof model === "string" ? resolveModelDisplayLabel(model) : model.id ?? model.name;
61
+ if (typeof model !== "string")
62
+ return model.id ?? model.name;
63
+ const served = catalog?.[model]?.id;
64
+ return served ?? resolveModelDisplayLabel(model);
62
65
  }
63
66
  export const WORKFLOW_SUBAGENT_PROMPT = `You are a subagent spawned by a workflow orchestration script. Use the tools available to complete the task.
64
67
 
@@ -441,6 +444,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
441
444
  }
442
445
  const concurrency = normalizeConcurrency(opts.concurrency);
443
446
  const agentRegistry = workflowAgentRegistry(opts);
447
+ const modelCatalog = runner.agentCatalog?.models;
444
448
  const store = opts.store;
445
449
  const maxAgents = normalizeWorkflowHardCap("maxAgents", opts.maxAgents);
446
450
  const maxLogChars = normalizeWorkflowHardCap("maxLogChars", opts.maxLogChars);
@@ -633,11 +637,11 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
633
637
  const bceParentToolCall = opts.parentToolCallId !== undefined ? { parentToolCallId: opts.parentToolCallId } : {};
634
638
  const waIdOf = (callKey) => `wa${createHash("sha256").update(`${runId}:${callKey}`).digest("hex").slice(0, 16)}`;
635
639
  const bceLive = new Map();
636
- const bceSpawn = (callKey, label, agentType, replayed, sessionId) => {
640
+ const bceSpawn = (callKey, label, agentType, replayed, sessionId, model) => {
637
641
  if (!bceSink)
638
642
  return;
639
643
  const id = waIdOf(callKey);
640
- bceLive.set(id, { callKey, label, ...(agentType !== undefined ? { agentType } : {}), ...(sessionId !== undefined ? { sessionId } : {}) });
644
+ bceLive.set(id, { callKey, label, ...(agentType !== undefined ? { agentType } : {}), ...(sessionId !== undefined ? { sessionId } : {}), ...(model !== undefined ? { model } : {}) });
641
645
  bceEmit({
642
646
  kind: "spawn",
643
647
  taskId: id,
@@ -648,6 +652,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
648
652
  ...(scope !== undefined ? { scope } : {}),
649
653
  description: replayed ? `${label} (replayed)` : label,
650
654
  agentType: agentType ?? "workflow-agent",
655
+ ...(model !== undefined ? { model } : {}),
651
656
  name: label,
652
657
  ...(opts.parentTaskId !== undefined ? { parentTaskId: opts.parentTaskId } : {}),
653
658
  workflowRunId: runId,
@@ -679,6 +684,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
679
684
  workflowRunId: runId,
680
685
  ...(scope !== undefined ? { scope } : {}),
681
686
  ...(row.agentType !== undefined ? { agentType: row.agentType } : { agentType: "workflow-agent" }),
687
+ ...(row.model !== undefined ? { model: row.model } : {}),
682
688
  ...(row.sessionId !== undefined ? { sessionId: row.sessionId, transcriptId: row.sessionId } : {}),
683
689
  name: e.name ?? row.label,
684
690
  progressTaskId: e.taskId,
@@ -1003,7 +1009,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1003
1009
  const specForIdentity = inheritedModelSnap !== undefined ? { ...spec, model: inheritedModelSnap } : spec;
1004
1010
  const callKey = workflowAgentCallKey(run.agents.length, specForIdentity, agentOpts);
1005
1011
  const prompt = boundedRedactedSummary(spec.systemPrompt ? `${spec.systemPrompt}\n\n${spec.objective}` : spec.objective, MAX_TRANSCRIPT_CHARS);
1006
- const model = workflowModelLabel(specForIdentity);
1012
+ const specForLabel = spec.model === undefined && typeDefModel !== undefined ? { ...spec, model: typeDefModel } : specForIdentity;
1013
+ const model = workflowModelLabel(specForLabel, modelCatalog);
1007
1014
  return { label, phase, phaseInstance, groupId, inheritedModelSnap, callKey, prompt, model };
1008
1015
  };
1009
1016
  const reviewSpawnBeforeLaunch = async (lane, label, callKey, runSpec, effectiveSignal) => {
@@ -1161,13 +1168,14 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1161
1168
  const rs = r.stats;
1162
1169
  const cachedOutput = boundedRedactedSummary(r.structuredOutput ?? r.result, MAX_TRANSCRIPT_CHARS);
1163
1170
  const at = now();
1171
+ const replayModel = r.model;
1164
1172
  const replayRec = {
1165
1173
  label,
1166
1174
  callKey,
1167
1175
  ...(groupId !== undefined ? { groupId } : {}),
1168
1176
  phase,
1169
1177
  prompt,
1170
- ...(model !== undefined ? { model } : {}),
1178
+ ...(replayModel !== undefined ? { model: replayModel } : {}),
1171
1179
  replayed: true,
1172
1180
  status: r.status === "completed" ? "completed" : "failed",
1173
1181
  taskStatus: r.status,
@@ -1185,9 +1193,9 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1185
1193
  run.agents.push(replayRec);
1186
1194
  if (phaseInstance)
1187
1195
  agentPhaseOf.set(replayRec, phaseInstance);
1188
- emit({ type: "agent_start", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), callKey, prompt, ...(model !== undefined ? { model } : {}), replayed: true, ts: at });
1196
+ emit({ type: "agent_start", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), callKey, prompt, ...(replayModel !== undefined ? { model: replayModel } : {}), replayed: true, ts: at });
1189
1197
  emit({ type: "agent_end", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), status: replayRec.status, output: cachedOutput, ...(rs.toolCalls !== undefined ? { toolCalls: rs.toolCalls } : {}), replayed: true, ts: at });
1190
- bceSpawn(callKey, label, agentOpts.agentType, true, r.sessionId || undefined);
1198
+ bceSpawn(callKey, label, agentOpts.agentType, true, r.sessionId || undefined, replayModel);
1191
1199
  bceTerminal(callKey, replayRec.status === "completed" ? "completed" : "failed", cachedOutput, r.sessionId || undefined, replayRec.stats);
1192
1200
  accumulateStats(r, false);
1193
1201
  void persist("update");
@@ -1233,7 +1241,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1233
1241
  }
1234
1242
  rec.startedAt = now();
1235
1243
  const bornChildSessionId = resolveChildSessionIdAtSpawn(spec);
1236
- bceSpawn(callKey, label, agentOpts.agentType, false, bornChildSessionId);
1244
+ bceSpawn(callKey, label, agentOpts.agentType, false, bornChildSessionId, model);
1237
1245
  const typedSpec0 = applyWorkflowAgentType(spec, agentOpts.agentType, agentRegistry);
1238
1246
  const typedSpec = typedSpec0.model === undefined && inheritedModelSnap !== undefined ? { ...typedSpec0, model: inheritedModelSnap } : typedSpec0;
1239
1247
  const framedSpec = withWorkflowChildPersona(typedSpec, agentOpts.schema ?? typedSpec.outputSchema);
@@ -1545,7 +1553,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1545
1553
  }
1546
1554
  rec.startedAt = now();
1547
1555
  const childSessionId = resolveChildSessionIdAtSpawn(spec);
1548
- bceSpawn(callKey, label, agentOpts.agentType, false, childSessionId);
1556
+ bceSpawn(callKey, label, agentOpts.agentType, false, childSessionId, model);
1549
1557
  let stream;
1550
1558
  try {
1551
1559
  const typedSpec0 = applyWorkflowAgentType(spec, agentOpts.agentType, agentRegistry);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.0.0",
3
+ "version": "7.0.1",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",