@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
@@ -1,10 +1,20 @@
1
- import { logger } from "../../utils/logger.ts"
2
- import { sanitizeMessages } from "./interface"
1
+ import { logger } from "../../utils/logger"
2
+ import { sanitizeMessages, resolveMaxTokens, ensureArrayItems } from "./interface"
3
3
  import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface"
4
- import type { ContentPart, LLMMessage } from "./LLMClient.ts"
4
+ import type { ContentPart, LLMMessage } from "../llm-client"
5
5
 
6
6
  const log = logger.child("llm-client")
7
7
 
8
+ // Local models known to support Ollama's `think` request flag (returns
9
+ // message.thinking separate from message.content). Conservative allowlist —
10
+ // unrecognized models don't get `think:true` since some may error on it
11
+ // rather than silently ignore it.
12
+ const THINKING_CAPABLE_MODEL_PATTERNS = [/deepseek-r1/i, /^qwq/i, /qwen3.*think/i]
13
+
14
+ function supportsThinking(model: string): boolean {
15
+ return THINKING_CAPABLE_MODEL_PATTERNS.some((re) => re.test(model))
16
+ }
17
+
8
18
  export class OllamaProvider implements LLMProvider {
9
19
  private _convertMessage(msg: LLMMessage): any {
10
20
  if (typeof msg.content === "string") {
@@ -48,6 +58,9 @@ export class OllamaProvider implements LLMProvider {
48
58
  host,
49
59
  ...(Object.keys(headers).length ? { headers } : {}),
50
60
  })
61
+ // Ollama's client aborts all of its own in-flight streams — safe 1:1 with
62
+ // options.signal since a fresh client is created per call.
63
+ if (options.signal) options.signal.addEventListener("abort", () => client.abort(), { once: true })
51
64
 
52
65
  const messages = sanitizeMessages(options.messages).map((m): any => {
53
66
  if (m.role === "assistant" && m.tool_calls?.length) {
@@ -73,7 +86,7 @@ export class OllamaProvider implements LLMProvider {
73
86
  function: {
74
87
  name: t.function.name,
75
88
  description: t.function.description,
76
- parameters: t.function.parameters,
89
+ parameters: ensureArrayItems(t.function.parameters),
77
90
  },
78
91
  }))
79
92
 
@@ -85,6 +98,8 @@ export class OllamaProvider implements LLMProvider {
85
98
  }
86
99
  if (options.numGpu !== undefined) runtimeOptions.num_gpu = options.numGpu
87
100
  if (options.temperature !== undefined) runtimeOptions.temperature = options.temperature
101
+ const maxTokens = resolveMaxTokens(options.maxTokens, options.contextWindow)
102
+ if (maxTokens) runtimeOptions.num_predict = maxTokens
88
103
 
89
104
  try {
90
105
 
@@ -94,20 +109,30 @@ export class OllamaProvider implements LLMProvider {
94
109
  ` num_ctx=${runtimeOptions.num_ctx}`
95
110
  )
96
111
 
112
+ const thinkEnabled = options.thinking?.enabled && supportsThinking(modelName)
113
+
97
114
  const stream = await client.chat({
98
115
  model: modelName,
99
116
  messages,
100
117
  tools: tools?.length ? tools : undefined,
101
118
  options: Object.keys(runtimeOptions).length ? runtimeOptions : undefined,
102
119
  stream: true,
103
- })
120
+ ...(thinkEnabled ? { think: true } : {}),
121
+ } as any)
104
122
 
105
123
  let content = ""
124
+ let reasoning_content = ""
106
125
  let promptEvalCount = 0
107
126
  let evalCount = 0
108
127
  const tool_calls: LLMToolCall[] = []
109
128
 
110
129
  for await (const part of stream) {
130
+ const thinkingDelta = (part.message as any)?.thinking ?? ""
131
+ if (thinkingDelta) {
132
+ reasoning_content += thinkingDelta
133
+ options.onReasoningToken?.(thinkingDelta)
134
+ }
135
+
111
136
  const delta = part.message?.content ?? ""
112
137
  if (delta) {
113
138
  content += delta
@@ -133,6 +158,7 @@ export class OllamaProvider implements LLMProvider {
133
158
 
134
159
  return {
135
160
  content,
161
+ reasoning_content: reasoning_content || undefined,
136
162
  tool_calls: tool_calls.length ? tool_calls : undefined,
137
163
  stop_reason: tool_calls.length > 0 ? "tool_calls" : "stop",
138
164
  usage:
@@ -0,0 +1,418 @@
1
+ import { logger } from "../../utils/logger"
2
+ import {
3
+ sanitizeMessages, requiresTemperature1, OPENAI_COMPAT_BASE_URLS,
4
+ getProviderProfile, modelSupportsTools, normalizeToolName, normalizeToolSchema,
5
+ resolveMaxTokens,
6
+ } from "./interface"
7
+ import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface"
8
+ import type { ContentPart, LLMMessage } from "../llm-client"
9
+
10
+ const log = logger.child("llm-client")
11
+
12
+ /** Matches both generic "context length exceeded" phrasing and llama.cpp's exceed_context_size_error shape. */
13
+ function isContextOverflowError(err: any, errMsg: string): boolean {
14
+ const status = err?.status ?? err?.response?.status
15
+ if (status !== 400) return false
16
+ if (err?.error?.type === "exceed_context_size_error" || err?.type === "exceed_context_size_error") return true
17
+ return errMsg.includes("context length") || errMsg.includes("input_tokens")
18
+ || errMsg.includes("maximum input length") || errMsg.includes("context size")
19
+ }
20
+
21
+ /** llama.cpp-style errors report the server's real context size in n_ctx — use it when present. */
22
+ function extractRealContextSize(err: any): number | undefined {
23
+ return err?.error?.n_ctx ?? err?.n_ctx
24
+ }
25
+
26
+ /** Keeps system + last ~33% of messages, and shrinks max_tokens — using the real n_ctx when the error provided one. */
27
+ function compactBodyForContextOverflow(body: any, err: any): void {
28
+ const kept: any[] = []
29
+ let systemMsg: any = null
30
+ for (const m of body.messages) {
31
+ if (m.role === "system") { systemMsg = m; continue }
32
+ kept.push(m)
33
+ }
34
+ const keepRatio = Math.max(1, Math.floor(kept.length / 3))
35
+ const trimmed = kept.slice(-keepRatio)
36
+ body.messages = systemMsg ? [systemMsg, ...trimmed] : trimmed
37
+
38
+ const realCtx = extractRealContextSize(err)
39
+ if (realCtx) {
40
+ body.max_tokens = Math.min(body.max_tokens ?? realCtx, Math.floor(realCtx * 0.25))
41
+ } else if (body.max_tokens) {
42
+ body.max_tokens = Math.min(body.max_tokens, 4096)
43
+ }
44
+ }
45
+
46
+ export abstract class OpenAICompatBase implements LLMProvider {
47
+ constructor(protected readonly providerName: string) {}
48
+
49
+ /** Override to true when the provider requires reasoning_content to be round-tripped. */
50
+ protected needsReasoningRoundtrip(): boolean { return false }
51
+
52
+ /** Override to true for providers running on localhost. */
53
+ protected isLocalProvider(): boolean { return false }
54
+
55
+ /** Override to customize the OpenAI client (e.g. strip unwanted headers, add custom fetch). */
56
+ protected async resolveOpenAIClient(apiKey: string, baseURL: string | undefined): Promise<any> {
57
+ const { default: OpenAI } = await import("openai")
58
+ return new OpenAI({ apiKey, baseURL })
59
+ }
60
+
61
+ /** Hook called before each request. Override for e.g. auto-starting a local server. */
62
+ protected async beforeCall(_options: LLMCallOptions): Promise<void> {}
63
+
64
+ /**
65
+ * Hook called after tools are prepared when sendTools is true.
66
+ * Override to inject tool descriptions into the system prompt
67
+ * (for local models whose chat templates don't support native tool calling).
68
+ */
69
+ protected injectToolsIntoPrompt(_body: any, _preparedTools: any[]): void {}
70
+
71
+ /** Override to add provider-specific fields to the request body (e.g. extra_body for llama.cpp chat_template_kwargs). */
72
+ protected modifyRequestBody(body: any, _options: LLMCallOptions): any { return body }
73
+
74
+ private _convertContentPart(part: ContentPart): any {
75
+ switch (part.type) {
76
+ case "text":
77
+ return { type: "text", text: part.text }
78
+ case "image_url":
79
+ return { type: "image_url", image_url: { url: part.image_url.url } }
80
+ case "image_base64":
81
+ return { type: "image_url", image_url: { url: `data:${part.mimeType};base64,${part.base64}` } }
82
+ case "document":
83
+ log.warn(`[llm-client] ${this.providerName}: document content parts are not supported — content will be omitted`)
84
+ return { type: "text", text: `[Document: ${(part as any).fileName || "file"}] (content not supported for this provider)` }
85
+ default:
86
+ return { type: "text", text: JSON.stringify(part) }
87
+ }
88
+ }
89
+
90
+ private _convertMessage(msg: LLMMessage): any {
91
+ if (Array.isArray(msg.content)) {
92
+ return { ...msg, content: msg.content.map(p => this._convertContentPart(p)) }
93
+ }
94
+ return msg
95
+ }
96
+
97
+ async call(options: LLMCallOptions): Promise<LLMResponse> {
98
+ const baseURL = options.baseUrl?.trim() || OPENAI_COMPAT_BASE_URLS[this.providerName] || undefined
99
+ const isLocal = this.isLocalProvider()
100
+
101
+ await this.beforeCall(options)
102
+
103
+ const apiKey = options.apiKey || (isLocal ? "ollama" : undefined)
104
+ if (!apiKey) {
105
+ throw new Error(`API key missing for provider: ${this.providerName}. Configure it in Settings → Providers.`)
106
+ }
107
+
108
+ const client = await this.resolveOpenAIClient(apiKey, baseURL)
109
+
110
+ const sanitized = sanitizeMessages(options.messages)
111
+ const rawMessages = this.needsReasoningRoundtrip()
112
+ ? sanitized
113
+ : sanitized.map(({ reasoning_content: _rc, ...rest }) => rest as typeof sanitized[number])
114
+ const messagesForProvider = rawMessages.map(m => this._convertMessage(m))
115
+
116
+ const providerPrefix = new RegExp(`^${this.providerName}\\/`, "i")
117
+ const body: any = {
118
+ model: options.model.replace(providerPrefix, ""),
119
+ messages: messagesForProvider,
120
+ temperature: requiresTemperature1(this.providerName, options.model) ? 1 : (options.temperature ?? 0.7),
121
+ }
122
+ const maxTokens = resolveMaxTokens(options.maxTokens, options.contextWindow)
123
+ if (maxTokens) body.max_tokens = maxTokens
124
+ if (options.numCtx && isLocal) body.num_ctx = options.numCtx
125
+
126
+ const profile = getProviderProfile(this.providerName)
127
+ const sendTools = modelSupportsTools(this.providerName, options.model) && !!(options.tools?.length)
128
+
129
+ const toolNameMap = new Map<string, string>()
130
+
131
+ if (sendTools) {
132
+ const preparedTools = options.tools!.map((t) => {
133
+ const originalName = t.function.name
134
+ const wireName = profile.normalizeToolNames
135
+ ? normalizeToolName(originalName, profile.toolNameReplacement)
136
+ : originalName
137
+ if (wireName !== originalName) toolNameMap.set(wireName, originalName)
138
+ return {
139
+ ...t,
140
+ function: {
141
+ ...t.function,
142
+ name: wireName,
143
+ parameters: normalizeToolSchema(t.function.parameters as Record<string, unknown>, profile),
144
+ },
145
+ }
146
+ })
147
+ body.tools = preparedTools
148
+ body.tool_choice = profile.toolChoiceAuto
149
+ if (profile.disableParallelToolCalls) body.parallel_tool_calls = false
150
+
151
+ this.injectToolsIntoPrompt(body, preparedTools)
152
+ }
153
+
154
+ log.info(`[llm-client] ${this.providerName}/${body.model} — ${options.messages.length} msgs, ${options.tools?.length ?? 0} tools${sendTools ? "" : " (tools suppressed)"}`)
155
+
156
+ if (options.onToken) {
157
+ return this._streamCall(client, body, options, toolNameMap, sendTools, profile)
158
+ }
159
+
160
+ let response
161
+ try {
162
+ response = await client.chat.completions.create(this.modifyRequestBody(body, options), { signal: options.signal })
163
+ } catch (err: any) {
164
+ const status = err?.status ?? err?.response?.status
165
+ const errMsg = (err?.error?.message ?? err?.message ?? "").toLowerCase()
166
+
167
+ // Retry 1: context overflow — compact messages and retry. Checked BEFORE the
168
+ // tools-rejected branch below: a status-code-only check would otherwise catch
169
+ // a genuine context-overflow 400 first (many providers share 400 for both
170
+ // cases) and retry by stripping tools, which does nothing for an oversized
171
+ // prompt and just fails again the same way.
172
+ if (isContextOverflowError(err, errMsg)) {
173
+ log.warn(`[llm-client] ${this.providerName}: context overflow — compacting messages and retrying`)
174
+ const originalCount = body.messages.length
175
+ compactBodyForContextOverflow(body, err)
176
+ log.info(`[llm-client] ${this.providerName}: compacted ${originalCount} msgs → ${body.messages.length} msgs, max_tokens=${body.max_tokens}`)
177
+ response = await client.chat.completions.create(this.modifyRequestBody(body, options), { signal: options.signal })
178
+ }
179
+ // Retry 2: tools rejected by provider — remove tools and retry
180
+ else if (sendTools && profile.retryWithoutToolsOnCodes.includes(status)) {
181
+ log.warn(`[llm-client] ${this.providerName}: tools rejected (HTTP ${status}) — retrying without tools`)
182
+ const bodyNoTools = { ...body }
183
+ delete bodyNoTools.tools
184
+ delete bodyNoTools.tool_choice
185
+ delete bodyNoTools.parallel_tool_calls
186
+ response = await client.chat.completions.create(this.modifyRequestBody(bodyNoTools, options), { signal: options.signal })
187
+ }
188
+ else {
189
+ throw err
190
+ }
191
+ }
192
+
193
+ const choice = response.choices[0]
194
+ const msg = choice.message
195
+
196
+ let final_tool_calls: LLMToolCall[] | undefined = (msg.tool_calls as any[])?.map((tc: any) => ({
197
+ id: tc.id,
198
+ type: "function" as const,
199
+ function: {
200
+ name: toolNameMap.get(tc.function.name) ?? tc.function.name,
201
+ arguments: tc.function.arguments,
202
+ },
203
+ }))
204
+
205
+ let final_content = msg.content ?? ""
206
+
207
+ if (sendTools && (!final_tool_calls || final_tool_calls.length === 0) && final_content) {
208
+ const extracted = extractToolCallsFromText(final_content, toolNameMap)
209
+ if (extracted.tool_calls.length > 0) {
210
+ final_tool_calls = extracted.tool_calls
211
+ final_content = extracted.content
212
+ }
213
+ }
214
+
215
+ return {
216
+ content: final_content,
217
+ tool_calls: final_tool_calls,
218
+ reasoning_content: (msg as any).reasoning_content ?? undefined,
219
+ stop_reason:
220
+ choice.finish_reason === "tool_calls" ? "tool_calls"
221
+ : choice.finish_reason === "length" ? "max_tokens"
222
+ : "stop",
223
+ usage: response.usage ? {
224
+ input_tokens: response.usage.prompt_tokens,
225
+ output_tokens: response.usage.completion_tokens,
226
+ } : undefined,
227
+ }
228
+ }
229
+
230
+ private async _streamCall(
231
+ client: any,
232
+ body: any,
233
+ options: LLMCallOptions,
234
+ toolNameMap: Map<string, string>,
235
+ sendTools: boolean,
236
+ profile: ReturnType<typeof getProviderProfile>,
237
+ ): Promise<LLMResponse> {
238
+ let stream
239
+ try {
240
+ stream = await client.chat.completions.create({ ...this.modifyRequestBody(body, options), stream: true }, { signal: options.signal })
241
+ } catch (err: any) {
242
+ const status = err?.status ?? err?.response?.status
243
+ const errMsg = (err?.error?.message ?? err?.message ?? "").toLowerCase()
244
+
245
+ if (isContextOverflowError(err, errMsg)) {
246
+ log.warn(`[llm-client] ${this.providerName}: context overflow — compacting messages and retrying stream`)
247
+ const originalCount = body.messages.length
248
+ compactBodyForContextOverflow(body, err)
249
+ log.info(`[llm-client] ${this.providerName}: compacted ${originalCount} msgs → ${body.messages.length} msgs, max_tokens=${body.max_tokens}`)
250
+ stream = await client.chat.completions.create({ ...this.modifyRequestBody(body, options), stream: true }, { signal: options.signal })
251
+ } else if (sendTools && profile.retryWithoutToolsOnCodes.includes(status)) {
252
+ log.warn(`[llm-client] ${this.providerName}: tools rejected (HTTP ${status}) — retrying stream without tools`)
253
+ const bodyNoTools = { ...body }
254
+ delete bodyNoTools.tools
255
+ delete bodyNoTools.tool_choice
256
+ delete bodyNoTools.parallel_tool_calls
257
+ stream = await client.chat.completions.create({ ...this.modifyRequestBody(bodyNoTools, options), stream: true }, { signal: options.signal })
258
+ } else {
259
+ throw err
260
+ }
261
+ }
262
+
263
+ let content = ""
264
+ let reasoning_content = ""
265
+ let finish_reason = "stop"
266
+ const toolCallMap: Map<number, { id: string; name: string; arguments: string }> = new Map()
267
+ let input_tokens = 0
268
+ let output_tokens = 0
269
+
270
+ for await (const chunk of stream) {
271
+ const choice = chunk.choices?.[0]
272
+ if (!choice) continue
273
+
274
+ const delta = choice.delta as any
275
+ if (delta.content) {
276
+ content += delta.content
277
+ options.onToken!(delta.content)
278
+ }
279
+ if (delta.reasoning_content) {
280
+ reasoning_content += delta.reasoning_content
281
+ options.onReasoningToken?.(delta.reasoning_content)
282
+ }
283
+ if (delta.tool_calls) {
284
+ for (const tc of delta.tool_calls) {
285
+ const idx: number = tc.index
286
+ if (!toolCallMap.has(idx)) {
287
+ toolCallMap.set(idx, { id: tc.id ?? "", name: tc.function?.name ?? "", arguments: "" })
288
+ }
289
+ const entry = toolCallMap.get(idx)!
290
+ if (tc.id) entry.id = tc.id
291
+ if (tc.function?.name) entry.name = tc.function.name
292
+ if (tc.function?.arguments) entry.arguments += tc.function.arguments
293
+ }
294
+ }
295
+ if (choice.finish_reason) finish_reason = choice.finish_reason
296
+
297
+ if (chunk.usage) {
298
+ input_tokens = chunk.usage.prompt_tokens ?? 0
299
+ output_tokens = chunk.usage.completion_tokens ?? 0
300
+ }
301
+ }
302
+
303
+ const tool_calls: LLMToolCall[] = [...toolCallMap.values()].map((tc) => ({
304
+ id: tc.id,
305
+ type: "function" as const,
306
+ function: {
307
+ name: toolNameMap.get(tc.name) ?? tc.name,
308
+ arguments: tc.arguments || "{}",
309
+ },
310
+ }))
311
+
312
+ let final_tool_calls: LLMToolCall[] | undefined = tool_calls.length ? tool_calls : undefined
313
+ let final_content = content
314
+
315
+ if (sendTools && !final_tool_calls && final_content) {
316
+ const extracted = extractToolCallsFromText(final_content, toolNameMap)
317
+ if (extracted.tool_calls.length > 0) {
318
+ final_tool_calls = extracted.tool_calls
319
+ final_content = extracted.content
320
+ }
321
+ }
322
+
323
+ return {
324
+ content: final_content,
325
+ tool_calls: final_tool_calls,
326
+ reasoning_content: reasoning_content || undefined,
327
+ stop_reason:
328
+ finish_reason === "tool_calls" ? "tool_calls"
329
+ : finish_reason === "length" ? "max_tokens"
330
+ : "stop",
331
+ usage: input_tokens > 0 || output_tokens > 0
332
+ ? { input_tokens, output_tokens }
333
+ : undefined,
334
+ }
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Extracts tool_calls from text when the model fails to emit native tool_calls.
340
+ * Supports common formats used by Gemma, Qwen, and other local models.
341
+ */
342
+ function extractToolCallsFromText(
343
+ content: string,
344
+ toolNameMap: Map<string, string>,
345
+ knownToolNames?: Set<string>,
346
+ ): { content: string; tool_calls: LLMToolCall[] } {
347
+ const tool_calls: LLMToolCall[] = []
348
+ let extractedContent = content
349
+
350
+ // Regexes for wrapped tool-call blocks.
351
+ const regexes = [
352
+ /<tool_call>\s*({[\s\S]*?})\s*<\/tool_call>/g,
353
+ /<function_call>\s*({[\s\S]*?})\s*<\/function_call>/g,
354
+ /```(?:tool_call|json)\s*({[\s\S]*?})\s*```/g,
355
+ ]
356
+
357
+ for (const regex of regexes) {
358
+ let match
359
+ while ((match = regex.exec(content)) !== null) {
360
+ try {
361
+ const json = JSON.parse(match[1])
362
+ const calls = Array.isArray(json) ? json : [json]
363
+ for (const call of calls) {
364
+ if (!call) continue
365
+ // Accept both { name, arguments } and { function: { name, arguments } }
366
+ const fn = call.function || call
367
+ const name = fn.name ?? call.name
368
+ let args = fn.arguments ?? call.arguments ?? call.parameters
369
+ if (!name) continue
370
+ tool_calls.push({
371
+ id: crypto.randomUUID(),
372
+ type: "function",
373
+ function: {
374
+ name: toolNameMap.get(name) ?? name,
375
+ arguments: typeof args === "object" ? JSON.stringify(args) : (args || "{}"),
376
+ },
377
+ })
378
+ extractedContent = extractedContent.replace(match[0], "").trim()
379
+ }
380
+ } catch {
381
+ // ignore parse errors
382
+ }
383
+ }
384
+ }
385
+
386
+ // Fallback: entire output is a bare JSON tool call — only if name matches a known tool
387
+ if (tool_calls.length === 0 && knownToolNames && knownToolNames.size > 0) {
388
+ try {
389
+ const trimmed = content.trim()
390
+ // Strip common markdown fences before parsing.
391
+ const jsonText = trimmed.replace(/^```(?:json|tool_call)?\s*|\s*```$/g, "").trim()
392
+ const json = JSON.parse(jsonText)
393
+ const calls = Array.isArray(json) ? json : [json]
394
+ for (const call of calls) {
395
+ if (!call) continue
396
+ const fn = call.function || call
397
+ const name = fn.name ?? call.name
398
+ let args = fn.arguments ?? call.arguments ?? call.parameters
399
+ const resolvedName = toolNameMap.get(name) ?? name
400
+ if (name && knownToolNames.has(resolvedName) && (args !== undefined || calls.length === 1)) {
401
+ tool_calls.push({
402
+ id: crypto.randomUUID(),
403
+ type: "function",
404
+ function: {
405
+ name: resolvedName,
406
+ arguments: typeof args === "object" ? JSON.stringify(args) : (args || "{}"),
407
+ },
408
+ })
409
+ extractedContent = ""
410
+ }
411
+ }
412
+ } catch {
413
+ // not valid JSON
414
+ }
415
+ }
416
+
417
+ return { content: extractedContent, tool_calls }
418
+ }
@@ -0,0 +1,5 @@
1
+ import { OpenAICompatBase } from "./openai-compat-base"
2
+
3
+ export class OpenAIProvider extends OpenAICompatBase {
4
+ constructor() { super("openai") }
5
+ }
@@ -0,0 +1,9 @@
1
+ import { OpenAICompatBase } from "./openai-compat-base"
2
+
3
+ export class OpenCodeGoProvider extends OpenAICompatBase {
4
+ static readonly secretKey = "OPENCODE_GO_API_KEY"
5
+
6
+ constructor() {
7
+ super("opencode-go")
8
+ }
9
+ }
@@ -0,0 +1,5 @@
1
+ import { OpenAICompatBase } from "./openai-compat-base"
2
+
3
+ export class OpenRouterProvider extends OpenAICompatBase {
4
+ constructor() { super("openrouter") }
5
+ }
@@ -0,0 +1,5 @@
1
+ import { OpenAICompatBase } from "./openai-compat-base"
2
+
3
+ export class QwenProvider extends OpenAICompatBase {
4
+ constructor() { super("qwen") }
5
+ }
@@ -0,0 +1,5 @@
1
+ import { OpenAICompatBase } from "./openai-compat-base"
2
+
3
+ export class ZaiProvider extends OpenAICompatBase {
4
+ constructor() { super("z-ai") }
5
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Minimal loadout — the single source of truth for what the coordinator starts
3
+ * a turn with, and the rule that derives which skills ride along with it.
4
+ *
5
+ * Everything else is discovered at runtime through `search_knowledge`, which
6
+ * injects both the tool and its associated skill into the loadout
7
+ * (agent-loop.ts). So a skill that documents tools outside this set has nothing
8
+ * to teach a turn that hasn't discovered them yet — it only spends context.
9
+ *
10
+ * These used to be two hand-maintained lists (one here, one in skill-selector)
11
+ * and they drifted: 3 of the 4 "minimal" skills documented tools the agent did
12
+ * not have. The skill set is now derived from the tool set so it cannot.
13
+ */
14
+
15
+ /** Tools always present in the coordinator's loadout, without discovery. */
16
+ export const MINIMAL_TOOLS = new Set([
17
+ // Discovery — the entry point to everything else
18
+ "search_knowledge",
19
+ // Communication with the user
20
+ "notify",
21
+ "report_progress",
22
+ // Notes that survive context compaction
23
+ "save_note",
24
+ // Orchestration: the coordinator's own competency, not an optional capability
25
+ "task_delegate",
26
+ "task_revise",
27
+ "agent_find",
28
+ "task_status",
29
+ ])
30
+
31
+ /** Splits a SkillDoc's comma-separated `tools` column into tool names. */
32
+ export function parseSkillTools(toolsCsv: string | null | undefined): string[] {
33
+ return (toolsCsv ?? "")
34
+ .split(",")
35
+ .map((name) => name.trim())
36
+ .filter(Boolean)
37
+ }
38
+
39
+ /**
40
+ * A skill is minimal when every tool it documents is already in the loadout.
41
+ * A skill that declares no tools is not minimal: it has no anchor to the
42
+ * always-available set, so it belongs to discovery like any other.
43
+ */
44
+ export function isMinimalSkill(toolsCsv: string | null | undefined): boolean {
45
+ const tools = parseSkillTools(toolsCsv)
46
+ return tools.length > 0 && tools.every((name) => MINIMAL_TOOLS.has(name))
47
+ }