@cjhyy/code-shell-arena 0.8.0
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 +46 -0
- package/dist/arena.d.ts +45 -0
- package/dist/arena.js +362 -0
- package/dist/capability.d.ts +43 -0
- package/dist/capability.js +453 -0
- package/dist/context/context-tools.d.ts +16 -0
- package/dist/context/context-tools.js +272 -0
- package/dist/context/within-root.d.ts +7 -0
- package/dist/context/within-root.js +15 -0
- package/dist/detect-mode.d.ts +20 -0
- package/dist/detect-mode.js +78 -0
- package/dist/digest-builder.d.ts +25 -0
- package/dist/digest-builder.js +120 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +23 -0
- package/dist/index.runtime.d.ts +13 -0
- package/dist/index.runtime.js +11 -0
- package/dist/iterate/convergence.d.ts +25 -0
- package/dist/iterate/convergence.js +103 -0
- package/dist/iterate/formats/index.d.ts +22 -0
- package/dist/iterate/formats/index.js +283 -0
- package/dist/iterate/index.d.ts +11 -0
- package/dist/iterate/index.js +9 -0
- package/dist/iterate/iterative-arena.d.ts +31 -0
- package/dist/iterate/iterative-arena.js +252 -0
- package/dist/iterate/parse.d.ts +42 -0
- package/dist/iterate/parse.js +123 -0
- package/dist/iterate/phases/argue.d.ts +22 -0
- package/dist/iterate/phases/argue.js +165 -0
- package/dist/iterate/phases/revise.d.ts +16 -0
- package/dist/iterate/phases/revise.js +62 -0
- package/dist/iterate/phases/tournament.d.ts +34 -0
- package/dist/iterate/phases/tournament.js +113 -0
- package/dist/iterate/tools/web-tools.d.ts +13 -0
- package/dist/iterate/tools/web-tools.js +54 -0
- package/dist/iterate/types.d.ts +152 -0
- package/dist/iterate/types.js +8 -0
- package/dist/ledger.d.ts +47 -0
- package/dist/ledger.js +159 -0
- package/dist/lenses/architecture.d.ts +5 -0
- package/dist/lenses/architecture.js +22 -0
- package/dist/lenses/engineering.d.ts +5 -0
- package/dist/lenses/engineering.js +22 -0
- package/dist/lenses/general.d.ts +5 -0
- package/dist/lenses/general.js +20 -0
- package/dist/lenses/index.d.ts +16 -0
- package/dist/lenses/index.js +47 -0
- package/dist/lenses/product.d.ts +5 -0
- package/dist/lenses/product.js +22 -0
- package/dist/model-presets.d.ts +23 -0
- package/dist/model-presets.js +44 -0
- package/dist/phases/adjudication.d.ts +25 -0
- package/dist/phases/adjudication.js +142 -0
- package/dist/phases/build-consensus.d.ts +30 -0
- package/dist/phases/build-consensus.js +85 -0
- package/dist/phases/claim-registry.d.ts +26 -0
- package/dist/phases/claim-registry.js +60 -0
- package/dist/phases/cross-review.d.ts +47 -0
- package/dist/phases/cross-review.js +224 -0
- package/dist/phases/debate-rounds.d.ts +28 -0
- package/dist/phases/debate-rounds.js +161 -0
- package/dist/phases/participant-research.d.ts +39 -0
- package/dist/phases/participant-research.js +322 -0
- package/dist/phases/planning-detail-expansion.d.ts +39 -0
- package/dist/phases/planning-detail-expansion.js +120 -0
- package/dist/planner.d.ts +28 -0
- package/dist/planner.js +312 -0
- package/dist/providers/docs.d.ts +7 -0
- package/dist/providers/docs.js +111 -0
- package/dist/providers/git.d.ts +8 -0
- package/dist/providers/git.js +174 -0
- package/dist/providers/index.d.ts +32 -0
- package/dist/providers/index.js +132 -0
- package/dist/providers/none.d.ts +7 -0
- package/dist/providers/none.js +11 -0
- package/dist/providers/repo.d.ts +7 -0
- package/dist/providers/repo.js +258 -0
- package/dist/render/session.d.ts +17 -0
- package/dist/render/session.js +190 -0
- package/dist/render/terminal.d.ts +34 -0
- package/dist/render/terminal.js +286 -0
- package/dist/strategies/discussion.d.ts +25 -0
- package/dist/strategies/discussion.js +143 -0
- package/dist/strategies/index.d.ts +15 -0
- package/dist/strategies/index.js +28 -0
- package/dist/strategies/language-wrapper.d.ts +17 -0
- package/dist/strategies/language-wrapper.js +102 -0
- package/dist/strategies/lens-wrapper.d.ts +16 -0
- package/dist/strategies/lens-wrapper.js +236 -0
- package/dist/strategies/planning.d.ts +30 -0
- package/dist/strategies/planning.js +225 -0
- package/dist/strategies/review.d.ts +26 -0
- package/dist/strategies/review.js +168 -0
- package/dist/strategies/utils.d.ts +36 -0
- package/dist/strategies/utils.js +603 -0
- package/dist/tools/selector.d.ts +17 -0
- package/dist/tools/selector.js +61 -0
- package/dist/transitions.d.ts +53 -0
- package/dist/transitions.js +97 -0
- package/dist/types.d.ts +518 -0
- package/dist/types.js +27 -0
- package/package.json +45 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ParticipantResearch — each participant independently investigates
|
|
3
|
+
* the shared base context and produces structured findings.
|
|
4
|
+
*
|
|
5
|
+
* Phase 3 upgrade: now captures ToolTrace and builds EvidencePackets,
|
|
6
|
+
* outputting a full ResearchDossier alongside the ParticipantReport.
|
|
7
|
+
*/
|
|
8
|
+
import { createLLMClient } from "@cjhyy/code-shell-core/extension";
|
|
9
|
+
import { CONTEXT_TOOLS, MAX_TOOL_ROUNDS, executeContextTool } from "../context/context-tools.js";
|
|
10
|
+
import { logger } from "@cjhyy/code-shell-core/extension";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
/**
|
|
13
|
+
* Run participant research phase in parallel.
|
|
14
|
+
* Each participant reads the shared context, optionally requests more,
|
|
15
|
+
* then produces structured findings with evidence trails.
|
|
16
|
+
*/
|
|
17
|
+
export async function runParticipantResearch(options) {
|
|
18
|
+
const results = await runParticipantResearchWithDossiers(options);
|
|
19
|
+
return results.map((r) => r.report);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Run participant research phase with full dossier output.
|
|
23
|
+
* Returns both reports (backward compat) and dossiers (evidence trail).
|
|
24
|
+
*/
|
|
25
|
+
export async function runParticipantResearchWithDossiers(options) {
|
|
26
|
+
const { participants, strategy, topic, baseContext, enableContextTools, contextTools, signal, onProgress, onUsage } = options;
|
|
27
|
+
const tools = enableContextTools ? (contextTools ?? CONTEXT_TOOLS) : undefined;
|
|
28
|
+
const tasks = participants.map(async (p) => {
|
|
29
|
+
onProgress?.({ type: "research_start", participant: p.name });
|
|
30
|
+
const client = await createLLMClient(p.llm, p.clientDefaults);
|
|
31
|
+
const messages = [
|
|
32
|
+
{
|
|
33
|
+
role: "user",
|
|
34
|
+
content: strategy.researchUserPrompt(topic, baseContext),
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
let finalText = "";
|
|
38
|
+
let totalRounds = 0;
|
|
39
|
+
const toolTraces = [];
|
|
40
|
+
const evidencePackets = [];
|
|
41
|
+
// ── Tool-use loop ──────────────────────────────────────────
|
|
42
|
+
const MAX_MESSAGES = 30; // Cap messages to prevent token overflow
|
|
43
|
+
for (let round = 0; round <= MAX_TOOL_ROUNDS; round++) {
|
|
44
|
+
totalRounds = round;
|
|
45
|
+
// Safety: prevent unbounded message growth
|
|
46
|
+
if (messages.length >= MAX_MESSAGES) {
|
|
47
|
+
logger.warn("arena.research_message_limit", {
|
|
48
|
+
participant: p.name,
|
|
49
|
+
messageCount: messages.length,
|
|
50
|
+
round,
|
|
51
|
+
});
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
const response = await client.createMessage({
|
|
55
|
+
systemPrompt: strategy.researchSystemPrompt(p.name),
|
|
56
|
+
messages,
|
|
57
|
+
tools,
|
|
58
|
+
signal,
|
|
59
|
+
});
|
|
60
|
+
onUsage?.(response.usage);
|
|
61
|
+
const toolNames = (response.toolCalls ?? []).map((tc) => tc.toolName);
|
|
62
|
+
const hasTools = response.toolCalls && response.toolCalls.length > 0;
|
|
63
|
+
logger.info("arena.research_round", {
|
|
64
|
+
participant: p.name,
|
|
65
|
+
round,
|
|
66
|
+
toolCount: response.toolCalls?.length ?? 0,
|
|
67
|
+
toolNames,
|
|
68
|
+
textLen: response.text?.length ?? 0,
|
|
69
|
+
stopReason: response.stopReason ?? "unknown",
|
|
70
|
+
});
|
|
71
|
+
if (!hasTools) {
|
|
72
|
+
finalText = response.text;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
// Report tool usage to UI
|
|
76
|
+
onProgress?.({
|
|
77
|
+
type: "context_lookup",
|
|
78
|
+
participant: p.name,
|
|
79
|
+
tools: response.toolCalls.map((tc) => `${tc.toolName}(${JSON.stringify(tc.args)})`),
|
|
80
|
+
});
|
|
81
|
+
// Append assistant message with tool_use blocks
|
|
82
|
+
const assistantBlocks = [];
|
|
83
|
+
if (response.text) {
|
|
84
|
+
assistantBlocks.push({ type: "text", text: response.text });
|
|
85
|
+
}
|
|
86
|
+
for (const tc of response.toolCalls) {
|
|
87
|
+
assistantBlocks.push({
|
|
88
|
+
type: "tool_use",
|
|
89
|
+
id: tc.id,
|
|
90
|
+
name: tc.toolName,
|
|
91
|
+
input: tc.args,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
messages.push({ role: "assistant", content: assistantBlocks });
|
|
95
|
+
// Execute tools, record traces, and build evidence packets
|
|
96
|
+
const resultBlocks = [];
|
|
97
|
+
for (const tc of response.toolCalls) {
|
|
98
|
+
const result = executeContextTool(tc);
|
|
99
|
+
// Record tool trace
|
|
100
|
+
const trace = {
|
|
101
|
+
round,
|
|
102
|
+
toolName: tc.toolName,
|
|
103
|
+
args: tc.args,
|
|
104
|
+
resultRef: buildResultRef(tc),
|
|
105
|
+
keptAsEvidence: result.length > 50 && !result.startsWith("Error:") && !result.startsWith("Tool error:") && !result.startsWith("Unknown tool:"),
|
|
106
|
+
};
|
|
107
|
+
toolTraces.push(trace);
|
|
108
|
+
// Build evidence packet from successful tool results
|
|
109
|
+
if (trace.keptAsEvidence) {
|
|
110
|
+
const packet = buildEvidencePacketFromTool(p.name, tc, result, inferSourceKind(tc.toolName));
|
|
111
|
+
evidencePackets.push(packet);
|
|
112
|
+
}
|
|
113
|
+
resultBlocks.push({
|
|
114
|
+
type: "tool_result",
|
|
115
|
+
tool_use_id: tc.id,
|
|
116
|
+
content: result,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// ── Anti-loop hard nudge ───────────────────────────────────
|
|
120
|
+
// Thinking-mode models (DeepSeek V4 in particular) will
|
|
121
|
+
// otherwise spend every available round on read_file with
|
|
122
|
+
// textLen=0, then need a separate force_conclude pass to emit
|
|
123
|
+
// findings. We prepend the directive to the user turn so it
|
|
124
|
+
// appears BEFORE the tool results — trailing text after
|
|
125
|
+
// tool_result blocks gets ignored by some implementations.
|
|
126
|
+
const remaining = MAX_TOOL_ROUNDS - round;
|
|
127
|
+
const isLastAllowedRound = remaining <= 0;
|
|
128
|
+
const isPenultimate = remaining === 1;
|
|
129
|
+
if (isLastAllowedRound || isPenultimate) {
|
|
130
|
+
const directive = isLastAllowedRound
|
|
131
|
+
? "STOP. You have used your tool budget. Your NEXT response MUST be the findings JSON. Do NOT call any more tools — any further tool_use will be discarded."
|
|
132
|
+
: "FINAL ROUND. Use the tool results below plus any prior context to output the findings JSON in your NEXT response. Do not call more tools.";
|
|
133
|
+
resultBlocks.unshift({
|
|
134
|
+
type: "text",
|
|
135
|
+
text: `[arena-research:${p.name}] ${directive}`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
messages.push({ role: "user", content: resultBlocks });
|
|
139
|
+
}
|
|
140
|
+
// ── Force-conclude if no text yet ──────────────────────────
|
|
141
|
+
if (!finalText) {
|
|
142
|
+
logger.warn("arena.research_force_conclude", {
|
|
143
|
+
participant: p.name,
|
|
144
|
+
totalRounds,
|
|
145
|
+
messageCount: messages.length,
|
|
146
|
+
});
|
|
147
|
+
// Bail out before spending a request if the user already stopped. Without
|
|
148
|
+
// this (and the `signal` below) a Stop during the force-conclude phase
|
|
149
|
+
// left the final model request running to completion — still billed, and
|
|
150
|
+
// still holding the task alive until the provider answered on its own.
|
|
151
|
+
signal?.throwIfAborted();
|
|
152
|
+
const forceResponse = await client.createMessage({
|
|
153
|
+
systemPrompt: strategy.researchSystemPrompt(p.name),
|
|
154
|
+
messages: [
|
|
155
|
+
...messages,
|
|
156
|
+
{
|
|
157
|
+
role: "user",
|
|
158
|
+
content: "You have gathered enough context. " +
|
|
159
|
+
"Based on ALL the tool results above, output your findings NOW.\n\n" +
|
|
160
|
+
"Respond ONLY with the <report> XML element specified in the system prompt — " +
|
|
161
|
+
"output your highest-confidence findings (typically 5-15 for a substantive topic; " +
|
|
162
|
+
"each <summary> should be 80+ words with concrete evidence). " +
|
|
163
|
+
"Do NOT request any more tools. No prose outside the XML.",
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
signal,
|
|
167
|
+
});
|
|
168
|
+
onUsage?.(forceResponse.usage);
|
|
169
|
+
logger.info("arena.research_force_conclude_response", {
|
|
170
|
+
participant: p.name,
|
|
171
|
+
textLen: forceResponse.text?.length ?? 0,
|
|
172
|
+
stopReason: forceResponse.stopReason ?? "unknown",
|
|
173
|
+
});
|
|
174
|
+
finalText = forceResponse.text;
|
|
175
|
+
}
|
|
176
|
+
// ── Final output ───────────────────────────────────────────
|
|
177
|
+
logger.info("arena.research_raw_response", {
|
|
178
|
+
participant: p.name,
|
|
179
|
+
textLength: finalText.length,
|
|
180
|
+
text: finalText,
|
|
181
|
+
});
|
|
182
|
+
// If still empty after force-conclude, mark as failed
|
|
183
|
+
if (!finalText || finalText.trim().length === 0) {
|
|
184
|
+
logger.warn("arena.research_failed", {
|
|
185
|
+
participant: p.name,
|
|
186
|
+
totalRounds,
|
|
187
|
+
messageCount: messages.length,
|
|
188
|
+
});
|
|
189
|
+
const failedReport = {
|
|
190
|
+
participant: p.name,
|
|
191
|
+
contextSummary: `(research failed: model returned empty response after ${totalRounds} tool rounds)`,
|
|
192
|
+
findings: [],
|
|
193
|
+
};
|
|
194
|
+
const failedDossier = {
|
|
195
|
+
participant: p.name,
|
|
196
|
+
contextSummary: failedReport.contextSummary,
|
|
197
|
+
findings: [],
|
|
198
|
+
toolTrace: toolTraces,
|
|
199
|
+
evidencePackets,
|
|
200
|
+
findingEvidenceLinks: [],
|
|
201
|
+
};
|
|
202
|
+
onProgress?.({ type: "research_done", participant: p.name, report: failedReport });
|
|
203
|
+
return { report: failedReport, dossier: failedDossier };
|
|
204
|
+
}
|
|
205
|
+
const report = strategy.parseResearchResponse(p.name, finalText);
|
|
206
|
+
// Build finding-evidence links by matching finding evidence refs to packets
|
|
207
|
+
const findingEvidenceLinks = buildFindingEvidenceLinks(report, evidencePackets);
|
|
208
|
+
const dossier = {
|
|
209
|
+
participant: p.name,
|
|
210
|
+
contextSummary: report.contextSummary,
|
|
211
|
+
findings: report.findings,
|
|
212
|
+
toolTrace: toolTraces,
|
|
213
|
+
evidencePackets,
|
|
214
|
+
findingEvidenceLinks,
|
|
215
|
+
};
|
|
216
|
+
onProgress?.({ type: "research_done", participant: p.name, report });
|
|
217
|
+
return { report, dossier };
|
|
218
|
+
});
|
|
219
|
+
return Promise.all(tasks);
|
|
220
|
+
}
|
|
221
|
+
// ─── Helper functions ──────────────────────────────────────────
|
|
222
|
+
/** Build a stable ref string for a tool call */
|
|
223
|
+
function buildResultRef(tc) {
|
|
224
|
+
if (tc.toolName === "read_file" && tc.args.path)
|
|
225
|
+
return `file:${tc.args.path}`;
|
|
226
|
+
if (tc.toolName === "grep_code" && tc.args.pattern)
|
|
227
|
+
return `grep:${tc.args.pattern}`;
|
|
228
|
+
if (tc.toolName === "list_files" && tc.args.path)
|
|
229
|
+
return `dir:${tc.args.path}`;
|
|
230
|
+
if (tc.toolName === "git_show" && tc.args.ref)
|
|
231
|
+
return `git:${tc.args.ref}`;
|
|
232
|
+
if (tc.toolName === "git_blame" && tc.args.path)
|
|
233
|
+
return `blame:${tc.args.path}`;
|
|
234
|
+
return `${tc.toolName}:${JSON.stringify(tc.args).slice(0, 60)}`;
|
|
235
|
+
}
|
|
236
|
+
/** Generate a stable packet ID */
|
|
237
|
+
function generatePacketId(participant, source, ref, snippet) {
|
|
238
|
+
const hash = createHash("sha256")
|
|
239
|
+
.update(`${participant}:${source}:${ref}:${snippet.slice(0, 200)}`)
|
|
240
|
+
.digest("hex")
|
|
241
|
+
.slice(0, 12);
|
|
242
|
+
return `pkt-${hash}`;
|
|
243
|
+
}
|
|
244
|
+
/** Infer the evidence source kind from the tool that produced it */
|
|
245
|
+
function inferSourceKind(toolName) {
|
|
246
|
+
switch (toolName) {
|
|
247
|
+
case "git_show":
|
|
248
|
+
case "git_blame":
|
|
249
|
+
return "git";
|
|
250
|
+
case "read_file":
|
|
251
|
+
case "grep_code":
|
|
252
|
+
case "list_files":
|
|
253
|
+
return "repo";
|
|
254
|
+
default:
|
|
255
|
+
return "repo";
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/** Build an EvidencePacket from a tool call result */
|
|
259
|
+
function buildEvidencePacketFromTool(participant, tc, result, source) {
|
|
260
|
+
const ref = buildResultRef(tc);
|
|
261
|
+
const snippet = result.slice(0, 500);
|
|
262
|
+
const packetId = generatePacketId(participant, source, ref, snippet);
|
|
263
|
+
return {
|
|
264
|
+
packetId,
|
|
265
|
+
participant,
|
|
266
|
+
source,
|
|
267
|
+
title: `${tc.toolName}: ${formatToolArgs(tc.args)}`,
|
|
268
|
+
refs: [ref],
|
|
269
|
+
summary: result.slice(0, 200),
|
|
270
|
+
excerpts: [{
|
|
271
|
+
ref,
|
|
272
|
+
snippet,
|
|
273
|
+
note: `Result of ${tc.toolName} call`,
|
|
274
|
+
}],
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
/** Format tool args into a concise display string */
|
|
278
|
+
function formatToolArgs(args) {
|
|
279
|
+
const parts = [];
|
|
280
|
+
for (const [key, value] of Object.entries(args)) {
|
|
281
|
+
if (typeof value === "string")
|
|
282
|
+
parts.push(value.slice(0, 60));
|
|
283
|
+
else
|
|
284
|
+
parts.push(`${key}=${JSON.stringify(value)}`);
|
|
285
|
+
}
|
|
286
|
+
return parts.join(", ").slice(0, 100);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Build links between findings and evidence packets by matching
|
|
290
|
+
* finding evidence refs against packet refs.
|
|
291
|
+
*/
|
|
292
|
+
function buildFindingEvidenceLinks(report, packets) {
|
|
293
|
+
const links = [];
|
|
294
|
+
for (const finding of report.findings) {
|
|
295
|
+
const matchedPacketIds = [];
|
|
296
|
+
for (const evidence of finding.evidence) {
|
|
297
|
+
const evidenceRef = `${evidence.type}:${evidence.ref}`;
|
|
298
|
+
// Find packets whose refs overlap with this evidence ref
|
|
299
|
+
for (const packet of packets) {
|
|
300
|
+
const matches = packet.refs.some((pRef) => {
|
|
301
|
+
// Exact match
|
|
302
|
+
if (pRef === evidenceRef)
|
|
303
|
+
return true;
|
|
304
|
+
// Partial match — packet ref contains the file path
|
|
305
|
+
if (evidence.ref && pRef.includes(evidence.ref))
|
|
306
|
+
return true;
|
|
307
|
+
return false;
|
|
308
|
+
});
|
|
309
|
+
if (matches && !matchedPacketIds.includes(packet.packetId)) {
|
|
310
|
+
matchedPacketIds.push(packet.packetId);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (matchedPacketIds.length > 0) {
|
|
315
|
+
links.push({
|
|
316
|
+
findingId: finding.id,
|
|
317
|
+
evidencePacketIds: matchedPacketIds,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return links;
|
|
322
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlanningDetailExpansion — expands high-level roadmap phases into
|
|
3
|
+
* repo-level implementation plans.
|
|
4
|
+
*
|
|
5
|
+
* This phase is planning-mode specific. It takes the roadmap produced
|
|
6
|
+
* by consensus and, for each phase, calls the LLM to produce concrete
|
|
7
|
+
* implementation details: target files, code changes, interfaces,
|
|
8
|
+
* migration steps, validation, effort, and blockers.
|
|
9
|
+
*
|
|
10
|
+
* Tool access is available so the LLM can verify file paths and interfaces.
|
|
11
|
+
*/
|
|
12
|
+
import type { ArenaParticipant, ArenaRoadmapPhase, ArenaRoadmapPhaseDetail, ArenaProgressEvent, ArenaExecutionLimits, ArenaStrategyPlanning, ArenaUsageRecorder } from "../types.js";
|
|
13
|
+
import type { ToolDefinition } from "@cjhyy/code-shell-core/extension";
|
|
14
|
+
import type { ArenaLedger } from "../ledger.js";
|
|
15
|
+
interface DetailExpansionOptions {
|
|
16
|
+
/** The participant that performs the expansion (typically the concluder) */
|
|
17
|
+
concluder: ArenaParticipant;
|
|
18
|
+
strategy: ArenaStrategyPlanning;
|
|
19
|
+
topic: string;
|
|
20
|
+
/** Roadmap phases to expand */
|
|
21
|
+
phases: ArenaRoadmapPhase[];
|
|
22
|
+
ledger: ArenaLedger;
|
|
23
|
+
limits: ArenaExecutionLimits;
|
|
24
|
+
/** Enable read-only context tools for file path verification */
|
|
25
|
+
enableContextTools?: boolean;
|
|
26
|
+
/** Plan-selected tools override */
|
|
27
|
+
contextTools?: ToolDefinition[];
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
onProgress?: (event: ArenaProgressEvent) => void;
|
|
30
|
+
onUsage?: ArenaUsageRecorder;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Expand each roadmap phase into a repo-level implementation plan.
|
|
34
|
+
*
|
|
35
|
+
* Phases are expanded sequentially to avoid token explosion.
|
|
36
|
+
* The number of phases expanded is capped by limits.maxExpandedPhasesPerRun.
|
|
37
|
+
*/
|
|
38
|
+
export declare function runDetailExpansion(options: DetailExpansionOptions): Promise<ArenaRoadmapPhaseDetail[]>;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlanningDetailExpansion — expands high-level roadmap phases into
|
|
3
|
+
* repo-level implementation plans.
|
|
4
|
+
*
|
|
5
|
+
* This phase is planning-mode specific. It takes the roadmap produced
|
|
6
|
+
* by consensus and, for each phase, calls the LLM to produce concrete
|
|
7
|
+
* implementation details: target files, code changes, interfaces,
|
|
8
|
+
* migration steps, validation, effort, and blockers.
|
|
9
|
+
*
|
|
10
|
+
* Tool access is available so the LLM can verify file paths and interfaces.
|
|
11
|
+
*/
|
|
12
|
+
import { createLLMClient } from "@cjhyy/code-shell-core/extension";
|
|
13
|
+
import { logger } from "@cjhyy/code-shell-core/extension";
|
|
14
|
+
import { buildDigest } from "../digest-builder.js";
|
|
15
|
+
import { CONTEXT_TOOLS, MAX_TOOL_ROUNDS, executeContextTool } from "../context/context-tools.js";
|
|
16
|
+
/** Max tool rounds per phase expansion (lighter than research) */
|
|
17
|
+
const MAX_EXPANSION_TOOL_ROUNDS = 3;
|
|
18
|
+
/**
|
|
19
|
+
* Expand each roadmap phase into a repo-level implementation plan.
|
|
20
|
+
*
|
|
21
|
+
* Phases are expanded sequentially to avoid token explosion.
|
|
22
|
+
* The number of phases expanded is capped by limits.maxExpandedPhasesPerRun.
|
|
23
|
+
*/
|
|
24
|
+
export async function runDetailExpansion(options) {
|
|
25
|
+
const { concluder, strategy, topic, phases, ledger, limits, signal, onProgress, onUsage } = options;
|
|
26
|
+
const tools = options.enableContextTools !== false
|
|
27
|
+
? (options.contextTools ?? CONTEXT_TOOLS)
|
|
28
|
+
: undefined;
|
|
29
|
+
// Cap phases to expand
|
|
30
|
+
const toExpand = phases.slice(0, limits.maxExpandedPhasesPerRun);
|
|
31
|
+
onProgress?.({ type: "roadmap_expansion_start", phaseCount: toExpand.length });
|
|
32
|
+
logger.info("arena.detail_expansion_start", { phaseCount: toExpand.length });
|
|
33
|
+
const details = [];
|
|
34
|
+
const client = await createLLMClient(concluder.llm, concluder.clientDefaults);
|
|
35
|
+
const systemPrompt = strategy.detailExpansionSystemPrompt();
|
|
36
|
+
// Expand each phase sequentially
|
|
37
|
+
for (const phase of toExpand) {
|
|
38
|
+
signal?.throwIfAborted();
|
|
39
|
+
// Build phase-specific digest using related findings
|
|
40
|
+
const relevantClaimIds = phase.relatedFindings ?? [];
|
|
41
|
+
const digest = buildDigest(ledger, { round: 1, relevantClaimIds });
|
|
42
|
+
const userContent = strategy.detailExpansionUserPrompt(topic, phase, digest);
|
|
43
|
+
const messages = [{ role: "user", content: userContent }];
|
|
44
|
+
let finalText = "";
|
|
45
|
+
// Tool-use loop: allow LLM to verify file paths and interfaces.
|
|
46
|
+
// `maxRounds` bounds the number of tool rounds; after the last one we make
|
|
47
|
+
// one final call (without tools) so the model produces text FROM the tool
|
|
48
|
+
// results rather than the empty text it emitted alongside the tool calls.
|
|
49
|
+
const maxRounds = tools ? Math.min(MAX_EXPANSION_TOOL_ROUNDS, MAX_TOOL_ROUNDS) : 0;
|
|
50
|
+
for (let round = 0; round <= maxRounds; round++) {
|
|
51
|
+
// On the final round, stop offering tools so the model must answer.
|
|
52
|
+
const offerTools = round < maxRounds ? tools : undefined;
|
|
53
|
+
const response = await client.createMessage({
|
|
54
|
+
systemPrompt,
|
|
55
|
+
messages,
|
|
56
|
+
tools: offerTools,
|
|
57
|
+
signal,
|
|
58
|
+
});
|
|
59
|
+
onUsage?.(response.usage);
|
|
60
|
+
const hasTools = response.toolCalls && response.toolCalls.length > 0;
|
|
61
|
+
logger.info("arena.detail_expansion_round", {
|
|
62
|
+
phase: phase.title,
|
|
63
|
+
round,
|
|
64
|
+
toolCount: response.toolCalls?.length ?? 0,
|
|
65
|
+
stopReason: response.stopReason,
|
|
66
|
+
});
|
|
67
|
+
if (!hasTools) {
|
|
68
|
+
finalText = response.text;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
// Append assistant message with tool_use blocks
|
|
72
|
+
const assistantBlocks = [];
|
|
73
|
+
if (response.text) {
|
|
74
|
+
assistantBlocks.push({ type: "text", text: response.text });
|
|
75
|
+
}
|
|
76
|
+
for (const tc of response.toolCalls) {
|
|
77
|
+
assistantBlocks.push({
|
|
78
|
+
type: "tool_use",
|
|
79
|
+
id: tc.id,
|
|
80
|
+
name: tc.toolName,
|
|
81
|
+
input: tc.args,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
messages.push({ role: "assistant", content: assistantBlocks });
|
|
85
|
+
// Execute tools and append results
|
|
86
|
+
const toolResultBlocks = [];
|
|
87
|
+
for (const tc of response.toolCalls) {
|
|
88
|
+
const result = executeContextTool(tc);
|
|
89
|
+
toolResultBlocks.push({
|
|
90
|
+
type: "tool_result",
|
|
91
|
+
tool_use_id: tc.id,
|
|
92
|
+
content: result.slice(0, 15_000),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
messages.push({ role: "user", content: toolResultBlocks });
|
|
96
|
+
}
|
|
97
|
+
// Retry if truncated
|
|
98
|
+
if (!finalText) {
|
|
99
|
+
finalText = "";
|
|
100
|
+
}
|
|
101
|
+
logger.info("arena.detail_expansion_phase", {
|
|
102
|
+
phase: phase.title,
|
|
103
|
+
textLen: finalText.length,
|
|
104
|
+
});
|
|
105
|
+
const detail = strategy.parseDetailExpansionResponse(finalText);
|
|
106
|
+
if (!detail.phaseTitle)
|
|
107
|
+
detail.phaseTitle = phase.title;
|
|
108
|
+
// Log parse quality
|
|
109
|
+
if (detail.targetFiles.length === 0 && detail.codeChanges.length === 0) {
|
|
110
|
+
logger.warn("arena.detail_expansion_sparse", {
|
|
111
|
+
phase: phase.title,
|
|
112
|
+
objective: detail.objective.slice(0, 200),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
details.push(detail);
|
|
116
|
+
}
|
|
117
|
+
logger.info("arena.detail_expansion_done", { detailCount: details.length });
|
|
118
|
+
onProgress?.({ type: "roadmap_expansion_done", detailCount: details.length });
|
|
119
|
+
return details;
|
|
120
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Planner — the brain of Evidence-Driven Arena.
|
|
3
|
+
*
|
|
4
|
+
* Takes a natural language user request and produces a complete ArenaPlan:
|
|
5
|
+
* - mode (how to collaborate)
|
|
6
|
+
* - lenses (from what perspectives)
|
|
7
|
+
* - sources (where to gather evidence)
|
|
8
|
+
* - subject (what is being analyzed)
|
|
9
|
+
* - outputShape (how to structure the result)
|
|
10
|
+
*
|
|
11
|
+
* Replaces the old IntentResolver + ScopeResolver pipeline with a single
|
|
12
|
+
* LLM call that outputs a holistic execution plan.
|
|
13
|
+
*/
|
|
14
|
+
import type { LLMConfig } from "@cjhyy/code-shell-core/extension";
|
|
15
|
+
import type { ArenaUsageRecorder } from "./types.js";
|
|
16
|
+
import type { ArenaPlan, ArenaMode } from "./types.js";
|
|
17
|
+
/** Explicit flags the user may pass via CLI or tool args */
|
|
18
|
+
export interface PlannerFlags {
|
|
19
|
+
mode?: ArenaMode;
|
|
20
|
+
base?: string;
|
|
21
|
+
head?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Run the planner to produce an ArenaPlan from natural language.
|
|
25
|
+
*
|
|
26
|
+
* Priority: explicit flags > LLM plan > safe defaults.
|
|
27
|
+
*/
|
|
28
|
+
export declare function planArena(topic: string, llmConfig: LLMConfig, flags?: PlannerFlags, signal?: AbortSignal, onUsage?: ArenaUsageRecorder): Promise<ArenaPlan>;
|