@johpaz/hive-sdk 0.0.14 → 0.0.15

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 (199) hide show
  1. package/.github/CODEOWNERS +9 -0
  2. package/.github/workflows/publish.yml +89 -0
  3. package/.github/workflows/version-bump.yml +102 -0
  4. package/CHANGELOG.md +38 -0
  5. package/README.md +158 -0
  6. package/bun.lock +543 -0
  7. package/bunfig.toml +7 -0
  8. package/docs/API-AGENTS.md +316 -0
  9. package/docs/API-CONTEXT-COMPILER.md +252 -0
  10. package/docs/API-DAG-SCHEDULER.md +273 -0
  11. package/docs/API-TOOLS-SKILLS-CHANNELS.md +293 -0
  12. package/docs/API-WORKERS-EVENTS.md +152 -0
  13. package/docs/INDEX.md +141 -0
  14. package/docs/README.md +68 -0
  15. package/package.json +54 -105
  16. package/packages/cli/package.json +17 -0
  17. package/packages/cli/src/commands/init.ts +56 -0
  18. package/packages/cli/src/commands/run.ts +45 -0
  19. package/packages/cli/src/commands/test.ts +42 -0
  20. package/packages/cli/src/commands/trace.ts +55 -0
  21. package/packages/cli/src/index.ts +43 -0
  22. package/packages/core/package.json +58 -0
  23. package/packages/core/src/ace/Curator.ts +158 -0
  24. package/packages/core/src/ace/Reflector.ts +200 -0
  25. package/packages/core/src/ace/Tracer.ts +100 -0
  26. package/packages/core/src/ace/index.ts +4 -0
  27. package/packages/core/src/agent/AgentRunner.ts +699 -0
  28. package/packages/core/src/agent/Compaction.ts +221 -0
  29. package/packages/core/src/agent/ContextCompiler.ts +567 -0
  30. package/packages/core/src/agent/ContextGuard.ts +91 -0
  31. package/packages/core/src/agent/ConversationStore.ts +244 -0
  32. package/packages/core/src/agent/Hooks.ts +166 -0
  33. package/packages/core/src/agent/NativeTools.ts +31 -0
  34. package/packages/core/src/agent/PromptBuilder.ts +169 -0
  35. package/packages/core/src/agent/Service.ts +267 -0
  36. package/packages/core/src/agent/StuckLoop.ts +133 -0
  37. package/packages/core/src/agent/index.ts +12 -0
  38. package/packages/core/src/agent/providers/LLMClient.ts +149 -0
  39. package/packages/core/src/agent/providers/anthropic.ts +212 -0
  40. package/packages/core/src/agent/providers/gemini.ts +215 -0
  41. package/packages/core/src/agent/providers/index.ts +199 -0
  42. package/packages/core/src/agent/providers/interface.ts +195 -0
  43. package/packages/core/src/agent/providers/ollama.ts +175 -0
  44. package/packages/core/src/agent/providers/openai-compat.ts +231 -0
  45. package/packages/core/src/agent/providers.ts +1 -0
  46. package/packages/core/src/agent/selectors/PlaybookSelector.ts +147 -0
  47. package/packages/core/src/agent/selectors/SkillSelector.ts +478 -0
  48. package/packages/core/src/agent/selectors/ToolSelector.ts +577 -0
  49. package/packages/core/src/agent/selectors/index.ts +6 -0
  50. package/packages/core/src/api/createAgent.test.ts +48 -0
  51. package/packages/core/src/api/createAgent.ts +122 -0
  52. package/packages/core/src/api/index.ts +2 -0
  53. package/packages/core/src/canvas/CanvasManager.ts +390 -0
  54. package/packages/core/src/canvas/a2ui-tools.ts +255 -0
  55. package/packages/core/src/canvas/canvas-tools.ts +448 -0
  56. package/packages/core/src/canvas/emitter.ts +149 -0
  57. package/packages/core/src/canvas/index.ts +6 -0
  58. package/packages/core/src/config/index.ts +2 -0
  59. package/packages/core/src/config/loader.ts +554 -0
  60. package/packages/core/src/ethics/EthicsGuard.test.ts +54 -0
  61. package/packages/core/src/ethics/EthicsGuard.ts +66 -0
  62. package/packages/core/src/ethics/index.ts +2 -0
  63. package/packages/core/src/gateway/channel-notify.test.ts +14 -0
  64. package/packages/core/src/gateway/channel-notify.ts +12 -0
  65. package/packages/core/src/gateway/index.ts +1 -0
  66. package/packages/core/src/index.ts +37 -0
  67. package/packages/core/src/mcp/MCPClient.ts +439 -0
  68. package/packages/core/src/mcp/MCPToolAdapter.ts +176 -0
  69. package/packages/core/src/mcp/config.ts +13 -0
  70. package/packages/core/src/mcp/hot-reload.ts +147 -0
  71. package/packages/core/src/mcp/index.ts +11 -0
  72. package/packages/core/src/mcp/logger.ts +42 -0
  73. package/packages/core/src/mcp/singleton.ts +21 -0
  74. package/packages/core/src/mcp/transports/index.ts +67 -0
  75. package/packages/core/src/mcp/transports/sse.ts +241 -0
  76. package/packages/core/src/mcp/transports/websocket.ts +159 -0
  77. package/packages/core/src/memory/Scratchpad.test.ts +47 -0
  78. package/packages/core/src/memory/Scratchpad.ts +37 -0
  79. package/packages/core/src/memory/Storage.ts +6 -0
  80. package/packages/core/src/memory/index.ts +2 -0
  81. package/packages/core/src/multimodal/VisionService.ts +293 -0
  82. package/packages/core/src/multimodal/index.ts +2 -0
  83. package/packages/core/src/multimodal/types.ts +28 -0
  84. package/packages/core/src/security/Pairing.ts +250 -0
  85. package/packages/core/src/security/RateLimit.ts +270 -0
  86. package/packages/core/src/security/index.ts +4 -0
  87. package/packages/core/src/skills/SkillLoader.ts +388 -0
  88. package/packages/core/src/skills/bundled-data.generated.ts +3332 -0
  89. package/packages/core/src/skills/defineSkill.ts +18 -0
  90. package/packages/core/src/skills/index.ts +4 -0
  91. package/packages/core/src/state/index.ts +2 -0
  92. package/packages/core/src/state/store.ts +312 -0
  93. package/packages/core/src/storage/SQLiteStorage.ts +407 -0
  94. package/packages/core/src/storage/crypto.ts +101 -0
  95. package/packages/core/src/storage/index.ts +10 -0
  96. package/packages/core/src/storage/onboarding.ts +1603 -0
  97. package/packages/core/src/storage/schema.ts +689 -0
  98. package/packages/core/src/storage/seed.ts +740 -0
  99. package/packages/core/src/storage/usage.ts +374 -0
  100. package/packages/core/src/swarm/AgentBus.ts +460 -0
  101. package/packages/core/src/swarm/AgentExecutor.ts +53 -0
  102. package/packages/core/src/swarm/Coordinator.ts +251 -0
  103. package/packages/core/src/swarm/EventBridge.ts +122 -0
  104. package/packages/core/src/swarm/EventBus.ts +169 -0
  105. package/packages/core/src/swarm/TaskGraph.ts +192 -0
  106. package/packages/core/src/swarm/TaskNode.ts +97 -0
  107. package/packages/core/src/swarm/TaskResult.ts +22 -0
  108. package/packages/core/src/swarm/WorkerPool.ts +236 -0
  109. package/packages/core/src/swarm/errors.ts +37 -0
  110. package/packages/core/src/swarm/index.ts +30 -0
  111. package/packages/core/src/swarm/presets/HiveLearnPreset.ts +99 -0
  112. package/packages/core/src/swarm/presets/ResearchPreset.ts +97 -0
  113. package/packages/core/src/swarm/presets/index.ts +4 -0
  114. package/packages/core/src/swarm/strategies/ParallelStrategy.ts +21 -0
  115. package/packages/core/src/swarm/strategies/PriorityStrategy.ts +46 -0
  116. package/packages/core/src/swarm/strategies/index.ts +3 -0
  117. package/packages/core/src/swarm/types.ts +164 -0
  118. package/packages/core/src/tools/ToolExecutor.ts +58 -0
  119. package/packages/core/src/tools/ToolRegistry.test.ts +98 -0
  120. package/packages/core/src/tools/ToolRegistry.ts +61 -0
  121. package/packages/core/src/tools/agents/get-available-models.ts +118 -0
  122. package/packages/core/src/tools/agents/index.ts +715 -0
  123. package/packages/core/src/tools/bridge-events.ts +26 -0
  124. package/packages/core/src/tools/canvas/index.ts +375 -0
  125. package/packages/core/src/tools/cli/index.ts +142 -0
  126. package/packages/core/src/tools/codebridge/index.ts +342 -0
  127. package/packages/core/src/tools/core/index.ts +476 -0
  128. package/packages/core/src/tools/cron/index.ts +626 -0
  129. package/packages/core/src/tools/filesystem/fs-delete.ts +78 -0
  130. package/packages/core/src/tools/filesystem/fs-edit.ts +106 -0
  131. package/packages/core/src/tools/filesystem/fs-exists.ts +63 -0
  132. package/packages/core/src/tools/filesystem/fs-glob.ts +108 -0
  133. package/packages/core/src/tools/filesystem/fs-list.ts +129 -0
  134. package/packages/core/src/tools/filesystem/fs-read.ts +72 -0
  135. package/packages/core/src/tools/filesystem/fs-write.ts +67 -0
  136. package/packages/core/src/tools/filesystem/index.ts +34 -0
  137. package/packages/core/src/tools/filesystem/workspace-guard.ts +62 -0
  138. package/packages/core/src/tools/index.ts +231 -0
  139. package/packages/core/src/tools/meeting/index.ts +363 -0
  140. package/packages/core/src/tools/office/index.ts +47 -0
  141. package/packages/core/src/tools/office/office-escribir-docx.ts +192 -0
  142. package/packages/core/src/tools/office/office-escribir-pdf.ts +172 -0
  143. package/packages/core/src/tools/office/office-escribir-pptx.ts +174 -0
  144. package/packages/core/src/tools/office/office-escribir-xlsx.ts +116 -0
  145. package/packages/core/src/tools/office/office-leer-docx.ts +93 -0
  146. package/packages/core/src/tools/office/office-leer-pdf.ts +114 -0
  147. package/packages/core/src/tools/office/office-leer-pptx.ts +136 -0
  148. package/packages/core/src/tools/office/office-leer-xlsx.ts +124 -0
  149. package/packages/core/src/tools/projects/index.ts +37 -0
  150. package/packages/core/src/tools/projects/project-create.ts +94 -0
  151. package/packages/core/src/tools/projects/project-done.ts +66 -0
  152. package/packages/core/src/tools/projects/project-fail.ts +66 -0
  153. package/packages/core/src/tools/projects/project-list.ts +96 -0
  154. package/packages/core/src/tools/projects/project-update.ts +72 -0
  155. package/packages/core/src/tools/projects/task-create.ts +68 -0
  156. package/packages/core/src/tools/projects/task-evaluate.ts +93 -0
  157. package/packages/core/src/tools/projects/task-update.ts +93 -0
  158. package/packages/core/src/tools/types.ts +39 -0
  159. package/packages/core/src/tools/voice/index.ts +104 -0
  160. package/packages/core/src/tools/web/browser-click.ts +78 -0
  161. package/packages/core/src/tools/web/browser-extract.ts +139 -0
  162. package/packages/core/src/tools/web/browser-navigate.ts +106 -0
  163. package/packages/core/src/tools/web/browser-screenshot.ts +87 -0
  164. package/packages/core/src/tools/web/browser-script.ts +88 -0
  165. package/packages/core/src/tools/web/browser-service.ts +554 -0
  166. package/packages/core/src/tools/web/browser-type.ts +101 -0
  167. package/packages/core/src/tools/web/browser-wait.ts +136 -0
  168. package/packages/core/src/tools/web/index.ts +41 -0
  169. package/packages/core/src/tools/web/web-fetch.ts +78 -0
  170. package/packages/core/src/tools/web/web-search.ts +123 -0
  171. package/packages/core/src/utils/benchmark.ts +80 -0
  172. package/packages/core/src/utils/crypto.ts +73 -0
  173. package/packages/core/src/utils/date.ts +42 -0
  174. package/packages/core/src/utils/index.ts +10 -0
  175. package/packages/core/src/utils/logger.ts +389 -0
  176. package/packages/core/src/utils/retry.ts +70 -0
  177. package/packages/core/src/utils/toon.ts +253 -0
  178. package/packages/core/src/voice/index.ts +656 -0
  179. package/test/setup-db.ts +216 -0
  180. package/tsconfig.json +39 -0
  181. package/src/agents.ts +0 -1
  182. package/src/canvas.ts +0 -1
  183. package/src/channels.ts +0 -1
  184. package/src/config.ts +0 -1
  185. package/src/events.ts +0 -1
  186. package/src/gateway.ts +0 -1
  187. package/src/index.ts +0 -304
  188. package/src/mcp.ts +0 -1
  189. package/src/multimodal.ts +0 -1
  190. package/src/scheduler.ts +0 -1
  191. package/src/security.ts +0 -1
  192. package/src/skills.ts +0 -1
  193. package/src/state.ts +0 -1
  194. package/src/storage.ts +0 -1
  195. package/src/tools.ts +0 -1
  196. package/src/tts.ts +0 -1
  197. package/src/types.ts +0 -82
  198. package/src/utils.ts +0 -1
  199. package/src/voice.ts +0 -1
