@crewhaus/tool-task 0.5.7 → 0.6.0

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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { SubAgentDefinition, SubAgentFailure } from "@crewhaus/agent-context-isolation";
1
+ import { type SubAgentDefinition, type SubAgentFailure } from "@crewhaus/agent-context-isolation";
2
2
  import { CrewhausError } from "@crewhaus/errors";
3
3
  import type { RegisteredTool } from "@crewhaus/tool-catalog";
4
4
  import { z } from "zod";
@@ -25,14 +25,17 @@ declare const taskInputSchema: z.ZodObject<{
25
25
  description: z.ZodString;
26
26
  prompt: z.ZodString;
27
27
  subagent_type: z.ZodOptional<z.ZodString>;
28
+ profile: z.ZodOptional<z.ZodString>;
28
29
  }, "strip", z.ZodTypeAny, {
29
30
  description: string;
30
31
  prompt: string;
31
32
  subagent_type?: string | undefined;
33
+ profile?: string | undefined;
32
34
  }, {
33
35
  description: string;
34
36
  prompt: string;
35
37
  subagent_type?: string | undefined;
38
+ profile?: string | undefined;
36
39
  }>;
37
40
  export type TaskInput = z.infer<typeof taskInputSchema>;
38
41
  /**
package/dist/index.js CHANGED
@@ -1,11 +1,14 @@
1
1
  /**
2
2
  * Catalog R4 `tool-task` — Section 13.
3
3
  *
4
- * The `Task(description, prompt, subagent_type?)` tool. When invoked, it
5
- * resolves the named sub-agent definition, builds the child's tool catalog,
6
- * resolves child permissions, and dispatches `bridge.spawnSubAgent`.
7
- * Returns the child's final assistant text as the tool result so the parent
8
- * model can react to the child's verdict.
4
+ * The `Task(description, prompt, subagent_type?, profile?)` tool. When
5
+ * invoked, it resolves the named sub-agent definition, builds the child's tool
6
+ * catalog, resolves child permissions, validates the optional `profile`
7
+ * against the definition's allowlist (0.6.0 §7.7 — a model-filled model
8
+ * argument that can never name a model outside the spec), and dispatches
9
+ * `bridge.spawnSubAgent` with the ONE shared `projectParentHandle(bridge)`
10
+ * projection. Returns the child's final assistant text as the tool result so
11
+ * the parent model can react to the child's verdict.
9
12
  *
10
13
  * Resolution order for `subagent_type`:
11
14
  * 1. `opts.subAgents.get(name)` — inline spec map (codegen-supplied).
@@ -37,6 +40,7 @@
37
40
  */
38
41
  import { existsSync, readFileSync } from "node:fs";
39
42
  import { join } from "node:path";
43
+ import { projectParentHandle, subAgentProfileAllowlist, } from "@crewhaus/agent-context-isolation";
40
44
  import { CrewhausError } from "@crewhaus/errors";
41
45
  import { resolveChildPermissions } from "@crewhaus/sub-agent-permission-inheritance";
42
46
  import { buildTool } from "@crewhaus/tool-builder";
@@ -83,7 +87,49 @@ const taskInputSchema = z.object({
83
87
  .min(1)
84
88
  .optional()
85
89
  .describe("Name of the sub-agent definition to spawn. Falls back to 'general-purpose' if omitted."),
90
+ // 0.6.0 §7.7 — a model-filled MODEL argument. Validated in `execute` against
91
+ // the definition's allowlist (`allowed_profiles`, else the child's own
92
+ // model/profile) — never resolved, never passed through: it can only ever
93
+ // name a model the spec already declared for that child (§10.1).
94
+ profile: z
95
+ .string()
96
+ .min(1)
97
+ .optional()
98
+ .describe("Model profile to run the sub-agent on. Must be one of the sub-agent's allowed profiles; omit to use its default model."),
86
99
  });
