@jsleekr/graft 5.8.0 → 6.0.1
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 +106 -130
- package/dist/codegen/generic-backend.d.ts +17 -0
- package/dist/codegen/generic-backend.js +156 -0
- package/dist/compiler.d.ts +4 -3
- package/dist/compiler.js +6 -6
- package/dist/formatter.d.ts +5 -0
- package/dist/formatter.js +170 -0
- package/dist/generator.d.ts +28 -0
- package/dist/generator.js +255 -0
- package/dist/index.js +220 -20
- package/dist/runner.d.ts +3 -0
- package/dist/runner.js +7 -0
- package/dist/runtime/feedback.d.ts +20 -0
- package/dist/runtime/feedback.js +152 -0
- package/dist/runtime/result-formatter.d.ts +13 -0
- package/dist/runtime/result-formatter.js +143 -0
- package/dist/runtime/result-validator.d.ts +28 -0
- package/dist/runtime/result-validator.js +129 -0
- package/dist/test-runner.d.ts +33 -0
- package/dist/test-runner.js +223 -0
- package/package.json +1 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { formatExpr } from './format.js';
|
|
2
|
+
export function formatProgram(program) {
|
|
3
|
+
const sections = [];
|
|
4
|
+
// Imports
|
|
5
|
+
for (const imp of program.imports) {
|
|
6
|
+
sections.push(formatImport(imp));
|
|
7
|
+
}
|
|
8
|
+
// Memories
|
|
9
|
+
for (const mem of program.memories) {
|
|
10
|
+
sections.push(formatMemory(mem));
|
|
11
|
+
}
|
|
12
|
+
// Contexts
|
|
13
|
+
for (const ctx of program.contexts) {
|
|
14
|
+
if (!ctx.sourceFile || ctx.sourceFile === program.contexts[0]?.sourceFile) {
|
|
15
|
+
sections.push(formatContext(ctx));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Nodes
|
|
19
|
+
for (const node of program.nodes) {
|
|
20
|
+
if (!node.sourceFile || node.sourceFile === program.nodes[0]?.sourceFile) {
|
|
21
|
+
sections.push(formatNode(node));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Edges
|
|
25
|
+
for (const edge of program.edges) {
|
|
26
|
+
sections.push(formatEdge(edge));
|
|
27
|
+
}
|
|
28
|
+
// Graphs
|
|
29
|
+
for (const graph of program.graphs) {
|
|
30
|
+
sections.push(formatGraph(graph));
|
|
31
|
+
}
|
|
32
|
+
return sections.join('\n\n') + '\n';
|
|
33
|
+
}
|
|
34
|
+
function formatImport(imp) {
|
|
35
|
+
return `import { ${imp.names.join(', ')} } from "${imp.path}"`;
|
|
36
|
+
}
|
|
37
|
+
function formatMemory(mem) {
|
|
38
|
+
const fields = mem.fields.map(f => ` ${f.name}: ${formatType(f.type)}`).join('\n');
|
|
39
|
+
return `memory ${mem.name}(max_tokens: ${formatTokens(mem.maxTokens)}, storage: ${mem.storage}) {\n${fields}\n}`;
|
|
40
|
+
}
|
|
41
|
+
function formatContext(ctx) {
|
|
42
|
+
const fields = ctx.fields.map(f => ` ${f.name}: ${formatType(f.type)}`).join('\n');
|
|
43
|
+
return `context ${ctx.name}(max_tokens: ${formatTokens(ctx.maxTokens)}) {\n${fields}\n}`;
|
|
44
|
+
}
|
|
45
|
+
function formatNode(node) {
|
|
46
|
+
const lines = [];
|
|
47
|
+
lines.push(`node ${node.name}(model: ${node.model}, budget: ${formatTokens(node.budgetIn)}/${formatTokens(node.budgetOut)}) {`);
|
|
48
|
+
// reads
|
|
49
|
+
if (node.reads.length > 0) {
|
|
50
|
+
lines.push(` reads: [${node.reads.map(formatContextRef).join(', ')}]`);
|
|
51
|
+
}
|
|
52
|
+
// tools
|
|
53
|
+
if (node.tools.length > 0) {
|
|
54
|
+
lines.push(` tools: [${node.tools.join(', ')}]`);
|
|
55
|
+
}
|
|
56
|
+
// writes
|
|
57
|
+
if (node.writes.length > 0) {
|
|
58
|
+
lines.push(` writes: [${node.writes.map(w => w.field ? `${w.memory}.${w.field}` : w.memory).join(', ')}]`);
|
|
59
|
+
}
|
|
60
|
+
// on_failure
|
|
61
|
+
if (node.onFailure) {
|
|
62
|
+
lines.push(` on_failure: ${formatFailure(node.onFailure)}`);
|
|
63
|
+
}
|
|
64
|
+
// produces
|
|
65
|
+
lines.push('');
|
|
66
|
+
const producesFields = node.produces.fields.map(f => ` ${f.name}: ${formatType(f.type)}`).join('\n');
|
|
67
|
+
lines.push(` produces ${node.produces.name} {`);
|
|
68
|
+
lines.push(producesFields);
|
|
69
|
+
lines.push(` }`);
|
|
70
|
+
lines.push('}');
|
|
71
|
+
return lines.join('\n');
|
|
72
|
+
}
|
|
73
|
+
function formatEdge(edge) {
|
|
74
|
+
if (edge.target.kind === 'conditional') {
|
|
75
|
+
const branches = edge.target.branches.map(b => {
|
|
76
|
+
if (b.condition) {
|
|
77
|
+
return ` when ${formatExpr(b.condition)} -> ${b.target}`;
|
|
78
|
+
}
|
|
79
|
+
return ` else -> ${b.target}`;
|
|
80
|
+
}).join('\n');
|
|
81
|
+
return `edge ${edge.source} -> {\n${branches}\n}`;
|
|
82
|
+
}
|
|
83
|
+
let line = `edge ${edge.source} -> ${edge.target.node}`;
|
|
84
|
+
if (edge.transforms.length > 0) {
|
|
85
|
+
line += '\n | ' + edge.transforms.map(formatTransform).join('\n | ');
|
|
86
|
+
}
|
|
87
|
+
return line;
|
|
88
|
+
}
|
|
89
|
+
function formatGraph(graph) {
|
|
90
|
+
const params = graph.params.length > 0
|
|
91
|
+
? `, ${graph.params.map(p => `${p.name}: ${p.type}${p.default !== undefined ? ` = ${p.default}` : ''}`).join(', ')}`
|
|
92
|
+
: '';
|
|
93
|
+
const flowStr = formatFlow(graph.flow, 2);
|
|
94
|
+
return `graph ${graph.name}(input: ${graph.input}, output: ${graph.output}, budget: ${formatTokens(graph.budget)}${params}) {\n${flowStr}\n}`;
|
|
95
|
+
}
|
|
96
|
+
function formatFlow(flow, indent) {
|
|
97
|
+
const pad = ' '.repeat(indent);
|
|
98
|
+
const parts = [];
|
|
99
|
+
for (const step of flow) {
|
|
100
|
+
switch (step.kind) {
|
|
101
|
+
case 'node':
|
|
102
|
+
parts.push(step.name);
|
|
103
|
+
break;
|
|
104
|
+
case 'parallel':
|
|
105
|
+
parts.push(`parallel { ${step.branches.join(' ')} }`);
|
|
106
|
+
break;
|
|
107
|
+
case 'foreach': {
|
|
108
|
+
const body = formatFlow(step.body, indent + 2);
|
|
109
|
+
parts.push(`foreach ${step.source}.${step.field} as ${step.binding} (max: ${step.maxIterations}) {\n${body}\n${pad}}`);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case 'let':
|
|
113
|
+
parts.push(`let ${step.name} = ${formatExpr(step.value)}`);
|
|
114
|
+
break;
|
|
115
|
+
case 'graph_call':
|
|
116
|
+
parts.push(`${step.name}(${step.args.map(a => `${a.name}: ${formatExpr(a.value)}`).join(', ')})`);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Join with -> for sequential steps, but done is implicit
|
|
121
|
+
return pad + parts.join('\n' + pad + '-> ') + '\n' + pad + '-> done';
|
|
122
|
+
}
|
|
123
|
+
function formatTransform(t) {
|
|
124
|
+
switch (t.type) {
|
|
125
|
+
case 'select': return `select(${t.fields.join(', ')})`;
|
|
126
|
+
case 'drop': return `drop(${t.field})`;
|
|
127
|
+
case 'compact': return 'compact';
|
|
128
|
+
case 'truncate': return `truncate(${t.tokens})`;
|
|
129
|
+
case 'filter': return `filter(${t.field}, ${formatExpr(t.condition)})`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function formatContextRef(ref) {
|
|
133
|
+
if (ref.field && ref.field.length === 1) {
|
|
134
|
+
return `${ref.context}.${ref.field[0]}`;
|
|
135
|
+
}
|
|
136
|
+
if (ref.field && ref.field.length > 1) {
|
|
137
|
+
return `${ref.context}.{${ref.field.join(', ')}}`;
|
|
138
|
+
}
|
|
139
|
+
return ref.context;
|
|
140
|
+
}
|
|
141
|
+
function formatFailure(f) {
|
|
142
|
+
switch (f.type) {
|
|
143
|
+
case 'retry': return `retry(${f.max})`;
|
|
144
|
+
case 'fallback': return `fallback(${f.node})`;
|
|
145
|
+
case 'retry_then_fallback': return `retry(${f.max}, fallback: ${f.node})`;
|
|
146
|
+
case 'skip': return 'skip';
|
|
147
|
+
case 'abort': return 'abort';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function formatType(t) {
|
|
151
|
+
switch (t.kind) {
|
|
152
|
+
case 'primitive': return t.name;
|
|
153
|
+
case 'primitive_range': return `${t.name}(${t.min}..${t.max})`;
|
|
154
|
+
case 'list': return `List<${formatType(t.element)}>`;
|
|
155
|
+
case 'map': return `Map<${formatType(t.key)}, ${formatType(t.value)}>`;
|
|
156
|
+
case 'optional': return `Optional<${formatType(t.inner)}>`;
|
|
157
|
+
case 'token_bounded': return `TokenBounded<${formatType(t.inner)}, ${t.max}>`;
|
|
158
|
+
case 'enum': return `enum(${t.values.join(', ')})`;
|
|
159
|
+
case 'struct': {
|
|
160
|
+
const fields = t.fields.map(f => `${f.name}: ${formatType(f.type)}`).join(', ');
|
|
161
|
+
return `${t.name} { ${fields} }`;
|
|
162
|
+
}
|
|
163
|
+
case 'domain': return t.name;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function formatTokens(n) {
|
|
167
|
+
if (n >= 1000 && n % 1000 === 0)
|
|
168
|
+
return `${n / 1000}k`;
|
|
169
|
+
return String(n);
|
|
170
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { GraftError } from './errors/diagnostics.js';
|
|
2
|
+
/** Function that calls the LLM. Injectable for testing. */
|
|
3
|
+
export type LLMCaller = (params: {
|
|
4
|
+
system: string;
|
|
5
|
+
userMessage: string;
|
|
6
|
+
}) => Promise<string>;
|
|
7
|
+
export interface GenerateOptions {
|
|
8
|
+
output?: string;
|
|
9
|
+
/** Override the LLM caller (for testing). */
|
|
10
|
+
llmCaller?: LLMCaller;
|
|
11
|
+
}
|
|
12
|
+
export interface GenerateResult {
|
|
13
|
+
source: string;
|
|
14
|
+
errors: GraftError[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Extract .gft source from an LLM response.
|
|
18
|
+
* Multi-strategy: (1) ```gft fence, (2) any fence, (3) bare response.
|
|
19
|
+
*/
|
|
20
|
+
export declare function extractGftSource(response: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Build the system prompt for .gft generation.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildSystemPrompt(): string;
|
|
25
|
+
/**
|
|
26
|
+
* Generate a .gft file from a natural language description.
|
|
27
|
+
*/
|
|
28
|
+
export declare function generateGft(description: string, options?: GenerateOptions): Promise<GenerateResult>;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* graft generate — natural language to .gft pipeline generation via Claude Code subprocess.
|
|
3
|
+
*/
|
|
4
|
+
import { spawnClaude } from './runtime/subprocess.js';
|
|
5
|
+
import { compileToProgram } from './compiler.js';
|
|
6
|
+
import { GraftError } from './errors/diagnostics.js';
|
|
7
|
+
const SYSTEM_PROMPT = `You are a Graft (.gft) pipeline generator. Generate valid .gft source code based on the user's description.
|
|
8
|
+
|
|
9
|
+
## .gft Syntax Reference
|
|
10
|
+
|
|
11
|
+
### Context Declaration
|
|
12
|
+
\`\`\`
|
|
13
|
+
context <Name>(max_tokens: <N>) {
|
|
14
|
+
<fieldName>: <Type>
|
|
15
|
+
...
|
|
16
|
+
}
|
|
17
|
+
\`\`\`
|
|
18
|
+
Types: String, Int, Float, Bool, List<T>, Map<K,V>, Optional<T>
|
|
19
|
+
|
|
20
|
+
### Memory Declaration
|
|
21
|
+
\`\`\`
|
|
22
|
+
memory <Name>(max_tokens: <N>, storage: file) {
|
|
23
|
+
<fieldName>: <Type>
|
|
24
|
+
...
|
|
25
|
+
}
|
|
26
|
+
\`\`\`
|
|
27
|
+
|
|
28
|
+
### Node Declaration
|
|
29
|
+
\`\`\`
|
|
30
|
+
node <Name>(model: <model>, budget: <in>/<out>) {
|
|
31
|
+
reads: [<ContextOrProducesName>, ...]
|
|
32
|
+
|
|
33
|
+
produces <OutputName> {
|
|
34
|
+
<fieldName>: <Type>
|
|
35
|
+
...
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
\`\`\`
|
|
39
|
+
Models: haiku, sonnet, opus. Budget format: input/output in token shorthand (e.g., 4k/2k, 8k/4k, 12k/6k).
|
|
40
|
+
|
|
41
|
+
### Edge Declaration
|
|
42
|
+
\`\`\`
|
|
43
|
+
// Direct edge with transforms
|
|
44
|
+
edge <Source> -> <Target>
|
|
45
|
+
| select(<field1>, <field2>)
|
|
46
|
+
| compact
|
|
47
|
+
| filter(<field> <op> <value>)
|
|
48
|
+
| truncate(<N>)
|
|
49
|
+
| drop(<field>)
|
|
50
|
+
|
|
51
|
+
// Conditional edge
|
|
52
|
+
edge <Source> -> {
|
|
53
|
+
when <condition> -> <Target>
|
|
54
|
+
when <condition> -> <Target>
|
|
55
|
+
otherwise -> <Target>
|
|
56
|
+
}
|
|
57
|
+
\`\`\`
|
|
58
|
+
|
|
59
|
+
### Graph Declaration
|
|
60
|
+
\`\`\`
|
|
61
|
+
graph <Name>(input: <Context>, output: <Produces>, budget: <N>) {
|
|
62
|
+
// Sequential
|
|
63
|
+
<Node1> -> <Node2> -> done
|
|
64
|
+
|
|
65
|
+
// Parallel
|
|
66
|
+
parallel {
|
|
67
|
+
<Node1>
|
|
68
|
+
<Node2>
|
|
69
|
+
}
|
|
70
|
+
-> <Node3> -> done
|
|
71
|
+
|
|
72
|
+
// Foreach
|
|
73
|
+
foreach(<Source>.<field> as <var>, max_iterations: <N>) {
|
|
74
|
+
<Node> -> done
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
\`\`\`
|
|
78
|
+
|
|
79
|
+
## Rules
|
|
80
|
+
- Output ONLY valid .gft code inside a \`\`\`gft fenced block
|
|
81
|
+
- Do NOT use import statements
|
|
82
|
+
- Every node must have model, budget (in/out), reads, and produces with typed fields
|
|
83
|
+
- Every graph must declare input, output, and budget
|
|
84
|
+
- Use realistic token budgets: haiku 4k/2k, sonnet 8k/4k, opus 12k/6k
|
|
85
|
+
- Add edge transforms (select, compact) to reduce token flow between nodes
|
|
86
|
+
- Add comments to explain the pipeline
|
|
87
|
+
|
|
88
|
+
## Complete Example
|
|
89
|
+
|
|
90
|
+
\`\`\`gft
|
|
91
|
+
// Adversarial Code Review Pipeline
|
|
92
|
+
// Security + Performance + Logic reviewers challenge each other,
|
|
93
|
+
// then a senior reviewer makes the final call.
|
|
94
|
+
|
|
95
|
+
context PullRequest(max_tokens: 3k) {
|
|
96
|
+
diff: String
|
|
97
|
+
description: String
|
|
98
|
+
files_changed: List<String>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
node SecurityReviewer(model: sonnet, budget: 6k/3k) {
|
|
102
|
+
reads: [PullRequest]
|
|
103
|
+
|
|
104
|
+
produces SecurityAnalysis {
|
|
105
|
+
vulnerabilities: List<String>
|
|
106
|
+
severity: String
|
|
107
|
+
recommendation: String
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
node LogicReviewer(model: sonnet, budget: 6k/3k) {
|
|
112
|
+
reads: [PullRequest]
|
|
113
|
+
|
|
114
|
+
produces LogicAnalysis {
|
|
115
|
+
bugs: List<String>
|
|
116
|
+
edge_cases: List<String>
|
|
117
|
+
correctness: String
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
node PerformanceReviewer(model: haiku, budget: 4k/2k) {
|
|
122
|
+
reads: [PullRequest]
|
|
123
|
+
|
|
124
|
+
produces PerfAnalysis {
|
|
125
|
+
hotspots: List<String>
|
|
126
|
+
complexity_concerns: List<String>
|
|
127
|
+
impact: String
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
node SeniorReviewer(model: opus, budget: 10k/5k) {
|
|
132
|
+
reads: [PullRequest, SecurityAnalysis, LogicAnalysis, PerfAnalysis]
|
|
133
|
+
|
|
134
|
+
produces FinalReview {
|
|
135
|
+
approved: Bool
|
|
136
|
+
blocking_issues: List<String>
|
|
137
|
+
suggestions: List<String>
|
|
138
|
+
summary: String
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
edge SecurityReviewer -> SeniorReviewer
|
|
143
|
+
| select(vulnerabilities, severity)
|
|
144
|
+
| compact
|
|
145
|
+
|
|
146
|
+
edge LogicReviewer -> SeniorReviewer
|
|
147
|
+
| select(bugs, edge_cases)
|
|
148
|
+
| compact
|
|
149
|
+
|
|
150
|
+
edge PerformanceReviewer -> SeniorReviewer
|
|
151
|
+
| select(hotspots, complexity_concerns)
|
|
152
|
+
| compact
|
|
153
|
+
|
|
154
|
+
graph AdversarialReview(input: PullRequest, output: FinalReview, budget: 40k) {
|
|
155
|
+
parallel {
|
|
156
|
+
SecurityReviewer
|
|
157
|
+
LogicReviewer
|
|
158
|
+
PerformanceReviewer
|
|
159
|
+
}
|
|
160
|
+
-> SeniorReviewer -> done
|
|
161
|
+
}
|
|
162
|
+
\`\`\`
|
|
163
|
+
`;
|
|
164
|
+
const MAX_RETRIES = 2;
|
|
165
|
+
/**
|
|
166
|
+
* Extract .gft source from an LLM response.
|
|
167
|
+
* Multi-strategy: (1) ```gft fence, (2) any fence, (3) bare response.
|
|
168
|
+
*/
|
|
169
|
+
export function extractGftSource(response) {
|
|
170
|
+
// Strategy 1: ```gft ... ```
|
|
171
|
+
const gftFence = response.match(/```gft\s*\n([\s\S]*?)```/);
|
|
172
|
+
if (gftFence)
|
|
173
|
+
return gftFence[1].trim();
|
|
174
|
+
// Strategy 2: any ``` ... ```
|
|
175
|
+
const anyFence = response.match(/```\s*\n([\s\S]*?)```/);
|
|
176
|
+
if (anyFence)
|
|
177
|
+
return anyFence[1].trim();
|
|
178
|
+
// Strategy 3: bare response (strip obvious markdown)
|
|
179
|
+
return response.replace(/^#+\s.*$/gm, '').trim();
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Build the system prompt for .gft generation.
|
|
183
|
+
*/
|
|
184
|
+
export function buildSystemPrompt() {
|
|
185
|
+
return SYSTEM_PROMPT;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Validate generated .gft source using the Graft compiler.
|
|
189
|
+
*/
|
|
190
|
+
function validateSource(source) {
|
|
191
|
+
try {
|
|
192
|
+
const result = compileToProgram(source, 'generated.gft');
|
|
193
|
+
if (!result.success)
|
|
194
|
+
return result.errors;
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
199
|
+
return [new GraftError(msg, { line: 0, column: 0, offset: 0 }, 'error')];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Default LLM caller using Claude Code subprocess.
|
|
204
|
+
*/
|
|
205
|
+
function createDefaultCaller() {
|
|
206
|
+
return async ({ system, userMessage }) => {
|
|
207
|
+
const prompt = `${system}\n\n---\n\nUser request: ${userMessage}`;
|
|
208
|
+
const result = await spawnClaude({
|
|
209
|
+
args: ['--print', '--output-format', 'text', prompt],
|
|
210
|
+
cwd: process.cwd(),
|
|
211
|
+
timeoutMs: 120_000,
|
|
212
|
+
});
|
|
213
|
+
if (result.exitCode !== 0) {
|
|
214
|
+
throw new Error(`Claude Code exited with code ${result.exitCode}.\n` +
|
|
215
|
+
`Make sure Claude Code is installed: npm install -g @anthropic-ai/claude-code\n` +
|
|
216
|
+
(result.stderr ? `stderr: ${result.stderr.slice(0, 500)}` : ''));
|
|
217
|
+
}
|
|
218
|
+
return result.stdout;
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Generate a .gft file from a natural language description.
|
|
223
|
+
*/
|
|
224
|
+
export async function generateGft(description, options) {
|
|
225
|
+
if (!description.trim()) {
|
|
226
|
+
throw new Error('Description cannot be empty.');
|
|
227
|
+
}
|
|
228
|
+
const callLLM = options?.llmCaller ?? createDefaultCaller();
|
|
229
|
+
let bestSource = '';
|
|
230
|
+
let bestErrors = [];
|
|
231
|
+
let userMessage = description;
|
|
232
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
233
|
+
const text = await callLLM({ system: SYSTEM_PROMPT, userMessage });
|
|
234
|
+
const source = extractGftSource(text);
|
|
235
|
+
const errors = validateSource(source);
|
|
236
|
+
if (errors.length === 0) {
|
|
237
|
+
return { source, errors: [] };
|
|
238
|
+
}
|
|
239
|
+
// Track best attempt (fewest errors)
|
|
240
|
+
if (bestSource === '' || errors.length < bestErrors.length) {
|
|
241
|
+
bestSource = source;
|
|
242
|
+
bestErrors = errors;
|
|
243
|
+
}
|
|
244
|
+
// Build retry prompt with error feedback
|
|
245
|
+
if (attempt < MAX_RETRIES) {
|
|
246
|
+
const errorMessages = errors
|
|
247
|
+
.map(e => e.format(source, 'generated.gft'))
|
|
248
|
+
.join('\n');
|
|
249
|
+
userMessage =
|
|
250
|
+
`${description}\n\n` +
|
|
251
|
+
`Your previous attempt had these compilation errors. Fix them:\n\n${errorMessages}`;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return { source: bestSource, errors: bestErrors };
|
|
255
|
+
}
|