authorgravatar for 88523594+RakshithBhat03@users.noreply.github.comRakshith Bhat <88523594+RakshithBhat03@users.noreply.github.com> 2026-10-10 18:31:50+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-10 08:01:50-05:00
log98beed1a226c42b85c52ff5ee9d5dbb67d776a77
tree557c3447771d1bf5b9ba198161c3f7e52e0c50ff
parent94d1a8e93f0434d34fa894523a04fe6a6f6172f6
signature Signed by PGP key B5690EEEBB952194

fix(server): Claude subagents show the reasoning effort they run at (#17496)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

7 files changed, 530 insertions(+), 29 deletions(-)

apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts+369-4
...@@ -48,6 +48,7 @@ import * as TestClock from "effect/testing/TestClock";...@@ -48,6 +48,7 @@ import * as TestClock from "effect/testing/TestClock";
48import { Tool } from "effect/ai";48import { Tool } from "effect/ai";
49import { formatClaudeResumeCompactionQuestion } from "@t3tools/shared/claudeCompaction";49import { formatClaudeResumeCompactionQuestion } from "@t3tools/shared/claudeCompaction";
50import * as HostProcess from "@t3tools/shared/HostProcess";50import * as HostProcess from "@t3tools/shared/HostProcess";
51import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";
51import { SpawnExecutableResolution } from "@t3tools/shared/shell";52import { SpawnExecutableResolution } from "@t3tools/shared/shell";
5253
53import { attachmentRelativePath } from "../../attachmentStore.ts";54import { attachmentRelativePath } from "../../attachmentStore.ts";
...@@ -73,6 +74,29 @@ const DEFAULT_CLAUDE_SETTINGS = Schema.decodeSync(ClaudeSettings)({});...@@ -73,6 +74,29 @@ const DEFAULT_CLAUDE_SETTINGS = Schema.decodeSync(ClaudeSettings)({});
73const AUTO_COMPACT_CLAUDE_SETTINGS = Schema.decodeSync(ClaudeSettings)({74const AUTO_COMPACT_CLAUDE_SETTINGS = Schema.decodeSync(ClaudeSettings)({
74 autoCompactWindow: "300000",75 autoCompactWindow: "300000",
75});76});
77const CUSTOM_MODEL_CLAUDE_SETTINGS = Schema.decodeSync(ClaudeSettings)({
78 customModels: [
79 // A bare custom slug that shadows the built-in "opus" alias.
80 "opus",
81 {
82 slug: "claude-custom-tuned",
83 name: "Tuned",
84 capabilities: {
85 optionDescriptors: [
86 {
87 id: "effort",
88 label: "Reasoning",
89 type: "select",
90 options: [
91 { id: "gentle", label: "Gentle", isDefault: true },
92 { id: "brutal", label: "Brutal" },
93 ],
94 },
95 ],
96 },
97 },
98 ],
99});
76const CLAUDE_TEST_MODEL_SELECTION = {100const CLAUDE_TEST_MODEL_SELECTION = {
77 instanceId: ProviderInstanceId.make(ClaudeAdapterV2.CLAUDE_PROVIDER),101 instanceId: ProviderInstanceId.make(ClaudeAdapterV2.CLAUDE_PROVIDER),
78 model: "claude-sonnet-4-6",102 model: "claude-sonnet-4-6",
...@@ -2107,6 +2131,7 @@ describe("ClaudeAdapterV2 background wake turns", () => {...@@ -2107,6 +2131,7 @@ describe("ClaudeAdapterV2 background wake turns", () => {
2107 readonly close?: (sdkMessages: Queue.Queue<SDKMessage>) => Effect.Effect<void>;2131 readonly close?: (sdkMessages: Queue.Queue<SDKMessage>) => Effect.Effect<void>;
2108 readonly interrupt?: Effect.Effect<void>;2132 readonly interrupt?: Effect.Effect<void>;
2109 readonly environment?: NodeJS.ProcessEnv;2133 readonly environment?: NodeJS.ProcessEnv;
2134 readonly settings?: ClaudeSettings;
2110 // A CLI process opened after the first streams from its own queue, so the2135 // A CLI process opened after the first streams from its own queue, so the
2111 // first one can exit (Queue.shutdown) and a later turn can start another.2136 // first one can exit (Queue.shutdown) and a later turn can start another.
2112 readonly freshQueueOnReopen?: boolean;2137 readonly freshQueueOnReopen?: boolean;
...@@ -2141,7 +2166,7 @@ describe("ClaudeAdapterV2 background wake turns", () => {...@@ -2141,7 +2166,7 @@ describe("ClaudeAdapterV2 background wake turns", () => {
2141 let openedOptions: ClaudeAdapterV2.ClaudeAgentSdkQueryOptions | undefined;2166 let openedOptions: ClaudeAdapterV2.ClaudeAgentSdkQueryOptions | undefined;
2142 const adapter = yield* ClaudeAdapterV2.makeClaudeAdapterV2({2167 const adapter = yield* ClaudeAdapterV2.makeClaudeAdapterV2({
2143 instanceId: ClaudeAdapterV2.CLAUDE_DEFAULT_INSTANCE_ID,2168 instanceId: ClaudeAdapterV2.CLAUDE_DEFAULT_INSTANCE_ID,
2144 settings: DEFAULT_CLAUDE_SETTINGS,2169 settings: options?.settings ?? DEFAULT_CLAUDE_SETTINGS,
2145 environment: options?.environment ?? {},2170 environment: options?.environment ?? {},
2146 attachmentsDir,2171 attachmentsDir,
2147 fileSystem,2172 fileSystem,
...@@ -8139,18 +8164,20 @@ describe("ClaudeAdapterV2 background wake turns", () => {...@@ -8139,18 +8164,20 @@ describe("ClaudeAdapterV2 background wake turns", () => {
8139 );8164 );
8140 yield* Queue.take(harness.terminalReceipts);8165 yield* Queue.take(harness.terminalReceipts);
8141 const subagents = harness.events.filter((event) => event.type === "subagent.updated");8166 const subagents = harness.events.filter((event) => event.type === "subagent.updated");
8167 // Aliases and dated ids are recorded as the catalog slug.
8168 const observedSlug = "claude-haiku-4-5";
8142 const initialModel =8169 const initialModel =
8143 source === "observed-before"8170 source === "observed-before"
8144 ? observedModel8171 ? observedSlug
8145 : source === "inherit"8172 : source === "inherit"
8146 ? parentModel8173 ? parentModel
8147 : source === "unknown"8174 : source === "unknown"
8148 ? null8175 ? null
8149 : "haiku";8176 : "claude-haiku-5-5";
8150 assert.equal(subagents[0]?.subagent.model, initialModel);8177 assert.equal(subagents[0]?.subagent.model, initialModel);
8151 assert.equal(8178 assert.equal(
8152 subagents.at(-1)?.subagent.model,8179 subagents.at(-1)?.subagent.model,
8153 source.startsWith("observed") ? observedModel : initialModel,8180 source.startsWith("observed") ? observedSlug : initialModel,
8154 );8181 );
8155 const child = harness.events.find((event) => event.type === "app_thread.created");8182 const child = harness.events.find((event) => event.type === "app_thread.created");
8156 assert.equal(child?.appThread.modelSelection?.model, initialModel ?? parentModel);8183 assert.equal(child?.appThread.modelSelection?.model, initialModel ?? parentModel);
...@@ -8162,6 +8189,344 @@ describe("ClaudeAdapterV2 background wake turns", () => {...@@ -8162,6 +8189,344 @@ describe("ClaudeAdapterV2 background wake turns", () => {
8162 ),8189 ),
8163 );8190 );
81648191
8192 it.effect.each([
8193 { agentModel: "opus", slug: "claude-opus-5-5", requested: undefined, expected: "max" },
8194 { agentModel: "opus", slug: "claude-opus-5-5", requested: "low", expected: "low" },
8195 // claude-haiku-4-5 offers no effort, so none is sent or shown for it.
8196 { agentModel: "haiku-4.5", slug: "claude-haiku-4-5", requested: undefined, expected: null },
8197 ])(
8198 "records the effort a $agentModel subagent runs at (Agent effort $requested)",
8199 ({ agentModel, slug, requested, expected }) =>
8200 Effect.scoped(
8201 Effect.gen(function* () {
8202 const harness = yield* makeWakeHarness;
8203 const now = yield* DateTime.now;
8204 const toolUseId = "toolu-subagent-effort";
8205 const parentSelection = {
8206 instanceId: CLAUDE_TEST_MODEL_SELECTION.instanceId,
8207 model: "claude-opus-4-6",
8208 options: [
8209 { id: "effort", value: "max" },
8210 { id: "fastMode", value: false },
8211 ],
8212 } satisfies ModelSelection;
8213 yield* harness.runtime.startTurn(
8214 makeClaudeTestTurnInput({
8215 threadId: harness.threadId,
8216 providerThread: harness.providerThread,
8217 now,
8218 attemptId: RunAttemptId.make("attempt-subagent-effort"),
8219 text: "Spawn a subagent.",
8220 attachments: [],
8221 modelSelection: parentSelection,
8222 }),
8223 );
8224 for (const frame of [
8225 claudeSdkFrame({
8226 type: "assistant",
8227 parent_tool_use_id: null,
8228 message: {
8229 model: parentSelection.model,
8230 id: "msg_subagent_effort_launch",
8231 type: "message",
8232 role: "assistant",
8233 content: [
8234 {
8235 type: "tool_use",
8236 id: toolUseId,
8237 name: "Agent",
8238 input: {
8239 description: "Research",
8240 subagent_type: "general-purpose",
8241 model: agentModel,
8242 ...(requested === undefined ? {} : { effort: requested }),
8243 prompt: "Research the topic.",
8244 },
8245 },
8246 ],
8247 },
8248 uuid: "00000000-0000-4000-8000-000000000211",
8249 session_id: WAKE_NATIVE_SESSION,
8250 }),
8251 claudeSdkFrame({
8252 type: "system",
8253 subtype: "task_started",
8254 task_id: "task-subagent-effort",
8255 tool_use_id: toolUseId,
8256 description: "Research",
8257 task_type: "local_agent",
8258 uuid: "00000000-0000-4000-8000-000000000212",
8259 session_id: WAKE_NATIVE_SESSION,
8260 }),
8261 // The reply names the model the alias resolved to.
8262 claudeSdkFrame({
8263 type: "assistant",
8264 parent_tool_use_id: toolUseId,
8265 message: {
8266 model: slug,
8267 id: "msg_subagent_effort_observed",
8268 type: "message",
8269 role: "assistant",
8270 content: [{ type: "text", text: "Researching." }],
8271 },
8272 uuid: "00000000-0000-4000-8000-000000000213",
8273 session_id: WAKE_NATIVE_SESSION,
8274 }),
8275 makeResultFrame({
8276 uuid: "00000000-0000-4000-8000-000000000214",
8277 result: "Spawned the subagent.",
8278 }),
8279 ]) {
8280 yield* Queue.offer(harness.sdkMessages, frame);
8281 }
8282 yield* Queue.take(harness.terminalReceipts);
8283
8284 // The alias is stored as its slug, so the reply is no model change,
8285 // and the parent's fast mode stays with the parent.
8286 const subagentSelection = {
8287 instanceId: parentSelection.instanceId,
8288 model: slug,
8289 ...(expected === null ? {} : { options: [{ id: "effort", value: expected }] }),
8290 };
8291 const child = harness.events.find((event) => event.type === "app_thread.created");
8292 assert.deepEqual(child?.appThread.modelSelection, subagentSelection);
8293 const subagents = harness.events.filter((event) => event.type === "subagent.updated");
8294 assert.deepEqual(
8295 subagents.map((event) => event.subagent.modelSelection),
8296 subagents.map(() => subagentSelection),
8297 );
8298 }).pipe(
8299 Effect.provide(
8300 Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
8301 ),
8302 ),
8303 ),
8304 );
8305
8306 it.effect("resolves a subagent's model and effort against the instance's custom models", () =>
8307 Effect.scoped(
8308 Effect.gen(function* () {
8309 const TUNED_TOOL_USE_ID = "toolu-effort-custom-tuned";
8310 const SHADOW_TOOL_USE_ID = "toolu-effort-custom-shadow";
8311 const harness = yield* makeWakeHarnessWithOptions({
8312 settings: CUSTOM_MODEL_CLAUDE_SETTINGS,
8313 });
8314 const now = yield* DateTime.now;
8315 yield* harness.runtime.startTurn(
8316 makeClaudeTestTurnInput({
8317 threadId: harness.threadId,
8318 providerThread: harness.providerThread,
8319 now,
8320 attemptId: RunAttemptId.make("attempt-subagent-custom-effort"),
8321 text: "Spawn subagents on custom models.",
8322 attachments: [],
8323 }),
8324 );
8325 for (const frame of [
8326 claudeSdkFrame({
8327 type: "assistant",
8328 parent_tool_use_id: null,
8329 message: {
8330 model: CLAUDE_TEST_MODEL_SELECTION.model,
8331 id: "msg_custom_effort_launch",
8332 type: "message",
8333 role: "assistant",
8334 content: [
8335 {
8336 type: "tool_use",
8337 id: TUNED_TOOL_USE_ID,
8338 name: "Agent",
8339 input: {
8340 description: "Tuned",
8341 subagent_type: "general-purpose",
8342 model: "claude-custom-tuned",
8343 effort: "brutal",
8344 prompt: "Check one thing.",
8345 },
8346 },
8347 {
8348 type: "tool_use",
8349 id: SHADOW_TOOL_USE_ID,
8350 name: "Agent",
8351 input: {
8352 description: "Shadow",
8353 subagent_type: "general-purpose",
8354 model: "opus",
8355 prompt: "Check another thing.",
8356 },
8357 },
8358 ],
8359 },
8360 uuid: "00000000-0000-4000-8000-000000000231",
8361 session_id: WAKE_NATIVE_SESSION,
8362 }),
8363 ...[TUNED_TOOL_USE_ID, SHADOW_TOOL_USE_ID].map((id, index) =>
8364 claudeSdkFrame({
8365 type: "system",
8366 subtype: "task_started",
8367 task_id: `task-${id}`,
8368 tool_use_id: id,
8369 description: id,
8370 task_type: "local_agent",
8371 uuid: `00000000-0000-4000-8000-00000000023${index + 2}`,
8372 session_id: WAKE_NATIVE_SESSION,
8373 }),
8374 ),
8375 makeResultFrame({
8376 uuid: "00000000-0000-4000-8000-000000000234",
8377 result: "Spawned the subagents.",
8378 }),
8379 ]) {
8380 yield* Queue.offer(harness.sdkMessages, frame);
8381 }
8382 yield* Queue.take(harness.terminalReceipts);
8383
8384 const selections = Object.fromEntries(
8385 harness.events.flatMap((event) =>
8386 event.type === "subagent.updated"
8387 ? [[event.subagent.nativeTaskRef?.nativeId, event.subagent.modelSelection] as const]
8388 : [],
8389 ),
8390 );
8391 const instanceId = CLAUDE_TEST_MODEL_SELECTION.instanceId;
8392 assert.deepEqual(selections, {
8393 // The custom model's own effort levels apply.
8394 [`task-${TUNED_TOOL_USE_ID}`]: {
8395 instanceId,
8396 model: "claude-custom-tuned",
8397 options: [{ id: "effort", value: "brutal" }],
8398 },
8399 // The custom slug is not resolved to the built-in model it shadows.
8400 [`task-${SHADOW_TOOL_USE_ID}`]: { instanceId, model: "opus" },
8401 });
8402 }).pipe(
8403 Effect.provide(
8404 Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
8405 ),
8406 ),
8407 ),
8408 );
8409
8410 it.effect("gives a nested subagent its own effort, else the one its owner runs at", () =>
8411 Effect.scoped(
8412 Effect.gen(function* () {
8413 const OUTER_TOOL_USE_ID = "toolu-effort-outer";
8414 const INHERITING_TOOL_USE_ID = "toolu-effort-nested-inheriting";
8415 const OVERRIDING_TOOL_USE_ID = "toolu-effort-nested-overriding";
8416 const harness = yield* makeWakeHarness;
8417 const now = yield* DateTime.now;
8418 const parentSelection = {
8419 instanceId: CLAUDE_TEST_MODEL_SELECTION.instanceId,
8420 model: "claude-opus-4-6",
8421 options: [{ id: "effort", value: "high" }],
8422 } satisfies ModelSelection;
8423 yield* harness.runtime.startTurn(
8424 makeClaudeTestTurnInput({
8425 threadId: harness.threadId,
8426 providerThread: harness.providerThread,
8427 now,
8428 attemptId: RunAttemptId.make("attempt-subagent-nested-effort"),
8429 text: "Spawn a subagent that spawns two more.",
8430 attachments: [],
8431 modelSelection: parentSelection,
8432 }),
8433 );
8434 const agentCall = (id: string, effort?: string) => ({
8435 type: "tool_use" as const,
8436 id,
8437 name: "Agent",
8438 input: {
8439 description: id,
8440 subagent_type: "general-purpose",
8441 ...(effort === undefined ? {} : { effort }),
8442 prompt: "Check one thing.",
8443 },
8444 });
8445 const taskStarted = (id: string, uuid: string) =>
8446 claudeSdkFrame({
8447 type: "system",
8448 subtype: "task_started",
8449 task_id: `task-${id}`,
8450 tool_use_id: id,
8451 description: id,
8452 task_type: "local_agent",
8453 uuid,
8454 session_id: WAKE_NATIVE_SESSION,
8455 });
8456 for (const frame of [
8457 claudeSdkFrame({
8458 type: "assistant",
8459 parent_tool_use_id: null,
8460 message: {
8461 model: parentSelection.model,
8462 id: "msg_nested_effort_outer_launch",
8463 type: "message",
8464 role: "assistant",
8465 content: [agentCall(OUTER_TOOL_USE_ID, "low")],
8466 },
8467 uuid: "00000000-0000-4000-8000-000000000221",
8468 session_id: WAKE_NATIVE_SESSION,
8469 }),
8470 taskStarted(OUTER_TOOL_USE_ID, "00000000-0000-4000-8000-000000000222"),
8471 // The outer subagent's own Agent calls arrive in its snapshot.
8472 claudeSdkFrame({
8473 type: "assistant",
8474 parent_tool_use_id: OUTER_TOOL_USE_ID,
8475 message: {
8476 model: parentSelection.model,
8477 id: "msg_nested_effort_inner_launch",
8478 type: "message",
8479 role: "assistant",
8480 content: [
8481 agentCall(INHERITING_TOOL_USE_ID),
8482 agentCall(OVERRIDING_TOOL_USE_ID, "max"),
8483 ],
8484 },
8485 uuid: "00000000-0000-4000-8000-000000000223",
8486 session_id: WAKE_NATIVE_SESSION,
8487 }),
8488 taskStarted(INHERITING_TOOL_USE_ID, "00000000-0000-4000-8000-000000000224"),
8489 taskStarted(OVERRIDING_TOOL_USE_ID, "00000000-0000-4000-8000-000000000225"),
8490 makeResultFrame({
8491 uuid: "00000000-0000-4000-8000-000000000226",
8492 result: "Spawned the subagents.",
8493 }),
8494 ]) {
8495 yield* Queue.offer(harness.sdkMessages, frame);
8496 }
8497 yield* Queue.take(harness.terminalReceipts);
8498
8499 const efforts = new Map(
8500 harness.events.flatMap((event) =>
8501 event.type === "subagent.updated"
8502 ? [
8503 [
8504 event.subagent.nativeTaskRef?.nativeId,
8505 getModelSelectionStringOptionValue(event.subagent.modelSelection, "effort"),
8506 ] as const,
8507 ]
8508 : [],
8509 ),
8510 );
8511 assert.deepEqual(Object.fromEntries(efforts), {
8512 [`task-${OUTER_TOOL_USE_ID}`]: "low",
8513 [`task-${INHERITING_TOOL_USE_ID}`]: "low",
8514 [`task-${OVERRIDING_TOOL_USE_ID}`]: "max",
8515 });
8516 const childEfforts = harness.events.flatMap((event) =>
8517 event.type === "app_thread.created" && event.appThread.id !== harness.threadId
8518 ? [getModelSelectionStringOptionValue(event.appThread.modelSelection, "effort")]
8519 : [],
8520 );
8521 assert.deepEqual(childEfforts, ["low", "low", "max"]);
8522 }).pipe(
8523 Effect.provide(
8524 Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
8525 ),
8526 ),
8527 ),
8528 );
8529
8165 it.effect("extracts text from direct content-block subagent results", () =>8530 it.effect("extracts text from direct content-block subagent results", () =>
8166 Effect.scoped(8531 Effect.scoped(
8167 Effect.gen(function* () {8532 Effect.gen(function* () {
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts+92-13
...@@ -99,6 +99,10 @@ import {...@@ -99,6 +99,10 @@ import {
99import {99import {
100 BUNDLED_CLAUDE_MODEL_CATALOG,100 BUNDLED_CLAUDE_MODEL_CATALOG,
101 resolveClaudeCatalogContextWindowTokens,101 resolveClaudeCatalogContextWindowTokens,
102 type ClaudeModelCatalog,
103 getClaudeCatalogModelCapabilities,
104 resolveClaudeModelSlug,
105 scopeClaudeModelCatalog,
102} from "../../provider/ClaudeModelCatalog.ts";106} from "../../provider/ClaudeModelCatalog.ts";
103import {107import {
104 boundProviderEventForLogging,108 boundProviderEventForLogging,
...@@ -2841,6 +2845,9 @@ interface ActiveClaudeSubagent {...@@ -2841,6 +2845,9 @@ interface ActiveClaudeSubagent {
2841 // The tool call that started the current run: the Agent launch, then each2845 // The tool call that started the current run: the Agent launch, then each
2842 // SendMessage that resumes the subagent. A new one means a new prompt.2846 // SendMessage that resumes the subagent. A new one means a new prompt.
2843 readonly runToolUseId: string | null;2847 readonly runToolUseId: string | null;
2848 // The API effort the subagent runs at: its Agent call's, else its owner's,
2849 // else the session's. Undefined when none is sent and the model default applies.
2850 readonly effort: string | undefined;
2844 nextChildItemOrdinal: number;2851 nextChildItemOrdinal: number;
2845 resultItemOrdinal: number | null;2852 resultItemOrdinal: number | null;
2846 // The subagent's latest assistant message routed into its child thread,2853 // The subagent's latest assistant message routed into its child thread,
...@@ -2893,10 +2900,42 @@ interface ActiveClaudeToolCall {...@@ -2893,10 +2900,42 @@ interface ActiveClaudeToolCall {
2893// keyed by the tool_use_id that launches it.2900// keyed by the tool_use_id that launches it.
2894interface PendingClaudeSubagentLaunch {2901interface PendingClaudeSubagentLaunch {
2895 readonly model?: string;2902 readonly model?: string;
2903 readonly effort?: string;
2896 // parent_tool_use_id of the subagent whose own Agent call launches this one.2904 // parent_tool_use_id of the subagent whose own Agent call launches this one.
2897 readonly ownerToolUseId?: string;2905 readonly ownerToolUseId?: string;
2898}2906}
28992907
2908/**
2909 * Agent calls name aliases ("opus") and replies name dated ids; both resolve
2910 * to the catalog slug. A subagent's thread then keeps one model name across
2911 * its launch, its replies and a resume after a restart, so a later report of
2912 * the same model never reads as a model change that drops its effort. The
2913 * catalog is the instance's, so a custom model keeps its own name.
2914 */
2915function canonicalClaudeSubagentModel(catalog: ClaudeModelCatalog, model: string): string {
2916 return resolveClaudeModelSlug(catalog, model);
2917}
2918
2919/**
2920 * The selection a subagent's thread shows. An effort its model does not offer
2921 * is not sent for it, so it is left out (a nested subagent still inherits it).
2922 */
2923function claudeSubagentModelSelection(
2924 catalog: ClaudeModelCatalog,
2925 instanceId: ModelSelection["instanceId"],
2926 model: string,
2927 effort: string | undefined,
2928): ModelSelection {
2929 const descriptor = getClaudeCatalogModelCapabilities(catalog, model).optionDescriptors?.find(
2930 (candidate) => candidate.id === "effort",
2931 );
2932 const offered =
2933 effort !== undefined &&
2934 descriptor?.type === "select" &&
2935 descriptor.options.some((option) => option.id === effort);
2936 return { instanceId, model, ...(offered ? { options: [{ id: "effort", value: effort }] } : {}) };
2937}
2938
2900const PENDING_CLAUDE_SUBAGENT_CAP = 64;2939const PENDING_CLAUDE_SUBAGENT_CAP = 64;
2901// Per-subagent bound on frames held while waiting for task_started.2940// Per-subagent bound on frames held while waiting for task_started.
2902const PENDING_CLAUDE_SUBAGENT_FRAME_CAP = 256;2941const PENDING_CLAUDE_SUBAGENT_FRAME_CAP = 256;
...@@ -2917,11 +2956,13 @@ function rememberPendingClaudeSubagentLaunch(...@@ -2917,11 +2956,13 @@ function rememberPendingClaudeSubagentLaunch(
2917}2956}
29182957
2919/**2958/**
2920 * Agent calls carry model overrides even when the SDK omits child assistant2959 * Agent calls carry model and effort overrides even when the SDK omits child
2921 * snapshots. A subagent's own Agent call arrives only in its snapshot, so the2960 * assistant snapshots. A subagent's own Agent call arrives only in its
2922 * owner recorded here is all that links the subagent it starts back to it.2961 * snapshot, so the owner recorded here is all that links the subagent it
2962 * starts back to it.
2923 */2963 */
2924function rememberClaudeSubagentLaunch(2964function rememberClaudeSubagentLaunch(
2965 catalog: ClaudeModelCatalog,
2925 context: ActiveClaudeTurnContext,2966 context: ActiveClaudeTurnContext,
2926 pending: Map<string, PendingClaudeSubagentLaunch>,2967 pending: Map<string, PendingClaudeSubagentLaunch>,
2927 toolUseId: string,2968 toolUseId: string,
...@@ -2934,17 +2975,23 @@ function rememberClaudeSubagentLaunch(...@@ -2934,17 +2975,23 @@ function rememberClaudeSubagentLaunch(
2934 // task_started resolves it.2975 // task_started resolves it.
2935 const model =2976 const model =
2936 requested !== "inherit"2977 requested !== "inherit"
2937 ? requested2978 ? requested && canonicalClaudeSubagentModel(catalog, requested)
2938 : ownerToolUseId === null2979 : ownerToolUseId === null
2939 ? context.input.modelSelection.model2980 ? context.input.modelSelection.model
2940 : undefined;2981 : undefined;
2982 const effort = firstStringInputField(input, ["effort"]);
2941 // A model already known (a snapshot's, or an earlier sighting of this2983 // A model already known (a snapshot's, or an earlier sighting of this
2942 // call) wins over the requested one.2984 // call) wins over the requested one.
2943 const launch: PendingClaudeSubagentLaunch = {2985 const launch: PendingClaudeSubagentLaunch = {
2944 ...(model === undefined || pending.get(toolUseId)?.model !== undefined ? {} : { model }),2986 ...(model === undefined || pending.get(toolUseId)?.model !== undefined ? {} : { model }),
2987 ...(effort === undefined ? {} : { effort }),
2945 ...(ownerToolUseId === null ? {} : { ownerToolUseId }),2988 ...(ownerToolUseId === null ? {} : { ownerToolUseId }),
2946 };2989 };
2947 if (launch.model !== undefined || launch.ownerToolUseId !== undefined) {2990 if (
2991 launch.model !== undefined ||
2992 launch.effort !== undefined ||
2993 launch.ownerToolUseId !== undefined
2994 ) {
2948 rememberPendingClaudeSubagentLaunch(pending, toolUseId, launch);2995 rememberPendingClaudeSubagentLaunch(pending, toolUseId, launch);
2949 }2996 }
2950}2997}
...@@ -3088,6 +3135,11 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (...@@ -3088,6 +3135,11 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
3088) {3135) {
3089 const mcpSessions = yield* McpProviderSessions.McpProviderSessions;3136 const mcpSessions = yield* McpProviderSessions.McpProviderSessions;
3090 const { attachmentsDir, fileSystem, path, crypto, idAllocator, queryRunner } = adapterOptions;3137 const { attachmentsDir, fileSystem, path, crypto, idAllocator, queryRunner } = adapterOptions;
3138 // Subagent model names and effort levels, including the instance's custom models.
3139 const subagentModelCatalog = scopeClaudeModelCatalog(
3140 BUNDLED_CLAUDE_MODEL_CATALOG,
3141 adapterOptions.settings.customModels,
3142 );
3091 const continuationRequests = adapterOptions.continuationRequests ?? {3143 const continuationRequests = adapterOptions.continuationRequests ?? {
3092 offer: () => Effect.void,3144 offer: () => Effect.void,
3093 };3145 };
...@@ -4165,6 +4217,9 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (...@@ -4165,6 +4217,9 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
4165 `task:${resume.taskId}:subagent`,4217 `task:${resume.taskId}:subagent`,
4166 ),4218 ),
4167 runToolUseId: launchToolUseId,4219 runToolUseId: launchToolUseId,
4220 // The launch's effort went with the old process. Without a task
4221 // modelSelection the child thread keeps the one it was given.
4222 effort: undefined,
4168 nextChildItemOrdinal: 100,4223 nextChildItemOrdinal: 100,
4169 resultItemOrdinal: null,4224 resultItemOrdinal: null,
4170 lastAssistantText: null,4225 lastAssistantText: null,
...@@ -4185,8 +4240,9 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (...@@ -4185,8 +4240,9 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
4185 readonly prompt?: string;4240 readonly prompt?: string;
4186 readonly title?: string;4241 readonly title?: string;
4187 readonly model?: string;4242 readonly model?: string;
4188 // The subagent whose own Agent call started this one; read only4243 // The Agent call's effort and the subagent whose own Agent call
4189 // when this call registers the subagent.4244 // started this one; read only when this call registers the subagent.
4245 readonly effort?: string;
4190 readonly owner?: ActiveClaudeSubagent;4246 readonly owner?: ActiveClaudeSubagent;
4191 readonly progress?: string;4247 readonly progress?: string;
4192 readonly result?: string;4248 readonly result?: string;
...@@ -4285,6 +4341,13 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (...@@ -4285,6 +4341,13 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
4285 existingSubagent.task,4341 existingSubagent.task,
4286 )4342 )
4287 : existingSubagent.task;4343 : existingSubagent.task;
4344 const effort =
4345 existingSubagent === undefined
4346 ? (input.effort ??
4347 input.owner?.effort ??
4348 compileClaudeModelSelection(input.context.input.modelSelection).effort)
4349 : existingSubagent.effort;
4350 const model = input.model ?? priorTask?.model ?? null;
4288 const task = {4351 const task = {
4289 ...(priorTask ?? {4352 ...(priorTask ?? {
4290 id: nodeId,4353 id: nodeId,
...@@ -4323,7 +4386,19 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (...@@ -4323,7 +4386,19 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
4323 : {}),4386 : {}),
4324 ...(input.prompt === undefined ? {} : { prompt: input.prompt }),4387 ...(input.prompt === undefined ? {} : { prompt: input.prompt }),
4325 ...(input.title === undefined ? {} : { title: input.title }),4388 ...(input.title === undefined ? {} : { title: input.title }),
4326 ...(input.model === undefined ? {} : { model: input.model }),4389 model,
4390 // What the subagent runs with, unless it was recovered after a
4391 // restart. The parent's other options belong to the parent's session.
4392 ...(existingSubagent === undefined || existingSubagent.task.modelSelection !== undefined
4393 ? {
4394 modelSelection: claudeSubagentModelSelection(
4395 subagentModelCatalog,
4396 input.context.input.modelSelection.instanceId,
4397 model || input.context.input.modelSelection.model,
4398 effort,
4399 ),
4400 }
4401 : {}),
4327 ...(input.progress === undefined ? {} : { progress: input.progress }),4402 ...(input.progress === undefined ? {} : { progress: input.progress }),
4328 ...(input.result === undefined ? {} : { result: input.result }),4403 ...(input.result === undefined ? {} : { result: input.result }),
4329 ...(isReopen ? { startedAt: now } : {}),4404 ...(isReopen ? { startedAt: now } : {}),
...@@ -4341,6 +4416,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (...@@ -4341,6 +4416,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
4341 existingSubagent === undefined4416 existingSubagent === undefined
4342 ? (input.toolUseId ?? null)4417 ? (input.toolUseId ?? null)
4343 : (resumeToolUseId ?? existingSubagent.runToolUseId),4418 : (resumeToolUseId ?? existingSubagent.runToolUseId),
4419 effort,
4344 nextChildItemOrdinal: existingSubagent?.nextChildItemOrdinal ?? 100,4420 nextChildItemOrdinal: existingSubagent?.nextChildItemOrdinal ?? 100,
4345 resultItemOrdinal: existingSubagent?.resultItemOrdinal ?? null,4421 resultItemOrdinal: existingSubagent?.resultItemOrdinal ?? null,
4346 // Every task_started begins a new run of the subagent (including a4422 // Every task_started begins a new run of the subagent (including a
...@@ -4388,10 +4464,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (...@@ -4388,10 +4464,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
4388 parentNodeId: nodeId,4464 parentNodeId: nodeId,
4389 activeProviderThreadId: null,4465 activeProviderThreadId: null,
4390 providerInstanceId: input.context.input.modelSelection.instanceId,4466 providerInstanceId: input.context.input.modelSelection.instanceId,
4391 modelSelection:4467 modelSelection: task.modelSelection ?? input.context.input.modelSelection,
4392 task.model && task.model !== input.context.input.modelSelection.model
4393 ? { instanceId: input.context.input.modelSelection.instanceId, model: task.model }
4394 : input.context.input.modelSelection,
4395 title: subagentThreadTitle({4468 title: subagentThreadTitle({
4396 parentTitle: input.context.input.appThread.title,4469 parentTitle: input.context.input.appThread.title,
4397 prompt: task.prompt,4470 prompt: task.prompt,
...@@ -6194,7 +6267,10 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (...@@ -6194,7 +6267,10 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
6194 const parentToolUseId = message.parent_tool_use_id;6267 const parentToolUseId = message.parent_tool_use_id;
6195 const snapshotModel =6268 const snapshotModel =
6196 typeof message.message.model === "string" ? message.message.model.trim() : "";6269 typeof message.message.model === "string" ? message.message.model.trim() : "";
6197 const model = snapshotModel.length === 0 ? undefined : snapshotModel;6270 const model =
6271 snapshotModel.length === 0
6272 ? undefined
6273 : canonicalClaudeSubagentModel(subagentModelCatalog, snapshotModel);
6198 if (parentToolUseId !== null && model !== undefined) {6274 if (parentToolUseId !== null && model !== undefined) {
6199 const subagent = yield* resolveSubagentByToolUseId(context, parentToolUseId);6275 const subagent = yield* resolveSubagentByToolUseId(context, parentToolUseId);
6200 if (subagent === undefined) {6276 if (subagent === undefined) {
...@@ -6301,6 +6377,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (...@@ -6301,6 +6377,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
6301 ...(message.tool_use_id === undefined ? {} : { toolUseId: message.tool_use_id }),6377 ...(message.tool_use_id === undefined ? {} : { toolUseId: message.tool_use_id }),
6302 ...(message.prompt === undefined ? {} : { prompt: message.prompt }),6378 ...(message.prompt === undefined ? {} : { prompt: message.prompt }),
6303 ...(model === undefined ? {} : { model }),6379 ...(model === undefined ? {} : { model }),
6380 ...(launch?.effort === undefined ? {} : { effort: launch.effort }),
6304 ...(owner === undefined ? {} : { owner }),6381 ...(owner === undefined ? {} : { owner }),
6305 title: message.description,6382 title: message.description,
6306 status: "running",6383 status: "running",
...@@ -6402,6 +6479,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (...@@ -6402,6 +6479,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
6402 const nativeToolInput = claudeNativeToolInputFromUnknown(toolUse.input);6479 const nativeToolInput = claudeNativeToolInputFromUnknown(toolUse.input);
6403 if (toolUse.name === "Agent") {6480 if (toolUse.name === "Agent") {
6404 rememberClaudeSubagentLaunch(6481 rememberClaudeSubagentLaunch(
6482 subagentModelCatalog,
6405 context,6483 context,
6406 pendingSubagentLaunchesByToolUseId,6484 pendingSubagentLaunchesByToolUseId,
6407 toolUse.id,6485 toolUse.id,
...@@ -7000,6 +7078,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (...@@ -7000,6 +7078,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
7000 const heldForEcho = context.heldRootFrames.length > 0;7078 const heldForEcho = context.heldRootFrames.length > 0;
7001 if (toolName === "Agent") {7079 if (toolName === "Agent") {
7002 rememberClaudeSubagentLaunch(7080 rememberClaudeSubagentLaunch(
7081 subagentModelCatalog,
7003 context,7082 context,
7004 pendingSubagentLaunchesByToolUseId,7083 pendingSubagentLaunchesByToolUseId,
7005 nativeRequestId,7084 nativeRequestId,
apps/server/src/orchestration-v2/ProviderEventIngestor.test.ts+31
...@@ -1681,6 +1681,37 @@ layer("ProviderEventIngestorV2", (it) => {...@@ -1681,6 +1681,37 @@ layer("ProviderEventIngestorV2", (it) => {
1681 instanceId: modelSelection.instanceId,1681 instanceId: modelSelection.instanceId,
1682 model: "gpt-6.1-sol",1682 model: "gpt-6.1-sol",
1683 });1683 });
1684
1685 // A selection the provider reports, effort included, replaces the thread's.
1686 const reportedSelection = {
1687 instanceId: modelSelection.instanceId,
1688 model: "gpt-6.1-sol",
1689 options: [{ id: "reasoningEffort", value: "low" }],
1690 };
1691 const reported = yield* ingest({
1692 ...subagentUpdated,
1693 subagent: { ...subagentUpdated.subagent, modelSelection: reportedSelection },
1694 });
1695 assert.deepEqual(
1696 reported.map((stored) => stored.event.type),
1697 ["subagent.updated", "thread.model-selection-updated"],
1698 );
1699 assert.deepEqual(
1700 (yield* projectionStore.getThread(childThreadId)).modelSelection,
1701 reportedSelection,
1702 );
1703
1704 // A subagent recovered after a restart reports its model but no
1705 // selection; the same model keeps the effort the thread holds.
1706 const recovered = yield* ingest(subagentUpdated);
1707 assert.deepEqual(
1708 recovered.map((stored) => stored.event.type),
1709 ["subagent.updated"],
1710 );
1711 assert.deepEqual(
1712 (yield* projectionStore.getThread(childThreadId)).modelSelection,
1713 reportedSelection,
1714 );
1684 }),1715 }),
1685 );1716 );
1686});1717});
apps/server/src/orchestration-v2/ProviderEventIngestor.ts+21-8
...@@ -27,7 +27,7 @@ import * as Layer from "effect/Layer";...@@ -27,7 +27,7 @@ import * as Layer from "effect/Layer";
27import * as Schema from "effect/Schema";27import * as Schema from "effect/Schema";
2828
29import { AgentScope } from "@t3tools/shared/AgentScope";29import { AgentScope } from "@t3tools/shared/AgentScope";
30import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";30import { getModelSelectionStringOptionValue, modelSelectionsEqual } from "@t3tools/shared/model";
31import * as AnalyticsService from "../telemetry/AnalyticsService.ts";31import * as AnalyticsService from "../telemetry/AnalyticsService.ts";
32import * as EventSink from "./EventSink.ts";32import * as EventSink from "./EventSink.ts";
33import * as ProjectionStore from "./ProjectionStore.ts";33import * as ProjectionStore from "./ProjectionStore.ts";
...@@ -402,18 +402,32 @@ export const layer: Layer.Layer<...@@ -402,18 +402,32 @@ export const layer: Layer.Layer<
402 /**402 /**
403 * A native subagent's thread starts on the parent's model when the403 * A native subagent's thread starts on the parent's model when the
404 * provider names the real one later (a Claude agent file's model arrives404 * provider names the real one later (a Claude agent file's model arrives
405 * with the subagent's first reply). Clients read the thread's model, so405 * with the subagent's first reply). Clients read the thread's model and
406 * move the thread to the reported one. Thread commands rewrite the whole406 * effort, so move the thread to the reported selection, or to the reported
407 * thread row under the thread's lock, so this read and write take it too.407 * model alone when the provider reports no selection. Thread commands
408 * rewrite the whole thread row under the thread's lock, so this read and
409 * write take it too.
408 */410 */
409 const syncSubagentThreadModel = Effect.fn("ProviderEventIngestor.syncSubagentThreadModel")(411 const syncSubagentThreadModel = Effect.fn("ProviderEventIngestor.syncSubagentThreadModel")(
410 function* (input: ProviderEventIngestInput, subagent: OrchestrationV2Subagent) {412 function* (input: ProviderEventIngestInput, subagent: OrchestrationV2Subagent) {
411 const { childThreadId, model } = subagent;413 const { childThreadId, model } = subagent;
412 if (subagent.origin !== "provider_native" || childThreadId === null || model === null) {414 if (
415 subagent.origin !== "provider_native" ||
416 childThreadId === null ||
417 (model === null && subagent.modelSelection === undefined)
418 ) {
413 return [];419 return [];
414 }420 }
421 const reportedSelection = ({ modelSelection }: { modelSelection: ModelSelection }) =>
422 subagent.modelSelection ??
423 (model === null || model === modelSelection.model
424 ? modelSelection
425 : // The parent's options belong to the parent's model.
426 { instanceId: modelSelection.instanceId, model });
415 const staleThread = projections.getThread(childThreadId).pipe(427 const staleThread = projections.getThread(childThreadId).pipe(
416 Effect.map((thread) => (thread.modelSelection.model === model ? null : thread)),428 Effect.map((thread) =>
429 modelSelectionsEqual(thread.modelSelection, reportedSelection(thread)) ? null : thread,
430 ),
417 Effect.catchTags({ ProjectionStoreThreadNotFoundError: () => Effect.succeed(null) }),431 Effect.catchTags({ ProjectionStoreThreadNotFoundError: () => Effect.succeed(null) }),
418 );432 );
419 // Nearly every update already matches; only a mismatch takes the lock.433 // Nearly every update already matches; only a mismatch takes the lock.
...@@ -427,10 +441,9 @@ export const layer: Layer.Layer<...@@ -427,10 +441,9 @@ export const layer: Layer.Layer<
427 const event = yield* makeDomainEvent(input, {441 const event = yield* makeDomainEvent(input, {
428 type: "thread.model-selection-updated",442 type: "thread.model-selection-updated",
429 threadId: thread.id,443 threadId: thread.id,
430 // The parent's options belong to the parent's model.
431 payload: {444 payload: {
432 ...thread,445 ...thread,
433 modelSelection: { instanceId: thread.modelSelection.instanceId, model },446 modelSelection: reportedSelection(thread),
434 updatedAt: now,447 updatedAt: now,
435 },448 },
436 occurredAt: now,449 occurredAt: now,
apps/server/src/orchestration-v2/testkit/OrchestratorReplayRecovery.integration.test.ts+7
...@@ -384,6 +384,13 @@ describe("orchestrator replay recovery", () => {...@@ -384,6 +384,13 @@ describe("orchestrator replay recovery", () => {
384 assert.lengthOf(childThreads, 1);384 assert.lengthOf(childThreads, 1);
385 const child = childThreads[0];385 const child = childThreads[0];
386 assert.equal(child?.thread.id, subagent?.childThreadId);386 assert.equal(child?.thread.id, subagent?.childThreadId);
387 // The recovered subagent's replies name the model its thread already
388 // holds, so the effort recorded at launch survives the restart.
389 assert.deepEqual(child?.thread.modelSelection, {
390 instanceId: CLAUDE_MODEL_SELECTION.instanceId,
391 model: "claude-sonnet-5",
392 options: [{ id: "effort", value: "high" }],
393 });
387394
388 const conversation = (child?.turnItems ?? [])395 const conversation = (child?.turnItems ?? [])
389 .toSorted((left, right) => left.ordinal - right.ordinal)396 .toSorted((left, right) => left.ordinal - right.ordinal)
apps/server/src/orchestration-v2/testkit/fixtures/claude_background_subagent_lifecycle/output.ts+2-1
...@@ -22,7 +22,8 @@ import {...@@ -22,7 +22,8 @@ import {
2222
23const AGENT_A_TASK_ID = "a1a715b7d0bdfefea";23const AGENT_A_TASK_ID = "a1a715b7d0bdfefea";
24const AGENT_B_TASK_ID = "af44d5c14aa3ce867";24const AGENT_B_TASK_ID = "af44d5c14aa3ce867";
25const AGENT_A_OBSERVED_MODEL = "claude-haiku-4-5-20251001";25// Its snapshots report claude-haiku-4-5-20251001, recorded as the catalog slug.
26const AGENT_A_OBSERVED_MODEL = "claude-haiku-4-5";
2627
27function assistantTexts(28function assistantTexts(
28 projection: OrchestrationV2ThreadProjection,29 projection: OrchestrationV2ThreadProjection,
apps/server/src/orchestration-v2/testkit/fixtures/claude_nested_subagent_model/output.ts+8-3
...@@ -12,9 +12,10 @@ import {...@@ -12,9 +12,10 @@ import {
1212
13const OUTER_TASK_ID = "a457ca677bc1ecd82";13const OUTER_TASK_ID = "a457ca677bc1ecd82";
14const NESTED_TASK_ID = "aac1d7a2af5371c7d";14const NESTED_TASK_ID = "aac1d7a2af5371c7d";
15// The outer subagent's snapshots report this id; the nested one never streams15// The outer subagent's snapshots report claude-haiku-4-5-20251001, recorded as
16// a snapshot of its own (the CLI's session storage shows it ran on the same).16// the catalog slug; the nested one never streams a snapshot of its own (the
17const SUBAGENT_MODEL = "claude-haiku-4-5-20251001";17// CLI's session storage shows it ran on the same).
18const SUBAGENT_MODEL = "claude-haiku-4-5";
1819
19export function assertClaudeNestedSubagentModelOutput(20export function assertClaudeNestedSubagentModelOutput(
20 result: OrchestratorV2ScenarioResult,21 result: OrchestratorV2ScenarioResult,
...@@ -45,6 +46,10 @@ export function assertClaudeNestedSubagentModelOutput(...@@ -45,6 +46,10 @@ export function assertClaudeNestedSubagentModelOutput(
45 // only that snapshot can say who launched it and on which model.46 // only that snapshot can say who launched it and on which model.
46 assert.equal(nested.parentNodeId, outer.id, "a nested subagent hangs off its owner");47 assert.equal(nested.parentNodeId, outer.id, "a nested subagent hangs off its owner");
47 assert.equal(nested.model, SUBAGENT_MODEL, "a nested subagent runs on its owner's model");48 assert.equal(nested.model, SUBAGENT_MODEL, "a nested subagent runs on its owner's model");
49 // The session's effort is not sent for claude-haiku-4-5, which offers none.
50 const subagentSelection = { instanceId: outer.providerInstanceId, model: SUBAGENT_MODEL };
51 assert.deepEqual(outer.modelSelection, subagentSelection);
52 assert.deepEqual(nested.modelSelection, subagentSelection);
48 const nestedNode = projection.nodes.find((node) => node.id === nested.id);53 const nestedNode = projection.nodes.find((node) => node.id === nested.id);
49 assert.isDefined(nestedNode);54 assert.isDefined(nestedNode);
50 assert.equal(nestedNode.parentNodeId, outer.id);55 assert.equal(nestedNode.parentNodeId, outer.id);