@taico/adk-session-store 0.3.5 → 0.4.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 CHANGED
@@ -1,8 +1,10 @@
1
1
  # @taico/adk-session-store
2
2
 
3
- [![Socket Badge](https://badge.socket.dev/npm/package/@taico/adk-session-store/0.3.5)](https://badge.socket.dev/npm/package/@taico/adk-session-store/0.3.5)
3
+ [![Socket Badge](https://badge.socket.dev/npm/package/@taico/adk-session-store/0.4.0)](https://badge.socket.dev/npm/package/@taico/adk-session-store/0.4.0)
4
4
 
5
- SQLite-backed session storage for Google ADK.
5
+ PostgreSQL and SQLite session storage for Google ADK. Taico production uses
6
+ `PostgresSessionService`; the SQLite implementation remains available to package
7
+ consumers that need an embedded store.
6
8
 
7
9
  `SqliteSessionService` extends ADK `BaseSessionService` and is intended as a drop-in replacement for `InMemorySessionService` when you need persistence.
8
10
 
@@ -14,6 +16,21 @@ npm install @taico/adk-session-store
14
16
 
15
17
  ## Usage
16
18
 
19
+ ### PostgreSQL
20
+
21
+ ```ts
22
+ import { PostgresSessionService } from '@taico/adk-session-store';
23
+
24
+ const sessionService = new PostgresSessionService({
25
+ connectionString: process.env.DATABASE_URL!,
26
+ });
27
+ ```
28
+
29
+ The application migration must create the `adk_*` tables before this service is
30
+ used.
31
+
32
+ ### SQLite
33
+
17
34
  ```ts
18
35
  import { Runner } from '@google/adk';
19
36
  import { SqliteSessionService } from '@taico/adk-session-store';
@@ -33,6 +50,7 @@ Use in-memory SQLite by omitting `filename` or by setting `filename: ':memory:'`
33
50
 
34
51
  ## API
35
52
 
53
+ - `new PostgresSessionService({ connectionString, ssl? })`
36
54
  - `new SqliteSessionService(options?)`
37
55
  - `options.filename?: string` (defaults to `:memory:`)
38
56
  - `close(): Promise<void>` to cleanly close the SQLite connection
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export * from './sqlite-session-service.js';
2
+ export * from './postgres-session-service.js';
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export * from './sqlite-session-service.js';
2
+ export * from './postgres-session-service.js';
@@ -0,0 +1,23 @@
1
+ import { AppendEventRequest, BaseSessionService, CreateSessionRequest, DeleteSessionRequest, Event, GetSessionRequest, ListSessionsRequest, ListSessionsResponse, Session } from "@google/adk";
2
+ export interface PostgresSessionServiceOptions {
3
+ connectionString: string;
4
+ ssl?: boolean;
5
+ }
6
+ export declare class PostgresSessionService extends BaseSessionService {
7
+ private readonly pool;
8
+ private readonly ready;
9
+ constructor(options: PostgresSessionServiceOptions);
10
+ createSession({ appName, userId, state, sessionId, }: CreateSessionRequest): Promise<Session>;
11
+ getSession({ appName, userId, sessionId, config, }: GetSessionRequest): Promise<Session | undefined>;
12
+ listSessions({ appName, userId, }: ListSessionsRequest): Promise<ListSessionsResponse>;
13
+ deleteSession({ appName, userId, sessionId, }: DeleteSessionRequest): Promise<void>;
14
+ appendEvent({ session, event }: AppendEventRequest): Promise<Event>;
15
+ close(): Promise<void>;
16
+ private initialize;
17
+ private mergeState;
18
+ private persistScopedState;
19
+ private parseJson;
20
+ private cloneState;
21
+ private one;
22
+ private many;
23
+ }
@@ -0,0 +1,178 @@
1
+ import { BaseSessionService, State, createSession, } from "@google/adk";
2
+ import { randomUUID } from "node:crypto";
3
+ import { Pool } from "pg";
4
+ export class PostgresSessionService extends BaseSessionService {
5
+ pool;
6
+ ready;
7
+ constructor(options) {
8
+ super();
9
+ this.pool = new Pool({
10
+ connectionString: options.connectionString,
11
+ ssl: options.ssl ? { rejectUnauthorized: true } : false,
12
+ });
13
+ this.ready = this.initialize();
14
+ }
15
+ async createSession({ appName, userId, state, sessionId, }) {
16
+ await this.ready;
17
+ const session = createSession({
18
+ id: sessionId ?? randomUUID(),
19
+ appName,
20
+ userId,
21
+ state: state ?? {},
22
+ events: [],
23
+ lastUpdateTime: Date.now(),
24
+ });
25
+ await this.pool.query(`INSERT INTO adk_sessions (app_name, user_id, session_id, state_json, last_update_time)
26
+ VALUES ($1, $2, $3, $4::jsonb, $5)
27
+ ON CONFLICT (app_name, user_id, session_id) DO UPDATE SET
28
+ state_json = EXCLUDED.state_json,
29
+ last_update_time = EXCLUDED.last_update_time`, [
30
+ appName,
31
+ userId,
32
+ session.id,
33
+ JSON.stringify(session.state),
34
+ session.lastUpdateTime,
35
+ ]);
36
+ return this.mergeState(session);
37
+ }
38
+ async getSession({ appName, userId, sessionId, config, }) {
39
+ await this.ready;
40
+ const sessionRow = await this.one(`SELECT app_name, user_id, session_id, state_json, last_update_time
41
+ FROM adk_sessions WHERE app_name = $1 AND user_id = $2 AND session_id = $3`, [appName, userId, sessionId]);
42
+ if (!sessionRow)
43
+ return undefined;
44
+ const eventRows = await this.many(`SELECT event_json FROM adk_events
45
+ WHERE app_name = $1 AND user_id = $2 AND session_id = $3 ORDER BY sequence ASC`, [appName, userId, sessionId]);
46
+ let events = eventRows.map((row) => this.parseJson(row.event_json));
47
+ if (config?.numRecentEvents)
48
+ events = events.slice(-config.numRecentEvents);
49
+ if (config?.afterTimestamp) {
50
+ let index = events.length - 1;
51
+ while (index >= 0 && events[index].timestamp >= config.afterTimestamp)
52
+ index -= 1;
53
+ if (index >= 0)
54
+ events = events.slice(index + 1);
55
+ }
56
+ return this.mergeState(createSession({
57
+ id: sessionRow.session_id,
58
+ appName: sessionRow.app_name,
59
+ userId: sessionRow.user_id,
60
+ state: this.parseJson(sessionRow.state_json),
61
+ events,
62
+ lastUpdateTime: Number(sessionRow.last_update_time),
63
+ }));
64
+ }
65
+ async listSessions({ appName, userId, }) {
66
+ await this.ready;
67
+ const rows = await this.many(`SELECT app_name, user_id, session_id, state_json, last_update_time
68
+ FROM adk_sessions WHERE app_name = $1 AND user_id = $2`, [appName, userId]);
69
+ return {
70
+ sessions: rows.map((row) => createSession({
71
+ id: row.session_id,
72
+ appName: row.app_name,
73
+ userId: row.user_id,
74
+ state: {},
75
+ events: [],
76
+ lastUpdateTime: Number(row.last_update_time),
77
+ })),
78
+ };
79
+ }
80
+ async deleteSession({ appName, userId, sessionId, }) {
81
+ await this.ready;
82
+ await this.pool.query("DELETE FROM adk_sessions WHERE app_name = $1 AND user_id = $2 AND session_id = $3", [appName, userId, sessionId]);
83
+ }
84
+ async appendEvent({ session, event }) {
85
+ await this.ready;
86
+ await super.appendEvent({ session, event });
87
+ session.lastUpdateTime = event.timestamp;
88
+ const stored = await this.one(`SELECT app_name, user_id, session_id, state_json, last_update_time
89
+ FROM adk_sessions WHERE app_name = $1 AND user_id = $2 AND session_id = $3`, [session.appName, session.userId, session.id]);
90
+ if (!stored)
91
+ return event;
92
+ const storedState = this.parseJson(stored.state_json);
93
+ if (!event.partial && event.actions?.stateDelta) {
94
+ for (const [key, value] of Object.entries(event.actions.stateDelta)) {
95
+ if (!key.startsWith(State.TEMP_PREFIX))
96
+ storedState[key] = value;
97
+ }
98
+ await this.persistScopedState(session.appName, session.userId, event.actions.stateDelta);
99
+ }
100
+ await this.pool.query(`UPDATE adk_sessions SET state_json = $1::jsonb, last_update_time = $2
101
+ WHERE app_name = $3 AND user_id = $4 AND session_id = $5`, [
102
+ JSON.stringify(storedState),
103
+ event.timestamp,
104
+ session.appName,
105
+ session.userId,
106
+ session.id,
107
+ ]);
108
+ if (!event.partial) {
109
+ await this.pool.query(`INSERT INTO adk_events (app_name, user_id, session_id, event_id, timestamp, event_json)
110
+ VALUES ($1, $2, $3, $4, $5, $6::jsonb)`, [
111
+ session.appName,
112
+ session.userId,
113
+ session.id,
114
+ event.id,
115
+ event.timestamp,
116
+ JSON.stringify(event),
117
+ ]);
118
+ }
119
+ return event;
120
+ }
121
+ async close() {
122
+ await this.ready;
123
+ await this.pool.end();
124
+ }
125
+ async initialize() {
126
+ await this.pool.query("SELECT 1");
127
+ }
128
+ async mergeState(session) {
129
+ const merged = createSession({
130
+ id: session.id,
131
+ appName: session.appName,
132
+ userId: session.userId,
133
+ state: this.cloneState(session.state),
134
+ events: session.events,
135
+ lastUpdateTime: session.lastUpdateTime,
136
+ });
137
+ const appRows = await this.many("SELECT state_key, value_json FROM adk_app_state WHERE app_name = $1", [session.appName]);
138
+ for (const row of appRows) {
139
+ merged.state[`${State.APP_PREFIX}${row.state_key}`] = this.parseJson(row.value_json);
140
+ }
141
+ const userRows = await this.many("SELECT state_key, value_json FROM adk_user_state WHERE app_name = $1 AND user_id = $2", [session.appName, session.userId]);
142
+ for (const row of userRows) {
143
+ merged.state[`${State.USER_PREFIX}${row.state_key}`] = this.parseJson(row.value_json);
144
+ }
145
+ return merged;
146
+ }
147
+ async persistScopedState(appName, userId, delta) {
148
+ for (const [key, value] of Object.entries(delta)) {
149
+ if (key.startsWith(State.APP_PREFIX)) {
150
+ await this.pool.query(`INSERT INTO adk_app_state (app_name, state_key, value_json) VALUES ($1, $2, $3::jsonb)
151
+ ON CONFLICT (app_name, state_key) DO UPDATE SET value_json = EXCLUDED.value_json`, [appName, key.replace(State.APP_PREFIX, ""), JSON.stringify(value)]);
152
+ }
153
+ if (key.startsWith(State.USER_PREFIX)) {
154
+ await this.pool.query(`INSERT INTO adk_user_state (app_name, user_id, state_key, value_json) VALUES ($1, $2, $3, $4::jsonb)
155
+ ON CONFLICT (app_name, user_id, state_key) DO UPDATE SET value_json = EXCLUDED.value_json`, [
156
+ appName,
157
+ userId,
158
+ key.replace(State.USER_PREFIX, ""),
159
+ JSON.stringify(value),
160
+ ]);
161
+ }
162
+ }
163
+ }
164
+ parseJson(value) {
165
+ return (typeof value === "string" ? JSON.parse(value) : value);
166
+ }
167
+ cloneState(state) {
168
+ return JSON.parse(JSON.stringify(state));
169
+ }
170
+ async one(sql, params) {
171
+ const result = await this.pool.query(sql, params);
172
+ return result.rows[0];
173
+ }
174
+ async many(sql, params) {
175
+ const result = await this.pool.query(sql, params);
176
+ return result.rows;
177
+ }
178
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taico/adk-session-store",
3
- "version": "0.3.5",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "license": "PolyForm-Small-Business-1.0.0",
@@ -28,9 +28,11 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@google/adk": "^0.2.5",
31
+ "pg": "^8.16.3",
31
32
  "sqlite3": "^5.1.7"
32
33
  },
33
34
  "devDependencies": {
35
+ "@types/pg": "^8.15.5",
34
36
  "typescript": "^5.6.3"
35
37
  }
36
38
  }