README

#Event Package

The event package defines the event system used throughout the agent lifecycle for tracking and responding to state changes.

#Core Types

#Event

The main event structure with metadata.

///|
pub struct Event {
id : @uuid.Uuid
created : @clock.Timestamp
desc : EventDesc
} derive(Eq, Show)

#EventDesc

Enumeration of all possible event types in the agent lifecycle.

///|
pub(all) enum EventDesc {
ModelLoaded(name~ : String)
PreConversation
PostConversation
SystemPromptSet(String?)
MessageUnqueued(id~ : @uuid.Uuid)
MessageQueued(id~ : @uuid.Uuid)
ToolAdded(@tool.ToolDesc)
PreToolCall(@ai.ToolCall)
PostToolCall(@ai.ToolCall, result~ : Result[Json, Json], rendered~ : String)
TokenCounted(Int)
ContextPruned(origin_token_count~ : Int, pruned_token_count~ : Int)
AssistantMessage(
usage~ : @ai.Usage?,
tool_calls~ : Array[@ai.ToolCall],
String
)
UserMessage(String)
Cancelled
Failed(Json)
Pruned(id~ : @uuid.Uuid)
}

#Event Categories

#Lifecycle Events

EventDescription
ModelLoadedAI model has been loaded
PreConversationConversation is starting
PostConversationConversation has ended
CancelledAgent was cancelled
FailedAn error occurred

#Message Events

EventDescription
SystemPromptSetSystem prompt was set/cleared
MessageQueuedMessage added to pending queue
MessageUnqueuedMessage moved to processing
UserMessageUser or tool message content
AssistantMessageResponse from AI model

#Tool Events

EventDescription
ToolAddedTool registered with agent
PreToolCallBefore tool execution
PostToolCallAfter tool execution (with result)

#Context Management Events

EventDescription
TokenCountedToken count calculated
ContextPrunedContext trimmed for budget
PrunedSpecific event pruned from history

#Key APIs

#Event Creation

pub fn Event::new(
id~ : @uuid.Uuid,
created? : @clock.Timestamp,
desc : EventDesc,
) -> Event

#Event Classification

// Is this an incoming/external event?
pub fn Event::is_incoming(self : Event) -> Bool

// Does this event end the conversation?
pub fn Event::is_stopping(self : Event) -> Bool

// Does this event start the conversation?
pub fn Event::is_starting(self : Event) -> Bool

// Is this a cancellation event?
pub fn Event::is_cancellation(self : Event) -> Bool

#EventTarget

A type for emitting events to listeners.

type EventTarget

pub fn EventTarget::new(
uuid? : @uuid.Generator,
clock? : &@clock.Clock
) -> Self raise

pub fn EventTarget::emit(Self, EventDesc, id? : @uuid.Uuid) -> Unit
pub fn EventTarget::add_listener(Self, async (Event) -> Unit) -> Unit
pub async fn EventTarget::start(Self) -> Unit
pub async fn EventTarget::flush(Self) -> Unit

#ExternalEventQueue

A queue for receiving external events from the environment.

type ExternalEventQueue

pub fn ExternalEventQueue::new() -> Self raise
pub fn ExternalEventQueue::poll(Self) -> Array[Event]
pub fn ExternalEventQueue::send(Self, EventDesc) -> Unit

#Typical Event Flow

ModelLoaded │ ▼ SystemPromptSet │ ▼ MessageQueued ──► MessageUnqueued ──► UserMessage │ ▼ PreConversation │ ▼ TokenCounted │ ▼ ContextPruned (if needed) │ ▼ AssistantMessage │ ├──► PreToolCall ──► PostToolCall ──► UserMessage (tool result) │ │ │ └──► (loop back for more tool calls) │ ▼ PostConversation

#Usage Example

// Create event target
let target = @event.EventTarget::new()

// Add listener
target.add_listener(fn(event) {
match event.desc {
AssistantMessage(content, ..) => println("AI: \{content}")
PostToolCall(tc, result~, ..) => println("Tool \{tc.name}: \{result}")
_ => ()
}
})

// Start processing events
target.start()

