@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.
Files changed (272) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +78 -23
  3. package/bunfig.toml +4 -2
  4. package/docs/API-AGENTS.md +78 -27
  5. package/docs/API-CONTEXT-COMPILER.md +31 -34
  6. package/docs/API-TOOLS-SKILLS-CHANNELS.md +58 -22
  7. package/docs/HIVE-HARNESS.md +1 -1
  8. package/docs/INDEX.md +4 -4
  9. package/docs/TEMPLATE-HIVE-APP.md +10 -10
  10. package/package.json +9 -4
  11. package/packages/cli/package.json +2 -2
  12. package/packages/cli/src/commands/create-app.test.ts +36 -7
  13. package/packages/cli/src/commands/init.ts +3 -3
  14. package/packages/cli/src/commands/run.ts +1 -1
  15. package/packages/cli/src/commands/test.ts +37 -25
  16. package/packages/cli/src/commands/trace.ts +30 -28
  17. package/packages/cli/templates/hive-app/.env.example +10 -2
  18. package/packages/cli/templates/hive-app/README.md +103 -0
  19. package/packages/cli/templates/hive-app/hive.config.ts +9 -3
  20. package/packages/cli/templates/hive-app/src/agents/coordinator.ts +8 -1
  21. package/packages/cli/templates/hive-app/src/main.ts +12 -19
  22. package/packages/core/package.json +5 -4
  23. package/packages/core/src/agent/acceptance-checks.ts +166 -0
  24. package/packages/core/src/agent/agent-catalog.ts +348 -0
  25. package/packages/core/src/agent/agent-loop.ts +1373 -0
  26. package/packages/core/src/agent/capability-search.ts +186 -0
  27. package/packages/core/src/agent/catalog-selector.ts +103 -0
  28. package/packages/core/src/agent/{Compaction.ts → compaction.ts} +86 -63
  29. package/packages/core/src/agent/context-compiler.ts +689 -0
  30. package/packages/core/src/agent/conversation-store.ts +381 -0
  31. package/packages/core/src/agent/curator.ts +276 -0
  32. package/packages/core/src/agent/delegation-runtime.ts +241 -0
  33. package/packages/core/src/agent/goal-runner.ts +323 -0
  34. package/packages/core/src/agent/index.ts +17 -12
  35. package/packages/core/src/agent/llm-client.ts +266 -0
  36. package/packages/core/src/agent/llm-providers/anthropic.ts +264 -0
  37. package/packages/core/src/agent/llm-providers/deepseek.ts +8 -0
  38. package/packages/core/src/agent/{providers → llm-providers}/gemini.ts +98 -60
  39. package/packages/core/src/agent/llm-providers/groq.ts +5 -0
  40. package/packages/core/src/agent/llm-providers/hiveagents.ts +253 -0
  41. package/packages/core/src/agent/{providers → llm-providers}/interface.ts +73 -13
  42. package/packages/core/src/agent/llm-providers/kimi.ts +8 -0
  43. package/packages/core/src/agent/llm-providers/minimax.ts +13 -0
  44. package/packages/core/src/agent/llm-providers/mistral.ts +5 -0
  45. package/packages/core/src/agent/llm-providers/modelscope.ts +5 -0
  46. package/packages/core/src/agent/llm-providers/nvidia.ts +5 -0
  47. package/packages/core/src/agent/{providers → llm-providers}/ollama.ts +31 -5
  48. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +418 -0
  49. package/packages/core/src/agent/llm-providers/openai.ts +5 -0
  50. package/packages/core/src/agent/llm-providers/opencode-go.ts +9 -0
  51. package/packages/core/src/agent/llm-providers/openrouter.ts +5 -0
  52. package/packages/core/src/agent/llm-providers/qwen.ts +5 -0
  53. package/packages/core/src/agent/llm-providers/z-ai.ts +5 -0
  54. package/packages/core/src/agent/minimal-loadout.ts +47 -0
  55. package/packages/core/src/agent/playbook-selector.ts +119 -0
  56. package/packages/core/src/agent/{PromptBuilder.ts → prompt-builder.ts} +21 -22
  57. package/packages/core/src/{harness → agent}/proof-packet.ts +16 -21
  58. package/packages/core/src/agent/providers/index.ts +35 -16
  59. package/packages/core/src/agent/reflector.ts +320 -0
  60. package/packages/core/src/agent/routing-intent.ts +22 -0
  61. package/packages/core/src/{harness → agent}/run-epoch.ts +4 -3
  62. package/packages/core/src/{harness → agent}/run-store.ts +142 -81
  63. package/packages/core/src/agent/{Service.ts → service.ts} +37 -26
  64. package/packages/core/src/agent/skill-selector.ts +374 -0
  65. package/packages/core/src/agent/stuck-loop.ts +209 -0
  66. package/packages/core/src/agent/{selectors/ToolSelector.ts → tool-selector.ts} +188 -178
  67. package/packages/core/src/{ace/Tracer.ts → agent/tracer.ts} +37 -27
  68. package/packages/core/src/api/createAgent.test.ts +139 -27
  69. package/packages/core/src/api/createAgent.ts +232 -44
  70. package/packages/core/src/artifacts/store.ts +162 -0
  71. package/packages/core/src/canvas/canvas-manager.ts +161 -0
  72. package/packages/core/src/canvas/canvas.test.ts +8 -4
  73. package/packages/core/src/canvas/emitter.ts +131 -80
  74. package/packages/core/src/canvas/index.ts +1 -3
  75. package/packages/core/src/channels/base.ts +9 -1
  76. package/packages/core/src/channels/discord.ts +5 -4
  77. package/packages/core/src/channels/manager.ts +122 -30
  78. package/packages/core/src/channels/slack.ts +5 -4
  79. package/packages/core/src/channels/telegram.ts +36 -6
  80. package/packages/core/src/channels/webchat.ts +11 -10
  81. package/packages/core/src/channels/whatsapp.ts +23 -7
  82. package/packages/core/src/config/index.ts +13 -2
  83. package/packages/core/src/config/loader.ts +71 -29
  84. package/packages/core/src/ethics/EthicsGuard.test.ts +90 -36
  85. package/packages/core/src/ethics/EthicsGuard.ts +51 -47
  86. package/packages/core/src/events/agent-bus.ts +44 -68
  87. package/packages/core/src/events/channel-narration.ts +150 -0
  88. package/packages/core/src/events/narration.ts +82 -0
  89. package/packages/core/src/events/tool-narration.ts +62 -0
  90. package/packages/core/src/gateway/delegation-groups.ts +258 -0
  91. package/packages/core/src/{harness → gateway}/durable-queue.ts +102 -42
  92. package/packages/core/src/{harness → gateway}/job-store.ts +85 -48
  93. package/packages/core/src/gateway/lane-queue.ts +173 -0
  94. package/packages/core/src/gateway/notification-inbox.ts +57 -0
  95. package/packages/core/src/gateway/server.ts +1 -1
  96. package/packages/core/src/harness/index.ts +46 -27
  97. package/packages/core/src/index.ts +33 -27
  98. package/packages/core/src/mcp/hot-reload.ts +32 -23
  99. package/packages/core/src/mcp/index.ts +6 -3
  100. package/packages/core/src/mcp/singleton.ts +1 -4
  101. package/packages/core/src/mcp/tool-sync.ts +138 -0
  102. package/packages/core/src/memory/Scratchpad.test.ts +39 -20
  103. package/packages/core/src/memory/Scratchpad.ts +27 -34
  104. package/packages/core/src/multimodal/vision-service.ts +44 -38
  105. package/packages/core/src/resilience/retry.ts +95 -0
  106. package/packages/core/src/scheduler/CronScheduler.ts +334 -287
  107. package/packages/core/src/scheduler/index.ts +9 -7
  108. package/packages/core/src/scheduler/integration.ts +46 -26
  109. package/packages/core/src/scheduler/scheduler.test.ts +9 -13
  110. package/packages/core/src/scheduler/types.ts +7 -2
  111. package/packages/core/src/security/Pairing.ts +1 -1
  112. package/packages/core/src/skills/bundled/a2ui/a2ui_dashboard/SKILL.md +176 -0
  113. package/packages/core/src/skills/bundled/a2ui/a2ui_form/SKILL.md +202 -0
  114. package/packages/core/src/skills/bundled/a2ui/a2ui_interactive/SKILL.md +206 -0
  115. package/packages/core/src/skills/bundled/agents/agent_spawner/SKILL.md +173 -0
  116. package/packages/core/src/skills/bundled/agents/memory_manager/SKILL.md +143 -0
  117. package/packages/core/src/skills/bundled/agents/research_and_remember/SKILL.md +139 -0
  118. package/packages/core/src/skills/bundled/agents/task_orchestrator/SKILL.md +98 -0
  119. package/packages/core/src/skills/bundled/api/api_client/SKILL.md +132 -0
  120. package/packages/core/src/skills/bundled/cli/cli_pipeline/SKILL.md +135 -0
  121. package/packages/core/src/skills/bundled/cli/cli_safe_exec/SKILL.md +125 -0
  122. package/packages/core/src/skills/bundled/cli/software_engineering/SKILL.md +23 -0
  123. package/packages/core/src/skills/bundled/cron_manager/SKILL.md +188 -0
  124. package/packages/core/src/skills/bundled/cron_reminder/SKILL.md +112 -0
  125. package/packages/core/src/skills/bundled/filesystem/file_manager/SKILL.md +118 -0
  126. package/packages/core/src/skills/bundled/filesystem/file_read_and_summarize/SKILL.md +109 -0
  127. package/packages/core/src/skills/bundled/filesystem/file_writer/SKILL.md +129 -0
  128. package/packages/core/src/skills/bundled/filesystem/workspace_file_operator/SKILL.md +22 -0
  129. package/packages/core/src/skills/bundled/office/office_document_manager/SKILL.md +262 -0
  130. package/packages/core/src/skills/bundled/search_knowledge/capability_discovery/SKILL.md +75 -0
  131. package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +120 -0
  132. package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +109 -0
  133. package/packages/core/src/skills/bundled/web/web_monitor/SKILL.md +127 -0
  134. package/packages/core/src/skills/bundled/web/web_research/SKILL.md +119 -0
  135. package/packages/core/src/skills/bundled-data.generated.ts +731 -2678
  136. package/packages/core/src/skills/skills.test.ts +52 -11
  137. package/packages/core/src/{harness → storage}/boot-id.ts +5 -2
  138. package/packages/core/src/storage/bootstrap.ts +151 -0
  139. package/packages/core/src/storage/causal-events.ts +84 -0
  140. package/packages/core/src/storage/collections.ts +680 -0
  141. package/packages/core/src/storage/crypto.ts +205 -74
  142. package/packages/core/src/{harness/db-helpers.ts → storage/hive.ts} +63 -7
  143. package/packages/core/src/storage/hivedb.ts +61 -0
  144. package/packages/core/src/storage/index.ts +111 -18
  145. package/packages/core/src/storage/model-id.ts +53 -0
  146. package/packages/core/src/storage/onboarding.ts +540 -972
  147. package/packages/core/src/storage/reconcile.ts +238 -0
  148. package/packages/core/src/storage/seed.ts +572 -406
  149. package/packages/core/src/storage/usage.ts +285 -225
  150. package/packages/core/src/storage/user-email.ts +11 -0
  151. package/packages/core/src/swarm/AgentExecutor.ts +1 -1
  152. package/packages/core/src/swarm/EventBridge.ts +1 -1
  153. package/packages/core/src/swarm/index.ts +12 -9
  154. package/packages/core/src/tool-runtime/index.ts +146 -23
  155. package/packages/core/src/tool-runtime/tool-worker.ts +2 -2
  156. package/packages/core/src/tool-runtime/worker-tools.ts +27 -0
  157. package/packages/core/src/{canvas/a2ui-tools.ts → tools/a2ui/index.ts} +17 -8
  158. package/packages/core/src/tools/agents/get-available-models.ts +36 -54
  159. package/packages/core/src/tools/agents/index.ts +784 -292
  160. package/packages/core/src/tools/api/api-request.test.ts +164 -0
  161. package/packages/core/src/tools/api/api-request.ts +174 -0
  162. package/packages/core/src/tools/api/index.ts +16 -0
  163. package/packages/core/src/tools/cli/index.ts +4 -0
  164. package/packages/core/src/tools/core/index.ts +281 -112
  165. package/packages/core/src/tools/cron/index.ts +121 -124
  166. package/packages/core/src/tools/index.ts +63 -78
  167. package/packages/core/src/tools/office/office-escribir-xlsx.ts +3 -1
  168. package/packages/core/src/tools/types.ts +3 -1
  169. package/packages/core/src/tools/web/artifact-inspect.ts +23 -0
  170. package/packages/core/src/tools/web/browser-screenshot.ts +26 -5
  171. package/packages/core/src/tools/web/browser-service.ts +5 -0
  172. package/packages/core/src/tools/web/browser-type.ts +3 -8
  173. package/packages/core/src/tools/web/index.ts +4 -4
  174. package/packages/core/src/voice/index.ts +89 -63
  175. package/packages/core/src/workers/agent.worker.ts +2 -2
  176. package/packages/core/src/workers/workers.test.ts +3 -10
  177. package/scripts/bump-version.ts +248 -0
  178. package/scripts/generate-skill-bundle.ts +108 -0
  179. package/test/agent-loop-terminal-synthesis.test.ts +32 -0
  180. package/test/catalog-agents-stay-enabled.test.ts +117 -0
  181. package/test/causal-events.test.ts +117 -0
  182. package/test/compaction.test.ts +105 -0
  183. package/test/context-compiler.test.ts +269 -0
  184. package/test/curator.test.ts +130 -0
  185. package/test/durable-queue.test.ts +114 -0
  186. package/test/harness-barrel.test.ts +64 -0
  187. package/test/hive-helpers.test.ts +130 -0
  188. package/test/hivedb-search.test.ts +189 -0
  189. package/test/internal-turns.test.ts +166 -0
  190. package/test/job-idempotency.test.ts +68 -0
  191. package/test/job-retry-backoff.test.ts +184 -0
  192. package/test/job-store.test.ts +381 -0
  193. package/test/llm-retry.test.ts +97 -0
  194. package/test/memory-perf.test.ts +774 -0
  195. package/test/minimal-loadout.test.ts +78 -0
  196. package/test/model-catalog.test.ts +105 -0
  197. package/test/preload.ts +12 -0
  198. package/test/reflector.test.ts +320 -0
  199. package/test/retention-cap.test.ts +91 -0
  200. package/test/retired-capabilities-pruned.test.ts +192 -0
  201. package/test/run-store.test.ts +355 -0
  202. package/test/scratchpad.test.ts +74 -0
  203. package/test/secrets-durability.test.ts +119 -0
  204. package/test/seed-model-reseed.test.ts +155 -0
  205. package/test/setup-agent-seed.test.ts +264 -0
  206. package/test/tool-inventory.test.ts +65 -0
  207. package/test/tool-runtime.test.ts +258 -0
  208. package/test/toon.test.ts +429 -0
  209. package/tsconfig.json +2 -0
  210. package/packages/core/src/ace/Curator.ts +0 -158
  211. package/packages/core/src/ace/Reflector.ts +0 -200
  212. package/packages/core/src/ace/index.ts +0 -4
  213. package/packages/core/src/agent/AgentRunner.ts +0 -711
  214. package/packages/core/src/agent/ContextCompiler.ts +0 -567
  215. package/packages/core/src/agent/ContextGuard.ts +0 -91
  216. package/packages/core/src/agent/ConversationStore.ts +0 -254
  217. package/packages/core/src/agent/Hooks.ts +0 -166
  218. package/packages/core/src/agent/StuckLoop.ts +0 -133
  219. package/packages/core/src/agent/providers/LLMClient.ts +0 -149
  220. package/packages/core/src/agent/providers/anthropic.ts +0 -212
  221. package/packages/core/src/agent/providers/openai-compat.ts +0 -231
  222. package/packages/core/src/agent/selectors/PlaybookSelector.ts +0 -121
  223. package/packages/core/src/agent/selectors/SkillSelector.ts +0 -322
  224. package/packages/core/src/agent/selectors/index.ts +0 -6
  225. package/packages/core/src/auth/auth.ts +0 -121
  226. package/packages/core/src/auth/index.ts +0 -1
  227. package/packages/core/src/canvas/CanvasManager.ts +0 -390
  228. package/packages/core/src/canvas/canvas-tools.ts +0 -448
  229. package/packages/core/src/harness/collections.ts +0 -98
  230. package/packages/core/src/harness/goal-verifier.ts +0 -141
  231. package/packages/core/src/harness/harness.test.ts +0 -236
  232. package/packages/core/src/harness/reconcile.ts +0 -149
  233. package/packages/core/src/mcp/MCPToolAdapter.ts +0 -176
  234. package/packages/core/src/multimodal/VisionService.ts +0 -293
  235. package/packages/core/src/scheduler/dag/AgentExecutor.ts +0 -53
  236. package/packages/core/src/scheduler/dag/DAGScheduler.ts +0 -250
  237. package/packages/core/src/scheduler/dag/EventBridge.ts +0 -122
  238. package/packages/core/src/scheduler/dag/TaskGraph.ts +0 -192
  239. package/packages/core/src/scheduler/dag/TaskNode.ts +0 -97
  240. package/packages/core/src/scheduler/dag/TaskResult.ts +0 -22
  241. package/packages/core/src/scheduler/dag/errors.ts +0 -37
  242. package/packages/core/src/scheduler/dag/index.ts +0 -26
  243. package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +0 -97
  244. package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +0 -21
  245. package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +0 -46
  246. package/packages/core/src/storage/HiveDBStorage.ts +0 -64
  247. package/packages/core/src/storage/SQLiteStorage.ts +0 -414
  248. package/packages/core/src/storage/hiveSeed.ts +0 -308
  249. package/packages/core/src/storage/hiveStorage.test.ts +0 -38
  250. package/packages/core/src/storage/schema.ts +0 -689
  251. package/packages/core/src/storage/storage.test.ts +0 -37
  252. package/packages/core/src/swarm/AgentBus.ts +0 -460
  253. package/packages/core/src/swarm/EventBus.ts +0 -169
  254. package/packages/core/src/swarm/WorkerPool.ts +0 -236
  255. package/packages/core/src/tools/bridge-events.ts +0 -26
  256. package/packages/core/src/tools/canvas/index.ts +0 -375
  257. package/packages/core/src/tools/codebridge/index.ts +0 -342
  258. package/packages/core/src/tools/meeting/index.ts +0 -353
  259. package/packages/core/src/tools/projects/index.ts +0 -37
  260. package/packages/core/src/tools/projects/project-create.ts +0 -94
  261. package/packages/core/src/tools/projects/project-done.ts +0 -66
  262. package/packages/core/src/tools/projects/project-fail.ts +0 -66
  263. package/packages/core/src/tools/projects/project-list.ts +0 -96
  264. package/packages/core/src/tools/projects/project-update.ts +0 -72
  265. package/packages/core/src/tools/projects/task-create.ts +0 -68
  266. package/packages/core/src/tools/projects/task-evaluate.ts +0 -93
  267. package/packages/core/src/tools/projects/task-update.ts +0 -93
  268. package/packages/core/src/tools/voice/index.ts +0 -104
  269. package/packages/core/src/tools/web/api-request.test.ts +0 -170
  270. package/packages/core/src/tools/web/api-request.ts +0 -239
  271. package/test/setup-db.ts +0 -216
  272. /package/packages/core/src/agent/{NativeTools.ts → native-tools.ts} +0 -0
