Skip to main content
SONZAI

Events & Multi-Agent Dialogue

Trigger agent reactions to backend events and run conversations between two agents — for achievements, milestones, NPC interactions, or automated simulations.

Your backend knows things the agent doesn't: a user just levelled up, an order shipped, a milestone was hit. TriggerEvent lets you push those signals to an agent and get a tailored reaction — no user message required. Dialogue lets you orchestrate two agents talking to each other, turn by turn, so you can build NPC conversations, run evaluation simulations, or script automated specialist hand-offs.

Both primitives use the same enriched context pipeline as regular chat — the agent draws on memory, personality, and mood when it responds.

What you can build with it

Events

  • Level-up celebrations — your game backend detects a rank change and fires a level_up event; the agent congratulates the user in its own voice
  • Daily summaries — a cron job fires a daily_summary event with session stats in metadata; the agent writes a personalised recap
  • Achievement unlocks — trigger a proactive message the moment a user hits a milestone, so the agent's enthusiasm lands while the moment is fresh
  • External state changes — order shipped, appointment confirmed, subscription renewed; the agent reacts to your system events rather than waiting for the user to ask

Multi-Agent Dialogue

  • NPC interactions — two character AIs converse while the user watches; each agent stays in its own voice and draws on its own memory
  • Simulation runs — iterate N agents through scripted scenarios for offline evaluation without a real user in the loop
  • Specialist hand-offs — agent A poses a question to agent B and incorporates the answer before responding to the user

Quickstart — Trigger an event

Fire a level_up event with structured metadata. The agent generates a reaction and the platform queues it for delivery through the same channels as other proactive messages.

import { Sonzai } from "@sonzai-labs/agents";

const client = new Sonzai({ apiKey: process.env.SONZAI_API_KEY! });

const result = await client.agents.triggerBackendEvent("agent_abc", {
userId: "user_123",
eventType: "level_up",
eventDescription: "The user just reached level 25 — a major milestone in the game.",
metadata: {
  new_level: "25",
  previous_level: "24",
  xp_total: "12500",
},
});

console.log(result.accepted); // true
console.log(result.event_id); // "evt_01HX..."

Quickstart — Run a dialogue

Dialogue is a per-agent call. To run a conversation between two agents, you orchestrate turns yourself: call agent A, append its response to the message history, call agent B with that updated history, and so on. Each agent independently draws on its own memory and personality.

import { Sonzai } from "@sonzai-labs/agents";

const client = new Sonzai({ apiKey: process.env.SONZAI_API_KEY! });

// Seed the conversation — agent_b opens with the first message
const messages = [
{ role: "user" as const, content: "Tell me something interesting about the ancient ruins." },
];

// Turn 1 — agent_a responds
const turnA = await client.agents.dialogue("agent_a", {
userId: "user_123",
messages,
sceneGuidance: "Two NPCs are exploring ancient ruins together. Keep responses under 3 sentences.",
});

messages.push({ role: "assistant", content: turnA.response });

// Turn 2 — agent_b responds to what agent_a said
const turnB = await client.agents.dialogue("agent_b", {
userId: "user_123",
messages,
sceneGuidance: "Two NPCs are exploring ancient ruins together. Keep responses under 3 sentences.",
});

console.log("Agent A:", turnA.response);
console.log("Agent B:", turnB.response);

Core concepts

Events

EventType is free-form. There is no fixed enum. Common conventions used by tenants: "achievement", "daily_summary", "level_up", "order_shipped", "appointment_confirmed", "milestone". Pick names that are meaningful in your domain and stay consistent across your backend.

EventDescription is for the LLM. Write it as plain-English narration: "The user just cleared chapter 5 for the first time after 3 failed attempts." The agent's underlying model reads this and uses it to shape the reaction — be specific rather than terse.

Metadata is string-only. The metadata map accepts string → string pairs only. For nested or numeric data, either serialize into the event_description or flatten it with explicit keys ("xp_gained", "xp_total", "level_before", "level_after").

Messages field grounds the event in a prior conversation. If the event is closely tied to a conversation that just ended (for example, a daily_summary fired after a chat session), pass the recent messages. The platform uses them directly for context-sensitive generation — diary entries, summaries — instead of relying on lossy consolidation. Omit this field for cron-driven events that have no associated conversation.

TriggerEventResponse contains two fields:

  • accepted (bool) — whether the platform accepted the event for processing
  • event_id (string) — an opaque identifier for the queued event; store it if you want to correlate platform logs

Dialogue

Each call is per-agent. The dialogue method is scoped to a single agent: you pass an agentId and the current message history. To model a conversation between two agents, you manage the turn loop — append each response to the shared messages slice and alternate which agentId you call.

Messages carry the full context. Unlike chat, which manages conversation history server-side per session, dialogue expects you to pass the full message thread with every call. You control the window.

sceneGuidance steers both tone and constraints. Pass a brief instruction describing the scene and any constraints ("keep responses under 3 sentences", "the agents are rivals", "agent_a does not know about the treasure") so both sides stay in character.

requestType signals the call's purpose. An optional free-form tag ("npc_scene", "eval_round", "specialist_consult") that downstream analytics can use for filtering. Has no effect on generation.

DialogueResponse contains:

  • response (string) — the agent's generated text for this turn
  • side_effects — optional structured metadata emitted by the agent (tool calls, mood signals, etc.)

