Skip to main content

Overview

The Agent Orchestrator uses an event-driven architecture for communication between the lifecycle manager, notification system, and reaction engine. Events represent state transitions and important occurrences in session lifecycles.
There is no dedicated EventBus class - events are created and dispatched directly by the Lifecycle Manager. This page documents the event system as implemented across the codebase.
Key concepts:
  • Events are emitted on session state transitions
  • Each event has a priority level (urgent, action, warning, info)
  • Events are routed to notifiers based on priority
  • Events can trigger automated reactions

Event Structure

interface OrchestratorEvent {
  id: string;              // Unique UUID
  type: EventType;         // Event type identifier
  priority: EventPriority; // urgent | action | warning | info
  sessionId: SessionId;    // Associated session
  projectId: string;       // Associated project
  timestamp: Date;         // When event occurred
  message: string;         // Human-readable description
  data: Record<string, unknown>;  // Event-specific payload
}
Example:
{
  id: "a3b4c5d6-e7f8-1234-5678-9abcdef01234",
  type: "ci.failing",
  priority: "warning",
  sessionId: "my-app-1",
  projectId: "my-app",
  timestamp: new Date("2026-03-04T10:30:00Z"),
  message: "my-app-1: pr_open → ci_failed",
  data: {
    oldStatus: "pr_open",
    newStatus: "ci_failed",
    prUrl: "https://github.com/org/repo/pull/42"
  }
}

Event Types

All event types are string literals defined in the EventType union:
type EventType =
  // Session lifecycle
  | "session.spawned"
  | "session.working"
  | "session.exited"
  | "session.killed"
  | "session.stuck"
  | "session.needs_input"
  | "session.errored"
  // PR lifecycle
  | "pr.created"
  | "pr.updated"
  | "pr.merged"
  | "pr.closed"
  // CI
  | "ci.passing"
  | "ci.failing"
  | "ci.fix_sent"
  | "ci.fix_failed"
  // Reviews
  | "review.pending"
  | "review.approved"
  | "review.changes_requested"
  | "review.comments_sent"
  | "review.comments_unresolved"
  // Automated reviews
  | "automated_review.found"
  | "automated_review.fix_sent"
  // Merge
  | "merge.ready"
  | "merge.conflicts"
  | "merge.completed"
  // Reactions
  | "reaction.triggered"
  | "reaction.escalated"
  // Summary
  | "summary.all_complete";

Session Lifecycle Events

Events related to session creation, execution, and termination.
Trigger: Session enters spawning statusPriority: infoData:
{
  branch: string;
  issueId?: string;
  workspacePath: string;
}
Example:
{
  type: "session.spawned",
  message: "my-app-1: session created",
  data: {
    branch: "feat/INT-1234",
    issueId: "INT-1234",
    workspacePath: "/Users/foo/.agent-orchestrator/my-app/worktrees/my-app-1"
  }
}
Trigger: Session enters working statusPriority: infoData:
{
  oldStatus: SessionStatus;
  newStatus: "working";
}
Trigger: Agent process exits (activity state becomes exited)Priority: urgentData:
{
  lastActivityAt: Date;
  exitReason?: string;
}
Reaction: agent-exited - Notifies human immediately
Trigger: Session enters stuck status (agent idle for too long)Priority: urgentData:
{
  idleDurationMs: number;
  lastActivityAt: Date;
}
Reaction: agent-stuck - Notifies human after threshold (default: 10 minutes)
Trigger: Session enters needs_input status (agent asking a question)Priority: urgentData:
{
  prompt?: string;  // Agent's question if available
}
Reaction: agent-needs-input - Notifies human immediately
Trigger: Session enters errored statusPriority: urgentData:
{
  error: string;
  stackTrace?: string;
}

PR Lifecycle Events

Events related to pull request creation and status changes.
Trigger: Session enters pr_open statusPriority: infoData:
{
  prUrl: string;
  prNumber: number;
  branch: string;
  baseBranch: string;
}
Example:
{
  type: "pr.created",
  message: "my-app-1: PR #42 created",
  data: {
    prUrl: "https://github.com/org/repo/pull/42",
    prNumber: 42,
    branch: "feat/INT-1234",
    baseBranch: "main"
  }
}
Trigger: Session enters merged statusPriority: actionData:
{
  prUrl: string;
  mergedAt: Date;
  mergedBy?: string;
}
Trigger: PR is closed without mergingPriority: warningData:
{
  prUrl: string;
  closedAt: Date;
  reason?: string;
}

