agent-tower 0.4.15 → 0.4.16-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/dist/core/event-bus.d.ts +2 -0
  2. package/dist/core/event-bus.d.ts.map +1 -1
  3. package/dist/core/event-bus.js.map +1 -1
  4. package/dist/executors/__tests__/codex.executor.test.d.ts +2 -0
  5. package/dist/executors/__tests__/codex.executor.test.d.ts.map +1 -0
  6. package/dist/executors/__tests__/codex.executor.test.js +28 -0
  7. package/dist/executors/__tests__/codex.executor.test.js.map +1 -0
  8. package/dist/executors/codex.executor.d.ts +1 -0
  9. package/dist/executors/codex.executor.d.ts.map +1 -1
  10. package/dist/executors/codex.executor.js +19 -1
  11. package/dist/executors/codex.executor.js.map +1 -1
  12. package/dist/mcp/context.d.ts +3 -0
  13. package/dist/mcp/context.d.ts.map +1 -1
  14. package/dist/mcp/context.js +10 -1
  15. package/dist/mcp/context.js.map +1 -1
  16. package/dist/mcp/http-client.d.ts +24 -1
  17. package/dist/mcp/http-client.d.ts.map +1 -1
  18. package/dist/mcp/http-client.js +29 -2
  19. package/dist/mcp/http-client.js.map +1 -1
  20. package/dist/mcp/server.d.ts.map +1 -1
  21. package/dist/mcp/server.js +190 -0
  22. package/dist/mcp/server.js.map +1 -1
  23. package/dist/routes/index.d.ts.map +1 -1
  24. package/dist/routes/index.js +3 -0
  25. package/dist/routes/index.js.map +1 -1
  26. package/dist/routes/system.d.ts.map +1 -1
  27. package/dist/routes/system.js +35 -1
  28. package/dist/routes/system.js.map +1 -1
  29. package/dist/routes/team-runs.d.ts +11 -0
  30. package/dist/routes/team-runs.d.ts.map +1 -0
  31. package/dist/routes/team-runs.js +299 -0
  32. package/dist/routes/team-runs.js.map +1 -0
  33. package/dist/services/__tests__/session-manager.team-run.test.d.ts +2 -0
  34. package/dist/services/__tests__/session-manager.team-run.test.d.ts.map +1 -0
  35. package/dist/services/__tests__/session-manager.team-run.test.js +286 -0
  36. package/dist/services/__tests__/session-manager.team-run.test.js.map +1 -0
  37. package/dist/services/__tests__/team-lock.service.test.d.ts +2 -0
  38. package/dist/services/__tests__/team-lock.service.test.d.ts.map +1 -0
  39. package/dist/services/__tests__/team-lock.service.test.js +81 -0
  40. package/dist/services/__tests__/team-lock.service.test.js.map +1 -0
  41. package/dist/services/__tests__/team-reconciler.service.test.d.ts +2 -0
  42. package/dist/services/__tests__/team-reconciler.service.test.d.ts.map +1 -0
  43. package/dist/services/__tests__/team-reconciler.service.test.js +1038 -0
  44. package/dist/services/__tests__/team-reconciler.service.test.js.map +1 -0
  45. package/dist/services/__tests__/team-run.service.test.d.ts +2 -0
  46. package/dist/services/__tests__/team-run.service.test.d.ts.map +1 -0
  47. package/dist/services/__tests__/team-run.service.test.js +447 -0
  48. package/dist/services/__tests__/team-run.service.test.js.map +1 -0
  49. package/dist/services/__tests__/team-scheduler.service.test.d.ts +2 -0
  50. package/dist/services/__tests__/team-scheduler.service.test.d.ts.map +1 -0
  51. package/dist/services/__tests__/team-scheduler.service.test.js +1158 -0
  52. package/dist/services/__tests__/team-scheduler.service.test.js.map +1 -0
  53. package/dist/services/session-manager.d.ts +31 -1
  54. package/dist/services/session-manager.d.ts.map +1 -1
  55. package/dist/services/session-manager.js +110 -2
  56. package/dist/services/session-manager.js.map +1 -1
  57. package/dist/services/team-lock.service.d.ts +22 -0
  58. package/dist/services/team-lock.service.d.ts.map +1 -0
  59. package/dist/services/team-lock.service.js +45 -0
  60. package/dist/services/team-lock.service.js.map +1 -0
  61. package/dist/services/team-reconciler.service.d.ts +44 -0
  62. package/dist/services/team-reconciler.service.d.ts.map +1 -0
  63. package/dist/services/team-reconciler.service.js +286 -0
  64. package/dist/services/team-reconciler.service.js.map +1 -0
  65. package/dist/services/team-run-events.d.ts +13 -0
  66. package/dist/services/team-run-events.d.ts.map +1 -0
  67. package/dist/services/team-run-events.js +27 -0
  68. package/dist/services/team-run-events.js.map +1 -0
  69. package/dist/services/team-run.service.d.ts +89 -0
  70. package/dist/services/team-run.service.d.ts.map +1 -0
  71. package/dist/services/team-run.service.js +577 -0
  72. package/dist/services/team-run.service.js.map +1 -0
  73. package/dist/services/team-scheduler.service.d.ts +89 -0
  74. package/dist/services/team-scheduler.service.d.ts.map +1 -0
  75. package/dist/services/team-scheduler.service.js +750 -0
  76. package/dist/services/team-scheduler.service.js.map +1 -0
  77. package/dist/socket/events.d.ts +1 -1
  78. package/dist/socket/events.d.ts.map +1 -1
  79. package/dist/socket/events.js.map +1 -1
  80. package/dist/socket/socket-gateway.d.ts.map +1 -1
  81. package/dist/socket/socket-gateway.js +5 -1
  82. package/dist/socket/socket-gateway.js.map +1 -1
  83. package/dist/web/assets/AgentDemoPage-p9YI4_l4.js +1 -0
  84. package/dist/web/assets/{DemoPage-XwuS8vNB.js → DemoPage-B5DTSEbS.js} +3 -3
  85. package/dist/web/assets/{GeneralSettingsPage-CliIgpwf.js → GeneralSettingsPage-Cspr7Vol.js} +1 -1
  86. package/dist/web/assets/{NotificationSettingsPage-y3vhVgPv.js → NotificationSettingsPage-C9VfrRr-.js} +1 -1
  87. package/dist/web/assets/{ProfileSettingsPage-CkU_kZKG.js → ProfileSettingsPage-CNugU40a.js} +1 -1
  88. package/dist/web/assets/ProjectKanbanPage-CkGNuqxq.js +87 -0
  89. package/dist/web/assets/{ProjectSettingsPage-B6xhbziO.js → ProjectSettingsPage-f1dg0XMf.js} +1 -1
  90. package/dist/web/assets/{ProviderSettingsPage-CfvdeoEU.js → ProviderSettingsPage-D_KWkgRM.js} +1 -1
  91. package/dist/web/assets/TeamSettingsPage-B6WciZyi.js +1 -0
  92. package/dist/web/assets/{button-BWFTEdOr.js → button-B6JaSbDB.js} +1 -1
  93. package/dist/web/assets/{chevron-down-CuPdBAx-.js → chevron-down-CACy4UFq.js} +1 -1
  94. package/dist/web/assets/{chevron-right-Cs8vYTMn.js → chevron-right-DFWfnDJY.js} +1 -1
  95. package/dist/web/assets/chevron-up-CGlf6jzw.js +1 -0
  96. package/dist/web/assets/{circle-alert-EUyZcWhp.js → circle-alert-BSAUEd9O.js} +1 -1
  97. package/dist/web/assets/{circle-check-BXZTzqw0.js → circle-check-DMK8auwb.js} +1 -1
  98. package/dist/web/assets/{code-block-OCS4YCEC-BxUpvXK_.js → code-block-OCS4YCEC-Hn75KHRK.js} +1 -1
  99. package/dist/web/assets/{confirm-dialog-CDLHRthd.js → confirm-dialog-DHI2f7Ni.js} +1 -1
  100. package/dist/web/assets/{folder-picker-CUbhsnhi.js → folder-picker-CtQkbWfa.js} +1 -1
  101. package/dist/web/assets/index-BFAA3PTl.js +13 -0
  102. package/dist/web/assets/index-mBCb67dB.css +1 -0
  103. package/dist/web/assets/{loader-circle-BHzDVpxt.js → loader-circle-CkDnf8ST.js} +1 -1
  104. package/dist/web/assets/{mermaid-NOHMQCX5-BOSwJqP0.js → mermaid-NOHMQCX5-DJFgrXPd.js} +44 -44
  105. package/dist/web/assets/modal-B5IRN7QI.js +1 -0
  106. package/dist/web/assets/{pencil-BMxBxIhw.js → pencil-CJY6Ahn7.js} +1 -1
  107. package/dist/web/assets/{select-BUmRG0LY.js → select-BPZZlla1.js} +1 -1
  108. package/dist/web/assets/{use-profiles-C1vlPE-2.js → use-profiles-C2k04ICZ.js} +1 -1
  109. package/dist/web/assets/{use-projects-Bcd5hIOY.js → use-projects-BxuE-ulT.js} +1 -1
  110. package/dist/web/assets/{use-providers-Cdxr4Jbz.js → use-providers-C7fIDWzP.js} +1 -1
  111. package/dist/web/index.html +2 -2
  112. package/node_modules/@agent-tower/shared/dist/socket/events.d.ts +10 -0
  113. package/node_modules/@agent-tower/shared/dist/socket/events.d.ts.map +1 -1
  114. package/node_modules/@agent-tower/shared/dist/socket/events.js +1 -0
  115. package/node_modules/@agent-tower/shared/dist/socket/events.js.map +1 -1
  116. package/node_modules/@agent-tower/shared/dist/types.d.ts +153 -0
  117. package/node_modules/@agent-tower/shared/dist/types.d.ts.map +1 -1
  118. package/node_modules/@agent-tower/shared/dist/types.js.map +1 -1
  119. package/node_modules/@prisma/client/.prisma/client/default.d.ts +1 -0
  120. package/node_modules/@prisma/client/.prisma/client/default.js +1 -0
  121. package/node_modules/@prisma/client/.prisma/client/edge.d.ts +1 -0
  122. package/node_modules/@prisma/client/.prisma/client/edge.js +392 -0
  123. package/node_modules/@prisma/client/.prisma/client/index-browser.js +381 -0
  124. package/node_modules/@prisma/client/.prisma/client/index.d.ts +25768 -0
  125. package/node_modules/@prisma/client/.prisma/client/index.js +417 -0
  126. package/node_modules/@prisma/client/.prisma/client/libquery_engine-darwin-arm64.dylib.node +0 -0
  127. package/node_modules/@prisma/client/.prisma/client/package.json +97 -0
  128. package/node_modules/@prisma/client/.prisma/client/query_engine-windows.dll.node +0 -0
  129. package/node_modules/@prisma/client/.prisma/client/schema.prisma +280 -0
  130. package/node_modules/@prisma/client/.prisma/client/wasm.d.ts +1 -0
  131. package/node_modules/@prisma/client/.prisma/client/wasm.js +381 -0
  132. package/node_modules/@prisma/client/package.json +3 -2
  133. package/package.json +2 -1
  134. package/prisma/migrations/20260518000000_add_team_run_collaboration/migration.sql +150 -0
  135. package/prisma/migrations/20260522000000_add_team_member_session_policy/migration.sql +2 -0
  136. package/prisma/schema.prisma +131 -1
  137. package/dist/web/assets/AgentDemoPage-ClnGPAV9.js +0 -1
  138. package/dist/web/assets/ProjectKanbanPage-BddzfZRV.js +0 -87
  139. package/dist/web/assets/index-BGvfX18x.css +0 -1
  140. package/dist/web/assets/index-CHN8jahE.js +0 -13
  141. package/dist/web/assets/modal-D_AU4URz.js +0 -1