Full API

MethodReturnsDescription
triggerBackendEvent(agentId, opts) · trigger_backend_event(agent_id, ...) · TriggerEvent(ctx, agentID, opts)TriggerEventResponseFire a backend event and queue an agent reaction
dialogue(agentId, opts) · dialogue(agent_id, ...) · Dialogue(ctx, agentID, opts)DialogueResponseGenerate one turn of agent dialogue

TriggerEventOptions / trigger_backend_event kwargs:

FieldTypeRequiredDescription
userId / user_id / UserIDstringYesThe user this event belongs to
eventType / event_type / EventTypestringYesFree-form event name, e.g. "level_up"
eventDescription / event_description / EventDescriptionstringNoPlain-English narration for the LLM
metadata / MetadataRecord<string,string> / dict[str,str] / map[string]stringNoStructured string-only metadata
language / LanguagestringNoLocale override (e.g. "ja")
instanceId / instance_id / InstanceIDstringNoInstance scope
messages / MessagesChatMessage[]NoRecent conversation that triggered this event

DialogueOptions / dialogue kwargs:

FieldTypeRequiredDescription
userId / user_id / UserIDstringNoUser context for the agent
messages / MessagesChatMessage[]NoFull conversation history for this turn
sceneGuidance / scene_guidance / SceneGuidancestringNoInstruction scoping tone and constraints
requestType / request_type / RequestTypestringNoTag for analytics (e.g. "eval_round")
instanceId / instance_id / InstanceIDstringNoInstance scope

Combines with other features

With Proactive Messaging — events as the dev-controlled push source

Proactive Messaging has three sources: Scheduled Reminders (recurring cadence), Wakeups (one-off timed), and TriggerEvent (your backend fires it when something happens). TriggerEvent is the push-based source you control directly — no schedule required, no timer running. When the event is accepted, the platform routes the generated reaction through the same delivery channels as the other two sources: SSE if the user has an active stream, the polling notifications API, or your registered webhook.

// Proactive triangle in code form:

// Source 1 — recurring schedule (time-based)
await client.schedules.create("agent_abc", "user_123", {
  cadence: { simple: { frequency: "daily", times: ["09:00"] }, timezone: "Asia/Tokyo" },
  intent: "morning check-in",
  check_type: "reminder",
});

// Source 2 — one-off wakeup (time-based)
await client.agents.scheduleWakeup("agent_abc", {
  user_id: "user_123",
  check_type: "appointment_reminder",
  intent: "remind the user about their dentist appointment",
  delay_hours: 2,
});

// Source 3 — TriggerEvent (you push it when something happens)
await client.agents.triggerBackendEvent("agent_abc", {
  userId: "user_123",
  eventType: "appointment_confirmed",
  eventDescription: "The user just confirmed their 3pm dentist appointment for tomorrow.",
});

With Conversations — Messages field grounds the event in context

When a TriggerEvent fires immediately after a chat session — for example, a daily_summary event at session end — pass the recent conversation messages in the messages field. The platform uses them directly as conversation history for context-sensitive generation (diary entries, personality updates) instead of relying on condensed consolidation summaries. The agent's reaction then references what was actually said rather than a lossy reconstruction.

// After a chat session ends, fire a daily_summary event with the full message history
const sessionMessages = [
  { role: "user", content: "I finally finished that project I was stressing about." },
  { role: "assistant", content: "That's huge! You've been working on that for weeks." },
  { role: "user", content: "Yeah. Feels good. Think I'll take the evening off." },
];

await client.agents.triggerBackendEvent("agent_abc", {
  userId: "user_123",
  eventType: "daily_summary",
  eventDescription: "Session ended. User shared a work win and plans to rest.",
  messages: sessionMessages, // grounds the summary in what was actually said
});

With Evaluation — Dialogue as a scoring harness

Run a judge agent and a subject agent in a dialogue loop to score the subject's responses without a real user. The judge poses questions, the subject answers, and you feed both transcripts to your evaluation rubric. This lets you evaluate agent quality at scale offline.

import { Sonzai } from "@sonzai-labs/agents";

const client = new Sonzai({ apiKey: process.env.SONZAI_API_KEY! });

const JUDGE_AGENT    = "agent_judge";
const SUBJECT_AGENT  = "agent_subject";
const USER_ID        = "eval_run_001";

const messages = [
  { role: "user" as const, content: "I'm feeling really overwhelmed lately." },
];

// Subject responds to the user prompt
const subjectTurn = await client.agents.dialogue(SUBJECT_AGENT, {
  userId: USER_ID,
  messages,
  requestType: "eval_round",
});

messages.push({ role: "assistant", content: subjectTurn.response });

// Judge scores the subject's response
const judgeTurn = await client.agents.dialogue(JUDGE_AGENT, {
  userId: USER_ID,
  messages,
  sceneGuidance:
    "You are evaluating the previous assistant response for empathy and clarity. " +
    "Return a JSON object with keys: score (0–100), feedback (string).",
  requestType: "eval_judge",
});

console.log("Subject:", subjectTurn.response);
console.log("Judge verdict:", judgeTurn.response);

// Then score the exchange through the evaluation API
const evalResult = await client.agents.evaluate(SUBJECT_AGENT, {
  templateId: "empathy-rubric",
  messages,
});
console.log("Eval score:", evalResult.score);

Tutorials


Next steps

On this page