@warble/claude-agent-sdk 0.12.0 → 0.13.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.
@@ -254,6 +254,11 @@ interface EmittedMeta {
254
254
  }`;
255
255
  function emitAgentModule(prepared, opts = {}) {
256
256
  const standalone = opts.standalone ?? false;
257
+ if (prepared.dependencies.length > 0) {
258
+ throw new DispatchError(
259
+ "emit does not support component_invocation (wall-hit): a generated single-plan run() cannot carry the immutable prepared-callee registry, trusted active-step alias binding, or root budget ledger. Use the Agent SDK dispatch() runtime for composed entries."
260
+ );
261
+ }
257
262
  const header = [
258
263
  "// Generated by `warble-agent-sdk emit` \u2014 do not edit by hand.",
259
264
  `// Target: ${prepared.target}. Regenerate from the IR instead of editing.`,
@@ -294,6 +299,118 @@ function emitAgentModule(prepared, opts = {}) {
294
299
  ].join("\n");
295
300
  }
296
301
 
302
+ // src/closure.ts
303
+ function freezeArray(values) {
304
+ return Object.freeze(values);
305
+ }
306
+ function componentDependencies(ir) {
307
+ const dependencies = [];
308
+ for (const node of ir.components) {
309
+ const declaresCalls = node.llm_calls.some((step) => step.component_calls.length > 0);
310
+ if (declaresCalls && !node.required_capabilities.includes("component_invocation")) {
311
+ throw new DispatchError(
312
+ `invalid IR: component '${node.id}' declares component calls but does not require component_invocation, so the target capability wall could be bypassed`
313
+ );
314
+ }
315
+ for (const step of node.llm_calls) {
316
+ const aliases = /* @__PURE__ */ new Set();
317
+ for (const call of step.component_calls) {
318
+ if (aliases.has(call.alias)) {
319
+ throw new DispatchError(
320
+ `invalid IR: duplicate component call alias '${call.alias}' on '${node.id}.${step.name}'`
321
+ );
322
+ }
323
+ aliases.add(call.alias);
324
+ dependencies.push(
325
+ Object.freeze({ caller: node.id, step: step.name, alias: call.alias, component: call.component })
326
+ );
327
+ }
328
+ }
329
+ }
330
+ return freezeArray(dependencies);
331
+ }
332
+ function resolveComponentClosure(ir, requestedRoots) {
333
+ const byId = /* @__PURE__ */ new Map();
334
+ for (const node of ir.components) {
335
+ if (byId.has(node.id)) {
336
+ throw new DispatchError(`invalid IR: duplicate mounted component id '${node.id}'`);
337
+ }
338
+ byId.set(node.id, node);
339
+ }
340
+ const roots = requestedRoots ?? ir.components.filter((node) => node.entrypoint).map((node) => node.id);
341
+ const rootNodes = roots.map((id) => {
342
+ const node = byId.get(id);
343
+ if (!node) {
344
+ throw new DispatchError(
345
+ `component '${id}' not found in IR (available: ${ir.components.map((candidate) => candidate.id).join(", ")})`
346
+ );
347
+ }
348
+ if (!node.entrypoint) {
349
+ throw new DispatchError(
350
+ `component '${id}' is entrypoint:false and cannot be selected as a root entry`
351
+ );
352
+ }
353
+ return node;
354
+ });
355
+ const dependencies = componentDependencies(ir);
356
+ const outgoing = /* @__PURE__ */ new Map();
357
+ for (const node of ir.components) {
358
+ outgoing.set(
359
+ node.id,
360
+ dependencies.filter((dependency) => dependency.caller === node.id)
361
+ );
362
+ }
363
+ const entries = [];
364
+ const union = /* @__PURE__ */ new Set();
365
+ for (const root of rootNodes) {
366
+ const visited = /* @__PURE__ */ new Set();
367
+ const active = [];
368
+ const walk = (id) => {
369
+ if (active.includes(id)) {
370
+ const start = active.indexOf(id);
371
+ throw new DispatchError(
372
+ `invalid IR: component call cycle detected: ${[...active.slice(start), id].join(" -> ")}`
373
+ );
374
+ }
375
+ if (visited.has(id)) return;
376
+ const node = byId.get(id);
377
+ if (!node) {
378
+ const edge = dependencies.find((dependency) => dependency.component === id);
379
+ throw new DispatchError(
380
+ `invalid IR: component '${edge?.caller ?? root.id}' step '${edge?.step ?? "?"}' alias '${edge?.alias ?? "?"}' references missing mounted component '${id}'`
381
+ );
382
+ }
383
+ active.push(id);
384
+ visited.add(id);
385
+ union.add(id);
386
+ for (const edge of outgoing.get(id) ?? []) {
387
+ if (edge.component === id) {
388
+ throw new DispatchError(
389
+ `invalid IR: component '${id}' step '${edge.step}' alias '${edge.alias}' cannot call itself`
390
+ );
391
+ }
392
+ walk(edge.component);
393
+ }
394
+ active.pop();
395
+ };
396
+ walk(root.id);
397
+ entries.push(Object.freeze({ root: root.id, components: freezeArray([...visited]) }));
398
+ }
399
+ const components = ir.components.filter((node) => union.has(node.id));
400
+ const scopedDependencies = dependencies.filter(
401
+ (dependency) => union.has(dependency.caller) && union.has(dependency.component)
402
+ );
403
+ const calleeIds = new Set(scopedDependencies.map((dependency) => dependency.component));
404
+ const callees = components.filter((node) => calleeIds.has(node.id));
405
+ return Object.freeze({
406
+ roots: freezeArray([...rootNodes]),
407
+ callees: freezeArray(callees),
408
+ components: freezeArray(components),
409
+ entries: freezeArray(entries),
410
+ dependencies: freezeArray([...scopedDependencies])
411
+ });
412
+ }
413
+
297
414
  // src/ir.ts
298
415
  var REALIZATION_KINDS = ["skill", "tool", "gated-tool"];
