@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.
- package/CHANGELOG.md +167 -0
- package/README.md +11 -1
- package/package.json +26 -10
- package/packages/core/src/agent/acceptance-checks.ts +16 -10
- package/packages/core/src/agent/agent-catalog.ts +3 -3
- package/packages/core/src/agent/agent-loop.ts +115 -26
- package/packages/core/src/agent/capability-search.ts +2 -2
- package/packages/core/src/agent/catalog-selector.ts +4 -4
- package/packages/core/src/agent/compaction.ts +10 -9
- package/packages/core/src/agent/context-compiler.ts +58 -34
- package/packages/core/src/agent/conversation-store.ts +31 -7
- package/packages/core/src/agent/curator.ts +4 -4
- package/packages/core/src/agent/delegation-runtime.ts +5 -5
- package/packages/core/src/agent/goal-runner.ts +9 -9
- package/packages/core/src/agent/index.ts +1 -0
- package/packages/core/src/agent/llm-client.ts +98 -37
- package/packages/core/src/agent/llm-providers/anthropic.ts +4 -4
- package/packages/core/src/agent/llm-providers/deepseek.ts +1 -1
- package/packages/core/src/agent/llm-providers/gemini.ts +4 -4
- package/packages/core/src/agent/llm-providers/groq.ts +1 -1
- package/packages/core/src/agent/llm-providers/hiveagents.ts +3 -3
- package/packages/core/src/agent/llm-providers/interface.ts +2 -2
- package/packages/core/src/agent/llm-providers/kimi.ts +1 -1
- package/packages/core/src/agent/llm-providers/minimax.ts +1 -1
- package/packages/core/src/agent/llm-providers/mistral.ts +1 -1
- package/packages/core/src/agent/llm-providers/modelscope.ts +1 -1
- package/packages/core/src/agent/llm-providers/nvidia.ts +1 -1
- package/packages/core/src/agent/llm-providers/ollama.ts +4 -4
- package/packages/core/src/agent/llm-providers/openai-compat-base.ts +9 -5
- package/packages/core/src/agent/llm-providers/openai.ts +1 -1
- package/packages/core/src/agent/llm-providers/opencode-go.ts +1 -1
- package/packages/core/src/agent/llm-providers/openrouter.ts +1 -1
- package/packages/core/src/agent/llm-providers/qwen.ts +1 -1
- package/packages/core/src/agent/llm-providers/z-ai.ts +1 -1
- package/packages/core/src/agent/mcp-result-normalizer.ts +192 -0
- package/packages/core/src/agent/playbook-selector.ts +4 -4
- package/packages/core/src/agent/prompt-builder.ts +5 -5
- package/packages/core/src/agent/proof-packet.ts +5 -5
- package/packages/core/src/agent/providers/index.ts +4 -4
- package/packages/core/src/agent/realtime-providers/gemini-live.ts +238 -0
- package/packages/core/src/agent/realtime-providers/index.ts +29 -0
- package/packages/core/src/agent/realtime-providers/interface.ts +108 -0
- package/packages/core/src/agent/reflector.ts +6 -6
- package/packages/core/src/agent/run-store.ts +8 -8
- package/packages/core/src/agent/service.ts +10 -10
- package/packages/core/src/agent/skill-selector.ts +6 -6
- package/packages/core/src/agent/thread-id.ts +71 -0
- package/packages/core/src/agent/thread-store.ts +250 -0
- package/packages/core/src/agent/tool-selector.ts +7 -5
- package/packages/core/src/agent/tracer.ts +5 -5
- package/packages/core/src/api/createAgent.ts +1 -1
- package/packages/core/src/artifacts/store.ts +84 -3
- package/packages/core/src/canvas/emitter.ts +2 -2
- package/packages/core/src/channels/telegram.ts +1 -1
- package/packages/core/src/channels/webchat.ts +1 -1
- package/packages/core/src/config/loader.ts +15 -1
- package/packages/core/src/events/agent-bus.ts +3 -3
- package/packages/core/src/events/channel-narration.ts +3 -3
- package/packages/core/src/events/event-bus.ts +1 -1
- package/packages/core/src/events/narration.ts +3 -3
- package/packages/core/src/gateway/delegation-groups.ts +4 -4
- package/packages/core/src/gateway/durable-queue.ts +5 -5
- package/packages/core/src/gateway/job-store.ts +5 -5
- package/packages/core/src/gateway/notification-inbox.ts +2 -2
- package/packages/core/src/gateway/server.ts +2 -2
- package/packages/core/src/mcp/MCPClient.ts +3 -3
- package/packages/core/src/mcp/hot-reload.ts +5 -5
- package/packages/core/src/mcp/tool-sync.ts +5 -5
- package/packages/core/src/mcp/transports/index.ts +2 -2
- package/packages/core/src/mcp/transports/sse.ts +1 -1
- package/packages/core/src/models/index.ts +36 -0
- package/packages/core/src/multimodal/index.ts +2 -2
- package/packages/core/src/multimodal/vision-service.ts +6 -6
- package/packages/core/src/plugins/loader.ts +4 -1
- package/packages/core/src/resilience/circuit-breaker.ts +16 -5
- package/packages/core/src/resilience/retry.ts +1 -1
- package/packages/core/src/scheduler/CronScheduler.ts +6 -6
- package/packages/core/src/scheduler/integration.ts +9 -9
- package/packages/core/src/sessions/index.ts +266 -0
- package/packages/core/src/storage/bootstrap.ts +34 -8
- package/packages/core/src/storage/causal-events.ts +1 -1
- package/packages/core/src/storage/collections.ts +32 -1
- package/packages/core/src/storage/crypto.ts +16 -2
- package/packages/core/src/storage/hive.ts +1 -1
- package/packages/core/src/storage/hivedb.ts +10 -1
- package/packages/core/src/storage/onboarding.ts +6 -6
- package/packages/core/src/storage/reconcile.ts +5 -5
- package/packages/core/src/storage/seed.ts +103 -13
- package/packages/core/src/storage/usage.ts +3 -3
- package/packages/core/src/swarm/AgentExecutor.ts +2 -2
- package/packages/core/src/swarm/Coordinator.ts +8 -8
- package/packages/core/src/swarm/EventBridge.ts +2 -2
- package/packages/core/src/swarm/RoleSwarm.ts +234 -0
- package/packages/core/src/swarm/TaskGraph.ts +2 -2
- package/packages/core/src/swarm/index.ts +7 -0
- package/packages/core/src/swarm/presets/HiveLearnPreset.ts +2 -2
- package/packages/core/src/swarm/presets/ResearchPreset.ts +2 -2
- package/packages/core/src/swarm/strategies/ParallelStrategy.ts +1 -1
- package/packages/core/src/swarm/strategies/PriorityStrategy.ts +3 -3
- package/packages/core/src/tools/ToolExecutor.ts +7 -3
- package/packages/core/src/tools/core/index.ts +2 -2
- package/packages/core/src/tools/cron/index.ts +4 -4
- package/packages/core/src/tools/web/artifact-inspect.ts +2 -2
- package/packages/core/src/tools/web/artifact-read.ts +162 -0
- package/packages/core/src/tools/web/browser-backend.ts +226 -0
- package/packages/core/src/tools/web/browser-click.ts +2 -2
- package/packages/core/src/tools/web/browser-extract.ts +2 -2
- package/packages/core/src/tools/web/browser-navigate.ts +2 -2
- package/packages/core/src/tools/web/browser-screenshot.ts +12 -5
- package/packages/core/src/tools/web/browser-script.ts +2 -2
- package/packages/core/src/tools/web/browser-service.ts +85 -366
- package/packages/core/src/tools/web/browser-session.ts +125 -0
- package/packages/core/src/tools/web/browser-type.ts +2 -2
- package/packages/core/src/tools/web/browser-wait.ts +2 -2
- package/packages/core/src/tools/web/computer-use.ts +553 -0
- package/packages/core/src/tools/web/index.ts +8 -1
- package/packages/core/src/tools/web/webview-backend.ts +851 -0
- package/packages/core/src/utils/index.ts +1 -0
- package/packages/core/src/utils/logger.ts +12 -4
- package/packages/core/src/utils/redact-binary.ts +17 -0
- package/packages/core/src/utils/toon.ts +1 -1
- package/packages/core/src/voice/index.ts +6 -6
- package/bun.lock +0 -833
- package/bunfig.toml +0 -9
- package/docs/API-AGENTS.md +0 -367
- package/docs/API-CONTEXT-COMPILER.md +0 -249
- package/docs/API-DAG-SCHEDULER.md +0 -273
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +0 -446
- package/docs/API-WORKERS-EVENTS.md +0 -299
- package/docs/HIVE-HARNESS.md +0 -113
- package/docs/INDEX.md +0 -190
- package/docs/TEMPLATE-HIVE-APP.md +0 -360
- package/packages/cli/package.json +0 -17
- package/packages/cli/src/commands/create-app.test.ts +0 -180
- package/packages/core/package.json +0 -70
- package/packages/core/src/api/createAgent.test.ts +0 -160
- package/packages/core/src/canvas/canvas.test.ts +0 -36
- package/packages/core/src/channels/channels.test.ts +0 -18
- package/packages/core/src/ethics/EthicsGuard.test.ts +0 -108
- package/packages/core/src/gateway/gateway.test.ts +0 -38
- package/packages/core/src/memory/Scratchpad.test.ts +0 -68
- package/packages/core/src/scheduler/scheduler.test.ts +0 -15
- package/packages/core/src/skills/skills.test.ts +0 -62
- package/packages/core/src/swarm/swarm.test.ts +0 -24
- package/packages/core/src/tool-runtime/tool-runtime.test.ts +0 -99
- package/packages/core/src/tools/ToolRegistry.test.ts +0 -98
- package/packages/core/src/tools/api/api-request.test.ts +0 -164
- package/packages/core/src/tools/web/browser-service.test.ts +0 -83
- package/packages/core/src/workers/workers.test.ts +0 -41
- package/scripts/bump-version.ts +0 -248
- package/scripts/generate-skill-bundle.ts +0 -108
- package/test/agent-loop-terminal-synthesis.test.ts +0 -32
- package/test/catalog-agents-stay-enabled.test.ts +0 -117
- package/test/causal-events.test.ts +0 -117
- package/test/compaction.test.ts +0 -105
- package/test/context-compiler.test.ts +0 -269
- package/test/curator.test.ts +0 -130
- package/test/durable-queue.test.ts +0 -114
- package/test/harness-barrel.test.ts +0 -64
- package/test/hive-helpers.test.ts +0 -130
- package/test/hivedb-search.test.ts +0 -189
- package/test/internal-turns.test.ts +0 -166
- package/test/job-idempotency.test.ts +0 -68
- package/test/job-retry-backoff.test.ts +0 -184
- package/test/job-store.test.ts +0 -381
- package/test/llm-retry.test.ts +0 -97
- package/test/memory-perf.test.ts +0 -774
- package/test/minimal-loadout.test.ts +0 -78
- package/test/model-catalog.test.ts +0 -105
- package/test/preload.ts +0 -12
- package/test/reflector.test.ts +0 -320
- package/test/retention-cap.test.ts +0 -91
- package/test/retired-capabilities-pruned.test.ts +0 -192
- package/test/run-store.test.ts +0 -355
- package/test/scratchpad.test.ts +0 -74
- package/test/secrets-durability.test.ts +0 -119
- package/test/seed-model-reseed.test.ts +0 -155
- package/test/setup-agent-seed.test.ts +0 -264
- package/test/tool-inventory.test.ts +0 -65
- package/test/tool-runtime.test.ts +0 -258
- package/test/toon.test.ts +0 -429
- package/tsconfig.json +0 -42
|
@@ -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[];
|