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, stores | pnpm add glove-core |
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 |
Most projects need just glove-react + glove-next. glove-core is included as a dependency of glove-react.
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),
dofunction, optionalrender+renderResult. - Display Stack — Stack of UI slots tools push onto.
pushAndWaitblocks tool;pushAndForgetdoesn'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 byrenderResultfor 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 withis_compaction: true.
Quick Start (Next.js)
1. Install
pnpm add glove-core glove-react glove-next zod
2. Server route
// app/api/chat/route.ts
import { createChatHandler } from "glove-next";
export const POST = createChatHandler({
provider: "anthropic", // or "openai", "openrouter", "gemini", etc.
model: "claude-sonnet-4-20250514",
});
Set ANTHROPIC_API_KEY (or OPENAI_API_KEY, etc.) in .env.local.
3. Define tools with defineTool
// lib/glove.tsx
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, // Zod schema for display props
resolveSchema: z.string(), // Zod schema for resolve value
displayStrategy: "hide-on-complete", // Hide slot after user responds
async do(input, display) {
const selected = await display.pushAndWait(input); // typed!
return {
status: "success" as const,
data: `User selected: ${selected}`, // sent to AI
renderData: { question: input.question, selected }, // client-only
};
},
render({ props, resolve }) { // typed props, typed resolve
return (
<div>
<p>{props.question}</p>
{props.options.map(opt => (
<button key={opt.value} => resolve(opt.value)}>
{opt.label}
</button>
))}
</div>
);
},
renderResult({ data }) { // renders from history
const { question, selected } = data as { question: string; selected: string };
return <div><p>{question}</p><span>Selected: {selected}</span></div>;
},
});
// Tools without display stay as raw ToolConfig
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
// app/providers.tsx
"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>;
}
// app/page.tsx — using <Render> component
"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:
// app/page.tsx — manual rendering
"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 => { e.preventDefault(); sendMessage(input.trim()); setInput(""); }}>
<input value={input} => setInput(e.target.value)} disabled={busy} />
<button type="submit" disabled={busy}>Send</button>
</form>
</div>
);
}
Display Stack Patterns
pushAndForget — Show results (non-blocking)
async do(input, display) {
const data = await fetchData(input);
await display.pushAndForget({ input: data }); // Shows UI, tool continues
return { status: "success", data: "Displayed results", renderData: data };
},
render({ data }) {
return <Card>{data.title}</Card>;
},
renderResult({ data }) {
return <Card>{(data as any).title}</Card>; // Same card from history
},
pushAndWait — Collect user input (blocking)
async do(input, display) {
const confirmed = await display.pushAndWait({ input }); // Pauses until user responds
return {
status: "success",
data: confirmed ? "Confirmed" : "Cancelled",
renderData: { confirmed },
};
},
render({ data, resolve }) {
return (
<div>
<p>{data.message}</p>
<button => resolve(true)}>Yes</button>
<button => resolve(false)}>No</button>
</div>
);
},
renderResult({ data }) {
const { confirmed } = data as { confirmed: boolean };
return <div>{confirmed ? "Confirmed" : "Cancelled"}</div>;
},
Display Strategies
| Strategy | Behavior | Use for |
|---|---|---|
"stay" (default) |
Slot always visible | Info cards, results |
"hide-on-complete" |
Hidden when slot is resolved | Forms, confirmations, pickers |
"hide-on-new" |
Hidden when newer slot from same tool appears | Cart summaries, status panels |
SlotRenderProps
| Prop | Type | Description |
|---|---|---|
data |
T |
Input passed to pushAndWait/pushAndForget |
resolve |
(value: unknown) => void |
Resolves the slot. For pushAndWait, the value returns to do. For pushAndForget, use resolve() or removeSlot(id) to dismiss. |
reject |
(reason?: string) => void |
Rejects the slot. For pushAndWait, this causes the promise to reject. Use for cancellation flows. |
Tool Definition
defineTool (recommended for tools with UI)
import { defineTool } from "glove-react";
const tool = defineTool({
name: string,
description: string,
inputSchema: z.ZodType, // Zod schema for tool input
displayPropsSchema?: z.ZodType, // Zod schema for display props (recommended for tools with UI)
resolveSchema?: z.ZodType, // Zod schema for resolve value (omit for pushAndForget-only)
displayStrategy?: SlotDisplayStrategy,
requiresPermission?: boolean,
do(input, display): Promise<ToolResultData>, // display is TypedDisplay<D, R>
render?({ props, resolve, reject }): ReactNode,
renderResult?({ data, output, status }): ReactNode,
});
Key points:
do()should return{ status, data, renderData }—datagoes to model,renderDatastays client-onlyrender()gets typedprops(matching displayPropsSchema) and typedresolve(matching resolveSchema)renderResult()receivesrenderDatafor showing read-only views from historydisplayPropsSchemais optional but recommended — tools without display should use rawToolConfig
ToolConfig (for tools without UI or manual control)
interface ToolConfig<I = any> {
name: string;
description: string;
inputSchema: z.ZodType<I>;
do: (input: I, display: ToolDisplay) => Promise<ToolResultData>;
render?: (props: SlotRenderProps) => ReactNode;
renderResult?: (props: ToolResultRenderProps) => ReactNode;
displayStrategy?: SlotDisplayStrategy;
requiresPermission?: boolean;
}
ToolResultData
interface ToolResultData {
status: "success" | "error";
data: unknown; // Sent to the AI model
message?: string; // Error message (for status: "error")
renderData?: unknown; // Client-only — NOT sent to model, used by renderResult
}
Important: Model adapters explicitly strip renderData before sending to the AI. This makes it safe to store sensitive client-only data (e.g., email addresses, UI state) in renderData.
<Render> Component
Headless render component that replaces manual timeline rendering:
import { Render } from "glove-react";
<Render
glove={gloveHandle} // return value of useGlove()
strategy="interleaved" // "interleaved" | "slots-before" | "slots-after" | "slots-only"
renderMessage={({ entry, index, isLast }) => ...}
renderToolStatus={({ entry, index, hasSlot }) => ...}
renderStreaming={({ text }) => ...}
renderInput={({ send, busy, abort }) => ...}
renderSlotContainer={({ slots, renderSlot }) => ...}
as="div" // wrapper element
className="chat"
/>
Features:
- Automatic slot visibility based on
displayStrategy - Automatic
renderResultrendering for completed tools withrenderData - Interleaving: slots appear inline next to their tool call
- Sensible defaults for all render props
GloveHandle Interface
The interface consumed by <Render>, returned by useGlove():
interface GloveHandle {
timeline: TimelineEntry[];
streamingText: string;
busy: boolean;
slots: EnhancedSlot[];
sendMessage: (text: string, images?: { data: string; media_type: string }[]) => void;
abort: () => void;
renderSlot: (slot: EnhancedSlot) => ReactNode;
renderToolResult: (entry: ToolEntry) => ReactNode;
resolveSlot: (slotId: string, value: unknown) => void;
rejectSlot: (slotId: string, reason?: string) => void;
}
useGlove Hook Return
| Property | Type | Description |
|---|---|---|
timeline |
TimelineEntry[] |
Messages + tool calls |
streamingText |
string |
Current streaming buffer |
busy |
boolean |
Agent is processing |
slots |
EnhancedSlot[] |
Active display stack with metadata |
tasks |
Task[] |
Agent task list |
stats |
GloveStats |
{ turns, tokens_in, tokens_out } |
sendMessage(text, images?) |
void |
Send user message |
abort() |
void |
Cancel current request |
renderSlot(slot) |
ReactNode |
Render a display slot |
renderToolResult(entry) |
ReactNode |
Render a tool result from history |
resolveSlot(id, value) |
void |
Resolve a pushAndWait slot |
rejectSlot(id, reason?) |
void |
Reject a pushAndWait slot |
TimelineEntry
type TimelineEntry =
| { kind: "user"; text: string; images?: string[] }
| { kind: "agent_text"; text: string }
| { kind: "tool"; id: string; name: string; input: unknown; status: "running" | "success" | "error"; output?: string; renderData?: unknown };
type ToolEntry = Extract<TimelineEntry, { kind: "tool" }>;
Supported Providers
| Provider | Env Variable | Default Model | SDK Format |
|---|---|---|---|
openai |
OPENAI_API_KEY |
gpt-4.1 |
openai |
anthropic |
ANTHROPIC_API_KEY |
claude-sonnet-4-20250514 |
anthropic |
openrouter |
OPENROUTER_API_KEY |
anthropic/claude-sonnet-4 |
openai |
gemini |
GEMINI_API_KEY |
gemini-2.5-flash |
openai |
minimax |
MINIMAX_API_KEY |
MiniMax-M2.5 |
openai |
kimi |
MOONSHOT_API_KEY |
kimi-k2.5 |
openai |
glm |
ZHIPUAI_API_KEY |
glm-4-plus |
openai |
Pre-built Tool Registry
Available at https://glove.dterminal.net/tools — copy-paste into your project:
confirm_action— Yes/No confirmation dialogcollect_form— Multi-field formask_preference— Single-select preference pickertext_input— Free-text inputshow_info_card— Info/success/warning card (pushAndForget)suggest_options— Multiple-choice suggestionsapprove_plan— Step-by-step plan approval
Supporting Files
For detailed API reference, see api-reference.md. For example patterns from real implementations, see examples.md.
Common Gotchas
- model_response_complete vs model_response: Streaming adapters emit
model_response_complete, notmodel_response. Subscribers must handle both. - Closure capture in React hooks: When re-keying sessions, use mutable
let currentKey = keyto avoid stale closures. - React useEffect timing: State updates don't take effect in the same render cycle — guard with early returns.
- Browser-safe imports:
glove-corebarrel exports include native deps (better-sqlite3). For browser code, import from subpaths:glove-core/core,glove-core/glove,glove-core/display-manager,glove-core/tools/task-tool. Displaymanagercasing: The concrete class isDisplaymanager(lowercase 'm'), notDisplayManager. Import it as:import { Displaymanager } from "glove-core/display-manager".createAdapterstream default:streamdefaults totrue, notfalse. Passstream: falseexplicitly if you want synchronous responses.- Tool return values: The
dofunction should returnToolResultDatawith{ status, data, renderData? }.datagoes to the AI;renderDatastays client-only. - Zod .describe(): Always add
.describe()to schema fields — the AI reads these descriptions to understand what to provide. - displayPropsSchema is optional but recommended:
defineTool'sdisplayPropsSchemais optional, but recommended for tools with display UI — tools without display should use rawToolConfiginstead. - renderData is stripped by model adapters: Model adapters explicitly exclude
renderDatawhen formatting tool results for the AI, so it's safe for client-only data.