@agentplat/rooms 0.2.0-beta.1

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,1096 @@
1
+ import { AgentPlatError } from '@agentplat/core';
2
+ import { BoundedContextBuilder } from './context.js';
3
+ import { BasicPolicyEngine } from './policy.js';
4
+ const participantTypes = new Set(['human', 'agent']);
5
+ const messageRoles = new Set(['human', 'agent', 'system', 'tool']);
6
+ const actionLevels = new Set(['read', 'draft', 'execute', 'external_write']);
7
+ const approvalTargetTypes = new Set(['room', 'task', 'artifact', 'action']);
8
+ const memoryScopes = new Set([
9
+ 'ephemeral',
10
+ 'agent',
11
+ 'role',
12
+ 'room',
13
+ 'artifact',
14
+ 'organization',
15
+ ]);
16
+ const memoryRetentions = new Set(['transient', 'session', 'durable', 'until']);
17
+ export class RoomService {
18
+ repository;
19
+ runtime;
20
+ eventPublisher;
21
+ policyEngine;
22
+ contextBuilder;
23
+ idGenerator;
24
+ clock;
25
+ runTimeoutMs;
26
+ runLeaseGraceMs;
27
+ onEventPublishError;
28
+ constructor(options) {
29
+ this.repository = options.repository;
30
+ this.runtime = options.runtime;
31
+ this.eventPublisher = options.eventPublisher;
32
+ this.policyEngine = options.policyEngine ?? new BasicPolicyEngine();
33
+ this.idGenerator =
34
+ options.idGenerator ?? (() => globalThis.crypto.randomUUID());
35
+ this.clock = options.clock ?? (() => new Date());
36
+ this.contextBuilder =
37
+ options.contextBuilder ??
38
+ new BoundedContextBuilder({ clock: this.clock });
39
+ this.runTimeoutMs = options.runTimeoutMs ?? 300_000;
40
+ if (!Number.isInteger(this.runTimeoutMs) || this.runTimeoutMs <= 0) {
41
+ throw new AgentPlatError('VALIDATION_ERROR', 'runTimeoutMs must be a positive integer');
42
+ }
43
+ // Leave enough time for the timeout handler to fence and persist a failed
44
+ // run before another process can recover the lease.
45
+ this.runLeaseGraceMs = Math.min(30_000, Math.max(1_000, Math.ceil(this.runTimeoutMs * 0.1)));
46
+ this.onEventPublishError = options.onEventPublishError ?? (() => undefined);
47
+ }
48
+ async createRoom(tenantId, input) {
49
+ this.required(tenantId, 'tenantId');
50
+ this.required(input.title, 'title');
51
+ this.required(input.goal, 'goal');
52
+ const result = await this.repository.transaction(tenantId, async (transaction) => {
53
+ if (input.parentRoomId) {
54
+ const parent = await transaction.getRoom(tenantId, input.parentRoomId);
55
+ if (!parent)
56
+ throw this.notFound('Parent room', input.parentRoomId);
57
+ this.assertRoomWritable(parent);
58
+ }
59
+ const now = this.now();
60
+ const room = {
61
+ id: input.id ?? this.id(),
62
+ tenantId,
63
+ parentRoomId: input.parentRoomId,
64
+ title: input.title.trim(),
65
+ goal: input.goal.trim(),
66
+ status: 'active',
67
+ metadata: input.metadata,
68
+ createdBy: input.createdBy,
69
+ createdAt: now,
70
+ updatedAt: now,
71
+ };
72
+ const events = [
73
+ this.event(tenantId, room.id, input.parentRoomId ? 'subroom_created' : 'room_created', {
74
+ roomId: room.id,
75
+ parentRoomId: room.parentRoomId ?? null,
76
+ title: room.title,
77
+ }, input.createdBy),
78
+ ];
79
+ await transaction.insertRoom(room);
80
+ await this.appendEvents(transaction, events);
81
+ return { value: room, events };
82
+ });
83
+ await this.publish(result.events);
84
+ return result.value;
85
+ }
86
+ async updateRoom(tenantId, roomId, input) {
87
+ if (input.title !== undefined)
88
+ this.required(input.title, 'title');
89
+ if (input.goal !== undefined)
90
+ this.required(input.goal, 'goal');
91
+ return this.mutate(tenantId, async (transaction) => {
92
+ const room = await this.requireRoom(transaction, tenantId, roomId);
93
+ this.assertRoomWritable(room);
94
+ const updated = {
95
+ ...room,
96
+ title: input.title?.trim() || room.title,
97
+ goal: input.goal?.trim() || room.goal,
98
+ metadata: input.metadata ?? room.metadata,
99
+ updatedAt: this.now(),
100
+ };
101
+ const event = this.event(tenantId, roomId, 'room_updated', { roomId }, input.actorId);
102
+ await transaction.updateRoom(updated);
103
+ await transaction.appendEvent(event);
104
+ return { value: updated, events: [event] };
105
+ });
106
+ }
107
+ async transitionRoom(tenantId, roomId, action, actorId) {
108
+ const transitions = {
109
+ pause: { from: 'active', to: 'paused', event: 'room_paused' },
110
+ resume: { from: 'paused', to: 'active', event: 'room_resumed' },
111
+ complete: { from: 'active', to: 'completed', event: 'room_completed' },
112
+ archive: { from: 'completed', to: 'archived', event: 'room_archived' },
113
+ };
114
+ if (!Object.hasOwn(transitions, action)) {
115
+ throw new AgentPlatError('VALIDATION_ERROR', 'Room transition action is not supported');
116
+ }
117
+ return this.mutate(tenantId, async (transaction) => {
118
+ const room = await this.requireRoom(transaction, tenantId, roomId);
119
+ const transition = transitions[action];
120
+ if (room.status !== transition.from) {
121
+ throw new AgentPlatError('CONFLICT', `Room cannot ${action} from status \"${room.status}\"`, { statusCode: 409 });
122
+ }
123
+ if (action === 'complete') {
124
+ const state = await transaction.getRoomState(tenantId, roomId);
125
+ if (state?.tasks.some((task) => !['completed', 'canceled'].includes(task.status)) ||
126
+ state?.approvals.some((approval) => approval.status === 'requested') ||
127
+ state?.artifacts.some((artifact) => artifact.status !== 'approved') ||
128
+ state?.childRooms.some((child) => !['completed', 'archived'].includes(child.status))) {
129
+ throw new AgentPlatError('CONFLICT', 'Complete tasks and subrooms, resolve approvals, and approve all artifacts before completing the room', { statusCode: 409 });
130
+ }
131
+ }
132
+ const now = this.now();
133
+ const updated = {
134
+ ...room,
135
+ status: transition.to,
136
+ updatedAt: now,
137
+ completedAt: action === 'complete' ? now : room.completedAt,
138
+ archivedAt: action === 'archive' ? now : room.archivedAt,
139
+ };
140
+ const event = this.event(tenantId, roomId, transition.event, { roomId, status: updated.status }, actorId);
141
+ await transaction.updateRoom(updated);
142
+ await transaction.appendEvent(event);
143
+ return { value: updated, events: [event] };
144
+ });
145
+ }
146
+ async pauseRoom(tenantId, roomId, actorId) {
147
+ return this.transitionRoom(tenantId, roomId, 'pause', actorId);
148
+ }
149
+ async resumeRoom(tenantId, roomId, actorId) {
150
+ return this.transitionRoom(tenantId, roomId, 'resume', actorId);
151
+ }
152
+ async completeRoom(tenantId, roomId, actorId) {
153
+ return this.transitionRoom(tenantId, roomId, 'complete', actorId);
154
+ }
155
+ async archiveRoom(tenantId, roomId, actorId) {
156
+ return this.transitionRoom(tenantId, roomId, 'archive', actorId);
157
+ }
158
+ async addParticipant(tenantId, roomId, input, actorId) {
159
+ this.required(input.displayName, 'displayName');
160
+ this.required(input.role, 'role');
161
+ this.stringArray(input.permissions, 'permissions');
162
+ this.stringArray(input.boundaries, 'boundaries');
163
+ if (!participantTypes.has(input.type)) {
164
+ throw new AgentPlatError('VALIDATION_ERROR', 'type must be human or agent');
165
+ }
166
+ if (!Number.isInteger(input.authorityLevel ?? 0) ||
167
+ (input.authorityLevel ?? 0) < 0) {
168
+ throw new AgentPlatError('VALIDATION_ERROR', 'authorityLevel must be a non-negative integer');
169
+ }
170
+ if (input.type === 'agent' && !input.runtime?.platform) {
171
+ throw new AgentPlatError('VALIDATION_ERROR', 'Agent participants require runtime.platform');
172
+ }
173
+ if (input.runtime)
174
+ this.required(input.runtime.platform, 'runtime.platform');
175
+ if (input.memoryScope && !memoryScopes.has(input.memoryScope)) {
176
+ throw new AgentPlatError('VALIDATION_ERROR', 'memoryScope is not supported');
177
+ }
178
+ return this.mutate(tenantId, async (transaction) => {
179
+ const room = await this.requireRoom(transaction, tenantId, roomId);
180
+ this.assertRoomWritable(room);
181
+ const id = input.id ?? this.id();
182
+ const existing = await transaction.getParticipant(tenantId, id);
183
+ const now = this.now();
184
+ const participant = existing ?? {
185
+ id,
186
+ tenantId,
187
+ type: input.type,
188
+ displayName: input.displayName.trim(),
189
+ role: input.role.trim(),
190
+ authorityLevel: input.authorityLevel ?? 0,
191
+ permissions: input.permissions ?? [],
192
+ boundaries: input.boundaries ?? [],
193
+ memoryScope: input.memoryScope,
194
+ runtime: input.runtime,
195
+ metadata: input.metadata,
196
+ createdAt: now,
197
+ updatedAt: now,
198
+ };
199
+ if (!existing)
200
+ await transaction.insertParticipant(participant);
201
+ await transaction.addRoomParticipant({
202
+ tenantId,
203
+ roomId,
204
+ participantId: id,
205
+ joinedAt: now,
206
+ });
207
+ const event = this.event(tenantId, roomId, 'participant_added', {
208
+ roomId,
209
+ participantId: id,
210
+ participantType: participant.type,
211
+ role: participant.role,
212
+ }, actorId);
213
+ await transaction.appendEvent(event);
214
+ return { value: participant, events: [event] };
215
+ });
216
+ }
217
+ async sendMessage(tenantId, roomId, input) {
218
+ this.required(input.content, 'content');
219
+ if (!messageRoles.has(input.role)) {
220
+ throw new AgentPlatError('VALIDATION_ERROR', 'role is not a supported message role');
221
+ }
222
+ return this.mutate(tenantId, async (transaction) => {
223
+ const room = await this.requireRoom(transaction, tenantId, roomId);
224
+ this.assertRoomWritable(room);
225
+ if (input.authorParticipantId) {
226
+ await this.requireRoomParticipant(transaction, tenantId, roomId, input.authorParticipantId);
227
+ }
228
+ const message = {
229
+ id: input.id ?? this.id(),
230
+ tenantId,
231
+ roomId,
232
+ authorParticipantId: input.authorParticipantId,
233
+ role: input.role,
234
+ content: input.content,
235
+ metadata: input.metadata,
236
+ createdAt: this.now(),
237
+ };
238
+ const event = this.event(tenantId, roomId, 'message_created', { roomId, messageId: message.id, role: message.role }, input.authorParticipantId);
239
+ await transaction.insertMessage(message);
240
+ await transaction.appendEvent(event);
241
+ return { value: message, events: [event] };
242
+ });
243
+ }
244
+ async createTask(tenantId, roomId, input, actorId) {
245
+ this.required(input.stepId, 'stepId');
246
+ this.required(input.instruction, 'instruction');
247
+ this.required(input.expectedOutput, 'expectedOutput');
248
+ this.required(input.expectedArtifactKind, 'expectedArtifactKind');
249
+ this.stringArray(input.dependencies, 'dependencies');
250
+ this.stringArray(input.acceptanceCriteria, 'acceptanceCriteria');
251
+ this.stringArray(input.toolIds, 'toolIds');
252
+ if (input.actionLevel && !actionLevels.has(input.actionLevel)) {
253
+ throw new AgentPlatError('VALIDATION_ERROR', 'actionLevel is not supported');
254
+ }
255
+ if (input.approvalRequired !== undefined &&
256
+ typeof input.approvalRequired !== 'boolean') {
257
+ throw new AgentPlatError('VALIDATION_ERROR', 'approvalRequired must be a boolean');
258
+ }
259
+ return this.mutate(tenantId, async (transaction) => {
260
+ const room = await this.requireRoom(transaction, tenantId, roomId);
261
+ this.assertRoomWritable(room);
262
+ if (input.assignedParticipantId) {
263
+ const assignedParticipant = await this.requireRoomParticipant(transaction, tenantId, roomId, input.assignedParticipantId);
264
+ if (assignedParticipant.type !== 'agent') {
265
+ throw new AgentPlatError('VALIDATION_ERROR', 'Executable Room tasks can only be assigned to agent participants');
266
+ }
267
+ }
268
+ for (const dependencyId of input.dependencies ?? []) {
269
+ const dependency = await transaction.getTask(tenantId, dependencyId);
270
+ if (!dependency || dependency.roomId !== roomId) {
271
+ throw new AgentPlatError('VALIDATION_ERROR', `Dependency \"${dependencyId}\" is not in this room`);
272
+ }
273
+ }
274
+ const now = this.now();
275
+ const task = {
276
+ id: input.id ?? this.id(),
277
+ tenantId,
278
+ roomId,
279
+ stepId: input.stepId,
280
+ assignedParticipantId: input.assignedParticipantId,
281
+ assignedRole: input.assignedRole,
282
+ instruction: input.instruction,
283
+ expectedOutput: input.expectedOutput,
284
+ expectedArtifactKind: input.expectedArtifactKind,
285
+ dependencies: input.dependencies ?? [],
286
+ acceptanceCriteria: input.acceptanceCriteria ?? [],
287
+ actionLevel: input.actionLevel ?? 'execute',
288
+ approvalRequired: input.approvalRequired ?? false,
289
+ toolIds: input.toolIds ?? [],
290
+ status: 'pending',
291
+ metadata: input.metadata,
292
+ createdAt: now,
293
+ updatedAt: now,
294
+ };
295
+ const events = [
296
+ this.event(tenantId, roomId, 'task_created', { roomId, taskId: task.id, stepId: task.stepId }, actorId),
297
+ ];
298
+ if (task.assignedParticipantId || task.assignedRole) {
299
+ events.push(this.event(tenantId, roomId, 'task_assigned', {
300
+ roomId,
301
+ taskId: task.id,
302
+ participantId: task.assignedParticipantId ?? null,
303
+ role: task.assignedRole ?? null,
304
+ }, actorId));
305
+ }
306
+ await transaction.insertTask(task);
307
+ await this.appendEvents(transaction, events);
308
+ return { value: task, events };
309
+ });
310
+ }
311
+ async runTask(tenantId, roomId, taskId) {
312
+ if (!this.runtime) {
313
+ throw new AgentPlatError('ADAPTER_ERROR', 'No agent runtime is configured');
314
+ }
315
+ const runId = this.id();
316
+ const snapshotId = this.id();
317
+ const claim = await this.repository.transaction(tenantId, async (transaction) => {
318
+ const state = await transaction.getRoomState(tenantId, roomId);
319
+ if (!state)
320
+ throw this.notFound('Room', roomId);
321
+ if (state.room.status !== 'active') {
322
+ throw new AgentPlatError('CONFLICT', 'Tasks can only run in active rooms', {
323
+ statusCode: 409,
324
+ });
325
+ }
326
+ let currentTask = await transaction.getTask(tenantId, taskId);
327
+ if (!currentTask || currentTask.roomId !== roomId)
328
+ throw this.notFound('Task', taskId);
329
+ const startedAt = this.now();
330
+ const claimEvents = [];
331
+ if (currentTask.status === 'running') {
332
+ const staleRun = state.runs.find((run) => run.taskId === taskId &&
333
+ run.status === 'running' &&
334
+ new Date(run.leaseExpiresAt).getTime() <=
335
+ new Date(startedAt).getTime());
336
+ if (!staleRun) {
337
+ throw new AgentPlatError('CONFLICT', 'Task was already claimed by another run');
338
+ }
339
+ const staleMessage = 'Run lease expired before completion';
340
+ await transaction.updateRun({
341
+ ...staleRun,
342
+ status: 'failed',
343
+ errorMessage: staleMessage,
344
+ completedAt: startedAt,
345
+ });
346
+ currentTask = {
347
+ ...currentTask,
348
+ status: 'failed',
349
+ errorMessage: staleMessage,
350
+ updatedAt: startedAt,
351
+ };
352
+ await transaction.updateTask(currentTask);
353
+ claimEvents.push(this.event(tenantId, roomId, 'task_run_failed', {
354
+ roomId,
355
+ taskId,
356
+ runId: staleRun.id,
357
+ error: staleMessage,
358
+ recovered: true,
359
+ }));
360
+ }
361
+ if (!['pending', 'failed'].includes(currentTask.status)) {
362
+ throw new AgentPlatError('CONFLICT', 'Task was already claimed by another run');
363
+ }
364
+ const incompleteDependencyId = currentTask.dependencies.find((id) => state.tasks.find((candidate) => candidate.id === id)?.status !==
365
+ 'completed');
366
+ if (incompleteDependencyId) {
367
+ throw new AgentPlatError('CONFLICT', `Dependency \"${incompleteDependencyId}\" is missing or not completed`);
368
+ }
369
+ const participant = this.resolveAgentParticipant(state, currentTask);
370
+ const decision = this.policyEngine.evaluateTask(currentTask, participant, state.policies);
371
+ if (!decision.allowed) {
372
+ throw new AgentPlatError('FORBIDDEN', decision.reason, {
373
+ statusCode: 403,
374
+ });
375
+ }
376
+ if (decision.approvalRequired &&
377
+ !state.approvals.some((approval) => approval.targetType === 'task' &&
378
+ approval.targetId === currentTask.id &&
379
+ approval.action === `task.run.${currentTask.actionLevel}` &&
380
+ approval.status === 'approved')) {
381
+ throw new AgentPlatError('FORBIDDEN', 'Task execution requires a granted approval', { statusCode: 403 });
382
+ }
383
+ const context = this.contextBuilder.build(state, currentTask, participant);
384
+ const running = {
385
+ id: runId,
386
+ tenantId,
387
+ roomId,
388
+ taskId,
389
+ participantId: participant.id,
390
+ runtime: participant.runtime?.platform ?? 'unknown',
391
+ status: 'running',
392
+ startedAt,
393
+ leaseExpiresAt: new Date(new Date(startedAt).getTime() + this.runTimeoutMs).toISOString(),
394
+ };
395
+ await transaction.updateTask({
396
+ ...currentTask,
397
+ status: 'running',
398
+ errorMessage: undefined,
399
+ updatedAt: startedAt,
400
+ });
401
+ await transaction.insertRun(running);
402
+ await transaction.insertContextSnapshot({
403
+ id: snapshotId,
404
+ tenantId,
405
+ roomId,
406
+ taskId,
407
+ runId,
408
+ context,
409
+ createdAt: startedAt,
410
+ });
411
+ const event = this.event(tenantId, roomId, 'task_run_started', { roomId, taskId, runId, participantId: participant.id }, participant.id);
412
+ claimEvents.push(event);
413
+ await this.appendEvents(transaction, claimEvents);
414
+ return {
415
+ value: {
416
+ running,
417
+ context,
418
+ participant,
419
+ task: currentTask,
420
+ policies: state.policies,
421
+ },
422
+ events: claimEvents,
423
+ };
424
+ });
425
+ await this.publish(claim.events);
426
+ const { context, participant, task, policies } = claim.value;
427
+ // Publishing happens after the claim commits. It may be arbitrarily slow,
428
+ // so fence the claim again and start a fresh lease immediately before the
429
+ // provider is invoked. If another process recovered the original lease,
430
+ // this caller exits without producing duplicate runtime side effects.
431
+ const running = await this.repository.transaction(tenantId, async (transaction) => {
432
+ const state = await transaction.getRoomState(tenantId, roomId);
433
+ const currentTask = await transaction.getTask(tenantId, taskId);
434
+ const currentRun = state?.runs.find((candidate) => candidate.id === runId);
435
+ if (!currentTask ||
436
+ currentTask.status !== 'running' ||
437
+ currentRun?.status !== 'running') {
438
+ throw new AgentPlatError('CONFLICT', 'Run no longer owns the task execution lease', { statusCode: 409 });
439
+ }
440
+ const renewed = {
441
+ ...currentRun,
442
+ leaseExpiresAt: this.leaseExpiresAt(this.now()),
443
+ };
444
+ await transaction.updateRun(renewed);
445
+ return renewed;
446
+ });
447
+ const startedMs = this.clock().getTime();
448
+ const abortController = new AbortController();
449
+ try {
450
+ const runtimeResult = await this.withTimeout(this.runtime.run({
451
+ id: participant.id,
452
+ tenantId,
453
+ name: participant.displayName,
454
+ instructions: participant.runtime?.instructions,
455
+ platform: participant.runtime?.platform ?? 'mock',
456
+ modelName: participant.runtime?.modelName,
457
+ config: participant.runtime?.config,
458
+ }, {
459
+ input: [this.toJson(context)],
460
+ mode: 'invoke',
461
+ metadata: {
462
+ roomId,
463
+ taskId,
464
+ expectedArtifactKind: task.expectedArtifactKind,
465
+ },
466
+ }, {
467
+ tenant: { tenantId },
468
+ runId,
469
+ agentId: participant.id,
470
+ signal: abortController.signal,
471
+ policies: this.toJson({ policies }),
472
+ metadata: { roomId, taskId, contextSnapshotId: snapshotId },
473
+ }), this.runTimeoutMs, (timeoutError) => abortController.abort(timeoutError));
474
+ if (runtimeResult.status !== 'completed') {
475
+ throw new AgentPlatError('ADAPTER_ERROR', runtimeResult.errorMessage ?? 'Agent runtime did not complete');
476
+ }
477
+ const completedAt = this.now();
478
+ const latencyMs = Math.max(0, this.clock().getTime() - startedMs);
479
+ const artifactOutput = this.parseArtifactOutput(runtimeResult.result, task, runtimeResult.output);
480
+ return this.mutate(tenantId, async (transaction) => {
481
+ const state = await transaction.getRoomState(tenantId, roomId);
482
+ const currentTask = await transaction.getTask(tenantId, taskId);
483
+ const currentRun = state?.runs.find((candidate) => candidate.id === runId);
484
+ if (!currentTask ||
485
+ currentTask.status !== 'running' ||
486
+ currentRun?.status !== 'running') {
487
+ throw new AgentPlatError('CONFLICT', 'Run no longer owns the task completion lease', { statusCode: 409 });
488
+ }
489
+ const artifactId = this.id();
490
+ const artifact = {
491
+ id: artifactId,
492
+ tenantId,
493
+ roomId,
494
+ type: artifactOutput.type,
495
+ title: artifactOutput.title,
496
+ status: 'draft',
497
+ currentVersion: 1,
498
+ authors: [participant.id],
499
+ provenance: {
500
+ sourceMessageIds: context.provenance.messageIds,
501
+ sourceArtifactIds: context.provenance.artifactIds,
502
+ sourceMemoryIds: context.provenance.memoryIds,
503
+ runId,
504
+ },
505
+ assumptions: artifactOutput.assumptions,
506
+ risks: artifactOutput.risks,
507
+ createdAt: completedAt,
508
+ updatedAt: completedAt,
509
+ };
510
+ const version = {
511
+ id: this.id(),
512
+ tenantId,
513
+ artifactId,
514
+ version: 1,
515
+ content: artifactOutput.content,
516
+ contentType: artifactOutput.contentType,
517
+ createdBy: participant.id,
518
+ createdAt: completedAt,
519
+ };
520
+ const completedRun = {
521
+ ...running,
522
+ status: 'completed',
523
+ output: runtimeResult.output,
524
+ latencyMs,
525
+ completedAt,
526
+ };
527
+ await transaction.insertArtifact(artifact, version);
528
+ await transaction.updateTask({
529
+ ...currentTask,
530
+ status: 'completed',
531
+ errorMessage: undefined,
532
+ updatedAt: completedAt,
533
+ completedAt,
534
+ });
535
+ await transaction.updateRun(completedRun);
536
+ const events = [
537
+ this.event(tenantId, roomId, 'artifact_created', { roomId, artifactId, taskId, runId }, participant.id),
538
+ this.event(tenantId, roomId, 'task_run_completed', { roomId, taskId, runId, artifactId }, participant.id),
539
+ ];
540
+ await this.appendEvents(transaction, events);
541
+ return { value: completedRun, events };
542
+ });
543
+ }
544
+ catch (error) {
545
+ const failedAt = this.now();
546
+ const message = error instanceof Error ? error.message : 'Agent runtime failed';
547
+ await this.mutate(tenantId, async (transaction) => {
548
+ const state = await transaction.getRoomState(tenantId, roomId);
549
+ const currentTask = await transaction.getTask(tenantId, taskId);
550
+ const currentRun = state?.runs.find((candidate) => candidate.id === runId);
551
+ if (!currentTask ||
552
+ currentTask.status !== 'running' ||
553
+ currentRun?.status !== 'running') {
554
+ return { value: currentRun ?? running, events: [] };
555
+ }
556
+ const failedRun = {
557
+ ...running,
558
+ status: 'failed',
559
+ errorMessage: message,
560
+ completedAt: failedAt,
561
+ };
562
+ await transaction.updateTask({
563
+ ...currentTask,
564
+ status: 'failed',
565
+ errorMessage: message,
566
+ updatedAt: failedAt,
567
+ });
568
+ await transaction.updateRun(failedRun);
569
+ const event = this.event(tenantId, roomId, 'task_run_failed', {
570
+ roomId,
571
+ taskId,
572
+ runId,
573
+ error: message,
574
+ });
575
+ await transaction.appendEvent(event);
576
+ return { value: failedRun, events: [event] };
577
+ });
578
+ throw error;
579
+ }
580
+ }
581
+ async createArtifact(tenantId, roomId, input) {
582
+ this.required(input.type, 'type');
583
+ this.required(input.title, 'title');
584
+ this.stringArray(input.authors, 'authors');
585
+ this.stringArray(input.assumptions, 'assumptions');
586
+ this.stringArray(input.risks, 'risks');
587
+ this.stringArray(input.provenance?.sourceMessageIds, 'provenance.sourceMessageIds');
588
+ this.stringArray(input.provenance?.sourceArtifactIds, 'provenance.sourceArtifactIds');
589
+ this.stringArray(input.provenance?.sourceMemoryIds, 'provenance.sourceMemoryIds');
590
+ if (input.content === undefined) {
591
+ throw new AgentPlatError('VALIDATION_ERROR', 'content is required');
592
+ }
593
+ if (input.contentType !== undefined)
594
+ this.required(input.contentType, 'contentType');
595
+ return this.mutate(tenantId, async (transaction) => {
596
+ const room = await this.requireRoom(transaction, tenantId, roomId);
597
+ this.assertRoomWritable(room);
598
+ const now = this.now();
599
+ const artifact = {
600
+ id: input.id ?? this.id(),
601
+ tenantId,
602
+ roomId,
603
+ type: input.type,
604
+ title: input.title,
605
+ status: 'draft',
606
+ currentVersion: 1,
607
+ authors: input.authors ?? (input.createdBy ? [input.createdBy] : []),
608
+ provenance: {
609
+ sourceMessageIds: input.provenance?.sourceMessageIds ?? [],
610
+ sourceArtifactIds: input.provenance?.sourceArtifactIds ?? [],
611
+ sourceMemoryIds: input.provenance?.sourceMemoryIds ?? [],
612
+ runId: input.provenance?.runId,
613
+ },
614
+ assumptions: input.assumptions ?? [],
615
+ risks: input.risks ?? [],
616
+ metadata: input.metadata,
617
+ createdAt: now,
618
+ updatedAt: now,
619
+ };
620
+ const version = {
621
+ id: this.id(),
622
+ tenantId,
623
+ artifactId: artifact.id,
624
+ version: 1,
625
+ content: input.content,
626
+ contentType: input.contentType ?? 'application/json',
627
+ createdBy: input.createdBy,
628
+ createdAt: now,
629
+ };
630
+ const event = this.event(tenantId, roomId, 'artifact_created', { roomId, artifactId: artifact.id }, input.createdBy);
631
+ await transaction.insertArtifact(artifact, version);
632
+ await transaction.appendEvent(event);
633
+ return { value: artifact, events: [event] };
634
+ });
635
+ }
636
+ async createArtifactVersion(tenantId, roomId, artifactId, input) {
637
+ if (input.content === undefined) {
638
+ throw new AgentPlatError('VALIDATION_ERROR', 'content is required');
639
+ }
640
+ return this.mutate(tenantId, async (transaction) => {
641
+ const room = await this.requireRoom(transaction, tenantId, roomId);
642
+ this.assertRoomWritable(room);
643
+ const artifact = await transaction.getArtifact(tenantId, artifactId);
644
+ if (!artifact || artifact.roomId !== roomId)
645
+ throw this.notFound('Artifact', artifactId);
646
+ const state = await transaction.getRoomState(tenantId, roomId);
647
+ if (state?.approvals.some((approval) => approval.targetType === 'artifact' &&
648
+ approval.targetId === artifactId &&
649
+ approval.status === 'requested')) {
650
+ throw new AgentPlatError('CONFLICT', 'Resolve the requested approval before creating a new artifact version', { statusCode: 409 });
651
+ }
652
+ const now = this.now();
653
+ const version = {
654
+ id: this.id(),
655
+ tenantId,
656
+ artifactId,
657
+ version: artifact.currentVersion + 1,
658
+ content: input.content,
659
+ contentType: input.contentType ?? 'application/json',
660
+ createdBy: input.createdBy,
661
+ createdAt: now,
662
+ };
663
+ await transaction.insertArtifactVersion(version);
664
+ await transaction.updateArtifact({
665
+ ...artifact,
666
+ currentVersion: version.version,
667
+ status: 'draft',
668
+ updatedAt: now,
669
+ });
670
+ const event = this.event(tenantId, roomId, 'artifact_updated', { roomId, artifactId, version: version.version }, input.createdBy);
671
+ await transaction.appendEvent(event);
672
+ return { value: version, events: [event] };
673
+ });
674
+ }
675
+ async requestApproval(tenantId, roomId, input) {
676
+ if (!approvalTargetTypes.has(input.targetType)) {
677
+ throw new AgentPlatError('VALIDATION_ERROR', 'targetType is not supported');
678
+ }
679
+ this.required(input.targetId, 'targetId');
680
+ if (input.action !== undefined)
681
+ this.required(input.action, 'action');
682
+ return this.mutate(tenantId, async (transaction) => {
683
+ const room = await this.requireRoom(transaction, tenantId, roomId);
684
+ this.assertRoomWritable(room);
685
+ let targetVersion;
686
+ let action = input.action;
687
+ if (input.targetType === 'artifact') {
688
+ const artifact = await transaction.getArtifact(tenantId, input.targetId);
689
+ if (!artifact || artifact.roomId !== roomId)
690
+ throw this.notFound('Artifact', input.targetId);
691
+ targetVersion = artifact.currentVersion;
692
+ await transaction.updateArtifact({
693
+ ...artifact,
694
+ status: 'pending_approval',
695
+ updatedAt: this.now(),
696
+ });
697
+ }
698
+ else if (input.targetType === 'task') {
699
+ const task = await transaction.getTask(tenantId, input.targetId);
700
+ if (!task || task.roomId !== roomId)
701
+ throw this.notFound('Task', input.targetId);
702
+ action ??= `task.run.${task.actionLevel}`;
703
+ }
704
+ else if (input.targetType === 'room' && input.targetId !== roomId) {
705
+ throw new AgentPlatError('VALIDATION_ERROR', 'Approval room target must match the current room');
706
+ }
707
+ const state = await transaction.getRoomState(tenantId, roomId);
708
+ if (input.requestedBy &&
709
+ !state?.participants.some((participant) => participant.id === input.requestedBy)) {
710
+ throw new AgentPlatError('VALIDATION_ERROR', 'requestedBy must be a room participant');
711
+ }
712
+ if (state?.approvals.some((candidate) => candidate.status === 'requested' &&
713
+ candidate.targetType === input.targetType &&
714
+ candidate.targetId === input.targetId &&
715
+ candidate.targetVersion === targetVersion &&
716
+ candidate.action === action)) {
717
+ throw new AgentPlatError('CONFLICT', 'An approval is already requested for this target and action', { statusCode: 409 });
718
+ }
719
+ const now = this.now();
720
+ const approval = {
721
+ id: input.id ?? this.id(),
722
+ tenantId,
723
+ roomId,
724
+ targetType: input.targetType,
725
+ targetId: input.targetId,
726
+ targetVersion,
727
+ action,
728
+ status: 'requested',
729
+ requestedBy: input.requestedBy,
730
+ createdAt: now,
731
+ updatedAt: now,
732
+ };
733
+ const event = this.event(tenantId, roomId, 'approval_requested', {
734
+ roomId,
735
+ approvalId: approval.id,
736
+ targetType: approval.targetType,
737
+ targetId: approval.targetId,
738
+ targetVersion: approval.targetVersion ?? null,
739
+ }, input.requestedBy);
740
+ await transaction.insertApproval(approval);
741
+ await transaction.appendEvent(event);
742
+ return { value: approval, events: [event] };
743
+ });
744
+ }
745
+ async resolveApproval(tenantId, approvalId, status, input) {
746
+ this.required(input.decidedBy, 'decidedBy');
747
+ if (!['approved', 'rejected', 'needs_revision'].includes(status)) {
748
+ throw new AgentPlatError('VALIDATION_ERROR', 'Approval status is not supported');
749
+ }
750
+ return this.mutate(tenantId, async (transaction) => {
751
+ const approval = await transaction.getApproval(tenantId, approvalId);
752
+ if (!approval)
753
+ throw this.notFound('Approval', approvalId);
754
+ if (approval.status !== 'requested') {
755
+ throw new AgentPlatError('CONFLICT', 'Approval has already been resolved');
756
+ }
757
+ const state = await transaction.getRoomState(tenantId, approval.roomId);
758
+ if (!state)
759
+ throw this.notFound('Room', approval.roomId);
760
+ this.assertRoomWritable(state.room);
761
+ const decider = state.participants.find((participant) => participant.id === input.decidedBy);
762
+ if (!decider || decider.type !== 'human') {
763
+ throw new AgentPlatError('FORBIDDEN', 'Approvals must be resolved by a human room participant');
764
+ }
765
+ const approvalPermissions = [
766
+ '*',
767
+ 'approve',
768
+ 'approval.resolve',
769
+ `approve:${approval.targetType}`,
770
+ ...(approval.action ? [`approve:${approval.action}`] : []),
771
+ ];
772
+ if (!decider.permissions.some((permission) => approvalPermissions.includes(permission))) {
773
+ throw new AgentPlatError('FORBIDDEN', 'Participant does not have permission to resolve this approval');
774
+ }
775
+ const now = this.now();
776
+ const resolved = {
777
+ ...approval,
778
+ status,
779
+ decidedBy: input.decidedBy,
780
+ comment: input.comment,
781
+ updatedAt: now,
782
+ decidedAt: now,
783
+ };
784
+ await transaction.updateApproval(resolved);
785
+ if (approval.targetType === 'artifact') {
786
+ const artifact = await transaction.getArtifact(tenantId, approval.targetId);
787
+ if (!artifact || artifact.roomId !== approval.roomId) {
788
+ throw this.notFound('Artifact', approval.targetId);
789
+ }
790
+ if (artifact.currentVersion !== approval.targetVersion) {
791
+ throw new AgentPlatError('CONFLICT', 'Approval does not target the artifact current version', { statusCode: 409 });
792
+ }
793
+ await transaction.updateArtifact({
794
+ ...artifact,
795
+ status,
796
+ updatedAt: now,
797
+ });
798
+ }
799
+ else if (approval.targetType === 'task') {
800
+ const task = await transaction.getTask(tenantId, approval.targetId);
801
+ if (!task || task.roomId !== approval.roomId) {
802
+ throw this.notFound('Task', approval.targetId);
803
+ }
804
+ }
805
+ else if (approval.targetType === 'room' &&
806
+ approval.targetId !== approval.roomId) {
807
+ throw new AgentPlatError('VALIDATION_ERROR', 'Approval room target is invalid');
808
+ }
809
+ const eventTypes = {
810
+ approved: 'approval_granted',
811
+ rejected: 'approval_rejected',
812
+ needs_revision: 'approval_needs_revision',
813
+ };
814
+ const event = this.event(tenantId, approval.roomId, eventTypes[status], { roomId: approval.roomId, approvalId, targetId: approval.targetId }, input.decidedBy);
815
+ await transaction.appendEvent(event);
816
+ return { value: resolved, events: [event] };
817
+ });
818
+ }
819
+ async createPolicy(tenantId, roomId, input, actorId) {
820
+ this.required(input.name, 'name');
821
+ this.stringArray(input.allowedActions, 'allowedActions');
822
+ this.stringArray(input.deniedActions, 'deniedActions');
823
+ this.stringArray(input.requiredApprovals, 'requiredApprovals');
824
+ this.stringArray(input.toolPermissions, 'toolPermissions');
825
+ this.stringArray(input.memoryAccessRules, 'memoryAccessRules');
826
+ if (input.escalationRules !== undefined &&
827
+ (!Array.isArray(input.escalationRules) ||
828
+ input.escalationRules.some((rule) => !rule || typeof rule !== 'object' || Array.isArray(rule)))) {
829
+ throw new AgentPlatError('VALIDATION_ERROR', 'escalationRules must be an array of objects');
830
+ }
831
+ if ((input.memoryAccessRules ?? []).some((scope) => !memoryScopes.has(scope))) {
832
+ throw new AgentPlatError('VALIDATION_ERROR', 'memoryAccessRules contains an unsupported scope');
833
+ }
834
+ return this.mutate(tenantId, async (transaction) => {
835
+ const room = await this.requireRoom(transaction, tenantId, roomId);
836
+ this.assertRoomWritable(room);
837
+ const now = this.now();
838
+ const policy = {
839
+ id: input.id ?? this.id(),
840
+ tenantId,
841
+ roomId,
842
+ name: input.name,
843
+ allowedActions: input.allowedActions ?? [],
844
+ deniedActions: input.deniedActions ?? [],
845
+ requiredApprovals: input.requiredApprovals ?? [],
846
+ escalationRules: input.escalationRules ?? [],
847
+ toolPermissions: input.toolPermissions ?? [],
848
+ memoryAccessRules: input.memoryAccessRules ?? ['room'],
849
+ createdAt: now,
850
+ updatedAt: now,
851
+ };
852
+ const event = this.event(tenantId, roomId, 'policy_created', { roomId, policyId: policy.id }, actorId);
853
+ await transaction.insertPolicy(policy);
854
+ await transaction.appendEvent(event);
855
+ return { value: policy, events: [event] };
856
+ });
857
+ }
858
+ async writeMemory(tenantId, roomId, input, actorId) {
859
+ this.required(input.source, 'source');
860
+ if (input.content === undefined) {
861
+ throw new AgentPlatError('VALIDATION_ERROR', 'content is required');
862
+ }
863
+ if (!memoryScopes.has(input.scope)) {
864
+ throw new AgentPlatError('VALIDATION_ERROR', 'scope is not supported');
865
+ }
866
+ if (input.retention && !memoryRetentions.has(input.retention)) {
867
+ throw new AgentPlatError('VALIDATION_ERROR', 'retention is not supported');
868
+ }
869
+ if (input.retention === 'until' && !input.retainUntil) {
870
+ throw new AgentPlatError('VALIDATION_ERROR', 'retainUntil is required when retention is until');
871
+ }
872
+ if (['agent', 'role', 'artifact'].includes(input.scope) && !input.scopeId) {
873
+ throw new AgentPlatError('VALIDATION_ERROR', 'scopeId is required for scoped memory');
874
+ }
875
+ if (!Number.isFinite(input.confidence ?? 1) ||
876
+ (input.confidence ?? 1) < 0 ||
877
+ (input.confidence ?? 1) > 1) {
878
+ throw new AgentPlatError('VALIDATION_ERROR', 'confidence must be between 0 and 1');
879
+ }
880
+ if (input.retainUntil !== undefined &&
881
+ Number.isNaN(new Date(input.retainUntil).getTime())) {
882
+ throw new AgentPlatError('VALIDATION_ERROR', 'retainUntil must be a valid ISO date-time');
883
+ }
884
+ return this.mutate(tenantId, async (transaction) => {
885
+ const room = await this.requireRoom(transaction, tenantId, roomId);
886
+ this.assertRoomWritable(room);
887
+ const state = await transaction.getRoomState(tenantId, roomId);
888
+ let scopeId = input.scopeId;
889
+ if (input.scope === 'agent') {
890
+ if (!state?.participants.some((participant) => participant.id === scopeId)) {
891
+ throw new AgentPlatError('VALIDATION_ERROR', 'Agent memory scopeId must be a room participant');
892
+ }
893
+ }
894
+ else if (input.scope === 'role') {
895
+ if (!state?.participants.some((participant) => participant.role === scopeId)) {
896
+ throw new AgentPlatError('VALIDATION_ERROR', 'Role memory scopeId must be present in the room');
897
+ }
898
+ }
899
+ else if (input.scope === 'artifact') {
900
+ const artifact = scopeId
901
+ ? await transaction.getArtifact(tenantId, scopeId)
902
+ : undefined;
903
+ if (!artifact || artifact.roomId !== roomId) {
904
+ throw new AgentPlatError('VALIDATION_ERROR', 'Artifact memory scopeId must belong to the room');
905
+ }
906
+ }
907
+ else if (input.scope === 'organization') {
908
+ if (scopeId && scopeId !== tenantId) {
909
+ throw new AgentPlatError('VALIDATION_ERROR', 'Organization memory scopeId must match the current tenant');
910
+ }
911
+ scopeId = tenantId;
912
+ }
913
+ else {
914
+ scopeId = roomId;
915
+ }
916
+ const entry = {
917
+ id: input.id ?? this.id(),
918
+ tenantId,
919
+ roomId: input.scope === 'organization' ? undefined : roomId,
920
+ scope: input.scope,
921
+ scopeId,
922
+ content: input.content,
923
+ source: input.source,
924
+ confidence: input.confidence ?? 1,
925
+ retention: input.retention ?? 'durable',
926
+ retainUntil: input.retainUntil,
927
+ provenance: input.provenance ?? {},
928
+ createdAt: this.now(),
929
+ };
930
+ const event = this.event(tenantId, roomId, 'memory_written', { roomId, memoryId: entry.id, scope: entry.scope }, actorId);
931
+ await transaction.insertMemory(entry);
932
+ await transaction.appendEvent(event);
933
+ return { value: entry, events: [event] };
934
+ });
935
+ }
936
+ async getRoomState(tenantId, roomId) {
937
+ const state = await this.repository.getRoomState(tenantId, roomId);
938
+ if (!state)
939
+ throw this.notFound('Room', roomId);
940
+ return state;
941
+ }
942
+ async listRooms(tenantId) {
943
+ return this.repository.listRooms(tenantId);
944
+ }
945
+ async listEvents(tenantId, roomId) {
946
+ if (!(await this.repository.getRoom(tenantId, roomId)))
947
+ throw this.notFound('Room', roomId);
948
+ return this.repository.listEvents(tenantId, roomId);
949
+ }
950
+ async mutate(tenantId, work) {
951
+ this.required(tenantId, 'tenantId');
952
+ const result = await this.repository.transaction(tenantId, work);
953
+ await this.publish(result.events);
954
+ return result.value;
955
+ }
956
+ async publish(events) {
957
+ if (!this.eventPublisher)
958
+ return;
959
+ for (const event of events) {
960
+ try {
961
+ await this.eventPublisher.publish(event);
962
+ }
963
+ catch (error) {
964
+ try {
965
+ this.onEventPublishError(error, event);
966
+ }
967
+ catch {
968
+ // State and its durable event already committed. Observability
969
+ // hooks must not change the outcome of the domain operation.
970
+ }
971
+ }
972
+ }
973
+ }
974
+ async appendEvents(transaction, events) {
975
+ for (const event of events)
976
+ await transaction.appendEvent(event);
977
+ }
978
+ async requireRoom(transaction, tenantId, roomId) {
979
+ const room = await transaction.getRoom(tenantId, roomId);
980
+ if (!room)
981
+ throw this.notFound('Room', roomId);
982
+ return room;
983
+ }
984
+ async requireRoomParticipant(transaction, tenantId, roomId, participantId) {
985
+ const state = await transaction.getRoomState(tenantId, roomId);
986
+ const participant = state?.participants.find((item) => item.id === participantId);
987
+ if (!participant) {
988
+ throw new AgentPlatError('VALIDATION_ERROR', `Participant \"${participantId}\" is not in this room`);
989
+ }
990
+ return participant;
991
+ }
992
+ resolveAgentParticipant(state, task) {
993
+ const participant = task.assignedParticipantId
994
+ ? state.participants.find((candidate) => candidate.id === task.assignedParticipantId)
995
+ : state.participants.find((candidate) => candidate.type === 'agent' &&
996
+ (!task.assignedRole || candidate.role === task.assignedRole));
997
+ if (!participant || participant.type !== 'agent') {
998
+ throw new AgentPlatError('VALIDATION_ERROR', 'Task requires an assigned agent participant');
999
+ }
1000
+ return participant;
1001
+ }
1002
+ assertRoomWritable(room) {
1003
+ if (room.status === 'completed' || room.status === 'archived') {
1004
+ throw new AgentPlatError('CONFLICT', `Room is ${room.status} and cannot be modified`, { statusCode: 409 });
1005
+ }
1006
+ }
1007
+ parseArtifactOutput(result, task, output) {
1008
+ const candidate = result?.artifact;
1009
+ const artifact = candidate && typeof candidate === 'object' && !Array.isArray(candidate)
1010
+ ? candidate
1011
+ : {};
1012
+ const strings = (value) => Array.isArray(value)
1013
+ ? value.filter((item) => typeof item === 'string')
1014
+ : [];
1015
+ return {
1016
+ type: typeof artifact.type === 'string'
1017
+ ? artifact.type
1018
+ : task.expectedArtifactKind,
1019
+ title: typeof artifact.title === 'string'
1020
+ ? artifact.title
1021
+ : `${task.stepId} output`,
1022
+ content: artifact.content ?? output ?? '',
1023
+ contentType: typeof artifact.contentType === 'string'
1024
+ ? artifact.contentType
1025
+ : 'text/plain',
1026
+ assumptions: strings(artifact.assumptions),
1027
+ risks: strings(artifact.risks),
1028
+ };
1029
+ }
1030
+ event(tenantId, roomId, type, payload, actorId) {
1031
+ return {
1032
+ id: this.id(),
1033
+ tenantId,
1034
+ roomId,
1035
+ type,
1036
+ source: 'agentplat.rooms',
1037
+ subject: { type: 'room', id: roomId, tenantId },
1038
+ payload,
1039
+ metadata: actorId ? { actorId } : undefined,
1040
+ actorId,
1041
+ occurredAt: this.now(),
1042
+ };
1043
+ }
1044
+ toJson(value) {
1045
+ return JSON.parse(JSON.stringify(value));
1046
+ }
1047
+ async withTimeout(promise, timeoutMs, onTimeout) {
1048
+ let timeout;
1049
+ const timedOut = new Promise((_resolve, reject) => {
1050
+ timeout = setTimeout(() => {
1051
+ const error = new AgentPlatError('ADAPTER_ERROR', `Agent runtime timed out after ${timeoutMs}ms`);
1052
+ try {
1053
+ onTimeout?.(error);
1054
+ }
1055
+ catch {
1056
+ // Cancellation is cooperative. A provider-specific abort hook must
1057
+ // not prevent the timeout from fencing the persisted run.
1058
+ }
1059
+ reject(error);
1060
+ }, timeoutMs);
1061
+ });
1062
+ try {
1063
+ return await Promise.race([promise, timedOut]);
1064
+ }
1065
+ finally {
1066
+ if (timeout !== undefined)
1067
+ clearTimeout(timeout);
1068
+ }
1069
+ }
1070
+ id() {
1071
+ return this.idGenerator();
1072
+ }
1073
+ leaseExpiresAt(startedAt) {
1074
+ return new Date(new Date(startedAt).getTime() + this.runTimeoutMs + this.runLeaseGraceMs).toISOString();
1075
+ }
1076
+ now() {
1077
+ return this.clock().toISOString();
1078
+ }
1079
+ required(value, field) {
1080
+ if (typeof value !== 'string' || !value.trim()) {
1081
+ throw new AgentPlatError('VALIDATION_ERROR', `${field} is required`);
1082
+ }
1083
+ }
1084
+ stringArray(value, field) {
1085
+ if (value !== undefined &&
1086
+ (!Array.isArray(value) || value.some((item) => typeof item !== 'string'))) {
1087
+ throw new AgentPlatError('VALIDATION_ERROR', `${field} must be an array of strings`);
1088
+ }
1089
+ }
1090
+ notFound(label, id) {
1091
+ return new AgentPlatError('NOT_FOUND', `${label} \"${id}\" was not found`, {
1092
+ statusCode: 404,
1093
+ });
1094
+ }
1095
+ }
1096
+ //# sourceMappingURL=service.js.map