useathena 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -17
- package/dist/api/server.js +30 -4
- package/dist/cli/commands.js +81 -5
- package/dist/cli/export.js +35 -0
- package/dist/cli/service.js +99 -0
- package/dist/cli/setup.js +30 -2
- package/dist/cli.js +57 -24
- package/dist/core/fixtures.js +26 -0
- package/dist/core/refs.js +2 -0
- package/dist/engine/engine.js +15 -6
- package/dist/engine/facts.js +80 -0
- package/dist/engine/parse.js +31 -6
- package/dist/engine/prompts.js +10 -1
- package/dist/eval/run-eval.js +3 -3
- package/dist/mcp/server.js +38 -10
- package/dist/sensors/claude-code-hook.js +31 -2
- package/dist/serve/auto-learn.js +56 -0
- package/dist/serve/auto-outcome.js +63 -0
- package/dist/serve/brief.js +64 -11
- package/dist/store/open.js +4 -0
- package/dist/store/store.js +109 -0
- package/docs/schema.md +65 -6
- package/package.json +1 -1
package/dist/store/store.js
CHANGED
|
@@ -55,6 +55,19 @@ export class AthenaStore {
|
|
|
55
55
|
CREATE TABLE IF NOT EXISTS briefs (
|
|
56
56
|
id TEXT PRIMARY KEY, compiled_at TEXT NOT NULL, data TEXT NOT NULL
|
|
57
57
|
);
|
|
58
|
+
CREATE TABLE IF NOT EXISTS facts (
|
|
59
|
+
id TEXT PRIMARY KEY, object_id TEXT NOT NULL, domain TEXT NOT NULL,
|
|
60
|
+
last_confirmed_at TEXT NOT NULL, data TEXT NOT NULL
|
|
61
|
+
);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_facts_object ON facts(object_id);
|
|
63
|
+
CREATE TABLE IF NOT EXISTS drafts (
|
|
64
|
+
id TEXT PRIMARY KEY, brief_id TEXT NOT NULL, domain TEXT NOT NULL,
|
|
65
|
+
recorded_at TEXT NOT NULL, matched_outcome_id TEXT, data TEXT NOT NULL
|
|
66
|
+
);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_drafts_unmatched ON drafts(domain, recorded_at) WHERE matched_outcome_id IS NULL;
|
|
68
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
69
|
+
key TEXT PRIMARY KEY, value TEXT NOT NULL
|
|
70
|
+
);
|
|
58
71
|
CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(ref, lane, text);
|
|
59
72
|
`);
|
|
60
73
|
}
|
|
@@ -97,6 +110,13 @@ export class AthenaStore {
|
|
|
97
110
|
ORDER BY observed_at DESC LIMIT ${filter.limit ?? 200}`;
|
|
98
111
|
return this.db.prepare(sql).all(...params).map((rowData));
|
|
99
112
|
}
|
|
113
|
+
/** Instances observed strictly after `since` (all instances when omitted). */
|
|
114
|
+
countInstancesSince(since) {
|
|
115
|
+
const row = since
|
|
116
|
+
? this.db.prepare("SELECT COUNT(*) AS n FROM instances WHERE observed_at > ?").get(since)
|
|
117
|
+
: this.db.prepare("SELECT COUNT(*) AS n FROM instances").get();
|
|
118
|
+
return row.n;
|
|
119
|
+
}
|
|
100
120
|
// --- hypotheses (revisable views over evidence) ---
|
|
101
121
|
saveHypothesis(hypothesis) {
|
|
102
122
|
if (hypothesis.supportingInstanceIds.length === 0) {
|
|
@@ -170,6 +190,77 @@ export class AthenaStore {
|
|
|
170
190
|
.all(objectId, objectId)
|
|
171
191
|
.map((rowData));
|
|
172
192
|
}
|
|
193
|
+
// --- facts (revisable views over evidence, like hypotheses) ---
|
|
194
|
+
saveFact(fact) {
|
|
195
|
+
if (fact.supportingInstanceIds.length === 0) {
|
|
196
|
+
throw new Error(`fact ${fact.id} has no supporting instances — everything cites`);
|
|
197
|
+
}
|
|
198
|
+
const object = this.getObject(fact.objectId);
|
|
199
|
+
if (!object) {
|
|
200
|
+
throw new Error(`fact ${fact.id} references unknown object ${fact.objectId}`);
|
|
201
|
+
}
|
|
202
|
+
this.upsert("facts", "INSERT INTO facts (id, object_id, domain, last_confirmed_at, data) VALUES (?, ?, ?, ?, ?) " +
|
|
203
|
+
"ON CONFLICT(id) DO UPDATE SET object_id = excluded.object_id, domain = excluded.domain, " +
|
|
204
|
+
"last_confirmed_at = excluded.last_confirmed_at, data = excluded.data", [fact.id, fact.objectId, fact.domain, fact.lastConfirmedAt, JSON.stringify(fact)]);
|
|
205
|
+
this.index(`athena://fact/${fact.id}`, "fact", [fact.statement, fact.domain, object.name, object.aliases.join(" ")].join("\n"));
|
|
206
|
+
}
|
|
207
|
+
getFact(id) {
|
|
208
|
+
return this.getData("facts", id);
|
|
209
|
+
}
|
|
210
|
+
listFacts(filter = {}) {
|
|
211
|
+
const where = [];
|
|
212
|
+
const params = [];
|
|
213
|
+
if (filter.objectId) {
|
|
214
|
+
where.push("object_id = ?");
|
|
215
|
+
params.push(filter.objectId);
|
|
216
|
+
}
|
|
217
|
+
if (filter.domain) {
|
|
218
|
+
where.push("domain = ?");
|
|
219
|
+
params.push(filter.domain);
|
|
220
|
+
}
|
|
221
|
+
const sql = `SELECT data FROM facts ${where.length ? `WHERE ${where.join(" AND ")}` : ""}
|
|
222
|
+
ORDER BY last_confirmed_at DESC LIMIT ${filter.limit ?? 200}`;
|
|
223
|
+
return this.db.prepare(sql).all(...params).map((rowData));
|
|
224
|
+
}
|
|
225
|
+
/** Per-entity fact counts for the brief's knowledge map. */
|
|
226
|
+
factCountsByObject() {
|
|
227
|
+
return this.db
|
|
228
|
+
.prepare("SELECT object_id AS objectId, COUNT(*) AS count FROM facts GROUP BY object_id ORDER BY count DESC")
|
|
229
|
+
.all();
|
|
230
|
+
}
|
|
231
|
+
/** Per-domain servable-rule counts for the brief's knowledge map. */
|
|
232
|
+
hypothesisCountsByDomain() {
|
|
233
|
+
return this.db
|
|
234
|
+
.prepare("SELECT domain, COUNT(*) AS count FROM hypotheses WHERE status IN ('validated','active') GROUP BY domain ORDER BY count DESC")
|
|
235
|
+
.all();
|
|
236
|
+
}
|
|
237
|
+
// --- agent drafts (outputs awaiting an observed outcome) ---
|
|
238
|
+
saveDraft(draft) {
|
|
239
|
+
this.upsert("drafts", "INSERT INTO drafts (id, brief_id, domain, recorded_at, matched_outcome_id, data) VALUES (?, ?, ?, ?, ?, ?) " +
|
|
240
|
+
"ON CONFLICT(id) DO UPDATE SET matched_outcome_id = excluded.matched_outcome_id, data = excluded.data", [draft.id, draft.briefId, draft.domain, draft.recordedAt, draft.matchedOutcomeId ?? null, JSON.stringify(draft)]);
|
|
241
|
+
}
|
|
242
|
+
getDraft(id) {
|
|
243
|
+
return this.getData("drafts", id);
|
|
244
|
+
}
|
|
245
|
+
listUnmatchedDrafts(filter = {}) {
|
|
246
|
+
const where = ["matched_outcome_id IS NULL"];
|
|
247
|
+
const params = [];
|
|
248
|
+
if (filter.domain) {
|
|
249
|
+
where.push("domain = ?");
|
|
250
|
+
params.push(filter.domain);
|
|
251
|
+
}
|
|
252
|
+
if (filter.since) {
|
|
253
|
+
where.push("recorded_at >= ?");
|
|
254
|
+
params.push(filter.since);
|
|
255
|
+
}
|
|
256
|
+
return this.db
|
|
257
|
+
.prepare(`SELECT data FROM drafts WHERE ${where.join(" AND ")} ORDER BY recorded_at DESC LIMIT 100`)
|
|
258
|
+
.all(...params)
|
|
259
|
+
.map((rowData));
|
|
260
|
+
}
|
|
261
|
+
listDrafts() {
|
|
262
|
+
return this.db.prepare("SELECT data FROM drafts ORDER BY recorded_at DESC LIMIT 500").all().map((rowData));
|
|
263
|
+
}
|
|
173
264
|
// --- serving layer ---
|
|
174
265
|
saveBrief(brief) {
|
|
175
266
|
this.upsert("briefs", "INSERT INTO briefs (id, compiled_at, data) VALUES (?, ?, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data", [brief.id, brief.compiledAt, JSON.stringify(brief)]);
|
|
@@ -190,6 +281,22 @@ export class AthenaStore {
|
|
|
190
281
|
: this.db.prepare("SELECT data FROM outcomes ORDER BY recorded_at DESC").all();
|
|
191
282
|
return rows.map((rowData));
|
|
192
283
|
}
|
|
284
|
+
listBriefs(limit = 500) {
|
|
285
|
+
return this.db
|
|
286
|
+
.prepare(`SELECT data FROM briefs ORDER BY compiled_at DESC LIMIT ${limit}`)
|
|
287
|
+
.all()
|
|
288
|
+
.map((rowData));
|
|
289
|
+
}
|
|
290
|
+
// --- meta (operational state: last learn run, cooldowns) ---
|
|
291
|
+
getMeta(key) {
|
|
292
|
+
const row = this.db.prepare("SELECT value FROM meta WHERE key = ?").get(key);
|
|
293
|
+
return row ? row.value : undefined;
|
|
294
|
+
}
|
|
295
|
+
setMeta(key, value) {
|
|
296
|
+
this.db
|
|
297
|
+
.prepare("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value")
|
|
298
|
+
.run(key, value);
|
|
299
|
+
}
|
|
193
300
|
// --- stats ---
|
|
194
301
|
counts() {
|
|
195
302
|
const byColumn = (table, column) => {
|
|
@@ -205,6 +312,8 @@ export class AthenaStore {
|
|
|
205
312
|
sources: total("sources"),
|
|
206
313
|
outcomes: total("outcomes"),
|
|
207
314
|
briefs: total("briefs"),
|
|
315
|
+
facts: total("facts"),
|
|
316
|
+
unmatchedDrafts: this.db.prepare("SELECT COUNT(*) AS n FROM drafts WHERE matched_outcome_id IS NULL").get().n,
|
|
208
317
|
};
|
|
209
318
|
}
|
|
210
319
|
// --- search (lexical lane) ---
|
package/docs/schema.md
CHANGED
|
@@ -29,7 +29,8 @@ Mission framing: athena captures **tacit knowledge** from real work so agents be
|
|
|
29
29
|
## IDs
|
|
30
30
|
|
|
31
31
|
Prefixed ULIDs: `ins_` instance, `hyp_` hypothesis, `obj_` object, `src_` source,
|
|
32
|
-
`out_` outcome, `brf_` brief, `act_` actor, `sen_` sensor
|
|
32
|
+
`out_` outcome, `brf_` brief, `act_` actor, `sen_` sensor, `fct_` fact, `drf_` agent
|
|
33
|
+
draft. Sortable by creation time.
|
|
33
34
|
|
|
34
35
|
---
|
|
35
36
|
|
|
@@ -268,6 +269,30 @@ type ObjectRelation = {
|
|
|
268
269
|
};
|
|
269
270
|
```
|
|
270
271
|
|
|
272
|
+
### Fact — the explicit counterpart of a hypothesis
|
|
273
|
+
|
|
274
|
+
A declarative, entity-grounded statement extracted from evidence ("Acme's renewal
|
|
275
|
+
is in September" vs the tacit "be formal with Acme execs"). Facts are never entered
|
|
276
|
+
manually and agents cannot write them: the engine extracts them during learn, and
|
|
277
|
+
`materializeFacts` is the only write path — it resolves or creates the entity and
|
|
278
|
+
dedupes near-identical statements into a confirmation instead of a duplicate.
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
type Fact = {
|
|
282
|
+
id: FactId; // fct_…
|
|
283
|
+
objectId: ObjectId; // every fact is about exactly one entity
|
|
284
|
+
statement: string;
|
|
285
|
+
domain: string; // domain of the evidence cluster it came from
|
|
286
|
+
supportingInstanceIds: InstanceId[]; // facts cite, like everything else
|
|
287
|
+
confidence: number;
|
|
288
|
+
firstSeenAt: string;
|
|
289
|
+
lastConfirmedAt: string; // bumped when new evidence restates the fact
|
|
290
|
+
staleAfter: string; // unconfirmed past this → no longer served
|
|
291
|
+
supersededById?: FactId;
|
|
292
|
+
visibility: Exclude<Visibility, "user_private_raw">;
|
|
293
|
+
};
|
|
294
|
+
```
|
|
295
|
+
|
|
271
296
|
Markdown pages (per object, per domain) are **projections** generated from sources +
|
|
272
297
|
hypotheses + relations. Stored in `.athena/pages/` for humans and git; rebuilt, never edited
|
|
273
298
|
as truth.
|
|
@@ -284,6 +309,7 @@ Compact payload + dereferenceable pointers. Persisted for audit and outcome link
|
|
|
284
309
|
type Brief = {
|
|
285
310
|
id: BriefId;
|
|
286
311
|
task: string; // as the agent stated it
|
|
312
|
+
domain?: string; // when given — drafts registered against this brief match captures in it
|
|
287
313
|
compiledAt: string;
|
|
288
314
|
|
|
289
315
|
rules: {
|
|
@@ -305,6 +331,12 @@ type Brief = {
|
|
|
305
331
|
|
|
306
332
|
readiness: "act" | "act_with_caveats" | "inspect_first" | "ask_human";
|
|
307
333
|
refs: Ref[]; // additional evidence worth opening, ranked
|
|
334
|
+
|
|
335
|
+
map?: { // coordinates into everything else athena knows:
|
|
336
|
+
label: string; // "12 rules in email.outreach", "4 facts about Acme"
|
|
337
|
+
count: number;
|
|
338
|
+
query: string; // pass to athena_search — agents pull, never flooded
|
|
339
|
+
}[];
|
|
308
340
|
};
|
|
309
341
|
|
|
310
342
|
type Ref = string; // athena://hypothesis/hyp_x | athena://instance/ins_x
|
|
@@ -332,14 +364,39 @@ type AgentOutcome = {
|
|
|
332
364
|
This closes the loop: **brief → action → outcome → instance → hypothesis update**, and
|
|
333
365
|
gives us the north-star metric directly: correction rate per served brief, over time.
|
|
334
366
|
|
|
367
|
+
### AgentDraft — outcomes that record themselves
|
|
368
|
+
|
|
369
|
+
The artifact an agent produced from a brief, registered (`athena_record type=output`)
|
|
370
|
+
so athena can detect the outcome without anyone reporting: when a sensor later
|
|
371
|
+
captures what the human actually did with it, the instance matches back —
|
|
372
|
+
approval → `uncorrected`, correction → `corrected` with the instance as the
|
|
373
|
+
counterexample.
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
type AgentDraft = {
|
|
377
|
+
id: DraftId; // drf_…
|
|
378
|
+
briefId: BriefId;
|
|
379
|
+
content: string; // the artifact, verbatim
|
|
380
|
+
contentHash: string;
|
|
381
|
+
mediaType: string;
|
|
382
|
+
domain: string;
|
|
383
|
+
recordedAt: string;
|
|
384
|
+
matchedOutcomeId?: OutcomeId; // set once resolved (matched or expired)
|
|
385
|
+
};
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Matching is conservative — same domain, within 72h, captured before-text within
|
|
389
|
+
15% of the draft — because a false match would corrupt trust; a miss costs nothing.
|
|
390
|
+
Drafts that outlive the window expire to `result: "unknown"` (rule trust untouched).
|
|
391
|
+
|
|
335
392
|
### MCP surface (4 tools, not 19)
|
|
336
393
|
|
|
337
394
|
| Tool | Does |
|
|
338
395
|
|---|---|
|
|
339
396
|
| `athena_brief(task, domain?)` | compile + persist a Brief |
|
|
340
397
|
| `athena_open(ref)` | dereference any `athena://` ref |
|
|
341
|
-
| `athena_record(event)` |
|
|
342
|
-
| `athena_search(query, lane?)` | direct
|
|
398
|
+
| `athena_record(event)` | outcome report, draft registration (`type=output`), or SensorEvent: correction, manual note, rule *proposal* |
|
|
399
|
+
| `athena_search(query, lane?)` | direct search (lanes: instance, hypothesis, source, fact) when the agent wants to explore |
|
|
343
400
|
|
|
344
401
|
No durable-mutation tools. `athena_record` proposals enter the review queue like any sensor event.
|
|
345
402
|
|
|
@@ -348,9 +405,11 @@ No durable-mutation tools. `athena_record` proposals enter the review queue like
|
|
|
348
405
|
## 5. Storage mapping
|
|
349
406
|
|
|
350
407
|
SQLite, one file per workspace (`.athena/athena.db`):
|
|
351
|
-
- `instances`, `hypotheses`, `sources`, `objects`, `relations`, `outcomes`, `briefs
|
|
352
|
-
— JSON column + extracted indexed columns (id, kind, domain, status,
|
|
353
|
-
|
|
408
|
+
- `instances`, `hypotheses`, `sources`, `objects`, `relations`, `outcomes`, `briefs`,
|
|
409
|
+
`facts`, `drafts` — JSON column + extracted indexed columns (id, kind, domain, status,
|
|
410
|
+
observedAt, visibility).
|
|
411
|
+
- `meta` — key/value operational state (last learn run, auto-learn cooldown).
|
|
412
|
+
- FTS5 over instance summaries/diffs, hypothesis rules, source content, fact statements.
|
|
354
413
|
- `sqlite-vec` over situation/rule/source embeddings (optional lane — everything works without it).
|
|
355
414
|
- WAL mode; single-writer per file is acceptable for local; server mode serializes per workspace.
|
|
356
415
|
|