mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
fix: refine interfaces
This commit is contained in:
parent
98e88a91ac
commit
85755b902e
89 changed files with 958 additions and 434 deletions
|
|
@ -85,6 +85,5 @@ export interface IBackgroundService {
|
|||
): Promise<ForegroundTaskReleaseReason | undefined>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const IBackgroundService =
|
||||
createDecorator<IBackgroundService>('agentBackgroundService');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
|
|
@ -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',
|
||||
);
|
||||
|
|
@ -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';
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
|
|
@ -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');
|
||||
|
|
@ -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');
|
||||
|
|
@ -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';
|
||||
|
|
@ -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');
|
||||
10
packages/agent-core-v2/src/eventSink/eventSink.ts
Normal file
10
packages/agent-core-v2/src/eventSink/eventSink.ts
Normal 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');
|
||||
|
|
@ -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',
|
||||
);
|
||||
|
|
@ -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';
|
||||
|
|
@ -98,6 +98,5 @@ export interface IExternalHooksService {
|
|||
}): void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const IExternalHooksService =
|
||||
createDecorator<IExternalHooksService>('agentExternalHooksService');
|
||||
|
|
|
|||
|
|
@ -30,5 +30,4 @@ declare module '#/wireRecord' {
|
|||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const IFullCompaction = createDecorator<IFullCompaction>('agentFullCompactionService');
|
||||
|
|
|
|||
|
|
@ -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)],
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
});
|
||||
|
|
|
|||
|
|
@ -42,5 +42,4 @@ declare module '#/wireRecord' {
|
|||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const IGoalService = createDecorator<IGoalService>('agentGoalService');
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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' }));
|
||||
|
|
|
|||
119
packages/agent-core-v2/src/goalMode/injection/goalInjection.ts
Normal file
119
packages/agent-core-v2/src/goalMode/injection/goalInjection.ts
Normal 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('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>');
|
||||
}
|
||||
|
||||
function formatElapsed(ms: number): string {
|
||||
const totalSeconds = Math.round(ms / 1000);
|
||||
if (totalSeconds < 60) return `${totalSeconds}s`;
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
return `${minutes}m${seconds.toString().padStart(2, '0')}s`;
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,5 +30,4 @@ export interface McpServiceOptions {
|
|||
readonly manager?: McpConnectionManager;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const IMcpService = createDecorator<IMcpService>('agentMcpService');
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
293
packages/agent-core-v2/src/mcpRuntime/mcpRuntimeService.ts
Normal file
293
packages/agent-core-v2/src/mcpRuntime/mcpRuntimeService.ts
Normal 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',
|
||||
);
|
||||
|
|
@ -32,6 +32,5 @@ declare module '#/wireRecord' {
|
|||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const IMicroCompactionService =
|
||||
createDecorator<IMicroCompactionService>('agentMicroCompactionService');
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,5 @@ export interface IPermissionService {
|
|||
): Promise<AuthorizeToolExecutionResult | undefined>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const IPermissionService =
|
||||
createDecorator<IPermissionService>('agentPermissionService');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,5 @@ export interface IPermissionModeService {
|
|||
}>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const IPermissionModeService =
|
||||
createDecorator<IPermissionModeService>('agentPermissionModeService');
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -19,6 +19,5 @@ export interface IPermissionPolicyService {
|
|||
): Promise<PermissionPolicyEvaluation | undefined>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const IPermissionPolicyService =
|
||||
createDecorator<IPermissionPolicyService>('agentPermissionPolicyService');
|
||||
|
|
|
|||
|
|
@ -60,6 +60,5 @@ export interface IPermissionRulesService {
|
|||
}>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const IPermissionRulesService =
|
||||
createDecorator<IPermissionRulesService>('agentPermissionRulesService');
|
||||
|
|
|
|||
|
|
@ -34,6 +34,5 @@ declare module '#/wireRecord' {
|
|||
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const IPlanService =
|
||||
createDecorator<IPlanService>('agentPlanService');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -11,5 +11,4 @@ export interface IPromptService {
|
|||
clear(): void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const IPromptService = createDecorator<IPromptService>('promptService.agent');
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ export interface IReplayBuilderService {
|
|||
buildResult(): readonly AgentReplayRecord[];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const IReplayBuilderService = createDecorator<IReplayBuilderService>(
|
||||
'agentReplayBuilderService',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -53,5 +53,4 @@ export interface ISubagentHost {
|
|||
}
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const ISubagentHost = createDecorator<ISubagentHost>('agentSubagentHostService');
|
||||
|
|
|
|||
|
|
@ -19,5 +19,4 @@ declare module '#/wireRecord' {
|
|||
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const ISwarmService = createDecorator<ISwarmService>('agentSwarmService');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ export const nullTelemetryAppender: ITelemetryAppender = {
|
|||
shutdown: () => {},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const ITelemetryService = createDecorator<ITelemetryService>(
|
||||
'agentTelemetryService',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,5 +4,4 @@ export interface ITodoListService {
|
|||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const ITodoListService = createDecorator<ITodoListService>('agentTodoListService');
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,5 +17,4 @@ export interface IToolExecutor {
|
|||
): Promise<ToolResult>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const IToolExecutor = createDecorator<IToolExecutor>('toolExecutorService');
|
||||
|
|
|
|||
|
|
@ -66,5 +66,4 @@ export interface IToolRegistry {
|
|||
}>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const IToolRegistry = createDecorator<IToolRegistry>('agentToolRegistryService');
|
||||
|
|
|
|||
|
|
@ -23,5 +23,4 @@ export interface IToolStoreService extends ToolStore {
|
|||
}>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const IToolStoreService = createDecorator<IToolStoreService>('agentToolStoreService');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
324
packages/agent-core-v2/src/turnRunner/turnRunnerService.ts
Normal file
324
packages/agent-core-v2/src/turnRunner/turnRunnerService.ts
Normal 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));
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -90,5 +90,4 @@ export interface IWireRecord {
|
|||
}>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const IWireRecord = createDecorator<IWireRecord>('agentWireRecordService');
|
||||
|
|
|
|||
|
|
@ -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: () => {} });
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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: () => {} });
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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: () => [],
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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, {});
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue