@warble/claude-agent-sdk 0.11.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),
@@ -519,7 +699,46 @@ function parseComponent(value, at) {
519
699
  context_precondition: preconditionArray(obj, "context_precondition", 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
- brief: optStringU(obj, "brief")
702
+ brief: optStringU(obj, "brief"),
703
+ ...slotsField(obj, at),
704
+ ...assetsField(obj, at)
705
+ };
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
+ }
719
+ function slotsField(obj, at) {
720
+ if (obj["slots"] === void 0) return {};
721
+ const slots = requireArray(obj, "slots", at).map((v, i) => parseSlot(v, `${at}.slots[${i}]`));
722
+ return { slots };
723
+ }
724
+ function parseSlot(value, at) {
725
+ const obj = requireObject(value, at);
726
+ const variantsRaw = requireObject(obj["variants"], `${at}.variants`);
727
+ const variants = {};
728
+ for (const [key, text] of Object.entries(variantsRaw)) {
729
+ if (typeof text !== "string") fail(`${at}.variants.${key} must be a string`);
730
+ variants[key] = text;
731
+ }
732
+ const name = requireString(obj, "name", at);
733
+ const dflt = requireString(obj, "default", at);
734
+ if (!(dflt in variants)) {
735
+ fail(`${at}.default is '${dflt}', which is not one of its variants (${Object.keys(variants).join(", ")})`);
736
+ }
737
+ return {
738
+ name,
739
+ default: dflt,
740
+ variants,
741
+ ...obj["present_when"] === void 0 ? {} : { present_when: obj["present_when"] }
523
742
  };
524
743
  }
525
744
  function parseIr(json2) {
@@ -544,7 +763,8 @@ function parseIr(json2) {
544
763
  config,
545
764
  components: requireArray(obj, "components", "<root>").map(
546
765
  (c, i) => parseComponent(c, `components[${i}]`)
547
- )
766
+ ),
767
+ ...slotsField(obj, "<root>")
548
768
  };
549
769
  }
550
770
  function distinctTiers(calls) {
@@ -559,6 +779,124 @@ function distinctTiers(calls) {
559
779
  return out;
560
780
  }
561
781
 
782
+ // src/slots.ts
783
+ var SLOT_NAME = /^[a-z_][a-z0-9_]*$/;
784
+ function slotReferences(raw) {
785
+ const found = [];
786
+ let cursor = 0;
787
+ for (; ; ) {
788
+ const open = raw.indexOf("{{", cursor);
789
+ if (open === -1) return found;
790
+ const bodyStart = open + 2;
791
+ const close = raw.indexOf("}}", bodyStart);
792
+ if (close === -1) return found;
793
+ const body = raw.slice(bodyStart, close).trim();
794
+ if (body.startsWith("slot.")) {
795
+ const name = body.slice("slot.".length);
796
+ if (SLOT_NAME.test(name)) found.push({ start: open, end: close + 2, name });
797
+ }
798
+ cursor = close + 2;
799
+ }
800
+ }
801
+ function resolveSlots(decls, supply, scope, unanswered = "fail") {
802
+ const byName = new Map(decls.map((d) => [d.name, d]));
803
+ const resolved = /* @__PURE__ */ new Map();
804
+ const visiting = /* @__PURE__ */ new Set();
805
+ const resolve4 = (name) => {
806
+ if (resolved.has(name)) return resolved.get(name);
807
+ if (visiting.has(name)) {
808
+ throw new DispatchError(
809
+ `slot '${name}' in ${scope} is defined in terms of itself: a variant's text references a slot whose own variant references it back (chain: ${[...visiting, name].join(" -> ")}).`
810
+ );
811
+ }
812
+ const decl = byName.get(name);
813
+ const choice = supply[name];
814
+ if (choice === null) {
815
+ resolved.set(name, null);
816
+ return null;
817
+ }
818
+ if (choice === void 0) {
819
+ if (decl.present_when !== void 0 && unanswered === "fail") {
820
+ throw new DispatchError(
821
+ `slot '${name}' in ${scope} declares a present_when condition, and nothing answered it. Supply the slot as a variant name to include it, or as null to remove it; falling back to the default would ship wording for something that may not be there.`
822
+ );
823
+ }
824
+ }
825
+ const key = choice ?? decl.default;
826
+ const text = decl.variants[key];
827
+ if (text === void 0) {
828
+ throw new DispatchError(
829
+ `slot '${name}' in ${scope} was given variant '${key}', which it does not declare (declared: ${Object.keys(decl.variants).join(", ")}).`
830
+ );
831
+ }
832
+ visiting.add(name);
833
+ const expanded = substitute(text, resolve4, byName, scope);
834
+ visiting.delete(name);
835
+ resolved.set(name, expanded);
836
+ return expanded;
837
+ };
838
+ for (const decl of decls) resolve4(decl.name);
839
+ return resolved;
840
+ }
841
+ function substitute(raw, resolve4, byName, scope) {
842
+ const refs = slotReferences(raw);
843
+ if (refs.length === 0) return raw;
844
+ let out = "";
845
+ let cursor = 0;
846
+ for (const ref of refs) {
847
+ if (!byName.has(ref.name)) {
848
+ throw new DispatchError(
849
+ `prompt text in ${scope} references slot '${ref.name}', which is neither one of the component's slots nor one of the profile's.`
850
+ );
851
+ }
852
+ out += raw.slice(cursor, ref.start) + (resolve4(ref.name) ?? "");
853
+ cursor = ref.end;
854
+ }
855
+ return out + raw.slice(cursor);
856
+ }
857
+ function applySlots(raw, resolved, scope) {
858
+ const refs = slotReferences(raw);
859
+ if (refs.length === 0) return raw;
860
+ let out = "";
861
+ let cursor = 0;
862
+ for (const ref of refs) {
863
+ if (!resolved.has(ref.name)) {
864
+ throw new DispatchError(
865
+ `prompt text in ${scope} references slot '${ref.name}', which is not declared there.`
866
+ );
867
+ }
868
+ out += raw.slice(cursor, ref.start) + (resolved.get(ref.name) ?? "");
869
+ cursor = ref.end;
870
+ }
871
+ return out + raw.slice(cursor);
872
+ }
873
+ function assertNoSlotReferences(raw, owner) {
874
+ const refs = slotReferences(raw);
875
+ if (refs.length > 0) {
876
+ const names = [...new Set(refs.map((r) => r.name))].join(", ");
877
+ throw new DispatchError(
878
+ `${owner} still contains unresolved slot reference(s) (${names}) at dispatch. Prompt text must go through slot resolution before it is sent; shipping the placeholder would put literal '{{ slot.\u2026 }}' in front of the model.`
879
+ );
880
+ }
881
+ }
882
+ function parseSlotFlags(flags) {
883
+ const supply = {};
884
+ for (const flag of flags) {
885
+ const at = flag.indexOf("=");
886
+ if (at === -1) {
887
+ throw new DispatchError(
888
+ `--slot '${flag}' must be NAME=VARIANT, or NAME= to remove a conditional slot`
889
+ );
890
+ }
891
+ const name = flag.slice(0, at);
892
+ const variant = flag.slice(at + 1);
893
+ if (name === "") throw new DispatchError(`--slot '${flag}' has an empty slot name`);
894
+ if (name in supply) throw new DispatchError(`--slot ${name} was given more than once`);
895
+ supply[name] = variant === "" ? null : variant;
896
+ }
897
+ return supply;
898
+ }
899
+
562
900
  // src/models.ts
563
901
  import { parse as parseYaml } from "yaml";
564
902
  var STRONG_TIER = "strong";
@@ -766,14 +1104,14 @@ function planProviderRouting(node, models, anthropicSplit) {
766
1104
  }
767
1105
  return { mode, steps, providers };
768
1106
  }
