@zelari/core 0.7.9
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/LICENSE +21 -0
- package/dist/agents/advancedTools.d.ts +7 -0
- package/dist/agents/advancedTools.d.ts.map +1 -0
- package/dist/agents/advancedTools.js +115 -0
- package/dist/agents/advancedTools.js.map +1 -0
- package/dist/agents/councilApi.d.ts +328 -0
- package/dist/agents/councilApi.d.ts.map +1 -0
- package/dist/agents/councilApi.js +952 -0
- package/dist/agents/councilApi.js.map +1 -0
- package/dist/agents/councilDirectives.d.ts +26 -0
- package/dist/agents/councilDirectives.d.ts.map +1 -0
- package/dist/agents/councilDirectives.js +98 -0
- package/dist/agents/councilDirectives.js.map +1 -0
- package/dist/agents/harnessToolBridge.d.ts +26 -0
- package/dist/agents/harnessToolBridge.d.ts.map +1 -0
- package/dist/agents/harnessToolBridge.js +60 -0
- package/dist/agents/harnessToolBridge.js.map +1 -0
- package/dist/agents/promoteMember.d.ts +77 -0
- package/dist/agents/promoteMember.d.ts.map +1 -0
- package/dist/agents/promoteMember.js +210 -0
- package/dist/agents/promoteMember.js.map +1 -0
- package/dist/agents/promptModules.d.ts +20 -0
- package/dist/agents/promptModules.d.ts.map +1 -0
- package/dist/agents/promptModules.js +139 -0
- package/dist/agents/promptModules.js.map +1 -0
- package/dist/agents/roles.d.ts +40 -0
- package/dist/agents/roles.d.ts.map +1 -0
- package/dist/agents/roles.js +352 -0
- package/dist/agents/roles.js.map +1 -0
- package/dist/agents/skills/builtin/debugging.d.ts +2 -0
- package/dist/agents/skills/builtin/debugging.d.ts.map +1 -0
- package/dist/agents/skills/builtin/debugging.js +248 -0
- package/dist/agents/skills/builtin/debugging.js.map +1 -0
- package/dist/agents/skills/builtin/docs.d.ts +2 -0
- package/dist/agents/skills/builtin/docs.d.ts.map +1 -0
- package/dist/agents/skills/builtin/docs.js +291 -0
- package/dist/agents/skills/builtin/docs.js.map +1 -0
- package/dist/agents/skills/builtin/git-ops.d.ts +2 -0
- package/dist/agents/skills/builtin/git-ops.d.ts.map +1 -0
- package/dist/agents/skills/builtin/git-ops.js +301 -0
- package/dist/agents/skills/builtin/git-ops.js.map +1 -0
- package/dist/agents/skills/builtin/planning.d.ts +2 -0
- package/dist/agents/skills/builtin/planning.d.ts.map +1 -0
- package/dist/agents/skills/builtin/planning.js +335 -0
- package/dist/agents/skills/builtin/planning.js.map +1 -0
- package/dist/agents/skills/builtin/refactoring.d.ts +2 -0
- package/dist/agents/skills/builtin/refactoring.d.ts.map +1 -0
- package/dist/agents/skills/builtin/refactoring.js +261 -0
- package/dist/agents/skills/builtin/refactoring.js.map +1 -0
- package/dist/agents/skills/builtin/review.d.ts +2 -0
- package/dist/agents/skills/builtin/review.d.ts.map +1 -0
- package/dist/agents/skills/builtin/review.js +302 -0
- package/dist/agents/skills/builtin/review.js.map +1 -0
- package/dist/agents/skills/builtin/testing.d.ts +2 -0
- package/dist/agents/skills/builtin/testing.d.ts.map +1 -0
- package/dist/agents/skills/builtin/testing.js +245 -0
- package/dist/agents/skills/builtin/testing.js.map +1 -0
- package/dist/agents/skills.d.ts +138 -0
- package/dist/agents/skills.d.ts.map +1 -0
- package/dist/agents/skills.js +277 -0
- package/dist/agents/skills.js.map +1 -0
- package/dist/agents/systemPromptBuilder.d.ts +45 -0
- package/dist/agents/systemPromptBuilder.d.ts.map +1 -0
- package/dist/agents/systemPromptBuilder.js +159 -0
- package/dist/agents/systemPromptBuilder.js.map +1 -0
- package/dist/agents/toolSchemas.d.ts +55 -0
- package/dist/agents/toolSchemas.d.ts.map +1 -0
- package/dist/agents/toolSchemas.js +316 -0
- package/dist/agents/toolSchemas.js.map +1 -0
- package/dist/agents/tools.d.ts +67 -0
- package/dist/agents/tools.d.ts.map +1 -0
- package/dist/agents/tools.js +225 -0
- package/dist/agents/tools.js.map +1 -0
- package/dist/agents/vaultTools.d.ts +9 -0
- package/dist/agents/vaultTools.d.ts.map +1 -0
- package/dist/agents/vaultTools.js +134 -0
- package/dist/agents/vaultTools.js.map +1 -0
- package/dist/core/AgentHarness.d.ts +250 -0
- package/dist/core/AgentHarness.d.ts.map +1 -0
- package/dist/core/AgentHarness.js +614 -0
- package/dist/core/AgentHarness.js.map +1 -0
- package/dist/core/providerStream.d.ts +61 -0
- package/dist/core/providerStream.d.ts.map +1 -0
- package/dist/core/providerStream.js +99 -0
- package/dist/core/providerStream.js.map +1 -0
- package/dist/core/sessionJsonl.d.ts +50 -0
- package/dist/core/sessionJsonl.d.ts.map +1 -0
- package/dist/core/sessionJsonl.js +99 -0
- package/dist/core/sessionJsonl.js.map +1 -0
- package/dist/core/tools/auditLog.d.ts +14 -0
- package/dist/core/tools/auditLog.d.ts.map +1 -0
- package/dist/core/tools/auditLog.js +26 -0
- package/dist/core/tools/auditLog.js.map +1 -0
- package/dist/core/tools/builtin/_walk.d.ts +40 -0
- package/dist/core/tools/builtin/_walk.d.ts.map +1 -0
- package/dist/core/tools/builtin/_walk.js +129 -0
- package/dist/core/tools/builtin/_walk.js.map +1 -0
- package/dist/core/tools/builtin/diff.d.ts +45 -0
- package/dist/core/tools/builtin/diff.d.ts.map +1 -0
- package/dist/core/tools/builtin/diff.js +477 -0
- package/dist/core/tools/builtin/diff.js.map +1 -0
- package/dist/core/tools/builtin/filesystem.d.ts +45 -0
- package/dist/core/tools/builtin/filesystem.d.ts.map +1 -0
- package/dist/core/tools/builtin/filesystem.js +110 -0
- package/dist/core/tools/builtin/filesystem.js.map +1 -0
- package/dist/core/tools/builtin/listFiles.d.ts +18 -0
- package/dist/core/tools/builtin/listFiles.d.ts.map +1 -0
- package/dist/core/tools/builtin/listFiles.js +48 -0
- package/dist/core/tools/builtin/listFiles.js.map +1 -0
- package/dist/core/tools/builtin/search.d.ts +49 -0
- package/dist/core/tools/builtin/search.d.ts.map +1 -0
- package/dist/core/tools/builtin/search.js +148 -0
- package/dist/core/tools/builtin/search.js.map +1 -0
- package/dist/core/tools/builtin/shell.d.ts +27 -0
- package/dist/core/tools/builtin/shell.d.ts.map +1 -0
- package/dist/core/tools/builtin/shell.js +98 -0
- package/dist/core/tools/builtin/shell.js.map +1 -0
- package/dist/core/tools/builtin/shellResolver.d.ts +25 -0
- package/dist/core/tools/builtin/shellResolver.d.ts.map +1 -0
- package/dist/core/tools/builtin/shellResolver.js +117 -0
- package/dist/core/tools/builtin/shellResolver.js.map +1 -0
- package/dist/core/tools/builtin/web.d.ts +43 -0
- package/dist/core/tools/builtin/web.d.ts.map +1 -0
- package/dist/core/tools/builtin/web.js +208 -0
- package/dist/core/tools/builtin/web.js.map +1 -0
- package/dist/core/tools/registry.d.ts +31 -0
- package/dist/core/tools/registry.d.ts.map +1 -0
- package/dist/core/tools/registry.js +115 -0
- package/dist/core/tools/registry.js.map +1 -0
- package/dist/core/tools/toolTypes.d.ts +60 -0
- package/dist/core/tools/toolTypes.d.ts.map +1 -0
- package/dist/core/tools/toolTypes.js +8 -0
- package/dist/core/tools/toolTypes.js.map +1 -0
- package/dist/core/tools/zodBridge.d.ts +6 -0
- package/dist/core/tools/zodBridge.d.ts.map +1 -0
- package/dist/core/tools/zodBridge.js +56 -0
- package/dist/core/tools/zodBridge.js.map +1 -0
- package/dist/council/index.d.ts +14 -0
- package/dist/council/index.d.ts.map +1 -0
- package/dist/council/index.js +14 -0
- package/dist/council/index.js.map +1 -0
- package/dist/council/modeBanners.d.ts +5 -0
- package/dist/council/modeBanners.d.ts.map +1 -0
- package/dist/council/modeBanners.js +10 -0
- package/dist/council/modeBanners.js.map +1 -0
- package/dist/council/runMode.d.ts +25 -0
- package/dist/council/runMode.d.ts.map +1 -0
- package/dist/council/runMode.js +41 -0
- package/dist/council/runMode.js.map +1 -0
- package/dist/events/index.d.ts +6 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +6 -0
- package/dist/events/index.js.map +1 -0
- package/dist/harness/index.d.ts +9 -0
- package/dist/harness/index.d.ts.map +1 -0
- package/dist/harness/index.js +9 -0
- package/dist/harness/index.js.map +1 -0
- package/dist/harness/tools/index.d.ts +20 -0
- package/dist/harness/tools/index.d.ts.map +1 -0
- package/dist/harness/tools/index.js +19 -0
- package/dist/harness/tools/index.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/shared/eventBus.d.ts +58 -0
- package/dist/shared/eventBus.d.ts.map +1 -0
- package/dist/shared/eventBus.js +104 -0
- package/dist/shared/eventBus.js.map +1 -0
- package/dist/shared/events.d.ts +230 -0
- package/dist/shared/events.d.ts.map +1 -0
- package/dist/shared/events.js +76 -0
- package/dist/shared/events.js.map +1 -0
- package/dist/shared/index.d.ts +8 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/index.js +8 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/skills/index.d.ts +10 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +10 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/types/context.d.ts +54 -0
- package/dist/types/context.d.ts.map +1 -0
- package/dist/types/context.js +3 -0
- package/dist/types/context.js.map +1 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +12 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/knowledge.d.ts +79 -0
- package/dist/types/knowledge.d.ts.map +1 -0
- package/dist/types/knowledge.js +5 -0
- package/dist/types/knowledge.js.map +1 -0
- package/dist/types/legacy.d.ts +466 -0
- package/dist/types/legacy.d.ts.map +1 -0
- package/dist/types/legacy.js +2 -0
- package/dist/types/legacy.js.map +1 -0
- package/dist/types/systemTypes.d.ts +113 -0
- package/dist/types/systemTypes.d.ts.map +1 -0
- package/dist/types/systemTypes.js +10 -0
- package/dist/types/systemTypes.js.map +1 -0
- package/package.json +151 -0
|
@@ -0,0 +1,952 @@
|
|
|
1
|
+
import { getCouncilAgents, swapMembers } from './roles.js';
|
|
2
|
+
import { getProviderTools } from './toolSchemas.js';
|
|
3
|
+
import { buildSystemPrompt, computeAgentTools } from './systemPromptBuilder.js';
|
|
4
|
+
import { getAllTools } from './tools.js';
|
|
5
|
+
import { createBrainEvent } from '../shared/events.js';
|
|
6
|
+
import { AgentHarness } from '../core/AgentHarness.js';
|
|
7
|
+
import { councilModeBanner } from '../council/modeBanners.js';
|
|
8
|
+
import { councilTierFromSize } from '../council/runMode.js';
|
|
9
|
+
/**
|
|
10
|
+
* Council members whose tool-emission retry is DISABLED.
|
|
11
|
+
*
|
|
12
|
+
* v0.7.7 Opzione B put 'nettun' here because its contract (12 createTask
|
|
13
|
+
* + 1 createMilestone = 13+ sequential calls) exceeded what composer-2.5
|
|
14
|
+
* could persist in the 240s budget, making the retry a pure waste.
|
|
15
|
+
*
|
|
16
|
+
* v0.7.8 removes 'nettun': the plan contract is now satisfiable with a
|
|
17
|
+
* SINGLE `createPlan` batch call (phases + nested tasks + milestone in
|
|
18
|
+
* one emission), so the forced retry has the same 1-call budget that
|
|
19
|
+
* already works reliably for Minosse and Lucifero. The set stays
|
|
20
|
+
* exported as the opt-out mechanism for future members.
|
|
21
|
+
*/
|
|
22
|
+
export const NON_RETRY_AGENTS = new Set([]);
|
|
23
|
+
const QUESTION_MARKER = '---QUESTION---';
|
|
24
|
+
const QUESTION_END_MARKER = '---END---';
|
|
25
|
+
export function parseClarificationRequest(text) {
|
|
26
|
+
const start = text.indexOf(QUESTION_MARKER);
|
|
27
|
+
if (start < 0)
|
|
28
|
+
return null;
|
|
29
|
+
const rest = text.slice(start + QUESTION_MARKER.length);
|
|
30
|
+
const end = rest.indexOf(QUESTION_END_MARKER);
|
|
31
|
+
const block = end >= 0 ? rest.slice(0, end) : rest;
|
|
32
|
+
const cleaned = block.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
|
|
33
|
+
const objStart = cleaned.indexOf('{');
|
|
34
|
+
const objEnd = cleaned.lastIndexOf('}');
|
|
35
|
+
const jsonText = objStart >= 0 && objEnd > objStart ? cleaned.slice(objStart, objEnd + 1) : cleaned;
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(jsonText);
|
|
38
|
+
if (typeof parsed.question !== 'string' || !parsed.question.trim())
|
|
39
|
+
return null;
|
|
40
|
+
return {
|
|
41
|
+
question: parsed.question.trim(),
|
|
42
|
+
choices: Array.isArray(parsed.choices)
|
|
43
|
+
? parsed.choices.filter((c) => typeof c === 'string' && c.trim().length > 0).map((c) => c.trim())
|
|
44
|
+
: undefined,
|
|
45
|
+
context: typeof parsed.context === 'string' ? parsed.context.trim() : undefined,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function parseThinking(text) {
|
|
53
|
+
const match = text.match(/<think>([\s\S]*?)<\/think>/);
|
|
54
|
+
return match ? match[1].trim() : '';
|
|
55
|
+
}
|
|
56
|
+
export function cleanAgentContent(text) {
|
|
57
|
+
return text
|
|
58
|
+
.replace(/<think>[\s\S]*?<\/think>/g, '')
|
|
59
|
+
.replace(/<minimax:tool_call>[\s\S]*?<\/minimax:tool_call>/g, '')
|
|
60
|
+
.replace(/---QUESTION---[\s\S]*?---END---/g, '')
|
|
61
|
+
.trim();
|
|
62
|
+
}
|
|
63
|
+
function buildAgentMessages(agent, userMessage, ragContext, workspaceContext, priorOutputs, aiConfig, executableTools, runMode = 'implementation') {
|
|
64
|
+
// v0.7.5: the AVAILABLE TOOLS prompt block must match the schemas the
|
|
65
|
+
// harness actually advertises. The v0.7.3 fix filtered the schemas
|
|
66
|
+
// (filterExecutable) but NOT this prompt text, so members still read
|
|
67
|
+
// "searchRAG: search the knowledge base…" in their system prompt and
|
|
68
|
+
// called it — every call a guaranteed "Tool not found" (live test
|
|
69
|
+
// 2026-07-03, /council in Z:\EasyPeasy\test).
|
|
70
|
+
const allToolNames = computeAgentTools(agent, aiConfig);
|
|
71
|
+
const toolNames = executableTools
|
|
72
|
+
? allToolNames.filter((n) => executableTools.has(n))
|
|
73
|
+
: allToolNames;
|
|
74
|
+
const enhancedSystemPrompt = buildSystemPrompt(agent, {
|
|
75
|
+
tools: getAllTools(),
|
|
76
|
+
toolNames,
|
|
77
|
+
aiConfig,
|
|
78
|
+
workspaceContext,
|
|
79
|
+
ragContext,
|
|
80
|
+
});
|
|
81
|
+
const messages = [
|
|
82
|
+
{ role: 'system', content: enhancedSystemPrompt },
|
|
83
|
+
{ role: 'system', content: councilModeBanner(runMode) },
|
|
84
|
+
{ 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.' },
|
|
85
|
+
];
|
|
86
|
+
if (ragContext) {
|
|
87
|
+
messages.push({ role: 'system', content: `Relevant workspace context (from RAG retrieval):\n${ragContext}` });
|
|
88
|
+
}
|
|
89
|
+
if (workspaceContext) {
|
|
90
|
+
messages.push({ role: 'system', content: `Current workspace state:\n${workspaceContext}` });
|
|
91
|
+
}
|
|
92
|
+
if (priorOutputs.length > 0) {
|
|
93
|
+
const summary = priorOutputs.map((o) => `[${o.name} - ${o.role}]: ${o.content}`).join('\n\n');
|
|
94
|
+
messages.push({ role: 'user', content: `Previous council members have said:\n${summary}\n\nOriginal user request: ${userMessage}` });
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
messages.push({ role: 'user', content: userMessage });
|
|
98
|
+
}
|
|
99
|
+
return messages;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* PURE council orchestration. Loops through specialists, optionally runs
|
|
103
|
+
* the oracle (debate mode), then runs the chairman synthesis.
|
|
104
|
+
*
|
|
105
|
+
* Each agent invocation creates an AgentHarness internally that consumes
|
|
106
|
+
* the providerStream and emits BrainEvents. The orchestrator aggregates
|
|
107
|
+
* text deltas into a single message per agent.
|
|
108
|
+
*
|
|
109
|
+
* Returns AsyncIterable<BrainEvent> for the full council run.
|
|
110
|
+
*/
|
|
111
|
+
export async function* runCouncilPure(userMessage, config, callbacks = {}) {
|
|
112
|
+
const baseAgents = getCouncilAgents(config.councilSize);
|
|
113
|
+
// Apply optional member swap (Task I.3 close-out). Throws UnknownMemberError
|
|
114
|
+
// on typo in either side of the mapping — caller decides whether to catch
|
|
115
|
+
// or surface. When `memberSwap` is undefined/empty, swapMembers returns a
|
|
116
|
+
// shallow copy (no-op in effect).
|
|
117
|
+
const agents = swapMembers(baseAgents, config.memberSwap ?? {});
|
|
118
|
+
const messages = [];
|
|
119
|
+
const completedIds = new Set(config.completedAgentIds ?? []);
|
|
120
|
+
const agentOutputs = [
|
|
121
|
+
...(config.existingOutputs ?? []),
|
|
122
|
+
];
|
|
123
|
+
const sessionId = config.sessionId ?? crypto.randomUUID();
|
|
124
|
+
const runMode = config.runMode ?? 'implementation';
|
|
125
|
+
const isDesignPhase = runMode === 'design-phase';
|
|
126
|
+
yield createBrainEvent('council_mode', sessionId, {
|
|
127
|
+
tier: councilTierFromSize(config.councilSize),
|
|
128
|
+
councilSize: config.councilSize,
|
|
129
|
+
runMode,
|
|
130
|
+
});
|
|
131
|
+
// Emit council start
|
|
132
|
+
yield {
|
|
133
|
+
type: 'agent_start',
|
|
134
|
+
id: crypto.randomUUID(),
|
|
135
|
+
ts: Date.now(),
|
|
136
|
+
sessionId,
|
|
137
|
+
model: config.model,
|
|
138
|
+
provider: config.provider ?? 'minimax',
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Build a MemberCost payload and dispatch it via the callback + the
|
|
142
|
+
* event stream. Used by the I.1 per-member cost tracking — fires
|
|
143
|
+
* once per member (specialist, oracle, chairman) at the end of its run.
|
|
144
|
+
*/
|
|
145
|
+
const emitMemberCost = (input) => {
|
|
146
|
+
const usage = input.usage;
|
|
147
|
+
const prompt = usage?.promptTokens ?? 0;
|
|
148
|
+
const completion = usage?.completionTokens ?? 0;
|
|
149
|
+
const cost = {
|
|
150
|
+
memberId: input.memberId,
|
|
151
|
+
name: input.name,
|
|
152
|
+
promptTokens: prompt,
|
|
153
|
+
completionTokens: completion,
|
|
154
|
+
totalTokens: usage?.totalTokens ?? prompt + completion,
|
|
155
|
+
durationMs: Math.max(0, Math.round(input.durationMs)),
|
|
156
|
+
toolCalls: input.toolCalls,
|
|
157
|
+
errored: input.errored,
|
|
158
|
+
};
|
|
159
|
+
callbacks.onMemberCost?.(cost);
|
|
160
|
+
};
|
|
161
|
+
// Exposed to the harness loop below via closure — `yield` inside an
|
|
162
|
+
// async generator can't happen from a non-async helper, so the actual
|
|
163
|
+
// `member_cost` event is yielded inline at each call site (see the
|
|
164
|
+
// specialist / oracle / chairman loops). The helper only owns the
|
|
165
|
+
// callback dispatch to keep call sites short.
|
|
166
|
+
// v0.7.3: only advertise tools the executor registry can actually run.
|
|
167
|
+
// computeAgentTools unions role tools + skill requiredTools, which still
|
|
168
|
+
// include Electron-era tools (searchRAG, buildMindMap, addNode, …) the CLI
|
|
169
|
+
// registry does not implement. Advertising them makes the model call tools
|
|
170
|
+
// that fail with `Tool "searchRAG" not found` (live-test 2026-07-02) and
|
|
171
|
+
// burns its per-turn tool budget on guaranteed failures.
|
|
172
|
+
//
|
|
173
|
+
// v0.7.5 Bug B fix: union the role's tool list with the executor's tool
|
|
174
|
+
// list BEFORE filtering. When the executor is a workspace-only registry
|
|
175
|
+
// (e.g. createWorkspaceToolRegistry from dispatchCouncil), role.tools like
|
|
176
|
+
// list_files/read_file/grep_content are NOT in the executor — without the
|
|
177
|
+
// union, filterExecutable strips everything and the model sees an empty
|
|
178
|
+
// AVAILABLE TOOLS block. The harness still gates execution via the
|
|
179
|
+
// ToolRegistry.invoke call (AgentHarness.ts:539), so we never advertise
|
|
180
|
+
// a tool that the executor can't actually run.
|
|
181
|
+
const executorToolNames = config.tools ? config.tools.list() : [];
|
|
182
|
+
const executableNames = config.tools ? new Set(executorToolNames) : null;
|
|
183
|
+
const filterExecutable = (names) => {
|
|
184
|
+
if (!executableNames)
|
|
185
|
+
return names;
|
|
186
|
+
const merged = Array.from(new Set([...names, ...executorToolNames]));
|
|
187
|
+
return merged.filter((n) => executableNames.has(n));
|
|
188
|
+
};
|
|
189
|
+
// Apply optional feedback-driven specialist ordering (Task I.2 close-out).
|
|
190
|
+
// Minosse and chairman are extracted BEFORE ranking so their positions are
|
|
191
|
+
// fixed (debate review + final synthesis roles are not reorderable).
|
|
192
|
+
const allSpecialists = agents.filter((a) => a.id !== 'lucifer' && a.id !== 'minos');
|
|
193
|
+
const specialists = config.feedbackStore
|
|
194
|
+
? config.feedbackStore.ranked(allSpecialists)
|
|
195
|
+
: allSpecialists;
|
|
196
|
+
const oracle = agents.find((a) => a.id === 'minos');
|
|
197
|
+
const chairman = agents.find((a) => a.id === 'lucifer');
|
|
198
|
+
for (const agent of specialists) {
|
|
199
|
+
if (completedIds.has(agent.id))
|
|
200
|
+
continue;
|
|
201
|
+
callbacks.onAgentStart?.(agent);
|
|
202
|
+
const override = config.agentModels?.[agent.id];
|
|
203
|
+
const effectiveProvider = override?.providerId ?? config.provider ?? 'minimax';
|
|
204
|
+
const effectiveModel = override?.model ?? config.model;
|
|
205
|
+
const agentToolNames = filterExecutable(computeAgentTools(agent, config.aiConfig));
|
|
206
|
+
const agentTools = agentToolNames.length > 0
|
|
207
|
+
? getProviderTools(agentToolNames).map((t) => ({
|
|
208
|
+
name: t.function.name,
|
|
209
|
+
description: t.function.description,
|
|
210
|
+
parameters: t.function.parameters,
|
|
211
|
+
}))
|
|
212
|
+
: [];
|
|
213
|
+
const harness = new AgentHarness({
|
|
214
|
+
model: effectiveModel,
|
|
215
|
+
provider: effectiveProvider,
|
|
216
|
+
sessionId,
|
|
217
|
+
messages: buildAgentMessages(agent, userMessage, config.ragContext, config.workspaceContext, agentOutputs, config.aiConfig, executableNames, runMode),
|
|
218
|
+
tools: agentTools,
|
|
219
|
+
eventBus: config.eventBus,
|
|
220
|
+
toolRegistry: config.tools,
|
|
221
|
+
// Task G.2 — enforce per-turn tool-call limit (carryover from v3-C C.1.5).
|
|
222
|
+
// Council members can otherwise fire N tool calls in one turn and blow
|
|
223
|
+
// the message context. Default to 5 if not set by caller.
|
|
224
|
+
maxToolCallsPerTurn: config.maxToolCallsPerTurn ?? 5,
|
|
225
|
+
// Visible-reasoning wiring (v0.5.0): stamp every event the
|
|
226
|
+
// harness emits with the council-member identity so the UI can
|
|
227
|
+
// render "Caronte: …" headers above the streamed text.
|
|
228
|
+
memberId: agent.id,
|
|
229
|
+
memberName: agent.name,
|
|
230
|
+
providerStream: (params) => config.providerStream({
|
|
231
|
+
...params,
|
|
232
|
+
}),
|
|
233
|
+
});
|
|
234
|
+
let fullText = '';
|
|
235
|
+
let toolCalls = 0;
|
|
236
|
+
let usage = null;
|
|
237
|
+
let errored = false;
|
|
238
|
+
// v0.7.6: per-member tool emission tracking for the post-condition
|
|
239
|
+
// check below. Each entry is the toolName of a tool_execution_start
|
|
240
|
+
// event from this member's turn.
|
|
241
|
+
const emittedToolNames = [];
|
|
242
|
+
const memberStart = Date.now();
|
|
243
|
+
try {
|
|
244
|
+
for await (const event of harness.run()) {
|
|
245
|
+
yield event;
|
|
246
|
+
if (event.type === 'tool_execution_start') {
|
|
247
|
+
toolCalls += 1;
|
|
248
|
+
emittedToolNames.push(event.toolName);
|
|
249
|
+
}
|
|
250
|
+
if (event.type === 'message_end' && event.usage) {
|
|
251
|
+
usage = event.usage;
|
|
252
|
+
}
|
|
253
|
+
if (event.type === 'message_delta') {
|
|
254
|
+
fullText += event.delta;
|
|
255
|
+
callbacks.onAgentChunk?.(agent, event.delta);
|
|
256
|
+
}
|
|
257
|
+
// AgentHarness catches provider errors internally and re-emits
|
|
258
|
+
// them as BrainErrorEvent. Without this check, a streaming
|
|
259
|
+
// failure would silently leave `errored=false` and the partial
|
|
260
|
+
// output would be reported as a success. v0.6.0 audit HIGH-4.
|
|
261
|
+
if (event.type === 'error' && event.severity !== 'cancelled') {
|
|
262
|
+
errored = true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
// eslint-disable-next-line no-console
|
|
268
|
+
console.error(`[council] agent "${agent.id}" failed:`, err);
|
|
269
|
+
fullText = `Error: ${err instanceof Error ? err.message : 'Unknown'}`;
|
|
270
|
+
errored = true;
|
|
271
|
+
}
|
|
272
|
+
// v0.7.6: post-condition check — verify this member emitted the
|
|
273
|
+
// tools its role prompt requires. Logs a warning if any are missing.
|
|
274
|
+
// Does not block the council run (Pass 3 may add retry).
|
|
275
|
+
// v0.7.7 Pass 3: forced retry turn for the specialist. Same logic
|
|
276
|
+
// as the chairman and oracle — extracted into applyRetryIfMissing
|
|
277
|
+
// so the three loops share one implementation. Skipped when the
|
|
278
|
+
// specialist errored (retry is for tool gaps, not LLM failures).
|
|
279
|
+
// v0.7.7 Opzione B: skip the retry entirely for members in
|
|
280
|
+
// NON_RETRY_AGENTS (composer-2.5 cannot satisfy the heavy
|
|
281
|
+
// emission budget in the 240s window; the deterministic
|
|
282
|
+
// post-processor fills the gaps from a template).
|
|
283
|
+
if (isDesignPhase && !errored && !NON_RETRY_AGENTS.has(agent.id)) {
|
|
284
|
+
const specialistCheck = enforceDesignPhaseToolEmissions(agent.id, emittedToolNames);
|
|
285
|
+
yield* applyRetryIfMissing({
|
|
286
|
+
agent,
|
|
287
|
+
check: specialistCheck,
|
|
288
|
+
requirements: DESIGN_PHASE_REQUIREMENTS[agent.id],
|
|
289
|
+
emittedToolNames,
|
|
290
|
+
executableNames,
|
|
291
|
+
sessionId,
|
|
292
|
+
userMessage,
|
|
293
|
+
agentOutputs,
|
|
294
|
+
config,
|
|
295
|
+
effectiveProvider,
|
|
296
|
+
effectiveModel,
|
|
297
|
+
onToolCall: () => { toolCalls += 1; },
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
else if (isDesignPhase) {
|
|
301
|
+
enforceDesignPhaseToolEmissions(agent.id, emittedToolNames);
|
|
302
|
+
}
|
|
303
|
+
const memberDuration = Date.now() - memberStart;
|
|
304
|
+
emitMemberCost({
|
|
305
|
+
memberId: agent.id,
|
|
306
|
+
name: agent.name,
|
|
307
|
+
usage,
|
|
308
|
+
durationMs: memberDuration,
|
|
309
|
+
toolCalls,
|
|
310
|
+
errored,
|
|
311
|
+
});
|
|
312
|
+
yield createBrainEvent('member_cost', sessionId, {
|
|
313
|
+
cost: {
|
|
314
|
+
memberId: agent.id,
|
|
315
|
+
name: agent.name,
|
|
316
|
+
promptTokens: usage?.promptTokens ?? 0,
|
|
317
|
+
completionTokens: usage?.completionTokens ?? 0,
|
|
318
|
+
totalTokens: usage?.totalTokens ?? (usage?.promptTokens ?? 0) + (usage?.completionTokens ?? 0),
|
|
319
|
+
durationMs: memberDuration,
|
|
320
|
+
toolCalls,
|
|
321
|
+
errored,
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
const thinking = parseThinking(fullText);
|
|
325
|
+
const cleaned = cleanAgentContent(fullText);
|
|
326
|
+
callbacks.onAgentDone?.(agent, cleaned, thinking || undefined);
|
|
327
|
+
messages.push({
|
|
328
|
+
id: crypto.randomUUID().slice(0, 12),
|
|
329
|
+
role: 'assistant',
|
|
330
|
+
content: cleaned,
|
|
331
|
+
thinking: thinking || undefined,
|
|
332
|
+
agentId: agent.id,
|
|
333
|
+
agentName: agent.name,
|
|
334
|
+
agentRole: agent.role,
|
|
335
|
+
agentColor: agent.color,
|
|
336
|
+
timestamp: Date.now(),
|
|
337
|
+
});
|
|
338
|
+
agentOutputs.push({ name: agent.name, role: agent.role, content: cleaned });
|
|
339
|
+
const clarification = parseClarificationRequest(fullText);
|
|
340
|
+
if (clarification) {
|
|
341
|
+
// Note: Pure council doesn't pause for user clarifications.
|
|
342
|
+
// The CLI caller (Phase 14) handles this at the slash-command level.
|
|
343
|
+
// For now, just inject the question into shared context as a comment.
|
|
344
|
+
agentOutputs.push({
|
|
345
|
+
name: 'Clarification',
|
|
346
|
+
role: 'system',
|
|
347
|
+
content: `(Agent ${agent.name} asked: ${clarification.question})`,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Minosse (critic) — runs once regardless of debateMode.
|
|
352
|
+
// Fix v0.7.5 Bug C: previously gated on config.debateMode (default false),
|
|
353
|
+
// so 6-member councils silently ran only 5 members. A critic pass is
|
|
354
|
+
// always useful before final synthesis; multi-round debate loops remain
|
|
355
|
+
// debateMode-gated (TODO: see plan 2026-07-03-council-3-bugs-fix.md).
|
|
356
|
+
if (oracle && !completedIds.has(oracle.id)) {
|
|
357
|
+
callbacks.onAgentStart?.(oracle);
|
|
358
|
+
const override = config.agentModels?.[oracle.id];
|
|
359
|
+
const effectiveProvider = override?.providerId ?? config.provider ?? 'minimax';
|
|
360
|
+
const effectiveModel = override?.model ?? config.model;
|
|
361
|
+
const anonymized = agentOutputs.map((o, i) => ({
|
|
362
|
+
...o,
|
|
363
|
+
name: `Agent ${i + 1}`,
|
|
364
|
+
role: 'Specialist',
|
|
365
|
+
}));
|
|
366
|
+
const harness = new AgentHarness({
|
|
367
|
+
model: effectiveModel,
|
|
368
|
+
provider: effectiveProvider,
|
|
369
|
+
sessionId,
|
|
370
|
+
messages: buildAgentMessages(oracle, `Review these proposals for: "${userMessage}"`, '', '', anonymized, config.aiConfig, executableNames, runMode),
|
|
371
|
+
tools: (() => {
|
|
372
|
+
const oracleToolNames = filterExecutable(Array.from(new Set([
|
|
373
|
+
'createDocument',
|
|
374
|
+
'searchDocuments',
|
|
375
|
+
...computeAgentTools(oracle, config.aiConfig),
|
|
376
|
+
])));
|
|
377
|
+
return oracleToolNames.length > 0
|
|
378
|
+
? getProviderTools(oracleToolNames).map((tool) => ({
|
|
379
|
+
name: tool.function.name,
|
|
380
|
+
description: tool.function.description,
|
|
381
|
+
parameters: tool.function.parameters,
|
|
382
|
+
}))
|
|
383
|
+
: [];
|
|
384
|
+
})(),
|
|
385
|
+
eventBus: config.eventBus,
|
|
386
|
+
toolRegistry: config.tools,
|
|
387
|
+
// Task G.2 — same per-turn limit applies to oracle.
|
|
388
|
+
maxToolCallsPerTurn: config.maxToolCallsPerTurn ?? 5,
|
|
389
|
+
// Visible-reasoning (v0.5.0): same member-stamping as the
|
|
390
|
+
// specialist loop above. Minosse's events are marked as
|
|
391
|
+
// belonging to the oracle / debate round.
|
|
392
|
+
memberId: oracle.id,
|
|
393
|
+
memberName: oracle.name,
|
|
394
|
+
providerStream: (params) => config.providerStream({
|
|
395
|
+
...params,
|
|
396
|
+
}),
|
|
397
|
+
});
|
|
398
|
+
let fullText = '';
|
|
399
|
+
let toolCalls = 0;
|
|
400
|
+
let usage = null;
|
|
401
|
+
let errored = false;
|
|
402
|
+
// v0.7.6: per-member tool emission tracking (Minosse MUST emit at
|
|
403
|
+
// least one createDocument for risks.md).
|
|
404
|
+
const emittedToolNames = [];
|
|
405
|
+
const memberStart = Date.now();
|
|
406
|
+
try {
|
|
407
|
+
for await (const event of harness.run()) {
|
|
408
|
+
yield event;
|
|
409
|
+
if (event.type === 'tool_execution_start') {
|
|
410
|
+
toolCalls += 1;
|
|
411
|
+
emittedToolNames.push(event.toolName);
|
|
412
|
+
}
|
|
413
|
+
if (event.type === 'message_end' && event.usage) {
|
|
414
|
+
usage = event.usage;
|
|
415
|
+
}
|
|
416
|
+
if (event.type === 'message_delta') {
|
|
417
|
+
fullText += event.delta;
|
|
418
|
+
callbacks.onAgentChunk?.(oracle, event.delta);
|
|
419
|
+
}
|
|
420
|
+
// v0.6.0 audit HIGH-4 — detect AgentHarness-emitted error
|
|
421
|
+
// events so the oracle's `member_cost.errored` reflects reality.
|
|
422
|
+
if (event.type === 'error' && event.severity !== 'cancelled') {
|
|
423
|
+
errored = true;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
// eslint-disable-next-line no-console
|
|
429
|
+
console.error('[council] oracle failed:', err);
|
|
430
|
+
fullText = `Review error: ${err instanceof Error ? err.message : 'Unknown'}`;
|
|
431
|
+
errored = true;
|
|
432
|
+
}
|
|
433
|
+
// v0.7.6: post-condition check on Minosse's tool emission. The role
|
|
434
|
+
// prompt requires at least one createDocument (for risks.md).
|
|
435
|
+
// v0.7.7 Pass 3: forced retry if missing. Skipped on error.
|
|
436
|
+
if (isDesignPhase && !errored) {
|
|
437
|
+
const oracleCheck = enforceDesignPhaseToolEmissions(oracle.id, emittedToolNames);
|
|
438
|
+
yield* applyRetryIfMissing({
|
|
439
|
+
agent: oracle,
|
|
440
|
+
check: oracleCheck,
|
|
441
|
+
requirements: DESIGN_PHASE_REQUIREMENTS[oracle.id],
|
|
442
|
+
emittedToolNames,
|
|
443
|
+
executableNames,
|
|
444
|
+
sessionId,
|
|
445
|
+
userMessage: `Review these proposals for: "${userMessage}"`,
|
|
446
|
+
agentOutputs,
|
|
447
|
+
config,
|
|
448
|
+
effectiveProvider,
|
|
449
|
+
effectiveModel,
|
|
450
|
+
onToolCall: () => { toolCalls += 1; },
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
else if (isDesignPhase) {
|
|
454
|
+
enforceDesignPhaseToolEmissions(oracle.id, emittedToolNames);
|
|
455
|
+
}
|
|
456
|
+
const memberDuration = Date.now() - memberStart;
|
|
457
|
+
emitMemberCost({
|
|
458
|
+
memberId: oracle.id,
|
|
459
|
+
name: oracle.name,
|
|
460
|
+
usage,
|
|
461
|
+
durationMs: memberDuration,
|
|
462
|
+
toolCalls,
|
|
463
|
+
errored,
|
|
464
|
+
});
|
|
465
|
+
yield createBrainEvent('member_cost', sessionId, {
|
|
466
|
+
cost: {
|
|
467
|
+
memberId: oracle.id,
|
|
468
|
+
name: oracle.name,
|
|
469
|
+
promptTokens: usage?.promptTokens ?? 0,
|
|
470
|
+
completionTokens: usage?.completionTokens ?? 0,
|
|
471
|
+
totalTokens: usage?.totalTokens ?? (usage?.promptTokens ?? 0) + (usage?.completionTokens ?? 0),
|
|
472
|
+
durationMs: memberDuration,
|
|
473
|
+
toolCalls,
|
|
474
|
+
errored,
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
const thinking = parseThinking(fullText);
|
|
478
|
+
const cleaned = cleanAgentContent(fullText);
|
|
479
|
+
callbacks.onAgentDone?.(oracle, cleaned, thinking || undefined);
|
|
480
|
+
messages.push({
|
|
481
|
+
id: crypto.randomUUID().slice(0, 12),
|
|
482
|
+
role: 'assistant',
|
|
483
|
+
content: cleaned,
|
|
484
|
+
thinking: thinking || undefined,
|
|
485
|
+
agentId: oracle.id,
|
|
486
|
+
agentName: oracle.name,
|
|
487
|
+
agentRole: oracle.role,
|
|
488
|
+
agentColor: oracle.color,
|
|
489
|
+
isReview: true,
|
|
490
|
+
timestamp: Date.now(),
|
|
491
|
+
});
|
|
492
|
+
agentOutputs.push({ name: oracle.name, role: oracle.role, content: cleaned });
|
|
493
|
+
}
|
|
494
|
+
// Lucifero synthesis — v0.6.0: real chairman integration.
|
|
495
|
+
// Previously this was a stub ("Phase 13 will add full chairman
|
|
496
|
+
// integration"). v0.6.0 promotes Lucifero to a real AgentHarness
|
|
497
|
+
// run that streams message_delta events just like the 5
|
|
498
|
+
// specialists and Minosse. The chairman:
|
|
499
|
+
// 1. Receives the same systemPrompt defined in roles.ts (via
|
|
500
|
+
// buildAgentMessages with priorOutputs = all agent outputs).
|
|
501
|
+
// 2. Runs the same AgentHarness pipeline (tool calls allowed,
|
|
502
|
+
// per-turn cap honoured).
|
|
503
|
+
// 3. Emits agent_start / message_start / message_delta /
|
|
504
|
+
// message_end / agent_end / member_cost with memberId='lucifer'
|
|
505
|
+
// and memberName='Lucifero', so the TUI renders
|
|
506
|
+
// `· Lucifero` (purple #8b5cf6) just like the other roles.
|
|
507
|
+
// 4. Streams deltas through callbacks.onSynthesisChunk so the
|
|
508
|
+
// chat panel can do typewriter effect during synthesis.
|
|
509
|
+
// Robustness: if the chairman's LLM call fails, the council run
|
|
510
|
+
// does NOT abort — the 5 specialist outputs remain available,
|
|
511
|
+
// and we surface the error reason in agent_end.
|
|
512
|
+
if (chairman && !completedIds.has(chairman.id)) {
|
|
513
|
+
callbacks.onSynthesisStart?.();
|
|
514
|
+
callbacks.onAgentStart?.(chairman);
|
|
515
|
+
const override = config.agentModels?.[chairman.id];
|
|
516
|
+
const effectiveProvider = override?.providerId ?? config.provider ?? 'minimax';
|
|
517
|
+
const effectiveModel = override?.model ?? config.model;
|
|
518
|
+
const chairmanToolNames = filterExecutable(computeAgentTools(chairman, config.aiConfig));
|
|
519
|
+
const chairmanTools = chairmanToolNames.length > 0
|
|
520
|
+
? getProviderTools(chairmanToolNames).map((t) => ({
|
|
521
|
+
name: t.function.name,
|
|
522
|
+
description: t.function.description,
|
|
523
|
+
parameters: t.function.parameters,
|
|
524
|
+
}))
|
|
525
|
+
: [];
|
|
526
|
+
const chairmanHarness = new AgentHarness({
|
|
527
|
+
model: effectiveModel,
|
|
528
|
+
provider: effectiveProvider,
|
|
529
|
+
sessionId,
|
|
530
|
+
messages: buildAgentMessages(chairman, userMessage, config.ragContext, config.workspaceContext, agentOutputs, config.aiConfig, executableNames, runMode),
|
|
531
|
+
tools: chairmanTools,
|
|
532
|
+
eventBus: config.eventBus,
|
|
533
|
+
toolRegistry: config.tools,
|
|
534
|
+
maxToolCallsPerTurn: config.maxToolCallsPerTurn ?? 5,
|
|
535
|
+
// v0.5.0 visible-reasoning wiring: stamp every event with
|
|
536
|
+
// the chairman identity so the UI renders `· Lucifero` in
|
|
537
|
+
// purple. Same pattern as the specialist loop above.
|
|
538
|
+
memberId: chairman.id,
|
|
539
|
+
memberName: chairman.name,
|
|
540
|
+
providerStream: (params) => config.providerStream({
|
|
541
|
+
...params,
|
|
542
|
+
}),
|
|
543
|
+
});
|
|
544
|
+
let fullText = '';
|
|
545
|
+
let toolCalls = 0;
|
|
546
|
+
let usage = null;
|
|
547
|
+
let errored = false;
|
|
548
|
+
let lastErrorMessage = '';
|
|
549
|
+
// v0.7.6: per-member tool emission tracking for the post-condition
|
|
550
|
+
// check below (Lucifero MUST emit at least one createDocument for
|
|
551
|
+
// the synthesis; see enforceDesignPhaseToolEmissions).
|
|
552
|
+
const emittedToolNames = [];
|
|
553
|
+
const memberStart = Date.now();
|
|
554
|
+
try {
|
|
555
|
+
for await (const event of chairmanHarness.run()) {
|
|
556
|
+
yield event;
|
|
557
|
+
if (event.type === 'tool_execution_start') {
|
|
558
|
+
toolCalls += 1;
|
|
559
|
+
emittedToolNames.push(event.toolName);
|
|
560
|
+
}
|
|
561
|
+
if (event.type === 'message_end' && event.usage) {
|
|
562
|
+
usage = event.usage;
|
|
563
|
+
}
|
|
564
|
+
if (event.type === 'message_delta') {
|
|
565
|
+
fullText += event.delta;
|
|
566
|
+
callbacks.onSynthesisChunk?.(event.delta);
|
|
567
|
+
callbacks.onAgentChunk?.(chairman, event.delta);
|
|
568
|
+
}
|
|
569
|
+
if (event.type === 'error') {
|
|
570
|
+
// AgentHarness catches provider-level errors and re-emits them
|
|
571
|
+
// as BrainErrorEvent (severity 'recoverable' | 'fatal' | 'cancelled').
|
|
572
|
+
// We must detect this and mark the chairman as errored so the
|
|
573
|
+
// member_cost reflects reality, otherwise the synthesis appears
|
|
574
|
+
// successful when in fact the model never produced text.
|
|
575
|
+
if (event.severity !== 'cancelled') {
|
|
576
|
+
errored = true;
|
|
577
|
+
lastErrorMessage = event.message;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
catch (err) {
|
|
583
|
+
// Defensive: any escape from the harness (e.g. an AbortError that
|
|
584
|
+
// AgentHarness did not wrap) is also marked as errored.
|
|
585
|
+
// IMPORTANT: do NOT overwrite `fullText` here — the partial
|
|
586
|
+
// synthesis is more useful than the error string, and overwriting
|
|
587
|
+
// it would also break the `errored && fullText.length === 0` check
|
|
588
|
+
// that selects the fallback message below. (v0.6.0 audit HIGH-1)
|
|
589
|
+
// eslint-disable-next-line no-console
|
|
590
|
+
console.error(`[council] chairman "${chairman.id}" failed:`, err);
|
|
591
|
+
errored = true;
|
|
592
|
+
lastErrorMessage = err instanceof Error ? err.message : String(err);
|
|
593
|
+
}
|
|
594
|
+
// v0.7.6: post-condition check on Lucifero's tool emission. The
|
|
595
|
+
// role prompt requires at least one createDocument (for synthesis.md).
|
|
596
|
+
// v0.7.7 Pass 3: forced retry if missing — extracted into the shared
|
|
597
|
+
// helper applyRetryIfMissing. Skipped when the chairman errored (the
|
|
598
|
+
// retry is for tool-emission gaps, not for LLM-level failures).
|
|
599
|
+
if (isDesignPhase && !errored) {
|
|
600
|
+
const chairmanCheck = enforceDesignPhaseToolEmissions(chairman.id, emittedToolNames);
|
|
601
|
+
yield* applyRetryIfMissing({
|
|
602
|
+
agent: chairman,
|
|
603
|
+
check: chairmanCheck,
|
|
604
|
+
requirements: DESIGN_PHASE_REQUIREMENTS[chairman.id],
|
|
605
|
+
emittedToolNames,
|
|
606
|
+
executableNames,
|
|
607
|
+
sessionId,
|
|
608
|
+
userMessage,
|
|
609
|
+
agentOutputs,
|
|
610
|
+
config,
|
|
611
|
+
effectiveProvider,
|
|
612
|
+
effectiveModel,
|
|
613
|
+
onToolCall: () => { toolCalls += 1; },
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
else if (isDesignPhase) {
|
|
617
|
+
enforceDesignPhaseToolEmissions(chairman.id, emittedToolNames);
|
|
618
|
+
}
|
|
619
|
+
const memberDuration = Date.now() - memberStart;
|
|
620
|
+
// If the chairman errored mid-flight but produced some text, keep it
|
|
621
|
+
// (don't lose partial synthesis) but mark the run as errored.
|
|
622
|
+
const finalSynthesis = errored && fullText.length === 0
|
|
623
|
+
? `[Chairman synthesis failed: ${lastErrorMessage || 'unknown error'}]`
|
|
624
|
+
: fullText;
|
|
625
|
+
callbacks.onSynthesisDone?.(finalSynthesis, undefined, undefined);
|
|
626
|
+
emitMemberCost({
|
|
627
|
+
memberId: chairman.id,
|
|
628
|
+
name: chairman.name,
|
|
629
|
+
usage,
|
|
630
|
+
durationMs: memberDuration,
|
|
631
|
+
toolCalls,
|
|
632
|
+
errored,
|
|
633
|
+
});
|
|
634
|
+
yield createBrainEvent('member_cost', sessionId, {
|
|
635
|
+
cost: {
|
|
636
|
+
memberId: chairman.id,
|
|
637
|
+
name: chairman.name,
|
|
638
|
+
promptTokens: usage?.promptTokens ?? 0,
|
|
639
|
+
completionTokens: usage?.completionTokens ?? 0,
|
|
640
|
+
totalTokens: usage?.totalTokens ?? (usage?.promptTokens ?? 0) + (usage?.completionTokens ?? 0),
|
|
641
|
+
durationMs: memberDuration,
|
|
642
|
+
toolCalls,
|
|
643
|
+
errored,
|
|
644
|
+
},
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
yield {
|
|
648
|
+
type: 'agent_end',
|
|
649
|
+
id: crypto.randomUUID(),
|
|
650
|
+
ts: Date.now(),
|
|
651
|
+
sessionId,
|
|
652
|
+
reason: 'completed',
|
|
653
|
+
durationMs: 0,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Pure helper: given the list of tool names a member emitted during its
|
|
658
|
+
* turn, and a list of requirements, return whether all requirements are
|
|
659
|
+
* met.
|
|
660
|
+
*/
|
|
661
|
+
export function checkMemberToolEmissions(_memberId, emittedToolNames, requirements) {
|
|
662
|
+
if (requirements.length === 0) {
|
|
663
|
+
return { ok: true, missing: [] };
|
|
664
|
+
}
|
|
665
|
+
// Tally emitted counts once for all requirements.
|
|
666
|
+
const counts = new Map();
|
|
667
|
+
for (const name of emittedToolNames) {
|
|
668
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
669
|
+
}
|
|
670
|
+
const missing = [];
|
|
671
|
+
for (const req of requirements) {
|
|
672
|
+
const got = counts.get(req.name) ?? 0;
|
|
673
|
+
if (got < req.min) {
|
|
674
|
+
missing.push(`${req.name} (got ${got}, need >= ${req.min})`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return { ok: missing.length === 0, missing };
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* v0.7.8 — Per-member tool-emission requirement SETS for the design-phase
|
|
681
|
+
* council run. The outer array is an OR of alternatives: the member's turn
|
|
682
|
+
* is complete when ANY one set is fully satisfied. The FIRST set is the
|
|
683
|
+
* preferred contract — its unmet requirements drive the warning message
|
|
684
|
+
* and the forced-retry tool list.
|
|
685
|
+
*
|
|
686
|
+
* Nettuno has two ways to satisfy its contract:
|
|
687
|
+
* 1. (preferred) ONE `createPlan` batch call — phases + nested tasks +
|
|
688
|
+
* milestone in a single emission. Retry budget: 1 call, which
|
|
689
|
+
* composer-2.5 handles reliably (same shape as the Minosse/Lucifero
|
|
690
|
+
* retries that already work).
|
|
691
|
+
* 2. (legacy) the itemized trio — kept so stronger models (e.g. Opus)
|
|
692
|
+
* that emit createPhase/createTask/createMilestone directly are not
|
|
693
|
+
* flagged or retried.
|
|
694
|
+
*/
|
|
695
|
+
export const DESIGN_PHASE_REQUIREMENT_SETS = {
|
|
696
|
+
nettun: [
|
|
697
|
+
[{ name: 'createPlan', min: 1 }],
|
|
698
|
+
[
|
|
699
|
+
{ name: 'createPhase', min: 3 },
|
|
700
|
+
{ name: 'createTask', min: 6 },
|
|
701
|
+
{ name: 'createMilestone', min: 1 },
|
|
702
|
+
],
|
|
703
|
+
],
|
|
704
|
+
geryon: [
|
|
705
|
+
[{ name: 'createDocument', min: 3 }],
|
|
706
|
+
],
|
|
707
|
+
pluton: [
|
|
708
|
+
[{ name: 'createDocument', min: 1 }],
|
|
709
|
+
],
|
|
710
|
+
minos: [
|
|
711
|
+
[{ name: 'createDocument', min: 1 }],
|
|
712
|
+
],
|
|
713
|
+
lucifer: [
|
|
714
|
+
[{ name: 'createDocument', min: 1 }],
|
|
715
|
+
],
|
|
716
|
+
};
|
|
717
|
+
/**
|
|
718
|
+
* Preferred (first) requirement set per member — kept as the flat map the
|
|
719
|
+
* council loops pass to `applyRetryIfMissing` for the retry budget. For
|
|
720
|
+
* Nettuno this is `createPlan min 1`, so the forced retry advertises ONE
|
|
721
|
+
* tool with a 1-call budget instead of the old 13+-call itemized contract.
|
|
722
|
+
*/
|
|
723
|
+
export const DESIGN_PHASE_REQUIREMENTS = Object.fromEntries(Object.entries(DESIGN_PHASE_REQUIREMENT_SETS).map(([id, sets]) => [id, sets[0]]));
|
|
724
|
+
/**
|
|
725
|
+
* Pure helper: OR-of-sets variant of {@link checkMemberToolEmissions}.
|
|
726
|
+
* Returns ok when ANY set is fully satisfied. When none is, the missing
|
|
727
|
+
* list reflects the FIRST (preferred) set so the warning and the retry
|
|
728
|
+
* point the model at the cheapest way to comply.
|
|
729
|
+
*/
|
|
730
|
+
export function checkMemberToolEmissionSets(memberId, emittedToolNames, sets) {
|
|
731
|
+
if (sets.length === 0) {
|
|
732
|
+
return { ok: true, missing: [] };
|
|
733
|
+
}
|
|
734
|
+
const results = sets.map((set) => checkMemberToolEmissions(memberId, emittedToolNames, set));
|
|
735
|
+
if (results.some((r) => r.ok)) {
|
|
736
|
+
return { ok: true, missing: [] };
|
|
737
|
+
}
|
|
738
|
+
return results[0];
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Run the post-condition check for a member and emit a console.warn when
|
|
742
|
+
* any required tool was not emitted the minimum number of times. Returns
|
|
743
|
+
* the check result so callers can act on it (Pass 3 may add automatic
|
|
744
|
+
* retry; for now we only warn).
|
|
745
|
+
*/
|
|
746
|
+
export function enforceDesignPhaseToolEmissions(memberId, emittedToolNames) {
|
|
747
|
+
const sets = DESIGN_PHASE_REQUIREMENT_SETS[memberId];
|
|
748
|
+
if (!sets || sets.length === 0) {
|
|
749
|
+
return { ok: true, missing: [] };
|
|
750
|
+
}
|
|
751
|
+
const result = checkMemberToolEmissionSets(memberId, emittedToolNames, sets);
|
|
752
|
+
if (!result.ok) {
|
|
753
|
+
// eslint-disable-next-line no-console
|
|
754
|
+
console.warn(`[council] member "${memberId}" did not emit required tools: ${result.missing.join(', ')}. ` +
|
|
755
|
+
`The downstream .zelari/ deliverable may be incomplete. ` +
|
|
756
|
+
`(A forced retry turn scoped to the missing tools follows; the deterministic ` +
|
|
757
|
+
`complete-design fallback covers any remaining gap.)`);
|
|
758
|
+
}
|
|
759
|
+
return result;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Maximum number of forced retry turns per council member. Cap of 1
|
|
763
|
+
* keeps the worst-case council latency bounded (a single extra turn per
|
|
764
|
+
* member × 4 design-phase members ≈ 30-60 s on top of the base run).
|
|
765
|
+
* Going above 1 tends to produce hallucinated tool arguments because
|
|
766
|
+
* the model has already spent its "tool budget" on exploration.
|
|
767
|
+
*/
|
|
768
|
+
export const MAX_RETRY_PER_MEMBER = 1;
|
|
769
|
+
/**
|
|
770
|
+
* Pure helper: should the council loop spin up one more forced turn for
|
|
771
|
+
* this member to recover the missing tool emissions?
|
|
772
|
+
*
|
|
773
|
+
* Returns true when:
|
|
774
|
+
* - at least one tool is still missing after the post-condition check, AND
|
|
775
|
+
* - the retry budget for this member has not been exhausted.
|
|
776
|
+
*
|
|
777
|
+
* Returns false otherwise. Tested as a pure function so the council
|
|
778
|
+
* loop can branch on the answer without coupling to AgentHarness.
|
|
779
|
+
*/
|
|
780
|
+
export function shouldRetryMember(missingToolNames, attemptsSoFar) {
|
|
781
|
+
if (missingToolNames.length === 0)
|
|
782
|
+
return false;
|
|
783
|
+
if (attemptsSoFar >= MAX_RETRY_PER_MEMBER)
|
|
784
|
+
return false;
|
|
785
|
+
return true;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Build the one-line prompt that the retry turn sends to the model.
|
|
789
|
+
* The shape matters: the model is primed by its role prompt to produce
|
|
790
|
+
* prose, so the retry prompt must be unambiguous, imperative, and
|
|
791
|
+
* scoped to ONLY the missing tools.
|
|
792
|
+
*
|
|
793
|
+
* Format: "You did not emit: <names>. Call <names> NOW with concrete
|
|
794
|
+
* arguments. No prose."
|
|
795
|
+
*
|
|
796
|
+
* Multiple tools are listed comma-separated in the same call so the
|
|
797
|
+
* model can satisfy them in a single tool_calls turn (which is the
|
|
798
|
+
* cheapest path through AgentHarness).
|
|
799
|
+
*/
|
|
800
|
+
export function buildRetryPrompt(missingToolNames) {
|
|
801
|
+
const names = missingToolNames.join(', ');
|
|
802
|
+
return `You did not emit the required workspace tools: ${names}. Call ${names} NOW with concrete arguments. No prose. No search.`;
|
|
803
|
+
}
|
|
804
|
+
// ── Forced retry turn (v0.7.7 Pass 3) ──────────────────────────────────────
|
|
805
|
+
//
|
|
806
|
+
// When the post-condition check fails for a member, the council loop can
|
|
807
|
+
// spin up ONE more AgentHarness turn whose ONLY purpose is to force the
|
|
808
|
+
// missing tool emissions. This is a structural fix for the failure mode
|
|
809
|
+
// where the model terminates after exploration (`searchDocuments` × 2)
|
|
810
|
+
// without persisting the required artifacts.
|
|
811
|
+
//
|
|
812
|
+
// The retry turn is intentionally minimal:
|
|
813
|
+
// - System prompt: same as the original (via buildAgentMessages) so
|
|
814
|
+
// the model still has its role contract.
|
|
815
|
+
// - User message: the retry prompt (one line, imperative).
|
|
816
|
+
// - Tools: ONLY the missing tools (filtered through filterExecutable
|
|
817
|
+
// so we never advertise a tool the runtime cannot execute).
|
|
818
|
+
// - maxToolCallsPerTurn: exactly the number of missing tools — the
|
|
819
|
+
// model cannot explore again, it can only call what's missing.
|
|
820
|
+
//
|
|
821
|
+
// Returns the additional tool names emitted during the retry. The caller
|
|
822
|
+
// is responsible for re-running checkMemberToolEmissions with the
|
|
823
|
+
// union of original + retry emissions.
|
|
824
|
+
export async function* runRetryTurnForMember(args) {
|
|
825
|
+
// Filter the missing tools against what's actually executable in this
|
|
826
|
+
// runtime. If a tool is missing from executableTools, the retry can't
|
|
827
|
+
// emit it — log and skip.
|
|
828
|
+
const executableMissing = args.executableTools
|
|
829
|
+
? args.missingToolNames.filter((n) => args.executableTools.has(n))
|
|
830
|
+
: args.missingToolNames;
|
|
831
|
+
if (executableMissing.length === 0) {
|
|
832
|
+
return [];
|
|
833
|
+
}
|
|
834
|
+
// Build the minimal tool set — only the missing tools.
|
|
835
|
+
const retryToolNames = executableMissing;
|
|
836
|
+
const retryToolSpecs = getProviderTools(retryToolNames).map((t) => ({
|
|
837
|
+
name: t.function.name,
|
|
838
|
+
description: t.function.description,
|
|
839
|
+
parameters: t.function.parameters,
|
|
840
|
+
}));
|
|
841
|
+
// Build the messages: same system + role context as the original turn,
|
|
842
|
+
// then the retry prompt as a user message appended at the end.
|
|
843
|
+
const baseMessages = buildAgentMessages(args.agent, args.userMessage, args.ragContext, args.workspaceContext, args.priorOutputs, args.aiConfig, args.executableTools, args.runMode ?? 'implementation');
|
|
844
|
+
const retryMessages = [
|
|
845
|
+
...baseMessages,
|
|
846
|
+
{ role: 'user', content: buildRetryPrompt(executableMissing) },
|
|
847
|
+
];
|
|
848
|
+
// Budget the retry turn so the model can satisfy ALL minimums in a
|
|
849
|
+
// single tool_calls turn. For createDocument min:1 this is 1; for
|
|
850
|
+
// createTask min:12 this is 12. Falls back to `missingToolNames.length`
|
|
851
|
+
// when no per-tool min map is provided.
|
|
852
|
+
const maxToolCalls = args.minPerTool !== undefined
|
|
853
|
+
? Object.entries(args.minPerTool)
|
|
854
|
+
.filter(([name]) => executableMissing.includes(name))
|
|
855
|
+
.reduce((sum, [, min]) => sum + min, 0)
|
|
856
|
+
: retryToolNames.length;
|
|
857
|
+
const retryHarness = new AgentHarness({
|
|
858
|
+
model: args.effectiveModel,
|
|
859
|
+
provider: args.effectiveProvider,
|
|
860
|
+
sessionId: args.sessionId,
|
|
861
|
+
messages: retryMessages,
|
|
862
|
+
tools: retryToolSpecs,
|
|
863
|
+
eventBus: args.eventBus,
|
|
864
|
+
toolRegistry: args.toolRegistry,
|
|
865
|
+
// Budget the retry so the model can satisfy every requirement in
|
|
866
|
+
// ONE tool_calls turn. For createTask min:12 this needs 12 calls.
|
|
867
|
+
maxToolCallsPerTurn: maxToolCalls,
|
|
868
|
+
memberId: args.agent.id,
|
|
869
|
+
memberName: args.agent.name,
|
|
870
|
+
providerStream: (params) => args.providerStream(params),
|
|
871
|
+
});
|
|
872
|
+
const retryEmitted = [];
|
|
873
|
+
for await (const event of retryHarness.run()) {
|
|
874
|
+
if (event.type === 'tool_execution_start') {
|
|
875
|
+
retryEmitted.push(event.toolName);
|
|
876
|
+
}
|
|
877
|
+
yield event;
|
|
878
|
+
}
|
|
879
|
+
return retryEmitted;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Shared retry orchestrator used by specialist, oracle, and chairman
|
|
883
|
+
* loops. Given the post-condition check result, decides whether to
|
|
884
|
+
* spin up a forced retry turn, and if so yields the events from that
|
|
885
|
+
* turn back to the caller (so the UI sees them) while mutating the
|
|
886
|
+
* shared `emittedToolNames` array (so the next post-condition check
|
|
887
|
+
* sees the union of original + retry emissions).
|
|
888
|
+
*
|
|
889
|
+
* The retry turn is skipped when:
|
|
890
|
+
* - the check passed (no missing tools), OR
|
|
891
|
+
* - the retry budget for this member is exhausted (shouldRetryMember).
|
|
892
|
+
*
|
|
893
|
+
* On retry failure (network error, model error, etc.) the function logs
|
|
894
|
+
* the error and continues — it never throws. The next post-condition
|
|
895
|
+
* check will simply re-warn.
|
|
896
|
+
*/
|
|
897
|
+
export async function* applyRetryIfMissing(args) {
|
|
898
|
+
if (args.check.ok)
|
|
899
|
+
return;
|
|
900
|
+
const missingToolNames = args.check.missing.map((m) => m.split(' ')[0]);
|
|
901
|
+
if (!shouldRetryMember(missingToolNames, 0))
|
|
902
|
+
return;
|
|
903
|
+
// eslint-disable-next-line no-console
|
|
904
|
+
console.warn(`[council] ${args.agent.id} retrying missing tools: ${missingToolNames.join(', ')}`);
|
|
905
|
+
// Build the minPerTool map from the original requirements so the
|
|
906
|
+
// retry turn budgets enough tool calls to satisfy every minimum.
|
|
907
|
+
// Without this, a createTask min:12 requirement would be capped at
|
|
908
|
+
// 1 call (the number of distinct missing tools).
|
|
909
|
+
const minPerTool = {};
|
|
910
|
+
if (args.requirements) {
|
|
911
|
+
for (const req of args.requirements) {
|
|
912
|
+
if (missingToolNames.includes(req.name)) {
|
|
913
|
+
minPerTool[req.name] = req.min;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
try {
|
|
918
|
+
const retryGenerator = runRetryTurnForMember({
|
|
919
|
+
agent: args.agent,
|
|
920
|
+
missingToolNames,
|
|
921
|
+
minPerTool,
|
|
922
|
+
executableTools: args.executableNames,
|
|
923
|
+
userMessage: args.userMessage,
|
|
924
|
+
ragContext: args.config.ragContext,
|
|
925
|
+
workspaceContext: args.config.workspaceContext,
|
|
926
|
+
priorOutputs: args.agentOutputs,
|
|
927
|
+
aiConfig: args.config.aiConfig,
|
|
928
|
+
sessionId: args.sessionId,
|
|
929
|
+
effectiveModel: args.effectiveModel,
|
|
930
|
+
effectiveProvider: args.effectiveProvider,
|
|
931
|
+
eventBus: args.config.eventBus,
|
|
932
|
+
toolRegistry: args.config.tools,
|
|
933
|
+
providerStream: args.config.providerStream,
|
|
934
|
+
runMode: args.config.runMode,
|
|
935
|
+
});
|
|
936
|
+
for await (const event of retryGenerator) {
|
|
937
|
+
if (event.type === 'tool_execution_start') {
|
|
938
|
+
args.onToolCall();
|
|
939
|
+
args.emittedToolNames.push(event.toolName);
|
|
940
|
+
}
|
|
941
|
+
yield event;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
catch (retryErr) {
|
|
945
|
+
// eslint-disable-next-line no-console
|
|
946
|
+
console.error(`[council] ${args.agent.id} retry failed:`, retryErr);
|
|
947
|
+
}
|
|
948
|
+
// Re-run the check so the final warning reflects the union of
|
|
949
|
+
// original + retry emissions.
|
|
950
|
+
enforceDesignPhaseToolEmissions(args.agent.id, args.emittedToolNames);
|
|
951
|
+
}
|
|
952
|
+
//# sourceMappingURL=councilApi.js.map
|