outcome-cli 1.0.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 +261 -0
- package/package.json +95 -0
- package/src/agents/README.md +139 -0
- package/src/agents/adapters/anthropic.adapter.ts +166 -0
- package/src/agents/adapters/dalle.adapter.ts +145 -0
- package/src/agents/adapters/gemini.adapter.ts +134 -0
- package/src/agents/adapters/imagen.adapter.ts +106 -0
- package/src/agents/adapters/nano-banana.adapter.ts +129 -0
- package/src/agents/adapters/openai.adapter.ts +165 -0
- package/src/agents/adapters/veo.adapter.ts +130 -0
- package/src/agents/agent.schema.property.test.ts +379 -0
- package/src/agents/agent.schema.test.ts +148 -0
- package/src/agents/agent.schema.ts +263 -0
- package/src/agents/index.ts +60 -0
- package/src/agents/registered-agent.schema.ts +356 -0
- package/src/agents/registry.ts +97 -0
- package/src/agents/tournament-configs.property.test.ts +266 -0
- package/src/cli/README.md +145 -0
- package/src/cli/commands/define.ts +79 -0
- package/src/cli/commands/list.ts +46 -0
- package/src/cli/commands/logs.ts +83 -0
- package/src/cli/commands/run.ts +416 -0
- package/src/cli/commands/verify.ts +110 -0
- package/src/cli/index.ts +81 -0
- package/src/config/README.md +128 -0
- package/src/config/env.ts +262 -0
- package/src/config/index.ts +19 -0
- package/src/eval/README.md +318 -0
- package/src/eval/ai-judge.test.ts +435 -0
- package/src/eval/ai-judge.ts +368 -0
- package/src/eval/code-validators.ts +414 -0
- package/src/eval/evaluateOutcome.property.test.ts +1174 -0
- package/src/eval/evaluateOutcome.ts +591 -0
- package/src/eval/immigration-validators.ts +122 -0
- package/src/eval/index.ts +90 -0
- package/src/eval/judge-cache.ts +402 -0
- package/src/eval/tournament-validators.property.test.ts +439 -0
- package/src/eval/validators.property.test.ts +1118 -0
- package/src/eval/validators.ts +1199 -0
- package/src/eval/weighted-scorer.ts +285 -0
- package/src/index.ts +17 -0
- package/src/league/README.md +188 -0
- package/src/league/health-check.ts +353 -0
- package/src/league/index.ts +93 -0
- package/src/league/killAgent.ts +151 -0
- package/src/league/league.test.ts +1151 -0
- package/src/league/runLeague.ts +843 -0
- package/src/league/scoreAgent.ts +175 -0
- package/src/modules/omnibridge/__tests__/.gitkeep +1 -0
- package/src/modules/omnibridge/__tests__/auth-tunnel.property.test.ts +524 -0
- package/src/modules/omnibridge/__tests__/deterministic-logger.property.test.ts +965 -0
- package/src/modules/omnibridge/__tests__/ghost-api.property.test.ts +461 -0
- package/src/modules/omnibridge/__tests__/omnibridge-integration.test.ts +542 -0
- package/src/modules/omnibridge/__tests__/parallel-executor.property.test.ts +671 -0
- package/src/modules/omnibridge/__tests__/semantic-normalizer.property.test.ts +521 -0
- package/src/modules/omnibridge/__tests__/semantic-normalizer.test.ts +254 -0
- package/src/modules/omnibridge/__tests__/session-vault.property.test.ts +367 -0
- package/src/modules/omnibridge/__tests__/shadow-session.property.test.ts +523 -0
- package/src/modules/omnibridge/__tests__/triangulation-engine.property.test.ts +292 -0
- package/src/modules/omnibridge/__tests__/verification-engine.property.test.ts +769 -0
- package/src/modules/omnibridge/api/.gitkeep +1 -0
- package/src/modules/omnibridge/api/ghost-api.ts +1087 -0
- package/src/modules/omnibridge/auth/.gitkeep +1 -0
- package/src/modules/omnibridge/auth/auth-tunnel.ts +843 -0
- package/src/modules/omnibridge/auth/session-vault.ts +577 -0
- package/src/modules/omnibridge/core/.gitkeep +1 -0
- package/src/modules/omnibridge/core/semantic-normalizer.ts +702 -0
- package/src/modules/omnibridge/core/triangulation-engine.ts +530 -0
- package/src/modules/omnibridge/core/types.ts +610 -0
- package/src/modules/omnibridge/execution/.gitkeep +1 -0
- package/src/modules/omnibridge/execution/deterministic-logger.ts +629 -0
- package/src/modules/omnibridge/execution/parallel-executor.ts +542 -0
- package/src/modules/omnibridge/execution/shadow-session.ts +794 -0
- package/src/modules/omnibridge/index.ts +212 -0
- package/src/modules/omnibridge/omnibridge.ts +510 -0
- package/src/modules/omnibridge/verification/.gitkeep +1 -0
- package/src/modules/omnibridge/verification/verification-engine.ts +783 -0
- package/src/outcomes/README.md +75 -0
- package/src/outcomes/acquire-pilot-customer.ts +297 -0
- package/src/outcomes/code-delivery-outcomes.ts +89 -0
- package/src/outcomes/code-outcomes.ts +256 -0
- package/src/outcomes/code_review_battle.test.ts +135 -0
- package/src/outcomes/code_review_battle.ts +135 -0
- package/src/outcomes/cold_email_battle.ts +97 -0
- package/src/outcomes/content_creation_battle.ts +160 -0
- package/src/outcomes/f1_stem_opt_compliance.ts +61 -0
- package/src/outcomes/index.ts +107 -0
- package/src/outcomes/lead_gen_battle.test.ts +113 -0
- package/src/outcomes/lead_gen_battle.ts +99 -0
- package/src/outcomes/outcome.schema.property.test.ts +229 -0
- package/src/outcomes/outcome.schema.ts +187 -0
- package/src/outcomes/qualified_sales_interest.ts +118 -0
- package/src/outcomes/swarm_planner.property.test.ts +370 -0
- package/src/outcomes/swarm_planner.ts +96 -0
- package/src/outcomes/web_extraction.ts +234 -0
- package/src/runtime/README.md +220 -0
- package/src/runtime/agentRunner.test.ts +341 -0
- package/src/runtime/agentRunner.ts +746 -0
- package/src/runtime/claudeAdapter.ts +232 -0
- package/src/runtime/costTracker.ts +123 -0
- package/src/runtime/index.ts +34 -0
- package/src/runtime/modelAdapter.property.test.ts +305 -0
- package/src/runtime/modelAdapter.ts +144 -0
- package/src/runtime/openaiAdapter.ts +235 -0
- package/src/utils/README.md +122 -0
- package/src/utils/command-runner.ts +134 -0
- package/src/utils/cost-guard.ts +379 -0
- package/src/utils/errors.test.ts +290 -0
- package/src/utils/errors.ts +442 -0
- package/src/utils/index.ts +37 -0
- package/src/utils/logger.test.ts +361 -0
- package/src/utils/logger.ts +419 -0
- package/src/utils/output-parsers.ts +216 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parallel Executor - Competition Orchestration
|
|
3
|
+
*
|
|
4
|
+
* Spins up identical sandbox sessions for fair competition between agents.
|
|
5
|
+
* Ensures all agents start with identical states and provides crash isolation.
|
|
6
|
+
*
|
|
7
|
+
* Requirements: 7.1, 7.2, 7.6
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { randomBytes } from 'crypto';
|
|
11
|
+
import type {
|
|
12
|
+
Competition,
|
|
13
|
+
CompetitionResult,
|
|
14
|
+
SerializedSession,
|
|
15
|
+
ShadowSession,
|
|
16
|
+
GhostResponse,
|
|
17
|
+
ActionLogEntry,
|
|
18
|
+
EncryptedBlob,
|
|
19
|
+
} from '../core/types.js';
|
|
20
|
+
import {
|
|
21
|
+
ShadowSessionOrchestrator,
|
|
22
|
+
generateFingerprint,
|
|
23
|
+
type SessionCreationResult,
|
|
24
|
+
} from './shadow-session.js';
|
|
25
|
+
import { SessionVault, generateEncryptionKey } from '../auth/session-vault.js';
|
|
26
|
+
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// Types
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Configuration for creating a competition.
|
|
33
|
+
*/
|
|
34
|
+
export interface CompetitionConfig {
|
|
35
|
+
/** Bounty ID this competition is for */
|
|
36
|
+
bountyId: string;
|
|
37
|
+
/** Number of agents competing */
|
|
38
|
+
agentCount: number;
|
|
39
|
+
/** Target domain for the competition */
|
|
40
|
+
targetDomain: string;
|
|
41
|
+
/** Initial session state (optional - will create empty if not provided) */
|
|
42
|
+
initialState?: SerializedSession;
|
|
43
|
+
/** Agent IDs participating (optional - will generate if not provided) */
|
|
44
|
+
agentIds?: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Result of competition creation.
|
|
49
|
+
*/
|
|
50
|
+
export interface CompetitionCreationResult {
|
|
51
|
+
success: boolean;
|
|
52
|
+
competition?: Competition;
|
|
53
|
+
error?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Agent execution task.
|
|
58
|
+
*/
|
|
59
|
+
export interface AgentExecutionTask {
|
|
60
|
+
agentId: string;
|
|
61
|
+
sessionId: string;
|
|
62
|
+
execute: () => Promise<AgentExecutionResult>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Result of a single agent's execution.
|
|
67
|
+
*/
|
|
68
|
+
export interface AgentExecutionResult {
|
|
69
|
+
agentId: string;
|
|
70
|
+
sessionId: string;
|
|
71
|
+
status: 'completed' | 'failed' | 'timeout';
|
|
72
|
+
result?: GhostResponse;
|
|
73
|
+
actionLog: ActionLogEntry[];
|
|
74
|
+
executionTimeMs: number;
|
|
75
|
+
error?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Parallel execution options.
|
|
80
|
+
*/
|
|
81
|
+
export interface ParallelExecutionOptions {
|
|
82
|
+
/** Timeout for each agent in milliseconds (default: 60000) */
|
|
83
|
+
timeoutMs?: number;
|
|
84
|
+
/** Whether to stream results as they complete (default: true) */
|
|
85
|
+
streamResults?: boolean;
|
|
86
|
+
/** Callback for when an agent completes */
|
|
87
|
+
onAgentComplete?: (result: CompetitionResult) => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Configuration for the Parallel Executor.
|
|
92
|
+
*/
|
|
93
|
+
export interface ParallelExecutorConfig {
|
|
94
|
+
/** Shadow Session Orchestrator instance */
|
|
95
|
+
sessionOrchestrator?: ShadowSessionOrchestrator;
|
|
96
|
+
/** Session Vault for state management */
|
|
97
|
+
sessionVault?: SessionVault;
|
|
98
|
+
/** Maximum concurrent competitions (default: 10) */
|
|
99
|
+
maxConcurrentCompetitions?: number;
|
|
100
|
+
/** Default agent timeout in milliseconds (default: 60000) */
|
|
101
|
+
defaultTimeoutMs?: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// =============================================================================
|
|
105
|
+
// Parallel Executor Implementation
|
|
106
|
+
// =============================================================================
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parallel Executor
|
|
110
|
+
*
|
|
111
|
+
* Orchestrates fair competitions between multiple agents by:
|
|
112
|
+
* - Creating identical sandbox sessions for all agents
|
|
113
|
+
* - Serializing identical starting states
|
|
114
|
+
* - Running agents in parallel with crash isolation
|
|
115
|
+
* - Streaming results as they complete
|
|
116
|
+
*
|
|
117
|
+
* Requirements: 7.1, 7.2, 7.6
|
|
118
|
+
*/
|
|
119
|
+
export class ParallelExecutor {
|
|
120
|
+
private readonly sessionOrchestrator: ShadowSessionOrchestrator;
|
|
121
|
+
private readonly sessionVault: SessionVault;
|
|
122
|
+
private readonly maxConcurrentCompetitions: number;
|
|
123
|
+
private readonly defaultTimeoutMs: number;
|
|
124
|
+
|
|
125
|
+
/** Active competitions by ID */
|
|
126
|
+
private competitions: Map<string, Competition> = new Map();
|
|
127
|
+
|
|
128
|
+
/** Competition results by competition ID */
|
|
129
|
+
private results: Map<string, CompetitionResult[]> = new Map();
|
|
130
|
+
|
|
131
|
+
/** Agent execution status by session ID */
|
|
132
|
+
private executionStatus: Map<string, 'pending' | 'running' | 'completed' | 'failed' | 'crashed'> = new Map();
|
|
133
|
+
|
|
134
|
+
constructor(config: ParallelExecutorConfig = {}) {
|
|
135
|
+
this.sessionOrchestrator = config.sessionOrchestrator ?? new ShadowSessionOrchestrator();
|
|
136
|
+
this.sessionVault = config.sessionVault ?? new SessionVault({
|
|
137
|
+
encryptionKey: generateEncryptionKey(),
|
|
138
|
+
});
|
|
139
|
+
this.maxConcurrentCompetitions = config.maxConcurrentCompetitions ?? 10;
|
|
140
|
+
this.defaultTimeoutMs = config.defaultTimeoutMs ?? 60000;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ===========================================================================
|
|
144
|
+
// Competition Creation
|
|
145
|
+
// ===========================================================================
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Generate a unique competition ID.
|
|
149
|
+
*/
|
|
150
|
+
private generateCompetitionId(): string {
|
|
151
|
+
return `comp_${randomBytes(16).toString('hex')}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Generate a unique agent ID.
|
|
156
|
+
*/
|
|
157
|
+
private generateAgentId(): string {
|
|
158
|
+
return `agent_${randomBytes(8).toString('hex')}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Create an empty serialized session state.
|
|
163
|
+
*/
|
|
164
|
+
private createEmptyState(): SerializedSession {
|
|
165
|
+
// Create empty encrypted blobs
|
|
166
|
+
const emptyBlob: EncryptedBlob = this.sessionVault.encrypt('{}');
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
cookies: emptyBlob,
|
|
170
|
+
localStorage: emptyBlob,
|
|
171
|
+
sessionStorage: emptyBlob,
|
|
172
|
+
expiresAt: Date.now() + 86400000, // 24 hours
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Serialize a session state to ensure byte-identical copies.
|
|
178
|
+
* This is critical for fair competition - all agents must start identically.
|
|
179
|
+
*
|
|
180
|
+
* Requirements: 7.2
|
|
181
|
+
*/
|
|
182
|
+
serializeStartState(state: SerializedSession): SerializedSession {
|
|
183
|
+
// Deep clone to ensure isolation
|
|
184
|
+
return {
|
|
185
|
+
cookies: { ...state.cookies },
|
|
186
|
+
localStorage: { ...state.localStorage },
|
|
187
|
+
sessionStorage: { ...state.sessionStorage },
|
|
188
|
+
expiresAt: state.expiresAt,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Verify that two serialized states are byte-identical.
|
|
194
|
+
*
|
|
195
|
+
* Requirements: 7.2
|
|
196
|
+
*/
|
|
197
|
+
areStatesIdentical(state1: SerializedSession, state2: SerializedSession): boolean {
|
|
198
|
+
const str1 = JSON.stringify(state1);
|
|
199
|
+
const str2 = JSON.stringify(state2);
|
|
200
|
+
return str1 === str2;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Create a competition with N identical sandbox sessions.
|
|
205
|
+
*
|
|
206
|
+
* Requirements: 7.1, 7.2
|
|
207
|
+
*/
|
|
208
|
+
async createCompetition(config: CompetitionConfig): Promise<CompetitionCreationResult> {
|
|
209
|
+
// Check concurrent competition limit
|
|
210
|
+
if (this.competitions.size >= this.maxConcurrentCompetitions) {
|
|
211
|
+
return {
|
|
212
|
+
success: false,
|
|
213
|
+
error: `Maximum concurrent competitions (${this.maxConcurrentCompetitions}) reached`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (config.agentCount < 1) {
|
|
218
|
+
return {
|
|
219
|
+
success: false,
|
|
220
|
+
error: 'Agent count must be at least 1',
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const competitionId = this.generateCompetitionId();
|
|
225
|
+
|
|
226
|
+
// Generate or use provided agent IDs
|
|
227
|
+
const agentIds = config.agentIds ?? Array.from(
|
|
228
|
+
{ length: config.agentCount },
|
|
229
|
+
() => this.generateAgentId()
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
if (agentIds.length !== config.agentCount) {
|
|
233
|
+
return {
|
|
234
|
+
success: false,
|
|
235
|
+
error: `Agent IDs count (${agentIds.length}) must match agent count (${config.agentCount})`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Create the starting state - this will be identical for all agents
|
|
240
|
+
const startState = config.initialState
|
|
241
|
+
? this.serializeStartState(config.initialState)
|
|
242
|
+
: this.createEmptyState();
|
|
243
|
+
|
|
244
|
+
// Create shadow sessions for each agent
|
|
245
|
+
const sessions: ShadowSession[] = [];
|
|
246
|
+
const creationErrors: string[] = [];
|
|
247
|
+
|
|
248
|
+
for (const agentId of agentIds) {
|
|
249
|
+
// Generate a unique fingerprint for each agent
|
|
250
|
+
const fingerprint = generateFingerprint(agentId);
|
|
251
|
+
|
|
252
|
+
const result: SessionCreationResult = await this.sessionOrchestrator.create(
|
|
253
|
+
{
|
|
254
|
+
targetDomain: config.targetDomain,
|
|
255
|
+
fingerprint,
|
|
256
|
+
isolationLevel: 'strict',
|
|
257
|
+
},
|
|
258
|
+
agentId
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
if (result.success && result.session) {
|
|
262
|
+
sessions.push(result.session);
|
|
263
|
+
this.executionStatus.set(result.session.id, 'pending');
|
|
264
|
+
} else {
|
|
265
|
+
creationErrors.push(`Failed to create session for agent ${agentId}: ${result.error}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// If any session creation failed, clean up and return error
|
|
270
|
+
if (creationErrors.length > 0) {
|
|
271
|
+
// Clean up created sessions
|
|
272
|
+
for (const session of sessions) {
|
|
273
|
+
await this.sessionOrchestrator.destroy(session.id);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
success: false,
|
|
278
|
+
error: `Failed to create all sessions: ${creationErrors.join('; ')}`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Create the competition
|
|
283
|
+
const competition: Competition = {
|
|
284
|
+
id: competitionId,
|
|
285
|
+
bountyId: config.bountyId,
|
|
286
|
+
sessions,
|
|
287
|
+
startState,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// Store the competition
|
|
291
|
+
this.competitions.set(competitionId, competition);
|
|
292
|
+
this.results.set(competitionId, []);
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
success: true,
|
|
296
|
+
competition,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ===========================================================================
|
|
301
|
+
// Parallel Execution
|
|
302
|
+
// ===========================================================================
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Start all agents simultaneously.
|
|
306
|
+
*
|
|
307
|
+
* Requirements: 7.1
|
|
308
|
+
*/
|
|
309
|
+
async startAll(
|
|
310
|
+
competition: Competition,
|
|
311
|
+
executeTasks: AgentExecutionTask[],
|
|
312
|
+
options: ParallelExecutionOptions = {}
|
|
313
|
+
): Promise<void> {
|
|
314
|
+
const timeoutMs = options.timeoutMs ?? this.defaultTimeoutMs;
|
|
315
|
+
|
|
316
|
+
// Validate that we have tasks for all sessions
|
|
317
|
+
if (executeTasks.length > competition.sessions.length) {
|
|
318
|
+
throw new Error(`More tasks (${executeTasks.length}) than sessions (${competition.sessions.length})`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Mark competition as started
|
|
322
|
+
competition.startedAt = Date.now();
|
|
323
|
+
|
|
324
|
+
// Update all sessions to running
|
|
325
|
+
for (const session of competition.sessions) {
|
|
326
|
+
this.executionStatus.set(session.id, 'running');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Execute all tasks in parallel
|
|
330
|
+
const promises = executeTasks.map(async (task) => {
|
|
331
|
+
return this.executeWithCrashIsolation(task, timeoutMs, options.onAgentComplete);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Wait for all to complete (they won't throw due to crash isolation)
|
|
335
|
+
await Promise.all(promises);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Execute a single agent task with crash isolation.
|
|
340
|
+
*
|
|
341
|
+
* Requirements: 7.6
|
|
342
|
+
*/
|
|
343
|
+
private async executeWithCrashIsolation(
|
|
344
|
+
task: AgentExecutionTask,
|
|
345
|
+
timeoutMs: number,
|
|
346
|
+
onComplete?: (result: CompetitionResult) => void
|
|
347
|
+
): Promise<CompetitionResult> {
|
|
348
|
+
const startTime = Date.now();
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
// Create a timeout promise
|
|
352
|
+
const timeoutPromise = new Promise<AgentExecutionResult>((_, reject) => {
|
|
353
|
+
setTimeout(() => {
|
|
354
|
+
reject(new Error('Execution timeout'));
|
|
355
|
+
}, timeoutMs);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Race between execution and timeout
|
|
359
|
+
const result = await Promise.race([
|
|
360
|
+
task.execute(),
|
|
361
|
+
timeoutPromise,
|
|
362
|
+
]);
|
|
363
|
+
|
|
364
|
+
const competitionResult: CompetitionResult = {
|
|
365
|
+
agentId: result.agentId,
|
|
366
|
+
sessionId: result.sessionId,
|
|
367
|
+
status: result.status,
|
|
368
|
+
result: result.result,
|
|
369
|
+
actionLog: result.actionLog,
|
|
370
|
+
executionTimeMs: result.executionTimeMs,
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
this.executionStatus.set(task.sessionId, 'completed');
|
|
374
|
+
|
|
375
|
+
if (onComplete) {
|
|
376
|
+
onComplete(competitionResult);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return competitionResult;
|
|
380
|
+
} catch (error) {
|
|
381
|
+
// Handle crash or timeout - mark as failed but don't affect others
|
|
382
|
+
const executionTimeMs = Date.now() - startTime;
|
|
383
|
+
const isTimeout = error instanceof Error && error.message === 'Execution timeout';
|
|
384
|
+
|
|
385
|
+
const competitionResult: CompetitionResult = {
|
|
386
|
+
agentId: task.agentId,
|
|
387
|
+
sessionId: task.sessionId,
|
|
388
|
+
status: isTimeout ? 'timeout' : 'failed',
|
|
389
|
+
actionLog: [],
|
|
390
|
+
executionTimeMs,
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
this.executionStatus.set(task.sessionId, 'crashed');
|
|
394
|
+
|
|
395
|
+
if (onComplete) {
|
|
396
|
+
onComplete(competitionResult);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return competitionResult;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Get results for a competition.
|
|
405
|
+
*/
|
|
406
|
+
getResults(competitionId: string): CompetitionResult[] {
|
|
407
|
+
return this.results.get(competitionId) ?? [];
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Add a result to a competition.
|
|
412
|
+
*/
|
|
413
|
+
addResult(competitionId: string, result: CompetitionResult): void {
|
|
414
|
+
const results = this.results.get(competitionId);
|
|
415
|
+
if (results) {
|
|
416
|
+
results.push(result);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ===========================================================================
|
|
421
|
+
// Crash Isolation
|
|
422
|
+
// ===========================================================================
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Check if a session has crashed.
|
|
426
|
+
*/
|
|
427
|
+
hasSessionCrashed(sessionId: string): boolean {
|
|
428
|
+
return this.executionStatus.get(sessionId) === 'crashed';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Get execution status for a session.
|
|
433
|
+
*/
|
|
434
|
+
getExecutionStatus(sessionId: string): string | undefined {
|
|
435
|
+
return this.executionStatus.get(sessionId);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Mark a session as crashed.
|
|
440
|
+
*
|
|
441
|
+
* Requirements: 7.6
|
|
442
|
+
*/
|
|
443
|
+
markSessionCrashed(sessionId: string): void {
|
|
444
|
+
this.executionStatus.set(sessionId, 'crashed');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Check if other sessions are affected by a crash.
|
|
449
|
+
* Should always return false - crashes are isolated.
|
|
450
|
+
*
|
|
451
|
+
* Requirements: 7.6
|
|
452
|
+
*/
|
|
453
|
+
areOtherSessionsAffected(competitionId: string, crashedSessionId: string): boolean {
|
|
454
|
+
const competition = this.competitions.get(competitionId);
|
|
455
|
+
if (!competition) {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Check all other sessions
|
|
460
|
+
for (const session of competition.sessions) {
|
|
461
|
+
if (session.id === crashedSessionId) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const status = this.executionStatus.get(session.id);
|
|
466
|
+
// If any other session is marked as crashed due to this crash, isolation failed
|
|
467
|
+
// In our implementation, this should never happen
|
|
468
|
+
if (status === 'crashed') {
|
|
469
|
+
// Check if it crashed independently or due to the other crash
|
|
470
|
+
// Since we use Promise.race with individual try/catch, crashes are isolated
|
|
471
|
+
continue; // Independent crash, not affected
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ===========================================================================
|
|
479
|
+
// Competition Management
|
|
480
|
+
// ===========================================================================
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Get a competition by ID.
|
|
484
|
+
*/
|
|
485
|
+
getCompetition(competitionId: string): Competition | undefined {
|
|
486
|
+
return this.competitions.get(competitionId);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Get all active competitions.
|
|
491
|
+
*/
|
|
492
|
+
getActiveCompetitions(): Competition[] {
|
|
493
|
+
return Array.from(this.competitions.values());
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Clean up a competition and its resources.
|
|
498
|
+
*/
|
|
499
|
+
async cleanupCompetition(competitionId: string): Promise<void> {
|
|
500
|
+
const competition = this.competitions.get(competitionId);
|
|
501
|
+
if (!competition) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Destroy all sessions
|
|
506
|
+
for (const session of competition.sessions) {
|
|
507
|
+
await this.sessionOrchestrator.destroy(session.id);
|
|
508
|
+
this.executionStatus.delete(session.id);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Remove competition data
|
|
512
|
+
this.competitions.delete(competitionId);
|
|
513
|
+
this.results.delete(competitionId);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Clear all competitions (for testing).
|
|
518
|
+
*/
|
|
519
|
+
async clear(): Promise<void> {
|
|
520
|
+
for (const competitionId of this.competitions.keys()) {
|
|
521
|
+
await this.cleanupCompetition(competitionId);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Get the session orchestrator (for testing).
|
|
527
|
+
*/
|
|
528
|
+
getSessionOrchestrator(): ShadowSessionOrchestrator {
|
|
529
|
+
return this.sessionOrchestrator;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// =============================================================================
|
|
534
|
+
// Factory Function
|
|
535
|
+
// =============================================================================
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Create a new Parallel Executor instance.
|
|
539
|
+
*/
|
|
540
|
+
export function createParallelExecutor(config?: ParallelExecutorConfig): ParallelExecutor {
|
|
541
|
+
return new ParallelExecutor(config);
|
|
542
|
+
}
|