@johpaz/hive-sdk 0.1.6 → 0.3.0

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 (238) hide show
  1. package/CHANGELOG.md +441 -0
  2. package/README.md +21 -3
  3. package/package.json +27 -5
  4. package/packages/core/src/agent/acceptance-checks.ts +9 -9
  5. package/packages/core/src/agent/agent-catalog.ts +82 -25
  6. package/packages/core/src/agent/agent-loop.ts +115 -26
  7. package/packages/core/src/agent/capability-search.ts +2 -2
  8. package/packages/core/src/agent/catalog-selector.ts +4 -4
  9. package/packages/core/src/agent/compaction.ts +30 -10
  10. package/packages/core/src/agent/context-compiler.ts +63 -36
  11. package/packages/core/src/agent/conversation-store.ts +167 -9
  12. package/packages/core/src/agent/curator.ts +16 -7
  13. package/packages/core/src/agent/delegation-runtime.ts +5 -5
  14. package/packages/core/src/agent/goal-runner.ts +9 -9
  15. package/packages/core/src/agent/index.ts +1 -0
  16. package/packages/core/src/agent/llm-client.ts +98 -37
  17. package/packages/core/src/agent/llm-providers/anthropic.ts +4 -4
  18. package/packages/core/src/agent/llm-providers/deepseek.ts +1 -1
  19. package/packages/core/src/agent/llm-providers/gemini.ts +4 -4
  20. package/packages/core/src/agent/llm-providers/groq.ts +1 -1
  21. package/packages/core/src/agent/llm-providers/hiveagents.ts +3 -3
  22. package/packages/core/src/agent/llm-providers/interface.ts +2 -2
  23. package/packages/core/src/agent/llm-providers/kimi.ts +1 -1
  24. package/packages/core/src/agent/llm-providers/minimax.ts +1 -1
  25. package/packages/core/src/agent/llm-providers/mistral.ts +1 -1
  26. package/packages/core/src/agent/llm-providers/modelscope.ts +1 -1
  27. package/packages/core/src/agent/llm-providers/nvidia.ts +40 -1
  28. package/packages/core/src/agent/llm-providers/ollama.ts +4 -4
  29. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +47 -7
  30. package/packages/core/src/agent/llm-providers/openai.ts +1 -1
  31. package/packages/core/src/agent/llm-providers/opencode-go.ts +1 -1
  32. package/packages/core/src/agent/llm-providers/openrouter.ts +1 -1
  33. package/packages/core/src/agent/llm-providers/qwen.ts +1 -1
  34. package/packages/core/src/agent/llm-providers/z-ai.ts +1 -1
  35. package/packages/core/src/agent/mcp-result-normalizer.ts +192 -0
  36. package/packages/core/src/agent/playbook-selector.ts +22 -7
  37. package/packages/core/src/agent/prompt-builder.ts +5 -5
  38. package/packages/core/src/agent/proof-packet.ts +5 -5
  39. package/packages/core/src/agent/providers/index.ts +39 -4
  40. package/packages/core/src/agent/realtime-providers/gemini-live.ts +238 -0
  41. package/packages/core/src/agent/realtime-providers/index.ts +29 -0
  42. package/packages/core/src/agent/realtime-providers/interface.ts +108 -0
  43. package/packages/core/src/agent/reflector.ts +38 -15
  44. package/packages/core/src/agent/run-store.ts +8 -8
  45. package/packages/core/src/agent/service.ts +10 -10
  46. package/packages/core/src/agent/skill-selector.ts +6 -6
  47. package/packages/core/src/agent/thread-id.ts +71 -0
  48. package/packages/core/src/agent/thread-store.ts +293 -0
  49. package/packages/core/src/agent/tool-selector.ts +9 -5
  50. package/packages/core/src/agent/tracer.ts +5 -5
  51. package/packages/core/src/api/createAgent.ts +67 -2
  52. package/packages/core/src/artifacts/index.ts +15 -0
  53. package/packages/core/src/artifacts/store.ts +161 -5
  54. package/packages/core/src/canvas/emitter.ts +2 -2
  55. package/packages/core/src/canvas/index.ts +9 -0
  56. package/packages/core/src/channels/telegram.ts +1 -1
  57. package/packages/core/src/channels/webchat.ts +1 -1
  58. package/packages/core/src/config/loader.ts +14 -5
  59. package/packages/core/src/ethics/EthicsGuard.ts +7 -1
  60. package/packages/core/src/events/agent-bus.ts +3 -3
  61. package/packages/core/src/events/channel-narration.ts +3 -3
  62. package/packages/core/src/events/event-bus.ts +1 -1
  63. package/packages/core/src/events/index.ts +18 -0
  64. package/packages/core/src/events/narration.ts +3 -3
  65. package/packages/core/src/events/tool-narration.ts +4 -0
  66. package/packages/core/src/gateway/channel-notify.ts +103 -6
  67. package/packages/core/src/gateway/delegation-groups.ts +4 -4
  68. package/packages/core/src/gateway/durable-queue.ts +18 -6
  69. package/packages/core/src/gateway/index.ts +3 -0
  70. package/packages/core/src/gateway/job-store.ts +11 -5
  71. package/packages/core/src/gateway/notification-inbox.ts +2 -2
  72. package/packages/core/src/gateway/server.ts +2 -2
  73. package/packages/core/src/harness/executors.ts +493 -0
  74. package/packages/core/src/harness/index.ts +12 -2
  75. package/packages/core/src/hooks/index.ts +203 -0
  76. package/packages/core/src/images/index.ts +161 -0
  77. package/packages/core/src/index.ts +1 -0
  78. package/packages/core/src/mcp/MCPClient.ts +3 -3
  79. package/packages/core/src/mcp/hot-reload.ts +5 -5
  80. package/packages/core/src/mcp/tool-sync.ts +5 -5
  81. package/packages/core/src/mcp/transports/index.ts +2 -2
  82. package/packages/core/src/mcp/transports/sse.ts +1 -1
  83. package/packages/core/src/models/index.ts +36 -0
  84. package/packages/core/src/multimodal/index.ts +2 -2
  85. package/packages/core/src/multimodal/vision-service.ts +51 -19
  86. package/packages/core/src/plugins/loader.ts +4 -1
  87. package/packages/core/src/resilience/circuit-breaker.ts +16 -5
  88. package/packages/core/src/resilience/index.ts +13 -0
  89. package/packages/core/src/resilience/retry.ts +1 -1
  90. package/packages/core/src/scheduler/CronScheduler.ts +54 -27
  91. package/packages/core/src/scheduler/cron/expression.ts +165 -0
  92. package/packages/core/src/scheduler/cron/index.ts +10 -0
  93. package/packages/core/src/scheduler/cron/job.ts +339 -0
  94. package/packages/core/src/scheduler/cron/next-run.ts +121 -0
  95. package/packages/core/src/scheduler/cron/zoned-time.ts +138 -0
  96. package/packages/core/src/scheduler/index.ts +21 -3
  97. package/packages/core/src/scheduler/integration.ts +25 -14
  98. package/packages/core/src/scheduler/types.ts +3 -18
  99. package/packages/core/src/services/agents.ts +268 -0
  100. package/packages/core/src/services/cron.ts +257 -0
  101. package/packages/core/src/services/endpoints.ts +289 -0
  102. package/packages/core/src/services/ethics.ts +107 -0
  103. package/packages/core/src/services/images.ts +212 -0
  104. package/packages/core/src/services/index.ts +112 -0
  105. package/packages/core/src/services/mcp.ts +201 -0
  106. package/packages/core/src/services/memory.ts +133 -0
  107. package/packages/core/src/services/models.ts +179 -0
  108. package/packages/core/src/services/providers.ts +152 -0
  109. package/packages/core/src/services/setup.ts +222 -0
  110. package/packages/core/src/services/skills.ts +241 -0
  111. package/packages/core/src/services/swarms.ts +307 -0
  112. package/packages/core/src/services/tools.ts +106 -0
  113. package/packages/core/src/sessions/index.ts +268 -0
  114. package/packages/core/src/sessions/resolve.ts +108 -0
  115. package/packages/core/src/skills/SkillLoader.ts +8 -1
  116. package/packages/core/src/skills/bundled/artifacts/artifact_reader/SKILL.md +105 -0
  117. package/packages/core/src/skills/bundled/cron_manager/SKILL.md +21 -11
  118. package/packages/core/src/skills/bundled/images/image_editor/SKILL.md +120 -0
  119. package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +12 -3
  120. package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +22 -7
  121. package/packages/core/src/skills/bundled-data.generated.ts +110 -12
  122. package/packages/core/src/storage/bootstrap.ts +107 -12
  123. package/packages/core/src/storage/causal-events.ts +1 -1
  124. package/packages/core/src/storage/collections.ts +138 -2
  125. package/packages/core/src/storage/crypto.ts +35 -4
  126. package/packages/core/src/storage/hive.ts +1 -1
  127. package/packages/core/src/storage/hivedb.ts +10 -1
  128. package/packages/core/src/storage/index.ts +2 -1
  129. package/packages/core/src/storage/onboarding.ts +61 -45
  130. package/packages/core/src/storage/reconcile.ts +11 -6
  131. package/packages/core/src/storage/seed.ts +191 -23
  132. package/packages/core/src/storage/usage.ts +3 -3
  133. package/packages/core/src/swarm/AgentExecutor.ts +2 -2
  134. package/packages/core/src/swarm/Coordinator.ts +8 -8
  135. package/packages/core/src/swarm/EventBridge.ts +2 -2
  136. package/packages/core/src/swarm/RoleSwarm.ts +234 -0
  137. package/packages/core/src/swarm/TaskGraph.ts +2 -2
  138. package/packages/core/src/swarm/index.ts +7 -0
  139. package/packages/core/src/swarm/presets/HiveLearnPreset.ts +2 -2
  140. package/packages/core/src/swarm/presets/ResearchPreset.ts +2 -2
  141. package/packages/core/src/swarm/strategies/ParallelStrategy.ts +1 -1
  142. package/packages/core/src/swarm/strategies/PriorityStrategy.ts +3 -3
  143. package/packages/core/src/swarm/types.ts +3 -18
  144. package/packages/core/src/tool-runtime/embedded-worker.generated.ts +21 -0
  145. package/packages/core/src/tool-runtime/index.ts +129 -14
  146. package/packages/core/src/tools/ToolExecutor.ts +7 -3
  147. package/packages/core/src/tools/agents/index.ts +18 -60
  148. package/packages/core/src/tools/cli/index.ts +55 -0
  149. package/packages/core/src/tools/core/index.ts +52 -4
  150. package/packages/core/src/tools/cron/index.ts +8 -8
  151. package/packages/core/src/tools/images/index.ts +130 -0
  152. package/packages/core/src/tools/index.ts +14 -1
  153. package/packages/core/src/tools/office/office-escribir-xlsx.ts +2 -1
  154. package/packages/core/src/tools/office/office-leer-xlsx.ts +2 -1
  155. package/packages/core/src/tools/office/xlsx-loader.ts +19 -0
  156. package/packages/core/src/tools/web/artifact-inspect.ts +2 -2
  157. package/packages/core/src/tools/web/artifact-read.ts +162 -0
  158. package/packages/core/src/tools/web/browser-backend.ts +141 -44
  159. package/packages/core/src/tools/web/browser-click.ts +2 -2
  160. package/packages/core/src/tools/web/browser-extract.ts +2 -2
  161. package/packages/core/src/tools/web/browser-navigate.ts +2 -2
  162. package/packages/core/src/tools/web/browser-screenshot.ts +12 -5
  163. package/packages/core/src/tools/web/browser-script.ts +2 -2
  164. package/packages/core/src/tools/web/browser-service.ts +63 -384
  165. package/packages/core/src/tools/web/browser-session.ts +125 -0
  166. package/packages/core/src/tools/web/browser-type.ts +2 -2
  167. package/packages/core/src/tools/web/browser-wait.ts +2 -2
  168. package/packages/core/src/tools/web/computer-use.ts +553 -0
  169. package/packages/core/src/tools/web/index.ts +8 -1
  170. package/packages/core/src/tools/web/webview-backend.ts +460 -21
  171. package/packages/core/src/utils/index.ts +1 -0
  172. package/packages/core/src/utils/logger.ts +12 -4
  173. package/packages/core/src/utils/redact-binary.ts +17 -0
  174. package/packages/core/src/utils/toon.ts +1 -1
  175. package/packages/core/src/voice/index.ts +6 -6
  176. package/bun.lock +0 -859
  177. package/bunfig.toml +0 -9
  178. package/docs/API-AGENTS.md +0 -367
  179. package/docs/API-CONTEXT-COMPILER.md +0 -249
  180. package/docs/API-DAG-SCHEDULER.md +0 -273
  181. package/docs/API-TOOLS-SKILLS-CHANNELS.md +0 -446
  182. package/docs/API-WORKERS-EVENTS.md +0 -299
  183. package/docs/HIVE-HARNESS.md +0 -113
  184. package/docs/INDEX.md +0 -190
  185. package/docs/TEMPLATE-HIVE-APP.md +0 -360
  186. package/packages/cli/package.json +0 -17
  187. package/packages/cli/src/commands/create-app.test.ts +0 -180
  188. package/packages/core/package.json +0 -70
  189. package/packages/core/src/api/createAgent.test.ts +0 -160
  190. package/packages/core/src/canvas/canvas.test.ts +0 -36
  191. package/packages/core/src/channels/channels.test.ts +0 -18
  192. package/packages/core/src/ethics/EthicsGuard.test.ts +0 -108
  193. package/packages/core/src/gateway/gateway.test.ts +0 -38
  194. package/packages/core/src/memory/Scratchpad.test.ts +0 -68
  195. package/packages/core/src/scheduler/scheduler.test.ts +0 -15
  196. package/packages/core/src/skills/skills.test.ts +0 -62
  197. package/packages/core/src/swarm/swarm.test.ts +0 -24
  198. package/packages/core/src/tool-runtime/tool-runtime.test.ts +0 -99
  199. package/packages/core/src/tools/ToolRegistry.test.ts +0 -98
  200. package/packages/core/src/tools/api/api-request.test.ts +0 -164
  201. package/packages/core/src/tools/web/browser-service.test.ts +0 -83
  202. package/packages/core/src/workers/workers.test.ts +0 -41
  203. package/scripts/bump-version.ts +0 -248
  204. package/scripts/generate-skill-bundle.ts +0 -108
  205. package/test/acceptance-checks.test.ts +0 -403
  206. package/test/agent-loop-terminal-synthesis.test.ts +0 -32
  207. package/test/browser-backend.test.ts +0 -308
  208. package/test/catalog-agents-stay-enabled.test.ts +0 -117
  209. package/test/causal-events.test.ts +0 -117
  210. package/test/compaction.test.ts +0 -105
  211. package/test/context-compiler.test.ts +0 -269
  212. package/test/curator.test.ts +0 -130
  213. package/test/durable-queue.test.ts +0 -114
  214. package/test/harness-barrel.test.ts +0 -64
  215. package/test/hive-helpers.test.ts +0 -130
  216. package/test/hivedb-search.test.ts +0 -189
  217. package/test/internal-turns.test.ts +0 -166
  218. package/test/job-idempotency.test.ts +0 -68
  219. package/test/job-retry-backoff.test.ts +0 -184
  220. package/test/job-store.test.ts +0 -381
  221. package/test/llm-retry.test.ts +0 -97
  222. package/test/memory-perf.test.ts +0 -774
  223. package/test/minimal-loadout.test.ts +0 -78
  224. package/test/model-catalog.test.ts +0 -105
  225. package/test/preload.ts +0 -12
  226. package/test/reflector.test.ts +0 -320
  227. package/test/retention-cap.test.ts +0 -91
  228. package/test/retired-capabilities-pruned.test.ts +0 -192
  229. package/test/run-store.test.ts +0 -355
  230. package/test/scratchpad.test.ts +0 -74
  231. package/test/secrets-durability.test.ts +0 -119
  232. package/test/seed-model-reseed.test.ts +0 -155
  233. package/test/setup-agent-seed.test.ts +0 -264
  234. package/test/tool-inventory.test.ts +0 -65
  235. package/test/tool-runtime.test.ts +0 -258
  236. package/test/tool-selector-runtime-tools.test.ts +0 -117
  237. package/test/toon.test.ts +0 -429
  238. package/tsconfig.json +0 -42
