ac-framework 1.9.7 → 1.9.9
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 +37 -11
- package/package.json +1 -1
- package/src/agents/collab-summary.js +120 -0
- package/src/agents/config-store.js +3 -0
- package/src/agents/constants.js +3 -1
- package/src/agents/opencode-client.js +101 -5
- package/src/agents/orchestrator.js +225 -6
- package/src/agents/role-prompts.js +34 -1
- package/src/agents/run-state.js +113 -0
- package/src/agents/runtime.js +75 -3
- package/src/agents/state-store.js +73 -0
- package/src/commands/agents.js +616 -60
- package/src/commands/init.js +9 -5
- package/src/mcp/collab-server.js +378 -24
- package/src/mcp/test-harness.mjs +410 -0
- package/src/services/dependency-installer.js +72 -4
|
@@ -1,15 +1,38 @@
|
|
|
1
1
|
import { buildAgentPrompt, ROLE_SYSTEM_PROMPTS } from './role-prompts.js';
|
|
2
|
-
import {
|
|
2
|
+
import { runOpenCodePromptDetailed } from './opencode-client.js';
|
|
3
3
|
import { nextRole, shouldStop } from './scheduler.js';
|
|
4
4
|
import { resolveRoleModel } from './model-selection.js';
|
|
5
|
+
import { buildMeetingSummary, createTurnRecord, updateSharedContext } from './collab-summary.js';
|
|
6
|
+
import {
|
|
7
|
+
appendRunEvent,
|
|
8
|
+
extractFinalSummary,
|
|
9
|
+
incrementRoleRetry,
|
|
10
|
+
roleRetryCount,
|
|
11
|
+
} from './run-state.js';
|
|
5
12
|
import {
|
|
6
13
|
addAgentMessage,
|
|
14
|
+
appendMeetingTurn,
|
|
7
15
|
loadSessionState,
|
|
8
16
|
saveSessionState,
|
|
9
17
|
stopSession,
|
|
18
|
+
writeMeetingSummary,
|
|
10
19
|
withSessionLock,
|
|
11
20
|
} from './state-store.js';
|
|
12
21
|
|
|
22
|
+
async function finalizeSessionArtifacts(state) {
|
|
23
|
+
const runState = ensureRunState(state);
|
|
24
|
+
const summaryMd = buildMeetingSummary(state.messages, runState, runState.sharedContext);
|
|
25
|
+
await writeMeetingSummary(state.sessionId, summaryMd);
|
|
26
|
+
const completedRun = {
|
|
27
|
+
...runState,
|
|
28
|
+
finalSummary: extractFinalSummary(state.messages, runState),
|
|
29
|
+
};
|
|
30
|
+
return saveSessionState({
|
|
31
|
+
...state,
|
|
32
|
+
run: completedRun,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
13
36
|
function buildRuntimePrompt({ state, role }) {
|
|
14
37
|
const roleContext = ROLE_SYSTEM_PROMPTS[role] || '';
|
|
15
38
|
const collaborativePrompt = buildAgentPrompt({
|
|
@@ -17,17 +40,129 @@ function buildRuntimePrompt({ state, role }) {
|
|
|
17
40
|
task: state.task,
|
|
18
41
|
round: state.round,
|
|
19
42
|
messages: state.messages,
|
|
43
|
+
sharedContext: state.run?.sharedContext || null,
|
|
20
44
|
});
|
|
21
45
|
|
|
22
46
|
return [roleContext, '', collaborativePrompt].join('\n');
|
|
23
47
|
}
|
|
24
48
|
|
|
49
|
+
function ensureRunState(state) {
|
|
50
|
+
if (state.run && typeof state.run === 'object') {
|
|
51
|
+
return {
|
|
52
|
+
...state.run,
|
|
53
|
+
sharedContext: state.run.sharedContext && typeof state.run.sharedContext === 'object'
|
|
54
|
+
? {
|
|
55
|
+
decisions: Array.isArray(state.run.sharedContext.decisions) ? state.run.sharedContext.decisions : [],
|
|
56
|
+
openIssues: Array.isArray(state.run.sharedContext.openIssues) ? state.run.sharedContext.openIssues : [],
|
|
57
|
+
risks: Array.isArray(state.run.sharedContext.risks) ? state.run.sharedContext.risks : [],
|
|
58
|
+
actionItems: Array.isArray(state.run.sharedContext.actionItems) ? state.run.sharedContext.actionItems : [],
|
|
59
|
+
notes: Array.isArray(state.run.sharedContext.notes) ? state.run.sharedContext.notes : [],
|
|
60
|
+
}
|
|
61
|
+
: {
|
|
62
|
+
decisions: [],
|
|
63
|
+
openIssues: [],
|
|
64
|
+
risks: [],
|
|
65
|
+
actionItems: [],
|
|
66
|
+
notes: [],
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
runId: null,
|
|
72
|
+
status: 'idle',
|
|
73
|
+
startedAt: null,
|
|
74
|
+
finishedAt: null,
|
|
75
|
+
currentRole: null,
|
|
76
|
+
retriesUsed: {},
|
|
77
|
+
round: state.round || 1,
|
|
78
|
+
events: [],
|
|
79
|
+
finalSummary: null,
|
|
80
|
+
sharedContext: {
|
|
81
|
+
decisions: [],
|
|
82
|
+
openIssues: [],
|
|
83
|
+
risks: [],
|
|
84
|
+
actionItems: [],
|
|
85
|
+
notes: [],
|
|
86
|
+
},
|
|
87
|
+
lastError: null,
|
|
88
|
+
policy: {
|
|
89
|
+
timeoutPerRoleMs: 180000,
|
|
90
|
+
retryOnTimeout: 1,
|
|
91
|
+
fallbackOnFailure: 'abort',
|
|
92
|
+
maxRounds: state.maxRounds,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function applyRoleFailurePolicy(state, role, errorMessage) {
|
|
98
|
+
let run = ensureRunState(state);
|
|
99
|
+
const policy = run.policy || {};
|
|
100
|
+
const currentRetries = roleRetryCount(run, role);
|
|
101
|
+
const canRetry = currentRetries < (policy.retryOnTimeout ?? 0);
|
|
102
|
+
|
|
103
|
+
run = appendRunEvent(run, 'role_failed', {
|
|
104
|
+
role,
|
|
105
|
+
retry: canRetry,
|
|
106
|
+
error: errorMessage,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (canRetry) {
|
|
110
|
+
run = incrementRoleRetry(run, role);
|
|
111
|
+
return {
|
|
112
|
+
...state,
|
|
113
|
+
run: {
|
|
114
|
+
...run,
|
|
115
|
+
currentRole: role,
|
|
116
|
+
status: 'running',
|
|
117
|
+
lastError: null,
|
|
118
|
+
},
|
|
119
|
+
activeAgent: null,
|
|
120
|
+
// retry same role by rewinding index
|
|
121
|
+
nextRoleIndex: state.roles.indexOf(role),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const fallback = policy.fallbackOnFailure || 'abort';
|
|
126
|
+
if (fallback === 'skip') {
|
|
127
|
+
const skipped = appendRunEvent(run, 'role_skipped', { role, error: errorMessage });
|
|
128
|
+
return {
|
|
129
|
+
...state,
|
|
130
|
+
run: {
|
|
131
|
+
...skipped,
|
|
132
|
+
currentRole: null,
|
|
133
|
+
status: 'running',
|
|
134
|
+
lastError: null,
|
|
135
|
+
},
|
|
136
|
+
activeAgent: null,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const failed = appendRunEvent(run, 'run_failed', { role, error: errorMessage });
|
|
141
|
+
return {
|
|
142
|
+
...state,
|
|
143
|
+
status: 'failed',
|
|
144
|
+
activeAgent: null,
|
|
145
|
+
run: {
|
|
146
|
+
...failed,
|
|
147
|
+
status: 'failed',
|
|
148
|
+
currentRole: null,
|
|
149
|
+
finishedAt: new Date().toISOString(),
|
|
150
|
+
lastError: {
|
|
151
|
+
code: 'ROLE_FAILURE',
|
|
152
|
+
message: errorMessage,
|
|
153
|
+
role,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
25
159
|
export async function runTurn(sessionId, options = {}) {
|
|
26
160
|
return withSessionLock(sessionId, async () => {
|
|
27
161
|
let state = await loadSessionState(sessionId);
|
|
28
162
|
if (shouldStop(state)) {
|
|
29
163
|
if (state.status === 'running') {
|
|
30
164
|
state = await stopSession(state, 'completed');
|
|
165
|
+
state = await finalizeSessionArtifacts(state);
|
|
31
166
|
}
|
|
32
167
|
return state;
|
|
33
168
|
}
|
|
@@ -44,7 +179,7 @@ export async function runTurn(sessionId, options = {}) {
|
|
|
44
179
|
let content;
|
|
45
180
|
try {
|
|
46
181
|
const effectiveModel = resolveRoleModel(state, scheduled.role, options.model);
|
|
47
|
-
|
|
182
|
+
const output = await runOpenCodePromptDetailed({
|
|
48
183
|
prompt,
|
|
49
184
|
cwd: options.cwd || process.cwd(),
|
|
50
185
|
model: effectiveModel,
|
|
@@ -52,6 +187,7 @@ export async function runTurn(sessionId, options = {}) {
|
|
|
52
187
|
binaryPath: options.opencodeBin,
|
|
53
188
|
timeoutMs: options.timeoutMs,
|
|
54
189
|
});
|
|
190
|
+
content = output.text;
|
|
55
191
|
} catch (error) {
|
|
56
192
|
content = `Agent failed: ${error.message}`;
|
|
57
193
|
}
|
|
@@ -64,6 +200,7 @@ export async function runTurn(sessionId, options = {}) {
|
|
|
64
200
|
|
|
65
201
|
if (shouldStop(state)) {
|
|
66
202
|
state = await stopSession(state, 'completed');
|
|
203
|
+
state = await finalizeSessionArtifacts(state);
|
|
67
204
|
}
|
|
68
205
|
|
|
69
206
|
return state;
|
|
@@ -109,7 +246,7 @@ export async function executeActiveTurn(sessionId, role, options = {}) {
|
|
|
109
246
|
let content;
|
|
110
247
|
try {
|
|
111
248
|
const effectiveModel = resolveRoleModel(state, role, options.model);
|
|
112
|
-
|
|
249
|
+
const output = await runOpenCodePromptDetailed({
|
|
113
250
|
prompt,
|
|
114
251
|
cwd: options.cwd || process.cwd(),
|
|
115
252
|
model: effectiveModel,
|
|
@@ -117,6 +254,7 @@ export async function executeActiveTurn(sessionId, role, options = {}) {
|
|
|
117
254
|
binaryPath: options.opencodeBin,
|
|
118
255
|
timeoutMs: options.timeoutMs,
|
|
119
256
|
});
|
|
257
|
+
content = output.text;
|
|
120
258
|
} catch (error) {
|
|
121
259
|
content = `Agent failed: ${error.message}`;
|
|
122
260
|
}
|
|
@@ -129,6 +267,7 @@ export async function executeActiveTurn(sessionId, role, options = {}) {
|
|
|
129
267
|
|
|
130
268
|
if (shouldStop(state)) {
|
|
131
269
|
state = await stopSession(state, 'completed');
|
|
270
|
+
state = await finalizeSessionArtifacts(state);
|
|
132
271
|
}
|
|
133
272
|
|
|
134
273
|
return state;
|
|
@@ -141,13 +280,33 @@ export async function runWorkerIteration(sessionId, role, options = {}) {
|
|
|
141
280
|
if (state.status !== 'running') return state;
|
|
142
281
|
if (!state.roles.includes(role)) return state;
|
|
143
282
|
|
|
283
|
+
const run = ensureRunState(state);
|
|
284
|
+
if (run.status === 'cancelled' || run.status === 'failed' || run.status === 'completed') {
|
|
285
|
+
return state;
|
|
286
|
+
}
|
|
287
|
+
|
|
144
288
|
if (!state.activeAgent) {
|
|
145
289
|
const scheduled = nextRole(state);
|
|
290
|
+
const startedRun = run.status === 'idle'
|
|
291
|
+
? appendRunEvent({
|
|
292
|
+
...run,
|
|
293
|
+
status: 'running',
|
|
294
|
+
startedAt: new Date().toISOString(),
|
|
295
|
+
currentRole: scheduled.role,
|
|
296
|
+
round: scheduled.round,
|
|
297
|
+
}, 'run_started', { round: scheduled.round })
|
|
298
|
+
: appendRunEvent({
|
|
299
|
+
...run,
|
|
300
|
+
currentRole: scheduled.role,
|
|
301
|
+
round: scheduled.round,
|
|
302
|
+
}, 'role_scheduled', { role: scheduled.role, round: scheduled.round });
|
|
303
|
+
|
|
146
304
|
state = await saveSessionState({
|
|
147
305
|
...state,
|
|
148
306
|
activeAgent: scheduled.role,
|
|
149
307
|
nextRoleIndex: scheduled.nextRoleIndex,
|
|
150
308
|
round: scheduled.round,
|
|
309
|
+
run: startedRun,
|
|
151
310
|
});
|
|
152
311
|
}
|
|
153
312
|
|
|
@@ -157,28 +316,88 @@ export async function runWorkerIteration(sessionId, role, options = {}) {
|
|
|
157
316
|
|
|
158
317
|
const prompt = buildRuntimePrompt({ state, role });
|
|
159
318
|
let content;
|
|
319
|
+
let outputEvents = [];
|
|
320
|
+
let effectiveModel = null;
|
|
321
|
+
let failed = false;
|
|
322
|
+
let errorMessage = '';
|
|
160
323
|
try {
|
|
161
|
-
|
|
162
|
-
|
|
324
|
+
effectiveModel = resolveRoleModel(state, role, options.model);
|
|
325
|
+
state = await saveSessionState({
|
|
326
|
+
...state,
|
|
327
|
+
run: appendRunEvent({
|
|
328
|
+
...ensureRunState(state),
|
|
329
|
+
currentRole: role,
|
|
330
|
+
status: 'running',
|
|
331
|
+
}, 'role_started', { role, model: effectiveModel }),
|
|
332
|
+
});
|
|
333
|
+
const output = await runOpenCodePromptDetailed({
|
|
163
334
|
prompt,
|
|
164
335
|
cwd: options.cwd || process.cwd(),
|
|
165
336
|
model: effectiveModel,
|
|
166
337
|
agent: options.agent,
|
|
167
338
|
binaryPath: options.opencodeBin,
|
|
168
|
-
timeoutMs: options.timeoutMs,
|
|
339
|
+
timeoutMs: options.timeoutMs || ensureRunState(state).policy?.timeoutPerRoleMs || 180000,
|
|
169
340
|
});
|
|
341
|
+
content = output.text;
|
|
342
|
+
outputEvents = output.events || [];
|
|
170
343
|
} catch (error) {
|
|
344
|
+
failed = true;
|
|
345
|
+
errorMessage = error.message;
|
|
171
346
|
content = `Agent failed: ${error.message}`;
|
|
172
347
|
}
|
|
173
348
|
|
|
174
349
|
state = await addAgentMessage(state, role, content);
|
|
350
|
+
if (failed) {
|
|
351
|
+
await appendMeetingTurn(sessionId, createTurnRecord({
|
|
352
|
+
round: state.round,
|
|
353
|
+
role,
|
|
354
|
+
model: effectiveModel,
|
|
355
|
+
content,
|
|
356
|
+
events: outputEvents,
|
|
357
|
+
}));
|
|
358
|
+
state = await saveSessionState(applyRoleFailurePolicy(state, role, errorMessage));
|
|
359
|
+
if (state.status === 'failed') {
|
|
360
|
+
state = await finalizeSessionArtifacts(state);
|
|
361
|
+
}
|
|
362
|
+
return state;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const turnRecord = createTurnRecord({
|
|
366
|
+
round: state.round,
|
|
367
|
+
role,
|
|
368
|
+
model: effectiveModel,
|
|
369
|
+
content,
|
|
370
|
+
events: outputEvents,
|
|
371
|
+
});
|
|
372
|
+
await appendMeetingTurn(sessionId, turnRecord);
|
|
373
|
+
|
|
374
|
+
const updatedShared = updateSharedContext(ensureRunState(state).sharedContext, turnRecord);
|
|
375
|
+
const succeededRun = appendRunEvent({
|
|
376
|
+
...ensureRunState(state),
|
|
377
|
+
currentRole: null,
|
|
378
|
+
lastError: null,
|
|
379
|
+
sharedContext: updatedShared,
|
|
380
|
+
}, 'role_succeeded', { role, chars: content.length, events: outputEvents.length });
|
|
381
|
+
|
|
175
382
|
state = await saveSessionState({
|
|
176
383
|
...state,
|
|
177
384
|
activeAgent: null,
|
|
385
|
+
run: succeededRun,
|
|
178
386
|
});
|
|
179
387
|
|
|
180
388
|
if (shouldStop(state)) {
|
|
181
389
|
state = await stopSession(state, 'completed');
|
|
390
|
+
const finalRun = appendRunEvent({
|
|
391
|
+
...ensureRunState(state),
|
|
392
|
+
status: 'completed',
|
|
393
|
+
finishedAt: new Date().toISOString(),
|
|
394
|
+
finalSummary: extractFinalSummary(state.messages, ensureRunState(state)),
|
|
395
|
+
}, 'run_completed', { round: state.round });
|
|
396
|
+
state = await saveSessionState({
|
|
397
|
+
...state,
|
|
398
|
+
run: finalRun,
|
|
399
|
+
});
|
|
400
|
+
state = await finalizeSessionArtifacts(state);
|
|
182
401
|
}
|
|
183
402
|
|
|
184
403
|
return state;
|
|
@@ -25,11 +25,41 @@ export const ROLE_SYSTEM_PROMPTS = {
|
|
|
25
25
|
].join(' '),
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
function formatSharedContext(sharedContext) {
|
|
29
|
+
if (!sharedContext || typeof sharedContext !== 'object') {
|
|
30
|
+
return 'No shared summary yet.';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const sections = [
|
|
34
|
+
['Decisions', sharedContext.decisions],
|
|
35
|
+
['Open issues', sharedContext.openIssues],
|
|
36
|
+
['Risks', sharedContext.risks],
|
|
37
|
+
['Action items', sharedContext.actionItems],
|
|
38
|
+
['Notes', sharedContext.notes],
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const lines = [];
|
|
42
|
+
for (const [name, list] of sections) {
|
|
43
|
+
const items = Array.isArray(list) ? list.slice(-6) : [];
|
|
44
|
+
lines.push(`${name}:`);
|
|
45
|
+
if (items.length === 0) {
|
|
46
|
+
lines.push('- (none)');
|
|
47
|
+
} else {
|
|
48
|
+
for (const item of items) {
|
|
49
|
+
lines.push(`- ${item}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return lines.join('\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function buildAgentPrompt({ role, task, round, messages, sharedContext = null, maxMessages = 18 }) {
|
|
29
58
|
const recent = messages.slice(-maxMessages);
|
|
30
59
|
const transcript = recent.length
|
|
31
60
|
? recent.map((msg, idx) => `${idx + 1}. [${msg.from}] ${msg.content}`).join('\n')
|
|
32
61
|
: 'No previous messages.';
|
|
62
|
+
const shared = formatSharedContext(sharedContext);
|
|
33
63
|
|
|
34
64
|
return [
|
|
35
65
|
`ROLE: ${role}`,
|
|
@@ -37,6 +67,9 @@ export function buildAgentPrompt({ role, task, round, messages, maxMessages = 24
|
|
|
37
67
|
'',
|
|
38
68
|
`TASK: ${task}`,
|
|
39
69
|
'',
|
|
70
|
+
'SHARED CONTEXT SUMMARY:',
|
|
71
|
+
shared,
|
|
72
|
+
'',
|
|
40
73
|
'TEAM TRANSCRIPT (latest first-order history):',
|
|
41
74
|
transcript,
|
|
42
75
|
'',
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_ROLE_RETRIES,
|
|
4
|
+
DEFAULT_ROLE_TIMEOUT_MS,
|
|
5
|
+
DEFAULT_MAX_ROUNDS,
|
|
6
|
+
} from './constants.js';
|
|
7
|
+
|
|
8
|
+
export function normalizeRunPolicy(policy = {}, maxRounds = DEFAULT_MAX_ROUNDS) {
|
|
9
|
+
const timeoutPerRoleMs = Number.isInteger(policy.timeoutPerRoleMs) && policy.timeoutPerRoleMs > 0
|
|
10
|
+
? policy.timeoutPerRoleMs
|
|
11
|
+
: DEFAULT_ROLE_TIMEOUT_MS;
|
|
12
|
+
const retryOnTimeout = Number.isInteger(policy.retryOnTimeout) && policy.retryOnTimeout >= 0
|
|
13
|
+
? policy.retryOnTimeout
|
|
14
|
+
: DEFAULT_ROLE_RETRIES;
|
|
15
|
+
const fallbackOnFailure = ['retry', 'skip', 'abort'].includes(policy.fallbackOnFailure)
|
|
16
|
+
? policy.fallbackOnFailure
|
|
17
|
+
: 'abort';
|
|
18
|
+
const rounds = Number.isInteger(maxRounds) && maxRounds > 0 ? maxRounds : DEFAULT_MAX_ROUNDS;
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
timeoutPerRoleMs,
|
|
22
|
+
retryOnTimeout,
|
|
23
|
+
fallbackOnFailure,
|
|
24
|
+
maxRounds: rounds,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createRunState(policy = {}, maxRounds = DEFAULT_MAX_ROUNDS) {
|
|
29
|
+
return {
|
|
30
|
+
runId: randomUUID(),
|
|
31
|
+
status: 'idle',
|
|
32
|
+
startedAt: null,
|
|
33
|
+
finishedAt: null,
|
|
34
|
+
currentRole: null,
|
|
35
|
+
retriesUsed: {},
|
|
36
|
+
round: 1,
|
|
37
|
+
events: [],
|
|
38
|
+
finalSummary: null,
|
|
39
|
+
sharedContext: {
|
|
40
|
+
decisions: [],
|
|
41
|
+
openIssues: [],
|
|
42
|
+
risks: [],
|
|
43
|
+
actionItems: [],
|
|
44
|
+
notes: [],
|
|
45
|
+
},
|
|
46
|
+
lastError: null,
|
|
47
|
+
policy: normalizeRunPolicy(policy, maxRounds),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function appendRunEvent(run, type, details = {}) {
|
|
52
|
+
const event = {
|
|
53
|
+
id: randomUUID(),
|
|
54
|
+
type,
|
|
55
|
+
timestamp: new Date().toISOString(),
|
|
56
|
+
...details,
|
|
57
|
+
};
|
|
58
|
+
const events = [...(run.events || []), event];
|
|
59
|
+
return {
|
|
60
|
+
...run,
|
|
61
|
+
events,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function roleRetryCount(run, role) {
|
|
66
|
+
return Number(run?.retriesUsed?.[role] || 0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function incrementRoleRetry(run, role) {
|
|
70
|
+
return {
|
|
71
|
+
...run,
|
|
72
|
+
retriesUsed: {
|
|
73
|
+
...(run.retriesUsed || {}),
|
|
74
|
+
[role]: roleRetryCount(run, role) + 1,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function extractFinalSummary(messages = [], run = null) {
|
|
80
|
+
const agentMessages = messages.filter((msg) => msg?.from && msg.from !== 'user');
|
|
81
|
+
if (agentMessages.length === 0) return '';
|
|
82
|
+
const orderedRoles = ['planner', 'critic', 'coder', 'reviewer'];
|
|
83
|
+
const lastByRole = new Map();
|
|
84
|
+
for (const msg of agentMessages) lastByRole.set(msg.from, msg.content || '');
|
|
85
|
+
|
|
86
|
+
const sections = ['# SynapseGrid Final Summary', ''];
|
|
87
|
+
sections.push('## Per-role last contributions');
|
|
88
|
+
for (const role of orderedRoles) {
|
|
89
|
+
const content = String(lastByRole.get(role) || '').trim();
|
|
90
|
+
sections.push(`- ${role}: ${content ? content.slice(0, 500) : '(none)'}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const shared = run?.sharedContext;
|
|
94
|
+
if (shared && typeof shared === 'object') {
|
|
95
|
+
const writeList = (title, items) => {
|
|
96
|
+
sections.push('');
|
|
97
|
+
sections.push(`## ${title}`);
|
|
98
|
+
const list = Array.isArray(items) ? items.slice(-10) : [];
|
|
99
|
+
if (list.length === 0) {
|
|
100
|
+
sections.push('- (none)');
|
|
101
|
+
} else {
|
|
102
|
+
for (const item of list) sections.push(`- ${item}`);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
writeList('Decisions', shared.decisions);
|
|
107
|
+
writeList('Open issues', shared.openIssues);
|
|
108
|
+
writeList('Risks', shared.risks);
|
|
109
|
+
writeList('Action items', shared.actionItems);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return sections.join('\n').trim();
|
|
113
|
+
}
|
package/src/agents/runtime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { writeFile } from 'node:fs/promises';
|
|
4
5
|
import { COLLAB_ROLES } from './constants.js';
|
|
5
6
|
|
|
6
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -10,7 +11,7 @@ export function roleLogPath(sessionDir, role) {
|
|
|
10
11
|
return resolve(sessionDir, `${role}.log`);
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
function runCommand(command, args, options = {}) {
|
|
14
15
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
15
16
|
const child = spawn(command, args, {
|
|
16
17
|
cwd: options.cwd || process.cwd(),
|
|
@@ -42,6 +43,14 @@ export function runTmux(command, args, options = {}) {
|
|
|
42
43
|
});
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function workerCommand(sessionId, role, roleLog) {
|
|
47
|
+
return `bash -lc 'node "${runnerPath}" agents worker --session ${sessionId} --role ${role} 2>&1 | tee -a "${roleLog}"'`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function runTmux(command, args, options = {}) {
|
|
51
|
+
return runCommand(command, args, options);
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
export async function spawnTmuxSession({ sessionName, sessionDir, sessionId }) {
|
|
46
55
|
const role0 = COLLAB_ROLES[0];
|
|
47
56
|
const role0Log = roleLogPath(sessionDir, role0);
|
|
@@ -52,7 +61,7 @@ export async function spawnTmuxSession({ sessionName, sessionDir, sessionId }) {
|
|
|
52
61
|
sessionName,
|
|
53
62
|
'-n',
|
|
54
63
|
role0,
|
|
55
|
-
|
|
64
|
+
workerCommand(sessionId, role0, role0Log),
|
|
56
65
|
]);
|
|
57
66
|
|
|
58
67
|
for (let idx = 1; idx < COLLAB_ROLES.length; idx += 1) {
|
|
@@ -63,7 +72,7 @@ export async function spawnTmuxSession({ sessionName, sessionDir, sessionId }) {
|
|
|
63
72
|
'-t',
|
|
64
73
|
sessionName,
|
|
65
74
|
'-v',
|
|
66
|
-
|
|
75
|
+
workerCommand(sessionId, role, roleLog),
|
|
67
76
|
]);
|
|
68
77
|
}
|
|
69
78
|
|
|
@@ -80,3 +89,66 @@ export async function tmuxSessionExists(sessionName) {
|
|
|
80
89
|
return false;
|
|
81
90
|
}
|
|
82
91
|
}
|
|
92
|
+
|
|
93
|
+
async function writeZellijLayout({ layoutPath, sessionId, sessionDir }) {
|
|
94
|
+
const panes = COLLAB_ROLES.map((role) => {
|
|
95
|
+
const roleLog = roleLogPath(sessionDir, role);
|
|
96
|
+
const cmd = workerCommand(sessionId, role, roleLog).replace(/"/g, '\\"');
|
|
97
|
+
return ` pane name="${role}" command="bash" args { "-lc" "${cmd}" }`;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const content = [
|
|
101
|
+
'layout {',
|
|
102
|
+
' default_tab_template {',
|
|
103
|
+
' tab name="SynapseGrid" {',
|
|
104
|
+
' pane split_direction="vertical" {',
|
|
105
|
+
' pane split_direction="horizontal" {',
|
|
106
|
+
panes[0],
|
|
107
|
+
panes[1],
|
|
108
|
+
' }',
|
|
109
|
+
' pane split_direction="horizontal" {',
|
|
110
|
+
panes[2],
|
|
111
|
+
panes[3],
|
|
112
|
+
' }',
|
|
113
|
+
' }',
|
|
114
|
+
' }',
|
|
115
|
+
' }',
|
|
116
|
+
'}',
|
|
117
|
+
'',
|
|
118
|
+
].join('\n');
|
|
119
|
+
|
|
120
|
+
await writeFile(layoutPath, content, 'utf8');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function spawnZellijSession({ sessionName, sessionDir, sessionId }) {
|
|
124
|
+
const layoutPath = resolve(sessionDir, 'synapsegrid-layout.kdl');
|
|
125
|
+
await writeZellijLayout({ layoutPath, sessionId, sessionDir });
|
|
126
|
+
await runCommand('zellij', ['--session', sessionName, '--layout', layoutPath, '--detach']);
|
|
127
|
+
return { layoutPath };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function zellijSessionExists(sessionName) {
|
|
131
|
+
try {
|
|
132
|
+
const result = await runCommand('zellij', ['list-sessions']);
|
|
133
|
+
const lines = result.stdout.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
134
|
+
return lines.some((line) => line === sessionName || line.startsWith(`${sessionName} `));
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function runZellij(args, options = {}) {
|
|
141
|
+
return runCommand('zellij', args, options);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function resolveMultiplexer(preferred = 'auto', hasTmuxCommand = false, hasZellijCommand = false) {
|
|
145
|
+
if (preferred === 'tmux') {
|
|
146
|
+
return hasTmuxCommand ? 'tmux' : null;
|
|
147
|
+
}
|
|
148
|
+
if (preferred === 'zellij') {
|
|
149
|
+
return hasZellijCommand ? 'zellij' : null;
|
|
150
|
+
}
|
|
151
|
+
if (hasZellijCommand) return 'zellij';
|
|
152
|
+
if (hasTmuxCommand) return 'tmux';
|
|
153
|
+
return null;
|
|
154
|
+
}
|