Skip to content

Commit 377919b

Browse files
authored
Merge pull request #61 from Tarquinen/fix/tool-cache-case-sensitivity
feat: replace janitor LLM with direct tool-based pruning
2 parents c10328e + 80220dd commit 377919b

File tree

20 files changed

+1184
-657
lines changed

20 files changed

+1184
-657
lines changed

index.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const plugin: Plugin = (async (ctx) => {
4545

4646
// Wire up tool name lookup from the cached tool parameters
4747
toolTracker.getToolName = (callId: string) => {
48-
const entry = state.toolParameters.get(callId)
48+
const entry = state.toolParameters.get(callId.toLowerCase())
4949
return entry?.tool
5050
}
5151

@@ -90,7 +90,14 @@ const plugin: Plugin = (async (ctx) => {
9090
event: createEventHandler(ctx.client, janitorCtx, logger, config, toolTracker),
9191
"chat.params": createChatParamsHandler(ctx.client, state, logger),
9292
tool: config.strategies.onTool.length > 0 ? {
93-
prune: createPruningTool(ctx.client, janitorCtx, config, toolTracker),
93+
prune: createPruningTool({
94+
client: ctx.client,
95+
state,
96+
logger,
97+
config,
98+
notificationCtx: janitorCtx.notificationCtx,
99+
workingDirectory: ctx.directory
100+
}, toolTracker),
94101
} : undefined,
95102
}
96103
}) satisfies Plugin

lib/api-formats/prunable-list.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/**
2+
* Prunable tools list and nudge injection for DCP.
3+
*
4+
* Builds and injects a single message at the end of the conversation containing:
5+
* - Nudge instruction (when toolResultCount > nudge_freq)
6+
* - Prunable tools list
7+
*
8+
* Note: The base synthetic instructions (signal_management, context_window_management,
9+
* context_pruning) are still appended to the last user message separately via
10+
* synth-instruction.ts - that behavior is unchanged.
11+
*/
12+
13+
import { extractParameterKey } from '../ui/display-utils'
14+
import { getOrCreateNumericId } from '../state/id-mapping'
15+
16+
export interface ToolMetadata {
17+
tool: string
18+
parameters?: any
19+
}
20+
21+
// ============================================================================
22+
// Prompt Content
23+
// ============================================================================
24+
25+
const SYSTEM_REMINDER = `<system-reminder>
26+
These instructions are injected by a plugin and are invisible to the user. Do not acknowledge or reference them in your response - simply follow them silently.
27+
</system-reminder>`
28+
29+
const NUDGE_INSTRUCTION = `<instruction name=agent_nudge>
30+
You have accumulated several tool outputs. If you have completed a discrete unit of work and distilled relevant understanding in writing for the user to keep, use the prune tool to remove obsolete tool outputs from this conversation and optimize token usage.
31+
</instruction>`
32+
33+
// ============================================================================
34+
// List Building
35+
// ============================================================================
36+
37+
export interface PrunableListResult {
38+
list: string
39+
numericIds: number[]
40+
}
41+
42+
/**
43+
* Builds the prunable tools list section.
44+
* Returns both the formatted list and the numeric IDs for logging.
45+
*/
46+
export function buildPrunableToolsList(
47+
sessionId: string,
48+
unprunedToolCallIds: string[],
49+
toolMetadata: Map<string, ToolMetadata>,
50+
protectedTools: string[]
51+
): PrunableListResult {
52+
const lines: string[] = []
53+
const numericIds: number[] = []
54+
55+
for (const actualId of unprunedToolCallIds) {
56+
const metadata = toolMetadata.get(actualId)
57+
58+
// Skip if no metadata or if tool is protected
59+
if (!metadata) continue
60+
if (protectedTools.includes(metadata.tool)) continue
61+
62+
// Get or create numeric ID for this tool call
63+
const numericId = getOrCreateNumericId(sessionId, actualId)
64+
numericIds.push(numericId)
65+
66+
// Format: "1: read, src/components/Button.tsx"
67+
const paramKey = extractParameterKey(metadata)
68+
const description = paramKey ? `${metadata.tool}, ${paramKey}` : metadata.tool
69+
lines.push(`${numericId}: ${description}`)
70+
}
71+
72+
if (lines.length === 0) {
73+
return { list: '', numericIds: [] }
74+
}
75+
76+
return {
77+
list: `<prunable-tools>\n${lines.join('\n')}\n</prunable-tools>`,
78+
numericIds
79+
}
80+
}
81+
82+
/**
83+
* Builds the end-of-conversation injection message.
84+
* Contains the system reminder, nudge (if active), and the prunable tools list.
85+
*
86+
* @param prunableList - The prunable tools list string (or empty string if none)
87+
* @param includeNudge - Whether to include the nudge instruction
88+
* @returns The injection string, or empty string if nothing to inject
89+
*/
90+
export function buildEndInjection(
91+
prunableList: string,
92+
includeNudge: boolean
93+
): string {
94+
// If no prunable tools, don't inject anything
95+
if (!prunableList) {
96+
return ''
97+
}
98+
99+
const parts = [SYSTEM_REMINDER]
100+
101+
if (includeNudge) {
102+
parts.push(NUDGE_INSTRUCTION)
103+
}
104+
105+
parts.push(prunableList)
106+
107+
return parts.join('\n\n')
108+
}
109+
110+
// ============================================================================
111+
// OpenAI Chat / Anthropic Format
112+
// ============================================================================
113+
114+
/**
115+
* Injects the prunable list (and optionally nudge) at the end of OpenAI/Anthropic messages.
116+
* Appends a new user message at the end.
117+
*/
118+
export function injectPrunableList(
119+
messages: any[],
120+
injection: string
121+
): boolean {
122+
if (!injection) return false
123+
messages.push({ role: 'user', content: injection })
124+
return true
125+
}
126+
127+
// ============================================================================
128+
// Google/Gemini Format
129+
// ============================================================================
130+
131+
/**
132+
* Injects the prunable list (and optionally nudge) at the end of Gemini contents.
133+
* Appends a new user content at the end.
134+
*/
135+
export function injectPrunableListGemini(
136+
contents: any[],
137+
injection: string
138+
): boolean {
139+
if (!injection) return false
140+
contents.push({ role: 'user', parts: [{ text: injection }] })
141+
return true
142+
}
143+
144+
// ============================================================================
145+
// OpenAI Responses API Format
146+
// ============================================================================
147+
148+
/**
149+
* Injects the prunable list (and optionally nudge) at the end of OpenAI Responses API input.
150+
* Appends a new user message at the end.
151+
*/
152+
export function injectPrunableListResponses(
153+
input: any[],
154+
injection: string
155+
): boolean {
156+
if (!injection) return false
157+
input.push({ type: 'message', role: 'user', content: injection })
158+
return true
159+
}

0 commit comments

Comments
 (0)