Light Dark

Agents

Agents are typed groups of event handlers, schedules, and webhooks that share identity. An agent is defined as a Hot type with agent metadata, and functions declare membership via meta {agent: TypeName}. When deployed, Hot tracks agent runs, surfaces health metrics, and groups observability data by agent.

Agents group handlers by type reference, giving you compile-time validation and structured config fields.

Defining an Agent

An agent starts with a type definition that has agent in its metadata. The type's struct fields become the agent's configuration, and the doc or agent.description provides a human-readable summary.

Basic Example

::myapp::support ns

SupportAgent meta {
  doc: """AI-powered customer support agent"""
  agent: {
    name: "Support Agent"
    tags: ["support", "ai"]
  }
}
type {
  model: Str
  system: Str
  escalation-channel: Str
}

This registers SupportAgent as an agent. The type name is the identifier; name is a display label for the Hot App.

Full Example

::acme::support ns

::store ::hot::store
::ctx ::hot::ctx

SupportAgent meta {
  doc: """
    Customer support agent that responds to tickets,
    searches a knowledge base, escalates when uncertain,
    and reviews interactions daily.
  """
  agent: {
    name: "Support Agent"
    description: "AI-powered support with semantic KB search and escalation"
    tags: ["support", "ai", "customer-facing"]
  }
}
type {
  model: Str
  system: Str
  escalation-channel: Str
  tone: Str
}

support-agent SupportAgent({
  model: ::ctx/get("support.model", "claude-sonnet")
  system: ::ctx/get("support.system", "You are a helpful support agent.")
  escalation-channel: ::ctx/get("support.escalation", "#support")
  tone: ::ctx/get("support.tone", "professional")
})

// Shared knowledge base (static name, safe at namespace level)
kb ::store/Map({name: "support:kb", embedding: true})

on-ticket meta {agent: SupportAgent, on-event: "support:ticket"}
fn (event) {
  // Per-stream memory (needs event.stream-id, so created inside the handler)
  memory ::store/Map({name: `support:${event.stream-id}`, embedding: true})
  context ::store/search(kb, event.data.message, {limit: 5})
  history ::store/search(memory, event.data.message, {limit: 10})
  response generate-reply(support-agent, context, history, event.data.message)
  ::store/put(memory, Uuid(), {role: "assistant", content: response})
  send("support:response", {ticket-id: event.data.ticket-id, response: response})
}

on-feedback meta {agent: SupportAgent, on-event: "support:feedback"}
fn (event) {
  memory ::store/Map({name: `support:${event.stream-id}`, embedding: true})
  ::store/put(memory, Uuid(), {type: "feedback", rating: event.data.rating})
}

daily-review meta {agent: SupportAgent, schedule: "0 9 * * 1-5"}
fn () {
  summarize-yesterday(kb)
}

on-escalation meta {agent: SupportAgent, on-event: "support:escalate"}
fn (event) {
  ::hot::slack/post-message(support-agent.escalation-channel, `Needs help: ${event.data.reason}`)
}

This defines one agent with four handlers: two event-driven, one scheduled, and one for escalation. All share the support-agent instance for configuration and ::hot::store maps for memory.

Agent Metadata

The agent key in the type's metadata is a map with the following fields:

FieldRequiredDescription
nameNoDisplay name for the agent. Falls back to the type name (e.g., SupportAgent).
descriptionNoShort description for the agent. Falls back to the top-level doc metadata.
tagsNoList of strings for categorization and filtering in the Hot App.

The top-level doc metadata serves as the default description. If both doc and agent.description are present, agent.description takes priority in agent-specific contexts (the App dashboard, API responses).

Grouping Handlers

Functions declare membership in an agent via meta {agent: TypeName}. This works with all handler types:

Event Handlers

on-ticket meta {agent: SupportAgent, on-event: "support:ticket"}
fn (event) {
  process-ticket(event.data)
}

Scheduled Functions

daily-review meta {agent: SupportAgent, schedule: "0 9 * * 1-5"}
fn () {
  review-interactions()
}

Webhooks

