@johpaz/hive 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/CONTRIBUTING.md +44 -0
  2. package/README.md +310 -0
  3. package/package.json +96 -0
  4. package/packages/cli/package.json +28 -0
  5. package/packages/cli/src/commands/agent-run.ts +168 -0
  6. package/packages/cli/src/commands/agents.ts +398 -0
  7. package/packages/cli/src/commands/chat.ts +142 -0
  8. package/packages/cli/src/commands/config.ts +50 -0
  9. package/packages/cli/src/commands/cron.ts +161 -0
  10. package/packages/cli/src/commands/dev.ts +95 -0
  11. package/packages/cli/src/commands/doctor.ts +133 -0
  12. package/packages/cli/src/commands/gateway.ts +443 -0
  13. package/packages/cli/src/commands/logs.ts +57 -0
  14. package/packages/cli/src/commands/mcp.ts +175 -0
  15. package/packages/cli/src/commands/message.ts +77 -0
  16. package/packages/cli/src/commands/onboard.ts +1868 -0
  17. package/packages/cli/src/commands/security.ts +144 -0
  18. package/packages/cli/src/commands/service.ts +50 -0
  19. package/packages/cli/src/commands/sessions.ts +116 -0
  20. package/packages/cli/src/commands/skills.ts +187 -0
  21. package/packages/cli/src/commands/update.ts +25 -0
  22. package/packages/cli/src/index.ts +185 -0
  23. package/packages/cli/src/utils/token.ts +6 -0
  24. package/packages/code-bridge/README.md +78 -0
  25. package/packages/code-bridge/package.json +18 -0
  26. package/packages/code-bridge/src/index.ts +95 -0
  27. package/packages/code-bridge/src/process-manager.ts +212 -0
  28. package/packages/code-bridge/src/schemas.ts +133 -0
  29. package/packages/core/package.json +46 -0
  30. package/packages/core/src/agent/agent-loop.ts +369 -0
  31. package/packages/core/src/agent/compaction.ts +140 -0
  32. package/packages/core/src/agent/context-compiler.ts +378 -0
  33. package/packages/core/src/agent/context-guard.ts +91 -0
  34. package/packages/core/src/agent/context.ts +138 -0
  35. package/packages/core/src/agent/conversation-store.ts +198 -0
  36. package/packages/core/src/agent/curator.ts +158 -0
  37. package/packages/core/src/agent/hooks.ts +166 -0
  38. package/packages/core/src/agent/index.ts +116 -0
  39. package/packages/core/src/agent/llm-client.ts +503 -0
  40. package/packages/core/src/agent/native-tools.ts +505 -0
  41. package/packages/core/src/agent/prompt-builder.ts +532 -0
  42. package/packages/core/src/agent/providers/index.ts +167 -0
  43. package/packages/core/src/agent/providers.ts +1 -0
  44. package/packages/core/src/agent/reflector.ts +170 -0
  45. package/packages/core/src/agent/service.ts +64 -0
  46. package/packages/core/src/agent/stuck-loop.ts +133 -0
  47. package/packages/core/src/agent/supervisor.ts +39 -0
  48. package/packages/core/src/agent/tracer.ts +102 -0
  49. package/packages/core/src/agent/workspace.ts +110 -0
  50. package/packages/core/src/canvas/canvas-manager.test.ts +161 -0
  51. package/packages/core/src/canvas/canvas-manager.ts +319 -0
  52. package/packages/core/src/canvas/canvas-tools.ts +420 -0
  53. package/packages/core/src/canvas/emitter.ts +115 -0
  54. package/packages/core/src/canvas/index.ts +2 -0
  55. package/packages/core/src/channels/base.ts +138 -0
  56. package/packages/core/src/channels/discord.ts +260 -0
  57. package/packages/core/src/channels/index.ts +7 -0
  58. package/packages/core/src/channels/manager.ts +383 -0
  59. package/packages/core/src/channels/slack.ts +287 -0
  60. package/packages/core/src/channels/telegram.ts +502 -0
  61. package/packages/core/src/channels/webchat.ts +128 -0
  62. package/packages/core/src/channels/whatsapp.ts +375 -0
  63. package/packages/core/src/config/index.ts +12 -0
  64. package/packages/core/src/config/loader.ts +529 -0
  65. package/packages/core/src/events/event-bus.ts +169 -0
  66. package/packages/core/src/gateway/index.ts +5 -0
  67. package/packages/core/src/gateway/initializer.ts +290 -0
  68. package/packages/core/src/gateway/lane-queue.ts +169 -0
  69. package/packages/core/src/gateway/resolver.ts +108 -0
  70. package/packages/core/src/gateway/router.ts +124 -0
  71. package/packages/core/src/gateway/server.ts +3317 -0
  72. package/packages/core/src/gateway/session.ts +95 -0
  73. package/packages/core/src/gateway/slash-commands.ts +192 -0
  74. package/packages/core/src/heartbeat/index.ts +157 -0
  75. package/packages/core/src/index.ts +19 -0
  76. package/packages/core/src/integrations/catalog.ts +286 -0
  77. package/packages/core/src/integrations/env.ts +64 -0
  78. package/packages/core/src/integrations/index.ts +2 -0
  79. package/packages/core/src/memory/index.ts +1 -0
  80. package/packages/core/src/memory/notes.ts +68 -0
  81. package/packages/core/src/plugins/api.ts +128 -0
  82. package/packages/core/src/plugins/index.ts +2 -0
  83. package/packages/core/src/plugins/loader.ts +365 -0
  84. package/packages/core/src/resilience/circuit-breaker.ts +225 -0
  85. package/packages/core/src/security/google-chat.ts +269 -0
  86. package/packages/core/src/security/index.ts +192 -0
  87. package/packages/core/src/security/pairing.ts +250 -0
  88. package/packages/core/src/security/rate-limit.ts +270 -0
  89. package/packages/core/src/security/signal.ts +321 -0
  90. package/packages/core/src/state/store.ts +312 -0
  91. package/packages/core/src/storage/bun-sqlite-store.ts +188 -0
  92. package/packages/core/src/storage/crypto.ts +101 -0
  93. package/packages/core/src/storage/db-context.ts +333 -0
  94. package/packages/core/src/storage/onboarding.ts +1087 -0
  95. package/packages/core/src/storage/schema.ts +541 -0
  96. package/packages/core/src/storage/seed.ts +571 -0
  97. package/packages/core/src/storage/sqlite.ts +387 -0
  98. package/packages/core/src/storage/usage.ts +212 -0
  99. package/packages/core/src/tools/bridge-events.ts +74 -0
  100. package/packages/core/src/tools/browser.ts +275 -0
  101. package/packages/core/src/tools/codebridge.ts +421 -0
  102. package/packages/core/src/tools/coordinator-tools.ts +179 -0
  103. package/packages/core/src/tools/cron.ts +611 -0
  104. package/packages/core/src/tools/exec.ts +140 -0
  105. package/packages/core/src/tools/fs.ts +364 -0
  106. package/packages/core/src/tools/index.ts +12 -0
  107. package/packages/core/src/tools/memory.ts +176 -0
  108. package/packages/core/src/tools/notify.ts +113 -0
  109. package/packages/core/src/tools/project-management.ts +376 -0
  110. package/packages/core/src/tools/project.ts +375 -0
  111. package/packages/core/src/tools/read.ts +158 -0
  112. package/packages/core/src/tools/web.ts +436 -0
  113. package/packages/core/src/tools/workspace.ts +171 -0
  114. package/packages/core/src/utils/benchmark.ts +80 -0
  115. package/packages/core/src/utils/crypto.ts +73 -0
  116. package/packages/core/src/utils/date.ts +42 -0
  117. package/packages/core/src/utils/index.ts +4 -0
  118. package/packages/core/src/utils/logger.ts +388 -0
  119. package/packages/core/src/utils/retry.ts +70 -0
  120. package/packages/core/src/voice/index.ts +583 -0
  121. package/packages/core/tsconfig.json +9 -0
  122. package/packages/mcp/package.json +26 -0
  123. package/packages/mcp/src/config.ts +13 -0
  124. package/packages/mcp/src/index.ts +1 -0
  125. package/packages/mcp/src/logger.ts +42 -0
  126. package/packages/mcp/src/manager.ts +434 -0
  127. package/packages/mcp/src/transports/index.ts +67 -0
  128. package/packages/mcp/src/transports/sse.ts +241 -0
  129. package/packages/mcp/src/transports/websocket.ts +159 -0
  130. package/packages/skills/package.json +21 -0
  131. package/packages/skills/src/bundled/agent_management/SKILL.md +24 -0
  132. package/packages/skills/src/bundled/browser_automation/SKILL.md +30 -0
  133. package/packages/skills/src/bundled/context_compact/SKILL.md +35 -0
  134. package/packages/skills/src/bundled/cron_manager/SKILL.md +52 -0
  135. package/packages/skills/src/bundled/file_manager/SKILL.md +76 -0
  136. package/packages/skills/src/bundled/http_client/SKILL.md +24 -0
  137. package/packages/skills/src/bundled/memory/SKILL.md +42 -0
  138. package/packages/skills/src/bundled/project_management/SKILL.md +26 -0
  139. package/packages/skills/src/bundled/shell/SKILL.md +43 -0
  140. package/packages/skills/src/bundled/system_notify/SKILL.md +52 -0
  141. package/packages/skills/src/bundled/voice/SKILL.md +25 -0
  142. package/packages/skills/src/bundled/web_search/SKILL.md +29 -0
  143. package/packages/skills/src/index.ts +1 -0
  144. package/packages/skills/src/loader.ts +282 -0
  145. package/packages/tools/package.json +43 -0
  146. package/packages/tools/src/browser/browser.test.ts +111 -0
  147. package/packages/tools/src/browser/index.ts +272 -0
  148. package/packages/tools/src/canvas/index.ts +220 -0
  149. package/packages/tools/src/cron/cron.test.ts +164 -0
  150. package/packages/tools/src/cron/index.ts +304 -0
  151. package/packages/tools/src/filesystem/filesystem.test.ts +240 -0
  152. package/packages/tools/src/filesystem/index.ts +379 -0
  153. package/packages/tools/src/git/index.ts +239 -0
  154. package/packages/tools/src/index.ts +4 -0
  155. package/packages/tools/src/shell/detect-env.ts +70 -0
  156. package/packages/tools/tsconfig.json +9 -0