// Emit events
target.emit(UserMessage("Hello!"))
target.emit(AssistantMessage(usage=None, tool_calls=[], "Hi there!"))

#Dependencies

  • ai: Message and tool call types
  • tool: Tool descriptor types
  • uuid: Unique identifiers
  • clock: Timestamps

#
Event

Event type that occurs during agent conversation lifecycle.

A typical conversation lifecycle looks like this:

// add system and user messages
UserMessage (system)
UserMessage (user)
// calling agent.start()
PreConversation
while true {
// poll external events
ExternalEventReceived (if any)
// count tokens before request
TokenCounted
// prune context if necessary
ContextPruned
// receive assistant response
AssistantMessage
// executing tool call
PreToolCall
PostToolCall
UserMessage (tool)
// continue to iterate if there are more messages
}
// conversation ended
PostConversation
impl ToJson for Event
impl FromJson for Event

#
Event::is_cancellation

fn Event::is_cancellation(self : Event) -> Bool

Indicates whether the event represents a cancellation.

#
Event::is_incoming

fn Event::is_incoming(self : Event) -> Bool

External events that can be sent to the agent from outside sources. These events are collected via polling and can influence the conversation.

#
Event::is_starting

fn Event::is_starting(self : Event) -> Bool

Indicates whether the event signifies the start of a conversation.

#
Event::is_stopping

fn Event::is_stopping(self : Event) -> Bool

Indicates whether the event signifies the end of a conversation.

#
Event::new

Creates a new Event with the specified parameters.

Parameters:

  • id : The unique identifier for the event.
  • created : The timestamp when the event was created. Defaults to the current time if not provided.
  • desc : The event description that specifies the type and content of the event.

Returns a new Event instance.

#
EventDesc

pub(all) enum EventDesc {
ModelLoaded(name~ : String)
PreConversation
PostConversation
SystemPromptSet(String?)
MessageUnqueued(id~ :
Uuid
)
MessageQueued(id~ :
Uuid
)
ToolAdded(
ToolDesc
)
PreToolCall(
ToolCall
)
PostToolCall(
ToolCall
, result~ : Result[Json, Json], rendered~ : String)
TokenCounted(Int)
ContextPruned(origin_token_count~ : Int, pruned_token_count~ : Int)
AssistantMessageDelta(String)
AssistantMessage(usage~ :
Usage
?, tool_calls~ : Array[
ToolCall
], String)
UserMessage(String)
Cancelled
Failed(Json)
Pruned(id~ :
Uuid
)
} derive(Eq)

#
EventTarget

type EventTarget

The central event dispatcher for agent lifecycle events.

EventTarget implements an observer pattern where multiple listeners can subscribe to receive events. Events are queued and processed asynchronously, ensuring that event emission is always non-blocking.

Architecture

emit() ──▶ [Queue] ──▶ start() ──▶ [Listener 1] ──▶ [Listener 2] ──▶ [Listener N]

Threading Model

  • emit() is synchronous and non-blocking (enqueues event)
  • start() runs an async event loop that dispatches to listeners
  • Listeners are invoked sequentially for each event

#
EventTarget::add_listener

fn EventTarget::add_listener(self : EventTarget, f : async (Event) -> Unit) -> Unit

Registers an async listener function to receive events.

Listeners are called sequentially for each event in the order they were registered. Each listener receives every event emitted after registration.

Parameters

  • f: An async function that takes an Event and returns Unit.

Example

emitter.add_listener(async fn(event) {
match event {
PostToolCall(call, result~, rendered~) => {
// Log tool call results
println("Tool \(call.name) completed")
}
AssistantMessage(usage~, message~) => {
// Track token usage
if usage is Some(u) {
total_tokens u.total_tokens
}
}
_ => ()
}
})

#
EventTarget::emit

fn EventTarget::emit(self : EventTarget, desc : EventDesc, id? :
Uuid
) -> Unit

Emits an event to be processed by all registered listeners.

This method enqueues the event for asynchronous processing. The event will be dispatched to all listeners when start() processes it from the queue.

Parameters

  • event: The Event to emit.

Behavior

  • Non-blocking: Returns immediately after enqueuing.
  • Order-preserving: Events are processed in FIFO order.

