Skip to content

AgentChannel

AgentChannel is the typed event bus that every agent exposes. It is the only way plugins and user code communicate with the agent and with each other.

Interface

ts
interface AgentChannel {
  emit: <K extends string>(
    channel: K,
    event: K extends keyof AgentCustomEvent ? AgentCustomEvent[K] : unknown,
    options?: { save?: boolean },
  ) => MaybePromise<void>

  subscribe: <K extends string>(
    channel: K,
    listener: K extends keyof AgentCustomEvent
      ? AgentEventListener<AgentCustomEvent[K]>
      : AgentEventListener,
  ) => () => void
}
  • emit(channel, event, options?) — emits an event on a named channel.
  • subscribe(channel, listener) — registers a listener. Returns an unsubscribe function.

The 'apeira' channel

The built-in channel 'apeira' carries all core lifecycle and runner forwarding events. Every event includes a turnId.

ts
import { 
createAgent
} from '@apeira/core'
import {
responses
} from '@apeira/core/responses'
const
agent
=
createAgent
({
instructions
: 'You are a helpful assistant.',
runner
:
responses
({
apiKey
:
process
.
env
.
OPENAI_API_KEY
,
baseURL
: 'https://api.openai.com/v1/',
model
: 'gpt-5.5',
}), })
agent
.
subscribe
('apeira', (
event
) => {
console
.
log
(
event
.
turnId
,
event
.
type
)
})

Persisting events

Pass { save: true } to persist the event to storage as an event entry. This is how the core turn lifecycle events are recorded.

ts
import { 
createAgent
} from '@apeira/core'
import {
responses
} from '@apeira/core/responses'
const
agent
=
createAgent
({
instructions
: 'You are a helpful assistant.',
runner
:
responses
({
apiKey
:
process
.
env
.
OPENAI_API_KEY
,
baseURL
: 'https://api.openai.com/v1/',
model
: 'gpt-5.5',
}), }) await
agent
.
emit
('apeira', {
turnId
:
crypto
.
randomUUID
(),
type
: 'agent.reset',
}, {
save
: true })

Listener errors are silently ignored so that one broken subscriber cannot break event delivery to others.

Typed custom channels

Plugins can declare custom channels by extending AgentCustomEvent. Once declared, subscribe() infers the event type automatically.

ts
import type { AGUIEvent } from '@ag-ui/core'

declare module '@apeira/core' {
  interface AgentCustomEvent {
    'ag-ui': AGUIEvent
  }
}

Custom channels are also how plugins talk to each other during init():

ts
const pluginA = {
  init: (agent) => {
    agent.subscribe('custom-channel', (event) => {
      // handle event from plugin-b
    })
  },
  name: 'plugin-a',
}

const pluginB = {
  init: (agent) => {
    agent.emit('custom-channel', { ok: true })
  },
  name: 'plugin-b',
}

Design notes

  • Channels are stored in a Map<string, Set<AgentEventListener>>.
  • emit() resolves listeners and optional persistence in parallel with Promise.all.
  • Subscriptions are removed lazily: when the last listener unsubscribes, the channel entry is deleted.