@johpaz/hive-sdk 0.2.0 → 0.3.1
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 +306 -0
- package/README.md +11 -3
- package/package.json +10 -4
- package/packages/core/src/agent/agent-catalog.ts +81 -24
- package/packages/core/src/agent/agent-loop.ts +2 -2
- package/packages/core/src/agent/compaction.ts +20 -1
- package/packages/core/src/agent/context-compiler.ts +7 -4
- package/packages/core/src/agent/conversation-store.ts +136 -2
- package/packages/core/src/agent/curator.ts +12 -3
- package/packages/core/src/agent/llm-providers/nvidia.ts +39 -0
- package/packages/core/src/agent/llm-providers/openai-compat-base.ts +38 -2
- package/packages/core/src/agent/playbook-selector.ts +18 -3
- package/packages/core/src/agent/prompt-builder.ts +2 -2
- package/packages/core/src/agent/providers/index.ts +37 -2
- package/packages/core/src/agent/reflector.ts +32 -9
- package/packages/core/src/agent/skill-selector.ts +2 -2
- package/packages/core/src/agent/thread-store.ts +43 -0
- package/packages/core/src/agent/tool-selector.ts +2 -0
- package/packages/core/src/api/createAgent.ts +68 -2
- package/packages/core/src/artifacts/index.ts +15 -0
- package/packages/core/src/artifacts/store.ts +77 -2
- package/packages/core/src/canvas/index.ts +9 -0
- package/packages/core/src/ethics/EthicsGuard.ts +7 -1
- package/packages/core/src/events/index.ts +18 -0
- 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/durable-queue.ts +13 -1
- package/packages/core/src/gateway/index.ts +3 -0
- package/packages/core/src/gateway/job-store.ts +6 -0
- 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/multimodal/vision-service.ts +45 -13
- package/packages/core/src/resilience/index.ts +13 -0
- package/packages/core/src/scheduler/CronScheduler.ts +48 -21
- 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 +16 -5
- 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 +5 -3
- 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 +74 -5
- package/packages/core/src/storage/collections.ts +106 -1
- package/packages/core/src/storage/crypto.ts +24 -7
- package/packages/core/src/storage/hive.ts +9 -3
- package/packages/core/src/storage/index.ts +2 -1
- package/packages/core/src/storage/onboarding.ts +59 -43
- package/packages/core/src/storage/reconcile.ts +6 -1
- package/packages/core/src/storage/seed.ts +98 -14
- 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/agents/index.ts +18 -60
- package/packages/core/src/tools/cli/index.ts +55 -0
- package/packages/core/src/tools/core/index.ts +50 -2
- package/packages/core/src/tools/cron/index.ts +4 -4
- 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
|
@@ -29,6 +29,8 @@ import { estimateTokens } from "../utils/toon.ts"
|
|
|
29
29
|
import { callLLM, resolveProviderConfig, getDefaultLLM, type ContentPart } from "./llm-client.ts"
|
|
30
30
|
import { col, fromIndexable } from "../storage/hive.ts"
|
|
31
31
|
import type { AgentDoc, ModelDoc } from "../storage/collections.ts"
|
|
32
|
+
import { loadConfig } from "../config/loader.ts"
|
|
33
|
+
import { runBeforeCompaction } from "../hooks/index.ts"
|
|
32
34
|
|
|
33
35
|
const log = logger.child("compaction")
|
|
34
36
|
|
|
@@ -51,8 +53,15 @@ export async function maybeCompact(
|
|
|
51
53
|
try {
|
|
52
54
|
const totalTokens = await getTotalTokens(threadId)
|
|
53
55
|
|
|
54
|
-
//
|
|
56
|
+
// Orden de precedencia: lo que el usuario configuró gana sobre lo que se
|
|
57
|
+
// deduce del modelo, y eso gana sobre la constante.
|
|
58
|
+
//
|
|
59
|
+
// `agent.context.compactionThreshold` estaba en el esquema de configuración
|
|
60
|
+
// y **no lo leía nadie**: alguien podía ajustarlo y no pasaba nada. Una
|
|
61
|
+
// opción que no hace nada es peor que no tenerla, porque el usuario cree
|
|
62
|
+
// que cambió algo.
|
|
55
63
|
let effectiveThreshold = COMPACT_TOKEN_THRESHOLD
|
|
64
|
+
const configurado = loadConfig().agent?.context?.compactionThreshold
|
|
56
65
|
try {
|
|
57
66
|
const agentsCol = await col<AgentDoc>("agents")
|
|
58
67
|
const coordinators = await agentsCol.findBy("role", "coordinator", { limit: 1 })
|
|
@@ -69,8 +78,18 @@ export async function maybeCompact(
|
|
|
69
78
|
}
|
|
70
79
|
} catch { /* use default threshold */ }
|
|
71
80
|
|
|
81
|
+
if (configurado && configurado > 0) effectiveThreshold = configurado
|
|
82
|
+
|
|
72
83
|
if (totalTokens < effectiveThreshold) return
|
|
73
84
|
|
|
85
|
+
// Avisar antes de comprimir: es la última oportunidad de que alguien
|
|
86
|
+
// conserve algo del historial que está por resumirse.
|
|
87
|
+
await runBeforeCompaction({
|
|
88
|
+
threadId,
|
|
89
|
+
messageCount: await getMessageCount(threadId),
|
|
90
|
+
totalTokens,
|
|
91
|
+
}).catch(() => {})
|
|
92
|
+
|
|
74
93
|
const summary = await getSummary(threadId)
|
|
75
94
|
const totalMessages = await getMessageCount(threadId)
|
|
76
95
|
|
|
@@ -29,7 +29,7 @@ import type { MCPClientManager } from "../mcp/index.ts"
|
|
|
29
29
|
import { syncToolCatalogToIndex, mcpToolFullName } from "./tool-selector.ts"
|
|
30
30
|
import { syncSkillsToIndex, getMinimalSkills, selectSkills, getSkillByName, type SkillDescriptor } from "./skill-selector.ts"
|
|
31
31
|
import { syncPlaybookToIndex, selectPlaybookRules } from "./playbook-selector.ts"
|
|
32
|
-
import { getRecentMessages, getSummary, getScratchpad, toAPIMessages } from "./conversation-store.ts"
|
|
32
|
+
import { getRecentMessages, getSummary, getScratchpad, toAPIMessages, inflateRecentImages } from "./conversation-store.ts"
|
|
33
33
|
import { formatContext, estimateTokens } from "../utils/toon.ts"
|
|
34
34
|
import { buildSystemPromptWithProjects } from "./prompt-builder.ts"
|
|
35
35
|
import { createAllTools } from "../tools/index.ts"
|
|
@@ -188,7 +188,7 @@ export async function compileContext(opts: {
|
|
|
188
188
|
|
|
189
189
|
// [STEP-1] Load agent config
|
|
190
190
|
log.info(`[context-compiler] [STEP-1] Loading agent config for id=${agentId}`)
|
|
191
|
-
let agent: ReturnType<typeof fromAgentDoc>
|
|
191
|
+
let agent: ReturnType<typeof fromAgentDoc> | undefined
|
|
192
192
|
try {
|
|
193
193
|
const agentsCol = await col<AgentDoc>("agents")
|
|
194
194
|
const entry = await agentsCol.get(agentId)
|
|
@@ -518,7 +518,10 @@ export async function compileContext(opts: {
|
|
|
518
518
|
// or — on a context-overflow retry that keeps only the LAST system message
|
|
519
519
|
// (openai-compat-base.ts) — silently replaces the real prompt. The summary
|
|
520
520
|
// lives in `systemPrompt` instead (see conversationSummarySection below).
|
|
521
|
-
|
|
521
|
+
// En el historial las imágenes son referencias, para no reenviarlas enteras en
|
|
522
|
+
// cada turno. Las de los últimos mensajes se vuelven a poner en línea: el
|
|
523
|
+
// modelo todavía puede necesitar mirarlas, y una referencia no se mira.
|
|
524
|
+
const messages: LLMMessage[] = await inflateRecentImages(toAPIMessages(recentMessages))
|
|
522
525
|
|
|
523
526
|
// [STEP-10] STRATEGY 4: ISOLATE — Build context based on agent role
|
|
524
527
|
log.info(`[context-compiler] [STEP-10] Building system prompt...`)
|
|
@@ -562,7 +565,7 @@ export async function compileContext(opts: {
|
|
|
562
565
|
: Array.isArray(playbookInput)
|
|
563
566
|
? playbookInput.filter((part) => part.type === "text").map((part) => (part as any).text).join("\n")
|
|
564
567
|
: String(playbookInput)
|
|
565
|
-
const playbookRules = (await selectPlaybookRules(playbookText)).filter((rule) => {
|
|
568
|
+
const playbookRules = (await selectPlaybookRules(playbookText, userId)).filter((rule) => {
|
|
566
569
|
if (!rule.applicable_to || !rule.applicable_to.includes("agent:")) return true
|
|
567
570
|
return isCatalogAgent ? rule.applicable_to.includes(`agent:${agent.id}`) : false
|
|
568
571
|
})
|
|
@@ -111,6 +111,66 @@ export function getRecentMessageCount(windowMs = 5 * 60_000): number {
|
|
|
111
111
|
return recentMessageTimestamps.length
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/** El threadId es `${userId}/${canal}/${peer}`, así que el dueño ya está ahí. */
|
|
115
|
+
async function resolveOwnerId(threadId: string): Promise<string> {
|
|
116
|
+
const { parseThreadId } = await import("./thread-id.ts")
|
|
117
|
+
return parseThreadId(threadId)?.userId ?? threadId
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Cuánto ocupa una imagen en la ventana de contexto.
|
|
122
|
+
*
|
|
123
|
+
* Los proveedores cobran por área, no por bytes: la fórmula es la de los
|
|
124
|
+
* modelos de visión más comunes (~750 px² por token). Es una estimación, pero
|
|
125
|
+
* cualquier estimación es infinitamente mejor que la anterior, que era cero: la
|
|
126
|
+
* compactación creía que un hilo con diez fotos ocupaba lo que ocupa su texto,
|
|
127
|
+
* y no se disparaba hasta que el proveedor rechazaba el turno.
|
|
128
|
+
*/
|
|
129
|
+
export function estimateImageTokens(width?: number | null, height?: number | null): number {
|
|
130
|
+
if (!width || !height) return 1_500; // desconocida: el promedio de una foto
|
|
131
|
+
return Math.ceil((width * height) / 750);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Cambia las imágenes en línea por referencias a un artefacto.
|
|
136
|
+
*
|
|
137
|
+
* El base64 se guardaba entero en `content_multimodal` y `toAPIMessages` lo
|
|
138
|
+
* devolvía al modelo **en cada turno siguiente**: cinco fotos en una
|
|
139
|
+
* conversación eran cinco fotos reenviadas una y otra vez. Guardar el archivo y
|
|
140
|
+
* dejar una referencia corta ese crecimiento de raíz.
|
|
141
|
+
*
|
|
142
|
+
* La imagen no se pierde: `inflateRecentImages` la vuelve a poner en línea para
|
|
143
|
+
* los últimos turnos, que es donde el modelo todavía puede necesitar mirarla.
|
|
144
|
+
*/
|
|
145
|
+
async function imagesToRefs(content: ContentPart[], userId: string): Promise<ContentPart[]> {
|
|
146
|
+
const { createArtifact } = await import("../artifacts/store.ts")
|
|
147
|
+
const out: ContentPart[] = []
|
|
148
|
+
|
|
149
|
+
for (const part of content) {
|
|
150
|
+
if (part.type !== "image_base64") { out.push(part); continue }
|
|
151
|
+
try {
|
|
152
|
+
const bytes = Uint8Array.from(Buffer.from((part as { base64: string }).base64, "base64"))
|
|
153
|
+
const mimeType = (part as { mimeType?: string }).mimeType || "image/jpeg"
|
|
154
|
+
// Si no se puede medir, no es una imagen. Guardarla igual crearía un
|
|
155
|
+
// artefacto de tipo "image" con basura adentro, que aparecería en la
|
|
156
|
+
// galería del usuario; es mejor dejarla como venía.
|
|
157
|
+
const { measureImage } = await import("../images/index.ts")
|
|
158
|
+
const meta = await measureImage(bytes)
|
|
159
|
+
|
|
160
|
+
const art = await createArtifact({
|
|
161
|
+
bytes, mimeType, kind: "image", userId,
|
|
162
|
+
width: meta.width, height: meta.height, expiresAt: null,
|
|
163
|
+
})
|
|
164
|
+
out.push({ type: "artifact_ref", artifact_id: art.id, mime_type: mimeType, width: meta.width, height: meta.height } as unknown as ContentPart)
|
|
165
|
+
} catch {
|
|
166
|
+
// No es una imagen medible, o no se pudo guardar: viaja como venía.
|
|
167
|
+
// Perderla sería peor que no optimizarla.
|
|
168
|
+
out.push(part)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return out
|
|
172
|
+
}
|
|
173
|
+
|
|
114
174
|
export async function addMessage(
|
|
115
175
|
threadId: string,
|
|
116
176
|
role: StoredMessage["role"],
|
|
@@ -121,6 +181,8 @@ export async function addMessage(
|
|
|
121
181
|
tool_call_id?: string
|
|
122
182
|
reasoning_content?: string
|
|
123
183
|
source?: MessageSource
|
|
184
|
+
/** Dueño de los artefactos que se creen para este mensaje (imágenes). */
|
|
185
|
+
userId?: string
|
|
124
186
|
}
|
|
125
187
|
): Promise<number> {
|
|
126
188
|
// Handle multimodal content by extracting text for the content column
|
|
@@ -130,7 +192,12 @@ export async function addMessage(
|
|
|
130
192
|
? content.filter(p => p.type === "text").map(p => (p as any).text).join("\n")
|
|
131
193
|
: String(content)
|
|
132
194
|
|
|
133
|
-
|
|
195
|
+
// Las imágenes se guardan como archivo y en el historial queda una
|
|
196
|
+
// referencia: el base64 entero se reenviaba al modelo en cada turno.
|
|
197
|
+
const partes = Array.isArray(content)
|
|
198
|
+
? await imagesToRefs(content, opts?.userId ?? (await resolveOwnerId(threadId)))
|
|
199
|
+
: null
|
|
200
|
+
const content_multimodal = partes ? JSON.stringify(partes) : null
|
|
134
201
|
const tool_calls_json = opts?.tool_calls ? JSON.stringify(opts.tool_calls) : null
|
|
135
202
|
|
|
136
203
|
const paddedSeq = await nextId(`conversations:${threadId}`)
|
|
@@ -150,7 +217,19 @@ export async function addMessage(
|
|
|
150
217
|
reasoning_content: opts?.reasoning_content ?? null,
|
|
151
218
|
source: opts?.source ?? "message",
|
|
152
219
|
// Estimate tokens: content + tool_calls JSON
|
|
153
|
-
|
|
220
|
+
// Las imágenes cuentan: antes sumaban cero y la compactación creía que un
|
|
221
|
+
// hilo lleno de fotos ocupaba lo que ocupa su texto.
|
|
222
|
+
token_count: Math.max(
|
|
223
|
+
1,
|
|
224
|
+
estimateTokens(textContent) +
|
|
225
|
+
estimateTokens(tool_calls_json ?? "") +
|
|
226
|
+
(partes ?? []).reduce((n, p) => {
|
|
227
|
+
const q = p as { type: string; width?: number | null; height?: number | null }
|
|
228
|
+
return q.type === "artifact_ref" || q.type === "image_base64" || q.type === "image_url"
|
|
229
|
+
? n + estimateImageTokens(q.width, q.height)
|
|
230
|
+
: n
|
|
231
|
+
}, 0),
|
|
232
|
+
),
|
|
154
233
|
created_at: now,
|
|
155
234
|
updated_at: now,
|
|
156
235
|
}, { expectedVersion: 0 })
|
|
@@ -252,6 +331,61 @@ export async function getMessagesAfter(threadId: string, afterId: number): Promi
|
|
|
252
331
|
|
|
253
332
|
// ─── Convert stored messages → LLMMessage array ───────────────────────────────
|
|
254
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Cuántos mensajes del final conservan sus imágenes en línea.
|
|
336
|
+
*
|
|
337
|
+
* Mismo criterio que `clearOldToolResults` (compaction.ts), que ya poda
|
|
338
|
+
* resultados viejos y deja intactos los recientes. Es el compromiso: el modelo
|
|
339
|
+
* puede volver a mirar una imagen de hace un rato —"¿qué decía la factura?"—
|
|
340
|
+
* sin que una conversación larga arrastre todas las fotos para siempre.
|
|
341
|
+
*/
|
|
342
|
+
export const KEEP_IMAGES_LAST_N = 6
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Vuelve a poner en línea las imágenes de los últimos mensajes.
|
|
346
|
+
*
|
|
347
|
+
* En el historial las imágenes son referencias (ver `imagesToRefs`), que no
|
|
348
|
+
* ocupan contexto pero tampoco se pueden mirar: un modelo de visión no ve una
|
|
349
|
+
* foto desde un id. Para los últimos `keepLastN` mensajes se leen del disco y
|
|
350
|
+
* se devuelven como base64; los anteriores quedan como referencia, con sus
|
|
351
|
+
* dimensiones, para que el modelo sepa que hubo una imagen y cuál.
|
|
352
|
+
*/
|
|
353
|
+
export async function inflateRecentImages(
|
|
354
|
+
messages: LLMMessage[],
|
|
355
|
+
keepLastN = KEEP_IMAGES_LAST_N,
|
|
356
|
+
): Promise<LLMMessage[]> {
|
|
357
|
+
const desde = Math.max(0, messages.length - keepLastN)
|
|
358
|
+
const tieneRefs = messages.slice(desde).some((m) =>
|
|
359
|
+
Array.isArray(m.content) && m.content.some((p) => (p as { type?: string }).type === "artifact_ref"))
|
|
360
|
+
if (!tieneRefs) return messages
|
|
361
|
+
|
|
362
|
+
const { readArtifactBytes } = await import("../artifacts/store.ts")
|
|
363
|
+
|
|
364
|
+
return Promise.all(messages.map(async (msg, i) => {
|
|
365
|
+
if (i < desde || !Array.isArray(msg.content)) return msg
|
|
366
|
+
|
|
367
|
+
const partes = await Promise.all(msg.content.map(async (part) => {
|
|
368
|
+
const p = part as { type: string; artifact_id?: string; mime_type?: string }
|
|
369
|
+
if (p.type !== "artifact_ref" || !p.artifact_id) return part
|
|
370
|
+
if (!String(p.mime_type ?? "").startsWith("image/")) return part
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
const datos = await readArtifactBytes(p.artifact_id)
|
|
374
|
+
if (!datos) return part // caducó o se borró: queda la referencia
|
|
375
|
+
return {
|
|
376
|
+
type: "image_base64",
|
|
377
|
+
base64: Buffer.from(datos.bytes).toString("base64"),
|
|
378
|
+
mimeType: datos.mimeType,
|
|
379
|
+
} as unknown as ContentPart
|
|
380
|
+
} catch {
|
|
381
|
+
return part
|
|
382
|
+
}
|
|
383
|
+
}))
|
|
384
|
+
|
|
385
|
+
return { ...msg, content: partes }
|
|
386
|
+
}))
|
|
387
|
+
}
|
|
388
|
+
|
|
255
389
|
export function toAPIMessages(rows: StoredMessage[]): LLMMessage[] {
|
|
256
390
|
return rows.map((r) => {
|
|
257
391
|
let content: string | ContentPart[] = r.content
|
|
@@ -201,7 +201,13 @@ async function processReflection(
|
|
|
201
201
|
|
|
202
202
|
// Check if a similar rule already exists (fuzzy check by first 60 chars)
|
|
203
203
|
const prefix = reflection.description.substring(0, 60)
|
|
204
|
-
|
|
204
|
+
// La deduplicación también va por usuario: si dos personas producen la misma
|
|
205
|
+
// observación, son dos reglas. Buscando sólo por texto, lo aprendido de la
|
|
206
|
+
// segunda reforzaría la regla de la primera y la haría pesar más en un
|
|
207
|
+
// playbook que no es suyo.
|
|
208
|
+
const existing = allPlaybook.find(
|
|
209
|
+
e => e.doc.active && e.doc.user_id === reflection.user_id && e.doc.rule.startsWith(prefix)
|
|
210
|
+
)
|
|
205
211
|
|
|
206
212
|
if (existing) {
|
|
207
213
|
// Reinforce existing rule
|
|
@@ -216,6 +222,7 @@ async function processReflection(
|
|
|
216
222
|
id,
|
|
217
223
|
rule: reflection.description,
|
|
218
224
|
category,
|
|
225
|
+
user_id: reflection.user_id,
|
|
219
226
|
applicable_to: applicableTo,
|
|
220
227
|
helpful_count: 1,
|
|
221
228
|
harmful_count: 0,
|
|
@@ -224,7 +231,7 @@ async function processReflection(
|
|
|
224
231
|
created_at: now,
|
|
225
232
|
updated_at: now,
|
|
226
233
|
}, { expectedVersion: 0 })
|
|
227
|
-
allPlaybook.push({ id, version: 1, doc: { id, rule: reflection.description, category, applicable_to: applicableTo, helpful_count: 1, harmful_count: 0, active: true, source_reflection_id: toIndexable(reflection.id), created_at: now, updated_at: now } })
|
|
234
|
+
allPlaybook.push({ id, version: 1, doc: { id, rule: reflection.description, category, user_id: reflection.user_id, applicable_to: applicableTo, helpful_count: 1, harmful_count: 0, active: true, source_reflection_id: toIndexable(reflection.id), created_at: now, updated_at: now } })
|
|
228
235
|
}
|
|
229
236
|
|
|
230
237
|
function mapInsightTypeToCategory(
|
|
@@ -248,12 +255,13 @@ async function addOrUpdateRule(
|
|
|
248
255
|
opts: {
|
|
249
256
|
rule: string
|
|
250
257
|
category: string
|
|
258
|
+
user_id: string
|
|
251
259
|
applicable_to: string | null
|
|
252
260
|
sourceReflectionId: string | null
|
|
253
261
|
}
|
|
254
262
|
): Promise<void> {
|
|
255
263
|
const prefix = opts.rule.substring(0, 60)
|
|
256
|
-
const existing = allPlaybook.find(e => e.doc.rule.startsWith(prefix))
|
|
264
|
+
const existing = allPlaybook.find(e => e.doc.user_id === opts.user_id && e.doc.rule.startsWith(prefix))
|
|
257
265
|
|
|
258
266
|
if (existing) {
|
|
259
267
|
await playbookCol.put(existing.id, { ...existing.doc, helpful_count: existing.doc.helpful_count + 1, updated_at: Date.now() }, { expectedVersion: existing.version })
|
|
@@ -264,6 +272,7 @@ async function addOrUpdateRule(
|
|
|
264
272
|
id,
|
|
265
273
|
rule: opts.rule,
|
|
266
274
|
category: opts.category as PlaybookDoc["category"],
|
|
275
|
+
user_id: opts.user_id,
|
|
267
276
|
applicable_to: opts.applicable_to,
|
|
268
277
|
helpful_count: 1,
|
|
269
278
|
harmful_count: 0,
|
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
import { OpenAICompatBase } from "./openai-compat-base.ts"
|
|
2
|
+
import type { LLMCallOptions } from "../llm-client.ts"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* NIM mantiene el razonamiento APAGADO por defecto en su endpoint compatible
|
|
6
|
+
* con OpenAI, y el interruptor no es el `reasoning_effort` de OpenAI sino
|
|
7
|
+
* `chat_template_kwargs`, con una clave distinta por familia de modelo.
|
|
8
|
+
*
|
|
9
|
+
* Verificado en vivo contra integrate.api.nvidia.com (2026-08-15):
|
|
10
|
+
* `z-ai/glm-5.2` no emite un solo `reasoning_content` sin esto — el delta trae
|
|
11
|
+
* únicamente `role` y `content` — y sí lo emite con `enable_thinking`. Por eso
|
|
12
|
+
* la app no mostraba razonamiento con los modelos de NVIDIA: no llegaba.
|
|
13
|
+
*
|
|
14
|
+
* Las familias que no están acá se dejan en paz a propósito. Nemotron 3 ya
|
|
15
|
+
* emite `reasoning_content` sin ningún kwarg (también verificado), y mandarle
|
|
16
|
+
* una clave que su plantilla no acepta es justamente lo que rompe la llamada.
|
|
17
|
+
*/
|
|
18
|
+
const THINKING_KWARGS: Array<{ pattern: RegExp; kwargs: Record<string, unknown> }> = [
|
|
19
|
+
// Verificado con z-ai/glm-5.2.
|
|
20
|
+
{ pattern: /glm/i, kwargs: { enable_thinking: true, clear_thinking: false } },
|
|
21
|
+
// Verificado con minimaxai/minimax-m3.
|
|
22
|
+
{ pattern: /minimax/i, kwargs: { thinking_mode: "enabled" } },
|
|
23
|
+
// Documentado por NVIDIA, sin verificar: moonshotai/kimi-k2.6 responde 404
|
|
24
|
+
// en la cuenta con la que se probó. Si la plantilla lo rechaza, el reintento
|
|
25
|
+
// de openai-compat-base repite la llamada sin kwargs.
|
|
26
|
+
{ pattern: /kimi|deepseek/i, kwargs: { thinking: true } },
|
|
27
|
+
{ pattern: /qwen|qwq/i, kwargs: { enable_thinking: true } },
|
|
28
|
+
]
|
|
2
29
|
|
|
3
30
|
export class NvidiaProvider extends OpenAICompatBase {
|
|
4
31
|
constructor() { super("nvidia") }
|
|
32
|
+
|
|
33
|
+
protected modifyRequestBody(body: any, options: LLMCallOptions): any {
|
|
34
|
+
if (!options.thinking?.enabled) return body
|
|
35
|
+
|
|
36
|
+
const match = THINKING_KWARGS.find(({ pattern }) => pattern.test(options.model))
|
|
37
|
+
if (!match) return body
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
...body,
|
|
41
|
+
chat_template_kwargs: { ...(body.chat_template_kwargs ?? {}), ...match.kwargs },
|
|
42
|
+
}
|
|
43
|
+
}
|
|
5
44
|
}
|
|
@@ -7,6 +7,32 @@ import {
|
|
|
7
7
|
import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface.ts"
|
|
8
8
|
import type { ContentPart, LLMMessage } from "../llm-client.ts"
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Statuses that mean "this body is malformed or unsupported" — the only ones
|
|
12
|
+
* where dropping the provider extras can possibly help.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately narrow. A first version retried on any status and immediately
|
|
15
|
+
* misfired in production against a 429: a rate limit is not a body problem, so
|
|
16
|
+
* the retry spent a second request against the very limit that rejected the
|
|
17
|
+
* first, and turned thinking off for nothing. Same for 401/403 (auth) and 404
|
|
18
|
+
* (unknown model).
|
|
19
|
+
*/
|
|
20
|
+
const EXTRAS_REJECTED_CODES = [400, 422]
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Drops the non-standard fields a provider added on top of an OpenAI-shaped
|
|
24
|
+
* body, for use on a retry after the body itself was rejected. Right now that
|
|
25
|
+
* is only NIM's `chat_template_kwargs` (nvidia.ts): it enables the model's
|
|
26
|
+
* thinking, so losing it costs the reasoning display and nothing else — a far
|
|
27
|
+
* better outcome than a turn that dies because one model's chat template did
|
|
28
|
+
* not recognize the switch.
|
|
29
|
+
*/
|
|
30
|
+
function stripProviderExtras(body: any): any {
|
|
31
|
+
if (!body?.chat_template_kwargs) return body
|
|
32
|
+
const { chat_template_kwargs: _dropped, ...rest } = body
|
|
33
|
+
return rest
|
|
34
|
+
}
|
|
35
|
+
|
|
10
36
|
const log = logger.child("llm-client")
|
|
11
37
|
|
|
12
38
|
/** Matches both generic "context length exceeded" phrasing and llama.cpp's exceed_context_size_error shape. */
|
|
@@ -187,7 +213,14 @@ export abstract class OpenAICompatBase implements LLMProvider {
|
|
|
187
213
|
delete bodyNoTools.tools
|
|
188
214
|
delete bodyNoTools.tool_choice
|
|
189
215
|
delete bodyNoTools.parallel_tool_calls
|
|
190
|
-
response = await client.chat.completions.create(this.modifyRequestBody(bodyNoTools, options), { signal: options.signal })
|
|
216
|
+
response = await client.chat.completions.create(stripProviderExtras(this.modifyRequestBody(bodyNoTools, options)), { signal: options.signal })
|
|
217
|
+
}
|
|
218
|
+
// Retry 3: the provider-specific extras are the only other thing we added
|
|
219
|
+
// to an otherwise standard body (NIM's chat_template_kwargs — see
|
|
220
|
+
// nvidia.ts). Losing the reasoning display beats failing the turn.
|
|
221
|
+
else if (EXTRAS_REJECTED_CODES.includes(status) && this.modifyRequestBody(body, options).chat_template_kwargs) {
|
|
222
|
+
log.warn(`[llm-client] ${this.providerName}: request rejected (HTTP ${status}) — retrying without chat_template_kwargs`)
|
|
223
|
+
response = await client.chat.completions.create(stripProviderExtras(this.modifyRequestBody(body, options)), { signal: options.signal })
|
|
191
224
|
}
|
|
192
225
|
else {
|
|
193
226
|
throw err
|
|
@@ -258,7 +291,10 @@ export abstract class OpenAICompatBase implements LLMProvider {
|
|
|
258
291
|
delete bodyNoTools.tools
|
|
259
292
|
delete bodyNoTools.tool_choice
|
|
260
293
|
delete bodyNoTools.parallel_tool_calls
|
|
261
|
-
stream = await client.chat.completions.create({ ...this.modifyRequestBody(bodyNoTools, options), stream: true }, { signal: options.signal })
|
|
294
|
+
stream = await client.chat.completions.create({ ...stripProviderExtras(this.modifyRequestBody(bodyNoTools, options)), stream: true }, { signal: options.signal })
|
|
295
|
+
} else if (EXTRAS_REJECTED_CODES.includes(status) && this.modifyRequestBody(body, options).chat_template_kwargs) {
|
|
296
|
+
log.warn(`[llm-client] ${this.providerName}: request rejected (HTTP ${status}) — retrying stream without chat_template_kwargs`)
|
|
297
|
+
stream = await client.chat.completions.create({ ...stripProviderExtras(this.modifyRequestBody(body, options)), stream: true }, { signal: options.signal })
|
|
262
298
|
} else {
|
|
263
299
|
throw err
|
|
264
300
|
}
|
|
@@ -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,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { col } from "../storage/hive.ts"
|
|
17
|
-
import type { EthicsDoc, AgentDoc, UserDoc } from "../storage/collections
|
|
17
|
+
import type { EthicsDoc, AgentDoc, UserDoc } from "../storage/collections"
|
|
18
18
|
import { logger } from "../utils/logger.ts"
|
|
19
19
|
import { formatContext } from "../utils/toon.ts"
|
|
20
20
|
import { resolveUserId } from "../storage/onboarding.ts"
|
|
@@ -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`
|
|
@@ -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.ts"
|
|
10
|
+
import { getAgentLoop, buildAgentLoop } from "../agent-loop.ts"
|
|
11
11
|
import { resolveUserId, resolveAgentId } from "../../storage/onboarding.ts"
|
|
12
12
|
import type { ContentPart } from "../../multimodal/types.ts"
|
|
13
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,9 +116,10 @@ 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
|
-
let toolCalls: ModelResponse["toolCalls"] = []
|
|
122
|
+
let toolCalls: NonNullable<ModelResponse["toolCalls"]> = []
|
|
93
123
|
let totalInputTokens = 0
|
|
94
124
|
let totalOutputTokens = 0
|
|
95
125
|
|
|
@@ -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)
|
|
@@ -17,6 +17,7 @@ import { getHiveDb } from "../storage/hivedb.ts"
|
|
|
17
17
|
import { loadConfig } from "../config/loader.ts"
|
|
18
18
|
import type { HiveDB, ToolStats } from "@johpaz/hive-db"
|
|
19
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 —
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { col } from "../storage/hive.ts"
|
|
20
|
-
import type { SkillDoc } from "../storage/collections
|
|
20
|
+
import type { SkillDoc } from "../storage/collections"
|
|
21
21
|
import { logger } from "../utils/logger.ts"
|
|
22
22
|
import { isMinimalSkill } from "./minimal-loadout.ts"
|
|
23
23
|
import { isCalendarOperation } from "./routing-intent.ts"
|
|
@@ -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 */
|