@johpaz/hive-sdk 0.4.9 → 0.5.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 +52 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/packages/core/src/agent/agent-loop.ts +82 -5
- package/packages/core/src/agent/context-compiler.ts +122 -23
- package/packages/core/src/agent/index.ts +2 -0
- package/packages/core/src/agent/jev-decisions.ts +199 -0
- package/packages/core/src/agent/jev-planner.ts +298 -0
- package/packages/core/src/agent/tool-selector.ts +1 -0
- package/packages/core/src/canvas/emitter.ts +19 -0
- package/packages/core/src/events/channel-narration.ts +3 -0
- package/packages/core/src/services/swarms.ts +4 -0
- package/packages/core/src/storage/collections.ts +9 -2
- package/packages/core/src/storage/crypto.ts +15 -7
- package/packages/core/src/storage/seed.ts +4 -0
- package/packages/core/src/storage/usage.ts +55 -2
- package/packages/core/src/swarm/RoleSwarm.ts +6 -0
- package/packages/core/src/tool-runtime/index.ts +20 -1
- package/packages/core/src/tools/agents/get-available-models.ts +3 -1
- package/packages/core/src/tools/core/index.ts +33 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### Jev — plano de decisión (OpenRouter Decisions)
|
|
6
|
+
|
|
7
|
+
Portado de hive 1.1.0. Jev decide por turno qué historial, herramientas,
|
|
8
|
+
skills, notas y reglas del playbook entran al contexto; entre iteraciones poda
|
|
9
|
+
resultados viejos de herramientas y sugiere la siguiente acción; decide si un
|
|
10
|
+
lote de herramientas corre en paralelo, y conoce el mapa del enjambre
|
|
11
|
+
(especialistas y estado de cada MCP) para recomendar a quién delegar.
|
|
12
|
+
**Sin clave de OpenRouter, Jev no existe y todo corre igual que antes.**
|
|
13
|
+
|
|
14
|
+
- **Clave inyectable por llamada**: `jev?: { apiKey, mcpSettingsPath? } | false`
|
|
15
|
+
en `AgentLoopOptions`, `compileContext`, `IsolatedAgentOptions`,
|
|
16
|
+
`runRoleSwarm` y `runSwarm`, igual que `credentials`. `false` lo apaga;
|
|
17
|
+
sin la opción decide la fila `openrouter` del inquilino actual. Con un
|
|
18
|
+
inquilino activo **nunca** se usa `OPENROUTER_API_KEY` ni la caché de
|
|
19
|
+
secretos del proceso: la clave de la plataforma no se usa en nombre de un
|
|
20
|
+
cliente.
|
|
21
|
+
- **Estado por inquilino**: fallos, cooldown y totales se llevan por
|
|
22
|
+
`currentTenant()`; una clave inválida de un cliente no pone en fallback a
|
|
23
|
+
los demás.
|
|
24
|
+
- **Evento para el host**: cada decisión llega por `onStep` como
|
|
25
|
+
`StepEvent` `jev_decision` (`jev`: agente, tipo, resumen, tokens ahorrados,
|
|
26
|
+
latencia, costo, especialista recomendado, MCP apagados). También se emite
|
|
27
|
+
`canvas:jev_decision` / `canvas:jev_status` para hosts tipo hive.
|
|
28
|
+
- **Uso y costo**: `recordJevDecision` y los campos `jev*` de
|
|
29
|
+
`UsageRollupDoc`; `getUsageStats()` devuelve `jev` con el total y el
|
|
30
|
+
desglose por agente. El ahorro se estima (caracteres/4) y se cotiza con el
|
|
31
|
+
modelo del agente asesorado.
|
|
32
|
+
- **Catálogo**: modelo `openrouter/typesafe/jev-1.13` con `modelType:
|
|
33
|
+
"decision"`, excluido de `get_available_models` (y de `getDefaultLLM`, que
|
|
34
|
+
sólo toma modelos `llm`).
|
|
35
|
+
- Herramienta nueva `conversation_read`: recupera mensajes o notas que Jev
|
|
36
|
+
dejó fuera del contexto, siempre dentro del hilo actual.
|
|
37
|
+
- `executeToolBatch` acepta `parallelToolCalls` por lote.
|
|
38
|
+
- `NarrationEventDoc.kind` suma `"decision"`: un host puede anotar las
|
|
39
|
+
decisiones de Jev en `narrationEvents` para sus vistas de actividad.
|
|
40
|
+
`shouldDeliverToChannel` nunca lo entrega a un canal.
|
|
41
|
+
- `loadDurableProviderApiKey(id)`: lee la clave sólo de la colección
|
|
42
|
+
`secrets` (particionada por inquilino), sin pasar por la caché de proceso ni
|
|
43
|
+
el llavero del SO.
|
|
44
|
+
|
|
45
|
+
**Privacidad.** Con Jev activo se envían a la API de decisiones de OpenRouter
|
|
46
|
+
(`https://openrouter.ai/api/alpha/decisions`), con la clave del workspace:
|
|
47
|
+
el objetivo del turno (hasta 3 500 caracteres), extractos de hasta 450
|
|
48
|
+
caracteres de mensajes previos del hilo, nombre y descripción de herramientas
|
|
49
|
+
y skills candidatas, notas del scratchpad y reglas del playbook (hasta 350
|
|
50
|
+
caracteres cada una), extractos de resultados de herramientas (hasta 650
|
|
51
|
+
caracteres), argumentos de llamadas en lote (hasta 700) y el mapa del enjambre
|
|
52
|
+
(ids, nombres y descripciones de especialistas, nombres y estado de los MCP).
|
|
53
|
+
No se envían ids de MCP con prefijo de inquilino, credenciales ni adjuntos
|
|
54
|
+
binarios. Un host que no quiera enviar nada pasa `jev: false`.
|
|
4
55
|
|
|
5
56
|
### Plataforma y seguridad
|
|
6
57
|
|
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johpaz/hive-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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",
|
|
@@ -23,9 +23,12 @@ import { callLLM, resolveProviderConfig, getDefaultLLM, type LLMMessage, type Pr
|
|
|
23
23
|
import { addMessage } from "./conversation-store.ts"
|
|
24
24
|
import { saveTrace, recordLLMUsage } from "./tracer.ts"
|
|
25
25
|
import { maybeCompact, clearOldToolResults } from "./compaction.ts"
|
|
26
|
-
import { emitCanvas } from "../canvas/emitter.ts"
|
|
26
|
+
import { emitCanvas, type CanvasJevDecision } from "../canvas/emitter.ts"
|
|
27
27
|
import type { MCPClientManager } from "../mcp/index.ts"
|
|
28
28
|
import { compileContext } from "./context-compiler.ts"
|
|
29
|
+
import { MINIMAL_TOOLS } from "./minimal-loadout.ts"
|
|
30
|
+
import { jevWantsParallel, planJevIteration } from "./jev-planner.ts"
|
|
31
|
+
import { emitJevDecision, type JevOption } from "./jev-decisions.ts"
|
|
29
32
|
import { formatToolResult } from "../utils/toon.ts"
|
|
30
33
|
import { redactBinaryStrings } from "../utils/redact-binary.ts"
|
|
31
34
|
import { resolveUserId, resolveAgentId } from "../storage/onboarding.ts"
|
|
@@ -52,6 +55,10 @@ import { getNarration } from "../events/tool-narration.ts"
|
|
|
52
55
|
|
|
53
56
|
const log = logger.child("agent-loop")
|
|
54
57
|
|
|
58
|
+
const JEV_ACTION_LABELS: Record<string, string> = {
|
|
59
|
+
continue: "Continuar", delegate: "Delegar", discover: "Descubrir", finish: "Cerrar",
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
// Per-operation budget for a single LLM call — NOT an aggregate deadline for the
|
|
56
63
|
// whole turn. Each call gets its own fresh window; a slow-but-healthy multi-step
|
|
57
64
|
// turn (many quick operations) is never killed just for taking a while overall.
|
|
@@ -226,6 +233,12 @@ export interface AgentLoopOptions {
|
|
|
226
233
|
* dos inquilinos concurrentes en el mismo proceso compartían credencial.
|
|
227
234
|
*/
|
|
228
235
|
credentials?: ProviderCredentials
|
|
236
|
+
/**
|
|
237
|
+
* Jev (OpenRouter Decisions) for this run. `{ apiKey }` uses that key,
|
|
238
|
+
* `false` turns it off, undefined reads the current tenant's `openrouter`
|
|
239
|
+
* provider row. Travels with the run like `credentials`.
|
|
240
|
+
*/
|
|
241
|
+
jev?: JevOption
|
|
229
242
|
/** Whether to resume from a previously saved checkpoint */
|
|
230
243
|
resume?: boolean
|
|
231
244
|
/** Run budget — overrides agent.max_iterations when set */
|
|
@@ -258,12 +271,17 @@ export interface AgentLoopOptions {
|
|
|
258
271
|
export type { StepEvent as AgentStepEvent }
|
|
259
272
|
|
|
260
273
|
export interface StepEvent {
|
|
261
|
-
type: "text" | "tool_call" | "tool_result"
|
|
274
|
+
type: "text" | "tool_call" | "tool_result" | "jev_decision"
|
|
262
275
|
message: string
|
|
263
276
|
toolName?: string
|
|
264
277
|
isError?: boolean
|
|
278
|
+
/** Present on `jev_decision`: what Jev decided for this run, its cost and the estimated savings. */
|
|
279
|
+
jev?: JevStepDecision
|
|
265
280
|
}
|
|
266
281
|
|
|
282
|
+
/** One Jev decision as the host receives it through `onStep`. */
|
|
283
|
+
export type JevStepDecision = CanvasJevDecision
|
|
284
|
+
|
|
267
285
|
// ─── Stream chunk types (compatible with providers/index.ts) ─────────────────
|
|
268
286
|
|
|
269
287
|
export interface StreamChunk {
|
|
@@ -385,8 +403,25 @@ export async function* runAgent(
|
|
|
385
403
|
taskContext: opts.taskContext,
|
|
386
404
|
userId: opts.userId,
|
|
387
405
|
causalStreamId,
|
|
406
|
+
skipJev: !!opts.resume,
|
|
407
|
+
jev: opts.jev,
|
|
388
408
|
})
|
|
389
409
|
|
|
410
|
+
// Every decision goes to the canvas (hosts like hive) and to onStep (hosts
|
|
411
|
+
// that drive runAgent themselves, like hive-cloud).
|
|
412
|
+
const publishJev = async (decision: Parameters<typeof emitJevDecision>[0]): Promise<void> => {
|
|
413
|
+
const event = emitJevDecision(decision)
|
|
414
|
+
if (!opts.onStep) return
|
|
415
|
+
try {
|
|
416
|
+
await opts.onStep({ type: "jev_decision", message: event.summary, jev: event })
|
|
417
|
+
} catch (err) {
|
|
418
|
+
log.warn(`[agent-loop] onStep(jev_decision) failed: ${(err as Error).message}`)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (ctx.jevDecision) {
|
|
422
|
+
await publishJev({ ...ctx.jevDecision, agentId: opts.agentId, kind: "context", provider: providerCfg.provider, model: providerCfg.model })
|
|
423
|
+
}
|
|
424
|
+
|
|
390
425
|
// Force extra tools into the loadout (tests/evals)
|
|
391
426
|
if (opts.extraTools?.length) {
|
|
392
427
|
const existingNames = new Set(ctx.tools.map((t: any) => t.function?.name))
|
|
@@ -417,9 +452,12 @@ export async function* runAgent(
|
|
|
417
452
|
if (opts.isolated) {
|
|
418
453
|
messages.push({ role: "user", content: opts.userMessage })
|
|
419
454
|
}
|
|
455
|
+
const jevObjective = typeof opts.userMessage === "string" ? opts.userMessage :
|
|
456
|
+
opts.userMessage.filter((part) => part.type === "text").map((part) => (part as { text: string }).text).join("\n")
|
|
420
457
|
|
|
421
458
|
// ── Resume from checkpoint ─────────────────────────────────────────────────
|
|
422
|
-
|
|
459
|
+
// Seeded with the compiled loadout so a checkpoint records the tools Jev chose.
|
|
460
|
+
let injectedToolNames: string[] = ctx.tools.map(t => t.function.name).filter(name => !MINIMAL_TOOLS.has(name))
|
|
423
461
|
let systemPromptSkillSections: string[] = []
|
|
424
462
|
let resumedFromPending = false
|
|
425
463
|
let iterations = 0
|
|
@@ -440,6 +478,15 @@ export async function* runAgent(
|
|
|
440
478
|
if (restored) {
|
|
441
479
|
messages = restored.messages
|
|
442
480
|
injectedToolNames = restored.injectedToolNames ?? []
|
|
481
|
+
// A resume skips Jev: restore the loadout the checkpoint recorded.
|
|
482
|
+
const currentTools = new Set(ctx.tools.map(t => t.function.name))
|
|
483
|
+
for (const name of injectedToolNames) {
|
|
484
|
+
const tool = ctx.allTools.find(t => t.name === name)
|
|
485
|
+
if (tool && !currentTools.has(name)) {
|
|
486
|
+
ctx.tools.push({ type: "function", function: { name: tool.name, description: tool.description, parameters: tool.parameters } })
|
|
487
|
+
currentTools.add(name)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
443
490
|
systemPromptSkillSections = restored.systemPromptSkillSections ?? []
|
|
444
491
|
iterations = restored.iterations ?? 0
|
|
445
492
|
totalInputTokens = restored.totalInputTokens ?? 0
|
|
@@ -525,11 +572,27 @@ export async function* runAgent(
|
|
|
525
572
|
: null
|
|
526
573
|
let streamedThisCall = false
|
|
527
574
|
let response: Awaited<ReturnType<typeof callLLM>>
|
|
575
|
+
const jevIteration = await planJevIteration({ objective: jevObjective, messages, tools: ctx.tools, jev: opts.jev })
|
|
576
|
+
.catch((err) => { log.warn(`[agent-loop] Jev iteration fallback: ${(err as Error).message}`); return null })
|
|
577
|
+
const callMessages = jevIteration?.messages ?? messages
|
|
578
|
+
const callTools = jevIteration?.tools ?? ctx.tools
|
|
579
|
+
if (jevIteration) {
|
|
580
|
+
log.info(`[agent-loop] Jev action=${jevIteration.action} omitted_results=${jevIteration.omittedResults} tools=${callTools.map(t => t.function.name).join(",")}`)
|
|
581
|
+
// Measured on what the provider actually receives, after the usual truncation.
|
|
582
|
+
const payloadChars = (msgs: LLMMessage[], tools: typeof ctx.tools) =>
|
|
583
|
+
JSON.stringify(clearOldToolResults(msgs)).length + JSON.stringify(tools).length
|
|
584
|
+
await publishJev({
|
|
585
|
+
agentId: opts.agentId, kind: "iteration", provider: providerCfg.provider, model: providerCfg.model,
|
|
586
|
+
summary: `${JEV_ACTION_LABELS[jevIteration.action] ?? jevIteration.action} · ${jevIteration.omittedResults} resultado(s) omitido(s) · ${callTools.length}/${ctx.tools.length} herramientas`,
|
|
587
|
+
savedTokens: Math.round((payloadChars(messages, ctx.tools) - payloadChars(callMessages, callTools)) / 4),
|
|
588
|
+
latencyMs: jevIteration.decision.latencyMs, costUsd: jevIteration.decision.costUsd,
|
|
589
|
+
})
|
|
590
|
+
}
|
|
528
591
|
try {
|
|
529
592
|
response = await withTimeout(() => callLLM({
|
|
530
593
|
...providerCfg,
|
|
531
|
-
messages: clearOldToolResults(
|
|
532
|
-
tools:
|
|
594
|
+
messages: clearOldToolResults(callMessages) as LLMMessage[],
|
|
595
|
+
tools: callTools.length > 0 ? callTools : undefined,
|
|
533
596
|
signal: opts.signal,
|
|
534
597
|
sessionId: opts.threadId,
|
|
535
598
|
onToken: opts.onToken && !delegationGroupAtCall
|
|
@@ -689,6 +752,16 @@ export async function* runAgent(
|
|
|
689
752
|
}
|
|
690
753
|
}
|
|
691
754
|
|
|
755
|
+
const jevParallel = await jevWantsParallel(response.tool_calls, opts.jev)
|
|
756
|
+
.catch((err) => { log.warn(`[agent-loop] Jev parallel fallback: ${(err as Error).message}`); return null })
|
|
757
|
+
if (jevParallel?.decision) {
|
|
758
|
+
log.info(`[agent-loop] Jev parallel=${jevParallel.parallel} calls=${response.tool_calls.length}`)
|
|
759
|
+
await publishJev({
|
|
760
|
+
agentId: opts.agentId, kind: "parallel", provider: providerCfg.provider, model: providerCfg.model,
|
|
761
|
+
summary: `${response.tool_calls.length} herramientas ${jevParallel.parallel ? "en paralelo" : "en secuencia"}`,
|
|
762
|
+
savedTokens: 0, latencyMs: jevParallel.decision.latencyMs, costUsd: jevParallel.decision.costUsd,
|
|
763
|
+
})
|
|
764
|
+
}
|
|
692
765
|
const toolResults = await executeToolBatch({
|
|
693
766
|
toolCalls: response.tool_calls,
|
|
694
767
|
allTools: ctx.allTools,
|
|
@@ -709,6 +782,7 @@ export async function* runAgent(
|
|
|
709
782
|
},
|
|
710
783
|
hiveConfig,
|
|
711
784
|
workerPool: hiveConfig.tools?.workerPool,
|
|
785
|
+
parallelToolCalls: jevParallel?.parallel,
|
|
712
786
|
signal: opts.signal,
|
|
713
787
|
})
|
|
714
788
|
|
|
@@ -1305,6 +1379,8 @@ export interface IsolatedAgentOptions {
|
|
|
1305
1379
|
* reabría justo en el camino de delegación.
|
|
1306
1380
|
*/
|
|
1307
1381
|
credentials?: ProviderCredentials
|
|
1382
|
+
/** Jev for the worker, inherited from the delegating turn like `credentials`. */
|
|
1383
|
+
jev?: JevOption
|
|
1308
1384
|
}
|
|
1309
1385
|
|
|
1310
1386
|
export async function runAgentIsolatedDetailed(
|
|
@@ -1329,6 +1405,7 @@ export async function runAgentIsolatedDetailed(
|
|
|
1329
1405
|
channel: opts.channel,
|
|
1330
1406
|
sessionId: opts.sessionId,
|
|
1331
1407
|
credentials: opts.credentials,
|
|
1408
|
+
jev: opts.jev,
|
|
1332
1409
|
})) {
|
|
1333
1410
|
if (chunk.agent?.messages?.[0]?.content) {
|
|
1334
1411
|
lastContent = chunk.agent.messages[0].content
|
|
@@ -43,6 +43,8 @@ import { listCatalogAgents, renderAgentRoutingCatalog } from "./catalog-selector
|
|
|
43
43
|
import { expandToolAllowlist } from "./delegation-runtime.ts"
|
|
44
44
|
import { MINIMAL_TOOLS } from "./minimal-loadout.ts"
|
|
45
45
|
import { normalizeMcpResult } from "./mcp-result-normalizer.ts"
|
|
46
|
+
import { describeSwarmCapabilities, planJevContext, renderSpecialistLine } from "./jev-planner.ts"
|
|
47
|
+
import { DEFAULT_JEV_MCP_SETTINGS_PATH, getJevKey, type JevOption } from "./jev-decisions.ts"
|
|
46
48
|
|
|
47
49
|
const log = logger.child("context-compiler")
|
|
48
50
|
|
|
@@ -89,6 +91,8 @@ export interface CompiledContext {
|
|
|
89
91
|
tools: LLMToolDef[]
|
|
90
92
|
allTools: ContextTool[]
|
|
91
93
|
skills: SkillDescriptor[] // Skills loaded (minimal + discovered)
|
|
94
|
+
/** Jev's context plan, for the caller to publish once it knows the agent's resolved model. */
|
|
95
|
+
jevDecision?: { summary: string; savedTokens: number; latencyMs: number; costUsd: number; recommendedAgentId: string | null; mcpOff: string[] }
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
// ─── G9 causal context (buildAgentContext) ────────────────────────────────
|
|
@@ -166,6 +170,10 @@ export async function compileContext(opts: {
|
|
|
166
170
|
mcpManager?: MCPClientManager | null
|
|
167
171
|
/** G9 causal stream id for this invocation (agent-loop.ts's causalStreamId). */
|
|
168
172
|
causalStreamId?: string
|
|
173
|
+
/** A resumed run restores the exact previously selected prompt and loadout. */
|
|
174
|
+
skipJev?: boolean
|
|
175
|
+
/** Jev for this run: a key, `false` for off, or undefined for the tenant's `openrouter` row. */
|
|
176
|
+
jev?: JevOption
|
|
169
177
|
}): Promise<CompiledContext> {
|
|
170
178
|
const { agentId, threadId, mcpManager, userMessage, isolated, taskContext } = opts
|
|
171
179
|
|
|
@@ -521,7 +529,52 @@ export async function compileContext(opts: {
|
|
|
521
529
|
// En el historial las imágenes son referencias, para no reenviarlas enteras en
|
|
522
530
|
// cada turno. Las de los últimos mensajes se vuelven a poner en línea: el
|
|
523
531
|
// modelo todavía puede necesitar mirarlas, y una referencia no se mira.
|
|
524
|
-
|
|
532
|
+
let messages: LLMMessage[] = await inflateRecentImages(toAPIMessages(recentMessages))
|
|
533
|
+
let selectedSkills = allSkills
|
|
534
|
+
let jevAgentId: string | null = null
|
|
535
|
+
let jevAgentMcpOff: string[] = []
|
|
536
|
+
let omittedMessageIds: number[] = []
|
|
537
|
+
let omittedScratchpadKeys: string[] = []
|
|
538
|
+
const objectiveSource = taskContext || userMessage
|
|
539
|
+
const objective = typeof objectiveSource === "string"
|
|
540
|
+
? objectiveSource
|
|
541
|
+
: Array.isArray(objectiveSource)
|
|
542
|
+
? objectiveSource.filter((part) => part.type === "text").map((part) => (part as { text: string }).text).join("\n")
|
|
543
|
+
: String(objectiveSource)
|
|
544
|
+
const playbookRules = (await selectPlaybookRules(objective, userId)).filter((rule) => {
|
|
545
|
+
if (!rule.applicable_to || !rule.applicable_to.includes("agent:")) return true
|
|
546
|
+
return isCatalogAgent ? rule.applicable_to.includes(`agent:${agent.id}`) : false
|
|
547
|
+
})
|
|
548
|
+
// Without a key Jev does not exist: no swarm map, no request, the classic path below.
|
|
549
|
+
const jevEnabled = !opts.skipJev && !!await getJevKey(opts.jev).catch(() => null)
|
|
550
|
+
const swarm = jevEnabled
|
|
551
|
+
? await describeSwarmCapabilities(effectiveMcpManager, { includeSpecialists: !isWorker })
|
|
552
|
+
.catch((err) => { log.warn(`[context-compiler] Swarm capability map failed: ${(err as Error).message}`); return undefined })
|
|
553
|
+
: undefined
|
|
554
|
+
const jevPlan = jevEnabled ? await planJevContext({
|
|
555
|
+
objective, messages, tools: toolsForLLM, allTools, skills: allSkills, scratchpadNotes, playbookRules, isWorker, swarm, jev: opts.jev,
|
|
556
|
+
}).catch((err) => { log.warn(`[context-compiler] Jev planning failed: ${(err as Error).message}`); return null }) : null
|
|
557
|
+
// Characters the classic path would have sent minus what Jev's plan sends,
|
|
558
|
+
// accumulated section by section; reported as estimated savings.
|
|
559
|
+
let jevSavedChars = 0
|
|
560
|
+
const classicToolCount = toolsForLLM.length
|
|
561
|
+
if (jevPlan) {
|
|
562
|
+
jevSavedChars += JSON.stringify(messages).length - JSON.stringify(jevPlan.messages).length
|
|
563
|
+
jevSavedChars += JSON.stringify(toolsForLLM).length
|
|
564
|
+
messages = jevPlan.messages
|
|
565
|
+
toolsForLLM = jevPlan.tools
|
|
566
|
+
selectedSkills = allSkills.filter(s => minimalSkills.some(m => m.id === s.id) || jevPlan.selectedSkillNames.includes(s.name))
|
|
567
|
+
jevAgentId = jevPlan.agentId
|
|
568
|
+
jevAgentMcpOff = jevPlan.agentMcpOff
|
|
569
|
+
omittedMessageIds = recentMessages.filter((_, i) => !jevPlan.selectedMessageIds.includes(i)).map(row => row.id)
|
|
570
|
+
omittedScratchpadKeys = scratchpadNotes.filter(note => !jevPlan.selectedScratchpadKeys.includes(note.key)).map(note => note.key)
|
|
571
|
+
if (!isWorker && (omittedMessageIds.length > 0 || omittedScratchpadKeys.length > 0) && !toolsForLLM.some(t => t.function.name === "conversation_read")) {
|
|
572
|
+
const reader = allTools.find(t => t.name === "conversation_read")
|
|
573
|
+
if (reader) toolsForLLM.push({ type: "function", function: { name: reader.name, description: reader.description, parameters: reader.parameters } })
|
|
574
|
+
}
|
|
575
|
+
jevSavedChars -= JSON.stringify(toolsForLLM).length
|
|
576
|
+
log.info(`[context-compiler] Jev selected messages=${jevPlan.selectedMessageIds.join(",")} tools=${toolsForLLM.map(t => t.function.name).join(",")} skills=${selectedSkills.map(s => s.name).join(",")} agent=${jevAgentId ?? "coordinator"}`)
|
|
577
|
+
}
|
|
525
578
|
|
|
526
579
|
// [STEP-10] STRATEGY 4: ISOLATE — Build context based on agent role
|
|
527
580
|
log.info(`[context-compiler] [STEP-10] Building system prompt...`)
|
|
@@ -550,33 +603,54 @@ export async function compileContext(opts: {
|
|
|
550
603
|
// budget, so a summary appended last would be the first thing silently
|
|
551
604
|
// dropped on an oversized prompt.
|
|
552
605
|
systemPrompt += conversationSummarySection
|
|
606
|
+
if (omittedMessageIds.length > 0 || omittedScratchpadKeys.length > 0) {
|
|
607
|
+
const recoverable = `\n\n# CONTEXTO RECUPERABLE\nMensajes previos omitidos: ${omittedMessageIds.join(", ") || "ninguno"}. Notas omitidas: ${omittedScratchpadKeys.join(", ") || "ninguna"}. Si necesitas un dato de ellos, usa conversation_read con message_ids, note_keys o query antes de asumir que falta información.\n`
|
|
608
|
+
systemPrompt += recoverable
|
|
609
|
+
jevSavedChars -= recoverable.length
|
|
610
|
+
}
|
|
553
611
|
|
|
554
612
|
// Only the live roster goes here — how to delegate, fan-out/fan-in and the
|
|
555
613
|
// execution-truth rules are static doctrine and live in the coordinator's
|
|
556
614
|
// stored prompt (storage/onboarding.ts), not duplicated per turn.
|
|
557
615
|
if (!isWorker) {
|
|
558
|
-
const
|
|
559
|
-
|
|
616
|
+
const catalogAgents = await listCatalogAgents()
|
|
617
|
+
const fullCatalog = `\n\n# COLMENA DE AGENTES\nWorkers disponibles ahora mismo (globales del sistema, ya existen):\n\n${renderAgentRoutingCatalog(catalogAgents)}\n`
|
|
618
|
+
if (jevPlan) {
|
|
619
|
+
// Ids, names and MCP state only: without them the coordinator spent
|
|
620
|
+
// iterations on agent_find just to learn who exists and what is
|
|
621
|
+
// connected. Descriptions stay behind agent_find.
|
|
622
|
+
const roster = swarm?.specialists.length
|
|
623
|
+
? swarm.specialists.map(renderSpecialistLine).join("\n")
|
|
624
|
+
: catalogAgents.map(a => `- ${a.id} (${a.name})`).join("\n")
|
|
625
|
+
let rosterSection = roster ? `\n\n# COLMENA DE AGENTES\nWorkers disponibles (usa agent_find solo si necesitas su descripción). Un MCP "apagado" no está disponible; "disponible" se conecta al primer uso:\n${roster}\n` : ""
|
|
626
|
+
if (jevAgentId && jevAgentMcpOff.length > 0) {
|
|
627
|
+
// Turning a server on starts processes and uses credentials: that is
|
|
628
|
+
// the user's call, so the coordinator asks instead of delegating.
|
|
629
|
+
const settingsPath = (opts.jev && opts.jev.mcpSettingsPath) || DEFAULT_JEV_MCP_SETTINGS_PATH
|
|
630
|
+
const one = jevAgentMcpOff.length === 1
|
|
631
|
+
rosterSection += `\n\n# ESPECIALISTA RECOMENDADO — MCP APAGADO\nJev seleccionó ${jevAgentId} para esta tarea, pero depende de ${one ? "el servidor MCP" : "los servidores MCP"} ${jevAgentMcpOff.join(", ")}, que está${one ? "" : "n"} apagado${one ? "" : "s"}. No lo delegues todavía: dile al usuario que para hacerlo hace falta encender ${jevAgentMcpOff.join(", ")} en ${settingsPath} y que continúas en cuanto quede conectado. Si una parte se puede resolver sin ese MCP, ofrécela.\n`
|
|
632
|
+
} else if (jevAgentId) {
|
|
633
|
+
rosterSection += `\n\n# ESPECIALISTA RECOMENDADO\nJev seleccionó ${jevAgentId} para una subtarea acotada. Delega con task_delegate cuando puedas formular objetivo y criterios verificables.\n`
|
|
634
|
+
}
|
|
635
|
+
systemPrompt += rosterSection
|
|
636
|
+
jevSavedChars += fullCatalog.length - rosterSection.length
|
|
637
|
+
} else {
|
|
638
|
+
systemPrompt += fullCatalog
|
|
639
|
+
}
|
|
560
640
|
}
|
|
561
641
|
|
|
562
|
-
const
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
? playbookInput.filter((part) => part.type === "text").map((part) => (part as any).text).join("\n")
|
|
567
|
-
: String(playbookInput)
|
|
568
|
-
const playbookRules = (await selectPlaybookRules(playbookText, userId)).filter((rule) => {
|
|
569
|
-
if (!rule.applicable_to || !rule.applicable_to.includes("agent:")) return true
|
|
570
|
-
return isCatalogAgent ? rule.applicable_to.includes(`agent:${agent.id}`) : false
|
|
571
|
-
})
|
|
572
|
-
if (playbookRules.length > 0) {
|
|
573
|
-
systemPrompt += `\n\n# PLAYBOOK APRENDIDO\n${playbookRules.map((rule) => `- ${rule.rule}`).join("\n")}\n`
|
|
642
|
+
const selectedPlaybookRules = jevPlan ? playbookRules.filter(rule => jevPlan.selectedPlaybookIds.includes(rule.id)) : playbookRules
|
|
643
|
+
for (const rule of playbookRules) if (!selectedPlaybookRules.includes(rule)) jevSavedChars += rule.rule.length + 3
|
|
644
|
+
if (selectedPlaybookRules.length > 0) {
|
|
645
|
+
systemPrompt += `\n\n# PLAYBOOK APRENDIDO\n${selectedPlaybookRules.map((rule) => `- ${rule.rule}`).join("\n")}\n`
|
|
574
646
|
}
|
|
575
647
|
|
|
576
648
|
// Inject scratchpad (Strategy: WRITE) — usando TOON para ahorro de tokens
|
|
577
|
-
|
|
649
|
+
const selectedScratchpadNotes = jevPlan ? scratchpadNotes.filter(note => jevPlan.selectedScratchpadKeys.includes(note.key)) : scratchpadNotes
|
|
650
|
+
for (const note of scratchpadNotes) if (!selectedScratchpadNotes.includes(note)) jevSavedChars += note.key.length + note.value.length + 4
|
|
651
|
+
if (selectedScratchpadNotes.length > 0) {
|
|
578
652
|
const scratchpadData: Record<string, string> = {}
|
|
579
|
-
for (const n of
|
|
653
|
+
for (const n of selectedScratchpadNotes) {
|
|
580
654
|
scratchpadData[n.key] = n.value
|
|
581
655
|
}
|
|
582
656
|
// TOON comprime el formato clave-valor
|
|
@@ -636,10 +710,10 @@ export async function compileContext(opts: {
|
|
|
636
710
|
|
|
637
711
|
|
|
638
712
|
// Inject available skills (minimal + discovered)
|
|
639
|
-
if (
|
|
713
|
+
if (selectedSkills.length > 0) {
|
|
640
714
|
// Minimal skills: inject full body (always-loaded instructions)
|
|
641
715
|
const minimalNames = new Set(minimalSkills.map(s => s.name))
|
|
642
|
-
const minimalWithBody =
|
|
716
|
+
const minimalWithBody = selectedSkills.filter(s => minimalNames.has(s.name) && s.body)
|
|
643
717
|
if (minimalWithBody.length > 0) {
|
|
644
718
|
let minimalSection = `\n\n# SKILLS SIEMPRE ACTIVAS\n`
|
|
645
719
|
for (const skill of minimalWithBody) {
|
|
@@ -649,12 +723,23 @@ export async function compileContext(opts: {
|
|
|
649
723
|
}
|
|
650
724
|
|
|
651
725
|
// Discovered skills: list only (body arrives via agent-loop when tools are injected)
|
|
652
|
-
const discoveredOnly =
|
|
726
|
+
const discoveredOnly = selectedSkills.filter(s => !minimalNames.has(s.name))
|
|
727
|
+
if (jevPlan) {
|
|
728
|
+
// Classic lists every discovered skill in one line; Jev inlines the chosen bodies.
|
|
729
|
+
for (const skill of allSkills) {
|
|
730
|
+
if (minimalNames.has(skill.name)) continue
|
|
731
|
+
jevSavedChars += `- **${skill.name}**${skill.description ? ` — ${skill.description}` : ""}\n`.length
|
|
732
|
+
if (discoveredOnly.includes(skill)) jevSavedChars -= skill.body ? `\n## ${skill.name}\n${skill.body}\n`.length : 0
|
|
733
|
+
}
|
|
734
|
+
}
|
|
653
735
|
if (discoveredOnly.length > 0) {
|
|
654
736
|
let discoveredSection = `\n\n# SKILLS DESCUBIERTAS (relevantes para esta tarea)\n`
|
|
655
737
|
for (const skill of discoveredOnly) {
|
|
656
|
-
|
|
657
|
-
|
|
738
|
+
if (jevPlan && skill.body) discoveredSection += `\n## ${skill.name}\n${skill.body}\n`
|
|
739
|
+
else {
|
|
740
|
+
const desc = skill.description ? ` — ${skill.description}` : ""
|
|
741
|
+
discoveredSection += `- **${skill.name}**${desc}\n`
|
|
742
|
+
}
|
|
658
743
|
}
|
|
659
744
|
systemPrompt += discoveredSection
|
|
660
745
|
}
|
|
@@ -702,13 +787,27 @@ export async function compileContext(opts: {
|
|
|
702
787
|
`total=${estimatedTotal}/${modelContextWindow} (${budgetPct}%)`
|
|
703
788
|
)
|
|
704
789
|
|
|
790
|
+
const jevDecision = jevPlan ? {
|
|
791
|
+
summary: [
|
|
792
|
+
`${jevPlan.selectedMessageIds.length}/${recentMessages.length} mensajes`,
|
|
793
|
+
`${toolsForLLM.length}/${classicToolCount} herramientas`,
|
|
794
|
+
`${selectedSkills.length}/${allSkills.length} skills`,
|
|
795
|
+
].join(" · "),
|
|
796
|
+
savedTokens: Math.round(jevSavedChars / 4),
|
|
797
|
+
latencyMs: jevPlan.decision.latencyMs,
|
|
798
|
+
costUsd: jevPlan.decision.costUsd,
|
|
799
|
+
recommendedAgentId: jevAgentId,
|
|
800
|
+
mcpOff: jevAgentMcpOff,
|
|
801
|
+
} : undefined
|
|
802
|
+
|
|
705
803
|
return {
|
|
706
804
|
systemPrompt,
|
|
707
805
|
conversationSummarySection,
|
|
708
806
|
messages,
|
|
709
807
|
tools: toolsForLLM,
|
|
710
808
|
allTools,
|
|
711
|
-
skills:
|
|
809
|
+
skills: selectedSkills,
|
|
810
|
+
jevDecision,
|
|
712
811
|
}
|
|
713
812
|
}
|
|
714
813
|
|
|
@@ -6,6 +6,8 @@ export * from "./catalog-selector.ts";
|
|
|
6
6
|
export * from "./context-compiler.ts";
|
|
7
7
|
export * from "./conversation-store.ts";
|
|
8
8
|
export * from "./delegation-runtime.ts";
|
|
9
|
+
export * from "./jev-decisions.ts";
|
|
10
|
+
export * from "./jev-planner.ts";
|
|
9
11
|
export * from "./llm-client.ts";
|
|
10
12
|
export * from "./minimal-loadout.ts";
|
|
11
13
|
export * from "./playbook-selector.ts";
|