@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.
- package/LICENSE +21 -0
- package/README.md +235 -0
- package/dist/analyzer/estimator.d.ts +33 -0
- package/dist/analyzer/estimator.js +273 -0
- package/dist/analyzer/graph-checker.d.ts +13 -0
- package/dist/analyzer/graph-checker.js +153 -0
- package/dist/analyzer/scope.d.ts +21 -0
- package/dist/analyzer/scope.js +324 -0
- package/dist/analyzer/types.d.ts +17 -0
- package/dist/analyzer/types.js +323 -0
- package/dist/codegen/agents.d.ts +2 -0
- package/dist/codegen/agents.js +109 -0
- package/dist/codegen/backend.d.ts +16 -0
- package/dist/codegen/backend.js +1 -0
- package/dist/codegen/claude-backend.d.ts +9 -0
- package/dist/codegen/claude-backend.js +47 -0
- package/dist/codegen/codegen.d.ts +10 -0
- package/dist/codegen/codegen.js +57 -0
- package/dist/codegen/hooks.d.ts +2 -0
- package/dist/codegen/hooks.js +165 -0
- package/dist/codegen/orchestration.d.ts +3 -0
- package/dist/codegen/orchestration.js +250 -0
- package/dist/codegen/settings.d.ts +36 -0
- package/dist/codegen/settings.js +87 -0
- package/dist/compiler.d.ts +21 -0
- package/dist/compiler.js +101 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +13 -0
- package/dist/errors/diagnostics.d.ts +21 -0
- package/dist/errors/diagnostics.js +25 -0
- package/dist/format.d.ts +12 -0
- package/dist/format.js +46 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +181 -0
- package/dist/lexer/lexer.d.ts +23 -0
- package/dist/lexer/lexer.js +268 -0
- package/dist/lexer/tokens.d.ts +96 -0
- package/dist/lexer/tokens.js +150 -0
- package/dist/lsp/features/code-actions.d.ts +7 -0
- package/dist/lsp/features/code-actions.js +58 -0
- package/dist/lsp/features/completions.d.ts +7 -0
- package/dist/lsp/features/completions.js +271 -0
- package/dist/lsp/features/definition.d.ts +3 -0
- package/dist/lsp/features/definition.js +32 -0
- package/dist/lsp/features/diagnostics.d.ts +4 -0
- package/dist/lsp/features/diagnostics.js +33 -0
- package/dist/lsp/features/hover.d.ts +7 -0
- package/dist/lsp/features/hover.js +88 -0
- package/dist/lsp/features/index.d.ts +9 -0
- package/dist/lsp/features/index.js +9 -0
- package/dist/lsp/features/references.d.ts +7 -0
- package/dist/lsp/features/references.js +53 -0
- package/dist/lsp/features/rename.d.ts +17 -0
- package/dist/lsp/features/rename.js +198 -0
- package/dist/lsp/features/symbols.d.ts +7 -0
- package/dist/lsp/features/symbols.js +74 -0
- package/dist/lsp/features/utils.d.ts +3 -0
- package/dist/lsp/features/utils.js +65 -0
- package/dist/lsp/features.d.ts +20 -0
- package/dist/lsp/features.js +513 -0
- package/dist/lsp/server.d.ts +2 -0
- package/dist/lsp/server.js +327 -0
- package/dist/parser/ast.d.ts +244 -0
- package/dist/parser/ast.js +10 -0
- package/dist/parser/parser.d.ts +95 -0
- package/dist/parser/parser.js +1175 -0
- package/dist/program-index.d.ts +21 -0
- package/dist/program-index.js +74 -0
- package/dist/resolver/resolver.d.ts +9 -0
- package/dist/resolver/resolver.js +136 -0
- package/dist/runner.d.ts +13 -0
- package/dist/runner.js +41 -0
- package/dist/runtime/executor.d.ts +56 -0
- package/dist/runtime/executor.js +285 -0
- package/dist/runtime/expr-eval.d.ts +3 -0
- package/dist/runtime/expr-eval.js +138 -0
- package/dist/runtime/flow-runner.d.ts +21 -0
- package/dist/runtime/flow-runner.js +230 -0
- package/dist/runtime/memory.d.ts +5 -0
- package/dist/runtime/memory.js +41 -0
- package/dist/runtime/prompt-builder.d.ts +12 -0
- package/dist/runtime/prompt-builder.js +66 -0
- package/dist/runtime/subprocess.d.ts +20 -0
- package/dist/runtime/subprocess.js +99 -0
- package/dist/runtime/token-tracker.d.ts +36 -0
- package/dist/runtime/token-tracker.js +56 -0
- package/dist/runtime/transforms.d.ts +2 -0
- package/dist/runtime/transforms.js +104 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +35 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +11 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 JSLEEKR
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
[](https://www.npmjs.com/package/@graft-lang/graft)
|
|
2
|
+
[](https://github.com/JSLEEKR/graft/actions/workflows/ci.yml)
|
|
3
|
+
[](https://nodejs.org)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
|
|
6
|
+
# Graft
|
|
7
|
+
|
|
8
|
+
**A graph-native language for AI agent pipelines.**
|
|
9
|
+
|
|
10
|
+
Graft compiles `.gft` files into [Claude Code](https://docs.anthropic.com/en/docs/claude-code) harness structures. Define your multi-agent pipeline declaratively, and the compiler generates the agents, hooks, orchestration, and settings — with compile-time token budget analysis.
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g @graft-lang/graft
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Write a pipeline (`hello.gft`):
|
|
19
|
+
|
|
20
|
+
```graft
|
|
21
|
+
context UserRequest(max_tokens: 500) {
|
|
22
|
+
question: String
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
node Researcher(model: sonnet, budget: 2k/1k) {
|
|
26
|
+
reads: [UserRequest]
|
|
27
|
+
produces Research {
|
|
28
|
+
findings: List<String>
|
|
29
|
+
confidence: Float(0..1)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
node Writer(model: haiku, budget: 1500/800) {
|
|
34
|
+
reads: [Research.findings]
|
|
35
|
+
produces Answer { response: String }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
edge Researcher -> Writer | select(findings) | compact
|
|
39
|
+
|
|
40
|
+
graph SimpleQA(input: UserRequest, output: Answer, budget: 6k) {
|
|
41
|
+
Researcher -> Writer -> done
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Compile it:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
$ graft compile hello.gft
|
|
49
|
+
|
|
50
|
+
✓ Parse OK
|
|
51
|
+
✓ Scope check OK
|
|
52
|
+
✓ Type check OK
|
|
53
|
+
✓ Token analysis:
|
|
54
|
+
Researcher in ~ 500 out ~ 1,000
|
|
55
|
+
Writer in ~ 63 out ~ 800
|
|
56
|
+
Best path: 2,363 tokens ✓ within budget (6,000)
|
|
57
|
+
|
|
58
|
+
Generated:
|
|
59
|
+
.claude/agents/researcher.md ← agent definition
|
|
60
|
+
.claude/agents/writer.md ← agent definition
|
|
61
|
+
.claude/hooks/researcher-to-writer.js ← edge transform (Node.js)
|
|
62
|
+
.claude/CLAUDE.md ← orchestration plan
|
|
63
|
+
.claude/settings.json ← model routing + hooks
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Open the project directory in Claude Code — it picks up the generated `.claude/` structure and runs the pipeline.
|
|
67
|
+
|
|
68
|
+
## What Does Graft Generate?
|
|
69
|
+
|
|
70
|
+
| Graft Source | Generated Output | Purpose |
|
|
71
|
+
|-------------|-----------------|---------|
|
|
72
|
+
| `node` | `.claude/agents/*.md` | Agent with model, tools, output schema |
|
|
73
|
+
| `edge \| transform` | `.claude/hooks/*.js` | Node.js data transform between nodes |
|
|
74
|
+
| `graph` | `.claude/CLAUDE.md` | Step-by-step orchestration plan |
|
|
75
|
+
| `memory` | `.graft/memory/*.json` | Persistent state across runs |
|
|
76
|
+
| config | `.claude/settings.json` | Model routing, budget, hook registration |
|
|
77
|
+
|
|
78
|
+
## Why Graft?
|
|
79
|
+
|
|
80
|
+
Multi-agent systems waste tokens passing full context between agents. Graft fixes this:
|
|
81
|
+
|
|
82
|
+
- **Edge transforms** extract only what the next agent needs (`select`, `drop`, `compact`, `filter`)
|
|
83
|
+
- **Compile-time token analysis** catches budget overruns before you spend API credits
|
|
84
|
+
- **Typed output schemas** enforce structured JSON communication between agents
|
|
85
|
+
- **Explicit `reads`** declarations prevent context leaks — the compiler verifies scope
|
|
86
|
+
|
|
87
|
+
## Language Features
|
|
88
|
+
|
|
89
|
+
### Contexts and Nodes
|
|
90
|
+
|
|
91
|
+
```graft
|
|
92
|
+
context TaskSpec(max_tokens: 1k) {
|
|
93
|
+
description: String
|
|
94
|
+
criteria: List<String>
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
node Analyzer(model: sonnet, budget: 5k/2k) {
|
|
98
|
+
reads: [TaskSpec]
|
|
99
|
+
tools: [file_read, terminal]
|
|
100
|
+
on_failure: retry(2)
|
|
101
|
+
produces AnalysisResult {
|
|
102
|
+
issues: List<Issue { file: FilePath, severity: enum(low, medium, high) }>
|
|
103
|
+
risk_score: Float(0..1)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Edge Transforms
|
|
109
|
+
|
|
110
|
+
```graft
|
|
111
|
+
edge Analyzer -> Reviewer
|
|
112
|
+
| filter(issues, severity >= medium)
|
|
113
|
+
| drop(reasoning_trace)
|
|
114
|
+
| compact
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Flow Control
|
|
118
|
+
|
|
119
|
+
```graft
|
|
120
|
+
graph Pipeline(input: TaskSpec, output: Report, budget: 35k) {
|
|
121
|
+
Planner
|
|
122
|
+
-> parallel { SecurityReviewer PerformanceReviewer StyleReviewer }
|
|
123
|
+
-> Aggregator -> done
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Also supports: `foreach`, `let` variables with expressions, parameterized sub-graphs, conditional edge routing.
|
|
128
|
+
|
|
129
|
+
### Imports and Memory
|
|
130
|
+
|
|
131
|
+
```graft
|
|
132
|
+
import { UserMessage, SystemConfig } from "./shared.gft"
|
|
133
|
+
|
|
134
|
+
memory ConversationLog(max_tokens: 2k, storage: file) {
|
|
135
|
+
turns: List<Turn { role: String, content: String }>
|
|
136
|
+
summary: Optional<String>
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
node Responder(model: sonnet, budget: 4k/2k) {
|
|
140
|
+
reads: [UserMessage, SystemConfig, ConversationLog]
|
|
141
|
+
writes: [ConversationLog.turns]
|
|
142
|
+
produces Response { reply: String }
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Expressions
|
|
147
|
+
|
|
148
|
+
```graft
|
|
149
|
+
A -> let score = A.risk_score * 2
|
|
150
|
+
-> let label = "Found ${len(A.items)} items"
|
|
151
|
+
-> let safe = score >= 50 && score <= 100
|
|
152
|
+
-> let fallback = A.name ?? "unknown"
|
|
153
|
+
-> B -> done
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Supported: arithmetic (`+`, `-`, `*`, `/`, `%`), comparison (`>`, `>=`, `==`, `!=`), logical (`&&`, `||`), null coalescing (`??`), string interpolation, builtins (`len`, `max`, `min`, `abs`, `round`, `keys`, `str`).
|
|
157
|
+
|
|
158
|
+
### Type System
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
String, Int, Float, Float(0..1), Bool // primitives
|
|
162
|
+
List<T>, Map<K, V>, Optional<T> // collections
|
|
163
|
+
TokenBounded<String, 100> // token-bounded types
|
|
164
|
+
enum(low, medium, high) // inline enums
|
|
165
|
+
Issue { file: FilePath, severity: ... } // inline structs
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## CLI
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
graft compile <file.gft> [--out-dir <dir>] # Compile to .claude/ structure
|
|
172
|
+
graft check <file.gft> # Parse + analyze only
|
|
173
|
+
graft run <file.gft> --input <json> [--dry-run] [--verbose] # Compile and execute
|
|
174
|
+
graft init <name> # Scaffold a new project
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Editor Support
|
|
178
|
+
|
|
179
|
+
### VS Code
|
|
180
|
+
|
|
181
|
+
The [Graft VS Code extension](editors/vscode/) provides:
|
|
182
|
+
- Syntax highlighting (TextMate grammar)
|
|
183
|
+
- Real-time diagnostics
|
|
184
|
+
- Hover information (types, token budgets)
|
|
185
|
+
- Go-to-definition, find references, rename
|
|
186
|
+
- Completions (keywords, declarations, imports)
|
|
187
|
+
- Code actions (auto-import)
|
|
188
|
+
- Document symbols
|
|
189
|
+
|
|
190
|
+
## Programmatic API
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import { compileToProgram, compile } from '@graft-lang/graft/compiler';
|
|
194
|
+
import { Executor } from '@graft-lang/graft/runtime';
|
|
195
|
+
import type { Program, GraftErrorCode } from '@graft-lang/graft/types';
|
|
196
|
+
|
|
197
|
+
const result = compileToProgram(source, 'pipeline.gft');
|
|
198
|
+
if (result.success) {
|
|
199
|
+
console.log(`Parsed ${result.program.nodes.length} nodes`);
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Development
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
git clone https://github.com/JSLEEKR/graft.git
|
|
207
|
+
cd graft && npm install
|
|
208
|
+
npm run build # Compile TypeScript
|
|
209
|
+
npm test # Run all 1,344 tests
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Version History
|
|
213
|
+
|
|
214
|
+
| Version | Highlights |
|
|
215
|
+
|---------|-----------|
|
|
216
|
+
| **v5.3** | Parallel codegen test coverage, hook entry merging |
|
|
217
|
+
| **v5.2** | Parallel→sequential edge transforms, agent input overrides, graceful hooks |
|
|
218
|
+
| **v5.1** | Claude Code compatibility, `graft init`, CI/CD, README rewrite |
|
|
219
|
+
| **v5.0** | Condition-to-Expr AST unification, strict equality, codegen expression display |
|
|
220
|
+
| **v4.9** | Codegen expression display |
|
|
221
|
+
| **v4.8** | LSP expression intelligence — hover, go-to-def, completions |
|
|
222
|
+
| **v4.7** | Null coalescing (`??`), runtime expression hardening |
|
|
223
|
+
| **v4.6** | Logical operators (`&&`, `||`), conditional type warnings |
|
|
224
|
+
| **v4.5** | Comparison operators, conditional expressions |
|
|
225
|
+
| **v4.4** | String interpolation, expression extraction |
|
|
226
|
+
| **v4.0** | Variables, expressions, graph parameters, graph calls |
|
|
227
|
+
| **v3.0** | Pluggable codegen backends, field-level writes, failure strategies |
|
|
228
|
+
| **v2.0** | Import system, persistent memory |
|
|
229
|
+
| **v1.0** | Full compiler pipeline, CLI |
|
|
230
|
+
|
|
231
|
+
See [CHANGELOG.md](CHANGELOG.md) for full details.
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Program } from '../parser/ast.js';
|
|
2
|
+
import { GraftError } from '../errors/diagnostics.js';
|
|
3
|
+
import { ProgramIndex } from '../program-index.js';
|
|
4
|
+
export interface NodeTokenReport {
|
|
5
|
+
name: string;
|
|
6
|
+
estimatedIn: number;
|
|
7
|
+
estimatedOut: number;
|
|
8
|
+
}
|
|
9
|
+
export interface TokenReport {
|
|
10
|
+
graphName: string;
|
|
11
|
+
budget: number;
|
|
12
|
+
bestCase: number;
|
|
13
|
+
worstCase: number;
|
|
14
|
+
nodes: NodeTokenReport[];
|
|
15
|
+
warnings: GraftError[];
|
|
16
|
+
}
|
|
17
|
+
export declare class TokenEstimator {
|
|
18
|
+
private program;
|
|
19
|
+
private index;
|
|
20
|
+
private nodeMap;
|
|
21
|
+
private edgeMap;
|
|
22
|
+
private conditionalEdges;
|
|
23
|
+
constructor(program: Program, index?: ProgramIndex);
|
|
24
|
+
estimate(): TokenReport;
|
|
25
|
+
private collectNodeReports;
|
|
26
|
+
private computeFlowCosts;
|
|
27
|
+
private getConditionalBranchCosts;
|
|
28
|
+
private getNodeCost;
|
|
29
|
+
private getEstimatedIn;
|
|
30
|
+
private applyTransformReductions;
|
|
31
|
+
private getRetryMultiplier;
|
|
32
|
+
private getFallbackCost;
|
|
33
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { GraftError } from '../errors/diagnostics.js';
|
|
2
|
+
import { PARTIAL_FIELD_FACTOR, MAX_CONDITIONAL_HOPS } from '../constants.js';
|
|
3
|
+
import { ProgramIndex } from '../program-index.js';
|
|
4
|
+
export class TokenEstimator {
|
|
5
|
+
program;
|
|
6
|
+
index;
|
|
7
|
+
nodeMap;
|
|
8
|
+
edgeMap; // "source->target" key
|
|
9
|
+
conditionalEdges; // source -> branches
|
|
10
|
+
constructor(program, index) {
|
|
11
|
+
this.program = program;
|
|
12
|
+
this.index = index ?? new ProgramIndex(program);
|
|
13
|
+
this.nodeMap = this.index.nodeMap;
|
|
14
|
+
this.edgeMap = new Map();
|
|
15
|
+
this.conditionalEdges = new Map();
|
|
16
|
+
for (const edge of program.edges) {
|
|
17
|
+
if (edge.target.kind === 'direct') {
|
|
18
|
+
this.edgeMap.set(`${edge.source}->${edge.target.node}`, edge);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
this.conditionalEdges.set(edge.source, edge.target.branches);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
estimate() {
|
|
26
|
+
const graph = this.program.graphs[0]; // v1: single graph
|
|
27
|
+
if (!graph) {
|
|
28
|
+
return { graphName: '', budget: 0, bestCase: 0, worstCase: 0, nodes: [], warnings: [] };
|
|
29
|
+
}
|
|
30
|
+
const warnings = [];
|
|
31
|
+
const nodeReports = [];
|
|
32
|
+
// Populate node reports (for display)
|
|
33
|
+
this.collectNodeReports(graph.flow, nodeReports, warnings);
|
|
34
|
+
// Compute best/worst case costs
|
|
35
|
+
const { best, worst } = this.computeFlowCosts(graph.flow, warnings);
|
|
36
|
+
const bestCase = best;
|
|
37
|
+
const worstCase = worst;
|
|
38
|
+
if (worstCase > graph.budget) {
|
|
39
|
+
warnings.push(new GraftError(`Worst-case token usage (${worstCase}) exceeds budget (${graph.budget})`, graph.location, 'warning', 'BUDGET_EXCEEDED'));
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
graphName: graph.name,
|
|
43
|
+
budget: graph.budget,
|
|
44
|
+
bestCase,
|
|
45
|
+
worstCase,
|
|
46
|
+
nodes: nodeReports,
|
|
47
|
+
warnings,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
collectNodeReports(steps, reports, warnings) {
|
|
51
|
+
for (const step of steps) {
|
|
52
|
+
switch (step.kind) {
|
|
53
|
+
case 'node': {
|
|
54
|
+
const node = this.nodeMap.get(step.name);
|
|
55
|
+
if (!node)
|
|
56
|
+
break;
|
|
57
|
+
const estimatedIn = this.getEstimatedIn(step.name, node);
|
|
58
|
+
if (estimatedIn > node.budgetIn) {
|
|
59
|
+
warnings.push(new GraftError(`Node '${step.name}' estimated input (${estimatedIn}) exceeds budgetIn (${node.budgetIn})`, node.location, 'warning', 'BUDGET_NODE_EXCEEDED'));
|
|
60
|
+
}
|
|
61
|
+
reports.push({ name: step.name, estimatedIn, estimatedOut: node.budgetOut });
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case 'parallel':
|
|
65
|
+
for (const branchName of step.branches) {
|
|
66
|
+
const node = this.nodeMap.get(branchName);
|
|
67
|
+
if (!node)
|
|
68
|
+
continue;
|
|
69
|
+
const estimatedIn = this.getEstimatedIn(branchName, node);
|
|
70
|
+
if (estimatedIn > node.budgetIn) {
|
|
71
|
+
warnings.push(new GraftError(`Node '${branchName}' estimated input (${estimatedIn}) exceeds budgetIn (${node.budgetIn})`, node.location, 'warning', 'BUDGET_NODE_EXCEEDED'));
|
|
72
|
+
}
|
|
73
|
+
reports.push({ name: branchName, estimatedIn, estimatedOut: node.budgetOut });
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
case 'foreach':
|
|
77
|
+
this.collectNodeReports(step.body, reports, warnings);
|
|
78
|
+
break;
|
|
79
|
+
case 'let':
|
|
80
|
+
// Let bindings have no token cost
|
|
81
|
+
break;
|
|
82
|
+
case 'graph_call': {
|
|
83
|
+
// Recurse into called graph's flow for node reports
|
|
84
|
+
const calledGraph = this.index.graphMap.get(step.name);
|
|
85
|
+
if (calledGraph) {
|
|
86
|
+
this.collectNodeReports(calledGraph.flow, reports, warnings);
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
default: {
|
|
91
|
+
const _exhaustive = step;
|
|
92
|
+
throw new Error(`Unhandled FlowNode kind: ${_exhaustive.kind}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
computeFlowCosts(steps, warnings) {
|
|
98
|
+
let best = 0;
|
|
99
|
+
let worst = 0;
|
|
100
|
+
for (const step of steps) {
|
|
101
|
+
switch (step.kind) {
|
|
102
|
+
case 'node': {
|
|
103
|
+
const node = this.nodeMap.get(step.name);
|
|
104
|
+
if (!node)
|
|
105
|
+
break;
|
|
106
|
+
const cost = this.getNodeCost(step.name, node);
|
|
107
|
+
const retryMul = this.getRetryMultiplier(node);
|
|
108
|
+
best += cost;
|
|
109
|
+
worst += cost * retryMul + this.getFallbackCost(node);
|
|
110
|
+
// Add conditional edge branch costs (multi-hop recursive)
|
|
111
|
+
const branchCosts = this.getConditionalBranchCosts(step.name, warnings, new Set([step.name]), 0);
|
|
112
|
+
best += branchCosts.best;
|
|
113
|
+
worst += branchCosts.worst;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case 'parallel': {
|
|
117
|
+
// Parallel: all branches run. Total tokens = sum of all branches.
|
|
118
|
+
for (const branchName of step.branches) {
|
|
119
|
+
const node = this.nodeMap.get(branchName);
|
|
120
|
+
if (!node)
|
|
121
|
+
continue;
|
|
122
|
+
const cost = this.getNodeCost(branchName, node);
|
|
123
|
+
const retryMul = this.getRetryMultiplier(node);
|
|
124
|
+
best += cost;
|
|
125
|
+
worst += cost * retryMul + this.getFallbackCost(node);
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case 'foreach': {
|
|
130
|
+
// Foreach: body runs up to maxIterations times.
|
|
131
|
+
// Best case = 1 iteration. Worst case = maxIterations iterations.
|
|
132
|
+
const bodyCosts = this.computeFlowCosts(step.body, warnings);
|
|
133
|
+
best += bodyCosts.best * 1;
|
|
134
|
+
worst += bodyCosts.worst * step.maxIterations;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case 'let':
|
|
138
|
+
// Let bindings have zero token cost
|
|
139
|
+
break;
|
|
140
|
+
case 'graph_call': {
|
|
141
|
+
// Graph call cost = called graph's flow cost
|
|
142
|
+
const calledGraph = this.index.graphMap.get(step.name);
|
|
143
|
+
if (calledGraph) {
|
|
144
|
+
const subCosts = this.computeFlowCosts(calledGraph.flow, warnings);
|
|
145
|
+
best += subCosts.best;
|
|
146
|
+
worst += subCosts.worst;
|
|
147
|
+
}
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
default: {
|
|
151
|
+
const _exhaustive = step;
|
|
152
|
+
throw new Error(`Unhandled FlowNode kind: ${_exhaustive.kind}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { best, worst };
|
|
157
|
+
}
|
|
158
|
+
getConditionalBranchCosts(source, warnings, visited, depth) {
|
|
159
|
+
const branches = this.conditionalEdges.get(source);
|
|
160
|
+
if (!branches)
|
|
161
|
+
return { best: 0, worst: 0 };
|
|
162
|
+
if (depth >= MAX_CONDITIONAL_HOPS) {
|
|
163
|
+
warnings.push(new GraftError(`Conditional chain from '${source}' exceeded maximum depth of ${MAX_CONDITIONAL_HOPS}`, { line: 0, column: 0, offset: 0 }, 'warning', 'BUDGET_CHAIN_DEPTH'));
|
|
164
|
+
return { best: 0, worst: 0 };
|
|
165
|
+
}
|
|
166
|
+
const branchBestCosts = [];
|
|
167
|
+
const branchWorstCosts = [];
|
|
168
|
+
for (const branch of branches) {
|
|
169
|
+
if (branch.target === 'done') {
|
|
170
|
+
branchBestCosts.push(0);
|
|
171
|
+
branchWorstCosts.push(0);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (visited.has(branch.target)) {
|
|
175
|
+
// Cycle detected
|
|
176
|
+
warnings.push(new GraftError(`Conditional estimation cycle detected: ${[...visited, branch.target].join(' -> ')}`, { line: 0, column: 0, offset: 0 }, 'warning', 'BUDGET_CHAIN_CYCLE'));
|
|
177
|
+
branchBestCosts.push(0);
|
|
178
|
+
branchWorstCosts.push(0);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const node = this.nodeMap.get(branch.target);
|
|
182
|
+
if (!node)
|
|
183
|
+
continue; // unknown target, skip
|
|
184
|
+
const cost = this.getNodeCost(branch.target, node);
|
|
185
|
+
const retryMul = this.getRetryMultiplier(node);
|
|
186
|
+
// Per-branch visited set copy (handles diamonds correctly)
|
|
187
|
+
const branchVisited = new Set(visited);
|
|
188
|
+
branchVisited.add(branch.target);
|
|
189
|
+
const chainCosts = this.getConditionalBranchCosts(branch.target, warnings, branchVisited, depth + 1);
|
|
190
|
+
branchBestCosts.push(cost + chainCosts.best);
|
|
191
|
+
branchWorstCosts.push(cost * retryMul + chainCosts.worst);
|
|
192
|
+
}
|
|
193
|
+
if (branchBestCosts.length === 0)
|
|
194
|
+
return { best: 0, worst: 0 };
|
|
195
|
+
return { best: Math.min(...branchBestCosts), worst: Math.max(...branchWorstCosts) };
|
|
196
|
+
}
|
|
197
|
+
getNodeCost(nodeName, node) {
|
|
198
|
+
return this.getEstimatedIn(nodeName, node) + node.budgetOut;
|
|
199
|
+
}
|
|
200
|
+
getEstimatedIn(nodeName, node) {
|
|
201
|
+
let estimatedIn = 0;
|
|
202
|
+
for (const ref of node.reads) {
|
|
203
|
+
// If reading a context
|
|
204
|
+
const ctx = this.index.contextMap.get(ref.context);
|
|
205
|
+
if (ctx) {
|
|
206
|
+
estimatedIn += ref.field ? Math.floor(ctx.maxTokens * Math.min(PARTIAL_FIELD_FACTOR * ref.field.length, 1.0)) : ctx.maxTokens;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
// If reading a memory
|
|
210
|
+
const mem = this.index.memoryMap.get(ref.context);
|
|
211
|
+
if (mem) {
|
|
212
|
+
estimatedIn += ref.field ? Math.floor(mem.maxTokens * Math.min(PARTIAL_FIELD_FACTOR * ref.field.length, 1.0)) : mem.maxTokens;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
// If reading a produces output from upstream node
|
|
216
|
+
const sourceNode = this.index.producesNodeMap.get(ref.context);
|
|
217
|
+
if (sourceNode) {
|
|
218
|
+
let upstreamTokens = sourceNode.budgetOut;
|
|
219
|
+
// Check for edge transform reductions
|
|
220
|
+
const edgeKey = `${sourceNode.name}->${nodeName}`;
|
|
221
|
+
const edge = this.edgeMap.get(edgeKey);
|
|
222
|
+
if (edge) {
|
|
223
|
+
upstreamTokens = this.applyTransformReductions(upstreamTokens, edge.transforms);
|
|
224
|
+
}
|
|
225
|
+
estimatedIn += ref.field ? Math.floor(upstreamTokens * Math.min(PARTIAL_FIELD_FACTOR * ref.field.length, 1.0)) : upstreamTokens;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return estimatedIn;
|
|
229
|
+
}
|
|
230
|
+
applyTransformReductions(tokens, transforms) {
|
|
231
|
+
let result = tokens;
|
|
232
|
+
for (const t of transforms) {
|
|
233
|
+
switch (t.type) {
|
|
234
|
+
case 'select':
|
|
235
|
+
result = Math.floor(result * Math.min(PARTIAL_FIELD_FACTOR * t.fields.length, 1.0));
|
|
236
|
+
break;
|
|
237
|
+
case 'filter':
|
|
238
|
+
result = Math.floor(result * 0.5); // filter reduces ~50%
|
|
239
|
+
break;
|
|
240
|
+
case 'drop':
|
|
241
|
+
result = Math.floor(result * 0.85); // drop one field ~15% savings
|
|
242
|
+
break;
|
|
243
|
+
case 'compact':
|
|
244
|
+
result = Math.floor(result * 0.7); // compact ~30% reduction
|
|
245
|
+
break;
|
|
246
|
+
case 'truncate':
|
|
247
|
+
result = Math.min(result, t.tokens);
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
getRetryMultiplier(node) {
|
|
254
|
+
if (!node.onFailure)
|
|
255
|
+
return 1;
|
|
256
|
+
switch (node.onFailure.type) {
|
|
257
|
+
case 'retry':
|
|
258
|
+
return 1 + node.onFailure.max;
|
|
259
|
+
case 'retry_then_fallback':
|
|
260
|
+
return 1 + node.onFailure.max;
|
|
261
|
+
default:
|
|
262
|
+
return 1;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
getFallbackCost(node) {
|
|
266
|
+
if (!node.onFailure || node.onFailure.type !== 'retry_then_fallback')
|
|
267
|
+
return 0;
|
|
268
|
+
const fallbackNode = this.nodeMap.get(node.onFailure.node);
|
|
269
|
+
if (!fallbackNode)
|
|
270
|
+
return 0;
|
|
271
|
+
return this.getNodeCost(node.onFailure.node, fallbackNode);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FlowNode, Expr, GraphArg, GraphDecl } from '../parser/ast.js';
|
|
2
|
+
import { GraftError, SourceLocation } from '../errors/diagnostics.js';
|
|
3
|
+
import { ProgramIndex } from '../program-index.js';
|
|
4
|
+
export declare function checkVarCollision(name: string, graphName: string, location: SourceLocation, index: ProgramIndex, errors: GraftError[]): void;
|
|
5
|
+
export declare function checkExprSources(expr: Expr, seenNodes: Set<string>, declaredVars: Set<string>, graphName: string, errors: GraftError[]): void;
|
|
6
|
+
export declare function checkGraphCallArgs(args: GraphArg[], graphDecl: GraphDecl, seenNodes: Set<string>, declaredVars: Set<string>, graphName: string, location: SourceLocation, index: ProgramIndex, errors: GraftError[]): void;
|
|
7
|
+
export declare function checkGraphRecursion(graphs: {
|
|
8
|
+
name: string;
|
|
9
|
+
flow: FlowNode[];
|
|
10
|
+
location: SourceLocation;
|
|
11
|
+
}[], index: ProgramIndex, errors: GraftError[]): void;
|
|
12
|
+
export declare function collectGraphCalls(nodes: FlowNode[], calls: Set<string>): void;
|
|
13
|
+
export declare function checkLiteralParamType(value: string | number | boolean, type: 'Node' | 'Int' | 'String' | 'Bool'): boolean;
|