agent-relay 3.1.23 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- 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 +4053 -16716
- package/dist/src/cli/commands/setup.js +1 -1
- package/dist/src/cli/commands/setup.js.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.js +11 -0
- package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
- package/dist/src/cli/lib/relaycast-mcp-command.d.ts +5 -0
- package/dist/src/cli/lib/relaycast-mcp-command.d.ts.map +1 -0
- package/dist/src/cli/lib/relaycast-mcp-command.js +13 -0
- package/dist/src/cli/lib/relaycast-mcp-command.js.map +1 -0
- package/dist/src/cli/relaycast-mcp.d.ts +39 -0
- package/dist/src/cli/relaycast-mcp.d.ts.map +1 -0
- package/dist/src/cli/relaycast-mcp.js +432 -0
- package/dist/src/cli/relaycast-mcp.js.map +1 -0
- package/package.json +9 -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 +7 -7
- package/packages/openclaw/dist/identity/files.js +5 -5
- package/packages/openclaw/dist/identity/files.js.map +1 -1
- package/packages/openclaw/dist/setup.js +4 -4
- package/packages/openclaw/package.json +2 -2
- package/packages/openclaw/skill/SKILL.md +24 -24
- package/packages/openclaw/src/identity/files.ts +5 -5
- package/packages/openclaw/src/setup.ts +4 -4
- package/packages/openclaw/templates/SOUL.md.template +5 -5
- 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/__tests__/unit.test.js +8 -0
- package/packages/sdk/dist/__tests__/unit.test.js.map +1 -1
- package/packages/sdk/dist/client.js +2 -2
- package/packages/sdk/dist/client.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/examples/ralph-loop.js +6 -6
- package/packages/sdk/dist/examples/ralph-loop.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/relay.d.ts +1 -0
- package/packages/sdk/dist/relay.d.ts.map +1 -1
- package/packages/sdk/dist/relay.js +2 -0
- package/packages/sdk/dist/relay.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts +53 -2
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +1277 -94
- 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 +4 -4
- 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__/unit.test.ts +10 -0
- package/packages/sdk/src/__tests__/workflow-runner.test.ts +113 -4
- package/packages/sdk/src/client.ts +2 -2
- package/packages/sdk/src/examples/example.ts +1 -1
- package/packages/sdk/src/examples/ralph-loop.ts +6 -6
- package/packages/sdk/src/relay-adapter.ts +4 -4
- package/packages/sdk/src/relay.ts +2 -0
- package/packages/sdk/src/workflows/README.md +43 -11
- package/packages/sdk/src/workflows/runner.ts +1759 -102
- 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 +4 -4
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/sdk-py/src/agent_relay/models.py +11 -0
- package/packages/sdk-py/src/agent_relay/relay.py +9 -6
- package/packages/sdk-py/tests/test_relay_lifecycle_hooks.py +23 -0
- 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 +31 -43
|
@@ -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');
|
|
@@ -358,6 +358,16 @@ test('waitForIdle: idle resolves before timeout', async () => {
|
|
|
358
358
|
const result = await promise;
|
|
359
359
|
assert.equal(result, 'idle');
|
|
360
360
|
});
|
|
361
|
+
// ── shorthand spawners ───────────────────────────────────────────────────────
|
|
362
|
+
|
|
363
|
+
test('AgentRelay: has shorthand spawners for major CLIs', () => {
|
|
364
|
+
const relay = new AgentRelay({ channels: ['general'] });
|
|
365
|
+
assert.ok(relay.claude, 'relay.claude should be defined');
|
|
366
|
+
assert.ok(relay.codex, 'relay.codex should be defined');
|
|
367
|
+
assert.ok(relay.gemini, 'relay.gemini should be defined');
|
|
368
|
+
assert.ok(relay.opencode, 'relay.opencode should be defined');
|
|
369
|
+
});
|
|
370
|
+
|
|
361
371
|
// ── agent.status ────────────────────────────────────────────────────────────
|
|
362
372
|
|
|
363
373
|
test('agent.status: mock agent has ready status', () => {
|
|
@@ -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');
|
|
@@ -490,8 +490,8 @@ export class AgentRelayClient {
|
|
|
490
490
|
...this.options.binaryArgs,
|
|
491
491
|
];
|
|
492
492
|
|
|
493
|
-
// Ensure the SDK bin directory (containing agent-relay-broker
|
|
494
|
-
// PATH so spawned workers can find
|
|
493
|
+
// Ensure the SDK bin directory (containing agent-relay-broker) is on
|
|
494
|
+
// PATH so spawned workers can find it without any user setup.
|
|
495
495
|
const env = { ...this.options.env };
|
|
496
496
|
if (isExplicitPath(this.options.binaryPath)) {
|
|
497
497
|
const binDir = path.dirname(path.resolve(resolvedBinary));
|
|
@@ -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__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
|
|
@@ -101,9 +101,9 @@ function architectPrompt(story: Story, progress: string): string {
|
|
|
101
101
|
``,
|
|
102
102
|
`### How to communicate`,
|
|
103
103
|
`Use the Relaycast MCP tools to post messages to #general:`,
|
|
104
|
-
`1. Call
|
|
105
|
-
`2.
|
|
106
|
-
`3. Use
|
|
104
|
+
`1. Call workspace.set_key with your RELAY_API_KEY env var`,
|
|
105
|
+
`2. Call agent.register with your name`,
|
|
106
|
+
`3. Use message.post to send to the #general channel`,
|
|
107
107
|
``,
|
|
108
108
|
story.description,
|
|
109
109
|
``,
|
|
@@ -136,9 +136,9 @@ function builderPrompt(story: Story, progress: string, reviewFeedback?: string):
|
|
|
136
136
|
``,
|
|
137
137
|
`### How to communicate`,
|
|
138
138
|
`Use the Relaycast MCP tools to post messages to #general:`,
|
|
139
|
-
`1. Call
|
|
140
|
-
`2.
|
|
141
|
-
`3. Use
|
|
139
|
+
`1. Call workspace.set_key with your RELAY_API_KEY env var`,
|
|
140
|
+
`2. Call agent.register with your name`,
|
|
141
|
+
`3. Use message.post to send to the #general channel`,
|
|
142
142
|
``,
|
|
143
143
|
story.description,
|
|
144
144
|
``,
|
|
@@ -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__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__dm_send.',
|
|
40
|
+
'- Use mcp__relaycast__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('relay_send(') || (lower.includes('ack:') && lower.includes('done:'));
|
|
45
|
+
return lower.includes('mcp__relaycast__dm_send(') || lower.includes('relay_send(') || (lower.includes('ack:') && lower.includes('done:'));
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function buildSpawnTask(
|
|
@@ -271,6 +271,7 @@ export class AgentRelay {
|
|
|
271
271
|
readonly codex: AgentSpawner;
|
|
272
272
|
readonly claude: AgentSpawner;
|
|
273
273
|
readonly gemini: AgentSpawner;
|
|
274
|
+
readonly opencode: AgentSpawner;
|
|
274
275
|
|
|
275
276
|
private readonly clientOptions: AgentRelayClientOptions;
|
|
276
277
|
private readonly defaultChannels: string[];
|
|
@@ -316,6 +317,7 @@ export class AgentRelay {
|
|
|
316
317
|
this.codex = this.createSpawner('codex', 'Codex', 'pty');
|
|
317
318
|
this.claude = this.createSpawner('claude', 'Claude', 'pty');
|
|
318
319
|
this.gemini = this.createSpawner('gemini', 'Gemini', 'pty');
|
|
320
|
+
this.opencode = this.createSpawner('opencode', 'OpenCode', 'headless');
|
|
319
321
|
}
|
|
320
322
|
|
|
321
323
|
/**
|
|
@@ -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
|
|