@compilr-dev/agents 0.3.22 → 0.3.24

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/agent.js CHANGED
@@ -1700,15 +1700,23 @@ export class Agent {
1700
1700
  const filterSet = new Set(options.toolFilter);
1701
1701
  tools = tools.filter((tool) => filterSet.has(tool.name));
1702
1702
  }
1703
- // Run beforeLLM hooks (can modify messages and tools)
1703
+ // Run beforeLLM hooks (can modify messages, tools, and system prompt)
1704
1704
  if (this.hooksManager) {
1705
1705
  const llmHookResult = await this.hooksManager.runBeforeLLM({
1706
1706
  ...hookContext,
1707
1707
  messages,
1708
1708
  tools,
1709
+ systemPrompt: systemContent,
1709
1710
  });
1710
1711
  messages = llmHookResult.messages;
1711
1712
  tools = llmHookResult.tools;
1713
+ // If a hook modified the system prompt, update messages[0]
1714
+ if (llmHookResult.systemPrompt !== systemContent) {
1715
+ systemContent = llmHookResult.systemPrompt;
1716
+ if (messages.length > 0 && messages[0].role === 'system') {
1717
+ messages[0] = { role: 'system', content: systemContent };
1718
+ }
1719
+ }
1712
1720
  }
1713
1721
  // Call LLM
1714
1722
  emit({ type: 'llm_start' });
@@ -1772,6 +1780,7 @@ export class Agent {
1772
1780
  ...hookContext,
1773
1781
  messages,
1774
1782
  tools,
1783
+ systemPrompt: systemContent,
1775
1784
  text,
1776
1785
  toolUses,
1777
1786
  usage: usage
@@ -102,6 +102,7 @@ export declare class HooksManager {
102
102
  runBeforeLLM(context: LLMHookContext): Promise<{
103
103
  messages: Message[];
104
104
  tools: ToolDefinition[];
105
+ systemPrompt: string;
105
106
  }>;
106
107
  /**
107
108
  * Run after:llm hooks
@@ -330,6 +330,7 @@ export class HooksManager {
330
330
  async runBeforeLLM(context) {
331
331
  let messages = context.messages;
332
332
  let tools = context.tools;
333
+ let systemPrompt = context.systemPrompt;
333
334
  for (const registered of this.beforeLLMHooks) {
334
335
  const start = Date.now();
335
336
  try {
@@ -339,7 +340,7 @@ export class HooksManager {
339
340
  hookId: registered.id,
340
341
  hookName: registered.name,
341
342
  });
342
- const result = await registered.hook({ ...context, messages, tools });
343
+ const result = await registered.hook({ ...context, messages, tools, systemPrompt });
343
344
  this.emitEvent({
344
345
  type: 'hook:completed',
345
346
  hookType: 'beforeLLM',
@@ -353,6 +354,8 @@ export class HooksManager {
353
354
  messages = hookResult.messages;
354
355
  if (hookResult.tools)
355
356
  tools = hookResult.tools;
357
+ if (hookResult.systemPrompt !== undefined)
358
+ systemPrompt = hookResult.systemPrompt;
356
359
  }
357
360
  }
358
361
  catch (error) {
@@ -365,7 +368,7 @@ export class HooksManager {
365
368
  });
366
369
  }
367
370
  }
368
- return { messages, tools };
371
+ return { messages, tools, systemPrompt };
369
372
  }
370
373
  /**
371
374
  * Run after:llm hooks
@@ -94,6 +94,11 @@ export interface LLMHookContext extends HookContext {
94
94
  * Tool definitions available to LLM
95
95
  */
96
96
  tools: ToolDefinition[];
97
+ /**
98
+ * Current system prompt (fully assembled, including anchors).
99
+ * Hooks can read this and return a modified version in BeforeLLMHookResult.
100
+ */
101
+ systemPrompt: string;
97
102
  }
98
103
  /**
99
104
  * Result from before:llm hook that modifies messages or tools
@@ -107,6 +112,11 @@ export interface BeforeLLMHookResult {
107
112
  * Modified tools (optional, original used if not provided)
108
113
  */
109
114
  tools?: ToolDefinition[];
115
+ /**
116
+ * Modified system prompt (optional, original used if not provided).
117
+ * When returned, messages[0] (system message) is updated automatically.
118
+ */
119
+ systemPrompt?: string;
110
120
  }
