@johpaz/hive-sdk 0.4.8 → 0.4.9
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 +23 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/packages/core/src/agent/agent-loop.ts +10 -2
- package/packages/core/src/agent/compaction.ts +81 -35
package/CHANGELOG.md
CHANGED
|
@@ -579,6 +579,29 @@
|
|
|
579
579
|
job que genera un scaffold con `create-app` y lo typechequea contra el SDK de
|
|
580
580
|
ese commit.
|
|
581
581
|
|
|
582
|
+
## 0.4.9
|
|
583
|
+
|
|
584
|
+
### Corregido
|
|
585
|
+
|
|
586
|
+
- **La compactación de historial dejaba de ser excepcional y corría casi en cada
|
|
587
|
+
turno.** `agent.context.compactionThreshold` es una proporción de la ventana
|
|
588
|
+
del modelo —su valor por defecto es `0.8`, o sea el 80 %— pero se leía como un
|
|
589
|
+
número de tokens: el umbral efectivo quedaba en 0.8 tokens. Cualquier hilo con
|
|
590
|
+
más de cinco mensajes se resumía en cada turno, lo que cuesta una llamada extra
|
|
591
|
+
al modelo por mensaje y reemplaza el historial por un resumen desde el primer
|
|
592
|
+
intercambio. Ahora un valor menor o igual a 1 se aplica sobre la ventana del
|
|
593
|
+
modelo y uno mayor se sigue leyendo como tokens, para quien fijó un número
|
|
594
|
+
absoluto. El umbral se calcula además con la ventana del modelo que corre el
|
|
595
|
+
turno, no con la del coordinador.
|
|
596
|
+
- **El resumen se pedía con una credencial global.** `compactThread` resolvía el
|
|
597
|
+
modelo con `getDefaultLLM()` y llamaba a `resolveProviderConfig` sin
|
|
598
|
+
credenciales, así que la llave salía del secret store, del llavero del sistema
|
|
599
|
+
o del entorno del proceso. En una instalación de un solo usuario da igual; en
|
|
600
|
+
una multi-inquilino significa resumir la conversación de un cliente con la
|
|
601
|
+
llave de la plataforma o de otro cliente. `maybeCompact` y `compactThread`
|
|
602
|
+
aceptan ahora el modelo y las credenciales del turno (`CompactionLLM`), y el
|
|
603
|
+
agent loop les pasa los suyos. Sin ese dato se comportan como antes.
|
|
604
|
+
|
|
582
605
|
## 0.1.5
|
|
583
606
|
|
|
584
607
|
Sincronización del SDK con el runtime de agentes de `hive`. **Trae rupturas de
|
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johpaz/hive-sdk",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Hive SDK — The Agent Harness SDK. Build, deploy, and scale AI agent applications with multi-channel support, context engineering, and swarm orchestration.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -357,12 +357,20 @@ export async function* runAgent(
|
|
|
357
357
|
channel: opts.channel,
|
|
358
358
|
source: opts.historySource ?? "message",
|
|
359
359
|
})
|
|
360
|
-
// Run compaction if conversation history is getting large
|
|
360
|
+
// Run compaction if conversation history is getting large.
|
|
361
|
+
// El modelo del turno viaja con sus credenciales: el resumen es una llamada
|
|
362
|
+
// al modelo como cualquier otra y tiene que cobrarse a la misma cuenta.
|
|
361
363
|
await maybeCompact(
|
|
362
364
|
opts.threadId,
|
|
363
365
|
opts.channel && opts.userId
|
|
364
366
|
? { channel: opts.channel, userId: opts.userId }
|
|
365
|
-
: undefined
|
|
367
|
+
: undefined,
|
|
368
|
+
{
|
|
369
|
+
provider: providerCfg.provider,
|
|
370
|
+
model: providerCfg.model,
|
|
371
|
+
credentials: opts.credentials,
|
|
372
|
+
contextWindow: providerCfg.contextWindow,
|
|
373
|
+
}
|
|
366
374
|
)
|
|
367
375
|
}
|
|
368
376
|
|
|
@@ -26,7 +26,10 @@ import {
|
|
|
26
26
|
type StoredMessage,
|
|
27
27
|
} from "./conversation-store.ts"
|
|
28
28
|
import { estimateTokens } from "../utils/toon.ts"
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
callLLM, resolveProviderConfig, getDefaultLLM,
|
|
31
|
+
type ContentPart, type ProviderCredentials,
|
|
32
|
+
} from "./llm-client.ts"
|
|
30
33
|
import { col, fromIndexable } from "../storage/hive.ts"
|
|
31
34
|
import type { AgentDoc, ModelDoc } from "../storage/collections.ts"
|
|
32
35
|
import { loadConfig } from "../config/loader.ts"
|
|
@@ -41,6 +44,61 @@ const KEEP_LAST_N_MESSAGES = 5 // always keep most recent N messages
|
|
|
41
44
|
const TOOL_RESULT_MAX_CHARS = 200 // max chars for old tool results after clearing
|
|
42
45
|
const MAX_TRANSCRIPT_MSGS = 30 // cap messages sent to summarizer (avoids OOM on small models)
|
|
43
46
|
const MAX_MSG_CHARS = 300 // chars per message in transcript
|
|
47
|
+
/** Ventana asumida cuando no se conoce la del modelo: `COMPACT_TOKEN_THRESHOLD` es su 25 %. */
|
|
48
|
+
const ASSUMED_CONTEXT_WINDOW = 128_000
|
|
49
|
+
const DEFAULT_CONTEXT_RATIO = 0.25
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* El modelo con el que se pide el resumen: el del turno que disparó la
|
|
53
|
+
* compactación, con SUS credenciales.
|
|
54
|
+
*/
|
|
55
|
+
export interface CompactionLLM {
|
|
56
|
+
provider?: string
|
|
57
|
+
model?: string
|
|
58
|
+
/** En multi-inquilino, la llave del cliente. Sin esto se usaría una global. */
|
|
59
|
+
credentials?: ProviderCredentials
|
|
60
|
+
/** La ventana del modelo, si quien llama ya la resolvió. */
|
|
61
|
+
contextWindow?: number
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A partir de cuántos tokens de historial se compacta.
|
|
66
|
+
*
|
|
67
|
+
* `agent.context.compactionThreshold` es una PROPORCIÓN de la ventana del
|
|
68
|
+
* modelo —su valor por defecto es 0.8, o sea el 80 %—, pero se leía como si
|
|
69
|
+
* fueran tokens. Con la configuración por defecto el umbral quedaba en 0.8
|
|
70
|
+
* tokens: cualquier hilo con más de cinco mensajes se resumía en cada turno,
|
|
71
|
+
* pagando una llamada extra al modelo y reemplazando el historial por un
|
|
72
|
+
* resumen desde el primer intercambio. Un valor mayor que 1 se sigue leyendo
|
|
73
|
+
* como tokens, que es lo que espera quien fijó un número absoluto.
|
|
74
|
+
*/
|
|
75
|
+
export function resolveCompactionThreshold(configured: number | undefined, contextWindow?: number): number {
|
|
76
|
+
const known = contextWindow && contextWindow > 0 ? contextWindow : undefined
|
|
77
|
+
if (typeof configured === "number" && Number.isFinite(configured) && configured > 0) {
|
|
78
|
+
return configured <= 1
|
|
79
|
+
? Math.floor((known ?? ASSUMED_CONTEXT_WINDOW) * configured)
|
|
80
|
+
: Math.floor(configured)
|
|
81
|
+
}
|
|
82
|
+
return known ? Math.floor(known * DEFAULT_CONTEXT_RATIO) : COMPACT_TOKEN_THRESHOLD
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** La ventana del modelo del turno; si no se sabe cuál es, la del coordinador. */
|
|
86
|
+
async function modelContextWindow(modelId?: string): Promise<number | undefined> {
|
|
87
|
+
try {
|
|
88
|
+
const modelsCol = await col<ModelDoc>("models")
|
|
89
|
+
if (modelId) return (await modelsCol.get(modelId))?.doc.context_window || undefined
|
|
90
|
+
const agentsCol = await col<AgentDoc>("agents")
|
|
91
|
+
const coordinators = await agentsCol.findBy("role", "coordinator", { limit: 1 })
|
|
92
|
+
// El id se busca completo: recortar el primer segmento rompía cualquier
|
|
93
|
+
// modelo cuyo nombre lleve barra (meta/llama-3.3-70b-instruct buscaba
|
|
94
|
+
// "llama-3.3-70b-instruct", no encontraba nada y caía al default).
|
|
95
|
+
const id = fromIndexable(coordinators[0]?.doc.model_id ?? null)
|
|
96
|
+
if (!id) return undefined
|
|
97
|
+
return (await modelsCol.get(id))?.doc.context_window || undefined
|
|
98
|
+
} catch {
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
101
|
+
}
|
|
44
102
|
|
|
45
103
|
/**
|
|
46
104
|
* Check if compaction is needed and run it if so.
|
|
@@ -48,37 +106,16 @@ const MAX_MSG_CHARS = 300 // chars per message in transcript
|
|
|
48
106
|
*/
|
|
49
107
|
export async function maybeCompact(
|
|
50
108
|
threadId: string,
|
|
51
|
-
notify?: { channel: string; userId: string }
|
|
109
|
+
notify?: { channel: string; userId: string },
|
|
110
|
+
llm?: CompactionLLM
|
|
52
111
|
): Promise<void> {
|
|
53
112
|
try {
|
|
54
113
|
const totalTokens = await getTotalTokens(threadId)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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.
|
|
63
|
-
let effectiveThreshold = COMPACT_TOKEN_THRESHOLD
|
|
64
|
-
const configurado = loadConfig().agent?.context?.compactionThreshold
|
|
65
|
-
try {
|
|
66
|
-
const agentsCol = await col<AgentDoc>("agents")
|
|
67
|
-
const coordinators = await agentsCol.findBy("role", "coordinator", { limit: 1 })
|
|
68
|
-
const modelId = fromIndexable(coordinators[0]?.doc.model_id ?? null)
|
|
69
|
-
if (modelId) {
|
|
70
|
-
const modelsCol = await col<ModelDoc>("models")
|
|
71
|
-
// El id se busca completo: recortar el primer segmento rompía cualquier
|
|
72
|
-
// modelo cuyo nombre lleve barra (meta/llama-3.3-70b-instruct buscaba
|
|
73
|
-
// "llama-3.3-70b-instruct", no encontraba nada y caía al default).
|
|
74
|
-
const modelEntry = await modelsCol.get(modelId)
|
|
75
|
-
if (modelEntry?.doc.context_window) {
|
|
76
|
-
effectiveThreshold = Math.floor(modelEntry.doc.context_window * 0.25)
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
} catch { /* use default threshold */ }
|
|
80
|
-
|
|
81
|
-
if (configurado && configurado > 0) effectiveThreshold = configurado
|
|
114
|
+
const contextWindow = llm?.contextWindow ?? (await modelContextWindow(llm?.model))
|
|
115
|
+
const effectiveThreshold = resolveCompactionThreshold(
|
|
116
|
+
loadConfig().agent?.context?.compactionThreshold,
|
|
117
|
+
contextWindow,
|
|
118
|
+
)
|
|
82
119
|
|
|
83
120
|
if (totalTokens < effectiveThreshold) return
|
|
84
121
|
|
|
@@ -96,8 +133,8 @@ export async function maybeCompact(
|
|
|
96
133
|
// Already summarized up to near the current state
|
|
97
134
|
if (summary && summary.last_message_id > totalMessages - KEEP_LAST_N_MESSAGES) return
|
|
98
135
|
|
|
99
|
-
log.info(`[compaction] Compacting thread=${threadId} tokens=${totalTokens}`)
|
|
100
|
-
await compactThread(threadId, notify)
|
|
136
|
+
log.info(`[compaction] Compacting thread=${threadId} tokens=${totalTokens} threshold=${effectiveThreshold}`)
|
|
137
|
+
await compactThread(threadId, notify, llm)
|
|
101
138
|
} catch (err) {
|
|
102
139
|
log.warn("[compaction] Error during compaction check:", err)
|
|
103
140
|
}
|
|
@@ -139,7 +176,8 @@ export function renderTranscript(rows: StoredMessage[], maxMsgChars = MAX_MSG_CH
|
|
|
139
176
|
*/
|
|
140
177
|
export async function compactThread(
|
|
141
178
|
threadId: string,
|
|
142
|
-
notify?: { channel: string; userId: string }
|
|
179
|
+
notify?: { channel: string; userId: string },
|
|
180
|
+
llm?: CompactionLLM
|
|
143
181
|
): Promise<void> {
|
|
144
182
|
const allMessages = await getHistory(threadId)
|
|
145
183
|
if (allMessages.length <= KEEP_LAST_N_MESSAGES) return
|
|
@@ -162,10 +200,18 @@ export async function compactThread(
|
|
|
162
200
|
const capped = toSummarize.slice(-MAX_TRANSCRIPT_MSGS)
|
|
163
201
|
const transcript = renderTranscript(capped)
|
|
164
202
|
|
|
165
|
-
|
|
166
|
-
|
|
203
|
+
// El modelo del turno y SUS credenciales. Antes el resumen se pedía siempre
|
|
204
|
+
// con `getDefaultLLM()` y sin credenciales, así que `resolveProviderConfig`
|
|
205
|
+
// caía al secret store, al llavero del sistema o al entorno: en una
|
|
206
|
+
// instalación multi-inquilino eso resume la conversación de un cliente con
|
|
207
|
+
// la llave de la plataforma (o de otro cliente). Sin `llm` se comporta como
|
|
208
|
+
// antes, que es lo que necesita una instalación de un solo usuario.
|
|
209
|
+
const target = llm?.provider && llm.model
|
|
210
|
+
? { provider: llm.provider, model: llm.model }
|
|
211
|
+
: await getDefaultLLM()
|
|
212
|
+
if (!target) throw new Error("No active LLM providers/models configured in the database")
|
|
167
213
|
|
|
168
|
-
const providerCfg = await resolveProviderConfig(
|
|
214
|
+
const providerCfg = await resolveProviderConfig(target.provider, target.model, llm?.credentials)
|
|
169
215
|
|
|
170
216
|
const summaryResponse = await callLLM({
|
|
171
217
|
...providerCfg,
|