clementine-agent 1.0.0

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 (190) hide show
  1. package/.env.example +44 -0
  2. package/LICENSE +21 -0
  3. package/README.md +795 -0
  4. package/dist/agent/agent-manager.d.ts +69 -0
  5. package/dist/agent/agent-manager.js +441 -0
  6. package/dist/agent/assistant.d.ts +225 -0
  7. package/dist/agent/assistant.js +3888 -0
  8. package/dist/agent/auto-update.d.ts +32 -0
  9. package/dist/agent/auto-update.js +186 -0
  10. package/dist/agent/daily-planner.d.ts +24 -0
  11. package/dist/agent/daily-planner.js +379 -0
  12. package/dist/agent/execution-advisor.d.ts +10 -0
  13. package/dist/agent/execution-advisor.js +272 -0
  14. package/dist/agent/hooks.d.ts +45 -0
  15. package/dist/agent/hooks.js +564 -0
  16. package/dist/agent/insight-engine.d.ts +66 -0
  17. package/dist/agent/insight-engine.js +225 -0
  18. package/dist/agent/intent-classifier.d.ts +48 -0
  19. package/dist/agent/intent-classifier.js +214 -0
  20. package/dist/agent/link-extractor.d.ts +19 -0
  21. package/dist/agent/link-extractor.js +90 -0
  22. package/dist/agent/mcp-bridge.d.ts +62 -0
  23. package/dist/agent/mcp-bridge.js +435 -0
  24. package/dist/agent/metacognition.d.ts +66 -0
  25. package/dist/agent/metacognition.js +221 -0
  26. package/dist/agent/orchestrator.d.ts +81 -0
  27. package/dist/agent/orchestrator.js +790 -0
  28. package/dist/agent/profiles.d.ts +22 -0
  29. package/dist/agent/profiles.js +91 -0
  30. package/dist/agent/prompt-cache.d.ts +24 -0
  31. package/dist/agent/prompt-cache.js +68 -0
  32. package/dist/agent/prompt-evolver.d.ts +28 -0
  33. package/dist/agent/prompt-evolver.js +279 -0
  34. package/dist/agent/role-scaffolds.d.ts +28 -0
  35. package/dist/agent/role-scaffolds.js +433 -0
  36. package/dist/agent/safe-restart.d.ts +41 -0
  37. package/dist/agent/safe-restart.js +150 -0
  38. package/dist/agent/self-improve.d.ts +66 -0
  39. package/dist/agent/self-improve.js +1706 -0
  40. package/dist/agent/session-event-log.d.ts +114 -0
  41. package/dist/agent/session-event-log.js +233 -0
  42. package/dist/agent/skill-extractor.d.ts +72 -0
  43. package/dist/agent/skill-extractor.js +435 -0
  44. package/dist/agent/source-mods.d.ts +61 -0
  45. package/dist/agent/source-mods.js +230 -0
  46. package/dist/agent/source-preflight.d.ts +25 -0
  47. package/dist/agent/source-preflight.js +100 -0
  48. package/dist/agent/stall-guard.d.ts +62 -0
  49. package/dist/agent/stall-guard.js +109 -0
  50. package/dist/agent/strategic-planner.d.ts +60 -0
  51. package/dist/agent/strategic-planner.js +352 -0
  52. package/dist/agent/team-bus.d.ts +89 -0
  53. package/dist/agent/team-bus.js +556 -0
  54. package/dist/agent/team-router.d.ts +26 -0
  55. package/dist/agent/team-router.js +37 -0
  56. package/dist/agent/tool-loop-detector.d.ts +59 -0
  57. package/dist/agent/tool-loop-detector.js +242 -0
  58. package/dist/agent/workflow-runner.d.ts +36 -0
  59. package/dist/agent/workflow-runner.js +317 -0
  60. package/dist/agent/workflow-variables.d.ts +16 -0
  61. package/dist/agent/workflow-variables.js +62 -0
  62. package/dist/channels/discord-agent-bot.d.ts +101 -0
  63. package/dist/channels/discord-agent-bot.js +881 -0
  64. package/dist/channels/discord-bot-manager.d.ts +80 -0
  65. package/dist/channels/discord-bot-manager.js +262 -0
  66. package/dist/channels/discord-utils.d.ts +51 -0
  67. package/dist/channels/discord-utils.js +293 -0
  68. package/dist/channels/discord.d.ts +12 -0
  69. package/dist/channels/discord.js +1832 -0
  70. package/dist/channels/slack-agent-bot.d.ts +73 -0
  71. package/dist/channels/slack-agent-bot.js +320 -0
  72. package/dist/channels/slack-bot-manager.d.ts +66 -0
  73. package/dist/channels/slack-bot-manager.js +236 -0
  74. package/dist/channels/slack-utils.d.ts +39 -0
  75. package/dist/channels/slack-utils.js +189 -0
  76. package/dist/channels/slack.d.ts +11 -0
  77. package/dist/channels/slack.js +196 -0
  78. package/dist/channels/telegram.d.ts +10 -0
  79. package/dist/channels/telegram.js +235 -0
  80. package/dist/channels/webhook.d.ts +9 -0
  81. package/dist/channels/webhook.js +78 -0
  82. package/dist/channels/whatsapp.d.ts +11 -0
  83. package/dist/channels/whatsapp.js +181 -0
  84. package/dist/cli/chat.d.ts +14 -0
  85. package/dist/cli/chat.js +220 -0
  86. package/dist/cli/cron.d.ts +17 -0
  87. package/dist/cli/cron.js +552 -0
  88. package/dist/cli/dashboard.d.ts +15 -0
  89. package/dist/cli/dashboard.js +17677 -0
  90. package/dist/cli/index.d.ts +3 -0
  91. package/dist/cli/index.js +2474 -0
  92. package/dist/cli/routes/delegations.d.ts +19 -0
  93. package/dist/cli/routes/delegations.js +154 -0
  94. package/dist/cli/routes/digest.d.ts +17 -0
  95. package/dist/cli/routes/digest.js +375 -0
  96. package/dist/cli/routes/goals.d.ts +14 -0
  97. package/dist/cli/routes/goals.js +258 -0
  98. package/dist/cli/routes/workflows.d.ts +18 -0
  99. package/dist/cli/routes/workflows.js +97 -0
  100. package/dist/cli/setup.d.ts +8 -0
  101. package/dist/cli/setup.js +619 -0
  102. package/dist/cli/tunnel.d.ts +35 -0
  103. package/dist/cli/tunnel.js +141 -0
  104. package/dist/config.d.ts +145 -0
  105. package/dist/config.js +278 -0
  106. package/dist/events/bus.d.ts +43 -0
  107. package/dist/events/bus.js +136 -0
  108. package/dist/gateway/cron-scheduler.d.ts +166 -0
  109. package/dist/gateway/cron-scheduler.js +1767 -0
  110. package/dist/gateway/delivery-queue.d.ts +30 -0
  111. package/dist/gateway/delivery-queue.js +110 -0
  112. package/dist/gateway/heartbeat-scheduler.d.ts +99 -0
  113. package/dist/gateway/heartbeat-scheduler.js +1298 -0
  114. package/dist/gateway/heartbeat.d.ts +3 -0
  115. package/dist/gateway/heartbeat.js +3 -0
  116. package/dist/gateway/lanes.d.ts +24 -0
  117. package/dist/gateway/lanes.js +76 -0
  118. package/dist/gateway/notifications.d.ts +29 -0
  119. package/dist/gateway/notifications.js +75 -0
  120. package/dist/gateway/router.d.ts +210 -0
  121. package/dist/gateway/router.js +1330 -0
  122. package/dist/index.d.ts +12 -0
  123. package/dist/index.js +1015 -0
  124. package/dist/memory/chunker.d.ts +28 -0
  125. package/dist/memory/chunker.js +226 -0
  126. package/dist/memory/consolidation.d.ts +44 -0
  127. package/dist/memory/consolidation.js +171 -0
  128. package/dist/memory/context-assembler.d.ts +50 -0
  129. package/dist/memory/context-assembler.js +149 -0
  130. package/dist/memory/embeddings.d.ts +38 -0
  131. package/dist/memory/embeddings.js +180 -0
  132. package/dist/memory/graph-store.d.ts +66 -0
  133. package/dist/memory/graph-store.js +613 -0
  134. package/dist/memory/mmr.d.ts +21 -0
  135. package/dist/memory/mmr.js +75 -0
  136. package/dist/memory/search.d.ts +26 -0
  137. package/dist/memory/search.js +67 -0
  138. package/dist/memory/store.d.ts +530 -0
  139. package/dist/memory/store.js +2022 -0
  140. package/dist/security/integrity.d.ts +24 -0
  141. package/dist/security/integrity.js +58 -0
  142. package/dist/security/patterns.d.ts +34 -0
  143. package/dist/security/patterns.js +110 -0
  144. package/dist/security/scanner.d.ts +32 -0
  145. package/dist/security/scanner.js +263 -0
  146. package/dist/tools/admin-tools.d.ts +12 -0
  147. package/dist/tools/admin-tools.js +1278 -0
  148. package/dist/tools/external-tools.d.ts +11 -0
  149. package/dist/tools/external-tools.js +1327 -0
  150. package/dist/tools/goal-tools.d.ts +9 -0
  151. package/dist/tools/goal-tools.js +159 -0
  152. package/dist/tools/mcp-server.d.ts +13 -0
  153. package/dist/tools/mcp-server.js +141 -0
  154. package/dist/tools/memory-tools.d.ts +10 -0
  155. package/dist/tools/memory-tools.js +568 -0
  156. package/dist/tools/session-tools.d.ts +6 -0
  157. package/dist/tools/session-tools.js +146 -0
  158. package/dist/tools/shared.d.ts +216 -0
  159. package/dist/tools/shared.js +340 -0
  160. package/dist/tools/team-tools.d.ts +6 -0
  161. package/dist/tools/team-tools.js +447 -0
  162. package/dist/tools/tool-meta.d.ts +34 -0
  163. package/dist/tools/tool-meta.js +133 -0
  164. package/dist/tools/vault-tools.d.ts +8 -0
  165. package/dist/tools/vault-tools.js +457 -0
  166. package/dist/types.d.ts +716 -0
  167. package/dist/types.js +16 -0
  168. package/dist/vault-migrations/0001-add-execution-framework.d.ts +10 -0
  169. package/dist/vault-migrations/0001-add-execution-framework.js +47 -0
  170. package/dist/vault-migrations/0002-add-agentic-communication.d.ts +12 -0
  171. package/dist/vault-migrations/0002-add-agentic-communication.js +79 -0
  172. package/dist/vault-migrations/0003-update-execution-pipeline-narration.d.ts +11 -0
  173. package/dist/vault-migrations/0003-update-execution-pipeline-narration.js +73 -0
  174. package/dist/vault-migrations/helpers.d.ts +14 -0
  175. package/dist/vault-migrations/helpers.js +44 -0
  176. package/dist/vault-migrations/runner.d.ts +14 -0
  177. package/dist/vault-migrations/runner.js +139 -0
  178. package/dist/vault-migrations/types.d.ts +42 -0
  179. package/dist/vault-migrations/types.js +9 -0
  180. package/install.sh +320 -0
  181. package/package.json +84 -0
  182. package/scripts/postinstall.js +125 -0
  183. package/vault/00-System/AGENTS.md +66 -0
  184. package/vault/00-System/CRON.md +71 -0
  185. package/vault/00-System/HEARTBEAT.md +58 -0
  186. package/vault/00-System/MEMORY.md +16 -0
  187. package/vault/00-System/SOUL.md +96 -0
  188. package/vault/05-Tasks/TASKS.md +19 -0
  189. package/vault/06-Templates/_Daily-Template.md +28 -0
  190. package/vault/06-Templates/_People-Template.md +22 -0
