Glove ships three extension primitives: /hook directives that mutate agent state, /skill directives that inject context, and subagent factories the main agent routes to via the auto-registered glove_invoke_subagent tool.
Hooks and skills are parsed out of the user's text in processRequest and dispatched before the model sees the turn. Subagents are addressed by the model itself — the user's @name text reaches the model verbatim and acts as a routing signal that nudges the agent to call the dispatch tool with { name, prompt }.
Builders that register no extensions see no behavioural change.
| Primitive | How it's invoked | Typical use |
|---|---|---|
/hook | User-side /name directive. Handler runs before the model with full AgentControls; can rewrite the text or short-circuit the turn. | Force compaction, swap model mid-conversation, cancel a turn, run a one-off side effect. |
/skill | User-side /name directive that becomes a synthetic user message marked is_skill_injection: true. Optionally exposed to the agent via glove_invoke_skill. | Tone presets, persona overlays, attaching a checklist, pulling in a prompt template. |
| Subagent | Factory builds a fresh child Glove on every invocation. The parent agent calls glove_invoke_subagent with { name, prompt }; the framework runs the child and returns its final text as the tool result. | Specialised reviewers, planners, deterministic responders, hand-offs to external agents. |
/hook and /skill only bind when the name matches a registered handler — paths like /usr/local/bin survive untouched. @mention tokens are never parsed by glove at all, so emails like a@b.com reach the model unchanged.
When a user-side directive binds, the original /name token is replaced with a non-triggerable placeholder of the form [invoked_extension__hook_<name>] or [invoked_extension__skill_<name>]. The placeholder survives in the persisted user message — and in the parsedText handed to handlers — so transcripts can show what the user typed without the directive re-firing on a future parse.
Three builder methods complement fold. They're chainable and legal at any time, including after build() — the same as fold.
import { Glove, MemoryStore, Displaymanager, createAdapter } from "glove-core";
const agent = new Glove({
store: new MemoryStore("session"),
model: createAdapter({ provider: "anthropic" }),
displayManager: new Displaymanager(),
systemPrompt: "You are a helpful assistant.",
compaction_config: { compaction_instructions: "Summarize so far." },
})
.defineHook("compact", async ({ controls }) => {
await controls.forceCompaction();
})
.defineHook("stop", async () => ({
shortCircuit: {
message: { sender: "agent", text: "Stopped." },
},
}))
.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 ({ prompt, parentStore, parentControls }) => {
const subStore = await parentStore.createSubAgentStore?.("weather", false)
?? new MemoryStore(`weather-${Date.now()}`);
return new Glove({
store: subStore,
model: createAdapter({ provider: "anthropic" }),
displayManager: parentControls.displayManager,
systemPrompt: "You answer weather questions concisely.",
compaction_config: { compaction_instructions: "Summarize so far." },
}).build();
},
})
.build();
await agent.processRequest("/concise tell me about Rust");
await agent.processRequest("/compact what's next?");
// "@weather" reaches the model verbatim. The model then calls
// glove_invoke_subagent({ name: "weather", prompt: "NYC" }).
await agent.processRequest("@weather NYC");Anything not matching a registered name is left in place. A user sending "ping /me at 3pm" with no /me hook keeps the slash intact.
A hook is the most powerful primitive — it runs before the model sees the message and receives an AgentControls handle. Use it when you need to reach into the agent's internals.
type HookHandler = (ctx: HookContext) => Promise<HookResult | void>;
interface HookContext {
name: string;
rawText: string; // original user text
parsedText: string; // text with bound /name tokens replaced by placeholders
controls: AgentControls;
signal?: AbortSignal;
}
interface HookResult {
rewriteText?: string; // override parsedText for downstream skills + the user message
shortCircuit?:
| { message: Message }
| { result: ModelPromptResult };
}
interface AgentControls {
context: Context;
observer: Observer;
promptMachine: PromptMachine;
executor: Executor;
glove: IGloveRunnable;
store: StoreAdapter;
displayManager: DisplayManagerAdapter;
forceCompaction: () => Promise<void>;
}Hooks run sequentially in the order their tokens appear in the message. Returning { rewriteText } replaces the working text passed to subsequent hooks, skills, and the final user message. Returning shortCircuit persists the user message and immediately returns the supplied Message or ModelPromptResult — the model is not called.
Each hook invocation also emits a hook_invoked subscriber event with the bound name.
// Force compaction on demand. Useful for "/compact" before a long question.
agent.defineHook("compact", async ({ controls }) => {
await controls.forceCompaction();
});
// Swap to a stronger model for one specific turn.
agent.defineHook("opus", ({ controls }) => {
controls.glove.setModel(opusAdapter);
return; // no rewrite, no short-circuit
});
// Cancel the turn entirely with a canned response.
agent.defineHook("cancel", async () => ({
shortCircuit: {
message: { sender: "agent", text: "Cancelled — nothing was sent to the model." },
},
}));
// Rewrite the user message before any skill / model call.
agent.defineHook("formal", async ({ parsedText }) => ({
rewriteText: parsedText.replace(/\bgonna\b/g, "going to"),
}));Skills inject context. When a /skill token binds, its handler returns a string or ContentPart[]; Glove turns that into a synthetic user-role message persisted via context.appendMessages before the real user message, marked with is_skill_injection: true so consumers can style or filter them in the transcript. Each invocation emits a skill_invoked subscriber event with source: "user".
type SkillHandler = (ctx: SkillContext) => Promise<string | ContentPart[]>;
interface SkillContext {
name: string;
// when source = "user": user message with bound /name tokens replaced by placeholders.
// when source = "agent": same as args ?? "" (the model-supplied string).
parsedText: string;
args?: string; // model-supplied free-form args (only when source = "agent")
source: "user" | "agent";
controls: AgentControls;
}
interface SkillOptions {
description?: string; // shown to the agent in the invoke-skill tool
exposeToAgent?: boolean; // default false
}
// defineSkill takes an object form mirroring fold(GloveFoldArgs).
interface DefineSkillArgs extends SkillOptions {
name: string;
handler: SkillHandler;
}Set exposeToAgent: true on a skill and Glove auto-registers a single glove_invoke_skill tool. Its description lists every exposed skill (with the description you supply) and is rebuilt in place whenever a new exposed skill is defined — so additions registered post-build() are immediately visible to the model. Each agent-side invocation emits a skill_invoked event with source: "agent" and the supplied args.
agent.defineSkill({
name: "research-mode",
description: "Switch to long-form research mode with citations",
exposeToAgent: true,
handler: async ({ source, args, parsedText }) => {
if (source === "agent") {
// Agent invoked via glove_invoke_skill — args is the model-supplied string.
return `Switch into research mode. Focus: ${args ?? "general"}.`;
}
// source === "user" — parsedText contains the rest of the user message,
// with the /research-mode token replaced by [invoked_extension__skill_research-mode].
return `Switch into research mode. User said: ${parsedText}`;
},
});
// User can invoke it inline:
// "/research-mode tell me about ribosomes"
// or the agent can invoke it as a tool:
// glove_invoke_skill({ name: "research-mode", args: "ribosome assembly" })Tool result for glove_invoke_skill on success with a string handler return is { status: "success", data: { skill, content } }. When the handler returns a 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). On unknown or unexposed names the tool returns { status: "error", message: "Skill ... is not available", data: null }.
| Aspect | User /skill | Agent glove_invoke_skill |
|---|---|---|
| How it lands in context | 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 |
skill_invoked event source | "user" | "agent" (with args) |
defineSkill is the right primitive when the injected content has to be computed at invocation time — environment snapshots, time-aware briefings, anything that closes over runtime state. For coding agents the more common shape is static content: a body of instructions, a reference table, a checklist, sliced into a main file plus accompanying sections. ContentSkill is that shape — pure data, no handler.
Built for agents that may not have filesystem access (web, glovebox, embedded). Skills are loaded lazily through a single glove_read_skill tool whose description embeds a token- budgeted XML listing of every registered skill. The model picks one by name, calls the tool, and the content lands as a tool result. Mirrors Claude Code's on-disk .claude/skills/<name>/SKILL.md convention so you can ship the same skill bundles to either runtime.
interface ContentSkill {
name: string;
description: string;
/** Opaque reader handle (FS path, URL, KV key). Returned alongside content so the agent knows where extended access lives. */
path?: string;
/** Main body. Returned when the agent reads with no section. */
content: string;
/** Optional named sections (e.g. "api-reference", "examples"). */
sections?: Record<string, string>;
/** Higher priority keeps the description in the listing under tight budget. Default 0. */
priority?: number;
/** Omit from the listing but still readable by name. Default false. */
hidden?: boolean;
}
interface SkillReader {
list(): Promise<SkillSummary[]>;
read(name: string, section?: string): Promise<SkillReadResult | null>;
}useReadSkillIdiomatic with the useMemoryReader / useContext family from glove-memory. Pass an array of ContentSkills (or a custom SkillReader) and the helper folds the read tool, builds the listing block in its description, and wires user-side /skill-name directives through the existing parser.
import { Glove, MemoryStore, Displaymanager, createAdapter, useReadSkill } from "glove-core";
const agent = new Glove({
store: new MemoryStore("session"),
model: createAdapter({ provider: "anthropic" }),
displayManager: new Displaymanager(),
systemPrompt: "You are a coding assistant.",
compaction_config: { compaction_instructions: "Summarize so far." },
});
useReadSkill(agent, [
{
name: "python-debug",
description: "Diagnose Python errors and apply fix patterns.",
content: "# Python Debugging\nRead tracebacks bottom-up...",
sections: {
"api-reference": "# API\npdb.set_trace()...",
examples: "# Examples\n...",
},
},
{
name: "git-workflow",
description: "Branch / commit / PR conventions for this repo.",
content: "# Git\nNever force-push main...",
},
]);
agent.build();
// Agent-side: model calls glove_read_skill({ name: "python-debug" })
// User-side: "/python-debug let's debug this" materialises the content
// as a synthetic user message before the real turn.The tool description renders an XML-tagged <available_skills> block listing every registered skill. The default budget is 2000 tokens (~2% of a 100K compaction limit). Skills are sorted by priority desc; descriptions fit until the budget is exhausted; everything below the cut falls back to a name-only <skill name="..." /> line so the model still knows every name. If even name-only lines exceed the budget, an <!-- N more skills omitted --> comment is rendered as a fallback.
useReadSkill(agent, skills, {
// Default: 2000 tokens. Tune up if you've raised compaction_context_limit.
listingBudgetTokens: 4000,
// Optional: replace the in-memory reader with your own. Useful when the
// listing or content needs to come from a database, a remote service,
// or a file system.
reader: customReader,
// Optional: override the tool name (default "glove_read_skill").
toolName: "load_doc",
// Optional: appended after the listing block in the tool description.
descriptionSuffix: "Prefer reading api-reference before answering API questions.",
// Optional: skip the user-side /skill-name wiring. Default true.
wireUserDirectives: false,
});glove_read_skill takes { name, section? }. Omitted section returns the main body plus the list of available section names; supplied section returns that slice. Content is wrapped in XML — Claude-family models parse <skill>...<content>...</content></skill> with crisp delimiters that markdown headers can't match. The raw SkillReadResult is also placed on renderData for client-side renderers.
<skill name="python-debug" path="/skills/python-debug">
<content>
# Python Debugging
Read tracebacks bottom-up...
</content>
<sections>api-reference,examples</sections>
</skill>For a section read the wrapper adds a section attribute. Missing skill / missing section both return { status: "error", message: "...", data: null } with the list of known (non-hidden) names. The error message never leaks hidden: true skill names.
For server-side agents that do have a filesystem, the sibling subpath glove-core/content-skills-fs ships loadContentSkillsFromFs — a thin loader that walks a Claude Code-style .claude/skills/ directory and returns a ContentSkill[] ready for useReadSkill. Node-only (uses node:fs), kept in its own subpath so browser bundles don't pull it in.
import { Glove, useReadSkill } from "glove-core";
import { loadContentSkillsFromFs } from "glove-core/content-skills-fs";
const skills = await loadContentSkillsFromFs("./.claude/skills");
// Each subdirectory becomes a ContentSkill. SKILL.md is the main body
// (YAML frontmatter `name` and `description` override the directory
// name). Sibling .md files (api-reference.md, examples.md, ...) become
// sections keyed by filename without the extension.
useReadSkill(agent, skills);.claude/skills/
├── python-debug/
│ ├── SKILL.md ← optional YAML frontmatter: name, description, priority, hidden
│ ├── api-reference.md ← section "api-reference"
│ └── examples.md ← section "examples"
└── git-workflow/
└── SKILL.md| Use case | Reach for |
|---|---|
| Static instructions, references, checklists, prompt fragments | ContentSkill + useReadSkill |
| Live environment snapshots, time-aware briefings, computed context | defineSkill with a handler |
| Tone presets, persona overlays, simple text injection | Either works; ContentSkill if it's purely static |
| Bundled skill ships to multiple runtimes (web + glovebox + CLI) | ContentSkill — same bundle, no filesystem assumed |
Both coexist on the same agent. useReadSkill registers its skills with exposeToAgent: false internally so glove_invoke_skill never double-lists them; the model discovers content skills exclusively through glove_read_skill.
A subagent is an isolated child Glove the main agent routes to via glove_invoke_subagent({ name, prompt }). You register one with a factory — the framework calls it on every invocation, runs the returned runnable with the supplied prompt, and hands its final text back to the parent agent as the tool result.
type SubAgentFactory = (
ctx: SubAgentFactoryContext,
) => Promise<IGloveRunnable> | IGloveRunnable;
interface SubAgentFactoryContext {
/** Subagent name as registered with defineSubAgent. */
name: string;
/** The task prompt the parent agent supplied via glove_invoke_subagent. */
prompt: string;
/** The parent agent's store. Use createSubAgentStore(name, durable) to derive a child store. */
parentStore: StoreAdapter;
/** Full parent agent controls (context, observer, promptMachine, executor, glove, store, displayManager, forceCompaction). */
parentControls: AgentControls;
}
interface SubAgentOptions {
description?: string; // shown to the agent in the invoke-subagent tool
}
interface DefineSubAgentArgs extends SubAgentOptions {
name: string;
factory: SubAgentFactory;
}The factory runs once per invocation and must return a fully-built IGloveRunnable — i.e. the child Glove must already have build() called on it. The dispatcher then:
child.processRequest(prompt, signal) — the parent's abort signal is forwarded so a parent-side cancel unwinds the child's Agent.ask loop on the next iteration.data.content on the tool result.finally block so durable factories don't accumulate duplicate subscribers across invocations.The factory typically calls parentStore.createSubAgentStore(name, durable) to derive a child store. With durable: false (the default) every invocation gets a fresh store; with durable: true the same child store is returned for the same namespace so the subagent accumulates message history across calls. MemoryStore implements this out of the box, so the common case is friction-free.
import { Glove, MemoryStore, createAdapter } from "glove-core";
agent.defineSubAgent({
name: "reviewer",
description: "Code review specialist. Use when the user asks for a code review.",
factory: async ({ prompt, parentStore, parentControls }) => {
// Derive an isolated store from the parent. durable: false → fresh per call.
const subStore = await parentStore.createSubAgentStore?.("reviewer", false)
?? new MemoryStore(`reviewer-${Date.now()}`);
// Build a fresh child Glove with its own system prompt.
// Sharing the parent's display manager lets reviewer tools render in the
// same UI surface; pass a separate one to keep its UI isolated.
const child = new Glove({
store: subStore,
model: createAdapter({ provider: "anthropic" }),
displayManager: parentControls.displayManager,
systemPrompt: "You are a senior code reviewer. Be specific and direct.",
compaction_config: { compaction_instructions: "Summarize so far." },
});
// Fold reviewer-specific tools as needed before returning the built runnable.
return child.build();
},
});
// User: "@reviewer please look at PR #123"
// The model sees the full text including "@reviewer", picks
// glove_invoke_subagent, and calls it with { name: "reviewer", prompt: "..." }.
// The dispatcher invokes the factory, runs the child, and returns the child's
// final agent text as the tool result.Symmetric with glove_invoke_skill. On success: { status: "success", data: { subagent, content } }. When the factory throws or the child run fails: { status: "error", message: "...", data: null }. Unknown subagent names return an error result listing the registered names.
Every subagent run is bracketed by a matched pair of subscriber events with guaranteed 1:1 symmetry:
subagent_invoked — fired by the Executor immediately before the dispatcher runs. Carries { name, prompt }.subagent_completed — fired by the Executor after the dispatcher resolves, errors, or aborts. Carries { name, status: "success" | "error", message? }.The bracket fires from the Executor (not the dispatcher tool) so that even when an abort signal short-circuits the dispatcher's promise chain, the closing bracket still arrives. Anything the child emits between the open and close brackets — text_delta, tool_use, tool_use_result, even nested subagent brackets — belongs to that subagent run, because the parent's subscribers are temporarily attached to the child for the duration.
Match against the exported SUBAGENT_DISPATCH_TOOL_NAME constant (value: "glove_invoke_subagent") when filtering tool events you want to attribute to subagent dispatch.
Subagents do not see the parent conversation. The only channel from parent to child is the prompt string the agent supplies — the factory is responsible for whatever context the child needs. This isolation keeps the parent context window from bloating with the subagent's intermediate work and matches Claude Code's subagent context model.
Glove with createSubAgentStore(name, false) each call. Best for stateless reviewers, planners, classifiers.createSubAgentStore(name, true) so the child carries message history across invocations. Best for long-running assistants the parent agent dispatches to repeatedly.Glove with a no-op model adapter that always returns a canned message; bypasses any LLM call inside the subagent.IGloveRunnable that proxies processRequest to another service.For memory tools specifically, see the Memory guide and prefer the subagent-delegation pattern: the entity / episodic / resources tools belong on focused retrieval subagents rather than directly on the main agent.
processRequest walks the incoming text once, looking only for /name directive tokens (regex (^|\\s)\\/([A-Za-z][\\w-]*)(?=\\s|$)). For every match it asks the hook then skill registry whether the name binds. Bound tokens are replaced in place with a non-triggerable placeholder; unbound tokens stay untouched. @name tokens are not parsed — they pass through to the model verbatim.
The dispatch order on a single turn is:
/ directives from the raw text.hook_invoked for each. Apply any rewriteText; honour the first shortCircuit and return.source: "user"), emitting skill_invoked for each — each becomes a synthetic user message persisted before the real one.Message from the placeholder-substituted text (including any @mentions, untouched) plus any non-text ContentParts the caller passed.Agent.ask. Subagents surface through the agent loop via glove_invoke_subagent tool calls bracketed by subagent_invoked / subagent_completed events.is_skill_injection flagSkill-materialised messages set is_skill_injection: true on Message. Use it in your transcript renderer to distinguish them from real user turns — render in a muted style, collapse them by default, or filter them out entirely. Pair it with the existing is_compaction flag for similar treatment.
{messages.map((m, i) => {
if (m.is_skill_injection) {
return <div key={i} className="skill-injection">{m.text}</div>;
}
if (m.is_compaction) return null;
return <div key={i}>{m.sender}: {m.text}</div>;
})}pre_modified_text fieldWhen a hook's rewriteText changes the user's message before the model sees it, the original text is preserved on Message.pre_modified_text. Use this in transcript renderers that want to show the user the text they actually typed (rather than the rewritten version the model received).
controls.forceCompaction() calls Observer.runCompactionNow(), which is the same body as tryCompaction minus the token-threshold guard. It's safe to call whenever a hook fires. Subscribers see the usual compaction_start / compaction_end events.
// Builder methods (also available on the runnable post-build)
defineHook(name: string, handler: HookHandler): this;
defineSkill(args: DefineSkillArgs): this;
defineSubAgent(args: DefineSubAgentArgs): this;
// Auto-registered dispatch tool name — match against this constant
// when filtering tool events.
import { SUBAGENT_DISPATCH_TOOL_NAME } from "glove-core";
// SUBAGENT_DISPATCH_TOOL_NAME === "glove_invoke_subagent"
// Standalone helpers (rarely needed)
import {
parseTokens,
formatSkillMessage,
createSkillInvokeTool,
createSubAgentInvokeTool,
} from "glove-core";
// Content skills — pure-data, lazy-loaded, sectioned.
import {
useReadSkill,
createMemorySkillReader,
createReadSkillTool,
renderSkillListing,
renderReadSkillDescription,
renderSkillReadResultXml,
defaultListingBudgetTokens,
READ_SKILL_TOOL_NAME,
DEFAULT_LISTING_BUDGET_TOKENS,
type ContentSkill,
type SkillReader,
type SkillSummary,
type SkillReadResult,
} from "glove-core";
// FS loader for Claude Code-style .claude/skills/<name>/SKILL.md layout.
// Subpath because it uses node:fs.
import { loadContentSkillsFromFs } from "glove-core/content-skills-fs";For full type signatures see the Core API page.