Server-Side & Non-React Agents

Glove's core package (glove-core) runs anywhere Node.js does. You don't need React, Next.js, or a browser to build an agent. This guide covers how to use the Glove builder directly to create agents for CLI tools, backend services, WebSocket servers, or any non-browser environment.

Minimal Example

The simplest possible agent needs four things: a store, a model, a display manager, and a system prompt.

typescript
import { Glove, Displaymanager, createAdapter } from "glove-core";
import z from "zod";

// 1. In-memory store (see below for implementation)
const store = new MemoryStore("my-session");

// 2. Model adapter from the provider registry
const model = createAdapter({
  provider: "anthropic",
  model: "claude-sonnet-4-20250514",
  stream: true,
});

// 3. Display manager — required, but can be empty if your
//    tools don't need interactive UI
const dm = new Displaymanager();

// 4. Build the agent
const agent = new Glove({
  store,
  model,
  displayManager: dm,
  systemPrompt: "You are a helpful assistant.",
  compaction_config: {
    compaction_instructions: "Summarize the conversation.",
  },
})
  .fold({
    name: "get_weather",
    description: "Get weather for a city.",
    inputSchema: z.object({ city: z.string() }),
    async do(input) {
      const res = await fetch(`https://wttr.in/${encodeURIComponent(input.city)}?format=j1`);
      const data = await res.json();
      return { status: "success", data: data.current_condition?.[0] ?? {} };
    },
  })
  .build();

// 5. Send a message
const result = await agent.processRequest("What's the weather in Tokyo?");
console.log(result.messages[0]?.text);

That's it. No React, no Next.js, no browser. The agent will call the model, execute tools, loop until done, and return the final result.

In-Memory Store

For server-side agents that don't need persistence across restarts, implement StoreAdapter with plain arrays and counters. This is the minimum viable store.

typescript
import type { StoreAdapter, Message } from "glove-core";

class MemoryStore implements StoreAdapter {
  identifier: string;
  private messages: Message[] = [];
  private tokenCount = 0;
  private turnCount = 0;

  constructor(id: string) {
    this.identifier = id;
  }

  async getMessages() { return this.messages; }
  async appendMessages(msgs: Message[]) { this.messages.push(...msgs); }
  async getTokenCount() { return this.tokenCount; }
  async addTokens(count: number) { this.tokenCount += count; }
  async getTurnCount() { return this.turnCount; }
  async incrementTurn() { this.turnCount++; }
  async resetCounters() { this.tokenCount = 0; this.turnCount = 0; }
}

For persistent storage, use the built-in SqliteStore:

typescript
import { SqliteStore } from "glove-core";

const store = new SqliteStore({
  dbPath: "./my-agent.db",
  sessionId: "session-123",
});

The store interface is intentionally simple. You can implement it against Redis, Postgres, DynamoDB, or any backend. See the StoreAdapter reference for the full interface, including optional methods for tasks and permissions.

Tools Without Display

Most server-side tools don't need a display manager. The do function receives the display manager as its second argument, but you can simply ignore it.

typescript
import z from "zod";

agent.fold({
  name: "search_database",
  description: "Search the product database.",
  inputSchema: z.object({
    query: z.string(),
    limit: z.number().optional().default(10),
  }),
  async do(input) {
    // No display manager needed — just return the result
    const results = await db.products.search(input.query, input.limit);
    return {
      status: "success",
      data: results,
    };
  },
});

The do function returns a ToolResultData object. For convenience, you can also return a plain string — the framework wraps it into { status: "success", data: yourString } automatically.

typescript
async do(input) {
  const weather = await fetchWeather(input.city);
  // Returning a string works too
  return `${weather.temp}°C, ${weather.condition}`;
}

Subscribing to Events

Subscribers let you observe the agent in real time: streaming text, tool calls, results, and compaction events. This is how you wire up logging, WebSocket forwarding, metrics, or any side-channel output.

typescript
import type { SubscriberAdapter, SubscriberEvent, SubscriberEventDataMap } from "glove-core";

class LogSubscriber implements SubscriberAdapter {
  async record<T extends SubscriberEvent["type"]>(
    event_type: T,
    data: SubscriberEventDataMap[T],
  ) {
    switch (event_type) {
      case "text_delta": {
        const e = data as SubscriberEventDataMap["text_delta"];
        process.stdout.write(e.text);
        break;
      }
      case "tool_use": {
        const e = data as SubscriberEventDataMap["tool_use"];
        console.log(`\n[tool] ${e.name}(${JSON.stringify(e.input)})`);
        break;
      }
      case "tool_use_result": {
        const e = data as SubscriberEventDataMap["tool_use_result"];
        console.log(`[result] ${e.tool_name}: ${e.result.status}`);
        break;
      }
      case "model_response":
      case "model_response_complete": {
        const e = data as SubscriberEventDataMap["model_response"];
        console.log(`\n[done] tokens: ${e.tokens_in ?? 0} in, ${e.tokens_out ?? 0} out`);
        break;
      }
    }
  }
}

