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.
Files changed (81) hide show
  1. package/install.sh +32 -0
  2. package/package.json +21 -21
  3. package/packages/acp-bridge/package.json +2 -2
  4. package/packages/bridge/package.json +7 -7
  5. package/packages/broker-sdk/README.md +32 -0
  6. package/packages/broker-sdk/dist/__tests__/unit.test.js +70 -2
  7. package/packages/broker-sdk/dist/__tests__/unit.test.js.map +1 -1
  8. package/packages/broker-sdk/dist/client.d.ts +2 -0
  9. package/packages/broker-sdk/dist/client.d.ts.map +1 -1
  10. package/packages/broker-sdk/dist/client.js +10 -0
  11. package/packages/broker-sdk/dist/client.js.map +1 -1
  12. package/packages/broker-sdk/dist/protocol.d.ts +4 -0
  13. package/packages/broker-sdk/dist/protocol.d.ts.map +1 -1
  14. package/packages/broker-sdk/dist/relay.d.ts +10 -0
  15. package/packages/broker-sdk/dist/relay.d.ts.map +1 -1
  16. package/packages/broker-sdk/dist/relay.js +53 -0
  17. package/packages/broker-sdk/dist/relay.js.map +1 -1
  18. package/packages/broker-sdk/dist/relaycast.d.ts +10 -0
  19. package/packages/broker-sdk/dist/relaycast.d.ts.map +1 -1
  20. package/packages/broker-sdk/dist/relaycast.js +40 -0
  21. package/packages/broker-sdk/dist/relaycast.js.map +1 -1
  22. package/packages/broker-sdk/dist/workflows/coordinator.d.ts +1 -0
  23. package/packages/broker-sdk/dist/workflows/coordinator.d.ts.map +1 -1
  24. package/packages/broker-sdk/dist/workflows/coordinator.js +239 -7
  25. package/packages/broker-sdk/dist/workflows/coordinator.js.map +1 -1
  26. package/packages/broker-sdk/dist/workflows/index.d.ts +1 -0
  27. package/packages/broker-sdk/dist/workflows/index.d.ts.map +1 -1
  28. package/packages/broker-sdk/dist/workflows/index.js +1 -0
  29. package/packages/broker-sdk/dist/workflows/index.js.map +1 -1
  30. package/packages/broker-sdk/dist/workflows/run.d.ts +3 -1
  31. package/packages/broker-sdk/dist/workflows/run.d.ts.map +1 -1
  32. package/packages/broker-sdk/dist/workflows/run.js +4 -0
  33. package/packages/broker-sdk/dist/workflows/run.js.map +1 -1
  34. package/packages/broker-sdk/dist/workflows/runner.d.ts +9 -0
  35. package/packages/broker-sdk/dist/workflows/runner.d.ts.map +1 -1
  36. package/packages/broker-sdk/dist/workflows/runner.js +203 -14
  37. package/packages/broker-sdk/dist/workflows/runner.js.map +1 -1
  38. package/packages/broker-sdk/dist/workflows/trajectory.d.ts +80 -0
  39. package/packages/broker-sdk/dist/workflows/trajectory.d.ts.map +1 -0
  40. package/packages/broker-sdk/dist/workflows/trajectory.js +362 -0
  41. package/packages/broker-sdk/dist/workflows/trajectory.js.map +1 -0
  42. package/packages/broker-sdk/dist/workflows/types.d.ts +15 -1
  43. package/packages/broker-sdk/dist/workflows/types.d.ts.map +1 -1
  44. package/packages/broker-sdk/package.json +2 -2
  45. package/packages/broker-sdk/src/__tests__/swarm-coordinator.test.ts +356 -0
  46. package/packages/broker-sdk/src/__tests__/unit.test.ts +92 -1
  47. package/packages/broker-sdk/src/__tests__/workflow-trajectory.test.ts +408 -0
  48. package/packages/broker-sdk/src/client.ts +15 -0
  49. package/packages/broker-sdk/src/protocol.ts +5 -0
  50. package/packages/broker-sdk/src/relay.ts +59 -0
  51. package/packages/broker-sdk/src/relaycast.ts +42 -0
  52. package/packages/broker-sdk/src/workflows/README.md +64 -0
  53. package/packages/broker-sdk/src/workflows/coordinator.ts +246 -8
  54. package/packages/broker-sdk/src/workflows/index.ts +1 -0
  55. package/packages/broker-sdk/src/workflows/run.ts +9 -1
  56. package/packages/broker-sdk/src/workflows/runner.ts +249 -14
  57. package/packages/broker-sdk/src/workflows/schema.json +13 -1
  58. package/packages/broker-sdk/src/workflows/trajectory.ts +507 -0
  59. package/packages/broker-sdk/src/workflows/types.ts +31 -1
  60. package/packages/broker-sdk/tsconfig.json +1 -0
  61. package/packages/broker-sdk/vitest.config.ts +9 -0
  62. package/packages/config/package.json +2 -2
  63. package/packages/continuity/package.json +2 -2
  64. package/packages/daemon/package.json +12 -12
  65. package/packages/hooks/package.json +4 -4
  66. package/packages/mcp/package.json +5 -5
  67. package/packages/memory/package.json +2 -2
  68. package/packages/policy/package.json +2 -2
  69. package/packages/protocol/package.json +1 -1
  70. package/packages/resiliency/package.json +1 -1
  71. package/packages/sdk/package.json +3 -3
  72. package/packages/sdk-py/src/agent_relay/builder.py +4 -0
  73. package/packages/sdk-py/src/agent_relay/types.py +15 -0
  74. package/packages/spawner/package.json +1 -1
  75. package/packages/state/package.json +1 -1
  76. package/packages/storage/package.json +2 -2
  77. package/packages/telemetry/package.json +1 -1
  78. package/packages/trajectory/package.json +2 -2
  79. package/packages/user-directory/package.json +2 -2
  80. package/packages/utils/package.json +3 -3
  81. 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 — mark failed and throw so callers can apply error strategy
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: `${step.name}-${this.generateShortId()}`,
943
+ name: agentName,
801
944
  cli: agentDef.cli,
802
945
  args: agentDef.constraints?.model ? ['--model', agentDef.constraints.model] : [],
803
- channels: agentDef.channels,
946
+ channels: agentChannels,
947
+ task: taskWithExit,
948
+ idleThresholdSecs: agentDef.constraints?.idleThresholdSecs,
804
949
  });
805
950
 
806
- // Send the task as a message to the agent
807
- const system = this.relay.human({ name: 'WorkflowRunner' });
808
- await system.sendMessage({ to: agent.name, text: step.task });
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
- await agent.release();
815
- throw new Error(`Step "${step.name}" timed out after ${timeoutMs}ms`);
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
- if (existsSync(summaryPath)) {
821
- return await readFile(summaryPath, 'utf-8');
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 `Agent exited (${exitResult})`;
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": {