@@ -0,0 +1,212 @@
1
+ import { logger } from "../../utils/logger.ts"
2
+ import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface"
3
+ import type { ContentPart, LLMMessage } from "./LLMClient.ts"
4
+
5
+ const log = logger.child("llm-client")
6
+
7
+ // Models that support extended thinking (claude-3-7+ and claude-4.x).
8
+ const THINKING_CAPABLE_MODELS = new Set([
9
+ "claude-3-7-sonnet-20250219",
10
+ "claude-sonnet-4-5",
11
+ "claude-sonnet-4-6",
12
+ "claude-opus-4-5",
13
+ "claude-opus-4-6",
14
+ "claude-opus-4-7",
15
+ "claude-haiku-4-5",
16
+ "claude-haiku-4-5-20251001",
17
+ ])
18
+
19
+ function supportsThinking(model: string): boolean {
20
+ if (THINKING_CAPABLE_MODELS.has(model)) return true
21
+ // Also match any claude-4.x or claude-3-7+ by prefix
22
+ return /^claude-(4|3-7)/.test(model)
23
+ }
24
+
25
+ export class AnthropicProvider implements LLMProvider {
26
+ private _convertContentPart(part: ContentPart): any {
27
+ switch (part.type) {
28
+ case "text":
29
+ return { type: "text", text: part.text }
30
+ case "image_url": {
31
+ const url = part.image_url.url
32
+ if (url.startsWith("data:")) {
33
+ const match = url.match(/^data:([^;]+);base64,(.+)$/)
34
+ if (match) return { type: "image", source: { type: "base64", media_type: match[1], data: match[2] } }
35
+ }
36
+ return { type: "image", source: { type: "url", url } }
37
+ }
38
+ case "image_base64":
39
+ return { type: "image", source: { type: "base64", media_type: part.mimeType, data: part.base64 } }
40
+ case "document":
41
+ return { type: "document", source: { type: "base64", media_type: part.mimeType, data: part.base64 } }
42
+ default:
43
+ return { type: "text", text: JSON.stringify(part) }
44
+ }
45
+ }
46
+
47
+ private _convertUserContent(msg: LLMMessage): any[] {
48
+ if (Array.isArray(msg.content)) {
49
+ return msg.content.map(p => this._convertContentPart(p))
50
+ }
51
+ return [{ type: "text", text: msg.content }]
52
+ }
53
+
54
+ async call(options: LLMCallOptions): Promise<LLMResponse> {
55
+ const Anthropic = await import("@anthropic-ai/sdk")
56
+ const client = new Anthropic.default({ apiKey: options.apiKey })
57
+
58
+ const systemText = options.messages
59
+ .filter((m) => m.role === "system")
60
+ .map((m) => m.content)
61
+ .join("\n\n")
62
+
63
+ const anthropicMessages: any[] = []
64
+
65
+ for (const msg of options.messages) {
66
+ if (msg.role === "system") continue
67
+
68
+ if (msg.role === "tool") {
69
+ const block = { type: "tool_result", tool_use_id: msg.tool_call_id, content: msg.content }
70
+ const last = anthropicMessages[anthropicMessages.length - 1]
71
+ if (last?.role === "user" && Array.isArray(last.content)) {
72
+ last.content.push(block)
73
+ } else {
74
+ anthropicMessages.push({ role: "user", content: [block] })
75
+ }
76
+ continue
77
+ }
78
+
79
+ if (msg.role === "assistant" && msg.tool_calls?.length) {
80
+ const content: any[] = []
81
+ if (msg.content) content.push({ type: "text", text: msg.content })
82
+ for (const tc of msg.tool_calls) {
83
+ let input: Record<string, unknown>
84
+ try { input = JSON.parse(tc.function.arguments || "{}") } catch { input = {} }
85
+ content.push({ type: "tool_use", id: tc.id, name: tc.function.name, input })
86
+ }
87
+ anthropicMessages.push({ role: "assistant", content })
88
+ continue
89
+ }
90
+
91
+ anthropicMessages.push({ role: msg.role, content: Array.isArray(msg.content) ? this._convertUserContent(msg) : msg.content })
92
+ }
93
+
94
+ const tools: any[] = (options.tools ?? []).map((t) => ({
95
+ name: t.function.name,
96
+ description: t.function.description,
97
+ input_schema: t.function.parameters,
98
+ }))
99
+
100
+ const body: any = {
101
+ model: options.model,
102
+ max_tokens: options.maxTokens ?? 16384,
103
+ messages: anthropicMessages,
104
+ }
105
+ if (systemText) body.system = systemText
106
+ if (tools.length) body.tools = tools
107
+
108
+ // Extended thinking — only for supported models
109
+ const thinkingEnabled = options.thinking?.enabled && supportsThinking(options.model)
110
+ if (thinkingEnabled) {
111
+ body.thinking = { type: "enabled", budget_tokens: options.thinking?.budget_tokens ?? 10000 }
112
+ }
113
+
114
+ log.info(
115
+ `[llm-client] anthropic/${options.model} — ${anthropicMessages.length} msgs, ${tools.length} tools` +
116
+ (thinkingEnabled ? ` thinking=${body.thinking.budget_tokens}tok` : "")
117
+ )
118
+
119
+ let content = ""
120
+ let thinking_content = ""
121
+ const tool_calls: LLMToolCall[] = []
122
+
123
+ // Streaming via messages.stream()
124
+ const useStream = true // Always stream for better UX
125
+ if (useStream) {
126
+ const stream = client.messages.stream({ ...body, ...(options.signal ? {} : {}) })
127
+
128
+ // Track partial tool inputs by index
129
+ const partialInputs: Record<number, string> = {}
130
+ const toolMeta: Record<number, { id: string; name: string }> = {}
131
+
132
+ for await (const event of stream) {
133
+ if (event.type === "content_block_start") {
134
+ if (event.content_block.type === "tool_use") {
135
+ toolMeta[event.index] = { id: event.content_block.id, name: event.content_block.name }
136
+ partialInputs[event.index] = ""
137
+ }
138
+ } else if (event.type === "content_block_delta") {
139
+ if (event.delta.type === "text_delta") {
140
+ content += event.delta.text
141
+ if (options.onToken) options.onToken(event.delta.text)
142
+ } else if (event.delta.type === "thinking_delta") {
143
+ thinking_content += event.delta.thinking
144
+ } else if (event.delta.type === "input_json_delta") {
145
+ if (partialInputs[event.index] !== undefined) {
146
+ partialInputs[event.index] += event.delta.partial_json
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ const finalMsg = await stream.finalMessage()
153
+
154
+ // Build tool_calls from accumulated partial inputs
155
+ for (const [idx, meta] of Object.entries(toolMeta)) {
156
+ const args = partialInputs[Number(idx)] ?? "{}"
157
+ tool_calls.push({
158
+ id: meta.id,
159
+ type: "function",
160
+ function: { name: meta.name, arguments: args },
161
+ })
162
+ }
163
+
164
+ const usage = finalMsg.usage
165
+ return {
166
+ content,
167
+ thinking_content: thinking_content || undefined,
168
+ tool_calls: tool_calls.length ? tool_calls : undefined,
169
+ stop_reason:
170
+ finalMsg.stop_reason === "tool_use" ? "tool_calls"
171
+ : finalMsg.stop_reason === "max_tokens" ? "max_tokens"
172
+ : "stop",
173
+ usage: {
174
+ input_tokens: usage.input_tokens,
175
+ output_tokens: usage.output_tokens,
176
+ thinking_tokens: (usage as any).thinking_tokens ?? 0,
177
+ },
178
+ }
179
+ }
180
+
181
+ // Non-streaming fallback (kept for reference, unreachable with useStream=true)
182
+ const response = await client.messages.create(body)
183
+
184
+ for (const block of response.content) {
185
+ if (block.type === "text") content = block.text
186
+ if (block.type === "thinking") thinking_content = (block as any).thinking ?? ""
187
+ if (block.type === "tool_use") {
188
+ let args: string
189
+ try { args = JSON.stringify(block.input) } catch { args = "{}" }
190
+ tool_calls.push({
191
+ id: block.id,
192
+ type: "function",
193
+ function: { name: block.name, arguments: args },
194
+ })
195
+ }
196
+ }
197
+
198
+ return {
199
+ content,
200
+ thinking_content: thinking_content || undefined,
201
+ tool_calls: tool_calls.length ? tool_calls : undefined,
202
+ stop_reason:
203
+ response.stop_reason === "tool_use" ? "tool_calls"
204
+ : response.stop_reason === "max_tokens" ? "max_tokens"
205
+ : "stop",
206
+ usage: {
207
+ input_tokens: response.usage.input_tokens,
208
+ output_tokens: response.usage.output_tokens,
209
+ },
210
+ }
211
+ }
212
+ }
@@ -0,0 +1,215 @@
1
+ import { logger } from "../../utils/logger.ts"
2
+ import { sanitizeMessages } from "./interface"
3
+ import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface"
4
+ import type { ContentPart, LLMMessage } from "./LLMClient.ts"
5
+
6
+ const log = logger.child("llm-client")
7
+
8
+ export class GeminiProvider implements LLMProvider {
9
+ private _convertContentPart(part: ContentPart): any {
10
+ switch (part.type) {
11
+ case "text":
12
+ return { text: part.text }
13
+ case "image_url": {
14
+ const url = part.image_url.url
15
+ if (url.startsWith("data:")) {
16
+ const match = url.match(/^data:([^;]+);base64,(.+)$/)
17
+ if (match) return { inlineData: { mimeType: match[1], data: match[2] } }
18
+ }
19
+ return { text: `[Image URL: ${url}]` }
20
+ }
21
+ case "image_base64":
22
+ return { inlineData: { mimeType: part.mimeType, data: part.base64 } }
23
+ case "document":
24
+ return { inlineData: { mimeType: part.mimeType, data: part.base64 } }
25
+ default:
26
+ return { text: JSON.stringify(part) }
27
+ }
28
+ }
29
+
30
+ private _convertUserParts(msg: LLMMessage): any[] {
31
+ if (Array.isArray(msg.content)) {
32
+ return msg.content.map(p => this._convertContentPart(p))
33
+ }
34
+ return [{ text: msg.content }]
35
+ }
36
+
37
+ async call(options: LLMCallOptions): Promise<LLMResponse> {
38
+ const { GoogleGenAI } = await import("@google/genai")
39
+
40
+ const clientOpts: any = { apiKey: options.apiKey }
41
+ if (options.baseUrl?.trim()) clientOpts.httpOptions = { baseUrl: options.baseUrl.trim() }
42
+
43
+ const ai = new GoogleGenAI(clientOpts)
44
+
45
+ const cleanMessages = sanitizeMessages(options.messages)
46
+
47
+ // Build toolCallId → name map for converting tool results
48
+ const toolNameMap = new Map<string, string>()
49
+ for (const msg of cleanMessages) {
50
+ if (msg.role === "assistant" && msg.tool_calls) {
51
+ for (const tc of msg.tool_calls) toolNameMap.set(tc.id, tc.function.name)
52
+ }
53
+ }
54
+
55
+ // Convert canonical messages → Gemini Content[]
56
+ let systemText = ""
57
+ const rawContents: any[] = []
58
+
59
+ for (const msg of cleanMessages) {
60
+ if (msg.role === "system") {
61
+ systemText += (systemText ? "\n\n" : "") + msg.content
62
+ continue
63
+ }
64
+ if (msg.role === "user") {
65
+ rawContents.push({ role: "user", parts: this._convertUserParts(msg) })
66
+ continue
67
+ }
68
+ if (msg.role === "assistant") {
69
+ const parts: any[] = []
70
+ if (msg.content) parts.push({ text: msg.content })
71
+ for (const tc of msg.tool_calls ?? []) {
72
+ const fcPart: any = { functionCall: { name: tc.function.name, args: JSON.parse(tc.function.arguments || "{}") } }
73
+ if (tc.thought_signature) fcPart.thoughtSignature = tc.thought_signature
74
+ parts.push(fcPart)
75
+ }
76
+ if (parts.length) rawContents.push({ role: "model", parts })
77
+ continue
78
+ }
79
+ if (msg.role === "tool") {
80
+ const fnName = toolNameMap.get(msg.tool_call_id || "") || msg.name || "tool"
81
+ const frPart = { functionResponse: { name: fnName, response: { output: msg.content } } }
82
+ const last = rawContents[rawContents.length - 1]
83
+ if (last?.role === "user" && Array.isArray(last.parts)) {
84
+ last.parts.push(frPart)
85
+ } else {
86
+ rawContents.push({ role: "user", parts: [frPart] })
87
+ }
88
+ }
89
+ }
90
+
91
+ // Gemini constraint enforcement
92
+ const contents: any[] = rawContents
93
+
94
+ while (contents.length > 0 && contents[0].role === "model") {
95
+ log.warn(`[llm-client] Gemini: removed leading model turn (no preceding user turn)`)
96
+ contents.shift()
97
+ }
98
+
99
+ let changed = true
100
+ let safetyLimit = 10
101
+ while (changed && safetyLimit-- > 0) {
102
+ changed = false
103
+
104
+ for (let i = 0; i < contents.length; i++) {
105
+ const turn = contents[i]
106
+ const prev = i > 0 ? contents[i - 1] : null
107
+
108
+ // INV-3: merge consecutive model turns
109
+ if (turn.role === "model" && prev?.role === "model") {
110
+ prev.parts.push(...(turn.parts ?? []))
111
+ contents.splice(i, 1)
112
+ i--
113
+ changed = true
114
+ continue
115
+ }
116
+
117
+ // INV-1: model(fc) must come after user
118
+ if (turn.role === "model") {
119
+ const hasFc = turn.parts?.some((p: any) => p.functionCall)
120
+ if (hasFc && prev?.role !== "user") {
121
+ turn.parts = (turn.parts ?? []).filter((p: any) => !p.functionCall)
122
+ log.warn(`[llm-client] Gemini: stripped functionCall not after user turn (i=${i})`)
123
+ if (turn.parts.length === 0) { contents.splice(i, 1); i-- }
124
+ changed = true
125
+ continue
126
+ }
127
+ }
128
+
129
+ // INV-2: user(fr) must come after model(fc)
130
+ if (turn.role === "user") {
131
+ const hasFr = turn.parts?.some((p: any) => p.functionResponse)
132
+ const prevHasFc = prev?.role === "model" && prev?.parts?.some((p: any) => p.functionCall)
133
+ if (hasFr && !prevHasFc) {
134
+ turn.parts = (turn.parts ?? []).filter((p: any) => !p.functionResponse)
135
+ log.warn(`[llm-client] Gemini: stripped orphaned functionResponse (i=${i})`)
136
+ if (turn.parts.length === 0) { contents.splice(i, 1); i-- }
137
+ changed = true
138
+ continue
139
+ }
140
+ }
141
+ }
142
+ }
143
+
144
+ if (safetyLimit <= 0) {
145
+ log.error(`[llm-client] Gemini: constraint enforcement loop exhausted — message history may still violate Gemini constraints`)
146
+ }
147
+
148
+ const config: any = {}
149
+ if (systemText) config.systemInstruction = systemText
150
+ if (options.maxTokens) config.maxOutputTokens = options.maxTokens
151
+ if (options.temperature !== undefined) config.temperature = options.temperature
152
+ if (options.tools?.length) {
153
+ config.tools = [{
154
+ functionDeclarations: options.tools.map((t) => ({
155
+ name: t.function.name,
156
+ description: t.function.description,
157
+ parameters: t.function.parameters,
158
+ })),
159
+ }]
160
+ }
161
+
162
+ log.info(`[llm-client] gemini/${options.model} — ${contents.length} turns, ${options.tools?.length ?? 0} tools`)
163
+
164
+ const response = await ai.models.generateContent({ model: options.model, contents, config })
165
+
166
+ const candidate = response.candidates?.[0]
167
+
168
+ // Handle safety blocks explicitly
169
+ if (candidate?.finishReason === "SAFETY") {
170
+ log.warn(`[llm-client] Gemini: response blocked by safety filters (model=${options.model})`)
171
+ return {
172
+ content: "[Response blocked by Gemini safety filters]",
173
+ stop_reason: "stop",
174
+ usage: response.usageMetadata ? {
175
+ input_tokens: response.usageMetadata.promptTokenCount ?? 0,
176
+ output_tokens: 0,
177
+ } : undefined,
178
+ }
179
+ }
180
+
181
+ const parts: any[] = candidate?.content?.parts ?? []
182
+
183
+ let content = ""
184
+ const tool_calls: LLMToolCall[] = []
185
+
186
+ for (const part of parts) {
187
+ if (part.text) content += part.text
188
+ if (part.functionCall) {
189
+ tool_calls.push({
190
+ id: crypto.randomUUID(),
191
+ type: "function",
192
+ function: { name: part.functionCall.name, arguments: JSON.stringify(part.functionCall.args ?? {}) },
193
+ thought_signature: part.thoughtSignature ?? undefined,
194
+ })
195
+ }
196
+ }
197
+
198
+ const stop_reason: LLMResponse["stop_reason"] =
199
+ tool_calls.length > 0 ? "tool_calls"
200
+ : candidate?.finishReason === "MAX_TOKENS" ? "max_tokens"
201
+ : "stop"
202
+
203
+ const usageMeta = response.usageMetadata
204
+ return {
205
+ content,
206
+ tool_calls: tool_calls.length ? tool_calls : undefined,
207
+ stop_reason,
208
+ usage: usageMeta ? {
209
+ input_tokens: usageMeta.promptTokenCount ?? 0,
210
+ output_tokens: usageMeta.candidatesTokenCount ?? 0,
211
+ thinking_tokens: (usageMeta as any).thoughtsTokenCount ?? 0,
212
+ } : undefined,
213
+ }
214
+ }
215
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * AgentRunner — thin wrapper over the native AgentLoop.
3
+ *
4
+ * Keeps the same public API (generate()) so server.ts doesn't need changes.
5
+ * Internally uses agent-loop.ts instead of LangGraph.
6
+ */
7
+
8
+ import type { Config } from "../../config/loader.ts"
9
+ import { logger } from "../../utils/logger.ts"
10
+ import { getDb } from "../../storage/SQLiteStorage.ts"
11
+ import { getAgentLoop } from "../AgentRunner.ts"
12
+ import { resolveUserId, resolveAgentId } from "../../storage/onboarding.ts"
13
+ import type { ContentPart } from "../../multimodal/types.ts"
14
+
15
+ export type Provider = "openai" | "anthropic" | "gemini" | "mistral" | "kimi" | "ollama" | "openrouter" | "deepseek" | "nvidia"
16
+
17
+ import type { StepEvent } from "../AgentRunner.ts"
18
+
19
+ export interface ModelOptions {
20
+ provider?: Provider
21
+ model?: string
22
+ maxTokens?: number
23
+ temperature?: number
24
+ system?: string
25
+ messages: Array<{ role: string; content: string | ContentPart[] }>
26
+ tools?: Record<string, any>
27
+ maxSteps?: number
28
+ onToken?: (token: string) => void
29
+ onStep?: (step: StepEvent) => Promise<void>
30
+ threadId?: string
31
+ userId?: string
32
+ channel?: string
33
+ rawUserMessage?: string
34
+ signal?: AbortSignal
35
+ }
36
+
37
+ export interface ModelResponse {
38
+ content: string
39
+ toolCalls?: Array<{
40
+ id: string
41
+ name: string
42
+ args: Record<string, unknown>
43
+ }>
44
+ usage?: {
45
+ promptTokens: number
46
+ completionTokens: number
47
+ totalTokens: number
48
+ }
49
+ finishReason?: string
50
+ }
51
+
52
+ export class AgentRunner {
53
+ private config: Config
54
+
55
+ constructor(config: Config) {
56
+ this.config = config
57
+ }
58
+
59
+ async generate(options: ModelOptions): Promise<ModelResponse> {
60
+ const db = getDb()
61
+ // Resolve agentId from database (coordinator or first enabled)
62
+ const agentId = resolveAgentId(null) || "main"
63
+
64
+ // Resolve userId from database
65
+ const userId = options.userId || resolveUserId({})
66
+ if (!userId) {
67
+ throw new Error("No userId provided. Please complete onboarding first.")
68
+ }
69
+ const threadId = options.threadId || userId
70
+
71
+ const agentLoop = getAgentLoop()
72
+ if (!agentLoop) {
73
+ throw new Error("AgentLoop not initialized")
74
+ }
75
+
76
+ let lastAgentContent = ""
77
+ let accumulatedAgentContent = "" // Accumulate content from all agent chunks
78
+ let toolCalls: ModelResponse["toolCalls"] = []
79
+ let totalInputTokens = 0
80
+ let totalOutputTokens = 0
81
+
82
+ try {
83
+ const stream = agentLoop.stream(
84
+ { messages: options.messages },
85
+ {
86
+ configurable: {
87
+ thread_id: threadId,
88
+ agent_id: agentId,
89
+ user_id: userId,
90
+ // system_prompt intentionally omitted — context-compiler builds it
91
+ channel: options.channel,
92
+ raw_user_message: options.rawUserMessage,
93
+ },
94
+ signal: options.signal,
95
+ }
96
+ )
97
+
98
+ let chunkCount = 0
99
+ for await (const chunk of stream) {
100
+ chunkCount++
101
+
102
+ if (chunk.agent?.messages) {
103
+ const lastMsg = chunk.agent.messages[chunk.agent.messages.length - 1]
104
+ const hasToolCalls = (lastMsg as any)?.tool_calls?.length > 0
105
+ const contentLen = lastMsg?.content?.length ?? 0
106
+ const contentType = typeof lastMsg?.content
107
+ logger.info(
108
+ `[STREAM] chunk#${chunkCount} agent: contentLen=${contentLen} hasToolCalls=${hasToolCalls} contentType=${contentType}`
109
+ )
110
+
111
+ if (lastMsg?.content) {
112
+ const content = typeof lastMsg.content === "string"
113
+ ? lastMsg.content
114
+ : Array.isArray(lastMsg.content)
115
+ ? lastMsg.content.filter(p => p.type === "text").map(p => (p as any).text).join("\n")
116
+ : ""
117
+ lastAgentContent = content
118
+ // Accumulate non-empty content that's not just whitespace
119
+ if (content && content.trim().length > 0) {
120
+ accumulatedAgentContent += (accumulatedAgentContent ? "\n" : "") + content
121
+ logger.debug(`[STREAM] Accumulated content: total length=${accumulatedAgentContent.length}`)
122
+ } else {
123
+ logger.debug(`[STREAM] Content empty or whitespace only, skipping accumulation`)
124
+ }
125
+ if (options.onToken) options.onToken(content)
126
+ } else {
127
+ logger.debug(`[STREAM] No content in chunk, lastMsg.content is falsy`)
128
+ }
129
+
130
+ if (hasToolCalls) {
131
+ toolCalls = (lastMsg as any).tool_calls.map((tc: any) => ({
132
+ id: tc.id || tc.function?.name,
133
+ name: tc.function?.name || tc.name,
134
+ args: tc.function?.arguments
135
+ ? (typeof tc.function.arguments === "string"
136
+ ? JSON.parse(tc.function.arguments)
137
+ : tc.function.arguments)
138
+ : {},
139
+ }))
140
+
141
+ const narration = lastMsg?.content || ""
142
+ if (options.onStep && narration) {
143
+ await options.onStep({ type: "text", message: narration })
144
+ }
145
+ if (options.onStep) {
146
+ for (const tc of toolCalls) {
147
+ await options.onStep({
148
+ type: "tool_call",
149
+ toolName: tc.name,
150
+ message: `Calling tool: \`${tc.name}\``,
151
+ })
152
+ }
153
+ }
154
+ }
155
+ }
156
+
157
+ if (chunk.tools?.messages) {
158
+ const lastMsg = chunk.tools.messages[chunk.tools.messages.length - 1]
159
+ if (lastMsg?.content && options.onStep) {
160
+ await options.onStep({
161
+ type: "tool_result",
162
+ message: typeof lastMsg.content === "string" ? lastMsg.content : "",
163
+ })
164
+ }
165
+ }
166
+
167
+ if (chunk.usage) {
168
+ totalInputTokens += chunk.usage.input_tokens
169
+ totalOutputTokens += chunk.usage.output_tokens
170
+ }
171
+ }
172
+
173
+ logger.debug(`[STREAM] done. totalChunks=${chunkCount} lastAgentContent length=${lastAgentContent.length}, accumulated length=${accumulatedAgentContent.length}`)
174
+
175
+ // Use accumulated content if lastAgentContent is empty (handles case where final chunk has no text)
176
+ const finalContent = lastAgentContent || accumulatedAgentContent
177
+
178
+ logger.info(`[STREAM] Returning response: finalContent length=${finalContent.length}, lastAgentContent length=${lastAgentContent.length}, accumulated length=${accumulatedAgentContent.length}`)
179
+
180
+ return {
181
+ content: finalContent,
182
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
183
+ usage: {
184
+ promptTokens: totalInputTokens,
185
+ completionTokens: totalOutputTokens,
186
+ totalTokens: totalInputTokens + totalOutputTokens,
187
+ },
188
+ finishReason: "stop",
189
+ }
190
+ } catch (error) {
191
+ logger.error("AgentRunner error:", error)
192
+ throw error
193
+ }
194
+ }
195
+ }
196
+
197
+ export function createAgentRunner(config: Config): AgentRunner {
198
+ return new AgentRunner(config)
199
+ }