Skip to content

Latest commit

 

History

History
438 lines (357 loc) · 17.7 KB

File metadata and controls

438 lines (357 loc) · 17.7 KB

AGENTS.md — rocketchain repo

This file is the entry point for AI agents (Claude Code, similar) working on the rocketchain repo itself. If you are inside a generated Spring Boot project, read the AGENTS.md at that project's root instead.

Audience: an agent that needs to TRANSLATE a natural-language flow description into a .rocketflow JSON file — and then verify it compiles to Java. The repo is the visual editor + codegen for the agentic-helper Java runtime; flows authored here become Spring Boot apps.

TL;DR

A .rocketflow is a JSON DAG of typed nodes connected by exec edges (orange, control flow) and data edges (blue, value flow). The codegen turns it into a @Service plus a trigger wrapper (Controller / scheduled Routine / EventListener). Authoring by hand is fully supported — this guide explains how.

natural language goal
  ↓ identify trigger + decompose into steps (§1)
  ↓ map each step to a node type (§2)
  ↓ write agent / tool JSON if needed (§3)
  ↓ wire exec + data edges (§4)
  ↓ smoke test with the CLI (§5)
.rocketflow → mvn compile → runnable Spring Boot app

§1 — Methodology (6 steps)

1.1 Identify the trigger

Ask: what makes the flow run? The answer maps to a startNode.data.triggerType:

Trigger When to use Generates
controller HTTP endpoint (REST API call) @RestController with POST/GET/...
routine Scheduled task (cron / fixedRate) @Scheduled(...) bean
event Reaction to a Spring ApplicationEvent @EventListener

If the user says "every hour", "on startup", "when a webhook fires", that's the trigger.

1.2 Decompose into atomic steps

Walk through the user's description and list discrete actions: "fetch X", "if Y is empty then Z", "for each item ask the LLM to ...". Each becomes ONE node. Avoid macro-nodes — small composable nodes are easier to debug and to wire.

1.3 Map each step to a node type

