@johpaz/hive-sdk 0.1.4 → 0.1.6

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 (278) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/README.md +78 -23
  3. package/bun.lock +55 -29
  4. package/bunfig.toml +4 -2
  5. package/docs/API-AGENTS.md +78 -27
  6. package/docs/API-CONTEXT-COMPILER.md +31 -34
  7. package/docs/API-TOOLS-SKILLS-CHANNELS.md +58 -22
  8. package/docs/HIVE-HARNESS.md +1 -1
  9. package/docs/INDEX.md +4 -4
  10. package/docs/TEMPLATE-HIVE-APP.md +10 -10
  11. package/package.json +17 -12
  12. package/packages/cli/package.json +2 -2
  13. package/packages/cli/src/commands/create-app.test.ts +36 -7
  14. package/packages/cli/src/commands/init.ts +3 -3
  15. package/packages/cli/src/commands/run.ts +1 -1
  16. package/packages/cli/src/commands/test.ts +37 -25
  17. package/packages/cli/src/commands/trace.ts +30 -28
  18. package/packages/cli/templates/hive-app/.env.example +10 -2
  19. package/packages/cli/templates/hive-app/README.md +103 -0
  20. package/packages/cli/templates/hive-app/hive.config.ts +9 -3
  21. package/packages/cli/templates/hive-app/src/agents/coordinator.ts +8 -1
  22. package/packages/cli/templates/hive-app/src/main.ts +12 -19
  23. package/packages/core/package.json +13 -12
  24. package/packages/core/src/agent/acceptance-checks.ts +172 -0
  25. package/packages/core/src/agent/agent-catalog.ts +348 -0
  26. package/packages/core/src/agent/agent-loop.ts +1373 -0
  27. package/packages/core/src/agent/capability-search.ts +186 -0
  28. package/packages/core/src/agent/catalog-selector.ts +103 -0
  29. package/packages/core/src/agent/{Compaction.ts → compaction.ts} +86 -63
  30. package/packages/core/src/agent/context-compiler.ts +689 -0
  31. package/packages/core/src/agent/conversation-store.ts +381 -0
  32. package/packages/core/src/agent/curator.ts +276 -0
  33. package/packages/core/src/agent/delegation-runtime.ts +241 -0
  34. package/packages/core/src/agent/goal-runner.ts +323 -0
  35. package/packages/core/src/agent/index.ts +17 -12
  36. package/packages/core/src/agent/llm-client.ts +266 -0
  37. package/packages/core/src/agent/llm-providers/anthropic.ts +264 -0
  38. package/packages/core/src/agent/llm-providers/deepseek.ts +8 -0
  39. package/packages/core/src/agent/{providers → llm-providers}/gemini.ts +98 -60
  40. package/packages/core/src/agent/llm-providers/groq.ts +5 -0
  41. package/packages/core/src/agent/llm-providers/hiveagents.ts +253 -0
  42. package/packages/core/src/agent/{providers → llm-providers}/interface.ts +73 -13
  43. package/packages/core/src/agent/llm-providers/kimi.ts +8 -0
  44. package/packages/core/src/agent/llm-providers/minimax.ts +13 -0
  45. package/packages/core/src/agent/llm-providers/mistral.ts +5 -0
  46. package/packages/core/src/agent/llm-providers/modelscope.ts +5 -0
  47. package/packages/core/src/agent/llm-providers/nvidia.ts +5 -0
  48. package/packages/core/src/agent/{providers → llm-providers}/ollama.ts +31 -5
  49. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +418 -0
  50. package/packages/core/src/agent/llm-providers/openai.ts +5 -0
  51. package/packages/core/src/agent/llm-providers/opencode-go.ts +9 -0
  52. package/packages/core/src/agent/llm-providers/openrouter.ts +5 -0
  53. package/packages/core/src/agent/llm-providers/qwen.ts +5 -0
  54. package/packages/core/src/agent/llm-providers/z-ai.ts +5 -0
  55. package/packages/core/src/agent/minimal-loadout.ts +47 -0
  56. package/packages/core/src/agent/playbook-selector.ts +119 -0
  57. package/packages/core/src/agent/{PromptBuilder.ts → prompt-builder.ts} +21 -22
  58. package/packages/core/src/{harness → agent}/proof-packet.ts +16 -21
  59. package/packages/core/src/agent/providers/index.ts +35 -16
  60. package/packages/core/src/agent/reflector.ts +320 -0
  61. package/packages/core/src/agent/routing-intent.ts +22 -0
  62. package/packages/core/src/{harness → agent}/run-epoch.ts +4 -3
  63. package/packages/core/src/{harness → agent}/run-store.ts +142 -81
  64. package/packages/core/src/agent/{Service.ts → service.ts} +37 -26
  65. package/packages/core/src/agent/skill-selector.ts +374 -0
  66. package/packages/core/src/agent/stuck-loop.ts +209 -0
  67. package/packages/core/src/agent/{selectors/ToolSelector.ts → tool-selector.ts} +188 -178
  68. package/packages/core/src/{ace/Tracer.ts → agent/tracer.ts} +37 -27
  69. package/packages/core/src/api/createAgent.test.ts +139 -27
  70. package/packages/core/src/api/createAgent.ts +232 -44
  71. package/packages/core/src/artifacts/store.ts +162 -0
  72. package/packages/core/src/canvas/canvas-manager.ts +161 -0
  73. package/packages/core/src/canvas/canvas.test.ts +8 -4
  74. package/packages/core/src/canvas/emitter.ts +131 -80
  75. package/packages/core/src/canvas/index.ts +1 -3
  76. package/packages/core/src/channels/base.ts +9 -1
  77. package/packages/core/src/channels/discord.ts +5 -4
  78. package/packages/core/src/channels/manager.ts +122 -30
  79. package/packages/core/src/channels/slack.ts +5 -4
  80. package/packages/core/src/channels/telegram.ts +36 -6
  81. package/packages/core/src/channels/webchat.ts +11 -10
  82. package/packages/core/src/channels/whatsapp.ts +23 -7
  83. package/packages/core/src/config/index.ts +13 -2
  84. package/packages/core/src/config/loader.ts +76 -29
  85. package/packages/core/src/ethics/EthicsGuard.test.ts +90 -36
  86. package/packages/core/src/ethics/EthicsGuard.ts +51 -47
  87. package/packages/core/src/events/agent-bus.ts +44 -68
  88. package/packages/core/src/events/channel-narration.ts +150 -0
  89. package/packages/core/src/events/narration.ts +82 -0
  90. package/packages/core/src/events/tool-narration.ts +62 -0
  91. package/packages/core/src/gateway/delegation-groups.ts +258 -0
  92. package/packages/core/src/{harness → gateway}/durable-queue.ts +102 -42
  93. package/packages/core/src/{harness → gateway}/job-store.ts +85 -48
  94. package/packages/core/src/gateway/lane-queue.ts +173 -0
  95. package/packages/core/src/gateway/notification-inbox.ts +57 -0
  96. package/packages/core/src/gateway/server.ts +1 -1
  97. package/packages/core/src/harness/index.ts +46 -27
  98. package/packages/core/src/index.ts +33 -27
  99. package/packages/core/src/mcp/hot-reload.ts +32 -23
  100. package/packages/core/src/mcp/index.ts +6 -3
  101. package/packages/core/src/mcp/singleton.ts +1 -4
  102. package/packages/core/src/mcp/tool-sync.ts +138 -0
  103. package/packages/core/src/memory/Scratchpad.test.ts +39 -20
  104. package/packages/core/src/memory/Scratchpad.ts +27 -34
  105. package/packages/core/src/multimodal/vision-service.ts +44 -38
  106. package/packages/core/src/resilience/retry.ts +95 -0
  107. package/packages/core/src/scheduler/CronScheduler.ts +334 -287
  108. package/packages/core/src/scheduler/index.ts +9 -7
  109. package/packages/core/src/scheduler/integration.ts +46 -26
  110. package/packages/core/src/scheduler/scheduler.test.ts +9 -13
  111. package/packages/core/src/scheduler/types.ts +7 -2
  112. package/packages/core/src/security/Pairing.ts +1 -1
  113. package/packages/core/src/skills/bundled/a2ui/a2ui_dashboard/SKILL.md +176 -0
  114. package/packages/core/src/skills/bundled/a2ui/a2ui_form/SKILL.md +202 -0
  115. package/packages/core/src/skills/bundled/a2ui/a2ui_interactive/SKILL.md +206 -0
  116. package/packages/core/src/skills/bundled/agents/agent_spawner/SKILL.md +173 -0
  117. package/packages/core/src/skills/bundled/agents/memory_manager/SKILL.md +143 -0
  118. package/packages/core/src/skills/bundled/agents/research_and_remember/SKILL.md +139 -0
  119. package/packages/core/src/skills/bundled/agents/task_orchestrator/SKILL.md +98 -0
  120. package/packages/core/src/skills/bundled/api/api_client/SKILL.md +132 -0
  121. package/packages/core/src/skills/bundled/cli/cli_pipeline/SKILL.md +135 -0
  122. package/packages/core/src/skills/bundled/cli/cli_safe_exec/SKILL.md +125 -0
  123. package/packages/core/src/skills/bundled/cli/software_engineering/SKILL.md +23 -0
  124. package/packages/core/src/skills/bundled/cron_manager/SKILL.md +188 -0
  125. package/packages/core/src/skills/bundled/cron_reminder/SKILL.md +112 -0
  126. package/packages/core/src/skills/bundled/filesystem/file_manager/SKILL.md +118 -0
  127. package/packages/core/src/skills/bundled/filesystem/file_read_and_summarize/SKILL.md +109 -0
  128. package/packages/core/src/skills/bundled/filesystem/file_writer/SKILL.md +129 -0
  129. package/packages/core/src/skills/bundled/filesystem/workspace_file_operator/SKILL.md +22 -0
  130. package/packages/core/src/skills/bundled/office/office_document_manager/SKILL.md +262 -0
  131. package/packages/core/src/skills/bundled/search_knowledge/capability_discovery/SKILL.md +75 -0
  132. package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +120 -0
  133. package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +109 -0
  134. package/packages/core/src/skills/bundled/web/web_monitor/SKILL.md +127 -0
  135. package/packages/core/src/skills/bundled/web/web_research/SKILL.md +119 -0
  136. package/packages/core/src/skills/bundled-data.generated.ts +731 -2678
  137. package/packages/core/src/skills/skills.test.ts +52 -11
  138. package/packages/core/src/{harness → storage}/boot-id.ts +5 -2
  139. package/packages/core/src/storage/bootstrap.ts +151 -0
  140. package/packages/core/src/storage/causal-events.ts +84 -0
  141. package/packages/core/src/storage/collections.ts +680 -0
  142. package/packages/core/src/storage/crypto.ts +205 -74
  143. package/packages/core/src/{harness/db-helpers.ts → storage/hive.ts} +63 -7
  144. package/packages/core/src/storage/hivedb.ts +61 -0
  145. package/packages/core/src/storage/index.ts +111 -18
  146. package/packages/core/src/storage/model-id.ts +53 -0
  147. package/packages/core/src/storage/onboarding.ts +540 -972
  148. package/packages/core/src/storage/reconcile.ts +238 -0
  149. package/packages/core/src/storage/seed.ts +572 -406
  150. package/packages/core/src/storage/usage.ts +285 -225
  151. package/packages/core/src/storage/user-email.ts +11 -0
  152. package/packages/core/src/swarm/AgentExecutor.ts +1 -1
  153. package/packages/core/src/swarm/EventBridge.ts +1 -1
  154. package/packages/core/src/swarm/index.ts +12 -9
  155. package/packages/core/src/tool-runtime/index.ts +146 -23
  156. package/packages/core/src/tool-runtime/tool-worker.ts +2 -2
  157. package/packages/core/src/tool-runtime/worker-tools.ts +27 -0
  158. package/packages/core/src/{canvas/a2ui-tools.ts → tools/a2ui/index.ts} +17 -8
  159. package/packages/core/src/tools/agents/get-available-models.ts +36 -54
  160. package/packages/core/src/tools/agents/index.ts +784 -292
  161. package/packages/core/src/tools/api/api-request.test.ts +164 -0
  162. package/packages/core/src/tools/api/api-request.ts +174 -0
  163. package/packages/core/src/tools/api/index.ts +16 -0
  164. package/packages/core/src/tools/cli/index.ts +4 -0
  165. package/packages/core/src/tools/core/index.ts +281 -112
  166. package/packages/core/src/tools/cron/index.ts +121 -124
  167. package/packages/core/src/tools/index.ts +63 -78
  168. package/packages/core/src/tools/office/office-escribir-xlsx.ts +3 -1
  169. package/packages/core/src/tools/types.ts +3 -1
  170. package/packages/core/src/tools/web/artifact-inspect.ts +23 -0
  171. package/packages/core/src/tools/web/browser-backend.ts +129 -0
  172. package/packages/core/src/tools/web/browser-screenshot.ts +26 -5
  173. package/packages/core/src/tools/web/browser-service.ts +80 -35
  174. package/packages/core/src/tools/web/browser-type.ts +3 -8
  175. package/packages/core/src/tools/web/index.ts +4 -4
  176. package/packages/core/src/tools/web/webview-backend.ts +412 -0
  177. package/packages/core/src/voice/index.ts +89 -63
  178. package/packages/core/src/workers/agent.worker.ts +2 -2
  179. package/packages/core/src/workers/workers.test.ts +3 -10
  180. package/scripts/bump-version.ts +248 -0
  181. package/scripts/generate-skill-bundle.ts +108 -0
  182. package/test/acceptance-checks.test.ts +403 -0
  183. package/test/agent-loop-terminal-synthesis.test.ts +32 -0
  184. package/test/browser-backend.test.ts +308 -0
  185. package/test/catalog-agents-stay-enabled.test.ts +117 -0
  186. package/test/causal-events.test.ts +117 -0
  187. package/test/compaction.test.ts +105 -0
  188. package/test/context-compiler.test.ts +269 -0
  189. package/test/curator.test.ts +130 -0
  190. package/test/durable-queue.test.ts +114 -0
  191. package/test/harness-barrel.test.ts +64 -0
  192. package/test/hive-helpers.test.ts +130 -0
  193. package/test/hivedb-search.test.ts +189 -0
  194. package/test/internal-turns.test.ts +166 -0
  195. package/test/job-idempotency.test.ts +68 -0
  196. package/test/job-retry-backoff.test.ts +184 -0
  197. package/test/job-store.test.ts +381 -0
  198. package/test/llm-retry.test.ts +97 -0
  199. package/test/memory-perf.test.ts +774 -0
  200. package/test/minimal-loadout.test.ts +78 -0
  201. package/test/model-catalog.test.ts +105 -0
  202. package/test/preload.ts +12 -0
  203. package/test/reflector.test.ts +320 -0
  204. package/test/retention-cap.test.ts +91 -0
  205. package/test/retired-capabilities-pruned.test.ts +192 -0
  206. package/test/run-store.test.ts +355 -0
  207. package/test/scratchpad.test.ts +74 -0
  208. package/test/secrets-durability.test.ts +119 -0
  209. package/test/seed-model-reseed.test.ts +155 -0
  210. package/test/setup-agent-seed.test.ts +264 -0
  211. package/test/tool-inventory.test.ts +65 -0
  212. package/test/tool-runtime.test.ts +258 -0
  213. package/test/tool-selector-runtime-tools.test.ts +117 -0
  214. package/test/toon.test.ts +429 -0
  215. package/tsconfig.json +2 -0
  216. package/packages/core/src/ace/Curator.ts +0 -158
  217. package/packages/core/src/ace/Reflector.ts +0 -200
  218. package/packages/core/src/ace/index.ts +0 -4
  219. package/packages/core/src/agent/AgentRunner.ts +0 -711
  220. package/packages/core/src/agent/ContextCompiler.ts +0 -567
  221. package/packages/core/src/agent/ContextGuard.ts +0 -91
  222. package/packages/core/src/agent/ConversationStore.ts +0 -254
  223. package/packages/core/src/agent/Hooks.ts +0 -166
  224. package/packages/core/src/agent/StuckLoop.ts +0 -133
  225. package/packages/core/src/agent/providers/LLMClient.ts +0 -149
  226. package/packages/core/src/agent/providers/anthropic.ts +0 -212
  227. package/packages/core/src/agent/providers/openai-compat.ts +0 -231
  228. package/packages/core/src/agent/selectors/PlaybookSelector.ts +0 -121
  229. package/packages/core/src/agent/selectors/SkillSelector.ts +0 -322
  230. package/packages/core/src/agent/selectors/index.ts +0 -6
  231. package/packages/core/src/auth/auth.ts +0 -121
  232. package/packages/core/src/auth/index.ts +0 -1
  233. package/packages/core/src/canvas/CanvasManager.ts +0 -390
  234. package/packages/core/src/canvas/canvas-tools.ts +0 -448
  235. package/packages/core/src/harness/collections.ts +0 -98
  236. package/packages/core/src/harness/goal-verifier.ts +0 -141
  237. package/packages/core/src/harness/harness.test.ts +0 -236
  238. package/packages/core/src/harness/reconcile.ts +0 -149
  239. package/packages/core/src/mcp/MCPToolAdapter.ts +0 -176
  240. package/packages/core/src/multimodal/VisionService.ts +0 -293
  241. package/packages/core/src/scheduler/dag/AgentExecutor.ts +0 -53
  242. package/packages/core/src/scheduler/dag/DAGScheduler.ts +0 -250
  243. package/packages/core/src/scheduler/dag/EventBridge.ts +0 -122
  244. package/packages/core/src/scheduler/dag/TaskGraph.ts +0 -192
  245. package/packages/core/src/scheduler/dag/TaskNode.ts +0 -97
  246. package/packages/core/src/scheduler/dag/TaskResult.ts +0 -22
  247. package/packages/core/src/scheduler/dag/errors.ts +0 -37
  248. package/packages/core/src/scheduler/dag/index.ts +0 -26
  249. package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +0 -97
  250. package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +0 -21
  251. package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +0 -46
  252. package/packages/core/src/storage/HiveDBStorage.ts +0 -64
  253. package/packages/core/src/storage/SQLiteStorage.ts +0 -414
  254. package/packages/core/src/storage/hiveSeed.ts +0 -308
  255. package/packages/core/src/storage/hiveStorage.test.ts +0 -38
  256. package/packages/core/src/storage/schema.ts +0 -689
  257. package/packages/core/src/storage/storage.test.ts +0 -37
  258. package/packages/core/src/swarm/AgentBus.ts +0 -460
  259. package/packages/core/src/swarm/EventBus.ts +0 -169
  260. package/packages/core/src/swarm/WorkerPool.ts +0 -236
  261. package/packages/core/src/tools/bridge-events.ts +0 -26
  262. package/packages/core/src/tools/canvas/index.ts +0 -375
  263. package/packages/core/src/tools/codebridge/index.ts +0 -342
  264. package/packages/core/src/tools/meeting/index.ts +0 -353
  265. package/packages/core/src/tools/projects/index.ts +0 -37
  266. package/packages/core/src/tools/projects/project-create.ts +0 -94
  267. package/packages/core/src/tools/projects/project-done.ts +0 -66
  268. package/packages/core/src/tools/projects/project-fail.ts +0 -66
  269. package/packages/core/src/tools/projects/project-list.ts +0 -96
  270. package/packages/core/src/tools/projects/project-update.ts +0 -72
  271. package/packages/core/src/tools/projects/task-create.ts +0 -68
  272. package/packages/core/src/tools/projects/task-evaluate.ts +0 -93
  273. package/packages/core/src/tools/projects/task-update.ts +0 -93
  274. package/packages/core/src/tools/voice/index.ts +0 -104
  275. package/packages/core/src/tools/web/api-request.test.ts +0 -170
  276. package/packages/core/src/tools/web/api-request.ts +0 -239
  277. package/test/setup-db.ts +0 -216
  278. /package/packages/core/src/agent/{NativeTools.ts → native-tools.ts} +0 -0