111
121
  /**
112
122
  * Hook called before LLM call.
@@ -45,6 +45,19 @@ export interface ClaudeProviderConfig {
45
45
  * @default true
46
46
  */
47
47
  enablePromptCaching?: boolean;
48
+ /**
49
+ * Enable token-efficient tool use (Anthropic beta).
50
+ * Sends compact tool representation, reducing input tokens.
51
+ * No-op for Claude 4+ (already default).
52
+ * @default true
53
+ */
54
+ enableTokenEfficientTools?: boolean;
55
+ /**
56
+ * Optional token estimator function (e.g., tiktoken).
57
+ * When provided, debug payload reports token counts instead of char-based estimates.
58
+ * Fallback: Math.ceil(text.length / 4)
59
+ */
60
+ estimateTokens?: (text: string) => number;
48
61
  }
49
62
  /**
50
63
  * ClaudeProvider implements LLMProvider for Anthropic's Claude API
@@ -55,6 +68,8 @@ export declare class ClaudeProvider implements LLMProvider {
55
68
  private readonly defaultModel;
56
69
  private readonly defaultMaxTokens;
57
70
  private readonly enablePromptCaching;
71
+ private readonly enableTokenEfficientTools;
72
+ private readonly estimateTokensFn;
58
73
  constructor(config: ClaudeProviderConfig);
59
74
  /**
60
75
  * Send messages and stream the response
@@ -64,6 +79,11 @@ export declare class ClaudeProvider implements LLMProvider {
64
79
  * Count tokens in messages using tiktoken (cl100k_base encoding)
65
80
  */
66
81
  countTokens(messages: Message[]): Promise<number>;
82
+ /**
83
+ * Build request options with optional abort signal and beta header.
84
+ * The token-efficient tools beta reduces schema tokenization for older models.
85
+ */
86
+ private buildRequestOptions;
67
87
  /**
68
88
  * Convert our Message format to Anthropic's format
69
89
  */
