@townco/agent 0.1.102 → 0.1.104
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp-server/adapter.d.ts +10 -0
- package/dist/acp-server/adapter.js +101 -31
- package/dist/definition/index.d.ts +17 -4
- package/dist/definition/index.js +19 -2
- package/dist/runner/agent-runner.d.ts +6 -2
- package/dist/runner/hooks/executor.d.ts +5 -3
- package/dist/runner/hooks/executor.js +190 -150
- package/dist/runner/hooks/loader.d.ts +13 -1
- package/dist/runner/hooks/loader.js +27 -0
- package/dist/runner/hooks/predefined/compaction-tool.d.ts +3 -1
- package/dist/runner/hooks/predefined/compaction-tool.js +38 -2
- package/dist/runner/hooks/predefined/context-validator.d.ts +57 -0
- package/dist/runner/hooks/predefined/context-validator.js +92 -0
- package/dist/runner/hooks/predefined/document-context-extractor/chunk-manager.js +2 -2
- package/dist/runner/hooks/predefined/document-context-extractor/content-extractor.js +29 -0
- package/dist/runner/hooks/predefined/document-context-extractor/relevance-scorer.js +29 -0
- package/dist/runner/hooks/predefined/mid-turn-compaction.d.ts +17 -0
- package/dist/runner/hooks/predefined/mid-turn-compaction.js +224 -0
- package/dist/runner/hooks/predefined/token-utils.d.ts +11 -0
- package/dist/runner/hooks/predefined/token-utils.js +13 -0
- package/dist/runner/hooks/predefined/tool-response-compactor.js +155 -25
- package/dist/runner/hooks/registry.js +2 -0
- package/dist/runner/hooks/types.d.ts +37 -4
- package/dist/runner/index.d.ts +6 -2
- package/dist/runner/langchain/index.js +60 -8
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createLogger } from "../../logger.js";
|
|
2
|
+
import { countToolResultTokens } from "../../utils/token-counter";
|
|
2
3
|
import { getModelContextWindow } from "./constants";
|
|
4
|
+
import { loadHookCallbacks } from "./loader";
|
|
3
5
|
const logger = createLogger("hook-executor");
|
|
4
6
|
/**
|
|
5
7
|
* Hook executor manages hook lifecycle
|
|
@@ -12,7 +14,8 @@ export class HookExecutor {
|
|
|
12
14
|
agentDefinition;
|
|
13
15
|
storage;
|
|
14
16
|
sessionId;
|
|
15
|
-
|
|
17
|
+
agentDir;
|
|
18
|
+
constructor(hooks, model, loadCallback, onNotification, agentDefinition, storage, sessionId, agentDir) {
|
|
16
19
|
this.hooks = hooks;
|
|
17
20
|
this.model = model;
|
|
18
21
|
this.loadCallback = loadCallback;
|
|
@@ -20,6 +23,7 @@ export class HookExecutor {
|
|
|
20
23
|
this.agentDefinition = agentDefinition ?? { model, systemPrompt: null };
|
|
21
24
|
this.storage = storage;
|
|
22
25
|
this.sessionId = sessionId;
|
|
26
|
+
this.agentDir = agentDir;
|
|
23
27
|
}
|
|
24
28
|
/**
|
|
25
29
|
* Emit a notification - sends immediately if callback provided, otherwise collects for batch return
|
|
@@ -58,37 +62,33 @@ export class HookExecutor {
|
|
|
58
62
|
}
|
|
59
63
|
/**
|
|
60
64
|
* Execute a context_size hook
|
|
65
|
+
* The callback is responsible for checking its own threshold and deciding whether to run.
|
|
61
66
|
*/
|
|
62
67
|
async executeContextSizeHook(hook, session, actualInputTokens) {
|
|
68
|
+
// Get callback config from either the callbacks array or the deprecated callback field
|
|
69
|
+
const callbackConfig = hook.callbacks?.[0];
|
|
70
|
+
const callbackName = callbackConfig?.name ?? hook.callback ?? "compaction_tool";
|
|
71
|
+
// Get settings from new callbacks format or deprecated hook.setting
|
|
72
|
+
const callbackSetting = (callbackConfig?.setting ?? hook.setting);
|
|
63
73
|
const maxTokens = getModelContextWindow(this.model);
|
|
64
74
|
const percentage = (actualInputTokens / maxTokens) * 100;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
logger.info(`Context hook triggered: ${actualInputTokens} tokens (${percentage.toFixed(1)}%) exceeds threshold ${threshold}%`);
|
|
75
|
+
logger.info("Executing context_size hook", {
|
|
76
|
+
callback: callbackName,
|
|
77
|
+
tokens: actualInputTokens,
|
|
78
|
+
percentage: `${percentage.toFixed(1)}%`,
|
|
79
|
+
maxTokens,
|
|
80
|
+
});
|
|
73
81
|
const notifications = [];
|
|
74
82
|
const triggeredAt = Date.now();
|
|
75
|
-
|
|
76
|
-
this.emitNotification({
|
|
77
|
-
type: "hook_triggered",
|
|
78
|
-
hookType: "context_size",
|
|
79
|
-
threshold,
|
|
80
|
-
currentPercentage: percentage,
|
|
81
|
-
callback: hook.callback,
|
|
82
|
-
triggeredAt,
|
|
83
|
-
}, notifications);
|
|
83
|
+
const threshold = callbackSetting?.threshold ?? 80;
|
|
84
84
|
try {
|
|
85
85
|
// Load and execute callback
|
|
86
86
|
logger.info("Loading context_size hook callback", {
|
|
87
|
-
callback:
|
|
87
|
+
callback: callbackName,
|
|
88
88
|
});
|
|
89
|
-
const callback = await this.loadCallback(
|
|
89
|
+
const callback = await this.loadCallback(callbackName);
|
|
90
90
|
logger.info("Loaded context_size hook callback, executing...", {
|
|
91
|
-
callback:
|
|
91
|
+
callback: callbackName,
|
|
92
92
|
});
|
|
93
93
|
const hookContext = {
|
|
94
94
|
session,
|
|
@@ -99,21 +99,36 @@ export class HookExecutor {
|
|
|
99
99
|
agent: this.agentDefinition,
|
|
100
100
|
sessionId: this.sessionId,
|
|
101
101
|
storage: this.storage,
|
|
102
|
+
callbackSetting,
|
|
102
103
|
};
|
|
103
104
|
const result = await callback(hookContext);
|
|
105
|
+
// Check if actual compaction happened (action !== "none")
|
|
106
|
+
const action = result.metadata?.action;
|
|
107
|
+
const actuallyCompacted = action && action !== "none";
|
|
104
108
|
logger.info("Context_size hook callback completed", {
|
|
105
|
-
callback:
|
|
109
|
+
callback: callbackName,
|
|
106
110
|
hasNewContextEntry: !!result.newContextEntry,
|
|
111
|
+
actuallyCompacted,
|
|
107
112
|
metadata: result.metadata,
|
|
108
113
|
});
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
// Only emit notifications if actual compaction happened
|
|
115
|
+
if (actuallyCompacted) {
|
|
116
|
+
this.emitNotification({
|
|
117
|
+
type: "hook_triggered",
|
|
118
|
+
hookType: "context_size",
|
|
119
|
+
threshold,
|
|
120
|
+
currentPercentage: percentage,
|
|
121
|
+
callback: callbackName,
|
|
122
|
+
triggeredAt,
|
|
123
|
+
}, notifications);
|
|
124
|
+
this.emitNotification({
|
|
125
|
+
type: "hook_completed",
|
|
126
|
+
hookType: "context_size",
|
|
127
|
+
callback: callbackName,
|
|
128
|
+
metadata: result.metadata,
|
|
129
|
+
completedAt: Date.now(),
|
|
130
|
+
}, notifications);
|
|
131
|
+
}
|
|
117
132
|
return {
|
|
118
133
|
newContextEntry: result.newContextEntry,
|
|
119
134
|
notifications,
|
|
@@ -121,7 +136,7 @@ export class HookExecutor {
|
|
|
121
136
|
}
|
|
122
137
|
catch (error) {
|
|
123
138
|
logger.error("Context_size hook callback failed", {
|
|
124
|
-
callback:
|
|
139
|
+
callback: callbackName,
|
|
125
140
|
error: error instanceof Error ? error.message : String(error),
|
|
126
141
|
stack: error instanceof Error ? error.stack : undefined,
|
|
127
142
|
});
|
|
@@ -129,7 +144,7 @@ export class HookExecutor {
|
|
|
129
144
|
this.emitNotification({
|
|
130
145
|
type: "hook_error",
|
|
131
146
|
hookType: "context_size",
|
|
132
|
-
callback:
|
|
147
|
+
callback: callbackName,
|
|
133
148
|
error: error instanceof Error ? error.message : String(error),
|
|
134
149
|
completedAt: Date.now(),
|
|
135
150
|
}, notifications);
|
|
@@ -141,7 +156,8 @@ export class HookExecutor {
|
|
|
141
156
|
}
|
|
142
157
|
}
|
|
143
158
|
/**
|
|
144
|
-
* Execute tool_response hooks when a tool returns output
|
|
159
|
+
* Execute tool_response hooks when a tool returns output.
|
|
160
|
+
* Chains callbacks in order, passing modified output between them.
|
|
145
161
|
*/
|
|
146
162
|
async executeToolResponseHooks(session, currentContextTokens, toolResponse) {
|
|
147
163
|
logger.info(`Executing tool_response hooks - found ${this.hooks.length} hook(s)`, {
|
|
@@ -150,131 +166,155 @@ export class HookExecutor {
|
|
|
150
166
|
outputTokens: toolResponse.outputTokens,
|
|
151
167
|
});
|
|
152
168
|
const notifications = [];
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const response = { notifications };
|
|
159
|
-
if (result.modifiedOutput !== undefined) {
|
|
160
|
-
response.modifiedOutput = result.modifiedOutput;
|
|
161
|
-
}
|
|
162
|
-
if (result.truncationWarning !== undefined) {
|
|
163
|
-
response.truncationWarning = result.truncationWarning;
|
|
164
|
-
}
|
|
165
|
-
if (result.metadata !== undefined) {
|
|
166
|
-
response.metadata = result.metadata;
|
|
167
|
-
}
|
|
168
|
-
return response;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return { notifications }; // No modifications
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Execute a single tool_response hook
|
|
176
|
-
*/
|
|
177
|
-
async executeToolResponseHook(hook, session, currentContextTokens, toolResponse) {
|
|
169
|
+
let currentOutput = toolResponse.rawOutput;
|
|
170
|
+
let currentTokens = currentContextTokens;
|
|
171
|
+
let combinedMetadata = {};
|
|
172
|
+
let newContextEntry = null;
|
|
173
|
+
let truncationWarning;
|
|
178
174
|
const maxTokens = getModelContextWindow(this.model);
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
// Capture start time and emit hook_triggered BEFORE callback runs
|
|
189
|
-
// This allows the UI to show the loading state immediately
|
|
190
|
-
const triggeredAt = Date.now();
|
|
191
|
-
this.emitNotification({
|
|
192
|
-
type: "hook_triggered",
|
|
193
|
-
hookType: "tool_response",
|
|
194
|
-
threshold,
|
|
195
|
-
currentPercentage: percentage,
|
|
196
|
-
callback: hook.callback,
|
|
197
|
-
triggeredAt,
|
|
198
|
-
toolCallId: toolResponse.toolCallId,
|
|
199
|
-
}, notifications);
|
|
200
|
-
try {
|
|
201
|
-
// Load and execute callback
|
|
202
|
-
const callback = await this.loadCallback(hook.callback);
|
|
203
|
-
// Pass hook settings through requestParams
|
|
204
|
-
const sessionWithSettings = {
|
|
205
|
-
...session,
|
|
206
|
-
requestParams: {
|
|
207
|
-
...session.requestParams,
|
|
208
|
-
hookSettings: hook.setting,
|
|
209
|
-
},
|
|
210
|
-
};
|
|
211
|
-
const hookContext = {
|
|
212
|
-
session: sessionWithSettings,
|
|
213
|
-
currentTokens: currentContextTokens,
|
|
214
|
-
maxTokens,
|
|
215
|
-
percentage,
|
|
216
|
-
model: this.model,
|
|
217
|
-
agent: this.agentDefinition,
|
|
218
|
-
sessionId: this.sessionId,
|
|
219
|
-
storage: this.storage,
|
|
220
|
-
toolResponse,
|
|
221
|
-
};
|
|
222
|
-
const result = await callback(hookContext);
|
|
223
|
-
logger.info("Hook callback result", {
|
|
224
|
-
hasMetadata: !!result.metadata,
|
|
225
|
-
metadataAction: result.metadata?.action,
|
|
226
|
-
hasModifiedOutput: !!result.metadata?.modifiedOutput,
|
|
227
|
-
modifiedOutputType: typeof result.metadata?.modifiedOutput,
|
|
228
|
-
toolCallId: toolResponse.toolCallId,
|
|
175
|
+
for (const hook of this.hooks) {
|
|
176
|
+
if (hook.type !== "tool_response")
|
|
177
|
+
continue;
|
|
178
|
+
// Load all callbacks for this hook
|
|
179
|
+
const loadedCallbacks = await loadHookCallbacks(hook, this.agentDir);
|
|
180
|
+
logger.debug("Loaded callbacks for hook", {
|
|
181
|
+
hookType: hook.type,
|
|
182
|
+
callbackCount: loadedCallbacks.length,
|
|
183
|
+
callbackNames: loadedCallbacks.map((lc) => lc.config.name),
|
|
229
184
|
});
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
185
|
+
// Build mutable tool response that gets updated after each callback
|
|
186
|
+
let currentToolResponse = {
|
|
187
|
+
...toolResponse,
|
|
188
|
+
rawOutput: currentOutput,
|
|
189
|
+
outputTokens: toolResponse.outputTokens,
|
|
190
|
+
};
|
|
191
|
+
// Execute callbacks in order - each callback decides if it should run
|
|
192
|
+
for (const { callback, config } of loadedCallbacks) {
|
|
193
|
+
// Include pending tool response in token calculation
|
|
194
|
+
const effectiveTokens = currentTokens + currentToolResponse.outputTokens;
|
|
195
|
+
const percentage = (effectiveTokens / maxTokens) * 100;
|
|
196
|
+
const triggeredAt = Date.now();
|
|
197
|
+
// Emit hook_triggered notification
|
|
242
198
|
this.emitNotification({
|
|
243
|
-
type: "
|
|
199
|
+
type: "hook_triggered",
|
|
244
200
|
hookType: "tool_response",
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
201
|
+
threshold: 0, // Callbacks decide their own thresholds
|
|
202
|
+
currentPercentage: percentage,
|
|
203
|
+
callback: config.name,
|
|
204
|
+
triggeredAt,
|
|
248
205
|
toolCallId: toolResponse.toolCallId,
|
|
249
206
|
}, notifications);
|
|
250
|
-
|
|
207
|
+
try {
|
|
208
|
+
const hookContext = {
|
|
209
|
+
session,
|
|
210
|
+
currentTokens, // Base context tokens (without tool response)
|
|
211
|
+
maxTokens,
|
|
212
|
+
percentage,
|
|
213
|
+
toolResponseTokens: currentToolResponse.outputTokens,
|
|
214
|
+
model: this.model,
|
|
215
|
+
agent: this.agentDefinition,
|
|
216
|
+
sessionId: this.sessionId,
|
|
217
|
+
storage: this.storage,
|
|
218
|
+
callbackSetting: config.setting, // Pass callback-specific settings
|
|
219
|
+
toolResponse: currentToolResponse, // Pass updated tool response
|
|
220
|
+
};
|
|
221
|
+
// Callback decides whether to run based on its settings and context
|
|
222
|
+
const result = await callback(hookContext);
|
|
223
|
+
logger.debug("Callback result", {
|
|
224
|
+
callbackName: config.name,
|
|
225
|
+
hasMetadata: !!result.metadata,
|
|
226
|
+
metadataAction: result.metadata?.action,
|
|
227
|
+
hasModifiedOutput: !!result.metadata?.modifiedOutput,
|
|
228
|
+
hasNewContextEntry: !!result.newContextEntry,
|
|
229
|
+
});
|
|
230
|
+
// Update tool response if callback modified it
|
|
231
|
+
if (result.metadata?.modifiedOutput) {
|
|
232
|
+
const newOutput = result.metadata.modifiedOutput;
|
|
233
|
+
const newOutputTokens = result.metadata.finalTokens ??
|
|
234
|
+
countToolResultTokens(newOutput);
|
|
235
|
+
currentOutput = newOutput;
|
|
236
|
+
currentToolResponse = {
|
|
237
|
+
...currentToolResponse,
|
|
238
|
+
rawOutput: newOutput,
|
|
239
|
+
outputTokens: newOutputTokens,
|
|
240
|
+
};
|
|
241
|
+
logger.debug("Updated tool response after callback", {
|
|
242
|
+
callbackName: config.name,
|
|
243
|
+
previousTokens: result.metadata.originalTokens,
|
|
244
|
+
newTokens: newOutputTokens,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
// Update context entry if callback created one (e.g., compaction)
|
|
248
|
+
if (result.newContextEntry) {
|
|
249
|
+
newContextEntry = result.newContextEntry;
|
|
250
|
+
// Recalculate tokens after context change
|
|
251
|
+
const contextSize = result.newContextEntry.context_size;
|
|
252
|
+
if (contextSize) {
|
|
253
|
+
currentTokens = contextSize.totalEstimated;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Capture truncation warning
|
|
257
|
+
if (result.metadata?.truncationWarning) {
|
|
258
|
+
truncationWarning = result.metadata.truncationWarning;
|
|
259
|
+
}
|
|
260
|
+
// Merge metadata
|
|
261
|
+
combinedMetadata = { ...combinedMetadata, ...result.metadata };
|
|
262
|
+
// Emit hook_completed notification
|
|
263
|
+
this.emitNotification({
|
|
264
|
+
type: "hook_completed",
|
|
265
|
+
hookType: "tool_response",
|
|
266
|
+
callback: config.name,
|
|
267
|
+
metadata: result.metadata,
|
|
268
|
+
completedAt: Date.now(),
|
|
269
|
+
toolCallId: toolResponse.toolCallId,
|
|
270
|
+
}, notifications);
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
logger.error("Hook callback failed", {
|
|
274
|
+
callbackName: config.name,
|
|
275
|
+
error: error instanceof Error ? error.message : String(error),
|
|
276
|
+
});
|
|
277
|
+
// Emit hook_error notification
|
|
278
|
+
this.emitNotification({
|
|
279
|
+
type: "hook_error",
|
|
280
|
+
hookType: "tool_response",
|
|
281
|
+
callback: config.name,
|
|
282
|
+
error: error instanceof Error ? error.message : String(error),
|
|
283
|
+
completedAt: Date.now(),
|
|
284
|
+
toolCallId: toolResponse.toolCallId,
|
|
285
|
+
}, notifications);
|
|
286
|
+
// Stop on error, but return error in output so agent can handle it
|
|
287
|
+
return {
|
|
288
|
+
modifiedOutput: {
|
|
289
|
+
error: `Tool response processing failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
290
|
+
originalToolName: toolResponse.toolName,
|
|
291
|
+
suggestion: "The tool response was too large to process. Consider using more specific queries or breaking the task into smaller parts.",
|
|
292
|
+
},
|
|
293
|
+
metadata: {
|
|
294
|
+
action: "error",
|
|
295
|
+
failedCallback: config.name,
|
|
296
|
+
error: error instanceof Error ? error.message : String(error),
|
|
297
|
+
},
|
|
298
|
+
notifications,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
251
301
|
}
|
|
252
|
-
// No action was taken - emit completed with no-op metadata
|
|
253
|
-
this.emitNotification({
|
|
254
|
-
type: "hook_completed",
|
|
255
|
-
hookType: "tool_response",
|
|
256
|
-
callback: hook.callback,
|
|
257
|
-
metadata: { action: "no_action_needed" },
|
|
258
|
-
completedAt: Date.now(),
|
|
259
|
-
toolCallId: toolResponse.toolCallId,
|
|
260
|
-
}, notifications);
|
|
261
|
-
return { notifications };
|
|
262
302
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
return { notifications }; // Return notifications even on error
|
|
303
|
+
// Build response
|
|
304
|
+
const response = { notifications };
|
|
305
|
+
// Only include modifiedOutput if it was actually modified
|
|
306
|
+
if (currentOutput !== toolResponse.rawOutput) {
|
|
307
|
+
response.modifiedOutput = currentOutput;
|
|
308
|
+
}
|
|
309
|
+
if (truncationWarning) {
|
|
310
|
+
response.truncationWarning = truncationWarning;
|
|
311
|
+
}
|
|
312
|
+
if (Object.keys(combinedMetadata).length > 0) {
|
|
313
|
+
response.metadata = combinedMetadata;
|
|
314
|
+
}
|
|
315
|
+
if (newContextEntry) {
|
|
316
|
+
response.newContextEntry = newContextEntry;
|
|
278
317
|
}
|
|
318
|
+
return response;
|
|
279
319
|
}
|
|
280
320
|
}
|
|
@@ -1,5 +1,17 @@
|
|
|
1
|
-
import type { HookCallback } from "./types";
|
|
1
|
+
import type { CallbackConfig, HookCallback, HookConfig } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Loaded callback with its configuration
|
|
4
|
+
*/
|
|
5
|
+
export interface LoadedCallback {
|
|
6
|
+
callback: HookCallback;
|
|
7
|
+
config: CallbackConfig;
|
|
8
|
+
}
|
|
2
9
|
/**
|
|
3
10
|
* Load a hook callback from either registry or custom file
|
|
4
11
|
*/
|
|
5
12
|
export declare function loadHookCallback(callbackRef: string, agentDir?: string): Promise<HookCallback>;
|
|
13
|
+
/**
|
|
14
|
+
* Load all callbacks for a hook configuration.
|
|
15
|
+
* Supports both the deprecated single `callback` field and the new `callbacks` array.
|
|
16
|
+
*/
|
|
17
|
+
export declare function loadHookCallbacks(hookConfig: HookConfig, agentDir?: string): Promise<LoadedCallback[]>;
|
|
@@ -47,3 +47,30 @@ export async function loadHookCallback(callbackRef, agentDir) {
|
|
|
47
47
|
throw new Error(`Failed to load custom hook "${callbackRef}": ${error instanceof Error ? error.message : String(error)}`);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Load all callbacks for a hook configuration.
|
|
52
|
+
* Supports both the deprecated single `callback` field and the new `callbacks` array.
|
|
53
|
+
*/
|
|
54
|
+
export async function loadHookCallbacks(hookConfig, agentDir) {
|
|
55
|
+
// Normalize to CallbackConfig array
|
|
56
|
+
const callbackConfigs = hookConfig.callbacks ?? [];
|
|
57
|
+
// Handle deprecated single callback field
|
|
58
|
+
if (hookConfig.callback && callbackConfigs.length === 0) {
|
|
59
|
+
callbackConfigs.push({
|
|
60
|
+
name: hookConfig.callback,
|
|
61
|
+
// Migrate legacy setting to callback-level setting
|
|
62
|
+
setting: hookConfig.setting,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// Load all callbacks in parallel
|
|
66
|
+
const loadedCallbacks = await Promise.all(callbackConfigs.map(async (config) => ({
|
|
67
|
+
callback: await loadHookCallback(config.name, agentDir),
|
|
68
|
+
config,
|
|
69
|
+
})));
|
|
70
|
+
logger.debug("Loaded hook callbacks", {
|
|
71
|
+
hookType: hookConfig.type,
|
|
72
|
+
callbackCount: loadedCallbacks.length,
|
|
73
|
+
callbackNames: callbackConfigs.map((c) => c.name),
|
|
74
|
+
});
|
|
75
|
+
return loadedCallbacks;
|
|
76
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type HookCallback } from "../types";
|
|
2
2
|
/**
|
|
3
3
|
* Compaction tool that uses an LLM to summarize conversation history
|
|
4
|
-
* when context size reaches the configured threshold
|
|
4
|
+
* when context size reaches the configured threshold.
|
|
5
|
+
*
|
|
6
|
+
* This callback checks its own threshold setting to decide whether to run.
|
|
5
7
|
*/
|
|
6
8
|
export declare const compactionTool: HookCallback;
|
|
@@ -2,16 +2,52 @@ import { ChatAnthropic } from "@langchain/anthropic";
|
|
|
2
2
|
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
|
|
3
3
|
import { createLogger } from "../../../logger.js";
|
|
4
4
|
import { createContextEntry, createFullMessageEntry, } from "../types";
|
|
5
|
+
import { applyTokenPadding } from "./token-utils.js";
|
|
5
6
|
const logger = createLogger("compaction-tool");
|
|
6
7
|
/**
|
|
7
8
|
* Compaction tool that uses an LLM to summarize conversation history
|
|
8
|
-
* when context size reaches the configured threshold
|
|
9
|
+
* when context size reaches the configured threshold.
|
|
10
|
+
*
|
|
11
|
+
* This callback checks its own threshold setting to decide whether to run.
|
|
9
12
|
*/
|
|
10
13
|
export const compactionTool = async (ctx) => {
|
|
14
|
+
// Read settings from callbackSetting
|
|
15
|
+
const settings = ctx.callbackSetting;
|
|
16
|
+
const threshold = settings?.threshold ?? 80;
|
|
17
|
+
// Calculate effective token usage including pending tool response if present
|
|
18
|
+
const toolResponseTokens = ctx.toolResponse?.outputTokens ?? 0;
|
|
19
|
+
const estimatedTokens = ctx.currentTokens + toolResponseTokens;
|
|
20
|
+
// Apply 10% padding to account for token estimation inaccuracies
|
|
21
|
+
const paddedTokens = applyTokenPadding(estimatedTokens);
|
|
22
|
+
const effectivePercentage = (paddedTokens / ctx.maxTokens) * 100;
|
|
23
|
+
// Check if we should run based on context percentage (including tool response)
|
|
24
|
+
if (effectivePercentage < threshold) {
|
|
25
|
+
logger.debug("Context below threshold, skipping compaction", {
|
|
26
|
+
currentTokens: ctx.currentTokens,
|
|
27
|
+
toolResponseTokens,
|
|
28
|
+
estimatedTokens,
|
|
29
|
+
paddedTokens,
|
|
30
|
+
effectivePercentage: effectivePercentage.toFixed(2),
|
|
31
|
+
threshold,
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
newContextEntry: null,
|
|
35
|
+
metadata: {
|
|
36
|
+
action: "none",
|
|
37
|
+
reason: "below_threshold",
|
|
38
|
+
percentage: effectivePercentage,
|
|
39
|
+
threshold,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
11
43
|
logger.info("Compaction tool triggered", {
|
|
12
44
|
currentTokens: ctx.currentTokens,
|
|
45
|
+
toolResponseTokens,
|
|
46
|
+
estimatedTokens,
|
|
47
|
+
paddedTokens,
|
|
13
48
|
maxTokens: ctx.maxTokens,
|
|
14
|
-
|
|
49
|
+
effectivePercentage: `${effectivePercentage.toFixed(2)}%`,
|
|
50
|
+
threshold,
|
|
15
51
|
contextEntries: ctx.session.context.length,
|
|
16
52
|
totalMessages: ctx.session.messages.length,
|
|
17
53
|
model: ctx.model,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context validation utilities for ensuring LLM calls don't exceed context limits.
|
|
3
|
+
*
|
|
4
|
+
* Key principle: Before passing content to ANY LLM, validate it's within
|
|
5
|
+
* the model's context limit minus a safety buffer.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Result of context validation
|
|
9
|
+
*/
|
|
10
|
+
export interface ValidationResult {
|
|
11
|
+
/** Whether the content fits within the allowed context size */
|
|
12
|
+
isValid: boolean;
|
|
13
|
+
/** Total tokens that would be used (current + new content) */
|
|
14
|
+
totalTokens: number;
|
|
15
|
+
/** Maximum allowed tokens (modelLimit * (1 - bufferPercent)) */
|
|
16
|
+
maxAllowedTokens: number;
|
|
17
|
+
/** Model's full context window size */
|
|
18
|
+
modelContextWindow: number;
|
|
19
|
+
/** How many tokens over the limit (undefined if within limits) */
|
|
20
|
+
excess?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Default buffer percentage (10%)
|
|
24
|
+
* Accounts for tokenizer estimation differences and API overhead
|
|
25
|
+
*/
|
|
26
|
+
export declare const DEFAULT_BUFFER_PERCENT = 0.1;
|
|
27
|
+
/**
|
|
28
|
+
* Validates whether adding new content would exceed the model's context limit.
|
|
29
|
+
*
|
|
30
|
+
* @param contentTokens - Tokens in the new content to be added
|
|
31
|
+
* @param currentContextTokens - Tokens already in the context
|
|
32
|
+
* @param modelContextWindow - Model's full context window size
|
|
33
|
+
* @param bufferPercent - Safety buffer as a percentage (default 10%)
|
|
34
|
+
* @returns Validation result indicating if content fits
|
|
35
|
+
*/
|
|
36
|
+
export declare function validateContextFits(contentTokens: number, currentContextTokens: number, modelContextWindow: number, bufferPercent?: number): ValidationResult;
|
|
37
|
+
/**
|
|
38
|
+
* Validates whether a prompt string fits within a model's context limit.
|
|
39
|
+
* Convenience function that counts tokens from the prompt string.
|
|
40
|
+
*
|
|
41
|
+
* @param prompt - The prompt string to validate
|
|
42
|
+
* @param modelName - Name of the model to get context window for
|
|
43
|
+
* @param bufferPercent - Safety buffer as a percentage (default 10%)
|
|
44
|
+
* @returns Validation result indicating if prompt fits
|
|
45
|
+
*/
|
|
46
|
+
export declare function validatePromptFits(prompt: string, modelName: string, bufferPercent?: number): ValidationResult;
|
|
47
|
+
/**
|
|
48
|
+
* Checks if an error is a context overflow error from the Anthropic API.
|
|
49
|
+
*
|
|
50
|
+
* @param error - The error to check
|
|
51
|
+
* @returns true if the error indicates context overflow
|
|
52
|
+
*/
|
|
53
|
+
export declare function isContextOverflowError(error: unknown): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Logs validation result with appropriate severity
|
|
56
|
+
*/
|
|
57
|
+
export declare function logValidationResult(result: ValidationResult, context: string): void;
|