Skip to content
RELKIT
Esc
↑↓navigate↵open⌘Jpreview
On this page

Define an agent

Give a model a clear task, the tools it needs, and limits on each run.

An agent uses a language model to decide which steps and tools can answer a request. For a fixed sequence of steps, use a function.

Pass compatible LangChain models, tools, and middleware directly. RELKIT validates the declared input, output, limits, tool policies, and public client contract around the native execution. For exact options, use the native agent reference.

See Native runtime and DeepAgents for the supported package cohort, public typing boundary, and deep-agent limitations. For explicit workflows and durable continuation, use LangGraph, persistence, and resume.

Pre-setup

Complete agent setup to install the native runtime and configure Docker Redis for agent-state and realtime. Keep the agent’s stateProfile aligned with the configured default. A hosted model additionally needs its SDK and secret environment value; the deterministic example needs no model key. Filesystem state remains an explicit alternative in the offline tutorial.

Scaffold an agent

Add a native agent to an existing service with the installed CLI:

bun run relkit add agent Assistant --service hello \
  --instructions "Answer greeting questions."

This creates src/services/hello/agents/assistant.agent.ts with an offline model and no tools. Replace its input/output schemas and instructions with your task. Add tools explicitly when needed; scaffolding does not expose an HTTP endpoint or enable an agent client automatically.

As an alternative to manually creating the agent, this scaffold plans and normally installs the native integration and four SDK peers itself. With --no-install, run bun install and bun run check afterward. It does not select your realtime/agent-state profiles for you. The scaffold uses a deterministic offline model. Replace that model in server code when you are ready to configure a production provider.

For every agent scaffolding flag, see the agent command reference.

Scaffold a reusable prompt

Create a prompt before referencing it from an agent:

bun run relkit add prompt greeting-policy --service hello \
  --text "Answer greeting questions." --text "Keep answers brief."

The prompt is saved as src/services/hello/prompts/greeting-policy.prompt.ts; repeated non-empty --text values preserve separate prompt segments. In headless execution at least one --text is required. Prompts are internal descriptors, not public service members. Pass the prompt descriptor as the agent’s instructions when you need reusable policy text.

When creating a new agent, select it with --prompt greeting-policy instead of --instructions. Use --tool <tool> for each existing tool to attach. For an existing agent, edit its descriptor instead of running add agent again. See constants and prompts for their shared-value contract.

For every prompt scaffolding flag, see the prompt command reference.

Install the native runtime

From this guide’s application/package root, install only its selected requirements:

bun add @langchain/core@1.2.9 @langchain/langgraph@1.4.14 @relkit/langchain@0.7.13 @relkit/local@0.7.12 langchain@1.5.10 langsmith@0.10.8

Keep these owner-specific versions and the lockfile together. Runtime SDKs belong in dependencies; build/type tooling belongs in devDependencies. Installing an adapter does not supply its optional SDK peers or enable the feature’s configuration.

The recipe includes the optional filesystem providers used by the offline tutorial. For the default running application, keep the Docker Redis profiles from pre-setup. Generic contracts from @relkit/agents or @relkit/app/agents do not install LangChain.

Model-provider packages are application dependencies. The generated sample uses LangChain’s offline FakeToolCallingModel, so its tests and first run need no account, network request, or paid service.

Define the task

This agent answers greeting questions using a single tool. Create it after defining the lookup tool.

/** Runs an offline greeting agent with RELKIT/native tools, todo middleware and public state observation. */
import { defineAgent } from "@relkit/langchain";
import { z } from "@relkit/app/schema";
import { FakeToolCallingModel, todoListMiddleware, tool } from "langchain";
import lookup from "@app/services/hello/tools/lookup.tool.js";

export const uppercase = tool(async ({ text }: { readonly text: string }) => text.toUpperCase(), {
  name: "uppercase",
  description: "Uppercase text without leaving the process",
  schema: z.object({ text: z.string() }),
});

