@darkhorseprojects/circuitry 0.3.9 → 0.4.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/SPEC.md ADDED
@@ -0,0 +1,96 @@
1
+ # Circuitry 0.4
2
+
3
+ Circuitry is a resource-native source format for executable graphs.
4
+
5
+ A Circuitry file describes named resources and the dependencies between them. Running a graph means resolving an entry resource. Runtimes decide how to execute supported resource types.
6
+
7
+ ## Top-level shape
8
+
9
+ ```yaml
10
+ circuitry: "0.4"
11
+ title: string
12
+ description: string
13
+ imports: []
14
+ inputs: {}
15
+ entry: resource_id
16
+ entries: {}
17
+ resources: {}
18
+ outputs: {}
19
+ ```
20
+
21
+ ## Resources
22
+
23
+ Resources are the graph. A resource is identified by its key under `resources`.
24
+
25
+ ```yaml
26
+ resources:
27
+ id:
28
+ type: string
29
+ label: string
30
+ description: string
31
+ inputs: []
32
+ from: string
33
+ value: any
34
+ path: string
35
+ uri: string
36
+ mime: string
37
+ identity: string
38
+ instructions: string
39
+ tools: []
40
+ graph: string
41
+ entry: string
42
+ expect: {}
43
+ ```
44
+
45
+ `inputs` may be a list of resource ids or a map of local input names to resource ids.
46
+
47
+ ## Inputs
48
+
49
+ Top-level `inputs` declare runtime inputs. Resolvers may inject matching `type: input` resources when absent.
50
+
51
+ ```yaml
52
+ inputs:
53
+ user_turn:
54
+ type: text
55
+ required: true
56
+ ```
57
+
58
+ ## Entry points
59
+
60
+ `entry` is the default resource to resolve. `entries` optionally names multiple run surfaces.
61
+
62
+ ```yaml
63
+ entry: assistant
64
+ entries:
65
+ recover: recovered_context
66
+ ```
67
+
68
+ ## Run resources
69
+
70
+ `type: run` represents declarative graph execution as a resource.
71
+
72
+ ```yaml
73
+ resources:
74
+ recovered_context:
75
+ type: run
76
+ graph: ./context.circuitry.yaml
77
+ entry: recovery
78
+ inputs:
79
+ user_turn: user_turn
80
+ ```
81
+
82
+ A runtime tool such as `run_graph` is dynamic capability. A `type: run` resource is declarative topology.
83
+
84
+ ## Outputs
85
+
86
+ ```yaml
87
+ outputs:
88
+ response:
89
+ from: assistant.response
90
+ schema:
91
+ response: str
92
+ ```
93
+
94
+ ## Extensions
95
+
96
+ Fields beginning with `x-` are preserved for runtimes and ecosystems.
package/dist/cli.js CHANGED
@@ -12,150 +12,128 @@ import YAML2 from "yaml";
12
12
 
13
13
  // src/graph.ts
14
14
  import YAML from "yaml";
15
- var CIRCUITRY_SPEC_VERSION = "0.3.2";
16
- var DEFAULT_CIRCUITRY_VALIDATION_RULES = [
17
- "no-self-loops",
18
- "no-unknown-edge-endpoints",
19
- "require-executable-inputs",
20
- "no-cycles"
21
- ];
15
+ var CIRCUITRY_SPEC_VERSION = "0.4";
22
16
  var DEFAULT_CIRCUITRY_VALIDATION_STANDARD = {
23
17
  version: CIRCUITRY_SPEC_VERSION,
24
- requireSpecVersion: true,
25
- rules: DEFAULT_CIRCUITRY_VALIDATION_RULES,
26
- executableKinds: ["agent", "tool", "output"]
18
+ requireSpecVersion: true
27
19
  };
