chainlesschain 0.47.6 → 0.47.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.
Files changed (107) hide show
  1. package/package.json +2 -2
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/Analytics-BFI7jbwM.css +1 -0
  4. package/src/assets/web-panel/assets/Analytics-DQ135mAd.js +3 -0
  5. package/src/assets/web-panel/assets/AppLayout-6SPt_8Y_.js +1 -0
  6. package/src/assets/web-panel/assets/AppLayout-BFJ-Fofn.css +1 -0
  7. package/src/assets/web-panel/assets/{Backup-Ba9UybpT.js → Backup-DbVRG5vE.js} +1 -1
  8. package/src/assets/web-panel/assets/{Chat-BwXskT21.js → Chat-wVhrFK9C.js} +1 -1
  9. package/src/assets/web-panel/assets/{Cowork-UmOe7qvE.js → Cowork-lOC25IW2.js} +1 -1
  10. package/src/assets/web-panel/assets/{Cron-JHS-rc-4.js → Cron-3P0eVLTV.js} +1 -1
  11. package/src/assets/web-panel/assets/{Dashboard-B95cMCO7.js → Dashboard-Br7kCwKJ.js} +1 -1
  12. package/src/assets/web-panel/assets/{Git-CSYO0_zk.js → Git-CrDCcBig.js} +2 -2
  13. package/src/assets/web-panel/assets/{Logs-Hxw_K0km.js → Logs-BfTE8urP.js} +1 -1
  14. package/src/assets/web-panel/assets/{McpTools-DIE75TrB.js → McpTools-CsGIijNe.js} +1 -1
  15. package/src/assets/web-panel/assets/{Memory-C4KVnLlp.js → Memory-BXX_yMKJ.js} +1 -1
  16. package/src/assets/web-panel/assets/{Notes-DuzrHMAk.js → Notes-DU6Vf2cL.js} +1 -1
  17. package/src/assets/web-panel/assets/{Organization-DTq6uF82.js → Organization-Bny6yOPV.js} +4 -4
  18. package/src/assets/web-panel/assets/{P2P-C0hjlhsR.js → P2P-BxFZ1Bit.js} +2 -2
  19. package/src/assets/web-panel/assets/{Permissions-Ec0NH-xC.js → Permissions-B1j3Mtms.js} +3 -3
  20. package/src/assets/web-panel/assets/{Projects-U8D0asCS.js → Projects-D-CGscDu.js} +1 -1
  21. package/src/assets/web-panel/assets/{Providers-BngtTLvJ.js → Providers-r6NaBYMf.js} +1 -1
  22. package/src/assets/web-panel/assets/{RssFeed-B9NbwCKM.js → RssFeed-D7b68C5q.js} +1 -1
  23. package/src/assets/web-panel/assets/{Security-BL5Rkr1T.js → Security-MJfKv0EJ.js} +3 -3
  24. package/src/assets/web-panel/assets/{Services-D4MJzLld.js → Services-Yb_Q1V3d.js} +1 -1
  25. package/src/assets/web-panel/assets/{Skills-CQTOMDwF.js → Skills-DLTHcH5T.js} +1 -1
  26. package/src/assets/web-panel/assets/{Tasks-DepbJMnL.js → Tasks-CqycpPjS.js} +1 -1
  27. package/src/assets/web-panel/assets/{Templates-C24PVZPu.js → Templates-y01u2Zis.js} +1 -1
  28. package/src/assets/web-panel/assets/VideoEditing-BA1N-5kq.css +1 -0
  29. package/src/assets/web-panel/assets/VideoEditing-B_nPKw6B.js +1 -0
  30. package/src/assets/web-panel/assets/{Wallet-PQoSpN_P.js → Wallet-CsRgnjJY.js} +1 -1
  31. package/src/assets/web-panel/assets/{WebAuthn-BcuyQ4Lr.js → WebAuthn-DWoR5ADp.js} +1 -1
  32. package/src/assets/web-panel/assets/{WorkflowEditor-C-SvXbHW.js → WorkflowEditor-DBJhFPMN.js} +1 -1
  33. package/src/assets/web-panel/assets/{antd-DEjZPGMj.js → antd-Dh2t0vGq.js} +84 -84
  34. package/src/assets/web-panel/assets/index-tN-8TosE.js +2 -0
  35. package/src/assets/web-panel/assets/{markdown-CusdXFxb.js → markdown-CBnGGMzE.js} +1 -1
  36. package/src/assets/web-panel/index.html +2 -2
  37. package/src/commands/agent.js +20 -0
  38. package/src/commands/mcp.js +86 -4
  39. package/src/commands/memory.js +85 -4
  40. package/src/commands/sandbox.js +80 -6
  41. package/src/commands/serve.js +10 -0
  42. package/src/commands/session.js +250 -0
  43. package/src/commands/stream.js +75 -0
  44. package/src/commands/video.js +363 -0
  45. package/src/gateways/http/envelope-http-server.js +194 -0
  46. package/src/gateways/ws/message-dispatcher.js +123 -0
  47. package/src/gateways/ws/session-core-protocol.js +427 -0
  48. package/src/gateways/ws/session-protocol.js +42 -1
  49. package/src/gateways/ws/video-protocol.js +230 -0
  50. package/src/gateways/ws/ws-server.js +72 -0
  51. package/src/gateways/ws/ws-session-gateway.js +7 -3
  52. package/src/harness/jsonl-session-store.js +17 -9
  53. package/src/index.js +8 -0
  54. package/src/lib/agent-stream.js +63 -0
  55. package/src/lib/chat-core.js +183 -6
  56. package/src/lib/cowork/ab-comparator-cli.js +44 -23
  57. package/src/lib/cowork/agent-group-runner.js +145 -0
  58. package/src/lib/cowork/debate-review-cli.js +47 -25
  59. package/src/lib/cowork/project-style-analyzer-cli.js +34 -7
  60. package/src/lib/interaction-adapter.js +59 -1
  61. package/src/lib/jsonl-session-store.js +2 -0
  62. package/src/lib/memory-injection.js +90 -0
  63. package/src/lib/provider-stream.js +120 -0
  64. package/src/lib/sandbox-v2.js +198 -3
  65. package/src/lib/session-consolidator.js +125 -0
  66. package/src/lib/session-core-singletons.js +56 -0
  67. package/src/lib/session-tail.js +128 -0
  68. package/src/lib/session-usage.js +166 -0
  69. package/src/lib/shell-approval.js +96 -0
  70. package/src/lib/ws-chat-handler.js +3 -0
  71. package/src/repl/agent-repl.js +294 -6
  72. package/src/repl/chat-repl.js +87 -100
  73. package/src/runtime/agent-core.js +98 -15
  74. package/src/runtime/agent-runtime.js +105 -3
  75. package/src/runtime/policies/agent-policy.js +10 -0
  76. package/src/skills/video-editing/SKILL.md +46 -0
  77. package/src/skills/video-editing/beat-snap.js +127 -0
  78. package/src/skills/video-editing/extractors/audio-extractor.js +212 -0
  79. package/src/skills/video-editing/extractors/subtitle-extractor.js +90 -0
  80. package/src/skills/video-editing/extractors/video-extractor.js +137 -0
  81. package/src/skills/video-editing/parallel-orchestrator.js +212 -0
  82. package/src/skills/video-editing/pipeline.js +480 -0
  83. package/src/skills/video-editing/prompts/aesthetic-analysis.md +21 -0
  84. package/src/skills/video-editing/prompts/audio-segment.md +15 -0
  85. package/src/skills/video-editing/prompts/character-identify.md +19 -0
  86. package/src/skills/video-editing/prompts/dense-caption.md +20 -0
  87. package/src/skills/video-editing/prompts/editor-system.md +29 -0
  88. package/src/skills/video-editing/prompts/hook-dialogue.md +17 -0
  89. package/src/skills/video-editing/prompts/protagonist-detect.md +20 -0
  90. package/src/skills/video-editing/prompts/scene-caption.md +16 -0
  91. package/src/skills/video-editing/prompts/shot-caption.md +25 -0
  92. package/src/skills/video-editing/prompts/shot-plan.md +28 -0
  93. package/src/skills/video-editing/prompts/structure-proposal.md +16 -0
  94. package/src/skills/video-editing/prompts/vlog-scene-caption.md +18 -0
  95. package/src/skills/video-editing/render/audio-mix.js +128 -0
  96. package/src/skills/video-editing/render/ffmpeg-concat.js +45 -0
  97. package/src/skills/video-editing/render/ffmpeg-extract.js +67 -0
  98. package/src/skills/video-editing/reviewer.js +161 -0
  99. package/src/skills/video-editing/tools/commit.js +108 -0
  100. package/src/skills/video-editing/tools/review-clip.js +46 -0
  101. package/src/skills/video-editing/tools/semantic-retrieval.js +56 -0
  102. package/src/skills/video-editing/tools/shot-trimming.js +73 -0
  103. package/src/assets/web-panel/assets/Analytics-B4OM8S8X.css +0 -1
  104. package/src/assets/web-panel/assets/Analytics-DgypYeUB.js +0 -3
  105. package/src/assets/web-panel/assets/AppLayout-Bzf3mSZI.js +0 -1
  106. package/src/assets/web-panel/assets/AppLayout-DQyDwGut.css +0 -1
  107. package/src/assets/web-panel/assets/index-CwvzTTw_.js +0 -2
