← Blog

Graft v4.0: Variables, Expressions, and Graph Composition

4 min read
graftcompilerllmadversarial-debateclaude

Graft is a domain-specific language for defining LLM agent pipelines. The compiler takes .gft files and generates Claude Code harness structures — contexts, agents, hooks, and orchestration files. Development uses an adversarial debate harness where multiple AI agents independently analyze, critique, and converge before implementation.

v4.0 is the first major version bump since v3.0. Six rounds, ~14 agent calls, 90 new tests (980 total). Four interconnected features: variable bindings, expressions, graph parameters, and graph calls.

What This Version Adds

Variable bindings. let captures intermediate values in graph flow:

graph Pipeline(input: Spec, output: Report, budget: 20k) {
  Analyzer -> let score = Analyzer.risk_score -> Router -> done
}

Variables participate in conditions, expressions, and graph call arguments. The scope checker validates collision (no shadowing nodes/contexts/memories), ordering (sources must precede the let), and foreach body isolation (body variables don't leak).

Expressions. Arithmetic (+, -, /), string concatenation, field access, unary operators (-, !), and grouping. The expression AST is a 5-kind discriminated union. Division sits at additive precedence — the debate explicitly rejected a multiplicative level since Graft doesn't have * or %.

Graph parameters. Typed parameters with optional defaults:

graph Analysis(input: Spec, output: Result, budget: 10k,
               threshold: Int = 50, label: String = "default") {
  Worker -> done
}

Int, String, Bool types. Node-type parameters pass node references. Compile-time validation for missing params, wrong types, and recursion.

Graph calls. Invoke sub-graphs from flow:

graph Main(input: Spec, output: Report, budget: 20k) {
  Classifier -> Sub(threshold: 80) -> Summarizer -> done
}

LL(1) disambiguation (Identifier + LParen). Child contexts get isolated variable scopes. Nested calls supported (A→B→C); self-recursion and mutual recursion detected via DFS.

The Debates That Mattered

R1: Condition.left migration (HIGH, 4-agent debate). The biggest cross-cutting change: Condition.field (string) → Condition.left (Expr). Touched 4 source files and 142 test occurrences across 15 files. A1-Architect proposed direct replacement; A2-Pragmatist pushed for a bridge function. The convergence adopted both: conditionFieldName() enables safe incremental migration while new code uses Expr directly.

A1 was forced dissenter (highest confidence at 9). Four self-rebuttals: "Node should be a keyword" (rejected — it's an identifier value), "foreach should reject graph_call" (accepted then reversed — spec allows it), "multiplicative precedence needed" (rejected — YAGNI), "bridge is unnecessary" (rejected — bridge enables safe migration).

R2: Variable-first resolution (MEDIUM, A2+A3 debate). A3-Skeptic identified the critical ambiguity: when score appears in an expression, is it a variable or a node output field? Three answers were proposed. The adopted rule: single-segment names check variables first, multi-segment names always go through outputs. This was ratchet-locked and implemented consistently across scope checker, type checker, and runtime.

A3 also found that foreach body must clone declaredVars (body variables don't leak) and that Node-type graph params need special handling (pre-populated in seenNodes, skip flow-order check).

New Error Codes

| Code | Meaning | |------|---------| | SCOPE_VAR_COLLISION | Variable name conflicts with node/context/memory/graph | | SCOPE_VAR_UNDECLARED | Expression references undeclared source | | SCOPE_VAR_ORDER | Expression references source not yet in flow | | SCOPE_GRAPH_RECURSION | Graph call creates recursive cycle | | SCOPE_GRAPH_PARAM_MISSING | Required parameter not provided | | SCOPE_GRAPH_PARAM_TYPE | Argument type doesn't match parameter | | TYPE_EXPR_MISMATCH | Incompatible types in binary expression | | TYPE_VAR_CONDITION | Non-numeric variable in ordered comparison |

Stats

| Metric | v3.9 | v4.0 | Delta | |--------|------|------|-------| | Tests | 890 | 980 | +90 | | Ratchet decisions | ~235 | ~268 | +33 | | Rounds | 3 | 6 | +3 | | Agent calls | ~9 | ~14 | +5 | | Error codes | 30+ | 38+ | +8 | | First-try pass | 2/3 | 6/6 | 100% |

Try It

npm install -g @graft-lang/graft
graft compile pipeline.gft
graft run pipeline.gft --input '{"query":"test"}' --dry-run

Source: github.com/JSLEEKR/graft