100
+ /** Frontmatter mirror of a `thinking` block (spec grammar: exactly one of the two). */
101
+ const FRONTMATTER_THINKING = z.union([
102
+ z.object({ budget_tokens: z.number().int().min(1024) }).strict(),
103
+ z.object({ effort: z.enum(["low", "medium", "high"]) }).strict(),
104
+ ]);
105
+ const FRONTMATTER_CIRCUIT_BREAKER = z
106
+ .object({
107
+ failureThreshold: z.number().int().positive().optional(),
108
+ windowMs: z.number().int().positive().optional(),
109
+ cooldownMs: z.number().int().positive().optional(),
110
+ })
111
+ .strict();
112
+ const FRONTMATTER_TIER_ROUTING = z
113
+ .object({
114
+ contextTokenThreshold: z.number().int().positive().optional(),
115
+ toolsToDefault: z.boolean().optional(),
116
+ firstTurnToDefault: z.boolean().optional(),
117
+ priorToolDensityThreshold: z.number().nonnegative().optional(),
118
+ })
119
+ .strict();
120
+ /**
121
+ * On-disk `<name>.md` frontmatter. Mirrors the spec's `sub_agents.<n>` keys
122
+ * (0.6.0 §7.7 adds the routing quartet, the params, `budget_share`,
123
+ * `inherit_routing` and `allowed_profiles`). Two honest differences from the
124
+ * spec form, because a file on disk has no `models:` registry to resolve
125
+ * against: `model` and every candidate are plain model strings (no `$ref`),
126
+ * and `allowed_profiles` entries name their model explicitly —
127
+ * `{ profile, model, thinking?, max_tokens?, temperature?, model_fallbacks?,
128
+ * circuit_breaker? }` — the already-resolved shape the compiler produces for a
129
+ * spec-declared sub-agent. `model_pool` is validated for its routing identity
130
+ * (`candidates[].{model, tags}`, `policy`) and otherwise passed through: the
131
+ * runtime owns the per-candidate settings grammar.
132
+ */
87
133
  const FRONTMATTER_SCHEMA = z.object({
88
134
  name: z.string().min(1),
89
135
  description: z.string().min(1),
@@ -96,7 +142,50 @@ const FRONTMATTER_SCHEMA = z.object({
96
142
  ])
97
143
  .optional(),
98
144
  inherit_bypass: z.boolean().optional(),
145
+ overlay: z.string().min(1).optional(),
146
+ thinking: FRONTMATTER_THINKING.optional(),
147
+ max_tokens: z.number().int().positive().optional(),
148
+ temperature: z.number().min(0).max(2).optional(),
149
+ model_fallbacks: z.array(z.string().min(1)).min(1).optional(),
150
+ circuit_breaker: FRONTMATTER_CIRCUIT_BREAKER.optional(),
151
+ model_tiers: z
152
+ .object({
153
+ fast: z.string().min(1),
154
+ default: z.string().min(1),
155
+ routing: FRONTMATTER_TIER_ROUTING.optional(),
156
+ })
157
+ .strict()
158
+ .optional(),
159
+ model_pool: z
160
+ .object({
161
+ candidates: z
162
+ .array(z.object({ model: z.string().min(1), tags: z.array(z.string().min(1)) }).passthrough())
163
+ .min(1),
164
+ policy: z.enum(["static", "heuristic", "learned", "classifier"]).optional(),
165
+ })
166
+ .passthrough()
167
+ .optional(),
168
+ budget_share: z.number().gt(0).max(1).optional(),
169
+ inherit_routing: z.boolean().optional(),
170
+ allowed_profiles: z
171
+ .array(z
172
+ .object({
173
+ profile: z.string().min(1),
174
+ model: z.string().min(1),
175
+ thinking: FRONTMATTER_THINKING.optional(),
176
+ max_tokens: z.number().int().positive().optional(),
177
+ temperature: z.number().min(0).max(2).optional(),
178
+ model_fallbacks: z.array(z.string().min(1)).min(1).optional(),
179
+ circuit_breaker: FRONTMATTER_CIRCUIT_BREAKER.optional(),
180
+ })
181
+ .strict())
182
+ .min(1)
183
+ .optional(),
99
184
  });
