@compilr-dev/cli 0.5.16 → 0.6.0

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
@@ -1,44 +1,55 @@
1
1
  /**
2
2
  * Agent Configuration
3
3
  *
4
- * Creates and configures the Agent instance with
5
- * ClaudeProvider, OllamaProvider, OpenAIProvider, or GeminiProvider
6
- * and available tools.
4
+ * Thin wrapper around createCompilrAgent() from @compilr-dev/sdk.
5
+ * Handles CLI-specific concerns: credential resolution, system prompt
6
+ * assembly, subagent callbacks, and token estimates.
7
7
  */
8
- import { Agent, ContextManager, DEFAULT_CONTEXT_CONFIG, createTaskTool, createSuggestTool, defaultAgentTypes, TOOL_SETS, BUILTIN_GUARDRAILS, createProviderFromType, CapabilityManager, CapabilityContext, CAPABILITY_PACKS, GIT_SAFETY_MODULE, PLATFORM_TOOL_HINTS_MODULE, FACTORY_TOOL_HINTS_MODULE, TOOL_USAGE_META_MODULE, createLoadCapabilityTool, createCapabilityHook, resolveProfileGroups, resolveUpfrontGroups, } from '@compilr-dev/sdk';
8
+ import { log } from './foundation/logger.js';
9
+ import { createCompilrAgent, createTaskTool, defaultAgentTypes, TOOL_SETS, BUILTIN_GUARDRAILS, } from '@compilr-dev/sdk';
9
10
  import { isAutoCompactEnabled, isDelegationEnabled, getSetting } from './settings/index.js';
10
11
  import { getApiKey } from './utils/credentials.js';
11
- import { createToolRegistry, createMinimalToolRegistry, getDirectTools, getMetaTools, initializeMetaTools, getToolIndexForSystemPrompt, getFilteredToolIndexForSystemPrompt, getToolStats, setMetaToolFilter, createToolFallback, getRegisteredMetaTools, } from './tools.js';
12
- // TOOL_GROUPS no longer needed — profile resolution moved to SDK
13
- import { setCapabilityManager } from './multi-agent/capability-loader.js';
14
12
  import { getAgentRegistry } from './agents/registry.js';
15
13
  import { SystemPromptBuilder } from './system-prompt/index.js';
16
- import { getDefaultModelForTier, getModelContextWindow } from './models/index.js';
14
+ import { getDefaultModelForTier } from './models/index.js';
17
15
  import { PROVIDER_METADATA } from './models/providers.js';
18
16
  import { setStaticEstimates, estimateTokens, estimateJsonTokens, } from './utils/token-tracker.js';
19
17
  import { createFileLockCheckHook, createFileLockAcquireHook } from './multi-agent/file-lock-hook.js';
20
18
  import { getActiveProject } from './tools/project-db.js';
19
+ import { allDbTools, allFactoryTools } from './tools/db-tools.js';
21
20
  /**
22
21
  * Default permission rules for potentially dangerous operations
23
- * Note: Tool names are lowercase (bash, write_file, edit, git_commit)
24
22
  */
25
23
  const DEFAULT_PERMISSION_RULES = [
26
- // File writes need approval each time (user can see diff before approving)
27
24
  { toolName: 'write_file', level: 'once', description: 'Write/create files' },
28
25
  { toolName: 'edit', level: 'once', description: 'Edit file contents' },
29
- // Git operations that modify state
30
26
  { toolName: 'git_commit', level: 'once', description: 'Create git commit' },
31
27
  { toolName: 'git_branch', level: 'once', description: 'Create/delete git branches' },
32
- // Shell commands need approval each time
33
28
  { toolName: 'bash', level: 'once', description: 'Execute shell command' },
34
- // Project runners (can have side effects)
35
29
  { toolName: 'run_tests', level: 'once', description: 'Run test suite' },
36
30
  { toolName: 'run_lint', level: 'once', description: 'Run linter (may auto-fix)' },
37
- // Note: backlog_write removed - use workitem_* tools which modify database
38
31
  ];
32
+ /** Delegation config for tool result auto-summarization */
33
+ const CLI_DELEGATION_CONFIG = {
34
+ enabled: true,
35
+ delegationThreshold: 8000,
36
+ summaryMaxTokens: 800,
37
+ resultTTL: 600_000,
38
+ maxStoredResults: 50,
39
+ strategy: 'auto',
40
+ toolOverrides: {
41
+ bash: { threshold: 12000 },
42
+ grep: { threshold: 4000 },
43
+ git_diff: { threshold: 6000 },
44
+ get_tool_info: { enabled: false },
45
+ list_tools: { enabled: false },
46
+ ask_user: { enabled: false },
47
+ ask_user_simple: { enabled: false },
48
+ todo_read: { enabled: false },
49
+ },
50
+ };
39
51
  /**
40
52
  * Get permission info for a tool by name.
41
- * Returns undefined if the tool doesn't require special permission (always allowed).
42
53
  */
