Getting Started with the Geppetto JavaScript API

Step-by-step tutorial for turns, sessions, engines, middlewares, tools, and toolloop hooks from JavaScript.

Sections

Terminology & Glossary
📖 Documentation
Navigation
54 sectionsv0.1
📄 Getting Started with the Geppetto JavaScript API — glaze help geppetto-js-api-getting-started
geppetto-js-api-getting-started

Getting Started with the Geppetto JavaScript API

Step-by-step tutorial for turns, sessions, engines, middlewares, tools, and toolloop hooks from JavaScript.

Tutorialgeppettojavascriptgojatutorial

This tutorial is intentionally deep and script-first. You will learn the model behind the API, then build from a blank script file to a multi-step tool-enabled flow. Every stage is executable.

Reference docs:

How to Read This Guide

Each step follows one structure:

  • fundamentals: the core idea and why it exists
  • APIs used: the exact JS functions involved
  • pseudocode: implementation logic independent of syntax details
  • diagram: data/control flow
  • runnable script: copy/paste code
  • validation checklist: what to confirm after execution

If you already know Geppetto’s internal Go architecture, focus on the API and flow sections. If you are new, read every fundamentals section in order.

Mental Model First

Before writing code, keep this picture in mind.

+------------------------------ JS Script -------------------------------+
| require("geppetto")                                                 |
|                                                                      |
|  turn -> session -> engine -> middlewares -> tools -> toolloop       |
|                                                                      |
+------------------------------+----------------------------------------+
                               |
                               v
+--------------------------- Goja Boundary -----------------------------+
|  decode JS values -> Go structs -> execute -> encode back to JS      |
+------------------------------+----------------------------------------+
                               |
                               v
+-------------------------- Geppetto Runtime ---------------------------+
| turns, sessions, inference engines, middleware chain, tool execution  |
+----------------------------------------------------------------------+

Key entities:

  • Turn: one inference state container containing blocks plus metadata/data maps.
  • Block: typed unit inside a turn (user, system, llm_text, tool_call, tool_use, ...).
  • Engine: function-like inference component that transforms a turn.
  • Session: stateful history wrapper around repeated engine execution.
  • Middleware: interceptors around engine execution.
  • ToolRegistry: callable tool definitions available to the model and runtime.
  • Toolloop: iterative cycle that executes tool calls and re-enters inference.

Prerequisites

  • Go toolchain installed
  • repository checked out
  • run commands from repository root

Step 0: Validate the Host Runtime

Fundamentals

Your JS does not run in Node.js in this workflow. It runs inside goja, hosted by geppetto-js-lab.

That host is responsible for:

  • registering native module require("geppetto")
  • providing helper globals (assert, console, ENV)
  • injecting host-side Go tool registry for useGoTools

Without a host, your JS code has no access to Geppetto API surfaces.

APIs Used

  • CLI: go run ./cmd/examples/geppetto-js-lab --list-go-tools
  • JS: none yet (this step validates host setup)

Pseudocode

start host
register geppetto native module
register host go tools
if --list-go-tools:
  print tool names

Diagram

Terminal command
    |
    v
geppetto-js-lab
    |
    +--> builds Go tool registry
    +--> configures goja runtime
    +--> registers require("geppetto")
    |
    v
prints host capabilities

Run

go run ./cmd/examples/geppetto-js-lab --list-go-tools

Expected output includes:

  • go_double
  • go_concat

Validation Checklist

  • command exits with status 0
  • tool names printed
  • no JS execution errors

Step 1: Turns and Blocks from First Principles

Fundamentals

A Turn is the core payload for inference. Think of it as an envelope with two categories of content:

  • ordered blocks: conversational and tool events
  • side-channel maps: metadata and data

A Block has:

  • kind: semantic type (user, tool_call, etc.)
  • payload: type-specific fields

Why start here:

  • every later API (session, middleware, tools) reads/writes turns
  • shape errors here propagate everywhere

Normalization matters because JS objects are flexible while Go structs are typed. turns.normalize round-trips through the canonical mapper and guarantees your turn shape is compatible with runtime expectations.

APIs Used

  • gp.turns.newTurn(opts)
  • gp.turns.newUserBlock(text)
  • gp.turns.newToolCallBlock(id, name, args)
  • gp.turns.normalize(turn)

Pseudocode

build user block
build tool_call block
construct turn with blocks + metadata
assert shape
normalize turn through codec
assert normalized fields preserved

Diagram

JS object literal
   |
   v
