@zelari/core 2.33.0 → 2.34.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 +1 -1
- package/dist/agents/council/chairmanDelivery.d.ts +92 -0
- package/dist/agents/council/chairmanDelivery.d.ts.map +1 -0
- package/dist/agents/council/chairmanDelivery.js +226 -0
- package/dist/agents/council/chairmanDelivery.js.map +1 -0
- package/dist/agents/council/chairmanFixLoop.d.ts +47 -0
- package/dist/agents/council/chairmanFixLoop.d.ts.map +1 -0
- package/dist/agents/council/chairmanFixLoop.js +127 -0
- package/dist/agents/council/chairmanFixLoop.js.map +1 -0
- package/dist/agents/council/memberMessages.d.ts +32 -0
- package/dist/agents/council/memberMessages.d.ts.map +1 -0
- package/dist/agents/council/memberMessages.js +95 -0
- package/dist/agents/council/memberMessages.js.map +1 -0
- package/dist/agents/council/outputCleaning.d.ts +37 -0
- package/dist/agents/council/outputCleaning.d.ts.map +1 -0
- package/dist/agents/council/outputCleaning.js +150 -0
- package/dist/agents/council/outputCleaning.js.map +1 -0
- package/dist/agents/council/retryTurn.d.ts +128 -0
- package/dist/agents/council/retryTurn.d.ts.map +1 -0
- package/dist/agents/council/retryTurn.js +200 -0
- package/dist/agents/council/retryTurn.js.map +1 -0
- package/dist/agents/council/toolEmission.d.ts +61 -0
- package/dist/agents/council/toolEmission.d.ts.map +1 -0
- package/dist/agents/council/toolEmission.js +110 -0
- package/dist/agents/council/toolEmission.js.map +1 -0
- package/dist/agents/council/types.d.ts +184 -0
- package/dist/agents/council/types.d.ts.map +1 -0
- package/dist/agents/council/types.js +15 -0
- package/dist/agents/council/types.js.map +1 -0
- package/dist/agents/councilApi.d.ts +11 -526
- package/dist/agents/councilApi.d.ts.map +1 -1
- package/dist/agents/councilApi.js +19 -893
- package/dist/agents/councilApi.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -1,268 +1,22 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { getAgent, getCouncilAgents,
|
|
3
|
+
import { getAgent, getCouncilAgents, swapMembers } from './roles.js';
|
|
4
4
|
import { getProviderTools } from './toolSchemas.js';
|
|
5
|
-
import {
|
|
6
|
-
import { getAllTools } from './tools.js';
|
|
5
|
+
import { computeAgentTools, } from './systemPromptBuilder.js';
|
|
7
6
|
import { buildLanguagePolicyModuleFor } from './languagePolicy.js';
|
|
8
|
-
import { scrubProprietaryLeak } from './secrecyPolicy.js';
|
|
9
7
|
import { createBrainEvent } from '../shared/events.js';
|
|
10
|
-
import { AgentHarness,
|
|
11
|
-
import { councilModeBanner } from '../council/modeBanners.js';
|
|
8
|
+
import { AgentHarness, } from '../core/AgentHarness.js';
|
|
12
9
|
import { councilTierFromSize } from '../council/runMode.js';
|
|
13
10
|
import { parseProjectRootFromWorkspaceContext, runChairmanMicroGate, } from '../council/verification/microGate.js';
|
|
14
|
-
import { buildImplementationVerifyRetryPrompt, checkImplementationCompletion, resolveVerifyRetryTool, } from '../council/verification/completion.js';
|
|
15
11
|
import { warnIfNfrSpecMissing } from '../council/scope/nfrSpecWarn.js';
|
|
16
|
-
import { loadNfrSpec, DEFAULT_NFR_SPEC,
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* could persist in the 240s budget, making the retry a pure waste.
|
|
25
|
-
*
|
|
26
|
-
* v0.7.8 removes 'nettun': the plan contract is now satisfiable with a
|
|
27
|
-
* SINGLE `createPlan` batch call (phases + nested tasks + milestone in
|
|
28
|
-
* one emission), so the forced retry has the same 1-call budget that
|
|
29
|
-
* already works reliably for Minosse and Lucifero. The set stays
|
|
30
|
-
* exported as the opt-out mechanism for future members.
|
|
31
|
-
*/
|
|
32
|
-
export const NON_RETRY_AGENTS = new Set([]);
|
|
33
|
-
const QUESTION_MARKER = '---QUESTION---';
|
|
34
|
-
const QUESTION_END_MARKER = '---END---';
|
|
35
|
-
/**
|
|
36
|
-
* Extract the first top-level JSON object from `s` using brace depth so trailing
|
|
37
|
-
* MiniMax/tool garbage after `}` does not break JSON.parse (common failure mode:
|
|
38
|
-
* `---QUESTION--- {…}]<]minimax…` without ---END---).
|
|
39
|
-
*/
|
|
40
|
-
function extractBalancedJsonObject(s) {
|
|
41
|
-
const start = s.indexOf('{');
|
|
42
|
-
if (start < 0)
|
|
43
|
-
return null;
|
|
44
|
-
let depth = 0;
|
|
45
|
-
let inString = false;
|
|
46
|
-
let escape = false;
|
|
47
|
-
for (let i = start; i < s.length; i++) {
|
|
48
|
-
const ch = s[i];
|
|
49
|
-
if (inString) {
|
|
50
|
-
if (escape) {
|
|
51
|
-
escape = false;
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
if (ch === '\\') {
|
|
55
|
-
escape = true;
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
if (ch === '"')
|
|
59
|
-
inString = false;
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
if (ch === '"') {
|
|
63
|
-
inString = true;
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
if (ch === '{')
|
|
67
|
-
depth++;
|
|
68
|
-
else if (ch === '}') {
|
|
69
|
-
depth--;
|
|
70
|
-
if (depth === 0)
|
|
71
|
-
return s.slice(start, i + 1);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
export function parseClarificationRequest(text) {
|
|
77
|
-
const start = text.indexOf(QUESTION_MARKER);
|
|
78
|
-
if (start < 0)
|
|
79
|
-
return null;
|
|
80
|
-
const rest = text.slice(start + QUESTION_MARKER.length);
|
|
81
|
-
const end = rest.indexOf(QUESTION_END_MARKER);
|
|
82
|
-
const block = end >= 0 ? rest.slice(0, end) : rest;
|
|
83
|
-
const cleaned = block.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
|
|
84
|
-
const jsonText = extractBalancedJsonObject(cleaned) ??
|
|
85
|
-
(() => {
|
|
86
|
-
const objStart = cleaned.indexOf('{');
|
|
87
|
-
const objEnd = cleaned.lastIndexOf('}');
|
|
88
|
-
return objStart >= 0 && objEnd > objStart
|
|
89
|
-
? cleaned.slice(objStart, objEnd + 1)
|
|
90
|
-
: cleaned;
|
|
91
|
-
})();
|
|
92
|
-
try {
|
|
93
|
-
const parsed = JSON.parse(jsonText);
|
|
94
|
-
if (typeof parsed.question !== 'string' || !parsed.question.trim())
|
|
95
|
-
return null;
|
|
96
|
-
return {
|
|
97
|
-
question: parsed.question.trim(),
|
|
98
|
-
choices: Array.isArray(parsed.choices)
|
|
99
|
-
? parsed.choices.filter((c) => typeof c === 'string' && c.trim().length > 0).map((c) => c.trim())
|
|
100
|
-
: undefined,
|
|
101
|
-
context: typeof parsed.context === 'string' ? parsed.context.trim() : undefined,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
/** True when text contains a structured question with ≥2 choices (pause UI). */
|
|
109
|
-
export function hasInteractiveClarification(text) {
|
|
110
|
-
const c = parseClarificationRequest(text);
|
|
111
|
-
return !!(c && c.choices && c.choices.length >= 2);
|
|
112
|
-
}
|
|
113
|
-
export function parseThinking(text) {
|
|
114
|
-
// Prefer complete blocks; fall back to unclosed trailing block (common mid-stream).
|
|
115
|
-
const complete = text.match(/<think(?:ing)?>([\s\S]*?)<\/think(?:ing)?>/i);
|
|
116
|
-
if (complete)
|
|
117
|
-
return complete[1].trim();
|
|
118
|
-
const open = text.match(/<think(?:ing)?>([\s\S]*)$/i);
|
|
119
|
-
return open ? open[1].trim() : '';
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Strip model "private" channels from text shown to the user / re-fed as
|
|
123
|
-
* history. Covers:
|
|
124
|
-
* - complete + unclosed `<think>` / `<thinking>` (GLM/MiniMax style) — optional
|
|
125
|
-
* - MiniMax XML tool-call wrappers
|
|
126
|
-
* - clarifying-question JSON blocks (display only; keep in provider history)
|
|
127
|
-
*
|
|
128
|
-
* Without unclosed-tag stripping, streamed thinking that never got a closing
|
|
129
|
-
* tag leaked into the TUI as visible assistant prose (v1.8.1).
|
|
130
|
-
*/
|
|
131
|
-
export function cleanAgentContent(text, opts = {}) {
|
|
132
|
-
const stripQuestion = opts.stripQuestion !== false;
|
|
133
|
-
const stripThink = opts.stripThink !== false;
|
|
134
|
-
let out = text;
|
|
135
|
-
if (stripThink) {
|
|
136
|
-
out = out
|
|
137
|
-
.replace(/<think(?:ing)?>[\s\S]*?<\/think(?:ing)?>/gi, '')
|
|
138
|
-
.replace(/<think(?:ing)?>[\s\S]*$/gi, '')
|
|
139
|
-
.replace(/<\/think(?:ing)?>/gi, '');
|
|
140
|
-
}
|
|
141
|
-
// Tool-call markup that models sometimes dump into prose (MiniMax / GLM / generic).
|
|
142
|
-
// Prefer closed-block removal first; only then strip *trailing* unclosed
|
|
143
|
-
// open tags. Never use a mid-string open-tag → EOF wipe for tool tags when
|
|
144
|
-
// there may still be real prose after a broken unclosed tag sequence —
|
|
145
|
-
// headless streamScrub re-cleans the full buffer each push, so closed pairs
|
|
146
|
-
// are enough mid-stream; trailing open is for end-of-turn.
|
|
147
|
-
out = out
|
|
148
|
-
.replace(/<minimax:tool_call>[\s\S]*?<\/minimax:tool_call>/gi, '')
|
|
149
|
-
.replace(/<\/?minimax:tool_call>/gi, '')
|
|
150
|
-
.replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '')
|
|
151
|
-
.replace(/<\/?tool_call>/gi, '')
|
|
152
|
-
.replace(/<function_call>[\s\S]*?<\/function_call>/gi, '')
|
|
153
|
-
.replace(/<\/?function_call>/gi, '')
|
|
154
|
-
.replace(/<invoke\b[^>]*>[\s\S]*?<\/invoke>/gi, '')
|
|
155
|
-
.replace(/<\/invoke>/gi, '')
|
|
156
|
-
.replace(/<parameter\b[^>]*>[\s\S]*?<\/parameter>/gi, '')
|
|
157
|
-
.replace(/<\/parameter>/gi, '')
|
|
158
|
-
.replace(/\]\s*<\]\s*minimax\s*\[>\s*\[?<invoke\b[^>]*>[\s\S]*?<\/invoke>/gi, '')
|
|
159
|
-
// Trailing unclosed tool channels only (end of buffer)
|
|
160
|
-
.replace(/<minimax:tool_call>[\s\S]*$/gi, '')
|
|
161
|
-
.replace(/<tool_call>[\s\S]*$/gi, '')
|
|
162
|
-
.replace(/<function_call>[\s\S]*$/gi, '')
|
|
163
|
-
.replace(/<invoke\b[^>]*>[\s\S]*$/gi, '')
|
|
164
|
-
.replace(/\]\s*<\]\s*minimax\s*\[>[\s\S]*$/gi, '')
|
|
165
|
-
.replace(/^\s*\]\s*<\]\s*minimax\s*\[>.*$/gim, '')
|
|
166
|
-
.replace(/^\s*<\/?(?:tool_call|function_call|invoke|parameter|minimax:tool_call)\b[^>]*>\s*$/gim, '');
|
|
167
|
-
if (stripQuestion) {
|
|
168
|
-
// Closed block, or unclosed (model often omits ---END--- then dumps tools).
|
|
169
|
-
out = out
|
|
170
|
-
.replace(/---QUESTION---[\s\S]*?---END---/g, '')
|
|
171
|
-
.replace(/---QUESTION---[\s\S]*$/g, '');
|
|
172
|
-
}
|
|
173
|
-
out = out.replace(/\n{3,}/g, '\n\n').trim();
|
|
174
|
-
// Defense-in-depth: strip proprietary prompt dumps that escaped model policy.
|
|
175
|
-
return scrubProprietaryLeak(out);
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Tools that mutate project files. In implementation-mode council runs only the
|
|
179
|
-
* chairman (Lucifero) implements; specialists + Minosse analyze and hand off.
|
|
180
|
-
* We strip these from every non-implementer so multiple agents never edit the
|
|
181
|
-
* same files (multi-writer chaos) and "who implemented" stays unambiguous.
|
|
182
|
-
* Note: specialists inherit write tools via skill `requiredTools`, so this must
|
|
183
|
-
* filter the RESULT of computeAgentTools, not just the declared role tools.
|
|
184
|
-
*/
|
|
185
|
-
export const MUTATING_PROJECT_TOOLS = ['write_file', 'edit_file'];
|
|
186
|
-
/**
|
|
187
|
-
* Remove file-mutating tools for non-implementer members in implementation mode.
|
|
188
|
-
* Design-phase and the implementer (chairman) keep the full set unchanged.
|
|
189
|
-
*/
|
|
190
|
-
export function restrictImplementationWrites(toolNames, opts) {
|
|
191
|
-
if (opts.runMode !== 'implementation' || opts.isImplementer)
|
|
192
|
-
return toolNames;
|
|
193
|
-
return toolNames.filter((t) => !MUTATING_PROJECT_TOOLS.includes(t));
|
|
194
|
-
}
|
|
195
|
-
function buildAgentMessages(agent, userMessage, ragContext, workspaceContext, priorOutputs, aiConfig, executableTools, runMode = 'implementation', languageModule) {
|
|
196
|
-
// v0.7.5: the AVAILABLE TOOLS prompt block must match the schemas the
|
|
197
|
-
// harness actually advertises. The v0.7.3 fix filtered the schemas
|
|
198
|
-
// (filterExecutable) but NOT this prompt text, so members still read
|
|
199
|
-
// "searchRAG: search the knowledge base…" in their system prompt and
|
|
200
|
-
// called it — every call a guaranteed "Tool not found" (live test
|
|
201
|
-
// 2026-07-03, /council in Z:\EasyPeasy\test).
|
|
202
|
-
const allToolNames = computeAgentTools(agent, aiConfig);
|
|
203
|
-
const toolNames = executableTools
|
|
204
|
-
? allToolNames.filter((n) => executableTools.has(n))
|
|
205
|
-
: allToolNames;
|
|
206
|
-
// Merge language policy into custom modules so every primary council turn
|
|
207
|
-
// gets it (retries previously received the arg but never used it).
|
|
208
|
-
const mergedAiConfig = languageModule
|
|
209
|
-
? {
|
|
210
|
-
enabledSkills: aiConfig?.enabledSkills ?? [],
|
|
211
|
-
enabledTools: aiConfig?.enabledTools ?? [],
|
|
212
|
-
agentSkillConfigs: aiConfig?.agentSkillConfigs ?? [],
|
|
213
|
-
customSkills: aiConfig?.customSkills,
|
|
214
|
-
customPromptModules: [
|
|
215
|
-
...(aiConfig?.customPromptModules ?? []),
|
|
216
|
-
languageModule,
|
|
217
|
-
],
|
|
218
|
-
}
|
|
219
|
-
: aiConfig;
|
|
220
|
-
// Mode-split: design mandatories only when runMode is design-phase.
|
|
221
|
-
const modeAwareAgent = {
|
|
222
|
-
...agent,
|
|
223
|
-
systemPrompt: resolveRoleSystemPrompt(agent, runMode),
|
|
224
|
-
};
|
|
225
|
-
// Cache-efficient split (Cache Wars): stable = identity/tools/role;
|
|
226
|
-
// volatile = workspace/RAG; banners stay trailing system msgs (volatile).
|
|
227
|
-
const split = buildSystemPromptSplit(modeAwareAgent, {
|
|
228
|
-
tools: getAllTools(),
|
|
229
|
-
toolNames,
|
|
230
|
-
aiConfig: mergedAiConfig,
|
|
231
|
-
workspaceContext,
|
|
232
|
-
ragContext,
|
|
233
|
-
mode: 'council',
|
|
234
|
-
includeWorkspaceInPrompt: true,
|
|
235
|
-
});
|
|
236
|
-
const messages = [
|
|
237
|
-
{ role: 'system', content: split.stable },
|
|
238
|
-
];
|
|
239
|
-
if (split.volatile.trim()) {
|
|
240
|
-
messages.push({ role: 'system', content: split.volatile });
|
|
241
|
-
}
|
|
242
|
-
messages.push({ role: 'system', content: councilModeBanner(runMode, { isImplementer: agent.id === 'lucifer' }) }, { role: 'system', content: 'IMPORTANT: Before making any tool calls or expensive operations, check if the information already exists in the shared context from previous agents. Avoid redundant work.' });
|
|
243
|
-
if (priorOutputs.length > 0) {
|
|
244
|
-
// Cap each prior member blob so one verbose specialist cannot saturate
|
|
245
|
-
// downstream members (chairman especially). Full text is not product law.
|
|
246
|
-
const MAX_PRIOR_CHARS = 2800;
|
|
247
|
-
const summary = priorOutputs
|
|
248
|
-
.map((o) => {
|
|
249
|
-
const body = o.content.length > MAX_PRIOR_CHARS
|
|
250
|
-
? `${o.content.slice(0, MAX_PRIOR_CHARS)}\n… [truncated ${o.content.length}→${MAX_PRIOR_CHARS} chars; treat as hypothesis]`
|
|
251
|
-
: o.content;
|
|
252
|
-
return `[${o.name} - ${o.role}]: ${body}`;
|
|
253
|
-
})
|
|
254
|
-
.join('\n\n');
|
|
255
|
-
messages.push({
|
|
256
|
-
role: 'user',
|
|
257
|
-
content: `Previous council members have said (hypotheses — prefer product files on disk if they conflict):\n${summary}\n\n` +
|
|
258
|
-
`Original user request: ${userMessage}`,
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
messages.push({ role: 'user', content: userMessage });
|
|
263
|
-
}
|
|
264
|
-
return messages;
|
|
265
|
-
}
|
|
12
|
+
import { loadNfrSpec, DEFAULT_NFR_SPEC, } from '../council/verification/runChecks.js';
|
|
13
|
+
import { checkImplementationDelivery, countEmittedWriteTools, } from '../council/verification/implementationDelivery.js';
|
|
14
|
+
import { NON_RETRY_AGENTS } from './council/types.js';
|
|
15
|
+
export { NON_RETRY_AGENTS } from './council/types.js';
|
|
16
|
+
import { cleanAgentContent, parseClarificationRequest, parseThinking } from './council/outputCleaning.js';
|
|
17
|
+
export { cleanAgentContent, hasInteractiveClarification, parseClarificationRequest, parseThinking } from './council/outputCleaning.js';
|
|
18
|
+
import { buildAgentMessages, restrictImplementationWrites } from './council/memberMessages.js';
|
|
19
|
+
export { MUTATING_PROJECT_TOOLS, restrictImplementationWrites } from './council/memberMessages.js';
|
|
266
20
|
/**
|
|
267
21
|
* PURE council orchestration. Loops through specialists, optionally runs
|
|
268
22
|
* the oracle (debate mode), then runs the chairman synthesis.
|
|
@@ -1036,640 +790,12 @@ export async function* runCouncilPure(userMessage, config, callbacks = {}) {
|
|
|
1036
790
|
durationMs: 0,
|
|
1037
791
|
};
|
|
1038
792
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
export
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
if (!retryTool) {
|
|
1048
|
-
// eslint-disable-next-line no-console
|
|
1049
|
-
console.warn('[council] implementation verify retry skipped — no grep_content/bash in registry');
|
|
1050
|
-
return;
|
|
1051
|
-
}
|
|
1052
|
-
if (!shouldRetryMember([retryTool], 0))
|
|
1053
|
-
return;
|
|
1054
|
-
// eslint-disable-next-line no-console
|
|
1055
|
-
console.warn(`[council] ${args.agent.id} retrying missing verify tool: ${retryTool}`);
|
|
1056
|
-
try {
|
|
1057
|
-
const retryGenerator = runRetryTurnForMember({
|
|
1058
|
-
agent: args.agent,
|
|
1059
|
-
missingToolNames: [retryTool],
|
|
1060
|
-
executableTools: args.executableNames,
|
|
1061
|
-
userMessage: args.userMessage,
|
|
1062
|
-
ragContext: args.config.ragContext,
|
|
1063
|
-
workspaceContext: args.config.workspaceContext,
|
|
1064
|
-
priorOutputs: args.agentOutputs,
|
|
1065
|
-
aiConfig: args.config.aiConfig,
|
|
1066
|
-
sessionId: args.sessionId,
|
|
1067
|
-
effectiveModel: args.effectiveModel,
|
|
1068
|
-
effectiveProvider: args.effectiveProvider,
|
|
1069
|
-
eventBus: args.config.eventBus,
|
|
1070
|
-
toolRegistry: args.config.tools,
|
|
1071
|
-
providerStream: args.config.providerStream,
|
|
1072
|
-
runMode: args.config.runMode,
|
|
1073
|
-
retryPrompt: buildImplementationVerifyRetryPrompt(retryTool),
|
|
1074
|
-
languageModule: args.languageModule,
|
|
1075
|
-
});
|
|
1076
|
-
for await (const event of retryGenerator) {
|
|
1077
|
-
if (event.type === 'tool_execution_start') {
|
|
1078
|
-
args.onToolCall();
|
|
1079
|
-
args.emittedToolNames.push(event.toolName);
|
|
1080
|
-
}
|
|
1081
|
-
yield event;
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
catch (retryErr) {
|
|
1085
|
-
// eslint-disable-next-line no-console
|
|
1086
|
-
console.error(`[council] ${args.agent.id} verify retry failed:`, retryErr);
|
|
1087
|
-
}
|
|
1088
|
-
const after = checkImplementationCompletion(args.emittedToolNames);
|
|
1089
|
-
if (!after.ok) {
|
|
1090
|
-
// eslint-disable-next-line no-console
|
|
1091
|
-
console.warn(`[council] ${args.agent.id} still missing verify after retry: ${after.reason}`);
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
/**
|
|
1095
|
-
* Build the scoped fix instruction for the chairman from deterministic motion
|
|
1096
|
-
* violations. Groups by file, lists file:line + rule, and constrains the model
|
|
1097
|
-
* to fix ONLY these (no new features, no rewrites). Exported for testing.
|
|
1098
|
-
*/
|
|
1099
|
-
export function buildMotionFixPrompt(violations) {
|
|
1100
|
-
const byFile = new Map();
|
|
1101
|
-
for (const v of violations) {
|
|
1102
|
-
const file = v.file || 'index.html';
|
|
1103
|
-
const loc = v.line ? `${file}:L${v.line}` : file;
|
|
1104
|
-
const list = byFile.get(file) ?? [];
|
|
1105
|
-
list.push(` - ${loc}: ${v.message}`);
|
|
1106
|
-
byFile.set(file, list);
|
|
1107
|
-
}
|
|
1108
|
-
const blocks = Array.from(byFile.values()).map((lines) => lines.join('\n')).join('\n');
|
|
1109
|
-
return (`Deterministic verification found ${violations.length} motion violation(s) in the file(s) you just edited. ` +
|
|
1110
|
-
`Fix ONLY these — do not add features, do not rewrite sections, do not touch anything else:\n${blocks}\n\n` +
|
|
1111
|
-
`Rules: animate ONLY transform and opacity. Replace any box-shadow / background / background-position / ` +
|
|
1112
|
-
`filter / color / border-color / width / height / grid-template-rows used in @keyframes or transitions with ` +
|
|
1113
|
-
`transform/opacity equivalents (e.g. render a glow via a pseudo-element that scales and fades). For every ` +
|
|
1114
|
-
`classList.add('x') in the script, add a matching '.x' CSS rule. Use read_file to see the exact lines, then ` +
|
|
1115
|
-
`edit_file. When the listed items are fixed, stop — no summary.`);
|
|
1116
|
-
}
|
|
1117
|
-
/**
|
|
1118
|
-
* Forced retry when Lucifero finished without a successful write_file/edit_file.
|
|
1119
|
-
*/
|
|
1120
|
-
export async function* applyImplementationWriteRetry(args) {
|
|
1121
|
-
if (args.check.ok)
|
|
1122
|
-
return;
|
|
1123
|
-
if (!shouldRetryMember(['write_file'], 0))
|
|
1124
|
-
return;
|
|
1125
|
-
const statusMsg = `[council] ${args.chairman.id} implementation write retry: ${args.check.missing.join(', ')}`;
|
|
1126
|
-
args.onCouncilStatus?.(statusMsg);
|
|
1127
|
-
// eslint-disable-next-line no-console
|
|
1128
|
-
console.warn(statusMsg);
|
|
1129
|
-
try {
|
|
1130
|
-
const retryGenerator = runRetryTurnForMember({
|
|
1131
|
-
agent: args.chairman,
|
|
1132
|
-
missingToolNames: ['read_file', 'write_file', 'edit_file'],
|
|
1133
|
-
minPerTool: { read_file: 3, edit_file: 8, write_file: 1 },
|
|
1134
|
-
executableTools: args.executableNames,
|
|
1135
|
-
userMessage: args.userMessage,
|
|
1136
|
-
ragContext: args.config.ragContext,
|
|
1137
|
-
workspaceContext: args.config.workspaceContext,
|
|
1138
|
-
priorOutputs: args.agentOutputs,
|
|
1139
|
-
aiConfig: args.config.aiConfig,
|
|
1140
|
-
sessionId: args.sessionId,
|
|
1141
|
-
effectiveModel: args.effectiveModel,
|
|
1142
|
-
effectiveProvider: args.effectiveProvider,
|
|
1143
|
-
eventBus: args.config.eventBus,
|
|
1144
|
-
toolRegistry: args.config.tools,
|
|
1145
|
-
providerStream: args.config.providerStream,
|
|
1146
|
-
runMode: 'implementation',
|
|
1147
|
-
retryPrompt: buildImplementationWriteRetryPrompt(args.userMessage),
|
|
1148
|
-
languageModule: args.languageModule,
|
|
1149
|
-
});
|
|
1150
|
-
for await (const event of retryGenerator) {
|
|
1151
|
-
if (event.type === 'tool_execution_start')
|
|
1152
|
-
args.onToolCall?.();
|
|
1153
|
-
if (event.type === 'tool_execution_end' &&
|
|
1154
|
-
!event.isError &&
|
|
1155
|
-
typeof event.result === 'string') {
|
|
1156
|
-
try {
|
|
1157
|
-
const parsed = JSON.parse(event.result);
|
|
1158
|
-
if ((parsed.bytesWritten ?? 0) > 0 ||
|
|
1159
|
-
(parsed.occurrencesReplaced ?? 0) > 0) {
|
|
1160
|
-
args.onSuccessfulWrite?.();
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
catch {
|
|
1164
|
-
if (event.result.includes('bytesWritten') || event.result.includes('occurrencesReplaced')) {
|
|
1165
|
-
args.onSuccessfulWrite?.();
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
yield event;
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
catch (retryErr) {
|
|
1173
|
-
// eslint-disable-next-line no-console
|
|
1174
|
-
console.error(`[council] ${args.chairman.id} implementation write retry failed:`, retryErr);
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
/** Max verify-driven delivery passes after the chairman turn (inline-js, motion, etc.). */
|
|
1178
|
-
export const MAX_DELIVERY_ATTEMPTS = 2;
|
|
1179
|
-
/**
|
|
1180
|
-
* Increment 5 — verify-driven delivery loop. Re-runs deterministic verification
|
|
1181
|
-
* and forces scoped chairman fix turns until blocking technical issues clear
|
|
1182
|
-
* or the attempt cap is hit.
|
|
1183
|
-
*/
|
|
1184
|
-
export async function* runChairmanDeliveryLoop(args) {
|
|
1185
|
-
const maxAttempts = args.maxAttempts ?? MAX_DELIVERY_ATTEMPTS;
|
|
1186
|
-
const zelariRoot = `${args.projectRoot}/.zelari`;
|
|
1187
|
-
let attempt = 0;
|
|
1188
|
-
while (attempt < maxAttempts) {
|
|
1189
|
-
const report = runImplementationVerification({
|
|
1190
|
-
projectRoot: args.projectRoot,
|
|
1191
|
-
zelariRoot,
|
|
1192
|
-
});
|
|
1193
|
-
const blocking = filterDeliveryBlockingFails(report.results);
|
|
1194
|
-
if (blocking.length === 0)
|
|
1195
|
-
return true;
|
|
1196
|
-
if (blocking.some((b) => b.id === 'inline-js.budget')) {
|
|
1197
|
-
const jsFix = applyInlineJsAutofix(args.projectRoot, report);
|
|
1198
|
-
if (jsFix.applied) {
|
|
1199
|
-
args.onCouncilStatus?.(`[council] ${args.chairman.id} inline-js autofix: ${jsFix.fixes.join('; ')}`);
|
|
1200
|
-
const afterJs = runImplementationVerification({
|
|
1201
|
-
projectRoot: args.projectRoot,
|
|
1202
|
-
zelariRoot,
|
|
1203
|
-
});
|
|
1204
|
-
if (filterDeliveryBlockingFails(afterJs.results).length === 0)
|
|
1205
|
-
return true;
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
attempt++;
|
|
1209
|
-
const statusMsg = `[council] ${args.chairman.id} delivery pass ${attempt}/${maxAttempts}: ${blocking.map((b) => b.id).join(', ')}`;
|
|
1210
|
-
args.onCouncilStatus?.(statusMsg);
|
|
1211
|
-
// eslint-disable-next-line no-console
|
|
1212
|
-
console.warn(statusMsg);
|
|
1213
|
-
try {
|
|
1214
|
-
const fixGenerator = runRetryTurnForMember({
|
|
1215
|
-
agent: args.chairman,
|
|
1216
|
-
missingToolNames: ['read_file', 'edit_file'],
|
|
1217
|
-
minPerTool: { read_file: 3, edit_file: 10 },
|
|
1218
|
-
executableTools: args.executableNames,
|
|
1219
|
-
userMessage: args.userMessage,
|
|
1220
|
-
ragContext: args.config.ragContext,
|
|
1221
|
-
workspaceContext: args.config.workspaceContext,
|
|
1222
|
-
priorOutputs: args.agentOutputs,
|
|
1223
|
-
aiConfig: args.config.aiConfig,
|
|
1224
|
-
sessionId: args.sessionId,
|
|
1225
|
-
effectiveModel: args.effectiveModel,
|
|
1226
|
-
effectiveProvider: args.effectiveProvider,
|
|
1227
|
-
eventBus: args.config.eventBus,
|
|
1228
|
-
toolRegistry: args.config.tools,
|
|
1229
|
-
providerStream: args.config.providerStream,
|
|
1230
|
-
runMode: 'implementation',
|
|
1231
|
-
retryPrompt: buildDeliveryFixPrompt(blocking, args.userMessage),
|
|
1232
|
-
languageModule: args.languageModule,
|
|
1233
|
-
});
|
|
1234
|
-
for await (const event of fixGenerator) {
|
|
1235
|
-
if (event.type === 'tool_execution_start')
|
|
1236
|
-
args.onToolCall?.();
|
|
1237
|
-
yield event;
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
catch (deliveryErr) {
|
|
1241
|
-
// eslint-disable-next-line no-console
|
|
1242
|
-
console.error(`[council] ${args.chairman.id} delivery pass ${attempt} failed:`, deliveryErr);
|
|
1243
|
-
break;
|
|
1244
|
-
}
|
|
1245
|
-
for (const rel of args.changedFiles) {
|
|
1246
|
-
for (const w of runChairmanMicroGate({ projectRoot: args.projectRoot, relPath: rel, zelariRoot })) {
|
|
1247
|
-
// refresh changed set — delivery may touch same targets
|
|
1248
|
-
args.changedFiles.add(w.file ?? rel);
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
const finalReport = runImplementationVerification({
|
|
1253
|
-
projectRoot: args.projectRoot,
|
|
1254
|
-
zelariRoot,
|
|
1255
|
-
});
|
|
1256
|
-
return filterDeliveryBlockingFails(finalReport.results).length === 0;
|
|
1257
|
-
}
|
|
1258
|
-
/**
|
|
1259
|
-
* Re-execute edit_file/write_file calls from a `---TOOLS---` block after the
|
|
1260
|
-
* chairman turn. Safety net when the harness path parsed the block but edits
|
|
1261
|
-
* failed (oldString drift) or the block was not fully executed.
|
|
1262
|
-
*/
|
|
1263
|
-
export async function* replayChairmanTextTools(args) {
|
|
1264
|
-
const tools = parseTextToolCalls(args.synthesisText);
|
|
1265
|
-
if (tools.length === 0)
|
|
1266
|
-
return 0;
|
|
1267
|
-
let applied = 0;
|
|
1268
|
-
for (const tt of tools) {
|
|
1269
|
-
if (tt.name !== 'edit_file' && tt.name !== 'write_file')
|
|
1270
|
-
continue;
|
|
1271
|
-
const normalized = normalizeTextToolArgs(tt.name, tt.args);
|
|
1272
|
-
const toolCallId = `replay-${crypto.randomUUID().slice(0, 8)}`;
|
|
1273
|
-
yield createBrainEvent('tool_execution_start', args.sessionId, {
|
|
1274
|
-
toolCallId,
|
|
1275
|
-
toolName: tt.name,
|
|
1276
|
-
args: normalized,
|
|
1277
|
-
...(args.memberId ? { memberId: args.memberId } : {}),
|
|
1278
|
-
});
|
|
1279
|
-
const startMs = Date.now();
|
|
1280
|
-
let resultStr = '';
|
|
1281
|
-
let isError = false;
|
|
1282
|
-
try {
|
|
1283
|
-
const result = await args.toolRegistry.invoke(tt.name, normalized, {
|
|
1284
|
-
cwd: args.projectRoot,
|
|
1285
|
-
sessionId: args.sessionId,
|
|
1286
|
-
});
|
|
1287
|
-
if (result.ok) {
|
|
1288
|
-
const val = result.value;
|
|
1289
|
-
if (tt.name === 'edit_file' && val.occurrencesReplaced === 0) {
|
|
1290
|
-
resultStr = `edit_file: no match for oldString (replay)`;
|
|
1291
|
-
isError = true;
|
|
1292
|
-
}
|
|
1293
|
-
else {
|
|
1294
|
-
resultStr =
|
|
1295
|
-
typeof result.value === 'string'
|
|
1296
|
-
? result.value
|
|
1297
|
-
: JSON.stringify(result.value, null, 2);
|
|
1298
|
-
applied += 1;
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
else {
|
|
1302
|
-
resultStr = result.error;
|
|
1303
|
-
isError = true;
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
catch (err) {
|
|
1307
|
-
resultStr = err instanceof Error ? err.message : String(err);
|
|
1308
|
-
isError = true;
|
|
1309
|
-
}
|
|
1310
|
-
yield createBrainEvent('tool_execution_end', args.sessionId, {
|
|
1311
|
-
toolCallId,
|
|
1312
|
-
result: resultStr,
|
|
1313
|
-
isError,
|
|
1314
|
-
durationMs: Date.now() - startMs,
|
|
1315
|
-
});
|
|
1316
|
-
}
|
|
1317
|
-
return applied;
|
|
1318
|
-
}
|
|
1319
|
-
/**
|
|
1320
|
-
* Increment 4 — bounded deterministic fix loop for the chairman. When the
|
|
1321
|
-
* micro-gate flagged motion violations in Lucifero's writes, force a scoped
|
|
1322
|
-
* fix turn (read_file + edit_file only), re-scan the changed files, and repeat
|
|
1323
|
-
* until clean or the attempt cap. Emits only the fix turn's own events; the
|
|
1324
|
-
* post-council verification reports the final PASS/FAIL to the user.
|
|
1325
|
-
*/
|
|
1326
|
-
export async function* runChairmanFixLoop(args) {
|
|
1327
|
-
const maxAttempts = args.maxAttempts ?? 3;
|
|
1328
|
-
const zelariRoot = `${args.projectRoot}/.zelari`;
|
|
1329
|
-
let current = Array.from(args.violations.values());
|
|
1330
|
-
let attempt = 0;
|
|
1331
|
-
while (current.length > 0 && attempt < maxAttempts) {
|
|
1332
|
-
attempt++;
|
|
1333
|
-
try {
|
|
1334
|
-
const fixGenerator = runRetryTurnForMember({
|
|
1335
|
-
agent: args.chairman,
|
|
1336
|
-
missingToolNames: ['read_file', 'edit_file'],
|
|
1337
|
-
minPerTool: { read_file: 2, edit_file: 8 },
|
|
1338
|
-
executableTools: args.executableNames,
|
|
1339
|
-
userMessage: args.userMessage,
|
|
1340
|
-
ragContext: args.config.ragContext,
|
|
1341
|
-
workspaceContext: args.config.workspaceContext,
|
|
1342
|
-
priorOutputs: args.agentOutputs,
|
|
1343
|
-
aiConfig: args.config.aiConfig,
|
|
1344
|
-
sessionId: args.sessionId,
|
|
1345
|
-
effectiveModel: args.effectiveModel,
|
|
1346
|
-
effectiveProvider: args.effectiveProvider,
|
|
1347
|
-
eventBus: args.config.eventBus,
|
|
1348
|
-
toolRegistry: args.config.tools,
|
|
1349
|
-
providerStream: args.config.providerStream,
|
|
1350
|
-
runMode: 'implementation',
|
|
1351
|
-
retryPrompt: buildMotionFixPrompt(current),
|
|
1352
|
-
languageModule: args.languageModule,
|
|
1353
|
-
});
|
|
1354
|
-
for await (const event of fixGenerator) {
|
|
1355
|
-
if (event.type === 'tool_execution_start')
|
|
1356
|
-
args.onToolCall?.();
|
|
1357
|
-
yield event;
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
catch (fixErr) {
|
|
1361
|
-
// eslint-disable-next-line no-console
|
|
1362
|
-
console.error(`[council] chairman fix pass ${attempt} failed:`, fixErr);
|
|
1363
|
-
break;
|
|
1364
|
-
}
|
|
1365
|
-
// Re-scan the changed target files for the next iteration / termination.
|
|
1366
|
-
const rescanned = new Map();
|
|
1367
|
-
for (const relPath of args.changedFiles) {
|
|
1368
|
-
for (const w of runChairmanMicroGate({ projectRoot: args.projectRoot, relPath, zelariRoot })) {
|
|
1369
|
-
rescanned.set(`${w.id}|${w.file}|${w.line ?? ''}`, w);
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
current = Array.from(rescanned.values());
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
/**
|
|
1376
|
-
* Pure helper: given the list of tool names a member emitted during its
|
|
1377
|
-
* turn, and a list of requirements, return whether all requirements are
|
|
1378
|
-
* met.
|
|
1379
|
-
*/
|
|
1380
|
-
export function checkMemberToolEmissions(_memberId, emittedToolNames, requirements) {
|
|
1381
|
-
if (requirements.length === 0) {
|
|
1382
|
-
return { ok: true, missing: [] };
|
|
1383
|
-
}
|
|
1384
|
-
// Tally emitted counts once for all requirements.
|
|
1385
|
-
const counts = new Map();
|
|
1386
|
-
for (const name of emittedToolNames) {
|
|
1387
|
-
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
1388
|
-
}
|
|
1389
|
-
const missing = [];
|
|
1390
|
-
for (const req of requirements) {
|
|
1391
|
-
const got = counts.get(req.name) ?? 0;
|
|
1392
|
-
if (got < req.min) {
|
|
1393
|
-
missing.push(`${req.name} (got ${got}, need >= ${req.min})`);
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
return { ok: missing.length === 0, missing };
|
|
1397
|
-
}
|
|
1398
|
-
/**
|
|
1399
|
-
* v0.7.8 — Per-member tool-emission requirement SETS for the design-phase
|
|
1400
|
-
* council run. The outer array is an OR of alternatives: the member's turn
|
|
1401
|
-
* is complete when ANY one set is fully satisfied. The FIRST set is the
|
|
1402
|
-
* preferred contract — its unmet requirements drive the warning message
|
|
1403
|
-
* and the forced-retry tool list.
|
|
1404
|
-
*
|
|
1405
|
-
* Nettuno has two ways to satisfy its contract:
|
|
1406
|
-
* 1. (preferred) ONE `createPlan` batch call — phases + nested tasks +
|
|
1407
|
-
* milestone in a single emission. Retry budget: 1 call, which
|
|
1408
|
-
* composer-2.5 handles reliably (same shape as the Minosse/Lucifero
|
|
1409
|
-
* retries that already work).
|
|
1410
|
-
* 2. (legacy) the itemized trio — kept so stronger models (e.g. Opus)
|
|
1411
|
-
* that emit createPhase/createTask/createMilestone directly are not
|
|
1412
|
-
* flagged or retried.
|
|
1413
|
-
*/
|
|
1414
|
-
export const DESIGN_PHASE_REQUIREMENT_SETS = {
|
|
1415
|
-
nettun: [
|
|
1416
|
-
[{ name: 'createPlan', min: 1 }],
|
|
1417
|
-
[
|
|
1418
|
-
{ name: 'createPhase', min: 3 },
|
|
1419
|
-
{ name: 'createTask', min: 6 },
|
|
1420
|
-
{ name: 'createMilestone', min: 1 },
|
|
1421
|
-
],
|
|
1422
|
-
],
|
|
1423
|
-
geryon: [
|
|
1424
|
-
[{ name: 'createDocument', min: 3 }],
|
|
1425
|
-
],
|
|
1426
|
-
pluton: [
|
|
1427
|
-
[{ name: 'createDocument', min: 1 }],
|
|
1428
|
-
],
|
|
1429
|
-
minos: [
|
|
1430
|
-
[{ name: 'createDocument', min: 1 }],
|
|
1431
|
-
],
|
|
1432
|
-
lucifer: [
|
|
1433
|
-
[{ name: 'createDocument', min: 1 }],
|
|
1434
|
-
],
|
|
1435
|
-
};
|
|
1436
|
-
/**
|
|
1437
|
-
* Preferred (first) requirement set per member — kept as the flat map the
|
|
1438
|
-
* council loops pass to `applyRetryIfMissing` for the retry budget. For
|
|
1439
|
-
* Nettuno this is `createPlan min 1`, so the forced retry advertises ONE
|
|
1440
|
-
* tool with a 1-call budget instead of the old 13+-call itemized contract.
|
|
1441
|
-
*/
|
|
1442
|
-
export const DESIGN_PHASE_REQUIREMENTS = Object.fromEntries(Object.entries(DESIGN_PHASE_REQUIREMENT_SETS).map(([id, sets]) => [id, sets[0]]));
|
|
1443
|
-
/**
|
|
1444
|
-
* Pure helper: OR-of-sets variant of {@link checkMemberToolEmissions}.
|
|
1445
|
-
* Returns ok when ANY set is fully satisfied. When none is, the missing
|
|
1446
|
-
* list reflects the FIRST (preferred) set so the warning and the retry
|
|
1447
|
-
* point the model at the cheapest way to comply.
|
|
1448
|
-
*/
|
|
1449
|
-
export function checkMemberToolEmissionSets(memberId, emittedToolNames, sets) {
|
|
1450
|
-
if (sets.length === 0) {
|
|
1451
|
-
return { ok: true, missing: [] };
|
|
1452
|
-
}
|
|
1453
|
-
const results = sets.map((set) => checkMemberToolEmissions(memberId, emittedToolNames, set));
|
|
1454
|
-
if (results.some((r) => r.ok)) {
|
|
1455
|
-
return { ok: true, missing: [] };
|
|
1456
|
-
}
|
|
1457
|
-
return results[0];
|
|
1458
|
-
}
|
|
1459
|
-
/**
|
|
1460
|
-
* Run the post-condition check for a member and emit a console.warn when
|
|
1461
|
-
* any required tool was not emitted the minimum number of times. Returns
|
|
1462
|
-
* the check result so callers can act on it (Pass 3 may add automatic
|
|
1463
|
-
* retry; for now we only warn).
|
|
1464
|
-
*/
|
|
1465
|
-
export function enforceDesignPhaseToolEmissions(memberId, emittedToolNames) {
|
|
1466
|
-
const sets = DESIGN_PHASE_REQUIREMENT_SETS[memberId];
|
|
1467
|
-
if (!sets || sets.length === 0) {
|
|
1468
|
-
return { ok: true, missing: [] };
|
|
1469
|
-
}
|
|
1470
|
-
const result = checkMemberToolEmissionSets(memberId, emittedToolNames, sets);
|
|
1471
|
-
if (!result.ok) {
|
|
1472
|
-
// eslint-disable-next-line no-console
|
|
1473
|
-
console.warn(`[council] member "${memberId}" did not emit required tools: ${result.missing.join(', ')}. ` +
|
|
1474
|
-
`The downstream .zelari/ deliverable may be incomplete. ` +
|
|
1475
|
-
`(A forced retry turn scoped to the missing tools follows; the deterministic ` +
|
|
1476
|
-
`complete-design fallback covers any remaining gap.)`);
|
|
1477
|
-
}
|
|
1478
|
-
return result;
|
|
1479
|
-
}
|
|
1480
|
-
/**
|
|
1481
|
-
* Maximum number of forced retry turns per council member. Cap of 1
|
|
1482
|
-
* keeps the worst-case council latency bounded (a single extra turn per
|
|
1483
|
-
* member × 4 design-phase members ≈ 30-60 s on top of the base run).
|
|
1484
|
-
* Going above 1 tends to produce hallucinated tool arguments because
|
|
1485
|
-
* the model has already spent its "tool budget" on exploration.
|
|
1486
|
-
*/
|
|
1487
|
-
export const MAX_RETRY_PER_MEMBER = 1;
|
|
1488
|
-
/**
|
|
1489
|
-
* Pure helper: should the council loop spin up one more forced turn for
|
|
1490
|
-
* this member to recover the missing tool emissions?
|
|
1491
|
-
*
|
|
1492
|
-
* Returns true when:
|
|
1493
|
-
* - at least one tool is still missing after the post-condition check, AND
|
|
1494
|
-
* - the retry budget for this member has not been exhausted.
|
|
1495
|
-
*
|
|
1496
|
-
* Returns false otherwise. Tested as a pure function so the council
|
|
1497
|
-
* loop can branch on the answer without coupling to AgentHarness.
|
|
1498
|
-
*/
|
|
1499
|
-
export function shouldRetryMember(missingToolNames, attemptsSoFar) {
|
|
1500
|
-
if (missingToolNames.length === 0)
|
|
1501
|
-
return false;
|
|
1502
|
-
if (attemptsSoFar >= MAX_RETRY_PER_MEMBER)
|
|
1503
|
-
return false;
|
|
1504
|
-
return true;
|
|
1505
|
-
}
|
|
1506
|
-
/**
|
|
1507
|
-
* Build the one-line prompt that the retry turn sends to the model.
|
|
1508
|
-
* The shape matters: the model is primed by its role prompt to produce
|
|
1509
|
-
* prose, so the retry prompt must be unambiguous, imperative, and
|
|
1510
|
-
* scoped to ONLY the missing tools.
|
|
1511
|
-
*
|
|
1512
|
-
* Format: "You did not emit: <names>. Call <names> NOW with concrete
|
|
1513
|
-
* arguments. No prose."
|
|
1514
|
-
*
|
|
1515
|
-
* Multiple tools are listed comma-separated in the same call so the
|
|
1516
|
-
* model can satisfy them in a single tool_calls turn (which is the
|
|
1517
|
-
* cheapest path through AgentHarness).
|
|
1518
|
-
*/
|
|
1519
|
-
export function buildRetryPrompt(missingToolNames) {
|
|
1520
|
-
const names = missingToolNames.join(', ');
|
|
1521
|
-
return `You did not emit the required workspace tools: ${names}. Call ${names} NOW with concrete arguments. No prose. No search.`;
|
|
1522
|
-
}
|
|
1523
|
-
// ── Forced retry turn (v0.7.7 Pass 3) ──────────────────────────────────────
|
|
1524
|
-
//
|
|
1525
|
-
// When the post-condition check fails for a member, the council loop can
|
|
1526
|
-
// spin up ONE more AgentHarness turn whose ONLY purpose is to force the
|
|
1527
|
-
// missing tool emissions. This is a structural fix for the failure mode
|
|
1528
|
-
// where the model terminates after exploration (`searchDocuments` × 2)
|
|
1529
|
-
// without persisting the required artifacts.
|
|
1530
|
-
//
|
|
1531
|
-
// The retry turn is intentionally minimal:
|
|
1532
|
-
// - System prompt: same as the original (via buildAgentMessages) so
|
|
1533
|
-
// the model still has its role contract.
|
|
1534
|
-
// - User message: the retry prompt (one line, imperative).
|
|
1535
|
-
// - Tools: ONLY the missing tools (filtered through filterExecutable
|
|
1536
|
-
// so we never advertise a tool the runtime cannot execute).
|
|
1537
|
-
// - maxToolCallsPerTurn: exactly the number of missing tools — the
|
|
1538
|
-
// model cannot explore again, it can only call what's missing.
|
|
1539
|
-
//
|
|
1540
|
-
// Returns the additional tool names emitted during the retry. The caller
|
|
1541
|
-
// is responsible for re-running checkMemberToolEmissions with the
|
|
1542
|
-
// union of original + retry emissions.
|
|
1543
|
-
export async function* runRetryTurnForMember(args) {
|
|
1544
|
-
// Filter the missing tools against what's actually executable in this
|
|
1545
|
-
// runtime. If a tool is missing from executableTools, the retry can't
|
|
1546
|
-
// emit it — log and skip.
|
|
1547
|
-
const executableMissing = args.executableTools
|
|
1548
|
-
? args.missingToolNames.filter((n) => args.executableTools.has(n))
|
|
1549
|
-
: args.missingToolNames;
|
|
1550
|
-
if (executableMissing.length === 0) {
|
|
1551
|
-
return [];
|
|
1552
|
-
}
|
|
1553
|
-
// Build the minimal tool set — only the missing tools.
|
|
1554
|
-
const retryToolNames = executableMissing;
|
|
1555
|
-
const retryToolSpecs = getProviderTools(retryToolNames).map((t) => ({
|
|
1556
|
-
name: t.function.name,
|
|
1557
|
-
description: t.function.description,
|
|
1558
|
-
parameters: t.function.parameters,
|
|
1559
|
-
}));
|
|
1560
|
-
// Build the messages: same system + role context as the original turn,
|
|
1561
|
-
// then the retry prompt as a user message appended at the end.
|
|
1562
|
-
const baseMessages = buildAgentMessages(args.agent, args.userMessage, args.ragContext, args.workspaceContext, args.priorOutputs, args.aiConfig, args.executableTools, args.runMode ?? 'implementation', args.languageModule);
|
|
1563
|
-
const retryMessages = [
|
|
1564
|
-
...baseMessages,
|
|
1565
|
-
{
|
|
1566
|
-
role: 'user',
|
|
1567
|
-
content: args.retryPrompt ?? buildRetryPrompt(executableMissing),
|
|
1568
|
-
},
|
|
1569
|
-
];
|
|
1570
|
-
// Budget the retry turn so the model can satisfy ALL minimums in a
|
|
1571
|
-
// single tool_calls turn. For createDocument min:1 this is 1; for
|
|
1572
|
-
// createTask min:12 this is 12. Falls back to `missingToolNames.length`
|
|
1573
|
-
// when no per-tool min map is provided.
|
|
1574
|
-
const maxToolCalls = args.minPerTool !== undefined
|
|
1575
|
-
? Object.entries(args.minPerTool)
|
|
1576
|
-
.filter(([name]) => executableMissing.includes(name))
|
|
1577
|
-
.reduce((sum, [, min]) => sum + min, 0)
|
|
1578
|
-
: retryToolNames.length;
|
|
1579
|
-
const retryHarness = new AgentHarness({
|
|
1580
|
-
model: args.effectiveModel,
|
|
1581
|
-
provider: args.effectiveProvider,
|
|
1582
|
-
sessionId: args.sessionId,
|
|
1583
|
-
messages: retryMessages,
|
|
1584
|
-
tools: retryToolSpecs,
|
|
1585
|
-
eventBus: args.eventBus,
|
|
1586
|
-
toolRegistry: args.toolRegistry,
|
|
1587
|
-
// Budget the retry so the model can satisfy every requirement in
|
|
1588
|
-
// ONE tool_calls turn. For createTask min:12 this needs 12 calls.
|
|
1589
|
-
maxToolCallsPerTurn: maxToolCalls,
|
|
1590
|
-
memberId: args.agent.id,
|
|
1591
|
-
memberName: args.agent.name,
|
|
1592
|
-
providerStream: (params) => args.providerStream(params),
|
|
1593
|
-
});
|
|
1594
|
-
const retryEmitted = [];
|
|
1595
|
-
for await (const event of retryHarness.run()) {
|
|
1596
|
-
if (event.type === 'tool_execution_start') {
|
|
1597
|
-
retryEmitted.push(event.toolName);
|
|
1598
|
-
}
|
|
1599
|
-
yield event;
|
|
1600
|
-
}
|
|
1601
|
-
return retryEmitted;
|
|
1602
|
-
}
|
|
1603
|
-
/**
|
|
1604
|
-
* Shared retry orchestrator used by specialist, oracle, and chairman
|
|
1605
|
-
* loops. Given the post-condition check result, decides whether to
|
|
1606
|
-
* spin up a forced retry turn, and if so yields the events from that
|
|
1607
|
-
* turn back to the caller (so the UI sees them) while mutating the
|
|
1608
|
-
* shared `emittedToolNames` array (so the next post-condition check
|
|
1609
|
-
* sees the union of original + retry emissions).
|
|
1610
|
-
*
|
|
1611
|
-
* The retry turn is skipped when:
|
|
1612
|
-
* - the check passed (no missing tools), OR
|
|
1613
|
-
* - the retry budget for this member is exhausted (shouldRetryMember).
|
|
1614
|
-
*
|
|
1615
|
-
* On retry failure (network error, model error, etc.) the function logs
|
|
1616
|
-
* the error and continues — it never throws. The next post-condition
|
|
1617
|
-
* check will simply re-warn.
|
|
1618
|
-
*/
|
|
1619
|
-
export async function* applyRetryIfMissing(args) {
|
|
1620
|
-
if (args.check.ok)
|
|
1621
|
-
return;
|
|
1622
|
-
const missingToolNames = args.check.missing.map((m) => m.split(' ')[0]);
|
|
1623
|
-
if (!shouldRetryMember(missingToolNames, 0))
|
|
1624
|
-
return;
|
|
1625
|
-
// eslint-disable-next-line no-console
|
|
1626
|
-
console.warn(`[council] ${args.agent.id} retrying missing tools: ${missingToolNames.join(', ')}`);
|
|
1627
|
-
// Build the minPerTool map from the original requirements so the
|
|
1628
|
-
// retry turn budgets enough tool calls to satisfy every minimum.
|
|
1629
|
-
// Without this, a createTask min:12 requirement would be capped at
|
|
1630
|
-
// 1 call (the number of distinct missing tools).
|
|
1631
|
-
const minPerTool = {};
|
|
1632
|
-
if (args.requirements) {
|
|
1633
|
-
for (const req of args.requirements) {
|
|
1634
|
-
if (missingToolNames.includes(req.name)) {
|
|
1635
|
-
minPerTool[req.name] = req.min;
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
try {
|
|
1640
|
-
const retryGenerator = runRetryTurnForMember({
|
|
1641
|
-
agent: args.agent,
|
|
1642
|
-
missingToolNames,
|
|
1643
|
-
minPerTool,
|
|
1644
|
-
executableTools: args.executableNames,
|
|
1645
|
-
userMessage: args.userMessage,
|
|
1646
|
-
ragContext: args.config.ragContext,
|
|
1647
|
-
workspaceContext: args.config.workspaceContext,
|
|
1648
|
-
priorOutputs: args.agentOutputs,
|
|
1649
|
-
aiConfig: args.config.aiConfig,
|
|
1650
|
-
sessionId: args.sessionId,
|
|
1651
|
-
effectiveModel: args.effectiveModel,
|
|
1652
|
-
effectiveProvider: args.effectiveProvider,
|
|
1653
|
-
eventBus: args.config.eventBus,
|
|
1654
|
-
toolRegistry: args.config.tools,
|
|
1655
|
-
providerStream: args.config.providerStream,
|
|
1656
|
-
runMode: args.config.runMode,
|
|
1657
|
-
languageModule: args.languageModule,
|
|
1658
|
-
});
|
|
1659
|
-
for await (const event of retryGenerator) {
|
|
1660
|
-
if (event.type === 'tool_execution_start') {
|
|
1661
|
-
args.onToolCall();
|
|
1662
|
-
args.emittedToolNames.push(event.toolName);
|
|
1663
|
-
}
|
|
1664
|
-
yield event;
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
catch (retryErr) {
|
|
1668
|
-
// eslint-disable-next-line no-console
|
|
1669
|
-
console.error(`[council] ${args.agent.id} retry failed:`, retryErr);
|
|
1670
|
-
}
|
|
1671
|
-
// Re-run the check so the final warning reflects the union of
|
|
1672
|
-
// original + retry emissions.
|
|
1673
|
-
enforceDesignPhaseToolEmissions(args.agent.id, args.emittedToolNames);
|
|
1674
|
-
}
|
|
793
|
+
import { applyImplementationWriteRetry, runChairmanDeliveryLoop } from './council/chairmanDelivery.js';
|
|
794
|
+
export { applyCompletionRetry, applyImplementationWriteRetry, buildMotionFixPrompt, MAX_DELIVERY_ATTEMPTS, runChairmanDeliveryLoop } from './council/chairmanDelivery.js';
|
|
795
|
+
import { replayChairmanTextTools, runChairmanFixLoop } from './council/chairmanFixLoop.js';
|
|
796
|
+
export { replayChairmanTextTools, runChairmanFixLoop } from './council/chairmanFixLoop.js';
|
|
797
|
+
import { DESIGN_PHASE_REQUIREMENTS, enforceDesignPhaseToolEmissions } from './council/toolEmission.js';
|
|
798
|
+
export { checkMemberToolEmissionSets, checkMemberToolEmissions, DESIGN_PHASE_REQUIREMENTS, DESIGN_PHASE_REQUIREMENT_SETS, enforceDesignPhaseToolEmissions } from './council/toolEmission.js';
|
|
799
|
+
import { applyRetryIfMissing } from './council/retryTurn.js';
|
|
800
|
+
export { applyRetryIfMissing, buildRetryPrompt, MAX_RETRY_PER_MEMBER, runRetryTurnForMember, shouldRetryMember } from './council/retryTurn.js';
|
|
1675
801
|
//# sourceMappingURL=councilApi.js.map
|