@johpaz/hive-sdk 0.0.18 → 0.1.4
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/bun.lock +291 -1
- package/docs/HIVE-HARNESS.md +113 -0
- package/package.json +36 -2
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +13 -2
- package/packages/core/src/ace/Tracer.ts +1 -1
- package/packages/core/src/agent/AgentRunner.ts +12 -0
- package/packages/core/src/agent/ContextCompiler.ts +4 -4
- package/packages/core/src/agent/ConversationStore.ts +30 -20
- package/packages/core/src/agent/selectors/PlaybookSelector.ts +50 -76
- package/packages/core/src/agent/selectors/SkillSelector.ts +106 -262
- package/packages/core/src/agent/selectors/ToolSelector.ts +53 -89
- package/packages/core/src/auth/auth.ts +36 -23
- package/packages/core/src/harness/boot-id.ts +20 -0
- package/packages/core/src/harness/collections.ts +98 -0
- package/packages/core/src/harness/db-helpers.ts +87 -0
- package/packages/core/src/harness/durable-queue.ts +337 -0
- package/packages/core/src/harness/goal-verifier.ts +141 -0
- package/packages/core/src/harness/harness.test.ts +236 -0
- package/packages/core/src/harness/index.ts +34 -0
- package/packages/core/src/harness/job-store.ts +399 -0
- package/packages/core/src/harness/proof-packet.ts +69 -0
- package/packages/core/src/harness/reconcile.ts +149 -0
- package/packages/core/src/harness/run-epoch.ts +32 -0
- package/packages/core/src/harness/run-store.ts +334 -0
- package/packages/core/src/index.ts +13 -0
- package/packages/core/src/memory/Scratchpad.test.ts +23 -21
- package/packages/core/src/memory/Scratchpad.ts +41 -24
- package/packages/core/src/storage/HiveDBStorage.ts +64 -0
- package/packages/core/src/storage/SQLiteStorage.ts +7 -0
- package/packages/core/src/storage/hiveSeed.ts +308 -0
- package/packages/core/src/storage/hiveStorage.test.ts +38 -0
- package/packages/core/src/storage/index.ts +11 -0
- package/packages/core/src/storage/seed.ts +5 -1
- package/packages/core/src/storage/usage.ts +106 -167
- package/packages/core/src/tool-runtime/tool-runtime.test.ts +11 -3
- package/packages/core/src/tools/agents/get-available-models.ts +52 -56
- package/packages/core/src/tools/agents/index.ts +77 -60
- package/packages/core/src/tools/core/index.ts +106 -291
- package/packages/core/src/tools/meeting/index.ts +83 -93
- package/packages/core/src/utils/toon.ts +4 -4
|
@@ -40,8 +40,9 @@
|
|
|
40
40
|
* - core (notify, report_progress, save_note)
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
|
-
import {
|
|
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
|
|
|
@@ -302,11 +303,11 @@ function getAbstractionPreference(): "atomic" | "orchestration" {
|
|
|
302
303
|
* 5. If ambiguous → prefer atomic over orchestration
|
|
303
304
|
* 6. Return top maxTools results (default: MAX_TOOLS_PER_TURN)
|
|
304
305
|
*/
|
|
305
|
-
export function selectTools(
|
|
306
|
+
export async function selectTools(
|
|
306
307
|
userMessage: string,
|
|
307
308
|
fullToolList: ToolDescriptor[] = CORE_TOOL_CATALOG,
|
|
308
309
|
maxTools: number = MAX_TOOLS_PER_TURN
|
|
309
|
-
): ToolDescriptor[] {
|
|
310
|
+
): Promise<ToolDescriptor[]> {
|
|
310
311
|
const startTime = performance.now()
|
|
311
312
|
|
|
312
313
|
// Log incoming user message for debugging/validation
|
|
@@ -318,78 +319,58 @@ export function selectTools(
|
|
|
318
319
|
return []
|
|
319
320
|
}
|
|
320
321
|
|
|
321
|
-
// Step 2: Build
|
|
322
|
-
const
|
|
323
|
-
if (!
|
|
324
|
-
log.debug(`[tool-selector] No valid
|
|
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`)
|
|
325
326
|
return []
|
|
326
327
|
}
|
|
327
328
|
|
|
328
|
-
log.debug(`[tool-selector]
|
|
329
|
-
|
|
330
|
-
// Step 3: Execute FTS5 query with bm25 scoring
|
|
331
|
-
const db = getDb()
|
|
332
|
-
|
|
333
|
-
// Use bm25() with column weights for relevance scoring
|
|
334
|
-
// FTS5 table columns: tool_name, name, description, category
|
|
335
|
-
// Weights: tool_name=1.0, name=5.0, description=3.0, category=1.0
|
|
336
|
-
// Higher weight on name (5.0) for exact tool name matching
|
|
337
|
-
// Get more initially (maxTools * 2) for filtering, then limit to maxTools
|
|
338
|
-
const ftsResults = db.query(`
|
|
339
|
-
SELECT tool_name, bm25(tools_fts, 1.0, 5.0, 3.0, 1.0) as bm25_score
|
|
340
|
-
FROM tools_fts
|
|
341
|
-
WHERE tools_fts MATCH ?
|
|
342
|
-
ORDER BY bm25_score ASC
|
|
343
|
-
LIMIT ?
|
|
344
|
-
`).all(ftsQuery, maxTools * 2) as { tool_name: string; bm25_score: number }[]
|
|
345
|
-
|
|
346
|
-
if (ftsResults.length === 0) {
|
|
347
|
-
log.debug(`[tool-selector] No FTS matches, returning empty array`)
|
|
348
|
-
return []
|
|
349
|
-
}
|
|
329
|
+
log.debug(`[tool-selector] Search query: "${searchQuery}"`)
|
|
350
330
|
|
|
351
|
-
//
|
|
352
|
-
|
|
331
|
+
// Step 3: Execute hybrid search over the HiveDB semantic index
|
|
332
|
+
const db = await getHiveDB()
|
|
353
333
|
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
|
|
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
|
+
})
|
|
357
340
|
|
|
358
|
-
if (
|
|
359
|
-
log.debug(`[tool-selector]
|
|
341
|
+
if (hits.length === 0) {
|
|
342
|
+
log.debug(`[tool-selector] No index matches, returning empty array`)
|
|
360
343
|
return []
|
|
361
344
|
}
|
|
362
345
|
|
|
363
|
-
//
|
|
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
|
|
364
350
|
const toolMap = new Map(fullToolList.map(t => [t.name, t]))
|
|
365
351
|
|
|
366
352
|
const scoredTools: SelectedTool[] = []
|
|
367
353
|
|
|
368
|
-
for (const
|
|
369
|
-
const tool = toolMap.get(
|
|
354
|
+
for (const hit of hits) {
|
|
355
|
+
const tool = toolMap.get(hit.id)
|
|
370
356
|
if (tool) {
|
|
371
357
|
scoredTools.push({
|
|
372
358
|
name: tool.name,
|
|
373
|
-
score:
|
|
359
|
+
score: hit.score,
|
|
374
360
|
category: tool.category,
|
|
375
361
|
})
|
|
376
362
|
}
|
|
377
363
|
}
|
|
378
364
|
|
|
379
|
-
// Step
|
|
380
|
-
// If we have more than MAX_TOOLS_PER_TURN, prioritize by abstraction level
|
|
365
|
+
// Step 5: Prefer atomic over orchestration when ambiguous
|
|
381
366
|
const abstractionPref = getAbstractionPreference()
|
|
382
367
|
|
|
383
368
|
if (scoredTools.length > MAX_TOOLS_PER_TURN) {
|
|
384
|
-
// Sort by score
|
|
385
|
-
// CRITICAL FIX: bm25() returns NEGATIVE scores where closer to 0 = more relevant
|
|
386
|
-
// 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)
|
|
387
370
|
scoredTools.sort((a, b) => {
|
|
388
|
-
// First by score (ascending for bm25 - closer to 0 is better)
|
|
389
371
|
if (Math.abs(a.score - b.score) > 0.1) {
|
|
390
|
-
return
|
|
372
|
+
return b.score - a.score
|
|
391
373
|
}
|
|
392
|
-
// Then by abstraction preference (preferred type first)
|
|
393
374
|
const aTool = toolMap.get(a.name)
|
|
394
375
|
const bTool = toolMap.get(b.name)
|
|
395
376
|
const aLevel = aTool?.abstractionLevel ?? "atomic"
|
|
@@ -403,15 +384,14 @@ export function selectTools(
|
|
|
403
384
|
})
|
|
404
385
|
}
|
|
405
386
|
|
|
406
|
-
// Step
|
|
387
|
+
// Step 6: Take top N tools
|
|
407
388
|
const topTools = scoredTools.slice(0, maxTools)
|
|
408
389
|
|
|
409
|
-
// Step
|
|
390
|
+
// Step 7: Return as ToolDescriptor array
|
|
410
391
|
const result = topTools.map(t => toolMap.get(t.name)!).filter(Boolean)
|
|
411
392
|
|
|
412
393
|
const timing = performance.now() - startTime
|
|
413
394
|
|
|
414
|
-
// Log final selected tools with info level (important for tracking tool selection process)
|
|
415
395
|
if (result.length > 0) {
|
|
416
396
|
log.info(`[tool-selector] Selected ${result.length} tools in ${timing.toFixed(2)}ms:`,
|
|
417
397
|
result.map(t => ({ name: t.name, category: t.category })))
|
|
@@ -434,29 +414,29 @@ export function selectTools(
|
|
|
434
414
|
* @param tools - Optional array of tools to sync. If not provided, fetches from DB.
|
|
435
415
|
*/
|
|
436
416
|
export async function syncToolCatalogToFTS(tools?: ToolDescriptor[]): Promise<void> {
|
|
437
|
-
const db =
|
|
417
|
+
const db = await getHiveDB()
|
|
438
418
|
|
|
439
419
|
try {
|
|
440
|
-
// Step 1: Build full catalog = CORE_TOOL_CATALOG + any
|
|
441
|
-
// 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
|
|
442
421
|
const catalogByName = new Map<string, ToolDescriptor>(
|
|
443
422
|
CORE_TOOL_CATALOG.map(t => [t.name, t])
|
|
444
423
|
)
|
|
445
424
|
|
|
446
|
-
// Merge in any tools
|
|
447
|
-
const
|
|
448
|
-
|
|
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
|
|
449
430
|
if (!catalogByName.has(row.name)) {
|
|
450
431
|
catalogByName.set(row.name, {
|
|
451
432
|
name: row.name,
|
|
452
433
|
description: row.description ?? row.name,
|
|
453
|
-
category:
|
|
454
|
-
abstractionLevel: "atomic",
|
|
434
|
+
category: row.category ?? "core",
|
|
435
|
+
abstractionLevel: row.abstractionLevel ?? "atomic",
|
|
455
436
|
})
|
|
456
437
|
}
|
|
457
438
|
}
|
|
458
439
|
|
|
459
|
-
// Also merge any explicitly passed tools (e.g. from initializer)
|
|
460
440
|
for (const t of (tools || [])) {
|
|
461
441
|
if (!catalogByName.has(t.name)) {
|
|
462
442
|
catalogByName.set(t.name, t)
|
|
@@ -465,39 +445,23 @@ export async function syncToolCatalogToFTS(tools?: ToolDescriptor[]): Promise<vo
|
|
|
465
445
|
|
|
466
446
|
const toolCatalog = Array.from(catalogByName.values())
|
|
467
447
|
|
|
468
|
-
// Step 2:
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
// A: Clear existing data
|
|
478
|
-
db.run("DELETE FROM tools_fts")
|
|
479
|
-
|
|
480
|
-
// B: Prepare insertion
|
|
481
|
-
const insert = db.prepare(`
|
|
482
|
-
INSERT INTO tools_fts(tool_name, name, description, category)
|
|
483
|
-
VALUES (?, ?, ?, ?)
|
|
484
|
-
`)
|
|
485
|
-
|
|
486
|
-
// C: Re-populate
|
|
487
|
-
for (const tool of toolCatalog) {
|
|
488
|
-
const enriched = enrichToolDescription(tool)
|
|
489
|
-
insert.run(tool.name, tool.name, enriched, tool.category)
|
|
490
|
-
}
|
|
491
|
-
})
|
|
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
|
+
}))
|
|
492
456
|
|
|
493
|
-
|
|
494
|
-
|
|
457
|
+
await db.clearIndex()
|
|
458
|
+
await db.upsertBatch(docs)
|
|
495
459
|
|
|
496
|
-
log.info(`[tool-selector] Atomic sync complete: ${toolCatalog.length} tools indexed in
|
|
460
|
+
log.info(`[tool-selector] Atomic sync complete: ${toolCatalog.length} tools indexed in HiveDB`)
|
|
497
461
|
|
|
498
462
|
} catch (err) {
|
|
499
463
|
log.error(`[tool-selector] Transactional sync failed:`, err)
|
|
500
|
-
throw err
|
|
464
|
+
throw err
|
|
501
465
|
}
|
|
502
466
|
}
|
|
503
467
|
|
|
@@ -506,7 +470,7 @@ export async function syncToolCatalogToFTS(tools?: ToolDescriptor[]): Promise<vo
|
|
|
506
470
|
*
|
|
507
471
|
* This improves FTS5 matching for both English and Spanish queries.
|
|
508
472
|
*/
|
|
509
|
-
function enrichToolDescription(tool: ToolDescriptor): string {
|
|
473
|
+
export function enrichToolDescription(tool: ToolDescriptor): string {
|
|
510
474
|
const keywordsByCategory: Record<string, string> = {
|
|
511
475
|
scheduling: "programar recordatorio alarma cron schedule reminder task future tiempo",
|
|
512
476
|
projects: "proyecto tarea plan organizer milestone backlog sprint work",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import jwt from "jsonwebtoken";
|
|
2
2
|
import { hashString } from "../utils/crypto.ts";
|
|
3
|
-
import {
|
|
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
|
|
35
|
-
|
|
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
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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.
|
|
78
|
-
|
|
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
|
-
|
|
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
|
|
102
|
-
|
|
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
|
|
107
|
-
|
|
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
|
}
|
|
@@ -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
|
+
}
|