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.
- package/.env.example +44 -0
- package/LICENSE +21 -0
- package/README.md +795 -0
- package/dist/agent/agent-manager.d.ts +69 -0
- package/dist/agent/agent-manager.js +441 -0
- package/dist/agent/assistant.d.ts +225 -0
- package/dist/agent/assistant.js +3888 -0
- package/dist/agent/auto-update.d.ts +32 -0
- package/dist/agent/auto-update.js +186 -0
- package/dist/agent/daily-planner.d.ts +24 -0
- package/dist/agent/daily-planner.js +379 -0
- package/dist/agent/execution-advisor.d.ts +10 -0
- package/dist/agent/execution-advisor.js +272 -0
- package/dist/agent/hooks.d.ts +45 -0
- package/dist/agent/hooks.js +564 -0
- package/dist/agent/insight-engine.d.ts +66 -0
- package/dist/agent/insight-engine.js +225 -0
- package/dist/agent/intent-classifier.d.ts +48 -0
- package/dist/agent/intent-classifier.js +214 -0
- package/dist/agent/link-extractor.d.ts +19 -0
- package/dist/agent/link-extractor.js +90 -0
- package/dist/agent/mcp-bridge.d.ts +62 -0
- package/dist/agent/mcp-bridge.js +435 -0
- package/dist/agent/metacognition.d.ts +66 -0
- package/dist/agent/metacognition.js +221 -0
- package/dist/agent/orchestrator.d.ts +81 -0
- package/dist/agent/orchestrator.js +790 -0
- package/dist/agent/profiles.d.ts +22 -0
- package/dist/agent/profiles.js +91 -0
- package/dist/agent/prompt-cache.d.ts +24 -0
- package/dist/agent/prompt-cache.js +68 -0
- package/dist/agent/prompt-evolver.d.ts +28 -0
- package/dist/agent/prompt-evolver.js +279 -0
- package/dist/agent/role-scaffolds.d.ts +28 -0
- package/dist/agent/role-scaffolds.js +433 -0
- package/dist/agent/safe-restart.d.ts +41 -0
- package/dist/agent/safe-restart.js +150 -0
- package/dist/agent/self-improve.d.ts +66 -0
- package/dist/agent/self-improve.js +1706 -0
- package/dist/agent/session-event-log.d.ts +114 -0
- package/dist/agent/session-event-log.js +233 -0
- package/dist/agent/skill-extractor.d.ts +72 -0
- package/dist/agent/skill-extractor.js +435 -0
- package/dist/agent/source-mods.d.ts +61 -0
- package/dist/agent/source-mods.js +230 -0
- package/dist/agent/source-preflight.d.ts +25 -0
- package/dist/agent/source-preflight.js +100 -0
- package/dist/agent/stall-guard.d.ts +62 -0
- package/dist/agent/stall-guard.js +109 -0
- package/dist/agent/strategic-planner.d.ts +60 -0
- package/dist/agent/strategic-planner.js +352 -0
- package/dist/agent/team-bus.d.ts +89 -0
- package/dist/agent/team-bus.js +556 -0
- package/dist/agent/team-router.d.ts +26 -0
- package/dist/agent/team-router.js +37 -0
- package/dist/agent/tool-loop-detector.d.ts +59 -0
- package/dist/agent/tool-loop-detector.js +242 -0
- package/dist/agent/workflow-runner.d.ts +36 -0
- package/dist/agent/workflow-runner.js +317 -0
- package/dist/agent/workflow-variables.d.ts +16 -0
- package/dist/agent/workflow-variables.js +62 -0
- package/dist/channels/discord-agent-bot.d.ts +101 -0
- package/dist/channels/discord-agent-bot.js +881 -0
- package/dist/channels/discord-bot-manager.d.ts +80 -0
- package/dist/channels/discord-bot-manager.js +262 -0
- package/dist/channels/discord-utils.d.ts +51 -0
- package/dist/channels/discord-utils.js +293 -0
- package/dist/channels/discord.d.ts +12 -0
- package/dist/channels/discord.js +1832 -0
- package/dist/channels/slack-agent-bot.d.ts +73 -0
- package/dist/channels/slack-agent-bot.js +320 -0
- package/dist/channels/slack-bot-manager.d.ts +66 -0
- package/dist/channels/slack-bot-manager.js +236 -0
- package/dist/channels/slack-utils.d.ts +39 -0
- package/dist/channels/slack-utils.js +189 -0
- package/dist/channels/slack.d.ts +11 -0
- package/dist/channels/slack.js +196 -0
- package/dist/channels/telegram.d.ts +10 -0
- package/dist/channels/telegram.js +235 -0
- package/dist/channels/webhook.d.ts +9 -0
- package/dist/channels/webhook.js +78 -0
- package/dist/channels/whatsapp.d.ts +11 -0
- package/dist/channels/whatsapp.js +181 -0
- package/dist/cli/chat.d.ts +14 -0
- package/dist/cli/chat.js +220 -0
- package/dist/cli/cron.d.ts +17 -0
- package/dist/cli/cron.js +552 -0
- package/dist/cli/dashboard.d.ts +15 -0
- package/dist/cli/dashboard.js +17677 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +2474 -0
- package/dist/cli/routes/delegations.d.ts +19 -0
- package/dist/cli/routes/delegations.js +154 -0
- package/dist/cli/routes/digest.d.ts +17 -0
- package/dist/cli/routes/digest.js +375 -0
- package/dist/cli/routes/goals.d.ts +14 -0
- package/dist/cli/routes/goals.js +258 -0
- package/dist/cli/routes/workflows.d.ts +18 -0
- package/dist/cli/routes/workflows.js +97 -0
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.js +619 -0
- package/dist/cli/tunnel.d.ts +35 -0
- package/dist/cli/tunnel.js +141 -0
- package/dist/config.d.ts +145 -0
- package/dist/config.js +278 -0
- package/dist/events/bus.d.ts +43 -0
- package/dist/events/bus.js +136 -0
- package/dist/gateway/cron-scheduler.d.ts +166 -0
- package/dist/gateway/cron-scheduler.js +1767 -0
- package/dist/gateway/delivery-queue.d.ts +30 -0
- package/dist/gateway/delivery-queue.js +110 -0
- package/dist/gateway/heartbeat-scheduler.d.ts +99 -0
- package/dist/gateway/heartbeat-scheduler.js +1298 -0
- package/dist/gateway/heartbeat.d.ts +3 -0
- package/dist/gateway/heartbeat.js +3 -0
- package/dist/gateway/lanes.d.ts +24 -0
- package/dist/gateway/lanes.js +76 -0
- package/dist/gateway/notifications.d.ts +29 -0
- package/dist/gateway/notifications.js +75 -0
- package/dist/gateway/router.d.ts +210 -0
- package/dist/gateway/router.js +1330 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +1015 -0
- package/dist/memory/chunker.d.ts +28 -0
- package/dist/memory/chunker.js +226 -0
- package/dist/memory/consolidation.d.ts +44 -0
- package/dist/memory/consolidation.js +171 -0
- package/dist/memory/context-assembler.d.ts +50 -0
- package/dist/memory/context-assembler.js +149 -0
- package/dist/memory/embeddings.d.ts +38 -0
- package/dist/memory/embeddings.js +180 -0
- package/dist/memory/graph-store.d.ts +66 -0
- package/dist/memory/graph-store.js +613 -0
- package/dist/memory/mmr.d.ts +21 -0
- package/dist/memory/mmr.js +75 -0
- package/dist/memory/search.d.ts +26 -0
- package/dist/memory/search.js +67 -0
- package/dist/memory/store.d.ts +530 -0
- package/dist/memory/store.js +2022 -0
- package/dist/security/integrity.d.ts +24 -0
- package/dist/security/integrity.js +58 -0
- package/dist/security/patterns.d.ts +34 -0
- package/dist/security/patterns.js +110 -0
- package/dist/security/scanner.d.ts +32 -0
- package/dist/security/scanner.js +263 -0
- package/dist/tools/admin-tools.d.ts +12 -0
- package/dist/tools/admin-tools.js +1278 -0
- package/dist/tools/external-tools.d.ts +11 -0
- package/dist/tools/external-tools.js +1327 -0
- package/dist/tools/goal-tools.d.ts +9 -0
- package/dist/tools/goal-tools.js +159 -0
- package/dist/tools/mcp-server.d.ts +13 -0
- package/dist/tools/mcp-server.js +141 -0
- package/dist/tools/memory-tools.d.ts +10 -0
- package/dist/tools/memory-tools.js +568 -0
- package/dist/tools/session-tools.d.ts +6 -0
- package/dist/tools/session-tools.js +146 -0
- package/dist/tools/shared.d.ts +216 -0
- package/dist/tools/shared.js +340 -0
- package/dist/tools/team-tools.d.ts +6 -0
- package/dist/tools/team-tools.js +447 -0
- package/dist/tools/tool-meta.d.ts +34 -0
- package/dist/tools/tool-meta.js +133 -0
- package/dist/tools/vault-tools.d.ts +8 -0
- package/dist/tools/vault-tools.js +457 -0
- package/dist/types.d.ts +716 -0
- package/dist/types.js +16 -0
- package/dist/vault-migrations/0001-add-execution-framework.d.ts +10 -0
- package/dist/vault-migrations/0001-add-execution-framework.js +47 -0
- package/dist/vault-migrations/0002-add-agentic-communication.d.ts +12 -0
- package/dist/vault-migrations/0002-add-agentic-communication.js +79 -0
- package/dist/vault-migrations/0003-update-execution-pipeline-narration.d.ts +11 -0
- package/dist/vault-migrations/0003-update-execution-pipeline-narration.js +73 -0
- package/dist/vault-migrations/helpers.d.ts +14 -0
- package/dist/vault-migrations/helpers.js +44 -0
- package/dist/vault-migrations/runner.d.ts +14 -0
- package/dist/vault-migrations/runner.js +139 -0
- package/dist/vault-migrations/types.d.ts +42 -0
- package/dist/vault-migrations/types.js +9 -0
- package/install.sh +320 -0
- package/package.json +84 -0
- package/scripts/postinstall.js +125 -0
- package/vault/00-System/AGENTS.md +66 -0
- package/vault/00-System/CRON.md +71 -0
- package/vault/00-System/HEARTBEAT.md +58 -0
- package/vault/00-System/MEMORY.md +16 -0
- package/vault/00-System/SOUL.md +96 -0
- package/vault/05-Tasks/TASKS.md +19 -0
- package/vault/06-Templates/_Daily-Template.md +28 -0
- 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
|