agent-relay 4.0.19 → 4.0.21

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 (130) hide show
  1. package/dist/index.cjs +1407 -537
  2. package/dist/src/cli/commands/messaging.d.ts +44 -0
  3. package/dist/src/cli/commands/messaging.d.ts.map +1 -1
  4. package/dist/src/cli/commands/messaging.js +195 -8
  5. package/dist/src/cli/commands/messaging.js.map +1 -1
  6. package/dist/src/cli/commands/setup.d.ts +30 -1
  7. package/dist/src/cli/commands/setup.d.ts.map +1 -1
  8. package/dist/src/cli/commands/setup.js +102 -85
  9. package/dist/src/cli/commands/setup.js.map +1 -1
  10. package/node_modules/@agent-relay/cloud/dist/auth.d.ts +2 -2
  11. package/node_modules/@agent-relay/cloud/dist/auth.d.ts.map +1 -1
  12. package/node_modules/@agent-relay/cloud/dist/auth.js +108 -62
  13. package/node_modules/@agent-relay/cloud/dist/auth.js.map +1 -1
  14. package/node_modules/@agent-relay/cloud/package.json +2 -2
  15. package/node_modules/@agent-relay/config/package.json +1 -1
  16. package/node_modules/@agent-relay/hooks/package.json +4 -4
  17. package/node_modules/@agent-relay/sdk/dist/workflows/trajectory.d.ts +5 -35
  18. package/node_modules/@agent-relay/sdk/dist/workflows/trajectory.d.ts.map +1 -1
  19. package/node_modules/@agent-relay/sdk/dist/workflows/trajectory.js +158 -292
  20. package/node_modules/@agent-relay/sdk/dist/workflows/trajectory.js.map +1 -1
  21. package/node_modules/@agent-relay/sdk/package.json +3 -2
  22. package/node_modules/@agent-relay/telemetry/package.json +1 -1
  23. package/node_modules/@agent-relay/trajectory/package.json +2 -2
  24. package/node_modules/@agent-relay/user-directory/package.json +2 -2
  25. package/node_modules/@agent-relay/utils/package.json +2 -2
  26. package/node_modules/@clack/core/CHANGELOG.md +200 -0
  27. package/node_modules/@clack/core/LICENSE +9 -0
  28. package/node_modules/@clack/core/README.md +22 -0
  29. package/node_modules/@clack/core/dist/index.cjs +15 -0
  30. package/node_modules/@clack/core/dist/index.cjs.map +1 -0
  31. package/node_modules/@clack/core/dist/index.d.cts +151 -0
  32. package/node_modules/@clack/core/dist/index.d.mts +151 -0
  33. package/node_modules/@clack/core/dist/index.d.ts +151 -0
  34. package/node_modules/@clack/core/dist/index.mjs +15 -0
  35. package/node_modules/@clack/core/dist/index.mjs.map +1 -0
  36. package/node_modules/@clack/core/package.json +62 -0
  37. package/node_modules/@clack/prompts/CHANGELOG.md +256 -0
  38. package/node_modules/@clack/prompts/LICENSE +23 -0
  39. package/node_modules/@clack/prompts/README.md +158 -0
  40. package/node_modules/@clack/prompts/dist/index.cjs +77 -0
  41. package/node_modules/@clack/prompts/dist/index.d.ts +106 -0
  42. package/node_modules/@clack/prompts/dist/index.mjs +77 -0
  43. package/node_modules/@clack/prompts/node_modules/is-unicode-supported/index.d.ts +12 -0
  44. package/node_modules/@clack/prompts/node_modules/is-unicode-supported/index.js +17 -0
  45. package/node_modules/@clack/prompts/node_modules/is-unicode-supported/license +9 -0
  46. package/node_modules/@clack/prompts/node_modules/is-unicode-supported/package.json +43 -0
  47. package/node_modules/@clack/prompts/node_modules/is-unicode-supported/readme.md +35 -0
  48. package/node_modules/@clack/prompts/package.json +65 -0
  49. package/node_modules/@smithy/config-resolver/package.json +2 -2
  50. package/node_modules/@smithy/util-defaults-mode-node/package.json +2 -2
  51. package/node_modules/@smithy/util-endpoints/dist-cjs/index.js +154 -61
  52. package/node_modules/@smithy/util-endpoints/dist-es/bdd/BinaryDecisionDiagram.js +15 -0
  53. package/node_modules/@smithy/util-endpoints/dist-es/decideEndpoint.js +41 -0
  54. package/node_modules/@smithy/util-endpoints/dist-es/index.js +2 -0
  55. package/node_modules/@smithy/util-endpoints/dist-es/lib/coalesce.js +8 -0
  56. package/node_modules/@smithy/util-endpoints/dist-es/lib/index.js +3 -0
  57. package/node_modules/@smithy/util-endpoints/dist-es/lib/ite.js +3 -0
  58. package/node_modules/@smithy/util-endpoints/dist-es/lib/split.js +13 -0
  59. package/node_modules/@smithy/util-endpoints/dist-es/lib/substring.js +1 -1
  60. package/node_modules/@smithy/util-endpoints/dist-es/utils/endpointFunctions.js +4 -1
  61. package/node_modules/@smithy/util-endpoints/dist-es/utils/evaluateExpression.js +20 -5
  62. package/node_modules/@smithy/util-endpoints/dist-es/utils/evaluateTemplate.js +3 -6
  63. package/node_modules/@smithy/util-endpoints/dist-es/utils/getReferenceValue.js +1 -5
  64. package/node_modules/@smithy/util-endpoints/dist-types/bdd/BinaryDecisionDiagram.d.ts +22 -0
  65. package/node_modules/@smithy/util-endpoints/dist-types/decideEndpoint.d.ts +7 -0
  66. package/node_modules/@smithy/util-endpoints/dist-types/index.d.ts +2 -0
  67. package/node_modules/@smithy/util-endpoints/dist-types/lib/coalesce.d.ts +7 -0
  68. package/node_modules/@smithy/util-endpoints/dist-types/lib/index.d.ts +3 -0
  69. package/node_modules/@smithy/util-endpoints/dist-types/lib/ite.d.ts +6 -0
  70. package/node_modules/@smithy/util-endpoints/dist-types/lib/split.d.ts +11 -0
  71. package/node_modules/@smithy/util-endpoints/dist-types/utils/endpointFunctions.d.ts +4 -0
  72. package/node_modules/@smithy/util-endpoints/dist-types/utils/getReferenceValue.d.ts +3 -1
  73. package/node_modules/@smithy/util-endpoints/package.json +1 -1
  74. package/node_modules/agent-trajectories/README.md +562 -0
  75. package/node_modules/agent-trajectories/dist/chunk-W222QB6V.js +2036 -0
  76. package/node_modules/agent-trajectories/dist/chunk-W222QB6V.js.map +1 -0
  77. package/node_modules/agent-trajectories/dist/cli/index.d.ts +2 -0
  78. package/node_modules/agent-trajectories/dist/cli/index.js +4592 -0
  79. package/node_modules/agent-trajectories/dist/cli/index.js.map +1 -0
  80. package/node_modules/agent-trajectories/dist/index-7tzw_CMS.d.ts +1777 -0
  81. package/node_modules/agent-trajectories/dist/index.d.ts +90 -0
  82. package/node_modules/agent-trajectories/dist/index.js +73 -0
  83. package/node_modules/agent-trajectories/dist/index.js.map +1 -0
  84. package/node_modules/agent-trajectories/dist/sdk/index.d.ts +2 -0
  85. package/node_modules/agent-trajectories/dist/sdk/index.js +39 -0
  86. package/node_modules/agent-trajectories/dist/sdk/index.js.map +1 -0
  87. package/node_modules/agent-trajectories/package.json +72 -0
  88. package/node_modules/commander/LICENSE +22 -0
  89. package/node_modules/commander/Readme.md +1157 -0
  90. package/node_modules/commander/esm.mjs +16 -0
  91. package/node_modules/commander/index.js +24 -0
  92. package/node_modules/commander/lib/argument.js +149 -0
  93. package/node_modules/commander/lib/command.js +2509 -0
  94. package/node_modules/commander/lib/error.js +39 -0
  95. package/node_modules/commander/lib/help.js +520 -0
  96. package/node_modules/commander/lib/option.js +330 -0
  97. package/node_modules/commander/lib/suggestSimilar.js +101 -0
  98. package/node_modules/commander/package-support.json +16 -0
  99. package/node_modules/commander/package.json +84 -0
  100. package/node_modules/commander/typings/esm.d.mts +3 -0
  101. package/node_modules/commander/typings/index.d.ts +969 -0
  102. package/node_modules/picocolors/LICENSE +15 -0
  103. package/node_modules/picocolors/README.md +21 -0
  104. package/node_modules/picocolors/package.json +25 -0
  105. package/node_modules/picocolors/picocolors.browser.js +4 -0
  106. package/node_modules/picocolors/picocolors.d.ts +5 -0
  107. package/node_modules/picocolors/picocolors.js +75 -0
  108. package/node_modules/picocolors/types.d.ts +51 -0
  109. package/node_modules/sisteransi/license +21 -0
  110. package/node_modules/sisteransi/package.json +34 -0
  111. package/node_modules/sisteransi/readme.md +113 -0
  112. package/node_modules/sisteransi/src/index.js +58 -0
  113. package/node_modules/sisteransi/src/sisteransi.d.ts +35 -0
  114. package/package.json +10 -10
  115. package/packages/cloud/dist/auth.d.ts +2 -2
  116. package/packages/cloud/dist/auth.d.ts.map +1 -1
  117. package/packages/cloud/dist/auth.js +108 -62
  118. package/packages/cloud/dist/auth.js.map +1 -1
  119. package/packages/cloud/package.json +2 -2
  120. package/packages/config/package.json +1 -1
  121. package/packages/hooks/package.json +4 -4
  122. package/packages/sdk/dist/workflows/trajectory.d.ts +5 -35
  123. package/packages/sdk/dist/workflows/trajectory.d.ts.map +1 -1
  124. package/packages/sdk/dist/workflows/trajectory.js +158 -292
  125. package/packages/sdk/dist/workflows/trajectory.js.map +1 -1
  126. package/packages/sdk/package.json +3 -2
  127. package/packages/telemetry/package.json +1 -1
  128. package/packages/trajectory/package.json +2 -2
  129. package/packages/user-directory/package.json +2 -2
  130. package/packages/utils/package.json +2 -2