299
416
  var COMPONENT_TYPES = [
@@ -305,7 +422,7 @@ var COMPONENT_TYPES = [
305
422
  ];
306
423
  var TRIGGER_KINDS = ["one_shot", "scheduled", "event"];
307
424
  var OUTCOME_KINDS = ["none", "assertion", "mutation", "dispatch"];
308
- var SUPPORTED_IR_VERSIONS = ["0.7"];
425
+ var SUPPORTED_IR_VERSIONS = ["0.8"];
309
426
  function assertSupportedIrVersion(version) {
310
427
  if (!SUPPORTED_IR_VERSIONS.includes(version)) {
311
428
  throw new DispatchError(
@@ -313,6 +430,53 @@ function assertSupportedIrVersion(version) {
313
430
  );
314
431
  }
315
432
  }
433
+ function parseIrInput(input) {
434
+ if (typeof input === "string") return parseIr(input);
435
+ let raw;
436
+ try {
437
+ raw = JSON.stringify(input);
438
+ } catch (error) {
439
+ throw new DispatchError(`invalid IR object: ${String(error)}`);
440
+ }
441
+ if (raw === void 0) throw new DispatchError("invalid IR object: value is not serializable");
442
+ return parseIr(raw);
443
+ }
444
+ function assertComponentCompositionNodeFields(node) {
445
+ if (typeof node.entrypoint !== "boolean") {
446
+ fail(`component '${node.id}'.entrypoint must be a boolean`);
447
+ }
448
+ for (const call of node.llm_calls) {
449
+ if (!Array.isArray(call.component_calls)) {
450
+ fail(`component '${node.id}' step '${call.name}'.component_calls must be an array`);
451
+ }
452
+ for (const [index, componentCall] of call.component_calls.entries()) {
453
+ if (!isObject(componentCall) || typeof componentCall["alias"] !== "string" || typeof componentCall["component"] !== "string") {
454
+ fail(
455
+ `component '${node.id}' step '${call.name}'.component_calls[${index}] must contain string alias/component`
456
+ );
457
+ }
458
+ }
459
+ }
460
+ }
461
+ function assertComponentCompositionFields(ir) {
462
+ for (const node of ir.components) assertComponentCompositionNodeFields(node);
463
+ }
464
+ function assertNoComponentCompositionNode(node) {
465
+ assertComponentCompositionNodeFields(node);
466
+ if (!node.entrypoint) {
467
+ throw new DispatchError(
468
+ `component '${node.id}' is entrypoint:false, but this target cannot prepare callee-only mounts yet (component composition wall-hit)`
469
+ );
470
+ }
471
+ for (const call of node.llm_calls) {
472
+ const componentCall = call.component_calls[0];
473
+ if (componentCall) {
474
+ throw new DispatchError(
475
+ `step '${call.name}' on component '${node.id}' authorizes component call alias '${componentCall.alias}' to '${componentCall.component}', but this target cannot realize component invocation yet (wall-hit)`
476
+ );
477
+ }
478
+ }
479
+ }
316
480
  function isObject(value) {
317
481
  return typeof value === "object" && value !== null && !Array.isArray(value);
318
482
  }
@@ -328,6 +492,11 @@ function requireString(obj, key, at) {
328
492
  if (typeof value !== "string") fail(`${at}.${key} must be a string`);
329
493
  return value;
330
494
  }
495
+ function requireNumber(obj, key, at) {
496
+ const value = obj[key];
497
+ if (typeof value !== "number") fail(`${at}.${key} must be a number`);
498
+ return value;
499
+ }
331
500
  function requireBool(obj, key, at) {
332
501
  const value = obj[key];
333
502
  if (typeof value !== "boolean") fail(`${at}.${key} must be a boolean`);
@@ -413,7 +582,17 @@ function parseLlmCall(value, at) {
413
582
  produces: optString(obj, "produces"),
414
583
  prompt: requireString(obj, "prompt", at),
415
584
  conditional: boolWithDefault(obj, "conditional", at),
416
- when: whenRaw === void 0 || whenRaw === null ? null : parseWhenGuard(whenRaw, `${at}.when`)
585
+ when: whenRaw === void 0 || whenRaw === null ? null : parseWhenGuard(whenRaw, `${at}.when`),
586
+ component_calls: obj["component_calls"] === void 0 ? [] : requireArray(obj, "component_calls", at).map(
587
+ (call, index) => parseComponentCall(call, `${at}.component_calls[${index}]`)
588
+ )
589
+ };
590
+ }
591
+ function parseComponentCall(value, at) {
592
+ const obj = requireObject(value, at);
593
+ return {
594
+ alias: requireString(obj, "alias", at),
595
+ component: requireString(obj, "component", at)
417
596
  };
418
597
  }
419
598
  function parseGuardrail(value, at) {
@@ -495,6 +674,7 @@ function parseComponent(value, at) {
495
674
  const trigger = requireObject(obj["trigger"], `${at}.trigger`);
496
675
  return {
497
676
  id: requireString(obj, "id", at),
677
+ entrypoint: requireBool(obj, "entrypoint", at),
498
678
  verb: requireString(obj, "verb", at),
499
679
  type: requireEnum(obj, "type", at, COMPONENT_TYPES),
500
680
  realization_kind: requireEnum(obj, "realization_kind", at, REALIZATION_KINDS),
@@ -520,9 +700,22 @@ function parseComponent(value, at) {
520
700
  params: paramArray(obj, "params", at),
521
701
  eval: obj["eval"] === void 0 || obj["eval"] === null ? null : parseEvalSpec(obj["eval"], `${at}.eval`),
522
702
  brief: optStringU(obj, "brief"),
523
- ...slotsField(obj, at)
703
+ ...slotsField(obj, at),
704
+ ...assetsField(obj, at)
524
705
  };
525
706
  }
707
+ function assetsField(obj, at) {
708
+ if (obj["assets"] === void 0) return {};
709
+ const assets = requireArray(obj, "assets", at).map((value, i) => {
710
+ const entry2 = requireObject(value, `${at}.assets[${i}]`);
711
+ return {
712
+ path: requireString(entry2, "path", `${at}.assets[${i}]`),
713
+ hash: requireString(entry2, "hash", `${at}.assets[${i}]`),
714
+ bytes: requireNumber(entry2, "bytes", `${at}.assets[${i}]`)
715
+ };
716
+ });
717
+ return { assets };
718
+ }
526
719
  function slotsField(obj, at) {
527
720
  if (obj["slots"] === void 0) return {};
528
721
  const slots = requireArray(obj, "slots", at).map((v, i) => parseSlot(v, `${at}.slots[${i}]`));
@@ -609,7 +802,7 @@ function resolveSlots(decls, supply, scope, unanswered = "fail") {
609
802
  const byName = new Map(decls.map((d) => [d.name, d]));
610
803
  const resolved = /* @__PURE__ */ new Map();
611
804
  const visiting = /* @__PURE__ */ new Set();
612
- const resolve3 = (name) => {
805
+ const resolve4 = (name) => {
613
806
  if (resolved.has(name)) return resolved.get(name);
614
807
  if (visiting.has(name)) {
615
808
  throw new DispatchError(
@@ -637,15 +830,15 @@ function resolveSlots(decls, supply, scope, unanswered = "fail") {
637
830
  );
638
831
  }
639
832
  visiting.add(name);
640
- const expanded = substitute(text, resolve3, byName, scope);
833
+ const expanded = substitute(text, resolve4, byName, scope);
641
834
  visiting.delete(name);
642
835
  resolved.set(name, expanded);
643
836
  return expanded;
644
837
  };
645
- for (const decl of decls) resolve3(decl.name);
838
+ for (const decl of decls) resolve4(decl.name);
646
839
  return resolved;
647
840
  }
648
- function substitute(raw, resolve3, byName, scope) {
841
+ function substitute(raw, resolve4, byName, scope) {
649
842
  const refs = slotReferences(raw);
650
843
  if (refs.length === 0) return raw;
651
844
  let out = "";
@@ -656,7 +849,7 @@ function substitute(raw, resolve3, byName, scope) {
656
849
  `prompt text in ${scope} references slot '${ref.name}', which is neither one of the component's slots nor one of the profile's.`
657
850
  );
658
851
  }
659
- out += raw.slice(cursor, ref.start) + (resolve3(ref.name) ?? "");
852
+ out += raw.slice(cursor, ref.start) + (resolve4(ref.name) ?? "");
660
853
  cursor = ref.end;
661
854
  }
662
855
  return out + raw.slice(cursor);
@@ -942,6 +1135,13 @@ function entry(outcome, via, provided_by, criticality, note) {
942
1135
  }
943
1136
  function localProfile() {
944
1137
  return {
1138
+ component_invocation: entry(
1139
+ "native",
1140
+ "isolated-child-query",
1141
+ "runtime",
1142
+ "required",
1143
+ null
1144
+ ),
945
1145
  "sql_execution:read_only": entry("native", "bash-wren", "runtime", "required", null),
946
1146
  genbi_build: entry("native", "bash-wren", "runtime", "required", null),
947
1147
  // Reading the semantic model's structure (models/metrics/lineage) is borrowed from the `wren`
@@ -1384,6 +1584,12 @@ function tierCollapseNote(node, model) {
1384
1584
  return `per-step tiers [${steps}] collapsed to single model '${model}'`;
1385
1585
  }
1386
1586
  function buildDispatchPlan(node, report, cfg) {
1587
+ assertNoComponentCompositionNode(node);
1588
+ const plan = buildDispatchPlanUnchecked(node, report, cfg);
1589
+ assertPlanHasNoSlotReferences(plan, node.id);
1590
+ return plan;
1591
+ }
1592
+ function buildPreparedNodePlan(node, report, cfg) {
1387
1593
  const plan = buildDispatchPlanUnchecked(node, report, cfg);
1388
1594
  assertPlanHasNoSlotReferences(plan, node.id);
1389
1595
  return plan;
@@ -1491,7 +1697,8 @@ function buildDispatchPlanUnchecked(node, report, cfg) {
1491
1697
  mode: "sdk-split",
1492
1698
  providers: ["anthropic"],
1493
1699
  stagedSteps: [],
1494
- setupScope
1700
+ setupScope,
1701
+ componentInvocation: node.llm_calls.some((step) => step.component_calls.length > 0)
1495
1702
  }
1496
1703
  };
1497
1704
  }
@@ -1531,7 +1738,8 @@ function buildDispatchPlanUnchecked(node, report, cfg) {
1531
1738
  mode: "single",
1532
1739
  providers: ["anthropic"],
1533
1740
  stagedSteps: [],
1534
- setupScope
1741
+ setupScope,
1742
+ componentInvocation: node.llm_calls.some((step) => step.component_calls.length > 0)
1535
1743
  }
1536
1744
  };
1537
1745
  }
@@ -1583,7 +1791,8 @@ function buildHybridStagedPlan(node, gate, cfg, base, readOnly, providers, steps
1583
1791
  // Hybrid+setup is out of scope (locked decision): a setup component's steps are not staged
1584
1792
  // across providers, so this path never sees setup_execution in practice; null is the safe,
1585
1793
  // explicit default rather than silently inheriting a scope this path doesn't enforce.
1586
- setupScope: null
1794
+ setupScope: null,
1795
+ componentInvocation: node.llm_calls.some((step) => step.component_calls.length > 0)
1587
1796
  }
1588
1797
  };
1589
1798
  }
@@ -1650,6 +1859,7 @@ function inspectCapabilities(node, targetId, profile) {
1650
1859
  provided_by: e.provided_by,
1651
1860
  criticality: e.criticality
1652
1861
  };
1862
+ if (e.via !== null) resolved.via = e.via;
1653
1863
  if (e.note !== null) resolved.note = e.note;
1654
1864
  return resolved;
1655
1865
  });
@@ -1686,11 +1896,99 @@ function inspectNodeCapabilities(node, targetId) {
1686
1896
  return inspectCapabilities(node, targetId, localProfile());
1687
1897
  }
1688
1898
 
1689
- // src/fingerprint.ts
1899
+ // src/assets.ts
1690
1900
  import { createHash } from "crypto";
1901
+ import {
1902
+ existsSync,
1903
+ lstatSync,
1904
+ mkdirSync,
1905
+ readFileSync,
1906
+ realpathSync,
1907
+ statSync,
1908
+ writeFileSync
1909
+ } from "fs";
1910
+ import { dirname, isAbsolute, join, parse, resolve, sep } from "path";
1911
+ function assetDirForIr(irPath) {
1912
+ const { dir, name } = parse(resolve(irPath));
1913
+ return join(dir, `${name}.assets`);
1914
+ }
1915
+ function prepareAssetLanding(ir, irPath, cwd) {
1916
+ const sourceRoot = assetDirForIr(irPath);
1917
+ const pending = [];
1918
+ for (const node of ir.components) {
1919
+ for (const asset of node.assets ?? []) {
1920
+ assertContainedRelativePath(asset.path, node.id);
1921
+ const source = join(sourceRoot, node.id, asset.path);
1922
+ try {
1923
+ lstatSync(source);
1924
+ } catch (e) {
1925
+ throw new DispatchError(
1926
+ `asset '${asset.path}' of component '${node.id}' is declared in the IR but missing from ${sourceRoot} (${e.message}). An IR's assets travel in that directory; copying the IR without it leaves a component without the files it declared.`
1927
+ );
1928
+ }
1929
+ assertResolvesInside(sourceRoot, source, "asset source");
1930
+ if (!statSync(source).isFile()) {
1931
+ throw new DispatchError(
1932
+ `asset '${asset.path}' of component '${node.id}' is not a regular file. Reading a pipe or device would block the dispatch indefinitely instead of failing, so anything that is not a plain file is refused.`
1933
+ );
1934
+ }
1935
+ const data = readFileSync(source);
1936
+ const actual = `sha256:${createHash("sha256").update(data).digest("hex")}`;
1937
+ if (actual !== asset.hash) {
1938
+ throw new DispatchError(
1939
+ `asset '${asset.path}' of component '${node.id}' does not match its manifest (expected ${asset.hash}, found ${actual}). The file changed after the IR was compiled; recompile rather than dispatching content the manifest does not name.`
1940
+ );
1941
+ }
1942
+ const target = join(cwd, asset.path);
1943
+ assertResolvesInside(cwd, target, "asset target");
1944
+ pending.push({ target, data });
1945
+ }
1946
+ }
1947
+ return pending;
1948
+ }
1949
+ function validateAssets(ir, irPath, cwd) {
1950
+ prepareAssetLanding(ir, irPath, cwd);
1951
+ }
1952
+ function landAssets(ir, irPath, cwd) {
1953
+ const pending = prepareAssetLanding(ir, irPath, cwd);
1954
+ for (const { target, data } of pending) {
1955
+ mkdirSync(dirname(target), { recursive: true });
1956
+ writeFileSync(target, data);
1957
+ }
1958
+ return pending.map(({ target }) => target);
1959
+ }
1960
+ function assertContainedRelativePath(relative, componentId) {
1961
+ const climbs = relative.split(/[\\/]/).includes("..");
1962
+ if (isAbsolute(relative) || climbs || relative.startsWith(sep)) {
1963
+ throw new DispatchError(
1964
+ `asset path '${relative}' of component '${componentId}' must be a relative path with no '..' segments. A manifest naming a path outside the directory it lands in would be an arbitrary file write, so it is refused rather than resolved.`
1965
+ );
1966
+ }
1967
+ }
1968
+ function assertResolvesInside(root, target, what) {
1969
+ const canonicalRoot = realpathSync(root);
1970
+ let existing = resolve(target);
1971
+ while (!existsSync(existing)) {
1972
+ const parent = dirname(existing);
1973
+ if (parent === existing) {
1974
+ throw new DispatchError(`${what} '${target}' has no resolvable ancestor`);
1975
+ }
1976
+ existing = parent;
1977
+ }
1978
+ const anchor = realpathSync(existing);
1979
+ const contained = anchor === canonicalRoot || anchor.startsWith(canonicalRoot + sep);
1980
+ if (!contained) {
1981
+ throw new DispatchError(
1982
+ `${what} '${target}' resolves outside ${canonicalRoot} once symlinks are followed. A path that looks contained but is not is refused rather than followed, because the manifest is data and the filesystem is what decides where a write actually lands.`
1983
+ );
1984
+ }
1985
+ }
1986
+
1987
+ // src/fingerprint.ts
1988
+ import { createHash as createHash2 } from "crypto";
1691
1989
  var ALGORITHM = "sha256";
1692
1990
  function sha256(text) {
1693
- return createHash(ALGORITHM).update(text, "utf8").digest("hex");
1991
+ return createHash2(ALGORITHM).update(text, "utf8").digest("hex");
1694
1992
  }
1695
1993
  function fingerprintSurfaces(surfaces) {
1696
1994
  const perSurface = {};
@@ -1903,8 +2201,8 @@ async function callOpenAiCompat(opts) {
1903
2201
  }
1904
2202
 
1905
2203
  // src/hybridTool.ts
1906
- import { existsSync, mkdirSync, writeFileSync } from "fs";
1907
- import { join } from "path";
2204
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
2205
+ import { join as join2 } from "path";
1908
2206
  import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
1909
2207
  import { z } from "zod";
1910
2208
  function isResult(msg) {
@@ -1985,7 +2283,7 @@ ${step.prompt}`,
1985
2283
  return requireFinalText(msgs.find(isResult));
1986
2284
  }
1987
2285
  async function runHybridTool(plan, cfg) {
1988
- mkdirSync(cfg.outDir, { recursive: true });
2286
+ mkdirSync2(cfg.outDir, { recursive: true });
1989
2287
  const cwd = plan.options.cwd ?? process.cwd();
1990
2288
  const { canUseTool, denials, hooks } = makeReadOnlyGuard({
1991
2289
  readOnly: plan.meta.readOnly,
@@ -1993,8 +2291,8 @@ async function runHybridTool(plan, cfg) {
1993
2291
  cwd,
1994
2292
  setupScope: plan.meta.setupScope
1995
2293
  });
1996
- const venvBin = join(cwd, ".venv", "bin");
1997
- const pathEnv = existsSync(venvBin) ? `${venvBin}:${process.env.PATH ?? ""}` : process.env.PATH ?? "";
2294
+ const venvBin = join2(cwd, ".venv", "bin");
2295
+ const pathEnv = existsSync2(venvBin) ? `${venvBin}:${process.env.PATH ?? ""}` : process.env.PATH ?? "";
1998
2296
  const env = { ...process.env, PATH: pathEnv };
1999
2297
  const stepCanUseTool = composeCanUseTool(plan.options.canUseTool, canUseTool);
2000
2298
  const stepHooks = composeHooks(plan.options.hooks, hooks);
@@ -2080,8 +2378,8 @@ async function runHybridTool(plan, cfg) {
2080
2378
  steps: traceSteps,
2081
2379
  denials
2082
2380
  };
2083
- writeFileSync(join(cfg.outDir, "result.txt"), finalText, "utf8");
2084
- writeFileSync(join(cfg.outDir, "trace.json"), JSON.stringify(trace, null, 2) + "\n", "utf8");
2381
+ writeFileSync2(join2(cfg.outDir, "result.txt"), finalText, "utf8");
2382
+ writeFileSync2(join2(cfg.outDir, "trace.json"), JSON.stringify(trace, null, 2) + "\n", "utf8");
2085
2383
  return {
2086
2384
  finalText,
2087
2385
  trace,
@@ -2094,13 +2392,13 @@ async function runHybridTool(plan, cfg) {
2094
2392
 
2095
2393
  // src/render.ts
2096
2394
  import { spawnSync } from "child_process";
2097
- import { mkdtempSync, writeFileSync as writeFileSync2 } from "fs";
2395
+ import { mkdtempSync, writeFileSync as writeFileSync3 } from "fs";
2098
2396
  import { tmpdir } from "os";
2099
- import { join as join2 } from "path";
2397
+ import { join as join3 } from "path";
2100
2398
  function renderEnvelope(finalText, outPath, opts) {
2101
- const dir = mkdtempSync(join2(tmpdir(), "warble-sdk-"));
2102
- const envelopePath = join2(dir, "envelope.txt");
2103
- writeFileSync2(envelopePath, finalText, "utf8");
2399
+ const dir = mkdtempSync(join3(tmpdir(), "warble-sdk-"));
2400
+ const envelopePath = join3(dir, "envelope.txt");
2401
+ writeFileSync3(envelopePath, finalText, "utf8");
2104
2402
  const args = ["render", envelopePath, "--out", outPath];
2105
2403
  if (opts.title) args.push("--title", opts.title);
2106
2404
  const proc = spawnSync(opts.warbleBin, args, { encoding: "utf8" });
@@ -2124,8 +2422,8 @@ The 'warble' binary was not found. Install it with 'cargo install warble-cli' (r
2124
2422
  }
2125
2423
 
2126
2424
  // src/run.ts
2127
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
2128
- import { join as join3 } from "path";
2425
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
2426
+ import { join as join4 } from "path";
2129
2427
  import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
2130
2428
 
2131
2429
  // src/conditional.ts
@@ -2338,10 +2636,15 @@ function requireFinalText2(result) {
2338
2636
  return result.result;
2339
2637
  }
2340
2638
  async function runDispatch(plan, cfg) {
2639
+ if (plan.meta.componentInvocation) {
2640
+ throw new DispatchError(
2641
+ "component_invocation requires the immutable prepared registry and trusted active-step runtime; use dispatch() or runComposedDispatch() instead of runDispatch(plan)"
2642
+ );
2643
+ }
2341
2644
  if (plan.meta.mode === "hybrid-staged") {
2342
2645
  return process.env["WARBLE_HYBRID_MODE"] === "tool" ? runHybridTool(plan, cfg) : runHybridStaged(plan, cfg);
2343
2646
  }
2344
- mkdirSync2(cfg.outDir, { recursive: true });
2647
+ mkdirSync3(cfg.outDir, { recursive: true });
2345
2648
  const cwd = plan.options.cwd ?? process.cwd();
2346
2649
  const gate = plan.meta.render;
2347
2650
  const writeScope = gate.kind === "realize" && gate.flavor === "prompt" ? gate.scope : null;
@@ -2351,8 +2654,8 @@ async function runDispatch(plan, cfg) {
2351
2654
  cwd,
2352
2655
  setupScope: plan.meta.setupScope
2353
2656
  });
2354
- const venvBin = join3(cwd, ".venv", "bin");
2355
- const pathEnv = existsSync2(venvBin) ? `${venvBin}:${process.env.PATH ?? ""}` : process.env.PATH ?? "";
2657
+ const venvBin = join4(cwd, ".venv", "bin");
2658
+ const pathEnv = existsSync3(venvBin) ? `${venvBin}:${process.env.PATH ?? ""}` : process.env.PATH ?? "";
2356
2659
  const env = { ...process.env, PATH: pathEnv };
2357
2660
  const options = {
2358
2661
  ...plan.options,
@@ -2363,6 +2666,7 @@ async function runDispatch(plan, cfg) {
2363
2666
  env,
2364
2667
  ...cfg.resume ? { resume: cfg.resume } : {}
2365
2668
  };
2669
+ if (cfg.assets) landAssets(cfg.assets.ir, cfg.assets.irPath, cwd);
2366
2670
  reportPromptFingerprint(cfg, options);
2367
2671
  const mapper = new ChatEventMapper(plan.meta.verb);
2368
2672
  const messages = [];
@@ -2375,12 +2679,12 @@ async function runDispatch(plan, cfg) {
2375
2679
  if (cfg.onEvent) for (const event of mapper.finish(true)) cfg.onEvent(event);
2376
2680
  const trace = aggregateTrace(messages, plan.meta, denials);
2377
2681
  const sessionId = result?.session_id ?? null;
2378
- writeFileSync3(join3(cfg.outDir, "result.txt"), finalText, "utf8");
2379
- writeFileSync3(join3(cfg.outDir, "trace.json"), JSON.stringify(trace, null, 2) + "\n", "utf8");
2682
+ writeFileSync4(join4(cfg.outDir, "result.txt"), finalText, "utf8");
2683
+ writeFileSync4(join4(cfg.outDir, "trace.json"), JSON.stringify(trace, null, 2) + "\n", "utf8");
2380
2684
  let htmlPath = null;
2381
2685
  let renderDegraded = null;
2382
2686
  if (gate.kind === "realize" && gate.flavor === "programmatic") {
2383
- const out = join3(cfg.outDir, "dashboard.html");
2687
+ const out = join4(cfg.outDir, "dashboard.html");
2384
2688
  const realized = realizeRender(gate, finalText, out, {
2385
2689
  warbleBin: cfg.warbleBin,
2386
2690
  ...cfg.title ? { title: cfg.title } : {}
@@ -2388,7 +2692,7 @@ async function runDispatch(plan, cfg) {
2388
2692
  htmlPath = realized.htmlPath;
2389
2693
  renderDegraded = realized.renderDegraded;
2390
2694
  } else if (plan.meta.assertion) {
2391
- const out = join3(cfg.outDir, "status.html");
2695
+ const out = join4(cfg.outDir, "status.html");
2392
2696
  renderEnvelope(finalText, out, {
2393
2697
  warbleBin: cfg.warbleBin,
2394
2698
  ...cfg.title ? { title: cfg.title } : {}
@@ -2464,7 +2768,7 @@ ${step.prompt}`,
2464
2768
  }
2465
2769
  }
2466
2770
  async function runHybridStaged(plan, cfg) {
2467
- mkdirSync2(cfg.outDir, { recursive: true });
2771
+ mkdirSync3(cfg.outDir, { recursive: true });
2468
2772
  const cwd = plan.options.cwd ?? process.cwd();
2469
2773
  const { canUseTool, denials, hooks } = makeReadOnlyGuard({
2470
2774
  readOnly: plan.meta.readOnly,
@@ -2472,8 +2776,8 @@ async function runHybridStaged(plan, cfg) {
2472
2776
  cwd,
2473
2777
  setupScope: plan.meta.setupScope
2474
2778
  });
2475
- const venvBin = join3(cwd, ".venv", "bin");
2476
- const pathEnv = existsSync2(venvBin) ? `${venvBin}:${process.env.PATH ?? ""}` : process.env.PATH ?? "";
2779
+ const venvBin = join4(cwd, ".venv", "bin");
2780
+ const pathEnv = existsSync3(venvBin) ? `${venvBin}:${process.env.PATH ?? ""}` : process.env.PATH ?? "";
2477
2781
  const env = { ...process.env, PATH: pathEnv };
2478
2782
  const artifacts = {};
2479
2783
  const outcomes = {};
@@ -2560,15 +2864,947 @@ async function runHybridStaged(plan, cfg) {
2560
2864
  steps,
2561
2865
  denials
2562
2866
  };
2563
- writeFileSync3(join3(cfg.outDir, "result.txt"), finalText, "utf8");
2564
- writeFileSync3(join3(cfg.outDir, "trace.json"), JSON.stringify(trace, null, 2) + "\n", "utf8");
2867
+ writeFileSync4(join4(cfg.outDir, "result.txt"), finalText, "utf8");
2868
+ writeFileSync4(join4(cfg.outDir, "trace.json"), JSON.stringify(trace, null, 2) + "\n", "utf8");
2565
2869
  return { finalText, trace, htmlPath: null, denials, sessionId: null, renderDegraded: null };
2566
2870
  }
2567
2871
 
2872
+ // src/componentInvocation.ts
2873
+ import { randomUUID } from "crypto";
2874
+ var DEFAULT_COMPONENT_INVOCATION_LIMITS = Object.freeze({
2875
+ maxDepth: 8,
2876
+ maxAttempts: 32,
2877
+ maxRequestBytes: 65536,
2878
+ maxResultBytes: 1048576,
2879
+ maxTurns: 40,
2880
+ maxTurnsPerChild: 12
2881
+ });
2882
+ var ComponentStepExecutionError = class extends DispatchError {
2883
+ constructor(message, telemetry, retryable) {
2884
+ super(message);
2885
+ this.telemetry = telemetry;
2886
+ this.retryable = retryable;
2887
+ this.name = "ComponentStepExecutionError";
2888
+ }
2889
+ telemetry;
2890
+ retryable;
2891
+ };
2892
+ var FORBIDDEN_CALLEE_CAPABILITIES = /* @__PURE__ */ new Set([
2893
+ "artifact_write",
2894
+ "data_write",
2895
+ "context_write",
2896
+ "setup_execution",
2897
+ "human_approval",
2898
+ "scheduler",
2899
+ "event_bus",
2900
+ "notify_channel",
2901
+ "write_authz",
2902
+ "version_control",
2903
+ "source_connect",
2904
+ "context_build",
2905
+ "blast_radius"
2906
+ ]);
2907
+ var FORBIDDEN_CALLEE_GUARDRAILS = /* @__PURE__ */ new Set([
2908
+ "artifact_write",
2909
+ "data_write",
2910
+ "context_write",
2911
+ "context_write_authz",
2912
+ "setup_execution",
2913
+ "human_approval"
2914
+ ]);
2915
+ function positiveInteger(value, fallback, name) {
2916
+ const resolved = value ?? fallback;
2917
+ if (!Number.isInteger(resolved) || resolved < 1 || resolved > fallback) {
2918
+ throw new DispatchError(`${name} must be an integer between 1 and ${fallback}`);
2919
+ }
2920
+ return resolved;
2921
+ }
2922
+ function resolveLimits(input) {
2923
+ assertSupportedComponentInvocationLimits(input);
2924
+ const limits = {
2925
+ maxDepth: positiveInteger(input?.maxDepth, DEFAULT_COMPONENT_INVOCATION_LIMITS.maxDepth, "maxDepth"),
2926
+ maxAttempts: positiveInteger(input?.maxAttempts, DEFAULT_COMPONENT_INVOCATION_LIMITS.maxAttempts, "maxAttempts"),
2927
+ maxRequestBytes: positiveInteger(input?.maxRequestBytes, DEFAULT_COMPONENT_INVOCATION_LIMITS.maxRequestBytes, "maxRequestBytes"),
2928
+ maxResultBytes: positiveInteger(input?.maxResultBytes, DEFAULT_COMPONENT_INVOCATION_LIMITS.maxResultBytes, "maxResultBytes"),
2929
+ maxTurns: positiveInteger(input?.maxTurns, DEFAULT_COMPONENT_INVOCATION_LIMITS.maxTurns, "maxTurns"),
2930
+ maxTurnsPerChild: positiveInteger(input?.maxTurnsPerChild, DEFAULT_COMPONENT_INVOCATION_LIMITS.maxTurnsPerChild, "maxTurnsPerChild")
2931
+ };
2932
+ if (input?.deadlineAt !== void 0) {
2933
+ if (!Number.isFinite(input.deadlineAt)) throw new DispatchError("deadlineAt must be a finite epoch timestamp");
2934
+ limits.deadlineAt = input.deadlineAt;
2935
+ }
2936
+ return limits;
2937
+ }
2938
+ function assertSupportedComponentInvocationLimits(input) {
2939
+ if (input?.maxCostUsd !== void 0) {
2940
+ throw new DispatchError(
2941
+ "unsupported_component_budget: maxCostUsd cannot be enforced before Agent SDK provider spend; component composition requires a reservable hard dollar cap"
2942
+ );
2943
+ }
2944
+ }
2945
+ function byteLength(value) {
2946
+ return Buffer.byteLength(JSON.stringify(value), "utf8");
2947
+ }
2948
+ function boundedMessage(message) {
2949
+ const collapsed = message.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
2950
+ const withoutPaths = collapsed.replace(/(?:^|\s)\/(?:[^\s/]+\/)*[^\s]*/g, " [redacted path]");
2951
+ const safe = /\b(select|insert|update|delete|merge|alter|drop|create)\b/i.test(withoutPaths) ? "The callee did not complete the request." : withoutPaths;
2952
+ return (safe || "The child run did not complete.").slice(0, 240);
2953
+ }
2954
+ function isJsonValue(value, seen = /* @__PURE__ */ new Set()) {
2955
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
2956
+ if (typeof value === "number") return Number.isFinite(value);
2957
+ if (typeof value !== "object") return false;
2958
+ if (seen.has(value)) return false;
2959
+ seen.add(value);
2960
+ let valid;
2961
+ if (Array.isArray(value)) {
2962
+ valid = value.every((entry2) => isJsonValue(entry2, seen));
2963
+ } else {
2964
+ const prototype = Object.getPrototypeOf(value);
2965
+ valid = (prototype === Object.prototype || prototype === null) && Object.values(value).every((entry2) => isJsonValue(entry2, seen));
2966
+ }
2967
+ seen.delete(value);
2968
+ return valid;
2969
+ }
2970
+ function errorResult(code, message, retryable = false) {
2971
+ return { status: "error", code, message: boundedMessage(message), retryable };
2972
+ }
2973
+ function normalizeComponentRequest(value, maxBytes = DEFAULT_COMPONENT_INVOCATION_LIMITS.maxRequestBytes) {
2974
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2975
+ return { error: errorResult("invalid_request", "The component request must be a JSON object.") };
2976
+ }
2977
+ const obj = value;
2978
+ const keys = Object.keys(obj);
2979
+ if (keys.some((key) => key !== "request" && key !== "input")) {
2980
+ return { error: errorResult("invalid_request", "The component request contains an unsupported field.") };
2981
+ }
2982
+ if (typeof obj["request"] !== "string" || obj["request"].trim() === "") {
2983
+ return { error: errorResult("invalid_request", "The component request must contain a non-empty request string.") };
2984
+ }
2985
+ if (obj["input"] !== void 0 && (typeof obj["input"] !== "object" || obj["input"] === null || Array.isArray(obj["input"]))) {
2986
+ return { error: errorResult("invalid_request", "The component request input must be a JSON object.") };
2987
+ }
2988
+ if (!isJsonValue(obj["input"] ?? {})) {
2989
+ return { error: errorResult("invalid_request", "The component request input must contain only JSON values.") };
2990
+ }
2991
+ const normalized = {
2992
+ request: obj["request"],
2993
+ input: JSON.parse(JSON.stringify(obj["input"] ?? {}))
2994
+ };
2995
+ let bytes;
2996
+ try {
2997
+ bytes = byteLength(normalized);
2998
+ } catch {
2999
+ return { error: errorResult("invalid_request", "The component request must be JSON serializable.") };
3000
+ }
3001
+ if (bytes > maxBytes) {
3002
+ return { error: errorResult("invalid_request", "The component request exceeds the configured byte limit.") };
3003
+ }
3004
+ return { value: normalized, bytes };
3005
+ }
3006
+ function parseJsonText(text) {
3007
+ const trimmed = text.trim();
3008
+ const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/i.exec(trimmed);
3009
+ return JSON.parse(fenced ? fenced[1] : trimmed);
3010
+ }
3011
+ function matchesFieldType(value, type) {
3012
+ const nullable = type.endsWith("?");
3013
+ if (value === null || value === void 0) return nullable;
3014
+ const normalized = nullable ? type.slice(0, -1) : type;
3015
+ if (normalized.includes("|")) {
3016
+ const variants = normalized.split("|");
3017
+ const primitives = /* @__PURE__ */ new Set(["string", "number", "boolean", "row"]);
3018
+ return variants.every((variant) => primitives.has(variant)) ? variants.some((variant) => matchesFieldType(value, variant)) : typeof value === "string" && variants.includes(value);
3019
+ }
3020
+ if (normalized.endsWith("[]")) {
3021
+ if (!Array.isArray(value)) return false;
3022
+ const element = normalized.slice(0, -2);
3023
+ return value.every((entry2) => matchesFieldType(entry2, element));
3024
+ }
3025
+ switch (normalized) {
3026
+ case "string":
3027
+ return typeof value === "string";
3028
+ case "number":
3029
+ return typeof value === "number" && Number.isFinite(value);
3030
+ case "boolean":
3031
+ return typeof value === "boolean";
3032
+ case "row":
3033
+ return Array.isArray(value) && value.every(
3034
+ (entry2) => entry2 === null || typeof entry2 === "string" || typeof entry2 === "boolean" || typeof entry2 === "number" && Number.isFinite(entry2)
3035
+ );
3036
+ default:
3037
+ return typeof value === "string" && value === normalized;
3038
+ }
3039
+ }
3040
+ function validateRenderBlocks(value, contract) {
3041
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
3042
+ const envelope = value;
3043
+ if (!Array.isArray(envelope["blocks"])) return null;
3044
+ const contracts = new Map(contract.map((block) => [block.type, block]));
3045
+ const blocks = [];
3046
+ for (const raw of envelope["blocks"]) {
3047
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
3048
+ const block = raw;
3049
+ if (typeof block["type"] !== "string") return null;
3050
+ const expected = contracts.get(block["type"]);
3051
+ if (!expected) return null;
3052
+ for (const [field, type] of Object.entries(expected.fields)) {
3053
+ if (!matchesFieldType(block[field], type)) return null;
3054
+ }
3055
+ blocks.push(block);
3056
+ }
3057
+ if (envelope["summary"] !== void 0 && typeof envelope["summary"] !== "string") return null;
3058
+ const provenance = {};
3059
+ if (typeof envelope["verified"] === "boolean") provenance.verified = envelope["verified"];
3060
+ if (envelope["definition"] !== void 0) provenance.definition = envelope["definition"];
3061
+ return {
3062
+ blocks,
3063
+ ...typeof envelope["summary"] === "string" ? { summary: envelope["summary"] } : {},
3064
+ ...Object.keys(provenance).length > 0 ? { provenance } : {}
3065
+ };
3066
+ }
3067
+ function normalizeComponentResult(finalText, renderContract, maxBytes = DEFAULT_COMPONENT_INVOCATION_LIMITS.maxResultBytes) {
3068
+ let parsed;
3069
+ try {
3070
+ parsed = parseJsonText(finalText);
3071
+ } catch {
3072
+ const value2 = errorResult("invalid_result", "The child returned a non-JSON result.");
3073
+ return { value: value2, bytes: byteLength(value2) };
3074
+ }
3075
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
3076
+ const obj = parsed;
3077
+ if (obj["status"] === "refused") {
3078
+ const value2 = {
3079
+ status: "refused",
3080
+ code: "callee_refused",
3081
+ // Child-authored prose may contain provider errors, paths, SQL, or secrets.
3082
+ message: "The callee refused the request."
3083
+ };
3084
+ return { value: value2, bytes: byteLength(value2) };
3085
+ }
3086
+ }
3087
+ let value;
3088
+ if (renderContract.length > 0) {
3089
+ const render = validateRenderBlocks(parsed, renderContract);
3090
+ value = render ? {
3091
+ status: "ok",
3092
+ output: { kind: "render", blocks: render.blocks, ...render.summary ? { summary: render.summary } : {} },
3093
+ ...render.provenance ? { provenance: render.provenance } : {}
3094
+ } : errorResult("invalid_result", "The child result does not satisfy its render contract.");
3095
+ } else {
3096
+ const provenance = {};
3097
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
3098
+ const obj = parsed;
3099
+ if (typeof obj["verified"] === "boolean") provenance.verified = obj["verified"];
3100
+ if (obj["definition"] !== void 0) provenance.definition = obj["definition"];
3101
+ }
3102
+ value = {
3103
+ status: "ok",
3104
+ output: { kind: "value", value: parsed },
3105
+ ...Object.keys(provenance).length > 0 ? { provenance } : {}
3106
+ };
3107
+ }
3108
+ const bytes = byteLength(value);
3109
+ if (bytes > maxBytes) {
3110
+ const oversized = errorResult("invalid_result", "The normalized child result exceeds the configured byte limit.");
3111
+ return { value: oversized, bytes };
3112
+ }
3113
+ return { value, bytes };
3114
+ }
3115
+ function assertEligibleComponentCallee(component) {
3116
+ const node = component.node;
3117
+ const reason = node.trigger.kind !== "one_shot" ? `trigger '${node.trigger.kind}'` : node.realization_kind !== "skill" ? `realization '${node.realization_kind}'` : node.effect.outcome.kind !== "none" ? `outcome '${node.effect.outcome.kind}'` : !node.guardrails.some((guardrail) => guardrail.name === "read_only_execution" && guardrail.locked) ? "missing locked read_only_execution guardrail" : node.required_capabilities.some((capability) => FORBIDDEN_CALLEE_CAPABILITIES.has(capability)) ? `capability '${node.required_capabilities.find((capability) => FORBIDDEN_CALLEE_CAPABILITIES.has(capability))}'` : node.guardrails.some((guardrail) => FORBIDDEN_CALLEE_GUARDRAILS.has(guardrail.name)) ? `guardrail '${node.guardrails.find((guardrail) => FORBIDDEN_CALLEE_GUARDRAILS.has(guardrail.name)).name}'` : node.borrowed_actions.length > 0 ? "borrowed action transport" : null;
3118
+ const providerReason = component.steps.find((step) => step.provider !== "anthropic");
3119
+ if (reason !== null || providerReason) {
3120
+ throw new DispatchError(
3121
+ `unsupported_callee: component '${node.id}' is not eligible for isolated invocation (${reason ?? `provider '${providerReason.provider}' cannot propagate the Agent SDK cancellation/tool boundary`})`
3122
+ );
3123
+ }
3124
+ }
3125
+ function assertEligibleComponentCaller(component) {
3126
+ const readOnly = component.node.guardrails.some(
3127
+ (guardrail) => guardrail.name === "read_only_execution" && guardrail.locked
3128
+ );
3129
+ const callingStepNames = new Set(
3130
+ component.node.llm_calls.filter((step) => step.component_calls.length > 0).map((step) => step.name)
3131
+ );
3132
+ const unsupportedProvider = component.steps.find((step) => step.provider !== "anthropic");
3133
+ const tools = Array.isArray(component.plan.options.tools) ? component.plan.options.tools : [];
3134
+ const unsafeCommandSurface = callingStepNames.size > 0 && tools.includes("Bash");
3135
+ const unsupportedPromptRender = component.plan.meta.render.kind === "realize" && component.plan.meta.render.flavor === "prompt";
3136
+ if (!readOnly || unsupportedProvider || unsafeCommandSurface || unsupportedPromptRender) {
3137
+ const reason = !readOnly ? "missing locked read_only_execution guardrail" : unsupportedProvider ? `provider '${unsupportedProvider.provider}' on executable step '${unsupportedProvider.name}'` : unsafeCommandSurface ? "a calling step has an effective Bash command surface that can execute SQL directly" : "prompt-render roots require model-owned artifact writes";
3138
+ throw new DispatchError(
3139
+ `unsupported_callee: component '${component.id}' cannot expose a trusted step-scoped invocation surface (${reason})`
3140
+ );
3141
+ }
3142
+ }
3143
+ function stepOf(component, call) {
3144
+ const staged = component.steps.find((candidate) => candidate.name === call.name);
3145
+ if (!staged) throw new DispatchError(`prepared component '${component.id}' has no resolved step '${call.name}'`);
3146
+ return staged;
3147
+ }
3148
+ function artifactKey2(step) {
3149
+ return step.produces ?? step.name;
3150
+ }
3151
+ function aggregateTokenUsage(entries) {
3152
+ const totals = {
3153
+ input_tokens: 0,
3154
+ output_tokens: 0,
3155
+ cache_creation_input_tokens: 0,
3156
+ cache_read_input_tokens: 0
3157
+ };
3158
+ let observed = false;
3159
+ for (const entry2 of entries) {
3160
+ if (typeof entry2.usage !== "object" || entry2.usage === null) continue;
3161
+ const usage = entry2.usage;
3162
+ for (const key of Object.keys(totals)) {
3163
+ const value = usage[key];
3164
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
3165
+ totals[key] += value;
3166
+ observed = true;
3167
+ }
3168
+ }
3169
+ }
3170
+ return observed ? totals : null;
3171
+ }
3172
+ var ComponentInvocationRuntime = class {
3173
+ constructor(options) {
3174
+ this.options = options;
3175
+ this.limits = resolveLimits(options.limits);
3176
+ this.availableTurns = this.limits.maxTurns;
3177
+ this.now = options.now ?? Date.now;
3178
+ this.newId = options.newId ?? randomUUID;
3179
+ this.rootRunId = this.newId();
3180
+ for (const component of [...options.prepared.components, ...options.prepared.preparedCallees]) {
3181
+ this.byId.set(component.id, component);
3182
+ }
3183
+ for (const dependency of options.prepared.dependencies) {
3184
+ const key = `${dependency.caller}\0${dependency.step}`;
3185
+ const aliases = new Map(this.edgeByStep.get(key) ?? []);
3186
+ const callee = this.byId.get(dependency.component);
3187
+ if (!callee) throw new DispatchError(`prepared callee '${dependency.component}' is absent from the immutable registry`);
3188
+ aliases.set(dependency.alias, callee);
3189
+ this.edgeByStep.set(key, aliases);
3190
+ }
3191
+ if (options.signal) {
3192
+ if (options.signal.aborted) this.cancel();
3193
+ else options.signal.addEventListener("abort", () => this.cancel(), { once: true });
3194
+ }
3195
+ if (this.limits.deadlineAt !== void 0) {
3196
+ const delay = Math.max(0, this.limits.deadlineAt - this.now());
3197
+ this.deadlineTimer = setTimeout(() => this.cancel(), delay);
3198
+ }
3199
+ }
3200
+ options;
3201
+ rootRunId;
3202
+ componentCalls = [];
3203
+ limits;
3204
+ abortController = new AbortController();
3205
+ now;
3206
+ newId;
3207
+ byId = /* @__PURE__ */ new Map();
3208
+ edgeByStep = /* @__PURE__ */ new Map();
3209
+ descendantDepth = /* @__PURE__ */ new Map();
3210
+ attempts = 0;
3211
+ availableTurns;
3212
+ totalTurnsObserved = 0;
3213
+ totalCostUsd = 0;
3214
+ allUsage = [];
3215
+ cancelledAt = null;
3216
+ deadlineTimer = null;
3217
+ get signal() {
3218
+ return this.abortController.signal;
3219
+ }
3220
+ cancel() {
3221
+ if (this.cancelledAt !== null) return;
3222
+ this.cancelledAt = this.now();
3223
+ this.abortController.abort();
3224
+ }
3225
+ async runRoot(componentId, request) {
3226
+ const component = this.options.prepared.components.find((candidate) => candidate.id === componentId);
3227
+ if (!component) throw new DispatchError(`component '${componentId}' is not a prepared root entry`);
3228
+ const normalized = normalizeComponentRequest(request, this.limits.maxRequestBytes);
3229
+ if ("error" in normalized) throw new DispatchError(normalized.error.status === "error" ? normalized.error.message : "invalid request");
3230
+ const frame = {
3231
+ component,
3232
+ callId: null,
3233
+ parentCallId: null,
3234
+ ancestry: [component.id],
3235
+ depth: 0,
3236
+ reservation: null,
3237
+ queue: Promise.resolve()
3238
+ };
3239
+ try {
3240
+ const result = await this.executeComponent(frame, normalized.value);
3241
+ this.throwIfCancelled();
3242
+ return {
3243
+ finalText: result.text,
3244
+ turns: this.totalTurnsObserved,
3245
+ totalCostUsd: this.totalCostUsd,
3246
+ usage: [...this.allUsage],
3247
+ componentCalls: [...this.componentCalls]
3248
+ };
3249
+ } finally {
3250
+ if (this.deadlineTimer) clearTimeout(this.deadlineTimer);
3251
+ }
3252
+ }
3253
+ throwIfCancelled() {
3254
+ if (this.cancelledAt !== null) throw new DispatchError("cancelled: the root component run was cancelled");
3255
+ if (this.limits.deadlineAt !== void 0 && this.now() >= this.limits.deadlineAt) {
3256
+ this.cancel();
3257
+ throw new DispatchError("cancelled: the root component run deadline elapsed");
3258
+ }
3259
+ }
3260
+ aliasesFor(frame, step) {
3261
+ if (!frame.component.node.llm_calls.some((candidate) => candidate === step)) {
3262
+ throw new DispatchError("unauthorized_call: forged active-step identity");
3263
+ }
3264
+ return this.edgeByStep.get(`${frame.component.id}\0${step.name}`) ?? /* @__PURE__ */ new Map();
3265
+ }
3266
+ availableFor(frame) {
3267
+ return frame.reservation?.remainingTurns ?? this.availableTurns;
3268
+ }
3269
+ charge(frame, result) {
3270
+ if (!Number.isInteger(result.turns) || result.turns < 0) throw new DispatchError("callee_failed: invalid child turn telemetry");
3271
+ if (!Number.isFinite(result.totalCostUsd) || result.totalCostUsd < 0) throw new DispatchError("callee_failed: invalid child cost telemetry");
3272
+ this.totalTurnsObserved += result.turns;
3273
+ this.totalCostUsd += result.totalCostUsd;
3274
+ this.allUsage.push(...result.usage ?? []);
3275
+ if (result.turns > this.availableFor(frame)) {
3276
+ if (frame.reservation) frame.reservation.remainingTurns = 0;
3277
+ else this.availableTurns = 0;
3278
+ throw new DispatchError("budget_exhausted: model turn budget was exceeded");
3279
+ }
3280
+ if (frame.reservation) frame.reservation.remainingTurns -= result.turns;
3281
+ else this.availableTurns -= result.turns;
3282
+ }
3283
+ maxDescendantDepth(componentId, visiting = /* @__PURE__ */ new Set()) {
3284
+ const cached = this.descendantDepth.get(componentId);
3285
+ if (cached !== void 0) return cached;
3286
+ if (visiting.has(componentId)) return this.limits.maxDepth;
3287
+ visiting.add(componentId);
3288
+ let depth = 0;
3289
+ for (const dependency of this.options.prepared.dependencies) {
3290
+ if (dependency.caller !== componentId) continue;
3291
+ depth = Math.max(depth, 1 + this.maxDescendantDepth(dependency.component, visiting));
3292
+ }
3293
+ visiting.delete(componentId);
3294
+ this.descendantDepth.set(componentId, depth);
3295
+ return depth;
3296
+ }
3297
+ descendantDepthForStep(frame, step) {
3298
+ let depth = 0;
3299
+ for (const callee of this.aliasesFor(frame, step).values()) {
3300
+ depth = Math.max(depth, 1 + this.maxDescendantDepth(callee.id));
3301
+ }
3302
+ return depth;
3303
+ }
3304
+ /** Reserve an active root SDK query before its synchronous child handlers may be admitted. */
3305
+ reserveRootStep(frame, step) {
3306
+ const available = this.availableTurns;
3307
+ if (available < 1) throw new DispatchError("budget_exhausted: no model turns remain");
3308
+ const desiredDescendantTurns = this.descendantDepthForStep(frame, step) * this.limits.maxTurnsPerChild;
3309
+ const heldForDescendants = Math.min(desiredDescendantTurns, Math.max(0, available - 1));
3310
+ const reservedTurns = available - heldForDescendants;
3311
+ this.availableTurns -= reservedTurns;
3312
+ return { reservedTurns, remainingTurns: reservedTurns };
3313
+ }
3314
+ async executeComponent(frame, request) {
3315
+ this.throwIfCancelled();
3316
+ const artifacts = {};
3317
+ const outcomes = {};
3318
+ const usage = [];
3319
+ let finalText = "";
3320
+ let turns = 0;
3321
+ let totalCostUsd = 0;
3322
+ let degradation = null;
3323
+ const failureTargets = new Set(
3324
+ frame.component.node.llm_calls.filter((step) => step.conditional && step.when?.guard === "on_failure").map((step) => step.when.target)
3325
+ );
3326
+ const execute = async (call, tolerant) => {
3327
+ const step = stepOf(frame.component, call);
3328
+ const aliases = this.aliasesFor(frame, call);
3329
+ const rootStepReservation = frame.reservation === null ? this.reserveRootStep(frame, call) : null;
3330
+ const accountingFrame = rootStepReservation === null ? frame : { ...frame, reservation: rootStepReservation };
3331
+ try {
3332
+ this.throwIfCancelled();
3333
+ const maxTurns = this.availableFor(accountingFrame);
3334
+ if (maxTurns < 1) throw new DispatchError("budget_exhausted: no model turns remain");
3335
+ const result = await this.options.runStep({
3336
+ rootRunId: this.rootRunId,
3337
+ callId: frame.callId,
3338
+ parentCallId: frame.parentCallId,
3339
+ component: frame.component,
3340
+ step,
3341
+ request,
3342
+ artifacts,
3343
+ aliases: [...aliases.keys()],
3344
+ signal: this.signal,
3345
+ maxTurns,
3346
+ invoke: (alias, childRequest) => this.enqueueInvocation(frame, call, alias, childRequest)
3347
+ });
3348
+ this.charge(accountingFrame, result);
3349
+ turns += result.turns;
3350
+ totalCostUsd += result.totalCostUsd;
3351
+ usage.push(...result.usage ?? []);
3352
+ degradation ??= result.degradation ?? null;
3353
+ this.throwIfCancelled();
3354
+ return { outcome: "success", text: result.text };
3355
+ } catch (caught) {
3356
+ let error = caught;
3357
+ if (caught instanceof ComponentStepExecutionError) {
3358
+ try {
3359
+ this.charge(accountingFrame, caught.telemetry);
3360
+ turns += caught.telemetry.turns;
3361
+ totalCostUsd += caught.telemetry.totalCostUsd;
3362
+ usage.push(...caught.telemetry.usage ?? []);
3363
+ degradation ??= caught.telemetry.degradation ?? null;
3364
+ } catch (chargeError) {
3365
+ error = chargeError;
3366
+ }
3367
+ }
3368
+ const terminal = error instanceof DispatchError && /^(?:budget_exhausted|cancelled|unauthorized_call):/.test(error.message);
3369
+ if (!tolerant || terminal) throw error;
3370
+ return { outcome: "failure", text: boundedMessage(error instanceof Error ? error.message : String(error)) };
3371
+ } finally {
3372
+ if (rootStepReservation !== null) {
3373
+ this.availableTurns += rootStepReservation.remainingTurns;
3374
+ }
3375
+ }
3376
+ };
3377
+ const calls = frame.component.node.llm_calls;
3378
+ for (let index = 0; index < calls.length; index++) {
3379
+ const call = calls[index];
3380
+ if (call.conditional) {
3381
+ if (!call.when) throw new DispatchError(`conditional step '${call.name}' has no 'when' guard`);
3382
+ const preceding = index > 0 ? calls[index - 1] : null;
3383
+ const precedingIdentity = preceding ? { name: preceding.name, produces: preceding.produces } : null;
3384
+ const decision = classifyConditionalStep(call.when, call.consumes, precedingIdentity, { artifacts, outcomes });
3385
+ if (decision.kind === "skip") continue;
3386
+ if (decision.kind === "repair") {
3387
+ const repair = await runRepairLoop(DEFAULT_MAX_REPAIR_ATTEMPTS, async () => {
3388
+ const result2 = await execute(call, true);
3389
+ outcomes[call.name] = result2.outcome;
3390
+ artifacts[artifactKey2(stepOf(frame.component, call))] = result2.text;
3391
+ if (result2.outcome === "success") finalText = result2.text;
3392
+ return { failed: result2.outcome === "failure" };
3393
+ });
3394
+ if (!repair.recovered) throw new DispatchError(`callee_failed: repair step '${call.name}' did not recover`);
3395
+ continue;
3396
+ }
3397
+ }
3398
+ const result = await execute(call, failureTargets.has(call.name));
3399
+ outcomes[call.name] = result.outcome;
3400
+ artifacts[artifactKey2(stepOf(frame.component, call))] = result.text;
3401
+ if (result.outcome === "success") finalText = result.text;
3402
+ }
3403
+ return { text: finalText, turns, totalCostUsd, usage, degradation };
3404
+ }
3405
+ enqueueInvocation(frame, step, alias, request) {
3406
+ let release;
3407
+ const predecessor = frame.queue;
3408
+ frame.queue = new Promise((resolve4) => {
3409
+ release = resolve4;
3410
+ });
3411
+ return predecessor.then(() => this.invoke(frame, step, alias, request)).finally(release);
3412
+ }
3413
+ async invoke(frame, step, alias, request) {
3414
+ this.throwIfCancelled();
3415
+ const aliases = this.aliasesFor(frame, step);
3416
+ const callee = aliases.get(alias);
3417
+ if (!callee) {
3418
+ this.cancel();
3419
+ throw new DispatchError(
3420
+ `unauthorized_call: the requested alias is not authorized for trusted step '${frame.component.id}.${step.name}'`
3421
+ );
3422
+ }
3423
+ const normalized = normalizeComponentRequest(request, this.limits.maxRequestBytes);
3424
+ if ("error" in normalized) return normalized.error;
3425
+ if (frame.depth + 1 > this.limits.maxDepth || this.attempts >= this.limits.maxAttempts) {
3426
+ return errorResult("budget_exhausted", "The root component-call budget is exhausted.");
3427
+ }
3428
+ if (frame.ancestry.includes(callee.id)) {
3429
+ this.cancel();
3430
+ throw new DispatchError(`unauthorized_call: recursive re-entry into component '${callee.id}' was rejected`);
3431
+ }
3432
+ const desiredDescendantTurns = this.maxDescendantDepth(callee.id) * this.limits.maxTurnsPerChild;
3433
+ const heldForDescendants = Math.min(
3434
+ desiredDescendantTurns,
3435
+ Math.max(0, this.availableTurns - 1)
3436
+ );
3437
+ const available = Math.min(
3438
+ this.availableTurns - heldForDescendants,
3439
+ this.limits.maxTurnsPerChild
3440
+ );
3441
+ if (available < 1) return errorResult("budget_exhausted", "The root model-turn budget is exhausted.");
3442
+ this.attempts += 1;
3443
+ this.availableTurns -= available;
3444
+ const reservation = { reservedTurns: available, remainingTurns: available };
3445
+ const callId = this.newId();
3446
+ const admittedAt = this.now();
3447
+ const trace = {
3448
+ call_id: callId,
3449
+ parent_call_id: frame.callId,
3450
+ caller_mount: frame.component.id,
3451
+ trusted_step_id: step.name,
3452
+ alias,
3453
+ callee_mount: callee.id,
3454
+ attempt: this.attempts,
3455
+ depth: frame.depth + 1,
3456
+ admitted_at: admittedAt,
3457
+ completed_at: admittedAt,
3458
+ status: "error",
3459
+ request_bytes: normalized.bytes,
3460
+ result_bytes: 0,
3461
+ model_turns: 0,
3462
+ total_cost_usd: 0,
3463
+ token_usage: null,
3464
+ degradation: null
3465
+ };
3466
+ this.componentCalls.push(trace);
3467
+ const turnsBefore = this.totalTurnsObserved;
3468
+ const costBefore = this.totalCostUsd;
3469
+ const usageBefore = this.allUsage.length;
3470
+ try {
3471
+ const child = await this.executeComponent({
3472
+ component: callee,
3473
+ callId,
3474
+ parentCallId: frame.callId,
3475
+ ancestry: [...frame.ancestry, callee.id],
3476
+ depth: frame.depth + 1,
3477
+ reservation,
3478
+ queue: Promise.resolve()
3479
+ }, normalized.value);
3480
+ trace.model_turns = this.totalTurnsObserved - turnsBefore;
3481
+ trace.total_cost_usd = this.totalCostUsd - costBefore;
3482
+ trace.token_usage = aggregateTokenUsage(this.allUsage.slice(usageBefore));
3483
+ trace.degradation = child.degradation ? boundedMessage(child.degradation) : null;
3484
+ const result = normalizeComponentResult(child.text, callee.node.effect.render_blocks, this.limits.maxResultBytes);
3485
+ trace.result_bytes = result.bytes;
3486
+ trace.completed_at = this.now();
3487
+ if (this.cancelledAt !== null && trace.completed_at >= this.cancelledAt) {
3488
+ trace.status = "late_discarded";
3489
+ return errorResult("cancelled", "The component call was cancelled.");
3490
+ }
3491
+ trace.status = result.value.status === "ok" ? "ok" : result.value.status;
3492
+ return result.value;
3493
+ } catch (error) {
3494
+ trace.completed_at = this.now();
3495
+ trace.model_turns = this.totalTurnsObserved - turnsBefore;
3496
+ trace.total_cost_usd = this.totalCostUsd - costBefore;
3497
+ trace.token_usage = aggregateTokenUsage(this.allUsage.slice(usageBefore));
3498
+ if (error instanceof ComponentStepExecutionError && error.telemetry.degradation) {
3499
+ trace.degradation = boundedMessage(error.telemetry.degradation);
3500
+ }
3501
+ if (this.cancelledAt !== null && trace.completed_at >= this.cancelledAt) {
3502
+ trace.status = "late_discarded";
3503
+ return errorResult("cancelled", "The component call was cancelled.");
3504
+ }
3505
+ trace.status = "error";
3506
+ if (error instanceof DispatchError && error.message.startsWith("budget_exhausted:")) {
3507
+ return errorResult("budget_exhausted", "The root model-turn budget is exhausted.");
3508
+ }
3509
+ if (error instanceof ComponentStepExecutionError && error.retryable) {
3510
+ return errorResult("transient_transport", "The child transport did not complete.", true);
3511
+ }
3512
+ if (error instanceof DispatchError) {
3513
+ return errorResult("callee_failed", "The child run did not complete.");
3514
+ }
3515
+ return errorResult("transient_transport", "The child transport did not complete.", true);
3516
+ } finally {
3517
+ this.availableTurns += reservation.remainingTurns;
3518
+ }
3519
+ }
3520
+ };
3521
+
3522
+ // src/componentSdk.ts
3523
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
3524
+ import { join as join5 } from "path";
3525
+ import { createSdkMcpServer as createSdkMcpServer2, query as query3, tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
3526
+ import { z as z2 } from "zod";
3527
+ var MCP_SERVER_NAME2 = "warble_components";
3528
+ function isResult3(message) {
3529
+ return message.type === "result";
3530
+ }
3531
+ function isAssistant3(message) {
3532
+ return message.type === "assistant";
3533
+ }
3534
+ function requireFinalText3(result) {
3535
+ if (!result) throw new DispatchError("component step ended without a result message");
3536
+ if (result.subtype !== "success") {
3537
+ if (result.subtype === "error_max_turns" || result.subtype === "error_max_budget_usd") {
3538
+ throw new DispatchError("budget_exhausted: child model limit reached");
3539
+ }
3540
+ throw new DispatchError(`component step failed (${result.subtype})`);
3541
+ }
3542
+ return result.result;
3543
+ }
3544
+ function stepTelemetry(messages, result) {
3545
+ const assistant = messages.filter(isAssistant3);
3546
+ return {
3547
+ text: "",
3548
+ turns: result?.num_turns ?? assistant.length,
3549
+ totalCostUsd: result?.total_cost_usd ?? 0,
3550
+ usage: assistant.map((message) => ({
3551
+ model: message.message.model,
3552
+ usage: message.message.usage
3553
+ })),
3554
+ degradation: null
3555
+ };
3556
+ }
3557
+ function componentStepPrompt(run) {
3558
+ const consumed = run.step.consumes.map((name) => {
3559
+ const value = run.artifacts[name];
3560
+ return value === void 0 ? `Input '${name}' was not produced by an earlier step.` : `Input '${name}':
3561
+ ${value}`;
3562
+ });
3563
+ return [
3564
+ `Request: ${run.request.request}`,
3565
+ `Structured input: ${JSON.stringify(run.request.input)}`,
3566
+ ...consumed
3567
+ ].join("\n\n");
3568
+ }
3569
+ function componentStepSystemPrompt(run) {
3570
+ const isLast = run.component.steps.at(-1)?.name === run.step.name;
3571
+ const behavior = run.component.steps.length === 1 ? run.component.node.prompt_fragment || run.step.prompt : run.step.prompt;
3572
+ const render = run.component.node.effect.render_blocks;
3573
+ const output = !isLast ? "Return only this step's output for the next declared step." : render.length > 0 ? [
3574
+ "Your final message must be one JSON render envelope with a blocks array and optional summary.",
3575
+ "Every block type and field must satisfy this callee-owned contract:",
3576
+ JSON.stringify(render),
3577
+ "Do not render or write an artifact; return the envelope as data only."
3578
+ ].join("\n") : [
3579
+ "Your final message must be one JSON value and nothing else.",
3580
+ 'If policy requires refusal, return {"status":"refused","message":"a bounded explanation"}.'
3581
+ ].join("\n");
3582
+ return [
3583
+ buildPreamble(run.component.plan.options.cwd ?? process.cwd()),
3584
+ ...run.component.node.brief ? [run.component.node.brief] : [],
3585
+ behavior,
3586
+ run.aliases.length > 0 ? `This step may call only these logical component aliases: ${run.aliases.map((alias) => `\`${alias}\``).join(", ")}.` : "This step has no callable component aliases.",
3587
+ output
3588
+ ].join("\n\n");
3589
+ }
3590
+ function componentTools(component) {
3591
+ const configured = component.plan.options.tools;
3592
+ if (!Array.isArray(configured)) return ["Read"];
3593
+ return configured.filter((name) => name !== "Task" && name !== "Write" && name !== "Edit");
3594
+ }
3595
+ function createSdkComponentStepRunner(cfg = {}) {
3596
+ const denials = [];
3597
+ const runStep = async (run) => {
3598
+ if (run.step.provider !== "anthropic") {
3599
+ throw new DispatchError(`unsupported_callee: provider '${run.step.provider}' has no step-scoped invocation transport`);
3600
+ }
3601
+ const cwd = run.component.plan.options.cwd ?? process.cwd();
3602
+ const guard = makeReadOnlyGuard({
3603
+ readOnly: true,
3604
+ writeScope: null,
3605
+ cwd,
3606
+ setupScope: null
3607
+ });
3608
+ const planAndFloor = composeCanUseTool(run.component.plan.options.canUseTool, guard.canUseTool);
3609
+ const originalCanUseTool = composeCanUseTool(cfg.hostCanUseTool, planAndFloor);
3610
+ const planHooks = {
3611
+ ...cfg.hostHooks,
3612
+ ...run.component.plan.options.hooks,
3613
+ PreToolUse: [
3614
+ ...cfg.hostHooks?.PreToolUse ?? [],
3615
+ ...run.component.plan.options.hooks?.PreToolUse ?? []
3616
+ ]
3617
+ };
3618
+ const hooks = composeHooks(planHooks, guard.hooks);
3619
+ const toolNames = new Set(run.aliases.map((alias) => `mcp__${MCP_SERVER_NAME2}__${alias}`));
3620
+ const approvedAliasInputs = /* @__PURE__ */ new Map();
3621
+ const approvalKey = (name, input) => JSON.stringify([
3622
+ name,
3623
+ toolNames.has(name) && input["input"] === void 0 ? { ...input, input: {} } : input
3624
+ ]);
3625
+ const rememberApproval = (name, input) => {
3626
+ const key = approvalKey(name, input);
3627
+ approvedAliasInputs.set(key, (approvedAliasInputs.get(key) ?? 0) + 1);
3628
+ };
3629
+ const consumeApproval = (name, input) => {
3630
+ const key = approvalKey(name, input);
3631
+ const count = approvedAliasInputs.get(key) ?? 0;
3632
+ if (count < 1) return false;
3633
+ if (count === 1) approvedAliasInputs.delete(key);
3634
+ else approvedAliasInputs.set(key, count - 1);
3635
+ return true;
3636
+ };
3637
+ const canUseTool = async (name, input, options2) => {
3638
+ if (!toolNames.has(name)) return originalCanUseTool(name, input, options2);
3639
+ if (!cfg.hostCanUseTool) return { behavior: "allow", updatedInput: input };
3640
+ const permission = await cfg.hostCanUseTool(name, input, options2);
3641
+ if (permission.behavior === "allow") rememberApproval(name, permission.updatedInput);
3642
+ return permission;
3643
+ };
3644
+ const componentCalls = run.aliases.map((alias) => tool2(
3645
+ alias,
3646
+ `Invoke the statically authorized '${alias}' component alias with an isolated JSON request.`,
3647
+ { request: z2.string(), input: z2.record(z2.string(), z2.unknown()).optional() },
3648
+ async (args) => {
3649
+ const name = `mcp__${MCP_SERVER_NAME2}__${alias}`;
3650
+ let input = { request: args.request, input: args.input ?? {} };
3651
+ if (cfg.hostCanUseTool && !consumeApproval(name, input)) {
3652
+ const permission = await cfg.hostCanUseTool(name, input, {
3653
+ signal: run.signal,
3654
+ toolUseID: `${run.rootRunId}:${run.callId ?? "root"}:${run.step.name}:${alias}`
3655
+ });
3656
+ if (permission.behavior !== "allow") {
3657
+ throw new DispatchError("component invocation was denied by the embedding host");
3658
+ }
3659
+ input = permission.updatedInput;
3660
+ }
3661
+ const result2 = await run.invoke(alias, input);
3662
+ return {
3663
+ content: [{ type: "text", text: JSON.stringify(result2) }]
3664
+ };
3665
+ }
3666
+ ));
3667
+ const server = componentCalls.length > 0 ? createSdkMcpServer2({ name: MCP_SERVER_NAME2, version: "0.0.0", tools: componentCalls }) : null;
3668
+ const venvBin = join5(cwd, ".venv", "bin");
3669
+ const path = existsSync4(venvBin) ? `${venvBin}:${process.env.PATH ?? ""}` : process.env.PATH ?? "";
3670
+ const abortController = new AbortController();
3671
+ if (run.signal.aborted) abortController.abort();
3672
+ else run.signal.addEventListener("abort", () => abortController.abort(), { once: true });
3673
+ const options = {
3674
+ cwd,
3675
+ permissionMode: "default",
3676
+ persistSession: false,
3677
+ abortController,
3678
+ maxTurns: run.maxTurns,
3679
+ model: run.step.model,
3680
+ systemPrompt: componentStepSystemPrompt(run),
3681
+ tools: componentTools(run.component),
3682
+ // Aliases must reach canUseTool; allowedTools would auto-authorize them ahead of host policy.
3683
+ allowedTools: ["Read"],
3684
+ disallowedTools: run.component.plan.options.disallowedTools,
3685
+ canUseTool,
3686
+ hooks,
3687
+ env: { ...process.env, PATH: path },
3688
+ ...server ? { mcpServers: { [MCP_SERVER_NAME2]: server } } : {}
3689
+ };
3690
+ cfg.onPromptFingerprint?.(fingerprintSurfaces(promptSurfacesOf(options)));
3691
+ const messages = [];
3692
+ try {
3693
+ for await (const message of query3({ prompt: componentStepPrompt(run), options })) messages.push(message);
3694
+ } catch {
3695
+ denials.push(...guard.denials);
3696
+ throw new ComponentStepExecutionError(
3697
+ "component step transport failed",
3698
+ stepTelemetry(messages, void 0),
3699
+ true
3700
+ );
3701
+ }
3702
+ const result = messages.find(isResult3);
3703
+ denials.push(...guard.denials);
3704
+ let text;
3705
+ try {
3706
+ text = requireFinalText3(result);
3707
+ } catch (error) {
3708
+ throw new ComponentStepExecutionError(
3709
+ error instanceof Error ? error.message : "component step failed",
3710
+ stepTelemetry(messages, result),
3711
+ false
3712
+ );
3713
+ }
3714
+ const telemetry = stepTelemetry(messages, result);
3715
+ return {
3716
+ text,
3717
+ turns: telemetry.turns,
3718
+ totalCostUsd: telemetry.totalCostUsd,
3719
+ usage: telemetry.usage,
3720
+ degradation: null
3721
+ };
3722
+ };
3723
+ return { runStep, denials };
3724
+ }
3725
+ async function runComposedDispatch(plan, cfg) {
3726
+ const sdk = cfg.runStep ? { runStep: cfg.runStep, denials: [] } : createSdkComponentStepRunner(cfg);
3727
+ const runtime = new ComponentInvocationRuntime({
3728
+ prepared: cfg.prepared,
3729
+ runStep: sdk.runStep,
3730
+ limits: {
3731
+ ...cfg.limits,
3732
+ // Composition may only preserve or lower the established DispatchInput/CLI turn limit.
3733
+ maxTurns: Math.min(
3734
+ typeof plan.options.maxTurns === "number" ? plan.options.maxTurns : DEFAULT_COMPONENT_INVOCATION_LIMITS.maxTurns,
3735
+ cfg.limits?.maxTurns ?? DEFAULT_COMPONENT_INVOCATION_LIMITS.maxTurns
3736
+ )
3737
+ },
3738
+ ...cfg.signal ? { signal: cfg.signal } : {}
3739
+ });
3740
+ const started = Date.now();
3741
+ const result = await runtime.runRoot(cfg.root.id, { request: plan.prompt, input: {} });
3742
+ if (runtime.signal.aborted) throw new DispatchError("cancelled: the root component run was cancelled");
3743
+ const traceSteps = result.usage.map((entry2) => ({
3744
+ model: entry2.model,
3745
+ parent_tool_use_id: null,
3746
+ usage: entry2.usage
3747
+ }));
3748
+ const trace = {
3749
+ target: plan.meta.target,
3750
+ verb: plan.meta.verb,
3751
+ model: plan.meta.model,
3752
+ split: true,
3753
+ run: {
3754
+ total_cost_usd: result.totalCostUsd,
3755
+ duration_ms: Date.now() - started,
3756
+ duration_api_ms: 0,
3757
+ num_turns: result.turns
3758
+ },
3759
+ usage: null,
3760
+ modelUsage: {},
3761
+ steps: traceSteps,
3762
+ denials: sdk.denials.map(() => ({
3763
+ tool: "[redacted]",
3764
+ reason: "component guardrail denied a tool call"
3765
+ })),
3766
+ componentCalls: result.componentCalls
3767
+ };
3768
+ mkdirSync4(cfg.outDir, { recursive: true });
3769
+ writeFileSync5(join5(cfg.outDir, "result.txt"), result.finalText, "utf8");
3770
+ writeFileSync5(join5(cfg.outDir, "trace.json"), JSON.stringify(trace, null, 2) + "\n", "utf8");
3771
+ let htmlPath = null;
3772
+ let renderDegraded = null;
3773
+ const gate = plan.meta.render;
3774
+ if (gate.kind === "realize" && gate.flavor === "programmatic") {
3775
+ const rendered = realizeRender(gate, result.finalText, join5(cfg.outDir, "dashboard.html"), {
3776
+ warbleBin: cfg.warbleBin,
3777
+ ...cfg.title ? { title: cfg.title } : {}
3778
+ });
3779
+ htmlPath = rendered.htmlPath;
3780
+ renderDegraded = rendered.renderDegraded;
3781
+ } else if (plan.meta.assertion) {
3782
+ const out = join5(cfg.outDir, "status.html");
3783
+ renderEnvelope(result.finalText, out, {
3784
+ warbleBin: cfg.warbleBin,
3785
+ ...cfg.title ? { title: cfg.title } : {}
3786
+ });
3787
+ htmlPath = out;
3788
+ }
3789
+ return {
3790
+ finalText: result.finalText,
3791
+ trace,
3792
+ htmlPath,
3793
+ denials: trace.denials,
3794
+ sessionId: null,
3795
+ renderDegraded
3796
+ };
3797
+ }
3798
+
2568
3799
  // src/dispatch.ts
2569
- import { dirname, isAbsolute, resolve } from "path";
3800
+ import { dirname as dirname2, isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
2570
3801
  var UNAVAILABLE_COMPONENT_REASON = "component is unavailable on the configured runtime";
2571
- function buildPreparedComponent(node, report, input, target, models) {
3802
+ function deepFreeze(value) {
3803
+ if (typeof value !== "object" || value === null || Object.isFrozen(value)) return value;
3804
+ for (const nested of Object.values(value)) deepFreeze(nested);
3805
+ return Object.freeze(value);
3806
+ }
3807
+ function buildPreparedComponent(node, report, input, target, models, role = "entry") {
2572
3808
  const cfg = {
2573
3809
  target,
2574
3810
  flavor: input.flavor ?? DEFAULT_RENDER_FLAVOR,
@@ -2577,39 +3813,96 @@ function buildPreparedComponent(node, report, input, target, models) {
2577
3813
  cwd: resolveProjectCwd(node, { ...input.project !== void 0 ? { project: input.project } : {}, ...input.irPath !== void 0 ? { irPath: input.irPath } : {} }),
2578
3814
  ...input.maxTurns !== void 0 ? { maxTurns: input.maxTurns } : {}
2579
3815
  };
2580
- return { id: node.id, node, report, plan: buildDispatchPlan(node, report, cfg) };
3816
+ const plan = buildPreparedNodePlan(node, report, cfg);
3817
+ return deepFreeze({ id: node.id, node, report, plan, steps: resolveStagedSteps(node, models), role });
3818
+ }
3819
+ function assertExecutableReport(node, report, target, deferInvocationWall) {
3820
+ const failed = report.find(
3821
+ (entry2) => entry2.outcome === "fail" && !(deferInvocationWall && entry2.capability === "component_invocation")
3822
+ );
3823
+ if (!failed) return;
3824
+ const reason = failed.note ?? "unsupported on this target";
3825
+ throw new DispatchError(
3826
+ `${failed.capability}: fail on ${target} (${reason}) \u2014 component '${node.verb}' cannot be dispatched`
3827
+ );
3828
+ }
3829
+ function assertInvocationRealization(closure, reports, target) {
3830
+ if (closure.dependencies.length === 0) return;
3831
+ const edge = closure.dependencies.find(
3832
+ (dependency) => reports.get(dependency.caller)?.some(
3833
+ (entry2) => entry2.capability === "component_invocation" && entry2.outcome === "fail"
3834
+ )
3835
+ );
3836
+ if (!edge) return;
3837
+ const resolution = reports.get(edge.caller).find((entry2) => entry2.capability === "component_invocation");
3838
+ throw new DispatchError(
3839
+ `step '${edge.step}' on component '${edge.caller}' authorizes component call alias '${edge.alias}' to '${edge.component}', but component_invocation resolves ${resolution.outcome} on ${target} (${resolution.note ?? "no trusted invocation handler"}) (wall-hit)`
3840
+ );
2581
3841
  }
2582
3842
  function resolveProjectCwd(node, opts) {
2583
- if (opts.project) return resolve(opts.project);
3843
+ if (opts.project) return resolve2(opts.project);
2584
3844
  const p = node.context_binding.project;
2585
- if (isAbsolute(p)) return p;
2586
- const baseDir = opts.irPath ? dirname(resolve(opts.irPath)) : process.cwd();
2587
- return resolve(baseDir, p);
3845
+ if (isAbsolute2(p)) return p;
3846
+ const baseDir = opts.irPath ? dirname2(resolve2(opts.irPath)) : process.cwd();
3847
+ return resolve2(baseDir, p);
2588
3848
  }
2589
3849
  function prepareDispatch(input) {
2590
- const ir = typeof input.ir === "string" ? parseIr(input.ir) : input.ir;
3850
+ const ir = parseIrInput(input.ir);
2591
3851
  assertSupportedIrVersion(ir.warble_ir_version);
2592
3852
  const target = input.target ?? DEFAULT_TARGET;
2593
3853
  const models = input.models ?? ModelConfig.default();
2594
- models.validate(ir);
2595
- let scoped = ir.components;
2596
- if (input.componentId !== void 0) {
2597
- const node = ir.components.find((candidate) => candidate.id === input.componentId);
2598
- if (!node) {
2599
- throw new DispatchError(
2600
- `component '${input.componentId}' not found in IR (available: ${ir.components.map((c) => c.id).join(", ")})`
2601
- );
2602
- }
2603
- scoped = [node];
2604
- }
3854
+ const closure = resolveComponentClosure(
3855
+ ir,
3856
+ input.componentId === void 0 ? void 0 : [input.componentId]
3857
+ );
3858
+ models.validate({ ...ir, components: [...closure.components] });
2605
3859
  const supply = input.slots ?? {};
2606
- const components = scoped.map((node) => {
3860
+ const resolvedNodes = /* @__PURE__ */ new Map();
3861
+ const reports = /* @__PURE__ */ new Map();
3862
+ for (const node of closure.components) {
2607
3863
  const resolved = resolveSlotsForNode(node, ir, supply);
2608
3864
  const withSlots = resolved === null ? node : applyNodeSlots(node, resolved);
2609
- const report = resolveNodeCapabilities(withSlots, target);
2610
- return buildPreparedComponent(withSlots, report, input, target, models);
3865
+ const report = inspectNodeCapabilities(withSlots, target);
3866
+ resolvedNodes.set(node.id, withSlots);
3867
+ reports.set(node.id, report);
3868
+ }
3869
+ for (const node of closure.components) {
3870
+ assertExecutableReport(
3871
+ resolvedNodes.get(node.id),
3872
+ reports.get(node.id),
3873
+ target,
3874
+ closure.dependencies.some((dependency) => dependency.caller === node.id)
3875
+ );
3876
+ }
3877
+ const components = closure.roots.map(
3878
+ (node) => buildPreparedComponent(resolvedNodes.get(node.id), reports.get(node.id), input, target, models, "entry")
3879
+ );
3880
+ const preparedCallees = closure.callees.map(
3881
+ (node) => buildPreparedComponent(
3882
+ resolvedNodes.get(node.id),
3883
+ reports.get(node.id),
3884
+ input,
3885
+ target,
3886
+ models,
3887
+ "callee"
3888
+ )
3889
+ );
3890
+ for (const callee of preparedCallees) assertEligibleComponentCallee(callee);
3891
+ const preparedById = new Map([...components, ...preparedCallees].map((component) => [component.id, component]));
3892
+ for (const caller of new Set(closure.dependencies.map((dependency) => dependency.caller))) {
3893
+ assertEligibleComponentCaller(preparedById.get(caller));
3894
+ }
3895
+ assertInvocationRealization(closure, reports, target);
3896
+ return Object.freeze({
3897
+ target,
3898
+ profile: ir.profile,
3899
+ // Keep the established mutable TypeScript surface for compatibility while freezing the actual
3900
+ // registry returned for a run. Callers cannot widen it after preflight.
3901
+ components: Object.freeze(components),
3902
+ preparedCallees: Object.freeze(preparedCallees),
3903
+ entries: closure.entries,
3904
+ dependencies: closure.dependencies
2611
3905
  });
2612
- return { target, components };
2613
3906
  }
2614
3907
  function resolveSlotsForNode(node, ir, supply, unanswered = "fail") {
2615
3908
  const decls = [...ir.slots ?? [], ...node.slots ?? []];
@@ -2629,36 +3922,115 @@ function applyNodeSlots(node, resolved) {
2629
3922
  };
2630
3923
  }
2631
3924
  function prepareDisplayManifest(input) {
2632
- const ir = typeof input.ir === "string" ? parseIr(input.ir) : input.ir;
3925
+ const ir = parseIrInput(input.ir);
2633
3926
  assertSupportedIrVersion(ir.warble_ir_version);
3927
+ assertComponentCompositionFields(ir);
2634
3928
  const target = input.target ?? DEFAULT_TARGET;
2635
3929
  const models = input.models ?? ModelConfig.default();
2636
- models.validate(ir);
3930
+ const closure = resolveComponentClosure(ir);
3931
+ const calleeIds = new Set(closure.dependencies.map((dependency) => dependency.component));
3932
+ const callerIds = new Set(closure.dependencies.map((dependency) => dependency.caller));
2637
3933
  const supply = input.slots ?? {};
2638
3934
  const components = ir.components.map((node) => {
2639
3935
  const resolved = resolveSlotsForNode(node, ir, supply, "default");
2640
3936
  const withSlots = resolved === null ? node : applyNodeSlots(node, resolved);
2641
3937
  const report = inspectNodeCapabilities(withSlots, target);
2642
- if (report.some((entry2) => entry2.outcome === "fail")) {
3938
+ let modelReady = true;
3939
+ try {
3940
+ models.validate({ ...ir, components: [withSlots] });
3941
+ } catch {
3942
+ modelReady = false;
3943
+ }
3944
+ if (!modelReady || report.some((entry2) => entry2.outcome === "fail")) {
2643
3945
  return {
2644
3946
  id: withSlots.id,
2645
3947
  node: withSlots,
3948
+ inspection: report,
3949
+ availability: { status: "unavailable", reason: UNAVAILABLE_COMPONENT_REASON }
3950
+ };
3951
+ }
3952
+ const prepared = buildPreparedComponent(withSlots, report, input, target, models, withSlots.entrypoint ? "entry" : "callee");
3953
+ try {
3954
+ if (calleeIds.has(prepared.id)) assertEligibleComponentCallee(prepared);
3955
+ if (callerIds.has(prepared.id)) assertEligibleComponentCaller(prepared);
3956
+ } catch {
3957
+ return {
3958
+ id: withSlots.id,
3959
+ node: withSlots,
3960
+ inspection: report,
2646
3961
  availability: { status: "unavailable", reason: UNAVAILABLE_COMPONENT_REASON }
2647
3962
  };
2648
3963
  }
2649
- const prepared = buildPreparedComponent(withSlots, report, input, target, models);
2650
3964
  return { id: prepared.id, node: prepared.node, report: prepared.report };
2651
3965
  });
2652
- return { target, components };
3966
+ return {
3967
+ target,
3968
+ profile: ir.profile,
3969
+ components,
3970
+ entries: closure.entries,
3971
+ dependencies: closure.dependencies
3972
+ };
3973
+ }
3974
+ function preflightDispatchAssets(input, prepared) {
3975
+ const all = [...new Map(
3976
+ [...prepared.components, ...prepared.preparedCallees].map((component) => [component.id, component])
3977
+ ).values()];
3978
+ if (!all.some((component) => (component.node.assets?.length ?? 0) > 0)) return;
3979
+ if (!input.irPath) {
3980
+ throw new DispatchError(
3981
+ "reachable components declare assets, but executable preparation has no irPath from which to resolve the travelling asset directory"
3982
+ );
3983
+ }
3984
+ const ir = parseIrInput(input.ir);
3985
+ for (const component of all) {
3986
+ if ((component.node.assets?.length ?? 0) === 0) continue;
3987
+ validateAssets(
3988
+ { ...ir, components: [component.node] },
3989
+ input.irPath,
3990
+ component.plan.options.cwd ?? process.cwd()
3991
+ );
3992
+ }
2653
3993
  }
2654
3994
  async function dispatch(input, runCfg) {
2655
3995
  const prepared = prepareDispatch(input);
3996
+ if (prepared.dependencies.length > 0) {
3997
+ assertSupportedComponentInvocationLimits(runCfg.componentLimits);
3998
+ }
3999
+ preflightDispatchAssets(input, prepared);
2656
4000
  const warbleBin = runCfg.warbleBin ?? "warble";
4001
+ if (input.irPath && prepared.dependencies.length > 0) {
4002
+ const ir = parseIrInput(input.ir);
4003
+ const all = [...new Map(
4004
+ [...prepared.components, ...prepared.preparedCallees].map((component) => [component.id, component])
4005
+ ).values()];
4006
+ for (const component of all) {
4007
+ if ((component.node.assets?.length ?? 0) === 0) continue;
4008
+ landAssets(
4009
+ { ...ir, components: [component.node] },
4010
+ input.irPath,
4011
+ component.plan.options.cwd ?? process.cwd()
4012
+ );
4013
+ }
4014
+ }
2657
4015
  const components = [];
2658
4016
  for (const c of prepared.components) {
2659
- const result = await runDispatch(c.plan, {
4017
+ const entry2 = prepared.entries.find((candidate) => candidate.root === c.id);
4018
+ const composed = prepared.dependencies.some((dependency) => entry2?.components.includes(dependency.caller));
4019
+ const result = composed ? await runComposedDispatch(c.plan, {
4020
+ prepared,
4021
+ root: c,
4022
+ outDir: runCfg.outDir,
4023
+ warbleBin,
4024
+ ...runCfg.title ? { title: runCfg.title } : {},
4025
+ ...runCfg.componentLimits ? { limits: runCfg.componentLimits } : {},
4026
+ ...runCfg.signal ? { signal: runCfg.signal } : {},
4027
+ ...runCfg.componentStepRunner ? { runStep: runCfg.componentStepRunner } : {},
4028
+ ...runCfg.hostCanUseTool ? { hostCanUseTool: runCfg.hostCanUseTool } : {},
4029
+ ...runCfg.hostHooks ? { hostHooks: runCfg.hostHooks } : {}
4030
+ }) : await runDispatch(c.plan, {
2660
4031
  outDir: runCfg.outDir,
2661
4032
  warbleBin,
4033
+ ...input.irPath ? { assets: { ir: { ...parseIrInput(input.ir), components: [c.node] }, irPath: input.irPath } } : {},
2662
4034
  ...runCfg.title ? { title: runCfg.title } : {}
2663
4035
  });
2664
4036
  components.push({ id: c.id, report: c.report, plan: c.plan, result });
@@ -2667,9 +4039,9 @@ async function dispatch(input, runCfg) {
2667
4039
  }
2668
4040
 
2669
4041
  // src/manifest.ts
2670
- var MANIFEST_VERSION = "0.1";
2671
- var MIN_SUPPORTED_IR_VERSION = "0.7";
2672
- var MAX_SUPPORTED_IR_VERSION = "0.7";
4042
+ var MANIFEST_VERSION = "0.3";
4043
+ var MIN_SUPPORTED_IR_VERSION = "0.8";
4044
+ var MAX_SUPPORTED_IR_VERSION = "0.8";
2673
4045
  var DEFAULT_MAX_ATTEMPTS = 1;
2674
4046
  function classifyStep(node, stepIndex) {
2675
4047
  const when = node.llm_calls[stepIndex].when;
@@ -2804,10 +4176,11 @@ function buildTools2(node) {
2804
4176
  }
2805
4177
  return out;
2806
4178
  }
2807
- function buildAgentManifest(component) {
4179
+ function buildAgentManifest(component, dependencies = []) {
2808
4180
  const node = component.node;
2809
4181
  return {
2810
4182
  id: node.id,
4183
+ entrypoint: node.entrypoint,
2811
4184
  verb: node.verb,
2812
4185
  component_type: node.type,
2813
4186
  realization_kind: node.realization_kind,
@@ -2818,13 +4191,15 @@ function buildAgentManifest(component) {
2818
4191
  tools: buildTools2(node),
2819
4192
  output_schema: outputSchemaFor(node.effect),
2820
4193
  capabilities: component.report,
4194
+ dependencies: dependencies.filter((dependency) => dependency.caller === node.id).map(({ step, alias, component: callee }) => ({ step, alias, component: callee })),
2821
4195
  ...node.brief !== void 0 ? { brief: node.brief } : {}
2822
4196
  };
2823
4197
  }
2824
- function buildUnavailableAgentManifest(component) {
4198
+ function buildUnavailableAgentManifest(component, dependencies = []) {
2825
4199
  const node = component.node;
2826
4200
  return {
2827
4201
  id: node.id,
4202
+ entrypoint: node.entrypoint,
2828
4203
  verb: node.verb,
2829
4204
  component_type: node.type,
2830
4205
  realization_kind: node.realization_kind,
@@ -2835,23 +4210,48 @@ function buildUnavailableAgentManifest(component) {
2835
4210
  tools: [],
2836
4211
  output_schema: {},
2837
4212
  capabilities: [],
4213
+ capability_inspection: component.inspection,
4214
+ dependencies: dependencies.filter((dependency) => dependency.caller === node.id).map(({ step, alias, component: callee }) => ({ step, alias, component: callee })),
2838
4215
  availability: component.availability
2839
4216
  };
2840
4217
  }
2841
4218
  function buildManifest(prepared, raw) {
2842
4219
  const ir = parseIr(raw);
4220
+ const manifestComponents = "preparedCallees" in prepared ? [...new Map(
4221
+ [...prepared.components, ...prepared.preparedCallees].map((component) => [component.id, component])
4222
+ ).values()] : prepared.components;
4223
+ const byId = new Map(manifestComponents.map((component) => [component.id, component]));
4224
+ const entries = prepared.entries.map((entry2) => {
4225
+ const closureComponents = entry2.components.map((id) => byId.get(id)).filter((component) => component !== void 0);
4226
+ const unavailable2 = closureComponents.find((component) => "availability" in component);
4227
+ const closureIds = new Set(entry2.components);
4228
+ const declaresInvocation = prepared.dependencies.some(
4229
+ (dependency) => closureIds.has(dependency.caller)
4230
+ );
4231
+ const invocation = declaresInvocation ? closureComponents.flatMap((component) => {
4232
+ const report = "report" in component ? component.report : component.inspection;
4233
+ return report.filter((capability) => capability.capability === "component_invocation");
4234
+ })[0] ?? null : null;
4235
+ return {
4236
+ id: entry2.root,
4237
+ closure: [...entry2.components],
4238
+ availability: unavailable2 ? { status: "unavailable", reason: UNAVAILABLE_COMPONENT_REASON } : { status: "available" },
4239
+ invocation_realization: invocation
4240
+ };
4241
+ });
2843
4242
  return {
2844
4243
  manifest_version: MANIFEST_VERSION,
2845
4244
  compat: { min_ir_version: MIN_SUPPORTED_IR_VERSION, max_ir_version: MAX_SUPPORTED_IR_VERSION },
2846
4245
  profile: ir.profile,
2847
4246
  target: prepared.target,
2848
- agents: prepared.components.map((component) => "availability" in component ? buildUnavailableAgentManifest(component) : buildAgentManifest(component))
4247
+ entries,
4248
+ agents: manifestComponents.map((component) => "availability" in component ? buildUnavailableAgentManifest(component, prepared.dependencies) : buildAgentManifest(component, prepared.dependencies))
2849
4249
  };
2850
4250
  }
2851
4251
 
2852
4252
  // src/model_catalog.ts
2853
- import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
2854
- import { resolve as resolve2 } from "path";
4253
+ import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
4254
+ import { resolve as resolve3 } from "path";
2855
4255
  var MODEL_CATALOG_VERSION = 1;
2856
4256
  var CLEANUP_GRACE_MS = 25;
2857
4257
  function unavailable(code, retryable) {
@@ -2901,8 +4301,8 @@ async function settleCleanup(operation) {
2901
4301
  try {
2902
4302
  await Promise.race([
2903
4303
  operation.catch(() => void 0),
2904
- new Promise((resolve3) => {
2905
- timer = setTimeout(resolve3, CLEANUP_GRACE_MS);
4304
+ new Promise((resolve4) => {
4305
+ timer = setTimeout(resolve4, CLEANUP_GRACE_MS);
2906
4306
  })
2907
4307
  ]);
2908
4308
  } finally {
@@ -2915,11 +4315,11 @@ async function discoverClaudeModels(options = {}) {
2915
4315
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return unavailable("protocol_error", false);
2916
4316
  let catalogQuery;
2917
4317
  try {
2918
- const queryFactory = options.queryFactory ?? query3;
4318
+ const queryFactory = options.queryFactory ?? query4;
2919
4319
  catalogQuery = queryFactory({
2920
4320
  prompt: idleInput(),
2921
4321
  options: {
2922
- cwd: resolve2(options.cwd ?? process.cwd()),
4322
+ cwd: resolve3(options.cwd ?? process.cwd()),
2923
4323
  tools: [],
2924
4324
  mcpServers: [],
2925
4325
  settingSources: [],
@@ -2948,8 +4348,8 @@ async function discoverClaudeModels(options = {}) {
2948
4348
  }
2949
4349
 
2950
4350
  // src/session.ts
2951
- import { mkdirSync as mkdirSync3 } from "fs";
2952
- import { join as join4 } from "path";
4351
+ import { mkdirSync as mkdirSync5 } from "fs";
4352
+ import { join as join6 } from "path";
2953
4353
  var BREAKDOWN_RE = /\bby\s+([a-z][a-z0-9_]*(?:\s*(?:,|and)\s*[a-z][a-z0-9_]*)*)/i;
2954
4354
  var FILTER_OVERRIDE_RE = /\b(where|only|instead of|excluding|filtered?\s+to)\b/i;
2955
4355
  function extractBreakdownDimensions(question) {
@@ -3025,8 +4425,8 @@ var ChatSession = class {
3025
4425
  const prompt = buildTurnPrompt(this.state, question);
3026
4426
  const resume = lastSessionId(this.state) ?? this.initialResumeSessionId ?? null;
3027
4427
  const turnPlan = { ...this.plan, prompt };
3028
- const turnOutDir = join4(this.runCfg.outDir, `turn-${this.state.turns.length + 1}`);
3029
- mkdirSync3(turnOutDir, { recursive: true });
4428
+ const turnOutDir = join6(this.runCfg.outDir, `turn-${this.state.turns.length + 1}`);
4429
+ mkdirSync5(turnOutDir, { recursive: true });
3030
4430
  const result = await runDispatch(turnPlan, {
3031
4431
  ...this.runCfg,
3032
4432
  outDir: turnOutDir,
@@ -3050,6 +4450,8 @@ function createChatSession(plan, runCfg, initialResumeSessionId) {
3050
4450
  export {
3051
4451
  DispatchError,
3052
4452
  emitAgentModule,
4453
+ componentDependencies,
4454
+ resolveComponentClosure,
3053
4455
  REALIZATION_KINDS,
3054
4456
  COMPONENT_TYPES,
3055
4457
  TRIGGER_KINDS,
@@ -3081,6 +4483,9 @@ export {
3081
4483
  resolveCapabilities,
3082
4484
  resolveNodeCapabilities,
3083
4485
  inspectNodeCapabilities,
4486
+ assetDirForIr,
4487
+ validateAssets,
4488
+ landAssets,
3084
4489
  fingerprintSurfaces,
3085
4490
  promptSurfacesOf,
3086
4491
  promptSurfaces,
@@ -3097,10 +4502,20 @@ export {
3097
4502
  aggregateTrace,
3098
4503
  DispatchSessionError,
3099
4504
  runDispatch,
4505
+ DEFAULT_COMPONENT_INVOCATION_LIMITS,
4506
+ ComponentStepExecutionError,
4507
+ normalizeComponentRequest,
4508
+ normalizeComponentResult,
4509
+ assertEligibleComponentCallee,
4510
+ assertEligibleComponentCaller,
4511
+ ComponentInvocationRuntime,
4512
+ createSdkComponentStepRunner,
4513
+ runComposedDispatch,
3100
4514
  UNAVAILABLE_COMPONENT_REASON,
3101
4515
  resolveProjectCwd,
3102
4516
  prepareDispatch,
3103
4517
  prepareDisplayManifest,
4518
+ preflightDispatchAssets,
3104
4519
  dispatch,
3105
4520
  buildAgentManifest,
3106
4521
  buildUnavailableAgentManifest,
@@ -3118,4 +4533,4 @@ export {
3118
4533
  ChatSession,
3119
4534
  createChatSession
3120
4535
  };
3121
- //# sourceMappingURL=chunk-7XQ5F6FM.js.map
4536
+ //# sourceMappingURL=chunk-2VDMVARW.js.map