@johpaz/hive-sdk 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +135 -0
- package/README.md +11 -1
- package/package.json +18 -2
- package/packages/core/src/agent/acceptance-checks.ts +9 -9
- package/packages/core/src/agent/agent-catalog.ts +3 -3
- package/packages/core/src/agent/agent-loop.ts +115 -26
- package/packages/core/src/agent/capability-search.ts +2 -2
- package/packages/core/src/agent/catalog-selector.ts +4 -4
- package/packages/core/src/agent/compaction.ts +10 -9
- package/packages/core/src/agent/context-compiler.ts +58 -34
- package/packages/core/src/agent/conversation-store.ts +31 -7
- package/packages/core/src/agent/curator.ts +4 -4
- package/packages/core/src/agent/delegation-runtime.ts +5 -5
- package/packages/core/src/agent/goal-runner.ts +9 -9
- package/packages/core/src/agent/index.ts +1 -0
- package/packages/core/src/agent/llm-client.ts +98 -37
- package/packages/core/src/agent/llm-providers/anthropic.ts +4 -4
- package/packages/core/src/agent/llm-providers/deepseek.ts +1 -1
- package/packages/core/src/agent/llm-providers/gemini.ts +4 -4
- package/packages/core/src/agent/llm-providers/groq.ts +1 -1
- package/packages/core/src/agent/llm-providers/hiveagents.ts +3 -3
- package/packages/core/src/agent/llm-providers/interface.ts +2 -2
- package/packages/core/src/agent/llm-providers/kimi.ts +1 -1
- package/packages/core/src/agent/llm-providers/minimax.ts +1 -1
- package/packages/core/src/agent/llm-providers/mistral.ts +1 -1
- package/packages/core/src/agent/llm-providers/modelscope.ts +1 -1
- package/packages/core/src/agent/llm-providers/nvidia.ts +1 -1
- package/packages/core/src/agent/llm-providers/ollama.ts +4 -4
- package/packages/core/src/agent/llm-providers/openai-compat-base.ts +9 -5
- package/packages/core/src/agent/llm-providers/openai.ts +1 -1
- package/packages/core/src/agent/llm-providers/opencode-go.ts +1 -1
- package/packages/core/src/agent/llm-providers/openrouter.ts +1 -1
- package/packages/core/src/agent/llm-providers/qwen.ts +1 -1
- package/packages/core/src/agent/llm-providers/z-ai.ts +1 -1
- package/packages/core/src/agent/mcp-result-normalizer.ts +192 -0
- package/packages/core/src/agent/playbook-selector.ts +4 -4
- package/packages/core/src/agent/prompt-builder.ts +5 -5
- package/packages/core/src/agent/proof-packet.ts +5 -5
- package/packages/core/src/agent/providers/index.ts +4 -4
- package/packages/core/src/agent/realtime-providers/gemini-live.ts +238 -0
- package/packages/core/src/agent/realtime-providers/index.ts +29 -0
- package/packages/core/src/agent/realtime-providers/interface.ts +108 -0
- package/packages/core/src/agent/reflector.ts +6 -6
- package/packages/core/src/agent/run-store.ts +8 -8
- package/packages/core/src/agent/service.ts +10 -10
- package/packages/core/src/agent/skill-selector.ts +6 -6
- package/packages/core/src/agent/thread-id.ts +71 -0
- package/packages/core/src/agent/thread-store.ts +250 -0
- package/packages/core/src/agent/tool-selector.ts +7 -5
- package/packages/core/src/agent/tracer.ts +5 -5
- package/packages/core/src/api/createAgent.ts +1 -1
- package/packages/core/src/artifacts/store.ts +84 -3
- package/packages/core/src/canvas/emitter.ts +2 -2
- package/packages/core/src/channels/telegram.ts +1 -1
- package/packages/core/src/channels/webchat.ts +1 -1
- package/packages/core/src/config/loader.ts +14 -5
- package/packages/core/src/events/agent-bus.ts +3 -3
- package/packages/core/src/events/channel-narration.ts +3 -3
- package/packages/core/src/events/event-bus.ts +1 -1
- package/packages/core/src/events/narration.ts +3 -3
- package/packages/core/src/gateway/delegation-groups.ts +4 -4
- package/packages/core/src/gateway/durable-queue.ts +5 -5
- package/packages/core/src/gateway/job-store.ts +5 -5
- package/packages/core/src/gateway/notification-inbox.ts +2 -2
- package/packages/core/src/gateway/server.ts +2 -2
- package/packages/core/src/mcp/MCPClient.ts +3 -3
- package/packages/core/src/mcp/hot-reload.ts +5 -5
- package/packages/core/src/mcp/tool-sync.ts +5 -5
- package/packages/core/src/mcp/transports/index.ts +2 -2
- package/packages/core/src/mcp/transports/sse.ts +1 -1
- package/packages/core/src/models/index.ts +36 -0
- package/packages/core/src/multimodal/index.ts +2 -2
- package/packages/core/src/multimodal/vision-service.ts +6 -6
- package/packages/core/src/plugins/loader.ts +4 -1
- package/packages/core/src/resilience/circuit-breaker.ts +16 -5
- package/packages/core/src/resilience/retry.ts +1 -1
- package/packages/core/src/scheduler/CronScheduler.ts +6 -6
- package/packages/core/src/scheduler/integration.ts +9 -9
- package/packages/core/src/sessions/index.ts +266 -0
- package/packages/core/src/storage/bootstrap.ts +34 -8
- package/packages/core/src/storage/causal-events.ts +1 -1
- package/packages/core/src/storage/collections.ts +32 -1
- package/packages/core/src/storage/crypto.ts +16 -2
- package/packages/core/src/storage/hive.ts +1 -1
- package/packages/core/src/storage/hivedb.ts +10 -1
- package/packages/core/src/storage/onboarding.ts +6 -6
- package/packages/core/src/storage/reconcile.ts +5 -5
- package/packages/core/src/storage/seed.ts +103 -13
- package/packages/core/src/storage/usage.ts +3 -3
- package/packages/core/src/swarm/AgentExecutor.ts +2 -2
- package/packages/core/src/swarm/Coordinator.ts +8 -8
- package/packages/core/src/swarm/EventBridge.ts +2 -2
- package/packages/core/src/swarm/RoleSwarm.ts +234 -0
- package/packages/core/src/swarm/TaskGraph.ts +2 -2
- package/packages/core/src/swarm/index.ts +7 -0
- package/packages/core/src/swarm/presets/HiveLearnPreset.ts +2 -2
- package/packages/core/src/swarm/presets/ResearchPreset.ts +2 -2
- package/packages/core/src/swarm/strategies/ParallelStrategy.ts +1 -1
- package/packages/core/src/swarm/strategies/PriorityStrategy.ts +3 -3
- package/packages/core/src/tools/ToolExecutor.ts +7 -3
- package/packages/core/src/tools/core/index.ts +2 -2
- package/packages/core/src/tools/cron/index.ts +4 -4
- package/packages/core/src/tools/web/artifact-inspect.ts +2 -2
- package/packages/core/src/tools/web/artifact-read.ts +162 -0
- package/packages/core/src/tools/web/browser-backend.ts +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
|
@@ -11,12 +11,12 @@
|
|
|
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
20
|
|
|
21
21
|
const log = logger.child("reflector")
|
|
22
22
|
|
|
@@ -88,7 +88,7 @@ export async function runReflector(): Promise<void> {
|
|
|
88
88
|
await cursorsCol.put(CURSOR_ID, { value: newCursor }, cursorEntry ? { expectedVersion: cursorEntry.version } : { expectedVersion: 0 })
|
|
89
89
|
|
|
90
90
|
// Trigger curator
|
|
91
|
-
const { runCurator } = await import("./curator")
|
|
91
|
+
const { runCurator } = await import("./curator.ts")
|
|
92
92
|
await runCurator()
|
|
93
93
|
|
|
94
94
|
log.info(`[reflector] Reflection cycle completed successfully`)
|
|
@@ -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"
|
|
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"
|
|
19
|
+
import { col } from "../storage/hive.ts"
|
|
20
|
+
import type { SkillDoc } from "../storage/collections.ts"
|
|
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
|
|
|
@@ -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,250 @@
|
|
|
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
|
+
|
|
18
|
+
const log = logger.child("thread-store")
|
|
19
|
+
|
|
20
|
+
const TITLE_MAX_CHARS = 60
|
|
21
|
+
|
|
22
|
+
/** Reintentos del contador ante mensajes concurrentes del mismo hilo. */
|
|
23
|
+
const TOUCH_MAX_RETRIES = 5
|
|
24
|
+
|
|
25
|
+
export interface EnsureThreadInput {
|
|
26
|
+
userId: string
|
|
27
|
+
channel: string
|
|
28
|
+
peerId: string
|
|
29
|
+
peerKind?: "direct" | "group"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function threadsCol() {
|
|
33
|
+
return col<ConversationThreadDoc>("conversationThreads")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function getThread(threadId: string): Promise<ConversationThreadDoc | null> {
|
|
37
|
+
const c = await threadsCol()
|
|
38
|
+
const entry = await c.get(threadId)
|
|
39
|
+
return entry?.doc ?? null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Crea la fila de la conversación si aún no existe y devuelve su threadId.
|
|
44
|
+
* Idempotente: se llama en cada turno desde resolveContext.
|
|
45
|
+
*/
|
|
46
|
+
export async function ensureThread(input: EnsureThreadInput): Promise<string> {
|
|
47
|
+
const threadId = makeThreadId(input.userId, input.channel, input.peerId)
|
|
48
|
+
const c = await threadsCol()
|
|
49
|
+
const existing = await c.get(threadId)
|
|
50
|
+
if (existing) return threadId
|
|
51
|
+
|
|
52
|
+
const now = Date.now()
|
|
53
|
+
try {
|
|
54
|
+
await c.put(threadId, {
|
|
55
|
+
id: threadId,
|
|
56
|
+
user_id: input.userId,
|
|
57
|
+
channel: input.channel,
|
|
58
|
+
peer_id: input.peerId,
|
|
59
|
+
peer_kind: input.peerKind ?? "direct",
|
|
60
|
+
title: null,
|
|
61
|
+
archived: false,
|
|
62
|
+
created_at: now,
|
|
63
|
+
last_message_at: now,
|
|
64
|
+
message_count: 0,
|
|
65
|
+
}, { expectedVersion: 0 })
|
|
66
|
+
} catch {
|
|
67
|
+
// Otro turno del mismo canal la creó primero — ambos quieren lo mismo.
|
|
68
|
+
}
|
|
69
|
+
return threadId
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Conversación nueva de la web. El id lo elige el servidor, nunca el cliente. */
|
|
73
|
+
export async function createWebConversation(userId: string, title?: string): Promise<ConversationThreadDoc> {
|
|
74
|
+
const peerId = newWebConversationId()
|
|
75
|
+
const threadId = await ensureThread({ userId, channel: "webchat", peerId, peerKind: "direct" })
|
|
76
|
+
if (title?.trim()) await renameThread(threadId, title)
|
|
77
|
+
const doc = await getThread(threadId)
|
|
78
|
+
if (!doc) throw new Error(`No pude crear la conversación ${threadId}`)
|
|
79
|
+
return doc
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Título derivado del primer mensaje del usuario. El contenido llega con el
|
|
84
|
+
* prefijo `[Timestamp: ...]` que le añaden el gateway y las rutas REST: esa línea
|
|
85
|
+
* no describe nada, así que se descarta antes de recortar.
|
|
86
|
+
*/
|
|
87
|
+
export function deriveTitle(text: string): string | null {
|
|
88
|
+
const withoutTimestamp = text.replace(/^\[Timestamp:[^\]]*\]\s*/, "").trim()
|
|
89
|
+
const firstLine = withoutTimestamp.split("\n").find((l) => l.trim().length > 0)?.trim()
|
|
90
|
+
if (!firstLine) return null
|
|
91
|
+
return firstLine.length > TITLE_MAX_CHARS
|
|
92
|
+
? `${firstLine.slice(0, TITLE_MAX_CHARS - 1)}…`
|
|
93
|
+
: firstLine
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Registra actividad en el hilo: contador, fecha del último mensaje y —la primera
|
|
98
|
+
* vez que habla el usuario— el título.
|
|
99
|
+
*
|
|
100
|
+
* Si el hilo tiene formato y no existe la fila, la crea (es el caso de un hilo que
|
|
101
|
+
* nació fuera de resolveContext, como el de una tarea programada). Si no lo tiene
|
|
102
|
+
* —hilo legacy, hilos aislados `task-*`— solo actualiza una fila ya existente:
|
|
103
|
+
* nunca inventa conversaciones para los hilos internos de los workers.
|
|
104
|
+
*/
|
|
105
|
+
export async function touchThread(
|
|
106
|
+
threadId: string,
|
|
107
|
+
opts: { role: "user" | "assistant" | "tool"; text?: string; internal?: boolean }
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
try {
|
|
110
|
+
const c = await threadsCol()
|
|
111
|
+
let existing = await c.get(threadId)
|
|
112
|
+
|
|
113
|
+
if (!existing) {
|
|
114
|
+
const parts = parseThreadId(threadId)
|
|
115
|
+
if (!parts) return
|
|
116
|
+
await ensureThread({ userId: parts.userId, channel: parts.channel, peerId: parts.peerId })
|
|
117
|
+
existing = await c.get(threadId)
|
|
118
|
+
if (!existing) return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// El contador se recalcula DENTRO del reintento, no fuera. `addMessage` llama
|
|
122
|
+
// acá sin esperar el resultado, así que dos mensajes seguidos se solapan: con
|
|
123
|
+
// el incremento calculado una sola vez, el reintento por conflicto de versión
|
|
124
|
+
// vuelve a escribir el valor viejo y el conteo se queda corto para siempre.
|
|
125
|
+
for (let attempt = 0; attempt < TOUCH_MAX_RETRIES; attempt++) {
|
|
126
|
+
const fresh = attempt === 0 ? existing : await c.get(threadId)
|
|
127
|
+
if (!fresh) return
|
|
128
|
+
|
|
129
|
+
const next: ConversationThreadDoc = {
|
|
130
|
+
...fresh.doc,
|
|
131
|
+
last_message_at: Date.now(),
|
|
132
|
+
message_count: fresh.doc.message_count + 1,
|
|
133
|
+
}
|
|
134
|
+
// El título sale del primer mensaje humano: ni de la respuesta del agente ni
|
|
135
|
+
// de un evento interno (fan-in de delegación), que no describen el tema.
|
|
136
|
+
if (!fresh.doc.title && opts.role === "user" && !opts.internal && opts.text) {
|
|
137
|
+
const title = deriveTitle(opts.text)
|
|
138
|
+
if (title) next.title = title
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await c.put(threadId, next, { expectedVersion: fresh.version })
|
|
143
|
+
return
|
|
144
|
+
} catch {
|
|
145
|
+
// Conflicto de versión: otro mensaje del mismo hilo llegó primero.
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
log.warn(`demasiada contención actualizando el hilo ${threadId}`)
|
|
149
|
+
} catch (err) {
|
|
150
|
+
// El registro es un catálogo, no la fuente de verdad: nunca debe tumbar un turno.
|
|
151
|
+
log.warn(`no pude actualizar el hilo ${threadId}: ${(err as Error).message}`)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function listThreads(
|
|
156
|
+
userId: string,
|
|
157
|
+
opts?: { channel?: string; includeArchived?: boolean }
|
|
158
|
+
): Promise<ConversationThreadDoc[]> {
|
|
159
|
+
const c = await threadsCol()
|
|
160
|
+
const rows = await c.findBy("user_id", userId)
|
|
161
|
+
return rows
|
|
162
|
+
.map((r) => r.doc)
|
|
163
|
+
.filter((d) => (opts?.channel ? d.channel === opts.channel : true))
|
|
164
|
+
.filter((d) => (opts?.includeArchived ? true : !d.archived))
|
|
165
|
+
.sort((a, b) => b.last_message_at - a.last_message_at)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** La conversación de webchat en la que seguiría escribiendo el usuario. */
|
|
169
|
+
export async function mostRecentWebThread(userId: string): Promise<ConversationThreadDoc | null> {
|
|
170
|
+
const threads = await listThreads(userId, { channel: "webchat" })
|
|
171
|
+
return threads[0] ?? null
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function renameThread(threadId: string, title: string): Promise<void> {
|
|
175
|
+
const clean = title.trim().slice(0, 200)
|
|
176
|
+
await updateDoc<ConversationThreadDoc>("conversationThreads", threadId, { title: clean || null })
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Borra la conversación entera: mensajes, resumen, notas y la fila del registro.
|
|
181
|
+
* Los mensajes se localizan por prefijo, igual que los lee conversation-store.
|
|
182
|
+
*/
|
|
183
|
+
export async function deleteThread(threadId: string): Promise<void> {
|
|
184
|
+
const conversations = await col<ConversationDoc>("conversations")
|
|
185
|
+
const messages = await conversations.scan({ prefix: `${threadId}:` })
|
|
186
|
+
for (const m of messages) await conversations.delete(m.id)
|
|
187
|
+
|
|
188
|
+
const summaries = await col<SummaryDoc>("summaries")
|
|
189
|
+
await summaries.delete(threadId).catch(() => {})
|
|
190
|
+
|
|
191
|
+
const scratchpad = await col<{ threadId: string }>("scratchpad")
|
|
192
|
+
const notes = await scratchpad.scan({ prefix: `${threadId}:` })
|
|
193
|
+
for (const n of notes) await scratchpad.delete(n.id)
|
|
194
|
+
|
|
195
|
+
const c = await threadsCol()
|
|
196
|
+
await c.delete(threadId).catch(() => {})
|
|
197
|
+
|
|
198
|
+
log.info(`conversación ${threadId} borrada (${messages.length} mensajes)`)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* El hilo por el que hablarle a alguien en un canal cuando no venimos de un
|
|
203
|
+
* mensaje suyo (avisos de tareas programadas, por ejemplo).
|
|
204
|
+
*
|
|
205
|
+
* En la web es la conversación activa; en los demás canales, el contacto al que
|
|
206
|
+
* apunta `userIdentities`. Devuelve null si no hay ninguno, y quien llame decide
|
|
207
|
+
* el fallback.
|
|
208
|
+
*/
|
|
209
|
+
export async function threadForChannel(userId: string, channel: string): Promise<string | null> {
|
|
210
|
+
if (channel === "webchat") return (await mostRecentWebThread(userId))?.id ?? null
|
|
211
|
+
|
|
212
|
+
const identities = await col<{ channel_user_id?: string }>("userIdentities")
|
|
213
|
+
const identity = await identities.get(`${userId}:${channel}`)
|
|
214
|
+
const peerId = identity?.doc.channel_user_id
|
|
215
|
+
return peerId ? makeThreadId(userId, channel, peerId) : null
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Registra el hilo anterior a la separación por canal —`thread_id = userId`, un
|
|
220
|
+
* único hilo compartido por todos los canales— como una conversación más de la
|
|
221
|
+
* web, para que su historial siga siendo accesible. No mueve ni reescribe ningún
|
|
222
|
+
* mensaje: la fila apunta al mismo prefijo de siempre.
|
|
223
|
+
*/
|
|
224
|
+
export async function ensureLegacyThread(userId: string): Promise<boolean> {
|
|
225
|
+
const c = await threadsCol()
|
|
226
|
+
if (await c.get(userId)) return false
|
|
227
|
+
|
|
228
|
+
const conversations = await col<ConversationDoc>("conversations")
|
|
229
|
+
const sample = await conversations.scan({ prefix: `${userId}:`, limit: 1 })
|
|
230
|
+
if (sample.length === 0) return false
|
|
231
|
+
|
|
232
|
+
const all = await conversations.scan({ prefix: `${userId}:` })
|
|
233
|
+
const lastAt = all.reduce((max, e) => Math.max(max, e.doc.created_at), 0)
|
|
234
|
+
|
|
235
|
+
await c.put(userId, {
|
|
236
|
+
id: userId,
|
|
237
|
+
user_id: userId,
|
|
238
|
+
channel: "webchat",
|
|
239
|
+
peer_id: "legacy",
|
|
240
|
+
peer_kind: "direct",
|
|
241
|
+
title: "Conversación anterior",
|
|
242
|
+
archived: false,
|
|
243
|
+
created_at: all[0]?.doc.created_at ?? Date.now(),
|
|
244
|
+
last_message_at: lastAt || Date.now(),
|
|
245
|
+
message_count: all.length,
|
|
246
|
+
}, { expectedVersion: 0 }).catch(() => {})
|
|
247
|
+
|
|
248
|
+
log.info(`hilo legacy ${userId} registrado como conversación (${all.length} mensajes)`)
|
|
249
|
+
return true
|
|
250
|
+
}
|
|
@@ -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,9 @@ 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" },
|
|
204
206
|
|
|
205
207
|
// Office documents — read
|
|
206
208
|
{ 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,
|
|
@@ -92,7 +92,7 @@ export async function createAgent(config: AgentConfig): Promise<Agent> {
|
|
|
92
92
|
|
|
93
93
|
const coreConfig = await loadConfig();
|
|
94
94
|
|
|
95
|
-
// Browser automation (
|
|
95
|
+
// Browser automation (Bun.WebView) si está habilitado.
|
|
96
96
|
try {
|
|
97
97
|
const { initializeBrowserService } = await import("../tools/web/browser-service.ts");
|
|
98
98
|
const browserService = initializeBrowserService(coreConfig);
|
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
writeFileSync,
|
|
9
9
|
} from "node:fs";
|
|
10
10
|
import { extname, join } from "node:path";
|
|
11
|
-
import { getHiveDir } from "../config/loader";
|
|
12
|
-
import { col, updateDoc } from "../storage/hive";
|
|
13
|
-
import type { ArtifactDoc } from "../storage/collections";
|
|
11
|
+
import { getHiveDir } from "../config/loader.ts";
|
|
12
|
+
import { col, updateDoc } from "../storage/hive.ts";
|
|
13
|
+
import type { ArtifactDoc } from "../storage/collections.ts";
|
|
14
14
|
|
|
15
15
|
const ARTIFACT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
16
16
|
|
|
@@ -91,6 +91,87 @@ export async function createArtifact(input: {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Reads an artifact's bytes straight off disk — for server-side consumers
|
|
96
|
+
* that don't need an HTTP round trip (routes/artifacts.ts is for the
|
|
97
|
+
* browser; channels/*.ts send()s use this directly to build a Telegram/
|
|
98
|
+
* Discord/Slack/WhatsApp photo attachment from the same file).
|
|
99
|
+
*/
|
|
100
|
+
export async function readArtifactBytes(
|
|
101
|
+
artifactId: string,
|
|
102
|
+
): Promise<{ bytes: Buffer; mimeType: string } | null> {
|
|
103
|
+
const artifacts = await col<ArtifactDoc>("artifacts");
|
|
104
|
+
const entry = await artifacts.get(artifactId);
|
|
105
|
+
if (!entry || entry.doc.status !== "active") return null;
|
|
106
|
+
if (!existsSync(entry.doc.path)) return null;
|
|
107
|
+
return { bytes: readFileSync(entry.doc.path), mimeType: entry.doc.mime_type };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Beyond this an artifact stops being something an agent can read into a context window. */
|
|
111
|
+
const MAX_TEXT_ARTIFACT_BYTES = 25 * 1024 * 1024;
|
|
112
|
+
|
|
113
|
+
function looksBinary(mimeType: string, bytes: Buffer): boolean {
|
|
114
|
+
if (/^(image|audio|video)\//.test(mimeType)) return true;
|
|
115
|
+
if (mimeType === "application/octet-stream") return true;
|
|
116
|
+
// A NUL byte in the first KB is the classic "this is not text" tell — a
|
|
117
|
+
// mislabelled text/plain artifact would otherwise decode to garbage.
|
|
118
|
+
return bytes.subarray(0, 1024).includes(0x00);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type ArtifactTextResult = {
|
|
122
|
+
ok: boolean;
|
|
123
|
+
text?: string;
|
|
124
|
+
artifact?: ArtifactDoc;
|
|
125
|
+
error?: string;
|
|
126
|
+
status?: string;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Decodes a text artifact for in-process consumers that need its *content*,
|
|
131
|
+
* not just its metadata (inspectArtifact) or its raw bytes (readArtifactBytes).
|
|
132
|
+
*
|
|
133
|
+
* This exists because mcp-result-normalizer.ts moves oversized MCP text results
|
|
134
|
+
* out of the context window and hands the model an `artifact_ref` instead. Until
|
|
135
|
+
* this, nothing could read that reference back: the agent held a receipt it
|
|
136
|
+
* could not cash, burned its iterations guessing, and the turn died on an empty
|
|
137
|
+
* synthesis. Ownership and status checks mirror inspectArtifact's.
|
|
138
|
+
*/
|
|
139
|
+
export async function readArtifactText(
|
|
140
|
+
artifactId: string,
|
|
141
|
+
options: { userId?: string } = {},
|
|
142
|
+
): Promise<ArtifactTextResult> {
|
|
143
|
+
const artifacts = await col<ArtifactDoc>("artifacts");
|
|
144
|
+
const entry = await artifacts.get(artifactId);
|
|
145
|
+
if (!entry) return { ok: false, error: "Artifact not found" };
|
|
146
|
+
const artifact = entry.doc;
|
|
147
|
+
|
|
148
|
+
if (options.userId && artifact.user_id && artifact.user_id !== options.userId) {
|
|
149
|
+
return { ok: false, error: "Artifact not accessible" };
|
|
150
|
+
}
|
|
151
|
+
if (artifact.status !== "active") {
|
|
152
|
+
return { ok: false, error: "Artifact binary has expired", status: artifact.status };
|
|
153
|
+
}
|
|
154
|
+
if (!existsSync(artifact.path)) {
|
|
155
|
+
return { ok: false, error: "Artifact binary is missing", status: "missing" };
|
|
156
|
+
}
|
|
157
|
+
if (artifact.size > MAX_TEXT_ARTIFACT_BYTES) {
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
error: `Artifact is too large to read as text (${artifact.size} bytes, limit ${MAX_TEXT_ARTIFACT_BYTES})`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const bytes = readFileSync(artifact.path);
|
|
165
|
+
if (looksBinary(detectedMime(bytes, artifact.mime_type), bytes)) {
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
error: `Artifact is binary (${detectedMime(bytes, artifact.mime_type)}) — use artifact_inspect for its metadata`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { ok: true, text: bytes.toString("utf-8"), artifact };
|
|
173
|
+
}
|
|
174
|
+
|
|
94
175
|
export async function inspectArtifact(
|
|
95
176
|
artifactId: string,
|
|
96
177
|
options: { userId?: string } = {},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { col, fromIndexable } from "../storage/hive"
|
|
2
|
-
import type { AgentDoc, McpServerDoc } from "../storage/collections"
|
|
1
|
+
import { col, fromIndexable } from "../storage/hive.ts"
|
|
2
|
+
import type { AgentDoc, McpServerDoc } from "../storage/collections.ts"
|
|
3
3
|
|
|
4
4
|
export interface CanvasEvent {
|
|
5
5
|
type: CanvasEventType
|
|
@@ -3,7 +3,7 @@ import { BaseChannel, type ChannelConfig, type IncomingMessage, type OutboundMes
|
|
|
3
3
|
import { logger } from "../utils/logger.ts";
|
|
4
4
|
import { col, updateDoc } from "../storage/hive.ts";
|
|
5
5
|
import type { ChannelDoc, UserIdentityDoc } from "../storage/collections.ts";
|
|
6
|
-
import { resolveUserId } from "../storage/onboarding";
|
|
6
|
+
import { resolveUserId } from "../storage/onboarding.ts";
|
|
7
7
|
|
|
8
8
|
export interface TelegramConfig extends ChannelConfig {
|
|
9
9
|
botToken: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ServerWebSocket } from "bun";
|
|
2
2
|
import { BaseChannel, type ChannelConfig, type IncomingMessage, type OutboundMessage } from "./base.ts";
|
|
3
3
|
import { logger } from "../utils/logger.ts";
|
|
4
|
-
import { resolveUserId } from "../storage/onboarding";
|
|
4
|
+
import { resolveUserId } from "../storage/onboarding.ts";
|
|
5
5
|
|
|
6
6
|
export interface WebChatConfig extends ChannelConfig {
|
|
7
7
|
accountId?: string;
|