@johpaz/hive-sdk 0.1.5 → 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 (182) hide show
  1. package/CHANGELOG.md +167 -0
  2. package/README.md +11 -1
  3. package/package.json +26 -10
  4. package/packages/core/src/agent/acceptance-checks.ts +16 -10
  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 +15 -1
  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 +226 -0
  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 +85 -366
  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 +851 -0
  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 -833
  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/agent-loop-terminal-synthesis.test.ts +0 -32
  153. package/test/catalog-agents-stay-enabled.test.ts +0 -117
  154. package/test/causal-events.test.ts +0 -117
  155. package/test/compaction.test.ts +0 -105
  156. package/test/context-compiler.test.ts +0 -269
  157. package/test/curator.test.ts +0 -130
  158. package/test/durable-queue.test.ts +0 -114
  159. package/test/harness-barrel.test.ts +0 -64
  160. package/test/hive-helpers.test.ts +0 -130
  161. package/test/hivedb-search.test.ts +0 -189
  162. package/test/internal-turns.test.ts +0 -166
  163. package/test/job-idempotency.test.ts +0 -68
  164. package/test/job-retry-backoff.test.ts +0 -184
  165. package/test/job-store.test.ts +0 -381
  166. package/test/llm-retry.test.ts +0 -97
  167. package/test/memory-perf.test.ts +0 -774
  168. package/test/minimal-loadout.test.ts +0 -78
  169. package/test/model-catalog.test.ts +0 -105
  170. package/test/preload.ts +0 -12
  171. package/test/reflector.test.ts +0 -320
  172. package/test/retention-cap.test.ts +0 -91
  173. package/test/retired-capabilities-pruned.test.ts +0 -192
  174. package/test/run-store.test.ts +0 -355
  175. package/test/scratchpad.test.ts +0 -74
  176. package/test/secrets-durability.test.ts +0 -119
  177. package/test/seed-model-reseed.test.ts +0 -155
  178. package/test/setup-agent-seed.test.ts +0 -264
  179. package/test/tool-inventory.test.ts +0 -65
  180. package/test/tool-runtime.test.ts +0 -258
  181. package/test/toon.test.ts +0 -429
  182. package/tsconfig.json +0 -42
@@ -1,4 +1,4 @@
1
- import { OpenAICompatBase } from "./openai-compat-base"
1
+ import { OpenAICompatBase } from "./openai-compat-base.ts"
2
2
 
