@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/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
+ }