@darkhorseprojects/circuitry 0.2.32 → 0.2.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/graph.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export declare const CIRCUITRY_SPEC_VERSION: "0.2";
2
- export type CircuitrySpecVersion = typeof CIRCUITRY_SPEC_VERSION;
1
+ export declare const CIRCUITRY_SPEC_VERSION: "0.2.99";
2
+ export type CircuitrySpecVersion = typeof CIRCUITRY_SPEC_VERSION | "0.2";
3
3
  export type CircuitryNodeKind = "agent" | "input" | "tool" | "output" | string;
4
4
  export type CircuitryEdgeKind = "context" | "dependency" | "message" | "control" | string;
5
5
  export type CircuitryInputKind = "text" | "file" | "url" | "image" | "uri" | "canvas" | "mcp" | string;
@@ -170,12 +170,20 @@ export type CircuitryRuntimeInputs = Record<string, unknown>;
170
170
  * graph source stable and catches misspelled input names before execution.
171
171
  */
172
172
  export declare const applyCircuitryRuntimeInputs: (graph: CircuitryGraph, inputs?: CircuitryRuntimeInputs) => CircuitryGraph;
173
+ export type CircuitryGraphLink = string | {
174
+ /** Path to another .circuitry.yaml/.json file, resolved relative to this file. */
175
+ path: string;
176
+ /** Optional id prefix applied to imported resources. */
177
+ prefix?: string;
178
+ };
173
179
  export type CircuitryGraph = {
174
- /** Spec version. Use "0.2" for new graphs. */
180
+ /** Spec version. Use "0.2.99" for linked graph files. */
175
181
  circuitry: CircuitrySpecVersion | string;
176
182
  id?: string;
177
183
  title?: string;
178
184
  description?: string;
185
+ /** Files whose resources are merged into this graph before validation/execution. */
186
+ links?: CircuitryGraphLink[];
179
187
  resources?: Record<string, CircuitryResourceEntry>;
180
188
  /** Internal normalized execution nodes. Authored graph files must not set this. */
181
189
  nodes?: CircuitryNode[];
@@ -215,7 +223,7 @@ export type CircuitryValidationResult = {
215
223
  };
216
224
  export declare const DEFAULT_CIRCUITRY_VALIDATION_RULES: CircuitryValidationRuleEntry[];
217
225
  export declare const DEFAULT_CIRCUITRY_VALIDATION_STANDARD: {
218
- readonly version: "0.2";
226
+ readonly version: "0.2.99";
219
227
  readonly requireSpecVersion: true;
220
228
  readonly rules: CircuitryValidationRuleEntry[];
221
229
  readonly executableKinds: ["agent", "tool", "output"];
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ var isNodeElement = (element) => {
27
27
  };
28
28
 
29
29
  // src/graph.ts
30
- var CIRCUITRY_SPEC_VERSION = "0.2";
30
+ var CIRCUITRY_SPEC_VERSION = "0.2.99";
31
31
  var runtimeInputToText = (value) => {
32
32
  if (typeof value === "string") return value;
33
33
  if (value === void 0) return "";
@@ -136,7 +136,7 @@ var normalizeCircuitryGraph = (graph) => {
136
136
  const { nodes, edges } = expandResources(graph.resources);
137
137
  return { ...graph, nodes, edges };
138
138
  };
139
- var VALID_SPEC_VERSIONS = /* @__PURE__ */ new Set([CIRCUITRY_SPEC_VERSION]);
139
+ var VALID_SPEC_VERSIONS = /* @__PURE__ */ new Set(["0.2", CIRCUITRY_SPEC_VERSION]);
140
140
  var validateCircuitryGraphInternal = (graph, standard = {}, options) => {
141
141
  const resolvedStandard = createCircuitryValidationStandard(
142
142
  standard,
@@ -519,6 +519,96 @@ ${contextSection}`
519
519
  ].filter(Boolean).join("\n\n");
520
520
  };
521
521
  var collectImages = (contextInputs) => contextInputs.filter((item) => item.kind === "image" && item.image).map((item) => item.image);
522
+ var stripJsonFence = (output) => {
523
+ const trimmed = output.trim();
524
+ const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
525
+ return fenced ? fenced[1].trim() : trimmed;
526
+ };
527
+ var parseExpectedOutput = (nodeId, output) => {
528
+ try {
529
+ return JSON.parse(stripJsonFence(output));
530
+ } catch (error) {
531
+ const detail = error instanceof Error ? error.message : String(error);
532
+ throw new Error(`Node ${nodeId} output does not match expect: output is not valid JSON (${detail})`);
533
+ }
534
+ };
535
+ var isFieldObject = (schema) => !!schema && typeof schema === "object" && !Array.isArray(schema) && typeof schema.type === "string";
536
+ var describeExpectedType = (schema) => Array.isArray(schema) ? "list" : typeof schema === "string" ? schema : isFieldObject(schema) ? schema.type : "dict";
537
+ var matchesPrimitiveType = (value, type) => {
538
+ switch (type) {
539
+ case "str":
540
+ return typeof value === "string";
541
+ case "int":
542
+ return Number.isInteger(value);
543
+ case "float":
544
+ return typeof value === "number" && Number.isFinite(value);
545
+ case "bool":
546
+ return typeof value === "boolean";
547
+ case "list":
548
+ return Array.isArray(value);
549
+ case "dict":
550
+ return !!value && typeof value === "object" && !Array.isArray(value);
551
+ default:
552
+ return true;
553
+ }
554
+ };
555
+ var validateExpectValue = (value, schema, path, errors) => {
556
+ if (typeof schema === "string") {
557
+ if (!matchesPrimitiveType(value, schema)) {
558
+ errors.push(`${path} expected ${schema}, got ${Array.isArray(value) ? "list" : typeof value}`);
559
+ }
560
+ return;
561
+ }
562
+ if (Array.isArray(schema)) {
563
+ if (!Array.isArray(value)) {
564
+ errors.push(`${path} expected list, got ${typeof value}`);
565
+ return;
566
+ }
567
+ if (schema.length > 0) {
568
+ value.forEach((item, index) => validateExpectValue(item, schema[0], `${path}[${index}]`, errors));
569
+ }
570
+ return;
571
+ }
572
+ if (isFieldObject(schema)) {
573
+ if (schema.optional && value === void 0) return;
574
+ if (!matchesPrimitiveType(value, schema.type)) {
575
+ errors.push(`${path} expected ${schema.type}, got ${Array.isArray(value) ? "list" : typeof value}`);
576
+ return;
577
+ }
578
+ if (schema.contains && typeof value === "string" && !value.includes(schema.contains)) {
579
+ errors.push(`${path} must include string: ${schema.contains}`);
580
+ }
581
+ if (schema.type === "list" && schema.items && Array.isArray(value)) {
582
+ value.forEach((item, index) => validateExpectValue(item, schema.items, `${path}[${index}]`, errors));
583
+ }
584
+ return;
585
+ }
586
+ if (schema && typeof schema === "object") {
587
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
588
+ errors.push(`${path} expected dict, got ${Array.isArray(value) ? "list" : typeof value}`);
589
+ return;
590
+ }
591
+ for (const [key, childSchema] of Object.entries(schema)) {
592
+ const childOptional = isFieldObject(childSchema) && childSchema.optional;
593
+ const childValue = value[key];
594
+ if (childValue === void 0 && !childOptional) {
595
+ errors.push(`${path}.${key} is missing required field of type ${describeExpectedType(childSchema)}`);
596
+ continue;
597
+ }
598
+ validateExpectValue(childValue, childSchema, `${path}.${key}`, errors);
599
+ }
600
+ }
601
+ };
602
+ var assertExpectedOutput = (node, output) => {
603
+ if (!node.expect) return;
604
+ const parsed = parseExpectedOutput(node.id, output);
605
+ const errors = [];
606
+ validateExpectValue(parsed, node.expect, node.id, errors);
607
+ if (errors.length) {
608
+ throw new Error(`Node ${node.id} output does not match expect:
609
+ ${errors.join("\n")}`);
610
+ }
611
+ };
522
612
  var executeCircuitryNode = async ({
523
613
  graph,
524
614
  item,
@@ -542,8 +632,9 @@ var executeCircuitryNode = async ({
542
632
  };
543
633
  }
544
634
  const agent = item.node.agent || {};
635
+ let result;
545
636
  try {
546
- const result = await executeNode({
637
+ result = await executeNode({
547
638
  nodeId: item.id,
548
639
  model: (agent.model === "inherit" ? void 0 : agent.model) || (defaultModel === "inherit" ? void 0 : defaultModel) || "inherit",
549
640
  tools: agent.tools || [],
@@ -556,13 +647,6 @@ var executeCircuitryNode = async ({
556
647
  images: collectImages(contextInputs),
557
648
  prompt: composeCircuitryPrompt(graph, item.node, inputPayload, contextInputs)
558
649
  });
559
- onNodeComplete?.(item.id, { output: result.output });
560
- return {
561
- nodeId: item.id,
562
- fallbackCycle,
563
- inputNodeIds: inputPayload.map((input) => input.nodeId),
564
- output: result.output
565
- };
566
650
  } catch (error) {
567
651
  const message = error instanceof Error ? error.message : String(error);
568
652
  onNodeComplete?.(item.id, { error: message });
@@ -574,6 +658,14 @@ var executeCircuitryNode = async ({
574
658
  error: message
575
659
  };
576
660
  }
661
+ assertExpectedOutput(item.node, result.output);
662
+ onNodeComplete?.(item.id, { output: result.output });
663
+ return {
664
+ nodeId: item.id,
665
+ fallbackCycle,
666
+ inputNodeIds: inputPayload.map((input) => input.nodeId),
667
+ output: result.output
668
+ };
577
669
  };
578
670
  var runCircuitryGraphExecution = async ({
579
671
  graph,
package/dist/node.d.ts CHANGED
@@ -8,6 +8,12 @@ export declare class NodeCircuitryHost implements CircuitryHost {
8
8
  private exists;
9
9
  private parseIssue;
10
10
  private parseGraphForValidation;
11
+ private linkPath;
12
+ private linkPrefix;
13
+ private prefixResourceIds;
14
+ private mergeResources;
15
+ private resolveGraphLinks;
16
+ private parseAndResolveGraph;
11
17
  private readGraphFile;
12
18
  readGraph(input?: {
13
19
  filename?: string;
package/dist/node.js CHANGED
@@ -8,7 +8,7 @@ import os from "node:os";
8
8
  import YAML from "yaml";
9
9
 
10
10
  // src/graph.ts
11
- var CIRCUITRY_SPEC_VERSION = "0.2";
11
+ var CIRCUITRY_SPEC_VERSION = "0.2.99";
12
12
  var runtimeInputToText = (value) => {
13
13
  if (typeof value === "string") return value;
14
14
  if (value === void 0) return "";
@@ -117,7 +117,7 @@ var normalizeCircuitryGraph = (graph) => {
117
117
  const { nodes, edges } = expandResources(graph.resources);
118
118
  return { ...graph, nodes, edges };
119
119
  };
120
- var VALID_SPEC_VERSIONS = /* @__PURE__ */ new Set([CIRCUITRY_SPEC_VERSION]);
120
+ var VALID_SPEC_VERSIONS = /* @__PURE__ */ new Set(["0.2", CIRCUITRY_SPEC_VERSION]);
121
121
  var validateCircuitryGraphInternal = (graph, standard = {}, options) => {
122
122
  const resolvedStandard = createCircuitryValidationStandard(
123
123
  standard,
@@ -441,6 +441,96 @@ ${contextSection}`
441
441
  ].filter(Boolean).join("\n\n");
442
442
  };
443
443
  var collectImages = (contextInputs) => contextInputs.filter((item) => item.kind === "image" && item.image).map((item) => item.image);
444
+ var stripJsonFence = (output) => {
445
+ const trimmed = output.trim();
446
+ const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
447
+ return fenced ? fenced[1].trim() : trimmed;
448
+ };
449
+ var parseExpectedOutput = (nodeId, output) => {
450
+ try {
451
+ return JSON.parse(stripJsonFence(output));
452
+ } catch (error) {
453
+ const detail = error instanceof Error ? error.message : String(error);
454
+ throw new Error(`Node ${nodeId} output does not match expect: output is not valid JSON (${detail})`);
455
+ }
456
+ };
457
+ var isFieldObject = (schema) => !!schema && typeof schema === "object" && !Array.isArray(schema) && typeof schema.type === "string";
458
+ var describeExpectedType = (schema) => Array.isArray(schema) ? "list" : typeof schema === "string" ? schema : isFieldObject(schema) ? schema.type : "dict";
459
+ var matchesPrimitiveType = (value, type) => {
460
+ switch (type) {
461
+ case "str":
462
+ return typeof value === "string";
463
+ case "int":
464
+ return Number.isInteger(value);
465
+ case "float":
466
+ return typeof value === "number" && Number.isFinite(value);
467
+ case "bool":
468
+ return typeof value === "boolean";
469
+ case "list":
470
+ return Array.isArray(value);
471
+ case "dict":
472
+ return !!value && typeof value === "object" && !Array.isArray(value);
473
+ default:
474
+ return true;
475
+ }
476
+ };
477
+ var validateExpectValue = (value, schema, path2, errors) => {
478
+ if (typeof schema === "string") {
479
+ if (!matchesPrimitiveType(value, schema)) {
480
+ errors.push(`${path2} expected ${schema}, got ${Array.isArray(value) ? "list" : typeof value}`);
481
+ }
482
+ return;
483
+ }
484
+ if (Array.isArray(schema)) {
485
+ if (!Array.isArray(value)) {
486
+ errors.push(`${path2} expected list, got ${typeof value}`);
487
+ return;
488
+ }
489
+ if (schema.length > 0) {
490
+ value.forEach((item, index) => validateExpectValue(item, schema[0], `${path2}[${index}]`, errors));
491
+ }
492
+ return;
493
+ }
494
+ if (isFieldObject(schema)) {
495
+ if (schema.optional && value === void 0) return;
496
+ if (!matchesPrimitiveType(value, schema.type)) {
497
+ errors.push(`${path2} expected ${schema.type}, got ${Array.isArray(value) ? "list" : typeof value}`);
498
+ return;
499
+ }
500
+ if (schema.contains && typeof value === "string" && !value.includes(schema.contains)) {
501
+ errors.push(`${path2} must include string: ${schema.contains}`);
502
+ }
503
+ if (schema.type === "list" && schema.items && Array.isArray(value)) {
504
+ value.forEach((item, index) => validateExpectValue(item, schema.items, `${path2}[${index}]`, errors));
505
+ }
506
+ return;
507
+ }
508
+ if (schema && typeof schema === "object") {
509
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
510
+ errors.push(`${path2} expected dict, got ${Array.isArray(value) ? "list" : typeof value}`);
511
+ return;
512
+ }
513
+ for (const [key, childSchema] of Object.entries(schema)) {
514
+ const childOptional = isFieldObject(childSchema) && childSchema.optional;
515
+ const childValue = value[key];
516
+ if (childValue === void 0 && !childOptional) {
517
+ errors.push(`${path2}.${key} is missing required field of type ${describeExpectedType(childSchema)}`);
518
+ continue;
519
+ }
520
+ validateExpectValue(childValue, childSchema, `${path2}.${key}`, errors);
521
+ }
522
+ }
523
+ };
524
+ var assertExpectedOutput = (node, output) => {
525
+ if (!node.expect) return;
526
+ const parsed = parseExpectedOutput(node.id, output);
527
+ const errors = [];
528
+ validateExpectValue(parsed, node.expect, node.id, errors);
529
+ if (errors.length) {
530
+ throw new Error(`Node ${node.id} output does not match expect:
531
+ ${errors.join("\n")}`);
532
+ }
533
+ };
444
534
  var executeCircuitryNode = async ({
445
535
  graph,
446
536
  item,
@@ -464,8 +554,9 @@ var executeCircuitryNode = async ({
464
554
  };
465
555
  }
466
556
  const agent = item.node.agent || {};
557
+ let result;
467
558
  try {
468
- const result = await executeNode({
559
+ result = await executeNode({
469
560
  nodeId: item.id,
470
561
  model: (agent.model === "inherit" ? void 0 : agent.model) || (defaultModel === "inherit" ? void 0 : defaultModel) || "inherit",
471
562
  tools: agent.tools || [],
@@ -478,13 +569,6 @@ var executeCircuitryNode = async ({
478
569
  images: collectImages(contextInputs),
479
570
  prompt: composeCircuitryPrompt(graph, item.node, inputPayload, contextInputs)
480
571
  });
481
- onNodeComplete?.(item.id, { output: result.output });
482
- return {
483
- nodeId: item.id,
484
- fallbackCycle,
485
- inputNodeIds: inputPayload.map((input) => input.nodeId),
486
- output: result.output
487
- };
488
572
  } catch (error) {
489
573
  const message = error instanceof Error ? error.message : String(error);
490
574
  onNodeComplete?.(item.id, { error: message });
@@ -496,6 +580,14 @@ var executeCircuitryNode = async ({
496
580
  error: message
497
581
  };
498
582
  }
583
+ assertExpectedOutput(item.node, result.output);
584
+ onNodeComplete?.(item.id, { output: result.output });
585
+ return {
586
+ nodeId: item.id,
587
+ fallbackCycle,
588
+ inputNodeIds: inputPayload.map((input) => input.nodeId),
589
+ output: result.output
590
+ };
499
591
  };
500
592
  var runCircuitryGraphExecution = async ({
501
593
  graph,
@@ -745,11 +837,84 @@ var NodeCircuitryHost = class {
745
837
  return { error: this.parseIssue(error, filename) };
746
838
  }
747
839
  }
840
+ linkPath(link) {
841
+ return typeof link === "string" ? link : link.path;
842
+ }
843
+ linkPrefix(link) {
844
+ return typeof link === "string" ? "" : link.prefix || "";
845
+ }
846
+ prefixResourceIds(resources, prefix) {
847
+ if (!prefix) return resources;
848
+ const ids = new Set(Object.keys(resources));
849
+ const prefixed = {};
850
+ for (const [id, resource] of Object.entries(resources)) {
851
+ const next = { ...resource };
852
+ if ((next.type === "agent" || next.type === "tool") && next.inputs) {
853
+ next.inputs = next.inputs.map((input) => ids.has(input) ? `${prefix}${input}` : input);
854
+ }
855
+ prefixed[`${prefix}${id}`] = next;
856
+ }
857
+ return prefixed;
858
+ }
859
+ mergeResources(target, source, fromFile) {
860
+ for (const [id, resource] of Object.entries(source)) {
861
+ if (target[id]) {
862
+ throw new Error(`Linked Circuitry resource id collision: ${id} from ${fromFile}`);
863
+ }
864
+ target[id] = resource;
865
+ }
866
+ }
867
+ async resolveGraphLinks(graph, filename, standard, stack = []) {
868
+ const graphFile = path.resolve(filename);
869
+ if (stack.includes(graphFile)) {
870
+ throw new Error(`Circuitry graph link cycle: ${[...stack, graphFile].join(" -> ")}`);
871
+ }
872
+ const linkedResources = {};
873
+ const nextStack = [...stack, graphFile];
874
+ for (const link of graph.links || []) {
875
+ const rawPath = this.linkPath(link);
876
+ if (!rawPath) throw new Error(`Circuitry graph link is missing path in ${graphFile}`);
877
+ const linkedFile = path.resolve(path.dirname(graphFile), rawPath);
878
+ const linkedText = await readFile(linkedFile, "utf8");
879
+ const parsed = parseCircuitryText(linkedText, linkedFile, standard, { validate: false });
880
+ const resolved = await this.resolveGraphLinks(parsed, linkedFile, standard, nextStack);
881
+ this.mergeResources(
882
+ linkedResources,
883
+ this.prefixResourceIds(resolved.resources || {}, this.linkPrefix(link)),
884
+ linkedFile
885
+ );
886
+ }
887
+ return {
888
+ ...graph,
889
+ resources: {
890
+ ...linkedResources,
891
+ ...graph.resources || {}
892
+ }
893
+ };
894
+ }
895
+ async parseAndResolveGraph(text, filename, standard) {
896
+ const parsed = this.parseGraphForValidation(text, filename, standard);
897
+ if (!parsed.graph) return parsed;
898
+ try {
899
+ return { graph: await this.resolveGraphLinks(parsed.graph, filename, standard) };
900
+ } catch (error) {
901
+ return { error: this.parseIssue(error, filename) };
902
+ }
903
+ }
748
904
  async readGraphFile(filename) {
749
905
  const graphFile = this.resolveGraphFile(filename);
750
906
  const text = await readFile(graphFile, "utf8");
751
- const graph = parseCircuitryText(text, graphFile);
752
- return { graphFile, text, graph };
907
+ const parsed = await this.parseAndResolveGraph(text, graphFile);
908
+ if (!parsed.graph) throw new Error(`Invalid Circuitry graph:
909
+ ${parsed.error?.message}`);
910
+ const validation = validateCircuitryGraphWithStandard(parsed.graph);
911
+ if (validation.errors.length) {
912
+ throw new Error(
913
+ `Invalid Circuitry graph:
914
+ ${validation.errors.map((e) => e.message).join("\n")}`
915
+ );
916
+ }
917
+ return { graphFile, text, graph: normalizeCircuitryGraph(parsed.graph) };
753
918
  }
754
919
  async readGraph(input = {}) {
755
920
  const { graphFile, text, graph } = await this.readGraphFile(input.filename);
@@ -761,8 +926,8 @@ var NodeCircuitryHost = class {
761
926
  if (input.graph)
762
927
  return validateCircuitryGraphWithStandard(input.graph, input.standard);
763
928
  const filename = input.filename || defaultFilename;
764
- const parsed = this.parseGraphForValidation(input.text, filename, input.standard);
765
- if (parsed.error) {
929
+ const parsed = await this.parseAndResolveGraph(input.text, filename, input.standard);
930
+ if ("error" in parsed) {
766
931
  return {
767
932
  ok: false,
768
933
  errors: [parsed.error],
@@ -773,7 +938,7 @@ var NodeCircuitryHost = class {
773
938
  }
774
939
  async writeGraph(input) {
775
940
  const graphFile = this.resolveGraphFile(input.filename);
776
- const parsed = input.graph ? { graph: input.graph } : this.parseGraphForValidation(
941
+ const parsed = input.graph ? { graph: input.graph } : await this.parseAndResolveGraph(
777
942
  input.text,
778
943
  input.filename || graphFile,
779
944
  input.standard
@@ -804,7 +969,7 @@ ${validation.errors.map((e) => e.message).join("\n")}`
804
969
  async runGraph(input = {}) {
805
970
  const source = input.source || (input.text ? "text" : "current");
806
971
  const graphFile = this.resolveGraphFile(input.filename);
807
- const parsed = source === "text" ? this.parseGraphForValidation(
972
+ const parsed = source === "text" ? await this.parseAndResolveGraph(
808
973
  input.text,
809
974
  input.filename || graphFile,
810
975
  input.standard
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darkhorseprojects/circuitry",
3
- "version": "0.2.32",
3
+ "version": "0.2.99",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",