@@ -30,6 +30,8 @@ export class ClaudeProvider {
30
30
  defaultModel;
31
31
  defaultMaxTokens;
32
32
  enablePromptCaching;
33
+ enableTokenEfficientTools;
34
+ estimateTokensFn;
33
35
  constructor(config) {
34
36
  this.client = new Anthropic({
35
37
  apiKey: config.apiKey,
@@ -38,6 +40,9 @@ export class ClaudeProvider {
38
40
  this.defaultModel = config.model ?? DEFAULT_MODEL;
39
41
  this.defaultMaxTokens = config.maxTokens ?? DEFAULT_MAX_TOKENS;
40
42
  this.enablePromptCaching = config.enablePromptCaching ?? true;
43
+ this.enableTokenEfficientTools = config.enableTokenEfficientTools ?? true;
44
+ this.estimateTokensFn =
45
+ config.estimateTokens ?? ((s) => Math.ceil(s.length / 4));
41
46
  }
42
47
  /**
43
48
  * Send messages and stream the response
@@ -46,11 +51,12 @@ export class ClaudeProvider {
46
51
  const { systemPrompt, anthropicMessages } = this.convertMessages(messages);
47
52
  const tools = this.convertTools(options?.tools);
48
53
  const thinking = this.convertThinking(options?.thinking);
49
- // Calculate payload sizes for debugging (same as gemini-native.ts)
54
+ // Calculate payload sizes for debugging (token estimates)
55
+ const estimate = this.estimateTokensFn;
50
56
  const debugPayload = {
51
- systemChars: systemPrompt.length,
52
- contentsChars: JSON.stringify(anthropicMessages).length,
53
- toolsChars: JSON.stringify(tools).length,
57
+ systemTokens: estimate(systemPrompt),
58
+ contentsTokens: estimate(JSON.stringify(anthropicMessages)),
59
+ toolsTokens: estimate(JSON.stringify(tools)),
54
60
  };
55
61
  try {
56
62
  // Determine if prompt caching is enabled
@@ -74,8 +80,8 @@ export class ClaudeProvider {
74
80
  if (thinking) {
75
81
  Object.assign(params, { thinking });
76
82
  }
77
- // Pass abort signal to SDK for immediate cancellation
78
- const requestOptions = options?.signal ? { signal: options.signal } : undefined;
83
+ // Pass abort signal and optional beta header to SDK
84
+ const requestOptions = this.buildRequestOptions(options?.signal, tools.length > 0);
79
85
  const stream = this.client.messages.stream(params, requestOptions);
80
86
  const model = options?.model ?? this.defaultModel;
81
87
  let currentToolId = '';
@@ -130,6 +136,19 @@ export class ClaudeProvider {
130
136
  countTokens(messages) {
131
137
  return Promise.resolve(countMessageTokens(messages));
132
138
  }
139
+ /**
140
+ * Build request options with optional abort signal and beta header.
141
+ * The token-efficient tools beta reduces schema tokenization for older models.
142
+ */
143
+ buildRequestOptions(signal, hasTools) {
144
+ const needsBeta = this.enableTokenEfficientTools && hasTools;
145
+ if (!signal && !needsBeta)
146
+ return undefined;
147
+ return {
148
+ ...(signal ? { signal } : {}),
149
+ ...(needsBeta ? { headers: { 'anthropic-beta': 'token-efficient-tools-2025-02-19' } } : {}),
150
+ };
151
+ }
133
152
  /**
134
153
  * Convert our Message format to Anthropic's format
135
154
  */
@@ -34,6 +34,8 @@ export interface FireworksProviderConfig {
34
34
  maxTokens?: number;
35
35
  /** Request timeout in milliseconds (default: 120000) */
36
36
  timeout?: number;
37
+ /** Optional token estimator (e.g., tiktoken) for debug payload */
38
+ estimateTokens?: (text: string) => number;
37
39
  }
38
40
  /**
39
41
  * Fireworks AI LLM Provider
@@ -41,6 +41,7 @@ export class FireworksProvider extends OpenAICompatibleProvider {
41
41
  model: config.model ?? DEFAULT_MODEL,
42
42
  maxTokens: config.maxTokens,
43
43
  timeout: config.timeout,
44
+ estimateTokens: config.estimateTokens,
44
45
  };
45
46
  super(baseConfig);
46
47
  this.apiKey = apiKey;
@@ -32,6 +32,12 @@ export interface GeminiNativeProviderConfig {
32
32
  * @default 4096
33
33
  */
34
34
  maxTokens?: number;
35
+ /**
36
+ * Optional token estimator function (e.g., tiktoken).
37
+ * When provided, debug payload reports token counts instead of char-based estimates.
38
+ * Fallback: Math.ceil(text.length / 4)
39
+ */
40
+ estimateTokens?: (text: string) => number;
35
41
  }
36
42
  /**
37
43
  * GeminiNativeProvider implements LLMProvider using the native Google Gen AI SDK
@@ -46,6 +52,7 @@ export declare class GeminiNativeProvider implements LLMProvider {
46
52
  private readonly client;
47
53
  private readonly defaultModel;
48
54
  private readonly defaultMaxTokens;
55
+ private readonly estimateTokensFn;
49
56
  constructor(config: GeminiNativeProviderConfig);
50
57
  /**
51
58
  * Send messages and stream the response
@@ -37,10 +37,13 @@ export class GeminiNativeProvider {
37
37
  client;
38
38
  defaultModel;
39
39
  defaultMaxTokens;
40
+ estimateTokensFn;
40
41
  constructor(config) {
41
42
  this.client = new GoogleGenAI({ apiKey: config.apiKey });
42
43
  this.defaultModel = config.model ?? DEFAULT_MODEL;
43
44
  this.defaultMaxTokens = config.maxTokens ?? DEFAULT_MAX_TOKENS;
45
+ this.estimateTokensFn =
46
+ config.estimateTokens ?? ((s) => Math.ceil(s.length / 4));
44
47
  }
45
48
  /**
46
49
  * Send messages and stream the response
@@ -49,11 +52,12 @@ export class GeminiNativeProvider {
49
52
  const { systemInstruction, contents } = this.convertMessages(messages);
50
53
  const tools = this.convertTools(options?.tools);
51
54
  const model = options?.model ?? this.defaultModel;
52
- // Calculate payload sizes for debugging
55
+ // Calculate payload sizes for debugging (token estimates)
56
+ const estimate = this.estimateTokensFn;
53
57
  const debugPayload = {
54
- systemChars: systemInstruction?.length ?? 0,
55
- contentsChars: JSON.stringify(contents).length,
56
- toolsChars: JSON.stringify(tools).length,
58
+ systemTokens: estimate(systemInstruction ?? ''),
59
+ contentsTokens: estimate(JSON.stringify(contents)),
60
+ toolsTokens: estimate(JSON.stringify(tools)),
57
61
  };
58
62
  try {
59
63
  // Build config
@@ -34,6 +34,8 @@ export interface GroqProviderConfig {
34
34
  maxTokens?: number;
35
35
  /** Request timeout in milliseconds (default: 120000) */
36
36
  timeout?: number;
37
+ /** Optional token estimator (e.g., tiktoken) for debug payload */
38
+ estimateTokens?: (text: string) => number;
37
39
  }
38
40
  /**
39
41
  * Groq LLM Provider
@@ -41,6 +41,7 @@ export class GroqProvider extends OpenAICompatibleProvider {
41
41
  model: config.model ?? DEFAULT_MODEL,
42
42
  maxTokens: config.maxTokens,
43
43
  timeout: config.timeout,
44
+ estimateTokens: config.estimateTokens,
44
45
  };
45
46
  super(baseConfig);
46
47
  this.apiKey = apiKey;
@@ -34,6 +34,8 @@ export interface OllamaProviderConfig {
34
34
  timeout?: number;
35
35
  /** Keep alive duration for model in memory (default: '5m') */
36
36
  keepAlive?: string;
37
+ /** Optional token estimator (e.g., tiktoken) for debug payload */
38
+ estimateTokens?: (text: string) => number;
37
39
  }
38
40
  /**
39
41
  * Ollama LLM Provider
@@ -37,6 +37,7 @@ export class OllamaProvider extends OpenAICompatibleProvider {
37
37
  model: config.model ?? DEFAULT_MODEL,
38
38
  maxTokens: config.maxTokens,
39
39
  timeout: config.timeout,
40
+ estimateTokens: config.estimateTokens,
40
41
  };
41
42
  super(baseConfig);
42
43
  this.keepAlive = config.keepAlive ?? '5m';
@@ -102,6 +102,12 @@ export interface OpenAICompatibleConfig {
102
102
  maxTokens?: number;
103
103
  /** Request timeout in milliseconds (default: 120000) */
104
104
  timeout?: number;
105
+ /**
106
+ * Optional token estimator function (e.g., tiktoken).
107
+ * When provided, debug payload reports token counts instead of char-based estimates.
108
+ * Fallback: Math.ceil(text.length / 4)
109
+ */
110
+ estimateTokens?: (text: string) => number;
105
111
  }
