Event Types Reference
Complete reference for all event types in Nexus, organized by domain.
Core Events
| Event Type | Payload | Description |
|---|---|---|
core.boot | BootConfig | Engine boot started |
core.ready | (none) | All plugins initialized and ready |
core.shutdown | ShutdownReason | Engine shutting down |
core.tick | TickInfo | Periodic heartbeat |
core.error | ErrorInfo | Error reported by a plugin |
core.config.reload.request | ConfigReloadRequest | External trigger asks the engine to re-read its config |
core.config.reload.result | ConfigReloadResult | Engine response to a reload request (success / error) |
Payloads
BootConfig
| Field | Type | Description |
|---|---|---|
ConfigPath | string | Path to the config file |
Profile | string | Profile name |
ShutdownReason
| Field | Type | Description |
|---|---|---|
Reason | string | Why: "user", "error", or "signal" |
Error | error | Associated error (if any) |
TickInfo
| Field | Type | Description |
|---|---|---|
Sequence | int | Tick counter |
Time | time.Time | When the tick occurred |
ErrorInfo
| Field | Type | Description |
|---|---|---|
Source | string | Plugin that reported the error |
Err | error | The error |
Fatal | bool | Whether this should trigger shutdown |
Retryable | bool | Whether this error class is retryable (429, 5xx) |
RetriesExhausted | bool | Provider’s own retry logic gave up |
RequestMeta | map[string]any | Echo of LLMRequest.Metadata for correlation |
core.error is vetoable — providers emit before:core.error first. The fallback plugin can veto the error to suppress it and retry with an alternate provider.
I/O Events
| Event Type | Payload | Description |
|---|---|---|
io.input | UserInput | User submitted a message |
io.output | AgentOutput | Agent produced output |
io.output.stream | OutputChunk | Streaming output chunk |
io.output.stream.end | StreamRef | Streaming complete |
io.output.clear | (none) | Clear partial streamed content (used by fallback) |
io.status | StatusUpdate | Agent state changed |
io.approval.request | ApprovalRequest | Approval needed for an action |
io.approval.response | ApprovalResponse | User responded to approval |
io.ask | AskUser | Agent asking user a question |
io.ask.response | AskUserResponse | User answered the question |
io.history.replay | HistoryReplay | Replaying conversation on resume |
io.session.start | SessionInfo | Session started |
io.session.end | (none) | Session ended |
Payloads
UserInput
| Field | Type | Description |
|---|---|---|
Content | string | Message text |
Files | []FileAttachment | Attached files |
SessionID | string | Current session |
FileAttachment
| Field | Type | Description |
|---|---|---|
Name | string | Filename |
MimeType | string | MIME type |
Data | []byte | File contents |
AgentOutput
| Field | Type | Description |
|---|---|---|
Content | string | Output text |
Role | string | "assistant", "system", or "tool" |
Metadata | map[string]any | Additional context |
TurnID | string | Associated turn |
OutputChunk
| Field | Type | Description |
|---|---|---|
Content | string | Chunk text |
TurnID | string | Associated turn |
Index | int | Chunk sequence number |
StatusUpdate
| Field | Type | Description |
|---|---|---|
State | string | "idle", "thinking", "tool_running", "streaming" |
Detail | string | Additional info |
ToolID | string | Tool being run (if applicable) |
ApprovalRequest
| Field | Type | Description |
|---|---|---|
PromptID | string | Unique identifier for this approval |
Description | string | What needs approval |
ToolCall | any | The tool call details |
Risk | string | "low", "medium", "high" |
ApprovalResponse
| Field | Type | Description |
|---|---|---|
PromptID | string | Matches the request |
Approved | bool | Whether approved |
Always | bool | Remember this decision |
AskUser
| Field | Type | Description |
|---|---|---|
PromptID | string | Unique identifier |
Question | string | The question text |
TurnID | string | Associated turn |
AskUserResponse
| Field | Type | Description |
|---|---|---|
PromptID | string | Matches the request |
Answer | string | User’s response |
Provider Events
| Event Type | Payload | Description |
|---|---|---|
provider.fallback | ProviderFallback | Provider switch due to failure |
provider.fanout.start | ProviderFanoutStart | Parallel fanout initiated |
provider.fanout.response | ProviderFanoutResponse | Individual provider responded |
provider.fanout.complete | ProviderFanoutComplete | All fanout responses collected |
Payloads
ProviderFallback
| Field | Type | Description |
|---|---|---|
Role | string | Model role being resolved |
FailedProvider | string | Plugin ID of the provider that failed |
FailedModel | string | Model that failed |
Error | string | Error description |
NextProvider | string | Plugin ID of the fallback provider |
NextModel | string | Model being tried next |
Attempt | int | 0-based index in the fallback chain |
ProviderFanoutStart
| Field | Type | Description |
|---|---|---|
FanoutID | string | Unique fanout sequence identifier |
Role | string | Model role being resolved |
Strategy | string | Selection strategy (all, llm_judge, heuristic, user) |
Targets | []ProviderFanoutTarget | Providers being dispatched to |
ProviderFanoutTarget
| Field | Type | Description |
|---|---|---|
Provider | string | Plugin ID |
Model | string | Model identifier |
ProviderFanoutResponse
| Field | Type | Description |
|---|---|---|
FanoutID | string | Fanout sequence identifier |
Provider | string | Plugin ID that responded |
Model | string | Model that responded |
Success | bool | false if provider errored or timed out |
Error | string | Non-empty on failure |
ProviderFanoutComplete
| Field | Type | Description |
|---|---|---|
FanoutID | string | Fanout sequence identifier |
Role | string | Model role |
Strategy | string | Selection strategy used |
Succeeded | int | Number of successful responses |
Failed | int | Number of errors or timeouts |
LLM Events
| Event Type | Payload | Description |
|---|---|---|
llm.request | LLMRequest | Request to an LLM provider |
llm.response | LLMResponse | Complete LLM response |
llm.stream.chunk | StreamChunk | Streaming response chunk |
llm.stream.end | StreamEnd | Streaming complete |
Payloads
LLMRequest
| Field | Type | Description |
|---|---|---|
Role | string | Model role name (resolved by provider) |
Model | string | Explicit model ID (optional, overrides role) |
Messages | []Message | Conversation messages |
Tools | []ToolDef | Available tools |
ToolChoice | *ToolChoice | Tool choice constraint (nil = provider default) |
ToolFilter | *ToolFilter | Tool include/exclude filter (nil = no filtering) |
ResponseFormat | *ResponseFormat | Structured output constraint (nil = no constraint) |
MaxTokens | int | Max response tokens |
Temperature | *float64 | Sampling temperature (nil = provider default) |
Stream | bool | Enable streaming |
Metadata | map[string]any | Additional context (e.g., _source for planner tagging) |
Message
| Field | Type | Description |
|---|---|---|
Role | string | "system", "user", "assistant", "tool" |
Content | string | Message text |
ToolCallID | string | For tool role: which call this responds to |
ToolCalls | []ToolCallRequest | For assistant role: tool calls made |
ToolCallRequest
| Field | Type | Description |
|---|---|---|
ID | string | Unique call identifier |
Name | string | Tool name |
Arguments | string | JSON-encoded arguments |
ToolDef
| Field | Type | Description |
|---|---|---|
Name | string | Tool name |
Description | string | What the tool does |
Parameters | string | JSON Schema for parameters |
ToolChoice
| Field | Type | Description |
|---|---|---|
Mode | string | "auto", "required", "none", "tool" |
Name | string | Tool name (only when Mode is "tool") |
ToolFilter
| Field | Type | Description |
|---|---|---|
Include | []string | Only these tools (empty = all). Takes precedence over Exclude |
Exclude | []string | Remove these tools |
ResponseFormat
| Field | Type | Description |
|---|---|---|
Type | string | "text", "json_object", or "json_schema" |
Name | string | Schema name (required by OpenAI for json_schema type) |
Schema | map[string]any | JSON Schema definition |
Strict | bool | Enforce strict schema adherence |
Metadata Conventions
Certain metadata keys carry special meaning:
| Key | On | Type | Description |
|---|---|---|---|
_source | LLMRequest / LLMResponse | string | Correlates retry requests with responses (used by gates) |
_expects_schema | LLMRequest | string | Schema name to attach via the schema registry |
_structured_output | LLMResponse | bool | true when provider enforced structured output (native or simulated) |
LLMResponse
| Field | Type | Description |
|---|---|---|
Content | string | Response text |
ToolCalls | []ToolCallRequest | Tool calls in the response |
Usage | Usage | Token usage statistics |
CostUSD | float64 | Provider-computed cost in USD for this request |
Model | string | Model that was used |
FinishReason | string | Why the response ended |
Metadata | map[string]any | Additional context |
Alternatives | []LLMResponse | Additional responses from parallel fanout providers (nil for non-fanout) |
Usage
| Field | Type | Description |
|---|---|---|
PromptTokens | int | Input tokens consumed |
CompletionTokens | int | Output tokens generated |
TotalTokens | int | Total tokens |
ReasoningTokens | int | Thinking / reasoning tokens (Gemini 2.5 thoughtTokenCount, etc.) |
CachedTokens | int | Tokens served from a prompt cache |
CacheWriteTokens | int | Tokens written into a prompt cache |
ModalityBreakdown | map[string]int | Per-modality token counts when the provider reports them. Lowercase keys: text, image, audio, video, document. Empty when the provider does not split modalities (Anthropic) or the request was text-only. Cost-attribution consumers read this to bill image / audio turns separately. |
Multimodal tool results
ToolResult carries an OutputParts []MessagePart field for tools that
emit multimodal content (image, audio, document, video). When non-empty,
memory plugins copy the parts onto the resulting tool-role
Message.Parts so the next LLM request includes the multimodal content
alongside (or in place of) Output.
Large payloads should be stored via pkg/engine/blobs and referenced via
MessagePart.URI = "nexus-blob:<sha256>" so the journal stays compact;
small payloads can ride inline as MessagePart.Data. Provider plugins
resolve nexus-blob: URIs at request-assembly time.
StreamChunk
| Field | Type | Description |
|---|---|---|
Content | string | Chunk text |
ToolCall | *ToolCallRequest | Partial tool call (if applicable) |
Index | int | Chunk sequence number |
TurnID | string | Associated turn |
StreamEnd
| Field | Type | Description |
|---|---|---|
TurnID | string | Associated turn |
Usage | Usage | Final token usage |
FinishReason | string | Why the stream ended |
Tool Events
| Event Type | Payload | Description |
|---|---|---|
tool.register | ToolDef | Tool available for use |
before:tool.invoke | ToolCall | Before tool execution (vetoable) |
tool.invoke | ToolCall | Tool invocation |
before:tool.result | ToolResult | Before tool result propagation (vetoable) |
tool.result | ToolResult | Tool execution result |
Payloads
ToolCall
| Field | Type | Description |
|---|---|---|
ID | string | Call identifier |
Name | string | Tool name |
Arguments | map[string]any | Parsed arguments |
TurnID | string | Associated turn |
ToolResult
| Field | Type | Description |
|---|---|---|
ID | string | Matches the call ID |
Name | string | Tool name |
Output | string | Human-readable result text (what the LLM sees) |
Error | string | Error message (if failed) |
OutputFile | string | Path to output file (optional) |
OutputData | []byte | Binary output data (optional) |
OutputStructured | map[string]any | Optional structured payload. When the tool’s ToolDef.OutputSchema is set, this should match that schema. Consumed by typed consumers like run_code’s bindings — Output stays freeform text for the LLM. |
TurnID | string | Associated turn |
ToolDef
| Field | Type | Description |
|---|---|---|
Name | string | Tool name the LLM will use |
Description | string | Description shown to the LLM |
Parameters | map[string]any | JSON Schema for inputs |
OutputSchema | map[string]any | Optional JSON Schema describing the shape of ToolResult.OutputStructured. When set, run_code generates a typed Go struct bound to this schema. |
Class / Subclass / Tags | string / []string | Semantic metadata for filtering |
Code Execution Events
Emitted by nexus.tool.code_exec alongside the standard tool.invoke/tool.result pair. Let other plugins observe programmatic tool-calling scripts without recomputing the run_code envelope from a tool result.
| Event Type | Payload | Description |
|---|---|---|
code.exec.request | CodeExecRequest | Script about to run; carries source, imports, active skills |
code.exec.stdout | CodeExecStdout | Incremental stdout chunk produced while the script runs; Final=true marks the last chunk |
code.exec.result | CodeExecResult | Script finished (success, compile error, runtime error, veto, or timeout) |
CodeExecRequest
| Field | Type | Description |
|---|---|---|
CallID | string | Matches the outer run_code tool call ID |
TurnID | string | Associated turn |
Script | string | Full Go source submitted by the LLM |
Imports | []string | Import paths referenced by the script |
Skills | []string | Names of currently-active skills whose helpers were staged |
CodeExecStdout
| Field | Type | Description |
|---|---|---|
CallID | string | Matches the outer run_code tool call ID |
TurnID | string | Associated turn |
Chunk | string | UTF-8 bytes written to stdout since the last emission; may contain newlines |
Final | bool | True for the closing chunk of this call; code.exec.result follows immediately after |
Truncated | bool | Only meaningful on the final chunk — true when total stdout exceeded max_output_bytes |
Chunks are flushed on every newline and on a ~512-byte threshold so long lines without newlines still reach the UI promptly. Consumers should concatenate Chunk values across events to reconstruct the full stdout stream; the final CodeExecResult.Output also carries the full aggregated string for non-streaming consumers.
CodeExecResult
| Field | Type | Description |
|---|---|---|
CallID | string | Matches the outer run_code tool call ID |
TurnID | string | Associated turn |
Output | string | Captured stdout (capped at max_output_bytes) |
Result | string | JSON-marshaled Run() return value |
Error | string | First error encountered (AST rejection, compile, runtime, timeout) |
Duration | int64 | Execution wall time in milliseconds |
Truncated | bool | Stdout was truncated by the output cap |
Agent Events
| Event Type | Payload | Description |
|---|---|---|
agent.turn.start | TurnInfo | Agent began processing |
agent.turn.end | TurnInfo | Agent finished processing |
agent.plan | Plan | Agent’s current plan |
agent.tool_choice | AgentToolChoice | Dynamic tool choice override |
Payloads
TurnInfo
| Field | Type | Description |
|---|---|---|
TurnID | string | Unique turn identifier |
Iteration | int | Current iteration count |
SessionID | string | Current session |
Plan
| Field | Type | Description |
|---|---|---|
Steps | []PlanStep | Plan steps |
TurnID | string | Associated turn |
PlanStep
| Field | Type | Description |
|---|---|---|
Description | string | What this step does |
Status | string | "pending", "active", "completed", "failed" |
AgentToolChoice
| Field | Type | Description |
|---|---|---|
Mode | string | "auto", "required", "none", "tool" |
ToolName | string | Tool name when Mode is "tool" |
Duration | string | "once" (next request only) or "sticky" (until replaced) |
Subagent Events
| Event Type | Payload | Description |
|---|---|---|
subagent.spawn | SubagentSpawn | Subagent creation requested |
subagent.started | SubagentStarted | Subagent began execution |
subagent.iteration | SubagentIteration | Subagent completed an iteration |
subagent.complete | SubagentComplete | Subagent finished |
Payloads
SubagentSpawn
| Field | Type | Description |
|---|---|---|
SpawnID | string | Unique spawn identifier |
Task | string | Task description |
SystemPrompt | string | Override system prompt |
Tools | []string | Available tools |
ModelRole | string | Model role |
ParentTurnID | string | Parent’s turn ID |
SubagentComplete
| Field | Type | Description |
|---|---|---|
SpawnID | string | Spawn identifier |
Result | string | Final result |
Error | string | Error (if failed) |
Iterations | int | Number of iterations used |
TokensUsed | Usage | Token consumption |
CostUSD | float64 | Accumulated cost in USD |
ParentTurnID | string | Parent’s turn ID |
Memory Events
| Event Type | Payload | Description |
|---|---|---|
memory.store | MemoryEntry | Store a memory entry |
memory.query | MemoryQuery | Query conversation history |
memory.result | MemoryResult | Query results |
memory.compaction.triggered | CompactionTriggered | Compaction started |
memory.compacted | CompactionComplete | Compaction finished |
Payloads
MemoryEntry
| Field | Type | Description |
|---|---|---|
Key | string | Entry identifier |
Content | string | Content to store |
Metadata | map[string]any | Additional context |
SessionID | string | Current session |
MemoryQuery
| Field | Type | Description |
|---|---|---|
Query | string | Search query (empty = all) |
Limit | int | Max results |
SessionID | string | Current session |
CompactionTriggered
| Field | Type | Description |
|---|---|---|
Reason | string | Why compaction triggered |
MessageCount | int | Messages before compaction |
BackupPath | string | Backup file location |
CompactionComplete
| Field | Type | Description |
|---|---|---|
Messages | []Message | New compacted message set |
BackupPath | string | Backup file location |
MessageCount | int | Messages after compaction |
PrevCount | int | Messages before compaction |
Long-Term Memory Events
| Event Type | Payload | Description |
|---|---|---|
memory.longterm.loaded | LongTermMemoryLoaded | Memory index injected into system prompt |
memory.longterm.store | LongTermMemoryStoreRequest | Write or update a memory entry |
memory.longterm.stored | LongTermMemoryStored | Write confirmed |
memory.longterm.read | LongTermMemoryReadRequest | Read a memory entry |
memory.longterm.result | LongTermMemoryReadResult | Read result |
memory.longterm.delete | LongTermMemoryDeleteRequest | Delete a memory entry |
memory.longterm.deleted | LongTermMemoryDeleted | Delete confirmed |
memory.longterm.list | LongTermMemoryQuery | List/filter memories |
memory.longterm.list.result | LongTermMemoryListResult | List result |
LongTermMemoryIndex
| Field | Type | Description |
|---|---|---|
Key | string | Memory key |
Preview | string | First line of content |
Tags | map[string]string | Key-value tags |
Updated | time.Time | Last update timestamp |
LongTermMemoryStoreRequest
| Field | Type | Description |
|---|---|---|
Key | string | Memory key |
Content | string | Markdown content |
Tags | map[string]string | Optional tags |
Plan Events
| Event Type | Payload | Description |
|---|---|---|
plan.request | PlanRequest | Request plan generation |
plan.result | PlanResult | Plan generated |
plan.created | PlanResult | Plan ready for display |
plan.approval.request | (plan data) | User approval needed |
plan.approval.response | (approval) | User responded |
plan.progress | PlanProgress | Step status updated |
Payloads
PlanRequest
| Field | Type | Description |
|---|---|---|
TurnID | string | Associated turn |
SessionID | string | Current session |
Input | string | User’s original input |
PlanResult
| Field | Type | Description |
|---|---|---|
TurnID | string | Associated turn |
PlanID | string | Unique plan identifier |
Steps | []PlanResultStep | Plan steps |
Summary | string | Plan summary |
Approved | bool | Whether approved |
Source | string | "dynamic" or "static" |
PlanResultStep
| Field | Type | Description |
|---|---|---|
ID | string | Step identifier |
Description | string | What this step does |
Instructions | string | Detailed instructions (optional) |
Status | string | Current status |
Order | int | Execution order |
PlanProgress
| Field | Type | Description |
|---|---|---|
TurnID | string | Associated turn |
PlanID | string | Plan identifier |
StepID | string | Step being updated |
Status | string | New status |
Detail | string | Additional info |
Skill Events
| Event Type | Payload | Description |
|---|---|---|
skill.discover | SkillCatalog | Skills catalog assembled |
skill.activate | SkillActivation | Skill activation requested |
before:skill.activate | SkillActivation | Before activation (vetoable) |
skill.loaded | SkillContent | Skill content loaded |
skill.deactivate | SkillRef | Skill deactivation requested |
skill.resource.read | SkillResourceReq | Resource file requested |
skill.resource.result | SkillResourceData | Resource content returned |
Payloads
SkillCatalog
| Field | Type | Description |
|---|---|---|
Skills | []SkillSummary | All discovered skills |
SkillSummary
| Field | Type | Description |
|---|---|---|
Name | string | Skill name |
Description | string | What the skill does |
Location | string | Directory path |
Scope | string | "project", "user", "builtin", "config" |
SkillContent
| Field | Type | Description |
|---|---|---|
Name | string | Skill name |
Body | string | Markdown content |
Resources | []string | Available resource files |
Scope | string | Skill scope |
BaseDir | string | Skill directory |
Schema Events
| Event Type | Payload | Description |
|---|---|---|
schema.register | SchemaRegistration | Register an output schema with the registry |
schema.deregister | SchemaDeregistration | Remove a schema from the registry |
Payloads
SchemaRegistration
| Field | Type | Description |
|---|---|---|
Name | string | Schema name (e.g. "skill.code_review.output") |
Schema | map[string]any | JSON Schema definition |
Source | string | Plugin ID that registered it |
SchemaDeregistration
| Field | Type | Description |
|---|---|---|
Name | string | Schema name to remove |
Source | string | Plugin ID that registered it |
Session Events
| Event Type | Payload | Description |
|---|---|---|
session.file.created | map (see below) | A file appeared in the session tree |
session.file.updated | map (see below) | An existing file in the session tree changed |
session.snapshot.request | SessionSnapshotRequest | Ask the engine to snapshot the session tree to the object store |
session.snapshot.result | SessionSnapshotResult | Outcome of one whole-tree snapshot |
session.owner.conflict | SessionOwnerConflict | A second host appears to hold this session — detection only, nothing is refused |
session.storage.degraded | SessionStorageDegraded | The object store stopped accepting this session’s state; the engine is running against the local working copy and retrying |
session.storage.recovered | SessionStorageRecovered | The backlog drained and the session is durably stored again |
before:session.tag.set | SessionTagSetRequest (vetoable) | Request to write one key/value pair into SessionMeta.Labels |
before:session.tag.delete | SessionTagDeleteRequest (vetoable) | Request to remove one key from SessionMeta.Labels |
session.tag.set | SessionTagSet | A session label was written, by either the general (bus) path or the reserved (direct-Go-call) path |
session.tag.deleted | SessionTagDeleted | A session label was removed, by either path |
The five object-store rows exist only when core.object_store.backend names a
backend. With none configured — the default — nothing subscribes, a
session.snapshot.request is inert, and none of the other four is ever
emitted. See Object Storage. The four session
tag events are independent of the object store and always active — see
Session Tag Events below.
session.file.created / session.file.updated payload
Emitted as a map[string]any, not as the events.SessionFile struct, because every
subscriber type-asserts a map. The struct is nonetheless the definition of the shape:
the payload is built by events.SessionFile.Map, so renaming or retyping a field
trips make check-events, and adding one without a wire key fails its own test.
Before that the struct was declared and unused, and this payload was guarded by
nothing at all.
| Key | Type | Description |
|---|---|---|
_schema_version | int | events.SessionFileVersion — 2. A payload journaled before the struct became the definition has no such key, which the v0 == v1 rule in pkg/events/compat covers; the other five keys are unchanged, so no migrator is registered |
session_id | string | Session the file belongs to |
path | string | Path relative to the session root, always slash-separated |
size | int | Size of the file in bytes after the write — not the size of the change |
offset | int | Byte offset at which the change begins |
bytes_added | int | Number of bytes written at offset |
The three counts are Go int, not int64. That is the type that has always been on
the wire and subscribers assert it directly, so widening the struct field would turn
every one of those assertions into a silent zero.
There is deliberately no action key. The action is the event type, and a copy of it
in the payload is a second source of truth that can disagree with the first — which is
what nexus.tool.pdf did, reporting every update as a creation.
Which event fires is decided by whether the path existed before the write, so a
subscriber can rely on seeing exactly one created per path per session.
Who emits it. SessionWorkspace.WriteFile, AppendFile and SaveMeta, plus the
two announcement helpers AnnounceWrite / AnnounceAppend that writers holding their
own os.* call use — today nexus.scene (its state file and patch journal),
nexus.workflows.icm (stage artifacts and copied inputs), nexus.tool.fileio
(write_file) and the desktop matcher plugin. Every one of them publishes the
session-relative path, so it can be used directly as an object key. nexus.tool.pdf used to emit a second, hand-built
event beside the one WriteFile already produced, carrying the bare basename and no
delta; it was removed rather than reconciled, and the plugin no longer emits
session.file.* at all.
Writers that deliberately stay silent — the journal, the tool cache, the blob store, per-plugin SQLite and the session lock among them — each have a recorded reason. See Sessions.
The append-aware delta. offset and bytes_added together say that the region
[offset, offset+bytes_added) is the only part of the object that changed:
| Shape | Meaning |
|---|---|
offset == 0 && bytes_added == size | The whole object is new. Every WriteFile, and the first append that creates a file. |
offset > 0 && offset + bytes_added == size | A pure append. Every byte before offset is byte-identical to what the last event for this path described. |
They were added to this payload rather than introduced as a separate
session.file.appended event: a new type would have carried the same three numbers,
cost every existing subscriber a change just to keep seeing appends, and — because
context/conversation.jsonl is written by both WriteFile and AppendFile — forced
each of them to merge two streams to reconstruct one file’s history. New map keys are
also invisible to subscribers that do not read them.
Object stores have no append primitive. These keys do not let a sync backend write
appended bytes into an existing object; S3, GCS and every S3-compatible store replace
whole objects. What they buy is the freedom to coalesce and defer — collapse a run of
tail appends into one upload of the current file at the next boundary, knowing no
rewrite was missed in between. offset is not a seek position in the bucket.
If the offset cannot be determined, offset is reported as 0, which reads as a whole
-object change: a backend then re-uploads a file it could have coalesced, rather than
coalescing a change it should have treated as a rewrite.
Who emits them
| Emitter | Covers |
|---|---|
SessionWorkspace.WriteFile | Whole-file writes: config snapshot, plugin manifest, memory rewrites, tool output |
SessionWorkspace.AppendFile | Appends: context/conversation.jsonl, metadata/timing.jsonl, compaction output, shell history, the HITL cache |
SessionWorkspace.SaveMeta | metadata/session.json — rewritten on every llm.response and every agent.turn.end |
SessionWorkspace.AnnounceWrite / AnnounceAppend | Writers holding their own os.* call: nexus.scene, nexus.workflows.icm, nexus.tool.fileio, and the desktop matcher plugin |
Every emitter goes through one of those five entry points, so every payload on the
wire is built in one place. Two hand-built emitters that were not — nexus.tool.pdf
and cmd/desktop/internal/matcher — published malformed events (a bare basename as
path, an absolute host path, missing session_id, no delta keys); the first was
removed and the second was routed through AnnounceWrite. Because the payload is an
untyped map, a subscriber should still treat offset and bytes_added as optional
and fall back to “the whole object changed” when they are absent — which is the same
conservative reading offset == 0 already asks for.
AppendFile deliberately reuses the same two event types rather than introducing an
append-specific one, so every existing subscriber sees appends without being changed.
The events say this path changed, and here is which part of it changed; they never
carry the changed bytes themselves.
Two moments are deliberately silent. Creating or loading a session workspace writes
metadata/session.json without emitting, because that happens before the journal writer
subscribes to the bus and an event there would consume a dispatch sequence number the
journal never receives — which stalls its writer permanently. Engine.StartSession
re-saves the metadata once the journal is running, so the file is still announced.
SessionSnapshotRequest
| Field | Type | Description |
|---|---|---|
Reason | string | Free-form; appears in the log line and in the result |
SessionSnapshotResult
| Field | Type | Description |
|---|---|---|
SessionID | string | Session that was snapshotted |
Trigger | string | "turn", "shutdown", "request" or "retry" |
Sequence | uint64 | Per-run snapshot counter, starting at 1 |
Generation | uint64 | The session’s commit generation. Unlike Sequence it is seeded from the manifest the previous holder committed, so it keeps increasing across a resume onto a different host. It is the stamp the commit marker and the per-object manifest in the bucket both carry. Zero when a snapshot failed before claiming one |
TurnID | string | Turn whose boundary triggered it; empty otherwise |
Objects | int | Size of the committed object set — everything the snapshot asserts is durably present, excluding the commit marker and the manifest. Includes objects immutable-skip did not re-upload, and it is exactly the set the per-object manifest names |
Bytes | int64 | Total size of those objects — how big the stored session is |
ObjectsUploaded | int | The share of that set this snapshot actually transferred |
BytesUploaded | int64 | Their total size. This, not Bytes, is the per-turn cost |
ObjectsSkipped | int | Files whose identity proves they cannot have changed (sealed journal segments, content-addressed blobs) and that a listing confirmed the store already holds |
BytesSkipped | int64 | Their total size — what immutable-skip saved |
DurationMs | float64 | Wall time of the whole snapshot |
OK | bool | Whether the snapshot was made durable |
ErrorMessage | string | Empty on success |
Both events exist only when core.object_store.backend names a
backend; with none configured nothing subscribes and a request is inert.
SessionOwnerConflict
| Field | Type | Description |
|---|---|---|
SessionID | string | Session both hosts appear to be writing |
HolderHost | string | Hostname recorded in the owner marker found in the store |
HolderPID | int | The holder’s OS process ID. Only meaningful together with HolderHost |
HolderInstanceID | string | Unique per engine run — what tells two containers sharing a hostname and a PID apart |
HolderHeartbeatAt | time.Time | The holder’s last heartbeat, by the holder’s own clock |
HeartbeatAgeSeconds | float64 | How old that heartbeat looked from here — the number the staleness decision was made on |
LocalHost | string | This process’s hostname |
LocalPID | int | This process’s PID |
LocalInstanceID | string | This run’s instance ID |
Emitted at most once per run, during Boot, and only when
core.object_store.backend names a backend. Detection, not prevention: by the
time it is emitted the engine has already claimed the session and is running
normally. No lock is taken, no fencing token is issued, nothing is refused and
nothing waits. A subscriber that wants to act — page an operator, stop the run —
has to do so itself. See
Sessions → Two hosts, one session.
SessionStorageDegraded
| Field | Type | Description |
|---|---|---|
SessionID | string | Session whose state could not be stored |
Backend | string | Registered backend name from core.object_store.backend |
FailurePolicy | string | Resolved core.object_store.failure_policy: degrade or strict |
Since | time.Time | When the outage began — the first failure, not this event |
ConsecutiveFailures | uint64 | Persistence failures so far in this outage |
QueuedPushes | int | Depth of the bounded retry queue (capacity 256) |
DroppedPushes | uint64 | Pushes discarded on queue overflow — escalated to a whole-tree snapshot, not lost |
SnapshotPending | bool | A whole-tree snapshot is owed: the backstop that covers anything the queue could not |
TurnsBlocked | bool | Further io.input is being refused. Only ever true under failure_policy: strict |
Error | string | The most recent failure’s message |
SessionStorageRecovered
| Field | Type | Description |
|---|---|---|
SessionID | string | Session that is durably stored again |
Backend | string | Registered backend name |
FailurePolicy | string | Resolved failure policy |
DegradedForSeconds | float64 | Wall time from the first failure to recovery |
Failures | uint64 | Persistence failures the outage saw in total |
RetryAttempts | uint64 | Backoff attempts the recovery worker made. Zero when an ordinary turn-boundary snapshot healed it first |
DrainedPushes | uint64 | Deferred pushes the retries got through |
DroppedPushes | uint64 | Pushes discarded on overflow and covered by a snapshot instead |
The two pair: exactly one session.storage.degraded per outage and one
session.storage.recovered when it ends, so a subscriber counts outages rather
than failed requests. Neither is emitted when core.object_store.backend is
empty.
Under degrade the session keeps taking turns while degraded. The honest
caveat is that the durability guarantee is not being met for as long as the
outage lasts, even though nothing is failing.
Under strict TurnsBlocked is true and every subsequent io.input is
vetoed until the state is stored. The turn that hit the outage already ran —
its output was streamed, its tools executed — and the engine cannot un-run it.
What strict guarantees is that no further turn runs against unstored state.
Recovery is automatic under both policies; see Configuration → Failure
policy.
The engine snapshots at every agent.turn.end on its own, so nothing needs to
emit session.snapshot.request in a normal run — it is the escape hatch for
embedders driving the engine outside an agent loop, and for custom agents that
emit no turn events. Snapshotting is handled in core and never depends on plugin
cooperation. See
Sessions → Turn-boundary snapshots.
Session Tag Events
SessionMeta.Labels — the session’s key/value tag store — is split into two
disjoint namespaces: reserved keys (starting with _) and general keys
(everything else). Only general keys are reachable through the two
before:* events below; a reserved key is rejected unconditionally,
regardless of caller. See Session Tags
for the full mechanism.
SessionTagSetRequest — payload of before:session.tag.set
| Field | Type | Description |
|---|---|---|
Key | string | The label key to write |
Value | string | The label value |
SessionTagDeleteRequest — payload of before:session.tag.delete
| Field | Type | Description |
|---|---|---|
Key | string | The label key to remove |
Both ride the bus as the struct itself (via VetoablePayload.Original), not
as a hand-spelled map — the same shape before:llm.request uses. The single
handler for both lives in the core engine: it vetoes unconditionally when
Key matches the reserved namespace (engine.IsReservedLabelKey), with a
VetoResult.Reason naming the rejection, and otherwise applies the write
immediately and fires the matching announce event below. There is no second
“apply” step after the veto check passes — the request event doubles as the
trigger, because the core engine is the only thing that knows how to mutate
SessionMeta.Labels safely.
SessionTagSet — payload of session.tag.set
| Field | Type | Description |
|---|---|---|
SessionID | string | Session the label belongs to |
Key | string | The label key that was written |
Value | string | The label value that was written |
SessionTagDeleted — payload of session.tag.deleted
| Field | Type | Description |
|---|---|---|
SessionID | string | Session the label belonged to |
Key | string | The label key that was removed |
Both announce events fire from every successful write, whichever path
produced it: the vetoable general path above, or the reserved-namespace
direct-Go-call path (SessionWorkspace.SetReservedLabel /
DeleteReservedLabel, e.g. nexus.io.agui’s _principal_id identity
binding), which never touches the bus at all. A subscriber that only cares
“this session’s tags changed” needs only these two event types regardless of
which path wrote the label. SessionTagDeleted is also the end-of-run
signal for nexus.io.agui’s identity binding: a transport that set
_principal_id on run start deletes it on run end.
Unlike the object-store events above, all four session tag events are always
active — they do not depend on core.object_store.backend.
Cancellation Events
| Event Type | Payload | Description |
|---|---|---|
cancel.request | CancelRequest | User requested cancellation |
cancel.active | CancelActive | Cancellation broadcast |
cancel.complete | CancelComplete | Cancellation processed |
cancel.resume | CancelResume | Resume after cancellation |
CancelRequest
| Field | Type | Description |
|---|---|---|
TurnID | string | Turn to cancel |
Source | string | Who requested: "tui", "browser", etc. |
Embeddings Events
| Event Type | Payload | Description |
|---|---|---|
embeddings.request | *EmbeddingsRequest | Embed a batch of texts. Pointer-fill — provider mutates in place. |
Payloads
EmbeddingsRequest
| Field | Type | Description |
|---|---|---|
Texts | []string | Batch of strings to embed (input). Used when Inputs is empty — back-compat with text-only adapters. |
Inputs | []EmbeddingsInput | Polymorphic batch (text + image inputs) for multimodal-aware providers. When non-empty, providers consume Inputs and ignore Texts. |
Model | string | Requested model; provider may echo back actual model used. |
Dimensions | int | Optional truncation hint. Zero = provider default. |
Vectors | [][]float32 | Result vectors, in input order (output). |
Provider | string | Plugin ID of the adapter that answered (output). |
Usage | EmbeddingsUsage | Token usage when reported by the provider (output). |
Error | string | Non-empty on failure (output). |
EmbeddingsInput
| Field | Type | Description |
|---|---|---|
Text | string | Text snippet. Mutually exclusive with Image and ImageURI. |
Image | []byte | Inline image bytes. Mutually exclusive with Text and ImageURI. Requires MimeType. |
ImageURI | string | Image reference: nexus-blob:<sha> (engine blob store) or external URL. Mutually exclusive with Text and Image. |
MimeType | string | IANA media type (e.g. image/png). Required when Image is set; recommended for ImageURI. |
Adapters that don’t support image inputs must return a clear error when
they encounter an image-bearing input rather than silently downgrading.
See nexus.embeddings.cohere_multimodal for a multimodal-aware reference
adapter.
EmbeddingsUsage
| Field | Type | Description |
|---|---|---|
PromptTokens | int | Tokens consumed by the input. |
TotalTokens | int | Total billable tokens. |
Vector Store Events
| Event Type | Payload | Description |
|---|---|---|
vector.upsert | *VectorUpsert | Insert or replace docs in a namespace. |
vector.query | *VectorQuery | Nearest-neighbor lookup in a namespace. |
vector.delete | *VectorDelete | Remove docs by ID. |
vector.namespace.drop | *VectorNamespaceDrop | Remove an entire namespace (idempotent). |
All four are pointer-fill — adapter sets Provider / Error (and Matches on query) in place.
Payloads
VectorDoc
| Field | Type | Description |
|---|---|---|
ID | string | Stable identifier; upsert replaces by ID. |
Vector | []float32 | Embedding. Adapters may require unit-normalized. |
Content | string | Original text (optional but typically stored for re-ranking / display). |
Metadata | map[string]string | String-keyed metadata. |
VectorMatch
| Field | Type | Description |
|---|---|---|
ID | string | Doc ID. |
Content | string | Doc content. |
Metadata | map[string]string | Doc metadata. |
Similarity | float32 | Cosine similarity in [-1, 1]. |
VectorUpsert
| Field | Type | Description |
|---|---|---|
Namespace | string | Target namespace (input). |
Docs | []VectorDoc | Docs to upsert (input). |
Provider | string | Adapter plugin ID (output). |
Error | string | Non-empty on failure (output). |
VectorQuery
| Field | Type | Description |
|---|---|---|
Namespace | string | Target namespace (input). |
Vector | []float32 | Query vector (input). |
K | int | Max results (input). |
Filter | map[string]string | Exact-match metadata filter (input, optional). |
Matches | []VectorMatch | Hits sorted by similarity desc (output). |
Provider | string | Adapter plugin ID (output). |
Error | string | Non-empty on failure (output). |
VectorDelete
| Field | Type | Description |
|---|---|---|
Namespace | string | Target namespace (input). |
IDs | []string | Doc IDs to remove. Unknown IDs are ignored (input). |
Provider | string | Adapter plugin ID (output). |
Error | string | Non-empty on failure (output). |
VectorNamespaceDrop
| Field | Type | Description |
|---|---|---|
Namespace | string | Namespace to drop (input). |
Provider | string | Adapter plugin ID (output). |
Error | string | Non-empty on failure (output). |
RAG Events
| Event Type | Payload | Description |
|---|---|---|
rag.ingest | *RAGIngest | Ingest one file. Pointer-fill. |
rag.ingest.delete | *RAGIngestDelete | Drop a file’s chunks. Pointer-fill. |
rag.ingest.result | *RAGIngest | Notification emitted after rag.ingest completes (read-only). |
memory.vector.store | *VectorMemoryStore | Explicit store into vector memory. Pointer-fill. |
Payloads
RAGIngest
| Field | Type | Description |
|---|---|---|
Path | string | File path (input). |
Namespace | string | Target namespace (input). |
Metadata | map[string]string | Optional metadata merged into every chunk (input). |
Provider | string | Ingest plugin ID (output). |
Chunks | int | Number of chunks upserted (output). |
SkippedCached | int | How many of those came from the embedding cache (output). |
Error | string | Non-empty on failure (output). |
RAGIngestDelete
| Field | Type | Description |
|---|---|---|
Path | string | File path (input). |
Namespace | string | Target namespace (input). |
Provider | string | Ingest plugin ID (output). |
Deleted | int | Reserved; currently always zero (output). |
Error | string | Non-empty on failure (output). |
VectorMemoryStore
| Field | Type | Description |
|---|---|---|
Content | string | Content to store (input). |
Source | string | Short label recorded as metadata (e.g. user, agent, compaction) (input). |
Metadata | map[string]string | Extra metadata merged into the stored doc (input). |
Provider | string | Plugin ID of the memory.vector plugin (output). |
Error | string | Non-empty on failure (output). |
Delegate Events
Emitted by nexus.agent.delegate when a parent agent invokes a posture via
the delegate tool. Payloads are map[string]any records — see
pkg/delegate.Runtime for the canonical fields.
| Event Type | Payload | Description |
|---|---|---|
delegate.start | map | Sub-session is starting. Keys: sub_session_id, posture, posture_ver, task, parent_turn, depth. |
delegate.complete | map | Sub-session finished. Keys: sub_session_id, posture, posture_ver, status (success/partial/error/timeout/cancelled/cache_hit), error, tokens_used, tool_calls_used, elapsed_ms, result, depth. |
Posture Events
Emitted by nexus.agent.postures as posture YAML loads or changes.
| Event Type | Payload | Description |
|---|---|---|
posture.registered | map | A posture was installed or updated. Keys: name, version, source (file path or scan dir). |
posture.removed | map | A posture was removed (file deleted / load error). Keys: name. |
Scene Events
Emitted by nexus.scene on every mutation. agent_id on the payload comes
from Event.Causation.AgentID — sub-agent contributions remain attributable
in the journal.
| Event Type | Payload | Description |
|---|---|---|
scene.created | map | New Scene created. Keys: session_id, scene_id, schema, version, agent_id, content (initial content). |
scene.patched | map | Scene content updated. Keys: session_id, scene_id, version, agent_id, content (full post-merge content). |
scene.deleted | map | Scene removed. Keys: session_id, scene_id, agent_id. |
Tool Stream Events
Emitted by pkg/streamtool.Bridge while a ChannelTool runs. Consumers
that only need the final value still subscribe to tool.result; UIs and
observability collectors that want incremental progress subscribe here.
| Event Type | Payload | Description |
|---|---|---|
tool.stream.progress | map | Status-only update from a streaming tool. Keys: tool_name, tool_id, turn_id, sequence, progress (0.0–1.0 or -1), payload. |
tool.stream.partial | map | Incremental data the consumer can render now. Keys: tool_name, tool_id, turn_id, sequence, payload. |
Thinking Events
| Event Type | Payload | Description |
|---|---|---|
thinking.step | ThinkingStep | Agent reasoning step |
ThinkingStep
| Field | Type | Description |
|---|---|---|
TurnID | string | Associated turn |
Source | string | Plugin that generated this |
Content | string | Thinking content |
Phase | string | "planning", "executing", "reasoning" |
Timestamp | time.Time | When this occurred |
ICM Workflow Events
Emitted by nexus.workflows.icm on top of the generic plan.created /
plan.progress surface. Every payload carries a _schema_version field
(constants in plugins/workflows/icm/icmtypes/types.go).
| Event Type | Payload | Description |
|---|---|---|
icm.run.started | ICMRunStarted | Workspace loaded; run created; before stage 1 dispatches. |
icm.run.completed | ICMRunCompleted | All stages finished without halt. |
icm.run.halted | ICMRunHalted | Stage error policy halted, gate rejected, or run context cancelled. |
icm.stage.started | ICMStageStarted | Stage execution begins, before any human_gate: start. |
icm.stage.completed | ICMStageCompleted | Artifact written; any end gate resolved. |
icm.stage.failed | ICMStageFailed | Stage halted (dispatch error, rejected gate, or loop.on_exhausted: error). |
icm.stage.iteration | ICMStageIteration | One per loop iteration, immediately before the iteration’s invocation. |
icm.turn | ICMTurn | After each turn within an invocation (richer-UI feed; basic UIs already see plan.progress). |
icm.fanout.item | ICMFanoutItem | Item lifecycle boundary in a fan-out stage (active → completed | failed). |
icm.predicate.failed | ICMPredicateFailed | Any predicate evaluation returns verdict=false. Pass paths are not emitted. |
ICMRunStarted
| Field | Type | Description |
|---|---|---|
RunID | string | Run identifier (r_<id>); also the on-disk dir under the plugin’s session data dir. |
InstanceID | string | Plugin instance ID (nexus.workflows.icm or nexus.workflows.icm/<suffix>). |
WorkspaceRoot | string | Absolute path to the loaded workspace. |
WorkspaceName | string | Last folder name of the workspace path. |
Stages | int | Stage count. |
ICMRunCompleted
| Field | Type | Description |
|---|---|---|
RunID | string | Run identifier. |
StagesRun | int | Number of stages that completed. |
AggregatePath | string | Optional run aggregate path (set when a top-level aggregate is produced). |
ElapsedSeconds | int64 | Wall-clock seconds from icm.run.started. |
ICMRunHalted
| Field | Type | Description |
|---|---|---|
RunID | string | Run identifier. |
Reason | string | Free-text halt reason. |
HaltedAtStage | string | Stage ID where the halt fired (empty for pre-stage halts). |
Cancelled | bool | true when the halt was a context cancellation rather than a gate reject. |
ElapsedSeconds | int64 | Wall-clock seconds from icm.run.started. |
ICMStageStarted
| Field | Type | Description |
|---|---|---|
RunID | string | Run identifier. |
StageID | string | Stage folder name (e.g. 02_brief). |
PostureName | string | Derived posture name registered for this stage (icm.<runID>.<stage_id>). |
Order | int | 1-based stage order in the workspace. |
ICMStageCompleted
| Field | Type | Description |
|---|---|---|
RunID | string | Run identifier. |
StageID | string | Stage folder name. |
ArtifactPath | string | Path to the final artifact. |
IterationsRun | int | Loop iterations executed (0 for non-looping stages). |
ConvergenceFailed | bool | true when the loop exhausted without satisfying until. |
ICMStageFailed
| Field | Type | Description |
|---|---|---|
RunID | string | Run identifier. |
StageID | string | Stage folder name. |
Reason | string | Free-text failure reason. |
ICMStageIteration
| Field | Type | Description |
|---|---|---|
RunID | string | Run identifier. |
StageID | string | Stage folder name. |
ItemID | string | Fan-out item ID (empty for non-fan-out stages). |
Iteration | int | 1-based iteration index. |
MaxIterations | int | loop.max_iterations. |
ExitFailures | []ConditionResult | Previous iteration’s failing until predicates (empty on iteration 1). |
ICMTurn
| Field | Type | Description |
|---|---|---|
RunID | string | Run identifier. |
StageID | string | Stage folder name. |
ItemID | string | Fan-out item ID (empty for non-fan-out stages). |
Iteration | int | Loop iteration index (0 for non-looping stages). |
Turn | int | 1-based turn index. |
MaxTurns | int | turns.max. |
LastFailures | []ConditionResult | Previous turn’s failing validators (drives until_valid retry). |
ICMFanoutItem
| Field | Type | Description |
|---|---|---|
RunID | string | Run identifier. |
StageID | string | Stage folder name. |
ItemID | string | Per-item ID (from fan_out.item_id gojq or fallback index). |
Index | int | 1-based item index. |
Total | int | Total items in the fan-out. |
Status | string | "active" | "completed" | "failed". |
Error | string | Failure reason when Status="failed". |
ICMPredicateFailed
| Field | Type | Description |
|---|---|---|
RunID | string | Run identifier. |
StageID | string | Stage folder name. |
ItemID | string | Fan-out item ID (empty for non-fan-out stages). |
Container | string | "output.validators" | "loop.until" | "verifier". |
PredicateName | string | Predicate name: (or <type>_<index> fallback). |
PredicateType | string | "schema" | "regex" | "native" | "command" | "llm" | "human". |
Feedback | string | Predicate-supplied failure feedback. |
ConditionResult (shared by ICMStageIteration.ExitFailures and ICMTurn.LastFailures)
| Field | Type | Description |
|---|---|---|
Type | string | Predicate type. |
Name | string | Predicate name. |
Verdict | string | "pass" | "fail". |
Feedback | string | Predicate-supplied feedback. |
Score | *float64 | Optional numeric score (LLM judges typically populate this). |