@@ -10,11 +10,14 @@
10
10
  */
11
11
 
12
12
  import { BUILT_IN_PROVIDERS } from "./llm-providers.js";
13
+ import { appendTokenUsage } from "../harness/jsonl-session-store.js";
13
14
 
14
15
  /**
15
- * Stream a response from Ollama
16
+ * Stream a response from Ollama.
17
+ * If `onUsage` is provided, it's called with `{inputTokens, outputTokens}`
18
+ * derived from Ollama's terminal `prompt_eval_count` / `eval_count` fields.
16
19
  */
17
- export async function streamOllama(messages, model, baseUrl, onToken) {
20
+ export async function streamOllama(messages, model, baseUrl, onToken, onUsage) {
18
21
  const response = await fetch(`${baseUrl}/api/chat`, {
19
22
  method: "POST",
20
23
  headers: { "Content-Type": "application/json" },
@@ -47,6 +50,13 @@ export async function streamOllama(messages, model, baseUrl, onToken) {
47
50
  fullResponse += json.message.content;
48
51
  onToken(json.message.content);
49
52
  }
53
+ if (json.done && onUsage) {
54
+ const inputTokens = Number(json.prompt_eval_count) || 0;
55
+ const outputTokens = Number(json.eval_count) || 0;
56
+ if (inputTokens || outputTokens) {
57
+ onUsage({ inputTokens, outputTokens });
58
+ }
59
+ }
50
60
  } catch {
51
61
  // Partial JSON, skip
52
62
  }
@@ -59,7 +69,14 @@ export async function streamOllama(messages, model, baseUrl, onToken) {
59
69
  /**
60
70
  * Stream a response from OpenAI-compatible API
61
71
  */
62
- export async function streamOpenAI(messages, model, baseUrl, apiKey, onToken) {
72
+ export async function streamOpenAI(
73
+ messages,
74
+ model,
75
+ baseUrl,
76
+ apiKey,
77
+ onToken,
78
+ onUsage,
79
+ ) {
63
80
  const response = await fetch(`${baseUrl}/chat/completions`, {
64
81
  method: "POST",
65
82
  headers: {
@@ -70,6 +87,9 @@ export async function streamOpenAI(messages, model, baseUrl, apiKey, onToken) {
70
87
  model,
71
88
  messages,
72
89
  stream: true,
90
+ // Opt-in token usage in the terminal chunk (OpenAI-compatible).
91
+ // Servers that don't understand it simply ignore it.
92
+ stream_options: { include_usage: true },
73
93
  }),
74
94
  });
75
95
 
@@ -99,6 +119,13 @@ export async function streamOpenAI(messages, model, baseUrl, apiKey, onToken) {
99
119
  fullResponse += content;
100
120
  onToken(content);
101
121
  }
122
+ if (json.usage && onUsage) {
123
+ const inputTokens = Number(json.usage.prompt_tokens) || 0;
124
+ const outputTokens = Number(json.usage.completion_tokens) || 0;
125
+ if (inputTokens || outputTokens) {
126
+ onUsage({ inputTokens, outputTokens });
127
+ }
128
+ }
102
129
  } catch {
103
130
  // Partial data
104
131
  }
@@ -109,6 +136,99 @@ export async function streamOpenAI(messages, model, baseUrl, apiKey, onToken) {
109
136
  return fullResponse;
110
137
  }
111
138
 
139
+ /**
140
+ * Stream a response from Anthropic's /v1/messages API.
141
+ * SSE chunks carry `message_start` (usage.input_tokens) and `message_delta`
142
+ * (usage.output_tokens). Content comes from `content_block_delta` events.
143
+ */
144
+ export async function streamAnthropic(
145
+ messages,
146
+ model,
147
+ baseUrl,
148
+ apiKey,
149
+ onToken,
150
+ onUsage,
151
+ ) {
152
+ // Split out a leading system prompt (Anthropic requires it as top-level
153
+ // `system`, not an OpenAI-style role=system message).
154
+ let system;
155
+ const convo = [];
156
+ for (const m of messages) {
157
+ if (m.role === "system" && system === undefined) {
158
+ system = m.content;
159
+ } else {
160
+ convo.push(m);
161
+ }
162
+ }
163
+
164
+ const response = await fetch(`${baseUrl}/messages`, {
165
+ method: "POST",
166
+ headers: {
167
+ "Content-Type": "application/json",
168
+ "x-api-key": apiKey,
169
+ "anthropic-version": "2023-06-01",
170
+ },
171
+ body: JSON.stringify({
172
+ model,
173
+ max_tokens: 4096,
174
+ stream: true,
175
+ ...(system ? { system } : {}),
176
+ messages: convo,
177
+ }),
178
+ });
179
+
180
+ if (!response.ok) {
181
+ throw new Error(
182
+ `Anthropic error: ${response.status} ${response.statusText}`,
183
+ );
184
+ }
185
+
186
+ const reader = response.body.getReader();
187
+ const decoder = new TextDecoder();
188
+ let fullResponse = "";
189
+ let buf = "";
190
+ let inputTokens = 0;
191
+ let outputTokens = 0;
192
+
193
+ while (true) {
194
+ const { done, value } = await reader.read();
195
+ if (done) break;
196
+ buf += decoder.decode(value, { stream: true });
197
+ const lines = buf.split("\n");
198
+ buf = lines.pop() || "";
199
+ for (const raw of lines) {
200
+ const line = raw.trim();
201
+ if (!line || !line.startsWith("data:")) continue;
202
+ const payload = line.slice(5).trim();
203
+ if (!payload) continue;
204
+ try {
205
+ const obj = JSON.parse(payload);
206
+ if (obj.type === "content_block_delta") {
207
+ const delta = obj.delta?.text;
208
+ if (delta) {
209
+ fullResponse += delta;
210
+ onToken(delta);
211
+ }
212
+ } else if (obj.type === "message_start") {
213
+ inputTokens = Number(obj.message?.usage?.input_tokens) || inputTokens;
214
+ outputTokens =
215
+ Number(obj.message?.usage?.output_tokens) || outputTokens;
216
+ } else if (obj.type === "message_delta") {
217
+ outputTokens = Number(obj.usage?.output_tokens) || outputTokens;
218
+ }
219
+ } catch {
220
+ /* skip malformed */
221
+ }
222
+ }
223
+ }
224
+
225
+ if (onUsage && (inputTokens || outputTokens)) {
226
+ onUsage({ inputTokens, outputTokens });
227
+ }
228
+
229
+ return fullResponse;
230
+ }
231
+
112
232
  /**
113
233
  * Async generator that streams a chat response.
114
234
  *
@@ -120,17 +240,50 @@ export async function streamOpenAI(messages, model, baseUrl, apiKey, onToken) {
120
240
  * @param {object} options - provider, model, baseUrl, apiKey
121
241
  */
122
242
  export async function* chatStream(messages, options) {
123
- const { provider, model, baseUrl, apiKey } = options;
243
+ const { provider, model, baseUrl, apiKey, sessionId } = options;
124
244
 
125
245
  const tokens = [];
126
246
  const onToken = (token) => {
127
247
  tokens.push(token);
128
248
  };
129
249
 
250
+ let capturedUsage = null;
251
+ const onUsage = (u) => {
252
+ capturedUsage = u;
253
+ };
254
+
130
255
  let fullResponse;
131
256
 
132
257
  if (provider === "ollama") {
133
- fullResponse = await streamOllama(messages, model, baseUrl, onToken);
258
+ fullResponse = await streamOllama(
259
+ messages,
260
+ model,
261
+ baseUrl,
262
+ onToken,
263
+ onUsage,
264
+ );
265
+ } else if (provider === "anthropic") {
266
+ const providerDef = BUILT_IN_PROVIDERS.anthropic;
267
+ const url =
268
+ baseUrl && baseUrl !== "http://localhost:11434"
269
+ ? baseUrl
270
+ : providerDef?.baseUrl || "https://api.anthropic.com/v1";
271
+ const key =
272
+ apiKey ||
273
+ (providerDef?.apiKeyEnv ? process.env[providerDef.apiKeyEnv] : null);
274
+ if (!key) {
275
+ throw new Error(
276
+ `API key required for anthropic (set ${providerDef?.apiKeyEnv || "ANTHROPIC_API_KEY"})`,
277
+ );
278
+ }
279
+ fullResponse = await streamAnthropic(
280
+ messages,
281
+ model,
282
+ url,
283
+ key,
284
+ onToken,
285
+ onUsage,
286
+ );
134
287
  } else {
135
288
  const providerDef = BUILT_IN_PROVIDERS[provider];
136
289
  const url =
@@ -145,7 +298,31 @@ export async function* chatStream(messages, options) {
145
298
  `API key required for ${provider} (set ${providerDef?.apiKeyEnv || "API key"})`,
146
299
  );
147
300
  }
148
- fullResponse = await streamOpenAI(messages, model, url, key, onToken);
301
+ fullResponse = await streamOpenAI(
302
+ messages,
303
+ model,
304
+ url,
305
+ key,
306
+ onToken,
307
+ onUsage,
308
+ );
309
+ }
310
+
311
+ // Phase J — auto-record token usage to JSONL session store so
312
+ // `cc session usage` and the `usage.*` WS routes see real data.
313
+ if (sessionId && capturedUsage) {
314
+ try {
315
+ appendTokenUsage(sessionId, {
316
+ provider,
317
+ model,
318
+ usage: {
319
+ input_tokens: capturedUsage.inputTokens,
320
+ output_tokens: capturedUsage.outputTokens,
321
+ },
322
+ });
323
+ } catch {
324
+ // Best-effort — never break the stream because accounting failed.
325
+ }
149
326
  }
150
327
 
151
328
  // Yield all collected tokens
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { createChatFn } from "../cowork-adapter.js";
9
+ import { runPeerGroup } from "./agent-group-runner.js";
9
10
 
10
11
  const DEFAULT_CRITERIA = ["quality", "performance", "readability"];
11
12
 
@@ -51,33 +52,47 @@ export async function compare({
51
52
  const chat = createChatFn(llmOptions);
52
53
  const numVariants = Math.min(variants, VARIANT_PROFILES.length);
53
54
  const profiles = VARIANT_PROFILES.slice(0, numVariants);
54
- const generatedVariants = [];
55
-
56
- // Phase 1: Generate variants
57
- for (const profile of profiles) {
58
- const messages = [
59
- { role: "system", content: profile.system },
60
- {
61
- role: "user",
62
- content: `Provide a solution for the following task. Include code if applicable.\n\nTask: ${prompt}\n\nProvide your solution with:\n1. Approach summary (1-2 sentences)\n2. Implementation (code or detailed steps)\n3. Trade-offs (pros and cons of this approach)`,
63
- },
64
- ];
65
-
66
- try {
55
+
56
+ // Phase 1: Team of peer variant-generators; judge is the coordinator.
57
+ const peers = profiles.map((profile) => ({
58
+ agentId: `variant_${profile.name}`,
59
+ role: profile.name,
60
+ taskTitle: `Generate variant (${profile.name})`,
61
+ taskDescription: prompt,
62
+ payload: { profile },
63
+ }));
64
+
65
+ const runResult = await runPeerGroup({
66
+ peers,
67
+ coordinator: { agentId: "judge", role: "Judge" },
68
+ metadata: { kind: "ab-comparator", prompt },
69
+ runPeer: async (peer) => {
70
+ const profile = peer.payload.profile;
71
+ const messages = [
72
+ { role: "system", content: profile.system },
73
+ {
74
+ role: "user",
75
+ content: `Provide a solution for the following task. Include code if applicable.\n\nTask: ${prompt}\n\nProvide your solution with:\n1. Approach summary (1-2 sentences)\n2. Implementation (code or detailed steps)\n3. Trade-offs (pros and cons of this approach)`,
76
+ },
77
+ ];
67
78
  const response = await chat(messages, { maxTokens: 2000 });
68
- generatedVariants.push({
79
+ return {
69
80
  name: profile.name,
70
81
  profile: profile.system,
71
82
  solution: response,
72
- });
73
- } catch (err) {
74
- generatedVariants.push({
75
- name: profile.name,
76
- profile: profile.system,
77
- solution: `Error generating variant: ${err.message}`,
78
- });
79
- }
80
- }
83
+ };
84
+ },
85
+ });
86
+
87
+ const generatedVariants = runResult.results.map((r) => {
88
+ if (r.ok) return r.value;
89
+ const profile = r.peer.payload.profile;
90
+ return {
91
+ name: profile.name,
92
+ profile: profile.system,
93
+ solution: `Error generating variant: ${r.error?.message || r.error}`,
94
+ };
95
+ });
81
96
 
82
97
  // Phase 2: Score each variant against criteria
83
98
  const scoringPrompt = `You are an impartial judge evaluating ${numVariants} solution variants against these criteria: ${criteria.join(", ")}.
@@ -136,6 +151,12 @@ REASON: (1-2 sentence justification)`;
136
151
  ranking,
137
152
  winner,
138
153
  reason,
154
+ group: {
155
+ groupId: runResult.groupId,
156
+ parentAgentId: runResult.parentAgentId,
157
+ members: runResult.members,
158
+ tasks: runResult.tasks,
159
+ },
139
160
  };
140
161
  }
141
162
 
@@ -0,0 +1,145 @@
1
+ /**
2
+ * agent-group-runner — thin orchestration helper that wraps any N-peer cowork
3
+ * flow (debate reviewers, A/B variants, analyzers) with session-core's
4
+ * AgentGroup + SharedTaskList.
5
+ *
6
+ * Managed Agents parity Phase G item #1: cowork debate/compare/analyze must
7
+ * stop hand-writing team/subagent semantics. All three flows now share this
8
+ * runner, which makes team(peer) vs coordinator(parent) explicit and gives us
9
+ * a unified task list snapshot for audit/UI.
10
+ *
11
+ * Semantics:
12
+ * - peers[] → added to the AgentGroup as RELATIONSHIPS.PEER members
13
+ * - coordinator → optional parent (moderator / judge), stored as
14
+ * `parentAgentId` so peer↔coordinator messages are visible
15
+ * per AgentGroup's rules
16
+ * - Each peer gets a task in the SharedTaskList: `claim → run → complete`
17
+ * (or `blocked` on error). Errors are captured per peer and do NOT abort
18
+ * the group — matches pre-existing debate/compare behavior.
19
+ */
20
+
21
+ import {
22
+ AgentGroup,
23
+ SharedTaskList,
24
+ RELATIONSHIPS,
25
+ TASK_STATUS,
26
+ } from "@chainlesschain/session-core";
27
+
28
+ /**
29
+ * Run N peer members in parallel (default) or serial, each claiming a
30
+ * SharedTaskList entry.
31
+ *
32
+ * @param {object} params
33
+ * @param {Array<{agentId, sessionId?, role?, taskTitle, taskDescription?, payload?}>} params.peers
34
+ * @param {{agentId, sessionId?, role?}} [params.coordinator]
35
+ * Optional moderator/judge — recorded on the group as parentAgentId.
36
+ * @param {(peer, task, ctx) => Promise<any>} params.runPeer
37
+ * Called once per peer after its task is claimed. Return value is
38
+ * captured into `results[]`. Throwing marks the task `BLOCKED`.
39
+ * @param {object} [params.metadata] Stored on the AgentGroup.
40
+ * @param {"parallel"|"serial"} [params.mode="parallel"]
41
+ *
42
+ * @returns {Promise<{
43
+ * groupId: string,
44
+ * parentAgentId: string|null,
45
+ * members: Array<object>,
46
+ * tasks: Array<object>,
47
+ * results: Array<{peer, ok, value?, error?}>,
48
+ * taskList: SharedTaskList,
49
+ * group: AgentGroup,
50
+ * }>}
51
+ */
52
+ export async function runPeerGroup({
53
+ peers,
54
+ coordinator = null,
55
+ runPeer,
56
+ metadata = {},
57
+ mode = "parallel",
58
+ } = {}) {
59
+ if (!Array.isArray(peers) || peers.length === 0) {
60
+ throw new Error("runPeerGroup: peers[] required");
61
+ }
62
+ if (typeof runPeer !== "function") {
63
+ throw new Error("runPeerGroup: runPeer function required");
64
+ }
65
+
66
+ const taskList = new SharedTaskList();
67
+ const group = new AgentGroup({
68
+ parentAgentId: coordinator?.agentId || null,
69
+ sharedTaskList: taskList,
70
+ metadata,
71
+ });
72
+ taskList.groupId = group.groupId;
73
+
74
+ // Register peers + create one task per peer (keyed by agentId).
75
+ const taskByAgent = new Map();
76
+ for (const peer of peers) {
77
+ group.addMember({
78
+ agentId: peer.agentId,
79
+ sessionId: peer.sessionId || `sess_${peer.agentId}`,
80
+ relationship: RELATIONSHIPS.PEER,
81
+ role: peer.role || null,
82
+ });
83
+ const task = taskList.add({
84
+ title: peer.taskTitle || `${peer.agentId} task`,
85
+ description: peer.taskDescription || "",
86
+ assignee: peer.agentId,
87
+ createdBy: coordinator?.agentId || peer.agentId,
88
+ });
89
+ taskByAgent.set(peer.agentId, task);
90
+ }
91
+
92
+ const runOne = async (peer) => {
93
+ const task = taskByAgent.get(peer.agentId);
94
+ let claimed;
95
+ try {
96
+ claimed = taskList.claim(task.id, { agentId: peer.agentId });
97
+ } catch (err) {
98
+ return { peer, ok: false, error: err };
99
+ }
100
+ try {
101
+ const value = await runPeer(peer, claimed || task, {
102
+ group,
103
+ taskList,
104
+ coordinator,
105
+ });
106
+ taskList.complete(claimed.id, { actor: peer.agentId });
107
+ return { peer, ok: true, value };
108
+ } catch (err) {
109
+ // Mark task blocked so the group snapshot reflects failure mode.
110
+ const current = taskList.get(claimed.id);
111
+ try {
112
+ taskList.update(claimed.id, {
113
+ rev: current.rev,
114
+ patch: { status: TASK_STATUS.BLOCKED },
115
+ actor: peer.agentId,
116
+ });
117
+ } catch (_e) {
118
+ /* swallow — best effort */
119
+ }
120
+ return { peer, ok: false, error: err };
121
+ }
122
+ };
123
+
124
+ let results;
125
+ if (mode === "serial") {
126
+ results = [];
127
+ for (const peer of peers) {
128
+ results.push(await runOne(peer));
129
+ }
130
+ } else {
131
+ results = await Promise.all(peers.map(runOne));
132
+ }
133
+
134
+ return {
135
+ groupId: group.groupId,
136
+ parentAgentId: group.parentAgentId,
137
+ members: group.listMembers(),
138
+ tasks: taskList.list(),
139
+ results,
140
+ taskList,
141
+ group,
142
+ };
143
+ }
144
+
145
+ export { AgentGroup, SharedTaskList, RELATIONSHIPS, TASK_STATUS };
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { createChatFn } from "../cowork-adapter.js";
9
+ import { runPeerGroup } from "./agent-group-runner.js";
9
10
 
10
11
  const DEFAULT_PERSPECTIVES = ["performance", "security", "maintainability"];
11
12
 
@@ -54,41 +55,56 @@ export async function startDebate({
54
55
  llmOptions = {},
55
56
  }) {
56
57
  const chat = createChatFn(llmOptions);
57
- const reviews = [];
58
58
 
59
- // Phase 1: Independent reviews from each perspective
60
- for (const perspective of perspectives) {
59
+ // Phase 1: Team of peer reviewers — each perspective is a peer AgentGroup
60
+ // member, the moderator is the coordinator (parent). SharedTaskList tracks
61
+ // one review task per perspective.
62
+ const peers = perspectives.map((perspective) => {
61
63
  const config = PERSPECTIVE_PROMPTS[perspective] || {
62
64
  role: `${perspective} Reviewer`,
63
65
  system: `You are a ${perspective}-focused code reviewer. Provide specific, actionable feedback.`,
64
66
  };
67
+ return {
68
+ agentId: `reviewer_${perspective}`,
69
+ role: config.role,
70
+ taskTitle: `Review (${perspective})`,
71
+ taskDescription: target,
72
+ payload: { perspective, config },
73
+ };
74
+ });
65
75
 
66
- const messages = [
67
- { role: "system", content: config.system },
68
- {
69
- role: "user",
70
- content: `Review the following code/content.\n\nTarget: ${target}\n\n\`\`\`\n${code}\n\`\`\`\n\nProvide your review as a ${config.role}. Format your response as:\n\n## Issues Found\n- List each issue with severity (HIGH/MEDIUM/LOW)\n\n## Recommendations\n- List specific improvements\n\n## Verdict\nAPPROVE, NEEDS_WORK, or REJECT with a brief reason.`,
71
- },
72
- ];
73
-
74
- try {
76
+ const runResult = await runPeerGroup({
77
+ peers,
78
+ coordinator: { agentId: "moderator", role: "Moderator" },
79
+ metadata: { kind: "debate-review", target },
80
+ runPeer: async (peer) => {
81
+ const { perspective, config } = peer.payload;
82
+ const messages = [
83
+ { role: "system", content: config.system },
84
+ {
85
+ role: "user",
86
+ content: `Review the following code/content.\n\nTarget: ${target}\n\n\`\`\`\n${code}\n\`\`\`\n\nProvide your review as a ${config.role}. Format your response as:\n\n## Issues Found\n- List each issue with severity (HIGH/MEDIUM/LOW)\n\n## Recommendations\n- List specific improvements\n\n## Verdict\nAPPROVE, NEEDS_WORK, or REJECT with a brief reason.`,
87
+ },
88
+ ];
75
89
  const response = await chat(messages, { maxTokens: 1500 });
76
- const verdict = extractVerdict(response);
77
- reviews.push({
90
+ return {
78
91
  perspective,
79
92
  role: config.role,
80
93
  review: response,
81
- verdict,
82
- });
83
- } catch (err) {
84
- reviews.push({
85
- perspective,
86
- role: config.role,
87
- review: `Error: ${err.message}`,
88
- verdict: "ERROR",
89
- });
90
- }
91
- }
94
+ verdict: extractVerdict(response),
95
+ };
96
+ },
97
+ });
98
+
99
+ const reviews = runResult.results.map((r) => {
100
+ if (r.ok) return r.value;
101
+ return {
102
+ perspective: r.peer.payload.perspective,
103
+ role: r.peer.payload.config.role,
104
+ review: `Error: ${r.error?.message || r.error}`,
105
+ verdict: "ERROR",
106
+ };
107
+ });
92
108
 
93
109
  // Phase 2: Moderator synthesizes final verdict
94
110
  // Summarize each reviewer's output to reduce context pollution for the moderator
@@ -136,6 +152,12 @@ export async function startDebate({
136
152
  verdict: finalVerdict,
137
153
  consensusScore,
138
154
  summary,
155
+ group: {
156
+ groupId: runResult.groupId,
157
+ parentAgentId: runResult.parentAgentId,
158
+ members: runResult.members,
159
+ tasks: runResult.tasks,
160
+ },
139
161
  };
140
162
  }
141
163
 
@@ -7,6 +7,7 @@
7
7
  import fs from "fs";
8
8
  import path from "path";
9
9
  import { createChatFn } from "../cowork-adapter.js";
10
+ import { runPeerGroup } from "./agent-group-runner.js";
10
11
 
11
12
  const CODE_EXTENSIONS = new Set([
12
13
  ".js",
@@ -118,21 +119,47 @@ Be specific with examples from the code samples provided.`,
118
119
  },
119
120
  ];
120
121
 
121
- try {
122
- const response = await chat(messages, { maxTokens: 2000 });
122
+ // Single-peer AgentGroup (analyzer) — no coordinator. Keeps cowork
123
+ // semantics uniform across debate/compare/analyze.
124
+ const runResult = await runPeerGroup({
125
+ peers: [
126
+ {
127
+ agentId: "style_analyzer",
128
+ role: "Project Style Analyzer",
129
+ taskTitle: `Analyze style (${targetPath})`,
130
+ taskDescription: `${samples.length} samples, ${configFiles.length} config file(s)`,
131
+ },
132
+ ],
133
+ metadata: { kind: "project-style-analyzer", targetPath },
134
+ runPeer: async () => chat(messages, { maxTokens: 2000 }),
135
+ });
123
136
 
137
+ const outcome = runResult.results[0];
138
+ if (outcome.ok) {
139
+ const response = outcome.value;
124
140
  return {
125
141
  samplesAnalyzed: samples.length,
126
142
  configFilesFound: configFiles.map((c) => c.name),
127
143
  analysis: response,
128
144
  summary: `Style Analysis for: ${targetPath}\n Samples: ${samples.length} files\n Config: ${configFiles.map((c) => c.name).join(", ") || "none found"}\n\n${response}`,
129
- };
130
- } catch (err) {
131
- return {
132
- samplesAnalyzed: samples.length,
133
- summary: `Style analysis failed: ${err.message}`,
145
+ group: {
146
+ groupId: runResult.groupId,
147
+ parentAgentId: runResult.parentAgentId,
148
+ members: runResult.members,
149
+ tasks: runResult.tasks,
150
+ },
134
151
  };
135
152
  }
153
+ return {
154
+ samplesAnalyzed: samples.length,
155
+ summary: `Style analysis failed: ${outcome.error?.message || outcome.error}`,
156
+ group: {
157
+ groupId: runResult.groupId,
158
+ parentAgentId: runResult.parentAgentId,
159
+ members: runResult.members,
160
+ tasks: runResult.tasks,
161
+ },
162
+ };
136
163
  }
137
164
 
138
165
  function collectSampleFiles(dir, maxFiles) {