106
112
  /**
107
113
  * Abstract base class for OpenAI-compatible LLM providers
@@ -118,6 +124,7 @@ export declare abstract class OpenAICompatibleProvider implements LLMProvider {
118
124
  protected readonly defaultModel: string;
119
125
  protected readonly defaultMaxTokens: number;
120
126
  protected readonly timeout: number;
127
+ protected readonly estimateTokensFn: (text: string) => number;
121
128
  constructor(config: OpenAICompatibleConfig);
122
129
  /**
123
130
  * Get authentication headers for API requests
@@ -37,11 +37,14 @@ export class OpenAICompatibleProvider {
37
37
  defaultModel;
38
38
  defaultMaxTokens;
39
39
  timeout;
40
+ estimateTokensFn;
40
41
  constructor(config) {
41
42
  this.baseUrl = config.baseUrl;
42
43
  this.defaultModel = config.model;
43
44
  this.defaultMaxTokens = config.maxTokens ?? DEFAULT_MAX_TOKENS;
44
45
  this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
46
+ this.estimateTokensFn =
47
+ config.estimateTokens ?? ((s) => Math.ceil(s.length / 4));
45
48
  }
46
49
  /**
47
50
  * Extract cache statistics from response headers.
@@ -67,14 +70,14 @@ export class OpenAICompatibleProvider {
67
70
  const openaiMessages = this.convertMessages(messages);
68
71
  // Convert tools if provided
69
72
  const tools = options?.tools ? this.convertTools(options.tools) : undefined;
70
- // Calculate payload sizes for debugging
71
- // Note: OpenAI format has system message in messages array, not separate
73
+ // Calculate payload sizes for debugging (token estimates)
74
+ const estimate = this.estimateTokensFn;
72
75
  const systemMsg = openaiMessages.find((m) => m.role === 'system');
73
- const systemChars = systemMsg && typeof systemMsg.content === 'string' ? systemMsg.content.length : 0;
76
+ const systemTokens = systemMsg && typeof systemMsg.content === 'string' ? estimate(systemMsg.content) : 0;
74
77
  const debugPayload = {
75
- systemChars,
76
- contentsChars: JSON.stringify(openaiMessages).length,
77
- toolsChars: tools ? JSON.stringify(tools).length : 0,
78
+ systemTokens,
79
+ contentsTokens: estimate(JSON.stringify(openaiMessages)),
80
+ toolsTokens: tools ? estimate(JSON.stringify(tools)) : 0,
78
81
  };
79
82
  // Build request body
80
83
  const body = {
@@ -36,6 +36,8 @@ export interface OpenAIProviderConfig {
36
36
  timeout?: number;
37
37
  /** OpenAI organization ID (optional) */
