@xneog/dsh-tool-subagent 0.1.0 → 0.1.3-alpha.1

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