Skip to content
Closed
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ DCP uses its own config file:
> "contextLimit": 100000,
> // Additional tools to protect from pruning
> "protectedTools": [],
> // Model name patterns that should use text parts instead of tool parts
> // for DCP context injection. Prevents 400 errors with providers that use
> // strict tool call/result pairing (e.g., Antigravity Claude models).
> // Uses case-insensitive substring matching against the model ID.
> "textPartModels": ["antigravity-claude"],
Comment on lines +113 to +117
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README documents the new tools.settings.textPartModels option, but the referenced JSON schema (dcp.schema.json) does not currently include this property. Update the schema to keep editor validation/autocomplete in sync with the documented config.

Suggested change
> // Model name patterns that should use text parts instead of tool parts
> // for DCP context injection. Prevents 400 errors with providers that use
> // strict tool call/result pairing (e.g., Antigravity Claude models).
> // Uses case-insensitive substring matching against the model ID.
> "textPartModels": ["antigravity-claude"],

Copilot uses AI. Check for mistakes.
> },
> // Distills key findings into preserved knowledge before removing raw content
> "distill": {
Expand Down
15 changes: 15 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface ToolSettings {
nudgeFrequency: number
protectedTools: string[]
contextLimit: number | `${number}%`
textPartModels: string[]
}

export interface Tools {
Expand Down Expand Up @@ -107,6 +108,7 @@ export const VALID_CONFIG_KEYS = new Set([
"tools.settings.nudgeFrequency",
"tools.settings.protectedTools",
"tools.settings.contextLimit",
"tools.settings.textPartModels",
"tools.distill",
"tools.distill.permission",
"tools.distill.showDistillation",
Expand Down Expand Up @@ -303,6 +305,16 @@ function validateConfigTypes(config: Record<string, any>): ValidationError[] {
})
}
}
if (
tools.settings.textPartModels !== undefined &&
!Array.isArray(tools.settings.textPartModels)
) {
errors.push({
key: "tools.settings.textPartModels",
expected: "string[]",
actual: typeof tools.settings.textPartModels,
})
Comment on lines +308 to +316
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

textPartModels is only validated as an array, but insertPruneToolContext calls pattern.toLowerCase(). If the config contains non-string entries, this will throw at runtime. Extend validation to ensure every element in tools.settings.textPartModels is a string (or coerce/filter invalid entries).

Suggested change
if (
tools.settings.textPartModels !== undefined &&
!Array.isArray(tools.settings.textPartModels)
) {
errors.push({
key: "tools.settings.textPartModels",
expected: "string[]",
actual: typeof tools.settings.textPartModels,
})
if (tools.settings.textPartModels !== undefined) {
if (!Array.isArray(tools.settings.textPartModels)) {
errors.push({
key: "tools.settings.textPartModels",
expected: "string[]",
actual: typeof tools.settings.textPartModels,
})
} else if (
!tools.settings.textPartModels.every(
(model) => typeof model === "string"
)
) {
errors.push({
key: "tools.settings.textPartModels",
expected: "string[]",
actual: JSON.stringify(tools.settings.textPartModels),
})
}

Copilot uses AI. Check for mistakes.
}
}
if (tools.distill) {
if (tools.distill.permission !== undefined) {
Expand Down Expand Up @@ -505,6 +517,7 @@ const defaultConfig: PluginConfig = {
nudgeFrequency: 10,
protectedTools: [...DEFAULT_PROTECTED_TOOLS],
contextLimit: 100000,
textPartModels: ["antigravity-claude"],
},
distill: {
permission: "allow",
Expand Down Expand Up @@ -684,6 +697,7 @@ function mergeTools(
]),
],
contextLimit: override.settings?.contextLimit ?? base.settings.contextLimit,
textPartModels: override.settings?.textPartModels ?? base.settings.textPartModels,
},
distill: {
permission: override.distill?.permission ?? base.distill.permission,
Expand Down Expand Up @@ -724,6 +738,7 @@ function deepCloneConfig(config: PluginConfig): PluginConfig {
settings: {
...config.tools.settings,
protectedTools: [...config.tools.settings.protectedTools],
textPartModels: [...config.tools.settings.textPartModels],
},
distill: { ...config.tools.distill },
compress: { ...config.tools.compress },
Expand Down
10 changes: 8 additions & 2 deletions lib/messages/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,13 +253,19 @@ export const insertPruneToolContext = (

// When following a user message, append a synthetic text part since models like Claude
// expect assistant turns to start with reasoning parts which cannot be easily faked.
// For models listed in textPartModels, always use text parts to avoid tool pairing issues.
// For all other cases, append a synthetic tool part to the last message which works
// across all models without disrupting their behavior.
if (lastNonIgnoredMessage.info.role === "user") {
const modelID = userInfo.model?.modelID || ""
const lowerModelID = modelID.toLowerCase()
const useTextPart = config.tools.settings.textPartModels.some(
(pattern) => lowerModelID.includes(pattern.toLowerCase()),
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pattern.toLowerCase() assumes every entry in config.tools.settings.textPartModels is a string. Even with config validation, it would be safer to guard here (e.g., skip non-string patterns) to avoid a runtime crash on malformed configs.

Suggested change
(pattern) => lowerModelID.includes(pattern.toLowerCase()),
(pattern) =>
typeof pattern === "string" &&
lowerModelID.includes(pattern.toLowerCase()),

Copilot uses AI. Check for mistakes.
)

if (lastNonIgnoredMessage.info.role === "user" || useTextPart) {
const textPart = createSyntheticTextPart(lastNonIgnoredMessage, combinedContent)
lastNonIgnoredMessage.parts.push(textPart)
} else {
const modelID = userInfo.model?.modelID || ""
const toolPart = createSyntheticToolPart(lastNonIgnoredMessage, combinedContent, modelID)
lastNonIgnoredMessage.parts.push(toolPart)
}
Expand Down
36 changes: 18 additions & 18 deletions lib/messages/prune.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,14 @@ export const prune = (
}

const pruneFullTool = (state: SessionState, logger: Logger, messages: WithParts[]): void => {
const messagesToRemove: string[] = []
let prunedCount = 0

for (const msg of messages) {
if (isMessageCompacted(state, msg)) {
continue
}

const parts = Array.isArray(msg.parts) ? msg.parts : []
const partsToRemove: string[] = []

for (const part of parts) {
if (part.type !== "tool") {
Expand All @@ -45,26 +44,27 @@ const pruneFullTool = (state: SessionState, logger: Logger, messages: WithParts[
continue
}

partsToRemove.push(part.callID)
}

if (partsToRemove.length === 0) {
continue
}

msg.parts = parts.filter(
(part) => part.type !== "tool" || !partsToRemove.includes(part.callID),
)
// Instead of removing the tool part entirely (which breaks Claude's
// tool_use/tool_result pairing in VALIDATED mode), replace the content
// with a placeholder. This preserves the tool part structure so the
// model-level conversion still generates matched functionCall/functionResponse pairs.
if (part.state?.input && typeof part.state.input === "object") {
for (const key of Object.keys(part.state.input)) {
if (typeof part.state.input[key] === "string") {
part.state.input[key] = PRUNED_TOOL_ERROR_INPUT_REPLACEMENT
}
Comment on lines +51 to +55
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pruneFullTool replaces successful edit/write tool inputs with PRUNED_TOOL_ERROR_INPUT_REPLACEMENT ("failed tool call"), which is misleading when the tool status is completed. Use a neutral replacement string for successful tool inputs (and reserve the error-specific placeholder for errored tool calls).

Copilot uses AI. Check for mistakes.
}
}
if (part.state?.status === "completed") {
part.state.output = PRUNED_TOOL_OUTPUT_REPLACEMENT
Comment on lines +58 to +59
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For pruned edit/write tool parts, only status === "completed" outputs are replaced; for status === "error", part.state.error (and any output) are left intact. Previously this code removed the entire tool part, so this change can leave large error payloads in context and reduce token savings. Consider also replacing state.error/state.output with placeholders for non-completed statuses when the tool is being fully pruned.

Suggested change
if (part.state?.status === "completed") {
part.state.output = PRUNED_TOOL_OUTPUT_REPLACEMENT
if (part.state) {
// When fully pruning a tool call, scrub outputs and errors regardless of status
if ("output" in part.state) {
part.state.output = PRUNED_TOOL_OUTPUT_REPLACEMENT
}
if ("error" in part.state) {
part.state.error = PRUNED_TOOL_OUTPUT_REPLACEMENT
}

Copilot uses AI. Check for mistakes.
}

if (msg.parts.length === 0) {
messagesToRemove.push(msg.info.id)
prunedCount++
}
}

if (messagesToRemove.length > 0) {
const result = messages.filter((msg) => !messagesToRemove.includes(msg.info.id))
messages.length = 0
messages.push(...result)
if (prunedCount > 0) {
logger.info(`Pruned content for ${prunedCount} edit/write tool parts`)
}
}

Expand Down
8 changes: 5 additions & 3 deletions lib/tools/prune-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ensureSessionInitialized } from "../state"
import { saveSessionState } from "../state/persistence"
import { calculateTokensSaved, getCurrentParams } from "../strategies/utils"
import { getFilePathsFromParameters, isProtected } from "../protected-file-patterns"
import { buildToolIdList } from "../messages/utils"

// Shared logic for executing prune operations.
export async function executePruneOperation(
Expand Down Expand Up @@ -47,10 +48,11 @@ export async function executePruneOperation(
})
const messages: WithParts[] = messagesResponse.data || messagesResponse

await ensureSessionInitialized(ctx.client, state, sessionId, logger, messages)
await syncToolCache(state, config, logger, messages)
await ensureSessionInitialized(ctx.client, state, sessionId, logger, messages)
await syncToolCache(state, config, logger, messages)
buildToolIdList(state, messages, logger)

const currentParams = getCurrentParams(state, messages, logger)
const currentParams = getCurrentParams(state, messages, logger)
Comment on lines +51 to +55
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new lines are indented inconsistently (extra leading space before await/buildToolIdList), which will likely fail linting/formatting checks and makes the block harder to read. Align indentation with the surrounding code (4 spaces).

Copilot uses AI. Check for mistakes.

const toolIdList = state.toolIdList

Expand Down
Loading