@sema-agent/core 7.9.1 → 7.9.2

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 7.9.2 — 2026-09-09
4
+
5
+ ### Fixes — a delegated child runs on the parent run's model, not the deployment's boot default (#641 · B-050; @cli @server)
6
+ - **One seat, one rule.** `childModelSeat` (subagent.ts) is now the only place an engine-spawned run's model is decided — the delegation child (`buildChildSpec`, every lane: sync / background / fork / revival) and the observer sidecar share it: an EXPLICIT model for the spawn (per-call `model` > agent definition `model` > tool-level `model`) > the deployment's STATED subagent tier (`roles.subagent` naming a model source of its own — the literal `"default"` spells "no tier of my own", a `select` form counts as stated) > the **parent run's effective model** (the caller's current `Model` object, per-model routing and credentials included) > the bare `subagent` role (only reachable outside a Runner-enriched ctx). The cell that changed is the last resort: it used to be `FALLBACK.subagent → "default"`, i.e. the boot default — a run whose main turn ran on model B spawned children on boot model A, and a bare deployment (catalog, no roles) spawned children that died at prepare with `No model for role "subagent"`. Both now run on the parent's model. A deployment that stated a cheap tier keeps it (that statement is operator intent; children do not follow the parent there).
7
+ - **Role identity rides the seat.** Every non-explicit child spec carries `modelRole: "subagent"` beside the inherited `model`, so a deployment's `roles.subagent` `systemPrompt` / `thinking` preset still applies when the model is the parent's (the explicit-model branch of `resolveTaskModel` keys presets on the role).
8
+ - **Precedence note (definition lane).** The tool-level `model` (`createSubagentTool({ model })`) is an explicit seat and now outranks "inherit the parent" for a definition that names no model; it used to sit below the parent snapshot on that lane only. Fork children are unchanged (always the caller's model).
9
+ - **The workflow lane reads the same seat.** A model-less script child (`agent()` / `agentStream()`) used to take the host's `Model` unconditionally; the workflow mount's host-model getter now folds the deployment's stated tier first (`deploymentSubagentTierModel(runnerSelf.agentCatalog) ?? host model`, prepare-caps-and-workflow.ts — read off the LIVE catalog per call, so a `swapModels` between two spawns is seen by the second), so the Agent lanes and the workflow lane can no longer answer one deployment two ways. A script `model` and an `agentType` model keep outranking it (explicit), the allow-list keeps gating script words. A stated tier whose catalog ref is dead fails THAT call on the recorded path (failed agent record + `agent_end`, then the throw the script's own `catch` sees — the unknown-`agentType` shape; the next call's slot is released), never a substituted model. Both lanes' pre-launch catch now stamps an ordinary throw's message as the record's `errorMessage` (no machine code minted), so the row says why the ordinal has no child — previously only a review refusal or a budget stop named its cause.
10
+ - **The durable row names the inherited model.** A background child's record (`model`, the tier-3 revival key) records the seat's model — the parent's id for an inherited child, exactly what the child's own ticks say — so a revival from another parent keeps running the model the row was spawned on instead of silently moving to the reviver's; a stated-tier child's row stays blank as before (the role resolves it).
11
+ - **Read face (additive, @server @cli):** `Runner.agentCatalog` / `RunnerSelfSeat.agentCatalog` gains `roles?: ModelRoles` (a copy of `RunnerDeps.roles`) — the seat's reader. No wire change; no request-lane roles seat (the deployment/request boundary stands).
12
+ - **Engine side channels are unchanged by design** (`@contract roles.side_channels_deployment_only`): the auto-mode classifier, the compaction/summarize lane, memory consolidation and next-prompt suggestion resolve on the deployment roles alone and never take a run's model — they are deployment tools, not children of the run.
13
+ - Consumers: `@cli` the `_subagent-model-follows-run-live-pty.mjs` probe's `EXPECTED_VERDICT=b` door flips to `a` on this engine; `@server` zero code (`roles.subagent = "default"` when `MODEL_CHEAP_ID` is unset is exactly the "not stated" spelling).
14
+
15
+ ### Docs — the sub-run inheritance matrix (#643; @cli @server @test)
16
+ - `docs/SUBRUN-INHERITANCE.md`: every inheritance face (model, approver seat + park facility, posture + spawn review, inherited gate + ancestor rules, thinking, role fallback, budgets/TTLs, event-forwarding identity keys, principal + auth hook, session/run lineage, contentOrigin) × every child lane (Agent sync / background / workflow agents / fork·team·synthesize / scheduler wake-up / observer·verifier·advisor / resume-rebuilt / tier-3 revival), each cell "inherits / falls back to (file:line) / gated?". Appendix A lists every deployment-default fallback position with its verdict (design → contract sentence; defect → ticket); Appendix B lists every fail-closed deny mouth with who reaches it and where it is visible.
17
+ - **B-049 (#642) is DEFINED here, not fixed in this release** (see the matrix §B-049): a workflow-lane child whose inherited ask the host approver answers `unavailable` denies fail-closed exactly like a SYNC delegation child (pinned three-lane: `test/backlog642-child-ask-lanes.test.ts`); only the BACKGROUND lane parks, because only it is handed the parent's `durableApproval` (§7.3 park family). Making workflow children park is the same park family extended to the workflow row (`wa*` row parked state + the workflow's own pause cascade + a resume path) — a design slice, scheduled separately; the interim is the loud terminal (the deny sentence in the tool result and `tool_end.gate.settlement.kind = "approver_unavailable"`).
18
+
3
19
  ## 7.9.1 — 2026-09-08
4
20
 
5
21
  ### Wire (additive) — `mcp.injection_dropped`: the request-lane MCP drop reaches the user (L-167; @server @cli @client-core)
@@ -0,0 +1,51 @@
1
+ import type { Model, ModelRef, ModelRoles } from "../core/types.js";
2
+ /** The deployment role map behind the runner (`Runner.agentCatalog.roles`) — same posture as `runnerModelCatalog` (subagent.ts):
3
+ * host code, a throwing/absent stand-in reads as "no role map", never as a failed delegation. */
4
+ export declare function runnerRoleMap(runner: unknown): ModelRoles | undefined;
5
+ /**
6
+ * Did the deployment STATE a model for its subagents? True iff `roles.subagent` names a model source of its
7
+ * own: a string ref other than the literal `"default"`, a `Model` object, or a `{ model | select }` config
8
+ * whose `model` is not that literal. The word `"default"` is how a deployment spells "no subagent tier of my
9
+ * own — same as the main model" (a catalog key of that name is the boot default), so it reads as NOT stated;
10
+ * a role entry carrying only a `thinking` / `systemPrompt` preset states no model either.
11
+ */
12
+ export declare function deploymentStatesSubagentModel(roles: ModelRoles | undefined): boolean;
13
+ /**
14
+ * The deployment's stated subagent tier as a resolved `Model` — for a spawn lane that needs the concrete
15
+ * model up front (the workflow lane keys call identity and the display label on it), so both lanes read the
16
+ * same rule: `undefined` when the deployment stated nothing (the caller then falls to the parent's model),
17
+ * else the role chain's answer for `subagent`. A stated ref the catalog cannot resolve throws here, loudly,
18
+ * exactly where a child's own prepare would have thrown.
19
+ */
20
+ export declare function deploymentSubagentTierModel(deps: {
21
+ models?: Record<string, Model>;
22
+ roles?: ModelRoles;
23
+ } | undefined): Model | undefined;
24
+ /**
25
+ * The child run's model seat — ONE rule for every engine-spawned run (the delegation child at
26
+ * `buildChildSpec`, the observer sidecar): an EXPLICIT model for this spawn wins (per-call `model`, the
27
+ * agent definition's `model`, the tool-level `model`); else a deployment that STATED a subagent tier gets
28
+ * it (`{ modelRole: "subagent" }` — the role chain resolves the stated entry, `select` forms included);
29
+ * else the child runs on the PARENT run's effective model (the caller's current `Model` object, so
30
+ * per-model routing/credentials ride along); else — no parent model readable, which only happens outside
31
+ * a Runner-enriched ctx — the bare role, whose chain ends at the deployment default.
32
+ *
33
+ * The role identity `modelRole: "subagent"` stays on every non-explicit seat: the explicit-model branch of
34
+ * `resolveTaskModel` keys the role's `systemPrompt` / `thinking` presets on it, so a deployment's
35
+ * `roles.subagent` preset is not lost when the MODEL is inherited from the parent.
36
+ *
37
+ * Why "default" was the wrong last resort: `FALLBACK.subagent = ["default"]` resolves to the deployment's
38
+ * boot default, which is not the model the parent run was switched to — a run whose main turn was moved to
39
+ * model B by the caller spawned children on boot model A (and a deployment whose boot default was removed
40
+ * from the roster spawned children that failed at prepare). The parent's effective model is the honest
41
+ * default; the deployment's stated tier keeps outranking it because that statement is operator intent.
42
+ */
43
+ /** The seat: an explicit ref alone, or the role identity with an optional inherited Model (see {@link childModelSeat}). */
44
+ export type ChildModelSeat = {
45
+ model: ModelRef;
46
+ modelRole?: undefined;
47
+ } | {
48
+ modelRole: "subagent";
49
+ model?: Model;
50
+ };
51
+ export declare function childModelSeat(explicit: ModelRef | undefined, deploymentStated: boolean, parent: Model | undefined): ChildModelSeat;
@@ -0,0 +1,34 @@
1
+ import { resolveTaskModel } from "../core/roles.js";
2
+ export function runnerRoleMap(runner) {
3
+ try {
4
+ return runner?.agentCatalog?.roles;
5
+ }
6
+ catch {
7
+ return undefined;
8
+ }
9
+ }
10
+ export function deploymentStatesSubagentModel(roles) {
11
+ const s = roles?.subagent;
12
+ if (s == null)
13
+ return false;
14
+ if (typeof s === "string")
15
+ return s !== "default";
16
+ if ("id" in s && "api" in s)
17
+ return true;
18
+ const cfg = s;
19
+ if (cfg.model !== undefined)
20
+ return cfg.model !== "default";
21
+ return cfg.select !== undefined;
22
+ }
23
+ export function deploymentSubagentTierModel(deps) {
24
+ return deps !== undefined && deploymentStatesSubagentModel(deps.roles) ? resolveTaskModel({ modelRole: "subagent" }, deps).model : undefined;
25
+ }
26
+ export function childModelSeat(explicit, deploymentStated, parent) {
27
+ if (explicit !== undefined)
28
+ return { model: explicit };
29
+ if (deploymentStated)
30
+ return { modelRole: "subagent" };
31
+ if (parent !== undefined)
32
+ return { modelRole: "subagent", model: parent };
33
+ return { modelRole: "subagent" };
34
+ }
@@ -1,5 +1,6 @@
1
1
  import type { RunInternals, RunnerSelfSeat } from "../core/runner/contracts.js";
2
2
  import type { AgentDefinition, Model, ModelRef, TaskLimits, TerminalCause, ToolSpec } from "../core/types.js";
3
+ export { deploymentSubagentTierModel } from "./child-model-seat.js";
3
4
  import { type ExecutionEnv } from "../internal/harness.js";
4
5
  import type { TaskNotificationPayload } from "../core/task-notification.js";
5
6
  import { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX } from "../config/defaults.js";
@@ -679,12 +680,13 @@ export interface SubagentToolOptions {
679
680
  */
680
681
  ensureChildSessionDurable?: (sessionId: string) => Promise<void>;
681
682
  };
682
- /** Model the child runs on. If omitted, the child resolves the `subagent` role ( `default`). */
683
+ /** Model the child runs on when neither the call nor the agent definition names one (the tool-level explicit seat).
684
+ * Omitted too ⇒ the deployment's stated `roles.subagent` tier, else the parent run's effective model (`childModelSeat`). */
683
685
  model?: ModelRef;
684
686
  /**
685
687
  * Declarative sub-agents the model can pick from (design/38 1B). When set, the tool exposes an `agent`
686
688
  * parameter (enum of these names) and the chosen definition's config builds the child task — its
687
- * `allowTools`/`denyTools` REPLACE the tool-level ones, `model` omitted = inherit the caller's model.
689
+ * `allowTools`/`denyTools` REPLACE the tool-level ones, `model` omitted = tool-level `model` > stated `roles.subagent` tier > the caller's model (`childModelSeat`).
688
690
  * design/141 件A: when OMITTED, defaults to the runner's deployment catalog (`RunnerDeps.agents`) —
689
691
  * one Runner, one registry across the Agent-tool and workflow lanes. Pass explicitly to narrow.
690
692
  */
@@ -7,6 +7,8 @@ import { newDelegationProvenanceAggregate, reduceDelegationAttestation } from ".
7
7
  import { registerDelegationLaunch, replayExternalSettlementEffects, settleDelegation } from "../core/memory-engine/delegation-settlement.js";
8
8
  import { enqueueMemoryAnnouncement } from "../core/memory-engine/layout.js";
9
9
  import { resolveModel, resolveModelDisplayLabel } from "../core/roles.js";
10
+ import { childModelSeat, deploymentStatesSubagentModel, runnerRoleMap } from "./child-model-seat.js";
11
+ export { deploymentSubagentTierModel } from "./child-model-seat.js";
10
12
  import { OUTPUT_TOOL_NAME, REPORT_BLOCKED_TOOL_NAME } from "../core/runner/synthetic-tools.js";
11
13
  import { TOOL_SEARCH_NAME } from "../core/runner/tool-disclosure.js";
12
14
  import { OFFLOAD_TOOL_NAME } from "../core/tool-result-store.js";
@@ -1466,7 +1468,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1466
1468
  })),
