@johpaz/hive-sdk 0.1.6 → 0.2.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 (185) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/README.md +11 -1
  3. package/package.json +18 -2
  4. package/packages/core/src/agent/acceptance-checks.ts +9 -9
  5. package/packages/core/src/agent/agent-catalog.ts +3 -3
  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 +10 -9
  10. package/packages/core/src/agent/context-compiler.ts +58 -34
  11. package/packages/core/src/agent/conversation-store.ts +31 -7
  12. package/packages/core/src/agent/curator.ts +4 -4
  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 +1 -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 +9 -5
  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 +4 -4
  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 +4 -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 +6 -6
  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 +250 -0
  49. package/packages/core/src/agent/tool-selector.ts +7 -5
  50. package/packages/core/src/agent/tracer.ts +5 -5
  51. package/packages/core/src/api/createAgent.ts +1 -1
  52. package/packages/core/src/artifacts/store.ts +84 -3
  53. package/packages/core/src/canvas/emitter.ts +2 -2
  54. package/packages/core/src/channels/telegram.ts +1 -1
  55. package/packages/core/src/channels/webchat.ts +1 -1
  56. package/packages/core/src/config/loader.ts +14 -5
  57. package/packages/core/src/events/agent-bus.ts +3 -3
  58. package/packages/core/src/events/channel-narration.ts +3 -3
  59. package/packages/core/src/events/event-bus.ts +1 -1
  60. package/packages/core/src/events/narration.ts +3 -3
  61. package/packages/core/src/gateway/delegation-groups.ts +4 -4
  62. package/packages/core/src/gateway/durable-queue.ts +5 -5
  63. package/packages/core/src/gateway/job-store.ts +5 -5
  64. package/packages/core/src/gateway/notification-inbox.ts +2 -2
  65. package/packages/core/src/gateway/server.ts +2 -2
  66. package/packages/core/src/mcp/MCPClient.ts +3 -3
  67. package/packages/core/src/mcp/hot-reload.ts +5 -5
  68. package/packages/core/src/mcp/tool-sync.ts +5 -5
  69. package/packages/core/src/mcp/transports/index.ts +2 -2
  70. package/packages/core/src/mcp/transports/sse.ts +1 -1
  71. package/packages/core/src/models/index.ts +36 -0
  72. package/packages/core/src/multimodal/index.ts +2 -2
  73. package/packages/core/src/multimodal/vision-service.ts +6 -6
  74. package/packages/core/src/plugins/loader.ts +4 -1
  75. package/packages/core/src/resilience/circuit-breaker.ts +16 -5
  76. package/packages/core/src/resilience/retry.ts +1 -1
  77. package/packages/core/src/scheduler/CronScheduler.ts +6 -6
  78. package/packages/core/src/scheduler/integration.ts +9 -9
  79. package/packages/core/src/sessions/index.ts +266 -0
  80. package/packages/core/src/storage/bootstrap.ts +34 -8
  81. package/packages/core/src/storage/causal-events.ts +1 -1
  82. package/packages/core/src/storage/collections.ts +32 -1
  83. package/packages/core/src/storage/crypto.ts +16 -2
  84. package/packages/core/src/storage/hive.ts +1 -1
  85. package/packages/core/src/storage/hivedb.ts +10 -1
  86. package/packages/core/src/storage/onboarding.ts +6 -6
  87. package/packages/core/src/storage/reconcile.ts +5 -5
  88. package/packages/core/src/storage/seed.ts +103 -13
  89. package/packages/core/src/storage/usage.ts +3 -3
  90. package/packages/core/src/swarm/AgentExecutor.ts +2 -2
  91. package/packages/core/src/swarm/Coordinator.ts +8 -8
  92. package/packages/core/src/swarm/EventBridge.ts +2 -2
  93. package/packages/core/src/swarm/RoleSwarm.ts +234 -0
  94. package/packages/core/src/swarm/TaskGraph.ts +2 -2
  95. package/packages/core/src/swarm/index.ts +7 -0
  96. package/packages/core/src/swarm/presets/HiveLearnPreset.ts +2 -2
  97. package/packages/core/src/swarm/presets/ResearchPreset.ts +2 -2
  98. package/packages/core/src/swarm/strategies/ParallelStrategy.ts +1 -1
  99. package/packages/core/src/swarm/strategies/PriorityStrategy.ts +3 -3
  100. package/packages/core/src/tools/ToolExecutor.ts +7 -3
  101. package/packages/core/src/tools/core/index.ts +2 -2
  102. package/packages/core/src/tools/cron/index.ts +4 -4
  103. package/packages/core/src/tools/web/artifact-inspect.ts +2 -2
  104. package/packages/core/src/tools/web/artifact-read.ts +162 -0
  105. package/packages/core/src/tools/web/browser-backend.ts +141 -44
  106. package/packages/core/src/tools/web/browser-click.ts +2 -2
  107. package/packages/core/src/tools/web/browser-extract.ts +2 -2
  108. package/packages/core/src/tools/web/browser-navigate.ts +2 -2
  109. package/packages/core/src/tools/web/browser-screenshot.ts +12 -5
  110. package/packages/core/src/tools/web/browser-script.ts +2 -2
  111. package/packages/core/src/tools/web/browser-service.ts +63 -384
  112. package/packages/core/src/tools/web/browser-session.ts +125 -0
  113. package/packages/core/src/tools/web/browser-type.ts +2 -2
  114. package/packages/core/src/tools/web/browser-wait.ts +2 -2
  115. package/packages/core/src/tools/web/computer-use.ts +553 -0
  116. package/packages/core/src/tools/web/index.ts +8 -1
  117. package/packages/core/src/tools/web/webview-backend.ts +460 -21
  118. package/packages/core/src/utils/index.ts +1 -0
  119. package/packages/core/src/utils/logger.ts +12 -4
  120. package/packages/core/src/utils/redact-binary.ts +17 -0
  121. package/packages/core/src/utils/toon.ts +1 -1
  122. package/packages/core/src/voice/index.ts +6 -6
  123. package/bun.lock +0 -859
  124. package/bunfig.toml +0 -9
  125. package/docs/API-AGENTS.md +0 -367
  126. package/docs/API-CONTEXT-COMPILER.md +0 -249
  127. package/docs/API-DAG-SCHEDULER.md +0 -273
  128. package/docs/API-TOOLS-SKILLS-CHANNELS.md +0 -446
  129. package/docs/API-WORKERS-EVENTS.md +0 -299
  130. package/docs/HIVE-HARNESS.md +0 -113
  131. package/docs/INDEX.md +0 -190
  132. package/docs/TEMPLATE-HIVE-APP.md +0 -360
  133. package/packages/cli/package.json +0 -17
  134. package/packages/cli/src/commands/create-app.test.ts +0 -180
  135. package/packages/core/package.json +0 -70
  136. package/packages/core/src/api/createAgent.test.ts +0 -160
  137. package/packages/core/src/canvas/canvas.test.ts +0 -36
  138. package/packages/core/src/channels/channels.test.ts +0 -18
  139. package/packages/core/src/ethics/EthicsGuard.test.ts +0 -108
  140. package/packages/core/src/gateway/gateway.test.ts +0 -38
  141. package/packages/core/src/memory/Scratchpad.test.ts +0 -68
  142. package/packages/core/src/scheduler/scheduler.test.ts +0 -15
  143. package/packages/core/src/skills/skills.test.ts +0 -62
  144. package/packages/core/src/swarm/swarm.test.ts +0 -24
  145. package/packages/core/src/tool-runtime/tool-runtime.test.ts +0 -99
  146. package/packages/core/src/tools/ToolRegistry.test.ts +0 -98
  147. package/packages/core/src/tools/api/api-request.test.ts +0 -164
  148. package/packages/core/src/tools/web/browser-service.test.ts +0 -83
  149. package/packages/core/src/workers/workers.test.ts +0 -41
  150. package/scripts/bump-version.ts +0 -248
  151. package/scripts/generate-skill-bundle.ts +0 -108
  152. package/test/acceptance-checks.test.ts +0 -403
  153. package/test/agent-loop-terminal-synthesis.test.ts +0 -32
  154. package/test/browser-backend.test.ts +0 -308
  155. package/test/catalog-agents-stay-enabled.test.ts +0 -117
  156. package/test/causal-events.test.ts +0 -117
  157. package/test/compaction.test.ts +0 -105
  158. package/test/context-compiler.test.ts +0 -269
  159. package/test/curator.test.ts +0 -130
  160. package/test/durable-queue.test.ts +0 -114
  161. package/test/harness-barrel.test.ts +0 -64
  162. package/test/hive-helpers.test.ts +0 -130
  163. package/test/hivedb-search.test.ts +0 -189
  164. package/test/internal-turns.test.ts +0 -166
  165. package/test/job-idempotency.test.ts +0 -68
  166. package/test/job-retry-backoff.test.ts +0 -184
  167. package/test/job-store.test.ts +0 -381
  168. package/test/llm-retry.test.ts +0 -97
  169. package/test/memory-perf.test.ts +0 -774
  170. package/test/minimal-loadout.test.ts +0 -78
  171. package/test/model-catalog.test.ts +0 -105
  172. package/test/preload.ts +0 -12
  173. package/test/reflector.test.ts +0 -320
  174. package/test/retention-cap.test.ts +0 -91
  175. package/test/retired-capabilities-pruned.test.ts +0 -192
  176. package/test/run-store.test.ts +0 -355
  177. package/test/scratchpad.test.ts +0 -74
  178. package/test/secrets-durability.test.ts +0 -119
  179. package/test/seed-model-reseed.test.ts +0 -155
  180. package/test/setup-agent-seed.test.ts +0 -264
  181. package/test/tool-inventory.test.ts +0 -65
  182. package/test/tool-runtime.test.ts +0 -258
  183. package/test/tool-selector-runtime-tools.test.ts +0 -117
  184. package/test/toon.test.ts +0 -429
  185. package/tsconfig.json +0 -42
