Files
personamanagmentlayer__pcl/docs/guides/EVENT-SYSTEM.md
ANGX fb8288d2e8 feat: implement Phase 1.0D workflow validation tests
Created comprehensive test suite for workflow validation features:

- 10/10 tests passing for implemented workflow validations
- Tests for unreachable code detection in conditionals
- Tests for parallel branch count limits
- Tests for complex workflow structures

Key additions:
- Fixed ParenthesizedExpression handling in evaluateConstantCondition()
- Created workflow-validation-core.test.mjs with 10 passing tests
- Tests verify rich error messages include suggestions

Test coverage:
✓ Unreachable else branch detection (condition always true)
✓ Unreachable then branch detection (condition always false)
✓ Dynamic conditions allowed without warnings
✓ Boolean expression constant evaluation
✓ Negation operator constant evaluation
✓ Parallel branch count warnings (>10 branches)
✓ No warnings for reasonable parallel branches (≤10)
✓ Nested grouped workflow validation
✓ Sequential then parallel workflow validation
✓ Choice workflow validation

Note: Loop validation tests deferred (awaiting parser implementation)

All workflow validation tests pass with proper error messages and suggestions.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 05:35:38 +01:00

12 KiB

PCL Event System Guide

Status: Proposed (Phase 2 Implementation) Last Updated: 2026-01-17

The PCL Event System provides observability hooks into the runtime, enabling logging, analytics, cost tracking, database integration, and error monitoring.


Table of Contents

  1. Quick Start
  2. Event Types
  3. Use Cases
  4. Best Practices
  5. API Reference
  6. Advanced Patterns

Quick Start

Basic Event Listener

import { PCLRuntime } from '@pcl/sdk';

const runtime = new PCLRuntime();

// Listen to persona execution
runtime.on('persona:before', (persona, query) => {
  console.log(`▶️ Executing ${persona.id}: ${query}`);
});

runtime.on('persona:after', (persona, result) => {
  console.log(`✅ Completed ${persona.id} in ${result.duration}ms`);
});

// Execute persona
await runtime.execute('ARCHI', 'Design a microservices architecture');

Output:

▶️ Executing ARCHI: Design a microservices architecture
✅ Completed ARCHI in 2340ms

Event Types

Persona Lifecycle

Event Parameters Description
persona:before (persona, query) Before persona executes
persona:after (persona, result) After persona completes
persona:error (persona, error) When persona fails

Workflow Lifecycle

Event Parameters Description
workflow:start (workflow) Workflow begins
workflow:step (step, result) Each workflow step
workflow:complete (workflow, result) Workflow completes
workflow:error (workflow, error) Workflow fails

LLM Integration

Event Parameters Description
llm:call (provider, prompt) Before LLM call
llm:response (provider, response) After LLM responds
llm:error (provider, error) LLM call fails

Use Cases

1. 📊 Logging & Observability

import { createLogger } from 'winston';

const logger = createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [new winston.transports.File({ filename: 'pcl-runtime.log' })],
});

runtime.on('persona:before', (persona, query) => {
  logger.info('persona_start', {
    personaId: persona.id,
    query: query,
    timestamp: new Date().toISOString(),
  });
});

runtime.on('persona:error', (persona, error) => {
  logger.error('persona_error', {
    personaId: persona.id,
    error: error.message,
    stack: error.stack,
  });
});

2. 📈 Analytics & Metrics

import { Analytics } from '@segment/analytics-node';

const analytics = new Analytics({ writeKey: 'YOUR_KEY' });

runtime.on('persona:after', (persona, result) => {
  analytics.track({
    userId: 'system',
    event: 'Persona Execution',
    properties: {
      personaId: persona.id,
      duration: result.duration,
      tokensUsed: result.tokens,
      success: true,
    },
  });
});

runtime.on('workflow:complete', (workflow, result) => {
  analytics.track({
    userId: 'system',
    event: 'Workflow Completion',
    properties: {
      workflowId: workflow.id,
      steps: workflow.steps.length,
      totalDuration: result.totalDuration,
      success: true,
    },
  });
});