769
- function buildStepMessages(step, question, slots) {
1107
+ function buildStepMessages(step, question, artifacts) {
770
1108
  const parts = [`Question: ${question}`];
771
- for (const slot of step.consumes) {
772
- const value = slots[slot];
1109
+ for (const artifact of step.consumes) {
1110
+ const value = artifacts[artifact];
773
1111
  parts.push(
774
1112
  value === void 0 ? `
775
- [input '${slot}' was not produced by an earlier step]` : `
776
- Input '${slot}':
1113
+ [input '${artifact}' was not produced by an earlier step]` : `
1114
+ Input '${artifact}':
777
1115
  ${value}`
778
1116
  );
779
1117
  }
@@ -797,6 +1135,13 @@ function entry(outcome, via, provided_by, criticality, note) {
797
1135
  }
798
1136
  function localProfile() {
799
1137
  return {
1138
+ component_invocation: entry(
1139
+ "native",
1140
+ "isolated-child-query",
1141
+ "runtime",
1142
+ "required",
1143
+ null
1144
+ ),
800
1145
  "sql_execution:read_only": entry("native", "bash-wren", "runtime", "required", null),
801
1146
  genbi_build: entry("native", "bash-wren", "runtime", "required", null),
802
1147
  // Reading the semantic model's structure (models/metrics/lineage) is borrowed from the `wren`
@@ -1193,9 +1538,9 @@ function buildDriverBody(node) {
1193
1538
  `Run the \`${subagentName(node.verb, call.name)}\` subagent (step \`${call.name}\`) via the Task tool.`
1194
1539
  ];
1195
1540
  if (call.consumes.length > 0) {
1196
- const sources = call.consumes.map((slot) => {
1197
- const producer = producers.get(slot);
1198
- return producer ? `\`${slot}\` (the \`${producer}\` subagent's output)` : `\`${slot}\``;
1541
+ const sources = call.consumes.map((artifact) => {
1542
+ const producer = producers.get(artifact);
1543
+ return producer ? `\`${artifact}\` (the \`${producer}\` subagent's output)` : `\`${artifact}\``;
1199
1544
  }).join(", ");
1200
1545
  parts.push(`Pass it ${sources} as input.`);
1201
1546
  }
@@ -1239,6 +1584,29 @@ function tierCollapseNote(node, model) {
1239
1584
  return `per-step tiers [${steps}] collapsed to single model '${model}'`;
1240
1585
  }
1241
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) {
1593
+ const plan = buildDispatchPlanUnchecked(node, report, cfg);
1594
+ assertPlanHasNoSlotReferences(plan, node.id);
1595
+ return plan;
1596
+ }
1597
+ function assertPlanHasNoSlotReferences(plan, componentId) {
1598
+ const system = plan.options.systemPrompt;
1599
+ if (typeof system === "string") {
1600
+ assertNoSlotReferences(system, `the system prompt of component '${componentId}'`);
1601
+ }
1602
+ for (const [name, agent] of Object.entries(plan.options.agents ?? {})) {
1603
+ assertNoSlotReferences(agent.prompt, `subagent '${name}' of component '${componentId}'`);
1604
+ }
1605
+ for (const step of plan.meta.stagedSteps) {
1606
+ assertNoSlotReferences(step.prompt, `staged step '${step.name}' of component '${componentId}'`);
1607
+ }
1608
+ }
1609
+ function buildDispatchPlanUnchecked(node, report, cfg) {
1242
1610
  if (!realizationSupported(node)) {
1243
1611
  throw unsupported("realization_kind", node.realization_kind);
1244
1612
  }
@@ -1329,7 +1697,8 @@ function buildDispatchPlan(node, report, cfg) {
1329
1697
  mode: "sdk-split",
1330
1698
  providers: ["anthropic"],
1331
1699
  stagedSteps: [],
1332
- setupScope
1700
+ setupScope,
1701
+ componentInvocation: node.llm_calls.some((step) => step.component_calls.length > 0)
1333
1702
  }
1334
1703
  };
1335
1704
  }
@@ -1369,7 +1738,8 @@ function buildDispatchPlan(node, report, cfg) {
1369
1738
  mode: "single",
1370
1739
  providers: ["anthropic"],
1371
1740
  stagedSteps: [],
1372
- setupScope
1741
+ setupScope,
1742
+ componentInvocation: node.llm_calls.some((step) => step.component_calls.length > 0)
1373
1743
  }
1374
1744
  };
1375
1745
  }
@@ -1421,7 +1791,8 @@ function buildHybridStagedPlan(node, gate, cfg, base, readOnly, providers, steps
1421
1791
  // Hybrid+setup is out of scope (locked decision): a setup component's steps are not staged
1422
1792
  // across providers, so this path never sees setup_execution in practice; null is the safe,
1423
1793
  // explicit default rather than silently inheriting a scope this path doesn't enforce.
1424
- setupScope: null
1794
+ setupScope: null,
1795
+ componentInvocation: node.llm_calls.some((step) => step.component_calls.length > 0)
1425
1796
  }
1426
1797
  };
1427
1798
  }
@@ -1488,6 +1859,7 @@ function inspectCapabilities(node, targetId, profile) {
1488
1859
  provided_by: e.provided_by,
1489
1860
  criticality: e.criticality
1490
1861
  };
1862
+ if (e.via !== null) resolved.via = e.via;
1491
1863
  if (e.note !== null) resolved.note = e.note;
1492
1864
  return resolved;
1493
1865
  });
@@ -1524,11 +1896,147 @@ function inspectNodeCapabilities(node, targetId) {
1524
1896
  return inspectCapabilities(node, targetId, localProfile());
1525
1897
  }
1526
1898
 
