fix: refine interfaces

This commit is contained in:
_Kerman 2026-06-26 21:06:12 +08:00
parent 98e88a91ac
commit 85755b902e
89 changed files with 958 additions and 434 deletions

View file

@ -85,6 +85,5 @@ export interface IBackgroundService {
): Promise<ForegroundTaskReleaseReason | undefined>;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IBackgroundService =
createDecorator<IBackgroundService>('agentBackgroundService');

View file

@ -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<void> {
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;
});
}

View file

@ -17,7 +17,6 @@ export interface IBlobStoreService {
isBlobRef(url: string): boolean;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IBlobStoreService = createDecorator<IBlobStoreService>(
'agentBlobStoreService',
);

View file

@ -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<string | undefined>;
export interface IContextInjector {
register(
variant: string,
provider: ContextInjectionProvider,
options?: ContextInjectionOptions,
): IDisposable;
}
export const IContextInjector = createDecorator<IContextInjector>(
'contextInjectorService',
);

View file

@ -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<DynamicInjectionEntry>();
private readonly selfInsertedMessages = new WeakMap<ContextMessage, DynamicInjectionEntry>();
export class ContextInjectorService extends Disposable implements IContextInjector {
private readonly entries = new Set<ContextInjectionEntry>();
private readonly selfInsertedMessages = new WeakMap<ContextMessage, ContextInjectionEntry>();
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<void> {
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<DynamicInjectionEntry, number>();
const selfInserted = new Map<ContextInjectionEntry, number>();
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',
);

View file

@ -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';

View file

@ -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<IContextMemory>('agentContextMemoryService');

View file

@ -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,

View file

@ -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<IContextProjector>(
'agentContextProjectorService',
);

View file

@ -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<MicroCompactingProjectorOptions>,
): 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);
}

View file

@ -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<IContextSizeService>('agentContextSizeService');

View file

@ -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,

View file

@ -68,5 +68,4 @@ export interface ICronService extends CronToolManager {
flushPersist(): Promise<void>;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const ICronService = createDecorator<ICronService>('agentCronService');

View file

@ -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,

View file

@ -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<string | undefined>;
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<IDynamicInjector>(
'agentDynamicInjectorService',
);

View file

@ -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<IEventService> =
createDecorator<IEventService>('eventService');

View file

@ -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<Listener>();
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');

View file

@ -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';

View file

@ -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<IEventBus>('agentEventBusService');

View file

@ -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<IEventSink>('agentEventSink');

View file

@ -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<AgentEvent>());
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',
);

View file

@ -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';

View file

@ -98,6 +98,5 @@ export interface IExternalHooksService {
}): void;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IExternalHooksService =
createDecorator<IExternalHooksService>('agentExternalHooksService');

View file

@ -30,5 +30,4 @@ declare module '#/wireRecord' {
}
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IFullCompaction = createDecorator<IFullCompaction>('agentFullCompactionService');

View file

@ -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<CompactionResult | undefined> {
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)],

View file

@ -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<void> {
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<unknown> {
@ -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(() => {
});

View file

@ -42,5 +42,4 @@ declare module '#/wireRecord' {
}
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IGoalService = createDecorator<IGoalService>('agentGoalService');

View file

@ -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]);
}
}

View file

@ -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' }));

View file

