@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.
- package/README.md +83 -203
- package/bun.lock +833 -0
- package/bunfig.toml +7 -0
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +60 -0
- 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 +54 -89
- package/packages/core/src/api/createAgent.ts +10 -0
- package/packages/core/src/auth/auth.ts +36 -23
- package/packages/core/src/config/loader.ts +2 -2
- 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 +19 -0
- package/packages/core/src/memory/Scratchpad.test.ts +23 -21
- package/packages/core/src/memory/Scratchpad.ts +41 -24
- package/packages/core/src/skills/bundled-data.generated.ts +50 -0
- package/packages/core/src/skills/skills.test.ts +21 -0
- 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 +10 -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/index.ts +1 -0
- package/packages/core/src/tools/meeting/index.ts +83 -93
- package/packages/core/src/tools/web/api-request.test.ts +170 -0
- package/packages/core/src/tools/web/api-request.ts +239 -0
- package/packages/core/src/tools/web/browser-click.ts +2 -2
- package/packages/core/src/tools/web/browser-extract.ts +22 -6
- package/packages/core/src/tools/web/browser-navigate.ts +34 -18
- package/packages/core/src/tools/web/browser-screenshot.ts +40 -8
- package/packages/core/src/tools/web/browser-script.ts +2 -2
- package/packages/core/src/tools/web/browser-service.test.ts +83 -0
- package/packages/core/src/tools/web/browser-service.ts +290 -341
- package/packages/core/src/tools/web/browser-type.ts +2 -2
- package/packages/core/src/tools/web/browser-wait.ts +2 -2
- package/packages/core/src/tools/web/index.ts +3 -0
- package/packages/core/src/utils/toon.ts +4 -4
- package/CHANGELOG.md +0 -72
- package/docs/README.md +0 -161
|
@@ -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
|
|
|
@@ -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
|
|
321
|
-
const
|
|
322
|
-
if (!
|
|
323
|
-
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`)
|
|
324
326
|
return []
|
|
325
327
|
}
|
|
326
328
|
|
|
327
|
-
log.debug(`[tool-selector]
|
|
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
|
-
//
|
|
351
|
-
|
|
331
|
+
// Step 3: Execute hybrid search over the HiveDB semantic index
|
|
332
|
+
const db = await getHiveDB()
|
|
352
333
|
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
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 (
|
|
358
|
-
log.debug(`[tool-selector]
|
|
341
|
+
if (hits.length === 0) {
|
|
342
|
+
log.debug(`[tool-selector] No index matches, returning empty array`)
|
|
359
343
|
return []
|
|
360
344
|
}
|
|
361
345
|
|
|
362
|
-
//
|
|
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
|
|
368
|
-
const tool = toolMap.get(
|
|
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:
|
|
359
|
+
score: hit.score,
|
|
373
360
|
category: tool.category,
|
|
374
361
|
})
|
|
375
362
|
}
|
|
376
363
|
}
|
|
377
364
|
|
|
378
|
-
// Step
|
|
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
|
|
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
|
|
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
|
|
387
|
+
// Step 6: Take top N tools
|
|
406
388
|
const topTools = scoredTools.slice(0, maxTools)
|
|
407
389
|
|
|
408
|
-
// Step
|
|
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 =
|
|
417
|
+
const db = await getHiveDB()
|
|
437
418
|
|
|
438
419
|
try {
|
|
439
|
-
// Step 1: Build full catalog = CORE_TOOL_CATALOG + any
|
|
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
|
|
446
|
-
const
|
|
447
|
-
|
|
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:
|
|
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:
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
493
|
-
|
|
457
|
+
await db.clearIndex()
|
|
458
|
+
await db.upsertBatch(docs)
|
|
494
459
|
|
|
495
|
-
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`)
|
|
496
461
|
|
|
497
462
|
} catch (err) {
|
|
498
463
|
log.error(`[tool-selector] Transactional sync failed:`, err)
|
|
499
|
-
throw err
|
|
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 {
|
|
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
|
}
|
|
@@ -116,7 +116,7 @@ const WebConfigSchema = z.object({
|
|
|
116
116
|
|
|
117
117
|
const BrowserConfigSchema = z.object({
|
|
118
118
|
enabled: z.boolean().optional(),
|
|
119
|
-
|
|
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
|
-
|
|
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
|
+
}
|