Skip to content
Gruntend v0.5.0 beta

How Gruntend works

Gruntend runs JavaScript plans without giving those plans direct access to your application.

Part Responsibility
Tool contract Describes one operation and validates its input and output.
Handler Performs the operation with application permissions and services.
Plan Composes tool calls with JavaScript and returns data or UI.
Executor Evaluates the plan and routes each call through its matching handler.

In a conventional tool-calling loop, the host repeatedly asks a model what to call next. Gruntend instead lets a model produce one small program. That program can transform results, branch on runtime values, and coordinate several calls while the application retains control of every operation.

A plan runs inside an explicitly selected code-plan executor. The plan receives a small runtime surface:

Runtime value Available capability
input Task data supplied when the plan starts.
tools Registered application capabilities.
console Diagnostic logging through the lifecycle stream.
html Tagged templates when generated UI is enabled.

The plan does not receive imports, fetch, window, document, storage, or application services. Standard JavaScript built-ins such as Promise remain available for orchestration.

A plan is ordinary JavaScript, not a fixed list of steps. It can use values returned at runtime to choose what happens next: branch with if, transform collections, loop over results, return early, or run independent tool calls with Promise.all().

const { items } = await tools.menu.items.list({
  menuId: input.menuId,
});

return items.filter((item) => item.price < 10);

When the plan calls menu.items.list, Gruntend:

  1. Finds the registered tool contract.
  2. Validates the supplied input.
  3. Calls handlers["menu.items.list"].
  4. Validates the successful output.
  5. Returns that output to the plan.

The application handler owns permissions, persistence, network access, and business rules. The plan never receives those dependencies.

The dotted tool name connects every part:

tool definition   menu.items.list
plan call         tools.menu.items.list(...)
handler key       handlers["menu.items.list"]
runtime event     event.tool === "menu.items.list"

Application code receives a flat array from defineTools(). The runtime uses that array for registration, manifests, and handler typing:

const tools = defineTools({ menu: { items: { list: spec } } });
tools[0].name; // "menu.items.list"

Inside a plan, Gruntend exposes the same registered names as a callable namespace:

await tools.menu.items.list({ menuId: "menu_1" });

The plan-side namespace is created by the runtime; it is not a second tool definition.

That separation is the product:

Plans decide how capabilities are composed. Applications decide what each capability is allowed to do.

A plan normally returns data. When the application enables the html runtime value, it may instead return a tagged HTML template or render function.

Generated UI uses the same tool boundary. Browser interactions do not bypass app-owned handlers.

For UI plans, the application also selects a renderer. The UI compiler produces an inert frame containing markup and delegated handler identifiers; the renderer owns the mounted session and commits that frame to its target. Gruntend provides DOMPurify as its only built-in browser renderer while keeping GeneratedUiRenderer<TTarget> available for application-defined targets.

Renderer typing, markup sanitization, code execution, and tool authorization are separate boundaries. See Generated UI for the compiler flow and Renderers for mounting and browser commit behavior.

Run your first task for the complete API in one example. Then read Code plans for the interpreted JavaScript contract.