@warble/claude-agent-sdk 0.11.0 → 0.12.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.
@@ -519,7 +519,33 @@ function parseComponent(value, at) {
519
519
  context_precondition: preconditionArray(obj, "context_precondition", at),
520
520
  params: paramArray(obj, "params", at),
521
521
  eval: obj["eval"] === void 0 || obj["eval"] === null ? null : parseEvalSpec(obj["eval"], `${at}.eval`),
522
- brief: optStringU(obj, "brief")
522
+ brief: optStringU(obj, "brief"),
523
+ ...slotsField(obj, at)
524
+ };
525
+ }
526
+ function slotsField(obj, at) {
527
+ if (obj["slots"] === void 0) return {};
528
+ const slots = requireArray(obj, "slots", at).map((v, i) => parseSlot(v, `${at}.slots[${i}]`));
529
+ return { slots };
530
+ }
531
+ function parseSlot(value, at) {
532
+ const obj = requireObject(value, at);
533
+ const variantsRaw = requireObject(obj["variants"], `${at}.variants`);
534
+ const variants = {};
535
+ for (const [key, text] of Object.entries(variantsRaw)) {
536
+ if (typeof text !== "string") fail(`${at}.variants.${key} must be a string`);
537
+ variants[key] = text;
538
+ }
539
+ const name = requireString(obj, "name", at);
540
+ const dflt = requireString(obj, "default", at);
541
+ if (!(dflt in variants)) {
542
+ fail(`${at}.default is '${dflt}', which is not one of its variants (${Object.keys(variants).join(", ")})`);
543
+ }
544
+ return {
545
+ name,
546
+ default: dflt,
547
+ variants,
548
+ ...obj["present_when"] === void 0 ? {} : { present_when: obj["present_when"] }
523
549
  };
524
550
  }
525
551
  function parseIr(json2) {
@@ -544,7 +570,8 @@ function parseIr(json2) {
544
570
  config,
545
571
  components: requireArray(obj, "components", "<root>").map(
546
572
  (c, i) => parseComponent(c, `components[${i}]`)
547
- )
573
+ ),
574
+ ...slotsField(obj, "<root>")
548
575
  };
549
576
  }
