# Events & Lifecycle

Lifecycle hooks for monitoring and extending agent behavior

Events give you lifecycle hooks for observability and lightweight coordination. Use them for logging, metrics, analytics, and cleanup that should stay close to the runtime.

## Agent Events

Track individual agent execution with `started`, `completed`, and `errored` events:

```typescript
import { createAgent } from '@agentuity/runtime';
import { s } from '@agentuity/schema';

const agent = createAgent('TaskProcessor', {
  schema: {
    input: s.object({ task: s.string() }),
    output: s.object({ result: s.string() }),
  },
  handler: async (ctx, input) => {
    ctx.logger.info('Processing task', { task: input.task });
    return { result: `Completed: ${input.task}` };
  },
});

// Track execution timing
agent.addEventListener('started', (event, agent, ctx) => {
  ctx.state.set('startTime', Date.now());
  ctx.logger.info('Agent started', { agent: agent.metadata.name });
});

agent.addEventListener('completed', (event, agent, ctx) => {
  const startTime = ctx.state.get('startTime') as number;
  const duration = Date.now() - startTime;

  ctx.logger.info('Agent completed', {
    agent: agent.metadata.name,
    durationMs: duration,
  });

  // Warn on slow executions
  if (duration > 1000) {
    ctx.logger.warn('Slow execution detected', { duration, threshold: 1000 });
  }
});

agent.addEventListener('errored', (event, agent, ctx, error) => {
  const startTime = ctx.state.get('startTime') as number;
  const duration = Date.now() - startTime;

  ctx.logger.error('Agent failed', {
    agent: agent.metadata.name,
    error: error.message,
    durationMs: duration,
  });
});

export default agent;
```

Agent listeners receive the event name, agent instance, context, and for `errored`, the thrown error.

## App-Level Events

Monitor all agents, sessions, and threads globally with the top-level `addEventListener()` function. Register these listeners at module scope during startup.

```typescript
import { addEventListener } from '@agentuity/runtime';

// Track all agent executions
addEventListener('agent.started', (event, agent, ctx) => {
  ctx.logger.info('Agent execution started', {
    agent: agent.metadata.name,
    sessionId: ctx.sessionId,
  });
});

addEventListener('agent.completed', (event, agent, ctx) => {
  ctx.logger.info('Agent execution completed', {
    agent: agent.metadata.name,
    sessionId: ctx.sessionId,
  });
});

addEventListener('agent.errored', (event, agent, ctx, error) => {
  ctx.logger.error('Agent execution failed', {
    agent: agent.metadata.name,
    error: error.message,
    sessionId: ctx.sessionId,
  });
});
```

Agent events receive `(eventName, agent, ctx)` or `(eventName, agent, ctx, error)`. Session and thread events receive `(eventName, session)` or `(eventName, thread)`.

### Available App Events

| Event | Description |
|-------|-------------|
| `agent.started` | Any agent starts execution |
| `agent.completed` | Any agent completes successfully |
| `agent.errored` | Any agent throws an error |
| `session.started` | New session begins |
| `session.completed` | Session ends |
| `thread.created` | New thread created |
| `thread.destroyed` | Thread is explicitly destroyed or removed by the active provider |

## App Startup and Shutdown

For shared resources, prefer module initialization plus `registerShutdownHook()`:

```typescript
import { createApp, registerShutdownHook } from '@agentuity/runtime';
import api from './src/api/index';
import agents from './src/agent';
import { db } from './db';

registerShutdownHook(async () => {
  await db.close();
});

export default await createApp({
  router: { path: '/api', router: api },
  agents,
});
```

> [!NOTE]
> **Current Guidance**
> Agent-level `setup` and `shutdown` are still useful for agent-local config exposed through `ctx.config`. For shared app-wide dependencies, the current stable path is module-scoped initialization plus `registerShutdownHook()`, not new examples built around `ctx.app`.

## Agent Lifecycle Hooks

Individual agents can also define `setup` and `shutdown` functions for agent-specific initialization:

```typescript
import { createAgent } from '@agentuity/runtime';
import { s } from '@agentuity/schema';

const agent = createAgent('CachedLookup', {
  schema: {
    input: s.object({ key: s.string() }),
    output: s.object({ value: s.string() }),
  },
  // Called once when the app starts. Return value becomes ctx.config.
  setup: async () => {
    const cache = new Map<string, string>();
    return { cache };
  },
  // Called when the app shuts down.
  shutdown: async (_app, config) => {
    config.cache.clear();
  },
  handler: async (ctx, input) => {
    // ctx.config is typed from setup's return value
    const cached = ctx.config.cache.get(input.key);
    if (cached) {
      return { value: cached };
    }

    // Fetch and cache
    const value = await fetchValue(input.key);
    ctx.config.cache.set(input.key, value);
    return { value };
  },
});

export default agent;
```

`setup` returns agent-specific config, exposed via `ctx.config`. Use it for agent-local caches, clients, or precomputed data that should stay scoped to that agent.

> [!TIP]
> **When to Use Module Setup vs Agent Setup**
> - **Module setup**: Shared resources like database pools, Redis clients, or service singletons
> - **Agent setup**: Agent-local caches, preloaded models, or isolated clients exposed through `ctx.config`

## Shared State

Event handlers on the same request can share data through `ctx.state`:

```typescript
agent.addEventListener('started', (event, agent, ctx) => {
  ctx.state.set('startTime', Date.now());
  ctx.state.set('metadata', { userId: '123', source: 'api' });
});

agent.addEventListener('completed', (event, agent, ctx) => {
  const startTime = ctx.state.get('startTime') as number;
  const metadata = ctx.state.get('metadata') as Record<string, string>;

  ctx.logger.info('Execution complete', {
    duration: Date.now() - startTime,
    ...metadata,
  });
});
```

> [!TIP]
> **Background Work**
> Use `ctx.waitUntil()` in event handlers for non-blocking operations like sending metrics to external services:
>
> ```typescript
> agent.addEventListener('completed', (event, agent, ctx) => {
>   ctx.waitUntil(async () => {
>     await sendMetricsToExternalService({ agent: agent.metadata.name });
>   });
> });
> ```

## Events and Output Review

Use events for observability, logging, and side effects that belong close to execution. When you need a separate quality check after a response is complete, use a pattern like [LLM as a Judge](/cookbook/patterns/llm-as-a-judge).

## Next Steps

- [LLM as a Judge](/cookbook/patterns/llm-as-a-judge): Add post-response quality checks
- [State Management](/agents/state-management): Thread and session state patterns
- [Calling Other Agents](/agents/calling-other-agents): Multi-agent coordination
- [Logging](/services/observability/logging): Structured logging for debugging events
- [Tracing](/services/observability/tracing): Track timing across event handlers