@hbbio/nanoagent
    Preparing search index...

    @hbbio/nanoagent

    NanoAgent Logo

    NanoAgent

    NanoAgent is a micro‑framework (≈ 1 kLOC) for running LLM‑powered agents in pure TypeScript with zero runtime dependencies outside of bun. You only need your favorite chat models: OpenAI, or a local engine like Ollama.

    Why another agent runtime?
    Model Context Protocol (MCP) is bringing the opportunity to de-clutter agent frameworks. Most features should be tools, retrieval sources, etc. in a standard JSON envelope, then hand that context to any model. NanoAgent focuses on one job: the control loop and leaves RAG, vector search, databases and cloud calls to MCP‑compatible tools. The result is a tiny, transparent core you can audit in an afternoon.

    Note that this projects implements a few extensions over the current specifications of MCP and/or tool calling.

    • Pure functions, repeatable runs – every step returns a new AgentState; nothing mutates in place.
    • Deterministic steppingstepAgent drives exactly one model call → tool call → state update.
    • Built‑in stuck detection – empty answers, duplicate assistant messages or missing tool output trigger a recovery hook.
    • Halting reasonsawait_user, tool_error, done, stopped.
    • Multi‑stage workflows – chain Sequence objects for wizard‑style flows.
    • JSON‑Schema tools – validate inputs at the boundary, patch memory with pure lambdas.
    • No hidden packages – just TypeScript.
    • First‑class Bun support – fast test runner, edge‑ready.
    import {
    type AgentContext,
    type AgentState,
    type ChatMemory,
    ChatModel,
    Llama32,
    SystemMessage,
    ToolRegistry,
    UserMessage,
    content,
    lastMessageIncludes,
    loopAgent,
    tool,
    } from "@hbbio/nanoagent";

    // 1) a trivial tool
    const echo = tool(
    "echo",
    "Echo user input back in uppercase",
    {
    type: "object",
    properties: { txt: { type: "string" } },
    required: ["txt"],
    },
    async ({ txt }: { txt: string }) => content(txt.toUpperCase()),
    );

    // 2) agent context
    const ctx: AgentContext<ChatMemory> = {
    registry: new ToolRegistry({ echo }),
    isFinal: lastMessageIncludes("HELLO"),
    };

    // 3) initial state
    const init: AgentState<ChatMemory> = {
    model: new ChatModel(Llama32),
    messages: [
    SystemMessage(
    "You must call the `echo` tool once. Reply very concisely and NEVER ASK any further question to the user!",
    ),
    UserMessage(
    "Call the tool with the parameter `hello` and tell me what is the response",
    ),
    ],
    };

    // 4) run and display the whole conversation
    const done = await loopAgent(ctx, init);
    console.log(done.messages);

    Run it with Bun:

    bun run examples/echo.ts
    
    Concept What it holds
    AgentState Immutable snapshot: model driver, messages, memory, halt
    AgentContext Pure hooks: goal test, tool registry, controller, etc.
    stepAgent One transition – may call the model and at most one tool
    loopAgent While‑loop around stepAgent until a halt condition
    Sequence Wrapper that chains multi‑stage flows

    Memory is plain JSON. Tools may patch it by returning { memPatch(state)‐>newState }.

    const seq1 = new Sequence(ctxStage1, state1, { maxSteps: 8 });
    const { final, history } = await runWorkflow(seq1);

    Each stage may produce a fresh context and state; user input handling can be preserved across stages.

    NanoAgent ships a tiny MCP server helper (serveMCP) and an MCP client (MCPClient). Your tools can therefore live outside the agent process—behind an HTTP endpoint—yet feel local.

    • RAG anywhere – retrieval can run on an edge function, a GPU pod, or a browser worker.
    • Horizontal scaling – tools are stateless HTTP handlers; use normal infra.
    • Polyglot – heavy lifting in Go, Python or Rust without bloating the TS runtime.
    import { ToolRegistry, serveMCP, tool, content } from "@hbbio/nanoagent";

    const tools = {
    echo: tool(
    "echo",
    "Echo input back",
    {
    type: "object",
    properties: { text: { type: "string" } },
    required: ["text"],
    },
    async ({ text }) => content(`Echo: ${text}`),
    ),
    };

    serveMCP(new ToolRegistry(tools), 3123); // → http://localhost:3123/v1/…
    import { MCPClient, ToolRegistry } from "@hbbio/nanoagent";

    const mcp = new MCPClient("http://localhost:3123");
    const echoT = await mcp.registeredTool("echo");

    const ctx = {
    registry: new ToolRegistry({ echoT }),
    /* … other AgentContext props … */
    };

    MCPClient provides the following features:

    • listTools(): Discover server capabilities (with a default 5‑minute cache)
    • tool(name): Fetch a single tool
    • callTool(name, input, memory?): Plain HTTP tool call
    • registeredTool(name): Wrap a remote tool so agents can call it seamlessly
    bun add nanoagent   # or:  npm i nanoagent  pnpm add nanoagent  yarn add nanoagent
    

    The package is published as ES 2020 modules with type‑definitions included.

    export CHATGPT_KEY=...
    

    And then create instances with:

    import { ChatModel, ChatGPT4o } from "@hbbio/nanoagent";
    const model = new ChatModel(ChatGPT4o);

    or one of the predefined model names. Call any present or future model using chatgpt("name").

    By default Ollama host is http://localhost:11434, but you can optionally define another host:

    export OLLAMA_HOST=...
    

    Then run any model, such as:

    import { ChatModel, MistralSmall } from "@hbbio/nanoagent";
    const model = new ChatModel(MistralSmall);

    Pass { debug: true } to stepAgent, loopAgent or Sequence. You will see:

    STEP id=- msgs=3 last=assistant halted=-
    💬 { role: "assistant", … }
    💾 memory keys []

    Provide your own logger via options.logger.

    Contributions are welcome: Make sure that all tests pass and that coverage includes your new code and feel free to submit PRs.

    Please follow the coding guidelines and keep the project free of extra dependencies.

    Written by Henri Binsztok and released under the MIT license.