3. 💰 Cost Tracking

class CostTracker {
  private totalCost = 0;

  private readonly costs = {
    'claude-3-sonnet': { input: 0.003, output: 0.015 }, // per 1K tokens
    'gpt-4': { input: 0.03, output: 0.06 },
  };

  attach(runtime: PCLRuntime) {
    runtime.on('llm:response', (provider, response) => {
      const rates = this.costs[provider];
      const cost =
        (response.usage.input_tokens / 1000) * rates.input +
        (response.usage.output_tokens / 1000) * rates.output;

      this.totalCost += cost;

      console.log(`💵 Call cost: $${cost.toFixed(4)}`);
      console.log(`💰 Total cost: $${this.totalCost.toFixed(4)}`);
    });
  }

  getTotal(): number {
    return this.totalCost;
  }
}

const tracker = new CostTracker();
tracker.attach(runtime);

4. 🗄️ Database Integration

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

runtime.on('persona:after', async (persona, result) => {
  await prisma.personaExecution.create({
    data: {
      personaId: persona.id,
      query: result.query,
      response: result.response,
      duration: result.duration,
      tokens: result.tokens,
      timestamp: new Date(),
    },
  });
});

runtime.on('workflow:complete', async (workflow, result) => {
  await prisma.workflowExecution.create({
    data: {
      workflowId: workflow.id,
      steps: workflow.steps.length,
      totalDuration: result.totalDuration,
      success: true,
      timestamp: new Date(),
    },
  });
});

5. 🚨 Error Tracking

import * as Sentry from '@sentry/node';

Sentry.init({ dsn: 'YOUR_DSN' });

runtime.on('persona:error', (persona, error) => {
  Sentry.captureException(error, {
    tags: {
      personaId: persona.id,
      component: 'persona-execution',
    },
    contexts: {
      persona: {
        id: persona.id,
        skills: persona.skills.map((s) => s.id),
      },
    },
  });
});

runtime.on('llm:error', (provider, error) => {
  Sentry.captureException(error, {
    tags: {
      provider: provider,
      component: 'llm-integration',
    },
  });
});

6. 📊 Progress Tracking

import cliProgress from 'cli-progress';

const progressBar = new cliProgress.SingleBar(
  {},
  cliProgress.Presets.shades_classic
);

runtime.on('workflow:start', (workflow) => {
  progressBar.start(workflow.steps.length, 0);
  console.log(`🚀 Starting workflow: ${workflow.id}`);
});

runtime.on('workflow:step', (step, result) => {
  progressBar.increment();
  console.log(`✅ Completed step: ${step.id}`);
});

runtime.on('workflow:complete', (workflow, result) => {
  progressBar.stop();
  console.log(`🎉 Workflow complete: ${workflow.id}`);
});

Best Practices

✅ DO

1. Keep listeners lightweight

// ✅ GOOD: Fast, non-blocking
runtime.on('persona:before', (persona) => {
  console.log(`Starting ${persona.id}`);
});

2. Handle errors in async listeners

// ✅ GOOD: Error handling
runtime.on('persona:after', async (persona, result) => {
  try {
    await db.save(result);
  } catch (error) {
    console.error('Failed to save result:', error);
  }
});

3. Unsubscribe when done

// ✅ GOOD: Cleanup
const listener = (persona) => console.log(persona.id);
runtime.on('persona:before', listener);

// Later...
runtime.off('persona:before', listener);

4. Use once() for one-time events

// ✅ GOOD: Auto-cleanup
runtime.once('workflow:complete', (workflow) => {
  console.log('First workflow done!');
});

❌ DON'T

1. Block event loop

// ❌ BAD: Synchronous heavy computation
runtime.on('persona:after', (persona, result) => {
  const hash = expensiveSyncHash(result.response); // Blocks!
});

// ✅ GOOD: Offload to worker thread
runtime.on('persona:after', async (persona, result) => {
  await workerPool.hash(result.response);
});