185
+ /** Spec-spelled thinking block → the runtime `IrThinking` shape. */
186
+ function thinkingFromFrontmatter(t) {
187
+ return "budget_tokens" in t ? { budgetTokens: t.budget_tokens } : { effort: t.effort };
188
+ }
100
189
  /**
101
190
  * Parse a `<dir>/<name>.md` sub-agent file. Same shape as SKILL.md: a
102
191
  * leading `---` frontmatter block; the body is the agent instructions.
@@ -144,6 +233,45 @@ export function parseSubAgentFile(content, fallbackName) {
144
233
  ...(parsed.data.inherit_bypass !== undefined
145
234
  ? { inherit_bypass: parsed.data.inherit_bypass }
146
235
  : {}),
236
+ // 0.6.0 §7.7 — routing + params, spelled as the runtime definition holds
237
+ // them (the IR's names). Every key is copied only when present. `overlay`
238
+ // is the default-profile prompt prefix a pinned `allowed_profiles` entry
239
+ // replaces (the spawner folds whichever applies; the body stays raw).
240
+ ...(parsed.data.overlay !== undefined ? { overlay: parsed.data.overlay } : {}),
241
+ ...(parsed.data.thinking !== undefined
242
+ ? { thinking: thinkingFromFrontmatter(parsed.data.thinking) }
243
+ : {}),
244
+ ...(parsed.data.max_tokens !== undefined ? { maxTokens: parsed.data.max_tokens } : {}),
245
+ ...(parsed.data.temperature !== undefined ? { temperature: parsed.data.temperature } : {}),
246
+ ...(parsed.data.model_fallbacks !== undefined
247
+ ? { modelFallbacks: parsed.data.model_fallbacks }
248
+ : {}),
249
+ ...(parsed.data.circuit_breaker !== undefined
250
+ ? { circuitBreaker: parsed.data.circuit_breaker }
251
+ : {}),
252
+ ...(parsed.data.model_tiers !== undefined ? { modelTiers: parsed.data.model_tiers } : {}),
253
+ ...(parsed.data.model_pool !== undefined
254
+ ? { modelPool: parsed.data.model_pool }
255
+ : {}),
256
+ ...(parsed.data.budget_share !== undefined ? { budgetShare: parsed.data.budget_share } : {}),
257
+ ...(parsed.data.inherit_routing !== undefined
258
+ ? { inheritRouting: parsed.data.inherit_routing }
259
+ : {}),
260
+ ...(parsed.data.allowed_profiles !== undefined
261
+ ? {
262
+ allowedProfiles: parsed.data.allowed_profiles.map((o) => ({
263
+ profile: o.profile,
264
+ model: o.model,
265
+ ...(o.thinking !== undefined
266
+ ? { thinking: thinkingFromFrontmatter(o.thinking) }
267
+ : {}),
268
+ ...(o.max_tokens !== undefined ? { maxTokens: o.max_tokens } : {}),
269
+ ...(o.temperature !== undefined ? { temperature: o.temperature } : {}),
270
+ ...(o.model_fallbacks !== undefined ? { modelFallbacks: o.model_fallbacks } : {}),
271
+ ...(o.circuit_breaker !== undefined ? { circuitBreaker: o.circuit_breaker } : {}),
272
+ })),
273
+ }
274
+ : {}),
147
275
  };
148
276
  return def;
149
277
  }
@@ -210,9 +338,22 @@ function buildChildCatalog(parentTools, def) {
210
338
  }
211
339
  export function createTaskTool(opts = {}) {
212
340
  const knownNames = opts.subAgents !== undefined ? [...opts.subAgents.keys()] : [];
213
- const description = knownNames.length === 0
341
+ // 0.6.0 §7.7 — advertise the `profile` argument only when some definition
342
+ // declares an allowlist, so a spec without `allowed_profiles` keeps the
343
+ // exact description it had (the tool list sits in the cached prefix).
344
+ const profileNote = opts.subAgents !== undefined &&
345
+ [...opts.subAgents.values()].some((d) => d.allowedProfiles !== undefined)
346
+ ? ` Pass \`profile\` to run a sub-agent on one of its allowed model profiles (${[
347
+ ...opts.subAgents.values(),
348
+ ]
349
+ .filter((d) => d.allowedProfiles !== undefined)
350
+ .map((d) => `${d.name}: ${(d.allowedProfiles ?? []).map((o) => o.profile).join("/")}`)
351
+ .join("; ")}).`
352
+ : "";
353
+ const description = (knownNames.length === 0
214
354
  ? "Spawn a sub-agent to handle a delegated task. Pass a `description` (label) and a `prompt` (the task instructions). The sub-agent runs in an isolated context and returns a single final message."
215
- : `Spawn a sub-agent to handle a delegated task. Pass a \`description\` (label), a \`prompt\` (the task instructions), and optionally a \`subagent_type\` to pick a specialist. Available subagent_type values: ${knownNames.join(", ")}. The sub-agent runs in an isolated context and returns a single final message.`;
355
+ : `Spawn a sub-agent to handle a delegated task. Pass a \`description\` (label), a \`prompt\` (the task instructions), and optionally a \`subagent_type\` to pick a specialist. Available subagent_type values: ${knownNames.join(", ")}. The sub-agent runs in an isolated context and returns a single final message.`) +
356
+ profileNote;
216
357
  return buildTool({
217
358
  name: "Task",
218
359
  description,
@@ -220,6 +361,13 @@ export function createTaskTool(opts = {}) {
220
361
  concurrencySafe: true,
221
362
  readOnly: false,
222
363
  destructive: false,
364
+ // Pillar 3 rule 6 (AGENTS.md): the sub-agent's `finalMessage` is already
365
+ // classified and lineage-tagged at the spawner boundary (origin
366
+ // "subagent"), so the runtime's post-tool pass at origin "tool" is
367
+ // skipped here — a second pass would double-warn and burn the cache.
368
+ // The rule documented this flag for years without a line setting it
369
+ // (0.6.0 plan §7.5 noted the gap); this is that line.
370
+ classifyOutput: false,
223
371
  // Per-call parallel eligibility (see the module header). A dispatch is
224
372
  // parallel-safe iff the resolved sub-agent's ENTIRE effective tool set
225
373
  // is read-only + concurrency-safe + non-destructive. Fail-closed: an
@@ -259,35 +407,25 @@ export function createTaskTool(opts = {}) {
259
407
  catch (err) {
260
408
  return `[Task error] ${err.message}`;
261
409
  }
410
+ // 0.6.0 §7.7 / §10.1 — the model-filled `profile` argument is checked
411
+ // against the definition's allowlist BEFORE anything is spawned. Thrown
412
+ // (not returned) so tool-executor renders it `is_error: true`: the
413
+ // parent model sees a refusal, not a result to build on.
414
+ if (input.profile !== undefined) {
415
+ const allowed = subAgentProfileAllowlist(def, bridge.model);
416
+ if (!allowed.includes(input.profile)) {
417
+ throw new SubAgentResolutionError(`profile "${input.profile}" is not allowed for sub-agent "${def.name}" — allowed: ${allowed.join(", ")}`);
418
+ }
419
+ }
262
420
  const childTools = buildChildCatalog(bridge.tools, def);
263
421
  const childPerms = resolveChildPermissions({ mode: bridge.permissionMode, rules: bridge.permissionRules }, def);
264
- const parentHandle = {
265
- runContext: bridge.runContext,
266
- eventLog: bridge.eventLog,
267
- permissionMode: bridge.permissionMode,
268
- permissionRules: bridge.permissionRules,
269
- tools: bridge.tools,
270
- model: bridge.model,
271
- maxTokens: bridge.maxTokens,
272
- ...(bridge.sessionRootDir !== undefined ? { sessionRootDir: bridge.sessionRootDir } : {}),
273
- // v0.3.0 §7.1 — the four child seams, projected by runtime-core onto
274
- // the bridge (recall-only memory, skills list, failure taxonomy,
275
- // read-only continuity) and handed to the spawner verbatim.
276
- ...(bridge.memory !== undefined ? { memory: bridge.memory } : {}),
277
- ...(bridge.skills !== undefined ? { skills: bridge.skills } : {}),
278
- ...(bridge.failureTaxonomy !== undefined
279
- ? { failureTaxonomy: bridge.failureTaxonomy }
280
- : {}),
281
- ...(bridge.continuity !== undefined ? { continuity: bridge.continuity } : {}),
282
- // Loop contract 0.4 (G11) — the parent's ask_mode + approval seam, so
283
- // a child's headless `ask` parks against the same store instead of
284
- // collapsing to a denial. THIS projection is the mechanism of the bug
285
- // it fixes: `RuntimeBridge` extends `ParentRunHandle`, but the handle
286
- // is hand-copied field by field and every seam field is optional — an
287
- // omission type-checks perfectly and drops the capability silently.
288
- ...(bridge.askMode !== undefined ? { askMode: bridge.askMode } : {}),
289
- ...(bridge.approvals !== undefined ? { approvals: bridge.approvals } : {}),
290
- };
422
+ // 0.6.0 §10.2 — ONE shared projection replaces the field-by-field hand
423
+ // copy this tool carried (and whose optional-seam omissions were the
424
+ // mechanism of the G11 approval bug): `projectParentHandle` lives beside
425
+ // `ParentRunHandle`, so a new handle field lands in the projection in
426
+ // the same file. The routing projection (served arm, budget cap) rides
427
+ // along for `inherit_routing` / `budget_share`.
428
+ const parentHandle = projectParentHandle(bridge);
291
429
  // A fatal child failure (billing/auth) makes `spawnSubAgent` throw
292
430
  // `RunFailedError`; it propagates through here and through
293
431
  // tool-executor's carve-out so the whole run halts with the child's
@@ -299,6 +437,7 @@ export function createTaskTool(opts = {}) {
299
437
  permissionRules: childPerms.rules,
300
438
  childTools,
301
439
  ...(bridge.sessionRootDir !== undefined ? { sessionRootDir: bridge.sessionRootDir } : {}),
440
+ ...(input.profile !== undefined ? { profile: input.profile } : {}),
302
441
  });
303
442
  if (result.failure !== undefined) {
304
443
  // Non-fatal classified failure → an is_error tool result carrying
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewhaus/tool-task",
3
- "version": "0.5.7",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Task(description, prompt, subagent_type?) tool — spawns a sub-agent and returns its final message (Section 13)",
6
6
  "main": "dist/index.js",
@@ -15,11 +15,11 @@
15
15
  "test": "bun test src"
16
16
  },
17
17
  "dependencies": {
18
- "@crewhaus/agent-context-isolation": "0.5.7",
19
- "@crewhaus/errors": "0.5.7",
20
- "@crewhaus/sub-agent-permission-inheritance": "0.5.7",
21
- "@crewhaus/tool-builder": "0.5.7",
22
- "@crewhaus/tool-catalog": "0.5.7",
18
+ "@crewhaus/agent-context-isolation": "0.6.0",
19
+ "@crewhaus/errors": "0.6.0",
20
+ "@crewhaus/sub-agent-permission-inheritance": "0.6.0",
21
+ "@crewhaus/tool-builder": "0.6.0",
22
+ "@crewhaus/tool-catalog": "0.6.0",
23
23
  "yaml": "^2.6.0",
24
24
  "zod": "^3.23.8"
25
25
  },