@hexis-ai/engram-server 0.16.0 → 0.17.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/adapters/memory.d.ts +13 -1
- package/dist/adapters/memory.js +54 -1
- package/dist/adapters/postgres.d.ts +14 -1
- package/dist/adapters/postgres.js +82 -1
- package/dist/migrations/0010-person-memories.d.ts +2 -0
- package/dist/migrations/0010-person-memories.js +34 -0
- package/dist/migrations/index.js +2 -0
- package/dist/openapi.js +58 -1
- package/dist/routes/persons.d.ts +13 -8
- package/dist/routes/persons.js +53 -9
- package/dist/schemas.d.ts +5 -0
- package/dist/schemas.js +9 -0
- package/dist/server.js +4 -2
- package/dist/storage.d.ts +49 -1
- package/dist/storage.js +14 -0
- package/openapi.json +208 -0
- package/package.json +2 -2
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { Session } from "@hexis-ai/engram-core";
|
|
2
|
-
import type { AliasInfo, AliasUpsert, IdentityInfo, IdentityUpsert, PersonCreate, PersonInfo, PersonUpdate, SessionEvent, SessionInit, SessionUpdate } from "@hexis-ai/engram-sdk";
|
|
2
|
+
import type { AliasInfo, AliasUpsert, IdentityInfo, IdentityUpsert, PersonCreate, PersonInfo, PersonMemory, PersonMemoryCreate, PersonUpdate, SessionEvent, SessionInit, SessionUpdate } from "@hexis-ai/engram-sdk";
|
|
3
3
|
import { type StorageAdapter } from "../storage";
|
|
4
4
|
export interface InMemoryAdapterOptions {
|
|
5
5
|
/** Override for tests. Default: `p_${random}` with 8 chars. */
|
|
6
6
|
newPersonId?: () => string;
|
|
7
|
+
/** Override for tests. Default: `mem_${random}` with 12 chars. */
|
|
8
|
+
newMemoryId?: () => string;
|
|
7
9
|
}
|
|
8
10
|
/**
|
|
9
11
|
* In-process storage adapter for tests, dev, and small single-node deploys.
|
|
@@ -16,7 +18,10 @@ export declare class InMemoryAdapter implements StorageAdapter {
|
|
|
16
18
|
private readonly aliases;
|
|
17
19
|
/** Keyed by ref (e.g. `slack:U12345`). */
|
|
18
20
|
private readonly identities;
|
|
21
|
+
/** Keyed by memory id. */
|
|
22
|
+
private readonly memories;
|
|
19
23
|
private readonly newPersonId;
|
|
24
|
+
private readonly newMemoryId;
|
|
20
25
|
constructor(opts?: InMemoryAdapterOptions);
|
|
21
26
|
createSession(init: SessionInit & {
|
|
22
27
|
id: string;
|
|
@@ -43,6 +48,7 @@ export declare class InMemoryAdapter implements StorageAdapter {
|
|
|
43
48
|
createPerson(input: PersonCreate): Promise<PersonInfo>;
|
|
44
49
|
upsertPerson(id: string, input: PersonCreate): Promise<PersonInfo>;
|
|
45
50
|
updatePerson(id: string, patch: PersonUpdate): Promise<PersonInfo | null>;
|
|
51
|
+
deletePerson(id: string): Promise<boolean>;
|
|
46
52
|
getPerson(id: string): Promise<PersonInfo | null>;
|
|
47
53
|
getPersons(ids: string[]): Promise<PersonInfo[]>;
|
|
48
54
|
listPersons(opts: {
|
|
@@ -53,8 +59,14 @@ export declare class InMemoryAdapter implements StorageAdapter {
|
|
|
53
59
|
name: string;
|
|
54
60
|
} & AliasUpsert): Promise<AliasInfo | null>;
|
|
55
61
|
listAliases(personId: string): Promise<AliasInfo[]>;
|
|
62
|
+
deleteAlias(personId: string, name: string): Promise<boolean>;
|
|
56
63
|
findAliasesByName(name: string): Promise<AliasInfo[]>;
|
|
57
64
|
upsertIdentity(ref: string, input: IdentityUpsert): Promise<IdentityInfo | null>;
|
|
58
65
|
getIdentityByRef(ref: string): Promise<IdentityInfo | null>;
|
|
59
66
|
listIdentitiesByPerson(personId: string): Promise<IdentityInfo[]>;
|
|
67
|
+
addPersonMemory(personId: string, input: PersonMemoryCreate): Promise<PersonMemory | null>;
|
|
68
|
+
listPersonMemories(personId: string, opts: {
|
|
69
|
+
limit: number;
|
|
70
|
+
}): Promise<PersonMemory[]>;
|
|
71
|
+
deletePersonMemory(memoryId: string): Promise<boolean>;
|
|
60
72
|
}
|
package/dist/adapters/memory.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { foldEvents, newPersonId } from "../storage";
|
|
1
|
+
import { foldEvents, newPersonId, newPersonMemoryId, } from "../storage";
|
|
2
2
|
import { applyPartial } from "./util";
|
|
3
3
|
/**
|
|
4
4
|
* In-process storage adapter for tests, dev, and small single-node deploys.
|
|
@@ -11,9 +11,13 @@ export class InMemoryAdapter {
|
|
|
11
11
|
aliases = new Map();
|
|
12
12
|
/** Keyed by ref (e.g. `slack:U12345`). */
|
|
13
13
|
identities = new Map();
|
|
14
|
+
/** Keyed by memory id. */
|
|
15
|
+
memories = new Map();
|
|
14
16
|
newPersonId;
|
|
17
|
+
newMemoryId;
|
|
15
18
|
constructor(opts = {}) {
|
|
16
19
|
this.newPersonId = opts.newPersonId ?? newPersonId;
|
|
20
|
+
this.newMemoryId = opts.newMemoryId ?? newPersonMemoryId;
|
|
17
21
|
}
|
|
18
22
|
// --- Sessions -----------------------------------------------------
|
|
19
23
|
async createSession(init) {
|
|
@@ -188,6 +192,27 @@ export class InMemoryAdapter {
|
|
|
188
192
|
this.persons.set(id, next);
|
|
189
193
|
return next;
|
|
190
194
|
}
|
|
195
|
+
async deletePerson(id) {
|
|
196
|
+
if (!this.persons.has(id))
|
|
197
|
+
return false;
|
|
198
|
+
this.persons.delete(id);
|
|
199
|
+
// Mirror the postgres FK cascade so behaviour is symmetric:
|
|
200
|
+
// dependent aliases / identities / memories vanish with the
|
|
201
|
+
// person.
|
|
202
|
+
for (const [key, alias] of this.aliases) {
|
|
203
|
+
if (alias.person_id === id)
|
|
204
|
+
this.aliases.delete(key);
|
|
205
|
+
}
|
|
206
|
+
for (const [ref, identity] of this.identities) {
|
|
207
|
+
if (identity.person_id === id)
|
|
208
|
+
this.identities.delete(ref);
|
|
209
|
+
}
|
|
210
|
+
for (const [memId, memory] of this.memories) {
|
|
211
|
+
if (memory.person_id === id)
|
|
212
|
+
this.memories.delete(memId);
|
|
213
|
+
}
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
191
216
|
async getPerson(id) {
|
|
192
217
|
return this.persons.get(id) ?? null;
|
|
193
218
|
}
|
|
@@ -268,6 +293,9 @@ export class InMemoryAdapter {
|
|
|
268
293
|
matches.sort((a, b) => b.last_used.localeCompare(a.last_used));
|
|
269
294
|
return matches;
|
|
270
295
|
}
|
|
296
|
+
async deleteAlias(personId, name) {
|
|
297
|
+
return this.aliases.delete(aliasKey(personId, name));
|
|
298
|
+
}
|
|
271
299
|
async findAliasesByName(name) {
|
|
272
300
|
const lower = name.toLowerCase();
|
|
273
301
|
const matches = [...this.aliases.values()].filter((a) => a.name.toLowerCase() === lower);
|
|
@@ -328,6 +356,31 @@ export class InMemoryAdapter {
|
|
|
328
356
|
matches.sort((a, b) => b.linked_at.localeCompare(a.linked_at));
|
|
329
357
|
return matches;
|
|
330
358
|
}
|
|
359
|
+
// --- Person memories ---------------------------------------------
|
|
360
|
+
async addPersonMemory(personId, input) {
|
|
361
|
+
if (!this.persons.has(personId))
|
|
362
|
+
return null;
|
|
363
|
+
const now = new Date().toISOString();
|
|
364
|
+
const memory = {
|
|
365
|
+
id: this.newMemoryId(),
|
|
366
|
+
person_id: personId,
|
|
367
|
+
content: input.content,
|
|
368
|
+
source: input.source ?? "agent",
|
|
369
|
+
source_session_id: input.source_session_id ?? null,
|
|
370
|
+
created_at: now,
|
|
371
|
+
updated_at: now,
|
|
372
|
+
};
|
|
373
|
+
this.memories.set(memory.id, memory);
|
|
374
|
+
return memory;
|
|
375
|
+
}
|
|
376
|
+
async listPersonMemories(personId, opts) {
|
|
377
|
+
const matches = [...this.memories.values()].filter((m) => m.person_id === personId);
|
|
378
|
+
matches.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
379
|
+
return matches.slice(0, opts.limit);
|
|
380
|
+
}
|
|
381
|
+
async deletePersonMemory(memoryId) {
|
|
382
|
+
return this.memories.delete(memoryId);
|
|
383
|
+
}
|
|
331
384
|
}
|
|
332
385
|
function aliasKey(personId, name) {
|
|
333
386
|
return `${personId}${name.toLowerCase()}`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Session } from "@hexis-ai/engram-core";
|
|
2
|
-
import type { AliasInfo, AliasUpsert, IdentityInfo, IdentityUpsert, PersonCreate, PersonInfo, PersonUpdate, SessionEvent, SessionInit, SessionUpdate } from "@hexis-ai/engram-sdk";
|
|
2
|
+
import type { AliasInfo, AliasUpsert, IdentityInfo, IdentityUpsert, PersonCreate, PersonInfo, PersonMemory, PersonMemoryCreate, PersonUpdate, SessionEvent, SessionInit, SessionUpdate } from "@hexis-ai/engram-sdk";
|
|
3
3
|
import { type StorageAdapter } from "../storage";
|
|
4
4
|
/**
|
|
5
5
|
* Minimal subset of `postgres` driver's tagged-template surface that this
|
|
@@ -26,11 +26,17 @@ export interface PostgresAdapterOptions {
|
|
|
26
26
|
* Override in tests for determinism.
|
|
27
27
|
*/
|
|
28
28
|
newPersonId?: () => string;
|
|
29
|
+
/**
|
|
30
|
+
* Generates ids for newly created person-memories. Default:
|
|
31
|
+
* `mem_${randomShort()}`. Override in tests for determinism.
|
|
32
|
+
*/
|
|
33
|
+
newMemoryId?: () => string;
|
|
29
34
|
}
|
|
30
35
|
export declare class PostgresAdapter implements StorageAdapter {
|
|
31
36
|
private readonly workspaceId;
|
|
32
37
|
private readonly sql;
|
|
33
38
|
private readonly newPersonId;
|
|
39
|
+
private readonly newMemoryId;
|
|
34
40
|
constructor(opts: PostgresAdapterOptions);
|
|
35
41
|
/**
|
|
36
42
|
* Apply all pending schema migrations. Safe to call repeatedly and
|
|
@@ -62,6 +68,7 @@ export declare class PostgresAdapter implements StorageAdapter {
|
|
|
62
68
|
createPerson(input: PersonCreate): Promise<PersonInfo>;
|
|
63
69
|
upsertPerson(id: string, input: PersonCreate): Promise<PersonInfo>;
|
|
64
70
|
updatePerson(id: string, patch: PersonUpdate): Promise<PersonInfo | null>;
|
|
71
|
+
deletePerson(id: string): Promise<boolean>;
|
|
65
72
|
getPerson(id: string): Promise<PersonInfo | null>;
|
|
66
73
|
getPersons(ids: string[]): Promise<PersonInfo[]>;
|
|
67
74
|
listPersons(opts: {
|
|
@@ -72,8 +79,14 @@ export declare class PostgresAdapter implements StorageAdapter {
|
|
|
72
79
|
name: string;
|
|
73
80
|
} & AliasUpsert): Promise<AliasInfo | null>;
|
|
74
81
|
listAliases(personId: string): Promise<AliasInfo[]>;
|
|
82
|
+
deleteAlias(personId: string, name: string): Promise<boolean>;
|
|
75
83
|
findAliasesByName(name: string): Promise<AliasInfo[]>;
|
|
76
84
|
upsertIdentity(ref: string, input: IdentityUpsert): Promise<IdentityInfo | null>;
|
|
77
85
|
getIdentityByRef(ref: string): Promise<IdentityInfo | null>;
|
|
78
86
|
listIdentitiesByPerson(personId: string): Promise<IdentityInfo[]>;
|
|
87
|
+
addPersonMemory(personId: string, input: PersonMemoryCreate): Promise<PersonMemory | null>;
|
|
88
|
+
listPersonMemories(personId: string, opts: {
|
|
89
|
+
limit: number;
|
|
90
|
+
}): Promise<PersonMemory[]>;
|
|
91
|
+
deletePersonMemory(memoryId: string): Promise<boolean>;
|
|
79
92
|
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { runMigrations } from "../migrator";
|
|
2
|
-
import { foldEvents, newPersonId } from "../storage";
|
|
2
|
+
import { foldEvents, newPersonId, newPersonMemoryId, } from "../storage";
|
|
3
3
|
import { isoString, pickPatch } from "./util";
|
|
4
4
|
export class PostgresAdapter {
|
|
5
5
|
workspaceId;
|
|
6
6
|
sql;
|
|
7
7
|
newPersonId;
|
|
8
|
+
newMemoryId;
|
|
8
9
|
constructor(opts) {
|
|
9
10
|
this.workspaceId = opts.workspaceId;
|
|
10
11
|
this.sql = opts.sql;
|
|
11
12
|
this.newPersonId = opts.newPersonId ?? newPersonId;
|
|
13
|
+
this.newMemoryId = opts.newMemoryId ?? newPersonMemoryId;
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* Apply all pending schema migrations. Safe to call repeatedly and
|
|
@@ -304,6 +306,17 @@ export class PostgresAdapter {
|
|
|
304
306
|
return null;
|
|
305
307
|
return toPersonInfo(rows[0]);
|
|
306
308
|
}
|
|
309
|
+
async deletePerson(id) {
|
|
310
|
+
// Aliases / identities / memories vanish via FK ON DELETE CASCADE.
|
|
311
|
+
// Sessions are not FK'd to engram_persons (participant ids are
|
|
312
|
+
// opaque text) and are intentionally left alone.
|
|
313
|
+
const rows = await this.sql `
|
|
314
|
+
DELETE FROM engram_persons
|
|
315
|
+
WHERE workspace_id = ${this.workspaceId} AND id = ${id}
|
|
316
|
+
RETURNING id
|
|
317
|
+
`;
|
|
318
|
+
return rows.length > 0;
|
|
319
|
+
}
|
|
307
320
|
async getPerson(id) {
|
|
308
321
|
const rows = await this.sql `
|
|
309
322
|
SELECT id, display_name, role, team, source, created_at, updated_at
|
|
@@ -412,6 +425,18 @@ export class PostgresAdapter {
|
|
|
412
425
|
`;
|
|
413
426
|
return rows.map(toAliasInfo);
|
|
414
427
|
}
|
|
428
|
+
async deleteAlias(personId, name) {
|
|
429
|
+
// name_lower (generated stored) keeps this index-only regardless
|
|
430
|
+
// of the input casing.
|
|
431
|
+
const rows = await this.sql `
|
|
432
|
+
DELETE FROM engram_aliases
|
|
433
|
+
WHERE workspace_id = ${this.workspaceId}
|
|
434
|
+
AND person_id = ${personId}
|
|
435
|
+
AND name_lower = ${name.toLowerCase()}
|
|
436
|
+
RETURNING name
|
|
437
|
+
`;
|
|
438
|
+
return rows.length > 0;
|
|
439
|
+
}
|
|
415
440
|
async findAliasesByName(name) {
|
|
416
441
|
// The `name_lower` column is a STORED generated lower(name) backed
|
|
417
442
|
// by `idx_engram_aliases_name_lower (workspace_id, name_lower)`, so
|
|
@@ -495,6 +520,51 @@ export class PostgresAdapter {
|
|
|
495
520
|
`;
|
|
496
521
|
return rows.map(toIdentityInfo);
|
|
497
522
|
}
|
|
523
|
+
// --- Person memories ----------------------------------------------
|
|
524
|
+
async addPersonMemory(personId, input) {
|
|
525
|
+
// Existence-check + insert in one round-trip via INSERT…SELECT.
|
|
526
|
+
// Unlike aliases/identities the adapter does NOT auto-create a
|
|
527
|
+
// stub person — memories are first-class facts about a known
|
|
528
|
+
// person; writing one for an unknown id is almost always a bug.
|
|
529
|
+
const id = this.newMemoryId();
|
|
530
|
+
const rows = await this.sql `
|
|
531
|
+
INSERT INTO engram_person_memories (
|
|
532
|
+
workspace_id, id, person_id, content, source, source_session_id
|
|
533
|
+
)
|
|
534
|
+
SELECT
|
|
535
|
+
${this.workspaceId}, ${id}, ${personId},
|
|
536
|
+
${input.content}, ${input.source ?? "agent"},
|
|
537
|
+
${input.source_session_id ?? null}
|
|
538
|
+
WHERE EXISTS (
|
|
539
|
+
SELECT 1 FROM engram_persons
|
|
540
|
+
WHERE workspace_id = ${this.workspaceId} AND id = ${personId}
|
|
541
|
+
)
|
|
542
|
+
RETURNING id, person_id, content, source, source_session_id,
|
|
543
|
+
created_at, updated_at
|
|
544
|
+
`;
|
|
545
|
+
if (rows.length === 0)
|
|
546
|
+
return null;
|
|
547
|
+
return toPersonMemory(rows[0]);
|
|
548
|
+
}
|
|
549
|
+
async listPersonMemories(personId, opts) {
|
|
550
|
+
const rows = await this.sql `
|
|
551
|
+
SELECT id, person_id, content, source, source_session_id,
|
|
552
|
+
created_at, updated_at
|
|
553
|
+
FROM engram_person_memories
|
|
554
|
+
WHERE workspace_id = ${this.workspaceId} AND person_id = ${personId}
|
|
555
|
+
ORDER BY created_at DESC
|
|
556
|
+
LIMIT ${opts.limit}
|
|
557
|
+
`;
|
|
558
|
+
return rows.map(toPersonMemory);
|
|
559
|
+
}
|
|
560
|
+
async deletePersonMemory(memoryId) {
|
|
561
|
+
const rows = await this.sql `
|
|
562
|
+
DELETE FROM engram_person_memories
|
|
563
|
+
WHERE workspace_id = ${this.workspaceId} AND id = ${memoryId}
|
|
564
|
+
RETURNING id
|
|
565
|
+
`;
|
|
566
|
+
return rows.length > 0;
|
|
567
|
+
}
|
|
498
568
|
}
|
|
499
569
|
function toPersonInfo(r) {
|
|
500
570
|
return {
|
|
@@ -570,3 +640,14 @@ function toIdentityInfo(r) {
|
|
|
570
640
|
updated_at: isoString(r.updated_at),
|
|
571
641
|
};
|
|
572
642
|
}
|
|
643
|
+
function toPersonMemory(r) {
|
|
644
|
+
return {
|
|
645
|
+
id: r.id,
|
|
646
|
+
person_id: r.person_id,
|
|
647
|
+
content: r.content,
|
|
648
|
+
source: r.source,
|
|
649
|
+
source_session_id: r.source_session_id,
|
|
650
|
+
created_at: isoString(r.created_at),
|
|
651
|
+
updated_at: isoString(r.updated_at),
|
|
652
|
+
};
|
|
653
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const name = "0010-person-memories";
|
|
2
|
+
export declare const sql = "\n-- Per-person free-form memory store. ChatGPT/Claude-style \"facts about\n-- this person\" that the host agent reads on each turn and writes via a\n-- save_person_memory tool. Workspace-scoped, soft-FK to engram_persons\n-- so a memory row cannot outlive its person.\nCREATE TABLE IF NOT EXISTS engram_person_memories (\n workspace_id TEXT NOT NULL,\n id TEXT NOT NULL,\n person_id TEXT NOT NULL,\n content TEXT NOT NULL,\n -- Where the memory came from. 'agent' = saved by the LLM via tool,\n -- 'manual' = entered by a human (engram-web UI), 'auto' = derived by\n -- a pipeline (future). Mirrors engram_persons.source so callers can\n -- filter/render provenance consistently.\n source TEXT NOT NULL DEFAULT 'agent',\n -- Session that produced this memory. Nullable because UI-entered\n -- memories have no originating session. No FK \u2014 sessions can be\n -- purged independently and we don't want a CASCADE to vaporise the\n -- memory.\n source_session_id TEXT,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n PRIMARY KEY (workspace_id, id),\n FOREIGN KEY (workspace_id, person_id)\n REFERENCES engram_persons (workspace_id, id) ON DELETE CASCADE\n);\n\n-- Hot path: \"give me this person's N most recent memories\" \u2014 fired\n-- on every agent turn for every participant. Index covers the\n-- (workspace, person) filter and the created_at desc sort.\nCREATE INDEX IF NOT EXISTS idx_engram_person_memories_person_created\n ON engram_person_memories (workspace_id, person_id, created_at DESC);\n";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const name = "0010-person-memories";
|
|
2
|
+
export const sql = `
|
|
3
|
+
-- Per-person free-form memory store. ChatGPT/Claude-style "facts about
|
|
4
|
+
-- this person" that the host agent reads on each turn and writes via a
|
|
5
|
+
-- save_person_memory tool. Workspace-scoped, soft-FK to engram_persons
|
|
6
|
+
-- so a memory row cannot outlive its person.
|
|
7
|
+
CREATE TABLE IF NOT EXISTS engram_person_memories (
|
|
8
|
+
workspace_id TEXT NOT NULL,
|
|
9
|
+
id TEXT NOT NULL,
|
|
10
|
+
person_id TEXT NOT NULL,
|
|
11
|
+
content TEXT NOT NULL,
|
|
12
|
+
-- Where the memory came from. 'agent' = saved by the LLM via tool,
|
|
13
|
+
-- 'manual' = entered by a human (engram-web UI), 'auto' = derived by
|
|
14
|
+
-- a pipeline (future). Mirrors engram_persons.source so callers can
|
|
15
|
+
-- filter/render provenance consistently.
|
|
16
|
+
source TEXT NOT NULL DEFAULT 'agent',
|
|
17
|
+
-- Session that produced this memory. Nullable because UI-entered
|
|
18
|
+
-- memories have no originating session. No FK — sessions can be
|
|
19
|
+
-- purged independently and we don't want a CASCADE to vaporise the
|
|
20
|
+
-- memory.
|
|
21
|
+
source_session_id TEXT,
|
|
22
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
23
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
24
|
+
PRIMARY KEY (workspace_id, id),
|
|
25
|
+
FOREIGN KEY (workspace_id, person_id)
|
|
26
|
+
REFERENCES engram_persons (workspace_id, id) ON DELETE CASCADE
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
-- Hot path: "give me this person's N most recent memories" — fired
|
|
30
|
+
-- on every agent turn for every participant. Index covers the
|
|
31
|
+
-- (workspace, person) filter and the created_at desc sort.
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_engram_person_memories_person_created
|
|
33
|
+
ON engram_person_memories (workspace_id, person_id, created_at DESC);
|
|
34
|
+
`;
|
package/dist/migrations/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import * as m0006 from "./0006-auth";
|
|
|
7
7
|
import * as m0007 from "./0007-orgs";
|
|
8
8
|
import * as m0008 from "./0008-trigger-metadata";
|
|
9
9
|
import * as m0009 from "./0009-events-type-index";
|
|
10
|
+
import * as m0010 from "./0010-person-memories";
|
|
10
11
|
/**
|
|
11
12
|
* Schema migrations, applied in array order. Add a new file under
|
|
12
13
|
* `migrations/NNNN-<slug>.ts` exporting `name` and `sql`, then append it
|
|
@@ -24,4 +25,5 @@ export const MIGRATIONS = [
|
|
|
24
25
|
{ name: m0007.name, sql: m0007.sql },
|
|
25
26
|
{ name: m0008.name, sql: m0008.sql },
|
|
26
27
|
{ name: m0009.name, sql: m0009.sql },
|
|
28
|
+
{ name: m0010.name, sql: m0010.sql },
|
|
27
29
|
];
|
package/dist/openapi.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* codes stay as-is.
|
|
19
19
|
*/
|
|
20
20
|
import { z } from "zod";
|
|
21
|
-
import { addMemberSchema, createOrgSchema, createWorkspaceSchema, eventBatchSchema, issueKeySchema, orgPatchSchema, personCreateSchema, personUpdateSchema, searchRequestSchema, sessionInitSchema, sessionUpdateSchema, aliasUpsertSchema, identityUpsertSchema, workspacePatchSchema, } from "./schemas";
|
|
21
|
+
import { addMemberSchema, createOrgSchema, createWorkspaceSchema, eventBatchSchema, issueKeySchema, orgPatchSchema, personCreateSchema, personMemoryCreateSchema, personUpdateSchema, searchRequestSchema, sessionInitSchema, sessionUpdateSchema, aliasUpsertSchema, identityUpsertSchema, workspacePatchSchema, } from "./schemas";
|
|
22
22
|
/** Convert a Zod schema to a JSON Schema object for an OpenAPI component. */
|
|
23
23
|
function toComponent(schema) {
|
|
24
24
|
const js = z.toJSONSchema(schema);
|
|
@@ -237,6 +237,15 @@ function buildPaths() {
|
|
|
237
237
|
"401": res("認証エラー"),
|
|
238
238
|
},
|
|
239
239
|
},
|
|
240
|
+
delete: {
|
|
241
|
+
summary: "person を hard-delete する。aliases / identities / memories は FK CASCADE で消える。session の participants 配列に残った id はそのまま(engram は participant id を opaque 文字列として扱う)。",
|
|
242
|
+
parameters: [pathParam("id", "person id。")],
|
|
243
|
+
responses: {
|
|
244
|
+
"204": res("削除完了"),
|
|
245
|
+
"404": res("person が見つからない"),
|
|
246
|
+
"401": res("認証エラー"),
|
|
247
|
+
},
|
|
248
|
+
},
|
|
240
249
|
get: {
|
|
241
250
|
summary: "単一の person を取得する。",
|
|
242
251
|
parameters: [pathParam("id", "person id。")],
|
|
@@ -290,6 +299,18 @@ function buildPaths() {
|
|
|
290
299
|
"401": res("認証エラー"),
|
|
291
300
|
},
|
|
292
301
|
},
|
|
302
|
+
delete: {
|
|
303
|
+
summary: "person の alias を削除する。name は case-insensitive。",
|
|
304
|
+
parameters: [
|
|
305
|
+
pathParam("id", "person id。"),
|
|
306
|
+
pathParam("name", "alias の名前(URL-encoded)。"),
|
|
307
|
+
],
|
|
308
|
+
responses: {
|
|
309
|
+
"204": res("削除完了"),
|
|
310
|
+
"404": res("alias が見つからない"),
|
|
311
|
+
"401": res("認証エラー"),
|
|
312
|
+
},
|
|
313
|
+
},
|
|
293
314
|
}),
|
|
294
315
|
"/v1/persons/{id}/identities": tagged("Persons", {
|
|
295
316
|
get: {
|
|
@@ -301,6 +322,41 @@ function buildPaths() {
|
|
|
301
322
|
},
|
|
302
323
|
},
|
|
303
324
|
}),
|
|
325
|
+
"/v1/persons/{id}/memories": tagged("Persons", {
|
|
326
|
+
get: {
|
|
327
|
+
summary: "この person の memory(ChatGPT/Claude 風の自由文 fact)を新しい順に取得する。harness は会話開始時に participants 全員分をここから prefetch する。",
|
|
328
|
+
parameters: [pathParam("id", "person id。"), limitParam],
|
|
329
|
+
responses: {
|
|
330
|
+
"200": res("memory 一覧"),
|
|
331
|
+
"401": res("認証エラー"),
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
post: {
|
|
335
|
+
summary: "person に memory を追記する。agent からは save_person_memory ツール経由で呼ばれる。",
|
|
336
|
+
parameters: [pathParam("id", "person id。")],
|
|
337
|
+
requestBody: jsonBody("PersonMemoryCreate"),
|
|
338
|
+
responses: {
|
|
339
|
+
"201": res("追加された memory"),
|
|
340
|
+
"400": res("リクエストボディが不正"),
|
|
341
|
+
"404": res("person が見つからない"),
|
|
342
|
+
"401": res("認証エラー"),
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
}),
|
|
346
|
+
"/v1/persons/{id}/memories/{memoryId}": tagged("Persons", {
|
|
347
|
+
delete: {
|
|
348
|
+
summary: "memory を削除する。person 削除に伴う CASCADE で消えるので、手動削除はキュレーション用途。",
|
|
349
|
+
parameters: [
|
|
350
|
+
pathParam("id", "person id。"),
|
|
351
|
+
pathParam("memoryId", "memory id。"),
|
|
352
|
+
],
|
|
353
|
+
responses: {
|
|
354
|
+
"204": res("削除完了"),
|
|
355
|
+
"404": res("memory が見つからない"),
|
|
356
|
+
"401": res("認証エラー"),
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
}),
|
|
304
360
|
"/v1/aliases": tagged("Aliases", {
|
|
305
361
|
get: {
|
|
306
362
|
summary: "ワークスペース全体で alias 名を case-insensitive に逆引き。同名の alias を持つ複数の person を `last_used` desc で返す。`persons` map も同梱。",
|
|
@@ -673,6 +729,7 @@ export function buildOpenApiDocument() {
|
|
|
673
729
|
EventBatch: toComponent(eventBatchSchema),
|
|
674
730
|
PersonCreate: toComponent(personCreateSchema),
|
|
675
731
|
PersonUpdate: toComponent(personUpdateSchema),
|
|
732
|
+
PersonMemoryCreate: toComponent(personMemoryCreateSchema),
|
|
676
733
|
AliasUpsert: toComponent(aliasUpsertSchema),
|
|
677
734
|
IdentityUpsert: toComponent(identityUpsertSchema),
|
|
678
735
|
SearchRequest: toComponent(searchRequestSchema),
|
package/dist/routes/persons.d.ts
CHANGED
|
@@ -3,13 +3,18 @@ import type { Env } from "../context";
|
|
|
3
3
|
import { type RouteConfig } from "./helpers";
|
|
4
4
|
/**
|
|
5
5
|
* Person routes. Mount under `/v1`:
|
|
6
|
-
* POST
|
|
7
|
-
* PUT
|
|
8
|
-
* PATCH
|
|
9
|
-
*
|
|
10
|
-
* GET
|
|
11
|
-
* GET
|
|
12
|
-
*
|
|
13
|
-
*
|
|
6
|
+
* POST /v1/persons create a person (server allocates the id)
|
|
7
|
+
* PUT /v1/persons/:id upsert at a host-supplied id
|
|
8
|
+
* PATCH /v1/persons/:id patch profile fields
|
|
9
|
+
* DELETE /v1/persons/:id hard-delete (cascades to aliases / identities / memories)
|
|
10
|
+
* GET /v1/persons/:id fetch one person
|
|
11
|
+
* GET /v1/persons list / free-text search persons
|
|
12
|
+
* GET /v1/persons/:id/sessions sessions this person participates in / can view
|
|
13
|
+
* PUT /v1/persons/:id/aliases/:name upsert an alias for this person
|
|
14
|
+
* DELETE /v1/persons/:id/aliases/:name remove an alias (case-insensitive)
|
|
15
|
+
* GET /v1/persons/:id/aliases list aliases for this person (newest-used-first)
|
|
16
|
+
* GET /v1/persons/:id/memories list this person's memories (newest-first)
|
|
17
|
+
* POST /v1/persons/:id/memories append a memory
|
|
18
|
+
* DELETE /v1/persons/:id/memories/:mid remove a memory
|
|
14
19
|
*/
|
|
15
20
|
export declare function personsRoutes(cfg: RouteConfig): Hono<Env>;
|
package/dist/routes/persons.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import { aliasUpsertSchema, parseJsonBody, personCreateSchema, personUpdateSchema, } from "../schemas";
|
|
2
|
+
import { aliasUpsertSchema, parseJsonBody, personCreateSchema, personMemoryCreateSchema, personUpdateSchema, } from "../schemas";
|
|
3
3
|
import { clampLimit, resolvePersonMap } from "./helpers";
|
|
4
4
|
/**
|
|
5
5
|
* Person routes. Mount under `/v1`:
|
|
6
|
-
* POST
|
|
7
|
-
* PUT
|
|
8
|
-
* PATCH
|
|
9
|
-
*
|
|
10
|
-
* GET
|
|
11
|
-
* GET
|
|
12
|
-
*
|
|
13
|
-
*
|
|
6
|
+
* POST /v1/persons create a person (server allocates the id)
|
|
7
|
+
* PUT /v1/persons/:id upsert at a host-supplied id
|
|
8
|
+
* PATCH /v1/persons/:id patch profile fields
|
|
9
|
+
* DELETE /v1/persons/:id hard-delete (cascades to aliases / identities / memories)
|
|
10
|
+
* GET /v1/persons/:id fetch one person
|
|
11
|
+
* GET /v1/persons list / free-text search persons
|
|
12
|
+
* GET /v1/persons/:id/sessions sessions this person participates in / can view
|
|
13
|
+
* PUT /v1/persons/:id/aliases/:name upsert an alias for this person
|
|
14
|
+
* DELETE /v1/persons/:id/aliases/:name remove an alias (case-insensitive)
|
|
15
|
+
* GET /v1/persons/:id/aliases list aliases for this person (newest-used-first)
|
|
16
|
+
* GET /v1/persons/:id/memories list this person's memories (newest-first)
|
|
17
|
+
* POST /v1/persons/:id/memories append a memory
|
|
18
|
+
* DELETE /v1/persons/:id/memories/:mid remove a memory
|
|
14
19
|
*/
|
|
15
20
|
export function personsRoutes(cfg) {
|
|
16
21
|
const app = new Hono();
|
|
@@ -39,6 +44,13 @@ export function personsRoutes(cfg) {
|
|
|
39
44
|
return c.json({ error: "person_not_found" }, 404);
|
|
40
45
|
return c.json(p);
|
|
41
46
|
});
|
|
47
|
+
app.delete("/persons/:id", async (c) => {
|
|
48
|
+
const id = c.req.param("id");
|
|
49
|
+
const removed = await c.var.ctx.storage.deletePerson(id);
|
|
50
|
+
if (!removed)
|
|
51
|
+
return c.json({ error: "person_not_found" }, 404);
|
|
52
|
+
return c.body(null, 204);
|
|
53
|
+
});
|
|
42
54
|
app.get("/persons/:id", async (c) => {
|
|
43
55
|
const id = c.req.param("id");
|
|
44
56
|
const p = await c.var.ctx.storage.getPerson(id);
|
|
@@ -68,6 +80,14 @@ export function personsRoutes(cfg) {
|
|
|
68
80
|
const aliases = await c.var.ctx.storage.listAliases(id);
|
|
69
81
|
return c.json({ aliases });
|
|
70
82
|
});
|
|
83
|
+
app.delete("/persons/:id/aliases/:name", async (c) => {
|
|
84
|
+
const id = c.req.param("id");
|
|
85
|
+
const name = c.req.param("name");
|
|
86
|
+
const removed = await c.var.ctx.storage.deleteAlias(id, name);
|
|
87
|
+
if (!removed)
|
|
88
|
+
return c.json({ error: "alias_not_found" }, 404);
|
|
89
|
+
return c.body(null, 204);
|
|
90
|
+
});
|
|
71
91
|
app.get("/persons/:id/identities", async (c) => {
|
|
72
92
|
const id = c.req.param("id");
|
|
73
93
|
const identities = await c.var.ctx.storage.listIdentitiesByPerson(id);
|
|
@@ -86,5 +106,29 @@ export function personsRoutes(cfg) {
|
|
|
86
106
|
const persons = await resolvePersonMap(c.var.ctx.storage, sessions);
|
|
87
107
|
return c.json({ sessions, persons });
|
|
88
108
|
});
|
|
109
|
+
// --- Person memories -------------------------------------------------
|
|
110
|
+
app.get("/persons/:id/memories", async (c) => {
|
|
111
|
+
const id = c.req.param("id");
|
|
112
|
+
const limit = clampLimit(c, cfg.defaultListLimit, cfg.maxListLimit);
|
|
113
|
+
const memories = await c.var.ctx.storage.listPersonMemories(id, { limit });
|
|
114
|
+
return c.json({ memories });
|
|
115
|
+
});
|
|
116
|
+
app.post("/persons/:id/memories", async (c) => {
|
|
117
|
+
const id = c.req.param("id");
|
|
118
|
+
const body = await parseJsonBody(c, personMemoryCreateSchema);
|
|
119
|
+
if (body instanceof Response)
|
|
120
|
+
return body;
|
|
121
|
+
const memory = await c.var.ctx.storage.addPersonMemory(id, body);
|
|
122
|
+
if (!memory)
|
|
123
|
+
return c.json({ error: "person_not_found" }, 404);
|
|
124
|
+
return c.json(memory, 201);
|
|
125
|
+
});
|
|
126
|
+
app.delete("/persons/:id/memories/:memoryId", async (c) => {
|
|
127
|
+
const memoryId = c.req.param("memoryId");
|
|
128
|
+
const removed = await c.var.ctx.storage.deletePersonMemory(memoryId);
|
|
129
|
+
if (!removed)
|
|
130
|
+
return c.json({ error: "memory_not_found" }, 404);
|
|
131
|
+
return c.body(null, 204);
|
|
132
|
+
});
|
|
89
133
|
return app;
|
|
90
134
|
}
|
package/dist/schemas.d.ts
CHANGED
|
@@ -131,6 +131,11 @@ export declare const aliasUpsertSchema: z.ZodObject<{
|
|
|
131
131
|
last_used: z.ZodString;
|
|
132
132
|
increment: z.ZodOptional<z.ZodBoolean>;
|
|
133
133
|
}, z.core.$strip>;
|
|
134
|
+
export declare const personMemoryCreateSchema: z.ZodObject<{
|
|
135
|
+
content: z.ZodString;
|
|
136
|
+
source: z.ZodOptional<z.ZodString>;
|
|
137
|
+
source_session_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
138
|
+
}, z.core.$strip>;
|
|
134
139
|
export declare const identityUpsertSchema: z.ZodObject<{
|
|
135
140
|
person_id: z.ZodString;
|
|
136
141
|
service: z.ZodString;
|
package/dist/schemas.js
CHANGED
|
@@ -114,6 +114,15 @@ export const aliasUpsertSchema = z.object({
|
|
|
114
114
|
.regex(/^\d{4}-\d{2}-\d{2}/, "expected YYYY-MM-DD"),
|
|
115
115
|
increment: z.boolean().optional(),
|
|
116
116
|
});
|
|
117
|
+
// --- Person memories --------------------------------------------------
|
|
118
|
+
export const personMemoryCreateSchema = z.object({
|
|
119
|
+
// Free-form fact about the person. Min(1) rejects empty strings —
|
|
120
|
+
// memories that say nothing are spam noise the engram-web UI would
|
|
121
|
+
// have to filter anyway.
|
|
122
|
+
content: z.string().min(1),
|
|
123
|
+
source: z.string().optional(),
|
|
124
|
+
source_session_id: z.string().nullable().optional(),
|
|
125
|
+
});
|
|
117
126
|
// --- Identities -------------------------------------------------------
|
|
118
127
|
export const identityUpsertSchema = z.object({
|
|
119
128
|
person_id: z.string().min(1),
|
package/dist/server.js
CHANGED
|
@@ -83,13 +83,15 @@ export function createServer(opts) {
|
|
|
83
83
|
sessionEvents: "GET /v1/sessions/:id/events",
|
|
84
84
|
search: "POST /v1/search",
|
|
85
85
|
persons: "POST/GET /v1/persons",
|
|
86
|
-
personById: "GET/PUT/PATCH /v1/persons/:id",
|
|
86
|
+
personById: "GET/PUT/PATCH/DELETE /v1/persons/:id",
|
|
87
87
|
personSessions: "GET /v1/persons/:id/sessions",
|
|
88
88
|
personAliases: "GET /v1/persons/:id/aliases",
|
|
89
|
-
upsertAlias: "PUT /v1/persons/:id/aliases/:name",
|
|
89
|
+
upsertAlias: "PUT/DELETE /v1/persons/:id/aliases/:name",
|
|
90
90
|
findAliasesByName: "GET /v1/aliases?name=…",
|
|
91
91
|
identityByRef: "GET/PUT /v1/identities/:ref",
|
|
92
92
|
personIdentities: "GET /v1/persons/:id/identities",
|
|
93
|
+
personMemories: "GET/POST /v1/persons/:id/memories",
|
|
94
|
+
deletePersonMemory: "DELETE /v1/persons/:id/memories/:mid",
|
|
93
95
|
},
|
|
94
96
|
}));
|
|
95
97
|
app.get("/healthz", (c) => c.json({ ok: true }));
|
package/dist/storage.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Session } from "@hexis-ai/engram-core";
|
|
2
|
-
import type { AliasInfo, AliasUpsert, IdentityInfo, IdentityUpsert, PersonCreate, PersonInfo, PersonUpdate, SessionEvent, SessionInit, SessionUpdate } from "@hexis-ai/engram-sdk";
|
|
2
|
+
import type { AliasInfo, AliasUpsert, IdentityInfo, IdentityUpsert, PersonCreate, PersonInfo, PersonMemory, PersonMemoryCreate, PersonUpdate, SessionEvent, SessionInit, SessionUpdate } from "@hexis-ai/engram-sdk";
|
|
3
3
|
/**
|
|
4
4
|
* Storage adapter interface. Each implementation owns persistence for
|
|
5
5
|
* a single workspace's sessions and persons. Multi-tenancy is the host's
|
|
@@ -67,6 +67,18 @@ export interface StorageAdapter {
|
|
|
67
67
|
upsertPerson(id: string, input: PersonCreate): Promise<PersonInfo>;
|
|
68
68
|
/** Update profile fields. Returns the updated record. */
|
|
69
69
|
updatePerson(id: string, patch: PersonUpdate): Promise<PersonInfo | null>;
|
|
70
|
+
/**
|
|
71
|
+
* Hard-delete a person. Cascades to engram_aliases, engram_identities,
|
|
72
|
+
* and engram_person_memories via FK ON DELETE CASCADE. Returns `false`
|
|
73
|
+
* when the id doesn't exist in this workspace (idempotent — second
|
|
74
|
+
* call returns false but doesn't throw).
|
|
75
|
+
*
|
|
76
|
+
* Sessions are left intact: `participants` / `viewable_by` arrays may
|
|
77
|
+
* still reference the deleted id. Hosts that need to scrub references
|
|
78
|
+
* can do so via their own batch pass — engram treats a participant id
|
|
79
|
+
* as opaque text, not an FK.
|
|
80
|
+
*/
|
|
81
|
+
deletePerson(id: string): Promise<boolean>;
|
|
70
82
|
getPerson(id: string): Promise<PersonInfo | null>;
|
|
71
83
|
/** Batch fetch — used by response envelopes to inline `persons` maps. */
|
|
72
84
|
getPersons(ids: string[]): Promise<PersonInfo[]>;
|
|
@@ -106,6 +118,13 @@ export interface StorageAdapter {
|
|
|
106
118
|
} & AliasUpsert): Promise<AliasInfo | null>;
|
|
107
119
|
/** A person's aliases, ordered newest-used-first. */
|
|
108
120
|
listAliases(personId: string): Promise<AliasInfo[]>;
|
|
121
|
+
/**
|
|
122
|
+
* Delete one alias (`person_id`, `name` case-insensitively).
|
|
123
|
+
* Returns `false` when no row matched — both "person has no such
|
|
124
|
+
* alias" and "person doesn't exist" collapse into the same boolean
|
|
125
|
+
* because callers (curation UI, host cleanup) treat both as no-ops.
|
|
126
|
+
*/
|
|
127
|
+
deleteAlias(personId: string, name: string): Promise<boolean>;
|
|
109
128
|
/**
|
|
110
129
|
* Workspace-wide lookup by alias name (case-insensitive). Returns
|
|
111
130
|
* every row whose `lower(name)` matches `lower(name)` across all
|
|
@@ -129,6 +148,28 @@ export interface StorageAdapter {
|
|
|
129
148
|
getIdentityByRef(ref: string): Promise<IdentityInfo | null>;
|
|
130
149
|
/** All identities for a person, ordered newest-linked-first. */
|
|
131
150
|
listIdentitiesByPerson(personId: string): Promise<IdentityInfo[]>;
|
|
151
|
+
/**
|
|
152
|
+
* Append a free-form memory to a person. Returns `null` when the
|
|
153
|
+
* person doesn't exist — unlike aliases/identities, memories do not
|
|
154
|
+
* auto-create a stub person, because writing a memory for a
|
|
155
|
+
* non-existent id is almost always a programming mistake, not a
|
|
156
|
+
* race.
|
|
157
|
+
*/
|
|
158
|
+
addPersonMemory(personId: string, input: PersonMemoryCreate): Promise<PersonMemory | null>;
|
|
159
|
+
/**
|
|
160
|
+
* A person's recent memories, ordered newest-first. `limit` caps the
|
|
161
|
+
* fetched window — the agent harness uses this on every turn so the
|
|
162
|
+
* adapter must keep it cheap.
|
|
163
|
+
*/
|
|
164
|
+
listPersonMemories(personId: string, opts: {
|
|
165
|
+
limit: number;
|
|
166
|
+
}): Promise<PersonMemory[]>;
|
|
167
|
+
/**
|
|
168
|
+
* Remove a single memory by id. Returns `false` when the row is not
|
|
169
|
+
* found in this workspace (idempotent — callers can ignore the
|
|
170
|
+
* boolean if they don't care whether the row existed).
|
|
171
|
+
*/
|
|
172
|
+
deletePersonMemory(memoryId: string): Promise<boolean>;
|
|
132
173
|
}
|
|
133
174
|
/**
|
|
134
175
|
* Pure fold of an event log into the parts a Session needs. Used by adapters
|
|
@@ -157,3 +198,10 @@ export declare function foldEvents(row: SessionRow, events: SessionEvent[], now:
|
|
|
157
198
|
* shape and entropy stay aligned.
|
|
158
199
|
*/
|
|
159
200
|
export declare function newPersonId(): string;
|
|
201
|
+
/**
|
|
202
|
+
* Allocate a fresh person-memory id (`mem_` + 12 alphanumeric chars).
|
|
203
|
+
* Wider than person ids because memories accumulate fast — twelve
|
|
204
|
+
* chars buys ~62 bits of entropy, plenty for the foreseeable lifetime
|
|
205
|
+
* of a workspace.
|
|
206
|
+
*/
|
|
207
|
+
export declare function newPersonMemoryId(): string;
|
package/dist/storage.js
CHANGED
|
@@ -58,3 +58,17 @@ export function newPersonId() {
|
|
|
58
58
|
out += PERSON_ID_ALPHA[b % PERSON_ID_ALPHA.length];
|
|
59
59
|
return out;
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Allocate a fresh person-memory id (`mem_` + 12 alphanumeric chars).
|
|
63
|
+
* Wider than person ids because memories accumulate fast — twelve
|
|
64
|
+
* chars buys ~62 bits of entropy, plenty for the foreseeable lifetime
|
|
65
|
+
* of a workspace.
|
|
66
|
+
*/
|
|
67
|
+
export function newPersonMemoryId() {
|
|
68
|
+
const buf = new Uint8Array(12);
|
|
69
|
+
crypto.getRandomValues(buf);
|
|
70
|
+
let out = "mem_";
|
|
71
|
+
for (const b of buf)
|
|
72
|
+
out += PERSON_ID_ALPHA[b % PERSON_ID_ALPHA.length];
|
|
73
|
+
return out;
|
|
74
|
+
}
|
package/openapi.json
CHANGED
|
@@ -524,6 +524,32 @@
|
|
|
524
524
|
},
|
|
525
525
|
"additionalProperties": false
|
|
526
526
|
},
|
|
527
|
+
"PersonMemoryCreate": {
|
|
528
|
+
"type": "object",
|
|
529
|
+
"properties": {
|
|
530
|
+
"content": {
|
|
531
|
+
"type": "string",
|
|
532
|
+
"minLength": 1
|
|
533
|
+
},
|
|
534
|
+
"source": {
|
|
535
|
+
"type": "string"
|
|
536
|
+
},
|
|
537
|
+
"source_session_id": {
|
|
538
|
+
"anyOf": [
|
|
539
|
+
{
|
|
540
|
+
"type": "string"
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
"type": "null"
|
|
544
|
+
}
|
|
545
|
+
]
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
"required": [
|
|
549
|
+
"content"
|
|
550
|
+
],
|
|
551
|
+
"additionalProperties": false
|
|
552
|
+
},
|
|
527
553
|
"AliasUpsert": {
|
|
528
554
|
"type": "object",
|
|
529
555
|
"properties": {
|
|
@@ -1225,6 +1251,34 @@
|
|
|
1225
1251
|
"Persons"
|
|
1226
1252
|
]
|
|
1227
1253
|
},
|
|
1254
|
+
"delete": {
|
|
1255
|
+
"summary": "person を hard-delete する。aliases / identities / memories は FK CASCADE で消える。session の participants 配列に残った id はそのまま(engram は participant id を opaque 文字列として扱う)。",
|
|
1256
|
+
"parameters": [
|
|
1257
|
+
{
|
|
1258
|
+
"name": "id",
|
|
1259
|
+
"in": "path",
|
|
1260
|
+
"required": true,
|
|
1261
|
+
"schema": {
|
|
1262
|
+
"type": "string"
|
|
1263
|
+
},
|
|
1264
|
+
"description": "person id。"
|
|
1265
|
+
}
|
|
1266
|
+
],
|
|
1267
|
+
"responses": {
|
|
1268
|
+
"204": {
|
|
1269
|
+
"description": "削除完了"
|
|
1270
|
+
},
|
|
1271
|
+
"401": {
|
|
1272
|
+
"description": "認証エラー"
|
|
1273
|
+
},
|
|
1274
|
+
"404": {
|
|
1275
|
+
"description": "person が見つからない"
|
|
1276
|
+
}
|
|
1277
|
+
},
|
|
1278
|
+
"tags": [
|
|
1279
|
+
"Persons"
|
|
1280
|
+
]
|
|
1281
|
+
},
|
|
1228
1282
|
"get": {
|
|
1229
1283
|
"summary": "単一の person を取得する。",
|
|
1230
1284
|
"parameters": [
|
|
@@ -1390,6 +1444,43 @@
|
|
|
1390
1444
|
"tags": [
|
|
1391
1445
|
"Persons"
|
|
1392
1446
|
]
|
|
1447
|
+
},
|
|
1448
|
+
"delete": {
|
|
1449
|
+
"summary": "person の alias を削除する。name は case-insensitive。",
|
|
1450
|
+
"parameters": [
|
|
1451
|
+
{
|
|
1452
|
+
"name": "id",
|
|
1453
|
+
"in": "path",
|
|
1454
|
+
"required": true,
|
|
1455
|
+
"schema": {
|
|
1456
|
+
"type": "string"
|
|
1457
|
+
},
|
|
1458
|
+
"description": "person id。"
|
|
1459
|
+
},
|
|
1460
|
+
{
|
|
1461
|
+
"name": "name",
|
|
1462
|
+
"in": "path",
|
|
1463
|
+
"required": true,
|
|
1464
|
+
"schema": {
|
|
1465
|
+
"type": "string"
|
|
1466
|
+
},
|
|
1467
|
+
"description": "alias の名前(URL-encoded)。"
|
|
1468
|
+
}
|
|
1469
|
+
],
|
|
1470
|
+
"responses": {
|
|
1471
|
+
"204": {
|
|
1472
|
+
"description": "削除完了"
|
|
1473
|
+
},
|
|
1474
|
+
"401": {
|
|
1475
|
+
"description": "認証エラー"
|
|
1476
|
+
},
|
|
1477
|
+
"404": {
|
|
1478
|
+
"description": "alias が見つからない"
|
|
1479
|
+
}
|
|
1480
|
+
},
|
|
1481
|
+
"tags": [
|
|
1482
|
+
"Persons"
|
|
1483
|
+
]
|
|
1393
1484
|
}
|
|
1394
1485
|
},
|
|
1395
1486
|
"/v1/persons/{id}/identities": {
|
|
@@ -1419,6 +1510,123 @@
|
|
|
1419
1510
|
]
|
|
1420
1511
|
}
|
|
1421
1512
|
},
|
|
1513
|
+
"/v1/persons/{id}/memories": {
|
|
1514
|
+
"get": {
|
|
1515
|
+
"summary": "この person の memory(ChatGPT/Claude 風の自由文 fact)を新しい順に取得する。harness は会話開始時に participants 全員分をここから prefetch する。",
|
|
1516
|
+
"parameters": [
|
|
1517
|
+
{
|
|
1518
|
+
"name": "id",
|
|
1519
|
+
"in": "path",
|
|
1520
|
+
"required": true,
|
|
1521
|
+
"schema": {
|
|
1522
|
+
"type": "string"
|
|
1523
|
+
},
|
|
1524
|
+
"description": "person id。"
|
|
1525
|
+
},
|
|
1526
|
+
{
|
|
1527
|
+
"name": "limit",
|
|
1528
|
+
"in": "query",
|
|
1529
|
+
"required": false,
|
|
1530
|
+
"schema": {
|
|
1531
|
+
"type": "integer",
|
|
1532
|
+
"minimum": 1
|
|
1533
|
+
},
|
|
1534
|
+
"description": "ページサイズ。サーバー側で上限が適用される。"
|
|
1535
|
+
}
|
|
1536
|
+
],
|
|
1537
|
+
"responses": {
|
|
1538
|
+
"200": {
|
|
1539
|
+
"description": "memory 一覧"
|
|
1540
|
+
},
|
|
1541
|
+
"401": {
|
|
1542
|
+
"description": "認証エラー"
|
|
1543
|
+
}
|
|
1544
|
+
},
|
|
1545
|
+
"tags": [
|
|
1546
|
+
"Persons"
|
|
1547
|
+
]
|
|
1548
|
+
},
|
|
1549
|
+
"post": {
|
|
1550
|
+
"summary": "person に memory を追記する。agent からは save_person_memory ツール経由で呼ばれる。",
|
|
1551
|
+
"parameters": [
|
|
1552
|
+
{
|
|
1553
|
+
"name": "id",
|
|
1554
|
+
"in": "path",
|
|
1555
|
+
"required": true,
|
|
1556
|
+
"schema": {
|
|
1557
|
+
"type": "string"
|
|
1558
|
+
},
|
|
1559
|
+
"description": "person id。"
|
|
1560
|
+
}
|
|
1561
|
+
],
|
|
1562
|
+
"requestBody": {
|
|
1563
|
+
"required": true,
|
|
1564
|
+
"content": {
|
|
1565
|
+
"application/json": {
|
|
1566
|
+
"schema": {
|
|
1567
|
+
"$ref": "#/components/schemas/PersonMemoryCreate"
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
},
|
|
1572
|
+
"responses": {
|
|
1573
|
+
"201": {
|
|
1574
|
+
"description": "追加された memory"
|
|
1575
|
+
},
|
|
1576
|
+
"400": {
|
|
1577
|
+
"description": "リクエストボディが不正"
|
|
1578
|
+
},
|
|
1579
|
+
"401": {
|
|
1580
|
+
"description": "認証エラー"
|
|
1581
|
+
},
|
|
1582
|
+
"404": {
|
|
1583
|
+
"description": "person が見つからない"
|
|
1584
|
+
}
|
|
1585
|
+
},
|
|
1586
|
+
"tags": [
|
|
1587
|
+
"Persons"
|
|
1588
|
+
]
|
|
1589
|
+
}
|
|
1590
|
+
},
|
|
1591
|
+
"/v1/persons/{id}/memories/{memoryId}": {
|
|
1592
|
+
"delete": {
|
|
1593
|
+
"summary": "memory を削除する。person 削除に伴う CASCADE で消えるので、手動削除はキュレーション用途。",
|
|
1594
|
+
"parameters": [
|
|
1595
|
+
{
|
|
1596
|
+
"name": "id",
|
|
1597
|
+
"in": "path",
|
|
1598
|
+
"required": true,
|
|
1599
|
+
"schema": {
|
|
1600
|
+
"type": "string"
|
|
1601
|
+
},
|
|
1602
|
+
"description": "person id。"
|
|
1603
|
+
},
|
|
1604
|
+
{
|
|
1605
|
+
"name": "memoryId",
|
|
1606
|
+
"in": "path",
|
|
1607
|
+
"required": true,
|
|
1608
|
+
"schema": {
|
|
1609
|
+
"type": "string"
|
|
1610
|
+
},
|
|
1611
|
+
"description": "memory id。"
|
|
1612
|
+
}
|
|
1613
|
+
],
|
|
1614
|
+
"responses": {
|
|
1615
|
+
"204": {
|
|
1616
|
+
"description": "削除完了"
|
|
1617
|
+
},
|
|
1618
|
+
"401": {
|
|
1619
|
+
"description": "認証エラー"
|
|
1620
|
+
},
|
|
1621
|
+
"404": {
|
|
1622
|
+
"description": "memory が見つからない"
|
|
1623
|
+
}
|
|
1624
|
+
},
|
|
1625
|
+
"tags": [
|
|
1626
|
+
"Persons"
|
|
1627
|
+
]
|
|
1628
|
+
}
|
|
1629
|
+
},
|
|
1422
1630
|
"/v1/aliases": {
|
|
1423
1631
|
"get": {
|
|
1424
1632
|
"summary": "ワークスペース全体で alias 名を case-insensitive に逆引き。同名の alias を持つ複数の person を `last_used` desc で返す。`persons` map も同梱。",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hexis-ai/engram-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Engram server: ingest agent session events, persist via a pluggable adapter, expose search.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"engram",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@hexis-ai/engram-core": "^0.3.0",
|
|
53
|
-
"@hexis-ai/engram-sdk": "^0.
|
|
53
|
+
"@hexis-ai/engram-sdk": "^0.17.0",
|
|
54
54
|
"better-auth": "^1.6.11",
|
|
55
55
|
"hono": "^4.6.0",
|
|
56
56
|
"pg": "^8.13.0",
|