@jsleekr/graft 5.7.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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +235 -0
  3. package/dist/analyzer/estimator.d.ts +33 -0
  4. package/dist/analyzer/estimator.js +273 -0
  5. package/dist/analyzer/graph-checker.d.ts +13 -0
  6. package/dist/analyzer/graph-checker.js +153 -0
  7. package/dist/analyzer/scope.d.ts +21 -0
  8. package/dist/analyzer/scope.js +324 -0
  9. package/dist/analyzer/types.d.ts +17 -0
  10. package/dist/analyzer/types.js +323 -0
  11. package/dist/codegen/agents.d.ts +2 -0
  12. package/dist/codegen/agents.js +109 -0
  13. package/dist/codegen/backend.d.ts +16 -0
  14. package/dist/codegen/backend.js +1 -0
  15. package/dist/codegen/claude-backend.d.ts +9 -0
  16. package/dist/codegen/claude-backend.js +47 -0
  17. package/dist/codegen/codegen.d.ts +10 -0
  18. package/dist/codegen/codegen.js +57 -0
  19. package/dist/codegen/hooks.d.ts +2 -0
  20. package/dist/codegen/hooks.js +165 -0
  21. package/dist/codegen/orchestration.d.ts +3 -0
  22. package/dist/codegen/orchestration.js +250 -0
  23. package/dist/codegen/settings.d.ts +36 -0
  24. package/dist/codegen/settings.js +87 -0
  25. package/dist/compiler.d.ts +21 -0
  26. package/dist/compiler.js +101 -0
  27. package/dist/constants.d.ts +9 -0
  28. package/dist/constants.js +13 -0
  29. package/dist/errors/diagnostics.d.ts +21 -0
  30. package/dist/errors/diagnostics.js +25 -0
  31. package/dist/format.d.ts +12 -0
  32. package/dist/format.js +46 -0
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.js +181 -0
  35. package/dist/lexer/lexer.d.ts +23 -0
  36. package/dist/lexer/lexer.js +268 -0
  37. package/dist/lexer/tokens.d.ts +96 -0
  38. package/dist/lexer/tokens.js +150 -0
  39. package/dist/lsp/features/code-actions.d.ts +7 -0
  40. package/dist/lsp/features/code-actions.js +58 -0
  41. package/dist/lsp/features/completions.d.ts +7 -0
  42. package/dist/lsp/features/completions.js +271 -0
  43. package/dist/lsp/features/definition.d.ts +3 -0
  44. package/dist/lsp/features/definition.js +32 -0
  45. package/dist/lsp/features/diagnostics.d.ts +4 -0
  46. package/dist/lsp/features/diagnostics.js +33 -0
  47. package/dist/lsp/features/hover.d.ts +7 -0
  48. package/dist/lsp/features/hover.js +88 -0
  49. package/dist/lsp/features/index.d.ts +9 -0
  50. package/dist/lsp/features/index.js +9 -0
  51. package/dist/lsp/features/references.d.ts +7 -0
  52. package/dist/lsp/features/references.js +53 -0
  53. package/dist/lsp/features/rename.d.ts +17 -0
  54. package/dist/lsp/features/rename.js +198 -0
  55. package/dist/lsp/features/symbols.d.ts +7 -0
  56. package/dist/lsp/features/symbols.js +74 -0
  57. package/dist/lsp/features/utils.d.ts +3 -0
  58. package/dist/lsp/features/utils.js +65 -0
  59. package/dist/lsp/features.d.ts +20 -0
  60. package/dist/lsp/features.js +513 -0
  61. package/dist/lsp/server.d.ts +2 -0
  62. package/dist/lsp/server.js +327 -0
  63. package/dist/parser/ast.d.ts +244 -0
  64. package/dist/parser/ast.js +10 -0
  65. package/dist/parser/parser.d.ts +95 -0
  66. package/dist/parser/parser.js +1175 -0
  67. package/dist/program-index.d.ts +21 -0
  68. package/dist/program-index.js +74 -0
  69. package/dist/resolver/resolver.d.ts +9 -0
  70. package/dist/resolver/resolver.js +136 -0
  71. package/dist/runner.d.ts +13 -0
  72. package/dist/runner.js +41 -0
  73. package/dist/runtime/executor.d.ts +56 -0
  74. package/dist/runtime/executor.js +285 -0
  75. package/dist/runtime/expr-eval.d.ts +3 -0
  76. package/dist/runtime/expr-eval.js +138 -0
  77. package/dist/runtime/flow-runner.d.ts +21 -0
  78. package/dist/runtime/flow-runner.js +230 -0
  79. package/dist/runtime/memory.d.ts +5 -0
  80. package/dist/runtime/memory.js +41 -0
  81. package/dist/runtime/prompt-builder.d.ts +12 -0
  82. package/dist/runtime/prompt-builder.js +66 -0
  83. package/dist/runtime/subprocess.d.ts +20 -0
  84. package/dist/runtime/subprocess.js +99 -0
  85. package/dist/runtime/token-tracker.d.ts +36 -0
  86. package/dist/runtime/token-tracker.js +56 -0
  87. package/dist/runtime/transforms.d.ts +2 -0
  88. package/dist/runtime/transforms.js +104 -0
  89. package/dist/types.d.ts +10 -0
  90. package/dist/types.js +1 -0
  91. package/dist/utils.d.ts +3 -0
  92. package/dist/utils.js +35 -0
  93. package/dist/version.d.ts +1 -0
  94. package/dist/version.js +11 -0
  95. package/package.json +70 -0
