@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,267 @@
1
+ /**
2
+ * AgentService — Wrapper del nuevo agent-loop nativo.
3
+ *
4
+ * Reemplaza la clase Agent legacy.
5
+ * Mantiene compatibilidad con server.ts pero usa el agent-loop nativo por debajo.
6
+ *
7
+ * Responsabilidades:
8
+ * - Cargar agente desde DB
9
+ * - Cargar ética desde DB
10
+ * - Obtener MCP Manager
11
+ * - Hot reload (MCP, skills, ethics)
12
+ * - Eventos (cron, etc.)
13
+ */
14
+
15
+ import { getDb } from "../storage/SQLiteStorage.ts"
16
+ import { logger } from "../utils/logger.ts"
17
+ import { buildSystemPromptWithProjects } from "./PromptBuilder"
18
+ import { getAgentLoop, rebuildAgentLoop } from "./AgentRunner"
19
+ import type { MCPClientManager } from "../mcp/index.ts"
20
+ import { resolveAgentId, resolveUserId } from "../storage/onboarding.ts"
21
+ import { getMCPManager as getSingletonMCPManager } from "../mcp/singleton.ts"
22
+ import type { ContentPart } from "./providers/LLMClient"
23
+
24
+ const log = logger.child("agent-service")
25
+
26
+ // Event handler types
27
+ type CronHandler = (sessionId: string, task: string, jobId?: string, context?: any) => Promise<void>
28
+
29
+ export interface AgentServiceConfig {
30
+ agentId?: string
31
+ workspacePath?: string
32
+ }
33
+
34
+ export interface AgentDBRecord {
35
+ id: string
36
+ user_id: string
37
+ name: string
38
+ description: string | null
39
+ system_prompt: string | null
40
+ tone: string | null
41
+ role: string
42
+ status: string
43
+ enabled: number
44
+ provider_id: string
45
+ model_id: string
46
+ tools_json: string | null
47
+ skills_json: string | null
48
+ parent_id: string | null
49
+ max_iterations: number
50
+ headers_encrypted: string | null
51
+ headers_iv: string | null
52
+ created_at: number
53
+ updated_at: number
54
+ }
55
+
56
+ export class AgentService {
57
+ private agentId: string
58
+ private workspacePath: string
59
+ private mcpManager: MCPClientManager | null = null
60
+ private cronHandlers: CronHandler[] = []
61
+ private initialized: boolean = false
62
+
63
+ constructor(config?: AgentServiceConfig) {
64
+ // Resolve agentId from database if not provided
65
+ this.agentId = config?.agentId || resolveAgentId(null) || "main"
66
+ this.workspacePath = config?.workspacePath || ""
67
+ }
68
+
69
+ /**
70
+ * Inicializa el servicio del agente
71
+ * - Carga el MCP Manager
72
+ * - Configura el supervisor graph
73
+ */
74
+ async initialize(): Promise<void> {
75
+ if (this.initialized) {
76
+ log.debug("AgentService already initialized")
77
+ return
78
+ }
79
+
80
+ try {
81
+ // Obtener MCP Manager del agent loop
82
+ const agentLoop = getAgentLoop()
83
+ if (agentLoop) {
84
+ // MCP Manager se inicializa en el agent-loop
85
+ log.info("AgentService: MCP Manager available from agent-loop")
86
+ }
87
+
88
+ this.initialized = true
89
+ log.info(`AgentService initialized for agent=${this.agentId}`)
90
+ } catch (error) {
91
+ log.error(`Failed to initialize AgentService: ${(error as Error).message}`)
92
+ throw error
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Obtiene el registro del agente desde la DB
98
+ */
99
+ async getAgent(agentId?: string): Promise<AgentDBRecord | null> {
100
+ const db = getDb()
101
+ const id = agentId || this.agentId
102
+
103
+ const agent = db.query<any, [string]>(
104
+ "SELECT * FROM agents WHERE id = ? LIMIT 1"
105
+ ).get(id) as AgentDBRecord | undefined
106
+
107
+ return agent || null
108
+ }
109
+
110
+ /**
111
+ * Obtiene la ética desde la DB
112
+ */
113
+ async getEthics(): Promise<string> {
114
+ const db = getDb()
115
+ const ethics = db.query<any, []>(
116
+ "SELECT content FROM ethics WHERE active = 1 LIMIT 1"
117
+ ).get() as { content: string } | undefined
118
+
119
+ return ethics?.content || ""
120
+ }
121
+
122
+ /**
123
+ * Obtiene el MCP Manager
124
+ */
125
+ getMCPManager(): MCPClientManager | null {
126
+ const agentLoop = getAgentLoop()
127
+ if (agentLoop && (agentLoop as any).mcpManager) {
128
+ return (agentLoop as any).mcpManager as MCPClientManager
129
+ }
130
+ // Fallback to singleton
131
+ return getSingletonMCPManager()
132
+ }
133
+
134
+ /**
135
+ * Recarga la configuración del MCP
136
+ */
137
+ async reloadMCP(): Promise<void> {
138
+ log.info("Reloading MCP configuration...")
139
+ const mcp = this.getMCPManager()
140
+ if (mcp) {
141
+ await mcp.reconnectAll().catch(err => {
142
+ log.warn(`Failed to reconnect MCP: ${(err as Error).message}`)
143
+ })
144
+ }
145
+ log.info("MCP reloaded")
146
+ }
147
+
148
+ /**
149
+ * Recarga los skills desde la DB
150
+ */
151
+ async reloadSkills(): Promise<void> {
152
+ log.info("Reloading skills...")
153
+ const { syncSkillsToFTS } = await import("./selectors/index.ts")
154
+ await syncSkillsToFTS()
155
+ log.info("Skills reloaded")
156
+ }
157
+
158
+ /**
159
+ * Recarga la ética desde la DB
160
+ */
161
+ async reloadEthics(): Promise<void> {
162
+ log.info("Reloading ethics...")
163
+ // La ética se carga automáticamente en buildSystemPrompt()
164
+ // No hay acción necesaria aquí
165
+ log.info("Ethics reloaded (will be picked up on next agent call)")
166
+ }
167
+
168
+ /**
169
+ * Recarga el soul (system prompt del agente)
170
+ */
171
+ async reloadSoul(): Promise<void> {
172
+ log.info("Reloading soul...")
173
+ // El soul se carga automáticamente desde DB en buildSystemPrompt()
174
+ log.info("Soul reloaded (will be picked up on next agent call)")
175
+ }
176
+
177
+ /**
178
+ * Recarga la configuración del usuario
179
+ */
180
+ async reloadUser(): Promise<void> {
181
+ log.info("Reloading user configuration...")
182
+ // La configuración del usuario se carga desde DB en buildSystemPrompt()
183
+ log.info("User configuration reloaded (will be picked up on next agent call)")
184
+ }
185
+
186
+ /**
187
+ * Actualiza la configuración del agente
188
+ */
189
+ async updateConfig(config: any): Promise<void> {
190
+ log.info("Updating agent configuration...")
191
+ // La configuración ahora se carga desde DB dinámicamente
192
+ // No hay acción necesaria aquí
193
+ log.info("Configuration updated (will be picked up from DB)")
194
+ }
195
+
196
+ /**
197
+ * Recarga el agente (hot reload)
198
+ */
199
+ async reload(): Promise<void> {
200
+ log.info("Reloading agent...")
201
+ await this.reloadMCP()
202
+ await this.reloadSkills()
203
+ await this.reloadEthics()
204
+ log.info("Agent reloaded")
205
+ }
206
+
207
+ /**
208
+ * Registra un handler para eventos cron
209
+ */
210
+ on(event: 'cron', handler: CronHandler): void {
211
+ if (event === 'cron') {
212
+ this.cronHandlers.push(handler)
213
+ log.debug(`Registered cron handler, total=${this.cronHandlers.length}`)
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Emite un evento cron
219
+ */
220
+ emit(event: 'cron', sessionId: string, task: string, jobId?: string, context?: any): void {
221
+ if (event === 'cron') {
222
+ log.debug(`Emitting cron event: task=${task}, sessionId=${sessionId}, jobId=${jobId}`)
223
+ for (const handler of this.cronHandlers) {
224
+ handler(sessionId, task, jobId, context).catch(err => {
225
+ log.error(`Cron handler error: ${(err as Error).message}`)
226
+ })
227
+ }
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Obtiene el system prompt para un agente
233
+ */
234
+ async getSystemPrompt(agentId?: string, userId?: string): Promise<string> {
235
+ const id = agentId || this.agentId
236
+ const uid = userId || resolveUserId({}) || "default"
237
+ return buildSystemPromptWithProjects({ agentId: id, userId: uid })
238
+ }
239
+
240
+ /**
241
+ * Ejecuta un agente con un mensaje
242
+ */
243
+ async runAgent(message: string | ContentPart[], threadId: string, userId?: string): Promise<string> {
244
+ const { runAgentIsolated } = await import("./AgentRunner.ts")
245
+ const result = await runAgentIsolated({
246
+ agentId: this.agentId,
247
+ taskDescription: message,
248
+ threadId,
249
+ })
250
+ return result
251
+ }
252
+ }
253
+
254
+ // Singleton para compatibilidad
255
+ let _agentService: AgentService | null = null
256
+
257
+ export function getAgentService(): AgentService {
258
+ if (!_agentService) {
259
+ _agentService = new AgentService()
260
+ }
261
+ return _agentService
262
+ }
263
+
264
+ export function createAgentService(config?: AgentServiceConfig): AgentService {
265
+ _agentService = new AgentService(config)
266
+ return _agentService
267
+ }
@@ -0,0 +1,133 @@
1
+ import type { Config } from "../config/loader.ts";
2
+ import { logger } from "../utils/logger.ts";
3
+ import { hashObject } from "../utils/crypto.ts";
4
+
5
+ interface ToolCallRecord {
6
+ toolName: string;
7
+ argsHash: string;
8
+ errorMessage?: string;
9
+ timestamp: number;
10
+ }
11
+
12
+ interface StuckLoopState {
13
+ detected: boolean;
14
+ toolName: string;
15
+ count: number;
16
+ lastError?: string;
17
+ }
18
+
19
+ export class StuckLoopDetector {
20
+ private log = logger.child("stuck-loop");
21
+ private history: Map<string, ToolCallRecord[]> = new Map();
22
+ private readonly maxHistoryPerSession = 50;
23
+ private readonly triggerThreshold = 3;
24
+
25
+ constructor(_config: Config) {}
26
+
27
+ recordToolCall(
28
+ sessionId: string,
29
+ toolName: string,
30
+ args: Record<string, unknown>,
31
+ error?: string
32
+ ): void {
33
+ let sessionHistory = this.history.get(sessionId);
34
+ if (!sessionHistory) {
35
+ sessionHistory = [];
36
+ this.history.set(sessionId, sessionHistory);
37
+ }
38
+
39
+ const record: ToolCallRecord = {
40
+ toolName,
41
+ argsHash: hashObject(args),
42
+ errorMessage: error,
43
+ timestamp: Date.now(),
44
+ };
45
+
46
+ sessionHistory.push(record);
47
+
48
+ if (sessionHistory.length > this.maxHistoryPerSession) {
49
+ sessionHistory.shift();
50
+ }
51
+
52
+ this.log.debug(`Recorded tool call: ${toolName} for session ${sessionId}`);
53
+ }
54
+
55
+ check(sessionId: string): StuckLoopState {
56
+ const sessionHistory = this.history.get(sessionId) ?? [];
57
+
58
+ if (sessionHistory.length < this.triggerThreshold) {
59
+ return { detected: false, toolName: "", count: 0 };
60
+ }
61
+
62
+ const recent = sessionHistory.slice(-10);
63
+ const counts = new Map<string, { count: number; error?: string }>();
64
+
65
+ for (const record of recent) {
66
+ const key = `${record.toolName}:${record.argsHash}`;
67
+ const existing = counts.get(key);
68
+
69
+ if (existing) {
70
+ existing.count++;
71
+ if (record.errorMessage) {
72
+ existing.error = record.errorMessage;
73
+ }
74
+ } else {
75
+ counts.set(key, { count: 1, error: record.errorMessage });
76
+ }
77
+ }
78
+
79
+ for (const [key, data] of counts) {
80
+ if (data.count >= this.triggerThreshold && data.error) {
81
+ const toolName = key.split(":")[0] ?? "unknown";
82
+
83
+ this.log.warn(`Stuck loop detected: ${toolName} called ${data.count} times with same args and error`);
84
+
85
+ return {
86
+ detected: true,
87
+ toolName,
88
+ count: data.count,
89
+ lastError: data.error,
90
+ };
91
+ }
92
+ }
93
+
94
+ return { detected: false, toolName: "", count: 0 };
95
+ }
96
+
97
+ getInterventionMessage(state: StuckLoopState): string | null {
98
+ if (!state.detected) return null;
99
+
100
+ if (state.count >= this.triggerThreshold + 1) {
101
+ return `CRITICAL: You have called ${state.toolName} ${state.count} times with the same arguments and it keeps failing with: "${state.lastError}". The user has been notified. You MUST try a completely different approach or ask the user for guidance.`;
102
+ }
103
+
104
+ return `WARNING: You have called ${state.toolName} ${state.count} times with the same arguments and it keeps failing. You MUST try a completely different approach instead of repeating the same action.`;
105
+ }
106
+
107
+ clear(sessionId: string): void {
108
+ this.history.delete(sessionId);
109
+ this.log.debug(`Cleared stuck loop history for session ${sessionId}`);
110
+ }
111
+
112
+ prune(maxAgeMs: number = 30 * 60 * 1000): number {
113
+ const now = Date.now();
114
+ let pruned = 0;
115
+
116
+ for (const [sessionId, history] of this.history) {
117
+ const filtered = history.filter(r => now - r.timestamp < maxAgeMs);
118
+
119
+ if (filtered.length === 0) {
120
+ this.history.delete(sessionId);
121
+ pruned++;
122
+ } else if (filtered.length !== history.length) {
123
+ this.history.set(sessionId, filtered);
124
+ }
125
+ }
126
+
127
+ return pruned;
128
+ }
129
+ }
130
+
131
+ export function createStuckLoopDetector(config: Config): StuckLoopDetector {
132
+ return new StuckLoopDetector(config);
133
+ }
@@ -0,0 +1,12 @@
1
+ export * from "./AgentRunner.ts";
2
+ export * from "./Compaction.ts";
3
+ export * from "./ContextCompiler.ts";
4
+ export * from "./ContextGuard.ts";
5
+ export * from "./ConversationStore.ts";
6
+ export * from "./Hooks.ts";
7
+ export * from "./NativeTools.ts";
8
+ export * from "./PromptBuilder.ts";
9
+ export * from "./Service.ts";
10
+ export * from "./StuckLoop.ts";
11
+ export * from "./providers/index.ts";
12
+ export * from "./selectors/index.ts";
@@ -0,0 +1,149 @@
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
+ * everything else → openai npm package (OpenAI-compatible endpoint)
8
+ *
9
+ * Public interface (LLMMessage, callLLM, resolveProviderConfig) is stable.
10
+ */
11
+
12
+ import { logger } from "../../utils/logger.ts"
13
+ import { GeminiProvider } from "./gemini.ts"
14
+ import { AnthropicProvider } from "./anthropic.ts"
15
+ import { OllamaProvider } from "./ollama.ts"
16
+ import { OpenAICompatProvider } from "./openai-compat.ts"
17
+ import type { LLMProvider } from "./interface.ts"
18
+
19
+ const log = logger.child("llm-client")
20
+
21
+ // ─── Canonical types ───────────────────────────────────────────────────────────
22
+
23
+ export interface LLMToolCall {
24
+ id: string
25
+ type: "function"
26
+ function: { name: string; arguments: string }
27
+ /** Gemini 3.x thought signature — must be round-tripped for tool-calling. */
28
+ thought_signature?: string
29
+ }
30
+
31
+ export type ContentPart =
32
+ | { type: "text"; text: string }
33
+ | { type: "image_url"; image_url: { url: string } }
34
+ | { type: "image_base64"; base64: string; mimeType: string }
35
+ | { type: "document"; base64: string; mimeType: string; fileName?: string }
36
+
37
+ export interface LLMMessage {
38
+ role: "system" | "user" | "assistant" | "tool"
39
+ content: string | ContentPart[]
40
+ tool_calls?: LLMToolCall[]
41
+ tool_call_id?: string
42
+ name?: string
43
+ /** Kimi K2 thinking mode — must be round-tripped when tool calls are present. */
44
+ reasoning_content?: string
45
+ }
46
+
47
+ export interface LLMToolDef {
48
+ type: "function"
49
+ function: {
50
+ name: string
51
+ description: string
52
+ parameters: Record<string, unknown>
53
+ }
54
+ }
55
+
56
+ export interface LLMCallOptions {
57
+ provider: string
58
+ model: string
59
+ apiKey: string
60
+ baseUrl?: string
61
+ numCtx?: number
62
+ messages: LLMMessage[]
63
+ tools?: LLMToolDef[]
64
+ temperature?: number
65
+ maxTokens?: number
66
+ numGpu?: number
67
+ onToken?: (token: string) => void
68
+ signal?: AbortSignal
69
+ /** Enable extended thinking for supported models (Anthropic Claude 3.7+). */
70
+ thinking?: { enabled: boolean; budget_tokens?: number }
71
+ }
72
+
73
+ export interface LLMResponse {
74
+ content: string
75
+ tool_calls?: LLMToolCall[]
76
+ stop_reason: "stop" | "tool_calls" | "max_tokens" | "error"
77
+ usage?: { input_tokens: number; output_tokens: number; thinking_tokens?: number }
78
+ /** Kimi K2 / DeepSeek thinking mode — must be round-tripped in assistant messages. */
79
+ reasoning_content?: string
80
+ /** Anthropic extended thinking content (not sent to LLM, for display only). */
81
+ thinking_content?: string
82
+ }
83
+
84
+ // ─── Provider factory ─────────────────────────────────────────────────────────
85
+
86
+ const GEMINI_PROVIDERS = new Set(["gemini", "google"])
87
+
88
+ const KNOWN_PROVIDERS = new Set(["anthropic", "gemini", "google", "ollama", "openai", "groq", "mistral", "openrouter", "deepseek", "kimi", "local-llama", "nvidia"])
89
+
90
+ function getProvider(provider: string): LLMProvider {
91
+ if (GEMINI_PROVIDERS.has(provider)) return new GeminiProvider()
92
+ if (provider === "anthropic") return new AnthropicProvider()
93
+ if (provider === "ollama") return new OllamaProvider()
94
+ if (!KNOWN_PROVIDERS.has(provider)) {
95
+ log.warn(`[llm-client] Unknown provider "${provider}" — falling back to OpenAI-compatible endpoint`)
96
+ }
97
+ return new OpenAICompatProvider()
98
+ }
99
+
100
+ // ─── Public API ────────────────────────────────────────────────────────────────
101
+
102
+ /**
103
+ * Call any LLM provider. Returns a canonical LLMResponse regardless of provider.
104
+ */
105
+ export async function callLLM(options: LLMCallOptions): Promise<LLMResponse> {
106
+ try {
107
+ return await getProvider(options.provider).call(options)
108
+ } catch (err) {
109
+ const msg = (err as Error).message
110
+ const cleanModel = options.model.replace(new RegExp(`^${options.provider}\\/`), "")
111
+ log.error(`[llm-client] Error calling ${options.provider}/${cleanModel}: ${msg}`, err)
112
+ return { content: `[LLM Error] ${msg}`, stop_reason: "error" }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Resolve provider config from DB (decrypts API key).
118
+ */
119
+ export async function resolveProviderConfig(
120
+ providerId: string,
121
+ modelId: string
122
+ ): Promise<Pick<LLMCallOptions, "provider" | "model" | "apiKey" | "baseUrl" | "numCtx" | "numGpu">> {
123
+ const { getDb } = await import("../../storage/SQLiteStorage.ts")
124
+ const { decryptApiKey } = await import("../../storage/crypto.ts")
125
+
126
+ const db = getDb()
127
+ const providerRow = db
128
+ .query<any, [string]>("SELECT * FROM providers WHERE id = ? AND enabled = 1")
129
+ .get(providerId)
130
+
131
+ let apiKey = ""
132
+ if (providerRow?.api_key_encrypted && providerRow?.api_key_iv) {
133
+ try {
134
+ apiKey = await decryptApiKey(providerRow.api_key_encrypted, providerRow.api_key_iv)
135
+ } catch { /* fall through to env var */ }
136
+ }
137
+ if (!apiKey) {
138
+ apiKey = process.env[`${providerId.toUpperCase()}_API_KEY`] || ""
139
+ }
140
+
141
+ return {
142
+ provider: providerId,
143
+ model: modelId,
144
+ apiKey,
145
+ baseUrl: providerRow?.base_url || undefined,
146
+ numCtx: providerRow?.num_ctx ?? undefined,
147
+ numGpu: providerRow?.num_gpu ?? undefined,
148
+ }
149
+ }