@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.
@@ -117,6 +117,7 @@ export const SEED_DATA: SeedData = {
117
117
  { id: "notify", name: "notify", category: "core", description: "Enviar notificación al usuario. Sinónimos: notificar, enviar notificación, alertar, aviso" },
118
118
  { id: "save_note", name: "save_note", category: "core", description: "Guardar nota persistente en el scratchpad. Sinónimos: guardar nota, escribir nota, recordatorio rápido, apuntar" },
119
119
  { id: "report_progress", name: "report_progress", category: "core", description: "Reportar progreso actual al usuario. Sinónimos: reportar progreso, informar estado, actualizar progreso, porcentaje" },
120
+ { id: "conversation_read", name: "conversation_read", category: "core", description: "Recuperar mensajes anteriores de esta conversación por ID o búsqueda de texto. Sinónimos: leer conversación, recuperar contexto, historial omitido" },
120
121
 
121
122
  // ─────────────────────────────────────────
122
123
  // 10. OFFICE — Archivos Office (PDF, DOCX, XLSX, PPTX)
@@ -235,6 +236,9 @@ export const SEED_DATA: SeedData = {
235
236
  { id: "kimi-k2.6", providerId: "kimi", name: "Kimi K2.6", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code"]), inputPer1M: 0.6, outputPer1M: 3.41 },
236
237
 
237
238
  // ── OpenRouter (fuente: GET https://openrouter.ai/api/v1/models) ──
239
+ // Jev: modelo de decisiones (API Decisions, nunca chat). `modelType:
240
+ // "decision"` lo deja fuera de getDefaultLLM y de get_available_models.
241
+ { id: "typesafe/jev-1.13", providerId: "openrouter", name: "Jev 1.13 (Decisions)", modelType: "decision", contextWindow: 32000, capabilities: JSON.stringify(["choice", "noul", "score"]), inputPer1M: 0.042, outputPer1M: 0 },
238
242
  // Solo modelos vivos con `tools` en supported_parameters y publicados desde
239
243
  // 2025-07. contextWindow = context_length reportado por el propio catálogo,
240
244
  // y los precios también salen de ahí — son los de la ruta de OpenRouter, no
@@ -154,6 +154,15 @@ export interface UsageRecord {
154
154
  created_at: number;
155
155
  }
156
156
 
157
+ export interface JevUsageSummary {
158
+ decisions: number;
159
+ costUsd: number;
160
+ /** Estimated main-model input tokens avoided; negative when Jev added more than it removed. */
161
+ savedTokens: number;
162
+ savedCostUsd: number;
163
+ byAgent: Record<string, { decisions: number; costUsd: number; savedTokens: number; savedCostUsd: number }>;
164
+ }
165
+
157
166
  export interface UsageSummary {
158
167
  totalTokens: number;
159
168
  totalInputTokens: number;
@@ -169,6 +178,7 @@ export interface UsageSummary {
169
178
  byProvider: Record<string, { tokens: number; costUsd: number; inputTokens: number; outputTokens: number }>;
170
179
  byModel: Record<string, { tokens: number; costUsd: number; provider: string; inputTokens: number; outputTokens: number }>;
171
180
  recentRecords: UsageRecord[];
181
+ jev: JevUsageSummary;
172
182
  }
173
183
 
174
184
  export function recordUsage(options: {
@@ -233,13 +243,15 @@ export async function getUsageStats(hours: number = 24): Promise<UsageSummary> {
233
243
  const rollupsCol = await col<UsageRollupDoc>("usageRollups");
234
244
  const buckets = hourBucketsSince(hours);
235
245
  const rollups = (await Promise.all(buckets.map((id) => rollupsCol.get(id))))
236
- .map((e) => e?.doc ?? emptyRollup());
246
+ // A bucket touched only by recordJevDecision lacks the token fields.
247
+ .map((e) => ({ ...emptyRollup(), ...e?.doc }));
237
248
 
238
249
  const providerMap: UsageSummary["byProvider"] = {};
239
250
  const modelMap: UsageSummary["byModel"] = {};
240
251
  let totalInput = 0, totalOutput = 0, totalCost = 0;
241
252
  let toonSavedTokens = 0, toonSavedCost = 0, toonSavedBytes = 0;
242
253
  let toonJsonTokens = 0, toonToonTokens = 0, toonJsonBytes = 0;
254
+ const jev: JevUsageSummary = { decisions: 0, costUsd: 0, savedTokens: 0, savedCostUsd: 0, byAgent: {} };
243
255
 
244
256
  for (const r of rollups) {
245
257
  totalInput += r.inputTokens;
@@ -251,6 +263,18 @@ export async function getUsageStats(hours: number = 24): Promise<UsageSummary> {
251
263
  toonJsonTokens += r.toonJsonTokens;
252
264
  toonToonTokens += r.toonToonTokens;
253
265
  toonJsonBytes += r.toonJsonBytes;
266
+ jev.decisions += r.jevDecisions ?? 0;
267
+ jev.costUsd += r.jevCostUsd ?? 0;
268
+ jev.savedTokens += r.jevSavedTokens ?? 0;
269
+ jev.savedCostUsd += r.jevSavedCostUsd ?? 0;
270
+ for (const [agentId, a] of Object.entries(r.jevByAgent ?? {})) {
271
+ const cur = jev.byAgent[agentId] ?? { decisions: 0, costUsd: 0, savedTokens: 0, savedCostUsd: 0 };
272
+ cur.decisions += a.jevDecisions ?? 0;
273
+ cur.costUsd += a.jevCostUsd ?? 0;
274
+ cur.savedTokens += a.jevSavedTokens ?? 0;
275
+ cur.savedCostUsd += a.jevSavedCostUsd ?? 0;
276
+ jev.byAgent[agentId] = cur;
277
+ }
254
278
 
255
279
  for (const [provider, p] of Object.entries(r.byProvider ?? {})) {
256
280
  const cur = providerMap[provider] ?? { tokens: 0, costUsd: 0, inputTokens: 0, outputTokens: 0 };
@@ -301,10 +325,39 @@ export async function getUsageStats(hours: number = 24): Promise<UsageSummary> {
301
325
  toonSavingsPercent,
302
326
  byProvider: providerMap,
303
327
  byModel: modelMap,
304
- recentRecords
328
+ recentRecords,
329
+ jev,
305
330
  };
306
331
  }
307
332
 
333
+ /**
334
+ * Persists one Jev decision into the hourly rollup. The avoided tokens are
335
+ * priced with the advised agent's own model (input rate), through the same
336
+ * catalog lookup as real usage, so "saved" and "spent" are comparable.
337
+ */
338
+ export function recordJevDecision(options: {
339
+ agentId: string;
340
+ provider: string;
341
+ model: string;
342
+ savedTokens: number;
343
+ costUsd: number;
344
+ }): void {
345
+ Promise.resolve().then(async () => {
346
+ try {
347
+ const unitCost = await calculateCost(options.provider, options.model, Math.abs(options.savedTokens), 0);
348
+ const savedCostUsd = Math.sign(options.savedTokens) * unitCost;
349
+ await bumpRollup("usageRollups", hourBucket(Date.now()), {
350
+ jevDecisions: 1,
351
+ jevCostUsd: options.costUsd,
352
+ jevSavedTokens: options.savedTokens,
353
+ jevSavedCostUsd: savedCostUsd,
354
+ }, { field: "jevByAgent", key: options.agentId });
355
+ } catch (error) {
356
+ log.warn(`[JEV] Failed to record decision:`, error);
357
+ }
358
+ });
359
+ }
360
+
308
361
  /**
309
362
  * Record TOON savings for metrics tracking
310
363
  * This updates the usage_records table with complete TOON compression metrics
@@ -21,6 +21,7 @@
21
21
  */
22
22
 
23
23
  import { runAgentIsolated } from "../agent/agent-loop.ts"
24
+ import type { JevOption } from "../agent/jev-decisions.ts"
24
25
  import type { ProviderCredentials } from "../agent/llm-client.ts"
25
26
  import { logger } from "../utils/logger.ts"
26
27
 
@@ -50,6 +51,7 @@ export type AgentInvoker = (input: {
50
51
  threadId: string
51
52
  channel?: string
52
53
  credentials?: ProviderCredentials
54
+ jev?: JevOption
53
55
  signal?: AbortSignal
54
56
  }) => Promise<string>
55
57
 
@@ -60,6 +62,7 @@ export const defaultInvoker: AgentInvoker = async (input) =>
60
62
  threadId: input.threadId,
61
63
  channel: input.channel,
62
64
  credentials: input.credentials,
65
+ jev: input.jev,
63
66
  signal: input.signal,
64
67
  })
65
68
 
@@ -83,6 +86,8 @@ export interface RoleSwarmOptions {
83
86
  maxDelegations?: number
84
87
  /** Credenciales del inquilino, propagadas a cada agente. */
85
88
  credentials?: ProviderCredentials
89
+ /** Jev del inquilino, propagado a cada agente igual que `credentials`. */
90
+ jev?: JevOption
86
91
  signal?: AbortSignal
87
92
  /** Se llama en cada paso; acá persiste el consumidor si quiere. */
88
93
  onMessage?: (message: SwarmMessage) => void | Promise<void>
@@ -129,6 +134,7 @@ export async function runRoleSwarm(opts: RoleSwarmOptions): Promise<RoleSwarmRes
129
134
  threadId,
130
135
  channel: opts.channel,
131
136
  credentials: opts.credentials,
137
+ jev: opts.jev,
132
138
  signal: opts.signal,
133
139
  })
134
140
  }
@@ -47,6 +47,8 @@ export type ExecuteToolBatchOptions = {
47
47
  }
48
48
  hiveConfig?: Config
49
49
  workerPool?: ToolRuntimeConfig
50
+ /** Per-batch decision (Jev). Undefined preserves the configured behavior. */
51
+ parallelToolCalls?: boolean
50
52
  mainThreadToolNames?: string[]
51
53
  signal?: AbortSignal
52
54
  }
@@ -199,6 +201,7 @@ const DEFAULT_MAIN_THREAD_TOOL_NAMES = new Set([
199
201
  // These tools depend on process-local singleton state (HiveDB handle, live
200
202
  // channel senders, schedulers, browser sessions, or in-memory services).
201
203
  "search_knowledge",
204
+ "conversation_read",
202
205
  "save_note",
203
206
  "memory_write",
204
207
  "memory_read",
@@ -670,6 +673,7 @@ export async function executeToolBatch(options: ExecuteToolBatchOptions): Promis
670
673
 
671
674
  async function executeToolBatchInner(options: ExecuteToolBatchOptions): Promise<ToolBatchResult[]> {
672
675
  const runtimeConfig = resolveRuntimeConfig(options.workerPool)
676
+ const parallelToolCalls = options.parallelToolCalls ?? runtimeConfig.parallelToolCalls
673
677
  const hiveConfig = options.hiveConfig ?? loadConfig()
674
678
  const mainThreadToolNames = [
675
679
  ...DEFAULT_MAIN_THREAD_TOOL_NAMES,
@@ -690,11 +694,26 @@ async function executeToolBatchInner(options: ExecuteToolBatchOptions): Promise<
690
694
 
691
695
  // A null pool means workers are disabled, unnecessary (single call), or
692
696
  // unavailable in this build — all three degrade to the main thread.
693
- const pool = runtimeConfig.enabled && runtimeConfig.parallelToolCalls && options.toolCalls.length > 1
697
+ const pool = runtimeConfig.enabled && parallelToolCalls && options.toolCalls.length > 1
694
698
  ? getPool(runtimeConfig.maxWorkers)
695
699
  : null
696
700
 
697
701
  if (!pool) {
702
+ // Jev judged the batch independent: run it concurrently even without workers.
703
+ if (options.parallelToolCalls === true && options.toolCalls.length > 1) {
704
+ return Promise.all(options.toolCalls.map(async (toolCall) => {
705
+ const startedAt = performance.now()
706
+ const effectiveTimeout = resolveToolTimeout(toolCall.function.name, options.allTools, hiveConfig, runtimeConfig.toolTimeoutMs)
707
+ const result = await executeInMainThreadWithTimeout({
708
+ toolCall, allTools: options.allTools, toolConfig: options.toolConfig, signal: options.signal,
709
+ }, effectiveTimeout)
710
+ return {
711
+ toolCall, toolName: toolCall.function.name, result,
712
+ ok: !(result && typeof result === "object" && (result as any).error === true),
713
+ durationMs: Math.round(performance.now() - startedAt),
714
+ }
715
+ }))
716
+ }
698
717
  const results: ToolBatchResult[] = []
699
718
  for (const toolCall of options.toolCalls) {
700
719
  const startedAt = performance.now()
@@ -60,7 +60,9 @@ export const getAvailableModelsTool: Tool = {
60
60
  .filter(m => m.enabled && providersById.has(m.provider_id));
61
61
 
62
62
  if (providerId) models = models.filter(m => m.provider_id === providerId);
63
- if (modelType) models = models.filter(m => m.model_type === modelType);
63
+ // Decision models (Jev) only answer the Decisions API, never chat: an
64
+ // agent assigned one would fail every turn.
65
+ models = modelType ? models.filter(m => m.model_type === modelType) : models.filter(m => m.model_type !== "decision");
64
66
  if (capabilities) models = models.filter(m => (m.capabilities ?? "").includes(capabilities));
65
67
 
66
68
  // Transformar a formato amigable
@@ -14,7 +14,7 @@ import {
14
14
  type CapabilityType,
15
15
  } from "../../agent/capability-search.ts";
16
16
  import { CORE_TOOL_CATALOG } from "../../agent/tool-selector.ts";
17
- import { saveScratchpadNote } from "../../agent/conversation-store.ts";
17
+ import { saveScratchpadNote, getRecentMessages, getScratchpad } from "../../agent/conversation-store.ts";
18
18
 
19
19
  const log = logger.child("core");
20
20
 
@@ -503,6 +503,37 @@ export const reportProgressTool: Tool = {
503
503
  },
504
504
  };
505
505
 
506
+ /** Recover a small, scoped slice of conversation omitted by Jev's context plan. */
507
+ export const conversationReadTool: Tool = {
508
+ name: "conversation_read",
509
+ description: "Read earlier messages from the current conversation by message IDs or text query when selected context is insufficient.",
510
+ parameters: {
511
+ type: "object",
512
+ properties: {
513
+ message_ids: { type: "array", description: "Message IDs shown in the context plan", items: { type: "number" } },
514
+ note_keys: { type: "array", description: "Scratchpad note keys shown in the context plan", items: { type: "string" } },
515
+ query: { type: "string", description: "Optional case-insensitive text to search in earlier messages" },
516
+ },
517
+ },
518
+ execute: async (params, config) => {
519
+ const threadId = config?.configurable?.thread_id as string | undefined
520
+ if (!threadId) return { ok: false, error: "Conversation scope unavailable" }
521
+ const ids = new Set(Array.isArray(params.message_ids) ? params.message_ids.map(Number).filter(Number.isFinite) : [])
522
+ const noteKeys = new Set(Array.isArray(params.note_keys) ? params.note_keys.map(String) : [])
523
+ const query = String(params.query ?? "").trim().toLocaleLowerCase()
524
+ if (!ids.size && !noteKeys.size && !query) return { ok: false, error: "Provide message_ids, note_keys or query" }
525
+ const rows = await getRecentMessages(threadId, 200)
526
+ const notes = await getScratchpad(threadId)
527
+ return {
528
+ ok: true,
529
+ messages: rows.filter(row => (ids.size && ids.has(row.id)) || (query && row.content.toLocaleLowerCase().includes(query)))
530
+ .slice(-8).map(row => ({ id: row.id, role: row.role, source: row.source, content: row.content.slice(0, 2000) })),
531
+ notes: notes.filter(note => noteKeys.has(note.key) || (query && `${note.key} ${note.value}`.toLocaleLowerCase().includes(query)))
532
+ .slice(0, 8).map(note => ({ key: note.key, value: note.value.slice(0, 2000) })),
533
+ }
534
+ },
535
+ };
536
+
506
537
  export function createTools(): Tool[] {
507
- return [searchKnowledgeTool, notifyTool, saveNoteTool, reportProgressTool];
538
+ return [searchKnowledgeTool, notifyTool, saveNoteTool, reportProgressTool, conversationReadTool];
508
539
  }