Panics

Aborts if the queue is full (should not happen with unbounded queue).

Example

emitter.emit(PreConversation)
emitter.emit(TokenCounted(1500))
emitter.emit(PostConversation)

#
EventTarget::flush

async fn EventTarget::flush(self : EventTarget) -> Unit

Immediately processes all pending events in the queue.

Unlike start(), this method does not wait for new events. It processes all currently queued events and returns, making it useful for ensuring all events are handled before a checkpoint.

Behavior

  • Non-blocking on empty: Returns immediately if queue is empty.
  • Draining: Processes all pending events.
  • Does not terminate: Does not affect the start() loop.

Example

// Ensure all events are processed before saving state
emitter.flush()
save_checkpoint()
CR: listener is suspendable, should we collcect all events and then call listeners outside of the loop to avoid racing? assuming listerns can take a long time

#
EventTarget::new

Creates a new EventTarget with an empty listener list.

The event target uses an unbounded queue to ensure emit() never blocks. Remember to call start() in a background task to begin processing events.

Returns

A new EventTarget instance ready to receive listeners and events.

Example

let emitter = EventTarget::new()
emitter.add_listener(async fn(event) { println(event) })
// Start in background
spawn(() => emitter.start())

#
EventTarget::start

async fn EventTarget::start(self : EventTarget) -> Unit

Starts the event processing loop.

This async function runs continuously, waiting for events from the queue and dispatching them to all registered listeners. It blocks until a None sentinel is received (via close()).

Behavior

  • Blocking: Waits for events when queue is empty.
  • Sequential dispatch: Listeners are called one at a time per event.
  • Terminates: Exits when close() sends the termination signal.

Usage

Should typically be spawned as a background task:

@async.with_task_group((group) => {
group.spawn_bg(() => { emitter.start() }, no_wait=true)
// ... rest of the application
})

#
ExternalEventQueue

type ExternalEventQueue

A queue for receiving external events from the environment.

The ExternalEventQueue provides a thread-safe, non-blocking mechanism for external sources (IDE, user input, environment) to communicate with the agent. The environment pushes events via send(), and the agent polls via poll().

Example

let queue = ExternalEventQueue::new()

// From external source (e.g., IDE integration)
queue.send(Diagnostics(diagnostics))

// From agent (polling during conversation)
let events = queue.poll() // Returns all pending events

#
ExternalEventQueue::new

Creates a new ExternalEventQueue with an unbounded async queue.

The queue is unbounded to prevent blocking when external sources send events, ensuring that event producers never need to wait.

Returns

A new ExternalEventQueue instance ready to receive events.

#
ExternalEventQueue::poll

Polls and retrieves all pending external events from the queue.

This method is called by the agent to check for and process any external events that have been sent since the last poll. It drains the queue, returning all accumulated events.

Returns

An Array[ExternalEvent] containing all pending events. Returns an empty array if no events are pending.

Behavior

  • Non-blocking: Returns immediately with available events.
  • Draining: Removes all events from the queue.
  • Order-preserving: Events are returned in FIFO order.
  • Incoming-only: Only returns events intended for the consumption of the agent.

Example

// During conversation loop
let external_events = queue.poll()
for event in external_events {
match event {
Cancelled => return // Stop conversation
Diagnostics(d) => process_diagnostics(d)
UserMessage(msg) => handle_message(msg)
}
}

#
ExternalEventQueue::send

fn ExternalEventQueue::send(self : ExternalEventQueue, desc : EventDesc) -> Unit

Sends an external event to the agent.

This method is designed to be called from external contexts (environment, IDE integration, user input handlers) to communicate with the agent.

Parameters

  • event: The ExternalEvent to send to the agent.

Behavior

  • Non-blocking: Returns immediately without waiting.
  • Thread-safe: Can be safely called from any context.
  • Fire-and-forget: Silently ignores failures (queue full scenario).

Example

// Send IDE diagnostics
queue.send(Diagnostics(diagnostics))

// Request cancellation
queue.send(Cancelled)

// Send user message
queue.send(UserMessage("Stop and explain"))