@johpaz/hive-sdk 0.1.5 → 0.2.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 (182) hide show
  1. package/CHANGELOG.md +167 -0
  2. package/README.md +11 -1
  3. package/package.json +26 -10
  4. package/packages/core/src/agent/acceptance-checks.ts +16 -10
  5. package/packages/core/src/agent/agent-catalog.ts +3 -3
  6. package/packages/core/src/agent/agent-loop.ts +115 -26
  7. package/packages/core/src/agent/capability-search.ts +2 -2
  8. package/packages/core/src/agent/catalog-selector.ts +4 -4
  9. package/packages/core/src/agent/compaction.ts +10 -9
  10. package/packages/core/src/agent/context-compiler.ts +58 -34
  11. package/packages/core/src/agent/conversation-store.ts +31 -7
  12. package/packages/core/src/agent/curator.ts +4 -4
  13. package/packages/core/src/agent/delegation-runtime.ts +5 -5
  14. package/packages/core/src/agent/goal-runner.ts +9 -9
  15. package/packages/core/src/agent/index.ts +1 -0
  16. package/packages/core/src/agent/llm-client.ts +98 -37
  17. package/packages/core/src/agent/llm-providers/anthropic.ts +4 -4
  18. package/packages/core/src/agent/llm-providers/deepseek.ts +1 -1
  19. package/packages/core/src/agent/llm-providers/gemini.ts +4 -4
  20. package/packages/core/src/agent/llm-providers/groq.ts +1 -1
  21. package/packages/core/src/agent/llm-providers/hiveagents.ts +3 -3
  22. package/packages/core/src/agent/llm-providers/interface.ts +2 -2
  23. package/packages/core/src/agent/llm-providers/kimi.ts +1 -1
  24. package/packages/core/src/agent/llm-providers/minimax.ts +1 -1
  25. package/packages/core/src/agent/llm-providers/mistral.ts +1 -1
  26. package/packages/core/src/agent/llm-providers/modelscope.ts +1 -1
  27. package/packages/core/src/agent/llm-providers/nvidia.ts +1 -1
  28. package/packages/core/src/agent/llm-providers/ollama.ts +4 -4
  29. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +9 -5
  30. package/packages/core/src/agent/llm-providers/openai.ts +1 -1
  31. package/packages/core/src/agent/llm-providers/opencode-go.ts +1 -1
  32. package/packages/core/src/agent/llm-providers/openrouter.ts +1 -1
  33. package/packages/core/src/agent/llm-providers/qwen.ts +1 -1
  34. package/packages/core/src/agent/llm-providers/z-ai.ts +1 -1
  35. package/packages/core/src/agent/mcp-result-normalizer.ts +192 -0
  36. package/packages/core/src/agent/playbook-selector.ts +4 -4
  37. package/packages/core/src/agent/prompt-builder.ts +5 -5
  38. package/packages/core/src/agent/proof-packet.ts +5 -5
  39. package/packages/core/src/agent/providers/index.ts +4 -4
  40. package/packages/core/src/agent/realtime-providers/gemini-live.ts +238 -0
  41. package/packages/core/src/agent/realtime-providers/index.ts +29 -0
  42. package/packages/core/src/agent/realtime-providers/interface.ts +108 -0
  43. package/packages/core/src/agent/reflector.ts +6 -6
  44. package/packages/core/src/agent/run-store.ts +8 -8
  45. package/packages/core/src/agent/service.ts +10 -10
  46. package/packages/core/src/agent/skill-selector.ts +6 -6
  47. package/packages/core/src/agent/thread-id.ts +71 -0
  48. package/packages/core/src/agent/thread-store.ts +250 -0
  49. package/packages/core/src/agent/tool-selector.ts +7 -5
  50. package/packages/core/src/agent/tracer.ts +5 -5
  51. package/packages/core/src/api/createAgent.ts +1 -1
  52. package/packages/core/src/artifacts/store.ts +84 -3
  53. package/packages/core/src/canvas/emitter.ts +2 -2
  54. package/packages/core/src/channels/telegram.ts +1 -1
  55. package/packages/core/src/channels/webchat.ts +1 -1
  56. package/packages/core/src/config/loader.ts +15 -1
  57. package/packages/core/src/events/agent-bus.ts +3 -3
  58. package/packages/core/src/events/channel-narration.ts +3 -3
  59. package/packages/core/src/events/event-bus.ts +1 -1
  60. package/packages/core/src/events/narration.ts +3 -3
  61. package/packages/core/src/gateway/delegation-groups.ts +4 -4
  62. package/packages/core/src/gateway/durable-queue.ts +5 -5
  63. package/packages/core/src/gateway/job-store.ts +5 -5
  64. package/packages/core/src/gateway/notification-inbox.ts +2 -2
  65. package/packages/core/src/gateway/server.ts +2 -2
  66. package/packages/core/src/mcp/MCPClient.ts +3 -3
  67. package/packages/core/src/mcp/hot-reload.ts +5 -5
  68. package/packages/core/src/mcp/tool-sync.ts +5 -5
  69. package/packages/core/src/mcp/transports/index.ts +2 -2
  70. package/packages/core/src/mcp/transports/sse.ts +1 -1
  71. package/packages/core/src/models/index.ts +36 -0
  72. package/packages/core/src/multimodal/index.ts +2 -2
  73. package/packages/core/src/multimodal/vision-service.ts +6 -6
  74. package/packages/core/src/plugins/loader.ts +4 -1
  75. package/packages/core/src/resilience/circuit-breaker.ts +16 -5
  76. package/packages/core/src/resilience/retry.ts +1 -1
  77. package/packages/core/src/scheduler/CronScheduler.ts +6 -6
  78. package/packages/core/src/scheduler/integration.ts +9 -9
  79. package/packages/core/src/sessions/index.ts +266 -0
  80. package/packages/core/src/storage/bootstrap.ts +34 -8
  81. package/packages/core/src/storage/causal-events.ts +1 -1
  82. package/packages/core/src/storage/collections.ts +32 -1
  83. package/packages/core/src/storage/crypto.ts +16 -2
  84. package/packages/core/src/storage/hive.ts +1 -1
  85. package/packages/core/src/storage/hivedb.ts +10 -1
  86. package/packages/core/src/storage/onboarding.ts +6 -6
  87. package/packages/core/src/storage/reconcile.ts +5 -5
  88. package/packages/core/src/storage/seed.ts +103 -13
  89. package/packages/core/src/storage/usage.ts +3 -3
  90. package/packages/core/src/swarm/AgentExecutor.ts +2 -2
  91. package/packages/core/src/swarm/Coordinator.ts +8 -8
  92. package/packages/core/src/swarm/EventBridge.ts +2 -2
  93. package/packages/core/src/swarm/RoleSwarm.ts +234 -0
  94. package/packages/core/src/swarm/TaskGraph.ts +2 -2
  95. package/packages/core/src/swarm/index.ts +7 -0
  96. package/packages/core/src/swarm/presets/HiveLearnPreset.ts +2 -2
  97. package/packages/core/src/swarm/presets/ResearchPreset.ts +2 -2
  98. package/packages/core/src/swarm/strategies/ParallelStrategy.ts +1 -1
  99. package/packages/core/src/swarm/strategies/PriorityStrategy.ts +3 -3
  100. package/packages/core/src/tools/ToolExecutor.ts +7 -3
  101. package/packages/core/src/tools/core/index.ts +2 -2
  102. package/packages/core/src/tools/cron/index.ts +4 -4
  103. package/packages/core/src/tools/web/artifact-inspect.ts +2 -2
  104. package/packages/core/src/tools/web/artifact-read.ts +162 -0
  105. package/packages/core/src/tools/web/browser-backend.ts +226 -0
  106. package/packages/core/src/tools/web/browser-click.ts +2 -2
  107. package/packages/core/src/tools/web/browser-extract.ts +2 -2
  108. package/packages/core/src/tools/web/browser-navigate.ts +2 -2
  109. package/packages/core/src/tools/web/browser-screenshot.ts +12 -5
  110. package/packages/core/src/tools/web/browser-script.ts +2 -2
  111. package/packages/core/src/tools/web/browser-service.ts +85 -366
  112. package/packages/core/src/tools/web/browser-session.ts +125 -0
  113. package/packages/core/src/tools/web/browser-type.ts +2 -2
  114. package/packages/core/src/tools/web/browser-wait.ts +2 -2
  115. package/packages/core/src/tools/web/computer-use.ts +553 -0
  116. package/packages/core/src/tools/web/index.ts +8 -1
  117. package/packages/core/src/tools/web/webview-backend.ts +851 -0
  118. package/packages/core/src/utils/index.ts +1 -0
  119. package/packages/core/src/utils/logger.ts +12 -4
  120. package/packages/core/src/utils/redact-binary.ts +17 -0
  121. package/packages/core/src/utils/toon.ts +1 -1
  122. package/packages/core/src/voice/index.ts +6 -6
  123. package/bun.lock +0 -833
  124. package/bunfig.toml +0 -9
  125. package/docs/API-AGENTS.md +0 -367
  126. package/docs/API-CONTEXT-COMPILER.md +0 -249
  127. package/docs/API-DAG-SCHEDULER.md +0 -273
  128. package/docs/API-TOOLS-SKILLS-CHANNELS.md +0 -446
  129. package/docs/API-WORKERS-EVENTS.md +0 -299
  130. package/docs/HIVE-HARNESS.md +0 -113
  131. package/docs/INDEX.md +0 -190
  132. package/docs/TEMPLATE-HIVE-APP.md +0 -360
  133. package/packages/cli/package.json +0 -17
  134. package/packages/cli/src/commands/create-app.test.ts +0 -180
  135. package/packages/core/package.json +0 -70
  136. package/packages/core/src/api/createAgent.test.ts +0 -160
  137. package/packages/core/src/canvas/canvas.test.ts +0 -36
  138. package/packages/core/src/channels/channels.test.ts +0 -18
  139. package/packages/core/src/ethics/EthicsGuard.test.ts +0 -108
  140. package/packages/core/src/gateway/gateway.test.ts +0 -38
  141. package/packages/core/src/memory/Scratchpad.test.ts +0 -68
  142. package/packages/core/src/scheduler/scheduler.test.ts +0 -15
  143. package/packages/core/src/skills/skills.test.ts +0 -62
  144. package/packages/core/src/swarm/swarm.test.ts +0 -24
  145. package/packages/core/src/tool-runtime/tool-runtime.test.ts +0 -99
  146. package/packages/core/src/tools/ToolRegistry.test.ts +0 -98
  147. package/packages/core/src/tools/api/api-request.test.ts +0 -164
  148. package/packages/core/src/tools/web/browser-service.test.ts +0 -83
  149. package/packages/core/src/workers/workers.test.ts +0 -41
  150. package/scripts/bump-version.ts +0 -248
  151. package/scripts/generate-skill-bundle.ts +0 -108
  152. package/test/agent-loop-terminal-synthesis.test.ts +0 -32
  153. package/test/catalog-agents-stay-enabled.test.ts +0 -117
  154. package/test/causal-events.test.ts +0 -117
  155. package/test/compaction.test.ts +0 -105
  156. package/test/context-compiler.test.ts +0 -269
  157. package/test/curator.test.ts +0 -130
  158. package/test/durable-queue.test.ts +0 -114
  159. package/test/harness-barrel.test.ts +0 -64
  160. package/test/hive-helpers.test.ts +0 -130
  161. package/test/hivedb-search.test.ts +0 -189
  162. package/test/internal-turns.test.ts +0 -166
  163. package/test/job-idempotency.test.ts +0 -68
  164. package/test/job-retry-backoff.test.ts +0 -184
  165. package/test/job-store.test.ts +0 -381
  166. package/test/llm-retry.test.ts +0 -97
  167. package/test/memory-perf.test.ts +0 -774
  168. package/test/minimal-loadout.test.ts +0 -78
  169. package/test/model-catalog.test.ts +0 -105
  170. package/test/preload.ts +0 -12
  171. package/test/reflector.test.ts +0 -320
  172. package/test/retention-cap.test.ts +0 -91
  173. package/test/retired-capabilities-pruned.test.ts +0 -192
  174. package/test/run-store.test.ts +0 -355
  175. package/test/scratchpad.test.ts +0 -74
  176. package/test/secrets-durability.test.ts +0 -119
  177. package/test/seed-model-reseed.test.ts +0 -155
  178. package/test/setup-agent-seed.test.ts +0 -264
  179. package/test/tool-inventory.test.ts +0 -65
  180. package/test/tool-runtime.test.ts +0 -258
  181. package/test/toon.test.ts +0 -429
  182. package/tsconfig.json +0 -42