@@ -0,0 +1,689 @@
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 { col, fromIndexable } from "../storage/hive"
25
+ import type { AgentDoc, ModelDoc } from "../storage/collections"
26
+ import { logger } from "../utils/logger"
27
+ import type { LLMMessage, LLMToolDef, ContentPart } from "./llm-client"
28
+ import type { MCPClientManager } from "../mcp/index.ts"
29
+ import { syncToolCatalogToIndex, mcpToolFullName } from "./tool-selector"
30
+ import { syncSkillsToIndex, getMinimalSkills, selectSkills, getSkillByName, type SkillDescriptor } from "./skill-selector"
31
+ import { syncPlaybookToIndex, selectPlaybookRules } from "./playbook-selector"
32
+ import { getRecentMessages, getSummary, getScratchpad, toAPIMessages } from "./conversation-store"
33
+ import { formatContext, estimateTokens } from "../utils/toon"
34
+ import { buildSystemPromptWithProjects } from "./prompt-builder"
35
+ import { createAllTools } from "../tools/index.ts"
36
+ import { resolveUserId } from "../storage/onboarding"
37
+ import { getMCPManager as getSingletonMCPManager } from "../mcp/singleton"
38
+ import { syncMCPToolsToDB, syncMCPToolsToIndex } from "../mcp/tool-sync"
39
+ import { getUserDate, getUserTime } from "../utils/date"
40
+ import { loadConfig } from "../config/loader"
41
+ import { getHiveDb } from "../storage/hivedb"
42
+ import { listCatalogAgents, renderAgentRoutingCatalog } from "./catalog-selector"
43
+ import { expandToolAllowlist } from "./delegation-runtime"
44
+ import { MINIMAL_TOOLS } from "./minimal-loadout"
45
+
46
+ const log = logger.child("context-compiler")
47
+
48
+ // Configuration constants
49
+ const KEEP_LAST_N_MESSAGES = 15 // Always keep last N messages (Strategy: SELECT) — only user+assistant text, no tool results
50
+ const DEFAULT_CONTEXT_WINDOW = 250000 // Default context window when model is unknown
51
+ const COMPACT_RATIO = 0.80 // Reserve budget: truncate system prompt when it would exceed 80% of context window
52
+ const MAX_SYSTEM_PROMPT_CHARS_CAP = 128000 // Hard cap for pathological prompts; normal budget is model-aware
53
+ const MCP_LAZY_CONNECT_TIMEOUT_MS = 8000 // Bound for on-demand connect of a dormant MCP server
54
+ const SUMMARY_MAX_CHARS = 4000 // Cap the compacted summary so it can't itself blow the system-prompt budget
55
+
56
+
57
+
58
+ /** Bounds a dormant MCP server's on-demand wake so one dead server can't stall a whole turn. */
59
+ async function withMcpConnectTimeout(op: () => Promise<void>, timeoutMs: number): Promise<void> {
60
+ let timer: ReturnType<typeof setTimeout>
61
+ const timeout = new Promise<never>((_, reject) => {
62
+ timer = setTimeout(() => reject(new Error(`MCP connect timed out after ${timeoutMs}ms`)), timeoutMs)
63
+ })
64
+ try {
65
+ await Promise.race([op(), timeout])
66
+ } finally {
67
+ clearTimeout(timer!)
68
+ }
69
+ }
70
+
71
+ // ─── Types ─────────────────────────────────────────────────────────────────
72
+
73
+ // Simple tool interface for context compilation
74
+ export interface ContextTool {
75
+ name: string
76
+ description: string
77
+ parameters: Record<string, unknown>
78
+ execute?: (params: Record<string, unknown>) => Promise<unknown>
79
+ /** Per-tool timeout (ms) override from the Tool definition. */
80
+ timeoutMs?: number
81
+ }
82
+
83
+ export interface CompiledContext {
84
+ systemPrompt: string
85
+ /** The `# RESUMEN DE LA CONVERSACIÓN` block already folded into `systemPrompt` — exposed so a caller-supplied `systemPromptOverride` can compose it in instead of discarding it (see agent-loop.ts). "" when compaction hasn't fired. */
86
+ conversationSummarySection: string
87
+ messages: LLMMessage[]
88
+ tools: LLMToolDef[]
89
+ allTools: ContextTool[]
90
+ skills: SkillDescriptor[] // Skills loaded (minimal + discovered)
91
+ }
92
+
93
+ // ─── G9 causal context (buildAgentContext) ────────────────────────────────
94
+
95
+ interface AgentContextItemShape {
96
+ type: "decision" | "toolCall" | "anomaly" | "episode" | "phaseSummary"
97
+ seq?: number
98
+ phase?: string
99
+ text?: string
100
+ taskId?: string
101
+ summary?: string
102
+ keyDecisions?: number[]
103
+ }
104
+
105
+ interface AgentContextShape {
106
+ items: AgentContextItemShape[]
107
+ similarEpisodes: Array<{ taskId: string; summary: string }>
108
+ anomalies: AgentContextItemShape[]
109
+ }
110
+
111
+ function formatCausalContextItem(item: AgentContextItemShape): string | null {
112
+ switch (item.type) {
113
+ case "decision":
114
+ case "toolCall":
115
+ case "phaseSummary":
116
+ return item.text ? `- ${item.text}` : null
117
+ case "anomaly":
118
+ return item.text ? `- ⚠ ${item.text}` : null
119
+ case "episode":
120
+ return item.summary ? `- (episodio previo) ${item.summary}` : null
121
+ default:
122
+ return null
123
+ }
124
+ }
125
+
126
+ /** Maps the stored AgentDoc (sentinel-encoded FKs) to the shape context-compiler works with. */
127
+ function fromAgentDoc(doc: AgentDoc) {
128
+ return {
129
+ id: doc.id,
130
+ user_id: doc.user_id,
131
+ name: doc.name,
132
+ role: doc.role,
133
+ system_prompt: doc.system_prompt,
134
+ tone: doc.tone,
135
+ provider_id: fromIndexable(doc.provider_id),
136
+ model_id: fromIndexable(doc.model_id),
137
+ tools_json: doc.tools_json,
138
+ tool_allowlist_json: doc.tool_allowlist_json,
139
+ skills_json: doc.skills_json,
140
+ active_mcp_json: doc.active_mcp_json,
141
+ mcp_server_ids_json: doc.mcp_server_ids_json,
142
+ source: doc.source,
143
+ max_iterations: doc.max_iterations,
144
+ workspace: doc.workspace,
145
+ }
146
+ }
147
+
148
+ // ─── Main compiler ─────────────────────────────────────────────────────────
149
+
150
+ /**
151
+ * Compile context for agent execution implementing 4 strategies:
152
+ * 1. WRITE - Load scratchpad notes
153
+ * 2. SELECT - Tool loadout, playbook rules, selective history
154
+ * 3. COMPRESS - Use summaries, clear old tool results
155
+ * 4. ISOLATE - Worker gets minimal context
156
+ */
157
+ export async function compileContext(opts: {
158
+ agentId: string
159
+ threadId: string
160
+ userId?: string
161
+ userMessage: string | ContentPart[]
162
+ channel?: string
163
+ isolated?: boolean
164
+ taskContext?: string | ContentPart[]
165
+ mcpManager?: MCPClientManager | null
166
+ /** G9 causal stream id for this invocation (agent-loop.ts's causalStreamId). */
167
+ causalStreamId?: string
168
+ }): Promise<CompiledContext> {
169
+ const { agentId, threadId, mcpManager, userMessage, isolated, taskContext } = opts
170
+
171
+ // Fallback: Get MCP Manager from singleton if not provided
172
+ const effectiveMcpManager = mcpManager ?? (() => {
173
+ const singletonMcp = getSingletonMCPManager()
174
+ if (singletonMcp) {
175
+ log.info(`[context-compiler] Using MCP Manager from singleton`)
176
+ return singletonMcp
177
+ }
178
+ return null
179
+ })()
180
+
181
+ // Resolve userId from database with priority: explicit param → channel identity → single user
182
+ const userId = opts.userId || (await resolveUserId({
183
+ threadId,
184
+ channel: opts.channel,
185
+ channelUserId: threadId
186
+ })) || threadId || ""
187
+
188
+ // [STEP-1] Load agent config
189
+ log.info(`[context-compiler] [STEP-1] Loading agent config for id=${agentId}`)
190
+ let agent: ReturnType<typeof fromAgentDoc>
191
+ try {
192
+ const agentsCol = await col<AgentDoc>("agents")
193
+ const entry = await agentsCol.get(agentId)
194
+ agent = entry ? fromAgentDoc(entry.doc) : undefined
195
+ } catch (err) {
196
+ log.error(`[context-compiler] [STEP-1] ❌ FAILED loading agent: ${JSON.stringify(err)}`)
197
+ throw err
198
+ }
199
+
200
+ if (!agent) {
201
+ throw new Error(`Agent not found: ${agentId}`)
202
+ }
203
+
204
+ const isWorker = agent.role === 'worker' || !!isolated
205
+ const canDiscoverAllMcp = agent.role === "coordinator" && !isolated
206
+ // A catalog-seeded agent (agent-catalog.ts) has its loadout fully
207
+ // curated (tool_allowlist_json/skills_json/mcp scope) — plain agent_create
208
+ // workers and the coordinator get the open/minimal defaults below instead.
209
+ const isCatalogAgent = agent.source === "catalog"
210
+ log.info(`[context-compiler] [STEP-1] ✅ Compiling for ${isWorker ? 'worker' : 'coordinator'} agent=${agent.name}`)
211
+
212
+ // Load model's context window for compaction decisions
213
+ let modelContextWindow = DEFAULT_CONTEXT_WINDOW
214
+ if (agent.model_id) {
215
+ try {
216
+ const modelsCol = await col<ModelDoc>("models")
217
+ // Id completo: el recorte del primer segmento fallaba para todo modelo
218
+ // con barra en el nombre y dejaba el context window en el default.
219
+ const modelEntry = await modelsCol.get(agent.model_id)
220
+ if (modelEntry?.doc.context_window) modelContextWindow = modelEntry.doc.context_window
221
+ } catch { /* use default */ }
222
+ }
223
+
224
+ // [STEP-2] STRATEGY 1: WRITE — Load scratchpad (persistent notes)
225
+ log.info(`[context-compiler] [STEP-2] Loading scratchpad...`)
226
+ let scratchpadNotes: Awaited<ReturnType<typeof getScratchpad>> = []
227
+ try {
228
+ scratchpadNotes = await getScratchpad(threadId)
229
+ log.info(`[context-compiler] [STEP-2] ✅ Loaded ${scratchpadNotes.length} scratchpad notes`)
230
+ } catch (err) {
231
+ log.error(`[context-compiler] [STEP-2] ❌ FAILED loading scratchpad: ${JSON.stringify(err)}`)
232
+ throw err
233
+ }
234
+
235
+ // [STEP-3c] Load MCP tools (executors only — index sync happens here too)
236
+ log.info(`[context-compiler] [STEP-3c] Loading MCP tools...`)
237
+ const mcpToolExecutors: ContextTool[] = []
238
+
239
+ if (effectiveMcpManager) {
240
+ try {
241
+ const mcpServersCol = await col<import("../storage/collections").McpServerDoc>("mcpServers")
242
+ const assignedMcpIds = new Set<string>([
243
+ ...(agent.mcp_server_ids_json ? JSON.parse(agent.mcp_server_ids_json) : []),
244
+ ...(agent.active_mcp_json ? JSON.parse(agent.active_mcp_json) : []),
245
+ ])
246
+ const dbServers = (await mcpServersCol.scan({}))
247
+ .map(e => e.doc)
248
+ .filter(s => s.enabled && (canDiscoverAllMcp || assignedMcpIds.has(s.id)))
249
+
250
+ for (const server of dbServers) {
251
+ // Try ID first (normalized), then name
252
+ let resolvedServerKey = server.id
253
+ let serverTools = effectiveMcpManager.getServerTools(server.id)
254
+ if (!serverTools || serverTools.length === 0) {
255
+ resolvedServerKey = server.name
256
+ serverTools = effectiveMcpManager.getServerTools(server.name)
257
+ }
258
+
259
+ // Lazy wake: the server is registered but dormant (no agent has leased
260
+ // it yet). Connect on demand so the coordinator isn't permanently cut off
261
+ // from tools nothing else ever wakes.
262
+ if (!serverTools || serverTools.length === 0) {
263
+ for (const key of [server.id, server.name]) {
264
+ try {
265
+ await withMcpConnectTimeout(() => effectiveMcpManager!.connectServer(key), MCP_LAZY_CONNECT_TIMEOUT_MS)
266
+ const woken = effectiveMcpManager.getServerTools(key)
267
+ if (woken && woken.length > 0) {
268
+ resolvedServerKey = key
269
+ serverTools = woken
270
+ break
271
+ }
272
+ } catch (err) {
273
+ log.warn(`[context-compiler] [STEP-3c] Lazy connect failed for ${server.name} (${key}): ${(err as Error).message}`)
274
+ }
275
+ }
276
+ }
277
+
278
+ if (serverTools && serverTools.length > 0) {
279
+ log.info(`[context-compiler] [STEP-3c] Server ${server.name}: ${serverTools.length} tools`)
280
+
281
+ for (const mcpTool of serverTools) {
282
+ // Sanitized name valid for all LLM providers (no spaces, max 64 chars)
283
+ const fullName = mcpToolFullName(server.name, mcpTool.name)
284
+
285
+ // Skip tools whose sanitized name is empty or fails provider validation
286
+ if (!fullName || !/^[a-zA-Z0-9_-]{1,64}$/.test(fullName)) {
287
+ log.warn(`[context-compiler] Skipping MCP tool with unsupported name: "${mcpTool.name}" (server: ${server.name}, sanitized: "${fullName}")`)
288
+ continue
289
+ }
290
+
291
+ // Executor for agent-loop (has the real call)
292
+ mcpToolExecutors.push({
293
+ name: fullName,
294
+ description: mcpTool.description || `Tool from ${server.name}`,
295
+ parameters: mcpTool.inputSchema || { type: "object", properties: {} },
296
+ execute: async (params: Record<string, unknown>) => {
297
+ // Return raw JS value — agent-loop will TOON-encode via formatToolResult.
298
+ // Never pre-stringify here: formatToolResult(string) double-encodes.
299
+ return await effectiveMcpManager.callTool(resolvedServerKey, mcpTool.name, params)
300
+ },
301
+ })
302
+
303
+ }
304
+ } else {
305
+ log.warn(`[context-compiler] [STEP-3c] Server ${server.name} has no tools (not connected yet)`)
306
+ }
307
+ }
308
+
309
+ log.info(`[context-compiler] [STEP-3c] ✅ Loaded ${mcpToolExecutors.length} MCP tools`)
310
+
311
+ // Persist MCP tool definitions to DB for search_knowledge (HiveDB index)
312
+ if (mcpToolExecutors.length > 0) {
313
+ try {
314
+ for (const server of dbServers) {
315
+ let serverTools = effectiveMcpManager!.getServerTools(server.id)
316
+ if (!serverTools || serverTools.length === 0) {
317
+ serverTools = effectiveMcpManager!.getServerTools(server.name)
318
+ }
319
+ if (serverTools && serverTools.length > 0) {
320
+ await syncMCPToolsToDB(server.id || server.name, server.name, serverTools)
321
+ }
322
+ }
323
+ await syncMCPToolsToIndex();
324
+ log.info(`[context-compiler] [STEP-3c] ✅ Persisted MCP tools to DB + HiveDB index`)
325
+ } catch (syncErr) {
326
+ log.warn(`[context-compiler] [STEP-3c] ⚠️ Failed to persist MCP tools to DB: ${(syncErr as Error).message}`)
327
+ }
328
+ }
329
+ } catch (err) {
330
+ log.error(`[context-compiler] [STEP-3c] ❌ Failed: ${(err as Error).message}`)
331
+ }
332
+ } else {
333
+ log.info(`[context-compiler] [STEP-3c] ⚠️ No MCP manager, skipping MCP tools`)
334
+ }
335
+
336
+ // [STEP-4] Minimal tool set — agent discovers the rest via search_knowledge
337
+ log.info(`[context-compiler] [STEP-4] Building minimal tool set`)
338
+
339
+ // [STEP-8] Combine native tools + MCP executors loaded in STEP-3c
340
+ const config = { tools: {} }
341
+ const allNativeTools = createAllTools(config)
342
+ const nativeTools: ContextTool[] = allNativeTools.map(t => ({
343
+ name: t.name,
344
+ description: t.description || "",
345
+ parameters: t.parameters as any,
346
+ execute: t.execute,
347
+ timeoutMs: t.timeoutMs,
348
+ }))
349
+
350
+ let allTools = [...nativeTools, ...mcpToolExecutors]
351
+
352
+ // Only native minimal tools in LLM context
353
+ // MCP tools are discovered dynamically via search_knowledge(type="mcp")
354
+ let filteredNativeTools: ContextTool[] = nativeTools.filter(t => MINIMAL_TOOLS.has(t.name))
355
+ if (isCatalogAgent) {
356
+ const allowedNames = new Set<string>(
357
+ agent.tool_allowlist_json
358
+ ? expandToolAllowlist(
359
+ JSON.parse(agent.tool_allowlist_json),
360
+ nativeTools.map((tool) => tool.name),
361
+ )
362
+ : agent.tools_json
363
+ ? JSON.parse(agent.tools_json)
364
+ : [],
365
+ )
366
+ filteredNativeTools = nativeTools.filter((tool) => allowedNames.has(tool.name))
367
+ allTools = [...filteredNativeTools, ...mcpToolExecutors]
368
+ }
369
+
370
+ const nativeToolsForLLM: LLMToolDef[] = filteredNativeTools.map(t => ({
371
+ type: "function" as const,
372
+ function: {
373
+ name: t.name,
374
+ description: t.description,
375
+ parameters: t.parameters,
376
+ },
377
+ }))
378
+
379
+ let toolsForLLM: LLMToolDef[] = nativeToolsForLLM
380
+ // Workers receive MCP tools directly only when prepareDelegation has
381
+ // activated a persistent assignment (or the verifier's internal readback
382
+ // scope). The coordinator keeps every enabled MCP executor discoverable,
383
+ // but out of its initial prompt.
384
+ if (!canDiscoverAllMcp && mcpToolExecutors.length > 0) {
385
+ toolsForLLM = [
386
+ ...toolsForLLM,
387
+ ...mcpToolExecutors.map((tool) => ({
388
+ type: "function" as const,
389
+ function: {
390
+ name: tool.name,
391
+ description: tool.description,
392
+ parameters: tool.parameters,
393
+ },
394
+ })),
395
+ ]
396
+ }
397
+
398
+ log.info(`[context-compiler] [STEP-4] Minimal native tool set: ${filteredNativeTools.length} tools`)
399
+ log.info(
400
+ canDiscoverAllMcp
401
+ ? `[context-compiler] [STEP-4b] MCP tools discoverable by coordinator: ${mcpToolExecutors.length}`
402
+ : `[context-compiler] [STEP-4b] MCP tools assigned directly to worker: ${mcpToolExecutors.length}`,
403
+ )
404
+ log.info(`[context-compiler] [STEP-8] ✅ Combined tools: ${allTools.length} total executors, ${toolsForLLM.length} in LLM context`)
405
+
406
+ // [STEP-8b] STRATEGY 2: SELECT — Skill Loadout (minimal + discovered)
407
+ log.info(`[context-compiler] [STEP-8b] Building skill loadout...`)
408
+ let minimalSkills: SkillDescriptor[] = []
409
+ let discoveredSkills: SkillDescriptor[] = []
410
+
411
+ try {
412
+ // Load minimal skills (always available)
413
+ minimalSkills = await getMinimalSkills()
414
+ log.info(`[context-compiler] [STEP-8b] ✅ Loaded ${minimalSkills.length} minimal skills`)
415
+
416
+ // Discover additional skills via HiveDB search (coordinator only)
417
+ if (!isWorker) {
418
+ const inputForSkills = taskContext || userMessage
419
+ const textMessage = typeof inputForSkills === "string"
420
+ ? inputForSkills
421
+ : Array.isArray(inputForSkills)
422
+ ? inputForSkills.filter(p => p.type === "text").map(p => (p as any).text).join("\n")
423
+ : String(inputForSkills)
424
+ discoveredSkills = await selectSkills(textMessage)
425
+ log.info(`[context-compiler] [STEP-8b] ✅ Discovered ${discoveredSkills.length} additional skills via HiveDB`)
426
+ }
427
+ if (isCatalogAgent && agent.skills_json) {
428
+ for (const skillId of JSON.parse(agent.skills_json) as string[]) {
429
+ const forced = await getSkillByName(skillId)
430
+ if (forced) discoveredSkills.push(forced)
431
+ }
432
+ log.info(`[context-compiler] [STEP-8b] ✅ Loaded ${discoveredSkills.length} catalog agent skills`)
433
+ }
434
+ } catch (err) {
435
+ log.warn(`[context-compiler] [STEP-8b] ⚠️ Skill loadout failed: ${(err as Error).message}`)
436
+ }
437
+
438
+ // Combine skills (minimal + discovered, avoiding duplicates)
439
+ const skillMap = new Map<string, SkillDescriptor>()
440
+ for (const skill of minimalSkills) {
441
+ skillMap.set(skill.name, skill)
442
+ }
443
+ for (const skill of discoveredSkills) {
444
+ if (!skillMap.has(skill.name)) {
445
+ skillMap.set(skill.name, skill)
446
+ }
447
+ }
448
+ const allSkills = Array.from(skillMap.values())
449
+
450
+ // [STEP-9] STRATEGY 3: COMPRESS — Load history with compaction
451
+ log.info(`[context-compiler] [STEP-9] Loading conversation history...`)
452
+ let recentMessages: Awaited<ReturnType<typeof getRecentMessages>> = []
453
+ try {
454
+ recentMessages = await getRecentMessages(threadId, KEEP_LAST_N_MESSAGES)
455
+ log.info(`[context-compiler] [STEP-9] ✅ Loaded ${recentMessages.length} recent messages`)
456
+ } catch (err) {
457
+ log.error(`[context-compiler] [STEP-9] ❌ FAILED loading history: ${JSON.stringify(err)}`)
458
+ throw err
459
+ }
460
+
461
+ // Check if we need to use summary (conversation is long)
462
+ let summary: Awaited<ReturnType<typeof getSummary>> = null
463
+ try {
464
+ summary = await getSummary(threadId)
465
+ } catch (err) {
466
+ log.error(`[context-compiler] [STEP-9b] ❌ FAILED loading summary: ${JSON.stringify(err)}`)
467
+ throw err
468
+ }
469
+
470
+ // A summary applies when it covers messages that fell out of the current
471
+ // window — i.e. the window's earliest message is strictly after the last
472
+ // message the summary covers. (Comparing window tokens against a
473
+ // context-window-sized threshold, as before, almost never fires: the
474
+ // window is capped at KEEP_LAST_N_MESSAGES messages, which rarely
475
+ // approaches 80% of the context window on its own — so the summary was
476
+ // computed, stored, and the user notified, but never actually reached the
477
+ // model.)
478
+ const summaryApplies = !!(
479
+ summary && summary.last_message_id > 0 &&
480
+ (recentMessages.length === 0 || recentMessages[0].id > summary.last_message_id)
481
+ )
482
+
483
+ const conversationSummarySection = summaryApplies
484
+ ? `\n\n# RESUMEN DE LA CONVERSACIÓN (turnos anteriores, compactados)\n${summary!.summary.slice(0, SUMMARY_MAX_CHARS)}\n`
485
+ : ""
486
+
487
+ if (summaryApplies) {
488
+ log.info(`[context-compiler] [STEP-9c] Summary applies (${summary!.messages_covered} messages compressed) — folded into system prompt`)
489
+ }
490
+
491
+ // Never a second "system" turn here — every provider (Gemini, Anthropic,
492
+ // OpenAI-compat) hoists ALL role:"system" messages into a single top-level
493
+ // system instruction, so a second one either gets silently merged (fine)
494
+ // or — on a context-overflow retry that keeps only the LAST system message
495
+ // (openai-compat-base.ts) — silently replaces the real prompt. The summary
496
+ // lives in `systemPrompt` instead (see conversationSummarySection below).
497
+ const messages: LLMMessage[] = toAPIMessages(recentMessages)
498
+
499
+ // [STEP-10] STRATEGY 4: ISOLATE — Build context based on agent role
500
+ log.info(`[context-compiler] [STEP-10] Building system prompt...`)
501
+ let systemPrompt: string
502
+ try {
503
+ systemPrompt = await buildSystemPromptWithProjects({ agentId, userId })
504
+ log.info(`[context-compiler] [STEP-10] ✅ System prompt built (${systemPrompt.length} chars)`)
505
+ } catch (err) {
506
+ log.error(`[context-compiler] [STEP-10] ❌ FAILED building system prompt: ${JSON.stringify(err)}`)
507
+ throw err
508
+ }
509
+
510
+ // [STEP-10b] Inject current date/time (ENTORNO ACTUAL)
511
+ const usersCol = await col<import("../storage/collections").UserDoc>("users")
512
+ const userRow = await usersCol.get(userId)
513
+ const userTimezone = userRow?.doc.timezone || "UTC"
514
+ const now = new Date()
515
+ const fecha = getUserDate(userTimezone, now)
516
+ const hora = getUserTime(userTimezone, now)
517
+ const workspaceLine = agent.workspace ? `\n**Workspace**: ${agent.workspace} (usa SIEMPRE este path como basePath en herramientas de filesystem)` : ""
518
+ systemPrompt += `\n\n# ENTORNO ACTUAL\n**Fecha**: ${fecha}\n**Hora**: ${hora}\n**Zona horaria**: ${userTimezone}${workspaceLine}\n`
519
+ log.info(`[context-compiler] [STEP-10b] ✅ Injected current date/time: ${fecha} ${hora} (${userTimezone})`)
520
+
521
+ // Placed early (right after ENTORNO ACTUAL), not appended at the end: the
522
+ // truncation guard below cuts the system prompt's TAIL when it's over
523
+ // budget, so a summary appended last would be the first thing silently
524
+ // dropped on an oversized prompt.
525
+ systemPrompt += conversationSummarySection
526
+
527
+ // Only the live roster goes here — how to delegate, fan-out/fan-in and the
528
+ // execution-truth rules are static doctrine and live in the coordinator's
529
+ // stored prompt (storage/onboarding.ts), not duplicated per turn.
530
+ if (!isWorker) {
531
+ const routingCatalog = renderAgentRoutingCatalog(await listCatalogAgents())
532
+ systemPrompt += `\n\n# COLMENA DE AGENTES\nWorkers disponibles ahora mismo (globales del sistema, ya existen):\n\n${routingCatalog}\n`
533
+ }
534
+
535
+ const playbookInput = taskContext || userMessage
536
+ const playbookText = typeof playbookInput === "string"
537
+ ? playbookInput
538
+ : Array.isArray(playbookInput)
539
+ ? playbookInput.filter((part) => part.type === "text").map((part) => (part as any).text).join("\n")
540
+ : String(playbookInput)
541
+ const playbookRules = (await selectPlaybookRules(playbookText)).filter((rule) => {
542
+ if (!rule.applicable_to || !rule.applicable_to.includes("agent:")) return true
543
+ return isCatalogAgent ? rule.applicable_to.includes(`agent:${agent.id}`) : false
544
+ })
545
+ if (playbookRules.length > 0) {
546
+ systemPrompt += `\n\n# PLAYBOOK APRENDIDO\n${playbookRules.map((rule) => `- ${rule.rule}`).join("\n")}\n`
547
+ }
548
+
549
+ // Inject scratchpad (Strategy: WRITE) — usando TOON para ahorro de tokens
550
+ if (scratchpadNotes.length > 0) {
551
+ const scratchpadData: Record<string, string> = {}
552
+ for (const n of scratchpadNotes) {
553
+ scratchpadData[n.key] = n.value
554
+ }
555
+ // TOON comprime el formato clave-valor
556
+ const scratchpadContent = formatContext(scratchpadData)
557
+ systemPrompt += `\n\n# SCRATCHPAD (Persistent Notes)\n${scratchpadContent}\n`
558
+ }
559
+
560
+ // G9: causal context window (buildAgentContext) — only when the summary
561
+ // applies this turn (a real DB round-trip, not a per-turn cost) and
562
+ // there's a causal stream to build it from. episodicSimilarity is omitted:
563
+ // it requires embeddings hive doesn't generate anywhere yet.
564
+ if (summaryApplies && opts.causalStreamId && loadConfig().causalLog?.enabled) {
565
+ try {
566
+ const causalDb = await getHiveDb()
567
+ const objectiveSource = taskContext || userMessage
568
+ const currentObjective = typeof objectiveSource === "string"
569
+ ? objectiveSource
570
+ : Array.isArray(objectiveSource)
571
+ ? objectiveSource.filter((p) => p.type === "text").map((p) => (p as any).text).join("\n")
572
+ : String(objectiveSource)
573
+ const causalMaxTokens = Math.max(500, Math.min(4000, Math.floor(modelContextWindow * 0.05)))
574
+
575
+ const causalCtx = (await causalDb.buildAgentContext({
576
+ taskId: opts.causalStreamId,
577
+ currentPhase: "current",
578
+ currentObjective: currentObjective.slice(0, 2000),
579
+ maxTokens: causalMaxTokens,
580
+ strategy: { causalAnchors: true, compressCompletedPhases: true },
581
+ })) as AgentContextShape
582
+
583
+ const causalLines = [...(causalCtx.items ?? []), ...(causalCtx.anomalies ?? [])]
584
+ .map(formatCausalContextItem)
585
+ .filter((line): line is string => !!line)
586
+
587
+ if (causalLines.length > 0) {
588
+ systemPrompt += `\n\n# CAUSAL CONTEXT (decisiones y tool calls de este turno, previos a la compactación — priorizá la conversación actual; usalo solo para no repetir algo que ya funcionó o ya falló)\n${causalLines.join("\n")}\n`
589
+ log.info(`[context-compiler] [STEP-9d] ✅ Injected ${causalLines.length} causal context item(s)`)
590
+ }
591
+ } catch (err) {
592
+ log.warn(`[context-compiler] [STEP-9d] ⚠️ Causal context build failed: ${(err as Error).message}`)
593
+ }
594
+ }
595
+
596
+ // Coordinator only. Just the live loadout — how to use it is doctrine and
597
+ // lives in the stored prompt + the capability_discovery skill.
598
+ if (!isWorker) {
599
+ const minimalToolsDocs = filteredNativeTools
600
+ .filter(t => MINIMAL_TOOLS.has(t.name))
601
+ .map(t => `- **${t.name}**: ${t.description || "Herramienta nativa"}`)
602
+ .join("\n")
603
+
604
+ systemPrompt += `\n\n# HERRAMIENTAS SIEMPRE DISPONIBLES\n${minimalToolsDocs}\n`
605
+
606
+
607
+ // Inject available skills (minimal + discovered)
608
+ if (allSkills.length > 0) {
609
+ // Minimal skills: inject full body (always-loaded instructions)
610
+ const minimalNames = new Set(minimalSkills.map(s => s.name))
611
+ const minimalWithBody = allSkills.filter(s => minimalNames.has(s.name) && s.body)
612
+ if (minimalWithBody.length > 0) {
613
+ let minimalSection = `\n\n# SKILLS SIEMPRE ACTIVAS\n`
614
+ for (const skill of minimalWithBody) {
615
+ minimalSection += `\n## ${skill.name}\n${skill.body}\n`
616
+ }
617
+ systemPrompt += minimalSection
618
+ }
619
+
620
+ // Discovered skills: list only (body arrives via agent-loop when tools are injected)
621
+ const discoveredOnly = allSkills.filter(s => !minimalNames.has(s.name))
622
+ if (discoveredOnly.length > 0) {
623
+ let discoveredSection = `\n\n# SKILLS DESCUBIERTAS (relevantes para esta tarea)\n`
624
+ for (const skill of discoveredOnly) {
625
+ const desc = skill.description ? ` — ${skill.description}` : ""
626
+ discoveredSection += `- **${skill.name}**${desc}\n`
627
+ }
628
+ systemPrompt += discoveredSection
629
+ }
630
+
631
+ log.info(`[context-compiler] [STEP-10d] Injected ${minimalWithBody.length} minimal skill bodies + ${discoveredOnly.length} discovered skills`)
632
+ }
633
+
634
+ }
635
+
636
+ // For isolated workers, add task context + tool discovery instruction
637
+ if (isWorker && opts.taskContext && !isCatalogAgent) {
638
+ systemPrompt += `\n\n# HERRAMIENTAS DISPONIBLES\n` +
639
+ `Arrancas con herramientas básicas. Si tu tarea requiere herramientas adicionales (web_search, fs_read, browser_navigate, etc.):\n` +
640
+ `1. Usá \`search_knowledge(type="tools", query="<herramienta o tarea>")\` para encontrarlas.\n` +
641
+ `2. Las herramientas que encuentres estarán disponibles para usar inmediatamente.\n` +
642
+ `Si el coordinador te indicó herramientas específicas, buscalas primero con search_knowledge antes de ejecutar tu tarea.\n` +
643
+ `\n# CURRENT TASK\n${opts.taskContext}\n\nFocus ONLY on this task. Do not deviate.`
644
+ } else if (isWorker && opts.taskContext) {
645
+ systemPrompt += `\n\n# CURRENT TASK\n${opts.taskContext}\n\nFocus ONLY on this task and return the required structured delivery.`
646
+ }
647
+
648
+ // Truncate system prompt only when it exceeds a model-aware budget.
649
+ const maxSystemPromptChars = Math.min(
650
+ MAX_SYSTEM_PROMPT_CHARS_CAP,
651
+ Math.max(8000, Math.floor(modelContextWindow * COMPACT_RATIO * 4))
652
+ )
653
+ if (systemPrompt.length > maxSystemPromptChars) {
654
+ const originalLen = systemPrompt.length
655
+ systemPrompt = systemPrompt.substring(0, maxSystemPromptChars) +
656
+ `\n\n[... System prompt truncated (${originalLen} chars → ${maxSystemPromptChars} chars) ...]`
657
+ log.info(`[context-compiler] System prompt truncated: ${originalLen} → ${maxSystemPromptChars} chars`)
658
+ }
659
+
660
+ const estimatedSystemTokens = estimateTokens(systemPrompt)
661
+ const estimatedMsgTokens = messages.reduce((sum, m) => sum + estimateTokens(typeof m.content === 'string' ? m.content : JSON.stringify(m.content)), 0)
662
+ const estimatedToolTokens = toolsForLLM.reduce((sum, t) => sum + estimateTokens(JSON.stringify(t)), 0)
663
+ const estimatedTotal = estimatedSystemTokens + estimatedMsgTokens + estimatedToolTokens
664
+ const budgetPct = modelContextWindow > 0 ? Math.round((estimatedTotal / modelContextWindow) * 100) : 0
665
+
666
+ log.info(
667
+ `[context-compiler] ✅ DONE: ${allTools.length} total tools, ` +
668
+ `${toolsForLLM.length} selected tools, ${messages.length} messages, ` +
669
+ `${allSkills.length} skills, isolated=${isWorker}, ` +
670
+ `est.tokens: sys=${estimatedSystemTokens} msgs=${estimatedMsgTokens} tools=${estimatedToolTokens} ` +
671
+ `total=${estimatedTotal}/${modelContextWindow} (${budgetPct}%)`
672
+ )
673
+
674
+ return {
675
+ systemPrompt,
676
+ conversationSummarySection,
677
+ messages,
678
+ tools: toolsForLLM,
679
+ allTools,
680
+ skills: allSkills,
681
+ }
682
+ }
683
+
684
+ // Re-export sync functions for gateway/initializer
685
+ export {
686
+ syncToolCatalogToIndex as syncToolsToIndex,
687
+ syncSkillsToIndex,
688
+ syncPlaybookToIndex,
689
+ }