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.
The simplest possible agent needs four things: a store, a model, a display manager, and a system prompt.
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.
For server-side agents that don't need persistence across restarts, implement StoreAdapter with plain arrays and counters. This is the minimum viable store.
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:
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.
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.
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.
async do(input) {
const weather = await fetchWeather(input.city);
// Returning a string works too
return `${weather.temp}°C, ${weather.condition}`;
}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.
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.
If you only care about a few events, you don't need the generic signature. A simple untyped subscriber works fine:
const subscriber: SubscriberAdapter = {
async record(event_type, data) {
if (event_type === "text_delta") {
process.stdout.write((data as any).text);
}
},
};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.
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;
}
});
});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.
| Pattern | Display Manager Usage | Example |
|---|---|---|
| Autonomous agent | Empty — tools return results directly | CLI scripts, cron jobs, batch processing |
| Interactive server | pushAndWait for user input, pushAndForget for status | WebSocket servers, Slack bots, coding agents |
| Terminal UI | Subscribe to stack changes, render with ink/blessed | Weather agent, interactive CLI tools |
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.
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 cancelUse pushAndForget for status indicators, progress updates, or any display that doesn't need user input.
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 };
}Pass an AbortSignal to processRequest to support cancellation. The signal propagates to the model adapter and tool execution.
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:
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." };
},
});After building, you can swap the model adapter at runtime. This is useful for letting users choose their preferred model mid-session.
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.");The StoreAdapter has several optional methods that unlock features automatically when implemented.
| Methods | Feature | What happens |
|---|---|---|
| getTasks, addTasks, updateTask | Built-in task tool | The glove_update_tasks tool is auto-registered, letting the model track work progress. |
| getPermission, setPermission | Permission system | Tools 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.
#!/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);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);
}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
}When you call agent.processRequest(message), the framework runs the following loop internally:
PromptMachineExecutor, append tool results, and go to step 2Observer, then return the resultSubscribers are notified at each step. The display manager is only involved when a tool explicitly calls pushAndWait or pushAndForget.
Compared to the React integration (glove-react), a server-side agent doesn't need:
GloveProvider / useGlove — those are React hooks for state managementdefineTool / ToolConfig — those add React renderers. Use .fold() directly<Render> — the headless React component for chat UIscreateEndpointModel / createRemoteStore — those are client-side adapters for talking to a server. On the server, you use model adapters directlyYou 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({ ... })).