@@ -10,14 +10,14 @@
10
10
  * should write to a run; single-writer pattern keeps contention minimal.
11
11
  */
12
12
 
13
- import { col, updateDoc, nextId, toIndexable } from "../storage/hive";
14
- import type { AgentRunDoc } from "../storage/collections";
15
- import { getBootId } from "../storage/boot-id";
16
- import { logger } from "../utils/logger";
17
- import { loadConfig } from "../config/loader";
18
- import type { LLMMessage } from "./llm-client";
19
- import type { RunEpoch } from "./run-epoch";
20
- import { formatInternalEvent } from "./conversation-store";
13
+ import { col, updateDoc, nextId, toIndexable } from "../storage/hive.ts";
14
+ import type { AgentRunDoc } from "../storage/collections.ts";
15
+ import { getBootId } from "../storage/boot-id.ts";
16
+ import { logger } from "../utils/logger.ts";
17
+ import { loadConfig } from "../config/loader.ts";
18
+ import type { LLMMessage } from "./llm-client.ts";
19
+ import type { RunEpoch } from "./run-epoch.ts";
20
+ import { formatInternalEvent } from "./conversation-store.ts";
21
21
 
22
22
  const log = logger.child("run-store");
23
23
 
@@ -12,15 +12,15 @@
12
12
  * - Eventos (cron, etc.)