550
577
  function distinctTiers(calls) {
@@ -559,6 +586,124 @@ function distinctTiers(calls) {
559
586
  return out;
560
587
  }
561
588
 
589
+ // src/slots.ts
590
+ var SLOT_NAME = /^[a-z_][a-z0-9_]*$/;
591
+ function slotReferences(raw) {
592
+ const found = [];
593
+ let cursor = 0;
594
+ for (; ; ) {
595
+ const open = raw.indexOf("{{", cursor);
596
+ if (open === -1) return found;
597
+ const bodyStart = open + 2;
598
+ const close = raw.indexOf("}}", bodyStart);
599
+ if (close === -1) return found;
600
+ const body = raw.slice(bodyStart, close).trim();
601
+ if (body.startsWith("slot.")) {
602
+ const name = body.slice("slot.".length);
603
+ if (SLOT_NAME.test(name)) found.push({ start: open, end: close + 2, name });
604
+ }
605
+ cursor = close + 2;
606
+ }
607
+ }
608
+ function resolveSlots(decls, supply, scope, unanswered = "fail") {
609
+ const byName = new Map(decls.map((d) => [d.name, d]));
610
+ const resolved = /* @__PURE__ */ new Map();
611
+ const visiting = /* @__PURE__ */ new Set();
612
+ const resolve3 = (name) => {
613
+ if (resolved.has(name)) return resolved.get(name);
614
+ if (visiting.has(name)) {
615
+ throw new DispatchError(
616
+ `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(" -> ")}).`
617
+ );
618
+ }
619
+ const decl = byName.get(name);
620
+ const choice = supply[name];
621
+ if (choice === null) {
622
+ resolved.set(name, null);
623
+ return null;
624
+ }
625
+ if (choice === void 0) {
626
+ if (decl.present_when !== void 0 && unanswered === "fail") {
627
+ throw new DispatchError(
628
+ `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.`
629
+ );
630
+ }
631
+ }
632
+ const key = choice ?? decl.default;
633
+ const text = decl.variants[key];
634
+ if (text === void 0) {
635
+ throw new DispatchError(
636
+ `slot '${name}' in ${scope} was given variant '${key}', which it does not declare (declared: ${Object.keys(decl.variants).join(", ")}).`
637
+ );
638
+ }
639
+ visiting.add(name);
640
+ const expanded = substitute(text, resolve3, byName, scope);
641
+ visiting.delete(name);
642
+ resolved.set(name, expanded);
643
+ return expanded;
644
+ };
645
+ for (const decl of decls) resolve3(decl.name);
646
+ return resolved;
647
+ }
648
+ function substitute(raw, resolve3, byName, scope) {
649
+ const refs = slotReferences(raw);
650
+ if (refs.length === 0) return raw;
651
+ let out = "";
652
+ let cursor = 0;
653
+ for (const ref of refs) {
654
+ if (!byName.has(ref.name)) {
655
+ throw new DispatchError(
656
+ `prompt text in ${scope} references slot '${ref.name}', which is neither one of the component's slots nor one of the profile's.`
657
+ );
658
+ }
659
+ out += raw.slice(cursor, ref.start) + (resolve3(ref.name) ?? "");
660
+ cursor = ref.end;
661
+ }
662
+ return out + raw.slice(cursor);
663
+ }
664
+ function applySlots(raw, resolved, scope) {
665
+ const refs = slotReferences(raw);
666
+ if (refs.length === 0) return raw;
667
+ let out = "";
668
+ let cursor = 0;
669
+ for (const ref of refs) {
670
+ if (!resolved.has(ref.name)) {
671
+ throw new DispatchError(
672
+ `prompt text in ${scope} references slot '${ref.name}', which is not declared there.`
673
+ );
674
+ }
675
+ out += raw.slice(cursor, ref.start) + (resolved.get(ref.name) ?? "");
676
+ cursor = ref.end;
677
+ }
678
+ return out + raw.slice(cursor);
679
+ }
680
+ function assertNoSlotReferences(raw, owner) {
681
+ const refs = slotReferences(raw);
682
+ if (refs.length > 0) {
683
+ const names = [...new Set(refs.map((r) => r.name))].join(", ");
684
+ throw new DispatchError(
685
+ `${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.`
686
+ );
687
+ }
688
+ }
689
+ function parseSlotFlags(flags) {
690
+ const supply = {};
691
+ for (const flag of flags) {
692
+ const at = flag.indexOf("=");
693
+ if (at === -1) {
694
+ throw new DispatchError(
695
+ `--slot '${flag}' must be NAME=VARIANT, or NAME= to remove a conditional slot`
696
+ );
697
+ }
698
+ const name = flag.slice(0, at);
699
+ const variant = flag.slice(at + 1);
700
+ if (name === "") throw new DispatchError(`--slot '${flag}' has an empty slot name`);
701
+ if (name in supply) throw new DispatchError(`--slot ${name} was given more than once`);
702
+ supply[name] = variant === "" ? null : variant;
703
+ }
704
+ return supply;
705
+ }
706
+
562
707
  // src/models.ts
563
708
  import { parse as parseYaml } from "yaml";
564
709
  var STRONG_TIER = "strong";
@@ -766,14 +911,14 @@ function planProviderRouting(node, models, anthropicSplit) {
766
911
  }
767
912
  return { mode, steps, providers };
768
913
  }
