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>
13 KiB
Events API Reference
Status: Proposed (Phase 2 Implementation) Last Updated: 2026-01-17
Complete reference for PCL Runtime event system.
Table of Contents
Overview
The PCL Runtime extends Node.js EventEmitter to provide lifecycle hooks for observability, logging, analytics, and integration.
import { PCLRuntime } from '@pcl/sdk';
const runtime = new PCLRuntime();
// Subscribe to events
runtime.on('persona:before', (persona, query) => {
console.log(`Executing: ${persona.id}`);
});
// Execute personas, workflows, etc.
await runtime.execute('ARCHI', 'Design system');
Event Emitter API
on(event, listener)
Subscribes to an event. Listener is called every time the event is emitted.
runtime.on(event: string, listener: (...args: any[]) => void): this
Example:
runtime.on('persona:before', (persona, query) => {
console.log(`Starting ${persona.id}`);
});
Returns: The runtime instance (for chaining).
once(event, listener)
Subscribes to an event, but automatically unsubscribes after first emission.
runtime.once(event: string, listener: (...args: any[]) => void): this
Example:
runtime.once('workflow:complete', (workflow, result) => {
console.log('First workflow completed!');
});
Returns: The runtime instance (for chaining).
off(event, listener)
Unsubscribes a listener from an event.
runtime.off(event: string, listener: (...args: any[]) => void): this
Example:
const listener = (persona) => console.log(persona.id);
runtime.on('persona:before', listener);
// Later...
runtime.off('persona:before', listener);
Returns: The runtime instance (for chaining).
removeAllListeners(event?)
Removes all listeners for a specific event, or all listeners if no event specified.
runtime.removeAllListeners(event?: string): this
Example:
// Remove all 'persona:before' listeners
runtime.removeAllListeners('persona:before');
// Remove ALL listeners
runtime.removeAllListeners();
Returns: The runtime instance (for chaining).
emit(event, ...args)
Emits an event (typically called internally by the runtime).
runtime.emit(event: string, ...args: any[]): boolean
Returns: true if event had listeners, false otherwise.
listenerCount(event)
Returns the number of listeners for a specific event.
runtime.listenerCount(event: string): number
Example:
const count = runtime.listenerCount('persona:before');
console.log(`${count} listeners registered`);
listeners(event)
Returns an array of listeners for a specific event.
runtime.listeners(event: string): Function[]
Example:
const listeners = runtime.listeners('persona:before');
console.log(`${listeners.length} listeners`);
Persona Events
persona:before
Emitted before a persona begins execution.
runtime.on('persona:before', (persona: Persona, query: string) => void)
Parameters:
persona: Persona- The persona being executedquery: string- The user query/prompt
Example:
runtime.on('persona:before', (persona, query) => {
console.log(`▶️ ${persona.id}: ${query}`);
console.log(`Skills: ${persona.skills.map((s) => s.id).join(', ')}`);
});
Use Cases:
- Logging execution start
- Metrics collection
- Request validation
- Rate limiting
persona:after
Emitted after a persona completes execution successfully.
runtime.on('persona:after', (persona: Persona, result: PersonaResult) => void)
Parameters:
persona: Persona- The executed personaresult: PersonaResult- Execution results
Result Type:
interface PersonaResult {
query: string; // Original query
response: string; // Persona's response
duration: number; // Execution time (ms)
tokens: {
input: number;
output: number;
total: number;
};
provider: string; // LLM provider used
model: string; // Model name
metadata: {
timestamp: Date;
cost?: number;
};
}
Example:
runtime.on('persona:after', (persona, result) => {
console.log(`✅ ${persona.id} completed in ${result.duration}ms`);
console.log(`Tokens: ${result.tokens.total}`);
console.log(`Response: ${result.response.substring(0, 100)}...`);
});
Use Cases:
- Logging results
- Cost tracking
- Database persistence
- Analytics
persona:error
Emitted when a persona execution fails.
runtime.on('persona:error', (persona: Persona, error: Error) => void)
Parameters:
persona: Persona- The persona that failederror: Error- The error that occurred
Example:
runtime.on('persona:error', (persona, error) => {
console.error(`❌ ${persona.id} failed: ${error.message}`);
console.error(error.stack);
// Send to error tracking
Sentry.captureException(error, {
tags: { personaId: persona.id },
});
});
Use Cases:
- Error logging
- Error tracking (Sentry, Rollbar)
- Retry logic
- Fallback strategies
Workflow Events
workflow:start
Emitted when a workflow begins execution.
runtime.on('workflow:start', (workflow: Workflow) => void)
Parameters:
workflow: Workflow- The workflow starting
Workflow Type:
interface Workflow {
id: string;
name: string;
steps: WorkflowStep[];
metadata: {
startTime: Date;
estimatedDuration?: number;
};
}
Example:
runtime.on('workflow:start', (workflow) => {
console.log(`🚀 Starting workflow: ${workflow.name}`);
console.log(`Steps: ${workflow.steps.length}`);
progressBar.start(workflow.steps.length, 0);
});
workflow:step
Emitted after each workflow step completes.
runtime.on('workflow:step', (step: WorkflowStep, result: any) => void)
Parameters:
step: WorkflowStep- The completed stepresult: any- Step execution result
Step Type:
interface WorkflowStep {
id: string;
personaId: string;
query: string;
dependencies: string[];
metadata: {
stepNumber: number;
totalSteps: number;
};
}
Example:
runtime.on('workflow:step', (step, result) => {
const progress = (step.metadata.stepNumber / step.metadata.totalSteps) * 100;
console.log(
`📍 Step ${step.metadata.stepNumber}/${step.metadata.totalSteps} (${progress.toFixed(0)}%)`
);
progressBar.increment();
});
workflow:complete
Emitted when a workflow completes successfully.
runtime.on('workflow:complete', (workflow: Workflow, result: WorkflowResult) => void)
Parameters:
workflow: Workflow- The completed workflowresult: WorkflowResult- Final results
Result Type:
interface WorkflowResult {
workflowId: string;
steps: StepResult[];
totalDuration: number;
totalTokens: number;
totalCost: number;
success: boolean;
metadata: {
startTime: Date;
endTime: Date;
};
}
Example:
runtime.on('workflow:complete', (workflow, result) => {
console.log(`🎉 Workflow complete: ${workflow.name}`);
console.log(`Duration: ${result.totalDuration}ms`);
console.log(`Total cost: $${result.totalCost.toFixed(4)}`);
progressBar.stop();
});
workflow:error
Emitted when a workflow fails.
runtime.on('workflow:error', (workflow: Workflow, error: Error) => void)
Parameters:
workflow: Workflow- The failed workflowerror: Error- The error that occurred
Example:
runtime.on('workflow:error', (workflow, error) => {
console.error(`❌ Workflow failed: ${workflow.name}`);
console.error(`Error: ${error.message}`);
progressBar.stop();
// Notify team
slack.send({
channel: '#alerts',
text: `Workflow ${workflow.name} failed: ${error.message}`,
});
});
LLM Events
llm:call
Emitted before making an LLM API call.
runtime.on('llm:call', (provider: string, prompt: string) => void)
Parameters:
provider: string- LLM provider name ('anthropic','openai', etc.)prompt: string- The prompt being sent
Example:
runtime.on('llm:call', (provider, prompt) => {
console.log(`📡 Calling ${provider}`);
console.log(`Prompt length: ${prompt.length} chars`);
});
llm:response
Emitted after receiving an LLM response.
runtime.on('llm:response', (provider: string, response: LLMResponse) => void)
Parameters:
provider: string- LLM provider nameresponse: LLMResponse- The response data
Response Type:
interface LLMResponse {
content: string;
model: string;
usage: {
input_tokens: number;
output_tokens: number;
total_tokens: number;
};
metadata: {
latency: number;
cost?: number;
};
}
Example:
runtime.on('llm:response', (provider, response) => {
console.log(`✅ ${provider} responded in ${response.metadata.latency}ms`);
console.log(`Tokens: ${response.usage.total_tokens}`);
// Track cost
const cost = calculateCost(provider, response.usage);
totalCost += cost;
});
llm:error
Emitted when an LLM call fails.
runtime.on('llm:error', (provider: string, error: Error) => void)
Parameters:
provider: string- LLM provider nameerror: Error- The error that occurred
Example:
runtime.on('llm:error', (provider, error) => {
console.error(`❌ ${provider} call failed: ${error.message}`);
if (error.message.includes('rate_limit')) {
console.log('⏳ Rate limited, retrying in 60s...');
}
});
Event Data Types
Complete Type Definitions
// Core types
interface Persona {
id: string;
name: string;
intent: string;
skills: Skill[];
extends?: string[];
implements?: string[];
metadata: Record<string, any>;
}
interface Skill {
id: string;
name: string;
type: 'foundation' | 'technical' | 'specialized';
description: string;
}
interface PersonaResult {
query: string;
response: string;
duration: number;
tokens: {
input: number;
output: number;
total: number;
};
provider: string;
model: string;
metadata: {
timestamp: Date;
cost?: number;
};
}
interface Workflow {
id: string;
name: string;
steps: WorkflowStep[];
metadata: {
startTime: Date;
estimatedDuration?: number;
};
}
interface WorkflowStep {
id: string;
personaId: string;
query: string;
dependencies: string[];
metadata: {
stepNumber: number;
totalSteps: number;
};
}
interface WorkflowResult {
workflowId: string;
steps: StepResult[];
totalDuration: number;
totalTokens: number;
totalCost: number;
success: boolean;
metadata: {
startTime: Date;
endTime: Date;
};
}
interface LLMResponse {
content: string;
model: string;
usage: {
input_tokens: number;
output_tokens: number;
total_tokens: number;
};
metadata: {
latency: number;
cost?: number;
};
}
// Event listener types
type PersonaBeforeListener = (persona: Persona, query: string) => void;
type PersonaAfterListener = (persona: Persona, result: PersonaResult) => void;
type PersonaErrorListener = (persona: Persona, error: Error) => void;
type WorkflowStartListener = (workflow: Workflow) => void;
type WorkflowStepListener = (step: WorkflowStep, result: any) => void;
type WorkflowCompleteListener = (
workflow: Workflow,
result: WorkflowResult
) => void;
type WorkflowErrorListener = (workflow: Workflow, error: Error) => void;
type LLMCallListener = (provider: string, prompt: string) => void;
type LLMResponseListener = (provider: string, response: LLMResponse) => void;
type LLMErrorListener = (provider: string, error: Error) => void;
// Complete event map
interface RuntimeEvents {
'persona:before': PersonaBeforeListener;
'persona:after': PersonaAfterListener;
'persona:error': PersonaErrorListener;
'workflow:start': WorkflowStartListener;
'workflow:step': WorkflowStepListener;
'workflow:complete': WorkflowCompleteListener;
'workflow:error': WorkflowErrorListener;
'llm:call': LLMCallListener;
'llm:response': LLMResponseListener;
'llm:error': LLMErrorListener;
}
See Also
Questions? Open an issue on GitHub