// Register before or after build()
gloveBuilder.addSubscriber(new LogSubscriber());

Events are fully typed. See the Subscriber Events reference for all event types and their data shapes.

Simplified subscriber

If you only care about a few events, you don't need the generic signature. A simple untyped subscriber works fine:

typescript
const subscriber: SubscriberAdapter = {
  async record(event_type, data) {
    if (event_type === "text_delta") {
      process.stdout.write((data as any).text);
    }
  },
};

WebSocket Server Pattern

The coding agent example demonstrates the full pattern for a WebSocket-based agent server. Each connected client gets its own session with an isolated store, display manager, and subscriber.

typescript
import { WebSocketServer, WebSocket } from "ws";
import {
  Glove, Displaymanager, SqliteStore,
  createAdapter, type SubscriberAdapter,
} from "glove-core";

const wss = new WebSocketServer({ port: 8080 });

wss.on("connection", (ws) => {
  // Each connection gets its own agent instance
  const store = new SqliteStore({ dbPath: "./agent.db", sessionId: randomUUID() });
  const dm = new Displaymanager();

  // Forward events to the WebSocket client
  const subscriber: SubscriberAdapter = {
    async record(event_type, data) {
      ws.send(JSON.stringify({ type: event_type, data }));
    },
  };

  const agent = new Glove({
    store,
    model: createAdapter({ provider: "anthropic", stream: true }),
    displayManager: dm,
    systemPrompt: "You are a helpful assistant.",
    compaction_config: {
      compaction_instructions: "Summarize the conversation.",
    },
  })
    .fold({ /* ... register tools ... */ })
    .addSubscriber(subscriber)
    .build();

  // Handle display slots — forward to client, resolve on response
  dm.subscribe(async (stack) => {
    for (const slot of stack) {
      if (dm.resolverStore.has(slot.id)) {
        ws.send(JSON.stringify({
          type: "slot_push",
          data: { id: slot.id, renderer: slot.renderer, input: slot.input },
        }));
      }
    }
  });

  ws.on("message", async (raw) => {
    const msg = JSON.parse(raw.toString());

    switch (msg.type) {
      case "chat":
        await agent.processRequest(msg.text);
        break;
      case "slot_resolve":
        dm.resolve(msg.slotId, msg.value);
        break;
      case "abort":
        // Use AbortController for cancellation
        break;
    }
  });
});

When You Need a Display Manager

The DisplayManager is required in GloveConfig, but for purely autonomous agents you can pass an empty new Displaymanager() and never use it. It only matters when tools need to interact with a user mid-execution.

PatternDisplay Manager UsageExample
Autonomous agentEmpty — tools return results directlyCLI scripts, cron jobs, batch processing
Interactive serverpushAndWait for user input, pushAndForget for statusWebSocket servers, Slack bots, coding agents
Terminal UISubscribe to stack changes, render with ink/blessedWeather agent, interactive CLI tools

Interactive tools with pushAndWait

When a tool calls display.pushAndWait(), the agent loop blocks until someone calls dm.resolve(slotId, value). This is how you build tools that ask the user for confirmation, collect form input, or present choices.

typescript
gloveBuilder.fold({
  name: "confirm_action",
  description: "Ask the user to confirm a destructive action.",
  inputSchema: z.object({ action: z.string() }),
  async do(input, display) {
    // This blocks until your UI layer resolves the slot
    const confirmed = await display.pushAndWait({
      renderer: "confirm",
      input: { message: `Proceed with: ${input.action}?` },
    });

    if (!confirmed) {
      return { status: "error", data: null, message: "User cancelled." };
    }

    // ... perform the action ...
    return { status: "success", data: "Action completed." };
  },
});

// In your WebSocket/terminal handler, resolve when the user responds:
dm.resolve(slotId, true);  // or false to cancel

Non-blocking display with pushAndForget

Use pushAndForget for status indicators, progress updates, or any display that doesn't need user input.

typescript
async do(input, display) {
  // Show a loading indicator (non-blocking)
  const slotId = await display.pushAndForget({
    renderer: "loading",
    input: { message: "Fetching data..." },
  });

  const data = await fetchData(input.query);

  // Remove the loading indicator
  display.removeSlot(slotId);

  // Show the result (non-blocking)
  await display.pushAndForget({
    renderer: "result_card",
    input: data,
  });

  return { status: "success", data };
}

