@johpaz/hive-sdk 0.1.4 → 0.1.6
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/CHANGELOG.md +129 -0
- package/README.md +78 -23
- package/bun.lock +55 -29
- package/bunfig.toml +4 -2
- package/docs/API-AGENTS.md +78 -27
- package/docs/API-CONTEXT-COMPILER.md +31 -34
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +58 -22
- package/docs/HIVE-HARNESS.md +1 -1
- package/docs/INDEX.md +4 -4
- package/docs/TEMPLATE-HIVE-APP.md +10 -10
- package/package.json +17 -12
- package/packages/cli/package.json +2 -2
- package/packages/cli/src/commands/create-app.test.ts +36 -7
- package/packages/cli/src/commands/init.ts +3 -3
- package/packages/cli/src/commands/run.ts +1 -1
- package/packages/cli/src/commands/test.ts +37 -25
- package/packages/cli/src/commands/trace.ts +30 -28
- package/packages/cli/templates/hive-app/.env.example +10 -2
- package/packages/cli/templates/hive-app/README.md +103 -0
- package/packages/cli/templates/hive-app/hive.config.ts +9 -3
- package/packages/cli/templates/hive-app/src/agents/coordinator.ts +8 -1
- package/packages/cli/templates/hive-app/src/main.ts +12 -19
- package/packages/core/package.json +13 -12
- package/packages/core/src/agent/acceptance-checks.ts +172 -0
- package/packages/core/src/agent/agent-catalog.ts +348 -0
- package/packages/core/src/agent/agent-loop.ts +1373 -0
- package/packages/core/src/agent/capability-search.ts +186 -0
- package/packages/core/src/agent/catalog-selector.ts +103 -0
- package/packages/core/src/agent/{Compaction.ts → compaction.ts} +86 -63
- package/packages/core/src/agent/context-compiler.ts +689 -0
- package/packages/core/src/agent/conversation-store.ts +381 -0
- package/packages/core/src/agent/curator.ts +276 -0
- package/packages/core/src/agent/delegation-runtime.ts +241 -0
- package/packages/core/src/agent/goal-runner.ts +323 -0
- package/packages/core/src/agent/index.ts +17 -12
- package/packages/core/src/agent/llm-client.ts +266 -0
- package/packages/core/src/agent/llm-providers/anthropic.ts +264 -0
- package/packages/core/src/agent/llm-providers/deepseek.ts +8 -0
- package/packages/core/src/agent/{providers → llm-providers}/gemini.ts +98 -60
- package/packages/core/src/agent/llm-providers/groq.ts +5 -0
- package/packages/core/src/agent/llm-providers/hiveagents.ts +253 -0
- package/packages/core/src/agent/{providers → llm-providers}/interface.ts +73 -13
- package/packages/core/src/agent/llm-providers/kimi.ts +8 -0
- package/packages/core/src/agent/llm-providers/minimax.ts +13 -0
- package/packages/core/src/agent/llm-providers/mistral.ts +5 -0
- package/packages/core/src/agent/llm-providers/modelscope.ts +5 -0
- package/packages/core/src/agent/llm-providers/nvidia.ts +5 -0
- package/packages/core/src/agent/{providers → llm-providers}/ollama.ts +31 -5
- package/packages/core/src/agent/llm-providers/openai-compat-base.ts +418 -0
- package/packages/core/src/agent/llm-providers/openai.ts +5 -0
- package/packages/core/src/agent/llm-providers/opencode-go.ts +9 -0
- package/packages/core/src/agent/llm-providers/openrouter.ts +5 -0
- package/packages/core/src/agent/llm-providers/qwen.ts +5 -0
- package/packages/core/src/agent/llm-providers/z-ai.ts +5 -0
- package/packages/core/src/agent/minimal-loadout.ts +47 -0
- package/packages/core/src/agent/playbook-selector.ts +119 -0
- package/packages/core/src/agent/{PromptBuilder.ts → prompt-builder.ts} +21 -22
- package/packages/core/src/{harness → agent}/proof-packet.ts +16 -21
- package/packages/core/src/agent/providers/index.ts +35 -16
- package/packages/core/src/agent/reflector.ts +320 -0
- package/packages/core/src/agent/routing-intent.ts +22 -0
- package/packages/core/src/{harness → agent}/run-epoch.ts +4 -3
- package/packages/core/src/{harness → agent}/run-store.ts +142 -81
- package/packages/core/src/agent/{Service.ts → service.ts} +37 -26
- package/packages/core/src/agent/skill-selector.ts +374 -0
- package/packages/core/src/agent/stuck-loop.ts +209 -0
- package/packages/core/src/agent/{selectors/ToolSelector.ts → tool-selector.ts} +188 -178
- package/packages/core/src/{ace/Tracer.ts → agent/tracer.ts} +37 -27
- package/packages/core/src/api/createAgent.test.ts +139 -27
- package/packages/core/src/api/createAgent.ts +232 -44
- package/packages/core/src/artifacts/store.ts +162 -0
- package/packages/core/src/canvas/canvas-manager.ts +161 -0
- package/packages/core/src/canvas/canvas.test.ts +8 -4
- package/packages/core/src/canvas/emitter.ts +131 -80
- package/packages/core/src/canvas/index.ts +1 -3
- package/packages/core/src/channels/base.ts +9 -1
- package/packages/core/src/channels/discord.ts +5 -4
- package/packages/core/src/channels/manager.ts +122 -30
- package/packages/core/src/channels/slack.ts +5 -4
- package/packages/core/src/channels/telegram.ts +36 -6
- package/packages/core/src/channels/webchat.ts +11 -10
- package/packages/core/src/channels/whatsapp.ts +23 -7
- package/packages/core/src/config/index.ts +13 -2
- package/packages/core/src/config/loader.ts +76 -29
- package/packages/core/src/ethics/EthicsGuard.test.ts +90 -36
- package/packages/core/src/ethics/EthicsGuard.ts +51 -47
- package/packages/core/src/events/agent-bus.ts +44 -68
- package/packages/core/src/events/channel-narration.ts +150 -0
- package/packages/core/src/events/narration.ts +82 -0
- package/packages/core/src/events/tool-narration.ts +62 -0
- package/packages/core/src/gateway/delegation-groups.ts +258 -0
- package/packages/core/src/{harness → gateway}/durable-queue.ts +102 -42
- package/packages/core/src/{harness → gateway}/job-store.ts +85 -48
- package/packages/core/src/gateway/lane-queue.ts +173 -0
- package/packages/core/src/gateway/notification-inbox.ts +57 -0
- package/packages/core/src/gateway/server.ts +1 -1
- package/packages/core/src/harness/index.ts +46 -27
- package/packages/core/src/index.ts +33 -27
- package/packages/core/src/mcp/hot-reload.ts +32 -23
- package/packages/core/src/mcp/index.ts +6 -3
- package/packages/core/src/mcp/singleton.ts +1 -4
- package/packages/core/src/mcp/tool-sync.ts +138 -0
- package/packages/core/src/memory/Scratchpad.test.ts +39 -20
- package/packages/core/src/memory/Scratchpad.ts +27 -34
- package/packages/core/src/multimodal/vision-service.ts +44 -38
- package/packages/core/src/resilience/retry.ts +95 -0
- package/packages/core/src/scheduler/CronScheduler.ts +334 -287
- package/packages/core/src/scheduler/index.ts +9 -7
- package/packages/core/src/scheduler/integration.ts +46 -26
- package/packages/core/src/scheduler/scheduler.test.ts +9 -13
- package/packages/core/src/scheduler/types.ts +7 -2
- package/packages/core/src/security/Pairing.ts +1 -1
- package/packages/core/src/skills/bundled/a2ui/a2ui_dashboard/SKILL.md +176 -0
- package/packages/core/src/skills/bundled/a2ui/a2ui_form/SKILL.md +202 -0
- package/packages/core/src/skills/bundled/a2ui/a2ui_interactive/SKILL.md +206 -0
- package/packages/core/src/skills/bundled/agents/agent_spawner/SKILL.md +173 -0
- package/packages/core/src/skills/bundled/agents/memory_manager/SKILL.md +143 -0
- package/packages/core/src/skills/bundled/agents/research_and_remember/SKILL.md +139 -0
- package/packages/core/src/skills/bundled/agents/task_orchestrator/SKILL.md +98 -0
- package/packages/core/src/skills/bundled/api/api_client/SKILL.md +132 -0
- package/packages/core/src/skills/bundled/cli/cli_pipeline/SKILL.md +135 -0
- package/packages/core/src/skills/bundled/cli/cli_safe_exec/SKILL.md +125 -0
- package/packages/core/src/skills/bundled/cli/software_engineering/SKILL.md +23 -0
- package/packages/core/src/skills/bundled/cron_manager/SKILL.md +188 -0
- package/packages/core/src/skills/bundled/cron_reminder/SKILL.md +112 -0
- package/packages/core/src/skills/bundled/filesystem/file_manager/SKILL.md +118 -0
- package/packages/core/src/skills/bundled/filesystem/file_read_and_summarize/SKILL.md +109 -0
- package/packages/core/src/skills/bundled/filesystem/file_writer/SKILL.md +129 -0
- package/packages/core/src/skills/bundled/filesystem/workspace_file_operator/SKILL.md +22 -0
- package/packages/core/src/skills/bundled/office/office_document_manager/SKILL.md +262 -0
- package/packages/core/src/skills/bundled/search_knowledge/capability_discovery/SKILL.md +75 -0
- package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +120 -0
- package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +109 -0
- package/packages/core/src/skills/bundled/web/web_monitor/SKILL.md +127 -0
- package/packages/core/src/skills/bundled/web/web_research/SKILL.md +119 -0
- package/packages/core/src/skills/bundled-data.generated.ts +731 -2678
- package/packages/core/src/skills/skills.test.ts +52 -11
- package/packages/core/src/{harness → storage}/boot-id.ts +5 -2
- package/packages/core/src/storage/bootstrap.ts +151 -0
- package/packages/core/src/storage/causal-events.ts +84 -0
- package/packages/core/src/storage/collections.ts +680 -0
- package/packages/core/src/storage/crypto.ts +205 -74
- package/packages/core/src/{harness/db-helpers.ts → storage/hive.ts} +63 -7
- package/packages/core/src/storage/hivedb.ts +61 -0
- package/packages/core/src/storage/index.ts +111 -18
- package/packages/core/src/storage/model-id.ts +53 -0
- package/packages/core/src/storage/onboarding.ts +540 -972
- package/packages/core/src/storage/reconcile.ts +238 -0
- package/packages/core/src/storage/seed.ts +572 -406
- package/packages/core/src/storage/usage.ts +285 -225
- package/packages/core/src/storage/user-email.ts +11 -0
- package/packages/core/src/swarm/AgentExecutor.ts +1 -1
- package/packages/core/src/swarm/EventBridge.ts +1 -1
- package/packages/core/src/swarm/index.ts +12 -9
- package/packages/core/src/tool-runtime/index.ts +146 -23
- package/packages/core/src/tool-runtime/tool-worker.ts +2 -2
- package/packages/core/src/tool-runtime/worker-tools.ts +27 -0
- package/packages/core/src/{canvas/a2ui-tools.ts → tools/a2ui/index.ts} +17 -8
- package/packages/core/src/tools/agents/get-available-models.ts +36 -54
- package/packages/core/src/tools/agents/index.ts +784 -292
- package/packages/core/src/tools/api/api-request.test.ts +164 -0
- package/packages/core/src/tools/api/api-request.ts +174 -0
- package/packages/core/src/tools/api/index.ts +16 -0
- package/packages/core/src/tools/cli/index.ts +4 -0
- package/packages/core/src/tools/core/index.ts +281 -112
- package/packages/core/src/tools/cron/index.ts +121 -124
- package/packages/core/src/tools/index.ts +63 -78
- package/packages/core/src/tools/office/office-escribir-xlsx.ts +3 -1
- package/packages/core/src/tools/types.ts +3 -1
- package/packages/core/src/tools/web/artifact-inspect.ts +23 -0
- package/packages/core/src/tools/web/browser-backend.ts +129 -0
- package/packages/core/src/tools/web/browser-screenshot.ts +26 -5
- package/packages/core/src/tools/web/browser-service.ts +80 -35
- package/packages/core/src/tools/web/browser-type.ts +3 -8
- package/packages/core/src/tools/web/index.ts +4 -4
- package/packages/core/src/tools/web/webview-backend.ts +412 -0
- package/packages/core/src/voice/index.ts +89 -63
- package/packages/core/src/workers/agent.worker.ts +2 -2
- package/packages/core/src/workers/workers.test.ts +3 -10
- package/scripts/bump-version.ts +248 -0
- package/scripts/generate-skill-bundle.ts +108 -0
- package/test/acceptance-checks.test.ts +403 -0
- package/test/agent-loop-terminal-synthesis.test.ts +32 -0
- package/test/browser-backend.test.ts +308 -0
- package/test/catalog-agents-stay-enabled.test.ts +117 -0
- package/test/causal-events.test.ts +117 -0
- package/test/compaction.test.ts +105 -0
- package/test/context-compiler.test.ts +269 -0
- package/test/curator.test.ts +130 -0
- package/test/durable-queue.test.ts +114 -0
- package/test/harness-barrel.test.ts +64 -0
- package/test/hive-helpers.test.ts +130 -0
- package/test/hivedb-search.test.ts +189 -0
- package/test/internal-turns.test.ts +166 -0
- package/test/job-idempotency.test.ts +68 -0
- package/test/job-retry-backoff.test.ts +184 -0
- package/test/job-store.test.ts +381 -0
- package/test/llm-retry.test.ts +97 -0
- package/test/memory-perf.test.ts +774 -0
- package/test/minimal-loadout.test.ts +78 -0
- package/test/model-catalog.test.ts +105 -0
- package/test/preload.ts +12 -0
- package/test/reflector.test.ts +320 -0
- package/test/retention-cap.test.ts +91 -0
- package/test/retired-capabilities-pruned.test.ts +192 -0
- package/test/run-store.test.ts +355 -0
- package/test/scratchpad.test.ts +74 -0
- package/test/secrets-durability.test.ts +119 -0
- package/test/seed-model-reseed.test.ts +155 -0
- package/test/setup-agent-seed.test.ts +264 -0
- package/test/tool-inventory.test.ts +65 -0
- package/test/tool-runtime.test.ts +258 -0
- package/test/tool-selector-runtime-tools.test.ts +117 -0
- package/test/toon.test.ts +429 -0
- package/tsconfig.json +2 -0
- package/packages/core/src/ace/Curator.ts +0 -158
- package/packages/core/src/ace/Reflector.ts +0 -200
- package/packages/core/src/ace/index.ts +0 -4
- package/packages/core/src/agent/AgentRunner.ts +0 -711
- package/packages/core/src/agent/ContextCompiler.ts +0 -567
- package/packages/core/src/agent/ContextGuard.ts +0 -91
- package/packages/core/src/agent/ConversationStore.ts +0 -254
- package/packages/core/src/agent/Hooks.ts +0 -166
- package/packages/core/src/agent/StuckLoop.ts +0 -133
- package/packages/core/src/agent/providers/LLMClient.ts +0 -149
- package/packages/core/src/agent/providers/anthropic.ts +0 -212
- package/packages/core/src/agent/providers/openai-compat.ts +0 -231
- package/packages/core/src/agent/selectors/PlaybookSelector.ts +0 -121
- package/packages/core/src/agent/selectors/SkillSelector.ts +0 -322
- package/packages/core/src/agent/selectors/index.ts +0 -6
- package/packages/core/src/auth/auth.ts +0 -121
- package/packages/core/src/auth/index.ts +0 -1
- package/packages/core/src/canvas/CanvasManager.ts +0 -390
- package/packages/core/src/canvas/canvas-tools.ts +0 -448
- package/packages/core/src/harness/collections.ts +0 -98
- package/packages/core/src/harness/goal-verifier.ts +0 -141
- package/packages/core/src/harness/harness.test.ts +0 -236
- package/packages/core/src/harness/reconcile.ts +0 -149
- package/packages/core/src/mcp/MCPToolAdapter.ts +0 -176
- package/packages/core/src/multimodal/VisionService.ts +0 -293
- package/packages/core/src/scheduler/dag/AgentExecutor.ts +0 -53
- package/packages/core/src/scheduler/dag/DAGScheduler.ts +0 -250
- package/packages/core/src/scheduler/dag/EventBridge.ts +0 -122
- package/packages/core/src/scheduler/dag/TaskGraph.ts +0 -192
- package/packages/core/src/scheduler/dag/TaskNode.ts +0 -97
- package/packages/core/src/scheduler/dag/TaskResult.ts +0 -22
- package/packages/core/src/scheduler/dag/errors.ts +0 -37
- package/packages/core/src/scheduler/dag/index.ts +0 -26
- package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +0 -97
- package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +0 -21
- package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +0 -46
- package/packages/core/src/storage/HiveDBStorage.ts +0 -64
- package/packages/core/src/storage/SQLiteStorage.ts +0 -414
- package/packages/core/src/storage/hiveSeed.ts +0 -308
- package/packages/core/src/storage/hiveStorage.test.ts +0 -38
- package/packages/core/src/storage/schema.ts +0 -689
- package/packages/core/src/storage/storage.test.ts +0 -37
- package/packages/core/src/swarm/AgentBus.ts +0 -460
- package/packages/core/src/swarm/EventBus.ts +0 -169
- package/packages/core/src/swarm/WorkerPool.ts +0 -236
- package/packages/core/src/tools/bridge-events.ts +0 -26
- package/packages/core/src/tools/canvas/index.ts +0 -375
- package/packages/core/src/tools/codebridge/index.ts +0 -342
- package/packages/core/src/tools/meeting/index.ts +0 -353
- package/packages/core/src/tools/projects/index.ts +0 -37
- package/packages/core/src/tools/projects/project-create.ts +0 -94
- package/packages/core/src/tools/projects/project-done.ts +0 -66
- package/packages/core/src/tools/projects/project-fail.ts +0 -66
- package/packages/core/src/tools/projects/project-list.ts +0 -96
- package/packages/core/src/tools/projects/project-update.ts +0 -72
- package/packages/core/src/tools/projects/task-create.ts +0 -68
- package/packages/core/src/tools/projects/task-evaluate.ts +0 -93
- package/packages/core/src/tools/projects/task-update.ts +0 -93
- package/packages/core/src/tools/voice/index.ts +0 -104
- package/packages/core/src/tools/web/api-request.test.ts +0 -170
- package/packages/core/src/tools/web/api-request.ts +0 -239
- package/test/setup-db.ts +0 -216
- /package/packages/core/src/agent/{NativeTools.ts → native-tools.ts} +0 -0
|
@@ -0,0 +1,1373 @@
|
|
|
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"
|
|
17
|
+
import { col, fromIndexable } from "../storage/hive"
|
|
18
|
+
import { getHiveDb } from "../storage/hivedb"
|
|
19
|
+
import type { HiveDB, EventInput } from "@johpaz/hive-db"
|
|
20
|
+
import type { AgentDoc, TurnSource } from "../storage/collections"
|
|
21
|
+
import { callLLM, resolveProviderConfig, getDefaultLLM, type LLMMessage } from "./llm-client"
|
|
22
|
+
import { addMessage } from "./conversation-store"
|
|
23
|
+
import { saveTrace, recordLLMUsage } from "./tracer"
|
|
24
|
+
import { maybeCompact, clearOldToolResults } from "./compaction"
|
|
25
|
+
import { emitCanvas } from "../canvas/emitter"
|
|
26
|
+
import type { MCPClientManager } from "../mcp/index.ts"
|
|
27
|
+
import { compileContext } from "./context-compiler"
|
|
28
|
+
import { formatToolResult } from "../utils/toon"
|
|
29
|
+
import { resolveUserId, resolveAgentId } from "../storage/onboarding"
|
|
30
|
+
import type { ContentPart } from "../multimodal/types"
|
|
31
|
+
import { loadConfig } from "../config/loader"
|
|
32
|
+
import { executeToolBatch } from "../tool-runtime"
|
|
33
|
+
import { createStuckLoopDetector, getInterventionMessage, type StuckLoopState } from "./stuck-loop"
|
|
34
|
+
import {
|
|
35
|
+
createRun as createAgentRun,
|
|
36
|
+
checkpoint as checkpointRun,
|
|
37
|
+
completeRun,
|
|
38
|
+
failRun,
|
|
39
|
+
interruptRun,
|
|
40
|
+
getRun,
|
|
41
|
+
reclaimRun,
|
|
42
|
+
deserializeCheckpoint,
|
|
43
|
+
bumpTurn,
|
|
44
|
+
startLeaseRenewal,
|
|
45
|
+
stopLeaseRenewal,
|
|
46
|
+
type RunCheckpointState,
|
|
47
|
+
} from "./run-store"
|
|
48
|
+
import { publishNarration } from "../events/narration"
|
|
49
|
+
import { getNarration } from "../events/tool-narration"
|
|
50
|
+
|
|
51
|
+
const log = logger.child("agent-loop")
|
|
52
|
+
|
|
53
|
+
// Per-operation budget for a single LLM call — NOT an aggregate deadline for the
|
|
54
|
+
// whole turn. Each call gets its own fresh window; a slow-but-healthy multi-step
|
|
55
|
+
// turn (many quick operations) is never killed just for taking a while overall.
|
|
56
|
+
const LLM_CALL_TIMEOUT_MS = 3 * 60 * 1000
|
|
57
|
+
|
|
58
|
+
export class LLMCallTimeoutError extends Error {
|
|
59
|
+
constructor(ms: number) {
|
|
60
|
+
super(`LLM call timed out after ${ms}ms`)
|
|
61
|
+
this.name = "LLMCallTimeoutError"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class AgentSynthesisError extends Error {
|
|
66
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
67
|
+
super(message, options)
|
|
68
|
+
this.name = "AgentSynthesisError"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Produce a terminal response with one bounded retry. Empty model output is a
|
|
74
|
+
* failure: callers must never turn an unknown outcome into a success message.
|
|
75
|
+
*/
|
|
76
|
+
export async function synthesizeFinalResponse(
|
|
77
|
+
operation: () => Promise<string | null | undefined>,
|
|
78
|
+
): Promise<string> {
|
|
79
|
+
let lastError: unknown
|
|
80
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
81
|
+
try {
|
|
82
|
+
const content = (await operation())?.trim()
|
|
83
|
+
if (content) return content
|
|
84
|
+
lastError = new Error("The model returned an empty synthesis")
|
|
85
|
+
} catch (err) {
|
|
86
|
+
lastError = err
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const detail = lastError instanceof Error ? lastError.message : String(lastError)
|
|
91
|
+
throw new AgentSynthesisError(
|
|
92
|
+
`No se pudo generar la respuesta final del agente después de 2 intentos: ${detail}`,
|
|
93
|
+
{ cause: lastError },
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Bounds a single async operation to its own timeout window, independent of any caller. */
|
|
98
|
+
export async function withTimeout<T>(op: () => Promise<T>, timeoutMs: number): Promise<T> {
|
|
99
|
+
let timer: ReturnType<typeof setTimeout>
|
|
100
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
101
|
+
timer = setTimeout(() => reject(new LLMCallTimeoutError(timeoutMs)), timeoutMs)
|
|
102
|
+
})
|
|
103
|
+
try {
|
|
104
|
+
return await Promise.race([op(), timeout])
|
|
105
|
+
} finally {
|
|
106
|
+
clearTimeout(timer!)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Append one G9 causal event (IntentLogged/StateTransition/ToolCall) to HiveDB's
|
|
112
|
+
* event log. Never throws: a broken causal log must never break the agent loop.
|
|
113
|
+
* See hiveBD's docs/AGENT_INTEGRATION.md for the payload vocabulary contract.
|
|
114
|
+
*/
|
|
115
|
+
async function appendCausalEvent(
|
|
116
|
+
db: HiveDB,
|
|
117
|
+
input: {
|
|
118
|
+
agentId: string
|
|
119
|
+
streamId: string
|
|
120
|
+
kind: EventInput["kind"]
|
|
121
|
+
payload: Record<string, unknown>
|
|
122
|
+
causation?: number
|
|
123
|
+
correlation?: string
|
|
124
|
+
}
|
|
125
|
+
): Promise<number | undefined> {
|
|
126
|
+
try {
|
|
127
|
+
return await db.append({
|
|
128
|
+
agentId: input.agentId,
|
|
129
|
+
streamId: input.streamId,
|
|
130
|
+
kind: input.kind,
|
|
131
|
+
payload: JSON.stringify(input.payload),
|
|
132
|
+
causation: input.causation,
|
|
133
|
+
correlation: input.correlation,
|
|
134
|
+
})
|
|
135
|
+
} catch (err) {
|
|
136
|
+
log.warn(`[agent-loop] causal event append failed (kind=${input.kind}): ${(err as Error).message}`)
|
|
137
|
+
return undefined
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
export interface AgentLoopOptions {
|
|
144
|
+
agentId: string
|
|
145
|
+
userMessage: string | ContentPart[]
|
|
146
|
+
threadId: string
|
|
147
|
+
channel?: string
|
|
148
|
+
mcpManager?: MCPClientManager | null
|
|
149
|
+
/** System prompt override (from server.ts config) */
|
|
150
|
+
systemPromptOverride?: string
|
|
151
|
+
/** Worker mode: isolated context + single-task execution */
|
|
152
|
+
isolated?: boolean
|
|
153
|
+
taskContext?: string | ContentPart[]
|
|
154
|
+
onStep?: (step: StepEvent) => Promise<void>
|
|
155
|
+
onToken?: (token: string) => void
|
|
156
|
+
/** Live reasoning/thinking tokens as they stream, for display only. */
|
|
157
|
+
onReasoningToken?: (token: string) => void
|
|
158
|
+
/** User ID for context propagation */
|
|
159
|
+
userId?: string
|
|
160
|
+
/** Abort signal to stop generation mid-execution */
|
|
161
|
+
signal?: AbortSignal
|
|
162
|
+
/** Clean text for search selectors and tracing (extracted from userMessage if multimodal) */
|
|
163
|
+
rawUserMessage?: string
|
|
164
|
+
/** Extra tools to force into the LLM loadout (used by tests/evals). */
|
|
165
|
+
extraTools?: any[]
|
|
166
|
+
/** Run ID for an existing AgentRun — enables resume from checkpoint */
|
|
167
|
+
runId?: string
|
|
168
|
+
/** Stable originating chat-turn id used to group delegated work. */
|
|
169
|
+
turnId?: string
|
|
170
|
+
/** Durable task id when executing a delegated worker. */
|
|
171
|
+
taskId?: string
|
|
172
|
+
/** External routing/session id for progress delivery. */
|
|
173
|
+
sessionId?: string
|
|
174
|
+
/** Whether to resume from a previously saved checkpoint */
|
|
175
|
+
resume?: boolean
|
|
176
|
+
/** Run budget — overrides agent.max_iterations when set */
|
|
177
|
+
budget?: {
|
|
178
|
+
maxIterations?: number
|
|
179
|
+
maxTurns?: number | null
|
|
180
|
+
maxTokens?: number | null
|
|
181
|
+
}
|
|
182
|
+
/** Goal-based continuation parameters */
|
|
183
|
+
goal?: {
|
|
184
|
+
text: string
|
|
185
|
+
checkTool?: string
|
|
186
|
+
}
|
|
187
|
+
/** Whether to checkpoint this run durably (default false for chat; auto-promoted for long turns) */
|
|
188
|
+
durable?: boolean
|
|
189
|
+
/** Kind of run for the AgentRun record */
|
|
190
|
+
runKind?: "chat" | "worker" | "goal" | "cron" | "project"
|
|
191
|
+
/**
|
|
192
|
+
* Provenance to persist the trigger message with (default "message"). The
|
|
193
|
+
* trigger message is always persisted with role:"user" — including
|
|
194
|
+
* system-originated turns (async-delegation fan-in/fan-out notices from
|
|
195
|
+
* delegation-groups.ts and delegation-notify.ts), since AgentLoop.stream()
|
|
196
|
+
* extracts the last role:"user" message as the turn's trigger content.
|
|
197
|
+
* `historySource` records where it actually came from so history/compaction/
|
|
198
|
+
* LLM serialization can recognize and frame it without a second role.
|
|
199
|
+
*/
|
|
200
|
+
historySource?: TurnSource
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export type { StepEvent as AgentStepEvent }
|
|
204
|
+
|
|
205
|
+
export interface StepEvent {
|
|
206
|
+
type: "text" | "tool_call" | "tool_result"
|
|
207
|
+
message: string
|
|
208
|
+
toolName?: string
|
|
209
|
+
isError?: boolean
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── Stream chunk types (compatible with providers/index.ts) ─────────────────
|
|
213
|
+
|
|
214
|
+
export interface StreamChunk {
|
|
215
|
+
agent?: { messages: any[]; streamed?: boolean }
|
|
216
|
+
tools?: { messages: any[] }
|
|
217
|
+
usage?: { input_tokens: number; output_tokens: number }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ─── Main agent loop ──────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
export async function* runAgent(
|
|
223
|
+
opts: AgentLoopOptions
|
|
224
|
+
): AsyncGenerator<StreamChunk> {
|
|
225
|
+
const t0 = performance.now()
|
|
226
|
+
|
|
227
|
+
// Load agent config from DB
|
|
228
|
+
const agentsCol = await col<AgentDoc>("agents")
|
|
229
|
+
const agentEntry = await agentsCol.get(opts.agentId)
|
|
230
|
+
if (!agentEntry) throw new Error(`Agent not found: ${opts.agentId}`)
|
|
231
|
+
const agent = agentEntry.doc
|
|
232
|
+
|
|
233
|
+
const agentName = agent.name || opts.agentId
|
|
234
|
+
const maxIterations = opts.budget?.maxIterations ?? agent.max_iterations ?? 10
|
|
235
|
+
|
|
236
|
+
// ── Durable run tracking ─────────────────────────────────────────────────
|
|
237
|
+
let runId: string | null = opts.runId ?? null
|
|
238
|
+
let isDurable = !!opts.durable || !!opts.runId || !!opts.goal
|
|
239
|
+
const runKind = opts.runKind ?? (opts.isolated ? "worker" : "chat")
|
|
240
|
+
const DURABLE_PROMOTION_THRESHOLD = 6 // promote chat to durable after N iterations
|
|
241
|
+
|
|
242
|
+
// ── G9 causal event log (HiveDB) ─────────────────────────────────────────
|
|
243
|
+
// One stream per invocation (a chat turn, or a runAgentIsolated() task) — NOT
|
|
244
|
+
// the persistent chat threadId, since causalThread() reconstructs without a
|
|
245
|
+
// checkpointed projection and a months-old thread would be O(full history)
|
|
246
|
+
// on every call. On resume, opts.runId is reused so the stream isn't split.
|
|
247
|
+
const causalLogEnabled = !!loadConfig().causalLog?.enabled
|
|
248
|
+
const causalDb = causalLogEnabled ? await getHiveDb() : null
|
|
249
|
+
const causalStreamId = opts.runId || crypto.randomUUID()
|
|
250
|
+
// hive has no mid-turn topic-change classifier yet, so the whole stream shares
|
|
251
|
+
// one correlation id. objectiveDrift is technically wired but won't fire in
|
|
252
|
+
// practice until that heuristic exists (documented v1 limitation).
|
|
253
|
+
const causalCorrelationId = crypto.randomUUID()
|
|
254
|
+
let lastCausalSeq: number | undefined
|
|
255
|
+
if (causalDb) {
|
|
256
|
+
const intentText = opts.rawUserMessage || (typeof opts.userMessage === "string"
|
|
257
|
+
? opts.userMessage
|
|
258
|
+
: Array.isArray(opts.userMessage)
|
|
259
|
+
? opts.userMessage.filter(p => p.type === "text").map(p => (p as any).text).join("\n")
|
|
260
|
+
: String(opts.userMessage))
|
|
261
|
+
lastCausalSeq = await appendCausalEvent(causalDb, {
|
|
262
|
+
agentId: opts.agentId,
|
|
263
|
+
streamId: causalStreamId,
|
|
264
|
+
kind: "IntentLogged",
|
|
265
|
+
payload: { actor: opts.agentId, intent: intentText.slice(0, 2000) },
|
|
266
|
+
correlation: causalCorrelationId,
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Stuck-loop protection
|
|
271
|
+
const stuckDetector = createStuckLoopDetector(loadConfig())
|
|
272
|
+
let stuckState: StuckLoopState | undefined
|
|
273
|
+
|
|
274
|
+
// Resolve LLM provider config (default from DB when the agent has none configured)
|
|
275
|
+
let agentProvider = fromIndexable(agent.provider_id)
|
|
276
|
+
let agentModel = fromIndexable(agent.model_id)
|
|
277
|
+
if (!agentProvider || !agentModel) {
|
|
278
|
+
const defaultLLM = await getDefaultLLM()
|
|
279
|
+
if (!defaultLLM) throw new Error("No active LLM providers/models configured in the database")
|
|
280
|
+
agentProvider = agentProvider || defaultLLM.provider
|
|
281
|
+
agentModel = agentModel || defaultLLM.model
|
|
282
|
+
}
|
|
283
|
+
const providerCfg = await resolveProviderConfig(agentProvider, agentModel)
|
|
284
|
+
|
|
285
|
+
const cleanModel = providerCfg.model.replace(new RegExp(`^${providerCfg.provider}\\/`), "")
|
|
286
|
+
log.info(`[agent-loop] Starting: agent=${agentName} thread=${opts.threadId} provider=${providerCfg.provider}/${cleanModel}`)
|
|
287
|
+
|
|
288
|
+
emitCanvas("canvas:node_update", {
|
|
289
|
+
nodeId: opts.agentId,
|
|
290
|
+
changes: { status: "thinking" },
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
// Store the user message in conversation history
|
|
294
|
+
if (!opts.isolated) {
|
|
295
|
+
// If userMessage is multimodal, addMessage extracts text for history storage.
|
|
296
|
+
// historySource records provenance; system-originated turns (delegation
|
|
297
|
+
// fan-in) persist as role:"user" like everything else and stay off the
|
|
298
|
+
// visible transcript purely because their source is internal.
|
|
299
|
+
await addMessage(opts.threadId, "user", opts.userMessage, {
|
|
300
|
+
channel: opts.channel,
|
|
301
|
+
source: opts.historySource ?? "message",
|
|
302
|
+
})
|
|
303
|
+
// Run compaction if conversation history is getting large
|
|
304
|
+
await maybeCompact(
|
|
305
|
+
opts.threadId,
|
|
306
|
+
opts.channel && opts.userId
|
|
307
|
+
? { channel: opts.channel, userId: opts.userId }
|
|
308
|
+
: undefined
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Compile context (system prompt + history + tools)
|
|
313
|
+
const ctx = await compileContext({
|
|
314
|
+
agentId: opts.agentId,
|
|
315
|
+
threadId: opts.threadId,
|
|
316
|
+
userMessage: opts.userMessage,
|
|
317
|
+
channel: opts.channel,
|
|
318
|
+
mcpManager: opts.mcpManager,
|
|
319
|
+
isolated: opts.isolated,
|
|
320
|
+
taskContext: opts.taskContext,
|
|
321
|
+
userId: opts.userId,
|
|
322
|
+
causalStreamId,
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
// Force extra tools into the loadout (tests/evals)
|
|
326
|
+
if (opts.extraTools?.length) {
|
|
327
|
+
const existingNames = new Set(ctx.tools.map((t: any) => t.function?.name))
|
|
328
|
+
for (const tool of opts.extraTools) {
|
|
329
|
+
const name = tool.function?.name || tool.name
|
|
330
|
+
if (name && !existingNames.has(name)) {
|
|
331
|
+
ctx.tools.push(tool)
|
|
332
|
+
existingNames.add(name)
|
|
333
|
+
log.info(`[agent-loop] Force-injected tool into loadout: ${name}`)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Compose rather than discard: an override must still carry the
|
|
339
|
+
// conversation summary context-compiler folded in, or a compacted thread
|
|
340
|
+
// silently loses it whenever a caller supplies systemPromptOverride.
|
|
341
|
+
const systemPrompt = opts.systemPromptOverride
|
|
342
|
+
? opts.systemPromptOverride + ctx.conversationSummarySection
|
|
343
|
+
: ctx.systemPrompt
|
|
344
|
+
|
|
345
|
+
// Build initial messages array for the model
|
|
346
|
+
let messages: LLMMessage[] = [
|
|
347
|
+
{ role: "system", content: systemPrompt },
|
|
348
|
+
...ctx.messages,
|
|
349
|
+
]
|
|
350
|
+
|
|
351
|
+
// For isolated workers the user message is the task context, not from history
|
|
352
|
+
if (opts.isolated) {
|
|
353
|
+
messages.push({ role: "user", content: opts.userMessage })
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ── Resume from checkpoint ─────────────────────────────────────────────────
|
|
357
|
+
let injectedToolNames: string[] = []
|
|
358
|
+
let systemPromptSkillSections: string[] = []
|
|
359
|
+
let resumedFromPending = false
|
|
360
|
+
let iterations = 0
|
|
361
|
+
let totalInputTokens = 0
|
|
362
|
+
let totalOutputTokens = 0
|
|
363
|
+
let lastToolSignature = ""
|
|
364
|
+
let consecutiveRepeat = 0
|
|
365
|
+
let idleIterations = 0
|
|
366
|
+
|
|
367
|
+
if (opts.resume && runId) {
|
|
368
|
+
const existing = await getRun(runId)
|
|
369
|
+
if (existing && existing.state_json) {
|
|
370
|
+
const restored = deserializeCheckpoint(existing)
|
|
371
|
+
if (restored) {
|
|
372
|
+
messages = restored.messages
|
|
373
|
+
injectedToolNames = restored.injectedToolNames ?? []
|
|
374
|
+
systemPromptSkillSections = restored.systemPromptSkillSections ?? []
|
|
375
|
+
iterations = restored.iterations ?? 0
|
|
376
|
+
totalInputTokens = restored.totalInputTokens ?? 0
|
|
377
|
+
totalOutputTokens = restored.totalOutputTokens ?? 0
|
|
378
|
+
lastToolSignature = restored.lastToolSignature ?? ""
|
|
379
|
+
consecutiveRepeat = restored.consecutiveRepeat ?? 0
|
|
380
|
+
idleIterations = restored.idleIterations ?? 0
|
|
381
|
+
if (existing.pending_tool_calls_json) {
|
|
382
|
+
try {
|
|
383
|
+
const pending = JSON.parse(existing.pending_tool_calls_json)
|
|
384
|
+
const interruptedMsgs = pending.map((tc: any) => ({
|
|
385
|
+
role: "tool" as const,
|
|
386
|
+
content: "[interrupted] El proceso se reinició mientras esta herramienta corría. El resultado no está disponible — decidí si reintentar o continuar sin él.",
|
|
387
|
+
tool_call_id: tc.id,
|
|
388
|
+
}))
|
|
389
|
+
messages.push(...interruptedMsgs)
|
|
390
|
+
resumedFromPending = true
|
|
391
|
+
log.info(`[agent-loop] Resume: injected ${interruptedMsgs.length} synthetic [interrupted] tool message(s)`)
|
|
392
|
+
} catch { /* ignore bad json */ }
|
|
393
|
+
}
|
|
394
|
+
log.info(`[agent-loop] Resume: restored ${messages.length} messages, ${iterations} iterations from run ${runId}`)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ── Create AgentRun if durable ────────────────────────────────────────────
|
|
400
|
+
if (!runId && isDurable) {
|
|
401
|
+
const run = await createAgentRun({
|
|
402
|
+
thread_id: opts.threadId,
|
|
403
|
+
agent_id: opts.agentId,
|
|
404
|
+
user_id: opts.userId ?? "",
|
|
405
|
+
channel: opts.channel ?? null,
|
|
406
|
+
kind: runKind,
|
|
407
|
+
max_iterations: maxIterations,
|
|
408
|
+
max_turns: opts.budget?.maxTurns ?? null,
|
|
409
|
+
max_tokens: opts.budget?.maxTokens ?? null,
|
|
410
|
+
goal: opts.goal?.text ?? null,
|
|
411
|
+
goal_check_tool: opts.goal?.checkTool ?? null,
|
|
412
|
+
resume_policy: "resume",
|
|
413
|
+
})
|
|
414
|
+
runId = run.id
|
|
415
|
+
log.info(`[agent-loop] Created durable run ${runId} (kind=${runKind})`)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Re-running an existing run (e.g. a job re-claimed after a crash): reconcile
|
|
419
|
+
// may have left it "interrupted" with a stale boot_id, and the lease renewer
|
|
420
|
+
// self-stops unless status is "running" — take ownership before executing.
|
|
421
|
+
if (opts.runId && isDurable) {
|
|
422
|
+
await reclaimRun(opts.runId).catch((err) =>
|
|
423
|
+
log.warn(`[agent-loop] Failed to reclaim run ${opts.runId}: ${(err as Error).message}`)
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Start lease renewal timer if durable
|
|
428
|
+
if (runId && isDurable) {
|
|
429
|
+
startLeaseRenewal(runId)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// The try wraps the whole loop + synthesis + finalization WITHOUT re-indenting
|
|
433
|
+
// the body: it guarantees the durable run never stays "running" with a live
|
|
434
|
+
// lease when the loop throws or the consumer abandons the generator.
|
|
435
|
+
try {
|
|
436
|
+
|
|
437
|
+
let finalContent = ""
|
|
438
|
+
// Whether finalContent was already emitted to the stream (normal completion path
|
|
439
|
+
// yields it at the top of the iteration; internal breaks set finalContent without yielding)
|
|
440
|
+
let finalEmitted = false
|
|
441
|
+
let loopDetected = false
|
|
442
|
+
const PROGRESS_TOOLS = new Set(["browser_type", "browser_click", "browser_navigate"])
|
|
443
|
+
|
|
444
|
+
// ── The loop ────────────────────────────────────────────────────────────
|
|
445
|
+
while (iterations < maxIterations) {
|
|
446
|
+
if (opts.signal?.aborted) {
|
|
447
|
+
log.info(`[agent-loop] Aborted by signal at iteration ${iterations}`)
|
|
448
|
+
finalContent = "Generación detenida."
|
|
449
|
+
break
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
iterations++
|
|
453
|
+
|
|
454
|
+
const delegationGroupAtCall = opts.turnId && !opts.isolated
|
|
455
|
+
? await import("../gateway/delegation-groups").then((mod) => mod.getDelegationGroup(opts.turnId!))
|
|
456
|
+
: null
|
|
457
|
+
let streamedThisCall = false
|
|
458
|
+
let response: Awaited<ReturnType<typeof callLLM>>
|
|
459
|
+
try {
|
|
460
|
+
response = await withTimeout(() => callLLM({
|
|
461
|
+
...providerCfg,
|
|
462
|
+
messages: clearOldToolResults(messages) as LLMMessage[],
|
|
463
|
+
tools: ctx.tools.length > 0 ? ctx.tools : undefined,
|
|
464
|
+
signal: opts.signal,
|
|
465
|
+
onToken: opts.onToken && !delegationGroupAtCall
|
|
466
|
+
? (token: string) => {
|
|
467
|
+
streamedThisCall = true
|
|
468
|
+
opts.onToken?.(token)
|
|
469
|
+
}
|
|
470
|
+
: undefined,
|
|
471
|
+
onReasoningToken: opts.onReasoningToken,
|
|
472
|
+
// Always requested; each provider decides internally whether/how to honor
|
|
473
|
+
// it based on its own model-capability checks (safe no-op otherwise).
|
|
474
|
+
thinking: { enabled: true },
|
|
475
|
+
}), LLM_CALL_TIMEOUT_MS)
|
|
476
|
+
} catch (err) {
|
|
477
|
+
if (err instanceof LLMCallTimeoutError) {
|
|
478
|
+
log.warn(`[agent-loop] ${err.message} at iteration ${iterations}. Breaking.`)
|
|
479
|
+
finalContent = "El modelo tardó demasiado en responder. Intentá de nuevo o simplificá la consulta."
|
|
480
|
+
break
|
|
481
|
+
}
|
|
482
|
+
throw err
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (
|
|
486
|
+
delegationGroupAtCall &&
|
|
487
|
+
(!response.tool_calls?.length || response.stop_reason !== "tool_calls")
|
|
488
|
+
) {
|
|
489
|
+
response.content = ""
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Accumulate usage
|
|
493
|
+
if (response.usage) {
|
|
494
|
+
totalInputTokens += response.usage.input_tokens
|
|
495
|
+
totalOutputTokens += response.usage.output_tokens
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// G9: record this LLM response as a causal "decision", chained off the
|
|
499
|
+
// previous decision (or the initial IntentLogged for the first one).
|
|
500
|
+
if (causalDb) {
|
|
501
|
+
const description = response.content?.trim()
|
|
502
|
+
|| (response.tool_calls?.length
|
|
503
|
+
? `Calling ${response.tool_calls.map((tc) => tc.function.name).join(", ")}`
|
|
504
|
+
: "(empty response)")
|
|
505
|
+
const seq = await appendCausalEvent(causalDb, {
|
|
506
|
+
agentId: opts.agentId,
|
|
507
|
+
streamId: causalStreamId,
|
|
508
|
+
kind: "StateTransition",
|
|
509
|
+
payload: { description: description.slice(0, 2000) },
|
|
510
|
+
causation: lastCausalSeq,
|
|
511
|
+
correlation: causalCorrelationId,
|
|
512
|
+
})
|
|
513
|
+
if (seq !== undefined) lastCausalSeq = seq
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Emit agent chunk (compatible with providers/index.ts)
|
|
517
|
+
const agentMsg: any = { content: response.content }
|
|
518
|
+
if (response.tool_calls?.length) agentMsg.tool_calls = response.tool_calls
|
|
519
|
+
yield { agent: { messages: [agentMsg], streamed: streamedThisCall } }
|
|
520
|
+
|
|
521
|
+
// Notify onStep for narration text
|
|
522
|
+
if (opts.onStep && response.content) {
|
|
523
|
+
await opts.onStep({ type: "text", message: response.content })
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ── Provider failure → surface it, but never let it enter the history ───
|
|
527
|
+
// callLLM returns errors as a normal response whose `content` is the error
|
|
528
|
+
// text. That text is for the user's screen only: persisting it would make
|
|
529
|
+
// the next turn replay a provider outage as something the agent "said".
|
|
530
|
+
if (response.stop_reason === "error") {
|
|
531
|
+
log.error(`[agent-loop] LLM call failed at iteration ${iterations}: ${response.error?.message ?? response.content}`)
|
|
532
|
+
finalContent = response.content?.trim() || ""
|
|
533
|
+
finalEmitted = true // already yielded above as the agent chunk
|
|
534
|
+
break
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ── No tool calls → final response ──────────────────────────────────
|
|
538
|
+
if (!response.tool_calls?.length || response.stop_reason !== "tool_calls") {
|
|
539
|
+
finalContent = response.content?.trim() || ""
|
|
540
|
+
finalEmitted = true // already yielded above as the agent chunk
|
|
541
|
+
// Only save to history if we have real content; empty → synthesis block will handle it
|
|
542
|
+
if (finalContent && !opts.isolated) {
|
|
543
|
+
await addMessage(opts.threadId, "assistant", finalContent)
|
|
544
|
+
}
|
|
545
|
+
break
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ── Tool calls → execute each tool ──────────────────────────────────
|
|
549
|
+
// Add assistant message with tool_calls to local messages array AND persist
|
|
550
|
+
messages.push({
|
|
551
|
+
role: "assistant",
|
|
552
|
+
content: response.content,
|
|
553
|
+
tool_calls: response.tool_calls,
|
|
554
|
+
reasoning_content: response.reasoning_content,
|
|
555
|
+
thinking_blocks: response.thinking_blocks,
|
|
556
|
+
})
|
|
557
|
+
// Note: assistant messages with tool_calls are NOT persisted to DB.
|
|
558
|
+
// Only the final text response to the user is saved.
|
|
559
|
+
// Tool-call round-tripping happens in-memory via the 'messages' array above.
|
|
560
|
+
|
|
561
|
+
for (const tc of response.tool_calls) {
|
|
562
|
+
const toolName = tc.function.name
|
|
563
|
+
|
|
564
|
+
emitCanvas("canvas:node_update", {
|
|
565
|
+
nodeId: opts.agentId,
|
|
566
|
+
changes: { status: "tool_call", currentTool: toolName },
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
if (opts.onStep) {
|
|
570
|
+
if (response.content) {
|
|
571
|
+
await opts.onStep({ type: "text", message: response.content })
|
|
572
|
+
}
|
|
573
|
+
await opts.onStep({
|
|
574
|
+
type: "tool_call",
|
|
575
|
+
toolName,
|
|
576
|
+
message: `Calling tool: \`${toolName}\``,
|
|
577
|
+
})
|
|
578
|
+
}
|
|
579
|
+
if (opts.turnId) {
|
|
580
|
+
await publishNarration({
|
|
581
|
+
turnId: opts.turnId,
|
|
582
|
+
threadId: opts.threadId,
|
|
583
|
+
channel: opts.channel,
|
|
584
|
+
userId: opts.userId,
|
|
585
|
+
sessionId: opts.sessionId,
|
|
586
|
+
agentId: opts.agentId,
|
|
587
|
+
agentName,
|
|
588
|
+
kind: "tool_call",
|
|
589
|
+
status: "running",
|
|
590
|
+
// Human-readable text, not the raw tool id — this label is what a
|
|
591
|
+
// WhatsApp/Telegram user reads.
|
|
592
|
+
label: `${agentName}: ${getNarration(toolName)}`,
|
|
593
|
+
dedupeKey: `tool_call:${iterations}:${tc.id}:${toolName}`,
|
|
594
|
+
})
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const hiveConfig = loadConfig()
|
|
599
|
+
|
|
600
|
+
// ── Checkpoint: persist pending tool_calls BEFORE execution ──────────────
|
|
601
|
+
// If the process crashes mid-tool, the resume will inject synthetic
|
|
602
|
+
// [interrupted] tool messages instead of re-executing the tool.
|
|
603
|
+
if (runId && isDurable) {
|
|
604
|
+
try {
|
|
605
|
+
await checkpointRun(runId, {
|
|
606
|
+
version: 1,
|
|
607
|
+
messages: [...messages],
|
|
608
|
+
iterations,
|
|
609
|
+
totalInputTokens,
|
|
610
|
+
totalOutputTokens,
|
|
611
|
+
lastToolSignature,
|
|
612
|
+
consecutiveRepeat,
|
|
613
|
+
idleIterations,
|
|
614
|
+
injectedToolNames,
|
|
615
|
+
systemPromptSkillSections,
|
|
616
|
+
}, response.tool_calls)
|
|
617
|
+
} catch (err) {
|
|
618
|
+
log.warn(`[agent-loop] Pre-tool checkpoint failed: ${(err as Error).message}`)
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const toolResults = await executeToolBatch({
|
|
623
|
+
toolCalls: response.tool_calls,
|
|
624
|
+
allTools: ctx.allTools,
|
|
625
|
+
toolConfig: {
|
|
626
|
+
user_id: opts.userId,
|
|
627
|
+
thread_id: opts.threadId,
|
|
628
|
+
channel: opts.channel,
|
|
629
|
+
workspace: agent.workspace ?? null,
|
|
630
|
+
// Tools read this to know who's calling them (task_delegate's parent
|
|
631
|
+
// lookup, agent_create's parent_id, bus_publish's sender) — was
|
|
632
|
+
// missing entirely before, so config.configurable.agent_id was always
|
|
633
|
+
// undefined inside every tool execute().
|
|
634
|
+
agent_id: opts.agentId,
|
|
635
|
+
run_id: runId ?? opts.runId,
|
|
636
|
+
turn_id: opts.turnId,
|
|
637
|
+
task_id: opts.taskId,
|
|
638
|
+
session_id: opts.sessionId,
|
|
639
|
+
},
|
|
640
|
+
hiveConfig,
|
|
641
|
+
workerPool: hiveConfig.tools?.workerPool,
|
|
642
|
+
signal: opts.signal,
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
for (const batchResult of toolResults) {
|
|
646
|
+
const tc = batchResult.toolCall
|
|
647
|
+
const toolName = batchResult.toolName
|
|
648
|
+
const toolResultJS = batchResult.result
|
|
649
|
+
const toolMs = batchResult.durationMs
|
|
650
|
+
|
|
651
|
+
// Encode TOON only for LLM consumption (with cost calculation)
|
|
652
|
+
const toolResultLLM = formatToolResult(toolResultJS, cleanModel)
|
|
653
|
+
|
|
654
|
+
log.info(`[agent-loop] Tool ${toolName} completed in ${toolMs}ms`)
|
|
655
|
+
|
|
656
|
+
// Log tool result preview (truncated to avoid flooding logs)
|
|
657
|
+
const resultPreview = toolResultLLM.length > 500
|
|
658
|
+
? toolResultLLM.substring(0, 500) + `… (+${toolResultLLM.length - 500} chars)`
|
|
659
|
+
: toolResultLLM
|
|
660
|
+
log.info(`[agent-loop] Tool result [${toolName}]: ${resultPreview}`)
|
|
661
|
+
|
|
662
|
+
// Extract text for trace summary
|
|
663
|
+
const textMessage = typeof opts.userMessage === "string"
|
|
664
|
+
? opts.userMessage
|
|
665
|
+
: Array.isArray(opts.userMessage)
|
|
666
|
+
? opts.userMessage.filter(p => p.type === "text").map(p => (p as any).text).join("\n")
|
|
667
|
+
: String(opts.userMessage)
|
|
668
|
+
|
|
669
|
+
// Clean timestamp from message for trace
|
|
670
|
+
const cleanMessage = textMessage.replace(/^\[Timestamp:.*?\]\n/, "")
|
|
671
|
+
|
|
672
|
+
// Save tool call trace
|
|
673
|
+
saveTrace({
|
|
674
|
+
threadId: opts.threadId,
|
|
675
|
+
agentId: opts.agentId,
|
|
676
|
+
agentName,
|
|
677
|
+
toolUsed: toolName,
|
|
678
|
+
inputSummary: `${cleanMessage.substring(0, 200)} → ${toolName}`,
|
|
679
|
+
outputSummary: toolResultLLM.substring(0, 300),
|
|
680
|
+
success: !toolResultLLM.startsWith("[Tool Error]"),
|
|
681
|
+
errorMessage: toolResultLLM.startsWith("[Tool Error]") ? toolResultLLM : null,
|
|
682
|
+
durationMs: toolMs,
|
|
683
|
+
causalStreamId,
|
|
684
|
+
catalogAgentId: agent.source === "catalog" ? agent.id : undefined,
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
// G9: record the tool call, caused by the decision that requested it.
|
|
688
|
+
// Canonical outcome shape ("Ok" | "Timeout" | {Err}) — see
|
|
689
|
+
// hiveBD docs/AGENT_INTEGRATION.md; anything else silently counts as Ok.
|
|
690
|
+
if (causalDb) {
|
|
691
|
+
const outcome = batchResult.timedOut
|
|
692
|
+
? "Timeout"
|
|
693
|
+
: batchResult.ok
|
|
694
|
+
? "Ok"
|
|
695
|
+
: { Err: batchResult.error?.message ?? toolResultLLM.slice(0, 300) }
|
|
696
|
+
await appendCausalEvent(causalDb, {
|
|
697
|
+
agentId: opts.agentId,
|
|
698
|
+
streamId: causalStreamId,
|
|
699
|
+
kind: "ToolCall",
|
|
700
|
+
payload: { tool: toolName, latency_ms: toolMs, outcome },
|
|
701
|
+
causation: lastCausalSeq,
|
|
702
|
+
correlation: causalCorrelationId,
|
|
703
|
+
})
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Emit tool chunk (TOON encoded for LLM)
|
|
707
|
+
yield { tools: { messages: [{ content: toolResultLLM, tool_call_id: tc.id, name: toolName }] } }
|
|
708
|
+
|
|
709
|
+
if (opts.onStep) {
|
|
710
|
+
await opts.onStep({ type: "tool_result", message: toolResultLLM })
|
|
711
|
+
}
|
|
712
|
+
if (opts.turnId) {
|
|
713
|
+
await publishNarration({
|
|
714
|
+
turnId: opts.turnId,
|
|
715
|
+
threadId: opts.threadId,
|
|
716
|
+
channel: opts.channel,
|
|
717
|
+
userId: opts.userId,
|
|
718
|
+
sessionId: opts.sessionId,
|
|
719
|
+
agentId: opts.agentId,
|
|
720
|
+
agentName,
|
|
721
|
+
kind: "tool_result",
|
|
722
|
+
status: batchResult.ok ? "done" : "error",
|
|
723
|
+
label: batchResult.ok
|
|
724
|
+
? `${agentName} recibió el resultado de ${toolName}`
|
|
725
|
+
: `${agentName} recibió un error de ${toolName}`,
|
|
726
|
+
detail: batchResult.ok ? null : batchResult.error?.message ?? null,
|
|
727
|
+
dedupeKey: `tool_result:${iterations}:${tc.id}:${toolName}`,
|
|
728
|
+
})
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Add tool result to messages for next model call (in-memory only, NOT persisted to DB)
|
|
732
|
+
messages.push({
|
|
733
|
+
role: "tool",
|
|
734
|
+
content: toolResultLLM,
|
|
735
|
+
tool_call_id: tc.id,
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
// Record tool call for stuck-loop detection
|
|
739
|
+
const errorMessage = toolResultLLM.startsWith("[Tool Error]") ? toolResultLLM : undefined
|
|
740
|
+
stuckDetector.recordToolCall(opts.threadId, toolName, tc.function.arguments as Record<string, unknown>, errorMessage)
|
|
741
|
+
|
|
742
|
+
// Dynamic tool injection: when search_knowledge finds tools (native or MCP), add them to ctx.tools
|
|
743
|
+
if (toolName === "search_knowledge") {
|
|
744
|
+
// Use JS object directly (no parse needed)
|
|
745
|
+
try {
|
|
746
|
+
const result = toolResultJS as any
|
|
747
|
+
const foundTools: Array<{ name: string }> = result?.tools ?? []
|
|
748
|
+
const foundMcpTools: Array<{ tool_name: string; full_name?: string; id?: string }> = result?.toolsmcp ?? []
|
|
749
|
+
const currentToolNames = new Set(ctx.tools.map((t: any) => t.function?.name))
|
|
750
|
+
|
|
751
|
+
// Track which tools were injected for skill lookup
|
|
752
|
+
const injectedTools: string[] = []
|
|
753
|
+
|
|
754
|
+
// Inject native tools
|
|
755
|
+
for (const found of foundTools) {
|
|
756
|
+
if (!currentToolNames.has(found.name)) {
|
|
757
|
+
let nativeTool = ctx.allTools.find(t => t.name === found.name)
|
|
758
|
+
// Fallback: try alternative naming (dots ↔ underscores for legacy DB names)
|
|
759
|
+
if (!nativeTool) {
|
|
760
|
+
const altName = found.name.includes(".")
|
|
761
|
+
? found.name.replace(/\./g, "_")
|
|
762
|
+
: found.name.replace(/_/g, ".")
|
|
763
|
+
nativeTool = ctx.allTools.find(t => t.name === altName)
|
|
764
|
+
if (nativeTool) {
|
|
765
|
+
log.info(`[agent-loop] Resolved legacy tool name "${found.name}" → "${nativeTool.name}"`)
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (nativeTool) {
|
|
769
|
+
ctx.tools.push({
|
|
770
|
+
type: "function",
|
|
771
|
+
function: {
|
|
772
|
+
name: nativeTool.name,
|
|
773
|
+
description: (nativeTool as any).description ?? "",
|
|
774
|
+
parameters: (nativeTool as any).parameters ?? { type: "object", properties: {} },
|
|
775
|
+
},
|
|
776
|
+
})
|
|
777
|
+
log.info(`[agent-loop] Injected discovered native tool into loadout: ${nativeTool.name}`)
|
|
778
|
+
currentToolNames.add(found.name)
|
|
779
|
+
injectedTools.push(nativeTool.name)
|
|
780
|
+
} else {
|
|
781
|
+
log.warn(`[agent-loop] search_knowledge returned tool "${found.name}" but no matching executor found in allTools`)
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Inject MCP tools discovered via search_knowledge(type="mcp")
|
|
787
|
+
for (const found of foundMcpTools) {
|
|
788
|
+
// Use full_name (sanitized compound id) because ctx.allTools stores MCP tools
|
|
789
|
+
// under the sanitized name (e.g. "Instagram__mis_estadisticas_de_instagram"),
|
|
790
|
+
// NOT the original tool_name (e.g. "mis estadisticas de instagram").
|
|
791
|
+
const mcpFullName = found.full_name || found.id
|
|
792
|
+
log.debug(`[agent-loop] MCP discovery candidate: tool_name="${found.tool_name}", full_name="${found.full_name}", id="${found.id}", resolved="${mcpFullName}"`)
|
|
793
|
+
if (!currentToolNames.has(mcpFullName)) {
|
|
794
|
+
const mcpTool = ctx.allTools.find(t => t.name === mcpFullName)
|
|
795
|
+
if (mcpTool) {
|
|
796
|
+
ctx.tools.push({
|
|
797
|
+
type: "function",
|
|
798
|
+
function: {
|
|
799
|
+
name: mcpTool.name,
|
|
800
|
+
description: (mcpTool as any).description ?? "",
|
|
801
|
+
parameters: (mcpTool as any).parameters ?? { type: "object", properties: {} },
|
|
802
|
+
},
|
|
803
|
+
})
|
|
804
|
+
log.info(`[agent-loop] Injected discovered MCP tool into loadout: ${mcpTool.name}`)
|
|
805
|
+
currentToolNames.add(mcpFullName)
|
|
806
|
+
} else {
|
|
807
|
+
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(', ')})`)
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Inject skills associated with the injected tools
|
|
813
|
+
if (injectedTools.length > 0) {
|
|
814
|
+
try {
|
|
815
|
+
const skillsCol = await col<import("../storage/collections").SkillDoc>("skills")
|
|
816
|
+
// Find skills that use any of the injected tools
|
|
817
|
+
const activeSkills = (await skillsCol.scan({})).filter(e => e.doc.active)
|
|
818
|
+
const skillsWithTools = activeSkills
|
|
819
|
+
.filter(e => injectedTools.some(t => e.doc.tools?.includes(t)))
|
|
820
|
+
.map(e => ({ name: e.doc.name, body: e.doc.body, tools: e.doc.tools }))
|
|
821
|
+
|
|
822
|
+
// Filter to only skills that actually contain the tools (not partial matches)
|
|
823
|
+
const matchingSkills = skillsWithTools.filter(s => {
|
|
824
|
+
const skillTools = s.tools?.split(",").map(t => t.trim()) ?? []
|
|
825
|
+
return injectedTools.some(injected => skillTools.includes(injected))
|
|
826
|
+
})
|
|
827
|
+
|
|
828
|
+
if (matchingSkills.length > 0) {
|
|
829
|
+
const skillSection = matchingSkills
|
|
830
|
+
.map(s => `## Skill: ${s.name}\n${s.body}`)
|
|
831
|
+
.join("\n\n")
|
|
832
|
+
|
|
833
|
+
// Add skill instructions to system prompt. messages[0] is
|
|
834
|
+
// always the system prompt by construction — there is
|
|
835
|
+
// exactly one system message in the array (see its
|
|
836
|
+
// construction above), so index instead of scanning.
|
|
837
|
+
const systemMsg = messages[0]?.role === "system" ? messages[0] : undefined
|
|
838
|
+
if (systemMsg && typeof systemMsg.content === "string") {
|
|
839
|
+
// Check if we already added this skill
|
|
840
|
+
const existingSkillNames = new Set(
|
|
841
|
+
(systemMsg.content.match(/## Skill: ([^\n]+)/g) || [])
|
|
842
|
+
.map(m => m.replace("## Skill: ", "").trim())
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
const newSkills = matchingSkills.filter(s => !existingSkillNames.has(s.name))
|
|
846
|
+
if (newSkills.length > 0) {
|
|
847
|
+
const newSkillSection = newSkills
|
|
848
|
+
.map(s => `## Skill: ${s.name}\n${s.body}`)
|
|
849
|
+
.join("\n\n")
|
|
850
|
+
|
|
851
|
+
systemMsg.content += `\n\n--- SKILL INSTRUCTIONS (Auto-loaded) ---\n${newSkillSection}`
|
|
852
|
+
log.info(`[agent-loop] Injected ${newSkills.length} skill(s) for tools: ${newSkills.map(s => s.name).join(", ")}`)
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
} catch (skillErr) {
|
|
857
|
+
log.warn(`[agent-loop] Failed to inject skills for tools: ${(skillErr as Error).message}`)
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
} catch (err) {
|
|
861
|
+
log.warn(`[agent-loop] search_knowledge tool injection failed: ${(err as Error).message}`)
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Enrich the tool result with skill instructions and playbook rules
|
|
865
|
+
try {
|
|
866
|
+
const result = toolResultJS as any
|
|
867
|
+
const foundSkills: Array<{ name: string; body?: string }> = result?.skills ?? []
|
|
868
|
+
const foundPlaybook: Array<{ rule: string; category?: string }> = result?.playbook ?? []
|
|
869
|
+
|
|
870
|
+
if (foundSkills.length > 0 || foundPlaybook.length > 0) {
|
|
871
|
+
const extras: string[] = []
|
|
872
|
+
|
|
873
|
+
if (foundSkills.some((s: any) => s.body)) {
|
|
874
|
+
const section = foundSkills
|
|
875
|
+
.filter((s: any) => s.body)
|
|
876
|
+
.map((s: any) => `## Skill: ${s.name}\n${s.body}`)
|
|
877
|
+
.join("\n\n")
|
|
878
|
+
extras.push(`\n\n--- SKILL INSTRUCTIONS ---\n${section}`)
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (foundPlaybook.length > 0) {
|
|
882
|
+
const section = foundPlaybook.map((p: any) => `- [${p.category ?? "general"}] ${p.rule}`).join("\n")
|
|
883
|
+
extras.push(`\n\n--- PLAYBOOK RULES ---\n${section}`)
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (extras.length > 0) {
|
|
887
|
+
const lastMsg = messages[messages.length - 1]
|
|
888
|
+
if (lastMsg?.role === "tool") {
|
|
889
|
+
lastMsg.content += extras.join("")
|
|
890
|
+
log.info(`[agent-loop] Enriched search_knowledge result with ${foundSkills.length} skill(s) and ${foundPlaybook.length} rule(s)`)
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
} catch (err) {
|
|
895
|
+
log.warn(`[agent-loop] search_knowledge enrichment failed: ${(err as Error).message}`)
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Loop detection: same tool + same args called consecutively → break
|
|
900
|
+
const sig = `${toolName}:${JSON.stringify(tc.function.arguments)}`
|
|
901
|
+
if (sig === lastToolSignature) {
|
|
902
|
+
consecutiveRepeat++
|
|
903
|
+
if (consecutiveRepeat >= 2) {
|
|
904
|
+
log.warn(`[agent-loop] Loop detected: "${toolName}" x${consecutiveRepeat + 1} with same args. Breaking.`)
|
|
905
|
+
finalContent = "No pude completar la tarea porque no encontré las herramientas necesarias para ello."
|
|
906
|
+
loopDetected = true
|
|
907
|
+
}
|
|
908
|
+
} else {
|
|
909
|
+
lastToolSignature = sig
|
|
910
|
+
consecutiveRepeat = 0
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (loopDetected) break
|
|
915
|
+
|
|
916
|
+
// Check for stuck loop after each iteration
|
|
917
|
+
stuckState = stuckDetector.check(opts.threadId)
|
|
918
|
+
if (stuckState.detected) {
|
|
919
|
+
const intervention = getInterventionMessage(stuckState)
|
|
920
|
+
log.warn(`[agent-loop] ${intervention}`)
|
|
921
|
+
|
|
922
|
+
if (stuckState.count >= 4) {
|
|
923
|
+
// Critical: break and notify user instead of looping forever
|
|
924
|
+
finalContent = intervention
|
|
925
|
+
loopDetected = true
|
|
926
|
+
emitCanvas("canvas:node_update", {
|
|
927
|
+
nodeId: opts.agentId,
|
|
928
|
+
changes: { status: "stuck", currentTool: stuckState.toolName },
|
|
929
|
+
})
|
|
930
|
+
break
|
|
931
|
+
} else {
|
|
932
|
+
// Warning: inject intervention message so the model changes strategy
|
|
933
|
+
messages.push({
|
|
934
|
+
role: "user",
|
|
935
|
+
content: intervention,
|
|
936
|
+
})
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Stall detection: browser task inspecting the page repeatedly without acting on it.
|
|
941
|
+
// Only iterations that actually used browser tools count — filesystem/knowledge/etc.
|
|
942
|
+
// tool calls are real progress for non-browser tasks and must not trip this heuristic.
|
|
943
|
+
const usedBrowserTools = toolResults.some((r) => r.toolName.startsWith("browser_"))
|
|
944
|
+
const hadProgress = toolResults.some(
|
|
945
|
+
(r) => PROGRESS_TOOLS.has(r.toolName) && !String(r.result).startsWith("[Tool Error]")
|
|
946
|
+
)
|
|
947
|
+
if (hadProgress) {
|
|
948
|
+
idleIterations = 0
|
|
949
|
+
} else if (usedBrowserTools) {
|
|
950
|
+
idleIterations++
|
|
951
|
+
}
|
|
952
|
+
if (idleIterations >= 3 && idleIterations < 5) {
|
|
953
|
+
const stallMsg = "ADVERTENCIA: Llevas varios pasos sin modificar la página. Si ya completaste el formulario, responde al usuario. Si no, avanza con browser_type/browser_click en lugar de seguir inspeccionando."
|
|
954
|
+
log.warn(`[agent-loop] ${stallMsg}`)
|
|
955
|
+
messages.push({ role: "user", content: stallMsg })
|
|
956
|
+
} else if (idleIterations >= 5) {
|
|
957
|
+
const stallMsg = "No logré avanzar en el formulario después de varios intentos. Puede que la página no sea compatible o que falten instrucciones. Te sugiero revisar la URL o darme más detalles."
|
|
958
|
+
log.warn(`[agent-loop] Stall break: ${stallMsg}`)
|
|
959
|
+
finalContent = stallMsg
|
|
960
|
+
emitCanvas("canvas:node_update", {
|
|
961
|
+
nodeId: opts.agentId,
|
|
962
|
+
changes: { status: "stuck", currentTool: "NO_PROGRESS" },
|
|
963
|
+
})
|
|
964
|
+
break
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
emitCanvas("canvas:node_update", {
|
|
968
|
+
nodeId: opts.agentId,
|
|
969
|
+
changes: { status: "thinking", currentTool: null },
|
|
970
|
+
})
|
|
971
|
+
|
|
972
|
+
// ── Post-tool checkpoint (pending_tool_calls cleared) ───────────────────
|
|
973
|
+
// Track injected tools for checkpoint
|
|
974
|
+
if (toolResults.some(r => r.toolName === "search_knowledge")) {
|
|
975
|
+
const alreadyTracked = new Set(injectedToolNames)
|
|
976
|
+
for (const tr of toolResults) {
|
|
977
|
+
if (tr.toolName !== "search_knowledge") {
|
|
978
|
+
const nativeTool = ctx.allTools.find(t => t.name === tr.toolName)
|
|
979
|
+
if (nativeTool && !alreadyTracked.has(tr.toolName)) {
|
|
980
|
+
injectedToolNames.push(tr.toolName)
|
|
981
|
+
alreadyTracked.add(tr.toolName)
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Auto-promote to durable for long-running chat turns
|
|
988
|
+
if (!isDurable && !opts.isolated && iterations >= DURABLE_PROMOTION_THRESHOLD) {
|
|
989
|
+
isDurable = true
|
|
990
|
+
const promoted = await createAgentRun({
|
|
991
|
+
thread_id: opts.threadId,
|
|
992
|
+
agent_id: opts.agentId,
|
|
993
|
+
user_id: opts.userId ?? "",
|
|
994
|
+
channel: opts.channel ?? null,
|
|
995
|
+
kind: "chat",
|
|
996
|
+
max_iterations: maxIterations,
|
|
997
|
+
resume_policy: "resume",
|
|
998
|
+
})
|
|
999
|
+
runId = promoted.id
|
|
1000
|
+
startLeaseRenewal(runId)
|
|
1001
|
+
log.info(`[agent-loop] Auto-promoted to durable run ${runId} at iteration ${iterations}`)
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
if (runId && isDurable) {
|
|
1005
|
+
try {
|
|
1006
|
+
await checkpointRun(runId, {
|
|
1007
|
+
version: 1,
|
|
1008
|
+
messages: [...messages],
|
|
1009
|
+
iterations,
|
|
1010
|
+
totalInputTokens,
|
|
1011
|
+
totalOutputTokens,
|
|
1012
|
+
lastToolSignature,
|
|
1013
|
+
consecutiveRepeat,
|
|
1014
|
+
idleIterations,
|
|
1015
|
+
injectedToolNames,
|
|
1016
|
+
systemPromptSkillSections,
|
|
1017
|
+
}, null) // null = no pending tool calls (tools just completed)
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
log.warn(`[agent-loop] Post-tool checkpoint failed: ${(err as Error).message}`)
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Budget check: tokens
|
|
1024
|
+
if (opts.budget?.maxTokens && (totalInputTokens + totalOutputTokens) >= opts.budget.maxTokens) {
|
|
1025
|
+
log.info(`[agent-loop] Token budget exhausted (${totalInputTokens + totalOutputTokens}/${opts.budget.maxTokens})`)
|
|
1026
|
+
break
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// ── Synthesis call when max iterations hit without a text response ────────
|
|
1031
|
+
// The agent spent all iterations on tool calls and never produced a final message.
|
|
1032
|
+
// Make one extra call without tools so it summarizes what it did.
|
|
1033
|
+
if (!finalContent) {
|
|
1034
|
+
const pendingDelegation = opts.turnId && !opts.isolated
|
|
1035
|
+
? await import("../gateway/delegation-groups").then((mod) => mod.getDelegationGroup(opts.turnId!))
|
|
1036
|
+
: null
|
|
1037
|
+
if (pendingDelegation) {
|
|
1038
|
+
log.info(`[agent-loop] Suppressing terminal synthesis while delegation group ${opts.turnId} is pending`)
|
|
1039
|
+
finalContent = ""
|
|
1040
|
+
} else {
|
|
1041
|
+
log.info(`[agent-loop] Max iterations hit with no text response — requesting synthesis (isolated=${!!opts.isolated})`)
|
|
1042
|
+
messages.push({
|
|
1043
|
+
role: "user",
|
|
1044
|
+
content: "Basándote en lo que hiciste hasta ahora, responde al usuario con un resumen claro y estrictamente factual de lo completado, lo pendiente o los errores. No declares éxito sin evidencia. Sé conciso.",
|
|
1045
|
+
})
|
|
1046
|
+
let synthesisAttempt = 0
|
|
1047
|
+
finalContent = await synthesizeFinalResponse(async () => {
|
|
1048
|
+
synthesisAttempt++
|
|
1049
|
+
if (synthesisAttempt > 1) {
|
|
1050
|
+
log.warn("[agent-loop] Retrying terminal synthesis after an empty or failed response")
|
|
1051
|
+
}
|
|
1052
|
+
const synthesis = await callLLM({
|
|
1053
|
+
...providerCfg,
|
|
1054
|
+
messages: clearOldToolResults(messages) as LLMMessage[],
|
|
1055
|
+
tools: undefined, // no tools — force text response
|
|
1056
|
+
})
|
|
1057
|
+
if (synthesis.usage) {
|
|
1058
|
+
totalInputTokens += synthesis.usage.input_tokens
|
|
1059
|
+
totalOutputTokens += synthesis.usage.output_tokens
|
|
1060
|
+
}
|
|
1061
|
+
// A provider failure comes back as non-empty `content`, which the
|
|
1062
|
+
// empty-content check above would happily accept as a valid synthesis and
|
|
1063
|
+
// persist. Raise instead so the retry/AgentSynthesisError path runs.
|
|
1064
|
+
if (synthesis.stop_reason === "error") {
|
|
1065
|
+
throw new Error(synthesis.error?.message ?? synthesis.content)
|
|
1066
|
+
}
|
|
1067
|
+
return synthesis.content
|
|
1068
|
+
})
|
|
1069
|
+
if (!opts.isolated) {
|
|
1070
|
+
await addMessage(opts.threadId, "assistant", finalContent)
|
|
1071
|
+
}
|
|
1072
|
+
yield { agent: { messages: [{ content: finalContent }] } }
|
|
1073
|
+
}
|
|
1074
|
+
} else if (!finalEmitted) {
|
|
1075
|
+
// Internal break (stall, loop detected, stuck, timeout, abort) set finalContent
|
|
1076
|
+
// without yielding it — emit and persist it so the user gets a non-empty response
|
|
1077
|
+
if (!opts.isolated) {
|
|
1078
|
+
await addMessage(opts.threadId, "assistant", finalContent)
|
|
1079
|
+
}
|
|
1080
|
+
yield { agent: { messages: [{ content: finalContent }] } }
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Emit final usage so consumers (e.g. AgentRunner) can surface real token counts
|
|
1084
|
+
if (totalInputTokens > 0 || totalOutputTokens > 0) {
|
|
1085
|
+
yield { usage: { input_tokens: totalInputTokens, output_tokens: totalOutputTokens } }
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// ── Post-loop ────────────────────────────────────────────────────────────
|
|
1089
|
+
const durationMs = Math.round(performance.now() - t0)
|
|
1090
|
+
|
|
1091
|
+
emitCanvas("canvas:node_update", {
|
|
1092
|
+
nodeId: opts.agentId,
|
|
1093
|
+
changes: { status: "idle", currentTool: null },
|
|
1094
|
+
})
|
|
1095
|
+
|
|
1096
|
+
// Record usage
|
|
1097
|
+
recordLLMUsage({
|
|
1098
|
+
provider: providerCfg.provider,
|
|
1099
|
+
model: providerCfg.model,
|
|
1100
|
+
inputTokens: totalInputTokens,
|
|
1101
|
+
outputTokens: totalOutputTokens,
|
|
1102
|
+
})
|
|
1103
|
+
|
|
1104
|
+
// Extract text for trace summary
|
|
1105
|
+
const textMessageFinal = opts.rawUserMessage || (typeof opts.userMessage === "string"
|
|
1106
|
+
? opts.userMessage
|
|
1107
|
+
: Array.isArray(opts.userMessage)
|
|
1108
|
+
? opts.userMessage.filter(p => p.type === "text").map(p => (p as any).text).join("\n")
|
|
1109
|
+
: String(opts.userMessage))
|
|
1110
|
+
|
|
1111
|
+
// Save overall trace
|
|
1112
|
+
const cleanMessageFinal = textMessageFinal.replace(/^\[Timestamp:.*?\]\n/, "")
|
|
1113
|
+
saveTrace({
|
|
1114
|
+
threadId: opts.threadId,
|
|
1115
|
+
agentId: opts.agentId,
|
|
1116
|
+
agentName,
|
|
1117
|
+
inputSummary: cleanMessageFinal.substring(0, 300),
|
|
1118
|
+
outputSummary: finalContent.substring(0, 300),
|
|
1119
|
+
success: true,
|
|
1120
|
+
durationMs,
|
|
1121
|
+
tokensUsed: totalInputTokens + totalOutputTokens,
|
|
1122
|
+
causalStreamId,
|
|
1123
|
+
catalogAgentId: agent.source === "catalog" ? agent.id : undefined,
|
|
1124
|
+
})
|
|
1125
|
+
|
|
1126
|
+
log.info(
|
|
1127
|
+
`[agent-loop] Done: agent=${agentName} iterations=${iterations} ` +
|
|
1128
|
+
`tokens=${totalInputTokens + totalOutputTokens} elapsed=${durationMs}ms`
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1131
|
+
// ── Durable run finalization ──────────────────────────────────────────────
|
|
1132
|
+
if (runId && isDurable) {
|
|
1133
|
+
// Ensure final iteration count is persisted (loop may have broken before the
|
|
1134
|
+
// in-loop post-tool checkpoint was reached, e.g. when LLM returns text immediately).
|
|
1135
|
+
try {
|
|
1136
|
+
await checkpointRun(runId, {
|
|
1137
|
+
version: 1,
|
|
1138
|
+
messages: [...messages],
|
|
1139
|
+
iterations,
|
|
1140
|
+
totalInputTokens,
|
|
1141
|
+
totalOutputTokens,
|
|
1142
|
+
lastToolSignature,
|
|
1143
|
+
consecutiveRepeat,
|
|
1144
|
+
idleIterations,
|
|
1145
|
+
injectedToolNames,
|
|
1146
|
+
systemPromptSkillSections,
|
|
1147
|
+
}, null)
|
|
1148
|
+
} catch { /* best-effort */ }
|
|
1149
|
+
stopLeaseRenewal(runId)
|
|
1150
|
+
if (opts.signal?.aborted) {
|
|
1151
|
+
await interruptRun(runId, "Generation aborted by signal").catch(() => {})
|
|
1152
|
+
} else {
|
|
1153
|
+
await completeRun(runId).catch(() => {})
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
// The loop threw (LLM/tool error): release the run so reconcile never sees
|
|
1159
|
+
// a phantom "running" row with a self-renewing lease.
|
|
1160
|
+
if (runId && isDurable) {
|
|
1161
|
+
if (opts.signal?.aborted) {
|
|
1162
|
+
await interruptRun(runId, "Generation aborted by signal").catch(() => {})
|
|
1163
|
+
} else {
|
|
1164
|
+
await failRun(runId, (err as Error).message).catch(() => {})
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
throw err
|
|
1168
|
+
} finally {
|
|
1169
|
+
if (runId) {
|
|
1170
|
+
stopLeaseRenewal(runId)
|
|
1171
|
+
if (isDurable) {
|
|
1172
|
+
// Consumer abandoned the generator (break/.return()) before the normal
|
|
1173
|
+
// finalization ran: leave the run resumable instead of phantom-running.
|
|
1174
|
+
const finalState = await getRun(runId).catch(() => null)
|
|
1175
|
+
if (finalState && finalState.status === "running") {
|
|
1176
|
+
await interruptRun(runId, "Loop terminated early without finalization").catch(() => {})
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// ─── Isolated worker execution (Fase 4.4) ───────────────────────────────────
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Run a worker agent in an isolated context.
|
|
1187
|
+
* Returns the final response string.
|
|
1188
|
+
*
|
|
1189
|
+
* Passing `runId` + `durable` links the run to an existing AgentRun so the
|
|
1190
|
+
* worker checkpoints per round-trip and can resume mid-task after a crash.
|
|
1191
|
+
*/
|
|
1192
|
+
export interface IsolatedAgentOptions {
|
|
1193
|
+
agentId: string
|
|
1194
|
+
taskDescription: string | ContentPart[]
|
|
1195
|
+
threadId: string
|
|
1196
|
+
mcpManager?: MCPClientManager | null
|
|
1197
|
+
runId?: string
|
|
1198
|
+
resume?: boolean
|
|
1199
|
+
durable?: boolean
|
|
1200
|
+
signal?: AbortSignal
|
|
1201
|
+
turnId?: string
|
|
1202
|
+
taskId?: string
|
|
1203
|
+
userId?: string
|
|
1204
|
+
channel?: string
|
|
1205
|
+
sessionId?: string
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
export async function runAgentIsolatedDetailed(
|
|
1209
|
+
opts: IsolatedAgentOptions,
|
|
1210
|
+
): Promise<{ content: string; toolEvidence: string[] }> {
|
|
1211
|
+
let lastContent = ""
|
|
1212
|
+
const toolEvidence: string[] = []
|
|
1213
|
+
for await (const chunk of runAgent({
|
|
1214
|
+
agentId: opts.agentId,
|
|
1215
|
+
userMessage: opts.taskDescription,
|
|
1216
|
+
threadId: opts.threadId,
|
|
1217
|
+
isolated: true,
|
|
1218
|
+
taskContext: opts.taskDescription,
|
|
1219
|
+
mcpManager: opts.mcpManager,
|
|
1220
|
+
runId: opts.runId,
|
|
1221
|
+
resume: opts.resume,
|
|
1222
|
+
durable: opts.durable,
|
|
1223
|
+
signal: opts.signal,
|
|
1224
|
+
turnId: opts.turnId,
|
|
1225
|
+
taskId: opts.taskId,
|
|
1226
|
+
userId: opts.userId,
|
|
1227
|
+
channel: opts.channel,
|
|
1228
|
+
sessionId: opts.sessionId,
|
|
1229
|
+
})) {
|
|
1230
|
+
if (chunk.agent?.messages?.[0]?.content) {
|
|
1231
|
+
lastContent = chunk.agent.messages[0].content
|
|
1232
|
+
}
|
|
1233
|
+
for (const message of chunk.tools?.messages ?? []) {
|
|
1234
|
+
const raw = typeof message.content === "string" ? message.content : JSON.stringify(message.content)
|
|
1235
|
+
const safe = raw
|
|
1236
|
+
.replace(/data:[a-z0-9.+-]+\/[a-z0-9.+-]+;base64,[a-z0-9+/=\s]+/gi, "[REDACTED_BINARY]")
|
|
1237
|
+
.replace(/[A-Za-z0-9+/]{1000,}={0,2}/g, "[REDACTED_BINARY]")
|
|
1238
|
+
toolEvidence.push(`${message.name ?? "tool"}: ${safe.slice(0, 4000)}`)
|
|
1239
|
+
if (toolEvidence.length > 8) toolEvidence.shift()
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return { content: lastContent, toolEvidence }
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
export async function runAgentIsolated(opts: IsolatedAgentOptions): Promise<string> {
|
|
1246
|
+
return (await runAgentIsolatedDetailed(opts)).content
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// ─── Shim: AgentLoop class with stream() compatible with providers/index.ts ──
|
|
1250
|
+
|
|
1251
|
+
export class AgentLoop {
|
|
1252
|
+
private mcpManager: MCPClientManager | null = null
|
|
1253
|
+
|
|
1254
|
+
setMCPManager(m: MCPClientManager) {
|
|
1255
|
+
this.mcpManager = m
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Returns an async iterable that emits chunks compatible with
|
|
1260
|
+
* the existing providers/index.ts stream consumer.
|
|
1261
|
+
*/
|
|
1262
|
+
async *stream(
|
|
1263
|
+
input: { messages: Array<{ role: string; content: string | ContentPart[] }> },
|
|
1264
|
+
config: {
|
|
1265
|
+
configurable?: {
|
|
1266
|
+
thread_id?: string
|
|
1267
|
+
agent_id?: string
|
|
1268
|
+
user_id?: string
|
|
1269
|
+
system_prompt?: string
|
|
1270
|
+
channel?: string
|
|
1271
|
+
raw_user_message?: string
|
|
1272
|
+
/** Durable run options — checkpoint/resume via agentRuns. */
|
|
1273
|
+
run_id?: string
|
|
1274
|
+
resume?: boolean
|
|
1275
|
+
durable?: boolean
|
|
1276
|
+
turn_id?: string
|
|
1277
|
+
session_id?: string
|
|
1278
|
+
/** See AgentLoopOptions.historySource. */
|
|
1279
|
+
history_source?: TurnSource
|
|
1280
|
+
}
|
|
1281
|
+
signal?: AbortSignal
|
|
1282
|
+
onToken?: (token: string) => void
|
|
1283
|
+
onReasoningToken?: (token: string) => void
|
|
1284
|
+
onStep?: (step: StepEvent) => Promise<void>
|
|
1285
|
+
/** Extra tools to force into the LLM loadout (tests/evals). */
|
|
1286
|
+
extraTools?: any[]
|
|
1287
|
+
}
|
|
1288
|
+
): AsyncIterable<StreamChunk> {
|
|
1289
|
+
// Resolve from database with priority: explicit param → DB lookup → single user/agent
|
|
1290
|
+
const threadId = config.configurable?.thread_id || (await resolveUserId({})) || "default"
|
|
1291
|
+
const agentId = config.configurable?.agent_id || (await resolveAgentId(config.configurable?.agent_id)) || (await this._resolveCoordinatorId()) || "main"
|
|
1292
|
+
const systemPromptOverride = config.configurable?.system_prompt
|
|
1293
|
+
const channel = config.configurable?.channel
|
|
1294
|
+
const userId = config.configurable?.user_id || (await resolveUserId({
|
|
1295
|
+
channel: config.configurable?.channel ? (config.configurable?.channel as string).split(':')[0] : null,
|
|
1296
|
+
channelUserId: config.configurable?.thread_id
|
|
1297
|
+
}))
|
|
1298
|
+
|
|
1299
|
+
// Log MCP Manager status
|
|
1300
|
+
log.info(`[AgentLoop.stream] MCP Manager available: ${this.mcpManager !== null}`)
|
|
1301
|
+
if (this.mcpManager) {
|
|
1302
|
+
try {
|
|
1303
|
+
const servers = this.mcpManager.listServers?.() || []
|
|
1304
|
+
log.info(`[AgentLoop.stream] MCP servers: ${servers.length} registered`)
|
|
1305
|
+
for (const s of servers) {
|
|
1306
|
+
log.info(` - ${s.name}: ${s.status} (${s.tools?.length || 0} tools)`)
|
|
1307
|
+
}
|
|
1308
|
+
} catch (e) {
|
|
1309
|
+
log.warn(`[AgentLoop.stream] Failed to list MCP servers: ${(e as Error).message}`)
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// Extract the last user message from the input
|
|
1314
|
+
const lastUserMsg = [...input.messages].reverse().find((m) => m.role === "user")
|
|
1315
|
+
const userMessage = lastUserMsg?.content || ""
|
|
1316
|
+
|
|
1317
|
+
// Use clean message (without timestamp) for the search selectors
|
|
1318
|
+
const rawUserMessage = config.configurable?.raw_user_message ||
|
|
1319
|
+
(typeof userMessage === "string" ? userMessage : userMessage.filter(p => p.type === "text").map(p => (p as any).text).join("\n"))
|
|
1320
|
+
|
|
1321
|
+
yield* runAgent({
|
|
1322
|
+
agentId,
|
|
1323
|
+
userMessage, // FULL MULTIMODAL MESSAGE
|
|
1324
|
+
rawUserMessage, // CLEAN TEXT for search selectors
|
|
1325
|
+
threadId,
|
|
1326
|
+
channel,
|
|
1327
|
+
systemPromptOverride,
|
|
1328
|
+
mcpManager: this.mcpManager,
|
|
1329
|
+
userId,
|
|
1330
|
+
signal: config.signal,
|
|
1331
|
+
onToken: config.onToken,
|
|
1332
|
+
onReasoningToken: config.onReasoningToken,
|
|
1333
|
+
onStep: config.onStep,
|
|
1334
|
+
extraTools: config.extraTools,
|
|
1335
|
+
historySource: config.configurable?.history_source,
|
|
1336
|
+
runId: config.configurable?.run_id,
|
|
1337
|
+
resume: config.configurable?.resume,
|
|
1338
|
+
durable: config.configurable?.durable,
|
|
1339
|
+
turnId: config.configurable?.turn_id,
|
|
1340
|
+
sessionId: config.configurable?.session_id,
|
|
1341
|
+
runKind: "chat",
|
|
1342
|
+
})
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
private async _resolveCoordinatorId(): Promise<string> {
|
|
1346
|
+
// Use the storage helper to get coordinator agent ID from database
|
|
1347
|
+
const coordinatorId = await resolveAgentId(null);
|
|
1348
|
+
return coordinatorId || "main";
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// Singleton
|
|
1353
|
+
let _agentLoop: AgentLoop | null = null
|
|
1354
|
+
|
|
1355
|
+
export function getAgentLoop(): AgentLoop | null {
|
|
1356
|
+
return _agentLoop
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
export function buildAgentLoop(opts: { mcpManager?: MCPClientManager | null } = {}): AgentLoop {
|
|
1360
|
+
_agentLoop = new AgentLoop()
|
|
1361
|
+
if (opts.mcpManager) {
|
|
1362
|
+
_agentLoop.setMCPManager(opts.mcpManager)
|
|
1363
|
+
log.info("[buildAgentLoop] MCP Manager set successfully")
|
|
1364
|
+
} else {
|
|
1365
|
+
log.warn("[buildAgentLoop] No MCP Manager provided, agent will not have MCP tools")
|
|
1366
|
+
}
|
|
1367
|
+
return _agentLoop
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
export async function rebuildAgentLoop(opts: { mcpManager?: MCPClientManager | null } = {}): Promise<AgentLoop> {
|
|
1371
|
+
_agentLoop = null
|
|
1372
|
+
return buildAgentLoop(opts)
|
|
1373
|
+
}
|