CI Events

Events related to continuous integration checks.
Trigger: Session enters ci_failed statusPriority: warningData:
{
  prUrl: string;
  failedChecks: string[];  // Names of failed checks
  checkUrl?: string;
}
Reaction: ci-failed - Sends message to agent with instructions to fixExample:
{
  type: "ci.failing",
  message: "my-app-1: CI checks failing",
  data: {
    prUrl: "https://github.com/org/repo/pull/42",
    failedChecks: ["test", "lint", "typecheck"],
    checkUrl: "https://github.com/org/repo/actions/runs/123"
  }
}
Trigger: All CI checks pass after previously failingPriority: infoData:
{
  prUrl: string;
  passedChecks: string[];
}

Review Events

Events related to code review activity.
Trigger: Session enters review_pending statusPriority: infoData:
{
  prUrl: string;
  reviewers: string[];
}
Trigger: Session enters approved statusPriority: actionData:
{
  prUrl: string;
  approver: string;
  approvedAt: Date;
}
Trigger: Session enters changes_requested statusPriority: warningData:
{
  prUrl: string;
  reviewer: string;
  comments: Array<{
    path: string;
    line: number;
    body: string;
  }>;
}
Reaction: changes-requested - Sends comments to agent with instructions to address

Merge Events

Events related to merge readiness and completion.
Trigger: Session enters mergeable status (approved + CI passing + no conflicts)Priority: actionData:
{
  prUrl: string;
  ciStatus: "passing";
  reviewDecision: "approved";
  conflictsResolved: true;
}
Reaction: approved-and-green - Notifies human that PR is ready to merge
Trigger: PR is successfully mergedPriority: actionData:
{
  prUrl: string;
  mergedAt: Date;
  mergedBy: string;
  method: "merge" | "squash" | "rebase";
}

Reaction Events

Events emitted by the reaction system itself.
Trigger: A reaction is executedPriority: info (or reaction’s configured priority)Data:
{
  reactionKey: string;
  action: "send-to-agent" | "notify" | "auto-merge";
  message?: string;
}
Trigger: A reaction escalates to human notification after retries/timeoutPriority: urgentData:
{
  reactionKey: string;
  attempts: number;
  reason: "max_retries" | "timeout";
}

Summary Events

Trigger: All sessions are in terminal states (merged/killed)Priority: infoData:
{
  totalSessions: number;
  merged: number;
  killed: number;
  summary: string;
}
Reaction: all-complete - Sends summary notification

Event Priorities

Events have one of four priority levels:
type EventPriority = "urgent" | "action" | "warning" | "info";

Priority Levels

urgent
EventPriority
Critical issues requiring immediate human attention.Examples:
  • Agent stuck or crashed
  • Agent asking a question (needs_input)
  • Session errored
  • Reaction escalated after max retries
Default routing: Desktop + Composio notifications
action
EventPriority
Actionable events that may require human decision.Examples:
  • PR approved and ready to merge
  • PR merged successfully
  • Review completed
Default routing: Desktop + Composio notifications
warning
EventPriority
Issues that may need attention but aren’t blocking.Examples:
  • CI checks failing (but reaction is auto-fixing)
  • Review changes requested (but reaction is handling)
  • Merge conflicts detected
Default routing: Composio notifications only
info
EventPriority
Informational updates about progress.Examples:
  • Session spawned
  • PR created
  • CI passing
  • All sessions complete
Default routing: Composio notifications only

Priority Inference

The lifecycle manager automatically infers priority from event type:
function inferPriority(type: EventType): EventPriority {
  if (type.includes('stuck') || type.includes('needs_input') || type.includes('errored')) {
    return 'urgent';
  }
  if (type.startsWith('summary.')) {
    return 'info';
  }
  if (type.includes('approved') || type.includes('ready') || type.includes('merged')) {
    return 'action';
  }
  if (type.includes('fail') || type.includes('changes_requested')) {
    return 'warning';
  }
  return 'info';
}

Event Creation