Abort & Cancellation

Pass an AbortSignal to processRequest to support cancellation. The signal propagates to the model adapter and tool execution.

typescript
const controller = new AbortController();

// Cancel after 30 seconds
setTimeout(() => controller.abort(), 30_000);

try {
  const result = await agent.processRequest("Analyze this codebase", controller.signal);
} catch (err) {
  if (err instanceof AbortError) {
    console.log("Request was cancelled.");
  }
}

Mark tools as unAbortable if they perform mutations that must complete even when the request is cancelled:

typescript
gloveBuilder.fold({
  name: "save_to_database",
  description: "Persist data to the database.",
  inputSchema: z.object({ data: z.unknown() }),
  unAbortable: true,  // Runs to completion even if abort fires
  async do(input) {
    await db.save(input.data);
    return { status: "success", data: "Saved." };
  },
});

Runtime Model Swapping

After building, you can swap the model adapter at runtime. This is useful for letting users choose their preferred model mid-session.

typescript
const agent = gloveBuilder.build();

// Later — swap to a different model
agent.setModel(createAdapter({
  provider: "openai",
  model: "gpt-4.1",
  stream: true,
}));

// Next processRequest uses the new model
await agent.processRequest("Continue our conversation.");

Optional Store Features

The StoreAdapter has several optional methods that unlock features automatically when implemented.

MethodsFeatureWhat happens
getTasks, addTasks, updateTaskBuilt-in task toolThe glove_update_tasks tool is auto-registered, letting the model track work progress.
getPermission, setPermissionPermission systemTools with requiresPermission: true will check/store user consent before execution.

If your store doesn't implement these methods, the features are silently disabled. No errors, no configuration needed.

Common Patterns

CLI script

typescript
#!/usr/bin/env npx tsx
import { Glove, Displaymanager, createAdapter } from "glove-core";
import z from "zod";

const agent = new Glove({
  store: new MemoryStore("cli"),
  model: createAdapter({ provider: "anthropic", stream: true }),
  displayManager: new Displaymanager(),
  systemPrompt: "You analyze code and report issues.",
  compaction_config: { compaction_instructions: "Summarize findings." },
})
  .fold({
    name: "read_file",
    description: "Read a file from disk.",
    inputSchema: z.object({ path: z.string() }),
    async do(input) {
      const fs = await import("fs/promises");
      return { status: "success", data: await fs.readFile(input.path, "utf-8") };
    },
  })
  .build();

const result = await agent.processRequest(`Review ${process.argv[2]}`);
console.log(result.messages.at(-1)?.text);

Background worker

typescript
import { Glove, Displaymanager, SqliteStore, createAdapter } from "glove-core";

async function processJob(job: { id: string; prompt: string }) {
  const store = new SqliteStore({ dbPath: "./jobs.db", sessionId: job.id });

  const agent = new Glove({
    store,
    model: createAdapter({ provider: "openai", stream: false }),
    displayManager: new Displaymanager(),
    systemPrompt: "You process data analysis jobs.",
    compaction_config: { compaction_instructions: "Summarize analysis." },
  })
    .fold({ /* tools */ })
    .build();

  return agent.processRequest(job.prompt);
}

// Process jobs from a queue
for await (const job of jobQueue) {
  const result = await processJob(job);
  await jobQueue.complete(job.id, result);
}

Multi-turn conversation loop

typescript
import * as readline from "readline/promises";

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const agent = buildAgent(); // your Glove builder setup

// Stream text as it arrives
agent.addSubscriber({
  async record(event_type, data) {
    if (event_type === "text_delta") {
      process.stdout.write((data as any).text);
    }
  },
});

while (true) {
  const input = await rl.question("\n> ");
  if (input === "exit") break;

  // processRequest appends to the store, so context accumulates
  await agent.processRequest(input);
  console.log(); // newline after streamed response
}

How It Works

When you call agent.processRequest(message), the framework runs the following loop internally:

  1. Append the user message to the store
  2. Load messages from the context (filtered to the last compaction point)
  3. Send messages + tool definitions to the model via PromptMachine
  4. Append the model's response to the store
  5. If the model made tool calls: execute them via Executor, append tool results, and go to step 2
  6. If no tool calls: check compaction thresholds via Observer, then return the result

Subscribers are notified at each step. The display manager is only involved when a tool explicitly calls pushAndWait or pushAndForget.

What you can skip

Compared to the React integration (glove-react), a server-side agent doesn't need:

You work directly with glove-core: the Glove builder, Displaymanager, SqliteStore (or your own store), and model adapters from createAdapter or instantiated directly (e.g. new AnthropicAdapter({ ... })).