@@ -0,0 +1,138 @@
1
+ export function evaluateExpr(expr, outputs, variables, warnings) {
2
+ switch (expr.kind) {
3
+ case 'literal':
4
+ return expr.value;
5
+ case 'field_access': {
6
+ // Single-segment: check variables first (variable-first resolution per R2 ratchet v4.0-R21)
7
+ if (expr.segments.length === 1) {
8
+ if (variables?.has(expr.segments[0])) {
9
+ return variables.get(expr.segments[0]);
10
+ }
11
+ // Could be a node name with single output
12
+ return outputs.get(expr.segments[0]);
13
+ }
14
+ // Multi-segment: first segment is source name, rest are nested field access
15
+ const root = outputs.get(expr.segments[0]);
16
+ if (root === undefined || root === null)
17
+ return undefined;
18
+ let current = root;
19
+ for (let i = 1; i < expr.segments.length; i++) {
20
+ if (current === null || current === undefined || typeof current !== 'object')
21
+ return undefined;
22
+ current = current[expr.segments[i]];
23
+ }
24
+ return current;
25
+ }
26
+ case 'binary': {
27
+ // Short-circuit for logical and null coalescing operators
28
+ if (expr.op === '&&') {
29
+ const left = evaluateExpr(expr.left, outputs, variables, warnings);
30
+ return left ? evaluateExpr(expr.right, outputs, variables, warnings) : left;
31
+ }
32
+ if (expr.op === '||') {
33
+ const left = evaluateExpr(expr.left, outputs, variables, warnings);
34
+ return left ? left : evaluateExpr(expr.right, outputs, variables, warnings);
35
+ }
36
+ if (expr.op === '??') {
37
+ const left = evaluateExpr(expr.left, outputs, variables, warnings);
38
+ return left !== null && left !== undefined ? left : evaluateExpr(expr.right, outputs, variables, warnings);
39
+ }
40
+ const left = evaluateExpr(expr.left, outputs, variables, warnings);
41
+ const right = evaluateExpr(expr.right, outputs, variables, warnings);
42
+ switch (expr.op) {
43
+ case '+':
44
+ if (typeof left === 'string' || typeof right === 'string')
45
+ return String(left) + String(right);
46
+ return Number(left) + Number(right);
47
+ case '-': return Number(left) - Number(right);
48
+ case '*': return Number(left) * Number(right);
49
+ case '/': {
50
+ const divisor = Number(right);
51
+ if (divisor === 0) {
52
+ warnings?.push('division by zero in expression');
53
+ return 0;
54
+ }
55
+ return Number(left) / divisor;
56
+ }
57
+ case '%': {
58
+ const divisor = Number(right);
59
+ if (divisor === 0) {
60
+ warnings?.push('division by zero in expression');
61
+ return 0;
62
+ }
63
+ return Number(left) % divisor;
64
+ }
65
+ case '>': return Number(left) > Number(right);
66
+ case '<': return Number(left) < Number(right);
67
+ case '>=': return Number(left) >= Number(right);
68
+ case '<=': return Number(left) <= Number(right);
69
+ case '==': return left === right;
70
+ case '!=': return left !== right;
71
+ }
72
+ break;
73
+ }
74
+ case 'unary': {
75
+ const operand = evaluateExpr(expr.operand, outputs, variables, warnings);
76
+ if (expr.op === '-')
77
+ return -Number(operand);
78
+ if (expr.op === '!')
79
+ return !operand;
80
+ return operand;
81
+ }
82
+ case 'group':
83
+ return evaluateExpr(expr.inner, outputs, variables, warnings);
84
+ case 'call': {
85
+ const args = expr.args.map(a => evaluateExpr(a, outputs, variables, warnings));
86
+ switch (expr.name) {
87
+ case 'len': {
88
+ const val = args[0];
89
+ if (Array.isArray(val))
90
+ return val.length;
91
+ if (typeof val === 'string')
92
+ return val.length;
93
+ return 0;
94
+ }
95
+ case 'max': return Math.max(Number(args[0]), Number(args[1]));
96
+ case 'min': return Math.min(Number(args[0]), Number(args[1]));
97
+ case 'str': return typeof args[0] === 'object' && args[0] !== null ? JSON.stringify(args[0]) : String(args[0]);
98
+ case 'abs': return Math.abs(Number(args[0]));
99
+ case 'round': return Math.round(Number(args[0]));
100
+ case 'keys': {
101
+ const val = args[0];
102
+ if (typeof val === 'object' && val !== null && !Array.isArray(val)) {
103
+ return Object.keys(val);
104
+ }
105
+ return [];
106
+ }
107
+ default: return undefined;
108
+ }
109
+ }
110
+ case 'template': {
111
+ return expr.parts.map(part => {
112
+ if (part.kind === 'text')
113
+ return part.value;
114
+ const val = evaluateExpr(part.value, outputs, variables, warnings);
115
+ return String(val);
116
+ }).join('');
117
+ }
118
+ case 'conditional': {
119
+ const cond = evaluateExpr(expr.condition, outputs, variables, warnings);
120
+ return cond
121
+ ? evaluateExpr(expr.consequent, outputs, variables, warnings)
122
+ : evaluateExpr(expr.alternate, outputs, variables, warnings);
123
+ }
124
+ default: {
125
+ const _exhaustive = expr;
126
+ return _exhaustive;
127
+ }
128
+ }
129
+ }
130
+ export function resolveNestedField(segments, obj) {
131
+ let current = obj;
132
+ for (const seg of segments) {
133
+ if (current === null || current === undefined || typeof current !== 'object')
134
+ return undefined;
135
+ current = current[seg];
136
+ }
137
+ return current;
138
+ }
@@ -0,0 +1,21 @@
1
+ import { FlowNode, FailureStrategy, ConditionalBranch, Transform, GraphDecl } from '../parser/ast.js';
2
+ import { NodeResult } from './executor.js';
3
+ import { RuntimeState } from './prompt-builder.js';
4
+ import { MAX_CONDITIONAL_HOPS } from '../constants.js';
5
+ import { evaluateExpr, resolveNestedField } from './expr-eval.js';
6
+ export { MAX_CONDITIONAL_HOPS };
7
+ export { evaluateExpr, resolveNestedField };
8
+ export interface ConditionalEdgeInfo {
9
+ branches: ConditionalBranch[];
10
+ transforms: Transform[];
11
+ }
12
+ export interface FlowContext extends RuntimeState {
13
+ executeNode: (name: string) => Promise<NodeResult>;
14
+ getFailureStrategy?: (name: string) => FailureStrategy | undefined;
15
+ getConditionalEdge?: (sourceName: string) => ConditionalEdgeInfo | null;
16
+ variables?: Map<string, unknown>;
17
+ getGraphDecl?: (name: string) => GraphDecl | undefined;
18
+ }
19
+ export declare function applyFallbackAlias(name: string, result: NodeResult, ctx: FlowContext): void;
20
+ export declare function executeConditionalChain(flowNodeName: string, result: NodeResult, ctx: FlowContext, nodeResults: NodeResult[], errors: string[]): Promise<void>;
21
+ export declare function executeFlowNodes(flow: FlowNode[], nodeResults: NodeResult[], errors: string[], ctx: FlowContext): Promise<void>;
@@ -0,0 +1,230 @@
1
+ import { resolveField } from './prompt-builder.js';
2
+ import { applyTransforms } from './transforms.js';
3
+ import { MAX_CONDITIONAL_HOPS } from '../constants.js';
4
+ import { evaluateExpr, resolveNestedField } from './expr-eval.js';
5
+ export { MAX_CONDITIONAL_HOPS };
6
+ export { evaluateExpr, resolveNestedField };
7
+ async function executeWithFailureStrategy(name, nodeResults, errors, ctx) {
8
+ const result = await ctx.executeNode(name);
9
+ if (result.success)
10
+ return result;
11
+ const strategy = ctx.getFailureStrategy?.(name);
12
+ if (!strategy || strategy.type === 'abort') {
13
+ nodeResults.push(result);
14
+ errors.push(result.error ?? `Node ${name} failed`);
15
+ return null;
16
+ }
17
+ switch (strategy.type) {
18
+ case 'skip':
19
+ nodeResults.push(result);
20
+ return null;
21
+ case 'retry': {
22
+ for (let attempt = 1; attempt <= strategy.max; attempt++) {
23
+ const retry = await ctx.executeNode(name);
24
+ if (retry.success)
25
+ return retry;
26
+ }
27
+ nodeResults.push(result);
28
+ errors.push(result.error ?? `Node ${name} failed after ${strategy.max} retries`);
29
+ return null;
30
+ }
31
+ case 'fallback': {
32
+ const fallback = await ctx.executeNode(strategy.node);
33
+ if (fallback.success)
34
+ return fallback;
35
+ nodeResults.push(fallback);
36
+ errors.push(fallback.error ?? `Fallback node ${strategy.node} also failed`);
37
+ return null;
38
+ }
39
+ case 'retry_then_fallback': {
40
+ for (let attempt = 1; attempt <= strategy.max; attempt++) {
41
+ const retry = await ctx.executeNode(name);
42
+ if (retry.success)
43
+ return retry;
44
+ }
45
+ const fallback = await ctx.executeNode(strategy.node);
46
+ if (fallback.success)
47
+ return fallback;
48
+ nodeResults.push(fallback);
49
+ errors.push(fallback.error ?? `Fallback node ${strategy.node} failed after ${strategy.max} retries of ${name}`);
50
+ return null;
51
+ }
52
+ }
53
+ }
54
+ export function applyFallbackAlias(name, result, ctx) {
55
+ if (result.node !== name) {
56
+ ctx.outputs.set(name, result.output);
57
+ }
58
+ }
59
+ export async function executeConditionalChain(flowNodeName, result, ctx, nodeResults, errors) {
60
+ const visited = new Set();
61
+ visited.add(flowNodeName);
62
+ let currentNodeName = flowNodeName;
63
+ let currentOutput = result.output;
64
+ let hop = 0;
65
+ for (; hop < MAX_CONDITIONAL_HOPS; hop++) {
66
+ if (errors.length > 0)
67
+ break;
68
+ const edgeInfo = ctx.getConditionalEdge?.(currentNodeName);
69
+ if (!edgeInfo || !currentOutput || typeof currentOutput !== 'object')
70
+ break;
71
+ const { branches, transforms } = edgeInfo;
72
+ const output = currentOutput;
73
+ const outputMap = new Map(Object.entries(output));
74
+ let routedTo = null;
75
+ let elseBranch = null;
76
+ for (const branch of branches) {
77
+ if (!branch.condition) {
78
+ elseBranch = branch.target;
79
+ }
80
+ else if (!!evaluateExpr(branch.condition, outputMap, ctx.variables)) {
81
+ routedTo = branch.target;
82
+ break;
83
+ }
84
+ }
85
+ const target = routedTo ?? elseBranch;
86
+ if (!target || target === 'done')
87
+ break;
88
+ // Apply transforms after condition evaluation, before target execution
89
+ if (transforms.length > 0) {
90
+ const transformed = applyTransforms(currentOutput, transforms);
91
+ ctx.outputs.set(currentNodeName, transformed);
92
+ }
93
+ // Cycle detection
94
+ if (visited.has(target)) {
95
+ errors.push(`Conditional edge cycle detected: ${[...visited, target].join(' -> ')}`);
96
+ break;
97
+ }
98
+ visited.add(target);
99
+ const conditionalResult = await executeWithFailureStrategy(target, nodeResults, errors, ctx);
100
+ if (!conditionalResult)
101
+ break;
102
+ nodeResults.push(conditionalResult);
103
+ applyFallbackAlias(target, conditionalResult, ctx);
104
+ currentNodeName = target;
105
+ currentOutput = conditionalResult.output;
106
+ }
107
+ // Post-loop depth limit check: if we used all hops without breaking, the chain is too deep
108
+ if (hop >= MAX_CONDITIONAL_HOPS && errors.length === 0) {
109
+ errors.push(`Conditional chain from '${flowNodeName}' exceeded maximum depth of ${MAX_CONDITIONAL_HOPS} hops`);
110
+ }
111
+ }
112
+ export async function executeFlowNodes(flow, nodeResults, errors, ctx) {
113
+ for (const flowNode of flow) {
114
+ if (errors.length > 0)
115
+ break; // abort on failure
116
+ switch (flowNode.kind) {
117
+ case 'node': {
118
+ if (flowNode.name === 'done')
119
+ continue;
120
+ const result = await executeWithFailureStrategy(flowNode.name, nodeResults, errors, ctx);
121
+ if (result) {
122
+ nodeResults.push(result);
123
+ applyFallbackAlias(flowNode.name, result, ctx);
124
+ await executeConditionalChain(flowNode.name, result, ctx, nodeResults, errors);
125
+ }
126
+ break;
127
+ }
128
+ case 'parallel': {
129
+ const branches = flowNode.branches.filter(name => name !== 'done');
130
+ const promises = branches.map(name => executeWithFailureStrategy(name, nodeResults, errors, ctx)
131
+ .then(result => { if (result)
132
+ nodeResults.push(result); })
133
+ .catch((reason) => {
134
+ const nr = {
135
+ node: name,
136
+ output: null,
137
+ durationMs: 0,
138
+ success: false,
139
+ error: reason instanceof Error ? reason.message : String(reason),
140
+ };
141
+ nodeResults.push(nr);
142
+ errors.push(nr.error);
143
+ }));
144
+ await Promise.allSettled(promises);
145
+ break;
146
+ }
147
+ case 'foreach': {
148
+ const sourceData = ctx.outputs.get(flowNode.source);
149
+ if (sourceData === undefined) {
150
+ // Source node produced no output (skipped or not executed)
151
+ break;
152
+ }
153
+ const items = resolveField(sourceData, flowNode.field);
154
+ if (!Array.isArray(items)) {
155
+ errors.push(`foreach: ${flowNode.source}.${flowNode.field} is not an array`);
156
+ break;
157
+ }
158
+ const hadBinding = ctx.outputs.has(flowNode.binding);
159
+ const prevBinding = ctx.outputs.get(flowNode.binding);
160
+ const maxIter = Math.min(items.length, flowNode.maxIterations);
161
+ for (let i = 0; i < maxIter; i++) {
162
+ if (errors.length > 0)
163
+ break;
164
+ ctx.outputs.set(flowNode.binding, items[i]);
165
+ const errorsBefore = errors.length;
166
+ await executeFlowNodes(flowNode.body, nodeResults, errors, ctx);
167
+ // Annotate any new errors with iteration context
168
+ for (let e = errorsBefore; e < errors.length; e++) {
169
+ errors[e] = `${errors[e]} (foreach iteration ${i + 1} of ${maxIter})`;
170
+ }
171
+ }
172
+ // Restore or clean up binding
173
+ if (hadBinding) {
174
+ ctx.outputs.set(flowNode.binding, prevBinding);
175
+ }
176
+ else {
177
+ ctx.outputs.delete(flowNode.binding);
178
+ }
179
+ break;
180
+ }
181
+ case 'let': {
182
+ if (!ctx.variables)
183
+ ctx.variables = new Map();
184
+ const value = evaluateExpr(flowNode.value, ctx.outputs, ctx.variables);
185
+ ctx.variables.set(flowNode.name, value);
186
+ break;
187
+ }
188
+ case 'graph_call': {
189
+ const graphDecl = ctx.getGraphDecl?.(flowNode.name);
190
+ if (!graphDecl) {
191
+ errors.push(`Graph '${flowNode.name}' not found`);
192
+ break;
193
+ }
194
+ // Build child variable map from params
195
+ const childVars = new Map();
196
+ const paramMap = new Map(graphDecl.params.map(p => [p.name, p]));
197
+ for (const arg of flowNode.args) {
198
+ const val = evaluateExpr(arg.value, ctx.outputs, ctx.variables);
199
+ childVars.set(arg.name, val);
200
+ }
201
+ // Fill defaults for missing params
202
+ for (const param of graphDecl.params) {
203
+ if (!childVars.has(param.name) && param.default !== undefined) {
204
+ childVars.set(param.name, param.default);
205
+ }
206
+ }
207
+ // Execute child graph with its own variable scope and isolated outputs
208
+ const childCtx = {
209
+ ...ctx,
210
+ variables: childVars,
211
+ outputs: new Map(ctx.outputs),
212
+ };
213
+ const beforeCount = nodeResults.length;
214
+ await executeFlowNodes(graphDecl.flow, nodeResults, errors, childCtx);
215
+ // Capture last node output from child execution and store under graph call name
216
+ if (nodeResults.length > beforeCount) {
217
+ const lastResult = nodeResults[nodeResults.length - 1];
218
+ if (lastResult.success) {
219
+ ctx.outputs.set(flowNode.name, lastResult.output);
220
+ }
221
+ }
222
+ break;
223
+ }
224
+ default: {
225
+ const _exhaustive = flowNode;
226
+ throw new Error(`Unhandled FlowNode kind: ${_exhaustive.kind}`);
227
+ }
228
+ }
229
+ }
230
+ }
@@ -0,0 +1,5 @@
1
+ import { MemoryDecl } from '../parser/ast.js';
2
+ export declare function loadMemory(memoryDir: string, name: string, options?: {
3
+ verbose?: boolean;
4
+ }): Record<string, unknown> | null;
5
+ export declare function saveMemory(memoryDir: string, mem: MemoryDecl, nodeOutput: unknown, fields?: string[]): void;
@@ -0,0 +1,41 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ export function loadMemory(memoryDir, name, options) {
4
+ const filePath = path.join(memoryDir, `${name.toLowerCase()}.json`);
5
+ if (!fs.existsSync(filePath))
6
+ return null;
7
+ try {
8
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
9
+ }
10
+ catch {
11
+ if (options?.verbose) {
12
+ console.warn(`[MEMORY] Warning: ${filePath} exists but contains invalid JSON — treating as empty`);
13
+ }
14
+ return null;
15
+ }
16
+ }
17
+ export function saveMemory(memoryDir, mem, nodeOutput, fields) {
18
+ fs.mkdirSync(memoryDir, { recursive: true });
19
+ const filePath = path.join(memoryDir, `${mem.name.toLowerCase()}.json`);
20
+ let current = {};
21
+ if (fs.existsSync(filePath)) {
22
+ try {
23
+ current = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
24
+ }
25
+ catch {
26
+ current = {};
27
+ }
28
+ }
29
+ if (typeof nodeOutput === 'object' && nodeOutput !== null) {
30
+ const output = nodeOutput;
31
+ const targetFields = fields
32
+ ? mem.fields.filter(f => fields.includes(f.name))
33
+ : mem.fields;
34
+ for (const field of targetFields) {
35
+ if (field.name in output) {
36
+ current[field.name] = output[field.name];
37
+ }
38
+ }
39
+ }
40
+ fs.writeFileSync(filePath, JSON.stringify(current, null, 2));
41
+ }
@@ -0,0 +1,12 @@
1
+ import { NodeDecl } from '../parser/ast.js';
2
+ export interface RuntimeState {
3
+ outputs: Map<string, unknown>;
4
+ input: Record<string, unknown>;
5
+ }
6
+ export interface PromptContext extends RuntimeState {
7
+ graphInputName: string;
8
+ }
9
+ export declare function resolveField(data: unknown, field: string): unknown;
10
+ export declare function buildPrompt(nodeDecl: NodeDecl, ctx: PromptContext): string;
11
+ export declare function buildContextSection(nodeDecl: NodeDecl, ctx: PromptContext): string;
12
+ export declare function generateMockOutput(nodeDecl: NodeDecl): Record<string, unknown>;
@@ -0,0 +1,66 @@
1
+ import { fieldsToJsonExample } from '../utils.js';
2
+ export function resolveField(data, field) {
3
+ if (data === null || data === undefined || typeof data !== 'object')
4
+ return undefined;
5
+ return data[field];
6
+ }
7
+ export function buildPrompt(nodeDecl, ctx) {
8
+ const jsonSchema = fieldsToJsonExample(nodeDecl.produces.fields);
9
+ const contextSection = buildContextSection(nodeDecl, ctx);
10
+ return `# ${nodeDecl.name} Agent
11
+
12
+ ## Task
13
+ You are the ${nodeDecl.name} node in a Graft pipeline.
14
+
15
+ ## Input Context
16
+ ${contextSection}
17
+
18
+ ## Output Contract
19
+ Produce JSON output matching this schema:
20
+ \`\`\`json
21
+ ${JSON.stringify(jsonSchema, null, 2)}
22
+ \`\`\`
23
+
24
+ ## Rules
25
+ - Output ONLY valid JSON. No explanations, no markdown, no code fences.
26
+ - Stay within ${nodeDecl.budgetOut} output tokens.
27
+ `;
28
+ }
29
+ export function buildContextSection(nodeDecl, ctx) {
30
+ const sections = [];
31
+ for (const ref of nodeDecl.reads) {
32
+ const contextData = ctx.outputs.get(ref.context);
33
+ if (contextData !== undefined) {
34
+ if (ref.field) {
35
+ for (const f of ref.field) {
36
+ const fieldVal = resolveField(contextData, f);
37
+ sections.push(`### ${ref.context}.${f}\n\`\`\`json\n${JSON.stringify(fieldVal, null, 2)}\n\`\`\``);
38
+ }
39
+ }
40
+ else {
41
+ sections.push(`### ${ref.context}\n\`\`\`json\n${JSON.stringify(contextData, null, 2)}\n\`\`\``);
42
+ }
43
+ }
44
+ else {
45
+ // Check if input matches the context name
46
+ if (ref.context === ctx.graphInputName) {
47
+ if (ref.field) {
48
+ for (const f of ref.field) {
49
+ const fieldVal = resolveField(ctx.input, f);
50
+ sections.push(`### ${ref.context}.${f}\n\`\`\`json\n${JSON.stringify(fieldVal, null, 2)}\n\`\`\``);
51
+ }
52
+ }
53
+ else {
54
+ sections.push(`### ${ref.context}\n\`\`\`json\n${JSON.stringify(ctx.input, null, 2)}\n\`\`\``);
55
+ }
56
+ }
57
+ else {
58
+ sections.push(`### ${ref.context}\nNo data available.`);
59
+ }
60
+ }
61
+ }
62
+ return sections.length > 0 ? sections.join('\n\n') : 'No external context required.';
63
+ }
64
+ export function generateMockOutput(nodeDecl) {
65
+ return fieldsToJsonExample(nodeDecl.produces.fields);
66
+ }
@@ -0,0 +1,20 @@
1
+ export interface SpawnResult {
2
+ stdout: string;
3
+ stderr: string;
4
+ exitCode: number | null;
5
+ }
6
+ export interface SpawnOptions {
7
+ args: string[];
8
+ cwd: string;
9
+ timeoutMs: number;
10
+ }
11
+ export declare function spawnClaude(options: SpawnOptions): Promise<SpawnResult>;
12
+ export interface TokenUsage {
13
+ inputTokens: number;
14
+ outputTokens: number;
15
+ }
16
+ export declare function parseCLIOutput(stdout: string): {
17
+ content: unknown;
18
+ tokenUsage?: TokenUsage;
19
+ };
20
+ export declare function extractJson(stdout: string): unknown;
@@ -0,0 +1,99 @@
1
+ import { spawn } from 'node:child_process';
2
+ export function spawnClaude(options) {
3
+ return new Promise((resolve, reject) => {
4
+ const child = spawn('claude', options.args, {
5
+ cwd: options.cwd,
6
+ stdio: ['pipe', 'pipe', 'pipe'],
7
+ shell: process.platform === 'win32',
8
+ });
9
+ child.stdin?.end(); // Critical: close stdin immediately
10
+ const stdoutChunks = [];
11
+ const stderrChunks = [];
12
+ child.stdout?.on('data', (chunk) => stdoutChunks.push(chunk));
13
+ child.stderr?.on('data', (chunk) => stderrChunks.push(chunk));
14
+ let timedOut = false;
15
+ const timer = setTimeout(() => {
16
+ timedOut = true;
17
+ child.kill('SIGTERM');
18
+ setTimeout(() => child.kill('SIGKILL'), 5000);
19
+ }, options.timeoutMs);
20
+ child.on('error', (err) => {
21
+ clearTimeout(timer);
22
+ if (err.code === 'ENOENT') {
23
+ reject(new Error('Claude CLI not found. Install: npm install -g @anthropic-ai/claude-code'));
24
+ }
25
+ else {
26
+ reject(err);
27
+ }
28
+ });
29
+ child.on('close', (code) => {
30
+ clearTimeout(timer);
31
+ const stdout = Buffer.concat(stdoutChunks).toString('utf-8');
32
+ const stderr = Buffer.concat(stderrChunks).toString('utf-8');
33
+ if (timedOut) {
34
+ reject(new Error(`Claude subprocess timed out after ${options.timeoutMs / 1000}s. stderr: ${stderr.slice(0, 500)}`));
35
+ return;
36
+ }
37
+ resolve({ stdout, stderr, exitCode: code });
38
+ });
39
+ });
40
+ }
41
+ export function parseCLIOutput(stdout) {
42
+ const trimmed = stdout.trim();
43
+ try {
44
+ const parsed = JSON.parse(trimmed);
45
+ // Only treat as CLI envelope if result AND a metadata field present
46
+ if (typeof parsed === 'object' && parsed !== null && 'result' in parsed &&
47
+ ('usage' in parsed || 'model' in parsed || 'cost_usd' in parsed)) {
48
+ let tokenUsage;
49
+ if (parsed.usage && typeof parsed.usage === 'object' &&
50
+ typeof parsed.usage.input_tokens === 'number' &&
51
+ typeof parsed.usage.output_tokens === 'number') {
52
+ tokenUsage = {
53
+ inputTokens: parsed.usage.input_tokens,
54
+ outputTokens: parsed.usage.output_tokens,
55
+ };
56
+ }
57
+ let content;
58
+ if (typeof parsed.result === 'string') {
59
+ try {
60
+ content = JSON.parse(parsed.result);
61
+ }
62
+ catch {
63
+ content = parsed.result;
64
+ }
65
+ }
66
+ else {
67
+ content = parsed.result;
68
+ }
69
+ return { content, tokenUsage };
70
+ }
71
+ }
72
+ catch { /* not JSON */ }
73
+ const content = extractJson(stdout);
74
+ return { content, tokenUsage: undefined };
75
+ }
76
+ export function extractJson(stdout) {
77
+ const trimmed = stdout.trim();
78
+ try {
79
+ return JSON.parse(trimmed);
80
+ }
81
+ catch { /* fallback */ }
82
+ const firstBrace = trimmed.indexOf('{');
83
+ const lastBrace = trimmed.lastIndexOf('}');
84
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
85
+ try {
86
+ return JSON.parse(trimmed.slice(firstBrace, lastBrace + 1));
87
+ }
88
+ catch { /* fallback */ }
89
+ }
90
+ const firstBracket = trimmed.indexOf('[');
91
+ const lastBracket = trimmed.lastIndexOf(']');
92
+ if (firstBracket !== -1 && lastBracket > firstBracket) {
93
+ try {
94
+ return JSON.parse(trimmed.slice(firstBracket, lastBracket + 1));
95
+ }
96
+ catch { /* fallback */ }
97
+ }
98
+ throw new Error(`Failed to parse JSON from claude output. First 200 chars: ${trimmed.slice(0, 200)}`);
99
+ }
@@ -0,0 +1,36 @@
1
+ export interface TokenLogEntry {
2
+ nodeName: string;
3
+ estimated: number;
4
+ actual: number | undefined;
5
+ cumulative: number;
6
+ timestamp: string;
7
+ }
8
+ export declare class TokenTracker {
9
+ private budget;
10
+ private consumed;
11
+ private entries;
12
+ private logPath;
13
+ constructor(budget: number, logPath?: string | null);
14
+ record(nodeName: string, usage: {
15
+ inputTokens: number;
16
+ outputTokens: number;
17
+ } | undefined, estimated: {
18
+ in: number;
19
+ out: number;
20
+ }): void;
21
+ get fraction(): number;
22
+ get isWarning(): boolean;
23
+ get isCritical(): boolean;
24
+ get totalConsumed(): number;
25
+ getEntries(): TokenLogEntry[];
26
+ getSummary(): {
27
+ budget: number;
28
+ consumed: number;
29
+ fraction: number;
30
+ perNode: Array<{
31
+ node: string;
32
+ actual?: number;
33
+ estimated: number;
34
+ }>;
35
+ };
36
+ }