on-stripe-payment meta {
  agent: BillingAgent,
  webhook: {service: "billing", path: "/stripe"}
}
fn (request: HttpRequest): HttpResponse {
  process-payment(request.body)
  HttpResponse({status: 200, body: {ok: true}})
}

The agent reference is a type name, not a string. The compiler resolves it to the agent type, catching typos at compile time. A single agent can have any number of handlers across event handlers, schedules, and webhooks.

Config Fields

The agent type's struct fields define its configuration. These are the per-deployment knobs — model selection, system prompts, channel names, thresholds.

SupportAgent meta {
  agent: {name: "Support Agent"}
}
type {
  model: Str
  system: Str
  escalation-channel: Str
  confidence-threshold: Dec
}

Create an instance using context variables for environment-specific values:

support-agent SupportAgent({
  model: ::ctx/get("support.model", "claude-sonnet")
  system: ::ctx/get("support.system", "You are a helpful support agent.")
  escalation-channel: ::ctx/get("support.escalation", "#support")
  confidence-threshold: Dec(::ctx/get("support.threshold", "0.85"))
})

Handlers reference the instance directly — support-agent.model, support-agent.escalation-channel. Config fields are visible in the Agent Dashboard's Overview tab.

Agent Runs

When a handler with meta {agent: TypeName} executes, the run is automatically tagged with the agent's qualified name (e.g., ::acme::support/SupportAgent). This tagging enables:

  • Filtering — view runs by agent in the Hot App
  • Metrics — per-agent success rate, average duration, and run count
  • Health monitoring — the Dashboard shows agent health with color-coded indicators
  • Attribution — trace any run back to the agent that produced it

No additional code is needed. The agent tagging happens at the runtime level when a handler declares meta {agent: ...}.

Agent Memory

Agents use ::hot::store for persistent memory. Store maps support optional embedding-based semantic search, which is useful for knowledge bases and conversation history.

Per-Stream Memory

Each stream can have its own memory, isolated by stream ID. Since the map name depends on the stream ID, create it inside the handler where event is available:

on-ticket meta {agent: SupportAgent, on-event: "support:ticket"}
fn (event) {
  memory ::store/Map({name: `support:${event.stream-id}`, embedding: true})

  history ::store/search(memory, event.data.message, {limit: 10})
  ::store/put(memory, Uuid(), {role: "user", content: event.data.message})

  response generate-reply(support-agent, history, event.data.message)
  ::store/put(memory, Uuid(), {role: "assistant", content: response})
}

Shared Knowledge Base

A knowledge base shared across all streams uses a static name, so the map definition goes at the namespace level. Handlers search it; a separate handler or schedule populates it:

kb ::store/Map({name: "support:kb", embedding: true})

on-ticket meta {agent: SupportAgent, on-event: "support:ticket"}
fn (event) {
  context ::store/search(kb, event.data.message, {limit: 5})
}

seed-kb meta {agent: SupportAgent, on-event: "kb:seed"}
fn (event) {
  ::store/put-many(kb, {
    "returns": {title: "Returns", content: "Refunds available within 30 days of purchase."}
    "shipping": {title: "Shipping", content: "Free shipping on orders over $50."}
  })
}

Plain Key-Value State

Not all agent state needs embeddings. Use plain maps for counters, flags, and structured data:

counters ::store/Map({name: "support:counters"})

on-ticket meta {agent: SupportAgent, on-event: "support:ticket"}
fn (event) {
  total or(::store/get(counters, "total-tickets"), 0)
  ::store/put(counters, "total-tickets", add(total, 1))
}

Lifecycle

Agents are metadata-driven — they're discovered from your source code and registered automatically:

  1. Define — Add agent metadata to a type and meta {agent: TypeName} to handler functions
  2. Deploy — Run hot deploy (or hot dev for local development). The compiler scans types for agent metadata and registers agent definitions.
  3. Execute — When events arrive, schedules fire, or webhooks receive requests, handlers run with agent attribution. Each run is tagged with the agent's qualified name.
  4. Observe — View agent health, metrics, handlers, and runs in the Hot App

When you redeploy, agent definitions are updated automatically. If a type's agent metadata is removed, the agent is unregistered. If handler functions remove their agent reference, those handlers still execute but are no longer attributed to the agent.

