@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.
- package/README.md +5 -2
- package/dist/{chunk-3ON7TSGW.js → chunk-2VDMVARW.js} +1814 -122
- package/dist/chunk-2VDMVARW.js.map +1 -0
- package/dist/cli.js +85 -23
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +743 -209
- package/dist/index.js +53 -3
- package/package.json +4 -4
- package/dist/chunk-3ON7TSGW.js.map +0 -1
|
@@ -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.
|
|
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,
|
|
1107
|
+
function buildStepMessages(step, question, artifacts) {
|
|
770
1108
|
const parts = [`Question: ${question}`];
|
|
771
|
-
for (const
|
|
772
|
-
const value =
|
|
1109
|
+
for (const artifact of step.consumes) {
|
|
1110
|
+
const value = artifacts[artifact];
|
|
773
1111
|
parts.push(
|
|
774
1112
|
value === void 0 ? `
|
|
775
|
-
[input '${
|
|
776
|
-
Input '${
|
|
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((
|
|
1197
|
-
const producer = producers.get(
|
|
1198
|
-
return producer ? `\`${
|
|
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((
|
|
1732
|
-
const p = producers.get(
|
|
1733
|
-
return p ? `the text step "${p}" returned` : `"${
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
1783
|
-
const pathEnv =
|
|
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
|
-
|
|
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:
|
|
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: [
|
|
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
|
-
|
|
1852
|
-
|
|
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
|
|
2395
|
+
import { mkdtempSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1866
2396
|
import { tmpdir } from "os";
|
|
1867
|
-
import { join as
|
|
2397
|
+
import { join as join3 } from "path";
|
|
1868
2398
|
function renderEnvelope(finalText, outPath, opts) {
|
|
1869
|
-
const dir = mkdtempSync(
|
|
1870
|
-
const envelopePath =
|
|
1871
|
-
|
|
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
|
|
1896
|
-
import { join as
|
|
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(
|
|
1909
|
-
const [
|
|
1910
|
-
if (
|
|
1911
|
-
const raw =
|
|
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.
|
|
2455
|
+
return readFlag(state.artifacts, when.target);
|
|
1926
2456
|
case "on_missing":
|
|
1927
|
-
return state.
|
|
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
|
-
|
|
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 =
|
|
2123
|
-
const pathEnv =
|
|
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:
|
|
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
|
-
|
|
2146
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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,
|
|
2177
|
-
const messages = buildStepMessages(step, ctx.plan.prompt,
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
2238
|
-
const pathEnv =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
2827
|
+
const attempt = await executeStep(step, artifacts, execCtx, true);
|
|
2285
2828
|
outcomes[step.name] = attempt.outcome;
|
|
2286
|
-
|
|
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,
|
|
2846
|
+
const outcome = await executeStep(step, artifacts, execCtx, failureGuardTargets.has(step.name));
|
|
2304
2847
|
outcomes[step.name] = outcome.outcome;
|
|
2305
|
-
|
|
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
|
-
|
|
2325
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
3843
|
+
if (opts.project) return resolve2(opts.project);
|
|
2345
3844
|
const p = node.context_binding.project;
|
|
2346
|
-
if (
|
|
2347
|
-
const baseDir = opts.irPath ?
|
|
2348
|
-
return
|
|
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 =
|
|
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
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
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 =
|
|
2367
|
-
|
|
2368
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
2380
|
-
|
|
2381
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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.
|
|
2404
|
-
var MIN_SUPPORTED_IR_VERSION = "0.
|
|
2405
|
-
var MAX_SUPPORTED_IR_VERSION = "0.
|
|
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
|
-
|
|
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
|
|
2587
|
-
import { resolve as
|
|
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((
|
|
2638
|
-
timer = setTimeout(
|
|
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 ??
|
|
4318
|
+
const queryFactory = options.queryFactory ?? query4;
|
|
2652
4319
|
catalogQuery = queryFactory({
|
|
2653
4320
|
prompt: idleInput(),
|
|
2654
4321
|
options: {
|
|
2655
|
-
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
|
|
2685
|
-
import { join as
|
|
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 =
|
|
2762
|
-
|
|
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-
|
|
4536
|
+
//# sourceMappingURL=chunk-2VDMVARW.js.map
|