13
13
  */
14
14
 
15
- import { logger } from "../utils/logger"
16
- import { buildSystemPromptWithProjects } from "./prompt-builder"
17
- import { getAgentLoop, rebuildAgentLoop } from "./agent-loop"
15
+ import { logger } from "../utils/logger.ts"
16
+ import { buildSystemPromptWithProjects } from "./prompt-builder.ts"
17
+ import { getAgentLoop, rebuildAgentLoop } from "./agent-loop.ts"
18
18
  import type { MCPClientManager } from "../mcp/index.ts"
19
- import { resolveAgentId, resolveUserId } from "../storage/onboarding"
20
- import { getMCPManager as getSingletonMCPManager } from "../mcp/singleton"
21
- import type { ContentPart } from "./llm-client"
22
- import { col, fromIndexable } from "../storage/hive"
23
- import type { AgentDoc, EthicsDoc } from "../storage/collections"
19
+ import { resolveAgentId, resolveUserId } from "../storage/onboarding.ts"
20
+ import { getMCPManager as getSingletonMCPManager } from "../mcp/singleton.ts"
21
+ import type { ContentPart } from "./llm-client.ts"
22
+ import { col, fromIndexable } from "../storage/hive.ts"
23
+ import type { AgentDoc, EthicsDoc } from "../storage/collections.ts"
24
24
 
