@hexis-ai/engram-server 0.15.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 +20 -1
- package/dist/adapters/postgres.js +202 -73
- package/dist/migrations/0009-events-type-index.d.ts +2 -0
- package/dist/migrations/0009-events-type-index.js +14 -0
- package/dist/migrations/0010-person-memories.d.ts +2 -0
- package/dist/migrations/0010-person-memories.js +34 -0
- package/dist/migrations/index.js +4 -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
|
|
@@ -10,6 +10,12 @@ export interface SqlClient {
|
|
|
10
10
|
<T = Record<string, unknown>>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T[]>;
|
|
11
11
|
/** Raw query for DDL / multi-statement strings. */
|
|
12
12
|
unsafe: (query: string) => Promise<unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* Open a transaction. Statements issued via the callback's `tx`
|
|
15
|
+
* argument commit together, or roll back together if the callback
|
|
16
|
+
* throws. Mirrors the `postgres` package's signature.
|
|
17
|
+
*/
|
|
18
|
+
begin<T>(fn: (tx: SqlClient) => Promise<T>): Promise<T>;
|
|
13
19
|
}
|
|
14
20
|
export interface PostgresAdapterOptions {
|
|
15
21
|
/** Workspace scope baked into every row. Required for multi-tenant isolation. */
|
|
@@ -20,11 +26,17 @@ export interface PostgresAdapterOptions {
|
|
|
20
26
|
* Override in tests for determinism.
|
|
21
27
|
*/
|
|
22
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;
|
|
23
34
|
}
|
|
24
35
|
export declare class PostgresAdapter implements StorageAdapter {
|
|
25
36
|
private readonly workspaceId;
|
|
26
37
|
private readonly sql;
|
|
27
38
|
private readonly newPersonId;
|
|
39
|
+
private readonly newMemoryId;
|
|
28
40
|
constructor(opts: PostgresAdapterOptions);
|
|
29
41
|
/**
|
|
30
42
|
* Apply all pending schema migrations. Safe to call repeatedly and
|
|
@@ -56,6 +68,7 @@ export declare class PostgresAdapter implements StorageAdapter {
|
|
|
56
68
|
createPerson(input: PersonCreate): Promise<PersonInfo>;
|
|
57
69
|
upsertPerson(id: string, input: PersonCreate): Promise<PersonInfo>;
|
|
58
70
|
updatePerson(id: string, patch: PersonUpdate): Promise<PersonInfo | null>;
|
|
71
|
+
deletePerson(id: string): Promise<boolean>;
|
|
59
72
|
getPerson(id: string): Promise<PersonInfo | null>;
|
|
60
73
|
getPersons(ids: string[]): Promise<PersonInfo[]>;
|
|
61
74
|
listPersons(opts: {
|
|
@@ -66,8 +79,14 @@ export declare class PostgresAdapter implements StorageAdapter {
|
|
|
66
79
|
name: string;
|
|
67
80
|
} & AliasUpsert): Promise<AliasInfo | null>;
|
|
68
81
|
listAliases(personId: string): Promise<AliasInfo[]>;
|
|
82
|
+
deleteAlias(personId: string, name: string): Promise<boolean>;
|
|
69
83
|
findAliasesByName(name: string): Promise<AliasInfo[]>;
|
|
70
84
|
upsertIdentity(ref: string, input: IdentityUpsert): Promise<IdentityInfo | null>;
|
|
71
85
|
getIdentityByRef(ref: string): Promise<IdentityInfo | null>;
|
|
72
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>;
|
|
73
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
|
|
@@ -57,63 +59,73 @@ export class PostgresAdapter {
|
|
|
57
59
|
async appendEvents(sessionId, events) {
|
|
58
60
|
if (events.length === 0)
|
|
59
61
|
return;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
SET participants = (
|
|
80
|
-
SELECT ARRAY(SELECT DISTINCT unnest(participants || ARRAY[${ev.personId}]::text[]))
|
|
81
|
-
),
|
|
82
|
-
viewable_by = (
|
|
83
|
-
SELECT ARRAY(SELECT DISTINCT unnest(viewable_by || ARRAY[${ev.personId}]::text[]))
|
|
84
|
-
)
|
|
85
|
-
WHERE workspace_id = ${this.workspaceId} AND id = ${sessionId}
|
|
62
|
+
// Wrap the whole batch in a transaction so the event log and the
|
|
63
|
+
// denormalized session row (participants[] / viewable_by[] /
|
|
64
|
+
// updated_at) can never diverge. Previously each INSERT + UPDATE
|
|
65
|
+
// landed as a separate statement; a mid-batch failure left the
|
|
66
|
+
// materialized state out of sync with the ledger with no way to
|
|
67
|
+
// reconstruct it.
|
|
68
|
+
await this.sql.begin(async (tx) => {
|
|
69
|
+
for (const ev of events) {
|
|
70
|
+
await tx `
|
|
71
|
+
INSERT INTO engram_events (workspace_id, session_id, seq, type, at, payload)
|
|
72
|
+
VALUES (
|
|
73
|
+
${this.workspaceId},
|
|
74
|
+
${sessionId},
|
|
75
|
+
${ev.seq},
|
|
76
|
+
${ev.type},
|
|
77
|
+
${ev.at},
|
|
78
|
+
${ev}
|
|
79
|
+
)
|
|
80
|
+
ON CONFLICT (workspace_id, session_id, seq) DO NOTHING
|
|
86
81
|
`;
|
|
82
|
+
// Participant events also widen the session's participants array
|
|
83
|
+
// so a one-shot listSessions can answer "who took part" without
|
|
84
|
+
// folding events at read time. viewable_by widens too
|
|
85
|
+
// (participants ⊆ viewable_by).
|
|
86
|
+
if (ev.type === "participant") {
|
|
87
|
+
await tx `
|
|
88
|
+
UPDATE engram_sessions
|
|
89
|
+
SET participants = (
|
|
90
|
+
SELECT ARRAY(SELECT DISTINCT unnest(participants || ARRAY[${ev.personId}]::text[]))
|
|
91
|
+
),
|
|
92
|
+
viewable_by = (
|
|
93
|
+
SELECT ARRAY(SELECT DISTINCT unnest(viewable_by || ARRAY[${ev.personId}]::text[]))
|
|
94
|
+
)
|
|
95
|
+
WHERE workspace_id = ${this.workspaceId} AND id = ${sessionId}
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
87
98
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
// Bump updated_at once per batch so list-by-recent-activity stays
|
|
100
|
+
// accurate. Using the latest event's `at` (not now()) keeps the
|
|
101
|
+
// semantics deterministic for back-dated batches.
|
|
102
|
+
const latest = events[events.length - 1];
|
|
103
|
+
await tx `
|
|
104
|
+
UPDATE engram_sessions
|
|
105
|
+
SET updated_at = GREATEST(updated_at, ${latest.at}::timestamptz)
|
|
106
|
+
WHERE workspace_id = ${this.workspaceId} AND id = ${sessionId}
|
|
107
|
+
`;
|
|
108
|
+
});
|
|
98
109
|
}
|
|
99
110
|
async getSession(sessionId) {
|
|
100
111
|
const rows = await this.sql `
|
|
101
|
-
SELECT id, title, channel, participants, viewable_by,
|
|
102
|
-
updated_at, status, summary, model,
|
|
103
|
-
trigger_conversation_id, trigger_event_id,
|
|
104
|
-
trigger_purpose, trigger_resume_hint
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
SELECT s.id, s.title, s.channel, s.participants, s.viewable_by,
|
|
113
|
+
s.created_at, s.updated_at, s.status, s.summary, s.model,
|
|
114
|
+
s.trigger_conversation_id, s.trigger_event_id,
|
|
115
|
+
s.trigger_purpose, s.trigger_resume_hint,
|
|
116
|
+
COALESCE(
|
|
117
|
+
(SELECT json_agg(e.payload ORDER BY e.seq)
|
|
118
|
+
FROM engram_events e
|
|
119
|
+
WHERE e.workspace_id = s.workspace_id AND e.session_id = s.id),
|
|
120
|
+
'[]'::json
|
|
121
|
+
) AS events
|
|
122
|
+
FROM engram_sessions s
|
|
123
|
+
WHERE s.workspace_id = ${this.workspaceId} AND s.id = ${sessionId}
|
|
107
124
|
LIMIT 1
|
|
108
125
|
`;
|
|
109
126
|
if (rows.length === 0)
|
|
110
127
|
return null;
|
|
111
|
-
|
|
112
|
-
SELECT payload FROM engram_events
|
|
113
|
-
WHERE workspace_id = ${this.workspaceId} AND session_id = ${sessionId}
|
|
114
|
-
ORDER BY seq
|
|
115
|
-
`;
|
|
116
|
-
return foldEvents(toSessionRow(rows[0]), events.map((e) => e.payload), new Date());
|
|
128
|
+
return foldRowWithEvents(rows[0]);
|
|
117
129
|
}
|
|
118
130
|
async updateSession(sessionId, patch) {
|
|
119
131
|
// Per-column "provided" flags drive a CASE WHEN per field: undefined
|
|
@@ -173,45 +185,78 @@ export class PostgresAdapter {
|
|
|
173
185
|
const hasTrigger = opts.has_trigger === true;
|
|
174
186
|
const noSummary = opts.no_summary === true;
|
|
175
187
|
const updatedBefore = opts.updated_before ?? null;
|
|
188
|
+
// Single query: session columns + the per-session event payload
|
|
189
|
+
// array via a correlated subquery (uses the engram_events PK index
|
|
190
|
+
// (workspace_id, session_id, seq)). Replaces the prior pattern of
|
|
191
|
+
// SELECT id + N follow-up getSession() round-trips.
|
|
176
192
|
const rows = await this.sql `
|
|
177
|
-
SELECT id
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
193
|
+
SELECT s.id, s.title, s.channel, s.participants, s.viewable_by,
|
|
194
|
+
s.created_at, s.updated_at, s.status, s.summary, s.model,
|
|
195
|
+
s.trigger_conversation_id, s.trigger_event_id,
|
|
196
|
+
s.trigger_purpose, s.trigger_resume_hint,
|
|
197
|
+
COALESCE(
|
|
198
|
+
(SELECT json_agg(e.payload ORDER BY e.seq)
|
|
199
|
+
FROM engram_events e
|
|
200
|
+
WHERE e.workspace_id = s.workspace_id AND e.session_id = s.id),
|
|
201
|
+
'[]'::json
|
|
202
|
+
) AS events
|
|
203
|
+
FROM engram_sessions s
|
|
204
|
+
WHERE s.workspace_id = ${this.workspaceId}
|
|
205
|
+
AND (${channelFilter}::text IS NULL OR s.channel = ${channelFilter}::text)
|
|
206
|
+
AND (${channelPrefix}::text IS NULL OR s.channel LIKE ${channelPrefix ? channelPrefix + "%" : null}::text)
|
|
207
|
+
AND (${statusFilter}::text IS NULL OR s.status = ${statusFilter}::text)
|
|
208
|
+
AND (${hasTrigger}::boolean = FALSE OR s.trigger_conversation_id IS NOT NULL)
|
|
209
|
+
AND (${noSummary}::boolean = FALSE OR s.summary IS NULL)
|
|
210
|
+
AND (${updatedBefore}::timestamptz IS NULL OR s.updated_at < ${updatedBefore}::timestamptz)
|
|
211
|
+
ORDER BY s.updated_at DESC
|
|
186
212
|
LIMIT ${opts.limit}
|
|
187
213
|
`;
|
|
188
|
-
|
|
189
|
-
return sessions.filter((s) => s !== null);
|
|
214
|
+
return rows.map(foldRowWithEvents);
|
|
190
215
|
}
|
|
191
216
|
async sessionsForPerson(personId, opts) {
|
|
192
217
|
const channelFilter = opts.channel ?? null;
|
|
193
218
|
const scope = opts.scope ?? "participant";
|
|
194
219
|
// Identifier (column name) can't be parameterized in tagged templates,
|
|
195
|
-
// so branch the query. Both arms are otherwise identical
|
|
220
|
+
// so branch the query. Both arms are otherwise identical and share
|
|
221
|
+
// the same single-query fold as listSessions.
|
|
196
222
|
const rows = scope === "viewable"
|
|
197
223
|
? await this.sql `
|
|
198
|
-
SELECT id
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
224
|
+
SELECT s.id, s.title, s.channel, s.participants, s.viewable_by,
|
|
225
|
+
s.created_at, s.updated_at, s.status, s.summary, s.model,
|
|
226
|
+
s.trigger_conversation_id, s.trigger_event_id,
|
|
227
|
+
s.trigger_purpose, s.trigger_resume_hint,
|
|
228
|
+
COALESCE(
|
|
229
|
+
(SELECT json_agg(e.payload ORDER BY e.seq)
|
|
230
|
+
FROM engram_events e
|
|
231
|
+
WHERE e.workspace_id = s.workspace_id AND e.session_id = s.id),
|
|
232
|
+
'[]'::json
|
|
233
|
+
) AS events
|
|
234
|
+
FROM engram_sessions s
|
|
235
|
+
WHERE s.workspace_id = ${this.workspaceId}
|
|
236
|
+
AND s.viewable_by @> ARRAY[${personId}]::text[]
|
|
237
|
+
AND (${channelFilter}::text IS NULL OR s.channel = ${channelFilter}::text)
|
|
238
|
+
ORDER BY s.created_at DESC
|
|
203
239
|
LIMIT ${opts.limit}
|
|
204
240
|
`
|
|
205
241
|
: await this.sql `
|
|
206
|
-
SELECT id
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
242
|
+
SELECT s.id, s.title, s.channel, s.participants, s.viewable_by,
|
|
243
|
+
s.created_at, s.updated_at, s.status, s.summary, s.model,
|
|
244
|
+
s.trigger_conversation_id, s.trigger_event_id,
|
|
245
|
+
s.trigger_purpose, s.trigger_resume_hint,
|
|
246
|
+
COALESCE(
|
|
247
|
+
(SELECT json_agg(e.payload ORDER BY e.seq)
|
|
248
|
+
FROM engram_events e
|
|
249
|
+
WHERE e.workspace_id = s.workspace_id AND e.session_id = s.id),
|
|
250
|
+
'[]'::json
|
|
251
|
+
) AS events
|
|
252
|
+
FROM engram_sessions s
|
|
253
|
+
WHERE s.workspace_id = ${this.workspaceId}
|
|
254
|
+
AND s.participants @> ARRAY[${personId}]::text[]
|
|
255
|
+
AND (${channelFilter}::text IS NULL OR s.channel = ${channelFilter}::text)
|
|
256
|
+
ORDER BY s.created_at DESC
|
|
211
257
|
LIMIT ${opts.limit}
|
|
212
258
|
`;
|
|
213
|
-
|
|
214
|
-
return sessions.filter((s) => s !== null);
|
|
259
|
+
return rows.map(foldRowWithEvents);
|
|
215
260
|
}
|
|
216
261
|
// --- Persons ------------------------------------------------------
|
|
217
262
|
async createPerson(input) {
|
|
@@ -261,6 +306,17 @@ export class PostgresAdapter {
|
|
|
261
306
|
return null;
|
|
262
307
|
return toPersonInfo(rows[0]);
|
|
263
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
|
+
}
|
|
264
320
|
async getPerson(id) {
|
|
265
321
|
const rows = await this.sql `
|
|
266
322
|
SELECT id, display_name, role, team, source, created_at, updated_at
|
|
@@ -369,6 +425,18 @@ export class PostgresAdapter {
|
|
|
369
425
|
`;
|
|
370
426
|
return rows.map(toAliasInfo);
|
|
371
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
|
+
}
|
|
372
440
|
async findAliasesByName(name) {
|
|
373
441
|
// The `name_lower` column is a STORED generated lower(name) backed
|
|
374
442
|
// by `idx_engram_aliases_name_lower (workspace_id, name_lower)`, so
|
|
@@ -452,6 +520,51 @@ export class PostgresAdapter {
|
|
|
452
520
|
`;
|
|
453
521
|
return rows.map(toIdentityInfo);
|
|
454
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
|
+
}
|
|
455
568
|
}
|
|
456
569
|
function toPersonInfo(r) {
|
|
457
570
|
return {
|
|
@@ -464,6 +577,11 @@ function toPersonInfo(r) {
|
|
|
464
577
|
updated_at: isoString(r.updated_at),
|
|
465
578
|
};
|
|
466
579
|
}
|
|
580
|
+
/** Convenience wrapper used by every read path: lifts the joined row
|
|
581
|
+
* into a SessionRow + events and folds in a single step. */
|
|
582
|
+
function foldRowWithEvents(r) {
|
|
583
|
+
return foldEvents(toSessionRow(r), r.events ?? [], new Date());
|
|
584
|
+
}
|
|
467
585
|
function toSessionRow(r) {
|
|
468
586
|
return {
|
|
469
587
|
id: r.id,
|
|
@@ -522,3 +640,14 @@ function toIdentityInfo(r) {
|
|
|
522
640
|
updated_at: isoString(r.updated_at),
|
|
523
641
|
};
|
|
524
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 = "0009-events-type-index";
|
|
2
|
+
export declare const sql = "\n-- Type-aware event index for callers that filter by event type\n-- (the most common is monet's conversation-repository, which fetches\n-- only `type='message'` events to materialize the chat transcript).\n--\n-- The existing PK `(workspace_id, session_id, seq)` lets PG find\n-- events for a session quickly, but it has to read every row to filter\n-- by type. This composite index trades a small write-side cost for an\n-- index-only scan on the typical \"messages for this session\" query.\n\nCREATE INDEX IF NOT EXISTS idx_engram_events_workspace_session_type\n ON engram_events (workspace_id, session_id, type);\n";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const name = "0009-events-type-index";
|
|
2
|
+
export const sql = `
|
|
3
|
+
-- Type-aware event index for callers that filter by event type
|
|
4
|
+
-- (the most common is monet's conversation-repository, which fetches
|
|
5
|
+
-- only \`type='message'\` events to materialize the chat transcript).
|
|
6
|
+
--
|
|
7
|
+
-- The existing PK \`(workspace_id, session_id, seq)\` lets PG find
|
|
8
|
+
-- events for a session quickly, but it has to read every row to filter
|
|
9
|
+
-- by type. This composite index trades a small write-side cost for an
|
|
10
|
+
-- index-only scan on the typical "messages for this session" query.
|
|
11
|
+
|
|
12
|
+
CREATE INDEX IF NOT EXISTS idx_engram_events_workspace_session_type
|
|
13
|
+
ON engram_events (workspace_id, session_id, type);
|
|
14
|
+
`;
|
|
@@ -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
|
@@ -6,6 +6,8 @@ import * as m0005 from "./0005-session-updated-at";
|
|
|
6
6
|
import * as m0006 from "./0006-auth";
|
|
7
7
|
import * as m0007 from "./0007-orgs";
|
|
8
8
|
import * as m0008 from "./0008-trigger-metadata";
|
|
9
|
+
import * as m0009 from "./0009-events-type-index";
|
|
10
|
+
import * as m0010 from "./0010-person-memories";
|
|
9
11
|
/**
|
|
10
12
|
* Schema migrations, applied in array order. Add a new file under
|
|
11
13
|
* `migrations/NNNN-<slug>.ts` exporting `name` and `sql`, then append it
|
|
@@ -22,4 +24,6 @@ export const MIGRATIONS = [
|
|
|
22
24
|
{ name: m0006.name, sql: m0006.sql },
|
|
23
25
|
{ name: m0007.name, sql: m0007.sql },
|
|
24
26
|
{ name: m0008.name, sql: m0008.sql },
|
|
27
|
+
{ name: m0009.name, sql: m0009.sql },
|
|
28
|
+
{ name: m0010.name, sql: m0010.sql },
|
|
25
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",
|