@johpaz/hive-sdk 0.1.4 → 0.1.5

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 (272) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +78 -23
  3. package/bunfig.toml +4 -2
  4. package/docs/API-AGENTS.md +78 -27
  5. package/docs/API-CONTEXT-COMPILER.md +31 -34
  6. package/docs/API-TOOLS-SKILLS-CHANNELS.md +58 -22
  7. package/docs/HIVE-HARNESS.md +1 -1
  8. package/docs/INDEX.md +4 -4
  9. package/docs/TEMPLATE-HIVE-APP.md +10 -10
  10. package/package.json +9 -4
  11. package/packages/cli/package.json +2 -2
  12. package/packages/cli/src/commands/create-app.test.ts +36 -7
  13. package/packages/cli/src/commands/init.ts +3 -3
  14. package/packages/cli/src/commands/run.ts +1 -1
  15. package/packages/cli/src/commands/test.ts +37 -25
  16. package/packages/cli/src/commands/trace.ts +30 -28
  17. package/packages/cli/templates/hive-app/.env.example +10 -2
  18. package/packages/cli/templates/hive-app/README.md +103 -0
  19. package/packages/cli/templates/hive-app/hive.config.ts +9 -3
  20. package/packages/cli/templates/hive-app/src/agents/coordinator.ts +8 -1
  21. package/packages/cli/templates/hive-app/src/main.ts +12 -19
  22. package/packages/core/package.json +5 -4
  23. package/packages/core/src/agent/acceptance-checks.ts +166 -0
  24. package/packages/core/src/agent/agent-catalog.ts +348 -0
  25. package/packages/core/src/agent/agent-loop.ts +1373 -0
  26. package/packages/core/src/agent/capability-search.ts +186 -0
  27. package/packages/core/src/agent/catalog-selector.ts +103 -0
  28. package/packages/core/src/agent/{Compaction.ts → compaction.ts} +86 -63
  29. package/packages/core/src/agent/context-compiler.ts +689 -0
  30. package/packages/core/src/agent/conversation-store.ts +381 -0
  31. package/packages/core/src/agent/curator.ts +276 -0
  32. package/packages/core/src/agent/delegation-runtime.ts +241 -0
  33. package/packages/core/src/agent/goal-runner.ts +323 -0
  34. package/packages/core/src/agent/index.ts +17 -12
  35. package/packages/core/src/agent/llm-client.ts +266 -0
  36. package/packages/core/src/agent/llm-providers/anthropic.ts +264 -0
  37. package/packages/core/src/agent/llm-providers/deepseek.ts +8 -0
  38. package/packages/core/src/agent/{providers → llm-providers}/gemini.ts +98 -60
  39. package/packages/core/src/agent/llm-providers/groq.ts +5 -0
  40. package/packages/core/src/agent/llm-providers/hiveagents.ts +253 -0
  41. package/packages/core/src/agent/{providers → llm-providers}/interface.ts +73 -13
  42. package/packages/core/src/agent/llm-providers/kimi.ts +8 -0
  43. package/packages/core/src/agent/llm-providers/minimax.ts +13 -0
  44. package/packages/core/src/agent/llm-providers/mistral.ts +5 -0
  45. package/packages/core/src/agent/llm-providers/modelscope.ts +5 -0
  46. package/packages/core/src/agent/llm-providers/nvidia.ts +5 -0
  47. package/packages/core/src/agent/{providers → llm-providers}/ollama.ts +31 -5
  48. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +418 -0
  49. package/packages/core/src/agent/llm-providers/openai.ts +5 -0
  50. package/packages/core/src/agent/llm-providers/opencode-go.ts +9 -0
  51. package/packages/core/src/agent/llm-providers/openrouter.ts +5 -0
  52. package/packages/core/src/agent/llm-providers/qwen.ts +5 -0
  53. package/packages/core/src/agent/llm-providers/z-ai.ts +5 -0
  54. package/packages/core/src/agent/minimal-loadout.ts +47 -0
  55. package/packages/core/src/agent/playbook-selector.ts +119 -0
  56. package/packages/core/src/agent/{PromptBuilder.ts → prompt-builder.ts} +21 -22
  57. package/packages/core/src/{harness → agent}/proof-packet.ts +16 -21
  58. package/packages/core/src/agent/providers/index.ts +35 -16
  59. package/packages/core/src/agent/reflector.ts +320 -0
  60. package/packages/core/src/agent/routing-intent.ts +22 -0
  61. package/packages/core/src/{harness → agent}/run-epoch.ts +4 -3
  62. package/packages/core/src/{harness → agent}/run-store.ts +142 -81
  63. package/packages/core/src/agent/{Service.ts → service.ts} +37 -26
  64. package/packages/core/src/agent/skill-selector.ts +374 -0
  65. package/packages/core/src/agent/stuck-loop.ts +209 -0
  66. package/packages/core/src/agent/{selectors/ToolSelector.ts → tool-selector.ts} +188 -178
  67. package/packages/core/src/{ace/Tracer.ts → agent/tracer.ts} +37 -27
  68. package/packages/core/src/api/createAgent.test.ts +139 -27
  69. package/packages/core/src/api/createAgent.ts +232 -44
  70. package/packages/core/src/artifacts/store.ts +162 -0
  71. package/packages/core/src/canvas/canvas-manager.ts +161 -0
  72. package/packages/core/src/canvas/canvas.test.ts +8 -4
  73. package/packages/core/src/canvas/emitter.ts +131 -80
  74. package/packages/core/src/canvas/index.ts +1 -3
  75. package/packages/core/src/channels/base.ts +9 -1
  76. package/packages/core/src/channels/discord.ts +5 -4
  77. package/packages/core/src/channels/manager.ts +122 -30
  78. package/packages/core/src/channels/slack.ts +5 -4
  79. package/packages/core/src/channels/telegram.ts +36 -6
  80. package/packages/core/src/channels/webchat.ts +11 -10
  81. package/packages/core/src/channels/whatsapp.ts +23 -7
  82. package/packages/core/src/config/index.ts +13 -2
  83. package/packages/core/src/config/loader.ts +71 -29
  84. package/packages/core/src/ethics/EthicsGuard.test.ts +90 -36
  85. package/packages/core/src/ethics/EthicsGuard.ts +51 -47
  86. package/packages/core/src/events/agent-bus.ts +44 -68
  87. package/packages/core/src/events/channel-narration.ts +150 -0
  88. package/packages/core/src/events/narration.ts +82 -0
  89. package/packages/core/src/events/tool-narration.ts +62 -0
  90. package/packages/core/src/gateway/delegation-groups.ts +258 -0
  91. package/packages/core/src/{harness → gateway}/durable-queue.ts +102 -42
  92. package/packages/core/src/{harness → gateway}/job-store.ts +85 -48
  93. package/packages/core/src/gateway/lane-queue.ts +173 -0
  94. package/packages/core/src/gateway/notification-inbox.ts +57 -0
  95. package/packages/core/src/gateway/server.ts +1 -1
  96. package/packages/core/src/harness/index.ts +46 -27
  97. package/packages/core/src/index.ts +33 -27
  98. package/packages/core/src/mcp/hot-reload.ts +32 -23
  99. package/packages/core/src/mcp/index.ts +6 -3
  100. package/packages/core/src/mcp/singleton.ts +1 -4
  101. package/packages/core/src/mcp/tool-sync.ts +138 -0
  102. package/packages/core/src/memory/Scratchpad.test.ts +39 -20
  103. package/packages/core/src/memory/Scratchpad.ts +27 -34
  104. package/packages/core/src/multimodal/vision-service.ts +44 -38
  105. package/packages/core/src/resilience/retry.ts +95 -0
  106. package/packages/core/src/scheduler/CronScheduler.ts +334 -287
  107. package/packages/core/src/scheduler/index.ts +9 -7
  108. package/packages/core/src/scheduler/integration.ts +46 -26
  109. package/packages/core/src/scheduler/scheduler.test.ts +9 -13
  110. package/packages/core/src/scheduler/types.ts +7 -2
  111. package/packages/core/src/security/Pairing.ts +1 -1
  112. package/packages/core/src/skills/bundled/a2ui/a2ui_dashboard/SKILL.md +176 -0
  113. package/packages/core/src/skills/bundled/a2ui/a2ui_form/SKILL.md +202 -0
  114. package/packages/core/src/skills/bundled/a2ui/a2ui_interactive/SKILL.md +206 -0
  115. package/packages/core/src/skills/bundled/agents/agent_spawner/SKILL.md +173 -0
  116. package/packages/core/src/skills/bundled/agents/memory_manager/SKILL.md +143 -0
  117. package/packages/core/src/skills/bundled/agents/research_and_remember/SKILL.md +139 -0
  118. package/packages/core/src/skills/bundled/agents/task_orchestrator/SKILL.md +98 -0
  119. package/packages/core/src/skills/bundled/api/api_client/SKILL.md +132 -0
  120. package/packages/core/src/skills/bundled/cli/cli_pipeline/SKILL.md +135 -0
  121. package/packages/core/src/skills/bundled/cli/cli_safe_exec/SKILL.md +125 -0
  122. package/packages/core/src/skills/bundled/cli/software_engineering/SKILL.md +23 -0
  123. package/packages/core/src/skills/bundled/cron_manager/SKILL.md +188 -0
  124. package/packages/core/src/skills/bundled/cron_reminder/SKILL.md +112 -0
  125. package/packages/core/src/skills/bundled/filesystem/file_manager/SKILL.md +118 -0
  126. package/packages/core/src/skills/bundled/filesystem/file_read_and_summarize/SKILL.md +109 -0
  127. package/packages/core/src/skills/bundled/filesystem/file_writer/SKILL.md +129 -0
  128. package/packages/core/src/skills/bundled/filesystem/workspace_file_operator/SKILL.md +22 -0
  129. package/packages/core/src/skills/bundled/office/office_document_manager/SKILL.md +262 -0
  130. package/packages/core/src/skills/bundled/search_knowledge/capability_discovery/SKILL.md +75 -0
  131. package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +120 -0
  132. package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +109 -0
  133. package/packages/core/src/skills/bundled/web/web_monitor/SKILL.md +127 -0
  134. package/packages/core/src/skills/bundled/web/web_research/SKILL.md +119 -0
  135. package/packages/core/src/skills/bundled-data.generated.ts +731 -2678
  136. package/packages/core/src/skills/skills.test.ts +52 -11
  137. package/packages/core/src/{harness → storage}/boot-id.ts +5 -2
  138. package/packages/core/src/storage/bootstrap.ts +151 -0
  139. package/packages/core/src/storage/causal-events.ts +84 -0
  140. package/packages/core/src/storage/collections.ts +680 -0
  141. package/packages/core/src/storage/crypto.ts +205 -74
  142. package/packages/core/src/{harness/db-helpers.ts → storage/hive.ts} +63 -7
  143. package/packages/core/src/storage/hivedb.ts +61 -0
  144. package/packages/core/src/storage/index.ts +111 -18
  145. package/packages/core/src/storage/model-id.ts +53 -0
  146. package/packages/core/src/storage/onboarding.ts +540 -972
  147. package/packages/core/src/storage/reconcile.ts +238 -0
  148. package/packages/core/src/storage/seed.ts +572 -406
  149. package/packages/core/src/storage/usage.ts +285 -225
  150. package/packages/core/src/storage/user-email.ts +11 -0
  151. package/packages/core/src/swarm/AgentExecutor.ts +1 -1
  152. package/packages/core/src/swarm/EventBridge.ts +1 -1
  153. package/packages/core/src/swarm/index.ts +12 -9
  154. package/packages/core/src/tool-runtime/index.ts +146 -23
  155. package/packages/core/src/tool-runtime/tool-worker.ts +2 -2
  156. package/packages/core/src/tool-runtime/worker-tools.ts +27 -0
  157. package/packages/core/src/{canvas/a2ui-tools.ts → tools/a2ui/index.ts} +17 -8
  158. package/packages/core/src/tools/agents/get-available-models.ts +36 -54
  159. package/packages/core/src/tools/agents/index.ts +784 -292
  160. package/packages/core/src/tools/api/api-request.test.ts +164 -0
  161. package/packages/core/src/tools/api/api-request.ts +174 -0
  162. package/packages/core/src/tools/api/index.ts +16 -0
  163. package/packages/core/src/tools/cli/index.ts +4 -0
  164. package/packages/core/src/tools/core/index.ts +281 -112
  165. package/packages/core/src/tools/cron/index.ts +121 -124
  166. package/packages/core/src/tools/index.ts +63 -78
  167. package/packages/core/src/tools/office/office-escribir-xlsx.ts +3 -1
  168. package/packages/core/src/tools/types.ts +3 -1
  169. package/packages/core/src/tools/web/artifact-inspect.ts +23 -0
  170. package/packages/core/src/tools/web/browser-screenshot.ts +26 -5
  171. package/packages/core/src/tools/web/browser-service.ts +5 -0
  172. package/packages/core/src/tools/web/browser-type.ts +3 -8
  173. package/packages/core/src/tools/web/index.ts +4 -4
  174. package/packages/core/src/voice/index.ts +89 -63
  175. package/packages/core/src/workers/agent.worker.ts +2 -2
  176. package/packages/core/src/workers/workers.test.ts +3 -10
  177. package/scripts/bump-version.ts +248 -0
  178. package/scripts/generate-skill-bundle.ts +108 -0
  179. package/test/agent-loop-terminal-synthesis.test.ts +32 -0
  180. package/test/catalog-agents-stay-enabled.test.ts +117 -0
  181. package/test/causal-events.test.ts +117 -0
  182. package/test/compaction.test.ts +105 -0
  183. package/test/context-compiler.test.ts +269 -0
  184. package/test/curator.test.ts +130 -0
  185. package/test/durable-queue.test.ts +114 -0
  186. package/test/harness-barrel.test.ts +64 -0
  187. package/test/hive-helpers.test.ts +130 -0
  188. package/test/hivedb-search.test.ts +189 -0
  189. package/test/internal-turns.test.ts +166 -0
  190. package/test/job-idempotency.test.ts +68 -0
  191. package/test/job-retry-backoff.test.ts +184 -0
  192. package/test/job-store.test.ts +381 -0
  193. package/test/llm-retry.test.ts +97 -0
  194. package/test/memory-perf.test.ts +774 -0
  195. package/test/minimal-loadout.test.ts +78 -0
  196. package/test/model-catalog.test.ts +105 -0
  197. package/test/preload.ts +12 -0
  198. package/test/reflector.test.ts +320 -0
  199. package/test/retention-cap.test.ts +91 -0
  200. package/test/retired-capabilities-pruned.test.ts +192 -0
  201. package/test/run-store.test.ts +355 -0
  202. package/test/scratchpad.test.ts +74 -0
  203. package/test/secrets-durability.test.ts +119 -0
  204. package/test/seed-model-reseed.test.ts +155 -0
  205. package/test/setup-agent-seed.test.ts +264 -0
  206. package/test/tool-inventory.test.ts +65 -0
  207. package/test/tool-runtime.test.ts +258 -0
  208. package/test/toon.test.ts +429 -0
  209. package/tsconfig.json +2 -0
  210. package/packages/core/src/ace/Curator.ts +0 -158
  211. package/packages/core/src/ace/Reflector.ts +0 -200
  212. package/packages/core/src/ace/index.ts +0 -4
  213. package/packages/core/src/agent/AgentRunner.ts +0 -711
  214. package/packages/core/src/agent/ContextCompiler.ts +0 -567
  215. package/packages/core/src/agent/ContextGuard.ts +0 -91
  216. package/packages/core/src/agent/ConversationStore.ts +0 -254
  217. package/packages/core/src/agent/Hooks.ts +0 -166
  218. package/packages/core/src/agent/StuckLoop.ts +0 -133
  219. package/packages/core/src/agent/providers/LLMClient.ts +0 -149
  220. package/packages/core/src/agent/providers/anthropic.ts +0 -212
  221. package/packages/core/src/agent/providers/openai-compat.ts +0 -231
  222. package/packages/core/src/agent/selectors/PlaybookSelector.ts +0 -121
  223. package/packages/core/src/agent/selectors/SkillSelector.ts +0 -322
  224. package/packages/core/src/agent/selectors/index.ts +0 -6
  225. package/packages/core/src/auth/auth.ts +0 -121
  226. package/packages/core/src/auth/index.ts +0 -1
  227. package/packages/core/src/canvas/CanvasManager.ts +0 -390
  228. package/packages/core/src/canvas/canvas-tools.ts +0 -448
  229. package/packages/core/src/harness/collections.ts +0 -98
  230. package/packages/core/src/harness/goal-verifier.ts +0 -141
  231. package/packages/core/src/harness/harness.test.ts +0 -236
  232. package/packages/core/src/harness/reconcile.ts +0 -149
  233. package/packages/core/src/mcp/MCPToolAdapter.ts +0 -176
  234. package/packages/core/src/multimodal/VisionService.ts +0 -293
  235. package/packages/core/src/scheduler/dag/AgentExecutor.ts +0 -53
  236. package/packages/core/src/scheduler/dag/DAGScheduler.ts +0 -250
  237. package/packages/core/src/scheduler/dag/EventBridge.ts +0 -122
  238. package/packages/core/src/scheduler/dag/TaskGraph.ts +0 -192
  239. package/packages/core/src/scheduler/dag/TaskNode.ts +0 -97
  240. package/packages/core/src/scheduler/dag/TaskResult.ts +0 -22
  241. package/packages/core/src/scheduler/dag/errors.ts +0 -37
  242. package/packages/core/src/scheduler/dag/index.ts +0 -26
  243. package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +0 -97
  244. package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +0 -21
  245. package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +0 -46
  246. package/packages/core/src/storage/HiveDBStorage.ts +0 -64
  247. package/packages/core/src/storage/SQLiteStorage.ts +0 -414
  248. package/packages/core/src/storage/hiveSeed.ts +0 -308
  249. package/packages/core/src/storage/hiveStorage.test.ts +0 -38
  250. package/packages/core/src/storage/schema.ts +0 -689
  251. package/packages/core/src/storage/storage.test.ts +0 -37
  252. package/packages/core/src/swarm/AgentBus.ts +0 -460
  253. package/packages/core/src/swarm/EventBus.ts +0 -169
  254. package/packages/core/src/swarm/WorkerPool.ts +0 -236
  255. package/packages/core/src/tools/bridge-events.ts +0 -26
  256. package/packages/core/src/tools/canvas/index.ts +0 -375
  257. package/packages/core/src/tools/codebridge/index.ts +0 -342
  258. package/packages/core/src/tools/meeting/index.ts +0 -353
  259. package/packages/core/src/tools/projects/index.ts +0 -37
  260. package/packages/core/src/tools/projects/project-create.ts +0 -94
  261. package/packages/core/src/tools/projects/project-done.ts +0 -66
  262. package/packages/core/src/tools/projects/project-fail.ts +0 -66
  263. package/packages/core/src/tools/projects/project-list.ts +0 -96
  264. package/packages/core/src/tools/projects/project-update.ts +0 -72
  265. package/packages/core/src/tools/projects/task-create.ts +0 -68
  266. package/packages/core/src/tools/projects/task-evaluate.ts +0 -93
  267. package/packages/core/src/tools/projects/task-update.ts +0 -93
  268. package/packages/core/src/tools/voice/index.ts +0 -104
  269. package/packages/core/src/tools/web/api-request.test.ts +0 -170
  270. package/packages/core/src/tools/web/api-request.ts +0 -239
  271. package/test/setup-db.ts +0 -216
  272. /package/packages/core/src/agent/{NativeTools.ts → native-tools.ts} +0 -0