1467
1469
  model: Type.Optional(Type.String({
1468
1470
  description: rosterNames !== undefined && rosterNames.length > 0
1469
- ? `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.`
1471
+ ? `Optional model override for this agent. Takes precedence over the agent definition's model. If omitted, uses the agent definition's model, else the deployment's subagent tier, else 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.`
1470
1472
  : `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}".`,
1471
1473
  })),
1472
1474
  isolation: Type.Optional(Type.Literal("worktree", {
@@ -1784,9 +1786,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1784
1786
  const childTools = def
1785
1787
  ? resolveToolSubset(childPool, def.allowTools, def.denyTools)
1786
1788
  : resolveToolSubset(childPool, opts.allowTools, opts.denyTools);
1787
- let childModel = wantsFork
1788
- ? ctx.model ?? opts.model
1789
- : perCallModel ?? (def ? (def.model ?? ctx.model ?? opts.model) : opts.model);
1789
+ const explicitModelChain = wantsFork ? [ctx.model, opts.model] : [perCallModel, def?.model, opts.model];
1790
+ let childModel = explicitModelChain.find((c) => c !== undefined);
1791
+ const deploymentStatedSubagentModel = deploymentStatesSubagentModel(runnerRoleMap(opts.runner));
1790
1792
  if (reviveClaim !== undefined && typeof childModel === "string") {
1791
1793
  const resolvable = (ref) => {
1792
1794
  try {
@@ -1799,24 +1801,26 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1799
1801
  };
1800
1802
  if (!resolvable(childModel)) {
1801
1803
  const dead = childModel;
1802
- const rest = def !== undefined ? [ctx.model, opts.model] : [opts.model];
1804
+ const rest = explicitModelChain.slice(explicitModelChain.indexOf(dead) + 1);
1803
1805
  childModel = rest.find((c) => c !== undefined && (typeof c !== "string" || resolvable(c)));
1804
1806
  if (modelNote === undefined) {
1805
1807
  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.`;
1806
1808
  }
1807
1809
  }
1808
1810
  }
1809
- const spawnModel = typeof childModel === "string"
1811
+ const childSeat = childModelSeat(childModel, deploymentStatedSubagentModel, ctx.model);
1812
+ const seatModel = childSeat.model;
1813
+ const spawnModel = typeof seatModel === "string"
1810
1814
  ? (() => {
1811
1815
  try {
1812
- return resolveModel(childModel, judgedModels).id;
1816
+ return resolveModel(seatModel, judgedModels).id;
1813
1817
  }
1814
1818
  catch {
1815
- return resolveModelDisplayLabel(childModel);
1819
+ return resolveModelDisplayLabel(seatModel);
1816
1820
  }
1817
1821
  })()
1818
- : childModel?.id;
1819
- const rowModelKey = perCallModel !== undefined ? requestedModel ?? perCallModel.id : typeof childModel === "string" ? childModel : childModel?.id;
1822
+ : seatModel?.id;
1823
+ const rowModelKey = perCallModel !== undefined ? requestedModel ?? perCallModel.id : typeof seatModel === "string" ? seatModel : seatModel?.id;
1820
1824
  const childSystemPrompt = def?.systemPrompt ?? opts.systemPrompt;
1821
1825
  const childDefaultPersona = childSystemPrompt === undefined && !wantsFork ? SUBAGENT_PROMPT : undefined;
1822
1826
  const defMaxTurns = typeof def?.maxTurns === "number" && Number.isFinite(def.maxTurns) && def.maxTurns > 0 ? def.maxTurns : undefined;
@@ -1888,7 +1892,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1888
1892
  sessionId: sid,
1889
1893
  ...(resume ? { requireExistingSession: true } : {}),
1890
1894
  objective: objectiveText,
1891
- ...(ctx.model !== undefined ? { model: ctx.model } : { modelRole: "subagent" }),
1895
+ ...childModelSeat(undefined, deploymentStatesSubagentModel(runnerRoleMap(opts.runner)), ctx.model),
1892
1896
  ...(observerDef.systemPrompt !== undefined && !resume ? { systemPrompt: observerDef.systemPrompt } : {}),
1893
1897
  ...(ctx.principal !== undefined ? { principal: ctx.principal } : {}),
1894
1898
  ...(ctx.getApiKeyAndHeaders !== undefined ? { getApiKeyAndHeaders: ctx.getApiKeyAndHeaders } : {}),
@@ -2140,7 +2144,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2140
2144
  : undefined;
2141
2145
  const buildChildSpec = (signal) => ({
2142
2146
  objective: prompt,
2143
- ...(childModel ? { model: childModel } : { modelRole: "subagent" }),
2147
+ ...childSeat,
2144
2148
  ...(childThinking !== undefined ? { thinking: childThinking } : {}),
2145
2149
  tools: childTools,
2146
2150
  systemPrompt: childSystemPrompt,
@@ -98,6 +98,14 @@ export interface ResolvedRole {
98
98
  * Resolve the model (and default thinking) for a task. Explicit `spec.model` wins; otherwise walk the
99
99
  * `modelRole`'s fallback chain over the merged roles (task overrides runner) and use the first role
100
100
  * that supplies a model. Throws a helpful error if nothing resolves.
101
+ *
102
+ * @contract roles.side_channels_deployment_only — an engine SIDE CHANNEL (the auto-mode classifier,
103
+ * the compaction/summarize lane, memory consolidation, next-prompt suggestion) resolves here with a
104
+ * `modelRole` and NO run model: it runs on the deployment's roles alone — the boot default at the end of
105
+ * its chain — and never follows the model a run was switched to. Only a run's CHILDREN follow the parent:
106
+ * the delegation seat (`childModelSeat`) passes the parent's `Model` as `spec.model` with the role kept
107
+ * on `modelRole`, so the role's presets still apply. A side channel that must follow a run passes that
108
+ * run's model explicitly; nothing here infers it.
101
109
  */
102
110
  export declare function resolveTaskModel(spec: {
103
111
  model?: ModelRef;
@@ -43,7 +43,7 @@ import type { SystemInjectionPriority, TaskNotificationPayload } from "../task-n
43
43
  import type { OnAsk, PermissionResult, ResolvedAsk, ToolCallRequest, ToolPolicy } from "../tool-policy.js";
44
44
  import type { RemoteExecutionEnv, SnapshotId, WorkspaceHandle } from "../remote-env.js";
45
45
  import type { ToolDisclosureManifest } from "../trace.js";
46
- import type { AgentDefinition, Brain, CompactOutcome, NestedUsageAccum, RunnerDeps, TaskEvent, TaskResult, TaskSpec, TaskStream, ToolActivity, ToolEffect } from "../types.js";
46
+ import type { AgentDefinition, Brain, CompactOutcome, ModelRoles, NestedUsageAccum, RunnerDeps, TaskEvent, TaskResult, TaskSpec, TaskStream, ToolActivity, ToolEffect } from "../types.js";
47
47
  import type { WiringManifest } from "../wiring-manifest.js";
48
48
  import type { ActiveSkillFrame } from "./active-skill-scope.js";
49
49
  import type { BudgetAxis } from "./assemble-result.js";
@@ -2377,11 +2377,12 @@ export interface RunnerDepsSeat {
2377
2377
  export interface RunnerSelfSeat {
2378
2378
  /** The session store this Runner was constructed over — the delegation tools acquire / release / fork through it. */
2379
2379
  readonly sessions: SessionStore;
2380
- /** The deployment agent + model catalog, READ-ONLY (a fresh copy per read; the definitions are the shared objects). */
2380
+ /** The deployment agent + model catalog, READ-ONLY (a fresh copy per read; the definitions are the shared objects); `roles` = `RunnerDeps.roles` (the delegation seat's `childModelSeat` reads `roles.subagent`). */
2381
2381
  readonly agentCatalog: {
2382
2382
  agents?: AgentDefinition[];
2383
2383
  builtinAgents?: boolean;
2384
2384
  models?: Record<string, Model>;
2385
+ roles?: ModelRoles;
2385
2386
  };
2386
2387
  /** The deployment-level gating baseline (deps toolPolicy / hooks) as delegate wrappers — for assemblers that compose with it. */
2387
2388
  readonly gateBaseline: {
@@ -1,6 +1,6 @@
1
1
  import { isSyntheticApiErrorMessage } from "../../internal/harness.js";
2
2
  import { createHash } from "node:crypto";
3
- import { forkGovernanceDenial } from "../../agents/subagent.js";
3
+ import { deploymentSubagentTierModel, forkGovernanceDenial } from "../../agents/subagent.js";
4
4
  import { CROSS_SESSION_CLASSIFIER_RULE } from "../../agents/cross-session-envelope.js";
5
5
  import { createRunWorkflowTool } from "../../orchestration/run-workflow-tool.js";
6
6
  import { isSelfOrchestrationActive } from "../../orchestration/workflow-script-runner.js";
@@ -243,7 +243,7 @@ export async function prepareCapsAndWorkflow(input) {
243
243
  taskOwner: hostTaskId,
244
244
  limits: deps.workflowLimits,
245
245
  sourceTaskId: hostTaskId,
246
- parentModel: () => harnessRef.current?.getModel(),
246
+ parentModel: () => deploymentSubagentTierModel(runnerSelf.agentCatalog) ?? harnessRef.current?.getModel(),
247
247
  ...(spec.getApiKeyAndHeaders !== undefined ? { parentGetApiKeyAndHeaders: spec.getApiKeyAndHeaders } : {}),
248
248
  ...(frozenOnAsk !== undefined ? { parentOnAsk: frozenOnAsk } : {}),
249
249
  principal: spec.principal,
@@ -3,7 +3,7 @@ import { type CheckpointToken, type ResumeOutcome } from "../checkpoint-store.js
3
3
  import { type TaskOutcome } from "../task-outcome.js";
4
4
  import { type SideQuerySpec, type SideQueryResult } from "../side-query.js";
5
5
  import type { SessionStore } from "../session.js";
6
- import type { AgentDefinition, ModelRef, RunnerDeps, TaskEvent, TaskResult, TaskSpec, TaskStream } from "../types.js";
6
+ import type { AgentDefinition, ModelRef, ModelRoles, RunnerDeps, TaskEvent, TaskResult, TaskSpec, TaskStream } from "../types.js";
7
7
  import type { ResumeRun, ResumeTaskConfig, RunInternals, RunnerSelfSeat } from "./contracts.js";
8
8
  export type { ResumeTaskConfig } from "./contracts.js";
9
9
  /**
@@ -74,6 +74,7 @@ export declare class Runner implements RunnerSelfSeat {
74
74
  agents?: AgentDefinition[];
75
75
  builtinAgents?: boolean;
76
76
  models?: Record<string, Model>;
77
+ roles?: ModelRoles;
77
78
  };
78
79
  /** design/125 (实现审 B1) — the deployment-level gating baseline, exposed READ-ONLY so thin
79
80
  * assemblers over the Runner (`runSpec`) can COMPOSE with it. The task-level engine semantic is a
@@ -138,7 +138,7 @@ export class Runner {
138
138
  return {
139
139
  ...(this.deps.agents ? { agents: [...this.deps.agents] } : {}),
140
140
  ...(this.deps.builtinAgents !== undefined ? { builtinAgents: this.deps.builtinAgents } : {}),
141
- ...(this.deps.models ? { models: { ...this.deps.models } } : {}),
141
+ ...(this.deps.models ? { models: { ...this.deps.models } } : {}), ...(this.deps.roles ? { roles: { ...this.deps.roles } } : {}),
142
142
  };
143
143
  }
144
144
  get gateBaseline() {
@@ -1028,13 +1028,22 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1028
1028
  const phaseInstance = resolveAgentPhase(agentOpts.phase);
1029
1029
  const groupId = currentGroup;
1030
1030
  const typeDefModel = agentOpts.agentType !== undefined ? agentRegistry.find((d) => d.name === agentOpts.agentType)?.model : undefined;
1031
- const inheritedModelSnap = spec.model === undefined && typeDefModel === undefined ? opts.defaultModel?.() : undefined;
1031
+ let inheritedModelSnap;
1032
+ let inheritedModelError;
1033
+ if (spec.model === undefined && typeDefModel === undefined) {
1034
+ try {
1035
+ inheritedModelSnap = opts.defaultModel?.();
1036
+ }
1037
+ catch (err) {
1038
+ inheritedModelError = err instanceof Error ? err : new Error(String(err));
1039
+ }
1040
+ }
1032
1041
  const specForIdentity = inheritedModelSnap !== undefined ? { ...spec, model: inheritedModelSnap } : spec;
1033
1042
  const callKey = workflowAgentCallKey(run.agents.length, specForIdentity, agentOpts);
1034
1043
  const prompt = boundedRedactedSummary(spec.systemPrompt ? `${spec.systemPrompt}\n\n${spec.objective}` : spec.objective, MAX_TRANSCRIPT_CHARS);
1035
1044
  const specForLabel = spec.model === undefined && typeDefModel !== undefined ? { ...spec, model: typeDefModel } : specForIdentity;
1036
1045
  const model = workflowModelLabel(specForLabel, modelCatalog);
1037
- return { label, phase, phaseInstance, groupId, inheritedModelSnap, callKey, prompt, model };
1046
+ return { label, phase, phaseInstance, groupId, inheritedModelSnap, inheritedModelError, callKey, prompt, model };
1038
1047
  };
1039
1048
  const reviewSpawnBeforeLaunch = async (lane, label, callKey, runSpec, effectiveSignal) => {
1040
1049
  const review = opts.autoModeReview;
@@ -1181,7 +1190,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1181
1190
  },
1182
1191
  async agent(spec, agentOpts = {}) {
1183
1192
  const effectiveSignal = assertAgentSpawnAllowed("ctx.agent", spec, agentOpts);
1184
- const { label, phase, phaseInstance, groupId, inheritedModelSnap, callKey, prompt, model } = prepareAgentCall(spec, agentOpts);
1193
+ const { label, phase, phaseInstance, groupId, inheritedModelSnap, inheritedModelError, callKey, prompt, model } = prepareAgentCall(spec, agentOpts);
1185
1194
  if (opts.resumeFromRunId !== undefined && !diverged) {
1186
1195
  const cached = replayByOrdinal[run.agents.length];
1187
1196
  if (cached && cached.callKey === callKey && TERMINAL_CAUSE_IS_REPLAYABLE[cached.result.terminal.kind]) {
@@ -1265,6 +1274,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1265
1274
  rec.startedAt = now();
1266
1275
  const bornChildSessionId = resolveChildSessionIdAtSpawn(spec);
1267
1276
  bceSpawn(callKey, label, agentOpts.agentType, false, bornChildSessionId, model);
1277
+ if (inheritedModelError !== undefined)
1278
+ throw inheritedModelError;
1268
1279
  const typedSpec0 = applyWorkflowAgentType(spec, agentOpts.agentType, agentRegistry);
1269
1280
  const typedSpec = typedSpec0.model === undefined && inheritedModelSnap !== undefined ? { ...typedSpec0, model: inheritedModelSnap } : typedSpec0;
1270
1281
  const framedSpec = withWorkflowChildPersona(typedSpec, agentOpts.schema ?? typedSpec.outputSchema);
@@ -1490,6 +1501,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1490
1501
  rec.errorCode = err.code;
1491
1502
  rec.errorMessage = boundedRedactedSummary(err.message, MAX_TRANSCRIPT_CHARS);
1492
1503
  }
1504
+ if (rec.errorMessage === undefined && err instanceof Error && err.message !== "")
1505
+ rec.errorMessage = boundedRedactedSummary(err.message, MAX_TRANSCRIPT_CHARS);
1493
1506
  if (err instanceof WorkflowAgentStalledError && err.attempts > 1) {
1494
1507
  rec.attempts = err.attempts;
1495
1508
  rec.lastAttemptReason = "stalled";
@@ -1529,7 +1542,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1529
1542
  if (budgetTotal !== null && spent() >= budgetTotal) {
1530
1543
  throw new WorkflowBudgetExceededError(spent(), budgetTotal);
1531
1544
  }
1532
- const { label, phase, phaseInstance, groupId, inheritedModelSnap, callKey, prompt, model } = prepareAgentCall(spec, agentOpts);
1545
+ const { label, phase, phaseInstance, groupId, inheritedModelSnap, inheritedModelError, callKey, prompt, model } = prepareAgentCall(spec, agentOpts);
1533
1546
  noteDivergence(run.agents.length, "ctx.agentStream results are never replayed");
1534
1547
  diverged = true;
1535
1548
  const rec = { label, callKey, ...(groupId !== undefined ? { groupId } : {}), phase, prompt, ...(model !== undefined ? { model } : {}), status: "running", queuedAt: now() };
@@ -1581,6 +1594,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1581
1594
  bceSpawn(callKey, label, agentOpts.agentType, false, childSessionId, model);
1582
1595
  let stream;
1583
1596
  try {
1597
+ if (inheritedModelError !== undefined)
1598
+ throw inheritedModelError;
1584
1599
  const typedSpec0 = applyWorkflowAgentType(spec, agentOpts.agentType, agentRegistry);
1585
1600
  const typedSpec = typedSpec0.model === undefined && inheritedModelSnap !== undefined ? { ...typedSpec0, model: inheritedModelSnap } : typedSpec0;
1586
1601
  const framedSpec = withWorkflowChildPersona(typedSpec, agentOpts.schema ?? typedSpec.outputSchema);
@@ -1622,6 +1637,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1622
1637
  stream = workflowDepthStore.run({ depth: depth + 1 }, () => runner.runTaskStream(runSpec, undefined, runInternals));
1623
1638
  }
1624
1639
  catch (err) {
1640
+ if (!finalized && rec.errorMessage === undefined && err instanceof Error && err.message !== "")
1641
+ rec.errorMessage = boundedRedactedSummary(err.message, MAX_TRANSCRIPT_CHARS);
1625
1642
  if (!finalized && err instanceof WorkflowAgentBlockedError) {
1626
1643
  rec.errorCode = WORKFLOW_SPAWN_BLOCKED_ERROR_CODE;
1627
1644
  rec.errorMessage = boundedRedactedSummary(err.message, MAX_TRANSCRIPT_CHARS);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.9.1",
3
+ "version": "7.9.2",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",