Inspector & WebChat
Every Foundry server ships an inspector at its root URL: browse definitions and instances, chat with any instance over its real stored history, start runs, follow each run’s event trace live, and approve or deny tool calls. It is a client of the same HTTP API your code uses, and it keeps no state of its own.
Open it
glove foundry dev
# Glove Foundry
# Local: http://127.0.0.1:4141The server returns the inspector for every GET path outside /api/*, /v1/* and /health*, so every view below is a real, bookmarkable URL. When the application sets requestAuthorization, the inspector, its API calls and its event stream all pass through it (see Deploy & security).
Pages
| URL | Page | What it answers |
|---|---|---|
/ | Overview | Is the runtime healthy, what is running, and which approvals are pending? |
/agents | Agents | Which definitions exist and which instances were created from them. |
/agents/:definitionId | Agent definition | What the file route can assemble: lazy fields, catalogue, layers, subscribers, schedules, playbooks. |
/instances/:agentId | Agent instance | Its context, installations, playbooks, conversations and runs. |
/chat, /chat/:conversationId?agent=:agentId | Chat (WebChat) | What this instance and conversation actually said; continue, stop or redirect it. |
/runs, /runs?status=&source=&q= | Runs | Which invocations happened, from which source, with what status and duration. |
/runs/:runId | Run detail | Which phases and events produced this outcome. |
/automations | Automations | Schedules, sleeping runs, playbook subscriptions and inbound application connections. |
/integrations | Integrations | Transmissions, safe account references, routes and bindings. |
/workspaces, /workspaces/:workspaceId | Workspaces | Shared entries, inbox items, tasks and non-secret environment values. |
The inspector displays installations but does not change them: install and uninstall with client.install / PUT /api/installations (see Client & HTTP API). It can create instances and conversations, start, stop and redirect runs, and resolve approvals. Every truncated id has a copy button.
WebChat
Chat is a browser client for a real instance and conversation, not a second transcript system:
- The left rail lists runtime instances and each instance’s conversations. New conversation (or
c) creates one; the first text turn becomes its title, and Rename changes the title without changing the id. - History is the exact native Glove
Messagelist read from the agent’sstoreor the application’sconversationStore, including tool calls and results. It loads the latest 100 messages; Earlier and Newer page through longer sessions. Every refresh reloads it from the server. - The header shows the store’s turn and token counters. Export downloads the loaded transcript page as JSON.
- Attach files with the paperclip: images, video and other documents are sent as base64 content parts, so large files need a higher
server.messageBodyBytesthan the 1 MB default. Entersends,Shift+Enteradds a line.- While a run is active, streamed
text_deltaevents form a temporary reply that is replaced by the stored message when the run settles; output buffered by a failed provider attempt never appears. - Stop cancels the active run cooperatively. Sending while a run is active becomes Redirect: Foundry interrupts the run and starts your message as a replacement run in the same conversation (see Approvals & steering).
With no store configured, the page says “This agent has no conversation store” and shows no history. With a new MemoryStore(...) store (the templates’ default) the history is empty too, because each run executes in its own process. Configure a durable store first: Persistence.
Runs and the run trace
Run detail opens with a four-phase spine: Accepted (the invocation and its source were recorded), Assembled (context-dependent fields resolved and mounted), Agent work (model and tool activity), and the outcome (completed, failed, cancelled, or still running with a live duration). A failed run states its error above the spine. Run again reopens the run drawer with the same instance and message.
- The event trace is collapsed by default. Each row shows wall-clock time and offset from the start of the run.
- Filter the trace by event category, expand a row to see its payload, and copy the payload from the expanded row.
- A Conversation guidance card appears when goals, facts, forms or context providers are mounted: goal progress, fact and claim counts, form status. It never includes answer values or fact bodies.
- Pending approvals show the exact tool input with approve and deny controls, on the overview and on the blocked run.
The Runs list keeps its filters in the query string, so a filtered view is a shareable link:
| Parameter | Values |
|---|---|
status | running (includes pending), completed, failed, cancelled |
source | A recorded source kind, such as direct, transmission, activation, spawn |
q | Free text matched against agent, run id, instance id and recorded input |
/runs?status=failed&q=invoice opens the failed runs that mention an invoice. Start new work from any page with New run: choose a definition and an existing instance or “Create a new instance”; the inspector reuses that instance’s latest conversation (or creates one) and opens the new run.
Live updates, search and keyboard
The inspector subscribes to /api/events over server-sent events and also reconciles on a slow timer. Bursts of events are coalesced into one refresh, and a refresh keeps your scroll position, expanded rows, open panels and filter text. Relative times and running durations tick every second.
| Key | Action |
|---|---|
⌘K / Ctrl+K | Search pages, definitions, instances and retained runs |
j / k | Move down / up the current list |
Enter or o | Open the highlighted row |
/ | Focus the run filter, or open search elsewhere |
c | Start a run (on Chat: a new conversation) |
r | Refresh runtime data |
Esc | Close the drawer or search |
⌘Enter / Ctrl+Enter | Submit the run drawer |
The event envelope
Everything the inspector shows comes from one stream of FoundryEvent records:
interface FoundryEvent {
readonly id: string; // UUID
readonly sequence: number; // increasing per runtime process; restarts at 1
readonly timestamp: string; // ISO 8601
readonly type: string; // e.g. "run.completed", "agent.tool_use_result"
readonly category: FoundryEventCategory;
readonly agent?: string; // the agent definition id (route), not the instance id
readonly runId?: string;
readonly data: unknown; // instance, conversation and other details live here
}| Category | Typical types |
|---|---|
run | run.dispatched, run.started, run.completed, run.failed, run.cancelled, run.retry, run.timeout |
model | agent.text_delta, agent.model_response, agent.model_response_complete, agent.token_consumption |
tool | agent.tool_use, agent.tool_use_result |
agent | agent.instance.created, agent.instance.configured, conversation.created, conversation.updated |
application | installation.installed, transmission.dispatched, transmission.outbound.delivered, playbook subscription changes |
activation | scheduled-action.created, …paused, …resumed, …cancelled |
memory | agent.foundry.guidance.state, Glove memory events |
extension | Layer and subscriber mounts, hooks, skills, subagents |
mcp, inbox | MCP connection and discovery events; inbox activity (MCP tool calls are tool events) |
system | Assembly (agent.foundry.assembly.*), steering, approvals, and any custom event you emit |
log | log.stdout, log.stderr: plain output lines from a job process |
Events emitted inside a run arrive prefixed with agent.: a Glove tool_use_result becomes agent.tool_use_result, and a custom support.work-note becomes agent.support.work-note in category system.
Work notes, not hidden reasoning
Foundry records what is observable (requests, model responses, tool calls and results, state changes and notes you emit) and adds no channel for a model’s private chain-of-thought. To show intent, have the agent publish short, safe notes. Any assembly resolver can emit through ctx.controls.emit; handlers, layers and subscribers get the same function as context.emit.
import type { GloveFoldArgs } from "glove-core";
import type { AgentAssemblyContext } from "glove-foundry";
import { z } from "zod";
const input = z.object({ note: z.string().max(280) });
/** Lets the agent publish a short, user-safe progress note into the run trace. */
export function workNote(ctx: AgentAssemblyContext): GloveFoldArgs<z.infer<typeof input>> {
return {
name: "work_note",
description:
"Record one short progress note: what you are doing next and why. " +
"Never include secrets, credentials or customer personal data.",
inputSchema: input,
async do({ note }) {
ctx.controls.emit({ type: "support.work-note", data: { note } });
return { status: "success", data: "noted" };
},
};
}Custom subscribers
A subscriber receives every native Glove event of a run (text_delta, model_response_complete, tool_use, tool_use_result, token_consumption, compaction, hook, skill and subagent events) without changing behaviour. It runs in the job process. A defineSubscriber file must be in the agent’s catalogue and selected in subscribers; a plain SubscriberAdapter object can be passed in subscribers directly.
import type { SubscriberEventDataMap } from "glove-core";
import { defineSubscriber } from "glove-foundry";
/** One compact, payload-free audit event per finished tool call. */
export default defineSubscriber({
description: "Audit tool outcomes without recording their payloads",
create: (context) => ({
async record(type, data) {
if (type !== "tool_use_result") return;
const result = data as SubscriberEventDataMap["tool_use_result"];
context.emit({
type: "support.audit.tool",
data: {
tool: result.tool_name,
status: result.result.status,
durationMs: result.duration_ms,
conversationId: context.conversationId,
},
});
},
}),
});import { createAdapter } from "glove-core/models/providers";
import { composeAgent, defineAgent } from "glove-foundry";
import toolAudit from "./subscribers/tool-audit.subscriber.js";
import { workNote } from "./work-note.js";
export default defineAgent({
description: "Answers customer questions and keeps case notes",
// Defined subscribers must be in the catalogue AND selected in `subscribers`.
components: composeAgent(toolAudit),
subscribers: [toolAudit],
tools: (_agent, ctx) => [workNote(ctx)],
model: () =>
createAdapter({
provider: "openrouter",
model: process.env.OPENROUTER_MODEL ?? "openai/gpt-4.1-mini",
stream: true,
}),
systemPrompt:
"You are a support agent. Before each multi-step task, call work_note with one sentence about your plan.",
});Run against a real runtime, one tool call produced agent.support.work-note, agent.tool_use_result and agent.support.audit.tool in the run’s trace.
Exporting events
Events live in an in-memory buffer of the last observability.maxEvents (default 10,000) and are gone after a restart. Read them over HTTP:
| Request | Returns |
|---|---|
GET /api/events | JSON array. Query: after (sequence), agent (definition id), runId, category, limit (default 500, max 5,000) |
GET /api/events with Accept: text/event-stream | Matching retained events, then live ones. Each message is id: <sequence> plus data: <FoundryEvent JSON>; a comment heartbeat every 15 s. Resume with ?after=<last sequence> (the Last-Event-ID header is not read). |
GET /api/runs/:id/events, GET /v1/runs/:id/events | One run’s events (up to 5,000). The /v1 stream closes when the run ends. |
client.getEvents({ runId, agent, after, category }) | The same JSON from the typed client. |
curl -N -H 'accept: text/event-stream' 'http://127.0.0.1:4141/api/events?category=run'To ship events to OpenTelemetry, a warehouse or an audit store from inside the process, host the runtime yourself and pass an observability adapter. The CLI always uses the built-in MemoryObservabilityAdapter; a custom one must implement FoundryObservabilityAdapter (append, which returns the stored event synchronously, list, subscribe and optional clear). Wrapping the built-in adapter is the shortest path:
import { appendFile, mkdir } from "node:fs/promises";
import { resolve } from "node:path";
import {
FoundryRuntime,
FoundryServer,
MemoryObservabilityAdapter,
type FoundryEvent,
} from "glove-foundry";
import application from "../foundry.application.js";
import config from "../foundry.config.js";
const rootDir = resolve(import.meta.dirname, "..");
const exportFile = resolve(rootDir, ".data/events.ndjson");
await mkdir(resolve(rootDir, ".data"), { recursive: true });
const observability = new MemoryObservabilityAdapter({ maxEvents: 10_000 });
// Listeners run synchronously inside every append: never throw, never block.
let queue = Promise.resolve();
observability.subscribe((event: FoundryEvent) => {
queue = queue
.then(() => appendFile(exportFile, `${JSON.stringify(event)}\n`)) // your code: OTel, a warehouse…
.catch(() => undefined);
});
const runtime = await FoundryRuntime.discover({
rootDir,
agentsDir: resolve(rootDir, "agents"),
application,
// Job processes import the application from this path; without it they run without your adapters.
applicationFilePath: resolve(rootDir, "foundry.application.ts"),
config,
observability,
});
await runtime.start();
const server = new FoundryServer(runtime, { host: "127.0.0.1", port: Number(process.env.PORT ?? 4141) });
console.log(`Foundry listening on ${(await server.listen()).url}`);
const stop = async () => {
await server.close();
await runtime.stop();
};
process.once("SIGTERM", () => void stop());
process.once("SIGINT", () => void stop());Run it with node --import tsx scripts/serve.ts. Unlike the CLI, this host does not load .env files or regenerate .foundry/routes.d.ts (call writeGeneratedTypes if you need them). Dedupe exported events by id: sequence restarts with the process.
What is recorded, and what is redacted
- Native Glove events are recorded as they are: model responses, tool inputs and results, and runtime context can contain conversation content. Apply access control and retention to the event stream like you would to transcripts.
- Outbound transmission payloads are redacted by default (recorded as
{ redacted: true }). Defineoutbound.observe(input)on the transmission to record a deliberate, secret-safe projection such as a thread id and a character count (see Apps & transmissions). - The
foundry.guidance.stateevent carries progress metadata only, never form answers or fact bodies. - Request credentials checked by
requestAuthorizationand account sessions never enter events. Never put secrets in instance context, tool results or emitted notes. - If you enable reasoning capture on a model adapter, that text is stored on transcript messages like any other field; the inspector does not render it.
Branding
import { defineConfig } from "glove-foundry/config";
export default defineConfig({
branding: {
name: "Acme Support", // shell title; default "Glove Foundry"
label: "Agent operations", // line under the name; default "Runtime inspector"
descriptor: "Console", // browser title suffix; default "Inspector"
accent: "#2f6fed", // six-digit hex; anything else is ignored
},
});The browser title becomes <name> · <descriptor>. Branding is read by glove foundry dev/start; a custom host passes it as new FoundryServer(runtime, { branding }).