Build it up · Step 1 of 8

A loop and a tool

The smallest harness is a model, one tool, and the loop between them. Across these eight steps we build one agent: a release assistant for a software team. It starts here as a script that answers "what shipped this week?" and ends as a deployed system that people message, that wakes on its own, and that asks before it tags a release.

Surfacechat · voice · APIContextprompt · history · live stateModelany providerStatestore · memory · inboxToolsvalidated · gatedTeamsubagents · meshWorkspacefiles · code · webloopuntil the model answersprompttool callsresultshistorymessageanswerdisplay stack: show · ask · waita subagent is a toolacts on
This step: the loop and its tools. State, context and the model form the cycle every request runs through.

Install

terminalbash
pnpm add glove-core zod
export ANTHROPIC_API_KEY=sk-ant-...

The whole agent

release.tstypescript
import { Glove, Displaymanager, MemoryStore } from "glove-core";
import { createAdapter } from "glove-core/models/providers";
import { z } from "zod";

// Stand-in for your Git host's API.
const merged = [
  { number: 412, title: "Retry failed webhooks", mergedAt: "2026-10-20", labels: ["fix"] },
  { number: 415, title: "Export invoices as CSV", mergedAt: "2026-10-21", labels: ["feature"] },
  { number: 418, title: "Bump Node to 22.13", mergedAt: "2026-10-22", labels: ["chore"] },
];

const agent = new Glove({
  store: new MemoryStore("release-assistant"),
  model: createAdapter({ provider: "anthropic", model: "claude-sonnet-4-20250514" }),
  displayManager: new Displaymanager(),
  systemPrompt:
    "You help a software team ship releases. Use tools for facts about the code; never guess.",
  compaction_config: {
    compaction_instructions: "Summarise the release work so far: what is merged, what is decided.",
  },
})
  .fold({
    name: "list_merged_changes",
    description: "List pull requests merged on or after a date, newest first.",
    inputSchema: z.object({
      since: z.string().describe("ISO date, e.g. 2026-10-19"),
    }),
    async do({ since }) {
      const changes = merged.filter((pr) => pr.mergedAt >= since).reverse();
      return { status: "success", data: changes };
    },
  })
  .build();

const result = await agent.processRequest("What has shipped since the 19th?");
// A hook can answer with a single Message; normally it is the model's result.
console.log("messages" in result ? result.messages[0]?.text : result.text);

A Glove needs a model, a display manager (the surface tools can render to; empty for now), a system prompt and compaction instructions. The store, where the conversation lives, is optional and defaults to a fresh MemoryStore. fold adds a tool (before or after build); build binds the store and makes the agent runnable; processRequest runs the loop until the model answers without asking for a tool.

What the loop already does

That processRequest call is doing more than calling the model in a loop. Each of these is something people write by hand the first time:

  • Validation. Input is checked against the Zod schema before your code runs. A bad call returns TOOL_INPUT_INVALID with the field errors, so the model corrects itself instead of your tool crashing.
  • Unknown tools. A misspelled tool name gets an error listing the tools that exist and the nearest match.
  • Retries. A tool that throws is retried (3 times by default, maxRetries) before the error goes back to the model.
  • Pairing. Every tool call is saved with its result before anything else happens, so history never holds an orphaned call that a provider would reject.
  • A turn limit. One request stops after 120 model turns by default (compaction_config.max_turns) and says so, so a confused agent can't loop forever.
  • Compaction. When the conversation nears the context limit, it is summarised with your instructions. The store keeps every message for your UI; the model sees the summary.
  • Token accounting. Input, output and prompt-cache tokens are counted per call and kept in the store.

Stream it, stop it, switch models

A script wants output as it arrives. Subscribers receive typed events from every stage of the loop; an AbortSignal stops a request between steps.

release.tstypescript
import type { SubscriberEventDataMap as Events } from "glove-core";

agent.addSubscriber({
  async record(type, data) {
    if (type === "text_delta") process.stdout.write((data as Events["text_delta"]).text);
    if (type === "tool_use") {
      const call = data as Events["tool_use"];
      console.error(`\n→ ${call.name}`, call.input);
    }
    if (type === "token_consumption") console.error("tokens", (data as Events["token_consumption"]).consumption);
  },
});

const controller = new AbortController();
process.on("SIGINT", () => controller.abort());
await agent.processRequest("Anything risky in this release?", controller.signal);

// Same agent, same history, another provider:
agent.setModel(createAdapter({ provider: "openrouter", model: "openai/gpt-4.1-mini" }));

