@deepseek-ai/dsh-tool-subagent 0.1.1-rc.2 → 0.1.2-alpha.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/lib/index.js CHANGED
@@ -1,6 +1,236 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
+ import { scopeChainOf, scopeOf } from "@deepseek-ai/dsh-scope";
2
3
  import { defineTool } from "@deepseek-ai/dsh-tools";
3
- import { assertSubagentMaxDepth, settleRun } from "@deepseek-ai/dsh-subagent";
4
+ import { assertSubagentMaxDepth, parentAgentOptionsForDelegation, settleRun } from "@deepseek-ai/dsh-subagent";
5
+ import { ReasoningEffortId } from "@deepseek-ai/dsh-llm";
6
+ import { z as z$1 } from "zod";
7
+ z.object({
8
+ provider: z.string().min(1).required(),
9
+ model: z.string().min(1).required()
10
+ });
11
+ /**
12
+ * Stable identity for one provider/model pair.
13
+ * @param route - Exact provider/model route.
14
+ * @returns Opaque key for equality checks.
15
+ */
16
+ function modelRouteKey(route) {
17
+ return `${route.provider}\0${route.model}`;
18
+ }
19
+ /**
20
+ * Reject malformed or duplicate route policy entries at a durable or configuration boundary.
21
+ * @param routes - Candidate exact routes to validate.
22
+ * @returns an assertion that the candidate is a validated exact-route array.
23
+ */
24
+ function assertAllowedModelRoutes(routes) {
25
+ if (!Array.isArray(routes)) throw new Error("subagent model selection requires an array of routes");
26
+ const seen = /* @__PURE__ */ new Set();
27
+ const candidates = routes;
28
+ for (const candidate of candidates) {
29
+ if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate) || !("provider" in candidate) || typeof candidate.provider !== "string" || !("model" in candidate) || typeof candidate.model !== "string" || candidate.provider.length === 0 || candidate.model.length === 0) throw new Error("subagent model selection requires non-empty provider and model ids");
30
+ const route = {
31
+ provider: candidate.provider,
32
+ model: candidate.model
33
+ };
34
+ const key = modelRouteKey(route);
35
+ if (seen.has(key)) throw new Error(`subagent model selection repeats route "${route.provider}/${route.model}"`);
36
+ seen.add(key);
37
+ }
38
+ }
39
+ /**
40
+ * Whether a call explicitly selects any child LLM value.
41
+ * @param request - Model-facing route fields from the tool call.
42
+ * @returns Whether at least one route or effort field is present.
43
+ */
44
+ function hasDelegationModelRequest(request) {
45
+ return request.provider !== void 0 || request.model !== void 0 || request.reasoning_effort !== void 0;
46
+ }
47
+ /** Reject an empty model-facing route value at the tool JSON boundary. */
48
+ function assertNonEmpty(value, field) {
49
+ if (value !== void 0 && value.length === 0) throw new Error(`child LLM \`${field}\` must be non-empty`);
50
+ }
51
+ /**
52
+ * Merge model-supplied selection fields over configured child defaults.
53
+ * Provider and model form one route and must be supplied together. Changing
54
+ * that route without an effort clears the configured route-owned effort.
55
+ * @param parentOptions - Current parent values that supply missing child values.
56
+ * @param configured - Tool-instance child defaults.
57
+ * @param request - Model-facing route override.
58
+ * @param enabled - Whether this tool instance permits model-facing selection.
59
+ * @returns Child Agent options, preserving omission when no layer contributes one.
60
+ */
61
+ function requestedAgentOptions(parentOptions, configured, request, enabled) {
62
+ if (!hasDelegationModelRequest(request)) return configured;
63
+ if (!enabled) throw new Error("child model selection is disabled for this tool instance");
64
+ assertNonEmpty(request.provider, "provider");
65
+ assertNonEmpty(request.model, "model");
66
+ assertNonEmpty(request.reasoning_effort, "reasoning_effort");
67
+ if (request.provider === void 0 !== (request.model === void 0)) throw new Error("child LLM `provider` and `model` must be supplied together");
68
+ const baselineProvider = configured?.provider ?? parentOptions.provider;
69
+ const baselineModel = configured?.model ?? parentOptions.model;
70
+ const routeChanged = request.provider !== void 0 && (request.provider !== baselineProvider || request.model !== baselineModel);
71
+ const { reasoningEffort: _configuredReasoningEffort, ...configuredWithoutReasoning } = configured ?? {};
72
+ return {
73
+ ...routeChanged && request.reasoning_effort === void 0 ? configuredWithoutReasoning : configured,
74
+ ...request.provider === void 0 ? {} : {
75
+ provider: request.provider,
76
+ model: request.model
77
+ },
78
+ ...request.reasoning_effort === void 0 ? {} : { reasoningEffort: ReasoningEffortId(request.reasoning_effort) }
79
+ };
80
+ }
81
+ /**
82
+ * Enforce a settings-owned route list at the operation that creates the child.
83
+ * Pure inheritance remains outside this policy because no model-facing choice
84
+ * occurred; any explicit route or effort field must resolve to an allowed route.
85
+ * @param policy - Selection authority captured for this Session.
86
+ * @param parentOptions - Current parent values that supply missing child values.
87
+ * @param requested - Effective child options after request/config merging.
88
+ * @param request - Model-facing selection fields from the tool call.
89
+ */
90
+ function assertAllowedModelSelection(policy, parentOptions, requested, request) {
91
+ if (policy === void 0 || !hasDelegationModelRequest(request)) return;
92
+ const provider = requested?.provider ?? parentOptions.provider;
93
+ const model = requested?.model ?? parentOptions.model;
94
+ if (provider === void 0 || model === void 0) throw new Error("cannot select child LLM values without an effective provider and model");
95
+ if (policy.routes.some((route) => route.provider === provider && route.model === model)) return;
96
+ throw new Error(`child LLM route "${provider}/${model}" is not allowed for this Session`);
97
+ }
98
+ /**
99
+ * Whether configured Agent options require route validation before delegation.
100
+ * @param options - Tool-instance child defaults.
101
+ * @returns Whether configured provider, model, or effort values must be resolved.
102
+ */
103
+ function hasConfiguredLlmSelection(options) {
104
+ return options?.provider !== void 0 || options?.model !== void 0 || options?.reasoningEffort !== void 0;
105
+ }
106
+ /**
107
+ * Resolve an effective child route through its live adapter before the child is
108
+ * created. The LLM runtime owns provider lookup, exact-model metadata, effort
109
+ * validation, and adapter defaults.
110
+ * @param llm - Live LLM runtime.
111
+ * @param parentOptions - Current parent values whose compatible fields the child inherits.
112
+ * @param requested - Per-child options after request/config merging.
113
+ * @param signal - Tool-call cancellation signal.
114
+ * @param inheritParentReasoningEffort - Whether an omitted effort may inherit from the parent route.
115
+ */
116
+ async function preflightChildLlmRoute(llm, parentOptions, requested, signal, inheritParentReasoningEffort = true) {
117
+ const provider = requested?.provider ?? parentOptions.provider;
118
+ const model = requested?.model ?? parentOptions.model;
119
+ if (provider === void 0 || model === void 0) throw new Error("cannot select child LLM values without an effective provider and model");
120
+ const routeChanged = provider !== parentOptions.provider || model !== parentOptions.model;
121
+ const reasoningEffort = requested?.reasoningEffort ?? (inheritParentReasoningEffort && !routeChanged ? parentOptions.reasoningEffort : void 0);
122
+ await llm.resolveCallConfig({
123
+ provider,
124
+ model,
125
+ ...reasoningEffort === void 0 ? {} : { reasoningEffort }
126
+ }, signal);
127
+ }
128
+ //#endregion
129
+ //#region lib/types/list-models.js
130
+ /** Model-facing discovery of LLM routes available to child Agents. */
131
+ /** Resolve one registered provider with a model-correctable diagnostic. */
132
+ function registeredProvider(llm, policy, providerId) {
133
+ const providers = llm.listProviders();
134
+ const provider = providers.find((candidate) => candidate.id === providerId);
135
+ if (provider !== void 0) return provider;
136
+ const available = providers.filter((candidate) => policy.routes.some((route) => route.provider === candidate.id)).map((candidate) => candidate.id).join(", ") || "(none)";
137
+ throw new Error(`LLM provider "${providerId}" is not registered; available providers: ${available}`);
138
+ }
139
+ /** Render one advertised or resolved model. */
140
+ function modelLine(provider, model) {
141
+ return `${provider}/${model.id} — ${model.name}${model.description === void 0 ? "" : `: ${model.description}`}`;
142
+ }
143
+ /** Read the requested provider, advertised models, or exact-model efforts. */
144
+ async function listSubagentModels(ctx, policy, request, signal) {
145
+ const llm = ctx.get("llm");
146
+ if (llm === void 0) throw new Error("cannot discover child LLM routes because the `llm` service is unavailable");
147
+ if (request.model !== void 0 && request.provider === void 0) throw new Error("`model` requires `provider`");
148
+ if (request.provider === void 0) {
149
+ const providers = llm.listProviders().filter((provider) => policy.routes.some((route) => route.provider === provider.id));
150
+ return providers.length === 0 ? "(no LLM providers)" : providers.map((provider) => `${provider.id} — ${provider.name}`).join("\n");
151
+ }
152
+ if (request.provider.length === 0) throw new Error("`provider` must be non-empty");
153
+ const allowedRoutes = policy.routes.filter((route) => route.provider === request.provider);
154
+ if (allowedRoutes.length === 0) throw new Error(`LLM provider "${request.provider}" is not allowed for this Session`);
155
+ const provider = registeredProvider(llm, policy, request.provider);
156
+ if (request.model === void 0) {
157
+ const models = (await llm.listModels(provider.id)).filter((model) => allowedRoutes.some((route) => route.model === model.id));
158
+ return models.length === 0 ? `(no advertised models for ${provider.id})` : models.map((model) => modelLine(provider.id, model)).join("\n");
159
+ }
160
+ if (request.model.length === 0) throw new Error("`model` must be non-empty");
161
+ if (!allowedRoutes.some((route) => route.model === request.model)) throw new Error(`child LLM route "${provider.id}/${request.model}" is not allowed for this Session`);
162
+ const model = await llm.resolveModelInfo(provider.id, request.model, signal);
163
+ const efforts = model.reasoning?.efforts.map((effort) => `${effort.id}${model.reasoning?.defaultEffort === effort.id ? " (default)" : ""} — ${effort.name}` + (effort.description === void 0 ? "" : `: ${effort.description}`)).join("\n") || "(no advertised reasoning efforts)";
164
+ return `${modelLine(provider.id, model)}\nReasoning efforts:\n${efforts}`;
165
+ }
166
+ /**
167
+ * Register `list_subagent_models` for one owning delegation-tool instance.
168
+ * @param ctx - Context whose tool registry owns the fixed discovery definition.
169
+ * @param policy - Route policy captured for this Session.
170
+ */
171
+ function registerListSubagentModels(ctx, policy) {
172
+ ctx.tools.register(defineTool({
173
+ name: "list_subagent_models",
174
+ description: "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.",
175
+ parameters: {
176
+ provider: {
177
+ type: "string",
178
+ description: "Registered LLM provider id. Omit to list providers."
179
+ },
180
+ model: {
181
+ type: "string",
182
+ description: "Exact model id to inspect. Requires provider; omit to list that provider's advertised models."
183
+ }
184
+ },
185
+ output: {
186
+ schema: { type: "string" },
187
+ render: (_args, result) => [{
188
+ type: "text",
189
+ text: result
190
+ }]
191
+ },
192
+ execute(args, exec) {
193
+ return listSubagentModels(ctx, policy, args, exec.signal);
194
+ }
195
+ }));
196
+ }
197
+ /** Host-only projection of the durable model-selection policy. */
198
+ const subagentModelSelectionProjectionDefinition = {
199
+ key: "subagentModelSelectionPolicy",
200
+ stateVersion: 1,
201
+ stateSchema: z$1.array(z$1.object({
202
+ provider: z$1.string().min(1),
203
+ model: z$1.string().min(1)
204
+ }).strict()).min(1).nullable(),
205
+ init: () => null,
206
+ apply: (policy, event) => {
207
+ if (policy !== null || event.type !== "subagent/model-selection-policy") return policy;
208
+ const { allowedModels } = event.data;
209
+ assertAllowedModelRoutes(allowedModels);
210
+ if (allowedModels.length === 0) throw new Error("subagent/model-selection-policy requires at least one route");
211
+ return allowedModels;
212
+ }
213
+ };
214
+ /**
215
+ * Read the exact route list captured for a model-selectable definition.
216
+ * @param projections - registry that owns the policy projection.
217
+ * @param session - session whose durable decision is read.
218
+ * @returns a detached route list, or undefined for the fixed-route definition.
219
+ */
220
+ function subagentModelSelectionPolicy(projections, session) {
221
+ return projections.stateOf(session, "subagentModelSelectionPolicy")?.map((route) => ({ ...route }));
222
+ }
223
+ /**
224
+ * Append the route policy once, before its definition can reach a model request.
225
+ * @param projections - registry that owns the policy projection.
226
+ * @param session - session receiving the model-selectable definition.
227
+ * @param allowedModels - exact routes the definition may select explicitly.
228
+ */
229
+ function recordSubagentModelSelection(projections, session, allowedModels) {
230
+ if (subagentModelSelectionPolicy(projections, session) !== void 0) return;
231
+ session.append("subagent/model-selection-policy", { allowedModels: allowedModels.map((route) => ({ ...route })) });
232
+ }
233
+ //#endregion
4
234
  //#region lib/types/index.js
