@johpaz/hive-sdk 0.0.12 → 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,699 @@
1
+ /**
2
+ * Agent Loop — native implementation, no LangGraph.
3
+ *
4
+ * Replaces supervisor.ts + graph.ts.
5
+ *
6
+ * Pattern:
7
+ * user message → context compiler → model call → [tool call → model call]* → response
8
+ *
9
+ * Exposes an async generator compatible with the existing providers/index.ts stream API:
10
+ * yield { agent: { messages: [AIMessage] } }
11
+ * yield { tools: { messages: [ToolMessage] } }
12
+ *
13
+ * Also used directly by runAgentIsolated() for worker tasks.
14
+ */
15
+
16
+ import { logger } from "../utils/logger.ts"
17
+ import { getDb } from "../storage/SQLiteStorage.ts"
18
+ import { callLLM, resolveProviderConfig, type LLMMessage } from "./providers/LLMClient"
19
+ import { addMessage } from "./ConversationStore"
20
+ import { saveTrace, recordLLMUsage } from "../ace/Tracer"
21
+ import { maybeCompact, clearOldToolResults } from "./Compaction"
22
+ import { emitCanvas } from "../canvas/emitter.ts"
23
+ import type { MCPClientManager } from "../mcp/index.ts"
24
+ import { compileContext } from "./ContextCompiler"
25
+ import { formatToolResult } from "../utils/toon.ts"
26
+ import { getAverageTokenCost } from "../storage/usage.ts"
27
+ import { resolveUserId, resolveAgentId } from "../storage/onboarding.ts"
28
+ import type { ContentPart } from "../multimodal/types.ts"
29
+
30
+ /**
31
+ * Execute a tool by name from the available tools list
32
+ * This is a local helper function since executeTool is not exported elsewhere
33
+ *
34
+ * Returns: JS object normal (se encodea solo al enviar al LLM)
35
+ */
36
+ async function executeTool(
37
+ allTools: Array<{ name: string; execute?: (params: Record<string, unknown>, config?: any) => Promise<unknown> }>,
38
+ toolName: string,
39
+ args: unknown,
40
+ config: { user_id?: string; thread_id?: string; channel?: string; workspace?: string | null }
41
+ ): Promise<unknown> {
42
+ const tool = allTools.find(t => t.name === toolName)
43
+ if (!tool?.execute) {
44
+ return { error: true, message: `Tool '${toolName}' not found or not executable` }
45
+ }
46
+ try {
47
+ const parsedArgs = typeof args === 'string' ? JSON.parse(args) : args
48
+ return await tool.execute(parsedArgs as Record<string, unknown>, { configurable: config })
49
+ } catch (err) {
50
+ return {
51
+ error: true,
52
+ tool: toolName,
53
+ message: (err as Error).message,
54
+ timestamp: new Date().toISOString(),
55
+ }
56
+ }
57
+ }
58
+
59
+ const log = logger.child("agent-loop")
60
+
61
+ // ─── Types ────────────────────────────────────────────────────────────────────
62
+
63
+ export interface AgentLoopOptions {
64
+ agentId: string
65
+ userMessage: string | ContentPart[]
66
+ threadId: string
67
+ channel?: string
68
+ mcpManager?: MCPClientManager | null
69
+ /** System prompt override (from server.ts config) */
70
+ systemPromptOverride?: string
71
+ /** Worker mode: isolated context + single-task execution */
72
+ isolated?: boolean
73
+ taskContext?: string | ContentPart[]
74
+ onStep?: (step: StepEvent) => Promise<void>
75
+ /** User ID for context propagation */
76
+ userId?: string
77
+ /** Abort signal to stop generation mid-execution */
78
+ signal?: AbortSignal
79
+ /** Clean text for FTS5 and tracing (extracted from userMessage if multimodal) */
80
+ rawUserMessage?: string
81
+ }
82
+
83
+ export interface StepEvent {
84
+ type: "text" | "tool_call" | "tool_result"
85
+ message: string
86
+ toolName?: string
87
+ isError?: boolean
88
+ }
89
+
90
+ // ─── Stream chunk types (compatible with providers/index.ts) ─────────────────
91
+
92
+ export interface StreamChunk {
93
+ agent?: { messages: any[] }
94
+ tools?: { messages: any[] }
95
+ usage?: { input_tokens: number; output_tokens: number }
96
+ }
97
+
98
+ // ─── Main agent loop ──────────────────────────────────────────────────────────
99
+
100
+ export async function* runAgent(
101
+ opts: AgentLoopOptions
102
+ ): AsyncGenerator<StreamChunk> {
103
+ const t0 = performance.now()
104
+ const db = getDb()
105
+
106
+ // Load agent config from DB
107
+ const agent = db.query<any, [string]>("SELECT * FROM agents WHERE id = ?").get(opts.agentId)
108
+ if (!agent) throw new Error(`Agent not found: ${opts.agentId}`)
109
+
110
+ const agentName = agent.name || opts.agentId
111
+ const maxIterations = agent.max_iterations || 10
112
+
113
+ // Resolve LLM provider config
114
+ const providerCfg = await resolveProviderConfig(
115
+ agent.provider_id || "openai",
116
+ agent.model_id || "gpt-4o-mini"
117
+ )
118
+
119
+ const cleanModel = providerCfg.model.replace(new RegExp(`^${providerCfg.provider}\\/`), "")
120
+ log.info(`[agent-loop] Starting: agent=${agentName} thread=${opts.threadId} provider=${providerCfg.provider}/${cleanModel}`)
121
+
122
+ emitCanvas("canvas:node_update", {
123
+ nodeId: opts.agentId,
124
+ changes: { status: "thinking" },
125
+ })
126
+
127
+ // Store the user message in conversation history
128
+ if (!opts.isolated) {
129
+ // If userMessage is multimodal, addMessage extracts text for history storage
130
+ addMessage(opts.threadId, "user", opts.userMessage, { channel: opts.channel })
131
+ // Run compaction if conversation history is getting large
132
+ await maybeCompact(
133
+ opts.threadId,
134
+ opts.channel && opts.userId
135
+ ? { channel: opts.channel, userId: opts.userId }
136
+ : undefined
137
+ )
138
+ }
139
+
140
+ // Compile context (system prompt + history + tools)
141
+ const ctx = await compileContext({
142
+ agentId: opts.agentId,
143
+ threadId: opts.threadId,
144
+ userMessage: opts.userMessage,
145
+ channel: opts.channel,
146
+ mcpManager: opts.mcpManager,
147
+ isolated: opts.isolated,
148
+ taskContext: opts.taskContext,
149
+ userId: opts.userId,
150
+ })
151
+
152
+ const systemPrompt = opts.systemPromptOverride || ctx.systemPrompt
153
+
154
+ // Build initial messages array for the model
155
+ let messages: LLMMessage[] = [
156
+ { role: "system", content: systemPrompt },
157
+ ...ctx.messages,
158
+ ]
159
+
160
+ // For isolated workers the user message is the task context, not from history
161
+ if (opts.isolated) {
162
+ messages.push({ role: "user", content: opts.userMessage })
163
+ }
164
+
165
+ let iterations = 0
166
+ let totalInputTokens = 0
167
+ let totalOutputTokens = 0
168
+ let finalContent = ""
169
+ // Loop detection: track last tool call signature to break identical consecutive calls
170
+ let lastToolSignature = ""
171
+ let consecutiveRepeat = 0
172
+ let loopDetected = false
173
+
174
+ // ── The loop ────────────────────────────────────────────────────────────
175
+ while (iterations < maxIterations) {
176
+ if (opts.signal?.aborted) {
177
+ log.info(`[agent-loop] Aborted by signal at iteration ${iterations}`)
178
+ finalContent = "Generación detenida."
179
+ break
180
+ }
181
+
182
+ iterations++
183
+
184
+ const response = await callLLM({
185
+ ...providerCfg,
186
+ messages: clearOldToolResults(messages) as LLMMessage[],
187
+ tools: ctx.tools.length > 0 ? ctx.tools : undefined,
188
+ })
189
+
190
+ // Accumulate usage
191
+ if (response.usage) {
192
+ totalInputTokens += response.usage.input_tokens
193
+ totalOutputTokens += response.usage.output_tokens
194
+ }
195
+
196
+ // Emit agent chunk (compatible with providers/index.ts)
197
+ const agentMsg: any = { content: response.content }
198
+ if (response.tool_calls?.length) agentMsg.tool_calls = response.tool_calls
199
+ yield { agent: { messages: [agentMsg] } }
200
+
201
+ // Notify onStep for narration text
202
+ if (opts.onStep && response.content) {
203
+ await opts.onStep({ type: "text", message: response.content })
204
+ }
205
+
206
+ // ── No tool calls → final response ──────────────────────────────────
207
+ if (!response.tool_calls?.length || response.stop_reason !== "tool_calls") {
208
+ finalContent = response.content?.trim() || ""
209
+ // Only save to history if we have real content; empty → synthesis block will handle it
210
+ if (finalContent && !opts.isolated) {
211
+ addMessage(opts.threadId, "assistant", finalContent)
212
+ }
213
+ break
214
+ }
215
+
216
+ // ── Tool calls → execute each tool ──────────────────────────────────
217
+ // Add assistant message with tool_calls to local messages array AND persist
218
+ messages.push({
219
+ role: "assistant",
220
+ content: response.content,
221
+ tool_calls: response.tool_calls,
222
+ reasoning_content: response.reasoning_content,
223
+ })
224
+ if (!opts.isolated) {
225
+ addMessage(opts.threadId, "assistant", response.content || "", {
226
+ channel: opts.channel,
227
+ tool_calls: response.tool_calls,
228
+ reasoning_content: response.reasoning_content,
229
+ })
230
+ }
231
+
232
+ for (const tc of response.tool_calls) {
233
+ const toolName = tc.function.name
234
+
235
+ emitCanvas("canvas:node_update", {
236
+ nodeId: opts.agentId,
237
+ changes: { status: "tool_call", currentTool: toolName },
238
+ })
239
+
240
+ if (opts.onStep) {
241
+ if (response.content) {
242
+ await opts.onStep({ type: "text", message: response.content })
243
+ }
244
+ await opts.onStep({
245
+ type: "tool_call",
246
+ toolName,
247
+ message: `Calling tool: \`${toolName}\``,
248
+ })
249
+ }
250
+
251
+ const tTool = performance.now()
252
+ const toolResultJS = await executeTool(
253
+ ctx.allTools,
254
+ toolName,
255
+ tc.function.arguments,
256
+ {
257
+ user_id: opts.userId,
258
+ thread_id: opts.threadId,
259
+ channel: opts.channel,
260
+ workspace: agent.workspace ?? null,
261
+ }
262
+ )
263
+ const toolMs = Math.round(performance.now() - tTool)
264
+
265
+ // Encode TOON only for LLM consumption (with cost calculation)
266
+ const toolResultLLM = formatToolResult(toolResultJS, cleanModel)
267
+
268
+ log.info(`[agent-loop] Tool ${toolName} completed in ${toolMs}ms`)
269
+
270
+ // Log tool result preview (truncated to avoid flooding logs)
271
+ const resultPreview = toolResultLLM.length > 500
272
+ ? toolResultLLM.substring(0, 500) + `… (+${toolResultLLM.length - 500} chars)`
273
+ : toolResultLLM
274
+ log.info(`[agent-loop] Tool result [${toolName}]: ${resultPreview}`)
275
+
276
+ // Extract text for trace summary
277
+ const textMessage = typeof opts.userMessage === "string"
278
+ ? opts.userMessage
279
+ : Array.isArray(opts.userMessage)
280
+ ? opts.userMessage.filter(p => p.type === "text").map(p => (p as any).text).join("\n")
281
+ : String(opts.userMessage)
282
+
283
+ // Clean timestamp from message for trace
284
+ const cleanMessage = textMessage.replace(/^\[Timestamp:.*?\]\n/, "")
285
+
286
+ // Save tool call trace
287
+ saveTrace({
288
+ threadId: opts.threadId,
289
+ agentId: opts.agentId,
290
+ agentName,
291
+ toolUsed: toolName,
292
+ inputSummary: `${cleanMessage.substring(0, 200)} → ${toolName}`,
293
+ outputSummary: toolResultLLM.substring(0, 300),
294
+ success: !toolResultLLM.startsWith("[Tool Error]"),
295
+ errorMessage: toolResultLLM.startsWith("[Tool Error]") ? toolResultLLM : null,
296
+ durationMs: toolMs,
297
+ })
298
+
299
+ // Emit tool chunk (TOON encoded for LLM)
300
+ yield { tools: { messages: [{ content: toolResultLLM, tool_call_id: tc.id }] } }
301
+
302
+ if (opts.onStep) {
303
+ await opts.onStep({ type: "tool_result", message: toolResultLLM })
304
+ }
305
+
306
+ // Add tool result to messages for next model call AND persist (TOON encoded)
307
+ messages.push({
308
+ role: "tool",
309
+ content: toolResultLLM,
310
+ tool_call_id: tc.id,
311
+ })
312
+ if (!opts.isolated) {
313
+ addMessage(opts.threadId, "tool", toolResultLLM, {
314
+ channel: opts.channel,
315
+ tool_call_id: tc.id,
316
+ })
317
+ }
318
+
319
+ // Dynamic tool injection: when search_knowledge finds tools (native or MCP), add them to ctx.tools
320
+ if (toolName === "search_knowledge") {
321
+ // Use JS object directly (no parse needed)
322
+ try {
323
+ const result = toolResultJS as any
324
+ const foundTools: Array<{ name: string }> = result?.tools ?? []
325
+ const foundMcpTools: Array<{ tool_name: string; full_name?: string; id?: string }> = result?.toolsmcp ?? []
326
+ const currentToolNames = new Set(ctx.tools.map((t: any) => t.function?.name))
327
+
328
+ // Track which tools were injected for skill lookup
329
+ const injectedTools: string[] = []
330
+
331
+ // Inject native tools
332
+ for (const found of foundTools) {
333
+ if (!currentToolNames.has(found.name)) {
334
+ const nativeTool = ctx.allTools.find(t => t.name === found.name)
335
+ if (nativeTool) {
336
+ ctx.tools.push({
337
+ type: "function",
338
+ function: {
339
+ name: nativeTool.name,
340
+ description: (nativeTool as any).description ?? "",
341
+ parameters: (nativeTool as any).parameters ?? { type: "object", properties: {} },
342
+ },
343
+ })
344
+ log.info(`[agent-loop] Injected discovered native tool into loadout: ${nativeTool.name}`)
345
+ currentToolNames.add(found.name)
346
+ injectedTools.push(nativeTool.name)
347
+ }
348
+ }
349
+ }
350
+
351
+ // Inject MCP tools discovered via search_knowledge(type="mcp")
352
+ for (const found of foundMcpTools) {
353
+ // Use full_name (sanitized compound id) because ctx.allTools stores MCP tools
354
+ // under the sanitized name (e.g. "Instagram__mis_estadisticas_de_instagram"),
355
+ // NOT the original tool_name (e.g. "mis estadisticas de instagram").
356
+ const mcpFullName = found.full_name || found.id
357
+ log.debug(`[agent-loop] MCP discovery candidate: tool_name="${found.tool_name}", full_name="${found.full_name}", id="${found.id}", resolved="${mcpFullName}"`)
358
+ if (!currentToolNames.has(mcpFullName)) {
359
+ const mcpTool = ctx.allTools.find(t => t.name === mcpFullName)
360
+ if (mcpTool) {
361
+ ctx.tools.push({
362
+ type: "function",
363
+ function: {
364
+ name: mcpTool.name,
365
+ description: (mcpTool as any).description ?? "",
366
+ parameters: (mcpTool as any).parameters ?? { type: "object", properties: {} },
367
+ },
368
+ })
369
+ log.info(`[agent-loop] Injected discovered MCP tool into loadout: ${mcpTool.name}`)
370
+ currentToolNames.add(mcpFullName)
371
+ } else {
372
+ log.warn(`[agent-loop] MCP tool "${mcpFullName}" not found in allTools (available MCP: ${ctx.allTools.filter(t => t.name.includes('__')).map(t => t.name).join(', ')})`)
373
+ }
374
+ }
375
+ }
376
+
377
+ // Inject skills associated with the injected tools
378
+ if (injectedTools.length > 0) {
379
+ try {
380
+ const db = getDb()
381
+ // Find skills that use any of the injected tools
382
+ const placeholders = injectedTools.map(() => "?").join(",")
383
+ const skillsWithTools = db.query(`
384
+ SELECT DISTINCT s.name, s.body, s.tools
385
+ FROM skills s
386
+ WHERE s.active = 1
387
+ AND (
388
+ ${injectedTools.map(() => `s.tools LIKE ?`).join(" OR ")}
389
+ )
390
+ `).all(...injectedTools.map(t => `%${t}%`)) as Array<{ name: string; body: string; tools: string }>
391
+
392
+ // Filter to only skills that actually contain the tools (not partial matches)
393
+ const matchingSkills = skillsWithTools.filter(s => {
394
+ const skillTools = s.tools?.split(",").map(t => t.trim()) ?? []
395
+ return injectedTools.some(injected => skillTools.includes(injected))
396
+ })
397
+
398
+ if (matchingSkills.length > 0) {
399
+ const skillSection = matchingSkills
400
+ .map(s => `## Skill: ${s.name}\n${s.body}`)
401
+ .join("\n\n")
402
+
403
+ // Add skill instructions to system prompt (first message)
404
+ const systemMsg = messages.find(m => m.role === "system")
405
+ if (systemMsg && typeof systemMsg.content === "string") {
406
+ // Check if we already added this skill
407
+ const existingSkillNames = new Set(
408
+ (systemMsg.content.match(/## Skill: ([^\n]+)/g) || [])
409
+ .map(m => m.replace("## Skill: ", "").trim())
410
+ )
411
+
412
+ const newSkills = matchingSkills.filter(s => !existingSkillNames.has(s.name))
413
+ if (newSkills.length > 0) {
414
+ const newSkillSection = newSkills
415
+ .map(s => `## Skill: ${s.name}\n${s.body}`)
416
+ .join("\n\n")
417
+
418
+ systemMsg.content += `\n\n--- SKILL INSTRUCTIONS (Auto-loaded) ---\n${newSkillSection}`
419
+ log.info(`[agent-loop] Injected ${newSkills.length} skill(s) for tools: ${newSkills.map(s => s.name).join(", ")}`)
420
+ }
421
+ }
422
+ }
423
+ } catch (skillErr) {
424
+ log.warn(`[agent-loop] Failed to inject skills for tools: ${(skillErr as Error).message}`)
425
+ }
426
+ }
427
+ } catch (err) {
428
+ log.warn(`[agent-loop] search_knowledge tool injection failed: ${(err as Error).message}`)
429
+ }
430
+
431
+ // Enrich the tool result with skill instructions and playbook rules
432
+ try {
433
+ const result = toolResultJS as any
434
+ const foundSkills: Array<{ name: string; body?: string }> = result?.skills ?? []
435
+ const foundPlaybook: Array<{ rule: string; category?: string }> = result?.playbook ?? []
436
+
437
+ if (foundSkills.length > 0 || foundPlaybook.length > 0) {
438
+ const extras: string[] = []
439
+
440
+ if (foundSkills.some((s: any) => s.body)) {
441
+ const section = foundSkills
442
+ .filter((s: any) => s.body)
443
+ .map((s: any) => `## Skill: ${s.name}\n${s.body}`)
444
+ .join("\n\n")
445
+ extras.push(`\n\n--- SKILL INSTRUCTIONS ---\n${section}`)
446
+ }
447
+
448
+ if (foundPlaybook.length > 0) {
449
+ const section = foundPlaybook.map((p: any) => `- [${p.category ?? "general"}] ${p.rule}`).join("\n")
450
+ extras.push(`\n\n--- PLAYBOOK RULES ---\n${section}`)
451
+ }
452
+
453
+ if (extras.length > 0) {
454
+ const lastMsg = messages[messages.length - 1]
455
+ if (lastMsg?.role === "tool") {
456
+ lastMsg.content += extras.join("")
457
+ log.info(`[agent-loop] Enriched search_knowledge result with ${foundSkills.length} skill(s) and ${foundPlaybook.length} rule(s)`)
458
+ }
459
+ }
460
+ }
461
+ } catch (err) {
462
+ log.warn(`[agent-loop] search_knowledge enrichment failed: ${(err as Error).message}`)
463
+ }
464
+ }
465
+
466
+ // Loop detection: same tool + same args called consecutively → break
467
+ const sig = `${toolName}:${JSON.stringify(tc.function.arguments)}`
468
+ if (sig === lastToolSignature) {
469
+ consecutiveRepeat++
470
+ if (consecutiveRepeat >= 2) {
471
+ log.warn(`[agent-loop] Loop detected: "${toolName}" x${consecutiveRepeat + 1} with same args. Breaking.`)
472
+ finalContent = "No pude completar la tarea porque no encontré las herramientas necesarias para ello."
473
+ loopDetected = true
474
+ break
475
+ }
476
+ } else {
477
+ lastToolSignature = sig
478
+ consecutiveRepeat = 0
479
+ }
480
+ }
481
+
482
+ if (loopDetected) break
483
+
484
+ emitCanvas("canvas:node_update", {
485
+ nodeId: opts.agentId,
486
+ changes: { status: "thinking", currentTool: null },
487
+ })
488
+ }
489
+
490
+ // ── Synthesis call when max iterations hit without a text response ────────
491
+ // The agent spent all iterations on tool calls and never produced a final message.
492
+ // Make one extra call without tools so it summarizes what it did.
493
+ if (!finalContent) {
494
+ log.info(`[agent-loop] Max iterations hit with no text response — requesting synthesis (isolated=${!!opts.isolated})`)
495
+ try {
496
+ messages.push({
497
+ role: "user",
498
+ content: "Basándote en lo que hiciste hasta ahora, responde al usuario con un resumen claro de lo que completaste o del estado actual. Sé conciso.",
499
+ })
500
+ const synthesis = await callLLM({
501
+ ...providerCfg,
502
+ messages: clearOldToolResults(messages) as LLMMessage[],
503
+ tools: undefined, // no tools — force text response
504
+ })
505
+ if (synthesis.usage) {
506
+ totalInputTokens += synthesis.usage.input_tokens
507
+ totalOutputTokens += synthesis.usage.output_tokens
508
+ }
509
+ finalContent = synthesis.content?.trim() || "He completado las tareas solicitadas."
510
+ if (!opts.isolated) {
511
+ addMessage(opts.threadId, "assistant", finalContent)
512
+ }
513
+ yield { agent: { messages: [{ content: finalContent }] } }
514
+ } catch (err) {
515
+ log.warn(`[agent-loop] Synthesis call failed: ${(err as Error).message}`)
516
+ finalContent = "He completado las tareas solicitadas."
517
+ if (!opts.isolated) {
518
+ addMessage(opts.threadId, "assistant", finalContent)
519
+ }
520
+ yield { agent: { messages: [{ content: finalContent }] } }
521
+ }
522
+ }
523
+
524
+ // Emit final usage so consumers (e.g. AgentRunner) can surface real token counts
525
+ if (totalInputTokens > 0 || totalOutputTokens > 0) {
526
+ yield { usage: { input_tokens: totalInputTokens, output_tokens: totalOutputTokens } }
527
+ }
528
+
529
+ // ── Post-loop ────────────────────────────────────────────────────────────
530
+ const durationMs = Math.round(performance.now() - t0)
531
+
532
+ emitCanvas("canvas:node_update", {
533
+ nodeId: opts.agentId,
534
+ changes: { status: "idle", currentTool: null },
535
+ })
536
+
537
+ // Record usage
538
+ recordLLMUsage({
539
+ provider: providerCfg.provider,
540
+ model: providerCfg.model,
541
+ inputTokens: totalInputTokens,
542
+ outputTokens: totalOutputTokens,
543
+ })
544
+
545
+ // Extract text for trace summary
546
+ const textMessageFinal = opts.rawUserMessage || (typeof opts.userMessage === "string"
547
+ ? opts.userMessage
548
+ : Array.isArray(opts.userMessage)
549
+ ? opts.userMessage.filter(p => p.type === "text").map(p => (p as any).text).join("\n")
550
+ : String(opts.userMessage))
551
+
552
+ // Save overall trace
553
+ const cleanMessageFinal = textMessageFinal.replace(/^\[Timestamp:.*?\]\n/, "")
554
+ saveTrace({
555
+ threadId: opts.threadId,
556
+ agentId: opts.agentId,
557
+ agentName,
558
+ inputSummary: cleanMessageFinal.substring(0, 300),
559
+ outputSummary: finalContent.substring(0, 300),
560
+ success: true,
561
+ durationMs,
562
+ tokensUsed: totalInputTokens + totalOutputTokens,
563
+ })
564
+
565
+ log.info(
566
+ `[agent-loop] Done: agent=${agentName} iterations=${iterations} ` +
567
+ `tokens=${totalInputTokens + totalOutputTokens} elapsed=${durationMs}ms`
568
+ )
569
+ }
570
+
571
+ // ─── Isolated worker execution (Fase 4.4) ───────────────────────────────────
572
+
573
+ /**
574
+ * Run a worker agent in an isolated context.
575
+ * Returns the final response string.
576
+ */
577
+ export async function runAgentIsolated(opts: {
578
+ agentId: string
579
+ taskDescription: string | ContentPart[]
580
+ threadId: string
581
+ mcpManager?: MCPClientManager | null
582
+ }): Promise<string> {
583
+ let lastContent = ""
584
+ for await (const chunk of runAgent({
585
+ agentId: opts.agentId,
586
+ userMessage: opts.taskDescription,
587
+ threadId: opts.threadId,
588
+ isolated: true,
589
+ taskContext: opts.taskDescription,
590
+ mcpManager: opts.mcpManager,
591
+ })) {
592
+ if (chunk.agent?.messages?.[0]?.content) {
593
+ lastContent = chunk.agent.messages[0].content
594
+ }
595
+ }
596
+ return lastContent
597
+ }
598
+
599
+ // ─── Shim: AgentLoop class with stream() compatible with providers/index.ts ──
600
+
601
+ export class AgentLoop {
602
+ private mcpManager: MCPClientManager | null = null
603
+
604
+ setMCPManager(m: MCPClientManager) {
605
+ this.mcpManager = m
606
+ }
607
+
608
+ /**
609
+ * Returns an async iterable that emits chunks compatible with
610
+ * the existing providers/index.ts stream consumer.
611
+ */
612
+ stream(
613
+ input: { messages: Array<{ role: string; content: string | ContentPart[] }> },
614
+ config: {
615
+ configurable?: {
616
+ thread_id?: string
617
+ agent_id?: string
618
+ user_id?: string
619
+ system_prompt?: string
620
+ channel?: string
621
+ raw_user_message?: string
622
+ }
623
+ signal?: AbortSignal
624
+ }
625
+ ): AsyncIterable<StreamChunk> {
626
+ // Resolve from database with priority: explicit param → DB lookup → single user/agent
627
+ const threadId = config.configurable?.thread_id || resolveUserId({}) || "default"
628
+ const agentId = config.configurable?.agent_id || resolveAgentId(config.configurable?.agent_id) || this._resolveCoordinatorId() || "main"
629
+ const systemPromptOverride = config.configurable?.system_prompt
630
+ const channel = config.configurable?.channel
631
+ const userId = config.configurable?.user_id || resolveUserId({
632
+ channel: config.configurable?.channel ? (config.configurable?.channel as string).split(':')[0] : null,
633
+ channelUserId: config.configurable?.thread_id
634
+ })
635
+
636
+ // Log MCP Manager status
637
+ log.info(`[AgentLoop.stream] MCP Manager available: ${this.mcpManager !== null}`)
638
+ if (this.mcpManager) {
639
+ try {
640
+ const servers = this.mcpManager.listServers?.() || []
641
+ log.info(`[AgentLoop.stream] MCP servers: ${servers.length} registered`)
642
+ for (const s of servers) {
643
+ log.info(` - ${s.name}: ${s.status} (${s.tools?.length || 0} tools)`)
644
+ }
645
+ } catch (e) {
646
+ log.warn(`[AgentLoop.stream] Failed to list MCP servers: ${(e as Error).message}`)
647
+ }
648
+ }
649
+
650
+ // Extract the last user message from the input
651
+ const lastUserMsg = [...input.messages].reverse().find((m) => m.role === "user")
652
+ const userMessage = lastUserMsg?.content || ""
653
+
654
+ // Use clean message (without timestamp) for FTS5 selectors
655
+ const rawUserMessage = config.configurable?.raw_user_message ||
656
+ (typeof userMessage === "string" ? userMessage : userMessage.filter(p => p.type === "text").map(p => (p as any).text).join("\n"))
657
+
658
+ return runAgent({
659
+ agentId,
660
+ userMessage, // FULL MULTIMODAL MESSAGE
661
+ rawUserMessage, // CLEAN TEXT for FTS5
662
+ threadId,
663
+ channel,
664
+ systemPromptOverride,
665
+ mcpManager: this.mcpManager,
666
+ userId,
667
+ signal: config.signal,
668
+ })
669
+ }
670
+
671
+ private _resolveCoordinatorId(): string {
672
+ // Use the storage helper to get coordinator agent ID from database
673
+ const coordinatorId = resolveAgentId(null);
674
+ return coordinatorId || "main";
675
+ }
676
+ }
677
+
678
+ // Singleton
679
+ let _agentLoop: AgentLoop | null = null
680
+
681
+ export function getAgentLoop(): AgentLoop | null {
682
+ return _agentLoop
683
+ }
684
+
685
+ export function buildAgentLoop(opts: { mcpManager?: MCPClientManager | null } = {}): AgentLoop {
686
+ _agentLoop = new AgentLoop()
687
+ if (opts.mcpManager) {
688
+ _agentLoop.setMCPManager(opts.mcpManager)
689
+ log.info("[buildAgentLoop] MCP Manager set successfully")
690
+ } else {
691
+ log.warn("[buildAgentLoop] No MCP Manager provided, agent will not have MCP tools")
692
+ }
693
+ return _agentLoop
694
+ }
695
+
696
+ export async function rebuildAgentLoop(opts: { mcpManager?: MCPClientManager | null } = {}): Promise<AgentLoop> {
697
+ _agentLoop = null
698
+ return buildAgentLoop(opts)
699
+ }