@sema-agent/core 6.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/agents/launch-receipt-contract.d.ts +34 -0
  3. package/dist/agents/launch-receipt-contract.js +5 -0
  4. package/dist/agents/subagent.d.ts +134 -2
  5. package/dist/agents/subagent.js +132 -31
  6. package/dist/core/file-history-store.js +24 -2
  7. package/dist/core/governance-codes.d.ts +11 -2
  8. package/dist/core/governance-codes.js +1 -0
  9. package/dist/core/permission-rule-consent.d.ts +42 -2
  10. package/dist/core/permission-rule-consent.js +93 -11
  11. package/dist/core/permission-rule-model.d.ts +51 -9
  12. package/dist/core/permission-rule-model.js +4 -2
  13. package/dist/core/permission-rule-session.d.ts +124 -0
  14. package/dist/core/permission-rule-session.js +121 -0
  15. package/dist/core/permission-rule-store.d.ts +65 -2
  16. package/dist/core/permission-rule-store.js +60 -6
  17. package/dist/core/permission-rule-sync.d.ts +9 -0
  18. package/dist/core/permission-rule-sync.js +37 -8
  19. package/dist/core/roles.js +1 -1
  20. package/dist/core/runner/prepare-task.js +35 -2
  21. package/dist/core/runner/runtask.js +2 -0
  22. package/dist/core/store-contracts/permission-rule-sync-contract.js +15 -1
  23. package/dist/core/task-notification.d.ts +20 -0
  24. package/dist/core/trace.d.ts +7 -2
  25. package/dist/core/types.d.ts +85 -1
  26. package/dist/core/wiring-manifest.d.ts +18 -1
  27. package/dist/index.d.ts +2 -1
  28. package/dist/index.js +2 -1
  29. package/dist/orchestration/run-workflow-tool.js +2 -2
  30. package/dist/orchestration/workflow.js +18 -10
  31. package/dist/stores/file/permission-rule-store.d.ts +11 -0
  32. package/dist/stores/file/permission-rule-store.js +22 -9
  33. package/package.json +1 -1
  34. package/test/export-surface.snapshot.json +15 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
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
+
24
+ ## 7.0.0 — 2026-08-30
25
+
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.
27
+
28
+ ### BREAKING (design/382 P1 — the three-dimensional rule scope)
29
+ - **`RuleScope` grows a third member** `{ kind: "session"; sessionId }` (design/382 §4): exhaustive switches over the two-member set no longer compile; `sameScope`/`scopeKey` answer three members. Org is deliberately NOT a member — an approval card is structurally unable to spell an org-wide grant. Two NAMED validation faces replace any one-table reading: `isValidConsentScope` (three members; the consent protocol and its server projection) vs `isValidDurableScope` (two members; the persisted store and rule-sync — a session row is refused at every durable entrance).
30
+ - **The consent scope default flips** (#490②): `prepareCardApproval` no longer defaults an absent `scope` to global. Explicit scope wins (validated; garbage refuses `config.invalid_argument`); absent scope with `cwd` present derives `{ kind: "project", root: cwd }`; BOTH absent refuses loudly with the new code `config.missing_scope`. Hosts relying on absent-scope-⇒-global must pass `{kind:"global"}` explicitly or thread the adjudicated cwd — the failure direction is toward asking, never a silent widening. Starter batch stays literal global; import scope mapping unchanged.
31
+ - **A session row is refused at every durable entrance** (§4.3, the open-set duty): `redemption-add`/`tighten-delete` deltas throw typed `unsupported.session_scope_store` (new shared backend assert `assertWriteDeltaScopeDurable`); sync-join drops session rules AND tombstones per-row with the new closed-set reason `session_scope_not_durable` (`RULE_SYNC_DROP_CODES` +1; `RuleQuarantineReason` excludes it — the quarantine area is itself durable) without withholding the round; a session-scope quarantine instruction throws; the file backend screens at-rest bytes off every read face including the sync outbound `readRaw`; `removePersistedRule` refuses a session scope — a session rule is not individually deletable, it dies with its session. The `permission.rule_sync_resurrected/dropped` trace events' `scopeKind` widens to include `"session"`.
32
+
33
+ ### Added
34
+ - **The session-rule overlay** (`permission-rule-session.ts`): `SessionRuleOverlay` contract + `InMemorySessionRuleOverlay` reference implementation (apply idempotent by dot; termination seals the lifetime epoch FIRST then clears — a late in-flight write refuses `session_ended`; `snapshotSession`/`restoreSession` carry rows across park/resume, restore fail-closed on scope and on the canonical re-projection). `RuleConsentDeps.sessionRules` lands session-scope redemptions there (the store write leg is never taken; dot-first crash order and re-apply-on-replay preserved); `RunnerDeps.sessionPermissionRules` splices the current session's rows in front of the persisted `list()` at the gate lane (narrowest-first report order; `sessionId` becomes the eligibility context's third axis, fail-closed when unthreaded). Overlay-served rows pass the same `normalizePersistedRule` re-projection screen the sync face runs on store rows, at all three re-entry doors (gate lane, prepare coverage, restore). New exports: `isValidConsentScope`, `isValidDurableScope`, `assertWriteDeltaScopeDurable`, `SessionRuleOverlay`, `SessionRuleOverlayAdd`, `SessionRuleOverlayApplyResult`, `InMemorySessionRuleOverlay`. The rule-sync contract kit grows the durable-two-member-face conformance vector (server twin duty, mechanized).
35
+
3
36
  ## 6.0.0 — 2026-08-30
4
37
 
5
38
  ### BREAKING
@@ -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[];