28
- var ruleName = (rule) => typeof rule === "string" ? rule : rule.rule;
29
- var executableKindsFromRules = (rules) => rules.find((rule) => typeof rule !== "string" && rule.rule === "require-executable-inputs")?.executableKinds;
30
- var createCircuitryValidationStandard = (standard = {}, graph) => {
31
- const rules = [...DEFAULT_CIRCUITRY_VALIDATION_RULES, ...graph?.validation?.rules || [], ...standard.rules || []];
32
- return {
33
- version: standard.version || DEFAULT_CIRCUITRY_VALIDATION_STANDARD.version,
34
- requireSpecVersion: standard.requireSpecVersion ?? DEFAULT_CIRCUITRY_VALIDATION_STANDARD.requireSpecVersion,
35
- rules,
36
- executableKinds: standard.executableKinds || executableKindsFromRules(rules) || [...DEFAULT_CIRCUITRY_VALIDATION_STANDARD.executableKinds]
20
+ var createCircuitryValidationStandard = (standard = {}) => ({
21
+ version: standard.version || DEFAULT_CIRCUITRY_VALIDATION_STANDARD.version,
22
+ requireSpecVersion: standard.requireSpecVersion ?? DEFAULT_CIRCUITRY_VALIDATION_STANDARD.requireSpecVersion
23
+ });
24
+ var getResourceInputs = (resource) => {
25
+ const inputs = resource.inputs;
26
+ if (!inputs) return [];
27
+ if (Array.isArray(inputs)) return inputs.map((from) => ({ from, to: "" }));
28
+ return Object.entries(inputs).map(([name, from]) => ({ from, to: "", name }));
29
+ };
30
+ var resourceInputIds = (resource) => getResourceInputs(resource).map((dependency) => dependency.from);
31
+ var getCircuitryDependencies = (graph) => {
32
+ const dependencies = [];
33
+ for (const [to, resource] of Object.entries(graph.resources || {})) {
34
+ for (const dependency of getResourceInputs(resource)) dependencies.push({ ...dependency, to });
35
+ }
36
+ return dependencies;
37
+ };
38
+ var getCircuitryRunSet = (graph, entry = graph.entry) => {
39
+ if (!entry) return [];
40
+ const visited = /* @__PURE__ */ new Set();
41
+ const visit = (id) => {
42
+ if (visited.has(id)) return;
43
+ const resource = graph.resources?.[id];
44
+ if (!resource) return;
45
+ for (const input of resourceInputIds(resource)) visit(input);
46
+ visited.add(id);
37
47
  };
48
+ visit(entry);
49
+ return [...visited];
38
50
  };
39
- var expandResources = (resources) => {
40
- const nodes = [];
41
- const edges = [];
42
- for (const [id, resource] of Object.entries(resources)) {
43
- if (resource.type === "text") {
44
- nodes.push({ id, kind: "input", label: resource.label || id, input: { type: "text", value: resource.value } });
45
- } else if (resource.type === "agent") {
46
- const agent = resource;
47
- nodes.push({
48
- id,
49
- kind: "agent",
50
- label: agent.label || agent.identity || id,
51
- agent: {
52
- identity: agent.identity || agent.label || id,
53
- model: agent.model,
54
- tools: agent.tools,
55
- instructions: agent.instructions,
56
- personality: agent.personality,
57
- context: agent.context
58
- },
59
- skills: agent.skills,
60
- expect: agent.expect
61
- });
62
- for (const input of agent.inputs || []) edges.push({ from: input, to: id, kind: "dependency" });
63
- } else if (resource.type === "tool") {
64
- const tool = resource;
65
- nodes.push({ id, kind: "tool", label: tool.label || id, agent: { instructions: tool.instructions } });
66
- for (const input of tool.inputs || []) edges.push({ from: input, to: id, kind: "dependency" });
67
- } else {
68
- const input = resource;
69
- nodes.push({ id, kind: "input", label: input.label || id, input: { type: input.type, value: input.value, uri: input.uri || input.path, mimeType: input.mimeType, data: input.data } });
51
+ var toposortCircuitryResources = (graph, entry) => {
52
+ const ids = entry ? new Set(getCircuitryRunSet(graph, entry)) : new Set(Object.keys(graph.resources || {}));
53
+ const visiting = /* @__PURE__ */ new Set();
54
+ const visited = /* @__PURE__ */ new Set();
55
+ const ordered = [];
56
+ const visit = (id) => {
57
+ if (!ids.has(id) || visited.has(id)) return;
58
+ if (visiting.has(id)) throw new Error(`Circuitry resource dependency cycle at ${id}`);
59
+ visiting.add(id);
60
+ const resource = graph.resources?.[id];
61
+ if (resource) for (const input of resourceInputIds(resource)) visit(input);
62
+ visiting.delete(id);
63
+ visited.add(id);
64
+ ordered.push(id);
65
+ };
66
+ for (const id of ids) visit(id);
67
+ return ordered;
68
+ };
69
+ var resolveCircuitrySource = (graph) => {
70
+ const inputs = graph.inputs || graph.args || {};
71
+ const resources = { ...graph.resources || {} };
72
+ for (const [id, input] of Object.entries(inputs)) {
73
+ if (!resources[id]) {
74
+ resources[id] = {
75
+ type: "input",
76
+ from: id,
77
+ label: input.label,
78
+ description: input.description,
79
+ mime: input.mime || input.mimeType
80
+ };
70
81
  }
71
82
  }
72
- return { nodes, edges };
83
+ return { ...graph, circuitry: CIRCUITRY_SPEC_VERSION, imports: graph.imports || [], inputs, resources };
73
84
  };
74
- var normalizeCircuitryGraph = (graph) => {
75
- if (!graph.resources) return { ...graph };
76
- return { ...graph, ...expandResources(graph.resources) };
77
- };
78
- var validateCircuitryGraphInternal = (graph, standard = {}, source) => {
79
- const resolved = createCircuitryValidationStandard(standard, graph && typeof graph === "object" && !Array.isArray(graph) ? graph : void 0);
85
+ var validateCircuitrySource = (graph, standard = {}) => {
86
+ const resolvedStandard = createCircuitryValidationStandard(standard);
80
87
  const errors = [];
81
88
  const issue = (code, message = code, path2) => errors.push({ code, message, ...path2 ? { path: path2 } : {} });
82
- if (!graph || typeof graph !== "object" || Array.isArray(graph)) return { ok: false, errors: [{ code: "invalid_graph", message: "invalid graph" }], standard: resolved };
89
+ if (!graph || typeof graph !== "object" || Array.isArray(graph)) {
90
+ return { ok: false, errors: [{ code: "invalid_graph", message: "invalid graph" }], standard: resolvedStandard };
91
+ }
83
92
  const g = graph;
84
- if (resolved.requireSpecVersion && g.circuitry !== resolved.version) issue("invalid_version", "invalid version", ["circuitry"]);
85
- if (source) {
86
- const raw = g;
87
- if (!g.resources || Object.keys(g.resources).length === 0) issue("missing_resources", "missing resources", ["resources"]);
88
- if (raw.agents) issue("forbidden_agents", "forbidden agents", ["agents"]);
89
- if (raw.inputs) issue("forbidden_inputs", "forbidden inputs", ["inputs"]);
90
- if (g.nodes?.length) issue("forbidden_nodes", "forbidden nodes", ["nodes"]);
91
- if (g.edges?.length) issue("forbidden_edges", "forbidden edges", ["edges"]);
92
- if (raw.links) issue("forbidden_links", "forbidden links", ["links"]);
93
+ if (resolvedStandard.requireSpecVersion && g.circuitry !== resolvedStandard.version) issue("invalid_version", "invalid version", ["circuitry"]);
94
+ if (!g.resources || Object.keys(g.resources).length === 0) issue("missing_resources", "missing resources", ["resources"]);
95
+ if (g.nodes) issue("forbidden_nodes", "forbidden nodes", ["nodes"]);
96
+ if (g.edges) issue("forbidden_edges", "forbidden edges", ["edges"]);
97
+ if (g.agents) issue("forbidden_agents", "forbidden agents", ["agents"]);
98
+ const resolved = resolveCircuitrySource(g);
99
+ const ids = new Set(Object.keys(resolved.resources || {}));
100
+ if (resolved.entry && !ids.has(resolved.entry)) issue("unknown_entry", "unknown entry", ["entry"]);
101
+ for (const [name, target] of Object.entries(resolved.entries || {})) {
102
+ if (!ids.has(target)) issue("unknown_entry", `unknown entry: ${name}`, ["entries", name]);
93
103
  }
94
- const rules = new Set(resolved.rules.map(ruleName));
95
- const normalized = normalizeCircuitryGraph(g);
96
- const ids = /* @__PURE__ */ new Set();
97
- for (const [index, node] of (normalized.nodes || []).entries()) {
98
- if (!node.id) {
99
- issue("missing_node_id", "missing node id", ["nodes", index, "id"]);
100
- continue;
104
+ for (const [id, resource] of Object.entries(resolved.resources || {})) {
105
+ if (!resource.type) issue("missing_resource_type", "missing resource type", ["resources", id, "type"]);
106
+ for (const input of resourceInputIds(resource)) {
107
+ if (!ids.has(input)) issue("unknown_resource_input", `unknown resource input: ${input}`, ["resources", id, "inputs"]);
108
+ if (input === id) issue("self_input", "self input", ["resources", id, "inputs"]);
101
109
  }
102
- if (ids.has(node.id)) issue("duplicate_node_id", "duplicate node id", ["nodes", index, "id"]);
103
- ids.add(node.id);
104
- if (!node.kind) issue("missing_node_kind", "missing node kind", ["nodes", index, "kind"]);
105
110
  }
106
- const adjacency = /* @__PURE__ */ new Map();
107
- const incoming = /* @__PURE__ */ new Map();
108
- for (const id of ids) {
109
- adjacency.set(id, []);
110
- incoming.set(id, 0);
111
+ for (const [name, output] of Object.entries(resolved.outputs || {})) {
112
+ const root = output.from.split(".")[0];
113
+ if (!ids.has(root)) issue("unknown_output_source", `unknown output source: ${output.from}`, ["outputs", name, "from"]);
111
114
  }
112
- for (const [index, edge] of (normalized.edges || []).entries()) {
113
- if (!edge.from || !edge.to) {
114
- issue("missing_edge_endpoint", "missing edge endpoint", ["edges", index]);
115
- continue;
116
- }
117
- if (rules.has("no-self-loops") && edge.from === edge.to) {
118
- issue("self_loop", "self loop", ["edges", index]);
119
- continue;
120
- }
121
- const ok = ids.has(edge.from) && ids.has(edge.to);
122
- if (rules.has("no-unknown-edge-endpoints")) {
123
- if (!ids.has(edge.from)) issue("unknown_edge_source", "unknown edge source", ["edges", index, "from"]);
124
- if (!ids.has(edge.to)) issue("unknown_edge_target", "unknown edge target", ["edges", index, "to"]);
125
- }
126
- if (ok) {
127
- adjacency.get(edge.from).push(edge.to);
128
- incoming.set(edge.to, (incoming.get(edge.to) || 0) + 1);
129
- }
130
- }
131
- if (rules.has("require-executable-inputs")) {
132
- const executable = new Set(resolved.executableKinds);
133
- for (const [index, node] of (normalized.nodes || []).entries()) {
134
- if (executable.has(node.kind) && (incoming.get(node.id) || 0) === 0) issue("missing_executable_input", "missing executable input", ["nodes", index]);
135
- }
136
- }
137
- if (rules.has("no-cycles")) {
138
- const visiting = /* @__PURE__ */ new Set();
139
- const visited = /* @__PURE__ */ new Set();
140
- const visit = (id) => {
141
- if (visiting.has(id)) return true;
142
- if (visited.has(id)) return false;
143
- visiting.add(id);
144
- for (const next of adjacency.get(id) || []) if (visit(next)) return true;
145
- visiting.delete(id);
146
- visited.add(id);
147
- return false;
148
- };
149
- for (const id of ids) if (visit(id)) {
150
- issue("cycle", "cycle");
151
- break;
152
- }
115
+ try {
116
+ toposortCircuitryResources(resolved);
117
+ } catch {
118
+ issue("cycle", "cycle");
153
119
  }
154
- return { ok: errors.length === 0, errors, standard: resolved };
120
+ return { ok: errors.length === 0, errors, standard: resolvedStandard };
121
+ };
122
+ var validateCircuitryExecutionGraphWithStandard = validateCircuitrySource;
123
+ var validateCircuitryGraph = (graph, standard = {}) => validateCircuitrySource(graph, standard).errors.map((error) => error.message);
124
+ var inspectCircuitrySource = (graph) => {
125
+ const resolved = resolveCircuitrySource(graph);
126
+ return {
127
+ version: String(resolved.circuitry),
128
+ title: resolved.title,
129
+ entry: resolved.entry,
130
+ entries: resolved.entries || {},
131
+ inputs: Object.keys(resolved.inputs || {}),
132
+ resources: Object.keys(resolved.resources || {}),
133
+ outputs: Object.keys(resolved.outputs || {}),
134
+ dependencies: getCircuitryDependencies(resolved)
135
+ };
155
136
  };
156
- var validateCircuitryGraphWithStandard = (graph, standard = {}) => validateCircuitryGraphInternal(graph, standard, true);
157
- var validateCircuitryExecutionGraphWithStandard = (graph, standard = {}) => validateCircuitryGraphInternal(graph, standard, false);
158
- var validateCircuitryGraph = (graph, standard = {}) => validateCircuitryGraphWithStandard(graph, standard).errors.map((error) => error.message);
159
137
 
160
138
  // src/yaml.ts
161
139
  var parseYamlData = (text) => {
@@ -186,10 +164,19 @@ import YAML3 from "yaml";
186
164
 
187
165
  // src/node.ts
188
166
  var defaultFilename = "graph.circuitry.yaml";
189
- var resourceInputs = (resource) => "inputs" in resource && Array.isArray(resource.inputs) ? resource.inputs : [];
190
- var withResourceInputs = (resource, inputs) => "inputs" in resource ? { ...resource, inputs } : resource;
167
+ var resourceInputs = (resource) => {
168
+ if (!("inputs" in resource) || !resource.inputs) return [];
169
+ return Array.isArray(resource.inputs) ? resource.inputs : Object.values(resource.inputs);
170
+ };
171
+ var withResourceInputs = (resource, inputs) => {
172
+ if (!("inputs" in resource) || !resource.inputs) return resource;
173
+ if (Array.isArray(resource.inputs)) return { ...resource, inputs };
174
+ const inputMap = resource.inputs;
175
+ const names = Object.keys(inputMap);
176
+ return { ...resource, inputs: Object.fromEntries(names.map((name, index) => [name, inputs[index] || inputMap[name]])) };
177
+ };
191
178
  var selectedImportResources = (imp, resources) => {
192
- if (imp.resources === "*") return Object.fromEntries(Object.keys(resources).map((id) => [id, id]));
179
+ if (!imp.resources || imp.resources === "*") return Object.fromEntries(Object.keys(resources).map((id) => [id, id]));
193
180
  if (Array.isArray(imp.resources)) return Object.fromEntries(imp.resources.map((id) => [id, id]));
194
181
  return imp.resources;
195
182
  };
@@ -227,7 +214,7 @@ var resolveCircuitryGraph = async (parsed, graphFile, text, standard, stack = []
227
214
  resources[id] = resource;
228
215
  origins[id] = graphFile;
229
216
  }
230
- const graph = normalizeCircuitryGraph({ ...parsed, imports: [], resources });
217
+ const graph = { ...parsed, circuitry: "0.4", imports: [], inputs: parsed.inputs || parsed.args || {}, resources };
231
218
  if (stack.length === 0) {
232
219
  const validation = validateCircuitryExecutionGraphWithStandard(graph, standard);
233
220
  if (validation.errors.length) {
@@ -247,27 +234,30 @@ var loadCircuitryGraphFile = async (filename2 = defaultFilename, standard, stack
247
234
  // src/cli.ts
248
235
  var [, , command, filename] = process.argv;
249
236
  var usage = () => {
250
- console.error("usage: circuitry <check|parse> <file.yaml>");
237
+ console.error("usage: circuitry <check|resolve|inspect|parse> <file.yaml|--stdin>");
251
238
  process.exit(2);
252
239
  };
240
+ var readInput = async (name) => name === "--stdin" ? await new Promise((resolve, reject) => {
241
+ let data = "";
242
+ process.stdin.on("data", (chunk) => data += chunk);
243
+ process.stdin.on("end", () => resolve(data));
244
+ process.stdin.on("error", reject);
245
+ }) : await readFile2(name, "utf8");
253
246
  if (!command || !filename) usage();
254
247
  try {
255
- if (command === "parse") {
248
+ if (command === "resolve" || command === "parse") {
256
249
  if (filename.endsWith(".circuitry.yaml") || filename.endsWith(".circuitry.yml")) {
257
250
  const loaded = await loadCircuitryGraphFile(filename);
258
- process.stdout.write(JSON.stringify({ graph: loaded.graph, origins: loaded.origins }));
251
+ process.stdout.write(JSON.stringify(loaded.graph, null, 2));
259
252
  } else {
260
- const text = filename === "--stdin" ? await new Promise((resolve, reject) => {
261
- let data = "";
262
- process.stdin.on("data", (chunk) => data += chunk);
263
- process.stdin.on("end", () => resolve(data));
264
- process.stdin.on("error", reject);
265
- }) : await readFile2(filename, "utf8");
266
- process.stdout.write(JSON.stringify(parseYamlData(text)));
253
+ process.stdout.write(JSON.stringify(parseYamlData(await readInput(filename)), null, 2));
267
254
  }
255
+ } else if (command === "inspect") {
256
+ const loaded = await loadCircuitryGraphFile(filename);
257
+ process.stdout.write(JSON.stringify(inspectCircuitrySource(loaded.graph), null, 2));
268
258
  } else if (command === "check") {
269
259
  const loaded = await loadCircuitryGraphFile(filename);
270
- const result = validateCircuitryExecutionGraphWithStandard(loaded.graph);
260
+ const result = validateCircuitrySource(loaded.graph);
271
261
  if (!result.ok) {
272
262
  console.error(result.errors.map((error) => error.message).join("\n"));
273
263
  process.exit(1);