Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 5 additions & 20 deletions lib/messages/prune.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ import type { Logger } from "../logger"
import type { PluginConfig } from "../config"
import { isMessageCompacted } from "../shared-utils"

const PRUNED_TOOL_INPUT_REPLACEMENT =
"[content removed to save context, this is not what was written to the file, but a placeholder]"
const PRUNED_TOOL_OUTPUT_REPLACEMENT =
"[Output removed to save context - information superseded or no longer needed]"
const PRUNED_TOOL_ERROR_INPUT_REPLACEMENT = "[input removed due to failed tool call]"
const PRUNED_QUESTION_INPUT_REPLACEMENT = "[questions removed - see output for user's answers]"

export const prune = (
state: SessionState,
Expand All @@ -33,7 +32,8 @@ const pruneToolOutputs = (state: SessionState, logger: Logger, messages: WithPar
if (!state.prune.toolIds.includes(part.callID)) {
continue
}
if (part.tool === "write" || part.tool === "edit") {
// Skip write/edit (protected) and question (output contains answers we want to keep)
if (part.tool === "write" || part.tool === "edit" || part.tool === "question") {
continue
}
if (part.state.status === "completed") {
Expand All @@ -43,10 +43,6 @@ const pruneToolOutputs = (state: SessionState, logger: Logger, messages: WithPar
}
}

// NOTE: This function is currently unused because "write" and "edit" are protected by default.
// Some models incorrectly use PRUNED_TOOL_INPUT_REPLACEMENT in their output when they see it in context.
// See: https://github.com/Opencode-DCP/opencode-dynamic-context-pruning/issues/215
// Keeping this function in case the bug is resolved in the future.
const pruneToolInputs = (state: SessionState, logger: Logger, messages: WithParts[]): void => {
for (const msg of messages) {
if (isMessageCompacted(state, msg)) {
Expand All @@ -60,23 +56,12 @@ const pruneToolInputs = (state: SessionState, logger: Logger, messages: WithPart
if (!state.prune.toolIds.includes(part.callID)) {
continue
}
if (part.tool !== "write" && part.tool !== "edit") {
continue
}
if (part.state.status !== "completed") {
continue
}

if (part.tool === "write" && part.state.input?.content !== undefined) {
part.state.input.content = PRUNED_TOOL_INPUT_REPLACEMENT
}
if (part.tool === "edit") {
if (part.state.input?.oldString !== undefined) {
part.state.input.oldString = PRUNED_TOOL_INPUT_REPLACEMENT
}
if (part.state.input?.newString !== undefined) {
part.state.input.newString = PRUNED_TOOL_INPUT_REPLACEMENT
}
if (part.tool === "question" && part.state.input?.questions !== undefined) {
part.state.input.questions = PRUNED_QUESTION_INPUT_REPLACEMENT
}
}
}
Expand Down
20 changes: 20 additions & 0 deletions lib/messages/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,26 @@ export const extractParameterKey = (tool: string, parameters: any): string => {
return op
}

if (tool === "question") {
const questions = parameters.questions
if (Array.isArray(questions) && questions.length > 0) {
const headers = questions
.map((q: any) => q.header || "")
.filter(Boolean)
.slice(0, 3)

const count = questions.length
const plural = count > 1 ? "s" : ""

if (headers.length > 0) {
const suffix = count > 3 ? ` (+${count - 3} more)` : ""
return `${count} question${plural}: ${headers.join(", ")}${suffix}`
}
return `${count} question${plural}`
}
return "question"
}

const paramStr = JSON.stringify(parameters)
if (paramStr === "{}" || paramStr === "[]" || paramStr === "null") {
return ""
Expand Down
26 changes: 6 additions & 20 deletions lib/strategies/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,29 +62,15 @@ export const calculateTokensSaved = (
if (part.type !== "tool" || !pruneToolIds.includes(part.callID)) {
continue
}
// For write and edit tools, count input content as that is all we prune for these tools
// (input is present in both completed and error states)
if (part.tool === "write") {
const inputContent = part.state.input?.content
const content =
typeof inputContent === "string"
? inputContent
: JSON.stringify(inputContent ?? "")
contents.push(content)
continue
}
if (part.tool === "edit") {
const oldString = part.state.input?.oldString
const newString = part.state.input?.newString
if (typeof oldString === "string") {
contents.push(oldString)
}
if (typeof newString === "string") {
contents.push(newString)
if (part.tool === "question") {
const questions = part.state.input?.questions
if (questions !== undefined) {
const content =
typeof questions === "string" ? questions : JSON.stringify(questions)
contents.push(content)
}
continue
}
// For other tools, count output or error based on status
if (part.state.status === "completed") {
const content =
typeof part.state.output === "string"
Expand Down