@johpaz/hive-sdk 0.1.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +441 -0
- package/README.md +21 -3
- package/package.json +27 -5
- package/packages/core/src/agent/acceptance-checks.ts +9 -9
- package/packages/core/src/agent/agent-catalog.ts +82 -25
- 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 +30 -10
- package/packages/core/src/agent/context-compiler.ts +63 -36
- package/packages/core/src/agent/conversation-store.ts +167 -9
- package/packages/core/src/agent/curator.ts +16 -7
- 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 +40 -1
- package/packages/core/src/agent/llm-providers/ollama.ts +4 -4
- package/packages/core/src/agent/llm-providers/openai-compat-base.ts +47 -7
- 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 +22 -7
- 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 +39 -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 +38 -15
- 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 +293 -0
- package/packages/core/src/agent/tool-selector.ts +9 -5
- package/packages/core/src/agent/tracer.ts +5 -5
- package/packages/core/src/api/createAgent.ts +67 -2
- package/packages/core/src/artifacts/index.ts +15 -0
- package/packages/core/src/artifacts/store.ts +161 -5
- package/packages/core/src/canvas/emitter.ts +2 -2
- package/packages/core/src/canvas/index.ts +9 -0
- 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 +14 -5
- package/packages/core/src/ethics/EthicsGuard.ts +7 -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/index.ts +18 -0
- package/packages/core/src/events/narration.ts +3 -3
- package/packages/core/src/events/tool-narration.ts +4 -0
- package/packages/core/src/gateway/channel-notify.ts +103 -6
- package/packages/core/src/gateway/delegation-groups.ts +4 -4
- package/packages/core/src/gateway/durable-queue.ts +18 -6
- package/packages/core/src/gateway/index.ts +3 -0
- package/packages/core/src/gateway/job-store.ts +11 -5
- package/packages/core/src/gateway/notification-inbox.ts +2 -2
- package/packages/core/src/gateway/server.ts +2 -2
- package/packages/core/src/harness/executors.ts +493 -0
- package/packages/core/src/harness/index.ts +12 -2
- package/packages/core/src/hooks/index.ts +203 -0
- package/packages/core/src/images/index.ts +161 -0
- package/packages/core/src/index.ts +1 -0
- 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 +51 -19
- package/packages/core/src/plugins/loader.ts +4 -1
- package/packages/core/src/resilience/circuit-breaker.ts +16 -5
- package/packages/core/src/resilience/index.ts +13 -0
- package/packages/core/src/resilience/retry.ts +1 -1
- package/packages/core/src/scheduler/CronScheduler.ts +54 -27
- package/packages/core/src/scheduler/cron/expression.ts +165 -0
- package/packages/core/src/scheduler/cron/index.ts +10 -0
- package/packages/core/src/scheduler/cron/job.ts +339 -0
- package/packages/core/src/scheduler/cron/next-run.ts +121 -0
- package/packages/core/src/scheduler/cron/zoned-time.ts +138 -0
- package/packages/core/src/scheduler/index.ts +21 -3
- package/packages/core/src/scheduler/integration.ts +25 -14
- package/packages/core/src/scheduler/types.ts +3 -18
- package/packages/core/src/services/agents.ts +268 -0
- package/packages/core/src/services/cron.ts +257 -0
- package/packages/core/src/services/endpoints.ts +289 -0
- package/packages/core/src/services/ethics.ts +107 -0
- package/packages/core/src/services/images.ts +212 -0
- package/packages/core/src/services/index.ts +112 -0
- package/packages/core/src/services/mcp.ts +201 -0
- package/packages/core/src/services/memory.ts +133 -0
- package/packages/core/src/services/models.ts +179 -0
- package/packages/core/src/services/providers.ts +152 -0
- package/packages/core/src/services/setup.ts +222 -0
- package/packages/core/src/services/skills.ts +241 -0
- package/packages/core/src/services/swarms.ts +307 -0
- package/packages/core/src/services/tools.ts +106 -0
- package/packages/core/src/sessions/index.ts +268 -0
- package/packages/core/src/sessions/resolve.ts +108 -0
- package/packages/core/src/skills/SkillLoader.ts +8 -1
- package/packages/core/src/skills/bundled/artifacts/artifact_reader/SKILL.md +105 -0
- package/packages/core/src/skills/bundled/cron_manager/SKILL.md +21 -11
- package/packages/core/src/skills/bundled/images/image_editor/SKILL.md +120 -0
- package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +12 -3
- package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +22 -7
- package/packages/core/src/skills/bundled-data.generated.ts +110 -12
- package/packages/core/src/storage/bootstrap.ts +107 -12
- package/packages/core/src/storage/causal-events.ts +1 -1
- package/packages/core/src/storage/collections.ts +138 -2
- package/packages/core/src/storage/crypto.ts +35 -4
- package/packages/core/src/storage/hive.ts +1 -1
- package/packages/core/src/storage/hivedb.ts +10 -1
- package/packages/core/src/storage/index.ts +2 -1
- package/packages/core/src/storage/onboarding.ts +61 -45
- package/packages/core/src/storage/reconcile.ts +11 -6
- package/packages/core/src/storage/seed.ts +191 -23
- 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/swarm/types.ts +3 -18
- package/packages/core/src/tool-runtime/embedded-worker.generated.ts +21 -0
- package/packages/core/src/tool-runtime/index.ts +129 -14
- package/packages/core/src/tools/ToolExecutor.ts +7 -3
- package/packages/core/src/tools/agents/index.ts +18 -60
- package/packages/core/src/tools/cli/index.ts +55 -0
- package/packages/core/src/tools/core/index.ts +52 -4
- package/packages/core/src/tools/cron/index.ts +8 -8
- package/packages/core/src/tools/images/index.ts +130 -0
- package/packages/core/src/tools/index.ts +14 -1
- package/packages/core/src/tools/office/office-escribir-xlsx.ts +2 -1
- package/packages/core/src/tools/office/office-leer-xlsx.ts +2 -1
- package/packages/core/src/tools/office/xlsx-loader.ts +19 -0
- 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 +141 -44
- 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 +63 -384
- 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 +460 -21
- 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 -859
- 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/acceptance-checks.test.ts +0 -403
- package/test/agent-loop-terminal-synthesis.test.ts +0 -32
- package/test/browser-backend.test.ts +0 -308
- 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/tool-selector-runtime-tools.test.ts +0 -117
- package/test/toon.test.ts +0 -429
- package/tsconfig.json +0 -42
|
@@ -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
|
|
|
@@ -39,20 +39,33 @@ const MAX_RULES_PER_TURN = 5
|
|
|
39
39
|
*/
|
|
40
40
|
const RELEVANCE_RATIO = 0.3
|
|
41
41
|
|
|
42
|
+
/** Cuántos candidatos se piden de más para sobrevivir al filtro por usuario. */
|
|
43
|
+
const OVERFETCH_FACTOR = 4
|
|
44
|
+
|
|
42
45
|
// ─── Selection Logic ───────────────────────────────────────────────────────────
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
|
-
* Select relevant rules from the Playbook based on semantic matching
|
|
48
|
+
* Select relevant rules from the Playbook based on semantic matching.
|
|
49
|
+
*
|
|
50
|
+
* `userId` acota lo aprendido a quien corresponde: una regla se aplica si es
|
|
51
|
+
* global (`user_id === ""`, el conocimiento sembrado con el producto) o si
|
|
52
|
+
* salió de las trazas de este mismo usuario. Sin este filtro, lo que el agente
|
|
53
|
+
* aprende hablando con una persona termina inyectado en el prompt de otra.
|
|
54
|
+
* Omitirlo devuelve sólo las reglas globales.
|
|
46
55
|
*/
|
|
47
|
-
export async function selectPlaybookRules(message: string): Promise<PlaybookRule[]> {
|
|
56
|
+
export async function selectPlaybookRules(message: string, userId?: string): Promise<PlaybookRule[]> {
|
|
48
57
|
const startTime = performance.now()
|
|
49
58
|
|
|
50
59
|
if (!message.trim()) return []
|
|
51
60
|
|
|
52
61
|
try {
|
|
62
|
+
// El índice BM25 es uno solo para todos los usuarios, así que pedir k
|
|
63
|
+
// resultados y filtrar después dejaría a un usuario sin reglas cuando
|
|
64
|
+
// las mejor puntuadas son de otro. Se pide un pozo más ancho y se
|
|
65
|
+
// recorta a MAX_RULES_PER_TURN recién después de filtrar.
|
|
53
66
|
const hits = await searchCapabilities(message, {
|
|
54
67
|
types: ["playbook"],
|
|
55
|
-
k: MAX_RULES_PER_TURN,
|
|
68
|
+
k: MAX_RULES_PER_TURN * OVERFETCH_FACTOR,
|
|
56
69
|
})
|
|
57
70
|
|
|
58
71
|
const relevantIds = applyRelativeCutoff(hits, RELEVANCE_RATIO).map(h => h.rawId)
|
|
@@ -64,6 +77,8 @@ export async function selectPlaybookRules(message: string): Promise<PlaybookRule
|
|
|
64
77
|
const entries = await Promise.all(relevantIds.map(id => playbookCol.get(id)))
|
|
65
78
|
const rules: PlaybookRule[] = entries
|
|
66
79
|
.filter((e): e is NonNullable<typeof e> => !!e && e.doc.active)
|
|
80
|
+
.filter(e => e.doc.user_id === "" || e.doc.user_id === (userId ?? ""))
|
|
81
|
+
.slice(0, MAX_RULES_PER_TURN)
|
|
67
82
|
.map(e => ({
|
|
68
83
|
id: e.id,
|
|
69
84
|
rule: e.doc.rule,
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
* - Skills activos
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { col } from "../storage/hive"
|
|
16
|
+
import { col } from "../storage/hive.ts"
|
|
17
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"
|
|
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
|
|
|
@@ -136,7 +136,7 @@ export async function buildSystemPrompt(opts: BuildSystemPromptOpts): Promise<st
|
|
|
136
136
|
if (Object.keys(userData).length > 0) {
|
|
137
137
|
userSection += formatContext(userData) + "\n\n"
|
|
138
138
|
if (agent.role === "coordinator" && user.email) {
|
|
139
|
-
userSection += `Cuando el usuario diga "envíame", "mándame" o "a mi correo" sin indicar otro destinatario,
|
|
139
|
+
userSection += `Cuando el usuario diga "envíame", "mándame" o "a mi correo" sin indicar otro destinatario, usa CorreoPropio. Para terceras personas, resuelve su dirección por separado.\n\n`
|
|
140
140
|
}
|
|
141
141
|
} else {
|
|
142
142
|
userSection += `Usuario ID: ${userId}\n\n`
|
|
@@ -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,11 @@
|
|
|
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, buildAgentLoop } 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
|
+
import type { MCPClientManager } from "../../mcp/index.ts"
|
|
14
15
|
|
|
15
16
|
export type Provider = "openai" | "anthropic" | "gemini" | "mistral" | "kimi" | "ollama" | "openrouter" | "deepseek" | "nvidia" | "hiveagents" | "z-ai" | "modelscope" | "minimax" | "qwen" | "groq" | "opencode-go"
|
|
16
17
|
|
|
@@ -62,6 +63,34 @@ export interface ModelResponse {
|
|
|
62
63
|
totalTokens: number
|
|
63
64
|
}
|
|
64
65
|
finishReason?: string
|
|
66
|
+
/** Image artifacts (mcp-result-normalizer.ts, agent-loop.ts's turnImageArtifacts) produced by tools this turn. */
|
|
67
|
+
imageArtifacts?: Array<{ artifactId: string; mimeType: string }>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Crea un `AgentRunner` listo para usar.
|
|
72
|
+
*
|
|
73
|
+
* `AgentRunner.generate()` necesita que el loop global exista
|
|
74
|
+
* (`getAgentLoop()`), y construirlo es un paso aparte que hay que recordar:
|
|
75
|
+
* `new AgentRunner(config)` a secas compila, se instancia sin quejarse y falla
|
|
76
|
+
* recién en la primera llamada con "AgentLoop not initialized". En hive ese paso
|
|
77
|
+
* lo hace su initializer; en el SDK no lo hacía nadie, así que la clase estaba
|
|
78
|
+
* exportada pero no era utilizable.
|
|
79
|
+
*
|
|
80
|
+
* Esta fábrica hace las dos cosas en el orden correcto. Es idempotente: llamarla
|
|
81
|
+
* dos veces reconstruye el loop con el manager que se le pase.
|
|
82
|
+
*
|
|
83
|
+
* ```ts
|
|
84
|
+
* const runner = await createAgentRunner(loadConfig(), { mcpManager });
|
|
85
|
+
* const res = await runner.generate({ messages: [...] });
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export async function createAgentRunner(
|
|
89
|
+
config: Config,
|
|
90
|
+
opts: { mcpManager?: MCPClientManager | null } = {},
|
|
91
|
+
): Promise<AgentRunner> {
|
|
92
|
+
buildAgentLoop({ mcpManager: opts.mcpManager ?? null })
|
|
93
|
+
return new AgentRunner(config)
|
|
65
94
|
}
|
|
66
95
|
|
|
67
96
|
export class AgentRunner {
|
|
@@ -87,6 +116,7 @@ export class AgentRunner {
|
|
|
87
116
|
throw new Error("AgentLoop not initialized")
|
|
88
117
|
}
|
|
89
118
|
|
|
119
|
+
let imageArtifacts: ModelResponse["imageArtifacts"]
|
|
90
120
|
let lastAgentContent = ""
|
|
91
121
|
let accumulatedAgentContent = "" // Accumulate content from all agent chunks
|
|
92
122
|
let toolCalls: ModelResponse["toolCalls"] = []
|
|
@@ -190,6 +220,10 @@ export class AgentRunner {
|
|
|
190
220
|
totalInputTokens += chunk.usage.input_tokens
|
|
191
221
|
totalOutputTokens += chunk.usage.output_tokens
|
|
192
222
|
}
|
|
223
|
+
|
|
224
|
+
if (chunk.artifacts?.images?.length) {
|
|
225
|
+
imageArtifacts = [...(imageArtifacts ?? []), ...chunk.artifacts.images]
|
|
226
|
+
}
|
|
193
227
|
}
|
|
194
228
|
|
|
195
229
|
logger.debug(`[STREAM] done. totalChunks=${chunkCount} lastAgentContent length=${lastAgentContent.length}, accumulated length=${accumulatedAgentContent.length}`)
|
|
@@ -208,6 +242,7 @@ export class AgentRunner {
|
|
|
208
242
|
totalTokens: totalInputTokens + totalOutputTokens,
|
|
209
243
|
},
|
|
210
244
|
finishReason: "stop",
|
|
245
|
+
imageArtifacts,
|
|
211
246
|
}
|
|
212
247
|
} catch (error) {
|
|
213
248
|
logger.error("AgentRunner error:", error)
|
|
@@ -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[];
|
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
* trick the SQLite version used — HiveDB has no equivalent primitive.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { logger } from "../utils/logger"
|
|
15
|
-
import { col, nextId } from "../storage/hive"
|
|
16
|
-
import { getHiveDb } from "../storage/hivedb"
|
|
17
|
-
import { loadConfig } from "../config/loader"
|
|
14
|
+
import { logger } from "../utils/logger.ts"
|
|
15
|
+
import { col, nextId } from "../storage/hive.ts"
|
|
16
|
+
import { getHiveDb } from "../storage/hivedb.ts"
|
|
17
|
+
import { loadConfig } from "../config/loader.ts"
|
|
18
18
|
import type { HiveDB, ToolStats } from "@johpaz/hive-db"
|
|
19
|
-
import type { TraceDoc, ReflectionDoc, CursorDoc } from "../storage/collections"
|
|
19
|
+
import type { TraceDoc, ReflectionDoc, CursorDoc } from "../storage/collections.ts"
|
|
20
|
+
import { parseThreadId } from "./thread-id.ts"
|
|
20
21
|
|
|
21
22
|
const log = logger.child("reflector")
|
|
22
23
|
|
|
@@ -51,25 +52,42 @@ export async function runReflector(): Promise<void> {
|
|
|
51
52
|
|
|
52
53
|
log.info(`[reflector] Analyzing ${traces.length} traces...`)
|
|
53
54
|
|
|
55
|
+
// Las trazas se agrupan por usuario antes de analizarlas.
|
|
56
|
+
//
|
|
57
|
+
// Antes se analizaba el lote entero de una vez, así que una reflexión —y la
|
|
58
|
+
// regla de playbook que sale de ella— mezclaba lo aprendido de varias
|
|
59
|
+
// personas. En una instalación de un solo usuario da igual; en un host
|
|
60
|
+
// multi-inquilino significa que lo aprendido de un workspace se le aplica a
|
|
61
|
+
// otro. El usuario sale del `thread_id`, que ya lo lleva dentro.
|
|
62
|
+
const porUsuario = new Map<string, typeof traceEntries>()
|
|
63
|
+
for (const entry of traceEntries) {
|
|
64
|
+
const usuario = parseThreadId(entry.doc.thread_id)?.userId ?? ""
|
|
65
|
+
const grupo = porUsuario.get(usuario) ?? []
|
|
66
|
+
grupo.push(entry)
|
|
67
|
+
porUsuario.set(usuario, grupo)
|
|
68
|
+
}
|
|
69
|
+
|
|
54
70
|
// G9: when enabled, per-tool insights use whole-history stats from HiveDB's
|
|
55
71
|
// event log (toolStats) instead of counters built from just this batch, and
|
|
56
72
|
// causal threads add root-cause/learning-proposal insights on top.
|
|
57
73
|
const causalDb = loadConfig().causalLog?.enabled ? await getHiveDb() : null
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
const insights = [...localInsights, ...causalInsights]
|
|
74
|
+
const reflectionsCol = await col<ReflectionDoc>("reflections")
|
|
75
|
+
let totalInsights = 0
|
|
61
76
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
const
|
|
77
|
+
for (const [usuario, entradas] of porUsuario) {
|
|
78
|
+
const delGrupo = entradas.map(e => e.doc)
|
|
79
|
+
const localInsights = await analyzeTracesLocally(delGrupo, causalDb)
|
|
80
|
+
const causalInsights = await analyzeCausalThreads(delGrupo, causalDb)
|
|
81
|
+
const insights = [...localInsights, ...causalInsights]
|
|
82
|
+
if (insights.length === 0) continue
|
|
67
83
|
|
|
84
|
+
const traceIds = JSON.stringify(entradas.map(e => e.id))
|
|
68
85
|
for (const insight of insights) {
|
|
69
86
|
const id = await nextId("reflections")
|
|
70
87
|
await reflectionsCol.put(id, {
|
|
71
88
|
id,
|
|
72
89
|
trace_ids: traceIds,
|
|
90
|
+
user_id: usuario,
|
|
73
91
|
insight_type: insight.type,
|
|
74
92
|
description: insight.description,
|
|
75
93
|
affected_tools: insight.affectedTools ? JSON.stringify(insight.affectedTools) : null,
|
|
@@ -78,8 +96,13 @@ export async function runReflector(): Promise<void> {
|
|
|
78
96
|
created_at: Date.now(),
|
|
79
97
|
})
|
|
80
98
|
}
|
|
99
|
+
totalInsights += insights.length
|
|
100
|
+
}
|
|
81
101
|
|
|
82
|
-
|
|
102
|
+
if (totalInsights === 0) {
|
|
103
|
+
log.debug("[reflector] No insights generated")
|
|
104
|
+
} else {
|
|
105
|
+
log.info(`[reflector] Generated ${totalInsights} insights across ${porUsuario.size} user(s)`)
|
|
83
106
|
}
|
|
84
107
|
|
|
85
108
|
// Advance the cursor regardless of whether insights were generated —
|
|
@@ -88,7 +111,7 @@ export async function runReflector(): Promise<void> {
|
|
|
88
111
|
await cursorsCol.put(CURSOR_ID, { value: newCursor }, cursorEntry ? { expectedVersion: cursorEntry.version } : { expectedVersion: 0 })
|
|
89
112
|
|
|
90
113
|
// Trigger curator
|
|
91
|
-
const { runCurator } = await import("./curator")
|
|
114
|
+
const { runCurator } = await import("./curator.ts")
|
|
92
115
|
await runCurator()
|
|
93
116
|
|
|
94
117
|
log.info(`[reflector] Reflection cycle completed successfully`)
|