@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.
Files changed (27) hide show
  1. package/dist/acp-server/adapter.d.ts +10 -0
  2. package/dist/acp-server/adapter.js +101 -31
  3. package/dist/definition/index.d.ts +17 -4
  4. package/dist/definition/index.js +19 -2
  5. package/dist/runner/agent-runner.d.ts +6 -2
  6. package/dist/runner/hooks/executor.d.ts +5 -3
  7. package/dist/runner/hooks/executor.js +190 -150
  8. package/dist/runner/hooks/loader.d.ts +13 -1
  9. package/dist/runner/hooks/loader.js +27 -0
  10. package/dist/runner/hooks/predefined/compaction-tool.d.ts +3 -1
  11. package/dist/runner/hooks/predefined/compaction-tool.js +38 -2
  12. package/dist/runner/hooks/predefined/context-validator.d.ts +57 -0
  13. package/dist/runner/hooks/predefined/context-validator.js +92 -0
  14. package/dist/runner/hooks/predefined/document-context-extractor/chunk-manager.js +2 -2
  15. package/dist/runner/hooks/predefined/document-context-extractor/content-extractor.js +29 -0
  16. package/dist/runner/hooks/predefined/document-context-extractor/relevance-scorer.js +29 -0
  17. package/dist/runner/hooks/predefined/mid-turn-compaction.d.ts +17 -0
  18. package/dist/runner/hooks/predefined/mid-turn-compaction.js +224 -0
  19. package/dist/runner/hooks/predefined/token-utils.d.ts +11 -0
  20. package/dist/runner/hooks/predefined/token-utils.js +13 -0
  21. package/dist/runner/hooks/predefined/tool-response-compactor.js +155 -25
  22. package/dist/runner/hooks/registry.js +2 -0
  23. package/dist/runner/hooks/types.d.ts +37 -4
  24. package/dist/runner/index.d.ts +6 -2
  25. package/dist/runner/langchain/index.js +60 -8
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. 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
- constructor(hooks, model, loadCallback, onNotification, agentDefinition, storage, sessionId) {
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
- // Default threshold is 95%
66
- const threshold = hook.setting?.threshold ?? 95;
67
- // Check if threshold exceeded
68
- if (percentage < threshold) {
69
- logger.info(`Context hook not triggered: ${actualInputTokens} tokens (${percentage.toFixed(1)}%) < threshold ${threshold}%`);
70
- return null; // No action needed
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
- // Notify that hook is triggered - sent immediately via callback if available
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: hook.callback,
87
+ callback: callbackName,
88
88
  });
89
- const callback = await this.loadCallback(hook.callback);
89
+ const callback = await this.loadCallback(callbackName);
90
90
  logger.info("Loaded context_size hook callback, executing...", {
91
- callback: hook.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: hook.callback,
109
+ callback: callbackName,
106
110
  hasNewContextEntry: !!result.newContextEntry,
111
+ actuallyCompacted,
107
112
  metadata: result.metadata,
108
113
  });
109
- // Notify completion
110
- this.emitNotification({
111
- type: "hook_completed",
112
- hookType: "context_size",
113
- callback: hook.callback,
114
- metadata: result.metadata,
115
- completedAt: Date.now(),
116
- }, notifications);
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: hook.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: hook.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
- for (const hook of this.hooks) {
154
- if (hook.type === "tool_response") {
155
- const result = await this.executeToolResponseHook(hook, session, currentContextTokens, toolResponse);
156
- if (result) {
157
- notifications.push(...result.notifications);
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 percentage = (currentContextTokens / maxTokens) * 100;
180
- const notifications = [];
181
- // Get threshold from settings
182
- const settings = hook.setting;
183
- // For notifications, calculate a percentage based on maxTokensSize relative to maxTokens
184
- // Default to 20000 tokens, which we'll convert to a percentage for display
185
- const maxTokensSize = settings?.maxTokensSize ?? 20000;
186
- // Calculate approximate percentage: maxTokensSize / maxTokens * 100
187
- const threshold = Math.min(100, Math.round((maxTokensSize / maxTokens) * 100));
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
- // Extract modified output and warnings from metadata
231
- if (result.metadata?.modifiedOutput) {
232
- // Hook took action - emit completed notification
233
- const response = { notifications };
234
- response.modifiedOutput = result.metadata.modifiedOutput;
235
- if (result.metadata.truncationWarning) {
236
- response.truncationWarning = result.metadata
237
- .truncationWarning;
238
- }
239
- // Include full metadata for persistence
240
- response.metadata = result.metadata;
241
- // Notify completion
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: "hook_completed",
199
+ type: "hook_triggered",
244
200
  hookType: "tool_response",
245
- callback: hook.callback,
246
- metadata: result.metadata,
247
- completedAt: Date.now(),
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
- return response;
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
- catch (error) {
264
- // Notify error
265
- this.emitNotification({
266
- type: "hook_error",
267
- hookType: "tool_response",
268
- callback: hook.callback,
269
- error: error instanceof Error ? error.message : String(error),
270
- completedAt: Date.now(),
271
- toolCallId: toolResponse.toolCallId,
272
- }, notifications);
273
- logger.error("Tool response hook execution failed", {
274
- callback: hook.callback,
275
- error: error instanceof Error ? error.message : String(error),
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
- percentage: `${ctx.percentage.toFixed(2)}%`,
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;