1899
+ // src/assets.ts
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";
1989
+ var ALGORITHM = "sha256";
1990
+ function sha256(text) {
1991
+ return createHash2(ALGORITHM).update(text, "utf8").digest("hex");
1992
+ }
1993
+ function fingerprintSurfaces(surfaces) {
1994
+ const perSurface = {};
1995
+ const parts = [];
1996
+ for (const name of Object.keys(surfaces).sort()) {
1997
+ const text = surfaces[name];
1998
+ perSurface[name] = sha256(text);
1999
+ parts.push(`${name.length}:${name} ${text.length}:${text}`);
2000
+ }
2001
+ return { algorithm: ALGORITHM, digest: sha256(parts.join(" ")), surfaces: perSurface };
2002
+ }
2003
+ function promptSurfacesOf(options) {
2004
+ const surfaces = {};
2005
+ const system = options.systemPrompt;
2006
+ if (typeof system === "string") surfaces["driver.systemPrompt"] = system;
2007
+ for (const [name, agent] of Object.entries(options.agents ?? {})) {
2008
+ surfaces[`subagent.${name}`] = agent.prompt;
2009
+ }
2010
+ return surfaces;
2011
+ }
2012
+ function promptSurfacesOfMessages(messages) {
2013
+ return Object.fromEntries(
2014
+ messages.map((message, index) => [`local.${index}.${message.role}`, message.content])
2015
+ );
2016
+ }
2017
+ function promptSurfaces(plan) {
2018
+ return promptSurfacesOf(plan.options);
2019
+ }
2020
+ function fingerprintPrompts(plan) {
2021
+ return fingerprintSurfaces(promptSurfaces(plan));
2022
+ }
2023
+
1527
2024
  // src/guardrails.ts
1528
2025
  import { resolve as resolvePath, sep as pathSep } from "path";
1529
2026
  function withinScope(abs, scopeAbs) {
1530
2027
  return abs === scopeAbs || abs.startsWith(scopeAbs.endsWith(pathSep) ? scopeAbs : scopeAbs + pathSep);
1531
2028
  }
2029
+ function composeCanUseTool(embedder, floor) {
2030
+ if (embedder === void 0) return floor;
2031
+ return async (toolName, input, options) => {
2032
+ const fromEmbedder = await embedder(toolName, input, options);
2033
+ if (fromEmbedder.behavior !== "allow") return fromEmbedder;
2034
+ return floor(toolName, fromEmbedder.updatedInput, options);
2035
+ };
2036
+ }
2037
+ function composeHooks(embedder, guardHooks) {
2038
+ return { ...embedder, PreToolUse: [...embedder?.PreToolUse ?? [], ...guardHooks] };
2039
+ }
1532
2040
  var DESTRUCTIVE = /\b(rm|sudo|dd|mkfs|shutdown|reboot|kill|chmod|chown|mv|cp)\b/;
1533
2041
  var REDIRECTION = /(^|[^>])>>?[^>]/;
1534
2042
  var DOTENV_READER_COMMANDS = /\b(cat|head|tail|less|more|od|xxd|strings|grep|awk|sed)\b/;
@@ -1693,8 +2201,8 @@ async function callOpenAiCompat(opts) {
1693
2201
  }
1694
2202
 
1695
2203
  // src/hybridTool.ts
1696
- import { existsSync, mkdirSync, writeFileSync } from "fs";
1697
- import { join } from "path";
2204
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
2205
+ import { join as join2 } from "path";
1698
2206
  import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
1699
2207
  import { z } from "zod";
