Skip to content

Event System

Amplifier uses an event-driven architecture for observability. The kernel emits canonical events that hooks can observe, modify, or act upon.

Design Principles

  1. If it's important, emit an event
  2. If it's not observable, it didn't happen
  3. Single source of truth: One JSONL log
  4. Hooks observe without blocking (by default)

Canonical Events

Session Lifecycle

Event When Data
session:start Session initialized session_id, mount_plan
session:end Session cleanup session_id, duration
session:fork Sub-session created parent_id, child_id
session:resume Session resumed session_id

Prompt Lifecycle

Event When Data
prompt:submit User prompt received prompt, session_id
prompt:complete Response generated response, duration

Provider Events

Event When Data
provider:request Before LLM call messages, model
provider:response After LLM response response, usage
provider:error LLM call failed error, model

Tool Events

Event When Data
tool:pre Before tool execution tool_name, input
tool:post After tool execution tool_name, result
tool:error Tool execution failed tool_name, error

Context Events

Event When Data
context:pre_compact Before compaction message_count, tokens
context:post_compact After compaction message_count, tokens
context:include Context injected source, content

Approval Events

Event When Data
approval:required Approval requested operation, prompt
approval:granted User approved operation
approval:denied User denied operation, reason

Orchestrator Events

Event When Data
orchestrator:complete Main loop finished iterations, tokens

Event Schema

Events are logged as JSONL with this schema:

{
  "ts": "2024-01-15T10:30:00.123Z",
  "lvl": "info",
  "schema": {"name": "amplifier.log", "ver": "1.0.0"},
  "session_id": "abc123",
  "request_id": "def456",
  "span_id": "ghi789",
  "event": "provider:request",
  "component": "orchestrator",
  "module": "provider-anthropic",
  "status": "success",
  "duration_ms": 1234,
  "data": {
    "model": "claude-sonnet-4-5",
    "message_count": 5
  },
  "error": null
}

Schema Fields

Field Type Description
ts ISO8601 Timestamp
lvl string Log level (info, warn, error)
schema object Schema name and version
session_id string Session identifier
request_id string? Request identifier
span_id string? Span identifier for tracing
event string Event name
component string Emitting component
module string? Module name
status string? success or error
duration_ms number? Duration in milliseconds
data object Event-specific data
error object? Error details if applicable

Hooks and Events

Observing Events

Hooks register handlers for events:

async def my_handler(event: str, data: dict) -> HookResult:
    if event == "tool:post":
        print(f"Tool {data['tool_name']} completed")
    return HookResult(action="continue")

# Register
coordinator.hooks.register("tool:post", my_handler)

Hook Actions

Handlers return HookResult with an action:

Action Effect
continue Proceed normally
deny Block the operation
modify Modify event data
inject_context Add to conversation
ask_user Request approval

Example: Blocking Dangerous Operations

async def safety_hook(event: str, data: dict) -> HookResult:
    if event == "tool:pre" and data["tool_name"] == "bash":
        command = data["input"].get("command", "")
        if "rm -rf" in command:
            return HookResult(
                action="deny",
                reason="Destructive command blocked"
            )
    return HookResult(action="continue")

Example: Context Injection

async def linter_hook(event: str, data: dict) -> HookResult:
    if event == "tool:post" and data["tool_name"] == "write":
        # Run linter on written file
        errors = run_linter(data["file_path"])
        if errors:
            return HookResult(
                action="inject_context",
                context_injection=f"Linter errors:\n{errors}",
                user_message="Found linting issues"
            )
    return HookResult(action="continue")

Event Logging

Default Logging

The hooks-logging module logs all events to JSONL:

hooks:
  - module: hooks-logging
    config:
      level: info
      output: ~/.amplifier/logs/

Log Levels

Level Events Logged
error Errors only
warn Errors + warnings
info Standard events
debug + LLM request/response summaries
raw + Complete API payloads

Finding Logs

# Session logs
ls ~/.amplifier/projects/<project>/sessions/<session-id>/events.jsonl

# Search logs
grep '"event":"tool:error"' events.jsonl

# Pretty print
cat events.jsonl | jq 'select(.event == "provider:request")'

Custom Events

Modules can emit custom events:

async def mount(coordinator, config):
    # Declare observable events
    coordinator.register_contributor(
        "observability.events",
        "my-module",
        lambda: [
            "my-module:initialized",
            "my-module:processing",
            "my-module:completed"
        ]
    )

    # Emit events
    await coordinator.hooks.emit("my-module:initialized", {
        "version": "1.0.0"
    })

Custom events follow the namespace:action convention.

Tracing

Trace Context

Events include tracing identifiers:

  • session_id: Identifies the session
  • request_id: Identifies a single prompt/response cycle
  • span_id: Identifies a specific operation

Sub-Session Tracing

When agents spawn sub-sessions, IDs are related:

Parent: session_id=abc123
  └── Child: session_id=abc123-def456_explorer

Best Practices

  1. Emit events for important operations
  2. Include useful data: Enough to debug, not too much to be noise
  3. Use canonical event names: Follow the component:action pattern
  4. Handle hook results: Check for deny and inject_context
  5. Aggregate logs centrally: For production debugging

References