@@ -0,0 +1,790 @@
1
+ /**
2
+ * Clementine TypeScript — Plan Orchestrator.
3
+ *
4
+ * Decomposes a task into steps, runs independent steps in parallel
5
+ * via concurrent query() calls, then synthesizes a final response.
6
+ */
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
8
+ import pino from 'pino';
9
+ import { PLAN_STATE_DIR } from '../config.js';
10
+ const logger = pino({ name: 'clementine.orchestrator' });
11
+ const MAX_STEPS = 10;
12
+ const MAX_CONCURRENT_STEPS = 3;
13
+ const RESULT_TRUNCATE_CHARS = 4000;
14
+ const LONG_PLAN_WARNING_MS = 30 * 60 * 1000; // 30 minutes
15
+ const ALLOWED_MODELS = ['haiku', 'sonnet'];
16
+ const PLANNER_PROMPT = `You are a task planner for an AI assistant. Decompose the following request into executable steps.
17
+
18
+ **Planning Principles:**
19
+ - Each step runs in a FRESH CONTEXT (separate sub-agent) — no context rot, peak quality
20
+ - Steps should be ATOMIC — completable in one focused session, not vague or open-ended
21
+ - MAXIMIZE PARALLELISM — independent steps run concurrently in separate contexts
22
+ - Follow Research → Execute → Verify — research steps feed into execution steps, verification confirms delivery
23
+ - Size for quality: each step should complete within 15-50 tool calls, not sprawl indefinitely
24
+
25
+ Output ONLY valid JSON matching this schema (no markdown fences, no prose):
26
+
27
+ {
28
+ "steps": [
29
+ {
30
+ "id": "step-1",
31
+ "description": "Short human-readable label",
32
+ "prompt": "Detailed, self-contained instructions for the sub-agent. Be specific — the agent has no prior conversation context. Include tool names to use, what to look for, and what output to produce. End with a clear deliverable: 'Deliver: ...'",
33
+ "dependsOn": [],
34
+ "maxTurns": 15,
35
+ "model": "sonnet"
36
+ }
37
+ ],
38
+ "synthesisPrompt": "Instructions for combining all step results into a final response"
39
+ }
40
+
41
+ Rules:
42
+ - MAXIMIZE PARALLELISM: if steps don't need each other's output, give them no dependencies
43
+ - Each step prompt must be SELF-CONTAINED — the sub-agent has memory/vault access but no prior conversation context
44
+ - Each step must end with a clear deliverable statement ("Deliver: the list of...", "Deliver: a draft email...")
45
+ - Set maxTurns based on complexity: simple lookup = 5, moderate task = 15, complex work = 30-50
46
+ - Set model based on step complexity: "haiku" for simple lookups/formatting, "sonnet" for reasoning/writing/analysis
47
+ - Keep step count between 2-8. Simple tasks = fewer steps. Complex tasks = more.
48
+ - If the task has a verification component, include it as a final dependent step
49
+ - The synthesis step combines everything — it should produce a coherent final message for the user
50
+
51
+ Available tools for sub-agents: Outlook (inbox, search, draft, send, calendar), memory (read/write/search), vault (notes, tasks), Bash, WebSearch, WebFetch, discord_channel_send, github_prs, rss_fetch, browser_screenshot, and file tools.
52
+
53
+ <user_request>
54
+ `;
55
+ const PLANNER_PROMPT_SUFFIX = `
56
+ </user_request>`;
57
+ /**
58
+ * Compute execution waves from a dependency graph via topological sort.
59
+ * Steps with empty dependsOn = wave 0. Steps whose deps are all in wave N = wave N+1.
60
+ */
61
+ export function computeWaves(steps) {
62
+ const stepMap = new Map(steps.map(s => [s.id, s]));
63
+ const waveOf = new Map();
64
+ const visiting = new Set(); // shared across all roots for cycle detection
65
+ function getWave(id) {
66
+ if (waveOf.has(id))
67
+ return waveOf.get(id);
68
+ if (visiting.has(id))
69
+ throw new Error(`Circular dependency detected involving step ${id}`);
70
+ visiting.add(id);
71
+ const step = stepMap.get(id);
72
+ if (!step || step.dependsOn.length === 0) {
73
+ visiting.delete(id);
74
+ waveOf.set(id, 0);
75
+ return 0;
76
+ }
77
+ let maxDepWave = 0;
78
+ for (const depId of step.dependsOn) {
79
+ if (!stepMap.has(depId))
80
+ continue; // unknown deps stripped during validation
81
+ maxDepWave = Math.max(maxDepWave, getWave(depId) + 1);
82
+ }
83
+ visiting.delete(id);
84
+ waveOf.set(id, maxDepWave);
85
+ return maxDepWave;
86
+ }
87
+ for (const step of steps) {
88
+ getWave(step.id);
89
+ }
90
+ // Group into waves
91
+ const maxWave = Math.max(0, ...waveOf.values());
92
+ const waves = Array.from({ length: maxWave + 1 }, () => []);
93
+ for (const step of steps) {
94
+ waves[waveOf.get(step.id) ?? 0].push(step);
95
+ }
96
+ return waves.filter(w => w.length > 0);
97
+ }
98
+ /**
99
+ * Run promises with a concurrency limit.
100
+ */
101
+ export async function settledWithLimit(tasks, limit) {
102
+ const results = new Array(tasks.length);
103
+ let idx = 0;
104
+ async function worker() {
105
+ while (idx < tasks.length) {
106
+ const i = idx++;
107
+ try {
108
+ results[i] = { status: 'fulfilled', value: await tasks[i]() };
109
+ }
110
+ catch (reason) {
111
+ results[i] = { status: 'rejected', reason };
112
+ }
113
+ }
114
+ }
115
+ const workers = Array.from({ length: Math.min(limit, tasks.length) }, () => worker());
116
+ await Promise.all(workers);
117
+ return results;
118
+ }
119
+ export class PlanOrchestrator {
120
+ assistant;
121
+ stepStatuses = new Map();
122
+ stepStartTimes = new Map();
123
+ startTime = 0;
124
+ stateId;
125
+ agentProfiles = new Map();
126
+ constructor(assistant) {
127
+ this.assistant = assistant;
128
+ this.stateId = `plan-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
129
+ }
130
+ // ── State persistence ────────────────────────────────────────────────
131
+ saveState(state) {
132
+ try {
133
+ if (!existsSync(PLAN_STATE_DIR))
134
+ mkdirSync(PLAN_STATE_DIR, { recursive: true });
135
+ state.updatedAt = new Date().toISOString();
136
+ writeFileSync(`${PLAN_STATE_DIR}/${state.id}.json`, JSON.stringify(state, null, 2));
137
+ }
138
+ catch (err) {
139
+ logger.debug({ err }, 'Failed to save plan state (non-fatal)');
140
+ }
141
+ }
142
+ cleanupState() {
143
+ try {
144
+ const filePath = `${PLAN_STATE_DIR}/${this.stateId}.json`;
145
+ if (existsSync(filePath))
146
+ unlinkSync(filePath);
147
+ }
148
+ catch { /* non-fatal */ }
149
+ }
150
+ /** Load a previously interrupted plan state (for future resumability). */
151
+ static loadState(stateId) {
152
+ try {
153
+ const filePath = `${PLAN_STATE_DIR}/${stateId}.json`;
154
+ if (!existsSync(filePath))
155
+ return null;
156
+ return JSON.parse(readFileSync(filePath, 'utf-8'));
157
+ }
158
+ catch {
159
+ return null;
160
+ }
161
+ }
162
+ /**
163
+ * Main entry: plan → approve → execute → synthesize → return final response.
164
+ */
165
+ async run(taskDescription, onProgress, onApproval, availableAgents) {
166
+ // Reset instance state for reuse safety
167
+ this.stepStatuses.clear();
168
+ this.stepStartTimes.clear();
169
+ this.agentProfiles.clear();
170
+ this.startTime = Date.now();
171
+ // Index available agents for delegation lookups
172
+ if (availableAgents) {
173
+ for (const agent of availableAgents) {
174
+ this.agentProfiles.set(agent.slug, agent);
175
+ }
176
+ }
177
+ const safeProgress = async (updates) => {
178
+ try {
179
+ await onProgress?.(updates);
180
+ }
181
+ catch (err) {
182
+ logger.warn({ err }, 'Progress callback error (non-fatal)');
183
+ }
184
+ };
185
+ // 1. Generate plan (with revision loop)
186
+ const MAX_REVISIONS = 3;
187
+ let revisionCount = 0;
188
+ let effectiveTask = taskDescription;
189
+ let plan;
190
+ let waves;
191
+ // Plan → approval → (optional revision) loop
192
+ planLoop: while (true) {
193
+ try {
194
+ plan = await this.generatePlan(effectiveTask, availableAgents);
195
+ }
196
+ catch (err) {
197
+ logger.warn({ err }, 'Plan generation failed — running as single step');
198
+ return this.runSingleStep(taskDescription);
199
+ }
200
+ // Enforce max steps
201
+ if (plan.steps.length > MAX_STEPS) {
202
+ plan.steps = plan.steps.slice(0, MAX_STEPS);
203
+ }
204
+ if (plan.steps.length === 0) {
205
+ logger.warn('Plan has no valid steps — running as single step');
206
+ return this.runSingleStep(taskDescription);
207
+ }
208
+ logger.info({ goal: effectiveTask, stepCount: plan.steps.length, steps: plan.steps.map(s => s.id), revision: revisionCount }, 'Plan generated');
209
+ // 2. Initialize statuses
210
+ this.stepStatuses.clear();
211
+ for (const step of plan.steps) {
212
+ this.stepStatuses.set(step.id, {
213
+ stepId: step.id,
214
+ status: 'waiting',
215
+ description: step.description,
216
+ });
217
+ }
218
+ await safeProgress(this.getAllUpdates());
219
+ // 3. Compute waves
220
+ try {
221
+ waves = computeWaves(plan.steps);
222
+ }
223
+ catch (err) {
224
+ logger.error({ err }, 'Dependency graph error');
225
+ return this.runSingleStep(taskDescription);
226
+ }
227
+ // 3b. Approval gate — show plan before executing
228
+ if (onApproval) {
229
+ const planSummary = waves
230
+ .map((wave, wi) => wave.map(s => ` [Wave ${wi + 1}] ${s.id}: ${s.description}`).join('\n'))
231
+ .join('\n');
232
+ const result = await onApproval(planSummary, plan.steps);
233
+ if (result === false) {
234
+ logger.info({ goal: taskDescription }, 'Plan cancelled by user');
235
+ return 'Plan cancelled.';
236
+ }
237
+ if (typeof result === 'string') {
238
+ // Revision feedback — regenerate the plan
239
+ revisionCount++;
240
+ if (revisionCount > MAX_REVISIONS) {
241
+ logger.warn({ goal: taskDescription, revisions: revisionCount }, 'Max plan revisions reached');
242
+ return 'Plan cancelled — too many revisions.';
243
+ }
244
+ logger.info({ goal: taskDescription, revision: revisionCount, feedback: result }, 'Plan revision requested');
245
+ effectiveTask = `${taskDescription}\n\n[Revision ${revisionCount}] The user reviewed the previous plan and asked for changes:\n${result}`;
246
+ continue planLoop;
247
+ }
248
+ }
249
+ break; // Approved — proceed to execution
250
+ }
251
+ // 4. Execute waves — with state persistence for resumability
252
+ const results = new Map();
253
+ let longPlanWarned = false;
254
+ // Wave summaries for cross-step learning: compact findings from completed waves
255
+ // injected into ALL subsequent steps (not just dependents)
256
+ let waveSummaryContext = '';
257
+ const state = {
258
+ id: this.stateId,
259
+ goal: taskDescription,
260
+ status: 'executing',
261
+ startedAt: new Date(this.startTime).toISOString(),
262
+ updatedAt: new Date().toISOString(),
263
+ plan: plan,
264
+ totalWaves: waves.length,
265
+ wavesCompleted: 0,
266
+ results: {},
267
+ errors: [],
268
+ retries: {},
269
+ };
270
+ this.saveState(state);
271
+ for (const wave of waves) {
272
+ // Mark running
273
+ for (const step of wave) {
274
+ this.stepStatuses.set(step.id, {
275
+ stepId: step.id,
276
+ status: 'running',
277
+ description: step.description,
278
+ });
279
+ this.stepStartTimes.set(step.id, Date.now());
280
+ }
281
+ await safeProgress(this.getAllUpdates());
282
+ // Run wave steps with concurrency limit
283
+ const settled = await settledWithLimit(wave.map((step) => async () => {
284
+ const prompt = this.buildStepPrompt(step, results, waveSummaryContext);
285
+ const delegateProfile = step.delegateTo ? this.agentProfiles.get(step.delegateTo) : undefined;
286
+ if (step.delegateTo) {
287
+ logger.info({ stepId: step.id, delegateTo: step.delegateTo }, 'Delegating step to specialist agent');
288
+ }
289
+ const result = await this.assistant.runPlanStep(step.id, prompt, {
290
+ tier: step.tier ?? 2,
291
+ maxTurns: step.maxTurns ?? 15,
292
+ model: step.model,
293
+ delegateProfile,
294
+ });
295
+ return { stepId: step.id, result };
296
+ }), MAX_CONCURRENT_STEPS);
297
+ // Collect results
298
+ for (let i = 0; i < wave.length; i++) {
299
+ const step = wave[i];
300
+ const outcome = settled[i];
301
+ const elapsed = Date.now() - (this.stepStartTimes.get(step.id) ?? this.startTime);
302
+ if (outcome.status === 'fulfilled') {
303
+ const resultText = outcome.value.result || '[No output produced]';
304
+ if (!outcome.value.result) {
305
+ logger.warn({ stepId: step.id }, 'Plan step produced empty output');
306
+ }
307
+ results.set(step.id, resultText);
308
+ this.stepStatuses.set(step.id, {
309
+ stepId: step.id,
310
+ status: 'done',
311
+ description: step.description,
312
+ durationMs: elapsed,
313
+ resultPreview: resultText.slice(0, 100),
314
+ });
315
+ }
316
+ else {
317
+ const errMsg = `[FAILED: ${outcome.reason}]`;
318
+ results.set(step.id, errMsg);
319
+ this.stepStatuses.set(step.id, {
320
+ stepId: step.id,
321
+ status: 'failed',
322
+ description: step.description,
323
+ durationMs: elapsed,
324
+ resultPreview: errMsg.slice(0, 100),
325
+ });
326
+ logger.error({ stepId: step.id, err: outcome.reason }, 'Plan step failed');
327
+ }
328
+ }
329
+ await safeProgress(this.getAllUpdates());
330
+ // Inter-wave spot-check with severity levels: critical issues trigger repair
331
+ const spotCheckIssues = this.spotCheckWaveResults(wave, results);
332
+ if (spotCheckIssues.length > 0) {
333
+ logger.warn({ issues: spotCheckIssues }, 'Spot-check found issues in wave results');
334
+ for (const issue of spotCheckIssues) {
335
+ if (issue.severity === 'critical' && this.hasDependents(issue.stepId, plan.steps)) {
336
+ // Critical issue on a step with dependents — attempt repair
337
+ const retryCount = state.retries[issue.stepId] ?? 0;
338
+ if (retryCount < 1) {
339
+ const decision = await this.getRepairDecision(issue, results.get(issue.stepId) ?? '');
340
+ if (decision === 'retry') {
341
+ logger.info({ stepId: issue.stepId, attempt: retryCount + 1 }, 'Retrying failed step');
342
+ state.retries[issue.stepId] = retryCount + 1;
343
+ const step = wave.find(s => s.id === issue.stepId);
344
+ try {
345
+ const retryPrompt = `[RETRY — Previous attempt failed: ${issue.issue}]\n\n` +
346
+ this.buildStepPrompt(step, results, waveSummaryContext);
347
+ const retryResult = await this.assistant.runPlanStep(step.id, retryPrompt, {
348
+ tier: step.tier ?? 2,
349
+ maxTurns: step.maxTurns ?? 15,
350
+ model: step.model,
351
+ });
352
+ results.set(step.id, retryResult || '[No output on retry]');
353
+ this.stepStatuses.set(step.id, {
354
+ stepId: step.id,
355
+ status: retryResult ? 'done' : 'failed',
356
+ description: step.description,
357
+ durationMs: Date.now() - (this.stepStartTimes.get(step.id) ?? this.startTime),
358
+ resultPreview: (retryResult || '[No output on retry]').slice(0, 100),
359
+ });
360
+ logger.info({ stepId: step.id, success: !!retryResult }, 'Step retry completed');
361
+ }
362
+ catch (err) {
363
+ logger.warn({ stepId: step.id, err }, 'Step retry also failed');
364
+ }
365
+ }
366
+ else if (decision === 'abort') {
367
+ logger.warn({ stepId: issue.stepId }, 'Repair decision: abort plan');
368
+ state.status = 'failed';
369
+ this.saveState(state);
370
+ return `Plan aborted — step "${this.stepStatuses.get(issue.stepId)?.description ?? issue.stepId}" failed critically and could not be repaired.`;
371
+ }
372
+ // 'skip' falls through — annotate and continue
373
+ }
374
+ }
375
+ // Annotate results so the synthesis step knows about issues
376
+ const existing = results.get(issue.stepId) ?? '';
377
+ results.set(issue.stepId, existing + `\n\n[SPOT-CHECK ${issue.severity.toUpperCase()}: ${issue.issue}]`);
378
+ }
379
+ }
380
+ // ── Cross-step learning: build wave summary ─────────────────
381
+ // Compact summary of this wave's findings, injected into ALL subsequent steps
382
+ const waveFindings = [];
383
+ for (const step of wave) {
384
+ const status = this.stepStatuses.get(step.id);
385
+ if (status?.status === 'done') {
386
+ const result = results.get(step.id) ?? '';
387
+ // Extract first substantive sentence (skip headers, blank lines)
388
+ const lines = result.split('\n').filter(l => l.trim() && !l.startsWith('#') && !l.startsWith('['));
389
+ const summary = lines[0]?.slice(0, 150) ?? '';
390
+ if (summary) {
391
+ waveFindings.push(`- ${step.description}: ${summary}`);
392
+ }
393
+ }
394
+ else if (status?.status === 'failed') {
395
+ waveFindings.push(`- ${step.description}: FAILED`);
396
+ }
397
+ }
398
+ if (waveFindings.length > 0) {
399
+ waveSummaryContext += (waveSummaryContext ? '\n' : '') + waveFindings.join('\n');
400
+ // Cap total wave summary to prevent prompt bloat
401
+ if (waveSummaryContext.length > 1500) {
402
+ waveSummaryContext = waveSummaryContext.slice(-1500);
403
+ }
404
+ }
405
+ // Persist state after each wave for resumability
406
+ state.wavesCompleted++;
407
+ state.results = Object.fromEntries(results);
408
+ for (const step of wave) {
409
+ const s = this.stepStatuses.get(step.id);
410
+ if (s?.status === 'failed') {
411
+ state.errors.push({ stepId: step.id, error: s.resultPreview ?? 'unknown' });
412
+ }
413
+ }
414
+ this.saveState(state);
415
+ // Long-running warning
416
+ if (!longPlanWarned && Date.now() - this.startTime > LONG_PLAN_WARNING_MS) {
417
+ logger.warn({ elapsed: Date.now() - this.startTime }, 'Plan has been running for 30+ minutes');
418
+ longPlanWarned = true;
419
+ }
420
+ }
421
+ // 5. Synthesis
422
+ const synthesisStepId = '__synthesis__';
423
+ this.stepStatuses.set(synthesisStepId, {
424
+ stepId: synthesisStepId,
425
+ status: 'running',
426
+ description: 'Synthesize final response',
427
+ });
428
+ this.stepStartTimes.set(synthesisStepId, Date.now());
429
+ await safeProgress(this.getAllUpdates());
430
+ const synthesisPrompt = this.buildSynthesisPrompt(plan, results);
431
+ let finalResult;
432
+ try {
433
+ finalResult = await this.assistant.runPlanStep(synthesisStepId, synthesisPrompt, {
434
+ tier: 2,
435
+ maxTurns: 5,
436
+ disableTools: true,
437
+ });
438
+ }
439
+ catch (err) {
440
+ logger.error({ err }, 'Synthesis step failed');
441
+ // Fallback: concatenate results
442
+ finalResult = Array.from(results.entries())
443
+ .map(([id, r]) => `**${this.stepStatuses.get(id)?.description ?? id}:**\n${r}`)
444
+ .join('\n\n');
445
+ }
446
+ const synthElapsed = Date.now() - (this.stepStartTimes.get(synthesisStepId) ?? this.startTime);
447
+ this.stepStatuses.set(synthesisStepId, {
448
+ stepId: synthesisStepId,
449
+ status: 'done',
450
+ description: 'Synthesize final response',
451
+ durationMs: synthElapsed,
452
+ });
453
+ await safeProgress(this.getAllUpdates());
454
+ const totalMs = Date.now() - this.startTime;
455
+ logger.info({ totalMs, steps: plan.steps.length }, 'Plan execution complete');
456
+ // Mark state as complete and clean up
457
+ state.status = 'complete';
458
+ this.saveState(state);
459
+ // Clean up state file on successful completion (it served its purpose)
460
+ this.cleanupState();
461
+ // 6. Post-synthesis reflection (async, non-blocking)
462
+ this.runReflection(taskDescription, finalResult).catch(err => {
463
+ logger.debug({ err }, 'Post-plan reflection failed (non-fatal)');
464
+ });
465
+ return finalResult;
466
+ }
467
+ /**
468
+ * Goal-backward verification pass using Haiku after plan synthesis.
469
+ * Verifies outcomes rather than just rating quality:
470
+ * - Did each step produce a real result?
471
+ * - Is the synthesized output substantive (not restating the question)?
472
+ * - Are there gaps between request and output?
473
+ */
474
+ async runReflection(taskDescription, output) {
475
+ if (!output || output.length < 50)
476
+ return;
477
+ // Build a step results summary for the verifier
478
+ const stepSummary = [...this.stepStatuses.entries()]
479
+ .filter(([id]) => id !== '__synthesis__')
480
+ .map(([_id, s]) => `- ${s.description}: ${s.status}${s.status === 'failed' ? ' FAILED' : ''}`)
481
+ .join('\n');
482
+ const reflectionPrompt = `Verify the outcome of this orchestrated plan using goal-backward verification.\n\n` +
483
+ `**Original request:** ${taskDescription.slice(0, 400)}\n\n` +
484
+ `**Step results:**\n${stepSummary}\n\n` +
485
+ `**Final output (first 1000 chars):** ${output.slice(0, 1000)}\n\n` +
486
+ `Verify:\n` +
487
+ `1. COMPLETENESS: Does the output address ALL parts of the original request? (not just the easy parts)\n` +
488
+ `2. SUBSTANCE: Is each claim backed by data/evidence? (not vague summaries or restating the question)\n` +
489
+ `3. WIRED: Are the step results actually connected in the synthesis? (not just concatenated)\n` +
490
+ `4. GAPS: What specific parts of the request were missed or under-addressed?\n\n` +
491
+ `Respond with ONLY a JSON object (no markdown):\n` +
492
+ `{"completeness": true/false, "substance": true/false, "wired": true/false, ` +
493
+ `"quality": 1-10, "gaps": "specific gaps or 'none'", "improvement": "one concrete thing to do differently"}`;
494
+ try {
495
+ const result = await this.assistant.runPlanStep('plan-reflection', reflectionPrompt, {
496
+ tier: 1,
497
+ maxTurns: 1,
498
+ model: 'haiku',
499
+ disableTools: true,
500
+ });
501
+ const jsonMatch = result.match(/\{[\s\S]*?\}/);
502
+ if (jsonMatch) {
503
+ const reflection = JSON.parse(jsonMatch[0]);
504
+ logger.info({
505
+ quality: reflection.quality,
506
+ completeness: reflection.completeness,
507
+ substance: reflection.substance,
508
+ wired: reflection.wired,
509
+ gaps: reflection.gaps?.slice(0, 100),
510
+ improvement: reflection.improvement?.slice(0, 100),
511
+ }, 'Plan reflection completed');
512
+ }
513
+ }
514
+ catch {
515
+ // Non-fatal — reflection is best-effort
516
+ }
517
+ }
518
+ /**
519
+ * Get formatted progress lines for display.
520
+ */
521
+ getProgressLines() {
522
+ const lines = [];
523
+ const entries = [...this.stepStatuses.values()];
524
+ const total = entries.length;
525
+ for (let i = 0; i < entries.length; i++) {
526
+ const u = entries[i];
527
+ const num = `[${i + 1}/${total}]`;
528
+ const elapsed = this.stepStartTimes.has(u.stepId)
529
+ ? Math.round((Date.now() - this.stepStartTimes.get(u.stepId)) / 1000)
530
+ : 0;
531
+ switch (u.status) {
532
+ case 'done':
533
+ lines.push(`${num} ${u.description} \u2713 (${Math.round((u.durationMs ?? 0) / 1000)}s)`);
534
+ break;
535
+ case 'running':
536
+ lines.push(`${num} ${u.description} \u23f3 running... (${elapsed}s)`);
537
+ break;
538
+ case 'failed':
539
+ lines.push(`${num} ${u.description} \u2717 failed (${Math.round((u.durationMs ?? 0) / 1000)}s)`);
540
+ break;
541
+ case 'waiting':
542
+ lines.push(`${num} ${u.description} \u25cb waiting`);
543
+ break;
544
+ }
545
+ }
546
+ return lines;
547
+ }
548
+ /**
549
+ * Inter-wave spot-check: verify that step results contain substance.
550
+ * Catches empty outputs, error-only results, and placeholder/stub responses
551
+ * before downstream waves try to build on them.
552
+ */
553
+ spotCheckWaveResults(wave, results) {
554
+ const issues = [];
555
+ for (const step of wave) {
556
+ const result = results.get(step.id) ?? '';
557
+ const status = this.stepStatuses.get(step.id);
558
+ // Skip already-failed steps
559
+ if (status?.status === 'failed')
560
+ continue;
561
+ // Check 1: Empty or near-empty output — critical (nothing to build on)
562
+ if (result.length < 20) {
563
+ issues.push({ stepId: step.id, issue: `Output is empty or trivial (${result.length} chars)`, severity: 'critical' });
564
+ continue;
565
+ }
566
+ // Check 2: Output is just an error message — critical
567
+ if (result.startsWith('[FAILED:') || result.startsWith('Error:') || result.startsWith('Something went wrong')) {
568
+ issues.push({ stepId: step.id, issue: 'Output appears to be an error, not a result', severity: 'critical' });
569
+ continue;
570
+ }
571
+ // Check 3: Stub detection — warning (might have partial content)
572
+ const stubPatterns = [
573
+ /^I('ll| will) (start|begin|look into|investigate|check)/i,
574
+ /^Let me (start|begin|look into|investigate|check)/i,
575
+ /^(Sure|OK|Alright),? (I'll|let me)/i,
576
+ ];
577
+ const firstLine = result.split('\n')[0];
578
+ if (stubPatterns.some(p => p.test(firstLine)) && result.length < 200) {
579
+ issues.push({ stepId: step.id, issue: 'Output appears to be a stub/placeholder (restates intent without delivering results)', severity: 'warning' });
580
+ }
581
+ }
582
+ return issues;
583
+ }
584
+ /** Check if a step has downstream dependents in the plan. */
585
+ hasDependents(stepId, steps) {
586
+ return steps.some(s => s.dependsOn.includes(stepId));
587
+ }
588
+ /** Fast Haiku call to decide how to repair a failed critical step. */
589
+ async getRepairDecision(issue, result) {
590
+ try {
591
+ const prompt = `A plan step failed during orchestration. Decide the best recovery action.\n\n` +
592
+ `Step: ${this.stepStatuses.get(issue.stepId)?.description ?? issue.stepId}\n` +
593
+ `Issue: ${issue.issue}\n` +
594
+ `Output (first 300 chars): ${result.slice(0, 300)}\n\n` +
595
+ `Options:\n` +
596
+ `- "retry": Try the step again with the error context appended (good if the failure seems transient or fixable)\n` +
597
+ `- "skip": Skip this step and let downstream steps work around the missing data (good if the step is non-essential)\n` +
598
+ `- "abort": Cancel the entire plan (good if this step is critical and can't be worked around)\n\n` +
599
+ `Respond with ONLY one word: retry, skip, or abort`;
600
+ const decision = await this.assistant.runPlanStep('repair-decision', prompt, {
601
+ tier: 1,
602
+ maxTurns: 1,
603
+ model: 'haiku',
604
+ disableTools: true,
605
+ });
606
+ const cleaned = decision.trim().toLowerCase();
607
+ if (cleaned.includes('retry'))
608
+ return 'retry';
609
+ if (cleaned.includes('abort'))
610
+ return 'abort';
611
+ return 'skip';
612
+ }
613
+ catch {
614
+ // If the decision call itself fails, default to skip
615
+ return 'skip';
616
+ }
617
+ }
618
+ // ── Private helpers ──────────────────────────────────────────────────
619
+ async generatePlan(task, availableAgents) {
620
+ // If team agents are available, inject their capabilities into the planner prompt
621
+ let agentContext = '';
622
+ if (availableAgents && availableAgents.length > 0) {
623
+ const agentLines = availableAgents.map(a => {
624
+ const tools = a.team?.allowedTools?.slice(0, 10).join(', ') || 'all tools';
625
+ return `- **${a.slug}**: ${a.description || a.name} (model: ${a.model || 'sonnet'}, tools: ${tools})`;
626
+ });
627
+ agentContext = `\n\n**Available Team Agents (delegate specialized steps to them via "delegateTo"):**\n${agentLines.join('\n')}\n` +
628
+ `If a step matches an agent's specialty, add "delegateTo": "agent-slug" to that step. ` +
629
+ `The delegated agent will run the step with their own personality, tools, and expertise.\n`;
630
+ }
631
+ const plannerResult = await this.assistant.runPlanStep('planner', PLANNER_PROMPT + agentContext + task + PLANNER_PROMPT_SUFFIX, { tier: 2, maxTurns: 1, model: 'sonnet', disableTools: true });
632
+ // Parse JSON from the planner response
633
+ const parsed = this.parseJsonFromResponse(plannerResult);
634
+ if (!parsed?.steps || !Array.isArray(parsed.steps) || parsed.steps.length === 0) {
635
+ throw new Error('Planner returned invalid plan structure');
636
+ }
637
+ // Validate and normalize steps
638
+ const seenIds = new Set();
639
+ const validStepIds = new Set(parsed.steps.map((s, i) => s.id || `step-${i + 1}`));
640
+ const steps = [];
641
+ for (let i = 0; i < parsed.steps.length; i++) {
642
+ const s = parsed.steps[i];
643
+ const id = s.id || `step-${i + 1}`;
644
+ // Skip duplicate IDs
645
+ if (seenIds.has(id)) {
646
+ logger.warn({ id }, 'Duplicate step ID — skipping');
647
+ continue;
648
+ }
649
+ seenIds.add(id);
650
+ // Skip empty prompts
651
+ const prompt = (s.prompt || '').trim();
652
+ if (!prompt) {
653
+ logger.warn({ id }, 'Step has empty prompt — skipping');
654
+ continue;
655
+ }
656
+ // Clean dependencies: remove self-refs and unknown refs
657
+ const deps = Array.isArray(s.dependsOn)
658
+ ? s.dependsOn.filter((d) => d !== id && validStepIds.has(d))
659
+ : [];
660
+ // Validate model
661
+ const model = ALLOWED_MODELS.includes(s.model) ? s.model : undefined;
662
+ // Validate delegateTo — must be one of the available agents
663
+ const delegateTo = (typeof s.delegateTo === 'string' && availableAgents?.some(a => a.slug === s.delegateTo))
664
+ ? s.delegateTo
665
+ : undefined;
666
+ steps.push({
667
+ id,
668
+ description: (s.description || `Step ${i + 1}`).slice(0, 80),
669
+ prompt,
670
+ dependsOn: deps,
671
+ maxTurns: Math.min(Math.max(s.maxTurns ?? 15, 1), 50),
672
+ tier: Math.min(s.tier ?? 2, 2), // Never exceed tier 2 from planner
673
+ model,
674
+ delegateTo,
675
+ });
676
+ }
677
+ if (steps.length === 0) {
678
+ throw new Error('No valid steps after validation');
679
+ }
680
+ return {
681
+ goal: task,
682
+ steps,
683
+ synthesisPrompt: parsed.synthesisPrompt || 'Combine all step results into a coherent response for the user.',
684
+ };
685
+ }
686
+ parseJsonFromResponse(text) {
687
+ // Try direct parse first
688
+ try {
689
+ return JSON.parse(text);
690
+ }
691
+ catch { /* fall through */ }
692
+ // Try extracting from markdown code fences
693
+ const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
694
+ if (fenceMatch) {
695
+ try {
696
+ return JSON.parse(fenceMatch[1]);
697
+ }
698
+ catch { /* fall through */ }
699
+ }
700
+ // Try finding the last balanced { ... } block containing "steps"
701
+ const lastBrace = text.lastIndexOf('}');
702
+ if (lastBrace !== -1) {
703
+ // Walk backward to find the matching opening brace
704
+ let depth = 0;
705
+ for (let i = lastBrace; i >= 0; i--) {
706
+ if (text[i] === '}')
707
+ depth++;
708
+ if (text[i] === '{')
709
+ depth--;
710
+ if (depth === 0) {
711
+ const candidate = text.slice(i, lastBrace + 1);
712
+ try {
713
+ const parsed = JSON.parse(candidate);
714
+ if (parsed?.steps)
715
+ return parsed;
716
+ }
717
+ catch { /* continue searching */ }
718
+ break;
719
+ }
720
+ }
721
+ }
722
+ return null;
723
+ }
724
+ buildStepPrompt(step, priorResults, waveSummaryContext) {
725
+ const parts = [];
726
+ // Cross-step learning: inject compact summary from ALL prior waves
727
+ // so this step benefits from earlier findings even without explicit dependencies
728
+ if (waveSummaryContext) {
729
+ parts.push('## Context from prior work\n' + waveSummaryContext);
730
+ }
731
+ // Inject prior step results for dependencies
732
+ if (step.dependsOn.length > 0) {
733
+ const depResults = [];
734
+ for (const depId of step.dependsOn) {
735
+ const result = priorResults.get(depId);
736
+ if (result) {
737
+ const desc = this.stepStatuses.get(depId)?.description ?? depId;
738
+ const isFailed = result.startsWith('[FAILED:');
739
+ const truncated = result.length > RESULT_TRUNCATE_CHARS
740
+ ? result.slice(0, RESULT_TRUNCATE_CHARS) + '\n...[truncated]'
741
+ : result;
742
+ if (isFailed) {
743
+ depResults.push(`### ${depId}: ${desc} (FAILED)\n${truncated}\n\n*This dependency failed — work around the missing data or note the gap.*`);
744
+ }
745
+ else {
746
+ depResults.push(`### ${depId}: ${desc}\n${truncated}`);
747
+ }
748
+ }
749
+ }
750
+ if (depResults.length > 0) {
751
+ parts.push('## Results from prior steps:\n' + depResults.join('\n\n'));
752
+ }
753
+ }
754
+ parts.push('## Your task:\n' + step.prompt);
755
+ // Agentic communication guidance for sub-agents
756
+ parts.push(`## Communication Style
757
+ Work through this task narrating your reasoning:
758
+ - After reading/searching, say what you found and what it means before acting.
759
+ - If something fails or is unexpected, explain what happened and what you'll try instead.
760
+ - End with a clear deliverable — the concrete output, not a promise to produce one.`);
761
+ return parts.join('\n\n');
762
+ }
763
+ buildSynthesisPrompt(plan, results) {
764
+ const parts = [
765
+ `## Original request:\n${plan.goal}`,
766
+ '',
767
+ '## Step results:',
768
+ ];
769
+ for (const step of plan.steps) {
770
+ const result = results.get(step.id) ?? '[no result]';
771
+ const truncated = result.length > RESULT_TRUNCATE_CHARS
772
+ ? result.slice(0, RESULT_TRUNCATE_CHARS) + '\n...[truncated]'
773
+ : result;
774
+ parts.push(`### ${step.id}: ${step.description}\n${truncated}`);
775
+ }
776
+ parts.push('', `## Instructions:\n${plan.synthesisPrompt}`);
777
+ parts.push('\nWrite a coherent final response for the user. Be concise and conversational — this is going to a Discord DM.');
778
+ return parts.join('\n');
779
+ }
780
+ async runSingleStep(task) {
781
+ return this.assistant.runPlanStep('fallback', task, {
782
+ tier: 2,
783
+ maxTurns: 25,
784
+ });
785
+ }
786
+ getAllUpdates() {
787
+ return [...this.stepStatuses.values()];
788
+ }
789
+ }
790
+ //# sourceMappingURL=orchestrator.js.map