1700
2208
  function isResult(msg) {
@@ -1728,9 +2236,9 @@ function buildToolDriverPrompt(steps) {
1728
2236
  const lines = steps.map((s, i) => {
1729
2237
  const parts = [`${i + 1}. Call the \`dispatch_step\` tool with step="${s.name}".`];
1730
2238
  if (s.consumes.length > 0) {
1731
- const srcs = s.consumes.map((slot) => {
1732
- const p = producers.get(slot);
1733
- return p ? `the text step "${p}" returned` : `"${slot}"`;
2239
+ const srcs = s.consumes.map((artifact) => {
2240
+ const p = producers.get(artifact);
2241
+ return p ? `the text step "${p}" returned` : `"${artifact}"`;
1734
2242
  }).join(", ");
1735
2243
  parts.push(`Pass ${srcs} as the tool's \`inputs\` argument.`);
1736
2244
  }
@@ -1748,6 +2256,9 @@ function buildToolDriverPrompt(steps) {
1748
2256
  "Marshal each step's returned text into the next step's `inputs` exactly as noted above. Your FINAL message MUST be the last executed step's returned text verbatim \u2014 do not summarize it or add commentary."
1749
2257
  ].join("\n");
1750
2258
  }
2259
+ var MCP_SERVER_NAME = "warble";
2260
+ var DISPATCH_STEP_NAME = "dispatch_step";
2261
+ var DISPATCH_STEP_TOOL = `mcp__${MCP_SERVER_NAME}__${DISPATCH_STEP_NAME}`;
1751
2262
  async function runCloudStep(step, question, inputsText, ctx) {
1752
2263
  const options = {
1753
2264
  cwd: ctx.cwd,
@@ -1763,15 +2274,16 @@ ${step.prompt}`,
1763
2274
  canUseTool: ctx.canUseTool,
1764
2275
  // Read never reaches `canUseTool` for an in-cwd path in the real SDK (see guardrails.ts); this
1765
2276
  // hook is the live enforcement point for the +Setup dotenv-read gap's Read side.
1766
- hooks: { PreToolUse: ctx.hooks },
2277
+ hooks: ctx.hooks,
1767
2278
  env: ctx.env
1768
2279
  };
2280
+ ctx.cfg.onPromptFingerprint?.(fingerprintSurfaces(promptSurfacesOf(options)));
1769
2281
  const msgs = [];
1770
2282
  for await (const m of query({ prompt: stepUserPrompt(question, inputsText), options })) msgs.push(m);
1771
2283
  return requireFinalText(msgs.find(isResult));
1772
2284
  }
1773
2285
  async function runHybridTool(plan, cfg) {
1774
- mkdirSync(cfg.outDir, { recursive: true });
2286
+ mkdirSync2(cfg.outDir, { recursive: true });
1775
2287
  const cwd = plan.options.cwd ?? process.cwd();
1776
2288
  const { canUseTool, denials, hooks } = makeReadOnlyGuard({
1777
2289
  readOnly: plan.meta.readOnly,
@@ -1779,15 +2291,18 @@ async function runHybridTool(plan, cfg) {
1779
2291
  cwd,
1780
2292
  setupScope: plan.meta.setupScope
1781
2293
  });
1782
- const venvBin = join(cwd, ".venv", "bin");
1783
- 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 ?? "";
1784
2296
  const env = { ...process.env, PATH: pathEnv };
2297
+ const stepCanUseTool = composeCanUseTool(plan.options.canUseTool, canUseTool);
2298
+ const stepHooks = composeHooks(plan.options.hooks, hooks);
2299
+ const driverCanUseTool = async (toolName, input, options) => toolName === DISPATCH_STEP_TOOL ? { behavior: "allow", updatedInput: input } : stepCanUseTool(toolName, input, options);
1785
2300
  const steps = plan.meta.stagedSteps;
1786
2301
  const question = plan.prompt;
1787
2302
  const maxTurns = plan.options.maxTurns ?? 40;
1788
2303
  const traceSteps = [];
1789
2304
  const dispatchStep = tool(
1790
- "dispatch_step",
2305
+ DISPATCH_STEP_NAME,
1791
2306
  "Execute one named step of the task on its own configured model and return its text output.",
1792
2307
  { step: z.string(), inputs: z.string().optional() },
1793
2308
  async (args) => {
@@ -1799,18 +2314,20 @@ async function runHybridTool(plan, cfg) {
1799
2314
  let text;
1800
2315
  if (step.provider === "openai_compat") {
1801
2316
  if (!step.endpoint) throw new DispatchError(`local step '${step.name}' has no endpoint`);
2317
+ const localMessages = [
2318
+ { role: "system", content: step.prompt },
2319
+ { role: "user", content: stepUserPrompt(question, inputsText) }
2320
+ ];
2321
+ cfg.onPromptFingerprint?.(fingerprintSurfaces(promptSurfacesOfMessages(localMessages)));
1802
2322
  text = await callOpenAiCompat({
1803
2323
  endpoint: step.endpoint,
1804
2324
  model: step.model,
1805
- messages: [
1806
- { role: "system", content: step.prompt },
1807
- { role: "user", content: stepUserPrompt(question, inputsText) }
1808
- ]
2325
+ messages: localMessages
1809
2326
  });
1810
2327
  process.stderr.write(`warble hybrid-tool: step '${step.name}' \u2192 local ${step.model}
1811
2328
  `);
1812
2329
  } else {
1813
- text = await runCloudStep(step, question, inputsText, { cwd, env, maxTurns, canUseTool, hooks });
2330
+ text = await runCloudStep(step, question, inputsText, { cfg, cwd, env, maxTurns, canUseTool: stepCanUseTool, hooks: stepHooks });
1814
2331
  process.stderr.write(`warble hybrid-tool: step '${step.name}' \u2192 cloud ${step.model}
1815
2332
  `);
1816
2333
  }
@@ -1818,7 +2335,7 @@ async function runHybridTool(plan, cfg) {
1818
2335
  return { content: [{ type: "text", text }] };
1819
2336
  }
1820
2337
  );
1821
- const server = createSdkMcpServer({ name: "warble", version: "0.0.0", tools: [dispatchStep] });
2338
+ const server = createSdkMcpServer({ name: MCP_SERVER_NAME, version: "0.0.0", tools: [dispatchStep] });
1822
2339
  const driverModel = plan.options.model ?? "sonnet";
1823
2340
  const driverOptions = {
1824
2341
  cwd,
@@ -1827,9 +2344,22 @@ async function runHybridTool(plan, cfg) {
1827
2344
  model: driverModel,
1828
2345
  systemPrompt: buildToolDriverPrompt(steps),
1829
2346
  mcpServers: { warble: server },
1830
- allowedTools: ["mcp__warble__dispatch_step"],
1831
- env
2347
+ allowedTools: [DISPATCH_STEP_TOOL],
2348
+ env,
2349
+ // The floor is composed in here too, even though the driver prompt asks this turn to do nothing
2350
+ // but call `dispatch_step`. `allowedTools` does not restrict the toolset — the SDK documents it
2351
+ // as "auto-allowed without prompting" and says to use `tools` to restrict — and `tools` is not
2352
+ // set here, so the default built-in set (Bash, Write, Edit, …) is on the table for this turn.
2353
+ // Composing the floor is therefore the difference between the prompt asking the model not to
2354
+ // reach for them and something actually stopping it.
2355
+ //
2356
+ // This tightens behaviour rather than preserving it: before, no `canUseTool` reached this turn
2357
+ // at all. That is the intended direction — a guardrail floor applying where it previously did
2358
+ // not — and it is why this is not spread conditionally like an embedder-only passthrough.
2359
+ canUseTool: driverCanUseTool,
2360
+ hooks: stepHooks
1832
2361
  };
2362
+ cfg.onPromptFingerprint?.(fingerprintSurfaces(promptSurfacesOf(driverOptions)));
1833
2363
  const msgs = [];
1834
2364
  for await (const m of query({ prompt: question, options: driverOptions })) msgs.push(m);
1835
2365
  const result = msgs.find(isResult);
@@ -1848,8 +2378,8 @@ async function runHybridTool(plan, cfg) {
1848
2378
  steps: traceSteps,
1849
2379
  denials
1850
2380
  };
1851
- writeFileSync(join(cfg.outDir, "result.txt"), finalText, "utf8");
1852
- 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");
1853
2383
  return {
1854
2384
  finalText,
1855
2385
  trace,
@@ -1862,13 +2392,13 @@ async function runHybridTool(plan, cfg) {
1862
2392
 
1863
2393
  // src/render.ts
1864
2394
  import { spawnSync } from "child_process";
1865
- import { mkdtempSync, writeFileSync as writeFileSync2 } from "fs";
2395
+ import { mkdtempSync, writeFileSync as writeFileSync3 } from "fs";
1866
2396
  import { tmpdir } from "os";
1867
- import { join as join2 } from "path";
2397
+ import { join as join3 } from "path";
1868
2398
  function renderEnvelope(finalText, outPath, opts) {
1869
- const dir = mkdtempSync(join2(tmpdir(), "warble-sdk-"));
1870
- const envelopePath = join2(dir, "envelope.txt");
1871
- writeFileSync2(envelopePath, finalText, "utf8");
2399
+ const dir = mkdtempSync(join3(tmpdir(), "warble-sdk-"));
2400
+ const envelopePath = join3(dir, "envelope.txt");
2401
+ writeFileSync3(envelopePath, finalText, "utf8");
1872
2402
  const args = ["render", envelopePath, "--out", outPath];
1873
2403
  if (opts.title) args.push("--title", opts.title);
1874
2404
  const proc = spawnSync(opts.warbleBin, args, { encoding: "utf8" });
@@ -1892,8 +2422,8 @@ The 'warble' binary was not found. Install it with 'cargo install warble-cli' (r
1892
2422
  }
1893
2423
 
1894
2424
  // src/run.ts
1895
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
1896
- 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";
1897
2427
  import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
1898
2428
 
1899
2429
  // src/conditional.ts
@@ -1905,10 +2435,10 @@ function tryParseJson(text) {
1905
2435
  return void 0;
1906
2436
  }
1907
2437
  }
1908
- function readFlag(slots, target) {
1909
- const [slotName, ...path] = target.split(".");
1910
- if (slotName === void 0) return false;
1911
- const raw = slots[slotName];
2438
+ function readFlag(artifacts, target) {
2439
+ const [artifactName, ...path] = target.split(".");
2440
+ if (artifactName === void 0) return false;
2441
+ const raw = artifacts[artifactName];
1912
2442
  if (raw === void 0) return false;
1913
2443
  let cur = tryParseJson(raw);
1914
2444
  for (const key of path) {
@@ -1922,9 +2452,9 @@ function evaluateGuard(when, state) {
1922
2452
  case "on_failure":
1923
2453
  return state.outcomes[when.target] === "failure";
1924
2454
  case "on_flag":
1925
- return readFlag(state.slots, when.target);
2455
+ return readFlag(state.artifacts, when.target);
1926
2456
  case "on_missing":
1927
- return state.slots[when.target] === void 0;
2457
+ return state.artifacts[when.target] === void 0;
1928
2458
  default:
1929
2459
  throw new DispatchError(
1930
2460
  `unknown guard '${when.guard}' (closed vocabulary: on_failure, on_flag, on_missing)`
@@ -2106,10 +2636,15 @@ function requireFinalText2(result) {
2106
2636
  return result.result;
2107
2637
  }
2108
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
+ }
2109
2644
  if (plan.meta.mode === "hybrid-staged") {
2110
2645
  return process.env["WARBLE_HYBRID_MODE"] === "tool" ? runHybridTool(plan, cfg) : runHybridStaged(plan, cfg);
2111
2646
  }
2112
- mkdirSync2(cfg.outDir, { recursive: true });
2647
+ mkdirSync3(cfg.outDir, { recursive: true });
2113
2648
  const cwd = plan.options.cwd ?? process.cwd();
2114
2649
  const gate = plan.meta.render;
2115
2650
  const writeScope = gate.kind === "realize" && gate.flavor === "prompt" ? gate.scope : null;
@@ -2119,18 +2654,20 @@ async function runDispatch(plan, cfg) {
2119
2654
  cwd,
2120
2655
  setupScope: plan.meta.setupScope
2121
2656
  });
2122
- const venvBin = join3(cwd, ".venv", "bin");
2123
- 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 ?? "";
2124
2659
  const env = { ...process.env, PATH: pathEnv };
2125
2660
  const options = {
2126
2661
  ...plan.options,
2127
- canUseTool,
2662
+ canUseTool: composeCanUseTool(plan.options.canUseTool, canUseTool),
2128
2663
  // Read never reaches `canUseTool` for an in-cwd path in the real SDK (see guardrails.ts); this
2129
2664
  // hook is the live enforcement point for the +Setup dotenv-read gap's Read side.
2130
- hooks: { ...plan.options.hooks, PreToolUse: [...plan.options.hooks?.PreToolUse ?? [], ...hooks] },
2665
+ hooks: composeHooks(plan.options.hooks, hooks),
2131
2666
  env,
2132
2667
  ...cfg.resume ? { resume: cfg.resume } : {}
2133
2668
  };
2669
+ if (cfg.assets) landAssets(cfg.assets.ir, cfg.assets.irPath, cwd);
2670
+ reportPromptFingerprint(cfg, options);
2134
2671
  const mapper = new ChatEventMapper(plan.meta.verb);
2135
2672
  const messages = [];
2136
2673
  for await (const message of query2({ prompt: plan.prompt, options })) {
@@ -2142,12 +2679,12 @@ async function runDispatch(plan, cfg) {
2142
2679
  if (cfg.onEvent) for (const event of mapper.finish(true)) cfg.onEvent(event);
2143
2680
  const trace = aggregateTrace(messages, plan.meta, denials);
2144
2681
  const sessionId = result?.session_id ?? null;
2145
- writeFileSync3(join3(cfg.outDir, "result.txt"), finalText, "utf8");
2146
- 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");
2147
2684
  let htmlPath = null;
2148
2685
  let renderDegraded = null;
2149
2686
  if (gate.kind === "realize" && gate.flavor === "programmatic") {
2150
- const out = join3(cfg.outDir, "dashboard.html");
2687
+ const out = join4(cfg.outDir, "dashboard.html");
2151
2688
  const realized = realizeRender(gate, finalText, out, {
2152
2689
  warbleBin: cfg.warbleBin,
2153
2690
  ...cfg.title ? { title: cfg.title } : {}
@@ -2155,7 +2692,7 @@ async function runDispatch(plan, cfg) {
2155
2692
  htmlPath = realized.htmlPath;
2156
2693
  renderDegraded = realized.renderDegraded;
2157
2694
  } else if (plan.meta.assertion) {
2158
- const out = join3(cfg.outDir, "status.html");
2695
+ const out = join4(cfg.outDir, "status.html");
2159
2696
  renderEnvelope(finalText, out, {
2160
2697
  warbleBin: cfg.warbleBin,
2161
2698
  ...cfg.title ? { title: cfg.title } : {}
@@ -2170,15 +2707,19 @@ function hybridCloudPreamble(cwd) {
2170
2707
  "All data access MUST go through the `wren` CLI \u2014 never raw SQL clients."
2171
2708
  ].join("\n");
2172
2709
  }
2173
- function slotKey(step) {
2710
+ function reportPromptFingerprint(cfg, options) {
2711
+ cfg.onPromptFingerprint?.(fingerprintSurfaces(promptSurfacesOf(options)));
2712
+ }
2713
+ function artifactKey(step) {
2174
2714
  return step.produces ?? step.name;
2175
2715
  }
2176
- async function executeStep(step, slots, ctx, tolerant) {
2177
- const messages = buildStepMessages(step, ctx.plan.prompt, slots);
2716
+ async function executeStep(step, artifacts, ctx, tolerant) {
2717
+ const messages = buildStepMessages(step, ctx.plan.prompt, artifacts);
2178
2718
  const userPrompt = messages.find((m) => m.role === "user")?.content ?? ctx.plan.prompt;
2179
2719
  try {
2180
2720
  if (step.provider === "openai_compat") {
2181
2721
  if (!step.endpoint) throw new DispatchError(`local step '${step.name}' has no endpoint`);
2722
+ ctx.cfg.onPromptFingerprint?.(fingerprintSurfaces(promptSurfacesOfMessages(messages)));
2182
2723
  const text2 = await callOpenAiCompat({ endpoint: step.endpoint, model: step.model, messages });
2183
2724
  ctx.steps.push({ model: `openai_compat:${step.model}`, parent_tool_use_id: step.name, usage: null });
2184
2725
  process.stderr.write(`warble hybrid: step '${step.name}' \u2192 local ${step.model}
@@ -2199,9 +2740,10 @@ ${step.prompt}`,
2199
2740
  canUseTool: ctx.canUseTool,
2200
2741
  // Read never reaches `canUseTool` for an in-cwd path in the real SDK (see guardrails.ts); this
2201
2742
  // hook is the live enforcement point for the +Setup dotenv-read gap's Read side.
2202
- hooks: { PreToolUse: ctx.hooks },
2743
+ hooks: ctx.hooks,
2203
2744
  env: ctx.env
2204
2745
  };
2746
+ reportPromptFingerprint(ctx.cfg, stepOptions);
2205
2747
  const msgs = [];
2206
2748
  for await (const message of query2({ prompt: userPrompt, options: stepOptions })) {
2207
2749
  msgs.push(message);
@@ -2226,7 +2768,7 @@ ${step.prompt}`,
2226
2768
  }
2227
2769
  }
2228
2770
  async function runHybridStaged(plan, cfg) {
2229
- mkdirSync2(cfg.outDir, { recursive: true });
2771
+ mkdirSync3(cfg.outDir, { recursive: true });
2230
2772
  const cwd = plan.options.cwd ?? process.cwd();
2231
2773
  const { canUseTool, denials, hooks } = makeReadOnlyGuard({
2232
2774
  readOnly: plan.meta.readOnly,
@@ -2234,19 +2776,20 @@ async function runHybridStaged(plan, cfg) {
2234
2776
  cwd,
2235
2777
  setupScope: plan.meta.setupScope
2236
2778
  });
2237
- const venvBin = join3(cwd, ".venv", "bin");
2238
- 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 ?? "";
2239
2781
  const env = { ...process.env, PATH: pathEnv };
2240
- const slots = {};
2782
+ const artifacts = {};
2241
2783
  const outcomes = {};
2242
2784
  const steps = [];
2243
2785
  let finalText = "";
2244
2786
  let totalCost = 0;
2245
2787
  const startedAll = Date.now();
2246
2788
  const execCtx = {
2789
+ cfg,
2247
2790
  cwd,
2248
- canUseTool,
2249
- hooks,
2791
+ canUseTool: composeCanUseTool(plan.options.canUseTool, canUseTool),
2792
+ hooks: composeHooks(plan.options.hooks, hooks),
2250
2793
  env,
2251
2794
  plan,
2252
2795
  steps,
@@ -2270,7 +2813,7 @@ async function runHybridStaged(plan, cfg) {
2270
2813
  const preceding = i > 0 ? stagedSteps[i - 1] : null;
2271
2814
  const precedingIdentity = preceding === null ? null : { name: preceding.name, produces: preceding.produces };
2272
2815
  const decision = classifyConditionalStep(step.when, step.consumes, precedingIdentity, {
2273
- slots,
2816
+ artifacts,
2274
2817
  outcomes
2275
2818
  });
2276
2819
  if (decision.kind === "skip") {
@@ -2279,11 +2822,11 @@ async function runHybridStaged(plan, cfg) {
2279
2822
  continue;
2280
2823
  }
2281
2824
  if (decision.kind === "repair") {
2282
- let lastFailureText = slots[decision.target.produces ?? decision.target.name] ?? "";
2825
+ let lastFailureText = artifacts[decision.target.produces ?? decision.target.name] ?? "";
2283
2826
  const { recovered, attempts } = await runRepairLoop(DEFAULT_MAX_REPAIR_ATTEMPTS, async () => {
2284
- const attempt = await executeStep(step, slots, execCtx, true);
2827
+ const attempt = await executeStep(step, artifacts, execCtx, true);
2285
2828
  outcomes[step.name] = attempt.outcome;
2286
- slots[slotKey(step)] = attempt.text;
2829
+ artifacts[artifactKey(step)] = attempt.text;
2287
2830
  if (attempt.outcome === "success") finalText = attempt.text;
2288
2831
  else lastFailureText = attempt.text;
2289
2832
  return { failed: attempt.outcome === "failure" };
@@ -2300,9 +2843,9 @@ async function runHybridStaged(plan, cfg) {
2300
2843
  continue;
2301
2844
  }
2302
2845
  }
2303
- const outcome = await executeStep(step, slots, execCtx, failureGuardTargets.has(step.name));
2846
+ const outcome = await executeStep(step, artifacts, execCtx, failureGuardTargets.has(step.name));
2304
2847
  outcomes[step.name] = outcome.outcome;
2305
- slots[slotKey(step)] = outcome.text;
2848
+ artifacts[artifactKey(step)] = outcome.text;
2306
2849
  if (outcome.outcome === "success") finalText = outcome.text;
2307
2850
  }
2308
2851
  const trace = {
@@ -2321,15 +2864,947 @@ async function runHybridStaged(plan, cfg) {
2321
2864
  steps,
2322
2865
  denials
2323
2866
  };
2324
- writeFileSync3(join3(cfg.outDir, "result.txt"), finalText, "utf8");
2325
- 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");
2326
2869
  return { finalText, trace, htmlPath: null, denials, sessionId: null, renderDegraded: null };
2327
2870
  }
2328
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
+
2329
3799
  // src/dispatch.ts
2330
- import { dirname, isAbsolute, resolve } from "path";
3800
+ import { dirname as dirname2, isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
2331
3801
  var UNAVAILABLE_COMPONENT_REASON = "component is unavailable on the configured runtime";
2332
- 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") {
2333
3808
  const cfg = {
2334
3809
  target,
2335
3810
  flavor: input.flavor ?? DEFAULT_RENDER_FLAVOR,
@@ -2338,60 +3813,224 @@ function buildPreparedComponent(node, report, input, target, models) {
2338
3813
  cwd: resolveProjectCwd(node, { ...input.project !== void 0 ? { project: input.project } : {}, ...input.irPath !== void 0 ? { irPath: input.irPath } : {} }),
2339
3814
  ...input.maxTurns !== void 0 ? { maxTurns: input.maxTurns } : {}
2340
3815
  };
2341
- 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
+ );
2342
3841
  }
2343
3842
  function resolveProjectCwd(node, opts) {
2344
- if (opts.project) return resolve(opts.project);
3843
+ if (opts.project) return resolve2(opts.project);
2345
3844
  const p = node.context_binding.project;
2346
- if (isAbsolute(p)) return p;
2347
- const baseDir = opts.irPath ? dirname(resolve(opts.irPath)) : process.cwd();
2348
- 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);
2349
3848
  }
2350
3849
  function prepareDispatch(input) {
2351
- const ir = typeof input.ir === "string" ? parseIr(input.ir) : input.ir;
3850
+ const ir = parseIrInput(input.ir);
2352
3851
  assertSupportedIrVersion(ir.warble_ir_version);
2353
3852
  const target = input.target ?? DEFAULT_TARGET;
2354
3853
  const models = input.models ?? ModelConfig.default();
2355
- models.validate(ir);
2356
- let scoped = ir.components;
2357
- if (input.componentId !== void 0) {
2358
- const node = ir.components.find((candidate) => candidate.id === input.componentId);
2359
- if (!node) {
2360
- throw new DispatchError(
2361
- `component '${input.componentId}' not found in IR (available: ${ir.components.map((c) => c.id).join(", ")})`
2362
- );
2363
- }
2364
- scoped = [node];
3854
+ const closure = resolveComponentClosure(
3855
+ ir,
3856
+ input.componentId === void 0 ? void 0 : [input.componentId]
3857
+ );
3858
+ models.validate({ ...ir, components: [...closure.components] });
3859
+ const supply = input.slots ?? {};
3860
+ const resolvedNodes = /* @__PURE__ */ new Map();
3861
+ const reports = /* @__PURE__ */ new Map();
3862
+ for (const node of closure.components) {
3863
+ const resolved = resolveSlotsForNode(node, ir, supply);
3864
+ const withSlots = resolved === null ? node : applyNodeSlots(node, resolved);
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
+ );
2365
3876
  }
2366
- const components = scoped.map((node) => {
2367
- const report = resolveNodeCapabilities(node, target);
2368
- return buildPreparedComponent(node, report, input, target, models);
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
2369
3905
  });
2370
- return { target, components };
3906
+ }
3907
+ function resolveSlotsForNode(node, ir, supply, unanswered = "fail") {
3908
+ const decls = [...ir.slots ?? [], ...node.slots ?? []];
3909
+ if (decls.length === 0) return null;
3910
+ return resolveSlots(decls, supply, `component '${node.id}'`, unanswered);
3911
+ }
3912
+ function applyNodeSlots(node, resolved) {
3913
+ const scope = `component '${node.id}'`;
3914
+ return {
3915
+ ...node,
3916
+ ...node.brief === void 0 ? {} : { brief: applySlots(node.brief, resolved, scope) },
3917
+ prompt_fragment: applySlots(node.prompt_fragment, resolved, scope),
3918
+ llm_calls: node.llm_calls.map((call) => ({
3919
+ ...call,
3920
+ prompt: applySlots(call.prompt, resolved, scope)
3921
+ }))
3922
+ };
2371
3923
  }
2372
3924
  function prepareDisplayManifest(input) {
2373
- const ir = typeof input.ir === "string" ? parseIr(input.ir) : input.ir;
3925
+ const ir = parseIrInput(input.ir);
2374
3926
  assertSupportedIrVersion(ir.warble_ir_version);
3927
+ assertComponentCompositionFields(ir);
2375
3928
  const target = input.target ?? DEFAULT_TARGET;
2376
3929
  const models = input.models ?? ModelConfig.default();
2377
- 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));
3933
+ const supply = input.slots ?? {};
2378
3934
  const components = ir.components.map((node) => {
2379
- const report = inspectNodeCapabilities(node, target);
2380
- if (report.some((entry2) => entry2.outcome === "fail")) {
2381
- return { id: node.id, node, availability: { status: "unavailable", reason: UNAVAILABLE_COMPONENT_REASON } };
3935
+ const resolved = resolveSlotsForNode(node, ir, supply, "default");
3936
+ const withSlots = resolved === null ? node : applyNodeSlots(node, resolved);
3937
+ const report = inspectNodeCapabilities(withSlots, target);
3938
+ let modelReady = true;
3939
+ try {
3940
+ models.validate({ ...ir, components: [withSlots] });
3941
+ } catch {
3942
+ modelReady = false;
2382
3943
  }
2383
- return buildPreparedComponent(node, report, input, target, models);
3944
+ if (!modelReady || report.some((entry2) => entry2.outcome === "fail")) {
3945
+ return {
3946
+ id: withSlots.id,
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,
3961
+ availability: { status: "unavailable", reason: UNAVAILABLE_COMPONENT_REASON }
3962
+ };
3963
+ }
3964
+ return { id: prepared.id, node: prepared.node, report: prepared.report };
2384
3965
  });
2385
- 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
+ }
2386
3993
  }
2387
3994
  async function dispatch(input, runCfg) {
2388
3995
  const prepared = prepareDispatch(input);
3996
+ if (prepared.dependencies.length > 0) {
3997
+ assertSupportedComponentInvocationLimits(runCfg.componentLimits);
3998
+ }
3999
+ preflightDispatchAssets(input, prepared);
2389
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
+ }
2390
4015
  const components = [];
2391
4016
  for (const c of prepared.components) {
2392
- 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,
2393
4022
  outDir: runCfg.outDir,
2394
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, {
4031
+ outDir: runCfg.outDir,
4032
+ warbleBin,
4033
+ ...input.irPath ? { assets: { ir: { ...parseIrInput(input.ir), components: [c.node] }, irPath: input.irPath } } : {},
2395
4034
  ...runCfg.title ? { title: runCfg.title } : {}
2396
4035
  });
2397
4036
  components.push({ id: c.id, report: c.report, plan: c.plan, result });
@@ -2400,9 +4039,9 @@ async function dispatch(input, runCfg) {
2400
4039
  }
2401
4040
 
2402
4041
  // src/manifest.ts
2403
- var MANIFEST_VERSION = "0.1";
2404
- var MIN_SUPPORTED_IR_VERSION = "0.7";
2405
- 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";
2406
4045
  var DEFAULT_MAX_ATTEMPTS = 1;
2407
4046
  function classifyStep(node, stepIndex) {
2408
4047
  const when = node.llm_calls[stepIndex].when;
@@ -2537,10 +4176,11 @@ function buildTools2(node) {
2537
4176
  }
2538
4177
  return out;
2539
4178
  }
2540
- function buildAgentManifest(component) {
4179
+ function buildAgentManifest(component, dependencies = []) {
2541
4180
  const node = component.node;
2542
4181
  return {
2543
4182
  id: node.id,
4183
+ entrypoint: node.entrypoint,
2544
4184
  verb: node.verb,
2545
4185
  component_type: node.type,
2546
4186
  realization_kind: node.realization_kind,
@@ -2551,13 +4191,15 @@ function buildAgentManifest(component) {
2551
4191
  tools: buildTools2(node),
2552
4192
  output_schema: outputSchemaFor(node.effect),
2553
4193
  capabilities: component.report,
4194
+ dependencies: dependencies.filter((dependency) => dependency.caller === node.id).map(({ step, alias, component: callee }) => ({ step, alias, component: callee })),
2554
4195
  ...node.brief !== void 0 ? { brief: node.brief } : {}
2555
4196
  };
2556
4197
  }
2557
- function buildUnavailableAgentManifest(component) {
4198
+ function buildUnavailableAgentManifest(component, dependencies = []) {
2558
4199
  const node = component.node;
2559
4200
  return {
2560
4201
  id: node.id,
4202
+ entrypoint: node.entrypoint,
2561
4203
  verb: node.verb,
2562
4204
  component_type: node.type,
2563
4205
  realization_kind: node.realization_kind,
@@ -2568,23 +4210,48 @@ function buildUnavailableAgentManifest(component) {
2568
4210
  tools: [],
2569
4211
  output_schema: {},
2570
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 })),
2571
4215
  availability: component.availability
2572
4216
  };
2573
4217
  }
2574
4218
  function buildManifest(prepared, raw) {
2575
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
+ });
2576
4242
  return {
2577
4243
  manifest_version: MANIFEST_VERSION,
2578
4244
  compat: { min_ir_version: MIN_SUPPORTED_IR_VERSION, max_ir_version: MAX_SUPPORTED_IR_VERSION },
2579
4245
  profile: ir.profile,
2580
4246
  target: prepared.target,
2581
- 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))
2582
4249
  };
2583
4250
  }
2584
4251
 
2585
4252
  // src/model_catalog.ts
2586
- import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
2587
- import { resolve as resolve2 } from "path";
4253
+ import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
4254
+ import { resolve as resolve3 } from "path";
2588
4255
  var MODEL_CATALOG_VERSION = 1;
2589
4256
  var CLEANUP_GRACE_MS = 25;
2590
4257
  function unavailable(code, retryable) {
@@ -2634,8 +4301,8 @@ async function settleCleanup(operation) {
2634
4301
  try {
2635
4302
  await Promise.race([
2636
4303
  operation.catch(() => void 0),
2637
- new Promise((resolve3) => {
2638
- timer = setTimeout(resolve3, CLEANUP_GRACE_MS);
4304
+ new Promise((resolve4) => {
4305
+ timer = setTimeout(resolve4, CLEANUP_GRACE_MS);
2639
4306
  })
2640
4307
  ]);
2641
4308
  } finally {
@@ -2648,11 +4315,11 @@ async function discoverClaudeModels(options = {}) {
2648
4315
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return unavailable("protocol_error", false);
2649
4316
  let catalogQuery;
2650
4317
  try {
2651
- const queryFactory = options.queryFactory ?? query3;
4318
+ const queryFactory = options.queryFactory ?? query4;
2652
4319
  catalogQuery = queryFactory({
2653
4320
  prompt: idleInput(),
2654
4321
  options: {
2655
- cwd: resolve2(options.cwd ?? process.cwd()),
4322
+ cwd: resolve3(options.cwd ?? process.cwd()),
2656
4323
  tools: [],
2657
4324
  mcpServers: [],
2658
4325
  settingSources: [],
@@ -2681,8 +4348,8 @@ async function discoverClaudeModels(options = {}) {
2681
4348
  }
2682
4349
 
2683
4350
  // src/session.ts
2684
- import { mkdirSync as mkdirSync3 } from "fs";
2685
- import { join as join4 } from "path";
4351
+ import { mkdirSync as mkdirSync5 } from "fs";
4352
+ import { join as join6 } from "path";
2686
4353
  var BREAKDOWN_RE = /\bby\s+([a-z][a-z0-9_]*(?:\s*(?:,|and)\s*[a-z][a-z0-9_]*)*)/i;
2687
4354
  var FILTER_OVERRIDE_RE = /\b(where|only|instead of|excluding|filtered?\s+to)\b/i;
2688
4355
  function extractBreakdownDimensions(question) {
@@ -2758,8 +4425,8 @@ var ChatSession = class {
2758
4425
  const prompt = buildTurnPrompt(this.state, question);
2759
4426
  const resume = lastSessionId(this.state) ?? this.initialResumeSessionId ?? null;
2760
4427
  const turnPlan = { ...this.plan, prompt };
2761
- const turnOutDir = join4(this.runCfg.outDir, `turn-${this.state.turns.length + 1}`);
2762
- mkdirSync3(turnOutDir, { recursive: true });
4428
+ const turnOutDir = join6(this.runCfg.outDir, `turn-${this.state.turns.length + 1}`);
4429
+ mkdirSync5(turnOutDir, { recursive: true });
2763
4430
  const result = await runDispatch(turnPlan, {
2764
4431
  ...this.runCfg,
2765
4432
  outDir: turnOutDir,
@@ -2783,6 +4450,8 @@ function createChatSession(plan, runCfg, initialResumeSessionId) {
2783
4450
  export {
2784
4451
  DispatchError,
2785
4452
  emitAgentModule,
4453
+ componentDependencies,
4454
+ resolveComponentClosure,
2786
4455
  REALIZATION_KINDS,
2787
4456
  COMPONENT_TYPES,
2788
4457
  TRIGGER_KINDS,
@@ -2790,6 +4459,10 @@ export {
2790
4459
  SUPPORTED_IR_VERSIONS,
2791
4460
  parseIr,
2792
4461
  distinctTiers,
4462
+ resolveSlots,
4463
+ applySlots,
4464
+ assertNoSlotReferences,
4465
+ parseSlotFlags,
2793
4466
  ModelConfig,
2794
4467
  resolveStagedSteps,
2795
4468
  distinctProviders,
@@ -2810,6 +4483,15 @@ export {
2810
4483
  resolveCapabilities,
2811
4484
  resolveNodeCapabilities,
2812
4485
  inspectNodeCapabilities,
4486
+ assetDirForIr,
4487
+ validateAssets,
4488
+ landAssets,
4489
+ fingerprintSurfaces,
4490
+ promptSurfacesOf,
4491
+ promptSurfaces,
4492
+ fingerprintPrompts,
4493
+ composeCanUseTool,
4494
+ composeHooks,
2813
4495
  makeReadOnlyGuard,
2814
4496
  buildChatRequest,
2815
4497
  extractCompletionText,
@@ -2820,10 +4502,20 @@ export {
2820
4502
  aggregateTrace,
2821
4503
  DispatchSessionError,
2822
4504
  runDispatch,
4505
+ DEFAULT_COMPONENT_INVOCATION_LIMITS,
4506
+ ComponentStepExecutionError,
4507
+ normalizeComponentRequest,
4508
+ normalizeComponentResult,
4509
+ assertEligibleComponentCallee,
4510
+ assertEligibleComponentCaller,
4511
+ ComponentInvocationRuntime,
4512
+ createSdkComponentStepRunner,
4513
+ runComposedDispatch,
2823
4514
  UNAVAILABLE_COMPONENT_REASON,
2824
4515
  resolveProjectCwd,
2825
4516
  prepareDispatch,
2826
4517
  prepareDisplayManifest,
4518
+ preflightDispatchAssets,
2827
4519
  dispatch,
2828
4520
  buildAgentManifest,
2829
4521
  buildUnavailableAgentManifest,
@@ -2841,4 +4533,4 @@ export {
2841
4533
  ChatSession,
2842
4534
  createChatSession
2843
4535
  };
2844
- //# sourceMappingURL=chunk-3ON7TSGW.js.map
4536
+ //# sourceMappingURL=chunk-2VDMVARW.js.map