2025-10-23 14:14:14 -04:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { EventEmitter } from 'node:events';
|
2025-11-07 09:07:25 -08:00
|
|
|
import type { LoadServerHierarchicalMemoryResponse } from './memoryDiscovery.js';
|
2025-10-23 14:14:14 -04:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Defines the severity level for user-facing feedback.
|
|
|
|
|
* This maps loosely to UI `MessageType`
|
|
|
|
|
*/
|
|
|
|
|
export type FeedbackSeverity = 'info' | 'warning' | 'error';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Payload for the 'user-feedback' event.
|
|
|
|
|
*/
|
|
|
|
|
export interface UserFeedbackPayload {
|
|
|
|
|
/**
|
|
|
|
|
* The severity level determines how the message is rendered in the UI
|
|
|
|
|
* (e.g. colored text, specific icon).
|
|
|
|
|
*/
|
|
|
|
|
severity: FeedbackSeverity;
|
|
|
|
|
/**
|
|
|
|
|
* The main message to display to the user in the chat history or stdout.
|
|
|
|
|
*/
|
|
|
|
|
message: string;
|
|
|
|
|
/**
|
|
|
|
|
* The original error object, if applicable.
|
|
|
|
|
* Listeners can use this to extract stack traces for debug logging
|
|
|
|
|
* or verbose output, while keeping the 'message' field clean for end users.
|
|
|
|
|
*/
|
|
|
|
|
error?: unknown;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-27 15:33:12 -07:00
|
|
|
/**
|
|
|
|
|
* Payload for the 'fallback-mode-changed' event.
|
|
|
|
|
*/
|
|
|
|
|
export interface FallbackModeChangedPayload {
|
|
|
|
|
/**
|
|
|
|
|
* Whether fallback mode is now active.
|
|
|
|
|
*/
|
|
|
|
|
isInFallbackMode: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-03 14:59:51 -05:00
|
|
|
/**
|
|
|
|
|
* Payload for the 'model-changed' event.
|
|
|
|
|
*/
|
|
|
|
|
export interface ModelChangedPayload {
|
|
|
|
|
/**
|
|
|
|
|
* The new model that was set.
|
|
|
|
|
*/
|
|
|
|
|
model: string;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-07 09:07:25 -08:00
|
|
|
/**
|
|
|
|
|
* Payload for the 'memory-changed' event.
|
|
|
|
|
*/
|
|
|
|
|
export type MemoryChangedPayload = LoadServerHierarchicalMemoryResponse;
|
|
|
|
|
|
2025-10-23 14:14:14 -04:00
|
|
|
export enum CoreEvent {
|
|
|
|
|
UserFeedback = 'user-feedback',
|
2025-10-27 15:33:12 -07:00
|
|
|
FallbackModeChanged = 'fallback-mode-changed',
|
2025-11-03 14:59:51 -05:00
|
|
|
ModelChanged = 'model-changed',
|
2025-11-07 09:07:25 -08:00
|
|
|
MemoryChanged = 'memory-changed',
|
2025-10-23 14:14:14 -04:00
|
|
|
}
|
|
|
|
|
|
2025-11-07 09:07:25 -08:00
|
|
|
export interface CoreEvents {
|
|
|
|
|
[CoreEvent.UserFeedback]: [UserFeedbackPayload];
|
|
|
|
|
[CoreEvent.FallbackModeChanged]: [FallbackModeChangedPayload];
|
|
|
|
|
[CoreEvent.ModelChanged]: [ModelChangedPayload];
|
|
|
|
|
[CoreEvent.MemoryChanged]: [MemoryChangedPayload];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class CoreEventEmitter extends EventEmitter<CoreEvents> {
|
2025-10-23 14:14:14 -04:00
|
|
|
private _feedbackBacklog: UserFeedbackPayload[] = [];
|
|
|
|
|
private static readonly MAX_BACKLOG_SIZE = 10000;
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sends actionable feedback to the user.
|
|
|
|
|
* Buffers automatically if the UI hasn't subscribed yet.
|
|
|
|
|
*/
|
|
|
|
|
emitFeedback(
|
|
|
|
|
severity: FeedbackSeverity,
|
|
|
|
|
message: string,
|
|
|
|
|
error?: unknown,
|
|
|
|
|
): void {
|
|
|
|
|
const payload: UserFeedbackPayload = { severity, message, error };
|
|
|
|
|
|
|
|
|
|
if (this.listenerCount(CoreEvent.UserFeedback) === 0) {
|
|
|
|
|
if (this._feedbackBacklog.length >= CoreEventEmitter.MAX_BACKLOG_SIZE) {
|
|
|
|
|
this._feedbackBacklog.shift();
|
|
|
|
|
}
|
|
|
|
|
this._feedbackBacklog.push(payload);
|
|
|
|
|
} else {
|
|
|
|
|
this.emit(CoreEvent.UserFeedback, payload);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-27 15:33:12 -07:00
|
|
|
/**
|
|
|
|
|
* Notifies subscribers that fallback mode has changed.
|
|
|
|
|
* This is synchronous and doesn't use backlog (UI should already be initialized).
|
|
|
|
|
*/
|
|
|
|
|
emitFallbackModeChanged(isInFallbackMode: boolean): void {
|
|
|
|
|
const payload: FallbackModeChangedPayload = { isInFallbackMode };
|
|
|
|
|
this.emit(CoreEvent.FallbackModeChanged, payload);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-03 14:59:51 -05:00
|
|
|
/**
|
|
|
|
|
* Notifies subscribers that the model has changed.
|
|
|
|
|
*/
|
|
|
|
|
emitModelChanged(model: string): void {
|
|
|
|
|
const payload: ModelChangedPayload = { model };
|
|
|
|
|
this.emit(CoreEvent.ModelChanged, payload);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-23 14:14:14 -04:00
|
|
|
/**
|
|
|
|
|
* Flushes buffered messages. Call this immediately after primary UI listener
|
|
|
|
|
* subscribes.
|
|
|
|
|
*/
|
|
|
|
|
drainFeedbackBacklog(): void {
|
|
|
|
|
const backlog = [...this._feedbackBacklog];
|
|
|
|
|
this._feedbackBacklog.length = 0; // Clear in-place
|
|
|
|
|
for (const payload of backlog) {
|
|
|
|
|
this.emit(CoreEvent.UserFeedback, payload);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const coreEvents = new CoreEventEmitter();
|