3
3
  export class OpenCodeGoProvider extends OpenAICompatBase {
4
4
  static readonly secretKey = "OPENCODE_GO_API_KEY"
@@ -1,4 +1,4 @@
1
- import { OpenAICompatBase } from "./openai-compat-base"
1
+ import { OpenAICompatBase } from "./openai-compat-base.ts"
2
2
 
3
3
  export class OpenRouterProvider extends OpenAICompatBase {
4
4
  constructor() { super("openrouter") }
@@ -1,4 +1,4 @@
1
- import { OpenAICompatBase } from "./openai-compat-base"
1
+ import { OpenAICompatBase } from "./openai-compat-base.ts"
2
2
 
3
3
  export class QwenProvider extends OpenAICompatBase {
4
4
  constructor() { super("qwen") }
@@ -1,4 +1,4 @@
1
- import { OpenAICompatBase } from "./openai-compat-base"
1
+ import { OpenAICompatBase } from "./openai-compat-base.ts"
2
2
 
3
3
  export class ZaiProvider extends OpenAICompatBase {
4
4
  constructor() { super("z-ai") }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * mcp-result-normalizer — keeps large/binary MCP tool results out of the LLM
3
+ * context window.
4
+ *
5
+ * `MCPClientManager.callTool()` (packages/mcp/src/manager.ts) returns the raw
6
+ * `content` array from the MCP SDK's CallToolResult. Per the MCP spec that
7
+ * array can contain `image`/`audio` blocks (base64 `data`) or `resource`
8
+ * blocks (base64 `blob`) alongside plain `text` blocks. Nothing downstream
9
+ * (agent-loop.ts's formatToolResult) inspects those blocks — a base64 image
10
+ * would be serialized whole into a `role:"tool"` message and sent to the
11
+ * model, which is exactly what filled the context window and hung the agent
12
+ * in the incident this module fixes.
13
+ *
14
+ * This mirrors the pattern already used by browser-screenshot.ts: binary
15
+ * content gets persisted via createArtifact() (artifacts/store.ts) and the
16
+ * model only ever sees a lightweight { type: "artifact_ref", ... } reference.
17
+ */
18
+
19
+ import { createArtifact } from "../artifacts/store.ts";
20
+ import { logger } from "../utils/logger.ts";
21
+
22
+ const log = logger.child("mcp-result-normalizer");
23
+
24
+ /** Blocks longer than this (in chars) get materialized as a text artifact instead of inlined. Override via HIVE_MCP_INLINE_MAX_CHARS. */
25
+ const MCP_INLINE_MAX_CHARS = Number(process.env.HIVE_MCP_INLINE_MAX_CHARS) || 20_000;
26
+
27
+ export interface McpNormalizeContext {
28
+ userId?: string;
29
+ runId?: string | null;
30
+ taskId?: string | null;
31
+ }
32
+
33
+ type McpContentBlock = Record<string, unknown> & { type?: unknown };
34
+
35
+ function isBinaryBlock(block: McpContentBlock): block is McpContentBlock & { type: "image" | "audio"; data: string; mimeType: string } {
36
+ return (block.type === "image" || block.type === "audio") && typeof block.data === "string";
37
+ }
38
+
39
+ function isBlobResourceBlock(block: McpContentBlock): block is McpContentBlock & { type: "resource"; resource: { blob: string; mimeType?: string; uri?: string } } {
40
+ if (block.type !== "resource") return false;
41
+ const resource = block.resource as Record<string, unknown> | undefined;
42
+ return !!resource && typeof resource.blob === "string";
43
+ }
44
+
45
+ function isOversizedTextBlock(block: McpContentBlock): block is McpContentBlock & { type: "text"; text: string } {
46
+ return block.type === "text" && typeof block.text === "string" && block.text.length > MCP_INLINE_MAX_CHARS;
47
+ }
48
+
49
+ async function materializeBinary(
50
+ bytesBase64: string,
51
+ mimeType: string,
52
+ kind: string,
53
+ ctx: McpNormalizeContext,
54
+ ): Promise<Record<string, unknown>> {
55
+ if (!ctx.userId) {
56
+ // Without a user_id we can't set ownership on the artifact (inspectArtifact
57
+ // would reject any read against it later) — never let raw base64 through,
58
+ // just describe what was omitted.
59
+ const approxBytes = Math.floor((bytesBase64.length * 3) / 4);
60
+ log.warn(`[materializeBinary] No user_id in tool context — omitting ${mimeType} block (${approxBytes} bytes) instead of creating an artifact`);
61
+ return { type: "artifact_omitted", mime_type: mimeType, approx_size: approxBytes, reason: "no user_id in tool context" };
62
+ }
63
+
64
+ const bytes = Buffer.from(bytesBase64, "base64");
65
+ const artifact = await createArtifact({
66
+ bytes,
67
+ mimeType,
68
+ kind,
69
+ userId: ctx.userId,
70
+ runId: ctx.runId ?? null,
71
+ taskId: ctx.taskId ?? null,
72
+ });
73
+ log.info(`[materializeBinary] Stored ${kind} as artifact ${artifact.id} (${artifact.size} bytes, ${artifact.mime_type})`);
74
+ return {
75
+ type: "artifact_ref",
76
+ artifact_id: artifact.id,
77
+ mime_type: artifact.mime_type,
78
+ size: artifact.size,
79
+ sha256: artifact.sha256,
80
+ expires_at: artifact.expires_at,
81
+ };
82
+ }
83
+
84
+ /** Above this, parsing the text just to describe it costs more than it explains. */
85
+ const MAX_SHAPE_PROBE_CHARS = 5_000_000;
86
+
87
+ /**
88
+ * Describes the shape of a JSON payload so the model can aim `artifact_read`
89
+ * instead of paging blind.
90
+ *
91
+ * The 500-char preview alone is close to useless on the payload this was
92
+ * written for — a Gmail MCP result where the first message's `received`
93
+ * headers eat the whole window before a single subject line appears. Knowing
94
+ * "12 items, keyed id/threadId/labelIds/headers" is what turns a search into
95
+ * one call.
96
+ */
97
+ function describeJsonShape(text: string): Record<string, unknown> {
98
+ if (text.length > MAX_SHAPE_PROBE_CHARS) return {};
99
+ const trimmed = text.trimStart();
100
+ if (!trimmed.startsWith("[") && !trimmed.startsWith("{")) return {};
101
+
102
+ try {
103
+ const parsed = JSON.parse(text);
104
+ if (Array.isArray(parsed)) {
105
+ const first = parsed.find((item) => item && typeof item === "object" && !Array.isArray(item));
106
+ return {
107
+ json_items: parsed.length,
108
+ ...(first ? { json_item_keys: Object.keys(first as Record<string, unknown>).slice(0, 25) } : {}),
109
+ };
110
+ }
111
+ if (parsed && typeof parsed === "object") {
112
+ return { json_keys: Object.keys(parsed as Record<string, unknown>).slice(0, 25) };
113
+ }
114
+ } catch {
115
+ // Not JSON, or truncated JSON — the plain preview still stands.
116
+ }
117
+ return {};
118
+ }
119
+
120
+ async function materializeText(text: string, ctx: McpNormalizeContext): Promise<Record<string, unknown>> {
121
+ if (!ctx.userId) {
122
+ log.warn(`[materializeText] No user_id in tool context — truncating oversized text block (${text.length} chars) instead of creating an artifact`);
123
+ return { type: "text", text: `${text.slice(0, 500)}… [truncated: ${text.length} chars total, no user_id to persist full text as artifact]` };
124
+ }
125
+
126
+ const artifact = await createArtifact({
127
+ bytes: Buffer.from(text, "utf-8"),
128
+ mimeType: "text/plain",
129
+ kind: "mcp_text_result",
130
+ userId: ctx.userId,
131
+ runId: ctx.runId ?? null,
132
+ taskId: ctx.taskId ?? null,
133
+ });
134
+ log.info(`[materializeText] Stored oversized text result as artifact ${artifact.id} (${artifact.size} bytes)`);
135
+ return {
136
+ type: "artifact_ref",
137
+ artifact_id: artifact.id,
138
+ mime_type: "text/plain",
139
+ size: artifact.size,
140
+ chars: text.length,
141
+ preview: text.length > 500 ? `${text.slice(0, 500)}…` : text,
142
+ ...describeJsonShape(text),
143
+ // Without this the model only knows the data exists somewhere. It used to
144
+ // reach for artifact_inspect (metadata only), find nothing usable, and
145
+ // spend its remaining iterations guessing.
146
+ hint: "Full content is available via artifact_read (artifactId + offset/limit, or search).",
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Normalizes a raw MCP CallToolResult.content array: binary blocks (image,
152
+ * audio, resource-with-blob) become { type: "artifact_ref", ... }, oversized
153
+ * text blocks are persisted the same way with a short preview kept inline,
154
+ * everything else (text, resource_link) passes through unchanged.
155
+ *
156
+ * Never pre-stringifies — returns JS values so formatToolResult (toon.ts)
157
+ * keeps doing the actual TOON/JSON encoding, same contract context-compiler.ts
158
+ * already documents for MCP tool executors.
159
+ */
160
+ export async function normalizeMcpResult(content: unknown, ctx: McpNormalizeContext): Promise<unknown> {
161
+ if (!Array.isArray(content)) return content;
162
+
163
+ const out: unknown[] = [];
164
+ for (const raw of content) {
165
+ if (!raw || typeof raw !== "object") {
166
+ out.push(raw);
167
+ continue;
168
+ }
169
+ const block = raw as McpContentBlock;
170
+ try {
171
+ if (isBinaryBlock(block)) {
172
+ out.push(await materializeBinary(block.data, block.mimeType, "mcp_result", ctx));
173
+ continue;
174
+ }
175
+ if (isBlobResourceBlock(block)) {
176
+ const mimeType = block.resource.mimeType || "application/octet-stream";
177
+ out.push(await materializeBinary(block.resource.blob, mimeType, "mcp_result", ctx));
178
+ continue;
179
+ }
180
+ if (isOversizedTextBlock(block)) {
181
+ out.push(await materializeText(block.text, ctx));
182
+ continue;
183
+ }
184
+ } catch (err) {
185
+ log.error(`[normalizeMcpResult] Failed to materialize block (type=${String(block.type)}): ${(err as Error).message}`);
186
+ out.push({ type: "artifact_error", mime_type: (block as { mimeType?: string }).mimeType, error: (err as Error).message });
187
+ continue;
188
+ }
189
+ out.push(block);
190
+ }
191
+ return out;
192
+ }
@@ -7,15 +7,15 @@
7
7
  * folding, lenient parsing — raw user text never throws).
8
8
  */
9
9
 
10
- import { col } from "../storage/hive"
11
- import type { PlaybookDoc } from "../storage/collections"
12
- import { logger } from "../utils/logger"
10
+ import { col } from "../storage/hive.ts"
11
+ import type { PlaybookDoc } from "../storage/collections.ts"
12
+ import { logger } from "../utils/logger.ts"
13
13
  import {
14
14
  searchCapabilities,
15
15
  applyRelativeCutoff,
16
16
  replaceCapabilityDocs,
17
17
  type CapabilityDoc,
18
- } from "./capability-search"
18
+ } from "./capability-search.ts"
19
19
 
20
20
  const log = logger.child("playbook-selector")
21
21
 
@@ -13,11 +13,11 @@
13
13
  * - Skills activos
14
14
  */
15
15
 
16
- import { col } from "../storage/hive"
17
- import type { EthicsDoc, AgentDoc, UserDoc } from "../storage/collections"
18
- import { logger } from "../utils/logger"
19
- import { formatContext } from "../utils/toon"
20
- import { resolveUserId } from "../storage/onboarding"
16
+ import { col } from "../storage/hive.ts"
17
+ import type { EthicsDoc, AgentDoc, UserDoc } from "../storage/collections.ts"
18
+ import { logger } from "../utils/logger.ts"
19
+ import { formatContext } from "../utils/toon.ts"
20
+ import { resolveUserId } from "../storage/onboarding.ts"
21
21
 
22
22
  const log = logger.child("prompt-builder")
23
23
 
@@ -6,11 +6,11 @@
6
6
  * reviewer doesn't have to replay the whole run to trust its outcome.
7
7
  */
8
8
 
9
- import { col, nextId, toIndexable } from "../storage/hive";
10
- import type { ProofPacketDoc } from "../storage/collections";
11
- import type { AcceptanceResult } from "./goal-runner";
12
- import type { RunEpoch } from "./run-epoch";
13
- import { logger } from "../utils/logger";
9
+ import { col, nextId, toIndexable } from "../storage/hive.ts";
10
+ import type { ProofPacketDoc } from "../storage/collections.ts";
11
+ import type { AcceptanceResult } from "./goal-runner.ts";
12
+ import type { RunEpoch } from "./run-epoch.ts";
13
+ import { logger } from "../utils/logger.ts";
14
14
 
15
15
  const log = logger.child("proof-packet");
16
16
 
@@ -7,10 +7,10 @@
7
7
 
8
8
  import type { Config } from "../../config/loader.ts"
9
9
  import { logger } from "../../utils/logger.ts"
10
- import { getAgentLoop } from "../agent-loop"
11
- import { resolveUserId, resolveAgentId } from "../../storage/onboarding"
12
- import type { ContentPart } from "../../multimodal/types"
13
- import type { TurnSource } from "../../storage/collections"
10
+ import { getAgentLoop } from "../agent-loop.ts"
11
+ import { resolveUserId, resolveAgentId } from "../../storage/onboarding.ts"
12
+ import type { ContentPart } from "../../multimodal/types.ts"
13
+ import type { TurnSource } from "../../storage/collections.ts"
14
14
 
15
15
  export type Provider = "openai" | "anthropic" | "gemini" | "mistral" | "kimi" | "ollama" | "openrouter" | "deepseek" | "nvidia" | "hiveagents" | "z-ai" | "modelscope" | "minimax" | "qwen" | "groq" | "opencode-go"
16
16
 
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Adaptador de la Live API de Gemini (`bidiGenerateContent`).
3
+ *
4
+ * Sobre `ai.live.connect()` de @google/genai. El import es dinámico igual que en
5
+ * ../llm-providers/gemini.ts: el SDK arrastra dependencias de Vertex que no hacen
6
+ * falta con API key, y cargarlo perezosamente evita pagarlas en el arranque.
7
+ *
8
+ * Verificado contra gemini-3.1-flash-live-preview (2026-08): la inyección de
9
+ * texto va por `sendRealtimeInput({text})`; `sendClientContent` quedó restringido
10
+ * al contexto inicial en 3.x, así que no se usa acá.
11
+ */
12
+
13
+ import { logger } from "../../utils/logger.ts";
14
+ import { ensureArrayItems } from "../llm-providers/interface.ts";
15
+ import type {
16
+ RealtimeProvider,
17
+ RealtimeSession,
18
+ RealtimeSessionOptions,
19
+ RealtimeToolCall,
20
+ } from "./interface.ts";
21
+
22
+ const log = logger.child("realtime:gemini");
23
+
24
+ /** Modelos con `bidiGenerateContent`. El id vive en la BD; esto es sólo el fallback. */
25
+ export const DEFAULT_GEMINI_LIVE_MODEL = "gemini-3.1-flash-live-preview";
26
+
27
+ class GeminiLiveSession implements RealtimeSession {
28
+ closed = false;
29
+
30
+ readonly model: string;
31
+ private readonly session: any;
32
+ private readonly callbacks: RealtimeSessionOptions["callbacks"];
33
+
34
+ constructor(
35
+ model: string,
36
+ session: any,
37
+ callbacks: RealtimeSessionOptions["callbacks"],
38
+ ) {
39
+ this.model = model;
40
+ this.session = session;
41
+ this.callbacks = callbacks;
42
+ }
43
+
44
+ sendAudio(pcm: ArrayBufferLike): void {
45
+ if (this.closed) return;
46
+ try {
47
+ this.session.sendRealtimeInput({
48
+ audio: {
49
+ data: Buffer.from(pcm as ArrayBuffer).toString("base64"),
50
+ mimeType: "audio/pcm;rate=16000",
51
+ },
52
+ });
53
+ } catch (error) {
54
+ this.fail(error, "sendAudio");
55
+ }
56
+ }
57
+
58
+ sendVideoFrame(base64: string, mimeType: string): void {
59
+ if (this.closed) return;
60
+ try {
61
+ this.session.sendRealtimeInput({ video: { data: base64, mimeType } });
62
+ } catch (error) {
63
+ this.fail(error, "sendVideoFrame");
64
+ }
65
+ }
66
+
67
+ sendText(text: string): void {
68
+ if (this.closed || !text.trim()) return;
69
+ try {
70
+ this.session.sendRealtimeInput({ text });
71
+ } catch (error) {
72
+ this.fail(error, "sendText");
73
+ }
74
+ }
75
+
76
+ sendToolResult(id: string, name: string, result: Record<string, unknown>): void {
77
+ if (this.closed) return;
78
+ try {
79
+ this.session.sendToolResponse({ functionResponses: [{ id, name, response: result }] });
80
+ } catch (error) {
81
+ this.fail(error, "sendToolResult");
82
+ }
83
+ }
84
+
85
+ close(): void {
86
+ if (this.closed) return;
87
+ this.closed = true;
88
+ try {
89
+ this.session.close();
90
+ } catch {
91
+ /* el socket ya estaba cerrado */
92
+ }
93
+ }
94
+
95
+ /** Marca la sesión como muerta: seguir escribiendo en un socket roto sólo genera ruido. */
96
+ private fail(error: unknown, op: string): void {
97
+ this.closed = true;
98
+ const err = error instanceof Error ? error : new Error(String(error));
99
+ log.warn(`${op} falló: ${err.message}`);
100
+ this.callbacks.onError?.(err);
101
+ }
102
+ }
103
+
104
+ export class GeminiLiveProvider implements RealtimeProvider {
105
+ readonly id = "gemini";
106
+
107
+ async connect(options: RealtimeSessionOptions): Promise<RealtimeSession> {
108
+ const { GoogleGenAI } = await import("@google/genai");
109
+ const ai = new GoogleGenAI({ apiKey: options.apiKey });
110
+ const cb = options.callbacks;
111
+
112
+ const config: Record<string, unknown> = {
113
+ responseModalities: ["AUDIO"],
114
+ systemInstruction: options.systemInstruction,
115
+ // Necesarias para persistir el hilo y pintarlo en el chat: sin esto sólo
116
+ // llegan bytes de audio y la conversación hablada no deja rastro escrito.
117
+ inputAudioTranscription: {},
118
+ outputAudioTranscription: {},
119
+ // Sin compresión la sesión muere a los 15 min de audio.
120
+ contextWindowCompression: { slidingWindow: {} },
121
+ // Handle para reanudar tras un goAway sin perder el hilo.
122
+ sessionResumption: options.resumptionHandle ? { handle: options.resumptionHandle } : {},
123
+ };
124
+
125
+ if (options.voice || options.language) {
126
+ const speechConfig: Record<string, unknown> = {};
127
+ if (options.voice) {
128
+ speechConfig.voiceConfig = { prebuiltVoiceConfig: { voiceName: options.voice } };
129
+ }
130
+ if (options.language) speechConfig.languageCode = options.language;
131
+ config.speechConfig = speechConfig;
132
+ }
133
+ if (options.silenceDurationMs || options.startOfSpeechSensitivity) {
134
+ const deteccion: Record<string, unknown> = {};
135
+ if (options.silenceDurationMs) deteccion.silenceDurationMs = options.silenceDurationMs;
136
+ if (options.startOfSpeechSensitivity) {
137
+ deteccion.startOfSpeechSensitivity = options.startOfSpeechSensitivity;
138
+ }
139
+ config.realtimeInputConfig = { automaticActivityDetection: deteccion };
140
+ }
141
+ if (options.tools?.length) {
142
+ config.tools = [
143
+ {
144
+ functionDeclarations: options.tools.map((t) => ({
145
+ name: t.function.name,
146
+ description: t.function.description,
147
+ parameters: ensureArrayItems(t.function.parameters),
148
+ })),
149
+ },
150
+ ];
151
+ }
152
+
153
+ let wrapper: GeminiLiveSession | null = null;
154
+
155
+ const session = await ai.live.connect({
156
+ model: options.model,
157
+ config: config as any,
158
+ callbacks: {
159
+ onopen: () => log.info(`sesión abierta (${options.model})`),
160
+ onmessage: (msg: any) => {
161
+ try {
162
+ handleMessage(msg, cb);
163
+ } catch (error) {
164
+ cb.onError?.(error instanceof Error ? error : new Error(String(error)));
165
+ }
166
+ },
167
+ onerror: (e: any) => {
168
+ cb.onError?.(new Error(e?.message ?? String(e)));
169
+ },
170
+ onclose: (e: any) => {
171
+ if (wrapper) wrapper.closed = true;
172
+ cb.onClose?.(e?.reason || "");
173
+ },
174
+ },
175
+ });
176
+
177
+ wrapper = new GeminiLiveSession(options.model, session, cb);
178
+ return wrapper;
179
+ }
180
+ }
181
+
182
+ function handleMessage(msg: any, cb: RealtimeSessionOptions["callbacks"]): void {
183
+ const content = msg.serverContent;
184
+ if (content) {
185
+ for (const part of content.modelTurn?.parts ?? []) {
186
+ const data = part.inlineData?.data;
187
+ if (data) cb.onAudio?.(Buffer.from(data, "base64"));
188
+ }
189
+ if (content.inputTranscription?.text) cb.onInputTranscript?.(content.inputTranscription.text);
190
+ if (content.outputTranscription?.text) cb.onOutputTranscript?.(content.outputTranscription.text);
191
+ if (content.interrupted) cb.onInterrupted?.();
192
+ if (content.turnComplete) cb.onTurnComplete?.();
193
+ }
194
+
195
+ if (msg.toolCall?.functionCalls?.length) {
196
+ const calls: RealtimeToolCall[] = msg.toolCall.functionCalls.map((fc: any) => ({
197
+ id: fc.id ?? "",
198
+ name: fc.name ?? "",
199
+ args: (fc.args ?? {}) as Record<string, unknown>,
200
+ }));
201
+ cb.onToolCall?.(calls);
202
+ }
203
+
204
+ if (msg.toolCallCancellation?.ids?.length) {
205
+ cb.onToolCallCancellation?.(msg.toolCallCancellation.ids);
206
+ }
207
+
208
+ if (msg.usageMetadata) {
209
+ const u = msg.usageMetadata;
210
+ cb.onUsage?.({
211
+ inputTokens: Number(u.promptTokenCount ?? 0),
212
+ outputTokens: Number(u.responseTokenCount ?? 0),
213
+ totalTokens: Number(u.totalTokenCount ?? 0),
214
+ });
215
+ }
216
+
217
+ if (msg.sessionResumptionUpdate?.newHandle) {
218
+ cb.onResumptionHandle?.(msg.sessionResumptionUpdate.newHandle);
219
+ }
220
+
221
+ if (msg.goAway) {
222
+ cb.onGoAway?.(parseDuration(msg.goAway.timeLeft));
223
+ }
224
+ }
225
+
226
+ /** `timeLeft` llega como duración protobuf ("9.5s") o como objeto {seconds,nanos}. */
227
+ function parseDuration(value: unknown): number {
228
+ if (typeof value === "string") {
229
+ const seconds = parseFloat(value.replace(/s$/, ""));
230
+ return Number.isFinite(seconds) ? Math.round(seconds * 1000) : 0;
231
+ }
232
+ if (value && typeof value === "object") {
233
+ const v = value as { seconds?: number | string; nanos?: number };
234
+ const seconds = Number(v.seconds ?? 0);
235
+ return Math.round(seconds * 1000 + (v.nanos ?? 0) / 1e6);
236
+ }
237
+ return 0;
238
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Registro de proveedores de voz en tiempo real.
3
+ *
4
+ * Hoy sólo Gemini Live. La interfaz existe desacoplada para que añadir OpenAI
5
+ * Realtime o Nova Sonic no obligue a tocar el gateway ni el cliente: cambia el
6
+ * adaptador, no el transporte.
7
+ */
8
+
9
+ import { GeminiLiveProvider } from "./gemini-live.ts";
10
+ import type { RealtimeProvider } from "./interface.ts";
11
+
12
+ export * from "./interface.ts";
13
+ export { DEFAULT_GEMINI_LIVE_MODEL } from "./gemini-live.ts";
14
+
15
+ const providers = new Map<string, RealtimeProvider>([["gemini", new GeminiLiveProvider()]]);
16
+
17
+ export function getRealtimeProvider(providerId: string): RealtimeProvider {
18
+ const provider = providers.get(providerId);
19
+ if (!provider) {
20
+ throw new Error(
21
+ `El proveedor "${providerId}" no soporta voz en tiempo real. Disponibles: ${[...providers.keys()].join(", ")}`,
22
+ );
23
+ }
24
+ return provider;
25
+ }
26
+
27
+ export function isRealtimeProvider(providerId: string): boolean {
28
+ return providers.has(providerId);
29
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * realtime-providers — sesiones de voz full-duplex (speech-to-speech).
3
+ *
4
+ * `LLMProvider` (../llm-providers/interface.ts) es one-shot: `call()` recibe un
5
+ * historial y devuelve una respuesta. Una sesión Live no encaja ahí — es un
6
+ * socket que vive minutos, recibe audio continuo y emite audio continuo. De ahí
7
+ * esta interfaz hermana en vez de una subclase.
8
+ *
9
+ * El modelo realtime NO es el cerebro de Hive: sólo oye, habla e interrumpe. El
10
+ * trabajo real lo hace el agent-loop de siempre (Bee → especialistas → tools).
11
+ * Por eso las herramientas que se declaran acá son un puñado fijo de funciones
12
+ * puente (gateway/realtime/bridge-tools.ts) y no el catálogo del agente: la Live
13
+ * API congela las tools en el `setup` y no admite cambiarlas a mitad de sesión.
14
+ */
15
+
16
+ import type { LLMToolDef } from "../llm-client.ts";
17
+
18
+ /** Lo que el micrófono debe entregar: PCM16 mono little-endian. */
19
+ export const REALTIME_INPUT_SAMPLE_RATE = 16_000;
20
+ /** Lo que el modelo devuelve: PCM16 mono. */
21
+ export const REALTIME_OUTPUT_SAMPLE_RATE = 24_000;
22
+
23
+ export interface RealtimeToolCall {
24
+ id: string;
25
+ name: string;
26
+ args: Record<string, unknown>;
27
+ }
28
+
29
+ export interface RealtimeCallbacks {
30
+ /** Audio del modelo, PCM16 mono a REALTIME_OUTPUT_SAMPLE_RATE. */
31
+ onAudio?: (pcm: Buffer) => void;
32
+ /** Transcripción de lo que dijo el usuario (fragmentos incrementales). */
33
+ onInputTranscript?: (text: string) => void;
34
+ /** Transcripción de lo que dijo el modelo (fragmentos incrementales). */
35
+ onOutputTranscript?: (text: string) => void;
36
+ onToolCall?: (calls: RealtimeToolCall[]) => void;
37
+ /** El modelo canceló tool calls ya emitidas (el usuario cambió de tema). */
38
+ onToolCallCancellation?: (ids: string[]) => void;
39
+ /** Barge-in: el usuario habló encima. Hay que vaciar la cola de reproducción YA. */
40
+ onInterrupted?: () => void;
41
+ onTurnComplete?: () => void;
42
+ /** Consumo acumulado que reporta el proveedor. */
43
+ onUsage?: (usage: { inputTokens: number; outputTokens: number; totalTokens: number }) => void;
44
+ /** Handle para reanudar la sesión tras un corte (válido ~2 h). */
45
+ onResumptionHandle?: (handle: string) => void;
46
+ /** El servidor va a cerrar la conexión; hay que reconectar con el handle. */
47
+ onGoAway?: (timeLeftMs: number) => void;
48
+ onError?: (error: Error) => void;
49
+ onClose?: (reason: string) => void;
50
+ }
51
+
52
+ export interface RealtimeSessionOptions {
53
+ model: string;
54
+ apiKey: string;
55
+ /** Prompt de la sesión de voz (no es el system prompt del agent-loop). */
56
+ systemInstruction: string;
57
+ /** Funciones puente. Congeladas: la Live API no permite cambiarlas después. */
58
+ tools?: LLMToolDef[];
59
+ /** Voz del proveedor (ej. "Kore"). Ver voiceService.getGeminiVoices(). */
60
+ voice?: string;
61
+ /**
62
+ * Idioma con acento regional, en BCP-47 (es-CO, es-MX, en-US…).
63
+ *
64
+ * Los modelos de audio nativo cambian de idioma solos siguiendo al usuario,
65
+ * pero sin esto eligen el acento por su cuenta: un usuario colombiano puede
66
+ * terminar escuchando español rioplatense.
67
+ */
68
+ language?: string;
69
+ /** Silencio en ms que cierra el turno del usuario (VAD automático). */
70
+ silenceDurationMs?: number;
71
+ /**
72
+ * Cuánto cuesta que el modelo dé por empezada una intervención del usuario.
73
+ * `START_SENSITIVITY_LOW` sube el listón: hace falta voz clara para
74
+ * interrumpirle, en vez de cualquier ruido. Es lo que salva las manos libres,
75
+ * donde lo que entra por el micrófono incluye su propia voz rebotando.
76
+ */
77
+ startOfSpeechSensitivity?: "START_SENSITIVITY_LOW" | "START_SENSITIVITY_HIGH";
78
+ /** Handle devuelto por una sesión anterior, para retomar el hilo. */
79
+ resumptionHandle?: string;
80
+ callbacks: RealtimeCallbacks;
81
+ }
82
+
83
+ export interface RealtimeSession {
84
+ readonly model: string;
85
+ /** PCM16 mono a REALTIME_INPUT_SAMPLE_RATE. */
86
+ sendAudio(pcm: ArrayBufferLike): void;
87
+ /**
88
+ * Un fotograma de la cámara, ya codificado (JPEG en base64).
89
+ *
90
+ * Va a baja cadencia a propósito: con vídeo, la Live API recorta la sesión de
91
+ * 15 minutos a unos 2 y cada imagen cuesta cientos de tokens frente a los 25
92
+ * por segundo del audio.
93
+ */
94
+ sendVideoFrame(base64: string, mimeType: string): void;
95
+ /** Inyecta texto en la conversación (narración de la colmena, avisos). */
96
+ sendText(text: string): void;
97
+ sendToolResult(id: string, name: string, result: Record<string, unknown>): void;
98
+ close(): void;
99
+ readonly closed: boolean;
100
+ }
101
+
102
+ export interface RealtimeProvider {
103
+ readonly id: string;
104
+ connect(options: RealtimeSessionOptions): Promise<RealtimeSession>;
105
+ }
106
+
107
+ /** Toolset fijo de una sesión de voz, en el formato canónico de Hive. */
108
+ export type RealtimeToolset = LLMToolDef[];