43
54
  export function getToolPermissionInfo(toolName) {
44
55
  const rule = DEFAULT_PERMISSION_RULES.find(r => r.toolName === toolName);
@@ -52,7 +63,6 @@ export function getToolPermissionInfo(toolName) {
52
63
  }
53
64
  /**
54
65
  * Get information about built-in guardrails.
55
- * Used for displaying guardrail info in /help or status overlays.
56
66
  */
57
67
  export function getBuiltinGuardrailInfo() {
58
68
  return BUILTIN_GUARDRAILS.map((g) => ({
@@ -65,21 +75,16 @@ export function getBuiltinGuardrailInfo() {
65
75
  }
66
76
  /**
67
77
  * Merges built-in agent types with custom agents from registry.
68
- * Custom agents get safe read-only tools by default.
69
78
  */
70
79
  function getMergedAgentTypes() {
71
80
  const registry = getAgentRegistry();
72
81
  const customAgents = registry.getCustomAgents();
73
- // Start with default agent types (library now has correct tool names)
74
82
  const mergedTypes = { ...defaultAgentTypes };
75
- // Add custom agents from registry
76
83
  for (const agent of customAgents) {
77
84
  mergedTypes[agent.name] = {
78
85
  description: agent.description,
79
86
  systemPrompt: agent.systemPrompt,
80
87
  defaultModel: agent.model === 'inherit' ? undefined : agent.model,
81
- // Safe read-only tools for custom agents (using library's TOOL_SETS)
82
- // Type assertion needed because TOOL_SETS typing might not be exported correctly
83
88
  allowedTools: Array.isArray(TOOL_SETS.READ_ONLY) ? [...(TOOL_SETS.READ_ONLY)] : [],
84
89
  maxIterations: 10,
85
90
  contextMode: 'inherit-summary',
@@ -87,189 +92,52 @@ function getMergedAgentTypes() {
87
92
  }
88
93
  return mergedTypes;
89
94
  }
90
- /**
91
- * Creates the appropriate LLM provider based on configuration.
92
- * Resolves CLI-specific concerns (credentials, settings) then delegates
93
- * provider instantiation to the SDK's createProviderFromType().
94
- */
95
- function createProvider(options) {
95
+ // ─── Provider Resolution (CLI-specific credentials) ─────────────────────────
96
+ function resolveProviderConfig(options) {
96
97
  const providerType = options.provider ?? 'claude';
97
- const quiet = options.quiet ?? false;
98
98
  const meta = PROVIDER_METADATA[providerType];
99
- // 1. Resolve API key from CLI credential store
100
99
  const apiKey = meta.requiresKey ? getApiKey(meta.credentialKey) : undefined;
101
100
  if (!apiKey && meta.requiresKey) {
102
101
  throw new Error(`${meta.displayName} API key not found.\n` +
103
102
  `Run /keys to set it, or: export ${meta.envVar}=...` +
104
103
  (providerType === 'claude' ? '\nOr use --provider ollama for local models.' : ''));
105
104
  }
106
- // 2. Resolve model (from options or registry default for balanced tier)
107
105
  const model = options.model ?? getDefaultModelForTier(providerType, 'balanced')?.id;
108
- // 3. Resolve Ollama base URL from settings
109
106
  const baseUrl = providerType === 'ollama'
110
107
  ? ((typeof getSetting('ollamaBaseUrl') === 'string'
111
108
  ? getSetting('ollamaBaseUrl')
112
- : undefined) ??
113
- process.env.OLLAMA_BASE_URL ??
114
- 'http://localhost:11434')
109
+ : undefined) ?? process.env.OLLAMA_BASE_URL ?? 'http://localhost:11434')
115
110
  : undefined;
116
- // 4. Log (unless quiet)
117
- if (!quiet)
118
- console.log(`Using ${meta.displayName} with model: ${model ?? 'default'}`);
119
- if (!quiet && providerType === 'ollama' && baseUrl !== 'http://localhost:11434') {
120
- console.log(` Ollama server: ${baseUrl ?? ''}`);
121
- }
122
- // 5. Delegate to SDK (with tiktoken-based token estimator for debug payload)
123
- return createProviderFromType(providerType, {
124
- model,
125
- apiKey: apiKey ?? undefined,
126
- baseUrl,
127
- siteName: providerType === 'openrouter' ? 'compilr-cli' : undefined,
128
- estimateTokens,
129
- });
130
- }
131
- // =============================================================================
132
- // Dynamic Capability Loading Helpers
133
- // =============================================================================
134
- /**
135
- * Derive profile group IDs from a tool filter (array of tool names).
136
- * A group is included if any of its tools are in the filter.
137
- */
138
- // getProfileGroupsFromToolFilter → replaced by resolveProfileGroups from SDK
139
- // autoDetectCapabilities → replaced by autoDetectCapabilities from SDK
140
- // createCapabilityHook → replaced by createCapabilityHook + CapabilityContext from SDK
141
- /**
142
- * Creates an Agent instance configured with all tools.
143
- */
144
- export function createAgent(options = {}) {
145
- const provider = createProvider(options);
146
- // Create context manager with model-specific limits
147
- // Uses library defaults: compact at 50%, summarize at 90%, emergency at 95%
148
- // Auto-compact can be disabled via settings (requires restart)
149
- const autoCompact = isAutoCompactEnabled();
150
- const contextManager = new ContextManager({
151
- provider,
152
- config: {
153
- maxContextTokens: getModelContextWindow(options.model ?? '', options.provider),
154
- // If auto-compact disabled, set threshold to 100% (never triggers)
155
- compaction: autoCompact
156
- ? DEFAULT_CONTEXT_CONFIG.compaction
157
- : {
158
- ...DEFAULT_CONTEXT_CONFIG.compaction,
159
- triggerThreshold: 1.0, // Never auto-trigger
160
- triggerInterval: Number.MAX_SAFE_INTEGER, // Never interval-trigger
161
- },
162
- },
163
- });
164
- // Create event handler for verbose mode
165
- const onEvent = options.verbose
166
- ? (event) => {
167
- if (event.type === 'tool_start') {
168
- console.log(`\n[Tool: ${event.name}] Starting...`);
169
- }
170
- else if (event.type === 'tool_end') {
171
- console.log(`[Tool: ${event.name}] Done`);
172
- }
173
- }
174
- : undefined;
175
- // Build modular system prompt based on current context
176
- // Note: Anchors are NOT added here - they're handled by library's AnchorManager
177
- // which re-injects them on every LLM call (survives compaction)
178
- // Initialize meta-tools if enabled (needed for tool index)
179
- // includeWarmTools=true moves 11 warm tools into meta-registry (hot-tools mode)
180
- const useMetaTools = options.enableMetaTools && !options.minimal;
181
- if (useMetaTools) {
182
- initializeMetaTools(true);
183
- }
184
- // ==========================================================================
185
- // Dynamic Capability Loading
186
- // When meta-tools are enabled, create a CapabilityManager to control which
187
- // tools and prompt modules are active per-turn.
188
- // ==========================================================================
189
- let capabilityManager;
190
- let capabilityContext;
191
- let orphanToolNames = [];
192
- if (useMetaTools) {
193
- // Use SDK's profile resolver instead of local implementation
194
- const profileGroups = options.toolFilter
195
- ? resolveProfileGroups(options.toolFilter)
196
- : Object.keys(CAPABILITY_PACKS);
197
- const upfrontGroupIds = resolveUpfrontGroups(profileGroups);
198
- capabilityManager = new CapabilityManager({
199
- profileGroups,
200
- packs: CAPABILITY_PACKS,
201
- upfrontGroups: upfrontGroupIds,
202
- });
203
- // Expose the capability manager for slash command handlers (Phase 3)
204
- setCapabilityManager(capabilityManager);
205
- // Compute orphan tools (meta-registry tools not in any capability pack)
206
- const packedTools = new Set();
207
- for (const pack of Object.values(CAPABILITY_PACKS)) {
208
- for (const tool of pack.tools) {
209
- packedTools.add(tool);
210
- }
211
- }
212
- const registered = getRegisteredMetaTools();
213
- orphanToolNames = registered
214
- .map((t) => t.definition.name)
215
- .filter((name) => !packedTools.has(name));
216
- // Create capability context (SDK) for filter sync + auto-load callback
217
- capabilityContext = new CapabilityContext({
218
- manager: capabilityManager,
219
- orphanTools: orphanToolNames,
220
- onFilterUpdate: (allowed) => { setMetaToolFilter(allowed); },
221
- });
222
- capabilityContext.syncFilter();
223
- // Log stats (unless quiet mode)
224
- if (!options.quiet) {
225
- const stats = getToolStats();
226
- const loadedPacks = capabilityManager.getLoadedPackIds();
227
- const loadableCount = capabilityManager.getCatalog().length;
228
- console.log(`[Meta-tools] Direct: ${String(stats.directTools)}, ` +
229
- `Meta: ${String(stats.metaRegistryTools)}, ` +
230
- `Est. savings: ~${String(stats.tokenSavings)} tokens`);
231
- console.log(`[Capabilities] Upfront: ${String(loadedPacks.length)} packs, ` +
232
- `Loadable: ${String(loadableCount)} packs`);
111
+ if (!options.quiet) {
112
+ log.info({ component: 'agent', provider: meta.displayName, model: model ?? 'default' }, 'Provider configured');
113
+ if (providerType === 'ollama' && baseUrl !== 'http://localhost:11434') {
114
+ log.info({ component: 'agent', baseUrl }, 'Ollama server');
233
115
  }
234
116
  }
235
- // Generate tool index for static system prompt (only when NOT using capability loading)
236
- const metaToolsIndex = useMetaTools && !capabilityManager
237
- ? getToolIndexForSystemPrompt()
238
- : undefined;
239
- let systemPrompt;
240
- // Check if we should use minimal system prompt (for testing role identity)
117
+ return { provider: providerType, model, apiKey: apiKey ?? undefined, baseUrl };
118
+ }
119
+ // ─── System Prompt Assembly ─────────────────────────────────────────────────
120
+ function buildSystemPrompt(options) {
241
121
  if (options.useMinimalSystemPrompt && options.systemPromptAddition) {
242
- // Skip all modules - just use the role prompt directly
243
- systemPrompt = options.systemPromptAddition;
122
+ return options.systemPromptAddition;
244
123
  }
245
- else {
246
- // Use modular system prompt builder
247
- // When capability loading is active, exclude conditional modules:
248
- // git-safety, platform-tool-hints, factory-tool-hints, tool-usage-meta
249
- // These are dynamically added by the BeforeLLM hook based on loaded capabilities.
250
- const promptBuilder = new SystemPromptBuilder({
251
- enableMetaTools: capabilityManager ? false : useMetaTools,
252
- hasGit: capabilityManager ? false : undefined, // false = skip git-safety; undefined = auto-detect
253
- projectName: options.projectName,
254
- projectSlug: options.projectSlug,
255
- projectId: options.projectId,
256
- projectContext: options.projectContext,
257
- guidedModeContext: options.guidedModeContext,
258
- planModeContext: options.planModeContext,
259
- metaToolsIndex,
260
- hasRoleIdentity: !!options.systemPromptAddition, // Skip default identity when role is specified
261
- });
262
- const buildResult = promptBuilder.build();
263
- systemPrompt = buildResult.prompt;
264
- // Prepend role-specific system prompt (for team agents)
265
- // The default identity module is already excluded via hasRoleIdentity flag
266
- if (options.systemPromptAddition) {
267
- // Extract role name from the role prompt (format: "# ROLE: ROLE_NAME")
268
- const roleMatch = options.systemPromptAddition.match(/^#\s*ROLE:\s*(.+)$/m);
269
- const roleName = roleMatch ? roleMatch[1].trim() : 'specialized team member';
270
- // Put FULL role prompt at the END (recency effect is stronger than primacy)
271
- // LLMs weight the end of context more heavily
272
- const roleEnding = `
124
+ const promptBuilder = new SystemPromptBuilder({
125
+ enableMetaTools: false, // SDK handles capability-based prompt assembly
126
+ hasGit: undefined, // Auto-detect (SDK capability hook will override if needed)
127
+ projectName: options.projectName,
128
+ projectSlug: options.projectSlug,
129
+ projectId: options.projectId,
130
+ projectContext: options.projectContext,
131
+ guidedModeContext: options.guidedModeContext,
132
+ planModeContext: options.planModeContext,
133
+ hasRoleIdentity: !!options.systemPromptAddition,
134
+ });
135
+ let systemPrompt = promptBuilder.build().prompt;
136
+ // Wrap role identity at beginning AND end for team agents
137
+ if (options.systemPromptAddition) {
138
+ const roleMatch = options.systemPromptAddition.match(/^#\s*ROLE:\s*(.+)$/m);
139
+ const roleName = roleMatch ? roleMatch[1].trim() : 'specialized team member';
140
+ const roleEnding = `
273
141
 
274
142
  ---
275
143
 
@@ -278,113 +146,118 @@ export function createAgent(options = {}) {
278
146
  ${options.systemPromptAddition}
279
147
 
280
148
  **CRITICAL**: When asked "what is your role?", "what are you?", or similar identity questions, your response should acknowledge your assigned role. For example: "In this team, I'm the ${roleName}" or "I'm operating as the ${roleName} in this session."`;
281
- // Put role at BOTH beginning AND end, with full role prompt at the end
282
- systemPrompt = options.systemPromptAddition + '\n\n' + systemPrompt + roleEnding;
283
- }
149
+ systemPrompt = options.systemPromptAddition + '\n\n' + systemPrompt + roleEnding;
284
150
  }
285
- // Store base prompt for capability-aware BeforeLLM hook
286
- const baseSystemPrompt = capabilityManager ? systemPrompt : undefined;
287
- const agent = new Agent({
151
+ return systemPrompt;
152
+ }
153
+ // ─── Context Pressure Hook ──────────────────────────────────────────────────
154
+ function createContextPressureHook() {
155
+ return (ctx) => {
156
+ const msgCount = ctx.messages.length;
157
+ if (msgCount < 20)
158
+ return undefined;
159
+ let hint;
160
+ if (msgCount < 40) {
161
+ hint = `[Context: ~${String(msgCount)} messages] Consider task() for multi-file operations.`;
162
+ }
163
+ else if (msgCount < 60) {
164
+ hint = `[Context: ~${String(msgCount)} messages - HIGH] Prefer task() sub-agents for file reads and code search.`;
165
+ }
166
+ else {
167
+ hint = `[Context: ~${String(msgCount)} messages - CRITICAL] Use task() for ALL exploration. Do not read files directly.`;
168
+ }
169
+ const messages = [...ctx.messages];
170
+ messages.splice(1, 0, { role: 'user', content: hint });
171
+ return { messages };
172
+ };
173
+ }
174
+ // ─── Main Factory ───────────────────────────────────────────────────────────
175
+ /**
176
+ * Creates an Agent instance configured with all tools.
177
+ *
178
+ * Delegates to createCompilrAgent() from @compilr-dev/sdk for:
179
+ * - Provider creation, context management, observation masking
180
+ * - Smart windowing, tool output guards
181
+ * - Capability loading (meta-tools, dynamic prompt assembly)
182
+ * - Permission handling, guardrails, delegation
183
+ *
184
+ * CLI adds on top:
185
+ * - Credential resolution from CLI keystore
186
+ * - System prompt with project context and role identity
187
+ * - Subagent callbacks for UI updates
188
+ * - Token estimate logging
189
+ */
190
+ export function createAgent(options = {}) {
191
+ const { provider, model, apiKey } = resolveProviderConfig(options);
192
+ const systemPrompt = buildSystemPrompt(options);
193
+ // ── Create agent via SDK ──────────────────────────────────────────────────
194
+ const compilrAgent = createCompilrAgent({
288
195
  provider,
196
+ model,
197
+ apiKey,
289
198
  systemPrompt,
290
199
  maxIterations: options.maxIterations ?? 50,
291
- onEvent,
292
- contextManager,
293
- // Set tool timeout to 2 minutes (default is 30s which is too short for complex operations)
294
- // Sub-agents inherit this timeout from the parent agent
295
200
  toolTimeoutMs: 120000,
296
- // Enable file tracking for context restoration hints
297
201
  enableFileTracking: true,
298
- // Enable permissions if handler is provided
202
+ logger: log,
203
+ // Permissions
299
204
  permissions: options.onPermissionRequest
300
- ? {
301
- enabled: true,
302
- defaultLevel: 'always', // Allow tools by default, unless specified in rules
303
- rules: DEFAULT_PERMISSION_RULES,
304
- onPermissionRequest: options.onPermissionRequest,
305
- }
306
- : undefined,
307
- // Iteration limit handler - asks user if they want to continue
205
+ ? options.onPermissionRequest
206
+ : 'auto',
207
+ permissionRules: DEFAULT_PERMISSION_RULES,
208
+ includeDefaultRules: true,
209
+ // Iteration limits
308
210
  onIterationLimitReached: options.onIterationLimitReached,
309
- // Anchors - critical info that survives context compaction
310
- // Library re-injects anchors on every LLM call
311
- anchors: options.enableAnchors
312
- ? {
313
- maxAnchors: 50,
314
- maxTokens: 4000,
315
- includeDefaults: true,
316
- // Note: We manage persistence ourselves via project-anchors.ts
317
- // Don't set persistPath - we load anchors manually below
318
- }
211
+ // Pins / anchors
212
+ pins: options.enableAnchors
213
+ ? { maxAnchors: 50, maxTokens: 4000, includeDefaults: true }
319
214
  : undefined,
320
- // Guardrails - pattern-based safety checks for risky operations
321
- // 15 built-in patterns: git destructive ops, rm -rf, DROP TABLE, secrets, etc.
215
+ // Guardrails
322
216
  guardrails: options.enableGuardrails
323
- ? {
324
- enabled: true,
325
- includeDefaults: true,
326
- onTriggered: options.onGuardrailTriggered,
217
+ ? { includeDefaults: true }
218
+ : false,
219
+ // Delegation
220
+ delegation: isDelegationEnabled() ? CLI_DELEGATION_CONFIG : undefined,
221
+ // Context management
222
+ context: {
223
+ compactionThreshold: isAutoCompactEnabled() ? undefined : 1.0,
224
+ },
225
+ // Suggest tool
226
+ onSuggest: options.onSuggest,
227
+ // Verbose event handler
228
+ onEvent: options.verbose
229
+ ? (event) => {
230
+ if (event.type === 'tool_start') {
231
+ log.debug({ component: 'agent', tool: event.name }, 'Tool starting');
232
+ }
233
+ else if (event.type === 'tool_end') {
234
+ log.debug({ component: 'agent', tool: event.name }, 'Tool done');
235
+ }
327
236
  }
328
237
  : undefined,
329
- // Tool result delegation auto-summarize large results to conserve context
330
- delegation: isDelegationEnabled()
238
+ // Capability loading (replaces CLI's manual meta-tools wiring)
239
+ // Platform tools (DB, factory) + MCP tools are passed as additionalTools
240
+ capabilities: (options.enableMetaTools && !options.minimal && !options.noTools)
331
241
  ? {
332
242
  enabled: true,
333
- delegationThreshold: 8000,
334
- summaryMaxTokens: 800,
335
- resultTTL: 600_000,
336
- maxStoredResults: 50,
337
- strategy: 'auto',
338
- toolOverrides: {
339
- // Custom thresholds
340
- bash: { threshold: 12000 },
341
- grep: { threshold: 4000 },
342
- git_diff: { threshold: 6000 },
343
- // Never summarize — agent needs verbatim results
344
- get_tool_info: { enabled: false },
345
- list_tools: { enabled: false },
346
- ask_user: { enabled: false },
347
- ask_user_simple: { enabled: false },
348
- todo_read: { enabled: false },
349
- },
243
+ profile: options.toolFilter ?? 'full',
244
+ additionalTools: [
245
+ ...allDbTools,
246
+ ...allFactoryTools,
247
+ ...(options.mcpTools ?? []),
248
+ ],
350
249
  }
351
- : undefined,
352
- // Hooks: context pressure, capability loading, file lock check/acquire
250
+ : {
251
+ enabled: false,
252
+ additionalTools: [
253
+ ...allDbTools,
254
+ ...allFactoryTools,
255
+ ...(options.mcpTools ?? []),
256
+ ],
257
+ },
258
+ // CLI-specific hooks
353
259
  hooks: {
354
- beforeLLM: [
355
- // Context pressure hook
356
- (ctx) => {
357
- const utilization = contextManager.getUtilization();
358
- if (utilization < 0.3)
359
- return undefined;
360
- const pct = Math.round(utilization * 100);
361
- let hint;
362
- if (utilization < 0.6) {
363
- hint = `[Context: ${String(pct)}%] Consider task() for multi-file operations.`;
364
- }
365
- else if (utilization < 0.8) {
366
- hint = `[Context: ${String(pct)}% - HIGH] Prefer task() sub-agents for file reads and code search.`;
367
- }
368
- else {
369
- hint = `[Context: ${String(pct)}% - CRITICAL] Use task() for ALL exploration. Do not read files directly.`;
370
- }
371
- const messages = [...ctx.messages];
372
- messages.splice(1, 0, { role: 'user', content: hint });
373
- return { messages };
374
- },
375
- // Capability-aware system prompt assembly (SDK hook with auto-detection)
376
- ...(capabilityContext && baseSystemPrompt
377
- ? [createCapabilityHook(capabilityContext, baseSystemPrompt, {
378
- staticSections: [TOOL_USAGE_META_MODULE.content],
379
- conditionalModules: [
380
- { content: GIT_SAFETY_MODULE.content, whenModuleActive: ['git-safety'] },
381
- { content: PLATFORM_TOOL_HINTS_MODULE.content, whenModuleActive: ['platform-tool-hints'] },
382
- { content: FACTORY_TOOL_HINTS_MODULE.content, whenModuleActive: ['factory-tool-hints'] },
383
- ],
384
- getToolIndex: (allowedNames) => getFilteredToolIndexForSystemPrompt(allowedNames),
385
- })]
386
- : []),
387
- ],
260
+ beforeLLM: [createContextPressureHook()],
388
261
  beforeTool: [createFileLockCheckHook(getActiveProject)],
389
262
  afterTool: [
390
263
  createFileLockAcquireHook(getActiveProject),
@@ -392,7 +265,8 @@ ${options.systemPromptAddition}
392
265
  ],
393
266
  },
394
267
  });
395
- // Load persisted anchors into the agent's pin manager
268
+ const agent = compilrAgent.unwrap();
269
+ // ── Load persisted anchors ────────────────────────────────────────────────
396
270
  if (options.enableAnchors && options.persistedAnchors) {
397
271
  for (const anchor of options.persistedAnchors) {
398
272
  agent.addPin({
@@ -401,112 +275,44 @@ ${options.systemPromptAddition}
401
275
  priority: anchor.priority,
402
276
  scope: anchor.scope,
403
277
  tags: anchor.tags,
404
- // Note: projectId is for our tracking, not needed by library
405
278
  });
406
279
  }
407
280
  }
408
- // Register tools using registerTools method (skip if noTools is set)
409
- if (!options.noTools) {
410
- // When meta-tools is enabled, use direct tools + meta-tools instead of all tools
411
- let tools;
412
- if (options.minimal) {
413
- tools = createMinimalToolRegistry();
414
- }
415
- else if (options.enableMetaTools) {
416
- // Hot-tools mode: only hot tools + get_tool_info (for schema introspection)
417
- // Warm tools + meta-registry tools are accessed transparently via fallback handler
418
- tools = [...getDirectTools(true), ...getMetaTools()];
419
- // Add load_capability tool when the agent has loadable packs
420
- if (capabilityManager?.hasLoadablePacks()) {
421
- tools.push(createLoadCapabilityTool(capabilityManager));
422
- }
423
- }
424
- else {
425
- // Legacy mode: all tools declared directly
426
- // eslint-disable-next-line @typescript-eslint/no-deprecated -- Intentional for backward compatibility
427
- tools = createToolRegistry();
428
- }
429
- // Apply tool filter if specified (filter direct tools)
430
- if (options.toolFilter && options.toolFilter.length > 0) {
431
- const allowedTools = new Set(options.toolFilter);
432
- // get_tool_info and load_capability are always allowed
433
- const alwaysAllowed = new Set(['get_tool_info', 'load_capability']);
434
- tools = tools.filter((tool) => allowedTools.has(tool.definition.name) || alwaysAllowed.has(tool.definition.name));
435
- // When capability loading is active, the BeforeLLM hook manages the meta-tool filter.
436
- // Otherwise, set it to the full tool filter.
437
- if (!capabilityManager) {
438
- setMetaToolFilter(options.toolFilter);
439
- }
440
- }
441
- else if (!capabilityManager) {
442
- // No filter and no capability loading - clear any previous filter
443
- setMetaToolFilter(null);
444
- }
445
- // Type assertion needed for tool registry compatibility
446
- agent.registerTools(tools);
447
- // Register MCP tools (from external MCP servers)
448
- // These are registered as direct tools since their schemas are unknown at build time
449
- if (options.mcpTools && options.mcpTools.length > 0) {
450
- agent.registerTools(options.mcpTools);
451
- }
452
- // Set up transparent fallback to meta-registry
453
- // When the agent calls a tool not in direct tools (e.g., workitem_add),
454
- // the fallback handler checks the meta-registry and executes it.
455
- if (options.enableMetaTools && !options.minimal) {
456
- agent.getToolRegistry().setFallbackHandler(createToolFallback());
457
- }
458
- }
459
- // Register Task tool for sub-agent spawning (only in full mode)
281
+ // ── Register subagent task tool (CLI-specific callbacks) ──────────────────
460
282
  if (!options.minimal && !options.noTools) {
461
- // Track active subagents to correlate spawn/complete/events
462
- // Track active subagents by toolUseId for token counting
463
283
  const activeSubagents = new Map();
464
- // Provide a way for caller to clear tracking state between runs
465
284
  if (options.onSubagentTrackingReady) {
466
- options.onSubagentTrackingReady(() => {
467
- activeSubagents.clear();
468
- });
285
+ options.onSubagentTrackingReady(() => { activeSubagents.clear(); });
469
286
  }
470
287
  const taskTool = createTaskTool({
471
288
  parentAgent: agent,
472
289
  agentTypes: getMergedAgentTypes(),
473
290
  enableEventStreaming: true,
474
- defaultTimeout: 120000, // 2 minutes for subagent operations
475
- // Now uses toolUseId for direct correlation (no more FIFO matching!)
291
+ defaultTimeout: 120000,
476
292
  onSpawn: (agentType, description, toolUseId) => {
477
293
  if (options.verbose) {
478
- console.log(`\n[Sub-agent] Spawning ${agentType}: ${description} (${toolUseId ?? 'no-id'})`);
294
+ log.debug({ component: 'agent', agentType, toolUseId }, 'Spawning sub-agent: %s', description);
479
295
  }
480
- // Immediately notify with toolUseId - no more queuing/matching!
481
296
  if (toolUseId) {
482
297
  activeSubagents.set(toolUseId, { agentType, tokenCount: 0 });
483
- if (options.onSubagentStart) {
484
- options.onSubagentStart(toolUseId, agentType, description);
485
- }
298
+ options.onSubagentStart?.(toolUseId, agentType, description);
486
299
  }
487
300
  },
488
301
  onComplete: (agentType, result, toolUseId) => {
489
302
  if (options.verbose) {
490
- console.log(`[Sub-agent] ${agentType} completed (${String(result.iterations)} iterations)`);
303
+ log.debug({ component: 'agent', agentType, iterations: result.iterations }, 'Sub-agent completed');
491
304
  }
492
- // Direct notification with toolUseId - no more searching!
493
- if (toolUseId && options.onSubagentEnd) {
494
- options.onSubagentEnd(toolUseId, true, result.toolCalls);
305
+ if (toolUseId) {
306
+ options.onSubagentEnd?.(toolUseId, true, result.toolCalls);
495
307
  activeSubagents.delete(toolUseId);
496
308
  }
497
309
  },
498
- // Stream subagent events for live updates
499
310
  onSubAgentEvent: (eventInfo) => {
500
311
  const { toolUseId, event } = eventInfo;
501
- // Handle tool events for live updates
502
312
  if (event.type === 'tool_start' && toolUseId && event.name && options.onSubagentToolUse) {
503
- // Extract tool name and input summary
504
- const toolName = event.name;
505
- let summary;
506
- // Try to get a meaningful summary from input
507
313
  const input = event.input;
314
+ let summary;
508
315
  if (input) {
509
- // Common patterns for tool input summaries
510
316
  const path = input.path;
511
317
  const pattern = input.pattern;
512
318
  const command = input.command;
@@ -520,64 +326,42 @@ ${options.systemPromptAddition}
520
326
  else if (typeof filePath === 'string')
521
327
  summary = filePath;
522
328
  }
523
- options.onSubagentToolUse(toolUseId, toolName, summary);
329
+ options.onSubagentToolUse(toolUseId, event.name, summary);
524
330
  }
525
331
  },
526
332
  });
527
- // Type assertion needed for task tool compatibility
528
333
  agent.registerTool(taskTool);
529
- // Register Suggest tool for next action suggestions
530
- const suggestTool = createSuggestTool({
531
- onSuggest: options.onSuggest,
532
- });
533
- agent.registerTool(suggestTool);
334
+ // Note: suggest tool is already registered by createCompilrAgent via onSuggest config
534
335
  }
535
- // ==========================================================================
536
- // Calculate and store token estimates for debugging
537
- // ==========================================================================
336
+ // ── Token estimate logging ────────────────────────────────────────────────
538
337
  if (!options.quiet) {
539
- // Calculate tool tokens from the tools we registered
338
+ const toolDefs = agent.getToolRegistry().getDefinitions();
540
339
  let toolTokens = 0;
541
- // Get the tools array based on options
542
- let toolsForEstimate = [];
543
- if (!options.noTools) {
544
- if (options.minimal) {
545
- toolsForEstimate = createMinimalToolRegistry();
546
- }
547
- else if (options.enableMetaTools) {
548
- toolsForEstimate = [...getDirectTools(true), ...getMetaTools()];
549
- }
550
- else {
551
- // eslint-disable-next-line @typescript-eslint/no-deprecated
552
- toolsForEstimate = createToolRegistry();
553
- }
554
- }
555
- for (const tool of toolsForEstimate) {
556
- const def = tool.definition;
340
+ for (const def of toolDefs) {
557
341
  toolTokens += estimateJsonTokens({
558
342
  name: def.name,
559
343
  description: def.description,
560
344
  input_schema: def.inputSchema,
561
345
  });
562
346
  }
563
- // Add task tool and suggest tool estimates (if registered)
564
- if (!options.minimal && !options.noTools) {
565
- // Task tool has a large description with all agent types
566
- toolTokens += 700; // Approximate for task tool
567
- toolTokens += 200; // Approximate for suggest tool
568
- }
569
- // Calculate estimates
570
347
  const systemTokens = estimateTokens(systemPrompt);
571
348
  const roleTokens = options.systemPromptAddition
572
- ? estimateTokens(options.systemPromptAddition) * 2 // Role appears at start and end
349
+ ? estimateTokens(options.systemPromptAddition) * 2
573
350
  : 0;
574
351
  const contextTokens = estimateTokens(options.projectContext);
575
352
  setStaticEstimates({
576
- system: systemTokens - roleTokens, // Subtract role since it's counted separately
353
+ system: systemTokens - roleTokens,
577
354
  role: roleTokens,
578
355
  tools: toolTokens,
579
356
  context: contextTokens,
580
357
  });
358
+ log.info({
359
+ component: 'agent',
360
+ toolCount: toolDefs.length,
361
+ systemTokens,
362
+ toolTokens,
363
+ contextTokens,
364
+ }, 'Agent created');
581
365
  }
582
366
  return agent;
583
367
  }