@@ -21,27 +21,28 @@
21
21
  * TODOS los datos se formatean en TOON para ahorro de tokens.
22
22
  */
23
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"
24
+ import { col, fromIndexable } from "../storage/hive.ts"
25
+ import type { AgentDoc, ModelDoc } from "../storage/collections.ts"
26
+ import { logger } from "../utils/logger.ts"
27
+ import type { LLMMessage, LLMToolDef, ContentPart } from "./llm-client.ts"
28
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"
29
+ import { syncToolCatalogToIndex, mcpToolFullName } from "./tool-selector.ts"
30
+ import { syncSkillsToIndex, getMinimalSkills, selectSkills, getSkillByName, type SkillDescriptor } from "./skill-selector.ts"
31
+ import { syncPlaybookToIndex, selectPlaybookRules } from "./playbook-selector.ts"
32
+ import { getRecentMessages, getSummary, getScratchpad, toAPIMessages } from "./conversation-store.ts"
33
+ import { formatContext, estimateTokens } from "../utils/toon.ts"
34
+ import { buildSystemPromptWithProjects } from "./prompt-builder.ts"
35
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"
36
+ import { resolveUserId } from "../storage/onboarding.ts"
37
+ import { getMCPManager as getSingletonMCPManager } from "../mcp/singleton.ts"
38
+ import { syncMCPToolsToDB, syncMCPToolsToIndex } from "../mcp/tool-sync.ts"
39
+ import { getUserDate, getUserTime } from "../utils/date.ts"
40
+ import { loadConfig } from "../config/loader.ts"
41
+ import { getHiveDb } from "../storage/hivedb.ts"
42
+ import { listCatalogAgents, renderAgentRoutingCatalog } from "./catalog-selector.ts"
43
+ import { expandToolAllowlist } from "./delegation-runtime.ts"
44
+ import { MINIMAL_TOOLS } from "./minimal-loadout.ts"
45
+ import { normalizeMcpResult } from "./mcp-result-normalizer.ts"
45
46
 
