@taico/adk-session-store 0.1.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/README.md +40 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/sqlite-session-service.d.ts +22 -0
- package/dist/sqlite-session-service.js +215 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# @taico/adk-session-store
|
|
2
|
+
|
|
3
|
+
SQLite-backed session storage for Google ADK.
|
|
4
|
+
|
|
5
|
+
`SqliteSessionService` extends ADK `BaseSessionService` and is intended as a drop-in replacement for `InMemorySessionService` when you need persistence.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @taico/adk-session-store
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { Runner } from '@google/adk';
|
|
17
|
+
import { SqliteSessionService } from '@taico/adk-session-store';
|
|
18
|
+
|
|
19
|
+
const sessionService = new SqliteSessionService({
|
|
20
|
+
filename: './adk-sessions.sqlite',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const runner = new Runner({
|
|
24
|
+
appName: 'my-app',
|
|
25
|
+
agent,
|
|
26
|
+
sessionService,
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Use in-memory SQLite by omitting `filename` or by setting `filename: ':memory:'`.
|
|
31
|
+
|
|
32
|
+
## API
|
|
33
|
+
|
|
34
|
+
- `new SqliteSessionService(options?)`
|
|
35
|
+
- `options.filename?: string` (defaults to `:memory:`)
|
|
36
|
+
- `close(): Promise<void>` to cleanly close the SQLite connection
|
|
37
|
+
|
|
38
|
+
## Example
|
|
39
|
+
|
|
40
|
+
See `examples/basic.ts` for a minimal end-to-end usage snippet.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './sqlite-session-service.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './sqlite-session-service.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AppendEventRequest, BaseSessionService, CreateSessionRequest, DeleteSessionRequest, Event, GetSessionRequest, ListSessionsRequest, ListSessionsResponse, Session } from '@google/adk';
|
|
2
|
+
export interface SqliteSessionServiceOptions {
|
|
3
|
+
filename?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare class SqliteSessionService extends BaseSessionService {
|
|
6
|
+
private readonly db;
|
|
7
|
+
private readonly ready;
|
|
8
|
+
constructor(options?: SqliteSessionServiceOptions);
|
|
9
|
+
createSession({ appName, userId, state, sessionId }: CreateSessionRequest): Promise<Session>;
|
|
10
|
+
getSession({ appName, userId, sessionId, config }: GetSessionRequest): Promise<Session | undefined>;
|
|
11
|
+
listSessions({ appName, userId }: ListSessionsRequest): Promise<ListSessionsResponse>;
|
|
12
|
+
deleteSession({ appName, userId, sessionId }: DeleteSessionRequest): Promise<void>;
|
|
13
|
+
appendEvent({ session, event }: AppendEventRequest): Promise<Event>;
|
|
14
|
+
close(): Promise<void>;
|
|
15
|
+
private initialize;
|
|
16
|
+
private mergeState;
|
|
17
|
+
private persistScopedState;
|
|
18
|
+
private cloneState;
|
|
19
|
+
private run;
|
|
20
|
+
private get;
|
|
21
|
+
private all;
|
|
22
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { BaseSessionService, State, createSession, } from '@google/adk';
|
|
2
|
+
import * as sqlite3 from 'sqlite3';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
const sqlite3Module = sqlite3.default ?? sqlite3;
|
|
5
|
+
export class SqliteSessionService extends BaseSessionService {
|
|
6
|
+
db;
|
|
7
|
+
ready;
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
super();
|
|
10
|
+
this.db = new sqlite3Module.Database(options.filename ?? ':memory:');
|
|
11
|
+
this.ready = this.initialize();
|
|
12
|
+
}
|
|
13
|
+
async createSession({ appName, userId, state, sessionId }) {
|
|
14
|
+
await this.ready;
|
|
15
|
+
const session = createSession({
|
|
16
|
+
id: sessionId ?? randomUUID(),
|
|
17
|
+
appName,
|
|
18
|
+
userId,
|
|
19
|
+
state: state ?? {},
|
|
20
|
+
events: [],
|
|
21
|
+
lastUpdateTime: Date.now(),
|
|
22
|
+
});
|
|
23
|
+
await this.run('INSERT OR REPLACE INTO sessions (app_name, user_id, session_id, state_json, last_update_time) VALUES (?, ?, ?, ?, ?)', [appName, userId, session.id, JSON.stringify(session.state), session.lastUpdateTime]);
|
|
24
|
+
return this.mergeState(session);
|
|
25
|
+
}
|
|
26
|
+
async getSession({ appName, userId, sessionId, config }) {
|
|
27
|
+
await this.ready;
|
|
28
|
+
const sessionRow = await this.get('SELECT app_name, user_id, session_id, state_json, last_update_time FROM sessions WHERE app_name = ? AND user_id = ? AND session_id = ?', [appName, userId, sessionId]);
|
|
29
|
+
if (!sessionRow) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const eventRows = await this.all('SELECT event_json FROM events WHERE app_name = ? AND user_id = ? AND session_id = ? ORDER BY sequence ASC', [appName, userId, sessionId]);
|
|
33
|
+
let events = eventRows.map((row) => JSON.parse(row.event_json));
|
|
34
|
+
if (config?.numRecentEvents) {
|
|
35
|
+
events = events.slice(-config.numRecentEvents);
|
|
36
|
+
}
|
|
37
|
+
if (config?.afterTimestamp) {
|
|
38
|
+
let index = events.length - 1;
|
|
39
|
+
while (index >= 0) {
|
|
40
|
+
if (events[index].timestamp < config.afterTimestamp) {
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
index -= 1;
|
|
44
|
+
}
|
|
45
|
+
if (index >= 0) {
|
|
46
|
+
events = events.slice(index + 1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const session = createSession({
|
|
50
|
+
id: sessionRow.session_id,
|
|
51
|
+
appName: sessionRow.app_name,
|
|
52
|
+
userId: sessionRow.user_id,
|
|
53
|
+
state: JSON.parse(sessionRow.state_json),
|
|
54
|
+
events,
|
|
55
|
+
lastUpdateTime: sessionRow.last_update_time,
|
|
56
|
+
});
|
|
57
|
+
return this.mergeState(session);
|
|
58
|
+
}
|
|
59
|
+
async listSessions({ appName, userId }) {
|
|
60
|
+
await this.ready;
|
|
61
|
+
const rows = await this.all('SELECT app_name, user_id, session_id, last_update_time FROM sessions WHERE app_name = ? AND user_id = ?', [appName, userId]);
|
|
62
|
+
return {
|
|
63
|
+
sessions: rows.map((row) => createSession({
|
|
64
|
+
id: row.session_id,
|
|
65
|
+
appName: row.app_name,
|
|
66
|
+
userId: row.user_id,
|
|
67
|
+
state: {},
|
|
68
|
+
events: [],
|
|
69
|
+
lastUpdateTime: row.last_update_time,
|
|
70
|
+
})),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async deleteSession({ appName, userId, sessionId }) {
|
|
74
|
+
await this.ready;
|
|
75
|
+
await this.run('DELETE FROM events WHERE app_name = ? AND user_id = ? AND session_id = ?', [appName, userId, sessionId]);
|
|
76
|
+
await this.run('DELETE FROM sessions WHERE app_name = ? AND user_id = ? AND session_id = ?', [appName, userId, sessionId]);
|
|
77
|
+
}
|
|
78
|
+
async appendEvent({ session, event }) {
|
|
79
|
+
await this.ready;
|
|
80
|
+
await super.appendEvent({ session, event });
|
|
81
|
+
session.lastUpdateTime = event.timestamp;
|
|
82
|
+
const storedSession = await this.get('SELECT state_json FROM sessions WHERE app_name = ? AND user_id = ? AND session_id = ?', [session.appName, session.userId, session.id]);
|
|
83
|
+
if (!storedSession) {
|
|
84
|
+
return event;
|
|
85
|
+
}
|
|
86
|
+
let storedState = JSON.parse(storedSession.state_json);
|
|
87
|
+
if (!event.partial && event.actions?.stateDelta) {
|
|
88
|
+
for (const [key, value] of Object.entries(event.actions.stateDelta)) {
|
|
89
|
+
if (!key.startsWith(State.TEMP_PREFIX)) {
|
|
90
|
+
storedState[key] = value;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
await this.persistScopedState(session.appName, session.userId, event.actions.stateDelta);
|
|
94
|
+
}
|
|
95
|
+
await this.run('UPDATE sessions SET state_json = ?, last_update_time = ? WHERE app_name = ? AND user_id = ? AND session_id = ?', [JSON.stringify(storedState), event.timestamp, session.appName, session.userId, session.id]);
|
|
96
|
+
if (!event.partial) {
|
|
97
|
+
await this.run('INSERT INTO events (app_name, user_id, session_id, event_id, timestamp, event_json) VALUES (?, ?, ?, ?, ?, ?)', [session.appName, session.userId, session.id, event.id, event.timestamp, JSON.stringify(event)]);
|
|
98
|
+
}
|
|
99
|
+
return event;
|
|
100
|
+
}
|
|
101
|
+
async close() {
|
|
102
|
+
await this.ready;
|
|
103
|
+
await new Promise((resolve, reject) => {
|
|
104
|
+
this.db.close((error) => {
|
|
105
|
+
if (error) {
|
|
106
|
+
reject(error);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
resolve();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async initialize() {
|
|
114
|
+
await this.run('PRAGMA foreign_keys = ON');
|
|
115
|
+
await this.run(`CREATE TABLE IF NOT EXISTS sessions (
|
|
116
|
+
app_name TEXT NOT NULL,
|
|
117
|
+
user_id TEXT NOT NULL,
|
|
118
|
+
session_id TEXT NOT NULL,
|
|
119
|
+
state_json TEXT NOT NULL,
|
|
120
|
+
last_update_time INTEGER NOT NULL,
|
|
121
|
+
PRIMARY KEY (app_name, user_id, session_id)
|
|
122
|
+
)`);
|
|
123
|
+
await this.run(`CREATE TABLE IF NOT EXISTS events (
|
|
124
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
125
|
+
app_name TEXT NOT NULL,
|
|
126
|
+
user_id TEXT NOT NULL,
|
|
127
|
+
session_id TEXT NOT NULL,
|
|
128
|
+
event_id TEXT NOT NULL,
|
|
129
|
+
timestamp INTEGER NOT NULL,
|
|
130
|
+
event_json TEXT NOT NULL,
|
|
131
|
+
FOREIGN KEY (app_name, user_id, session_id)
|
|
132
|
+
REFERENCES sessions (app_name, user_id, session_id)
|
|
133
|
+
ON DELETE CASCADE
|
|
134
|
+
)`);
|
|
135
|
+
await this.run(`CREATE TABLE IF NOT EXISTS app_state (
|
|
136
|
+
app_name TEXT NOT NULL,
|
|
137
|
+
state_key TEXT NOT NULL,
|
|
138
|
+
value_json TEXT NOT NULL,
|
|
139
|
+
PRIMARY KEY (app_name, state_key)
|
|
140
|
+
)`);
|
|
141
|
+
await this.run(`CREATE TABLE IF NOT EXISTS user_state (
|
|
142
|
+
app_name TEXT NOT NULL,
|
|
143
|
+
user_id TEXT NOT NULL,
|
|
144
|
+
state_key TEXT NOT NULL,
|
|
145
|
+
value_json TEXT NOT NULL,
|
|
146
|
+
PRIMARY KEY (app_name, user_id, state_key)
|
|
147
|
+
)`);
|
|
148
|
+
}
|
|
149
|
+
async mergeState(session) {
|
|
150
|
+
const merged = createSession({
|
|
151
|
+
id: session.id,
|
|
152
|
+
appName: session.appName,
|
|
153
|
+
userId: session.userId,
|
|
154
|
+
state: this.cloneState(session.state),
|
|
155
|
+
events: session.events,
|
|
156
|
+
lastUpdateTime: session.lastUpdateTime,
|
|
157
|
+
});
|
|
158
|
+
const appRows = await this.all('SELECT state_key, value_json FROM app_state WHERE app_name = ?', [session.appName]);
|
|
159
|
+
for (const row of appRows) {
|
|
160
|
+
merged.state[`${State.APP_PREFIX}${row.state_key}`] = JSON.parse(row.value_json);
|
|
161
|
+
}
|
|
162
|
+
const userRows = await this.all('SELECT state_key, value_json FROM user_state WHERE app_name = ? AND user_id = ?', [session.appName, session.userId]);
|
|
163
|
+
for (const row of userRows) {
|
|
164
|
+
merged.state[`${State.USER_PREFIX}${row.state_key}`] = JSON.parse(row.value_json);
|
|
165
|
+
}
|
|
166
|
+
return merged;
|
|
167
|
+
}
|
|
168
|
+
async persistScopedState(appName, userId, delta) {
|
|
169
|
+
const entries = Object.entries(delta);
|
|
170
|
+
for (const [key, value] of entries) {
|
|
171
|
+
if (key.startsWith(State.APP_PREFIX)) {
|
|
172
|
+
await this.run('INSERT INTO app_state (app_name, state_key, value_json) VALUES (?, ?, ?) ON CONFLICT(app_name, state_key) DO UPDATE SET value_json = excluded.value_json', [appName, key.replace(State.APP_PREFIX, ''), JSON.stringify(value)]);
|
|
173
|
+
}
|
|
174
|
+
if (key.startsWith(State.USER_PREFIX)) {
|
|
175
|
+
await this.run('INSERT INTO user_state (app_name, user_id, state_key, value_json) VALUES (?, ?, ?, ?) ON CONFLICT(app_name, user_id, state_key) DO UPDATE SET value_json = excluded.value_json', [appName, userId, key.replace(State.USER_PREFIX, ''), JSON.stringify(value)]);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
cloneState(state) {
|
|
180
|
+
return JSON.parse(JSON.stringify(state));
|
|
181
|
+
}
|
|
182
|
+
run(sql, params = []) {
|
|
183
|
+
return new Promise((resolve, reject) => {
|
|
184
|
+
this.db.run(sql, params, (error) => {
|
|
185
|
+
if (error) {
|
|
186
|
+
reject(error);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
resolve();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
get(sql, params = []) {
|
|
194
|
+
return new Promise((resolve, reject) => {
|
|
195
|
+
this.db.get(sql, params, (error, row) => {
|
|
196
|
+
if (error) {
|
|
197
|
+
reject(error);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
resolve(row ?? undefined);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
all(sql, params = []) {
|
|
205
|
+
return new Promise((resolve, reject) => {
|
|
206
|
+
this.db.all(sql, params, (error, rows) => {
|
|
207
|
+
if (error) {
|
|
208
|
+
reject(error);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
resolve(rows);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@taico/adk-session-store",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"private": false,
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"package.json"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.build.json",
|
|
24
|
+
"typecheck": "tsc -p tsconfig.build.json --noEmit",
|
|
25
|
+
"pack": "npm run build && npm pack",
|
|
26
|
+
"release": "npm run build && npm publish --access public"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@google/adk": "^0.2.5",
|
|
30
|
+
"sqlite3": "^5.1.7"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"typescript": "^5.6.3"
|
|
34
|
+
}
|
|
35
|
+
}
|