minovative-mind-cli 1.5.0 → 2.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/README.md +51 -45
- package/dist/commands/chat.js +10 -5
- package/dist/services/agent/slashCommands.js +163 -37
- package/dist/services/agent/toolLoop.d.ts +1 -1
- package/dist/services/agent/toolLoop.js +7 -2
- package/dist/services/agent/types.d.ts +2 -0
- package/dist/services/agent-tools.d.ts +9 -4
- package/dist/services/agent-tools.js +145 -21
- package/dist/services/agent.d.ts +8 -0
- package/dist/services/agent.js +294 -38
- package/dist/services/ai.d.ts +19 -5
- package/dist/services/ai.js +167 -35
- package/dist/services/changeLogger.d.ts +142 -0
- package/dist/services/changeLogger.js +132 -3
- package/dist/services/contextAgent.d.ts +6 -1
- package/dist/services/contextAgent.js +95 -14
- package/dist/services/embeddingIndex.d.ts +82 -0
- package/dist/services/embeddingIndex.js +613 -0
- package/dist/services/investigationComplexity.d.ts +45 -0
- package/dist/services/investigationComplexity.js +91 -0
- package/dist/services/metrics.d.ts +18 -0
- package/dist/services/metrics.js +7 -0
- package/dist/services/orchestration/fileLockRegistry.d.ts +125 -0
- package/dist/services/orchestration/fileLockRegistry.js +276 -0
- package/dist/services/orchestration/investigationAgent.d.ts +85 -0
- package/dist/services/orchestration/investigationAgent.js +359 -0
- package/dist/services/orchestration/investigationOrchestrator.d.ts +53 -0
- package/dist/services/orchestration/investigationOrchestrator.js +180 -0
- package/dist/services/orchestration/messageBus.d.ts +162 -0
- package/dist/services/orchestration/messageBus.js +225 -0
- package/dist/services/orchestration/orchestrator.d.ts +45 -0
- package/dist/services/orchestration/orchestrator.js +214 -0
- package/dist/services/orchestration/readCache.d.ts +79 -0
- package/dist/services/orchestration/readCache.js +108 -0
- package/dist/services/orchestration/scopedTools.d.ts +57 -0
- package/dist/services/orchestration/scopedTools.js +172 -0
- package/dist/services/orchestration/subAgent.d.ts +58 -0
- package/dist/services/orchestration/subAgent.js +187 -0
- package/dist/services/orchestration/taskGraph.d.ts +129 -0
- package/dist/services/orchestration/taskGraph.js +254 -0
- package/dist/services/proxyClient.d.ts +25 -0
- package/dist/services/proxyClient.js +60 -0
- package/dist/utils/asyncContext.d.ts +16 -0
- package/dist/utils/asyncContext.js +25 -0
- package/dist/utils/config.d.ts +3 -1
- package/dist/utils/config.js +3 -1
- package/dist/utils/contextPrompts.js +3 -2
- package/dist/utils/dependencyTracer/modules/api.d.ts +9 -0
- package/dist/utils/dependencyTracer/modules/api.js +62 -0
- package/dist/utils/dependencyTracer/modules/graph.d.ts +9 -0
- package/dist/utils/dependencyTracer/modules/graph.js +23 -0
- package/dist/utils/dependencyTracer/modules/profiles.d.ts +7 -0
- package/dist/utils/dependencyTracer/modules/profiles.js +120 -0
- package/dist/utils/dependencyTracer/modules/resolver.d.ts +7 -0
- package/dist/utils/dependencyTracer/modules/resolver.js +51 -0
- package/dist/utils/dependencyTracer/modules/types.d.ts +4 -0
- package/dist/utils/dependencyTracer/modules/types.js +1 -0
- package/dist/utils/dependencyTracer/modules/walker.d.ts +1 -0
- package/dist/utils/dependencyTracer/modules/walker.js +48 -0
- package/dist/utils/dependencyTracer.js +31 -17
- package/dist/utils/excludedExtensions.js +0 -1
- package/dist/utils/historyPrompt.d.ts +9 -0
- package/dist/utils/historyPrompt.js +87 -0
- package/dist/utils/logo.d.ts +2 -0
- package/dist/utils/logo.js +31 -10
- package/dist/utils/paste.d.ts +21 -0
- package/dist/utils/paste.js +22 -1
- package/dist/utils/profiles.d.ts +2 -0
- package/dist/utils/profiles.js +44 -0
- package/dist/utils/projectStorage.js +10 -7
- package/dist/utils/systemPrompts.d.ts +6 -3
- package/dist/utils/systemPrompts.js +106 -5
- package/dist/utils/types.d.ts +33 -0
- package/dist/utils/types.js +1 -0
- package/oclif.manifest.json +2 -2
- package/package.json +5 -3
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Two-Layer Message Bus for Sub-Agent Orchestration.
|
|
3
|
+
*
|
|
4
|
+
* Provides the core inter-agent communication primitive for the orchestration system.
|
|
5
|
+
* Two layers of communication:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Automatic Activity Log** — Zero-cost tool call records captured by the orchestrator.
|
|
8
|
+
* Every tool execution by any sub-agent is auto-logged without spending agent tokens.
|
|
9
|
+
*
|
|
10
|
+
* 2. **Agent-Posted Semantic Signals** — Typed coordination messages (discovery, warning,
|
|
11
|
+
* request, completion) that agents manually post when they need to communicate intent
|
|
12
|
+
* or changes that the activity log alone cannot capture.
|
|
13
|
+
*
|
|
14
|
+
* The bus is fully disk-backed via atomic writes to `.minovativemind/orchestration/` for
|
|
15
|
+
* crash recovery. If the CLI process dies mid-orchestration, the next invocation can
|
|
16
|
+
* restore the full bus state and resume.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* A single auto-captured tool execution record. Generated by the orchestrator's
|
|
20
|
+
* scoped tool wrapper — agents never spend tokens producing these.
|
|
21
|
+
*/
|
|
22
|
+
export interface ActivityEntry {
|
|
23
|
+
type: 'activity';
|
|
24
|
+
timestamp: number;
|
|
25
|
+
agentId: string;
|
|
26
|
+
/** The tool that was executed (e.g., 'write_file', 'modify_file', 'grep_search') */
|
|
27
|
+
tool: string;
|
|
28
|
+
/** What file/target was acted on (e.g., 'src/services/redis.ts', pattern string) */
|
|
29
|
+
target: string;
|
|
30
|
+
/** Short description of what happened (e.g., 'Created (52 lines)', 'Modified (added interface)') */
|
|
31
|
+
action: string;
|
|
32
|
+
/** Whether the tool call succeeded or errored */
|
|
33
|
+
status: 'success' | 'error';
|
|
34
|
+
/** Optional brief summary of the result */
|
|
35
|
+
resultSummary?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Union type for all agent-posted semantic signals.
|
|
39
|
+
* These cost tokens (the agent decides to call the `post_message` tool)
|
|
40
|
+
* but carry semantic meaning the activity log can't express.
|
|
41
|
+
*/
|
|
42
|
+
export type BusSignal = DiscoverySignal | WarningSignal | RequestSignal | CompletionSignal;
|
|
43
|
+
export interface DiscoverySignal {
|
|
44
|
+
type: 'discovery';
|
|
45
|
+
timestamp: number;
|
|
46
|
+
fromAgent: string;
|
|
47
|
+
content: string;
|
|
48
|
+
affectedFiles: string[];
|
|
49
|
+
}
|
|
50
|
+
export interface WarningSignal {
|
|
51
|
+
type: 'warning';
|
|
52
|
+
timestamp: number;
|
|
53
|
+
fromAgent: string;
|
|
54
|
+
content: string;
|
|
55
|
+
changedSignature?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface RequestSignal {
|
|
58
|
+
type: 'request';
|
|
59
|
+
timestamp: number;
|
|
60
|
+
fromAgent: string;
|
|
61
|
+
toAgent: string;
|
|
62
|
+
content: string;
|
|
63
|
+
}
|
|
64
|
+
export interface CompletionSignal {
|
|
65
|
+
type: 'completion';
|
|
66
|
+
timestamp: number;
|
|
67
|
+
fromAgent: string;
|
|
68
|
+
summary: string;
|
|
69
|
+
exports: Record<string, string>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Two-layer, disk-backed message bus for sub-agent coordination.
|
|
73
|
+
*
|
|
74
|
+
* - Layer 1 (Activity Log): Auto-captured tool executions. Zero agent cost.
|
|
75
|
+
* - Layer 2 (Semantic Signals): Agent-posted coordination messages. Capped at
|
|
76
|
+
* `MAX_SIGNALS_PER_AGENT` per agent to prevent chatty flooding.
|
|
77
|
+
*
|
|
78
|
+
* All mutations are atomically persisted to disk so the orchestrator can survive
|
|
79
|
+
* a CLI crash and resume from the last known state.
|
|
80
|
+
*/
|
|
81
|
+
export declare class MessageBus {
|
|
82
|
+
private activityLog;
|
|
83
|
+
private signals;
|
|
84
|
+
private activityCursors;
|
|
85
|
+
private signalCursors;
|
|
86
|
+
private readonly persistPath;
|
|
87
|
+
private readonly workspaceRoot;
|
|
88
|
+
/** Maximum semantic signals any single agent can post */
|
|
89
|
+
static readonly MAX_SIGNALS_PER_AGENT = 20;
|
|
90
|
+
constructor(workspaceRoot: string, conversationId: string);
|
|
91
|
+
/**
|
|
92
|
+
* Records a tool execution into the activity log. Called by the scoped tool
|
|
93
|
+
* wrapper in `scopedTools.ts` — zero cost to the agent.
|
|
94
|
+
*/
|
|
95
|
+
logActivity(entry: ActivityEntry): void;
|
|
96
|
+
/**
|
|
97
|
+
* Posts a semantic signal from an agent. These cost tokens (the agent calls
|
|
98
|
+
* `post_message`) but carry intent that raw tool logs cannot express.
|
|
99
|
+
*
|
|
100
|
+
* Enforces per-agent signal cap to prevent runaway agents from flooding the bus.
|
|
101
|
+
*
|
|
102
|
+
* @returns `true` if the signal was accepted, `false` if the agent hit the cap.
|
|
103
|
+
*/
|
|
104
|
+
postSignal(signal: BusSignal): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Retrieves all unread activity entries and semantic signals for a specific agent.
|
|
107
|
+
* Advances the agent's cursors so subsequent calls return only new entries.
|
|
108
|
+
*
|
|
109
|
+
* Filters out the requesting agent's own entries (an agent doesn't need to
|
|
110
|
+
* re-read its own tool logs or signals).
|
|
111
|
+
*/
|
|
112
|
+
getUnread(agentId: string): {
|
|
113
|
+
activities: ActivityEntry[];
|
|
114
|
+
signals: BusSignal[];
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Returns the complete, unfiltered bus state for PM reconciliation.
|
|
118
|
+
* Used after all sub-agents complete to give the PM full visibility.
|
|
119
|
+
*/
|
|
120
|
+
getFullLog(): {
|
|
121
|
+
activities: ActivityEntry[];
|
|
122
|
+
signals: BusSignal[];
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Returns all activity entries for a specific agent (used for dead agent
|
|
126
|
+
* recovery — collecting partial progress before re-dispatch).
|
|
127
|
+
*/
|
|
128
|
+
getAgentActivity(agentId: string): ActivityEntry[];
|
|
129
|
+
/**
|
|
130
|
+
* Returns the total number of activity entries and signals in the bus.
|
|
131
|
+
* Used for terminal display and diagnostics.
|
|
132
|
+
*/
|
|
133
|
+
getStats(): {
|
|
134
|
+
activityCount: number;
|
|
135
|
+
signalCount: number;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Formats a batch of activity entries into a compact, human-readable string
|
|
139
|
+
* suitable for injection into an agent's context window.
|
|
140
|
+
*/
|
|
141
|
+
static formatActivityEntries(entries: ActivityEntry[]): string;
|
|
142
|
+
/**
|
|
143
|
+
* Formats semantic signals into a readable string for agent context injection.
|
|
144
|
+
*/
|
|
145
|
+
static formatSignals(signals: BusSignal[]): string;
|
|
146
|
+
/**
|
|
147
|
+
* Writes the full bus state to disk atomically. Called after every mutation
|
|
148
|
+
* to ensure crash resilience. Uses fire-and-forget to avoid blocking the
|
|
149
|
+
* hot path — write failures are logged but non-fatal.
|
|
150
|
+
*/
|
|
151
|
+
private persistToDiskAsync;
|
|
152
|
+
/**
|
|
153
|
+
* Restores bus state from a previous disk snapshot. Called in the constructor.
|
|
154
|
+
* If the file doesn't exist or is corrupted, starts with a clean slate.
|
|
155
|
+
*/
|
|
156
|
+
private restoreFromDisk;
|
|
157
|
+
/**
|
|
158
|
+
* Clears all bus state and removes the persistence file.
|
|
159
|
+
* Called when orchestration completes successfully (no crash recovery needed).
|
|
160
|
+
*/
|
|
161
|
+
cleanup(): Promise<void>;
|
|
162
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Two-Layer Message Bus for Sub-Agent Orchestration.
|
|
3
|
+
*
|
|
4
|
+
* Provides the core inter-agent communication primitive for the orchestration system.
|
|
5
|
+
* Two layers of communication:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Automatic Activity Log** — Zero-cost tool call records captured by the orchestrator.
|
|
8
|
+
* Every tool execution by any sub-agent is auto-logged without spending agent tokens.
|
|
9
|
+
*
|
|
10
|
+
* 2. **Agent-Posted Semantic Signals** — Typed coordination messages (discovery, warning,
|
|
11
|
+
* request, completion) that agents manually post when they need to communicate intent
|
|
12
|
+
* or changes that the activity log alone cannot capture.
|
|
13
|
+
*
|
|
14
|
+
* The bus is fully disk-backed via atomic writes to `.minovativemind/orchestration/` for
|
|
15
|
+
* crash recovery. If the CLI process dies mid-orchestration, the next invocation can
|
|
16
|
+
* restore the full bus state and resume.
|
|
17
|
+
*/
|
|
18
|
+
import { promises as fs, readFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { atomicWriteFile } from '../../utils/atomicWrite.js';
|
|
21
|
+
import { debugLog } from '../../utils/logger.js';
|
|
22
|
+
// ─── Message Bus Implementation ──────────────────────────────────────
|
|
23
|
+
/**
|
|
24
|
+
* Two-layer, disk-backed message bus for sub-agent coordination.
|
|
25
|
+
*
|
|
26
|
+
* - Layer 1 (Activity Log): Auto-captured tool executions. Zero agent cost.
|
|
27
|
+
* - Layer 2 (Semantic Signals): Agent-posted coordination messages. Capped at
|
|
28
|
+
* `MAX_SIGNALS_PER_AGENT` per agent to prevent chatty flooding.
|
|
29
|
+
*
|
|
30
|
+
* All mutations are atomically persisted to disk so the orchestrator can survive
|
|
31
|
+
* a CLI crash and resume from the last known state.
|
|
32
|
+
*/
|
|
33
|
+
export class MessageBus {
|
|
34
|
+
activityLog = [];
|
|
35
|
+
signals = [];
|
|
36
|
+
activityCursors = new Map();
|
|
37
|
+
signalCursors = new Map();
|
|
38
|
+
persistPath;
|
|
39
|
+
workspaceRoot;
|
|
40
|
+
/** Maximum semantic signals any single agent can post */
|
|
41
|
+
static MAX_SIGNALS_PER_AGENT = 20;
|
|
42
|
+
constructor(workspaceRoot, conversationId) {
|
|
43
|
+
this.workspaceRoot = workspaceRoot;
|
|
44
|
+
const orchestrationDir = path.join(workspaceRoot, '.minovativemind', 'orchestration');
|
|
45
|
+
this.persistPath = path.join(orchestrationDir, `${conversationId}-bus.json`);
|
|
46
|
+
this.restoreFromDisk();
|
|
47
|
+
}
|
|
48
|
+
// ─── Layer 1: Automatic Activity Log ─────────────────────────────
|
|
49
|
+
/**
|
|
50
|
+
* Records a tool execution into the activity log. Called by the scoped tool
|
|
51
|
+
* wrapper in `scopedTools.ts` — zero cost to the agent.
|
|
52
|
+
*/
|
|
53
|
+
logActivity(entry) {
|
|
54
|
+
this.activityLog.push(entry);
|
|
55
|
+
this.persistToDiskAsync();
|
|
56
|
+
}
|
|
57
|
+
// ─── Layer 2: Semantic Signals ───────────────────────────────────
|
|
58
|
+
/**
|
|
59
|
+
* Posts a semantic signal from an agent. These cost tokens (the agent calls
|
|
60
|
+
* `post_message`) but carry intent that raw tool logs cannot express.
|
|
61
|
+
*
|
|
62
|
+
* Enforces per-agent signal cap to prevent runaway agents from flooding the bus.
|
|
63
|
+
*
|
|
64
|
+
* @returns `true` if the signal was accepted, `false` if the agent hit the cap.
|
|
65
|
+
*/
|
|
66
|
+
postSignal(signal) {
|
|
67
|
+
const agentSignalCount = this.signals.filter(s => s.fromAgent === signal.fromAgent).length;
|
|
68
|
+
if (agentSignalCount >= MessageBus.MAX_SIGNALS_PER_AGENT) {
|
|
69
|
+
debugLog(`MessageBus: Agent ${signal.fromAgent} hit signal cap (${MessageBus.MAX_SIGNALS_PER_AGENT}). ` +
|
|
70
|
+
`Dropping signal of type "${signal.type}".`);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
this.signals.push(signal);
|
|
74
|
+
this.persistToDiskAsync();
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
// ─── Reading ─────────────────────────────────────────────────────
|
|
78
|
+
/**
|
|
79
|
+
* Retrieves all unread activity entries and semantic signals for a specific agent.
|
|
80
|
+
* Advances the agent's cursors so subsequent calls return only new entries.
|
|
81
|
+
*
|
|
82
|
+
* Filters out the requesting agent's own entries (an agent doesn't need to
|
|
83
|
+
* re-read its own tool logs or signals).
|
|
84
|
+
*/
|
|
85
|
+
getUnread(agentId) {
|
|
86
|
+
const actCursor = this.activityCursors.get(agentId) ?? 0;
|
|
87
|
+
const sigCursor = this.signalCursors.get(agentId) ?? 0;
|
|
88
|
+
const activities = this.activityLog
|
|
89
|
+
.slice(actCursor)
|
|
90
|
+
.filter(e => e.agentId !== agentId);
|
|
91
|
+
const signals = this.signals
|
|
92
|
+
.slice(sigCursor)
|
|
93
|
+
.filter(s => s.fromAgent !== agentId);
|
|
94
|
+
// Advance cursors to current end
|
|
95
|
+
this.activityCursors.set(agentId, this.activityLog.length);
|
|
96
|
+
this.signalCursors.set(agentId, this.signals.length);
|
|
97
|
+
return { activities, signals };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Returns the complete, unfiltered bus state for PM reconciliation.
|
|
101
|
+
* Used after all sub-agents complete to give the PM full visibility.
|
|
102
|
+
*/
|
|
103
|
+
getFullLog() {
|
|
104
|
+
return {
|
|
105
|
+
activities: [...this.activityLog],
|
|
106
|
+
signals: [...this.signals],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Returns all activity entries for a specific agent (used for dead agent
|
|
111
|
+
* recovery — collecting partial progress before re-dispatch).
|
|
112
|
+
*/
|
|
113
|
+
getAgentActivity(agentId) {
|
|
114
|
+
return this.activityLog.filter(e => e.agentId === agentId);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Returns the total number of activity entries and signals in the bus.
|
|
118
|
+
* Used for terminal display and diagnostics.
|
|
119
|
+
*/
|
|
120
|
+
getStats() {
|
|
121
|
+
return {
|
|
122
|
+
activityCount: this.activityLog.length,
|
|
123
|
+
signalCount: this.signals.length,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// ─── Formatting Helpers ──────────────────────────────────────────
|
|
127
|
+
/**
|
|
128
|
+
* Formats a batch of activity entries into a compact, human-readable string
|
|
129
|
+
* suitable for injection into an agent's context window.
|
|
130
|
+
*/
|
|
131
|
+
static formatActivityEntries(entries) {
|
|
132
|
+
if (entries.length === 0)
|
|
133
|
+
return '';
|
|
134
|
+
return entries.map(e => {
|
|
135
|
+
const statusIcon = e.status === 'success' ? '✓' : '✗';
|
|
136
|
+
const summary = e.resultSummary ? ` | ${e.resultSummary}` : '';
|
|
137
|
+
return ` ${e.agentId} | ${e.tool.padEnd(14)} → ${e.target.padEnd(40)} | ${statusIcon} ${e.action}${summary}`;
|
|
138
|
+
}).join('\n');
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Formats semantic signals into a readable string for agent context injection.
|
|
142
|
+
*/
|
|
143
|
+
static formatSignals(signals) {
|
|
144
|
+
if (signals.length === 0)
|
|
145
|
+
return '';
|
|
146
|
+
return signals.map(s => {
|
|
147
|
+
const tag = s.type.toUpperCase();
|
|
148
|
+
switch (s.type) {
|
|
149
|
+
case 'discovery':
|
|
150
|
+
return ` [${tag} from ${s.fromAgent}]: "${s.content}" (affects: ${s.affectedFiles.join(', ')})`;
|
|
151
|
+
case 'warning':
|
|
152
|
+
return ` [${tag} from ${s.fromAgent}]: "${s.content}"${s.changedSignature ? ` (signature: ${s.changedSignature})` : ''}`;
|
|
153
|
+
case 'request':
|
|
154
|
+
return ` [${tag} from ${s.fromAgent} → ${s.toAgent}]: "${s.content}"`;
|
|
155
|
+
case 'completion':
|
|
156
|
+
return ` [${tag} from ${s.fromAgent}]: "${s.summary}"`;
|
|
157
|
+
default:
|
|
158
|
+
return ` [SIGNAL from ${s.fromAgent}]: ${JSON.stringify(s)}`;
|
|
159
|
+
}
|
|
160
|
+
}).join('\n');
|
|
161
|
+
}
|
|
162
|
+
// ─── Persistence ─────────────────────────────────────────────────
|
|
163
|
+
/**
|
|
164
|
+
* Writes the full bus state to disk atomically. Called after every mutation
|
|
165
|
+
* to ensure crash resilience. Uses fire-and-forget to avoid blocking the
|
|
166
|
+
* hot path — write failures are logged but non-fatal.
|
|
167
|
+
*/
|
|
168
|
+
persistToDiskAsync() {
|
|
169
|
+
const snapshot = {
|
|
170
|
+
activityLog: this.activityLog,
|
|
171
|
+
signals: this.signals,
|
|
172
|
+
activityCursors: Object.fromEntries(this.activityCursors),
|
|
173
|
+
signalCursors: Object.fromEntries(this.signalCursors),
|
|
174
|
+
};
|
|
175
|
+
// Fire-and-forget — don't block the calling agent's tool loop
|
|
176
|
+
atomicWriteFile(this.persistPath, JSON.stringify(snapshot)).catch(err => {
|
|
177
|
+
debugLog(`MessageBus: Failed to persist to disk: ${err}`);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Restores bus state from a previous disk snapshot. Called in the constructor.
|
|
182
|
+
* If the file doesn't exist or is corrupted, starts with a clean slate.
|
|
183
|
+
*/
|
|
184
|
+
restoreFromDisk() {
|
|
185
|
+
try {
|
|
186
|
+
// Sync FS functions imported at top of file
|
|
187
|
+
const dir = path.dirname(this.persistPath);
|
|
188
|
+
if (!existsSync(dir)) {
|
|
189
|
+
mkdirSync(dir, { recursive: true });
|
|
190
|
+
}
|
|
191
|
+
if (!existsSync(this.persistPath))
|
|
192
|
+
return;
|
|
193
|
+
const raw = readFileSync(this.persistPath, 'utf-8');
|
|
194
|
+
const snapshot = JSON.parse(raw);
|
|
195
|
+
this.activityLog = snapshot.activityLog ?? [];
|
|
196
|
+
this.signals = snapshot.signals ?? [];
|
|
197
|
+
this.activityCursors = new Map(Object.entries(snapshot.activityCursors ?? {}));
|
|
198
|
+
this.signalCursors = new Map(Object.entries(snapshot.signalCursors ?? {}));
|
|
199
|
+
debugLog(`MessageBus: Restored ${this.activityLog.length} activity entries and ${this.signals.length} signals from disk.`);
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
debugLog(`MessageBus: Could not restore from disk (starting clean): ${err}`);
|
|
203
|
+
this.activityLog = [];
|
|
204
|
+
this.signals = [];
|
|
205
|
+
this.activityCursors = new Map();
|
|
206
|
+
this.signalCursors = new Map();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Clears all bus state and removes the persistence file.
|
|
211
|
+
* Called when orchestration completes successfully (no crash recovery needed).
|
|
212
|
+
*/
|
|
213
|
+
async cleanup() {
|
|
214
|
+
this.activityLog = [];
|
|
215
|
+
this.signals = [];
|
|
216
|
+
this.activityCursors = new Map();
|
|
217
|
+
this.signalCursors = new Map();
|
|
218
|
+
try {
|
|
219
|
+
await fs.unlink(this.persistPath);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// File may not exist — non-fatal
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Main Orchestrator for Sub-Agent Dispatch and Coordination.
|
|
3
|
+
*
|
|
4
|
+
* The orchestrator acts as the "PM Kernel", responsible for:
|
|
5
|
+
* 1. Task Decomposition (using gemini-3.5-flash)
|
|
6
|
+
* 2. Graph Validation (Cycle detection via Kahn's algorithm)
|
|
7
|
+
* 3. Lock Ordering (Conflict resolution across parallel waves)
|
|
8
|
+
* 4. Parallel Dispatch (Executing waves sequentially, agents in parallel)
|
|
9
|
+
* 5. Error Routing and Cross-Agent Integration Reconciliation
|
|
10
|
+
*/
|
|
11
|
+
import { AsyncInputHandler } from '../agent/inputHandler.js';
|
|
12
|
+
export declare class Orchestrator {
|
|
13
|
+
private readonly workspaceRoot;
|
|
14
|
+
private readonly conversationId;
|
|
15
|
+
private readonly inputHandler;
|
|
16
|
+
private bus;
|
|
17
|
+
private locks;
|
|
18
|
+
private pmChat;
|
|
19
|
+
private agentResults;
|
|
20
|
+
constructor(workspaceRoot: string, conversationId: string, inputHandler: AsyncInputHandler);
|
|
21
|
+
/**
|
|
22
|
+
* Main entry point for orchestration. Decomposes the task, validates the graph,
|
|
23
|
+
* schedules waves, and dispatches sub-agents.
|
|
24
|
+
*
|
|
25
|
+
* @returns true if orchestration completed successfully, false if the task
|
|
26
|
+
* was too simple and should be handled by the main agent loop.
|
|
27
|
+
*/
|
|
28
|
+
runOrchestration(objective: string, contextInjection: string, signal: AbortSignal): Promise<boolean | string>;
|
|
29
|
+
/**
|
|
30
|
+
* Calls the PM Agent to decompose the task into a TaskGraph.
|
|
31
|
+
*/
|
|
32
|
+
private decomposeTask;
|
|
33
|
+
/**
|
|
34
|
+
* Computes execution waves and resolves file lock conflicts via pre-allocation.
|
|
35
|
+
*/
|
|
36
|
+
private scheduleWaves;
|
|
37
|
+
/**
|
|
38
|
+
* Dispatches a single sub-agent and records its result.
|
|
39
|
+
*/
|
|
40
|
+
private dispatchAgent;
|
|
41
|
+
/**
|
|
42
|
+
* Final reconciliation phase after all waves complete.
|
|
43
|
+
*/
|
|
44
|
+
private reconcile;
|
|
45
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Main Orchestrator for Sub-Agent Dispatch and Coordination.
|
|
3
|
+
*
|
|
4
|
+
* The orchestrator acts as the "PM Kernel", responsible for:
|
|
5
|
+
* 1. Task Decomposition (using gemini-3.5-flash)
|
|
6
|
+
* 2. Graph Validation (Cycle detection via Kahn's algorithm)
|
|
7
|
+
* 3. Lock Ordering (Conflict resolution across parallel waves)
|
|
8
|
+
* 4. Parallel Dispatch (Executing waves sequentially, agents in parallel)
|
|
9
|
+
* 5. Error Routing and Cross-Agent Integration Reconciliation
|
|
10
|
+
*/
|
|
11
|
+
import * as p from '@clack/prompts';
|
|
12
|
+
import pc from 'picocolors';
|
|
13
|
+
import { ProxyChatSession } from '../ai.js';
|
|
14
|
+
import { GEMINI_MODELS, MAX_OUTPUT_TOKENS } from '../../utils/config.js';
|
|
15
|
+
import { debugLog } from '../../utils/logger.js';
|
|
16
|
+
import { MessageBus } from './messageBus.js';
|
|
17
|
+
import { FileLockRegistry } from './fileLockRegistry.js';
|
|
18
|
+
import { SubAgentRunner } from './subAgent.js';
|
|
19
|
+
import { validateTaskGraph, computeExecutionWaves, detectFileConflicts, resolveFileConflicts, buildCycleCorrectionPrompt, CyclicDependencyError, } from './taskGraph.js';
|
|
20
|
+
// ─── Constants ───────────────────────────────────────────────────────
|
|
21
|
+
const PM_SYSTEM_INSTRUCTION = `You are the PM Agent (Project Manager).
|
|
22
|
+
Your job is to decompose the user's objective into a parallelizable Task Graph for sub-agents.
|
|
23
|
+
You must output ONLY raw JSON representing the TaskGraph object. Do not wrap in markdown code blocks.
|
|
24
|
+
|
|
25
|
+
Requirements:
|
|
26
|
+
- Break the work down logically based on file isolation and dependency chains.
|
|
27
|
+
- Tasks that don't depend on each other will run in parallel.
|
|
28
|
+
- If a task depends on another, it must list its ID in "dependsOn".
|
|
29
|
+
- Do NOT create cyclic dependencies (A -> B -> A).
|
|
30
|
+
- If the entire objective is very simple and only requires modifying 1-2 files sequentially, output a graph with exactly 1 task.
|
|
31
|
+
- Ensure 'targetFiles' lists all files the task will modify.
|
|
32
|
+
|
|
33
|
+
JSON Schema:
|
|
34
|
+
{
|
|
35
|
+
"objective": "string",
|
|
36
|
+
"tasks": [
|
|
37
|
+
{
|
|
38
|
+
"id": "string (unique)",
|
|
39
|
+
"intent": "string (what to do)",
|
|
40
|
+
"targetFiles": ["string"],
|
|
41
|
+
"readOnlyFiles": ["string"],
|
|
42
|
+
"dependsOn": ["string (IDs)"],
|
|
43
|
+
"exports": ["string (what this provides)"]
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"constraints": ["string"]
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
// ─── Orchestrator Implementation ─────────────────────────────────────
|
|
50
|
+
export class Orchestrator {
|
|
51
|
+
workspaceRoot;
|
|
52
|
+
conversationId;
|
|
53
|
+
inputHandler;
|
|
54
|
+
bus;
|
|
55
|
+
locks;
|
|
56
|
+
pmChat;
|
|
57
|
+
agentResults = new Map();
|
|
58
|
+
constructor(workspaceRoot, conversationId, inputHandler) {
|
|
59
|
+
this.workspaceRoot = workspaceRoot;
|
|
60
|
+
this.conversationId = conversationId;
|
|
61
|
+
this.inputHandler = inputHandler;
|
|
62
|
+
this.bus = new MessageBus(workspaceRoot, conversationId);
|
|
63
|
+
this.locks = new FileLockRegistry();
|
|
64
|
+
this.pmChat = new ProxyChatSession(GEMINI_MODELS.FLASH_3_5, PM_SYSTEM_INSTRUCTION, [], {
|
|
65
|
+
maxOutputTokens: MAX_OUTPUT_TOKENS,
|
|
66
|
+
temperature: 0.1,
|
|
67
|
+
responseMimeType: 'application/json',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Main entry point for orchestration. Decomposes the task, validates the graph,
|
|
72
|
+
* schedules waves, and dispatches sub-agents.
|
|
73
|
+
*
|
|
74
|
+
* @returns true if orchestration completed successfully, false if the task
|
|
75
|
+
* was too simple and should be handled by the main agent loop.
|
|
76
|
+
*/
|
|
77
|
+
async runOrchestration(objective, contextInjection, signal) {
|
|
78
|
+
p.log.step(pc.cyan('Orchestrator: Planning task execution...'));
|
|
79
|
+
// 1. Task Decomposition
|
|
80
|
+
const graph = await this.decomposeTask(objective, contextInjection, signal);
|
|
81
|
+
if (!graph)
|
|
82
|
+
return false; // Aborted
|
|
83
|
+
if (graph.tasks.length <= 1) {
|
|
84
|
+
debugLog('Orchestrator: Only 1 task generated. Bypassing orchestration overhead.');
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
// 2. Validation & Scheduling
|
|
88
|
+
const waves = this.scheduleWaves(graph);
|
|
89
|
+
if (!waves || waves.length === 0)
|
|
90
|
+
return false; // Aborted or failed cycle detection
|
|
91
|
+
p.log.info(pc.blue(`Orchestrator: Generated ${waves.length} execution wave(s) with ${graph.tasks.length} total tasks.`));
|
|
92
|
+
// 3. Dispatch Waves
|
|
93
|
+
for (const wave of waves) {
|
|
94
|
+
if (signal.aborted)
|
|
95
|
+
break;
|
|
96
|
+
p.log.step(pc.magenta(`Starting Wave ${wave.depth + 1}...`));
|
|
97
|
+
const wavePromises = wave.taskIds.map(taskId => {
|
|
98
|
+
const taskDef = graph.tasks.find(t => t.id === taskId);
|
|
99
|
+
const globalContext = `Objective:\n${objective}\n\nContext:\n${contextInjection}`;
|
|
100
|
+
return this.dispatchAgent(taskDef, globalContext, signal);
|
|
101
|
+
});
|
|
102
|
+
// Run all agents in this wave concurrently
|
|
103
|
+
const results = await Promise.all(wavePromises);
|
|
104
|
+
// Post-wave evaluation
|
|
105
|
+
const failedCount = results.filter(r => !r.success).length;
|
|
106
|
+
if (failedCount > 0) {
|
|
107
|
+
p.log.warn(pc.yellow(`Wave ${wave.depth + 1} finished with ${failedCount} failure(s).`));
|
|
108
|
+
// Phase 3 implementation note: Right now, we continue and let downstream
|
|
109
|
+
// agents attempt to run, but in the future we will implement PM-driven
|
|
110
|
+
// downstream re-validation and dynamic graph patching here.
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
p.log.success(pc.green(`Wave ${wave.depth + 1} completed successfully.`));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// 4. Reconciliation
|
|
117
|
+
const finalSummary = await this.reconcile(graph);
|
|
118
|
+
return finalSummary;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Calls the PM Agent to decompose the task into a TaskGraph.
|
|
122
|
+
*/
|
|
123
|
+
async decomposeTask(objective, contextInjection, signal) {
|
|
124
|
+
const prompt = `Objective:\n${objective}\n\nContext:\n${contextInjection}`;
|
|
125
|
+
let result = await this.pmChat.sendMessage(prompt, undefined, signal);
|
|
126
|
+
if (signal.aborted)
|
|
127
|
+
return null;
|
|
128
|
+
let rawJson = result.response.text();
|
|
129
|
+
// Cycle Self-Correction Loop (Up to 3 attempts)
|
|
130
|
+
let attempts = 0;
|
|
131
|
+
while (attempts < 3) {
|
|
132
|
+
try {
|
|
133
|
+
const graph = JSON.parse(rawJson);
|
|
134
|
+
// Run Kahn's algorithm
|
|
135
|
+
validateTaskGraph(graph);
|
|
136
|
+
return graph; // Validation passed!
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
if (err instanceof CyclicDependencyError) {
|
|
140
|
+
debugLog(`PM Agent generated a cyclic graph. Attempt ${attempts + 1}/3 to self-correct.`);
|
|
141
|
+
const correctionPrompt = buildCycleCorrectionPrompt(err.cycleNodes);
|
|
142
|
+
result = await this.pmChat.sendMessage(correctionPrompt, undefined, signal);
|
|
143
|
+
if (signal.aborted)
|
|
144
|
+
return null;
|
|
145
|
+
rawJson = result.response.text();
|
|
146
|
+
attempts++;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// JSON parsing error or InvalidDependencyError
|
|
150
|
+
debugLog(`PM Agent generated invalid graph: ${err.message}. Retrying...`);
|
|
151
|
+
result = await this.pmChat.sendMessage(`Invalid JSON or missing dependency ID: ${err.message}. Please fix.`, undefined, signal);
|
|
152
|
+
if (signal.aborted)
|
|
153
|
+
return null;
|
|
154
|
+
rawJson = result.response.text();
|
|
155
|
+
attempts++;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
p.log.error('Orchestrator: PM Agent failed to generate a valid, acyclic task graph after 3 attempts.');
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Computes execution waves and resolves file lock conflicts via pre-allocation.
|
|
164
|
+
*/
|
|
165
|
+
scheduleWaves(graph) {
|
|
166
|
+
try {
|
|
167
|
+
const rawWaves = computeExecutionWaves(graph);
|
|
168
|
+
const conflicts = detectFileConflicts(graph, rawWaves);
|
|
169
|
+
return resolveFileConflicts(rawWaves, conflicts);
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
debugLog(`Orchestrator: Failed to schedule waves: ${err.message}`);
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Dispatches a single sub-agent and records its result.
|
|
178
|
+
*/
|
|
179
|
+
async dispatchAgent(taskDef, globalContext, signal) {
|
|
180
|
+
const runner = new SubAgentRunner(taskDef.id, taskDef.intent, this.workspaceRoot, this.bus, this.locks, globalContext);
|
|
181
|
+
const result = await runner.execute(signal);
|
|
182
|
+
// Clean up any stray locks if the agent crashed or stalled
|
|
183
|
+
if (result.crashed) {
|
|
184
|
+
this.locks.forceReleaseAll(taskDef.id);
|
|
185
|
+
}
|
|
186
|
+
this.agentResults.set(taskDef.id, result);
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Final reconciliation phase after all waves complete.
|
|
191
|
+
*/
|
|
192
|
+
async reconcile(graph) {
|
|
193
|
+
p.log.step(pc.cyan('Orchestrator: Reconciling results...'));
|
|
194
|
+
const stats = this.bus.getStats();
|
|
195
|
+
let totalTokens = 0;
|
|
196
|
+
let failedTasks = 0;
|
|
197
|
+
let finalSummary = '[Sub-Agent Orchestration Completed]\\n\\n';
|
|
198
|
+
for (const [taskId, res] of this.agentResults.entries()) {
|
|
199
|
+
totalTokens += res.creditsUsed;
|
|
200
|
+
if (!res.success)
|
|
201
|
+
failedTasks++;
|
|
202
|
+
debugLog(`Task ${taskId} summary: ${res.summary.substring(0, 100)}...`);
|
|
203
|
+
finalSummary += `**Task: ${taskId}**\\nStatus: ${res.success ? 'Success' : 'Failed'}\\n${res.summary}\\n\\n`;
|
|
204
|
+
}
|
|
205
|
+
p.log.info(`${pc.green('✓')} Sub-agent execution complete.\n` +
|
|
206
|
+
` Total tasks: ${graph.tasks.length} (${failedTasks} failed)\n` +
|
|
207
|
+
` Bus Activity: ${stats.activityCount} actions, ${stats.signalCount} signals\n` +
|
|
208
|
+
` Total Tokens: ${totalTokens.toLocaleString()}`);
|
|
209
|
+
// Clear the bus persistence now that orchestration is done
|
|
210
|
+
await this.bus.cleanup();
|
|
211
|
+
this.locks.shutdown();
|
|
212
|
+
return finalSummary.trim();
|
|
213
|
+
}
|
|
214
|
+
}
|