praxis-agent 0.35.0 → 0.37.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.
- package/README.md +20 -2
- package/dist/application/background-agent-manager.d.ts +16 -6
- package/dist/application/background-agent-manager.js +139 -97
- package/dist/application/context-engine.d.ts +56 -0
- package/dist/application/context-engine.js +129 -0
- package/dist/application/durable-follow-up.d.ts +14 -0
- package/dist/application/durable-follow-up.js +52 -0
- package/dist/application/native-session-transcript.d.ts +72 -0
- package/dist/application/native-session-transcript.js +351 -0
- package/dist/application/native-sidechain-transcript.d.ts +34 -0
- package/dist/application/native-sidechain-transcript.js +156 -0
- package/dist/application/session-service.d.ts +23 -4
- package/dist/application/session-service.js +1130 -494
- package/dist/application/subagent-service.d.ts +5 -1
- package/dist/application/subagent-service.js +749 -320
- package/dist/application/team-agent-runtime.d.ts +29 -0
- package/dist/application/team-agent-runtime.js +169 -0
- package/dist/application/team-capability.d.ts +27 -0
- package/dist/application/team-capability.js +41 -0
- package/dist/application/team-lead-operations.d.ts +38 -0
- package/dist/application/team-lead-operations.js +155 -0
- package/dist/application/team-mailbox.d.ts +54 -0
- package/dist/application/team-mailbox.js +196 -0
- package/dist/application/team-manager.d.ts +113 -0
- package/dist/application/team-manager.js +681 -0
- package/dist/application/team-workspace.d.ts +29 -0
- package/dist/application/team-workspace.js +197 -0
- package/dist/application/top-level-agent-manager.d.ts +1 -0
- package/dist/application/top-level-agent-manager.js +1025 -403
- package/dist/application/transcript-projection.d.ts +35 -0
- package/dist/application/transcript-projection.js +139 -0
- package/dist/application/turn-lifecycle.d.ts +2 -0
- package/dist/application/turn-memory-coordinator.d.ts +43 -0
- package/dist/application/turn-memory-coordinator.js +98 -0
- package/dist/cli/agents-dashboard.js +10 -5
- package/dist/cli/interactive.d.ts +7 -0
- package/dist/cli/interactive.js +645 -334
- package/dist/cli/tui/claude-style.d.ts +8 -25
- package/dist/cli/tui/claude-style.js +299 -277
- package/dist/cli/tui/decision-surface-model.d.ts +71 -0
- package/dist/cli/tui/decision-surface-model.js +131 -0
- package/dist/cli/tui/decision-surface.d.ts +7 -0
- package/dist/cli/tui/decision-surface.js +41 -0
- package/dist/cli/tui/help-surface-model.d.ts +68 -0
- package/dist/cli/tui/help-surface-model.js +131 -0
- package/dist/cli/tui/mcp-panel.js +1 -2
- package/dist/cli/tui/permission-surface-model.d.ts +195 -0
- package/dist/cli/tui/permission-surface-model.js +360 -0
- package/dist/cli/tui/permission-surface.d.ts +7 -0
- package/dist/cli/tui/permission-surface.js +69 -0
- package/dist/cli/tui/presentation-environment.d.ts +32 -0
- package/dist/cli/tui/presentation-environment.js +149 -0
- package/dist/cli/tui/task-panel.js +10 -1
- package/dist/cli/tui/theme.d.ts +46 -3
- package/dist/cli/tui/theme.js +414 -56
- package/dist/cli/tui/tool-permission.js +16 -8
- package/dist/cli/tui/transcript-presentation.d.ts +19 -1
- package/dist/cli/tui/transcript-presentation.js +26 -6
- package/dist/cli/tui/transcript-viewport.d.ts +18 -0
- package/dist/cli/tui/transcript-viewport.js +517 -111
- package/dist/cli/tui/transcript-window-model.d.ts +108 -0
- package/dist/cli/tui/transcript-window-model.js +772 -0
- package/dist/cli/tui/tui-interaction-router.d.ts +78 -0
- package/dist/cli/tui/tui-interaction-router.js +127 -0
- package/dist/cli/tui/tui-screen-model.d.ts +114 -0
- package/dist/cli/tui/tui-screen-model.js +210 -0
- package/dist/cli/tui/tui-view-model.d.ts +2 -49
- package/dist/cli/tui/tui-view-model.js +1 -68
- package/dist/cli-runtime.d.ts +4 -2
- package/dist/cli-runtime.js +397 -32
- package/dist/compatibility/claude/transcript-codec.js +6 -0
- package/dist/core/agent-orchestration.d.ts +31 -0
- package/dist/core/agent-orchestration.js +235 -0
- package/dist/core/permission-policy.d.ts +4 -0
- package/dist/core/permission-policy.js +21 -0
- package/dist/core/runtime.d.ts +6 -0
- package/dist/core/runtime.js +20 -2
- package/dist/core/team-mailbox.d.ts +54 -0
- package/dist/core/team-mailbox.js +212 -0
- package/dist/core/team-ownership.d.ts +84 -0
- package/dist/core/team-ownership.js +505 -0
- package/dist/core/tool-execution-scheduler.js +6 -1
- package/dist/core/transcript-event.d.ts +5 -1
- package/dist/core/transcript-event.js +12 -0
- package/dist/persistence/claude-job-store.d.ts +58 -0
- package/dist/persistence/claude-job-store.js +483 -1
- package/dist/persistence/claude-transcript-store.d.ts +2 -1
- package/dist/persistence/claude-transcript-store.js +2 -10
- package/dist/persistence/native-team-mailbox.d.ts +68 -0
- package/dist/persistence/native-team-mailbox.js +542 -0
- package/dist/persistence/native-team-store.d.ts +34 -0
- package/dist/persistence/native-team-store.js +257 -0
- package/dist/persistence/native-transcript-reader.d.ts +53 -0
- package/dist/persistence/native-transcript-reader.js +283 -0
- package/dist/persistence/native-transcript-store.d.ts +69 -0
- package/dist/persistence/native-transcript-store.js +321 -0
- package/dist/persistence/subagent-lifecycle-store.d.ts +41 -8
- package/dist/persistence/subagent-lifecycle-store.js +373 -146
- package/dist/persistence/transcript-file-append.d.ts +2 -0
- package/dist/persistence/transcript-file-append.js +10 -0
- package/dist/platform/exclusive-file-lease.d.ts +3 -0
- package/dist/platform/exclusive-file-lease.js +3 -0
- package/dist/tools/claude-capabilities.d.ts +4 -0
- package/dist/tools/claude-capabilities.js +18 -2
- package/dist/tools/team-lead-tools.d.ts +19 -0
- package/dist/tools/team-lead-tools.js +479 -0
- package/dist/tools/team-member-tools.d.ts +21 -0
- package/dist/tools/team-member-tools.js +259 -0
- package/package.json +1 -1
- package/dist/cli/tui/fullscreen-renderer.d.ts +0 -14
- package/dist/cli/tui/fullscreen-renderer.js +0 -13
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import { createAgentLifecycle, markLifecycleOrphaned, transitionLifecycle, } from '../core/agent-orchestration.js';
|
|
2
|
+
import { DEFAULT_TEAM_BUDGETS, parseTeamSnapshot, selectTeamTaskAdmissions, withTeamTaskExecution, acceptTeamTaskExecution, recordTeamTaskProgress, updateTeamUsageDuration, } from '../core/team-ownership.js';
|
|
3
|
+
import { NativeTeamStore, } from '../persistence/native-team-store.js';
|
|
4
|
+
import { NativeTeamWorkspaceProvider, } from './team-workspace.js';
|
|
5
|
+
import { TeamMemberToolRegistry } from '../tools/team-member-tools.js';
|
|
6
|
+
import { TeamMailboxService } from './team-mailbox.js';
|
|
7
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
8
|
+
function owner(claim) {
|
|
9
|
+
return {
|
|
10
|
+
token: claim.token,
|
|
11
|
+
pid: claim.pid,
|
|
12
|
+
acquiredAt: claim.acquiredAt,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function now() {
|
|
16
|
+
return new Date().toISOString();
|
|
17
|
+
}
|
|
18
|
+
function parseTeamAgentRunResult(value) {
|
|
19
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
20
|
+
throw new Error('Invalid Team agent result');
|
|
21
|
+
const result = value;
|
|
22
|
+
if (Object.keys(result).some((key) => !['status', 'totalTokens', 'durationMs'].includes(key)) ||
|
|
23
|
+
!['completed', 'failed', 'orphaned'].includes(String(result.status)) ||
|
|
24
|
+
!Number.isSafeInteger(result.totalTokens) ||
|
|
25
|
+
Number(result.totalTokens) < 0 ||
|
|
26
|
+
!Number.isSafeInteger(result.durationMs) ||
|
|
27
|
+
Number(result.durationMs) < 0)
|
|
28
|
+
throw new Error('Invalid Team agent result');
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
status: result.status,
|
|
31
|
+
totalTokens: result.totalTokens,
|
|
32
|
+
durationMs: result.durationMs,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export class LocalTeamManager {
|
|
36
|
+
store;
|
|
37
|
+
options;
|
|
38
|
+
workspace;
|
|
39
|
+
constructor(store, options, workspace) {
|
|
40
|
+
this.store = store;
|
|
41
|
+
this.options = options;
|
|
42
|
+
this.workspace = workspace;
|
|
43
|
+
}
|
|
44
|
+
static async open(options) {
|
|
45
|
+
if (!Number.isSafeInteger(options.maxConcurrent) ||
|
|
46
|
+
options.maxConcurrent <= 0)
|
|
47
|
+
throw new Error('Invalid maxConcurrent');
|
|
48
|
+
const store = await NativeTeamStore.open({
|
|
49
|
+
nativeRoot: options.nativeRoot,
|
|
50
|
+
cwd: options.cwd,
|
|
51
|
+
});
|
|
52
|
+
const workspace = options.workspace ??
|
|
53
|
+
(await NativeTeamWorkspaceProvider.open({
|
|
54
|
+
nativeRoot: options.nativeRoot,
|
|
55
|
+
cwd: options.cwd,
|
|
56
|
+
projectIdentity: store.projectIdentity,
|
|
57
|
+
}));
|
|
58
|
+
return new LocalTeamManager(store, options, workspace);
|
|
59
|
+
}
|
|
60
|
+
async create(input) {
|
|
61
|
+
if (input.budgets?.maxConcurrent !== undefined &&
|
|
62
|
+
input.budgets.maxConcurrent > this.options.maxConcurrent)
|
|
63
|
+
throw new Error('Team maxConcurrent exceeds local host ceiling');
|
|
64
|
+
const timestamp = now();
|
|
65
|
+
const snapshot = parseTeamSnapshot({
|
|
66
|
+
version: 2,
|
|
67
|
+
revision: 0,
|
|
68
|
+
teamId: input.teamId,
|
|
69
|
+
name: input.name,
|
|
70
|
+
projectIdentity: this.store.projectIdentity,
|
|
71
|
+
leadSessionId: input.leadSessionId,
|
|
72
|
+
roster: input.roster,
|
|
73
|
+
tasks: input.tasks.map((task) => ({
|
|
74
|
+
...task,
|
|
75
|
+
execution: null,
|
|
76
|
+
usage: { generation: 0, totalTokens: 0, durationMs: 0 },
|
|
77
|
+
})),
|
|
78
|
+
policy: {
|
|
79
|
+
lead: input.leadPolicy ?? 'hybrid',
|
|
80
|
+
execution: input.executionPolicy ?? 'sequential',
|
|
81
|
+
commit: input.commitPolicy ?? 'lead',
|
|
82
|
+
},
|
|
83
|
+
budgets: { ...DEFAULT_TEAM_BUDGETS, ...(input.budgets ?? {}) },
|
|
84
|
+
usage: { totalTokens: 0, durationMs: 0, exhausted: null },
|
|
85
|
+
createdAt: timestamp,
|
|
86
|
+
updatedAt: timestamp,
|
|
87
|
+
});
|
|
88
|
+
let claim;
|
|
89
|
+
let team;
|
|
90
|
+
try {
|
|
91
|
+
claim = await this.store.createAndClaim(snapshot);
|
|
92
|
+
team = new LocalTeam(this.options, this.workspace, claim, new TeamMailboxService({
|
|
93
|
+
nativeRoot: this.options.nativeRoot,
|
|
94
|
+
projectIdentity: this.store.projectIdentity,
|
|
95
|
+
teamId: snapshot.teamId,
|
|
96
|
+
participants: [
|
|
97
|
+
'lead',
|
|
98
|
+
...snapshot.roster.map((member) => member.name),
|
|
99
|
+
],
|
|
100
|
+
}));
|
|
101
|
+
await team.initialize(false);
|
|
102
|
+
return team;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (!team)
|
|
106
|
+
await claim?.release();
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async resume(input) {
|
|
111
|
+
let claim;
|
|
112
|
+
let team;
|
|
113
|
+
try {
|
|
114
|
+
claim = await this.store.claim(input.teamId);
|
|
115
|
+
const current = await claim.read();
|
|
116
|
+
if (current.leadSessionId !== input.leadSessionId)
|
|
117
|
+
throw new Error('Team lead session mismatch');
|
|
118
|
+
team = new LocalTeam(this.options, this.workspace, claim, new TeamMailboxService({
|
|
119
|
+
nativeRoot: this.options.nativeRoot,
|
|
120
|
+
projectIdentity: this.store.projectIdentity,
|
|
121
|
+
teamId: current.teamId,
|
|
122
|
+
participants: [
|
|
123
|
+
'lead',
|
|
124
|
+
...current.roster.map((member) => member.name),
|
|
125
|
+
],
|
|
126
|
+
}));
|
|
127
|
+
await team.initialize(true);
|
|
128
|
+
return team;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (!team)
|
|
132
|
+
await claim?.release();
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
list() {
|
|
137
|
+
return this.store.list();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export class LocalTeam {
|
|
141
|
+
options;
|
|
142
|
+
workspace;
|
|
143
|
+
claim;
|
|
144
|
+
mailbox;
|
|
145
|
+
mutation = Promise.resolve();
|
|
146
|
+
active = new Map();
|
|
147
|
+
admissionOpen = true;
|
|
148
|
+
stopping;
|
|
149
|
+
detaching;
|
|
150
|
+
detachedSnapshot;
|
|
151
|
+
finalSnapshot;
|
|
152
|
+
pumpFailure;
|
|
153
|
+
indeterminate = new Set();
|
|
154
|
+
progressWrites = new Map();
|
|
155
|
+
stopStarted;
|
|
156
|
+
resolveStopStarted;
|
|
157
|
+
deadlineTimer;
|
|
158
|
+
progressOpen = true;
|
|
159
|
+
constructor(options, workspace, claim, mailbox) {
|
|
160
|
+
this.options = options;
|
|
161
|
+
this.workspace = workspace;
|
|
162
|
+
this.claim = claim;
|
|
163
|
+
this.mailbox = mailbox;
|
|
164
|
+
let resolveStopStarted;
|
|
165
|
+
this.stopStarted = new Promise((resolve) => {
|
|
166
|
+
resolveStopStarted = resolve;
|
|
167
|
+
});
|
|
168
|
+
this.resolveStopStarted = resolveStopStarted;
|
|
169
|
+
}
|
|
170
|
+
mailboxEndpoint(participant) {
|
|
171
|
+
return this.mailbox.endpoint(participant);
|
|
172
|
+
}
|
|
173
|
+
async initialize(resume) {
|
|
174
|
+
try {
|
|
175
|
+
if (resume)
|
|
176
|
+
await this.enqueue(() => this.reconcileOrphans());
|
|
177
|
+
await this.initializeDeadline();
|
|
178
|
+
await this.pump();
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
await this.stop({ drainMs: 0 }).catch(() => undefined);
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
snapshot() {
|
|
186
|
+
if (this.finalSnapshot)
|
|
187
|
+
return Promise.resolve(this.finalSnapshot);
|
|
188
|
+
if (this.detachedSnapshot)
|
|
189
|
+
return Promise.resolve(this.detachedSnapshot);
|
|
190
|
+
if (this.stopping)
|
|
191
|
+
return this.stopping;
|
|
192
|
+
return this.enqueue(() => this.claim.read());
|
|
193
|
+
}
|
|
194
|
+
async waitForIdle() {
|
|
195
|
+
if (this.finalSnapshot)
|
|
196
|
+
return this.finalSnapshot;
|
|
197
|
+
if (this.detachedSnapshot)
|
|
198
|
+
return this.detachedSnapshot;
|
|
199
|
+
if (this.stopping)
|
|
200
|
+
return this.stopping;
|
|
201
|
+
for (;;) {
|
|
202
|
+
if (this.pumpFailure)
|
|
203
|
+
throw this.pumpFailure;
|
|
204
|
+
await this.mutation;
|
|
205
|
+
if (this.stopping)
|
|
206
|
+
return this.stopping;
|
|
207
|
+
if (!this.admissionOpen && this.active.size === 0) {
|
|
208
|
+
const snapshot = await this.claim.read();
|
|
209
|
+
if (snapshot.usage.exhausted) {
|
|
210
|
+
this.beginBudgetShutdown();
|
|
211
|
+
if (this.stopping)
|
|
212
|
+
return this.stopping;
|
|
213
|
+
}
|
|
214
|
+
return snapshot;
|
|
215
|
+
}
|
|
216
|
+
if (this.active.size > 0) {
|
|
217
|
+
await Promise.race([
|
|
218
|
+
Promise.allSettled([...this.active.values()].map((entry) => entry.promise)),
|
|
219
|
+
this.stopStarted,
|
|
220
|
+
]);
|
|
221
|
+
if (this.stopping)
|
|
222
|
+
return this.stopping;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
await this.pump();
|
|
226
|
+
await this.mutation;
|
|
227
|
+
if (this.stopping)
|
|
228
|
+
return this.stopping;
|
|
229
|
+
if (this.active.size === 0) {
|
|
230
|
+
const snapshot = await this.claim.read();
|
|
231
|
+
if (selectTeamTaskAdmissions({
|
|
232
|
+
snapshot,
|
|
233
|
+
maxConcurrent: this.options.maxConcurrent,
|
|
234
|
+
}).length === 0)
|
|
235
|
+
return snapshot;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async accept(taskId, generation, decision = 'accepted') {
|
|
240
|
+
if (this.detaching)
|
|
241
|
+
throw new Error('Team is detaching');
|
|
242
|
+
if (this.detachedSnapshot)
|
|
243
|
+
throw new Error('Team is detached');
|
|
244
|
+
if (!this.admissionOpen || this.finalSnapshot)
|
|
245
|
+
throw new Error('Team is stopped');
|
|
246
|
+
await this.enqueue(async () => {
|
|
247
|
+
const current = await this.claim.read();
|
|
248
|
+
const next = acceptTeamTaskExecution(current, taskId, generation, decision);
|
|
249
|
+
return this.claim.save(current.revision, next);
|
|
250
|
+
});
|
|
251
|
+
await this.pump();
|
|
252
|
+
return this.snapshot();
|
|
253
|
+
}
|
|
254
|
+
async stop(options = {}) {
|
|
255
|
+
if (this.detaching)
|
|
256
|
+
throw new Error('Team is detaching');
|
|
257
|
+
if (this.detachedSnapshot)
|
|
258
|
+
throw new Error('Team is detached');
|
|
259
|
+
if (this.finalSnapshot)
|
|
260
|
+
return this.finalSnapshot;
|
|
261
|
+
if (this.stopping)
|
|
262
|
+
return this.stopping;
|
|
263
|
+
const stopping = this.stopWithBudget(options.drainMs);
|
|
264
|
+
this.stopping = stopping;
|
|
265
|
+
try {
|
|
266
|
+
this.finalSnapshot = await stopping;
|
|
267
|
+
return this.finalSnapshot;
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
if (this.stopping === stopping)
|
|
271
|
+
this.stopping = undefined;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async detach() {
|
|
275
|
+
if (this.finalSnapshot || this.stopping)
|
|
276
|
+
throw new Error('Team is stopping');
|
|
277
|
+
if (this.detachedSnapshot)
|
|
278
|
+
return this.detachedSnapshot;
|
|
279
|
+
if (this.detaching)
|
|
280
|
+
return this.detaching;
|
|
281
|
+
this.detaching = this.detachInternal();
|
|
282
|
+
try {
|
|
283
|
+
const snapshot = await this.detaching;
|
|
284
|
+
this.detachedSnapshot = snapshot;
|
|
285
|
+
return snapshot;
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
this.detaching = undefined;
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
if (this.detachedSnapshot)
|
|
293
|
+
this.detaching = undefined;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async detachInternal() {
|
|
297
|
+
for (;;) {
|
|
298
|
+
await this.mutation;
|
|
299
|
+
if (this.pumpFailure)
|
|
300
|
+
throw this.pumpFailure;
|
|
301
|
+
if (this.stopping)
|
|
302
|
+
throw new Error('Team is stopping');
|
|
303
|
+
if (this.active.size > 0) {
|
|
304
|
+
await Promise.allSettled([...this.active.values()].map((entry) => entry.promise));
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
await this.pump();
|
|
308
|
+
await this.mutation;
|
|
309
|
+
if (this.active.size > 0)
|
|
310
|
+
continue;
|
|
311
|
+
const current = await this.claim.read();
|
|
312
|
+
if (selectTeamTaskAdmissions({
|
|
313
|
+
snapshot: current,
|
|
314
|
+
maxConcurrent: this.options.maxConcurrent,
|
|
315
|
+
}).length > 0)
|
|
316
|
+
continue;
|
|
317
|
+
this.clearDeadlineTimer();
|
|
318
|
+
this.progressOpen = false;
|
|
319
|
+
await this.mutation;
|
|
320
|
+
if (this.stopping)
|
|
321
|
+
throw new Error('Team is stopping');
|
|
322
|
+
const snapshot = await this.enqueue(() => this.claim.read());
|
|
323
|
+
await this.claim.release();
|
|
324
|
+
return snapshot;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async stopWithBudget(requestedDrainMs) {
|
|
328
|
+
const current = await this.enqueue(() => this.claim.read());
|
|
329
|
+
const drainMs = requestedDrainMs ?? current.budgets.shutdownDrainMs;
|
|
330
|
+
if (!Number.isSafeInteger(drainMs) ||
|
|
331
|
+
drainMs < 0 ||
|
|
332
|
+
drainMs > current.budgets.shutdownDrainMs)
|
|
333
|
+
throw new Error('Invalid drainMs');
|
|
334
|
+
this.resolveStopStarted();
|
|
335
|
+
try {
|
|
336
|
+
return await this.stopInternal(drainMs);
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
this.pumpFailure ??= error;
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async stopInternal(drainMs) {
|
|
344
|
+
this.clearDeadlineTimer();
|
|
345
|
+
this.admissionOpen = false;
|
|
346
|
+
await this.enqueue(() => this.markCancelling());
|
|
347
|
+
const activeAtStop = [...this.active.entries()];
|
|
348
|
+
for (const entry of this.active.values())
|
|
349
|
+
entry.controller.abort();
|
|
350
|
+
const runtimes = [...this.active.values()].map((entry) => entry.promise);
|
|
351
|
+
if (runtimes.length > 0) {
|
|
352
|
+
let timer;
|
|
353
|
+
const timeout = new Promise((resolve) => {
|
|
354
|
+
timer = setTimeout(resolve, drainMs);
|
|
355
|
+
});
|
|
356
|
+
const winner = await Promise.race([
|
|
357
|
+
Promise.allSettled(runtimes).then(() => 'drained'),
|
|
358
|
+
timeout.then(() => 'timeout'),
|
|
359
|
+
]);
|
|
360
|
+
if (timer)
|
|
361
|
+
clearTimeout(timer);
|
|
362
|
+
// Individual completion is determined from each tracked runtime below;
|
|
363
|
+
// one timed-out sibling must not orphan a sibling that drained.
|
|
364
|
+
void winner;
|
|
365
|
+
}
|
|
366
|
+
this.progressOpen = false;
|
|
367
|
+
await this.mutation;
|
|
368
|
+
const snapshot = await this.enqueue(() => this.finishStopping(activeAtStop));
|
|
369
|
+
await this.claim.release();
|
|
370
|
+
return snapshot;
|
|
371
|
+
}
|
|
372
|
+
clearDeadlineTimer() {
|
|
373
|
+
if (!this.deadlineTimer)
|
|
374
|
+
return;
|
|
375
|
+
clearTimeout(this.deadlineTimer);
|
|
376
|
+
this.deadlineTimer = undefined;
|
|
377
|
+
}
|
|
378
|
+
enqueue(operation) {
|
|
379
|
+
const result = this.mutation.then(operation, operation);
|
|
380
|
+
this.mutation = result.then(() => undefined, () => undefined);
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
async reconcileOrphans() {
|
|
384
|
+
let current = await this.claim.read();
|
|
385
|
+
for (const task of current.tasks) {
|
|
386
|
+
const execution = task.execution;
|
|
387
|
+
if (execution &&
|
|
388
|
+
['queued', 'running', 'waiting', 'cancelling'].includes(execution.state) &&
|
|
389
|
+
execution.owner?.token !== this.claim.token) {
|
|
390
|
+
const nextExecution = markLifecycleOrphaned(execution, execution.owner?.token ?? '');
|
|
391
|
+
const next = withTeamTaskExecution(current, task.id, nextExecution);
|
|
392
|
+
current = await this.claim.save(current.revision, next);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async pump() {
|
|
397
|
+
if (!this.admissionOpen)
|
|
398
|
+
return;
|
|
399
|
+
try {
|
|
400
|
+
await this.enqueue(() => this.pumpMutation());
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
this.pumpFailure ??= error;
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
async pumpMutation() {
|
|
408
|
+
if (!this.admissionOpen)
|
|
409
|
+
return;
|
|
410
|
+
for (;;) {
|
|
411
|
+
let current = await this.claim.read();
|
|
412
|
+
const selected = selectTeamTaskAdmissions({
|
|
413
|
+
snapshot: current,
|
|
414
|
+
maxConcurrent: this.options.maxConcurrent,
|
|
415
|
+
});
|
|
416
|
+
if (selected.length === 0)
|
|
417
|
+
return;
|
|
418
|
+
const launches = [];
|
|
419
|
+
for (const taskId of selected) {
|
|
420
|
+
const task = current.tasks.find((candidate) => candidate.id === taskId);
|
|
421
|
+
if (!task)
|
|
422
|
+
throw new Error(`Unknown admitted task: ${taskId}`);
|
|
423
|
+
const queued = withTeamTaskExecution(current, task.id, createAgentLifecycle(owner(this.claim), 1));
|
|
424
|
+
current = await this.claim.save(current.revision, queued);
|
|
425
|
+
const member = current.roster.find((candidate) => candidate.name === task.assignee);
|
|
426
|
+
if (!member)
|
|
427
|
+
throw new Error(`Unknown Team member: ${task.assignee}`);
|
|
428
|
+
let workspace;
|
|
429
|
+
try {
|
|
430
|
+
workspace = await this.workspace.acquire({
|
|
431
|
+
teamId: current.teamId,
|
|
432
|
+
taskId: task.id,
|
|
433
|
+
generation: 1,
|
|
434
|
+
access: member.access,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
const failed = transitionLifecycle(current.tasks.find((candidate) => candidate.id === task.id)
|
|
439
|
+
?.execution ?? createAgentLifecycle(owner(this.claim)), 'failed', this.claim.token);
|
|
440
|
+
current = await this.claim.save(current.revision, withTeamTaskExecution(current, task.id, failed));
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const queuedTask = current.tasks.find((candidate) => candidate.id === task.id);
|
|
444
|
+
if (!queuedTask?.execution)
|
|
445
|
+
throw new Error('Missing queued Team execution');
|
|
446
|
+
const running = transitionLifecycle(queuedTask.execution, 'running', this.claim.token);
|
|
447
|
+
current = await this.claim.save(current.revision, withTeamTaskExecution(current, task.id, running));
|
|
448
|
+
const persistedTask = current.tasks.find((candidate) => candidate.id === task.id);
|
|
449
|
+
if (!persistedTask)
|
|
450
|
+
throw new Error('Missing running Team task');
|
|
451
|
+
launches.push({
|
|
452
|
+
task: persistedTask,
|
|
453
|
+
member,
|
|
454
|
+
cwd: workspace.cwd,
|
|
455
|
+
branch: workspace.branch,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
for (const launch of launches)
|
|
459
|
+
this.launch(launch.task, launch.member, launch.cwd, launch.branch);
|
|
460
|
+
if (launches.length === selected.length)
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
launch(task, member, cwd, branch) {
|
|
465
|
+
const controller = new AbortController();
|
|
466
|
+
const entry = {};
|
|
467
|
+
const promise = (async () => {
|
|
468
|
+
let result;
|
|
469
|
+
try {
|
|
470
|
+
result = parseTeamAgentRunResult(await this.options.runtime.run({
|
|
471
|
+
teamId: this.claim.teamId,
|
|
472
|
+
task,
|
|
473
|
+
member,
|
|
474
|
+
generation: task.execution?.generation ?? 1,
|
|
475
|
+
cwd,
|
|
476
|
+
branch,
|
|
477
|
+
tools: new TeamMemberToolRegistry({
|
|
478
|
+
base: this.options.baseTools,
|
|
479
|
+
access: member.access,
|
|
480
|
+
cwd,
|
|
481
|
+
}),
|
|
482
|
+
permissions: this.options.permissions,
|
|
483
|
+
signal: controller.signal,
|
|
484
|
+
mailbox: this.mailbox.endpoint(member.name),
|
|
485
|
+
reportProgress: (progress) => {
|
|
486
|
+
if (entry.runtimeSettled || entry.settled)
|
|
487
|
+
return;
|
|
488
|
+
void this.queueProgress(task.id, task.execution?.generation ?? 1, progress);
|
|
489
|
+
},
|
|
490
|
+
}));
|
|
491
|
+
entry.runtimeSettled = true;
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
entry.runtimeSettled = true;
|
|
495
|
+
try {
|
|
496
|
+
await (this.progressWrites.get(task.id) ?? Promise.resolve());
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
entry.settled = true;
|
|
500
|
+
this.active.delete(task.id);
|
|
501
|
+
this.progressWrites.delete(task.id);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
await this.complete(task.id, task.execution?.generation ?? 1, {
|
|
505
|
+
status: 'failed',
|
|
506
|
+
});
|
|
507
|
+
entry.settled = true;
|
|
508
|
+
this.active.delete(task.id);
|
|
509
|
+
this.progressWrites.delete(task.id);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
await (this.progressWrites.get(task.id) ?? Promise.resolve());
|
|
514
|
+
await this.queueProgress(task.id, entry.generation, {
|
|
515
|
+
generation: entry.generation,
|
|
516
|
+
totalTokens: result.totalTokens,
|
|
517
|
+
durationMs: result.durationMs,
|
|
518
|
+
});
|
|
519
|
+
await this.complete(task.id, entry.generation, {
|
|
520
|
+
status: result.status,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
finally {
|
|
524
|
+
entry.settled = true;
|
|
525
|
+
this.active.delete(task.id);
|
|
526
|
+
this.progressWrites.delete(task.id);
|
|
527
|
+
}
|
|
528
|
+
})();
|
|
529
|
+
entry.generation = task.execution?.generation ?? 1;
|
|
530
|
+
entry.controller = controller;
|
|
531
|
+
entry.promise = promise;
|
|
532
|
+
entry.runtimeSettled = false;
|
|
533
|
+
entry.settled = false;
|
|
534
|
+
this.active.set(task.id, entry);
|
|
535
|
+
}
|
|
536
|
+
async complete(taskId, generation, result) {
|
|
537
|
+
if (!this.progressOpen || this.finalSnapshot || this.detachedSnapshot)
|
|
538
|
+
return;
|
|
539
|
+
try {
|
|
540
|
+
await this.enqueue(async () => {
|
|
541
|
+
const current = await this.claim.read();
|
|
542
|
+
const task = current.tasks.find((candidate) => candidate.id === taskId);
|
|
543
|
+
if (!task?.execution || task.execution.generation !== generation)
|
|
544
|
+
return;
|
|
545
|
+
if (!['queued', 'running', 'waiting', 'cancelling'].includes(task.execution.state))
|
|
546
|
+
return;
|
|
547
|
+
const nextExecution = result.status === 'orphaned'
|
|
548
|
+
? markLifecycleOrphaned(task.execution, this.claim.token)
|
|
549
|
+
: transitionLifecycle(task.execution, task.execution.state === 'cancelling'
|
|
550
|
+
? 'cancelled'
|
|
551
|
+
: result.status, this.claim.token);
|
|
552
|
+
await this.claim.save(current.revision, withTeamTaskExecution(current, taskId, nextExecution));
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
this.indeterminate.add(taskId);
|
|
557
|
+
this.pumpFailure ??= error;
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (this.admissionOpen) {
|
|
561
|
+
try {
|
|
562
|
+
await this.pump();
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
this.pumpFailure ??= error;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
queueProgress(taskId, generation, progress) {
|
|
570
|
+
if (!this.progressOpen || this.finalSnapshot || this.detachedSnapshot)
|
|
571
|
+
return Promise.resolve();
|
|
572
|
+
const write = this.enqueue(() => this.persistProgress(taskId, generation, progress));
|
|
573
|
+
const settled = write.then((exhausted) => {
|
|
574
|
+
if (exhausted)
|
|
575
|
+
this.beginBudgetShutdown();
|
|
576
|
+
});
|
|
577
|
+
this.progressWrites.set(taskId, settled);
|
|
578
|
+
void settled.catch((error) => {
|
|
579
|
+
this.indeterminate.add(taskId);
|
|
580
|
+
this.pumpFailure ??= error;
|
|
581
|
+
});
|
|
582
|
+
return settled;
|
|
583
|
+
}
|
|
584
|
+
async persistProgress(taskId, generation, progress) {
|
|
585
|
+
const current = await this.claim.read();
|
|
586
|
+
const task = current.tasks.find((candidate) => candidate.id === taskId);
|
|
587
|
+
if (progress.generation !== generation ||
|
|
588
|
+
!task?.execution ||
|
|
589
|
+
task.execution.generation !== generation ||
|
|
590
|
+
!['queued', 'running', 'waiting', 'cancelling'].includes(task.execution.state))
|
|
591
|
+
return false;
|
|
592
|
+
const taskNext = recordTeamTaskProgress(current, taskId, progress);
|
|
593
|
+
let saved = await this.claim.save(current.revision, taskNext);
|
|
594
|
+
const next = updateTeamUsageDuration(saved, Math.max(saved.usage.durationMs, Date.now() - Date.parse(saved.createdAt)));
|
|
595
|
+
saved = await this.claim.save(saved.revision, next);
|
|
596
|
+
const exhausted = saved.usage.exhausted !== null;
|
|
597
|
+
if (exhausted)
|
|
598
|
+
this.admissionOpen = false;
|
|
599
|
+
return exhausted;
|
|
600
|
+
}
|
|
601
|
+
async initializeDeadline() {
|
|
602
|
+
const snapshot = await this.claim.read();
|
|
603
|
+
if (snapshot.usage.exhausted) {
|
|
604
|
+
this.admissionOpen = false;
|
|
605
|
+
this.beginBudgetShutdown();
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const deadline = Date.parse(snapshot.createdAt) + snapshot.budgets.maxDurationMs;
|
|
609
|
+
const remaining = deadline - Date.now();
|
|
610
|
+
if (remaining <= 0) {
|
|
611
|
+
const exhausted = await this.enqueue(() => this.exhaustDuration());
|
|
612
|
+
if (exhausted)
|
|
613
|
+
this.beginBudgetShutdown();
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const delay = Math.min(remaining, MAX_TIMER_MS);
|
|
617
|
+
this.deadlineTimer = setTimeout(() => {
|
|
618
|
+
this.deadlineTimer = undefined;
|
|
619
|
+
void this.initializeDeadline().catch((error) => {
|
|
620
|
+
this.pumpFailure ??= error;
|
|
621
|
+
});
|
|
622
|
+
}, delay);
|
|
623
|
+
if (typeof this.deadlineTimer === 'object' && 'unref' in this.deadlineTimer)
|
|
624
|
+
this.deadlineTimer.unref();
|
|
625
|
+
}
|
|
626
|
+
async exhaustDuration() {
|
|
627
|
+
const current = await this.claim.read();
|
|
628
|
+
if (current.usage.exhausted) {
|
|
629
|
+
this.admissionOpen = false;
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
const next = updateTeamUsageDuration(current, Math.max(current.usage.durationMs, Date.now() - Date.parse(current.createdAt), current.budgets.maxDurationMs));
|
|
633
|
+
const saved = await this.claim.save(current.revision, next);
|
|
634
|
+
const exhausted = saved.usage.exhausted !== null;
|
|
635
|
+
if (exhausted)
|
|
636
|
+
this.admissionOpen = false;
|
|
637
|
+
return exhausted;
|
|
638
|
+
}
|
|
639
|
+
beginBudgetShutdown() {
|
|
640
|
+
if (this.stopping || this.finalSnapshot || this.detachedSnapshot)
|
|
641
|
+
return;
|
|
642
|
+
this.admissionOpen = false;
|
|
643
|
+
const shutdown = this.stopWithBudget();
|
|
644
|
+
this.stopping = shutdown;
|
|
645
|
+
void shutdown
|
|
646
|
+
.then((snapshot) => {
|
|
647
|
+
this.finalSnapshot = snapshot;
|
|
648
|
+
})
|
|
649
|
+
.catch((error) => {
|
|
650
|
+
this.pumpFailure ??= error;
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
async markCancelling() {
|
|
654
|
+
let current = await this.claim.read();
|
|
655
|
+
for (const task of current.tasks) {
|
|
656
|
+
const execution = task.execution;
|
|
657
|
+
if (execution &&
|
|
658
|
+
['queued', 'running', 'waiting'].includes(execution.state)) {
|
|
659
|
+
const next = transitionLifecycle(execution, 'cancelling', this.claim.token);
|
|
660
|
+
current = await this.claim.save(current.revision, withTeamTaskExecution(current, task.id, next));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
async finishStopping(activeAtStop) {
|
|
665
|
+
let current = await this.claim.read();
|
|
666
|
+
const tracked = new Map(activeAtStop);
|
|
667
|
+
for (const task of current.tasks) {
|
|
668
|
+
const execution = task.execution;
|
|
669
|
+
if (execution?.state !== 'cancelling')
|
|
670
|
+
continue;
|
|
671
|
+
const active = tracked.get(task.id);
|
|
672
|
+
const next = this.indeterminate.has(task.id) ||
|
|
673
|
+
(active !== undefined && !active.settled)
|
|
674
|
+
? markLifecycleOrphaned(execution, this.claim.token)
|
|
675
|
+
: transitionLifecycle(execution, 'cancelled', this.claim.token);
|
|
676
|
+
current = await this.claim.save(current.revision, withTeamTaskExecution(current, task.id, next));
|
|
677
|
+
}
|
|
678
|
+
return current;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
//# sourceMappingURL=team-manager.js.map
|