fix(compaction): remove hardcoded Claude model from compaction hooks
This commit is contained in:
parent
f1b2f6f3f7
commit
60bbeb7304
@ -1,14 +1,4 @@
|
|||||||
import { describe, expect, it, mock, beforeEach } from "bun:test"
|
import { describe, expect, it, mock } from "bun:test"
|
||||||
|
|
||||||
// Mock dependencies before importing
|
|
||||||
const mockInjectHookMessage = mock(() => true)
|
|
||||||
mock.module("../../features/hook-message-injector", () => ({
|
|
||||||
injectHookMessage: mockInjectHookMessage,
|
|
||||||
}))
|
|
||||||
|
|
||||||
mock.module("../../shared/logger", () => ({
|
|
||||||
log: () => {},
|
|
||||||
}))
|
|
||||||
|
|
||||||
mock.module("../../shared/system-directive", () => ({
|
mock.module("../../shared/system-directive", () => ({
|
||||||
createSystemDirective: (type: string) => `[DIRECTIVE:${type}]`,
|
createSystemDirective: (type: string) => `[DIRECTIVE:${type}]`,
|
||||||
@ -25,78 +15,45 @@ mock.module("../../shared/system-directive", () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
import { createCompactionContextInjector } from "./index"
|
import { createCompactionContextInjector } from "./index"
|
||||||
import type { SummarizeContext } from "./index"
|
|
||||||
|
|
||||||
describe("createCompactionContextInjector", () => {
|
describe("createCompactionContextInjector", () => {
|
||||||
beforeEach(() => {
|
|
||||||
mockInjectHookMessage.mockClear()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("Agent Verification State preservation", () => {
|
describe("Agent Verification State preservation", () => {
|
||||||
it("includes Agent Verification State section in compaction prompt", async () => {
|
it("includes Agent Verification State section in compaction prompt", async () => {
|
||||||
// given
|
//#given
|
||||||
const injector = createCompactionContextInjector()
|
const injector = createCompactionContextInjector()
|
||||||
const context: SummarizeContext = {
|
|
||||||
sessionID: "test-session",
|
|
||||||
providerID: "anthropic",
|
|
||||||
modelID: "claude-sonnet-4-5",
|
|
||||||
usageRatio: 0.85,
|
|
||||||
directory: "/test/dir",
|
|
||||||
}
|
|
||||||
|
|
||||||
// when
|
//#when
|
||||||
await injector(context)
|
const prompt = injector()
|
||||||
|
|
||||||
// then
|
//#then
|
||||||
expect(mockInjectHookMessage).toHaveBeenCalledTimes(1)
|
expect(prompt).toContain("Agent Verification State")
|
||||||
const calls = mockInjectHookMessage.mock.calls as unknown as [string, string, unknown][]
|
expect(prompt).toContain("Current Agent")
|
||||||
const injectedPrompt = calls[0]?.[1] ?? ""
|
expect(prompt).toContain("Verification Progress")
|
||||||
expect(injectedPrompt).toContain("Agent Verification State")
|
|
||||||
expect(injectedPrompt).toContain("Current Agent")
|
|
||||||
expect(injectedPrompt).toContain("Verification Progress")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("includes Momus-specific context for reviewer agents", async () => {
|
it("includes reviewer-agent continuity fields", async () => {
|
||||||
// given
|
//#given
|
||||||
const injector = createCompactionContextInjector()
|
const injector = createCompactionContextInjector()
|
||||||
const context: SummarizeContext = {
|
|
||||||
sessionID: "test-session",
|
|
||||||
providerID: "anthropic",
|
|
||||||
modelID: "claude-sonnet-4-5",
|
|
||||||
usageRatio: 0.9,
|
|
||||||
directory: "/test/dir",
|
|
||||||
}
|
|
||||||
|
|
||||||
// when
|
//#when
|
||||||
await injector(context)
|
const prompt = injector()
|
||||||
|
|
||||||
// then
|
//#then
|
||||||
const calls = mockInjectHookMessage.mock.calls as unknown as [string, string, unknown][]
|
expect(prompt).toContain("Previous Rejections")
|
||||||
const injectedPrompt = calls[0]?.[1] ?? ""
|
expect(prompt).toContain("Acceptance Status")
|
||||||
expect(injectedPrompt).toContain("Previous Rejections")
|
expect(prompt).toContain("reviewer agents")
|
||||||
expect(injectedPrompt).toContain("Acceptance Status")
|
|
||||||
expect(injectedPrompt).toContain("reviewer agents")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("preserves file verification progress in compaction prompt", async () => {
|
it("preserves file verification progress fields", async () => {
|
||||||
// given
|
//#given
|
||||||
const injector = createCompactionContextInjector()
|
const injector = createCompactionContextInjector()
|
||||||
const context: SummarizeContext = {
|
|
||||||
sessionID: "test-session",
|
|
||||||
providerID: "anthropic",
|
|
||||||
modelID: "claude-sonnet-4-5",
|
|
||||||
usageRatio: 0.95,
|
|
||||||
directory: "/test/dir",
|
|
||||||
}
|
|
||||||
|
|
||||||
// when
|
//#when
|
||||||
await injector(context)
|
const prompt = injector()
|
||||||
|
|
||||||
// then
|
//#then
|
||||||
const calls = mockInjectHookMessage.mock.calls as unknown as [string, string, unknown][]
|
expect(prompt).toContain("Pending Verifications")
|
||||||
const injectedPrompt = calls[0]?.[1] ?? ""
|
expect(prompt).toContain("Files already verified")
|
||||||
expect(injectedPrompt).toContain("Pending Verifications")
|
|
||||||
expect(injectedPrompt).toContain("Files already verified")
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,16 +1,6 @@
|
|||||||
import { injectHookMessage } from "../../features/hook-message-injector"
|
|
||||||
import { log } from "../../shared/logger"
|
|
||||||
import { createSystemDirective, SystemDirectiveTypes } from "../../shared/system-directive"
|
import { createSystemDirective, SystemDirectiveTypes } from "../../shared/system-directive"
|
||||||
|
|
||||||
export interface SummarizeContext {
|
const COMPACTION_CONTEXT_PROMPT = `${createSystemDirective(SystemDirectiveTypes.COMPACTION_CONTEXT)}
|
||||||
sessionID: string
|
|
||||||
providerID: string
|
|
||||||
modelID: string
|
|
||||||
usageRatio: number
|
|
||||||
directory: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const SUMMARIZE_CONTEXT_PROMPT = `${createSystemDirective(SystemDirectiveTypes.COMPACTION_CONTEXT)}
|
|
||||||
|
|
||||||
When summarizing this session, you MUST include the following sections in your summary:
|
When summarizing this session, you MUST include the following sections in your summary:
|
||||||
|
|
||||||
@ -58,19 +48,5 @@ This context is critical for maintaining continuity after compaction.
|
|||||||
`
|
`
|
||||||
|
|
||||||
export function createCompactionContextInjector() {
|
export function createCompactionContextInjector() {
|
||||||
return async (ctx: SummarizeContext): Promise<void> => {
|
return (): string => COMPACTION_CONTEXT_PROMPT
|
||||||
log("[compaction-context-injector] injecting context", { sessionID: ctx.sessionID })
|
|
||||||
|
|
||||||
const success = injectHookMessage(ctx.sessionID, SUMMARIZE_CONTEXT_PROMPT, {
|
|
||||||
agent: "general",
|
|
||||||
model: { providerID: ctx.providerID, modelID: ctx.modelID },
|
|
||||||
path: { cwd: ctx.directory },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
log("[compaction-context-injector] context injected", { sessionID: ctx.sessionID })
|
|
||||||
} else {
|
|
||||||
log("[compaction-context-injector] injection failed", { sessionID: ctx.sessionID })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export { createDelegateTaskRetryHook } from "./delegate-task-retry";
|
|||||||
export { createQuestionLabelTruncatorHook } from "./question-label-truncator";
|
export { createQuestionLabelTruncatorHook } from "./question-label-truncator";
|
||||||
export { createSubagentQuestionBlockerHook } from "./subagent-question-blocker";
|
export { createSubagentQuestionBlockerHook } from "./subagent-question-blocker";
|
||||||
export { createStopContinuationGuardHook, type StopContinuationGuard } from "./stop-continuation-guard";
|
export { createStopContinuationGuardHook, type StopContinuationGuard } from "./stop-continuation-guard";
|
||||||
export { createCompactionContextInjector, type SummarizeContext } from "./compaction-context-injector";
|
export { createCompactionContextInjector } from "./compaction-context-injector";
|
||||||
export { createUnstableAgentBabysitterHook } from "./unstable-agent-babysitter";
|
export { createUnstableAgentBabysitterHook } from "./unstable-agent-babysitter";
|
||||||
export { createPreemptiveCompactionHook } from "./preemptive-compaction";
|
export { createPreemptiveCompactionHook } from "./preemptive-compaction";
|
||||||
export { createTasksTodowriteDisablerHook } from "./tasks-todowrite-disabler";
|
export { createTasksTodowriteDisablerHook } from "./tasks-todowrite-disabler";
|
||||||
|
|||||||
@ -60,6 +60,41 @@ describe("preemptive-compaction", () => {
|
|||||||
expect(summarize).toHaveBeenCalled()
|
expect(summarize).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("triggers summarize for non-anthropic providers when usage exceeds threshold", async () => {
|
||||||
|
//#given
|
||||||
|
const messages = mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
info: {
|
||||||
|
role: "assistant",
|
||||||
|
providerID: "openai",
|
||||||
|
modelID: "gpt-5.2",
|
||||||
|
tokens: {
|
||||||
|
input: 180000,
|
||||||
|
output: 0,
|
||||||
|
reasoning: 0,
|
||||||
|
cache: { read: 0, write: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const summarize = mock(() => Promise.resolve())
|
||||||
|
const hook = createPreemptiveCompactionHook(createMockCtx({ messages, summarize }))
|
||||||
|
const output = { title: "", output: "", metadata: {} }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
await hook["tool.execute.after"](
|
||||||
|
{ tool: "Read", sessionID, callID: "call-3" },
|
||||||
|
output
|
||||||
|
)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(summarize).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
test("does not summarize when usage is below threshold", async () => {
|
test("does not summarize when usage is below threshold", async () => {
|
||||||
// #given
|
// #given
|
||||||
const messages = mock(() =>
|
const messages = mock(() =>
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
|
const DEFAULT_ACTUAL_LIMIT = 200_000
|
||||||
|
|
||||||
const ANTHROPIC_ACTUAL_LIMIT =
|
const ANTHROPIC_ACTUAL_LIMIT =
|
||||||
process.env.ANTHROPIC_1M_CONTEXT === "true" ||
|
process.env.ANTHROPIC_1M_CONTEXT === "true" ||
|
||||||
process.env.VERTEX_ANTHROPIC_1M_CONTEXT === "true"
|
process.env.VERTEX_ANTHROPIC_1M_CONTEXT === "true"
|
||||||
? 1_000_000
|
? 1_000_000
|
||||||
: 200_000
|
: DEFAULT_ACTUAL_LIMIT
|
||||||
|
|
||||||
const PREEMPTIVE_COMPACTION_THRESHOLD = 0.78
|
const PREEMPTIVE_COMPACTION_THRESHOLD = 0.78
|
||||||
|
|
||||||
@ -59,11 +61,14 @@ export function createPreemptiveCompactionHook(ctx: PluginInput) {
|
|||||||
if (assistantMessages.length === 0) return
|
if (assistantMessages.length === 0) return
|
||||||
|
|
||||||
const lastAssistant = assistantMessages[assistantMessages.length - 1]
|
const lastAssistant = assistantMessages[assistantMessages.length - 1]
|
||||||
if (lastAssistant.providerID !== "anthropic") return
|
const actualLimit =
|
||||||
|
lastAssistant.providerID === "anthropic"
|
||||||
|
? ANTHROPIC_ACTUAL_LIMIT
|
||||||
|
: DEFAULT_ACTUAL_LIMIT
|
||||||
|
|
||||||
const lastTokens = lastAssistant.tokens
|
const lastTokens = lastAssistant.tokens
|
||||||
const totalInputTokens = (lastTokens?.input ?? 0) + (lastTokens?.cache?.read ?? 0)
|
const totalInputTokens = (lastTokens?.input ?? 0) + (lastTokens?.cache?.read ?? 0)
|
||||||
const usageRatio = totalInputTokens / ANTHROPIC_ACTUAL_LIMIT
|
const usageRatio = totalInputTokens / actualLimit
|
||||||
|
|
||||||
if (usageRatio < PREEMPTIVE_COMPACTION_THRESHOLD) return
|
if (usageRatio < PREEMPTIVE_COMPACTION_THRESHOLD) return
|
||||||
|
|
||||||
|
|||||||
21
src/index.compaction-model-agnostic.static.test.ts
Normal file
21
src/index.compaction-model-agnostic.static.test.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { readFileSync } from "node:fs"
|
||||||
|
|
||||||
|
describe("experimental.session.compacting", () => {
|
||||||
|
test("does not hardcode a model and uses output.context", () => {
|
||||||
|
//#given
|
||||||
|
const indexUrl = new URL("./index.ts", import.meta.url)
|
||||||
|
const content = readFileSync(indexUrl, "utf-8")
|
||||||
|
const hookIndex = content.indexOf('"experimental.session.compacting"')
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const hookSlice = hookIndex >= 0 ? content.slice(hookIndex, hookIndex + 1200) : ""
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(hookIndex).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(content.includes('modelID: "claude-opus-4-6"')).toBe(false)
|
||||||
|
expect(hookSlice.includes("output.context.push")).toBe(true)
|
||||||
|
expect(hookSlice.includes("providerID:")).toBe(false)
|
||||||
|
expect(hookSlice.includes("modelID:")).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
28
src/index.ts
28
src/index.ts
@ -107,6 +107,7 @@ import {
|
|||||||
OPENCODE_NATIVE_AGENTS_INJECTION_VERSION,
|
OPENCODE_NATIVE_AGENTS_INJECTION_VERSION,
|
||||||
injectServerAuthIntoClient,
|
injectServerAuthIntoClient,
|
||||||
} from "./shared";
|
} from "./shared";
|
||||||
|
import { filterDisabledTools } from "./shared/disabled-tools";
|
||||||
import { loadPluginConfig } from "./plugin-config";
|
import { loadPluginConfig } from "./plugin-config";
|
||||||
import { createModelCacheState } from "./plugin-state";
|
import { createModelCacheState } from "./plugin-state";
|
||||||
import { createConfigHandler } from "./plugin-handlers";
|
import { createConfigHandler } from "./plugin-handlers";
|
||||||
@ -121,7 +122,6 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
|
|
||||||
const pluginConfig = loadPluginConfig(ctx.directory, ctx);
|
const pluginConfig = loadPluginConfig(ctx.directory, ctx);
|
||||||
const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
|
const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
|
||||||
const disabledTools = new Set(pluginConfig.disabled_tools ?? []);
|
|
||||||
const firstMessageVariantGate = createFirstMessageVariantGate();
|
const firstMessageVariantGate = createFirstMessageVariantGate();
|
||||||
|
|
||||||
const tmuxConfig = {
|
const tmuxConfig = {
|
||||||
@ -537,15 +537,10 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
...taskToolsRecord,
|
...taskToolsRecord,
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredTools: Record<string, ToolDefinition> =
|
const filteredTools: Record<string, ToolDefinition> = filterDisabledTools(
|
||||||
disabledTools.size > 0 ? {} : allTools;
|
allTools,
|
||||||
if (disabledTools.size > 0) {
|
pluginConfig.disabled_tools,
|
||||||
for (const [toolName, toolDefinition] of Object.entries(allTools)) {
|
);
|
||||||
if (!disabledTools.has(toolName)) {
|
|
||||||
filteredTools[toolName] = toolDefinition;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tool: filteredTools,
|
tool: filteredTools,
|
||||||
@ -891,17 +886,14 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
await taskResumeInfo["tool.execute.after"](input, output);
|
await taskResumeInfo["tool.execute.after"](input, output);
|
||||||
},
|
},
|
||||||
|
|
||||||
"experimental.session.compacting": async (input: { sessionID: string }) => {
|
"experimental.session.compacting": async (
|
||||||
|
_input: { sessionID: string },
|
||||||
|
output: { context: string[] },
|
||||||
|
): Promise<void> => {
|
||||||
if (!compactionContextInjector) {
|
if (!compactionContextInjector) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await compactionContextInjector({
|
output.context.push(compactionContextInjector());
|
||||||
sessionID: input.sessionID,
|
|
||||||
providerID: "anthropic",
|
|
||||||
modelID: "claude-opus-4-6",
|
|
||||||
usageRatio: 0.8,
|
|
||||||
directory: ctx.directory,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
19
src/shared/disabled-tools.ts
Normal file
19
src/shared/disabled-tools.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import type { ToolDefinition } from "@opencode-ai/plugin"
|
||||||
|
|
||||||
|
export function filterDisabledTools(
|
||||||
|
tools: Record<string, ToolDefinition>,
|
||||||
|
disabledTools: readonly string[] | undefined
|
||||||
|
): Record<string, ToolDefinition> {
|
||||||
|
if (!disabledTools || disabledTools.length === 0) {
|
||||||
|
return tools
|
||||||
|
}
|
||||||
|
|
||||||
|
const disabledToolSet = new Set(disabledTools)
|
||||||
|
const filtered: Record<string, ToolDefinition> = {}
|
||||||
|
for (const [toolName, toolDefinition] of Object.entries(tools)) {
|
||||||
|
if (!disabledToolSet.has(toolName)) {
|
||||||
|
filtered[toolName] = toolDefinition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user