Operate / Inspector & WebChat

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

terminalbash
glove foundry dev
# Glove Foundry
#   Local:    http://127.0.0.1:4141

The 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

URLPageWhat it answers
/OverviewIs the runtime healthy, what is running, and which approvals are pending?
/agentsAgentsWhich definitions exist and which instances were created from them.
/agents/:definitionIdAgent definitionWhat the file route can assemble: lazy fields, catalogue, layers, subscribers, schedules, playbooks.
/instances/:agentIdAgent instanceIts context, installations, playbooks, conversations and runs.
/chat, /chat/:conversationId?agent=:agentIdChat (WebChat)What this instance and conversation actually said; continue, stop or redirect it.
/runs, /runs?status=&source=&q=RunsWhich invocations happened, from which source, with what status and duration.
/runs/:runIdRun detailWhich phases and events produced this outcome.
/automationsAutomationsSchedules, sleeping runs, playbook subscriptions and inbound application connections.
/integrationsIntegrationsTransmissions, safe account references, routes and bindings.
/workspaces, /workspaces/:workspaceIdWorkspacesShared 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 Message list read from the agent’s store or the application’s conversationStore, 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.messageBodyBytes than the 1 MB default.
  • Enter sends, Shift+Enter adds a line.
  • While a run is active, streamed text_delta events 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:

ParameterValues
statusrunning (includes pending), completed, failed, cancelled
sourceA recorded source kind, such as direct, transmission, activation, spawn
qFree 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.

KeyAction
⌘K / Ctrl+KSearch pages, definitions, instances and retained runs
j / kMove down / up the current list
Enter or oOpen the highlighted row
/Focus the run filter, or open search elsewhere
cStart a run (on Chat: a new conversation)
rRefresh runtime data
EscClose the drawer or search
⌘Enter / Ctrl+EnterSubmit the run drawer

The event envelope

Everything the inspector shows comes from one stream of FoundryEvent records:

glove-foundrytypescript
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
}
CategoryTypical types
runrun.dispatched, run.started, run.completed, run.failed, run.cancelled, run.retry, run.timeout
modelagent.text_delta, agent.model_response, agent.model_response_complete, agent.token_consumption
toolagent.tool_use, agent.tool_use_result
agentagent.instance.created, agent.instance.configured, conversation.created, conversation.updated
applicationinstallation.installed, transmission.dispatched, transmission.outbound.delivered, playbook subscription changes
activationscheduled-action.created, …paused, …resumed, …cancelled
memoryagent.foundry.guidance.state, Glove memory events
extensionLayer and subscriber mounts, hooks, skills, subagents
mcp, inboxMCP connection and discovery events; inbox activity (MCP tool calls are tool events)
systemAssembly (agent.foundry.assembly.*), steering, approvals, and any custom event you emit
loglog.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.

agents/support/work-note.tstypescript
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.

agents/support/subscribers/tool-audit.subscriber.tstypescript
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,
        },
      });
    },
  }),
});
agents/support/agent.tstypescript
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:

RequestReturns
GET /api/eventsJSON array. Query: after (sequence), agent (definition id), runId, category, limit (default 500, max 5,000)
GET /api/events with Accept: text/event-streamMatching 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/eventsOne 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.
terminalbash
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:

scripts/serve.tstypescript
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 }). Define outbound.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.state event carries progress metadata only, never form answers or fact bodies.
  • Request credentials checked by requestAuthorization and 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

foundry.config.tstypescript
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 }).