Skip to content

Building with a coding agent ​

The same examples this guide is built from ship with an agent pack: a short orientation file, one task recipe per function kind, and copy-paste skeletons. Point a coding agent at it and it can add a working function — registered, tested, and verified end-to-end — without you narrating the framework.

It lives in the agents/ directory of the repo, alongside the examples/ project the recipes build into.

PieceWhat it is
AGENTS.mdOrientation — the model in three sentences, how to pick a kind, the required methods, conventions, and the gotchas that bite.
recipes/One self-contained task per kind: a goal, a prompt you can hand over verbatim, the files to touch, and the exact verification.
skeletons/A minimal, TODO-marked starting class per kind — the shape to fill in.

Pick the kind ​

The recipe an agent follows is chosen by the shape of the work. This is the same decision the function pages walk through, in one table:

KindBase typeShapeBuild when…
scalarScalarFnrow → rowone value in, one value out
tableCountdownTableFunctionargs → rowsgenerating or scanning rows
table-in-outPassthroughTIOFunctionrows → rows, per batchstreaming relation transform
aggregateAggregateFunction<State>rows → one per groupparallel reduction
bufferingTableBufferingFunctionall rows → rowssort / top-k / whole-relation

The decision rule for the three table-shaped kinds: emits per input batch → table-in-out; folds rows into per-group state → aggregate; must see every row before emitting → buffering.

What the agent must get right ​

The pack front-loads the handful of rules the framework actually enforces, so an agent doesn't discover them through allocator-leak errors:

  • -parameters is mandatory. Argument names come from method parameter names; without it, binding breaks. The examples/ build already passes it.
  • Wire names are snake_case and equal the Java parameter name — a long batchSize parameter becomes the batch_size argument.
  • emit() takes ownership. Never close() a root you emitted; do close roots you read and didn't emit. Never emit(input.root()) in a table-in-out — the input root is reused for the next batch.
  • Guard integer math with Math.addExact / Math.multiplyExact and raise a function-named error ("my_fn: int64 overflow").
  • Producer / exchange / State classes need a public no-arg constructor — the framework serializes them between ticks and across process boundaries.

The full list, with the reasoning behind each, is in AGENTS.md.

How a function gets verified ​

Every recipe ends the same way, and the loop is now fully public — it resolves farm.query:vgi from Maven Central and runs a real engine through uvx, with no local engine build to check out.

bash
cd examples
./gradlew installDist                                  # resolves vgi from Central
BIN="$PWD/build/install/vgi-java-examples/bin/vgi-java-examples"

# Quick assertion — pipe SQL through haybarn-cli and check the value:
printf "INSTALL vgi FROM community; LOAD vgi;
ATTACH 'demo' AS demo (TYPE vgi, LOCATION 'launch:$BIN');
SELECT demo.upper_case('hello');
DETACH demo;\n" | uvx haybarn-cli -noheader -list      # -> HELLO

To run the whole examples.test golden-file suite, use uvx haybarn-unittest. It discovers .test files under test/sql/ and needs vgi loaded, so stage a self-contained copy first (the require vgi directive only auto-loads core extensions, not community ones):

bash
mkdir -p test/sql
sed 's/^require vgi$/statement ok\nINSTALL vgi FROM community;\n\nstatement ok\nLOAD vgi;/' \
    test/examples.test > test/sql/examples.test
VGI_TEST_WORKER="launch:$BIN" uvx haybarn-unittest test/sql/examples.test

A green All tests passed on a clean machine is the definition of done. See testing for the same three levels written for humans.

Next: the task recipes →