@@ -0,0 +1,750 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { getProviderById } from '../executors/index.js';
3
+ import { getSessionManager } from '../core/container.js';
4
+ import { NotFoundError, ServiceError } from '../errors.js';
5
+ import { prisma } from '../utils/index.js';
6
+ import { TeamLockService } from './team-lock.service.js';
7
+ import { WorkspaceService } from './workspace.service.js';
8
+ import { emitTeamRunInvalidated } from './team-run-events.js';
9
+ const ACTIVE_INVOCATION_STATUSES = [
10
+ 'QUEUED',
11
+ 'RUNNING',
12
+ 'SESSION_ENDED',
13
+ 'WAITING_ROOM_REPLY',
14
+ ];
15
+ const STOPPABLE_INVOCATION_STATUSES = [
16
+ 'QUEUED',
17
+ 'RUNNING',
18
+ 'SESSION_ENDED',
19
+ 'WAITING_ROOM_REPLY',
20
+ ];
21
+ const CANCELLABLE_QUEUED_WORK_REQUEST_STATUSES = [
22
+ 'PENDING_APPROVAL',
23
+ 'QUEUED',
24
+ ];
25
+ const DEFAULT_CAPABILITIES = {
26
+ readRoom: false,
27
+ postRoomMessage: false,
28
+ mentionMembers: false,
29
+ stopMemberWork: false,
30
+ markReadyForReview: false,
31
+ readFiles: false,
32
+ writeFiles: false,
33
+ runCommands: false,
34
+ readDiff: false,
35
+ mergeWorkspace: false,
36
+ };
37
+ const defaultLockService = new TeamLockService();
38
+ function parseJsonField(value, fallback) {
39
+ if (value == null || value === '') {
40
+ return fallback;
41
+ }
42
+ try {
43
+ return JSON.parse(value);
44
+ }
45
+ catch {
46
+ return fallback;
47
+ }
48
+ }
49
+ function toIso(date) {
50
+ return date.toISOString();
51
+ }
52
+ function invalidTransition(action, status) {
53
+ return new ServiceError(`Cannot ${action} WorkRequest in ${status} status`, 'INVALID_STATE_TRANSITION', 400);
54
+ }
55
+ export class TeamSchedulerService {
56
+ lockService;
57
+ static memberSchedulingLocks = new Set();
58
+ static sharedWorkspaceClaims = new Map();
59
+ workspaceService;
60
+ sessionManager;
61
+ providerLookup;
62
+ constructor(lockService = defaultLockService, dependencies = {}) {
63
+ this.lockService = lockService;
64
+ this.workspaceService = dependencies.workspaceService ?? new WorkspaceService();
65
+ this.sessionManager = dependencies.sessionManager ?? getSessionManager();
66
+ this.providerLookup = dependencies.getProviderById ?? getProviderById;
67
+ }
68
+ async planNext(teamRunId) {
69
+ const context = await this.getSchedulingContext(teamRunId);
70
+ const activeMemberIds = await this.findActiveMemberIds(teamRunId);
71
+ const plannedMemberIds = new Set();
72
+ const plannedLockKeys = new Set();
73
+ const plans = [];
74
+ for (const workRequest of context.workRequests) {
75
+ const member = context.memberById.get(workRequest.targetMemberId);
76
+ if (!member) {
77
+ plans.push({
78
+ workRequestId: workRequest.id,
79
+ memberId: workRequest.targetMemberId,
80
+ canStart: false,
81
+ blockedReason: 'member_not_found',
82
+ requiresStopCurrent: false,
83
+ lockKeys: [],
84
+ workspaceId: null,
85
+ projectId: context.teamRun.task.projectId,
86
+ });
87
+ continue;
88
+ }
89
+ const workspaceId = this.resolveInvocationWorkspaceId(context.teamRun, member);
90
+ const projectId = context.teamRun.task.projectId;
91
+ const lockKeys = this.getRequiredLocks(context.teamRun, member);
92
+ const requiresStopCurrent = workRequest.ifBusy === 'cancel_current_and_start'
93
+ && activeMemberIds.has(member.id);
94
+ if (member.workspacePolicy === 'dedicated') {
95
+ plans.push({
96
+ workRequestId: workRequest.id,
97
+ memberId: member.id,
98
+ canStart: false,
99
+ blockedReason: 'unsupported_workspace_policy',
100
+ requiresStopCurrent: false,
101
+ lockKeys,
102
+ workspaceId,
103
+ projectId,
104
+ });
105
+ continue;
106
+ }
107
+ if (activeMemberIds.has(member.id)) {
108
+ plans.push({
109
+ workRequestId: workRequest.id,
110
+ memberId: member.id,
111
+ canStart: false,
112
+ blockedReason: 'member_busy',
113
+ requiresStopCurrent,
114
+ lockKeys,
115
+ workspaceId,
116
+ projectId,
117
+ });
118
+ continue;
119
+ }
120
+ if (plannedMemberIds.has(member.id)) {
121
+ plans.push({
122
+ workRequestId: workRequest.id,
123
+ memberId: member.id,
124
+ canStart: false,
125
+ blockedReason: 'member_already_planned',
126
+ requiresStopCurrent: false,
127
+ lockKeys,
128
+ workspaceId,
129
+ projectId,
130
+ });
131
+ continue;
132
+ }
133
+ const hasPlannedLockConflict = lockKeys.some((key) => plannedLockKeys.has(key));
134
+ if (hasPlannedLockConflict || !this.lockService.canAcquire(lockKeys)) {
135
+ plans.push({
136
+ workRequestId: workRequest.id,
137
+ memberId: member.id,
138
+ canStart: false,
139
+ blockedReason: 'resource_locked',
140
+ requiresStopCurrent: false,
141
+ lockKeys,
142
+ workspaceId,
143
+ projectId,
144
+ });
145
+ continue;
146
+ }
147
+ plannedMemberIds.add(member.id);
148
+ for (const key of lockKeys) {
149
+ plannedLockKeys.add(key);
150
+ }
151
+ plans.push({
152
+ workRequestId: workRequest.id,
153
+ memberId: member.id,
154
+ canStart: true,
155
+ requiresStopCurrent: false,
156
+ lockKeys,
157
+ workspaceId,
158
+ projectId,
159
+ });
160
+ }
161
+ return plans;
162
+ }
163
+ async startNext(teamRunId) {
164
+ const context = await this.getSchedulingContext(teamRunId);
165
+ const startedInvocations = [];
166
+ for (const workRequest of context.workRequests) {
167
+ const member = context.memberById.get(workRequest.targetMemberId);
168
+ if (!member || member.workspacePolicy === 'dedicated') {
169
+ continue;
170
+ }
171
+ const memberLockKey = this.memberSchedulingLockKey(teamRunId, member.id);
172
+ if (!this.acquireMemberSchedulingLock(memberLockKey)) {
173
+ continue;
174
+ }
175
+ let invocationId = null;
176
+ try {
177
+ if (await this.hasActiveInvocation(teamRunId, member.id)) {
178
+ continue;
179
+ }
180
+ const freshWorkRequest = await prisma.workRequest.findUnique({
181
+ where: { id: workRequest.id },
182
+ });
183
+ if (!freshWorkRequest || freshWorkRequest.status !== 'QUEUED') {
184
+ continue;
185
+ }
186
+ const lockKeys = this.getRequiredLocks(context.teamRun, member);
187
+ invocationId = randomUUID();
188
+ if (!this.lockService.acquire(invocationId, lockKeys)) {
189
+ invocationId = null;
190
+ continue;
191
+ }
192
+ const createdInvocation = await this.createInvocationForClaimedWorkRequest(context.teamRun, member, freshWorkRequest, invocationId);
193
+ if (!createdInvocation) {
194
+ this.lockService.releaseByOwner(invocationId);
195
+ invocationId = null;
196
+ continue;
197
+ }
198
+ startedInvocations.push(this.serializeAgentInvocation(createdInvocation));
199
+ await this.emitTeamRunInvalidated(context.teamRun, ['work-requests', 'agent-invocations'], 'agent-invocation-updated');
200
+ invocationId = null;
201
+ }
202
+ catch (error) {
203
+ if (invocationId) {
204
+ this.lockService.releaseByOwner(invocationId);
205
+ }
206
+ throw error;
207
+ }
208
+ finally {
209
+ this.releaseMemberSchedulingLock(memberLockKey);
210
+ }
211
+ }
212
+ return startedInvocations;
213
+ }
214
+ async startNextSessions(teamRunId) {
215
+ const context = await this.getSchedulingContext(teamRunId);
216
+ const startedInvocations = [];
217
+ for (const workRequest of context.workRequests) {
218
+ const member = context.memberById.get(workRequest.targetMemberId);
219
+ if (!member || member.workspacePolicy === 'dedicated') {
220
+ continue;
221
+ }
222
+ const memberLockKey = this.memberSchedulingLockKey(teamRunId, member.id);
223
+ if (!this.acquireMemberSchedulingLock(memberLockKey)) {
224
+ continue;
225
+ }
226
+ let invocationId = null;
227
+ try {
228
+ if (await this.hasActiveInvocation(teamRunId, member.id)) {
229
+ continue;
230
+ }
231
+ const freshWorkRequest = await prisma.workRequest.findUnique({
232
+ where: { id: workRequest.id },
233
+ });
234
+ if (!freshWorkRequest || freshWorkRequest.status !== 'QUEUED') {
235
+ continue;
236
+ }
237
+ const lockKeys = this.getRequiredLocks(context.teamRun, member);
238
+ invocationId = randomUUID();
239
+ if (!this.lockService.acquire(invocationId, lockKeys)) {
240
+ invocationId = null;
241
+ continue;
242
+ }
243
+ const provider = this.providerLookup(member.providerId);
244
+ if (!provider) {
245
+ this.lockService.releaseByOwner(invocationId);
246
+ invocationId = null;
247
+ throw new ServiceError(`Provider not found: ${member.providerId}`, 'PROVIDER_NOT_FOUND', 400);
248
+ }
249
+ const workspace = await this.getOrCreateSharedWorkspace(context.teamRun);
250
+ const session = await this.sessionManager.create(workspace.id, provider.agentType, this.buildSessionPrompt(member, freshWorkRequest), 'DEFAULT', member.providerId);
251
+ const createdInvocation = await this.createRunningInvocationForClaimedWorkRequest(context.teamRun, member, freshWorkRequest, invocationId, workspace.id, session.id);
252
+ if (!createdInvocation) {
253
+ await this.markSessionFailed(session.id);
254
+ this.lockService.releaseByOwner(invocationId);
255
+ invocationId = null;
256
+ continue;
257
+ }
258
+ const resumeFromSessionId = await this.findResumeSourceSessionId(member, session.id);
259
+ try {
260
+ if (resumeFromSessionId && this.sessionManager.startFollowUp) {
261
+ await this.sessionManager.startFollowUp(session.id, resumeFromSessionId);
262
+ }
263
+ else {
264
+ await this.sessionManager.start(session.id);
265
+ }
266
+ }
267
+ catch (error) {
268
+ await this.markInvocationStartFailed(createdInvocation.id, session.id);
269
+ this.lockService.releaseByOwner(createdInvocation.id);
270
+ invocationId = null;
271
+ throw error;
272
+ }
273
+ startedInvocations.push(this.serializeAgentInvocation(createdInvocation));
274
+ await this.emitTeamRunInvalidated(context.teamRun, ['work-requests', 'agent-invocations', 'workspaces'], 'agent-invocation-updated');
275
+ invocationId = null;
276
+ }
277
+ catch (error) {
278
+ if (invocationId) {
279
+ this.lockService.releaseByOwner(invocationId);
280
+ }
281
+ throw error;
282
+ }
283
+ finally {
284
+ this.releaseMemberSchedulingLock(memberLockKey);
285
+ }
286
+ }
287
+ return startedInvocations;
288
+ }
289
+ async approveWorkRequest(workRequestId) {
290
+ const workRequest = await this.transitionWorkRequestStatus(workRequestId, 'approve', ['PENDING_APPROVAL'], 'QUEUED');
291
+ await this.emitTeamRunInvalidatedById(workRequest.teamRunId, ['work-requests', 'team-run'], 'work-request-updated');
292
+ return workRequest;
293
+ }
294
+ async approveWorkRequestAndStartNext(workRequestId) {
295
+ const workRequest = await this.approveWorkRequest(workRequestId);
296
+ const startedInvocations = await this.startNextSessions(workRequest.teamRunId);
297
+ return { workRequest, startedInvocations };
298
+ }
299
+ async rejectWorkRequest(workRequestId) {
300
+ const workRequest = await this.transitionWorkRequestStatus(workRequestId, 'reject', ['PENDING_APPROVAL'], 'REJECTED');
301
+ await this.emitTeamRunInvalidatedById(workRequest.teamRunId, ['work-requests', 'team-run'], 'work-request-updated');
302
+ return workRequest;
303
+ }
304
+ async cancelWorkRequest(workRequestId) {
305
+ const workRequest = await this.transitionWorkRequestStatus(workRequestId, 'cancel', ['PENDING_APPROVAL', 'QUEUED'], 'CANCELLED');
306
+ await this.emitTeamRunInvalidatedById(workRequest.teamRunId, ['work-requests', 'team-run'], 'work-request-updated');
307
+ return workRequest;
308
+ }
309
+ async stopMemberWork(teamRunId, memberId, options = {}) {
310
+ await this.getTeamMemberOrThrow(teamRunId, memberId);
311
+ const activeInvocations = await prisma.agentInvocation.findMany({
312
+ where: {
313
+ teamRunId,
314
+ memberId,
315
+ status: { in: STOPPABLE_INVOCATION_STATUSES },
316
+ },
317
+ orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
318
+ });
319
+ const stoppedSessionIds = [];
320
+ const queuedInvocationIds = activeInvocations
321
+ .filter((invocation) => invocation.sessionId == null)
322
+ .map((invocation) => invocation.id);
323
+ const queuedCancellation = queuedInvocationIds.length > 0
324
+ ? await this.cancelInvocationsWithoutSession(queuedInvocationIds)
325
+ : { cancelledInvocationIds: [], cancelledWorkRequestIds: [] };
326
+ if (options.cancelQueued) {
327
+ const cancelledQueuedRequestIds = await this.cancelQueuedWorkRequestsForMember(teamRunId, memberId);
328
+ queuedCancellation.cancelledWorkRequestIds.push(...cancelledQueuedRequestIds);
329
+ }
330
+ const sessionIds = activeInvocations
331
+ .map((invocation) => invocation.sessionId)
332
+ .filter((sessionId) => sessionId != null);
333
+ if (sessionIds.length > 0) {
334
+ if (!this.sessionManager.stop) {
335
+ throw new ServiceError('Session stop is not available', 'SESSION_STOP_UNAVAILABLE', 500);
336
+ }
337
+ for (const sessionId of sessionIds) {
338
+ const stopped = await this.sessionManager.stop(sessionId);
339
+ if (stopped) {
340
+ stoppedSessionIds.push(sessionId);
341
+ }
342
+ }
343
+ }
344
+ const cancelledWorkRequestIds = Array.from(new Set(queuedCancellation.cancelledWorkRequestIds));
345
+ const shouldStartNext = stoppedSessionIds.length > 0
346
+ || queuedCancellation.cancelledInvocationIds.length > 0
347
+ || cancelledWorkRequestIds.length > 0;
348
+ const startedInvocations = shouldStartNext
349
+ ? await this.startNextSessions(teamRunId)
350
+ : [];
351
+ if (shouldStartNext) {
352
+ await this.emitTeamRunInvalidatedById(teamRunId, ['work-requests', 'agent-invocations', 'team-run'], 'member-work-stopped');
353
+ }
354
+ return {
355
+ stoppedSessionIds,
356
+ cancelledInvocationIds: queuedCancellation.cancelledInvocationIds,
357
+ cancelledWorkRequestIds,
358
+ startedInvocations,
359
+ };
360
+ }
361
+ releaseInvocationLocks(invocationId) {
362
+ this.lockService.releaseByOwner(invocationId);
363
+ }
364
+ async createInvocationForClaimedWorkRequest(teamRun, member, workRequest, invocationId) {
365
+ return prisma.$transaction(async (tx) => {
366
+ const claimed = await tx.workRequest.updateMany({
367
+ where: {
368
+ id: workRequest.id,
369
+ teamRunId: teamRun.id,
370
+ status: 'QUEUED',
371
+ },
372
+ data: { status: 'STARTED' },
373
+ });
374
+ if (claimed.count !== 1) {
375
+ return null;
376
+ }
377
+ if (workRequest.cancelQueued) {
378
+ await tx.workRequest.updateMany({
379
+ where: {
380
+ teamRunId: teamRun.id,
381
+ targetMemberId: member.id,
382
+ status: 'QUEUED',
383
+ id: { not: workRequest.id },
384
+ },
385
+ data: { status: 'CANCELLED' },
386
+ });
387
+ }
388
+ return tx.agentInvocation.create({
389
+ data: {
390
+ id: invocationId,
391
+ teamRunId: teamRun.id,
392
+ workRequestId: workRequest.id,
393
+ memberId: member.id,
394
+ workspaceId: this.resolveInvocationWorkspaceId(teamRun, member),
395
+ sessionId: null,
396
+ status: 'QUEUED',
397
+ },
398
+ });
399
+ });
400
+ }
401
+ async createRunningInvocationForClaimedWorkRequest(teamRun, member, workRequest, invocationId, workspaceId, sessionId) {
402
+ return prisma.$transaction(async (tx) => {
403
+ const claimed = await tx.workRequest.updateMany({
404
+ where: {
405
+ id: workRequest.id,
406
+ teamRunId: teamRun.id,
407
+ status: 'QUEUED',
408
+ },
409
+ data: { status: 'STARTED' },
410
+ });
411
+ if (claimed.count !== 1) {
412
+ return null;
413
+ }
414
+ if (workRequest.cancelQueued) {
415
+ await tx.workRequest.updateMany({
416
+ where: {
417
+ teamRunId: teamRun.id,
418
+ targetMemberId: member.id,
419
+ status: 'QUEUED',
420
+ id: { not: workRequest.id },
421
+ },
422
+ data: { status: 'CANCELLED' },
423
+ });
424
+ }
425
+ return tx.agentInvocation.create({
426
+ data: {
427
+ id: invocationId,
428
+ teamRunId: teamRun.id,
429
+ workRequestId: workRequest.id,
430
+ memberId: member.id,
431
+ workspaceId,
432
+ sessionId,
433
+ status: 'RUNNING',
434
+ },
435
+ });
436
+ });
437
+ }
438
+ async getSchedulingContext(teamRunId) {
439
+ const teamRun = await prisma.teamRun.findUnique({
440
+ where: { id: teamRunId },
441
+ include: {
442
+ task: {
443
+ include: {
444
+ workspaces: {
445
+ where: { status: 'ACTIVE' },
446
+ orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
447
+ take: 1,
448
+ },
449
+ },
450
+ },
451
+ },
452
+ });
453
+ if (!teamRun) {
454
+ throw new NotFoundError('TeamRun', teamRunId);
455
+ }
456
+ const [members, workRequests] = await Promise.all([
457
+ prisma.teamMember.findMany({
458
+ where: { teamRunId },
459
+ orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
460
+ }),
461
+ prisma.workRequest.findMany({
462
+ where: { teamRunId, status: 'QUEUED' },
463
+ orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
464
+ }),
465
+ ]);
466
+ return {
467
+ teamRun,
468
+ memberById: new Map(members.map((member) => [member.id, member])),
469
+ workRequests,
470
+ };
471
+ }
472
+ async getWorkRequestOrThrow(workRequestId) {
473
+ const workRequest = await prisma.workRequest.findUnique({ where: { id: workRequestId } });
474
+ if (!workRequest) {
475
+ throw new NotFoundError('WorkRequest', workRequestId);
476
+ }
477
+ return workRequest;
478
+ }
479
+ async getTeamMemberOrThrow(teamRunId, memberId) {
480
+ const member = await prisma.teamMember.findFirst({ where: { id: memberId, teamRunId } });
481
+ if (member) {
482
+ return member;
483
+ }
484
+ const teamRun = await prisma.teamRun.findUnique({
485
+ where: { id: teamRunId },
486
+ select: { id: true },
487
+ });
488
+ if (!teamRun) {
489
+ throw new NotFoundError('TeamRun', teamRunId);
490
+ }
491
+ throw new NotFoundError('TeamMember', memberId);
492
+ }
493
+ async transitionWorkRequestStatus(workRequestId, action, allowedStatuses, nextStatus) {
494
+ const updated = await prisma.$transaction(async (tx) => {
495
+ const claimed = allowedStatuses.length === 1
496
+ ? await tx.workRequest.updateMany({
497
+ where: { id: workRequestId, status: allowedStatuses[0] },
498
+ data: { status: nextStatus },
499
+ })
500
+ : await tx.workRequest.updateMany({
501
+ where: { id: workRequestId, status: { in: allowedStatuses } },
502
+ data: { status: nextStatus },
503
+ });
504
+ if (claimed.count !== 1) {
505
+ return null;
506
+ }
507
+ return tx.workRequest.findUnique({ where: { id: workRequestId } });
508
+ });
509
+ if (!updated) {
510
+ const current = await this.getWorkRequestOrThrow(workRequestId);
511
+ throw invalidTransition(action, current.status);
512
+ }
513
+ return this.serializeWorkRequest(updated);
514
+ }
515
+ async cancelInvocationsWithoutSession(invocationIds) {
516
+ if (invocationIds.length === 0) {
517
+ return { cancelledInvocationIds: [], cancelledWorkRequestIds: [] };
518
+ }
519
+ const cancelled = await prisma.$transaction(async (tx) => {
520
+ const invocations = await tx.agentInvocation.findMany({
521
+ where: {
522
+ id: { in: invocationIds },
523
+ sessionId: null,
524
+ status: { in: STOPPABLE_INVOCATION_STATUSES },
525
+ },
526
+ select: { id: true, workRequestId: true },
527
+ });
528
+ if (invocations.length === 0) {
529
+ return { invocationIds: [], workRequestIds: [] };
530
+ }
531
+ const cancellableWorkRequestIds = invocations.map((invocation) => invocation.workRequestId);
532
+ await tx.agentInvocation.updateMany({
533
+ where: { id: { in: invocations.map((invocation) => invocation.id) } },
534
+ data: {
535
+ status: 'CANCELLED',
536
+ nextRoomReplyReminderAt: null,
537
+ },
538
+ });
539
+ await tx.workRequest.updateMany({
540
+ where: {
541
+ id: { in: cancellableWorkRequestIds },
542
+ status: { in: ['PENDING_APPROVAL', 'QUEUED', 'STARTED'] },
543
+ },
544
+ data: { status: 'CANCELLED' },
545
+ });
546
+ return {
547
+ invocationIds: invocations.map((invocation) => invocation.id),
548
+ workRequestIds: cancellableWorkRequestIds,
549
+ };
550
+ });
551
+ for (const invocationId of cancelled.invocationIds) {
552
+ this.releaseInvocationLocks(invocationId);
553
+ }
554
+ return {
555
+ cancelledInvocationIds: cancelled.invocationIds,
556
+ cancelledWorkRequestIds: cancelled.workRequestIds,
557
+ };
558
+ }
559
+ async cancelQueuedWorkRequestsForMember(teamRunId, memberId) {
560
+ const workRequests = await prisma.workRequest.findMany({
561
+ where: {
562
+ teamRunId,
563
+ targetMemberId: memberId,
564
+ status: { in: CANCELLABLE_QUEUED_WORK_REQUEST_STATUSES },
565
+ },
566
+ select: { id: true },
567
+ });
568
+ if (workRequests.length === 0) {
569
+ return [];
570
+ }
571
+ const workRequestIds = workRequests.map((workRequest) => workRequest.id);
572
+ await prisma.workRequest.updateMany({
573
+ where: {
574
+ id: { in: workRequestIds },
575
+ status: { in: CANCELLABLE_QUEUED_WORK_REQUEST_STATUSES },
576
+ },
577
+ data: { status: 'CANCELLED' },
578
+ });
579
+ return workRequestIds;
580
+ }
581
+ async findActiveMemberIds(teamRunId) {
582
+ const activeInvocations = await prisma.agentInvocation.findMany({
583
+ where: {
584
+ teamRunId,
585
+ status: { in: ACTIVE_INVOCATION_STATUSES },
586
+ },
587
+ select: { memberId: true },
588
+ });
589
+ return new Set(activeInvocations.map((invocation) => invocation.memberId));
590
+ }
591
+ async hasActiveInvocation(teamRunId, memberId) {
592
+ const count = await prisma.agentInvocation.count({
593
+ where: {
594
+ teamRunId,
595
+ memberId,
596
+ status: { in: ACTIVE_INVOCATION_STATUSES },
597
+ },
598
+ });
599
+ return count > 0;
600
+ }
601
+ async getOrCreateSharedWorkspace(teamRun) {
602
+ const existingClaim = TeamSchedulerService.sharedWorkspaceClaims.get(teamRun.taskId);
603
+ if (existingClaim) {
604
+ return existingClaim;
605
+ }
606
+ const claim = this.findOrCreateSharedWorkspace(teamRun);
607
+ TeamSchedulerService.sharedWorkspaceClaims.set(teamRun.taskId, claim);
608
+ try {
609
+ return await claim;
610
+ }
611
+ finally {
612
+ if (TeamSchedulerService.sharedWorkspaceClaims.get(teamRun.taskId) === claim) {
613
+ TeamSchedulerService.sharedWorkspaceClaims.delete(teamRun.taskId);
614
+ }
615
+ }
616
+ }
617
+ async findOrCreateSharedWorkspace(teamRun) {
618
+ const activeWorkspace = await prisma.workspace.findFirst({
619
+ where: { taskId: teamRun.taskId, status: 'ACTIVE' },
620
+ orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
621
+ select: { id: true },
622
+ });
623
+ if (activeWorkspace) {
624
+ return activeWorkspace;
625
+ }
626
+ const workspace = await this.workspaceService.create(teamRun.taskId);
627
+ await this.emitTeamRunInvalidated(teamRun, ['workspaces'], 'agent-invocation-updated');
628
+ return workspace;
629
+ }
630
+ buildSessionPrompt(member, workRequest) {
631
+ return `${member.rolePrompt}\n\nTask:\n${workRequest.instruction}`;
632
+ }
633
+ async findResumeSourceSessionId(member, currentSessionId) {
634
+ if (member.sessionPolicy !== 'resume_last') {
635
+ return null;
636
+ }
637
+ const previousInvocation = await prisma.agentInvocation.findFirst({
638
+ where: {
639
+ memberId: member.id,
640
+ sessionId: {
641
+ not: null,
642
+ notIn: [currentSessionId],
643
+ },
644
+ },
645
+ orderBy: [{ updatedAt: 'desc' }, { createdAt: 'desc' }, { id: 'desc' }],
646
+ select: {
647
+ sessionId: true,
648
+ },
649
+ });
650
+ if (!previousInvocation?.sessionId) {
651
+ return null;
652
+ }
653
+ const previousSession = await prisma.session.findUnique({
654
+ where: { id: previousInvocation.sessionId },
655
+ select: { logSnapshot: true },
656
+ });
657
+ if (!previousSession?.logSnapshot) {
658
+ return null;
659
+ }
660
+ return previousInvocation.sessionId;
661
+ }
662
+ async markInvocationStartFailed(invocationId, sessionId) {
663
+ await prisma.$transaction([
664
+ prisma.agentInvocation.update({
665
+ where: { id: invocationId },
666
+ data: { status: 'FAILED' },
667
+ }),
668
+ prisma.session.update({
669
+ where: { id: sessionId },
670
+ data: { status: 'FAILED' },
671
+ }),
672
+ ]);
673
+ }
674
+ async emitTeamRunInvalidated(teamRun, scopes, reason) {
675
+ await emitTeamRunInvalidated({
676
+ teamRunId: teamRun.id,
677
+ taskId: teamRun.taskId,
678
+ projectId: teamRun.task.projectId,
679
+ scopes,
680
+ reason,
681
+ });
682
+ }
683
+ async emitTeamRunInvalidatedById(teamRunId, scopes, reason) {
684
+ await emitTeamRunInvalidated({ teamRunId, scopes, reason });
685
+ }
686
+ async markSessionFailed(sessionId) {
687
+ await prisma.session.update({
688
+ where: { id: sessionId },
689
+ data: { status: 'FAILED' },
690
+ });
691
+ }
692
+ getRequiredLocks(teamRun, member) {
693
+ const request = {
694
+ teamRunId: teamRun.id,
695
+ memberId: member.id,
696
+ workspaceId: this.resolveLockWorkspaceId(teamRun, member),
697
+ projectId: teamRun.task.projectId,
698
+ capabilities: parseJsonField(member.capabilities, DEFAULT_CAPABILITIES),
699
+ workspacePolicy: member.workspacePolicy,
700
+ };
701
+ return this.lockService.getRequiredLocks(request);
702
+ }
703
+ resolveInvocationWorkspaceId(teamRun, member) {
704
+ if (member.workspacePolicy !== 'shared' && member.workspacePolicy !== 'none') {
705
+ return null;
706
+ }
707
+ return teamRun.task.workspaces[0]?.id ?? null;
708
+ }
709
+ resolveLockWorkspaceId(teamRun, member) {
710
+ if (member.workspacePolicy !== 'shared') {
711
+ return null;
712
+ }
713
+ return `task:${teamRun.taskId}`;
714
+ }
715
+ memberSchedulingLockKey(teamRunId, memberId) {
716
+ return `scheduling:${teamRunId}:member:${memberId}`;
717
+ }
718
+ acquireMemberSchedulingLock(lockKey) {
719
+ if (TeamSchedulerService.memberSchedulingLocks.has(lockKey)) {
720
+ return false;
721
+ }
722
+ TeamSchedulerService.memberSchedulingLocks.add(lockKey);
723
+ return true;
724
+ }
725
+ releaseMemberSchedulingLock(lockKey) {
726
+ TeamSchedulerService.memberSchedulingLocks.delete(lockKey);
727
+ }
728
+ serializeWorkRequest(workRequest) {
729
+ return {
730
+ ...workRequest,
731
+ requesterType: workRequest.requesterType,
732
+ ifBusy: workRequest.ifBusy,
733
+ status: workRequest.status,
734
+ createdAt: toIso(workRequest.createdAt),
735
+ updatedAt: toIso(workRequest.updatedAt),
736
+ };
737
+ }
738
+ serializeAgentInvocation(invocation) {
739
+ return {
740
+ ...invocation,
741
+ status: invocation.status,
742
+ createdAt: toIso(invocation.createdAt),
743
+ updatedAt: toIso(invocation.updatedAt),
744
+ nextRoomReplyReminderAt: invocation.nextRoomReplyReminderAt
745
+ ? toIso(invocation.nextRoomReplyReminderAt)
746
+ : null,
747
+ };
748
+ }
749
+ }
750
+ //# sourceMappingURL=team-scheduler.service.js.map