import { fromCallback, setup } from 'xstate'; import { createDecorator, IInstantiationService } from '#/_base/di/instantiation'; import { toDisposable, type IDisposable } from '#/_base/di/lifecycle'; import { ILogService } from '#/_base/log/log'; import { AgentActorService, type AgentActorContext, type AgentActorRestoreEvent, } from '#/agent/actorService/agentActorService'; import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory'; import { isCompactionSummaryMessage } from '#/agent/contextMemory/compactionHandoff'; import { ContextSpliced } from '#/agent/contextMemory/contextEvents'; import type { ContextMessage } from '#/agent/contextMemory/types'; import { IAgentLoopService, type BeforeStepContext } from '#/agent/loop/loop'; import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext'; import { IEventBus } from '#/app/event/eventBus'; import { IEventDispatcher } from '#/state/eventDispatcher'; import { wrapSystemReminder } from './systemReminder'; import type { ContextInjectionContent, ContextInjectionContext, ContextInjectionMessage, ContextInjectionProvider, ContextInjectionResult, ReminderNotification, ReminderRegistration, } from './types'; interface ReminderEntry { readonly provider: ContextInjectionProvider; readonly variant: string; } const REMINDER_VARIANT_PRIORITY = new Map([['date_change', -1]]); interface ReminderActorContext { readonly entries: Set; readonly runtime: AgentActorContext; } interface ReminderRegisterEvent { readonly type: 'reminder.register'; readonly entry: ReminderEntry; } interface ReminderUnregisterEvent { readonly type: 'reminder.unregister'; readonly entry: ReminderEntry; } type ReminderActorEvent = AgentActorRestoreEvent | ReminderRegisterEvent | ReminderUnregisterEvent; function actorContext(runtime: AgentActorContext): ReminderActorContext { return runtime.getLogicState(); } function appendReminder( runtime: AgentActorContext, content: string, notification: ReminderNotification, ): void { runtime.get(IAgentContextMemoryService).append({ role: 'user', content: [{ type: 'text', text: wrapSystemReminder(content) }], toolCalls: [], origin: { kind: 'injection', variant: notification.variant, ownerPromptId: notification.ownerPromptId, }, }); } function providerContext( runtime: AgentActorContext, entry: ReminderEntry, isNewTurn: boolean, ): ContextInjectionContext { const history = runtime.get(IAgentContextMemoryService).get(); const injectedPositions = findInjections(history, entry.variant); const lastInjectedAt = injectedPositions.at(-1) ?? null; const lastInjection = lastInjectedAt === null ? undefined : history[lastInjectedAt]; return { injectedPositions, lastInjectedAt, lastInjection, lastDisclosure: lastInjection?.origin?.kind === 'injection' ? lastInjection.origin.disclosure : undefined, isNewTurn, }; } async function injectEntry( runtime: AgentActorContext, entry: ReminderEntry, isNewTurn: boolean, ): Promise { let content: Awaited>; try { content = await entry.provider(providerContext(runtime, entry, isNewTurn)); } catch (error) { runtime.get(ILogService).error('context provider failed; skipping it', { name: entry.variant, error, }); return; } if (!actorContext(runtime).entries.has(entry)) return; appendResult(runtime, entry, content); } function appendResult( runtime: AgentActorContext, entry: ReminderEntry, content: ContextInjectionContent | ContextInjectionResult | undefined, ): void { if (content === undefined) return; const result: ContextInjectionResult = isInjectionResult(content) ? content : { content }; const origin = { kind: 'injection' as const, variant: entry.variant, disclosure: result.disclosure, }; const resolved = result.content; if (typeof resolved === 'string') { if (resolved.trim().length === 0) return; runtime.get(IAgentContextMemoryService).append({ role: 'user', content: [{ type: 'text', text: wrapSystemReminder(resolved) }], toolCalls: [], origin, }); return; } if (isRawInjectionMessage(resolved)) { const message = resolved.message; if (message.content.length === 0 && (message.tools === undefined || message.tools.length === 0)) { return; } runtime.get(IAgentContextMemoryService).append({ role: message.role, content: [...message.content], toolCalls: [], tools: message.tools, origin, }); return; } if (resolved.length === 0) return; runtime.get(IAgentContextMemoryService).append({ role: 'user', content: [...resolved], toolCalls: [], origin, }); } async function inject(runtime: AgentActorContext, isNewTurn: boolean): Promise { const entries = [...actorContext(runtime).entries].sort( (left, right) => (REMINDER_VARIANT_PRIORITY.get(left.variant) ?? 0) - (REMINDER_VARIANT_PRIORITY.get(right.variant) ?? 0), ); for (const entry of entries) await injectEntry(runtime, entry, isNewTurn); } const reminderEffects = fromCallback(({ input }: { input: { readonly runtime: AgentActorContext } }) => { let compactionRearmPending = false; const loop = input.runtime.get(IAgentLoopService); const takeCompactionRearm = (): boolean => { const pending = compactionRearmPending; compactionRearmPending = false; return pending; }; const reconcileAroundStep = async ( context: BeforeStepContext, next: (context?: BeforeStepContext) => Promise, ): Promise => { const rearmed = takeCompactionRearm(); await inject(input.runtime, context.firstStepOfTurn || rearmed); await next(); if (takeCompactionRearm()) await inject(input.runtime, true); }; let hook: IDisposable; try { hook = loop.hooks.onWillBeginStep.register('context-injector', reconcileAroundStep, { before: 'full-compaction', }); } catch { hook = loop.hooks.onWillBeginStep.register('context-injector', reconcileAroundStep); } const splice = input.runtime.get(IEventBus).subscribe(ContextSpliced, (event) => { if (isCompactionSplice(event)) compactionRearmPending = true; }); return () => { splice.dispose(); hook.dispose(); actorContext(input.runtime).entries.clear(); }; }); const reminderActorLogic = setup({ types: {} as { context: ReminderActorContext; input: AgentActorContext; events: ReminderActorEvent; }, actors: { reminderEffects }, }).createMachine({ context: ({ input }) => ({ entries: new Set(), runtime: input }), initial: 'beforeRestore', states: { beforeRestore: { on: { 'runtime.restore': 'active' }, }, active: { invoke: { src: 'reminderEffects', input: ({ context }) => ({ runtime: context.runtime }), }, }, }, on: { 'reminder.register': { actions: ({ context, event }) => { context.entries.add(event.entry); }, }, 'reminder.unregister': { actions: ({ context, event }) => { context.entries.delete(event.entry); }, }, }, }); export interface IAgentReminderService { readonly _serviceBrand: undefined; register(variant: string, provider: ContextInjectionProvider): ReminderRegistration; notify(content: string, notification: ReminderNotification): void; reconcileWhenIdle(variant: string): Promise; } export const IAgentReminderService = createDecorator('agentReminderService'); export class AgentReminderService extends AgentActorService implements IAgentReminderService { declare readonly _serviceBrand: undefined; private readonly actor: AgentActorContext; private disposed = false; constructor( @IEventDispatcher dispatcher: IEventDispatcher, @IAgentScopeContext scopeContext: IAgentScopeContext, @IInstantiationService instantiation: IInstantiationService, ) { super(dispatcher, scopeContext, instantiation); this.actor = this.attachActor(reminderActorLogic, { id: 'reminder' }); this._register(toDisposable(() => { this.disposed = true; })); } register(variant: string, provider: ContextInjectionProvider): ReminderRegistration { const entry: ReminderEntry = { provider: provider as ContextInjectionProvider, variant, }; this.actor.send({ type: 'reminder.register', entry }); return toDisposable(() => { if (this.disposed) return; try { this.actor.send({ type: 'reminder.unregister', entry }); } catch {} }); } notify(content: string, notification: ReminderNotification): void { appendReminder(this.actor, content, notification); } async reconcileWhenIdle(variant: string): Promise { const loop = this.actor.get(IAgentLoopService); const quiescence = loop.tryAcquireQuiescence(); if (quiescence === undefined) return; try { for (const entry of actorContext(this.actor).entries) { if (entry.variant === variant) await injectEntry(this.actor, entry, false); } } finally { quiescence.dispose(); } } } function isCompactionSplice(splice: { readonly deleteCount: number; readonly messages: readonly ContextMessage[]; }): boolean { return splice.deleteCount > 0 && splice.messages.some(isCompactionSummaryMessage); } function isRawInjectionMessage( content: Exclude, ): content is { readonly message: ContextInjectionMessage } { return !Array.isArray(content); } function isInjectionResult( content: ContextInjectionContent | ContextInjectionResult, ): content is ContextInjectionResult { return typeof content === 'object' && content !== null && !Array.isArray(content) && 'content' in content; } function findInjections(history: readonly ContextMessage[], variant: string): number[] { const positions: number[] = []; history.forEach((message, index) => { if (message.origin?.kind === 'injection' && message.origin.variant === variant) positions.push(index); }); return positions; }