ac-framework 1.9.6 → 1.9.8
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 +27 -1
- package/package.json +1 -1
- package/src/agents/collab-summary.js +120 -0
- package/src/agents/config-store.js +2 -1
- package/src/agents/constants.js +3 -0
- package/src/agents/opencode-client.js +166 -12
- package/src/agents/orchestrator.js +199 -6
- package/src/agents/role-prompts.js +34 -1
- package/src/agents/run-state.js +113 -0
- package/src/agents/runtime.js +4 -2
- package/src/agents/state-store.js +69 -1
- package/src/commands/agents.js +408 -5
- package/src/mcp/collab-server.js +307 -2
- package/src/mcp/test-harness.mjs +410 -0
package/src/mcp/collab-server.js
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
9
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { readFile } from 'node:fs/promises';
|
|
13
|
+
import { dirname, resolve } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
10
15
|
import { z } from 'zod';
|
|
11
16
|
import { COLLAB_ROLES } from '../agents/constants.js';
|
|
12
17
|
import { buildEffectiveRoleModels, sanitizeRoleModels } from '../agents/model-selection.js';
|
|
@@ -25,6 +30,43 @@ import {
|
|
|
25
30
|
} from '../agents/state-store.js';
|
|
26
31
|
import { hasCommand, resolveCommandPath } from '../services/dependency-installer.js';
|
|
27
32
|
|
|
33
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
const runnerPath = resolve(__dirname, '../../bin/acfm.js');
|
|
35
|
+
|
|
36
|
+
function summarizeRun(state) {
|
|
37
|
+
const run = state.run || null;
|
|
38
|
+
return {
|
|
39
|
+
status: run?.status || 'idle',
|
|
40
|
+
runId: run?.runId || null,
|
|
41
|
+
currentRole: run?.currentRole || state.activeAgent || null,
|
|
42
|
+
round: run?.round || state.round,
|
|
43
|
+
policy: run?.policy || null,
|
|
44
|
+
lastError: run?.lastError || null,
|
|
45
|
+
eventCount: Array.isArray(run?.events) ? run.events.length : 0,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function latestRunEvent(state) {
|
|
50
|
+
const events = state?.run?.events;
|
|
51
|
+
if (!Array.isArray(events) || events.length === 0) return null;
|
|
52
|
+
return events[events.length - 1];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function readSessionArtifact(sessionId, filename) {
|
|
56
|
+
const path = resolve(getSessionDir(sessionId), filename);
|
|
57
|
+
if (!existsSync(path)) return null;
|
|
58
|
+
return readFile(path, 'utf8');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function launchAutopilot(sessionId) {
|
|
62
|
+
const child = spawn('node', [runnerPath, 'agents', 'autopilot', '--session', sessionId], {
|
|
63
|
+
cwd: process.cwd(),
|
|
64
|
+
detached: true,
|
|
65
|
+
stdio: 'ignore',
|
|
66
|
+
});
|
|
67
|
+
child.unref();
|
|
68
|
+
}
|
|
69
|
+
|
|
28
70
|
class MCPCollabServer {
|
|
29
71
|
constructor() {
|
|
30
72
|
this.server = new McpServer({
|
|
@@ -51,8 +93,13 @@ class MCPCollabServer {
|
|
|
51
93
|
}).partial().optional().describe('Optional per-role models (provider/model)'),
|
|
52
94
|
cwd: z.string().optional().describe('Working directory for agents'),
|
|
53
95
|
spawnWorkers: z.boolean().default(true).describe('Create tmux workers and panes'),
|
|
96
|
+
runPolicy: z.object({
|
|
97
|
+
timeoutPerRoleMs: z.number().int().positive().optional(),
|
|
98
|
+
retryOnTimeout: z.number().int().min(0).optional(),
|
|
99
|
+
fallbackOnFailure: z.enum(['retry', 'skip', 'abort']).optional(),
|
|
100
|
+
}).partial().optional().describe('Optional run execution policy'),
|
|
54
101
|
},
|
|
55
|
-
async ({ task, maxRounds, model, roleModels, cwd, spawnWorkers }) => {
|
|
102
|
+
async ({ task, maxRounds, model, roleModels, cwd, spawnWorkers, runPolicy }) => {
|
|
56
103
|
try {
|
|
57
104
|
const workingDirectory = cwd || process.cwd();
|
|
58
105
|
const opencodeBin = resolveCommandPath('opencode');
|
|
@@ -71,6 +118,7 @@ class MCPCollabServer {
|
|
|
71
118
|
roleModels: sanitizeRoleModels(roleModels),
|
|
72
119
|
workingDirectory,
|
|
73
120
|
opencodeBin,
|
|
121
|
+
runPolicy,
|
|
74
122
|
});
|
|
75
123
|
let updated = state;
|
|
76
124
|
if (spawnWorkers) {
|
|
@@ -93,6 +141,7 @@ class MCPCollabServer {
|
|
|
93
141
|
model: updated.model || null,
|
|
94
142
|
roleModels: updated.roleModels || {},
|
|
95
143
|
effectiveRoleModels: buildEffectiveRoleModels(updated, updated.model || null),
|
|
144
|
+
run: summarizeRun(updated),
|
|
96
145
|
tmuxSessionName,
|
|
97
146
|
attachCommand,
|
|
98
147
|
}, null, 2),
|
|
@@ -104,6 +153,186 @@ class MCPCollabServer {
|
|
|
104
153
|
}
|
|
105
154
|
);
|
|
106
155
|
|
|
156
|
+
this.server.tool(
|
|
157
|
+
'collab_invoke_team',
|
|
158
|
+
'Invoke full 4-agent collaborative run and return async run handle',
|
|
159
|
+
{
|
|
160
|
+
sessionId: z.string().optional().describe('Session ID (defaults to current session)'),
|
|
161
|
+
waitMs: z.number().int().min(0).max(30000).default(0).describe('Optional wait for progress before returning'),
|
|
162
|
+
},
|
|
163
|
+
async ({ sessionId, waitMs }) => {
|
|
164
|
+
try {
|
|
165
|
+
const id = sessionId || await loadCurrentSessionId();
|
|
166
|
+
if (!id) throw new Error('No active session found');
|
|
167
|
+
let state = await loadSessionState(id);
|
|
168
|
+
if (state.status !== 'running') {
|
|
169
|
+
throw new Error(`Session is ${state.status}. Resume/start before invoking.`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!state.tmuxSessionName) {
|
|
173
|
+
launchAutopilot(state.sessionId);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (waitMs > 0) {
|
|
177
|
+
const started = Date.now();
|
|
178
|
+
const initialEvents = state.run?.events?.length || 0;
|
|
179
|
+
while (Date.now() - started < waitMs) {
|
|
180
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
181
|
+
state = await loadSessionState(id);
|
|
182
|
+
const currentEvents = state.run?.events?.length || 0;
|
|
183
|
+
if (state.status !== 'running' || currentEvents > initialEvents) break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
content: [{
|
|
189
|
+
type: 'text',
|
|
190
|
+
text: JSON.stringify({
|
|
191
|
+
success: true,
|
|
192
|
+
sessionId: state.sessionId,
|
|
193
|
+
status: state.status,
|
|
194
|
+
run: summarizeRun(state),
|
|
195
|
+
latestEvent: latestRunEvent(state),
|
|
196
|
+
tmuxSessionName: state.tmuxSessionName || null,
|
|
197
|
+
attachCommand: state.tmuxSessionName ? `tmux attach -t ${state.tmuxSessionName}` : null,
|
|
198
|
+
}, null, 2),
|
|
199
|
+
}],
|
|
200
|
+
};
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }], isError: true };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
this.server.tool(
|
|
208
|
+
'collab_wait_run',
|
|
209
|
+
'Wait for collaborative run to complete/fail or timeout',
|
|
210
|
+
{
|
|
211
|
+
sessionId: z.string().optional().describe('Session ID (defaults to current session)'),
|
|
212
|
+
waitMs: z.number().int().positive().max(60000).default(10000).describe('Max wait in milliseconds'),
|
|
213
|
+
},
|
|
214
|
+
async ({ sessionId, waitMs }) => {
|
|
215
|
+
try {
|
|
216
|
+
const id = sessionId || await loadCurrentSessionId();
|
|
217
|
+
if (!id) throw new Error('No active session found');
|
|
218
|
+
const started = Date.now();
|
|
219
|
+
let state = await loadSessionState(id);
|
|
220
|
+
|
|
221
|
+
while (Date.now() - started < waitMs) {
|
|
222
|
+
const runStatus = state.run?.status || 'idle';
|
|
223
|
+
if (state.status !== 'running' || ['completed', 'failed', 'cancelled'].includes(runStatus)) break;
|
|
224
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
225
|
+
state = await loadSessionState(id);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
content: [{
|
|
230
|
+
type: 'text',
|
|
231
|
+
text: JSON.stringify({
|
|
232
|
+
success: true,
|
|
233
|
+
sessionId: state.sessionId,
|
|
234
|
+
status: state.status,
|
|
235
|
+
run: summarizeRun(state),
|
|
236
|
+
latestEvent: latestRunEvent(state),
|
|
237
|
+
timedOut: state.status === 'running' && !['completed', 'failed', 'cancelled'].includes(state.run?.status || 'idle'),
|
|
238
|
+
}, null, 2),
|
|
239
|
+
}],
|
|
240
|
+
};
|
|
241
|
+
} catch (error) {
|
|
242
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }], isError: true };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
this.server.tool(
|
|
248
|
+
'collab_get_result',
|
|
249
|
+
'Get final consolidated team output and run diagnostics',
|
|
250
|
+
{
|
|
251
|
+
sessionId: z.string().optional().describe('Session ID (defaults to current session)'),
|
|
252
|
+
},
|
|
253
|
+
async ({ sessionId }) => {
|
|
254
|
+
try {
|
|
255
|
+
const id = sessionId || await loadCurrentSessionId();
|
|
256
|
+
if (!id) throw new Error('No active session found');
|
|
257
|
+
const state = await loadSessionState(id);
|
|
258
|
+
const transcript = await loadTranscript(id);
|
|
259
|
+
const run = summarizeRun(state);
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
content: [{
|
|
263
|
+
type: 'text',
|
|
264
|
+
text: JSON.stringify({
|
|
265
|
+
success: true,
|
|
266
|
+
sessionId: state.sessionId,
|
|
267
|
+
status: state.status,
|
|
268
|
+
run,
|
|
269
|
+
latestEvent: latestRunEvent(state),
|
|
270
|
+
finalSummary: state.run?.finalSummary || null,
|
|
271
|
+
sharedContext: state.run?.sharedContext || null,
|
|
272
|
+
meetingSummary: await readSessionArtifact(id, 'meeting-summary.md'),
|
|
273
|
+
meetingLog: await readSessionArtifact(id, 'meeting-log.md'),
|
|
274
|
+
lastMessage: state.messages?.[state.messages.length - 1] || null,
|
|
275
|
+
transcriptCount: transcript.length,
|
|
276
|
+
}, null, 2),
|
|
277
|
+
}],
|
|
278
|
+
};
|
|
279
|
+
} catch (error) {
|
|
280
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }], isError: true };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
this.server.tool(
|
|
286
|
+
'collab_cancel_run',
|
|
287
|
+
'Cancel active collaborative run while keeping session state',
|
|
288
|
+
{
|
|
289
|
+
sessionId: z.string().optional().describe('Session ID (defaults to current session)'),
|
|
290
|
+
},
|
|
291
|
+
async ({ sessionId }) => {
|
|
292
|
+
try {
|
|
293
|
+
const id = sessionId || await loadCurrentSessionId();
|
|
294
|
+
if (!id) throw new Error('No active session found');
|
|
295
|
+
const state = await loadSessionState(id);
|
|
296
|
+
const run = state.run || {};
|
|
297
|
+
const updated = await saveSessionState({
|
|
298
|
+
...state,
|
|
299
|
+
status: 'running',
|
|
300
|
+
activeAgent: null,
|
|
301
|
+
run: {
|
|
302
|
+
...run,
|
|
303
|
+
status: 'cancelled',
|
|
304
|
+
currentRole: null,
|
|
305
|
+
finishedAt: new Date().toISOString(),
|
|
306
|
+
lastError: {
|
|
307
|
+
code: 'RUN_CANCELLED',
|
|
308
|
+
message: 'Cancelled by MCP caller',
|
|
309
|
+
},
|
|
310
|
+
events: [
|
|
311
|
+
...(Array.isArray(run.events) ? run.events : []),
|
|
312
|
+
{
|
|
313
|
+
id: `evt-${Date.now()}`,
|
|
314
|
+
type: 'run_cancelled',
|
|
315
|
+
timestamp: new Date().toISOString(),
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
return {
|
|
321
|
+
content: [{
|
|
322
|
+
type: 'text',
|
|
323
|
+
text: JSON.stringify({
|
|
324
|
+
success: true,
|
|
325
|
+
sessionId: updated.sessionId,
|
|
326
|
+
run: summarizeRun(updated),
|
|
327
|
+
}, null, 2),
|
|
328
|
+
}],
|
|
329
|
+
};
|
|
330
|
+
} catch (error) {
|
|
331
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }], isError: true };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
);
|
|
335
|
+
|
|
107
336
|
this.server.tool(
|
|
108
337
|
'collab_send_message',
|
|
109
338
|
'Send a user message to the active collaborative session',
|
|
@@ -150,6 +379,7 @@ class MCPCollabServer {
|
|
|
150
379
|
const state = await runWorkerIteration(id, role, {
|
|
151
380
|
cwd: process.cwd(),
|
|
152
381
|
opencodeBin: loaded.opencodeBin || resolveCommandPath('opencode') || undefined,
|
|
382
|
+
timeoutMs: loaded.run?.policy?.timeoutPerRoleMs || 180000,
|
|
153
383
|
});
|
|
154
384
|
|
|
155
385
|
return {
|
|
@@ -164,6 +394,8 @@ class MCPCollabServer {
|
|
|
164
394
|
model: state.model || null,
|
|
165
395
|
roleModels: state.roleModels || {},
|
|
166
396
|
effectiveRoleModels: buildEffectiveRoleModels(state, state.model || null),
|
|
397
|
+
run: summarizeRun(state),
|
|
398
|
+
latestEvent: latestRunEvent(state),
|
|
167
399
|
messageCount: state.messages.length,
|
|
168
400
|
}, null, 2),
|
|
169
401
|
}],
|
|
@@ -244,6 +476,9 @@ class MCPCollabServer {
|
|
|
244
476
|
text: JSON.stringify({
|
|
245
477
|
state,
|
|
246
478
|
effectiveRoleModels: buildEffectiveRoleModels(state, state.model || null),
|
|
479
|
+
run: summarizeRun(state),
|
|
480
|
+
latestEvent: latestRunEvent(state),
|
|
481
|
+
sharedContext: state.run?.sharedContext || null,
|
|
247
482
|
transcript,
|
|
248
483
|
}, null, 2),
|
|
249
484
|
}],
|
|
@@ -254,6 +489,71 @@ class MCPCollabServer {
|
|
|
254
489
|
}
|
|
255
490
|
);
|
|
256
491
|
|
|
492
|
+
this.server.tool(
|
|
493
|
+
'collab_get_transcript',
|
|
494
|
+
'Get transcript messages with optional role filtering',
|
|
495
|
+
{
|
|
496
|
+
sessionId: z.string().optional().describe('Session ID (defaults to current session)'),
|
|
497
|
+
role: z.enum(['planner', 'critic', 'coder', 'reviewer', 'all']).default('all').describe('Role filter'),
|
|
498
|
+
limit: z.number().int().positive().max(500).default(100).describe('Max messages to return'),
|
|
499
|
+
},
|
|
500
|
+
async ({ sessionId, role, limit }) => {
|
|
501
|
+
try {
|
|
502
|
+
const id = sessionId || await loadCurrentSessionId();
|
|
503
|
+
if (!id) throw new Error('No active session found');
|
|
504
|
+
const transcript = await loadTranscript(id);
|
|
505
|
+
const filtered = transcript
|
|
506
|
+
.filter((msg) => role === 'all' || msg.from === role)
|
|
507
|
+
.slice(-limit);
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
content: [{
|
|
511
|
+
type: 'text',
|
|
512
|
+
text: JSON.stringify({
|
|
513
|
+
sessionId: id,
|
|
514
|
+
role,
|
|
515
|
+
count: filtered.length,
|
|
516
|
+
transcript: filtered,
|
|
517
|
+
}, null, 2),
|
|
518
|
+
}],
|
|
519
|
+
};
|
|
520
|
+
} catch (error) {
|
|
521
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }], isError: true };
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
this.server.tool(
|
|
527
|
+
'collab_get_meeting_log',
|
|
528
|
+
'Get generated meeting log and summary artifacts',
|
|
529
|
+
{
|
|
530
|
+
sessionId: z.string().optional().describe('Session ID (defaults to current session)'),
|
|
531
|
+
},
|
|
532
|
+
async ({ sessionId }) => {
|
|
533
|
+
try {
|
|
534
|
+
const id = sessionId || await loadCurrentSessionId();
|
|
535
|
+
if (!id) throw new Error('No active session found');
|
|
536
|
+
const state = await loadSessionState(id);
|
|
537
|
+
|
|
538
|
+
return {
|
|
539
|
+
content: [{
|
|
540
|
+
type: 'text',
|
|
541
|
+
text: JSON.stringify({
|
|
542
|
+
sessionId: id,
|
|
543
|
+
status: state.status,
|
|
544
|
+
finalSummary: state.run?.finalSummary || null,
|
|
545
|
+
sharedContext: state.run?.sharedContext || null,
|
|
546
|
+
meetingSummary: await readSessionArtifact(id, 'meeting-summary.md'),
|
|
547
|
+
meetingLog: await readSessionArtifact(id, 'meeting-log.md'),
|
|
548
|
+
}, null, 2),
|
|
549
|
+
}],
|
|
550
|
+
};
|
|
551
|
+
} catch (error) {
|
|
552
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }], isError: true };
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
);
|
|
556
|
+
|
|
257
557
|
this.server.tool(
|
|
258
558
|
'collab_stop_session',
|
|
259
559
|
'Stop current collaborative session',
|
|
@@ -271,7 +571,12 @@ class MCPCollabServer {
|
|
|
271
571
|
return {
|
|
272
572
|
content: [{
|
|
273
573
|
type: 'text',
|
|
274
|
-
text: JSON.stringify({
|
|
574
|
+
text: JSON.stringify({
|
|
575
|
+
success: true,
|
|
576
|
+
sessionId: id,
|
|
577
|
+
status: updated.status,
|
|
578
|
+
run: summarizeRun(updated),
|
|
579
|
+
}, null, 2),
|
|
275
580
|
}],
|
|
276
581
|
};
|
|
277
582
|
} catch (error) {
|