@agent-assistant/sessions 0.1.0 → 0.1.2

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.
@@ -0,0 +1,3 @@
1
+ export { createSessionStore, defaultAffinityResolver, InMemorySessionStoreAdapter, resolveSession, } from './sessions.js';
2
+ export { SessionConflictError, SessionNotFoundError, SessionStateError, } from './types.js';
3
+ export type { AffinityResolver, CreateSessionInput, Session, SessionQuery, SessionResolvableMessage, SessionState, SessionStore, SessionStoreAdapter, SessionStoreConfig, } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { createSessionStore, defaultAffinityResolver, InMemorySessionStoreAdapter, resolveSession, } from './sessions.js';
2
+ export { SessionConflictError, SessionNotFoundError, SessionStateError, } from './types.js';
@@ -0,0 +1,12 @@
1
+ import type { AffinityResolver, Session, SessionQuery, SessionResolvableMessage, SessionStore, SessionStoreAdapter, SessionStoreConfig } from './types.js';
2
+ export declare function createSessionStore(config: SessionStoreConfig): SessionStore;
3
+ export declare class InMemorySessionStoreAdapter implements SessionStoreAdapter {
4
+ private readonly sessions;
5
+ insert(session: Session): Promise<void>;
6
+ fetchById(sessionId: string): Promise<Session | null>;
7
+ fetchMany(query: SessionQuery): Promise<Session[]>;
8
+ update(sessionId: string, patch: Partial<Session>): Promise<Session>;
9
+ delete(sessionId: string): Promise<void>;
10
+ }
11
+ export declare function resolveSession(message: SessionResolvableMessage, store: SessionStore, resolver: AffinityResolver): Promise<Session>;
12
+ export declare function defaultAffinityResolver(store: SessionStore): AffinityResolver;
@@ -0,0 +1,217 @@
1
+ import { SessionConflictError, SessionNotFoundError, SessionStateError, } from './types.js';
2
+ const DEFAULT_TTL_MS = 3_600_000;
3
+ const DEFAULT_FIND_LIMIT = 50;
4
+ function cloneSession(value) {
5
+ return structuredClone(value);
6
+ }
7
+ function nowIso() {
8
+ return new Date().toISOString();
9
+ }
10
+ function normalizeLimit(limit) {
11
+ return limit ?? DEFAULT_FIND_LIMIT;
12
+ }
13
+ function normalizeStateFilter(state) {
14
+ if (!state) {
15
+ return undefined;
16
+ }
17
+ return Array.isArray(state) ? state : [state];
18
+ }
19
+ function sortByRecentActivity(sessions) {
20
+ return [...sessions].sort((left, right) => {
21
+ return (Date.parse(right.lastActivityAt) - Date.parse(left.lastActivityAt) ||
22
+ Date.parse(right.createdAt) - Date.parse(left.createdAt));
23
+ });
24
+ }
25
+ async function getRequiredSession(adapter, sessionId) {
26
+ const session = await adapter.fetchById(sessionId);
27
+ if (!session) {
28
+ throw new SessionNotFoundError(sessionId);
29
+ }
30
+ return session;
31
+ }
32
+ export function createSessionStore(config) {
33
+ const { adapter } = config;
34
+ const defaultTtlMs = config.defaultTtlMs ?? DEFAULT_TTL_MS;
35
+ return {
36
+ async create(input) {
37
+ const existing = await adapter.fetchById(input.id);
38
+ if (existing) {
39
+ throw new SessionConflictError(input.id);
40
+ }
41
+ const timestamp = nowIso();
42
+ const session = {
43
+ id: input.id,
44
+ userId: input.userId,
45
+ workspaceId: input.workspaceId,
46
+ state: 'created',
47
+ createdAt: timestamp,
48
+ lastActivityAt: timestamp,
49
+ attachedSurfaces: input.initialSurfaceId ? [input.initialSurfaceId] : [],
50
+ metadata: { ...(input.metadata ?? {}) },
51
+ };
52
+ await adapter.insert(session);
53
+ return cloneSession(session);
54
+ },
55
+ async get(sessionId) {
56
+ return adapter.fetchById(sessionId);
57
+ },
58
+ async find(query) {
59
+ return adapter.fetchMany({
60
+ ...query,
61
+ limit: normalizeLimit(query.limit),
62
+ });
63
+ },
64
+ async touch(sessionId) {
65
+ const session = await getRequiredSession(adapter, sessionId);
66
+ if (session.state === 'expired') {
67
+ throw new SessionStateError(sessionId, session.state, 'touch');
68
+ }
69
+ const timestamp = nowIso();
70
+ const patch = {
71
+ lastActivityAt: timestamp,
72
+ };
73
+ if (session.state === 'created' || session.state === 'suspended') {
74
+ patch.state = 'active';
75
+ patch.stateChangedAt = timestamp;
76
+ }
77
+ return adapter.update(sessionId, patch);
78
+ },
79
+ async attachSurface(sessionId, surfaceId) {
80
+ const session = await getRequiredSession(adapter, sessionId);
81
+ if (session.attachedSurfaces.includes(surfaceId)) {
82
+ return session;
83
+ }
84
+ return adapter.update(sessionId, {
85
+ attachedSurfaces: [...session.attachedSurfaces, surfaceId],
86
+ });
87
+ },
88
+ async detachSurface(sessionId, surfaceId) {
89
+ const session = await getRequiredSession(adapter, sessionId);
90
+ if (!session.attachedSurfaces.includes(surfaceId)) {
91
+ return session;
92
+ }
93
+ return adapter.update(sessionId, {
94
+ attachedSurfaces: session.attachedSurfaces.filter((value) => value !== surfaceId),
95
+ });
96
+ },
97
+ async expire(sessionId) {
98
+ const session = await getRequiredSession(adapter, sessionId);
99
+ if (session.state === 'expired') {
100
+ return session;
101
+ }
102
+ return adapter.update(sessionId, {
103
+ state: 'expired',
104
+ stateChangedAt: nowIso(),
105
+ });
106
+ },
107
+ async sweepStale(ttlMs) {
108
+ const effectiveTtlMs = ttlMs ?? defaultTtlMs;
109
+ const cutoff = Date.now() - effectiveTtlMs;
110
+ const activeSessions = await adapter.fetchMany({
111
+ state: 'active',
112
+ limit: Number.MAX_SAFE_INTEGER,
113
+ });
114
+ const staleSessions = activeSessions.filter((session) => {
115
+ return Date.parse(session.lastActivityAt) < cutoff;
116
+ });
117
+ const transitioned = [];
118
+ for (const session of staleSessions) {
119
+ transitioned.push(await adapter.update(session.id, {
120
+ state: 'suspended',
121
+ stateChangedAt: nowIso(),
122
+ }));
123
+ }
124
+ return transitioned;
125
+ },
126
+ async updateMetadata(sessionId, metadata) {
127
+ const session = await getRequiredSession(adapter, sessionId);
128
+ return adapter.update(sessionId, {
129
+ metadata: {
130
+ ...session.metadata,
131
+ ...metadata,
132
+ },
133
+ });
134
+ },
135
+ };
136
+ }
137
+ export class InMemorySessionStoreAdapter {
138
+ sessions = new Map();
139
+ async insert(session) {
140
+ if (this.sessions.has(session.id)) {
141
+ throw new SessionConflictError(session.id);
142
+ }
143
+ this.sessions.set(session.id, cloneSession(session));
144
+ }
145
+ async fetchById(sessionId) {
146
+ const session = this.sessions.get(sessionId);
147
+ return session ? cloneSession(session) : null;
148
+ }
149
+ async fetchMany(query) {
150
+ const states = normalizeStateFilter(query.state);
151
+ const limit = normalizeLimit(query.limit);
152
+ const matches = [...this.sessions.values()].filter((session) => {
153
+ if (query.userId && session.userId !== query.userId) {
154
+ return false;
155
+ }
156
+ if (query.workspaceId && session.workspaceId !== query.workspaceId) {
157
+ return false;
158
+ }
159
+ if (states && !states.includes(session.state)) {
160
+ return false;
161
+ }
162
+ if (query.surfaceId && !session.attachedSurfaces.includes(query.surfaceId)) {
163
+ return false;
164
+ }
165
+ if (query.activeAfter && Date.parse(session.lastActivityAt) <= Date.parse(query.activeAfter)) {
166
+ return false;
167
+ }
168
+ return true;
169
+ });
170
+ return matches.slice(0, limit).map((session) => cloneSession(session));
171
+ }
172
+ async update(sessionId, patch) {
173
+ const existing = this.sessions.get(sessionId);
174
+ if (!existing) {
175
+ throw new SessionNotFoundError(sessionId);
176
+ }
177
+ const next = cloneSession({
178
+ ...existing,
179
+ ...patch,
180
+ });
181
+ this.sessions.set(sessionId, next);
182
+ return cloneSession(next);
183
+ }
184
+ async delete(sessionId) {
185
+ this.sessions.delete(sessionId);
186
+ }
187
+ }
188
+ export async function resolveSession(message, store, resolver) {
189
+ const existing = await resolver.resolve(message.userId, message.surfaceId);
190
+ if (existing) {
191
+ return store.touch(existing.id);
192
+ }
193
+ return store.create({
194
+ id: globalThis.crypto.randomUUID(),
195
+ userId: message.userId,
196
+ workspaceId: message.workspaceId,
197
+ initialSurfaceId: message.surfaceId,
198
+ });
199
+ }
200
+ export function defaultAffinityResolver(store) {
201
+ return {
202
+ async resolve(userId, surfaceId) {
203
+ const sessions = sortByRecentActivity(await store.find({
204
+ userId,
205
+ state: ['active', 'suspended'],
206
+ limit: DEFAULT_FIND_LIMIT,
207
+ }));
208
+ if (surfaceId) {
209
+ const attached = sessions.find((session) => session.attachedSurfaces.includes(surfaceId));
210
+ if (attached) {
211
+ return attached;
212
+ }
213
+ }
214
+ return sessions[0] ?? null;
215
+ },
216
+ };
217
+ }
@@ -0,0 +1,71 @@
1
+ export interface Session {
2
+ id: string;
3
+ userId: string;
4
+ workspaceId?: string;
5
+ state: SessionState;
6
+ createdAt: string;
7
+ lastActivityAt: string;
8
+ stateChangedAt?: string;
9
+ attachedSurfaces: string[];
10
+ metadata: Record<string, unknown>;
11
+ }
12
+ export type SessionState = 'created' | 'active' | 'suspended' | 'expired';
13
+ export interface SessionStore {
14
+ create(input: CreateSessionInput): Promise<Session>;
15
+ get(sessionId: string): Promise<Session | null>;
16
+ find(query: SessionQuery): Promise<Session[]>;
17
+ touch(sessionId: string): Promise<Session>;
18
+ attachSurface(sessionId: string, surfaceId: string): Promise<Session>;
19
+ detachSurface(sessionId: string, surfaceId: string): Promise<Session>;
20
+ expire(sessionId: string): Promise<Session>;
21
+ sweepStale(ttlMs: number): Promise<Session[]>;
22
+ updateMetadata(sessionId: string, metadata: Record<string, unknown>): Promise<Session>;
23
+ }
24
+ export interface CreateSessionInput {
25
+ id: string;
26
+ userId: string;
27
+ workspaceId?: string;
28
+ initialSurfaceId?: string;
29
+ metadata?: Record<string, unknown>;
30
+ }
31
+ export interface SessionQuery {
32
+ userId?: string;
33
+ workspaceId?: string;
34
+ state?: SessionState | SessionState[];
35
+ surfaceId?: string;
36
+ activeAfter?: string;
37
+ limit?: number;
38
+ }
39
+ export interface SessionStoreAdapter {
40
+ insert(session: Session): Promise<void>;
41
+ fetchById(sessionId: string): Promise<Session | null>;
42
+ fetchMany(query: SessionQuery): Promise<Session[]>;
43
+ update(sessionId: string, patch: Partial<Session>): Promise<Session>;
44
+ delete(sessionId: string): Promise<void>;
45
+ }
46
+ export interface AffinityResolver {
47
+ resolve(userId: string, surfaceId?: string): Promise<Session | null>;
48
+ }
49
+ export interface SessionStoreConfig {
50
+ adapter: SessionStoreAdapter;
51
+ defaultTtlMs?: number;
52
+ }
53
+ export interface SessionResolvableMessage {
54
+ userId: string;
55
+ workspaceId?: string;
56
+ surfaceId: string;
57
+ }
58
+ export declare class SessionNotFoundError extends Error {
59
+ readonly sessionId: string;
60
+ constructor(sessionId: string);
61
+ }
62
+ export declare class SessionConflictError extends Error {
63
+ readonly sessionId: string;
64
+ constructor(sessionId: string);
65
+ }
66
+ export declare class SessionStateError extends Error {
67
+ readonly sessionId: string;
68
+ readonly currentState: SessionState;
69
+ readonly attemptedTransition: string;
70
+ constructor(sessionId: string, currentState: SessionState, attemptedTransition: string);
71
+ }
package/dist/types.js ADDED
@@ -0,0 +1,28 @@
1
+ export class SessionNotFoundError extends Error {
2
+ sessionId;
3
+ constructor(sessionId) {
4
+ super(`Session not found: ${sessionId}`);
5
+ this.sessionId = sessionId;
6
+ this.name = 'SessionNotFoundError';
7
+ }
8
+ }
9
+ export class SessionConflictError extends Error {
10
+ sessionId;
11
+ constructor(sessionId) {
12
+ super(`Session already exists: ${sessionId}`);
13
+ this.sessionId = sessionId;
14
+ this.name = 'SessionConflictError';
15
+ }
16
+ }
17
+ export class SessionStateError extends Error {
18
+ sessionId;
19
+ currentState;
20
+ attemptedTransition;
21
+ constructor(sessionId, currentState, attemptedTransition) {
22
+ super(`Invalid transition '${attemptedTransition}' from state '${currentState}' for session ${sessionId}`);
23
+ this.sessionId = sessionId;
24
+ this.currentState = currentState;
25
+ this.attemptedTransition = attemptedTransition;
26
+ this.name = 'SessionStateError';
27
+ }
28
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-assistant/sessions",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Session lifecycle, storage, and affinity for Agent Assistant SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",