createAdapter covers Anthropic, OpenAI, Gemini, OpenRouter, Bedrock, the Vercel AI Gateway, Kimi, GLM, MiniMax, MiMo, Ollama and LM Studio. Pass cache: true for prompt caching where the provider needs it asked for. Your tools never change between providers.

Write tools the model can use

  • The description is the interface. The model picks tools by reading it. Say what it returns and when to use it.
  • Return small, structured data. A tool result stays in context for every later turn. Return what the model needs to decide, and put anything only your UI needs in renderData, which the model never sees.
  • Return status: "error" with a message for expected failures (not found, not allowed) so the model can react. Throw only for real faults, which get retried.
  • Mark tools that must finish (a payment, a write that can't be half-done) with unAbortable: true. Abort waits for them.

Already built for you

Everything Glove gives you for the loop and for tools. Several of these, such as MCP, code-as-tools and classifiers, come up again in later steps.

PrimitiveWhat you getInstead of building
Agent loopglove-coreModel → tool calls → results → model, until it answers. Matched tool call/result pairs, retries, a per-request turn limit.new Glove({ model, store, displayManager, systemPrompt, compaction_config }).build().processRequest(text)a while-loop that calls the model, parses tool calls, runs them and appends results
Model adaptersglove-coreAnthropic, OpenAI, Gemini, OpenRouter, Bedrock, Vercel AI Gateway, Kimi, GLM, MiniMax, MiMo, Ollama and LM Studio behind one interface, with streaming, reasoning and prompt caching.createAdapter({ provider: "anthropic", model, cache: true })provider SDK glue, a streaming parser and tool-call format translation per provider
Hot-swap model and promptglove-coreChange the model or prompt between requests without rebuilding the agent or losing history.glove.setModel(adapter) · glove.setSystemPrompt(text)tearing the agent down to switch models
Abort and un-abortable toolsglove-coreCancel a request mid-loop; tools that must finish (a payment, a write) run to completion first.processRequest(text, signal) · fold({ ..., unAbortable: true })custom cancellation flags threaded through every tool
Subscribersglove-coreTyped events for text deltas, tool calls and results, compaction, tokens and subagents: stream them, log them, meter them.glove.addSubscriber({ record(type, data) { … } })wrapping the loop to stream text, log tool calls or count tokens
Toolsglove-coreZod-validated input, results the model reads, optional renderData the model never sees.glove.fold({ name, description, inputSchema, do })hand-written JSON schemas, input validation and a dispatch switch
Permission gatesglove-coreAsk before a tool runs, per call or always; decisions are stored per tool and input. In Foundry it becomes an approval record.fold({ ..., requiresPermission: (input) => input.destructive })confirm-before-delete logic inside each tool
Tools that change at runtimeglove-coreAdd tools mid-session or swap a whole set atomically.glove.fold(tool) after build · glove.replaceTools(previousNames, nextTools)rebuilding the agent when the available tools change
Tool result summariesglove-coreLarge results replaced by a short summary in later requests.enableToolResultSummary: true · fold({ ..., generateToolSummary }) · do() returns generateSummaryArgstruncating big tool results by hand
MCP serversglove-mcpAny MCP server's tools as Glove tools, a discovery subagent that activates servers mid-conversation, OAuth helpers.mountMcp(glove, { adapter, entries })an MCP client and a schema bridge
Code as the tool surfaceglove-js · glove-python · glove-lispOne execute tool over a catalogue of functions: the model writes a program that loops, filters and branches, and only the answer returns to context.mountJs(glove, { session: JsSession.create() })fifty tool definitions and a round-trip per step
Capabilities as tablesglove-scratchpadResources become SQL tables behind one execute_sql tool, with discovery, joins and staged writes.mountDatabase(glove, { db })a list/get/search tool for every resource
Classifiersglove-classifierFast, calibrated yes/no, choice and score answers from classifier models, with an LLM fallback and confidence gates.mountClassifier(glove, { classifier: jev() })a chat model call for every routing or triage decision
Image and video workflowsglove-image · glove-videoPrompt pipelines, durable characters and scenes, references, review, lineage and cost tracking.mountImage(glove, { adapter, assets, library }) · mountVideo(…)a one-shot generate_image tool with no consistency between images

Next

The agent answers, but only in a terminal, and it can't ask anyone anything. In step 2 it gets a surface: streaming into a web UI, tools that render components, and a tool that waits for a person to confirm.

The whole path

  1. 01A loop and a toolLoop · Tools
  2. 02Show your workSurface
  3. 03Control what it seesContext
  4. 04Make it rememberState
  5. 05Give it a workspaceWorkspace
  6. 06Split the workTeam
  7. 07Make it a systemIdentity · Time · Arrival · Oversight · Access
  8. 08Put it in productionRun · Keep · Reach · Watch