export default defineAgent({
  id: "hello.assistant",

  input: z.object({ message: z.string().min(1) }),
  output: z.object({ answer: z.string() }),

  model: new FakeToolCallingModel({
    toolCalls: [
      [
        {
          name: "write_todos",
          args: { todos: [{ content: "Answer the greeting", status: "completed" }] },
          id: "todo-1",
        },
      ],
      [
        {
          name: "relkit_output",
          args: { value: { answer: "Hello from the offline LangChain model." } },
          id: "output-1",
        },
      ],
    ],
  }),
  instructions: "Answer greeting questions with the available tools.",
  tools: [lookup, uppercase],
  middleware: [todoListMiddleware()],
  stateProfile: "default",
  client: {
    public: true,
    state: ["todos"],
    events: { notice: z.object({ message: z.string() }) },
  },
  chat: { input: "message", output: "answer" },
  controls: ["stop"],

  limits: {
    maxSteps: 4,
    maxToolCalls: 4,
    timeoutMs: 10_000,
  },
});

input describes what your app sends; output describes the answer your app expects. RELKIT validates both. Output validation checks the shape, not whether the model’s answer is factually correct.

Write instructions that explain when to use each tool and how to answer. The agent can call only tools in its tools array. Start with the smallest set needed for the task.

Use this agent from the frontend

After frontend setup and pulling the greeting agent’s contract, render this component under the provider. Supply a stable business threadId owned by your application, such as the ID of the conversation the user opened:

"use client";

/** Sends one greeting to the documented hello.assistant agent and observes its typed answer. */
import { useAgent, useRelkitClient } from "@relkit/client/react";
import { useState } from "react";

/** Keeps the application-owned thread identity stable across rendering and observation.
 * @param props - Authorized conversation identity retained by the application.
 * @returns A greeting action, the observed answer, and explicit submission failure. */
export function Assistant({ threadId }: { readonly threadId: string }) {
  const { status } = useRelkitClient();
  const assistant = useAgent("hello.assistant");
  const [failure, setFailure] = useState("");

  /** Submits once and preserves uncertain outcomes for explicit recovery.
   * @returns Completion after acceptance or displaying the failure. */
  async function sendGreeting() {
    try {
      await assistant.send("Hello, RELKIT!", { threadId });
    } catch (error) {
      setFailure(String(error));
    }
  }

  return (
    <section>
      <button
        disabled={status !== "ready" || assistant.status !== "idle" || failure !== ""}
        onClick={sendGreeting}
      >
        Say hello
      </button>
      <p role="status">{assistant.output?.answer ?? assistant.status}</p>
      {failure && <p role="alert">{failure}. Check this thread before submitting again.</p>}
    </section>
  );
}

This is the same hello.assistant declaration: client.public exposes it, chat.input: "message" makes send accept a string, and chat.output: "answer" selects its typed answer. Mounting useAgent does not start a run. The backend owns models, credentials, tools, and authorization.

Reference: useAgent. Continue with streaming and controls or thread recovery.

Set limits

All three limits are required positive integers:

Option What it limits
maxSteps Model steps in a run.
maxToolCalls Total tool calls in a run.
timeoutMs Total run time in milliseconds.

If a run exceeds a limit, it fails rather than continuing indefinitely. Set limits that fit your task and handle failures in the calling application.

Choose a model

model accepts a compatible native LangChain model instance or a lazy application model. Install and configure the provider package in server code; never put credentials, live instances, or provider payloads in browser contracts. Subagents inherit the parent model unless they declare an override.

Run the typed client

Expose the agent through client, then select it with useAgent. Run, observe, stop, and resume calls all require the caller’s stable business thread ID:

For this declaration, select hello.assistant; send(message, { threadId }) accepts a string because chat.input maps it to message, and the final output.answer follows the output schema. stop({ threadId }) is available because the agent declares stop. The Frontend guide establishes the provider and contract pull, and Agent streaming and controls shows a complete typed UI, including how a different agent’s policy changes the available controls.

Reference: useAgent.

The generated selector narrows initial input, final output, selected middleware state, tools, custom events, waiting replies, and controls. A resume uses the same thread with { resume: true }; it does not start a replacement run.

Keep authorization in your application functions. Model instructions are not a replacement for checking whether the current user may read or change data.

Streaming

The client-exposed agent has a durable thread journal: useAgent restores its authorized snapshot and observes text parts, tool calls, progress, approvals, selected state, nested execution, and terminal output incrementally. Mounting the hook never creates a thread or starts a run. See Agent streaming and Threads and recovery.