| name | glove |
| description | Expert guide for building AI-powered applications with the Glove framework. Use when working with glove-core, glove-react, glove-next, tools, display stack, model adapters, stores, any Glove example project, or deploying agents as sandboxed runtime services with Glovebox (glovebox / glovebox-kit / glovebox-client). |
Glove Framework — Development Guide
You are an expert on the Glove framework. Use this knowledge when writing, debugging, or reviewing Glove code.
What Glove Is
Glove is an open-source TypeScript framework for building AI-powered applications. Users describe what they want in conversation, and an AI decides which capabilities (tools) to invoke. Developers define tools and renderers; Glove handles the agent loop.
Repository: https://github.com/porkytheblack/glove
Docs site: https://glove.dterminal.net
License: MIT (dterminal)
Package Overview
| Package | Purpose | Install |
|---|
glove-core | Runtime engine: agent loop, tool execution, display manager, model adapters, MemoryStore default in-memory StoreAdapter (browser-safe — no native deps) | pnpm add glove-core |
glove-sqlite | Deprecated. SqliteStore — persistent SQLite-backed store. Prefer MemoryStore from glove-core for prototyping or BYO StoreAdapter for production. | pnpm add glove-sqlite |
glove-react | React hooks (useGlove), GloveClient, GloveProvider, defineTool, <Render>, MemoryStore, ToolConfig with colocated renderers | pnpm add glove-react |
glove-next | One-line Next.js API route handler (createChatHandler) for streaming SSE | pnpm add glove-next |
glove-mcp | Bridge MCP servers into a Glove agent: mountMcp, connectMcp, bridgeMcpTool, McpAdapter, discovermcp discovery subagent. Opt-in OAuth helpers at glove-mcp/oauth. | pnpm add glove-mcp |
glove-memory | Schema-first memory layer with four sibling subsystems: entity graph, episodic timeline, resource filesystem, and ambient context. BYO storage via the adapter contracts; reference in-memory adapters ship for dev/test. Storage backends (glove-memory-sqlite, glove-memory-postgres) are companion packages — not yet released. Draft v0.1. | pnpm add glove-memory |
glove-mesh | Inter-agent communication on top of the inbox primitive: mountMesh, MeshAdapter (BYO transport), MeshNetwork + InMemoryMeshAdapter reference impl. Four tools — glove_mesh_send_message, glove_mesh_broadcast, glove_mesh_list_agents, glove_mesh_acknowledge. No auth (consumer's job). | pnpm add glove-mesh |
glove-continuum-signal | Subprocess-based runtime substrate for agent collaboration across time. Two modes: triggered (cold, spawn-per-wakeup) and concurrent (warm, long-lived subprocess notified inline). agent() builder, ContinuumRunner (discovery + supervision + IPC), ContinuumAdapter (BYO persistence; MemoryAdapter default), ContinuumSubscriber (lifecycle + forwarded Glove events). Pairs with glove-mesh for inter-agent talk — substrate provides the supervised subprocesses, mesh provides the messaging. | pnpm add glove-continuum-signal |
glovebox-core | Authoring + glovebox build CLI. glovebox.wrap(runnable, config) packages a built Glove agent into a deployable artifact (Dockerfile + nixpacks.toml + bundled server + manifest + auth key). Storage DSL (rule.*, composite) and wire protocol types live here too. The unscoped glovebox name is taken on npm — install as glovebox-core; the CLI binary is still glovebox. | pnpm add glovebox-core |
glovebox-kit | In-container runtime. startGlovebox({ app, port, key, manifestPath, ... }) boots the WS server, auto-injects glovebox skills/hooks, and bridges Glove's display stack onto the wire. Storage adapters: InlineStorage, UrlStorage, LocalServerStorage, S3Storage. | (transitive — bundled by glovebox build) |
glovebox-client | Client SDK. GloveboxClient.make({ endpoints }), client.box(name).prompt(text, { files }), result.read(name), box.environment(). Symmetric ClientStorage interface with a default inline+url implementation. | pnpm add glovebox-client |
Most projects need just glove-react + glove-next. glove-core is included as a dependency of glove-react. For server-side or non-React agents, use glove-core directly — see Server-Side Agents below. For agents that need third-party tools via the Model Context Protocol, see MCP Integration.
What's in the framework
glove-core — agent loop, tools, display stack, store/model/subscriber adapters, context compaction, inbox, hooks/skills/subagents, MemoryStore default in-memory StoreAdapter.
glove-react — colocated renderers via defineTool, <Render>, useGlove, MemoryStore, createRemoteStore, createEndpointModel, createRemoteModel.
glove-next — createChatHandler (one-line SSE route), voice token handler.
glove-sqlite — deprecated; SqliteStore for persistence (server-side only).
glove-voice — full-duplex voice pipeline: STT/TTS/VAD adapters, GloveVoice, useGloveVoice, useGlovePTT, <VoicePTTButton>.
glove-mcp — MCP servers as first-class tools: mountMcp, connectMcp, bridgeMcpTool, McpAdapter (consumer-supplied per-conversation seam). discovermcp discovery subagent (registered via glove.defineSubAgent(discoverySubAgent({...}))). Opt-in OAuth helpers at glove-mcp/oauth (runMcpOAuth, FsOAuthStore, MemoryOAuthStore, McpOAuthProvider).
glove-memory — Memory layer with four sibling subsystems (entity graph / episodic timeline / resource filesystem / ambient context) and matching useMemoryReader / useMemoryCurator, useEpisodicReader / useEpisodicCurator, useResourcesReader / useResourcesCurator, and useContext helper families. Storage-agnostic adapter contracts plus reference InMemory* adapters for dev/test.
glove-mesh — Inter-agent messaging on top of the inbox primitive: mountMesh(glove, { adapter, identity }) registers an agent and folds glove_mesh_send_message / _broadcast / _list_agents / _acknowledge. MeshAdapter is the consumer-supplied transport (BYO); ships InMemoryMeshAdapter + MeshNetwork for in-process dev/test. Each agent keeps its own inbox; incoming messages land as resolved InboxItems so the existing inbox-injection path surfaces them on the next ask(). No authentication — sender ids are unverified.
glove-continuum-signal — Subprocess-based runtime substrate modeled on station-signal but agent-shaped. agent("name").input(zod).triggered()|.concurrent() builder produces branded agents; ContinuumRunner discovers them from a directory, pre-warms concurrent ones, dispatches triggered runs from an adapter queue (per-spawn isolation), and routes notify IPC envelopes to warm subprocesses inline. ContinuumAdapter is the persistence contract (MemoryAdapter ships; consumers BYO for SQLite/Postgres/etc.). Single fat onAgentEvent(envelope) subscriber forwards every Glove SubscriberEvent from any child upstream with the agent identity attached. Mesh integrates by being mounted per-agent inside the factory — no special IPC machinery.
Architecture at a Glance
User message → Agent Loop → Model decides tool calls → Execute tools → Feed results back → Loop until done
↓
Display Stack (pushAndWait / pushAndForget)
↓
React renders UI slots
Core Concepts
- Agent — AI coordinator that replaces router/navigation logic. Reads tools, decides which to call.
- Tool — A capability: name, description, inputSchema (Zod),
do function, optional render + renderResult.
- Display Stack — Stack of UI slots tools push onto.
pushAndWait blocks tool; pushAndForget doesn't.
- Display Strategy — Controls slot visibility lifecycle:
"stay", "hide-on-complete", "hide-on-new".
- renderData — Client-only data returned from
do() that is NOT sent to the AI model. Used by renderResult for history rendering.
- Adapter — Pluggable interfaces for Model, Store, DisplayManager, and Subscriber. Swap providers without changing app code.
- Context Compaction — Auto-summarizes long conversations to stay within context window limits. The store preserves full message history (so frontends can display the entire chat), while
Context.getMessages() splits at the last compaction summary so the model only sees post-compaction context. Summary messages are marked with is_compaction: true.
- Inbox — Persistent async mailbox for cross-instance communication. An agent posts a request (text) that can't be resolved now; an external service resolves it later (text response). Resolved items are automatically injected into the agent's context on the next
ask() call. Items can be blocking (agent should wait) or non-blocking. Built-in glove_post_to_inbox tool auto-registered when store supports inbox methods.
- Extensions (hooks, skills, subagents) —
/hookname runs a builder-defined handler with full agent controls (force compaction, swap model, short-circuit a turn). /skillname materialises a synthetic user message before the real one (marked is_skill_injection: true). defineSubAgent({ name, factory }) registers a subagent the main agent can route to via the auto-registered glove_invoke_subagent tool — the user's @name text is NOT parsed by glove, it reaches the model verbatim and acts as a routing signal (mirrors Claude Code's subagent convention). / tokens are replaced with non-triggerable placeholders ([invoked_extension__hook_<name>] / [invoked_extension__skill_<name>]) so the model sees that an extension fired without the placeholder re-binding on a future parse; unbound / tokens stay untouched (so /usr/local survives). Skills can be exposed to the agent (exposeToAgent: true) so the agent pulls them in via the auto-registered glove_invoke_skill tool.
- MCP catalogue + adapter —
glove-mcp introduces two pieces: a static McpCatalogueEntry[] describing servers the app supports, and a per-conversation McpAdapter holding active ids and resolving access tokens. mountMcp reloads previously active servers and registers a discovermcp discovery subagent (via glove.defineSubAgent(discoverySubAgent({...}))) — the model invokes it through glove_invoke_subagent({ name: "discovermcp", prompt: "..." }) to find and activate servers mid-conversation.
- Mesh network —
glove-mesh lets Glove agents send each other messages via a consumer-supplied MeshAdapter (transport). Each agent keeps its own StoreAdapter+inbox; when A sends to B, the framework drops a status: "resolved" InboxItem into B's store so the existing inbox-injection path surfaces it on B's next ask(). Blocking sends insert a pending blocking: true item that resolves on ack or in_reply_to reply. No auth in v1.
- Continuum (subprocess runtime) —
glove-continuum-signal supervises Glove agents as Node subprocesses. Triggered agents are cold and spawn-per-wakeup (resume from a persistent StoreAdapter each time); concurrent agents are warm and notified inline. The agent() builder forks into mode-specific types after .triggered() / .concurrent() so mode-specific setters (.retries(), .every()) are type-level guarded. Persistent stores configured via .store(name => StoreAdapter) so the same store implementation can be injected across an agent fleet. Parent is single source of truth for run status (children only emit IPC). Mesh integration: mount in the factory — substrate stays out of inter-agent protocol.
Quick Start (Next.js)
1. Install
pnpm add glove-core glove-react glove-next zod
2. Server route
import { createChatHandler } from "glove-next";
export const POST = createChatHandler({
provider: "anthropic",
model: "claude-sonnet-4-20250514",
});
Set ANTHROPIC_API_KEY (or OPENAI_API_KEY, etc.) in .env.local.
3. Define tools with defineTool
import { GloveClient, defineTool } from "glove-react";
import type { ToolConfig } from "glove-react";
import { z } from "zod";
const inputSchema = z.object({
question: z.string().describe("The question to display"),
options: z.array(z.object({
label: z.string().describe("Display text"),
value: z.string().describe("Value returned when selected"),
})),
});
const askPreferenceTool = defineTool({
name: "ask_preference",
description: "Present options for the user to choose from.",
inputSchema,
displayPropsSchema: inputSchema,
resolveSchema: z.string(),
displayStrategy: "hide-on-complete",
async do(input, display) {
const selected = await display.pushAndWait(input);
return {
status: "success" as const,
data: `User selected: ${selected}`,
renderData: { question: input.question, selected },
};
},
render({ props, resolve }) {
return (
<div>
<p>{props.question}</p>
{props.options.map(opt => (
<button key={opt.value} onClick={() => resolve(opt.value)}>
{opt.label}
</button>
))}
</div>
);
},
renderResult({ data }) {
const { question, selected } = data as { question: string; selected: string };
return <div><p>{question}</p><span>Selected: {selected}</span></div>;
},
});
const getDateTool: ToolConfig = {
name: "get_date",
description: "Get today's date",
inputSchema: z.object({}),
async do() { return { status: "success", data: new Date().toLocaleDateString() }; },
};
export const gloveClient = new GloveClient({
endpoint: "/api/chat",
systemPrompt: "You are a helpful assistant.",
tools: [askPreferenceTool, getDateTool],
});
4. Provider + Render
"use client";
import { GloveProvider } from "glove-react";
import { gloveClient } from "@/lib/glove";
export function Providers({ children }: { children: React.ReactNode }) {
return <GloveProvider client={gloveClient}>{children}</GloveProvider>;
}
"use client";
import { useGlove, Render } from "glove-react";
export default function Chat() {
const glove = useGlove();
return (
<Render
glove={glove}
strategy="interleaved"
renderMessage={({ entry }) => (
<div><strong>{entry.kind === "user" ? "You" : "AI"}:</strong> {entry.text}</div>
)}
renderStreaming={({ text }) => <div style={{ opacity: 0.7 }}>{text}</div>}
/>
);
}
Or use useGlove() directly for full manual control:
"use client";
import { useState } from "react";
import { useGlove } from "glove-react";
export default function Chat() {
const { timeline, streamingText, busy, slots, sendMessage, renderSlot, renderToolResult } = useGlove();
const [input, setInput] = useState("");
return (
<div>
{timeline.map((entry, i) => (
<div key={i}>
{entry.kind === "user" && <p><strong>You:</strong> {entry.text}</p>}
{entry.kind === "agent_text" && <p><strong>AI:</strong> {entry.text}</p>}
{entry.kind === "tool" && (
<>
<p>Tool: {entry.name} — {entry.status}</p>
{entry.renderData !== undefined && renderToolResult(entry)}
</>
)}
</div>
))}
{streamingText && <p style={{ opacity: 0.7 }}>{streamingText}</p>}
{slots.map(renderSlot)}
<form onSubmit={(e) => { e.preventDefault(); sendMessage(input.trim()); setInput(""); }}>
<input value={input} onChange={(e) => setInput(e.target.value)} disabled={busy} />
<button type="submit" disabled={busy}>Send</button>
</form>
</div>
);
}
Server-Side Agents
For CLI tools, backend services, WebSocket servers, or any non-browser environment, use glove-core directly. No React, Next.js, or browser required.
Minimal Setup
import { Glove, Displaymanager, MemoryStore, createAdapter } from "glove-core";
import z from "zod";
const store = new MemoryStore("my-session");
const agent = new Glove({
store,
model: createAdapter({ provider: "anthropic", stream: true }),
displayManager: new Displaymanager(),
systemPrompt: "You are a helpful assistant.",
serverMode: true,
compaction_config: {
compaction_instructions: "Summarize the conversation.",
},
})
.fold({
name: "search",
description: "Search the database.",
inputSchema: z.object({ query: z.string() }),
async do(input) {
const results = await db.search(input.query);
return { status: "success", data: results };
},
})
.build();
const result = await agent.processRequest("Find recent orders");
console.log(result.messages[0]?.text);
MemoryStore from glove-core
MemoryStore is the default StoreAdapter used when Glove is constructed without a store. It implements every optional surface — messages, tokens, turns, tasks, permissions, inbox, and createSubAgentStore — so subagents work out of the box.
import { MemoryStore } from "glove-core";
const store = new MemoryStore("my-session");
const childStore = await store.createSubAgentStore("researcher", false);
MemoryStore is process-local — it loses data on restart. For persistence, implement StoreAdapter against your own backend. glove-sqlite is deprecated; new projects should prefer MemoryStore for prototyping or BYO StoreAdapter for production.
Key Differences from React
React (glove-react) | Server-side (glove-core) |
|---|
defineTool with render/renderResult | .fold() with just do — no renderers needed |
useGlove() hook manages state | Call agent.processRequest() directly |
GloveClient + GloveProvider | new Glove({...}).build() |
createEndpointModel (SSE client) | createAdapter() or direct adapter (e.g. new AnthropicAdapter()) |
MemoryStore from glove-react | MemoryStore from glove-core (default) — or implement StoreAdapter for persistence |
Tools Without Display
Most server-side tools ignore the display manager — just return a result:
gloveBuilder.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/${input.city}?format=j1`);
return { status: "success", data: await res.json() };
},
});
Returning a plain string also works — auto-wrapped to { status: "success", data: yourString }.
Interactive Tools (pushAndWait)
When a tool calls display.pushAndWait(), the agent loop blocks until dm.resolve(slotId, value) is called. Wire this to your UI layer (WebSocket, terminal, Slack, etc.):
async do(input, display) {
const confirmed = await display.pushAndWait({
renderer: "confirm",
input: { message: `Delete ${input.file}?` },
});
if (!confirmed) return { status: "error", data: null, message: "Cancelled" };
}
dm.resolve(slotId, true);
Subscribers (Logging, Forwarding)
import type { SubscriberAdapter } from "glove-core";
const logger: SubscriberAdapter = {
async record(event_type, data) {
if (event_type === "text_delta") process.stdout.write((data as any).text);
if (event_type === "tool_use") console.log(`\n[tool] ${(data as any).name}`);
},
};
gloveBuilder.addSubscriber(logger);
Common Patterns
- CLI script: Build agent, call
processRequest(), print result
- Multi-turn REPL: Loop with readline, each
processRequest() accumulates in the store
- WebSocket server: Per-connection session with isolated store/dm/subscriber, forward events via
record()
- Background worker: Build agent per job, process from a queue, no display needed
- Hot-swap model: Call
agent.setModel(newAdapter) at runtime
- MCP-backed agent: Set
serverMode: true, call mountMcp(glove, { adapter, entries }) before build(). See MCP Integration.
Optional Store Features
- Tasks (
getTasks, addTasks, updateTask): Auto-registers glove_update_tasks tool
- Permissions (
getPermission(name, input?), setPermission(name, status, input?)): Tools with requiresPermission: true (or a (input) => boolean gate) check consent. The Executor passes the model-supplied input on every gated call so the store can scope decisions per-input. The default MemoryStore uses exact-match keying via the exported permissionKey(name, input) helper.
- Inbox (
getInboxItems, addInboxItem, updateInboxItem, getResolvedInboxItems): Auto-registers glove_post_to_inbox tool. Enables async cross-instance communication.
If your store doesn't implement these, they're silently disabled.
Inbox (Async Mailbox)
The inbox enables agents to post requests that will be resolved later by external services — surviving across sessions and instances.
How It Works
- Agent calls
glove_post_to_inbox with a tag, request text, and blocking flag
- Item persists in the store with status
pending
- External service resolves the item (via
SqliteStore.resolveInboxItem() from glove-sqlite, or store API)
- Next time
agent.ask() runs, resolved items are injected as text messages and marked consumed
- Pending blocking items are surfaced as transient reminders (not persisted)
- Compaction preserves pending inbox items in the summary block
Built-in Tool: glove_post_to_inbox
Auto-registered when store implements inbox methods. Input schema:
{
tag: string,
request: string,
blocking: boolean,
}
External Resolution
import { SqliteStore } from "glove-sqlite";
SqliteStore.resolveInboxItem(
"path/to/db.db",
"inbox_item_id",
"The item you requested is now available."
);
Or via REST if you've set up inbox API routes (see coffee example).
InboxItem Type
interface InboxItem {
id: string;
tag: string;
request: string;
response: string | null;
status: "pending" | "resolved" | "consumed";
blocking: boolean;
created_at: string;
resolved_at: string | null;
}
Store Methods (Optional)
getInboxItems?(): Promise<InboxItem[]>
addInboxItem?(item: InboxItem): Promise<void>
updateInboxItem?(itemId: string, updates: Partial<Pick<InboxItem, "status" | "response" | "resolved_at">>): Promise<void>
getResolvedInboxItems?(): Promise<InboxItem[]>
All store implementations (SqliteStore from glove-sqlite, MemoryStore, createRemoteStore) support inbox.
React Integration
useGlove() returns inbox: InboxItem[] alongside tasks:
const { inbox, tasks, timeline, sendMessage } = useGlove({ tools, sessionId });
{inbox.filter(i => i.status === "pending").map(item => (
<div key={item.id}>{item.tag}: {item.request}</div>
))}
Remote Store Actions
When using createRemoteStore, add inbox actions to persist to your backend:
const storeActions: RemoteStoreActions = {
getInboxItems: (sid) => fetch(`/api/sessions/${sid}/inbox`).then(r => r.json()),
addInboxItem: (sid, item) => fetch(`/api/sessions/${sid}/inbox`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ item }) }),
updateInboxItem: (sid, itemId, updates) => fetch(`/api/sessions/${sid}/inbox/update`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ itemId, updates }) }),
getResolvedInboxItems: (sid) => fetch(`/api/sessions/${sid}/inbox/resolved`).then(r => r.json()),
};
Extensions: Hooks, Skills & Subagents
processRequest parses two kinds of inline directive out of the user text and dispatches them before the model is called. Subagents are a third extension surface, but they are NOT parsed from user text — they are routed by the model through a dispatch tool. Builders register handlers via three builder methods (chainable, callable post-build() like fold):
| Token / mechanism | Purpose | Builder method |
|---|
/hookname | Mutate agent state, force compaction, swap model, short-circuit a turn | defineHook(name, handler) |
/skillname | Inject context as a synthetic user message marked is_skill_injection: true | defineSkill({ name, handler, description?, exposeToAgent? }) |
glove_invoke_subagent({ name, prompt }) (model-side tool) | Register a child Glove the main agent can route a self-contained task to. The user's @name text reaches the model verbatim — it's a routing signal, not a parsed directive. Mirrors Claude Code's subagent convention. | defineSubAgent({ name, factory, description? }) |
/ tokens only bind when the name matches a registered hook or skill — /usr/local/bin survives untouched. Bound / tokens are replaced, not stripped, with a non-triggerable placeholder of the form [invoked_extension__hook_<name>] or [invoked_extension__skill_<name>]. The placeholder doesn't re-bind on a future parse, but it keeps the persisted user message structurally honest — the model can see that an extension fired. @ tokens are never parsed by glove at all, so emails like a@b.com reach the model unchanged.
Quick example
import { Glove, MemoryStore } from "glove-core";
const agent = new Glove({ })
.defineHook("compact", async ({ controls }) => {
await controls.forceCompaction();
})
.defineHook("stop", async () => ({
shortCircuit: { message: { sender: "agent", text: "Cancelled." } },
}))
.defineSkill({
name: "concise",
description: "Tighter, snappier responses",
exposeToAgent: true,
handler: async ({ source, args }) =>
`Be terse. (source=${source}, hint=${args ?? "none"})`,
})
.defineSubAgent({
name: "weather",
description: "Run the weather subagent. Use for weather questions.",
factory: async ({ parentStore, parentControls, prompt }) => {
const subStore = await parentStore.createSubAgentStore?.("weather", false)
?? new MemoryStore(`weather_${Date.now()}`);
return new Glove({
store: subStore,
model: parentControls.glove.model,
displayManager: parentControls.displayManager,
systemPrompt: "You are a weather assistant. Answer the prompt and return.",
compaction_config: { compaction_instructions: "Summarise weather lookups." },
})
.fold(weatherTool)
.build();
},
})
.build();
await agent.processRequest("/concise tell me about Rust");
await agent.processRequest("/compact what's next?");
await agent.processRequest("@weather NYC");
Hooks
type HookHandler = (ctx: HookContext) => Promise<HookResult | void>;
interface HookContext {
name: string;
rawText: string;
parsedText: string;
controls: AgentControls;
signal?: AbortSignal;
}
interface HookResult {
rewriteText?: string;
shortCircuit?:
| { message: Message }
| { result: ModelPromptResult };
}
interface AgentControls {
context: Context;
observer: Observer;
promptMachine: PromptMachine;
executor: Executor;
glove: IGloveRunnable;
store: StoreAdapter;
displayManager: DisplayManagerAdapter;
forceCompaction: () => Promise<void>;
}
forceCompaction calls Observer.runCompactionNow() — same body as tryCompaction minus the token-threshold guard. Subscribers still see compaction_start / compaction_end.
Hooks run in document order. rewriteText overrides the working text passed to subsequent hooks, skills, and the final user message. shortCircuit persists the user message and returns immediately — the model is not called. Glove emits hook_invoked ({ name }) on subscribers just before each hook handler runs.
Skills
type SkillHandler = (ctx: SkillContext) => Promise<string | ContentPart[]>;
interface SkillContext {
name: string;
parsedText: string;
args?: string;
source: "user" | "agent";
controls: AgentControls;
}
interface SkillOptions {
description?: string;
exposeToAgent?: boolean;
}
interface DefineSkillArgs extends SkillOptions {
name: string;
handler: SkillHandler;
}
interface RegisteredSkill {
handler: SkillHandler;
description?: string;
exposeToAgent: boolean;
}
Skill-injected messages set is_skill_injection: true on Message, alongside the existing is_compaction and is_compaction_request flags. Use it in transcript renderers to render injected context differently from real user turns. Glove emits skill_invoked ({ name, source: "user" | "agent", args? }) on subscribers — for user-side directives, the dispatch fires from Glove.processRequest; for agent-side calls, it fires from inside the glove_invoke_skill tool.
Exposing skills to the agent
Set exposeToAgent: true and Glove auto-registers a single glove_invoke_skill tool on the executor. Its description lists every exposed skill (- name — description) and is rebuilt in place each time a new exposed skill is defined, so post-build() registrations are picked up immediately.
agent.defineSkill({
name: "research-mode",
description: "Switch to long-form research mode with citations",
exposeToAgent: true,
handler: async ({ source, args, parsedText }) => {
if (source === "agent") {
return `Switch into research mode. Focus: ${args ?? "general"}.`;
}
return `Switch into research mode. User said: ${parsedText}`;
},
});
The tool returns { status: "success", data: { skill, content } } on success and { status: "error", message: 'Skill "..." is not available', data: null } for unknown or unexposed names. When the skill returns ContentPart[], text parts are joined into data.content (visible to the model) and the full part list is preserved on renderData (visible to client renderers, mirroring the MCP-bridge convention).
| Aspect | User /skill | Agent glove_invoke_skill |
|---|
| Where it lands | Synthetic user message before the real turn (is_skill_injection: true) | Tool result on the agent's tool_use |
SkillContext.source | "user" | "agent" |
SkillContext.args | undefined | free-form string the model supplied |
Gated by exposeToAgent | No — user-invoked always works | Yes — only exposed skills are callable |
Subagents
Modelled on Claude Code's subagent convention. Defining one auto-registers a glove_invoke_subagent tool (constant SUBAGENT_DISPATCH_TOOL_NAME exported from core) the main agent calls with { name, prompt }. The user's @name text in the original message is not parsed by glove — it reaches the model verbatim and acts as a routing signal. The factory builds a fresh child Glove for each invocation and the dispatcher runs it.
interface SubAgentFactoryContext {
name: string;
prompt: string;
parentStore: StoreAdapter;
parentControls: AgentControls;
}
type SubAgentFactory = (
ctx: SubAgentFactoryContext,
) => Promise<IGloveRunnable> | IGloveRunnable;
interface SubAgentOptions {
description?: string;
}
interface DefineSubAgentArgs extends SubAgentOptions {
name: string;
factory: SubAgentFactory;
}
interface RegisteredSubAgent {
factory: SubAgentFactory;
description?: string;
}
Canonical factory
import { Glove, MemoryStore } from "glove-core";
glove.defineSubAgent({
name: "researcher",
description: "Deep research subagent",
factory: async ({ parentStore, parentControls }) => {
const subStore = await parentStore.createSubAgentStore?.("researcher", false)
?? new MemoryStore(`researcher_${Date.now()}`);
return new Glove({
store: subStore,
model: parentControls.glove.model,
displayManager: parentControls.displayManager,
systemPrompt: "You are a researcher.",
compaction_config: { compaction_instructions: "Summarize research progress." },
})
.fold(searchTool)
.fold(fetchTool)
.build();
},
});
The dispatcher attaches the parent's subscribers to the child for the run, calls child.processRequest(prompt, signal) (forwarding the parent's abort signal), then detaches them. The child's final agent text is returned as the tool result.
Tool result shape
Symmetric with glove_invoke_skill:
{ name: string, prompt: string }
{ status: "success", data: { subagent: string, content: string } }
{ status: "error", message: 'Subagent "..." is not registered. Use one of: ...', data: null }
{ status: "error", message: 'Subagent "..." factory threw: ...', data: null }
{ status: "error", message: 'Subagent "..." failed: ...', data: null }
Bracket events — guaranteed 1:1 symmetry
The Executor (not the dispatcher) brackets every glove_invoke_subagent call with subagent_invoked ({ name, prompt }) before the run and subagent_completed ({ name, status: "success" | "error", message? }) after. The bracket is symmetric even when a parent abort short-circuits the dispatcher's promise chain: the executor's abort handler still fires the close bracket. Events emitted by the child Glove between them belong to that subagent — parent subscribers are attached to the child for the duration of the run.
Common patterns
- Fresh child per call: factory builds a new
Glove each invocation, with its own MemoryStore (or a non-durable sub-store). Default and recommended.
- Durable child:
parentStore.createSubAgentStore("name", true) returns the same store for the namespace, so the subagent carries message history across invocations. The factory still builds a fresh Glove; only the store is reused.
- Multiple in one message (
"@reviewer @architect please discuss this design"): both names reach the model, which can call glove_invoke_subagent once per subagent (sequentially or in parallel via separate tool calls — its choice).
Sub-stores: createSubAgentStore
StoreAdapter exposes an optional createSubAgentStore(namespace: string, durable?: boolean): Promise<StoreAdapter> factory:
durable: false (default) → a fresh, isolated store per call. The subagent starts from zero context every invocation.
durable: true → the same instance is returned for the same namespace, so the subagent retains messages, tasks, tokens, and counters across invocations within the parent's lifetime.
MemoryStore from glove-core implements both modes. If your store doesn't implement createSubAgentStore, fall back to new MemoryStore(...) inside the factory — that preserves the "fresh per call" behavior.
setDisplayManager (chainable)
Glove (both builder and runnable forms) exposes setDisplayManager(displayManager). Subagent factories typically pass parentControls.displayManager into the child's constructor, but they can also opt in mid-flight by calling child.setDisplayManager(parentControls.displayManager) after build. Returns this for chaining.
Dispatch order in processRequest
- Parse
/ directives from the raw text (regex (^|\s)\/([A-Za-z][\w-]*)(?=\s|$)). Bound tokens are replaced with [invoked_extension__<type>_<name>] placeholders; unbound tokens stay in place. @ tokens are not parsed at all.
- Run hooks in document order. Glove emits
hook_invoked per hook. Apply any rewriteText; honour the first shortCircuit and return.
- Materialise skills (
source: "user") — each becomes a synthetic user message persisted via context.appendMessages before the real one. Glove emits skill_invoked per skill.
- Build the real user
Message from the placeholder-substituted text (still contains any @mentions untouched) + any non-text ContentParts the caller passed.
- Hand off to
Agent.ask. Subagent invocations happen inside the agent loop via glove_invoke_subagent tool calls; the executor brackets each one with subagent_invoked / subagent_completed.
is_skill_injection flag
Skill-materialised user messages set is_skill_injection: true on Message. Pair it with is_compaction for transcript rendering — collapse, mute, or filter injected messages so they're visually distinct from real user turns.
pre_modified_text on Message
When a hook rewrites the user message via rewriteText, the original raw text is preserved on Message.pre_modified_text so frontends can render what the user actually typed alongside the rewritten version the model received.
Public API surface
import {
Glove,
HookHandler, HookContext, HookResult,
SkillHandler, SkillContext, SkillOptions, DefineSkillArgs, RegisteredSkill,
SubAgentFactory, SubAgentFactoryContext, SubAgentOptions, DefineSubAgentArgs, RegisteredSubAgent,
AgentControls,
SUBAGENT_DISPATCH_TOOL_NAME,
parseTokens, formatSkillMessage,
createSkillInvokeTool, renderSkillToolDescription,
createSubAgentInvokeTool, renderSubAgentToolDescription,
} from "glove-core";
Available at the main entry and the glove-core/extensions subpath.
MCP Integration (glove-mcp)
glove-mcp bridges Model Context Protocol servers (Notion, Gmail, Linear, Slack, an internal MCP wrapper around your own APIs, …) into a Glove agent so their tools appear in the model's tool list as ordinary Glove tools. Streamable HTTP transport only in v1.
When to use it
- You need third-party capabilities a vendor already exposes via MCP — Notion, Gmail, Linear, Slack, Zapier-MCP, etc.
- You have multiple internal services and want a single integration shape across them.
- You want the agent to discover and activate capabilities mid-conversation rather than wiring all tools at startup.
If you control both ends and just need a few first-party tools, hand-rolled glove.fold(...) is still simpler. MCP earns its keep when the catalogue is large or the servers are not yours.
Mental model: catalogue + adapter
Two pieces, deliberately split:
-
McpCatalogueEntry[] — a static list authored at the application level. One entry per MCP server the app supports: id, name, description, url, tags?, metadata?. Identical across users. The id doubles as the tool namespace prefix and the activation key.
-
McpAdapter — a per-conversation interface the consumer implements (analogous to StoreAdapter). Holds the conversation's active server ids and resolves access tokens.
interface McpAdapter {
identifier: string;
getActive(): Promise<string[]>;
activate(id: string): Promise<void>;
deactivate(id: string): Promise<void>;
getAccessToken(id: string): Promise<string>;
}
getAccessToken is the only auth seam. The framework wraps the returned string in Authorization: Bearer .... Token acquisition, refresh, and persistence are entirely the consumer's responsibility — env vars, vault, your own OAuth flow, the opt-in runMcpOAuth helper, all valid.
mountMcp — the canonical entry point
After new Glove(...) and before glove.build():
import { mountMcp } from "glove-mcp";
const glove = new Glove({ , serverMode: true });
await mountMcp(glove, {
adapter,
entries,
ambiguityPolicy: { type: "auto-pick-best" },
subagentModel: undefined,
subagentSystemPrompt: undefined,
clientInfo: { name: "My App", version: "1.0.0" },
});
glove.build();
What it does, in order:
- Reads
adapter.getActive(), opens an MCP connection per active id (using getAccessToken), lists tools, and folds each one onto the main agent via bridgeMcpTool. Per-server reload failures are logged and skipped — a transient outage doesn't kill the agent.
- Registers the
discovermcp discovery subagent via glove.defineSubAgent(discoverySubAgent({...})) so the model can ask it to activate more servers mid-conversation. The model invokes it via glove_invoke_subagent({ name: "discovermcp", prompt: "..." }).
mountMcp returns when reload + subagent registration are complete. Call it before build() for the cleanest init order, but fold() / defineSubAgent() after build() work too.
Bridged tool shape
bridgeMcpTool(connection, tool, serverMode) produces a GloveFoldArgs with these conventions:
- Name:
${entry.id}__${tool.name} (e.g. notion__search). The __ separator (exported as MCP_NAMESPACE_SEP) is regex-safe across all model providers.
- Schema: raw JSON Schema from the MCP server, passed via
jsonSchema (no Zod). The MCP server is the source of truth.
requiresPermission: in serverMode always false; otherwise true unless the MCP tool annotates readOnlyHint: true.
- Result: server
content[] text is joined into data (what the model sees); the full content[] is also passed through as renderData so React renderers can use it.
- Auth-expired contract: any 401-shaped error during
callTool is mapped to { status: "error", message: "auth_expired", data: null }. Detect this from the conversation log, refresh your token, and the next call picks up the new value via getAccessToken.
Discovery (discovermcp) and ambiguity policies
mountMcp registers a single subagent the model can route to: discovermcp. The model invokes it via glove_invoke_subagent({ name: "discovermcp", prompt: "send an email" }). The factory builds a child Glove (with its own sub-store from parentStore.createSubAgentStore("discovermcp", false), falling back to a private DiscoveryMemoryStore when sub-stores aren't supported, inheriting the main agent's model, displayManager, and serverMode), and folds these tools onto the child:
list_capabilities(query?, tags?) — substring search the catalogue.
activate(id) — connect, bridge tools onto the main agent, persist active state. Tools become available to the main model on its next turn.
deactivate(id) — flip persisted state. v1 limitation: tools stay loaded until session refresh.
ask_user(question, options) — only registered under the interactive policy. Renders via the mcp_picker renderer on the main displayManager.
The ambiguity policy controls what happens when the subagent finds multiple plausible matches:
| Policy | Behavior | When to use |
|---|
{ type: "interactive" } | Subagent calls ask_user via pushAndWait. Requires an mcp_picker renderer on your displayManager. | Browser UIs / chat apps with a renderer wired up. Default when serverMode: false. |
{ type: "auto-pick-best" } | Subagent always picks the highest-ranked match. No human in the loop. | Headless / server-side / CLI. Default when serverMode: true. |
{ type: "defer-to-main" } | Subagent returns the candidate list as text and lets the main agent decide what to activate. | Multi-MCP discovery flows where the main model has more conversation context than the subagent. |
serverMode: true on the Glove config is the canonical "I am headless" flag — drives both the default ambiguity policy and the default requiresPermission on bridged tools (never gate).
Auth model — bearer-only
The framework only knows about static bearer tokens. connectMcp ships an auth: bearer(token | () => token) helper; pass either a string or a thunk that resolves a fresh token per connection. mountMcp and the discovery activate tool both use the thunk form so every connection re-reads getAccessToken.
import { bearer, connectMcp } from "glove-mcp";
const conn = await connectMcp({
namespace: "notion",
url: "https://mcp.notion.com/mcp",
auth: bearer(() => adapter.getAccessToken("notion")),
clientInfo: { name: "My App", version: "1.0.0" },
});
auth_expired contract
Mid-call, an expired token surfaces as { status: "error", message: "auth_expired" } on the bridged tool result. The framework does not refresh tokens. Your app must:
- Detect
auth_expired on the conversation log (subscriber tool_use_result event, or post-hoc).
- Refresh / re-auth via whatever mechanism owns the credential.
- Update your store; the next bridged call pulls a fresh token from
getAccessToken.
For UI consumers this is usually a "Reconnect Notion" toast. For CLIs, instructing the user to re-run the auth command is normal.
glove-mcp/oauth — opt-in OAuth tooling
If you don't already have an OAuth flow, the glove-mcp/oauth subpath ships a small reference implementation built on the MCP authorization spec:
runMcpOAuth(opts) — one call, end-to-end flow. Spins up a local listener on http://localhost:53683/callback (configurable), drives the SDK through DCR (or skips it via preRegisteredClient), opens the user's browser, exchanges the code for tokens, and verifies via listTools (or a callTool of your choice). Used by the examples/mcp-cli/*-mcp-auth.ts scripts.
FsOAuthStore / MemoryOAuthStore — OAuthStore implementations. FsOAuthStore writes a single JSON file with mode 0600 and atomic temp+rename. Replace with your DB for production.
McpOAuthProvider — lower-level OAuthClientProvider for advanced consumers driving auth() from the SDK directly.
buildClientMetadata, MCP_DEFAULT_CLIENT_INFO, emptyOAuthState — small helpers.
Consumers who already have tokens (env vars, internal integrations, vault, an existing OAuth setup) can ignore this subpath entirely — getAccessToken returns the bearer, full stop.
See api-reference.md — glove-mcp/oauth for full type signatures, and examples.md — Pattern: MCP OAuth flow for a worked example.
Production lift-and-shift
The reference examples/mcp-cli setup is a single-user Node CLI; production typically wants:
- Multi-user store — replace
FsOAuthStore with a per-user OAuthStore backed by your DB. The interface is three methods (get, set, delete).
- OAuth flow in route handlers —
GET /oauth/<id>/start calls runMcpOAuth (or the lower-level SDK auth() directly), GET /oauth/<id>/callback finishes it. Same machinery, different invocation. The local-listener flavour of runMcpOAuth is convenient for CLIs but not what you want behind a load balancer.
- Background refresh — refresh expired tokens however your stack does it;
getAccessToken just reads the latest bearer string.
- Persistent active state — the
McpAdapter shown in examples uses an in-memory Set for active ids. In production, persist active ids per conversation (alongside messages) so reload after restart actually does something.
The agent code itself doesn't change — McpAdapter.getAccessToken is the only seam.
Quick reference — where things live
| Need | Symbol |
|---|
| Mount MCP onto an agent | mountMcp(glove, { adapter, entries, ... }) |
| Implement consumer adapter | McpAdapter interface |
| Author catalogue entries | McpCatalogueEntry |
| One-off connect (preflight, custom flow) | connectMcp({ namespace, url, auth }) |
| Bridge a tool by hand | bridgeMcpTool(connection, tool, serverMode) |
| Bearer header helper | `bearer(token |
| Discovery subagent factory | discoverySubAgent({ adapter, entries, ambiguityPolicy }) (returns DefineSubAgentArgs; pass to glove.defineSubAgent(...)) |
| Tool namespace separator | MCP_NAMESPACE_SEP ("__") |
| 401 detection on raw connect | UnauthorizedError |
| Run the OAuth flow | runMcpOAuth(opts) from glove-mcp/oauth |
| Persist OAuth state | FsOAuthStore, MemoryOAuthStore from glove-mcp/oauth |
| Build client metadata | buildClientMetadata(opts) from glove-mcp/oauth |
Memory (glove-memory)
Schema-first memory layer with four sibling subsystems. Storage-agnostic adapter contracts; reference in-memory adapters ship for dev/test. Status: draft v0.1; companion storage backends (glove-memory-sqlite, glove-memory-postgres) are not yet released.
The four subsystems
| Subsystem | Adapter | What it's for |
|---|
| Entity | EntityMemoryAdapter | Graph-shaped, schema-first, deterministic identity resolution. Nodes (people, organizations, projects) and typed edges between them. Curator-written, agent-read. |
| Episodic | EpisodicMemoryAdapter | Timeline-bound, append-only events. Meetings, decisions, observations. Time is a first-class field; semantic search is opt-in (advertised by supportsSemanticSearch). |
| Resources | ResourceFsAdapter | POSIX-style virtual filesystem the agent navigates with ls / read / grep / glob / edit. Holds research notes, transcripts, link collections. Text-only; absolute paths only (no . / ..). |
| Context | ContextAdapter | User-configured ambient context, auto-injected into the system prompt every turn. Different shape: not curator-extracted, no reader/curator split — one registration gives the agent both read and write tools. |
Architectural recommendation: don't dump memory tools on the main Glove
If you're building an agent that needs memory access, do not attach the entity / episodic / resources tools directly to your main Glove. Build subagents — one per retrieval task — and register them on the main agent. Each subagent attaches only the adapter slice it needs.
Why:
- Bounded prompt surface. Tool descriptions render the schema slice for that role only — token cost scales with role, not with total ontology size.
- Sharper routing.
lookup / recall / find-notes subagent names are themselves a reasoning surface. Tighter signal than "you have eight memory tools, decide which".
- Mutation scope is structural. A subagent attached with
useMemoryReader cannot write; the affordance isn't there.
- Adapters stay shared. All subagents read and write to the same underlying graph / timeline / filesystem. Splitting tools does not split the data.
Same advice on the curator side: a parent curator that routes to specialised write-side subagents (entity-linker, episode-recorder, resource-filer) beats a single curator with every write tool attached.
The exception is useContext. Context is small (4 tools), user-driven ("remember that…"), and ships with the system-prompt-injection wrapper that has to live on the agent the user actually talks to. Keep useContext on the main agent.
See examples.md — Memory: subagent-delegated reader / curator composition / context flow for worked-out patterns.
Helper families
Each helper folds the relevant tool surface onto a Glove. All return the same G for chaining; all operate on either an IGloveBuilder or an IGloveRunnable (anything that exposes fold).
| Helper | Folds | Notes |
|---|
useMemoryReader(glove, adapter) | glove_memory_find, _get, _query | Read-only entity graph access. |
useMemoryCurator(glove, adapter) | reader tools + _add_node, _update_node, _connect, _disconnect, _merge_nodes | Full entity write access. |
useEpisodicReader(glove, adapter) | glove_episodic_find, _timeline, _search | _search only registered when adapter.supportsSemanticSearch === true. |
useEpisodicCurator(glove, adapter) | reader tools + _record, _update, _delete | |
useResourcesReader(glove, adapter) | glove_resources_ls, _read, _stat, _grep, _glob, _search, _links_for | _search only when supportsSemanticSearch. |
useResourcesCurator(glove, adapter) | reader tools + _write, _edit, _mkdir, _move, _remove, _set_metadata | |
useContext(glove, adapter) | glove_context_get, _set, _update, _unset | Also wraps processRequest to call adapter.render() and prepend the rendered markdown block to the system prompt every turn. |
Tool inventory
Entity (useMemoryReader / useMemoryCurator)
| Tool | Purpose |
|---|
glove_memory_find | Find nodes by class + filter, optional fuzzy |
glove_memory_get | Fetch a node by id + one-hop neighbourhood |
glove_memory_query | Full structured query via the query DSL |
glove_memory_add_node | Create or upsert a node by identity keys (curator) |
glove_memory_update_node | Patch a node's properties (curator) |
glove_memory_connect | Create or update an edge (curator) |
glove_memory_disconnect | Remove an edge (curator) |
glove_memory_merge_nodes | Fold one node into another (curator) |
Episodic (useEpisodicReader / useEpisodicCurator)
| Tool | Purpose |
|---|
glove_episodic_search | Semantic search over episode content (only when supportsSemanticSearch) |
glove_episodic_find | Structured filter — by kind, participant, time range, properties |
glove_episodic_timeline | Chronological listing for an entity or time window |
glove_episodic_record | Append a new episode (curator) |
glove_episodic_update | Patch an existing episode (curator) |
glove_episodic_delete | Remove an episode (curator) |
Resources (useResourcesReader / useResourcesCurator)
| Tool | Purpose |
|---|
glove_resources_ls | List directory contents |
glove_resources_read | Read a file body, with optional line range |
glove_resources_stat | Get metadata about a single path |
glove_resources_grep | Text/regex search across the tree |
glove_resources_glob | Find paths by name pattern |
glove_resources_search | Semantic search (only when supportsSemanticSearch) |
glove_resources_links_for | Reverse-lookup: find resources linking to a target |
glove_resources_write | Create or overwrite a file (curator) |
glove_resources_edit | Replace a unique substring (curator) |
glove_resources_mkdir | Create an empty directory (curator) |
glove_resources_move | Rename or relocate (curator) |
glove_resources_remove | Delete a file or directory (curator) |
glove_resources_set_metadata | Patch metadata without rewriting body (curator) |
Context (useContext)
| Tool | Purpose |
|---|
glove_context_get | Read entries by section or list all |
glove_context_set | Add a new entry |
glove_context_update | Patch an existing entry in place |
glove_context_unset | Remove an entry or wipe an entire section |
MemorySchema — the shared ontology
One schema object is passed to every adapter. Lives in code; the package does not persist it, validate it across deployments, or expose migration primitives — that's the consumer's concern.
import { MemorySchema } from "glove-memory/core";
import { z } from "zod";
const schema = new MemorySchema()
.defineNodeClass({
name: "Person",
schema: z.object({ name: z.string(), email: z.string().optional() }),
identityKeys: [["email"], ["name"]],
searchableProperties: ["name", "email"],
})
.defineRelationship({
type: "worksAt",
from: "Person",
to: "Organization",
propertiesSchema: z.object({ since: z.string().optional() }).optional(),
multi: false,
})
.defineEpisodeKind({
name: "meeting",
description: "A scheduled gathering.",
propertiesSchema: z.object({ duration_min: z.number() }).optional(),
})
.defineResourceRoot({
path: "/research",
description: "External research artifacts.",
semanticSearch: true,
});
What's safe at runtime:
- Adding a new node class, relationship, or episode kind is always safe.
- Adding an optional property is always safe.
- Adding a required property won't break reads; new writes that don't supply it fail validation.
- Removing or renaming properties needs a consumer-managed rewrite — the adapter won't notice.
- Changing identity keys may silently collapse or split nodes on subsequent writes.
Provenance — required, append-only, every write
Every adapter write takes a Provenance. It's append-only per node, edge, episode, resource, and context entry. Reader-facing tools filter provenance out of results; only direct adapter calls return it.
interface Provenance {
source: string;
actor: string;
timestamp: string;
note?: string;
}
Link is the shared cross-reference vocabulary — episodes pointing at people, resources pointing at episodes, context entries pointing at projects.
interface Link {
kind: "entity" | "episode" | "resource";
id: string;
relation?: string;
}
The package does not validate that link targets exist — adapters stay decoupled. Cross-validation is the curator / orchestrator's job.
Embedding lifecycle — out-of-band, BYO adapter
Episodic and resources use the same lifecycle. Writes mark records embeddingStatus: "missing" (initial) or "stale" (content change) and return immediately. A separate process — typically a Station signal — does the embed pass:
interface EmbeddingAdapter {
dimensions: number;
embed(texts: string[]): Promise<number[][]>;
}
The refresh loop:
const pending = await episodic.findEpisodesNeedingEmbedding({ limit: 50 });
const vectors = await embedder.embed(pending.map((p) => p.content));
for (let i = 0; i < pending.length; i++) {
await episodic.setEmbedding(pending[i].id, vectors[i]);
}
Resources use findFilesNeedingEmbedding / setEmbedding (both optional on ResourceFsAdapter, present only when supportsSemanticSearch === true). Stale marking on episodes is content-only in the in-memory adapter — kind / participants / properties / occurredAt patches don't re-embed; consumers wanting different behaviour can delete + re-record. The recency blend in searchEpisodes defaults to recencyWeight = 0.2 with a 30-day half-life.
Reconciliation primitives
The package's contract is deliberately narrow: store, query, write, search. It does not cascade across adapters. When an entity is merged or deleted, episodes that reference its old ID don't auto-update. Orchestrators reach for these primitives:
| Action | Primitive |
|---|
| Entity merged | episodic.replaceParticipantId(oldId, newId, prov), resources.replaceLinkTarget("entity", oldId, newId, prov) |
| Entity deleted | episodic.findEpisodes({ where: { participantIds: [id] } }), resources.linksFor("entity", id) then orchestrator decides |
| Resource moved | resources.replaceLinkTarget("resource", fromPath, toPath, prov) |
| Episode deleted | resources.linksFor("episode", id) then orchestrator decides |
| Stale embeddings | findEpisodesNeedingEmbedding / findFilesNeedingEmbedding → embed → setEmbedding |
Reference in-memory adapters
For dev / tests / quick prototypes. All exported from glove-memory/in-memory (and re-exported from the barrel).
import {
InMemoryEntityAdapter,
InMemoryEpisodicAdapter,
InMemoryResourcesAdapter,
InMemoryContextAdapter,
} from "glove-memory";
const entity = new InMemoryEntityAdapter({ schema });
const episodic = new InMemoryEpisodicAdapter({ schema, embedder });
const resources = new InMemoryResourcesAdapter({ schema, embedder });
const context = new InMemoryContextAdapter({ schema });
Process-local — they lose data on restart. Production projects swap in a companion package or BYO adapter.
Out of scope
- Triggering, scheduling, or pipeline orchestration (Station's territory).
- Curation logic itself (configured by the consumer).
- Embedding generation — consumers plug in their own
EmbeddingAdapter.
- Schema persistence or migration — schema lives in code only.
- Cross-adapter cascade on entity merge, episode delete, or resource rename — that's reconciliation, an orchestrator responsibility.
- The user-side write path for context — the adapter exposes
set / update / unset; the UI / API / form / wherever users edit their preferences calls those directly.
- Binary resources. Resources is text-only.
. and .. path resolution. All resource paths are absolute.
Quick reference — where things live
| Need | Symbol |
|---|
| Define the ontology | MemorySchema from glove-memory/core |
| Required write metadata | Provenance from glove-memory/core |
| Cross-reference between subsystems | Link from glove-memory/core |
| Embedding contract | EmbeddingAdapter from glove-memory/core |
| Entity contract | EntityMemoryAdapter from glove-memory/entity |
| Episodic contract | EpisodicMemoryAdapter from glove-memory/episodic |
| Resources contract | ResourceFsAdapter from glove-memory/resources |
| Context contract | ContextAdapter from glove-memory/context |
| Reader / curator helpers | useMemoryReader / useMemoryCurator, useEpisodicReader / useEpisodicCurator, useResourcesReader / useResourcesCurator, useContext from glove-memory/tools |
| Reference in-process adapters | InMemoryEntityAdapter, InMemoryEpisodicAdapter, InMemoryResourcesAdapter, InMemoryContextAdapter from glove-memory/in-memory |
| Error classes | MemoryError, MemoryNotFoundError, MemorySchemaError, MemoryQueryError, MemoryWriteError, EpisodicMemoryError, ResourceFsError, ContextError from glove-memory/core |
See api-reference.md — glove-memory for full type signatures, and examples.md — Memory for worked examples (schema definition, subagent-delegated reader, curator composition, context flow).
Mesh Network (glove-mesh)
glove-mesh lets multiple Glove agents talk to each other — direct messages, broadcasts, acknowledgements — on top of the existing glove-inbox primitive. The package is behaviorally additive to glove-core except for one small runtime API addition: IGloveRunnable.store (a readonly accessor that mountMesh reads to write resolved inbox items directly). No agent-loop semantics change. The package itself ships no authentication; the consumer's MeshAdapter owns transport and any signing/verification.
When to use it
- Two or more Glove agents running async or in parallel in the same process (or across processes/hosts) need to coordinate.
- You want the agent loop to surface peer messages as plain context the model can read, without writing custom subscriber/store integration.
If you just need one agent to delegate work to another sub-task in isolation, use defineSubAgent instead — subagents run nested under the parent. Mesh is for peers.
Mental model
Each agent owns its own StoreAdapter+inbox. The MeshAdapter is a per-agent view of the network (matches McpAdapter's per-conversation pattern). When agent A calls glove_mesh_send_message({ to: "b", content: ... }), the framework drops a status: "resolved" InboxItem with tag mesh:from:a into B's store. B's existing Agent.injectResolvedInboxItems path (glove-core) surfaces it as a synthetic user message on B's next ask() — exactly like an externally-resolved inbox item.
The "shared inbox" is conceptual: the mesh is shared; the inbox stays per-agent.
mountMesh — the canonical entry point
After new Glove(...).build():
import { mountMesh, MeshNetwork, InMemoryMeshAdapter } from "glove-mesh";
const network = new MeshNetwork();
await mountMesh(glove, {
adapter: new InMemoryMeshAdapter(network, "agent-a"),
identity: {
id: "agent-a",
name: "Agent A",
description: "Plans tasks for the team.",
capabilities: ["chat", "planning"],
},
});
What it does:
- Validates
glove.store implements the four inbox methods (getInboxItems, addInboxItem, updateInboxItem, getResolvedInboxItems) — throws MeshStoreUnsupportedError otherwise.
- Awaits
adapter.register(identity) so peers can discover this agent.
- Calls
adapter.subscribe(handler) once. The handler converts incoming messages into resolved inbox items (or resolves a pending blocking item on ack).
- Folds four tools onto the running Glove:
glove_mesh_send_message, glove_mesh_broadcast, glove_mesh_list_agents, glove_mesh_acknowledge.
Returns Promise<void> (not chainable). Mirrors mountMcp's async-setup convention.
The four tools
| Tool | Input | Behavior |
|---|
glove_mesh_send_message | { to, content, in_reply_to?, blocking? } | Calls adapter.send. Blocking inserts a pending blocking inbox item tagged mesh:waiting:<msg_id> that resolves on ack or reply. |
glove_mesh_broadcast | { content, blocking? } | Calls adapter.broadcast. Blocking resolves on the FIRST ack received. |
glove_mesh_list_agents | { filter?: { capability?, name_contains? } } | Calls adapter.listAgents; filters; excludes self. |
glove_mesh_acknowledge | { message_id, note? } | Calls adapter.acknowledge. Lightweight confirmation; for substantive replies use glove_mesh_send_message with in_reply_to instead. |
MeshAdapter contract
Implement one per agent. Consumer-supplied — the package never knows about your transport.
interface MeshAdapter {
identifier: string;
register(identity: AgentIdentity): Promise<void>;
unregister(): Promise<void>;
listAgents(): Promise<AgentIdentity[]>;
getAgent(id: string): Promise<AgentIdentity | null>;
send(message: MeshMessage): Promise<void>;
broadcast(message: Omit<MeshMessage, "to">): Promise<void>;
acknowledge(messageId: string, note?: string): Promise<void>;
subscribe(handler: (msg: IncomingMeshMessage) => Promise<void>): () => void;
}
Adapter guarantees the framework relies on:
send resolves when the transport has accepted the message, not when the recipient handles it.
broadcast excludes the sender from fan-out.
- Handler errors must NOT bubble — log and continue so fan-out to other agents stays intact.
acknowledge routes an IncomingMeshMessage with kind: "ack" back to the original sender of messageId.
Reference InMemoryMeshAdapter
For dev, tests, and single-host setups. MeshNetwork is the shared bus; construct once, hand the same instance to every InMemoryMeshAdapter in the process.
import { MeshNetwork, InMemoryMeshAdapter } from "glove-mesh";
const net = new MeshNetwork();
const a = new InMemoryMeshAdapter(net, "agent-a");
const b = new InMemoryMeshAdapter(net, "agent-b");
MeshNetwork keeps a bounded LRU (default 1024) of message_id → sender_id so acknowledge can route back without the model threading sender on every ack.
Blocking sends
| Tool call | Pending item? | Resolves on |
|---|
glove_mesh_send_message({ blocking: false }) | No | n/a |
glove_mesh_send_message({ blocking: true }) | Yes — tag mesh:waiting:<msg_id> | ack with ack_of === msg_id, OR a reply (glove_mesh_send_message with in_reply_to === msg_id) |
glove_mesh_broadcast({ blocking: true }) | Yes | first ack from any peer |
glove_mesh_acknowledge (this agent acking inbound) | No | n/a — itself |
A pending blocking item synthesises a transient reminder each turn via Agent.buildPendingBlockingMessage until it resolves. When the ack/reply arrives, the resolved item shows up via the standard [Inbox: N item(s) resolved] injection.
Reply implies ack. A direct incoming with in_reply_to: X does BOTH: inserts a new resolved inbox item with the reply body AND resolves the pending item for X. Saves the recipient one tool call.
Tag convention
Mesh items use namespaced tags so consumers can filter mesh traffic out of inbox histories:
| Tag prefix | Direction | Meaning |
|---|
mesh:from:<sender> | inbound | direct message |
mesh:broadcast:from:<sender> | inbound | broadcast |
mesh:waiting:<msg_id> | local | pending blocking item for an outbound send |
Auth model — there isn't one
MeshMessage.from is sender-claimed and unverified. If you need authenticated messaging, sign messages before calling send/broadcast and verify in your subscribe handler. Mirrors how McpAdapter.getAccessToken keeps auth a consumer concern.
Quick reference — where things live
| Need | Symbol |
|---|
| Mount mesh on an agent | mountMesh(glove, { adapter, identity }) from glove-mesh |
| Adapter contract | MeshAdapter from glove-mesh/core |
| Message types | AgentIdentity, MeshMessage, IncomingMeshMessage from glove-mesh/core |
| In-process bus | MeshNetwork, InMemoryMeshAdapter from glove-mesh/in-memory |
| Individual tool builders | buildMeshSendTool, buildMeshBroadcastTool, buildMeshListAgentsTool, buildMeshAcknowledgeTool from glove-mesh/tools |
| Error classes | MeshError, MeshNotRegisteredError, MeshUnknownAgentError, MeshUnknownMessageError, MeshStoreUnsupportedError from glove-mesh/core |
How it differs from glove_post_to_inbox
glove_post_to_inbox — "I will resolve this myself later from outside the conversation" (external service, webhook, cron).
glove_mesh_send_message — "I'm talking to another Glove agent on the mesh" (peer-to-peer).
Both write to the same StoreAdapter inbox surface; the tag prefix tells them apart.
Limitations (v1)
InMemoryMeshAdapter is process-local; restarts wipe state. Real transports are the consumer's job.
- Sender-table LRU caps at 1024 — acks for very old messages are best-effort.
- Broadcast blocking resolves on the FIRST ack, not all peers.
- No new
SubscriberEvent types; observability rides on tool_use_result for the four tools and inbox-state writes.
- No group/topic concept. Broadcast targets every registered agent.
Continuum (glove-continuum-signal)
Subprocess-based runtime substrate that supervises Glove agents like station-signal supervises background jobs. Two execution modes:
- Triggered (asynchronous) — agents are cold by default. An external force (
.trigger(input), a schedule fire, an inbound mesh message) wakes them. They resume their persistent store, run a turn, return, go cold. Each wakeup spawns a fresh subprocess.
- Concurrent (synchronous) — agents are warm in long-lived subprocesses. The runner keeps them alive and pushes notifications inline via
runner.notify(name, input); mid-loop pickup is immediate, no spawn latency.
The substrate is NOT an inter-agent protocol — that's glove-mesh. Continuum gives mesh a stable per-agent identity, an inbox-capable persistent store, and a long-lived subprocess for warm agents; mesh runs entirely inside that subprocess against whatever transport the consumer's MeshAdapter provides.
When to use
- Multiple long-running agents in one deployment, each with isolated subprocesses but observed centrally.
- Agents that keep state across many wakeups (continuity-of-context for triggered agents).
- Firing agent work from an HTTP handler / cron / webhook and picking it up async — like a background job, but the job is a full Glove agent.
- Mesh between agents on the same host without an external broker (pair with the example
FilesystemMeshAdapter).
For a single in-process agent in a Next.js handler, you don't need continuum — keep using createChatHandler.
The agent() builder — mode-as-fork
import { agent, z } from "glove-continuum-signal";
import { Glove, Displaymanager } from "glove-core";
import { createAdapter } from "glove-core/models/providers";
export const pizzaBaker = agent("pizza-baker")
.input(z.object({ orderId: z.string() }))
.output(z.object({ ready: z.boolean() }))
.triggered()
.timeout(60_000)
.retries(2)
.every("5m").withInput({ orderId: "tick" })
.env({ OVEN: "hot" })
.store((name) => new MyPersistentStore(`./agents/${name}.db`))
.onComplete(async (out, in_) => audit(out, in_))
.factory(async (ctx) =>
new Glove({
store: ctx.store ?? undefined,
model: createAdapter({ provider: "anthropic" }),
displayManager: new Displaymanager(),
systemPrompt: "You bake pizzas.",
compaction_config: { compaction_instructions: "..." },
})
.fold(checkOrderTool)
.build(ctx.store ?? undefined),
);
const runId = await pizzaBaker.trigger({ orderId: "abc-123" });
.triggered() returns TriggeredAgentBuilder<TInput> — .retries() / .every() / .withInput() are only available here. .concurrent() returns ConcurrentAgentBuilder<TInput> — its built agent gets a .notify(input) instance method on top of .trigger(input) (both enqueue kind: "notify" runs that route to the warm subprocess; notify() is the clearer name when you're sure the peer is warm). Calling .notify() on a triggered agent is a type error, not a runtime error.
Factory context (AgentFactoryContext):
name — registered agent name
runId — per-wakeup for triggered; "warmup" during concurrent factory setup
mode — "triggered" | "concurrent"
store — the StoreAdapter the runtime built from .store(factory) (or null if .store(...) wasn't called)
subscriber — an IPC-forwarding SubscriberAdapter the bootstrap re-attaches defensively after the factory returns
controls.emit({ type, data }) — emit a custom event back to the runner's subscribers, wrapped as an agent:event envelope
controls.signal — AbortSignal that fires on graceful stop / restart / terminal fail; use it to unmount mesh, close DB pools, etc.
The ContinuumRunner
import {
ContinuumRunner,
MemoryAdapter,
ConsoleSubscriber,
} from "glove-continuum-signal";
const runner = new ContinuumRunner({
agentsDir: "./agents",
adapter: new MemoryAdapter(),
subscribers: [new ConsoleSubscriber()],
pollIntervalMs: 1_000,
maxConcurrent: 5,
warmRestartPolicy: { maxRestarts: 5, backoffMs: 1_000 },
});
await runner.start();
const runId = await pizzaBaker.trigger({ orderId: "abc-123" });
const final = await runner.waitForRun(runId);
const notifyId = await runner.notify("pizza-watcher", { event: "oven_ready" });
await runner.stop({ graceful: true, timeoutMs: 10_000 });
runner.notify(name, input) is the runner-bound equivalent of concurrentAgent.notify(input) — handy when you have the runner reference but not the agent reference (e.g. from a wrapper that holds the runner).
Persistent stores