@johpaz/hive-sdk 0.0.12 → 0.0.15

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 (199) hide show
  1. package/.github/CODEOWNERS +9 -0
  2. package/.github/workflows/publish.yml +89 -0
  3. package/.github/workflows/version-bump.yml +102 -0
  4. package/CHANGELOG.md +38 -0
  5. package/README.md +158 -0
  6. package/bun.lock +543 -0
  7. package/bunfig.toml +7 -0
  8. package/docs/API-AGENTS.md +316 -0
  9. package/docs/API-CONTEXT-COMPILER.md +252 -0
  10. package/docs/API-DAG-SCHEDULER.md +273 -0
  11. package/docs/API-TOOLS-SKILLS-CHANNELS.md +293 -0
  12. package/docs/API-WORKERS-EVENTS.md +152 -0
  13. package/docs/INDEX.md +141 -0
  14. package/docs/README.md +68 -0
  15. package/package.json +54 -105
  16. package/packages/cli/package.json +17 -0
  17. package/packages/cli/src/commands/init.ts +56 -0
  18. package/packages/cli/src/commands/run.ts +45 -0
  19. package/packages/cli/src/commands/test.ts +42 -0
  20. package/packages/cli/src/commands/trace.ts +55 -0
  21. package/packages/cli/src/index.ts +43 -0
  22. package/packages/core/package.json +58 -0
  23. package/packages/core/src/ace/Curator.ts +158 -0
  24. package/packages/core/src/ace/Reflector.ts +200 -0
  25. package/packages/core/src/ace/Tracer.ts +100 -0
  26. package/packages/core/src/ace/index.ts +4 -0
  27. package/packages/core/src/agent/AgentRunner.ts +699 -0
  28. package/packages/core/src/agent/Compaction.ts +221 -0
  29. package/packages/core/src/agent/ContextCompiler.ts +567 -0
  30. package/packages/core/src/agent/ContextGuard.ts +91 -0
  31. package/packages/core/src/agent/ConversationStore.ts +244 -0
  32. package/packages/core/src/agent/Hooks.ts +166 -0
  33. package/packages/core/src/agent/NativeTools.ts +31 -0
  34. package/packages/core/src/agent/PromptBuilder.ts +169 -0
  35. package/packages/core/src/agent/Service.ts +267 -0
  36. package/packages/core/src/agent/StuckLoop.ts +133 -0
  37. package/packages/core/src/agent/index.ts +12 -0
  38. package/packages/core/src/agent/providers/LLMClient.ts +149 -0
  39. package/packages/core/src/agent/providers/anthropic.ts +212 -0
  40. package/packages/core/src/agent/providers/gemini.ts +215 -0
  41. package/packages/core/src/agent/providers/index.ts +199 -0
  42. package/packages/core/src/agent/providers/interface.ts +195 -0
  43. package/packages/core/src/agent/providers/ollama.ts +175 -0
  44. package/packages/core/src/agent/providers/openai-compat.ts +231 -0
  45. package/packages/core/src/agent/providers.ts +1 -0
  46. package/packages/core/src/agent/selectors/PlaybookSelector.ts +147 -0
  47. package/packages/core/src/agent/selectors/SkillSelector.ts +478 -0
  48. package/packages/core/src/agent/selectors/ToolSelector.ts +577 -0
  49. package/packages/core/src/agent/selectors/index.ts +6 -0
  50. package/packages/core/src/api/createAgent.test.ts +48 -0
  51. package/packages/core/src/api/createAgent.ts +122 -0
  52. package/packages/core/src/api/index.ts +2 -0
  53. package/packages/core/src/canvas/CanvasManager.ts +390 -0
  54. package/packages/core/src/canvas/a2ui-tools.ts +255 -0
  55. package/packages/core/src/canvas/canvas-tools.ts +448 -0
  56. package/packages/core/src/canvas/emitter.ts +149 -0
  57. package/packages/core/src/canvas/index.ts +6 -0
  58. package/packages/core/src/config/index.ts +2 -0
  59. package/packages/core/src/config/loader.ts +554 -0
  60. package/packages/core/src/ethics/EthicsGuard.test.ts +54 -0
  61. package/packages/core/src/ethics/EthicsGuard.ts +66 -0
  62. package/packages/core/src/ethics/index.ts +2 -0
  63. package/packages/core/src/gateway/channel-notify.test.ts +14 -0
  64. package/packages/core/src/gateway/channel-notify.ts +12 -0
  65. package/packages/core/src/gateway/index.ts +1 -0
  66. package/packages/core/src/index.ts +37 -0
  67. package/packages/core/src/mcp/MCPClient.ts +439 -0
  68. package/packages/core/src/mcp/MCPToolAdapter.ts +176 -0
  69. package/packages/core/src/mcp/config.ts +13 -0
  70. package/packages/core/src/mcp/hot-reload.ts +147 -0
  71. package/packages/core/src/mcp/index.ts +11 -0
  72. package/packages/core/src/mcp/logger.ts +42 -0
  73. package/packages/core/src/mcp/singleton.ts +21 -0
  74. package/packages/core/src/mcp/transports/index.ts +67 -0
  75. package/packages/core/src/mcp/transports/sse.ts +241 -0
  76. package/packages/core/src/mcp/transports/websocket.ts +159 -0
  77. package/packages/core/src/memory/Scratchpad.test.ts +47 -0
  78. package/packages/core/src/memory/Scratchpad.ts +37 -0
  79. package/packages/core/src/memory/Storage.ts +6 -0
  80. package/packages/core/src/memory/index.ts +2 -0
  81. package/packages/core/src/multimodal/VisionService.ts +293 -0
  82. package/packages/core/src/multimodal/index.ts +2 -0
  83. package/packages/core/src/multimodal/types.ts +28 -0
  84. package/packages/core/src/security/Pairing.ts +250 -0
  85. package/packages/core/src/security/RateLimit.ts +270 -0
  86. package/packages/core/src/security/index.ts +4 -0
  87. package/packages/core/src/skills/SkillLoader.ts +388 -0
  88. package/packages/core/src/skills/bundled-data.generated.ts +3332 -0
  89. package/packages/core/src/skills/defineSkill.ts +18 -0
  90. package/packages/core/src/skills/index.ts +4 -0
  91. package/packages/core/src/state/index.ts +2 -0
  92. package/packages/core/src/state/store.ts +312 -0
  93. package/packages/core/src/storage/SQLiteStorage.ts +407 -0
  94. package/packages/core/src/storage/crypto.ts +101 -0
  95. package/packages/core/src/storage/index.ts +10 -0
  96. package/packages/core/src/storage/onboarding.ts +1603 -0
  97. package/packages/core/src/storage/schema.ts +689 -0
  98. package/packages/core/src/storage/seed.ts +740 -0
  99. package/packages/core/src/storage/usage.ts +374 -0
  100. package/packages/core/src/swarm/AgentBus.ts +460 -0
  101. package/packages/core/src/swarm/AgentExecutor.ts +53 -0
  102. package/packages/core/src/swarm/Coordinator.ts +251 -0
  103. package/packages/core/src/swarm/EventBridge.ts +122 -0
  104. package/packages/core/src/swarm/EventBus.ts +169 -0
  105. package/packages/core/src/swarm/TaskGraph.ts +192 -0
  106. package/packages/core/src/swarm/TaskNode.ts +97 -0
  107. package/packages/core/src/swarm/TaskResult.ts +22 -0
  108. package/packages/core/src/swarm/WorkerPool.ts +236 -0
  109. package/packages/core/src/swarm/errors.ts +37 -0
  110. package/packages/core/src/swarm/index.ts +30 -0
  111. package/packages/core/src/swarm/presets/HiveLearnPreset.ts +99 -0
  112. package/packages/core/src/swarm/presets/ResearchPreset.ts +97 -0
  113. package/packages/core/src/swarm/presets/index.ts +4 -0
  114. package/packages/core/src/swarm/strategies/ParallelStrategy.ts +21 -0
  115. package/packages/core/src/swarm/strategies/PriorityStrategy.ts +46 -0
  116. package/packages/core/src/swarm/strategies/index.ts +3 -0
  117. package/packages/core/src/swarm/types.ts +164 -0
  118. package/packages/core/src/tools/ToolExecutor.ts +58 -0
  119. package/packages/core/src/tools/ToolRegistry.test.ts +98 -0
  120. package/packages/core/src/tools/ToolRegistry.ts +61 -0
  121. package/packages/core/src/tools/agents/get-available-models.ts +118 -0
  122. package/packages/core/src/tools/agents/index.ts +715 -0
  123. package/packages/core/src/tools/bridge-events.ts +26 -0
  124. package/packages/core/src/tools/canvas/index.ts +375 -0
  125. package/packages/core/src/tools/cli/index.ts +142 -0
  126. package/packages/core/src/tools/codebridge/index.ts +342 -0
  127. package/packages/core/src/tools/core/index.ts +476 -0
  128. package/packages/core/src/tools/cron/index.ts +626 -0
  129. package/packages/core/src/tools/filesystem/fs-delete.ts +78 -0
  130. package/packages/core/src/tools/filesystem/fs-edit.ts +106 -0
  131. package/packages/core/src/tools/filesystem/fs-exists.ts +63 -0
  132. package/packages/core/src/tools/filesystem/fs-glob.ts +108 -0
  133. package/packages/core/src/tools/filesystem/fs-list.ts +129 -0
  134. package/packages/core/src/tools/filesystem/fs-read.ts +72 -0
  135. package/packages/core/src/tools/filesystem/fs-write.ts +67 -0
  136. package/packages/core/src/tools/filesystem/index.ts +34 -0
  137. package/packages/core/src/tools/filesystem/workspace-guard.ts +62 -0
  138. package/packages/core/src/tools/index.ts +231 -0
  139. package/packages/core/src/tools/meeting/index.ts +363 -0
  140. package/packages/core/src/tools/office/index.ts +47 -0
  141. package/packages/core/src/tools/office/office-escribir-docx.ts +192 -0
  142. package/packages/core/src/tools/office/office-escribir-pdf.ts +172 -0
  143. package/packages/core/src/tools/office/office-escribir-pptx.ts +174 -0
  144. package/packages/core/src/tools/office/office-escribir-xlsx.ts +116 -0
  145. package/packages/core/src/tools/office/office-leer-docx.ts +93 -0
  146. package/packages/core/src/tools/office/office-leer-pdf.ts +114 -0
  147. package/packages/core/src/tools/office/office-leer-pptx.ts +136 -0
  148. package/packages/core/src/tools/office/office-leer-xlsx.ts +124 -0
  149. package/packages/core/src/tools/projects/index.ts +37 -0
  150. package/packages/core/src/tools/projects/project-create.ts +94 -0
  151. package/packages/core/src/tools/projects/project-done.ts +66 -0
  152. package/packages/core/src/tools/projects/project-fail.ts +66 -0
  153. package/packages/core/src/tools/projects/project-list.ts +96 -0
  154. package/packages/core/src/tools/projects/project-update.ts +72 -0
  155. package/packages/core/src/tools/projects/task-create.ts +68 -0
  156. package/packages/core/src/tools/projects/task-evaluate.ts +93 -0
  157. package/packages/core/src/tools/projects/task-update.ts +93 -0
  158. package/packages/core/src/tools/types.ts +39 -0
  159. package/packages/core/src/tools/voice/index.ts +104 -0
  160. package/packages/core/src/tools/web/browser-click.ts +78 -0
  161. package/packages/core/src/tools/web/browser-extract.ts +139 -0
  162. package/packages/core/src/tools/web/browser-navigate.ts +106 -0
  163. package/packages/core/src/tools/web/browser-screenshot.ts +87 -0
  164. package/packages/core/src/tools/web/browser-script.ts +88 -0
  165. package/packages/core/src/tools/web/browser-service.ts +554 -0
  166. package/packages/core/src/tools/web/browser-type.ts +101 -0
  167. package/packages/core/src/tools/web/browser-wait.ts +136 -0
  168. package/packages/core/src/tools/web/index.ts +41 -0
  169. package/packages/core/src/tools/web/web-fetch.ts +78 -0
  170. package/packages/core/src/tools/web/web-search.ts +123 -0
  171. package/packages/core/src/utils/benchmark.ts +80 -0
  172. package/packages/core/src/utils/crypto.ts +73 -0
  173. package/packages/core/src/utils/date.ts +42 -0
  174. package/packages/core/src/utils/index.ts +10 -0
  175. package/packages/core/src/utils/logger.ts +389 -0
  176. package/packages/core/src/utils/retry.ts +70 -0
  177. package/packages/core/src/utils/toon.ts +253 -0
  178. package/packages/core/src/voice/index.ts +656 -0
  179. package/test/setup-db.ts +216 -0
  180. package/tsconfig.json +39 -0
  181. package/src/agents.ts +0 -1
  182. package/src/canvas.ts +0 -1
  183. package/src/channels.ts +0 -1
  184. package/src/config.ts +0 -1
  185. package/src/events.ts +0 -1
  186. package/src/gateway.ts +0 -1
  187. package/src/index.ts +0 -304
  188. package/src/mcp.ts +0 -1
  189. package/src/multimodal.ts +0 -1
  190. package/src/scheduler.ts +0 -1
  191. package/src/security.ts +0 -1
  192. package/src/skills.ts +0 -1
  193. package/src/state.ts +0 -1
  194. package/src/storage.ts +0 -1
  195. package/src/tools.ts +0 -1
  196. package/src/tts.ts +0 -1
  197. package/src/types.ts +0 -82
  198. package/src/utils.ts +0 -1
  199. package/src/voice.ts +0 -1
