@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.
Files changed (202) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agents/advancedTools.d.ts +7 -0
  3. package/dist/agents/advancedTools.d.ts.map +1 -0
  4. package/dist/agents/advancedTools.js +115 -0
  5. package/dist/agents/advancedTools.js.map +1 -0
  6. package/dist/agents/councilApi.d.ts +328 -0
  7. package/dist/agents/councilApi.d.ts.map +1 -0
  8. package/dist/agents/councilApi.js +952 -0
  9. package/dist/agents/councilApi.js.map +1 -0
  10. package/dist/agents/councilDirectives.d.ts +26 -0
  11. package/dist/agents/councilDirectives.d.ts.map +1 -0
  12. package/dist/agents/councilDirectives.js +98 -0
  13. package/dist/agents/councilDirectives.js.map +1 -0
  14. package/dist/agents/harnessToolBridge.d.ts +26 -0
  15. package/dist/agents/harnessToolBridge.d.ts.map +1 -0
  16. package/dist/agents/harnessToolBridge.js +60 -0
  17. package/dist/agents/harnessToolBridge.js.map +1 -0
  18. package/dist/agents/promoteMember.d.ts +77 -0
  19. package/dist/agents/promoteMember.d.ts.map +1 -0
  20. package/dist/agents/promoteMember.js +210 -0
  21. package/dist/agents/promoteMember.js.map +1 -0
  22. package/dist/agents/promptModules.d.ts +20 -0
  23. package/dist/agents/promptModules.d.ts.map +1 -0
  24. package/dist/agents/promptModules.js +139 -0
  25. package/dist/agents/promptModules.js.map +1 -0
  26. package/dist/agents/roles.d.ts +40 -0
  27. package/dist/agents/roles.d.ts.map +1 -0
  28. package/dist/agents/roles.js +352 -0
  29. package/dist/agents/roles.js.map +1 -0
  30. package/dist/agents/skills/builtin/debugging.d.ts +2 -0
  31. package/dist/agents/skills/builtin/debugging.d.ts.map +1 -0
  32. package/dist/agents/skills/builtin/debugging.js +248 -0
  33. package/dist/agents/skills/builtin/debugging.js.map +1 -0
  34. package/dist/agents/skills/builtin/docs.d.ts +2 -0
  35. package/dist/agents/skills/builtin/docs.d.ts.map +1 -0
  36. package/dist/agents/skills/builtin/docs.js +291 -0
  37. package/dist/agents/skills/builtin/docs.js.map +1 -0
  38. package/dist/agents/skills/builtin/git-ops.d.ts +2 -0
  39. package/dist/agents/skills/builtin/git-ops.d.ts.map +1 -0
  40. package/dist/agents/skills/builtin/git-ops.js +301 -0
  41. package/dist/agents/skills/builtin/git-ops.js.map +1 -0
  42. package/dist/agents/skills/builtin/planning.d.ts +2 -0
  43. package/dist/agents/skills/builtin/planning.d.ts.map +1 -0
  44. package/dist/agents/skills/builtin/planning.js +335 -0
  45. package/dist/agents/skills/builtin/planning.js.map +1 -0
  46. package/dist/agents/skills/builtin/refactoring.d.ts +2 -0
  47. package/dist/agents/skills/builtin/refactoring.d.ts.map +1 -0
  48. package/dist/agents/skills/builtin/refactoring.js +261 -0
  49. package/dist/agents/skills/builtin/refactoring.js.map +1 -0
  50. package/dist/agents/skills/builtin/review.d.ts +2 -0
  51. package/dist/agents/skills/builtin/review.d.ts.map +1 -0
  52. package/dist/agents/skills/builtin/review.js +302 -0
  53. package/dist/agents/skills/builtin/review.js.map +1 -0
  54. package/dist/agents/skills/builtin/testing.d.ts +2 -0
  55. package/dist/agents/skills/builtin/testing.d.ts.map +1 -0
  56. package/dist/agents/skills/builtin/testing.js +245 -0
  57. package/dist/agents/skills/builtin/testing.js.map +1 -0
  58. package/dist/agents/skills.d.ts +138 -0
  59. package/dist/agents/skills.d.ts.map +1 -0
  60. package/dist/agents/skills.js +277 -0
  61. package/dist/agents/skills.js.map +1 -0
  62. package/dist/agents/systemPromptBuilder.d.ts +45 -0
  63. package/dist/agents/systemPromptBuilder.d.ts.map +1 -0
  64. package/dist/agents/systemPromptBuilder.js +159 -0
  65. package/dist/agents/systemPromptBuilder.js.map +1 -0
  66. package/dist/agents/toolSchemas.d.ts +55 -0
  67. package/dist/agents/toolSchemas.d.ts.map +1 -0
  68. package/dist/agents/toolSchemas.js +316 -0
  69. package/dist/agents/toolSchemas.js.map +1 -0
  70. package/dist/agents/tools.d.ts +67 -0
  71. package/dist/agents/tools.d.ts.map +1 -0
  72. package/dist/agents/tools.js +225 -0
  73. package/dist/agents/tools.js.map +1 -0
  74. package/dist/agents/vaultTools.d.ts +9 -0
  75. package/dist/agents/vaultTools.d.ts.map +1 -0
  76. package/dist/agents/vaultTools.js +134 -0
  77. package/dist/agents/vaultTools.js.map +1 -0
  78. package/dist/core/AgentHarness.d.ts +250 -0
  79. package/dist/core/AgentHarness.d.ts.map +1 -0
  80. package/dist/core/AgentHarness.js +614 -0
  81. package/dist/core/AgentHarness.js.map +1 -0
  82. package/dist/core/providerStream.d.ts +61 -0
  83. package/dist/core/providerStream.d.ts.map +1 -0
  84. package/dist/core/providerStream.js +99 -0
  85. package/dist/core/providerStream.js.map +1 -0
  86. package/dist/core/sessionJsonl.d.ts +50 -0
  87. package/dist/core/sessionJsonl.d.ts.map +1 -0
  88. package/dist/core/sessionJsonl.js +99 -0
  89. package/dist/core/sessionJsonl.js.map +1 -0
  90. package/dist/core/tools/auditLog.d.ts +14 -0
  91. package/dist/core/tools/auditLog.d.ts.map +1 -0
  92. package/dist/core/tools/auditLog.js +26 -0
  93. package/dist/core/tools/auditLog.js.map +1 -0
  94. package/dist/core/tools/builtin/_walk.d.ts +40 -0
  95. package/dist/core/tools/builtin/_walk.d.ts.map +1 -0
  96. package/dist/core/tools/builtin/_walk.js +129 -0
  97. package/dist/core/tools/builtin/_walk.js.map +1 -0
  98. package/dist/core/tools/builtin/diff.d.ts +45 -0
  99. package/dist/core/tools/builtin/diff.d.ts.map +1 -0
  100. package/dist/core/tools/builtin/diff.js +477 -0
  101. package/dist/core/tools/builtin/diff.js.map +1 -0
  102. package/dist/core/tools/builtin/filesystem.d.ts +45 -0
  103. package/dist/core/tools/builtin/filesystem.d.ts.map +1 -0
  104. package/dist/core/tools/builtin/filesystem.js +110 -0
  105. package/dist/core/tools/builtin/filesystem.js.map +1 -0
  106. package/dist/core/tools/builtin/listFiles.d.ts +18 -0
  107. package/dist/core/tools/builtin/listFiles.d.ts.map +1 -0
  108. package/dist/core/tools/builtin/listFiles.js +48 -0
  109. package/dist/core/tools/builtin/listFiles.js.map +1 -0
  110. package/dist/core/tools/builtin/search.d.ts +49 -0
  111. package/dist/core/tools/builtin/search.d.ts.map +1 -0
  112. package/dist/core/tools/builtin/search.js +148 -0
  113. package/dist/core/tools/builtin/search.js.map +1 -0
  114. package/dist/core/tools/builtin/shell.d.ts +27 -0
  115. package/dist/core/tools/builtin/shell.d.ts.map +1 -0
  116. package/dist/core/tools/builtin/shell.js +98 -0
  117. package/dist/core/tools/builtin/shell.js.map +1 -0
  118. package/dist/core/tools/builtin/shellResolver.d.ts +25 -0
  119. package/dist/core/tools/builtin/shellResolver.d.ts.map +1 -0
  120. package/dist/core/tools/builtin/shellResolver.js +117 -0
  121. package/dist/core/tools/builtin/shellResolver.js.map +1 -0
  122. package/dist/core/tools/builtin/web.d.ts +43 -0
  123. package/dist/core/tools/builtin/web.d.ts.map +1 -0
  124. package/dist/core/tools/builtin/web.js +208 -0
  125. package/dist/core/tools/builtin/web.js.map +1 -0
  126. package/dist/core/tools/registry.d.ts +31 -0
  127. package/dist/core/tools/registry.d.ts.map +1 -0
  128. package/dist/core/tools/registry.js +115 -0
  129. package/dist/core/tools/registry.js.map +1 -0
  130. package/dist/core/tools/toolTypes.d.ts +60 -0
  131. package/dist/core/tools/toolTypes.d.ts.map +1 -0
  132. package/dist/core/tools/toolTypes.js +8 -0
  133. package/dist/core/tools/toolTypes.js.map +1 -0
  134. package/dist/core/tools/zodBridge.d.ts +6 -0
  135. package/dist/core/tools/zodBridge.d.ts.map +1 -0
  136. package/dist/core/tools/zodBridge.js +56 -0
  137. package/dist/core/tools/zodBridge.js.map +1 -0
  138. package/dist/council/index.d.ts +14 -0
  139. package/dist/council/index.d.ts.map +1 -0
  140. package/dist/council/index.js +14 -0
  141. package/dist/council/index.js.map +1 -0
  142. package/dist/council/modeBanners.d.ts +5 -0
  143. package/dist/council/modeBanners.d.ts.map +1 -0
  144. package/dist/council/modeBanners.js +10 -0
  145. package/dist/council/modeBanners.js.map +1 -0
  146. package/dist/council/runMode.d.ts +25 -0
  147. package/dist/council/runMode.d.ts.map +1 -0
  148. package/dist/council/runMode.js +41 -0
  149. package/dist/council/runMode.js.map +1 -0
  150. package/dist/events/index.d.ts +6 -0
  151. package/dist/events/index.d.ts.map +1 -0
  152. package/dist/events/index.js +6 -0
  153. package/dist/events/index.js.map +1 -0
  154. package/dist/harness/index.d.ts +9 -0
  155. package/dist/harness/index.d.ts.map +1 -0
  156. package/dist/harness/index.js +9 -0
  157. package/dist/harness/index.js.map +1 -0
  158. package/dist/harness/tools/index.d.ts +20 -0
  159. package/dist/harness/tools/index.d.ts.map +1 -0
  160. package/dist/harness/tools/index.js +19 -0
  161. package/dist/harness/tools/index.js.map +1 -0
  162. package/dist/index.d.ts +13 -0
  163. package/dist/index.d.ts.map +1 -0
  164. package/dist/index.js +18 -0
  165. package/dist/index.js.map +1 -0
  166. package/dist/shared/eventBus.d.ts +58 -0
  167. package/dist/shared/eventBus.d.ts.map +1 -0
  168. package/dist/shared/eventBus.js +104 -0
  169. package/dist/shared/eventBus.js.map +1 -0
  170. package/dist/shared/events.d.ts +230 -0
  171. package/dist/shared/events.d.ts.map +1 -0
  172. package/dist/shared/events.js +76 -0
  173. package/dist/shared/events.js.map +1 -0
  174. package/dist/shared/index.d.ts +8 -0
  175. package/dist/shared/index.d.ts.map +1 -0
  176. package/dist/shared/index.js +8 -0
  177. package/dist/shared/index.js.map +1 -0
  178. package/dist/skills/index.d.ts +10 -0
  179. package/dist/skills/index.d.ts.map +1 -0
  180. package/dist/skills/index.js +10 -0
  181. package/dist/skills/index.js.map +1 -0
  182. package/dist/types/context.d.ts +54 -0
  183. package/dist/types/context.d.ts.map +1 -0
  184. package/dist/types/context.js +3 -0
  185. package/dist/types/context.js.map +1 -0
  186. package/dist/types/index.d.ts +13 -0
  187. package/dist/types/index.d.ts.map +1 -0
  188. package/dist/types/index.js +12 -0
  189. package/dist/types/index.js.map +1 -0
  190. package/dist/types/knowledge.d.ts +79 -0
  191. package/dist/types/knowledge.d.ts.map +1 -0
  192. package/dist/types/knowledge.js +5 -0
  193. package/dist/types/knowledge.js.map +1 -0
  194. package/dist/types/legacy.d.ts +466 -0
  195. package/dist/types/legacy.d.ts.map +1 -0
  196. package/dist/types/legacy.js +2 -0
  197. package/dist/types/legacy.js.map +1 -0
  198. package/dist/types/systemTypes.d.ts +113 -0
  199. package/dist/types/systemTypes.d.ts.map +1 -0
  200. package/dist/types/systemTypes.js +10 -0
  201. package/dist/types/systemTypes.js.map +1 -0
  202. 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