@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
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Hooks — engancharse al ciclo de vida sin bifurcar el SDK.
3
+ *
4
+ * `HooksConfigSchema` (`config/loader.ts`) declaraba 14 hooks y **ninguno se
5
+ * invocaba**: era un esquema sin implementación. Quien lo encontrara asumiría
6
+ * que funciona.
7
+ *
8
+ * Acá se implementan, con dos formas y una decisión de diseño detrás:
9
+ *
10
+ * - **Callbacks en proceso** (`registerHook`) — tipados, sin costo de arranque,
11
+ * y pueden **devolver una decisión**: `beforeToolCall` puede impedir que una
12
+ * tool se ejecute. Es el primitivo, porque quien consume el SDK ya está en el
13
+ * mismo proceso.
14
+ * - **Scripts externos** (`hooks.scripts` en la configuración) — para quien no
15
+ * escribe TypeScript. Se montan encima del primitivo: cada script declarado
16
+ * se registra como un callback que lo ejecuta. Cuestan un `Bun.spawn` por
17
+ * invocación, así que conviene reservarlos para lo que no ocurre en cada
18
+ * tool call.
19
+ *
20
+ * Sólo están los hooks que tienen un uso claro. Los otros nueve del esquema
21
+ * original se dejaron fuera a propósito: cada hook es una promesa que después
22
+ * hay que sostener, y uno que nadie usa es superficie que envejece mal.
23
+ */
24
+
25
+ import { logger } from "../utils/logger.ts";
26
+ import { loadConfig } from "../config/loader.ts";
27
+
28
+ const log = logger.child("hooks");
29
+
30
+ /** Lo que recibe un hook de tool. */
31
+ export interface ToolCallContext {
32
+ toolName: string;
33
+ args: Record<string, unknown>;
34
+ agentId?: string;
35
+ userId?: string;
36
+ threadId?: string;
37
+ }
38
+
39
+ export interface ToolResultContext extends ToolCallContext {
40
+ result: unknown;
41
+ ok: boolean;
42
+ durationMs: number;
43
+ }
44
+
45
+ export interface CompactionContext {
46
+ threadId: string;
47
+ messageCount: number;
48
+ totalTokens: number;
49
+ }
50
+
51
+ export interface SessionContext {
52
+ threadId: string;
53
+ userId?: string;
54
+ channel?: string;
55
+ }
56
+
57
+ /**
58
+ * Lo que puede devolver `beforeToolCall`.
59
+ *
60
+ * `void` o `undefined` = seguir adelante. Devolver `{ block }` impide la
61
+ * ejecución y el motivo le llega al modelo como resultado de la tool, para que
62
+ * sepa por qué no se hizo en vez de reintentar a ciegas.
63
+ */
64
+ export type BeforeToolCallResult = void | undefined | { block: true; reason: string };
65
+
66
+ export interface HookMap {
67
+ beforeToolCall: (ctx: ToolCallContext) => BeforeToolCallResult | Promise<BeforeToolCallResult>;
68
+ afterToolCall: (ctx: ToolResultContext) => void | Promise<void>;
69
+ beforeCompaction: (ctx: CompactionContext) => void | Promise<void>;
70
+ sessionStart: (ctx: SessionContext) => void | Promise<void>;
71
+ sessionEnd: (ctx: SessionContext) => void | Promise<void>;
72
+ }
73
+
74
+ export type HookName = keyof HookMap;
75
+
76
+ const registry: { [K in HookName]: Array<HookMap[K]> } = {
77
+ beforeToolCall: [],
78
+ afterToolCall: [],
79
+ beforeCompaction: [],
80
+ sessionStart: [],
81
+ sessionEnd: [],
82
+ };
83
+
84
+ /**
85
+ * Registra un hook. Devuelve la función para quitarlo.
86
+ *
87
+ * Se pueden registrar varios del mismo tipo: corren en orden de registro.
88
+ */
89
+ export function registerHook<K extends HookName>(name: K, fn: HookMap[K]): () => void {
90
+ registry[name].push(fn);
91
+ return () => {
92
+ const i = registry[name].indexOf(fn);
93
+ if (i >= 0) registry[name].splice(i, 1);
94
+ };
95
+ }
96
+
97
+ /** Quita todos los hooks. Pensado para tests. */
98
+ export function clearHooks(name?: HookName): void {
99
+ if (name) registry[name] = [];
100
+ else for (const k of Object.keys(registry) as HookName[]) registry[k] = [];
101
+ }
102
+
103
+ export function hasHooks(name: HookName): boolean {
104
+ return registry[name].length > 0;
105
+ }
106
+
107
+ /**
108
+ * Corre los `beforeToolCall` y devuelve el motivo del bloqueo, si alguno objeta.
109
+ *
110
+ * El primero que bloquea gana: no tiene sentido seguir preguntando cuando ya
111
+ * hay una negativa. Un hook que lanza **no** bloquea la ejecución —un error en
112
+ * el observador no debería impedir el trabajo— pero se registra.
113
+ */
114
+ export async function runBeforeToolCall(ctx: ToolCallContext): Promise<string | null> {
115
+ for (const fn of registry.beforeToolCall) {
116
+ try {
117
+ const r = await fn(ctx);
118
+ if (r && typeof r === "object" && r.block) return r.reason;
119
+ } catch (err) {
120
+ log.warn(`beforeToolCall falló para ${ctx.toolName}: ${(err as Error).message}`);
121
+ }
122
+ }
123
+ return null;
124
+ }
125
+
126
+ /** Corre los hooks de observación. Nunca lanza: son observadores. */
127
+ async function runObservers<K extends "afterToolCall" | "beforeCompaction" | "sessionStart" | "sessionEnd">(
128
+ name: K,
129
+ ctx: Parameters<HookMap[K]>[0],
130
+ ): Promise<void> {
131
+ for (const fn of registry[name]) {
132
+ try {
133
+ await (fn as (c: unknown) => unknown)(ctx);
134
+ } catch (err) {
135
+ log.warn(`${name} falló: ${(err as Error).message}`);
136
+ }
137
+ }
138
+ }
139
+
140
+ export const runAfterToolCall = (ctx: ToolResultContext) => runObservers("afterToolCall", ctx);
141
+ export const runBeforeCompaction = (ctx: CompactionContext) => runObservers("beforeCompaction", ctx);
142
+ export const runSessionStart = (ctx: SessionContext) => runObservers("sessionStart", ctx);
143
+ export const runSessionEnd = (ctx: SessionContext) => runObservers("sessionEnd", ctx);
144
+
145
+ // ─── Scripts declarados en la configuración ──────────────────────────────────
146
+
147
+ /** Nombres del esquema de config → hooks implementados. */
148
+ const SCRIPT_MAP: Record<string, HookName> = {
149
+ before_tool_call: "beforeToolCall",
150
+ after_tool_call: "afterToolCall",
151
+ before_compaction: "beforeCompaction",
152
+ session_start: "sessionStart",
153
+ session_end: "sessionEnd",
154
+ };
155
+
156
+ /**
157
+ * Ejecuta un script pasándole el contexto como JSON por stdin.
158
+ *
159
+ * Convención para `before_tool_call`: **salir con código distinto de 0 bloquea**
160
+ * la ejecución, y lo que el script escriba en stdout es el motivo que se le
161
+ * cuenta al modelo. Es el equivalente en procesos a devolver `{ block }`.
162
+ */
163
+ async function runScript(path: string, ctx: unknown): Promise<{ blocked: boolean; reason: string }> {
164
+ const proc = Bun.spawn(["bun", path], { stdin: "pipe", stdout: "pipe", stderr: "pipe" });
165
+ proc.stdin.write(JSON.stringify(ctx));
166
+ proc.stdin.end();
167
+
168
+ const [salida, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
169
+ return { blocked: code !== 0, reason: salida.trim() || `el hook ${path} salió con código ${code}` };
170
+ }
171
+
172
+ let scriptsCargados = false;
173
+
174
+ /**
175
+ * Registra los scripts declarados en `hooks.scripts`.
176
+ *
177
+ * Es opt-in y explícito: ejecutar procesos externos no es algo que deba pasar
178
+ * por el solo hecho de importar el SDK. Idempotente.
179
+ */
180
+ export function loadConfiguredHookScripts(): number {
181
+ if (scriptsCargados) return 0;
182
+ const scripts = loadConfig().hooks?.scripts;
183
+ if (!scripts) return 0;
184
+
185
+ let n = 0;
186
+ for (const [clave, hook] of Object.entries(SCRIPT_MAP)) {
187
+ const path = (scripts as Record<string, string | undefined>)[clave];
188
+ if (!path) continue;
189
+
190
+ if (hook === "beforeToolCall") {
191
+ registerHook("beforeToolCall", async (ctx) => {
192
+ const r = await runScript(path, ctx);
193
+ return r.blocked ? { block: true as const, reason: r.reason } : undefined;
194
+ });
195
+ } else {
196
+ registerHook(hook as "afterToolCall", async (ctx) => { await runScript(path, ctx); });
197
+ }
198
+ n++;
199
+ log.info(`hook ${clave} → ${path}`);
200
+ }
201
+ scriptsCargados = true;
202
+ return n;
203
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Imágenes — redimensionar, convertir y medir, sin dependencias nativas.
3
+ *
4
+ * Usa `Bun.Image` (Bun 1.4), que es sharp integrado en el runtime: no hay que
5
+ * instalar nada ni compilar bindings. El SDK ya exige Bun ≥ 1.4, así que esto
6
+ * no agrega ningún requisito.
7
+ *
8
+ * Sirve para dos cosas distintas y conviene no confundirlas:
9
+ *
10
+ * 1. **Tools activables** para el agente (`tools/images/`) — recortar, convertir
11
+ * de formato, leer dimensiones. Es una capacidad de producto.
12
+ * 2. **Normalizar lo que entra** — una foto de 4 MB que un usuario manda por
13
+ * WhatsApp no tiene por qué viajar entera al modelo. Redimensionarla antes
14
+ * cuesta una fracción de los tokens y no cambia lo que el modelo puede ver.
15
+ *
16
+ * Nota sobre `Bun.Image`: las transformaciones son **diferidas**. `metadata()`
17
+ * sobre una cadena sin materializar devuelve las dimensiones del origen, no las
18
+ * del resultado — hay que pedir los bytes y releerlos. `measureImage()` lo hace
19
+ * por vos cuando hace falta.
20
+ */
21
+
22
+ import { logger } from "../utils/logger.ts";
23
+
24
+ const log = logger.child("images");
25
+
26
+ /** Los que `Bun.Image` sabe escribir. */
27
+ export type ImageFormat = "jpeg" | "png" | "webp" | "avif" | "heic";
28
+
29
+ export interface ImageMetadata {
30
+ width: number;
31
+ height: number;
32
+ format: string;
33
+ }
34
+
35
+ export interface TransformOptions {
36
+ /** Ancho máximo; la altura se ajusta manteniendo la proporción si no se da. */
37
+ width?: number;
38
+ height?: number;
39
+ format?: ImageFormat;
40
+ /** 1–100. Sólo lo respetan los formatos con pérdida. */
41
+ quality?: number;
42
+ /** Grados: 90, 180, 270. */
43
+ rotate?: number;
44
+ flip?: boolean;
45
+ flop?: boolean;
46
+ }
47
+
48
+ function bunImage(): any {
49
+ const I = (Bun as any).Image;
50
+ if (!I) {
51
+ throw new Error("Bun.Image no está disponible: se necesita Bun >= 1.4");
52
+ }
53
+ return I;
54
+ }
55
+
56
+ function toBytes(input: Uint8Array | ArrayBuffer | Buffer | string): Uint8Array {
57
+ if (typeof input === "string") return Uint8Array.from(Buffer.from(input, "base64"));
58
+ if (input instanceof Uint8Array) return input;
59
+ return new Uint8Array(input as ArrayBuffer);
60
+ }
61
+
62
+ /** Dimensiones y formato sin decodificar la imagen entera. */
63
+ export async function measureImage(input: Uint8Array | ArrayBuffer | Buffer | string): Promise<ImageMetadata> {
64
+ const meta = await new (bunImage())(toBytes(input)).metadata();
65
+ return { width: meta.width, height: meta.height, format: meta.format };
66
+ }
67
+
68
+ /**
69
+ * Aplica las transformaciones y devuelve los bytes resultantes.
70
+ *
71
+ * Sin `format` conserva el de origen. Sin nada que hacer devuelve la entrada tal
72
+ * cual, para no recomprimir de gusto.
73
+ */
74
+ export async function transformImage(
75
+ input: Uint8Array | ArrayBuffer | Buffer | string,
76
+ opts: TransformOptions,
77
+ ): Promise<{ bytes: Uint8Array; metadata: ImageMetadata }> {
78
+ const bytes = toBytes(input);
79
+ const hayQueHacer = opts.width || opts.height || opts.format || opts.rotate || opts.flip || opts.flop;
80
+ if (!hayQueHacer) return { bytes, metadata: await measureImage(bytes) };
81
+
82
+ let img = new (bunImage())(bytes);
83
+
84
+ if (opts.width || opts.height) img = img.resize(opts.width, opts.height);
85
+ if (opts.rotate) img = img.rotate(opts.rotate);
86
+ if (opts.flip) img = img.flip();
87
+ if (opts.flop) img = img.flop();
88
+
89
+ const formato = opts.format ?? ((await measureImage(bytes)).format as ImageFormat);
90
+ const args = opts.quality !== undefined ? [{ quality: opts.quality }] : [];
91
+ img = typeof img[formato] === "function" ? img[formato](...args) : img;
92
+
93
+ const salida: Uint8Array = await img.bytes();
94
+ // Se releen los bytes porque `metadata()` sobre la cadena diferida informa el
95
+ // origen, no el resultado.
96
+ return { bytes: salida, metadata: await measureImage(salida) };
97
+ }
98
+
99
+ export interface NormalizeOptions {
100
+ /** Lado mayor permitido. Por encima se reduce manteniendo la proporción. */
101
+ maxDimension?: number;
102
+ /** Formato de salida; `webp` pesa mucho menos que un JPEG equivalente. */
103
+ format?: ImageFormat;
104
+ quality?: number;
105
+ }
106
+
107
+ /** Por defecto: 1024 px de lado mayor y webp al 80 — el punto donde un modelo de visión deja de ganar detalle. */
108
+ const NORMALIZE_DEFAULTS: Required<NormalizeOptions> = {
109
+ maxDimension: 1024,
110
+ format: "webp",
111
+ quality: 80,
112
+ };
113
+
114
+ export interface NormalizeResult {
115
+ bytes: Uint8Array;
116
+ metadata: ImageMetadata;
117
+ /** Bytes originales, para saber cuánto se ahorró. */
118
+ originalBytes: number;
119
+ /** true si hubo que tocarla; false si ya era chica. */
120
+ changed: boolean;
121
+ }
122
+
123
+ /**
124
+ * Deja una imagen entrante en un tamaño razonable para mandársela a un modelo.
125
+ *
126
+ * Una foto de teléfono son varios megabytes y unos cuantos miles de tokens; a
127
+ * 1024 px el modelo ve lo mismo por una fracción del costo. Si ya está por
128
+ * debajo del límite no se toca — recomprimir una imagen chica sólo la empeora.
129
+ */
130
+ export async function normalizeForModel(
131
+ input: Uint8Array | ArrayBuffer | Buffer | string,
132
+ opts: NormalizeOptions = {},
133
+ ): Promise<NormalizeResult> {
134
+ const cfg = { ...NORMALIZE_DEFAULTS, ...opts };
135
+ const bytes = toBytes(input);
136
+ const meta = await measureImage(bytes);
137
+ const mayor = Math.max(meta.width, meta.height);
138
+
139
+ if (mayor <= cfg.maxDimension && meta.format === cfg.format) {
140
+ return { bytes, metadata: meta, originalBytes: bytes.length, changed: false };
141
+ }
142
+
143
+ const escala = mayor > cfg.maxDimension ? cfg.maxDimension / mayor : 1;
144
+ const { bytes: salida, metadata } = await transformImage(bytes, {
145
+ width: Math.round(meta.width * escala),
146
+ height: Math.round(meta.height * escala),
147
+ format: cfg.format,
148
+ quality: cfg.quality,
149
+ });
150
+
151
+ log.info(
152
+ `imagen normalizada: ${meta.width}x${meta.height} ${meta.format} (${bytes.length}b) → ` +
153
+ `${metadata.width}x${metadata.height} ${metadata.format} (${salida.length}b)`,
154
+ );
155
+ return { bytes: salida, metadata, originalBytes: bytes.length, changed: true };
156
+ }
157
+
158
+ /** true si este runtime puede procesar imágenes. */
159
+ export function imagesSupported(): boolean {
160
+ return typeof (Bun as any).Image === "function";
161
+ }
@@ -22,6 +22,7 @@ export type { Skill, SkillStep, OutputFormat, SkillsConfig } from "./skills/inde
22
22
  export { runAgent, runAgentIsolated, AgentLoop, getAgentLoop, buildAgentLoop, rebuildAgentLoop } from "./agent/agent-loop.ts";
23
23
  export type { AgentLoopOptions, StepEvent, StreamChunk } from "./agent/agent-loop.ts";
24
24
  export type { Provider } from "./agent/providers/index.ts";
25
+ export { AgentRunner, createAgentRunner } from "./agent/providers/index.ts";
25
26
 
26
27
  // El cliente LLM es parte de la superficie pública: hasta 0.1.5 sólo se exportaba
27
28
  // el wrapper `AgentRunner`, así que no había forma de llamar a un provider ni de
@@ -1,12 +1,12 @@
1
1
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
2
  import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
3
- import type { MCPConfig, MCPServerConfig } from "./config";
4
- import { logger, type LogHandler } from "./logger";
3
+ import type { MCPConfig, MCPServerConfig } from "./config.ts";
4
+ import { logger, type LogHandler } from "./logger.ts";
5
5
  import * as path from "node:path";
6
6
  import {
7
7
  createTransport,
8
8
  type TransportType,
9
- } from "./transports/index";
9
+ } from "./transports/index.ts";
10
10
 
11
11
  export interface MCPTool {
12
12
  name: string;
@@ -8,11 +8,11 @@
8
8
  * - MCP tools are loaded at runtime from connected servers (not stored in DB)
9
9
  */
10
10
 
11
- import { col, updateDoc } from "../storage/hive";
12
- import type { McpServerDoc } from "../storage/collections";
13
- import { logger } from "../utils/logger";
14
- import { loadMcpHeaders } from "../storage/crypto";
15
- import { syncMCPToolsToDB, syncMCPToolsToIndex, clearMCPToolsFromDB } from "./tool-sync";
11
+ import { col, updateDoc } from "../storage/hive.ts";
12
+ import type { McpServerDoc } from "../storage/collections.ts";
13
+ import { logger } from "../utils/logger.ts";
14
+ import { loadMcpHeaders } from "../storage/crypto.ts";
15
+ import { syncMCPToolsToDB, syncMCPToolsToIndex, clearMCPToolsFromDB } from "./tool-sync.ts";
16
16
  import type { MCPClientManager } from "./MCPClient.ts";
17
17
 
18
18
  const log = logger.child("mcp:hot-reload");
@@ -13,15 +13,15 @@
13
13
  * 3. Offline visibility of what MCP tools were available
14
14
  */
15
15
 
16
- import { col } from "../storage/hive"
17
- import type { McpToolDoc } from "../storage/collections"
18
- import { logger } from "../utils/logger"
19
- import { mcpToolFullName } from "../agent/tool-selector"
16
+ import { col } from "../storage/hive.ts"
17
+ import type { McpToolDoc } from "../storage/collections.ts"
18
+ import { logger } from "../utils/logger.ts"
19
+ import { mcpToolFullName } from "../agent/tool-selector.ts"
20
20
  import {
21
21
  replaceCapabilityDocs,
22
22
  deleteCapabilitiesByServer,
23
23
  type CapabilityDoc,
24
- } from "../agent/capability-search"
24
+ } from "../agent/capability-search.ts"
25
25
 
26
26
  const log = logger.child("mcp:tool-sync")
27
27
 
@@ -4,8 +4,8 @@ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
4
4
  // CORRECCIÓN 1 — quitar extensión .ts de los imports
5
5
  // Bun resuelve los módulos sin extensión correctamente
6
6
  // Con .ts puede fallar en algunos contextos de build/bundle
7
- import { SSETransport, type SSETransportConfig } from "./sse";
8
- import { WebSocketTransport, type WebSocketTransportConfig } from "./websocket";
7
+ import { SSETransport, type SSETransportConfig } from "./sse.ts";
8
+ import { WebSocketTransport, type WebSocketTransportConfig } from "./websocket.ts";
9
9
 
10
10
  export { SSETransport, type SSETransportConfig };
11
11
  export { WebSocketTransport, type WebSocketTransportConfig };
@@ -1,5 +1,5 @@
1
1
  import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
2
- import { logger } from "../logger";
2
+ import { logger } from "../logger.ts";
3
3
 
4
4
  export interface SSETransportConfig {
5
5
  url: string; // URL base
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Models — el catálogo de proveedores y modelos con el que arranca una colmena.
3
+ *
4
+ * El "seed de modelos" ya existía, pero repartido en `storage/`: los datos en
5
+ * `seed.ts`, las claves en `model-id.ts`, el precio en `usage.ts`. Quien quería
6
+ * consumirlo tenía que saber que un catálogo de modelos vive bajo "storage".
7
+ * Esto le da nombre y un punto de entrada; no mueve la implementación ni
8
+ * duplica nada.
9
+ *
10
+ * `SEED_DATA` es la fuente de verdad del catálogo (18 proveedores, ~110 modelos)
11
+ * y `seedAllData()` la aplica de forma idempotente en cada arranque — la llama
12
+ * `ensureHiveDb()`, así que normalmente no hace falta invocarla a mano.
13
+ *
14
+ * Nota de precios: cada modelo LLM declara `inputPer1M`/`outputPer1M`. Un modelo
15
+ * sin ellos no es "gratis", es un modelo cuyo costo no se puede calcular.
16
+ */
17
+
18
+ // ─── Catálogo ────────────────────────────────────────────────────────────────
19
+ export { SEED_DATA, seedAllData, seedToolsAndSkills } from "../storage/seed.ts";
20
+ export type { SeedData } from "../storage/seed.ts";
21
+ export { activateElement, deactivateElement, getAllElements, getActiveElements } from "../storage/seed.ts";
22
+
23
+ // ─── Identidad de modelo ─────────────────────────────────────────────────────
24
+ /**
25
+ * Los revendedores (openrouter, nvidia, groq, opencode-go, modelscope) sirven
26
+ * modelos de terceros bajo su propio endpoint, así que el id del wire colisiona
27
+ * entre proveedores. `catalogModelKey` prefija; `wireModelId` deshace el prefijo
28
+ * antes de salir a la API.
29
+ */
30
+ export { catalogModelKey, wireModelId, isResellerProvider } from "../storage/model-id.ts";
31
+
32
+ // ─── Costos ──────────────────────────────────────────────────────────────────
33
+ export { calculateCost, invalidateModelPricingCache } from "../storage/usage.ts";
34
+
35
+ // ─── Shapes ──────────────────────────────────────────────────────────────────
36
+ export type { ModelDoc, ProviderDoc } from "../storage/collections.ts";
@@ -1,2 +1,2 @@
1
- export type { ContentPart, ImageInput, DocumentInput, VisionConfig, MultimodalMessageType } from "./types"
2
- export { multimodalService } from "./vision-service"
1
+ export type { ContentPart, ImageInput, DocumentInput, VisionConfig, MultimodalMessageType } from "./types.ts"
2
+ export { multimodalService } from "./vision-service.ts"
@@ -1,9 +1,9 @@
1
- import { col } from "../storage/hive"
2
- import type { ChannelDoc, ModelDoc, ProviderDoc } from "../storage/collections"
3
- import { loadProviderApiKey } from "../storage/crypto"
4
- import { logger } from "../utils/logger"
5
- import type { ImageInput, DocumentInput, VisionConfig } from "./types"
6
- import type { ContentPart } from "./types"
1
+ import { col } from "../storage/hive.ts"
2
+ import type { ChannelDoc, ModelDoc, ProviderDoc } from "../storage/collections.ts"
3
+ import { loadProviderApiKey } from "../storage/crypto.ts"
4
+ import { logger } from "../utils/logger.ts"
5
+ import type { ImageInput, DocumentInput, VisionConfig } from "./types.ts"
6
+ import type { ContentPart } from "./types.ts"
7
7
 
8
8
  const log = logger.child("multimodal")
9
9
 
@@ -35,6 +35,19 @@ class MultimodalService {
35
35
  }
36
36
  }
37
37
 
38
+ /**
39
+ * Convierte una imagen entrante en las partes que ve el modelo.
40
+ *
41
+ * Antes de mandarla la **normaliza**: una foto de teléfono son varios
42
+ * megabytes y unos cuantos miles de tokens, y a 1024 px el modelo ve lo mismo
43
+ * por una fracción del costo. Esa imagen no viaja una sola vez — queda en el
44
+ * historial y se reenvía en cada turno siguiente, así que el ahorro se
45
+ * multiplica por la longitud de la conversación.
46
+ *
47
+ * Es best-effort: si el runtime no puede procesarla (Bun < 1.4) o la imagen
48
+ * está corrupta, se manda tal cual. Perder la imagen sería peor que mandarla
49
+ * grande.
50
+ */
38
51
  async processImage(image: ImageInput, visionModelId?: string): Promise<ContentPart[]> {
39
52
  const parts: ContentPart[] = []
40
53
 
@@ -43,25 +56,44 @@ class MultimodalService {
43
56
  }
44
57
 
45
58
  if (image.type === "url") {
59
+ // Una URL no ocupa contexto: la descarga la hace el proveedor.
46
60
  parts.push({ type: "image_url", image_url: { url: image.data as string } })
47
- } else if (image.type === "base64") {
48
- parts.push({
49
- type: "image_base64",
50
- base64: image.data as string,
51
- mimeType: image.mimeType || "image/jpeg",
52
- })
53
- } else if (image.type === "buffer") {
54
- const base64 = Buffer.from(image.data as Buffer).toString("base64")
55
- parts.push({
56
- type: "image_base64",
57
- base64,
58
- mimeType: image.mimeType || "image/jpeg",
59
- })
61
+ return parts
60
62
  }
61
63
 
64
+ const crudo = image.type === "base64"
65
+ ? (image.data as string)
66
+ : Buffer.from(image.data as Buffer).toString("base64")
67
+
68
+ const { base64, mimeType } = await this.normalizeIncoming(crudo, image.mimeType)
69
+ parts.push({ type: "image_base64", base64, mimeType })
70
+
62
71
  return parts
63
72
  }
64
73
 
74
+ /** Achica la imagen para el modelo; ante cualquier problema devuelve la original. */
75
+ private async normalizeIncoming(
76
+ base64: string,
77
+ mimeType?: string,
78
+ ): Promise<{ base64: string; mimeType: string }> {
79
+ const original = { base64, mimeType: mimeType || "image/jpeg" }
80
+ try {
81
+ const { imagesSupported, normalizeForModel } = await import("../images/index.ts")
82
+ if (!imagesSupported()) return original
83
+
84
+ const r = await normalizeForModel(base64)
85
+ if (!r.changed) return original
86
+
87
+ return {
88
+ base64: Buffer.from(r.bytes).toString("base64"),
89
+ mimeType: `image/${r.metadata.format}`,
90
+ }
91
+ } catch (err) {
92
+ log.warn(`no pude normalizar la imagen entrante, va sin achicar: ${(err as Error).message}`)
93
+ return original
94
+ }
95
+ }
96
+
65
97
  async ocrImage(image: ImageInput, providerId?: string): Promise<string> {
66
98
  const resolved = providerId || "openai"
67
99
 
@@ -30,7 +30,10 @@ export class PluginLoader {
30
30
  private commands: Map<string, CLICommand> = new Map();
31
31
  private log = logger.child("plugins");
32
32
 
33
- constructor(private options: PluginLoaderOptions) {
33
+ private options: PluginLoaderOptions;
34
+
35
+ constructor(options: PluginLoaderOptions) {
36
+ this.options = options;
34
37
  if (!existsSync(options.pluginDir)) {
35
38
  mkdirSync(options.pluginDir, { recursive: true });
36
39
  }
@@ -39,10 +39,16 @@ export class CircuitBreaker {
39
39
  private totalFailures = 0;
40
40
  private totalSuccesses = 0;
41
41
 
42
+ private readonly name: string;
43
+ private readonly options: CircuitBreakerOptions;
44
+
42
45
  constructor(
43
- private readonly name: string,
44
- private readonly options: CircuitBreakerOptions = defaultOptions
45
- ) { }
46
+ name: string,
47
+ options: CircuitBreakerOptions = defaultOptions
48
+ ) {
49
+ this.name = name;
50
+ this.options = options;
51
+ }
46
52
 
47
53
  async execute<T>(fn: () => Promise<T>): Promise<T> {
48
54
  if (this.state === "open") {
@@ -175,12 +181,17 @@ export class CircuitBreaker {
175
181
  }
176
182
 
177
183
  export class CircuitBreakerOpenError extends Error {
184
+ public readonly circuitName: string;
185
+ public readonly retryAfterMs: number;
186
+
178
187
  constructor(
179
188
  message: string,
180
- public readonly circuitName: string,
181
- public readonly retryAfterMs: number
189
+ circuitName: string,
190
+ retryAfterMs: number
182
191
  ) {
183
192
  super(message);
193
+ this.circuitName = circuitName;
194
+ this.retryAfterMs = retryAfterMs;
184
195
  this.name = "CircuitBreakerOpenError";
185
196
  }
186
197
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Resilience — reintentos y circuit breakers.
3
+ *
4
+ * `withRetry` aplica backoff exponencial con jitter y respeta `Retry-After`
5
+ * cuando el proveedor lo manda; `isRetryableError` decide qué merece otro
6
+ * intento (429/5xx/timeout/red) y qué no (un 400 no mejora reintentando).
7
+ *
8
+ * El `CircuitBreaker` corta las llamadas a un servicio que ya viene fallando,
9
+ * en vez de seguir gastando intentos contra algo caído.
10
+ */
11
+
12
+ export * from "./retry.ts";
13
+ export * from "./circuit-breaker.ts";
@@ -5,7 +5,7 @@
5
5
  * only decides whether a single call should be retried and how long to wait.
6
6
  */
7
7
 
8
- import { logger } from "../utils/logger";
8
+ import { logger } from "../utils/logger.ts";
9
9
 
10
10
  const log = logger.child("retry");
11
11