@@ -0,0 +1,266 @@
1
+ /**
2
+ * LLM client — direct official SDKs, no abstraction layers.
3
+ *
4
+ * gemini / google → native Gemini REST API (v1beta, ?key=)
5
+ * anthropic → @anthropic-ai/sdk
6
+ * ollama → ollama npm package
7
+ * openai → openai npm package
8
+ * groq / mistral / openrouter / deepseek / kimi / nvidia / qwen
9
+ * → openai npm package (OpenAI-compatible endpoint, per-provider adapter)
10
+ *
11
+ * Public interface (LLMMessage, callLLM, resolveProviderConfig) is stable.
12
+ */
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"
34
+
35
+ const log = logger.child("llm-client")
36
+
37
+ // ─── Canonical types ───────────────────────────────────────────────────────────
38
+
39
+ export interface LLMToolCall {
40
+ id: string
41
+ type: "function"
42
+ function: { name: string; arguments: string }
43
+ /** Gemini 3.x thought signature — must be round-tripped for tool-calling. */
44
+ thought_signature?: string
45
+ }
46
+
47
+ export type ContentPart =
48
+ | { type: "text"; text: string }
49
+ | { type: "image_url"; image_url: { url: string } }
50
+ | { type: "image_base64"; base64: string; mimeType: string }
51
+ | { type: "document"; base64: string; mimeType: string; fileName?: string }
52
+
53
+ /** Raw Anthropic extended-thinking content block, round-tripped verbatim (signature required by the API when tool_use follows). */
54
+ export interface ThinkingBlock {
55
+ type: "thinking" | "redacted_thinking"
56
+ thinking?: string
57
+ signature?: string
58
+ data?: string
59
+ }
60
+
61
+ export interface LLMMessage {
62
+ role: "system" | "user" | "assistant" | "tool"
63
+ content: string | ContentPart[]
64
+ tool_calls?: LLMToolCall[]
65
+ tool_call_id?: string
66
+ name?: string
67
+ /** Kimi K2 thinking mode — must be round-tripped when tool calls are present. */
68
+ reasoning_content?: string
69
+ /** Anthropic extended thinking — must be round-tripped verbatim when tool calls are present. */
70
+ thinking_blocks?: ThinkingBlock[]
71
+ }
72
+
73
+ export interface LLMToolDef {
74
+ type: "function"
75
+ function: {
76
+ name: string
77
+ description: string
78
+ parameters: Record<string, unknown>
79
+ }
80
+ }
81
+
82
+ export interface LLMCallOptions {
83
+ provider: string
84
+ model: string
85
+ apiKey: string
86
+ baseUrl?: string
87
+ numCtx?: number
88
+ contextWindow?: number
89
+ messages: LLMMessage[]
90
+ tools?: LLMToolDef[]
91
+ temperature?: number
92
+ maxTokens?: number
93
+ numGpu?: number
94
+ onToken?: (token: string) => void
95
+ /** Live reasoning/thinking tokens as they stream, for display only (never sent back to the LLM). */
96
+ onReasoningToken?: (token: string) => void
97
+ signal?: AbortSignal
98
+ /** Enable extended thinking for supported models (Anthropic Claude 3.7+). */
99
+ thinking?: { enabled: boolean; budget_tokens?: number }
100
+ }
101
+
102
+ export interface LLMResponse {
103
+ content: string
104
+ tool_calls?: LLMToolCall[]
105
+ stop_reason: "stop" | "tool_calls" | "max_tokens" | "error"
106
+ usage?: { input_tokens: number; output_tokens: number; thinking_tokens?: number }
107
+ /** Kimi K2 / DeepSeek thinking mode — must be round-tripped in assistant messages. */
108
+ reasoning_content?: string
109
+ /** Anthropic extended thinking content (not sent to LLM, for display only). */
110
+ thinking_content?: string
111
+ /** Anthropic extended thinking raw blocks — must be round-tripped verbatim when tool calls follow. */
112
+ thinking_blocks?: ThinkingBlock[]
113
+ /**
114
+ * Set only when stop_reason === "error". `content` carries a human-readable
115
+ * version of the same failure for display, but it is NOT model output —
116
+ * callers must check this field before persisting `content` anywhere durable
117
+ * (conversation history, summaries, task results).
118
+ */
119
+ error?: {
120
+ message: string
121
+ status?: number
122
+ /** The model id no longer resolves at the provider (HTTP 404/410) — the config needs to change, retrying won't help. */
123
+ modelUnavailable?: boolean
124
+ }
125
+ }
126
+
127
+ // ─── Provider factory ─────────────────────────────────────────────────────────
128
+
129
+ function getProvider(provider: string): LLMProvider {
130
+ switch (provider) {
131
+ case "gemini":
132
+ case "google": return new GeminiProvider()
133
+ case "anthropic": return new AnthropicProvider()
134
+ case "ollama": return new OllamaProvider()
135
+ case "openai": return new OpenAIProvider()
136
+ case "groq": return new GroqProvider()
137
+ case "mistral": return new MistralProvider()
138
+ case "openrouter": return new OpenRouterProvider()
139
+ case "deepseek": return new DeepSeekProvider()
140
+ case "kimi": return new KimiProvider()
141
+ case "nvidia": return new NvidiaProvider()
142
+ case "qwen": return new QwenProvider()
143
+ case "minimax": return new MiniMaxProvider()
144
+ case "opencode-go": return new OpenCodeGoProvider()
145
+ case "hiveagents": return new HiveAgentsProvider()
146
+ case "z-ai": return new ZaiProvider()
147
+ case "modelscope": return new ModelScopeProvider()
148
+ default:
149
+ log.warn(`[llm-client] Unknown provider "${provider}" — falling back to OpenAI-compatible endpoint`)
150
+ return new OpenAIProvider()
151
+ }
152
+ }
153
+
154
+ // ─── Public API ────────────────────────────────────────────────────────────────
155
+
156
+ /**
157
+ * Call any LLM provider. Returns a canonical LLMResponse regardless of provider.
158
+ *
159
+ * Retries transient failures (429/5xx/timeout/network) with exponential
160
+ * backoff + jitter per `config.retry`, honoring `Retry-After` when the
161
+ * provider sends one. An aborted signal never retries. Note: this retries
162
+ * the whole call, so a failure that happens mid-stream (after onToken has
163
+ * already fired) can produce a duplicated partial response — acceptable
164
+ * because provider failures overwhelmingly happen before the stream starts
165
+ * (auth/rate-limit/connection errors).
166
+ */
167
+ export async function callLLM(options: LLMCallOptions): Promise<LLMResponse> {
168
+ const retryCfg = loadConfig().retry
169
+ const policy: RetryPolicy = {
170
+ maxAttempts: retryCfg?.maxAttempts ?? 3,
171
+ initialDelayMs: retryCfg?.initialDelayMs ?? 1000,
172
+ backoffMultiplier: retryCfg?.backoffMultiplier ?? 2,
173
+ maxDelayMs: retryCfg?.maxDelayMs ?? 30000,
174
+ }
175
+ try {
176
+ return await withRetry(
177
+ () => getProvider(options.provider).call(options),
178
+ policy,
179
+ (err) => !options.signal?.aborted && isRetryableError(err)
180
+ )
181
+ } catch (err) {
182
+ const cleanModel = options.model.replace(new RegExp(`^${options.provider}\\/`), "")
183
+ const status = extractErrorStatus(err)
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
190
+ log.error(`[llm-client] Error calling ${options.provider}/${cleanModel}: ${msg}`, err)
191
+ return {
192
+ content: `[LLM Error] ${msg}`,
193
+ stop_reason: "error",
194
+ error: { message: msg, status, modelUnavailable },
195
+ }
196
+ }
197
+ }
198
+
199
+ /** Provider SDKs disagree on where the HTTP status lands; check every shape we've seen. */
200
+ function extractErrorStatus(err: unknown): number | undefined {
201
+ const e = err as { status?: number; statusCode?: number; response?: { status?: number } }
202
+ return e?.status ?? e?.statusCode ?? e?.response?.status
203
+ }
204
+
205
+ /**
206
+ * Default provider/model resolved from the DB, used when an agent has none configured:
207
+ * 1. the coordinator agent's config, 2. the first active LLM model of an active provider.
208
+ * Returns null when the DB has no usable LLM (e.g. fresh install before setup).
209
+ */
210
+ 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")
215
+
216
+ const coordinators = await agentsCol.findBy("role", "coordinator")
217
+ const coordinator = coordinators[0]
218
+ const coordinatorProvider = fromIndexable(coordinator?.doc.provider_id ?? null)
219
+ const coordinatorModel = fromIndexable(coordinator?.doc.model_id ?? null)
220
+ if (coordinatorProvider && coordinatorModel) {
221
+ return { provider: coordinatorProvider, model: coordinatorModel }
222
+ }
223
+
224
+ const activeModels = (await modelsCol.findBy("model_type", "llm")).filter(m => m.doc.active)
225
+ for (const m of activeModels) {
226
+ const provider = await providersCol.get(m.doc.provider_id)
227
+ if (provider?.doc.active) {
228
+ return { provider: m.doc.provider_id, model: m.doc.id }
229
+ }
230
+ }
231
+ return null
232
+ }
233
+
234
+ /**
235
+ * Resolve provider config from DB (decrypts API key).
236
+ */
237
+ export async function resolveProviderConfig(
238
+ providerId: string,
239
+ modelId: string
240
+ ): 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")
245
+
246
+ const providerEntry = await providersCol.get(providerId)
247
+ const providerRow = (providerEntry?.doc.enabled && providerEntry?.doc.active) ? providerEntry.doc : undefined
248
+
249
+ // Load model's context window for token budget management
250
+ const modelEntry = await modelsCol.get(modelId)
251
+
252
+ let apiKey = await loadProviderApiKey(providerId)
253
+ if (!apiKey) {
254
+ apiKey = process.env[`${providerId.toUpperCase()}_API_KEY`] || ""
255
+ }
256
+
257
+ return {
258
+ provider: providerId,
259
+ model: modelId,
260
+ apiKey,
261
+ baseUrl: providerRow?.base_url || undefined,
262
+ numCtx: providerRow?.num_ctx ?? undefined,
263
+ numGpu: providerRow?.num_gpu ?? undefined,
264
+ contextWindow: modelEntry?.doc.context_window ?? undefined,
265
+ }
266
+ }
@@ -0,0 +1,264 @@
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"
5
+
6
+ const log = logger.child("llm-client")
7
+
8
+ // Models that accept the explicit `thinking: { type: "enabled" }` parameter, per
9
+ // the "Extended thinking" row of platform.claude.com/docs/en/about-claude/models/overview.
10
+ // This is an allowlist on purpose: from Opus 4.7 onward Anthropic replaced extended
11
+ // thinking with *adaptive* thinking, which is server-side and always on — those models
12
+ // reject the parameter, so being absent here is the correct behavior, not an omission.
13
+ const THINKING_CAPABLE_MODELS = new Set([
14
+ "claude-3-7-sonnet-20250219",
15
+ "claude-sonnet-4-5",
16
+ "claude-sonnet-4-5-20250929",
17
+ "claude-sonnet-4-6", // deprecated but still accepted
18
+ "claude-opus-4-5",
19
+ "claude-opus-4-5-20251101",
20
+ "claude-opus-4-6", // deprecated but still accepted
21
+ "claude-haiku-4-5",
22
+ "claude-haiku-4-5-20251001",
23
+ ])
24
+
25
+ function supportsThinking(model: string): boolean {
26
+ return THINKING_CAPABLE_MODELS.has(model)
27
+ }
28
+
29
+ /** Anthropic's documented shape for context-length errors: 400 + invalid_request_error + a "too long"/"maximum" message. */
30
+ function isAnthropicContextOverflowError(err: any): boolean {
31
+ const status = err?.status
32
+ if (status !== 400) return false
33
+ const type = err?.error?.type ?? err?.type
34
+ const msg = (err?.error?.message ?? err?.message ?? "").toLowerCase()
35
+ if (type && type !== "invalid_request_error") return false
36
+ return msg.includes("too long") || msg.includes("maximum context") || msg.includes("prompt is too long") || msg.includes("context length")
37
+ }
38
+
39
+ /** Keeps the first `thinking`/`tool_use` round-trip block intact per message, keeps only the last ~33% of the conversation, and shrinks max_tokens. */
40
+ function compactAnthropicBody(body: any): void {
41
+ const keepRatio = Math.max(1, Math.floor(body.messages.length / 3))
42
+ body.messages = body.messages.slice(-keepRatio)
43
+ if (body.max_tokens) body.max_tokens = Math.min(body.max_tokens, 4096)
44
+ }
45
+
46
+ export class AnthropicProvider implements LLMProvider {
47
+ private _convertContentPart(part: ContentPart): any {
48
+ switch (part.type) {
49
+ case "text":
50
+ return { type: "text", text: part.text }
51
+ case "image_url": {
52
+ const url = part.image_url.url
53
+ if (url.startsWith("data:")) {
54
+ const match = url.match(/^data:([^;]+);base64,(.+)$/)
55
+ if (match) return { type: "image", source: { type: "base64", media_type: match[1], data: match[2] } }
56
+ }
57
+ return { type: "image", source: { type: "url", url } }
58
+ }
59
+ case "image_base64":
60
+ return { type: "image", source: { type: "base64", media_type: part.mimeType, data: part.base64 } }
61
+ case "document":
62
+ return { type: "document", source: { type: "base64", media_type: part.mimeType, data: part.base64 } }
63
+ default:
64
+ return { type: "text", text: JSON.stringify(part) }
65
+ }
66
+ }
67
+
68
+ private _convertUserContent(msg: LLMMessage): any[] {
69
+ if (Array.isArray(msg.content)) {
70
+ return msg.content.map(p => this._convertContentPart(p))
71
+ }
72
+ return [{ type: "text", text: msg.content }]
73
+ }
74
+
75
+ async call(options: LLMCallOptions): Promise<LLMResponse> {
76
+ const Anthropic = await import("@anthropic-ai/sdk")
77
+ const client = new Anthropic.default({ apiKey: options.apiKey })
78
+
79
+ // Anthropic requires tool names to match ^[a-zA-Z0-9_-]{1,128}$
80
+ // Native Hive tools use dots (e.g. cron.create) which violate this.
81
+ const toolNameMap = new Map<string, string>() // wireName -> originalName
82
+
83
+ const systemText = options.messages
84
+ .filter((m) => m.role === "system")
85
+ .map((m) => m.content)
86
+ .join("\n\n")
87
+
88
+ const anthropicMessages: any[] = []
89
+
90
+ for (const msg of options.messages) {
91
+ if (msg.role === "system") continue
92
+
93
+ if (msg.role === "tool") {
94
+ const block = { type: "tool_result", tool_use_id: msg.tool_call_id, content: msg.content }
95
+ const last = anthropicMessages[anthropicMessages.length - 1]
96
+ if (last?.role === "user" && Array.isArray(last.content)) {
97
+ last.content.push(block)
98
+ } else {
99
+ anthropicMessages.push({ role: "user", content: [block] })
100
+ }
101
+ continue
102
+ }
103
+
104
+ if (msg.role === "assistant" && msg.tool_calls?.length) {
105
+ const content: any[] = []
106
+ // Extended thinking blocks must be replayed verbatim (with signature) before
107
+ // any tool_use block, or the API rejects the request with a 400.
108
+ if (msg.thinking_blocks?.length) {
109
+ for (const tb of msg.thinking_blocks) {
110
+ content.push(
111
+ tb.type === "thinking"
112
+ ? { type: "thinking", thinking: tb.thinking, signature: tb.signature }
113
+ : { type: "redacted_thinking", data: tb.data }
114
+ )
115
+ }
116
+ }
117
+ if (msg.content) content.push({ type: "text", text: msg.content })
118
+ for (const tc of msg.tool_calls) {
119
+ let input: Record<string, unknown>
120
+ try { input = JSON.parse(tc.function.arguments || "{}") } catch { input = {} }
121
+ const wireName = normalizeToolName(tc.function.name, "_")
122
+ if (wireName !== tc.function.name) toolNameMap.set(wireName, tc.function.name)
123
+ content.push({ type: "tool_use", id: tc.id, name: wireName, input })
124
+ }
125
+ anthropicMessages.push({ role: "assistant", content })
126
+ continue
127
+ }
128
+
129
+ anthropicMessages.push({ role: msg.role, content: Array.isArray(msg.content) ? this._convertUserContent(msg) : msg.content })
130
+ }
131
+
132
+ const tools: any[] = (options.tools ?? []).map((t) => {
133
+ const originalName = t.function.name
134
+ const wireName = normalizeToolName(originalName, "_")
135
+ if (wireName !== originalName) toolNameMap.set(wireName, originalName)
136
+ return {
137
+ name: wireName,
138
+ description: t.function.description,
139
+ input_schema: ensureArrayItems(t.function.parameters),
140
+ }
141
+ })
142
+
143
+ const body: any = {
144
+ model: options.model,
145
+ max_tokens: resolveMaxTokens(options.maxTokens, options.contextWindow) ?? 16384,
146
+ messages: anthropicMessages,
147
+ }
148
+ if (systemText) body.system = systemText
149
+ if (tools.length) body.tools = tools
150
+
151
+ // Extended thinking — only for supported models
152
+ const thinkingEnabled = options.thinking?.enabled && supportsThinking(options.model)
153
+ if (thinkingEnabled) {
154
+ body.thinking = { type: "enabled", budget_tokens: options.thinking?.budget_tokens ?? 10000 }
155
+ }
156
+
157
+ log.info(
158
+ `[llm-client] anthropic/${options.model} — ${anthropicMessages.length} msgs, ${tools.length} tools` +
159
+ (thinkingEnabled ? ` thinking=${body.thinking.budget_tokens}tok` : "")
160
+ )
161
+
162
+ // Streaming via messages.stream()
163
+ const useStream = true // Always stream for better UX
164
+ if (useStream) {
165
+ for (let attempt = 0; attempt < 2; attempt++) {
166
+ let content = ""
167
+ let thinking_content = ""
168
+ const tool_calls: LLMToolCall[] = []
169
+
170
+ try {
171
+ const stream = client.messages.stream(body, options.signal ? { signal: options.signal } : undefined)
172
+
173
+ // Track partial tool inputs by index
174
+ const partialInputs: Record<number, string> = {}
175
+ const toolMeta: Record<number, { id: string; name: string }> = {}
176
+ // Track thinking/redacted_thinking blocks by index — needed both for live
177
+ // display (onReasoningToken) and to round-trip the signed block verbatim
178
+ // in the next turn if this response also contains tool calls.
179
+ const thinkingBlockState: Record<number, { type: "thinking" | "redacted_thinking"; thinking: string; signature: string; data: string }> = {}
180
+
181
+ for await (const event of stream) {
182
+ if (event.type === "content_block_start") {
183
+ if (event.content_block.type === "tool_use") {
184
+ const wireName = event.content_block.name
185
+ const originalName = toolNameMap.get(wireName) ?? wireName
186
+ toolMeta[event.index] = { id: event.content_block.id, name: originalName }
187
+ partialInputs[event.index] = ""
188
+ } else if (event.content_block.type === "thinking") {
189
+ thinkingBlockState[event.index] = { type: "thinking", thinking: "", signature: "", data: "" }
190
+ } else if ((event.content_block as any).type === "redacted_thinking") {
191
+ thinkingBlockState[event.index] = { type: "redacted_thinking", thinking: "", signature: "", data: (event.content_block as any).data ?? "" }
192
+ }
193
+ } else if (event.type === "content_block_delta") {
194
+ if (event.delta.type === "text_delta") {
195
+ content += event.delta.text
196
+ if (options.onToken) options.onToken(event.delta.text)
197
+ } else if (event.delta.type === "thinking_delta") {
198
+ thinking_content += event.delta.thinking
199
+ options.onReasoningToken?.(event.delta.thinking)
200
+ if (thinkingBlockState[event.index]) thinkingBlockState[event.index].thinking += event.delta.thinking
201
+ } else if ((event.delta as any).type === "signature_delta") {
202
+ if (thinkingBlockState[event.index]) thinkingBlockState[event.index].signature += (event.delta as any).signature
203
+ } else if (event.delta.type === "input_json_delta") {
204
+ if (partialInputs[event.index] !== undefined) {
205
+ partialInputs[event.index] += event.delta.partial_json
206
+ }
207
+ }
208
+ }
209
+ }
210
+
211
+ const thinking_blocks: ThinkingBlock[] = Object.keys(thinkingBlockState)
212
+ .map(Number)
213
+ .sort((a, b) => a - b)
214
+ .map((idx) => {
215
+ const b = thinkingBlockState[idx]
216
+ return b.type === "thinking"
217
+ ? { type: "thinking" as const, thinking: b.thinking, signature: b.signature }
218
+ : { type: "redacted_thinking" as const, data: b.data }
219
+ })
220
+
221
+ const finalMsg = await stream.finalMessage()
222
+
223
+ // Build tool_calls from accumulated partial inputs
224
+ for (const [idx, meta] of Object.entries(toolMeta)) {
225
+ const args = partialInputs[Number(idx)] ?? "{}"
226
+ tool_calls.push({
227
+ id: meta.id,
228
+ type: "function",
229
+ function: { name: meta.name, arguments: args },
230
+ })
231
+ }
232
+
233
+ const usage = finalMsg.usage
234
+ return {
235
+ content,
236
+ thinking_content: thinking_content || undefined,
237
+ thinking_blocks: thinking_blocks.length ? thinking_blocks : undefined,
238
+ tool_calls: tool_calls.length ? tool_calls : undefined,
239
+ stop_reason:
240
+ finalMsg.stop_reason === "tool_use" ? "tool_calls"
241
+ : finalMsg.stop_reason === "max_tokens" ? "max_tokens"
242
+ : "stop",
243
+ usage: {
244
+ input_tokens: usage.input_tokens,
245
+ output_tokens: usage.output_tokens,
246
+ thinking_tokens: (usage as any).thinking_tokens ?? 0,
247
+ },
248
+ }
249
+ } catch (err: any) {
250
+ if (attempt === 0 && isAnthropicContextOverflowError(err)) {
251
+ log.warn(`[llm-client] anthropic: context overflow — compacting messages and retrying`)
252
+ const originalCount = body.messages.length
253
+ compactAnthropicBody(body)
254
+ log.info(`[llm-client] anthropic: compacted ${originalCount} msgs → ${body.messages.length} msgs, max_tokens=${body.max_tokens}`)
255
+ continue
256
+ }
257
+ throw err
258
+ }
259
+ }
260
+ }
261
+
262
+ throw new Error("unreachable")
263
+ }
264
+ }
@@ -0,0 +1,8 @@
1
+ import { OpenAICompatBase } from "./openai-compat-base"
2
+
3
+ export class DeepSeekProvider extends OpenAICompatBase {
4
+ constructor() { super("deepseek") }
5
+
6
+ /** DeepSeek reasoner models return reasoning_content that must be round-tripped. */
7
+ protected needsReasoningRoundtrip(): boolean { return true }
8
+ }