diff --git a/README.md b/README.md index 34d280b6..596c4bee 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,14 @@ DCP uses its own config file: > // Additional tools to protect from pruning via commands (e.g., /dcp sweep) > "protectedTools": [], > }, +> // Manual mode: disables autonomous context management, +> // tools only run when explicitly triggered via /dcp commands +> "manualMode": { +> "enabled": false, +> // When true, automatic strategies (deduplication, supersedeWrites, purgeErrors) +> // still run even in manual mode +> "automaticStrategies": true, +> }, > // Protect from pruning for message turns past tool invocation > "turnProtection": { > "enabled": false, @@ -172,6 +180,10 @@ DCP provides a `/dcp` slash command: - `/dcp context` — Shows a breakdown of your current session's token usage by category (system, user, assistant, tools, etc.) and how much has been saved through pruning. - `/dcp stats` — Shows cumulative pruning statistics across all sessions. - `/dcp sweep` — Prunes all tools since the last user message. Accepts an optional count: `/dcp sweep 10` prunes the last 10 tools. Respects `commands.protectedTools`. +- `/dcp manual [on|off]` — Toggle manual mode or set explicit state. When on, the AI will not autonomously use context management tools. +- `/dcp prune [focus]` — Trigger a single prune tool execution. Optional focus text directs the AI's pruning decisions. +- `/dcp distill [focus]` — Trigger a single distill tool execution. Optional focus text directs what to distill. +- `/dcp compress [focus]` — Trigger a single compress tool execution. Optional focus text directs what range to compress. ### Protected Tools diff --git a/dcp.schema.json b/dcp.schema.json index 60160a49..49d8fbe1 100644 --- a/dcp.schema.json +++ b/dcp.schema.json @@ -56,6 +56,27 @@ "protectedTools": [] } }, + "manualMode": { + "type": "object", + "description": "Manual mode behavior for context management tools", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Start new sessions with manual mode enabled" + }, + "automaticStrategies": { + "type": "boolean", + "default": true, + "description": "When manual mode is enabled, keep automatic deduplication/supersede/purge strategies running" + } + }, + "default": { + "enabled": false, + "automaticStrategies": true + } + }, "turnProtection": { "type": "object", "description": "Protect recent tool outputs from being pruned", diff --git a/lib/commands/help.ts b/lib/commands/help.ts index 32b9a195..f456d933 100644 --- a/lib/commands/help.ts +++ b/lib/commands/help.ts @@ -4,6 +4,7 @@ */ import type { Logger } from "../logger" +import type { PluginConfig } from "../config" import type { SessionState, WithParts } from "../state" import { sendIgnoredMessage } from "../ui/notification" import { getCurrentParams } from "../strategies/utils" @@ -11,21 +12,49 @@ import { getCurrentParams } from "../strategies/utils" export interface HelpCommandContext { client: any state: SessionState + config: PluginConfig logger: Logger sessionId: string messages: WithParts[] } -function formatHelpMessage(): string { +const BASE_COMMANDS: [string, string][] = [ + ["/dcp context", "Show token usage breakdown for current session"], + ["/dcp stats", "Show DCP pruning statistics"], + ["/dcp sweep [n]", "Prune tools since last user message, or last n tools"], + ["/dcp manual [on|off]", "Toggle manual mode or set explicit state"], +] + +const TOOL_COMMANDS: Record = { + prune: ["/dcp prune [focus]", "Trigger manual prune tool execution"], + distill: ["/dcp distill [focus]", "Trigger manual distill tool execution"], + compress: ["/dcp compress [focus]", "Trigger manual compress tool execution"], +} + +function getVisibleCommands(config: PluginConfig): [string, string][] { + const commands = [...BASE_COMMANDS] + for (const tool of ["prune", "distill", "compress"] as const) { + if (config.tools[tool].permission !== "deny") { + commands.push(TOOL_COMMANDS[tool]) + } + } + return commands +} + +function formatHelpMessage(manualMode: boolean, config: PluginConfig): string { + const commands = getVisibleCommands(config) + const colWidth = Math.max(...commands.map(([cmd]) => cmd.length)) + 4 const lines: string[] = [] - lines.push("╭───────────────────────────────────────────────────────────╮") - lines.push("│ DCP Commands │") - lines.push("╰───────────────────────────────────────────────────────────╯") + lines.push("╭─────────────────────────────────────────────────────────────────────────╮") + lines.push("│ DCP Commands │") + lines.push("╰─────────────────────────────────────────────────────────────────────────╯") + lines.push("") + lines.push(` ${"Manual mode:".padEnd(colWidth)}${manualMode ? "ON" : "OFF"}`) lines.push("") - lines.push(" /dcp context Show token usage breakdown for current session") - lines.push(" /dcp stats Show DCP pruning statistics") - lines.push(" /dcp sweep [n] Prune tools since last user message, or last n tools") + for (const [cmd, desc] of commands) { + lines.push(` ${cmd.padEnd(colWidth)}${desc}`) + } lines.push("") return lines.join("\n") @@ -34,7 +63,8 @@ function formatHelpMessage(): string { export async function handleHelpCommand(ctx: HelpCommandContext): Promise { const { client, state, logger, sessionId, messages } = ctx - const message = formatHelpMessage() + const { config } = ctx + const message = formatHelpMessage(state.manualMode, config) const params = getCurrentParams(state, messages, logger) await sendIgnoredMessage(client, sessionId, message, params, logger) diff --git a/lib/commands/manual.ts b/lib/commands/manual.ts new file mode 100644 index 00000000..2c5c1815 --- /dev/null +++ b/lib/commands/manual.ts @@ -0,0 +1,131 @@ +/** + * DCP Manual mode command handler. + * Handles toggling manual mode and triggering individual tool executions. + * + * Usage: + * /dcp manual [on|off] - Toggle manual mode or set explicit state + * /dcp prune [focus] - Trigger manual prune execution + * /dcp distill [focus] - Trigger manual distill execution + * /dcp compress [focus] - Trigger manual compress execution + */ + +import type { Logger } from "../logger" +import type { SessionState, WithParts } from "../state" +import type { PluginConfig } from "../config" +import { sendIgnoredMessage } from "../ui/notification" +import { getCurrentParams } from "../strategies/utils" +import { syncToolCache } from "../state/tool-cache" +import { buildToolIdList } from "../messages/utils" +import { buildPrunableToolsList } from "../messages/inject" + +const MANUAL_MODE_ON = + "Manual mode is now ON. Use /dcp prune, /dcp distill, or /dcp compress to trigger context tools manually." + +const MANUAL_MODE_OFF = "Manual mode is now OFF." + +const NO_PRUNABLE_TOOLS = "No prunable tool outputs are currently available for manual triggering." + +const PRUNE_TRIGGER_PROMPT = [ + "", + "Manual mode trigger received. You must now use the prune tool exactly once.", + "Find the most significant set of prunable tool outputs to remove safely.", + "Follow prune policy and avoid pruning outputs that may be needed later.", + "Return after prune with a brief explanation of what you pruned and why.", +].join("\n\n") + +const DISTILL_TRIGGER_PROMPT = [ + "", + "Manual mode trigger received. You must now use the distill tool.", + "Select the most information-dense prunable outputs and distill them into complete technical substitutes.", + "Be exhaustive and preserve all critical technical details.", + "Return after distill with a brief explanation of what was distilled and why.", +].join("\n\n") + +const COMPRESS_TRIGGER_PROMPT = [ + "", + "Manual mode trigger received. You must now use the compress tool.", + "Find the most significant completed section of the conversation that can be compressed into a high-fidelity technical summary.", + "Choose safe boundaries and preserve all critical implementation details.", + "Return after compress with a brief explanation of what range was compressed.", +].join("\n\n") + +function getTriggerPrompt( + tool: "prune" | "distill" | "compress", + context?: string, + userFocus?: string, +): string { + const base = + tool === "prune" + ? PRUNE_TRIGGER_PROMPT + : tool === "distill" + ? DISTILL_TRIGGER_PROMPT + : COMPRESS_TRIGGER_PROMPT + + const sections = [base] + if (userFocus && userFocus.trim().length > 0) { + sections.push(`Additional user focus:\n${userFocus.trim()}`) + } + if (context) { + sections.push(context) + } + + return sections.join("\n\n") +} + +export interface ManualCommandContext { + client: any + state: SessionState + config: PluginConfig + logger: Logger + sessionId: string + messages: WithParts[] +} + +export async function handleManualToggleCommand( + ctx: ManualCommandContext, + modeArg?: string, +): Promise { + const { client, state, logger, sessionId, messages } = ctx + + if (modeArg === "on") { + state.manualMode = true + } else if (modeArg === "off") { + state.manualMode = false + } else { + state.manualMode = !state.manualMode + } + + const params = getCurrentParams(state, messages, logger) + await sendIgnoredMessage( + client, + sessionId, + state.manualMode ? MANUAL_MODE_ON : MANUAL_MODE_OFF, + params, + logger, + ) + + logger.info("Manual mode toggled", { manualMode: state.manualMode }) +} + +export async function handleManualTriggerCommand( + ctx: ManualCommandContext, + tool: "prune" | "distill" | "compress", + userFocus?: string, +): Promise { + const { client, state, config, logger, sessionId, messages } = ctx + + if (tool === "prune" || tool === "distill") { + syncToolCache(state, config, logger, messages) + buildToolIdList(state, messages, logger) + const prunableToolsList = buildPrunableToolsList(state, config, logger) + if (!prunableToolsList) { + const params = getCurrentParams(state, messages, logger) + await sendIgnoredMessage(client, sessionId, NO_PRUNABLE_TOOLS, params, logger) + return null + } + + return getTriggerPrompt(tool, prunableToolsList, userFocus) + } + + return getTriggerPrompt("compress", undefined, userFocus) +} diff --git a/lib/config.ts b/lib/config.ts index ddfb08f2..9f7c2120 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -43,6 +43,11 @@ export interface Commands { protectedTools: string[] } +export interface ManualModeConfig { + enabled: boolean + automaticStrategies: boolean +} + export interface SupersedeWrites { enabled: boolean } @@ -64,6 +69,7 @@ export interface PluginConfig { pruneNotification: "off" | "minimal" | "detailed" pruneNotificationType: "chat" | "toast" commands: Commands + manualMode: ManualModeConfig turnProtection: TurnProtection protectedFilePatterns: string[] tools: Tools @@ -102,6 +108,9 @@ export const VALID_CONFIG_KEYS = new Set([ "commands", "commands.enabled", "commands.protectedTools", + "manualMode", + "manualMode.enabled", + "manualMode.automaticStrategies", "tools", "tools.settings", "tools.settings.nudgeEnabled", @@ -263,6 +272,36 @@ export function validateConfigTypes(config: Record): ValidationErro } } + // Manual mode validator + const manualMode = config.manualMode + if (manualMode !== undefined) { + if (typeof manualMode === "object") { + if (manualMode.enabled !== undefined && typeof manualMode.enabled !== "boolean") { + errors.push({ + key: "manualMode.enabled", + expected: "boolean", + actual: typeof manualMode.enabled, + }) + } + if ( + manualMode.automaticStrategies !== undefined && + typeof manualMode.automaticStrategies !== "boolean" + ) { + errors.push({ + key: "manualMode.automaticStrategies", + expected: "boolean", + actual: typeof manualMode.automaticStrategies, + }) + } + } else { + errors.push({ + key: "manualMode", + expected: "{ enabled: boolean, automaticStrategies: boolean }", + actual: typeof manualMode, + }) + } + } + // Tools validators const tools = config.tools if (tools) { @@ -529,6 +568,10 @@ const defaultConfig: PluginConfig = { enabled: true, protectedTools: [...DEFAULT_PROTECTED_TOOLS], }, + manualMode: { + enabled: false, + automaticStrategies: true, + }, turnProtection: { enabled: false, turns: 4, @@ -747,6 +790,18 @@ function mergeCommands( } } +function mergeManualMode( + base: PluginConfig["manualMode"], + override?: Partial, +): PluginConfig["manualMode"] { + if (override === undefined) return base + + return { + enabled: override.enabled ?? base.enabled, + automaticStrategies: override.automaticStrategies ?? base.automaticStrategies, + } +} + function deepCloneConfig(config: PluginConfig): PluginConfig { return { ...config, @@ -754,6 +809,10 @@ function deepCloneConfig(config: PluginConfig): PluginConfig { enabled: config.commands.enabled, protectedTools: [...config.commands.protectedTools], }, + manualMode: { + enabled: config.manualMode.enabled, + automaticStrategies: config.manualMode.automaticStrategies, + }, turnProtection: { ...config.turnProtection }, protectedFilePatterns: [...config.protectedFilePatterns], tools: { @@ -812,6 +871,7 @@ export function getConfig(ctx: PluginInput): PluginConfig { pruneNotificationType: result.data.pruneNotificationType ?? config.pruneNotificationType, commands: mergeCommands(config.commands, result.data.commands as any), + manualMode: mergeManualMode(config.manualMode, result.data.manualMode as any), turnProtection: { enabled: result.data.turnProtection?.enabled ?? config.turnProtection.enabled, turns: result.data.turnProtection?.turns ?? config.turnProtection.turns, @@ -857,6 +917,7 @@ export function getConfig(ctx: PluginInput): PluginConfig { pruneNotificationType: result.data.pruneNotificationType ?? config.pruneNotificationType, commands: mergeCommands(config.commands, result.data.commands as any), + manualMode: mergeManualMode(config.manualMode, result.data.manualMode as any), turnProtection: { enabled: result.data.turnProtection?.enabled ?? config.turnProtection.enabled, turns: result.data.turnProtection?.turns ?? config.turnProtection.turns, @@ -899,6 +960,7 @@ export function getConfig(ctx: PluginInput): PluginConfig { pruneNotificationType: result.data.pruneNotificationType ?? config.pruneNotificationType, commands: mergeCommands(config.commands, result.data.commands as any), + manualMode: mergeManualMode(config.manualMode, result.data.manualMode as any), turnProtection: { enabled: result.data.turnProtection?.enabled ?? config.turnProtection.enabled, turns: result.data.turnProtection?.turns ?? config.turnProtection.turns, diff --git a/lib/hooks.ts b/lib/hooks.ts index 83c74cc2..54c1232d 100644 --- a/lib/hooks.ts +++ b/lib/hooks.ts @@ -4,14 +4,16 @@ import type { PluginConfig } from "./config" import { syncToolCache } from "./state/tool-cache" import { deduplicate, supersedeWrites, purgeErrors } from "./strategies" import { prune, insertPruneToolContext } from "./messages" -import { buildToolIdList } from "./messages/utils" +import { buildToolIdList, isIgnoredUserMessage } from "./messages/utils" import { checkSession } from "./state" import { renderSystemPrompt } from "./prompts" import { handleStatsCommand } from "./commands/stats" import { handleContextCommand } from "./commands/context" import { handleHelpCommand } from "./commands/help" import { handleSweepCommand } from "./commands/sweep" +import { handleManualToggleCommand, handleManualTriggerCommand } from "./commands/manual" import { ensureSessionInitialized } from "./state/state" +import { getCurrentParams } from "./strategies/utils" const INTERNAL_AGENT_SIGNATURES = [ "You are a title generator", @@ -19,6 +21,42 @@ const INTERNAL_AGENT_SIGNATURES = [ "Summarize what was done in this conversation", ] +function applyPendingManualTriggerPrompt( + state: SessionState, + messages: WithParts[], + logger: Logger, +): void { + const pending = state.pendingManualTrigger + if (!pending) { + return + } + + if (!state.sessionId || pending.sessionId !== state.sessionId) { + state.pendingManualTrigger = null + return + } + + for (let i = messages.length - 1; i >= 0; i--) { + const msg = messages[i] + if (msg.info.role !== "user" || isIgnoredUserMessage(msg)) { + continue + } + + for (const part of msg.parts) { + if (part.type !== "text" || part.ignored || part.synthetic) { + continue + } + + part.text = pending.prompt + state.pendingManualTrigger = null + logger.debug("Applied pending manual trigger prompt", { sessionId: pending.sessionId }) + return + } + } + + state.pendingManualTrigger = null +} + export function createSystemPromptHandler( state: SessionState, logger: Logger, @@ -47,6 +85,7 @@ export function createSystemPromptHandler( prune: config.tools.prune.permission !== "deny", distill: config.tools.distill.permission !== "deny", compress: config.tools.compress.permission !== "deny", + manual: state.manualMode, } if (!flags.prune && !flags.distill && !flags.compress) { @@ -64,7 +103,7 @@ export function createChatMessageTransformHandler( config: PluginConfig, ) { return async (input: {}, output: { messages: WithParts[] }) => { - await checkSession(client, state, logger, output.messages) + await checkSession(client, state, logger, output.messages, config.manualMode.enabled) if (state.isSubAgent) { return @@ -78,9 +117,10 @@ export function createChatMessageTransformHandler( purgeErrors(state, logger, config, output.messages) prune(state, logger, config, output.messages) - insertPruneToolContext(state, config, logger, output.messages) + applyPendingManualTriggerPrompt(state, output.messages, logger) + if (state.sessionId) { await logger.saveContext(state.sessionId, output.messages) } @@ -96,7 +136,7 @@ export function createCommandExecuteHandler( ) { return async ( input: { command: string; sessionID: string; arguments: string }, - _output: { parts: any[] }, + output: { parts: any[] }, ) => { if (!config.commands.enabled) { return @@ -108,55 +148,76 @@ export function createCommandExecuteHandler( }) const messages = (messagesResponse.data || messagesResponse) as WithParts[] - await ensureSessionInitialized(client, state, input.sessionID, logger, messages) + await ensureSessionInitialized( + client, + state, + input.sessionID, + logger, + messages, + config.manualMode.enabled, + ) const args = (input.arguments || "").trim().split(/\s+/).filter(Boolean) const subcommand = args[0]?.toLowerCase() || "" - const _subArgs = args.slice(1) + const subArgs = args.slice(1) + + const commandCtx = { + client, + state, + config, + logger, + sessionId: input.sessionID, + messages, + } if (subcommand === "context") { - await handleContextCommand({ - client, - state, - logger, - sessionId: input.sessionID, - messages, - }) + await handleContextCommand(commandCtx) throw new Error("__DCP_CONTEXT_HANDLED__") } if (subcommand === "stats") { - await handleStatsCommand({ - client, - state, - logger, - sessionId: input.sessionID, - messages, - }) + await handleStatsCommand(commandCtx) throw new Error("__DCP_STATS_HANDLED__") } if (subcommand === "sweep") { await handleSweepCommand({ - client, - state, - config, - logger, - sessionId: input.sessionID, - messages, - args: _subArgs, + ...commandCtx, + args: subArgs, workingDirectory, }) throw new Error("__DCP_SWEEP_HANDLED__") } - await handleHelpCommand({ - client, - state, - logger, - sessionId: input.sessionID, - messages, - }) + if (subcommand === "manual") { + await handleManualToggleCommand(commandCtx, subArgs[0]?.toLowerCase()) + throw new Error("__DCP_MANUAL_HANDLED__") + } + + if ( + (subcommand === "prune" || subcommand === "distill" || subcommand === "compress") && + config.tools[subcommand].permission !== "deny" + ) { + const userFocus = subArgs.join(" ").trim() + const prompt = await handleManualTriggerCommand(commandCtx, subcommand, userFocus) + if (!prompt) { + throw new Error("__DCP_MANUAL_TRIGGER_BLOCKED__") + } + + state.pendingManualTrigger = { + sessionId: input.sessionID, + prompt, + } + const rawArgs = (input.arguments || "").trim() + output.parts.length = 0 + output.parts.push({ + type: "text", + text: rawArgs ? `/dcp ${rawArgs}` : `/dcp ${subcommand}`, + }) + return + } + + await handleHelpCommand(commandCtx) throw new Error("__DCP_HELP_HANDLED__") } } diff --git a/lib/messages/inject.ts b/lib/messages/inject.ts index 744b632a..83256f30 100644 --- a/lib/messages/inject.ts +++ b/lib/messages/inject.ts @@ -131,6 +131,7 @@ const getNudgeString = (config: PluginConfig): string => { prune: config.tools.prune.permission !== "deny", distill: config.tools.distill.permission !== "deny", compress: config.tools.compress.permission !== "deny", + manual: false, } if (!flags.prune && !flags.distill && !flags.compress) { @@ -153,7 +154,7 @@ const buildCompressContext = (state: SessionState, messages: WithParts[]): strin return wrapCompressContext(messageCount) } -const buildPrunableToolsList = ( +export const buildPrunableToolsList = ( state: SessionState, config: PluginConfig, logger: Logger, @@ -214,6 +215,10 @@ export const insertPruneToolContext = ( logger: Logger, messages: WithParts[], ): void => { + if (state.manualMode || state.pendingManualTrigger) { + return + } + const pruneEnabled = config.tools.prune.permission !== "deny" const distillEnabled = config.tools.distill.permission !== "deny" const compressEnabled = config.tools.compress.permission !== "deny" diff --git a/lib/prompts/index.ts b/lib/prompts/index.ts index e764099a..d46f3eac 100644 --- a/lib/prompts/index.ts +++ b/lib/prompts/index.ts @@ -10,10 +10,11 @@ export interface ToolFlags { distill: boolean compress: boolean prune: boolean + manual: boolean } function processConditionals(template: string, flags: ToolFlags): string { - const tools = ["distill", "compress", "prune"] as const + const tools = ["distill", "compress", "prune", "manual"] as const let result = template // Strip comments: // ... // result = result.replace(/\/\/.*?\/\//g, "") diff --git a/lib/prompts/system.md b/lib/prompts/system.md index 4ea2b6b1..48ce2a4b 100644 --- a/lib/prompts/system.md +++ b/lib/prompts/system.md @@ -35,6 +35,16 @@ The session is your responsibility, and effective context management is CRITICAL Be respectful of the user's API usage, manage context methodically as you work through the task and avoid calling ONLY context management tools in your responses. + +Manual mode is enabled. Do NOT use distill, compress, or prune unless the user has explicitly triggered it through a manual marker. + +Only use the prune tool after seeing `` in the current user instruction context. +Only use the distill tool after seeing `` in the current user instruction context. +Only use the compress tool after seeing `` in the current user instruction context. + +After completing a manually triggered context-management action, STOP IMMEDIATELY. Do NOT continue with any task execution. End your response right after the tool use completes and wait for the next user input. + + This chat environment injects context information on your behalf in the form of a list to help you manage context effectively. Carefully read the list and use it to inform your management decisions. The list is automatically updated after each turn to reflect the current state of manageable tools and context usage. If no list is present, do NOT attempt to prune anything. There may be tools in session context that do not appear in the list, this is expected, remember that you can ONLY prune what you see in list. diff --git a/lib/state/state.ts b/lib/state/state.ts index 21d7ef8f..d6e5296f 100644 --- a/lib/state/state.ts +++ b/lib/state/state.ts @@ -15,6 +15,7 @@ export const checkSession = async ( state: SessionState, logger: Logger, messages: WithParts[], + manualModeDefault: boolean, ): Promise => { const lastUserMessage = getLastUserMessage(messages) if (!lastUserMessage) { @@ -26,7 +27,14 @@ export const checkSession = async ( if (state.sessionId === null || state.sessionId !== lastSessionId) { logger.info(`Session changed: ${state.sessionId} -> ${lastSessionId}`) try { - await ensureSessionInitialized(client, state, lastSessionId, logger, messages) + await ensureSessionInitialized( + client, + state, + lastSessionId, + logger, + messages, + manualModeDefault, + ) } catch (err: any) { logger.error("Failed to initialize session state", { error: err.message }) } @@ -48,6 +56,8 @@ export function createSessionState(): SessionState { return { sessionId: null, isSubAgent: false, + manualMode: false, + pendingManualTrigger: null, prune: { tools: new Map(), messages: new Map(), @@ -71,6 +81,8 @@ export function createSessionState(): SessionState { export function resetSessionState(state: SessionState): void { state.sessionId = null state.isSubAgent = false + state.manualMode = false + state.pendingManualTrigger = null state.prune = { tools: new Map(), messages: new Map(), @@ -96,20 +108,22 @@ export async function ensureSessionInitialized( sessionId: string, logger: Logger, messages: WithParts[], + manualModeDefault: boolean, ): Promise { if (state.sessionId === sessionId) { return } - logger.info("session ID = " + sessionId) - logger.info("Initializing session state", { sessionId: sessionId }) + // logger.info("session ID = " + sessionId) + // logger.info("Initializing session state", { sessionId: sessionId }) resetSessionState(state) + state.manualMode = manualModeDefault state.sessionId = sessionId const isSubAgent = await isSubAgentSession(client, sessionId) state.isSubAgent = isSubAgent - logger.info("isSubAgent = " + isSubAgent) + // logger.info("isSubAgent = " + isSubAgent) state.lastCompaction = findLastCompactionTimestamp(messages) state.currentTurn = countTurns(state, messages) diff --git a/lib/state/types.ts b/lib/state/types.ts index 6e004dae..b9942289 100644 --- a/lib/state/types.ts +++ b/lib/state/types.ts @@ -31,9 +31,16 @@ export interface Prune { messages: Map } +export interface PendingManualTrigger { + sessionId: string + prompt: string +} + export interface SessionState { sessionId: string | null isSubAgent: boolean + manualMode: boolean + pendingManualTrigger: PendingManualTrigger | null prune: Prune compressSummaries: CompressSummary[] stats: SessionStats diff --git a/lib/strategies/deduplication.ts b/lib/strategies/deduplication.ts index 773e9ce3..aff2d019 100644 --- a/lib/strategies/deduplication.ts +++ b/lib/strategies/deduplication.ts @@ -15,6 +15,10 @@ export const deduplicate = ( config: PluginConfig, messages: WithParts[], ): void => { + if (state.manualMode && !config.manualMode.automaticStrategies) { + return + } + if (!config.strategies.deduplication.enabled) { return } diff --git a/lib/strategies/purge-errors.ts b/lib/strategies/purge-errors.ts index 681c05a2..b8a140b9 100644 --- a/lib/strategies/purge-errors.ts +++ b/lib/strategies/purge-errors.ts @@ -18,6 +18,10 @@ export const purgeErrors = ( config: PluginConfig, messages: WithParts[], ): void => { + if (state.manualMode && !config.manualMode.automaticStrategies) { + return + } + if (!config.strategies.purgeErrors.enabled) { return } diff --git a/lib/strategies/supersede-writes.ts b/lib/strategies/supersede-writes.ts index eb187823..e61b1d0a 100644 --- a/lib/strategies/supersede-writes.ts +++ b/lib/strategies/supersede-writes.ts @@ -18,6 +18,10 @@ export const supersedeWrites = ( config: PluginConfig, messages: WithParts[], ): void => { + if (state.manualMode && !config.manualMode.automaticStrategies) { + return + } + if (!config.strategies.supersedeWrites.enabled) { return } diff --git a/lib/tools/compress.ts b/lib/tools/compress.ts index 839c7b94..5c019aed 100644 --- a/lib/tools/compress.ts +++ b/lib/tools/compress.ts @@ -73,7 +73,14 @@ export function createCompressTool(ctx: PruneToolContext): ReturnType