@johpaz/hive-sdk 0.1.4 → 0.1.5
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 +97 -0
- package/README.md +78 -23
- 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 +9 -4
- 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 +5 -4
- package/packages/core/src/agent/acceptance-checks.ts +166 -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 +71 -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-screenshot.ts +26 -5
- package/packages/core/src/tools/web/browser-service.ts +5 -0
- 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/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/agent-loop-terminal-synthesis.test.ts +32 -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/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,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool Sync — Persist MCP tool definitions to DB and the HiveDB index
|
|
3
|
+
*
|
|
4
|
+
* When an MCP server connects, its tool definitions are persisted to
|
|
5
|
+
* the `mcpTools` collection and indexed in the HiveDB capability index
|
|
6
|
+
* for search_knowledge. When the server disconnects, all its tools are
|
|
7
|
+
* deleted from both (the index side uses a `server_id` filter, so only
|
|
8
|
+
* that server's documents are removed).
|
|
9
|
+
*
|
|
10
|
+
* This enables:
|
|
11
|
+
* 1. search_knowledge to find MCP tools via HiveDB BM25
|
|
12
|
+
* 2. Tool definitions to survive across context compiler invocations
|
|
13
|
+
* 3. Offline visibility of what MCP tools were available
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { col } from "../storage/hive"
|
|
17
|
+
import type { McpToolDoc } from "../storage/collections"
|
|
18
|
+
import { logger } from "../utils/logger"
|
|
19
|
+
import { mcpToolFullName } from "../agent/tool-selector"
|
|
20
|
+
import {
|
|
21
|
+
replaceCapabilityDocs,
|
|
22
|
+
deleteCapabilitiesByServer,
|
|
23
|
+
type CapabilityDoc,
|
|
24
|
+
} from "../agent/capability-search"
|
|
25
|
+
|
|
26
|
+
const log = logger.child("mcp:tool-sync")
|
|
27
|
+
|
|
28
|
+
export interface MCPToolDefinition {
|
|
29
|
+
name: string
|
|
30
|
+
description: string
|
|
31
|
+
inputSchema?: Record<string, unknown>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generate a stable ID for an MCP tool based on server + tool name.
|
|
36
|
+
*
|
|
37
|
+
* MUST be identical to the LLM function name the agent calls — agent-loop
|
|
38
|
+
* resolves search results against executors by this exact string — so it
|
|
39
|
+
* delegates to mcpToolFullName instead of duplicating the algorithm.
|
|
40
|
+
*/
|
|
41
|
+
function mcpToolId(serverName: string, toolName: string): string {
|
|
42
|
+
return mcpToolFullName(serverName, toolName)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Persist MCP tool definitions to the `mcpTools` collection.
|
|
47
|
+
* Called when a server connects or reconnects.
|
|
48
|
+
* Replaces existing tools for the server first, then inserts fresh data.
|
|
49
|
+
*/
|
|
50
|
+
export async function syncMCPToolsToDB(
|
|
51
|
+
serverId: string,
|
|
52
|
+
serverName: string,
|
|
53
|
+
tools: MCPToolDefinition[]
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
try {
|
|
56
|
+
const mcpToolsCol = await col<McpToolDoc>("mcpTools")
|
|
57
|
+
const existing = await mcpToolsCol.findBy("server_id", serverId)
|
|
58
|
+
for (const e of existing) await mcpToolsCol.delete(e.id)
|
|
59
|
+
|
|
60
|
+
if (tools.length === 0) {
|
|
61
|
+
log.debug(`[mcp:tool-sync] No tools to persist for server ${serverName}`)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let count = 0
|
|
66
|
+
for (const tool of tools) {
|
|
67
|
+
const id = mcpToolId(serverName, tool.name)
|
|
68
|
+
await mcpToolsCol.put(id, {
|
|
69
|
+
id, server_id: serverId, server_name: serverName, tool_name: tool.name,
|
|
70
|
+
description: tool.description || "", category: "mcp", active: true,
|
|
71
|
+
})
|
|
72
|
+
count++
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
log.info(`[mcp:tool-sync] Persisted ${count} MCP tools for server ${serverName} to mcpTools`)
|
|
76
|
+
} catch (err) {
|
|
77
|
+
log.error(`[mcp:tool-sync] Failed to persist MCP tools for server ${serverName}:`, err)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Sync all active MCP tools from the `mcpTools` collection to the HiveDB
|
|
83
|
+
* capability index. Called after syncMCPToolsToDB or after startup to ensure
|
|
84
|
+
* the index is in sync.
|
|
85
|
+
*
|
|
86
|
+
* This does a full replace of `type=mcp` documents to avoid drift.
|
|
87
|
+
*/
|
|
88
|
+
export async function syncMCPToolsToIndex(): Promise<void> {
|
|
89
|
+
try {
|
|
90
|
+
const mcpToolsCol = await col<McpToolDoc>("mcpTools")
|
|
91
|
+
const mcpTools = (await mcpToolsCol.scan({})).map(e => e.doc).filter(t => t.active)
|
|
92
|
+
|
|
93
|
+
// Searchable text is the tool's OWN name/description only. The server
|
|
94
|
+
// name is deliberately NOT indexed: generic server names (e.g.
|
|
95
|
+
// "herramientas administrativas") would make every query matching the
|
|
96
|
+
// server name flood the results with that server's entire tool set.
|
|
97
|
+
// The server stays reachable via the server_id filter and appears in
|
|
98
|
+
// hydrated results.
|
|
99
|
+
//
|
|
100
|
+
// camelCase names ("crearEvento") tokenize as a single word, so a
|
|
101
|
+
// space-split variant goes into the searchable text ("crear Evento").
|
|
102
|
+
const splitCamel = (s: string) => s.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
103
|
+
const docs: CapabilityDoc[] = mcpTools.map(tool => ({
|
|
104
|
+
type: "mcp" as const,
|
|
105
|
+
rawId: tool.id,
|
|
106
|
+
name: `${tool.tool_name} ${splitCamel(tool.tool_name)}`,
|
|
107
|
+
body: tool.description || tool.tool_name,
|
|
108
|
+
tags: tool.category ?? "",
|
|
109
|
+
extraFilters: [{ field: "server_id", value: tool.server_id }],
|
|
110
|
+
}))
|
|
111
|
+
|
|
112
|
+
await replaceCapabilityDocs("mcp", docs)
|
|
113
|
+
|
|
114
|
+
log.info(`[mcp:tool-sync] Synced ${mcpTools.length} MCP tools to HiveDB index`)
|
|
115
|
+
} catch (err) {
|
|
116
|
+
log.error(`[mcp:tool-sync] Failed to sync MCP tools to HiveDB index:`, err)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Delete all MCP tool definitions for a server from both the `mcpTools`
|
|
122
|
+
* collection and the HiveDB capability index. Called when a server
|
|
123
|
+
* disconnects or is removed.
|
|
124
|
+
*/
|
|
125
|
+
export async function clearMCPToolsFromDB(serverId: string): Promise<void> {
|
|
126
|
+
try {
|
|
127
|
+
const mcpToolsCol = await col<McpToolDoc>("mcpTools")
|
|
128
|
+
const existing = await mcpToolsCol.findBy("server_id", serverId)
|
|
129
|
+
for (const e of existing) await mcpToolsCol.delete(e.id)
|
|
130
|
+
|
|
131
|
+
// Remove only this server's documents from the index
|
|
132
|
+
await deleteCapabilitiesByServer(serverId)
|
|
133
|
+
|
|
134
|
+
log.info(`[mcp:tool-sync] Cleared MCP tools for server_id=${serverId}`)
|
|
135
|
+
} catch (err) {
|
|
136
|
+
log.error(`[mcp:tool-sync] Failed to clear MCP tools for server_id=${serverId}:`, err)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -1,33 +1,35 @@
|
|
|
1
|
-
|
|
1
|
+
process.env.HIVE_DB_PATH = ":memory:";
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
2
4
|
import { Scratchpad } from "./Scratchpad.ts";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
+
import { closeHiveDb } from "../storage/hivedb.ts";
|
|
6
|
+
import { ensureHiveDb } from "../storage/bootstrap.ts";
|
|
7
|
+
import { getScratchpad } from "../agent/conversation-store.ts";
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
let pad: Scratchpad;
|
|
9
|
+
const THREAD = "test-thread";
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
initializeDatabase();
|
|
11
|
-
await getHiveDB();
|
|
12
|
-
pad = new Scratchpad();
|
|
13
|
-
});
|
|
11
|
+
let pad: Scratchpad;
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
closeHiveDb();
|
|
15
|
+
await ensureHiveDb();
|
|
16
|
+
pad = new Scratchpad();
|
|
17
|
+
});
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
closeHiveDb();
|
|
21
|
+
});
|
|
21
22
|
|
|
23
|
+
describe("Scratchpad", () => {
|
|
22
24
|
it("writes and reads a note", async () => {
|
|
23
25
|
await pad.write(THREAD, "test-1", "hello world");
|
|
24
|
-
|
|
25
|
-
expect(value).toBe("hello world");
|
|
26
|
+
expect(await pad.read(THREAD, "test-1")).toBe("hello world");
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
it("lists notes as key-value map", async () => {
|
|
29
30
|
await pad.write(THREAD, "list-a", "aaa");
|
|
30
31
|
await pad.write(THREAD, "list-b", "bbb");
|
|
32
|
+
|
|
31
33
|
const notes = await pad.list(THREAD);
|
|
32
34
|
expect(notes["list-a"]).toBe("aaa");
|
|
33
35
|
expect(notes["list-b"]).toBe("bbb");
|
|
@@ -36,14 +38,31 @@ describe("Scratchpad", () => {
|
|
|
36
38
|
it("deletes a note", async () => {
|
|
37
39
|
await pad.write(THREAD, "to-delete", "delete me");
|
|
38
40
|
await pad.delete(THREAD, "to-delete");
|
|
39
|
-
|
|
40
|
-
expect(value).toBeUndefined();
|
|
41
|
+
expect(await pad.read(THREAD, "to-delete")).toBeUndefined();
|
|
41
42
|
});
|
|
42
43
|
|
|
43
44
|
it("clear removes all notes for a thread", async () => {
|
|
44
45
|
await pad.write(THREAD, "clear-a", "a");
|
|
45
46
|
await pad.write(THREAD, "clear-b", "b");
|
|
46
47
|
await pad.clear(THREAD);
|
|
47
|
-
expect(Object.keys(await pad.list(THREAD))
|
|
48
|
+
expect(Object.keys(await pad.list(THREAD))).toHaveLength(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("no toca las notas de otros threads", async () => {
|
|
52
|
+
await pad.write("thread-uno", "k", "uno");
|
|
53
|
+
await pad.write("thread-dos", "k", "dos");
|
|
54
|
+
await pad.clear("thread-uno");
|
|
55
|
+
|
|
56
|
+
expect(await pad.read("thread-dos", "k")).toBe("dos");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("escribe el mismo documento que lee el context compiler", async () => {
|
|
60
|
+
// La clase y `conversation-store` comparten colección e id. Antes la clase
|
|
61
|
+
// guardaba un doc incompleto (sin `source`, `createdAt` ni `seq`), así que
|
|
62
|
+
// una nota escrita por acá se ordenaba mal dentro del prompt.
|
|
63
|
+
await pad.write(THREAD, "compartida", "valor", "test");
|
|
64
|
+
|
|
65
|
+
const notes = await getScratchpad(THREAD);
|
|
66
|
+
expect(notes).toEqual([{ key: "compartida", value: "valor" }]);
|
|
48
67
|
});
|
|
49
68
|
});
|
|
@@ -1,54 +1,47 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Scratchpad — notas persistentes por conversación.
|
|
3
|
+
*
|
|
4
|
+
* Es una fachada sobre las funciones de `agent/conversation-store.ts`, que son
|
|
5
|
+
* las que realmente usa el context compiler. Antes esta clase tenía su propia
|
|
6
|
+
* implementación contra la colección `scratchpad` y, como usaba el mismo id
|
|
7
|
+
* (`<threadId>:<key>`), escribía las mismas filas con un documento incompleto:
|
|
8
|
+
* sin `source`, sin `createdAt` y sin `seq`. Una nota guardada por acá quedaba
|
|
9
|
+
* sin el desempate por recencia que aplica `getScratchpad`, así que aparecía en
|
|
10
|
+
* un orden arbitrario dentro del prompt.
|
|
11
|
+
*/
|
|
2
12
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
13
|
+
import {
|
|
14
|
+
saveScratchpadNote,
|
|
15
|
+
getScratchpad,
|
|
16
|
+
deleteScratchpadNote,
|
|
17
|
+
} from "../agent/conversation-store.ts";
|
|
9
18
|
|
|
10
19
|
export class Scratchpad {
|
|
11
|
-
async write(threadId: string, key: string, value: string): Promise<void> {
|
|
12
|
-
|
|
13
|
-
const col = db.collection<ScratchpadDoc>("scratchpad");
|
|
14
|
-
await col.put(this.docId(threadId, key), { threadId, key, value, updatedAt: Date.now() });
|
|
20
|
+
async write(threadId: string, key: string, value: string, source?: string): Promise<void> {
|
|
21
|
+
await saveScratchpadNote(threadId, key, value, source);
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
async read(threadId: string, key: string): Promise<string | undefined> {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
const entry = await col.get(this.docId(threadId, key));
|
|
21
|
-
return entry?.doc.value;
|
|
25
|
+
const notes = await getScratchpad(threadId);
|
|
26
|
+
return notes.find((n) => n.key === key)?.value;
|
|
22
27
|
}
|
|
23
28
|
|
|
29
|
+
/** Notas del thread, más recientes primero. */
|
|
24
30
|
async list(threadId: string): Promise<Record<string, string>> {
|
|
25
|
-
const db = await getHiveDB();
|
|
26
|
-
const col = db.collection<ScratchpadDoc>("scratchpad");
|
|
27
|
-
const entries = await col.scan();
|
|
28
31
|
const result: Record<string, string> = {};
|
|
29
|
-
for (const
|
|
30
|
-
|
|
31
|
-
result[e.doc.key] = e.doc.value;
|
|
32
|
-
}
|
|
32
|
+
for (const note of await getScratchpad(threadId)) {
|
|
33
|
+
result[note.key] = note.value;
|
|
33
34
|
}
|
|
34
35
|
return result;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
async delete(threadId: string, key: string): Promise<void> {
|
|
38
|
-
|
|
39
|
-
const col = db.collection<ScratchpadDoc>("scratchpad");
|
|
40
|
-
await col.delete(this.docId(threadId, key));
|
|
39
|
+
await deleteScratchpadNote(threadId, key);
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
async clear(threadId: string): Promise<void> {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const ids = entries.filter(e => e.doc.threadId === threadId).map(e => e.id);
|
|
48
|
-
await db.batch(ids.map(id => ({ op: "delete" as const, collection: "scratchpad", id })));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
private docId(threadId: string, key: string): string {
|
|
52
|
-
return `${threadId}:${key}`;
|
|
43
|
+
for (const note of await getScratchpad(threadId)) {
|
|
44
|
+
await deleteScratchpadNote(threadId, note.key);
|
|
45
|
+
}
|
|
53
46
|
}
|
|
54
47
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { col } from "../storage/hive"
|
|
2
|
+
import type { ChannelDoc, ModelDoc, ProviderDoc } from "../storage/collections"
|
|
2
3
|
import { loadProviderApiKey } from "../storage/crypto"
|
|
3
4
|
import { logger } from "../utils/logger"
|
|
4
5
|
import type { ImageInput, DocumentInput, VisionConfig } from "./types"
|
|
@@ -18,27 +19,19 @@ class MultimodalService {
|
|
|
18
19
|
return MultimodalService.instance
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
getChannelVisionConfig(channelId: string): VisionConfig {
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
`).get(channelId) as {
|
|
27
|
-
vision_enabled: number
|
|
28
|
-
ocr_provider: string | null
|
|
29
|
-
vision_provider: string | null
|
|
30
|
-
vision_model_id: string | null
|
|
31
|
-
} | undefined
|
|
32
|
-
|
|
33
|
-
if (!result) {
|
|
22
|
+
async getChannelVisionConfig(channelId: string): Promise<VisionConfig> {
|
|
23
|
+
const channelsCol = await col<ChannelDoc>("channels")
|
|
24
|
+
const entry = await channelsCol.get(channelId)
|
|
25
|
+
|
|
26
|
+
if (!entry) {
|
|
34
27
|
return { visionEnabled: false, ocrProvider: null, visionProvider: null, visionModelId: null }
|
|
35
28
|
}
|
|
36
29
|
|
|
37
30
|
return {
|
|
38
|
-
visionEnabled:
|
|
39
|
-
ocrProvider:
|
|
40
|
-
visionProvider:
|
|
41
|
-
visionModelId:
|
|
31
|
+
visionEnabled: entry.doc.vision_enabled,
|
|
32
|
+
ocrProvider: entry.doc.ocr_provider,
|
|
33
|
+
visionProvider: entry.doc.vision_provider,
|
|
34
|
+
visionModelId: entry.doc.vision_model_id,
|
|
42
35
|
}
|
|
43
36
|
}
|
|
44
37
|
|
|
@@ -131,11 +124,26 @@ class MultimodalService {
|
|
|
131
124
|
}
|
|
132
125
|
|
|
133
126
|
private async getProviderApiKey(providerId: string): Promise<string | null> {
|
|
134
|
-
const db = getDb()
|
|
135
127
|
const apiKey = await loadProviderApiKey(providerId)
|
|
136
128
|
return apiKey || null
|
|
137
129
|
}
|
|
138
130
|
|
|
131
|
+
/** Modelo con capacidad de visión del provider según la BD; el literal queda como último recurso. */
|
|
132
|
+
private async getVisionModel(providerId: string, fallback: string): Promise<string> {
|
|
133
|
+
try {
|
|
134
|
+
const modelsCol = await col<ModelDoc>("models")
|
|
135
|
+
const models = await modelsCol.findBy("provider_id", providerId)
|
|
136
|
+
const candidates = models.filter(e =>
|
|
137
|
+
e.doc.model_type === "llm" &&
|
|
138
|
+
(e.doc.capabilities?.includes('"vision"') || e.doc.capabilities?.includes('"ocr"'))
|
|
139
|
+
)
|
|
140
|
+
const best = [...candidates].sort((a, b) => Number(b.doc.active) - Number(a.doc.active))[0]
|
|
141
|
+
return best?.doc.id || fallback
|
|
142
|
+
} catch {
|
|
143
|
+
return fallback
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
139
147
|
private async ocrWithOpenAI(image: ImageInput): Promise<string> {
|
|
140
148
|
const key = await this.getProviderApiKey("openai") || process.env.OPENAI_API_KEY
|
|
141
149
|
if (!key) throw new Error("OPENAI_API_KEY not configured for OCR")
|
|
@@ -146,7 +154,7 @@ class MultimodalService {
|
|
|
146
154
|
method: "POST",
|
|
147
155
|
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` },
|
|
148
156
|
body: JSON.stringify({
|
|
149
|
-
model: "gpt-4o-mini",
|
|
157
|
+
model: await this.getVisionModel("openai", "gpt-4o-mini"),
|
|
150
158
|
messages: [{
|
|
151
159
|
role: "user",
|
|
152
160
|
content: [
|
|
@@ -183,7 +191,7 @@ class MultimodalService {
|
|
|
183
191
|
}
|
|
184
192
|
|
|
185
193
|
const response = await fetch(
|
|
186
|
-
`https://generativelanguage.googleapis.com/v1beta/models
|
|
194
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${await this.getVisionModel("gemini", "gemini-2.0-flash")}:generateContent?key=${key}`,
|
|
187
195
|
{
|
|
188
196
|
method: "POST",
|
|
189
197
|
headers: { "Content-Type": "application/json" },
|
|
@@ -229,7 +237,7 @@ class MultimodalService {
|
|
|
229
237
|
"anthropic-dangerous-direct-browser-access": "true",
|
|
230
238
|
},
|
|
231
239
|
body: JSON.stringify({
|
|
232
|
-
model: "claude-haiku-4-5-20251001",
|
|
240
|
+
model: await this.getVisionModel("anthropic", "claude-haiku-4-5-20251001"),
|
|
233
241
|
max_tokens: 1000,
|
|
234
242
|
messages: [{
|
|
235
243
|
role: "user",
|
|
@@ -251,28 +259,26 @@ class MultimodalService {
|
|
|
251
259
|
return textBlock?.text || ""
|
|
252
260
|
}
|
|
253
261
|
|
|
254
|
-
getConfiguredVisionProviders(): Record<string, boolean
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
return !!row
|
|
261
|
-
}
|
|
262
|
+
async getConfiguredVisionProviders(): Promise<Record<string, boolean>> {
|
|
263
|
+
const hasDbKey = async (providerId: string): Promise<boolean> => !!(await loadProviderApiKey(providerId))
|
|
264
|
+
|
|
265
|
+
const [openai, gemini, anthropic] = await Promise.all([
|
|
266
|
+
hasDbKey("openai"), hasDbKey("gemini"), hasDbKey("anthropic"),
|
|
267
|
+
])
|
|
262
268
|
|
|
263
269
|
return {
|
|
264
|
-
openai:
|
|
265
|
-
gemini:
|
|
266
|
-
anthropic:
|
|
270
|
+
openai: openai || !!(process.env.OPENAI_API_KEY),
|
|
271
|
+
gemini: gemini || !!(process.env.GEMINI_API_KEY),
|
|
272
|
+
anthropic: anthropic || !!(process.env.ANTHROPIC_API_KEY),
|
|
267
273
|
}
|
|
268
274
|
}
|
|
269
275
|
|
|
270
|
-
modelSupportsVision(providerId: string, modelId: string): boolean {
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
if (!
|
|
276
|
+
async modelSupportsVision(providerId: string, modelId: string): Promise<boolean> {
|
|
277
|
+
const modelsCol = await col<ModelDoc>("models")
|
|
278
|
+
const entry = await modelsCol.get(modelId)
|
|
279
|
+
if (!entry || entry.doc.provider_id !== providerId || !entry.doc.capabilities) return false
|
|
274
280
|
try {
|
|
275
|
-
const caps = JSON.parse(
|
|
281
|
+
const caps = JSON.parse(entry.doc.capabilities) as string[]
|
|
276
282
|
return caps.includes("vision")
|
|
277
283
|
} catch {
|
|
278
284
|
return false
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic retry-with-backoff helper for transient failures (LLM provider
|
|
3
|
+
* calls, network/tool errors). Complements CircuitBreaker (circuit-breaker.ts),
|
|
4
|
+
* which sits one layer out and trips on sustained failure rates — this module
|
|
5
|
+
* only decides whether a single call should be retried and how long to wait.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { logger } from "../utils/logger";
|
|
9
|
+
|
|
10
|
+
const log = logger.child("retry");
|
|
11
|
+
|
|
12
|
+
export interface RetryPolicy {
|
|
13
|
+
maxAttempts: number;
|
|
14
|
+
initialDelayMs: number;
|
|
15
|
+
backoffMultiplier: number;
|
|
16
|
+
maxDelayMs: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DEFAULT_RETRY_POLICY: RetryPolicy = {
|
|
20
|
+
maxAttempts: 3,
|
|
21
|
+
initialDelayMs: 1000,
|
|
22
|
+
backoffMultiplier: 2,
|
|
23
|
+
maxDelayMs: 30_000,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Exponential backoff with full jitter, capped at policy.maxDelayMs. */
|
|
27
|
+
export function computeRetryDelay(attempt: number, policy: RetryPolicy, retryAfterMs?: number): number {
|
|
28
|
+
if (retryAfterMs !== undefined) return Math.min(retryAfterMs, policy.maxDelayMs);
|
|
29
|
+
const base = Math.min(policy.maxDelayMs, policy.initialDelayMs * Math.pow(policy.backoffMultiplier, attempt));
|
|
30
|
+
return Math.round(base * (0.5 + Math.random() * 0.5));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* HTTP-status-aware transient classifier: 408/429/5xx/network/timeout are
|
|
35
|
+
* retryable, 400/401/403/404 (and anything else) are terminal. Looks for a
|
|
36
|
+
* `status`/`statusCode` property (most provider SDKs set one) or common
|
|
37
|
+
* network/timeout error names/messages as a fallback.
|
|
38
|
+
*/
|
|
39
|
+
export function isRetryableError(err: unknown): boolean {
|
|
40
|
+
const status = extractStatus(err);
|
|
41
|
+
if (status !== undefined) {
|
|
42
|
+
if (status === 408 || status === 429) return true;
|
|
43
|
+
if (status >= 500 && status < 600) return true;
|
|
44
|
+
if (status >= 400 && status < 500) return false;
|
|
45
|
+
}
|
|
46
|
+
const name = (err as { name?: string })?.name ?? "";
|
|
47
|
+
const message = (err as { message?: string })?.message ?? String(err);
|
|
48
|
+
if (name === "AbortError") return false;
|
|
49
|
+
if (/timeout|timed out|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|network|fetch failed/i.test(`${name} ${message}`)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function extractStatus(err: unknown): number | undefined {
|
|
56
|
+
const e = err as { status?: number; statusCode?: number; response?: { status?: number } };
|
|
57
|
+
return e?.status ?? e?.statusCode ?? e?.response?.status;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Extract a `Retry-After` value (seconds or HTTP-date) if the error/response carries one. */
|
|
61
|
+
function extractRetryAfterMs(err: unknown): number | undefined {
|
|
62
|
+
const headers = (err as { headers?: Record<string, string>; response?: { headers?: Record<string, string> } })?.headers
|
|
63
|
+
?? (err as { response?: { headers?: Record<string, string> } })?.response?.headers;
|
|
64
|
+
const raw = headers?.["retry-after"] ?? headers?.["Retry-After"];
|
|
65
|
+
if (!raw) return undefined;
|
|
66
|
+
const seconds = Number(raw);
|
|
67
|
+
if (!Number.isNaN(seconds)) return seconds * 1000;
|
|
68
|
+
const dateMs = Date.parse(raw);
|
|
69
|
+
return Number.isNaN(dateMs) ? undefined : Math.max(0, dateMs - Date.now());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Run `fn`, retrying on transient failure per `policy`. `isRetryable`
|
|
74
|
+
* defaults to {@link isRetryableError}; pass a custom classifier for
|
|
75
|
+
* domain-specific errors (e.g. job executors).
|
|
76
|
+
*/
|
|
77
|
+
export async function withRetry<T>(
|
|
78
|
+
fn: (attempt: number) => Promise<T>,
|
|
79
|
+
policy: RetryPolicy = DEFAULT_RETRY_POLICY,
|
|
80
|
+
isRetryable: (err: unknown) => boolean = isRetryableError
|
|
81
|
+
): Promise<T> {
|
|
82
|
+
let lastErr: unknown;
|
|
83
|
+
for (let attempt = 0; attempt < policy.maxAttempts; attempt++) {
|
|
84
|
+
try {
|
|
85
|
+
return await fn(attempt);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
lastErr = err;
|
|
88
|
+
if (attempt === policy.maxAttempts - 1 || !isRetryable(err)) throw err;
|
|
89
|
+
const delay = computeRetryDelay(attempt, policy, extractRetryAfterMs(err));
|
|
90
|
+
log.warn(`[withRetry] Attempt ${attempt + 1}/${policy.maxAttempts} failed (${(err as Error).message}); retrying in ${delay}ms`);
|
|
91
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
throw lastErr;
|
|
95
|
+
}
|