Examples
Raw SQL Agent Event Store
Persist Anvia agent stream events with a raw SQL adapter.
Use a raw SQL event store when your application talks directly to the database and needs replayable runtime events for streamed agent runs.
Event storage is for operational visibility: debugging bad answers, replaying tool behavior, inspecting nested agent activity, and linking a product run to trace metadata. It is not memory and should not be used as the conversation transcript for future prompts.
Scenario
A Node service uses Postgres through pg. The support agent streams to an internal operations surface, and every stream event is persisted under the same run id.
Table
CREATE TABLE agent_events (
id bigserial PRIMARY KEY,
run_id text NOT NULL,
agent_id text NOT NULL,
agent_name text,
turn integer,
tool_name text,
tool_call_id text,
internal_call_id text,
event jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX agent_events_run_idx
ON agent_events (run_id, created_at);
CREATE INDEX agent_events_agent_idx
ON agent_events (agent_id, created_at);
CREATE INDEX agent_events_tool_idx
ON agent_events (tool_name, created_at);
Expected Event Records
AgentEventStore.load(runId) returns Promise<AgentEventRecord[]>. Each record contains indexed fields and the original stream event payload.
[
{
"runId": "run_123",
"agentId": "support",
"agentName": "Support",
"turn": 1,
"event": {
"type": "turn_start",
"turn": 1,
"prompt": {
"role": "user",
"content": [{ "type": "text", "text": "Where is order A-100?" }]
},
"history": []
},
"createdAt": "2026-06-29T02:40:00.000Z"
},
{
"runId": "run_123",
"agentId": "support",
"agentName": "Support",
"turn": 1,
"event": {
"type": "text_delta",
"turn": 1,
"delta": "Checking"
},
"createdAt": "2026-06-29T02:40:01.000Z"
},
{
"runId": "run_123",
"agentId": "support",
"agentName": "Support",
"turn": 1,
"toolName": "lookup_order",
"toolCallId": "fc_1",
"internalCallId": "tool_internal_1",
"event": {
"type": "tool_result",
"turn": 1,
"toolName": "lookup_order",
"toolCallId": "fc_1",
"internalCallId": "tool_internal_1",
"args": "{\"orderId\":\"A-100\"}",
"result": "{\"status\":\"shipped\"}"
},
"createdAt": "2026-06-29T02:40:02.000Z"
},
{
"runId": "run_123",
"agentId": "support",
"agentName": "Support",
"event": {
"type": "final",
"runId": "run_123",
"output": "Order A-100 has shipped.",
"usage": {
"inputTokens": 120,
"outputTokens": 20,
"totalTokens": 140,
"cachedInputTokens": 0,
"cacheCreationInputTokens": 0
},
"messages": []
},
"createdAt": "2026-06-29T02:40:03.000Z"
}
]
Other event payloads can include reasoning_delta, tool_call, turn_end, agent_tool_event, and error. Store event as JSONB so those variants can be preserved without adding columns.
Event Store
import type { Pool } from "pg";
import type { AgentEventAppendInput, AgentEventRecord, AgentEventStore } from "@anvia/core/agent";
function toJsonSafeEvent(value: unknown): unknown {
return JSON.parse(
JSON.stringify(value, (_key, nested) => {
if (nested instanceof Error) {
return { name: nested.name, message: nested.message, stack: nested.stack };
}
return nested;
}),
);
}
function fromJson(value: unknown): unknown {
return typeof value === "string" ? JSON.parse(value) : value;
}
export class SqlAgentEventStore implements AgentEventStore {
constructor(private readonly pool: Pool) {}
async append(input: AgentEventAppendInput): Promise<void> {
await this.pool.query(
`INSERT INTO agent_events
(run_id, agent_id, agent_name, turn, tool_name, tool_call_id, internal_call_id, event)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb)`,
[
input.runId,
input.agentId,
input.agentName ?? null,
input.turn ?? null,
input.toolName ?? null,
input.toolCallId ?? null,
input.internalCallId ?? null,
JSON.stringify(toJsonSafeEvent(input.event)),
],
);
}
async load(runId: string): Promise<AgentEventRecord[]> {
const { rows } = await this.pool.query<{
run_id: string;
agent_id: string;
agent_name: string | null;
turn: number | null;
tool_name: string | null;
tool_call_id: string | null;
internal_call_id: string | null;
event: unknown;
created_at: Date;
}>(
`SELECT run_id, agent_id, agent_name, turn, tool_name, tool_call_id,
internal_call_id, event, created_at
FROM agent_events
WHERE run_id = $1
ORDER BY created_at ASC, id ASC`,
[runId],
);
return rows.map((row) => ({
runId: row.run_id,
agentId: row.agent_id,
agentName: row.agent_name ?? undefined,
turn: row.turn ?? undefined,
toolName: row.tool_name ?? undefined,
toolCallId: row.tool_call_id ?? undefined,
internalCallId: row.internal_call_id ?? undefined,
event: fromJson(row.event),
createdAt: row.created_at,
}));
}
async clear(runId: string): Promise<void> {
await this.pool.query("DELETE FROM agent_events WHERE run_id = $1", [runId]);
}
}
Use It
import { Pool } from "pg";
import { AgentBuilder } from "@anvia/core";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const eventStore = new SqlAgentEventStore(pool);
const agent = new AgentBuilder("support", model)
.tools(supportTools)
.eventStore(eventStore, { include: "all" })
.build();
let final;
for await (const event of agent.prompt("Where is order A-100?").stream()) {
if (event.type === "final") {
final = event;
}
}
if (final === undefined) {
throw new Error("Agent stream ended without a final event.");
}
const eventLog = await eventStore.load(final.runId);
Production Checks
- Drain the stream to persist events.
- Keep raw event logs internal unless product policy explicitly allows exposing them.
- Redact or expire events that include private prompts, history, tool arguments, tool results, reasoning, provider payloads, or errors.
- Use product tables for user-visible state and event logs for operational replay.
