@defai.digital/session-domain 13.0.3
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/LICENSE +214 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/manager.d.ts +99 -0
- package/dist/manager.d.ts.map +1 -0
- package/dist/manager.js +609 -0
- package/dist/manager.js.map +1 -0
- package/dist/store.d.ts +67 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +188 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +155 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/package.json +48 -0
- package/src/index.ts +75 -0
- package/src/manager.ts +834 -0
- package/src/store.ts +240 -0
- package/src/types.ts +214 -0
package/src/manager.ts
ADDED
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager Implementation
|
|
3
|
+
*
|
|
4
|
+
* Manages session lifecycle, participants, and tasks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type Session,
|
|
9
|
+
type SessionTask,
|
|
10
|
+
type SessionParticipant,
|
|
11
|
+
type SessionEvent,
|
|
12
|
+
type CreateSessionInput,
|
|
13
|
+
type JoinSessionInput,
|
|
14
|
+
type StartTaskInput,
|
|
15
|
+
type CompleteTaskInput,
|
|
16
|
+
type FailTaskInput,
|
|
17
|
+
SessionErrorCode,
|
|
18
|
+
isValidSessionTransition,
|
|
19
|
+
isValidTaskTransition,
|
|
20
|
+
} from '@defai.digital/contracts';
|
|
21
|
+
import type {
|
|
22
|
+
SessionManager,
|
|
23
|
+
SessionStore,
|
|
24
|
+
SessionFilter,
|
|
25
|
+
SessionFailure,
|
|
26
|
+
SessionDomainConfig,
|
|
27
|
+
SessionEventEmitter,
|
|
28
|
+
} from './types.js';
|
|
29
|
+
import type {
|
|
30
|
+
RunRecord,
|
|
31
|
+
RunStatus,
|
|
32
|
+
HistoryQuery,
|
|
33
|
+
} from '@defai.digital/contracts';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Default session manager implementation
|
|
37
|
+
*
|
|
38
|
+
* Emits events for all state changes when an event emitter is provided.
|
|
39
|
+
* INV-SESS-EVENT: All state changes MUST emit corresponding events
|
|
40
|
+
*/
|
|
41
|
+
export class DefaultSessionManager implements SessionManager {
|
|
42
|
+
private readonly eventEmitter: SessionEventEmitter | undefined;
|
|
43
|
+
private eventVersion = 1;
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
private readonly store: SessionStore,
|
|
47
|
+
private readonly config: SessionDomainConfig,
|
|
48
|
+
eventEmitter?: SessionEventEmitter
|
|
49
|
+
) {
|
|
50
|
+
this.eventEmitter = eventEmitter;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Emits a session event if an event emitter is configured
|
|
55
|
+
* INV-SESS-EVENT: Events emitted for all state changes
|
|
56
|
+
*
|
|
57
|
+
* Event structure follows the contract schema:
|
|
58
|
+
* - aggregateId: the session ID
|
|
59
|
+
* - type: 'session.created' | 'session.agentJoined' | etc.
|
|
60
|
+
* - payload: discriminated union with type: 'created' | 'agentJoined' | etc.
|
|
61
|
+
*/
|
|
62
|
+
private emitEvent(
|
|
63
|
+
type: SessionEvent['type'],
|
|
64
|
+
sessionId: string,
|
|
65
|
+
payload: SessionEvent['payload'],
|
|
66
|
+
correlationId?: string
|
|
67
|
+
): void {
|
|
68
|
+
if (this.eventEmitter === undefined) return;
|
|
69
|
+
|
|
70
|
+
const event: SessionEvent = {
|
|
71
|
+
eventId: crypto.randomUUID(),
|
|
72
|
+
aggregateId: sessionId,
|
|
73
|
+
type,
|
|
74
|
+
payload,
|
|
75
|
+
timestamp: new Date().toISOString(),
|
|
76
|
+
version: this.eventVersion++,
|
|
77
|
+
correlationId: correlationId ?? crypto.randomUUID(),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
this.eventEmitter.emit(event);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create a new session
|
|
85
|
+
*/
|
|
86
|
+
async createSession(input: CreateSessionInput): Promise<Session> {
|
|
87
|
+
const session = await this.store.create(input);
|
|
88
|
+
|
|
89
|
+
// Emit session.created event
|
|
90
|
+
this.emitEvent('session.created', session.sessionId, {
|
|
91
|
+
type: 'created',
|
|
92
|
+
initiator: input.initiator,
|
|
93
|
+
task: input.task,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return session;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get a session by ID
|
|
101
|
+
*/
|
|
102
|
+
async getSession(sessionId: string): Promise<Session | undefined> {
|
|
103
|
+
return this.store.get(sessionId);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Join an existing session
|
|
108
|
+
*/
|
|
109
|
+
async joinSession(input: JoinSessionInput): Promise<Session> {
|
|
110
|
+
const session = await this.store.get(input.sessionId);
|
|
111
|
+
|
|
112
|
+
if (session === undefined) {
|
|
113
|
+
throw new SessionError(
|
|
114
|
+
SessionErrorCode.SESSION_NOT_FOUND,
|
|
115
|
+
`Session ${input.sessionId} not found`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (session.status !== 'active') {
|
|
120
|
+
throw new SessionError(
|
|
121
|
+
SessionErrorCode.SESSION_ALREADY_COMPLETED,
|
|
122
|
+
`Session ${input.sessionId} is ${session.status}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check if already a participant
|
|
127
|
+
const existingParticipant = session.participants.find(
|
|
128
|
+
(p) => p.agentId === input.agentId && p.leftAt === undefined
|
|
129
|
+
);
|
|
130
|
+
if (existingParticipant !== undefined) {
|
|
131
|
+
return session; // Already joined
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check max participants
|
|
135
|
+
const activeParticipants = session.participants.filter(
|
|
136
|
+
(p) => p.leftAt === undefined
|
|
137
|
+
);
|
|
138
|
+
if (activeParticipants.length >= this.config.maxParticipants) {
|
|
139
|
+
throw new SessionError(
|
|
140
|
+
SessionErrorCode.SESSION_MAX_PARTICIPANTS,
|
|
141
|
+
`Session ${input.sessionId} has reached max participants (${this.config.maxParticipants})`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Add participant
|
|
146
|
+
const newParticipant: SessionParticipant = {
|
|
147
|
+
agentId: input.agentId,
|
|
148
|
+
role: input.role ?? 'collaborator',
|
|
149
|
+
joinedAt: new Date().toISOString(),
|
|
150
|
+
tasks: [],
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const updatedSession: Session = {
|
|
154
|
+
sessionId: session.sessionId,
|
|
155
|
+
initiator: session.initiator,
|
|
156
|
+
task: session.task,
|
|
157
|
+
participants: [...session.participants, newParticipant],
|
|
158
|
+
status: session.status,
|
|
159
|
+
createdAt: session.createdAt,
|
|
160
|
+
updatedAt: new Date().toISOString(),
|
|
161
|
+
completedAt: session.completedAt,
|
|
162
|
+
version: session.version + 1,
|
|
163
|
+
workspace: session.workspace,
|
|
164
|
+
metadata: session.metadata,
|
|
165
|
+
appliedPolicies: session.appliedPolicies ?? [],
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
await this.store.update(input.sessionId, updatedSession);
|
|
169
|
+
|
|
170
|
+
// Emit session.agentJoined event
|
|
171
|
+
this.emitEvent('session.agentJoined', input.sessionId, {
|
|
172
|
+
type: 'agentJoined',
|
|
173
|
+
agentId: input.agentId,
|
|
174
|
+
role: newParticipant.role,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return updatedSession;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Leave a session
|
|
182
|
+
*/
|
|
183
|
+
async leaveSession(sessionId: string, agentId: string): Promise<Session> {
|
|
184
|
+
const session = await this.store.get(sessionId);
|
|
185
|
+
|
|
186
|
+
if (session === undefined) {
|
|
187
|
+
throw new SessionError(
|
|
188
|
+
SessionErrorCode.SESSION_NOT_FOUND,
|
|
189
|
+
`Session ${sessionId} not found`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const participantIndex = session.participants.findIndex(
|
|
194
|
+
(p) => p.agentId === agentId && p.leftAt === undefined
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
if (participantIndex === -1) {
|
|
198
|
+
throw new SessionError(
|
|
199
|
+
SessionErrorCode.SESSION_AGENT_NOT_PARTICIPANT,
|
|
200
|
+
`Agent ${agentId} is not a participant in session ${sessionId}`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Update participant with leftAt timestamp
|
|
205
|
+
const participant = session.participants[participantIndex];
|
|
206
|
+
if (participant === undefined) {
|
|
207
|
+
throw new SessionError(
|
|
208
|
+
SessionErrorCode.SESSION_AGENT_NOT_PARTICIPANT,
|
|
209
|
+
`Agent ${agentId} is not a participant in session ${sessionId}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const updatedParticipant: SessionParticipant = {
|
|
214
|
+
agentId: participant.agentId,
|
|
215
|
+
role: participant.role,
|
|
216
|
+
joinedAt: participant.joinedAt,
|
|
217
|
+
leftAt: new Date().toISOString(),
|
|
218
|
+
tasks: participant.tasks,
|
|
219
|
+
metadata: participant.metadata,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const updatedParticipants = [...session.participants];
|
|
223
|
+
updatedParticipants[participantIndex] = updatedParticipant;
|
|
224
|
+
|
|
225
|
+
const updatedSession: Session = {
|
|
226
|
+
sessionId: session.sessionId,
|
|
227
|
+
initiator: session.initiator,
|
|
228
|
+
task: session.task,
|
|
229
|
+
participants: updatedParticipants,
|
|
230
|
+
status: session.status,
|
|
231
|
+
createdAt: session.createdAt,
|
|
232
|
+
updatedAt: new Date().toISOString(),
|
|
233
|
+
completedAt: session.completedAt,
|
|
234
|
+
version: session.version + 1,
|
|
235
|
+
workspace: session.workspace,
|
|
236
|
+
metadata: session.metadata,
|
|
237
|
+
appliedPolicies: session.appliedPolicies ?? [],
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
await this.store.update(sessionId, updatedSession);
|
|
241
|
+
|
|
242
|
+
// Emit session.agentLeft event
|
|
243
|
+
this.emitEvent('session.agentLeft', sessionId, {
|
|
244
|
+
type: 'agentLeft',
|
|
245
|
+
agentId,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
return updatedSession;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Start a task within a session
|
|
253
|
+
*/
|
|
254
|
+
async startTask(input: StartTaskInput): Promise<SessionTask> {
|
|
255
|
+
const session = await this.store.get(input.sessionId);
|
|
256
|
+
|
|
257
|
+
if (session === undefined) {
|
|
258
|
+
throw new SessionError(
|
|
259
|
+
SessionErrorCode.SESSION_NOT_FOUND,
|
|
260
|
+
`Session ${input.sessionId} not found`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (session.status !== 'active') {
|
|
265
|
+
throw new SessionError(
|
|
266
|
+
SessionErrorCode.SESSION_ALREADY_COMPLETED,
|
|
267
|
+
`Session ${input.sessionId} is ${session.status}`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Find participant
|
|
272
|
+
const participantIndex = session.participants.findIndex(
|
|
273
|
+
(p) => p.agentId === input.agentId && p.leftAt === undefined
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
if (participantIndex === -1) {
|
|
277
|
+
throw new SessionError(
|
|
278
|
+
SessionErrorCode.SESSION_AGENT_NOT_PARTICIPANT,
|
|
279
|
+
`Agent ${input.agentId} is not a participant in session ${input.sessionId}`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const participant = session.participants[participantIndex];
|
|
284
|
+
if (participant === undefined) {
|
|
285
|
+
throw new SessionError(
|
|
286
|
+
SessionErrorCode.SESSION_AGENT_NOT_PARTICIPANT,
|
|
287
|
+
`Agent ${input.agentId} is not a participant in session ${input.sessionId}`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Check task limit
|
|
292
|
+
if (participant.tasks.length >= this.config.maxTasksPerParticipant) {
|
|
293
|
+
throw new SessionError(
|
|
294
|
+
SessionErrorCode.SESSION_VALIDATION_ERROR,
|
|
295
|
+
`Agent ${input.agentId} has reached max tasks (${this.config.maxTasksPerParticipant})`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Create task
|
|
300
|
+
const task: SessionTask = {
|
|
301
|
+
taskId: crypto.randomUUID(),
|
|
302
|
+
title: input.title,
|
|
303
|
+
description: input.description,
|
|
304
|
+
status: 'running',
|
|
305
|
+
startedAt: new Date().toISOString(),
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Update participant's tasks
|
|
309
|
+
const updatedParticipant: SessionParticipant = {
|
|
310
|
+
agentId: participant.agentId,
|
|
311
|
+
role: participant.role,
|
|
312
|
+
joinedAt: participant.joinedAt,
|
|
313
|
+
leftAt: participant.leftAt,
|
|
314
|
+
tasks: [...participant.tasks, task],
|
|
315
|
+
metadata: participant.metadata,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const updatedParticipants = [...session.participants];
|
|
319
|
+
updatedParticipants[participantIndex] = updatedParticipant;
|
|
320
|
+
|
|
321
|
+
const updatedSession: Session = {
|
|
322
|
+
sessionId: session.sessionId,
|
|
323
|
+
initiator: session.initiator,
|
|
324
|
+
task: session.task,
|
|
325
|
+
participants: updatedParticipants,
|
|
326
|
+
status: session.status,
|
|
327
|
+
createdAt: session.createdAt,
|
|
328
|
+
updatedAt: new Date().toISOString(),
|
|
329
|
+
completedAt: session.completedAt,
|
|
330
|
+
version: session.version + 1,
|
|
331
|
+
workspace: session.workspace,
|
|
332
|
+
metadata: session.metadata,
|
|
333
|
+
appliedPolicies: session.appliedPolicies ?? [],
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
await this.store.update(input.sessionId, updatedSession);
|
|
337
|
+
|
|
338
|
+
// Emit session.taskStarted event
|
|
339
|
+
this.emitEvent('session.taskStarted', input.sessionId, {
|
|
340
|
+
type: 'taskStarted',
|
|
341
|
+
taskId: task.taskId,
|
|
342
|
+
agentId: input.agentId,
|
|
343
|
+
title: task.title,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
return task;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Complete a task
|
|
351
|
+
*/
|
|
352
|
+
async completeTask(input: CompleteTaskInput): Promise<SessionTask> {
|
|
353
|
+
const session = await this.store.get(input.sessionId);
|
|
354
|
+
|
|
355
|
+
if (session === undefined) {
|
|
356
|
+
throw new SessionError(
|
|
357
|
+
SessionErrorCode.SESSION_NOT_FOUND,
|
|
358
|
+
`Session ${input.sessionId} not found`
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Find task across all participants
|
|
363
|
+
let foundParticipantIndex = -1;
|
|
364
|
+
let foundTaskIndex = -1;
|
|
365
|
+
|
|
366
|
+
for (let pi = 0; pi < session.participants.length; pi++) {
|
|
367
|
+
const participant = session.participants[pi];
|
|
368
|
+
if (participant === undefined) continue;
|
|
369
|
+
const ti = participant.tasks.findIndex((t) => t.taskId === input.taskId);
|
|
370
|
+
if (ti !== -1) {
|
|
371
|
+
foundParticipantIndex = pi;
|
|
372
|
+
foundTaskIndex = ti;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (foundParticipantIndex === -1 || foundTaskIndex === -1) {
|
|
378
|
+
throw new SessionError(
|
|
379
|
+
SessionErrorCode.SESSION_TASK_NOT_FOUND,
|
|
380
|
+
`Task ${input.taskId} not found in session ${input.sessionId}`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const participant = session.participants[foundParticipantIndex];
|
|
385
|
+
if (participant === undefined) {
|
|
386
|
+
throw new SessionError(
|
|
387
|
+
SessionErrorCode.SESSION_TASK_NOT_FOUND,
|
|
388
|
+
`Task ${input.taskId} not found in session ${input.sessionId}`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const task = participant.tasks[foundTaskIndex];
|
|
393
|
+
if (task === undefined) {
|
|
394
|
+
throw new SessionError(
|
|
395
|
+
SessionErrorCode.SESSION_TASK_NOT_FOUND,
|
|
396
|
+
`Task ${input.taskId} not found in session ${input.sessionId}`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Validate transition
|
|
401
|
+
if (!isValidTaskTransition(task.status, 'completed')) {
|
|
402
|
+
throw new SessionError(
|
|
403
|
+
SessionErrorCode.SESSION_INVALID_TRANSITION,
|
|
404
|
+
`Cannot transition task from ${task.status} to completed`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const now = new Date().toISOString();
|
|
409
|
+
const startTime = new Date(task.startedAt).getTime();
|
|
410
|
+
const durationMs = new Date(now).getTime() - startTime;
|
|
411
|
+
|
|
412
|
+
// Update task
|
|
413
|
+
const updatedTask: SessionTask = {
|
|
414
|
+
taskId: task.taskId,
|
|
415
|
+
title: task.title,
|
|
416
|
+
description: task.description,
|
|
417
|
+
status: 'completed',
|
|
418
|
+
startedAt: task.startedAt,
|
|
419
|
+
completedAt: now,
|
|
420
|
+
durationMs,
|
|
421
|
+
output: input.output,
|
|
422
|
+
error: task.error,
|
|
423
|
+
metadata: task.metadata,
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// Update session
|
|
427
|
+
const updatedTasks = [...participant.tasks];
|
|
428
|
+
updatedTasks[foundTaskIndex] = updatedTask;
|
|
429
|
+
|
|
430
|
+
const updatedParticipant: SessionParticipant = {
|
|
431
|
+
agentId: participant.agentId,
|
|
432
|
+
role: participant.role,
|
|
433
|
+
joinedAt: participant.joinedAt,
|
|
434
|
+
leftAt: participant.leftAt,
|
|
435
|
+
tasks: updatedTasks,
|
|
436
|
+
metadata: participant.metadata,
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const updatedParticipants = [...session.participants];
|
|
440
|
+
updatedParticipants[foundParticipantIndex] = updatedParticipant;
|
|
441
|
+
|
|
442
|
+
const updatedSession: Session = {
|
|
443
|
+
sessionId: session.sessionId,
|
|
444
|
+
initiator: session.initiator,
|
|
445
|
+
task: session.task,
|
|
446
|
+
participants: updatedParticipants,
|
|
447
|
+
status: session.status,
|
|
448
|
+
createdAt: session.createdAt,
|
|
449
|
+
updatedAt: now,
|
|
450
|
+
completedAt: session.completedAt,
|
|
451
|
+
version: session.version + 1,
|
|
452
|
+
workspace: session.workspace,
|
|
453
|
+
metadata: session.metadata,
|
|
454
|
+
appliedPolicies: session.appliedPolicies ?? [],
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
await this.store.update(input.sessionId, updatedSession);
|
|
458
|
+
|
|
459
|
+
// Emit session.taskCompleted event
|
|
460
|
+
this.emitEvent('session.taskCompleted', input.sessionId, {
|
|
461
|
+
type: 'taskCompleted',
|
|
462
|
+
taskId: input.taskId,
|
|
463
|
+
agentId: participant.agentId,
|
|
464
|
+
durationMs: updatedTask.durationMs ?? 0,
|
|
465
|
+
output: input.output,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
return updatedTask;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Fail a task
|
|
473
|
+
*/
|
|
474
|
+
async failTask(input: FailTaskInput): Promise<SessionTask> {
|
|
475
|
+
const session = await this.store.get(input.sessionId);
|
|
476
|
+
|
|
477
|
+
if (session === undefined) {
|
|
478
|
+
throw new SessionError(
|
|
479
|
+
SessionErrorCode.SESSION_NOT_FOUND,
|
|
480
|
+
`Session ${input.sessionId} not found`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Find task across all participants
|
|
485
|
+
let foundParticipantIndex = -1;
|
|
486
|
+
let foundTaskIndex = -1;
|
|
487
|
+
|
|
488
|
+
for (let pi = 0; pi < session.participants.length; pi++) {
|
|
489
|
+
const participant = session.participants[pi];
|
|
490
|
+
if (participant === undefined) continue;
|
|
491
|
+
const ti = participant.tasks.findIndex((t) => t.taskId === input.taskId);
|
|
492
|
+
if (ti !== -1) {
|
|
493
|
+
foundParticipantIndex = pi;
|
|
494
|
+
foundTaskIndex = ti;
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (foundParticipantIndex === -1 || foundTaskIndex === -1) {
|
|
500
|
+
throw new SessionError(
|
|
501
|
+
SessionErrorCode.SESSION_TASK_NOT_FOUND,
|
|
502
|
+
`Task ${input.taskId} not found in session ${input.sessionId}`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const participant = session.participants[foundParticipantIndex];
|
|
507
|
+
if (participant === undefined) {
|
|
508
|
+
throw new SessionError(
|
|
509
|
+
SessionErrorCode.SESSION_TASK_NOT_FOUND,
|
|
510
|
+
`Task ${input.taskId} not found in session ${input.sessionId}`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const task = participant.tasks[foundTaskIndex];
|
|
515
|
+
if (task === undefined) {
|
|
516
|
+
throw new SessionError(
|
|
517
|
+
SessionErrorCode.SESSION_TASK_NOT_FOUND,
|
|
518
|
+
`Task ${input.taskId} not found in session ${input.sessionId}`
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Validate transition
|
|
523
|
+
if (!isValidTaskTransition(task.status, 'failed')) {
|
|
524
|
+
throw new SessionError(
|
|
525
|
+
SessionErrorCode.SESSION_INVALID_TRANSITION,
|
|
526
|
+
`Cannot transition task from ${task.status} to failed`
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const now = new Date().toISOString();
|
|
531
|
+
const startTime = new Date(task.startedAt).getTime();
|
|
532
|
+
const durationMs = new Date(now).getTime() - startTime;
|
|
533
|
+
|
|
534
|
+
// Update task
|
|
535
|
+
const updatedTask: SessionTask = {
|
|
536
|
+
taskId: task.taskId,
|
|
537
|
+
title: task.title,
|
|
538
|
+
description: task.description,
|
|
539
|
+
status: 'failed',
|
|
540
|
+
startedAt: task.startedAt,
|
|
541
|
+
completedAt: now,
|
|
542
|
+
durationMs,
|
|
543
|
+
output: task.output,
|
|
544
|
+
error: input.error,
|
|
545
|
+
metadata: task.metadata,
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
// Update session
|
|
549
|
+
const updatedTasks = [...participant.tasks];
|
|
550
|
+
updatedTasks[foundTaskIndex] = updatedTask;
|
|
551
|
+
|
|
552
|
+
const updatedParticipant: SessionParticipant = {
|
|
553
|
+
agentId: participant.agentId,
|
|
554
|
+
role: participant.role,
|
|
555
|
+
joinedAt: participant.joinedAt,
|
|
556
|
+
leftAt: participant.leftAt,
|
|
557
|
+
tasks: updatedTasks,
|
|
558
|
+
metadata: participant.metadata,
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const updatedParticipants = [...session.participants];
|
|
562
|
+
updatedParticipants[foundParticipantIndex] = updatedParticipant;
|
|
563
|
+
|
|
564
|
+
const updatedSession: Session = {
|
|
565
|
+
sessionId: session.sessionId,
|
|
566
|
+
initiator: session.initiator,
|
|
567
|
+
task: session.task,
|
|
568
|
+
participants: updatedParticipants,
|
|
569
|
+
status: session.status,
|
|
570
|
+
createdAt: session.createdAt,
|
|
571
|
+
updatedAt: now,
|
|
572
|
+
completedAt: session.completedAt,
|
|
573
|
+
version: session.version + 1,
|
|
574
|
+
workspace: session.workspace,
|
|
575
|
+
metadata: session.metadata,
|
|
576
|
+
appliedPolicies: session.appliedPolicies ?? [],
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
await this.store.update(input.sessionId, updatedSession);
|
|
580
|
+
|
|
581
|
+
// Emit session.taskFailed event
|
|
582
|
+
this.emitEvent('session.taskFailed', input.sessionId, {
|
|
583
|
+
type: 'taskFailed',
|
|
584
|
+
taskId: input.taskId,
|
|
585
|
+
agentId: participant.agentId,
|
|
586
|
+
error: input.error,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
return updatedTask;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Complete a session
|
|
594
|
+
*/
|
|
595
|
+
async completeSession(sessionId: string, summary?: string): Promise<Session> {
|
|
596
|
+
const session = await this.store.get(sessionId);
|
|
597
|
+
|
|
598
|
+
if (session === undefined) {
|
|
599
|
+
throw new SessionError(
|
|
600
|
+
SessionErrorCode.SESSION_NOT_FOUND,
|
|
601
|
+
`Session ${sessionId} not found`
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (!isValidSessionTransition(session.status, 'completed')) {
|
|
606
|
+
throw new SessionError(
|
|
607
|
+
SessionErrorCode.SESSION_INVALID_TRANSITION,
|
|
608
|
+
`Cannot transition session from ${session.status} to completed`
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const now = new Date().toISOString();
|
|
613
|
+
|
|
614
|
+
const updatedSession: Session = {
|
|
615
|
+
sessionId: session.sessionId,
|
|
616
|
+
initiator: session.initiator,
|
|
617
|
+
task: session.task,
|
|
618
|
+
participants: session.participants,
|
|
619
|
+
status: 'completed',
|
|
620
|
+
createdAt: session.createdAt,
|
|
621
|
+
updatedAt: now,
|
|
622
|
+
completedAt: now,
|
|
623
|
+
version: session.version + 1,
|
|
624
|
+
workspace: session.workspace,
|
|
625
|
+
metadata: summary !== undefined
|
|
626
|
+
? { ...session.metadata, summary }
|
|
627
|
+
: session.metadata,
|
|
628
|
+
appliedPolicies: session.appliedPolicies ?? [],
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
await this.store.update(sessionId, updatedSession);
|
|
632
|
+
|
|
633
|
+
// Calculate session duration
|
|
634
|
+
const startTime = new Date(session.createdAt).getTime();
|
|
635
|
+
const durationMs = new Date(now).getTime() - startTime;
|
|
636
|
+
|
|
637
|
+
// Emit session.completed event
|
|
638
|
+
this.emitEvent('session.completed', sessionId, {
|
|
639
|
+
type: 'completed',
|
|
640
|
+
summary,
|
|
641
|
+
durationMs,
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
return updatedSession;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Fail a session
|
|
649
|
+
*/
|
|
650
|
+
async failSession(sessionId: string, error: SessionFailure): Promise<Session> {
|
|
651
|
+
const session = await this.store.get(sessionId);
|
|
652
|
+
|
|
653
|
+
if (session === undefined) {
|
|
654
|
+
throw new SessionError(
|
|
655
|
+
SessionErrorCode.SESSION_NOT_FOUND,
|
|
656
|
+
`Session ${sessionId} not found`
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (!isValidSessionTransition(session.status, 'failed')) {
|
|
661
|
+
throw new SessionError(
|
|
662
|
+
SessionErrorCode.SESSION_INVALID_TRANSITION,
|
|
663
|
+
`Cannot transition session from ${session.status} to failed`
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const now = new Date().toISOString();
|
|
668
|
+
|
|
669
|
+
const updatedSession: Session = {
|
|
670
|
+
sessionId: session.sessionId,
|
|
671
|
+
initiator: session.initiator,
|
|
672
|
+
task: session.task,
|
|
673
|
+
participants: session.participants,
|
|
674
|
+
status: 'failed',
|
|
675
|
+
createdAt: session.createdAt,
|
|
676
|
+
updatedAt: now,
|
|
677
|
+
completedAt: now,
|
|
678
|
+
version: session.version + 1,
|
|
679
|
+
workspace: session.workspace,
|
|
680
|
+
metadata: {
|
|
681
|
+
...session.metadata,
|
|
682
|
+
error: {
|
|
683
|
+
code: error.code,
|
|
684
|
+
message: error.message,
|
|
685
|
+
taskId: error.taskId,
|
|
686
|
+
details: error.details,
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
appliedPolicies: session.appliedPolicies ?? [],
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
await this.store.update(sessionId, updatedSession);
|
|
693
|
+
|
|
694
|
+
// Emit session.failed event
|
|
695
|
+
this.emitEvent('session.failed', sessionId, {
|
|
696
|
+
type: 'failed',
|
|
697
|
+
error: {
|
|
698
|
+
code: error.code,
|
|
699
|
+
message: error.message,
|
|
700
|
+
taskId: error.taskId,
|
|
701
|
+
retryable: error.retryable,
|
|
702
|
+
details: error.details,
|
|
703
|
+
},
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
return updatedSession;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* List sessions
|
|
711
|
+
*/
|
|
712
|
+
async listSessions(filter?: SessionFilter): Promise<Session[]> {
|
|
713
|
+
return this.store.list(filter);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Get run history across all sessions
|
|
718
|
+
* Aggregates tasks from all participants across sessions
|
|
719
|
+
*/
|
|
720
|
+
async getRunHistory(options?: HistoryQuery): Promise<RunRecord[]> {
|
|
721
|
+
const sessions = await this.store.list();
|
|
722
|
+
const records: RunRecord[] = [];
|
|
723
|
+
|
|
724
|
+
for (const session of sessions) {
|
|
725
|
+
for (const participant of session.participants) {
|
|
726
|
+
// Apply agent filter if specified
|
|
727
|
+
if (options?.agentId !== undefined && participant.agentId !== options.agentId) {
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
for (const task of participant.tasks) {
|
|
732
|
+
// Map task status to run status
|
|
733
|
+
const runStatus = this.mapTaskStatusToRunStatus(task.status);
|
|
734
|
+
|
|
735
|
+
// Apply status filter if specified
|
|
736
|
+
if (options?.status !== undefined && runStatus !== options.status) {
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Build record with optional properties only when defined
|
|
741
|
+
const record: RunRecord = {
|
|
742
|
+
runId: task.taskId,
|
|
743
|
+
agentId: participant.agentId,
|
|
744
|
+
sessionId: session.sessionId,
|
|
745
|
+
task: task.title,
|
|
746
|
+
status: runStatus,
|
|
747
|
+
startedAt: task.startedAt,
|
|
748
|
+
stepsCompleted: task.status === 'completed' ? 1 : 0,
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
// Add optional properties only when they have values
|
|
752
|
+
if (task.error?.message !== undefined) {
|
|
753
|
+
record.error = task.error.message;
|
|
754
|
+
}
|
|
755
|
+
if (task.completedAt !== undefined) {
|
|
756
|
+
record.completedAt = task.completedAt;
|
|
757
|
+
}
|
|
758
|
+
if (task.durationMs !== undefined) {
|
|
759
|
+
record.durationMs = task.durationMs;
|
|
760
|
+
}
|
|
761
|
+
if (task.metadata?.tokensUsed !== undefined) {
|
|
762
|
+
record.tokensUsed = task.metadata.tokensUsed as number;
|
|
763
|
+
}
|
|
764
|
+
if (task.metadata?.providerId !== undefined) {
|
|
765
|
+
record.providerId = task.metadata.providerId as string;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
records.push(record);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Sort by startedAt descending (most recent first)
|
|
774
|
+
records.sort((a, b) => {
|
|
775
|
+
return new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime();
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
// Apply limit
|
|
779
|
+
const limit = options?.limit ?? 50;
|
|
780
|
+
return records.slice(0, limit);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Count active sessions
|
|
785
|
+
*/
|
|
786
|
+
async countActiveSessions(): Promise<number> {
|
|
787
|
+
const sessions = await this.store.list({ status: 'active' });
|
|
788
|
+
return sessions.length;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Maps SessionTask status to RunStatus
|
|
793
|
+
*/
|
|
794
|
+
private mapTaskStatusToRunStatus(taskStatus: string): RunStatus {
|
|
795
|
+
switch (taskStatus) {
|
|
796
|
+
case 'running':
|
|
797
|
+
case 'pending':
|
|
798
|
+
return 'running';
|
|
799
|
+
case 'completed':
|
|
800
|
+
return 'completed';
|
|
801
|
+
case 'failed':
|
|
802
|
+
return 'failed';
|
|
803
|
+
default:
|
|
804
|
+
return 'running';
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Session error
|
|
811
|
+
*/
|
|
812
|
+
export class SessionError extends Error {
|
|
813
|
+
constructor(
|
|
814
|
+
public readonly code: string,
|
|
815
|
+
message: string
|
|
816
|
+
) {
|
|
817
|
+
super(message);
|
|
818
|
+
this.name = 'SessionError';
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Creates a new session manager
|
|
824
|
+
* @param store - Session store implementation
|
|
825
|
+
* @param config - Session domain configuration
|
|
826
|
+
* @param eventEmitter - Optional event emitter for state change events
|
|
827
|
+
*/
|
|
828
|
+
export function createSessionManager(
|
|
829
|
+
store: SessionStore,
|
|
830
|
+
config: SessionDomainConfig,
|
|
831
|
+
eventEmitter?: SessionEventEmitter
|
|
832
|
+
): SessionManager {
|
|
833
|
+
return new DefaultSessionManager(store, config, eventEmitter);
|
|
834
|
+
}
|