@johpaz/hive-sdk 0.0.17 → 0.1.3

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.
Files changed (63) hide show
  1. package/README.md +83 -203
  2. package/bun.lock +833 -0
  3. package/bunfig.toml +7 -0
  4. package/docs/API-TOOLS-SKILLS-CHANNELS.md +60 -0
  5. package/docs/HIVE-HARNESS.md +113 -0
  6. package/package.json +36 -2
  7. package/packages/cli/package.json +1 -1
  8. package/packages/core/package.json +13 -2
  9. package/packages/core/src/ace/Tracer.ts +1 -1
  10. package/packages/core/src/agent/AgentRunner.ts +12 -0
  11. package/packages/core/src/agent/ContextCompiler.ts +4 -4
  12. package/packages/core/src/agent/ConversationStore.ts +30 -20
  13. package/packages/core/src/agent/selectors/PlaybookSelector.ts +50 -76
  14. package/packages/core/src/agent/selectors/SkillSelector.ts +106 -262
  15. package/packages/core/src/agent/selectors/ToolSelector.ts +54 -89
  16. package/packages/core/src/api/createAgent.ts +10 -0
  17. package/packages/core/src/auth/auth.ts +36 -23
  18. package/packages/core/src/config/loader.ts +2 -2
  19. package/packages/core/src/harness/boot-id.ts +20 -0
  20. package/packages/core/src/harness/collections.ts +98 -0
  21. package/packages/core/src/harness/db-helpers.ts +87 -0
  22. package/packages/core/src/harness/durable-queue.ts +337 -0
  23. package/packages/core/src/harness/goal-verifier.ts +141 -0
  24. package/packages/core/src/harness/harness.test.ts +236 -0
  25. package/packages/core/src/harness/index.ts +34 -0
  26. package/packages/core/src/harness/job-store.ts +399 -0
  27. package/packages/core/src/harness/proof-packet.ts +69 -0
  28. package/packages/core/src/harness/reconcile.ts +149 -0
  29. package/packages/core/src/harness/run-epoch.ts +32 -0
  30. package/packages/core/src/harness/run-store.ts +334 -0
  31. package/packages/core/src/index.ts +19 -0
  32. package/packages/core/src/memory/Scratchpad.test.ts +23 -21
  33. package/packages/core/src/memory/Scratchpad.ts +41 -24
  34. package/packages/core/src/skills/bundled-data.generated.ts +50 -0
  35. package/packages/core/src/skills/skills.test.ts +21 -0
  36. package/packages/core/src/storage/HiveDBStorage.ts +64 -0
  37. package/packages/core/src/storage/SQLiteStorage.ts +7 -0
  38. package/packages/core/src/storage/hiveSeed.ts +308 -0
  39. package/packages/core/src/storage/hiveStorage.test.ts +38 -0
  40. package/packages/core/src/storage/index.ts +10 -0
  41. package/packages/core/src/storage/seed.ts +5 -1
  42. package/packages/core/src/storage/usage.ts +106 -167
  43. package/packages/core/src/tool-runtime/tool-runtime.test.ts +11 -3
  44. package/packages/core/src/tools/agents/get-available-models.ts +52 -56
  45. package/packages/core/src/tools/agents/index.ts +77 -60
  46. package/packages/core/src/tools/core/index.ts +106 -291
  47. package/packages/core/src/tools/index.ts +1 -0
  48. package/packages/core/src/tools/meeting/index.ts +83 -93
  49. package/packages/core/src/tools/web/api-request.test.ts +170 -0
  50. package/packages/core/src/tools/web/api-request.ts +239 -0
  51. package/packages/core/src/tools/web/browser-click.ts +2 -2
  52. package/packages/core/src/tools/web/browser-extract.ts +22 -6
  53. package/packages/core/src/tools/web/browser-navigate.ts +34 -18
  54. package/packages/core/src/tools/web/browser-screenshot.ts +40 -8
  55. package/packages/core/src/tools/web/browser-script.ts +2 -2
  56. package/packages/core/src/tools/web/browser-service.test.ts +83 -0
  57. package/packages/core/src/tools/web/browser-service.ts +290 -341
  58. package/packages/core/src/tools/web/browser-type.ts +2 -2
  59. package/packages/core/src/tools/web/browser-wait.ts +2 -2
  60. package/packages/core/src/tools/web/index.ts +3 -0
  61. package/packages/core/src/utils/toon.ts +4 -4
  62. package/CHANGELOG.md +0 -72
  63. package/docs/README.md +0 -161