@@ -0,0 +1,567 @@
1
+ /**
2
+ * Context Compiler — Implementa las 4 estrategias de Context Engineering:
3
+ *
4
+ * 1. ESCRIBIR (Write) — Guardar información fuera del contexto:
5
+ * - Scratchpad: notas persistentes por conversación
6
+ * - Trazas de ejecución: registro en traces table
7
+ *
8
+ * 2. SELECCIONAR (Select) — Traer solo lo relevante:
9
+ * - Tool Loadout: máx 3-5 tools relevantes por turno
10
+ * - Playbook filtering: reglas ACE aplicables a esta tarea
11
+ * - Historial selectivo: resumen + mensajes recientes
12
+ *
13
+ * 3. COMPRIMIR (Compress) — Reducir tokens manteniendo información:
14
+ * - Compaction: resumir mensajes viejos
15
+ * - Tool result clearing: reemplazar resultados antiguos por resúmenes
16
+ *
17
+ * 4. AISLAR (Isolate) — Separar contextos por agente:
18
+ * - Cada worker recibe su propio contexto mínimo
19
+ * - El Coordinador ve el panorama completo
20
+ *
21
+ * TODOS los datos se formatean en TOON para ahorro de tokens.
22
+ */
23
+
24
+ import { getDb } from "../storage/SQLiteStorage.ts"
25
+ import { logger } from "../utils/logger.ts"
26
+ import type { LLMMessage, LLMToolDef, ContentPart } from "./providers/LLMClient"
27
+ import type { MCPClientManager } from "../mcp/index.ts"
28
+ import { syncToolCatalogToFTS, mcpToolFullName } from "./selectors/index.ts"
29
+ import { syncSkillsToFTS, getMinimalSkills, selectSkills, type SkillDescriptor } from "./selectors/index.ts"
30
+ import { syncPlaybookToFTS } from "./selectors/index.ts"
31
+ import { getRecentMessages, getSummary, getScratchpad, toAPIMessages } from "./ConversationStore"
32
+ import { formatContext, estimateTokens } from "../utils/toon.ts"
33
+ import { buildSystemPromptWithProjects } from "./PromptBuilder"
34
+ import { createAllTools } from "../tools/index.ts"
35
+ import { resolveUserId } from "../storage/onboarding.ts"
36
+ import { getMCPManager as getSingletonMCPManager } from "../mcp/singleton.ts"
37
+ import { syncMCPToolsToDB, syncMCPToolsToFTS } from "../mcp/MCPToolAdapter.ts"
38
+ import { getUserDate, getUserTime } from "../utils/date.ts"
39
+
40
+ const log = logger.child("context-compiler")
41
+
42
+ // Configuration constants
43
+ const KEEP_LAST_N_MESSAGES = 40 // Always keep last N messages (Strategy: SELECT) — increased because tool calls/results are now persisted
44
+ const TOKEN_COMPACT_THRESHOLD = 6000 // Compact when exceeds this (Strategy: COMPRESS)
45
+
46
+ // MINIMAL TOOL SET — fixed always-available tools
47
+ // The agent discovers the rest via search_knowledge
48
+ const MINIMAL_TOOLS = new Set([
49
+ "save_note",
50
+ "notify",
51
+ "report_progress",
52
+ "search_knowledge",
53
+ ])
54
+
55
+ // MINIMAL SKILL SET — fixed always-available skills
56
+ // These skills are ALWAYS in context - the agent uses them to discover everything else
57
+ const MINIMAL_SKILL_NAMES = [
58
+ "busqueda_fts5", // Core: how to find tools, skills, MCP, playbook via search_knowledge
59
+ "canvas_report", // Display results to users with charts, tables, cards
60
+ "memory_manager", // Persistent notes that survive context compression
61
+ ]
62
+
63
+ // ─── Types ─────────────────────────────────────────────────────────────────
64
+
65
+ // Simple tool interface for context compilation
66
+ export interface ContextTool {
67
+ name: string
68
+ description: string
69
+ parameters: Record<string, unknown>
70
+ execute?: (params: Record<string, unknown>) => Promise<unknown>
71
+ }
72
+
73
+ export interface CompiledContext {
74
+ systemPrompt: string
75
+ messages: LLMMessage[]
76
+ tools: LLMToolDef[]
77
+ allTools: ContextTool[]
78
+ skills: SkillDescriptor[] // Skills loaded (minimal + discovered)
79
+ }
80
+
81
+ // ─── Main compiler ─────────────────────────────────────────────────────────
82
+
83
+ /**
84
+ * Compile context for agent execution implementing 4 strategies:
85
+ * 1. WRITE - Load scratchpad notes
86
+ * 2. SELECT - Tool loadout, playbook rules, selective history
87
+ * 3. COMPRESS - Use summaries, clear old tool results
88
+ * 4. ISOLATE - Worker gets minimal context
89
+ */
90
+ export async function compileContext(opts: {
91
+ agentId: string
92
+ threadId: string
93
+ userId?: string
94
+ userMessage: string | ContentPart[]
95
+ channel?: string
96
+ isolated?: boolean
97
+ taskContext?: string | ContentPart[]
98
+ mcpManager?: MCPClientManager | null
99
+ }): Promise<CompiledContext> {
100
+ const db = getDb()
101
+ const { agentId, threadId, mcpManager, userMessage, isolated, taskContext } = opts
102
+
103
+ // Fallback: Get MCP Manager from singleton if not provided
104
+ const effectiveMcpManager = mcpManager ?? (() => {
105
+ const singletonMcp = getSingletonMCPManager()
106
+ if (singletonMcp) {
107
+ log.info(`[context-compiler] Using MCP Manager from singleton`)
108
+ return singletonMcp
109
+ }
110
+ return null
111
+ })()
112
+
113
+ // Resolve userId from database with priority: explicit param → channel identity → single user
114
+ const userId = opts.userId || resolveUserId({
115
+ threadId,
116
+ channel: opts.channel,
117
+ channelUserId: threadId
118
+ }) || threadId || ""
119
+
120
+ // [STEP-1] Load agent config
121
+ log.info(`[context-compiler] [STEP-1] Loading agent config for id=${agentId}`)
122
+ let agent: any
123
+ try {
124
+ agent = db.query<any, [string]>(
125
+ "SELECT * FROM agents WHERE id = ?"
126
+ ).get(agentId)
127
+ } catch (err) {
128
+ log.error(`[context-compiler] [STEP-1] ❌ FAILED loading agent: ${JSON.stringify(err)}`)
129
+ throw err
130
+ }
131
+
132
+ if (!agent) {
133
+ throw new Error(`Agent not found: ${agentId}`)
134
+ }
135
+
136
+ const isWorker = agent.role === 'worker' || !!isolated
137
+ log.info(`[context-compiler] [STEP-1] ✅ Compiling for ${isWorker ? 'worker' : 'coordinator'} agent=${agent.name}`)
138
+
139
+ // [STEP-2] STRATEGY 1: WRITE — Load scratchpad (persistent notes)
140
+ log.info(`[context-compiler] [STEP-2] Loading scratchpad...`)
141
+ let scratchpadNotes: ReturnType<typeof getScratchpad> = []
142
+ try {
143
+ scratchpadNotes = getScratchpad(threadId)
144
+ log.info(`[context-compiler] [STEP-2] ✅ Loaded ${scratchpadNotes.length} scratchpad notes`)
145
+ } catch (err) {
146
+ log.error(`[context-compiler] [STEP-2] ❌ FAILED loading scratchpad: ${JSON.stringify(err)}`)
147
+ throw err
148
+ }
149
+
150
+ // [STEP-3c] Load MCP tools (executors only — FTS sync happens here too)
151
+ log.info(`[context-compiler] [STEP-3c] Loading MCP tools...`)
152
+ const mcpToolExecutors: ContextTool[] = []
153
+
154
+ if (effectiveMcpManager) {
155
+ try {
156
+ const dbServers = db.query<any, []>(
157
+ "SELECT id, name, status FROM mcp_servers WHERE enabled = 1"
158
+ ).all()
159
+
160
+ for (const server of dbServers) {
161
+ // Try ID first (normalized), then name
162
+ let serverTools = effectiveMcpManager.getServerTools(server.id)
163
+ if (!serverTools || serverTools.length === 0) {
164
+ serverTools = effectiveMcpManager.getServerTools(server.name)
165
+ }
166
+
167
+ if (serverTools && serverTools.length > 0) {
168
+ log.info(`[context-compiler] [STEP-3c] Server ${server.name}: ${serverTools.length} tools`)
169
+
170
+ for (const mcpTool of serverTools) {
171
+ // Sanitized name valid for all LLM providers (no spaces, max 64 chars)
172
+ const fullName = mcpToolFullName(server.name, mcpTool.name)
173
+
174
+ // Executor for agent-loop (has the real call)
175
+ mcpToolExecutors.push({
176
+ name: fullName,
177
+ description: mcpTool.description || `Tool from ${server.name}`,
178
+ parameters: mcpTool.inputSchema || { type: "object", properties: {} },
179
+ execute: async (params: Record<string, unknown>) => {
180
+ // Return raw JS value — agent-loop will TOON-encode via formatToolResult.
181
+ // Never pre-stringify here: formatToolResult(string) double-encodes.
182
+ return await effectiveMcpManager.callTool(server.id, mcpTool.name, params)
183
+ },
184
+ })
185
+
186
+ }
187
+ } else {
188
+ log.warn(`[context-compiler] [STEP-3c] Server ${server.name} has no tools (not connected yet)`)
189
+ }
190
+ }
191
+
192
+ log.info(`[context-compiler] [STEP-3c] ✅ Loaded ${mcpToolExecutors.length} MCP tools`)
193
+
194
+ // Persist MCP tool definitions to DB for search_knowledge and FTS5 search
195
+ if (mcpToolExecutors.length > 0) {
196
+ try {
197
+ for (const server of dbServers) {
198
+ let serverTools = effectiveMcpManager!.getServerTools(server.id)
199
+ if (!serverTools || serverTools.length === 0) {
200
+ serverTools = effectiveMcpManager!.getServerTools(server.name)
201
+ }
202
+ if (serverTools && serverTools.length > 0) {
203
+ syncMCPToolsToDB(server.id || server.name, server.name, serverTools)
204
+ }
205
+ }
206
+ await syncMCPToolsToFTS();
207
+ log.info(`[context-compiler] [STEP-3c] ✅ Persisted MCP tools to DB + FTS5`)
208
+ } catch (syncErr) {
209
+ log.warn(`[context-compiler] [STEP-3c] ⚠️ Failed to persist MCP tools to DB: ${(syncErr as Error).message}`)
210
+ }
211
+ }
212
+ } catch (err) {
213
+ log.error(`[context-compiler] [STEP-3c] ❌ Failed: ${(err as Error).message}`)
214
+ }
215
+ } else {
216
+ log.info(`[context-compiler] [STEP-3c] ⚠️ No MCP manager, skipping MCP tools`)
217
+ }
218
+
219
+ // [STEP-4] Minimal tool set — agent discovers the rest via search_knowledge
220
+ log.info(`[context-compiler] [STEP-4] Building minimal tool set`)
221
+
222
+ // [STEP-8] Combine native tools + MCP executors loaded in STEP-3c
223
+ const config = { tools: {} }
224
+ const allNativeTools = createAllTools(config)
225
+ const nativeTools: ContextTool[] = allNativeTools.map(t => ({
226
+ name: t.name,
227
+ description: t.description || "",
228
+ parameters: t.parameters as any,
229
+ execute: t.execute,
230
+ }))
231
+
232
+ const allTools = [...nativeTools, ...mcpToolExecutors]
233
+
234
+ // Only native minimal tools in LLM context
235
+ // MCP tools are discovered dynamically via search_knowledge(type="mcp")
236
+ const filteredNativeTools: ContextTool[] = nativeTools.filter(t => MINIMAL_TOOLS.has(t.name))
237
+
238
+ const nativeToolsForLLM: LLMToolDef[] = filteredNativeTools.map(t => ({
239
+ type: "function" as const,
240
+ function: {
241
+ name: t.name,
242
+ description: t.description,
243
+ parameters: t.parameters,
244
+ },
245
+ }))
246
+
247
+ const toolsForLLM: LLMToolDef[] = nativeToolsForLLM
248
+
249
+ log.info(`[context-compiler] [STEP-4] Minimal native tool set: ${filteredNativeTools.length} tools`)
250
+ log.info(`[context-compiler] [STEP-4b] MCP tools available via search_knowledge: ${mcpToolExecutors.length} (not injected)`)
251
+ log.info(`[context-compiler] [STEP-8] ✅ Combined tools: ${allTools.length} total executors, ${toolsForLLM.length} in LLM context`)
252
+
253
+ // [STEP-8b] STRATEGY 2: SELECT — Skill Loadout (minimal + discovered)
254
+ log.info(`[context-compiler] [STEP-8b] Building skill loadout...`)
255
+ let minimalSkills: SkillDescriptor[] = []
256
+ let discoveredSkills: SkillDescriptor[] = []
257
+
258
+ try {
259
+ // Load minimal skills (always available)
260
+ minimalSkills = getMinimalSkills()
261
+ log.info(`[context-compiler] [STEP-8b] ✅ Loaded ${minimalSkills.length} minimal skills`)
262
+
263
+ // Discover additional skills via FTS5 (coordinator only)
264
+ if (!isWorker) {
265
+ const inputForSkills = taskContext || userMessage
266
+ const textMessage = typeof inputForSkills === "string"
267
+ ? inputForSkills
268
+ : Array.isArray(inputForSkills)
269
+ ? inputForSkills.filter(p => p.type === "text").map(p => (p as any).text).join("\n")
270
+ : String(inputForSkills)
271
+ discoveredSkills = selectSkills(textMessage)
272
+ log.info(`[context-compiler] [STEP-8b] ✅ Discovered ${discoveredSkills.length} additional skills via FTS5`)
273
+ }
274
+ } catch (err) {
275
+ log.warn(`[context-compiler] [STEP-8b] ⚠️ Skill loadout failed: ${(err as Error).message}`)
276
+ }
277
+
278
+ // Combine skills (minimal + discovered, avoiding duplicates)
279
+ const skillMap = new Map<string, SkillDescriptor>()
280
+ for (const skill of minimalSkills) {
281
+ skillMap.set(skill.name, skill)
282
+ }
283
+ for (const skill of discoveredSkills) {
284
+ if (!skillMap.has(skill.name)) {
285
+ skillMap.set(skill.name, skill)
286
+ }
287
+ }
288
+ const allSkills = Array.from(skillMap.values())
289
+
290
+ // [STEP-9] STRATEGY 3: COMPRESS — Load history with compaction
291
+ log.info(`[context-compiler] [STEP-9] Loading conversation history...`)
292
+ let recentMessages: ReturnType<typeof getRecentMessages> = []
293
+ try {
294
+ recentMessages = getRecentMessages(threadId, KEEP_LAST_N_MESSAGES)
295
+ log.info(`[context-compiler] [STEP-9] ✅ Loaded ${recentMessages.length} recent messages`)
296
+ } catch (err) {
297
+ log.error(`[context-compiler] [STEP-9] ❌ FAILED loading history: ${JSON.stringify(err)}`)
298
+ throw err
299
+ }
300
+
301
+ // Check if we need to use summary (conversation is long)
302
+ let summary: ReturnType<typeof getSummary> = null
303
+ try {
304
+ summary = getSummary(threadId)
305
+ } catch (err) {
306
+ log.error(`[context-compiler] [STEP-9b] ❌ FAILED loading summary: ${JSON.stringify(err)}`)
307
+ throw err
308
+ }
309
+
310
+ const totalTokens = recentMessages.reduce((sum, m) => sum + estimateTokens(m.content), 0)
311
+
312
+ let messages: LLMMessage[]
313
+
314
+ if (summary && totalTokens > TOKEN_COMPACT_THRESHOLD) {
315
+ // Use summary + recent messages (Strategy: COMPRESS)
316
+ messages = [
317
+ { role: "system", content: `[Conversation Summary]: ${summary.summary}` },
318
+ ...toAPIMessages(recentMessages),
319
+ ]
320
+ log.info(`[context-compiler] [STEP-9c] Using summary (${summary.messages_covered} messages compressed)`)
321
+ } else {
322
+ // Conversation is short enough, use all recent messages
323
+ messages = toAPIMessages(recentMessages)
324
+ }
325
+
326
+ // [STEP-10] STRATEGY 4: ISOLATE — Build context based on agent role
327
+ log.info(`[context-compiler] [STEP-10] Building system prompt...`)
328
+ let systemPrompt: string
329
+ try {
330
+ systemPrompt = await buildSystemPromptWithProjects({ agentId, userId })
331
+ log.info(`[context-compiler] [STEP-10] ✅ System prompt built (${systemPrompt.length} chars)`)
332
+ } catch (err) {
333
+ log.error(`[context-compiler] [STEP-10] ❌ FAILED building system prompt: ${JSON.stringify(err)}`)
334
+ throw err
335
+ }
336
+
337
+ // [STEP-10b] Inject current date/time (ENTORNO ACTUAL)
338
+ const userRow = db.query<any, [string]>(
339
+ "SELECT timezone FROM users WHERE id = ?"
340
+ ).get(userId)
341
+ const userTimezone = userRow?.timezone || "UTC"
342
+ const now = new Date()
343
+ const fecha = getUserDate(userTimezone, now)
344
+ const hora = getUserTime(userTimezone, now)
345
+ const workspaceLine = agent.workspace ? `\n**Workspace**: ${agent.workspace} (usa SIEMPRE este path como basePath en herramientas de filesystem)` : ""
346
+ systemPrompt += `\n\n# ENTORNO ACTUAL\n**Fecha**: ${fecha}\n**Hora**: ${hora}\n**Zona horaria**: ${userTimezone}${workspaceLine}\n`
347
+ log.info(`[context-compiler] [STEP-10b] ✅ Injected current date/time: ${fecha} ${hora} (${userTimezone})`)
348
+
349
+ // Inject scratchpad (Strategy: WRITE) — usando TOON para ahorro de tokens
350
+ if (scratchpadNotes.length > 0) {
351
+ const scratchpadData: Record<string, string> = {}
352
+ for (const n of scratchpadNotes) {
353
+ scratchpadData[n.key] = n.value
354
+ }
355
+ // TOON comprime el formato clave-valor
356
+ const scratchpadContent = formatContext(scratchpadData)
357
+ systemPrompt += `\n\n# SCRATCHPAD (Persistent Notes)\n${scratchpadContent}\n`
358
+ }
359
+
360
+ // Inject active/recent project state from DB (coordinator only)
361
+ if (!isWorker) {
362
+ try {
363
+ const recentProjects = db.query<any, []>(`
364
+ SELECT p.id, p.name, p.status, p.progress, p.description,
365
+ COUNT(t.id) as total_tasks,
366
+ SUM(CASE WHEN t.status = 'completed' THEN 1 ELSE 0 END) as done_tasks
367
+ FROM projects p
368
+ LEFT JOIN tasks t ON t.project_id = p.id
369
+ WHERE p.status IN ('active', 'pending', 'paused')
370
+ GROUP BY p.id
371
+ ORDER BY p.updated_at DESC
372
+ LIMIT 10
373
+ `).all()
374
+
375
+ if (recentProjects.length > 0) {
376
+ let projectSection = `\n\n# ESTADO DE PROYECTOS\n`
377
+ for (const proj of recentProjects) {
378
+ projectSection += `\n## ${proj.name} [${proj.status.toUpperCase()}] (${proj.done_tasks}/${proj.total_tasks} tareas, ${proj.progress ?? 0}%)\n`
379
+ if (proj.description) projectSection += `> ${proj.description}\n`
380
+
381
+ // Load tasks for this project
382
+ const tasks = db.query<any, [string]>(
383
+ "SELECT name, status, progress, result FROM tasks WHERE project_id = ? ORDER BY id ASC"
384
+ ).all(proj.id)
385
+ for (const task of tasks) {
386
+ const resultSummary = task.result
387
+ ? ` → ${task.result.substring(0, 120)}${task.result.length > 120 ? "…" : ""}`
388
+ : ""
389
+ projectSection += ` - [${task.status}] ${task.name}${resultSummary}\n`
390
+ }
391
+ }
392
+ systemPrompt += projectSection
393
+ log.info(`[context-compiler] [STEP-10c] Injected ${recentProjects.length} projects into context`)
394
+ }
395
+ } catch (err) {
396
+ log.warn(`[context-compiler] [STEP-10c] Failed to inject projects: ${(err as Error).message}`)
397
+ }
398
+ }
399
+
400
+ // Dynamic tool discovery instruction (coordinator only)
401
+ // Note: MCP tools are already available directly, no search needed
402
+ if (!isWorker) {
403
+ // Build minimal tools documentation from filtered native tools
404
+ const minimalToolsDocs = filteredNativeTools
405
+ .filter(t => MINIMAL_TOOLS.has(t.name))
406
+ .map(t => `- **${t.name}**: ${t.description || "Herramienta nativa"}`)
407
+ .join("\n")
408
+
409
+ systemPrompt += `\n\n# HERRAMIENTAS NATIVAS BÁSICAS (SIEMPRE DISPONIBLES)\n` +
410
+ `Estas 4 herramientas nativas están SIEMPRE disponibles en tu contexto y tienen prioridad sobre MCP:\n\n` +
411
+ `${minimalToolsDocs}\n\n` +
412
+ `**REGLAS DE USO:**\n` +
413
+ `1. Si necesitas una herramienta que no esté en la lista arriba → USA \`search_knowledge\` para encontrarla:\n` +
414
+ ` - Herramientas nativas: \`search_knowledge(type="tools", query="<qué necesitas>")\`\n` +
415
+ ` - Herramientas MCP (externas): \`search_knowledge(type="mcp", query="<qué necesitas>")\`\n` +
416
+ ` - Todo junto: \`search_knowledge(type="all", query="<qué necesitas>")\`\n` +
417
+ `2. NUNCA uses una herramienta MCP si existe una nativa equivalente en el catálogo\n` +
418
+ `3. Las herramientas MCP se activan dinámicamente vía search_knowledge — NO están en tu contexto por defecto\n\n` +
419
+ `# CATÁLOGO DE HERRAMIENTAS\n` +
420
+ `Usá \`search_knowledge\` para descubrir:\n` +
421
+ `- Skills (instrucciones de tareas complejas): type="skills"\n` +
422
+ `- Playbook (buenas prácticas): type="playbook"\n` +
423
+ `- Herramientas nativas: type="tools"\n` +
424
+ `- Herramientas MCP (externas): type="mcp"\n` +
425
+ `- Todo: type="all"\n` +
426
+ `\n## REGLA CRÍTICA — Delegación a workers\n` +
427
+ `Los workers arrancan con herramientas mínimas (save_note, notify, report_progress, search_knowledge).\n` +
428
+ `**ANTES de crear o delegar a un worker**, SIEMPRE debes:\n` +
429
+ `1. Usar \`search_knowledge(type="tools", query="<tarea del worker>")\` para identificar qué herramientas necesita.\n` +
430
+ `2. Incluir esas herramientas en el campo \`tools\` al crear el agente con \`create_agent\`, o\n` +
431
+ ` en el campo \`task_description\` de \`task_delegate\` como instrucción explícita:\n` +
432
+ ` "Usa las herramientas: web_search, fs_read, ... para completar esta tarea."\n` +
433
+ `3. El worker con esa instrucción usará \`search_knowledge\` para activar las tools por nombre.\n` +
434
+ `Ejemplo: si el worker debe investigar en internet → busca "web search herramienta internet, herramientas de navegacion, browser" → obtienes "web_search" → dile al worker que use web_search.\n` +
435
+ `4. Las herramientas se inyectan dinamicamente vía search_knowledge — NO están en tu contexto por defecto\n`
436
+
437
+
438
+ // Inject available skills (minimal + discovered)
439
+ if (allSkills.length > 0) {
440
+ let skillsSection = `\n\n# SKILLS ACTIVAS\n`
441
+ skillsSection += `Usá estas skills como guía cuando sea relevante:\n\n`
442
+
443
+ for (const skill of allSkills) {
444
+ const isMinimal = MINIMAL_SKILL_NAMES.includes(skill.name)
445
+ const badge = isMinimal ? "[SIEMPRE]" : "[DISCOVERED]"
446
+ const desc = skill.description ? ` — ${skill.description}` : ""
447
+ skillsSection += `- **${skill.name}** ${badge}${desc}\n`
448
+ }
449
+
450
+ systemPrompt += skillsSection
451
+ log.info(`[context-compiler] [STEP-10d] Injected ${allSkills.length} skills (${minimalSkills.length} minimal, ${discoveredSkills.length} discovered)`)
452
+ }
453
+
454
+ // Inject Canvas A2UI component documentation
455
+ systemPrompt += `\n\n# 🎨 CANVAS A2UI — Componentes disponibles para \`canvas_render\`\n` +
456
+ `**REGLA**: Usá \`canvas_render\` con el tipo específico en vez de siempre usar \`canvas_show_card\` + markdown.\n\n` +
457
+ `## Tipos de visualización:\n` +
458
+ `- **chart** — Gráficos. Props: \`{type:"bar"|"line"|"area"|"pie", data:[{name,...}], xKey:"name", keys:["valor"], colors:[], title}\`\n` +
459
+ `- **table** — Tablas de datos. Props: \`{title, columns:[{header,key}], data:[{...}]}\`\n` +
460
+ `- **progress** — Barras de progreso. Props: \`{bars:[{label,value:0-100}]}\`\n` +
461
+ `- **markdown** — Texto rich. Props: \`{content:"## título\\n..."}\`\n` +
462
+ `- **card** — Tarjeta con items. Props: \`{title, description, items:[{label,value}], footer}\`\n` +
463
+ `- **accordion** — Secciones colapsables. Props: \`{items:[{value,title,content}]}\`\n` +
464
+ `- **tabs** — Pestañas. Props: \`{tabs:[{value,label,content}]}\`\n` +
465
+ `- **badge** — Etiqueta. Props: \`{label, variant:"default"|"secondary"|"destructive"|"outline"}\`\n` +
466
+ `- **separator** — Línea divisora\n` +
467
+ `- **bee-loader** — Animación de carga. Props: \`{message}\`\n\n` +
468
+ `## Tipos interactivos (bloquean hasta respuesta del usuario):\n` +
469
+ `- **form** — Formulario. Props: \`{title, fields:[{name,label,type,placeholder,options}], submitLabel}\`\n` +
470
+ ` → Tipos de campo: \`text\`, \`email\`, \`number\`, \`textarea\`, \`select\`, \`checkbox\`\n` +
471
+ ` → Al Submit recibirás: \`{data:{campo:valor,...}}\`\n` +
472
+ `- **button** — Botón clickeable. Props: \`{label, variant:"default"|"outline"|"secondary"|"destructive"}\`\n` +
473
+ ` → Al click recibirás: \`{action:"click", data:{label}}\`\n` +
474
+ `- **alert-dialog** — Confirmación. Props: \`{title, description, confirmLabel, cancelLabel}\`\n` +
475
+ ` → Al confirmar recibirás: \`{data:{confirmed:true|false}}\`\n\n` +
476
+ `## Cuándo usar cada uno:\n` +
477
+ `- Estadísticas/datos numéricos → **chart** (bar/line/pie)\n` +
478
+ `- Listas de filas/columnas → **table**\n` +
479
+ `- Texto largo / análisis → **markdown**\n` +
480
+ `- Pedir datos al usuario → **canvas_ask** o **canvas_render con form**\n` +
481
+ `- Confirmar acción peligrosa → **canvas_confirm** o **canvas_render con alert-dialog**\n` +
482
+ `- Mostrar progreso de tarea → **canvas_show_progress**\n\n` +
483
+ `## Ejemplos:\n` +
484
+ `\`\`\`\n` +
485
+ `canvas_render(component:"chart", data:{type:"bar", data:[{mes:"Ene",ventas:1200},{mes:"Feb",ventas:1800}], xKey:"mes", keys:["ventas"], title:"Ventas por mes"})\n` +
486
+ `canvas_render(component:"table", data:{title:"Archivos", columns:[{header:"Nombre",key:"name"},{header:"Tamaño",key:"size"}], data:[{name:"app.ts",size:"12KB"}]})\n` +
487
+ `canvas_render(component:"form", data:{title:"Configuración", fields:[{name:"nombre",label:"Nombre",type:"text"},{name:"tipo",label:"Tipo",type:"select",options:[{value:"a",label:"A"},{value:"b",label:"B"}]}], submitLabel:"Guardar"})\n` +
488
+ `\`\`\`\n\n` +
489
+ `# 🎨🎨 CANVAS A2UI v0.9 — Superficies interactivas ricas\n` +
490
+ `Además de los componentes shadcn, podes crear superficies A2UI v0.9 (protocolo estándar de Google) para UIs ricas e interactivas.\n\n` +
491
+ `## Flujo A2UI:\n` +
492
+ `1. \`a2ui_create_surface\` — Crear la superficie (obligatorio primero)\n` +
493
+ `2. \`a2ui_update_components\` — Enviar componentes (puedes enviar múltiples veces)\n` +
494
+ `3. \`a2ui_update_data_model\` — Actualizar datos dinámicos\n` +
495
+ `4. \`a2ui_delete_surface\` — Eliminar la superficie\n\n` +
496
+ `## Componentes A2UI v0.9:\n` +
497
+ `- **Column** — Contenedor vertical. Props: children (array de IDs), distribution, alignment\n` +
498
+ `- **Row** — Contenedor horizontal. Props: children (array de IDs), distribution, alignment\n` +
499
+ `- **Card** — Tarjeta con child\n` +
500
+ `- **Text** — Texto con usageHint: h1-h5, body, caption, code, label\n` +
501
+ `- **Button** — Botón interactivo. Props: child, variant ("primary"|"borderless"), action\n` +
502
+ `- **TextField** — Campo de texto. Props: label, value (path), variant, placeholder, checks\n` +
503
+ `- **CheckBox** — Checkbox. Props: label, value (path)\n` +
504
+ `- **ChoicePicker** — Selección múltiple. Props: options, variant, maxAllowedSelections, selections\n` +
505
+ `- **Slider** — Slider numérico. Props: value (path), minValue, maxValue\n` +
506
+ `- **DateTimeInput** — Fecha/hora. Props: value (path), enableDate, enableTime\n` +
507
+ `- **List** — Lista scrolleable. Props: children, direction\n` +
508
+ `- **Tabs** — Pestañas. Props: tabItems\n` +
509
+ `- **Modal** — Diálogo. Props: entryPointChild, contentChild\n` +
510
+ `- **Divider** — Línea divisora. Props: axis\n` +
511
+ `- **Image** — Imagen. Props: url, fit, usageHint\n` +
512
+ `- **Icon** — Ícono. Props: name\n` +
513
+ `- **Video** — Video. Props: url\n` +
514
+ `- **AudioPlayer** — Reproductor de audio. Props: url, description\n\n` +
515
+ `## Data Binding (Dynamic Values):\n` +
516
+ `- Valor literal: \`"texto"\` o número directo\n` +
517
+ `- Path del data model: \`{"path": "/user/name"}\` — se resuelve contra el data model de la superficie\n` +
518
+ `- Function call: \`{"call": "formatDate", "args": {"value": {"path": "/date"}, "format": "yyyy-MM-dd"}}\`\n\n` +
519
+ `## Acciones:\n` +
520
+ `- Evento: \`{"event": {"name": "submit_form", "context": {"email": {"path": "/form/email"}}}}\`\n` +
521
+ `- El contexto se resuelve contra el data model antes de enviar\n\n` +
522
+ `## Ejemplo completo:\n` +
523
+ `\`\`\`\n` +
524
+ `// 1. Crear superficie\n` +
525
+ `a2ui_create_surface(surfaceId:"contact_form", catalogId:"https://a2ui.org/specification/v0_9/basic_catalog.json", theme:{primaryColor:"#3B82F6", agentDisplayName:"Asistente"})\n\n` +
526
+ `// 2. Enviar componentes\n` +
527
+ `a2ui_update_components(surfaceId:"contact_form", components:[\n` +
528
+ ` {id:"root", component:"Column", children:{array:["header","name_field","email_field","submit_btn"]}},\n` +
529
+ ` {id:"header", component:"Text", text:"Contacto", usageHint:"h2"},\n` +
530
+ ` {id:"name_field", component:"TextField", label:"Nombre", value:{path:"/form/name"}, variant:"shortText"},\n` +
531
+ ` {id:"email_field", component:"TextField", label:"Email", value:{path:"/form/email"}, variant:"shortText", checks:[{call:"required",args:{value:{path:"/form/email"}},message:"Email es obligatorio"},{call:"email",args:{value:{path:"/form/email"}},message:"Email inválido"}]},\n` +
532
+ ` {id:"submit_text", component:"Text", text:"Enviar"},\n` +
533
+ ` {id:"submit_btn", component:"Button", child:"submit_text", variant:"primary", action:{event:{name:"submit_contact",context:{name:{path:"/form/name"},email:{path:"/form/email"}}}}}\n` +
534
+ `])\n\n` +
535
+ `// 3. Poblar data model\n` +
536
+ `a2ui_update_data_model(surfaceId:"contact_form", path:"/form", value:{name:"",email:""})\n` +
537
+ `\`\`\`\n`
538
+ }
539
+
540
+ // For isolated workers, add task context + tool discovery instruction
541
+ if (isWorker && opts.taskContext) {
542
+ systemPrompt += `\n\n# HERRAMIENTAS DISPONIBLES\n` +
543
+ `Arrancas con herramientas básicas. Si tu tarea requiere herramientas adicionales (web_search, fs_read, browser_navigate, etc.):\n` +
544
+ `1. Usá \`search_knowledge(type="tools", query="<herramienta o tarea>")\` para encontrarlas.\n` +
545
+ `2. Las herramientas que encuentres estarán disponibles para usar inmediatamente.\n` +
546
+ `Si el coordinador te indicó herramientas específicas, buscalas primero con search_knowledge antes de ejecutar tu tarea.\n` +
547
+ `\n# CURRENT TASK\n${opts.taskContext}\n\nFocus ONLY on this task. Do not deviate.`
548
+ }
549
+
550
+ log.info(
551
+ `[context-compiler] ✅ DONE: ${allTools.length} total tools, ` +
552
+ `${toolsForLLM.length} selected tools, ${messages.length} messages, ` +
553
+ `${allSkills.length} skills (${minimalSkills.length} minimal, ${discoveredSkills.length} discovered), ` +
554
+ `isolated=${isWorker}`
555
+ )
556
+
557
+ return {
558
+ systemPrompt,
559
+ messages,
560
+ tools: toolsForLLM,
561
+ allTools,
562
+ skills: allSkills,
563
+ }
564
+ }
565
+
566
+ // Re-export sync functions for gateway/initializer
567
+
@@ -0,0 +1,91 @@
1
+ import type { Config } from "../config/loader.ts";
2
+ import { logger } from "../utils/logger.ts";
3
+ import type { LLMMessage as Message } from "../agent/providers/LLMClient.ts";
4
+
5
+ export interface ContextGuardResult {
6
+ canProceed: boolean;
7
+ currentTokens: number;
8
+ maxTokens: number;
9
+ utilizationPercent: number;
10
+ needsCompaction: boolean;
11
+ }
12
+
13
+ export class ContextGuard {
14
+ private config: Config;
15
+ private log = logger.child("context-guard");
16
+
17
+ constructor(config: Config) {
18
+ this.config = config;
19
+ }
20
+
21
+ estimateTokens(messages: Message[]): number {
22
+ let total = 0;
23
+
24
+ for (const msg of messages) {
25
+ total += Math.ceil(msg.content.length / 4);
26
+
27
+ if (msg.name) {
28
+ total += Math.ceil(msg.name.length / 4);
29
+ }
30
+
31
+ if (msg.tool_calls) {
32
+ for (const tc of msg.tool_calls) {
33
+ total += Math.ceil(tc.function.name.length / 4);
34
+ total += Math.ceil(tc.function.arguments.length / 4);
35
+ }
36
+ }
37
+ }
38
+
39
+ return total;
40
+ }
41
+
42
+ check(messages: Message[], systemPrompt?: string): ContextGuardResult {
43
+ const maxTokens = this.config.agent?.context?.maxTokens || 128000;
44
+ const threshold = this.config.agent?.context?.compactionThreshold || 0.8;
45
+
46
+ let currentTokens = this.estimateTokens(messages);
47
+
48
+ if (systemPrompt) {
49
+ currentTokens += Math.ceil(systemPrompt.length / 4);
50
+ }
51
+
52
+ currentTokens += 500;
53
+
54
+ const utilizationPercent = currentTokens / maxTokens;
55
+ const needsCompaction = utilizationPercent >= threshold;
56
+ const canProceed = currentTokens < maxTokens * 0.95;
57
+
58
+ this.log.debug(`Context check: ${currentTokens}/${maxTokens} tokens (${(utilizationPercent * 100).toFixed(1)}%)`);
59
+
60
+ return {
61
+ canProceed,
62
+ currentTokens,
63
+ maxTokens,
64
+ utilizationPercent,
65
+ needsCompaction,
66
+ };
67
+ }
68
+
69
+ shouldCompact(messages: Message[], systemPrompt?: string): boolean {
70
+ const result = this.check(messages, systemPrompt);
71
+ return result.needsCompaction;
72
+ }
73
+
74
+ getRecommendedAction(messages: Message[], systemPrompt?: string): "proceed" | "compact" | "error" {
75
+ const result = this.check(messages, systemPrompt);
76
+
77
+ if (result.utilizationPercent >= 0.95) {
78
+ return "error";
79
+ }
80
+
81
+ if (result.needsCompaction) {
82
+ return "compact";
83
+ }
84
+
85
+ return "proceed";
86
+ }
87
+ }
88
+
89
+ export function createContextGuard(config: Config): ContextGuard {
90
+ return new ContextGuard(config);
91
+ }