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.
@@ -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. Sortable by creation time.
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)` | SensorEvent in: outcome, correction, manual note, rule *proposal* |
342
- | `athena_search(query, lane?)` | direct lexical+semantic search when the agent wants to explore |
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, observedAt, visibility).
353
- - FTS5 over instance summaries/diffs, hypothesis rules, source content.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "useathena",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "athena captures tacit knowledge from real work so agents become truly autonomous and reliable.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {