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
|
@@ -1,12 +1,21 @@
|
|
|
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
|
|
|
@@ -17,11 +26,115 @@ function buildRuntimePrompt({ state, role }) {
|
|
|
17
26
|
task: state.task,
|
|
18
27
|
round: state.round,
|
|
19
28
|
messages: state.messages,
|
|
29
|
+
sharedContext: state.run?.sharedContext || null,
|
|
20
30
|
});
|
|
21
31
|
|
|
22
32
|
return [roleContext, '', collaborativePrompt].join('\n');
|
|
23
33
|
}
|
|
24
34
|
|
|
35
|
+
function ensureRunState(state) {
|
|
36
|
+
if (state.run && typeof state.run === 'object') {
|
|
37
|
+
return {
|
|
38
|
+
...state.run,
|
|
39
|
+
sharedContext: state.run.sharedContext && typeof state.run.sharedContext === 'object'
|
|
40
|
+
? {
|
|
41
|
+
decisions: Array.isArray(state.run.sharedContext.decisions) ? state.run.sharedContext.decisions : [],
|
|
42
|
+
openIssues: Array.isArray(state.run.sharedContext.openIssues) ? state.run.sharedContext.openIssues : [],
|
|
43
|
+
risks: Array.isArray(state.run.sharedContext.risks) ? state.run.sharedContext.risks : [],
|
|
44
|
+
actionItems: Array.isArray(state.run.sharedContext.actionItems) ? state.run.sharedContext.actionItems : [],
|
|
45
|
+
notes: Array.isArray(state.run.sharedContext.notes) ? state.run.sharedContext.notes : [],
|
|
46
|
+
}
|
|
47
|
+
: {
|
|
48
|
+
decisions: [],
|
|
49
|
+
openIssues: [],
|
|
50
|
+
risks: [],
|
|
51
|
+
actionItems: [],
|
|
52
|
+
notes: [],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
runId: null,
|
|
58
|
+
status: 'idle',
|
|
59
|
+
startedAt: null,
|
|
60
|
+
finishedAt: null,
|
|
61
|
+
currentRole: null,
|
|
62
|
+
retriesUsed: {},
|
|
63
|
+
round: state.round || 1,
|
|
64
|
+
events: [],
|
|
65
|
+
finalSummary: null,
|
|
66
|
+
lastError: null,
|
|
67
|
+
policy: {
|
|
68
|
+
timeoutPerRoleMs: 180000,
|
|
69
|
+
retryOnTimeout: 1,
|
|
70
|
+
fallbackOnFailure: 'abort',
|
|
71
|
+
maxRounds: state.maxRounds,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function applyRoleFailurePolicy(state, role, errorMessage) {
|
|
77
|
+
let run = ensureRunState(state);
|
|
78
|
+
const policy = run.policy || {};
|
|
79
|
+
const currentRetries = roleRetryCount(run, role);
|
|
80
|
+
const canRetry = currentRetries < (policy.retryOnTimeout ?? 0);
|
|
81
|
+
|
|
82
|
+
run = appendRunEvent(run, 'role_failed', {
|
|
83
|
+
role,
|
|
84
|
+
retry: canRetry,
|
|
85
|
+
error: errorMessage,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (canRetry) {
|
|
89
|
+
run = incrementRoleRetry(run, role);
|
|
90
|
+
return {
|
|
91
|
+
...state,
|
|
92
|
+
run: {
|
|
93
|
+
...run,
|
|
94
|
+
currentRole: role,
|
|
95
|
+
status: 'running',
|
|
96
|
+
lastError: null,
|
|
97
|
+
},
|
|
98
|
+
activeAgent: null,
|
|
99
|
+
// retry same role by rewinding index
|
|
100
|
+
nextRoleIndex: state.roles.indexOf(role),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const fallback = policy.fallbackOnFailure || 'abort';
|
|
105
|
+
if (fallback === 'skip') {
|
|
106
|
+
const skipped = appendRunEvent(run, 'role_skipped', { role, error: errorMessage });
|
|
107
|
+
return {
|
|
108
|
+
...state,
|
|
109
|
+
run: {
|
|
110
|
+
...skipped,
|
|
111
|
+
currentRole: null,
|
|
112
|
+
status: 'running',
|
|
113
|
+
lastError: null,
|
|
114
|
+
},
|
|
115
|
+
activeAgent: null,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const failed = appendRunEvent(run, 'run_failed', { role, error: errorMessage });
|
|
120
|
+
return {
|
|
121
|
+
...state,
|
|
122
|
+
status: 'failed',
|
|
123
|
+
activeAgent: null,
|
|
124
|
+
run: {
|
|
125
|
+
...failed,
|
|
126
|
+
status: 'failed',
|
|
127
|
+
currentRole: null,
|
|
128
|
+
finishedAt: new Date().toISOString(),
|
|
129
|
+
lastError: {
|
|
130
|
+
code: 'ROLE_FAILURE',
|
|
131
|
+
message: errorMessage,
|
|
132
|
+
role,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
25
138
|
export async function runTurn(sessionId, options = {}) {
|
|
26
139
|
return withSessionLock(sessionId, async () => {
|
|
27
140
|
let state = await loadSessionState(sessionId);
|
|
@@ -44,7 +157,7 @@ export async function runTurn(sessionId, options = {}) {
|
|
|
44
157
|
let content;
|
|
45
158
|
try {
|
|
46
159
|
const effectiveModel = resolveRoleModel(state, scheduled.role, options.model);
|
|
47
|
-
|
|
160
|
+
const output = await runOpenCodePromptDetailed({
|
|
48
161
|
prompt,
|
|
49
162
|
cwd: options.cwd || process.cwd(),
|
|
50
163
|
model: effectiveModel,
|
|
@@ -52,6 +165,7 @@ export async function runTurn(sessionId, options = {}) {
|
|
|
52
165
|
binaryPath: options.opencodeBin,
|
|
53
166
|
timeoutMs: options.timeoutMs,
|
|
54
167
|
});
|
|
168
|
+
content = output.text;
|
|
55
169
|
} catch (error) {
|
|
56
170
|
content = `Agent failed: ${error.message}`;
|
|
57
171
|
}
|
|
@@ -109,7 +223,7 @@ export async function executeActiveTurn(sessionId, role, options = {}) {
|
|
|
109
223
|
let content;
|
|
110
224
|
try {
|
|
111
225
|
const effectiveModel = resolveRoleModel(state, role, options.model);
|
|
112
|
-
|
|
226
|
+
const output = await runOpenCodePromptDetailed({
|
|
113
227
|
prompt,
|
|
114
228
|
cwd: options.cwd || process.cwd(),
|
|
115
229
|
model: effectiveModel,
|
|
@@ -117,6 +231,7 @@ export async function executeActiveTurn(sessionId, role, options = {}) {
|
|
|
117
231
|
binaryPath: options.opencodeBin,
|
|
118
232
|
timeoutMs: options.timeoutMs,
|
|
119
233
|
});
|
|
234
|
+
content = output.text;
|
|
120
235
|
} catch (error) {
|
|
121
236
|
content = `Agent failed: ${error.message}`;
|
|
122
237
|
}
|
|
@@ -141,13 +256,33 @@ export async function runWorkerIteration(sessionId, role, options = {}) {
|
|
|
141
256
|
if (state.status !== 'running') return state;
|
|
142
257
|
if (!state.roles.includes(role)) return state;
|
|
143
258
|
|
|
259
|
+
const run = ensureRunState(state);
|
|
260
|
+
if (run.status === 'cancelled' || run.status === 'failed' || run.status === 'completed') {
|
|
261
|
+
return state;
|
|
262
|
+
}
|
|
263
|
+
|
|
144
264
|
if (!state.activeAgent) {
|
|
145
265
|
const scheduled = nextRole(state);
|
|
266
|
+
const startedRun = run.status === 'idle'
|
|
267
|
+
? appendRunEvent({
|
|
268
|
+
...run,
|
|
269
|
+
status: 'running',
|
|
270
|
+
startedAt: new Date().toISOString(),
|
|
271
|
+
currentRole: scheduled.role,
|
|
272
|
+
round: scheduled.round,
|
|
273
|
+
}, 'run_started', { round: scheduled.round })
|
|
274
|
+
: appendRunEvent({
|
|
275
|
+
...run,
|
|
276
|
+
currentRole: scheduled.role,
|
|
277
|
+
round: scheduled.round,
|
|
278
|
+
}, 'role_scheduled', { role: scheduled.role, round: scheduled.round });
|
|
279
|
+
|
|
146
280
|
state = await saveSessionState({
|
|
147
281
|
...state,
|
|
148
282
|
activeAgent: scheduled.role,
|
|
149
283
|
nextRoleIndex: scheduled.nextRoleIndex,
|
|
150
284
|
round: scheduled.round,
|
|
285
|
+
run: startedRun,
|
|
151
286
|
});
|
|
152
287
|
}
|
|
153
288
|
|
|
@@ -157,28 +292,86 @@ export async function runWorkerIteration(sessionId, role, options = {}) {
|
|
|
157
292
|
|
|
158
293
|
const prompt = buildRuntimePrompt({ state, role });
|
|
159
294
|
let content;
|
|
295
|
+
let outputEvents = [];
|
|
296
|
+
let effectiveModel = null;
|
|
297
|
+
let failed = false;
|
|
298
|
+
let errorMessage = '';
|
|
160
299
|
try {
|
|
161
|
-
|
|
162
|
-
|
|
300
|
+
effectiveModel = resolveRoleModel(state, role, options.model);
|
|
301
|
+
state = await saveSessionState({
|
|
302
|
+
...state,
|
|
303
|
+
run: appendRunEvent({
|
|
304
|
+
...ensureRunState(state),
|
|
305
|
+
currentRole: role,
|
|
306
|
+
status: 'running',
|
|
307
|
+
}, 'role_started', { role, model: effectiveModel }),
|
|
308
|
+
});
|
|
309
|
+
const output = await runOpenCodePromptDetailed({
|
|
163
310
|
prompt,
|
|
164
311
|
cwd: options.cwd || process.cwd(),
|
|
165
312
|
model: effectiveModel,
|
|
166
313
|
agent: options.agent,
|
|
167
314
|
binaryPath: options.opencodeBin,
|
|
168
|
-
timeoutMs: options.timeoutMs,
|
|
315
|
+
timeoutMs: options.timeoutMs || ensureRunState(state).policy?.timeoutPerRoleMs || 180000,
|
|
169
316
|
});
|
|
317
|
+
content = output.text;
|
|
318
|
+
outputEvents = output.events || [];
|
|
170
319
|
} catch (error) {
|
|
320
|
+
failed = true;
|
|
321
|
+
errorMessage = error.message;
|
|
171
322
|
content = `Agent failed: ${error.message}`;
|
|
172
323
|
}
|
|
173
324
|
|
|
174
325
|
state = await addAgentMessage(state, role, content);
|
|
326
|
+
if (failed) {
|
|
327
|
+
await appendMeetingTurn(sessionId, createTurnRecord({
|
|
328
|
+
round: state.round,
|
|
329
|
+
role,
|
|
330
|
+
model: effectiveModel,
|
|
331
|
+
content,
|
|
332
|
+
events: outputEvents,
|
|
333
|
+
}));
|
|
334
|
+
state = await saveSessionState(applyRoleFailurePolicy(state, role, errorMessage));
|
|
335
|
+
return state;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const turnRecord = createTurnRecord({
|
|
339
|
+
round: state.round,
|
|
340
|
+
role,
|
|
341
|
+
model: effectiveModel,
|
|
342
|
+
content,
|
|
343
|
+
events: outputEvents,
|
|
344
|
+
});
|
|
345
|
+
await appendMeetingTurn(sessionId, turnRecord);
|
|
346
|
+
|
|
347
|
+
const updatedShared = updateSharedContext(ensureRunState(state).sharedContext, turnRecord);
|
|
348
|
+
const succeededRun = appendRunEvent({
|
|
349
|
+
...ensureRunState(state),
|
|
350
|
+
currentRole: null,
|
|
351
|
+
lastError: null,
|
|
352
|
+
sharedContext: updatedShared,
|
|
353
|
+
}, 'role_succeeded', { role, chars: content.length, events: outputEvents.length });
|
|
354
|
+
|
|
175
355
|
state = await saveSessionState({
|
|
176
356
|
...state,
|
|
177
357
|
activeAgent: null,
|
|
358
|
+
run: succeededRun,
|
|
178
359
|
});
|
|
179
360
|
|
|
180
361
|
if (shouldStop(state)) {
|
|
181
362
|
state = await stopSession(state, 'completed');
|
|
363
|
+
const summaryMd = buildMeetingSummary(state.messages, ensureRunState(state), ensureRunState(state).sharedContext);
|
|
364
|
+
await writeMeetingSummary(sessionId, summaryMd);
|
|
365
|
+
const finalRun = appendRunEvent({
|
|
366
|
+
...ensureRunState(state),
|
|
367
|
+
status: 'completed',
|
|
368
|
+
finishedAt: new Date().toISOString(),
|
|
369
|
+
finalSummary: extractFinalSummary(state.messages, ensureRunState(state)),
|
|
370
|
+
}, 'run_completed', { round: state.round });
|
|
371
|
+
state = await saveSessionState({
|
|
372
|
+
...state,
|
|
373
|
+
run: finalRun,
|
|
374
|
+
});
|
|
182
375
|
}
|
|
183
376
|
|
|
184
377
|
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
|
@@ -44,6 +44,7 @@ export function runTmux(command, args, options = {}) {
|
|
|
44
44
|
|
|
45
45
|
export async function spawnTmuxSession({ sessionName, sessionDir, sessionId }) {
|
|
46
46
|
const role0 = COLLAB_ROLES[0];
|
|
47
|
+
const role0Log = roleLogPath(sessionDir, role0);
|
|
47
48
|
await runTmux('tmux', [
|
|
48
49
|
'new-session',
|
|
49
50
|
'-d',
|
|
@@ -51,17 +52,18 @@ export async function spawnTmuxSession({ sessionName, sessionDir, sessionId }) {
|
|
|
51
52
|
sessionName,
|
|
52
53
|
'-n',
|
|
53
54
|
role0,
|
|
54
|
-
`bash -lc 'node "${runnerPath}" agents worker --session ${sessionId} --role ${role0}
|
|
55
|
+
`bash -lc 'node "${runnerPath}" agents worker --session ${sessionId} --role ${role0} 2>&1 | tee -a "${role0Log}"'`,
|
|
55
56
|
]);
|
|
56
57
|
|
|
57
58
|
for (let idx = 1; idx < COLLAB_ROLES.length; idx += 1) {
|
|
58
59
|
const role = COLLAB_ROLES[idx];
|
|
60
|
+
const roleLog = roleLogPath(sessionDir, role);
|
|
59
61
|
await runTmux('tmux', [
|
|
60
62
|
'split-window',
|
|
61
63
|
'-t',
|
|
62
64
|
sessionName,
|
|
63
65
|
'-v',
|
|
64
|
-
`bash -lc 'node "${runnerPath}" agents worker --session ${sessionId} --role ${role}
|
|
66
|
+
`bash -lc 'node "${runnerPath}" agents worker --session ${sessionId} --role ${role} 2>&1 | tee -a "${roleLog}"'`,
|
|
65
67
|
]);
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
SESSION_ROOT_DIR,
|
|
9
9
|
CURRENT_SESSION_FILE,
|
|
10
10
|
} from './constants.js';
|
|
11
|
+
import { createRunState } from './run-state.js';
|
|
11
12
|
import { sanitizeRoleModels } from './model-selection.js';
|
|
12
13
|
|
|
13
14
|
function sleep(ms) {
|
|
@@ -38,6 +39,22 @@ function getTranscriptPath(sessionId) {
|
|
|
38
39
|
return join(getSessionDir(sessionId), 'transcript.jsonl');
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
function getTurnsDir(sessionId) {
|
|
43
|
+
return join(getSessionDir(sessionId), 'turns');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getMeetingLogPath(sessionId) {
|
|
47
|
+
return join(getSessionDir(sessionId), 'meeting-log.md');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getMeetingLogJsonlPath(sessionId) {
|
|
51
|
+
return join(getSessionDir(sessionId), 'meeting-log.jsonl');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getMeetingSummaryPath(sessionId) {
|
|
55
|
+
return join(getSessionDir(sessionId), 'meeting-summary.md');
|
|
56
|
+
}
|
|
57
|
+
|
|
41
58
|
function initialState(task, options = {}) {
|
|
42
59
|
const sessionId = randomUUID();
|
|
43
60
|
const createdAt = new Date().toISOString();
|
|
@@ -57,6 +74,7 @@ function initialState(task, options = {}) {
|
|
|
57
74
|
roleModels: sanitizeRoleModels(options.roleModels),
|
|
58
75
|
opencodeBin: options.opencodeBin || null,
|
|
59
76
|
tmuxSessionName: options.tmuxSessionName || null,
|
|
77
|
+
run: createRunState(options.runPolicy, Number.isInteger(options.maxRounds) ? options.maxRounds : DEFAULT_MAX_ROUNDS),
|
|
60
78
|
messages: [
|
|
61
79
|
{
|
|
62
80
|
from: 'user',
|
|
@@ -91,6 +109,57 @@ export async function appendTranscript(sessionId, message) {
|
|
|
91
109
|
await appendFile(transcriptPath, line, 'utf8');
|
|
92
110
|
}
|
|
93
111
|
|
|
112
|
+
export async function appendMeetingTurn(sessionId, turnRecord) {
|
|
113
|
+
const sessionDir = getSessionDir(sessionId);
|
|
114
|
+
const turnsDir = getTurnsDir(sessionId);
|
|
115
|
+
await mkdir(sessionDir, { recursive: true });
|
|
116
|
+
await mkdir(turnsDir, { recursive: true });
|
|
117
|
+
|
|
118
|
+
const safeRole = String(turnRecord?.role || 'unknown').replace(/[^a-z0-9_-]/gi, '_');
|
|
119
|
+
const safeRound = Number.isInteger(turnRecord?.round) ? turnRecord.round : 0;
|
|
120
|
+
const turnFilePath = join(turnsDir, `${String(safeRound).padStart(3, '0')}-${safeRole}.json`);
|
|
121
|
+
await writeFile(turnFilePath, JSON.stringify(turnRecord, null, 2) + '\n', 'utf8');
|
|
122
|
+
|
|
123
|
+
const mdPath = getMeetingLogPath(sessionId);
|
|
124
|
+
const jsonlPath = getMeetingLogJsonlPath(sessionId);
|
|
125
|
+
const snippet = (turnRecord?.snippet || '').trim() || '(empty output)';
|
|
126
|
+
const keyPoints = Array.isArray(turnRecord?.keyPoints) ? turnRecord.keyPoints : [];
|
|
127
|
+
|
|
128
|
+
const mdBlock = [
|
|
129
|
+
`## Round ${safeRound} - ${safeRole}`,
|
|
130
|
+
`- timestamp: ${turnRecord?.timestamp || new Date().toISOString()}`,
|
|
131
|
+
`- model: ${turnRecord?.model || '(default)'}`,
|
|
132
|
+
`- events: ${turnRecord?.eventCount ?? 0}`,
|
|
133
|
+
'',
|
|
134
|
+
'### Output snippet',
|
|
135
|
+
snippet,
|
|
136
|
+
'',
|
|
137
|
+
'### Key points',
|
|
138
|
+
...(keyPoints.length > 0 ? keyPoints.map((line) => `- ${line.replace(/^[-*]\s+/, '')}`) : ['- (none)']),
|
|
139
|
+
'',
|
|
140
|
+
].join('\n');
|
|
141
|
+
|
|
142
|
+
if (!existsSync(mdPath)) {
|
|
143
|
+
const header = `# SynapseGrid Meeting Log\n\nSession: ${sessionId}\n\n`;
|
|
144
|
+
await writeFile(mdPath, header + mdBlock, 'utf8');
|
|
145
|
+
} else {
|
|
146
|
+
await appendFile(mdPath, mdBlock, 'utf8');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
await appendFile(jsonlPath, JSON.stringify(turnRecord) + '\n', 'utf8');
|
|
150
|
+
return {
|
|
151
|
+
turnFilePath,
|
|
152
|
+
meetingLogPath: mdPath,
|
|
153
|
+
meetingJsonlPath: jsonlPath,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function writeMeetingSummary(sessionId, summaryMarkdown) {
|
|
158
|
+
const outputPath = getMeetingSummaryPath(sessionId);
|
|
159
|
+
await writeFile(outputPath, String(summaryMarkdown || '').trimEnd() + '\n', 'utf8');
|
|
160
|
+
return outputPath;
|
|
161
|
+
}
|
|
162
|
+
|
|
94
163
|
export async function loadCurrentSessionId() {
|
|
95
164
|
if (!existsSync(CURRENT_SESSION_FILE)) return null;
|
|
96
165
|
const raw = await readFile(CURRENT_SESSION_FILE, 'utf8');
|
|
@@ -109,7 +178,6 @@ export async function saveSessionState(state) {
|
|
|
109
178
|
updatedAt: new Date().toISOString(),
|
|
110
179
|
};
|
|
111
180
|
await writeFile(getSessionStatePath(updated.sessionId), JSON.stringify(updated, null, 2) + '\n', 'utf8');
|
|
112
|
-
await writeCurrentSession(updated.sessionId, updated.updatedAt);
|
|
113
181
|
return updated;
|
|
114
182
|
}
|
|
115
183
|
|