ac-framework 1.9.7 → 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 +25 -2
- package/package.json +1 -1
- package/src/agents/collab-summary.js +120 -0
- package/src/agents/constants.js +3 -1
- package/src/agents/opencode-client.js +101 -5
- 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/state-store.js +69 -0
- package/src/commands/agents.js +318 -4
- package/src/mcp/collab-server.js +307 -2
- package/src/mcp/test-harness.mjs +410 -0
package/README.md
CHANGED
|
@@ -150,24 +150,47 @@ Each role runs in turn against a shared, accumulating context so outputs from on
|
|
|
150
150
|
| `acfm agents attach` | Attach directly to the SynapseGrid tmux session |
|
|
151
151
|
| `acfm agents live` | Attach to full live tmux view (all agents) |
|
|
152
152
|
| `acfm agents logs` | Show recent worker logs (all roles or one role) |
|
|
153
|
+
| `acfm agents transcript --role all --limit 40` | Show captured cross-agent transcript |
|
|
154
|
+
| `acfm agents summary` | Show generated collaboration meeting summary |
|
|
153
155
|
| `acfm agents export --format md --out file.md` | Export transcript in Markdown or JSON |
|
|
154
156
|
| `acfm agents send "..."` | Send a new user message into the active session |
|
|
155
157
|
| `acfm agents status` | Show current collaborative session state |
|
|
158
|
+
| `acfm agents model list` | List available models grouped by provider |
|
|
159
|
+
| `acfm agents model choose` | Interactively pick provider/model and save target role |
|
|
156
160
|
| `acfm agents model get` | Show default model config (global and per-role) |
|
|
157
161
|
| `acfm agents model set --role coder provider/model` | Persist a default model for one role |
|
|
158
162
|
| `acfm agents model clear --role all` | Clear persisted model defaults |
|
|
159
163
|
| `acfm agents stop` | Stop the active collaborative session |
|
|
160
164
|
|
|
165
|
+
### MCP collaborative run mode (recommended)
|
|
166
|
+
|
|
167
|
+
When driving SynapseGrid from another agent via MCP, prefer asynchronous run tools over role-by-role stepping:
|
|
168
|
+
|
|
169
|
+
- `collab_start_session` to initialize session and optional tmux workers
|
|
170
|
+
- `collab_invoke_team` to launch full 4-role collaboration run
|
|
171
|
+
- `collab_wait_run` to wait for completion/failure with bounded timeout
|
|
172
|
+
- `collab_get_result` to fetch final consolidated output and run diagnostics
|
|
173
|
+
- `collab_cancel_run` to cancel a running collaboration safely
|
|
174
|
+
|
|
175
|
+
`collab_step` remains available for manual/debug control, but is less robust for long tasks.
|
|
176
|
+
|
|
161
177
|
### SynapseGrid troubleshooting
|
|
162
178
|
|
|
163
179
|
- If transcript entries show `Agent failed: spawn opencode ENOENT`, run `acfm agents setup` to install dependencies and then retry.
|
|
164
180
|
- Attach to worker panes with `acfm agents live` (or `acfm agents attach`) to see real-time role discussion.
|
|
165
181
|
- Inspect worker errors quickly with `acfm agents logs --role all --lines 120`.
|
|
182
|
+
- Inspect collaborative discussion with `acfm agents transcript` and `acfm agents summary`.
|
|
166
183
|
- MCP starts can now create tmux workers directly; if your assistant used headless steps before, start a new session and ensure worker spawning is enabled.
|
|
167
|
-
- Configure role models directly at start (for example `--model-planner`, `--model-coder`) or persist defaults via `acfm agents model set`.
|
|
168
|
-
- Default SynapseGrid model fallback is `opencode/
|
|
184
|
+
- Configure role models directly at start (for example `--model-planner`, `--model-coder`) or persist defaults via `acfm agents model choose` / `acfm agents model set`.
|
|
185
|
+
- Default SynapseGrid model fallback is `opencode/mimo-v2-pro-free`.
|
|
169
186
|
- Run `acfm agents doctor` when panes look idle to confirm model/provider preflight health.
|
|
170
187
|
|
|
188
|
+
Each collaborative session now keeps human-readable artifacts under `~/.acfm/synapsegrid/<sessionId>/`:
|
|
189
|
+
- `transcript.jsonl`: full chronological message stream
|
|
190
|
+
- `turns/*.json`: one file per round/role turn with captured output metadata
|
|
191
|
+
- `meeting-log.md`: incremental meeting notes generated per turn
|
|
192
|
+
- `meeting-summary.md`: final consolidated summary (roles, decisions, open issues, risks, action items)
|
|
193
|
+
|
|
171
194
|
### Spec Workflow
|
|
172
195
|
|
|
173
196
|
| Command | Description |
|
package/package.json
CHANGED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export function summarizeText(text, maxLen = 700) {
|
|
2
|
+
if (typeof text !== 'string') return '';
|
|
3
|
+
const normalized = text.replace(/\r\n/g, '\n').trim();
|
|
4
|
+
if (!normalized) return '';
|
|
5
|
+
if (normalized.length <= maxLen) return normalized;
|
|
6
|
+
return `${normalized.slice(0, Math.max(0, maxLen - 3)).trimEnd()}...`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function extractBulletLines(text, limit = 5) {
|
|
10
|
+
if (typeof text !== 'string') return [];
|
|
11
|
+
const bullets = text
|
|
12
|
+
.split('\n')
|
|
13
|
+
.map((line) => line.trim())
|
|
14
|
+
.filter((line) => /^[-*]\s+/.test(line) || /^\d+\)\s+/.test(line));
|
|
15
|
+
return bullets.slice(0, Math.max(0, limit));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createTurnRecord({ round, role, model, content, events }) {
|
|
19
|
+
const snippet = summarizeText(content, 1000);
|
|
20
|
+
const keyPoints = extractBulletLines(content, 6);
|
|
21
|
+
return {
|
|
22
|
+
round,
|
|
23
|
+
role,
|
|
24
|
+
model: model || null,
|
|
25
|
+
timestamp: new Date().toISOString(),
|
|
26
|
+
snippet,
|
|
27
|
+
keyPoints,
|
|
28
|
+
eventCount: Array.isArray(events) ? events.length : 0,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function updateSharedContext(prev, turn) {
|
|
33
|
+
const base = prev && typeof prev === 'object'
|
|
34
|
+
? prev
|
|
35
|
+
: { decisions: [], openIssues: [], risks: [], actionItems: [], notes: [] };
|
|
36
|
+
|
|
37
|
+
const next = {
|
|
38
|
+
decisions: [...(base.decisions || [])],
|
|
39
|
+
openIssues: [...(base.openIssues || [])],
|
|
40
|
+
risks: [...(base.risks || [])],
|
|
41
|
+
actionItems: [...(base.actionItems || [])],
|
|
42
|
+
notes: [...(base.notes || [])],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const summary = summarizeText(turn?.snippet || turn?.content || '', 300);
|
|
46
|
+
if (summary) {
|
|
47
|
+
next.notes.push(`[r${turn.round}] ${turn.role}: ${summary}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const lines = String(turn?.snippet || turn?.content || '')
|
|
51
|
+
.split('\n')
|
|
52
|
+
.map((line) => line.trim())
|
|
53
|
+
.filter(Boolean);
|
|
54
|
+
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
const lower = line.toLowerCase();
|
|
57
|
+
if (lower.includes('risk') || lower.includes('failure') || lower.includes('blind spot')) {
|
|
58
|
+
next.risks.push(`[r${turn.round}] ${turn.role}: ${line}`);
|
|
59
|
+
}
|
|
60
|
+
if (lower.includes('open issue') || lower.includes('blocker') || lower.includes('unresolved')) {
|
|
61
|
+
next.openIssues.push(`[r${turn.round}] ${turn.role}: ${line}`);
|
|
62
|
+
}
|
|
63
|
+
if (lower.includes('action') || lower.includes('next step') || lower.includes('implement')) {
|
|
64
|
+
next.actionItems.push(`[r${turn.round}] ${turn.role}: ${line}`);
|
|
65
|
+
}
|
|
66
|
+
if (lower.includes('decision') || lower.includes('agreed') || lower.includes('approve')) {
|
|
67
|
+
next.decisions.push(`[r${turn.round}] ${turn.role}: ${line}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const dedupe = (arr) => [...new Set(arr)].slice(-30);
|
|
72
|
+
return {
|
|
73
|
+
decisions: dedupe(next.decisions),
|
|
74
|
+
openIssues: dedupe(next.openIssues),
|
|
75
|
+
risks: dedupe(next.risks),
|
|
76
|
+
actionItems: dedupe(next.actionItems),
|
|
77
|
+
notes: dedupe(next.notes),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function buildMeetingSummary(messages = [], run = null, sharedContext = null) {
|
|
82
|
+
const byRole = new Map();
|
|
83
|
+
for (const msg of messages) {
|
|
84
|
+
if (!msg?.from || msg.from === 'user') continue;
|
|
85
|
+
if (!byRole.has(msg.from)) byRole.set(msg.from, []);
|
|
86
|
+
byRole.get(msg.from).push(msg.content || '');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const roles = ['planner', 'critic', 'coder', 'reviewer'];
|
|
90
|
+
const lines = [];
|
|
91
|
+
lines.push('# SynapseGrid Meeting Summary');
|
|
92
|
+
if (run?.runId) lines.push(`Run: ${run.runId}`);
|
|
93
|
+
if (run?.status) lines.push(`Status: ${run.status}`);
|
|
94
|
+
lines.push('');
|
|
95
|
+
lines.push('## Per-role recap');
|
|
96
|
+
|
|
97
|
+
for (const role of roles) {
|
|
98
|
+
const items = byRole.get(role) || [];
|
|
99
|
+
const last = items.length > 0 ? items[items.length - 1] : '';
|
|
100
|
+
lines.push(`- ${role}: ${summarizeText(last, 500) || '(no contribution captured)'}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const ctx = sharedContext && typeof sharedContext === 'object' ? sharedContext : null;
|
|
104
|
+
if (ctx) {
|
|
105
|
+
lines.push('');
|
|
106
|
+
lines.push('## Decisions');
|
|
107
|
+
for (const entry of (ctx.decisions || []).slice(-8)) lines.push(`- ${entry}`);
|
|
108
|
+
lines.push('');
|
|
109
|
+
lines.push('## Open issues');
|
|
110
|
+
for (const entry of (ctx.openIssues || []).slice(-8)) lines.push(`- ${entry}`);
|
|
111
|
+
lines.push('');
|
|
112
|
+
lines.push('## Risks');
|
|
113
|
+
for (const entry of (ctx.risks || []).slice(-8)) lines.push(`- ${entry}`);
|
|
114
|
+
lines.push('');
|
|
115
|
+
lines.push('## Action items');
|
|
116
|
+
for (const entry of (ctx.actionItems || []).slice(-8)) lines.push(`- ${entry}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return lines.join('\n').trim() + '\n';
|
|
120
|
+
}
|
package/src/agents/constants.js
CHANGED
|
@@ -4,6 +4,8 @@ import { join } from 'node:path';
|
|
|
4
4
|
export const COLLAB_SYSTEM_NAME = 'SynapseGrid';
|
|
5
5
|
export const COLLAB_ROLES = ['planner', 'critic', 'coder', 'reviewer'];
|
|
6
6
|
export const DEFAULT_MAX_ROUNDS = 3;
|
|
7
|
-
export const DEFAULT_SYNAPSE_MODEL = 'opencode/
|
|
7
|
+
export const DEFAULT_SYNAPSE_MODEL = 'opencode/mimo-v2-pro-free';
|
|
8
|
+
export const DEFAULT_ROLE_TIMEOUT_MS = 180000;
|
|
9
|
+
export const DEFAULT_ROLE_RETRIES = 1;
|
|
8
10
|
export const SESSION_ROOT_DIR = join(homedir(), '.acfm', 'synapsegrid');
|
|
9
11
|
export const CURRENT_SESSION_FILE = join(SESSION_ROOT_DIR, 'current-session.json');
|
|
@@ -42,7 +42,24 @@ function parseOpenCodeRunOutput(stdout) {
|
|
|
42
42
|
return stdout.trim();
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
function parseOpenCodeEvents(stdout) {
|
|
46
|
+
const lines = stdout
|
|
47
|
+
.split('\n')
|
|
48
|
+
.map((line) => line.trim())
|
|
49
|
+
.filter(Boolean);
|
|
50
|
+
|
|
51
|
+
const events = [];
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
try {
|
|
54
|
+
events.push(JSON.parse(line));
|
|
55
|
+
} catch {
|
|
56
|
+
// ignore malformed event lines
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return events;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function runOpenCodePromptDetailed({ prompt, cwd, model, agent, timeoutMs = 180000, binaryPath }) {
|
|
46
63
|
const binary = binaryPath || process.env.ACFM_OPENCODE_BIN || 'opencode';
|
|
47
64
|
const args = ['run', '--format', 'json'];
|
|
48
65
|
if (model) {
|
|
@@ -97,9 +114,88 @@ export async function runOpenCodePrompt({ prompt, cwd, model, agent, timeoutMs =
|
|
|
97
114
|
});
|
|
98
115
|
});
|
|
99
116
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
117
|
+
const text = parseOpenCodeRunOutput(stdout);
|
|
118
|
+
const events = parseOpenCodeEvents(stdout);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
text: text || (stderr?.trim() || ''),
|
|
122
|
+
stdout,
|
|
123
|
+
stderr,
|
|
124
|
+
events,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function runOpenCodePrompt({ prompt, cwd, model, agent, timeoutMs = 180000, binaryPath }) {
|
|
129
|
+
const detailed = await runOpenCodePromptDetailed({
|
|
130
|
+
prompt,
|
|
131
|
+
cwd,
|
|
132
|
+
model,
|
|
133
|
+
agent,
|
|
134
|
+
timeoutMs,
|
|
135
|
+
binaryPath,
|
|
136
|
+
});
|
|
137
|
+
return detailed.text;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function listOpenCodeModels({ provider = null, binaryPath, refresh = false, timeoutMs = 45000 }) {
|
|
141
|
+
const binary = binaryPath || process.env.ACFM_OPENCODE_BIN || 'opencode';
|
|
142
|
+
const args = ['models'];
|
|
143
|
+
if (provider) {
|
|
144
|
+
args.push(provider);
|
|
145
|
+
}
|
|
146
|
+
if (refresh) {
|
|
147
|
+
args.push('--refresh');
|
|
103
148
|
}
|
|
104
|
-
|
|
149
|
+
|
|
150
|
+
const stdout = await new Promise((resolvePromise, rejectPromise) => {
|
|
151
|
+
const child = spawn(binary, args, {
|
|
152
|
+
cwd: process.cwd(),
|
|
153
|
+
env: process.env,
|
|
154
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
let out = '';
|
|
158
|
+
let err = '';
|
|
159
|
+
let timedOut = false;
|
|
160
|
+
|
|
161
|
+
const timer = setTimeout(() => {
|
|
162
|
+
timedOut = true;
|
|
163
|
+
child.kill('SIGTERM');
|
|
164
|
+
setTimeout(() => child.kill('SIGKILL'), 1500).unref();
|
|
165
|
+
}, timeoutMs);
|
|
166
|
+
|
|
167
|
+
child.stdout.on('data', (chunk) => {
|
|
168
|
+
out += chunk.toString();
|
|
169
|
+
});
|
|
170
|
+
child.stderr.on('data', (chunk) => {
|
|
171
|
+
err += chunk.toString();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
child.on('error', (error) => {
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
rejectPromise(error);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
child.on('close', (code, signal) => {
|
|
180
|
+
clearTimeout(timer);
|
|
181
|
+
if (timedOut) {
|
|
182
|
+
rejectPromise(new Error(`opencode models timed out after ${timeoutMs}ms`));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (code !== 0) {
|
|
186
|
+
const details = [err.trim(), out.trim()].filter(Boolean).join(' | ');
|
|
187
|
+
rejectPromise(new Error(details || `opencode models exited with code ${code}${signal ? ` (${signal})` : ''}`));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
resolvePromise(out);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const models = stdout
|
|
195
|
+
.split('\n')
|
|
196
|
+
.map((line) => line.trim())
|
|
197
|
+
.filter(Boolean)
|
|
198
|
+
.filter((line) => line.includes('/'));
|
|
199
|
+
|
|
200
|
+
return [...new Set(models)].sort((a, b) => a.localeCompare(b));
|
|
105
201
|
}
|
|
@@ -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
|
'',
|