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