Monitor

Locales (Message Catalog Pattern)

Ship the agent's voice as a locale pack. defaultCommentaryMessages + defaultThinkingMessages + composeMessages + validateMessages — the i18n surface for agentfootprint observability prose.

agentfootprint emits two streams of user-facing prose: commentary (third-person — "the agent dispatched the refund tool") and thinking (first-person — "Looking up your order…"). Both live as flat Record<string, string> catalogs with {{var}} substitution. The Message Catalog Pattern lets you ship a Spanish, branded, or domain-specific catalog the way you'd ship an i18n bundle — composeMessages(defaults, overrides) + validateMessages(catalog, requiredKeys).

What ships in agentfootprint/locales

ExportPurpose
defaultCommentaryMessagesCanonical English commentary bundle (alias of defaultCommentaryTemplates)
defaultThinkingMessagesCanonical English thinking bundle (alias of defaultStatusTemplates)
composeMessages(defaults, overrides?)Spread overrides on defaults; missing keys fall back. Returns frozen catalog.
validateMessages(catalog, requiredKeys, opts?)Assert required keys are present; batches errors
MessageCatalog (type)Readonly<Record<string, string>>

Symbol identity is preserved: defaultCommentaryMessages === defaultCommentaryTemplates (the v2.4 name still works). The new naming reflects the pattern's purpose; the old name keeps existing imports valid.

The two catalogs

CatalogAudienceVoiceWhere it shows
CommentaryThird-person narratorPast-tense proseLens bottom panel, CLI tail, log files
ThinkingFirst-person statusPresent-tense status lineChat-bubble UIs, "the agent is doing X" indicators

Both share the same shape: a flat Record<string, string> with {{var}} substitution, but they use different key sets. Commentary keys mirror agentfootprint event types and payload branches (e.g., 'stream.tool_start', 'context.injected.rag', 'composition.enter.Sequence'). Thinking keys are the four chat-bubble states — idle, streaming, tool, paused — plus optional per-tool tool.<toolName> overrides.

Locale pack drop-in

import { Agent } from 'agentfootprint';
import {
  defaultThinkingMessages,
  composeMessages,
  validateMessages,
} from 'agentfootprint/locales';

// Spanish locale pack — override only the keys you customize.
// Thinking keys are the four chat-bubble states: idle, streaming, tool, paused
// (plus optional per-tool `tool.<toolName>` overrides). `{{appName}}` is injected
// automatically; `tool` also gets `{{toolName}}`, `streaming` gets `{{partial}}`,
// and `paused` gets `{{question}}`.
const esThinking = composeMessages(defaultThinkingMessages, {
  idle:   '{{appName}} está pensando…',
  tool:   'Trabajando en `{{toolName}}`…',
  paused: 'Esperándote: {{question}}',
});

// Catch drift between your pack and the framework's required keys at boot
validateMessages(esThinking, Object.keys(defaultThinkingMessages), 'es-MX thinking');

const agent = Agent.create({ provider, model: 'claude-sonnet-4-5' })
  .system('Eres un asistente.')
  .appName('Asistente')
  .thinkingTemplates(esThinking)
  .build();

Empty values are intentional

The default catalogs use '' (empty string) for events the renderer should skip — "don't render anything for this event in the UI." validateMessages accepts empty values by default to match this convention. If your locale-pack contract requires every key to have a non-empty value, opt in:

validateMessages(myCatalog, requiredKeys, { forbidEmpty: true });
// Throws on missing OR empty.

Composability

composeMessages returns a frozen object (no post-hoc mutation). Inputs are not mutated. Extra override keys are preserved (forward-compat for consumer-defined keys):

const merged = composeMessages(defaultCommentaryMessages, {
  brand:                       'Acme',           // consumer-defined extra
  'stream.tool_start':         '{{toolName}}',  // override existing
});
// merged.brand === 'Acme'
// merged['stream.tool_start'] === '{{toolName}}'
// every other defaultCommentaryMessages key falls back

Validate forms

// Bare label string (legacy-friendly)
validateMessages(myCatalog, requiredKeys, 'es-MX commentary');

// Options object (preferred — supports forbidEmpty)
validateMessages(myCatalog, requiredKeys, {
  label: 'es-MX commentary',
  forbidEmpty: true,
});

Why the Message Catalog name (vs "templates")

v2.4 named these defaultCommentaryTemplates / defaultStatusTemplates — accurate but generic. "Templates" overloads the term across TypeScript generics, templating-engine output, and i18n message bundles. The Message Catalog Pattern name has industry currency (Resource Bundle in Java, ICU MessageFormat, Fluent in Mozilla's i18n stack) and makes the i18n shape explicit.

The v2.4 names remain symbol-identical aliases so existing code keeps working unchanged through the v2.x line.

Anti-patterns

  • Don't bundle locale packs into your library code. Ship them as data (JSON, separate files) so your CI/CD / l10n pipeline can update them without touching agent logic.
  • Don't skip validateMessages at boot. A typo in a key name silently falls through to the default; you'll discover it weeks later when someone asks why the Spanish version says "Calling X..." in English. Validate at boot.
  • Don't mutate a returned composeMessages result. It's frozen for that reason. Re-compose to update.

Next steps

On this page