2. Throw unhandled errors

// ❌ BAD: Crashes runtime
runtime.on('persona:after', (persona, result) => {
  JSON.parse(result.invalidJson); // Throws!
});

// ✅ GOOD: Catch errors
runtime.on('persona:after', (persona, result) => {
  try {
    JSON.parse(result.maybeJson);
  } catch (error) {
    console.error('Parse failed:', error);
  }
});

3. Create memory leaks

// ❌ BAD: Listener never cleaned up
function setupTracking() {
  const tracker = new Tracker();
  runtime.on('persona:after', (persona) => {
    tracker.record(persona); // `tracker` never freed!
  });
}

// ✅ GOOD: Explicit cleanup
function setupTracking() {
  const tracker = new Tracker();
  const listener = (persona) => tracker.record(persona);

  runtime.on('persona:after', listener);

  return () => {
    runtime.off('persona:after', listener);
    tracker.dispose();
  };
}

API Reference

runtime.on(event, listener)

Subscribe to an event.

runtime.on('persona:before', (persona, query) => {
  // Handle event
});

runtime.once(event, listener)

Subscribe to event, auto-unsubscribe after first emission.

runtime.once('workflow:complete', (workflow) => {
  console.log('First workflow done!');
});

runtime.off(event, listener)

Unsubscribe from event.

const listener = (persona) => console.log(persona.id);
runtime.on('persona:before', listener);
runtime.off('persona:before', listener);

runtime.removeAllListeners(event?)

Remove all listeners for an event (or all events if no event specified).

runtime.removeAllListeners('persona:before'); // Remove all 'persona:before' listeners
runtime.removeAllListeners(); // Remove ALL listeners

Advanced Patterns

Composable Event Handlers

class EventHandler {
  attach(runtime: PCLRuntime) {
    throw new Error('Not implemented');
  }
}

class LoggingHandler extends EventHandler {
  attach(runtime: PCLRuntime) {
    runtime.on('persona:before', (persona) => {
      console.log(`Starting ${persona.id}`);
    });
  }
}

class CostTrackingHandler extends EventHandler {
  private cost = 0;

  attach(runtime: PCLRuntime) {
    runtime.on('llm:response', (provider, response) => {
      this.cost += calculateCost(provider, response);
    });
  }
}

// Compose multiple handlers
const handlers = [new LoggingHandler(), new CostTrackingHandler()];

handlers.forEach((h) => h.attach(runtime));

Event Middleware Chain

class EventMiddleware {
  private middlewares: Function[] = [];

  use(fn: Function) {
    this.middlewares.push(fn);
  }

  async execute(event: string, ...args: any[]) {
    for (const mw of this.middlewares) {
      await mw(event, ...args);
    }
  }
}

const middleware = new EventMiddleware();

middleware.use(async (event, ...args) => {
  console.log(`Event: ${event}`);
});

middleware.use(async (event, ...args) => {
  await logToDatabase(event, args);
});

runtime.on('persona:after', (...args) => {
  middleware.execute('persona:after', ...args);
});

Migration Guide

From Claude Code Hooks (If Applicable)

Before (Shell Commands):

{
  "hooks": {
    "on_persona_start": "echo 'Starting persona'",
    "on_persona_end": "notify-send 'Persona complete'"
  }
}

After (Event Listeners):

runtime.on('persona:before', (persona) => {
  console.log('Starting persona');
});

runtime.on('persona:after', (persona) => {
  notify('Persona complete');
});

Troubleshooting

Events not firing?

  1. Check event name spelling
  2. Verify listener is attached before execution
  3. Check for errors in listener (use try/catch)

Memory leaks?

  1. Always clean up listeners with off() or removeAllListeners()
  2. Use once() for one-time events
  3. Avoid closures that capture large objects

Performance issues?

  1. Profile listeners with console.time()
  2. Offload heavy work to worker threads
  3. Batch database writes instead of per-event

See Also


Questions? Open an issue on GitHub