@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
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
* should write to a run; single-writer pattern keeps contention minimal.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { col, updateDoc, nextId, toIndexable } from "../storage/hive";
|
|
14
|
-
import type { AgentRunDoc } from "../storage/collections";
|
|
15
|
-
import { getBootId } from "../storage/boot-id";
|
|
16
|
-
import { logger } from "../utils/logger";
|
|
17
|
-
import { loadConfig } from "../config/loader";
|
|
18
|
-
import type { LLMMessage } from "./llm-client";
|
|
19
|
-
import type { RunEpoch } from "./run-epoch";
|
|
20
|
-
import { formatInternalEvent } from "./conversation-store";
|
|
13
|
+
import { col, updateDoc, nextId, toIndexable } from "../storage/hive.ts";
|
|
14
|
+
import type { AgentRunDoc } from "../storage/collections.ts";
|
|
15
|
+
import { getBootId } from "../storage/boot-id.ts";
|
|
16
|
+
import { logger } from "../utils/logger.ts";
|
|
17
|
+
import { loadConfig } from "../config/loader.ts";
|
|
18
|
+
import type { LLMMessage } from "./llm-client.ts";
|
|
19
|
+
import type { RunEpoch } from "./run-epoch.ts";
|
|
20
|
+
import { formatInternalEvent } from "./conversation-store.ts";
|
|
21
21
|
|
|
22
22
|
const log = logger.child("run-store");
|
|
23
23
|
|
|
@@ -12,15 +12,15 @@
|
|
|
12
12
|
* - Eventos (cron, etc.)
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { logger } from "../utils/logger"
|
|
16
|
-
import { buildSystemPromptWithProjects } from "./prompt-builder"
|
|
17
|
-
import { getAgentLoop, rebuildAgentLoop } from "./agent-loop"
|
|
15
|
+
import { logger } from "../utils/logger.ts"
|
|
16
|
+
import { buildSystemPromptWithProjects } from "./prompt-builder.ts"
|
|
17
|
+
import { getAgentLoop, rebuildAgentLoop } from "./agent-loop.ts"
|
|
18
18
|
import type { MCPClientManager } from "../mcp/index.ts"
|
|
19
|
-
import { resolveAgentId, resolveUserId } from "../storage/onboarding"
|
|
20
|
-
import { getMCPManager as getSingletonMCPManager } from "../mcp/singleton"
|
|
21
|
-
import type { ContentPart } from "./llm-client"
|
|
22
|
-
import { col, fromIndexable } from "../storage/hive"
|
|
23
|
-
import type { AgentDoc, EthicsDoc } from "../storage/collections"
|
|
19
|
+
import { resolveAgentId, resolveUserId } from "../storage/onboarding.ts"
|
|
20
|
+
import { getMCPManager as getSingletonMCPManager } from "../mcp/singleton.ts"
|
|
21
|
+
import type { ContentPart } from "./llm-client.ts"
|
|
22
|
+
import { col, fromIndexable } from "../storage/hive.ts"
|
|
23
|
+
import type { AgentDoc, EthicsDoc } from "../storage/collections.ts"
|
|
24
24
|
|
|
25
25
|
const log = logger.child("agent-service")
|
|
26
26
|
|
|
@@ -161,7 +161,7 @@ export class AgentService {
|
|
|
161
161
|
*/
|
|
162
162
|
async reloadSkills(): Promise<void> {
|
|
163
163
|
log.info("Reloading skills...")
|
|
164
|
-
const { syncSkillsToIndex } = await import("./context-compiler")
|
|
164
|
+
const { syncSkillsToIndex } = await import("./context-compiler.ts")
|
|
165
165
|
await syncSkillsToIndex()
|
|
166
166
|
log.info("Skills reloaded")
|
|
167
167
|
}
|
|
@@ -252,7 +252,7 @@ export class AgentService {
|
|
|
252
252
|
* Ejecuta un agente con un mensaje
|
|
253
253
|
*/
|
|
254
254
|
async runAgent(message: string | ContentPart[], threadId: string, userId?: string): Promise<string> {
|
|
255
|
-
const { runAgentIsolated } = await import("./agent-loop")
|
|
255
|
+
const { runAgentIsolated } = await import("./agent-loop.ts")
|
|
256
256
|
const result = await runAgentIsolated({
|
|
257
257
|
agentId: this.agentId,
|
|
258
258
|
taskDescription: message,
|
|
@@ -16,17 +16,17 @@
|
|
|
16
16
|
* 5. Returns skill content for injection into system prompt
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { col } from "../storage/hive"
|
|
19
|
+
import { col } from "../storage/hive.ts"
|
|
20
20
|
import type { SkillDoc } from "../storage/collections"
|
|
21
|
-
import { logger } from "../utils/logger"
|
|
22
|
-
import { isMinimalSkill } from "./minimal-loadout"
|
|
23
|
-
import { isCalendarOperation } from "./routing-intent"
|
|
21
|
+
import { logger } from "../utils/logger.ts"
|
|
22
|
+
import { isMinimalSkill } from "./minimal-loadout.ts"
|
|
23
|
+
import { isCalendarOperation } from "./routing-intent.ts"
|
|
24
24
|
import {
|
|
25
25
|
searchCapabilities,
|
|
26
26
|
applyRelativeCutoff,
|
|
27
27
|
replaceCapabilityDocs,
|
|
28
28
|
type CapabilityDoc,
|
|
29
|
-
} from "./capability-search"
|
|
29
|
+
} from "./capability-search.ts"
|
|
30
30
|
|
|
31
31
|
const log = logger.child("skill-selector")
|
|
32
32
|
|
|
@@ -109,7 +109,7 @@ const STOPWORDS = new Set([
|
|
|
109
109
|
"más", "mas", "ya", "yo", "tu", "te", "ti", "mi", "me",
|
|
110
110
|
"hola", "hi", "hello", "hey", "gracias", "thank", "please",
|
|
111
111
|
"ok", "okay", "yes", "si", "no", "bien", "good", "great",
|
|
112
|
-
"puedes", "necesito", "quiero", "
|
|
112
|
+
"puedes", "necesito", "quiero", "necesitas", "quieres",
|
|
113
113
|
])
|
|
114
114
|
|
|
115
115
|
/** Conversational patterns that should return empty skill list */
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thread identity — `${userId}/${channel}/${peerId}`.
|
|
3
|
+
*
|
|
4
|
+
* Un hilo por canal Y por contacto: el chat privado de Telegram, cada grupo de
|
|
5
|
+
* WhatsApp y cada conversación de la web tienen historial propio. Antes había un
|
|
6
|
+
* único hilo por usuario (`threadId = userId`) compartido por todos los canales.
|
|
7
|
+
*
|
|
8
|
+
* Dos reglas de formato que no son cosméticas:
|
|
9
|
+
*
|
|
10
|
+
* 1. El separador es "/" y NO ":". Los mensajes viven en `conversations` con id
|
|
11
|
+
* `${threadId}:${seq}` y se leen por prefijo (`scan({ prefix: "${threadId}:" })`),
|
|
12
|
+
* sin índice secundario. El hilo legacy (id = userId pelado) se lee con prefijo
|
|
13
|
+
* `${userId}:`; si los hilos nuevos fueran `${userId}:webchat:...` ese scan se los
|
|
14
|
+
* tragaría enteros. Con "/" el historial viejo queda aislado sin migrar una fila.
|
|
15
|
+
*
|
|
16
|
+
* 2. Ningún segmento puede contener ":" — se sustituye por "_". Sin esto los peerIds
|
|
17
|
+
* compuestos rompen el aislamiento por prefijo: el DM de Telegram `12345` y el grupo
|
|
18
|
+
* `12345:678` (telegram.ts arma así el peerId de grupo) producirían los prefijos
|
|
19
|
+
* `.../12345:` y `.../12345:678:`, y el primero es prefijo del segundo — el DM leería
|
|
20
|
+
* los mensajes del grupo. Colapsar ":" hace que `${threadId}:` sea siempre un
|
|
21
|
+
* separador inequívoco, y mantiene válido el `lastIndexOf(":")` con que
|
|
22
|
+
* conversation-store extrae el número de secuencia.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export const THREAD_SEP = "/"
|
|
26
|
+
|
|
27
|
+
/** Canal sintético de las tareas programadas: un hilo estable por cron job. */
|
|
28
|
+
export const CRON_CHANNEL = "cron"
|
|
29
|
+
|
|
30
|
+
export interface ThreadParts {
|
|
31
|
+
userId: string
|
|
32
|
+
channel: string
|
|
33
|
+
peerId: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Deja un segmento apto para formar parte de un threadId: sin "/" (rompería el
|
|
38
|
+
* parseo) y sin ":" (rompería el aislamiento por prefijo — ver cabecera).
|
|
39
|
+
*/
|
|
40
|
+
export function sanitizeSegment(value: string): string {
|
|
41
|
+
return value.replace(/[/:]/g, "_").trim()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function makeThreadId(userId: string, channel: string, peerId: string): string {
|
|
45
|
+
const parts = [userId, channel, peerId].map((p) => sanitizeSegment(p || "") || "default")
|
|
46
|
+
return parts.join(THREAD_SEP)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Descompone un threadId con formato. Devuelve null para todo lo que no lo tenga:
|
|
51
|
+
* el hilo legacy (`userId` pelado), los hilos aislados de los workers
|
|
52
|
+
* (`task-<id>-<agente>`) y cualquier id heredado. Quien llame debe conservar su
|
|
53
|
+
* comportamiento anterior ante un null, nunca inventar partes.
|
|
54
|
+
*/
|
|
55
|
+
export function parseThreadId(threadId: string): ThreadParts | null {
|
|
56
|
+
if (!threadId) return null
|
|
57
|
+
const parts = threadId.split(THREAD_SEP)
|
|
58
|
+
if (parts.length !== 3) return null
|
|
59
|
+
const [userId, channel, peerId] = parts
|
|
60
|
+
if (!userId || !channel || !peerId) return null
|
|
61
|
+
return { userId, channel, peerId }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function isStructuredThreadId(threadId: string): boolean {
|
|
65
|
+
return parseThreadId(threadId) !== null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Id de una conversación nueva de la web. No lo elige el cliente. */
|
|
69
|
+
export function newWebConversationId(): string {
|
|
70
|
+
return `conv-${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`
|
|
71
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thread store — el registro de conversaciones (`conversationThreads`).
|
|
3
|
+
*
|
|
4
|
+
* `conversations` no tiene índice secundario: los mensajes se leen por prefijo del
|
|
5
|
+
* threadId. Esta colección es el catálogo que permite listar las conversaciones de
|
|
6
|
+
* un usuario, ponerles título y borrarlas sin escanear todos los mensajes.
|
|
7
|
+
*
|
|
8
|
+
* El id de cada fila ES el threadId. La única fila cuyo id no tiene formato
|
|
9
|
+
* `${user}/${canal}/${peer}` es la del hilo legacy anterior a la separación por
|
|
10
|
+
* canal (ver ensureLegacyThread).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { col, updateDoc } from "../storage/hive.ts"
|
|
14
|
+
import { logger } from "../utils/logger.ts"
|
|
15
|
+
import type { ConversationThreadDoc, ConversationDoc, SummaryDoc } from "../storage/collections.ts"
|
|
16
|
+
import { makeThreadId, parseThreadId, newWebConversationId } from "./thread-id.ts"
|
|
17
|
+
import { runSessionStart, runSessionEnd } from "../hooks/index.ts"
|
|
18
|
+
|
|
19
|
+
const log = logger.child("thread-store")
|
|
20
|
+
|
|
21
|
+
const TITLE_MAX_CHARS = 60
|
|
22
|
+
|
|
23
|
+
/** Reintentos del contador ante mensajes concurrentes del mismo hilo. */
|
|
24
|
+
const TOUCH_MAX_RETRIES = 5
|
|
25
|
+
|
|
26
|
+
export interface EnsureThreadInput {
|
|
27
|
+
userId: string
|
|
28
|
+
channel: string
|
|
29
|
+
peerId: string
|
|
30
|
+
peerKind?: "direct" | "group"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function threadsCol() {
|
|
34
|
+
return col<ConversationThreadDoc>("conversationThreads")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function getThread(threadId: string): Promise<ConversationThreadDoc | null> {
|
|
38
|
+
const c = await threadsCol()
|
|
39
|
+
const entry = await c.get(threadId)
|
|
40
|
+
return entry?.doc ?? null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Crea la fila de la conversación si aún no existe y devuelve su threadId.
|
|
45
|
+
* Idempotente: se llama en cada turno desde resolveContext.
|
|
46
|
+
*/
|
|
47
|
+
export async function ensureThread(input: EnsureThreadInput): Promise<string> {
|
|
48
|
+
const threadId = makeThreadId(input.userId, input.channel, input.peerId)
|
|
49
|
+
const c = await threadsCol()
|
|
50
|
+
const existing = await c.get(threadId)
|
|
51
|
+
if (existing) return threadId
|
|
52
|
+
|
|
53
|
+
const now = Date.now()
|
|
54
|
+
try {
|
|
55
|
+
await c.put(threadId, {
|
|
56
|
+
id: threadId,
|
|
57
|
+
user_id: input.userId,
|
|
58
|
+
channel: input.channel,
|
|
59
|
+
peer_id: input.peerId,
|
|
60
|
+
peer_kind: input.peerKind ?? "direct",
|
|
61
|
+
title: null,
|
|
62
|
+
archived: false,
|
|
63
|
+
created_at: now,
|
|
64
|
+
last_message_at: now,
|
|
65
|
+
message_count: 0,
|
|
66
|
+
}, { expectedVersion: 0 })
|
|
67
|
+
} catch {
|
|
68
|
+
// Otro turno del mismo canal la creó primero — ambos quieren lo mismo.
|
|
69
|
+
return threadId
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// `sessionStart` se dispara acá y no en `createSession` porque esa función es
|
|
73
|
+
// idempotente y se llama en cada turno: engancharla ahí haría que el hook
|
|
74
|
+
// corriera con cada mensaje. El `put` con `expectedVersion: 0` es el punto de
|
|
75
|
+
// serialización, así que dispararlo justo después de que ese put haya salido
|
|
76
|
+
// bien evita el doble disparo cuando dos turnos del mismo canal llegan juntos
|
|
77
|
+
// — el perdedor se fue por el catch de arriba. Cubre además a quien llame
|
|
78
|
+
// `ensureThread` directamente desde un canal, sin pasar por `sessions`.
|
|
79
|
+
await runSessionStart({ threadId, userId: input.userId, channel: input.channel })
|
|
80
|
+
return threadId
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Conversación nueva de la web. El id lo elige el servidor, nunca el cliente. */
|
|
84
|
+
export async function createWebConversation(userId: string, title?: string): Promise<ConversationThreadDoc> {
|
|
85
|
+
const peerId = newWebConversationId()
|
|
86
|
+
const threadId = await ensureThread({ userId, channel: "webchat", peerId, peerKind: "direct" })
|
|
87
|
+
if (title?.trim()) await renameThread(threadId, title)
|
|
88
|
+
const doc = await getThread(threadId)
|
|
89
|
+
if (!doc) throw new Error(`No pude crear la conversación ${threadId}`)
|
|
90
|
+
return doc
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Título derivado del primer mensaje del usuario. El contenido llega con el
|
|
95
|
+
* prefijo `[Timestamp: ...]` que le añaden el gateway y las rutas REST: esa línea
|
|
96
|
+
* no describe nada, así que se descarta antes de recortar.
|
|
97
|
+
*/
|
|
98
|
+
export function deriveTitle(text: string): string | null {
|
|
99
|
+
const withoutTimestamp = text.replace(/^\[Timestamp:[^\]]*\]\s*/, "").trim()
|
|
100
|
+
const firstLine = withoutTimestamp.split("\n").find((l) => l.trim().length > 0)?.trim()
|
|
101
|
+
if (!firstLine) return null
|
|
102
|
+
return firstLine.length > TITLE_MAX_CHARS
|
|
103
|
+
? `${firstLine.slice(0, TITLE_MAX_CHARS - 1)}…`
|
|
104
|
+
: firstLine
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Registra actividad en el hilo: contador, fecha del último mensaje y —la primera
|
|
109
|
+
* vez que habla el usuario— el título.
|
|
110
|
+
*
|
|
111
|
+
* Si el hilo tiene formato y no existe la fila, la crea (es el caso de un hilo que
|
|
112
|
+
* nació fuera de resolveContext, como el de una tarea programada). Si no lo tiene
|
|
113
|
+
* —hilo legacy, hilos aislados `task-*`— solo actualiza una fila ya existente:
|
|
114
|
+
* nunca inventa conversaciones para los hilos internos de los workers.
|
|
115
|
+
*/
|
|
116
|
+
export async function touchThread(
|
|
117
|
+
threadId: string,
|
|
118
|
+
opts: { role: "user" | "assistant" | "tool"; text?: string; internal?: boolean }
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
try {
|
|
121
|
+
const c = await threadsCol()
|
|
122
|
+
let existing = await c.get(threadId)
|
|
123
|
+
|
|
124
|
+
if (!existing) {
|
|
125
|
+
const parts = parseThreadId(threadId)
|
|
126
|
+
if (!parts) return
|
|
127
|
+
await ensureThread({ userId: parts.userId, channel: parts.channel, peerId: parts.peerId })
|
|
128
|
+
existing = await c.get(threadId)
|
|
129
|
+
if (!existing) return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// El contador se recalcula DENTRO del reintento, no fuera. `addMessage` llama
|
|
133
|
+
// acá sin esperar el resultado, así que dos mensajes seguidos se solapan: con
|
|
134
|
+
// el incremento calculado una sola vez, el reintento por conflicto de versión
|
|
135
|
+
// vuelve a escribir el valor viejo y el conteo se queda corto para siempre.
|
|
136
|
+
for (let attempt = 0; attempt < TOUCH_MAX_RETRIES; attempt++) {
|
|
137
|
+
const fresh = attempt === 0 ? existing : await c.get(threadId)
|
|
138
|
+
if (!fresh) return
|
|
139
|
+
|
|
140
|
+
const next: ConversationThreadDoc = {
|
|
141
|
+
...fresh.doc,
|
|
142
|
+
last_message_at: Date.now(),
|
|
143
|
+
message_count: fresh.doc.message_count + 1,
|
|
144
|
+
}
|
|
145
|
+
// El título sale del primer mensaje humano: ni de la respuesta del agente ni
|
|
146
|
+
// de un evento interno (fan-in de delegación), que no describen el tema.
|
|
147
|
+
if (!fresh.doc.title && opts.role === "user" && !opts.internal && opts.text) {
|
|
148
|
+
const title = deriveTitle(opts.text)
|
|
149
|
+
if (title) next.title = title
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
await c.put(threadId, next, { expectedVersion: fresh.version })
|
|
154
|
+
return
|
|
155
|
+
} catch {
|
|
156
|
+
// Conflicto de versión: otro mensaje del mismo hilo llegó primero.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
log.warn(`demasiada contención actualizando el hilo ${threadId}`)
|
|
160
|
+
} catch (err) {
|
|
161
|
+
// El registro es un catálogo, no la fuente de verdad: nunca debe tumbar un turno.
|
|
162
|
+
log.warn(`no pude actualizar el hilo ${threadId}: ${(err as Error).message}`)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function listThreads(
|
|
167
|
+
userId: string,
|
|
168
|
+
opts?: { channel?: string; includeArchived?: boolean }
|
|
169
|
+
): Promise<ConversationThreadDoc[]> {
|
|
170
|
+
const c = await threadsCol()
|
|
171
|
+
const rows = await c.findBy("user_id", userId)
|
|
172
|
+
return rows
|
|
173
|
+
.map((r) => r.doc)
|
|
174
|
+
.filter((d) => (opts?.channel ? d.channel === opts.channel : true))
|
|
175
|
+
.filter((d) => (opts?.includeArchived ? true : !d.archived))
|
|
176
|
+
.sort((a, b) => b.last_message_at - a.last_message_at)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** La conversación de webchat en la que seguiría escribiendo el usuario. */
|
|
180
|
+
export async function mostRecentWebThread(userId: string): Promise<ConversationThreadDoc | null> {
|
|
181
|
+
const threads = await listThreads(userId, { channel: "webchat" })
|
|
182
|
+
return threads[0] ?? null
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function renameThread(threadId: string, title: string): Promise<void> {
|
|
186
|
+
const clean = title.trim().slice(0, 200)
|
|
187
|
+
await updateDoc<ConversationThreadDoc>("conversationThreads", threadId, { title: clean || null })
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Archiva el hilo: sale de la lista pero no se pierde nada.
|
|
192
|
+
*
|
|
193
|
+
* Vive acá y no en `sessions/index.ts` para que las cuatro transiciones del
|
|
194
|
+
* ciclo de vida —crear, archivar, reabrir, borrar— disparen sus hooks desde un
|
|
195
|
+
* solo archivo. Repartidas, la próxima que se agregue se olvida de disparar.
|
|
196
|
+
*/
|
|
197
|
+
export async function archiveThread(threadId: string): Promise<void> {
|
|
198
|
+
const anterior = await getThread(threadId)
|
|
199
|
+
await updateDoc<ConversationThreadDoc>("conversationThreads", threadId, { archived: true })
|
|
200
|
+
// Archivar dos veces es un no-op, y el hook no debería enterarse dos veces.
|
|
201
|
+
if (anterior?.archived) return
|
|
202
|
+
await runSessionEnd({ threadId, userId: anterior?.user_id, channel: anterior?.channel })
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Reabre un hilo archivado: vuelve a estar activo, y eso es un `sessionStart`. */
|
|
206
|
+
export async function unarchiveThread(threadId: string): Promise<void> {
|
|
207
|
+
const anterior = await getThread(threadId)
|
|
208
|
+
await updateDoc<ConversationThreadDoc>("conversationThreads", threadId, { archived: false })
|
|
209
|
+
if (anterior && !anterior.archived) return
|
|
210
|
+
await runSessionStart({ threadId, userId: anterior?.user_id, channel: anterior?.channel })
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Borra la conversación entera: mensajes, resumen, notas y la fila del registro.
|
|
215
|
+
* Los mensajes se localizan por prefijo, igual que los lee conversation-store.
|
|
216
|
+
*/
|
|
217
|
+
export async function deleteThread(threadId: string): Promise<void> {
|
|
218
|
+
const conversations = await col<ConversationDoc>("conversations")
|
|
219
|
+
const messages = await conversations.scan({ prefix: `${threadId}:` })
|
|
220
|
+
for (const m of messages) await conversations.delete(m.id)
|
|
221
|
+
|
|
222
|
+
const summaries = await col<SummaryDoc>("summaries")
|
|
223
|
+
await summaries.delete(threadId).catch(() => {})
|
|
224
|
+
|
|
225
|
+
const scratchpad = await col<{ threadId: string }>("scratchpad")
|
|
226
|
+
const notes = await scratchpad.scan({ prefix: `${threadId}:` })
|
|
227
|
+
for (const n of notes) await scratchpad.delete(n.id)
|
|
228
|
+
|
|
229
|
+
const c = await threadsCol()
|
|
230
|
+
// La fila se lee antes de borrarla: después no hay de dónde sacar el usuario
|
|
231
|
+
// ni el canal para el contexto del hook. El threadId los lleva codificados,
|
|
232
|
+
// pero el hilo legacy es id = userId pelado y no parsea.
|
|
233
|
+
const fila = (await c.get(threadId))?.doc
|
|
234
|
+
await c.delete(threadId).catch(() => {})
|
|
235
|
+
|
|
236
|
+
log.info(`conversación ${threadId} borrada (${messages.length} mensajes)`)
|
|
237
|
+
await runSessionEnd({
|
|
238
|
+
threadId,
|
|
239
|
+
userId: fila?.user_id ?? parseThreadId(threadId)?.userId,
|
|
240
|
+
channel: fila?.channel ?? parseThreadId(threadId)?.channel,
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* El hilo por el que hablarle a alguien en un canal cuando no venimos de un
|
|
246
|
+
* mensaje suyo (avisos de tareas programadas, por ejemplo).
|
|
247
|
+
*
|
|
248
|
+
* En la web es la conversación activa; en los demás canales, el contacto al que
|
|
249
|
+
* apunta `userIdentities`. Devuelve null si no hay ninguno, y quien llame decide
|
|
250
|
+
* el fallback.
|
|
251
|
+
*/
|
|
252
|
+
export async function threadForChannel(userId: string, channel: string): Promise<string | null> {
|
|
253
|
+
if (channel === "webchat") return (await mostRecentWebThread(userId))?.id ?? null
|
|
254
|
+
|
|
255
|
+
const identities = await col<{ channel_user_id?: string }>("userIdentities")
|
|
256
|
+
const identity = await identities.get(`${userId}:${channel}`)
|
|
257
|
+
const peerId = identity?.doc.channel_user_id
|
|
258
|
+
return peerId ? makeThreadId(userId, channel, peerId) : null
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Registra el hilo anterior a la separación por canal —`thread_id = userId`, un
|
|
263
|
+
* único hilo compartido por todos los canales— como una conversación más de la
|
|
264
|
+
* web, para que su historial siga siendo accesible. No mueve ni reescribe ningún
|
|
265
|
+
* mensaje: la fila apunta al mismo prefijo de siempre.
|
|
266
|
+
*/
|
|
267
|
+
export async function ensureLegacyThread(userId: string): Promise<boolean> {
|
|
268
|
+
const c = await threadsCol()
|
|
269
|
+
if (await c.get(userId)) return false
|
|
270
|
+
|
|
271
|
+
const conversations = await col<ConversationDoc>("conversations")
|
|
272
|
+
const sample = await conversations.scan({ prefix: `${userId}:`, limit: 1 })
|
|
273
|
+
if (sample.length === 0) return false
|
|
274
|
+
|
|
275
|
+
const all = await conversations.scan({ prefix: `${userId}:` })
|
|
276
|
+
const lastAt = all.reduce((max, e) => Math.max(max, e.doc.created_at), 0)
|
|
277
|
+
|
|
278
|
+
await c.put(userId, {
|
|
279
|
+
id: userId,
|
|
280
|
+
user_id: userId,
|
|
281
|
+
channel: "webchat",
|
|
282
|
+
peer_id: "legacy",
|
|
283
|
+
peer_kind: "direct",
|
|
284
|
+
title: "Conversación anterior",
|
|
285
|
+
archived: false,
|
|
286
|
+
created_at: all[0]?.doc.created_at ?? Date.now(),
|
|
287
|
+
last_message_at: lastAt || Date.now(),
|
|
288
|
+
message_count: all.length,
|
|
289
|
+
}, { expectedVersion: 0 }).catch(() => {})
|
|
290
|
+
|
|
291
|
+
log.info(`hilo legacy ${userId} registrado como conversación (${all.length} mensajes)`)
|
|
292
|
+
return true
|
|
293
|
+
}
|
|
@@ -40,16 +40,16 @@
|
|
|
40
40
|
* - core (notify, report_progress, save_note)
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
|
-
import { col } from "../storage/hive"
|
|
44
|
-
import type { ToolDoc } from "../storage/collections"
|
|
45
|
-
import { logger } from "../utils/logger"
|
|
43
|
+
import { col } from "../storage/hive.ts"
|
|
44
|
+
import type { ToolDoc } from "../storage/collections.ts"
|
|
45
|
+
import { logger } from "../utils/logger.ts"
|
|
46
46
|
import {
|
|
47
47
|
searchCapabilities,
|
|
48
48
|
applyRelativeCutoff,
|
|
49
49
|
replaceCapabilityDocs,
|
|
50
50
|
type CapabilityDoc,
|
|
51
|
-
} from "./capability-search"
|
|
52
|
-
import { isCalendarOperation } from "./routing-intent"
|
|
51
|
+
} from "./capability-search.ts"
|
|
52
|
+
import { isCalendarOperation } from "./routing-intent.ts"
|
|
53
53
|
|
|
54
54
|
const log = logger.child("tool-selector")
|
|
55
55
|
|
|
@@ -200,7 +200,11 @@ export const CORE_TOOL_CATALOG: ToolDescriptor[] = [
|
|
|
200
200
|
{ name: "api_request", description: "Perform an authorized HTTP request against a REST endpoint and validate the response. Spanish keywords: llamar api, request rest, consumir endpoint, petición http, hacer get, hacer post", category: "api", abstractionLevel: "atomic" },
|
|
201
201
|
|
|
202
202
|
// Artifacts
|
|
203
|
+
{ name: "computer_use_task", description: "Operate the browser by looking at the screen: click by coordinates, type and navigate when no stable CSS selector exists (canvas, generated UIs, embedded viewers). Acts on Hive's own browser, never on the user screen. Spanish keywords: usar el navegador, hacer clic donde veas, operar una página, rellenar formulario, computer use, mirar la pantalla", category: "browser", abstractionLevel: "orchestration" },
|
|
203
204
|
{ name: "artifact_inspect", description: "Inspect a managed artifact's integrity and metadata without modifying it. Spanish keywords: inspeccionar artefacto, verificar archivo generado, metadatos artefacto, comprobar entrega", category: "web", abstractionLevel: "atomic" },
|
|
205
|
+
{ name: "artifact_read", description: "Read a managed artifact's text content in slices, or search inside it — the way to open any artifact_ref a tool returned. Spanish keywords: leer artefacto, ver contenido del artefacto, abrir resultado grande, buscar dentro del artefacto", category: "web", abstractionLevel: "atomic" },
|
|
206
|
+
{ name: "image_metadata", description: "Read a stored image's dimensions and format without loading it into context. Spanish keywords: medir imagen, dimensiones de la imagen, tamaño de la foto, formato de imagen", category: "images", abstractionLevel: "atomic" },
|
|
207
|
+
{ name: "image_transform", description: "Resize, rotate or convert a stored image, returning a new artifact — the original is untouched. Spanish keywords: redimensionar imagen, cambiar tamaño, convertir a webp, comprimir imagen, rotar foto, achicar imagen", category: "images", abstractionLevel: "atomic" },
|
|
204
208
|
|
|
205
209
|
// Office documents — read
|
|
206
210
|
{ name: "office_leer_pdf", description: "Read and extract text from a PDF document. Spanish keywords: leer pdf, extraer texto pdf, abrir pdf, contenido pdf", category: "office", abstractionLevel: "atomic" },
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* `agents.lastTraceAt` field used by the Curator's stale-worker detection.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { logger } from "../utils/logger"
|
|
10
|
-
import { col, nextId, updateDoc } from "../storage/hive"
|
|
11
|
-
import type { TraceDoc, AgentDoc } from "../storage/collections"
|
|
9
|
+
import { logger } from "../utils/logger.ts"
|
|
10
|
+
import { col, nextId, updateDoc } from "../storage/hive.ts"
|
|
11
|
+
import type { TraceDoc, AgentDoc } from "../storage/collections.ts"
|
|
12
12
|
|
|
13
13
|
const log = logger.child("tracer")
|
|
14
14
|
|
|
@@ -82,7 +82,7 @@ async function checkReflectorTrigger(): Promise<void> {
|
|
|
82
82
|
_tracesSinceLastReflection = 0
|
|
83
83
|
|
|
84
84
|
// Lazy import to avoid circular deps
|
|
85
|
-
const { runReflector } = await import("./reflector")
|
|
85
|
+
const { runReflector } = await import("./reflector.ts")
|
|
86
86
|
runReflector().catch((err) => {
|
|
87
87
|
log.warn("[tracer] Reflector run failed:", err)
|
|
88
88
|
})
|
|
@@ -98,7 +98,7 @@ export function recordLLMUsage(opts: {
|
|
|
98
98
|
}): void {
|
|
99
99
|
Promise.resolve().then(async () => {
|
|
100
100
|
try {
|
|
101
|
-
const { recordUsage } = await import("../storage/usage")
|
|
101
|
+
const { recordUsage } = await import("../storage/usage.ts")
|
|
102
102
|
recordUsage({
|
|
103
103
|
provider: opts.provider,
|
|
104
104
|
model: opts.model,
|
|
@@ -35,11 +35,28 @@ export interface Agent {
|
|
|
35
35
|
readonly name: string;
|
|
36
36
|
readonly id: string;
|
|
37
37
|
readonly config: AgentConfig;
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Con `stream: true` se emiten eventos `token` con los deltas del proveedor
|
|
40
|
+
* a medida que llegan, además del `text` con la respuesta completa del turno.
|
|
41
|
+
*/
|
|
42
|
+
chat(
|
|
43
|
+
message: string,
|
|
44
|
+
opts?: { threadId?: string; channel?: string; stream?: boolean },
|
|
45
|
+
): AsyncGenerator<AgentEvent>;
|
|
39
46
|
run(task: string, opts?: { threadId?: string; channel?: string }): Promise<string>;
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
export type AgentEvent =
|
|
50
|
+
/**
|
|
51
|
+
* Un fragmento recién llegado del proveedor, para pintar la respuesta
|
|
52
|
+
* mientras se genera.
|
|
53
|
+
*
|
|
54
|
+
* Sólo aparece si se pide `stream: true`. Los proveedores ya emitían estos
|
|
55
|
+
* deltas —el mecanismo estaba implementado— pero ningún punto de entrada los
|
|
56
|
+
* pasaba, así que nunca llegaban a nadie: la respuesta aparecía de golpe al
|
|
57
|
+
* terminar el turno.
|
|
58
|
+
*/
|
|
59
|
+
| { type: "token"; content: string }
|
|
43
60
|
| { type: "text"; content: string }
|
|
44
61
|
| { type: "tool_call"; name: string; args: Record<string, unknown> }
|
|
45
62
|
| { type: "tool_result"; name: string; result: unknown }
|
|
@@ -92,7 +109,7 @@ export async function createAgent(config: AgentConfig): Promise<Agent> {
|
|
|
92
109
|
|
|
93
110
|
const coreConfig = await loadConfig();
|
|
94
111
|
|
|
95
|
-
// Browser automation (
|
|
112
|
+
// Browser automation (Bun.WebView) si está habilitado.
|
|
96
113
|
try {
|
|
97
114
|
const { initializeBrowserService } = await import("../tools/web/browser-service.ts");
|
|
98
115
|
const browserService = initializeBrowserService(coreConfig);
|
|
@@ -128,6 +145,40 @@ export async function createAgent(config: AgentConfig): Promise<Agent> {
|
|
|
128
145
|
}
|
|
129
146
|
}
|
|
130
147
|
|
|
148
|
+
// Las skills declaradas seguían el mismo camino que las tools —y hasta acá no
|
|
149
|
+
// lo seguían: `config.skills` estaba tipado y se descartaba en silencio, así
|
|
150
|
+
// que declarar una skill no hacía absolutamente nada. Una skill no necesita
|
|
151
|
+
// ejecutor (es instruccional: metadatos más el cuerpo que se le inyecta al
|
|
152
|
+
// agente), pero sí necesita su fila y su entrada en el índice, o el modelo
|
|
153
|
+
// nunca la descubre.
|
|
154
|
+
if (config.skills?.length) {
|
|
155
|
+
const { createSkill, getSkill, updateSkill } = await import("../services/skills.ts");
|
|
156
|
+
|
|
157
|
+
for (const skill of config.skills) {
|
|
158
|
+
// El cuerpo se arma con los pasos declarados: es lo que lee el agente.
|
|
159
|
+
const body = [
|
|
160
|
+
skill.description,
|
|
161
|
+
"",
|
|
162
|
+
...skill.steps.map((s, i) => `${i + 1}. **${s.action}** — ${s.instruction}`),
|
|
163
|
+
].join("\n");
|
|
164
|
+
|
|
165
|
+
const campos = {
|
|
166
|
+
name: skill.name,
|
|
167
|
+
description: skill.description,
|
|
168
|
+
category: skill.category,
|
|
169
|
+
body,
|
|
170
|
+
tools: skill.tools,
|
|
171
|
+
triggers: skill.triggers,
|
|
172
|
+
version: skill.version,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Idempotente: declarar la misma skill dos veces la actualiza.
|
|
176
|
+
const existente = await getSkill(skill.name);
|
|
177
|
+
if (existente) await updateSkill(skill.name, campos);
|
|
178
|
+
else await createSkill({ id: skill.name, ...campos });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
131
182
|
// ─── Provider y modelo ────────────────────────────────────────────────────
|
|
132
183
|
// El loop los lee de la fila del agente, así que hay que dejarlos escritos.
|
|
133
184
|
const providerId = config.provider ?? "";
|
|
@@ -241,6 +292,17 @@ export async function createAgent(config: AgentConfig): Promise<Agent> {
|
|
|
241
292
|
const threadId = opts?.threadId ?? crypto.randomUUID();
|
|
242
293
|
let response = "";
|
|
243
294
|
|
|
295
|
+
// `onToken` es un callback y esto es un generador: los deltas se
|
|
296
|
+
// encolan y se drenan entre chunks. Sin buffer habría que elegir entre
|
|
297
|
+
// perder tokens o bloquear al proveedor mientras el consumidor lee.
|
|
298
|
+
const pendientes: string[] = [];
|
|
299
|
+
const onToken = opts?.stream ? (t: string) => { pendientes.push(t); } : undefined;
|
|
300
|
+
const drenar = function* () {
|
|
301
|
+
while (pendientes.length > 0) {
|
|
302
|
+
yield { type: "token" as const, content: pendientes.shift()! };
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
244
306
|
for await (const chunk of runAgent({
|
|
245
307
|
agentId,
|
|
246
308
|
userMessage: message,
|
|
@@ -248,7 +310,9 @@ export async function createAgent(config: AgentConfig): Promise<Agent> {
|
|
|
248
310
|
channel: opts?.channel ?? "cli",
|
|
249
311
|
mcpManager,
|
|
250
312
|
userId,
|
|
313
|
+
onToken,
|
|
251
314
|
})) {
|
|
315
|
+
yield* drenar();
|
|
252
316
|
for (const msg of chunk.agent?.messages ?? []) {
|
|
253
317
|
if (typeof msg.content === "string" && msg.content) {
|
|
254
318
|
response = msg.content;
|
|
@@ -268,6 +332,7 @@ export async function createAgent(config: AgentConfig): Promise<Agent> {
|
|
|
268
332
|
}
|
|
269
333
|
}
|
|
270
334
|
|
|
335
|
+
yield* drenar();
|
|
271
336
|
yield { type: "done" as const, response };
|
|
272
337
|
},
|
|
273
338
|
async run(task, opts) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artifacts — los archivos que produce un agente, fuera de la ventana de contexto.
|
|
3
|
+
*
|
|
4
|
+
* Cuando una tool devuelve algo grande —una imagen, un PDF, la salida enorme de
|
|
5
|
+
* un servidor MCP— serializarlo entero al prompt es lo peor que se puede hacer:
|
|
6
|
+
* se come el contexto y no aporta. En su lugar se guarda como artefacto y al
|
|
7
|
+
* modelo le llega una referencia (`artifact_ref`) que puede abrir con la tool
|
|
8
|
+
* `artifact_read` si de verdad necesita el contenido.
|
|
9
|
+
*
|
|
10
|
+
* `createArtifact` guarda; `inspectArtifact` da metadatos sin abrirlo;
|
|
11
|
+
* `readArtifactText` lo lee para consumo del propio proceso; `readArtifactBytes`
|
|
12
|
+
* devuelve los bytes crudos, que es lo que usa un canal para mandar la imagen.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export * from "./store.ts";
|