oxe-cc 1.0.0 → 1.2.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/.cursor/commands/oxe-ask.md +1 -1
- package/.cursor/commands/oxe-capabilities.md +1 -1
- package/.cursor/commands/oxe-checkpoint.md +1 -1
- package/.cursor/commands/oxe-compact.md +1 -1
- package/.cursor/commands/oxe-dashboard.md +1 -1
- package/.cursor/commands/oxe-debug.md +1 -1
- package/.cursor/commands/oxe-discuss.md +1 -1
- package/.cursor/commands/oxe-execute.md +2 -2
- package/.cursor/commands/oxe-forensics.md +1 -1
- package/.cursor/commands/oxe-help.md +1 -1
- package/.cursor/commands/oxe-loop.md +1 -1
- package/.cursor/commands/oxe-milestone.md +1 -1
- package/.cursor/commands/oxe-next.md +1 -1
- package/.cursor/commands/oxe-obs.md +1 -1
- package/.cursor/commands/oxe-plan-agent.md +1 -1
- package/.cursor/commands/oxe-plan.md +1 -1
- package/.cursor/commands/oxe-project.md +1 -1
- package/.cursor/commands/oxe-quick.md +1 -1
- package/.cursor/commands/oxe-research.md +1 -1
- package/.cursor/commands/oxe-retro.md +1 -1
- package/.cursor/commands/oxe-review-pr.md +1 -1
- package/.cursor/commands/oxe-route.md +1 -1
- package/.cursor/commands/oxe-scan.md +1 -1
- package/.cursor/commands/oxe-security.md +1 -1
- package/.cursor/commands/oxe-session.md +2 -2
- package/.cursor/commands/oxe-ship.md +45 -0
- package/.cursor/commands/oxe-skill.md +1 -1
- package/.cursor/commands/oxe-spec.md +1 -1
- package/.cursor/commands/oxe-ui-review.md +1 -1
- package/.cursor/commands/oxe-ui-spec.md +1 -1
- package/.cursor/commands/oxe-update.md +1 -1
- package/.cursor/commands/oxe-validate-gaps.md +1 -1
- package/.cursor/commands/oxe-verify.md +1 -1
- package/.cursor/commands/oxe-workstream.md +1 -1
- package/.cursor/commands/oxe.md +4 -4
- package/.github/copilot-instructions.md +91 -1
- package/.github/prompts/oxe-ask.prompt.md +1 -1
- package/.github/prompts/oxe-capabilities.prompt.md +1 -1
- package/.github/prompts/oxe-checkpoint.prompt.md +1 -1
- package/.github/prompts/oxe-compact.prompt.md +1 -1
- package/.github/prompts/oxe-dashboard.prompt.md +1 -1
- package/.github/prompts/oxe-debug.prompt.md +1 -1
- package/.github/prompts/oxe-discuss.prompt.md +1 -1
- package/.github/prompts/oxe-execute.prompt.md +2 -2
- package/.github/prompts/oxe-forensics.prompt.md +1 -1
- package/.github/prompts/oxe-help.prompt.md +1 -1
- package/.github/prompts/oxe-loop.prompt.md +1 -1
- package/.github/prompts/oxe-milestone.prompt.md +1 -1
- package/.github/prompts/oxe-next.prompt.md +1 -1
- package/.github/prompts/oxe-obs.prompt.md +1 -1
- package/.github/prompts/oxe-plan-agent.prompt.md +1 -1
- package/.github/prompts/oxe-plan.prompt.md +1 -1
- package/.github/prompts/oxe-project.prompt.md +1 -1
- package/.github/prompts/oxe-quick.prompt.md +1 -1
- package/.github/prompts/oxe-research.prompt.md +1 -1
- package/.github/prompts/oxe-retro.prompt.md +1 -1
- package/.github/prompts/oxe-review-pr.prompt.md +1 -1
- package/.github/prompts/oxe-route.prompt.md +1 -1
- package/.github/prompts/oxe-scan.prompt.md +1 -1
- package/.github/prompts/oxe-security.prompt.md +1 -1
- package/.github/prompts/oxe-session.prompt.md +2 -2
- package/.github/prompts/oxe-ship.prompt.md +45 -0
- package/.github/prompts/oxe-skill.prompt.md +1 -1
- package/.github/prompts/oxe-spec.prompt.md +1 -1
- package/.github/prompts/oxe-ui-review.prompt.md +1 -1
- package/.github/prompts/oxe-ui-spec.prompt.md +1 -1
- package/.github/prompts/oxe-update.prompt.md +1 -1
- package/.github/prompts/oxe-validate-gaps.prompt.md +1 -1
- package/.github/prompts/oxe-verify.prompt.md +1 -1
- package/.github/prompts/oxe-workstream.prompt.md +1 -1
- package/.github/prompts/oxe.prompt.md +3 -3
- package/AGENTS.md +43 -28
- package/CHANGELOG.md +158 -0
- package/README.md +72 -50
- package/bin/banner.txt +1 -1
- package/bin/lib/oxe-project-health.cjs +1 -1
- package/commands/oxe/ask.md +5 -1
- package/commands/oxe/checkpoint.md +1 -1
- package/commands/oxe/compact.md +1 -1
- package/commands/oxe/debug.md +1 -1
- package/commands/oxe/execute.md +2 -2
- package/commands/oxe/forensics.md +1 -1
- package/commands/oxe/loop.md +1 -1
- package/commands/oxe/milestone.md +1 -1
- package/commands/oxe/next.md +1 -1
- package/commands/oxe/obs.md +1 -1
- package/commands/oxe/oxe.md +3 -3
- package/commands/oxe/project.md +1 -1
- package/commands/oxe/research.md +1 -1
- package/commands/oxe/retro.md +1 -1
- package/commands/oxe/review-pr.md +1 -1
- package/commands/oxe/route.md +1 -1
- package/commands/oxe/scan.md +1 -1
- package/commands/oxe/security.md +1 -1
- package/commands/oxe/session.md +2 -2
- package/commands/oxe/ship.md +49 -0
- package/commands/oxe/spec.md +2 -2
- package/commands/oxe/ui-review.md +1 -1
- package/commands/oxe/ui-spec.md +1 -1
- package/commands/oxe/validate-gaps.md +1 -1
- package/commands/oxe/verify.md +2 -2
- package/commands/oxe/workstream.md +1 -1
- package/lib/runtime/audit/audit-trail.d.ts +71 -0
- package/lib/runtime/audit/audit-trail.js +154 -0
- package/lib/runtime/audit/index.d.ts +2 -0
- package/lib/runtime/audit/index.js +18 -0
- package/lib/runtime/audit/policy-pack.d.ts +15 -0
- package/lib/runtime/audit/policy-pack.js +57 -0
- package/lib/runtime/context/context-pack-builder.d.ts +15 -0
- package/lib/runtime/context/context-pack-builder.js +42 -0
- package/lib/runtime/context/context-pack-store.d.ts +38 -0
- package/lib/runtime/context/context-pack-store.js +142 -0
- package/lib/runtime/context/context-profiles.d.ts +11 -0
- package/lib/runtime/context/context-profiles.js +51 -0
- package/lib/runtime/context/index.d.ts +2 -0
- package/lib/runtime/context/index.js +2 -0
- package/lib/runtime/decision/decision-engine.d.ts +43 -0
- package/lib/runtime/decision/decision-engine.js +127 -0
- package/lib/runtime/decision/decision-memo.d.ts +53 -0
- package/lib/runtime/decision/decision-memo.js +173 -0
- package/lib/runtime/decision/index.d.ts +2 -0
- package/lib/runtime/decision/index.js +18 -0
- package/lib/runtime/delivery/index.d.ts +1 -0
- package/lib/runtime/delivery/index.js +1 -0
- package/lib/runtime/delivery/promotion-pipeline.d.ts +39 -0
- package/lib/runtime/delivery/promotion-pipeline.js +127 -0
- package/lib/runtime/index.d.ts +3 -0
- package/lib/runtime/index.js +4 -0
- package/lib/runtime/plugins/capability-matrix.d.ts +20 -0
- package/lib/runtime/plugins/capability-matrix.js +59 -0
- package/lib/runtime/plugins/index.d.ts +2 -0
- package/lib/runtime/plugins/index.js +2 -0
- package/lib/runtime/plugins/plugin-manifest.d.ts +22 -0
- package/lib/runtime/plugins/plugin-manifest.js +91 -0
- package/lib/runtime/plugins/plugin-registry.js +5 -0
- package/lib/runtime/policy/policy-engine.d.ts +28 -1
- package/lib/runtime/policy/policy-engine.js +96 -5
- package/lib/runtime/reducers/run-state-reducer.d.ts +26 -0
- package/lib/runtime/reducers/run-state-reducer.js +117 -1
- package/lib/runtime/scheduler/agent-registry.d.ts +44 -0
- package/lib/runtime/scheduler/agent-registry.js +96 -0
- package/lib/runtime/scheduler/agent-roles.d.ts +54 -0
- package/lib/runtime/scheduler/agent-roles.js +62 -0
- package/lib/runtime/scheduler/index.d.ts +3 -0
- package/lib/runtime/scheduler/index.js +3 -0
- package/lib/runtime/scheduler/multi-agent-coordinator.d.ts +2 -0
- package/lib/runtime/scheduler/multi-agent-coordinator.js +91 -4
- package/lib/runtime/scheduler/run-journal.d.ts +18 -0
- package/lib/runtime/scheduler/run-journal.js +54 -0
- package/lib/runtime/scheduler/scheduler.d.ts +11 -1
- package/lib/runtime/scheduler/scheduler.js +135 -7
- package/lib/runtime/verification/index.d.ts +1 -0
- package/lib/runtime/verification/index.js +1 -0
- package/lib/runtime/verification/verification-manifest.d.ts +58 -0
- package/lib/runtime/verification/verification-manifest.js +129 -0
- package/oxe/workflows/ask.md +4 -0
- package/oxe/workflows/checkpoint.md +14 -10
- package/oxe/workflows/debug.md +19 -15
- package/oxe/workflows/execute.md +30 -2
- package/oxe/workflows/forensics.md +13 -9
- package/oxe/workflows/help.md +97 -49
- package/oxe/workflows/loop.md +17 -13
- package/oxe/workflows/obs.md +4 -0
- package/oxe/workflows/oxe.md +64 -31
- package/oxe/workflows/project.md +6 -1
- package/oxe/workflows/references/workflow-runtime-contracts.json +23 -0
- package/oxe/workflows/research.md +32 -28
- package/oxe/workflows/retro.md +4 -0
- package/oxe/workflows/review-pr.md +15 -11
- package/oxe/workflows/scan.md +4 -0
- package/oxe/workflows/security.md +14 -10
- package/oxe/workflows/session.md +17 -1
- package/oxe/workflows/ship.md +142 -0
- package/oxe/workflows/spec.md +15 -0
- package/oxe/workflows/ui-review.md +20 -16
- package/oxe/workflows/ui-spec.md +7 -3
- package/oxe/workflows/validate-gaps.md +13 -9
- package/oxe/workflows/verify.md +42 -3
- package/package.json +1 -1
- package/packages/runtime/src/audit/audit-trail.ts +243 -0
- package/packages/runtime/src/audit/index.ts +2 -0
- package/packages/runtime/src/audit/policy-pack.ts +62 -0
- package/packages/runtime/src/context/context-pack-builder.ts +66 -0
- package/packages/runtime/src/context/context-pack-store.ts +197 -0
- package/packages/runtime/src/context/context-profiles.ts +60 -0
- package/packages/runtime/src/context/index.ts +2 -0
- package/packages/runtime/src/decision/decision-engine.ts +174 -0
- package/packages/runtime/src/decision/decision-memo.ts +211 -0
- package/packages/runtime/src/decision/index.ts +2 -0
- package/packages/runtime/src/delivery/index.ts +1 -0
- package/packages/runtime/src/delivery/promotion-pipeline.ts +180 -0
- package/packages/runtime/src/index.ts +5 -0
- package/packages/runtime/src/plugins/capability-matrix.ts +83 -0
- package/packages/runtime/src/plugins/index.ts +2 -0
- package/packages/runtime/src/plugins/plugin-manifest.ts +113 -0
- package/packages/runtime/src/plugins/plugin-registry.ts +5 -0
- package/packages/runtime/src/policy/policy-engine.ts +138 -7
- package/packages/runtime/src/reducers/run-state-reducer.ts +143 -1
- package/packages/runtime/src/scheduler/agent-registry.ts +132 -0
- package/packages/runtime/src/scheduler/agent-roles.ts +109 -0
- package/packages/runtime/src/scheduler/index.ts +3 -0
- package/packages/runtime/src/scheduler/multi-agent-coordinator.ts +106 -4
- package/packages/runtime/src/scheduler/run-journal.ts +62 -0
- package/packages/runtime/src/scheduler/scheduler.ts +168 -8
- package/packages/runtime/src/verification/index.ts +1 -0
- package/packages/runtime/src/verification/verification-manifest.ts +192 -0
- package/vscode-extension/oxe-agents-1.0.0.vsix +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { TaskResult } from './scheduler';
|
|
2
|
+
|
|
3
|
+
export type AgentRole = 'planner' | 'executor' | 'reviewer' | 'verifier';
|
|
4
|
+
|
|
5
|
+
export interface AgentBudget {
|
|
6
|
+
max_tokens: number;
|
|
7
|
+
max_time_ms: number;
|
|
8
|
+
max_retries: number;
|
|
9
|
+
consumed_tokens: number;
|
|
10
|
+
consumed_time_ms: number;
|
|
11
|
+
consumed_retries: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CooperativeHandoff {
|
|
15
|
+
handoff_id: string;
|
|
16
|
+
from_agent_id: string;
|
|
17
|
+
to_agent_id: string;
|
|
18
|
+
from_role: AgentRole;
|
|
19
|
+
to_role: AgentRole;
|
|
20
|
+
work_item_id: string;
|
|
21
|
+
context_pack_ref: string | null;
|
|
22
|
+
transferred_at: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AgentActionLog {
|
|
26
|
+
agent_id: string;
|
|
27
|
+
role: AgentRole;
|
|
28
|
+
work_item_id: string;
|
|
29
|
+
action: 'execute' | 'verify' | 'review' | 'plan';
|
|
30
|
+
result: 'success' | 'failure';
|
|
31
|
+
duration_ms: number;
|
|
32
|
+
timestamp: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createBudget(
|
|
36
|
+
opts: Partial<Omit<AgentBudget, 'consumed_tokens' | 'consumed_time_ms' | 'consumed_retries'>> = {}
|
|
37
|
+
): AgentBudget {
|
|
38
|
+
return {
|
|
39
|
+
max_tokens: opts.max_tokens ?? Infinity,
|
|
40
|
+
max_time_ms: opts.max_time_ms ?? Infinity,
|
|
41
|
+
max_retries: opts.max_retries ?? Infinity,
|
|
42
|
+
consumed_tokens: 0,
|
|
43
|
+
consumed_time_ms: 0,
|
|
44
|
+
consumed_retries: 0,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function consumeBudget(
|
|
49
|
+
budget: AgentBudget,
|
|
50
|
+
delta: { tokens?: number; time_ms?: number; retries?: number }
|
|
51
|
+
): AgentBudget {
|
|
52
|
+
return {
|
|
53
|
+
...budget,
|
|
54
|
+
consumed_tokens: budget.consumed_tokens + (delta.tokens ?? 0),
|
|
55
|
+
consumed_time_ms: budget.consumed_time_ms + (delta.time_ms ?? 0),
|
|
56
|
+
consumed_retries: budget.consumed_retries + (delta.retries ?? 0),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isBudgetExhausted(budget: AgentBudget): boolean {
|
|
61
|
+
return (
|
|
62
|
+
budget.consumed_tokens >= budget.max_tokens ||
|
|
63
|
+
budget.consumed_time_ms >= budget.max_time_ms ||
|
|
64
|
+
budget.consumed_retries >= budget.max_retries
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildHandoff(opts: {
|
|
69
|
+
from_agent_id: string;
|
|
70
|
+
to_agent_id: string;
|
|
71
|
+
from_role: AgentRole;
|
|
72
|
+
to_role: AgentRole;
|
|
73
|
+
work_item_id: string;
|
|
74
|
+
context_pack_ref?: string | null;
|
|
75
|
+
}): CooperativeHandoff {
|
|
76
|
+
return {
|
|
77
|
+
handoff_id: `hoff-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
|
|
78
|
+
from_agent_id: opts.from_agent_id,
|
|
79
|
+
to_agent_id: opts.to_agent_id,
|
|
80
|
+
from_role: opts.from_role,
|
|
81
|
+
to_role: opts.to_role,
|
|
82
|
+
work_item_id: opts.work_item_id,
|
|
83
|
+
context_pack_ref: opts.context_pack_ref ?? null,
|
|
84
|
+
transferred_at: new Date().toISOString(),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── ArbitrationEngine ────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
export class ArbitrationEngine {
|
|
91
|
+
/**
|
|
92
|
+
* Choose the best result from multiple competing agents.
|
|
93
|
+
* Prefers success; among successes prefers more evidence; falls back to first.
|
|
94
|
+
*/
|
|
95
|
+
arbitrate(results: Array<{ agent_id: string; result: TaskResult }>): TaskResult {
|
|
96
|
+
if (results.length === 0) {
|
|
97
|
+
return { success: false, failure_class: null, evidence: [], output: 'no results to arbitrate' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const successes = results.filter((r) => r.result.success);
|
|
101
|
+
if (successes.length === 0) {
|
|
102
|
+
return results[0].result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Among successes, prefer the one with most evidence
|
|
106
|
+
successes.sort((a, b) => b.result.evidence.length - a.result.evidence.length);
|
|
107
|
+
return successes[0].result;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -4,6 +4,8 @@ import type { WorkspaceManager } from '../workspace/workspace-manager';
|
|
|
4
4
|
import type { TaskExecutor, TaskResult, SchedulerContext } from './scheduler';
|
|
5
5
|
import { Scheduler } from './scheduler';
|
|
6
6
|
import type { WorkspaceLease } from '../models/workspace';
|
|
7
|
+
import { buildHandoff } from './agent-roles';
|
|
8
|
+
import type { CooperativeHandoff } from './agent-roles';
|
|
7
9
|
|
|
8
10
|
export type CoordinationMode = 'parallel' | 'competitive' | 'cooperative';
|
|
9
11
|
|
|
@@ -31,6 +33,7 @@ export interface CoordinationResult {
|
|
|
31
33
|
failed: string[];
|
|
32
34
|
blocked: string[];
|
|
33
35
|
agent_results: Array<{ agent_id: string; completed: string[]; failed: string[] }>;
|
|
36
|
+
handoffs?: CooperativeHandoff[];
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
// ─── Parallel mode ───────────────────────────────────────────────────────────
|
|
@@ -194,6 +197,108 @@ async function competeTwoAgents(
|
|
|
194
197
|
return winner;
|
|
195
198
|
}
|
|
196
199
|
|
|
200
|
+
// ─── Cooperative mode ────────────────────────────────────────────────────────
|
|
201
|
+
// planner (agent[0]) does a dry-run to collect context, then hands off to
|
|
202
|
+
// executor (agent[1]) which performs the real run. Handoffs are recorded.
|
|
203
|
+
|
|
204
|
+
async function runCooperative(
|
|
205
|
+
graph: ExecutionGraph,
|
|
206
|
+
opts: CoordinationOptions
|
|
207
|
+
): Promise<CoordinationResult> {
|
|
208
|
+
if (opts.agents.length < 2) {
|
|
209
|
+
throw new Error('Cooperative mode requires at least 2 agents');
|
|
210
|
+
}
|
|
211
|
+
const [planner, executor] = opts.agents;
|
|
212
|
+
const { projectRoot, sessionId, runId } = opts;
|
|
213
|
+
const handoffs: CooperativeHandoff[] = [];
|
|
214
|
+
|
|
215
|
+
appendEvent(projectRoot, sessionId, {
|
|
216
|
+
type: 'RunStarted',
|
|
217
|
+
run_id: runId,
|
|
218
|
+
payload: { mode: 'cooperative', planner: planner.id, executor: executor.id },
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const completed: string[] = [];
|
|
222
|
+
const failed: string[] = [];
|
|
223
|
+
|
|
224
|
+
for (const wave of graph.waves) {
|
|
225
|
+
for (const nodeId of wave.node_ids) {
|
|
226
|
+
const node = graph.nodes.get(nodeId)!;
|
|
227
|
+
|
|
228
|
+
// Phase 1: planner allocates workspace + signals readiness (no output used)
|
|
229
|
+
const planAlloc = await planner.workspaceManager.allocate({
|
|
230
|
+
work_item_id: nodeId,
|
|
231
|
+
attempt_number: 1,
|
|
232
|
+
strategy: node.workspace_strategy,
|
|
233
|
+
mutation_scope: node.mutation_scope,
|
|
234
|
+
});
|
|
235
|
+
await planner.workspaceManager.dispose(planAlloc.workspace_id).catch(() => {});
|
|
236
|
+
|
|
237
|
+
const handoff = buildHandoff({
|
|
238
|
+
from_agent_id: planner.id,
|
|
239
|
+
to_agent_id: executor.id,
|
|
240
|
+
from_role: 'planner',
|
|
241
|
+
to_role: 'executor',
|
|
242
|
+
work_item_id: nodeId,
|
|
243
|
+
context_pack_ref: null,
|
|
244
|
+
});
|
|
245
|
+
handoffs.push(handoff);
|
|
246
|
+
|
|
247
|
+
appendEvent(projectRoot, sessionId, {
|
|
248
|
+
type: 'AttemptStarted',
|
|
249
|
+
run_id: runId,
|
|
250
|
+
work_item_id: nodeId,
|
|
251
|
+
payload: { mode: 'cooperative', handoff_id: handoff.handoff_id },
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Phase 2: executor performs the real task
|
|
255
|
+
const execAlloc = await executor.workspaceManager.allocate({
|
|
256
|
+
work_item_id: nodeId,
|
|
257
|
+
attempt_number: 1,
|
|
258
|
+
strategy: node.workspace_strategy,
|
|
259
|
+
mutation_scope: node.mutation_scope,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
let result: TaskResult;
|
|
263
|
+
try {
|
|
264
|
+
result = await executor.executor.execute(node, execAlloc, runId, 1);
|
|
265
|
+
} catch (e) {
|
|
266
|
+
result = { success: false, failure_class: 'env', evidence: [], output: String(e) };
|
|
267
|
+
}
|
|
268
|
+
await executor.workspaceManager.dispose(execAlloc.workspace_id).catch(() => {});
|
|
269
|
+
|
|
270
|
+
if (result.success) {
|
|
271
|
+
completed.push(nodeId);
|
|
272
|
+
appendEvent(projectRoot, sessionId, { type: 'WorkItemCompleted', run_id: runId, work_item_id: nodeId, payload: { mode: 'cooperative' } });
|
|
273
|
+
} else {
|
|
274
|
+
failed.push(nodeId);
|
|
275
|
+
appendEvent(projectRoot, sessionId, { type: 'WorkItemBlocked', run_id: runId, work_item_id: nodeId, payload: { mode: 'cooperative', failure_class: result.failure_class } });
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (failed.length > 0) break;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
appendEvent(projectRoot, sessionId, {
|
|
283
|
+
type: 'RunCompleted',
|
|
284
|
+
run_id: runId,
|
|
285
|
+
payload: { mode: 'cooperative', completed: completed.length, failed: failed.length },
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
mode: 'cooperative',
|
|
290
|
+
run_id: runId,
|
|
291
|
+
completed,
|
|
292
|
+
failed,
|
|
293
|
+
blocked: [],
|
|
294
|
+
agent_results: [
|
|
295
|
+
{ agent_id: planner.id, completed: [], failed: [] },
|
|
296
|
+
{ agent_id: executor.id, completed, failed },
|
|
297
|
+
],
|
|
298
|
+
handoffs,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
197
302
|
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
198
303
|
|
|
199
304
|
export class MultiAgentCoordinator {
|
|
@@ -201,10 +306,7 @@ export class MultiAgentCoordinator {
|
|
|
201
306
|
switch (opts.mode) {
|
|
202
307
|
case 'parallel': return runParallel(graph, opts);
|
|
203
308
|
case 'competitive': return runCompetitive(graph, opts);
|
|
204
|
-
case 'cooperative':
|
|
205
|
-
// Cooperative mode: planner (agent[0]) prepares context,
|
|
206
|
-
// executor (agent[1]) implements. For R3, delegate to sequential parallel.
|
|
207
|
-
return runParallel(graph, { ...opts, mode: 'parallel' });
|
|
309
|
+
case 'cooperative': return runCooperative(graph, opts);
|
|
208
310
|
default:
|
|
209
311
|
throw new Error(`Unknown coordination mode: ${opts.mode}`);
|
|
210
312
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import type { RunResult } from './scheduler';
|
|
4
|
+
|
|
5
|
+
export interface RunJournal {
|
|
6
|
+
run_id: string;
|
|
7
|
+
paused_at: string | null;
|
|
8
|
+
cancelled: boolean;
|
|
9
|
+
eligible_work_items: string[];
|
|
10
|
+
completed_work_items: string[];
|
|
11
|
+
failed_work_items: string[];
|
|
12
|
+
blocked_work_items: string[];
|
|
13
|
+
pending_gates: string[];
|
|
14
|
+
replay_cursor: string | null;
|
|
15
|
+
scheduler_state: 'running' | 'paused' | 'cancelled' | 'completed';
|
|
16
|
+
partial_result: Omit<RunResult, 'status'> | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function journalPath(projectRoot: string, runId: string): string {
|
|
20
|
+
return path.join(projectRoot, '.oxe', 'runs', runId, 'journal.json');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function saveJournal(projectRoot: string, runId: string, journal: RunJournal): void {
|
|
24
|
+
const p = journalPath(projectRoot, runId);
|
|
25
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
26
|
+
fs.writeFileSync(p, JSON.stringify(journal, null, 2), 'utf8');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function loadJournal(projectRoot: string, runId: string): RunJournal | null {
|
|
30
|
+
const p = journalPath(projectRoot, runId);
|
|
31
|
+
if (!fs.existsSync(p)) return null;
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(fs.readFileSync(p, 'utf8')) as RunJournal;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function deleteJournal(projectRoot: string, runId: string): void {
|
|
40
|
+
const p = journalPath(projectRoot, runId);
|
|
41
|
+
try {
|
|
42
|
+
fs.unlinkSync(p);
|
|
43
|
+
} catch {
|
|
44
|
+
// ignore if not found
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createJournal(runId: string): RunJournal {
|
|
49
|
+
return {
|
|
50
|
+
run_id: runId,
|
|
51
|
+
paused_at: null,
|
|
52
|
+
cancelled: false,
|
|
53
|
+
eligible_work_items: [],
|
|
54
|
+
completed_work_items: [],
|
|
55
|
+
failed_work_items: [],
|
|
56
|
+
blocked_work_items: [],
|
|
57
|
+
pending_gates: [],
|
|
58
|
+
replay_cursor: null,
|
|
59
|
+
scheduler_state: 'running',
|
|
60
|
+
partial_result: null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -4,6 +4,13 @@ import type { EventInput } from '../events/bus';
|
|
|
4
4
|
import type { ExecutionGraph, GraphNode } from '../compiler/graph-compiler';
|
|
5
5
|
import type { WorkspaceManager, WorkspaceRequest } from '../workspace/workspace-manager';
|
|
6
6
|
import type { WorkspaceLease } from '../models/workspace';
|
|
7
|
+
import {
|
|
8
|
+
saveJournal,
|
|
9
|
+
loadJournal,
|
|
10
|
+
deleteJournal,
|
|
11
|
+
createJournal,
|
|
12
|
+
} from './run-journal';
|
|
13
|
+
import type { RunJournal } from './run-journal';
|
|
7
14
|
|
|
8
15
|
export interface TaskResult {
|
|
9
16
|
success: boolean;
|
|
@@ -32,7 +39,7 @@ export interface SchedulerContext {
|
|
|
32
39
|
|
|
33
40
|
export interface RunResult {
|
|
34
41
|
run_id: string;
|
|
35
|
-
status: 'completed' | 'failed' | 'cancelled';
|
|
42
|
+
status: 'completed' | 'failed' | 'cancelled' | 'paused';
|
|
36
43
|
completed: string[];
|
|
37
44
|
failed: string[];
|
|
38
45
|
blocked: string[];
|
|
@@ -43,10 +50,13 @@ type NodeStatus = 'pending' | 'ready' | 'running' | 'completed' | 'failed' | 'bl
|
|
|
43
50
|
export class Scheduler {
|
|
44
51
|
private cancelled = false;
|
|
45
52
|
private paused = false;
|
|
53
|
+
private journal: RunJournal | null = null;
|
|
54
|
+
private ctx: SchedulerContext | null = null;
|
|
46
55
|
|
|
47
56
|
async run(graph: ExecutionGraph, ctx: SchedulerContext): Promise<RunResult> {
|
|
48
57
|
this.cancelled = false;
|
|
49
58
|
this.paused = false;
|
|
59
|
+
this.ctx = ctx;
|
|
50
60
|
|
|
51
61
|
const status = new Map<string, NodeStatus>();
|
|
52
62
|
for (const id of graph.nodes.keys()) status.set(id, 'pending');
|
|
@@ -55,10 +65,26 @@ export class Scheduler {
|
|
|
55
65
|
const failed: string[] = [];
|
|
56
66
|
const blocked: string[] = [];
|
|
57
67
|
|
|
68
|
+
this.journal = createJournal(ctx.runId);
|
|
69
|
+
saveJournal(ctx.projectRoot, ctx.runId, this.journal);
|
|
70
|
+
|
|
58
71
|
this.emit(ctx, { type: 'RunStarted', payload: { run_id: ctx.runId } });
|
|
59
72
|
|
|
60
73
|
for (const wave of graph.waves) {
|
|
61
74
|
if (this.cancelled) break;
|
|
75
|
+
|
|
76
|
+
// Respect pause: persist journal and return paused result
|
|
77
|
+
if (this.paused) {
|
|
78
|
+
this.journal.scheduler_state = 'paused';
|
|
79
|
+
this.journal.paused_at = new Date().toISOString();
|
|
80
|
+
this.journal.completed_work_items = completed.slice();
|
|
81
|
+
this.journal.failed_work_items = failed.slice();
|
|
82
|
+
this.journal.blocked_work_items = blocked.slice();
|
|
83
|
+
this.journal.partial_result = { run_id: ctx.runId, completed, failed, blocked };
|
|
84
|
+
saveJournal(ctx.projectRoot, ctx.runId, this.journal);
|
|
85
|
+
return { run_id: ctx.runId, status: 'paused', completed, failed, blocked };
|
|
86
|
+
}
|
|
87
|
+
|
|
62
88
|
const waveFailed = await this.runWave(
|
|
63
89
|
wave.node_ids,
|
|
64
90
|
graph,
|
|
@@ -68,6 +94,13 @@ export class Scheduler {
|
|
|
68
94
|
failed,
|
|
69
95
|
blocked
|
|
70
96
|
);
|
|
97
|
+
|
|
98
|
+
// Sync journal after each wave
|
|
99
|
+
this.journal.completed_work_items = completed.slice();
|
|
100
|
+
this.journal.failed_work_items = failed.slice();
|
|
101
|
+
this.journal.blocked_work_items = blocked.slice();
|
|
102
|
+
saveJournal(ctx.projectRoot, ctx.runId, this.journal);
|
|
103
|
+
|
|
71
104
|
if (waveFailed) break;
|
|
72
105
|
}
|
|
73
106
|
|
|
@@ -95,6 +128,116 @@ export class Scheduler {
|
|
|
95
128
|
payload: { run_id: ctx.runId, status: finalStatus },
|
|
96
129
|
});
|
|
97
130
|
|
|
131
|
+
this.journal.scheduler_state = this.cancelled ? 'cancelled' : 'completed';
|
|
132
|
+
this.journal.completed_work_items = completed.slice();
|
|
133
|
+
this.journal.failed_work_items = failed.slice();
|
|
134
|
+
this.journal.blocked_work_items = blocked.slice();
|
|
135
|
+
saveJournal(ctx.projectRoot, ctx.runId, this.journal);
|
|
136
|
+
|
|
137
|
+
return { run_id: ctx.runId, status: finalStatus, completed, failed, blocked };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Recover a previously paused run by loading its journal and re-running
|
|
142
|
+
* only the work items that haven't completed yet.
|
|
143
|
+
*/
|
|
144
|
+
async recover(runId: string, ctx: SchedulerContext, graph: ExecutionGraph): Promise<RunResult | null> {
|
|
145
|
+
const journal = loadJournal(ctx.projectRoot, runId);
|
|
146
|
+
if (!journal || journal.scheduler_state !== 'paused') return null;
|
|
147
|
+
|
|
148
|
+
// Restore state from journal
|
|
149
|
+
this.cancelled = false;
|
|
150
|
+
this.paused = false;
|
|
151
|
+
this.ctx = ctx;
|
|
152
|
+
this.journal = { ...journal, scheduler_state: 'running', paused_at: null };
|
|
153
|
+
|
|
154
|
+
const restoredCompleted = new Set(journal.completed_work_items);
|
|
155
|
+
const restoredFailed = new Set(journal.failed_work_items);
|
|
156
|
+
const restoredBlocked = new Set(journal.blocked_work_items);
|
|
157
|
+
|
|
158
|
+
const status = new Map<string, NodeStatus>();
|
|
159
|
+
for (const id of graph.nodes.keys()) {
|
|
160
|
+
if (restoredCompleted.has(id)) status.set(id, 'completed');
|
|
161
|
+
else if (restoredFailed.has(id)) status.set(id, 'failed');
|
|
162
|
+
else if (restoredBlocked.has(id)) status.set(id, 'blocked');
|
|
163
|
+
else status.set(id, 'pending');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const completed = [...restoredCompleted];
|
|
167
|
+
const failed = [...restoredFailed];
|
|
168
|
+
const blocked = [...restoredBlocked];
|
|
169
|
+
|
|
170
|
+
saveJournal(ctx.projectRoot, runId, this.journal);
|
|
171
|
+
|
|
172
|
+
this.emit(ctx, { type: 'RunStarted', payload: { run_id: ctx.runId, recovered: true } });
|
|
173
|
+
|
|
174
|
+
for (const wave of graph.waves) {
|
|
175
|
+
if (this.cancelled) break;
|
|
176
|
+
if (this.paused) {
|
|
177
|
+
this.journal.scheduler_state = 'paused';
|
|
178
|
+
this.journal.paused_at = new Date().toISOString();
|
|
179
|
+
this.journal.completed_work_items = completed.slice();
|
|
180
|
+
this.journal.failed_work_items = failed.slice();
|
|
181
|
+
this.journal.blocked_work_items = blocked.slice();
|
|
182
|
+
this.journal.partial_result = { run_id: ctx.runId, completed, failed, blocked };
|
|
183
|
+
saveJournal(ctx.projectRoot, ctx.runId, this.journal);
|
|
184
|
+
return { run_id: ctx.runId, status: 'paused', completed, failed, blocked };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Skip waves fully completed
|
|
188
|
+
const allDone = wave.node_ids.every(
|
|
189
|
+
(id) => restoredCompleted.has(id) || restoredFailed.has(id) || restoredBlocked.has(id)
|
|
190
|
+
);
|
|
191
|
+
if (allDone) continue;
|
|
192
|
+
|
|
193
|
+
const waveFailed = await this.runWave(
|
|
194
|
+
wave.node_ids,
|
|
195
|
+
graph,
|
|
196
|
+
ctx,
|
|
197
|
+
status,
|
|
198
|
+
completed,
|
|
199
|
+
failed,
|
|
200
|
+
blocked
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
this.journal.completed_work_items = completed.slice();
|
|
204
|
+
this.journal.failed_work_items = failed.slice();
|
|
205
|
+
this.journal.blocked_work_items = blocked.slice();
|
|
206
|
+
saveJournal(ctx.projectRoot, ctx.runId, this.journal);
|
|
207
|
+
|
|
208
|
+
if (waveFailed) break;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const [id, s] of status) {
|
|
212
|
+
if (s === 'pending') {
|
|
213
|
+
status.set(id, 'blocked');
|
|
214
|
+
blocked.push(id);
|
|
215
|
+
this.emit(ctx, {
|
|
216
|
+
type: 'WorkItemBlocked',
|
|
217
|
+
work_item_id: id,
|
|
218
|
+
payload: { reason: 'upstream_wave_failed' },
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const finalStatus: RunResult['status'] = this.cancelled
|
|
224
|
+
? 'cancelled'
|
|
225
|
+
: failed.length > 0
|
|
226
|
+
? 'failed'
|
|
227
|
+
: 'completed';
|
|
228
|
+
|
|
229
|
+
this.emit(ctx, {
|
|
230
|
+
type: 'RunCompleted',
|
|
231
|
+
payload: { run_id: ctx.runId, status: finalStatus, recovered: true },
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
this.journal.scheduler_state = this.cancelled ? 'cancelled' : 'completed';
|
|
235
|
+
this.journal.completed_work_items = completed.slice();
|
|
236
|
+
this.journal.failed_work_items = failed.slice();
|
|
237
|
+
this.journal.blocked_work_items = blocked.slice();
|
|
238
|
+
saveJournal(ctx.projectRoot, ctx.runId, this.journal);
|
|
239
|
+
deleteJournal(ctx.projectRoot, ctx.runId);
|
|
240
|
+
|
|
98
241
|
return { run_id: ctx.runId, status: finalStatus, completed, failed, blocked };
|
|
99
242
|
}
|
|
100
243
|
|
|
@@ -107,11 +250,11 @@ export class Scheduler {
|
|
|
107
250
|
failed: string[],
|
|
108
251
|
blocked: string[]
|
|
109
252
|
): Promise<boolean> {
|
|
110
|
-
// Partition: eligible vs blocked-by-dep
|
|
111
253
|
const eligible: string[] = [];
|
|
112
254
|
const depsNotMet: string[] = [];
|
|
113
255
|
|
|
114
256
|
for (const id of nodeIds) {
|
|
257
|
+
if (status.get(id) === 'completed') continue; // already done in recovery
|
|
115
258
|
const node = graph.nodes.get(id)!;
|
|
116
259
|
const depsMet = node.depends_on.every((dep) => status.get(dep) === 'completed');
|
|
117
260
|
if (depsMet) {
|
|
@@ -121,7 +264,6 @@ export class Scheduler {
|
|
|
121
264
|
}
|
|
122
265
|
}
|
|
123
266
|
|
|
124
|
-
// Nodes whose deps weren't met in this wave → blocked
|
|
125
267
|
for (const id of depsNotMet) {
|
|
126
268
|
status.set(id, 'blocked');
|
|
127
269
|
blocked.push(id);
|
|
@@ -132,21 +274,18 @@ export class Scheduler {
|
|
|
132
274
|
});
|
|
133
275
|
}
|
|
134
276
|
|
|
135
|
-
// Separate read-only (no mutation_scope) from mutation nodes
|
|
136
277
|
const readOnly = eligible.filter((id) => {
|
|
137
278
|
const node = graph.nodes.get(id)!;
|
|
138
279
|
return node.mutation_scope.length === 0;
|
|
139
280
|
});
|
|
140
281
|
const mutations = eligible.filter((id) => !readOnly.includes(id));
|
|
141
282
|
|
|
142
|
-
// Run read-only nodes in parallel
|
|
143
283
|
if (readOnly.length > 0) {
|
|
144
284
|
await Promise.all(
|
|
145
285
|
readOnly.map((id) => this.runNode(id, graph, ctx, status, completed, failed))
|
|
146
286
|
);
|
|
147
287
|
}
|
|
148
288
|
|
|
149
|
-
// Run mutation nodes sequentially to avoid scope conflicts
|
|
150
289
|
for (const id of mutations) {
|
|
151
290
|
if (this.cancelled) break;
|
|
152
291
|
await this.runNode(id, graph, ctx, status, completed, failed);
|
|
@@ -214,7 +353,6 @@ export class Scheduler {
|
|
|
214
353
|
return;
|
|
215
354
|
}
|
|
216
355
|
|
|
217
|
-
// Policy failures never retry
|
|
218
356
|
if (lastResult.failure_class === 'policy') break;
|
|
219
357
|
|
|
220
358
|
if (attempt < maxAttempts) {
|
|
@@ -246,7 +384,6 @@ export class Scheduler {
|
|
|
246
384
|
}
|
|
247
385
|
}
|
|
248
386
|
|
|
249
|
-
// All attempts exhausted
|
|
250
387
|
this.emit(ctx, {
|
|
251
388
|
type: 'WorkItemBlocked',
|
|
252
389
|
work_item_id: nodeId,
|
|
@@ -258,14 +395,37 @@ export class Scheduler {
|
|
|
258
395
|
|
|
259
396
|
pause(): void {
|
|
260
397
|
this.paused = true;
|
|
398
|
+
if (this.journal && this.ctx) {
|
|
399
|
+
this.journal.scheduler_state = 'paused';
|
|
400
|
+
this.journal.paused_at = new Date().toISOString();
|
|
401
|
+
saveJournal(this.ctx.projectRoot, this.ctx.runId, this.journal);
|
|
402
|
+
}
|
|
261
403
|
}
|
|
262
404
|
|
|
263
405
|
resume(): void {
|
|
264
406
|
this.paused = false;
|
|
407
|
+
if (this.journal && this.ctx) {
|
|
408
|
+
this.journal.scheduler_state = 'running';
|
|
409
|
+
this.journal.paused_at = null;
|
|
410
|
+
saveJournal(this.ctx.projectRoot, this.ctx.runId, this.journal);
|
|
411
|
+
}
|
|
265
412
|
}
|
|
266
413
|
|
|
267
414
|
cancel(): void {
|
|
268
415
|
this.cancelled = true;
|
|
416
|
+
if (this.journal && this.ctx) {
|
|
417
|
+
this.journal.cancelled = true;
|
|
418
|
+
this.journal.scheduler_state = 'cancelled';
|
|
419
|
+
saveJournal(this.ctx.projectRoot, this.ctx.runId, this.journal);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
getJournal(): RunJournal | null {
|
|
424
|
+
return this.journal;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
static loadJournal(projectRoot: string, runId: string): RunJournal | null {
|
|
428
|
+
return loadJournal(projectRoot, runId);
|
|
269
429
|
}
|
|
270
430
|
|
|
271
431
|
private emit(
|