@@ -1,19 +1,6 @@
1
- /**
2
- * WorkflowTrajectory records a structured trajectory for each workflow run.
3
- *
4
- * Writes trajectory JSON files directly to `.trajectories/active/` in a format
5
- * compatible with `trail show`. No external CLI or package dependency required.
6
- *
7
- * Design principles:
8
- * 1. One trajectory per workflow run
9
- * 2. Chapters map to workflow phases, not individual steps
10
- * 3. Non-blocking — trajectory recording never fails the workflow
11
- * 4. Opt-in but default-on
12
- */
13
- import { randomBytes } from 'node:crypto';
14
- import { existsSync } from 'node:fs';
15
- import { mkdir, writeFile, rename } from 'node:fs/promises';
16
- import path from 'node:path';
1
+ /** WorkflowTrajectory records canonical workflow trajectories via agent-trajectories. */
2
+ import { dirname, join } from 'node:path';
3
+ import { FileStorage, abandonTrajectory, addChapter as appendChapter, addEvent as appendEvent, completeTrajectory, createTrajectory, } from 'agent-trajectories';
17
4
  function classifyFailure(error) {
18
5
  const e = error.toLowerCase();
19
6
  if (e.includes('timed out') || e.includes('timeout'))
@@ -31,35 +18,94 @@ function classifyFailure(error) {
31
18
  function diagnosisFor(cause, outcome) {
32
19
  switch (cause) {
33
20
  case 'timeout':
34
- if (outcome.nonInteractive) {
35
- return (`Non-interactive agent timed out — the task is likely too large or complex for a single subprocess call. ` +
36
- `Consider pre-reading large files in a deterministic step and injecting only the relevant excerpt via {{steps.X.output}}.`);
37
- }
38
- return (`Interactive agent timed out — it may have gone idle, failed to self-terminate, or the task scope was too broad. ` +
39
- `Check if the agent was waiting for relay signals that never arrived.`);
21
+ return outcome.nonInteractive
22
+ ? 'Non-interactive agent timed out — the task is likely too large or complex for a single subprocess call. Consider pre-reading large files in a deterministic step and injecting only the relevant excerpt via {{steps.X.output}}.'
23
+ : 'Interactive agent timed out — it may have gone idle, failed to self-terminate, or the task scope was too broad. Check if the agent was waiting for relay signals that never arrived.';
40
24
  case 'verification_mismatch':
41
- return (`Agent completed but did not output the expected sentinel "${outcome.verificationValue ?? '(unknown)'}". ` +
42
- `The task prompt may not clearly specify the required output format, ` +
43
- `or the agent produced correct work but did not emit the signal.`);
25
+ return `Agent completed but did not output the expected sentinel "${outcome.verificationValue ?? '(unknown)'}". The task prompt may not clearly specify the required output format, or the agent produced correct work but did not emit the signal.`;
44
26
  case 'spawn_failed':
45
- return `The agent process could not be started — the CLI binary may be missing from PATH or the working directory is incorrect.`;
27
+ return 'The agent process could not be started — the CLI binary may be missing from PATH or the working directory is incorrect.';
46
28
  case 'exit_nonzero':
47
- return `The agent process exited with a non-zero exit code. Check stderr for the root cause.`;
29
+ return 'The agent process exited with a non-zero exit code. Check stderr for the root cause.';
48
30
  case 'aborted':
49
- return `The step was cancelled (user interrupt or upstream abort).`;
31
+ return 'The step was cancelled (user interrupt or upstream abort).';
50
32
  default:
51
- return `Unexpected failure. Review the error and step definition.`;
33
+ return 'Unexpected failure. Review the error and step definition.';
52
34
  }
53
35
  }
54
- // ── WorkflowTrajectory ──────────────────────────────────────────────────────
36
+ function buildSynthesis(label, outcomes, unblocks) {
37
+ const completed = outcomes.filter((o) => o.status === 'completed');
38
+ const failed = outcomes.filter((o) => o.status === 'failed');
39
+ const retried = outcomes.filter((o) => o.attempts > 1 && o.status !== 'failed');
40
+ const parts = [`${label} resolved.`, `${completed.length}/${outcomes.length} steps completed.`];
41
+ if (failed.length > 0)
42
+ parts.push(`${failed.length} step(s) failed: ${failed.map((s) => s.name).join(', ')}.`);
43
+ if (retried.length > 0)
44
+ parts.push(`${retried.length} step(s) required retries: ${retried.map((s) => s.name).join(', ')}.`);
45
+ else if (failed.length === 0)
46
+ parts.push('All steps completed on first attempt.');
47
+ if (unblocks?.length)
48
+ parts.push(`Unblocking: ${unblocks.join(', ')}.`);
49
+ return parts.join(' ');
50
+ }
51
+ function computeConfidence(outcomes) {
52
+ if (outcomes.length === 0)
53
+ return 0.7;
54
+ const total = outcomes.length;
55
+ const completed = outcomes.filter((o) => o.status === 'completed').length;
56
+ const firstAttempt = outcomes.filter((o) => o.attempts === 1 && o.status === 'completed').length;
57
+ const verified = outcomes.filter((o) => o.verificationPassed).length;
58
+ return Math.min(1, 0.5 * (completed / total) + 0.25 * (firstAttempt / total) + 0.25 * (verified / total));
59
+ }
60
+ function formatElapsed(elapsed, long) {
61
+ return elapsed > 60_000
62
+ ? `${Math.round(elapsed / 60_000)}${long ? ' minutes' : 'min'}`
63
+ : `${Math.round(elapsed / 1_000)}${long ? ' seconds' : 's'}`;
64
+ }
65
+ function buildRunSummary(outcomes, startTime) {
66
+ const completed = outcomes.filter((o) => o.status === 'completed');
67
+ const failed = outcomes.filter((o) => o.status === 'failed');
68
+ const skipped = outcomes.filter((o) => o.status === 'skipped');
69
+ const elapsedStr = formatElapsed(Date.now() - startTime, false);
70
+ if (failed.length === 0) {
71
+ const retried = completed.filter((o) => o.attempts > 1);
72
+ const base = `All ${completed.length} steps completed in ${elapsedStr}.`;
73
+ return retried.length > 0
74
+ ? `${base} ${retried.length} step(s) needed retries: ${retried.map((o) => o.name).join(', ')}.`
75
+ : base;
76
+ }
77
+ const firstFailure = failed[0];
78
+ const cause = classifyFailure(firstFailure.error ?? '');
79
+ const cascaded = skipped.length > 0
80
+ ? ` Caused ${skipped.length} downstream step(s) to be skipped: ${skipped.map((o) => o.name).join(', ')}.`
81
+ : '';
82
+ return `Failed at "${firstFailure.name}" [${cause}] after ${elapsedStr}.${cascaded} ${completed.length}/${outcomes.length} steps completed before failure.`;
83
+ }
84
+ function extractLearnings(outcomes) {
85
+ const learnings = [];
86
+ const timeouts = outcomes.filter((o) => o.status === 'failed' && classifyFailure(o.error ?? '') === 'timeout');
87
+ if (timeouts.some((o) => o.nonInteractive))
88
+ learnings.push(`Non-interactive agent timeouts detected (${timeouts.map((o) => o.name).join(', ')}). Use deterministic steps to pre-read files and inject content — non-interactive agents should not discover information via tools.`);
89
+ const verifyFails = outcomes.filter((o) => o.status === 'failed' && classifyFailure(o.error ?? '') === 'verification_mismatch');
90
+ if (verifyFails.length > 0)
91
+ learnings.push(`Verification mismatch on: ${verifyFails.map((o) => `"${o.name}" (expected "${o.verificationValue ?? '?'}")`).join(', ')}. Make the required output format more explicit in the task prompt.`);
92
+ const retried = outcomes.filter((o) => o.attempts > 1 && o.status === 'completed');
93
+ if (retried.length > 0)
94
+ learnings.push(`${retried.map((o) => `"${o.name}" (${o.attempts} attempts)`).join(', ')} succeeded after retries — consider adding clearer output instructions to reduce retries.`);
95
+ return learnings;
96
+ }
97
+ const extractChallenges = (outcomes) => outcomes
98
+ .filter((o) => o.status === 'failed')
99
+ .map((step) => diagnosisFor(classifyFailure(step.error ?? ''), step));
55
100
  export class WorkflowTrajectory {
56
101
  trajectory = null;
57
- currentChapterId = null;
102
+ storage;
103
+ storageInit;
58
104
  enabled;
59
105
  reflectOnBarriers;
60
106
  reflectOnConverge;
61
107
  autoDecisions;
62
- dataDir;
108
+ storageBaseDir;
63
109
  runId;
64
110
  startTime = 0;
65
111
  swarmPattern = 'dag';
@@ -70,79 +116,56 @@ export class WorkflowTrajectory {
70
116
  this.reflectOnConverge = cfg.reflectOnConverge !== false;
71
117
  this.autoDecisions = cfg.autoDecisions !== false;
72
118
  this.runId = runId;
73
- this.dataDir = process.env.TRAJECTORIES_DATA_DIR ?? path.join(cwd, '.trajectories');
119
+ const dataDir = process.env.TRAJECTORIES_DATA_DIR ?? join(cwd, '.trajectories');
120
+ this.storageBaseDir = process.env.TRAJECTORIES_DATA_DIR ? dirname(dataDir) : cwd;
74
121
  }
75
- // ── Lifecycle ──────────────────────────────────────────────────────────────
76
- /** Start the trajectory (called at run:started). */
77
122
  async start(workflowName, stepCount, trackInfo, description, pattern) {
78
123
  if (!this.enabled)
79
124
  return;
80
125
  this.startTime = Date.now();
81
126
  this.swarmPattern = pattern ?? 'dag';
82
- const id = `traj_${Date.now()}_${randomBytes(4).toString('hex')}`;
83
- this.trajectory = {
84
- id,
85
- version: 1,
86
- task: {
87
- title: workflowName,
88
- source: { system: 'workflow-runner', id: this.runId },
89
- },
90
- status: 'active',
91
- startedAt: new Date().toISOString(),
92
- agents: [{ name: 'orchestrator', role: 'workflow-runner', joinedAt: new Date().toISOString() }],
93
- chapters: [],
94
- };
95
- // Open Planning chapter — record intent, not just mechanics
127
+ this.trajectory = createTrajectory({
128
+ title: workflowName,
129
+ description,
130
+ source: { system: 'workflow-runner', id: this.runId },
131
+ });
132
+ const workflowId = process.env.TRAJECTORIES_WORKFLOW_ID?.trim();
133
+ if (workflowId)
134
+ this.trajectory = { ...this.trajectory, workflowId };
135
+ this.trajectory.agents.push({
136
+ name: 'orchestrator',
137
+ role: 'workflow-runner',
138
+ joinedAt: new Date().toISOString(),
139
+ });
96
140
  this.openChapter('Planning', 'orchestrator');
97
- if (description) {
98
- // Record why this workflow exists
141
+ if (description)
99
142
  this.addEvent('note', `Purpose: ${description.trim()}`);
100
- }
101
143
  this.addEvent('note', `Approach: ${stepCount}-step ${this.swarmPattern} workflow${trackInfo ? ` — ${trackInfo}` : ''}`);
102
144
  await this.flush();
103
145
  }
104
- // ── Chapters ───────────────────────────────────────────────────────────────
105
- /** Begin a new parallel track chapter. */
106
146
  async beginTrack(trackName) {
107
- if (!this.enabled || !this.trajectory)
108
- return;
109
- this.closeCurrentChapter();
110
- this.openChapter(`Execution: ${trackName}`, 'orchestrator');
111
- await this.flush();
147
+ if (this.enabled && this.trajectory) {
148
+ this.openChapter(`Execution: ${trackName}`, 'orchestrator');
149
+ await this.flush();
150
+ }
112
151
  }
113
- /** Begin a convergence chapter (after barrier/parallel completion). */
114
152
  async beginConvergence(label) {
115
- if (!this.enabled || !this.trajectory)
116
- return;
117
- this.closeCurrentChapter();
118
- this.openChapter(`Convergence: ${label}`, 'orchestrator');
119
- await this.flush();
120
- }
121
- /** Begin the retrospective chapter. */
122
- openRetrospective() {
123
- if (!this.trajectory)
124
- return;
125
- this.closeCurrentChapter();
126
- this.openChapter('Retrospective', 'orchestrator');
153
+ if (this.enabled && this.trajectory) {
154
+ this.openChapter(`Convergence: ${label}`, 'orchestrator');
155
+ await this.flush();
156
+ }
127
157
  }
128
- // ── Step events ────────────────────────────────────────────────────────────
129
- /** Record step started — captures intent, not just assignment. */
130
158
  async stepStarted(step, agent, participants) {
131
159
  if (!this.enabled || !this.trajectory)
132
160
  return;
133
161
  await this.registerAgent(agent, participants?.role ?? step.agent ?? 'deterministic');
134
- if (participants?.owner && participants.owner !== agent) {
162
+ if (participants?.owner && participants.owner !== agent)
135
163
  await this.registerAgent(participants.owner, 'owner');
136
- }
137
- if (participants?.specialist) {
164
+ if (participants?.specialist)
138
165
  await this.registerAgent(participants.specialist, 'specialist');
139
- }
140
- if (participants?.reviewer) {
166
+ if (participants?.reviewer)
141
167
  await this.registerAgent(participants.reviewer, 'reviewer');
142
- }
143
- this.closeCurrentChapter();
144
168
  this.openChapter(`Execution: ${step.name}`, agent);
145
- // Capture the step's purpose: first non-empty sentence of the task
146
169
  const intent = step.task
147
170
  ? step.task
148
171
  .trim()
@@ -154,25 +177,18 @@ export class WorkflowTrajectory {
154
177
  await this.flush();
155
178
  }
156
179
  async registerAgent(name, role) {
157
- if (!this.enabled || !this.trajectory)
180
+ if (!this.enabled || !this.trajectory || this.trajectory.agents.some((agent) => agent.name === name))
158
181
  return;
159
- if (!this.trajectory.agents.some((a) => a.name === name)) {
160
- this.trajectory.agents.push({
161
- name,
162
- role,
163
- joinedAt: new Date().toISOString(),
164
- });
165
- await this.flush();
166
- }
182
+ this.trajectory.agents.push({ name, role, joinedAt: new Date().toISOString() });
183
+ await this.flush();
167
184
  }
168
185
  async stepSupervisionAssigned(step, supervised) {
169
186
  if (!this.enabled || !this.trajectory)
170
187
  return;
171
188
  await this.registerAgent(supervised.owner.name, 'owner');
172
189
  await this.registerAgent(supervised.specialist.name, 'specialist');
173
- if (supervised.reviewer?.name) {
190
+ if (supervised.reviewer?.name)
174
191
  await this.registerAgent(supervised.reviewer.name, 'reviewer');
175
- }
176
192
  const reviewerNote = supervised.reviewer?.name ? `, reviewer=${supervised.reviewer.name}` : '';
177
193
  this.addEvent('decision', `"${step.name}" supervision assigned → owner=${supervised.owner.name}, specialist=${supervised.specialist.name}${reviewerNote}`, 'medium', {
178
194
  owner: supervised.owner.name,
@@ -204,38 +220,25 @@ export class WorkflowTrajectory {
204
220
  const modeLabel = decision.mode === 'marker' ? 'marker-based' : `${decision.mode}-based`;
205
221
  const reason = decision.reason ? ` — ${decision.reason}` : '';
206
222
  const evidence = this.formatCompletionEvidenceSummary(decision.evidence);
207
- const evidenceSuffix = evidence ? ` (${evidence})` : '';
208
- this.addEvent(decision.mode === 'marker' ? 'completion-marker' : 'completion-evidence', `"${stepName}" ${modeLabel} completion${reason}${evidenceSuffix}`, 'medium', {
209
- stepName,
210
- completionMode: decision.mode,
211
- reason: decision.reason,
212
- evidence: decision.evidence,
213
- });
223
+ this.addEvent(decision.mode === 'marker' ? 'completion-marker' : 'completion-evidence', `"${stepName}" ${modeLabel} completion${reason}${evidence ? ` (${evidence})` : ''}`, 'medium', { stepName, completionMode: decision.mode, reason: decision.reason, evidence: decision.evidence });
214
224
  await this.flush();
215
225
  }
216
- /** Record step completed — captures what was accomplished. */
217
226
  async stepCompleted(step, output, attempt, decision) {
218
227
  if (!this.enabled || !this.trajectory)
219
228
  return;
220
- const suffix = attempt > 1 ? ` (after ${attempt} attempts)` : '';
221
- // Prefer the last non-empty line of output as the completion signal —
222
- // agents conventionally output their sentinel last (e.g. "ANALYSIS_DONE")
229
+ if (decision)
230
+ await this.stepCompletionDecision(step.name, decision);
223
231
  const lines = output
224
232
  .split('\n')
225
- .map((l) => l.trim())
233
+ .map((line) => line.trim())
226
234
  .filter(Boolean);
227
235
  const lastMeaningful = lines.at(-1) ?? '';
228
236
  const completion = lastMeaningful.length > 0 && lastMeaningful.length < 100
229
237
  ? lastMeaningful
230
238
  : output.trim().slice(0, 120) || '(no output)';
231
- if (decision) {
232
- await this.stepCompletionDecision(step.name, decision);
233
- }
234
- const modeSuffix = decision ? ` [${decision.mode}]` : '';
235
- this.addEvent('finding', `"${step.name}" completed${suffix}${modeSuffix} → ${completion}`, 'medium');
239
+ this.addEvent('finding', `"${step.name}" completed${attempt > 1 ? ` (after ${attempt} attempts)` : ''}${decision ? ` [${decision.mode}]` : ''} → ${completion}`, 'medium');
236
240
  await this.flush();
237
241
  }
238
- /** Record step failed — categorizes root cause for actionable diagnosis. */
239
242
  async stepFailed(step, error, attempt, maxRetries, outcome) {
240
243
  if (!this.enabled || !this.trajectory)
241
244
  return;
@@ -257,44 +260,30 @@ export class WorkflowTrajectory {
257
260
  });
258
261
  await this.flush();
259
262
  }
260
- /** Record step skipped — note the cascade impact. */
261
263
  async stepSkipped(step, reason) {
262
- if (!this.enabled || !this.trajectory)
263
- return;
264
- this.addEvent('note', `"${step.name}" skipped — ${reason}`);
265
- await this.flush();
264
+ if (this.enabled && this.trajectory) {
265
+ this.addEvent('note', `"${step.name}" skipped — ${reason}`);
266
+ await this.flush();
267
+ }
266
268
  }
267
- /** Record step retrying. */
268
269
  async stepRetrying(step, attempt, maxRetries) {
269
- if (!this.enabled || !this.trajectory)
270
- return;
271
- this.addEvent('note', `"${step.name}" retrying (attempt ${attempt}/${maxRetries + 1})`);
272
- await this.flush();
270
+ if (this.enabled && this.trajectory) {
271
+ this.addEvent('note', `"${step.name}" retrying (attempt ${attempt}/${maxRetries + 1})`);
272
+ await this.flush();
273
+ }
273
274
  }
274
- // ── Reflections ────────────────────────────────────────────────────────────
275
- /** Record a reflection at a convergence point. */
276
275
  async reflect(synthesis, confidence, focalPoints) {
277
276
  if (!this.enabled || !this.trajectory)
278
277
  return;
279
- const raw = { confidence };
280
- if (focalPoints?.length) {
281
- raw.focalPoints = focalPoints;
282
- }
283
- this.addEvent('reflection', synthesis, 'high', raw);
278
+ this.addEvent('reflection', synthesis, 'high', focalPoints?.length ? { confidence, focalPoints } : { confidence });
284
279
  await this.flush();
285
280
  }
286
- /** Synthesize and reflect after a set of steps complete (barrier or parallel convergence). */
287
281
  async synthesizeAndReflect(label, outcomes, unblocks) {
288
282
  if (!this.enabled || !this.trajectory)
289
283
  return;
290
- const synthesis = this.buildSynthesis(label, outcomes, unblocks);
291
- const confidence = this.computeConfidence(outcomes);
292
- const focalPoints = outcomes.map((o) => `${o.name}: ${o.status}`);
293
284
  await this.beginConvergence(label);
294
- await this.reflect(synthesis, confidence, focalPoints);
285
+ await this.reflect(buildSynthesis(label, outcomes, unblocks), computeConfidence(outcomes), outcomes.map((o) => `${o.name}: ${o.status}`));
295
286
  }
296
- // ── Decisions ──────────────────────────────────────────────────────────────
297
- /** Record an orchestrator decision. */
298
287
  async decide(question, chosen, reasoning) {
299
288
  if (!this.enabled || !this.trajectory || !this.autoDecisions)
300
289
  return;
@@ -305,52 +294,39 @@ export class WorkflowTrajectory {
305
294
  });
306
295
  await this.flush();
307
296
  }
308
- // ── Completion ─────────────────────────────────────────────────────────────
309
- /** Complete the trajectory with a summary. */
310
297
  async complete(summary, confidence, meta) {
311
298
  if (!this.enabled || !this.trajectory)
312
299
  return;
313
- this.openRetrospective();
314
- const elapsed = Date.now() - this.startTime;
315
- const elapsedStr = elapsed > 60_000 ? `${Math.round(elapsed / 60_000)} minutes` : `${Math.round(elapsed / 1_000)} seconds`;
316
- this.addEvent('reflection', `${summary} (completed in ${elapsedStr})`, 'high');
317
- this.trajectory.status = 'completed';
318
- this.trajectory.completedAt = new Date().toISOString();
319
- this.trajectory.retrospective = {
300
+ this.openChapter('Retrospective', 'orchestrator');
301
+ this.addEvent('reflection', `${summary} (completed in ${formatElapsed(Date.now() - this.startTime, true)})`, 'high');
302
+ this.trajectory = completeTrajectory(this.trajectory, {
320
303
  summary,
321
- approach: `${this.swarmPattern} workflow (${this.trajectory.agents.filter((a) => a.role !== 'workflow-runner').length} agents)`,
304
+ approach: this.buildApproach(),
322
305
  confidence,
323
306
  learnings: meta?.learnings,
324
307
  challenges: meta?.challenges,
325
- };
326
- this.closeCurrentChapter();
308
+ });
327
309
  await this.flush();
328
- await this.moveToCompleted();
329
310
  }
330
- /** Abandon the trajectory. */
331
311
  async abandon(reason, meta) {
332
312
  if (!this.enabled || !this.trajectory)
333
313
  return;
334
- const elapsed = Date.now() - this.startTime;
335
- const elapsedStr = elapsed > 60_000 ? `${Math.round(elapsed / 60_000)} minutes` : `${Math.round(elapsed / 1_000)} seconds`;
336
314
  const summary = meta?.summary ?? `Workflow abandoned: ${reason}`;
337
- this.openRetrospective();
338
- this.addEvent('reflection', `${summary} (abandoned after ${elapsedStr})`, 'high');
315
+ this.openChapter('Retrospective', 'orchestrator');
316
+ this.addEvent('reflection', `${summary} (abandoned after ${formatElapsed(Date.now() - this.startTime, true)})`, 'high');
339
317
  this.addEvent('error', `Workflow abandoned: ${reason}`, 'high');
340
- this.trajectory.status = 'abandoned';
341
- this.trajectory.completedAt = new Date().toISOString();
342
- this.trajectory.retrospective = {
343
- summary,
344
- approach: `${this.swarmPattern} workflow (${this.trajectory.agents.filter((a) => a.role !== 'workflow-runner').length} agents)`,
345
- confidence: meta?.confidence ?? 0,
346
- learnings: meta?.learnings,
347
- challenges: meta?.challenges,
318
+ this.trajectory = {
319
+ ...abandonTrajectory(this.trajectory),
320
+ retrospective: {
321
+ summary,
322
+ approach: this.buildApproach(),
323
+ confidence: meta?.confidence ?? 0,
324
+ learnings: meta?.learnings,
325
+ challenges: meta?.challenges,
326
+ },
348
327
  };
349
- this.closeCurrentChapter();
350
328
  await this.flush();
351
- await this.moveToCompleted();
352
329
  }
353
- // ── Getters ────────────────────────────────────────────────────────────────
354
330
  isEnabled() {
355
331
  return this.enabled;
356
332
  }
@@ -363,129 +339,33 @@ export class WorkflowTrajectory {
363
339
  getTrajectoryId() {
364
340
  return this.trajectory?.id ?? null;
365
341
  }
366
- // ── Synthesis helpers ──────────────────────────────────────────────────────
367
342
  buildSynthesis(label, outcomes, unblocks) {
368
- const completed = outcomes.filter((o) => o.status === 'completed');
369
- const failed = outcomes.filter((o) => o.status === 'failed');
370
- const retried = outcomes.filter((o) => o.attempts > 1 && o.status !== 'failed');
371
- const parts = [`${label} resolved.`, `${completed.length}/${outcomes.length} steps completed.`];
372
- if (failed.length > 0) {
373
- parts.push(`${failed.length} step(s) failed: ${failed.map((s) => s.name).join(', ')}.`);
374
- }
375
- if (retried.length > 0) {
376
- parts.push(`${retried.length} step(s) required retries: ${retried.map((s) => s.name).join(', ')}.`);
377
- }
378
- else if (failed.length === 0) {
379
- parts.push('All steps completed on first attempt.');
380
- }
381
- if (unblocks?.length) {
382
- parts.push(`Unblocking: ${unblocks.join(', ')}.`);
383
- }
384
- return parts.join(' ');
343
+ return buildSynthesis(label, outcomes, unblocks);
385
344
  }
386
345
  computeConfidence(outcomes) {
387
- if (outcomes.length === 0)
388
- return 0.7;
389
- const total = outcomes.length;
390
- const completed = outcomes.filter((o) => o.status === 'completed').length;
391
- const firstAttempt = outcomes.filter((o) => o.attempts === 1 && o.status === 'completed').length;
392
- const verified = outcomes.filter((o) => o.verificationPassed).length;
393
- // Base: 0.5 scaled by completion rate, +0.25 for first-attempt, +0.25 for verified
394
- const completionRate = completed / total;
395
- return Math.min(1.0, 0.5 * completionRate + (firstAttempt / total) * 0.25 + (verified / total) * 0.25);
346
+ return computeConfidence(outcomes);
396
347
  }
397
348
  buildRunSummary(outcomes) {
398
- const completed = outcomes.filter((o) => o.status === 'completed');
399
- const failed = outcomes.filter((o) => o.status === 'failed');
400
- const skipped = outcomes.filter((o) => o.status === 'skipped');
401
- const elapsed = Date.now() - this.startTime;
402
- const elapsedStr = elapsed > 60_000 ? `${Math.round(elapsed / 60_000)}min` : `${Math.round(elapsed / 1_000)}s`;
403
- if (failed.length === 0) {
404
- const retried = completed.filter((o) => o.attempts > 1);
405
- const base = `All ${completed.length} steps completed in ${elapsedStr}.`;
406
- return retried.length > 0
407
- ? `${base} ${retried.length} step(s) needed retries: ${retried.map((o) => o.name).join(', ')}.`
408
- : base;
409
- }
410
- // Failure narrative — focus on root cause of the first failure
411
- const firstFailure = failed[0];
412
- const cause = classifyFailure(firstFailure.error ?? '');
413
- const cascaded = skipped.length > 0
414
- ? ` Caused ${skipped.length} downstream step(s) to be skipped: ${skipped.map((o) => o.name).join(', ')}.`
415
- : '';
416
- return (`Failed at "${firstFailure.name}" [${cause}] after ${elapsedStr}.${cascaded} ` +
417
- `${completed.length}/${outcomes.length} steps completed before failure.`);
349
+ return buildRunSummary(outcomes, this.startTime);
418
350
  }
419
351
  extractLearnings(outcomes) {
420
- const learnings = [];
421
- const timeouts = outcomes.filter((o) => o.status === 'failed' && classifyFailure(o.error ?? '') === 'timeout');
422
- if (timeouts.some((o) => o.nonInteractive)) {
423
- learnings.push(`Non-interactive agent timeouts detected (${timeouts.map((o) => o.name).join(', ')}). ` +
424
- `Use deterministic steps to pre-read files and inject content — non-interactive agents should not discover information via tools.`);
425
- }
426
- const verifyFails = outcomes.filter((o) => o.status === 'failed' && classifyFailure(o.error ?? '') === 'verification_mismatch');
427
- if (verifyFails.length > 0) {
428
- learnings.push(`Verification mismatch on: ${verifyFails.map((o) => `"${o.name}" (expected "${o.verificationValue ?? '?'}")`).join(', ')}. ` +
429
- `Make the required output format more explicit in the task prompt.`);
430
- }
431
- const retried = outcomes.filter((o) => o.attempts > 1 && o.status === 'completed');
432
- if (retried.length > 0) {
433
- learnings.push(`${retried.map((o) => `"${o.name}" (${o.attempts} attempts)`).join(', ')} succeeded after retries — ` +
434
- `consider adding clearer output instructions to reduce retries.`);
435
- }
436
- return learnings;
352
+ return extractLearnings(outcomes);
437
353
  }
438
354
  extractChallenges(outcomes) {
439
- const challenges = [];
440
- const failed = outcomes.filter((o) => o.status === 'failed');
441
- for (const step of failed) {
442
- const cause = classifyFailure(step.error ?? '');
443
- challenges.push(diagnosisFor(cause, step));
444
- }
445
- return challenges;
355
+ return extractChallenges(outcomes);
446
356
  }
447
- // ── Internal helpers ───────────────────────────────────────────────────────
448
357
  openChapter(title, agentName) {
449
358
  if (!this.trajectory)
450
359
  return;
451
- const chapter = {
452
- id: `ch_${randomBytes(4).toString('hex')}`,
453
- title,
454
- agentName,
455
- startedAt: new Date().toISOString(),
456
- events: [],
457
- };
458
- this.trajectory.chapters.push(chapter);
459
- this.currentChapterId = chapter.id;
460
- }
461
- closeCurrentChapter() {
462
- if (!this.trajectory || !this.currentChapterId)
463
- return;
464
- const chapter = this.trajectory.chapters.find((c) => c.id === this.currentChapterId);
465
- if (chapter && !chapter.endedAt) {
466
- chapter.endedAt = new Date().toISOString();
467
- }
468
- this.currentChapterId = null;
360
+ this.trajectory = appendChapter(this.trajectory, { title, agentName });
469
361
  }
470
362
  addEvent(type, content, significance, raw) {
471
363
  if (!this.trajectory)
472
364
  return;
473
- // Find current chapter or create a default one
474
- let chapter = this.trajectory.chapters.find((c) => c.id === this.currentChapterId);
475
- if (!chapter) {
476
- this.openChapter('Execution', 'orchestrator');
477
- chapter = this.trajectory.chapters[this.trajectory.chapters.length - 1];
478
- }
479
- const event = {
480
- ts: Date.now(),
481
- type,
482
- content,
483
- };
484
- if (significance)
485
- event.significance = significance;
486
- if (raw)
487
- event.raw = raw;
488
- chapter.events.push(event);
365
+ this.trajectory = appendEvent(this.trajectory, { type, content, significance, raw });
366
+ }
367
+ buildApproach() {
368
+ return `${this.swarmPattern} workflow (${this.trajectory?.agents.filter((a) => a.role !== 'workflow-runner').length ?? 0} agents)`;
489
369
  }
490
370
  formatCompletionEvidenceSummary(evidence) {
491
371
  if (!evidence)
@@ -503,34 +383,20 @@ export class WorkflowTrajectory {
503
383
  parts.push(`exit=${evidence.exitCode}`);
504
384
  return parts.length > 0 ? parts.join('; ') : undefined;
505
385
  }
506
- async flush() {
507
- if (!this.trajectory)
508
- return;
509
- try {
510
- const activeDir = path.join(this.dataDir, 'active');
511
- await mkdir(activeDir, { recursive: true });
512
- const filePath = path.join(activeDir, `${this.trajectory.id}.json`);
513
- await writeFile(filePath, JSON.stringify(this.trajectory, null, 2), 'utf-8');
514
- }
515
- catch {
516
- // Non-blocking: trajectory recording failure should never break the workflow
517
- }
386
+ async ensureStorage() {
387
+ this.storage ??= new FileStorage(this.storageBaseDir);
388
+ this.storageInit ??= this.storage.initialize();
389
+ await this.storageInit;
518
390
  }
519
- async moveToCompleted() {
391
+ async flush() {
520
392
  if (!this.trajectory)
521
393
  return;
522
394
  try {
523
- const activeDir = path.join(this.dataDir, 'active');
524
- const completedDir = path.join(this.dataDir, 'completed');
525
- await mkdir(completedDir, { recursive: true });
526
- const activePath = path.join(activeDir, `${this.trajectory.id}.json`);
527
- const completedPath = path.join(completedDir, `${this.trajectory.id}.json`);
528
- if (existsSync(activePath)) {
529
- await rename(activePath, completedPath);
530
- }
395
+ await this.ensureStorage();
396
+ await this.storage?.save(this.trajectory);
531
397
  }
532
398
  catch {
533
- // Non-blocking
399
+ // non-blocking: flush failures must never break the workflow
534
400
  }
535
401
  }
536
402
  }