@hexis-ai/engram-server 0.7.0 → 0.8.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 +2 -1
- package/dist/adapters/memory.js +60 -0
- package/dist/adapters/postgres.d.ts +2 -1
- package/dist/adapters/postgres.js +71 -13
- package/dist/migrations/0004-schema-completion.d.ts +2 -0
- package/dist/migrations/0004-schema-completion.js +53 -0
- package/dist/migrations/index.js +2 -0
- package/dist/openapi.js +11 -0
- package/dist/routes/sessions.d.ts +6 -5
- package/dist/routes/sessions.js +18 -6
- package/dist/schemas.d.ts +22 -0
- package/dist/schemas.js +14 -0
- package/dist/server.js +1 -1
- package/dist/storage.d.ts +13 -1
- package/dist/storage.js +10 -0
- package/openapi.json +89 -0
- package/package.json +2 -2
|
@@ -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 } from "@hexis-ai/engram-sdk";
|
|
2
|
+
import type { AliasInfo, AliasUpsert, IdentityInfo, IdentityUpsert, PersonCreate, PersonInfo, 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. */
|
|
@@ -24,6 +24,7 @@ export declare class InMemoryAdapter implements StorageAdapter {
|
|
|
24
24
|
}): Promise<void>;
|
|
25
25
|
appendEvents(sessionId: string, events: SessionEvent[]): Promise<void>;
|
|
26
26
|
getSession(sessionId: string): Promise<Session | null>;
|
|
27
|
+
updateSession(sessionId: string, patch: SessionUpdate): Promise<Session | null>;
|
|
27
28
|
getSessionEvents(sessionId: string): Promise<SessionEvent[] | null>;
|
|
28
29
|
listSessions(opts: {
|
|
29
30
|
limit: number;
|
package/dist/adapters/memory.js
CHANGED
|
@@ -37,6 +37,16 @@ export class InMemoryAdapter {
|
|
|
37
37
|
participants,
|
|
38
38
|
viewable_by,
|
|
39
39
|
createdAt: init.createdAt,
|
|
40
|
+
// status defaults to 'active' to match the Postgres column default.
|
|
41
|
+
status: init.status ?? "active",
|
|
42
|
+
...(init.summary != null ? { summary: init.summary } : {}),
|
|
43
|
+
...(init.model != null ? { model: init.model } : {}),
|
|
44
|
+
...(init.trigger_conversation_id != null
|
|
45
|
+
? { trigger_conversation_id: init.trigger_conversation_id }
|
|
46
|
+
: {}),
|
|
47
|
+
...(init.trigger_event_id != null
|
|
48
|
+
? { trigger_event_id: init.trigger_event_id }
|
|
49
|
+
: {}),
|
|
40
50
|
},
|
|
41
51
|
events: new Map(),
|
|
42
52
|
});
|
|
@@ -66,6 +76,56 @@ export class InMemoryAdapter {
|
|
|
66
76
|
return null;
|
|
67
77
|
return foldEvents(s.row, [...s.events.values()], new Date());
|
|
68
78
|
}
|
|
79
|
+
async updateSession(sessionId, patch) {
|
|
80
|
+
const s = this.sessions.get(sessionId);
|
|
81
|
+
if (!s)
|
|
82
|
+
return null;
|
|
83
|
+
// Patch semantics: undefined = leave alone; null = clear; value = set.
|
|
84
|
+
// SessionRow optional fields use `string | undefined`, so a "null"
|
|
85
|
+
// request collapses to `undefined` storage-side.
|
|
86
|
+
const next = { ...s.row };
|
|
87
|
+
if (patch.title !== undefined) {
|
|
88
|
+
if (patch.title === null)
|
|
89
|
+
delete next.title;
|
|
90
|
+
else
|
|
91
|
+
next.title = patch.title;
|
|
92
|
+
}
|
|
93
|
+
if (patch.channel !== undefined) {
|
|
94
|
+
if (patch.channel === null)
|
|
95
|
+
delete next.channel;
|
|
96
|
+
else
|
|
97
|
+
next.channel = patch.channel;
|
|
98
|
+
}
|
|
99
|
+
if (patch.status !== undefined) {
|
|
100
|
+
next.status = patch.status;
|
|
101
|
+
}
|
|
102
|
+
if (patch.summary !== undefined) {
|
|
103
|
+
if (patch.summary === null)
|
|
104
|
+
delete next.summary;
|
|
105
|
+
else
|
|
106
|
+
next.summary = patch.summary;
|
|
107
|
+
}
|
|
108
|
+
if (patch.model !== undefined) {
|
|
109
|
+
if (patch.model === null)
|
|
110
|
+
delete next.model;
|
|
111
|
+
else
|
|
112
|
+
next.model = patch.model;
|
|
113
|
+
}
|
|
114
|
+
if (patch.trigger_conversation_id !== undefined) {
|
|
115
|
+
if (patch.trigger_conversation_id === null)
|
|
116
|
+
delete next.trigger_conversation_id;
|
|
117
|
+
else
|
|
118
|
+
next.trigger_conversation_id = patch.trigger_conversation_id;
|
|
119
|
+
}
|
|
120
|
+
if (patch.trigger_event_id !== undefined) {
|
|
121
|
+
if (patch.trigger_event_id === null)
|
|
122
|
+
delete next.trigger_event_id;
|
|
123
|
+
else
|
|
124
|
+
next.trigger_event_id = patch.trigger_event_id;
|
|
125
|
+
}
|
|
126
|
+
s.row = next;
|
|
127
|
+
return foldEvents(s.row, [...s.events.values()], new Date());
|
|
128
|
+
}
|
|
69
129
|
async getSessionEvents(sessionId) {
|
|
70
130
|
const s = this.sessions.get(sessionId);
|
|
71
131
|
if (!s)
|
|
@@ -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 } from "@hexis-ai/engram-sdk";
|
|
2
|
+
import type { AliasInfo, AliasUpsert, IdentityInfo, IdentityUpsert, PersonCreate, PersonInfo, 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
|
|
@@ -37,6 +37,7 @@ export declare class PostgresAdapter implements StorageAdapter {
|
|
|
37
37
|
}): Promise<void>;
|
|
38
38
|
appendEvents(sessionId: string, events: SessionEvent[]): Promise<void>;
|
|
39
39
|
getSession(sessionId: string): Promise<Session | null>;
|
|
40
|
+
updateSession(sessionId: string, patch: SessionUpdate): Promise<Session | null>;
|
|
40
41
|
getSessionEvents(sessionId: string): Promise<SessionEvent[] | null>;
|
|
41
42
|
listSessions(opts: {
|
|
42
43
|
limit: number;
|
|
@@ -38,8 +38,14 @@ export class PostgresAdapter {
|
|
|
38
38
|
const viewableBy = init.viewable_by
|
|
39
39
|
? Array.from(new Set([...init.viewable_by, ...participants]))
|
|
40
40
|
: [...participants];
|
|
41
|
+
// status defaults to 'active' server-side via the column default;
|
|
42
|
+
// a null value here is treated as "use the default" by NULL-fallback.
|
|
41
43
|
await this.sql `
|
|
42
|
-
INSERT INTO engram_sessions (
|
|
44
|
+
INSERT INTO engram_sessions (
|
|
45
|
+
workspace_id, id, title, channel, participants, viewable_by,
|
|
46
|
+
created_at, status, summary, model,
|
|
47
|
+
trigger_conversation_id, trigger_event_id
|
|
48
|
+
)
|
|
43
49
|
VALUES (
|
|
44
50
|
${this.workspaceId},
|
|
45
51
|
${init.id},
|
|
@@ -47,7 +53,12 @@ export class PostgresAdapter {
|
|
|
47
53
|
${init.channel ?? null},
|
|
48
54
|
${participants},
|
|
49
55
|
${viewableBy},
|
|
50
|
-
${init.createdAt}
|
|
56
|
+
${init.createdAt},
|
|
57
|
+
${init.status ?? "active"},
|
|
58
|
+
${init.summary ?? null},
|
|
59
|
+
${init.model ?? null},
|
|
60
|
+
${init.trigger_conversation_id ?? null},
|
|
61
|
+
${init.trigger_event_id ?? null}
|
|
51
62
|
)
|
|
52
63
|
ON CONFLICT (workspace_id, id) DO NOTHING
|
|
53
64
|
`;
|
|
@@ -87,28 +98,52 @@ export class PostgresAdapter {
|
|
|
87
98
|
}
|
|
88
99
|
async getSession(sessionId) {
|
|
89
100
|
const rows = await this.sql `
|
|
90
|
-
SELECT id, title, channel, participants, viewable_by, created_at
|
|
101
|
+
SELECT id, title, channel, participants, viewable_by, created_at,
|
|
102
|
+
status, summary, model,
|
|
103
|
+
trigger_conversation_id, trigger_event_id
|
|
91
104
|
FROM engram_sessions
|
|
92
105
|
WHERE workspace_id = ${this.workspaceId} AND id = ${sessionId}
|
|
93
106
|
LIMIT 1
|
|
94
107
|
`;
|
|
95
108
|
if (rows.length === 0)
|
|
96
109
|
return null;
|
|
97
|
-
const r = rows[0];
|
|
98
110
|
const events = await this.sql `
|
|
99
111
|
SELECT payload FROM engram_events
|
|
100
112
|
WHERE workspace_id = ${this.workspaceId} AND session_id = ${sessionId}
|
|
101
113
|
ORDER BY seq
|
|
102
114
|
`;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
return foldEvents(toSessionRow(rows[0]), events.map((e) => e.payload), new Date());
|
|
116
|
+
}
|
|
117
|
+
async updateSession(sessionId, patch) {
|
|
118
|
+
// Translate JS undefined → "no change" via a per-column "provided"
|
|
119
|
+
// flag the SQL evaluates with CASE WHEN. null in patch becomes a
|
|
120
|
+
// real null in the DB (clear), value becomes a set.
|
|
121
|
+
const titleProvided = patch.title !== undefined;
|
|
122
|
+
const channelProvided = patch.channel !== undefined;
|
|
123
|
+
const statusProvided = patch.status !== undefined;
|
|
124
|
+
const summaryProvided = patch.summary !== undefined;
|
|
125
|
+
const modelProvided = patch.model !== undefined;
|
|
126
|
+
const tcIdProvided = patch.trigger_conversation_id !== undefined;
|
|
127
|
+
const teIdProvided = patch.trigger_event_id !== undefined;
|
|
128
|
+
const rows = await this.sql `
|
|
129
|
+
UPDATE engram_sessions SET
|
|
130
|
+
title = CASE WHEN ${titleProvided} THEN ${patch.title ?? null} ELSE title END,
|
|
131
|
+
channel = CASE WHEN ${channelProvided} THEN ${patch.channel ?? null} ELSE channel END,
|
|
132
|
+
status = CASE WHEN ${statusProvided} THEN ${patch.status ?? "active"} ELSE status END,
|
|
133
|
+
summary = CASE WHEN ${summaryProvided} THEN ${patch.summary ?? null} ELSE summary END,
|
|
134
|
+
model = CASE WHEN ${modelProvided} THEN ${patch.model ?? null} ELSE model END,
|
|
135
|
+
trigger_conversation_id = CASE WHEN ${tcIdProvided}
|
|
136
|
+
THEN ${patch.trigger_conversation_id ?? null}
|
|
137
|
+
ELSE trigger_conversation_id END,
|
|
138
|
+
trigger_event_id = CASE WHEN ${teIdProvided}
|
|
139
|
+
THEN ${patch.trigger_event_id ?? null}
|
|
140
|
+
ELSE trigger_event_id END
|
|
141
|
+
WHERE workspace_id = ${this.workspaceId} AND id = ${sessionId}
|
|
142
|
+
RETURNING id
|
|
143
|
+
`;
|
|
144
|
+
if (rows.length === 0)
|
|
145
|
+
return null;
|
|
146
|
+
return this.getSession(sessionId);
|
|
112
147
|
}
|
|
113
148
|
async getSessionEvents(sessionId) {
|
|
114
149
|
const rows = await this.sql `
|
|
@@ -358,6 +393,29 @@ function toPersonInfo(r) {
|
|
|
358
393
|
updated_at: toIso(r.updated_at),
|
|
359
394
|
};
|
|
360
395
|
}
|
|
396
|
+
function toSessionRow(r) {
|
|
397
|
+
return {
|
|
398
|
+
id: r.id,
|
|
399
|
+
...(r.title ? { title: r.title } : {}),
|
|
400
|
+
...(r.channel ? { channel: r.channel } : {}),
|
|
401
|
+
participants: r.participants,
|
|
402
|
+
viewable_by: r.viewable_by,
|
|
403
|
+
createdAt: typeof r.created_at === "string"
|
|
404
|
+
? r.created_at
|
|
405
|
+
: r.created_at.toISOString(),
|
|
406
|
+
...(r.status === "active" || r.status === "idle" || r.status === "completed"
|
|
407
|
+
? { status: r.status }
|
|
408
|
+
: {}),
|
|
409
|
+
...(r.summary !== null ? { summary: r.summary } : {}),
|
|
410
|
+
...(r.model !== null ? { model: r.model } : {}),
|
|
411
|
+
...(r.trigger_conversation_id !== null
|
|
412
|
+
? { trigger_conversation_id: r.trigger_conversation_id }
|
|
413
|
+
: {}),
|
|
414
|
+
...(r.trigger_event_id !== null
|
|
415
|
+
? { trigger_event_id: r.trigger_event_id }
|
|
416
|
+
: {}),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
361
419
|
function toAliasInfo(r) {
|
|
362
420
|
const toIso = (v) => (typeof v === "string" ? v : v.toISOString());
|
|
363
421
|
// last_used is a DATE column; postgres-js returns it as a Date at UTC
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const name = "0004-schema-completion";
|
|
2
|
+
export declare const sql = "\n-- Wave 1 of the engram-as-sole-memory-source migration. Adds the\n-- richer columns monet needs to retire its in-tree memory tables, and\n-- tightens a few earlier schema choices before launch freezes the wire.\n\n-- Persons gain organizational fields. Previously engram was the\n-- retrieval primitive only, so role/team/source were intentionally\n-- absent (\"HR data belongs upstream\"). Now that engram is monet's\n-- canonical memory layer, those facts have to live here.\nALTER TABLE engram_persons ADD COLUMN IF NOT EXISTS role TEXT;\nALTER TABLE engram_persons ADD COLUMN IF NOT EXISTS team TEXT;\nALTER TABLE engram_persons ADD COLUMN IF NOT EXISTS source TEXT;\nUPDATE engram_persons SET source = 'auto' WHERE source IS NULL;\nALTER TABLE engram_persons ALTER COLUMN source SET DEFAULT 'auto';\nALTER TABLE engram_persons ALTER COLUMN source SET NOT NULL;\n\n-- Sessions gain monet-derived metadata so the host's conversation_*\n-- columns disappear.\nALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS status TEXT;\nUPDATE engram_sessions SET status = 'active' WHERE status IS NULL;\nALTER TABLE engram_sessions ALTER COLUMN status SET DEFAULT 'active';\nALTER TABLE engram_sessions ALTER COLUMN status SET NOT NULL;\nALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS summary TEXT;\nALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS model TEXT;\n-- A session can be triggered by another conversation (manual fan-out)\n-- or by an external event id (calendar event in monet, etc.). No FK on\n-- trigger_event_id \u2014 it crosses systems.\nALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS trigger_conversation_id TEXT;\nALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS trigger_event_id TEXT;\n\n-- Identities tightening \u2014 last chance to fix these before launch.\n-- is_primary: nullable BOOLEAN where null/false were ambiguous \u2192\n-- pin to NOT NULL DEFAULT false.\n-- unlinked_at: new \u2014 represents \"this external connection was\n-- revoked\", which the append-via-upsert model previously couldn't\n-- express. nullable; non-null means the identity is no longer\n-- authoritative.\n-- linked_at: was DATE (lossy). Widen to TIMESTAMPTZ to preserve\n-- full timing. The cast is a no-op when already TIMESTAMPTZ.\nUPDATE engram_identities SET is_primary = false WHERE is_primary IS NULL;\nALTER TABLE engram_identities ALTER COLUMN is_primary SET DEFAULT false;\nALTER TABLE engram_identities ALTER COLUMN is_primary SET NOT NULL;\nALTER TABLE engram_identities ADD COLUMN IF NOT EXISTS unlinked_at TIMESTAMPTZ;\nALTER TABLE engram_identities ALTER COLUMN linked_at TYPE TIMESTAMPTZ\n USING linked_at::TIMESTAMPTZ;\n\n-- Index supporting \"active identities only\" queries \u2014 common after\n-- adding unlinked_at since most callers want non-revoked rows.\nCREATE INDEX IF NOT EXISTS idx_engram_identities_active_service_external\n ON engram_identities (workspace_id, service, external_id)\n WHERE unlinked_at IS NULL;\n";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const name = "0004-schema-completion";
|
|
2
|
+
export const sql = `
|
|
3
|
+
-- Wave 1 of the engram-as-sole-memory-source migration. Adds the
|
|
4
|
+
-- richer columns monet needs to retire its in-tree memory tables, and
|
|
5
|
+
-- tightens a few earlier schema choices before launch freezes the wire.
|
|
6
|
+
|
|
7
|
+
-- Persons gain organizational fields. Previously engram was the
|
|
8
|
+
-- retrieval primitive only, so role/team/source were intentionally
|
|
9
|
+
-- absent ("HR data belongs upstream"). Now that engram is monet's
|
|
10
|
+
-- canonical memory layer, those facts have to live here.
|
|
11
|
+
ALTER TABLE engram_persons ADD COLUMN IF NOT EXISTS role TEXT;
|
|
12
|
+
ALTER TABLE engram_persons ADD COLUMN IF NOT EXISTS team TEXT;
|
|
13
|
+
ALTER TABLE engram_persons ADD COLUMN IF NOT EXISTS source TEXT;
|
|
14
|
+
UPDATE engram_persons SET source = 'auto' WHERE source IS NULL;
|
|
15
|
+
ALTER TABLE engram_persons ALTER COLUMN source SET DEFAULT 'auto';
|
|
16
|
+
ALTER TABLE engram_persons ALTER COLUMN source SET NOT NULL;
|
|
17
|
+
|
|
18
|
+
-- Sessions gain monet-derived metadata so the host's conversation_*
|
|
19
|
+
-- columns disappear.
|
|
20
|
+
ALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS status TEXT;
|
|
21
|
+
UPDATE engram_sessions SET status = 'active' WHERE status IS NULL;
|
|
22
|
+
ALTER TABLE engram_sessions ALTER COLUMN status SET DEFAULT 'active';
|
|
23
|
+
ALTER TABLE engram_sessions ALTER COLUMN status SET NOT NULL;
|
|
24
|
+
ALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS summary TEXT;
|
|
25
|
+
ALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS model TEXT;
|
|
26
|
+
-- A session can be triggered by another conversation (manual fan-out)
|
|
27
|
+
-- or by an external event id (calendar event in monet, etc.). No FK on
|
|
28
|
+
-- trigger_event_id — it crosses systems.
|
|
29
|
+
ALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS trigger_conversation_id TEXT;
|
|
30
|
+
ALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS trigger_event_id TEXT;
|
|
31
|
+
|
|
32
|
+
-- Identities tightening — last chance to fix these before launch.
|
|
33
|
+
-- is_primary: nullable BOOLEAN where null/false were ambiguous →
|
|
34
|
+
-- pin to NOT NULL DEFAULT false.
|
|
35
|
+
-- unlinked_at: new — represents "this external connection was
|
|
36
|
+
-- revoked", which the append-via-upsert model previously couldn't
|
|
37
|
+
-- express. nullable; non-null means the identity is no longer
|
|
38
|
+
-- authoritative.
|
|
39
|
+
-- linked_at: was DATE (lossy). Widen to TIMESTAMPTZ to preserve
|
|
40
|
+
-- full timing. The cast is a no-op when already TIMESTAMPTZ.
|
|
41
|
+
UPDATE engram_identities SET is_primary = false WHERE is_primary IS NULL;
|
|
42
|
+
ALTER TABLE engram_identities ALTER COLUMN is_primary SET DEFAULT false;
|
|
43
|
+
ALTER TABLE engram_identities ALTER COLUMN is_primary SET NOT NULL;
|
|
44
|
+
ALTER TABLE engram_identities ADD COLUMN IF NOT EXISTS unlinked_at TIMESTAMPTZ;
|
|
45
|
+
ALTER TABLE engram_identities ALTER COLUMN linked_at TYPE TIMESTAMPTZ
|
|
46
|
+
USING linked_at::TIMESTAMPTZ;
|
|
47
|
+
|
|
48
|
+
-- Index supporting "active identities only" queries — common after
|
|
49
|
+
-- adding unlinked_at since most callers want non-revoked rows.
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_engram_identities_active_service_external
|
|
51
|
+
ON engram_identities (workspace_id, service, external_id)
|
|
52
|
+
WHERE unlinked_at IS NULL;
|
|
53
|
+
`;
|
package/dist/migrations/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as m0001 from "./0001-baseline";
|
|
2
2
|
import * as m0002 from "./0002-aliases";
|
|
3
3
|
import * as m0003 from "./0003-identities";
|
|
4
|
+
import * as m0004 from "./0004-schema-completion";
|
|
4
5
|
/**
|
|
5
6
|
* Schema migrations, applied in array order. Add a new file under
|
|
6
7
|
* `migrations/NNNN-<slug>.ts` exporting `name` and `sql`, then append it
|
|
@@ -12,4 +13,5 @@ export const MIGRATIONS = [
|
|
|
12
13
|
{ name: m0001.name, sql: m0001.sql },
|
|
13
14
|
{ name: m0002.name, sql: m0002.sql },
|
|
14
15
|
{ name: m0003.name, sql: m0003.sql },
|
|
16
|
+
{ name: m0004.name, sql: m0004.sql },
|
|
15
17
|
];
|
package/dist/openapi.js
CHANGED
|
@@ -128,6 +128,17 @@ function buildPaths() {
|
|
|
128
128
|
"401": res("認証エラー"),
|
|
129
129
|
},
|
|
130
130
|
},
|
|
131
|
+
patch: {
|
|
132
|
+
summary: "セッションのメタデータ(title / channel / status / summary / model / trigger_*)を部分更新する。",
|
|
133
|
+
parameters: [pathParam("id", "セッション id。")],
|
|
134
|
+
requestBody: jsonBody("SessionUpdate"),
|
|
135
|
+
responses: {
|
|
136
|
+
"200": res("更新後のセッションのエンベロープ"),
|
|
137
|
+
"400": res("リクエストボディが不正"),
|
|
138
|
+
"404": res("セッションが見つからない"),
|
|
139
|
+
"401": res("認証エラー"),
|
|
140
|
+
},
|
|
141
|
+
},
|
|
131
142
|
}),
|
|
132
143
|
"/v1/sessions/{id}/events": tagged("Sessions", {
|
|
133
144
|
post: {
|
|
@@ -3,10 +3,11 @@ import type { Env } from "../context";
|
|
|
3
3
|
import { type RouteConfig } from "./helpers";
|
|
4
4
|
/**
|
|
5
5
|
* Session routes. Mount under `/v1`:
|
|
6
|
-
* POST
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* GET
|
|
10
|
-
* GET
|
|
6
|
+
* POST /v1/sessions create a session
|
|
7
|
+
* PATCH /v1/sessions/:id update session-level metadata
|
|
8
|
+
* POST /v1/sessions/:id/events append events
|
|
9
|
+
* GET /v1/sessions/:id fetch one session + its persons map
|
|
10
|
+
* GET /v1/sessions/:id/events fetch the raw, ordered event log
|
|
11
|
+
* GET /v1/sessions list recent sessions + persons map
|
|
11
12
|
*/
|
|
12
13
|
export declare function sessionsRoutes(cfg: RouteConfig): Hono<Env>;
|
package/dist/routes/sessions.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import { eventBatchSchema, parseJsonBody, sessionInitSchema } from "../schemas";
|
|
2
|
+
import { eventBatchSchema, parseJsonBody, sessionInitSchema, sessionUpdateSchema, } from "../schemas";
|
|
3
3
|
import { clampLimit, resolvePersonMap } from "./helpers";
|
|
4
4
|
/**
|
|
5
5
|
* Session routes. Mount under `/v1`:
|
|
6
|
-
* POST
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* GET
|
|
10
|
-
* GET
|
|
6
|
+
* POST /v1/sessions create a session
|
|
7
|
+
* PATCH /v1/sessions/:id update session-level metadata
|
|
8
|
+
* POST /v1/sessions/:id/events append events
|
|
9
|
+
* GET /v1/sessions/:id fetch one session + its persons map
|
|
10
|
+
* GET /v1/sessions/:id/events fetch the raw, ordered event log
|
|
11
|
+
* GET /v1/sessions list recent sessions + persons map
|
|
11
12
|
*/
|
|
12
13
|
export function sessionsRoutes(cfg) {
|
|
13
14
|
const app = new Hono();
|
|
@@ -33,6 +34,17 @@ export function sessionsRoutes(cfg) {
|
|
|
33
34
|
}
|
|
34
35
|
return c.body(null, 204);
|
|
35
36
|
});
|
|
37
|
+
app.patch("/sessions/:id", async (c) => {
|
|
38
|
+
const id = c.req.param("id");
|
|
39
|
+
const body = await parseJsonBody(c, sessionUpdateSchema);
|
|
40
|
+
if (body instanceof Response)
|
|
41
|
+
return body;
|
|
42
|
+
const session = await c.var.ctx.storage.updateSession(id, body);
|
|
43
|
+
if (!session)
|
|
44
|
+
return c.json({ error: "session_not_found" }, 404);
|
|
45
|
+
const persons = await resolvePersonMap(c.var.ctx.storage, [session]);
|
|
46
|
+
return c.json({ session, persons });
|
|
47
|
+
});
|
|
36
48
|
app.get("/sessions/:id", async (c) => {
|
|
37
49
|
const id = c.req.param("id");
|
|
38
50
|
const s = await c.var.ctx.storage.getSession(id);
|
package/dist/schemas.d.ts
CHANGED
|
@@ -15,6 +15,28 @@ export declare const sessionInitSchema: z.ZodObject<{
|
|
|
15
15
|
channel: z.ZodOptional<z.ZodString>;
|
|
16
16
|
participants: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
17
17
|
viewable_by: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
18
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
19
|
+
active: "active";
|
|
20
|
+
idle: "idle";
|
|
21
|
+
completed: "completed";
|
|
22
|
+
}>>;
|
|
23
|
+
summary: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
24
|
+
model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
25
|
+
trigger_conversation_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
26
|
+
trigger_event_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
27
|
+
}, z.core.$strip>;
|
|
28
|
+
export declare const sessionUpdateSchema: z.ZodObject<{
|
|
29
|
+
title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
30
|
+
channel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
31
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
32
|
+
active: "active";
|
|
33
|
+
idle: "idle";
|
|
34
|
+
completed: "completed";
|
|
35
|
+
}>>;
|
|
36
|
+
summary: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
37
|
+
model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
38
|
+
trigger_conversation_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
39
|
+
trigger_event_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
18
40
|
}, z.core.$strip>;
|
|
19
41
|
export declare const sessionEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
20
42
|
type: z.ZodLiteral<"step">;
|
package/dist/schemas.js
CHANGED
|
@@ -15,6 +15,20 @@ export const sessionInitSchema = z.object({
|
|
|
15
15
|
channel: z.string().optional(),
|
|
16
16
|
participants: z.array(z.string()).optional(),
|
|
17
17
|
viewable_by: z.array(z.string()).optional(),
|
|
18
|
+
status: z.enum(["active", "idle", "completed"]).optional(),
|
|
19
|
+
summary: z.string().nullable().optional(),
|
|
20
|
+
model: z.string().nullable().optional(),
|
|
21
|
+
trigger_conversation_id: z.string().nullable().optional(),
|
|
22
|
+
trigger_event_id: z.string().nullable().optional(),
|
|
23
|
+
});
|
|
24
|
+
export const sessionUpdateSchema = z.object({
|
|
25
|
+
title: z.string().nullable().optional(),
|
|
26
|
+
channel: z.string().nullable().optional(),
|
|
27
|
+
status: z.enum(["active", "idle", "completed"]).optional(),
|
|
28
|
+
summary: z.string().nullable().optional(),
|
|
29
|
+
model: z.string().nullable().optional(),
|
|
30
|
+
trigger_conversation_id: z.string().nullable().optional(),
|
|
31
|
+
trigger_event_id: z.string().nullable().optional(),
|
|
18
32
|
});
|
|
19
33
|
const stepEventSchema = z.object({
|
|
20
34
|
type: z.literal("step"),
|
package/dist/server.js
CHANGED
|
@@ -52,7 +52,7 @@ export function createServer(opts) {
|
|
|
52
52
|
ok: true,
|
|
53
53
|
routes: {
|
|
54
54
|
sessions: "POST/GET /v1/sessions",
|
|
55
|
-
sessionById: "GET /v1/sessions/:id",
|
|
55
|
+
sessionById: "GET/PATCH /v1/sessions/:id",
|
|
56
56
|
events: "POST /v1/sessions/:id/events",
|
|
57
57
|
sessionEvents: "GET /v1/sessions/:id/events",
|
|
58
58
|
search: "POST /v1/search",
|
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 } from "@hexis-ai/engram-sdk";
|
|
2
|
+
import type { AliasInfo, AliasUpsert, IdentityInfo, IdentityUpsert, PersonCreate, PersonInfo, 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
|
|
@@ -18,6 +18,13 @@ export interface StorageAdapter {
|
|
|
18
18
|
appendEvents(sessionId: string, events: SessionEvent[]): Promise<void>;
|
|
19
19
|
/** Materialize a session (events folded into Session shape). */
|
|
20
20
|
getSession(sessionId: string): Promise<Session | null>;
|
|
21
|
+
/**
|
|
22
|
+
* Patch session-level metadata (title / channel / status / summary /
|
|
23
|
+
* model / trigger_*). Each field updates only when present in `patch`
|
|
24
|
+
* — `undefined` leaves the column alone, `null` clears it. Returns
|
|
25
|
+
* the materialized session post-update, or `null` if unknown.
|
|
26
|
+
*/
|
|
27
|
+
updateSession(sessionId: string, patch: SessionUpdate): Promise<Session | null>;
|
|
21
28
|
/**
|
|
22
29
|
* Raw event log for a session, ordered by `seq`. Unlike `getSession`,
|
|
23
30
|
* this does not fold events — callers get per-event timestamps and the
|
|
@@ -102,5 +109,10 @@ export interface SessionRow {
|
|
|
102
109
|
participants: string[];
|
|
103
110
|
viewable_by: string[];
|
|
104
111
|
createdAt: string;
|
|
112
|
+
status?: "active" | "idle" | "completed";
|
|
113
|
+
summary?: string;
|
|
114
|
+
model?: string;
|
|
115
|
+
trigger_conversation_id?: string;
|
|
116
|
+
trigger_event_id?: string;
|
|
105
117
|
}
|
|
106
118
|
export declare function foldEvents(row: SessionRow, events: SessionEvent[], now: Date): Session;
|
package/dist/storage.js
CHANGED
|
@@ -21,9 +21,19 @@ export function foldEvents(row, events, now) {
|
|
|
21
21
|
return {
|
|
22
22
|
id: row.id,
|
|
23
23
|
...(title ? { title } : {}),
|
|
24
|
+
...(row.channel ? { channel: row.channel } : {}),
|
|
24
25
|
steps,
|
|
25
26
|
daysAgo,
|
|
26
27
|
...(participants.size > 0 ? { participants: [...participants] } : {}),
|
|
27
28
|
...(viewableSet.size > 0 ? { viewable_by: [...viewableSet] } : {}),
|
|
29
|
+
...(row.status ? { status: row.status } : {}),
|
|
30
|
+
...(row.summary !== undefined ? { summary: row.summary } : {}),
|
|
31
|
+
...(row.model !== undefined ? { model: row.model } : {}),
|
|
32
|
+
...(row.trigger_conversation_id !== undefined
|
|
33
|
+
? { trigger_conversation_id: row.trigger_conversation_id }
|
|
34
|
+
: {}),
|
|
35
|
+
...(row.trigger_event_id !== undefined
|
|
36
|
+
? { trigger_event_id: row.trigger_event_id }
|
|
37
|
+
: {}),
|
|
28
38
|
};
|
|
29
39
|
}
|
package/openapi.json
CHANGED
|
@@ -85,6 +85,54 @@
|
|
|
85
85
|
"items": {
|
|
86
86
|
"type": "string"
|
|
87
87
|
}
|
|
88
|
+
},
|
|
89
|
+
"status": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"enum": [
|
|
92
|
+
"active",
|
|
93
|
+
"idle",
|
|
94
|
+
"completed"
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
"summary": {
|
|
98
|
+
"anyOf": [
|
|
99
|
+
{
|
|
100
|
+
"type": "string"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"type": "null"
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
"model": {
|
|
108
|
+
"anyOf": [
|
|
109
|
+
{
|
|
110
|
+
"type": "string"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"type": "null"
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
"trigger_conversation_id": {
|
|
118
|
+
"anyOf": [
|
|
119
|
+
{
|
|
120
|
+
"type": "string"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"type": "null"
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
"trigger_event_id": {
|
|
128
|
+
"anyOf": [
|
|
129
|
+
{
|
|
130
|
+
"type": "string"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"type": "null"
|
|
134
|
+
}
|
|
135
|
+
]
|
|
88
136
|
}
|
|
89
137
|
},
|
|
90
138
|
"additionalProperties": false
|
|
@@ -536,6 +584,47 @@
|
|
|
536
584
|
"tags": [
|
|
537
585
|
"Sessions"
|
|
538
586
|
]
|
|
587
|
+
},
|
|
588
|
+
"patch": {
|
|
589
|
+
"summary": "セッションのメタデータ(title / channel / status / summary / model / trigger_*)を部分更新する。",
|
|
590
|
+
"parameters": [
|
|
591
|
+
{
|
|
592
|
+
"name": "id",
|
|
593
|
+
"in": "path",
|
|
594
|
+
"required": true,
|
|
595
|
+
"schema": {
|
|
596
|
+
"type": "string"
|
|
597
|
+
},
|
|
598
|
+
"description": "セッション id。"
|
|
599
|
+
}
|
|
600
|
+
],
|
|
601
|
+
"requestBody": {
|
|
602
|
+
"required": true,
|
|
603
|
+
"content": {
|
|
604
|
+
"application/json": {
|
|
605
|
+
"schema": {
|
|
606
|
+
"$ref": "#/components/schemas/SessionUpdate"
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
"responses": {
|
|
612
|
+
"200": {
|
|
613
|
+
"description": "更新後のセッションのエンベロープ"
|
|
614
|
+
},
|
|
615
|
+
"400": {
|
|
616
|
+
"description": "リクエストボディが不正"
|
|
617
|
+
},
|
|
618
|
+
"401": {
|
|
619
|
+
"description": "認証エラー"
|
|
620
|
+
},
|
|
621
|
+
"404": {
|
|
622
|
+
"description": "セッションが見つからない"
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
"tags": [
|
|
626
|
+
"Sessions"
|
|
627
|
+
]
|
|
539
628
|
}
|
|
540
629
|
},
|
|
541
630
|
"/v1/sessions/{id}/events": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hexis-ai/engram-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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.1.5",
|
|
53
|
-
"@hexis-ai/engram-sdk": "^0.
|
|
53
|
+
"@hexis-ai/engram-sdk": "^0.7.0",
|
|
54
54
|
"hono": "^4.6.0",
|
|
55
55
|
"zod": "^4.0.0"
|
|
56
56
|
},
|