agent-relay 2.3.11 → 2.3.13
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/install.sh +32 -0
- package/package.json +21 -21
- package/packages/acp-bridge/package.json +2 -2
- package/packages/bridge/package.json +7 -7
- package/packages/broker-sdk/README.md +32 -0
- package/packages/broker-sdk/dist/__tests__/unit.test.js +70 -2
- package/packages/broker-sdk/dist/__tests__/unit.test.js.map +1 -1
- package/packages/broker-sdk/dist/client.d.ts +2 -0
- package/packages/broker-sdk/dist/client.d.ts.map +1 -1
- package/packages/broker-sdk/dist/client.js +10 -0
- package/packages/broker-sdk/dist/client.js.map +1 -1
- package/packages/broker-sdk/dist/protocol.d.ts +4 -0
- package/packages/broker-sdk/dist/protocol.d.ts.map +1 -1
- package/packages/broker-sdk/dist/relay.d.ts +10 -0
- package/packages/broker-sdk/dist/relay.d.ts.map +1 -1
- package/packages/broker-sdk/dist/relay.js +53 -0
- package/packages/broker-sdk/dist/relay.js.map +1 -1
- package/packages/broker-sdk/dist/relaycast.d.ts +10 -0
- package/packages/broker-sdk/dist/relaycast.d.ts.map +1 -1
- package/packages/broker-sdk/dist/relaycast.js +40 -0
- package/packages/broker-sdk/dist/relaycast.js.map +1 -1
- package/packages/broker-sdk/dist/workflows/coordinator.d.ts +1 -0
- package/packages/broker-sdk/dist/workflows/coordinator.d.ts.map +1 -1
- package/packages/broker-sdk/dist/workflows/coordinator.js +239 -7
- package/packages/broker-sdk/dist/workflows/coordinator.js.map +1 -1
- package/packages/broker-sdk/dist/workflows/index.d.ts +1 -0
- package/packages/broker-sdk/dist/workflows/index.d.ts.map +1 -1
- package/packages/broker-sdk/dist/workflows/index.js +1 -0
- package/packages/broker-sdk/dist/workflows/index.js.map +1 -1
- package/packages/broker-sdk/dist/workflows/run.d.ts +3 -1
- package/packages/broker-sdk/dist/workflows/run.d.ts.map +1 -1
- package/packages/broker-sdk/dist/workflows/run.js +4 -0
- package/packages/broker-sdk/dist/workflows/run.js.map +1 -1
- package/packages/broker-sdk/dist/workflows/runner.d.ts +9 -0
- package/packages/broker-sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/broker-sdk/dist/workflows/runner.js +203 -14
- package/packages/broker-sdk/dist/workflows/runner.js.map +1 -1
- package/packages/broker-sdk/dist/workflows/trajectory.d.ts +80 -0
- package/packages/broker-sdk/dist/workflows/trajectory.d.ts.map +1 -0
- package/packages/broker-sdk/dist/workflows/trajectory.js +362 -0
- package/packages/broker-sdk/dist/workflows/trajectory.js.map +1 -0
- package/packages/broker-sdk/dist/workflows/types.d.ts +15 -1
- package/packages/broker-sdk/dist/workflows/types.d.ts.map +1 -1
- package/packages/broker-sdk/package.json +2 -2
- package/packages/broker-sdk/src/__tests__/swarm-coordinator.test.ts +356 -0
- package/packages/broker-sdk/src/__tests__/unit.test.ts +92 -1
- package/packages/broker-sdk/src/__tests__/workflow-trajectory.test.ts +408 -0
- package/packages/broker-sdk/src/client.ts +15 -0
- package/packages/broker-sdk/src/protocol.ts +5 -0
- package/packages/broker-sdk/src/relay.ts +59 -0
- package/packages/broker-sdk/src/relaycast.ts +42 -0
- package/packages/broker-sdk/src/workflows/README.md +64 -0
- package/packages/broker-sdk/src/workflows/coordinator.ts +246 -8
- package/packages/broker-sdk/src/workflows/index.ts +1 -0
- package/packages/broker-sdk/src/workflows/run.ts +9 -1
- package/packages/broker-sdk/src/workflows/runner.ts +249 -14
- package/packages/broker-sdk/src/workflows/schema.json +13 -1
- package/packages/broker-sdk/src/workflows/trajectory.ts +507 -0
- package/packages/broker-sdk/src/workflows/types.ts +31 -1
- package/packages/broker-sdk/tsconfig.json +1 -0
- package/packages/broker-sdk/vitest.config.ts +9 -0
- package/packages/config/package.json +2 -2
- package/packages/continuity/package.json +2 -2
- package/packages/daemon/package.json +12 -12
- package/packages/hooks/package.json +4 -4
- package/packages/mcp/package.json +5 -5
- package/packages/memory/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/protocol/package.json +1 -1
- package/packages/resiliency/package.json +1 -1
- package/packages/sdk/package.json +3 -3
- package/packages/sdk-py/src/agent_relay/builder.py +4 -0
- package/packages/sdk-py/src/agent_relay/types.py +15 -0
- package/packages/spawner/package.json +1 -1
- package/packages/state/package.json +1 -1
- package/packages/storage/package.json +2 -2
- 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 +3 -3
- package/packages/wrapper/package.json +5 -5
|
@@ -27,12 +27,14 @@ import type {
|
|
|
27
27
|
WorkflowStepRow,
|
|
28
28
|
WorkflowStepStatus,
|
|
29
29
|
} from './types.js';
|
|
30
|
+
import { WorkflowTrajectory, type StepOutcome } from './trajectory.js';
|
|
30
31
|
|
|
31
32
|
// ── AgentRelay SDK imports ──────────────────────────────────────────────────
|
|
32
33
|
|
|
33
34
|
// Import from sub-paths to avoid pulling in the full @relaycast/sdk dependency.
|
|
34
35
|
import { AgentRelay } from '../relay.js';
|
|
35
36
|
import type { Agent, AgentRelayOptions } from '../relay.js';
|
|
37
|
+
import { RelaycastApi } from '../relaycast.js';
|
|
36
38
|
|
|
37
39
|
// ── DB adapter interface ────────────────────────────────────────────────────
|
|
38
40
|
|
|
@@ -95,6 +97,9 @@ export class WorkflowRunner {
|
|
|
95
97
|
private readonly summaryDir: string;
|
|
96
98
|
|
|
97
99
|
private relay?: AgentRelay;
|
|
100
|
+
private relaycastApi?: RelaycastApi;
|
|
101
|
+
private channel?: string;
|
|
102
|
+
private trajectory?: WorkflowTrajectory;
|
|
98
103
|
private abortController?: AbortController;
|
|
99
104
|
private paused = false;
|
|
100
105
|
private pauseResolver?: () => void;
|
|
@@ -468,11 +473,19 @@ export class WorkflowRunner {
|
|
|
468
473
|
this.abortController = new AbortController();
|
|
469
474
|
this.paused = false;
|
|
470
475
|
|
|
476
|
+
// Initialize trajectory recording
|
|
477
|
+
this.trajectory = new WorkflowTrajectory(resolved.trajectories, runId, this.cwd);
|
|
478
|
+
|
|
471
479
|
try {
|
|
472
480
|
await this.updateRunStatus(runId, 'running');
|
|
473
481
|
this.emit({ type: 'run:started', runId });
|
|
474
482
|
|
|
483
|
+
// Analyze DAG for trajectory context
|
|
484
|
+
const dagInfo = this.analyzeDAG(workflow.steps);
|
|
485
|
+
await this.trajectory.start(workflow.name, workflow.steps.length, dagInfo);
|
|
486
|
+
|
|
475
487
|
const channel = resolved.swarm.channel ?? 'general';
|
|
488
|
+
this.channel = channel;
|
|
476
489
|
await this.ensureRelaycastApiKey(channel);
|
|
477
490
|
|
|
478
491
|
this.relay = new AgentRelay({
|
|
@@ -480,6 +493,14 @@ export class WorkflowRunner {
|
|
|
480
493
|
channels: [channel],
|
|
481
494
|
});
|
|
482
495
|
|
|
496
|
+
// Create the dedicated workflow channel and join it
|
|
497
|
+
this.relaycastApi = new RelaycastApi({ agentName: 'WorkflowRunner' });
|
|
498
|
+
await this.relaycastApi.createChannel(channel, workflow.description);
|
|
499
|
+
await this.relaycastApi.joinChannel(channel);
|
|
500
|
+
this.postToChannel(
|
|
501
|
+
`Workflow **${workflow.name}** started — ${workflow.steps.length} steps, pattern: ${resolved.swarm.pattern}`,
|
|
502
|
+
);
|
|
503
|
+
|
|
483
504
|
const agentMap = new Map<string, AgentDefinition>();
|
|
484
505
|
for (const agent of resolved.agents) {
|
|
485
506
|
agentMap.set(agent.name, agent);
|
|
@@ -501,11 +522,25 @@ export class WorkflowRunner {
|
|
|
501
522
|
if (allCompleted) {
|
|
502
523
|
await this.updateRunStatus(runId, 'completed');
|
|
503
524
|
this.emit({ type: 'run:completed', runId });
|
|
525
|
+
this.postToChannel(`Workflow **${workflow.name}** completed — all steps passed`);
|
|
526
|
+
|
|
527
|
+
// Complete trajectory with summary
|
|
528
|
+
const outcomes = this.collectOutcomes(stepStates, workflow.steps);
|
|
529
|
+
const summary = this.trajectory.buildRunSummary(outcomes);
|
|
530
|
+
const confidence = this.trajectory.computeConfidence(outcomes);
|
|
531
|
+
await this.trajectory.complete(summary, confidence, {
|
|
532
|
+
learnings: this.trajectory.extractLearnings(outcomes),
|
|
533
|
+
challenges: this.trajectory.extractChallenges(outcomes),
|
|
534
|
+
});
|
|
504
535
|
} else {
|
|
505
536
|
const failedStep = [...stepStates.values()].find((s) => s.row.status === 'failed');
|
|
506
537
|
const errorMsg = failedStep?.row.error ?? 'One or more steps failed';
|
|
507
538
|
await this.updateRunStatus(runId, 'failed', errorMsg);
|
|
508
539
|
this.emit({ type: 'run:failed', runId, error: errorMsg });
|
|
540
|
+
this.postToChannel(`Workflow **${workflow.name}** failed: ${errorMsg}`);
|
|
541
|
+
|
|
542
|
+
// Abandon trajectory on failure
|
|
543
|
+
await this.trajectory.abandon(errorMsg);
|
|
509
544
|
}
|
|
510
545
|
} catch (err) {
|
|
511
546
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -514,12 +549,19 @@ export class WorkflowRunner {
|
|
|
514
549
|
|
|
515
550
|
if (status === 'cancelled') {
|
|
516
551
|
this.emit({ type: 'run:cancelled', runId });
|
|
552
|
+
this.postToChannel(`Workflow cancelled`);
|
|
553
|
+
await this.trajectory.abandon('Cancelled by user');
|
|
517
554
|
} else {
|
|
518
555
|
this.emit({ type: 'run:failed', runId, error: errorMsg });
|
|
556
|
+
this.postToChannel(`Workflow failed: ${errorMsg}`);
|
|
557
|
+
await this.trajectory.abandon(errorMsg);
|
|
519
558
|
}
|
|
520
559
|
} finally {
|
|
521
560
|
await this.relay?.shutdown();
|
|
522
561
|
this.relay = undefined;
|
|
562
|
+
this.relaycastApi = undefined;
|
|
563
|
+
this.channel = undefined;
|
|
564
|
+
this.trajectory = undefined;
|
|
523
565
|
this.abortController = undefined;
|
|
524
566
|
}
|
|
525
567
|
|
|
@@ -567,10 +609,21 @@ export class WorkflowRunner {
|
|
|
567
609
|
this.abortController = new AbortController();
|
|
568
610
|
this.paused = false;
|
|
569
611
|
|
|
612
|
+
// Initialize trajectory for resumed run
|
|
613
|
+
this.trajectory = new WorkflowTrajectory(config.trajectories, runId, this.cwd);
|
|
614
|
+
|
|
570
615
|
try {
|
|
571
616
|
await this.updateRunStatus(runId, 'running');
|
|
572
617
|
|
|
618
|
+
const pendingCount = [...stepStates.values()].filter((s) => s.row.status === 'pending').length;
|
|
619
|
+
await this.trajectory.start(
|
|
620
|
+
workflow.name,
|
|
621
|
+
workflow.steps.length,
|
|
622
|
+
`Resumed run: ${pendingCount} pending steps of ${workflow.steps.length} total`,
|
|
623
|
+
);
|
|
624
|
+
|
|
573
625
|
const resumeChannel = config.swarm.channel ?? 'general';
|
|
626
|
+
this.channel = resumeChannel;
|
|
574
627
|
await this.ensureRelaycastApiKey(resumeChannel);
|
|
575
628
|
|
|
576
629
|
this.relay = new AgentRelay({
|
|
@@ -578,6 +631,14 @@ export class WorkflowRunner {
|
|
|
578
631
|
channels: [resumeChannel],
|
|
579
632
|
});
|
|
580
633
|
|
|
634
|
+
// Ensure channel exists and join it for resumed runs
|
|
635
|
+
this.relaycastApi = new RelaycastApi({ agentName: 'WorkflowRunner' });
|
|
636
|
+
await this.relaycastApi.createChannel(resumeChannel);
|
|
637
|
+
await this.relaycastApi.joinChannel(resumeChannel);
|
|
638
|
+
this.postToChannel(
|
|
639
|
+
`Workflow **${workflow.name}** resumed — ${pendingCount} pending steps`,
|
|
640
|
+
);
|
|
641
|
+
|
|
581
642
|
const agentMap = new Map<string, AgentDefinition>();
|
|
582
643
|
for (const agent of config.agents) {
|
|
583
644
|
agentMap.set(agent.name, agent);
|
|
@@ -592,19 +653,35 @@ export class WorkflowRunner {
|
|
|
592
653
|
if (allCompleted) {
|
|
593
654
|
await this.updateRunStatus(runId, 'completed');
|
|
594
655
|
this.emit({ type: 'run:completed', runId });
|
|
656
|
+
this.postToChannel(`Workflow **${workflow.name}** completed — all steps passed`);
|
|
657
|
+
|
|
658
|
+
const outcomes = this.collectOutcomes(stepStates, workflow.steps);
|
|
659
|
+
const summary = this.trajectory.buildRunSummary(outcomes);
|
|
660
|
+
const confidence = this.trajectory.computeConfidence(outcomes);
|
|
661
|
+
await this.trajectory.complete(summary, confidence, {
|
|
662
|
+
learnings: this.trajectory.extractLearnings(outcomes),
|
|
663
|
+
challenges: this.trajectory.extractChallenges(outcomes),
|
|
664
|
+
});
|
|
595
665
|
} else {
|
|
596
666
|
const failedStep = [...stepStates.values()].find((s) => s.row.status === 'failed');
|
|
597
667
|
const errorMsg = failedStep?.row.error ?? 'One or more steps failed';
|
|
598
668
|
await this.updateRunStatus(runId, 'failed', errorMsg);
|
|
599
669
|
this.emit({ type: 'run:failed', runId, error: errorMsg });
|
|
670
|
+
this.postToChannel(`Workflow **${workflow.name}** failed: ${errorMsg}`);
|
|
671
|
+
await this.trajectory.abandon(errorMsg);
|
|
600
672
|
}
|
|
601
673
|
} catch (err) {
|
|
602
674
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
603
675
|
await this.updateRunStatus(runId, 'failed', errorMsg);
|
|
604
676
|
this.emit({ type: 'run:failed', runId, error: errorMsg });
|
|
677
|
+
this.postToChannel(`Workflow failed: ${errorMsg}`);
|
|
678
|
+
await this.trajectory.abandon(errorMsg);
|
|
605
679
|
} finally {
|
|
606
680
|
await this.relay?.shutdown();
|
|
607
681
|
this.relay = undefined;
|
|
682
|
+
this.relaycastApi = undefined;
|
|
683
|
+
this.channel = undefined;
|
|
684
|
+
this.trajectory = undefined;
|
|
608
685
|
this.abortController = undefined;
|
|
609
686
|
}
|
|
610
687
|
|
|
@@ -660,23 +737,40 @@ export class WorkflowRunner {
|
|
|
660
737
|
break;
|
|
661
738
|
}
|
|
662
739
|
|
|
740
|
+
// Begin a track chapter if multiple parallel steps are starting
|
|
741
|
+
if (readySteps.length > 1 && this.trajectory) {
|
|
742
|
+
const trackNames = readySteps.map((s) => s.name).join(', ');
|
|
743
|
+
await this.trajectory.beginTrack(trackNames);
|
|
744
|
+
}
|
|
745
|
+
|
|
663
746
|
const results = await Promise.allSettled(
|
|
664
747
|
readySteps.map((step) =>
|
|
665
748
|
this.executeStep(step, stepStates, agentMap, errorHandling, runId),
|
|
666
749
|
),
|
|
667
750
|
);
|
|
668
751
|
|
|
752
|
+
// Collect outcomes from this batch for convergence reflection
|
|
753
|
+
const batchOutcomes: StepOutcome[] = [];
|
|
754
|
+
|
|
669
755
|
for (let i = 0; i < results.length; i++) {
|
|
670
756
|
const result = results[i];
|
|
671
757
|
const step = readySteps[i];
|
|
758
|
+
const state = stepStates.get(step.name);
|
|
672
759
|
|
|
673
760
|
if (result.status === 'rejected') {
|
|
674
761
|
const error = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
675
|
-
const state = stepStates.get(step.name);
|
|
676
762
|
if (state && state.row.status !== 'failed') {
|
|
677
763
|
await this.markStepFailed(state, error, runId);
|
|
678
764
|
}
|
|
679
765
|
|
|
766
|
+
batchOutcomes.push({
|
|
767
|
+
name: step.name,
|
|
768
|
+
agent: step.agent,
|
|
769
|
+
status: 'failed',
|
|
770
|
+
attempts: (state?.row.retryCount ?? 0) + 1,
|
|
771
|
+
error,
|
|
772
|
+
});
|
|
773
|
+
|
|
680
774
|
if (strategy === 'fail-fast') {
|
|
681
775
|
// Mark all pending downstream steps as skipped
|
|
682
776
|
await this.markDownstreamSkipped(step.name, workflow.steps, stepStates, runId);
|
|
@@ -686,8 +780,33 @@ export class WorkflowRunner {
|
|
|
686
780
|
if (strategy === 'continue') {
|
|
687
781
|
await this.markDownstreamSkipped(step.name, workflow.steps, stepStates, runId);
|
|
688
782
|
}
|
|
783
|
+
} else {
|
|
784
|
+
batchOutcomes.push({
|
|
785
|
+
name: step.name,
|
|
786
|
+
agent: step.agent,
|
|
787
|
+
status: state?.row.status === 'completed' ? 'completed' : 'failed',
|
|
788
|
+
attempts: (state?.row.retryCount ?? 0) + 1,
|
|
789
|
+
output: state?.row.output,
|
|
790
|
+
verificationPassed: state?.row.status === 'completed' && step.verification !== undefined,
|
|
791
|
+
});
|
|
689
792
|
}
|
|
690
793
|
}
|
|
794
|
+
|
|
795
|
+
// Reflect at convergence when a parallel batch completes
|
|
796
|
+
if (readySteps.length > 1 && this.trajectory?.shouldReflectOnConverge()) {
|
|
797
|
+
const label = readySteps.map((s) => s.name).join(' + ');
|
|
798
|
+
// Find steps that this batch unblocks
|
|
799
|
+
const completedNames = new Set(batchOutcomes.filter((o) => o.status === 'completed').map((o) => o.name));
|
|
800
|
+
const unblocked = workflow.steps
|
|
801
|
+
.filter((s) => s.dependsOn?.some((dep) => completedNames.has(dep)))
|
|
802
|
+
.filter((s) => {
|
|
803
|
+
const st = stepStates.get(s.name);
|
|
804
|
+
return st && st.row.status === 'pending';
|
|
805
|
+
})
|
|
806
|
+
.map((s) => s.name);
|
|
807
|
+
|
|
808
|
+
await this.trajectory.synthesizeAndReflect(label, batchOutcomes, unblocked.length > 0 ? unblocked : undefined);
|
|
809
|
+
}
|
|
691
810
|
}
|
|
692
811
|
}
|
|
693
812
|
|
|
@@ -733,11 +852,13 @@ export class WorkflowRunner {
|
|
|
733
852
|
|
|
734
853
|
if (attempt > 0) {
|
|
735
854
|
this.emit({ type: 'step:retrying', runId, stepName: step.name, attempt });
|
|
855
|
+
this.postToChannel(`**[${step.name}]** Retrying (attempt ${attempt + 1}/${maxRetries + 1})`);
|
|
736
856
|
state.row.retryCount = attempt;
|
|
737
857
|
await this.db.updateStep(state.row.id, {
|
|
738
858
|
retryCount: attempt,
|
|
739
859
|
updatedAt: new Date().toISOString(),
|
|
740
860
|
});
|
|
861
|
+
await this.trajectory?.stepRetrying(step, attempt, maxRetries);
|
|
741
862
|
await this.delay(retryDelay);
|
|
742
863
|
}
|
|
743
864
|
|
|
@@ -751,6 +872,8 @@ export class WorkflowRunner {
|
|
|
751
872
|
updatedAt: new Date().toISOString(),
|
|
752
873
|
});
|
|
753
874
|
this.emit({ type: 'step:started', runId, stepName: step.name });
|
|
875
|
+
this.postToChannel(`**[${step.name}]** Started (agent: ${agentDef.name})`);
|
|
876
|
+
await this.trajectory?.stepStarted(step, agentDef.name);
|
|
754
877
|
|
|
755
878
|
// Resolve step-output variables (e.g. {{steps.plan.output}}) at execution time
|
|
756
879
|
const stepOutputContext = this.buildStepOutputContext(stepStates);
|
|
@@ -776,13 +899,24 @@ export class WorkflowRunner {
|
|
|
776
899
|
updatedAt: new Date().toISOString(),
|
|
777
900
|
});
|
|
778
901
|
this.emit({ type: 'step:completed', runId, stepName: step.name, output });
|
|
902
|
+
this.postToChannel(
|
|
903
|
+
`**[${step.name}]** Completed\n${output.slice(0, 500)}${output.length > 500 ? '\n...(truncated)' : ''}`,
|
|
904
|
+
);
|
|
905
|
+
await this.trajectory?.stepCompleted(step, output, attempt + 1);
|
|
779
906
|
return;
|
|
780
907
|
} catch (err) {
|
|
781
908
|
lastError = err instanceof Error ? err.message : String(err);
|
|
782
909
|
}
|
|
783
910
|
}
|
|
784
911
|
|
|
785
|
-
// All retries exhausted —
|
|
912
|
+
// All retries exhausted — record decision and mark failed
|
|
913
|
+
await this.trajectory?.stepFailed(step, lastError ?? 'Unknown error', maxRetries + 1, maxRetries);
|
|
914
|
+
await this.trajectory?.decide(
|
|
915
|
+
`How to handle ${step.name} failure`,
|
|
916
|
+
'exhausted',
|
|
917
|
+
`All ${maxRetries + 1} attempts failed: ${lastError ?? 'Unknown error'}`,
|
|
918
|
+
);
|
|
919
|
+
this.postToChannel(`**[${step.name}]** Failed: ${lastError ?? 'Unknown error'}`);
|
|
786
920
|
await this.markStepFailed(state, lastError ?? 'Unknown error', runId);
|
|
787
921
|
throw new Error(`Step "${step.name}" failed after ${maxRetries} retries: ${lastError ?? 'Unknown error'}`);
|
|
788
922
|
}
|
|
@@ -796,32 +930,76 @@ export class WorkflowRunner {
|
|
|
796
930
|
throw new Error('AgentRelay not initialized');
|
|
797
931
|
}
|
|
798
932
|
|
|
933
|
+
// Append self-termination instructions to the task
|
|
934
|
+
const agentName = `${step.name}-${this.generateShortId()}`;
|
|
935
|
+
const taskWithExit = step.task + '\n\n---\n' +
|
|
936
|
+
'IMPORTANT: When you have fully completed this task, you MUST self-terminate by calling ' +
|
|
937
|
+
`the MCP tool: relay_release(name="${agentName}", reason="Task completed"). ` +
|
|
938
|
+
'Do not wait for further input — release yourself immediately after finishing.';
|
|
939
|
+
|
|
940
|
+
const agentChannels = this.channel ? [this.channel] : agentDef.channels;
|
|
941
|
+
|
|
799
942
|
const agent = await this.relay.spawnPty({
|
|
800
|
-
name:
|
|
943
|
+
name: agentName,
|
|
801
944
|
cli: agentDef.cli,
|
|
802
945
|
args: agentDef.constraints?.model ? ['--model', agentDef.constraints.model] : [],
|
|
803
|
-
channels:
|
|
946
|
+
channels: agentChannels,
|
|
947
|
+
task: taskWithExit,
|
|
948
|
+
idleThresholdSecs: agentDef.constraints?.idleThresholdSecs,
|
|
804
949
|
});
|
|
805
950
|
|
|
806
|
-
//
|
|
807
|
-
|
|
808
|
-
|
|
951
|
+
// Register the spawned agent in Relaycast for observability
|
|
952
|
+
if (this.relaycastApi) {
|
|
953
|
+
await this.relaycastApi.registerExternalAgent(
|
|
954
|
+
agent.name,
|
|
955
|
+
`Workflow agent for step "${step.name}" (${agentDef.cli})`,
|
|
956
|
+
).catch(() => {});
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Invite the spawned agent to the workflow channel
|
|
960
|
+
if (this.channel && this.relaycastApi) {
|
|
961
|
+
await this.relaycastApi.inviteToChannel(this.channel, agent.name).catch(() => {});
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Post task assignment to channel for observability
|
|
965
|
+
const taskPreview = step.task.slice(0, 500) + (step.task.length > 500 ? '...' : '');
|
|
966
|
+
this.postToChannel(`**[${step.name}]** Assigned to \`${agent.name}\`:\n${taskPreview}`);
|
|
967
|
+
|
|
968
|
+
// Task was already delivered as initial_task via spawnPty above.
|
|
809
969
|
|
|
810
|
-
// Wait for agent to exit
|
|
970
|
+
// Wait for agent to exit (self-termination via /exit)
|
|
811
971
|
const exitResult = await agent.waitForExit(timeoutMs);
|
|
812
972
|
|
|
813
973
|
if (exitResult === 'timeout') {
|
|
814
|
-
|
|
815
|
-
|
|
974
|
+
// Safety net: check if the verification file exists before giving up.
|
|
975
|
+
// The agent may have completed work but failed to /exit.
|
|
976
|
+
if (step.verification?.type === 'file_exists') {
|
|
977
|
+
const verifyPath = path.resolve(this.cwd, step.verification.value);
|
|
978
|
+
if (existsSync(verifyPath)) {
|
|
979
|
+
this.postToChannel(
|
|
980
|
+
`**[${step.name}]** Agent idle after completing work — releasing`,
|
|
981
|
+
);
|
|
982
|
+
await agent.release();
|
|
983
|
+
// Fall through to read output below
|
|
984
|
+
} else {
|
|
985
|
+
await agent.release();
|
|
986
|
+
throw new Error(`Step "${step.name}" timed out after ${timeoutMs}ms`);
|
|
987
|
+
}
|
|
988
|
+
} else {
|
|
989
|
+
await agent.release();
|
|
990
|
+
throw new Error(`Step "${step.name}" timed out after ${timeoutMs}ms`);
|
|
991
|
+
}
|
|
816
992
|
}
|
|
817
993
|
|
|
818
994
|
// Read output from summary file if it exists
|
|
819
995
|
const summaryPath = path.join(this.summaryDir, `${step.name}.md`);
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
996
|
+
const output = existsSync(summaryPath)
|
|
997
|
+
? await readFile(summaryPath, 'utf-8')
|
|
998
|
+
: exitResult === 'timeout'
|
|
999
|
+
? 'Agent completed (released after idle timeout)'
|
|
1000
|
+
: `Agent exited (${exitResult})`;
|
|
823
1001
|
|
|
824
|
-
return
|
|
1002
|
+
return output;
|
|
825
1003
|
}
|
|
826
1004
|
|
|
827
1005
|
// ── Verification ────────────────────────────────────────────────────────
|
|
@@ -911,6 +1089,13 @@ export class WorkflowRunner {
|
|
|
911
1089
|
updatedAt: new Date().toISOString(),
|
|
912
1090
|
});
|
|
913
1091
|
this.emit({ type: 'step:skipped', runId, stepName: step.name });
|
|
1092
|
+
this.postToChannel(`**[${step.name}]** Skipped — upstream dependency "${current}" failed`);
|
|
1093
|
+
await this.trajectory?.stepSkipped(step, `Upstream dependency "${current}" failed`);
|
|
1094
|
+
await this.trajectory?.decide(
|
|
1095
|
+
`Whether to skip ${step.name}`,
|
|
1096
|
+
'skip',
|
|
1097
|
+
`Upstream dependency "${current}" failed`,
|
|
1098
|
+
);
|
|
914
1099
|
queue.push(step.name);
|
|
915
1100
|
}
|
|
916
1101
|
}
|
|
@@ -937,6 +1122,56 @@ export class WorkflowRunner {
|
|
|
937
1122
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
938
1123
|
}
|
|
939
1124
|
|
|
1125
|
+
// ── Channel messaging ──────────────────────────────────────────────────
|
|
1126
|
+
|
|
1127
|
+
/** Post a message to the workflow channel. Fire-and-forget — never throws or blocks. */
|
|
1128
|
+
private postToChannel(text: string): void {
|
|
1129
|
+
if (!this.relaycastApi || !this.channel) return;
|
|
1130
|
+
this.relaycastApi.sendToChannel(this.channel, text).catch(() => {
|
|
1131
|
+
// Non-critical — don't break workflow execution
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// ── Trajectory helpers ────────────────────────────────────────────────
|
|
1136
|
+
|
|
1137
|
+
/** Analyze DAG structure for trajectory context. */
|
|
1138
|
+
private analyzeDAG(steps: WorkflowStep[]): string {
|
|
1139
|
+
const roots = steps.filter((s) => !s.dependsOn?.length);
|
|
1140
|
+
const withDeps = steps.filter((s) => s.dependsOn?.length);
|
|
1141
|
+
|
|
1142
|
+
const parts = [`Parsed ${steps.length} steps`];
|
|
1143
|
+
if (roots.length > 1) {
|
|
1144
|
+
parts.push(`${roots.length} parallel tracks`);
|
|
1145
|
+
}
|
|
1146
|
+
if (withDeps.length > 0) {
|
|
1147
|
+
parts.push(`${withDeps.length} dependent steps`);
|
|
1148
|
+
}
|
|
1149
|
+
parts.push('DAG validated, no cycles');
|
|
1150
|
+
return parts.join(', ');
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/** Collect step outcomes for trajectory synthesis. */
|
|
1154
|
+
private collectOutcomes(stepStates: Map<string, StepState>, steps?: WorkflowStep[]): StepOutcome[] {
|
|
1155
|
+
const stepsWithVerification = new Set(
|
|
1156
|
+
steps?.filter((s) => s.verification).map((s) => s.name) ?? [],
|
|
1157
|
+
);
|
|
1158
|
+
const outcomes: StepOutcome[] = [];
|
|
1159
|
+
for (const [name, state] of stepStates) {
|
|
1160
|
+
outcomes.push({
|
|
1161
|
+
name,
|
|
1162
|
+
agent: state.row.agentName,
|
|
1163
|
+
status: state.row.status === 'completed' ? 'completed'
|
|
1164
|
+
: state.row.status === 'skipped' ? 'skipped'
|
|
1165
|
+
: 'failed',
|
|
1166
|
+
attempts: state.row.retryCount + 1,
|
|
1167
|
+
output: state.row.output,
|
|
1168
|
+
error: state.row.error,
|
|
1169
|
+
verificationPassed: state.row.status === 'completed' && stepsWithVerification.has(name),
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
return outcomes;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
940
1175
|
// ── ID generation ─────────────────────────────────────────────────────
|
|
941
1176
|
|
|
942
1177
|
private generateId(): string {
|
|
@@ -82,7 +82,19 @@
|
|
|
82
82
|
"cascade",
|
|
83
83
|
"dag",
|
|
84
84
|
"debate",
|
|
85
|
-
"hierarchical"
|
|
85
|
+
"hierarchical",
|
|
86
|
+
"map-reduce",
|
|
87
|
+
"scatter-gather",
|
|
88
|
+
"supervisor",
|
|
89
|
+
"reflection",
|
|
90
|
+
"red-team",
|
|
91
|
+
"verifier",
|
|
92
|
+
"auction",
|
|
93
|
+
"escalation",
|
|
94
|
+
"saga",
|
|
95
|
+
"circuit-breaker",
|
|
96
|
+
"blackboard",
|
|
97
|
+
"swarm"
|
|
86
98
|
]
|
|
87
99
|
},
|
|
88
100
|
"AgentDefinition": {
|