@ -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('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;');
}
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`;
}

View file

@ -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';

View file

@ -9,7 +9,6 @@ export interface IKaosService {
chdir(cwd: string): Promise<void>;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IKaosService = createDecorator<IKaosService>('agentKaosService');
export type KaosFactoryOptions =
@ -21,7 +20,6 @@ export interface IKaosFactory {
create(options: KaosFactoryOptions): Promise<Kaos>;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IKaosFactory = createDecorator<IKaosFactory>('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<ISessionKaosService>('sessionKaosService');

View file

@ -18,6 +18,5 @@ export interface ILLMRequestLogService {
logRequest(input: LLMRequestLogInput): void;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const ILLMRequestLogService =
createDecorator<ILLMRequestLogService>('agentLLMRequestLogService');

View file

@ -28,5 +28,4 @@ export interface ILLMRequester {
request(overrides?: LLMRequestOverrides, signal?: AbortSignal): AsyncIterable<LLMEvent>;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const ILLMRequester = createDecorator<ILLMRequester>('agentLLMRequesterService');

View file

@ -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,
};

View file

@ -19,5 +19,4 @@ export interface ILoopService {
runTurn(turn: Turn, hooks?: LoopRunHooks): Promise<TurnResult>;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const ILoopService = createDecorator<ILoopService>('agentLoopService');

View file

@ -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);
}
}

View file

@ -30,5 +30,4 @@ export interface McpServiceOptions {
readonly manager?: McpConnectionManager;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IMcpService = createDecorator<IMcpService>('agentMcpService');

View file

@ -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();

View file

@ -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<string, McpToolRegistration>();
private readonly mcpToolsByServer = new Map<string, string[]>();
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<void> {
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<void> {
signal?.throwIfAborted();
await this.options.manager?.reconnect(name);
signal?.throwIfAborted();
}
onStatusChange(listener: Parameters<IMcpRuntimeService['onStatusChange']>[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<string>,
): {
readonly registered: readonly string[];
readonly collisions: readonly McpToolCollision[];
} {
this.unregisterMcpServer(serverName);
const qualifiedNames: string[] = [];
const collisions: McpToolCollision[] = [];
const seenInThisCall = new Map<string, string>();
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<string, unknown>,
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',
);

View file

@ -32,6 +32,5 @@ declare module '#/wireRecord' {
}
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IMicroCompactionService =
createDecorator<IMicroCompactionService>('agentMicroCompactionService');

View file

@ -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)) {

View file

@ -46,6 +46,5 @@ export interface IPermissionService {
): Promise<AuthorizeToolExecutionResult | undefined>;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IPermissionService =
createDecorator<IPermissionService>('agentPermissionService');

View file

@ -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<IPermissionModeService, 'mode'>,
): IDisposable {
let lastMode: PermissionMode | undefined;

View file

@ -18,6 +18,5 @@ export interface IPermissionModeService {
}>;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IPermissionModeService =
createDecorator<IPermissionModeService>('agentPermissionModeService');

View file

@ -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(

View file

@ -19,6 +19,5 @@ export interface IPermissionPolicyService {
): Promise<PermissionPolicyEvaluation | undefined>;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IPermissionPolicyService =
createDecorator<IPermissionPolicyService>('agentPermissionPolicyService');

View file

@ -60,6 +60,5 @@ export interface IPermissionRulesService {
}>;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IPermissionRulesService =
createDecorator<IPermissionRulesService>('agentPermissionRulesService');

View file

@ -34,6 +34,5 @@ declare module '#/wireRecord' {
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IPlanService =
createDecorator<IPlanService>('agentPlanService');

View file

@ -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;

View file

@ -86,5 +86,4 @@ export interface IProfileService {
removeActiveTool(name: string): void;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IProfileService = createDecorator<IProfileService>('profileService.agent');

View file

@ -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,

View file

@ -11,5 +11,4 @@ export interface IPromptService {
clear(): void;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IPromptService = createDecorator<IPromptService>('promptService.agent');

View file

@ -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 {

View file

@ -28,7 +28,6 @@ export interface IReplayBuilderService {
buildResult(): readonly AgentReplayRecord[];
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IReplayBuilderService = createDecorator<IReplayBuilderService>(
'agentReplayBuilderService',
);

View file

@ -9,10 +9,8 @@ export interface IAgentRPCService extends PromisableMethods<AgentAPI> {}
export interface ISessionRPCService extends PromisableMethods<SessionAPI> {}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IAgentRPCService =
createDecorator<IAgentRPCService>('agentRPCService');
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const ISessionRPCService =
createDecorator<ISessionRPCService>('agentSessionRPCService');

View file

@ -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,
};
}

View file

@ -23,6 +23,5 @@ export interface IAgentSkillService {
activateFromModel(input: ModelSkillActivationInput): ExecutableToolResult;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IAgentSkillService =
createDecorator<IAgentSkillService>('agentSkillService');

View file

@ -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,
) {

View file

@ -53,5 +53,4 @@ export interface ISubagentHost {
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const ISubagentHost = createDecorator<ISubagentHost>('agentSubagentHostService');

View file

@ -19,5 +19,4 @@ declare module '#/wireRecord' {
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const ISwarmService = createDecorator<ISwarmService>('agentSwarmService');

View file

@ -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;
}
}

View file

@ -56,7 +56,6 @@ export const nullTelemetryAppender: ITelemetryAppender = {
shutdown: () => {},
};
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const ITelemetryService = createDecorator<ITelemetryService>(
'agentTelemetryService',
);

View file

@ -4,5 +4,4 @@ export interface ITodoListService {
readonly _serviceBrand: undefined;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const ITodoListService = createDecorator<ITodoListService>('agentTodoListService');

View file

@ -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(),
});
}

View file

@ -17,5 +17,4 @@ export interface IToolExecutor {
): Promise<ToolResult>;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IToolExecutor = createDecorator<IToolExecutor>('toolExecutorService');

View file

@ -66,5 +66,4 @@ export interface IToolRegistry {
}>;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IToolRegistry = createDecorator<IToolRegistry>('agentToolRegistryService');

View file

@ -23,5 +23,4 @@ export interface IToolStoreService extends ToolStore {
}>;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IToolStoreService = createDecorator<IToolStoreService>('agentToolStoreService');

View file

@ -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<ITurnService>('turnService');

View file

@ -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<TurnResult> {
const startedAt = Date.now();
const telemetryMode = this.telemetryMode();
@ -201,7 +194,7 @@ export class TurnService implements ITurnService {
origin: PromptOrigin,
): Promise<TurnResult | undefined> {
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 {

View file

@ -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<Turn, ControlledPromise<void>>();
private readonly readySettled = new WeakSet<Turn>();
private readonly currentStepByTurn = new Map<number, number>();
private readonly interruptedTelemetryTurnIds = new Set<number>();
private readonly telemetryModeByTurn = new Map<number, 'agent' | 'plan'>();
readonly hooks = {
onLaunched: new OrderedHookSlot<{ turn: Turn }>(),
onEnded: new OrderedHookSlot<TurnEndedContext>(),
beforeStep: new OrderedHookSlot<TurnStepContext>(),
afterStep: new OrderedHookSlot<TurnStepContext>(),
};
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<void>();
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<TurnResult> {
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<TurnResult | undefined> {
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<T> {
readonly promise: Promise<T>;
resolve(value: T | PromiseLike<T>): void;
reject(reason?: unknown): void;
}
type MutableTurn = {
-readonly [K in keyof Turn]: Turn[K];
};
function createControlledPromise<T>(): ControlledPromise<T> {
let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}
registerSingleton(ITurnRunner, new SyncDescriptor(TurnRunnerService, [], true));

View file

@ -18,5 +18,4 @@ export interface IUsageService {
status(): UsageStatus | undefined;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IUsageService = createDecorator<IUsageService>('usageService.agent');

View file

@ -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');

View file

@ -25,5 +25,4 @@ export interface IUserToolService {
unregister(name: string): void;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IUserToolService = createDecorator<IUserToolService>('agentUserToolService');

View file

@ -90,5 +90,4 @@ export interface IWireRecord {
}>;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IWireRecord = createDecorator<IWireRecord>('agentWireRecordService');

View file

@ -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: () => {} });

View file

@ -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');

View file

@ -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']);
});
});

View file

@ -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,

View file

@ -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: () => {} });

View file

@ -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());

View file

@ -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();

View file

@ -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: () => [],

View file

@ -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']);
});
});

View file

@ -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.

View file

@ -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(

View file

@ -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, {});

View file

@ -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.

View file

@ -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());