@tpsdev-ai/flair 0.24.0 → 0.25.1
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 +13 -4
- package/dist/cli.js +32 -11
- package/dist/resources/Memory.js +75 -3
- package/dist/resources/MemoryBootstrap.js +85 -3
- package/dist/resources/RecordUsage.js +32 -80
- package/dist/resources/SemanticSearch.js +71 -3
- package/dist/resources/abstention.js +146 -0
- package/dist/resources/mcp-tools.js +42 -5
- package/dist/resources/semantic-retrieval-core.js +24 -2
- package/dist/resources/trust-block.js +187 -0
- package/dist/resources/usage-recording.js +160 -0
- package/package.json +3 -2
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usage-recording.ts — shared usage-ledger recording core (flair#744 slice A,
|
|
3
|
+
* "citation-on-write"; extracted from RecordUsage.ts's flair#683 signal so
|
|
4
|
+
* there is exactly ONE implementation of the ledger logic, not two that can
|
|
5
|
+
* drift).
|
|
6
|
+
*
|
|
7
|
+
* Two callers share this core, both crediting the SAME deduped,
|
|
8
|
+
* principal-bound `MemoryUsage` ledger + the targeted `Memory.usageCount`
|
|
9
|
+
* bump:
|
|
10
|
+
* - `POST /RecordUsage` (resources/RecordUsage.ts) — an agent explicitly
|
|
11
|
+
* reports "I used this memory" as a standalone call.
|
|
12
|
+
* - Citation-on-write (resources/Memory.ts's post()/put(), via
|
|
13
|
+
* `recordCitations()` below) — an agent cites `usedMemoryIds` inline on a
|
|
14
|
+
* memory WRITE; each cited id is credited the same way, post-commit and
|
|
15
|
+
* failure-isolated from the write itself.
|
|
16
|
+
*
|
|
17
|
+
* `recordUsageContribution()` is an EXACT extraction of what was
|
|
18
|
+
* RecordUsage.ts's private `_recordOne()` — see its doc below for the
|
|
19
|
+
* ledger-then-count ordering, the individually-wrapped `withDetachedTxn`
|
|
20
|
+
* discipline, and the accepted best-effort race on the final
|
|
21
|
+
* read-modify-write. This move changes WHERE the logic lives, not what it
|
|
22
|
+
* does — behavior is byte-identical for RecordUsage.ts's existing callers.
|
|
23
|
+
*
|
|
24
|
+
* `recordCitations()` is the NEW batch helper citation-on-write uses: the
|
|
25
|
+
* same agent-required / cap / dedup / per-id failure-isolation shape as
|
|
26
|
+
* RecordUsage.post()'s loop, parameterized so Memory.ts can drive it from a
|
|
27
|
+
* write body instead of a dedicated POST body. It NEVER reads the ledger for
|
|
28
|
+
* authority — it only writes contributions; `usedMemoryIds` must never enter
|
|
29
|
+
* an access/scope/attribution/dedup decision (flair#744 slice A invariant 3).
|
|
30
|
+
*/
|
|
31
|
+
import { databases } from "@harperfast/harper";
|
|
32
|
+
import { withDetachedTxn } from "./table-helpers.js";
|
|
33
|
+
/**
|
|
34
|
+
* Per-call cap on ids credited in one batch — shared by RecordUsage.post()'s
|
|
35
|
+
* validated `memoryIds` body (rejects a batch over the cap with a 400) and
|
|
36
|
+
* recordCitations()'s advisory `usedMemoryIds` (silently slices instead —
|
|
37
|
+
* see that function's doc for why the two differ here).
|
|
38
|
+
*/
|
|
39
|
+
export const MAX_USAGE_IDS_PER_CALL = 20;
|
|
40
|
+
/**
|
|
41
|
+
* One (agentId, memoryId) contribution — the shared ledger-write core.
|
|
42
|
+
*
|
|
43
|
+
* Ledger-row-create FIRST, THEN the Memory.usageCount bump — so a crash
|
|
44
|
+
* between the two leaves the SAFE failure state (ledger row exists, count
|
|
45
|
+
* not yet bumped: a later retry just re-checks and no-ops) rather than the
|
|
46
|
+
* reverse (count bumped, no ledger row → a retry would double-count).
|
|
47
|
+
*
|
|
48
|
+
* Every discrete Harper call is wrapped INDIVIDUALLY in its own
|
|
49
|
+
* withDetachedTxn — never one wrap around a multi-step helper. A request
|
|
50
|
+
* that reads/writes MULTIPLE tables (or the same table twice) in sequence
|
|
51
|
+
* can otherwise inherit a closed transaction from a prior call's drained
|
|
52
|
+
* chain (table-helpers.ts's withDetachedTxn doc); resources/Memory.ts's
|
|
53
|
+
* closeSupersededRecord documents exactly why a single wrap around a
|
|
54
|
+
* multi-await async function does NOT protect a later call inside it —
|
|
55
|
+
* this mirrors that function's literal shape (get wrapped, then a
|
|
56
|
+
* SEPARATE put wrapped) rather than delegating to the generic
|
|
57
|
+
* patchRecord() helper, which would combine both into one un-safe wrap.
|
|
58
|
+
*
|
|
59
|
+
* The final get-then-put for the increment is a best-effort (non-atomic)
|
|
60
|
+
* read-modify-write, same class of race already accepted elsewhere in
|
|
61
|
+
* this codebase for count fields (e.g. retrievalCount's bump in
|
|
62
|
+
* SemanticSearch.ts) — a concurrent contribution from a DIFFERENT agent
|
|
63
|
+
* landing between this call's read and write could lose one increment.
|
|
64
|
+
* Re-fetching immediately before the write (rather than reusing the
|
|
65
|
+
* earlier existence-check read) narrows, without eliminating, that
|
|
66
|
+
* window. Not solved here: bounded, low-severity (an undercount, never an
|
|
67
|
+
* inflation), and orthogonal to the anti-gaming properties the cap/floor
|
|
68
|
+
* and dedup ledger actually defend.
|
|
69
|
+
*
|
|
70
|
+
* Called by RecordUsage.post() (explicit usage feedback) and by
|
|
71
|
+
* recordCitations() below (citation-on-write) — identical ledger semantics
|
|
72
|
+
* regardless of which surface triggered the contribution.
|
|
73
|
+
*/
|
|
74
|
+
export async function recordUsageContribution(ctx, agentId, memoryId, attribution, now) {
|
|
75
|
+
const ledgerId = `${agentId}:${memoryId}`;
|
|
76
|
+
// Bypasses resources/MemoryUsage.ts's own auth wrapper by design — this
|
|
77
|
+
// IS the trusted internal caller that resource's module doc describes
|
|
78
|
+
// (same "raw table object" pattern resources/Memory.ts uses for
|
|
79
|
+
// MemoryGrant).
|
|
80
|
+
const existingContribution = await withDetachedTxn(ctx, () => databases.flair.MemoryUsage.get(ledgerId)).catch(() => null);
|
|
81
|
+
if (existingContribution)
|
|
82
|
+
return; // already counted by this agent — silent no-op
|
|
83
|
+
const memoryExists = await withDetachedTxn(ctx, () => databases.flair.Memory.get(memoryId)).catch(() => null);
|
|
84
|
+
if (!memoryExists)
|
|
85
|
+
return; // no such memory — silent no-op (no enumeration)
|
|
86
|
+
const ledgerRecord = { id: ledgerId, agentId, memoryId, createdAt: now };
|
|
87
|
+
if (attribution)
|
|
88
|
+
ledgerRecord.attribution = attribution;
|
|
89
|
+
// .put(), not .post(): Harper's raw TableResource has no default post()
|
|
90
|
+
// implementation for a static-style (non-`isCollection`-instantiated)
|
|
91
|
+
// call — confirmed live ("The MemoryUsage does not have a post method
|
|
92
|
+
// implemented", statusCode 405) — the SAME class of gotcha
|
|
93
|
+
// resources/Memory.ts documents for HTTP POST, but here it bites even
|
|
94
|
+
// this in-process call. Irrelevant anyway: ledgerId is already a
|
|
95
|
+
// deterministic composite key, so this is a create-with-explicit-id —
|
|
96
|
+
// exactly what PUT (upsert) is for, not an auto-generated-id insert.
|
|
97
|
+
await withDetachedTxn(ctx, () => databases.flair.MemoryUsage.put(ledgerRecord));
|
|
98
|
+
// Targeted usageCount-ONLY bump: read-full-record, merge just this one
|
|
99
|
+
// field, write — against the RAW Memory table, NEVER Memory.put() (the
|
|
100
|
+
// resource class): that would 403 this cross-agent write via its
|
|
101
|
+
// ownership check, and bypassing that check directly would risk letting
|
|
102
|
+
// this write path smuggle OTHER field changes through instead of just the
|
|
103
|
+
// count (RecordUsage.ts module doc's "WHY THIS IS ITS OWN ENDPOINT").
|
|
104
|
+
const fresh = await withDetachedTxn(ctx, () => databases.flair.Memory.get(memoryId)).catch(() => null);
|
|
105
|
+
if (!fresh)
|
|
106
|
+
return; // deleted between the checks above and now — no-op
|
|
107
|
+
await withDetachedTxn(ctx, () => databases.flair.Memory.put({ ...fresh, usageCount: (fresh.usageCount ?? 0) + 1 }));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Batch citation helper — credits every id in `usedMemoryIds` through
|
|
111
|
+
* `recordFn` (the real `recordUsageContribution` by default), one contribution
|
|
112
|
+
* per unique id, capped at `MAX_USAGE_IDS_PER_CALL`.
|
|
113
|
+
*
|
|
114
|
+
* Called by Memory.ts's post()/put() POST-COMMIT, wrapped in its own
|
|
115
|
+
* try/catch at the call site — this function itself never throws (every
|
|
116
|
+
* per-id failure is caught and logged below), but callers still isolate the
|
|
117
|
+
* call as a second line of defense so a write can never be affected by
|
|
118
|
+
* citation recording (flair#744 slice A invariant 1).
|
|
119
|
+
*
|
|
120
|
+
* - `auth.kind !== "agent"` ⇒ return immediately, no-op. Internal/admin/
|
|
121
|
+
* anonymous writes have no agent identity to attribute a contribution
|
|
122
|
+
* TO — same rule RecordUsage.post() applies ("usage feedback requires a
|
|
123
|
+
* verified agent identity").
|
|
124
|
+
* - `usedMemoryIds` not a non-empty array of non-empty strings ⇒ no-op
|
|
125
|
+
* (advisory field, never a validated request body — a malformed value
|
|
126
|
+
* is silently ignored, never a 400).
|
|
127
|
+
* - Deduped within the call (`[...new Set(...)]`), THEN capped at
|
|
128
|
+
* MAX_USAGE_IDS_PER_CALL by slicing — a citation list is advisory, not a
|
|
129
|
+
* validated request body, so an oversized list is trimmed rather than
|
|
130
|
+
* rejected (unlike RecordUsage.post()'s validated `memoryIds`, which
|
|
131
|
+
* 400s over the same cap).
|
|
132
|
+
* - Each id is credited independently: one id throwing never stops the
|
|
133
|
+
* rest, and the failure is logged server-side, never surfaced to the
|
|
134
|
+
* caller (the write already committed by the time this runs).
|
|
135
|
+
*
|
|
136
|
+
* `agentId` passed to `recordFn` is ALWAYS `auth.agentId` — the resolved
|
|
137
|
+
* auth context, never anything derived from `usedMemoryIds` or any other
|
|
138
|
+
* caller-supplied input (flair#744 slice A invariant 4: no forging on
|
|
139
|
+
* behalf of another identity).
|
|
140
|
+
*/
|
|
141
|
+
export async function recordCitations(ctx, auth, usedMemoryIds, now, recordFn = recordUsageContribution) {
|
|
142
|
+
if (auth.kind !== "agent")
|
|
143
|
+
return;
|
|
144
|
+
if (!Array.isArray(usedMemoryIds) ||
|
|
145
|
+
usedMemoryIds.length === 0 ||
|
|
146
|
+
!usedMemoryIds.every((id) => typeof id === "string" && id.length > 0)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const ids = [...new Set(usedMemoryIds)].slice(0, MAX_USAGE_IDS_PER_CALL);
|
|
150
|
+
for (const id of ids) {
|
|
151
|
+
try {
|
|
152
|
+
await recordFn(ctx, auth.agentId, id, undefined, now);
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
// Never let one bad id stop the batch — same no-op-on-error discipline
|
|
156
|
+
// as RecordUsage.post()'s loop, log server-side only.
|
|
157
|
+
console.error("recordCitations: failed to credit (no-op)", { memoryId: id, err });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.1",
|
|
4
|
+
"packageManager": "bun@1.3.10",
|
|
4
5
|
"description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"license": "Apache-2.0",
|
|
@@ -54,7 +55,7 @@
|
|
|
54
55
|
"node": ">=22"
|
|
55
56
|
},
|
|
56
57
|
"dependencies": {
|
|
57
|
-
"@harperfast/harper": "5.1.
|
|
58
|
+
"@harperfast/harper": "5.1.22",
|
|
58
59
|
"@harperfast/oauth": "2.4.0",
|
|
59
60
|
"@types/js-yaml": "4.0.9",
|
|
60
61
|
"commander": "14.0.3",
|