Genie React
Tool reference

App tools

Register app actions and queries that agents discover and call through Genie.

The JSON examples below show selected fields from the full response.

Apps can register their own tools with useGenieTool. They appear under the app group, named app_<name>, and are called like any built-in. Use them for what only the app can do: log in as a role, seed fixtures, inject API failures, jump a wizard step.

The tools on this page come from the demo apps (apps/vite-demo, apps/router-demo). Your app defines its own. The contract command is always the source of truth:

npx @genie-react/cli tools app

Settled native navigation

createNavigationTools from genie-react/navigation provides an opt-in adapter for Expo Router and React Navigation native stacks. Register its three tools and forward navigator events to it:

import { registerGenieTools } from 'genie-react/client'
import { createNavigationTools } from 'genie-react/navigation'

// Create once for this navigator's lifetime, for example inside the layout component's effect.
const integration = createNavigationTools({
  getState: () => navigation.getRootState(),
  isCurrentHref: (href) => matchesCurrentHrefIncludingParams(href),
  router: {
    push: (href) => router.push(href),
    replace: (href) => router.replace(href),
    navigate: (href) => router.navigate(href),
    dismissTo: (href) => router.dismissTo(href),
    back: () => router.back(),
    canGoBack: () => router.canGoBack(),
  },
})
const unregister = registerGenieTools(...integration.tools)

// Forward state, transitionStart and transitionEnd through each relevant native stack's
// screenListeners. Retain the controller in a ref when creating it inside an effect.
// <Stack screenListeners={integration.screenListeners} />

// On layout cleanup:
// unregister()
// integration.dispose()

The app supplies matchesCurrentHrefIncludingParams: compare the complete destination using your routing rules, including parameters. Comparing only a route name can incorrectly skip a requested parameter change. With Expo Router's typed routes, validate or cast hrefs to your app's Href type at this adapter boundary. The repository's complete Expo Router layout shows effect cleanup, refs, registration and event forwarding for its two known routes.

These tools appear only after registration. Replace your existing handlers with the integration; installing Genie alone does not change app-owned navigation tools.

ToolArgumentsResult
app_navigatehref: nonempty string; mode: navigate (default), push, replace, dismiss_to; timeoutMs: integer 1–15000, default 5000Resulting route, active route path, stack depth and settlement
app_navigate_backtimeoutMs: integer 1–15000, default 5000Resulting route, stack depth and settlement; no-op when back is unavailable
app_navigation_stateNoneCurrent route and stack snapshot, transitioning and unsettledNavigation; does not wait
npx @genie-react/cli call app_navigate \
  '{"href":"/details","mode":"push","timeoutMs":5000}' --json
{
  "currentRoute": { "key": "details-instance-2", "name": "details" },
  "stackDepth": 3,
  "stackScope": "deepest-active-stack",
  "settled": true,
  "reason": "transition-end"
}

Read the destination from this response; no sleep or follow-up state call is needed when settled:true. Explicit push can add another instance of an existing screen. stackDepth and stackRoutes expose that duplication immediately. In nested navigators, depth describes the deepest stack on the active route path, not the sum of every navigator's routes.

Success follows a matching native transitionEnd and the resulting navigation state. An already current navigate or dismiss_to, or a back action with no history, returns reason:"no-op". React render quiescence is independent of native transition completion.

Concurrent calls serialize, with at most 32 waiting calls. The timeout includes time waiting in the queue; an expired queued action is never dispatched. Read settled on every response: timeout, transition-in-progress, previous-navigation-unsettled, queue-full, unavailable, dispatch-error and disposed all report settled:false with the state observed at return time. A timeout does not undo navigation. Do not blindly retry a push.

After a dispatched action times out, further actions remain blocked until its matching transition finishes. If the navigator never emits that event, inspect the actual app state and correct the listener wiring before disposing and recreating the integration. Forward events from every stack that can perform the action. State changes alone, parameter-only updates without a transition, and non-native navigators do not establish native transition completion.

Register a tool

One declaration in a component. The zod schema drives validation, TypeScript types, and the advertised JSON Schema:

The examples import zod; add it to the app directly (pnpm add zod) — genie-react depends on it internally, but package managers do not expose transitive dependencies to your imports.

import { useGenieTool } from 'genie-react'
import { z } from 'zod'

useGenieTool({
  name: 'login_as',
  kind: 'action', // or 'query' — tells the agent read vs mutate
  description: 'Switches the session role and re-gates the UI.',
  input: z.object({ role: z.enum(['guest', 'member', 'admin']) }),
  handler: ({ role }) => switchRole(role),
})

The handler always sees the latest render's state. There is no dependency array. The name, schema, and description are fixed at mount — to change them at runtime, key the hook off enabled or the name.

Register several tools at once

One useGenieTools call registers a whole panel; inline handlers keep the same latest-render guarantee:

import { defineGenieTool, useGenieTools } from 'genie-react'
import { z } from 'zod'

useGenieTools([
  defineGenieTool({
    name: 'session',
    kind: 'query',
    description: 'Active role and what the task UI permits.',
    handler: () => ({ role }),
  }),
  defineGenieTool({
    name: 'login_as',
    kind: 'action',
    description: 'Switches the session role and re-gates the UI.',
    input: z.object({ role: z.enum(['guest', 'member', 'admin']) }),
    handler: ({ role: next }) => setRole(next),
  }),
])

apps/vite-demo/src/TaskPanel.tsx registers its five tools this way.

Register from a store or any module

Outside React, registerGenieTools works from a store, an API client, or module scope. It waits for the genie client if it has not started yet, and returns an unregister function:

import { defineGenieTool, registerGenieTools } from 'genie-react/client'
import { useCartStore } from './cart-store'

const unregister = registerGenieTools(
  defineGenieTool({
    name: 'cart_state',
    kind: 'query',
    description: 'Current cart line items and totals from the zustand store.',
    handler: () => useCartStore.getState().summary(),
  }),
  defineGenieTool({
    name: 'cart_clear',
    kind: 'action',
    destructive: true,
    description: 'Empties the cart store. No undo.',
    handler: () => useCartStore.getState().clear(),
  }),
)

Store handlers read current store state at call time with getState(). Calling unregister() tombstones the tools instead of deleting them. For tools that should live as long as the app, pass them to the provider instead: <Genie tools={[...]} />.

Options:

  • kind: 'query' advertises read-only and idempotent annotations. Its handler must honor them. kind: 'action' marks a mutation; actions also accept destructive: true and idempotent: true.
  • group: 'checkout' lists the tool under app.checkout for progressive discovery on large apps. tools app still covers every subgroup; tools app.checkout narrows to one. Omit it when the app exposes only a few tools.
  • output is an optional zod schema, advertised and drift-checked in dev builds.
  • maxResultBytes raises the 128KB result cap when a large result is intentional.
  • enabled: false gates registration, e.g. behind a feature flag.

Throw GenieToolError for failures the agent should act on:

throw new GenieToolError('cart is empty', { code: 'CART_EMPTY', hint: 'seed with app_seed_tasks' })

The runtime retains the error code and hint in its handler error message. The executable CLI returns a sanitized JSON error with reason: "tool-error"; it does not expose that raw message. If an agent must branch on an expected domain outcome such as CART_EMPTY, return a typed result with that code in your tool's output schema.

Discover and call

npx @genie-react/cli tools app
npx @genie-react/cli call app_login_as '{"role":"admin"}' --json

Output (selected fields):

{ "role": "admin", "canAdd": true, "canDelete": true }

Arguments are validated in the app by the tool's own schema: per-field errors, defaults applied, unknown keys rejected with the valid keys listed.

Inspect a tool's JSON descriptor with tools app_login_as before calling it. annotations reports readOnlyHint, idempotentHint, and destructiveHint when declared. These are handler contracts, not enforcement or a promise that a read has no runtime cost.

Unavailable tools

A tool registered by an unmounted component stays listed instead of vanishing:

npx @genie-react/cli tools app_checkout_state
npx @genie-react/cli call app_checkout_state '{}'

The descriptor exposes available: false and unavailableReason. The call returns a JSON error with reason: "tool-unavailable" and a nonzero exit code. This differs from reason: "tool-error" when a mounted handler throws. Read the descriptor's recovery reason, navigate to the UI that registers the tool, and discover it again before retrying.

Patterns

PatternDemo toolReplaces
Impersonationapp_login_asDriving a login form for every role
Fixturesapp_seed_tasks, app_reset_tasksHand-building test data per run
Fault injectionapp_chaosHoping an error state happens naturally
Step-jumpingapp_checkout_gotoFilling three wizard forms to test the last

apps/vite-demo/src/TaskPanel.tsx and apps/router-demo/src/routes/checkout.tsx show the full implementations.

Limits

  • App tool results have a 128 KiB runtime cap, configurable with maxResultBytes. Oversize results fail the tool call. The CLI output budget is separate; raising one cap does not raise the other.
  • Keep synchronous handler work well under a second; a blocked main thread reads as busy.
  • Tools exist only while the registering code runs. In production builds without a Genie client, registration is inert.

On this page