769
- function buildStepMessages(step, question, slots) {
914
+ function buildStepMessages(step, question, artifacts) {
770
915
  const parts = [`Question: ${question}`];
771
- for (const slot of step.consumes) {
772
- const value = slots[slot];
916
+ for (const artifact of step.consumes) {
917
+ const value = artifacts[artifact];
773
918
  parts.push(
774
919
  value === void 0 ? `
775
- [input '${slot}' was not produced by an earlier step]` : `
776
- Input '${slot}':
920
+ [input '${artifact}' was not produced by an earlier step]` : `
921
+ Input '${artifact}':
777
922
  ${value}`
778
923
  );
779
924
  }
@@ -1193,9 +1338,9 @@ function buildDriverBody(node) {
1193
1338
  `Run the \`${subagentName(node.verb, call.name)}\` subagent (step \`${call.name}\`) via the Task tool.`
1194
1339
  ];
1195
1340
  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}\``;
1341
+ const sources = call.consumes.map((artifact) => {
1342
+ const producer = producers.get(artifact);
1343
+ return producer ? `\`${artifact}\` (the \`${producer}\` subagent's output)` : `\`${artifact}\``;
1199
1344
  }).join(", ");
1200
1345
  parts.push(`Pass it ${sources} as input.`);
1201
1346
  }
@@ -1239,6 +1384,23 @@ function tierCollapseNote(node, model) {
1239
1384
  return `per-step tiers [${steps}] collapsed to single model '${model}'`;
1240
1385
  }
1241
1386
  function buildDispatchPlan(node, report, cfg) {
1387
+ const plan = buildDispatchPlanUnchecked(node, report, cfg);
1388
+ assertPlanHasNoSlotReferences(plan, node.id);
1389
+ return plan;
1390
+ }
1391
+ function assertPlanHasNoSlotReferences(plan, componentId) {
1392
+ const system = plan.options.systemPrompt;
1393
+ if (typeof system === "string") {
1394
+ assertNoSlotReferences(system, `the system prompt of component '${componentId}'`);
1395
+ }
1396
+ for (const [name, agent] of Object.entries(plan.options.agents ?? {})) {
1397
+ assertNoSlotReferences(agent.prompt, `subagent '${name}' of component '${componentId}'`);
1398
+ }
1399
+ for (const step of plan.meta.stagedSteps) {
1400
+ assertNoSlotReferences(step.prompt, `staged step '${step.name}' of component '${componentId}'`);
1401
+ }
1402
+ }
1403
+ function buildDispatchPlanUnchecked(node, report, cfg) {
1242
1404
  if (!realizationSupported(node)) {
1243
1405
  throw unsupported("realization_kind", node.realization_kind);
1244
1406
  }
@@ -1524,11 +1686,59 @@ function inspectNodeCapabilities(node, targetId) {
1524
1686
  return inspectCapabilities(node, targetId, localProfile());
1525
1687
  }
1526
1688
 
1689
+ // src/fingerprint.ts
1690
+ import { createHash } from "crypto";
1691
+ var ALGORITHM = "sha256";
1692
+ function sha256(text) {
1693
+ return createHash(ALGORITHM).update(text, "utf8").digest("hex");
1694
+ }
1695
+ function fingerprintSurfaces(surfaces) {
1696
+ const perSurface = {};
1697
+ const parts = [];
1698
+ for (const name of Object.keys(surfaces).sort()) {
1699
+ const text = surfaces[name];
1700
+ perSurface[name] = sha256(text);
1701
+ parts.push(`${name.length}:${name} ${text.length}:${text}`);
1702
+ }
1703
+ return { algorithm: ALGORITHM, digest: sha256(parts.join(" ")), surfaces: perSurface };
1704
+ }
1705
+ function promptSurfacesOf(options) {
1706
+ const surfaces = {};
1707
+ const system = options.systemPrompt;
1708
+ if (typeof system === "string") surfaces["driver.systemPrompt"] = system;
1709
+ for (const [name, agent] of Object.entries(options.agents ?? {})) {
1710
+ surfaces[`subagent.${name}`] = agent.prompt;
1711
+ }
1712
+ return surfaces;
1713
+ }
1714
+ function promptSurfacesOfMessages(messages) {
1715
+ return Object.fromEntries(
1716
+ messages.map((message, index) => [`local.${index}.${message.role}`, message.content])
1717
+ );
1718
+ }
1719
+ function promptSurfaces(plan) {
1720
+ return promptSurfacesOf(plan.options);
1721
+ }
1722
+ function fingerprintPrompts(plan) {
1723
+ return fingerprintSurfaces(promptSurfaces(plan));
1724
+ }
1725
+
1527
1726
  // src/guardrails.ts
1528
1727
  import { resolve as resolvePath, sep as pathSep } from "path";
1529
1728
  function withinScope(abs, scopeAbs) {
1530
1729
  return abs === scopeAbs || abs.startsWith(scopeAbs.endsWith(pathSep) ? scopeAbs : scopeAbs + pathSep);
1531
1730
  }
1731
+ function composeCanUseTool(embedder, floor) {
1732
+ if (embedder === void 0) return floor;
1733
+ return async (toolName, input, options) => {
1734
+ const fromEmbedder = await embedder(toolName, input, options);
1735
+ if (fromEmbedder.behavior !== "allow") return fromEmbedder;
1736
+ return floor(toolName, fromEmbedder.updatedInput, options);
1737
+ };
1738
+ }
1739
+ function composeHooks(embedder, guardHooks) {
1740
+ return { ...embedder, PreToolUse: [...embedder?.PreToolUse ?? [], ...guardHooks] };
1741
+ }
1532
1742
  var DESTRUCTIVE = /\b(rm|sudo|dd|mkfs|shutdown|reboot|kill|chmod|chown|mv|cp)\b/;
1533
1743
  var REDIRECTION = /(^|[^>])>>?[^>]/;
1534
1744
  var DOTENV_READER_COMMANDS = /\b(cat|head|tail|less|more|od|xxd|strings|grep|awk|sed)\b/;
@@ -1728,9 +1938,9 @@ function buildToolDriverPrompt(steps) {
1728
1938
  const lines = steps.map((s, i) => {
1729
1939
  const parts = [`${i + 1}. Call the \`dispatch_step\` tool with step="${s.name}".`];
1730
1940
  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}"`;
1941
+ const srcs = s.consumes.map((artifact) => {
1942
+ const p = producers.get(artifact);
1943
+ return p ? `the text step "${p}" returned` : `"${artifact}"`;
1734
1944
  }).join(", ");
1735
1945
  parts.push(`Pass ${srcs} as the tool's \`inputs\` argument.`);
1736
1946
  }
@@ -1748,6 +1958,9 @@ function buildToolDriverPrompt(steps) {
1748
1958
  "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
1959
  ].join("\n");
1750
1960
  }
1961
+ var MCP_SERVER_NAME = "warble";
1962
+ var DISPATCH_STEP_NAME = "dispatch_step";
1963
+ var DISPATCH_STEP_TOOL = `mcp__${MCP_SERVER_NAME}__${DISPATCH_STEP_NAME}`;
1751
1964
  async function runCloudStep(step, question, inputsText, ctx) {
1752
1965
  const options = {
1753
1966
  cwd: ctx.cwd,
@@ -1763,9 +1976,10 @@ ${step.prompt}`,
1763
1976
  canUseTool: ctx.canUseTool,
1764
1977
  // Read never reaches `canUseTool` for an in-cwd path in the real SDK (see guardrails.ts); this
1765
1978
  // hook is the live enforcement point for the +Setup dotenv-read gap's Read side.
1766
- hooks: { PreToolUse: ctx.hooks },
1979
+ hooks: ctx.hooks,
1767
1980
  env: ctx.env
1768
1981
  };
1982
+ ctx.cfg.onPromptFingerprint?.(fingerprintSurfaces(promptSurfacesOf(options)));
1769
1983
  const msgs = [];
1770
1984
  for await (const m of query({ prompt: stepUserPrompt(question, inputsText), options })) msgs.push(m);
1771
1985
  return requireFinalText(msgs.find(isResult));
@@ -1782,12 +1996,15 @@ async function runHybridTool(plan, cfg) {
1782
1996
  const venvBin = join(cwd, ".venv", "bin");
1783
1997
  const pathEnv = existsSync(venvBin) ? `${venvBin}:${process.env.PATH ?? ""}` : process.env.PATH ?? "";
1784
1998
  const env = { ...process.env, PATH: pathEnv };
1999
+ const stepCanUseTool = composeCanUseTool(plan.options.canUseTool, canUseTool);
2000
+ const stepHooks = composeHooks(plan.options.hooks, hooks);
2001
+ const driverCanUseTool = async (toolName, input, options) => toolName === DISPATCH_STEP_TOOL ? { behavior: "allow", updatedInput: input } : stepCanUseTool(toolName, input, options);
1785
2002
  const steps = plan.meta.stagedSteps;
1786
2003
  const question = plan.prompt;
1787
2004
  const maxTurns = plan.options.maxTurns ?? 40;
1788
2005
  const traceSteps = [];
1789
2006
  const dispatchStep = tool(
1790
- "dispatch_step",
2007
+ DISPATCH_STEP_NAME,
1791
2008
  "Execute one named step of the task on its own configured model and return its text output.",
1792
2009
  { step: z.string(), inputs: z.string().optional() },
1793
2010
  async (args) => {
@@ -1799,18 +2016,20 @@ async function runHybridTool(plan, cfg) {
1799
2016
  let text;
1800
2017
  if (step.provider === "openai_compat") {
1801
2018
  if (!step.endpoint) throw new DispatchError(`local step '${step.name}' has no endpoint`);
2019
+ const localMessages = [
2020
+ { role: "system", content: step.prompt },
2021
+ { role: "user", content: stepUserPrompt(question, inputsText) }
2022
+ ];
2023
+ cfg.onPromptFingerprint?.(fingerprintSurfaces(promptSurfacesOfMessages(localMessages)));
1802
2024
  text = await callOpenAiCompat({
1803
2025
  endpoint: step.endpoint,
1804
2026
  model: step.model,
1805
- messages: [
1806
- { role: "system", content: step.prompt },
1807
- { role: "user", content: stepUserPrompt(question, inputsText) }
1808
- ]
2027
+ messages: localMessages
1809
2028
  });
1810
2029
  process.stderr.write(`warble hybrid-tool: step '${step.name}' \u2192 local ${step.model}
1811
2030
  `);
1812
2031
  } else {
1813
- text = await runCloudStep(step, question, inputsText, { cwd, env, maxTurns, canUseTool, hooks });
2032
+ text = await runCloudStep(step, question, inputsText, { cfg, cwd, env, maxTurns, canUseTool: stepCanUseTool, hooks: stepHooks });
1814
2033
  process.stderr.write(`warble hybrid-tool: step '${step.name}' \u2192 cloud ${step.model}
1815
2034
  `);
1816
2035
  }
@@ -1818,7 +2037,7 @@ async function runHybridTool(plan, cfg) {
1818
2037
  return { content: [{ type: "text", text }] };
1819
2038
  }
1820
2039
  );
1821
- const server = createSdkMcpServer({ name: "warble", version: "0.0.0", tools: [dispatchStep] });
2040
+ const server = createSdkMcpServer({ name: MCP_SERVER_NAME, version: "0.0.0", tools: [dispatchStep] });
1822
2041
  const driverModel = plan.options.model ?? "sonnet";
1823
2042
  const driverOptions = {
1824
2043
  cwd,
@@ -1827,9 +2046,22 @@ async function runHybridTool(plan, cfg) {
1827
2046
  model: driverModel,
1828
2047
  systemPrompt: buildToolDriverPrompt(steps),
1829
2048
  mcpServers: { warble: server },
1830
- allowedTools: ["mcp__warble__dispatch_step"],
1831
- env
2049
+ allowedTools: [DISPATCH_STEP_TOOL],
2050
+ env,
2051
+ // The floor is composed in here too, even though the driver prompt asks this turn to do nothing
2052
+ // but call `dispatch_step`. `allowedTools` does not restrict the toolset — the SDK documents it
2053
+ // as "auto-allowed without prompting" and says to use `tools` to restrict — and `tools` is not
2054
+ // set here, so the default built-in set (Bash, Write, Edit, …) is on the table for this turn.
2055
+ // Composing the floor is therefore the difference between the prompt asking the model not to
2056
+ // reach for them and something actually stopping it.
2057
+ //
2058
+ // This tightens behaviour rather than preserving it: before, no `canUseTool` reached this turn
2059
+ // at all. That is the intended direction — a guardrail floor applying where it previously did
2060
+ // not — and it is why this is not spread conditionally like an embedder-only passthrough.
2061
+ canUseTool: driverCanUseTool,
2062
+ hooks: stepHooks
1832
2063
  };
2064
+ cfg.onPromptFingerprint?.(fingerprintSurfaces(promptSurfacesOf(driverOptions)));
1833
2065
  const msgs = [];
1834
2066
  for await (const m of query({ prompt: question, options: driverOptions })) msgs.push(m);
1835
2067
  const result = msgs.find(isResult);
@@ -1905,10 +2137,10 @@ function tryParseJson(text) {
1905
2137
  return void 0;
1906
2138
  }
1907
2139
  }
1908
- function readFlag(slots, target) {
1909
- const [slotName, ...path] = target.split(".");
1910
- if (slotName === void 0) return false;
1911
- const raw = slots[slotName];
2140
+ function readFlag(artifacts, target) {
2141
+ const [artifactName, ...path] = target.split(".");
2142
+ if (artifactName === void 0) return false;
2143
+ const raw = artifacts[artifactName];
1912
2144
  if (raw === void 0) return false;
1913
2145
  let cur = tryParseJson(raw);
1914
2146
  for (const key of path) {
@@ -1922,9 +2154,9 @@ function evaluateGuard(when, state) {
1922
2154
  case "on_failure":
1923
2155
  return state.outcomes[when.target] === "failure";
1924
2156
  case "on_flag":
1925
- return readFlag(state.slots, when.target);
2157
+ return readFlag(state.artifacts, when.target);
1926
2158
  case "on_missing":
1927
- return state.slots[when.target] === void 0;
2159
+ return state.artifacts[when.target] === void 0;
1928
2160
  default:
1929
2161
  throw new DispatchError(
1930
2162
  `unknown guard '${when.guard}' (closed vocabulary: on_failure, on_flag, on_missing)`
@@ -2124,13 +2356,14 @@ async function runDispatch(plan, cfg) {
2124
2356
  const env = { ...process.env, PATH: pathEnv };
2125
2357
  const options = {
2126
2358
  ...plan.options,
2127
- canUseTool,
2359
+ canUseTool: composeCanUseTool(plan.options.canUseTool, canUseTool),
2128
2360
  // Read never reaches `canUseTool` for an in-cwd path in the real SDK (see guardrails.ts); this
2129
2361
  // 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] },
2362
+ hooks: composeHooks(plan.options.hooks, hooks),
2131
2363
  env,
2132
2364
  ...cfg.resume ? { resume: cfg.resume } : {}
2133
2365
  };
2366
+ reportPromptFingerprint(cfg, options);
2134
2367
  const mapper = new ChatEventMapper(plan.meta.verb);
2135
2368
  const messages = [];
2136
2369
  for await (const message of query2({ prompt: plan.prompt, options })) {
@@ -2170,15 +2403,19 @@ function hybridCloudPreamble(cwd) {
2170
2403
  "All data access MUST go through the `wren` CLI \u2014 never raw SQL clients."
2171
2404
  ].join("\n");
2172
2405
  }
2173
- function slotKey(step) {
2406
+ function reportPromptFingerprint(cfg, options) {
2407
+ cfg.onPromptFingerprint?.(fingerprintSurfaces(promptSurfacesOf(options)));
2408
+ }
2409
+ function artifactKey(step) {
2174
2410
  return step.produces ?? step.name;
2175
2411
  }
2176
- async function executeStep(step, slots, ctx, tolerant) {
2177
- const messages = buildStepMessages(step, ctx.plan.prompt, slots);
2412
+ async function executeStep(step, artifacts, ctx, tolerant) {
2413
+ const messages = buildStepMessages(step, ctx.plan.prompt, artifacts);
2178
2414
  const userPrompt = messages.find((m) => m.role === "user")?.content ?? ctx.plan.prompt;
2179
2415
  try {
2180
2416
  if (step.provider === "openai_compat") {
2181
2417
  if (!step.endpoint) throw new DispatchError(`local step '${step.name}' has no endpoint`);
2418
+ ctx.cfg.onPromptFingerprint?.(fingerprintSurfaces(promptSurfacesOfMessages(messages)));
2182
2419
  const text2 = await callOpenAiCompat({ endpoint: step.endpoint, model: step.model, messages });
2183
2420
  ctx.steps.push({ model: `openai_compat:${step.model}`, parent_tool_use_id: step.name, usage: null });
2184
2421
  process.stderr.write(`warble hybrid: step '${step.name}' \u2192 local ${step.model}
@@ -2199,9 +2436,10 @@ ${step.prompt}`,
2199
2436
  canUseTool: ctx.canUseTool,
2200
2437
  // Read never reaches `canUseTool` for an in-cwd path in the real SDK (see guardrails.ts); this
2201
2438
  // hook is the live enforcement point for the +Setup dotenv-read gap's Read side.
2202
- hooks: { PreToolUse: ctx.hooks },
2439
+ hooks: ctx.hooks,
2203
2440
  env: ctx.env
2204
2441
  };
2442
+ reportPromptFingerprint(ctx.cfg, stepOptions);
2205
2443
  const msgs = [];
2206
2444
  for await (const message of query2({ prompt: userPrompt, options: stepOptions })) {
2207
2445
  msgs.push(message);
@@ -2237,16 +2475,17 @@ async function runHybridStaged(plan, cfg) {
2237
2475
  const venvBin = join3(cwd, ".venv", "bin");
2238
2476
  const pathEnv = existsSync2(venvBin) ? `${venvBin}:${process.env.PATH ?? ""}` : process.env.PATH ?? "";
2239
2477
  const env = { ...process.env, PATH: pathEnv };
2240
- const slots = {};
2478
+ const artifacts = {};
2241
2479
  const outcomes = {};
2242
2480
  const steps = [];
2243
2481
  let finalText = "";
2244
2482
  let totalCost = 0;
2245
2483
  const startedAll = Date.now();
2246
2484
  const execCtx = {
2485
+ cfg,
2247
2486
  cwd,
2248
- canUseTool,
2249
- hooks,
2487
+ canUseTool: composeCanUseTool(plan.options.canUseTool, canUseTool),
2488
+ hooks: composeHooks(plan.options.hooks, hooks),
2250
2489
  env,
2251
2490
  plan,
2252
2491
  steps,
@@ -2270,7 +2509,7 @@ async function runHybridStaged(plan, cfg) {
2270
2509
  const preceding = i > 0 ? stagedSteps[i - 1] : null;
2271
2510
  const precedingIdentity = preceding === null ? null : { name: preceding.name, produces: preceding.produces };
2272
2511
  const decision = classifyConditionalStep(step.when, step.consumes, precedingIdentity, {
2273
- slots,
2512
+ artifacts,
2274
2513
  outcomes
2275
2514
  });
2276
2515
  if (decision.kind === "skip") {
@@ -2279,11 +2518,11 @@ async function runHybridStaged(plan, cfg) {
2279
2518
  continue;
2280
2519
  }
2281
2520
  if (decision.kind === "repair") {
2282
- let lastFailureText = slots[decision.target.produces ?? decision.target.name] ?? "";
2521
+ let lastFailureText = artifacts[decision.target.produces ?? decision.target.name] ?? "";
2283
2522
  const { recovered, attempts } = await runRepairLoop(DEFAULT_MAX_REPAIR_ATTEMPTS, async () => {
2284
- const attempt = await executeStep(step, slots, execCtx, true);
2523
+ const attempt = await executeStep(step, artifacts, execCtx, true);
2285
2524
  outcomes[step.name] = attempt.outcome;
2286
- slots[slotKey(step)] = attempt.text;
2525
+ artifacts[artifactKey(step)] = attempt.text;
2287
2526
  if (attempt.outcome === "success") finalText = attempt.text;
2288
2527
  else lastFailureText = attempt.text;
2289
2528
  return { failed: attempt.outcome === "failure" };
@@ -2300,9 +2539,9 @@ async function runHybridStaged(plan, cfg) {
2300
2539
  continue;
2301
2540
  }
2302
2541
  }
2303
- const outcome = await executeStep(step, slots, execCtx, failureGuardTargets.has(step.name));
2542
+ const outcome = await executeStep(step, artifacts, execCtx, failureGuardTargets.has(step.name));
2304
2543
  outcomes[step.name] = outcome.outcome;
2305
- slots[slotKey(step)] = outcome.text;
2544
+ artifacts[artifactKey(step)] = outcome.text;
2306
2545
  if (outcome.outcome === "success") finalText = outcome.text;
2307
2546
  }
2308
2547
  const trace = {
@@ -2363,24 +2602,52 @@ function prepareDispatch(input) {
2363
2602
  }
2364
2603
  scoped = [node];
2365
2604
  }
2605
+ const supply = input.slots ?? {};
2366
2606
  const components = scoped.map((node) => {
2367
- const report = resolveNodeCapabilities(node, target);
2368
- return buildPreparedComponent(node, report, input, target, models);
2607
+ const resolved = resolveSlotsForNode(node, ir, supply);
2608
+ const withSlots = resolved === null ? node : applyNodeSlots(node, resolved);
2609
+ const report = resolveNodeCapabilities(withSlots, target);
2610
+ return buildPreparedComponent(withSlots, report, input, target, models);
2369
2611
  });
2370
2612
  return { target, components };
2371
2613
  }
2614
+ function resolveSlotsForNode(node, ir, supply, unanswered = "fail") {
2615
+ const decls = [...ir.slots ?? [], ...node.slots ?? []];
2616
+ if (decls.length === 0) return null;
2617
+ return resolveSlots(decls, supply, `component '${node.id}'`, unanswered);
2618
+ }
2619
+ function applyNodeSlots(node, resolved) {
2620
+ const scope = `component '${node.id}'`;
2621
+ return {
2622
+ ...node,
2623
+ ...node.brief === void 0 ? {} : { brief: applySlots(node.brief, resolved, scope) },
2624
+ prompt_fragment: applySlots(node.prompt_fragment, resolved, scope),
2625
+ llm_calls: node.llm_calls.map((call) => ({
2626
+ ...call,
2627
+ prompt: applySlots(call.prompt, resolved, scope)
2628
+ }))
2629
+ };
2630
+ }
2372
2631
  function prepareDisplayManifest(input) {
2373
2632
  const ir = typeof input.ir === "string" ? parseIr(input.ir) : input.ir;
2374
2633
  assertSupportedIrVersion(ir.warble_ir_version);
2375
2634
  const target = input.target ?? DEFAULT_TARGET;
2376
2635
  const models = input.models ?? ModelConfig.default();
2377
2636
  models.validate(ir);
2637
+ const supply = input.slots ?? {};
2378
2638
  const components = ir.components.map((node) => {
2379
- const report = inspectNodeCapabilities(node, target);
2639
+ const resolved = resolveSlotsForNode(node, ir, supply, "default");
2640
+ const withSlots = resolved === null ? node : applyNodeSlots(node, resolved);
2641
+ const report = inspectNodeCapabilities(withSlots, target);
2380
2642
  if (report.some((entry2) => entry2.outcome === "fail")) {
2381
- return { id: node.id, node, availability: { status: "unavailable", reason: UNAVAILABLE_COMPONENT_REASON } };
2643
+ return {
2644
+ id: withSlots.id,
2645
+ node: withSlots,
2646
+ availability: { status: "unavailable", reason: UNAVAILABLE_COMPONENT_REASON }
2647
+ };
2382
2648
  }
2383
- return buildPreparedComponent(node, report, input, target, models);
2649
+ const prepared = buildPreparedComponent(withSlots, report, input, target, models);
2650
+ return { id: prepared.id, node: prepared.node, report: prepared.report };
2384
2651
  });
2385
2652
  return { target, components };
2386
2653
  }
@@ -2790,6 +3057,10 @@ export {
2790
3057
  SUPPORTED_IR_VERSIONS,
2791
3058
  parseIr,
2792
3059
  distinctTiers,
3060
+ resolveSlots,
3061
+ applySlots,
3062
+ assertNoSlotReferences,
3063
+ parseSlotFlags,
2793
3064
  ModelConfig,
2794
3065
  resolveStagedSteps,
2795
3066
  distinctProviders,
@@ -2810,6 +3081,12 @@ export {
2810
3081
  resolveCapabilities,
2811
3082
  resolveNodeCapabilities,
2812
3083
  inspectNodeCapabilities,
3084
+ fingerprintSurfaces,
3085
+ promptSurfacesOf,
3086
+ promptSurfaces,
3087
+ fingerprintPrompts,
3088
+ composeCanUseTool,
3089
+ composeHooks,
2813
3090
  makeReadOnlyGuard,
2814
3091
  buildChatRequest,
2815
3092
  extractCompletionText,
@@ -2841,4 +3118,4 @@ export {
2841
3118
  ChatSession,
2842
3119
  createChatSession
2843
3120
  };
2844
- //# sourceMappingURL=chunk-3ON7TSGW.js.map
3121
+ //# sourceMappingURL=chunk-7XQ5F6FM.js.map