zelari-code 0.6.2 → 0.7.5
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/bin/zelari-code.js +1 -1
- package/dist/agents/councilApi.js +7 -7
- package/dist/agents/councilApi.js.map +1 -1
- package/dist/agents/councilDirectives.js +3 -3
- package/dist/agents/promoteMember.js +2 -2
- package/dist/agents/roles.js +50 -32
- package/dist/agents/roles.js.map +1 -1
- package/dist/agents/skills/builtin/debugging.js +4 -4
- package/dist/agents/skills/builtin/debugging.js.map +1 -1
- package/dist/agents/skills/builtin/docs.js +23 -13
- package/dist/agents/skills/builtin/docs.js.map +1 -1
- package/dist/agents/skills/builtin/git-ops.js +3 -3
- package/dist/agents/skills/builtin/git-ops.js.map +1 -1
- package/dist/agents/skills/builtin/planning.js +4 -4
- package/dist/agents/skills/builtin/planning.js.map +1 -1
- package/dist/agents/skills/builtin/refactoring.js +3 -3
- package/dist/agents/skills/builtin/refactoring.js.map +1 -1
- package/dist/agents/skills/builtin/review.js +9 -9
- package/dist/agents/skills/builtin/review.js.map +1 -1
- package/dist/agents/skills/builtin/testing.js +3 -3
- package/dist/agents/skills/builtin/testing.js.map +1 -1
- package/dist/agents/tools.js +2 -9
- package/dist/agents/tools.js.map +1 -1
- package/dist/cli/app.js +63 -29
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/components/ChatStream.js +40 -126
- package/dist/cli/components/ChatStream.js.map +1 -1
- package/dist/cli/components/CollapsibleToolOutput.js +8 -26
- package/dist/cli/components/CollapsibleToolOutput.js.map +1 -1
- package/dist/cli/components/Header.js +6 -18
- package/dist/cli/components/Header.js.map +1 -1
- package/dist/cli/components/LiveRegion.js +54 -0
- package/dist/cli/components/LiveRegion.js.map +1 -0
- package/dist/cli/components/Sidebar.js +33 -31
- package/dist/cli/components/Sidebar.js.map +1 -1
- package/dist/cli/components/StatusBar.js +42 -0
- package/dist/cli/components/StatusBar.js.map +1 -0
- package/dist/cli/components/ToolOutput.js +68 -0
- package/dist/cli/components/ToolOutput.js.map +1 -0
- package/dist/cli/components/toolFormat.js +195 -0
- package/dist/cli/components/toolFormat.js.map +1 -0
- package/dist/cli/grokOAuth.js +93 -75
- package/dist/cli/grokOAuth.js.map +1 -1
- package/dist/cli/hooks/chatState.js +139 -0
- package/dist/cli/hooks/chatState.js.map +1 -0
- package/dist/cli/hooks/eventsToMessages.js +1 -1
- package/dist/cli/hooks/eventsToMessages.js.map +1 -1
- package/dist/cli/hooks/messageHelpers.js +11 -4
- package/dist/cli/hooks/messageHelpers.js.map +1 -1
- package/dist/cli/hooks/useBatchedMessages.js +14 -10
- package/dist/cli/hooks/useBatchedMessages.js.map +1 -1
- package/dist/cli/hooks/useChatTurn.js +400 -87
- package/dist/cli/hooks/useChatTurn.js.map +1 -1
- package/dist/cli/hooks/useSession.js +23 -11
- package/dist/cli/hooks/useSession.js.map +1 -1
- package/dist/cli/hooks/useSlashDispatch.js +9 -1
- package/dist/cli/hooks/useSlashDispatch.js.map +1 -1
- package/dist/cli/main.bundled.js +16972 -15277
- package/dist/cli/main.bundled.js.map +4 -4
- package/dist/cli/main.js +35 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/mcp/mcpClient.js +152 -0
- package/dist/cli/mcp/mcpClient.js.map +1 -0
- package/dist/cli/mcp/mcpManager.js +139 -0
- package/dist/cli/mcp/mcpManager.js.map +1 -0
- package/dist/cli/refreshRegistry.js +9 -6
- package/dist/cli/refreshRegistry.js.map +1 -1
- package/dist/cli/skillsMd.js +155 -0
- package/dist/cli/skillsMd.js.map +1 -0
- package/dist/cli/slashHandlers/skills.js +16 -0
- package/dist/cli/slashHandlers/skills.js.map +1 -1
- package/dist/cli/toolRegistry.js +31 -0
- package/dist/cli/toolRegistry.js.map +1 -1
- package/dist/cli/wizard/index.js +1 -1
- package/dist/cli/workspace/paths.js +20 -13
- package/dist/cli/workspace/paths.js.map +1 -1
- package/dist/cli/workspace/storage.js +7 -3
- package/dist/cli/workspace/storage.js.map +1 -1
- package/dist/cli/workspace/stubs.js +122 -44
- package/dist/cli/workspace/stubs.js.map +1 -1
- package/dist/cli/workspace/workspaceSummary.js +255 -0
- package/dist/cli/workspace/workspaceSummary.js.map +1 -0
- package/dist/main/core/AgentHarness.js +36 -7
- package/dist/main/core/AgentHarness.js.map +1 -1
- package/dist/main/core/tools/builtin/listFiles.js +43 -5
- package/dist/main/core/tools/builtin/listFiles.js.map +1 -1
- package/dist/main/core/tools/builtin/search.js +28 -120
- package/dist/main/core/tools/builtin/search.js.map +1 -1
- package/package.json +2 -2
- package/dist/cli/oauthCallbackServer.js +0 -207
- package/dist/cli/oauthCallbackServer.js.map +0 -1
- package/dist/main/core/tools/builtin/_walk.js +0 -129
- package/dist/main/core/tools/builtin/_walk.js.map +0 -1
- package/dist/main/core/tools/builtin/diff.js +0 -477
- package/dist/main/core/tools/builtin/diff.js.map +0 -1
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
// @ts-nocheck — pre-existing strict-mode type narrowing issues carried over
|
|
2
2
|
// from app.tsx. Runtime is correct; tighten signatures in a follow-up.
|
|
3
|
-
import { useState, useRef, useCallback } from
|
|
4
|
-
import { AgentHarness } from
|
|
5
|
-
import { getMetricsLogger } from
|
|
6
|
-
import { openaiCompatibleProvider, providerFromEnv, providerConfigFor } from
|
|
7
|
-
import { providerFailover } from
|
|
8
|
-
import { resolveFailoverStream } from
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
3
|
+
import { useState, useRef, useCallback } from "react";
|
|
4
|
+
import { AgentHarness } from "@zelari/core/harness";
|
|
5
|
+
import { getMetricsLogger } from "../metrics.js";
|
|
6
|
+
import { openaiCompatibleProvider, providerFromEnv, providerConfigFor, resolveActiveProvider, } from "../provider/openai-compatible.js";
|
|
7
|
+
import { providerFailover } from "../providerFailover.js";
|
|
8
|
+
import { resolveFailoverStream } from "../crossProviderFailover.js";
|
|
9
|
+
import { resolveShell } from "@zelari/core/harness/tools/builtin/shellResolver";
|
|
10
|
+
import { PROVIDERS } from "../keyStore.js";
|
|
11
|
+
import { createBuiltinToolRegistry } from "../toolRegistry.js";
|
|
12
|
+
import { appendOrExtendStreamingAssistant, appendSystem, appendToolStart, finalizeStreamingAssistant, updateToolMessageEnd, } from "./messageHelpers.js";
|
|
13
|
+
import { setStreaming, finalizeStreaming, startTool, completeTool, } from "./chatState.js";
|
|
14
|
+
import { computeSessionStatsDelta } from "./chatStats.js";
|
|
13
15
|
export function useChatTurn(params) {
|
|
14
|
-
const { sessionId, writerRef, setMessages, commitStreaming, flushStreaming, setBusy, setSessionActive, setSessionStats } = params;
|
|
16
|
+
const { sessionId, writerRef, setMessages, commitStreaming, flushStreaming, setBusy, setSessionActive, setSessionStats, setLive, liveRef, } = params;
|
|
15
17
|
const harnessRef = useRef(null);
|
|
16
18
|
const [queueCount, setQueueCount] = useState(0);
|
|
17
|
-
|
|
19
|
+
// v0.7.0: when the live region is wired, streaming + tool events route
|
|
20
|
+
// there; otherwise we fall back to the v0.6 single-array behavior so the
|
|
21
|
+
// existing unit tests (which pass only setMessages/commitStreaming) keep
|
|
22
|
+
// asserting on `messages` directly.
|
|
23
|
+
const useLiveModel = !!(setLive && liveRef);
|
|
24
|
+
const dispatchPrompt = useCallback(async (userText, opts) => {
|
|
18
25
|
// v0.4.3 audit fix: provider resolution + harness construction now
|
|
19
26
|
// live INSIDE the try block. Previously, throws from providerFromEnv,
|
|
20
27
|
// resolveFailoverStream, or createBuiltinToolRegistry happened
|
|
@@ -27,14 +34,18 @@ export function useChatTurn(params) {
|
|
|
27
34
|
try {
|
|
28
35
|
envConfig = await providerFromEnv();
|
|
29
36
|
if (!envConfig) {
|
|
30
|
-
|
|
37
|
+
// Name the ACTIVE provider — the old hardcoded "OPENAI_API_KEY not
|
|
38
|
+
// set" message told grok/glm/minimax users to export the wrong var.
|
|
39
|
+
const active = resolveActiveProvider();
|
|
40
|
+
const spec = PROVIDERS.find((p) => p.id === active);
|
|
41
|
+
appendSystem(setMessages, `No API key for the active provider "${active}". Set ${spec?.envVar ?? "the provider API key env var"} or run /login ${active}.`);
|
|
31
42
|
return;
|
|
32
43
|
}
|
|
33
44
|
setBusy(true);
|
|
34
45
|
const { registry: toolRegistry } = createBuiltinToolRegistry();
|
|
35
46
|
const baseProviderStream = openaiCompatibleProvider(envConfig);
|
|
36
47
|
const failoverResolution = await resolveFailoverStream({
|
|
37
|
-
failoverEnabled: process.env.ANATHEMA_FAILOVER !==
|
|
48
|
+
failoverEnabled: process.env.ANATHEMA_FAILOVER !== "0",
|
|
38
49
|
envValue: process.env.ANATHEMA_FAILOVER_PROVIDER,
|
|
39
50
|
primaryProviderId: envConfig.providerId,
|
|
40
51
|
primary: baseProviderStream,
|
|
@@ -58,13 +69,101 @@ export function useChatTurn(params) {
|
|
|
58
69
|
fallback: failoverResolution.fallback,
|
|
59
70
|
});
|
|
60
71
|
const cwd = process.cwd();
|
|
72
|
+
// v0.7.3: surface the council plan (if any) to the single agent too.
|
|
73
|
+
// The plan lives in .zelari/plan.json but the agent had no idea it
|
|
74
|
+
// existed — users had to paste task-file paths by hand. Best-effort:
|
|
75
|
+
// no plan → null → zero prompt-token cost.
|
|
76
|
+
let planSummary = null;
|
|
77
|
+
// v0.7.4: give the single agent the same project awareness the council
|
|
78
|
+
// has (tech stack, file layout, scripts) plus a pointer to the council
|
|
79
|
+
// workspace so it reads .zelari/plan.json with its own tools.
|
|
80
|
+
let workspaceSummary = null;
|
|
81
|
+
let zelariReadHint = '';
|
|
82
|
+
try {
|
|
83
|
+
const { buildPlanSummary, buildWorkspaceSummary, buildZelariReadHint } = await import('../workspace/workspaceSummary.js');
|
|
84
|
+
planSummary = buildPlanSummary(cwd);
|
|
85
|
+
workspaceSummary = buildWorkspaceSummary(cwd);
|
|
86
|
+
zelariReadHint = buildZelariReadHint(cwd);
|
|
87
|
+
// v0.7.4: close the plan loop. The single agent implements the tasks
|
|
88
|
+
// the council planned, but had no official way to advance their
|
|
89
|
+
// status — it would have to hand-edit plan.json with write_file
|
|
90
|
+
// (racy, no validation). Register the workspace `updateTask` stub so
|
|
91
|
+
// status changes go through the same mutex + atomic plan.json write
|
|
92
|
+
// the council uses. Only when a plan exists: fresh projects don't pay
|
|
93
|
+
// the extra tool-schema prompt tokens.
|
|
94
|
+
// v0.7.5: also register any workspace stubs a /skill invocation
|
|
95
|
+
// requires (opts.requiredTools), mapping the Electron-era `searchRAG`
|
|
96
|
+
// to the CLI's `searchDocuments`.
|
|
97
|
+
const wantedWorkspaceTools = new Set();
|
|
98
|
+
if (planSummary)
|
|
99
|
+
wantedWorkspaceTools.add('updateTask');
|
|
100
|
+
const WORKSPACE_STUB_NAMES = new Set([
|
|
101
|
+
'createPhase', 'createTask', 'updateTask', 'addIdea', 'createMilestone',
|
|
102
|
+
'createDocument', 'searchDocuments', 'linkDocuments', 'getDocumentBacklinks',
|
|
103
|
+
]);
|
|
104
|
+
for (const raw of opts?.requiredTools ?? []) {
|
|
105
|
+
const name = raw === 'searchRAG' ? 'searchDocuments' : raw;
|
|
106
|
+
if (WORKSPACE_STUB_NAMES.has(name))
|
|
107
|
+
wantedWorkspaceTools.add(name);
|
|
108
|
+
}
|
|
109
|
+
if (wantedWorkspaceTools.size > 0) {
|
|
110
|
+
const { createWorkspaceContext } = await import('../workspace/stubs.js');
|
|
111
|
+
const { createWorkspaceToolRegistry } = await import('../workspace/toolRegistry.js');
|
|
112
|
+
const wsRegistry = createWorkspaceToolRegistry(createWorkspaceContext(cwd));
|
|
113
|
+
for (const name of wantedWorkspaceTools) {
|
|
114
|
+
const td = wsRegistry.get(name);
|
|
115
|
+
if (td)
|
|
116
|
+
toolRegistry.register(td);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// v0.7.5: MCP tools. Discovery runs once per process (lazy singleton);
|
|
120
|
+
// per-turn cost after that is just re-registering into the fresh
|
|
121
|
+
// registry. Disabled with ZELARI_MCP=0. Best-effort like the rest.
|
|
122
|
+
try {
|
|
123
|
+
const { registerMcpTools } = await import('../mcp/mcpManager.js');
|
|
124
|
+
const mcp = await registerMcpTools(toolRegistry, cwd);
|
|
125
|
+
for (const w of mcp.warnings)
|
|
126
|
+
appendSystem(setMessages, w);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// MCP is an enhancement — a broken server config must not block prompts.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Plan summary is a nice-to-have — never block a prompt on it.
|
|
134
|
+
}
|
|
135
|
+
// NOTE: computed AFTER the workspace wiring so updateTask (when
|
|
136
|
+
// registered) is advertised in the # Available Tools section too.
|
|
61
137
|
const toolList = toolRegistry.toOpenAITools().map((t) => `- ${t.function.name}: ${t.function.description}`).join('\n');
|
|
138
|
+
// v0.7.2 (C3): platform-aware shell guidance. The model must know which
|
|
139
|
+
// shell the `bash` tool actually runs in so it writes the right commands
|
|
140
|
+
// (POSIX for Git Bash, Windows-native for cmd.exe fallback).
|
|
141
|
+
const resolvedShell = resolveShell();
|
|
142
|
+
const isWindows = process.platform === 'win32';
|
|
143
|
+
const shellGuidance = resolvedShell.isBash
|
|
144
|
+
? `The bash tool runs commands via Git Bash / MSYS2 (${resolvedShell.shell}). Write POSIX commands: ls, grep, $VAR, &&, /c/Users/... all work.`
|
|
145
|
+
: isWindows
|
|
146
|
+
? `The bash tool runs commands via cmd.exe (Git Bash not found). Write Windows-native commands: use dir (not ls), %VAR% (not $VAR), avoid POSIX-only syntax.`
|
|
147
|
+
: `The bash tool runs commands via /bin/sh.`;
|
|
148
|
+
// v0.7.3: the shell has NO interactive stdin. Without this warning the
|
|
149
|
+
// model retried `npm create vite` four times against the interactive
|
|
150
|
+
// prompt ("Operation cancelled") and then gave up asking the user.
|
|
151
|
+
const nonInteractiveGuidance = 'The shell is NON-INTERACTIVE (stdin closed): commands that prompt for input fail immediately. ' +
|
|
152
|
+
'Always pass non-interactive flags (--yes, -y, --template, --force). ' +
|
|
153
|
+
'If a scaffolder still insists on prompting (e.g. `npm create vite` in a non-empty directory), do NOT retry it — ' +
|
|
154
|
+
'scaffold into a fresh empty subdirectory and move the files, or write package.json/configs/sources yourself with write_file, then run `npm install`.';
|
|
62
155
|
const systemPrompt = [
|
|
63
156
|
"You are Zelari Code, an interactive AI coding agent operating directly in the user's terminal.",
|
|
64
157
|
'',
|
|
65
158
|
'You ARE connected to this machine and have real tools to read, modify, and explore the codebase.',
|
|
66
159
|
"Never claim you lack filesystem or shell access — you have it. Use your tools instead of asking the user to paste file contents.",
|
|
67
160
|
'',
|
|
161
|
+
'# Platform & Shell',
|
|
162
|
+
`platform: ${process.platform}`,
|
|
163
|
+
`shell: ${resolvedShell.via}`,
|
|
164
|
+
shellGuidance,
|
|
165
|
+
nonInteractiveGuidance,
|
|
166
|
+
'',
|
|
68
167
|
'# Working Directory',
|
|
69
168
|
`You are running in: ${cwd}`,
|
|
70
169
|
'All relative file paths are resolved against this directory. Always work with real files here.',
|
|
@@ -72,12 +171,32 @@ export function useChatTurn(params) {
|
|
|
72
171
|
'# Available Tools',
|
|
73
172
|
'You can call these tools. Use them to take action and gather information autonomously:',
|
|
74
173
|
toolList,
|
|
174
|
+
...(workspaceSummary ? ['', workspaceSummary] : []),
|
|
175
|
+
...(zelariReadHint ? ['', zelariReadHint] : []),
|
|
176
|
+
...(planSummary ? ['', planSummary] : []),
|
|
75
177
|
'',
|
|
76
178
|
'# Guidelines',
|
|
77
|
-
'- When the user asks you to write code, debug, or explore, be proactive: list files (bash: ls
|
|
179
|
+
'- When the user asks you to write code, debug, or explore, be proactive: list files (list_files, or bash: ls/dir) and read key files (read_file) to understand the project instead of asking the user to paste file contents.',
|
|
180
|
+
'- If a command fails or is cancelled, do NOT retry variants of the same command: diagnose why (read the hint in the result if present) and take a DIFFERENT approach (e.g. create the files yourself with write_file).',
|
|
181
|
+
'- After a tool result, CONTINUE your answer from where you left off. NEVER restate or re-print text you already wrote earlier in this turn.',
|
|
78
182
|
"- Only invoke tools when they are necessary to answer the user's prompt. If the user is just saying hello or greeting them (e.g., \"ciao\", \"hello\"), simply greet them back and ask how you can help, without running any commands or tools.",
|
|
79
183
|
'- When you finish a task, briefly summarize what you did.',
|
|
184
|
+
...(planSummary
|
|
185
|
+
? [
|
|
186
|
+
'- Plan tasks: when you START working on a plan task call updateTask {taskId, status: "in_progress"}; when it is complete and verified call updateTask {taskId, status: "done"}. NEVER edit .zelari/plan.json by hand.',
|
|
187
|
+
]
|
|
188
|
+
: []),
|
|
80
189
|
].join('\n');
|
|
190
|
+
// v0.7.1 (A2): per-turn tool-call budget for single-prompt turns.
|
|
191
|
+
// The council sets 5; the single-prompt path previously set NONE, so a
|
|
192
|
+
// flailing model could loop for the full MAX_TOOL_LOOP_ITERATIONS (12)
|
|
193
|
+
// of junk calls (e.g. read_file same path ×3 then silence). Default 25,
|
|
194
|
+
// overridable via ZELARI_MAX_TOOL_CALLS.
|
|
195
|
+
const maxToolCallsPerTurn = (() => {
|
|
196
|
+
const raw = process.env.ZELARI_MAX_TOOL_CALLS;
|
|
197
|
+
const n = raw ? Number.parseInt(raw, 10) : 25;
|
|
198
|
+
return Number.isFinite(n) && n > 0 ? n : 25;
|
|
199
|
+
})();
|
|
81
200
|
const harness = new AgentHarness({
|
|
82
201
|
model: envConfig.model,
|
|
83
202
|
provider: 'openai-compatible',
|
|
@@ -93,6 +212,7 @@ export function useChatTurn(params) {
|
|
|
93
212
|
toolRegistry,
|
|
94
213
|
providerStream,
|
|
95
214
|
cwd,
|
|
215
|
+
maxToolCallsPerTurn,
|
|
96
216
|
});
|
|
97
217
|
harnessRef.current = harness;
|
|
98
218
|
setQueueCount(harness.queueLength);
|
|
@@ -117,7 +237,10 @@ export function useChatTurn(params) {
|
|
|
117
237
|
// Message boundary: drain buffered deltas, then seal the streamed
|
|
118
238
|
// bubble so the next message starts fresh instead of merging.
|
|
119
239
|
flushStreaming();
|
|
120
|
-
|
|
240
|
+
if (useLiveModel)
|
|
241
|
+
finalizeStreaming(setMessages, setLive);
|
|
242
|
+
else
|
|
243
|
+
finalizeStreamingAssistant(setMessages);
|
|
121
244
|
streamContent = '';
|
|
122
245
|
}
|
|
123
246
|
if (event.type === 'queue_update') {
|
|
@@ -162,12 +285,19 @@ export function useChatTurn(params) {
|
|
|
162
285
|
streamContent += event.delta;
|
|
163
286
|
// Route through the throttled setter so per-token deltas (50-200/sec)
|
|
164
287
|
// coalesce into ≤60 renders/sec instead of flickering the TUI.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
288
|
+
if (useLiveModel) {
|
|
289
|
+
setStreaming(commitStreaming, streamContent, Date.now(), {
|
|
290
|
+
...(event.memberId ? { memberId: event.memberId } : {}),
|
|
291
|
+
...(event.memberName ? { memberName: event.memberName } : {}),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
// Legacy single-array fallback (existing tests).
|
|
296
|
+
appendOrExtendStreamingAssistant(commitStreaming, streamContent, Date.now(), {
|
|
297
|
+
...(event.memberId ? { memberId: event.memberId } : {}),
|
|
298
|
+
...(event.memberName ? { memberName: event.memberName } : {}),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
171
301
|
}
|
|
172
302
|
else if (event.type === 'error') {
|
|
173
303
|
flushStreaming();
|
|
@@ -179,11 +309,26 @@ export function useChatTurn(params) {
|
|
|
179
309
|
// call renders above the tool line, not below it — then seal
|
|
180
310
|
// that bubble: it's complete once the model starts calling tools.
|
|
181
311
|
flushStreaming();
|
|
182
|
-
|
|
183
|
-
|
|
312
|
+
if (useLiveModel) {
|
|
313
|
+
finalizeStreaming(setMessages, setLive);
|
|
314
|
+
startTool(setLive, event.toolName, event.toolCallId, event.args, event.ts);
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
finalizeStreamingAssistant(setMessages);
|
|
318
|
+
appendToolStart(setMessages, event.toolName, event.toolCallId, event.args, event.ts);
|
|
319
|
+
}
|
|
320
|
+
// The pre-tool bubble is sealed: reset the display buffer so the
|
|
321
|
+
// next delta starts a fresh bubble instead of re-showing (and
|
|
322
|
+
// duplicating) the text already printed above the tool line.
|
|
323
|
+
streamContent = '';
|
|
184
324
|
}
|
|
185
325
|
else if (event.type === 'tool_execution_end') {
|
|
186
|
-
|
|
326
|
+
if (useLiveModel) {
|
|
327
|
+
completeTool(setMessages, setLive, event.toolCallId, event.isError, event.durationMs, event.result);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
updateToolMessageEnd(setMessages, event.toolCallId, event.isError, event.durationMs, event.result);
|
|
331
|
+
}
|
|
187
332
|
}
|
|
188
333
|
}
|
|
189
334
|
}
|
|
@@ -191,7 +336,10 @@ export function useChatTurn(params) {
|
|
|
191
336
|
// Drain any buffered streaming deltas so the final assistant message
|
|
192
337
|
// is committed before busy flips to false (and the input re-enables).
|
|
193
338
|
flushStreaming();
|
|
194
|
-
|
|
339
|
+
if (useLiveModel)
|
|
340
|
+
finalizeStreaming(setMessages, setLive);
|
|
341
|
+
else
|
|
342
|
+
finalizeStreamingAssistant(setMessages);
|
|
195
343
|
harnessRef.current = null;
|
|
196
344
|
setQueueCount(0);
|
|
197
345
|
setBusy(false);
|
|
@@ -209,7 +357,19 @@ export function useChatTurn(params) {
|
|
|
209
357
|
appendSystem(setMessages, `[dispatch error] ${err instanceof Error ? err.message : String(err)}`);
|
|
210
358
|
setBusy(false);
|
|
211
359
|
}
|
|
212
|
-
}, [
|
|
360
|
+
}, [
|
|
361
|
+
sessionId,
|
|
362
|
+
writerRef,
|
|
363
|
+
setMessages,
|
|
364
|
+
commitStreaming,
|
|
365
|
+
flushStreaming,
|
|
366
|
+
setBusy,
|
|
367
|
+
setSessionActive,
|
|
368
|
+
setSessionStats,
|
|
369
|
+
useLiveModel,
|
|
370
|
+
setLive,
|
|
371
|
+
liveRef,
|
|
372
|
+
]);
|
|
213
373
|
const dispatchCouncilPrompt = useCallback(async (text) => {
|
|
214
374
|
await dispatchCouncilPromptImpl(text, {
|
|
215
375
|
sessionId,
|
|
@@ -219,15 +379,33 @@ export function useChatTurn(params) {
|
|
|
219
379
|
flushStreaming,
|
|
220
380
|
setBusy,
|
|
221
381
|
setQueueCount,
|
|
382
|
+
setLive,
|
|
383
|
+
liveRef,
|
|
222
384
|
});
|
|
223
|
-
}, [
|
|
224
|
-
|
|
385
|
+
}, [
|
|
386
|
+
sessionId,
|
|
387
|
+
writerRef,
|
|
388
|
+
setMessages,
|
|
389
|
+
commitStreaming,
|
|
390
|
+
flushStreaming,
|
|
391
|
+
setBusy,
|
|
392
|
+
setQueueCount,
|
|
393
|
+
setLive,
|
|
394
|
+
liveRef,
|
|
395
|
+
]);
|
|
396
|
+
return {
|
|
397
|
+
dispatchPrompt,
|
|
398
|
+
dispatchCouncilPrompt,
|
|
399
|
+
harnessRef,
|
|
400
|
+
queueCount,
|
|
401
|
+
setQueueCount,
|
|
402
|
+
};
|
|
225
403
|
}
|
|
226
404
|
/**
|
|
227
405
|
* dispatchCouncilPrompt — multi-agent council dispatch.
|
|
228
406
|
*
|
|
229
|
-
* Surfaces tool_execution_start/end as 'tool' role messages so
|
|
230
|
-
* renders
|
|
407
|
+
* Surfaces tool_execution_start/end as 'tool' role messages so the live
|
|
408
|
+
* region renders them. Runs AGENTS.MD auto-maintenance after the
|
|
231
409
|
* council finishes (controlled by ZELARI_AGENTS_MD env var).
|
|
232
410
|
*
|
|
233
411
|
* Implementation lives outside the hook closure so it doesn't depend on the
|
|
@@ -235,20 +413,24 @@ export function useChatTurn(params) {
|
|
|
235
413
|
* useChatTurn wrapper.
|
|
236
414
|
*/
|
|
237
415
|
async function dispatchCouncilPromptImpl(text, deps) {
|
|
238
|
-
const { sessionId, writerRef, setMessages, commitStreaming, flushStreaming, setBusy } = deps;
|
|
416
|
+
const { sessionId, writerRef, setMessages, commitStreaming, flushStreaming, setBusy, setLive, liveRef, } = deps;
|
|
417
|
+
const useLiveModel = !!(setLive && liveRef);
|
|
239
418
|
const envConfig = await providerFromEnv();
|
|
240
419
|
if (!envConfig) {
|
|
241
|
-
|
|
420
|
+
const active = resolveActiveProvider();
|
|
421
|
+
const spec = PROVIDERS.find((p) => p.id === active);
|
|
422
|
+
appendSystem(setMessages, `No API key for the active provider "${active}". Set ${spec?.envVar ?? "the provider API key env var"} or run /login ${active} before invoking /council.`);
|
|
242
423
|
return;
|
|
243
424
|
}
|
|
244
425
|
setBusy(true);
|
|
245
426
|
// Import dynamically to avoid a circular dep at module-load time.
|
|
246
|
-
const { dispatchCouncil } = await import(
|
|
247
|
-
const { createWorkspaceContext, createWorkspaceStubs } = await import(
|
|
248
|
-
const { createWorkspaceToolRegistry } = await import(
|
|
249
|
-
const { setWorkspaceStubs } = await import(
|
|
250
|
-
const { runPostCouncilHook } = await import(
|
|
251
|
-
const {
|
|
427
|
+
const { dispatchCouncil } = await import("../councilDispatcher.js");
|
|
428
|
+
const { createWorkspaceContext, createWorkspaceStubs } = await import("../workspace/stubs.js");
|
|
429
|
+
const { createWorkspaceToolRegistry } = await import("../workspace/toolRegistry.js");
|
|
430
|
+
const { setWorkspaceStubs } = await import("@zelari/core/skills");
|
|
431
|
+
const { runPostCouncilHook } = await import("../workspace/postCouncilHook.js");
|
|
432
|
+
const { buildWorkspaceSummary, buildPlanSummary } = await import("../workspace/workspaceSummary.js");
|
|
433
|
+
const { FeedbackStore } = await import("../councilFeedback.js");
|
|
252
434
|
const { registry: councilToolRegistry } = createBuiltinToolRegistry();
|
|
253
435
|
const workspaceCtx = createWorkspaceContext();
|
|
254
436
|
const workspaceReg = createWorkspaceToolRegistry(workspaceCtx);
|
|
@@ -257,71 +439,190 @@ async function dispatchCouncilPromptImpl(text, deps) {
|
|
|
257
439
|
if (td)
|
|
258
440
|
councilToolRegistry.register(td);
|
|
259
441
|
}
|
|
442
|
+
// v0.7.5: MCP tools for the council too (same lazy singleton as the
|
|
443
|
+
// single-agent path — zero extra spawns).
|
|
444
|
+
try {
|
|
445
|
+
const { registerMcpTools } = await import('../mcp/mcpManager.js');
|
|
446
|
+
const mcp = await registerMcpTools(councilToolRegistry);
|
|
447
|
+
for (const w of mcp.warnings)
|
|
448
|
+
appendSystem(setMessages, w);
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
// Best-effort.
|
|
452
|
+
}
|
|
260
453
|
setWorkspaceStubs(createWorkspaceStubs(workspaceCtx));
|
|
261
454
|
const councilFeedbackStore = new FeedbackStore();
|
|
455
|
+
// v0.7.3: per-member display accumulator for the streaming bubble.
|
|
456
|
+
// The previous code accumulated by reading `liveRef.current.streaming` —
|
|
457
|
+
// but that ref only updates on render (useEffect) and the delta commits go
|
|
458
|
+
// through a 16ms throttle window, so every delta inside a window computed
|
|
459
|
+
// staleContent+delta and the LAST write won: most tokens were silently
|
|
460
|
+
// dropped (the mangled member text from the 2026-07-02 live test).
|
|
461
|
+
// The accumulator lives here, in the event loop, exactly like
|
|
462
|
+
// `streamContent` in dispatchPrompt.
|
|
463
|
+
let streamContent = "";
|
|
464
|
+
let streamMemberId = null;
|
|
465
|
+
// v0.7.3: council members legitimately need more than the core default of
|
|
466
|
+
// 5 tool calls per turn (a planner creating 8 tasks got 3 of them skipped
|
|
467
|
+
// with "[skipped] maxToolCallsPerTurn reached"). Same env override as the
|
|
468
|
+
// single-prompt path.
|
|
469
|
+
const councilMaxToolCalls = (() => {
|
|
470
|
+
const raw = process.env.ZELARI_MAX_TOOL_CALLS;
|
|
471
|
+
const n = raw ? Number.parseInt(raw, 10) : 15;
|
|
472
|
+
return Number.isFinite(n) && n > 0 ? n : 15;
|
|
473
|
+
})();
|
|
474
|
+
// v0.7.1 (A3): track member completion so the AGENTS.MD hook only runs when
|
|
475
|
+
// the council actually produced output. v0.7.1 (A4): track repeated provider
|
|
476
|
+
// errors to abort the remaining members instead of grinding through every
|
|
477
|
+
// specialist after the API is clearly broken.
|
|
478
|
+
let membersCompleted = 0;
|
|
479
|
+
let chairmanProducedOutput = false;
|
|
480
|
+
let consecutiveProviderErrors = 0;
|
|
481
|
+
let lastErrorMessage = "";
|
|
482
|
+
let councilAborted = false;
|
|
483
|
+
const PROVIDER_ERROR_ABORT_THRESHOLD = 2;
|
|
262
484
|
try {
|
|
263
485
|
for await (const event of dispatchCouncil(text, {
|
|
264
486
|
apiKey: envConfig.apiKey,
|
|
265
487
|
model: envConfig.model,
|
|
266
|
-
provider:
|
|
488
|
+
provider: "openai-compatible",
|
|
267
489
|
providerStream: openaiCompatibleProvider(envConfig),
|
|
268
490
|
sessionId,
|
|
269
491
|
tools: councilToolRegistry,
|
|
270
492
|
feedbackStore: councilFeedbackStore,
|
|
493
|
+
// v0.7.2 (B2): give the council the same project awareness the
|
|
494
|
+
// single-prompt path has — cwd, tech stack, file layout, build scripts.
|
|
495
|
+
// Without this, members had no idea which project they were operating
|
|
496
|
+
// on and projected their identity onto the task.
|
|
497
|
+
// v0.7.3: append the existing plan (if any) so a follow-up /council
|
|
498
|
+
// continues it instead of re-planning from scratch.
|
|
499
|
+
workspaceContext: [
|
|
500
|
+
buildWorkspaceSummary(process.cwd()),
|
|
501
|
+
buildPlanSummary(process.cwd()),
|
|
502
|
+
]
|
|
503
|
+
.filter(Boolean)
|
|
504
|
+
.join("\n\n"),
|
|
505
|
+
maxToolCallsPerTurn: councilMaxToolCalls,
|
|
271
506
|
})) {
|
|
507
|
+
if (councilAborted) {
|
|
508
|
+
// Drain remaining events silently after the abort decision.
|
|
509
|
+
if (writerRef.current)
|
|
510
|
+
await writerRef.current.append(event);
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
272
513
|
if (writerRef.current) {
|
|
273
514
|
await writerRef.current.append(event);
|
|
274
515
|
}
|
|
275
|
-
if (event.type ===
|
|
516
|
+
if (event.type === "message_delta") {
|
|
276
517
|
// Coalesce streaming assistant content through the throttled setter so
|
|
277
518
|
// per-token deltas don't flicker the TUI (same as dispatchPrompt).
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
(
|
|
289
|
-
|
|
519
|
+
if (useLiveModel) {
|
|
520
|
+
// v0.7.3: accumulate in the local `streamContent` (NOT via liveRef —
|
|
521
|
+
// stale under the throttle, see the accumulator comment above) and
|
|
522
|
+
// always push the FULL content: dropped intermediate commits are
|
|
523
|
+
// then harmless because the last one supersedes them.
|
|
524
|
+
const memberId = event.memberId ?? null;
|
|
525
|
+
if (memberId !== streamMemberId) {
|
|
526
|
+
// Member boundary without a message_end (defensive): seal the
|
|
527
|
+
// previous member's bubble before starting the new one.
|
|
528
|
+
flushStreaming();
|
|
529
|
+
finalizeStreaming(setMessages, setLive);
|
|
530
|
+
streamContent = "";
|
|
531
|
+
streamMemberId = memberId;
|
|
290
532
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
{
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
533
|
+
streamContent += event.delta;
|
|
534
|
+
setStreaming(commitStreaming, streamContent, event.ts, {
|
|
535
|
+
...(event.memberId ? { memberId: event.memberId } : {}),
|
|
536
|
+
...(event.memberName ? { memberName: event.memberName } : {}),
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
// Legacy single-array fallback. Extend the trailing streaming bubble
|
|
541
|
+
// only when it belongs to the SAME member — otherwise one specialist's
|
|
542
|
+
// text would be appended to (and attributed to) the previous one.
|
|
543
|
+
commitStreaming((prev) => {
|
|
544
|
+
const last = prev[prev.length - 1];
|
|
545
|
+
if (last &&
|
|
546
|
+
last.role === "assistant" &&
|
|
547
|
+
last.id.startsWith("streaming-") &&
|
|
548
|
+
(last.memberId ?? null) === (event.memberId ?? null)) {
|
|
549
|
+
return [
|
|
550
|
+
...prev.slice(0, -1),
|
|
551
|
+
{ ...last, content: last.content + event.delta },
|
|
552
|
+
];
|
|
553
|
+
}
|
|
554
|
+
return [
|
|
555
|
+
...prev,
|
|
556
|
+
{
|
|
557
|
+
id: `streaming-${crypto.randomUUID()}`,
|
|
558
|
+
role: "assistant",
|
|
559
|
+
content: event.delta,
|
|
560
|
+
ts: event.ts,
|
|
561
|
+
...(event.memberId ? { memberId: event.memberId } : {}),
|
|
562
|
+
...(event.memberName ? { memberName: event.memberName } : {}),
|
|
563
|
+
},
|
|
564
|
+
];
|
|
565
|
+
});
|
|
566
|
+
}
|
|
303
567
|
}
|
|
304
|
-
else if (event.type ===
|
|
568
|
+
else if (event.type === "message_end") {
|
|
305
569
|
// Member/turn boundary: drain buffered deltas and seal the bubble so
|
|
306
570
|
// the next streamed message starts fresh.
|
|
307
571
|
flushStreaming();
|
|
308
|
-
|
|
572
|
+
if (useLiveModel)
|
|
573
|
+
finalizeStreaming(setMessages, setLive);
|
|
574
|
+
else
|
|
575
|
+
finalizeStreamingAssistant(setMessages);
|
|
576
|
+
streamContent = "";
|
|
577
|
+
streamMemberId = null;
|
|
578
|
+
membersCompleted++;
|
|
579
|
+
// Chairman is the last member; any assistant content from it counts.
|
|
580
|
+
if (event.memberId === "lucifer" || event.memberName === "Lucifero") {
|
|
581
|
+
chairmanProducedOutput = true;
|
|
582
|
+
}
|
|
309
583
|
}
|
|
310
|
-
else if (event.type ===
|
|
311
|
-
// Single 'tool' role message per invocation (CollapsibleToolOutput);
|
|
312
|
-
// the previous extra `▶ name(args)` system line duplicated it.
|
|
584
|
+
else if (event.type === "tool_execution_start") {
|
|
313
585
|
// Drain buffered deltas first so ordering matches reality, and seal
|
|
314
586
|
// the pre-tool bubble (complete once the member starts calling tools).
|
|
315
587
|
flushStreaming();
|
|
316
|
-
|
|
317
|
-
|
|
588
|
+
if (useLiveModel) {
|
|
589
|
+
finalizeStreaming(setMessages, setLive);
|
|
590
|
+
startTool(setLive, event.toolName, event.toolCallId, event.args, event.ts);
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
finalizeStreamingAssistant(setMessages);
|
|
594
|
+
appendToolStart(setMessages, event.toolName, event.toolCallId, event.args, event.ts);
|
|
595
|
+
}
|
|
596
|
+
// The pre-tool bubble is sealed: the next delta starts a fresh one.
|
|
597
|
+
streamContent = "";
|
|
318
598
|
}
|
|
319
|
-
else if (event.type ===
|
|
320
|
-
|
|
599
|
+
else if (event.type === "tool_execution_end") {
|
|
600
|
+
if (useLiveModel) {
|
|
601
|
+
completeTool(setMessages, setLive, event.toolCallId, event.isError, event.durationMs, event.result);
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
updateToolMessageEnd(setMessages, event.toolCallId, event.isError, event.durationMs, event.result);
|
|
605
|
+
}
|
|
321
606
|
}
|
|
322
|
-
else if (event.type ===
|
|
607
|
+
else if (event.type === "error") {
|
|
323
608
|
flushStreaming();
|
|
324
|
-
|
|
609
|
+
// v0.7.1 (A4): attribute the error to the member when known, so the
|
|
610
|
+
// user sees `[error · Caronte] …` instead of three anonymous lines.
|
|
611
|
+
const memberTag = event.memberName ? ` · ${event.memberName}` : "";
|
|
612
|
+
appendSystem(setMessages, `[error${memberTag}] ${event.message}`, event.ts);
|
|
613
|
+
// v0.7.1 (A4): detect repeated identical provider errors and abort the
|
|
614
|
+
// remaining members instead of grinding through every specialist.
|
|
615
|
+
if (event.message === lastErrorMessage) {
|
|
616
|
+
consecutiveProviderErrors++;
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
consecutiveProviderErrors = 1;
|
|
620
|
+
lastErrorMessage = event.message;
|
|
621
|
+
}
|
|
622
|
+
if (consecutiveProviderErrors >= PROVIDER_ERROR_ABORT_THRESHOLD) {
|
|
623
|
+
councilAborted = true;
|
|
624
|
+
appendSystem(setMessages, `[council aborted: repeated provider error — ${consecutiveProviderErrors}× "${event.message.slice(0, 80)}"]`, Date.now());
|
|
625
|
+
}
|
|
325
626
|
}
|
|
326
627
|
}
|
|
327
628
|
}
|
|
@@ -334,20 +635,32 @@ async function dispatchCouncilPromptImpl(text, deps) {
|
|
|
334
635
|
// Drain any buffered streaming deltas before status messages / busy flip,
|
|
335
636
|
// so the final council output is committed to the chat.
|
|
336
637
|
flushStreaming();
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
638
|
+
if (useLiveModel)
|
|
639
|
+
finalizeStreaming(setMessages, setLive);
|
|
640
|
+
else
|
|
641
|
+
finalizeStreamingAssistant(setMessages);
|
|
642
|
+
// v0.7.1 (A3): only auto-write AGENTS.MD when the council actually produced
|
|
643
|
+
// output. Running the hook after an all-error run (e.g. the HTTP 400 from
|
|
644
|
+
// A1) dirtied the working tree with sections rewritten from nothing.
|
|
645
|
+
const hookShouldRun = membersCompleted > 0 || chairmanProducedOutput;
|
|
646
|
+
if (hookShouldRun) {
|
|
647
|
+
try {
|
|
648
|
+
const hook = await runPostCouncilHook(workspaceCtx);
|
|
649
|
+
if (hook.ran && hook.changed) {
|
|
650
|
+
appendSystem(setMessages, `[agents.md] updated: ${hook.sections.length} section(s) changed (${hook.sections.join(", ")})`, Date.now());
|
|
651
|
+
}
|
|
652
|
+
else if (hook.ran && hook.reason) {
|
|
653
|
+
if (!hook.reason.includes("disabled")) {
|
|
654
|
+
appendSystem(setMessages, `[agents.md] ${hook.reason}`, Date.now());
|
|
655
|
+
}
|
|
346
656
|
}
|
|
347
657
|
}
|
|
658
|
+
catch {
|
|
659
|
+
// Best-effort — never block on AGENTS.MD errors.
|
|
660
|
+
}
|
|
348
661
|
}
|
|
349
|
-
|
|
350
|
-
|
|
662
|
+
else if (!councilAborted) {
|
|
663
|
+
appendSystem(setMessages, "[agents.md] skipped — council produced no output", Date.now());
|
|
351
664
|
}
|
|
352
665
|
setBusy(false);
|
|
353
666
|
}
|