agent-relay 3.2.0 → 3.2.2
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-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/index.cjs +1421 -246
- package/dist/src/cli/commands/core.d.ts +1 -0
- package/dist/src/cli/commands/core.d.ts.map +1 -1
- package/dist/src/cli/commands/core.js +18 -0
- package/dist/src/cli/commands/core.js.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.js +16 -13
- package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
- package/dist/src/cli/relaycast-mcp.d.ts +4 -0
- package/dist/src/cli/relaycast-mcp.d.ts.map +1 -1
- package/dist/src/cli/relaycast-mcp.js +4 -4
- package/dist/src/cli/relaycast-mcp.js.map +1 -1
- 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/README.md +2 -2
- package/packages/openclaw/dist/identity/files.js +2 -2
- package/packages/openclaw/dist/identity/files.js.map +1 -1
- package/packages/openclaw/dist/setup.js +2 -2
- package/packages/openclaw/package.json +2 -2
- package/packages/openclaw/skill/SKILL.md +8 -8
- package/packages/openclaw/src/identity/files.ts +2 -2
- package/packages/openclaw/src/setup.ts +2 -2
- package/packages/openclaw/templates/SOUL.md.template +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts +14 -0
- package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts.map +1 -0
- package/packages/sdk/dist/__tests__/completion-pipeline.test.js +1476 -0
- package/packages/sdk/dist/__tests__/completion-pipeline.test.js.map +1 -0
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.js +2 -2
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.js.map +1 -1
- package/packages/sdk/dist/examples/example.js +1 -1
- package/packages/sdk/dist/examples/example.js.map +1 -1
- package/packages/sdk/dist/relay-adapter.js +4 -4
- package/packages/sdk/dist/relay-adapter.js.map +1 -1
- package/packages/sdk/dist/workflows/builder.d.ts +18 -3
- package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/builder.js +24 -12
- package/packages/sdk/dist/workflows/builder.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts +55 -2
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +1370 -108
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/dist/workflows/trajectory.d.ts +6 -2
- package/packages/sdk/dist/workflows/trajectory.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/trajectory.js +37 -2
- package/packages/sdk/dist/workflows/trajectory.js.map +1 -1
- package/packages/sdk/dist/workflows/types.d.ts +88 -0
- package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/types.js.map +1 -1
- package/packages/sdk/dist/workflows/validator.js +1 -1
- package/packages/sdk/dist/workflows/validator.js.map +1 -1
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/__tests__/completion-pipeline.test.ts +1820 -0
- package/packages/sdk/src/__tests__/e2e-owner-review.test.ts +2 -2
- package/packages/sdk/src/__tests__/idle-nudge.test.ts +68 -0
- package/packages/sdk/src/__tests__/workflow-runner.test.ts +113 -4
- package/packages/sdk/src/examples/example.ts +1 -1
- package/packages/sdk/src/relay-adapter.ts +4 -4
- package/packages/sdk/src/workflows/README.md +43 -11
- package/packages/sdk/src/workflows/builder.ts +38 -11
- package/packages/sdk/src/workflows/runner.ts +1860 -127
- package/packages/sdk/src/workflows/schema.json +6 -0
- package/packages/sdk/src/workflows/trajectory.ts +52 -3
- package/packages/sdk/src/workflows/types.ts +149 -0
- package/packages/sdk/src/workflows/validator.ts +1 -1
- 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
- package/relay-snippets/agent-relay-protocol.md +4 -4
- package/relay-snippets/agent-relay-snippet.md +9 -9
|
@@ -762,12 +762,12 @@ describe('PR #511 E2E: Auto Step Owner + Review Gating', () => {
|
|
|
762
762
|
// ── Scenario 9: Owner completion marker validation ─────────────────────
|
|
763
763
|
|
|
764
764
|
describe('Scenario 9: Owner completion marker', () => {
|
|
765
|
-
it('should fail when owner does not
|
|
765
|
+
it('should fail when owner does not provide a marker, decision, or evidence', async () => {
|
|
766
766
|
mockSpawnOutputs = ['The work is done but I forgot the sentinel.\n'];
|
|
767
767
|
|
|
768
768
|
const run = await runner.execute(makeConfig(), 'default');
|
|
769
769
|
expect(run.status).toBe('failed');
|
|
770
|
-
expect(run.error).toContain('owner completion
|
|
770
|
+
expect(run.error).toContain('owner completion decision missing');
|
|
771
771
|
}, 15000);
|
|
772
772
|
|
|
773
773
|
it('should succeed when owner produces correct STEP_COMPLETE:step-name', async () => {
|
|
@@ -340,6 +340,43 @@ describe('Idle Nudge Detection', () => {
|
|
|
340
340
|
expect(run.status).toBe('failed');
|
|
341
341
|
expect(run.error).toContain('timed out');
|
|
342
342
|
});
|
|
343
|
+
|
|
344
|
+
it('keeps a supervising lead alive after idle nudges are exhausted', async () => {
|
|
345
|
+
let exitCallCount = 0;
|
|
346
|
+
waitForExitFn = vi.fn().mockImplementation(() => {
|
|
347
|
+
exitCallCount++;
|
|
348
|
+
return Promise.resolve(exitCallCount < 3 ? 'timeout' : 'exited');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const config = makeConfig({
|
|
352
|
+
swarm: {
|
|
353
|
+
pattern: 'hub-spoke',
|
|
354
|
+
idleNudge: { nudgeAfterMs: 50, escalateAfterMs: 50, maxNudges: 1 },
|
|
355
|
+
channel: 'lead-supervision',
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
const agentDef = { name: 'team-lead', cli: 'claude', role: 'Lead coordinator' };
|
|
359
|
+
const step = {
|
|
360
|
+
name: 'step-1',
|
|
361
|
+
agent: 'team-lead',
|
|
362
|
+
task: 'Monitor #lead-supervision for WORKER_DONE, wait for the handoff, then exit.',
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
(runner as any).currentConfig = config;
|
|
366
|
+
expect((runner as any).shouldPreserveIdleSupervisor(agentDef, step)).toBe(true);
|
|
367
|
+
|
|
368
|
+
const result = await (runner as any).waitForExitWithIdleNudging(
|
|
369
|
+
mockAgent,
|
|
370
|
+
agentDef,
|
|
371
|
+
step,
|
|
372
|
+
500,
|
|
373
|
+
true
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
expect(result).toBe('exited');
|
|
377
|
+
expect(waitForExitFn).toHaveBeenCalledTimes(3);
|
|
378
|
+
expect(mockRelease).not.toHaveBeenCalled();
|
|
379
|
+
});
|
|
343
380
|
});
|
|
344
381
|
|
|
345
382
|
describe('Idle = done (no idleNudge config)', () => {
|
|
@@ -369,6 +406,37 @@ describe('Idle Nudge Detection', () => {
|
|
|
369
406
|
expect(mockRelease).not.toHaveBeenCalled();
|
|
370
407
|
});
|
|
371
408
|
|
|
409
|
+
it('does not treat supervisory lead idleness as completion', async () => {
|
|
410
|
+
waitForExitFn = vi.fn().mockResolvedValue('exited');
|
|
411
|
+
waitForIdleFn = vi.fn().mockResolvedValue('idle');
|
|
412
|
+
|
|
413
|
+
const config = makeConfig({
|
|
414
|
+
swarm: { pattern: 'hub-spoke', channel: 'lead-supervision' },
|
|
415
|
+
});
|
|
416
|
+
const agentDef = { name: 'team-lead', cli: 'claude', role: 'Lead coordinator' };
|
|
417
|
+
const step = {
|
|
418
|
+
name: 'step-1',
|
|
419
|
+
agent: 'team-lead',
|
|
420
|
+
task: 'Wait on #lead-supervision for WORKER_DONE before handing off.',
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
(runner as any).currentConfig = config;
|
|
424
|
+
expect((runner as any).shouldPreserveIdleSupervisor(agentDef, step)).toBe(true);
|
|
425
|
+
|
|
426
|
+
const result = await (runner as any).waitForExitWithIdleNudging(
|
|
427
|
+
mockAgent,
|
|
428
|
+
agentDef,
|
|
429
|
+
step,
|
|
430
|
+
500,
|
|
431
|
+
true
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
expect(result).toBe('exited');
|
|
435
|
+
expect(waitForExitFn).toHaveBeenCalledTimes(1);
|
|
436
|
+
expect(waitForIdleFn).not.toHaveBeenCalled();
|
|
437
|
+
expect(mockRelease).not.toHaveBeenCalled();
|
|
438
|
+
});
|
|
439
|
+
|
|
372
440
|
it('both timeout: fails step with timeout error', async () => {
|
|
373
441
|
waitForExitFn = vi.fn().mockResolvedValue('timeout');
|
|
374
442
|
waitForIdleFn = vi.fn().mockResolvedValue('timeout');
|
|
@@ -543,11 +543,11 @@ agents:
|
|
|
543
543
|
expect(run.status, run.error).toBe('completed');
|
|
544
544
|
});
|
|
545
545
|
|
|
546
|
-
it('should fail when owner response
|
|
546
|
+
it('should fail when owner response provides no decision, marker, or evidence', async () => {
|
|
547
547
|
mockSpawnOutputs = ['Owner completed work but forgot sentinel\n'];
|
|
548
548
|
const run = await runner.execute(makeConfig(), 'default');
|
|
549
549
|
expect(run.status).toBe('failed');
|
|
550
|
-
expect(run.error).toContain('owner completion
|
|
550
|
+
expect(run.error).toContain('owner completion decision missing');
|
|
551
551
|
});
|
|
552
552
|
|
|
553
553
|
it('should run specialist work in a separate process and mirror worker output to the channel', async () => {
|
|
@@ -564,8 +564,11 @@ agents:
|
|
|
564
564
|
expect(spawnCalls[0][0].name).toContain('step-1-worker');
|
|
565
565
|
expect(spawnCalls[1][0].name).toContain('step-1-owner');
|
|
566
566
|
expect(spawnCalls[0][0].task).not.toContain('STEP_COMPLETE:step-1');
|
|
567
|
+
expect(spawnCalls[0][0].task).toContain('WORKER COMPLETION CONTRACT');
|
|
568
|
+
expect(spawnCalls[0][0].task).toContain('WORKER_DONE: <brief summary>');
|
|
567
569
|
expect(spawnCalls[1][0].task).toContain('You are the step owner/supervisor for step "step-1".');
|
|
568
570
|
expect(spawnCalls[1][0].task).toContain('runtime: step-1-worker');
|
|
571
|
+
expect(spawnCalls[1][0].task).toContain('LEAD_DONE: <brief summary>');
|
|
569
572
|
|
|
570
573
|
const channelMessages = (mockRelaycastAgent.send as any).mock.calls.map(
|
|
571
574
|
([, text]: [string, string]) => text
|
|
@@ -574,6 +577,112 @@ agents:
|
|
|
574
577
|
expect(channelMessages.some((text: string) => text.includes('worker finished'))).toBe(true);
|
|
575
578
|
});
|
|
576
579
|
|
|
580
|
+
it('should apply verification fallback for self-owned interactive steps', async () => {
|
|
581
|
+
mockSpawnOutputs = [
|
|
582
|
+
'LEAD_DONE\n',
|
|
583
|
+
'REVIEW_DECISION: APPROVE\nREVIEW_REASON: verified\n',
|
|
584
|
+
];
|
|
585
|
+
|
|
586
|
+
const run = await runner.execute(
|
|
587
|
+
makeConfig({
|
|
588
|
+
agents: [{ name: 'team-lead', cli: 'claude', role: 'Lead coordinator' }],
|
|
589
|
+
workflows: [
|
|
590
|
+
{
|
|
591
|
+
name: 'default',
|
|
592
|
+
steps: [
|
|
593
|
+
{
|
|
594
|
+
name: 'lead-step',
|
|
595
|
+
agent: 'team-lead',
|
|
596
|
+
task: 'Output exactly:\nLEAD_DONE\n/exit',
|
|
597
|
+
verification: { type: 'exit_code', value: 0 },
|
|
598
|
+
},
|
|
599
|
+
],
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
}),
|
|
603
|
+
'default'
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
expect(run.status, run.error).toBe('completed');
|
|
607
|
+
const steps = await db.getStepsByRunId(run.id);
|
|
608
|
+
expect(steps[0]?.completionReason).toBe('completed_verified');
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('should keep explicit interactive workers self-owned without extra supervisor/reviewer spawns', async () => {
|
|
612
|
+
const ownerAssignments: Array<{ owner: string; specialist: string }> = [];
|
|
613
|
+
runner.on((event) => {
|
|
614
|
+
if (event.type === 'step:owner-assigned') {
|
|
615
|
+
ownerAssignments.push({ owner: event.ownerName, specialist: event.specialistName });
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
mockSpawnOutputs = ['STEP_COMPLETE:worker-step\nWORKER_DONE_LOCAL\n'];
|
|
620
|
+
|
|
621
|
+
const run = await runner.execute(
|
|
622
|
+
makeConfig({
|
|
623
|
+
agents: [
|
|
624
|
+
{ name: 'team-lead', cli: 'claude', role: 'Lead coordinator', preset: 'lead' },
|
|
625
|
+
{ name: 'relay-worker', cli: 'codex', preset: 'worker', interactive: true },
|
|
626
|
+
],
|
|
627
|
+
workflows: [
|
|
628
|
+
{
|
|
629
|
+
name: 'default',
|
|
630
|
+
steps: [
|
|
631
|
+
{
|
|
632
|
+
name: 'worker-step',
|
|
633
|
+
agent: 'relay-worker',
|
|
634
|
+
task: 'Output exactly:\nWORKER_DONE_LOCAL\n/exit',
|
|
635
|
+
verification: { type: 'output_contains', value: 'WORKER_DONE_LOCAL' },
|
|
636
|
+
},
|
|
637
|
+
],
|
|
638
|
+
},
|
|
639
|
+
],
|
|
640
|
+
}),
|
|
641
|
+
'default'
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
expect(ownerAssignments).toContainEqual({ owner: 'relay-worker', specialist: 'relay-worker' });
|
|
645
|
+
expect(run.status).toBe('failed');
|
|
646
|
+
expect(run.error).toContain('verification failed');
|
|
647
|
+
|
|
648
|
+
const spawnCalls = (mockRelayInstance.spawnPty as any).mock.calls;
|
|
649
|
+
expect(spawnCalls).toHaveLength(1);
|
|
650
|
+
expect(spawnCalls[0][0].task).toContain('STEP OWNER CONTRACT');
|
|
651
|
+
expect(spawnCalls[0][0].name).not.toContain('-owner-');
|
|
652
|
+
expect(spawnCalls[0][0].name).not.toContain('-review-');
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it('should pass canonical bypass args to interactive codex PTY spawns', async () => {
|
|
656
|
+
mockSpawnOutputs = [
|
|
657
|
+
'LEAD_DONE\n',
|
|
658
|
+
'REVIEW_DECISION: APPROVE\nREVIEW_REASON: verified\n',
|
|
659
|
+
];
|
|
660
|
+
|
|
661
|
+
const run = await runner.execute(
|
|
662
|
+
makeConfig({
|
|
663
|
+
agents: [{ name: 'lead', cli: 'codex', role: 'Lead coordinator' }],
|
|
664
|
+
workflows: [
|
|
665
|
+
{
|
|
666
|
+
name: 'default',
|
|
667
|
+
steps: [
|
|
668
|
+
{
|
|
669
|
+
name: 'lead-step',
|
|
670
|
+
agent: 'lead',
|
|
671
|
+
task: 'Output exactly:\nLEAD_DONE\n/exit',
|
|
672
|
+
verification: { type: 'exit_code', value: 0 },
|
|
673
|
+
},
|
|
674
|
+
],
|
|
675
|
+
},
|
|
676
|
+
],
|
|
677
|
+
}),
|
|
678
|
+
'default'
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
expect(run.status, run.error).toBe('completed');
|
|
682
|
+
const spawnCalls = (mockRelayInstance.spawnPty as any).mock.calls;
|
|
683
|
+
expect(spawnCalls[0][0].args).toEqual(['--dangerously-bypass-approvals-and-sandbox']);
|
|
684
|
+
});
|
|
685
|
+
|
|
577
686
|
it('should let the owner complete after checking file-based artifacts', async () => {
|
|
578
687
|
const tmpDir = mkdtempSync(path.join(os.tmpdir(), 'relay-owner-file-'));
|
|
579
688
|
const artifact = path.join(tmpDir, 'artifact.txt');
|
|
@@ -615,8 +724,8 @@ agents:
|
|
|
615
724
|
expect(stepRows[0].output).not.toContain('Worker already exited; artifacts look correct');
|
|
616
725
|
});
|
|
617
726
|
|
|
618
|
-
it('should fail
|
|
619
|
-
mockSpawnOutputs = ['STEP_COMPLETE:step-1\n', '
|
|
727
|
+
it('should fail when review response lacks any usable decision signal', async () => {
|
|
728
|
+
mockSpawnOutputs = ['STEP_COMPLETE:step-1\n', 'I need more context before deciding.\n'];
|
|
620
729
|
const run = await runner.execute(makeConfig(), 'default');
|
|
621
730
|
expect(run.status).toBe('failed');
|
|
622
731
|
expect(run.error).toContain('review response malformed');
|
|
@@ -78,7 +78,7 @@ async function main(): Promise<void> {
|
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
console.log(
|
|
81
|
-
`[${now()}] workers spawned. send kickoff via Relaycast (MCP
|
|
81
|
+
`[${now()}] workers spawned. send kickoff via Relaycast (MCP mcp__relaycast__message_dm_send) and watch events here (Ctrl+C to stop).`,
|
|
82
82
|
);
|
|
83
83
|
await new Promise<void>(() => {
|
|
84
84
|
// keep process alive while events stream
|
|
@@ -33,16 +33,16 @@ const WORKFLOW_BOOTSTRAP_TASK =
|
|
|
33
33
|
|
|
34
34
|
const WORKFLOW_CONVENTIONS = [
|
|
35
35
|
'Messaging requirements:',
|
|
36
|
-
'- When you receive `Relay message from <sender> ...`, reply using `
|
|
36
|
+
'- When you receive `Relay message from <sender> ...`, reply using `mcp__relaycast__message_dm_send(to: "<sender>", text: "...")`.',
|
|
37
37
|
'- Send `ACK: ...` when you receive a task.',
|
|
38
38
|
'- Send `DONE: ...` when the task is complete.',
|
|
39
|
-
'- Do not reply only in terminal text; send the response via
|
|
40
|
-
'- Use
|
|
39
|
+
'- Do not reply only in terminal text; send the response via mcp__relaycast__message_dm_send.',
|
|
40
|
+
'- Use mcp__relaycast__message_inbox_check() and mcp__relaycast__agent_list() when context is missing.',
|
|
41
41
|
].join('\n');
|
|
42
42
|
|
|
43
43
|
function hasWorkflowConventions(task: string): boolean {
|
|
44
44
|
const lower = task.toLowerCase();
|
|
45
|
-
return lower.includes('
|
|
45
|
+
return lower.includes('mcp__relaycast__message_dm_send(') || lower.includes('relay_send(') || (lower.includes('ack:') && lower.includes('done:'));
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function buildSpawnTask(
|
|
@@ -104,8 +104,8 @@ workflows:
|
|
|
104
104
|
agent: backend
|
|
105
105
|
task: "Build the REST API endpoints for user management"
|
|
106
106
|
verification:
|
|
107
|
-
type:
|
|
108
|
-
value: "
|
|
107
|
+
type: file_exists
|
|
108
|
+
value: "src/api/users.ts"
|
|
109
109
|
retries: 1
|
|
110
110
|
|
|
111
111
|
- name: write-tests
|
|
@@ -154,22 +154,50 @@ await runWorkflow("workflow.yaml", {
|
|
|
154
154
|
|
|
155
155
|
### Verification Checks
|
|
156
156
|
|
|
157
|
-
Each step can include a verification check
|
|
157
|
+
Each step can include a verification check. Verification is one input to the runner's **completion decision pipeline** — when verification passes, the step completes even without a sentinel marker.
|
|
158
158
|
|
|
159
159
|
| Type | Description |
|
|
160
160
|
|------|-------------|
|
|
161
|
-
| `
|
|
162
|
-
| `exit_code` | Agent must exit with the specified code |
|
|
161
|
+
| `exit_code` | Agent must exit with the specified code (preferred for code-editing steps) |
|
|
163
162
|
| `file_exists` | A file must exist at the specified path after the step |
|
|
163
|
+
| `output_contains` | Step output must contain the specified string (optional accelerator) |
|
|
164
164
|
| `custom` | No-op in the runner; handled by external callers |
|
|
165
165
|
|
|
166
166
|
```yaml
|
|
167
|
+
# Preferred — deterministic verification
|
|
168
|
+
verification:
|
|
169
|
+
type: exit_code
|
|
170
|
+
value: "0"
|
|
171
|
+
description: "Process exited successfully"
|
|
172
|
+
|
|
173
|
+
# Also valid — output_contains as an optional accelerator
|
|
167
174
|
verification:
|
|
168
175
|
type: output_contains
|
|
169
176
|
value: "IMPLEMENTATION_COMPLETE"
|
|
170
|
-
description: "Agent
|
|
177
|
+
description: "Agent confirms completion (optional fast-path)"
|
|
171
178
|
```
|
|
172
179
|
|
|
180
|
+
### Completion Decision Pipeline
|
|
181
|
+
|
|
182
|
+
The runner uses a multi-signal pipeline to decide step completion:
|
|
183
|
+
|
|
184
|
+
1. **Deterministic verification** — if a verification check passes, the step completes immediately (`completed_verified`)
|
|
185
|
+
2. **Owner decision** — the step owner can issue `OWNER_DECISION: COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL` (`completed_by_owner_decision`)
|
|
186
|
+
3. **Evidence-based completion** — channel messages, file artifacts, and exit codes are collected as evidence (`completed_by_evidence`)
|
|
187
|
+
4. **Marker fast-path** — `STEP_COMPLETE:<step-name>` still works as an accelerator but is never required
|
|
188
|
+
|
|
189
|
+
| Completion State | Meaning |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `completed_verified` | Deterministic verification passed |
|
|
192
|
+
| `completed_by_owner_decision` | Owner approved the step |
|
|
193
|
+
| `completed_by_evidence` | Evidence-based completion |
|
|
194
|
+
| `retry_requested_by_owner` | Owner requested retry |
|
|
195
|
+
| `failed_verification` | Verification explicitly failed |
|
|
196
|
+
| `failed_owner_decision` | Owner rejected the step |
|
|
197
|
+
| `failed_no_evidence` | No verification, no owner decision, no evidence |
|
|
198
|
+
|
|
199
|
+
**Review parsing is tolerant:** The runner accepts semantically equivalent outputs like "Approved", "Complete", "LGTM" — not just exact `REVIEW_DECISION: APPROVE` strings.
|
|
200
|
+
|
|
173
201
|
## Swarm Patterns
|
|
174
202
|
|
|
175
203
|
The `swarm.pattern` field controls how agents are coordinated:
|
|
@@ -642,12 +670,16 @@ The runner emits two new events for idle nudging:
|
|
|
642
670
|
|
|
643
671
|
## Automatic Step Owner and Review
|
|
644
672
|
|
|
645
|
-
For interactive agent steps, the runner
|
|
673
|
+
For interactive agent steps, the runner uses a point-person-led completion model:
|
|
674
|
+
|
|
675
|
+
1. **Elects a step owner** (prefers lead/coordinator-style agents, falls back to the step agent)
|
|
676
|
+
2. **Runs a completion decision pipeline** — checks deterministic verification first, then owner judgment, then evidence
|
|
677
|
+
3. **Owner can issue structured decisions** via `OWNER_DECISION: COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL|NEEDS_CLARIFICATION` with optional `REASON: <text>`
|
|
678
|
+
4. **Review parsing is tolerant** — accepts "Approved", "Complete", "LGTM", not just exact `REVIEW_DECISION: APPROVE`
|
|
679
|
+
5. **Markers are optional accelerators** — `STEP_COMPLETE:<step-name>` still works as a fast-path but is never required
|
|
680
|
+
6. Stores primary output plus review output in the step artifact
|
|
646
681
|
|
|
647
|
-
|
|
648
|
-
2. Requires the owner to provide an explicit completion signal (`STEP_COMPLETE:<step-name>`)
|
|
649
|
-
3. Runs a review pass before marking the step complete (prefers reviewer-style agents when present)
|
|
650
|
-
4. Stores primary output plus review output in the step artifact
|
|
682
|
+
**Evidence-based completion:** The runner collects channel messages, file artifacts, process exit codes, and coordination signals (e.g., WORKER_DONE posted in channel) as completion evidence. When sufficient evidence exists, the step completes without requiring any sentinel marker.
|
|
651
683
|
|
|
652
684
|
Deterministic and worktree steps are unchanged and do not require owner/review delegation.
|
|
653
685
|
|
|
@@ -4,6 +4,7 @@ import type { AgentRelayOptions } from '../relay.js';
|
|
|
4
4
|
import type {
|
|
5
5
|
AgentCli,
|
|
6
6
|
AgentDefinition,
|
|
7
|
+
AgentPreset,
|
|
7
8
|
DryRunReport,
|
|
8
9
|
ErrorHandlingConfig,
|
|
9
10
|
IdleNudgeConfig,
|
|
@@ -33,9 +34,12 @@ export interface AgentOptions {
|
|
|
33
34
|
/** When false, the agent runs as a non-interactive subprocess (no PTY, no relay messaging).
|
|
34
35
|
* Default: true. */
|
|
35
36
|
interactive?: boolean;
|
|
37
|
+
/** Agent preset: 'lead' (interactive PTY), 'worker' | 'reviewer' | 'analyst' (non-interactive subprocess). */
|
|
38
|
+
preset?: AgentPreset;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
/** Options for agent steps (default). */
|
|
42
|
+
export interface AgentStepOptions {
|
|
39
43
|
agent: string;
|
|
40
44
|
task: string;
|
|
41
45
|
dependsOn?: string[];
|
|
@@ -44,6 +48,20 @@ export interface StepOptions {
|
|
|
44
48
|
retries?: number;
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
/** Options for deterministic (shell command) steps. */
|
|
52
|
+
export interface DeterministicStepOptions {
|
|
53
|
+
type: 'deterministic';
|
|
54
|
+
command: string;
|
|
55
|
+
dependsOn?: string[];
|
|
56
|
+
/** Fail if command exit code is non-zero. Default: true. */
|
|
57
|
+
failOnError?: boolean;
|
|
58
|
+
/** Capture stdout as step output for downstream steps. Default: true. */
|
|
59
|
+
captureOutput?: boolean;
|
|
60
|
+
timeoutMs?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type StepOptions = AgentStepOptions | DeterministicStepOptions;
|
|
64
|
+
|
|
47
65
|
export interface ErrorOptions {
|
|
48
66
|
maxRetries?: number;
|
|
49
67
|
retryDelayMs?: number;
|
|
@@ -146,6 +164,7 @@ export class WorkflowBuilder {
|
|
|
146
164
|
if (options.role !== undefined) def.role = options.role;
|
|
147
165
|
if (options.task !== undefined) def.task = options.task;
|
|
148
166
|
if (options.channels !== undefined) def.channels = options.channels;
|
|
167
|
+
if (options.preset !== undefined) def.preset = options.preset;
|
|
149
168
|
if (options.interactive !== undefined) def.interactive = options.interactive;
|
|
150
169
|
|
|
151
170
|
if (
|
|
@@ -168,18 +187,25 @@ export class WorkflowBuilder {
|
|
|
168
187
|
return this;
|
|
169
188
|
}
|
|
170
189
|
|
|
171
|
-
/** Add a workflow step. */
|
|
190
|
+
/** Add a workflow step (agent or deterministic). */
|
|
172
191
|
step(name: string, options: StepOptions): this {
|
|
173
|
-
const step: WorkflowStep = {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
192
|
+
const step: WorkflowStep = { name };
|
|
193
|
+
|
|
194
|
+
if ('type' in options && options.type === 'deterministic') {
|
|
195
|
+
step.type = 'deterministic';
|
|
196
|
+
step.command = options.command;
|
|
197
|
+
if (options.failOnError !== undefined) step.failOnError = options.failOnError;
|
|
198
|
+
if (options.captureOutput !== undefined) step.captureOutput = options.captureOutput;
|
|
199
|
+
} else {
|
|
200
|
+
const agentOpts = options as AgentStepOptions;
|
|
201
|
+
step.agent = agentOpts.agent;
|
|
202
|
+
step.task = agentOpts.task;
|
|
203
|
+
if (agentOpts.verification !== undefined) step.verification = agentOpts.verification;
|
|
204
|
+
if (agentOpts.retries !== undefined) step.retries = agentOpts.retries;
|
|
205
|
+
}
|
|
178
206
|
|
|
179
207
|
if (options.dependsOn !== undefined) step.dependsOn = options.dependsOn;
|
|
180
|
-
if (options.verification !== undefined) step.verification = options.verification;
|
|
181
208
|
if (options.timeoutMs !== undefined) step.timeoutMs = options.timeoutMs;
|
|
182
|
-
if (options.retries !== undefined) step.retries = options.retries;
|
|
183
209
|
|
|
184
210
|
this._steps.push(step);
|
|
185
211
|
return this;
|
|
@@ -196,8 +222,9 @@ export class WorkflowBuilder {
|
|
|
196
222
|
|
|
197
223
|
/** Build and return the RelayYamlConfig object. */
|
|
198
224
|
toConfig(): RelayYamlConfig {
|
|
199
|
-
|
|
200
|
-
|
|
225
|
+
const hasAgentSteps = this._steps.some((s) => s.type !== 'deterministic' && s.type !== 'worktree');
|
|
226
|
+
if (hasAgentSteps && this._agents.length === 0) {
|
|
227
|
+
throw new Error('Workflow must have at least one agent when using agent steps');
|
|
201
228
|
}
|
|
202
229
|
if (this._steps.length === 0) {
|
|
203
230
|
throw new Error('Workflow must have at least one step');
|