@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,162 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ statSync,
7
+ unlinkSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { extname, join } from "node:path";
11
+ import { getHiveDir } from "../config/loader";
12
+ import { col, updateDoc } from "../storage/hive";
13
+ import type { ArtifactDoc } from "../storage/collections";
14
+
15
+ const ARTIFACT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
16
+
17
+ function extensionFor(mimeType: string): string {
18
+ if (mimeType === "image/png") return "png";
19
+ if (mimeType === "image/jpeg") return "jpg";
20
+ if (mimeType === "image/webp") return "webp";
21
+ return "bin";
22
+ }
23
+
24
+ function hash(bytes: Uint8Array): string {
25
+ return createHash("sha256").update(bytes).digest("hex");
26
+ }
27
+
28
+ function detectedMime(bytes: Uint8Array, fallback: string): string {
29
+ if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) {
30
+ return "image/png";
31
+ }
32
+ if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
33
+ return "image/jpeg";
34
+ }
35
+ if (
36
+ bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 &&
37
+ bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50
38
+ ) {
39
+ return "image/webp";
40
+ }
41
+ return fallback;
42
+ }
43
+
44
+ export async function createArtifact(input: {
45
+ bytes: Uint8Array;
46
+ mimeType: string;
47
+ kind: string;
48
+ userId: string;
49
+ runId?: string | null;
50
+ taskId?: string | null;
51
+ rootDir?: string;
52
+ width?: number | null;
53
+ height?: number | null;
54
+ now?: number;
55
+ }): Promise<ArtifactDoc> {
56
+ const now = input.now ?? Date.now();
57
+ const id = randomUUID();
58
+ const root = input.rootDir ?? getHiveDir();
59
+ const mimeType = detectedMime(input.bytes, input.mimeType);
60
+ const day = new Date(now).toISOString().slice(0, 10);
61
+ const dir = join(root, "artifacts", day);
62
+ mkdirSync(dir, { recursive: true });
63
+ const path = join(dir, `${id}.${extensionFor(mimeType)}`);
64
+ writeFileSync(path, input.bytes, { flag: "wx" });
65
+
66
+ const doc: ArtifactDoc = {
67
+ id,
68
+ run_id: input.runId ?? null,
69
+ task_id: input.taskId ?? null,
70
+ user_id: input.userId,
71
+ kind: input.kind,
72
+ path,
73
+ mime_type: mimeType,
74
+ size: input.bytes.byteLength,
75
+ sha256: hash(input.bytes),
76
+ width: input.width ?? null,
77
+ height: input.height ?? null,
78
+ status: "active",
79
+ created_at: now,
80
+ expires_at: now + ARTIFACT_RETENTION_MS,
81
+ expired_at: null,
82
+ };
83
+
84
+ try {
85
+ const artifacts = await col<ArtifactDoc>("artifacts");
86
+ await artifacts.put(id, doc, { expectedVersion: 0 });
87
+ return doc;
88
+ } catch (error) {
89
+ try { unlinkSync(path); } catch { /* best effort rollback */ }
90
+ throw error;
91
+ }
92
+ }
93
+
94
+ export async function inspectArtifact(
95
+ artifactId: string,
96
+ options: { userId?: string } = {},
97
+ ): Promise<Record<string, unknown>> {
98
+ const artifacts = await col<ArtifactDoc>("artifacts");
99
+ const entry = await artifacts.get(artifactId);
100
+ if (!entry) return { ok: false, artifact_id: artifactId, error: "Artifact not found" };
101
+ const artifact = entry.doc;
102
+ if (options.userId && artifact.user_id && artifact.user_id !== options.userId) {
103
+ return { ok: false, artifact_id: artifactId, error: "Artifact not accessible" };
104
+ }
105
+ if (artifact.status !== "active") {
106
+ return {
107
+ ok: false,
108
+ artifact_id: artifactId,
109
+ status: artifact.status,
110
+ expired_at: artifact.expired_at,
111
+ error: "Artifact binary has expired",
112
+ };
113
+ }
114
+ if (!existsSync(artifact.path)) {
115
+ return {
116
+ ok: false,
117
+ artifact_id: artifactId,
118
+ status: "missing",
119
+ error: "Artifact binary is missing",
120
+ };
121
+ }
122
+
123
+ const bytes = readFileSync(artifact.path);
124
+ const actualHash = hash(bytes);
125
+ const stat = statSync(artifact.path);
126
+ return {
127
+ ok: actualHash === artifact.sha256 && stat.size === artifact.size,
128
+ artifact_id: artifact.id,
129
+ kind: artifact.kind,
130
+ mime_type: detectedMime(bytes, artifact.mime_type),
131
+ extension: extname(artifact.path),
132
+ size: stat.size,
133
+ expected_size: artifact.size,
134
+ sha256: actualHash,
135
+ sha256_matches: actualHash === artifact.sha256,
136
+ width: artifact.width,
137
+ height: artifact.height,
138
+ created_at: artifact.created_at,
139
+ expires_at: artifact.expires_at,
140
+ status: artifact.status,
141
+ };
142
+ }
143
+
144
+ export async function expireArtifacts(now = Date.now()): Promise<{ expired: number }> {
145
+ const artifacts = await col<ArtifactDoc>("artifacts");
146
+ const rows = await artifacts.scan({});
147
+ let expired = 0;
148
+ for (const row of rows) {
149
+ if (row.doc.status !== "active" || row.doc.expires_at > now) continue;
150
+ try {
151
+ if (existsSync(row.doc.path)) unlinkSync(row.doc.path);
152
+ } catch {
153
+ continue;
154
+ }
155
+ await updateDoc<ArtifactDoc>("artifacts", row.id, {
156
+ status: "expired",
157
+ expired_at: now,
158
+ });
159
+ expired++;
160
+ }
161
+ return { expired };
162
+ }
@@ -0,0 +1,161 @@
1
+ import { logger } from "../utils/logger.ts";
2
+ import { eventBus } from "../events/event-bus.ts";
3
+
4
+ export interface WebSocketLike {
5
+ readyState: number;
6
+ send(data: string | ArrayBuffer | Uint8Array): number | boolean;
7
+ close(code?: number, reason?: string): void;
8
+ }
9
+
10
+ export const WebSocketState = {
11
+ CONNECTING: 0,
12
+ OPEN: 1,
13
+ CLOSING: 2,
14
+ CLOSED: 3,
15
+ };
16
+
17
+ interface A2UISurfaceCache {
18
+ createData: Record<string, unknown>;
19
+ components?: unknown[];
20
+ dataModel?: Record<string, unknown>;
21
+ }
22
+
23
+ export class CanvasManager {
24
+ private sessions: Map<string, WebSocketLike> = new Map();
25
+ private a2uiCache: Map<string, A2UISurfaceCache> = new Map();
26
+ private log = logger.child("canvas");
27
+ private heartbeatInterval: ReturnType<typeof setInterval> | null = null;
28
+
29
+ constructor() {
30
+ this.startHeartbeat();
31
+ }
32
+
33
+ private startHeartbeat(): void {
34
+ if (this.heartbeatInterval) return;
35
+
36
+ // Enviar ping a todas las sesiones cada 30 segundos
37
+ this.heartbeatInterval = setInterval(() => {
38
+ for (const [sessionId, ws] of this.sessions) {
39
+ if (ws.readyState === WebSocketState.OPEN) {
40
+ try {
41
+ ws.send(JSON.stringify({ type: "canvas:ping", sessionId }));
42
+ this.log.debug(`Heartbeat sent to ${sessionId}`);
43
+ } catch (e) {
44
+ this.log.error(`Failed to send heartbeat to ${sessionId}`);
45
+ }
46
+ }
47
+ }
48
+ }, 30000);
49
+ }
50
+
51
+ private stopHeartbeat(): void {
52
+ if (this.heartbeatInterval) {
53
+ clearInterval(this.heartbeatInterval);
54
+ this.heartbeatInterval = null;
55
+ }
56
+ }
57
+
58
+ registerSession(sessionId: string, ws: WebSocketLike): void {
59
+ this.sessions.set(sessionId, ws);
60
+ this.log.info(`Canvas session registered: ${sessionId}`);
61
+
62
+ eventBus.emit("tool:completed" as any, {
63
+ toolName: "canvas:session:register",
64
+ result: { sessionId },
65
+ duration: 0,
66
+ success: true,
67
+ });
68
+
69
+ // Notify the client that the session is registered
70
+ ws.send(JSON.stringify({ type: "canvas:connected", sessionId }));
71
+
72
+ // Replay any cached A2UI surfaces so late-connecting clients get current state
73
+ for (const [surfaceId, cache] of this.a2uiCache) {
74
+ try {
75
+ ws.send(JSON.stringify({ type: "a2ui:createSurface", data: cache.createData }));
76
+ if (cache.components && cache.components.length > 0) {
77
+ ws.send(JSON.stringify({ type: "a2ui:updateComponents", data: { surfaceId, components: cache.components } }));
78
+ }
79
+ if (cache.dataModel && Object.keys(cache.dataModel).length > 0) {
80
+ ws.send(JSON.stringify({ type: "a2ui:updateDataModel", data: { surfaceId, path: undefined, value: cache.dataModel } }));
81
+ }
82
+ this.log.debug(`Replayed A2UI surface '${surfaceId}' to session ${sessionId}`);
83
+ } catch (e) {
84
+ this.log.warn(`Failed to replay A2UI surface '${surfaceId}' to ${sessionId}`);
85
+ }
86
+ }
87
+ }
88
+
89
+ unregisterSession(sessionId: string, ws?: WebSocketLike): void {
90
+ if (ws && this.sessions.get(sessionId) !== ws) return;
91
+ this.sessions.delete(sessionId);
92
+ this.log.info(`Canvas session disconnected: ${sessionId}`);
93
+ }
94
+
95
+ async sendA2UIMessage(sessionId: string, messageType: string, data: Record<string, unknown>): Promise<void> {
96
+ // Update A2UI cache so late-connecting clients can receive current state
97
+ const surfaceId = data.surfaceId as string | undefined;
98
+ if (surfaceId) {
99
+ if (messageType === "a2ui:createSurface") {
100
+ this.a2uiCache.set(surfaceId, { createData: data });
101
+ } else if (messageType === "a2ui:updateComponents") {
102
+ const cached = this.a2uiCache.get(surfaceId);
103
+ if (cached) cached.components = data.components as unknown[];
104
+ } else if (messageType === "a2ui:updateDataModel") {
105
+ const cached = this.a2uiCache.get(surfaceId);
106
+ if (cached) {
107
+ const path = data.path as string | undefined;
108
+ const value = data.value as Record<string, unknown>;
109
+ if (!path || path === "/") {
110
+ cached.dataModel = value;
111
+ } else {
112
+ cached.dataModel = cached.dataModel ?? {};
113
+ // Store the full model snapshot when possible; partial paths accumulate
114
+ const key = path.replace(/^\//, "").split("/")[0];
115
+ if (key) cached.dataModel[key] = value;
116
+ }
117
+ }
118
+ } else if (messageType === "a2ui:deleteSurface") {
119
+ this.a2uiCache.delete(surfaceId);
120
+ }
121
+ }
122
+
123
+ const ws = this.sessions.get(sessionId);
124
+
125
+ if (!ws || ws.readyState !== WebSocketState.OPEN) {
126
+ const connected = this.getConnectedSessions();
127
+ this.log.warn(`Session ${sessionId} NOT connected for A2UI message. Cached for replay. Available: ${connected.join(", ")}`);
128
+ return;
129
+ }
130
+
131
+ ws.send(JSON.stringify({ type: messageType, data }));
132
+ this.log.debug(`Sent A2UI message '${messageType}' to session ${sessionId}`);
133
+ }
134
+
135
+ isSessionConnected(sessionId: string): boolean {
136
+ const ws = this.sessions.get(sessionId);
137
+ return ws !== undefined && ws.readyState === WebSocketState.OPEN;
138
+ }
139
+
140
+ getConnectedSessions(): string[] {
141
+ return Array.from(this.sessions.entries())
142
+ .filter(([_, ws]) => ws.readyState === WebSocketState.OPEN)
143
+ .map(([id]) => id);
144
+ }
145
+
146
+ getStats(): { totalSessions: number; activeSessions: number } {
147
+ return {
148
+ totalSessions: this.sessions.size,
149
+ activeSessions: this.getConnectedSessions().length,
150
+ };
151
+ }
152
+
153
+ clearAll(): void {
154
+ this.stopHeartbeat();
155
+ this.sessions.clear();
156
+ this.a2uiCache.clear();
157
+ this.log.info("Canvas manager cleared");
158
+ }
159
+ }
160
+
161
+ export const canvasManager = new CanvasManager();
@@ -1,6 +1,10 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
  import { emitCanvas, subscribeCanvas, unsubscribeCanvas } from "./emitter.ts";
3
3
 
4
+ // `canvas:render` desapareció en 0.1.5: el canvas dejó de emitir componentes
5
+ // sueltos y pasó a un modelo de grafo (`node_add` / `node_update` / `edge_*`),
6
+ // que es el que consume la vista de oficina 3D.
7
+
4
8
  describe("canvas emitter", () => {
5
9
  it("subscribes and unsubscribes websocket-like objects", () => {
6
10
  let received: any = null;
@@ -9,11 +13,11 @@ describe("canvas emitter", () => {
9
13
  };
10
14
 
11
15
  subscribeCanvas(ws);
12
- emitCanvas("canvas:render", { component: { id: "test", type: "card" } });
16
+ emitCanvas("canvas:node_update", { nodeId: "test", changes: { status: "thinking" } });
13
17
  unsubscribeCanvas(ws);
14
18
 
15
19
  expect(received).toBeDefined();
16
- expect(received.type).toBe("canvas:render");
20
+ expect(received.type).toBe("canvas:node_update");
17
21
  });
18
22
 
19
23
  it("stops receiving after unsubscribe", () => {
@@ -23,9 +27,9 @@ describe("canvas emitter", () => {
23
27
  };
24
28
 
25
29
  subscribeCanvas(ws);
26
- emitCanvas("canvas:render", { component: { id: "a", type: "card" } });
30
+ emitCanvas("canvas:node_update", { nodeId: "a", changes: { status: "thinking" } });
27
31
  unsubscribeCanvas(ws);
28
- emitCanvas("canvas:render", { component: { id: "b", type: "card" } });
32
+ emitCanvas("canvas:node_update", { nodeId: "b", changes: { status: "idle" } });
29
33
 
30
34
  expect(count).toBe(1);
31
35
  });
@@ -1,4 +1,5 @@
1
- import { getDb } from "../storage/SQLiteStorage"
1
+ import { col, fromIndexable } from "../storage/hive"
2
+ import type { AgentDoc, McpServerDoc } from "../storage/collections"
2
3
 
3
4
  export interface CanvasEvent {
4
5
  type: CanvasEventType
@@ -13,17 +14,48 @@ export type CanvasEventType =
13
14
  | "canvas:node_remove"
14
15
  | "canvas:edge_add"
15
16
  | "canvas:edge_remove"
16
- | "canvas:render"
17
- | "canvas:ask"
18
- | "canvas:confirm"
19
- | "canvas:clear"
17
+ | "canvas:work_event"
20
18
  | "ag-ui:event"
21
19
 
22
- const subscribers = new Set<{ send: (data: string) => void }>()
20
+ export type CanvasWorkPhase =
21
+ | "delegated"
22
+ | "review_passed"
23
+ | "review_failed"
24
+ | "completed"
25
+ | "failed"
26
+ | "aborted"
27
+ | "blocked"
28
+
29
+ export interface CanvasWorkEvent {
30
+ eventId: string
31
+ phase: CanvasWorkPhase
32
+ taskRef: string
33
+ taskName: string
34
+ actorId: string
35
+ targetId: string | null
36
+ toolName?: string | null
37
+ detail?: string | null
38
+ }
23
39
 
24
- interface AgentLiveState { status: string; currentTool: string | null }
40
+ const subscribers = new Set<{ send: (data: string) => void }>()
41
+ let workEventSequence = 0
42
+
43
+ interface AgentLiveState {
44
+ status: string
45
+ currentTool: string | null
46
+ currentTask: string | null
47
+ taskId: string | null
48
+ delegatedBy: string | null
49
+ }
25
50
  const agentLiveState = new Map<string, AgentLiveState>()
26
- const canvasComponents = new Map<string, unknown>()
51
+
52
+ const LIVE_DEFAULTS: AgentLiveState = {
53
+ status: "idle",
54
+ currentTool: null,
55
+ currentTask: null,
56
+ taskId: null,
57
+ delegatedBy: null,
58
+ }
27
59
 
28
60
  export function subscribeCanvas(ws: { send: (data: string) => void }) {
29
61
  subscribers.add(ws)
@@ -34,20 +66,16 @@ export function unsubscribeCanvas(ws: { send: (data: string) => void }) {
34
66
  }
35
67
 
36
68
  export function emitCanvas(type: CanvasEventType, data: any) {
37
- // Track canvas components for new subscribers
38
- if (type === "canvas:render" && data?.component?.id) {
39
- canvasComponents.set(data.component.id, data.component)
40
- }
41
- if (type === "canvas:clear") {
42
- canvasComponents.clear()
43
- }
44
-
45
69
  // Track live agent state for new subscribers
46
70
  if (type === "canvas:node_update" && data?.nodeId && data?.changes) {
47
- const prev = agentLiveState.get(data.nodeId) ?? { status: "idle", currentTool: null }
71
+ const prev = agentLiveState.get(data.nodeId) ?? { ...LIVE_DEFAULTS }
72
+ const c = data.changes
48
73
  agentLiveState.set(data.nodeId, {
49
- status: data.changes.status ?? prev.status,
50
- currentTool: "currentTool" in data.changes ? data.changes.currentTool : prev.currentTool,
74
+ status: c.status ?? prev.status,
75
+ currentTool: "currentTool" in c ? c.currentTool : prev.currentTool,
76
+ currentTask: "currentTask" in c ? c.currentTask : prev.currentTask,
77
+ taskId: "taskId" in c ? c.taskId : prev.taskId,
78
+ delegatedBy: "delegatedBy" in c ? c.delegatedBy : prev.delegatedBy,
51
79
  })
52
80
  }
53
81
 
@@ -62,17 +90,69 @@ export function emitCanvas(type: CanvasEventType, data: any) {
62
90
  }
63
91
  }
64
92
 
65
- export function removeCanvasComponent(id: string) {
66
- canvasComponents.delete(id);
93
+ /** Evento semántico y transitorio para presentar el trabajo sin inferir resultados. */
94
+ export function emitWorkEvent(event: Omit<CanvasWorkEvent, "eventId">): CanvasWorkEvent {
95
+ const emitted: CanvasWorkEvent = {
96
+ eventId: `work:${event.taskRef}:${event.phase}:${Date.now().toString(36)}:${++workEventSequence}`,
97
+ ...event,
98
+ taskName: event.taskName.slice(0, 120),
99
+ detail: event.detail?.slice(0, 180) ?? null,
100
+ }
101
+ emitCanvas("canvas:work_event", emitted)
102
+ return emitted
67
103
  }
68
104
 
69
- export function getCanvasSnapshot() {
70
- const db = getDb()
105
+ /**
106
+ * Marca visualmente el inicio de una delegación coordinador→worker:
107
+ * el worker muestra la tarea en curso y aparece el edge "delegates".
108
+ */
109
+ export function emitDelegationStarted(opts: {
110
+ workerId: string
111
+ parentAgentId: string
112
+ taskRef: string
113
+ taskName: string
114
+ }) {
115
+ emitWorkEvent({
116
+ phase: "delegated",
117
+ taskRef: opts.taskRef,
118
+ taskName: opts.taskName,
119
+ actorId: opts.parentAgentId,
120
+ targetId: opts.workerId,
121
+ })
122
+ emitCanvas("canvas:node_update", {
123
+ nodeId: opts.workerId,
124
+ changes: {
125
+ status: "thinking",
126
+ currentTask: opts.taskName,
127
+ taskId: opts.taskRef,
128
+ delegatedBy: opts.parentAgentId,
129
+ },
130
+ })
131
+ if (opts.parentAgentId) {
132
+ emitCanvas("canvas:edge_add", {
133
+ id: `deleg_${opts.taskRef}`,
134
+ source: opts.parentAgentId,
135
+ target: opts.workerId,
136
+ edgeType: "delegates",
137
+ data: { taskId: opts.taskRef, taskName: opts.taskName },
138
+ })
139
+ }
140
+ }
141
+
142
+ /** Limpia el estado visual de delegación (éxito, fallo o aborto). */
143
+ export function emitDelegationFinished(opts: { workerId: string; taskRef: string }) {
144
+ emitCanvas("canvas:node_update", {
145
+ nodeId: opts.workerId,
146
+ changes: { status: "idle", currentTool: null, currentTask: null, taskId: null, delegatedBy: null },
147
+ })
148
+ emitCanvas("canvas:edge_remove", { id: `deleg_${opts.taskRef}` })
149
+ }
71
150
 
72
- const agentNodes = db
73
- .query<any, []>("SELECT id, name, description, role, status FROM agents")
74
- .all()
75
- .map((a: any) => {
151
+ export async function getCanvasSnapshot() {
152
+ const agentsCol = await col<AgentDoc>("agents")
153
+ const agentNodes = (await agentsCol.scan({}))
154
+ .map(e => e.doc)
155
+ .map((a) => {
76
156
  const live = agentLiveState.get(a.id)
77
157
  return {
78
158
  id: a.id,
@@ -80,70 +160,41 @@ export function getCanvasSnapshot() {
80
160
  description: a.description,
81
161
  status: live?.status ?? a.status,
82
162
  type: "agent",
83
- data: { role: a.role, currentTool: live?.currentTool ?? null },
163
+ data: {
164
+ role: a.role,
165
+ currentTool: live?.currentTool ?? null,
166
+ currentTask: live?.currentTask ?? null,
167
+ taskId: live?.taskId ?? null,
168
+ delegatedBy: live?.delegatedBy ?? null,
169
+ source: a.source ?? null,
170
+ },
84
171
  }
85
172
  })
86
173
 
87
- const mcpNodes = db
88
- .query<any, []>("SELECT id, name, status FROM mcp_servers WHERE enabled = 1")
89
- .all()
90
- .map((m: any) => ({
174
+ const mcpServersCol = await col<McpServerDoc>("mcpServers")
175
+ const mcpNodes = (await mcpServersCol.scan({}))
176
+ .map(e => e.doc)
177
+ .filter(m => m.enabled)
178
+ .map((m) => ({
91
179
  id: `mcp:${m.id}`,
92
180
  name: m.name,
93
181
  status: m.status,
94
182
  type: "mcp",
95
183
  }))
96
184
 
97
- // Proyectos activos
98
- const projectNodes = db
99
- .query<any, []>("SELECT id, name, type, status, progress, agent_id FROM projects WHERE status IN ('active','pending','paused')")
100
- .all()
101
- .map((p: any) => ({
102
- id: `project_${p.id}`,
103
- name: p.name,
104
- status: p.status,
105
- type: "project",
106
- data: { progress: p.progress, projectType: p.type, agentId: p.agent_id },
107
- }))
108
-
109
- // Tareas de proyectos activos
110
- const taskNodes = db
111
- .query<any, []>(`
112
- SELECT t.id, t.name, t.status, t.progress, t.agent_id, t.project_id
113
- FROM tasks t
114
- INNER JOIN projects p ON t.project_id = p.id
115
- WHERE p.status IN ('active','pending','paused')
116
- `)
117
- .all()
118
- .map((t: any) => ({
119
- id: `task_${t.id}`,
120
- name: t.name,
121
- status: t.status,
122
- type: "task",
123
- data: { progress: t.progress, agentId: t.agent_id, projectId: t.project_id },
124
- }))
125
-
126
- // Edges: proyecto → tarea
127
- const projectTaskEdges = taskNodes.map((t: any) => ({
128
- id: `edge_proj_task_${t.id.replace("task_", "")}`,
129
- source: `project_${t.data.projectId}`,
130
- target: t.id,
131
- edgeType: "contains",
132
- }))
133
-
134
- // Edges: tarea → agente asignado
135
- const taskAgentEdges = taskNodes
136
- .filter((t: any) => t.data.agentId)
137
- .map((t: any) => ({
138
- id: `edge_task_agent_${t.id.replace("task_", "")}`,
139
- source: t.id,
140
- target: t.data.agentId,
141
- edgeType: "assigned_to",
185
+ // Edges: delegaciones activas coordinador → worker (sobreviven reconexiones)
186
+ const delegationEdges = Array.from(agentLiveState.entries())
187
+ .filter(([, s]) => s.delegatedBy && s.taskId)
188
+ .map(([workerId, s]) => ({
189
+ id: `deleg_${s.taskId}`,
190
+ source: s.delegatedBy as string,
191
+ target: workerId,
192
+ edgeType: "delegates",
193
+ data: { taskId: s.taskId, taskName: s.currentTask },
142
194
  }))
143
195
 
144
196
  return {
145
- nodes: [...agentNodes, ...mcpNodes, ...projectNodes, ...taskNodes],
146
- edges: [...projectTaskEdges, ...taskAgentEdges],
147
- components: Array.from(canvasComponents.values()),
197
+ nodes: [...agentNodes, ...mcpNodes],
198
+ edges: [...delegationEdges],
148
199
  }
149
200
  }
@@ -1,3 +1 @@
1
- export * from "./CanvasManager.ts";
2
- export * from "./canvas-tools.ts";
3
- export * from "./a2ui-tools.ts";
1
+ export * from "./canvas-manager.ts";
@@ -1,12 +1,18 @@
1
1
  export interface OutboundMessage {
2
- type: "message" | "stream" | "status" | "error" | "pong" | "command_result" | "log" | "typing" | "audio";
2
+ type: "message" | "stream" | "status" | "error" | "pong" | "command_result" | "log" | "typing" | "audio" | "process" | "progress" | "notification";
3
3
  sessionId: string;
4
4
  id?: string; // Message ID for streaming
5
+ messageId?: string;
5
6
  content?: string;
6
7
  chunk?: string;
7
8
  isChunk?: boolean; // True if this is a streaming chunk
8
9
  isLast?: boolean;
9
10
  isStep?: boolean;
11
+ processKind?: "analysis" | "tool" | "observation" | "writing";
12
+ processStatus?: "thinking" | "done" | "error";
13
+ label?: string;
14
+ detail?: string;
15
+ summary?: string;
10
16
  stepType?: "plan" | "tool_call" | "tool_result" | "text";
11
17
  audio?: {
12
18
  buffer?: Buffer;
@@ -20,6 +26,8 @@ export interface OutboundMessage {
20
26
  };
21
27
  error?: string;
22
28
  result?: unknown;
29
+ notificationId?: string;
30
+ createdAt?: number;
23
31
  logEntry?: {
24
32
  timestamp: string;
25
33
  level: string;
@@ -9,7 +9,8 @@ import {
9
9
  } from "discord.js";
10
10
  import { BaseChannel, type ChannelConfig, type IncomingMessage, type OutboundMessage } from "./base.ts";
11
11
  import { logger } from "../utils/logger.ts";
12
- import { getDb } from "../storage/SQLiteStorage.ts";
12
+ import { updateDoc } from "../storage/hive.ts";
13
+ import type { ChannelDoc } from "../storage/collections.ts";
13
14
 
14
15
  export interface DiscordConfig extends ChannelConfig {
15
16
  botToken: string;
@@ -56,11 +57,11 @@ export class DiscordChannel extends BaseChannel {
56
57
  this.log.error(`Discord client error: ${error.message}`);
57
58
  });
58
59
 
59
- this.client.once(Events.ClientReady, () => {
60
+ this.client.once(Events.ClientReady, async () => {
60
61
  this.log.info(`Discord bot started: ${this.client?.user?.tag ?? "unknown"}`);
61
62
  this.running = true;
62
63
  try {
63
- getDb().query(`UPDATE channels SET status = 'connected' WHERE id = ?`).run(this.accountId);
64
+ await updateDoc<ChannelDoc>("channels", this.accountId, { status: "connected" });
64
65
  } catch { /* ignore DB errors */ }
65
66
  });
66
67
 
@@ -137,7 +138,7 @@ export class DiscordChannel extends BaseChannel {
137
138
  this.running = false;
138
139
  this.log.info("Discord bot stopped");
139
140
  try {
140
- getDb().query(`UPDATE channels SET status = 'disconnected' WHERE id = ?`).run(this.accountId);
141
+ await updateDoc<ChannelDoc>("channels", this.accountId, { status: "disconnected" });
141
142
  } catch { /* ignore DB errors */ }
142
143
  }
143
144
  }