38
38
  organization?: string;
39
+ /** Optional token estimator (e.g., tiktoken) for debug payload */
40
+ estimateTokens?: (text: string) => number;
39
41
  }
40
42
  /**
41
43
  * OpenAI LLM Provider
@@ -42,6 +42,7 @@ export class OpenAIProvider extends OpenAICompatibleProvider {
42
42
  model: config.model ?? DEFAULT_MODEL,
43
43
  maxTokens: config.maxTokens,
44
44
  timeout: config.timeout,
45
+ estimateTokens: config.estimateTokens,
45
46
  };
46
47
  super(baseConfig);
47
48
  this.apiKey = apiKey;
@@ -39,6 +39,8 @@ export interface OpenRouterProviderConfig {
39
39
  siteUrl?: string;
40
40
  /** Site name for OpenRouter rankings (optional) */
41
41
  siteName?: string;
42
+ /** Optional token estimator (e.g., tiktoken) for debug payload */
43
+ estimateTokens?: (text: string) => number;
42
44
  }
43
45
  /**
44
46
  * OpenRouter LLM Provider
@@ -44,6 +44,7 @@ export class OpenRouterProvider extends OpenAICompatibleProvider {
44
44
  model: config.model ?? DEFAULT_MODEL,
45
45
  maxTokens: config.maxTokens,
46
46
  timeout: config.timeout,
47
+ estimateTokens: config.estimateTokens,
47
48
  };
48
49
  super(baseConfig);
49
50
  this.apiKey = apiKey;
@@ -34,6 +34,8 @@ export interface PerplexityProviderConfig {
34
34
  maxTokens?: number;
35
35
  /** Request timeout in milliseconds (default: 120000) */
36
36
  timeout?: number;
37
+ /** Optional token estimator (e.g., tiktoken) for debug payload */
38
+ estimateTokens?: (text: string) => number;
37
39
  }
38
40
  /**
39
41
  * Perplexity LLM Provider
@@ -41,6 +41,7 @@ export class PerplexityProvider extends OpenAICompatibleProvider {
41
41
  model: config.model ?? DEFAULT_MODEL,
42
42
  maxTokens: config.maxTokens,
43
43
  timeout: config.timeout,
44
+ estimateTokens: config.estimateTokens,
44
45
  };
45
46
  super(baseConfig);
46
47
  this.apiKey = apiKey;
@@ -34,6 +34,8 @@ export interface TogetherProviderConfig {
34
34
  maxTokens?: number;
35
35
  /** Request timeout in milliseconds (default: 120000) */
36
36
  timeout?: number;
37
+ /** Optional token estimator (e.g., tiktoken) for debug payload */
38
+ estimateTokens?: (text: string) => number;
37
39
  }
38
40
  /**
39
41
  * Together AI LLM Provider
@@ -41,6 +41,7 @@ export class TogetherProvider extends OpenAICompatibleProvider {
41
41
  model: config.model ?? DEFAULT_MODEL,
42
42
  maxTokens: config.maxTokens,
43
43
  timeout: config.timeout,
44
+ estimateTokens: config.estimateTokens,
44
45
  };
45
46
  super(baseConfig);
46
47
  this.apiKey = apiKey;
@@ -72,11 +72,11 @@ export interface LLMUsage {
72
72
  cacheCreationTokens?: number;
73
73
  /** Thinking tokens (Gemini 2.5+ models with thinking) */
74
74
  thinkingTokens?: number;
75
- /** Debug payload info - estimated char counts before sending to provider */
75
+ /** Debug payload info - estimated token counts before sending to provider */
76
76
  debugPayload?: {
77
- systemChars: number;
78
- contentsChars: number;
79
- toolsChars: number;
77
+ systemTokens: number;
78
+ contentsTokens: number;
79
+ toolsTokens: number;
80
80
  };
81
81
  }
82
82
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/agents",
3
- "version": "0.3.22",
3
+ "version": "0.3.24",
4
4
  "description": "Lightweight multi-LLM agent library for building CLI AI assistants",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",