25
25
  const log = logger.child("agent-service")
26
26
 
@@ -161,7 +161,7 @@ export class AgentService {
161
161
  */
162
162
  async reloadSkills(): Promise<void> {
163
163
  log.info("Reloading skills...")
164
- const { syncSkillsToIndex } = await import("./context-compiler")
164
+ const { syncSkillsToIndex } = await import("./context-compiler.ts")
165
165
  await syncSkillsToIndex()
166
166
  log.info("Skills reloaded")
167
167
  }
@@ -252,7 +252,7 @@ export class AgentService {
252
252
  * Ejecuta un agente con un mensaje
253
253
  */
254
254
  async runAgent(message: string | ContentPart[], threadId: string, userId?: string): Promise<string> {
255
- const { runAgentIsolated } = await import("./agent-loop")
255
+ const { runAgentIsolated } = await import("./agent-loop.ts")
256
256
  const result = await runAgentIsolated({
257
257
  agentId: this.agentId,
258
258
  taskDescription: message,
@@ -16,17 +16,17 @@
16
16
  * 5. Returns skill content for injection into system prompt
17
17
  */
18
18
 
19
- import { col } from "../storage/hive"
19
+ import { col } from "../storage/hive.ts"
20
20
  import type { SkillDoc } from "../storage/collections"
21
- import { logger } from "../utils/logger"
22
- import { isMinimalSkill } from "./minimal-loadout"
23
- import { isCalendarOperation } from "./routing-intent"
21
+ import { logger } from "../utils/logger.ts"
22
+ import { isMinimalSkill } from "./minimal-loadout.ts"
23
+ import { isCalendarOperation } from "./routing-intent.ts"
24
24
  import {
25
25
  searchCapabilities,
26
26
  applyRelativeCutoff,
27
27
  replaceCapabilityDocs,
28
28
  type CapabilityDoc,
29
- } from "./capability-search"
29
+ } from "./capability-search.ts"
30
30
 
31
31
  const log = logger.child("skill-selector")
32
32
 
@@ -109,7 +109,7 @@ const STOPWORDS = new Set([
109
109
  "más", "mas", "ya", "yo", "tu", "te", "ti", "mi", "me",
110
110
  "hola", "hi", "hello", "hey", "gracias", "thank", "please",
111
111
  "ok", "okay", "yes", "si", "no", "bien", "good", "great",
112
- "puedes", "necesito", "quiero", "podés", "necesitás", "querés",
112
+ "puedes", "necesito", "quiero", "necesitas", "quieres",
113
113
  ])
114
114
 
115
115
  /** Conversational patterns that should return empty skill list */
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Thread identity — `${userId}/${channel}/${peerId}`.
3
+ *
4
+ * Un hilo por canal Y por contacto: el chat privado de Telegram, cada grupo de
5
+ * WhatsApp y cada conversación de la web tienen historial propio. Antes había un
6
+ * único hilo por usuario (`threadId = userId`) compartido por todos los canales.
7
+ *
8
+ * Dos reglas de formato que no son cosméticas:
9
+ *
10
+ * 1. El separador es "/" y NO ":". Los mensajes viven en `conversations` con id
11
+ * `${threadId}:${seq}` y se leen por prefijo (`scan({ prefix: "${threadId}:" })`),
12
+ * sin índice secundario. El hilo legacy (id = userId pelado) se lee con prefijo
13
+ * `${userId}:`; si los hilos nuevos fueran `${userId}:webchat:...` ese scan se los
14
+ * tragaría enteros. Con "/" el historial viejo queda aislado sin migrar una fila.
15
+ *
16
+ * 2. Ningún segmento puede contener ":" — se sustituye por "_". Sin esto los peerIds
17
+ * compuestos rompen el aislamiento por prefijo: el DM de Telegram `12345` y el grupo
18
+ * `12345:678` (telegram.ts arma así el peerId de grupo) producirían los prefijos
19
+ * `.../12345:` y `.../12345:678:`, y el primero es prefijo del segundo — el DM leería
20
+ * los mensajes del grupo. Colapsar ":" hace que `${threadId}:` sea siempre un
21
+ * separador inequívoco, y mantiene válido el `lastIndexOf(":")` con que
22
+ * conversation-store extrae el número de secuencia.
23
+ */
24
+
25
+ export const THREAD_SEP = "/"
26
+
27
+ /** Canal sintético de las tareas programadas: un hilo estable por cron job. */
28
+ export const CRON_CHANNEL = "cron"
29
+
30
+ export interface ThreadParts {
31
+ userId: string
32
+ channel: string
33
+ peerId: string
34
+ }
35
+
36
+ /**
37
+ * Deja un segmento apto para formar parte de un threadId: sin "/" (rompería el
38
+ * parseo) y sin ":" (rompería el aislamiento por prefijo — ver cabecera).
39
+ */
40
+ export function sanitizeSegment(value: string): string {
41
+ return value.replace(/[/:]/g, "_").trim()
42
+ }
43
+
44
+ export function makeThreadId(userId: string, channel: string, peerId: string): string {
45
+ const parts = [userId, channel, peerId].map((p) => sanitizeSegment(p || "") || "default")
46
+ return parts.join(THREAD_SEP)
47
+ }
48
+
49
+ /**
50
+ * Descompone un threadId con formato. Devuelve null para todo lo que no lo tenga:
51
+ * el hilo legacy (`userId` pelado), los hilos aislados de los workers
52
+ * (`task-<id>-<agente>`) y cualquier id heredado. Quien llame debe conservar su
53
+ * comportamiento anterior ante un null, nunca inventar partes.
54
+ */
55
+ export function parseThreadId(threadId: string): ThreadParts | null {
56
+ if (!threadId) return null
57
+ const parts = threadId.split(THREAD_SEP)
58
+ if (parts.length !== 3) return null
59
+ const [userId, channel, peerId] = parts
60
+ if (!userId || !channel || !peerId) return null
61
+ return { userId, channel, peerId }
62
+ }
63
+
64
+ export function isStructuredThreadId(threadId: string): boolean {
65
+ return parseThreadId(threadId) !== null
66
+ }
67
+
68
+ /** Id de una conversación nueva de la web. No lo elige el cliente. */
69
+ export function newWebConversationId(): string {
70
+ return `conv-${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`
71
+ }
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Thread store — el registro de conversaciones (`conversationThreads`).
3
+ *
4
+ * `conversations` no tiene índice secundario: los mensajes se leen por prefijo del
5
+ * threadId. Esta colección es el catálogo que permite listar las conversaciones de
6
+ * un usuario, ponerles título y borrarlas sin escanear todos los mensajes.
7
+ *
8
+ * El id de cada fila ES el threadId. La única fila cuyo id no tiene formato
9
+ * `${user}/${canal}/${peer}` es la del hilo legacy anterior a la separación por
10
+ * canal (ver ensureLegacyThread).
11
+ */
12
+
13
+ import { col, updateDoc } from "../storage/hive.ts"
14
+ import { logger } from "../utils/logger.ts"
15
+ import type { ConversationThreadDoc, ConversationDoc, SummaryDoc } from "../storage/collections.ts"
16
+ import { makeThreadId, parseThreadId, newWebConversationId } from "./thread-id.ts"
17
+ import { runSessionStart, runSessionEnd } from "../hooks/index.ts"
18
+
19
+ const log = logger.child("thread-store")
20
+
21
+ const TITLE_MAX_CHARS = 60
22
+
23
+ /** Reintentos del contador ante mensajes concurrentes del mismo hilo. */
24
+ const TOUCH_MAX_RETRIES = 5
25
+
26
+ export interface EnsureThreadInput {
27
+ userId: string
28
+ channel: string
29
+ peerId: string
30
+ peerKind?: "direct" | "group"
31
+ }
32
+
33
+ async function threadsCol() {
34
+ return col<ConversationThreadDoc>("conversationThreads")
35
+ }
36
+
37
+ export async function getThread(threadId: string): Promise<ConversationThreadDoc | null> {
38
+ const c = await threadsCol()
39
+ const entry = await c.get(threadId)
40
+ return entry?.doc ?? null
41
+ }
42
+
43
+ /**
44
+ * Crea la fila de la conversación si aún no existe y devuelve su threadId.
45
+ * Idempotente: se llama en cada turno desde resolveContext.
46
+ */
47
+ export async function ensureThread(input: EnsureThreadInput): Promise<string> {
48
+ const threadId = makeThreadId(input.userId, input.channel, input.peerId)
49
+ const c = await threadsCol()
50
+ const existing = await c.get(threadId)
51
+ if (existing) return threadId
52
+
53
+ const now = Date.now()
54
+ try {
55
+ await c.put(threadId, {
56
+ id: threadId,
57
+ user_id: input.userId,
58
+ channel: input.channel,
59
+ peer_id: input.peerId,
60
+ peer_kind: input.peerKind ?? "direct",
61
+ title: null,
62
+ archived: false,
63
+ created_at: now,
64
+ last_message_at: now,
65
+ message_count: 0,
66
+ }, { expectedVersion: 0 })
67
+ } catch {
68
+ // Otro turno del mismo canal la creó primero — ambos quieren lo mismo.
69
+ return threadId
70
+ }
71
+
72
+ // `sessionStart` se dispara acá y no en `createSession` porque esa función es
73
+ // idempotente y se llama en cada turno: engancharla ahí haría que el hook
74
+ // corriera con cada mensaje. El `put` con `expectedVersion: 0` es el punto de
75
+ // serialización, así que dispararlo justo después de que ese put haya salido
76
+ // bien evita el doble disparo cuando dos turnos del mismo canal llegan juntos
77
+ // — el perdedor se fue por el catch de arriba. Cubre además a quien llame
78
+ // `ensureThread` directamente desde un canal, sin pasar por `sessions`.
79
+ await runSessionStart({ threadId, userId: input.userId, channel: input.channel })
80
+ return threadId
81
+ }
82
+
83
+ /** Conversación nueva de la web. El id lo elige el servidor, nunca el cliente. */
84
+ export async function createWebConversation(userId: string, title?: string): Promise<ConversationThreadDoc> {
85
+ const peerId = newWebConversationId()
86
+ const threadId = await ensureThread({ userId, channel: "webchat", peerId, peerKind: "direct" })
87
+ if (title?.trim()) await renameThread(threadId, title)
88
+ const doc = await getThread(threadId)
89
+ if (!doc) throw new Error(`No pude crear la conversación ${threadId}`)
90
+ return doc
91
+ }
92
+
93
+ /**
94
+ * Título derivado del primer mensaje del usuario. El contenido llega con el
95
+ * prefijo `[Timestamp: ...]` que le añaden el gateway y las rutas REST: esa línea
96
+ * no describe nada, así que se descarta antes de recortar.
97
+ */
98
+ export function deriveTitle(text: string): string | null {
99
+ const withoutTimestamp = text.replace(/^\[Timestamp:[^\]]*\]\s*/, "").trim()
100
+ const firstLine = withoutTimestamp.split("\n").find((l) => l.trim().length > 0)?.trim()
101
+ if (!firstLine) return null
102
+ return firstLine.length > TITLE_MAX_CHARS
103
+ ? `${firstLine.slice(0, TITLE_MAX_CHARS - 1)}…`
104
+ : firstLine
105
+ }
106
+
107
+ /**
108
+ * Registra actividad en el hilo: contador, fecha del último mensaje y —la primera
109
+ * vez que habla el usuario— el título.
110
+ *
111
+ * Si el hilo tiene formato y no existe la fila, la crea (es el caso de un hilo que
112
+ * nació fuera de resolveContext, como el de una tarea programada). Si no lo tiene
113
+ * —hilo legacy, hilos aislados `task-*`— solo actualiza una fila ya existente:
114
+ * nunca inventa conversaciones para los hilos internos de los workers.
115
+ */
116
+ export async function touchThread(
117
+ threadId: string,
118
+ opts: { role: "user" | "assistant" | "tool"; text?: string; internal?: boolean }
119
+ ): Promise<void> {
120
+ try {
121
+ const c = await threadsCol()
122
+ let existing = await c.get(threadId)
123
+
124
+ if (!existing) {
125
+ const parts = parseThreadId(threadId)
126
+ if (!parts) return
127
+ await ensureThread({ userId: parts.userId, channel: parts.channel, peerId: parts.peerId })
128
+ existing = await c.get(threadId)
129
+ if (!existing) return
130
+ }
131
+
132
+ // El contador se recalcula DENTRO del reintento, no fuera. `addMessage` llama
133
+ // acá sin esperar el resultado, así que dos mensajes seguidos se solapan: con
134
+ // el incremento calculado una sola vez, el reintento por conflicto de versión
135
+ // vuelve a escribir el valor viejo y el conteo se queda corto para siempre.
136
+ for (let attempt = 0; attempt < TOUCH_MAX_RETRIES; attempt++) {
137
+ const fresh = attempt === 0 ? existing : await c.get(threadId)
138
+ if (!fresh) return
139
+
140
+ const next: ConversationThreadDoc = {
141
+ ...fresh.doc,
142
+ last_message_at: Date.now(),
143
+ message_count: fresh.doc.message_count + 1,
144
+ }
145
+ // El título sale del primer mensaje humano: ni de la respuesta del agente ni
146
+ // de un evento interno (fan-in de delegación), que no describen el tema.
147
+ if (!fresh.doc.title && opts.role === "user" && !opts.internal && opts.text) {
148
+ const title = deriveTitle(opts.text)
149
+ if (title) next.title = title
150
+ }
151
+
152
+ try {
153
+ await c.put(threadId, next, { expectedVersion: fresh.version })
154
+ return
155
+ } catch {
156
+ // Conflicto de versión: otro mensaje del mismo hilo llegó primero.
157
+ }
158
+ }
159
+ log.warn(`demasiada contención actualizando el hilo ${threadId}`)
160
+ } catch (err) {
161
+ // El registro es un catálogo, no la fuente de verdad: nunca debe tumbar un turno.
162
+ log.warn(`no pude actualizar el hilo ${threadId}: ${(err as Error).message}`)
163
+ }
164
+ }
165
+
166
+ export async function listThreads(
167
+ userId: string,
168
+ opts?: { channel?: string; includeArchived?: boolean }
169
+ ): Promise<ConversationThreadDoc[]> {
170
+ const c = await threadsCol()
171
+ const rows = await c.findBy("user_id", userId)
172
+ return rows
173
+ .map((r) => r.doc)
174
+ .filter((d) => (opts?.channel ? d.channel === opts.channel : true))
175
+ .filter((d) => (opts?.includeArchived ? true : !d.archived))
176
+ .sort((a, b) => b.last_message_at - a.last_message_at)
177
+ }
178
+
179
+ /** La conversación de webchat en la que seguiría escribiendo el usuario. */
180
+ export async function mostRecentWebThread(userId: string): Promise<ConversationThreadDoc | null> {
181
+ const threads = await listThreads(userId, { channel: "webchat" })
182
+ return threads[0] ?? null
183
+ }
184
+
185
+ export async function renameThread(threadId: string, title: string): Promise<void> {
186
+ const clean = title.trim().slice(0, 200)
187
+ await updateDoc<ConversationThreadDoc>("conversationThreads", threadId, { title: clean || null })
188
+ }
189
+
190
+ /**
191
+ * Archiva el hilo: sale de la lista pero no se pierde nada.
192
+ *
193
+ * Vive acá y no en `sessions/index.ts` para que las cuatro transiciones del
194
+ * ciclo de vida —crear, archivar, reabrir, borrar— disparen sus hooks desde un
195
+ * solo archivo. Repartidas, la próxima que se agregue se olvida de disparar.
196
+ */
197
+ export async function archiveThread(threadId: string): Promise<void> {
198
+ const anterior = await getThread(threadId)
199
+ await updateDoc<ConversationThreadDoc>("conversationThreads", threadId, { archived: true })
200
+ // Archivar dos veces es un no-op, y el hook no debería enterarse dos veces.
201
+ if (anterior?.archived) return
202
+ await runSessionEnd({ threadId, userId: anterior?.user_id, channel: anterior?.channel })
203
+ }
204
+
205
+ /** Reabre un hilo archivado: vuelve a estar activo, y eso es un `sessionStart`. */
206
+ export async function unarchiveThread(threadId: string): Promise<void> {
207
+ const anterior = await getThread(threadId)
208
+ await updateDoc<ConversationThreadDoc>("conversationThreads", threadId, { archived: false })
209
+ if (anterior && !anterior.archived) return
210
+ await runSessionStart({ threadId, userId: anterior?.user_id, channel: anterior?.channel })
211
+ }
212
+
213
+ /**
214
+ * Borra la conversación entera: mensajes, resumen, notas y la fila del registro.
215
+ * Los mensajes se localizan por prefijo, igual que los lee conversation-store.
216
+ */
217
+ export async function deleteThread(threadId: string): Promise<void> {
218
+ const conversations = await col<ConversationDoc>("conversations")
219
+ const messages = await conversations.scan({ prefix: `${threadId}:` })
220
+ for (const m of messages) await conversations.delete(m.id)
221
+
222
+ const summaries = await col<SummaryDoc>("summaries")
223
+ await summaries.delete(threadId).catch(() => {})
224
+
225
+ const scratchpad = await col<{ threadId: string }>("scratchpad")
226
+ const notes = await scratchpad.scan({ prefix: `${threadId}:` })
227
+ for (const n of notes) await scratchpad.delete(n.id)
228
+
229
+ const c = await threadsCol()
230
+ // La fila se lee antes de borrarla: después no hay de dónde sacar el usuario
231
+ // ni el canal para el contexto del hook. El threadId los lleva codificados,
232
+ // pero el hilo legacy es id = userId pelado y no parsea.
233
+ const fila = (await c.get(threadId))?.doc
234
+ await c.delete(threadId).catch(() => {})
235
+
236
+ log.info(`conversación ${threadId} borrada (${messages.length} mensajes)`)
237
+ await runSessionEnd({
238
+ threadId,
239
+ userId: fila?.user_id ?? parseThreadId(threadId)?.userId,
240
+ channel: fila?.channel ?? parseThreadId(threadId)?.channel,
241
+ })
242
+ }
243
+
244
+ /**
245
+ * El hilo por el que hablarle a alguien en un canal cuando no venimos de un
246
+ * mensaje suyo (avisos de tareas programadas, por ejemplo).
247
+ *
248
+ * En la web es la conversación activa; en los demás canales, el contacto al que
249
+ * apunta `userIdentities`. Devuelve null si no hay ninguno, y quien llame decide
250
+ * el fallback.
251
+ */
252
+ export async function threadForChannel(userId: string, channel: string): Promise<string | null> {
253
+ if (channel === "webchat") return (await mostRecentWebThread(userId))?.id ?? null
254
+
255
+ const identities = await col<{ channel_user_id?: string }>("userIdentities")
256
+ const identity = await identities.get(`${userId}:${channel}`)
257
+ const peerId = identity?.doc.channel_user_id
258
+ return peerId ? makeThreadId(userId, channel, peerId) : null
259
+ }
260
+
261
+ /**
262
+ * Registra el hilo anterior a la separación por canal —`thread_id = userId`, un
263
+ * único hilo compartido por todos los canales— como una conversación más de la
264
+ * web, para que su historial siga siendo accesible. No mueve ni reescribe ningún
265
+ * mensaje: la fila apunta al mismo prefijo de siempre.
266
+ */
267
+ export async function ensureLegacyThread(userId: string): Promise<boolean> {
268
+ const c = await threadsCol()
269
+ if (await c.get(userId)) return false
270
+
271
+ const conversations = await col<ConversationDoc>("conversations")
272
+ const sample = await conversations.scan({ prefix: `${userId}:`, limit: 1 })
273
+ if (sample.length === 0) return false
274
+
275
+ const all = await conversations.scan({ prefix: `${userId}:` })
276
+ const lastAt = all.reduce((max, e) => Math.max(max, e.doc.created_at), 0)
277
+
278
+ await c.put(userId, {
279
+ id: userId,
280
+ user_id: userId,
281
+ channel: "webchat",
282
+ peer_id: "legacy",
283
+ peer_kind: "direct",
284
+ title: "Conversación anterior",
285
+ archived: false,
286
+ created_at: all[0]?.doc.created_at ?? Date.now(),
287
+ last_message_at: lastAt || Date.now(),
288
+ message_count: all.length,
289
+ }, { expectedVersion: 0 }).catch(() => {})
290
+
291
+ log.info(`hilo legacy ${userId} registrado como conversación (${all.length} mensajes)`)
292
+ return true
293
+ }
@@ -40,16 +40,16 @@
40
40
  * - core (notify, report_progress, save_note)
41
41
  */
42
42
 
43
- import { col } from "../storage/hive"
44
- import type { ToolDoc } from "../storage/collections"
45
- import { logger } from "../utils/logger"
43
+ import { col } from "../storage/hive.ts"
44
+ import type { ToolDoc } from "../storage/collections.ts"
45
+ import { logger } from "../utils/logger.ts"
46
46
  import {
47
47
  searchCapabilities,
48
48
  applyRelativeCutoff,
49
49
  replaceCapabilityDocs,
50
50
  type CapabilityDoc,
51
- } from "./capability-search"
52
- import { isCalendarOperation } from "./routing-intent"
51
+ } from "./capability-search.ts"
52
+ import { isCalendarOperation } from "./routing-intent.ts"
53
53
 
54
54
  const log = logger.child("tool-selector")
55
55
 
@@ -200,7 +200,11 @@ export const CORE_TOOL_CATALOG: ToolDescriptor[] = [
200
200
  { name: "api_request", description: "Perform an authorized HTTP request against a REST endpoint and validate the response. Spanish keywords: llamar api, request rest, consumir endpoint, petición http, hacer get, hacer post", category: "api", abstractionLevel: "atomic" },
201
201
 
202
202
  // Artifacts
203
+ { name: "computer_use_task", description: "Operate the browser by looking at the screen: click by coordinates, type and navigate when no stable CSS selector exists (canvas, generated UIs, embedded viewers). Acts on Hive's own browser, never on the user screen. Spanish keywords: usar el navegador, hacer clic donde veas, operar una página, rellenar formulario, computer use, mirar la pantalla", category: "browser", abstractionLevel: "orchestration" },
203
204
  { name: "artifact_inspect", description: "Inspect a managed artifact's integrity and metadata without modifying it. Spanish keywords: inspeccionar artefacto, verificar archivo generado, metadatos artefacto, comprobar entrega", category: "web", abstractionLevel: "atomic" },
205
+ { name: "artifact_read", description: "Read a managed artifact's text content in slices, or search inside it — the way to open any artifact_ref a tool returned. Spanish keywords: leer artefacto, ver contenido del artefacto, abrir resultado grande, buscar dentro del artefacto", category: "web", abstractionLevel: "atomic" },
206
+ { name: "image_metadata", description: "Read a stored image's dimensions and format without loading it into context. Spanish keywords: medir imagen, dimensiones de la imagen, tamaño de la foto, formato de imagen", category: "images", abstractionLevel: "atomic" },
207
+ { name: "image_transform", description: "Resize, rotate or convert a stored image, returning a new artifact — the original is untouched. Spanish keywords: redimensionar imagen, cambiar tamaño, convertir a webp, comprimir imagen, rotar foto, achicar imagen", category: "images", abstractionLevel: "atomic" },
204
208
 
205
209
  // Office documents — read
206
210
  { name: "office_leer_pdf", description: "Read and extract text from a PDF document. Spanish keywords: leer pdf, extraer texto pdf, abrir pdf, contenido pdf", category: "office", abstractionLevel: "atomic" },
@@ -6,9 +6,9 @@
6
6
  * `agents.lastTraceAt` field used by the Curator's stale-worker detection.
7
7
  */
8
8
 
9
- import { logger } from "../utils/logger"
10
- import { col, nextId, updateDoc } from "../storage/hive"
11
- import type { TraceDoc, AgentDoc } from "../storage/collections"
9
+ import { logger } from "../utils/logger.ts"
10
+ import { col, nextId, updateDoc } from "../storage/hive.ts"
11
+ import type { TraceDoc, AgentDoc } from "../storage/collections.ts"
12
12
 
13
13
  const log = logger.child("tracer")
14
14
 
@@ -82,7 +82,7 @@ async function checkReflectorTrigger(): Promise<void> {
82
82
  _tracesSinceLastReflection = 0
83
83
 
84
84
  // Lazy import to avoid circular deps
85
- const { runReflector } = await import("./reflector")
85
+ const { runReflector } = await import("./reflector.ts")
86
86
  runReflector().catch((err) => {
87
87
  log.warn("[tracer] Reflector run failed:", err)
88
88
  })
@@ -98,7 +98,7 @@ export function recordLLMUsage(opts: {
98
98
  }): void {
99
99
  Promise.resolve().then(async () => {
100
100
  try {
101
- const { recordUsage } = await import("../storage/usage")
101
+ const { recordUsage } = await import("../storage/usage.ts")
102
102
  recordUsage({
103
103
  provider: opts.provider,
104
104
  model: opts.model,
@@ -35,11 +35,28 @@ export interface Agent {
35
35
  readonly name: string;
36
36
  readonly id: string;
37
37
  readonly config: AgentConfig;
38
- chat(message: string, opts?: { threadId?: string; channel?: string }): AsyncGenerator<AgentEvent>;
38
+ /**
39
+ * Con `stream: true` se emiten eventos `token` con los deltas del proveedor
40
+ * a medida que llegan, además del `text` con la respuesta completa del turno.
41
+ */
42
+ chat(
43
+ message: string,
44
+ opts?: { threadId?: string; channel?: string; stream?: boolean },
45
+ ): AsyncGenerator<AgentEvent>;
39
46
  run(task: string, opts?: { threadId?: string; channel?: string }): Promise<string>;
40
47
  }
41
48
 
42
49
  export type AgentEvent =
50
+ /**
51
+ * Un fragmento recién llegado del proveedor, para pintar la respuesta
52
+ * mientras se genera.
53
+ *
54
+ * Sólo aparece si se pide `stream: true`. Los proveedores ya emitían estos
55
+ * deltas —el mecanismo estaba implementado— pero ningún punto de entrada los
56
+ * pasaba, así que nunca llegaban a nadie: la respuesta aparecía de golpe al
57
+ * terminar el turno.
58
+ */
59
+ | { type: "token"; content: string }
43
60
  | { type: "text"; content: string }
44
61
  | { type: "tool_call"; name: string; args: Record<string, unknown> }
45
62
  | { type: "tool_result"; name: string; result: unknown }
@@ -92,7 +109,7 @@ export async function createAgent(config: AgentConfig): Promise<Agent> {
92
109
 
93
110
  const coreConfig = await loadConfig();
94
111
 
95
- // Browser automation (agent-browser) si está habilitado.
112
+ // Browser automation (Bun.WebView) si está habilitado.
96
113
  try {
97
114
  const { initializeBrowserService } = await import("../tools/web/browser-service.ts");
98
115
  const browserService = initializeBrowserService(coreConfig);
@@ -128,6 +145,40 @@ export async function createAgent(config: AgentConfig): Promise<Agent> {
128
145
  }
129
146
  }
130
147
 
148
+ // Las skills declaradas seguían el mismo camino que las tools —y hasta acá no
149
+ // lo seguían: `config.skills` estaba tipado y se descartaba en silencio, así
150
+ // que declarar una skill no hacía absolutamente nada. Una skill no necesita
151
+ // ejecutor (es instruccional: metadatos más el cuerpo que se le inyecta al
152
+ // agente), pero sí necesita su fila y su entrada en el índice, o el modelo
153
+ // nunca la descubre.
154
+ if (config.skills?.length) {
155
+ const { createSkill, getSkill, updateSkill } = await import("../services/skills.ts");
156
+
157
+ for (const skill of config.skills) {
158
+ // El cuerpo se arma con los pasos declarados: es lo que lee el agente.
159
+ const body = [
160
+ skill.description,
161
+ "",
162
+ ...skill.steps.map((s, i) => `${i + 1}. **${s.action}** — ${s.instruction}`),
163
+ ].join("\n");
164
+
165
+ const campos = {
166
+ name: skill.name,
167
+ description: skill.description,
168
+ category: skill.category,
169
+ body,
170
+ tools: skill.tools,
171
+ triggers: skill.triggers,
172
+ version: skill.version,
173
+ };
174
+
175
+ // Idempotente: declarar la misma skill dos veces la actualiza.
176
+ const existente = await getSkill(skill.name);
177
+ if (existente) await updateSkill(skill.name, campos);
178
+ else await createSkill({ id: skill.name, ...campos });
179
+ }
180
+ }
181
+
131
182
  // ─── Provider y modelo ────────────────────────────────────────────────────
132
183
  // El loop los lee de la fila del agente, así que hay que dejarlos escritos.
133
184
  const providerId = config.provider ?? "";
@@ -241,6 +292,17 @@ export async function createAgent(config: AgentConfig): Promise<Agent> {
241
292
  const threadId = opts?.threadId ?? crypto.randomUUID();
242
293
  let response = "";
243
294
 
295
+ // `onToken` es un callback y esto es un generador: los deltas se
296
+ // encolan y se drenan entre chunks. Sin buffer habría que elegir entre
297
+ // perder tokens o bloquear al proveedor mientras el consumidor lee.
298
+ const pendientes: string[] = [];
299
+ const onToken = opts?.stream ? (t: string) => { pendientes.push(t); } : undefined;
300
+ const drenar = function* () {
301
+ while (pendientes.length > 0) {
302
+ yield { type: "token" as const, content: pendientes.shift()! };
303
+ }
304
+ };
305
+
244
306
  for await (const chunk of runAgent({
245
307
  agentId,
246
308
  userMessage: message,
@@ -248,7 +310,9 @@ export async function createAgent(config: AgentConfig): Promise<Agent> {
248
310
  channel: opts?.channel ?? "cli",
249
311
  mcpManager,
250
312
  userId,
313
+ onToken,
251
314
  })) {
315
+ yield* drenar();
252
316
  for (const msg of chunk.agent?.messages ?? []) {
253
317
  if (typeof msg.content === "string" && msg.content) {
254
318
  response = msg.content;
@@ -268,6 +332,7 @@ export async function createAgent(config: AgentConfig): Promise<Agent> {
268
332
  }
269
333
  }
270
334
 
335
+ yield* drenar();
271
336
  yield { type: "done" as const, response };
272
337
  },
273
338
  async run(task, opts) {
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Artifacts — los archivos que produce un agente, fuera de la ventana de contexto.
3
+ *
4
+ * Cuando una tool devuelve algo grande —una imagen, un PDF, la salida enorme de
5
+ * un servidor MCP— serializarlo entero al prompt es lo peor que se puede hacer:
6
+ * se come el contexto y no aporta. En su lugar se guarda como artefacto y al
7
+ * modelo le llega una referencia (`artifact_ref`) que puede abrir con la tool
8
+ * `artifact_read` si de verdad necesita el contenido.
9
+ *
10
+ * `createArtifact` guarda; `inspectArtifact` da metadatos sin abrirlo;
11
+ * `readArtifactText` lo lee para consumo del propio proceso; `readArtifactBytes`
12
+ * devuelve los bytes crudos, que es lo que usa un canal para mandar la imagen.
13
+ */
14
+
15
+ export * from "./store.ts";