From 248b30efc5db270644db8819aba6d643f749ca62 Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Wed, 8 Jul 2026 20:26:27 +0800 Subject: [PATCH] refactor(agent-core-v2): remove the microCompaction domain - delete the microCompaction domain (service, wire model/op, config section, experimental flag) and its dedicated tests - stop truncating old tool results in the context projector and drop the projector's now-unused instantiation dependency - remove the domain from the layer map, package exports, and the DI x Scope dependency diagram - retarget the flag-registry test and skill examples at a neutral flag --- .agents/skills/agent-core-dev/align.md | 2 +- .agents/skills/agent-core-dev/flags.md | 8 +- .../agent-core-v2/docs/di-scope-domains.puml | 10 - .../agent-core-v2/docs/di-scope-domains.svg | 4 + packages/agent-core-v2/docs/flag.md | 12 +- .../agent-core-v2/docs/rw-model-design.md | 2 +- packages/agent-core-v2/docs/service-design.md | 2 +- .../scripts/check-domain-layers.mjs | 1 - .../contextMemory/contextMemoryService.ts | 2 +- .../contextProjectorService.ts | 18 +- .../src/agent/fullCompaction/compactionOps.ts | 3 +- .../fullCompaction/fullCompactionService.ts | 5 +- .../agent/microCompaction/configSection.ts | 31 - .../src/agent/microCompaction/flag.ts | 20 - .../agent/microCompaction/microCompaction.ts | 18 - .../microCompaction/microCompactionOps.ts | 35 - .../microCompaction/microCompactionService.ts | 269 ----- packages/agent-core-v2/src/index.ts | 6 - .../projector-tool-exchanges.test.ts | 5 +- .../test/contextProjector/projector.bench.ts | 6 +- packages/agent-core-v2/test/flag/flag.test.ts | 87 +- .../test/fullCompaction/full.test.ts | 110 -- packages/agent-core-v2/test/harness/agent.ts | 38 - packages/agent-core-v2/test/harness/index.ts | 1 - .../test/microCompaction/micro.test.ts | 996 ------------------ 25 files changed, 69 insertions(+), 1622 deletions(-) delete mode 100644 packages/agent-core-v2/src/agent/microCompaction/configSection.ts delete mode 100644 packages/agent-core-v2/src/agent/microCompaction/flag.ts delete mode 100644 packages/agent-core-v2/src/agent/microCompaction/microCompaction.ts delete mode 100644 packages/agent-core-v2/src/agent/microCompaction/microCompactionOps.ts delete mode 100644 packages/agent-core-v2/src/agent/microCompaction/microCompactionService.ts delete mode 100644 packages/agent-core-v2/test/microCompaction/micro.test.ts diff --git a/.agents/skills/agent-core-dev/align.md b/.agents/skills/agent-core-dev/align.md index 0bb778711..4345b13d8 100644 --- a/.agents/skills/agent-core-dev/align.md +++ b/.agents/skills/agent-core-dev/align.md @@ -90,7 +90,7 @@ Reference mapping (a **starting point**, not gospel — verify against the curre | `services/session/`, `session/` | `session`, `sessionStore`, `sessionMetaStore`, `sessionActivity`, `sessionContext`, `agentLifecycle` | | `services/tool/`, `tools/`, `agent/tool/` | `toolRegistry`, `toolStore`, `toolExecutor`, `tooldedup`, `userTool` | | `loop/`, `agent/` (turn loop) | `loop`, `llmRequester`, `llmRequestLog`, `turn` | -| `agent/context/`, `agent/compaction/` | `contextMemory`, `contextProjector`, `contextSize`, `microCompaction`, `fullCompaction`, `dynamicInjector` | +| `agent/context/`, `agent/compaction/` | `contextMemory`, `contextProjector`, `contextSize`, `fullCompaction`, `dynamicInjector` | | `agent/permission/` | `permission`, `permissionMode`, `permissionPolicy`, `permissionRules`, `approval`, `externalHooks` | | `agent/goal/`, `agent/plan/`, `agent/swarm/`, `agent/cron/`, `agent/background/` | `goal`, `plan`, `swarm`, `cron`, `background`, `subagentHost` | | `services/config/`, `agent/config/` | `config` | diff --git a/.agents/skills/agent-core-dev/flags.md b/.agents/skills/agent-core-dev/flags.md index fe731fc3e..abae89ac3 100644 --- a/.agents/skills/agent-core-dev/flags.md +++ b/.agents/skills/agent-core-dev/flags.md @@ -11,7 +11,7 @@ Gate not-yet-public features behind `IFlagService.enabled(id)`, per the reposito - `src/flag/flag.ts` — `IFlagService` token + resolver types (`ExperimentalFlagMap`, `ExperimentalFlagConfig`, `ExperimentalFlagSource`, `ExperimentalFeatureState`) + `ExperimentalConfigSchema` / `ExperimentalConfig` (zod). - `src/flag/flagService.ts` — `FlagService` impl + `MASTER_ENV` (`KIMI_CODE_EXPERIMENTAL_FLAG`) + `EXPERIMENTAL_SECTION` (`experimental`); reads definitions from `IFlagRegistry`; self-registers at App scope. - `src/flag/index.ts` — **removed (no barrel)**; `src/index.ts` imports the `flag` leafs precisely instead (e.g. `import './flag/flagService'`). -- `src//flag.ts` — each domain that owns a flag declares it here and calls `registerFlagDefinition` at the module top level (e.g. `src/microCompaction/flag.ts`). The directory already names the domain, so the file is just `flag.ts`. +- `src//flag.ts` — each domain that owns a flag declares it here and calls `registerFlagDefinition` at the module top level (e.g. `src/multiServer/flag.ts`). The directory already names the domain, so the file is just `flag.ts`. ## Public surface @@ -25,7 +25,7 @@ Gate not-yet-public features behind `IFlagService.enabled(id)`, per the reposito Highest wins; env is read live on every call (nothing cached): 1. Master env `KIMI_CODE_EXPERIMENTAL_FLAG` truthy → every flag on. -2. Per-feature `def.env` (e.g. `KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION`) → forces on/off. +2. Per-feature `def.env` (e.g. `KIMI_CODE_EXPERIMENTAL_MY_FEATURE`) → forces on/off. 3. `[experimental]` config section per-flag override. 4. Registry `default`. @@ -42,7 +42,7 @@ Config shape: ```toml [experimental] -micro_compaction = false +my_feature = false ``` Keys are intentionally loose (`z.record(z.string(), z.boolean())`), so obsolete flags stay inert config. @@ -89,7 +89,7 @@ Inject `IFlagService` and gate on it. It is resolvable from any scope (App ances ```ts constructor(@IFlagService private readonly flags: IFlagService) {} // ... -if (!this.flags.enabled('micro_compaction')) return; +if (!this.flags.enabled('my_feature')) return; ``` ## Layering & scope diff --git a/packages/agent-core-v2/docs/di-scope-domains.puml b/packages/agent-core-v2/docs/di-scope-domains.puml index de989f2af..8ae6553b6 100644 --- a/packages/agent-core-v2/docs/di-scope-domains.puml +++ b/packages/agent-core-v2/docs/di-scope-domains.puml @@ -104,7 +104,6 @@ package "Agent scope (per agent)" #FDF5E6 { rectangle "swarm\nAgent\n IAgentSwarmService" as swarm #FDEBD0 rectangle "mcp\nAgent\n IAgentMcpService" as mcp #FDEBD0 rectangle "fullCompaction\nAgent\n IAgentFullCompactionService" as fullCompaction #FDEBD0 - rectangle "microCompaction\nAgent\n IAgentMicroCompactionService" as microCompaction #FDEBD0 rectangle "externalHooks\nAgent\n IAgentExternalHooksService" as externalHooks #FDEBD0 rectangle "usage\nAgent\n IAgentUsageService" as usage #FDEBD0 rectangle "rpc\nAgent\n IAgentRPCService" as rpc #FDEBD0 @@ -306,14 +305,6 @@ fullCompaction --> telemetry #34495E fullCompaction --> wireRecord #34495E fullCompaction --> turn #34495E fullCompaction --> loop #34495E -microCompaction --> contextMemory #34495E -microCompaction --> contextSize #34495E -microCompaction --> wire #34495E -microCompaction --> flag #34495E -microCompaction --> profile #34495E -microCompaction --> telemetry #34495E -microCompaction --> loop #34495E -microCompaction --> config #34495E externalHooks --> config #34495E externalHooks --> bootstrap #34495E externalHooks --> plugin #34495E @@ -383,7 +374,6 @@ goal ..> wireRecord #16A085 : goal.create/update/clear goal ..> turn #16A085 : turn lifecycle hooks goal ..> loop #16A085 : step/usage hooks goal ..> eventSink #16A085 : hook.result / goal.updated -microCompaction ..> wire #16A085 : micro_compaction.apply swarm ..> wire #16A085 : swarm_mode.enter/exit swarm ..> turn #16A085 : hooks.onEnded task ..> wire #16A085 : task.started/terminated diff --git a/packages/agent-core-v2/docs/di-scope-domains.svg b/packages/agent-core-v2/docs/di-scope-domains.svg index 9dcc66387..8ee4e17cd 100644 --- a/packages/agent-core-v2/docs/di-scope-domains.svg +++ b/packages/agent-core-v2/docs/di-scope-domains.svg @@ -1 +1,5 @@ +<<<<<<< HEAD App scope (process-wide)Session scope (per session)Agent scope (per agent)bootstrapAppIBootstrapServicelog_base · App bindingILogServicetelemetryAppITelemetryServiceeventAppIEventServicestorageAppIFileSystemStorageServiceIAppendLogStoreIAtomicDocumentStoreIAtomicTomlDocumentStorefilestoreAppIFileStoregatewayAppIRestGatewayIWSGatewaysessionLifecycleAppISessionLifecycleServicesessionExportAppISessionExportServicesessionIndexAppISessionIndexhostFsAppIHostFileSystemworkspaceRegistryAppIWorkspaceRegistryworkspaceLocalConfigAppIWorkspaceLocalConfigServicehostFolderBrowserAppIHostFolderBrowserhostEnvironmentAppIHostEnvironmenthostProcessAppIHostProcessServiceauthAppIOAuthServiceIAuthSummaryServiceIWebSearchProviderServicewebAppIWebFetchServiceeditAppIFileEditServiceproviderAppIProviderServiceplatformAppIPlatformServiceflagAppIFlagServiceIFlagRegistryconfigAppIConfigRegistryIConfigServicepluginAppIPluginServicechatProviderAppIChatProviderFactorymodelAppIModelServicemodelCatalogAppIModelCatalogServiceagentProfileCatalogAppIAgentProfileCatalogServiceskillCatalogAppISkillDiscoveryISkillSourceBuiltin/UserSkillSourcesessionContextSessionISessionContext (seed)sessionMetadataSessionISessionMetadatasessionActivitySessionISessionActivityagentLifecycleSessionIAgentLifecycleServicesessionSwarmSessionISessionSwarmServiceinteractionSessionIInteractionServiceworkspaceContextSessionIWorkspaceContextworkspaceCommandSessionISessionWorkspaceCommandServicesessionLogSession bindingILogServicesessionSkillCatalogSessionISessionSkillCatalogISkillCatalogSinkWorkspace/PluginSkillSourceexternalHooksSessionISessionExternalHooksServicesessionFsSessionISessionFsServiceapprovalSessionIApprovalServicequestionSessionIQuestionServiceprocessSessionIProcessRunnerIProcessterminalAppIHostTerminalServicesessionTerminalSessionISessionTerminalServicemodelProviderSessionIModelProvider (seed)todoSessionISessionTodoServicewireRecordAgentIAgentWireRecordService (event hub)wireAgentIAgentWireServiceblobStoreAgentIAgentBlobStoreServicecontextMemoryAgentIAgentContextMemoryServicecontextProjectorAgentIAgentContextProjectorServicecontextInjectorAgentIAgentContextInjectorServicecontextSizeAgentIAgentContextSizeServicesystemReminderAgentIAgentSystemReminderServiceprofileAgentIAgentProfileServicepromptAgentIAgentPromptServicepromptLegacyAgentIAgentPromptLegacyServiceturnAgentIAgentTurnServiceloopAgentIAgentLoopServicellmRequesterAgentIAgentLLMRequesterServicetoolRegistryAgentIAgentToolRegistryServicetoolExecutorAgentIAgentToolExecutorServicetoolDedupeAgentIAgentToolDedupeServicepermissionGateAgentIAgentPermissionGatepermissionModeAgentIAgentPermissionModeServicepermissionPolicyAgentIAgentPermissionPolicyServicepermissionRulesAgentIAgentPermissionRulesServiceplanAgentIAgentPlanServicegoalAgentIAgentGoalServiceskillAgentIAgentSkillServicequestionToolsAgentIAgentQuestionToolsServiceuserToolAgentIAgentUserToolServicetaskAgentIAgentTaskServicecronAgentIAgentCronServiceswarmAgentIAgentSwarmServicemcpAgentIAgentMcpServicefullCompactionAgentIAgentFullCompactionServicemicroCompactionAgentIAgentMicroCompactionServiceexternalHooksAgentIAgentExternalHooksServiceusageAgentIAgentUsageServicerpcAgentIAgentRPCServicefileToolsAgentRead/Write/Grep/Glob toolseditAgentEditToolshellToolsAgentIAgentShellToolsServicescopeContextAgentIAgentScopeContext (seed)environmenteventSinkonDidChangeConfigurationcontext_size.measuredconfig.update / tools.set_active_toolsagent.status.updated / warningturn.launchhooks.onSplicedllm.tools_snapshot / requestpermission.set_modepermission.rules.add / record_approval_resultplan_mode.enter/cancel/exitgoal.create/update/clearturn lifecycle hooksstep/usage hooksskill.activatetools.register_/unregister_user_toolhooks.onSplicedcron.add / delete / cursorswarm_mode.enter/exithooks.onErrormicro_compaction.applySessionStart/End hooksSubagentStart/Stop (observes run hooks)usage.recordcontext.splicetodo.set (main agent)full_compaction.begin/cancel/completehook.result / goal.updatedhooks.onEndedtask.started/terminatedonWillExecuteTool / onDidExecuteToolapproval hooksprompt/end hooksstop hookcompaction hooksnotification hookhooks.onResumeEndedScope = node color      App (process-wide)      Session (per session)      Agent (per agent)Edgessolid: DI injection (ctor @IX)dashed: event-driven (subscribe/emit)direction: consumer ---> providerNotesGenerated from `node scripts/dep-graph.mjs` output;`_base` / seed / options deps are omitted. +======= +App scope (process-wide)Session scope (per session)Agent scope (per agent)bootstrapAppIBootstrapServicelog_base · App bindingILogServicetelemetryAppITelemetryServiceeventAppIEventServicestorageAppIFileSystemStorageServiceIAppendLogStoreIAtomicDocumentStoreIAtomicTomlDocumentStorefilestoreAppIFileStoregatewayAppIRestGatewayIWSGatewaysessionLifecycleAppISessionLifecycleServicesessionExportAppISessionExportServicesessionIndexAppISessionIndexhostFsAppIHostFileSystemworkspaceRegistryAppIWorkspaceRegistryworkspaceLocalConfigAppIWorkspaceLocalConfigServicehostFolderBrowserAppIHostFolderBrowserhostEnvironmentAppIHostEnvironmenthostProcessAppIHostProcessServiceauthAppIOAuthServiceIAuthSummaryServiceIWebSearchProviderServicewebAppIWebFetchServiceeditAppIFileEditServiceproviderAppIProviderServiceplatformAppIPlatformServiceflagAppIFlagServiceIFlagRegistryconfigAppIConfigRegistryIConfigServicepluginAppIPluginServicechatProviderAppIChatProviderFactorymodelAppIModelServicemodelCatalogAppIModelCatalogServiceagentProfileCatalogAppIAgentProfileCatalogServiceskillCatalogAppISkillDiscoveryISkillSourceBuiltin/UserSkillSourcesessionContextSessionISessionContext (seed)sessionMetadataSessionISessionMetadatasessionActivitySessionISessionActivityagentLifecycleSessionIAgentLifecycleServicesessionSwarmSessionISessionSwarmServiceinteractionSessionIInteractionServiceworkspaceContextSessionIWorkspaceContextworkspaceCommandSessionISessionWorkspaceCommandServicesessionLogSession bindingILogServicesessionSkillCatalogSessionISessionSkillCatalogISkillCatalogSinkWorkspace/PluginSkillSourceexternalHooksSessionISessionExternalHooksServicesessionFsSessionISessionFsServiceapprovalSessionIApprovalServicequestionSessionIQuestionServiceprocessSessionIProcessRunnerIProcessterminalAppIHostTerminalServicesessionTerminalSessionISessionTerminalServicemodelProviderSessionIModelProvider (seed)todoSessionISessionTodoServicewireRecordAgentIAgentWireRecordService (event hub)wireAgentIAgentWireServiceblobStoreAgentIAgentBlobStoreServicecontextMemoryAgentIAgentContextMemoryServicecontextProjectorAgentIAgentContextProjectorServicecontextInjectorAgentIAgentContextInjectorServicecontextSizeAgentIAgentContextSizeServicesystemReminderAgentIAgentSystemReminderServiceprofileAgentIAgentProfileServicepromptAgentIAgentPromptServicepromptLegacyAgentIAgentPromptLegacyServiceturnAgentIAgentTurnServiceloopAgentIAgentLoopServicellmRequesterAgentIAgentLLMRequesterServicetoolRegistryAgentIAgentToolRegistryServicetoolExecutorAgentIAgentToolExecutorServicetoolDedupeAgentIAgentToolDedupeServicepermissionGateAgentIAgentPermissionGatepermissionModeAgentIAgentPermissionModeServicepermissionPolicyAgentIAgentPermissionPolicyServicepermissionRulesAgentIAgentPermissionRulesServiceplanAgentIAgentPlanServicegoalAgentIAgentGoalServiceskillAgentIAgentSkillServicequestionToolsAgentIAgentQuestionToolsServiceuserToolAgentIAgentUserToolServicetaskAgentIAgentTaskServicecronAgentIAgentCronServiceswarmAgentIAgentSwarmServicemcpAgentIAgentMcpServicefullCompactionAgentIAgentFullCompactionServiceexternalHooksAgentIAgentExternalHooksServiceusageAgentIAgentUsageServicerpcAgentIAgentRPCServicefileToolsAgentRead/Write/Grep/Glob toolseditAgentEditToolshellToolsAgentIAgentShellToolsServicescopeContextAgentIAgentScopeContext (seed)environmenteventSinkonDidChangeConfigurationcontext_size.measuredconfig.update / tools.set_active_toolsagent.status.updated / warningturn.launchhooks.onSplicedllm.tools_snapshot / requestpermission.set_modepermission.rules.add / record_approval_resultplan_mode.enter/cancel/exitgoal.create/update/clearturn lifecycle hooksstep/usage hooksskill.activatetools.register_/unregister_user_toolhooks.onSplicedcron.add / delete / cursorswarm_mode.enter/exithooks.onErrorSessionStart/End hooksSubagentStart/Stop (observes run hooks)usage.recordcontext.splicetodo.set (main agent)full_compaction.begin/cancel/completehook.result / goal.updatedhooks.onEndedtask.started/terminatedonWillExecuteTool / onDidExecuteToolapproval hooksprompt/end hooksstop hookcompaction hooksnotification hookhooks.onResumeEndedScope = node color      App (process-wide)      Session (per session)      Agent (per agent)Edgessolid: DI injection (ctor @IX)dashed: event-driven (subscribe/emit)direction: consumer ---> providerNotesGenerated from `node scripts/dep-graph.mjs` output;`_base` / seed / options deps are omitted. +>>>>>>> f1c114ebd (refactor(agent-core-v2): remove the microCompaction domain) diff --git a/packages/agent-core-v2/docs/flag.md b/packages/agent-core-v2/docs/flag.md index ed6955269..0ac9340f0 100644 --- a/packages/agent-core-v2/docs/flag.md +++ b/packages/agent-core-v2/docs/flag.md @@ -11,7 +11,7 @@ Gates not-yet-public features behind `IFlagService.enabled(id)`, per the reposit - `src/flag/flag.ts` — `IFlagService` token + resolver types (`ExperimentalFlagMap`, `ExperimentalFlagConfig`, `ExperimentalFlagSource`, `ExperimentalFeatureState`) + `ExperimentalConfigSchema` / `ExperimentalConfig` (zod). - `src/flag/flagService.ts` — `FlagService` impl + `MASTER_ENV` (`KIMI_CODE_EXPERIMENTAL_FLAG`) + `EXPERIMENTAL_SECTION` (`experimental`); reads definitions from `IFlagRegistry`; self-registers at App scope. - `src/flag/index.ts` — barrel; re-exported by `src/index.ts` at the L3 block. -- `src//flag.ts` — each domain that owns a flag declares it here and calls `registerFlagDefinition` at the module top level (e.g. `src/microCompaction/flag.ts`). The directory already names the domain, so the file is just `flag.ts`. +- `src//flag.ts` — each domain that owns a flag declares it here and calls `registerFlagDefinition` at the module top level (e.g. `src/multiServer/flag.ts`). The directory already names the domain, so the file is just `flag.ts`. ## Public surface @@ -25,7 +25,7 @@ Gates not-yet-public features behind `IFlagService.enabled(id)`, per the reposit Highest wins; env is read live on every call (nothing cached): 1. L1 master env `KIMI_CODE_EXPERIMENTAL_FLAG` truthy → every flag on. -2. L2 per-feature `def.env` (e.g. `KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION`) → forces on/off. +2. L2 per-feature `def.env` (e.g. `KIMI_CODE_EXPERIMENTAL_MY_FEATURE`) → forces on/off. 3. L3 `[experimental]` config section per-flag override. 4. L4 registry `default`. @@ -42,7 +42,7 @@ Config shape mirrors v1: ```toml [experimental] -micro_compaction = false +my_feature = false ``` Keys are intentionally loose (`z.record(z.string(), z.boolean())`), so obsolete flags stay inert config. @@ -90,11 +90,9 @@ Inject `IFlagService` and gate on it. It is resolvable from any scope (App ances ```ts constructor(@IFlagService private readonly flags: IFlagService) {} // ... -if (!this.flags.enabled('micro_compaction')) return; +if (!this.flags.enabled('my_feature')) return; ``` -Current consumer: `microCompaction` (Agent scope) gates `micro_compaction`. - ## Layering & scope - Domain `flag` is registered at **L3** (`scripts/check-domain-layers.mjs` → `['flag', 3]`). It imports only `config` (L2) downward. @@ -105,7 +103,7 @@ Current consumer: `microCompaction` (Agent scope) gates `micro_compaction`. ## References - `packages/agent-core-v2/src/flag/` — implementation (`IFlagRegistry` + `IFlagService`). -- `packages/agent-core-v2/src/microCompaction/flag.ts` — example per-domain flag contribution. +- `packages/agent-core-v2/src/app/multiServer/flag.ts` — example per-domain flag contribution. - `packages/agent-core-v2/test/flag/flag.test.ts` — precedence + config subscription tests. - `packages/agent-core/src/flags/` — v1 source this was ported from. - `plan/PLAN.md` §2/§3 — domain placement (`flag` at L3, not `_base/flags`). diff --git a/packages/agent-core-v2/docs/rw-model-design.md b/packages/agent-core-v2/docs/rw-model-design.md index ecbd52261..e05d361f0 100644 --- a/packages/agent-core-v2/docs/rw-model-design.md +++ b/packages/agent-core-v2/docs/rw-model-design.md @@ -281,7 +281,7 @@ setTodos(todos: TodoItem[]): void { - **Command 不持有可折叠状态**。所有"resume 后必须还在"的状态在 view 里。 service 私有字段只允许装真正的运行时资源(进程句柄、定时器、连接)。 - **Command 不在 replay 中运行**(相位机保证)。resume 复用 live 命令的 hack - (microCompaction)消失:replay 只折叠 fact。 + 消失:replay 只折叠 fact。 - 需要"先答应再补偿"的命令(plan.enter 失败后 cancel)就是两次 commit—— 补偿也是事实,天然可回放。 - `define()` 的 facet 机制退役:`resume` → view fold;`toLive` → 定义处 diff --git a/packages/agent-core-v2/docs/service-design.md b/packages/agent-core-v2/docs/service-design.md index 0e75278b9..f55f215c9 100644 --- a/packages/agent-core-v2/docs/service-design.md +++ b/packages/agent-core-v2/docs/service-design.md @@ -192,7 +192,7 @@ an **event**, or a **hook**. From first principles, they answer three different Note that the wire is the *durable record*, not the live notification channel: a live `spliceHistory(...)` call appends a `context.splice` record to the wire *and* applies it, and `contextMemory` then fires `hooks.onSpliced`, which `contextSize` / `loop` / - `background` / `microCompaction` / `dynamicInjector` actually subscribe to. Those listeners + `background` / `dynamicInjector` actually subscribe to. Those listeners react to the **hook**, not the wire — the wire is what makes the splice replayable. ### One-sentence rule diff --git a/packages/agent-core-v2/scripts/check-domain-layers.mjs b/packages/agent-core-v2/scripts/check-domain-layers.mjs index c503cd3a5..08365a0a3 100644 --- a/packages/agent-core-v2/scripts/check-domain-layers.mjs +++ b/packages/agent-core-v2/scripts/check-domain-layers.mjs @@ -148,7 +148,6 @@ const DOMAIN_LAYER = new Map([ ['contextProjector', 4], ['contextSize', 4], ['fullCompaction', 4], - ['microCompaction', 4], ['loop', 4], ['media', 4], // `edit` spans two scopes: the App-scope `IFileEditService` capability (pure diff --git a/packages/agent-core-v2/src/agent/contextMemory/contextMemoryService.ts b/packages/agent-core-v2/src/agent/contextMemory/contextMemoryService.ts index 7585c7b05..6589343f6 100644 --- a/packages/agent-core-v2/src/agent/contextMemory/contextMemoryService.ts +++ b/packages/agent-core-v2/src/agent/contextMemory/contextMemoryService.ts @@ -13,7 +13,7 @@ * are the unmeasured tail (see `contextSizeService`). Every mutation still fires * `onSpliced` from the live path only (replay rebuilds * the Model silently and never invokes these methods), so existing subscribers - * (micro-compaction, context-injector, task-notification) observe the same + * (context-injector, task-notification) observe the same * splice-shaped change events regardless of which 1.4 Op was persisted. Message * ids are stamped at the dispatch call site so `apply` stays pure. Blob * dehydrate/rehydrate is declared on `ContextModel.blobs`. Bound at diff --git a/packages/agent-core-v2/src/agent/contextProjector/contextProjectorService.ts b/packages/agent-core-v2/src/agent/contextProjector/contextProjectorService.ts index de5863c0f..ab81b6396 100644 --- a/packages/agent-core-v2/src/agent/contextProjector/contextProjectorService.ts +++ b/packages/agent-core-v2/src/agent/contextProjector/contextProjectorService.ts @@ -1,31 +1,20 @@ -import { IInstantiationService } from "#/_base/di/instantiation"; import { InstantiationType } from '#/_base/di/extensions'; import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; import { renderToolResultForModel } from '#/agent/contextMemory/toolResultRender'; import type { ContextMessage } from '#/agent/contextMemory/types'; import { ErrorCodes, KimiError } from '#/errors'; -import { IAgentMicroCompactionService } from '#/agent/microCompaction/microCompaction'; import type { ContentPart, Message } from '#/app/llmProtocol/message'; import { IAgentContextProjectorService } from './contextProjector'; export class AgentContextProjectorService implements IAgentContextProjectorService { declare readonly _serviceBrand: undefined; - constructor( - @IInstantiationService private readonly instantiation: IInstantiationService, - ) {} project(messages: readonly ContextMessage[]): readonly Message[] { - return project(this.microCompaction().compact(messages)); + return project(messages); } projectStrict(messages: readonly ContextMessage[]): readonly Message[] { - return projectStrict(this.microCompaction().compact(messages)); - } - - private microCompaction(): IAgentMicroCompactionService { - return this.instantiation.invokeFunction((accessor) => - accessor.get(IAgentMicroCompactionService), - ); + return projectStrict(messages); } } @@ -102,8 +91,7 @@ function dropLeadingNonUserMessages(messages: readonly Message[]): Message[] { // never anchor an exchange. Tool messages are skipped where they originally // sat — a result either lands in its call's slot or it is an orphan, // wire-invalid and useless to the model. A history with no assistant at all -// is a bare sizing slice (micro-compaction sizes single messages this way) -// and passes through as-is. Emitting cleans each message (drops empty / +// is a bare sizing slice and passes through as-is. Emitting cleans each message (drops empty / // whitespace-only text blocks, rejected by strict providers), merges runs of // adjacent user prompts (accumulated and materialized once per run), and // strips context-only metadata off the wire. diff --git a/packages/agent-core-v2/src/agent/fullCompaction/compactionOps.ts b/packages/agent-core-v2/src/agent/fullCompaction/compactionOps.ts index 40c007765..baa3e9391 100644 --- a/packages/agent-core-v2/src/agent/fullCompaction/compactionOps.ts +++ b/packages/agent-core-v2/src/agent/fullCompaction/compactionOps.ts @@ -33,8 +33,7 @@ * re-declared). The `full_compaction.*` record shapes stay declared in * `WireRecordMap` (see `fullCompactionService.ts`) because the records still * ride the per-agent `wire.jsonl` log read by `wireRecord.restore()` / - * `getRecords()` — `microCompaction` registers a `full_compaction.complete` - * resumer against that stream. Consumed by the Agent-scope `fullCompactionService`. + * `getRecords()`. Consumed by the Agent-scope `fullCompactionService`. */ import { defineModel } from '#/wire/model'; diff --git a/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts b/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts index 4fc999407..c79fa1d9a 100644 --- a/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts +++ b/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts @@ -61,9 +61,8 @@ import { OrderedHookSlot } from '#/hooks'; // The `full_compaction.*` record shapes stay declared in `WireRecordMap` // because the records still ride the per-agent `wire.jsonl` log read by -// `wireRecord.restore()` / `getRecords()`: `microCompaction` registers a -// `full_compaction.complete` resumer against that stream. fullCompaction itself -// no longer registers resumers here — its state rebuilds from the same log via +// `wireRecord.restore()` / `getRecords()`. fullCompaction itself no longer +// registers resumers here — its state rebuilds from the same log via // `wire.replay` into `CompactionModel`. declare module '#/agent/wireRecord/wireRecord' { interface WireRecordMap { diff --git a/packages/agent-core-v2/src/agent/microCompaction/configSection.ts b/packages/agent-core-v2/src/agent/microCompaction/configSection.ts deleted file mode 100644 index 555555ba4..000000000 --- a/packages/agent-core-v2/src/agent/microCompaction/configSection.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * `microCompaction` domain (L4) - micro-compaction config-section schema. - * - * Owns the `[micro_compaction]` tuning section consumed by - * `AgentMicroCompactionService`. Self-registered at module load via - * `registerConfigSection`. - */ - -import { z } from 'zod'; - -import { registerConfigSection } from '#/app/config/configSectionContributions'; - -export const MICRO_COMPACTION_SECTION = 'microCompaction'; - -const microCompactionConfigShape = { - keepRecentMessages: z.number().int().min(0), - minContentTokens: z.number().int().min(0), - cacheMissedThresholdMs: z.number().int().min(0), - truncatedMarker: z.string(), - minContextUsageRatio: z.number().min(0).max(1), -}; - -const microCompactionConfigObject = z.object(microCompactionConfigShape); - -export type MicroCompactionConfig = z.infer; - -export const MicroCompactionConfigSchema = microCompactionConfigObject.partial(); - -export type MicroCompactionConfigPatch = z.infer; - -registerConfigSection(MICRO_COMPACTION_SECTION, MicroCompactionConfigSchema); diff --git a/packages/agent-core-v2/src/agent/microCompaction/flag.ts b/packages/agent-core-v2/src/agent/microCompaction/flag.ts deleted file mode 100644 index f8a835139..000000000 --- a/packages/agent-core-v2/src/agent/microCompaction/flag.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * `microCompaction` domain — flag contribution. - * - * Registers the `micro_compaction` experimental flag at import time, so the - * flag is available process-wide before any consumer resolves `IFlagService`. - */ - -import { type FlagDefinitionInput, registerFlagDefinition } from '#/app/flag/flagRegistry'; - -export const microCompactionFlag: FlagDefinitionInput = { - id: 'micro_compaction', - title: 'Micro compaction', - description: - 'Trim older large tool results from context while keeping recent conversation intact.', - env: 'KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION', - default: true, - surface: 'core', -}; - -registerFlagDefinition(microCompactionFlag); diff --git a/packages/agent-core-v2/src/agent/microCompaction/microCompaction.ts b/packages/agent-core-v2/src/agent/microCompaction/microCompaction.ts deleted file mode 100644 index 43cec5675..000000000 --- a/packages/agent-core-v2/src/agent/microCompaction/microCompaction.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * `microCompaction` domain (L4) - micro-compaction service contract. - * - * Defines the Agent-scoped `IAgentMicroCompactionService` used by context - * projection. Bound at Agent scope. - */ - -import { createDecorator } from "#/_base/di/instantiation"; -import type { ContextMessage } from '#/agent/contextMemory/types'; - -export interface IAgentMicroCompactionService { - readonly _serviceBrand: undefined; - - compact(messages: readonly ContextMessage[]): readonly ContextMessage[]; -} - -export const IAgentMicroCompactionService = - createDecorator('agentMicroCompactionService'); diff --git a/packages/agent-core-v2/src/agent/microCompaction/microCompactionOps.ts b/packages/agent-core-v2/src/agent/microCompaction/microCompactionOps.ts deleted file mode 100644 index 5c1c4f46d..000000000 --- a/packages/agent-core-v2/src/agent/microCompaction/microCompactionOps.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * `microCompaction` domain (L4) — wire Model (`MicroCompactionModel`) and the - * `micro_compaction.apply` (`microCompactionApply`) Op for the cache-miss - * compaction cutoff over `contextMemory`. - * - * Declares the cutoff as `{ cutoff }` (initial `{ cutoff: 0 }`): the message - * index below which old tool results are truncated. `cutoff` is a non-negative - * index (0 truncates nothing), so the reset value folds into the same Op as - * `cutoff: 0` rather than a separate record — keeping the resume-time cutoff - * reset (previously a `full_compaction.complete` resumer) inside the same - * replayable stream. `apply` is pure and returns the SAME reference on a no-op - * so the wire's reference-equality gate stays quiet; it carries no - * non-determinism, so `wire.dispatch(microCompactionApply(...))` and - * `wire.replay` produce identical state — replay rebuilds the cutoff, including - * the explicit `cutoff: 0` resets recorded on the live full-compaction / - * context-clear paths. Consumed by the Agent-scope `microCompactionService`. - */ - -import { defineModel } from '#/wire/model'; -import { defineOp } from '#/wire/op'; - -export interface MicroCompactionState { - readonly cutoff: number; -} - -export const MicroCompactionModel = defineModel('microCompaction', () => ({ - cutoff: 0, -})); - -export const microCompactionApply = defineOp(MicroCompactionModel, 'micro_compaction.apply', { - apply: (s, p: MicroCompactionState): MicroCompactionState => { - const cutoff = Math.max(0, p.cutoff); - return s.cutoff === cutoff ? s : { cutoff }; - }, -}); diff --git a/packages/agent-core-v2/src/agent/microCompaction/microCompactionService.ts b/packages/agent-core-v2/src/agent/microCompaction/microCompactionService.ts deleted file mode 100644 index d520a536e..000000000 --- a/packages/agent-core-v2/src/agent/microCompaction/microCompactionService.ts +++ /dev/null @@ -1,269 +0,0 @@ -/** - * `microCompaction` domain (L4) - `IAgentMicroCompactionService` implementation. - * - * Tracks cache-miss compaction cutoffs over `contextMemory` in the wire - * `MicroCompactionModel` (`{ cutoff }`): reads it through `wire.getModel`, - * writes it through `wire.dispatch(microCompactionApply(...))` (both advances - * from `detect` and the `cutoff: 0` resets recorded on the live full-compaction - * / context-clear paths), so `wire.replay` rebuilds the cutoff — including the - * resume-time reset — without a `full_compaction.complete` resumer. Sizes - * context via `contextSize`, resolves model capacity through `profile`, gates - * behavior through `flag`, reads tuning through `config`, emits telemetry, and - * participates in `loop` hooks. The effective cutoff is clamped to the current - * context length at read time (an undo that shortens the context cannot leave a - * dangling cutoff), so the delete-clamp needs no persisted record. Bound at - * Agent scope. - */ - -import type { ContentPart } from '#/app/llmProtocol/message'; -import { InstantiationType } from '#/_base/di/extensions'; -import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; - -import { Disposable } from "#/_base/di/lifecycle"; -import { - estimateTokensForContentParts, - estimateTokensForMessages, -} from "#/_base/utils/tokens"; -import type { TelemetryProperties } from '#/app/telemetry/telemetry'; -import { IConfigService } from '#/app/config/config'; -import { IEventBus } from '#/app/event/eventBus'; -import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory'; -import { IAgentContextSizeService } from '#/agent/contextSize/contextSize'; -import { IFlagService } from '#/app/flag/flag'; -import { IAgentLoopService } from '#/agent/loop/loop'; -import { IAgentProfileService } from '#/agent/profile/profile'; -import { ITelemetryService } from '#/app/telemetry/telemetry'; -import type { ContextMessage } from '#/agent/contextMemory/types'; -import { IAgentWireService } from '#/wire/tokens'; -import type { IWireService } from '#/wire/wireService'; -import { IAgentMicroCompactionService } from './microCompaction'; -import { MicroCompactionModel, microCompactionApply } from './microCompactionOps'; -import { - MICRO_COMPACTION_SECTION, - type MicroCompactionConfig, - type MicroCompactionConfigPatch, -} from './configSection'; - -const DEFAULT_CONFIG: MicroCompactionConfig = { - keepRecentMessages: 20, - minContentTokens: 100, - cacheMissedThresholdMs: 60 * 60 * 1000, - truncatedMarker: '[Old tool result content cleared]', - minContextUsageRatio: 0.5, -}; - -export class AgentMicroCompactionService - extends Disposable - implements IAgentMicroCompactionService -{ - declare readonly _serviceBrand: undefined; - private microConfig: MicroCompactionConfig; - private _lastAssistantAt: number | null = null; - - constructor( - @IAgentContextMemoryService private readonly context: IAgentContextMemoryService, - @IAgentContextSizeService private readonly contextSize: IAgentContextSizeService, - @IAgentWireService private readonly wire: IWireService, - @IFlagService private readonly flags: IFlagService, - @IAgentProfileService private readonly profile: IAgentProfileService, - @ITelemetryService private readonly telemetry: ITelemetryService, - @IAgentLoopService loop: IAgentLoopService, - @IConfigService private readonly config: IConfigService, - @IEventBus private readonly eventBus: IEventBus, - ) { - super(); - this.microConfig = this.readConfig(); - this._register( - this.config.onDidSectionChange((event) => { - if (event.domain === MICRO_COMPACTION_SECTION) { - this.microConfig = this.readConfig(); - } - }), - ); - this._register( - loop.hooks.beforeStep.register( - 'micro-compaction', - async (_ctx, next) => { - this.detect(); - await next(); - }, - ), - ); - this._register( - this.eventBus.subscribe('context.spliced', (e) => this.observeSplice(e)), - ); - } - - get lastAssistantAt(): number | null { - return this._lastAssistantAt; - } - - private get cutoff(): number { - const cutoff = this.wire.getModel(MicroCompactionModel).cutoff; - const length = (this.context.get() as readonly ContextMessage[]).length; - return cutoff <= length ? cutoff : length; - } - - private reset(maxCutoff = 0): void { - const next = Math.min(this.cutoff, maxCutoff); - if (next === this.cutoff) return; - this.wire.dispatch(microCompactionApply({ cutoff: next })); - } - - private apply(cutoff: number): void { - this.wire.dispatch(microCompactionApply({ cutoff })); - } - - private detect(): void { - if (!this.flags.enabled('micro_compaction')) return; - - const lastAssistantAt = this._lastAssistantAt; - if (lastAssistantAt === null) return; - - const cacheAgeMs = Date.now() - lastAssistantAt; - if (cacheAgeMs < this.microConfig.cacheMissedThresholdMs) return; - - const history = this.context.get(); - if (this.contextSizeRatio() < this.microConfig.minContextUsageRatio) return; - - const previousCutoff = this.cutoff; - const nextCutoff = Math.max(0, history.length - this.microConfig.keepRecentMessages); - this.apply(nextCutoff); - if (previousCutoff === nextCutoff) return; - - const effect = this.measureEffect(history, nextCutoff); - const previousEffect = this.measureEffect(history, previousCutoff); - const rawContextTokens = estimateTokensForMessages(history); - const properties: TelemetryProperties = { - keep_recent_messages: this.microConfig.keepRecentMessages, - min_content_tokens: this.microConfig.minContentTokens, - cache_missed_threshold_ms: this.microConfig.cacheMissedThresholdMs, - truncated_marker: this.microConfig.truncatedMarker, - min_context_usage_ratio: this.microConfig.minContextUsageRatio, - truncated_tool_result_count: effect.truncatedToolResultCount, - truncated_tool_result_tokens_before: effect.truncatedToolResultTokensBefore, - truncated_tool_result_tokens_after: effect.truncatedToolResultTokensAfter, - tokens_before: - rawContextTokens - - previousEffect.truncatedToolResultTokensBefore + - previousEffect.truncatedToolResultTokensAfter, - tokens_after: - rawContextTokens - - effect.truncatedToolResultTokensBefore + - effect.truncatedToolResultTokensAfter, - previous_cutoff: previousCutoff, - cutoff: nextCutoff, - message_count: history.length, - cache_age_ms: cacheAgeMs, - thinking_level: this.profile.data().thinkingLevel, - }; - this.telemetry.track('micro_compaction_finished', properties); - } - - compact(messages: readonly ContextMessage[]): readonly ContextMessage[] { - if (!this.flags.enabled('micro_compaction')) return messages; - - const result: ContextMessage[] = []; - let index = 0; - for (const message of messages) { - if (this.shouldTruncate(message, index)) { - result.push({ - ...message, - content: [ - { type: 'text', text: this.microConfig.truncatedMarker } satisfies ContentPart, - ], - }); - } else { - result.push(message); - } - index++; - } - return result; - } - - private observeSplice(context: { - readonly deleteCount: number; - readonly messages: readonly ContextMessage[]; - }): void { - if (this.context.get().length === 0) { - this._lastAssistantAt = null; - this.reset(); - return; - } - - if (context.messages.some(isCompactionSummary)) { - this.reset(); - } - - if (context.messages.some(isAssistantCacheAnchor)) { - this._lastAssistantAt = Date.now(); - } - } - - private shouldTruncate(message: ContextMessage, index: number): boolean { - return ( - index < this.cutoff && - message.role === 'tool' && - message.toolCallId !== undefined && - estimateTokensForContentParts(message.content) >= this.microConfig.minContentTokens - ); - } - - private readConfig(): MicroCompactionConfig { - const config = this.config.get(MICRO_COMPACTION_SECTION); - return { ...DEFAULT_CONFIG, ...config }; - } - - private contextSizeRatio(): number { - const maxContextTokens = this.profile.getModelCapabilities().max_context_tokens; - if (maxContextTokens === undefined || maxContextTokens <= 0) return 1; - return this.contextSize.get().size / maxContextTokens; - } - - private measureEffect( - messages: readonly ContextMessage[], - cutoff: number, - ) { - let markerTokenCount: number | undefined; - let truncatedToolResultCount = 0; - let truncatedToolResultTokensBefore = 0; - let truncatedToolResultTokensAfter = 0; - for (let i = 0; i < messages.length && i < cutoff; i++) { - const message = messages[i]; - if (message === undefined || message.role !== 'tool' || message.toolCallId === undefined) { - continue; - } - - const contentTokens = estimateTokensForContentParts(message.content); - if (contentTokens < this.microConfig.minContentTokens) continue; - - markerTokenCount ??= estimateTokensForContentParts([ - { type: 'text', text: this.microConfig.truncatedMarker }, - ]); - truncatedToolResultCount += 1; - truncatedToolResultTokensBefore += contentTokens; - truncatedToolResultTokensAfter += markerTokenCount; - } - return { - truncatedToolResultCount, - truncatedToolResultTokensBefore, - truncatedToolResultTokensAfter, - }; - } -} - -function isCompactionSummary(message: ContextMessage): boolean { - return message.origin?.kind === 'compaction_summary'; -} - -function isAssistantCacheAnchor(message: ContextMessage): boolean { - return message.role === 'assistant' && !isCompactionSummary(message); -} - -registerScopedService( - LifecycleScope.Agent, - IAgentMicroCompactionService, - AgentMicroCompactionService, - InstantiationType.Eager, - 'microCompaction', -); diff --git a/packages/agent-core-v2/src/index.ts b/packages/agent-core-v2/src/index.ts index 21b45469a..aaa5fccef 100644 --- a/packages/agent-core-v2/src/index.ts +++ b/packages/agent-core-v2/src/index.ts @@ -340,12 +340,6 @@ export * from '#/agent/mcp/config-schema'; export * from '#/agent/media/mediaTools'; export * from '#/agent/media/mediaToolsRegistrar'; export * from '#/agent/media/registerMediaTools'; -import '#/agent/microCompaction/flag'; -export * from '#/agent/microCompaction/configSection'; -export * from '#/agent/microCompaction/microCompaction'; -export * from '#/agent/microCompaction/microCompactionOps'; -export * from '#/agent/microCompaction/flag'; -export * from '#/agent/microCompaction/microCompactionService'; export * from '#/agent/permissionMode/permissionMode'; export * from '#/agent/permissionMode/permissionModeService'; export * from '#/agent/permissionPolicy/permissionPolicy'; diff --git a/packages/agent-core-v2/test/contextProjector/projector-tool-exchanges.test.ts b/packages/agent-core-v2/test/contextProjector/projector-tool-exchanges.test.ts index 1ec10ca51..81dc95977 100644 --- a/packages/agent-core-v2/test/contextProjector/projector-tool-exchanges.test.ts +++ b/packages/agent-core-v2/test/contextProjector/projector-tool-exchanges.test.ts @@ -6,7 +6,6 @@ import { TestInstantiationService } from '#/_base/di/test'; import type { ContextMessage } from '#/agent/contextMemory/types'; import { IAgentContextProjectorService } from '#/agent/contextProjector/contextProjector'; import { AgentContextProjectorService } from '#/agent/contextProjector/contextProjectorService'; -import { IAgentMicroCompactionService } from '#/agent/microCompaction/microCompaction'; import { toProtocolMessage } from '#/agent/contextMemory/messageProjection'; import type { Message } from '#/app/llmProtocol/message'; @@ -51,7 +50,6 @@ describe('projector tool-exchange normalization', () => { disposables = new DisposableStore(); const ix = disposables.add(new TestInstantiationService()); ix.set(IAgentContextProjectorService, new SyncDescriptor(AgentContextProjectorService)); - ix.stub(IAgentMicroCompactionService, { compact: (messages) => messages }); projector = ix.get(IAgentContextProjectorService); }); @@ -201,8 +199,7 @@ describe('projector tool-exchange normalization', () => { }); it('keeps a bare result slice with no preceding assistant (used for sizing)', () => { - // micro-compaction projects single messages to size them — a leading result - // is kept rather than treated as an orphan. + // A leading result is kept rather than treated as an orphan. expect(shape([toolResult('c1', 'partial result')])).toEqual(['tool:c1']); }); diff --git a/packages/agent-core-v2/test/contextProjector/projector.bench.ts b/packages/agent-core-v2/test/contextProjector/projector.bench.ts index 6630424fc..165860a48 100644 --- a/packages/agent-core-v2/test/contextProjector/projector.bench.ts +++ b/packages/agent-core-v2/test/contextProjector/projector.bench.ts @@ -4,8 +4,8 @@ * * `projectLegacy` below is the previous implementation, copied verbatim so the * comparison stays runnable after the old code is gone. The "new" side goes - * through the real `AgentContextProjectorService` with micro-compaction - * stubbed to a pass-through, so it measures exactly the projection path. + * through the real `AgentContextProjectorService`, so it measures exactly the + * projection path. * * Run: * pnpm --filter @moonshot-ai/agent-core-v2 exec vitest bench test/contextProjector/projector.bench.ts @@ -19,7 +19,6 @@ import { TestInstantiationService } from '#/_base/di/test'; import type { ContextMessage } from '#/agent/contextMemory/types'; import { IAgentContextProjectorService } from '#/agent/contextProjector/contextProjector'; import { AgentContextProjectorService } from '#/agent/contextProjector/contextProjectorService'; -import { IAgentMicroCompactionService } from '#/agent/microCompaction/microCompaction'; import { ErrorCodes, KimiError } from '#/errors'; import type { ContentPart, Message, TextPart, ToolCall } from '#/app/llmProtocol/message'; @@ -187,7 +186,6 @@ function makeMixedHistory(turns: number): ContextMessage[] { function createProjector(disposables: DisposableStore): IAgentContextProjectorService { const ix = disposables.add(new TestInstantiationService()); ix.set(IAgentContextProjectorService, new SyncDescriptor(AgentContextProjectorService)); - ix.stub(IAgentMicroCompactionService, { compact: (messages) => messages }); return ix.get(IAgentContextProjectorService); } diff --git a/packages/agent-core-v2/test/flag/flag.test.ts b/packages/agent-core-v2/test/flag/flag.test.ts index e002dae9c..8775bec7d 100644 --- a/packages/agent-core-v2/test/flag/flag.test.ts +++ b/packages/agent-core-v2/test/flag/flag.test.ts @@ -22,12 +22,11 @@ import { IFileSystemStorageService } from '#/persistence/interface/storage'; import { stubBootstrap } from '../bootstrap/stubs'; import { stubLog } from '../log/stubs'; -const microCompactionFlag: FlagDefinitionInput = { - id: 'micro_compaction', - title: 'Micro compaction', - description: - 'Trim older large tool results from context while keeping recent conversation intact.', - env: 'KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION', +const exampleFlag: FlagDefinitionInput = { + id: 'example_flag', + title: 'Example flag', + description: 'Example experimental flag used to exercise the flag registry.', + env: 'KIMI_CODE_EXPERIMENTAL_EXAMPLE_FLAG', default: true, surface: 'core', }; @@ -35,9 +34,9 @@ const microCompactionFlag: FlagDefinitionInput = { describe('FlagRegistryService', () => { it('registers and resolves by id', () => { const reg = new FlagRegistryService(); - reg.register(microCompactionFlag); - expect(reg.list().map((d) => d.id)).toEqual(['micro_compaction']); - expect(reg.get('micro_compaction')?.env).toBe('KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION'); + reg.register(exampleFlag); + expect(reg.list().map((d) => d.id)).toEqual(['example_flag']); + expect(reg.get('example_flag')?.env).toBe('KIMI_CODE_EXPERIMENTAL_EXAMPLE_FLAG'); }); it('returns undefined for an unknown id', () => { @@ -47,15 +46,15 @@ describe('FlagRegistryService', () => { it('throws on a duplicate id', () => { const reg = new FlagRegistryService(); - reg.register(microCompactionFlag); - expect(() => reg.register(microCompactionFlag)).toThrow(); + reg.register(exampleFlag); + expect(() => reg.register(exampleFlag)).toThrow(); }); it('unregisters when the returned disposable is disposed', () => { const reg = new FlagRegistryService(); - const handle = reg.register(microCompactionFlag); + const handle = reg.register(exampleFlag); handle.dispose(); - expect(reg.get('micro_compaction')).toBeUndefined(); + expect(reg.get('example_flag')).toBeUndefined(); }); }); @@ -79,7 +78,7 @@ describe('FlagService', () => { ix.set(IConfigService, new SyncDescriptor(ConfigService)); ix.set(IFlagRegistry, new SyncDescriptor(FlagRegistryService)); ix.set(IFlagService, new SyncDescriptor(FlagService)); - ix.get(IFlagRegistry).register(microCompactionFlag); + ix.get(IFlagRegistry).register(exampleFlag); return { registry: ix.get(IConfigRegistry), config: ix.get(IConfigService), @@ -97,10 +96,10 @@ describe('FlagService', () => { it('resolves the registry default when nothing overrides it', () => { const { flags } = makeFlags(); - const state = flags.explain('micro_compaction'); + const state = flags.explain('example_flag'); expect(state?.enabled).toBe(true); expect(state?.source).toBe('default'); - expect(flags.enabled('micro_compaction')).toBe(true); + expect(flags.enabled('example_flag')).toBe(true); }); it('returns undefined for an unregistered flag', () => { @@ -111,8 +110,8 @@ describe('FlagService', () => { it('applies config overrides above the default', async () => { const { config, flags } = makeFlags(); - await config.set(EXPERIMENTAL_SECTION, { micro_compaction: false }); - const state = flags.explain('micro_compaction'); + await config.set(EXPERIMENTAL_SECTION, { example_flag: false }); + const state = flags.explain('example_flag'); expect(state?.enabled).toBe(false); expect(state?.source).toBe('config'); expect(state?.configValue).toBe(false); @@ -120,10 +119,10 @@ describe('FlagService', () => { it('lets per-feature env override config', async () => { const { config, flags } = makeFlags({ - KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION: 'true', + KIMI_CODE_EXPERIMENTAL_EXAMPLE_FLAG: 'true', }); - await config.set(EXPERIMENTAL_SECTION, { micro_compaction: false }); - const state = flags.explain('micro_compaction'); + await config.set(EXPERIMENTAL_SECTION, { example_flag: false }); + const state = flags.explain('example_flag'); expect(state?.enabled).toBe(true); expect(state?.source).toBe('env'); expect(state?.configValue).toBe(false); @@ -131,70 +130,70 @@ describe('FlagService', () => { it('lets the master env switch force every flag on', async () => { const { config, flags } = makeFlags({ [MASTER_ENV]: '1' }); - await config.set(EXPERIMENTAL_SECTION, { micro_compaction: false }); - const state = flags.explain('micro_compaction'); + await config.set(EXPERIMENTAL_SECTION, { example_flag: false }); + const state = flags.explain('example_flag'); expect(state?.enabled).toBe(true); expect(state?.source).toBe('master-env'); }); it('refreshes overrides when the experimental config section changes', async () => { const { config, flags } = makeFlags(); - expect(flags.enabled('micro_compaction')).toBe(true); - await config.set(EXPERIMENTAL_SECTION, { micro_compaction: false }); - expect(flags.enabled('micro_compaction')).toBe(false); - await config.set(EXPERIMENTAL_SECTION, { micro_compaction: true }); - expect(flags.enabled('micro_compaction')).toBe(true); + expect(flags.enabled('example_flag')).toBe(true); + await config.set(EXPERIMENTAL_SECTION, { example_flag: false }); + expect(flags.enabled('example_flag')).toBe(false); + await config.set(EXPERIMENTAL_SECTION, { example_flag: true }); + expect(flags.enabled('example_flag')).toBe(true); }); it('ignores unrelated config section changes', async () => { const { config, flags } = makeFlags(); await config.set('agent', { modelAlias: 'k2' }); - expect(flags.explain('micro_compaction')?.source).toBe('default'); + expect(flags.explain('example_flag')?.source).toBe('default'); }); it('supports imperative setConfigOverrides', () => { const { flags } = makeFlags(); - flags.setConfigOverrides({ micro_compaction: false }); - expect(flags.enabled('micro_compaction')).toBe(false); + flags.setConfigOverrides({ example_flag: false }); + expect(flags.enabled('example_flag')).toBe(false); flags.setConfigOverrides(undefined); - expect(flags.enabled('micro_compaction')).toBe(true); + expect(flags.enabled('example_flag')).toBe(true); }); it('exposes snapshot / enabledIds / explainAll', () => { const { flags } = makeFlags(); - expect(flags.snapshot()).toEqual({ micro_compaction: true }); - expect(flags.enabledIds()).toEqual(['micro_compaction']); - expect(flags.explainAll().map((s) => s.id)).toEqual(['micro_compaction']); + expect(flags.snapshot()).toEqual({ example_flag: true }); + expect(flags.enabledIds()).toEqual(['example_flag']); + expect(flags.explainAll().map((s) => s.id)).toEqual(['example_flag']); }); it('treats truthy env values case-insensitively', () => { - const { flags } = makeFlags({ KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION: 'YES' }); - expect(flags.enabled('micro_compaction')).toBe(true); + const { flags } = makeFlags({ KIMI_CODE_EXPERIMENTAL_EXAMPLE_FLAG: 'YES' }); + expect(flags.enabled('example_flag')).toBe(true); }); it('treats falsy env values case-insensitively', () => { - const { flags } = makeFlags({ KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION: 'off' }); - expect(flags.enabled('micro_compaction')).toBe(false); + const { flags } = makeFlags({ KIMI_CODE_EXPERIMENTAL_EXAMPLE_FLAG: 'off' }); + expect(flags.enabled('example_flag')).toBe(false); }); it('reads only the env name declared in the registry', () => { const { flags } = makeFlags({ KIMI_CODE_EXPERIMENTAL_UNKNOWN: 'false' }); - expect(flags.enabled('micro_compaction')).toBe(true); + expect(flags.enabled('example_flag')).toBe(true); }); it('ignores garbage env values', () => { - const { flags } = makeFlags({ KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION: 'maybe' }); - expect(flags.enabled('micro_compaction')).toBe(true); + const { flags } = makeFlags({ KIMI_CODE_EXPERIMENTAL_EXAMPLE_FLAG: 'maybe' }); + expect(flags.enabled('example_flag')).toBe(true); }); it('ignores obsolete config ids outside the registry', async () => { const { config, flags } = makeFlags(); await config.set(EXPERIMENTAL_SECTION, { obsolete_flag: false, - micro_compaction: false, + example_flag: false, }); - expect(flags.snapshot()).toEqual({ micro_compaction: false }); + expect(flags.snapshot()).toEqual({ example_flag: false }); expect(flags.explain('obsolete_flag')).toBeUndefined(); }); }); diff --git a/packages/agent-core-v2/test/fullCompaction/full.test.ts b/packages/agent-core-v2/test/fullCompaction/full.test.ts index 953a7157a..d0ad82815 100644 --- a/packages/agent-core-v2/test/fullCompaction/full.test.ts +++ b/packages/agent-core-v2/test/fullCompaction/full.test.ts @@ -15,15 +15,12 @@ import { import { makeHookRunner } from '../externalHooks/runner-stub'; import type { IExternalHooksRunnerService } from '#/app/externalHooksRunner/externalHooksRunner'; import { MASTER_ENV } from '#/app/flag/flagService'; -import { microCompactionFlag } from '#/agent/microCompaction/flag'; -import { COMPACTION_SUMMARY_PREFIX } from '#/agent/contextMemory/compactionHandoff'; import { estimateTokensForMessages } from '#/_base/utils/tokens'; import { recordingTelemetry, type TelemetryRecord } from '../telemetry/stubs'; import type { TestAgentContext, TestAgentOptions, TestAgentServiceOverride } from '../harness'; import { appServices, createCommandRunner, execEnvServices, sessionServices, testAgent } from '../harness'; import { IAgentFullCompactionService, - IAgentMicroCompactionService, IOAuthService, IAgentProfileService, ISessionTodoService, @@ -46,7 +43,6 @@ const CATALOGUED_MODEL_CAPABILITIES = { tool_use: true, max_context_tokens: 256_000, } as const; -const MICRO_COMPACTION_FLAG_ENV = getMicroCompactionFlagEnv(); const SNAPSHOT_VISIBLE_TOOLS = [ 'Agent', 'AgentSwarm', @@ -352,103 +348,6 @@ describe('FullCompaction', () => { ).toBe(false); }); - it('strips dynamic tool protocol context from the summary request', async () => { - const ctx = testAgent(); - ctx.configure({ - provider: CATALOGUED_PROVIDER, - modelCapabilities: CATALOGUED_MODEL_CAPABILITIES, - tools: SNAPSHOT_VISIBLE_TOOLS, - }); - ctx.appendExchange(1, 'old user one', 'old assistant one', 20); - await ctx.dispatch({ - type: 'context.splice', - start: ctx.context.get().length, - deleteCount: 0, - messages: [ - { - role: 'system', - content: [], - toolCalls: [], - tools: [ - { - name: 'LoadedDynamicTool', - description: 'schema text should not be summarized', - parameters: { type: 'object', properties: { query: { type: 'string' } } }, - }, - ], - origin: { kind: 'injection', variant: 'dynamic_tool_schema' }, - }, - { - role: 'user', - content: [{ type: 'text', text: '\nLoadedDynamicTool\n' }], - toolCalls: [], - origin: { kind: 'system_trigger', name: 'loadable-tools' }, - }, - ], - }); - ctx.appendExchange(2, 'recent user two', 'recent assistant two', 80); - const compacted = ctx.once('full_compaction.complete'); - const completed = ctx.once('compaction.completed'); - - ctx.mockNextResponse({ type: 'text', text: 'Summary without protocol context.' }); - await ctx.rpc.beginCompaction({}); - await compacted; - await completed; - - const [compactionCall] = ctx.llmCalls; - const inputText = compactionCall?.history.map(messageText).join('\n') ?? ''; - expect(compactionCall?.history.some((message) => message.tools !== undefined)).toBe(false); - expect(inputText).not.toContain('LoadedDynamicTool'); - expect(inputText).not.toContain(''); - expect(inputText).not.toContain('schema text should not be summarized'); - expect(inputText).toContain('old user one'); - expect(inputText).toContain('recent user two'); - await ctx.expectResumeMatches(); - }); - - it('micro-compacts old tool results before sending the summary request', async () => { - vi.useFakeTimers(); - enableMicroCompactionFlag(); - const ctx = testAgent({ - microCompaction: { - config: { - keepRecentMessages: 2, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * 60 * 1000, - minContextUsageRatio: 0, - - } - }, - }); - ctx.configure({ - provider: CATALOGUED_PROVIDER, - modelCapabilities: CATALOGUED_MODEL_CAPABILITIES, - tools: SNAPSHOT_VISIBLE_TOOLS, - }); - - // Force-construct the micro-compaction service so it registers its - // onSpliced observer before the tool exchanges are appended (otherwise the - // lazily-instantiated service never records the assistant cache anchor that - // `detect()` needs). - (ctx.get(IAgentMicroCompactionService) as any).compact([]); - - vi.setSystemTime(0); - ctx.appendToolExchange(); - ctx.appendToolExchange(); - - vi.setSystemTime(61 * 60 * 1000); - - (ctx.get(IAgentMicroCompactionService) as any).detect(); - const compacted = ctx.once('full_compaction.complete'); - ctx.mockNextResponse({ type: 'text', text: 'Compacted summary.' }); - await ctx.rpc.beginCompaction({ instruction: 'Summarize tool exchanges.' }); - await compacted; - - const [compactionCall] = ctx.llmCalls; - expect(messageText(compactionCall?.history[2])).toBe('[Old tool result content cleared]'); - expect(messageText(compactionCall?.history[5])).toBe('lookup result'); - }); - it('force-refreshes OAuth credentials on compaction 401 and falls back to login_required when replay 401', async () => { const tokenCalls: Array = []; const authKeys: string[] = []; @@ -2516,15 +2415,6 @@ afterEach(() => { vi.unstubAllEnvs(); }); -function enableMicroCompactionFlag(): void { - vi.stubEnv(MASTER_ENV, '0'); - vi.stubEnv(MICRO_COMPACTION_FLAG_ENV, '1'); -} - -function getMicroCompactionFlagEnv(): string { - return microCompactionFlag.env; -} - function deferred() { let resolve!: (value: T | PromiseLike) => void; let reject!: (reason?: unknown) => void; diff --git a/packages/agent-core-v2/test/harness/agent.ts b/packages/agent-core-v2/test/harness/agent.ts index c1af39558..8502843cb 100644 --- a/packages/agent-core-v2/test/harness/agent.ts +++ b/packages/agent-core-v2/test/harness/agent.ts @@ -22,7 +22,6 @@ import { CronTaskPersistenceService } from '#/app/cron/cronTaskPersistenceServic import { IAgentGoalService } from '#/agent/goal/goal'; import { AgentGoalService } from '#/agent/goal/goalService'; import type { McpServiceOptions } from '#/agent/mcp/mcp'; -import { MICRO_COMPACTION_SECTION, type MicroCompactionConfig } from '#/agent/microCompaction/configSection'; import type { PermissionMode } from '#/agent/permissionPolicy/types'; import type { PermissionRule } from '#/agent/permissionRules/permissionRules'; import { IAgentPlanService } from '#/agent/plan/plan'; @@ -80,7 +79,6 @@ import { IAgentLLMRequesterService, ILogService, IAgentMcpService, - IAgentMicroCompactionService, IAgentPermissionGate, IAgentPermissionModeService, IAgentPermissionRulesService, @@ -101,7 +99,6 @@ import { AgentLLMRequesterService, LifecycleScope, AgentMcpService, - AgentMicroCompactionService, AgentPermissionGate, AgentPermissionRulesService, AgentProfileService, @@ -291,11 +288,6 @@ export interface TestAgentOptions { readonly generate?: GenerateFn | undefined; readonly telemetry?: ITelemetryService | undefined; readonly persistence?: WireRecordPersistence | undefined; - readonly microCompaction?: - | { - readonly config?: Partial | undefined; - } - | undefined; readonly hookEngine?: | Pick | undefined; @@ -594,15 +586,6 @@ const noopHookRunner: IExternalHooksRunnerService = { fireAndForgetTrigger: async () => [], }; -export function microCompactionServices(options: { - readonly config?: Partial; -}): TestAgentServiceOverride { - return configServices(() => ({ - ...emptyConfig(), - [MICRO_COMPACTION_SECTION]: options.config, - })); -} - export function permissionModeServices(mode: PermissionMode): TestAgentServiceOverride { return agentService(IAgentPermissionModeService, createPermissionModeService(mode)); } @@ -725,17 +708,6 @@ function mergeTestAgentOptions(base: TestAgentOptions, next: TestAgentOptions): return { ...base, ...next, - microCompaction: - base.microCompaction === undefined && next.microCompaction === undefined - ? undefined - : { - ...base.microCompaction, - ...next.microCompaction, - config: { - ...base.microCompaction?.config, - ...next.microCompaction?.config, - }, - }, initialConfig: { ...base.initialConfig, ...next.initialConfig, @@ -1085,10 +1057,6 @@ export class AgentTestContext { IAgentExternalHooksService, new SyncDescriptor(AgentExternalHooksService), ); - reg.defineDescriptor( - IAgentMicroCompactionService, - new SyncDescriptor(AgentMicroCompactionService), - ); reg.defineDescriptor( IAgentFullCompactionService, new SyncDescriptor(AgentFullCompactionService), @@ -1224,8 +1192,6 @@ export class AgentTestContext { const swarm = this.get(IAgentSwarmService); context.get(); - const microCompaction = this.get(IAgentMicroCompactionService); - void microCompaction; void swarm.isActive; contextSize.get(); usage.status(); @@ -2123,10 +2089,6 @@ function applyTestAgentOptionsToConfig(config: KimiConfig, options: TestAgentOpt ...config.models, ...initialConfig.models, }, - [MICRO_COMPACTION_SECTION]: - options.microCompaction?.config ?? - initialConfig[MICRO_COMPACTION_SECTION] ?? - config[MICRO_COMPACTION_SECTION], }; } diff --git a/packages/agent-core-v2/test/harness/index.ts b/packages/agent-core-v2/test/harness/index.ts index 3419936c5..1878b8829 100644 --- a/packages/agent-core-v2/test/harness/index.ts +++ b/packages/agent-core-v2/test/harness/index.ts @@ -17,7 +17,6 @@ export { llmGenerateServices, logServices, mcpServices, - microCompactionServices, modelProviderOptionServices, modelProviderServices, permissionModeServices, diff --git a/packages/agent-core-v2/test/microCompaction/micro.test.ts b/packages/agent-core-v2/test/microCompaction/micro.test.ts deleted file mode 100644 index b456a3a2f..000000000 --- a/packages/agent-core-v2/test/microCompaction/micro.test.ts +++ /dev/null @@ -1,996 +0,0 @@ -import type { ContentPart } from '#/app/llmProtocol/message'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - -import { estimateTokensForMessages } from '#/_base/utils/tokens'; -import { microCompactionFlag } from '#/agent/microCompaction/flag'; -import { IFlagService } from '#/app/flag/flag'; -import { MASTER_ENV } from '#/app/flag/flagService'; -import { - AGENT_WIRE_PROTOCOL_VERSION, - IAgentMicroCompactionService, - type PersistedWireRecord, -} from '#/index'; - -import { InMemoryWireRecordPersistence, testAgent, type TestAgentContext } from '../harness'; -import { recordingTelemetry, type TelemetryRecord } from '../telemetry/stubs'; - -const CATALOGUED_PROVIDER = { - type: 'kimi', - apiKey: 'test-key', - model: 'kimi-code', -} as const; -const CATALOGUED_MODEL_CAPABILITIES = { - image_in: true, - video_in: true, - audio_in: false, - thinking: true, - tool_use: true, - max_context_tokens: 256_000, -} as const; - -const MINUTE = 60 * 1000; -const DEFAULT_MARKER = '[Old tool result content cleared]'; -const MICRO_COMPACTION_FLAG_ENV = getMicroCompactionFlagEnv(); - -describe('MicroCompaction', () => { - beforeEach(() => { - vi.stubEnv(MASTER_ENV, '0'); - vi.stubEnv(MICRO_COMPACTION_FLAG_ENV, '1'); - }); - - it('defaults the micro_compaction flag on', () => { - vi.unstubAllEnvs(); - vi.stubEnv(MASTER_ENV, '0'); - - const ctx = testAgent(); - - expect(ctx.get(IFlagService).enabled('micro_compaction')).toBe(true); - }); - - it('truncates old tool results after cache miss', () => { - vi.useFakeTimers(); - const ctx = testAgent({ - microCompaction: { - config: { - keepRecentMessages: 4, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * 60 * 1000, - minContextUsageRatio: 0, - }, - }, - }); - - vi.setSystemTime(0); - ctx.appendToolExchange(); - ctx.appendToolExchange(); - ctx.appendToolExchange(); - - expect(ctx.project()).toHaveLength(9); - - vi.setSystemTime(61 * 60 * 1000); - - (ctx.get(IAgentMicroCompactionService) as any).detect(); - const messages = ctx.project(); - expect(messages[2]).toMatchObject({ - role: 'tool', - content: [{ type: 'text', text: DEFAULT_MARKER }], - }); - expect(messages[5]).toMatchObject({ - role: 'tool', - content: [{ type: 'text', text: 'lookup result' }], - }); - expect(messages[8]).toMatchObject({ - role: 'tool', - content: [{ type: 'text', text: 'lookup result' }], - }); - }); - - it('does nothing before cache miss threshold', () => { - vi.useFakeTimers(); - const ctx = testAgent({ - microCompaction: { - config: { - keepRecentMessages: 4, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * 60 * 1000, - }, - }, - }); - - vi.setSystemTime(0); - ctx.appendToolExchange(); - ctx.appendToolExchange(); - ctx.appendToolExchange(); - - vi.setSystemTime(30 * 60 * 1000); - - const messages = ctx.project(); - expect(hasMarker(messages)).toBe(false); - }); - - it('persists cutoff across calls until cache miss resets it', () => { - vi.useFakeTimers(); - const ctx = testAgent({ - microCompaction: { - config: { - keepRecentMessages: 2, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * 60 * 1000, - minContextUsageRatio: 0, - }, - }, - }); - - vi.setSystemTime(0); - ctx.appendToolExchange(); - ctx.appendToolExchange(); - - vi.setSystemTime(61 * 60 * 1000); - - (ctx.get(IAgentMicroCompactionService) as any).detect(); - const first = ctx.project(); - expect(first[2]).toMatchObject({ - role: 'tool', - content: [{ type: 'text', text: DEFAULT_MARKER }], - }); - - vi.setSystemTime(62 * 60 * 1000); - - (ctx.get(IAgentMicroCompactionService) as any).detect(); - const second = ctx.project(); - expect(second[2]).toMatchObject({ - role: 'tool', - content: [{ type: 'text', text: DEFAULT_MARKER }], - }); - }); - - it('clears cutoff on reset', () => { - vi.useFakeTimers(); - const ctx = testAgent({ - microCompaction: { - config: { - keepRecentMessages: 4, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * 60 * 1000, - }, - }, - }); - - vi.setSystemTime(0); - ctx.appendToolExchange(); - ctx.appendToolExchange(); - - vi.setSystemTime(61 * 60 * 1000); - - (ctx.get(IAgentMicroCompactionService) as any).reset(); - - const messages = ctx.project(); - expect(hasMarker(messages)).toBe(false); - }); - - it('skips tool results below minContentTokens', () => { - vi.useFakeTimers(); - const ctx = testAgent({ - microCompaction: { - config: { - keepRecentMessages: 2, - minContentTokens: 100, - cacheMissedThresholdMs: 60 * 60 * 1000, - }, - }, - }); - - vi.setSystemTime(0); - ctx.appendToolExchange(); - ctx.appendToolExchange(); - - vi.setSystemTime(61 * 60 * 1000); - - const messages = ctx.project(); - expect(hasMarker(messages)).toBe(false); - }); - - it('skips non-tool messages', () => { - vi.useFakeTimers(); - const ctx = testAgent({ - microCompaction: { - config: { - keepRecentMessages: 2, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * 60 * 1000, - }, - }, - }); - - vi.setSystemTime(0); - ctx.appendExchange(1, 'user one', 'assistant one', 10); - ctx.appendExchange(2, 'user two', 'assistant two', 10); - ctx.appendExchange(3, 'user three', 'assistant three', 10); - - vi.setSystemTime(61 * 60 * 1000); - - const messages = ctx.project(); - expect(messages.every((m) => m.role === 'user' || m.role === 'assistant')).toBe(true); - expect(hasMarker(messages)).toBe(false); - }); - - it('clears cutoff on context clear', () => { - vi.useFakeTimers(); - const ctx = testAgent({ - microCompaction: { - config: { - keepRecentMessages: 2, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * 60 * 1000, - }, - }, - }); - - vi.setSystemTime(0); - ctx.appendToolExchange(); - ctx.appendToolExchange(); - - vi.setSystemTime(61 * 60 * 1000); - - ctx.clearContext(); - - expect(ctx.project()).toHaveLength(0); - expect((ctx.get(IAgentMicroCompactionService) as any).lastAssistantAt).toBeNull(); - }); - - it('sends truncated old tool results to the next model request without mutating history', async () => { - vi.useFakeTimers(); - const ctx = testAgent({ - microCompaction: { - config: { - keepRecentMessages: 4, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * MINUTE, - minContextUsageRatio: 0, - }, - }, - }); - ctx.configure({ - provider: CATALOGUED_PROVIDER, - modelCapabilities: CATALOGUED_MODEL_CAPABILITIES, - }); - - vi.setSystemTime(0); - appendMicroToolExchange(ctx, 1, { output: 'old result one' }); - appendMicroToolExchange(ctx, 2, { output: 'middle result two' }); - appendMicroToolExchange(ctx, 3, { output: 'recent result three' }); - - vi.setSystemTime(61 * MINUTE); - - ctx.mockNextResponse({ type: 'text', text: 'done after micro compaction' }); - await ctx.rpc.prompt({ input: [{ type: 'text', text: 'continue' }] }); - await ctx.untilTurnEnd(); - - const call = ctx.llmCalls.at(-1); - expect(textOf(call?.history[2])).toBe(DEFAULT_MARKER); - expect(textOf(call?.history[5])).toBe(DEFAULT_MARKER); - expect(textOf(call?.history[8])).toBe('recent result three'); - - expect(textOf(ctx.context.get()[2])).toBe('old result one'); - expect(textOf(ctx.context.get()[5])).toBe('middle result two'); - expect(textOf(ctx.context.get()[8])).toBe('recent result three'); - await ctx.expectResumeMatches(); - }); - - it('restores lastAssistantAt from record time before applying cache-miss rules', async () => { - vi.useFakeTimers(); - const assistantRecordTime = 2_000; - const ctx = testAgent({ - microCompaction: { - config: { - keepRecentMessages: 0, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * MINUTE, - minContextUsageRatio: 0, - }, - }, - persistence: new InMemoryWireRecordPersistence( - resumeToolExchangeRecords(assistantRecordTime), - ), - }); - - vi.setSystemTime(999_999); - await ctx.restorePersisted(); - - expect((ctx.get(IAgentMicroCompactionService) as any).lastAssistantAt).toBe( - assistantRecordTime, - ); - - vi.setSystemTime(assistantRecordTime + 30 * MINUTE); - expect(hasMarker(ctx.project())).toBe(false); - - vi.setSystemTime(assistantRecordTime + 61 * MINUTE); - (ctx.get(IAgentMicroCompactionService) as any).detect(); - expect(toolTexts(ctx.project())).toEqual([DEFAULT_MARKER]); - }); - - it('preserves the restored cutoff when resuming before the next cache miss', async () => { - vi.useFakeTimers(); - const persistence = new InMemoryWireRecordPersistence(); - const config = { - keepRecentMessages: 2, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * MINUTE, - minContextUsageRatio: 0, - }; - const ctx = testAgent({ - microCompaction: { config }, - persistence, - }); - - vi.setSystemTime(0); - appendMicroToolExchange(ctx, 1, { output: 'result one' }); - appendMicroToolExchange(ctx, 2, { output: 'result two' }); - - vi.setSystemTime(61 * MINUTE); - (ctx.get(IAgentMicroCompactionService) as any).detect(); - expect(toolTexts(ctx.project())).toEqual([DEFAULT_MARKER, 'result two']); - await ctx.wireRecord.flush(); - expect(lastMicroCompactionCutoff(persistence.records)).toBe(4); - - vi.setSystemTime(62 * MINUTE); - appendMicroToolExchange(ctx, 3, { output: 'result three' }); - await ctx.wireRecord.flush(); - - const resumed = testAgent({ - microCompaction: { config }, - persistence: new InMemoryWireRecordPersistence(cloneRecords(persistence.records)), - }); - - vi.setSystemTime(63 * MINUTE); - await resumed.restorePersisted(); - - expect((resumed.get(IAgentMicroCompactionService) as any).lastAssistantAt).toBe(62 * MINUTE); - expect(toolTexts(resumed.project())).toEqual([DEFAULT_MARKER, 'result two', 'result three']); - }); - - it('recomputes the restored cutoff when resuming after the cache-miss threshold', async () => { - vi.useFakeTimers(); - const persistence = new InMemoryWireRecordPersistence(); - const config = { - keepRecentMessages: 2, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * MINUTE, - minContextUsageRatio: 0, - }; - const ctx = testAgent({ - microCompaction: { config }, - persistence, - }); - - vi.setSystemTime(0); - appendMicroToolExchange(ctx, 1, { output: 'result one' }); - appendMicroToolExchange(ctx, 2, { output: 'result two' }); - - vi.setSystemTime(61 * MINUTE); - (ctx.get(IAgentMicroCompactionService) as any).detect(); - expect(toolTexts(ctx.project())).toEqual([DEFAULT_MARKER, 'result two']); - await ctx.wireRecord.flush(); - expect(lastMicroCompactionCutoff(persistence.records)).toBe(4); - - vi.setSystemTime(62 * MINUTE); - appendMicroToolExchange(ctx, 3, { output: 'result three' }); - await ctx.wireRecord.flush(); - - const resumedPersistence = new InMemoryWireRecordPersistence(cloneRecords(persistence.records)); - const resumed = testAgent({ - microCompaction: { config }, - persistence: resumedPersistence, - }); - - vi.setSystemTime(123 * MINUTE); - await resumed.restorePersisted(); - - expect((resumed.get(IAgentMicroCompactionService) as any).lastAssistantAt).toBe(62 * MINUTE); - (resumed.get(IAgentMicroCompactionService) as any).detect(); - expect(toolTexts(resumed.project())).toEqual([DEFAULT_MARKER, DEFAULT_MARKER, 'result three']); - await resumed.wireRecord.flush(); - expect(lastMicroCompactionCutoff(resumedPersistence.records)).toBe(7); - }); - - it('keeps an old cutoff while cache is warm and advances it on the next miss', () => { - vi.useFakeTimers(); - const ctx = testAgent({ - microCompaction: { - config: { - keepRecentMessages: 2, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * MINUTE, - minContextUsageRatio: 0, - }, - }, - }); - - vi.setSystemTime(0); - appendMicroToolExchange(ctx, 1, { output: 'result one' }); - appendMicroToolExchange(ctx, 2, { output: 'result two' }); - - vi.setSystemTime(61 * MINUTE); - (ctx.get(IAgentMicroCompactionService) as any).detect(); - expect(toolTexts(ctx.project())).toEqual([DEFAULT_MARKER, 'result two']); - - vi.setSystemTime(62 * MINUTE); - appendMicroToolExchange(ctx, 3, { output: 'result three' }); - - vi.setSystemTime(63 * MINUTE); - (ctx.get(IAgentMicroCompactionService) as any).detect(); - expect(toolTexts(ctx.project())).toEqual([DEFAULT_MARKER, 'result two', 'result three']); - - vi.setSystemTime(123 * MINUTE); - (ctx.get(IAgentMicroCompactionService) as any).detect(); - expect(toolTexts(ctx.project())).toEqual([DEFAULT_MARKER, DEFAULT_MARKER, 'result three']); - }); - - it('clamps cutoff when undo shortens the context', () => { - vi.useFakeTimers(); - const ctx = testAgent({ - microCompaction: { - config: { - keepRecentMessages: 2, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * MINUTE, - minContextUsageRatio: 0, - }, - }, - }); - - vi.setSystemTime(0); - appendMicroToolExchange(ctx, 1, { output: 'result one' }); - appendMicroToolExchange(ctx, 2, { output: 'result two' }); - appendMicroToolExchange(ctx, 3, { output: 'result three' }); - - vi.setSystemTime(61 * MINUTE); - (ctx.get(IAgentMicroCompactionService) as any).detect(); - expect(toolTexts(ctx.project())).toEqual([DEFAULT_MARKER, DEFAULT_MARKER, 'result three']); - - ctx.undoHistory(2); - appendMicroToolExchange(ctx, 4, { output: 'result four' }); - - expect(toolTexts(ctx.project())).toEqual([DEFAULT_MARKER, 'result four']); - }); - - it('tracks telemetry when a cache miss advances the micro_compaction cutoff', () => { - vi.useFakeTimers(); - const records: TelemetryRecord[] = []; - const microCompaction = { - keepRecentMessages: 2, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * MINUTE, - minContextUsageRatio: 0, - }; - const ctx = testAgent({ - telemetry: recordingTelemetry(records), - microCompaction: { config: microCompaction }, - }); - - vi.setSystemTime(0); - appendMicroToolExchange(ctx, 1, { output: 'result one '.repeat(20) }); - appendMicroToolExchange(ctx, 2, { output: 'result two '.repeat(20) }); - appendMicroToolExchange(ctx, 3, { output: 'result three' }); - - vi.setSystemTime(61 * MINUTE); - (ctx.get(IAgentMicroCompactionService) as any).detect(); - expect(toolTexts(ctx.project())).toEqual([DEFAULT_MARKER, DEFAULT_MARKER, 'result three']); - - const event = singleTelemetryEvent(records, 'micro_compaction_finished'); - expect(event.properties).toMatchObject({ - keep_recent_messages: microCompaction.keepRecentMessages, - min_content_tokens: microCompaction.minContentTokens, - cache_missed_threshold_ms: microCompaction.cacheMissedThresholdMs, - min_context_usage_ratio: microCompaction.minContextUsageRatio, - truncated_marker: DEFAULT_MARKER, - previous_cutoff: 0, - cutoff: 7, - message_count: 9, - cache_age_ms: 61 * MINUTE, - truncated_tool_result_count: 2, - truncated_tool_result_tokens_before: expect.any(Number), - truncated_tool_result_tokens_after: expect.any(Number), - tokens_before: expect.any(Number), - tokens_after: expect.any(Number), - thinking_level: 'off', - }); - expect(numberProperty(event, 'truncated_tool_result_tokens_before')).toBeGreaterThan( - numberProperty(event, 'truncated_tool_result_tokens_after'), - ); - expect(numberProperty(event, 'tokens_before')).toBeGreaterThan( - numberProperty(event, 'tokens_after'), - ); - - expect(ctx.project()).toHaveLength(9); - expect(records.filter((record) => record.event === 'micro_compaction_finished')).toHaveLength( - 1, - ); - }); - - it('reports context token deltas from the previously compacted projection', () => { - vi.useFakeTimers(); - const records: TelemetryRecord[] = []; - const microCompaction = { - keepRecentMessages: 2, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * MINUTE, - minContextUsageRatio: 0, - }; - const ctx = testAgent({ - telemetry: recordingTelemetry(records), - microCompaction: { config: microCompaction }, - }); - - vi.setSystemTime(0); - appendMicroToolExchange(ctx, 1, { output: 'result one '.repeat(20) }); - appendMicroToolExchange(ctx, 2, { output: 'result two '.repeat(20) }); - - vi.setSystemTime(61 * MINUTE); - (ctx.get(IAgentMicroCompactionService) as any).detect(); - expect(toolTexts(ctx.project())).toEqual([DEFAULT_MARKER, 'result two '.repeat(20)]); - - vi.setSystemTime(62 * MINUTE); - appendMicroToolExchange(ctx, 3, { output: 'result three' }); - const expectedContextTokensBefore = estimateTokensForMessages(ctx.project()); - - vi.setSystemTime(123 * MINUTE); - (ctx.get(IAgentMicroCompactionService) as any).detect(); - - const events = records.filter((record) => record.event === 'micro_compaction_finished'); - expect(events).toHaveLength(2); - const secondEvent = events[1]!; - expect(secondEvent.properties).toMatchObject({ - previous_cutoff: 4, - cutoff: 7, - truncated_tool_result_count: 2, - tokens_before: expectedContextTokensBefore, - tokens_after: estimateTokensForMessages(ctx.project()), - }); - }); - - it('leaves context unchanged when the micro_compaction flag is disabled', () => { - vi.stubEnv(MICRO_COMPACTION_FLAG_ENV, '0'); - vi.useFakeTimers(); - const persistence = new InMemoryWireRecordPersistence(); - const ctx = testAgent({ - microCompaction: { - config: { - keepRecentMessages: 0, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * MINUTE, - minContextUsageRatio: 0, - }, - }, - persistence, - }); - - vi.setSystemTime(0); - appendMicroToolExchange(ctx, 1, { output: 'result one' }); - - vi.setSystemTime(61 * MINUTE); - (ctx.get(IAgentMicroCompactionService) as any).detect(); - - expect(toolTexts(ctx.project())).toEqual(['result one']); - expect(lastMicroCompactionCutoff(persistence.records)).toBeUndefined(); - }); - - it('uses the custom marker at the minContentTokens boundary', () => { - vi.useFakeTimers(); - const marker = '[tool output removed for test]'; - const ctx = testAgent({ - microCompaction: { - config: { - keepRecentMessages: 0, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * MINUTE, - truncatedMarker: marker, - minContextUsageRatio: 0, - }, - }, - }); - - vi.setSystemTime(0); - appendMicroToolExchange(ctx, 1, { output: 'abcd' }); - - vi.setSystemTime(61 * MINUTE); - - (ctx.get(IAgentMicroCompactionService) as any).detect(); - expect(toolTexts(ctx.project())).toEqual([marker]); - expect(textOf(ctx.context.get()[2])).toBe('abcd'); - }); - - it('keeps raw pending token accounting even when projection truncates tool output', () => { - vi.useFakeTimers(); - const ctx = testAgent({ - microCompaction: { - config: { - keepRecentMessages: 0, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * MINUTE, - minContextUsageRatio: 0, - }, - }, - }); - ctx.configure(); - - vi.setSystemTime(0); - appendMicroToolExchange(ctx, 1, { - output: 'x'.repeat(400), - usageTokens: 50, - }); - - vi.setSystemTime(61 * MINUTE); - - (ctx.get(IAgentMicroCompactionService) as any).detect(); - const rawPending = ctx.context.get().slice(-1); - const projectedPending = (ctx.get(IAgentMicroCompactionService) as any).compact(rawPending); - expect(textOf(projectedPending[0])).toBe(DEFAULT_MARKER); - expect(ctx.contextSize.get().size).toBe( - ctx.contextSize.get().measured + estimateTokensForMessages(rawPending), - ); - expect(ctx.contextSize.get().size).toBeGreaterThan( - ctx.contextSize.get().measured + estimateTokensForMessages(projectedPending), - ); - }); - - it('replaces rich error tool content while preserving context metadata before projection', () => { - vi.useFakeTimers(); - const ctx = testAgent({ - microCompaction: { - config: { - keepRecentMessages: 0, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * MINUTE, - minContextUsageRatio: 0, - }, - }, - }); - - vi.setSystemTime(0); - appendMicroToolExchange(ctx, 1, { - output: [ - { type: 'text', text: 'large rich output' }, - { type: 'video_url', videoUrl: { url: 'ms://video-1', id: 'video-1' } }, - ], - isError: true, - }); - - vi.setSystemTime(61 * MINUTE); - - (ctx.get(IAgentMicroCompactionService) as any).detect(); - const compacted = (ctx.get(IAgentMicroCompactionService) as any).compact(ctx.context.get()); - const tool = compacted.find((message: any) => message.role === 'tool'); - expect(tool).toMatchObject({ - role: 'tool', - toolCallId: 'call_micro_1', - isError: true, - content: [{ type: 'text', text: DEFAULT_MARKER }], - }); - expect(tool?.content).toHaveLength(1); - }); - - it('does not truncate tool-shaped messages without a toolCallId', () => { - vi.useFakeTimers(); - const ctx = testAgent({ - microCompaction: { - config: { - keepRecentMessages: 0, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * MINUTE, - }, - }, - }); - - vi.setSystemTime(0); - ctx.context.splice(ctx.context.get().length, 0, [ - { - role: 'assistant', - content: [{ type: 'text', text: 'assistant anchor' }], - toolCalls: [], - }, - ]); - - vi.setSystemTime(61 * MINUTE); - ctx.context.splice(ctx.context.get().length, 0, [ - { - role: 'tool', - content: [{ type: 'text', text: 'orphan tool-like output' }], - toolCalls: [], - }, - ]); - - (ctx.get(IAgentMicroCompactionService) as any).detect(); - const compacted = (ctx.get(IAgentMicroCompactionService) as any).compact(ctx.context.get()); - expect(toolTexts(compacted)).toEqual(['orphan tool-like output']); - }); - - it('clears cutoff on full compaction', async () => { - vi.useFakeTimers(); - const ctx = testAgent({ - microCompaction: { - config: { - keepRecentMessages: 2, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * 60 * 1000, - }, - }, - }); - ctx.configure({ - provider: CATALOGUED_PROVIDER, - modelCapabilities: CATALOGUED_MODEL_CAPABILITIES, - }); - - vi.setSystemTime(0); - ctx.appendExchange(1, 'old user', 'old assistant', 20); - ctx.appendExchange(2, 'recent user', 'recent assistant', 80); - - vi.setSystemTime(61 * 60 * 1000); - - const compacted = ctx.once('full_compaction.complete'); - ctx.mockNextResponse({ type: 'text', text: 'Summary.' }); - await ctx.rpc.beginCompaction({}); - await compacted; - - expect(ctx.project()).toHaveLength(1); - expect(ctx.project()[0]).toMatchObject({ - role: 'assistant', - content: [{ type: 'text', text: 'Summary.' }], - }); - }); - - it('does not apply when context usage is below minContextUsageRatio', () => { - vi.useFakeTimers(); - const ctx = testAgent({ - microCompaction: { - config: { - keepRecentMessages: 0, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * MINUTE, - minContextUsageRatio: 0.9, - }, - }, - }); - ctx.configure({ - provider: CATALOGUED_PROVIDER, - modelCapabilities: CATALOGUED_MODEL_CAPABILITIES, - }); - - vi.setSystemTime(0); - appendMicroToolExchange(ctx, 1, { output: 'result one' }); - - vi.setSystemTime(61 * MINUTE); - - const messages = ctx.project(); - expect(hasMarker(messages)).toBe(false); - }); - - it('applies when context usage is above minContextUsageRatio', () => { - vi.useFakeTimers(); - const ctx = testAgent({ - microCompaction: { - config: { - keepRecentMessages: 0, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * MINUTE, - minContextUsageRatio: 0.5, - }, - }, - }); - ctx.configure({ - provider: CATALOGUED_PROVIDER, - modelCapabilities: { - image_in: true, - video_in: true, - audio_in: false, - thinking: true, - tool_use: true, - max_context_tokens: 100, - }, - }); - - vi.setSystemTime(0); - appendMicroToolExchange(ctx, 1, { output: 'x'.repeat(300) }); - - vi.setSystemTime(61 * MINUTE); - - (ctx.get(IAgentMicroCompactionService) as any).detect(); - const messages = ctx.project(); - expect(hasMarker(messages)).toBe(true); - }); - - it('does not truncate when messages are fewer than keepRecentMessages', () => { - vi.useFakeTimers(); - const ctx = testAgent({ - microCompaction: { - config: { - keepRecentMessages: 20, - minContentTokens: 1, - cacheMissedThresholdMs: 60 * 60 * 1000, - }, - }, - }); - - vi.setSystemTime(0); - ctx.appendToolExchange(); - ctx.appendToolExchange(); - - vi.setSystemTime(61 * 60 * 1000); - - const messages = ctx.project(); - expect(hasMarker(messages)).toBe(false); - }); -}); - -afterEach(() => { - vi.useRealTimers(); - vi.unstubAllEnvs(); -}); - -interface MicroToolExchangeOptions { - readonly output?: string | ContentPart[] | undefined; - readonly isError?: boolean | undefined; - readonly usageTokens?: number | undefined; -} - -function appendMicroToolExchange( - ctx: TestAgentContext, - index: number, - options: MicroToolExchangeOptions = {}, -): void { - const toolCallId = `call_micro_${String(index)}`; - const output = options.output ?? `lookup result ${String(index)}`; - const usage = - options.usageTokens === undefined - ? undefined - : { - inputOther: options.usageTokens - 1, - output: 1, - inputCacheRead: 0, - inputCacheCreation: 0, - }; - - ctx.appendUserMessage([{ type: 'text', text: `lookup ${String(index)}` }]); - - ctx.context.splice(ctx.context.get().length, 0, [ - { - role: 'assistant', - content: [{ type: 'text', text: `calling Lookup ${String(index)}` }], - toolCalls: [ - { - type: 'function', - id: toolCallId, - name: 'Lookup', - arguments: JSON.stringify({ query: `item-${String(index)}` }), - }, - ], - }, - ]); - - if (usage !== undefined) { - ctx.contextSize.measured(ctx.context.get(), [], usage); - } - - ctx.context.splice(ctx.context.get().length, 0, [ - { - role: 'tool', - content: typeof output === 'string' ? [{ type: 'text', text: output }] : [...output], - toolCalls: [], - toolCallId, - isError: options.isError, - }, - ]); -} - -function resumeToolExchangeRecords(assistantRecordTime: number): PersistedWireRecord[] { - return [ - { - type: 'metadata', - protocol_version: AGENT_WIRE_PROTOCOL_VERSION, - created_at: 1, - }, - { - type: 'context.splice', - time: 1_000, - start: 0, - deleteCount: 0, - messages: [ - { - role: 'user', - content: [{ type: 'text', text: 'lookup from restored session' }], - toolCalls: [], - origin: { kind: 'user' }, - }, - ], - }, - { - type: 'turn.launch', - time: assistantRecordTime, - turnId: 0, - origin: { kind: 'user' }, - }, - { - type: 'context.splice', - time: assistantRecordTime, - start: 1, - deleteCount: 0, - messages: [ - { - role: 'assistant', - content: [{ type: 'text', text: 'calling restored Lookup' }], - toolCalls: [ - { - type: 'function', - id: 'resume_micro_call', - name: 'Lookup', - arguments: JSON.stringify({ query: 'restored' }), - }, - ], - }, - ], - }, - { - type: 'context.splice', - time: assistantRecordTime + 2, - start: 2, - deleteCount: 0, - messages: [ - { - role: 'tool', - content: [{ type: 'text', text: 'restored lookup result' }], - toolCalls: [], - toolCallId: 'resume_micro_call', - }, - ], - }, - ] as PersistedWireRecord[]; -} - -function cloneRecords(records: readonly PersistedWireRecord[]): PersistedWireRecord[] { - return records.map((record) => structuredClone(record)); -} - -function lastMicroCompactionCutoff(records: readonly PersistedWireRecord[]): number | undefined { - return (records.findLast((record) => record.type === 'micro_compaction.apply') as any)?.cutoff; -} - -function toolTexts( - messages: readonly { role: string; content?: readonly { type: string; text?: string }[] }[], -): string[] { - return messages.filter((message) => message.role === 'tool').map((message) => textOf(message)); -} - -function textOf( - message: { content?: readonly { type: string; text?: string }[] } | undefined, -): string { - return ( - message?.content - ?.map((part) => { - if (part.type === 'text') return part.text; - return ''; - }) - .join('') ?? '' - ); -} - -function hasMarker( - messages: readonly { role: string; content?: readonly { type: string; text?: string }[] }[], -): boolean { - return toolTexts(messages).includes(DEFAULT_MARKER); -} - -function getMicroCompactionFlagEnv(): string { - return microCompactionFlag.env; -} - -function singleTelemetryEvent(records: readonly TelemetryRecord[], event: string): TelemetryRecord { - const matches = records.filter((record) => record.event === event); - expect(matches).toHaveLength(1); - return matches[0]!; -} - -function numberProperty(record: TelemetryRecord, key: string): number { - const value = record.properties?.[key]; - expect(typeof value).toBe('number'); - return value as number; -}