turns.newTurn(...)
   |
   +--> blocks array
   +--> metadata map
   +--> data map
   |
   v
turns.normalize(turn)
   |
   +--> decode JS -> Go Turn
   +--> encode Go Turn -> JS
   |
   v
canonical turn object

Script

Create scratch/js-lab/01_turns.js:

const gp = require("geppetto");

const turn = gp.turns.newTurn({
  id: "turn-1",
  blocks: [
    gp.turns.newUserBlock("hello"),
    gp.turns.newToolCallBlock("call-1", "js_add", { a: 2, b: 3 })
  ],
  metadata: { session_id: "s-1" }
});

assert(turn.blocks.length === 2, "expected two blocks");
assert(turn.blocks[0].kind === "user", "first block kind mismatch");
assert(turn.blocks[1].kind === "tool_call", "second block kind mismatch");

const normalized = gp.turns.normalize(turn);
assert(normalized.metadata.session_id === "s-1", "metadata mismatch");

console.log("PASS step 1");

Run it:

go run ./cmd/examples/geppetto-js-lab --script scratch/js-lab/01_turns.js

Validation Checklist

  • two blocks exist in the order created
  • kind values are exactly expected
  • metadata survives normalization

Step 2: Session Lifecycle and Deterministic Engine

Fundamentals

A Session provides conversation state and execution lifecycle:

  • append a seed turn
  • run inference
  • inspect history later

engines.echo is deterministic and should be your first engine in any new flow because it removes provider variability. You can test state wiring before involving real model APIs.

Conceptually:

  • Session is your state container
  • Engine is your transformation function
  • run() executes engine over current turn context and stores result

APIs Used

  • gp.engines.echo({ reply })
  • gp.createSession({ engine })
  • session.append(turn)
  • session.run()
  • session.turnCount()

Pseudocode

engine := echo("READY")
session := createSession(engine)
append user turn
out := session.run()
assert last block is llm_text READY
assert history length == 1

Diagram

user turn
   |
   v
session.append
   |
   v
session.run
   |
   v
echo engine appends llm_text("READY")
   |
   v
updated turn returned + stored in session history

Script

Create scratch/js-lab/02_session.js:

const gp = require("geppetto");

const session = gp.createSession({
  engine: gp.engines.echo({ reply: "READY" })
});

session.append(gp.turns.newTurn({ blocks: [gp.turns.newUserBlock("reply READY")] }));
const out = session.run();

const last = out.blocks[out.blocks.length - 1];
assert(last.kind === "llm_text", "missing llm_text output");
assert(last.payload.text === "READY", "assistant text mismatch");
assert(session.turnCount() === 1, "turnCount mismatch");

console.log("PASS step 2");

Run it:

go run ./cmd/examples/geppetto-js-lab --script scratch/js-lab/02_session.js

Validation Checklist

  • run() returns updated turn
  • assistant block is present and deterministic
  • history count increments as expected

Step 3: Middleware Composition (Go + JS)

Fundamentals

Middleware is a chain-of-responsibility around engine execution. It is used to enforce policies, inject prompts, log traces, or transform turn content.

A middleware receives:

  • current turn
  • next(turn) callback

It can:

  • mutate turn before execution
  • call next
  • mutate output after execution

Ordering is critical. If middleware A must run before B, register A first.

In this step:

  • Go middleware systemPrompt adds a system block
  • JS middleware adds metadata after next

APIs Used

  • gp.engines.fromFunction(fn)
  • gp.createBuilder()
  • builder.withEngine(engine)
  • builder.useGoMiddleware("systemPrompt", opts)
  • gp.middlewares.fromJS(fn, name?)
  • builder.useMiddleware(middleware)
  • builder.buildSession()

Pseudocode

engine := fromFunction(append "ok")
builder := createBuilder
builder.withEngine(engine)
builder.useGoMiddleware(systemPrompt, {prompt:"SYSTEM"})
builder.useMiddleware(jsMiddleware(add trace_id))
session := builder.buildSession
append user turn
out := session.run
assert system block inserted first
assert trace_id exists

Diagram

Input Turn
   |
   v
[Go systemPrompt middleware]
   |
   v
[JS trace middleware pre]
   |
   v
Engine (append assistant "ok")
   |
   v
[JS trace middleware post -> metadata.trace_id]
   |
   v
Output Turn

Script

Create scratch/js-lab/03_middleware.js:

const gp = require("geppetto");

const engine = gp.engines.fromFunction((turn) => {
  turn.blocks.push(gp.turns.newAssistantBlock("ok"));
  return turn;
});

