Skip to content

Commit cd16628

Browse files
feat: intra response synth instruction injection
cleanup fix
1 parent ea51b01 commit cd16628

File tree

3 files changed

+94
-2
lines changed

3 files changed

+94
-2
lines changed

index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Logger } from "./lib/logger"
55
import { Janitor, type SessionStats } from "./lib/janitor"
66
import { checkForUpdates } from "./lib/version-checker"
77
import { loadPrompt } from "./lib/prompt"
8-
import { injectSynthInstruction } from "./lib/synth-instruction"
8+
import { injectSynthInstruction, createToolResultTracker, maybeInjectToolResultNudge } from "./lib/synth-instruction"
99

1010
async function isSubagentSession(client: any, sessionID: string): Promise<boolean> {
1111
try {
@@ -18,6 +18,7 @@ async function isSubagentSession(client: any, sessionID: string): Promise<boolea
1818

1919
const TOOL_SYNTH_INSTRUCTION = loadPrompt("synthetic")
2020
const TOOL_CONTEXT_PRUNING_DESCRIPTION = loadPrompt("context_pruning")
21+
const TOOL_TOOL_PART_NUDGE = loadPrompt("tool-result-nudge")
2122

2223
const plugin: Plugin = (async (ctx) => {
2324
const { config, migrations } = getConfig(ctx)
@@ -36,6 +37,7 @@ const plugin: Plugin = (async (ctx) => {
3637
const toolParametersCache = new Map<string, any>()
3738
const modelCache = new Map<string, { providerID: string; modelID: string }>()
3839
const janitor = new Janitor(ctx.client, prunedIdsState, statsState, logger, toolParametersCache, config.protectedTools, modelCache, config.model, config.showModelErrorToasts, config.strictModelSelection, config.pruning_summary, ctx.directory)
40+
const toolResultTracker = createToolResultTracker()
3941

4042
const cacheToolParameters = (messages: any[]) => {
4143
for (const message of messages) {
@@ -75,6 +77,16 @@ const plugin: Plugin = (async (ctx) => {
7577

7678
let modified = false
7779

80+
// Inject periodic nudge every 5 tool results
81+
if (config.strategies.onTool.length > 0) {
82+
if (maybeInjectToolResultNudge(body.messages, toolResultTracker, TOOL_TOOL_PART_NUDGE)) {
83+
logger.debug("fetch", "Injected tool-result nudge", {
84+
toolResultCount: toolResultTracker.toolResultCount
85+
})
86+
modified = true
87+
}
88+
}
89+
7890
// Inject synthInstruction for the context_pruning tool
7991
if (config.strategies.onTool.length > 0) {
8092
if (injectSynthInstruction(body.messages, TOOL_SYNTH_INSTRUCTION)) {

lib/prompts/tool-result-nudge.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<instruction name=agent_nudge>
2+
You have accumulated several tool outputs. If you have completed a discrete unit of work and distilled relevant understanding in writting for the user to keep, use the context_pruning tool to remove obsolete tool outputs from this conversation and optimize token usage.
3+
</instruction>

lib/synth-instruction.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,73 @@
1+
export interface ToolResultTracker {
2+
seenToolResultIds: Set<string>
3+
toolResultCount: number
4+
}
5+
6+
export function createToolResultTracker(): ToolResultTracker {
7+
return {
8+
seenToolResultIds: new Set(),
9+
toolResultCount: 0
10+
}
11+
}
12+
13+
function countNewToolResults(messages: any[], tracker: ToolResultTracker): number {
14+
let newCount = 0
15+
16+
for (const m of messages) {
17+
if (m.role === 'tool' && m.tool_call_id) {
18+
const id = String(m.tool_call_id).toLowerCase()
19+
if (!tracker.seenToolResultIds.has(id)) {
20+
tracker.seenToolResultIds.add(id)
21+
newCount++
22+
}
23+
} else if (m.role === 'user' && Array.isArray(m.content)) {
24+
for (const part of m.content) {
25+
if (part.type === 'tool_result' && part.tool_use_id) {
26+
const id = String(part.tool_use_id).toLowerCase()
27+
if (!tracker.seenToolResultIds.has(id)) {
28+
tracker.seenToolResultIds.add(id)
29+
newCount++
30+
}
31+
}
32+
}
33+
}
34+
}
35+
36+
tracker.toolResultCount += newCount
37+
return newCount
38+
}
39+
40+
/**
41+
* Counts new tool results and injects nudge instruction every 5th tool result.
42+
* Returns true if injection happened.
43+
*/
44+
export function maybeInjectToolResultNudge(
45+
messages: any[],
46+
tracker: ToolResultTracker,
47+
nudgeText: string
48+
): boolean {
49+
const prevCount = tracker.toolResultCount
50+
const newCount = countNewToolResults(messages, tracker)
51+
52+
if (newCount > 0) {
53+
// Check if we crossed a multiple of 5
54+
const prevBucket = Math.floor(prevCount / 5)
55+
const newBucket = Math.floor(tracker.toolResultCount / 5)
56+
if (newBucket > prevBucket) {
57+
// Inject at the END of messages so it's in immediate context
58+
return injectNudgeAtEnd(messages, nudgeText)
59+
}
60+
}
61+
return false
62+
}
63+
164
export function isIgnoredUserMessage(msg: any): boolean {
265
if (!msg || msg.role !== 'user') {
366
return false
467
}
568

6-
if (msg.ignored || msg.info?.ignored) {
69+
// Skip ignored or synthetic messages
70+
if (msg.ignored || msg.info?.ignored || msg.synthetic) {
771
return true
872
}
973

@@ -17,6 +81,19 @@ export function isIgnoredUserMessage(msg: any): boolean {
1781
return false
1882
}
1983

84+
/**
85+
* Injects a nudge message at the END of the messages array as a new user message.
86+
* This ensures it's in the model's immediate context, not buried in old messages.
87+
*/
88+
export function injectNudgeAtEnd(messages: any[], nudgeText: string): boolean {
89+
messages.push({
90+
role: 'user',
91+
content: nudgeText,
92+
synthetic: true
93+
})
94+
return true
95+
}
96+
2097
export function injectSynthInstruction(messages: any[], instruction: string): boolean {
2198
// Find the last user message that is not ignored
2299
for (let i = messages.length - 1; i >= 0; i--) {

0 commit comments

Comments
 (0)