@@ -0,0 +1,378 @@
1
+ /**
2
+ * Context Compiler
3
+ *
4
+ * Compiled BEFORE every model call. Implements the 4 strategies:
5
+ * WRITE → scratchpad (persistent notes survive compression)
6
+ * SELECT → history (recent N), playbook rules (FTS5), tool loadout (3 levels)
7
+ * COMPRESS → summaries (if conversation is long)
8
+ * ISOLATE → workers receive only their task context, not full history
9
+ *
10
+ * Constitutional ethics layer: always injected first, never filtered.
11
+ *
12
+ * Output structure (used directly by agent-loop.ts):
13
+ * systemPrompt — assembled from layers 1-4
14
+ * messages — history to send to model
15
+ * tools — LLMToolDef[] after 3-level filter
16
+ */
17
+
18
+ import { getDb } from "../storage/sqlite"
19
+ import { logger } from "../utils/logger"
20
+ import type { LLMMessage, LLMToolDef } from "./llm-client"
21
+ import type { Tool } from "./native-tools"
22
+ import type { MCPClientManager } from "@johpaz/hive-mcp"
23
+ import {
24
+ getRecentMessages,
25
+ getSummary,
26
+ getScratchpad,
27
+ getMessageCount,
28
+ toAPIMessages,
29
+ type StoredMessage,
30
+ } from "./conversation-store"
31
+ import {
32
+ collectNativeTools,
33
+ collectMCPTools,
34
+ filterToolsByAgent,
35
+ selectToolLoadout,
36
+ syncNativeToolsToDB,
37
+ toToolDefs,
38
+ getToolGroup,
39
+ } from "./native-tools"
40
+ import { getUserDate, getUserTime } from "../utils/date"
41
+ import { encode } from "toon-format-parser"
42
+
43
+ const log = logger.child("context-compiler")
44
+
45
+ // ─── Configuration ─────────────────────────────────────────────────────────
46
+
47
+ const RECENT_MSGS_LIMIT = 6 // keep last N messages for context (small models need small context)
48
+ const PLAYBOOK_RULES_LIMIT = 5 // max playbook rules injected per turn
49
+ const MAX_TOOL_GROUPS = 3 // max semantic groups loaded per turn (each group = all tools in category)
50
+
51
+ // ─── Types ─────────────────────────────────────────────────────────────────
52
+
53
+ export interface CompileOptions {
54
+ agentId: string
55
+ threadId: string
56
+ userMessage: string
57
+ channel?: string
58
+ mcpManager?: MCPClientManager | null
59
+ /** If true, this is a worker receiving an isolated task — don't inject full history */
60
+ isolated?: boolean
61
+ /** For isolated workers: the task description to inject instead of full history */
62
+ taskContext?: string
63
+ }
64
+
65
+ export interface CompiledContext {
66
+ systemPrompt: string
67
+ messages: LLMMessage[]
68
+ tools: LLMToolDef[]
69
+ allTools: Tool[] // raw tool objects for execution
70
+ }
71
+
72
+ // ─── Main compiler ──────────────────────────────────────────────────────────
73
+
74
+ export async function compileContext(opts: CompileOptions): Promise<CompiledContext> {
75
+ const t0 = performance.now()
76
+ const db = getDb()
77
+
78
+ // Load agent row
79
+ const agent = db.query<any, [string]>("SELECT * FROM agents WHERE id = ?").get(opts.agentId)
80
+ if (!agent) throw new Error(`Agent not found: ${opts.agentId}`)
81
+
82
+ // Load user
83
+ const user = db.query<any, [string]>(
84
+ "SELECT * FROM users WHERE id = (SELECT user_id FROM agents WHERE id = ? LIMIT 1)"
85
+ ).get(opts.agentId) ?? db.query<any, []>("SELECT * FROM users LIMIT 1").get()
86
+
87
+ // ── Tool collection FIRST — needed to generate dynamic capabilities ────
88
+ // Level 1: full catalog (native + MCP) — must happen before system prompt assembly
89
+ const nativeTools = collectNativeTools()
90
+ const mcpTools = opts.mcpManager
91
+ ? await collectMCPTools(opts.mcpManager)
92
+ : []
93
+ const allTools = [...nativeTools, ...mcpTools]
94
+
95
+ // Sync catalog to DB so FTS5 is current (fast, ~1ms for 50 tools)
96
+ syncNativeToolsToDB(allTools)
97
+
98
+ // Level 2: filter by agent assignment (tools_json NULL = all allowed)
99
+ const agentFiltered = filterToolsByAgent(allTools, agent.tools_json)
100
+
101
+ // Level 3: Group selection — always-include + top N semantic groups expanded
102
+ const loadout = selectToolLoadout(agentFiltered, opts.userMessage, MAX_TOOL_GROUPS)
103
+
104
+ const systemParts: string[] = []
105
+
106
+ // ── Layer 1: Ethics (constitutional, always first) ──────────────────────
107
+ const ethicsText = loadEthics()
108
+ if (ethicsText) {
109
+ systemParts.push(ethicsText)
110
+ }
111
+
112
+ // ── Layer 2: Agent identity ─────────────────────────────────────────────
113
+ let agentPrompt: string
114
+ if (agent.system_prompt) {
115
+ agentPrompt = agent.description
116
+ ? `# ROLE\n${agent.description}\n\n${agent.system_prompt}`
117
+ : agent.system_prompt
118
+ } else if (agent.description) {
119
+ agentPrompt = `# ROLE\n${agent.description}\n\nYou are a helpful assistant. Use your available tools to complete the user's requests.`
120
+ } else {
121
+ agentPrompt = "You are a helpful assistant. Use your available tools to complete the user's requests."
122
+ }
123
+ systemParts.push(agentPrompt)
124
+
125
+ // ── Layer 2b: Hive Capabilities Manifest (dynamic — from actual loaded tools) ─
126
+ // Only for coordinators — workers get minimal context
127
+ if (!opts.isolated) {
128
+ const capabilities = loadHiveCapabilities(allTools)
129
+ if (capabilities) {
130
+ systemParts.push(capabilities)
131
+ }
132
+ }
133
+
134
+ // ── Layer 2c: User profile ──────────────────────────────────────────────
135
+ if (user) {
136
+ const userInfo: Record<string, string> = {}
137
+ if (user.name) userInfo.name = user.name
138
+ if (user.occupation) userInfo.occupation = user.occupation
139
+ if (user.language) userInfo.language = user.language
140
+ if (user.timezone) userInfo.timezone = user.timezone
141
+ if (user.notes) userInfo.notes = user.notes
142
+ if (Object.keys(userInfo).length > 0) {
143
+ systemParts.push(encode({ user: userInfo }))
144
+ }
145
+ }
146
+
147
+ // ── Layer 3: Playbook rules (ACE, FTS5) ─────────────────────────────────
148
+ const playbookRules = loadPlaybookRules(opts.userMessage)
149
+ if (playbookRules.length > 0) {
150
+ systemParts.push(encode({ playbook: playbookRules }))
151
+ }
152
+
153
+ // ── Layer 4: Scratchpad (persistent notes) ──────────────────────────────
154
+ if (!opts.isolated) {
155
+ const notes = getScratchpad(opts.threadId)
156
+ if (notes.length > 0) {
157
+ const notesMap: Record<string, string> = {}
158
+ for (const n of notes) notesMap[n.key] = n.value
159
+ systemParts.push(encode({ notes: notesMap }))
160
+ }
161
+ }
162
+
163
+ // ── Layer 5: Isolated task context (for workers) ─────────────────────────
164
+ if (opts.isolated && opts.taskContext) {
165
+ systemParts.push(`# YOUR TASK\n${opts.taskContext}`)
166
+ }
167
+
168
+ // ── Layer 6: Environment / temporal info ────────────────────────────────
169
+ const now = new Date()
170
+ const tz = user?.timezone || "UTC"
171
+ let localTime = "N/A"
172
+ try {
173
+ localTime = now.toLocaleString("en-US", { timeZone: tz, dateStyle: "full", timeStyle: "long" })
174
+ } catch { /* bad TZ */ }
175
+
176
+ systemParts.push(encode({
177
+ env: {
178
+ agent_id: agent.id,
179
+ thread_id: opts.threadId,
180
+ utc: now.toISOString(),
181
+ local: `${localTime} (${tz})`,
182
+ date: getUserDate(tz, now),
183
+ time: getUserTime(tz, now),
184
+ }
185
+ }))
186
+
187
+ // ── Tool loadout summary (TOON, grouped) — current turn selection ────────
188
+ if (loadout.length > 0) {
189
+ const grouped: Record<string, string[]> = {}
190
+ for (const t of loadout) {
191
+ const cat = t.name.includes("__")
192
+ ? `mcp:${t.name.split("__")[0]}`
193
+ : getToolGroup(t.name)
194
+ if (!grouped[cat]) grouped[cat] = []
195
+ grouped[cat].push(t.name)
196
+ }
197
+ systemParts.push(encode({ tools: grouped }))
198
+ }
199
+
200
+ // ── History selection ───────────────────────────────────────────────────
201
+ let messages: LLMMessage[]
202
+
203
+ if (opts.isolated) {
204
+ // Workers only get the task description — no full conversation history
205
+ messages = []
206
+ } else {
207
+ const totalMessages = getMessageCount(opts.threadId)
208
+ const summary = getSummary(opts.threadId)
209
+
210
+ if (summary && totalMessages > RECENT_MSGS_LIMIT) {
211
+ // Inject summary as a system message, then recent messages
212
+ const recent = getRecentMessages(opts.threadId, RECENT_MSGS_LIMIT)
213
+ messages = [
214
+ { role: "system", content: `[Previous conversation summary]\n${summary.summary}` },
215
+ ...toAPIMessages(recent),
216
+ ]
217
+ } else {
218
+ // Short conversation — pass all messages
219
+ const recent = getRecentMessages(opts.threadId, RECENT_MSGS_LIMIT)
220
+ messages = toAPIMessages(recent)
221
+ }
222
+ }
223
+
224
+ log.info(
225
+ `[context-compiler] agent=${agent.name} tools=${loadout.length}/${allTools.length} ` +
226
+ `msgs=${messages.length} playbook=${playbookRules.length} ` +
227
+ `elapsed=${(performance.now() - t0).toFixed(1)}ms`
228
+ )
229
+
230
+ return {
231
+ systemPrompt: systemParts.join("\n\n"),
232
+ messages,
233
+ tools: toToolDefs(loadout),
234
+ allTools: loadout,
235
+ }
236
+ }
237
+
238
+ // ─── Ethics loader ──────────────────────────────────────────────────────────
239
+
240
+ function loadEthics(): string {
241
+ try {
242
+ const db = getDb()
243
+ const rows = db.query<any, []>(
244
+ "SELECT content FROM ethics WHERE active = 1 ORDER BY id ASC"
245
+ ).all()
246
+ if (rows.length === 0) return ""
247
+ return encode({ ethics: rows.map((r: any) => r.content) })
248
+ } catch {
249
+ return ""
250
+ }
251
+ }
252
+
253
+ // ─── Playbook loader (FTS5) ─────────────────────────────────────────────────
254
+
255
+ function loadPlaybookRules(userMessage: string): string[] {
256
+ try {
257
+ const db = getDb()
258
+
259
+ // Try FTS5 first
260
+ const ftsRows = db.query<any, [string, number]>(`
261
+ SELECT p.rule FROM playbook p
262
+ JOIN playbook_fts f ON p.id = f.rowid
263
+ WHERE playbook_fts MATCH ?
264
+ AND p.active = 1
265
+ AND p.helpful_count >= p.harmful_count
266
+ ORDER BY rank
267
+ LIMIT ?
268
+ `).all(sanitizeFTSQuery(userMessage), PLAYBOOK_RULES_LIMIT)
269
+
270
+ if (ftsRows.length > 0) {
271
+ return ftsRows.map((r: any) => r.rule)
272
+ }
273
+
274
+ // Fallback: most-used active rules
275
+ const fallback = db.query<any, [number]>(`
276
+ SELECT rule FROM playbook
277
+ WHERE active = 1 AND helpful_count >= harmful_count
278
+ ORDER BY helpful_count DESC
279
+ LIMIT ?
280
+ `).all(PLAYBOOK_RULES_LIMIT)
281
+
282
+ return fallback.map((r: any) => r.rule)
283
+ } catch {
284
+ return []
285
+ }
286
+ }
287
+
288
+ function sanitizeFTSQuery(text: string): string {
289
+ // FTS5 query: take first N unique words, escape special chars
290
+ const words = text
291
+ .replace(/['"*]/g, " ")
292
+ .split(/\s+/)
293
+ .filter((w) => w.length > 2)
294
+ .slice(0, 8)
295
+ return words.join(" OR ")
296
+ }
297
+
298
+ // ─── Hive Capabilities loader ───────────────────────────────────────────────
299
+
300
+ /**
301
+ * Generates the Hive Capabilities manifest dynamically from:
302
+ * - Static sections in `hive_capabilities` DB (architecture, agents, canvas, etc.)
303
+ * - Native tools grouped by category (from allTools — always current)
304
+ * - MCP tools grouped by server (from allTools — always current)
305
+ *
306
+ * The `tools_native` and `mcp` DB sections are intentionally skipped;
307
+ * those are replaced by the dynamic blocks built from allTools.
308
+ */
309
+ function loadHiveCapabilities(allTools: Tool[]): string {
310
+ try {
311
+ const db = getDb()
312
+
313
+ // Load static architecture/context sections — skip tools & mcp (generated dynamically)
314
+ const rows = db.query<any, []>(
315
+ "SELECT section, title, content FROM hive_capabilities WHERE active = 1 ORDER BY sort_order ASC"
316
+ ).all()
317
+
318
+ const DYNAMIC_SECTIONS = new Set(["tools", "mcp"])
319
+ const sections: string[] = rows
320
+ .filter((r: any) => !DYNAMIC_SECTIONS.has(r.section))
321
+ .map((r: any) => `## ${r.title}\n${r.content}`)
322
+
323
+ // ── Dynamic: native tools grouped by category ────────────────────────
324
+ const nativeByGroup: Record<string, { name: string; description: string }[]> = {}
325
+ const mcpByServer: Record<string, string[]> = {}
326
+
327
+ for (const t of allTools) {
328
+ if (t.name.includes("__")) {
329
+ // MCP tool: "serverName__toolName"
330
+ const server = t.name.split("__")[0]
331
+ if (!mcpByServer[server]) mcpByServer[server] = []
332
+ mcpByServer[server].push(t.name.split("__")[1])
333
+ } else {
334
+ const cat = getToolGroup(t.name)
335
+ if (!nativeByGroup[cat]) nativeByGroup[cat] = []
336
+ nativeByGroup[cat].push({ name: t.name, description: t.description })
337
+ }
338
+ }
339
+
340
+ if (Object.keys(nativeByGroup).length > 0) {
341
+ const lines = ["## Herramientas Nativas Disponibles"]
342
+ for (const [cat, tools] of Object.entries(nativeByGroup)) {
343
+ lines.push(`\n**${cat.toUpperCase()}:**`)
344
+ for (const t of tools) {
345
+ lines.push(`- ${t.name}: ${t.description.split(".")[0].substring(0, 80)}`)
346
+ }
347
+ }
348
+ sections.push(lines.join("\n"))
349
+ }
350
+
351
+ // ── Dynamic: MCP servers — from DB (always) + loaded tools (if connected) ──
352
+ // Query DB so the agent always knows which servers exist, even if disconnected
353
+ try {
354
+ const dbServers = db.query<any, []>(
355
+ "SELECT name, transport, url, status, tools_count FROM mcp_servers WHERE enabled = 1"
356
+ ).all()
357
+
358
+ if (dbServers.length > 0) {
359
+ const lines = ["## Servidores MCP Configurados"]
360
+ for (const srv of dbServers) {
361
+ const loadedTools = mcpByServer[srv.name]
362
+ if (loadedTools && loadedTools.length > 0) {
363
+ lines.push(`\n**${srv.name}** [${srv.transport}] ✓ conectado — tools: ${loadedTools.join(", ")}`)
364
+ } else {
365
+ const endpoint = srv.url ? ` (${srv.url})` : ""
366
+ lines.push(`\n**${srv.name}** [${srv.transport}]${endpoint} — ${srv.status} (${srv.tools_count ?? 0} tools registradas)`)
367
+ }
368
+ }
369
+ sections.push(lines.join("\n"))
370
+ }
371
+ } catch { /* no mcp_servers table yet */ }
372
+
373
+ if (sections.length === 0) return ""
374
+ return `# HIVE — CAPACIDADES DEL SISTEMA\n${sections.join("\n\n")}`
375
+ } catch {
376
+ return ""
377
+ }
378
+ }
@@ -0,0 +1,91 @@
1
+ import type { Config } from "../config/loader.ts";
2
+ import { logger } from "../utils/logger.ts";
3
+ import type { Message } from "../agent/context.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.toolCalls) {
32
+ for (const tc of msg.toolCalls) {
33
+ total += Math.ceil(tc.name.length / 4);
34
+ total += Math.ceil(JSON.stringify(tc.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
+ }
@@ -0,0 +1,138 @@
1
+ export interface SoulConfig {
2
+ identity: string;
3
+ personality: string;
4
+ boundaries: string;
5
+ instructions: string;
6
+ capabilities: string[];
7
+ raw: string;
8
+ }
9
+
10
+ export interface UserConfig {
11
+ id: string;
12
+ name: string;
13
+ language: string;
14
+ timezone: string;
15
+ activeProjects: string[];
16
+ preferences: Record<string, unknown> | string;
17
+ raw: string;
18
+ }
19
+
20
+ export interface EthicsConfig {
21
+ raw: string;
22
+ loadedAt: Date;
23
+ path: string;
24
+ }
25
+
26
+ export interface Message {
27
+ role: "system" | "user" | "assistant" | "tool";
28
+ content: string;
29
+ name?: string;
30
+ toolCallId?: string;
31
+ toolCalls?: ToolCall[];
32
+ }
33
+
34
+ export interface ToolCall {
35
+ id: string;
36
+ name: string;
37
+ arguments: Record<string, unknown>;
38
+ }
39
+
40
+ export interface ToolInfo {
41
+ id: string;
42
+ name: string;
43
+ description: string | null;
44
+ category: string | null;
45
+ }
46
+
47
+ export interface SkillInfo {
48
+ id: string;
49
+ name: string;
50
+ description: string | null;
51
+ source: string;
52
+ }
53
+
54
+ export interface McpServerInfo {
55
+ id: string;
56
+ name: string;
57
+ transport: string;
58
+ enabled: boolean;
59
+ active: boolean;
60
+ }
61
+
62
+ export interface ChannelInfo {
63
+ id: string;
64
+ type: string;
65
+ accountId: string;
66
+ status: string;
67
+ }
68
+
69
+ export interface CodeBridgeInfo {
70
+ id: string;
71
+ name: string;
72
+ cliCommand: string;
73
+ port: number;
74
+ active: boolean;
75
+ }
76
+
77
+ export interface ProviderInfo {
78
+ id: string;
79
+ name: string;
80
+ baseUrl: string | null;
81
+ hasApiKey: boolean;
82
+ }
83
+
84
+ export interface ModelInfo {
85
+ id: string;
86
+ name: string;
87
+ providerId: string;
88
+ contextWindow: number | null;
89
+ }
90
+
91
+ export interface ContextOptions {
92
+ ethics: EthicsConfig | null;
93
+ soul: SoulConfig;
94
+ user: UserConfig | null;
95
+ skills: string[];
96
+ memory: string[];
97
+ channel?: string;
98
+ maxTokens: number;
99
+ // New structured data from DB
100
+ tools?: ToolInfo[];
101
+ mcpServers?: McpServerInfo[];
102
+ channels?: ChannelInfo[];
103
+ codeBridge?: CodeBridgeInfo[];
104
+ providers?: ProviderInfo[];
105
+ models?: ModelInfo[];
106
+ }
107
+
108
+
109
+
110
+ export function estimateTokens(text: string): number {
111
+ return Math.ceil(text.length / 4);
112
+ }
113
+
114
+ export function truncateToTokenLimit(
115
+ messages: Message[],
116
+ maxTokens: number,
117
+ keepLastN = 4
118
+ ): { messages: Message[]; truncated: number } {
119
+ let totalTokens = 0;
120
+ let truncated = 0;
121
+
122
+ const result: Message[] = [];
123
+ const toKeep = messages.slice(-keepLastN);
124
+
125
+ for (const msg of messages.slice(0, -keepLastN)) {
126
+ const tokens = estimateTokens(msg.content);
127
+ if (totalTokens + tokens < maxTokens * 0.7) {
128
+ result.push(msg);
129
+ totalTokens += tokens;
130
+ } else {
131
+ truncated++;
132
+ }
133
+ }
134
+
135
+ result.push(...toKeep);
136
+
137
+ return { messages: result, truncated };
138
+ }