const session = gp
  .createBuilder()
  .withEngine(engine)
  .useGoMiddleware("systemPrompt", { prompt: "SYSTEM" })
  .useMiddleware(gp.middlewares.fromJS((turn, next) => {
    const out = next(turn);
    out.metadata = out.metadata || {};
    out.metadata.trace_id = "js-mw";
    return out;
  }, "trace"))
  .buildSession();

session.append(gp.turns.newTurn({ blocks: [gp.turns.newUserBlock("ping")] }));
const out = session.run();

assert(out.blocks[0].kind === "system", "system prompt missing");
assert(out.metadata.trace_id === "js-mw", "trace metadata missing");

console.log("PASS step 3");

Run it:

go run ./cmd/examples/geppetto-js-lab --script scratch/js-lab/03_middleware.js

Validation Checklist

  • system block appears at index 0
  • metadata contains trace_id
  • no middleware ordering surprises

Step 4: Tool Registry and Toolloop Mechanics

Fundamentals

Tooling has two layers:

  • registry layer: what tools exist and how they execute
  • orchestration layer (toolloop): when and how tool calls are executed during inference

Core loop idea:

  1. engine emits tool_call
  2. runtime executes tool
  3. runtime appends tool_use result block
  4. engine runs again with new context
  5. loop stops when no tool calls or limits reached

This means your engine can act as orchestrator logic while toolloop handles reliable execution and retries/policies.

APIs Used

  • gp.tools.createRegistry()
  • registry.register(spec)
  • gp.turns.newToolCallBlock(id, name, args)
  • builder.withTools(registry, opts)
  • tool options: enabled, maxIterations, toolChoice, maxParallelTools

Pseudocode

register js_add tool
engine:
  if no tool_use yet:
    emit tool_call(js_add)
  else:
    emit assistant done
builder.withTools(reg, loopOpts)
run session
assert tool_use block exists with sum result

Diagram

Iteration 1:
  Engine -> tool_call(js_add)
  Toolloop executes js_add -> tool_use({sum:5})

Iteration 2:
  Engine sees tool_use -> assistant("done")
  No new tool_call -> stop

Script

Create scratch/js-lab/04_tools.js:

const gp = require("geppetto");

const reg = gp.tools.createRegistry();
reg.register({
  name: "js_add",
  description: "Add numbers",
  handler: ({ a, b }) => ({ sum: a + b })
});

const engine = gp.engines.fromFunction((turn) => {
  const hasToolUse = turn.blocks.some((b) => b.kind === "tool_use");
  if (!hasToolUse) {
    turn.blocks.push(gp.turns.newToolCallBlock("call-1", "js_add", { a: 2, b: 3 }));
    return turn;
  }
  turn.blocks.push(gp.turns.newAssistantBlock("done"));
  return turn;
});

const session = gp
  .createBuilder()
  .withEngine(engine)
  .withTools(reg, { enabled: true, maxIterations: 3, toolChoice: "auto" })
  .buildSession();

session.append(gp.turns.newTurn({ blocks: [gp.turns.newUserBlock("compute")] }));
const out = session.run();

const toolUse = out.blocks.find((b) => b.kind === "tool_use");
assert(!!toolUse, "missing tool_use block");
assert(String(toolUse.payload.result).includes("sum"), "tool result missing sum");

console.log("PASS step 4");

Run it:

go run ./cmd/examples/geppetto-js-lab --script scratch/js-lab/04_tools.js

Validation Checklist

  • at least one tool_call generated internally by engine
  • corresponding tool_use appended by runtime
  • final assistant block appears after tool result

Step 5: Hybrid Tooling (Import Go Tools into JS)

Fundamentals

In production, you often need mixed tools:

  • quick script-local JS tools
  • mature Go tools from backend systems

useGoTools imports host-exposed Go tools into your JS registry. This keeps one unified invocation model in JS while still using typed Go implementations.

Important constraint:

  • host must provide a Go tool registry
  • geppetto-js-lab does this automatically

APIs Used

  • registry.useGoTools([names])
  • registry.call(name, args)
  • builder.withTools(registry, opts) for loop-driven usage

Pseudocode

reg := createRegistry
reg.useGoTools(["go_double"])
result := reg.call("go_double", {n:21})
assert result.value == 42

Diagram

JS script
   |
   v
registry.useGoTools("go_double")
   |
   v
host Go tool registry lookup
   |
   v
tool becomes callable in JS registry
   |
   v
