@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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Hive Harness — durable task execution
|
|
2
|
+
|
|
3
|
+
The `harness` module (`@johpaz/hive-sdk/harness`) is the SDK's durable-execution
|
|
4
|
+
layer: a HiveDB-backed job queue with crash recovery, checkpointable runs,
|
|
5
|
+
retry with backoff, idempotent submission, goal verification, and proof
|
|
6
|
+
packets. It's what lets a host app (a `hive-app`, or a production service like
|
|
7
|
+
Hive Cloud) survive a process restart mid-task without losing work or
|
|
8
|
+
double-executing a tool call.
|
|
9
|
+
|
|
10
|
+
It is deliberately **not** wired into `AgentRunner` automatically, and it has
|
|
11
|
+
no built-in notion of "chat" vs "project task" vs any other app-specific job
|
|
12
|
+
type — job `type` and run `kind` are plain strings. The host app defines its
|
|
13
|
+
own vocabulary and registers executors for it. This is the same infrastructure
|
|
14
|
+
that powers `hive`'s durable-queue harness, generalized so any SDK consumer
|
|
15
|
+
can reuse it instead of re-implementing crash-safe job execution from scratch.
|
|
16
|
+
|
|
17
|
+
## Architecture
|
|
18
|
+
|
|
19
|
+
| Piece | File | Responsibility |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `JobDoc` / `HarnessRunDoc` / `ProofPacketDoc` | `collections.ts` | HiveDB document shapes |
|
|
22
|
+
| `db-helpers` | `db-helpers.ts` | `nextId`, `updateDoc`, `findByAny` — primitives HiveDB's `Collection` doesn't provide directly |
|
|
23
|
+
| `job-store` | `job-store.ts` | Durable job persistence: claim/lease/complete/fail/retry, all via OCC |
|
|
24
|
+
| `run-store` | `run-store.ts` | Checkpoint + lease for a single durable run (messages, iteration/token counters, pending tool calls) |
|
|
25
|
+
| `durable-queue` | `durable-queue.ts` | `DurableLaneQueue` — FIFO+priority per lane, global concurrency cap, executor registry |
|
|
26
|
+
| `goal-verifier` | `goal-verifier.ts` | `verifyGoal()` — deterministic check tool or LLM verifier, single goal or a list of acceptance criteria |
|
|
27
|
+
| `run-epoch` | `run-epoch.ts` | Fixed-worker epoch fingerprint (provider/model/app-version/tool-catalog) |
|
|
28
|
+
| `proof-packet` | `proof-packet.ts` | Compressed evidence artifact for a completed run |
|
|
29
|
+
| `reconcile` | `reconcile.ts` | `reconcileOnBoot()` — crash repair + retention cap, call once at startup |
|
|
30
|
+
|
|
31
|
+
## Durable queue semantics
|
|
32
|
+
|
|
33
|
+
- **Lanes**: a lane (e.g. a session id, or `task:<id>`) runs at most one job
|
|
34
|
+
at a time, FIFO within the lane, ordered by `priority` then creation order.
|
|
35
|
+
- **Global concurrency**: `maxGlobalConcurrency` caps how many jobs run at
|
|
36
|
+
once across all lanes (default 4). Types listed in `nonRetryableTypes`
|
|
37
|
+
(default `["chat_turn"]`) bypass this cap — a busy batch of background jobs
|
|
38
|
+
must not make an interactive/user-facing job type stop responding.
|
|
39
|
+
- **Leases**: a claimed job gets a lease (default 30 min); the queue renews
|
|
40
|
+
it every 30s while executing. A lease that expires (crashed process) is
|
|
41
|
+
reclaimed to `pending` or marked `interrupted` once `attempts >=
|
|
42
|
+
max_attempts` — checked by `reconcileOnBoot()` at startup and by the
|
|
43
|
+
queue's periodic maintenance tick thereafter.
|
|
44
|
+
- **Executors**: register one per job type with `registerExecutor(type, fn)`.
|
|
45
|
+
An executor receives the `JobDoc`, an `AbortSignal` (fired on cancel or
|
|
46
|
+
`taskTimeoutMs`), and any live callbacks passed to `enqueue()`.
|
|
47
|
+
|
|
48
|
+
## Retry & backoff
|
|
49
|
+
|
|
50
|
+
Two independent retry mechanisms:
|
|
51
|
+
|
|
52
|
+
1. **Crash retries** (`attempts` / `max_attempts`) — bumped on every claim,
|
|
53
|
+
checked by `reclaimOrInterrupt` after a lease expires. This is about
|
|
54
|
+
*the process dying*, not the job failing logically.
|
|
55
|
+
2. **Logical-failure retries** (`retry_count` / `JobRetryPolicy`) — when an
|
|
56
|
+
executor returns `{ok: false, retryable: true}` (the default unless set
|
|
57
|
+
`false`), `failJobOrRetry` reschedules the job with exponential backoff +
|
|
58
|
+
jitter instead of failing it immediately:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
delay = min(maxDelayMs, initialDelayMs * backoffMultiplier ** retryCount)
|
|
62
|
+
* (1 + jitter * random())
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Once `retryCount >= policy.maxRetries`, the job fails terminally. Types in
|
|
66
|
+
`nonRetryableTypes` never take this path — a failed interactive turn
|
|
67
|
+
should surface to the user immediately, not silently retry later.
|
|
68
|
+
|
|
69
|
+
## Idempotency
|
|
70
|
+
|
|
71
|
+
`createJob`/`enqueue` accept an optional `idempotency_key`. A repeated key
|
|
72
|
+
returns the existing job (whatever its status — pending, running, completed,
|
|
73
|
+
or terminally failed) instead of creating a duplicate, so a retried HTTP
|
|
74
|
+
request from a caller doesn't double-enqueue work.
|
|
75
|
+
|
|
76
|
+
## Goal verification & acceptance criteria
|
|
77
|
+
|
|
78
|
+
`verifyGoal()` answers "was this met" for a single goal or — when
|
|
79
|
+
`acceptance` criteria are supplied — for each criterion independently (its
|
|
80
|
+
own optional `checkTool`, or an LLM judgment against its own description).
|
|
81
|
+
The overall verdict is the conjunction of all criteria. The harness has no
|
|
82
|
+
built-in tool registry: pass a `runCheckTool` callback that resolves a
|
|
83
|
+
`checkTool` name to something the host app can actually execute.
|
|
84
|
+
|
|
85
|
+
## Proof packets
|
|
86
|
+
|
|
87
|
+
`buildProofPacket()` persists a compressed evidence artifact once a run
|
|
88
|
+
finishes: intended outcome, per-criterion results, checks run, evidence
|
|
89
|
+
snippets, known limits, and the run's fixed-worker epoch. Useful as an
|
|
90
|
+
audit trail without having to replay the full run transcript.
|
|
91
|
+
|
|
92
|
+
## Setup
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import {
|
|
96
|
+
ensureHarnessIndexes,
|
|
97
|
+
reconcileOnBoot,
|
|
98
|
+
initDurableQueue,
|
|
99
|
+
registerExecutor,
|
|
100
|
+
getBootId,
|
|
101
|
+
} from "@johpaz/hive-sdk/harness";
|
|
102
|
+
|
|
103
|
+
await ensureHarnessIndexes(); // idempotent — safe every boot
|
|
104
|
+
await reconcileOnBoot(getBootId());
|
|
105
|
+
|
|
106
|
+
registerExecutor("my_job_type", async (job, signal) => {
|
|
107
|
+
// ... do the work, honoring `signal` for cancellation/timeout
|
|
108
|
+
return { ok: true, result: "done" };
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const queue = initDurableQueue({ maxGlobalConcurrency: 4 });
|
|
112
|
+
await queue.enqueue({ lane: "session-1", type: "my_job_type", run_id: "r1", payload: {} });
|
|
113
|
+
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johpaz/hive-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"private": false,
|
|
3
|
+
"version": "0.1.4",
|
|
5
4
|
"description": "Hive SDK — The Agent Harness SDK. Build, deploy, and scale AI agent applications with multi-channel support, context engineering, and swarm orchestration.",
|
|
6
5
|
"license": "MIT",
|
|
7
6
|
"homepage": "https://github.com/johpaz/hive-sdk#readme",
|
|
@@ -28,6 +27,33 @@
|
|
|
28
27
|
],
|
|
29
28
|
"main": "./packages/core/src/index.ts",
|
|
30
29
|
"types": "./packages/core/src/index.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": "./packages/core/src/index.ts",
|
|
32
|
+
"./agent": "./packages/core/src/agent/index.ts",
|
|
33
|
+
"./agent/providers": "./packages/core/src/agent/providers/index.ts",
|
|
34
|
+
"./agent/selectors": "./packages/core/src/agent/selectors/index.ts",
|
|
35
|
+
"./tools": "./packages/core/src/tools/index.ts",
|
|
36
|
+
"./skills": "./packages/core/src/skills/index.ts",
|
|
37
|
+
"./storage": "./packages/core/src/storage/index.ts",
|
|
38
|
+
"./swarm": "./packages/core/src/swarm/index.ts",
|
|
39
|
+
"./swarm/strategies": "./packages/core/src/swarm/strategies/index.ts",
|
|
40
|
+
"./swarm/presets": "./packages/core/src/swarm/presets/index.ts",
|
|
41
|
+
"./ace": "./packages/core/src/ace/index.ts",
|
|
42
|
+
"./ethics": "./packages/core/src/ethics/index.ts",
|
|
43
|
+
"./canvas": "./packages/core/src/canvas/index.ts",
|
|
44
|
+
"./config": "./packages/core/src/config/index.ts",
|
|
45
|
+
"./mcp": "./packages/core/src/mcp/index.ts",
|
|
46
|
+
"./mcp/transports": "./packages/core/src/mcp/transports/index.ts",
|
|
47
|
+
"./memory": "./packages/core/src/memory/index.ts",
|
|
48
|
+
"./multimodal": "./packages/core/src/multimodal/index.ts",
|
|
49
|
+
"./security": "./packages/core/src/security/index.ts",
|
|
50
|
+
"./state": "./packages/core/src/state/index.ts",
|
|
51
|
+
"./utils": "./packages/core/src/utils/index.ts",
|
|
52
|
+
"./gateway": "./packages/core/src/gateway/index.ts",
|
|
53
|
+
"./api": "./packages/core/src/api/index.ts",
|
|
54
|
+
"./harness": "./packages/core/src/harness/index.ts",
|
|
55
|
+
"./package.json": "./package.json"
|
|
56
|
+
},
|
|
31
57
|
"bin": {
|
|
32
58
|
"hives": "./packages/cli/bin/hives"
|
|
33
59
|
},
|
|
@@ -42,15 +68,21 @@
|
|
|
42
68
|
"prepublish": "echo 'No build needed - Bun runs TypeScript directly'"
|
|
43
69
|
},
|
|
44
70
|
"dependencies": {
|
|
71
|
+
"@johpaz/hive-db": "^0.4.0",
|
|
45
72
|
"@anthropic-ai/sdk": "^0.74.0",
|
|
46
73
|
"@google/genai": "^1.43.0",
|
|
47
74
|
"@modelcontextprotocol/sdk": "latest",
|
|
48
75
|
"@sapphire/snowflake": "latest",
|
|
76
|
+
"@slack/bolt": "latest",
|
|
77
|
+
"@whiskeysockets/baileys": "latest",
|
|
49
78
|
"async-mutex": "^0.5.0",
|
|
50
79
|
"cron-parser": "^5.5.0",
|
|
51
80
|
"croner": "^10.0.1",
|
|
81
|
+
"discord.js": "latest",
|
|
52
82
|
"docx": "^9.6.1",
|
|
83
|
+
"grammy": "latest",
|
|
53
84
|
"groq-sdk": "^0.37.0",
|
|
85
|
+
"jsonwebtoken": "^9.0.3",
|
|
54
86
|
"jszip": "^3.10.1",
|
|
55
87
|
"pdf-lib": "^1.17.1",
|
|
56
88
|
"mammoth": "^1.12.0",
|
|
@@ -58,12 +90,14 @@
|
|
|
58
90
|
"openai": "^6.18.0",
|
|
59
91
|
"pdfjs-dist": "^5.6.205",
|
|
60
92
|
"pptxgenjs": "^4.0.1",
|
|
93
|
+
"qrcode-terminal": "latest",
|
|
61
94
|
"toon-format-parser": "^1.1.0",
|
|
62
95
|
"xlsx": "^0.18.5",
|
|
63
96
|
"zod": "latest"
|
|
64
97
|
},
|
|
65
98
|
"devDependencies": {
|
|
66
99
|
"@types/bun": "^1.3.13",
|
|
100
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
67
101
|
"typescript": "6.0.2"
|
|
68
102
|
}
|
|
69
103
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hive/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Hive Core — Agentes AI con Context Engineering, FTS5, ACE, Swarm",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,18 +29,25 @@
|
|
|
29
29
|
"./state": "./src/state/index.ts",
|
|
30
30
|
"./utils": "./src/utils/index.ts",
|
|
31
31
|
"./gateway": "./src/gateway/index.ts",
|
|
32
|
-
"./api": "./src/api/index.ts"
|
|
32
|
+
"./api": "./src/api/index.ts",
|
|
33
|
+
"./harness": "./src/harness/index.ts"
|
|
33
34
|
},
|
|
34
35
|
"dependencies": {
|
|
36
|
+
"@johpaz/hive-db": "^0.4.0",
|
|
35
37
|
"@anthropic-ai/sdk": "^0.74.0",
|
|
36
38
|
"@google/genai": "^1.43.0",
|
|
37
39
|
"@modelcontextprotocol/sdk": "latest",
|
|
38
40
|
"@sapphire/snowflake": "latest",
|
|
41
|
+
"@slack/bolt": "latest",
|
|
42
|
+
"@whiskeysockets/baileys": "latest",
|
|
39
43
|
"async-mutex": "^0.5.0",
|
|
40
44
|
"cron-parser": "^5.5.0",
|
|
41
45
|
"croner": "^10.0.1",
|
|
46
|
+
"discord.js": "latest",
|
|
42
47
|
"docx": "^9.6.1",
|
|
48
|
+
"grammy": "latest",
|
|
43
49
|
"groq-sdk": "^0.37.0",
|
|
50
|
+
"jsonwebtoken": "^9.0.3",
|
|
44
51
|
"jszip": "^3.10.1",
|
|
45
52
|
"pdf-lib": "^1.17.1",
|
|
46
53
|
"mammoth": "^1.12.0",
|
|
@@ -48,10 +55,14 @@
|
|
|
48
55
|
"openai": "^6.18.0",
|
|
49
56
|
"pdfjs-dist": "^5.6.205",
|
|
50
57
|
"pptxgenjs": "^4.0.1",
|
|
58
|
+
"qrcode-terminal": "latest",
|
|
51
59
|
"toon-format-parser": "^1.1.0",
|
|
52
60
|
"xlsx": "^0.18.5",
|
|
53
61
|
"zod": "latest"
|
|
54
62
|
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@types/jsonwebtoken": "^9.0.10"
|
|
65
|
+
},
|
|
55
66
|
"peerDependencies": {
|
|
56
67
|
"typescript": "^5.0.0"
|
|
57
68
|
}
|
|
@@ -89,7 +89,7 @@ export function recordLLMUsage(opts: {
|
|
|
89
89
|
Promise.resolve().then(async () => {
|
|
90
90
|
try {
|
|
91
91
|
const { recordUsage } = await import("../storage/usage.ts")
|
|
92
|
-
recordUsage({
|
|
92
|
+
await recordUsage({
|
|
93
93
|
provider: opts.provider,
|
|
94
94
|
model: opts.model,
|
|
95
95
|
inputTokens: opts.inputTokens,
|
|
@@ -78,6 +78,16 @@ export interface AgentLoopOptions {
|
|
|
78
78
|
signal?: AbortSignal
|
|
79
79
|
/** Clean text for FTS5 and tracing (extracted from userMessage if multimodal) */
|
|
80
80
|
rawUserMessage?: string
|
|
81
|
+
/**
|
|
82
|
+
* Per-call provider API key override, taking precedence over the DB-stored
|
|
83
|
+
* key resolved by `resolveProviderConfig`. Required for safe multi-tenant
|
|
84
|
+
* hosting (e.g. hive-cloud): without it, concurrent calls for different
|
|
85
|
+
* tenants using the same provider would race on `process.env[...]`, since
|
|
86
|
+
* that env var is process-global.
|
|
87
|
+
*/
|
|
88
|
+
apiKey?: string
|
|
89
|
+
/** Per-call provider base URL override (self-hosted/proxy endpoints), same rationale as `apiKey`. */
|
|
90
|
+
baseUrl?: string
|
|
81
91
|
}
|
|
82
92
|
|
|
83
93
|
export interface StepEvent {
|
|
@@ -115,6 +125,8 @@ export async function* runAgent(
|
|
|
115
125
|
agent.provider_id || "openai",
|
|
116
126
|
agent.model_id || "gpt-4o-mini"
|
|
117
127
|
)
|
|
128
|
+
if (opts.apiKey) providerCfg.apiKey = opts.apiKey
|
|
129
|
+
if (opts.baseUrl) providerCfg.baseUrl = opts.baseUrl
|
|
118
130
|
|
|
119
131
|
const cleanModel = providerCfg.model.replace(new RegExp(`^${providerCfg.provider}\\/`), "")
|
|
120
132
|
log.info(`[agent-loop] Starting: agent=${agentName} thread=${opts.threadId} provider=${providerCfg.provider}/${cleanModel}`)
|
|
@@ -138,9 +138,9 @@ export async function compileContext(opts: {
|
|
|
138
138
|
|
|
139
139
|
// [STEP-2] STRATEGY 1: WRITE — Load scratchpad (persistent notes)
|
|
140
140
|
log.info(`[context-compiler] [STEP-2] Loading scratchpad...`)
|
|
141
|
-
let scratchpadNotes: ReturnType<typeof getScratchpad
|
|
141
|
+
let scratchpadNotes: Awaited<ReturnType<typeof getScratchpad>> = []
|
|
142
142
|
try {
|
|
143
|
-
scratchpadNotes = getScratchpad(threadId)
|
|
143
|
+
scratchpadNotes = await getScratchpad(threadId)
|
|
144
144
|
log.info(`[context-compiler] [STEP-2] ✅ Loaded ${scratchpadNotes.length} scratchpad notes`)
|
|
145
145
|
} catch (err) {
|
|
146
146
|
log.error(`[context-compiler] [STEP-2] ❌ FAILED loading scratchpad: ${JSON.stringify(err)}`)
|
|
@@ -257,7 +257,7 @@ export async function compileContext(opts: {
|
|
|
257
257
|
|
|
258
258
|
try {
|
|
259
259
|
// Load minimal skills (always available)
|
|
260
|
-
minimalSkills = getMinimalSkills()
|
|
260
|
+
minimalSkills = await getMinimalSkills()
|
|
261
261
|
log.info(`[context-compiler] [STEP-8b] ✅ Loaded ${minimalSkills.length} minimal skills`)
|
|
262
262
|
|
|
263
263
|
// Discover additional skills via FTS5 (coordinator only)
|
|
@@ -268,7 +268,7 @@ export async function compileContext(opts: {
|
|
|
268
268
|
: Array.isArray(inputForSkills)
|
|
269
269
|
? inputForSkills.filter(p => p.type === "text").map(p => (p as any).text).join("\n")
|
|
270
270
|
: String(inputForSkills)
|
|
271
|
-
discoveredSkills = selectSkills(textMessage)
|
|
271
|
+
discoveredSkills = await selectSkills(textMessage)
|
|
272
272
|
log.info(`[context-compiler] [STEP-8b] ✅ Discovered ${discoveredSkills.length} additional skills via FTS5`)
|
|
273
273
|
}
|
|
274
274
|
} catch (err) {
|
|
@@ -212,33 +212,43 @@ export function saveSummary(
|
|
|
212
212
|
`).run(threadId, summary, messagesCovered, lastMessageId)
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
import { getHiveDB } from "../storage/HiveDBStorage.ts";
|
|
216
|
+
|
|
217
|
+
interface ScratchpadDoc {
|
|
218
|
+
threadId: string;
|
|
219
|
+
key: string;
|
|
220
|
+
value: string;
|
|
221
|
+
updatedAt: number;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function scratchpadDocId(threadId: string, key: string): string {
|
|
225
|
+
return `${threadId}:${key}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
215
228
|
// ─── Scratchpad ───────────────────────────────────────────────────────────────
|
|
216
229
|
|
|
217
|
-
export function saveScratchpadNote(
|
|
230
|
+
export async function saveScratchpadNote(
|
|
218
231
|
threadId: string,
|
|
219
232
|
key: string,
|
|
220
233
|
value: string,
|
|
221
|
-
|
|
222
|
-
): void {
|
|
223
|
-
const db =
|
|
224
|
-
db.
|
|
225
|
-
|
|
226
|
-
VALUES (?, ?, ?, ?)
|
|
227
|
-
ON CONFLICT(thread_id, key) DO UPDATE SET
|
|
228
|
-
value = excluded.value,
|
|
229
|
-
source = excluded.source,
|
|
230
|
-
updated_at = unixepoch()
|
|
231
|
-
`).run(threadId, key, value, source ?? null)
|
|
234
|
+
_source?: string
|
|
235
|
+
): Promise<void> {
|
|
236
|
+
const db = await getHiveDB();
|
|
237
|
+
const col = db.collection<ScratchpadDoc>("scratchpad");
|
|
238
|
+
await col.put(scratchpadDocId(threadId, key), { threadId, key, value, updatedAt: Date.now() });
|
|
232
239
|
}
|
|
233
240
|
|
|
234
|
-
export function getScratchpad(threadId: string): Array<{ key: string; value: string }
|
|
235
|
-
const db =
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
241
|
+
export async function getScratchpad(threadId: string): Promise<Array<{ key: string; value: string }>> {
|
|
242
|
+
const db = await getHiveDB();
|
|
243
|
+
const col = db.collection<ScratchpadDoc>("scratchpad");
|
|
244
|
+
const entries = await col.scan();
|
|
245
|
+
return entries
|
|
246
|
+
.filter(e => e.doc.threadId === threadId)
|
|
247
|
+
.map(e => ({ key: e.doc.key, value: e.doc.value }));
|
|
239
248
|
}
|
|
240
249
|
|
|
241
|
-
export function deleteScratchpadNote(threadId: string, key: string): void {
|
|
242
|
-
const db =
|
|
243
|
-
db.
|
|
250
|
+
export async function deleteScratchpadNote(threadId: string, key: string): Promise<void> {
|
|
251
|
+
const db = await getHiveDB();
|
|
252
|
+
const col = db.collection<ScratchpadDoc>("scratchpad");
|
|
253
|
+
await col.delete(scratchpadDocId(threadId, key));
|
|
244
254
|
}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* into the agent prompt based on semantic relevance to the current message.
|
|
2
|
+
* HiveDB-based Playbook Rules Selector (ACE Curator)
|
|
3
|
+
*
|
|
4
|
+
* Uses HiveDB hybrid search over the playbook index.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
import {
|
|
7
|
+
import { getHiveDB } from "../../storage/HiveDBStorage.ts"
|
|
9
8
|
import { logger } from "../../utils/logger.ts"
|
|
9
|
+
import type { HivePlaybookDoc } from "../../storage/hiveSeed.ts"
|
|
10
|
+
import type { IndexDoc } from "@johpaz/hive-db"
|
|
10
11
|
|
|
11
12
|
const log = logger.child("playbook-selector")
|
|
12
13
|
|
|
13
14
|
// ─── Types ───────────────────────────────────────────────────────────────────────
|
|
14
15
|
|
|
15
16
|
export interface PlaybookRule {
|
|
16
|
-
id:
|
|
17
|
+
id: string
|
|
17
18
|
rule: string
|
|
18
19
|
category: string
|
|
19
20
|
applicable_to?: string
|
|
@@ -21,25 +22,27 @@ export interface PlaybookRule {
|
|
|
21
22
|
|
|
22
23
|
// ─── Configuration ─────────────────────────────────────────────────────────────
|
|
23
24
|
|
|
24
|
-
/** Maximum rules to inject per context window */
|
|
25
25
|
const MAX_RULES_PER_TURN = 5
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
const MIN_RELEVANCE_THRESHOLD = -10 // Relaxed for better matching
|
|
27
|
+
const MIN_RELEVANCE_THRESHOLD = 0.5
|
|
29
28
|
|
|
30
29
|
// ─── Selection Logic ───────────────────────────────────────────────────────────
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
function toRule(id: string, doc: HivePlaybookDoc): PlaybookRule {
|
|
32
|
+
return {
|
|
33
|
+
id,
|
|
34
|
+
rule: doc.rule,
|
|
35
|
+
category: doc.category,
|
|
36
|
+
applicable_to: doc.applicableTo ? doc.applicableTo.join(",") : undefined,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function selectPlaybookRules(message: string): Promise<PlaybookRule[]> {
|
|
41
|
+
const db = await getHiveDB()
|
|
37
42
|
const startTime = performance.now()
|
|
38
43
|
|
|
39
|
-
// Clean query — use prefix matching for consistency with skill-selector and tool-selector
|
|
40
44
|
const keywords = message
|
|
41
45
|
.toLowerCase()
|
|
42
|
-
// Keep only letters, numbers, spaces (strips ALL FTS5 special syntax)
|
|
43
46
|
.replace(/[^\p{L}\p{N}\s]/gu, " ")
|
|
44
47
|
.split(/\s+/)
|
|
45
48
|
.filter(w => w.length > 3)
|
|
@@ -47,32 +50,29 @@ export function selectPlaybookRules(message: string): PlaybookRule[] {
|
|
|
47
50
|
|
|
48
51
|
if (keywords.length === 0) return []
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
const ftsQuery = keywords.map(w => `${w}*`).join(" OR ")
|
|
53
|
+
const query = keywords.join(" ")
|
|
52
54
|
|
|
53
55
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
`).all(ftsQuery, MAX_RULES_PER_TURN) as Array<{ rowid: number; score: number }>
|
|
62
|
-
|
|
63
|
-
const relevantIds = ftsResults
|
|
56
|
+
const hits = await db.queryHybrid({
|
|
57
|
+
text: query,
|
|
58
|
+
k: MAX_RULES_PER_TURN,
|
|
59
|
+
boosts: { body: 5.0, tags: 2.0, name: 1.0 },
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const relevantIds = hits
|
|
64
63
|
.filter(r => r.score >= MIN_RELEVANCE_THRESHOLD)
|
|
65
|
-
.map(r => r.
|
|
64
|
+
.map(r => r.id)
|
|
66
65
|
|
|
67
66
|
if (relevantIds.length === 0) return []
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
const rules =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
68
|
+
const playbookCol = db.collection<HivePlaybookDoc>("playbook")
|
|
69
|
+
const rules: PlaybookRule[] = []
|
|
70
|
+
for (const id of relevantIds) {
|
|
71
|
+
const entry = await playbookCol.get(id)
|
|
72
|
+
if (entry && entry.doc.active) {
|
|
73
|
+
rules.push(toRule(id, entry.doc))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
76
|
|
|
77
77
|
const timing = performance.now() - startTime
|
|
78
78
|
log.info(`[playbook-selector] Selected ${rules.length} rules in ${timing.toFixed(2)}ms`)
|
|
@@ -89,56 +89,30 @@ export function selectPlaybookRules(message: string): PlaybookRule[] {
|
|
|
89
89
|
|
|
90
90
|
// ─── Sync Logic ───────────────────────────────────────────────────────────────
|
|
91
91
|
|
|
92
|
-
/**
|
|
93
|
-
* Sync active playbook rules to FTS5 virtual table
|
|
94
|
-
*/
|
|
95
92
|
export async function syncPlaybookToFTS(): Promise<void> {
|
|
96
|
-
const db =
|
|
93
|
+
const db = await getHiveDB()
|
|
97
94
|
|
|
98
95
|
try {
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
FROM playbook
|
|
103
|
-
WHERE active = 1
|
|
104
|
-
`).all() as Array<{
|
|
105
|
-
id: number
|
|
106
|
-
rule: string
|
|
107
|
-
category: string
|
|
108
|
-
applicable_to: string
|
|
109
|
-
}>
|
|
96
|
+
const playbookCol = db.collection<HivePlaybookDoc>("playbook")
|
|
97
|
+
const entries = await playbookCol.scan()
|
|
98
|
+
const rules = entries.map(e => ({ id: e.id, doc: e.doc })).filter(r => r.doc.active)
|
|
110
99
|
|
|
111
100
|
if (rules.length === 0) {
|
|
112
101
|
log.debug(`[playbook-selector] No rules in playbook to sync`)
|
|
102
|
+
return
|
|
113
103
|
}
|
|
114
104
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// A: Clear existing data
|
|
124
|
-
db.run("DELETE FROM playbook_fts")
|
|
125
|
-
|
|
126
|
-
// B: Prepare insertion
|
|
127
|
-
const insert = db.prepare(`
|
|
128
|
-
INSERT INTO playbook_fts(rowid, rule, category, applicable_to)
|
|
129
|
-
VALUES (?, ?, ?, ?)
|
|
130
|
-
`)
|
|
131
|
-
|
|
132
|
-
// C: Re-populate
|
|
133
|
-
for (const item of rules) {
|
|
134
|
-
insert.run(item.id, item.rule, item.category, item.applicable_to)
|
|
135
|
-
}
|
|
136
|
-
})
|
|
105
|
+
const docs: IndexDoc[] = rules.map(r => ({
|
|
106
|
+
id: r.id,
|
|
107
|
+
name: r.doc.category,
|
|
108
|
+
body: r.doc.rule,
|
|
109
|
+
tags: r.doc.applicableTo ? r.doc.applicableTo.join(" ") : "",
|
|
110
|
+
filters: [{ field: "type", value: "playbook" }],
|
|
111
|
+
}))
|
|
137
112
|
|
|
138
|
-
|
|
139
|
-
syncTransaction()
|
|
113
|
+
await db.upsertBatch(docs)
|
|
140
114
|
|
|
141
|
-
log.info(`[playbook-selector] Atomic sync complete: ${rules.length} rules indexed in
|
|
115
|
+
log.info(`[playbook-selector] Atomic sync complete: ${rules.length} rules indexed in HiveDB`)
|
|
142
116
|
|
|
143
117
|
} catch (err) {
|
|
144
118
|
log.error(`[playbook-selector] Transactional sync failed:`, err)
|