@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.
- package/.github/CODEOWNERS +9 -0
- package/.github/workflows/publish.yml +89 -0
- package/.github/workflows/version-bump.yml +102 -0
- package/CHANGELOG.md +38 -0
- package/README.md +158 -0
- package/bun.lock +543 -0
- package/bunfig.toml +7 -0
- package/docs/API-AGENTS.md +316 -0
- package/docs/API-CONTEXT-COMPILER.md +252 -0
- package/docs/API-DAG-SCHEDULER.md +273 -0
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +293 -0
- package/docs/API-WORKERS-EVENTS.md +152 -0
- package/docs/INDEX.md +141 -0
- package/docs/README.md +68 -0
- package/package.json +54 -105
- package/packages/cli/package.json +17 -0
- package/packages/cli/src/commands/init.ts +56 -0
- package/packages/cli/src/commands/run.ts +45 -0
- package/packages/cli/src/commands/test.ts +42 -0
- package/packages/cli/src/commands/trace.ts +55 -0
- package/packages/cli/src/index.ts +43 -0
- package/packages/core/package.json +58 -0
- package/packages/core/src/ace/Curator.ts +158 -0
- package/packages/core/src/ace/Reflector.ts +200 -0
- package/packages/core/src/ace/Tracer.ts +100 -0
- package/packages/core/src/ace/index.ts +4 -0
- package/packages/core/src/agent/AgentRunner.ts +699 -0
- package/packages/core/src/agent/Compaction.ts +221 -0
- package/packages/core/src/agent/ContextCompiler.ts +567 -0
- package/packages/core/src/agent/ContextGuard.ts +91 -0
- package/packages/core/src/agent/ConversationStore.ts +244 -0
- package/packages/core/src/agent/Hooks.ts +166 -0
- package/packages/core/src/agent/NativeTools.ts +31 -0
- package/packages/core/src/agent/PromptBuilder.ts +169 -0
- package/packages/core/src/agent/Service.ts +267 -0
- package/packages/core/src/agent/StuckLoop.ts +133 -0
- package/packages/core/src/agent/index.ts +12 -0
- package/packages/core/src/agent/providers/LLMClient.ts +149 -0
- package/packages/core/src/agent/providers/anthropic.ts +212 -0
- package/packages/core/src/agent/providers/gemini.ts +215 -0
- package/packages/core/src/agent/providers/index.ts +199 -0
- package/packages/core/src/agent/providers/interface.ts +195 -0
- package/packages/core/src/agent/providers/ollama.ts +175 -0
- package/packages/core/src/agent/providers/openai-compat.ts +231 -0
- package/packages/core/src/agent/providers.ts +1 -0
- package/packages/core/src/agent/selectors/PlaybookSelector.ts +147 -0
- package/packages/core/src/agent/selectors/SkillSelector.ts +478 -0
- package/packages/core/src/agent/selectors/ToolSelector.ts +577 -0
- package/packages/core/src/agent/selectors/index.ts +6 -0
- package/packages/core/src/api/createAgent.test.ts +48 -0
- package/packages/core/src/api/createAgent.ts +122 -0
- package/packages/core/src/api/index.ts +2 -0
- package/packages/core/src/canvas/CanvasManager.ts +390 -0
- package/packages/core/src/canvas/a2ui-tools.ts +255 -0
- package/packages/core/src/canvas/canvas-tools.ts +448 -0
- package/packages/core/src/canvas/emitter.ts +149 -0
- package/packages/core/src/canvas/index.ts +6 -0
- package/packages/core/src/config/index.ts +2 -0
- package/packages/core/src/config/loader.ts +554 -0
- package/packages/core/src/ethics/EthicsGuard.test.ts +54 -0
- package/packages/core/src/ethics/EthicsGuard.ts +66 -0
- package/packages/core/src/ethics/index.ts +2 -0
- package/packages/core/src/gateway/channel-notify.test.ts +14 -0
- package/packages/core/src/gateway/channel-notify.ts +12 -0
- package/packages/core/src/gateway/index.ts +1 -0
- package/packages/core/src/index.ts +37 -0
- package/packages/core/src/mcp/MCPClient.ts +439 -0
- package/packages/core/src/mcp/MCPToolAdapter.ts +176 -0
- package/packages/core/src/mcp/config.ts +13 -0
- package/packages/core/src/mcp/hot-reload.ts +147 -0
- package/packages/core/src/mcp/index.ts +11 -0
- package/packages/core/src/mcp/logger.ts +42 -0
- package/packages/core/src/mcp/singleton.ts +21 -0
- package/packages/core/src/mcp/transports/index.ts +67 -0
- package/packages/core/src/mcp/transports/sse.ts +241 -0
- package/packages/core/src/mcp/transports/websocket.ts +159 -0
- package/packages/core/src/memory/Scratchpad.test.ts +47 -0
- package/packages/core/src/memory/Scratchpad.ts +37 -0
- package/packages/core/src/memory/Storage.ts +6 -0
- package/packages/core/src/memory/index.ts +2 -0
- package/packages/core/src/multimodal/VisionService.ts +293 -0
- package/packages/core/src/multimodal/index.ts +2 -0
- package/packages/core/src/multimodal/types.ts +28 -0
- package/packages/core/src/security/Pairing.ts +250 -0
- package/packages/core/src/security/RateLimit.ts +270 -0
- package/packages/core/src/security/index.ts +4 -0
- package/packages/core/src/skills/SkillLoader.ts +388 -0
- package/packages/core/src/skills/bundled-data.generated.ts +3332 -0
- package/packages/core/src/skills/defineSkill.ts +18 -0
- package/packages/core/src/skills/index.ts +4 -0
- package/packages/core/src/state/index.ts +2 -0
- package/packages/core/src/state/store.ts +312 -0
- package/packages/core/src/storage/SQLiteStorage.ts +407 -0
- package/packages/core/src/storage/crypto.ts +101 -0
- package/packages/core/src/storage/index.ts +10 -0
- package/packages/core/src/storage/onboarding.ts +1603 -0
- package/packages/core/src/storage/schema.ts +689 -0
- package/packages/core/src/storage/seed.ts +740 -0
- package/packages/core/src/storage/usage.ts +374 -0
- package/packages/core/src/swarm/AgentBus.ts +460 -0
- package/packages/core/src/swarm/AgentExecutor.ts +53 -0
- package/packages/core/src/swarm/Coordinator.ts +251 -0
- package/packages/core/src/swarm/EventBridge.ts +122 -0
- package/packages/core/src/swarm/EventBus.ts +169 -0
- package/packages/core/src/swarm/TaskGraph.ts +192 -0
- package/packages/core/src/swarm/TaskNode.ts +97 -0
- package/packages/core/src/swarm/TaskResult.ts +22 -0
- package/packages/core/src/swarm/WorkerPool.ts +236 -0
- package/packages/core/src/swarm/errors.ts +37 -0
- package/packages/core/src/swarm/index.ts +30 -0
- package/packages/core/src/swarm/presets/HiveLearnPreset.ts +99 -0
- package/packages/core/src/swarm/presets/ResearchPreset.ts +97 -0
- package/packages/core/src/swarm/presets/index.ts +4 -0
- package/packages/core/src/swarm/strategies/ParallelStrategy.ts +21 -0
- package/packages/core/src/swarm/strategies/PriorityStrategy.ts +46 -0
- package/packages/core/src/swarm/strategies/index.ts +3 -0
- package/packages/core/src/swarm/types.ts +164 -0
- package/packages/core/src/tools/ToolExecutor.ts +58 -0
- package/packages/core/src/tools/ToolRegistry.test.ts +98 -0
- package/packages/core/src/tools/ToolRegistry.ts +61 -0
- package/packages/core/src/tools/agents/get-available-models.ts +118 -0
- package/packages/core/src/tools/agents/index.ts +715 -0
- package/packages/core/src/tools/bridge-events.ts +26 -0
- package/packages/core/src/tools/canvas/index.ts +375 -0
- package/packages/core/src/tools/cli/index.ts +142 -0
- package/packages/core/src/tools/codebridge/index.ts +342 -0
- package/packages/core/src/tools/core/index.ts +476 -0
- package/packages/core/src/tools/cron/index.ts +626 -0
- package/packages/core/src/tools/filesystem/fs-delete.ts +78 -0
- package/packages/core/src/tools/filesystem/fs-edit.ts +106 -0
- package/packages/core/src/tools/filesystem/fs-exists.ts +63 -0
- package/packages/core/src/tools/filesystem/fs-glob.ts +108 -0
- package/packages/core/src/tools/filesystem/fs-list.ts +129 -0
- package/packages/core/src/tools/filesystem/fs-read.ts +72 -0
- package/packages/core/src/tools/filesystem/fs-write.ts +67 -0
- package/packages/core/src/tools/filesystem/index.ts +34 -0
- package/packages/core/src/tools/filesystem/workspace-guard.ts +62 -0
- package/packages/core/src/tools/index.ts +231 -0
- package/packages/core/src/tools/meeting/index.ts +363 -0
- package/packages/core/src/tools/office/index.ts +47 -0
- package/packages/core/src/tools/office/office-escribir-docx.ts +192 -0
- package/packages/core/src/tools/office/office-escribir-pdf.ts +172 -0
- package/packages/core/src/tools/office/office-escribir-pptx.ts +174 -0
- package/packages/core/src/tools/office/office-escribir-xlsx.ts +116 -0
- package/packages/core/src/tools/office/office-leer-docx.ts +93 -0
- package/packages/core/src/tools/office/office-leer-pdf.ts +114 -0
- package/packages/core/src/tools/office/office-leer-pptx.ts +136 -0
- package/packages/core/src/tools/office/office-leer-xlsx.ts +124 -0
- package/packages/core/src/tools/projects/index.ts +37 -0
- package/packages/core/src/tools/projects/project-create.ts +94 -0
- package/packages/core/src/tools/projects/project-done.ts +66 -0
- package/packages/core/src/tools/projects/project-fail.ts +66 -0
- package/packages/core/src/tools/projects/project-list.ts +96 -0
- package/packages/core/src/tools/projects/project-update.ts +72 -0
- package/packages/core/src/tools/projects/task-create.ts +68 -0
- package/packages/core/src/tools/projects/task-evaluate.ts +93 -0
- package/packages/core/src/tools/projects/task-update.ts +93 -0
- package/packages/core/src/tools/types.ts +39 -0
- package/packages/core/src/tools/voice/index.ts +104 -0
- package/packages/core/src/tools/web/browser-click.ts +78 -0
- package/packages/core/src/tools/web/browser-extract.ts +139 -0
- package/packages/core/src/tools/web/browser-navigate.ts +106 -0
- package/packages/core/src/tools/web/browser-screenshot.ts +87 -0
- package/packages/core/src/tools/web/browser-script.ts +88 -0
- package/packages/core/src/tools/web/browser-service.ts +554 -0
- package/packages/core/src/tools/web/browser-type.ts +101 -0
- package/packages/core/src/tools/web/browser-wait.ts +136 -0
- package/packages/core/src/tools/web/index.ts +41 -0
- package/packages/core/src/tools/web/web-fetch.ts +78 -0
- package/packages/core/src/tools/web/web-search.ts +123 -0
- package/packages/core/src/utils/benchmark.ts +80 -0
- package/packages/core/src/utils/crypto.ts +73 -0
- package/packages/core/src/utils/date.ts +42 -0
- package/packages/core/src/utils/index.ts +10 -0
- package/packages/core/src/utils/logger.ts +389 -0
- package/packages/core/src/utils/retry.ts +70 -0
- package/packages/core/src/utils/toon.ts +253 -0
- package/packages/core/src/voice/index.ts +656 -0
- package/test/setup-db.ts +216 -0
- package/tsconfig.json +39 -0
- package/src/agents.ts +0 -1
- package/src/canvas.ts +0 -1
- package/src/channels.ts +0 -1
- package/src/config.ts +0 -1
- package/src/events.ts +0 -1
- package/src/gateway.ts +0 -1
- package/src/index.ts +0 -304
- package/src/mcp.ts +0 -1
- package/src/multimodal.ts +0 -1
- package/src/scheduler.ts +0 -1
- package/src/security.ts +0 -1
- package/src/skills.ts +0 -1
- package/src/state.ts +0 -1
- package/src/storage.ts +0 -1
- package/src/tools.ts +0 -1
- package/src/tts.ts +0 -1
- package/src/types.ts +0 -82
- package/src/utils.ts +0 -1
- 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
|
+
}
|