71144825ec
- Added a `mode` property to `CompactOptions` to allow fine-grained control over compaction strategies. - Implemented `soft`, `remote`, and `snapcompact` submode overrides for the `/compact` command. - Integrated `parseCompactArgs` to enable robust subcommand routing and validation, including focus instruction rejection for specific modes. - Established a `CompactMode` registry to manage compaction strategies and verify remote availability.
106 lines
3.9 KiB
TypeScript
106 lines
3.9 KiB
TypeScript
/**
|
|
* Manual `/compact` subcommands. Kept in a dependency-free leaf module so the
|
|
* slash-command registry, the interactive controllers, and `AgentSession`
|
|
* can all import the mode metadata + parser without pulling in the heavy
|
|
* `agent-session` module graph (which would form an import cycle through the
|
|
* slash-command registry) — same rationale as `shake-types.ts`.
|
|
*
|
|
* Each mode is a one-off override layered on top of the configured
|
|
* `compaction.*` settings for a single invocation; it never mutates settings.
|
|
* Adding a mode is a single entry here: the command surface (autocomplete +
|
|
* ACP hint), the parser, and the engine override all read this table.
|
|
*/
|
|
|
|
/** Subcommand selecting a one-off compaction mode for manual `/compact`. */
|
|
export type CompactMode = "soft" | "remote" | "snapcompact";
|
|
|
|
/**
|
|
* Per-invocation overrides merged over the configured `compaction.*` settings.
|
|
* Narrowed to the two knobs the modes actually flip; the result stays
|
|
* assignable to the full `CompactionSettings`.
|
|
*/
|
|
export interface CompactionOverride {
|
|
strategy?: "context-full" | "snapcompact";
|
|
remoteEnabled?: boolean;
|
|
}
|
|
|
|
export interface CompactModeDef {
|
|
readonly name: CompactMode;
|
|
/** One-line description surfaced in autocomplete + help. */
|
|
readonly description: string;
|
|
/** Settings overrides applied on top of `compaction.*` for this run. */
|
|
readonly overrides: CompactionOverride;
|
|
/**
|
|
* When true, the mode produces no LLM summary, so trailing focus text is
|
|
* meaningless and rejected by the parser (snapcompact archives history into
|
|
* images without a directed summary).
|
|
*/
|
|
readonly rejectsFocus?: boolean;
|
|
/**
|
|
* When true, the mode explicitly demands a remote path; the engine warns and
|
|
* falls back to a local summary if neither a remote endpoint nor a
|
|
* provider-native compaction path is available.
|
|
*/
|
|
readonly requiresRemote?: boolean;
|
|
}
|
|
|
|
export const COMPACT_MODES: readonly CompactModeDef[] = [
|
|
{
|
|
name: "soft",
|
|
description: "Summarize locally with the active model (skip remote endpoints)",
|
|
overrides: { strategy: "context-full", remoteEnabled: false },
|
|
},
|
|
{
|
|
name: "remote",
|
|
description: "Summarize via the remote endpoint / provider-native compaction",
|
|
overrides: { strategy: "context-full", remoteEnabled: true },
|
|
requiresRemote: true,
|
|
},
|
|
{
|
|
name: "snapcompact",
|
|
description: "Archive history onto dense bitmap images the model reads back (no LLM call)",
|
|
overrides: { strategy: "snapcompact" },
|
|
rejectsFocus: true,
|
|
},
|
|
];
|
|
|
|
/** Resolve a subcommand token (case-insensitive) to its mode definition. */
|
|
export function findCompactMode(name: string): CompactModeDef | undefined {
|
|
const key = name.trim().toLowerCase();
|
|
return COMPACT_MODES.find(mode => mode.name === key);
|
|
}
|
|
|
|
/** Parsed `/compact` arguments: an optional mode plus optional focus text. */
|
|
export interface ParsedCompactArgs {
|
|
mode?: CompactMode;
|
|
instructions?: string;
|
|
}
|
|
|
|
/**
|
|
* Split `/compact` args into a leading mode subcommand + focus instructions.
|
|
*
|
|
* Backward compatible: when the first token is not a known mode, the entire
|
|
* argument string is treated as focus instructions (the historical behavior).
|
|
* A recognized mode with `rejectsFocus` and trailing text is an error.
|
|
*/
|
|
export function parseCompactArgs(args: string): ParsedCompactArgs | { error: string } {
|
|
const trimmed = args.trim();
|
|
if (!trimmed) return {};
|
|
|
|
const spaceIndex = trimmed.search(/\s/);
|
|
const firstToken = spaceIndex === -1 ? trimmed : trimmed.slice(0, spaceIndex);
|
|
const mode = findCompactMode(firstToken);
|
|
if (!mode) {
|
|
// No recognized mode prefix — keep the whole thing as focus instructions.
|
|
return { instructions: trimmed };
|
|
}
|
|
|
|
const focus = spaceIndex === -1 ? "" : trimmed.slice(spaceIndex + 1).trim();
|
|
if (mode.rejectsFocus && focus) {
|
|
return {
|
|
error: `/compact ${mode.name} does not take focus instructions (it archives history without an LLM summary).`,
|
|
};
|
|
}
|
|
return { mode: mode.name, instructions: focus || undefined };
|
|
}
|