mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
372 lines
9.2 KiB
TypeScript
372 lines
9.2 KiB
TypeScript
/**
|
|
* Event System for Agent State Changes
|
|
*
|
|
* This module provides a strongly-typed event emitter specifically designed
|
|
* for tracking agent state changes. It uses Node.js EventEmitter under the hood
|
|
* but provides type safety for all events.
|
|
*/
|
|
|
|
import { EventEmitter } from "events"
|
|
|
|
import { ClineMessage, ClineAsk } from "@roo-code/types"
|
|
|
|
import type { AgentStateInfo } from "./agent-state.js"
|
|
|
|
// =============================================================================
|
|
// Event Types
|
|
// =============================================================================
|
|
|
|
/**
|
|
* All events that can be emitted by the client.
|
|
*
|
|
* Design note: We use a string literal union type for event names to ensure
|
|
* type safety when subscribing to events. The payload type is determined by
|
|
* the event name.
|
|
*/
|
|
export interface ClientEventMap {
|
|
/**
|
|
* Emitted whenever the agent state changes.
|
|
* This is the primary event for tracking state.
|
|
*/
|
|
stateChange: AgentStateChangeEvent
|
|
|
|
/**
|
|
* Emitted when a new message is added to the message list.
|
|
*/
|
|
message: ClineMessage
|
|
|
|
/**
|
|
* Emitted when an existing message is updated (e.g., partial -> complete).
|
|
*/
|
|
messageUpdated: ClineMessage
|
|
|
|
/**
|
|
* Emitted when the agent starts waiting for user input.
|
|
* Convenience event - you can also use stateChange.
|
|
*/
|
|
waitingForInput: WaitingForInputEvent
|
|
|
|
/**
|
|
* Emitted when the agent stops waiting and resumes running.
|
|
*/
|
|
resumedRunning: void
|
|
|
|
/**
|
|
* Emitted when the agent starts streaming content.
|
|
*/
|
|
streamingStarted: void
|
|
|
|
/**
|
|
* Emitted when streaming ends.
|
|
*/
|
|
streamingEnded: void
|
|
|
|
/**
|
|
* Emitted when a task completes (either successfully or with error).
|
|
*/
|
|
taskCompleted: TaskCompletedEvent
|
|
|
|
/**
|
|
* Emitted when a task is cleared/cancelled.
|
|
*/
|
|
taskCleared: void
|
|
|
|
/**
|
|
* Emitted when the current mode changes.
|
|
*/
|
|
modeChanged: ModeChangedEvent
|
|
|
|
/**
|
|
* Emitted on any error during message processing.
|
|
*/
|
|
error: Error
|
|
}
|
|
|
|
/**
|
|
* Event payload for state changes.
|
|
*/
|
|
export interface AgentStateChangeEvent {
|
|
/** The previous state info */
|
|
previousState: AgentStateInfo
|
|
/** The new/current state info */
|
|
currentState: AgentStateInfo
|
|
/** Whether this is a significant state transition (state enum changed) */
|
|
isSignificantChange: boolean
|
|
}
|
|
|
|
/**
|
|
* Event payload when agent starts waiting for input.
|
|
*/
|
|
export interface WaitingForInputEvent {
|
|
/** The specific ask type */
|
|
ask: ClineAsk
|
|
/** Full state info for context */
|
|
stateInfo: AgentStateInfo
|
|
/** The message that triggered this wait */
|
|
message: ClineMessage
|
|
}
|
|
|
|
/**
|
|
* Event payload when a task completes.
|
|
*/
|
|
export interface TaskCompletedEvent {
|
|
/** Whether the task completed successfully */
|
|
success: boolean
|
|
/** The final state info */
|
|
stateInfo: AgentStateInfo
|
|
/** The completion message if available */
|
|
message?: ClineMessage
|
|
}
|
|
|
|
/**
|
|
* Event payload when mode changes.
|
|
*/
|
|
export interface ModeChangedEvent {
|
|
/** The previous mode (undefined if first mode set) */
|
|
previousMode: string | undefined
|
|
/** The new/current mode */
|
|
currentMode: string
|
|
}
|
|
|
|
// =============================================================================
|
|
// Typed Event Emitter
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Type-safe event emitter for client events.
|
|
*
|
|
* Usage:
|
|
* ```typescript
|
|
* const emitter = new TypedEventEmitter()
|
|
*
|
|
* // Type-safe subscription
|
|
* emitter.on('stateChange', (event) => {
|
|
* console.log(event.currentState) // TypeScript knows this is AgentStateChangeEvent
|
|
* })
|
|
*
|
|
* // Type-safe emission
|
|
* emitter.emit('stateChange', { previousState, currentState, isSignificantChange })
|
|
* ```
|
|
*/
|
|
export class TypedEventEmitter {
|
|
private emitter = new EventEmitter()
|
|
|
|
/**
|
|
* Subscribe to an event.
|
|
*
|
|
* @param event - The event name
|
|
* @param listener - The callback function
|
|
* @returns Function to unsubscribe
|
|
*/
|
|
on<K extends keyof ClientEventMap>(event: K, listener: (payload: ClientEventMap[K]) => void): () => void {
|
|
this.emitter.on(event, listener)
|
|
return () => this.emitter.off(event, listener)
|
|
}
|
|
|
|
/**
|
|
* Subscribe to an event, but only once.
|
|
*
|
|
* @param event - The event name
|
|
* @param listener - The callback function
|
|
*/
|
|
once<K extends keyof ClientEventMap>(event: K, listener: (payload: ClientEventMap[K]) => void): void {
|
|
this.emitter.once(event, listener)
|
|
}
|
|
|
|
/**
|
|
* Unsubscribe from an event.
|
|
*
|
|
* @param event - The event name
|
|
* @param listener - The callback function to remove
|
|
*/
|
|
off<K extends keyof ClientEventMap>(event: K, listener: (payload: ClientEventMap[K]) => void): void {
|
|
this.emitter.off(event, listener)
|
|
}
|
|
|
|
/**
|
|
* Emit an event.
|
|
*
|
|
* @param event - The event name
|
|
* @param payload - The event payload
|
|
*/
|
|
emit<K extends keyof ClientEventMap>(event: K, payload: ClientEventMap[K]): void {
|
|
this.emitter.emit(event, payload)
|
|
}
|
|
|
|
/**
|
|
* Remove all listeners for an event, or all events.
|
|
*
|
|
* @param event - Optional event name. If not provided, removes all listeners.
|
|
*/
|
|
removeAllListeners<K extends keyof ClientEventMap>(event?: K): void {
|
|
if (event) {
|
|
this.emitter.removeAllListeners(event)
|
|
} else {
|
|
this.emitter.removeAllListeners()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the number of listeners for an event.
|
|
*/
|
|
listenerCount<K extends keyof ClientEventMap>(event: K): number {
|
|
return this.emitter.listenerCount(event)
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// State Change Detector
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Helper to determine if a state change is "significant".
|
|
*
|
|
* A significant change is when the AgentLoopState enum value changes,
|
|
* as opposed to just internal state updates within the same state.
|
|
*/
|
|
export function isSignificantStateChange(previous: AgentStateInfo, current: AgentStateInfo): boolean {
|
|
return previous.state !== current.state
|
|
}
|
|
|
|
/**
|
|
* Helper to determine if we transitioned to waiting for input.
|
|
*/
|
|
export function transitionedToWaiting(previous: AgentStateInfo, current: AgentStateInfo): boolean {
|
|
return !previous.isWaitingForInput && current.isWaitingForInput
|
|
}
|
|
|
|
/**
|
|
* Helper to determine if we transitioned from waiting to running.
|
|
*/
|
|
export function transitionedToRunning(previous: AgentStateInfo, current: AgentStateInfo): boolean {
|
|
return previous.isWaitingForInput && !current.isWaitingForInput && current.isRunning
|
|
}
|
|
|
|
/**
|
|
* Helper to determine if streaming started.
|
|
*/
|
|
export function streamingStarted(previous: AgentStateInfo, current: AgentStateInfo): boolean {
|
|
return !previous.isStreaming && current.isStreaming
|
|
}
|
|
|
|
/**
|
|
* Helper to determine if streaming ended.
|
|
*/
|
|
export function streamingEnded(previous: AgentStateInfo, current: AgentStateInfo): boolean {
|
|
return previous.isStreaming && !current.isStreaming
|
|
}
|
|
|
|
/**
|
|
* Helper to determine if task completed.
|
|
*/
|
|
export function taskCompleted(previous: AgentStateInfo, current: AgentStateInfo): boolean {
|
|
const completionAsks = ["completion_result", "resume_completed_task"]
|
|
const wasNotComplete = !previous.currentAsk || !completionAsks.includes(previous.currentAsk)
|
|
const isNowComplete = current.currentAsk !== undefined && completionAsks.includes(current.currentAsk)
|
|
return wasNotComplete && isNowComplete
|
|
}
|
|
|
|
// =============================================================================
|
|
// Observable Pattern (Alternative API)
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Subscription function type for observable pattern.
|
|
*/
|
|
export type Observer<T> = (value: T) => void
|
|
|
|
/**
|
|
* Unsubscribe function type.
|
|
*/
|
|
export type Unsubscribe = () => void
|
|
|
|
/**
|
|
* Simple observable for state.
|
|
*
|
|
* This provides an alternative to the event emitter pattern
|
|
* for those who prefer a more functional approach.
|
|
*
|
|
* Usage:
|
|
* ```typescript
|
|
* const stateObservable = new Observable<AgentStateInfo>()
|
|
*
|
|
* const unsubscribe = stateObservable.subscribe((state) => {
|
|
* console.log('New state:', state)
|
|
* })
|
|
*
|
|
* // Later...
|
|
* unsubscribe()
|
|
* ```
|
|
*/
|
|
export class Observable<T> {
|
|
private observers: Set<Observer<T>> = new Set()
|
|
private currentValue: T | undefined
|
|
|
|
/**
|
|
* Create an observable with an optional initial value.
|
|
*/
|
|
constructor(initialValue?: T) {
|
|
this.currentValue = initialValue
|
|
}
|
|
|
|
/**
|
|
* Subscribe to value changes.
|
|
*
|
|
* @param observer - Function called when value changes
|
|
* @returns Unsubscribe function
|
|
*/
|
|
subscribe(observer: Observer<T>): Unsubscribe {
|
|
this.observers.add(observer)
|
|
|
|
// Immediately emit current value if we have one
|
|
if (this.currentValue !== undefined) {
|
|
observer(this.currentValue)
|
|
}
|
|
|
|
return () => {
|
|
this.observers.delete(observer)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the value and notify all subscribers.
|
|
*/
|
|
next(value: T): void {
|
|
this.currentValue = value
|
|
for (const observer of this.observers) {
|
|
try {
|
|
observer(value)
|
|
} catch (error) {
|
|
console.error("Error in observer:", error)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the current value without subscribing.
|
|
*/
|
|
getValue(): T | undefined {
|
|
return this.currentValue
|
|
}
|
|
|
|
/**
|
|
* Check if there are any subscribers.
|
|
*/
|
|
hasSubscribers(): boolean {
|
|
return this.observers.size > 0
|
|
}
|
|
|
|
/**
|
|
* Get the number of subscribers.
|
|
*/
|
|
getSubscriberCount(): number {
|
|
return this.observers.size
|
|
}
|
|
|
|
/**
|
|
* Remove all subscribers.
|
|
*/
|
|
clear(): void {
|
|
this.observers.clear()
|
|
}
|
|
}
|