Viewing Agents in the App

The Hot App provides dedicated views for agents. See Hot App > Agents for details.

Agents List

The Agents page shows all deployed agents as a card grid. Each card displays the agent name, namespace, description, tags, handler count, and project. Use the search bar to filter by name, namespace, or project.

Agent Dashboard

Click an agent to open its dashboard with:

  • Hero metrics — Run count (24h), success rate, average duration, active streams
  • Overview tab — Config fields, full description
  • Handlers tab — All event handlers, schedules, and webhooks linked to this agent with trigger details, retry config, and source locations
  • Runs tab — Paginated run history filtered to this agent
  • Streams tab — Streams where this agent participated

Dashboard Health Widget

The main Dashboard includes an Agent Health widget showing each deployed agent with a health indicator:

  • Green dot — 95%+ success rate
  • Yellow dot — 80–95% success rate
  • Red dot — Below 80% success rate

The widget also shows agent vs. non-agent run counts, giving you a quick sense of how much of your workload is agent-driven.

Patterns

Event-Driven Agent

The most common pattern. The agent responds to external events:

InboxTriager meta {agent: {name: "Inbox Triager", tags: ["email"]}}
type { rules: Vec }

on-email meta {agent: InboxTriager, on-event: "email:received"}
fn (event) {
  classify-and-route(event.data)
}

Scheduled Agent

An agent that runs on a schedule:

DailyBriefing meta {agent: {name: "Daily Briefing", tags: ["reporting"]}}
type { sources: Vec, channel: Str }

briefing DailyBriefing({
  sources: ["github", "stripe", "analytics"]
  channel: ::ctx/get("briefing.channel", "#general")
})

morning-report meta {agent: DailyBriefing, schedule: "0 8 * * 1-5"}
fn () {
  data aggregate-sources(briefing.sources)
  summary generate-summary(data)
  send-to-channel(briefing.channel, summary)
}

Hybrid Agent

Combines events, schedules, and webhooks:

LeadQualifier meta {
  agent: {name: "Lead Qualifier", tags: ["sales", "ai"]}
}
type { model: Str, threshold: Dec, crm-key: Str }

qualifier LeadQualifier({
  model: ::ctx/get("leads.model", "claude-sonnet")
  threshold: Dec(::ctx/get("leads.threshold", "0.7"))
  crm-key: ::ctx/get("leads.crm-key")
})

on-signup meta {agent: LeadQualifier, webhook: {service: "leads", path: "/signup"}}
fn (request: HttpRequest): HttpResponse {
  send("lead:new", request.body)
  HttpResponse({status: 200, body: {ok: true}})
}

qualify-lead meta {agent: LeadQualifier, on-event: "lead:new"}
fn (event) {
  score enrich-and-score(event.data, qualifier.model)
  if(gte(score, qualifier.threshold),
    send("lead:qualified", merge(event.data, {score: score})),
    send("lead:nurture", merge(event.data, {score: score})))
}

weekly-pipeline meta {agent: LeadQualifier, schedule: "0 9 * * 1"}
fn () {
  generate-pipeline-report()
}

Best Practices

Name agents by domain responsibility. An agent should own a coherent area of functionality. SupportAgent, BillingAgent, and LeadQualifier are clear; UtilityAgent or MainAgent are not.

Keep handler count focused. Each agent should have a small number of handlers with a clear purpose. If an agent has more than 8–10 handlers, consider splitting it into separate agents.

Use config fields for tunable parameters. Model names, thresholds, channel names, and system prompts belong in config fields. This makes agents reusable across environments without code changes.

Use tags for categorization. Tags like ["support", "ai"] or ["billing", "webhook"] help organize agents in the App, especially as the number of deployed agents grows.

Use ::hot::store for agent memory. Enable embeddings when you need semantic search (conversation history, knowledge bases). Use plain maps for counters, state flags, and structured data.

Use streams for multi-step workflows. Send events with a stream_id to group related runs under a single stream. This gives you end-to-end visibility into agent workflows in the Streams view.

Prefer event-driven over polling. Use on-event handlers to react to changes rather than scheduled polling. Events are more efficient and produce clearer audit trails.