chainlesschain 0.47.6 → 0.47.7
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/package.json +2 -2
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/Analytics-BFI7jbwM.css +1 -0
- package/src/assets/web-panel/assets/Analytics-DQ135mAd.js +3 -0
- package/src/assets/web-panel/assets/AppLayout-6SPt_8Y_.js +1 -0
- package/src/assets/web-panel/assets/AppLayout-BFJ-Fofn.css +1 -0
- package/src/assets/web-panel/assets/{Backup-Ba9UybpT.js → Backup-DbVRG5vE.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-BwXskT21.js → Chat-wVhrFK9C.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-UmOe7qvE.js → Cowork-lOC25IW2.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-JHS-rc-4.js → Cron-3P0eVLTV.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-B95cMCO7.js → Dashboard-Br7kCwKJ.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CSYO0_zk.js → Git-CrDCcBig.js} +2 -2
- package/src/assets/web-panel/assets/{Logs-Hxw_K0km.js → Logs-BfTE8urP.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DIE75TrB.js → McpTools-CsGIijNe.js} +1 -1
- package/src/assets/web-panel/assets/{Memory-C4KVnLlp.js → Memory-BXX_yMKJ.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DuzrHMAk.js → Notes-DU6Vf2cL.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-DTq6uF82.js → Organization-Bny6yOPV.js} +4 -4
- package/src/assets/web-panel/assets/{P2P-C0hjlhsR.js → P2P-BxFZ1Bit.js} +2 -2
- package/src/assets/web-panel/assets/{Permissions-Ec0NH-xC.js → Permissions-B1j3Mtms.js} +3 -3
- package/src/assets/web-panel/assets/{Projects-U8D0asCS.js → Projects-D-CGscDu.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-BngtTLvJ.js → Providers-r6NaBYMf.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-B9NbwCKM.js → RssFeed-D7b68C5q.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BL5Rkr1T.js → Security-MJfKv0EJ.js} +3 -3
- package/src/assets/web-panel/assets/{Services-D4MJzLld.js → Services-Yb_Q1V3d.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CQTOMDwF.js → Skills-DLTHcH5T.js} +1 -1
- package/src/assets/web-panel/assets/{Tasks-DepbJMnL.js → Tasks-CqycpPjS.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-C24PVZPu.js → Templates-y01u2Zis.js} +1 -1
- package/src/assets/web-panel/assets/VideoEditing-BA1N-5kq.css +1 -0
- package/src/assets/web-panel/assets/VideoEditing-B_nPKw6B.js +1 -0
- package/src/assets/web-panel/assets/{Wallet-PQoSpN_P.js → Wallet-CsRgnjJY.js} +1 -1
- package/src/assets/web-panel/assets/{WebAuthn-BcuyQ4Lr.js → WebAuthn-DWoR5ADp.js} +1 -1
- package/src/assets/web-panel/assets/{WorkflowEditor-C-SvXbHW.js → WorkflowEditor-DBJhFPMN.js} +1 -1
- package/src/assets/web-panel/assets/{antd-DEjZPGMj.js → antd-Dh2t0vGq.js} +84 -84
- package/src/assets/web-panel/assets/index-tN-8TosE.js +2 -0
- package/src/assets/web-panel/assets/{markdown-CusdXFxb.js → markdown-CBnGGMzE.js} +1 -1
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/agent.js +20 -0
- package/src/commands/mcp.js +86 -4
- package/src/commands/memory.js +85 -4
- package/src/commands/sandbox.js +80 -6
- package/src/commands/serve.js +10 -0
- package/src/commands/session.js +250 -0
- package/src/commands/stream.js +75 -0
- package/src/commands/video.js +363 -0
- package/src/gateways/http/envelope-http-server.js +194 -0
- package/src/gateways/ws/message-dispatcher.js +123 -0
- package/src/gateways/ws/session-core-protocol.js +427 -0
- package/src/gateways/ws/session-protocol.js +42 -1
- package/src/gateways/ws/video-protocol.js +230 -0
- package/src/gateways/ws/ws-server.js +72 -0
- package/src/gateways/ws/ws-session-gateway.js +7 -3
- package/src/harness/jsonl-session-store.js +17 -9
- package/src/index.js +8 -0
- package/src/lib/agent-stream.js +63 -0
- package/src/lib/chat-core.js +183 -6
- package/src/lib/cowork/ab-comparator-cli.js +44 -23
- package/src/lib/cowork/agent-group-runner.js +145 -0
- package/src/lib/cowork/debate-review-cli.js +47 -25
- package/src/lib/cowork/project-style-analyzer-cli.js +34 -7
- package/src/lib/interaction-adapter.js +59 -1
- package/src/lib/jsonl-session-store.js +2 -0
- package/src/lib/memory-injection.js +90 -0
- package/src/lib/provider-stream.js +120 -0
- package/src/lib/sandbox-v2.js +198 -3
- package/src/lib/session-consolidator.js +125 -0
- package/src/lib/session-core-singletons.js +56 -0
- package/src/lib/session-tail.js +128 -0
- package/src/lib/session-usage.js +166 -0
- package/src/lib/shell-approval.js +96 -0
- package/src/lib/ws-chat-handler.js +3 -0
- package/src/repl/agent-repl.js +271 -6
- package/src/repl/chat-repl.js +87 -100
- package/src/runtime/agent-core.js +98 -15
- package/src/runtime/agent-runtime.js +105 -3
- package/src/runtime/policies/agent-policy.js +10 -0
- package/src/skills/video-editing/SKILL.md +46 -0
- package/src/skills/video-editing/beat-snap.js +127 -0
- package/src/skills/video-editing/extractors/audio-extractor.js +212 -0
- package/src/skills/video-editing/extractors/subtitle-extractor.js +90 -0
- package/src/skills/video-editing/extractors/video-extractor.js +137 -0
- package/src/skills/video-editing/parallel-orchestrator.js +212 -0
- package/src/skills/video-editing/pipeline.js +480 -0
- package/src/skills/video-editing/prompts/aesthetic-analysis.md +21 -0
- package/src/skills/video-editing/prompts/audio-segment.md +15 -0
- package/src/skills/video-editing/prompts/character-identify.md +19 -0
- package/src/skills/video-editing/prompts/dense-caption.md +20 -0
- package/src/skills/video-editing/prompts/editor-system.md +29 -0
- package/src/skills/video-editing/prompts/hook-dialogue.md +17 -0
- package/src/skills/video-editing/prompts/protagonist-detect.md +20 -0
- package/src/skills/video-editing/prompts/scene-caption.md +16 -0
- package/src/skills/video-editing/prompts/shot-caption.md +25 -0
- package/src/skills/video-editing/prompts/shot-plan.md +28 -0
- package/src/skills/video-editing/prompts/structure-proposal.md +16 -0
- package/src/skills/video-editing/prompts/vlog-scene-caption.md +18 -0
- package/src/skills/video-editing/render/audio-mix.js +128 -0
- package/src/skills/video-editing/render/ffmpeg-concat.js +45 -0
- package/src/skills/video-editing/render/ffmpeg-extract.js +67 -0
- package/src/skills/video-editing/reviewer.js +161 -0
- package/src/skills/video-editing/tools/commit.js +108 -0
- package/src/skills/video-editing/tools/review-clip.js +46 -0
- package/src/skills/video-editing/tools/semantic-retrieval.js +56 -0
- package/src/skills/video-editing/tools/shot-trimming.js +73 -0
- package/src/assets/web-panel/assets/Analytics-B4OM8S8X.css +0 -1
- package/src/assets/web-panel/assets/Analytics-DgypYeUB.js +0 -3
- package/src/assets/web-panel/assets/AppLayout-Bzf3mSZI.js +0 -1
- package/src/assets/web-panel/assets/AppLayout-DQyDwGut.css +0 -1
- package/src/assets/web-panel/assets/index-CwvzTTw_.js +0 -2
package/src/lib/chat-core.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
79
|
+
return {
|
|
69
80
|
name: profile.name,
|
|
70
81
|
profile: profile.system,
|
|
71
82
|
solution: response,
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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:
|
|
60
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
reviews.push({
|
|
90
|
+
return {
|
|
78
91
|
perspective,
|
|
79
92
|
role: config.role,
|
|
80
93
|
review: response,
|
|
81
|
-
verdict,
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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) {
|