5
235
  /**
6
236
  * Model-facing delegation through one configured `ctx.subagents` provider.
@@ -15,18 +245,19 @@ const name = "tool-subagent";
15
245
  const inject = [
16
246
  "tools",
17
247
  "subagents",
18
- "systemPrompt"
248
+ "systemPrompt",
249
+ "sessionProjections"
19
250
  ];
20
- /** Prompt order after bounded delegation policy and before child reporting. */
21
- const SUBAGENT_SECTION_ORDER = 116.5;
22
251
  const Config = z.object({
23
252
  provider: z.string().required(),
24
253
  toolName: z.string().default("subagent"),
254
+ modelSelectionSettings: z.boolean().default(false),
25
255
  enableRunInBackground: z.boolean().default(true),
26
256
  backgroundMode: z.union(["one-shot", "continuable"]).default("one-shot"),
27
257
  agentOptions: z.object({
28
258
  provider: z.string(),
29
259
  model: z.string(),
260
+ reasoningEffort: z.string().min(1),
30
261
  maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER)
31
262
  }).default(void 0),
32
263
  persona: z.string(),
@@ -133,164 +364,286 @@ function apply(ctx, config) {
133
364
  const backgroundEnabled = config.enableRunInBackground !== false;
134
365
  const continuable = (config.backgroundMode ?? "one-shot") === "continuable";
135
366
  const toolName = config.toolName ?? "subagent";
136
- let disposeTool;
137
- const mount = (provider) => {
138
- if (typeof config.maxDepth === "number" && !provider.capabilities.depthLimit) throw new Error(`tool-subagent: provider "${provider.name}" cannot enforce maxDepth (no depthLimit capability) — set maxDepth: 'provider-managed' to leave the recursion budget to the provider`);
139
- const wording = providerWording(provider.inheritsParentContext);
140
- if (continuable && provider.prepareContinuable === void 0) throw new Error(`tool-subagent: provider "${provider.name}" does not support \`backgroundMode: continuable\``);
141
- disposeTool = ctx.tools.register(defineTool({
142
- name: toolName,
143
- description: wording.description + (backgroundEnabled ? continuable ? " This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result." : " This call waits for the result by default. Set `run_in_background: true` to return a job id; collect with `job_output` and stop with `job_kill`." : " This call waits for the subagent and returns its result."),
144
- parameters: {
145
- description: {
146
- type: "string",
147
- required: true,
148
- description: "A short (3-5 word) description of the delegated task, for display."
149
- },
150
- prompt: {
151
- type: "string",
152
- required: true,
153
- description: wording.promptDescription
154
- },
155
- ...backgroundEnabled ? { run_in_background: {
156
- type: "boolean",
157
- description: continuable ? "Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it." : "Whether to run as a background job and return its id. Defaults to false; collect with job_output or stop with job_kill."
158
- } } : {}
159
- },
160
- output: {
161
- schema: { oneOf: [
162
- {
163
- type: "object",
164
- additionalProperties: false,
165
- properties: {
166
- kind: {
367
+ const modelSelectionCapable = config.modelSelectionSettings === true;
368
+ ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition);
369
+ const assertSubagentProviderConfiguration = (subagentProvider) => {
370
+ if (typeof config.maxDepth === "number" && !subagentProvider.capabilities.depthLimit) throw new Error(`tool-subagent: provider "${subagentProvider.name}" cannot enforce maxDepth (no depthLimit capability) — set maxDepth: 'provider-managed' to leave the recursion budget to the provider`);
371
+ if (config.agentOptions !== void 0 && !subagentProvider.capabilities.agentOptions) throw new Error(`tool-subagent: provider "${subagentProvider.name}" does not support child agentOptions`);
372
+ if (modelSelectionCapable && !subagentProvider.capabilities.agentOptions) throw new Error(`tool-subagent: provider "${subagentProvider.name}" does not support child model selection`);
373
+ if (continuable && subagentProvider.prepareContinuable === void 0) throw new Error(`tool-subagent: provider "${subagentProvider.name}" does not support \`backgroundMode: continuable\``);
374
+ };
375
+ ctx.on("subagent/provider-added", (subagentProvider) => {
376
+ if (subagentProvider.name === config.provider) assertSubagentProviderConfiguration(subagentProvider);
377
+ });
378
+ const initialProvider = ctx.subagents.getProvider(config.provider);
379
+ if (initialProvider !== void 0) assertSubagentProviderConfiguration(initialProvider);
380
+ const install = (runtimeCtx, modelSelectionPolicy) => {
381
+ const modelSelectionEnabled = modelSelectionPolicy !== void 0;
382
+ if (modelSelectionPolicy !== void 0) registerListSubagentModels(runtimeCtx, modelSelectionPolicy);
383
+ let mounted;
384
+ const mount = (subagentProvider) => {
385
+ assertSubagentProviderConfiguration(subagentProvider);
386
+ const wording = providerWording(subagentProvider.inheritsParentContext);
387
+ const providerRouteDefaults = subagentProvider.agentRouteDefaults;
388
+ const choiceDescription = !modelSelectionEnabled ? "" : (providerRouteDefaults !== void 0 ? " Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and this provider's route defaults. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort." : " Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.") + (subagentProvider.inheritsParentContext ? " Changing the route can prevent provider-side reuse of the inherited conversation prefix." : "");
389
+ mounted = {
390
+ subagentProvider,
391
+ disposeTool: runtimeCtx.tools.register(defineTool({
392
+ name: toolName,
393
+ description: wording.description + (backgroundEnabled ? continuable ? " This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result." : " This call waits for the result by default. Set `run_in_background: true` to return a job id; collect with `job_output` and stop with `job_kill`." : " This call waits for the subagent and returns its result.") + choiceDescription,
394
+ parameters: {
395
+ description: {
396
+ type: "string",
397
+ required: true,
398
+ description: "A short (3-5 word) description of the delegated task, for display."
399
+ },
400
+ prompt: {
401
+ type: "string",
402
+ required: true,
403
+ description: wording.promptDescription
404
+ },
405
+ ...modelSelectionEnabled ? {
406
+ provider: {
167
407
  type: "string",
168
- required: true,
169
- const: "background"
408
+ description: providerRouteDefaults !== void 0 ? "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or this provider's route defaults." : "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route."
170
409
  },
171
- jobId: {
172
- type: "string",
173
- required: true
174
- }
175
- }
176
- },
177
- {
178
- type: "object",
179
- additionalProperties: false,
180
- properties: {
181
- kind: {
410
+ model: {
182
411
  type: "string",
183
- required: true,
184
- const: "continuable"
412
+ description: providerRouteDefaults !== void 0 ? "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or this provider's route defaults." : "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route."
185
413
  },
186
- subagentId: {
414
+ reasoning_effort: {
187
415
  type: "string",
188
- required: true
416
+ description: providerRouteDefaults !== void 0 ? "Adapter-owned reasoning effort for the effective child route. Omit to use a compatible configured effort or the selected model's default." : "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default."
189
417
  }
190
- }
418
+ } : {},
419
+ ...backgroundEnabled ? { run_in_background: {
420
+ type: "boolean",
421
+ description: continuable ? "Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it." : "Whether to run as a background job and return its id. Defaults to false; collect with job_output or stop with job_kill."
422
+ } } : {}
191
423
  },
192
- {
193
- type: "object",
194
- additionalProperties: false,
195
- properties: {
196
- kind: {
197
- type: "string",
198
- required: true,
199
- const: "foreground"
424
+ output: {
425
+ schema: { oneOf: [
426
+ {
427
+ type: "object",
428
+ additionalProperties: false,
429
+ properties: {
430
+ kind: {
431
+ type: "string",
432
+ required: true,
433
+ const: "background"
434
+ },
435
+ jobId: {
436
+ type: "string",
437
+ required: true
438
+ }
439
+ }
200
440
  },
201
- runId: {
202
- type: "string",
203
- required: true
441
+ {
442
+ type: "object",
443
+ additionalProperties: false,
444
+ properties: {
445
+ kind: {
446
+ type: "string",
447
+ required: true,
448
+ const: "continuable"
449
+ },
450
+ subagentId: {
451
+ type: "string",
452
+ required: true
453
+ }
454
+ }
204
455
  },
205
- output: {
206
- type: "array",
207
- required: true,
208
- items: { type: "json" }
456
+ {
457
+ type: "object",
458
+ additionalProperties: false,
459
+ properties: {
460
+ kind: {
461
+ type: "string",
462
+ required: true,
463
+ const: "foreground"
464
+ },
465
+ runId: {
466
+ type: "string",
467
+ required: true
468
+ },
469
+ output: {
470
+ type: "array",
471
+ required: true,
472
+ items: { type: "json" }
473
+ }
474
+ }
209
475
  }
476
+ ] },
477
+ render: (_args, value) => [{
478
+ type: "text",
479
+ text: value.kind === "background" ? `started background subagent job ${value.jobId}` : value.kind === "continuable" ? `started subagent ${value.subagentId}` : outputValueText(value.output)
480
+ }]
481
+ },
482
+ isConcurrencySafe: () => true,
483
+ async execute(args, exec) {
484
+ const parent = exec.agent;
485
+ if (!parent) throw new Error("subagent tool requires a calling agent (exec.agent was undefined)");
486
+ const modelRequest = args;
487
+ const parentOptions = parentAgentOptionsForDelegation(parent);
488
+ const requiresRoutePreflight = hasDelegationModelRequest(modelRequest) || hasConfiguredLlmSelection(config.agentOptions);
489
+ const requestedChildAgentOptions = requestedAgentOptions(parentOptions, requiresRoutePreflight && providerRouteDefaults !== void 0 ? {
490
+ ...providerRouteDefaults,
491
+ ...config.agentOptions
492
+ } : config.agentOptions, modelRequest, modelSelectionEnabled);
493
+ assertAllowedModelSelection(modelSelectionPolicy, parentOptions, requestedChildAgentOptions, modelRequest);
494
+ if (requiresRoutePreflight) {
495
+ const llm = runtimeCtx.get("llm");
496
+ if (llm === void 0) throw new Error("cannot resolve the selected child LLM route because the `llm` service is unavailable");
497
+ await preflightChildLlmRoute(llm, parentOptions, requestedChildAgentOptions, exec.signal, providerRouteDefaults === void 0);
498
+ if (runtimeCtx.subagents.getProvider(config.provider) !== subagentProvider) throw new Error(`subagent provider "${config.provider}" changed while resolving the child LLM route; retry the delegation`);
210
499
  }
211
- }
212
- ] },
213
- render: (_args, value) => [{
214
- type: "text",
215
- text: value.kind === "background" ? `started background subagent job ${value.jobId}` : value.kind === "continuable" ? `started subagent ${value.subagentId}` : outputValueText(value.output)
216
- }]
217
- },
218
- isConcurrencySafe: () => true,
219
- async execute(args, exec) {
220
- const parent = exec.agent;
221
- if (!parent) throw new Error("subagent tool requires a calling agent (exec.agent was undefined)");
222
- const maxDepth = typeof config.maxDepth === "number" ? config.maxDepth : void 0;
223
- const request = {
224
- label: args.description,
225
- prompt: [{
226
- type: "text",
227
- text: args.prompt
228
- }],
229
- parent,
230
- ...config.agentOptions !== void 0 ? { agentOptions: config.agentOptions } : {},
231
- ...config.persona !== void 0 ? { persona: config.persona } : {},
232
- ...config.toolFilter !== void 0 ? { toolFilter: config.toolFilter } : {},
233
- ...maxDepth !== void 0 ? { maxDepth } : {}
234
- };
235
- if (resolveDelegationRun(args, {
236
- backgroundEnabled,
237
- continuable
238
- }).runInBackground) {
239
- if (continuable) return {
240
- kind: "continuable",
241
- subagentId: (await ctx.subagents.startContinuable({
242
- provider: config.provider,
500
+ exec.signal.throwIfAborted();
501
+ const maxDepth = typeof config.maxDepth === "number" ? config.maxDepth : void 0;
502
+ const request = {
243
503
  label: args.description,
244
- request,
504
+ prompt: [{
505
+ type: "text",
506
+ text: args.prompt
507
+ }],
508
+ parent,
509
+ ...requestedChildAgentOptions !== void 0 ? { agentOptions: requestedChildAgentOptions } : {},
510
+ ...config.persona !== void 0 ? { persona: config.persona } : {},
511
+ ...config.toolFilter !== void 0 ? { toolFilter: config.toolFilter } : {},
512
+ ...maxDepth !== void 0 ? { maxDepth } : {}
513
+ };
514
+ if (resolveDelegationRun(args, {
515
+ backgroundEnabled,
516
+ continuable
517
+ }).runInBackground) {
518
+ if (continuable) return {
519
+ kind: "continuable",
520
+ subagentId: (await runtimeCtx.subagents.startContinuable({
521
+ provider: config.provider,
522
+ label: args.description,
523
+ request,
524
+ signal: exec.signal
525
+ })).childId
526
+ };
527
+ const jobs = runtimeCtx.get("jobs");
528
+ if (jobs === void 0) throw new Error("background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs");
529
+ return {
530
+ kind: "background",
531
+ jobId: jobs.start({
532
+ kind: "subagent",
533
+ label: args.description,
534
+ owner: parent,
535
+ run: () => {
536
+ const controller = new AbortController();
537
+ return {
538
+ cancel: (reason) => {
539
+ controller.abort(reason ?? "background subagent task killed");
540
+ },
541
+ done: settleStart(runtimeCtx.subagents.start(config.provider, {
542
+ ...request,
543
+ signal: controller.signal
544
+ }), controller.signal)
545
+ };
546
+ }
547
+ })
548
+ };
549
+ }
550
+ return settleForegroundRun(await runtimeCtx.subagents.start(config.provider, {
551
+ ...request,
245
552
  signal: exec.signal
246
- })).childId
247
- };
248
- const jobs = ctx.get("jobs");
249
- if (jobs === void 0) throw new Error("background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs");
250
- return {
251
- kind: "background",
252
- jobId: jobs.start({
253
- kind: "subagent",
254
- label: args.description,
255
- owner: parent,
256
- run: () => {
257
- const controller = new AbortController();
258
- return {
259
- cancel: (reason) => {
260
- controller.abort(reason ?? "background subagent task killed");
261
- },
262
- done: settleStart(ctx.subagents.start(config.provider, {
263
- ...request,
264
- signal: controller.signal
265
- }), controller.signal)
266
- };
267
- }
268
- })
269
- };
270
- }
271
- return settleForegroundRun(await ctx.subagents.start(config.provider, {
272
- ...request,
273
- signal: exec.signal
274
- }));
553
+ }));
554
+ }
555
+ }))
556
+ };
557
+ };
558
+ runtimeCtx.on("subagent/provider-added", (subagentProvider) => {
559
+ if (subagentProvider.name === config.provider && mounted === void 0) mount(subagentProvider);
560
+ });
561
+ runtimeCtx.on("subagent/provider-removed", (name) => {
562
+ if (name !== config.provider || mounted === void 0) return;
563
+ mounted.disposeTool();
564
+ mounted = void 0;
565
+ });
566
+ const present = runtimeCtx.subagents.getProvider(config.provider);
567
+ if (present !== void 0) mount(present);
568
+ else runtimeCtx.logger.info(`subagent provider "${config.provider}" not registered yet; the "${config.toolName ?? "subagent"}" tool will register when it appears`);
569
+ if (backgroundEnabled && continuable) runtimeCtx.systemPrompt.section({
570
+ name: `tool:${toolName}`,
571
+ order: runtimeCtx.systemPrompt.getSectionOrder("TOOL_SUBAGENT"),
572
+ text: (context) => mounted === void 0 || runtimeCtx.tools.get(toolName, context.scope) === void 0 ? "" : `Use ${toolName} in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set \`run_in_background: false\` only when your next action depends on that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.`
573
+ });
574
+ };
575
+ if (config.modelSelectionSettings !== true) {
576
+ install(ctx, void 0);
577
+ return;
578
+ }
579
+ const settings = ctx.get("subagentModelSelection");
580
+ if (settings === void 0) throw new Error("tool-subagent: `modelSelectionSettings` requires @deepseek-ai/dsh-tool-subagent/model-selection-settings in the Host scope");
581
+ const compositionScope = scopeOf(ctx);
582
+ if (compositionScope === void 0) throw new Error("tool-subagent: `modelSelectionSettings` requires an Agent or preset scope");
583
+ const selectForAgent = (agent) => {
584
+ let allowedModels = subagentModelSelectionPolicy(ctx.sessionProjections, agent.session);
585
+ if (allowedModels === void 0) {
586
+ const parentId = agent.session.header.origin === "subagent" ? agent.session.header.parentSession : void 0;
587
+ if (parentId !== void 0) {
588
+ const parent = ctx.get("agents")?.get(parentId);
589
+ allowedModels = parent === void 0 ? void 0 : subagentModelSelectionPolicy(ctx.sessionProjections, parent.session);
590
+ } else if (agent.session.firstLiveSeq === 0) {
591
+ const current = settings.current();
592
+ allowedModels = current.enabled ? current.allowedModels : void 0;
275
593
  }
276
- }));
594
+ }
595
+ if (allowedModels !== void 0) recordSubagentModelSelection(ctx.sessionProjections, agent.session, allowedModels);
596
+ return allowedModels === void 0 ? void 0 : { routes: allowedModels };
277
597
  };
278
- ctx.on("subagent/provider-added", (provider) => {
279
- if (provider.name === config.provider && disposeTool === void 0) mount(provider);
280
- });
281
- ctx.on("subagent/provider-removed", (name) => {
282
- if (name !== config.provider || disposeTool === void 0) return;
283
- disposeTool();
284
- disposeTool = void 0;
598
+ const agent = ctx.agent;
599
+ if (agent !== void 0) {
600
+ install(ctx, selectForAgent(agent));
601
+ return;
602
+ }
603
+ const agents = ctx.get("agents");
604
+ /* v8 ignore next -- Agent and preset scopes are minted only by the Agent registry. */
605
+ if (agents === void 0) throw new Error("tool-subagent: scoped model-selection settings require the Agent registry");
606
+ const scopedInstalls = /* @__PURE__ */ new WeakMap();
607
+ const installing = /* @__PURE__ */ new WeakSet();
608
+ const belongsToComposition = (candidate) => scopeChainOf(scopeOf(candidate.ctx)).includes(compositionScope);
609
+ const installScoped = (candidate) => {
610
+ if (scopedInstalls.has(candidate) || installing.has(candidate)) return;
611
+ installing.add(candidate);
612
+ let fiber;
613
+ try {
614
+ const policy = selectForAgent(candidate);
615
+ fiber = candidate.ctx.inject([
616
+ "tools",
617
+ "subagents",
618
+ "systemPrompt"
619
+ ], (runtimeCtx) => {
620
+ install(runtimeCtx, policy);
621
+ });
622
+ } finally {
623
+ installing.delete(candidate);
624
+ }
625
+ scopedInstalls.set(candidate, fiber);
626
+ };
627
+ const removeScoped = (candidate) => {
628
+ const fiber = scopedInstalls.get(candidate);
629
+ if (fiber === void 0) return;
630
+ scopedInstalls.delete(candidate);
631
+ /* v8 ignore next 3 -- Cordis Fiber disposal contains registration cleanup failures; this is the final diagnostic sink. */
632
+ fiber.dispose().catch((error) => {
633
+ ctx.logger.warn(`tool-subagent: failed to remove recomposed Agent "${candidate.id}" definitions: ${String(error)}`);
634
+ });
635
+ };
636
+ const reconcileComposedAgents = () => {
637
+ for (const candidate of agents.list()) if (belongsToComposition(candidate)) installScoped(candidate);
638
+ else removeScoped(candidate);
639
+ };
640
+ ctx.on("agent/created", ({ agent: created }) => {
641
+ installScoped(created);
285
642
  });
286
- const present = ctx.subagents.getProvider(config.provider);
287
- if (present !== void 0) mount(present);
288
- else ctx.logger.info(`subagent provider "${config.provider}" not registered yet; the "${config.toolName ?? "subagent"}" tool will register when it appears`);
289
- if (backgroundEnabled && continuable) ctx.systemPrompt.section({
290
- name: `tool:${toolName}`,
291
- order: SUBAGENT_SECTION_ORDER,
292
- text: (context) => disposeTool === void 0 || ctx.tools.get(toolName, context.scope) === void 0 ? "" : `Use ${toolName} in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set \`run_in_background: false\` only when your next action depends on that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.`
643
+ ctx.on("agent/disposed", ({ agent: disposed }) => {
644
+ removeScoped(disposed);
293
645
  });
646
+ ctx.on("tools/change", reconcileComposedAgents);
294
647
  }
295
648
  //#endregion
296
649
  export { Config, apply, inject, name };