Use the catalog in §2. The match is usually obvious: "ask an agent" → agentNode, "loop over a list" → forEachNode, "store in DB" → variableNode + a custom helper @Service (called via Variable's "call" mode — see registry).

1.4 Author agents / tools if the flow uses LLMs

  • An agent is a JSON file under prompts/agents/<name>.json declaring an LLM persona (model, instructions, optional tools).
  • A tool is a function the LLM can call mid-response (autonomous mode). Defined as JSON under prompts/tools/<name>.json with a javaImplBody field — the body of execute(FunctionCall call).

See §3 for exact shapes.

1.5 Wire handles

Every node declares data.handles.{inputs,outputs} with typed handle ids (exec-in-1, data-out-result, ...). Edges connect a sourceHandle on one node to a targetHandle on another. The parser infers kind from the prefix (exec-* = control flow, else data flow). See §4.

1.6 Test with the codegen CLI

Run npx tsx tools/codegen-cli.ts debug <flow>.rocketflow to dump each pipeline stage's output, or ... smoke <flow>.rocketflow to build a full Spring Boot project and run mvn compile. Iterate fast — a flow that compiles is one that the runtime will accept. See §5.


§2 — Node catalog (when to use each)

The 19 registered node types live in src/components/reactflow/registry/. For each, the registry's factoryDefaults is the authoritative shape of the node's data field — read it when in doubt.

Type Use when
startNode Every main flow needs one. Sets trigger + return type.
endNode Terminates an exec branch. Carries the return value if the flow has one.
inputNode / outputNode Sub-flow boundaries (one each, required for type: "sub").
valueNode Inline literal: number, string, boolean, list.
variableNode Read/write a typed variable (or call a custom helper service).
agentNode Send a prompt to an LLM persona. May expose dynamic prompt vars.
subflowNode Call another .rocketflow as a function.
ifElseNode 2-way branch on a Boolean.
switchNode N-way branch on a discrete value (or expression).
forEachNode / whileNode Loop over a collection / while a condition holds.
tryCatchNode Catch a typed exception.
operatorNode Math / comparison / logical / string ops (typed by category).
allOfNode Wait for N parallel branches, then continue.
forEachCompletableNode / listCompletableNode Async collection processing.
globalVariableNode Read project-level constant (<<<globalvar::X>>>).
eventEmitterNode Publish a Spring ApplicationEvent.

Pattern: classify a step → pick the node

  • "Decide between A or B" → ifElseNode (Boolean) or switchNode (enum-like)
  • "Run for every X" → forEachNode (sequential) or forEachCompletableNode (parallel)
  • "Ask the LLM to ..." → agentNode (+ agent JSON)
  • "Compute X+Y" → operatorNode
  • "Call function foo(a,b) from another flow" → subflowNode (+ .rocketflow for foo)
  • "Remember a value across iterations" → variableNode (write then read)

§3 — Agents and tools (the JSON shapes)

Agent JSON (prompts/agents/<name>.json)

{
  "type": "agent",
  "id": "summarizer",                 // matches the file name (no .json)
  "name": "summarizer",
  "model": "gpt-4o-mini",
  "instances": "any",                 // or array of LLM_INSTANCES ids
  "instructions": "Summarize: {{text}}",   // RUNTIME form (Mustache-rendered)
  "instruction_code": "Summarize: <<<var::text>>>",   // AUTHORING form (with var markers)
  "temperature": 0.0,
  "responseTimeout": 60000,
  "functions": ["echo"],              // optional: tool names if autonomous
  "autonomous": false,                // true = tool-use loop enabled
  "maxIterations": 5                  // only meaningful when autonomous
}

Two prompt forms:

  • instruction_code is the AUTHORING form. It uses <<<var::Y>>> for dynamic vars (wired at runtime via input handles on the AgentNode) and <<<globalvar::X>>> for codegen-time substitution (baked into the rendered instructions).
  • instructions is the RUNTIME form, after <<<var::Y>>>{{Y}} Mustache translation. The CLI smoke mode handles this conversion for you (substituteFlowVarsToMustache).

Dynamic prompt var → AgentNode handle: For each <<<var::Y>>> in instruction_code, the AgentNode must have a matching data.handles.inputs[] entry with id: "data-in-Y". The codegen emits a Map<String,Object> populated from those wires.

Tool JSON (prompts/tools/<name>.json)

{
  "type": "tool",
  "name": "echo",                     // ^[a-z][a-z0-9_]*$, unique
  "description": "Echo back the input string.",
  "parameters": {                     // JSON Schema, fed verbatim to the LLM
    "type": "object",
    "properties": { "text": { "type": "string" } },
    "required": ["text"]
  },
  "endsTurn": false,                  // true = calling the tool ends the loop
  "javaImplBody": "return call.getArgumentsAsMap().get(\"text\").toString();"
}

API gotcha (T8 friction, learned the hard way): the FunctionCall runtime API does not have call.getArgument("text"). The correct calls are:

  • call.getArgumentsAsMap()Map<String, Object>
  • call.getName() → tool name (the LLM-visible string)
  • call.getId() → call identifier

The javaImplBody is inserted verbatim inside execute(FunctionCall call) throws Exception. Field-injected Spring beans are allowed (just declare them on the class via... wait, no — codegen owns the class. Put your custom beans in a separate @Service and look them up by autowiring through a static ApplicationContextProvider if you really need them, OR keep the tool body purely functional). Keep tool bodies stateless when possible.


§4 — Wiring handles (the convention)

Handle id format (registry source-of-truth: src/components/reactflow/registry/):

exec-in-1     exec-out-1     exec-out-then     exec-out-else
data-in-X     data-out-X     data-in-condition data-out-result
  • prefix exec-* → control-flow handle
  • prefix data-* → data handle (the suffix after data-in- / data-out- is the parameter name on agent / sub-flow nodes)
  • The parser falls back to data for unknown prefixes, so out-1 is silently broken — always include the exec- or data- prefix.

Edge shape:

{ "id": "e1",
  "source": "nodeA", "target": "nodeB",
  "sourceHandle": "exec-out-1", "targetHandle": "exec-in-1" }

The kind field is OPTIONAL — the parser infers execution vs data from the sourceHandle prefix. Don't write inconsistent values.

Common handle layouts:

  • startNode: {inputs: [], outputs: [{id:"exec-out-1", type:"exec"}]}
  • endNode (with return): {inputs: [{id:"exec-in-1", type:"exec"}, {id:"data-in-1", type:"<T>"}]}
  • ifElseNode: 1 exec input + 1 data input (data-in-condition, Boolean) + 2 exec outputs (exec-out-then, exec-out-else)
  • subflowNode (for sub-flow with inputs n: Int and outputs doubled: Int): inputs = [exec-in-1, data-in-n], outputs = [exec-out-1, data-out-doubled]

§5 — Test workflow (CLI debug + smoke)

The CLI is the fastest feedback loop — use it before declaring "the codegen is broken".

# Stage-by-stage dump (parser → walker → AST → emitted Java).
npx tsx tools/codegen-cli.ts debug path/to/flow.rocketflow

# Full project build + mvn compile.
npx tsx tools/codegen-cli.ts smoke path/to/flow.rocketflow \
  --subflows=path/to/subflows-dir \
  --agents=path/to/agents-dir \
  --tools=path/to/tools-dir \
  --out=tmp/my-test-out \
  --package=com.example

Pre-flight checklist before you run:

  • Every node has id, type, position, and data.handles
  • Every edge's sourceHandle and targetHandle exist on the referenced nodes
  • startNode exists in main flows; inputNode + outputNode exist in sub-flows
  • Every dynamic agent var (<<<var::X>>>) has a matching data-in-X handle on its AgentNode
  • Every subflowNode.data.selectedSubflow matches a sub-flow filename you'll pass via --subflows
  • Tool javaImplBody uses call.getArgumentsAsMap(), not getArgument(...)

Debug mode tells you:

  • parser diagnostics (malformed JSON / orphan handles)
  • walker output (start node, main exec path, total visits)
  • AST kinds (Return, AgentCall, If, For, SubflowCall, ...)
  • emitted Java (first 40 lines per file)

Smoke mode tells you:

  • the same diagnostics
  • whether mvn compile PASSES on the resulting Spring Boot project

§6 — Annotated example (start to finish)

Goal: "When the user POSTs a number to /complex, double it via a sub-flow, then return 'positive' if the result is > 0 else 'non-positive'."

A self-contained example exercising sub-flow nesting + control flow.

main.rocketflow:

{
  "type": "main",
  "name": "complexFlow",
  "nodes": [
    // Trigger: HTTP controller, returns String.
    { "id": "start", "type": "startNode", "position": {"x":0,"y":0},
      "data": { "triggerType": "controller", "returnType": "String",
        "handles": { "inputs": [], "outputs": [{"id":"exec-out-1", "type":"exec"}] }}},

    // Inline literal: 5.
    { "id": "val", "type": "valueNode", "position": {"x":100,"y":200},
      "data": { "valueType": "Int", "value": 5,
        "handles": { "inputs": [], "outputs": [{"id":"data-out-1", "type":"Int"}] }}},

    // Call outer.rocketflow (a sub-flow that doubles).
    { "id": "outerCall", "type": "subflowNode", "position": {"x":200,"y":0},
      "data": { "selectedSubflow": "outer.rocketflow",
        "subflowSignature": {
          "inputs":  [{"name":"n", "type":"Int"}],
          "outputs": [{"name":"doubled", "type":"Int"}]
        },
        "handles": {
          "inputs":  [{"id":"exec-in-1","type":"exec"}, {"id":"data-in-n","type":"Int"}],
          "outputs": [{"id":"exec-out-1","type":"exec"}, {"id":"data-out-doubled","type":"Int"}]
        }
    }},

    // ... ifelse + 2 endNodes (then/else) — see file for full content.
  ],
  "edges": [
    { "id":"e1", "source":"start", "target":"outerCall",
      "sourceHandle":"exec-out-1", "targetHandle":"exec-in-1" },
    // data wire: 5 → outerCall.n
    { "id":"d1", "source":"val", "target":"outerCall",
      "sourceHandle":"data-out-1", "targetHandle":"data-in-n" }
    // ...
  ]
}

outer.rocketflow (sub-flow): takes n: Int, returns doubled: Int, internally calls a deeper inner.rocketflow then multiplies by 2.

Test:

npx tsx tools/codegen-cli.ts smoke path/to/main.rocketflow \
  --subflows=path/to/subflow-dir \
  --out=tmp/test-out
# → mvn compile PASSED

The codegen produces ComplexFlowController (POST endpoint) + ComplexFlowService containing _sub_outer(...) and _sub_inner(...) helpers — the transitive sub-flow resolver pulls in inner even though only outer is referenced from the main flow.


§7 — Common pitfalls (extracted from real friction)

  1. Handle id missing prefix → parser silently treats it as data → exec edges land in the data graph and codegen emits nothing. Always prefix with exec- or data-.

  2. data.handles missing on a node → AST builder can't resolve parameters → empty method body / silent drop. The schema in schema/rocketflow.schema.json documents the per-type expectations; VS Code auto-completes against it (see .vscode/settings.json).

  3. Sub-flow not in registry → "Sub-flow 'X' is referenced ... but was not provided in CompileOptions.subflows". Pass --subflows=DIR to the CLI (or include it in compileProjectToJava({ subflows })). The compiler now traverses sub-flows transitively, so loading every .rocketflow in the project dir is the safe default.

  4. <<<var::X>>> in agent prompt without a matching data-in-X handle on the AgentNode → the runtime gets no value for {{X}} → empty string in the rendered prompt. Always pair the var with a handle.

  5. Tool body uses non-existent API (call.getArgument("...")) → mvn compile fails. Use call.getArgumentsAsMap() instead.

  6. Position x/y omitted → parser crashes (required by ReactFlow). Use a rough grid (column × 200, row × 150) — codegen ignores positions, they're UI-only.

  7. EndNode without an exec input → orphan branch, never reached. Every endNode must have exec-in-1 (and a data-in-1 if the flow returns a value).

  8. Sub-flow without InputNode AND OutputNode → diagnostic error. Sub-flows always have exactly one of each, even when parameter-less (Input with empty inputs[], Output with empty outputs[]).


§8 — API surface (programmatic compile)

Two layers:

High-level (recommended for one-shot project compile):

import { compileProjectToJava } from './services/CodeGen';

const { files, diagnostics } = compileProjectToJava({
  mainFlow,                                  // Flow object (parsed JSON)
  subflows: { outer, inner },                // optional, keyed by basename
  agents: {                                  // optional
    summarizer: {
      instruction_code: 'Summarize: <<<var::text>>>',
      functions: ['echo']
    }
  },
  tools: [echoTool],                         // optional
  packageName: 'com.example',
});
// files is JavaFile[] — service + trigger + tool registry + per-tool classes

Low-level (advanced — when you need fine-grained control):

  • compileFlowToJava(flow, options){ files, diagnostics } for the flow service + trigger only.
  • generateToolsJava({ basePackage, tools }) → tool executor classes.
  • parseFlow({ raw, name }){ flow, diagnostics }.
  • walk(flow) → walker visit map.
  • buildAst(flow, diagnostics?, options?) → AST array.

All exported from src/services/CodeGen/index.ts.


§9 — Reference docs (read before guessing)

Source-of-truth files (in this repo):

  • schema/rocketflow.schema.json — JSON Schema for .rocketflow files, documents every per-node-type data field. VS Code auto-completes against this via .vscode/settings.json.
  • src/services/CodeGen/types.tsFlow, FlowNode, FlowEdge, NodeData, AstNode.
  • src/services/CodeGen/README.md — pipeline overview (parser → walker → astBuilder → emitters).
  • src/components/reactflow/registry/ — one entry per node type with the authoritative factoryDefaults (canonical data shape).
  • src/services/Validation/preCodegenChecks.ts — every validation rule and its ruleId (matches the labels surfaced by the Generate dialog).

§10 — Markers & user-edit preservation

Most generated Java files are fully overwritten on regen — anything the user wrote inside *.flow services / controllers / tools is lost.

The exception is the tab-driven Java families (Entity / Routine / Event / Trigger), which support marker preservation:

//{Edit here:}
// User code preserved across regen.
//{End Edit}

The merge logic lives in src/components/common/EntityTab/utils.ts (and siblings). It reads the on-disk file, extracts the user-edit sections, regenerates the file from template, and splices the user-edit content back in.

Practical guidance: for flows, assume zero preservation. Custom logic goes in a separate @Service bean called via a Variable node.


Recent agentic-helper bumps (lib version is in pom.xml.template)

  • 1.20.3: requestAgent(..., Map<String,Object> promptVars) for {{Y}} Mustache substitution at runtime.
  • 1.22.0: chatCompletion family deprecated in favour of requestAgent / requestAutonomous.
  • 1.23.0: native Grok / Azure-Grok / DeepSeek / Gemini providers + CUSTOM data-driven provider via CustomProviderSpec.
  • 1.24.0: prompt caching enabled on Azure Anthropic.