diff --git a/packages/agent-core-v2/src/background/background.ts b/packages/agent-core-v2/src/background/background.ts index a4a34fa26..95515be30 100644 --- a/packages/agent-core-v2/src/background/background.ts +++ b/packages/agent-core-v2/src/background/background.ts @@ -85,6 +85,5 @@ export interface IBackgroundService { ): Promise; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IBackgroundService = createDecorator('agentBackgroundService'); diff --git a/packages/agent-core-v2/src/background/backgroundService.ts b/packages/agent-core-v2/src/background/backgroundService.ts index 534b315ce..d6e0e3dba 100644 --- a/packages/agent-core-v2/src/background/backgroundService.ts +++ b/packages/agent-core-v2/src/background/backgroundService.ts @@ -18,7 +18,7 @@ import { } from './task'; import { IContextMemory } from '#/contextMemory'; -import { IEventBus } from '#/eventBus'; +import { IEventSink } from '../eventSink'; import { IExternalHooksService } from '#/externalHooks'; import { IPromptService } from '#/prompt'; import { ITelemetryService } from '#/telemetry'; @@ -109,7 +109,7 @@ export class BackgroundService extends Disposable implements IBackgroundService constructor( options: BackgroundServiceOptions = {}, - @IEventBus private readonly events: IEventBus, + @IEventSink private readonly events: IEventSink, @IWireRecord private readonly wireRecord: IWireRecord, @ITelemetryService private readonly telemetry: ITelemetryService, @IPromptService private readonly prompt: IPromptService, @@ -667,7 +667,7 @@ export class BackgroundService extends Disposable implements IBackgroundService private async restoreBackgroundTaskNotification(info: BackgroundTaskInfo): Promise { const context = await this.buildBackgroundTaskNotificationContext(info); if (context === undefined) return; - this.context.spliceHistory(this.context.getHistory().length, 0, [ + this.context.splice(this.context.get().length, 0, [ { role: 'user', content: [...context.content], @@ -744,7 +744,7 @@ export class BackgroundService extends Disposable implements IBackgroundService } private hasDeliveredNotification(key: string): boolean { - return this.context.getHistory().some((message) => { + return this.context.get().some((message) => { return message.origin?.kind === 'background_task' && notificationKey(message.origin) === key; }); } diff --git a/packages/agent-core-v2/src/blobStore/blobStore.ts b/packages/agent-core-v2/src/blobStore/blobStore.ts index 8047b819e..ff999f5d9 100644 --- a/packages/agent-core-v2/src/blobStore/blobStore.ts +++ b/packages/agent-core-v2/src/blobStore/blobStore.ts @@ -17,7 +17,6 @@ export interface IBlobStoreService { isBlobRef(url: string): boolean; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IBlobStoreService = createDecorator( 'agentBlobStoreService', ); diff --git a/packages/agent-core-v2/src/contextInjector/contextInjector.ts b/packages/agent-core-v2/src/contextInjector/contextInjector.ts new file mode 100644 index 000000000..93bc632bf --- /dev/null +++ b/packages/agent-core-v2/src/contextInjector/contextInjector.ts @@ -0,0 +1,26 @@ +import { createDecorator } from "#/_base/di"; +import type { IDisposable } from "#/_base/di"; + +export interface ContextInjectionContext { + readonly lastInjectedAt: number | null; +} + +export interface ContextInjectionOptions { + readonly cadence?: 'step' | 'turn'; +} + +export type ContextInjectionProvider = ( + context: ContextInjectionContext, +) => string | undefined | Promise; + +export interface IContextInjector { + register( + variant: string, + provider: ContextInjectionProvider, + options?: ContextInjectionOptions, + ): IDisposable; +} + +export const IContextInjector = createDecorator( + 'contextInjectorService', +); diff --git a/packages/agent-core-v2/src/dynamicInjector/dynamicInjectorService.ts b/packages/agent-core-v2/src/contextInjector/contextInjectorService.ts similarity index 81% rename from packages/agent-core-v2/src/dynamicInjector/dynamicInjectorService.ts rename to packages/agent-core-v2/src/contextInjector/contextInjectorService.ts index 31bbbdebd..56a60d99f 100644 --- a/packages/agent-core-v2/src/dynamicInjector/dynamicInjectorService.ts +++ b/packages/agent-core-v2/src/contextInjector/contextInjectorService.ts @@ -9,23 +9,23 @@ import { IContextMemory } from '../contextMemory'; import { ITurnService } from '../turn'; import type { ContextMessage } from '#/contextMemory'; import { - IDynamicInjector, - type DynamicInjectionOptions, - type DynamicInjectionProvider, -} from './dynamicInjector'; + IContextInjector, + type ContextInjectionOptions, + type ContextInjectionProvider, +} from './contextInjector'; -interface DynamicInjectionEntry { - readonly cadence: DynamicInjectionOptions['cadence']; - readonly provider: DynamicInjectionProvider; +interface ContextInjectionEntry { + readonly cadence: ContextInjectionOptions['cadence']; + readonly provider: ContextInjectionProvider; readonly variant: string; injectedAt: number | null; resolveHistory: boolean; turnConsumed: boolean; } -export class DynamicInjectorService extends Disposable implements IDynamicInjector { - private readonly entries = new Set(); - private readonly selfInsertedMessages = new WeakMap(); +export class ContextInjectorService extends Disposable implements IContextInjector { + private readonly entries = new Set(); + private readonly selfInsertedMessages = new WeakMap(); constructor( @IContextMemory private readonly context: IContextMemory, @@ -33,13 +33,13 @@ export class DynamicInjectorService extends Disposable implements IDynamicInject ) { super(); this._register( - turnService.hooks.beforeStep.register('dynamic-injector', async (_ctx, next) => { + turnService.hooks.beforeStep.register('context-injector', async (_ctx, next) => { await next(); await this.inject(); }), ); this._register( - turnService.hooks.onLaunched.register('dynamic-injector', (_ctx, next) => { + turnService.hooks.onLaunched.register('context-injector', (_ctx, next) => { for (const entry of this.entries) { if (entry.cadence !== 'turn') continue; entry.injectedAt = null; @@ -49,7 +49,7 @@ export class DynamicInjectorService extends Disposable implements IDynamicInject return next(); }), ); - context.hooks.onSpliced.register('dynamic-injector', (ctx, next) => { + context.hooks.onSpliced.register('context-injector', (ctx, next) => { this.handleSplice(ctx); return next(); }); @@ -57,10 +57,10 @@ export class DynamicInjectorService extends Disposable implements IDynamicInject register( variant: string, - provider: DynamicInjectionProvider, - options: DynamicInjectionOptions = {}, + provider: ContextInjectionProvider, + options: ContextInjectionOptions = {}, ) { - const entry: DynamicInjectionEntry = { + const entry: ContextInjectionEntry = { cadence: options.cadence ?? 'step', provider, variant, @@ -76,7 +76,7 @@ export class DynamicInjectorService extends Disposable implements IDynamicInject private async inject(): Promise { for (const entry of this.entries) { - const history = this.context.getHistory(); + const history = this.context.get(); if (entry.resolveHistory) { entry.injectedAt ??= findLastInjection(history, entry.variant); } @@ -85,14 +85,14 @@ export class DynamicInjectorService extends Disposable implements IDynamicInject entry.turnConsumed = true; } const content = await entry.provider({ - injectedAt: entry.injectedAt, + lastInjectedAt: entry.injectedAt, }); if (!this.entries.has(entry)) continue; if (content === undefined || content.trim().length === 0) continue; - const injectedAt = this.context.getHistory().length; + const injectedAt = this.context.get().length; const message = createInjectionMessage(content, entry.variant); this.selfInsertedMessages.set(message, entry); - this.context.spliceHistory(injectedAt, 0, [message]); + this.context.splice(injectedAt, 0, [message]); entry.injectedAt = injectedAt; entry.resolveHistory = false; this.selfInsertedMessages.delete(message); @@ -100,7 +100,7 @@ export class DynamicInjectorService extends Disposable implements IDynamicInject } private handleSplice(splice: ContextSplice): void { - const selfInserted = new Map(); + const selfInserted = new Map(); splice.messages.forEach((message, offset) => { const entry = this.selfInsertedMessages.get(message); if (entry !== undefined) { @@ -108,7 +108,7 @@ export class DynamicInjectorService extends Disposable implements IDynamicInject } }); const previousLength = - this.context.getHistory().length - splice.messages.length + splice.deleteCount; + this.context.get().length - splice.messages.length + splice.deleteCount; for (const entry of this.entries) { const ownInsertedAt = selfInserted.get(entry); @@ -196,8 +196,8 @@ function createInjectionMessage(content: string, variant: string): ContextMessag registerScopedService( LifecycleScope.Agent, - IDynamicInjector, - DynamicInjectorService, + IContextInjector, + ContextInjectorService, InstantiationType.Delayed, - 'dynamicInjector', + 'contextInjector', ); diff --git a/packages/agent-core-v2/src/dynamicInjector/index.ts b/packages/agent-core-v2/src/contextInjector/index.ts similarity index 59% rename from packages/agent-core-v2/src/dynamicInjector/index.ts rename to packages/agent-core-v2/src/contextInjector/index.ts index fcddec351..08029731c 100644 --- a/packages/agent-core-v2/src/dynamicInjector/index.ts +++ b/packages/agent-core-v2/src/contextInjector/index.ts @@ -2,5 +2,5 @@ * `dynamicInjector` domain barrel - re-exports the dynamicInjector service contract and implementation. */ -export * from './dynamicInjector'; -export * from './dynamicInjectorService'; +export * from './contextInjector'; +export * from './contextInjectorService'; diff --git a/packages/agent-core-v2/src/contextMemory/contextMemory.ts b/packages/agent-core-v2/src/contextMemory/contextMemory.ts index 3d7f36abc..42032a4fb 100644 --- a/packages/agent-core-v2/src/contextMemory/contextMemory.ts +++ b/packages/agent-core-v2/src/contextMemory/contextMemory.ts @@ -4,8 +4,8 @@ import type { Hooks } from '#/hooks'; import type { ContextMessage } from './types'; export interface IContextMemory { - getHistory(): readonly ContextMessage[]; - spliceHistory( + get(): readonly ContextMessage[]; + splice( start: number, deleteCount: number, messages: readonly ContextMessage[], @@ -22,5 +22,4 @@ export interface IContextMemory { }>; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IContextMemory = createDecorator('agentContextMemoryService'); diff --git a/packages/agent-core-v2/src/contextMemory/contextMemoryService.ts b/packages/agent-core-v2/src/contextMemory/contextMemoryService.ts index 1a738d063..fbf5f9006 100644 --- a/packages/agent-core-v2/src/contextMemory/contextMemoryService.ts +++ b/packages/agent-core-v2/src/contextMemory/contextMemoryService.ts @@ -1,7 +1,6 @@ import { Disposable, } from "#/_base/di"; -import { estimateTokensForMessages } from "#/_base/utils/tokens"; import { OrderedHookSlot } from '#/hooks'; import { IReplayBuilderService } from '#/replayBuilder'; import { IWireRecord, type WireRecord } from '#/wireRecord'; @@ -59,11 +58,11 @@ export class ContextMemoryService extends Disposable implements IContextMemory { ); } - getHistory(): readonly ContextMessage[] { + get(): readonly ContextMessage[] { return [...this.history]; } - spliceHistory( + splice( start: number, deleteCount: number, messages: readonly ContextMessage[], @@ -82,56 +81,18 @@ export class ContextMemoryService extends Disposable implements IContextMemory { private applySplice(record: WireRecord<'context.splice'>): void { const messages = [...record.messages]; - const wasCompactionSummary = isCompactionSummarySplice(record); - const tokensBefore = wasCompactionSummary ? estimateTokensForMessages(this.history) : 0; - // A splice that deletes the whole history mirrors `context.clear`: prior - // messages stay in the replay (as a boundary) and must not be removed. - // Only a partial delete (old `context.undo`) drops the deleted messages - // from the replay, symmetric to the insert `push` below. - const clearsHistory = record.start === 0 && record.deleteCount >= this.history.length; - const removedMessages = clearsHistory - ? [] - : this.history.slice(record.start, record.start + record.deleteCount); - this.history.splice(record.start, record.deleteCount, ...messages); - if (removedMessages.length > 0) { - this.replayBuilder.removeLastMessages(new Set(removedMessages)); + for (const message of messages) { + this.replayBuilder.push({ type: 'message', message }); } - if (wasCompactionSummary) { - this.replayBuilder.patchLast('compaction', { - result: { - summary: textContent(messages[0]), - compactedCount: record.deleteCount, - tokensBefore, - tokensAfter: record.tokens ?? estimateTokensForMessages(this.history), - }, - }); - } else { - for (const message of messages) { - this.replayBuilder.push({ type: 'message', message }); - } - } - const context = { + void this.hooks.onSpliced.run({ start: record.start, deleteCount: record.deleteCount, messages, tokens: record.tokens, - }; - void this.hooks.onSpliced.run(context); + }); } } -function isCompactionSummarySplice(record: WireRecord<'context.splice'>): boolean { - return record.messages.length === 1 && record.messages[0]?.origin?.kind === 'compaction_summary'; -} - -function textContent(message: ContextMessage | undefined): string { - return ( - message?.content - .map((part) => (part.type === 'text' && typeof part.text === 'string' ? part.text : '')) - .join('') ?? '' - ); -} - registerScopedService( LifecycleScope.Agent, IContextMemory, diff --git a/packages/agent-core-v2/src/contextProjector/contextProjector.ts b/packages/agent-core-v2/src/contextProjector/contextProjector.ts index 3532f469c..0228f89a1 100644 --- a/packages/agent-core-v2/src/contextProjector/contextProjector.ts +++ b/packages/agent-core-v2/src/contextProjector/contextProjector.ts @@ -7,7 +7,6 @@ export interface IContextProjector { project(messages: readonly ContextMessage[]): readonly Message[]; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IContextProjector = createDecorator( 'agentContextProjectorService', ); diff --git a/packages/agent-core-v2/src/contextProjector/microCompactingProjector.ts b/packages/agent-core-v2/src/contextProjector/microCompactingProjector.ts deleted file mode 100644 index 5c42ff774..000000000 --- a/packages/agent-core-v2/src/contextProjector/microCompactingProjector.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { ContentPart, Message } from '@moonshot-ai/kosong'; - -import type { ContextMessage } from '#/contextMemory'; -import type { IContextProjector } from './contextProjector'; - -export interface MicroCompactingProjectorOptions { - readonly keepRecentMessages?: number; - readonly minContentLength?: number; - readonly truncatedMarker?: string; -} - -export class MicroCompactingProjector implements IContextProjector { - private readonly keepRecentMessages: number; - private readonly minContentLength: number; - private readonly truncatedMarker: string; - - constructor( - private readonly previous: IContextProjector, - options: MicroCompactingProjectorOptions = {}, - ) { - this.keepRecentMessages = options.keepRecentMessages ?? 20; - this.minContentLength = options.minContentLength ?? 1000; - this.truncatedMarker = options.truncatedMarker ?? '[Old tool result content cleared]'; - } - - project(messages: readonly ContextMessage[]): readonly Message[] { - return microCompact(this.previous.project(messages), { - keepRecentMessages: this.keepRecentMessages, - minContentLength: this.minContentLength, - truncatedMarker: this.truncatedMarker, - }); - } -} - -function microCompact( - messages: readonly Message[], - options: Required, -): readonly Message[] { - const cutoff = Math.max(0, messages.length - options.keepRecentMessages); - return messages.map((message, index) => { - if (index >= cutoff || message.role !== 'tool' || message.toolCallId === undefined) { - return message; - } - const contentLength = textLength(message.content); - if (contentLength < options.minContentLength) { - return message; - } - return { - ...message, - content: [{ type: 'text', text: options.truncatedMarker } satisfies ContentPart], - }; - }); -} - -function textLength(content: readonly ContentPart[]): number { - return content.reduce((sum, part) => { - if (part.type === 'text') return sum + part.text.length; - if (part.type === 'think') return sum + part.think.length; - return sum; - }, 0); -} diff --git a/packages/agent-core-v2/src/contextSize/contextSize.ts b/packages/agent-core-v2/src/contextSize/contextSize.ts index accc7b145..c7d16b835 100644 --- a/packages/agent-core-v2/src/contextSize/contextSize.ts +++ b/packages/agent-core-v2/src/contextSize/contextSize.ts @@ -9,9 +9,8 @@ export interface IContextSizeService { readonly _serviceBrand: undefined; getStatus(): ContextSizeStatus; - measure(length: number, tokens: number): void; + measured(length: number, tokens: number): void; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IContextSizeService = createDecorator('agentContextSizeService'); diff --git a/packages/agent-core-v2/src/contextSize/contextSizeService.ts b/packages/agent-core-v2/src/contextSize/contextSizeService.ts index fbdda87d4..da38ba77a 100644 --- a/packages/agent-core-v2/src/contextSize/contextSizeService.ts +++ b/packages/agent-core-v2/src/contextSize/contextSizeService.ts @@ -6,7 +6,7 @@ import { } from "#/_base/utils/tokens"; import type { ContextMessage } from '#/contextMemory'; import { IContextMemory } from '#/contextMemory'; -import { IEventBus } from '#/eventBus'; +import { IEventSink } from '../eventSink'; import { IProfileService } from '#/profile'; import { IWireRecord, type WireRecord } from '#/wireRecord'; import { @@ -40,7 +40,7 @@ export class ContextSizeService constructor( @IContextMemory private readonly context: IContextMemory, - @IEventBus private readonly events: IEventBus, + @IEventSink private readonly events: IEventSink, @IProfileService private readonly profile: IProfileService, @IWireRecord private readonly wireRecord: IWireRecord, ) { @@ -67,7 +67,7 @@ export class ContextSizeService }; } - measure(length: number, tokens: number): void { + measured(length: number, tokens: number): void { const record: WireRecord<'context_size.measured'> = { type: 'context_size.measured', length, diff --git a/packages/agent-core-v2/src/cron/cron.ts b/packages/agent-core-v2/src/cron/cron.ts index c245f2e30..adf005649 100644 --- a/packages/agent-core-v2/src/cron/cron.ts +++ b/packages/agent-core-v2/src/cron/cron.ts @@ -68,5 +68,4 @@ export interface ICronService extends CronToolManager { flushPersist(): Promise; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const ICronService = createDecorator('agentCronService'); diff --git a/packages/agent-core-v2/src/cron/cronService.ts b/packages/agent-core-v2/src/cron/cronService.ts index de661d4d8..4a7101d98 100644 --- a/packages/agent-core-v2/src/cron/cronService.ts +++ b/packages/agent-core-v2/src/cron/cronService.ts @@ -7,7 +7,7 @@ import { toDisposable, } from "#/_base/di"; import type { ContextMessage } from '#/contextMemory'; -import { IEventBus } from '#/eventBus'; +import { IEventSink } from '../eventSink'; import { IPromptService } from '#/prompt'; import { ITelemetryService } from '#/telemetry'; import { IToolRegistry } from '#/toolRegistry'; @@ -82,7 +82,7 @@ export class CronService constructor( private readonly options: CronOptions = {}, @IPromptService private readonly prompt: IPromptService, - @IEventBus private readonly events: IEventBus, + @IEventSink private readonly events: IEventSink, @IWireRecord private readonly wireRecord: IWireRecord, @ITurnService private readonly turnService: ITurnService, @ITelemetryService private readonly telemetry: ITelemetryService, diff --git a/packages/agent-core-v2/src/dynamicInjector/dynamicInjector.ts b/packages/agent-core-v2/src/dynamicInjector/dynamicInjector.ts deleted file mode 100644 index 3732d1894..000000000 --- a/packages/agent-core-v2/src/dynamicInjector/dynamicInjector.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { createDecorator } from "#/_base/di"; -import type { IDisposable } from "#/_base/di"; - -export interface DynamicInjectionContext { - readonly injectedAt: number | null; -} - -export type DynamicInjectionProvider = ( - context: DynamicInjectionContext, -) => string | undefined | Promise; - -export interface IDynamicInjector { - register( - variant: string, - provider: DynamicInjectionProvider, - options?: DynamicInjectionOptions, - ): IDisposable; -} - -export interface DynamicInjectionOptions { - readonly cadence?: 'step' | 'turn'; -} - -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const IDynamicInjector = createDecorator( - 'agentDynamicInjectorService', -); diff --git a/packages/agent-core-v2/src/event/event.ts b/packages/agent-core-v2/src/event/event.ts deleted file mode 100644 index 625307ef8..000000000 --- a/packages/agent-core-v2/src/event/event.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * `event` domain (L7) — core-scope global pub-sub. - * - * Defines the public contract of the event bus: the `ProtocolEvent` model and - * the `IEventService` used by other domains to publish and subscribe to - * protocol events. Core-scoped — one shared bus for the application. - */ - -import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation'; -import type { IDisposable } from '#/_base/di/lifecycle'; - -export interface ProtocolEvent { - readonly type: string; - readonly payload: unknown; -} - -export interface IEventService { - readonly _serviceBrand: undefined; - publish(event: ProtocolEvent): void; - subscribe(handler: (event: ProtocolEvent) => void): IDisposable; -} - -export const IEventService: ServiceIdentifier = - createDecorator('eventService'); diff --git a/packages/agent-core-v2/src/event/eventService.ts b/packages/agent-core-v2/src/event/eventService.ts deleted file mode 100644 index 64530e735..000000000 --- a/packages/agent-core-v2/src/event/eventService.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * `event` domain (L7) — `IEventService` implementation. - * - * Owns the in-process pub-sub listener set and event fan-out. Bound at Core - * scope. - */ - -import { InstantiationType } from '#/_base/di/extensions'; -import { type IDisposable, toDisposable } from '#/_base/di/lifecycle'; -import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; - -import { type ProtocolEvent, IEventService } from './event'; - -type Listener = (event: ProtocolEvent) => void; - -export class EventService implements IEventService { - declare readonly _serviceBrand: undefined; - private readonly listeners = new Set(); - - publish(event: ProtocolEvent): void { - for (const listener of this.listeners) { - listener(event); - } - } - - subscribe(handler: (event: ProtocolEvent) => void): IDisposable { - this.listeners.add(handler); - return toDisposable(() => { - this.listeners.delete(handler); - }); - } -} - -registerScopedService(LifecycleScope.Core, IEventService, EventService, InstantiationType.Delayed, 'event'); diff --git a/packages/agent-core-v2/src/event/index.ts b/packages/agent-core-v2/src/event/index.ts deleted file mode 100644 index d1d9bf5cc..000000000 --- a/packages/agent-core-v2/src/event/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * `event` domain barrel — re-exports the event contract (`event`) and its - * scoped service (`eventService`). Importing this barrel registers the - * `IEventService` binding into the scope registry. - */ - -export * from './event'; -export * from './eventService'; diff --git a/packages/agent-core-v2/src/eventBus/eventBus.ts b/packages/agent-core-v2/src/eventBus/eventBus.ts deleted file mode 100644 index 6f7141a24..000000000 --- a/packages/agent-core-v2/src/eventBus/eventBus.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { AgentEvent as ProtocolAgentEvent } from '@moonshot-ai/protocol'; - -import { createDecorator } from "#/_base/di"; -import type { IDisposable } from "#/_base/di"; - -export interface IEventBus { - emit(event: ProtocolAgentEvent): void; - on(handler: (event: ProtocolAgentEvent) => void): IDisposable; -} - -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const IEventBus = createDecorator('agentEventBusService'); diff --git a/packages/agent-core-v2/src/eventSink/eventSink.ts b/packages/agent-core-v2/src/eventSink/eventSink.ts new file mode 100644 index 000000000..170608e10 --- /dev/null +++ b/packages/agent-core-v2/src/eventSink/eventSink.ts @@ -0,0 +1,10 @@ +import type { IDisposable } from "#/_base/di"; +import { createDecorator } from "#/_base/di"; +import type { AgentEvent } from '@moonshot-ai/protocol'; + +export interface IEventSink { + emit(event: AgentEvent): void; + on(handler: (event: AgentEvent) => void): IDisposable; +} + +export const IEventSink = createDecorator('agentEventSink'); diff --git a/packages/agent-core-v2/src/eventBus/eventBusService.ts b/packages/agent-core-v2/src/eventSink/eventSinkService.ts similarity index 83% rename from packages/agent-core-v2/src/eventBus/eventBusService.ts rename to packages/agent-core-v2/src/eventSink/eventSinkService.ts index dbfb41ad5..be40e81db 100644 --- a/packages/agent-core-v2/src/eventBus/eventBusService.ts +++ b/packages/agent-core-v2/src/eventSink/eventSinkService.ts @@ -6,11 +6,11 @@ import { } from "#/_base/di"; import { Emitter } from "#/_base/event"; -import { IEventBus } from '#/eventBus'; +import { IEventSink } from '.'; import type { AgentEvent } from '@moonshot-ai/protocol'; import { IWireRecord } from '#/wireRecord'; -export class EventBusService extends Disposable implements IEventBus { +export class EventSinkService extends Disposable implements IEventSink { private readonly onDidEmitEmitter = this._register(new Emitter()); constructor(@IWireRecord private readonly wireRecord: IWireRecord) { @@ -29,8 +29,8 @@ export class EventBusService extends Disposable implements IEventBus { registerScopedService( LifecycleScope.Agent, - IEventBus, - EventBusService, + IEventSink, + EventSinkService, InstantiationType.Delayed, - 'eventBus', + 'eventSink', ); diff --git a/packages/agent-core-v2/src/eventBus/index.ts b/packages/agent-core-v2/src/eventSink/index.ts similarity index 60% rename from packages/agent-core-v2/src/eventBus/index.ts rename to packages/agent-core-v2/src/eventSink/index.ts index 3ed3f4363..d3f121ae5 100644 --- a/packages/agent-core-v2/src/eventBus/index.ts +++ b/packages/agent-core-v2/src/eventSink/index.ts @@ -2,5 +2,5 @@ * `eventBus` domain barrel - re-exports the eventBus service contract and implementation. */ -export * from './eventBus'; -export * from './eventBusService'; +export * from './eventSink'; +export * from './eventSinkService'; diff --git a/packages/agent-core-v2/src/externalHooks/externalHooks.ts b/packages/agent-core-v2/src/externalHooks/externalHooks.ts index 0b27378c0..13e863c4c 100644 --- a/packages/agent-core-v2/src/externalHooks/externalHooks.ts +++ b/packages/agent-core-v2/src/externalHooks/externalHooks.ts @@ -98,6 +98,5 @@ export interface IExternalHooksService { }): void; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IExternalHooksService = createDecorator('agentExternalHooksService'); diff --git a/packages/agent-core-v2/src/fullCompaction/fullCompaction.ts b/packages/agent-core-v2/src/fullCompaction/fullCompaction.ts index 9f8ca3212..ff9b8a1c3 100644 --- a/packages/agent-core-v2/src/fullCompaction/fullCompaction.ts +++ b/packages/agent-core-v2/src/fullCompaction/fullCompaction.ts @@ -30,5 +30,4 @@ declare module '#/wireRecord' { } } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IFullCompaction = createDecorator('agentFullCompactionService'); diff --git a/packages/agent-core-v2/src/fullCompaction/fullCompactionService.ts b/packages/agent-core-v2/src/fullCompaction/fullCompactionService.ts index 67b4093e3..c50dd89e0 100644 --- a/packages/agent-core-v2/src/fullCompaction/fullCompactionService.ts +++ b/packages/agent-core-v2/src/fullCompaction/fullCompactionService.ts @@ -18,7 +18,7 @@ import { estimateTokens, estimateTokensForMessages } from "#/_base/utils/tokens" import { IContextMemory } from '#/contextMemory'; import { IContextProjector } from '#/contextProjector'; import { IContextSizeService } from '#/contextSize'; -import { IEventBus } from '#/eventBus'; +import { IEventSink } from '../eventSink'; import { IExternalHooksService } from '#/externalHooks'; import { ILLMRequester, type LLMEvent } from '#/llmRequester'; import { isAbortError } from '#/loop/errors'; @@ -97,7 +97,7 @@ export class FullCompactionService extends Disposable implements IFullCompaction @ITelemetryService private readonly telemetry: ITelemetryService, @IUsageService private readonly usage: IUsageService, @IWireRecord private readonly wireRecord: IWireRecord, - @IEventBus private readonly events: IEventBus, + @IEventSink private readonly events: IEventSink, @IReplayBuilderService private readonly replayBuilder: IReplayBuilderService, @IExternalHooksService private readonly externalHooks: IExternalHooksService, @ITurnService turnService: ITurnService, @@ -139,7 +139,7 @@ export class FullCompactionService extends Disposable implements IFullCompaction ); this._register( wireRecord.register('full_compaction.complete', (record) => { - const summary = compactionSummaryText(this.context.getHistory()); + const summary = compactionSummaryText(this.context.get()); if (summary === undefined) return; this.replayBuilder.patchLast('compaction', { result: { @@ -167,7 +167,7 @@ export class FullCompactionService extends Disposable implements IFullCompaction } if (this.compactionCountInTurn > this.strategy.maxCompactionPerTurn) return false; - const history = this.context.getHistory(); + const history = this.context.get(); const compactedCount = this.strategy.computeCompactCount(history, data.source); if (compactedCount === 0) { throw new KimiError(ErrorCodes.COMPACTION_UNABLE, 'No prefix that can be compacted in current history.'); @@ -294,7 +294,7 @@ export class FullCompactionService extends Disposable implements IFullCompaction if (result.tokensBefore - result.tokensAfter < 1024) break; if (!this.strategy.shouldBlock(result.tokensAfter)) break; - compactedCount = this.strategy.computeCompactCount(this.context.getHistory(), data.source); + compactedCount = this.strategy.computeCompactCount(this.context.get(), data.source); if (compactedCount === 0) break; } @@ -328,7 +328,7 @@ export class FullCompactionService extends Disposable implements IFullCompaction initialCompactedCount: number, ): Promise { const startedAt = Date.now(); - const originalHistory = [...this.context.getHistory()]; + const originalHistory = [...this.context.get()]; const tokensBefore = estimateTokensForMessages(originalHistory); let retryCount = 0; @@ -386,7 +386,7 @@ export class FullCompactionService extends Disposable implements IFullCompaction ); } - if (!historyUnchanged(this.context.getHistory(), originalHistory)) { + if (!historyUnchanged(this.context.get(), originalHistory)) { this.cancel(); return undefined; } @@ -413,7 +413,7 @@ export class FullCompactionService extends Disposable implements IFullCompaction ...data, }); - this.context.spliceHistory( + this.context.splice( 0, compactedCount, [createCompactionSummaryMessage(summary)], diff --git a/packages/agent-core-v2/src/gateway/gatewayService.ts b/packages/agent-core-v2/src/gateway/gatewayService.ts index 36c432071..a804f5a85 100644 --- a/packages/agent-core-v2/src/gateway/gatewayService.ts +++ b/packages/agent-core-v2/src/gateway/gatewayService.ts @@ -17,7 +17,7 @@ import { } from '#/_base/di/scope'; import { IInstantiationService } from '#/_base/di/instantiation'; import { IAgentLifecycleService } from '#/agent-lifecycle/agentLifecycle'; -import { IEventService } from '#/event'; +import { IEventSink } from '#/eventSink'; import { ILogService, ISessionLogService } from '#/log'; import { IPromptService } from '#/prompt'; import { ITurnService } from '#/turn'; @@ -92,7 +92,8 @@ export class RestGateway implements IRestGateway { return Promise.resolve(); } cancel(sessionId: string, agentId: string, reason?: string): Promise { - this.agent(sessionId, agentId).accessor.get(ITurnService).cancel(undefined, reason); + const activeTurn = this.agent(sessionId, agentId).accessor.get(ITurnService).getActiveTurn(); + activeTurn?.abortController.abort(reason); return Promise.resolve(); } getStatus(sessionId: string): Promise { @@ -116,7 +117,7 @@ export class WSGateway implements IWSGateway { constructor( @IScopeRegistry _scopes: IScopeRegistry, - @IEventService _event: IEventService, + @IEventSink _event: IEventSink, ) {} connect(connectionId: string): void { @@ -129,7 +130,7 @@ export class WSGateway implements IWSGateway { export class WSBroadcastService extends Disposable implements IWSBroadcastService { declare readonly _serviceBrand: undefined; - constructor(@IEventService event: IEventService) { + constructor(@IEventSink event: IEventSink) { super(); event.subscribe(() => { }); diff --git a/packages/agent-core-v2/src/goal/goal.ts b/packages/agent-core-v2/src/goal/goal.ts index bf5e4af66..9f0e1eac4 100644 --- a/packages/agent-core-v2/src/goal/goal.ts +++ b/packages/agent-core-v2/src/goal/goal.ts @@ -42,5 +42,4 @@ declare module '#/wireRecord' { } } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IGoalService = createDecorator('agentGoalService'); diff --git a/packages/agent-core-v2/src/goal/goalService.ts b/packages/agent-core-v2/src/goal/goalService.ts index f917c4883..12f20eb73 100644 --- a/packages/agent-core-v2/src/goal/goalService.ts +++ b/packages/agent-core-v2/src/goal/goalService.ts @@ -9,8 +9,8 @@ import { import { ErrorCodes, KimiError } from "#/errors"; import type { ContextMessage } from '#/contextMemory'; import { IContextMemory } from '#/contextMemory'; -import { IDynamicInjector } from '#/dynamicInjector'; -import { IEventBus } from '#/eventBus'; +import { IContextInjector } from '../contextInjector'; +import { IEventSink } from '../eventSink'; import { IReplayBuilderService } from '#/replayBuilder'; import type { TelemetryProperties } from '#/telemetry'; import { ITelemetryService } from '#/telemetry'; @@ -76,11 +76,11 @@ export class GoalService extends Disposable implements IGoalService { constructor( private readonly options: GoalServiceOptions = {}, @IWireRecord private readonly wireRecord: IWireRecord, - @IEventBus private readonly events: IEventBus, + @IEventSink private readonly events: IEventSink, @IContextMemory private readonly context: IContextMemory, @IReplayBuilderService private readonly replayBuilder: IReplayBuilderService, @ITelemetryService private readonly telemetry: ITelemetryService, - @IDynamicInjector private readonly dynamicInjector: IDynamicInjector, + @IContextInjector private readonly dynamicInjector: IContextInjector, ) { super(); this._register( @@ -515,7 +515,7 @@ export class GoalService extends Disposable implements IGoalService { toolCalls: [], origin: { kind: 'system_trigger', name }, }; - this.context.spliceHistory(this.context.getHistory().length, 0, [message]); + this.context.splice(this.context.get().length, 0, [message]); } } diff --git a/packages/agent-core-v2/src/goal/injection/goalInjection.ts b/packages/agent-core-v2/src/goal/injection/goalInjection.ts index aed044d8c..3bb28ff2b 100644 --- a/packages/agent-core-v2/src/goal/injection/goalInjection.ts +++ b/packages/agent-core-v2/src/goal/injection/goalInjection.ts @@ -1,7 +1,7 @@ import type { GoalSnapshot } from '#/goal'; import { Disposable } from "#/_base/di"; import { renderPrompt } from "#/_base/utils/render-prompt"; -import { IDynamicInjector } from '../../dynamicInjector/dynamicInjector'; +import { IContextInjector } from '../../contextInjector/contextInjector'; import GOAL_ACTIVE_REMINDER from './goal-active-reminder.md?raw'; import GOAL_BLOCKED_REMINDER from './goal-blocked-reminder.md?raw'; import GOAL_PAUSED_REMINDER from './goal-paused-reminder.md?raw'; @@ -14,7 +14,7 @@ export interface GoalInjectionOptions { export class GoalInjection extends Disposable { constructor( private readonly options: GoalInjectionOptions, - @IDynamicInjector dynamicInjector: IDynamicInjector, + @IContextInjector dynamicInjector: IContextInjector, ) { super(); this._register(dynamicInjector.register('goal', () => this.reminder(), { cadence: 'turn' })); diff --git a/packages/agent-core-v2/src/goalMode/injection/goalInjection.ts b/packages/agent-core-v2/src/goalMode/injection/goalInjection.ts new file mode 100644 index 000000000..9bd9d2c0a --- /dev/null +++ b/packages/agent-core-v2/src/goalMode/injection/goalInjection.ts @@ -0,0 +1,119 @@ +import type { GoalSnapshot } from '../../../../agent/goal'; +import { Disposable } from "#/_base/di"; +import { renderPrompt } from "#/_base/utils/render-prompt"; +import { IContextInjector } from '../../contextInjector/contextInjector'; +import GOAL_ACTIVE_REMINDER from './goal-active-reminder.md?raw'; +import GOAL_BLOCKED_REMINDER from './goal-blocked-reminder.md?raw'; +import GOAL_PAUSED_REMINDER from './goal-paused-reminder.md?raw'; + +export interface GoalInjectionOptions { + readonly getGoal: () => GoalSnapshot | null; + readonly enabled?: () => boolean; +} + +export class GoalInjection extends Disposable { + constructor( + private readonly options: GoalInjectionOptions, + @IContextInjector dynamicInjector: IContextInjector, + ) { + super(); + this._register(dynamicInjector.register('goal', () => this.reminder(), { cadence: 'turn' })); + } + + private reminder(): string | undefined { + if (this.options.enabled?.() === false) return undefined; + const goal = this.options.getGoal(); + if (goal === null) return undefined; + if (goal.status === 'active') return buildGoalReminder(goal); + if (goal.status === 'blocked') return buildBlockedNote(goal); + if (goal.status === 'paused') return buildPausedNote(goal); + return undefined; + } +} + +function buildBlockedNote(goal: GoalSnapshot): string { + const reason = goal.terminalReason; + return renderPrompt(GOAL_BLOCKED_REMINDER, { + reason: reason === undefined ? '' : escapeUntrustedText(reason), + objective: escapeUntrustedText(goal.objective), + completionCriterion: escapedCompletionCriterion(goal), + }); +} + +function buildPausedNote(goal: GoalSnapshot): string { + const reason = goal.terminalReason; + return renderPrompt(GOAL_PAUSED_REMINDER, { + reason: reason === undefined ? '' : escapeUntrustedText(reason), + objective: escapeUntrustedText(goal.objective), + completionCriterion: escapedCompletionCriterion(goal), + }); +} + +function buildGoalReminder(goal: GoalSnapshot): string { + return renderPrompt(GOAL_ACTIVE_REMINDER, { + objective: escapeUntrustedText(goal.objective), + completionCriterion: escapedCompletionCriterion(goal), + status: goal.status, + progress: `${goal.turnsUsed} continuation turns, ${goal.tokensUsed} tokens, ${formatElapsed(goal.wallClockMs)} elapsed`, + budgets: formatBudgets(goal), + nearingBudget: isNearingBudget(goal), + }); +} + +function escapedCompletionCriterion(goal: GoalSnapshot): string { + if (goal.completionCriterion === undefined) return ''; + return escapeUntrustedText(goal.completionCriterion); +} + +function formatBudgets(goal: GoalSnapshot): string { + const budgetLines: string[] = []; + if (goal.budget.turnBudget !== null) { + budgetLines.push( + `turns ${goal.turnsUsed}/${goal.budget.turnBudget} (remaining ${goal.budget.remainingTurns})`, + ); + } + if (goal.budget.tokenBudget !== null) { + budgetLines.push( + `tokens ${goal.tokensUsed}/${goal.budget.tokenBudget} (remaining ${goal.budget.remainingTokens})`, + ); + } + if (goal.budget.wallClockBudgetMs !== null) { + budgetLines.push( + `time ${formatElapsed(goal.wallClockMs)}/${formatElapsed(goal.budget.wallClockBudgetMs)} (remaining ${formatElapsed(goal.budget.remainingWallClockMs ?? 0)})`, + ); + } + return budgetLines.join('; '); +} + +function isNearingBudget(goal: GoalSnapshot): boolean { + return maxBudgetFraction(goal) >= 0.75; +} + +function maxBudgetFraction(goal: GoalSnapshot): number { + const fractions: number[] = []; + if (goal.budget.turnBudget !== null && goal.budget.turnBudget > 0) { + fractions.push(goal.turnsUsed / goal.budget.turnBudget); + } + if (goal.budget.tokenBudget !== null && goal.budget.tokenBudget > 0) { + fractions.push(goal.tokensUsed / goal.budget.tokenBudget); + } + if (goal.budget.wallClockBudgetMs !== null && goal.budget.wallClockBudgetMs > 0) { + fractions.push(goal.wallClockMs / goal.budget.wallClockBudgetMs); + } + return fractions.length === 0 ? 0 : Math.max(...fractions); +} + +function escapeUntrustedText(text: string): string { + return text + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>'); +} + +function formatElapsed(ms: number): string { + const totalSeconds = Math.round(ms / 1000); + if (totalSeconds < 60) return `${totalSeconds}s`; + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return `${minutes}m${seconds.toString().padStart(2, '0')}s`; +} diff --git a/packages/agent-core-v2/src/index.ts b/packages/agent-core-v2/src/index.ts index 1cd795631..fd11fe782 100644 --- a/packages/agent-core-v2/src/index.ts +++ b/packages/agent-core-v2/src/index.ts @@ -52,8 +52,8 @@ export * from './blobStore/index'; export * from './contextMemory/index'; export * from './contextProjector/index'; export * from './contextSize/index'; -export * from './dynamicInjector/index'; -export * from './eventBus/index'; +export * from './contextInjector/index'; +export * from './eventSink/index'; export * from './externalHooks/index'; export * from './fullCompaction/index'; export * from './llmRequestLog/index'; diff --git a/packages/agent-core-v2/src/kaos/kaos.ts b/packages/agent-core-v2/src/kaos/kaos.ts index a3c356074..bc48bbb34 100644 --- a/packages/agent-core-v2/src/kaos/kaos.ts +++ b/packages/agent-core-v2/src/kaos/kaos.ts @@ -9,7 +9,6 @@ export interface IKaosService { chdir(cwd: string): Promise; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IKaosService = createDecorator('agentKaosService'); export type KaosFactoryOptions = @@ -21,7 +20,6 @@ export interface IKaosFactory { create(options: KaosFactoryOptions): Promise; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IKaosFactory = createDecorator('kaosFactory'); export interface ISessionKaosService { @@ -36,6 +34,5 @@ export interface ISessionKaosService { removeAdditionalDir(dir: string): void; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const ISessionKaosService = createDecorator('sessionKaosService'); diff --git a/packages/agent-core-v2/src/llmRequestLog/llmRequestLog.ts b/packages/agent-core-v2/src/llmRequestLog/llmRequestLog.ts index d9814fe5d..085178d52 100644 --- a/packages/agent-core-v2/src/llmRequestLog/llmRequestLog.ts +++ b/packages/agent-core-v2/src/llmRequestLog/llmRequestLog.ts @@ -18,6 +18,5 @@ export interface ILLMRequestLogService { logRequest(input: LLMRequestLogInput): void; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const ILLMRequestLogService = createDecorator('agentLLMRequestLogService'); diff --git a/packages/agent-core-v2/src/llmRequester/llmRequester.ts b/packages/agent-core-v2/src/llmRequester/llmRequester.ts index eff4e4439..471a43c8b 100644 --- a/packages/agent-core-v2/src/llmRequester/llmRequester.ts +++ b/packages/agent-core-v2/src/llmRequester/llmRequester.ts @@ -28,5 +28,4 @@ export interface ILLMRequester { request(overrides?: LLMRequestOverrides, signal?: AbortSignal): AsyncIterable; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const ILLMRequester = createDecorator('agentLLMRequesterService'); diff --git a/packages/agent-core-v2/src/llmRequester/llmRequesterService.ts b/packages/agent-core-v2/src/llmRequester/llmRequesterService.ts index b3dbe9dce..89408cee5 100644 --- a/packages/agent-core-v2/src/llmRequester/llmRequesterService.ts +++ b/packages/agent-core-v2/src/llmRequester/llmRequesterService.ts @@ -172,7 +172,7 @@ export class LLMRequesterService implements ILLMRequester { modelAlias: resolved.modelAlias, systemPrompt: overrides.systemPrompt ?? this.profile.getSystemPrompt(), tools: [...(overrides.tools ?? this.defaultTools())], - messages: [...(overrides.messages ?? this.projector.project(this.context.getHistory()))], + messages: [...(overrides.messages ?? this.projector.project(this.context.get()))], requestLogFields: overrides.requestLogFields, generate: this.options.generate ?? generate, }; diff --git a/packages/agent-core-v2/src/loop/loop.ts b/packages/agent-core-v2/src/loop/loop.ts index 7739c97e9..25603feb8 100644 --- a/packages/agent-core-v2/src/loop/loop.ts +++ b/packages/agent-core-v2/src/loop/loop.ts @@ -19,5 +19,4 @@ export interface ILoopService { runTurn(turn: Turn, hooks?: LoopRunHooks): Promise; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const ILoopService = createDecorator('agentLoopService'); diff --git a/packages/agent-core-v2/src/loop/loopService.ts b/packages/agent-core-v2/src/loop/loopService.ts index 9025674d5..5468fe723 100644 --- a/packages/agent-core-v2/src/loop/loopService.ts +++ b/packages/agent-core-v2/src/loop/loopService.ts @@ -42,7 +42,7 @@ import { canonicalTelemetryArgs } from '#/_base/utils/canonical-args'; import { IContextMemory, type ContextMessage } from '#/contextMemory'; import { IContextProjector } from '#/contextProjector'; import { IContextSizeService } from '#/contextSize'; -import { IEventBus } from '#/eventBus'; +import { IEventSink } from '../eventSink'; import { IExternalHooksService } from '#/externalHooks'; import { IFullCompaction } from '#/fullCompaction'; import { ILLMRequester } from '#/llmRequester'; @@ -94,7 +94,7 @@ export class LoopService extends Disposable implements ILoopService { @IContextProjector private readonly projector: IContextProjector, @IContextSizeService private readonly contextSize: IContextSizeService, @ILLMRequester private readonly llmRequester: ILLMRequester, - @IEventBus private readonly events: IEventBus, + @IEventSink private readonly events: IEventSink, @IToolRegistry private readonly toolRegistry: IToolRegistry, @IToolExecutor private readonly toolExecutor: IToolExecutor, @IUsageService private readonly usage: IUsageService, @@ -144,7 +144,7 @@ export class LoopService extends Disposable implements ILoopService { turnId: String(turn.id), signal: turn.abortController.signal, llm, - buildMessages: () => [...this.projector.project(this.context.getHistory())], + buildMessages: () => [...this.projector.project(this.context.get())], dispatchEvent: this.dispatchEvent, tools: this.executableTools(), hooks: loopHooks, @@ -160,7 +160,7 @@ export class LoopService extends Disposable implements ILoopService { remainingToolCalls: context.toolCallCount, }); } else { - this.contextSize.measure(this.measurementLength(context.stepUuid), tokens); + this.contextSize.measured(this.measurementLength(context.stepUuid), tokens); } }, }); @@ -681,7 +681,7 @@ export class LoopService extends Disposable implements ILoopService { return; } - const history = this.context.getHistory(); + const history = this.context.get(); const index = history.indexOf(message.message); if (index < 0) { throw new Error(`Open loop step '${stepUuid}' is no longer present in context history`); @@ -701,11 +701,11 @@ export class LoopService extends Disposable implements ILoopService { private appendImmediately(...messages: ContextMessage[]): void { if (messages.length === 0) return; - this.spliceHistory(this.context.getHistory().length, 0, messages); + this.spliceHistory(this.context.get().length, 0, messages); } private removeMatchedTailMessage(matcher: (message: ContextMessage) => boolean): boolean { - const history = this.context.getHistory(); + const history = this.context.get(); const index = history.length - 1; const message = history[index]; if (message === undefined || !matcher(message)) return false; @@ -720,7 +720,7 @@ export class LoopService extends Disposable implements ILoopService { ): void { this.ownSpliceDepth++; try { - this.context.spliceHistory(start, deleteCount, messages); + this.context.splice(start, deleteCount, messages); } finally { this.ownSpliceDepth--; } @@ -732,7 +732,7 @@ export class LoopService extends Disposable implements ILoopService { private measurementLength(stepUuid: string): number { const openStep = this.openSteps.get(stepUuid); - const history = this.context.getHistory(); + const history = this.context.get(); if (openStep === undefined) return history.length; const index = history.indexOf(openStep.message); return index === -1 ? history.length : index + 1; @@ -752,7 +752,7 @@ export class LoopService extends Disposable implements ILoopService { } this.pendingMeasurements.delete(stepUuid); - this.contextSize.measure(this.measurementLength(stepUuid), pending.tokens); + this.contextSize.measured(this.measurementLength(stepUuid), pending.tokens); } } diff --git a/packages/agent-core-v2/src/mcp/mcp.ts b/packages/agent-core-v2/src/mcp/mcp.ts index 23fe2650a..11401b21c 100644 --- a/packages/agent-core-v2/src/mcp/mcp.ts +++ b/packages/agent-core-v2/src/mcp/mcp.ts @@ -30,5 +30,4 @@ export interface McpServiceOptions { readonly manager?: McpConnectionManager; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IMcpService = createDecorator('agentMcpService'); diff --git a/packages/agent-core-v2/src/mcp/mcpService.ts b/packages/agent-core-v2/src/mcp/mcpService.ts index d0785d9e7..c9646e4be 100644 --- a/packages/agent-core-v2/src/mcp/mcpService.ts +++ b/packages/agent-core-v2/src/mcp/mcpService.ts @@ -8,7 +8,7 @@ import { } from "#/_base/di"; import { ErrorCodes, makeErrorPayload } from "#/errors"; import type { ExecutableTool, ExecutableToolResult } from '#/loop'; -import { IEventBus } from '#/eventBus'; +import { IEventSink } from '../eventSink'; import { IToolRegistry } from '#/toolRegistry'; import { createMcpAuthTool } from './auth-tool'; import type { McpServerEntry } from './connection-manager'; @@ -37,7 +37,7 @@ export class McpService extends Disposable implements IMcpService { constructor( private readonly options: McpServiceOptions = {}, @IToolRegistry private readonly registry: IToolRegistry, - @IEventBus private readonly events: IEventBus, + @IEventSink private readonly events: IEventSink, ) { super(); this.attachMcpTools(); diff --git a/packages/agent-core-v2/src/mcpRuntime/mcpRuntimeService.ts b/packages/agent-core-v2/src/mcpRuntime/mcpRuntimeService.ts new file mode 100644 index 000000000..89e94b3eb --- /dev/null +++ b/packages/agent-core-v2/src/mcpRuntime/mcpRuntimeService.ts @@ -0,0 +1,293 @@ +import type { Tool as KosongTool } from '@moonshot-ai/kosong'; +import { InstantiationType } from '#/_base/di/extensions'; +import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; + +import { + Disposable, + type IDisposable, +} from "#/_base/di"; +import { ErrorCodes, makeErrorPayload } from "#/_base/errors"; +import type { ExecutableTool, ExecutableToolResult } from '../../../loop'; +import { createMcpAuthTool } from '../../../mcp/auth-tool'; +import type { McpServerEntry } from '../../../mcp/connection-manager'; +import { mcpResultToExecutableOutput } from '../../../mcp/output'; +import { qualifyMcpToolName } from '../../../mcp/tool-naming'; +import type { MCPClient } from '../../../mcp/types'; +import { IEventSink } from '../eventSink/eventSink'; +import { IToolRegistry } from '../toolRegistry/toolRegistry'; +import { IMcpRuntimeService, type McpRuntimeServiceOptions } from './mcpRuntime'; + +interface McpToolRegistration { + readonly disposable: IDisposable; + readonly serverName: string; +} + +interface McpToolCollision { + readonly qualified: string; + readonly toolName: string; + readonly collidesWith: + | { readonly kind: 'same_server'; readonly toolName: string } + | { readonly kind: 'other_server'; readonly serverName: string }; +} + +export class McpRuntimeService extends Disposable implements IMcpRuntimeService { + private readonly mcpTools = new Map(); + private readonly mcpToolsByServer = new Map(); + + constructor( + private readonly options: McpRuntimeServiceOptions = {}, + @IToolRegistry private readonly registry: IToolRegistry, + @IEventSink private readonly events: IEventSink, + ) { + super(); + this.attachMcpTools(); + } + + get oauthService() { + return this.options.manager?.oauthService; + } + + waitForInitialLoad(signal?: AbortSignal): Promise { + return this.options.manager?.waitForInitialLoad(signal) ?? Promise.resolve(); + } + + initialLoadDurationMs(): number { + return this.options.manager?.initialLoadDurationMs() ?? 0; + } + + list() { + return this.options.manager?.list() ?? []; + } + + resolved(name: string) { + return this.options.manager?.resolved(name); + } + + getRemoteServerUrl(name: string) { + return this.options.manager?.getRemoteServerUrl(name); + } + + async reconnect(name: string, signal?: AbortSignal): Promise { + signal?.throwIfAborted(); + await this.options.manager?.reconnect(name); + signal?.throwIfAborted(); + } + + onStatusChange(listener: Parameters[0]) { + const unsubscribe = this.options.manager?.onStatusChange(listener); + return { + dispose: unsubscribe ?? (() => undefined), + }; + } + + private attachMcpTools(): void { + for (const entry of this.list()) { + this.handleMcpServerStatusChange(entry); + } + this._register( + this.onStatusChange((entry) => { + this.handleMcpServerStatusChange(entry); + }), + ); + } + + private handleMcpServerStatusChange(entry: McpServerEntry): void { + this.events.emit({ + type: 'mcp.server.status', + server: { + name: entry.name, + transport: entry.transport, + status: entry.status, + toolCount: entry.toolCount, + error: entry.error, + }, + }); + if (entry.status === 'connected') { + this.registerConnectedMcpServer(entry); + return; + } + if (entry.status === 'needs-auth') { + this.registerNeedsAuthMcpServer(entry); + return; + } + if (entry.status === 'failed') { + this.unregisterMcpServer(entry.name); + this.events.emit({ + type: 'tool.list.updated', + reason: 'mcp.failed', + serverName: entry.name, + }); + return; + } + if (entry.status === 'disabled' || entry.status === 'pending') { + const removed = this.unregisterMcpServer(entry.name); + if (removed) { + this.events.emit({ + type: 'tool.list.updated', + reason: 'mcp.disconnected', + serverName: entry.name, + }); + } + } + } + + private registerConnectedMcpServer(entry: McpServerEntry): void { + const resolved = this.resolved(entry.name); + if (resolved === undefined) return; + const result = this.registerMcpServer( + entry.name, + resolved.client, + resolved.tools, + resolved.enabledNames, + ); + this.emitMcpToolCollisions(entry.name, result.collisions); + this.events.emit({ + type: 'tool.list.updated', + reason: 'mcp.connected', + serverName: entry.name, + }); + } + + private registerNeedsAuthMcpServer(entry: McpServerEntry): void { + this.unregisterMcpServer(entry.name); + const oauthService = this.oauthService; + const serverUrl = this.getRemoteServerUrl(entry.name); + if (oauthService === undefined || serverUrl === undefined) return; + const tool = createMcpAuthTool({ + serverName: entry.name, + serverUrl, + oauthService, + reconnect: (signal) => this.reconnect(entry.name, signal), + }); + const disposable = this._register(this.registry.register(tool, { source: 'mcp' })); + this.mcpTools.set(tool.name, { disposable, serverName: entry.name }); + this.mcpToolsByServer.set(entry.name, [tool.name]); + this.events.emit({ + type: 'tool.list.updated', + reason: 'mcp.connected', + serverName: entry.name, + }); + } + + private registerMcpServer( + serverName: string, + client: MCPClient, + tools: readonly KosongTool[], + enabledTools: ReadonlySet, + ): { + readonly registered: readonly string[]; + readonly collisions: readonly McpToolCollision[]; + } { + this.unregisterMcpServer(serverName); + const qualifiedNames: string[] = []; + const collisions: McpToolCollision[] = []; + const seenInThisCall = new Map(); + for (const tool of tools) { + if (!enabledTools.has(tool.name)) continue; + const qualified = qualifyMcpToolName(serverName, tool.name); + const firstInThisCall = seenInThisCall.get(qualified); + if (firstInThisCall !== undefined) { + collisions.push({ + qualified, + toolName: tool.name, + collidesWith: { kind: 'same_server', toolName: firstInThisCall }, + }); + continue; + } + const existingEntry = this.mcpTools.get(qualified); + if (existingEntry !== undefined) { + collisions.push({ + qualified, + toolName: tool.name, + collidesWith: { kind: 'other_server', serverName: existingEntry.serverName }, + }); + continue; + } + seenInThisCall.set(qualified, tool.name); + const disposable = this._register( + this.registry.register(this.createMcpTool(qualified, tool, client), { + source: 'mcp', + }), + ); + this.mcpTools.set(qualified, { disposable, serverName }); + qualifiedNames.push(qualified); + } + this.mcpToolsByServer.set(serverName, qualifiedNames); + return { registered: qualifiedNames, collisions }; + } + + private unregisterMcpServer(serverName: string): boolean { + const names = this.mcpToolsByServer.get(serverName); + if (names === undefined) return false; + for (const name of names) { + const entry = this.mcpTools.get(name); + entry?.disposable.dispose(); + this.mcpTools.delete(name); + } + this.mcpToolsByServer.delete(serverName); + return true; + } + + private createMcpTool( + qualifiedName: string, + tool: KosongTool, + client: MCPClient, + ): ExecutableTool { + return { + name: qualifiedName, + description: tool.description, + parameters: tool.parameters, + resolveExecution: (args) => ({ + approvalRule: qualifiedName, + execute: async (context) => { + const result = await client.callTool( + tool.name, + (args ?? {}) as Record, + context.signal, + ); + return normalizeMcpToolResult(mcpResultToExecutableOutput(result, qualifiedName)); + }, + }), + }; + } + + private emitMcpToolCollisions( + serverName: string, + collisions: readonly McpToolCollision[], + ): void { + if (collisions.length === 0) return; + const summary = collisions + .map((collision) => + collision.collidesWith.kind === 'same_server' + ? `"${collision.toolName}" -> ${collision.qualified} (collides with "${collision.collidesWith.toolName}" from the same server)` + : `"${collision.toolName}" -> ${collision.qualified} (collides with server "${collision.collidesWith.serverName}")`, + ) + .join('; '); + this.events.emit({ + type: 'error', + ...makeErrorPayload( + ErrorCodes.MCP_TOOL_NAME_COLLISION, + `MCP server "${serverName}" registered ${collisions.length} tool name` + + `${collisions.length === 1 ? '' : 's'} ` + + `that collide with existing qualified names; the losing tools were dropped: ${summary}`, + { details: { serverName, collisions: collisions as readonly unknown[] } }, + ), + }); + } +} + +function normalizeMcpToolResult(result: { + readonly output: ExecutableToolResult['output']; + readonly isError: boolean; +}): ExecutableToolResult { + if (result.isError) return { output: result.output, isError: true }; + return { output: result.output }; +} + +registerScopedService( + LifecycleScope.Agent, + IMcpRuntimeService, + McpRuntimeService, + InstantiationType.Delayed, + 'mcpRuntime', +); diff --git a/packages/agent-core-v2/src/microCompaction/microCompaction.ts b/packages/agent-core-v2/src/microCompaction/microCompaction.ts index c6483f8e0..d3a17a1aa 100644 --- a/packages/agent-core-v2/src/microCompaction/microCompaction.ts +++ b/packages/agent-core-v2/src/microCompaction/microCompaction.ts @@ -32,6 +32,5 @@ declare module '#/wireRecord' { } } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IMicroCompactionService = createDecorator('agentMicroCompactionService'); diff --git a/packages/agent-core-v2/src/microCompaction/microCompactionService.ts b/packages/agent-core-v2/src/microCompaction/microCompactionService.ts index f97d0fd2c..29c434536 100644 --- a/packages/agent-core-v2/src/microCompaction/microCompactionService.ts +++ b/packages/agent-core-v2/src/microCompaction/microCompactionService.ts @@ -106,7 +106,7 @@ export class MicroCompactionService const cacheAgeMs = Date.now() - lastAssistantAt; if (cacheAgeMs < this.config.cacheMissedThresholdMs) return; - const history = this.context.getHistory(); + const history = this.context.get(); if (this.contextSizeRatio() < this.config.minContextUsageRatio) return; const previousCutoff = this.cutoff; @@ -162,7 +162,7 @@ export class MicroCompactionService readonly deleteCount: number; readonly messages: readonly ContextMessage[]; }): void { - if (this.context.getHistory().length === 0) { + if (this.context.get().length === 0) { this._lastAssistantAt = null; this.reset(); return; @@ -171,7 +171,7 @@ export class MicroCompactionService if (context.messages.some(isCompactionSummary)) { this.reset(); } else if (context.deleteCount > 0) { - this.reset(this.context.getHistory().length); + this.reset(this.context.get().length); } if (context.messages.some(isAssistantCacheAnchor)) { diff --git a/packages/agent-core-v2/src/permission/permission.ts b/packages/agent-core-v2/src/permission/permission.ts index a191e5db4..0981b04d4 100644 --- a/packages/agent-core-v2/src/permission/permission.ts +++ b/packages/agent-core-v2/src/permission/permission.ts @@ -46,6 +46,5 @@ export interface IPermissionService { ): Promise; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IPermissionService = createDecorator('agentPermissionService'); diff --git a/packages/agent-core-v2/src/permissionMode/injection/permissionModeInjection.ts b/packages/agent-core-v2/src/permissionMode/injection/permissionModeInjection.ts index 799a4b765..f402d7eeb 100644 --- a/packages/agent-core-v2/src/permissionMode/injection/permissionModeInjection.ts +++ b/packages/agent-core-v2/src/permissionMode/injection/permissionModeInjection.ts @@ -1,6 +1,6 @@ import type { PermissionMode } from '#/permissionPolicy'; import type { IDisposable } from "#/_base/di"; -import type { IDynamicInjector } from '../../dynamicInjector/dynamicInjector'; +import type { IContextInjector } from '../../contextInjector/contextInjector'; import type { IPermissionModeService } from '../permissionMode'; import AUTO_MODE_ENTER_REMINDER from './permission-mode-auto-enter-reminder.md?raw'; import AUTO_MODE_EXIT_REMINDER from './permission-mode-auto-exit-reminder.md?raw'; @@ -8,7 +8,7 @@ import AUTO_MODE_EXIT_REMINDER from './permission-mode-auto-exit-reminder.md?raw const PERMISSION_MODE_INJECTION_VARIANT = 'permission_mode'; export function registerPermissionModeInjection( - dynamicInjector: IDynamicInjector, + dynamicInjector: IContextInjector, permissionMode: Pick, ): IDisposable { let lastMode: PermissionMode | undefined; diff --git a/packages/agent-core-v2/src/permissionMode/permissionMode.ts b/packages/agent-core-v2/src/permissionMode/permissionMode.ts index 938ab63aa..4553a306f 100644 --- a/packages/agent-core-v2/src/permissionMode/permissionMode.ts +++ b/packages/agent-core-v2/src/permissionMode/permissionMode.ts @@ -18,6 +18,5 @@ export interface IPermissionModeService { }>; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IPermissionModeService = createDecorator('agentPermissionModeService'); diff --git a/packages/agent-core-v2/src/permissionMode/permissionModeService.ts b/packages/agent-core-v2/src/permissionMode/permissionModeService.ts index b667c5e4f..b45daa895 100644 --- a/packages/agent-core-v2/src/permissionMode/permissionModeService.ts +++ b/packages/agent-core-v2/src/permissionMode/permissionModeService.ts @@ -5,8 +5,8 @@ import { import { InstantiationType } from '#/_base/di/extensions'; import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; -import { IDynamicInjector } from '#/dynamicInjector'; -import { IEventBus } from '#/eventBus'; +import { IContextInjector } from '../contextInjector'; +import { IEventSink } from '../eventSink'; import { OrderedHookSlot } from '../hooks'; import { IReplayBuilderService } from '#/replayBuilder'; import type { WireRecord } from '#/wireRecord'; @@ -36,9 +36,9 @@ export class PermissionModeService extends Disposable implements IPermissionMode constructor( @IWireRecord private readonly wireRecord: IWireRecord, - @IEventBus private readonly events: IEventBus, + @IEventSink private readonly events: IEventSink, @IReplayBuilderService private readonly replayBuilder: IReplayBuilderService, - @IDynamicInjector dynamicInjector: IDynamicInjector, + @IContextInjector dynamicInjector: IContextInjector, ) { super(); this._register( diff --git a/packages/agent-core-v2/src/permissionPolicy/permissionPolicy.ts b/packages/agent-core-v2/src/permissionPolicy/permissionPolicy.ts index 6ed8fc1a9..5a2b6e6ea 100644 --- a/packages/agent-core-v2/src/permissionPolicy/permissionPolicy.ts +++ b/packages/agent-core-v2/src/permissionPolicy/permissionPolicy.ts @@ -19,6 +19,5 @@ export interface IPermissionPolicyService { ): Promise; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IPermissionPolicyService = createDecorator('agentPermissionPolicyService'); diff --git a/packages/agent-core-v2/src/permissionRules/permissionRules.ts b/packages/agent-core-v2/src/permissionRules/permissionRules.ts index 2bf3bb801..81e5d9146 100644 --- a/packages/agent-core-v2/src/permissionRules/permissionRules.ts +++ b/packages/agent-core-v2/src/permissionRules/permissionRules.ts @@ -60,6 +60,5 @@ export interface IPermissionRulesService { }>; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IPermissionRulesService = createDecorator('agentPermissionRulesService'); diff --git a/packages/agent-core-v2/src/plan/plan.ts b/packages/agent-core-v2/src/plan/plan.ts index aa17057df..0c76deabc 100644 --- a/packages/agent-core-v2/src/plan/plan.ts +++ b/packages/agent-core-v2/src/plan/plan.ts @@ -34,6 +34,5 @@ declare module '#/wireRecord' { } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IPlanService = createDecorator('agentPlanService'); diff --git a/packages/agent-core-v2/src/plan/planService.ts b/packages/agent-core-v2/src/plan/planService.ts index e9c0e4671..e4d6a911e 100644 --- a/packages/agent-core-v2/src/plan/planService.ts +++ b/packages/agent-core-v2/src/plan/planService.ts @@ -14,8 +14,8 @@ import { import { toInputJsonSchema } from '#/_base/tools/support/input-schema'; import { generateHeroSlug } from "#/_base/utils/hero-slug"; import { IContextMemory, type ContextMessage } from '#/contextMemory'; -import { IDynamicInjector } from '#/dynamicInjector'; -import { IEventBus } from '#/eventBus'; +import { IContextInjector } from '../contextInjector'; +import { IEventSink } from '../eventSink'; import { IKaosService } from '#/kaos'; import { IProfileService } from '#/profile'; import { IReplayBuilderService } from '#/replayBuilder'; @@ -52,12 +52,12 @@ export class PlanService extends Disposable implements IPlanService { constructor( @IContextMemory private readonly context: IContextMemory, @IWireRecord private readonly wireRecord: IWireRecord, - @IEventBus private readonly events: IEventBus, + @IEventSink private readonly events: IEventSink, @IKaosService private readonly kaosService: IKaosService, @IProfileService private readonly profile: IProfileService, @IReplayBuilderService private readonly replayBuilder: IReplayBuilderService, @IToolRegistry toolRegistry: IToolRegistry, - @IDynamicInjector dynamicInjector: IDynamicInjector, + @IContextInjector dynamicInjector: IContextInjector, @ITelemetryService private readonly telemetry: ITelemetryService, ) { super(); @@ -112,7 +112,7 @@ export class PlanService extends Disposable implements IPlanService { let wasActive = false; this._register( - dynamicInjector.register(PLAN_MODE_INJECTION_VARIANT, async ({ injectedAt }) => { + dynamicInjector.register(PLAN_MODE_INJECTION_VARIANT, async ({ lastInjectedAt: injectedAt }) => { if (!this.isActive) { if (!wasActive) return undefined; wasActive = false; @@ -125,7 +125,7 @@ export class PlanService extends Disposable implements IPlanService { } return this.fullReminder(); } - const variant = planModeReminderVariant(injectedAt, this.context.getHistory()); + const variant = planModeReminderVariant(injectedAt, this.context.get()); if (variant === 'full') return this.fullReminder(); if (variant === 'sparse') return this.sparseReminder(); return undefined; diff --git a/packages/agent-core-v2/src/profile/profile.ts b/packages/agent-core-v2/src/profile/profile.ts index 5c9e5b561..82fc1fa92 100644 --- a/packages/agent-core-v2/src/profile/profile.ts +++ b/packages/agent-core-v2/src/profile/profile.ts @@ -86,5 +86,4 @@ export interface IProfileService { removeActiveTool(name: string): void; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IProfileService = createDecorator('profileService.agent'); diff --git a/packages/agent-core-v2/src/profile/profileService.ts b/packages/agent-core-v2/src/profile/profileService.ts index a315ca350..46d5e603f 100644 --- a/packages/agent-core-v2/src/profile/profileService.ts +++ b/packages/agent-core-v2/src/profile/profileService.ts @@ -21,7 +21,7 @@ import { isMcpToolName } from '#/mcp/tool-naming'; import type { ResolvedAgentProfile, SystemPromptContext } from '#/profile'; import type { ResolvedRuntimeProvider } from '#/session/provider-manager'; -import { IEventBus } from '#/eventBus'; +import { IEventSink } from '../eventSink'; import { IReplayBuilderService } from '#/replayBuilder'; import { ITelemetryService } from '#/telemetry'; import type { ToolSource } from '#/toolRegistry'; @@ -59,7 +59,7 @@ export class ProfileService implements IProfileService { constructor( options: ProfileServiceOptions = {}, @IWireRecord private readonly wireRecord: IWireRecord, - @IEventBus private readonly events: IEventBus, + @IEventSink private readonly events: IEventSink, @IReplayBuilderService private readonly replayBuilder: IReplayBuilderService, @ITelemetryService private readonly telemetry: ITelemetryService, @IConfigRegistry configRegistry: IConfigRegistry, diff --git a/packages/agent-core-v2/src/prompt/prompt.ts b/packages/agent-core-v2/src/prompt/prompt.ts index cf9725091..d1ed04fa8 100644 --- a/packages/agent-core-v2/src/prompt/prompt.ts +++ b/packages/agent-core-v2/src/prompt/prompt.ts @@ -11,5 +11,4 @@ export interface IPromptService { clear(): void; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IPromptService = createDecorator('promptService.agent'); diff --git a/packages/agent-core-v2/src/prompt/promptService.ts b/packages/agent-core-v2/src/prompt/promptService.ts index 1cea30802..7f7245a9e 100644 --- a/packages/agent-core-v2/src/prompt/promptService.ts +++ b/packages/agent-core-v2/src/prompt/promptService.ts @@ -3,7 +3,7 @@ import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; import { ErrorCodes, KimiError, makeErrorPayload } from "#/errors"; import { IContextMemory, USER_PROMPT_ORIGIN, type ContextMessage } from '#/contextMemory'; -import { IEventBus } from '#/eventBus'; +import { IEventSink } from '../eventSink'; import { ITurnService, type Turn } from '#/turn'; import { IWireRecord } from '#/wireRecord'; import { IPromptService } from './prompt'; @@ -16,7 +16,7 @@ export class PromptService implements IPromptService { @IContextMemory private readonly context: IContextMemory, @ITurnService private readonly turnService: ITurnService, @IWireRecord private readonly wireRecord: IWireRecord, - @IEventBus private readonly events: IEventBus, + @IEventSink private readonly events: IEventSink, ) { turnService.hooks.beforeStep.register('prompt-service-steer-before-step', async (_ctx, next) => { this.flushSteerQueue(); @@ -62,7 +62,7 @@ export class PromptService implements IPromptService { undo(count: number): number { if (count <= 0) return 0; - const history = this.context.getHistory(); + const history = this.context.get(); let removedUserCount = 0; let stoppedAtCompaction = false; for (let index = history.length - 1; index >= 0; index--) { @@ -74,7 +74,7 @@ export class PromptService implements IPromptService { break; } - this.context.spliceHistory(index, 1, []); + this.context.splice(index, 1, []); if (isRealUserPrompt(message)) { removedUserCount++; if (removedUserCount >= count) break; @@ -100,14 +100,14 @@ export class PromptService implements IPromptService { clear(): void { this.steerQueue.length = 0; - const historyLength = this.context.getHistory().length; + const historyLength = this.context.get().length; if (historyLength === 0) return; - this.context.spliceHistory(0, historyLength, []); + this.context.splice(0, historyLength, []); } private append(...messages: ContextMessage[]): void { if (messages.length === 0) return; - this.context.spliceHistory(this.context.getHistory().length, 0, messages); + this.context.splice(this.context.get().length, 0, messages); } private observe(turn: Turn): void { diff --git a/packages/agent-core-v2/src/replayBuilder/replayBuilder.ts b/packages/agent-core-v2/src/replayBuilder/replayBuilder.ts index 08da178ed..44f6d3acc 100644 --- a/packages/agent-core-v2/src/replayBuilder/replayBuilder.ts +++ b/packages/agent-core-v2/src/replayBuilder/replayBuilder.ts @@ -28,7 +28,6 @@ export interface IReplayBuilderService { buildResult(): readonly AgentReplayRecord[]; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IReplayBuilderService = createDecorator( 'agentReplayBuilderService', ); diff --git a/packages/agent-core-v2/src/rpc/rpc.ts b/packages/agent-core-v2/src/rpc/rpc.ts index 2758594b6..e61ef29c6 100644 --- a/packages/agent-core-v2/src/rpc/rpc.ts +++ b/packages/agent-core-v2/src/rpc/rpc.ts @@ -9,10 +9,8 @@ export interface IAgentRPCService extends PromisableMethods {} export interface ISessionRPCService extends PromisableMethods {} -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IAgentRPCService = createDecorator('agentRPCService'); -// eslint-disable-next-line @typescript-eslint/no-redeclare export const ISessionRPCService = createDecorator('agentSessionRPCService'); diff --git a/packages/agent-core-v2/src/rpc/rpcService.ts b/packages/agent-core-v2/src/rpc/rpcService.ts index 86ac64a5c..4cb0d04c4 100644 --- a/packages/agent-core-v2/src/rpc/rpcService.ts +++ b/packages/agent-core-v2/src/rpc/rpcService.ts @@ -62,7 +62,7 @@ export class AgentRPCService implements IAgentRPCService { @IUsageService private readonly usage: IUsageService, @ITelemetryService private readonly telemetry: ITelemetryService, @IGoalService private readonly goal: IGoalService, - ) {} + ) { } prompt(payload: PromptPayload): void { this.promptService.prompt({ @@ -81,11 +81,14 @@ export class AgentRPCService implements IAgentRPCService { }); } - cancel(payload: CancelPayload): void { + cancel({ turnId }: CancelPayload): void { if (this.turnService.getActiveTurn() !== undefined) { this.telemetry.track('cancel', { from: 'streaming' }); } - this.turnService.cancel(payload.turnId); + const turn = this.turnService.getActiveTurn(); + if (turn === undefined) return; + if (turnId !== undefined && turn.id !== turnId) return; + turn.abortController.abort(); } undoHistory(payload: UndoHistoryPayload): void { @@ -174,9 +177,9 @@ export class AgentRPCService implements IAgentRPCService { } clearContext(_payload: EmptyPayload): void { - const history = this.context.getHistory(); + const history = this.context.get(); if (history.length === 0) return; - this.context.spliceHistory(0, history.length, []); + this.context.splice(0, history.length, []); } activateSkill(payload: ActivateSkillPayload): void { @@ -213,7 +216,7 @@ export class AgentRPCService implements IAgentRPCService { getContext(_payload: EmptyPayload) { return { - history: this.context.getHistory(), + history: this.context.get(), tokenCount: this.contextSize.getStatus().contextTokens, }; } diff --git a/packages/agent-core-v2/src/skill/skill.ts b/packages/agent-core-v2/src/skill/skill.ts index 9501023f4..b838d4d2d 100644 --- a/packages/agent-core-v2/src/skill/skill.ts +++ b/packages/agent-core-v2/src/skill/skill.ts @@ -23,6 +23,5 @@ export interface IAgentSkillService { activateFromModel(input: ModelSkillActivationInput): ExecutableToolResult; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IAgentSkillService = createDecorator('agentSkillService'); diff --git a/packages/agent-core-v2/src/skill/skillService.ts b/packages/agent-core-v2/src/skill/skillService.ts index c5b95c635..9ba64a66d 100644 --- a/packages/agent-core-v2/src/skill/skillService.ts +++ b/packages/agent-core-v2/src/skill/skillService.ts @@ -20,7 +20,7 @@ import { type SkillCatalog, type SkillDefinition, } from './types'; -import { IEventBus } from '#/eventBus'; +import { IEventSink } from '../eventSink'; import { IPromptService } from '#/prompt'; import { ITelemetryService } from '#/telemetry'; import type { Turn } from '#/turn'; @@ -48,7 +48,7 @@ export class AgentSkillService extends Disposable implements IAgentSkillService constructor( options: AgentSkillServiceOptions = {}, @IPromptService private readonly prompt: IPromptService, - @IEventBus private readonly events: IEventBus, + @IEventSink private readonly events: IEventSink, @IWireRecord private readonly wireRecord: IWireRecord, @ITelemetryService private readonly telemetry: ITelemetryService, ) { diff --git a/packages/agent-core-v2/src/subagentHost/subagentHost.ts b/packages/agent-core-v2/src/subagentHost/subagentHost.ts index cefc079c8..ed9871cf9 100644 --- a/packages/agent-core-v2/src/subagentHost/subagentHost.ts +++ b/packages/agent-core-v2/src/subagentHost/subagentHost.ts @@ -53,5 +53,4 @@ export interface ISubagentHost { } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const ISubagentHost = createDecorator('agentSubagentHostService'); diff --git a/packages/agent-core-v2/src/swarm/swarm.ts b/packages/agent-core-v2/src/swarm/swarm.ts index 6504edf41..d23641739 100644 --- a/packages/agent-core-v2/src/swarm/swarm.ts +++ b/packages/agent-core-v2/src/swarm/swarm.ts @@ -19,5 +19,4 @@ declare module '#/wireRecord' { } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const ISwarmService = createDecorator('agentSwarmService'); diff --git a/packages/agent-core-v2/src/swarm/swarmService.ts b/packages/agent-core-v2/src/swarm/swarmService.ts index 64d00457d..1ec685a0b 100644 --- a/packages/agent-core-v2/src/swarm/swarmService.ts +++ b/packages/agent-core-v2/src/swarm/swarmService.ts @@ -5,7 +5,7 @@ import { InstantiationType } from '#/_base/di/extensions'; import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; import type { ContextMessage } from '#/contextMemory'; import { IContextMemory } from '#/contextMemory'; -import { IEventBus } from '#/eventBus'; +import { IEventSink } from '../eventSink'; import { ISubagentHost } from '#/subagentHost'; import { IToolRegistry } from '#/toolRegistry'; import { ITurnService } from '#/turn'; @@ -31,7 +31,7 @@ export class SwarmService extends Disposable implements ISwarmService { options: SwarmServiceOptions = {}, @IContextMemory private readonly context: IContextMemory, @IWireRecord private readonly wireRecord: IWireRecord, - @IEventBus private readonly events: IEventBus, + @IEventSink private readonly events: IEventSink, @ITurnService turnService?: ITurnService, @IToolRegistry toolRegistry?: IToolRegistry, @ISubagentHost subagentHost?: ISubagentHost, @@ -140,16 +140,16 @@ export class SwarmService extends Disposable implements ISwarmService { variant, }, }; - this.context.spliceHistory(this.context.getHistory().length, 0, [message]); + this.context.splice(this.context.get().length, 0, [message]); } private removeLastSwarmReminder(): boolean { - const history = this.context.getHistory(); + const history = this.context.get(); const lastIndex = history.length - 1; const last = history[lastIndex]; if (last?.origin?.kind !== 'injection') return false; if (last.origin.variant !== 'swarm_mode') return false; - this.context.spliceHistory(lastIndex, 1, []); + this.context.splice(lastIndex, 1, []); return true; } } diff --git a/packages/agent-core-v2/src/telemetry/telemetry.ts b/packages/agent-core-v2/src/telemetry/telemetry.ts index 0653751db..b5ceb7361 100644 --- a/packages/agent-core-v2/src/telemetry/telemetry.ts +++ b/packages/agent-core-v2/src/telemetry/telemetry.ts @@ -56,7 +56,6 @@ export const nullTelemetryAppender: ITelemetryAppender = { shutdown: () => {}, }; -// eslint-disable-next-line @typescript-eslint/no-redeclare export const ITelemetryService = createDecorator( 'agentTelemetryService', ); diff --git a/packages/agent-core-v2/src/todoList/todoList.ts b/packages/agent-core-v2/src/todoList/todoList.ts index 9f5d8acfa..093ae86d9 100644 --- a/packages/agent-core-v2/src/todoList/todoList.ts +++ b/packages/agent-core-v2/src/todoList/todoList.ts @@ -4,5 +4,4 @@ export interface ITodoListService { readonly _serviceBrand: undefined; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const ITodoListService = createDecorator('agentTodoListService'); diff --git a/packages/agent-core-v2/src/todoList/todoListService.ts b/packages/agent-core-v2/src/todoList/todoListService.ts index dbe554105..d55915842 100644 --- a/packages/agent-core-v2/src/todoList/todoListService.ts +++ b/packages/agent-core-v2/src/todoList/todoListService.ts @@ -13,7 +13,7 @@ import { todoListStaleReminder, } from './todoListReminder'; import { IContextMemory } from '#/contextMemory'; -import { IDynamicInjector } from '#/dynamicInjector'; +import { IContextInjector } from '../contextInjector'; import { IProfileService } from '#/profile'; import { IToolRegistry } from '#/toolRegistry'; import { IToolStoreService } from '#/toolStore'; @@ -29,7 +29,7 @@ export class TodoListService extends Disposable implements ITodoListService { @IProfileService private readonly profile: IProfileService, @IToolStoreService private readonly toolStore: IToolStoreService, @IToolRegistry toolRegistry: IToolRegistry, - @IDynamicInjector dynamicInjector: IDynamicInjector, + @IContextInjector dynamicInjector: IContextInjector, ) { super(); this._register(toolRegistry.register(new TodoListTool(toolStore))); @@ -45,7 +45,7 @@ export class TodoListService extends Disposable implements ITodoListService { private staleReminder(): string | undefined { return todoListStaleReminder({ active: this.profile.isToolActive(TODO_LIST_TOOL_NAME, 'builtin'), - history: this.context.getHistory(), + history: this.context.get(), todos: this.getTodos(), }); } diff --git a/packages/agent-core-v2/src/toolExecutor/toolExecutor.ts b/packages/agent-core-v2/src/toolExecutor/toolExecutor.ts index 8a8436fe7..28165442a 100644 --- a/packages/agent-core-v2/src/toolExecutor/toolExecutor.ts +++ b/packages/agent-core-v2/src/toolExecutor/toolExecutor.ts @@ -17,5 +17,4 @@ export interface IToolExecutor { ): Promise; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IToolExecutor = createDecorator('toolExecutorService'); diff --git a/packages/agent-core-v2/src/toolRegistry/toolRegistry.ts b/packages/agent-core-v2/src/toolRegistry/toolRegistry.ts index d3b223315..64d533e0b 100644 --- a/packages/agent-core-v2/src/toolRegistry/toolRegistry.ts +++ b/packages/agent-core-v2/src/toolRegistry/toolRegistry.ts @@ -66,5 +66,4 @@ export interface IToolRegistry { }>; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IToolRegistry = createDecorator('agentToolRegistryService'); diff --git a/packages/agent-core-v2/src/toolStore/toolStore.ts b/packages/agent-core-v2/src/toolStore/toolStore.ts index 7cfb655c2..12be1b6f6 100644 --- a/packages/agent-core-v2/src/toolStore/toolStore.ts +++ b/packages/agent-core-v2/src/toolStore/toolStore.ts @@ -23,5 +23,4 @@ export interface IToolStoreService extends ToolStore { }>; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IToolStoreService = createDecorator('agentToolStoreService'); diff --git a/packages/agent-core-v2/src/turn/turn.ts b/packages/agent-core-v2/src/turn/turn.ts index 2ef5e58d4..346ab65b3 100644 --- a/packages/agent-core-v2/src/turn/turn.ts +++ b/packages/agent-core-v2/src/turn/turn.ts @@ -60,7 +60,6 @@ export interface ITurnService { readonly _serviceBrand: undefined; launch(origin: PromptOrigin): Turn; getActiveTurn(): Turn | undefined; - cancel(turnId?: number, reason?: unknown): void; readonly hooks: Hooks<{ onLaunched: { turn: Turn }; @@ -72,5 +71,4 @@ export interface ITurnService { }>; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const ITurnService = createDecorator('turnService'); diff --git a/packages/agent-core-v2/src/turn/turnService.ts b/packages/agent-core-v2/src/turn/turnService.ts index b8c6af904..c4d0ea7d2 100644 --- a/packages/agent-core-v2/src/turn/turnService.ts +++ b/packages/agent-core-v2/src/turn/turnService.ts @@ -7,7 +7,7 @@ import { toKimiErrorPayload, type KimiErrorPayload } from "#/errors"; import { isUserCancellation, userCancellationReason } from "#/_base/utils/abort"; import type { ContextMessage, PromptOrigin } from '#/contextMemory'; import { IContextMemory, USER_PROMPT_ORIGIN } from '#/contextMemory'; -import { IEventBus } from '#/eventBus'; +import { IEventSink } from '../eventSink'; import { IExternalHooksService } from '#/externalHooks'; import { OrderedHookSlot } from '#/hooks'; import { ILoopService } from '#/loop'; @@ -56,7 +56,7 @@ export class TurnService implements ITurnService { constructor( @ILoopService private readonly loop: ILoopService, @IUsageService private readonly usage: IUsageService, - @IEventBus private readonly events: IEventBus, + @IEventSink private readonly events: IEventSink, @IWireRecord private readonly wireRecord: IWireRecord, @IContextMemory private readonly context: IContextMemory, @IExternalHooksService private readonly externalHooks: IExternalHooksService, @@ -113,13 +113,6 @@ export class TurnService implements ITurnService { return this.activeTurn; } - cancel(turnId?: number, reason?: unknown): void { - const turn = this.activeTurn; - if (turn === undefined) return; - if (turnId !== undefined && turn.id !== turnId) return; - turn.abortController.abort(reason ?? userCancellationReason()); - } - private async runTurn(turn: Turn, origin: PromptOrigin): Promise { const startedAt = Date.now(); const telemetryMode = this.telemetryMode(); @@ -201,7 +194,7 @@ export class TurnService implements ITurnService { origin: PromptOrigin, ): Promise { if (origin.kind !== 'user') return undefined; - const promptMessage = this.context.getHistory().at(-1); + const promptMessage = this.context.get().at(-1); if (!shouldRunUserPromptHook(promptMessage)) return undefined; const hookResult = await this.externalHooks.triggerUserPromptSubmit( @@ -244,7 +237,7 @@ export class TurnService implements ITurnService { private append(...messages: ContextMessage[]): void { if (messages.length === 0) return; - this.context.spliceHistory(this.context.getHistory().length, 0, messages); + this.context.splice(this.context.get().length, 0, messages); } private rejectReady(turn: Turn, reason: unknown): void { diff --git a/packages/agent-core-v2/src/turnRunner/turnRunnerService.ts b/packages/agent-core-v2/src/turnRunner/turnRunnerService.ts new file mode 100644 index 000000000..27aa8e94e --- /dev/null +++ b/packages/agent-core-v2/src/turnRunner/turnRunnerService.ts @@ -0,0 +1,324 @@ +import { + IInstantiationService, + registerSingleton, + SyncDescriptor, +} from "#/_base/di"; +import type { ContextMessage, PromptOrigin } from '../../../agent/context'; +import { USER_PROMPT_ORIGIN } from '../../../agent/context'; +import { toKimiErrorPayload, type KimiErrorPayload } from "#/_base/errors"; +import { isUserCancellation, userCancellationReason } from "#/_base/utils/abort"; +import { IContextMemory } from '../contextMemory/contextMemory'; +import { IEventSink } from '../eventSink/eventSink'; +import { IExternalHooksService } from '../externalHooks/externalHooks'; +import { OrderedHookSlot } from '../hooks'; +import { ILoopService } from '../loop/loop'; +import { IPlanService } from '../plan/planMode'; +import { ITelemetryService } from '../telemetry/telemetry'; +import type { + Turn, + TurnEndedContext, + TurnResult, + TurnStepContext, +} from '../types'; +import { IUsageService } from '../usage/usage'; +import { IWireRecord } from '../wireRecord/wireRecord'; +import { ITurnRunner } from './turnRunner'; + +declare module '../types' { + interface WireRecordMap { + 'turn.launch': { + turnId: number; + origin: PromptOrigin; + }; + } +} + +export class TurnRunnerService implements ITurnRunner { + private nextTurnId = 0; + private activeTurn: Turn | undefined; + private readonly readyControllers = new WeakMap>(); + private readonly readySettled = new WeakSet(); + private readonly currentStepByTurn = new Map(); + private readonly interruptedTelemetryTurnIds = new Set(); + private readonly telemetryModeByTurn = new Map(); + + readonly hooks = { + onLaunched: new OrderedHookSlot<{ turn: Turn }>(), + onEnded: new OrderedHookSlot(), + beforeStep: new OrderedHookSlot(), + afterStep: new OrderedHookSlot(), + }; + + constructor( + @ILoopService private readonly loop: ILoopService, + @IUsageService private readonly usage: IUsageService, + @IEventSink private readonly events: IEventSink, + @IWireRecord private readonly wireRecord: IWireRecord, + @IContextMemory private readonly context: IContextMemory, + @IExternalHooksService private readonly externalHooks: IExternalHooksService, + @IInstantiationService private readonly instantiation: IInstantiationService, + @ITelemetryService private readonly telemetry: ITelemetryService, + ) { + wireRecord.register('turn.launch', (record) => { + this.restoreLaunch(record.turnId); + }); + this.hooks.beforeStep.register('turn-before-step-event', async (ctx, next) => { + await next(); + this.resolveReady(ctx.turn); + }); + this.events.on((event) => { + if (event.type === 'turn.step.started') { + this.currentStepByTurn.set(event.turnId, event.step); + return; + } + if (event.type === 'turn.step.interrupted') { + this.trackTurnInterrupted(event.turnId, event.step); + } + }); + } + + launch(origin: PromptOrigin): Turn { + if (this.activeTurn !== undefined) { + throw new Error(`Cannot launch a new turn while turn ${this.activeTurn.id} is active`); + } + + const turnId = this.nextTurnId; + this.wireRecord.append({ type: 'turn.launch', turnId, origin }); + this.restoreLaunch(turnId); + const abortController = new AbortController(); + const ready = createControlledPromise(); + const turn: MutableTurn = { + id: turnId, + abortController, + ready: ready.promise, + result: Promise.resolve({ reason: 'failed' }), + }; + this.readyControllers.set(turn, ready); + void ready.promise.catch(() => undefined); + this.activeTurn = turn; + turn.result = this.runTurn(turn, origin); + void this.hooks.onLaunched.run({ turn }); + return turn; + } + + getActiveTurn(): Turn | undefined { + return this.activeTurn; + } + + cancel(turnId?: number, reason?: unknown): void { + const turn = this.activeTurn; + if (turn === undefined) return; + if (turnId !== undefined && turn.id !== turnId) return; + turn.abortController.abort(reason ?? userCancellationReason()); + } + + private async runTurn(turn: Turn, origin: PromptOrigin): Promise { + const startedAt = Date.now(); + const telemetryMode = this.telemetryMode(); + this.telemetryModeByTurn.set(turn.id, telemetryMode); + let result: TurnResult | undefined; + try { + this.usage.beginTurn(); + this.telemetry.track('turn_started', { mode: telemetryMode }); + this.events.emit({ type: 'turn.started', turnId: turn.id, origin }); + const promptHookResult = await this.applyUserPromptHook(turn, origin); + if (promptHookResult !== undefined) { + result = promptHookResult; + return result; + } + result = await this.loop.runTurn(turn, { + beforeStep: this.hooks.beforeStep, + afterStep: this.hooks.afterStep, + }); + return result; + } catch (error) { + if (turn.abortController.signal.aborted) { + result = { reason: 'cancelled', error: turn.abortController.signal.reason }; + this.rejectReady(turn, turn.abortController.signal.reason); + return result; + } + this.externalHooks.triggerStopFailure(error, turn.abortController.signal); + this.rejectReady(turn, error); + result = { reason: 'failed', error }; + return result; + } finally { + if (result !== undefined) { + this.rejectReady(turn, result); + } + this.usage.endTurn(); + if (this.activeTurn === turn) { + this.activeTurn = undefined; + } + if (result !== undefined) { + const ended = toTurnEndedEvent(turn, result, Date.now() - startedAt); + if ( + ended.reason === 'cancelled' && + isUserCancellation(turn.abortController.signal.reason) + ) { + this.externalHooks.triggerInterrupt({ turnId: turn.id, reason: 'cancelled' }); + } + this.events.emit(ended); + if (ended.error !== undefined) { + this.events.emit({ type: 'error', ...ended.error }); + } + if (ended.reason !== 'completed') { + this.trackTurnInterrupted(turn.id, this.currentStepByTurn.get(turn.id) ?? 0); + } + } + if (result !== undefined) { + await this.hooks.onEnded.run({ turn, result }); + } + this.currentStepByTurn.delete(turn.id); + this.interruptedTelemetryTurnIds.delete(turn.id); + this.telemetryModeByTurn.delete(turn.id); + } + } + + private resolveReady(turn: Turn): void { + if (this.readySettled.has(turn)) return; + this.readySettled.add(turn); + this.readyControllers.get(turn)?.resolve(); + } + + private restoreLaunch(turnId: number): void { + if (Number.isInteger(turnId) && turnId >= this.nextTurnId) { + this.nextTurnId = turnId + 1; + } + } + + private async applyUserPromptHook( + turn: Turn, + origin: PromptOrigin, + ): Promise { + if (origin.kind !== 'user') return undefined; + const promptMessage = this.context.get().at(-1); + if (!shouldRunUserPromptHook(promptMessage)) return undefined; + + const hookResult = await this.externalHooks.triggerUserPromptSubmit( + promptMessage.content, + turn.abortController.signal, + ); + if (hookResult?.action === 'block') { + this.append({ + role: 'assistant', + content: [{ type: 'text', text: hookResult.text }], + toolCalls: [], + origin: { kind: 'hook_result', event: hookResult.event, blocked: true }, + }); + this.events.emit({ + type: 'hook.result', + turnId: turn.id, + hookEvent: hookResult.event, + content: hookResult.message, + blocked: true, + }); + return { reason: 'completed' }; + } + + if (hookResult?.action === 'append') { + this.append({ + role: 'user', + content: [{ type: 'text', text: hookResult.text }], + toolCalls: [], + origin: { kind: 'hook_result', event: hookResult.event }, + }); + this.events.emit({ + type: 'hook.result', + turnId: turn.id, + hookEvent: hookResult.event, + content: hookResult.message, + }); + } + return undefined; + } + + private append(...messages: ContextMessage[]): void { + if (messages.length === 0) return; + this.context.splice(this.context.get().length, 0, messages); + } + + private rejectReady(turn: Turn, reason: unknown): void { + if (this.readySettled.has(turn)) return; + this.readySettled.add(turn); + this.readyControllers.get(turn)?.reject(reason); + } + + private trackTurnInterrupted(turnId: number, atStep: number): void { + if (this.interruptedTelemetryTurnIds.has(turnId)) return; + this.interruptedTelemetryTurnIds.add(turnId); + this.telemetry.track('turn_interrupted', { + mode: this.telemetryModeByTurn.get(turnId) ?? this.telemetryMode(), + at_step: atStep, + }); + } + + private telemetryMode(): 'agent' | 'plan' { + const planMode = this.instantiation.invokeFunction((accessor) => + accessor.get(IPlanService), + ); + return planMode.isActive ? 'plan' : 'agent'; + } +} + +function shouldRunUserPromptHook(message: ContextMessage | undefined): message is ContextMessage { + if (message === undefined || message.role !== 'user') return false; + return (message.origin ?? USER_PROMPT_ORIGIN).kind === 'user'; +} + +function toTurnEndedEvent( + turn: Turn, + result: TurnResult, + durationMs: number, +): { + type: 'turn.ended'; + turnId: number; + reason: TurnResult['reason']; + error?: KimiErrorPayload; + durationMs: number; +} { + if (result.reason !== 'failed' || result.error === undefined) { + return { type: 'turn.ended', turnId: turn.id, reason: result.reason, durationMs }; + } + return { + type: 'turn.ended', + turnId: turn.id, + reason: result.reason, + error: summarizeTurnError(result.error, turn.id), + durationMs, + }; +} + +const LLM_NOT_SET_MESSAGE = 'LLM not set, send "/login" to login'; + +function summarizeTurnError(error: unknown, turnId: number): KimiErrorPayload { + const payload = toKimiErrorPayload(error); + const details = { ...payload.details, turnId }; + // Substitute a friendlier, login-aware message for model-not-configured. The + // raw "Model not set" / "Provider not set" text is not actionable. + if (payload.code === 'model.not_configured') { + return { ...payload, message: LLM_NOT_SET_MESSAGE, details }; + } + return { ...payload, details }; +} + +interface ControlledPromise { + readonly promise: Promise; + resolve(value: T | PromiseLike): void; + reject(reason?: unknown): void; +} + +type MutableTurn = { + -readonly [K in keyof Turn]: Turn[K]; +}; + +function createControlledPromise(): ControlledPromise { + let resolve!: (value: T | PromiseLike) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +} + +registerSingleton(ITurnRunner, new SyncDescriptor(TurnRunnerService, [], true)); diff --git a/packages/agent-core-v2/src/usage/usage.ts b/packages/agent-core-v2/src/usage/usage.ts index c462c00d3..3b63293f0 100644 --- a/packages/agent-core-v2/src/usage/usage.ts +++ b/packages/agent-core-v2/src/usage/usage.ts @@ -18,5 +18,4 @@ export interface IUsageService { status(): UsageStatus | undefined; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IUsageService = createDecorator('usageService.agent'); diff --git a/packages/agent-core-v2/src/usage/usageService.ts b/packages/agent-core-v2/src/usage/usageService.ts index aa510c3ed..2309782b1 100644 --- a/packages/agent-core-v2/src/usage/usageService.ts +++ b/packages/agent-core-v2/src/usage/usageService.ts @@ -5,7 +5,7 @@ import { InstantiationType } from '#/_base/di/extensions'; import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; -import { IEventBus } from '#/eventBus'; +import { IEventSink } from '../eventSink'; import type { UsageRecordScope, UsageStatus } from './usage'; import { IUsageService } from './usage'; import { IWireRecord } from '#/wireRecord'; @@ -26,7 +26,7 @@ export class UsageService implements IUsageService { constructor( @IWireRecord private readonly wireRecord: IWireRecord, - @IEventBus private readonly events: IEventBus, + @IEventSink private readonly events: IEventSink, ) { wireRecord.register('usage.record', (record) => { this.apply(record.model, record.usage, 'session'); diff --git a/packages/agent-core-v2/src/userTool/userTool.ts b/packages/agent-core-v2/src/userTool/userTool.ts index 66deedba3..2890a67b5 100644 --- a/packages/agent-core-v2/src/userTool/userTool.ts +++ b/packages/agent-core-v2/src/userTool/userTool.ts @@ -25,5 +25,4 @@ export interface IUserToolService { unregister(name: string): void; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IUserToolService = createDecorator('agentUserToolService'); diff --git a/packages/agent-core-v2/src/wireRecord/wireRecord.ts b/packages/agent-core-v2/src/wireRecord/wireRecord.ts index 8ce058d19..b64844294 100644 --- a/packages/agent-core-v2/src/wireRecord/wireRecord.ts +++ b/packages/agent-core-v2/src/wireRecord/wireRecord.ts @@ -90,5 +90,4 @@ export interface IWireRecord { }>; } -// eslint-disable-next-line @typescript-eslint/no-redeclare export const IWireRecord = createDecorator('agentWireRecordService'); diff --git a/packages/agent-core-v2/test/background/background.test.ts b/packages/agent-core-v2/test/background/background.test.ts index 8094276e9..2d0a02baa 100644 --- a/packages/agent-core-v2/test/background/background.test.ts +++ b/packages/agent-core-v2/test/background/background.test.ts @@ -6,7 +6,7 @@ import { TestInstantiationService } from '#/_base/di/test'; import { IBackgroundService, type BackgroundTask } from '#/background'; import { BackgroundService } from '#/background/backgroundService'; import { IContextMemory } from '#/contextMemory'; -import { IEventBus } from '#/eventBus'; +import { IEventSink } from '../../src/eventSink'; import { IExternalHooksService } from '#/externalHooks'; import { IPromptService } from '#/prompt'; import { ITelemetryService } from '#/telemetry'; @@ -33,7 +33,7 @@ describe('BackgroundService', () => { ix = disposables.add(new TestInstantiationService()); ix.stub(IWireRecord, stubWireRecord()); ix.stub(IContextMemory, stubContextMemory()); - ix.stub(IEventBus, { emit: () => {}, on: () => toDisposable(() => {}) }); + ix.stub(IEventSink, { emit: () => {}, on: () => toDisposable(() => {}) }); ix.stub(ITelemetryService, { track: () => {} }); ix.stub(IPromptService, { steer: () => undefined }); ix.stub(IExternalHooksService, { triggerNotification: () => {} }); diff --git a/packages/agent-core-v2/test/compaction/compaction.test.ts b/packages/agent-core-v2/test/compaction/compaction.test.ts index 9bacfa859..e13290e3b 100644 --- a/packages/agent-core-v2/test/compaction/compaction.test.ts +++ b/packages/agent-core-v2/test/compaction/compaction.test.ts @@ -8,7 +8,7 @@ import { IContextMemory, type ContextMessage } from '#/contextMemory'; import { ContextMemoryService } from '#/contextMemory/contextMemoryService'; import { IContextProjector } from '#/contextProjector'; import { IContextSizeService } from '#/contextSize'; -import { IEventBus } from '#/eventBus'; +import { IEventSink } from '../../src/eventSink'; import { IExternalHooksService } from '#/externalHooks'; import { FullCompactionService } from '#/fullCompaction/fullCompactionService'; import type { CompactionStrategy } from '#/fullCompaction/strategy'; @@ -71,7 +71,7 @@ describe('FullCompactionService', () => { ix.stub(IContextProjector, { project: (messages) => [...messages] }); ix.stub(IContextSizeService, { getStatus: () => ({ contextTokens: 0, contextTokensWithPending: 0 }), - measure: () => {}, + measured: () => {}, }); ix.stub(ILLMRequester, { request: () => summaryStream('compacted summary') }); ix.stub(IProfileService, { @@ -90,7 +90,7 @@ describe('FullCompactionService', () => { ix.stub(IToolStoreService, { data: () => ({}), get: () => undefined, set: () => {} }); ix.stub(ITelemetryService, { track: () => {} }); ix.stub(IUsageService, { record: () => {} }); - ix.stub(IEventBus, { emit: () => {}, on: () => toDisposable(() => {}) }); + ix.stub(IEventSink, { emit: () => {}, on: () => toDisposable(() => {}) }); ix.stub(IExternalHooksService, { triggerPreCompact: () => Promise.resolve(), triggerPostCompact: () => {}, @@ -104,7 +104,7 @@ describe('FullCompactionService', () => { // container cannot bake into a singleton. See di-testing.md "Exceptions". it('replaces history with a compaction summary on overflow', async () => { const ctx = ix.get(IContextMemory); - ctx.spliceHistory(0, 0, [textMessage('user', 'x'.repeat(100)), textMessage('assistant', 'y')]); + ctx.splice(0, 0, [textMessage('user', 'x'.repeat(100)), textMessage('assistant', 'y')]); const svc = ix.createInstance(FullCompactionService as any, { compactionStrategy: forceCompactStrategy(), @@ -112,7 +112,7 @@ describe('FullCompactionService', () => { await svc.handleOverflowError(new AbortController().signal, new Error('context overflow')); - const history = ctx.getHistory(); + const history = ctx.get(); expect(history).toHaveLength(1); expect(history[0]?.origin).toEqual({ kind: 'compaction_summary' }); expect(textOf(history[0]!)).toBe('compacted summary'); diff --git a/packages/agent-core-v2/test/context/context.test.ts b/packages/agent-core-v2/test/context/context.test.ts index 5ff6fb9a5..bf330e49c 100644 --- a/packages/agent-core-v2/test/context/context.test.ts +++ b/packages/agent-core-v2/test/context/context.test.ts @@ -40,11 +40,11 @@ describe('ContextMemoryService', () => { it('returns spliced messages in insertion order', () => { const ctx = ix.get(IContextMemory); - ctx.spliceHistory(0, 0, [textMessage('user', 'hi')]); - ctx.spliceHistory(1, 0, [textMessage('assistant', 'hello')]); + ctx.splice(0, 0, [textMessage('user', 'hi')]); + ctx.splice(1, 0, [textMessage('assistant', 'hello')]); - expect(ctx.getHistory().map((m) => m.role)).toEqual(['user', 'assistant']); - expect(ctx.getHistory().map(textOf)).toEqual(['hi', 'hello']); + expect(ctx.get().map((m) => m.role)).toEqual(['user', 'assistant']); + expect(ctx.get().map(textOf)).toEqual(['hi', 'hello']); }); // NOTE: the legacy `ContextService.tokenUsage()` helper has no equivalent on @@ -54,15 +54,15 @@ describe('ContextMemoryService', () => { it('replaces the whole history with a compaction summary', () => { const ctx = ix.get(IContextMemory); - ctx.spliceHistory(0, 0, [textMessage('user', '1'), textMessage('assistant', '2')]); + ctx.splice(0, 0, [textMessage('user', '1'), textMessage('assistant', '2')]); const summary: ContextMessage = { ...textMessage('assistant', 'summary'), origin: { kind: 'compaction_summary' }, }; - ctx.spliceHistory(0, 2, [summary]); + ctx.splice(0, 2, [summary]); - expect(ctx.getHistory().map(textOf)).toEqual(['summary']); + expect(ctx.get().map(textOf)).toEqual(['summary']); }); // NOTE: the legacy `ContextService.undo()` snapshot/restore behavior has no @@ -72,10 +72,10 @@ describe('ContextMemoryService', () => { it('removes the last message with a deleting splice', () => { const ctx = ix.get(IContextMemory); - ctx.spliceHistory(0, 0, [textMessage('user', '1'), textMessage('user', '2')]); + ctx.splice(0, 0, [textMessage('user', '1'), textMessage('user', '2')]); - ctx.spliceHistory(1, 1, []); + ctx.splice(1, 1, []); - expect(ctx.getHistory().map(textOf)).toEqual(['1']); + expect(ctx.get().map(textOf)).toEqual(['1']); }); }); diff --git a/packages/agent-core-v2/test/contextMemory/stubs.ts b/packages/agent-core-v2/test/contextMemory/stubs.ts index 38621c2ba..a32b304e4 100644 --- a/packages/agent-core-v2/test/contextMemory/stubs.ts +++ b/packages/agent-core-v2/test/contextMemory/stubs.ts @@ -75,8 +75,8 @@ export function stubContextMemory(): StubContextMemory { get messages() { return messages; }, - getHistory: () => [...messages], - spliceHistory: (start, deleteCount, inserted, tokens) => { + get: () => [...messages], + splice: (start, deleteCount, inserted, tokens) => { messages.splice(start, deleteCount, ...inserted); void hooks.onSpliced.run({ start, diff --git a/packages/agent-core-v2/test/cron/cron.test.ts b/packages/agent-core-v2/test/cron/cron.test.ts index 94ca7fcc1..f08de0559 100644 --- a/packages/agent-core-v2/test/cron/cron.test.ts +++ b/packages/agent-core-v2/test/cron/cron.test.ts @@ -7,7 +7,7 @@ import type { ContextMessage } from '#/contextMemory'; import { ICronService } from '#/cron'; import { CronService } from '#/cron/cronService'; import type { ClockSources } from '#/cron/tools/clock'; -import { IEventBus } from '#/eventBus'; +import { IEventSink } from '../../src/eventSink'; import { IPromptService } from '#/prompt'; import { ITelemetryService } from '#/telemetry'; import { IToolRegistry } from '#/toolRegistry'; @@ -75,7 +75,7 @@ describe('CronService', () => { undo: () => 0, clear: () => {}, }); - ix.stub(IEventBus, { emit: () => {}, on: () => ({ dispose: () => {} }) }); + ix.stub(IEventSink, { emit: () => {}, on: () => ({ dispose: () => {} }) }); ix.stub(IWireRecord, stubWireRecord()); ix.stub(ITurnService, turnService); ix.stub(ITelemetryService, { track: () => {} }); diff --git a/packages/agent-core-v2/test/goal/goal.test.ts b/packages/agent-core-v2/test/goal/goal.test.ts index 19189171b..0ce587f2b 100644 --- a/packages/agent-core-v2/test/goal/goal.test.ts +++ b/packages/agent-core-v2/test/goal/goal.test.ts @@ -4,8 +4,8 @@ import { SyncDescriptor } from '#/_base/di/descriptors'; import { DisposableStore } from '#/_base/di/lifecycle'; import { TestInstantiationService } from '#/_base/di/test'; import { IContextMemory } from '#/contextMemory'; -import { IDynamicInjector } from '#/dynamicInjector'; -import { IEventBus } from '#/eventBus'; +import { IContextInjector } from '../../src/contextInjector'; +import { IEventSink } from '../../src/eventSink'; import { IGoalService } from '#/goal'; import { GoalService } from '#/goal/goalService'; import { IReplayBuilderService } from '#/replayBuilder'; @@ -25,11 +25,11 @@ describe('GoalService', () => { disposables = new DisposableStore(); ix = disposables.add(new TestInstantiationService()); ix.stub(IWireRecord, stubWireRecord()); - ix.stub(IEventBus, { emit: () => {}, on: () => ({ dispose: () => {} }) }); + ix.stub(IEventSink, { emit: () => {}, on: () => ({ dispose: () => {} }) }); ix.stub(IContextMemory, stubContextMemory()); ix.stub(IReplayBuilderService, stubReplayBuilder()); ix.stub(ITelemetryService, { track: () => {} }); - ix.stub(IDynamicInjector, { register: () => ({ dispose: () => {} }) }); + ix.stub(IContextInjector, { register: () => ({ dispose: () => {} }) }); ix.set(IGoalService, new SyncDescriptor(GoalService, [{}])); }); afterEach(() => disposables.dispose()); diff --git a/packages/agent-core-v2/test/injection/injection.test.ts b/packages/agent-core-v2/test/injection/injection.test.ts index cbd4fffd3..f17525f52 100644 --- a/packages/agent-core-v2/test/injection/injection.test.ts +++ b/packages/agent-core-v2/test/injection/injection.test.ts @@ -4,8 +4,8 @@ import { SyncDescriptor } from '#/_base/di/descriptors'; import { DisposableStore } from '#/_base/di/lifecycle'; import { TestInstantiationService } from '#/_base/di/test'; import { IContextMemory, type ContextMessage } from '#/contextMemory'; -import { IDynamicInjector } from '#/dynamicInjector'; -import { DynamicInjectorService } from '#/dynamicInjector/dynamicInjectorService'; +import { IContextInjector } from '../../src/contextInjector'; +import { ContextInjectorService } from '../../src/contextInjector/contextInjectorService'; import { ITurnService, type TurnStepContext } from '#/turn'; import { stubContextMemory, type StubContextMemory } from '../contextMemory/stubs'; import { stubTurnWithHooks } from '../turn/stubs'; @@ -32,7 +32,7 @@ describe('DynamicInjectorService', () => { ix = disposables.add(new TestInstantiationService()); ix.stub(IContextMemory, stubContextMemory()); ix.stub(ITurnService, stubTurnWithHooks()); - ix.set(IDynamicInjector, new SyncDescriptor(DynamicInjectorService)); + ix.set(IContextInjector, new SyncDescriptor(ContextInjectorService)); ctx = ix.get(IContextMemory) as StubContextMemory; }); afterEach(() => disposables.dispose()); @@ -47,7 +47,7 @@ describe('DynamicInjectorService', () => { } it('splices a registered provider content into context on a step', async () => { - const injector = ix.get(IDynamicInjector); + const injector = ix.get(IContextInjector); injector.register('reminder', () => 'hello injection'); await runBeforeStep(); @@ -58,7 +58,7 @@ describe('DynamicInjectorService', () => { }); it('stops injecting after the registration is disposed', async () => { - const injector = ix.get(IDynamicInjector); + const injector = ix.get(IContextInjector); const registration = injector.register('reminder', () => 'hello injection'); registration.dispose(); diff --git a/packages/agent-core-v2/test/mcp/mcp.test.ts b/packages/agent-core-v2/test/mcp/mcp.test.ts index 06ce62c61..b7868ee97 100644 --- a/packages/agent-core-v2/test/mcp/mcp.test.ts +++ b/packages/agent-core-v2/test/mcp/mcp.test.ts @@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { SyncDescriptor } from '#/_base/di/descriptors'; import { DisposableStore, toDisposable } from '#/_base/di/lifecycle'; import { TestInstantiationService } from '#/_base/di/test'; -import { IEventBus } from '#/eventBus'; +import { IEventSink } from '../../src/eventSink'; import type { McpConnectionManager, McpServerEntry } from '#/mcp/connection-manager'; import { IMcpService, McpService } from '#/mcp'; import { IToolRegistry } from '#/toolRegistry'; @@ -75,7 +75,7 @@ describe('McpService', () => { beforeEach(() => { disposables = new DisposableStore(); ix = disposables.add(new TestInstantiationService()); - ix.stub(IEventBus, { emit: () => {}, on: () => toDisposable(() => {}) }); + ix.stub(IEventSink, { emit: () => {}, on: () => toDisposable(() => {}) }); ix.stub(IToolRegistry, { register: () => toDisposable(() => {}), list: () => [], diff --git a/packages/agent-core-v2/test/message/message.test.ts b/packages/agent-core-v2/test/message/message.test.ts index ca1223dde..3bad38b6f 100644 --- a/packages/agent-core-v2/test/message/message.test.ts +++ b/packages/agent-core-v2/test/message/message.test.ts @@ -44,21 +44,21 @@ describe('message history (IContextMemory)', () => { it('round-trips user/assistant messages with their text content', () => { const ctx = ix.get(IContextMemory); - ctx.spliceHistory(0, 0, [textMessage('user', 'a')]); - ctx.spliceHistory(1, 0, [textMessage('assistant', 'b')]); + ctx.splice(0, 0, [textMessage('user', 'a')]); + ctx.splice(1, 0, [textMessage('assistant', 'b')]); - const history = ctx.getHistory(); + const history = ctx.get(); expect(history.map((m) => m.role)).toEqual(['user', 'assistant']); expect(history.map(textOf)).toEqual(['a', 'b']); }); it('returns a defensive copy from getHistory', () => { const ctx = ix.get(IContextMemory); - ctx.spliceHistory(0, 0, [textMessage('user', 'keep')]); + ctx.splice(0, 0, [textMessage('user', 'keep')]); - const view = ctx.getHistory(); + const view = ctx.get(); (view as ContextMessage[]).splice(0, view.length); - expect(ctx.getHistory().map(textOf)).toEqual(['keep']); + expect(ctx.get().map(textOf)).toEqual(['keep']); }); }); diff --git a/packages/agent-core-v2/test/plan/plan.test.ts b/packages/agent-core-v2/test/plan/plan.test.ts index 79d652c77..12ca94947 100644 --- a/packages/agent-core-v2/test/plan/plan.test.ts +++ b/packages/agent-core-v2/test/plan/plan.test.ts @@ -4,8 +4,8 @@ import { SyncDescriptor } from '#/_base/di/descriptors'; import { DisposableStore } from '#/_base/di/lifecycle'; import { TestInstantiationService } from '#/_base/di/test'; import { IContextMemory } from '#/contextMemory'; -import { IDynamicInjector } from '#/dynamicInjector'; -import { IEventBus } from '#/eventBus'; +import { IContextInjector } from '../../src/contextInjector'; +import { IEventSink } from '../../src/eventSink'; import { IKaosService } from '#/kaos'; import { IPlanService } from '#/plan'; import { PlanService } from '#/plan/planService'; @@ -35,10 +35,10 @@ describe('PlanService', () => { ix.set(IReplayBuilderService, stubReplayBuilder()); // No-op collaborators — only the members exercised by PlanService. - ix.stub(IEventBus, { emit() {} }); + ix.stub(IEventSink, { emit() {} }); ix.stub(ITelemetryService, { track() {} }); ix.stub(IToolRegistry, { register: () => ({ dispose() {} }) }); - ix.stub(IDynamicInjector, { register: () => ({ dispose() {} }) }); + ix.stub(IContextInjector, { register: () => ({ dispose() {} }) }); // kaos undefined → filesystem access short-circuits via optional chaining. ix.stub(IKaosService, { kaos: undefined }); // PlanService.currentCwd() reads profile.data().cwd. diff --git a/packages/agent-core-v2/test/skill/skill.test.ts b/packages/agent-core-v2/test/skill/skill.test.ts index 8a2c1c4e9..c1f472731 100644 --- a/packages/agent-core-v2/test/skill/skill.test.ts +++ b/packages/agent-core-v2/test/skill/skill.test.ts @@ -4,7 +4,7 @@ import { SyncDescriptor } from '#/_base/di/descriptors'; import { DisposableStore } from '#/_base/di/lifecycle'; import { TestInstantiationService } from '#/_base/di/test'; import type { ContextMessage } from '#/contextMemory'; -import { IEventBus } from '#/eventBus'; +import { IEventSink } from '../../src/eventSink'; import { IPromptService } from '#/prompt'; import { IAgentSkillService } from '#/skill'; import { AgentSkillService } from '#/skill/skillService'; @@ -71,7 +71,7 @@ describe('AgentSkillService', () => { undo: () => 0, clear: () => {}, }); - ix.stub(IEventBus, { emit: () => {}, on: () => ({ dispose: () => {} }) }); + ix.stub(IEventSink, { emit: () => {}, on: () => ({ dispose: () => {} }) }); ix.stub(IWireRecord, stubWireRecord()); ix.stub(ITelemetryService, { track: () => {} }); ix.set( diff --git a/packages/agent-core-v2/test/swarm/swarm.test.ts b/packages/agent-core-v2/test/swarm/swarm.test.ts index 79dcf1c35..fb8a7eab7 100644 --- a/packages/agent-core-v2/test/swarm/swarm.test.ts +++ b/packages/agent-core-v2/test/swarm/swarm.test.ts @@ -4,7 +4,7 @@ import { SyncDescriptor } from '#/_base/di/descriptors'; import { DisposableStore, toDisposable } from '#/_base/di/lifecycle'; import { TestInstantiationService } from '#/_base/di/test'; import { IContextMemory } from '#/contextMemory'; -import { IEventBus } from '#/eventBus'; +import { IEventSink } from '../../src/eventSink'; import { ISubagentHost } from '#/subagentHost'; import { ISwarmService } from '#/swarm'; import { SwarmService } from '#/swarm/swarmService'; @@ -24,7 +24,7 @@ describe('SwarmService', () => { ix = disposables.add(new TestInstantiationService()); ix.stub(IContextMemory, stubContextMemory()); ix.stub(IWireRecord, stubWireRecord()); - ix.stub(IEventBus, { emit: () => {}, on: () => toDisposable(() => {}) }); + ix.stub(IEventSink, { emit: () => {}, on: () => toDisposable(() => {}) }); ix.stub(ITurnService, stubTurnWithHooks()); ix.stub(IToolRegistry, {}); ix.stub(ISubagentHost, {}); diff --git a/packages/agent-core-v2/test/turn/turn.test.ts b/packages/agent-core-v2/test/turn/turn.test.ts index e54df1df8..7c3605aad 100644 --- a/packages/agent-core-v2/test/turn/turn.test.ts +++ b/packages/agent-core-v2/test/turn/turn.test.ts @@ -4,7 +4,7 @@ import { SyncDescriptor } from '#/_base/di/descriptors'; import { DisposableStore } from '#/_base/di/lifecycle'; import { TestInstantiationService } from '#/_base/di/test'; import { IContextMemory } from '#/contextMemory'; -import { IEventBus } from '#/eventBus'; +import { IEventSink } from '../../src/eventSink'; import { IExternalHooksService } from '#/externalHooks'; import { ILoopService } from '#/loop'; import { IPlanService } from '#/plan'; @@ -31,7 +31,7 @@ describe('TurnService', () => { // No-op collaborators — only the members exercised by TurnService. ix.stub(IUsageService, { beginTurn() {}, endTurn() {} }); - ix.stub(IEventBus, { emit() {}, on: () => ({ dispose() {} }) }); + ix.stub(IEventSink, { emit() {}, on: () => ({ dispose() {} }) }); ix.stub(IExternalHooksService, { triggerInterrupt() {} }); ix.stub(ITelemetryService, { track() {} }); // TurnService.telemetryMode() resolves IPlanService via IInstantiationService. diff --git a/packages/agent-core-v2/test/usage/usage.test.ts b/packages/agent-core-v2/test/usage/usage.test.ts index 890d7ab33..150071eb5 100644 --- a/packages/agent-core-v2/test/usage/usage.test.ts +++ b/packages/agent-core-v2/test/usage/usage.test.ts @@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { SyncDescriptor } from '#/_base/di/descriptors'; import { DisposableStore, toDisposable } from '#/_base/di/lifecycle'; import { TestInstantiationService } from '#/_base/di/test'; -import { IEventBus } from '#/eventBus'; +import { IEventSink } from '../../src/eventSink'; import { IUsageService } from '#/usage'; import { UsageService } from '#/usage/usageService'; import { IWireRecord } from '#/wireRecord'; @@ -18,7 +18,7 @@ describe('UsageService', () => { disposables = new DisposableStore(); ix = disposables.add(new TestInstantiationService()); ix.stub(IWireRecord, stubWireRecord()); - ix.stub(IEventBus, { emit: () => {}, on: () => toDisposable(() => {}) }); + ix.stub(IEventSink, { emit: () => {}, on: () => toDisposable(() => {}) }); ix.set(IUsageService, new SyncDescriptor(UsageService)); }); afterEach(() => disposables.dispose());