@@ -0,0 +1,150 @@
1
+ // ─── Narration delivery policy for messaging channels ─────────────────────────
2
+ // WebChat renders narration as ephemeral, collapsible "process" widgets. A
3
+ // messaging channel (WhatsApp, Telegram, Slack, Discord) has no such surface:
4
+ // every narration event becomes a permanent chat message. Without a filter a
5
+ // single turn with a few tools and two delegated workers produces dozens of
6
+ // messages before the actual answer — noise for the user and a rate-limit /
7
+ // ban risk on WhatsApp. This module decides what reaches those channels.
8
+
9
+ import { col } from "../storage/hive";
10
+ import type { ChannelDoc, NarrationEventDoc } from "../storage/collections";
11
+ import { logger } from "../utils/logger";
12
+
13
+ const log = logger.child("narration:channel");
14
+
15
+ /** `off` = silent, `milestones` = delegation lifecycle only, `all` = + per-tool steps. */
16
+ export type NarrationMode = "off" | "milestones" | "all";
17
+
18
+ export const DEFAULT_NARRATION_MODE: NarrationMode = "milestones";
19
+
20
+ /** Legacy `step_delivery_mode` values predate this feature — map them forward. */
21
+ function coerceMode(raw: string | null | undefined): NarrationMode {
22
+ switch (raw) {
23
+ case "off":
24
+ case "milestones":
25
+ case "all":
26
+ return raw;
27
+ // "new_messages" was the original (never-read) default.
28
+ case "new_messages":
29
+ return DEFAULT_NARRATION_MODE;
30
+ default:
31
+ return DEFAULT_NARRATION_MODE;
32
+ }
33
+ }
34
+
35
+ /** High-level events: what the user actually cares about seeing in a chat. */
36
+ const MILESTONE_KINDS = new Set<NarrationEventDoc["kind"]>([
37
+ "delegated",
38
+ "worker_started",
39
+ "verified",
40
+ "failed",
41
+ "group_ready",
42
+ ]);
43
+
44
+ const KIND_PREFIX: Record<NarrationEventDoc["kind"], string> = {
45
+ delegated: "📋",
46
+ worker_started: "▶️",
47
+ tool_call: "⚙️",
48
+ tool_result: "⚠️",
49
+ verified: "✅",
50
+ failed: "❌",
51
+ group_ready: "📝",
52
+ };
53
+
54
+ const DETAIL_MAX_CHARS = 200;
55
+
56
+ // ─── Mode lookup (cached) ─────────────────────────────────────────────────────
57
+ // Resolved per channel *type* because narration events only carry the type, not
58
+ // the account id. Short TTL so a settings change takes effect without a restart.
59
+
60
+ const MODE_CACHE_TTL_MS = 30_000;
61
+ const modeCache = new Map<string, { mode: NarrationMode; expiresAt: number }>();
62
+
63
+ export function invalidateNarrationModeCache(channelType?: string): void {
64
+ if (channelType) modeCache.delete(channelType);
65
+ else modeCache.clear();
66
+ }
67
+
68
+ export async function resolveNarrationMode(channelType: string): Promise<NarrationMode> {
69
+ const cached = modeCache.get(channelType);
70
+ if (cached && cached.expiresAt > Date.now()) return cached.mode;
71
+
72
+ let mode = DEFAULT_NARRATION_MODE;
73
+ try {
74
+ const channels = await col<ChannelDoc>("channels");
75
+ const row = (await channels.scan({})).find((e) => e.doc.type === channelType);
76
+ mode = coerceMode(row?.doc.step_delivery_mode);
77
+ } catch (error) {
78
+ log.debug(`Could not resolve narration mode for ${channelType}: ${(error as Error).message}`);
79
+ }
80
+
81
+ modeCache.set(channelType, { mode, expiresAt: Date.now() + MODE_CACHE_TTL_MS });
82
+ return mode;
83
+ }
84
+
85
+ // ─── Filtering ────────────────────────────────────────────────────────────────
86
+
87
+ export function shouldDeliverToChannel(event: NarrationEventDoc, mode: NarrationMode): boolean {
88
+ if (mode === "off") return false;
89
+ if (MILESTONE_KINDS.has(event.kind)) return true;
90
+ if (mode !== "all") return false;
91
+ // Even in `all`, a successful tool_result only restates the tool_call that
92
+ // preceded it. Errors are the part worth surfacing.
93
+ if (event.kind === "tool_result") return event.status === "error";
94
+ return true;
95
+ }
96
+
97
+ // ─── Formatting ───────────────────────────────────────────────────────────────
98
+
99
+ /** Collapses whitespace and trims — raw error details carry stacks and paths. */
100
+ function compactDetail(detail: string): string {
101
+ const flat = detail.replace(/\s+/g, " ").trim();
102
+ if (flat.length <= DETAIL_MAX_CHARS) return flat;
103
+ return `${flat.slice(0, DETAIL_MAX_CHARS - 1)}…`;
104
+ }
105
+
106
+ export function formatNarrationForChannel(event: NarrationEventDoc): string {
107
+ const prefix = KIND_PREFIX[event.kind] ?? "•";
108
+ const detail = event.detail ? compactDetail(event.detail) : "";
109
+ return detail ? `${prefix} ${event.label}\n${detail}` : `${prefix} ${event.label}`;
110
+ }
111
+
112
+ // ─── Ordered, off-critical-path delivery ──────────────────────────────────────
113
+ // `publishNarration` awaits the delivery adapter, so sending inline would put a
114
+ // round-trip to the channel's servers inside the agent loop for every event.
115
+ // Each conversation gets its own promise chain: the agent loop is never blocked,
116
+ // and messages still arrive in the order they were produced.
117
+
118
+ const sendQueues = new Map<string, Promise<void>>();
119
+
120
+ export function enqueueChannelNarration(
121
+ key: string,
122
+ send: () => Promise<void>,
123
+ ): void {
124
+ const previous = sendQueues.get(key) ?? Promise.resolve();
125
+ const next = previous
126
+ .then(send)
127
+ .catch((error: Error) => {
128
+ log.warn(`Narration delivery failed for ${key}: ${error.message}`);
129
+ })
130
+ .finally(() => {
131
+ // Drop the entry once this send is the last one queued, so the map does
132
+ // not grow with one retained promise per conversation seen.
133
+ if (sendQueues.get(key) === next) sendQueues.delete(key);
134
+ });
135
+ sendQueues.set(key, next);
136
+ }
137
+
138
+ /**
139
+ * Resolves once the narration queued for one conversation has been sent. Call
140
+ * before sending a turn's final answer so it cannot overtake pending progress
141
+ * messages, which would show the user the outcome before the steps.
142
+ */
143
+ export async function awaitChannelNarration(key: string): Promise<void> {
144
+ await (sendQueues.get(key) ?? Promise.resolve());
145
+ }
146
+
147
+ /** Test/shutdown helper: resolves once every queued narration has been sent. */
148
+ export async function flushChannelNarration(): Promise<void> {
149
+ await Promise.allSettled([...sendQueues.values()]);
150
+ }
@@ -0,0 +1,82 @@
1
+ import { createHash } from "node:crypto";
2
+ import { col } from "../storage/hive";
3
+ import type { NarrationEventDoc } from "../storage/collections";
4
+ import { logger } from "../utils/logger";
5
+
6
+ const log = logger.child("narration");
7
+
8
+ export type NarrationDelivery = (event: NarrationEventDoc) => Promise<void>;
9
+ const STATE_KEY = Symbol.for("hive.narration.delivery");
10
+ type NarrationState = { delivery: NarrationDelivery | null };
11
+
12
+ function state(): NarrationState {
13
+ const root = globalThis as any;
14
+ root[STATE_KEY] ??= { delivery: null };
15
+ return root[STATE_KEY];
16
+ }
17
+
18
+ export function setNarrationDelivery(next: NarrationDelivery | null): void {
19
+ state().delivery = next;
20
+ log.info(`Delivery adapter ${next ? "registered" : "cleared"}`);
21
+ }
22
+
23
+ function eventId(turnId: string, dedupeKey: string): string {
24
+ return createHash("sha256").update(`${turnId}\0${dedupeKey}`).digest("hex");
25
+ }
26
+
27
+ export async function publishNarration(input: {
28
+ turnId: string;
29
+ threadId: string;
30
+ channel?: string | null;
31
+ userId?: string | null;
32
+ sessionId?: string | null;
33
+ agentId?: string | null;
34
+ agentName?: string | null;
35
+ kind: NarrationEventDoc["kind"];
36
+ status: NarrationEventDoc["status"];
37
+ label: string;
38
+ detail?: string | null;
39
+ dedupeKey: string;
40
+ }): Promise<NarrationEventDoc> {
41
+ const id = eventId(input.turnId, input.dedupeKey);
42
+ const events = await col<NarrationEventDoc>("narrationEvents");
43
+ const existing = await events.get(id);
44
+ if (existing) return existing.doc;
45
+
46
+ const event: NarrationEventDoc = {
47
+ id,
48
+ turn_id: input.turnId,
49
+ thread_id: input.threadId,
50
+ channel: input.channel ?? "",
51
+ user_id: input.userId ?? "",
52
+ session_id: input.sessionId ?? "",
53
+ agent_id: input.agentId ?? "",
54
+ agent_name: input.agentName ?? "",
55
+ kind: input.kind,
56
+ status: input.status,
57
+ label: input.label,
58
+ detail: input.detail ?? null,
59
+ dedupe_key: input.dedupeKey,
60
+ created_at: Date.now(),
61
+ };
62
+
63
+ try {
64
+ await events.put(id, event, { expectedVersion: 0 });
65
+ } catch {
66
+ const raced = await events.get(id);
67
+ if (raced) return raced.doc;
68
+ throw new Error(`Could not persist narration event ${id}`);
69
+ }
70
+ log.info(`Persisted ${event.kind} for turn=${event.turn_id} agent=${event.agent_id || "coordinator"}`);
71
+
72
+ const delivery = state().delivery;
73
+ if (delivery) {
74
+ try {
75
+ await delivery(event);
76
+ log.info(`Delivered ${event.kind} for turn=${event.turn_id} channel=${event.channel || "none"}`);
77
+ } catch (error) {
78
+ log.warn(`Delivery failed for narration ${id}: ${(error as Error).message}`);
79
+ }
80
+ }
81
+ return event;
82
+ }
@@ -0,0 +1,62 @@
1
+ // ─── Tool narration map ───────────────────────────────────────────────────────
2
+ // Maps tool name prefixes/exact names to human-readable Spanish narrations.
3
+ // Shown to the user while the agent executes a tool.
4
+ const TOOL_NARRATIONS: Record<string, string> = {
5
+ // Web
6
+ web_search: "Buscando en la web...",
7
+ web_fetch: "Leyendo página web...",
8
+ // Files
9
+ read: "Leyendo archivo...",
10
+ write: "Escribiendo archivo...",
11
+ edit: "Editando archivo...",
12
+ exec: "Ejecutando comando...",
13
+ // Cron
14
+ "cron.create": "Programando tarea...",
15
+ "cron.list": "Consultando tareas programadas...",
16
+ "cron.update": "Actualizando tarea programada...",
17
+ "cron.delete": "Eliminando tarea programada...",
18
+ "cron.pause": "Pausando tarea programada...",
19
+ "cron.resume": "Reanudando tarea programada...",
20
+ "cron.trigger": "Ejecutando tarea ahora...",
21
+ "cron.history": "Consultando historial...",
22
+ // Agents
23
+ agent_create: "Creando agente worker...",
24
+ agent_find: "Buscando agente disponible...",
25
+ agent_archive: "Archivando agente...",
26
+ get_available_models: "Consultando modelos disponibles...",
27
+ task_delegate: "Delegando tarea a un agente...",
28
+ task_list: "Listando tareas en ejecución...",
29
+ task_status: "Consultando estado de la tarea...",
30
+ bus_publish: "Coordinando con otro agente...",
31
+ bus_read: "Leyendo mensajes de los agentes...",
32
+ // Discovery
33
+ search_knowledge: "Buscando capacidades disponibles...",
34
+ // Memory
35
+ save_note: "Guardando nota...",
36
+ memory_write: "Guardando en memoria...",
37
+ memory_read: "Leyendo memoria...",
38
+ memory_search: "Buscando en memoria...",
39
+ memory_delete: "Eliminando de memoria...",
40
+ memory_list: "Listando notas...",
41
+ // Browser
42
+ browser_navigate: "Navegando a la página...",
43
+ browser_click: "Haciendo clic...",
44
+ browser_type: "Escribiendo en la página...",
45
+ browser_screenshot: "Tomando captura de pantalla...",
46
+ browser_extract: "Extrayendo información de la página...",
47
+ // Canvas
48
+ canvas_add_node: "Actualizando canvas...",
49
+ canvas_update: "Actualizando canvas...",
50
+ // Notify
51
+ notify: "Enviando notificación...",
52
+ report_progress: "Reportando progreso...",
53
+ }
54
+
55
+ export function getNarration(toolName: string): string {
56
+ if (TOOL_NARRATIONS[toolName]) return TOOL_NARRATIONS[toolName]
57
+ // Prefix matching for MCP tools like "github__create_pr" → "Ejecutando github..."
58
+ const prefix = toolName.split("__")[0]
59
+ if (prefix && prefix !== toolName) return `Ejecutando ${prefix}...`
60
+ // Fallback
61
+ return `Ejecutando ${toolName.replace(/_/g, " ")}...`
62
+ }
@@ -0,0 +1,258 @@
1
+ import { col } from "../storage/hive";
2
+ import type {
3
+ DelegationGroupDoc,
4
+ DelegationGroupOutcome,
5
+ } from "../storage/collections";
6
+ import { logger } from "../utils/logger";
7
+ import { publishNarration } from "../events/narration";
8
+
9
+ const log = logger.child("delegation-groups");
10
+ const MAX_RETRIES = 8;
11
+
12
+ export type DelegationSummaryEnqueuer = (
13
+ group: DelegationGroupDoc,
14
+ content: string,
15
+ idempotencyKey: string,
16
+ ) => Promise<{ id: string }>;
17
+
18
+ let summaryEnqueuer: DelegationSummaryEnqueuer | null = null;
19
+
20
+ export function setDelegationSummaryEnqueuer(enqueuer: DelegationSummaryEnqueuer | null): void {
21
+ summaryEnqueuer = enqueuer;
22
+ }
23
+
24
+ function taskIds(group: DelegationGroupDoc): string[] {
25
+ return JSON.parse(group.task_ids_json || "[]");
26
+ }
27
+
28
+ function outcomes(group: DelegationGroupDoc): DelegationGroupOutcome[] {
29
+ return JSON.parse(group.outcomes_json || "[]");
30
+ }
31
+
32
+ async function mutate(
33
+ groupId: string,
34
+ fn: (doc: DelegationGroupDoc) => DelegationGroupDoc,
35
+ ): Promise<DelegationGroupDoc> {
36
+ const groups = await col<DelegationGroupDoc>("delegationGroups");
37
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
38
+ const current = await groups.get(groupId);
39
+ if (!current) throw new Error(`Delegation group not found: ${groupId}`);
40
+ const next = fn(current.doc);
41
+ try {
42
+ await groups.put(groupId, next, { expectedVersion: current.version });
43
+ return next;
44
+ } catch {
45
+ // optimistic concurrency conflict
46
+ }
47
+ }
48
+ throw new Error(`Delegation group contention: ${groupId}`);
49
+ }
50
+
51
+ export async function registerDelegatedTask(input: {
52
+ turnId: string;
53
+ taskId: string;
54
+ threadId: string;
55
+ channel?: string | null;
56
+ userId?: string | null;
57
+ sessionId?: string | null;
58
+ coordinatorAgentId?: string | null;
59
+ }): Promise<DelegationGroupDoc> {
60
+ const groups = await col<DelegationGroupDoc>("delegationGroups");
61
+ const existing = await groups.get(input.turnId);
62
+ if (!existing) {
63
+ const now = Date.now();
64
+ const created: DelegationGroupDoc = {
65
+ id: input.turnId,
66
+ turn_id: input.turnId,
67
+ thread_id: input.threadId,
68
+ channel: input.channel ?? "",
69
+ user_id: input.userId ?? "",
70
+ session_id: input.sessionId ?? "",
71
+ coordinator_agent_id: input.coordinatorAgentId ?? "",
72
+ status: "open",
73
+ task_ids_json: JSON.stringify([input.taskId]),
74
+ outcomes_json: "[]",
75
+ summary_job_id: null,
76
+ created_at: now,
77
+ sealed_at: null,
78
+ ready_at: null,
79
+ notified_at: null,
80
+ };
81
+ try {
82
+ await groups.put(input.turnId, created, { expectedVersion: 0 });
83
+ return created;
84
+ } catch {
85
+ // Another parallel task created the group; append below.
86
+ }
87
+ }
88
+
89
+ return mutate(input.turnId, (group) => {
90
+ if (group.status !== "open") {
91
+ throw new Error(`Delegation group ${input.turnId} is already sealed`);
92
+ }
93
+ const ids = taskIds(group);
94
+ if (!ids.includes(input.taskId)) ids.push(input.taskId);
95
+ return { ...group, task_ids_json: JSON.stringify(ids) };
96
+ });
97
+ }
98
+
99
+ export async function getDelegationGroup(turnId: string): Promise<DelegationGroupDoc | null> {
100
+ return (await (await col<DelegationGroupDoc>("delegationGroups")).get(turnId))?.doc ?? null;
101
+ }
102
+
103
+ function isComplete(group: DelegationGroupDoc): boolean {
104
+ const expected = new Set(taskIds(group));
105
+ const completed = new Set(outcomes(group).map((outcome) => outcome.task_id));
106
+ return expected.size > 0 && [...expected].every((id) => completed.has(id));
107
+ }
108
+
109
+ function summarize(value: unknown, maxLen = 800): unknown {
110
+ if (typeof value !== "string") return value;
111
+ return value.length > maxLen ? `${value.slice(0, maxLen)}…` : value;
112
+ }
113
+
114
+ function summaryPrompt(group: DelegationGroupDoc): string {
115
+ const factual = outcomes(group).map((outcome) => {
116
+ const result = outcome.result as { content?: unknown; acceptance?: unknown; checks?: { status?: string; summary?: string } } | null;
117
+ return {
118
+ task_id: outcome.task_id,
119
+ worker_id: outcome.worker_id,
120
+ task: outcome.task_name,
121
+ ok: outcome.ok,
122
+ content: summarize(result?.content ?? outcome.result),
123
+ acceptance: result?.acceptance ?? null,
124
+ checks: result?.checks ? { status: result.checks.status, summary: summarize(result.checks.summary) } : null,
125
+ error: outcome.error,
126
+ };
127
+ });
128
+ // No manual framing here — persisted with source:"delegation_summary" and
129
+ // framed at serialization time by formatInternalEvent (conversation-store.ts).
130
+ return [
131
+ "Todas las tareas delegadas de este turno alcanzaron estado terminal.",
132
+ "Para cada una, comprobá la entrega contra sus criterios de aceptación (`acceptance`):",
133
+ '- checks.status="passed" → verificado determinísticamente, aceptá.',
134
+ '- checks.status="failed" → NO cumplió (ok=false). No lo reportes como éxito.',
135
+ '- checks.status="unchecked" o ausente → juzgalo vos con el contenido y la evidencia adjunta.',
136
+ "Si una entrega no cumple sus criterios: usá `task_revise` con el task_id y un feedback concreto, o arreglala vos si es trivial y tenés las tools. No inventes trabajo ni evidencia, y no declares éxito para ok=false.",
137
+ "Si todas cumplen, escribí UNA sola respuesta final para el usuario, en lenguaje natural: nunca expongas task_id, worker_id, nombres de tools ni JSON crudo.",
138
+ JSON.stringify(factual),
139
+ ].join("\n\n");
140
+ }
141
+
142
+ async function finalizeIfReady(groupId: string): Promise<DelegationGroupDoc> {
143
+ let group = await getDelegationGroup(groupId);
144
+ if (!group || group.status === "open" || group.status === "notified" || !isComplete(group)) {
145
+ return group!;
146
+ }
147
+ if (group.status === "sealed") {
148
+ group = await mutate(groupId, (current) => {
149
+ if (current.status !== "sealed" || !isComplete(current)) return current;
150
+ return { ...current, status: "ready", ready_at: Date.now() };
151
+ });
152
+ }
153
+ if (group.status !== "ready") return group;
154
+ if (!summaryEnqueuer) {
155
+ log.warn(`Group ${groupId} is ready but no summary enqueuer is registered`);
156
+ return group;
157
+ }
158
+
159
+ const groups = await col<DelegationGroupDoc>("delegationGroups");
160
+ let claimed = false;
161
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
162
+ const current = await groups.get(groupId);
163
+ if (!current) return group;
164
+ if (current.doc.status !== "ready" || current.doc.summary_job_id !== null) {
165
+ return current.doc;
166
+ }
167
+ try {
168
+ await groups.put(groupId, {
169
+ ...current.doc,
170
+ summary_job_id: "__claim__",
171
+ }, { expectedVersion: current.version });
172
+ group = { ...current.doc, summary_job_id: "__claim__" };
173
+ claimed = true;
174
+ break;
175
+ } catch {
176
+ // another terminal hook is racing us
177
+ }
178
+ }
179
+ if (!claimed) return (await getDelegationGroup(groupId))!;
180
+
181
+ let job: { id: string };
182
+ try {
183
+ job = await summaryEnqueuer(
184
+ group,
185
+ summaryPrompt(group),
186
+ `delegation-summary:${group.id}`,
187
+ );
188
+ } catch (error) {
189
+ await mutate(groupId, (current) => current.summary_job_id === "__claim__"
190
+ ? { ...current, summary_job_id: null }
191
+ : current);
192
+ throw error;
193
+ }
194
+ group = await mutate(groupId, (current) => {
195
+ if (current.status === "notified") return current;
196
+ return {
197
+ ...current,
198
+ status: "notified",
199
+ summary_job_id: job.id,
200
+ notified_at: Date.now(),
201
+ };
202
+ });
203
+ await publishNarration({
204
+ turnId: group.turn_id,
205
+ threadId: group.thread_id,
206
+ channel: group.channel,
207
+ userId: group.user_id,
208
+ sessionId: group.session_id,
209
+ agentId: group.coordinator_agent_id,
210
+ agentName: "Coordinador",
211
+ kind: "group_ready",
212
+ status: "done",
213
+ label: "Todas las entregas fueron revisadas; el coordinador prepara una única respuesta final",
214
+ dedupeKey: "delegation_group_ready",
215
+ });
216
+ return group;
217
+ }
218
+
219
+ export async function sealDelegationGroup(turnId: string): Promise<DelegationGroupDoc | null> {
220
+ const existing = await getDelegationGroup(turnId);
221
+ if (!existing) return null;
222
+ if (existing.status === "open") {
223
+ await mutate(turnId, (group) => group.status === "open"
224
+ ? { ...group, status: "sealed", sealed_at: Date.now() }
225
+ : group);
226
+ }
227
+ return finalizeIfReady(turnId);
228
+ }
229
+
230
+ export async function recordDelegationOutcome(input: {
231
+ turnId: string;
232
+ taskId: string;
233
+ jobId: string;
234
+ workerId: string;
235
+ taskName: string;
236
+ ok: boolean;
237
+ result?: unknown;
238
+ error?: string | null;
239
+ }): Promise<DelegationGroupDoc | null> {
240
+ const existing = await getDelegationGroup(input.turnId);
241
+ if (!existing) return null;
242
+ await mutate(input.turnId, (group) => {
243
+ const current = outcomes(group);
244
+ if (current.some((outcome) => outcome.task_id === input.taskId)) return group;
245
+ current.push({
246
+ task_id: input.taskId,
247
+ job_id: input.jobId,
248
+ worker_id: input.workerId,
249
+ task_name: input.taskName,
250
+ ok: input.ok,
251
+ result: input.result ?? null,
252
+ error: input.error ?? null,
253
+ finished_at: Date.now(),
254
+ });
255
+ return { ...group, outcomes_json: JSON.stringify(current) };
256
+ });
257
+ return finalizeIfReady(input.turnId);
258
+ }