@@ -0,0 +1,553 @@
1
+ /**
2
+ * computer_use_task — Hive opera su propio navegador mirando la pantalla.
3
+ *
4
+ * @category web
5
+ * @seedId computer_use_task
6
+ * @spanish usar el navegador, hacer clic, operar una página, rellenar un formulario
7
+ *
8
+ * A diferencia del resto de `browser_*`, que actúan por selector CSS, aquí el
9
+ * modelo ve una captura y decide dónde pulsar. Sirve para páginas donde no hay
10
+ * selector estable: canvas, lienzos, interfaces generadas, PDFs incrustados.
11
+ *
12
+ * El bucle es el estándar de Computer Use: captura → el modelo devuelve una
13
+ * acción con coordenadas → se ejecuta → nueva captura, hasta terminar.
14
+ *
15
+ * Dos decisiones que conviene conocer:
16
+ *
17
+ * 1. **Las acciones van por CDP (`Input.dispatchMouseEvent`) cuando el motor lo
18
+ * expone, que es siempre que haya un Chromium.** Es entrada real del
19
+ * navegador: un canvas, un PDF incrustado o un mapa no reciben un MouseEvent
20
+ * sintético, y son justo las páginas para las que existe esta tool. Donde no
21
+ * hay CDP —el WebKit de macOS— se cae a `evaluate()`, que alcanza para HTML.
22
+ *
23
+ * 2. **Actúa sobre el navegador de Hive, nunca sobre la pantalla del usuario.**
24
+ * Si se equivoca, rompe su propia pestaña. En Wayland, además, ninguna
25
+ * aplicación puede mover el ratón de otras ventanas.
26
+ */
27
+
28
+ import type { Tool } from "../types.ts";
29
+ import { logger } from "../../utils/logger.ts";
30
+ import { getBrowserService } from "./browser-service.ts";
31
+ import { loadProviderApiKey } from "../../storage/crypto.ts";
32
+
33
+ const log = logger.child("computer-use");
34
+
35
+ /** Resolución recomendada por Google para este modelo. */
36
+ const VIEWPORT = { width: 1440, height: 900 };
37
+ /** Tope de acciones por tarea: un bucle mal cerrado no puede girar sin fin. */
38
+ const MAX_PASOS = 15;
39
+ /** El modelo devuelve coordenadas en 0–999; hay que escalarlas al viewport. */
40
+ const ESCALA_MODELO = 1000;
41
+
42
+ /**
43
+ * Ancho al que se reduce cada captura antes de mandarla al modelo.
44
+ *
45
+ * Medido sobre una página real: el viewport de 1440x900 al 70% pesa 72 KB y le
46
+ * cuesta ~1032 tokens a Gemini (cuatro tiles de 768). A 1024 de ancho son ~516,
47
+ * y el modelo sigue leyendo botones y texto sin problema. Reescalar cuesta
48
+ * ~11 ms con `Bun.Image`, contra los ~50 ms que ya cuesta la captura.
49
+ *
50
+ * Lo que se optimiza son píxeles, no bytes: el costo va por tiles de 768, así
51
+ * que en una página casi vacía el JPEG recodificado puede pesar algo más y aun
52
+ * así costar la mitad.
53
+ */
54
+ const ANCHO_CAPTURA = Number(process.env.HIVE_COMPUTER_USE_IMAGE_WIDTH) || 1024;
55
+
56
+ /** Calidad JPEG de la captura que ve el modelo. */
57
+ const CALIDAD_CAPTURA = 70;
58
+
59
+ /**
60
+ * Cuántas capturas se conservan en el historial.
61
+ *
62
+ * Cada paso agregaba una imagen y el historial se reenvía **entero** en cada
63
+ * llamada: una tarea de 15 pasos mandaba 120 imágenes en total (~124k tokens
64
+ * sólo de píxeles). El modelo decide mirando la pantalla actual; la anterior
65
+ * ayuda a ver si su última acción hizo algo, y de ahí para atrás no aporta.
66
+ */
67
+ const CAPTURAS_EN_HISTORIAL = 2;
68
+
69
+ const MODELO = process.env.HIVE_COMPUTER_USE_MODEL || "gemini-3.7-flash";
70
+
71
+ export interface Vista {
72
+ url: string;
73
+ navigate(url: string): Promise<void>;
74
+ evaluate<T = unknown>(script: string): Promise<T>;
75
+ screenshot(options?: Record<string, unknown>): Promise<string>;
76
+ type(text: string): Promise<void>;
77
+ press(key: string, options?: { modifiers?: string[] }): Promise<void>;
78
+ scroll(dx: number, dy: number): Promise<void>;
79
+ resize(width: number, height: number): Promise<void>;
80
+ back(): Promise<void>;
81
+ forward(): Promise<void>;
82
+ /** Puente CDP. Lo tienen los dos backends con Chrome; el WebKit de macOS no. */
83
+ cdp?<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
84
+ }
85
+
86
+ /**
87
+ * Reduce la captura antes de que salga hacia el modelo.
88
+ *
89
+ * `Bun.Image` (1.4+) hace el reescalado en proceso, sin dependencias nativas ni
90
+ * un binario aparte. Si no está —Bun más viejo— se manda la original: peor
91
+ * factura, mismo comportamiento.
92
+ */
93
+ export async function reducirCaptura(base64: string): Promise<string> {
94
+ const Imagen = (globalThis as { Bun?: { Image?: unknown } }).Bun?.Image as
95
+ | (new (datos: Uint8Array) => {
96
+ metadata(): Promise<{ width: number; height: number }>;
97
+ resize(ancho: number): { jpeg(o: { quality: number }): { toBuffer(): Promise<Buffer> } };
98
+ })
99
+ | undefined;
100
+ if (typeof Imagen !== "function") return base64;
101
+
102
+ try {
103
+ const bytes = Buffer.from(base64, "base64");
104
+ const imagen = new Imagen(bytes);
105
+ const { width } = await imagen.metadata();
106
+ // Nunca agrandar: una captura chica ya es barata y estirarla sólo empeora
107
+ // lo que el modelo ve.
108
+ if (width <= ANCHO_CAPTURA) return base64;
109
+
110
+ const reducida = await imagen.resize(ANCHO_CAPTURA).jpeg({ quality: CALIDAD_CAPTURA }).toBuffer();
111
+ return reducida.toString("base64");
112
+ } catch (error) {
113
+ log.info(`no se pudo reducir la captura, se manda entera: ${(error as Error).message}`);
114
+ return base64;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Saca del historial las capturas viejas, dejando las últimas
120
+ * `CAPTURAS_EN_HISTORIAL`. Se reemplazan por una línea de texto para que el
121
+ * modelo sepa que ahí hubo una pantalla y no crea que el paso no ocurrió.
122
+ */
123
+ export function podarCapturas(historial: any[]): void {
124
+ const conImagen: number[] = [];
125
+ for (let i = 0; i < historial.length; i++) {
126
+ if (historial[i]?.parts?.some((parte: any) => parte?.inlineData)) conImagen.push(i);
127
+ }
128
+
129
+ for (const indice of conImagen.slice(0, -CAPTURAS_EN_HISTORIAL)) {
130
+ const entrada = historial[indice];
131
+ entrada.parts = entrada.parts
132
+ .filter((parte: any) => !parte?.inlineData)
133
+ .concat([{ text: "(captura de este paso omitida para no cargar el contexto)" }]);
134
+ }
135
+ }
136
+
137
+ /** Escala una coordenada del espacio del modelo (0–999) al viewport real. */
138
+ function aPixeles(valor: unknown, tamano: number): number {
139
+ const n = Number(valor);
140
+ if (!Number.isFinite(n)) return 0;
141
+ return Math.round((n / ESCALA_MODELO) * tamano);
142
+ }
143
+
144
+ /** Qué hay en el punto — sólo para contarle al modelo dónde cayó el clic. */
145
+ function guionDescribir(x: number, y: number): string {
146
+ return `(() => {
147
+ const el = document.elementFromPoint(${x}, ${y});
148
+ if (!el) return "sin-elemento";
149
+ return (el.tagName || "?") + " " + ((el.innerText || el.value || "").trim().slice(0, 60));
150
+ })()`;
151
+ }
152
+
153
+ /**
154
+ * Clic sintético, para cuando no hay CDP.
155
+ *
156
+ * Se despacha la secuencia completa (mousedown, mouseup, click) sobre el
157
+ * elemento que hay en el punto: llamar sólo a `.click()` se salta los handlers
158
+ * que muchas interfaces cuelgan de mousedown, y el clic parece no hacer nada.
159
+ */
160
+ function guionClic(x: number, y: number, boton: number, veces: number): string {
161
+ return `(() => {
162
+ const el = document.elementFromPoint(${x}, ${y});
163
+ if (!el) return "sin-elemento";
164
+ const opts = { bubbles: true, cancelable: true, clientX: ${x}, clientY: ${y}, button: ${boton} };
165
+ for (let i = 0; i < ${veces}; i++) {
166
+ el.dispatchEvent(new MouseEvent("mousedown", opts));
167
+ el.dispatchEvent(new MouseEvent("mouseup", opts));
168
+ el.dispatchEvent(new MouseEvent("click", opts));
169
+ }
170
+ if (typeof el.focus === "function") el.focus();
171
+ return (el.tagName || "?") + " " + ((el.innerText || el.value || "").trim().slice(0, 60));
172
+ })()`;
173
+ }
174
+
175
+ /**
176
+ * Vistas que ya demostraron no tener CDP utilizable.
177
+ *
178
+ * Tener el método no alcanza: el motor WebKit lo expone y lanza. Se prueba una
179
+ * vez, y si falla esta vista queda marcada para el resto de la tarea en vez de
180
+ * pagar un error por cada acción.
181
+ */
182
+ const sinCdp = new WeakSet<object>();
183
+
184
+ function tieneCdp(vista: Vista): boolean {
185
+ return typeof vista.cdp === "function" && !sinCdp.has(vista as unknown as object);
186
+ }
187
+
188
+ /**
189
+ * Clic por coordenadas.
190
+ *
191
+ * Con CDP se despacha entrada real del navegador (`Input.dispatchMouseEvent`),
192
+ * que es lo único que atienden un canvas, un PDF incrustado o un mapa: ahí no
193
+ * hay elemento que reciba un MouseEvent sintético, y ese es justo el caso para
194
+ * el que existe esta tool. Donde no hay CDP —un mac sin ningún Chromium, que
195
+ * cae a WebKit— se usan los eventos sintéticos, que sirven para HTML normal.
196
+ */
197
+ export async function clicEnPunto(
198
+ vista: Vista,
199
+ x: number,
200
+ y: number,
201
+ boton: number,
202
+ veces: number,
203
+ ): Promise<string> {
204
+ if (tieneCdp(vista)) {
205
+ const nombre = boton === 2 ? "right" : boton === 1 ? "middle" : "left";
206
+ const mascara = boton === 2 ? 2 : boton === 1 ? 4 : 1;
207
+
208
+ try {
209
+ const donde = String(await vista.evaluate(guionDescribir(x, y)));
210
+
211
+ // El mouseMoved previo importa: sin él, los menús y tooltips que
212
+ // reaccionan al hover nunca llegan a abrirse antes de que baje el botón.
213
+ await vista.cdp!("Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "none", buttons: 0 });
214
+ for (let i = 1; i <= veces; i++) {
215
+ await vista.cdp!("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: nombre, buttons: mascara, clickCount: i });
216
+ await vista.cdp!("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: nombre, buttons: 0, clickCount: i });
217
+ }
218
+ return donde;
219
+ } catch (error) {
220
+ sinCdp.add(vista as unknown as object);
221
+ log.info(`sin entrada por CDP, se usan eventos sintéticos: ${(error as Error).message}`);
222
+ }
223
+ }
224
+
225
+ return String(await vista.evaluate(guionClic(x, y, boton, veces)));
226
+ }
227
+
228
+ export async function hoverEnPunto(vista: Vista, x: number, y: number): Promise<string> {
229
+ if (tieneCdp(vista)) {
230
+ try {
231
+ await vista.cdp!("Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "none", buttons: 0 });
232
+ return String(await vista.evaluate(guionDescribir(x, y)));
233
+ } catch (error) {
234
+ sinCdp.add(vista as unknown as object);
235
+ log.info(`sin hover por CDP, se usan eventos sintéticos: ${(error as Error).message}`);
236
+ }
237
+ }
238
+
239
+ return String(
240
+ await vista.evaluate(`(() => {
241
+ const el = document.elementFromPoint(${x}, ${y});
242
+ if (!el) return "sin-elemento";
243
+ const opts = { bubbles: true, clientX: ${x}, clientY: ${y} };
244
+ el.dispatchEvent(new MouseEvent("mouseover", opts));
245
+ el.dispatchEvent(new MouseEvent("mousemove", opts));
246
+ return el.tagName || "?";
247
+ })()`),
248
+ );
249
+ }
250
+
251
+ /**
252
+ * Ejecuta una acción del modelo.
253
+ *
254
+ * El nombre se compara por palabra clave y no contra una lista cerrada: los
255
+ * nombres de las funciones predefinidas cambian entre versiones del modelo
256
+ * (`click` vs `click_at`, `press_key` vs `key_combination`), y una tabla exacta
257
+ * se rompería en la siguiente.
258
+ */
259
+ async function ejecutarAccion(
260
+ vista: Vista,
261
+ nombre: string,
262
+ args: Record<string, unknown>,
263
+ viewport: { width: number; height: number },
264
+ ): Promise<string> {
265
+ const n = nombre.toLowerCase();
266
+ const x = aPixeles(args.x, viewport.width);
267
+ const y = aPixeles(args.y, viewport.height);
268
+
269
+ if (n.includes("navigate") || n.includes("go_to") || n === "search") {
270
+ const destino = String(args.url ?? args.query ?? "");
271
+ if (!destino) return "sin destino";
272
+ const url = destino.startsWith("http")
273
+ ? destino
274
+ : `https://www.google.com/search?q=${encodeURIComponent(destino)}`;
275
+ await vista.navigate(url);
276
+ await Bun.sleep(900);
277
+ return `navegado a ${url}`;
278
+ }
279
+
280
+ if (n.includes("back")) {
281
+ await vista.back();
282
+ await Bun.sleep(600);
283
+ return "atrás";
284
+ }
285
+
286
+ if (n.includes("forward")) {
287
+ await vista.forward();
288
+ await Bun.sleep(600);
289
+ return "adelante";
290
+ }
291
+
292
+ if (n.includes("wait")) {
293
+ await Bun.sleep(Math.min(5000, Number(args.seconds ?? 2) * 1000 || 2000));
294
+ return "esperado";
295
+ }
296
+
297
+ if (n.includes("scroll")) {
298
+ const direccion = String(args.direction ?? "down").toLowerCase();
299
+ const magnitud = Number(args.amount ?? args.dy ?? 400) || 400;
300
+ const dy = direccion.includes("up") ? -magnitud : magnitud;
301
+ const dx = direccion.includes("left") ? -magnitud : direccion.includes("right") ? magnitud : 0;
302
+ await vista.scroll(dx, dy || 0);
303
+ await Bun.sleep(350);
304
+ return `scroll ${direccion}`;
305
+ }
306
+
307
+ if (n.includes("hover") || n === "move" || n.includes("mouse_move")) {
308
+ return await hoverEnPunto(vista, x, y);
309
+ }
310
+
311
+ if (n.includes("type") || n.includes("write")) {
312
+ const texto = String(args.text ?? args.value ?? "");
313
+ // Si trae coordenadas, primero hay que poner el foco donde toca.
314
+ if (args.x !== undefined) await clicEnPunto(vista, x, y, 0, 1);
315
+ await vista.type(texto);
316
+ if (args.press_enter === true || args.submit === true) await vista.press("Enter");
317
+ return `escrito: ${texto.slice(0, 40)}`;
318
+ }
319
+
320
+ if (n.includes("key") || n.includes("press") || n.includes("hotkey")) {
321
+ const combinacion = String(args.keys ?? args.key ?? args.combination ?? "");
322
+ const partes = combinacion.split(/[+\s]+/).filter(Boolean);
323
+ const tecla = partes.pop() ?? "";
324
+ if (!tecla) return "sin tecla";
325
+ await vista.press(tecla, partes.length ? { modifiers: partes } : undefined);
326
+ return `tecla ${combinacion}`;
327
+ }
328
+
329
+ if (n.includes("drag")) {
330
+ const x2 = aPixeles(args.destination_x ?? args.to_x ?? args.x2, viewport.width);
331
+ const y2 = aPixeles(args.destination_y ?? args.to_y ?? args.y2, viewport.height);
332
+ if (tieneCdp(vista)) {
333
+ try {
334
+ // Un arrastre real necesita movimientos intermedios: las interfaces que
335
+ // siguen el puntero (sliders, lienzos, reordenables) ignoran un salto.
336
+ await vista.cdp!("Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "none", buttons: 0 });
337
+ await vista.cdp!("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", buttons: 1, clickCount: 1 });
338
+ for (let paso = 1; paso <= 5; paso++) {
339
+ await vista.cdp!("Input.dispatchMouseEvent", {
340
+ type: "mouseMoved",
341
+ x: Math.round(x + ((x2 - x) * paso) / 5),
342
+ y: Math.round(y + ((y2 - y) * paso) / 5),
343
+ button: "left",
344
+ buttons: 1,
345
+ });
346
+ }
347
+ await vista.cdp!("Input.dispatchMouseEvent", { type: "mouseReleased", x: x2, y: y2, button: "left", buttons: 0, clickCount: 1 });
348
+ return `arrastrado de (${x},${y}) a (${x2},${y2})`;
349
+ } catch (error) {
350
+ sinCdp.add(vista as unknown as object);
351
+ log.info(`sin arrastre por CDP, se usan eventos sintéticos: ${(error as Error).message}`);
352
+ }
353
+ }
354
+
355
+ return String(
356
+ await vista.evaluate(`(() => {
357
+ const a = document.elementFromPoint(${x}, ${y});
358
+ const b = document.elementFromPoint(${x2}, ${y2});
359
+ if (!a || !b) return "sin-elemento";
360
+ a.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, clientX: ${x}, clientY: ${y} }));
361
+ b.dispatchEvent(new MouseEvent("mousemove", { bubbles: true, clientX: ${x2}, clientY: ${y2} }));
362
+ b.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, clientX: ${x2}, clientY: ${y2} }));
363
+ return "arrastrado";
364
+ })()`),
365
+ );
366
+ }
367
+
368
+ if (n.includes("screenshot")) return "captura tomada";
369
+
370
+ if (n.includes("click")) {
371
+ const boton = n.includes("right") ? 2 : n.includes("middle") ? 1 : 0;
372
+ const veces = n.includes("triple") ? 3 : n.includes("double") ? 2 : 1;
373
+ const resultado = await clicEnPunto(vista, x, y, boton, veces);
374
+ await Bun.sleep(450);
375
+ return `clic en (${x},${y}) → ${resultado}`;
376
+ }
377
+
378
+ return `acción no soportada: ${nombre}`;
379
+ }
380
+
381
+ /** ¿El modelo pide confirmación humana para esta acción? */
382
+ function pideConfirmacion(args: Record<string, unknown>): boolean {
383
+ const decision = String(args.safety_decision ?? (args.safety as any)?.decision ?? "").toLowerCase();
384
+ return decision.includes("confirm");
385
+ }
386
+
387
+ export const computerUseTaskTool: Tool = {
388
+ name: "computer_use_task",
389
+ description:
390
+ "Opera el navegador de Hive mirando la pantalla: hace clic, escribe y navega guiado por lo que ve. " +
391
+ "Úsalo cuando la página no tenga selectores estables o cuando browser_click/browser_type no basten. " +
392
+ "Actúa sobre el navegador de Hive, NUNCA sobre la pantalla del usuario. " +
393
+ "Spanish: usar el navegador, hacer clic, operar una página, rellenar un formulario",
394
+ timeoutMs: 240_000,
395
+ parameters: {
396
+ type: "object",
397
+ properties: {
398
+ objetivo: {
399
+ type: "string",
400
+ description:
401
+ "Qué hay que lograr, en una frase concreta y verificable. Ej: 'buscar el precio del dólar en el Banco de la República y leer la cifra'.",
402
+ },
403
+ url: { type: "string", description: "Página desde la que empezar (opcional)." },
404
+ max_pasos: { type: "number", description: `Tope de acciones (default ${MAX_PASOS}).` },
405
+ confirmado: {
406
+ type: "boolean",
407
+ description:
408
+ "Ponlo en true SOLO si el usuario ya aprobó explícitamente una acción que quedó pendiente de confirmación.",
409
+ },
410
+ },
411
+ required: ["objetivo"],
412
+ },
413
+ execute: async (params: Record<string, unknown>, _config?: any) => {
414
+ const objetivo = String(params.objetivo ?? "").trim();
415
+ if (!objetivo) return { ok: false, error: "Falta el objetivo." };
416
+
417
+ const maxPasos = Math.max(1, Math.min(30, Number(params.max_pasos) || MAX_PASOS));
418
+ const confirmado = params.confirmado === true;
419
+
420
+ const apiKey = (await loadProviderApiKey("gemini")) || process.env.GEMINI_API_KEY;
421
+ if (!apiKey) {
422
+ return { ok: false, error: "Falta la API key de Gemini (Ajustes → Proveedores)." };
423
+ }
424
+
425
+ const servicio = getBrowserService();
426
+ if (!servicio?.isAvailable()) {
427
+ return { ok: false, error: "El navegador de Hive no está disponible." };
428
+ }
429
+ const vista = (await servicio.getView()) as unknown as Vista | null;
430
+ if (!vista) return { ok: false, error: "No se pudo abrir el navegador de Hive." };
431
+
432
+ const { GoogleGenAI } = await import("@google/genai");
433
+ const ai = new GoogleGenAI({ apiKey });
434
+
435
+ // El resize es un intento, no un requisito: algunos motores del WebView no
436
+ // implementan `Emulation.setDeviceMetricsOverride` y lanzan. Lo que importa
437
+ // es medir después el viewport de verdad, porque es contra ese tamaño
438
+ // —no contra el que pedimos— que hay que escalar las coordenadas del modelo.
439
+ try {
440
+ await vista.resize(VIEWPORT.width, VIEWPORT.height);
441
+ } catch (error) {
442
+ log.info(`el motor no permite fijar el viewport: ${(error as Error).message}`);
443
+ }
444
+
445
+ if (params.url) {
446
+ await vista.navigate(String(params.url));
447
+ await Bun.sleep(900);
448
+ }
449
+
450
+ const medido = await vista
451
+ .evaluate<{ w: number; h: number }>("({ w: window.innerWidth, h: window.innerHeight })")
452
+ .catch(() => null);
453
+ const viewport = {
454
+ width: Number(medido?.w) || VIEWPORT.width,
455
+ height: Number(medido?.h) || VIEWPORT.height,
456
+ };
457
+ log.info(`viewport real: ${viewport.width}x${viewport.height}`);
458
+
459
+ const capturar = async (): Promise<string> =>
460
+ reducirCaptura(
461
+ await vista.screenshot({ encoding: "base64", format: "jpeg", quality: CALIDAD_CAPTURA }),
462
+ );
463
+
464
+ const historial: any[] = [
465
+ {
466
+ role: "user",
467
+ parts: [
468
+ { text: `Objetivo: ${objetivo}\nURL actual: ${vista.url}` },
469
+ { inlineData: { mimeType: "image/jpeg", data: await capturar() } },
470
+ ],
471
+ },
472
+ ];
473
+
474
+ const acciones: string[] = [];
475
+
476
+ for (let paso = 0; paso < maxPasos; paso++) {
477
+ const respuesta = await ai.models.generateContent({
478
+ model: MODELO,
479
+ contents: historial,
480
+ config: {
481
+ tools: [{ computerUse: { environment: "ENVIRONMENT_BROWSER" } }],
482
+ } as any,
483
+ });
484
+
485
+ const partes = respuesta.candidates?.[0]?.content?.parts ?? [];
486
+ const llamada = partes.find((p: any) => p.functionCall)?.functionCall;
487
+ const texto = partes
488
+ .filter((p: any) => p.text)
489
+ .map((p: any) => p.text)
490
+ .join(" ")
491
+ .trim();
492
+
493
+ if (!llamada) {
494
+ log.info(`objetivo cumplido en ${paso} acciones`);
495
+ return { ok: true, completado: true, pasos: paso, acciones, resultado: texto || "Hecho.", url: vista.url };
496
+ }
497
+
498
+ const args = (llamada.args ?? {}) as Record<string, unknown>;
499
+
500
+ // La confirmación no se resuelve aquí: el runtime de tools no puede
501
+ // preguntarle a nadie. Se devuelve el control al coordinador, que sí
502
+ // habla con el usuario, y la tarea se reanuda con `confirmado: true`.
503
+ if (pideConfirmacion(args) && !confirmado) {
504
+ log.info(`pausada por seguridad: ${llamada.name}`);
505
+ return {
506
+ ok: true,
507
+ completado: false,
508
+ pendiente_confirmacion: true,
509
+ accion: llamada.name,
510
+ intencion: String(args.intent ?? texto ?? ""),
511
+ acciones,
512
+ url: vista.url,
513
+ nota:
514
+ "Esta acción necesita el visto bueno del usuario. Pregúntale con tus palabras y, si acepta, " +
515
+ "vuelve a llamar a computer_use_task con el mismo objetivo y confirmado: true.",
516
+ };
517
+ }
518
+
519
+ const resultado = await ejecutarAccion(vista, String(llamada.name), args, viewport);
520
+ acciones.push(`${llamada.name}: ${resultado}`);
521
+ log.info(`paso ${paso + 1}/${maxPasos} — ${llamada.name} → ${resultado}`);
522
+
523
+ // Se devuelve el contenido del modelo TAL CUAL vino, sin reconstruirlo.
524
+ // Gemini 3.x firma cada functionCall con un `thoughtSignature` y rechaza
525
+ // el turno siguiente si falta: reconstruir la parte a mano cortaba el
526
+ // bucle en la segunda acción con un 400.
527
+ historial.push(respuesta.candidates?.[0]?.content ?? { role: "model", parts: [{ functionCall: llamada }] });
528
+ historial.push({
529
+ role: "user",
530
+ parts: [
531
+ {
532
+ functionResponse: {
533
+ name: llamada.name,
534
+ response: { output: resultado, url: vista.url },
535
+ },
536
+ },
537
+ { inlineData: { mimeType: "image/jpeg", data: await capturar() } },
538
+ ],
539
+ });
540
+
541
+ podarCapturas(historial);
542
+ }
543
+
544
+ return {
545
+ ok: true,
546
+ completado: false,
547
+ pasos: maxPasos,
548
+ acciones,
549
+ url: vista.url,
550
+ nota: `Se alcanzó el tope de ${maxPasos} acciones sin terminar. Revisa el objetivo o divídelo en partes.`,
551
+ };
552
+ },
553
+ };
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Web Tools - Browser automation + Web utilities
3
3
  *
4
- * Browser tools use agent-browser (Rust CLI, auto-managed).
4
+ * Las browser tools hablan con `BrowserBackend`, que hoy tiene una sola
5
+ * implementación: Bun.WebView in-process sobre un Chromium del sistema.
5
6
  */
6
7
 
7
8
  import type { Tool } from "../types.ts";
@@ -9,12 +10,14 @@ import { webSearchTool } from "./web-search.ts";
9
10
  import { webFetchTool } from "./web-fetch.ts";
10
11
  import { browserNavigateTool } from "./browser-navigate.ts";
11
12
  import { browserScreenshotTool } from "./browser-screenshot.ts";
13
+ import { computerUseTaskTool } from "./computer-use.ts";
12
14
  import { browserClickTool } from "./browser-click.ts";
13
15
  import { browserTypeTool } from "./browser-type.ts";
14
16
  import { browserExtractTool } from "./browser-extract.ts";
15
17
  import { browserScriptTool } from "./browser-script.ts";
16
18
  import { browserWaitTool } from "./browser-wait.ts";
17
19
  import { artifactInspectTool } from "./artifact-inspect.ts";
20
+ import { artifactReadTool } from "./artifact-read.ts";
18
21
 
19
22
  export function createTools(): Tool[] {
20
23
  return [
@@ -22,12 +25,14 @@ export function createTools(): Tool[] {
22
25
  webFetchTool,
23
26
  browserNavigateTool,
24
27
  browserScreenshotTool,
28
+ computerUseTaskTool,
25
29
  browserClickTool,
26
30
  browserTypeTool,
27
31
  browserExtractTool,
28
32
  browserScriptTool,
29
33
  browserWaitTool,
30
34
  artifactInspectTool,
35
+ artifactReadTool,
31
36
  ];
32
37
  }
33
38
 
@@ -35,6 +40,7 @@ export * from "./web-search.ts";
35
40
  export * from "./web-fetch.ts";
36
41
  export * from "./browser-navigate.ts";
37
42
  export * from "./browser-screenshot.ts";
43
+ export * from "./computer-use.ts";
38
44
  export * from "./browser-click.ts";
39
45
  export * from "./browser-type.ts";
40
46
  export * from "./browser-extract.ts";
@@ -42,3 +48,4 @@ export * from "./browser-script.ts";
42
48
  export * from "./browser-wait.ts";
43
49
  export * from "./browser-service.ts";
44
50
  export * from "./artifact-inspect.ts";
51
+ export * from "./artifact-read.ts";