@@ -40,8 +40,9 @@
40
40
  * - core (notify, report_progress, save_note)
41
41
  */
42
42
 
43
- import { getDb } from "../../storage/SQLiteStorage.ts"
43
+ import { getHiveDB } from "../../storage/HiveDBStorage.ts"
44
44
  import { logger } from "../../utils/logger.ts"
45
+ import type { HiveDB, IndexDoc } from "@johpaz/hive-db";
45
46
 
46
47
  const log = logger.child("tool-selector")
47
48
 
@@ -147,6 +148,7 @@ export const CORE_TOOL_CATALOG: ToolDescriptor[] = [
147
148
  // Web tools
148
149
  { name: "web_search", description: "Search web for current information, find up-to-date news facts and research. Spanish keywords: buscar en internet, buscar web, información, noticias, investigación, buscar", category: "web", abstractionLevel: "atomic" },
149
150
  { name: "web_fetch", description: "Fetch content from URL, download and extract content from web pages. Spanish keywords: obtener página, descargar web, extraer contenido, obtener contenido, página web", category: "web", abstractionLevel: "atomic" },
151
+ { name: "api_request", description: "Connect to REST APIs, make HTTP requests with authentication and custom headers. Spanish keywords: conectar api, peticion http, llamada api, rest api, endpoint, bearer token, api key, basic auth", category: "web", abstractionLevel: "atomic" },
150
152
 
151
153
  // Memory tools
152
154
  { name: "memory_write", description: "Store in long-term memory, save information to persistent memory for later retrieval. Spanish keywords: guardar memoria, guardar información, recordar, guardar dato, memoria", category: "memory", abstractionLevel: "atomic" },
@@ -301,11 +303,11 @@ function getAbstractionPreference(): "atomic" | "orchestration" {
301
303
  * 5. If ambiguous → prefer atomic over orchestration
302
304
  * 6. Return top maxTools results (default: MAX_TOOLS_PER_TURN)
303
305
  */
304
- export function selectTools(
306
+ export async function selectTools(
305
307
  userMessage: string,
306
308
  fullToolList: ToolDescriptor[] = CORE_TOOL_CATALOG,
307
309
  maxTools: number = MAX_TOOLS_PER_TURN
308
- ): ToolDescriptor[] {
310
+ ): Promise<ToolDescriptor[]> {
309
311
  const startTime = performance.now()
310
312
 
311
313
  // Log incoming user message for debugging/validation
@@ -317,78 +319,58 @@ export function selectTools(
317
319
  return []
318
320
  }
319
321
 
320
- // Step 2: Build FTS5 query
321
- const ftsQuery = buildFTSQuery(userMessage)
322
- if (!ftsQuery) {
323
- log.debug(`[tool-selector] No valid FTS query terms, returning empty array`)
322
+ // Step 2: Build search query
323
+ const searchQuery = buildFTSQuery(userMessage)
324
+ if (!searchQuery) {
325
+ log.debug(`[tool-selector] No valid query terms, returning empty array`)
324
326
  return []
325
327
  }
326
328
 
327
- log.debug(`[tool-selector] FTS query: "${ftsQuery}"`)
328
-
329
- // Step 3: Execute FTS5 query with bm25 scoring
330
- const db = getDb()
331
-
332
- // Use bm25() with column weights for relevance scoring
333
- // FTS5 table columns: tool_name, name, description, category
334
- // Weights: tool_name=1.0, name=5.0, description=3.0, category=1.0
335
- // Higher weight on name (5.0) for exact tool name matching
336
- // Get more initially (maxTools * 2) for filtering, then limit to maxTools
337
- const ftsResults = db.query(`
338
- SELECT tool_name, bm25(tools_fts, 1.0, 5.0, 3.0, 1.0) as bm25_score
339
- FROM tools_fts
340
- WHERE tools_fts MATCH ?
341
- ORDER BY bm25_score ASC
342
- LIMIT ?
343
- `).all(ftsQuery, maxTools * 2) as { tool_name: string; bm25_score: number }[]
344
-
345
- if (ftsResults.length === 0) {
346
- log.debug(`[tool-selector] No FTS matches, returning empty array`)
347
- return []
348
- }
329
+ log.debug(`[tool-selector] Search query: "${searchQuery}"`)
349
330
 
350
- // Log raw scores for debugging
351
- log.info(`[tool-selector] Raw FTS scores: ${ftsResults.slice(0, 10).map(r => `${r.tool_name}=${r.bm25_score.toFixed(2)}`).join(", ")}`)
331
+ // Step 3: Execute hybrid search over the HiveDB semantic index
332
+ const db = await getHiveDB()
352
333
 
353
- // Step 4: Apply relevance threshold filter
354
- // bm25() returns negative scores; threshold is -0.5 (loosened from typical -5)
355
- const relevantResults = ftsResults.filter(r => r.bm25_score >= MIN_RELEVANCE_THRESHOLD)
334
+ // HiveDB text-only BM25 returns positive scores where higher is better.
335
+ const hits = await db.queryHybrid({
336
+ text: searchQuery,
337
+ k: maxTools * 2,
338
+ boosts: { name: 5.0, body: 3.0, tags: 1.0 },
339
+ })
356
340
 
357
- if (relevantResults.length === 0) {
358
- log.debug(`[tool-selector] All results below threshold ${MIN_RELEVANCE_THRESHOLD}, returning empty`)
341
+ if (hits.length === 0) {
342
+ log.debug(`[tool-selector] No index matches, returning empty array`)
359
343
  return []
360
344
  }
361
345
 
362
- // Step 5: Map to tool descriptors with additional metadata
346
+ // Log raw scores for debugging
347
+ log.info(`[tool-selector] Raw scores: ${hits.slice(0, 10).map(r => `${r.id}=${r.score.toFixed(2)}`).join(", ")}`)
348
+
349
+ // Step 4: Map to tool descriptors with additional metadata
363
350
  const toolMap = new Map(fullToolList.map(t => [t.name, t]))
364
351
 
365
352
  const scoredTools: SelectedTool[] = []
366
353
 
367
- for (const result of relevantResults) {
368
- const tool = toolMap.get(result.tool_name)
354
+ for (const hit of hits) {
355
+ const tool = toolMap.get(hit.id)
369
356
  if (tool) {
370
357
  scoredTools.push({
371
358
  name: tool.name,
372
- score: result.bm25_score,
359
+ score: hit.score,
373
360
  category: tool.category,
374
361
  })
375
362
  }
376
363
  }
377
364
 
378
- // Step 6: Prefer atomic over orchestration when ambiguous
379
- // If we have more than MAX_TOOLS_PER_TURN, prioritize by abstraction level
365
+ // Step 5: Prefer atomic over orchestration when ambiguous
380
366
  const abstractionPref = getAbstractionPreference()
381
367
 
382
368
  if (scoredTools.length > MAX_TOOLS_PER_TURN) {
383
- // Sort by score first, then by abstraction level preference
384
- // CRITICAL FIX: bm25() returns NEGATIVE scores where closer to 0 = more relevant
385
- // So we sort ASCENDING (a.score - b.score) to put -8.02 before -5.11
369
+ // Sort by score descending (higher HiveDB score = more relevant)
386
370
  scoredTools.sort((a, b) => {
387
- // First by score (ascending for bm25 - closer to 0 is better)
388
371
  if (Math.abs(a.score - b.score) > 0.1) {
389
- return a.score - b.score // ✅ Fixed: ascending for negative bm25 scores
372
+ return b.score - a.score
390
373
  }
391
- // Then by abstraction preference (preferred type first)
392
374
  const aTool = toolMap.get(a.name)
393
375
  const bTool = toolMap.get(b.name)
394
376
  const aLevel = aTool?.abstractionLevel ?? "atomic"
@@ -402,15 +384,14 @@ export function selectTools(
402
384
  })
403
385
  }
404
386
 
405
- // Step 7: Take top N tools
387
+ // Step 6: Take top N tools
406
388
  const topTools = scoredTools.slice(0, maxTools)
407
389
 
408
- // Step 8: Return as ToolDescriptor array
390
+ // Step 7: Return as ToolDescriptor array
409
391
  const result = topTools.map(t => toolMap.get(t.name)!).filter(Boolean)
410
392
 
411
393
  const timing = performance.now() - startTime
412
394
 
413
- // Log final selected tools with info level (important for tracking tool selection process)
414
395
  if (result.length > 0) {
415
396
  log.info(`[tool-selector] Selected ${result.length} tools in ${timing.toFixed(2)}ms:`,
416
397
  result.map(t => ({ name: t.name, category: t.category })))
@@ -433,29 +414,29 @@ export function selectTools(
433
414
  * @param tools - Optional array of tools to sync. If not provided, fetches from DB.
434
415
  */
435
416
  export async function syncToolCatalogToFTS(tools?: ToolDescriptor[]): Promise<void> {
436
- const db = getDb()
417
+ const db = await getHiveDB()
437
418
 
438
419
  try {
439
- // Step 1: Build full catalog = CORE_TOOL_CATALOG + any tools in DB not already covered
440
- // CORE_TOOL_CATALOG has bilingual keywords; DB tools may be dynamically registered
420
+ // Step 1: Build full catalog = CORE_TOOL_CATALOG + any explicitly passed tools
441
421
  const catalogByName = new Map<string, ToolDescriptor>(
442
422
  CORE_TOOL_CATALOG.map(t => [t.name, t])
443
423
  )
444
424
 
445
- // Merge in any tools from the DB that are missing from the static catalog
446
- const dbTools = db.query("SELECT name, description, category FROM tools").all() as Array<{ name: string; description: string | null; category: string | null }>
447
- for (const row of dbTools) {
425
+ // Merge in any tools persisted in HiveDB that are missing from the static catalog
426
+ const toolsCol = db.collection<ToolDescriptor>("tools")
427
+ const dbTools = await toolsCol.scan()
428
+ for (const entry of dbTools) {
429
+ const row = entry.doc
448
430
  if (!catalogByName.has(row.name)) {
449
431
  catalogByName.set(row.name, {
450
432
  name: row.name,
451
433
  description: row.description ?? row.name,
452
- category: (row.category ?? "core") as any,
453
- abstractionLevel: "atomic",
434
+ category: row.category ?? "core",
435
+ abstractionLevel: row.abstractionLevel ?? "atomic",
454
436
  })
455
437
  }
456
438
  }
457
439
 
458
- // Also merge any explicitly passed tools (e.g. from initializer)
459
440
  for (const t of (tools || [])) {
460
441
  if (!catalogByName.has(t.name)) {
461
442
  catalogByName.set(t.name, t)
@@ -464,39 +445,23 @@ export async function syncToolCatalogToFTS(tools?: ToolDescriptor[]): Promise<vo
464
445
 
465
446
  const toolCatalog = Array.from(catalogByName.values())
466
447
 
467
- // Step 2: Atomic transaction for FTS5 sync
468
- // We use a transaction to ensure that if sync fails, we don't end up with an empty FTS table
469
- const syncTransaction = db.transaction(() => {
470
- // Verify table exists inside transaction (optional but safer)
471
- const tableCheck = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='tools_fts'").get()
472
- if (!tableCheck) {
473
- throw new Error("tools_fts table does not exist!")
474
- }
475
-
476
- // A: Clear existing data
477
- db.run("DELETE FROM tools_fts")
478
-
479
- // B: Prepare insertion
480
- const insert = db.prepare(`
481
- INSERT INTO tools_fts(tool_name, name, description, category)
482
- VALUES (?, ?, ?, ?)
483
- `)
484
-
485
- // C: Re-populate
486
- for (const tool of toolCatalog) {
487
- const enriched = enrichToolDescription(tool)
488
- insert.run(tool.name, tool.name, enriched, tool.category)
489
- }
490
- })
448
+ // Step 2: Build index documents and upsert atomically via batch
449
+ const docs: IndexDoc[] = toolCatalog.map(tool => ({
450
+ id: tool.name,
451
+ name: tool.name,
452
+ body: enrichToolDescription(tool),
453
+ tags: tool.category,
454
+ filters: [{ field: "type", value: "tool" }],
455
+ }))
491
456
 
492
- // Execute transaction
493
- syncTransaction()
457
+ await db.clearIndex()
458
+ await db.upsertBatch(docs)
494
459
 
495
- log.info(`[tool-selector] Atomic sync complete: ${toolCatalog.length} tools indexed in FTS5`)
460
+ log.info(`[tool-selector] Atomic sync complete: ${toolCatalog.length} tools indexed in HiveDB`)
496
461
 
497
462
  } catch (err) {
498
463
  log.error(`[tool-selector] Transactional sync failed:`, err)
499
- throw err // Re-throw to inform initializer
464
+ throw err
500
465
  }
501
466
  }
502
467
 
@@ -505,7 +470,7 @@ export async function syncToolCatalogToFTS(tools?: ToolDescriptor[]): Promise<vo
505
470
  *
506
471
  * This improves FTS5 matching for both English and Spanish queries.
507
472
  */
508
- function enrichToolDescription(tool: ToolDescriptor): string {
473
+ export function enrichToolDescription(tool: ToolDescriptor): string {
509
474
  const keywordsByCategory: Record<string, string> = {
510
475
  scheduling: "programar recordatorio alarma cron schedule reminder task future tiempo",
511
476
  projects: "proyecto tarea plan organizer milestone backlog sprint work",
@@ -37,6 +37,16 @@ export async function createAgent(config: AgentConfig): Promise<Agent> {
37
37
  await initializeDatabase();
38
38
 
39
39
  const coreConfig = await loadConfig();
40
+
41
+ // Initialize browser automation (agent-browser) if enabled
42
+ try {
43
+ const { initializeBrowserService } = await import("../tools/web/browser-service.ts");
44
+ const browserService = initializeBrowserService(coreConfig);
45
+ await browserService.start();
46
+ } catch (err) {
47
+ log.warn(`Browser service initialization skipped: ${(err as Error).message}`);
48
+ }
49
+
40
50
  const allBuiltInTools = createAllTools(coreConfig);
41
51
 
42
52
  const customTools = (config.tools ?? []).map(t => ({
@@ -1,6 +1,6 @@
1
1
  import jwt from "jsonwebtoken";
2
2
  import { hashString } from "../utils/crypto.ts";
3
- import { getDb } from "../storage/SQLiteStorage.ts";
3
+ import { getHiveDB } from "../storage/HiveDBStorage.ts";
4
4
 
5
5
  const JWT_SECRET = process.env.JWT_SECRET || "hive-default-jwt-secret-change-in-production";
6
6
  const ACCESS_TOKEN_EXPIRY = "15m";
@@ -19,6 +19,17 @@ interface JwtPayload {
19
19
  type: "access" | "refresh";
20
20
  }
21
21
 
22
+ interface RefreshTokenDoc {
23
+ userId: string;
24
+ tokenHash: string;
25
+ expiresAt: number;
26
+ revoked: boolean;
27
+ }
28
+
29
+ function tokensCollection() {
30
+ return getHiveDB().then(db => db.collection<RefreshTokenDoc>("refresh_tokens"));
31
+ }
32
+
22
33
  export async function generateTokens(userId: string): Promise<AuthTokens> {
23
34
  const accessToken = jwt.sign({ userId, type: "access" } satisfies JwtPayload, JWT_SECRET, {
24
35
  expiresIn: ACCESS_TOKEN_EXPIRY,
@@ -31,12 +42,8 @@ export async function generateTokens(userId: string): Promise<AuthTokens> {
31
42
  const refreshTokenHash = hashString(refreshToken);
32
43
  const expiresAt = Math.floor(Date.now() / 1000) + REFRESH_TOKEN_EXPIRY_SECONDS;
33
44
 
34
- const db = getDb();
35
- db.run(
36
- `INSERT INTO refresh_tokens (user_id, token_hash, expires_at, revoked)
37
- VALUES (?, ?, ?, 0)`,
38
- [userId, refreshTokenHash, expiresAt]
39
- );
45
+ const col = await tokensCollection();
46
+ await col.put(refreshTokenHash, { userId, tokenHash: refreshTokenHash, expiresAt, revoked: false });
40
47
 
41
48
  return {
42
49
  accessToken,
@@ -59,27 +66,25 @@ export async function refreshAccessToken(refreshToken: string): Promise<AuthToke
59
66
  }
60
67
 
61
68
  const refreshTokenHash = hashString(refreshToken);
62
- const db = getDb();
63
- const tokenRow = db
64
- .query(
65
- `SELECT user_id, expires_at, revoked FROM refresh_tokens WHERE token_hash = ?`
66
- )
67
- .get(refreshTokenHash) as { user_id: string; expires_at: number; revoked: number } | undefined;
68
-
69
- if (!tokenRow) {
69
+ const col = await tokensCollection();
70
+ const entry = await col.get(refreshTokenHash);
71
+
72
+ if (!entry) {
70
73
  throw new Error("Refresh token not found");
71
74
  }
72
75
 
73
- if (tokenRow.revoked === 1) {
76
+ const tokenRow = entry.doc;
77
+
78
+ if (tokenRow.revoked) {
74
79
  throw new Error("Refresh token has been revoked");
75
80
  }
76
81
 
77
- if (tokenRow.expires_at < Math.floor(Date.now() / 1000)) {
78
- db.run(`DELETE FROM refresh_tokens WHERE token_hash = ?`, [refreshTokenHash]);
82
+ if (tokenRow.expiresAt < Math.floor(Date.now() / 1000)) {
83
+ await col.delete(refreshTokenHash);
79
84
  throw new Error("Refresh token has expired");
80
85
  }
81
86
 
82
- db.run(`DELETE FROM refresh_tokens WHERE token_hash = ?`, [refreshTokenHash]);
87
+ await col.delete(refreshTokenHash);
83
88
 
84
89
  return generateTokens(payload.userId);
85
90
  }
@@ -98,11 +103,19 @@ export async function validateAccessToken(token: string): Promise<{ userId: stri
98
103
 
99
104
  export async function revokeRefreshToken(refreshToken: string): Promise<void> {
100
105
  const refreshTokenHash = hashString(refreshToken);
101
- const db = getDb();
102
- db.run(`UPDATE refresh_tokens SET revoked = 1 WHERE token_hash = ?`, [refreshTokenHash]);
106
+ const col = await tokensCollection();
107
+ const entry = await col.get(refreshTokenHash);
108
+ if (entry) {
109
+ await col.put(refreshTokenHash, { ...entry.doc, revoked: true });
110
+ }
103
111
  }
104
112
 
105
113
  export async function revokeAllUserTokens(userId: string): Promise<void> {
106
- const db = getDb();
107
- db.run(`UPDATE refresh_tokens SET revoked = 1 WHERE user_id = ?`, [userId]);
114
+ const col = await tokensCollection();
115
+ const entries = await col.scan();
116
+ for (const e of entries) {
117
+ if (e.doc.userId === userId && !e.doc.revoked) {
118
+ await col.put(e.id, { ...e.doc, revoked: true });
119
+ }
120
+ }
108
121
  }
@@ -116,7 +116,7 @@ const WebConfigSchema = z.object({
116
116
 
117
117
  const BrowserConfigSchema = z.object({
118
118
  enabled: z.boolean().optional(),
119
- cdpUrl: z.string().optional(),
119
+ sessionName: z.string().optional(),
120
120
  headless: z.boolean().optional(),
121
121
  timeoutMs: z.number().optional(),
122
122
  });
@@ -437,7 +437,7 @@ function buildDefaultConfig(): Config {
437
437
  },
438
438
  browser: {
439
439
  enabled: true,
440
- cdpUrl: "ws://127.0.0.1:9222",
440
+ sessionName: "hive",
441
441
  headless: true,
442
442
  timeoutMs: 30000,
443
443
  },
@@ -0,0 +1,20 @@
1
+ /**
2
+ * boot-id — a unique id generated on every process start so that durable
3
+ * leases (harness runs / jobs) can detect which rows belong to a dead
4
+ * process after a crash/restart.
5
+ */
6
+
7
+ import { randomBytes } from "node:crypto";
8
+
9
+ let currentBootId: string | null = null;
10
+
11
+ export function getBootId(): string {
12
+ if (!currentBootId) {
13
+ currentBootId = randomBytes(8).toString("hex");
14
+ }
15
+ return currentBootId;
16
+ }
17
+
18
+ export function resetBootId(): void {
19
+ currentBootId = null;
20
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Document shapes for the harness's HiveDB collections. Ported from `hive`'s
3
+ * durable-task harness, generalized for SDK consumers: `JobDoc.type` and
4
+ * `HarnessRunDoc.kind` are plain strings (not a fixed union) so a host app
5
+ * (hive-cloud, a custom hive-sdk app, etc.) can define its own job/run
6
+ * vocabulary and register executors for it via `registerExecutor()`.
7
+ */
8
+
9
+ export interface HarnessRunDoc {
10
+ id: string
11
+ thread_id: string
12
+ agent_id: string
13
+ user_id: string
14
+ channel: string | null
15
+ /** Host-defined run kind, e.g. "chat" | "worker" | "goal". */
16
+ kind: string
17
+ status: "running" | "completed" | "failed" | "interrupted" | "aborted"
18
+
19
+ iterations_used: number
20
+ max_iterations: number
21
+ turns_used: number
22
+ max_turns: number | null
23
+ tokens_used: number
24
+ max_tokens: number | null
25
+
26
+ goal: string | null
27
+ goal_check_tool: string | null
28
+ goal_attempts: number
29
+
30
+ state_json: string
31
+ state_bytes: number
32
+ pending_tool_calls_json: string | null
33
+ checkpointed_at: number
34
+
35
+ boot_id: string
36
+ lease_expires_at: number
37
+ resume_policy: "resume" | "mark_interrupted" | "discard"
38
+
39
+ /** Whole-job acceptance criteria (harness-engineering "proof" concept): JSON array of AcceptanceCriterion. */
40
+ acceptance_json: string | null
41
+ /** Fixed-worker epoch recorded at run creation: RunEpoch JSON. */
42
+ epoch_json: string | null
43
+
44
+ error: string | null
45
+ created_at: number
46
+ updated_at: number
47
+ finished_at: number | null
48
+ }
49
+
50
+ export interface JobDoc {
51
+ id: string
52
+ lane: string
53
+ /** Host-defined job type, e.g. "chat_turn" | "worker_task" | "goal_run". */
54
+ type: string
55
+ status: "pending" | "running" | "completed" | "failed" | "cancelled" | "interrupted"
56
+ priority: number
57
+ payload_json: string
58
+ run_id: string
59
+ attempts: number
60
+ max_attempts: number
61
+ not_before: number
62
+ boot_id: string | null
63
+ lease_expires_at: number | null
64
+ result_json: string | null
65
+ error: string | null
66
+ created_at: number
67
+ started_at: number | null
68
+ finished_at: number | null
69
+ /** Logical-failure retries (executor returned {ok:false, retryable:true}). Separate from `attempts` (crash/lease-expiry only). */
70
+ retry_count: number
71
+ /** Error from the most recent logical-failure retry; `error` stays null until the job is terminal. */
72
+ last_error: string | null
73
+ /** `toIndexable`-encoded — sentinel when unset. Client-supplied dedup key for job creation. */
74
+ idempotency_key: string
75
+ }
76
+
77
+ /**
78
+ * Compressed evidence artifact for a completed run — the "proof packet"
79
+ * concept from harness-engineering's proof/verification practice: what was
80
+ * intended, what was checked, what evidence backs the verdict, known limits.
81
+ */
82
+ export interface ProofPacketDoc {
83
+ id: string
84
+ run_id: string
85
+ agent_id: string
86
+ intended_outcome: string
87
+ /** Per-acceptance-criterion verdicts: [{id, description, met, evidence}]. */
88
+ acceptance_results_json: string
89
+ /** Names of checks executed (tool ids, LLM verifier, etc). */
90
+ checks_run_json: string
91
+ /** Free-form evidence snippets backing the verdict (tool outputs, verifier reasons). */
92
+ evidence_json: string
93
+ known_limits: string | null
94
+ /** Fixed-worker epoch this run executed under — copied from HarnessRunDoc.epoch_json. */
95
+ epoch_json: string | null
96
+ met: boolean
97
+ created_at: number
98
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Reusable HiveDB collection helpers for the harness module — ported from
3
+ * `hive`'s `storage/hive.ts`. Provides the primitives HiveDB's `Collection`
4
+ * API doesn't have directly: autoincrement ids (`nextId`), whitelisted
5
+ * partial UPDATEs (`updateDoc`), and `WHERE field IN (...)` (`findByAny`).
6
+ */
7
+
8
+ import { hiveCollection } from "../storage/HiveDBStorage.ts";
9
+
10
+ const MAX_RETRIES = 5;
11
+
12
+ /** Sentinel for nullable FK-like fields used in equality indexes (`findBy`/`createIndex` reject `null`). */
13
+ export const NO_PARENT = "__none__";
14
+
15
+ /** Encode a nullable FK-like value for storage in an indexed field. */
16
+ export function toIndexable(value: string | null | undefined): string {
17
+ return value ?? NO_PARENT;
18
+ }
19
+
20
+ /** Decode a value stored via {@link toIndexable} back to its nullable form. */
21
+ export function fromIndexable(value: string): string | null {
22
+ return value === NO_PARENT ? null : value;
23
+ }
24
+
25
+ export async function col<T>(name: string) {
26
+ return hiveCollection<T>(name);
27
+ }
28
+
29
+ /**
30
+ * Monotonic counter, formatted as a zero-padded string so lexicographic
31
+ * `scan()` order matches numeric order. Retries on optimistic-concurrency
32
+ * conflicts (another writer bumped the same counter concurrently).
33
+ */
34
+ export async function nextId(counterName: string): Promise<string> {
35
+ const counters = await col<{ value: number }>("harness_counters");
36
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
37
+ const cur = await counters.get(counterName);
38
+ const next = (cur?.doc.value ?? 0) + 1;
39
+ try {
40
+ await counters.put(counterName, { value: next }, { expectedVersion: cur?.version ?? 0 });
41
+ return String(next).padStart(15, "0");
42
+ } catch {
43
+ // Version conflict — another writer won the race, retry.
44
+ }
45
+ }
46
+ throw new Error(`nextId: too much contention on counter "${counterName}"`);
47
+ }
48
+
49
+ /**
50
+ * Read-modify-write a document, replacing the whole-document `put()` with
51
+ * whitelisted-field UPDATE semantics. Retries on optimistic-concurrency
52
+ * conflicts.
53
+ */
54
+ export async function updateDoc<T extends object>(
55
+ collection: string,
56
+ id: string,
57
+ patch: Partial<T>
58
+ ): Promise<T> {
59
+ const c = await col<T>(collection);
60
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
61
+ const existing = await c.get(id);
62
+ if (!existing) throw new Error(`${collection}/${id} not found`);
63
+ const merged = { ...existing.doc, ...patch };
64
+ try {
65
+ await c.put(id, merged, { expectedVersion: existing.version });
66
+ return merged;
67
+ } catch {
68
+ // Version conflict — retry with a fresh read.
69
+ }
70
+ }
71
+ throw new Error(`updateDoc: too much contention on ${collection}/${id}`);
72
+ }
73
+
74
+ /**
75
+ * Fetch documents whose indexed `field` matches any of `values` — emulates
76
+ * `WHERE field IN (...)`. Requires a prior `createIndex(field)`.
77
+ */
78
+ export async function findByAny<T>(
79
+ collection: string,
80
+ field: string,
81
+ values: Array<string | number | boolean>
82
+ ): Promise<Array<{ id: string; version: number; doc: T }>> {
83
+ const c = await col<T>(collection);
84
+ const uniq = [...new Set(values)];
85
+ const chunks = await Promise.all(uniq.map((v) => c.findBy(field, v)));
86
+ return chunks.flat();
87
+ }