zelari-code 0.4.3 → 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/README.md +42 -0
- 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 +72 -33
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/compaction.js.map +1 -1
- package/dist/cli/components/ChatStream.js +43 -82
- package/dist/cli/components/ChatStream.js.map +1 -1
- package/dist/cli/components/CollapsibleToolOutput.js +8 -21
- 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/councilDispatcher.js +1 -1
- package/dist/cli/councilDispatcher.js.map +1 -1
- package/dist/cli/grokOAuth.js +93 -75
- package/dist/cli/grokOAuth.js.map +1 -1
- package/dist/cli/headless.js +137 -0
- package/dist/cli/headless.js.map +1 -0
- package/dist/cli/hooks/chatState.js +139 -0
- package/dist/cli/hooks/chatState.js.map +1 -0
- package/dist/cli/hooks/eventsToMessages.js +42 -11
- package/dist/cli/hooks/eventsToMessages.js.map +1 -1
- package/dist/cli/hooks/messageHelpers.js +72 -15
- package/dist/cli/hooks/messageHelpers.js.map +1 -1
- package/dist/cli/hooks/useBatchedMessages.js +71 -0
- package/dist/cli/hooks/useBatchedMessages.js.map +1 -0
- package/dist/cli/hooks/useChatTurn.js +453 -84
- package/dist/cli/hooks/useChatTurn.js.map +1 -1
- package/dist/cli/hooks/useSession.js +24 -3
- package/dist/cli/hooks/useSession.js.map +1 -1
- package/dist/cli/hooks/useSlashDispatch.js +13 -2
- package/dist/cli/hooks/useSlashDispatch.js.map +1 -1
- package/dist/cli/main.bundled.js +10607 -7889
- package/dist/cli/main.bundled.js.map +4 -4
- package/dist/cli/main.js +120 -2
- 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/provider/openai-compatible.js +9 -2
- package/dist/cli/provider/openai-compatible.js.map +1 -1
- package/dist/cli/refreshRegistry.js +9 -6
- package/dist/cli/refreshRegistry.js.map +1 -1
- package/dist/cli/runHeadless.js +160 -0
- package/dist/cli/runHeadless.js.map +1 -0
- package/dist/cli/sessionManager.js +1 -1
- package/dist/cli/sessionManager.js.map +1 -1
- package/dist/cli/skillSuggest.js +1 -1
- package/dist/cli/skillsMd.js +155 -0
- package/dist/cli/skillsMd.js.map +1 -0
- package/dist/cli/slashHandlers/branch.js +11 -2
- package/dist/cli/slashHandlers/branch.js.map +1 -1
- package/dist/cli/slashHandlers/git.js +0 -75
- package/dist/cli/slashHandlers/git.js.map +1 -1
- package/dist/cli/slashHandlers/promoteMember.js +25 -0
- package/dist/cli/slashHandlers/promoteMember.js.map +1 -0
- package/dist/cli/slashHandlers/skills.js +16 -0
- package/dist/cli/slashHandlers/skills.js.map +1 -1
- package/dist/cli/slashHandlers/transcript.js +13 -0
- package/dist/cli/slashHandlers/transcript.js.map +1 -0
- package/dist/cli/slashHandlers/updater.js +40 -0
- package/dist/cli/slashHandlers/updater.js.map +1 -0
- package/dist/cli/toolRegistry.js +37 -6
- package/dist/cli/toolRegistry.js.map +1 -1
- package/dist/cli/wizard/firstRun.js +63 -0
- package/dist/cli/wizard/firstRun.js.map +1 -0
- package/dist/cli/wizard/index.js +150 -0
- package/dist/cli/wizard/index.js.map +1 -0
- package/dist/cli/wizard/runWizard.js +137 -0
- package/dist/cli/wizard/runWizard.js.map +1 -0
- package/dist/cli/wizard/useWizardState.js +151 -0
- package/dist/cli/wizard/useWizardState.js.map +1 -0
- 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/toolRegistry.js +2 -2
- package/dist/cli/workspace/toolRegistry.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 +6 -1
- 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, 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,
|
|
@@ -43,7 +54,9 @@ export function useChatTurn(params) {
|
|
|
43
54
|
buildStream: (config) => openaiCompatibleProvider(config),
|
|
44
55
|
});
|
|
45
56
|
if (failoverResolution.warning) {
|
|
46
|
-
console.warn
|
|
57
|
+
// Surface in the chat instead of console.warn: writes that bypass
|
|
58
|
+
// Ink force a full repaint of the TUI frame (visible flicker).
|
|
59
|
+
appendSystem(setMessages, `[failover] ${failoverResolution.warning}`);
|
|
47
60
|
}
|
|
48
61
|
const providerStream = failoverResolution.fallbackLabel
|
|
49
62
|
? providerFailover({
|
|
@@ -56,13 +69,101 @@ export function useChatTurn(params) {
|
|
|
56
69
|
fallback: failoverResolution.fallback,
|
|
57
70
|
});
|
|
58
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.
|
|
59
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`.';
|
|
60
155
|
const systemPrompt = [
|
|
61
156
|
"You are Zelari Code, an interactive AI coding agent operating directly in the user's terminal.",
|
|
62
157
|
'',
|
|
63
158
|
'You ARE connected to this machine and have real tools to read, modify, and explore the codebase.',
|
|
64
159
|
"Never claim you lack filesystem or shell access — you have it. Use your tools instead of asking the user to paste file contents.",
|
|
65
160
|
'',
|
|
161
|
+
'# Platform & Shell',
|
|
162
|
+
`platform: ${process.platform}`,
|
|
163
|
+
`shell: ${resolvedShell.via}`,
|
|
164
|
+
shellGuidance,
|
|
165
|
+
nonInteractiveGuidance,
|
|
166
|
+
'',
|
|
66
167
|
'# Working Directory',
|
|
67
168
|
`You are running in: ${cwd}`,
|
|
68
169
|
'All relative file paths are resolved against this directory. Always work with real files here.',
|
|
@@ -70,12 +171,32 @@ export function useChatTurn(params) {
|
|
|
70
171
|
'# Available Tools',
|
|
71
172
|
'You can call these tools. Use them to take action and gather information autonomously:',
|
|
72
173
|
toolList,
|
|
174
|
+
...(workspaceSummary ? ['', workspaceSummary] : []),
|
|
175
|
+
...(zelariReadHint ? ['', zelariReadHint] : []),
|
|
176
|
+
...(planSummary ? ['', planSummary] : []),
|
|
73
177
|
'',
|
|
74
178
|
'# Guidelines',
|
|
75
|
-
'- 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.',
|
|
76
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.",
|
|
77
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
|
+
: []),
|
|
78
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
|
+
})();
|
|
79
200
|
const harness = new AgentHarness({
|
|
80
201
|
model: envConfig.model,
|
|
81
202
|
provider: 'openai-compatible',
|
|
@@ -91,16 +212,36 @@ export function useChatTurn(params) {
|
|
|
91
212
|
toolRegistry,
|
|
92
213
|
providerStream,
|
|
93
214
|
cwd,
|
|
215
|
+
maxToolCallsPerTurn,
|
|
94
216
|
});
|
|
95
217
|
harnessRef.current = harness;
|
|
96
218
|
setQueueCount(harness.queueLength);
|
|
219
|
+
// Total assistant output across the whole turn — feeds the token/cost
|
|
220
|
+
// estimate fallback in computeSessionStatsDelta.
|
|
97
221
|
let assistantContent = '';
|
|
222
|
+
// Display buffer for the CURRENT streamed message only. Reset on every
|
|
223
|
+
// message_end: without this, the post-tool-call message re-rendered the
|
|
224
|
+
// full accumulated turn text, duplicating everything said before the
|
|
225
|
+
// tool ran.
|
|
226
|
+
let streamContent = '';
|
|
227
|
+
// tool_execution_end doesn't carry toolName — remember it from the
|
|
228
|
+
// matching start event (keyed by toolCallId) for metrics.
|
|
229
|
+
const toolNameById = new Map();
|
|
98
230
|
const metrics = getMetricsLogger();
|
|
99
231
|
let realUsage = null;
|
|
100
232
|
try {
|
|
101
233
|
for await (const event of harness.run()) {
|
|
102
|
-
if (event.type === 'message_end'
|
|
103
|
-
|
|
234
|
+
if (event.type === 'message_end') {
|
|
235
|
+
if (event.usage)
|
|
236
|
+
realUsage = event.usage;
|
|
237
|
+
// Message boundary: drain buffered deltas, then seal the streamed
|
|
238
|
+
// bubble so the next message starts fresh instead of merging.
|
|
239
|
+
flushStreaming();
|
|
240
|
+
if (useLiveModel)
|
|
241
|
+
finalizeStreaming(setMessages, setLive);
|
|
242
|
+
else
|
|
243
|
+
finalizeStreamingAssistant(setMessages);
|
|
244
|
+
streamContent = '';
|
|
104
245
|
}
|
|
105
246
|
if (event.type === 'queue_update') {
|
|
106
247
|
setQueueCount(harness.queueLength);
|
|
@@ -133,7 +274,7 @@ export function useChatTurn(params) {
|
|
|
133
274
|
sessionId,
|
|
134
275
|
provider: envConfig.providerId,
|
|
135
276
|
model: envConfig.model,
|
|
136
|
-
toolName: event.
|
|
277
|
+
toolName: toolNameById.get(event.toolCallId) ?? 'unknown',
|
|
137
278
|
toolCallId: event.toolCallId,
|
|
138
279
|
durationMs: event.durationMs,
|
|
139
280
|
ok: !event.isError,
|
|
@@ -141,26 +282,64 @@ export function useChatTurn(params) {
|
|
|
141
282
|
}
|
|
142
283
|
if (event.type === 'message_delta') {
|
|
143
284
|
assistantContent += event.delta;
|
|
144
|
-
|
|
285
|
+
streamContent += event.delta;
|
|
286
|
+
// Route through the throttled setter so per-token deltas (50-200/sec)
|
|
287
|
+
// coalesce into ≤60 renders/sec instead of flickering the TUI.
|
|
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
|
+
}
|
|
145
301
|
}
|
|
146
302
|
else if (event.type === 'error') {
|
|
303
|
+
flushStreaming();
|
|
147
304
|
appendSystem(setMessages, `[error] ${event.message}`, Date.now());
|
|
148
305
|
}
|
|
149
|
-
else if (event.type === 'tool_call') {
|
|
150
|
-
appendSystem(setMessages, `[tool_call] ${event.toolName}(${JSON.stringify(event.arguments).slice(0, 80)})`, event.ts);
|
|
151
|
-
}
|
|
152
|
-
else if (event.type === 'tool_result') {
|
|
153
|
-
appendSystem(setMessages, `[tool_result] ${event.toolName} → ${event.ok ? 'ok' : 'error'}`, event.ts);
|
|
154
|
-
}
|
|
155
306
|
else if (event.type === 'tool_execution_start') {
|
|
156
|
-
|
|
307
|
+
toolNameById.set(event.toolCallId, event.toolName);
|
|
308
|
+
// Drain buffered deltas FIRST so the text streamed before the
|
|
309
|
+
// call renders above the tool line, not below it — then seal
|
|
310
|
+
// that bubble: it's complete once the model starts calling tools.
|
|
311
|
+
flushStreaming();
|
|
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 = '';
|
|
157
324
|
}
|
|
158
325
|
else if (event.type === 'tool_execution_end') {
|
|
159
|
-
|
|
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
|
+
}
|
|
160
332
|
}
|
|
161
333
|
}
|
|
162
334
|
}
|
|
163
335
|
finally {
|
|
336
|
+
// Drain any buffered streaming deltas so the final assistant message
|
|
337
|
+
// is committed before busy flips to false (and the input re-enables).
|
|
338
|
+
flushStreaming();
|
|
339
|
+
if (useLiveModel)
|
|
340
|
+
finalizeStreaming(setMessages, setLive);
|
|
341
|
+
else
|
|
342
|
+
finalizeStreamingAssistant(setMessages);
|
|
164
343
|
harnessRef.current = null;
|
|
165
344
|
setQueueCount(0);
|
|
166
345
|
setBusy(false);
|
|
@@ -172,26 +351,61 @@ export function useChatTurn(params) {
|
|
|
172
351
|
// resolveFailoverStream / AgentHarness construction that were
|
|
173
352
|
// previously escaping the function unhandled. Surfaces the error
|
|
174
353
|
// to the chat instead of hanging silently.
|
|
354
|
+
// Flush first so any partial assistant content streamed before the
|
|
355
|
+
// throw is committed before the error message renders.
|
|
356
|
+
flushStreaming();
|
|
175
357
|
appendSystem(setMessages, `[dispatch error] ${err instanceof Error ? err.message : String(err)}`);
|
|
176
358
|
setBusy(false);
|
|
177
359
|
}
|
|
178
|
-
}, [
|
|
360
|
+
}, [
|
|
361
|
+
sessionId,
|
|
362
|
+
writerRef,
|
|
363
|
+
setMessages,
|
|
364
|
+
commitStreaming,
|
|
365
|
+
flushStreaming,
|
|
366
|
+
setBusy,
|
|
367
|
+
setSessionActive,
|
|
368
|
+
setSessionStats,
|
|
369
|
+
useLiveModel,
|
|
370
|
+
setLive,
|
|
371
|
+
liveRef,
|
|
372
|
+
]);
|
|
179
373
|
const dispatchCouncilPrompt = useCallback(async (text) => {
|
|
180
374
|
await dispatchCouncilPromptImpl(text, {
|
|
181
375
|
sessionId,
|
|
182
376
|
writerRef,
|
|
183
377
|
setMessages,
|
|
378
|
+
commitStreaming,
|
|
379
|
+
flushStreaming,
|
|
184
380
|
setBusy,
|
|
185
381
|
setQueueCount,
|
|
382
|
+
setLive,
|
|
383
|
+
liveRef,
|
|
186
384
|
});
|
|
187
|
-
}, [
|
|
188
|
-
|
|
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
|
+
};
|
|
189
403
|
}
|
|
190
404
|
/**
|
|
191
405
|
* dispatchCouncilPrompt — multi-agent council dispatch.
|
|
192
406
|
*
|
|
193
|
-
* Surfaces tool_execution_start/end as 'tool' role messages so
|
|
194
|
-
* 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
|
|
195
409
|
* council finishes (controlled by ZELARI_AGENTS_MD env var).
|
|
196
410
|
*
|
|
197
411
|
* Implementation lives outside the hook closure so it doesn't depend on the
|
|
@@ -199,20 +413,24 @@ export function useChatTurn(params) {
|
|
|
199
413
|
* useChatTurn wrapper.
|
|
200
414
|
*/
|
|
201
415
|
async function dispatchCouncilPromptImpl(text, deps) {
|
|
202
|
-
const { sessionId, writerRef, setMessages, setBusy } = deps;
|
|
416
|
+
const { sessionId, writerRef, setMessages, commitStreaming, flushStreaming, setBusy, setLive, liveRef, } = deps;
|
|
417
|
+
const useLiveModel = !!(setLive && liveRef);
|
|
203
418
|
const envConfig = await providerFromEnv();
|
|
204
419
|
if (!envConfig) {
|
|
205
|
-
|
|
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.`);
|
|
206
423
|
return;
|
|
207
424
|
}
|
|
208
425
|
setBusy(true);
|
|
209
426
|
// Import dynamically to avoid a circular dep at module-load time.
|
|
210
|
-
const { dispatchCouncil } = await import(
|
|
211
|
-
const { createWorkspaceContext, createWorkspaceStubs } = await import(
|
|
212
|
-
const { createWorkspaceToolRegistry } = await import(
|
|
213
|
-
const { setWorkspaceStubs } = await import(
|
|
214
|
-
const { runPostCouncilHook } = await import(
|
|
215
|
-
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");
|
|
216
434
|
const { registry: councilToolRegistry } = createBuiltinToolRegistry();
|
|
217
435
|
const workspaceCtx = createWorkspaceContext();
|
|
218
436
|
const workspaceReg = createWorkspaceToolRegistry(workspaceCtx);
|
|
@@ -221,77 +439,228 @@ async function dispatchCouncilPromptImpl(text, deps) {
|
|
|
221
439
|
if (td)
|
|
222
440
|
councilToolRegistry.register(td);
|
|
223
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
|
+
}
|
|
224
453
|
setWorkspaceStubs(createWorkspaceStubs(workspaceCtx));
|
|
225
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;
|
|
226
484
|
try {
|
|
227
485
|
for await (const event of dispatchCouncil(text, {
|
|
228
486
|
apiKey: envConfig.apiKey,
|
|
229
487
|
model: envConfig.model,
|
|
230
|
-
provider:
|
|
488
|
+
provider: "openai-compatible",
|
|
231
489
|
providerStream: openaiCompatibleProvider(envConfig),
|
|
232
490
|
sessionId,
|
|
233
491
|
tools: councilToolRegistry,
|
|
234
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,
|
|
235
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
|
+
}
|
|
236
513
|
if (writerRef.current) {
|
|
237
514
|
await writerRef.current.append(event);
|
|
238
515
|
}
|
|
239
|
-
if (event.type ===
|
|
240
|
-
// Coalesce streaming assistant content
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
516
|
+
if (event.type === "message_delta") {
|
|
517
|
+
// Coalesce streaming assistant content through the throttled setter so
|
|
518
|
+
// per-token deltas don't flicker the TUI (same as dispatchPrompt).
|
|
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;
|
|
245
532
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
+
}
|
|
251
567
|
}
|
|
252
|
-
else if (event.type ===
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
},
|
|
268
|
-
]);
|
|
568
|
+
else if (event.type === "message_end") {
|
|
569
|
+
// Member/turn boundary: drain buffered deltas and seal the bubble so
|
|
570
|
+
// the next streamed message starts fresh.
|
|
571
|
+
flushStreaming();
|
|
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
|
+
}
|
|
269
583
|
}
|
|
270
|
-
else if (event.type ===
|
|
271
|
-
|
|
584
|
+
else if (event.type === "tool_execution_start") {
|
|
585
|
+
// Drain buffered deltas first so ordering matches reality, and seal
|
|
586
|
+
// the pre-tool bubble (complete once the member starts calling tools).
|
|
587
|
+
flushStreaming();
|
|
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 = "";
|
|
272
598
|
}
|
|
273
|
-
else if (event.type ===
|
|
274
|
-
|
|
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
|
+
}
|
|
606
|
+
}
|
|
607
|
+
else if (event.type === "error") {
|
|
608
|
+
flushStreaming();
|
|
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
|
+
}
|
|
275
626
|
}
|
|
276
627
|
}
|
|
277
628
|
}
|
|
278
629
|
catch (err) {
|
|
630
|
+
// Flush any partial streamed content before the error message renders.
|
|
631
|
+
flushStreaming();
|
|
279
632
|
appendSystem(setMessages, `[council error] ${err instanceof Error ? err.message : String(err)}`, Date.now());
|
|
280
633
|
}
|
|
281
634
|
finally {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
635
|
+
// Drain any buffered streaming deltas before status messages / busy flip,
|
|
636
|
+
// so the final council output is committed to the chat.
|
|
637
|
+
flushStreaming();
|
|
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
|
+
}
|
|
290
656
|
}
|
|
291
657
|
}
|
|
658
|
+
catch {
|
|
659
|
+
// Best-effort — never block on AGENTS.MD errors.
|
|
660
|
+
}
|
|
292
661
|
}
|
|
293
|
-
|
|
294
|
-
|
|
662
|
+
else if (!councilAborted) {
|
|
663
|
+
appendSystem(setMessages, "[agents.md] skipped — council produced no output", Date.now());
|
|
295
664
|
}
|
|
296
665
|
setBusy(false);
|
|
297
666
|
}
|