Events are created by the lifecycle manager using a factory function:
function createEvent(
  type: EventType,
  opts: {
    sessionId: SessionId;
    projectId: string;
    message: string;
    priority?: EventPriority;
    data?: Record<string, unknown>;
  }
): OrchestratorEvent {
  return {
    id: randomUUID(),
    type,
    priority: opts.priority ?? inferPriority(type),
    sessionId: opts.sessionId,
    projectId: opts.projectId,
    timestamp: new Date(),
    message: opts.message,
    data: opts.data ?? {},
  };
}
Example:
const event = createEvent('ci.failing', {
  sessionId: 'my-app-1',
  projectId: 'my-app',
  message: 'CI checks failing on PR #42',
  data: {
    prUrl: 'https://github.com/org/repo/pull/42',
    failedChecks: ['test', 'lint'],
  },
});

Event Routing

Events are routed to notifiers based on priority:
# agent-orchestrator.yaml
notificationRouting:
  urgent: ["desktop", "composio"]
  action: ["desktop", "composio"]
  warning: ["composio"]
  info: ["composio"]

Routing Logic

async function notifyHuman(event: OrchestratorEvent, priority: EventPriority): Promise<void> {
  const eventWithPriority = { ...event, priority };
  const notifierNames = config.notificationRouting[priority] ?? config.defaults.notifiers;

  for (const name of notifierNames) {
    const notifier = registry.get<Notifier>('notifier', name);
    if (notifier) {
      try {
        await notifier.notify(eventWithPriority);
      } catch {
        // Notifier failed - not much we can do
      }
    }
  }
}

Custom Routing

You can customize routing per priority level:
notificationRouting:
  urgent:
    - desktop      # OS notification
    - composio     # Composio dashboard
    - slack        # Slack message
  action:
    - desktop
    - composio
  warning:
    - slack        # Only Slack for warnings
  info:
    - composio     # Dashboard only for info

Event-Triggered Reactions

Events can trigger automated reactions. The lifecycle manager maps event types to reaction config keys:
function eventToReactionKey(eventType: EventType): string | null {
  switch (eventType) {
    case "ci.failing":
      return "ci-failed";
    case "review.changes_requested":
      return "changes-requested";
    case "automated_review.found":
      return "bugbot-comments";
    case "merge.conflicts":
      return "merge-conflicts";
    case "merge.ready":
      return "approved-and-green";
    case "session.stuck":
      return "agent-stuck";
    case "session.needs_input":
      return "agent-needs-input";
    case "session.killed":
      return "agent-exited";
    case "summary.all_complete":
      return "all-complete";
    default:
      return null;
  }
}
See Lifecycle Manager - Reaction System for details on how reactions work.

Complete Example

import { createLifecycleManager, createSessionManager, loadConfig, createPluginRegistry } from '@composio/ao-core';
import type { OrchestratorEvent } from '@composio/ao-core';

// Setup
const config = loadConfig();
const registry = createPluginRegistry();
await registry.loadBuiltins(config);

const sessionManager = createSessionManager({ config, registry });
const lifecycleManager = createLifecycleManager({
  config,
  registry,
  sessionManager,
});

// Custom event monitoring (hook into notifier)
class EventMonitorNotifier {
  name = 'event-monitor';
  
  async notify(event: OrchestratorEvent): Promise<void> {
    console.log('\n=== Event Received ===');
    console.log(`Type: ${event.type}`);
    console.log(`Priority: ${event.priority}`);
    console.log(`Session: ${event.sessionId}`);
    console.log(`Message: ${event.message}`);
    console.log(`Data:`, JSON.stringify(event.data, null, 2));
    
    // Log high-priority events to file
    if (event.priority === 'urgent' || event.priority === 'action') {
      await appendFile('important-events.log', 
        `${event.timestamp.toISOString()} [${event.priority}] ${event.message}\n`
      );
    }
  }
}

// Register custom notifier
registry.register({
  manifest: {
    name: 'event-monitor',
    slot: 'notifier',
    description: 'Event monitoring and logging',
    version: '1.0.0',
  },
  create: () => new EventMonitorNotifier(),
});

// Add to notification routing
config.notificationRouting.urgent.push('event-monitor');
config.notificationRouting.action.push('event-monitor');

// Start lifecycle manager
lifecycleManager.start();

// Spawn a session and watch events
const session = await sessionManager.spawn({
  projectId: 'my-app',
  issueId: 'INT-1234',
});

// Events will be logged as session progresses:
// - session.working
// - pr.created
// - ci.failing (if CI fails)
// - review.pending
// - review.approved
// - merge.ready
// - merge.completed

See Also

Build docs developers (and LLMs) love