Little documentation
Guides and APICookbook

Next.js useChat Route

Wire streamHarness() into a Next.js App Router route consumed by AI SDK useChat().

Little Harness: published on npm as little-harness@0.2.0-alpha.0 (2026-10-05). Sections labeled UNRELEASED describe source-only changes. Release status

This recipe uses a normal AI SDK chat client and a Next.js App Router route. Little Harness owns the session workspace, artifacts, trace, tool context, and AI SDK-compatible stream response.

Client

"use client";

import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";

export function AssistantChat({
  chatId,
  userId,
}: {
  chatId: string;
  userId: string;
}) {
  const { messages, sendMessage, status } = useChat({
    transport: new DefaultChatTransport({
      api: "/api/chat",
      body: { chatId, userId },
    }),
  });

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        const input = new FormData(event.currentTarget).get("message");
        if (typeof input === "string" && input.trim()) {
          sendMessage({ text: input });
          event.currentTarget.reset();
        }
      }}
    >
      {messages.map((message) => (
        <div key={message.id}>{message.role}</div>
      ))}
      <input name="message" disabled={status !== "ready"} />
      <button type="submit">Send</button>
    </form>
  );
}

Route

import type { UIMessage } from "ai";
import { createHarness, localHost, streamHarness } from "@little-stack/harness";
import { queryAccounts } from "@/lib/agent-tools";
import { modelForRequest, systemForRequest } from "@/lib/remote-agent-config";

export const runtime = "nodejs";

const harness = createHarness({
  host: localHost({ dataDir: ".little-harness" }),
  model: modelForRequest("default"),
  system: "Use the available tools and write large outputs to artifacts.",
  tools: { queryAccounts },
  runtime: {
    bash: true,
    network: false,
  },
});

export async function POST(req: Request) {
  const body = await req.json() as {
    messages: UIMessage[];
    chatId: string;
    userId: string;
  };

  const result = streamHarness({
    harness,
    messages: body.messages,
    session: body.chatId,
    extraBody: {
      userId: body.userId,
    },
    model: modelForRequest(body.userId),
    system: systemForRequest(body.userId),
    temperature: 0.2,
  });

  return result.toUIMessageStreamResponse();
}

Use runtime = "nodejs" for Local Host and the default just-bash runtime. Edge routes are a separate hosting target.

The harness can stay reusable even when model policy or system instructions come from request-time configuration. Pass model, system, and call settings to streamHarness() instead of rebuilding the harness for every request.

Bash Policy

The built-in bash tool is enabled by default. Make that choice explicit when the app has a policy decision to document:

const harness = createHarness({
  host: localHost({ dataDir: ".little-harness" }),
  model,
  runtime: {
    bash: false,
    network: false,
  },
});

You can also disable bash for one call:

streamHarness({
  harness,
  messages,
  runtime: { bash: false },
});

Disabling bash removes the built-in command tool from the model. App tools still receive ctx.files, ctx.artifacts, ctx.extraBody, and ctx.abortSignal.

On this page