46
47
  const log = logger.child("context-compiler")
47
48
 
@@ -238,7 +239,7 @@ export async function compileContext(opts: {
238
239
 
239
240
  if (effectiveMcpManager) {
240
241
  try {
241
- const mcpServersCol = await col<import("../storage/collections").McpServerDoc>("mcpServers")
242
+ const mcpServersCol = await col<import("../storage/collections.ts").McpServerDoc>("mcpServers")
242
243
  const assignedMcpIds = new Set<string>([
243
244
  ...(agent.mcp_server_ids_json ? JSON.parse(agent.mcp_server_ids_json) : []),
244
245
  ...(agent.active_mcp_json ? JSON.parse(agent.active_mcp_json) : []),
@@ -293,10 +294,20 @@ export async function compileContext(opts: {
293
294
  name: fullName,
294
295
  description: mcpTool.description || `Tool from ${server.name}`,
295
296
  parameters: mcpTool.inputSchema || { type: "object", properties: {} },
296
- execute: async (params: Record<string, unknown>) => {
297
+ execute: async (params: Record<string, unknown>, config?: { configurable?: Record<string, unknown> }) => {
297
298
  // Return raw JS value — agent-loop will TOON-encode via formatToolResult.
298
299
  // Never pre-stringify here: formatToolResult(string) double-encodes.
299
- return await effectiveMcpManager.callTool(resolvedServerKey, mcpTool.name, params)
300
+ const raw = await effectiveMcpManager.callTool(resolvedServerKey, mcpTool.name, params)
301
+ // MCP results can carry base64 image/audio/blob content blocks —
302
+ // normalize those into artifact_ref pointers so a large binary
303
+ // result never gets serialized whole into the LLM context (see
304
+ // mcp-result-normalizer.ts for the incident this prevents).
305
+ const configurable = config?.configurable ?? {}
306
+ return await normalizeMcpResult(raw, {
307
+ userId: configurable.user_id ? String(configurable.user_id) : undefined,
308
+ runId: configurable.run_id ? String(configurable.run_id) : null,
309
+ taskId: configurable.task_id ? String(configurable.task_id) : null,
310
+ })
300
311
  },
301
312
  })
302
313
 
@@ -352,17 +363,30 @@ export async function compileContext(opts: {
352
363
  // Only native minimal tools in LLM context
353
364
  // MCP tools are discovered dynamically via search_knowledge(type="mcp")
354
365
  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
- : [],
366
+
367
+ // La lista blanca se aplica a `allTools`, no sólo al prompt inicial. Filtrar
368
+ // únicamente el loadout de arranque no restringe nada: `search_knowledge`
369
+ // busca contra el índice completo y agent-loop.ts inyecta lo que encuentre
370
+ // resolviéndolo contra `allTools`, así que un agente restringido llegaba
371
+ // igual a cualquier tool nativa por descubrimiento dinámico. Sacarla de
372
+ // `allTools` la vuelve irresoluble: se puede encontrar en el índice, pero no
373
+ // se puede cargar ni llamar.
374
+ //
375
+ // Antes esto dependía de `isCatalogAgent`, y los agentes creados por el
376
+ // usuario —que son los que un host multi-tenant define— quedaban sin límite.
377
+ // Ahora depende de que el agente declare una lista: el coordinador, que no
378
+ // declara ninguna, conserva el descubrimiento abierto.
379
+ const declaredAllowlist = agent.tool_allowlist_json
380
+ ? expandToolAllowlist(
381
+ JSON.parse(agent.tool_allowlist_json),
382
+ nativeTools.map((tool) => tool.name),
365
383
  )
384
+ : agent.tools_json
385
+ ? (JSON.parse(agent.tools_json) as string[])
386
+ : null
387
+
388
+ if (declaredAllowlist) {
389
+ const allowedNames = new Set<string>(declaredAllowlist)
366
390
  filteredNativeTools = nativeTools.filter((tool) => allowedNames.has(tool.name))
367
391
  allTools = [...filteredNativeTools, ...mcpToolExecutors]
368
392
  }
@@ -508,7 +532,7 @@ export async function compileContext(opts: {
508
532
  }
509
533
 
510
534
  // [STEP-10b] Inject current date/time (ENTORNO ACTUAL)
511
- const usersCol = await col<import("../storage/collections").UserDoc>("users")
535
+ const usersCol = await col<import("../storage/collections.ts").UserDoc>("users")
512
536
  const userRow = await usersCol.get(userId)
513
537
  const userTimezone = userRow?.doc.timezone || "UTC"
514
538
  const now = new Date()
@@ -585,7 +609,7 @@ export async function compileContext(opts: {
585
609
  .filter((line): line is string => !!line)
586
610
 
587
611
  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`
612
+ systemPrompt += `\n\n# CAUSAL CONTEXT (decisiones y tool calls de este turno, previos a la compactación — prioriza la conversación actual; úsalo solo para no repetir algo que ya funcionó o ya falló)\n${causalLines.join("\n")}\n`
589
613
  log.info(`[context-compiler] [STEP-9d] ✅ Injected ${causalLines.length} causal context item(s)`)
590
614
  }
591
615
  } catch (err) {
@@ -5,12 +5,13 @@
5
5
  * Also manages: summaries and scratchpad, both HiveDB document collections.
6
6
  */
7
7
 
8
- import { col, nextId, bumpRollup } from "../storage/hive"
9
- import { getHiveDb } from "../storage/hivedb"
10
- import { logger } from "../utils/logger"
11
- import type { LLMMessage, ContentPart } from "./llm-client"
12
- import { estimateTokens } from "../utils/toon"
13
- import type { ConversationDoc, SummaryDoc, MessageSource } from "../storage/collections"
8
+ import { col, nextId, bumpRollup } from "../storage/hive.ts"
9
+ import { getHiveDb } from "../storage/hivedb.ts"
10
+ import { logger } from "../utils/logger.ts"
11
+ import type { LLMMessage, ContentPart } from "./llm-client.ts"
12
+ import { estimateTokens } from "../utils/toon.ts"
13
+ import type { ConversationDoc, SummaryDoc, MessageSource } from "../storage/collections.ts"
14
+ import { touchThread } from "./thread-store.ts"
14
15
 
15
16
  const log = logger.child("conv-store")
16
17
 
@@ -45,13 +46,26 @@ export interface StoredMessage {
45
46
  // stays clean and the wording can evolve without a migration.
46
47
 
47
48
  export const INTERNAL_SOURCES: ReadonlySet<string> =
48
- new Set(["task_complete", "delegation_summary", "legacy_internal"])
49
+ new Set(["task_complete", "delegation_summary", "legacy_internal", "realtime_chat"])
49
50
 
50
51
  export function isInternalSource(source: string | null | undefined): boolean {
51
52
  return !!source && INTERNAL_SOURCES.has(source)
52
53
  }
53
54
 
54
55
  export function formatInternalEvent(source: string, content: string): string {
56
+ // La charla hablada sí salió del usuario, pero la sesión de voz ya la
57
+ // respondió: es contexto, no trabajo por hacer. Sin esta distinción el
58
+ // coordinador leía cada frase suelta de una llamada como un pedido nuevo y
59
+ // delegaba una tarea por cada una.
60
+ if (source === "realtime_chat") {
61
+ return `<hive:voice_context>\n` +
62
+ `Fragmento de una conversación hablada que la voz de Hive YA respondió en el momento. ` +
63
+ `Es contexto de lo que vinieron hablando, NO un pedido pendiente: no ejecutes ni delegues nada por esto. ` +
64
+ `El trabajo real llega siempre como un mensaje aparte y explícito.\n\n` +
65
+ `${content}\n` +
66
+ `</hive:voice_context>`
67
+ }
68
+
55
69
  return `<hive:internal_event source="${source}">\n` +
56
70
  `Evento interno del sistema — NO es un mensaje del usuario. No lo cites literalmente, no expongas IDs internos (task_id, worker_id) ni JSON crudo. Respondé al usuario de forma natural y breve.\n\n` +
57
71
  `${content}\n` +
@@ -146,6 +160,16 @@ export async function addMessage(
146
160
  bumpRollup("activityRollups", hour, { messageCount: 1 }).catch(() => {})
147
161
  recentMessageTimestamps.push(now)
148
162
 
163
+ // Igual de opcional: el registro de conversaciones es el catálogo que alimenta la
164
+ // lista de la web (título, orden, contador). Se actualiza acá y no en cada llamador
165
+ // para que todo camino que escriba un mensaje —canales, webchat, API, voz— lo
166
+ // mantenga al día sin repetir la llamada.
167
+ touchThread(threadId, {
168
+ role,
169
+ text: textContent,
170
+ internal: isInternalSource(opts?.source),
171
+ }).catch(() => {})
172
+
149
173
  return seq
150
174
  }
151
175
 
@@ -13,8 +13,8 @@
13
13
  * (id="curator:lastReflection") instead of SQL's MAX(source_reflection_id).
14
14
  */
15
15
 
16
- import { logger } from "../utils/logger"
17
- import { col, nextId, toIndexable, fromIndexable } from "../storage/hive"
16
+ import { logger } from "../utils/logger.ts"
17
+ import { col, nextId, toIndexable, fromIndexable } from "../storage/hive.ts"
18
18
  import type {
19
19
  ReflectionDoc,
20
20
  PlaybookDoc,
@@ -22,7 +22,7 @@ import type {
22
22
  CursorDoc,
23
23
  AgentProposalDoc,
24
24
  TraceDoc,
25
- } from "../storage/collections"
25
+ } from "../storage/collections.ts"
26
26
 
27
27
  const log = logger.child("curator")
28
28
 
@@ -149,7 +149,7 @@ async function curateAgentStructure(): Promise<void> {
149
149
  })
150
150
  }
151
151
 
152
- const { syncCatalogAgentsToIndex } = await import("./catalog-selector")
152
+ const { syncCatalogAgentsToIndex } = await import("./catalog-selector.ts")
153
153
  await syncCatalogAgentsToIndex()
154
154
  }
155
155
 
@@ -1,4 +1,4 @@
1
- import { col, toIndexable, fromIndexable, updateDoc } from "../storage/hive";
1
+ import { col, toIndexable, fromIndexable, updateDoc } from "../storage/hive.ts";
2
2
  import type {
3
3
  AgentDoc,
4
4
  McpServerDoc,
@@ -6,11 +6,11 @@ import type {
6
6
  ProviderDoc,
7
7
  SkillDoc,
8
8
  AgentModelOverride,
9
- } from "../storage/collections";
10
- import { createAllTools } from "../tools";
11
- import { loadConfig } from "../config/loader";
9
+ } from "../storage/collections.ts";
10
+ import { createAllTools } from "../tools/index.ts";
11
+ import { loadConfig } from "../config/loader.ts";
12
12
  import type { MCPClientManager } from "../mcp/index.ts";
13
- import { logger } from "../utils/logger";
13
+ import { logger } from "../utils/logger.ts";
14
14
 
15
15
  const log = logger.child("delegation-runtime");
16
16
  const MCP_IDLE_TTL_MS = 2 * 60_000;
@@ -14,15 +14,15 @@
14
14
  * entire run (not per-turn). This prevents endless loops.
15
15
  */
16
16
 
17
- import { logger } from "../utils/logger";
18
- import { callLLM, type LLMMessage } from "./llm-client";
19
- import { createRun, type AcceptanceCriterion } from "./run-store";
20
- import { clearOldToolResults } from "./compaction";
21
- import { loadConfig } from "../config/loader";
17
+ import { logger } from "../utils/logger.ts";
18
+ import { callLLM, type LLMMessage } from "./llm-client.ts";
19
+ import { createRun, type AcceptanceCriterion } from "./run-store.ts";
20
+ import { clearOldToolResults } from "./compaction.ts";
21
+ import { loadConfig } from "../config/loader.ts";
22
22
  import { getDurableQueue } from "../gateway/durable-queue.ts";
23
- import { recordLLMUsage } from "./tracer";
23
+ import { recordLLMUsage } from "./tracer.ts";
24
24
 
25
- export type { AcceptanceCriterion } from "./run-store";
25
+ export type { AcceptanceCriterion } from "./run-store.ts";
26
26
 
27
27
  export interface AcceptanceResult {
28
28
  id: string;
@@ -126,8 +126,8 @@ export async function runGoal(opts: GoalRunOptions): Promise<GoalRunResult> {
126
126
  /** Runs a deterministic goal_check_tool, no LLM involved. */
127
127
  async function runDeterministicCheck(checkTool: string, goal: string): Promise<{ met: boolean; reason: string } | null> {
128
128
  try {
129
- const { executeToolBatch } = await import("../tool-runtime");
130
- const { createAllTools } = await import("../tools/index");
129
+ const { executeToolBatch } = await import("../tool-runtime/index.ts");
130
+ const { createAllTools } = await import("../tools/index.ts");
131
131
  const allTools = createAllTools(loadConfig());
132
132
  const toolDef = allTools.find((t) => t.name === checkTool);
133
133
  if (!toolDef) {
@@ -15,3 +15,4 @@ export * from "./run-store.ts";
15
15
  export * from "./service.ts";
16
16
  export * from "./skill-selector.ts";
17
17
  export * from "./tool-selector.ts";
18
+ export * from "./realtime-providers/index.ts";
@@ -11,26 +11,26 @@
11
11
  * Public interface (LLMMessage, callLLM, resolveProviderConfig) is stable.
12
12
  */
13
13
 
14
- import { logger } from "../utils/logger"
15
- import { loadConfig } from "../config/loader"
16
- import { withRetry, isRetryableError, type RetryPolicy } from "../resilience/retry"
17
- import { GeminiProvider } from "./llm-providers/gemini"
18
- import { AnthropicProvider } from "./llm-providers/anthropic"
19
- import { OllamaProvider } from "./llm-providers/ollama"
20
- import { OpenAIProvider } from "./llm-providers/openai"
21
- import { GroqProvider } from "./llm-providers/groq"
22
- import { MistralProvider } from "./llm-providers/mistral"
23
- import { OpenRouterProvider } from "./llm-providers/openrouter"
24
- import { DeepSeekProvider } from "./llm-providers/deepseek"
25
- import { KimiProvider } from "./llm-providers/kimi"
26
- import { NvidiaProvider } from "./llm-providers/nvidia"
27
- import { QwenProvider } from "./llm-providers/qwen"
28
- import { MiniMaxProvider } from "./llm-providers/minimax"
29
- import { OpenCodeGoProvider } from "./llm-providers/opencode-go"
30
- import { HiveAgentsProvider } from "./llm-providers/hiveagents"
31
- import { ZaiProvider } from "./llm-providers/z-ai"
32
- import { ModelScopeProvider } from "./llm-providers/modelscope"
33
- import type { LLMProvider } from "./llm-providers/interface"
14
+ import { logger } from "../utils/logger.ts"
15
+ import { loadConfig } from "../config/loader.ts"
16
+ import { withRetry, isRetryableError, type RetryPolicy } from "../resilience/retry.ts"
17
+ import { GeminiProvider } from "./llm-providers/gemini.ts"
18
+ import { AnthropicProvider } from "./llm-providers/anthropic.ts"
19
+ import { OllamaProvider } from "./llm-providers/ollama.ts"
20
+ import { OpenAIProvider } from "./llm-providers/openai.ts"
21
+ import { GroqProvider } from "./llm-providers/groq.ts"
22
+ import { MistralProvider } from "./llm-providers/mistral.ts"
23
+ import { OpenRouterProvider } from "./llm-providers/openrouter.ts"
24
+ import { DeepSeekProvider } from "./llm-providers/deepseek.ts"
25
+ import { KimiProvider } from "./llm-providers/kimi.ts"
26
+ import { NvidiaProvider } from "./llm-providers/nvidia.ts"
27
+ import { QwenProvider } from "./llm-providers/qwen.ts"
28
+ import { MiniMaxProvider } from "./llm-providers/minimax.ts"
29
+ import { OpenCodeGoProvider } from "./llm-providers/opencode-go.ts"
30
+ import { HiveAgentsProvider } from "./llm-providers/hiveagents.ts"
31
+ import { ZaiProvider } from "./llm-providers/z-ai.ts"
32
+ import { ModelScopeProvider } from "./llm-providers/modelscope.ts"
33
+ import type { LLMProvider } from "./llm-providers/interface.ts"
34
34
 
35
35
  const log = logger.child("llm-client")
36
36
 
@@ -182,20 +182,55 @@ export async function callLLM(options: LLMCallOptions): Promise<LLMResponse> {
182
182
  const cleanModel = options.model.replace(new RegExp(`^${options.provider}\\/`), "")
183
183
  const status = extractErrorStatus(err)
184
184
  const modelUnavailable = status === 404 || status === 410
185
- const msg = modelUnavailable
186
- ? `El modelo "${cleanModel}" ya no existe en ${options.provider} (HTTP ${status}). `
187
- + `El proveedor lo retiró de su catálogo; reintentar no sirve. `
188
- + `Elegí otro modelo en Ajustes → Proveedores.`
189
- : (err as Error).message
185
+ const msg = describeProviderFailure(err, status, options.provider, cleanModel)
190
186
  log.error(`[llm-client] Error calling ${options.provider}/${cleanModel}: ${msg}`, err)
191
187
  return {
192
- content: `[LLM Error] ${msg}`,
188
+ content: `${LLM_ERROR_PREFIX} ${msg}`,
193
189
  stop_reason: "error",
194
190
  error: { message: msg, status, modelUnavailable },
195
191
  }
196
192
  }
197
193
  }
198
194
 
195
+ /**
196
+ * Marks content that is a provider failure rather than something the agent
197
+ * said. webchat-turn.ts keys the failed-turn UI off this prefix, and
198
+ * agent-loop.ts keeps such content out of the conversation history.
199
+ */
200
+ export const LLM_ERROR_PREFIX = "[LLM Error]"
201
+
202
+ /**
203
+ * Turns a provider failure into something the user can act on.
204
+ *
205
+ * The raw SDK text is written for whoever is reading a stack trace: "429 status
206
+ * code (no body)" showed up verbatim in the chat and says nothing about what
207
+ * happened (the account hit its rate limit) or what to do about it. Statuses
208
+ * without a specific mapping keep the original message — a wrong explanation is
209
+ * worse than a technical one.
210
+ */
211
+ export function describeProviderFailure(
212
+ err: unknown,
213
+ status: number | undefined,
214
+ provider: string,
215
+ cleanModel: string,
216
+ ): string {
217
+ if (status === 404 || status === 410) {
218
+ return `El modelo "${cleanModel}" ya no existe en ${provider} (HTTP ${status}). `
219
+ + `El proveedor lo retiró de su catálogo; reintentar no sirve. `
220
+ + `Elige otro modelo en Ajustes → Proveedores.`
221
+ }
222
+ if (status === 429) {
223
+ return `${provider} está limitando las peticiones (HTTP 429) y los reintentos tampoco pasaron. `
224
+ + `Es el límite de uso de tu cuenta, no un problema del modelo: esperá unos minutos, `
225
+ + `revisa tu cuota con el proveedor, o cambiá de modelo en Ajustes → Proveedores.`
226
+ }
227
+ if (status === 401 || status === 403) {
228
+ return `${provider} rechazó la API key (HTTP ${status}). `
229
+ + `Revisa que siga siendo válida y tenga acceso a "${cleanModel}" en Ajustes → Proveedores.`
230
+ }
231
+ return (err as Error).message
232
+ }
233
+
199
234
  /** Provider SDKs disagree on where the HTTP status lands; check every shape we've seen. */
200
235
  function extractErrorStatus(err: unknown): number | undefined {
201
236
  const e = err as { status?: number; statusCode?: number; response?: { status?: number } }
@@ -208,10 +243,10 @@ function extractErrorStatus(err: unknown): number | undefined {
208
243
  * Returns null when the DB has no usable LLM (e.g. fresh install before setup).
209
244
  */
210
245
  export async function getDefaultLLM(): Promise<{ provider: string; model: string } | null> {
211
- const { col, fromIndexable } = await import("../storage/hive")
212
- const agentsCol = await col<import("../storage/collections").AgentDoc>("agents")
213
- const modelsCol = await col<import("../storage/collections").ModelDoc>("models")
214
- const providersCol = await col<import("../storage/collections").ProviderDoc>("providers")
246
+ const { col, fromIndexable } = await import("../storage/hive.ts")
247
+ const agentsCol = await col<import("../storage/collections.ts").AgentDoc>("agents")
248
+ const modelsCol = await col<import("../storage/collections.ts").ModelDoc>("models")
249
+ const providersCol = await col<import("../storage/collections.ts").ProviderDoc>("providers")
215
250
 
216
251
  const coordinators = await agentsCol.findBy("role", "coordinator")
217
252
  const coordinator = coordinators[0]
@@ -234,14 +269,34 @@ export async function getDefaultLLM(): Promise<{ provider: string; model: string
234
269
  /**
235
270
  * Resolve provider config from DB (decrypts API key).
236
271
  */
272
+ /**
273
+ * Credenciales y endpoint resueltos por quien llama, en vez de por el proceso.
274
+ *
275
+ * Sin esto, la única forma de elegir la key era el secret store de HiveDB o
276
+ * `process.env[PROVIDER_API_KEY]` — ambos globales al proceso. Un backend
277
+ * multi-tenant no tiene un proceso por inquilino: o mutaba `process.env` antes
278
+ * de cada llamada (y entonces dos workspaces concurrentes se pisaban la key), o
279
+ * levantaba un proceso por llamada sólo para aislarla. Pasar la credencial en la
280
+ * llamada elimina las dos cosas.
281
+ *
282
+ * Todo es opcional: lo que no venga acá se resuelve como siempre.
283
+ */
284
+ export interface ProviderCredentials {
285
+ /** Reemplaza la key del secret store y la de `process.env`. */
286
+ apiKey?: string
287
+ /** Reemplaza el `base_url` de la fila del proveedor. */
288
+ baseUrl?: string
289
+ }
290
+
237
291
  export async function resolveProviderConfig(
238
292
  providerId: string,
239
- modelId: string
293
+ modelId: string,
294
+ credentials?: ProviderCredentials
240
295
  ): Promise<Pick<LLMCallOptions, "provider" | "model" | "apiKey" | "baseUrl" | "numCtx" | "numGpu" | "contextWindow">> {
241
- const { col } = await import("../storage/hive")
242
- const { loadProviderApiKey } = await import("../storage/crypto")
243
- const providersCol = await col<import("../storage/collections").ProviderDoc>("providers")
244
- const modelsCol = await col<import("../storage/collections").ModelDoc>("models")
296
+ const { col } = await import("../storage/hive.ts")
297
+ const { loadProviderApiKey } = await import("../storage/crypto.ts")
298
+ const providersCol = await col<import("../storage/collections.ts").ProviderDoc>("providers")
299
+ const modelsCol = await col<import("../storage/collections.ts").ModelDoc>("models")
245
300
 
246
301
  const providerEntry = await providersCol.get(providerId)
247
302
  const providerRow = (providerEntry?.doc.enabled && providerEntry?.doc.active) ? providerEntry.doc : undefined
@@ -249,7 +304,13 @@ export async function resolveProviderConfig(
249
304
  // Load model's context window for token budget management
250
305
  const modelEntry = await modelsCol.get(modelId)
251
306
 
252
- let apiKey = await loadProviderApiKey(providerId)
307
+ // La credencial de la llamada gana, y corta acá: si vino una key explícita no
308
+ // se consulta el secret store ni el entorno, así que un fallback global no
309
+ // puede colarse en el lugar de la del inquilino.
310
+ let apiKey = credentials?.apiKey
311
+ if (!apiKey) {
312
+ apiKey = await loadProviderApiKey(providerId)
313
+ }
253
314
  if (!apiKey) {
254
315
  apiKey = process.env[`${providerId.toUpperCase()}_API_KEY`] || ""
255
316
  }
@@ -258,7 +319,7 @@ export async function resolveProviderConfig(
258
319
  provider: providerId,
259
320
  model: modelId,
260
321
  apiKey,
261
- baseUrl: providerRow?.base_url || undefined,
322
+ baseUrl: credentials?.baseUrl || providerRow?.base_url || undefined,
262
323
  numCtx: providerRow?.num_ctx ?? undefined,
263
324
  numGpu: providerRow?.num_gpu ?? undefined,
264
325
  contextWindow: modelEntry?.doc.context_window ?? undefined,
@@ -1,7 +1,7 @@
1
- import { logger } from "../../utils/logger"
2
- import { normalizeToolName, resolveMaxTokens, ensureArrayItems } from "./interface"
3
- import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall, ThinkingBlock } from "./interface"
4
- import type { ContentPart, LLMMessage } from "../llm-client"
1
+ import { logger } from "../../utils/logger.ts"
2
+ import { normalizeToolName, resolveMaxTokens, ensureArrayItems } from "./interface.ts"
3
+ import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall, ThinkingBlock } from "./interface.ts"
4
+ import type { ContentPart, LLMMessage } from "../llm-client.ts"
5
5
 
6
6
  const log = logger.child("llm-client")
7
7
 
@@ -1,4 +1,4 @@
1
- import { OpenAICompatBase } from "./openai-compat-base"
1
+ import { OpenAICompatBase } from "./openai-compat-base.ts"
2
2
 
3
3
  export class DeepSeekProvider extends OpenAICompatBase {
4
4
  constructor() { super("deepseek") }
@@ -1,7 +1,7 @@
1
- import { logger } from "../../utils/logger"
2
- import { sanitizeMessages, resolveMaxTokens, ensureArrayItems } from "./interface"
3
- import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface"
4
- import type { ContentPart, LLMMessage } from "../llm-client"
1
+ import { logger } from "../../utils/logger.ts"
2
+ import { sanitizeMessages, resolveMaxTokens, ensureArrayItems } from "./interface.ts"
3
+ import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface.ts"
4
+ import type { ContentPart, LLMMessage } from "../llm-client.ts"
5
5
 
6
6
  const log = logger.child("llm-client")
7
7
 
@@ -1,4 +1,4 @@
1
- import { OpenAICompatBase } from "./openai-compat-base"
1
+ import { OpenAICompatBase } from "./openai-compat-base.ts"
2
2
 
3
3
  export class GroqProvider extends OpenAICompatBase {
4
4
  constructor() { super("groq") }
@@ -1,6 +1,6 @@
1
- import { logger } from "../../utils/logger"
2
- import { OpenAICompatBase } from "./openai-compat-base"
3
- import type { LLMCallOptions, LLMResponse } from "./interface"
1
+ import { logger } from "../../utils/logger.ts"
2
+ import { OpenAICompatBase } from "./openai-compat-base.ts"
3
+ import type { LLMCallOptions, LLMResponse } from "./interface.ts"
4
4
 
5
5
  const log = logger.child("llm-client")
6
6
 
@@ -2,10 +2,10 @@
2
2
  * Shared types and utilities for LLM providers.
3
3
  */
4
4
 
5
- import type { LLMCallOptions, LLMMessage, LLMResponse, LLMToolCall, ContentPart, ThinkingBlock } from "../llm-client"
5
+ import type { LLMCallOptions, LLMMessage, LLMResponse, LLMToolCall, ContentPart, ThinkingBlock } from "../llm-client.ts"
6
6
  export type { LLMCallOptions, LLMMessage, LLMResponse, LLMToolCall, ContentPart, ThinkingBlock }
7
7
 
8
- import { logger } from "../../utils/logger"
8
+ import { logger } from "../../utils/logger.ts"
9
9
  const log = logger.child("llm-client")
10
10
 
11
11
  // ─── Provider interface ────────────────────────────────────────────────────────
@@ -1,4 +1,4 @@
1
- import { OpenAICompatBase } from "./openai-compat-base"
1
+ import { OpenAICompatBase } from "./openai-compat-base.ts"
2
2
 
3
3
  export class KimiProvider extends OpenAICompatBase {
4
4
  constructor() { super("kimi") }
@@ -1,4 +1,4 @@
1
- import { OpenAICompatBase } from "./openai-compat-base"
1
+ import { OpenAICompatBase } from "./openai-compat-base.ts"
2
2
 
3
3
  export class MiniMaxProvider extends OpenAICompatBase {
4
4
  static readonly secretKey = "MINIMAX_API_KEY"
@@ -1,4 +1,4 @@
1
- import { OpenAICompatBase } from "./openai-compat-base"
1
+ import { OpenAICompatBase } from "./openai-compat-base.ts"
2
2
 
3
3
  export class MistralProvider extends OpenAICompatBase {
4
4
  constructor() { super("mistral") }
@@ -1,4 +1,4 @@
1
- import { OpenAICompatBase } from "./openai-compat-base"
1
+ import { OpenAICompatBase } from "./openai-compat-base.ts"
2
2
 
3
3
  export class ModelScopeProvider extends OpenAICompatBase {
4
4
  constructor() { super("modelscope") }
@@ -1,4 +1,4 @@
1
- import { OpenAICompatBase } from "./openai-compat-base"
1
+ import { OpenAICompatBase } from "./openai-compat-base.ts"
2
2
 
3
3
  export class NvidiaProvider extends OpenAICompatBase {
4
4
  constructor() { super("nvidia") }
@@ -1,7 +1,7 @@
1
- import { logger } from "../../utils/logger"
2
- import { sanitizeMessages, resolveMaxTokens, ensureArrayItems } from "./interface"
3
- import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface"
4
- import type { ContentPart, LLMMessage } from "../llm-client"
1
+ import { logger } from "../../utils/logger.ts"
2
+ import { sanitizeMessages, resolveMaxTokens, ensureArrayItems } from "./interface.ts"
3
+ import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface.ts"
4
+ import type { ContentPart, LLMMessage } from "../llm-client.ts"
5
5
 
6
6
  const log = logger.child("llm-client")
7
7
 
@@ -1,11 +1,11 @@
1
- import { logger } from "../../utils/logger"
1
+ import { logger } from "../../utils/logger.ts"
2
2
  import {
3
3
  sanitizeMessages, requiresTemperature1, OPENAI_COMPAT_BASE_URLS,
4
4
  getProviderProfile, modelSupportsTools, normalizeToolName, normalizeToolSchema,
5
5
  resolveMaxTokens,
6
- } from "./interface"
7
- import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface"
8
- import type { ContentPart, LLMMessage } from "../llm-client"
6
+ } from "./interface.ts"
7
+ import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface.ts"
8
+ import type { ContentPart, LLMMessage } from "../llm-client.ts"
9
9
 
10
10
  const log = logger.child("llm-client")
11
11
 
@@ -44,7 +44,11 @@ function compactBodyForContextOverflow(body: any, err: any): void {
44
44
  }
45
45
 
46
46
  export abstract class OpenAICompatBase implements LLMProvider {
47
- constructor(protected readonly providerName: string) {}
47
+ protected readonly providerName: string
48
+
49
+ constructor(providerName: string) {
50
+ this.providerName = providerName
51
+ }
48
52
 
49
53
  /** Override to true when the provider requires reasoning_content to be round-tripped. */
50
54
  protected needsReasoningRoundtrip(): boolean { return false }
@@ -1,4 +1,4 @@
1
- import { OpenAICompatBase } from "./openai-compat-base"
1
+ import { OpenAICompatBase } from "./openai-compat-base.ts"
2
2
 
3
3
  export class OpenAIProvider extends OpenAICompatBase {
4
4
  constructor() { super("openai") }