@cef-ai/testing 3.5.3 → 4.0.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/dist/harness/memory-bank.d.ts +142 -69
- package/dist/harness/memory-bank.d.ts.map +1 -1
- package/dist/harness/memory-bank.js +624 -170
- package/dist/harness/memory-bank.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/platform/runtime.d.ts +9 -2
- package/dist/platform/runtime.d.ts.map +1 -1
- package/dist/platform/runtime.js +29 -20
- package/dist/platform/runtime.js.map +1 -1
- package/dist/test-agent.d.ts +6 -0
- package/dist/test-agent.d.ts.map +1 -1
- package/dist/test-agent.js +23 -20
- package/dist/test-agent.js.map +1 -1
- package/dist/test-platform.d.ts +14 -0
- package/dist/test-platform.d.ts.map +1 -1
- package/dist/test-platform.js +2 -0
- package/dist/test-platform.js.map +1 -1
- package/package.json +5 -4
|
@@ -1,191 +1,645 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* THE FAKE MEMORY BANK — vault-api's memory door, in memory (contract C2 §5).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* A JS model of the bank's own columns (the parity test diffs them against a
|
|
5
|
+
* SHA-pinned copy of ddc-node's DDL, `fixtures/vault-memory-ddl.sql`), served
|
|
6
|
+
* behind a fetch-level copy of the REST door: `POST <base>/<tool>` with the
|
|
7
|
+
* real request bodies, the real `{columns, rows}` projections, 204 writes, and
|
|
8
|
+
* vault-api's `{"error":{"code","message"}}` refusals. The agent-sdk's own
|
|
9
|
+
* `ctx.memory` shim runs against it unmodified — the fake is no longer a
|
|
10
|
+
* parallel object with method signatures of its own.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* FAITHFUL ON PURPOSE, where the old fake was not:
|
|
13
|
+
* - Reads are gated: scope ∈ the connection's scopes, privacy ≤ its ceiling.
|
|
14
|
+
* A hidden record and an absent one look the same.
|
|
15
|
+
* - Statuses: a new row with no status lands `accepted`; reads return
|
|
16
|
+
* accepted rows unless `statuses` says otherwise; `statuses` with `status`
|
|
17
|
+
* or `include_unaccepted` is a 400.
|
|
18
|
+
* - The write policy. The default is `curators` (the D1 target, and devnet):
|
|
19
|
+
* an ordinary connection's write is a 403; a harvester's upsert is forced
|
|
20
|
+
* to `proposed` and never rewrites a decided row; an analyzer writes only
|
|
21
|
+
* `metric:`/`obs:` ids and metric keys the rule admits.
|
|
22
|
+
* - A write is acked alike whether or not it landed: a hidden target, an edge
|
|
23
|
+
* to an invisible end, a metric on a decided record all answer 204 and
|
|
24
|
+
* change nothing.
|
|
25
|
+
* - Per platform, never a module singleton.
|
|
17
26
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
27
|
+
* APPROXIMATED, and said so: search is case-insensitive term matching over
|
|
28
|
+
* id, type, title and body (all terms, a trailing `*` makes a prefix), not
|
|
29
|
+
* FTS5 with stemming and bm25. `rank` is the negated hit count.
|
|
20
30
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* people. A fake that accepted one would let a workflow ship that the store
|
|
29
|
-
* then rejects in production.
|
|
30
|
-
*
|
|
31
|
-
* SEARCH NEEDS A TERM. The index is FTS5, so an empty match matches NOTHING
|
|
32
|
-
* rather than everything. A fake that returned everything for "" would make
|
|
33
|
-
* a node that silently reads nothing look like it works.
|
|
34
|
-
*
|
|
35
|
-
* Matching is substring rather than FTS5, with a trailing `*` tolerated, which
|
|
36
|
-
* is close enough to test a graph and not close enough to test a query.
|
|
37
|
-
*
|
|
38
|
-
* PROPS, CREATED_AT AND DOMAIN follow the bank's rules too, because a view
|
|
39
|
-
* that draws records reads them: props must be one object, a date may not be
|
|
40
|
-
* in the future, and a write that leaves either out keeps what the record
|
|
41
|
-
* already has (a new record is stamped now). Domain is the exception: the bank
|
|
42
|
-
* overwrites it from every write, so leaving it out clears it. A read returns props as JSON
|
|
43
|
-
* text, as the bank's own row does, so a workflow that parses them in
|
|
44
|
-
* production parses them here; `bank.records` holds the object, for
|
|
45
|
-
* assertions.
|
|
31
|
+
* `c3` (default true) models vault-api with contract C3: `id_prefix`,
|
|
32
|
+
* `domains` on `record_list`, the review stamp, the analyzer key rule, the
|
|
33
|
+
* `involves` harvester edge, and a 403 for a write above the writer's ceiling.
|
|
34
|
+
* `c3: false` models the vault-api deployed before it: those facets are
|
|
35
|
+
* IGNORED (as Go's JSON decoder ignores an unknown field), analyzer keys are
|
|
36
|
+
* the nine of the old allow-list, and an insert above the ceiling lands where
|
|
37
|
+
* its writer cannot see it.
|
|
46
38
|
*/
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
39
|
+
import { createMemoryHandle } from "@cef-ai/agent-sdk/runtime";
|
|
40
|
+
/* -------------------------------------------------------------------------- */
|
|
41
|
+
/* the model — named and ordered as the bank's own columns */
|
|
42
|
+
/* -------------------------------------------------------------------------- */
|
|
43
|
+
/** `record`'s columns (ddc-node memory migrations 0001, 0002, 0007; C3 §1.4). */
|
|
44
|
+
export const RECORD_COLUMNS = [
|
|
45
|
+
"id", "type", "title", "body", "scope", "privacy", "created_at", "updated_at",
|
|
46
|
+
"domain", "status", "version", "props", "reviewed_by", "reviewed_at",
|
|
47
|
+
];
|
|
48
|
+
/** `relation`'s columns (0001, 0002). */
|
|
49
|
+
export const RELATION_COLUMNS = [
|
|
50
|
+
"id", "in_id", "out_id", "type", "scope", "privacy", "created_at", "updated_at", "domain",
|
|
51
|
+
];
|
|
52
|
+
/** `record_metric`'s columns (0002). */
|
|
53
|
+
export const RECORD_METRIC_COLUMNS = ["record_id", "key", "value"];
|
|
54
|
+
/**
|
|
55
|
+
* Columns the model carries ahead of the pinned DDL because an approved
|
|
56
|
+
* contract adds them and the migration is not merged yet. The parity test
|
|
57
|
+
* fails when one of these appears in the DDL, so the list is emptied the day
|
|
58
|
+
* the fixture is refreshed past that migration.
|
|
59
|
+
*/
|
|
60
|
+
export const PENDING_COLUMNS = {
|
|
61
|
+
record: ["reviewed_by", "reviewed_at"], // C3 §1.4 review stamp, memory schema v9
|
|
69
62
|
};
|
|
63
|
+
/* -------------------------------------------------------------------------- */
|
|
64
|
+
/* rules mirrored from vault-api (ddc-node handlers/memory.go, memory/*.go) */
|
|
65
|
+
/* -------------------------------------------------------------------------- */
|
|
66
|
+
const PRIVACIES = ["public", "internal", "private", "restricted"];
|
|
67
|
+
const rank = (p) => PRIVACIES.indexOf(p);
|
|
68
|
+
const STATUSES = ["proposed", "accepted", "rejected", "superseded"];
|
|
69
|
+
const PLATFORM_EDGES = ["used", "produced", "ran_by", "caused_by"];
|
|
70
|
+
const HARVESTER_EDGES_PRE_C3 = ["derived_from", "supersedes", "about"];
|
|
71
|
+
const HARVESTER_EDGES_C3 = [...HARVESTER_EDGES_PRE_C3, "involves"];
|
|
72
|
+
const ANALYZER_EDGES = ["derived_from", "about"];
|
|
73
|
+
const ANALYZER_KEYS_PRE_C3 = ["n", "decided", "hits", "hit_rate", "base_rate", "lift", "window_days", "config_version", "dropped"];
|
|
74
|
+
const ANALYZER_KEY_RULE = /^[a-z][a-z0-9_]{0,62}$/;
|
|
75
|
+
const LEDGER_KEYS = ["a2a_cost", "a2a_tokens", "duration_ms"];
|
|
76
|
+
const MAX_LIMIT = 500;
|
|
77
|
+
const DEFAULT_LIMIT = 50;
|
|
78
|
+
const DEFAULT_BODY_CHARS = 4000;
|
|
79
|
+
const MAX_BODY_CHARS = 20000;
|
|
80
|
+
const MAX_PROPS_BYTES = 16 * 1024;
|
|
81
|
+
const CREATED_AT_SKEW_MS = 5 * 60 * 1000;
|
|
82
|
+
const WORKFLOW_AGENT_REFUSAL = "memory: workflow agents write to cubbies; only harvester connections write to the "
|
|
83
|
+
+ "memory bank — write this run's output to a cubby, or ask the vault owner to mark this connection as a harvester";
|
|
84
|
+
const NO_INITIATOR = "memory: this run holds no privacy ceiling, so it can write nothing — an agent's ceiling is "
|
|
85
|
+
+ "its job initiator's, and this job has no initiator (no person published the event that started it, directly "
|
|
86
|
+
+ "or through the agents that led to it)";
|
|
70
87
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* the question worth asking: "the step did not throw" is not the same claim as
|
|
75
|
-
* "the record is there, with that classification".
|
|
88
|
+
* record_list's cursor, as this fake mints it. A cursor that does not decode is
|
|
89
|
+
* the door's 400 (ddc-node memory/cursor.go decodeCursorTuple), never a thrown
|
|
90
|
+
* SyntaxError out of the fetch.
|
|
76
91
|
*/
|
|
77
|
-
|
|
92
|
+
function decodeListCursor(cursor) {
|
|
93
|
+
let parts;
|
|
94
|
+
try {
|
|
95
|
+
parts = JSON.parse(Buffer.from(cursor, "base64url").toString());
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
throw new Refusal(400, "BAD_REQUEST", "memory: record_list: cursor does not decode");
|
|
99
|
+
}
|
|
100
|
+
if (!Array.isArray(parts) || parts.length !== 2 || parts.some((x) => typeof x !== "string")) {
|
|
101
|
+
throw new Refusal(400, "BAD_REQUEST", "memory: record_list: cursor is the wrong tuple for this tool");
|
|
102
|
+
}
|
|
103
|
+
return parts;
|
|
104
|
+
}
|
|
105
|
+
/** "a, b and c" — how vault-api lists edge types in a refusal. */
|
|
106
|
+
function andList(xs) {
|
|
107
|
+
return xs.length < 2 ? xs.join("") : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
|
|
108
|
+
}
|
|
109
|
+
class Refusal extends Error {
|
|
110
|
+
status;
|
|
111
|
+
code;
|
|
112
|
+
constructor(status, code, message) {
|
|
113
|
+
super(message);
|
|
114
|
+
this.status = status;
|
|
115
|
+
this.code = code;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const bad = (msg) => new Refusal(400, "BAD_REQUEST", msg);
|
|
119
|
+
const denied = (msg) => new Refusal(403, "SCOPE_DENIED", msg);
|
|
120
|
+
const str = (v) => (typeof v === "string" ? v : "");
|
|
121
|
+
const strList = (v) => Array.isArray(v) ? v.map((x) => String(x)) : undefined;
|
|
122
|
+
/* -------------------------------------------------------------------------- */
|
|
123
|
+
export function fakeMemoryBank(opts = {}) {
|
|
78
124
|
const records = new Map();
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
125
|
+
const relations = [];
|
|
126
|
+
const metrics = new Map();
|
|
127
|
+
const requests = [];
|
|
128
|
+
const base = "http://fake-vault/api/v1/vaults/v-test/memory";
|
|
129
|
+
// A clock that never repeats, so `updated_at DESC` is a total order.
|
|
130
|
+
let last = 0;
|
|
131
|
+
const now = () => {
|
|
132
|
+
last = Math.max(Date.now(), last + 1);
|
|
133
|
+
return new Date(last).toISOString();
|
|
87
134
|
};
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
135
|
+
const bank = {
|
|
136
|
+
records,
|
|
137
|
+
relations,
|
|
138
|
+
metrics,
|
|
139
|
+
requests,
|
|
140
|
+
base,
|
|
141
|
+
options: { policy: opts.policy ?? "curators", c3: opts.c3 ?? true, connection: opts.connection, connections: opts.connections },
|
|
142
|
+
all: () => [...records.values()],
|
|
143
|
+
props: (id) => {
|
|
144
|
+
const r = records.get(id);
|
|
145
|
+
return r ? JSON.parse(r.props) : undefined;
|
|
146
|
+
},
|
|
147
|
+
seed(r) {
|
|
148
|
+
const at = now();
|
|
100
149
|
const row = {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
scope: str(r.scope),
|
|
106
|
-
privacy,
|
|
107
|
-
// Left out, each keeps what the record has — see the note above.
|
|
108
|
-
...(props ?? existing?.props ? { props: props ?? existing?.props } : {}),
|
|
109
|
-
created_at: createdAt ?? existing?.created_at ?? new Date().toISOString(),
|
|
110
|
-
// Domain is NOT kept: the bank overwrites it from every write, so a
|
|
111
|
-
// write that leaves it out clears it.
|
|
112
|
-
...(str(r.domain) ? { domain: str(r.domain) } : {}),
|
|
150
|
+
title: "", body: "", domain: "", status: "accepted", version: 1,
|
|
151
|
+
created_at: at, updated_at: at, reviewed_by: null, reviewed_at: null,
|
|
152
|
+
...r,
|
|
153
|
+
props: r.props === undefined ? "{}" : typeof r.props === "string" ? r.props : JSON.stringify(r.props),
|
|
113
154
|
};
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return { id };
|
|
117
|
-
// UPSERT, NOT APPEND — see the note above.
|
|
118
|
-
records.set(id, row);
|
|
119
|
-
return { id };
|
|
120
|
-
},
|
|
121
|
-
update: async (r) => {
|
|
122
|
-
const id = str(r.id);
|
|
123
|
-
const existing = records.get(id);
|
|
124
|
-
if (!existing)
|
|
125
|
-
throw new Error(`[bank] no record ${id}`);
|
|
126
|
-
records.set(id, { ...existing, ...r, id });
|
|
127
|
-
return { id };
|
|
128
|
-
},
|
|
129
|
-
setPrivacy: async (r) => {
|
|
130
|
-
const id = str(r.id);
|
|
131
|
-
const existing = records.get(id);
|
|
132
|
-
if (!existing)
|
|
133
|
-
throw new Error(`[bank] no record ${id}`);
|
|
134
|
-
records.set(id, { ...existing, privacy: classify(r.privacy, "record") });
|
|
135
|
-
return { id };
|
|
155
|
+
records.set(row.id, row);
|
|
156
|
+
return row;
|
|
136
157
|
},
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
158
|
+
accept: (id, o) => decide(id, "accepted", o?.by),
|
|
159
|
+
reject: (id, o) => decide(id, "rejected", o?.by),
|
|
160
|
+
supersede(id) {
|
|
161
|
+
const r = must(id);
|
|
162
|
+
r.status = "superseded";
|
|
163
|
+
r.updated_at = now();
|
|
140
164
|
},
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
165
|
+
fetch: (async (input, init) => {
|
|
166
|
+
const url = String(input instanceof Request ? input.url : input);
|
|
167
|
+
const tool = url.slice(url.lastIndexOf("/") + 1);
|
|
168
|
+
const headers = new Headers(init?.headers);
|
|
169
|
+
const agentId = headers.get("x-agent-id") ?? "";
|
|
170
|
+
let body;
|
|
171
|
+
try {
|
|
172
|
+
body = JSON.parse(String(init?.body ?? "{}"));
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return refusal(new Refusal(400, "BAD_REQUEST", "memory: bad body"));
|
|
176
|
+
}
|
|
177
|
+
requests.push({ tool, body, agentId });
|
|
178
|
+
try {
|
|
179
|
+
const out = serve(tool, body, connectionOf(agentId));
|
|
180
|
+
if (out === null)
|
|
181
|
+
return new Response(null, { status: 204 });
|
|
182
|
+
return new Response(JSON.stringify(out), { status: 200, headers: { "content-type": "application/json" } });
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
if (e instanceof Refusal)
|
|
186
|
+
return refusal(e);
|
|
187
|
+
throw e;
|
|
188
|
+
}
|
|
189
|
+
}),
|
|
190
|
+
handle(self = {}, build) {
|
|
191
|
+
return createMemoryHandle({
|
|
192
|
+
base,
|
|
193
|
+
headers: self.agentId ? { "x-agent-id": self.agentId } : {},
|
|
194
|
+
fetch: bank.fetch,
|
|
195
|
+
self,
|
|
196
|
+
build: build ?? { agent: (self.agentId ?? "test-agent").split(":").pop(), version: "0.0.0-test" },
|
|
155
197
|
});
|
|
156
|
-
return {};
|
|
157
198
|
},
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
199
|
+
};
|
|
200
|
+
function must(id) {
|
|
201
|
+
const r = records.get(id);
|
|
202
|
+
if (!r)
|
|
203
|
+
throw new Error(`[fake bank] no record ${id}`);
|
|
204
|
+
return r;
|
|
205
|
+
}
|
|
206
|
+
function decide(id, status, by = "0xperson") {
|
|
207
|
+
const r = must(id);
|
|
208
|
+
r.status = status;
|
|
209
|
+
r.updated_at = now();
|
|
210
|
+
if (bank.options.c3) {
|
|
211
|
+
r.reviewed_by = by;
|
|
212
|
+
r.reviewed_at = r.updated_at;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function refusal(e) {
|
|
216
|
+
return new Response(JSON.stringify({ error: { code: e.code, message: e.message, retryable: false } }), {
|
|
217
|
+
status: e.status,
|
|
218
|
+
headers: { "content-type": "application/json" },
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function connectionOf(agentId) {
|
|
222
|
+
const c = { ...(bank.options.connection ?? {}), ...(bank.options.connections?.[agentId] ?? {}) };
|
|
223
|
+
return { scopes: c.scopes ?? "*", ceiling: c.ceiling ?? "restricted", role: c.role ?? null, initiator: c.initiator ?? true };
|
|
224
|
+
}
|
|
225
|
+
/* --------------------------------- gate --------------------------------- */
|
|
226
|
+
const inScope = (c, scope) => c.scopes === "*" || c.scopes.includes(scope);
|
|
227
|
+
const visible = (c, r) => !!r && c.initiator !== false && inScope(c, r.scope) && rank(r.privacy) <= rank(c.ceiling);
|
|
228
|
+
/* -------------------------------- reads --------------------------------- */
|
|
229
|
+
function statusFilter(b) {
|
|
230
|
+
const statuses = strList(b.statuses);
|
|
231
|
+
if (statuses && statuses.length > 0) {
|
|
232
|
+
if (b.status || b.include_unaccepted)
|
|
233
|
+
throw bad("memory: send statuses, or status / include_unaccepted, not both");
|
|
234
|
+
for (const s of statuses)
|
|
235
|
+
if (!STATUSES.includes(s))
|
|
236
|
+
throw bad(`memory: unknown status "${s}"`);
|
|
237
|
+
return (r) => statuses.includes(r.status);
|
|
238
|
+
}
|
|
239
|
+
if (b.status) {
|
|
240
|
+
if (!STATUSES.includes(b.status))
|
|
241
|
+
throw bad(`memory: unknown status "${String(b.status)}"`);
|
|
242
|
+
return (r) => r.status === b.status;
|
|
243
|
+
}
|
|
244
|
+
if (b.include_unaccepted)
|
|
245
|
+
return () => true;
|
|
246
|
+
return (r) => r.status === "accepted";
|
|
247
|
+
}
|
|
248
|
+
function facets(b, list) {
|
|
249
|
+
const preds = [statusFilter(b)];
|
|
250
|
+
const types = strList(b.types);
|
|
251
|
+
if (b.type && types)
|
|
252
|
+
throw bad("memory: send type or types, not both");
|
|
253
|
+
if (types && types.length > 10)
|
|
254
|
+
throw bad(`memory: types may name at most 10 values (got ${types.length})`);
|
|
255
|
+
if (b.type)
|
|
256
|
+
preds.push((r) => r.type === b.type);
|
|
257
|
+
if (types && types.length > 0)
|
|
258
|
+
preds.push((r) => types.includes(r.type));
|
|
259
|
+
if (b.scope)
|
|
260
|
+
preds.push((r) => r.scope === b.scope);
|
|
261
|
+
if (b.privacy)
|
|
262
|
+
preds.push((r) => r.privacy === b.privacy);
|
|
263
|
+
const domains = strList(b.domains);
|
|
264
|
+
// record_search has always taken domains; record_list only with C3 (A2).
|
|
265
|
+
const domainsKnown = !list || bank.options.c3;
|
|
266
|
+
if (domains && domainsKnown) {
|
|
267
|
+
if (b.domain)
|
|
268
|
+
throw bad("memory: send domain or domains, not both");
|
|
269
|
+
if (domains.length > 20)
|
|
270
|
+
throw bad(`memory: domains may name at most 20 values (got ${domains.length})`);
|
|
271
|
+
preds.push((r) => domains.includes(r.domain));
|
|
272
|
+
}
|
|
273
|
+
if (b.domain)
|
|
274
|
+
preds.push((r) => r.domain === b.domain);
|
|
275
|
+
if (b.id_prefix !== undefined && bank.options.c3) {
|
|
276
|
+
const p = str(b.id_prefix);
|
|
277
|
+
if (p.length < 1 || p.length > 128)
|
|
278
|
+
throw bad("memory: id_prefix must be 1 to 128 characters");
|
|
279
|
+
preds.push((r) => r.id.startsWith(p));
|
|
280
|
+
}
|
|
281
|
+
return (r) => preds.every((f) => f(r));
|
|
282
|
+
}
|
|
283
|
+
function includeCols(b) {
|
|
284
|
+
const inc = strList(b.include) ?? [];
|
|
285
|
+
const known = ["body", "props", "created_at", "updated_at", "domain", "status", "version", ...(bank.options.c3 ? ["review"] : [])];
|
|
286
|
+
for (const i of inc)
|
|
287
|
+
if (!known.includes(i))
|
|
288
|
+
throw bad(`memory: include may name ${known.join(", ")} (got "${i}")`);
|
|
289
|
+
let chars = DEFAULT_BODY_CHARS;
|
|
290
|
+
if (b.body_chars !== undefined) {
|
|
291
|
+
if (!inc.includes("body"))
|
|
292
|
+
throw bad('memory: body_chars needs include ["body"]');
|
|
293
|
+
chars = Number(b.body_chars);
|
|
294
|
+
if (!Number.isInteger(chars) || chars < 1 || chars > MAX_BODY_CHARS) {
|
|
295
|
+
throw bad(`memory: body_chars must be between 1 and ${MAX_BODY_CHARS} (got ${String(b.body_chars)})`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const names = [];
|
|
299
|
+
const getters = [];
|
|
300
|
+
if (inc.includes("body")) {
|
|
301
|
+
names.push("body");
|
|
302
|
+
getters.push((r) => r.body.slice(0, chars));
|
|
303
|
+
}
|
|
304
|
+
if (inc.includes("props")) {
|
|
305
|
+
names.push("props");
|
|
306
|
+
getters.push((r) => r.props);
|
|
307
|
+
}
|
|
308
|
+
if (inc.includes("review")) {
|
|
309
|
+
names.push("reviewed_by", "reviewed_at");
|
|
310
|
+
getters.push((r) => r.reviewed_by, (r) => r.reviewed_at);
|
|
311
|
+
}
|
|
312
|
+
return { names, value: (r) => getters.map((g) => g(r)) };
|
|
313
|
+
}
|
|
314
|
+
const limitOf = (b) => {
|
|
315
|
+
const n = b.limit === undefined ? DEFAULT_LIMIT : Number(b.limit);
|
|
316
|
+
return Math.min(Math.max(Number.isFinite(n) && n > 0 ? Math.floor(n) : DEFAULT_LIMIT, 1), MAX_LIMIT);
|
|
317
|
+
};
|
|
318
|
+
function search(b, c) {
|
|
319
|
+
const match = str(b.match).trim();
|
|
320
|
+
if (!match && !b.query)
|
|
321
|
+
throw bad("memory: record_search needs match or query");
|
|
322
|
+
const terms = match.split(/\s+/).filter((t) => t && t.toUpperCase() !== "OR").map((t) => t.replace(/^"|"$/g, "").toLowerCase());
|
|
323
|
+
const any = /\sOR\s/.test(match);
|
|
324
|
+
const hits = (r) => {
|
|
325
|
+
const text = `${r.id} ${r.type} ${r.title} ${r.body}`.toLowerCase();
|
|
326
|
+
const each = terms.map((t) => (t.endsWith("*") ? text.includes(t.slice(0, -1)) : text.includes(t)));
|
|
327
|
+
return each.filter(Boolean).length;
|
|
328
|
+
};
|
|
329
|
+
const f = facets(b, false);
|
|
330
|
+
const inc = includeCols(b);
|
|
331
|
+
const rows = [...records.values()]
|
|
332
|
+
.filter((r) => visible(c, r) && f(r))
|
|
333
|
+
.map((r) => ({ r, n: hits(r) }))
|
|
334
|
+
.filter(({ n }) => (any ? n > 0 : n === terms.length))
|
|
335
|
+
.sort((a, z) => z.n - a.n || a.r.id.localeCompare(z.r.id))
|
|
336
|
+
.slice(0, limitOf(b));
|
|
337
|
+
return {
|
|
338
|
+
columns: ["id", "type", "title", "scope", "privacy", "rank", "snippet", "domain", "status", "version", ...inc.names],
|
|
339
|
+
rows: rows.map(({ r, n }) => [r.id, r.type, r.title, r.scope, r.privacy, -n, r.title, r.domain, r.status, r.version, ...inc.value(r)]),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function list(b, c) {
|
|
343
|
+
const f = facets(b, true);
|
|
344
|
+
const inc = includeCols(b);
|
|
345
|
+
let rows = [...records.values()]
|
|
346
|
+
.filter((r) => visible(c, r) && f(r))
|
|
347
|
+
.sort((a, z) => z.updated_at.localeCompare(a.updated_at) || a.id.localeCompare(z.id));
|
|
348
|
+
if (b.cursor) {
|
|
349
|
+
const [at, id] = decodeListCursor(str(b.cursor));
|
|
350
|
+
rows = rows.filter((r) => r.updated_at < at || (r.updated_at === at && r.id > id));
|
|
351
|
+
}
|
|
352
|
+
const limit = limitOf(b);
|
|
353
|
+
const page = rows.slice(0, limit);
|
|
354
|
+
const lastRow = page[page.length - 1];
|
|
355
|
+
const cursor = rows.length > limit && lastRow
|
|
356
|
+
? Buffer.from(JSON.stringify([lastRow.updated_at, lastRow.id])).toString("base64url")
|
|
357
|
+
: undefined;
|
|
358
|
+
return {
|
|
359
|
+
columns: ["id", "type", "title", "scope", "privacy", "updated_at", "domain", "status", "version", "created_at", ...inc.names],
|
|
360
|
+
rows: page.map((r) => [r.id, r.type, r.title, r.scope, r.privacy, r.updated_at, r.domain, r.status, r.version, r.created_at, ...inc.value(r)]),
|
|
361
|
+
...(cursor ? { cursor } : {}),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function get(b, c) {
|
|
365
|
+
const r = records.get(str(b.id));
|
|
366
|
+
const columns = ["id", "type", "title", "body", "scope", "privacy", "created_at", "updated_at", "domain", "status", "version", "props",
|
|
367
|
+
...(bank.options.c3 ? ["reviewed_by", "reviewed_at"] : [])];
|
|
368
|
+
if (!visible(c, r))
|
|
369
|
+
return { columns, rows: [] };
|
|
370
|
+
return {
|
|
371
|
+
columns,
|
|
372
|
+
rows: [[r.id, r.type, r.title, r.body, r.scope, r.privacy, r.created_at, r.updated_at, r.domain, r.status, r.version, r.props,
|
|
373
|
+
...(bank.options.c3 ? [r.reviewed_by, r.reviewed_at] : [])]],
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function neighbours(b, c) {
|
|
377
|
+
const id = str(b.id);
|
|
378
|
+
const dir = str(b.direction) || "both";
|
|
379
|
+
if (!["in", "out", "both"].includes(dir))
|
|
380
|
+
throw bad(`record_neighbours: direction must be one of in, out, both (got "${dir}")`);
|
|
381
|
+
const statuses = bank.options.c3 ? strList(b.statuses) : undefined;
|
|
382
|
+
const columns = ["id", "type", "title", "scope", "privacy", "type", "domain", "status", "version"];
|
|
383
|
+
if (!visible(c, records.get(id)))
|
|
384
|
+
return { columns, rows: [] };
|
|
385
|
+
const rows = [];
|
|
386
|
+
for (const e of relations) {
|
|
387
|
+
const touches = dir === "out" ? e.in_id === id : dir === "in" ? e.out_id === id : e.in_id === id || e.out_id === id;
|
|
388
|
+
if (!touches || (b.edge_type && e.type !== b.edge_type) || !visible(c, e))
|
|
389
|
+
continue;
|
|
390
|
+
const far = records.get(e.in_id === id ? e.out_id : e.in_id);
|
|
391
|
+
if (!far || !visible(c, far) || (statuses && !statuses.includes(far.status)))
|
|
392
|
+
continue;
|
|
393
|
+
rows.push([far.id, far.type, far.title, far.scope, far.privacy, e.type, far.domain, far.status, far.version]);
|
|
394
|
+
}
|
|
395
|
+
rows.sort((a, z) => String(a[0]).localeCompare(String(z[0])));
|
|
396
|
+
return { columns, rows: rows.slice(0, limitOf(b)) };
|
|
397
|
+
}
|
|
398
|
+
function recordMetrics(b, c) {
|
|
399
|
+
const ids = strList(b.ids);
|
|
400
|
+
if (ids && bank.options.c3) {
|
|
401
|
+
if (b.id)
|
|
402
|
+
throw bad("memory: send id or ids, not both");
|
|
403
|
+
if (ids.length > 50)
|
|
404
|
+
throw bad("memory: ids may name at most 50 records");
|
|
405
|
+
const rows = [];
|
|
406
|
+
for (const id of [...ids].sort()) {
|
|
407
|
+
if (!visible(c, records.get(id)))
|
|
408
|
+
continue;
|
|
409
|
+
for (const [k, v] of [...(metrics.get(id) ?? new Map())].sort())
|
|
410
|
+
rows.push([id, k, v]);
|
|
411
|
+
}
|
|
412
|
+
return { columns: ["record_id", "key", "value"], rows };
|
|
413
|
+
}
|
|
414
|
+
const id = str(b.id);
|
|
415
|
+
if (!visible(c, records.get(id)))
|
|
416
|
+
return { columns: ["key", "value"], rows: [] };
|
|
417
|
+
return { columns: ["key", "value"], rows: [...(metrics.get(id) ?? new Map())].sort() };
|
|
418
|
+
}
|
|
419
|
+
function countByType(b, c) {
|
|
420
|
+
const n = new Map();
|
|
421
|
+
for (const r of records.values()) {
|
|
422
|
+
if (!visible(c, r) || r.status !== "accepted" || (b.scope && r.scope !== b.scope) || (b.domain && r.domain !== b.domain))
|
|
423
|
+
continue;
|
|
424
|
+
n.set(r.type, (n.get(r.type) ?? 0) + 1);
|
|
425
|
+
}
|
|
426
|
+
return { columns: ["type", "n"], rows: [...n].sort() };
|
|
427
|
+
}
|
|
428
|
+
/* -------------------------------- writes -------------------------------- */
|
|
429
|
+
function writeGate(tool, b, c, scope, privacy) {
|
|
430
|
+
if (c.initiator === false)
|
|
431
|
+
throw denied(NO_INITIATOR);
|
|
432
|
+
if (!scope)
|
|
433
|
+
throw bad("memory: scope is required");
|
|
434
|
+
if (!PRIVACIES.includes(privacy)) {
|
|
435
|
+
throw bad("memory: privacy must be one of public, internal, private, restricted");
|
|
436
|
+
}
|
|
437
|
+
if (!inScope(c, scope)) {
|
|
438
|
+
const writable = c.scopes === "*" ? "every scope" : c.scopes.join(", ") || "none";
|
|
439
|
+
throw denied(`memory: not a scope you may write to: "${scope}"`
|
|
440
|
+
+ (writable === "none" ? " — you hold no writable scope in this vault" : ` — you may write to: ${writable}`));
|
|
441
|
+
}
|
|
442
|
+
if (bank.options.c3 && rank(privacy) > rank(c.ceiling)) {
|
|
443
|
+
throw new Refusal(403, "PRIVACY_ABOVE_CEILING", `memory: you may write at most ${c.ceiling} here (got ${privacy}); `
|
|
444
|
+
+ "a record above your own ceiling would be invisible to you — for an agent, the ceiling is its job initiator's");
|
|
445
|
+
}
|
|
446
|
+
if (bank.options.policy !== "curators")
|
|
447
|
+
return;
|
|
448
|
+
if (c.role === "embedder")
|
|
449
|
+
throw denied("memory: an embedder writes only record vectors (record_vec_upsert) — records, edges and metrics are other roles' writes");
|
|
450
|
+
if (c.role === "harvester") {
|
|
451
|
+
if (!["record_upsert", "relation_upsert"].includes(tool))
|
|
452
|
+
throw denied(`memory: a harvester may not call ${tool} — it files proposals and draws derived_from, supersedes, about and "
|
|
453
|
+
+ "involves edges; promoting, rewriting, reclassifying and deleting are a person's acts`);
|
|
454
|
+
if (tool === "record_upsert" && b.status && b.status !== "proposed") {
|
|
455
|
+
throw denied(`memory: a harvester writes proposals — status must be proposed or omitted (got "${String(b.status)}"); `
|
|
456
|
+
+ "a person accepts a proposal into the bank");
|
|
457
|
+
}
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (c.role === "analyzer") {
|
|
461
|
+
if (!["record_upsert", "relation_upsert", "record_metric_upsert"].includes(tool)) {
|
|
462
|
+
throw denied(`memory: an analyzer may not call ${tool} — it files proposed metric: and obs: records, draws derived_from `
|
|
463
|
+
+ "and about edges, and attaches counts to its own proposed metric: records");
|
|
464
|
+
}
|
|
465
|
+
const id = str(b.id);
|
|
466
|
+
if (tool !== "relation_upsert" && !id.startsWith("metric:") && !(tool === "record_upsert" && id.startsWith("obs:"))) {
|
|
467
|
+
throw denied(tool === "record_metric_upsert"
|
|
468
|
+
? `memory: an analyzer attaches counts only to metric: records (got id "${id}")`
|
|
469
|
+
: `memory: an analyzer writes only metric: and obs: records (got id "${id}")`);
|
|
470
|
+
}
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
throw denied(WORKFLOW_AGENT_REFUSAL);
|
|
474
|
+
}
|
|
475
|
+
function upsert(b, c) {
|
|
476
|
+
const id = str(b.id);
|
|
477
|
+
if (!id)
|
|
478
|
+
throw bad("memory: a record needs an id");
|
|
479
|
+
if (!str(b.type))
|
|
480
|
+
throw bad("memory: type is required");
|
|
481
|
+
if (id.startsWith("run:"))
|
|
482
|
+
throw new Refusal(403, "PLATFORM_RESERVED", "memory: run: records are the platform's");
|
|
483
|
+
if (b.status !== undefined && !STATUSES.includes(b.status))
|
|
484
|
+
throw bad(`memory: unknown status "${String(b.status)}"`);
|
|
485
|
+
let props;
|
|
486
|
+
if (b.props !== undefined && b.props !== null) {
|
|
487
|
+
if (typeof b.props !== "object" || Array.isArray(b.props))
|
|
488
|
+
throw bad("memory: props must be a JSON object");
|
|
489
|
+
props = JSON.stringify(b.props);
|
|
490
|
+
if (new TextEncoder().encode(props).length > MAX_PROPS_BYTES)
|
|
491
|
+
throw bad("memory: props over 16 KiB");
|
|
492
|
+
}
|
|
493
|
+
const createdAt = str(b.created_at);
|
|
494
|
+
if (createdAt) {
|
|
495
|
+
const ms = Date.parse(createdAt);
|
|
496
|
+
if (Number.isNaN(ms))
|
|
497
|
+
throw bad(`memory: created_at must be RFC 3339 (got "${createdAt}")`);
|
|
498
|
+
if (ms > Date.now() + CREATED_AT_SKEW_MS)
|
|
499
|
+
throw bad(`memory: created_at must not be in the future (got "${createdAt}")`);
|
|
500
|
+
}
|
|
501
|
+
const scope = str(b.scope);
|
|
502
|
+
const privacy = str(b.privacy);
|
|
503
|
+
writeGate("record_upsert", b, c, scope, privacy);
|
|
504
|
+
if (bank.refuseWrite?.({ id, type: str(b.type), scope, privacy }))
|
|
505
|
+
return null;
|
|
506
|
+
const prev = records.get(id);
|
|
507
|
+
const curator = bank.options.policy === "curators" && (c.role === "harvester" || c.role === "analyzer");
|
|
508
|
+
if (prev) {
|
|
509
|
+
// The conflict arm is gated: a row the writer cannot see is not touched,
|
|
510
|
+
// and the ack says nothing (the existence oracle is closed).
|
|
511
|
+
if (!visible(c, prev))
|
|
169
512
|
return null;
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
513
|
+
// The proposal builder rewrites only a row that is still proposed.
|
|
514
|
+
if (curator && prev.status !== "proposed")
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
const at = now();
|
|
518
|
+
const status = curator ? "proposed" : b.status ?? prev?.status ?? "accepted";
|
|
519
|
+
const changed = prev && prev.status !== status;
|
|
520
|
+
records.set(id, {
|
|
521
|
+
id,
|
|
522
|
+
type: str(b.type),
|
|
523
|
+
title: str(b.title),
|
|
524
|
+
body: str(b.body),
|
|
525
|
+
scope,
|
|
526
|
+
privacy: privacy,
|
|
527
|
+
created_at: createdAt || prev?.created_at || at,
|
|
528
|
+
updated_at: at,
|
|
529
|
+
// Overwritten on every write: omitting it clears it.
|
|
530
|
+
domain: str(b.domain),
|
|
531
|
+
status,
|
|
532
|
+
version: Math.max(prev?.version ?? 1, Number(b.version) || 1),
|
|
533
|
+
props: props ?? prev?.props ?? "{}",
|
|
534
|
+
// C3 §1.4: a person naming accepted/rejected stamps; any other status change clears.
|
|
535
|
+
reviewed_by: changed ? null : prev?.reviewed_by ?? null,
|
|
536
|
+
reviewed_at: changed ? null : prev?.reviewed_at ?? null,
|
|
537
|
+
});
|
|
538
|
+
// An insert above the writer's ceiling (pre-C3) lands, invisible to it.
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
function relationUpsert(b, c) {
|
|
542
|
+
const from = str(b.in);
|
|
543
|
+
const to = str(b.out);
|
|
544
|
+
const type = str(b.type);
|
|
545
|
+
if (!from || !to || !type)
|
|
546
|
+
throw bad("memory: a relation needs in, out and type");
|
|
547
|
+
if (PLATFORM_EDGES.includes(type))
|
|
548
|
+
throw new Refusal(403, "PLATFORM_RESERVED", `memory: "${type}" edges are the platform's`);
|
|
549
|
+
const scope = str(b.scope);
|
|
550
|
+
const privacy = str(b.privacy);
|
|
551
|
+
writeGate("relation_upsert", b, c, scope, privacy);
|
|
552
|
+
if (bank.options.policy === "curators") {
|
|
553
|
+
const allowed = c.role === "analyzer" ? ANALYZER_EDGES : bank.options.c3 ? HARVESTER_EDGES_C3 : HARVESTER_EDGES_PRE_C3;
|
|
554
|
+
if (!allowed.includes(type))
|
|
555
|
+
throw denied(`memory: ${c.role === "analyzer" ? "an" : "a"} ${c.role} may draw only ${andList(allowed)} edges (got "${type}")`);
|
|
556
|
+
}
|
|
557
|
+
// Both ends must be visible, or nothing is drawn — and the ack is the same.
|
|
558
|
+
if (!visible(c, records.get(from)) || !visible(c, records.get(to)))
|
|
559
|
+
return null;
|
|
560
|
+
const existing = relations.find((e) => e.in_id === from && e.out_id === to && e.type === type);
|
|
561
|
+
const at = now();
|
|
562
|
+
if (existing) {
|
|
563
|
+
Object.assign(existing, { scope, privacy, domain: str(b.domain), updated_at: at });
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
relations.push({
|
|
567
|
+
id: `rel:${relations.length + 1}`, in_id: from, out_id: to, type, scope,
|
|
568
|
+
privacy: privacy, created_at: at, updated_at: at, domain: str(b.domain),
|
|
180
569
|
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
570
|
+
}
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
function metricUpsert(b, c) {
|
|
574
|
+
const id = str(b.id);
|
|
575
|
+
const key = str(b.metric_key);
|
|
576
|
+
const value = Number(b.metric_value);
|
|
577
|
+
if (!id || !key)
|
|
578
|
+
throw bad("memory: record_metric_upsert needs id and metric_key");
|
|
579
|
+
if (!Number.isFinite(value) || Math.abs(value) > 1e15)
|
|
580
|
+
throw bad("memory: metric_value must be finite, |v| ≤ 1e15");
|
|
581
|
+
const r = records.get(id);
|
|
582
|
+
writeGate("record_metric_upsert", b, c, r?.scope ?? "", r?.privacy ?? "internal");
|
|
583
|
+
if (bank.options.policy === "curators" && c.role === "analyzer") {
|
|
584
|
+
const ok = bank.options.c3
|
|
585
|
+
? ANALYZER_KEY_RULE.test(key) && !LEDGER_KEYS.includes(key)
|
|
586
|
+
: ANALYZER_KEYS_PRE_C3.includes(key);
|
|
587
|
+
if (!ok) {
|
|
588
|
+
throw denied(bank.options.c3
|
|
589
|
+
? "memory: an analyzer metric key must be lower_snake_case, at most 63 characters, and not one of the platform's run keys "
|
|
590
|
+
+ `a2a_cost, a2a_tokens or duration_ms (got "${key}")`
|
|
591
|
+
: `memory: an analyzer may write only the keys ${ANALYZER_KEYS_PRE_C3.join(", ")} (got "${key}")`);
|
|
592
|
+
}
|
|
593
|
+
if (!visible(c, r) || r.status !== "proposed")
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
else if (!visible(c, r)) {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
const m = metrics.get(id) ?? new Map();
|
|
600
|
+
m.set(key, value);
|
|
601
|
+
metrics.set(id, m);
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
function personOnly(tool, b, c, apply) {
|
|
605
|
+
const r = records.get(str(b.id));
|
|
606
|
+
writeGate(tool, b, c, r?.scope ?? (c.scopes === "*" ? "x" : c.scopes[0] ?? ""), r?.privacy ?? "public");
|
|
607
|
+
if (r && visible(c, r))
|
|
608
|
+
apply(r);
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
function serve(tool, b, c) {
|
|
612
|
+
switch (tool) {
|
|
613
|
+
case "record_search": return search(b, c);
|
|
614
|
+
case "record_list": return list(b, c);
|
|
615
|
+
case "record_get": return get(b, c);
|
|
616
|
+
case "record_neighbours": return neighbours(b, c);
|
|
617
|
+
case "record_metrics": return recordMetrics(b, c);
|
|
618
|
+
case "record_count_by_type": return countByType(b, c);
|
|
619
|
+
case "record_upsert": return upsert(b, c);
|
|
620
|
+
case "relation_upsert": return relationUpsert(b, c);
|
|
621
|
+
case "record_metric_upsert": return metricUpsert(b, c);
|
|
622
|
+
case "record_update":
|
|
623
|
+
return personOnly(tool, b, c, (r) => {
|
|
624
|
+
if (b.title !== undefined)
|
|
625
|
+
r.title = str(b.title);
|
|
626
|
+
if (b.body !== undefined)
|
|
627
|
+
r.body = str(b.body);
|
|
628
|
+
r.updated_at = now();
|
|
629
|
+
});
|
|
630
|
+
case "record_set_privacy":
|
|
631
|
+
if (!PRIVACIES.includes(b.privacy))
|
|
632
|
+
throw bad("memory: privacy must be one of public, internal, private, restricted");
|
|
633
|
+
return personOnly(tool, b, c, (r) => { r.privacy = b.privacy; r.updated_at = now(); });
|
|
634
|
+
case "record_set_status":
|
|
635
|
+
if (!STATUSES.includes(b.status))
|
|
636
|
+
throw bad(`memory: unknown status "${String(b.status)}"`);
|
|
637
|
+
return personOnly(tool, b, c, (r) => { r.status = b.status; r.updated_at = now(); r.reviewed_by = null; r.reviewed_at = null; });
|
|
638
|
+
case "record_delete":
|
|
639
|
+
return personOnly(tool, b, c, (r) => { records.delete(r.id); });
|
|
640
|
+
}
|
|
641
|
+
throw new Refusal(404, "BAD_REQUEST", "unknown memory tool");
|
|
642
|
+
}
|
|
643
|
+
return bank;
|
|
190
644
|
}
|
|
191
645
|
//# sourceMappingURL=memory-bank.js.map
|