oxe-cc 1.8.3 → 1.9.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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +31 -0
- package/README.md +15 -12
- package/bin/lib/oxe-operational.cjs +209 -69
- package/bin/lib/oxe-project-health.cjs +2 -0
- package/bin/lib/oxe-release.cjs +26 -0
- package/bin/oxe-cc.js +22 -13
- package/docs/RELEASE-READINESS.md +6 -1
- package/lib/runtime/scheduler/multi-agent-coordinator.d.ts +39 -0
- package/lib/runtime/scheduler/multi-agent-coordinator.js +222 -13
- package/lib/runtime/workspace/strategies/git-worktree.js +18 -9
- package/package.json +6 -3
- package/packages/runtime/package.json +1 -1
- package/packages/runtime/src/scheduler/multi-agent-coordinator.ts +320 -46
- package/packages/runtime/src/workspace/strategies/git-worktree.ts +24 -16
- package/vscode-extension/package.json +1 -1
|
@@ -2,7 +2,8 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { appendEvent } from '../events/bus';
|
|
4
4
|
import type { ExecutionGraph, GraphNode } from '../compiler/graph-compiler';
|
|
5
|
-
import type { WorkspaceManager } from '../workspace/workspace-manager';
|
|
5
|
+
import type { WorkspaceManager, WorkspaceRequest } from '../workspace/workspace-manager';
|
|
6
|
+
import type { WorkspaceLease, SnapshotRef } from '../models/workspace';
|
|
6
7
|
import type { TaskExecutor, TaskResult, SchedulerContext, RunResult } from './scheduler';
|
|
7
8
|
import { Scheduler } from './scheduler';
|
|
8
9
|
import { buildHandoff } from './agent-roles';
|
|
@@ -27,6 +28,7 @@ export interface CoordinationOptions {
|
|
|
27
28
|
runId: string;
|
|
28
29
|
onEvent?: SchedulerContext['onEvent'];
|
|
29
30
|
heartbeatTimeoutMs?: number;
|
|
31
|
+
applyWorkspaceMerges?: boolean;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export interface ArbitrationRecord {
|
|
@@ -40,10 +42,39 @@ export interface ArbitrationRecord {
|
|
|
40
42
|
recorded_at: string;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
export interface MultiAgentOwnership {
|
|
44
|
-
work_item_id: string;
|
|
45
|
-
owner_agent_id: string;
|
|
46
|
-
}
|
|
45
|
+
export interface MultiAgentOwnership {
|
|
46
|
+
work_item_id: string;
|
|
47
|
+
owner_agent_id: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface WorkspaceMergeRecord {
|
|
51
|
+
work_item_id: string;
|
|
52
|
+
agent_id: string;
|
|
53
|
+
workspace_id: string;
|
|
54
|
+
strategy: string;
|
|
55
|
+
isolation_level: 'shared' | 'isolated';
|
|
56
|
+
branch: string | null;
|
|
57
|
+
base_commit: string | null;
|
|
58
|
+
root_path: string | null;
|
|
59
|
+
mutation_scope: string[];
|
|
60
|
+
diff_paths: string[];
|
|
61
|
+
evidence_count: number;
|
|
62
|
+
verify_status: 'pass' | 'fail' | 'partial';
|
|
63
|
+
status: 'ready' | 'merged' | 'blocked' | 'skipped';
|
|
64
|
+
blocker: string | null;
|
|
65
|
+
recorded_at: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface WorkspaceMergeReport {
|
|
69
|
+
schema_version: 1;
|
|
70
|
+
run_id: string;
|
|
71
|
+
generated_at: string;
|
|
72
|
+
workspace_isolation: 'git_worktree';
|
|
73
|
+
merge_readiness: 'ready' | 'blocked' | 'partial';
|
|
74
|
+
arbitration_required: boolean;
|
|
75
|
+
blockers: string[];
|
|
76
|
+
records: WorkspaceMergeRecord[];
|
|
77
|
+
}
|
|
47
78
|
|
|
48
79
|
export interface MultiAgentStatusSnapshot {
|
|
49
80
|
run_id: string;
|
|
@@ -64,6 +95,10 @@ export interface MultiAgentStatusSnapshot {
|
|
|
64
95
|
timed_out: boolean;
|
|
65
96
|
reassigned_task_ids: string[];
|
|
66
97
|
}>;
|
|
98
|
+
worktrees: WorkspaceMergeRecord[];
|
|
99
|
+
workspace_merge: WorkspaceMergeReport;
|
|
100
|
+
merge_blockers: string[];
|
|
101
|
+
arbitration_required: boolean;
|
|
67
102
|
orphan_reassignments: Array<{ from_agent_id: string; to_agent_id: string; work_item_ids: string[] }>;
|
|
68
103
|
timed_out_agents: Array<{ agent_id: string; work_item_ids: string[]; detected_at: string }>;
|
|
69
104
|
updated_at: string;
|
|
@@ -83,6 +118,10 @@ export interface MultiAgentOperationalSummary {
|
|
|
83
118
|
orphan_reassignment_count: number;
|
|
84
119
|
timeout_count: number;
|
|
85
120
|
participating_agents: string[];
|
|
121
|
+
workspace_isolation: 'git_worktree';
|
|
122
|
+
merge_readiness: WorkspaceMergeReport['merge_readiness'];
|
|
123
|
+
arbitration_required: boolean;
|
|
124
|
+
merge_blocker_count: number;
|
|
86
125
|
health: 'healthy' | 'degraded';
|
|
87
126
|
updated_at: string;
|
|
88
127
|
}
|
|
@@ -96,6 +135,7 @@ export interface CoordinationResult {
|
|
|
96
135
|
agent_results: Array<{ agent_id: string; completed: string[]; failed: string[] }>;
|
|
97
136
|
handoffs?: CooperativeHandoff[];
|
|
98
137
|
arbitration_results?: ArbitrationRecord[];
|
|
138
|
+
workspace_merge_report?: WorkspaceMergeReport;
|
|
99
139
|
state?: MultiAgentStatusSnapshot;
|
|
100
140
|
summary?: MultiAgentOperationalSummary;
|
|
101
141
|
}
|
|
@@ -111,12 +151,14 @@ function persistMultiAgentArtifacts(
|
|
|
111
151
|
runId: string,
|
|
112
152
|
state: MultiAgentStatusSnapshot,
|
|
113
153
|
handoffs: CooperativeHandoff[] = [],
|
|
114
|
-
arbitrationResults: ArbitrationRecord[] = []
|
|
154
|
+
arbitrationResults: ArbitrationRecord[] = [],
|
|
155
|
+
workspaceMergeReport: WorkspaceMergeReport = emptyWorkspaceMergeReport(runId)
|
|
115
156
|
): void {
|
|
116
157
|
const runDir = ensureRunDir(projectRoot, runId);
|
|
117
158
|
fs.writeFileSync(path.join(runDir, 'multi-agent-state.json'), JSON.stringify(state, null, 2), 'utf8');
|
|
118
159
|
fs.writeFileSync(path.join(runDir, 'handoffs.json'), JSON.stringify(handoffs, null, 2), 'utf8');
|
|
119
160
|
fs.writeFileSync(path.join(runDir, 'arbitration-results.json'), JSON.stringify(arbitrationResults, null, 2), 'utf8');
|
|
161
|
+
fs.writeFileSync(path.join(runDir, 'workspace-merge-report.json'), JSON.stringify(workspaceMergeReport, null, 2), 'utf8');
|
|
120
162
|
const summary: MultiAgentOperationalSummary = {
|
|
121
163
|
run_id: state.run_id,
|
|
122
164
|
mode: state.mode,
|
|
@@ -131,13 +173,17 @@ function persistMultiAgentArtifacts(
|
|
|
131
173
|
orphan_reassignment_count: state.orphan_reassignments.length,
|
|
132
174
|
timeout_count: state.timed_out_agents.length,
|
|
133
175
|
participating_agents: state.agent_results.map((entry) => entry.agent_id),
|
|
176
|
+
workspace_isolation: 'git_worktree',
|
|
177
|
+
merge_readiness: workspaceMergeReport.merge_readiness,
|
|
178
|
+
arbitration_required: workspaceMergeReport.arbitration_required,
|
|
179
|
+
merge_blocker_count: workspaceMergeReport.blockers.length,
|
|
134
180
|
health: state.timed_out_agents.length > 0 || state.failed.length > 0 ? 'degraded' : 'healthy',
|
|
135
181
|
updated_at: state.updated_at,
|
|
136
182
|
};
|
|
137
183
|
fs.writeFileSync(path.join(runDir, 'multi-agent-summary.json'), JSON.stringify(summary, null, 2), 'utf8');
|
|
138
184
|
}
|
|
139
185
|
|
|
140
|
-
function ensureIsolatedAgents(agents: AgentSpec[]): void {
|
|
186
|
+
function ensureIsolatedAgents(agents: AgentSpec[]): void {
|
|
141
187
|
const shared = agents.filter((agent) => agent.workspaceManager.isolation_level !== 'isolated');
|
|
142
188
|
if (shared.length === 0) return;
|
|
143
189
|
const ids = shared.map((agent) => `${agent.id}:${agent.workspaceManager.isolation_level}`).join(', ');
|
|
@@ -167,7 +213,8 @@ function makeState(
|
|
|
167
213
|
failed: string[],
|
|
168
214
|
blocked: string[],
|
|
169
215
|
orphanReassignments: Array<{ from_agent_id: string; to_agent_id: string; work_item_ids: string[] }>,
|
|
170
|
-
timedOutAgents: Array<{ agent_id: string; work_item_ids: string[]; detected_at: string }
|
|
216
|
+
timedOutAgents: Array<{ agent_id: string; work_item_ids: string[]; detected_at: string }>,
|
|
217
|
+
workspaceMergeReport: WorkspaceMergeReport = emptyWorkspaceMergeReport(runId)
|
|
171
218
|
): MultiAgentStatusSnapshot {
|
|
172
219
|
return {
|
|
173
220
|
run_id: runId,
|
|
@@ -179,7 +226,7 @@ function makeState(
|
|
|
179
226
|
completed,
|
|
180
227
|
failed,
|
|
181
228
|
blocked,
|
|
182
|
-
agent_results: agents.map((agent, idx) => {
|
|
229
|
+
agent_results: agents.map((agent, idx) => {
|
|
183
230
|
const result = agentResults.find((entry) => entry.agent_id === agent.id);
|
|
184
231
|
return {
|
|
185
232
|
agent_id: agent.id,
|
|
@@ -191,12 +238,150 @@ function makeState(
|
|
|
191
238
|
reassigned_task_ids: result?.reassigned_task_ids ?? [],
|
|
192
239
|
};
|
|
193
240
|
}),
|
|
241
|
+
worktrees: workspaceMergeReport.records,
|
|
242
|
+
workspace_merge: workspaceMergeReport,
|
|
243
|
+
merge_blockers: workspaceMergeReport.blockers,
|
|
244
|
+
arbitration_required: workspaceMergeReport.arbitration_required,
|
|
194
245
|
orphan_reassignments: orphanReassignments,
|
|
195
246
|
timed_out_agents: timedOutAgents,
|
|
196
247
|
updated_at: new Date().toISOString(),
|
|
197
248
|
};
|
|
198
249
|
}
|
|
199
250
|
|
|
251
|
+
function emptyWorkspaceMergeReport(runId: string): WorkspaceMergeReport {
|
|
252
|
+
return {
|
|
253
|
+
schema_version: 1,
|
|
254
|
+
run_id: runId,
|
|
255
|
+
generated_at: new Date().toISOString(),
|
|
256
|
+
workspace_isolation: 'git_worktree',
|
|
257
|
+
merge_readiness: 'ready',
|
|
258
|
+
arbitration_required: false,
|
|
259
|
+
blockers: [],
|
|
260
|
+
records: [],
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function ensureGitWorktreeLease(agent: AgentSpec, lease: WorkspaceLease): void {
|
|
265
|
+
if (lease.isolation_level !== 'isolated' || lease.strategy !== 'git_worktree') {
|
|
266
|
+
throw new Error(`Multi-agent real requires git_worktree isolated workspace. Agent ${agent.id} received ${lease.strategy}/${lease.isolation_level}.`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function mutationScopeOf(node?: GraphNode): string[] {
|
|
271
|
+
return Array.isArray(node?.mutation_scope) ? node!.mutation_scope.map(String).filter(Boolean) : [];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function createMergeRecord(
|
|
275
|
+
agent: AgentSpec,
|
|
276
|
+
node: GraphNode,
|
|
277
|
+
lease: WorkspaceLease,
|
|
278
|
+
result?: TaskResult
|
|
279
|
+
): WorkspaceMergeRecord {
|
|
280
|
+
const evidenceCount = Array.isArray(result?.evidence) ? result!.evidence.length : 0;
|
|
281
|
+
return {
|
|
282
|
+
work_item_id: node.id,
|
|
283
|
+
agent_id: agent.id,
|
|
284
|
+
workspace_id: lease.workspace_id,
|
|
285
|
+
strategy: lease.strategy,
|
|
286
|
+
isolation_level: lease.isolation_level,
|
|
287
|
+
branch: lease.branch ?? null,
|
|
288
|
+
base_commit: lease.base_commit ?? null,
|
|
289
|
+
root_path: lease.root_path ?? null,
|
|
290
|
+
mutation_scope: mutationScopeOf(node),
|
|
291
|
+
diff_paths: mutationScopeOf(node),
|
|
292
|
+
evidence_count: evidenceCount,
|
|
293
|
+
verify_status: result ? (result.success ? 'pass' : 'fail') : 'partial',
|
|
294
|
+
status: result ? (result.success ? 'ready' : 'blocked') : 'ready',
|
|
295
|
+
blocker: result && !result.success ? `verify_failed:${result.failure_class}` : null,
|
|
296
|
+
recorded_at: new Date().toISOString(),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function buildWorkspaceMergeReport(
|
|
301
|
+
runId: string,
|
|
302
|
+
records: WorkspaceMergeRecord[],
|
|
303
|
+
extraBlockers: string[] = [],
|
|
304
|
+
arbitrationRequired = false
|
|
305
|
+
): WorkspaceMergeReport {
|
|
306
|
+
const blockers = [
|
|
307
|
+
...extraBlockers,
|
|
308
|
+
...records.filter((record) => record.status === 'blocked' && record.blocker).map((record) => `${record.work_item_id}:${record.blocker}`),
|
|
309
|
+
];
|
|
310
|
+
return {
|
|
311
|
+
schema_version: 1,
|
|
312
|
+
run_id: runId,
|
|
313
|
+
generated_at: new Date().toISOString(),
|
|
314
|
+
workspace_isolation: 'git_worktree',
|
|
315
|
+
merge_readiness: blockers.length > 0 ? 'blocked' : records.some((record) => record.status === 'ready') ? 'partial' : 'ready',
|
|
316
|
+
arbitration_required: arbitrationRequired,
|
|
317
|
+
blockers,
|
|
318
|
+
records,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function detectMutationConflicts(graph: ExecutionGraph, agents: AgentSpec[], partitions: string[][]): string[] {
|
|
323
|
+
const owners = new Map<string, string>();
|
|
324
|
+
const conflicts: string[] = [];
|
|
325
|
+
for (let idx = 0; idx < agents.length; idx += 1) {
|
|
326
|
+
for (const nodeId of partitions[idx] ?? []) {
|
|
327
|
+
const node = graph.nodes.get(nodeId);
|
|
328
|
+
for (const scope of mutationScopeOf(node)) {
|
|
329
|
+
const previous = owners.get(scope);
|
|
330
|
+
if (previous && previous !== agents[idx].id) {
|
|
331
|
+
conflicts.push(`mutation_scope_overlap:${scope}:${previous}:${agents[idx].id}`);
|
|
332
|
+
} else {
|
|
333
|
+
owners.set(scope, agents[idx].id);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return conflicts;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function applyWorkspaceRecord(projectRoot: string, record: WorkspaceMergeRecord): WorkspaceMergeRecord {
|
|
342
|
+
if (!record.root_path || record.status !== 'ready') return record;
|
|
343
|
+
for (const relativePath of record.mutation_scope) {
|
|
344
|
+
const source = path.join(record.root_path, relativePath);
|
|
345
|
+
const target = path.join(projectRoot, relativePath);
|
|
346
|
+
if (!fs.existsSync(source) || fs.statSync(source).isDirectory()) {
|
|
347
|
+
return { ...record, status: 'blocked', blocker: `missing_output:${relativePath}` };
|
|
348
|
+
}
|
|
349
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
350
|
+
fs.copyFileSync(source, target);
|
|
351
|
+
}
|
|
352
|
+
return { ...record, status: 'merged', blocker: null };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function createTrackingWorkspaceManager(
|
|
356
|
+
base: WorkspaceManager,
|
|
357
|
+
agent: AgentSpec,
|
|
358
|
+
graph: ExecutionGraph,
|
|
359
|
+
records: WorkspaceMergeRecord[]
|
|
360
|
+
): WorkspaceManager & { disposeDeferred(): Promise<void> } {
|
|
361
|
+
const leases = new Map<string, WorkspaceLease>();
|
|
362
|
+
return {
|
|
363
|
+
isolation_level: base.isolation_level,
|
|
364
|
+
allocate: async (req: WorkspaceRequest) => {
|
|
365
|
+
const lease = await base.allocate(req);
|
|
366
|
+
ensureGitWorktreeLease(agent, lease);
|
|
367
|
+
leases.set(lease.workspace_id, lease);
|
|
368
|
+
const node = graph.nodes.get(req.work_item_id);
|
|
369
|
+
if (node) records.push(createMergeRecord(agent, node, lease));
|
|
370
|
+
return lease;
|
|
371
|
+
},
|
|
372
|
+
snapshot: (id: string): Promise<SnapshotRef> => base.snapshot(id),
|
|
373
|
+
reset: (id: string, snapRef: SnapshotRef): Promise<void> => base.reset(id, snapRef),
|
|
374
|
+
dispose: async () => {
|
|
375
|
+
// Scheduler calls dispose before the coordinator can reconcile diffs.
|
|
376
|
+
// Defer real cleanup until the merge report has been produced.
|
|
377
|
+
},
|
|
378
|
+
disposeDeferred: async () => {
|
|
379
|
+
await Promise.all([...leases.keys()].map((id) => base.dispose(id).catch(() => {})));
|
|
380
|
+
leases.clear();
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
200
385
|
async function runGraphForAgent(
|
|
201
386
|
graph: ExecutionGraph,
|
|
202
387
|
nodeIds: string[],
|
|
@@ -211,8 +396,11 @@ async function runGraphForAgent(
|
|
|
211
396
|
timed_out: boolean;
|
|
212
397
|
assigned_task_ids: string[];
|
|
213
398
|
reassigned_task_ids: string[];
|
|
399
|
+
workspace_records: WorkspaceMergeRecord[];
|
|
400
|
+
cleanup: () => Promise<void>;
|
|
214
401
|
}> {
|
|
215
402
|
const subGraph = subGraphFor(graph, nodeIds);
|
|
403
|
+
const workspaceRecords: WorkspaceMergeRecord[] = [];
|
|
216
404
|
if (subGraph.nodes.size === 0) {
|
|
217
405
|
return {
|
|
218
406
|
agent_id: agent.id,
|
|
@@ -221,14 +409,17 @@ async function runGraphForAgent(
|
|
|
221
409
|
timed_out: false,
|
|
222
410
|
assigned_task_ids: nodeIds,
|
|
223
411
|
reassigned_task_ids: [],
|
|
412
|
+
workspace_records: workspaceRecords,
|
|
413
|
+
cleanup: async () => {},
|
|
224
414
|
};
|
|
225
415
|
}
|
|
416
|
+
const trackingWorkspaceManager = createTrackingWorkspaceManager(agent.workspaceManager, agent, graph, workspaceRecords);
|
|
226
417
|
const ctx: SchedulerContext = {
|
|
227
418
|
projectRoot: opts.projectRoot,
|
|
228
419
|
sessionId: opts.sessionId,
|
|
229
420
|
runId: `${opts.runId}-agent${idx}`,
|
|
230
421
|
executor: agent.executor,
|
|
231
|
-
workspaceManager:
|
|
422
|
+
workspaceManager: trackingWorkspaceManager,
|
|
232
423
|
onEvent: opts.onEvent,
|
|
233
424
|
};
|
|
234
425
|
const scheduler = new Scheduler();
|
|
@@ -242,6 +433,8 @@ async function runGraphForAgent(
|
|
|
242
433
|
timed_out: false,
|
|
243
434
|
assigned_task_ids: nodeIds,
|
|
244
435
|
reassigned_task_ids: [],
|
|
436
|
+
workspace_records: workspaceRecords,
|
|
437
|
+
cleanup: () => trackingWorkspaceManager.disposeDeferred(),
|
|
245
438
|
};
|
|
246
439
|
}
|
|
247
440
|
let timer: NodeJS.Timeout | null = null;
|
|
@@ -260,6 +453,8 @@ async function runGraphForAgent(
|
|
|
260
453
|
timed_out: true,
|
|
261
454
|
assigned_task_ids: nodeIds,
|
|
262
455
|
reassigned_task_ids: [],
|
|
456
|
+
workspace_records: workspaceRecords,
|
|
457
|
+
cleanup: () => trackingWorkspaceManager.disposeDeferred(),
|
|
263
458
|
};
|
|
264
459
|
}
|
|
265
460
|
const result = raced.result;
|
|
@@ -270,6 +465,8 @@ async function runGraphForAgent(
|
|
|
270
465
|
timed_out: false,
|
|
271
466
|
assigned_task_ids: nodeIds,
|
|
272
467
|
reassigned_task_ids: [],
|
|
468
|
+
workspace_records: workspaceRecords,
|
|
469
|
+
cleanup: () => trackingWorkspaceManager.disposeDeferred(),
|
|
273
470
|
};
|
|
274
471
|
}
|
|
275
472
|
|
|
@@ -290,6 +487,30 @@ async function runParallel(
|
|
|
290
487
|
partitions[index % agents.length].push(id);
|
|
291
488
|
});
|
|
292
489
|
}
|
|
490
|
+
const mutationConflicts = detectMutationConflicts(graph, agents, partitions);
|
|
491
|
+
if (mutationConflicts.length > 0) {
|
|
492
|
+
const blocked = mutationConflicts;
|
|
493
|
+
const workspaceMergeReport = buildWorkspaceMergeReport(runId, [], blocked);
|
|
494
|
+
const state = makeState('parallel', runId, agents, partitions, [], [], [], blocked, [], [], workspaceMergeReport);
|
|
495
|
+
persistMultiAgentArtifacts(projectRoot, runId, state, [], [], workspaceMergeReport);
|
|
496
|
+
appendEvent(projectRoot, sessionId, {
|
|
497
|
+
type: 'WorkItemBlocked',
|
|
498
|
+
run_id: runId,
|
|
499
|
+
payload: { mode: 'parallel', blockers: blocked },
|
|
500
|
+
});
|
|
501
|
+
return {
|
|
502
|
+
mode: 'parallel',
|
|
503
|
+
run_id: runId,
|
|
504
|
+
completed: [],
|
|
505
|
+
failed: [],
|
|
506
|
+
blocked,
|
|
507
|
+
agent_results: [],
|
|
508
|
+
arbitration_results: [],
|
|
509
|
+
workspace_merge_report: workspaceMergeReport,
|
|
510
|
+
state,
|
|
511
|
+
summary: loadMultiAgentSummary(projectRoot, runId) || undefined,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
293
514
|
const registry = new AgentRegistry(heartbeatTimeoutMs == null ? 30_000 : heartbeatTimeoutMs);
|
|
294
515
|
agents.forEach((agent, idx) => {
|
|
295
516
|
registry.register(agent.id, agent.executor, agent.workspaceManager, partitions[idx] ?? []);
|
|
@@ -331,7 +552,13 @@ async function runParallel(
|
|
|
331
552
|
const rerun = await runGraphForAgent(graph, timedOut.assigned_task_ids, agents[fallbackIdx], fallbackIdx, opts, null);
|
|
332
553
|
fallback.completed.push(...rerun.completed);
|
|
333
554
|
fallback.failed.push(...rerun.failed);
|
|
555
|
+
fallback.workspace_records.push(...rerun.workspace_records);
|
|
334
556
|
fallback.reassigned_task_ids.push(...timedOut.assigned_task_ids);
|
|
557
|
+
const previousCleanup = fallback.cleanup;
|
|
558
|
+
fallback.cleanup = async () => {
|
|
559
|
+
await previousCleanup?.();
|
|
560
|
+
await rerun.cleanup?.();
|
|
561
|
+
};
|
|
335
562
|
partitions[fallbackIdx] = [...partitions[fallbackIdx], ...timedOut.assigned_task_ids];
|
|
336
563
|
partitions[timeoutIdx] = [];
|
|
337
564
|
orphanReassignments.push({
|
|
@@ -343,8 +570,14 @@ async function runParallel(
|
|
|
343
570
|
|
|
344
571
|
const completed = Array.from(new Set(agentResults.flatMap((result) => result.completed)));
|
|
345
572
|
const failed = Array.from(new Set(agentResults.flatMap((result) => result.failed)));
|
|
346
|
-
const
|
|
347
|
-
|
|
573
|
+
const rawRecords = agentResults.flatMap((result) => result.workspace_records || []);
|
|
574
|
+
const mergedRecords = opts.applyWorkspaceMerges
|
|
575
|
+
? rawRecords.map((record) => applyWorkspaceRecord(projectRoot, record))
|
|
576
|
+
: rawRecords;
|
|
577
|
+
await Promise.all(agentResults.map((result) => result.cleanup?.().catch(() => {})));
|
|
578
|
+
const workspaceMergeReport = buildWorkspaceMergeReport(runId, mergedRecords, blocked);
|
|
579
|
+
const state = makeState('parallel', runId, agents, partitions, agentResults, completed, failed, blocked, orphanReassignments, timedOutAgents, workspaceMergeReport);
|
|
580
|
+
persistMultiAgentArtifacts(projectRoot, runId, state, [], [], workspaceMergeReport);
|
|
348
581
|
|
|
349
582
|
appendEvent(projectRoot, sessionId, {
|
|
350
583
|
type: 'RunCompleted',
|
|
@@ -357,9 +590,10 @@ async function runParallel(
|
|
|
357
590
|
run_id: runId,
|
|
358
591
|
completed,
|
|
359
592
|
failed,
|
|
360
|
-
blocked,
|
|
593
|
+
blocked,
|
|
361
594
|
agent_results: agentResults,
|
|
362
595
|
arbitration_results: [],
|
|
596
|
+
workspace_merge_report: workspaceMergeReport,
|
|
363
597
|
state,
|
|
364
598
|
summary: loadMultiAgentSummary(projectRoot, runId) || undefined,
|
|
365
599
|
};
|
|
@@ -387,12 +621,13 @@ async function runCompetitive(
|
|
|
387
621
|
const completed: string[] = [];
|
|
388
622
|
const failed: string[] = [];
|
|
389
623
|
const blocked: string[] = [];
|
|
390
|
-
const arbitrationResults: ArbitrationRecord[] = [];
|
|
624
|
+
const arbitrationResults: ArbitrationRecord[] = [];
|
|
625
|
+
const workspaceRecords: WorkspaceMergeRecord[] = [];
|
|
391
626
|
|
|
392
627
|
for (const wave of graph.waves) {
|
|
393
628
|
for (const nodeId of wave.node_ids) {
|
|
394
629
|
const node = graph.nodes.get(nodeId)!;
|
|
395
|
-
const result = await competeTwoAgents(nodeId, node, agentA, agentB, opts, arbitrationResults);
|
|
630
|
+
const result = await competeTwoAgents(nodeId, node, agentA, agentB, opts, arbitrationResults, workspaceRecords);
|
|
396
631
|
if (result.success) completed.push(nodeId);
|
|
397
632
|
else failed.push(nodeId);
|
|
398
633
|
if (failed.length > 0) break;
|
|
@@ -401,6 +636,7 @@ async function runCompetitive(
|
|
|
401
636
|
}
|
|
402
637
|
|
|
403
638
|
const partitions = [Array.from(graph.nodes.keys()), Array.from(graph.nodes.keys())];
|
|
639
|
+
const workspaceMergeReport = buildWorkspaceMergeReport(runId, workspaceRecords, blocked, true);
|
|
404
640
|
const state = makeState(
|
|
405
641
|
'competitive',
|
|
406
642
|
runId,
|
|
@@ -414,9 +650,10 @@ async function runCompetitive(
|
|
|
414
650
|
failed,
|
|
415
651
|
blocked,
|
|
416
652
|
[],
|
|
417
|
-
[]
|
|
653
|
+
[],
|
|
654
|
+
workspaceMergeReport
|
|
418
655
|
);
|
|
419
|
-
persistMultiAgentArtifacts(projectRoot, runId, state, [], arbitrationResults);
|
|
656
|
+
persistMultiAgentArtifacts(projectRoot, runId, state, [], arbitrationResults, workspaceMergeReport);
|
|
420
657
|
|
|
421
658
|
appendEvent(projectRoot, sessionId, {
|
|
422
659
|
type: 'RunCompleted',
|
|
@@ -435,6 +672,7 @@ async function runCompetitive(
|
|
|
435
672
|
{ agent_id: agentB.id, completed: [], failed: [] },
|
|
436
673
|
],
|
|
437
674
|
arbitration_results: arbitrationResults,
|
|
675
|
+
workspace_merge_report: workspaceMergeReport,
|
|
438
676
|
state,
|
|
439
677
|
summary: loadMultiAgentSummary(projectRoot, runId) || undefined,
|
|
440
678
|
};
|
|
@@ -443,19 +681,22 @@ async function runCompetitive(
|
|
|
443
681
|
async function competeTwoAgents(
|
|
444
682
|
nodeId: string,
|
|
445
683
|
node: GraphNode,
|
|
446
|
-
agentA: AgentSpec,
|
|
447
|
-
agentB: AgentSpec,
|
|
448
|
-
opts: CoordinationOptions,
|
|
449
|
-
arbitrationResults: ArbitrationRecord[]
|
|
450
|
-
|
|
684
|
+
agentA: AgentSpec,
|
|
685
|
+
agentB: AgentSpec,
|
|
686
|
+
opts: CoordinationOptions,
|
|
687
|
+
arbitrationResults: ArbitrationRecord[],
|
|
688
|
+
workspaceRecords: WorkspaceMergeRecord[]
|
|
689
|
+
): Promise<TaskResult> {
|
|
451
690
|
const { projectRoot, sessionId, runId } = opts;
|
|
452
691
|
|
|
453
|
-
const allocA = await agentA.workspaceManager.allocate({
|
|
454
|
-
work_item_id: nodeId, attempt_number: 1, strategy: node.workspace_strategy, mutation_scope: node.mutation_scope,
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
692
|
+
const allocA = await agentA.workspaceManager.allocate({
|
|
693
|
+
work_item_id: nodeId, attempt_number: 1, strategy: node.workspace_strategy, mutation_scope: node.mutation_scope,
|
|
694
|
+
});
|
|
695
|
+
ensureGitWorktreeLease(agentA, allocA);
|
|
696
|
+
const allocB = await agentB.workspaceManager.allocate({
|
|
697
|
+
work_item_id: nodeId, attempt_number: 1, strategy: node.workspace_strategy, mutation_scope: node.mutation_scope,
|
|
698
|
+
});
|
|
699
|
+
ensureGitWorktreeLease(agentB, allocB);
|
|
459
700
|
|
|
460
701
|
appendEvent(projectRoot, sessionId, {
|
|
461
702
|
type: 'AttemptStarted',
|
|
@@ -473,13 +714,24 @@ async function competeTwoAgents(
|
|
|
473
714
|
})),
|
|
474
715
|
]);
|
|
475
716
|
|
|
476
|
-
|
|
477
|
-
agentA
|
|
478
|
-
agentB
|
|
479
|
-
])
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
717
|
+
const candidates = [
|
|
718
|
+
{ agent: agentA, alloc: allocA, result: resultA },
|
|
719
|
+
{ agent: agentB, alloc: allocB, result: resultB },
|
|
720
|
+
].sort((left, right) => {
|
|
721
|
+
const leftScore = (left.result.success ? 10_000 : 0) + left.result.evidence.length * 100 - String(left.result.output || '').length;
|
|
722
|
+
const rightScore = (right.result.success ? 10_000 : 0) + right.result.evidence.length * 100 - String(right.result.output || '').length;
|
|
723
|
+
return rightScore - leftScore;
|
|
724
|
+
});
|
|
725
|
+
const winnerCandidate = candidates[0];
|
|
726
|
+
const winner = winnerCandidate.result;
|
|
727
|
+
const winnerAgentId = winnerCandidate.agent.id;
|
|
728
|
+
const winnerRecord = createMergeRecord(winnerCandidate.agent, node, winnerCandidate.alloc, winner);
|
|
729
|
+
workspaceRecords.push(opts.applyWorkspaceMerges ? applyWorkspaceRecord(projectRoot, winnerRecord) : winnerRecord);
|
|
730
|
+
|
|
731
|
+
await Promise.all([
|
|
732
|
+
agentA.workspaceManager.dispose(allocA.workspace_id).catch(() => {}),
|
|
733
|
+
agentB.workspaceManager.dispose(allocB.workspace_id).catch(() => {}),
|
|
734
|
+
]);
|
|
483
735
|
arbitrationResults.push({
|
|
484
736
|
work_item_id: nodeId,
|
|
485
737
|
mode: 'competitive',
|
|
@@ -512,7 +764,8 @@ async function runCooperative(
|
|
|
512
764
|
ensureIsolatedAgents(opts.agents);
|
|
513
765
|
const [planner, executor] = opts.agents;
|
|
514
766
|
const { projectRoot, sessionId, runId } = opts;
|
|
515
|
-
const handoffs: CooperativeHandoff[] = [];
|
|
767
|
+
const handoffs: CooperativeHandoff[] = [];
|
|
768
|
+
const workspaceRecords: WorkspaceMergeRecord[] = [];
|
|
516
769
|
|
|
517
770
|
appendEvent(projectRoot, sessionId, {
|
|
518
771
|
type: 'RunStarted',
|
|
@@ -528,13 +781,14 @@ async function runCooperative(
|
|
|
528
781
|
for (const nodeId of wave.node_ids) {
|
|
529
782
|
const node = graph.nodes.get(nodeId)!;
|
|
530
783
|
|
|
531
|
-
const planAlloc = await planner.workspaceManager.allocate({
|
|
784
|
+
const planAlloc = await planner.workspaceManager.allocate({
|
|
532
785
|
work_item_id: nodeId,
|
|
533
786
|
attempt_number: 1,
|
|
534
787
|
strategy: node.workspace_strategy,
|
|
535
|
-
mutation_scope: node.mutation_scope,
|
|
536
|
-
});
|
|
537
|
-
|
|
788
|
+
mutation_scope: node.mutation_scope,
|
|
789
|
+
});
|
|
790
|
+
ensureGitWorktreeLease(planner, planAlloc);
|
|
791
|
+
await planner.workspaceManager.dispose(planAlloc.workspace_id).catch(() => {});
|
|
538
792
|
|
|
539
793
|
const handoff = buildHandoff({
|
|
540
794
|
from_agent_id: planner.id,
|
|
@@ -553,12 +807,13 @@ async function runCooperative(
|
|
|
553
807
|
payload: { mode: 'cooperative', handoff_id: handoff.handoff_id },
|
|
554
808
|
});
|
|
555
809
|
|
|
556
|
-
const execAlloc = await executor.workspaceManager.allocate({
|
|
810
|
+
const execAlloc = await executor.workspaceManager.allocate({
|
|
557
811
|
work_item_id: nodeId,
|
|
558
812
|
attempt_number: 1,
|
|
559
813
|
strategy: node.workspace_strategy,
|
|
560
|
-
mutation_scope: node.mutation_scope,
|
|
561
|
-
});
|
|
814
|
+
mutation_scope: node.mutation_scope,
|
|
815
|
+
});
|
|
816
|
+
ensureGitWorktreeLease(executor, execAlloc);
|
|
562
817
|
|
|
563
818
|
let result: TaskResult;
|
|
564
819
|
try {
|
|
@@ -566,7 +821,9 @@ async function runCooperative(
|
|
|
566
821
|
} catch (error) {
|
|
567
822
|
result = { success: false, failure_class: 'env', evidence: [], output: String(error) };
|
|
568
823
|
}
|
|
569
|
-
|
|
824
|
+
const mergeRecord = createMergeRecord(executor, node, execAlloc, result);
|
|
825
|
+
workspaceRecords.push(opts.applyWorkspaceMerges ? applyWorkspaceRecord(projectRoot, mergeRecord) : mergeRecord);
|
|
826
|
+
await executor.workspaceManager.dispose(execAlloc.workspace_id).catch(() => {});
|
|
570
827
|
|
|
571
828
|
if (result.success) {
|
|
572
829
|
completed.push(nodeId);
|
|
@@ -581,6 +838,7 @@ async function runCooperative(
|
|
|
581
838
|
}
|
|
582
839
|
|
|
583
840
|
const partitions = [Array.from(graph.nodes.keys()), Array.from(graph.nodes.keys())];
|
|
841
|
+
const workspaceMergeReport = buildWorkspaceMergeReport(runId, workspaceRecords, blocked);
|
|
584
842
|
const state = makeState(
|
|
585
843
|
'cooperative',
|
|
586
844
|
runId,
|
|
@@ -594,9 +852,10 @@ async function runCooperative(
|
|
|
594
852
|
failed,
|
|
595
853
|
blocked,
|
|
596
854
|
[],
|
|
597
|
-
[]
|
|
855
|
+
[],
|
|
856
|
+
workspaceMergeReport
|
|
598
857
|
);
|
|
599
|
-
persistMultiAgentArtifacts(projectRoot, runId, state, handoffs, []);
|
|
858
|
+
persistMultiAgentArtifacts(projectRoot, runId, state, handoffs, [], workspaceMergeReport);
|
|
600
859
|
|
|
601
860
|
appendEvent(projectRoot, sessionId, {
|
|
602
861
|
type: 'RunCompleted',
|
|
@@ -616,6 +875,7 @@ async function runCooperative(
|
|
|
616
875
|
],
|
|
617
876
|
handoffs,
|
|
618
877
|
arbitration_results: [],
|
|
878
|
+
workspace_merge_report: workspaceMergeReport,
|
|
619
879
|
state,
|
|
620
880
|
summary: loadMultiAgentSummary(projectRoot, runId) || undefined,
|
|
621
881
|
};
|
|
@@ -643,6 +903,10 @@ export function multiAgentSummaryPath(projectRoot: string, runId: string): strin
|
|
|
643
903
|
return path.join(projectRoot, '.oxe', 'runs', runId, 'multi-agent-summary.json');
|
|
644
904
|
}
|
|
645
905
|
|
|
906
|
+
export function workspaceMergeReportPath(projectRoot: string, runId: string): string {
|
|
907
|
+
return path.join(projectRoot, '.oxe', 'runs', runId, 'workspace-merge-report.json');
|
|
908
|
+
}
|
|
909
|
+
|
|
646
910
|
export function loadMultiAgentState(projectRoot: string, runId: string): MultiAgentStatusSnapshot | null {
|
|
647
911
|
const statePath = multiAgentStatePath(projectRoot, runId);
|
|
648
912
|
if (!fs.existsSync(statePath)) return null;
|
|
@@ -662,6 +926,16 @@ export function loadMultiAgentSummary(projectRoot: string, runId: string): Multi
|
|
|
662
926
|
return null;
|
|
663
927
|
}
|
|
664
928
|
}
|
|
929
|
+
|
|
930
|
+
export function loadWorkspaceMergeReport(projectRoot: string, runId: string): WorkspaceMergeReport | null {
|
|
931
|
+
const reportPath = workspaceMergeReportPath(projectRoot, runId);
|
|
932
|
+
if (!fs.existsSync(reportPath)) return null;
|
|
933
|
+
try {
|
|
934
|
+
return JSON.parse(fs.readFileSync(reportPath, 'utf8')) as WorkspaceMergeReport;
|
|
935
|
+
} catch {
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
665
939
|
|
|
666
940
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
667
941
|
|