zelari-code 1.7.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/app.js +18 -4
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/browser/driver.js +58 -10
- package/dist/cli/browser/driver.js.map +1 -1
- package/dist/cli/browser/tools.js +4 -1
- package/dist/cli/browser/tools.js.map +1 -1
- package/dist/cli/budget/tokenBudget.js +92 -0
- package/dist/cli/budget/tokenBudget.js.map +1 -0
- package/dist/cli/components/PluginGate.js +24 -1
- package/dist/cli/components/PluginGate.js.map +1 -1
- package/dist/cli/components/Sidebar.js +7 -31
- package/dist/cli/components/Sidebar.js.map +1 -1
- package/dist/cli/components/StatusBar.js +18 -3
- package/dist/cli/components/StatusBar.js.map +1 -1
- package/dist/cli/councilDispatcher.js +3 -0
- package/dist/cli/councilDispatcher.js.map +1 -1
- package/dist/cli/hooks/conversationContext.js +136 -0
- package/dist/cli/hooks/conversationContext.js.map +1 -0
- package/dist/cli/hooks/useChatTurn.js +156 -56
- package/dist/cli/hooks/useChatTurn.js.map +1 -1
- package/dist/cli/hooks/useSlashDispatch.js +55 -0
- package/dist/cli/hooks/useSlashDispatch.js.map +1 -1
- package/dist/cli/main.bundled.js +1043 -393
- package/dist/cli/main.bundled.js.map +4 -4
- package/dist/cli/phase.js +52 -0
- package/dist/cli/phase.js.map +1 -0
- package/dist/cli/phaseState.js +12 -0
- package/dist/cli/phaseState.js.map +1 -0
- package/dist/cli/plugins/registry.js +90 -37
- package/dist/cli/plugins/registry.js.map +1 -1
- package/dist/cli/slashCommands.js +22 -0
- package/dist/cli/slashCommands.js.map +1 -1
- package/dist/cli/slashHandlers/provider.js.map +1 -1
- package/dist/cli/toolRegistry.js +3 -2
- package/dist/cli/toolRegistry.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* conversationContext — shared rolling provider history for all dispatch modes
|
|
3
|
+
* (agent / council / zelari).
|
|
4
|
+
*
|
|
5
|
+
* v1.6.0 fixed single-agent context loss with an in-hook historyRef. That left
|
|
6
|
+
* council/zelari stateless across turns, and /clear|/new never reset history —
|
|
7
|
+
* so the model could still "forget" answers or leak prior sessions.
|
|
8
|
+
*
|
|
9
|
+
* This module is the single source of truth for provider-side AgentMessage[]
|
|
10
|
+
* history. Hooks read/write it; slash handlers call clear() on /clear|/new.
|
|
11
|
+
*
|
|
12
|
+
* @since v1.8.0 (PR-A context unification)
|
|
13
|
+
*/
|
|
14
|
+
import { compactHistory } from './historyCompaction.js';
|
|
15
|
+
let history = [];
|
|
16
|
+
let lastClarification = null;
|
|
17
|
+
/** Current rolling history (read-only view — callers must not mutate). */
|
|
18
|
+
export function getHistory() {
|
|
19
|
+
return history;
|
|
20
|
+
}
|
|
21
|
+
/** Replace history after compaction / hydrate. */
|
|
22
|
+
export function setHistory(messages) {
|
|
23
|
+
history = [...messages];
|
|
24
|
+
}
|
|
25
|
+
/** Compact in place using the same rules as the agent loop. */
|
|
26
|
+
export function compactInPlace() {
|
|
27
|
+
history = compactHistory(history);
|
|
28
|
+
}
|
|
29
|
+
/** Append messages (e.g. this turn's assistant+tool tail). */
|
|
30
|
+
export function appendMessages(msgs) {
|
|
31
|
+
if (msgs.length === 0)
|
|
32
|
+
return;
|
|
33
|
+
history = history.concat(msgs);
|
|
34
|
+
}
|
|
35
|
+
/** Drop everything ( /clear, /new ). */
|
|
36
|
+
export function clearHistory() {
|
|
37
|
+
history = [];
|
|
38
|
+
lastClarification = null;
|
|
39
|
+
}
|
|
40
|
+
/** Serialize for session sidecar / tests. */
|
|
41
|
+
export function serializeHistory() {
|
|
42
|
+
return [...history];
|
|
43
|
+
}
|
|
44
|
+
/** Hydrate from session restore. */
|
|
45
|
+
export function hydrateHistory(messages) {
|
|
46
|
+
history = [...messages];
|
|
47
|
+
}
|
|
48
|
+
export function getLastClarification() {
|
|
49
|
+
return lastClarification;
|
|
50
|
+
}
|
|
51
|
+
export function setLastClarification(c) {
|
|
52
|
+
lastClarification = c
|
|
53
|
+
? { question: c.question, choices: c.choices, at: Date.now() }
|
|
54
|
+
: null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* If the user sends a short reply that likely answers the last clarifying
|
|
58
|
+
* question, return a rewritten user message that re-anchors the question so
|
|
59
|
+
* the model cannot treat "full" / "2" / "sì" as a brand-new request.
|
|
60
|
+
*
|
|
61
|
+
* Returns null when no rewrite is needed (long free-form message, no prior
|
|
62
|
+
* question, or history already carries enough context).
|
|
63
|
+
*/
|
|
64
|
+
export function maybeAnchorShortAnswer(userText) {
|
|
65
|
+
const clar = lastClarification;
|
|
66
|
+
if (!clar)
|
|
67
|
+
return null;
|
|
68
|
+
const trimmed = userText.trim();
|
|
69
|
+
if (!trimmed)
|
|
70
|
+
return null;
|
|
71
|
+
// Long free-form answers already carry intent — don't wrap.
|
|
72
|
+
if (trimmed.length > 80 || trimmed.includes('\n'))
|
|
73
|
+
return null;
|
|
74
|
+
const lower = trimmed.toLowerCase();
|
|
75
|
+
const choices = clar.choices;
|
|
76
|
+
const matched = choices.find((c) => c.toLowerCase() === lower) ??
|
|
77
|
+
choices.find((c) => c.toLowerCase().startsWith(lower)) ??
|
|
78
|
+
choices.find((c) => lower.startsWith(c.toLowerCase().slice(0, Math.min(4, c.length))));
|
|
79
|
+
// Also treat numeric picks ("1", "2") as choice indices (1-based).
|
|
80
|
+
let choiceLabel = matched ?? null;
|
|
81
|
+
if (!choiceLabel && /^\d{1,2}$/.test(trimmed)) {
|
|
82
|
+
const idx = Number.parseInt(trimmed, 10) - 1;
|
|
83
|
+
if (idx >= 0 && idx < choices.length)
|
|
84
|
+
choiceLabel = choices[idx] ?? null;
|
|
85
|
+
}
|
|
86
|
+
// Very short replies without a choice match still get anchored if ≤ 24 chars
|
|
87
|
+
// (e.g. "sì", "ok", "la seconda") — the model needs the prior question.
|
|
88
|
+
if (!choiceLabel && trimmed.length > 24)
|
|
89
|
+
return null;
|
|
90
|
+
const picked = choiceLabel ?? trimmed;
|
|
91
|
+
return (`The user is answering your previous clarifying question.\n` +
|
|
92
|
+
`Question: ${clar.question}\n` +
|
|
93
|
+
`Choices were: ${choices.join(' | ')}\n` +
|
|
94
|
+
`User's answer: ${picked}\n` +
|
|
95
|
+
`Proceed using this answer; do not re-ask the same question unless the answer is still ambiguous.`);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Build a compact text block of recent conversation for council/zelari paths
|
|
99
|
+
* that do not feed full AgentMessage[] into every member (token control).
|
|
100
|
+
* Includes the last few user/assistant turns only.
|
|
101
|
+
*/
|
|
102
|
+
export function formatHistoryForCouncil(maxTurns = 4) {
|
|
103
|
+
if (history.length === 0)
|
|
104
|
+
return '';
|
|
105
|
+
const lines = [];
|
|
106
|
+
// Walk from the end; collect up to maxTurns user+assistant pairs.
|
|
107
|
+
let turns = 0;
|
|
108
|
+
const chunk = [];
|
|
109
|
+
for (let i = history.length - 1; i >= 0 && turns < maxTurns; i--) {
|
|
110
|
+
const m = history[i];
|
|
111
|
+
if (m.role === 'user') {
|
|
112
|
+
chunk.push(`User: ${truncate(m.content, 400)}`);
|
|
113
|
+
turns += 1;
|
|
114
|
+
}
|
|
115
|
+
else if (m.role === 'assistant' && m.content.trim()) {
|
|
116
|
+
chunk.push(`Assistant: ${truncate(m.content, 600)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (chunk.length === 0)
|
|
120
|
+
return '';
|
|
121
|
+
lines.push('## Prior conversation (rolling context)');
|
|
122
|
+
lines.push(...chunk.reverse());
|
|
123
|
+
return lines.join('\n');
|
|
124
|
+
}
|
|
125
|
+
function truncate(s, max) {
|
|
126
|
+
const t = s.replace(/\s+/g, ' ').trim();
|
|
127
|
+
if (t.length <= max)
|
|
128
|
+
return t;
|
|
129
|
+
return `${t.slice(0, max - 1)}…`;
|
|
130
|
+
}
|
|
131
|
+
/** Test-only: reset module state. */
|
|
132
|
+
export function _resetConversationContextForTests() {
|
|
133
|
+
history = [];
|
|
134
|
+
lastClarification = null;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=conversationContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversationContext.js","sourceRoot":"","sources":["../../../src/cli/hooks/conversationContext.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAUxD,IAAI,OAAO,GAAmB,EAAE,CAAC;AACjC,IAAI,iBAAiB,GAA6B,IAAI,CAAC;AAEvD,0EAA0E;AAC1E,MAAM,UAAU,UAAU;IACxB,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,kDAAkD;AAClD,MAAM,UAAU,UAAU,CAAC,QAAiC;IAC1D,OAAO,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC;AAC1B,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,cAAc;IAC5B,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;AACpC,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,cAAc,CAAC,IAA6B;IAC1D,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAC9B,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AACjC,CAAC;AAED,wCAAwC;AACxC,MAAM,UAAU,YAAY;IAC1B,OAAO,GAAG,EAAE,CAAC;IACb,iBAAiB,GAAG,IAAI,CAAC;AAC3B,CAAC;AAED,6CAA6C;AAC7C,MAAM,UAAU,gBAAgB;IAC9B,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC;AACtB,CAAC;AAED,oCAAoC;AACpC,MAAM,UAAU,cAAc,CAAC,QAAiC;IAC9D,OAAO,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,OAAO,iBAAiB,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,CAAiD;IAEjD,iBAAiB,GAAG,CAAC;QACnB,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE;QAC9D,CAAC,CAAC,IAAI,CAAC;AACX,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CAAC,QAAgB;IACrD,MAAM,IAAI,GAAG,iBAAiB,CAAC;IAC/B,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC;IAChC,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,4DAA4D;IAC5D,IAAI,OAAO,CAAC,MAAM,GAAG,EAAE,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAE/D,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IACpC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;IAC7B,MAAM,OAAO,GACX,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC;QAC9C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACtD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAEzF,mEAAmE;IACnE,IAAI,WAAW,GAAG,OAAO,IAAI,IAAI,CAAC;IAClC,IAAI,CAAC,WAAW,IAAI,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC9C,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC;QAC7C,IAAI,GAAG,IAAI,CAAC,IAAI,GAAG,GAAG,OAAO,CAAC,MAAM;YAAE,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;IAC3E,CAAC;IAED,6EAA6E;IAC7E,wEAAwE;IACxE,IAAI,CAAC,WAAW,IAAI,OAAO,CAAC,MAAM,GAAG,EAAE;QAAE,OAAO,IAAI,CAAC;IAErD,MAAM,MAAM,GAAG,WAAW,IAAI,OAAO,CAAC;IACtC,OAAO,CACL,4DAA4D;QAC5D,aAAa,IAAI,CAAC,QAAQ,IAAI;QAC9B,iBAAiB,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI;QACxC,kBAAkB,MAAM,IAAI;QAC5B,kGAAkG,CACnG,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,QAAQ,GAAG,CAAC;IAClD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACpC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,kEAAkE;IAClE,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,GAAG,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;QACjE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QACrB,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YACtB,KAAK,CAAC,IAAI,CAAC,SAAS,QAAQ,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;YAChD,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;aAAM,IAAI,CAAC,CAAC,IAAI,KAAK,WAAW,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;YACtD,KAAK,CAAC,IAAI,CAAC,cAAc,QAAQ,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAClC,KAAK,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAC;IACtD,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;IAC/B,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,SAAS,QAAQ,CAAC,CAAS,EAAE,GAAW;IACtC,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACxC,IAAI,CAAC,CAAC,MAAM,IAAI,GAAG;QAAE,OAAO,CAAC,CAAC;IAC9B,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC;AACnC,CAAC;AAED,qCAAqC;AACrC,MAAM,UAAU,iCAAiC;IAC/C,OAAO,GAAG,EAAE,CAAC;IACb,iBAAiB,GAAG,IAAI,CAAC;AAC3B,CAAC"}
|
|
@@ -13,27 +13,27 @@ import { buildSystemPrompt, getAllTools, SINGLE_AGENT_IDENTITY_MODULE, buildLang
|
|
|
13
13
|
import { parseClarificationRequest, cleanAgentContent, } from "@zelari/core";
|
|
14
14
|
import { appendOrExtendStreamingAssistant, appendSystem, appendToolStart, finalizeStreamingAssistant, updateToolMessageEnd, } from "./messageHelpers.js";
|
|
15
15
|
import { setStreaming, finalizeStreaming, startTool, completeTool, } from "./chatState.js";
|
|
16
|
-
import {
|
|
16
|
+
import { getHistory, compactInPlace, appendMessages, clearHistory, setLastClarification, maybeAnchorShortAnswer, formatHistoryForCouncil, setHistory, } from "./conversationContext.js";
|
|
17
17
|
import { computeSessionStatsDelta } from "./chatStats.js";
|
|
18
18
|
import { envNumber } from "../utils/envNumber.js";
|
|
19
|
+
import { getPhase } from "../phaseState.js";
|
|
20
|
+
import { describePhase } from "../phase.js";
|
|
21
|
+
import { applyBudgetPolicy } from "../budget/tokenBudget.js";
|
|
19
22
|
export function useChatTurn(params) {
|
|
20
23
|
const { sessionId, writerRef, setMessages, commitStreaming, flushStreaming, setBusy, setSessionActive, setSessionStats, setLive, liveRef, setPicker, } = params;
|
|
21
24
|
const harnessRef = useRef(null);
|
|
22
25
|
const [queueCount, setQueueCount] = useState(0);
|
|
23
|
-
// v1.
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// own prior question when the user answered with a short reply ("full",
|
|
27
|
-
// "sì", "la seconda"). This ref carries prior turns forward: the seed
|
|
28
|
-
// for turn N is [system, ...history, user_N], and after the run we
|
|
29
|
-
// snapshot the assistant+tool tail the harness accumulated and append
|
|
30
|
-
// it here for turn N+1. Capped by compactHistory() (ZELARI_HISTORY_TURNS).
|
|
31
|
-
const historyRef = useRef([]);
|
|
26
|
+
// v1.8.0: rolling history lives in conversationContext (shared by agent,
|
|
27
|
+
// council, zelari) so /clear|/new can reset it and short answers bind
|
|
28
|
+
// across all modes. Seed for turn N is [system, ...history, user_N].
|
|
32
29
|
// v0.7.0: when the live region is wired, streaming + tool events route
|
|
33
30
|
// there; otherwise we fall back to the v0.6 single-array behavior so the
|
|
34
31
|
// existing unit tests (which pass only setMessages/commitStreaming) keep
|
|
35
32
|
// asserting on `messages` directly.
|
|
36
33
|
const useLiveModel = !!(setLive && liveRef);
|
|
34
|
+
const clearConversationHistory = useCallback(() => {
|
|
35
|
+
clearHistory();
|
|
36
|
+
}, []);
|
|
37
37
|
const dispatchPrompt = useCallback(async (userText, opts) => {
|
|
38
38
|
// v0.4.3 audit fix: provider resolution + harness construction now
|
|
39
39
|
// live INSIDE the try block. Previously, throws from providerFromEnv,
|
|
@@ -54,13 +54,19 @@ export function useChatTurn(params) {
|
|
|
54
54
|
// with a partial assistant tail).
|
|
55
55
|
let turnSucceeded = false;
|
|
56
56
|
try {
|
|
57
|
-
// v1.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
// v1.8.0: budget-aware compact (phase plan/build + occupancy thresholds).
|
|
58
|
+
compactInPlace();
|
|
59
|
+
const budget = applyBudgetPolicy(getHistory(), getPhase());
|
|
60
|
+
setHistory(budget.history);
|
|
61
|
+
for (const w of budget.warnings) {
|
|
62
|
+
appendSystem(setMessages, w, Date.now());
|
|
63
|
+
}
|
|
64
|
+
historySeedLen = getHistory().length;
|
|
65
|
+
// Short-answer anchor: if the user is replying to a ---QUESTION---,
|
|
66
|
+
// rewrite the user message so the model cannot treat "full"/"2" as
|
|
67
|
+
// a brand-new request even if compaction dropped the prior turn.
|
|
68
|
+
const anchored = maybeAnchorShortAnswer(userText);
|
|
69
|
+
const effectiveUserText = anchored ?? userText;
|
|
64
70
|
envConfig = await providerFromEnv();
|
|
65
71
|
if (!envConfig) {
|
|
66
72
|
// Name the ACTIVE provider — the old hardcoded "OPENAI_API_KEY not
|
|
@@ -71,7 +77,10 @@ export function useChatTurn(params) {
|
|
|
71
77
|
return;
|
|
72
78
|
}
|
|
73
79
|
setBusy(true);
|
|
74
|
-
const
|
|
80
|
+
const workPhase = getPhase();
|
|
81
|
+
const { registry: toolRegistry } = createBuiltinToolRegistry({
|
|
82
|
+
planMode: workPhase === "plan",
|
|
83
|
+
});
|
|
75
84
|
const baseProviderStream = openaiCompatibleProvider(envConfig);
|
|
76
85
|
const failoverResolution = await resolveFailoverStream({
|
|
77
86
|
failoverEnabled: process.env.ANATHEMA_FAILOVER !== "0",
|
|
@@ -200,12 +209,30 @@ export function useChatTurn(params) {
|
|
|
200
209
|
// SINGLE_AGENT_IDENTITY_MODULE overrides the council-flavored
|
|
201
210
|
// 'base-identity' module so the persona is "Zelari Code in the terminal",
|
|
202
211
|
// not "member of an AI Council".
|
|
212
|
+
const planPhaseBlock = workPhase === "plan"
|
|
213
|
+
? [
|
|
214
|
+
"",
|
|
215
|
+
"# Work Phase: PLAN",
|
|
216
|
+
"You are in PLAN mode. Explore and design only.",
|
|
217
|
+
"- Do NOT implement production code or run destructive shell commands.",
|
|
218
|
+
"- write_file / edit_file / bash / apply_diff are unavailable.",
|
|
219
|
+
"- Produce a clear plan, ask clarifying questions (---QUESTION---), use workspace plan tools when relevant.",
|
|
220
|
+
"- When the plan is ready, tell the user to run /build to implement.",
|
|
221
|
+
].join("\n")
|
|
222
|
+
: workPhase === "build" && planSummary
|
|
223
|
+
? [
|
|
224
|
+
"",
|
|
225
|
+
"# Work Phase: BUILD",
|
|
226
|
+
"Implement the approved plan. Prefer acting over describing. Update plan task statuses as you go.",
|
|
227
|
+
].join("\n")
|
|
228
|
+
: "";
|
|
203
229
|
const shellContextBlock = [
|
|
204
230
|
"# Platform & Shell",
|
|
205
231
|
`platform: ${process.platform}`,
|
|
206
232
|
`shell: ${resolvedShell.via}`,
|
|
207
233
|
shellGuidance,
|
|
208
234
|
nonInteractiveGuidance,
|
|
235
|
+
planPhaseBlock,
|
|
209
236
|
"",
|
|
210
237
|
"# Working Directory",
|
|
211
238
|
`You are running in: ${cwd}`,
|
|
@@ -284,23 +311,19 @@ export function useChatTurn(params) {
|
|
|
284
311
|
default: 25,
|
|
285
312
|
min: 1,
|
|
286
313
|
});
|
|
287
|
-
// v1.5.2: tool-loop
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
const maxToolLoopIterations = envNumber(process.env.ZELARI_MAX_TOOL_LOOP_ITERATIONS, {
|
|
291
|
-
default: 90,
|
|
292
|
-
min: 1,
|
|
293
|
-
});
|
|
314
|
+
// v1.5.2 / v1.8.0: tool-loop cap — budget policy may lower under
|
|
315
|
+
// context pressure; env still wins as the ceiling via applyBudgetPolicy.
|
|
316
|
+
const maxToolLoopIterations = budget.maxToolLoopIterations;
|
|
294
317
|
const harness = new AgentHarness({
|
|
295
318
|
model: envConfig.model,
|
|
296
319
|
provider: "openai-compatible",
|
|
297
320
|
messages: [
|
|
298
321
|
{ role: "system", content: systemPrompt },
|
|
299
|
-
// v1.
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
...
|
|
303
|
-
{ role: "user", content:
|
|
322
|
+
// v1.8.0: shared rolling history (agent/council/zelari) so short
|
|
323
|
+
// answers bind to prior ---QUESTION--- blocks. Possibly empty
|
|
324
|
+
// when ZELARI_HISTORY_TURNS=0.
|
|
325
|
+
...getHistory(),
|
|
326
|
+
{ role: "user", content: effectiveUserText },
|
|
304
327
|
],
|
|
305
328
|
tools: toolRegistry.toOpenAITools().map((t) => ({
|
|
306
329
|
name: t.function.name,
|
|
@@ -455,24 +478,24 @@ export function useChatTurn(params) {
|
|
|
455
478
|
const all = h.getMessages();
|
|
456
479
|
const seedLen = 1 /*system*/ + historySeedLen + 1 /*user*/;
|
|
457
480
|
if (all.length > seedLen) {
|
|
458
|
-
|
|
481
|
+
appendMessages(all.slice(seedLen));
|
|
459
482
|
}
|
|
460
483
|
}
|
|
461
484
|
}
|
|
462
485
|
catch {
|
|
463
486
|
// Non-fatal: a snapshot failure must never break the turn.
|
|
464
487
|
}
|
|
465
|
-
// v1.6.0:
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
|
|
469
|
-
// turn, so the binding works whether the user picks or types. The
|
|
470
|
-
// picker is purely ergonomic. Skipped when setPicker is unset
|
|
471
|
-
// (tests) or the run failed.
|
|
472
|
-
if (turnSucceeded && setPicker && assistantContent) {
|
|
488
|
+
// v1.6.0/v1.8.0: clarifying-question picker + lastClarification for
|
|
489
|
+
// short-answer anchoring. Rolling history already binds answers;
|
|
490
|
+
// setLastClarification covers compaction edge cases.
|
|
491
|
+
if (turnSucceeded && assistantContent) {
|
|
473
492
|
try {
|
|
474
493
|
const clar = parseClarificationRequest(assistantContent);
|
|
475
494
|
if (clar && clar.choices && clar.choices.length >= 2) {
|
|
495
|
+
setLastClarification({
|
|
496
|
+
question: clar.question,
|
|
497
|
+
choices: clar.choices,
|
|
498
|
+
});
|
|
476
499
|
// Strip the raw ---QUESTION--- block from the finalized
|
|
477
500
|
// assistant message so the display shows prose, not JSON.
|
|
478
501
|
const cleaned = cleanAgentContent(assistantContent);
|
|
@@ -488,14 +511,20 @@ export function useChatTurn(params) {
|
|
|
488
511
|
return next;
|
|
489
512
|
});
|
|
490
513
|
}
|
|
491
|
-
setPicker
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
514
|
+
if (setPicker) {
|
|
515
|
+
setPicker({
|
|
516
|
+
kind: "clarification",
|
|
517
|
+
title: clar.question,
|
|
518
|
+
items: clar.choices.map((c) => ({ value: c, label: c })),
|
|
519
|
+
onAnswer: (value) => {
|
|
520
|
+
void dispatchPrompt(value);
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
// Free-form answer path next turn — clear stale question.
|
|
527
|
+
setLastClarification(null);
|
|
499
528
|
}
|
|
500
529
|
}
|
|
501
530
|
catch {
|
|
@@ -543,6 +572,7 @@ export function useChatTurn(params) {
|
|
|
543
572
|
setQueueCount,
|
|
544
573
|
setLive,
|
|
545
574
|
liveRef,
|
|
575
|
+
setPicker,
|
|
546
576
|
});
|
|
547
577
|
}, [
|
|
548
578
|
sessionId,
|
|
@@ -554,6 +584,7 @@ export function useChatTurn(params) {
|
|
|
554
584
|
setQueueCount,
|
|
555
585
|
setLive,
|
|
556
586
|
liveRef,
|
|
587
|
+
setPicker,
|
|
557
588
|
]);
|
|
558
589
|
const pendingZelariRef = useRef(null);
|
|
559
590
|
const dispatchZelariPrompt = useCallback(async (text) => {
|
|
@@ -586,10 +617,11 @@ export function useChatTurn(params) {
|
|
|
586
617
|
harnessRef,
|
|
587
618
|
queueCount,
|
|
588
619
|
setQueueCount,
|
|
620
|
+
clearConversationHistory,
|
|
589
621
|
};
|
|
590
622
|
}
|
|
591
623
|
async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
|
|
592
|
-
const { sessionId, writerRef, setMessages, commitStreaming, flushStreaming, setBusy, setLive, liveRef, } = deps;
|
|
624
|
+
const { sessionId, writerRef, setMessages, commitStreaming, flushStreaming, setBusy, setLive, liveRef, setPicker, } = deps;
|
|
593
625
|
const useLiveModel = !!(setLive && liveRef);
|
|
594
626
|
const envConfig = await providerFromEnv();
|
|
595
627
|
if (!envConfig) {
|
|
@@ -599,6 +631,16 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
|
|
|
599
631
|
return { completionOk: false, ran: false };
|
|
600
632
|
}
|
|
601
633
|
setBusy(true);
|
|
634
|
+
// v1.8.0: compact shared history + budget + short-answer anchor.
|
|
635
|
+
compactInPlace();
|
|
636
|
+
const councilBudget = applyBudgetPolicy(getHistory(), getPhase());
|
|
637
|
+
setHistory(councilBudget.history);
|
|
638
|
+
for (const w of councilBudget.warnings) {
|
|
639
|
+
appendSystem(setMessages, w, Date.now());
|
|
640
|
+
}
|
|
641
|
+
const anchored = maybeAnchorShortAnswer(text);
|
|
642
|
+
const effectiveText = anchored ?? text;
|
|
643
|
+
appendSystem(setMessages, `[phase] ${describePhase(getPhase())}`, Date.now());
|
|
602
644
|
// Import dynamically to avoid a circular dep at module-load time.
|
|
603
645
|
const { dispatchCouncil } = await import("../councilDispatcher.js");
|
|
604
646
|
const { createWorkspaceContext, createWorkspaceStubs } = await import("../workspace/stubs.js");
|
|
@@ -608,14 +650,23 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
|
|
|
608
650
|
const { buildWorkspaceSummary, buildPlanSummary } = await import("../workspace/workspaceSummary.js");
|
|
609
651
|
const { buildLessonsSummary } = await import("../workspace/buildLessonsSummary.js");
|
|
610
652
|
const { FeedbackStore } = await import("../councilFeedback.js");
|
|
611
|
-
const
|
|
653
|
+
const workPhase = getPhase();
|
|
654
|
+
const { registry: councilToolRegistry } = createBuiltinToolRegistry({
|
|
655
|
+
planMode: workPhase === "plan",
|
|
656
|
+
});
|
|
612
657
|
const workspaceCtx = createWorkspaceContext();
|
|
613
658
|
const workspaceReg = createWorkspaceToolRegistry(workspaceCtx);
|
|
614
659
|
for (const name of workspaceReg.list()) {
|
|
615
660
|
const td = workspaceReg.get(name);
|
|
616
|
-
if (td)
|
|
617
|
-
|
|
661
|
+
if (!td)
|
|
662
|
+
continue;
|
|
663
|
+
// Plan phase: keep workspace plan/doc tools (createPlan, …); skip any
|
|
664
|
+
// that are pure project-file mutators if ever added.
|
|
665
|
+
councilToolRegistry.register(td);
|
|
618
666
|
}
|
|
667
|
+
// Force council design-phase when UI phase is plan (and vice-versa for build).
|
|
668
|
+
const phaseRunMode = overrides.runMode ??
|
|
669
|
+
(workPhase === "plan" ? "design-phase" : "implementation");
|
|
619
670
|
// v0.7.5: MCP tools for the council too (same lazy singleton as the
|
|
620
671
|
// single-agent path — zero extra spawns).
|
|
621
672
|
try {
|
|
@@ -666,7 +717,7 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
|
|
|
666
717
|
let sliceDegraded = false;
|
|
667
718
|
const PROVIDER_ERROR_ABORT_THRESHOLD = 2;
|
|
668
719
|
try {
|
|
669
|
-
for await (const event of dispatchCouncil(
|
|
720
|
+
for await (const event of dispatchCouncil(effectiveText, {
|
|
670
721
|
apiKey: envConfig.apiKey,
|
|
671
722
|
model: envConfig.model,
|
|
672
723
|
provider: "openai-compatible",
|
|
@@ -680,10 +731,13 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
|
|
|
680
731
|
// on and projected their identity onto the task.
|
|
681
732
|
// v0.7.3: append the existing plan (if any) so a follow-up /council
|
|
682
733
|
// continues it instead of re-planning from scratch.
|
|
734
|
+
// v1.8.0: rolling conversation context so short answers bind across
|
|
735
|
+
// council turns (same history store as the single-agent path).
|
|
683
736
|
workspaceContext: [
|
|
684
737
|
buildWorkspaceSummary(process.cwd()),
|
|
685
|
-
buildPlanSummary(process.cwd(), { userMessage:
|
|
686
|
-
buildLessonsSummary(process.cwd(),
|
|
738
|
+
buildPlanSummary(process.cwd(), { userMessage: effectiveText }),
|
|
739
|
+
buildLessonsSummary(process.cwd(), effectiveText),
|
|
740
|
+
formatHistoryForCouncil(4),
|
|
687
741
|
]
|
|
688
742
|
.filter(Boolean)
|
|
689
743
|
.join("\n\n"),
|
|
@@ -691,13 +745,42 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
|
|
|
691
745
|
// v1.0: Zelari-mode per-slice overrides (memory RAG, forced run mode,
|
|
692
746
|
// raised chairman budget). No-ops for a normal /council run.
|
|
693
747
|
...(overrides.ragContext ? { ragContext: overrides.ragContext } : {}),
|
|
694
|
-
|
|
748
|
+
runMode: phaseRunMode,
|
|
695
749
|
...(overrides.maxToolCallsChairman
|
|
696
750
|
? { maxToolCallsChairman: overrides.maxToolCallsChairman }
|
|
697
751
|
: {}),
|
|
698
752
|
onCouncilStatus: (message) => {
|
|
699
753
|
appendSystem(setMessages, message, Date.now());
|
|
700
754
|
},
|
|
755
|
+
// v1.8.0: pause council when a member asks a structured question.
|
|
756
|
+
onClarification: setPicker
|
|
757
|
+
? (req) => new Promise((resolve) => {
|
|
758
|
+
const choices = req.choices ?? [];
|
|
759
|
+
if (choices.length < 2) {
|
|
760
|
+
resolve(null);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
setLastClarification({
|
|
764
|
+
question: req.question,
|
|
765
|
+
choices,
|
|
766
|
+
});
|
|
767
|
+
let settled = false;
|
|
768
|
+
const finish = (value) => {
|
|
769
|
+
if (settled)
|
|
770
|
+
return;
|
|
771
|
+
settled = true;
|
|
772
|
+
setPicker(null);
|
|
773
|
+
resolve(value);
|
|
774
|
+
};
|
|
775
|
+
setPicker({
|
|
776
|
+
kind: "clarification",
|
|
777
|
+
title: req.question,
|
|
778
|
+
items: choices.map((c) => ({ value: c, label: c })),
|
|
779
|
+
onAnswer: (value) => finish(value),
|
|
780
|
+
onCancel: () => finish(null),
|
|
781
|
+
});
|
|
782
|
+
})
|
|
783
|
+
: undefined,
|
|
701
784
|
})) {
|
|
702
785
|
if (councilAborted) {
|
|
703
786
|
// Drain remaining events silently after the abort decision.
|
|
@@ -857,6 +940,23 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
|
|
|
857
940
|
finalizeStreaming(setMessages, setLive);
|
|
858
941
|
else
|
|
859
942
|
finalizeStreamingAssistant(setMessages);
|
|
943
|
+
// v1.8.0: fold this council turn into shared rolling history so the next
|
|
944
|
+
// agent/council/zelari turn sees user + synthesis (short answers bind).
|
|
945
|
+
if (membersCompleted > 0 || chairmanProducedOutput) {
|
|
946
|
+
try {
|
|
947
|
+
appendMessages([
|
|
948
|
+
{ role: "user", content: effectiveText },
|
|
949
|
+
{
|
|
950
|
+
role: "assistant",
|
|
951
|
+
content: chairmanSynthesisText.trim() ||
|
|
952
|
+
"[council completed without chairman synthesis text]",
|
|
953
|
+
},
|
|
954
|
+
]);
|
|
955
|
+
}
|
|
956
|
+
catch {
|
|
957
|
+
// Non-fatal.
|
|
958
|
+
}
|
|
959
|
+
}
|
|
860
960
|
// v0.7.1 (A3): only auto-write AGENTS.MD when the council actually produced
|
|
861
961
|
// output. Running the hook after an all-error run (e.g. the HTTP 400 from
|
|
862
962
|
// A1) dirtied the working tree with sections rewritten from nothing.
|
|
@@ -878,7 +978,7 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
|
|
|
878
978
|
}
|
|
879
979
|
const hook = await runPostCouncilHook(workspaceCtx, {
|
|
880
980
|
runMode: councilRunMode,
|
|
881
|
-
userMessage:
|
|
981
|
+
userMessage: effectiveText,
|
|
882
982
|
synthesisText: chairmanSynthesisText || undefined,
|
|
883
983
|
degradedRun: degraded.degraded,
|
|
884
984
|
degradedReasons: degraded.reasons,
|