registry.call("go_double", {n:21}) -> {value:42}

Script

Create scratch/js-lab/05_go_tools.js:

const gp = require("geppetto");

const reg = gp.tools.createRegistry();
reg.useGoTools(["go_double"]);

const direct = reg.call("go_double", { n: 21 });
assert(direct.value === 42, "direct go tool call mismatch");

console.log("PASS step 5", JSON.stringify(direct));

Run it:

go run ./cmd/examples/geppetto-js-lab --script scratch/js-lab/05_go_tools.js

Validation Checklist

  • import call succeeds
  • direct invocation returns typed value payload
  • no registry configuration errors

Step 6: Live Provider Inference (Optional Final Stage)

Fundamentals

Live inference introduces non-determinism and credential dependency. That is why it appears last.

What changes from deterministic steps:

  • real network and provider API
  • model behavior variability
  • quota and latency constraints

Recommended order:

  • validate deterministic scripts first
  • then run live script as smoke test

APIs Used

  • gp.engines.fromConfig({ apiType, model, apiKey })
  • gp.createSession({ engine })
  • session.run()
  • environment via ENV

Pseudocode

apiKey := ENV.GEMINI_API_KEY or ENV.GOOGLE_API_KEY
if missing:
  print SKIP
  exit success
create engine fromConfig(gemini)
run one-turn session
assert output blocks exist
log final block

Diagram

Script start
   |
   +--> key exists? -- no --> print SKIP --> success exit
   |
  yes
   |
   v
create gemini engine -> run session -> inspect final block

Run

go run ./cmd/examples/geppetto-js-lab --script examples/js/geppetto/06_live_profile_inference.js

If keys are absent, script self-skips and still exits successfully.

Validation Checklist

  • with no key: skip message appears
  • with key: output turn contains assistant-like final block
  • no configuration panic/errors

End-to-End Fast Path

If you want all maintained example scripts in sequence:

go run ./cmd/examples/geppetto-js-lab --list-go-tools
go run ./cmd/examples/geppetto-js-lab --script examples/js/geppetto/01_turns_and_blocks.js
go run ./cmd/examples/geppetto-js-lab --script examples/js/geppetto/02_session_echo.js
go run ./cmd/examples/geppetto-js-lab --script examples/js/geppetto/03_middleware_composition.js
go run ./cmd/examples/geppetto-js-lab --script examples/js/geppetto/04_tools_and_toolloop.js
go run ./cmd/examples/geppetto-js-lab --script examples/js/geppetto/05_go_tools_from_js.js
go run ./cmd/examples/geppetto-js-lab --script examples/js/geppetto/06_live_profile_inference.js

API Summary by Step

StepMain APIsMain Concept
0geppetto-js-lab CLIhost runtime wiring
1turns.*canonical turn/block schema
2engines.echo, createSession, rundeterministic stateful inference
3createBuilder, useGoMiddleware, middlewares.fromJSchain-of-responsibility composition
4tools.createRegistry, withToolstool execution loop
5useGoTools, callhybrid JS+Go tool ecosystem
6engines.fromConfigreal provider smoke validation

Common Failure Modes

ProblemWhy it happensFix
module geppetto not foundruntime host did not register moduleuse geppetto-js-lab or register via gp.Register
builder has no engine configuredwithEngine omittedcall withEngine before buildSession
no go tool registry configureduseGoTools in host without Go registryrun in geppetto-js-lab or inject Options.GoToolRegistry
no tool executiontool registry not bound to builderuse .withTools(reg, { enabled: true })
runAsync requires module options Runner to be configuredmissing runtime runner in hostuse run() or register module with Options.Runner
live script fails authmissing/invalid API keyset GEMINI_API_KEY or GOOGLE_API_KEY

Design Guidance for Real Projects

Use these conventions when building larger JS automation packs:

  • one script per behavior slice
  • deterministic first, live-provider last
  • strict assertions on observable output (kind, payload fields, metadata)
  • avoid hidden global state between scripts
  • keep script names ordered by dependency and complexity

A practical naming pattern:

  • 01_... schema and shapes
  • 02_... basic session behavior
  • 03_... middleware behavior
  • 04_... toolloop behavior
  • 05_... integration with host tools
  • 06_... live smoke

Next Steps

  1. Use JS API User Guide for composition patterns and production tradeoffs.
  2. Use JS API Reference for exhaustive method contracts and options.
  3. Extend examples/js/geppetto with your own domain scripts and keep them executable through geppetto-js-lab.