@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.
- package/dist/{chunk-3ON7TSGW.js → chunk-7XQ5F6FM.js} +327 -50
- package/dist/chunk-7XQ5F6FM.js.map +1 -0
- package/dist/cli.js +24 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +238 -5
- package/dist/index.js +21 -1
- package/package.json +2 -2
- package/dist/chunk-3ON7TSGW.js.map +0 -1
|
@@ -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,
|
|
914
|
+
function buildStepMessages(step, question, artifacts) {
|
|
770
915
|
const parts = [`Question: ${question}`];
|
|
771
|
-
for (const
|
|
772
|
-
const value =
|
|
916
|
+
for (const artifact of step.consumes) {
|
|
917
|
+
const value = artifacts[artifact];
|
|
773
918
|
parts.push(
|
|
774
919
|
value === void 0 ? `
|
|
775
|
-
[input '${
|
|
776
|
-
Input '${
|
|
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((
|
|
1197
|
-
const producer = producers.get(
|
|
1198
|
-
return producer ? `\`${
|
|
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((
|
|
1732
|
-
const p = producers.get(
|
|
1733
|
-
return p ? `the text step "${p}" returned` : `"${
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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: [
|
|
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(
|
|
1909
|
-
const [
|
|
1910
|
-
if (
|
|
1911
|
-
const raw =
|
|
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.
|
|
2157
|
+
return readFlag(state.artifacts, when.target);
|
|
1926
2158
|
case "on_missing":
|
|
1927
|
-
return state.
|
|
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:
|
|
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
|
|
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,
|
|
2177
|
-
const messages = buildStepMessages(step, ctx.plan.prompt,
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
2523
|
+
const attempt = await executeStep(step, artifacts, execCtx, true);
|
|
2285
2524
|
outcomes[step.name] = attempt.outcome;
|
|
2286
|
-
|
|
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,
|
|
2542
|
+
const outcome = await executeStep(step, artifacts, execCtx, failureGuardTargets.has(step.name));
|
|
2304
2543
|
outcomes[step.name] = outcome.outcome;
|
|
2305
|
-
|
|
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
|
|
2368
|
-
|
|
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
|
|
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 {
|
|
2643
|
+
return {
|
|
2644
|
+
id: withSlots.id,
|
|
2645
|
+
node: withSlots,
|
|
2646
|
+
availability: { status: "unavailable", reason: UNAVAILABLE_COMPONENT_REASON }
|
|
2647
|
+
};
|
|
2382
2648
|
}
|
|
2383
|
-
|
|
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-
|
|
3121
|
+
//# sourceMappingURL=chunk-7XQ5F6FM.js.map
|