Skip to content

Audit Events

WasmAgent emits a structured, typed event for every significant action during an agent run. Every event is persisted to KV, resumable via SSE Last-Event-ID, and exportable to OpenTelemetry.


Event catalogue

All events share a common base:

ts
{
  traceId: string;        // unique ID for this agent run branch
  parentTraceId: string | null;  // parent agent's traceId (multi-agent fan-out)
  timestampMs: number;   // Unix timestamp in milliseconds
  event: string;         // discriminant — see table below
  channel: string;       // routing hint for UIs ("text" | "thinking" | "tool" | "model" | "status" | "action" | "artifact")
  data: { … };           // event-specific payload
}
eventchannelWhen emittedKey data fields
run_starttextAgent run beginstask, agentConfig (model, tools, maxSteps)
step_startthinkingEach reasoning step beginsstep
thinking_deltathinkingStreaming chain-of-thought tokensdelta, step
planningthinkingPlanner emits plan + factsstep, plan, facts
model_startmodelBefore each model.generate() callmodelId, step
model_donemodelAfter model stream endsmodelId, step, finishReason, inputTokens, outputTokens, thinkingTokens, cacheReadTokens, cacheHitRate, estimatedUsd, calls
tool_calltoolBefore a tool executestoolName, args, callId, batchId, batchSize, stepIndex
tool_resulttoolAfter a tool completescallId, toolName, output, error?, batchId, stepIndex
tool_fallback_offeredtoolA tool failed and alternatives existfailedTool, error, candidates, stepIndex
tool_synthesisedtoolAgent uses code-execution as a tool synthesis substratecodeToolName, callId, stepIndex
action_proposedactionAgent decides to take an actionactionId, type, path?, reason?
action_executingactionAction execution startsactionId, startedAtMs
action_completedactionAction finishesactionId, durationMs, success, error?
await_human_inputstatusHITL approval gate triggeredpromptId, prompt, step
guardrail_tripwirestatusA guardrail fired and blocked the agentguardrailName, layer (input/output/tool), toolName?, metadata?
goal_adaptation_proposedstatusGoalDirectedAgent proposes relaxing criteriakeepCriteria, relaxCriteria, droppedCriteria, iterationCount
handoffstatusControl transferred to another agenttargetAgentName, step
supervisor_decisionstatusSupervisor aborts or restarts the runaction (abort/restart), reason?, runCount
error_recoverystatusAgent classifies an error and chooses a recovery strategystrategy, errorType, attempt, maxAttempts, fixHint?
statusstatusTool execution phase markerphase, toolName?, callId?, step
final_answertextAgent produces its final answeranswer
errortextAgent terminates with an errorerror, step?
artifact_stream_startartifactStreaming file/component beginsartifactId, type, path?, label?
artifact_deltaartifactIncremental artifact chunkartifactId, delta, offset?
artifact_stream_endartifactArtifact fully receivedartifactId, contentHash, totalBytes

KV storage format

Events are stored under the key pattern:

evlog:<traceId>:<paddedSeq>
  • traceId — the agent run's unique identifier (UUID).
  • paddedSeq — 12-digit zero-padded integer (lexicographic sort = monotonic sort). This guarantees correct ordering with kv.list(prefix).
  • Value — JSON-serialised AgentEvent object.

Example key: evlog:a3b1c2d4-0000-0000-0000-000000000001:000000000042

Use kv.list("evlog:<traceId>:") to enumerate all events for a run in order.


Querying events

From TypeScript (server-side)

ts
import { EventLog } from "@wasmagent/core";
import { myKvBackend } from "./kv";

const log = new EventLog(myKvBackend);

// Replay all events for a run:
for await (const { eventId, event } of log.replay(traceId)) {
  console.log(eventId, event.event, event.data);
}

// Replay only events after a known ID (SSE resume):
const lastSeen = req.headers.get("Last-Event-ID");
for await (const { eventId, event } of log.replay(traceId, lastSeen)) {
  await sseWriter.write(formatSseFrame({ eventId, event }));
}

// High-water mark (last seen event ID for a trace):
const hwm = await log.highWaterMark(traceId);

// Clean up after a completed run:
await log.purge(traceId);

From the CLI (wrangler KV)

bash
# List all event keys for a trace
wrangler kv key list --binding=CHECKPOINTS_KV --prefix="evlog:<traceId>:"

# Read a specific event
wrangler kv key get --binding=CHECKPOINTS_KV "evlog:<traceId>:000000000042"

Tapping events during a live run

ts
import { EventLog, formatSseFrame } from "@wasmagent/core";

const log = new EventLog(kvBackend);

// agent.run() returns AsyncGenerator<AgentEvent>
for await (const logged of log.tap(agent.run(task), traceId)) {
  // logged.eventId — the assigned monotonic ID
  // logged.event   — the original AgentEvent
  await sseWriter.write(formatSseFrame(logged));
}

OpenTelemetry export

Map AgentEvent types to OTel GenAI semantic convention spans:

eventOTel mapping
model_startStart gen_ai.chat span; set gen_ai.system, gen_ai.request.model
model_doneEnd gen_ai.chat span; set gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, gen_ai.response.finish_reasons
tool_callStart gen_ai.tool child span; set gen_ai.tool.name, gen_ai.tool.call.id
tool_resultEnd gen_ai.tool span; set error.type if error is present
guardrail_tripwireEmit as a span event on the enclosing gen_ai.chat span; set guardrail.name, guardrail.layer
run_startStart top-level gen_ai.agent span; set gen_ai.agent.name, gen_ai.request.model
final_answer / errorEnd top-level gen_ai.agent span

Example OTel bridge (minimal):

ts
import { trace } from "@opentelemetry/api";

const tracer = trace.getTracer("wasmagent");

for await (const { eventId, event } of log.tap(agent.run(task), traceId)) {
  if (event.event === "model_start") {
    const span = tracer.startSpan("gen_ai.chat", {
      attributes: { "gen_ai.request.model": event.data.modelId },
    });
    activeSpans.set(event.traceId, span);
  } else if (event.event === "model_done") {
    const span = activeSpans.get(event.traceId);
    span?.setAttributes({
      "gen_ai.usage.input_tokens": event.data.inputTokens ?? 0,
      "gen_ai.usage.output_tokens": event.data.outputTokens ?? 0,
    });
    span?.end();
  }
  // … handle other events
}

Retention and redaction

  • Retention: call log.purge(traceId) after a completed run to delete all events. The EventLog does not auto-expire entries — set a TTL on the KV namespace or run a periodic cleanup job.
  • Redaction: the EventLog does not redact event payloads automatically. Wire a redactPostHook to strip sensitive content from tool outputs before they are emitted as tool_result events. For compliance deployments, filter data.answer in final_answer events before persisting to KV.
  • Access control: the KV namespace (CHECKPOINTS_KV) should be accessible only to the worker's service binding. Do not expose the namespace directly to external clients.

See also: packages/core/src/streaming/EventLog.ts, packages/core/src/types/events.ts.

Released under the Apache-2.0 License.