agent-relay 3.1.16 → 3.1.17
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/bin/agent-relay-broker-linux-arm64 +0 -0
- package/dist/index.cjs +565 -32
- package/package.json +8 -8
- package/packages/acp-bridge/package.json +2 -2
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts +16 -0
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts.map +1 -0
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.js +640 -0
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.js.map +1 -0
- package/packages/sdk/dist/workflows/cli.js +10 -0
- package/packages/sdk/dist/workflows/cli.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts +31 -0
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +534 -31
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/dist/workflows/trajectory.d.ts +22 -1
- package/packages/sdk/dist/workflows/trajectory.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/trajectory.js +55 -8
- package/packages/sdk/dist/workflows/trajectory.js.map +1 -1
- package/packages/sdk/dist/workflows/validator.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/validator.js +29 -0
- package/packages/sdk/dist/workflows/validator.js.map +1 -1
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/__tests__/e2e-owner-review.test.ts +778 -0
- package/packages/sdk/src/__tests__/workflow-runner.test.ts +484 -9
- package/packages/sdk/src/workflows/README.md +11 -0
- package/packages/sdk/src/workflows/cli.ts +10 -0
- package/packages/sdk/src/workflows/runner.ts +706 -33
- package/packages/sdk/src/workflows/trajectory.ts +89 -8
- package/packages/sdk/src/workflows/validator.ts +29 -0
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
|
@@ -72,7 +72,22 @@ export type WorkflowEvent =
|
|
|
72
72
|
| { type: 'run:failed'; runId: string; error: string }
|
|
73
73
|
| { type: 'run:cancelled'; runId: string }
|
|
74
74
|
| { type: 'step:started'; runId: string; stepName: string }
|
|
75
|
+
| {
|
|
76
|
+
type: 'step:owner-assigned';
|
|
77
|
+
runId: string;
|
|
78
|
+
stepName: string;
|
|
79
|
+
ownerName: string;
|
|
80
|
+
specialistName: string;
|
|
81
|
+
}
|
|
75
82
|
| { type: 'step:completed'; runId: string; stepName: string; output?: string }
|
|
83
|
+
| {
|
|
84
|
+
type: 'step:review-completed';
|
|
85
|
+
runId: string;
|
|
86
|
+
stepName: string;
|
|
87
|
+
reviewerName: string;
|
|
88
|
+
decision: 'approved' | 'rejected';
|
|
89
|
+
}
|
|
90
|
+
| { type: 'step:owner-timeout'; runId: string; stepName: string; ownerName: string }
|
|
76
91
|
| { type: 'step:failed'; runId: string; stepName: string; error: string }
|
|
77
92
|
| { type: 'step:skipped'; runId: string; stepName: string }
|
|
78
93
|
| { type: 'step:retrying'; runId: string; stepName: string; attempt: number }
|
|
@@ -127,6 +142,30 @@ interface StepState {
|
|
|
127
142
|
agent?: Agent;
|
|
128
143
|
}
|
|
129
144
|
|
|
145
|
+
interface SupervisedStep {
|
|
146
|
+
specialist: AgentDefinition;
|
|
147
|
+
owner: AgentDefinition;
|
|
148
|
+
reviewer?: AgentDefinition;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface SpawnedAgentInfo {
|
|
152
|
+
requestedName: string;
|
|
153
|
+
actualName: string;
|
|
154
|
+
agent: Agent;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
interface SpawnAndWaitOptions {
|
|
158
|
+
agentNameSuffix?: string;
|
|
159
|
+
onSpawned?: (info: SpawnedAgentInfo) => void | Promise<void>;
|
|
160
|
+
onChunk?: (info: { agentName: string; chunk: string }) => void;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
interface SupervisedRuntimeAgent {
|
|
164
|
+
stepName: string;
|
|
165
|
+
role: 'owner' | 'specialist';
|
|
166
|
+
logicalName: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
130
169
|
// ── CLI resolution ───────────────────────────────────────────────────────────
|
|
131
170
|
|
|
132
171
|
/**
|
|
@@ -203,6 +242,8 @@ export class WorkflowRunner {
|
|
|
203
242
|
private readonly lastIdleLog = new Map<string, number>();
|
|
204
243
|
/** Tracks last logged activity type per agent to avoid duplicate status lines. */
|
|
205
244
|
private readonly lastActivity = new Map<string, string>();
|
|
245
|
+
/** Runtime-name lookup for agents participating in supervised owner flows. */
|
|
246
|
+
private readonly supervisedRuntimeAgents = new Map<string, SupervisedRuntimeAgent>();
|
|
206
247
|
/** Resolved named paths from the top-level `paths` config, keyed by name → absolute directory. */
|
|
207
248
|
private resolvedPaths = new Map<string, string>();
|
|
208
249
|
|
|
@@ -1375,6 +1416,16 @@ export class WorkflowRunner {
|
|
|
1375
1416
|
const fromShort = msg.from.replace(/-[a-f0-9]{6,}$/, '');
|
|
1376
1417
|
const toShort = msg.to.replace(/-[a-f0-9]{6,}$/, '');
|
|
1377
1418
|
this.log(`[msg] ${fromShort} → ${toShort}: ${body}`);
|
|
1419
|
+
|
|
1420
|
+
const supervision = this.supervisedRuntimeAgents.get(msg.from);
|
|
1421
|
+
if (supervision?.role === 'owner') {
|
|
1422
|
+
void this.trajectory?.ownerMonitoringEvent(
|
|
1423
|
+
supervision.stepName,
|
|
1424
|
+
supervision.logicalName,
|
|
1425
|
+
`Messaged ${msg.to}: ${msg.text.slice(0, 120)}`,
|
|
1426
|
+
{ to: msg.to, text: msg.text }
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1378
1429
|
};
|
|
1379
1430
|
|
|
1380
1431
|
this.relay.onAgentSpawned = (agent) => {
|
|
@@ -1504,6 +1555,7 @@ export class WorkflowRunner {
|
|
|
1504
1555
|
}
|
|
1505
1556
|
this.lastIdleLog.clear();
|
|
1506
1557
|
this.lastActivity.clear();
|
|
1558
|
+
this.supervisedRuntimeAgents.clear();
|
|
1507
1559
|
|
|
1508
1560
|
this.log('Shutting down broker...');
|
|
1509
1561
|
await this.relay?.shutdown();
|
|
@@ -2209,12 +2261,29 @@ export class WorkflowRunner {
|
|
|
2209
2261
|
if (!rawAgentDef) {
|
|
2210
2262
|
throw new Error(`Agent "${agentName}" not found in config`);
|
|
2211
2263
|
}
|
|
2212
|
-
const
|
|
2213
|
-
|
|
2214
|
-
const
|
|
2264
|
+
const specialistDef = WorkflowRunner.resolveAgentDef(rawAgentDef);
|
|
2265
|
+
const usesOwnerFlow = specialistDef.interactive !== false;
|
|
2266
|
+
const ownerDef = usesOwnerFlow ? this.resolveAutoStepOwner(specialistDef, agentMap) : specialistDef;
|
|
2267
|
+
const reviewDef = usesOwnerFlow ? this.resolveAutoReviewAgent(ownerDef, agentMap) : undefined;
|
|
2268
|
+
const supervised: SupervisedStep = {
|
|
2269
|
+
specialist: specialistDef,
|
|
2270
|
+
owner: ownerDef,
|
|
2271
|
+
reviewer: reviewDef,
|
|
2272
|
+
};
|
|
2273
|
+
const usesDedicatedOwner = usesOwnerFlow && ownerDef.name !== specialistDef.name;
|
|
2274
|
+
|
|
2275
|
+
const maxRetries =
|
|
2276
|
+
step.retries ??
|
|
2277
|
+
ownerDef.constraints?.retries ??
|
|
2278
|
+
specialistDef.constraints?.retries ??
|
|
2279
|
+
errorHandling?.maxRetries ??
|
|
2280
|
+
0;
|
|
2215
2281
|
const retryDelay = errorHandling?.retryDelayMs ?? 1000;
|
|
2216
2282
|
const timeoutMs =
|
|
2217
|
-
step.timeoutMs ??
|
|
2283
|
+
step.timeoutMs ??
|
|
2284
|
+
ownerDef.constraints?.timeoutMs ??
|
|
2285
|
+
specialistDef.constraints?.timeoutMs ??
|
|
2286
|
+
this.currentConfig?.swarm?.timeoutMs;
|
|
2218
2287
|
|
|
2219
2288
|
let lastError: string | undefined;
|
|
2220
2289
|
|
|
@@ -2243,8 +2312,25 @@ export class WorkflowRunner {
|
|
|
2243
2312
|
updatedAt: new Date().toISOString(),
|
|
2244
2313
|
});
|
|
2245
2314
|
this.emit({ type: 'step:started', runId, stepName: step.name });
|
|
2246
|
-
this.postToChannel(
|
|
2247
|
-
|
|
2315
|
+
this.postToChannel(
|
|
2316
|
+
`**[${step.name}]** Started (owner: ${ownerDef.name}, specialist: ${specialistDef.name})`
|
|
2317
|
+
);
|
|
2318
|
+
await this.trajectory?.stepStarted(step, ownerDef.name, {
|
|
2319
|
+
role: usesDedicatedOwner ? 'owner' : 'specialist',
|
|
2320
|
+
owner: ownerDef.name,
|
|
2321
|
+
specialist: specialistDef.name,
|
|
2322
|
+
reviewer: reviewDef?.name,
|
|
2323
|
+
});
|
|
2324
|
+
if (usesDedicatedOwner) {
|
|
2325
|
+
await this.trajectory?.stepSupervisionAssigned(step, supervised);
|
|
2326
|
+
}
|
|
2327
|
+
this.emit({
|
|
2328
|
+
type: 'step:owner-assigned',
|
|
2329
|
+
runId,
|
|
2330
|
+
stepName: step.name,
|
|
2331
|
+
ownerName: ownerDef.name,
|
|
2332
|
+
specialistName: specialistDef.name,
|
|
2333
|
+
});
|
|
2248
2334
|
|
|
2249
2335
|
// Resolve step-output variables (e.g. {{steps.plan.output}}) at execution time
|
|
2250
2336
|
const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
|
|
@@ -2252,60 +2338,110 @@ export class WorkflowRunner {
|
|
|
2252
2338
|
|
|
2253
2339
|
// If this is an interactive agent, append awareness of non-interactive workers
|
|
2254
2340
|
// so the lead knows not to message them and to use step output chaining instead
|
|
2255
|
-
if (
|
|
2341
|
+
if (specialistDef.interactive !== false || ownerDef.interactive !== false) {
|
|
2256
2342
|
const nonInteractiveInfo = this.buildNonInteractiveAwareness(agentMap, stepStates);
|
|
2257
2343
|
if (nonInteractiveInfo) {
|
|
2258
2344
|
resolvedTask += nonInteractiveInfo;
|
|
2259
2345
|
}
|
|
2260
2346
|
}
|
|
2261
2347
|
|
|
2262
|
-
// Apply step-level workdir override to agent
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2348
|
+
// Apply step-level workdir override to agent definitions if present
|
|
2349
|
+
const applyStepWorkdir = (def: AgentDefinition): AgentDefinition => {
|
|
2350
|
+
if (step.workdir) {
|
|
2351
|
+
const stepWorkdir = this.resolveStepWorkdir(step);
|
|
2352
|
+
if (stepWorkdir) {
|
|
2353
|
+
return { ...def, cwd: stepWorkdir, workdir: undefined };
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
return def;
|
|
2357
|
+
};
|
|
2358
|
+
const effectiveSpecialist = applyStepWorkdir(specialistDef);
|
|
2359
|
+
const effectiveOwner = applyStepWorkdir(ownerDef);
|
|
2360
|
+
|
|
2361
|
+
let specialistOutput: string;
|
|
2362
|
+
let ownerOutput: string;
|
|
2363
|
+
let ownerElapsed: number;
|
|
2364
|
+
|
|
2365
|
+
if (usesDedicatedOwner) {
|
|
2366
|
+
const result = await this.executeSupervisedAgentStep(
|
|
2367
|
+
step,
|
|
2368
|
+
{ specialist: effectiveSpecialist, owner: effectiveOwner, reviewer: reviewDef },
|
|
2369
|
+
resolvedTask,
|
|
2370
|
+
timeoutMs
|
|
2371
|
+
);
|
|
2372
|
+
specialistOutput = result.specialistOutput;
|
|
2373
|
+
ownerOutput = result.ownerOutput;
|
|
2374
|
+
ownerElapsed = result.ownerElapsed;
|
|
2375
|
+
} else {
|
|
2376
|
+
const ownerTask = this.injectStepOwnerContract(step, resolvedTask, effectiveOwner, effectiveSpecialist);
|
|
2377
|
+
|
|
2378
|
+
this.log(`[${step.name}] Spawning owner "${effectiveOwner.name}" (cli: ${effectiveOwner.cli})${step.workdir ? ` [workdir: ${step.workdir}]` : ''}`);
|
|
2379
|
+
const resolvedStep = { ...step, task: ownerTask };
|
|
2380
|
+
const ownerStartTime = Date.now();
|
|
2381
|
+
const output = this.executor
|
|
2382
|
+
? await this.executor.executeAgentStep(resolvedStep, effectiveOwner, ownerTask, timeoutMs)
|
|
2383
|
+
: await this.spawnAndWait(effectiveOwner, resolvedStep, timeoutMs);
|
|
2384
|
+
ownerElapsed = Date.now() - ownerStartTime;
|
|
2385
|
+
this.log(`[${step.name}] Owner "${effectiveOwner.name}" exited`);
|
|
2386
|
+
if (usesOwnerFlow) {
|
|
2387
|
+
this.assertOwnerCompletionMarker(step, output, ownerTask);
|
|
2268
2388
|
}
|
|
2389
|
+
specialistOutput = output;
|
|
2390
|
+
ownerOutput = output;
|
|
2269
2391
|
}
|
|
2270
2392
|
|
|
2271
|
-
// Spawn agent via AgentRelay
|
|
2272
|
-
this.log(`[${step.name}] Spawning agent "${effectiveAgentDef.name}" (cli: ${effectiveAgentDef.cli})${step.workdir ? ` [workdir: ${step.workdir}]` : ''}`);
|
|
2273
|
-
const resolvedStep = { ...step, task: resolvedTask };
|
|
2274
|
-
const output = this.executor
|
|
2275
|
-
? await this.executor.executeAgentStep(resolvedStep, effectiveAgentDef, resolvedTask, timeoutMs)
|
|
2276
|
-
: await this.spawnAndWait(effectiveAgentDef, resolvedStep, timeoutMs);
|
|
2277
|
-
this.log(`[${step.name}] Agent "${agentDef.name}" exited`);
|
|
2278
|
-
|
|
2279
2393
|
// Run verification if configured
|
|
2280
2394
|
if (step.verification) {
|
|
2281
|
-
this.runVerification(step.verification,
|
|
2395
|
+
this.runVerification(step.verification, specialistOutput, step.name, resolvedTask);
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
// Every interactive step gets a review pass; pick a dedicated reviewer when available.
|
|
2399
|
+
let combinedOutput = specialistOutput;
|
|
2400
|
+
if (usesOwnerFlow && reviewDef) {
|
|
2401
|
+
const remainingMs = timeoutMs ? Math.max(0, timeoutMs - ownerElapsed) : undefined;
|
|
2402
|
+
const reviewOutput = await this.runStepReviewGate(
|
|
2403
|
+
step,
|
|
2404
|
+
resolvedTask,
|
|
2405
|
+
specialistOutput,
|
|
2406
|
+
ownerOutput,
|
|
2407
|
+
ownerDef,
|
|
2408
|
+
reviewDef,
|
|
2409
|
+
remainingMs
|
|
2410
|
+
);
|
|
2411
|
+
combinedOutput = this.combineStepAndReviewOutput(specialistOutput, reviewOutput);
|
|
2282
2412
|
}
|
|
2283
2413
|
|
|
2284
2414
|
// Mark completed
|
|
2285
2415
|
state.row.status = 'completed';
|
|
2286
|
-
state.row.output =
|
|
2416
|
+
state.row.output = combinedOutput;
|
|
2287
2417
|
state.row.completedAt = new Date().toISOString();
|
|
2288
2418
|
await this.db.updateStep(state.row.id, {
|
|
2289
2419
|
status: 'completed',
|
|
2290
|
-
output,
|
|
2420
|
+
output: combinedOutput,
|
|
2291
2421
|
completedAt: state.row.completedAt,
|
|
2292
2422
|
updatedAt: new Date().toISOString(),
|
|
2293
2423
|
});
|
|
2294
2424
|
|
|
2295
2425
|
// Persist step output to disk so it survives restarts and is inspectable
|
|
2296
|
-
await this.persistStepOutput(runId, step.name,
|
|
2426
|
+
await this.persistStepOutput(runId, step.name, combinedOutput);
|
|
2297
2427
|
|
|
2298
|
-
this.emit({ type: 'step:completed', runId, stepName: step.name, output });
|
|
2299
|
-
await this.trajectory?.stepCompleted(step,
|
|
2428
|
+
this.emit({ type: 'step:completed', runId, stepName: step.name, output: combinedOutput });
|
|
2429
|
+
await this.trajectory?.stepCompleted(step, combinedOutput, attempt + 1);
|
|
2300
2430
|
return;
|
|
2301
2431
|
} catch (err) {
|
|
2302
2432
|
lastError = err instanceof Error ? err.message : String(err);
|
|
2433
|
+
const ownerTimedOut = usesDedicatedOwner
|
|
2434
|
+
? /\bowner timed out\b/i.test(lastError)
|
|
2435
|
+
: /\btimed out\b/i.test(lastError) && !lastError.includes(`${step.name}-review`);
|
|
2436
|
+
if (ownerTimedOut) {
|
|
2437
|
+
this.emit({ type: 'step:owner-timeout', runId, stepName: step.name, ownerName: ownerDef.name });
|
|
2438
|
+
}
|
|
2303
2439
|
}
|
|
2304
2440
|
}
|
|
2305
2441
|
|
|
2306
2442
|
// All retries exhausted — record root-cause diagnosis and mark failed
|
|
2307
2443
|
const nonInteractive =
|
|
2308
|
-
|
|
2444
|
+
ownerDef.interactive === false || ['worker', 'reviewer', 'analyst'].includes(ownerDef.preset ?? '');
|
|
2309
2445
|
const verificationValue =
|
|
2310
2446
|
typeof step.verification === 'object' && 'value' in step.verification
|
|
2311
2447
|
? String(step.verification.value)
|
|
@@ -2322,6 +2458,535 @@ export class WorkflowRunner {
|
|
|
2322
2458
|
);
|
|
2323
2459
|
}
|
|
2324
2460
|
|
|
2461
|
+
private injectStepOwnerContract(
|
|
2462
|
+
step: WorkflowStep,
|
|
2463
|
+
resolvedTask: string,
|
|
2464
|
+
ownerDef: AgentDefinition,
|
|
2465
|
+
specialistDef: AgentDefinition
|
|
2466
|
+
): string {
|
|
2467
|
+
if (ownerDef.interactive === false) return resolvedTask;
|
|
2468
|
+
const specialistNote =
|
|
2469
|
+
ownerDef.name === specialistDef.name
|
|
2470
|
+
? ''
|
|
2471
|
+
: `Specialist intended for this step: "${specialistDef.name}" (${specialistDef.role ?? specialistDef.cli}).`;
|
|
2472
|
+
return (
|
|
2473
|
+
resolvedTask +
|
|
2474
|
+
'\n\n---\n' +
|
|
2475
|
+
`STEP OWNER CONTRACT:\n` +
|
|
2476
|
+
`- You are the accountable owner for step "${step.name}".\n` +
|
|
2477
|
+
(specialistNote ? `- ${specialistNote}\n` : '') +
|
|
2478
|
+
`- If you delegate, you must still verify completion yourself.\n` +
|
|
2479
|
+
`- Before exiting, provide an explicit completion line: STEP_COMPLETE:${step.name}\n` +
|
|
2480
|
+
`- Then self-terminate immediately with /exit.`
|
|
2481
|
+
);
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
private buildOwnerSupervisorTask(
|
|
2485
|
+
step: WorkflowStep,
|
|
2486
|
+
originalTask: string,
|
|
2487
|
+
supervised: SupervisedStep,
|
|
2488
|
+
workerRuntimeName: string
|
|
2489
|
+
): string {
|
|
2490
|
+
const verificationGuide = this.buildSupervisorVerificationGuide(step.verification);
|
|
2491
|
+
const channelLine = this.channel ? `#${this.channel}` : '(workflow channel unavailable)';
|
|
2492
|
+
return (
|
|
2493
|
+
`You are the step owner/supervisor for step "${step.name}".\n\n` +
|
|
2494
|
+
`Worker: ${supervised.specialist.name} (runtime: ${workerRuntimeName}) on ${channelLine}\n` +
|
|
2495
|
+
`Task: ${originalTask}\n\n` +
|
|
2496
|
+
`Your job: Monitor the worker and determine when the task is complete.\n\n` +
|
|
2497
|
+
`How to verify completion:\n` +
|
|
2498
|
+
`- Watch ${channelLine} for the worker's progress messages and mirrored PTY output\n` +
|
|
2499
|
+
`- Check file changes: run \`git diff --stat\` or inspect expected files directly\n` +
|
|
2500
|
+
`- Ask the worker directly on ${channelLine} if you need a status update\n` +
|
|
2501
|
+
verificationGuide +
|
|
2502
|
+
`\nWhen you're satisfied the work is done correctly:\n` +
|
|
2503
|
+
`Output exactly: STEP_COMPLETE:${step.name}`
|
|
2504
|
+
);
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
private buildSupervisorVerificationGuide(verification?: VerificationCheck): string {
|
|
2508
|
+
if (!verification) return '';
|
|
2509
|
+
switch (verification.type) {
|
|
2510
|
+
case 'output_contains':
|
|
2511
|
+
return `- Verification gate: confirm the worker output contains ${JSON.stringify(verification.value)}\n`;
|
|
2512
|
+
case 'file_exists':
|
|
2513
|
+
return `- Verification gate: confirm the file exists at ${JSON.stringify(verification.value)}\n`;
|
|
2514
|
+
case 'exit_code':
|
|
2515
|
+
return `- Verification gate: confirm the worker exits with code ${JSON.stringify(verification.value)}\n`;
|
|
2516
|
+
case 'custom':
|
|
2517
|
+
return `- Verification gate: apply the custom verification rule ${JSON.stringify(verification.value)}\n`;
|
|
2518
|
+
default:
|
|
2519
|
+
return '';
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
private async executeSupervisedAgentStep(
|
|
2524
|
+
step: WorkflowStep,
|
|
2525
|
+
supervised: SupervisedStep,
|
|
2526
|
+
resolvedTask: string,
|
|
2527
|
+
timeoutMs?: number
|
|
2528
|
+
): Promise<{ specialistOutput: string; ownerOutput: string; ownerElapsed: number }> {
|
|
2529
|
+
if (this.executor) {
|
|
2530
|
+
const supervisorTask = this.buildOwnerSupervisorTask(
|
|
2531
|
+
step,
|
|
2532
|
+
resolvedTask,
|
|
2533
|
+
supervised,
|
|
2534
|
+
supervised.specialist.name
|
|
2535
|
+
);
|
|
2536
|
+
const specialistStep = { ...step, task: resolvedTask };
|
|
2537
|
+
const ownerStep: WorkflowStep = {
|
|
2538
|
+
...step,
|
|
2539
|
+
name: `${step.name}-owner`,
|
|
2540
|
+
agent: supervised.owner.name,
|
|
2541
|
+
task: supervisorTask,
|
|
2542
|
+
};
|
|
2543
|
+
|
|
2544
|
+
this.log(
|
|
2545
|
+
`[${step.name}] Spawning specialist "${supervised.specialist.name}" and owner "${supervised.owner.name}"`
|
|
2546
|
+
);
|
|
2547
|
+
const specialistPromise = this.executor.executeAgentStep(
|
|
2548
|
+
specialistStep,
|
|
2549
|
+
supervised.specialist,
|
|
2550
|
+
resolvedTask,
|
|
2551
|
+
timeoutMs
|
|
2552
|
+
);
|
|
2553
|
+
const ownerStartTime = Date.now();
|
|
2554
|
+
const ownerOutput = await this.executor.executeAgentStep(
|
|
2555
|
+
ownerStep,
|
|
2556
|
+
supervised.owner,
|
|
2557
|
+
supervisorTask,
|
|
2558
|
+
timeoutMs
|
|
2559
|
+
);
|
|
2560
|
+
const ownerElapsed = Date.now() - ownerStartTime;
|
|
2561
|
+
|
|
2562
|
+
this.assertOwnerCompletionMarker(step, ownerOutput, supervisorTask);
|
|
2563
|
+
const specialistOutput = await specialistPromise;
|
|
2564
|
+
return { specialistOutput, ownerOutput, ownerElapsed };
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
let workerHandle: Agent | undefined;
|
|
2568
|
+
let workerRuntimeName = supervised.specialist.name;
|
|
2569
|
+
let workerSpawned = false;
|
|
2570
|
+
let workerReleased = false;
|
|
2571
|
+
let resolveWorkerSpawn!: () => void;
|
|
2572
|
+
let rejectWorkerSpawn!: (error: unknown) => void;
|
|
2573
|
+
const workerReady = new Promise<void>((resolve, reject) => {
|
|
2574
|
+
resolveWorkerSpawn = resolve;
|
|
2575
|
+
rejectWorkerSpawn = reject;
|
|
2576
|
+
});
|
|
2577
|
+
|
|
2578
|
+
const specialistStep = { ...step, task: resolvedTask };
|
|
2579
|
+
this.log(
|
|
2580
|
+
`[${step.name}] Spawning specialist "${supervised.specialist.name}" (cli: ${supervised.specialist.cli})`
|
|
2581
|
+
);
|
|
2582
|
+
const workerPromise = this.spawnAndWait(supervised.specialist, specialistStep, timeoutMs, {
|
|
2583
|
+
agentNameSuffix: 'worker',
|
|
2584
|
+
onSpawned: ({ actualName, agent }) => {
|
|
2585
|
+
workerHandle = agent;
|
|
2586
|
+
workerRuntimeName = actualName;
|
|
2587
|
+
this.supervisedRuntimeAgents.set(actualName, {
|
|
2588
|
+
stepName: step.name,
|
|
2589
|
+
role: 'specialist',
|
|
2590
|
+
logicalName: supervised.specialist.name,
|
|
2591
|
+
});
|
|
2592
|
+
if (!workerSpawned) {
|
|
2593
|
+
workerSpawned = true;
|
|
2594
|
+
resolveWorkerSpawn();
|
|
2595
|
+
}
|
|
2596
|
+
},
|
|
2597
|
+
onChunk: ({ agentName, chunk }) => {
|
|
2598
|
+
this.forwardAgentChunkToChannel(step.name, 'Worker', agentName, chunk);
|
|
2599
|
+
},
|
|
2600
|
+
}).catch((error) => {
|
|
2601
|
+
if (!workerSpawned) {
|
|
2602
|
+
workerSpawned = true;
|
|
2603
|
+
rejectWorkerSpawn(error);
|
|
2604
|
+
}
|
|
2605
|
+
throw error;
|
|
2606
|
+
});
|
|
2607
|
+
|
|
2608
|
+
const workerSettled = workerPromise.catch(() => undefined);
|
|
2609
|
+
workerPromise
|
|
2610
|
+
.then((output) => {
|
|
2611
|
+
workerReleased = true;
|
|
2612
|
+
this.postToChannel(`**[${step.name}]** Worker \`${workerRuntimeName}\` exited`);
|
|
2613
|
+
if (step.verification?.type === 'output_contains' && output.includes(step.verification.value)) {
|
|
2614
|
+
this.postToChannel(
|
|
2615
|
+
`**[${step.name}]** Verification gate observed: output contains ${JSON.stringify(step.verification.value)}`
|
|
2616
|
+
);
|
|
2617
|
+
}
|
|
2618
|
+
})
|
|
2619
|
+
.catch((error) => {
|
|
2620
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2621
|
+
this.postToChannel(
|
|
2622
|
+
`**[${step.name}]** Worker \`${workerRuntimeName}\` exited with error: ${message}`
|
|
2623
|
+
);
|
|
2624
|
+
});
|
|
2625
|
+
|
|
2626
|
+
await workerReady;
|
|
2627
|
+
|
|
2628
|
+
const supervisorTask = this.buildOwnerSupervisorTask(step, resolvedTask, supervised, workerRuntimeName);
|
|
2629
|
+
const ownerStep: WorkflowStep = {
|
|
2630
|
+
...step,
|
|
2631
|
+
name: `${step.name}-owner`,
|
|
2632
|
+
agent: supervised.owner.name,
|
|
2633
|
+
task: supervisorTask,
|
|
2634
|
+
};
|
|
2635
|
+
|
|
2636
|
+
this.log(`[${step.name}] Spawning owner "${supervised.owner.name}" (cli: ${supervised.owner.cli})`);
|
|
2637
|
+
const ownerStartTime = Date.now();
|
|
2638
|
+
|
|
2639
|
+
try {
|
|
2640
|
+
const ownerOutput = await this.spawnAndWait(supervised.owner, ownerStep, timeoutMs, {
|
|
2641
|
+
agentNameSuffix: 'owner',
|
|
2642
|
+
onSpawned: ({ actualName }) => {
|
|
2643
|
+
this.supervisedRuntimeAgents.set(actualName, {
|
|
2644
|
+
stepName: step.name,
|
|
2645
|
+
role: 'owner',
|
|
2646
|
+
logicalName: supervised.owner.name,
|
|
2647
|
+
});
|
|
2648
|
+
},
|
|
2649
|
+
onChunk: ({ chunk }) => {
|
|
2650
|
+
void this.recordOwnerMonitoringChunk(step, supervised.owner, chunk);
|
|
2651
|
+
},
|
|
2652
|
+
});
|
|
2653
|
+
const ownerElapsed = Date.now() - ownerStartTime;
|
|
2654
|
+
this.log(`[${step.name}] Owner "${supervised.owner.name}" exited`);
|
|
2655
|
+
this.assertOwnerCompletionMarker(step, ownerOutput, supervisorTask);
|
|
2656
|
+
|
|
2657
|
+
const specialistOutput = await workerPromise;
|
|
2658
|
+
return { specialistOutput, ownerOutput, ownerElapsed };
|
|
2659
|
+
} catch (error) {
|
|
2660
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2661
|
+
if (!workerReleased && workerHandle) {
|
|
2662
|
+
await workerHandle.release().catch(() => undefined);
|
|
2663
|
+
}
|
|
2664
|
+
await workerSettled;
|
|
2665
|
+
if (/\btimed out\b/i.test(message)) {
|
|
2666
|
+
throw new Error(`Step "${step.name}" owner timed out after ${timeoutMs ?? 'unknown'}ms`);
|
|
2667
|
+
}
|
|
2668
|
+
throw error;
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
private forwardAgentChunkToChannel(
|
|
2673
|
+
stepName: string,
|
|
2674
|
+
roleLabel: string,
|
|
2675
|
+
agentName: string,
|
|
2676
|
+
chunk: string
|
|
2677
|
+
): void {
|
|
2678
|
+
const lines = WorkflowRunner.stripAnsi(chunk)
|
|
2679
|
+
.split('\n')
|
|
2680
|
+
.map((line) => line.trim())
|
|
2681
|
+
.filter(Boolean)
|
|
2682
|
+
.slice(0, 3);
|
|
2683
|
+
for (const line of lines) {
|
|
2684
|
+
this.postToChannel(`**[${stepName}]** ${roleLabel} \`${agentName}\`: ${line.slice(0, 280)}`);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
private async recordOwnerMonitoringChunk(
|
|
2689
|
+
step: WorkflowStep,
|
|
2690
|
+
ownerDef: AgentDefinition,
|
|
2691
|
+
chunk: string
|
|
2692
|
+
): Promise<void> {
|
|
2693
|
+
const stripped = WorkflowRunner.stripAnsi(chunk);
|
|
2694
|
+
const details: string[] = [];
|
|
2695
|
+
if (/git diff --stat/i.test(stripped)) details.push('Checked git diff stats');
|
|
2696
|
+
if (/\bls -la\b/i.test(stripped)) details.push('Listed files for verification');
|
|
2697
|
+
if (/status update\?/i.test(stripped)) details.push('Asked the worker for a status update');
|
|
2698
|
+
if (/STEP_COMPLETE:/i.test(stripped)) details.push('Declared the step complete');
|
|
2699
|
+
|
|
2700
|
+
for (const detail of details) {
|
|
2701
|
+
await this.trajectory?.ownerMonitoringEvent(step.name, ownerDef.name, detail, {
|
|
2702
|
+
output: stripped.slice(0, 240),
|
|
2703
|
+
});
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
private resolveAutoStepOwner(
|
|
2708
|
+
specialistDef: AgentDefinition,
|
|
2709
|
+
agentMap: Map<string, AgentDefinition>
|
|
2710
|
+
): AgentDefinition {
|
|
2711
|
+
if (specialistDef.interactive === false) return specialistDef;
|
|
2712
|
+
|
|
2713
|
+
const allDefs = [...agentMap.values()].map((d) => WorkflowRunner.resolveAgentDef(d));
|
|
2714
|
+
const candidates = allDefs.filter((d) => d.interactive !== false);
|
|
2715
|
+
const matchesHubRole = (text: string): boolean =>
|
|
2716
|
+
[...WorkflowRunner.HUB_ROLES].some((r) => new RegExp(`\\b${r}\\b`, 'i').test(text));
|
|
2717
|
+
const ownerish = (def: AgentDefinition): boolean => {
|
|
2718
|
+
const nameLC = def.name.toLowerCase();
|
|
2719
|
+
const roleLC = def.role?.toLowerCase() ?? '';
|
|
2720
|
+
return matchesHubRole(nameLC) || matchesHubRole(roleLC);
|
|
2721
|
+
};
|
|
2722
|
+
const ownerPriority = (def: AgentDefinition): number => {
|
|
2723
|
+
const roleLC = def.role?.toLowerCase() ?? '';
|
|
2724
|
+
const nameLC = def.name.toLowerCase();
|
|
2725
|
+
if (/\blead\b/.test(roleLC) || /\blead\b/.test(nameLC)) return 6;
|
|
2726
|
+
if (/\bcoordinator\b/.test(roleLC) || /\bcoordinator\b/.test(nameLC)) return 5;
|
|
2727
|
+
if (/\bsupervisor\b/.test(roleLC) || /\bsupervisor\b/.test(nameLC)) return 4;
|
|
2728
|
+
if (/\borchestrator\b/.test(roleLC) || /\borchestrator\b/.test(nameLC)) return 3;
|
|
2729
|
+
if (/\bhub\b/.test(roleLC) || /\bhub\b/.test(nameLC)) return 2;
|
|
2730
|
+
return ownerish(def) ? 1 : 0;
|
|
2731
|
+
};
|
|
2732
|
+
const dedicatedOwner = candidates
|
|
2733
|
+
.filter((d) => d.name !== specialistDef.name && ownerish(d))
|
|
2734
|
+
.sort((a, b) => ownerPriority(b) - ownerPriority(a) || a.name.localeCompare(b.name))[0];
|
|
2735
|
+
if (dedicatedOwner) return dedicatedOwner;
|
|
2736
|
+
return specialistDef;
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
private resolveAutoReviewAgent(
|
|
2740
|
+
ownerDef: AgentDefinition,
|
|
2741
|
+
agentMap: Map<string, AgentDefinition>
|
|
2742
|
+
): AgentDefinition {
|
|
2743
|
+
const allDefs = [...agentMap.values()].map((d) => WorkflowRunner.resolveAgentDef(d));
|
|
2744
|
+
const isReviewer = (def: AgentDefinition): boolean => {
|
|
2745
|
+
const roleLC = def.role?.toLowerCase() ?? '';
|
|
2746
|
+
const nameLC = def.name.toLowerCase();
|
|
2747
|
+
return (
|
|
2748
|
+
def.preset === 'reviewer' ||
|
|
2749
|
+
roleLC.includes('review') ||
|
|
2750
|
+
roleLC.includes('critic') ||
|
|
2751
|
+
roleLC.includes('verifier') ||
|
|
2752
|
+
roleLC.includes('qa') ||
|
|
2753
|
+
nameLC.includes('review')
|
|
2754
|
+
);
|
|
2755
|
+
};
|
|
2756
|
+
const reviewerPriority = (def: AgentDefinition): number => {
|
|
2757
|
+
if (def.preset === 'reviewer') return 5;
|
|
2758
|
+
const roleLC = def.role?.toLowerCase() ?? '';
|
|
2759
|
+
const nameLC = def.name.toLowerCase();
|
|
2760
|
+
if (roleLC.includes('review') || nameLC.includes('review')) return 4;
|
|
2761
|
+
if (roleLC.includes('verifier') || roleLC.includes('qa')) return 3;
|
|
2762
|
+
if (roleLC.includes('critic')) return 2;
|
|
2763
|
+
return isReviewer(def) ? 1 : 0;
|
|
2764
|
+
};
|
|
2765
|
+
const dedicated = allDefs
|
|
2766
|
+
.filter((d) => d.name !== ownerDef.name && isReviewer(d))
|
|
2767
|
+
.sort((a, b) => reviewerPriority(b) - reviewerPriority(a) || a.name.localeCompare(b.name))[0];
|
|
2768
|
+
if (dedicated) return dedicated;
|
|
2769
|
+
|
|
2770
|
+
const alternate = allDefs.find((d) => d.name !== ownerDef.name && d.interactive !== false);
|
|
2771
|
+
if (alternate) return alternate;
|
|
2772
|
+
|
|
2773
|
+
// Self-review fallback — log a warning since owner reviewing itself is weak.
|
|
2774
|
+
return ownerDef;
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
private assertOwnerCompletionMarker(step: WorkflowStep, output: string, injectedTaskText: string): void {
|
|
2778
|
+
const marker = `STEP_COMPLETE:${step.name}`;
|
|
2779
|
+
const taskHasMarker = injectedTaskText.includes(marker);
|
|
2780
|
+
const first = output.indexOf(marker);
|
|
2781
|
+
if (first === -1) {
|
|
2782
|
+
throw new Error(`Step "${step.name}" owner completion marker missing: "${marker}"`);
|
|
2783
|
+
}
|
|
2784
|
+
// PTY output includes injected task text, so require a second marker occurrence
|
|
2785
|
+
// when the marker was present in the injected prompt (either owner contract or supervisor prompt).
|
|
2786
|
+
const outputLikelyContainsInjectedPrompt =
|
|
2787
|
+
output.includes('STEP OWNER CONTRACT') || output.includes('Output exactly: STEP_COMPLETE:');
|
|
2788
|
+
if (taskHasMarker && outputLikelyContainsInjectedPrompt) {
|
|
2789
|
+
const hasSecond = output.includes(marker, first + marker.length);
|
|
2790
|
+
if (!hasSecond) {
|
|
2791
|
+
throw new Error(`Step "${step.name}" owner completion marker missing in agent response: "${marker}"`);
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
private async runStepReviewGate(
|
|
2797
|
+
step: WorkflowStep,
|
|
2798
|
+
resolvedTask: string,
|
|
2799
|
+
specialistOutput: string,
|
|
2800
|
+
ownerOutput: string,
|
|
2801
|
+
ownerDef: AgentDefinition,
|
|
2802
|
+
reviewerDef: AgentDefinition,
|
|
2803
|
+
timeoutMs?: number
|
|
2804
|
+
): Promise<string> {
|
|
2805
|
+
const reviewSnippetMax = 12_000;
|
|
2806
|
+
let specialistSnippet = specialistOutput;
|
|
2807
|
+
if (specialistOutput.length > reviewSnippetMax) {
|
|
2808
|
+
const head = Math.floor(reviewSnippetMax / 2);
|
|
2809
|
+
const tail = reviewSnippetMax - head;
|
|
2810
|
+
const omitted = specialistOutput.length - head - tail;
|
|
2811
|
+
specialistSnippet =
|
|
2812
|
+
`${specialistOutput.slice(0, head)}\n` +
|
|
2813
|
+
`...[truncated ${omitted} chars for review]...\n` +
|
|
2814
|
+
`${specialistOutput.slice(specialistOutput.length - tail)}`;
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
let ownerSnippet = ownerOutput;
|
|
2818
|
+
if (ownerOutput.length > reviewSnippetMax) {
|
|
2819
|
+
const head = Math.floor(reviewSnippetMax / 2);
|
|
2820
|
+
const tail = reviewSnippetMax - head;
|
|
2821
|
+
const omitted = ownerOutput.length - head - tail;
|
|
2822
|
+
ownerSnippet =
|
|
2823
|
+
`${ownerOutput.slice(0, head)}\n` +
|
|
2824
|
+
`...[truncated ${omitted} chars for review]...\n` +
|
|
2825
|
+
`${ownerOutput.slice(ownerOutput.length - tail)}`;
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
const reviewTask =
|
|
2829
|
+
`Review workflow step "${step.name}" for completion and safe handoff.\n` +
|
|
2830
|
+
`Step owner: ${ownerDef.name}\n` +
|
|
2831
|
+
`Original objective:\n${resolvedTask}\n\n` +
|
|
2832
|
+
`Specialist output:\n${specialistSnippet}\n\n` +
|
|
2833
|
+
`Owner verification notes:\n${ownerSnippet}\n\n` +
|
|
2834
|
+
`Return exactly:\n` +
|
|
2835
|
+
`REVIEW_DECISION: APPROVE or REJECT\n` +
|
|
2836
|
+
`REVIEW_REASON: <one sentence>\n` +
|
|
2837
|
+
`Then output /exit.`;
|
|
2838
|
+
|
|
2839
|
+
const safetyTimeoutMs = timeoutMs ?? 600_000;
|
|
2840
|
+
const reviewStep: WorkflowStep = {
|
|
2841
|
+
name: `${step.name}-review`,
|
|
2842
|
+
type: 'agent',
|
|
2843
|
+
agent: reviewerDef.name,
|
|
2844
|
+
task: reviewTask,
|
|
2845
|
+
};
|
|
2846
|
+
|
|
2847
|
+
await this.trajectory?.registerAgent(reviewerDef.name, 'reviewer');
|
|
2848
|
+
this.postToChannel(`**[${step.name}]** Review started (reviewer: ${reviewerDef.name})`);
|
|
2849
|
+
const emitReviewCompleted = async (decision: 'approved' | 'rejected', reason?: string) => {
|
|
2850
|
+
await this.trajectory?.reviewCompleted(step.name, reviewerDef.name, decision, reason);
|
|
2851
|
+
this.emit({
|
|
2852
|
+
type: 'step:review-completed',
|
|
2853
|
+
runId: this.currentRunId ?? '',
|
|
2854
|
+
stepName: step.name,
|
|
2855
|
+
reviewerName: reviewerDef.name,
|
|
2856
|
+
decision,
|
|
2857
|
+
});
|
|
2858
|
+
};
|
|
2859
|
+
|
|
2860
|
+
if (this.executor) {
|
|
2861
|
+
const reviewOutput = await this.executor.executeAgentStep(
|
|
2862
|
+
reviewStep,
|
|
2863
|
+
reviewerDef,
|
|
2864
|
+
reviewTask,
|
|
2865
|
+
safetyTimeoutMs
|
|
2866
|
+
);
|
|
2867
|
+
const parsed = this.parseReviewDecision(reviewOutput);
|
|
2868
|
+
if (!parsed) {
|
|
2869
|
+
throw new Error(
|
|
2870
|
+
`Step "${step.name}" review response malformed from "${reviewerDef.name}" (missing REVIEW_DECISION)`
|
|
2871
|
+
);
|
|
2872
|
+
}
|
|
2873
|
+
await emitReviewCompleted(parsed.decision, parsed.reason);
|
|
2874
|
+
if (parsed.decision === 'rejected') {
|
|
2875
|
+
throw new Error(`Step "${step.name}" review rejected by "${reviewerDef.name}"`);
|
|
2876
|
+
}
|
|
2877
|
+
this.postToChannel(`**[${step.name}]** Review approved by \`${reviewerDef.name}\``);
|
|
2878
|
+
return reviewOutput;
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
let reviewerHandle: Agent | undefined;
|
|
2882
|
+
let reviewerReleased = false;
|
|
2883
|
+
let reviewOutput = '';
|
|
2884
|
+
let completedReview:
|
|
2885
|
+
| { decision: 'approved' | 'rejected'; reason?: string }
|
|
2886
|
+
| undefined;
|
|
2887
|
+
let reviewCompletionPromise: Promise<void> | undefined;
|
|
2888
|
+
const reviewCompletionStarted = { value: false };
|
|
2889
|
+
|
|
2890
|
+
const startReviewCompletion = (parsed: { decision: 'approved' | 'rejected'; reason?: string }) => {
|
|
2891
|
+
if (reviewCompletionStarted.value) return;
|
|
2892
|
+
reviewCompletionStarted.value = true;
|
|
2893
|
+
completedReview = parsed;
|
|
2894
|
+
reviewCompletionPromise = (async () => {
|
|
2895
|
+
await emitReviewCompleted(parsed.decision, parsed.reason);
|
|
2896
|
+
if (reviewerHandle && !reviewerReleased) {
|
|
2897
|
+
reviewerReleased = true;
|
|
2898
|
+
await reviewerHandle.release().catch(() => undefined);
|
|
2899
|
+
}
|
|
2900
|
+
})();
|
|
2901
|
+
};
|
|
2902
|
+
|
|
2903
|
+
try {
|
|
2904
|
+
reviewOutput = await this.spawnAndWait(reviewerDef, reviewStep, safetyTimeoutMs, {
|
|
2905
|
+
onSpawned: ({ agent }) => {
|
|
2906
|
+
reviewerHandle = agent;
|
|
2907
|
+
},
|
|
2908
|
+
onChunk: ({ chunk }) => {
|
|
2909
|
+
const nextOutput = reviewOutput + WorkflowRunner.stripAnsi(chunk);
|
|
2910
|
+
reviewOutput = nextOutput;
|
|
2911
|
+
const parsed = this.parseReviewDecision(nextOutput);
|
|
2912
|
+
if (parsed) {
|
|
2913
|
+
startReviewCompletion(parsed);
|
|
2914
|
+
}
|
|
2915
|
+
},
|
|
2916
|
+
});
|
|
2917
|
+
await reviewCompletionPromise;
|
|
2918
|
+
} catch (error) {
|
|
2919
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2920
|
+
if (/\btimed out\b/i.test(message)) {
|
|
2921
|
+
this.log(`[${step.name}] Review safety backstop timeout fired after ${safetyTimeoutMs}ms`);
|
|
2922
|
+
throw new Error(
|
|
2923
|
+
`Step "${step.name}" review safety backstop timed out after ${safetyTimeoutMs}ms`
|
|
2924
|
+
);
|
|
2925
|
+
}
|
|
2926
|
+
throw error;
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
if (!completedReview) {
|
|
2930
|
+
const parsed = this.parseReviewDecision(reviewOutput);
|
|
2931
|
+
if (!parsed) {
|
|
2932
|
+
throw new Error(
|
|
2933
|
+
`Step "${step.name}" review response malformed from "${reviewerDef.name}" (missing REVIEW_DECISION)`
|
|
2934
|
+
);
|
|
2935
|
+
}
|
|
2936
|
+
completedReview = parsed;
|
|
2937
|
+
await emitReviewCompleted(parsed.decision, parsed.reason);
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
if (completedReview.decision === 'rejected') {
|
|
2941
|
+
throw new Error(`Step "${step.name}" review rejected by "${reviewerDef.name}"`);
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
this.postToChannel(`**[${step.name}]** Review approved by \`${reviewerDef.name}\``);
|
|
2945
|
+
return reviewOutput;
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
private parseReviewDecision(
|
|
2949
|
+
reviewOutput: string
|
|
2950
|
+
): { decision: 'approved' | 'rejected'; reason?: string } | null {
|
|
2951
|
+
const decisionPattern = /REVIEW_DECISION:\s*(APPROVE|REJECT)/gi;
|
|
2952
|
+
const decisionMatches = [...reviewOutput.matchAll(decisionPattern)];
|
|
2953
|
+
if (decisionMatches.length === 0) {
|
|
2954
|
+
return null;
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
const outputLikelyContainsEchoedPrompt =
|
|
2958
|
+
reviewOutput.includes('Return exactly') || reviewOutput.includes('REVIEW_DECISION: APPROVE or REJECT');
|
|
2959
|
+
const decisionMatch =
|
|
2960
|
+
outputLikelyContainsEchoedPrompt && decisionMatches.length > 1
|
|
2961
|
+
? decisionMatches[decisionMatches.length - 1]
|
|
2962
|
+
: decisionMatches[0];
|
|
2963
|
+
const decision = decisionMatch?.[1]?.toUpperCase();
|
|
2964
|
+
if (decision !== 'APPROVE' && decision !== 'REJECT') {
|
|
2965
|
+
return null;
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
const reasonPattern = /REVIEW_REASON:\s*(.+)/gi;
|
|
2969
|
+
const reasonMatches = [...reviewOutput.matchAll(reasonPattern)];
|
|
2970
|
+
const reasonMatch =
|
|
2971
|
+
outputLikelyContainsEchoedPrompt && reasonMatches.length > 1
|
|
2972
|
+
? reasonMatches[reasonMatches.length - 1]
|
|
2973
|
+
: reasonMatches[0];
|
|
2974
|
+
const reason = reasonMatch?.[1]?.trim();
|
|
2975
|
+
|
|
2976
|
+
return {
|
|
2977
|
+
decision: decision === 'APPROVE' ? 'approved' : 'rejected',
|
|
2978
|
+
reason: reason && reason !== '<one sentence>' ? reason : undefined,
|
|
2979
|
+
};
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
private combineStepAndReviewOutput(stepOutput: string, reviewOutput: string): string {
|
|
2983
|
+
const primary = stepOutput.trimEnd();
|
|
2984
|
+
const review = reviewOutput.trim();
|
|
2985
|
+
if (!review) return primary;
|
|
2986
|
+
if (!primary) return `REVIEW_OUTPUT\n${review}\n`;
|
|
2987
|
+
return `${primary}\n\n---\nREVIEW_OUTPUT\n${review}\n`;
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2325
2990
|
/**
|
|
2326
2991
|
* Build the CLI command and arguments for a non-interactive agent execution.
|
|
2327
2992
|
* Each CLI has a specific flag for one-shot prompt mode.
|
|
@@ -2596,7 +3261,8 @@ export class WorkflowRunner {
|
|
|
2596
3261
|
private async spawnAndWait(
|
|
2597
3262
|
agentDef: AgentDefinition,
|
|
2598
3263
|
step: WorkflowStep,
|
|
2599
|
-
timeoutMs?: number
|
|
3264
|
+
timeoutMs?: number,
|
|
3265
|
+
options: SpawnAndWaitOptions = {}
|
|
2600
3266
|
): Promise<string> {
|
|
2601
3267
|
// Branch: non-interactive agents run as simple subprocesses
|
|
2602
3268
|
if (agentDef.interactive === false) {
|
|
@@ -2607,15 +3273,17 @@ export class WorkflowRunner {
|
|
|
2607
3273
|
throw new Error('AgentRelay not initialized');
|
|
2608
3274
|
}
|
|
2609
3275
|
|
|
2610
|
-
// Deterministic name: step name + first 8 chars of run ID.
|
|
2611
|
-
|
|
3276
|
+
// Deterministic name: step name + optional role suffix + first 8 chars of run ID.
|
|
3277
|
+
const requestedName = `${step.name}${options.agentNameSuffix ? `-${options.agentNameSuffix}` : ''}-${(this.currentRunId ?? this.generateShortId()).slice(0, 8)}`;
|
|
3278
|
+
let agentName = requestedName;
|
|
2612
3279
|
|
|
2613
3280
|
// Only inject delegation guidance for lead/coordinator agents, not spokes/workers.
|
|
2614
3281
|
// In non-hub patterns (pipeline, dag, etc.) every agent is autonomous so they all get it.
|
|
2615
3282
|
const role = agentDef.role?.toLowerCase() ?? '';
|
|
2616
3283
|
const nameLC = agentDef.name.toLowerCase();
|
|
2617
3284
|
const isHub =
|
|
2618
|
-
WorkflowRunner.HUB_ROLES.has(nameLC) ||
|
|
3285
|
+
WorkflowRunner.HUB_ROLES.has(nameLC) ||
|
|
3286
|
+
[...WorkflowRunner.HUB_ROLES].some((r) => new RegExp(`\\b${r}\\b`).test(role));
|
|
2619
3287
|
const pattern = this.currentConfig?.swarm.pattern;
|
|
2620
3288
|
const isHubPattern = pattern && WorkflowRunner.HUB_PATTERNS.has(pattern);
|
|
2621
3289
|
const delegationGuidance =
|
|
@@ -2651,6 +3319,7 @@ export class WorkflowRunner {
|
|
|
2651
3319
|
// Write raw output (with ANSI codes) to log file so dashboard's
|
|
2652
3320
|
// XTermLogViewer can render colors/formatting natively via xterm.js
|
|
2653
3321
|
logStream.write(chunk);
|
|
3322
|
+
options.onChunk?.({ agentName, chunk });
|
|
2654
3323
|
});
|
|
2655
3324
|
|
|
2656
3325
|
const agentChannels = this.channel ? [this.channel] : agentDef.channels;
|
|
@@ -2705,12 +3374,15 @@ export class WorkflowRunner {
|
|
|
2705
3374
|
const stripped = WorkflowRunner.stripAnsi(chunk);
|
|
2706
3375
|
this.ptyOutputBuffers.get(agent.name)?.push(stripped);
|
|
2707
3376
|
newLogStream.write(chunk);
|
|
3377
|
+
options.onChunk?.({ agentName: agent.name, chunk });
|
|
2708
3378
|
});
|
|
2709
3379
|
}
|
|
2710
3380
|
|
|
2711
3381
|
agentName = agent.name;
|
|
2712
3382
|
}
|
|
2713
3383
|
|
|
3384
|
+
await options.onSpawned?.({ requestedName, actualName: agent.name, agent });
|
|
3385
|
+
|
|
2714
3386
|
// Register in workers.json so `agents:kill` can find this agent
|
|
2715
3387
|
let workerPid: number | undefined;
|
|
2716
3388
|
try {
|
|
@@ -2791,6 +3463,7 @@ export class WorkflowRunner {
|
|
|
2791
3463
|
this.ptyLogStreams.delete(agentName);
|
|
2792
3464
|
}
|
|
2793
3465
|
this.unregisterWorker(agentName);
|
|
3466
|
+
this.supervisedRuntimeAgents.delete(agentName);
|
|
2794
3467
|
}
|
|
2795
3468
|
|
|
2796
3469
|
let output: string;
|
|
@@ -2975,7 +3648,7 @@ export class WorkflowRunner {
|
|
|
2975
3648
|
|
|
2976
3649
|
if (
|
|
2977
3650
|
WorkflowRunner.HUB_ROLES.has(nameLC) ||
|
|
2978
|
-
[...WorkflowRunner.HUB_ROLES].some((r) =>
|
|
3651
|
+
[...WorkflowRunner.HUB_ROLES].some((r) => new RegExp(`\\b${r}\\b`).test(role))
|
|
2979
3652
|
) {
|
|
2980
3653
|
// Found a hub candidate — check if we have a live handle
|
|
2981
3654
|
const handle = this.activeAgentHandles.get(agentDef.name);
|