@xingjisoft/neutrino 0.3.0 → 0.4.1
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 +33 -549
- package/assets/neutrino.ico +0 -0
- package/assets/neutrino.svg +7 -0
- package/assets/pro/backend_output_model.txt +5 -0
- package/assets/pro/claude-module-prompt.txt +65 -0
- package/assets/pro/common.txt +9 -0
- package/assets/pro/confirm_model.txt +5 -0
- package/assets/pro/front_output_model.txt +9 -0
- package/assets/pro/genimage_model.txt +1 -0
- package/assets/pro/main_model.txt +5 -0
- package/assets/pro/model-template.json +376 -0
- package/assets/pro/modeling_model.txt +5 -0
- package/assets/pro/plan_model.txt +8 -0
- package/assets/pro/thinking_model.txt +3 -0
- package/dist/agents.js +22 -3
- package/dist/app.js +199 -26
- package/dist/background.js +358 -0
- package/dist/bin.js +184 -44
- package/dist/browser.js +57 -0
- package/dist/config.js +174 -26
- package/dist/header.js +2 -1
- package/dist/index.js +1 -1
- package/dist/llm/approval.js +6 -2
- package/dist/llm/client.js +197 -28
- package/dist/llm/subagent.js +37 -1
- package/dist/llm/tools.js +3 -1
- package/dist/migration.js +33 -0
- package/dist/pro/agents.js +104 -0
- package/dist/pro/config.js +88 -0
- package/dist/pro/preflight.js +172 -0
- package/dist/screens/agentConversation.js +48 -5
- package/dist/screens/agentHistory.js +4 -2
- package/dist/screens/chat.js +14 -8
- package/dist/screens/modelSelect.js +15 -4
- package/dist/screens/settings.js +52 -9
- package/dist/screens/start.js +48 -15
- package/dist/state.js +1 -0
- package/dist/terminal/restore.js +3 -12
- package/dist/tray.js +138 -0
- package/dist/ui.js +58 -0
- package/dist/web/hub.js +26 -4
- package/dist/web/server.js +495 -74
- package/dist/web/service.js +135 -0
- package/dist/web/session.js +278 -25
- package/dist/web/settings.js +97 -24
- package/dist/web/terminalBridge.js +250 -0
- package/package.json +37 -37
- package/web/README.md +150 -16
- package/web/app.js +490 -25
- package/web/i18n.js +396 -345
- package/web/index.html +120 -15
- package/web/neutrino-pro.gif +0 -0
- package/web/neutrino.png +0 -0
- package/web/pro.js +184 -0
- package/web/settings.js +373 -247
- package/web/style.css +398 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { backgroundLog, clearBackgroundStatus, isProcessAlive, writeBackgroundStatus, } from "../background.js";
|
|
4
|
+
import { loadConfig, saveConfig } from "../config.js";
|
|
5
|
+
import { startBackgroundTray } from "../tray.js";
|
|
6
|
+
import { hub } from "./hub.js";
|
|
7
|
+
import { startWebServer } from "./server.js";
|
|
8
|
+
import { TerminalBridges } from "./terminalBridge.js";
|
|
9
|
+
/** The detached process owns browser conversations for their entire lifetime. */
|
|
10
|
+
export async function runWebService(options) {
|
|
11
|
+
const token = process.env.NEUTRINO_BACKGROUND_TOKEN?.trim() || randomBytes(32).toString("hex");
|
|
12
|
+
let resident = process.env.NEUTRINO_BACKGROUND_RESIDENT !== "0";
|
|
13
|
+
const owner = Number(process.env.NEUTRINO_BACKGROUND_OWNER);
|
|
14
|
+
let owners = new Set(Number.isSafeInteger(owner) && owner > 0 ? [owner] : []);
|
|
15
|
+
const startedAt = new Date().toISOString();
|
|
16
|
+
let handle = null;
|
|
17
|
+
let tray = null;
|
|
18
|
+
let monitor = null;
|
|
19
|
+
let stopping = false;
|
|
20
|
+
const bridges = new TerminalBridges();
|
|
21
|
+
const status = () => {
|
|
22
|
+
if (!handle)
|
|
23
|
+
throw new Error("the Web console is starting");
|
|
24
|
+
return {
|
|
25
|
+
pid: process.pid, port: handle.port, host: handle.host, url: handle.url,
|
|
26
|
+
auth_required: options.auth, token, started_at: startedAt,
|
|
27
|
+
resident, owner_pids: [...owners],
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
const syncTray = () => {
|
|
31
|
+
if (!resident) {
|
|
32
|
+
tray?.close();
|
|
33
|
+
tray = null;
|
|
34
|
+
}
|
|
35
|
+
else if (!tray && options.tray && handle) {
|
|
36
|
+
tray = startBackgroundTray({ url: handle.url, stopUrl: `${handle.url}/api/background/stop`, token, pid: process.pid });
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const shutdown = () => {
|
|
40
|
+
if (stopping)
|
|
41
|
+
return;
|
|
42
|
+
stopping = true;
|
|
43
|
+
if (monitor)
|
|
44
|
+
clearInterval(monitor);
|
|
45
|
+
tray?.close();
|
|
46
|
+
bridges.close();
|
|
47
|
+
backgroundLog(`stopping pid=${process.pid}`);
|
|
48
|
+
clearBackgroundStatus(process.pid);
|
|
49
|
+
const finish = () => process.exit(0);
|
|
50
|
+
if (handle)
|
|
51
|
+
handle.close().finally(finish).catch(finish);
|
|
52
|
+
else
|
|
53
|
+
finish();
|
|
54
|
+
setTimeout(finish, 3000).unref();
|
|
55
|
+
};
|
|
56
|
+
const configure = (update) => {
|
|
57
|
+
if (stopping)
|
|
58
|
+
throw new Error("the Web console is shutting down");
|
|
59
|
+
if (update.ownerPid !== undefined && !isProcessAlive(update.ownerPid)) {
|
|
60
|
+
throw new Error("the terminal that owns this console has already exited");
|
|
61
|
+
}
|
|
62
|
+
const previousResident = resident;
|
|
63
|
+
const previousOwners = owners;
|
|
64
|
+
const config = loadConfig();
|
|
65
|
+
const changed = update.resident !== undefined;
|
|
66
|
+
if (changed)
|
|
67
|
+
saveConfig({ ...config, background: update.resident });
|
|
68
|
+
resident = update.resident ?? resident;
|
|
69
|
+
owners = new Set(owners);
|
|
70
|
+
if (update.ownerPid !== undefined)
|
|
71
|
+
owners.add(update.ownerPid);
|
|
72
|
+
try {
|
|
73
|
+
writeBackgroundStatus(status());
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
resident = previousResident;
|
|
77
|
+
owners = previousOwners;
|
|
78
|
+
if (changed) {
|
|
79
|
+
try {
|
|
80
|
+
saveConfig(config);
|
|
81
|
+
}
|
|
82
|
+
catch { /* Report the original failure. */ }
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
if (update.bridge && update.ownerPid)
|
|
87
|
+
bridges.add(update.ownerPid, update.bridge);
|
|
88
|
+
syncTray();
|
|
89
|
+
return status();
|
|
90
|
+
};
|
|
91
|
+
try {
|
|
92
|
+
handle = await startWebServer({
|
|
93
|
+
port: options.port, host: options.host, auth: options.auth,
|
|
94
|
+
backgroundPrompt: !resident && process.env.NEUTRINO_BACKGROUND_PROMPT === "1",
|
|
95
|
+
backgroundService: { token, onStop: shutdown, status, configure },
|
|
96
|
+
onAddressChange: () => { if (handle)
|
|
97
|
+
writeBackgroundStatus(status()); },
|
|
98
|
+
});
|
|
99
|
+
writeBackgroundStatus(status());
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
backgroundLog(`failed to start pid=${process.pid}: ${error instanceof Error ? error.message : String(error)}`);
|
|
103
|
+
if (handle)
|
|
104
|
+
await handle.close();
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
backgroundLog(`started pid=${process.pid} on ${handle.url} (residency ${resident ? "on" : "off"})`);
|
|
108
|
+
syncTray();
|
|
109
|
+
// No helper processes: polling pids must remain invisible on Windows. An
|
|
110
|
+
// orphaned, non-resident console lets an in-flight browser turn finish before
|
|
111
|
+
// exiting, so disabling residency also cannot throw away running work.
|
|
112
|
+
monitor = setInterval(() => {
|
|
113
|
+
try {
|
|
114
|
+
const live = new Set([...owners].filter(isProcessAlive));
|
|
115
|
+
if (live.size !== owners.size) {
|
|
116
|
+
for (const owner of owners)
|
|
117
|
+
if (!live.has(owner))
|
|
118
|
+
bridges.remove(owner);
|
|
119
|
+
owners = live;
|
|
120
|
+
writeBackgroundStatus(status());
|
|
121
|
+
}
|
|
122
|
+
if (!resident && owners.size === 0 && !hub.list().some(entry => entry.busy || entry.paused || entry.queued || entry.compressing || entry.approval || entry.reviewing)) {
|
|
123
|
+
shutdown();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
backgroundLog(`lifetime check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
128
|
+
}
|
|
129
|
+
}, 500);
|
|
130
|
+
monitor.unref();
|
|
131
|
+
process.on("SIGINT", shutdown);
|
|
132
|
+
process.on("SIGTERM", shutdown);
|
|
133
|
+
if (process.platform !== "win32")
|
|
134
|
+
process.on("SIGHUP", () => { });
|
|
135
|
+
}
|
package/dist/web/session.js
CHANGED
|
@@ -11,9 +11,12 @@
|
|
|
11
11
|
* (snapshot, stream, pause, resume, message) work unchanged.
|
|
12
12
|
*/
|
|
13
13
|
import { randomUUID } from "node:crypto";
|
|
14
|
-
import { findModel, contextCompressionThreshold, loadConfig, loadPlannerPrompt, loadStepPrompt, loadSubAgentPrompt, normalizeApprovalMode, planFirstEnabled, loadModels, loadPrompt, saveSession, } from "../config.js";
|
|
14
|
+
import { findModel, contextCompressionThreshold, intensityForModel, loadConfig, loadPlannerPrompt, loadStepPrompt, loadSubAgentPrompt, normalizeApprovalMode, planFirstEnabled, loadModels, loadPrompt, saveSession, } from "../config.js";
|
|
15
15
|
import { COMPACT_KEEP, planCompaction, transcriptFor, modelHistory, normalizeContext, } from "../compact.js";
|
|
16
16
|
import { LLMClient, describeError } from "../llm/client.js";
|
|
17
|
+
import { defaultProPreset, loadProModels, loadProPrompt, proMaxRecursionDepth, requireProPreset } from "../pro/config.js";
|
|
18
|
+
import { resolveProTeam, ProPreflightError } from "../pro/preflight.js";
|
|
19
|
+
import { ProAgentTeam, getProToolDefinitions } from "../pro/agents.js";
|
|
17
20
|
import { advancePlan, buildPlannerInput, lastRequestText, needsPlan, parsePlan, planMessage, planTranscript, renderStepDirective, resumePlan, stopPlan, } from "../llm/plan.js";
|
|
18
21
|
import { DELEGATE_TOOL, executeTool, getSubAgentToolDefinitions, getToolDefinitions } from "../llm/tools.js";
|
|
19
22
|
import { buildSubAgentTask, formatSubAgentResult, runSubAgent as runSubAgentTask, } from "../llm/subagent.js";
|
|
@@ -24,8 +27,6 @@ import { writeCrashLog } from "../terminal/restore.js";
|
|
|
24
27
|
import { hub, liveApprovalDetails, } from "./hub.js";
|
|
25
28
|
/** Drop a resumed conversation nobody touched for this long. */
|
|
26
29
|
const IDLE_DISPOSE_MS = 30 * 60 * 1000;
|
|
27
|
-
/** Upper bound on tool round-trips inside a single turn. */
|
|
28
|
-
const MAX_TOOL_ROUNDS = 24;
|
|
29
30
|
/** Placeholder shown while a tool is still running. */
|
|
30
31
|
const TOOL_PENDING = "…";
|
|
31
32
|
/** A sub-agent gets the same tools minus delegation: one level, never a tree. */
|
|
@@ -44,7 +45,7 @@ export function newSessionId(now = new Date()) {
|
|
|
44
45
|
* written to disk until the first message, so an abandoned new-chat dialog
|
|
45
46
|
* cannot litter the history with empty transcripts.
|
|
46
47
|
*/
|
|
47
|
-
export function createWebSession(cwd, selection, approvalMode) {
|
|
48
|
+
export function createWebSession(cwd, selection, approvalMode, mode = "standard", proPreset) {
|
|
48
49
|
const now = new Date();
|
|
49
50
|
const source = {
|
|
50
51
|
id: newSessionId(now),
|
|
@@ -57,9 +58,23 @@ export function createWebSession(cwd, selection, approvalMode) {
|
|
|
57
58
|
title: "",
|
|
58
59
|
messages: [],
|
|
59
60
|
tokens: { prompt: 0, completion: 0 },
|
|
60
|
-
approval_mode: normalizeApprovalMode(approvalMode ?? loadConfig().approval_mode),
|
|
61
|
+
approval_mode: normalizeApprovalMode(approvalMode ?? loadConfig(mode).approval_mode),
|
|
62
|
+
...(mode === "pro" ? { pro_preset: requireProPreset(proPreset ?? defaultProPreset()) } : {}),
|
|
63
|
+
};
|
|
64
|
+
return new WebSession(source, selection.model, mode);
|
|
65
|
+
}
|
|
66
|
+
/** A preset may need fallback; defer model credentials/availability to preflight. */
|
|
67
|
+
export function proInitialSelection(preset = defaultProPreset()) {
|
|
68
|
+
const tier = loadProModels().tier.find((entry) => entry.name === requireProPreset(preset));
|
|
69
|
+
const setting = tier["model-setting"].find((entry) => entry.type === "main_model");
|
|
70
|
+
const provider = setting?.provider ?? "";
|
|
71
|
+
const name = setting?.model ?? "";
|
|
72
|
+
const model = loadModels().providers.find((entry) => entry.name === provider)?.models.find((entry) => entry.name === name);
|
|
73
|
+
return {
|
|
74
|
+
provider,
|
|
75
|
+
model: model ? { ...model } : { name, url: "", api_key: "", supports_streaming: true },
|
|
76
|
+
intensity: setting?.intensity || "high",
|
|
61
77
|
};
|
|
62
|
-
return new WebSession(source, selection.model);
|
|
63
78
|
}
|
|
64
79
|
function parseArguments(raw) {
|
|
65
80
|
const text = raw?.trim() ?? "";
|
|
@@ -93,15 +108,27 @@ export function resolveModel(modelName, providerName) {
|
|
|
93
108
|
return { ok: true, model };
|
|
94
109
|
}
|
|
95
110
|
/** Build a web session from a saved transcript, or explain why we cannot. */
|
|
96
|
-
export function resumeSession(source, selection) {
|
|
111
|
+
export function resumeSession(source, selection, mode = "standard") {
|
|
112
|
+
if (mode === "pro") {
|
|
113
|
+
try {
|
|
114
|
+
const initial = proInitialSelection(source.pro_preset ?? defaultProPreset());
|
|
115
|
+
return { ok: true, session: new WebSession({ ...source, provider: initial.provider,
|
|
116
|
+
model: initial.model.name, intensity: initial.intensity }, initial.model, mode) };
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
return { ok: false, error: describeError(error) };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
97
122
|
const updated = selection ? { ...source, ...selection } : source;
|
|
98
123
|
const resolved = resolveModel(updated.model, updated.provider);
|
|
99
124
|
if (!resolved.ok)
|
|
100
125
|
return resolved;
|
|
101
|
-
return { ok: true, session: new WebSession(updated, resolved.model) };
|
|
126
|
+
return { ok: true, session: new WebSession(updated, resolved.model, mode) };
|
|
102
127
|
}
|
|
103
128
|
export class WebSession {
|
|
104
129
|
key = null;
|
|
130
|
+
/** Workspace this conversation belongs to; keeps Pro and standard separate. */
|
|
131
|
+
mode;
|
|
105
132
|
model;
|
|
106
133
|
provider;
|
|
107
134
|
intensity;
|
|
@@ -120,6 +147,10 @@ export class WebSession {
|
|
|
120
147
|
plan = null;
|
|
121
148
|
/** True while the planner agent is writing the steps. */
|
|
122
149
|
planning = false;
|
|
150
|
+
proPreset = null;
|
|
151
|
+
proTeam = null;
|
|
152
|
+
proState;
|
|
153
|
+
proAbort = null;
|
|
123
154
|
/** What the sub-agent is doing, or null when none is running. */
|
|
124
155
|
subagent = null;
|
|
125
156
|
saveError = null;
|
|
@@ -138,11 +169,16 @@ export class WebSession {
|
|
|
138
169
|
pendingApproval = null;
|
|
139
170
|
/** Reviewer count while an automated review runs, null otherwise. */
|
|
140
171
|
reviewing = null;
|
|
141
|
-
constructor(source, model) {
|
|
172
|
+
constructor(source, model, mode = "standard") {
|
|
173
|
+
this.mode = mode;
|
|
174
|
+
if (mode === "pro")
|
|
175
|
+
this.proPreset = requireProPreset(source.pro_preset ?? defaultProPreset());
|
|
142
176
|
this.model = model;
|
|
143
|
-
this.approvalMode = normalizeApprovalMode(source.approval_mode ?? loadConfig().approval_mode);
|
|
177
|
+
this.approvalMode = normalizeApprovalMode(source.approval_mode ?? loadConfig(this.mode).approval_mode);
|
|
144
178
|
this.provider = source.provider;
|
|
145
|
-
|
|
179
|
+
// A transcript saved before the model's limits existed (or before the model
|
|
180
|
+
// itself was narrowed) must not replay an unsupported sampling level.
|
|
181
|
+
this.intensity = mode === "pro" ? source.intensity : intensityForModel(model, source.intensity, loadConfig(this.mode).default_intensity);
|
|
146
182
|
this.dir = source.cwd;
|
|
147
183
|
this.sessionId = source.id;
|
|
148
184
|
this.messages = source.messages.slice();
|
|
@@ -167,6 +203,7 @@ export class WebSession {
|
|
|
167
203
|
if (this.disposed)
|
|
168
204
|
return;
|
|
169
205
|
this.disposed = true;
|
|
206
|
+
this.proAbort?.abort();
|
|
170
207
|
this.turnToken++;
|
|
171
208
|
this.busy = false;
|
|
172
209
|
if (this.pendingApproval) {
|
|
@@ -183,6 +220,8 @@ export class WebSession {
|
|
|
183
220
|
return this.dir;
|
|
184
221
|
}
|
|
185
222
|
liveConfigure(selection) {
|
|
223
|
+
if (this.mode === "pro")
|
|
224
|
+
return { ok: false, error: "choose a Pro preset to change professional models" };
|
|
186
225
|
if (this.disposed)
|
|
187
226
|
return { ok: false, error: "this session has ended" };
|
|
188
227
|
if (this.busy || this.pendingApproval) {
|
|
@@ -190,13 +229,37 @@ export class WebSession {
|
|
|
190
229
|
}
|
|
191
230
|
this.model = { ...selection.model };
|
|
192
231
|
this.provider = selection.provider;
|
|
193
|
-
this.intensity = selection.intensity;
|
|
232
|
+
this.intensity = intensityForModel(this.model, selection.intensity, loadConfig(this.mode).default_intensity);
|
|
194
233
|
this.save();
|
|
195
234
|
this.publishLive();
|
|
196
235
|
return { ok: true };
|
|
197
236
|
}
|
|
237
|
+
liveSetProPreset(preset) {
|
|
238
|
+
if (this.mode !== "pro" || this.disposed)
|
|
239
|
+
return { ok: false, error: "this is not an active Pro session" };
|
|
240
|
+
if (this.busy || this.pendingApproval)
|
|
241
|
+
return { ok: false, error: "pause the current turn before changing presets" };
|
|
242
|
+
try {
|
|
243
|
+
this.proPreset = requireProPreset(preset);
|
|
244
|
+
const initial = proInitialSelection(this.proPreset);
|
|
245
|
+
this.model = initial.model;
|
|
246
|
+
this.provider = initial.provider;
|
|
247
|
+
this.intensity = initial.intensity;
|
|
248
|
+
this.proTeam = null;
|
|
249
|
+
this.proState = undefined;
|
|
250
|
+
this.save();
|
|
251
|
+
this.publishLive();
|
|
252
|
+
return { ok: true };
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
return { ok: false, error: describeError(error) };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
198
258
|
liveMeta() {
|
|
199
259
|
return {
|
|
260
|
+
...(this.mode === "pro" ? { pro: this.proState ?? {
|
|
261
|
+
preset: this.proPreset, checking: false, checkId: "", models: [], fallbacks: [],
|
|
262
|
+
} } : {}),
|
|
200
263
|
sessionId: this.sessionId,
|
|
201
264
|
title: this.title(),
|
|
202
265
|
provider: this.provider,
|
|
@@ -251,6 +314,7 @@ export class WebSession {
|
|
|
251
314
|
if (this.paused || (!this.busy && !this.queued))
|
|
252
315
|
return;
|
|
253
316
|
this.paused = true;
|
|
317
|
+
this.proAbort?.abort();
|
|
254
318
|
this.turnToken++;
|
|
255
319
|
// A queued decision would otherwise keep the promise (and the gate) alive.
|
|
256
320
|
if (this.pendingApproval) {
|
|
@@ -261,6 +325,13 @@ export class WebSession {
|
|
|
261
325
|
this.busy = false;
|
|
262
326
|
this.retainStreamingContent();
|
|
263
327
|
this.subagent = null;
|
|
328
|
+
if (this.proState)
|
|
329
|
+
this.proState.checking = false;
|
|
330
|
+
if (this.mode === "pro") {
|
|
331
|
+
this.reviewing = null;
|
|
332
|
+
this.retrying = null;
|
|
333
|
+
this.planning = false;
|
|
334
|
+
}
|
|
264
335
|
stopPlan(this.plan);
|
|
265
336
|
this.save();
|
|
266
337
|
this.publishLive();
|
|
@@ -307,6 +378,9 @@ export class WebSession {
|
|
|
307
378
|
return;
|
|
308
379
|
if (!this.checkpoint.flush())
|
|
309
380
|
return;
|
|
381
|
+
this.proAbort?.abort();
|
|
382
|
+
this.proTeam = null;
|
|
383
|
+
this.proState = undefined;
|
|
310
384
|
this.turnToken++;
|
|
311
385
|
if (this.pendingApproval) {
|
|
312
386
|
const pending = this.pendingApproval;
|
|
@@ -374,6 +448,8 @@ export class WebSession {
|
|
|
374
448
|
* pass, exactly as it did before planning existed.
|
|
375
449
|
*/
|
|
376
450
|
async runTurn(token, turnMessages, approvalContext) {
|
|
451
|
+
if (this.mode === "pro")
|
|
452
|
+
return this.runProTurn(token, turnMessages, approvalContext);
|
|
377
453
|
await this.autoCompress(token);
|
|
378
454
|
if (token !== this.turnToken || this.paused)
|
|
379
455
|
return;
|
|
@@ -405,6 +481,127 @@ export class WebSession {
|
|
|
405
481
|
}
|
|
406
482
|
this.onTurnFinished();
|
|
407
483
|
}
|
|
484
|
+
/** Freeze one resolved team per task, plan first, then give the whole brief to main. */
|
|
485
|
+
async runProTurn(token, turnMessages, approvalContext) {
|
|
486
|
+
const cancelled = () => token !== this.turnToken || this.paused || this.disposed;
|
|
487
|
+
const controller = new AbortController();
|
|
488
|
+
this.proAbort = controller;
|
|
489
|
+
this.proTeam = null;
|
|
490
|
+
this.plan = null;
|
|
491
|
+
this.proState = { preset: this.proPreset, checking: true, checkId: randomUUID(), models: [], fallbacks: [] };
|
|
492
|
+
this.publishLive();
|
|
493
|
+
try {
|
|
494
|
+
const resolved = await resolveProTeam(this.proPreset, {
|
|
495
|
+
signal: controller.signal,
|
|
496
|
+
onUsage: (usage) => {
|
|
497
|
+
if (!cancelled()) {
|
|
498
|
+
this.tokens.prompt += usage.prompt;
|
|
499
|
+
this.tokens.completion += usage.completion;
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
if (cancelled())
|
|
504
|
+
return;
|
|
505
|
+
this.proState.models = Object.values(resolved.models).map((entry) => ({
|
|
506
|
+
role: entry.role, provider: entry.provider, model: entry.model.name, intensity: entry.intensity, tier: entry.tier,
|
|
507
|
+
}));
|
|
508
|
+
this.proState.fallbacks = resolved.fallbacks;
|
|
509
|
+
this.proState.checking = false;
|
|
510
|
+
const team = new ProAgentTeam({
|
|
511
|
+
models: resolved.models, maxDepth: proMaxRecursionDepth(), cwd: this.dir, cancelled,
|
|
512
|
+
approve: (name, args) => this.requestApproval(name, args, {
|
|
513
|
+
...approvalContext, context: captureApprovalContext(this.turnView(turnMessages)).context,
|
|
514
|
+
}),
|
|
515
|
+
execute: (name, args) => executeTool(name, args, this.dir),
|
|
516
|
+
onProgress: (label) => { if (!cancelled()) {
|
|
517
|
+
this.subagent = label;
|
|
518
|
+
this.publishLive();
|
|
519
|
+
} },
|
|
520
|
+
onUsage: (usage) => {
|
|
521
|
+
if (!cancelled()) {
|
|
522
|
+
this.tokens.prompt += usage.prompt;
|
|
523
|
+
this.tokens.completion += usage.completion;
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
onRetry: (notice) => { if (!cancelled()) {
|
|
527
|
+
this.retrying = notice;
|
|
528
|
+
this.publishLive();
|
|
529
|
+
} },
|
|
530
|
+
});
|
|
531
|
+
this.proTeam = team;
|
|
532
|
+
const main = resolved.models.main_model;
|
|
533
|
+
this.model = main.model;
|
|
534
|
+
this.provider = main.provider;
|
|
535
|
+
this.intensity = main.intensity;
|
|
536
|
+
this.publishLive();
|
|
537
|
+
this.planning = true;
|
|
538
|
+
this.publishLive();
|
|
539
|
+
const planning = await team.call("plan_model", buildPlannerInput({
|
|
540
|
+
request: approvalContext.prompt, cwd: this.dir, transcript: planTranscript(turnMessages),
|
|
541
|
+
}), "Plan the task before execution. Do not modify files or execute commands.", 0, true);
|
|
542
|
+
if (cancelled())
|
|
543
|
+
return;
|
|
544
|
+
this.planning = false;
|
|
545
|
+
this.subagent = null;
|
|
546
|
+
this.retrying = null;
|
|
547
|
+
if (!planning.trim() || /\[Error:|\[Sub-agent .*without.*report/.test(planning)) {
|
|
548
|
+
throw new Error(`Pro planning failed: ${planning}`);
|
|
549
|
+
}
|
|
550
|
+
this.plan = parsePlan(planning);
|
|
551
|
+
if (!this.plan) {
|
|
552
|
+
// Standard planning skips single actions; Pro plans every task.
|
|
553
|
+
try {
|
|
554
|
+
const single = JSON.parse(planning.trim().replace(/^```(?:json)?\s*|\s*```$/g, ""));
|
|
555
|
+
if (single.steps?.length === 1 && typeof single.steps[0].title === "string" && single.steps[0].title.trim()) {
|
|
556
|
+
this.plan = { goal: String(single.goal ?? "").slice(0, 240), steps: [{
|
|
557
|
+
title: single.steps[0].title.slice(0, 160), detail: String(single.steps[0].detail ?? "").slice(0, 400), status: "pending",
|
|
558
|
+
}] };
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
catch { /* A useful prose plan can still guide the main agent. */ }
|
|
562
|
+
}
|
|
563
|
+
await this.autoCompress(token);
|
|
564
|
+
if (cancelled())
|
|
565
|
+
return;
|
|
566
|
+
this.messages.push({ role: "assistant", content: this.plan ? planMessage(this.plan) : planning });
|
|
567
|
+
this.save();
|
|
568
|
+
this.publishLive();
|
|
569
|
+
const directive = `The planning agent produced this plan for the current user request:\n${planning}\n\n` +
|
|
570
|
+
"Execute the complete task, using the appropriate professional agents for reasoning, frontend, backend and modeling work. Verify the result and report the outcome.";
|
|
571
|
+
if (this.plan?.steps.length)
|
|
572
|
+
this.setStep(0, "active");
|
|
573
|
+
const finished = await this.agentLoop(this.buildApiMessages(this.turnView(turnMessages), directive), token, approvalContext);
|
|
574
|
+
if (cancelled())
|
|
575
|
+
return;
|
|
576
|
+
if (this.plan) {
|
|
577
|
+
if (finished)
|
|
578
|
+
for (let i = 0; i < this.plan.steps.length; i++)
|
|
579
|
+
this.setStep(i, "done");
|
|
580
|
+
else
|
|
581
|
+
this.setStep(0, "failed");
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
if (cancelled())
|
|
586
|
+
return;
|
|
587
|
+
this.proState.checking = false;
|
|
588
|
+
this.proState.error = describeError(error);
|
|
589
|
+
if (error instanceof ProPreflightError)
|
|
590
|
+
this.proState.fallbacks = error.fallbacks;
|
|
591
|
+
throw error;
|
|
592
|
+
}
|
|
593
|
+
finally {
|
|
594
|
+
if (!cancelled()) {
|
|
595
|
+
this.planning = false;
|
|
596
|
+
this.subagent = null;
|
|
597
|
+
this.retrying = null;
|
|
598
|
+
this.proState.checking = false;
|
|
599
|
+
this.publishLive();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (!cancelled())
|
|
603
|
+
this.onTurnFinished();
|
|
604
|
+
}
|
|
408
605
|
/**
|
|
409
606
|
* The transcript this turn may see: the messages it started with plus what it
|
|
410
607
|
* added since. A user message queued while the turn runs belongs to the next
|
|
@@ -430,7 +627,7 @@ export class WebSession {
|
|
|
430
627
|
* a plan must improve a turn, never be a new way for one to fail.
|
|
431
628
|
*/
|
|
432
629
|
async makePlan(token) {
|
|
433
|
-
if (!planFirstEnabled())
|
|
630
|
+
if (!planFirstEnabled(this.mode))
|
|
434
631
|
return null;
|
|
435
632
|
const request = lastRequestText(this.messages);
|
|
436
633
|
if (!request.trim())
|
|
@@ -488,8 +685,9 @@ export class WebSession {
|
|
|
488
685
|
buildApiMessages(messages = this.messages, directive = "") {
|
|
489
686
|
// A step directive extends the system prompt: the plan is an instruction, not
|
|
490
687
|
// something the user just said.
|
|
688
|
+
const prompt = this.mode === "pro" ? loadProPrompt("main_model") : loadPrompt();
|
|
491
689
|
const api = [
|
|
492
|
-
{ role: "system", content: directive ? `${
|
|
690
|
+
{ role: "system", content: directive ? `${prompt}\n\n${directive}` : prompt },
|
|
493
691
|
];
|
|
494
692
|
for (const m of modelHistory(messages, this.context)) {
|
|
495
693
|
if (m.role === "user" || m.role === "assistant") {
|
|
@@ -507,12 +705,16 @@ export class WebSession {
|
|
|
507
705
|
* the caller owns the turn's end because a planned turn runs this per step.
|
|
508
706
|
*/
|
|
509
707
|
async agentLoop(apiMessages, token, approvalContext) {
|
|
510
|
-
const tools = getToolDefinitions();
|
|
708
|
+
const tools = this.mode === "pro" ? getProToolDefinitions() : getToolDefinitions();
|
|
709
|
+
const team = this.proTeam;
|
|
511
710
|
try {
|
|
512
|
-
|
|
711
|
+
while (true) {
|
|
513
712
|
if (token !== this.turnToken || this.paused)
|
|
514
713
|
return false;
|
|
515
|
-
|
|
714
|
+
// Match the terminal: long tasks can keep using tools until the model
|
|
715
|
+
// finishes, while older tool results are trimmed near the context limit.
|
|
716
|
+
apiMessages = this.trimApiMessages(apiMessages);
|
|
717
|
+
const client = team ? team.client("main_model") : new LLMClient(this.model, this.provider, this.intensity);
|
|
516
718
|
let text = "";
|
|
517
719
|
this.streamingContent = "";
|
|
518
720
|
this.publishLive();
|
|
@@ -552,6 +754,7 @@ export class WebSession {
|
|
|
552
754
|
role: "assistant",
|
|
553
755
|
content: text,
|
|
554
756
|
reasoning_content: client.reasoningContent ?? "",
|
|
757
|
+
...(client.anthropicContent.length ? { anthropic_content: client.anthropicContent } : {}),
|
|
555
758
|
...(toolCalls.length > 0
|
|
556
759
|
? {
|
|
557
760
|
tool_calls: toolCalls.map((call) => ({
|
|
@@ -565,8 +768,13 @@ export class WebSession {
|
|
|
565
768
|
}
|
|
566
769
|
this.save();
|
|
567
770
|
this.publishLive();
|
|
568
|
-
if (toolCalls.length === 0)
|
|
569
|
-
|
|
771
|
+
if (toolCalls.length === 0) {
|
|
772
|
+
if (this.mode === "pro" && !text.trim()) {
|
|
773
|
+
this.messages.push({ role: "assistant", content: "[Error: Pro main model returned an empty completion.]" });
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
return !/\[Error:/.test(text);
|
|
777
|
+
}
|
|
570
778
|
for (const call of toolCalls) {
|
|
571
779
|
if (token !== this.turnToken || this.paused)
|
|
572
780
|
return false;
|
|
@@ -618,7 +826,6 @@ export class WebSession {
|
|
|
618
826
|
}
|
|
619
827
|
this.save();
|
|
620
828
|
}
|
|
621
|
-
return true;
|
|
622
829
|
}
|
|
623
830
|
catch (err) {
|
|
624
831
|
if (token !== this.turnToken)
|
|
@@ -652,15 +859,28 @@ export class WebSession {
|
|
|
652
859
|
const risk = assessToolCall(tool, args, this.dir);
|
|
653
860
|
if (!risk)
|
|
654
861
|
return { approved: true, source: "prompt", reason: "routine call" };
|
|
862
|
+
const approvalToken = this.turnToken;
|
|
863
|
+
const team = this.proTeam;
|
|
655
864
|
const gate = new ApprovalGate({
|
|
656
865
|
askHuman: () => new Promise((resolve) => {
|
|
866
|
+
if (this.mode === "pro" && (approvalToken !== this.turnToken || this.paused || this.disposed)) {
|
|
867
|
+
resolve(false);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
657
870
|
this.pendingApproval = { tool, args, risk, resolve };
|
|
658
871
|
this.publishLive();
|
|
659
872
|
}),
|
|
660
873
|
reviewer: () => this.reviewerModel(),
|
|
874
|
+
...(this.mode === "pro" && team ? {
|
|
875
|
+
createReviewerClient: () => team.client("confirm_model"),
|
|
876
|
+
reviewerSystemPrompt: () => loadProPrompt("confirm_model"),
|
|
877
|
+
cancelled: () => approvalToken !== this.turnToken || this.paused || this.disposed,
|
|
878
|
+
} : {}),
|
|
661
879
|
autoApprovalEnabled: () => this.approvalMode === "auto",
|
|
662
|
-
extremeAutoApprovalEnabled: () => loadConfig().auto_approve_extreme_risk === true,
|
|
880
|
+
extremeAutoApprovalEnabled: () => loadConfig(this.mode).auto_approve_extreme_risk === true,
|
|
663
881
|
onReview: (info) => {
|
|
882
|
+
if (this.mode === "pro" && (approvalToken !== this.turnToken || this.paused || this.disposed))
|
|
883
|
+
return;
|
|
664
884
|
this.reviewing = info
|
|
665
885
|
? {
|
|
666
886
|
total: info.total, round: info.round ?? 1, rounds: info.rounds ?? 1,
|
|
@@ -674,7 +894,7 @@ export class WebSession {
|
|
|
674
894
|
}
|
|
675
895
|
/** Reviewer model: `approval_model` when set, otherwise the chat model. */
|
|
676
896
|
reviewerModel() {
|
|
677
|
-
const name = String(loadConfig().approval_model ?? "").trim();
|
|
897
|
+
const name = String(loadConfig(this.mode).approval_model ?? "").trim();
|
|
678
898
|
const found = name ? findModel(name) : null;
|
|
679
899
|
if (found) {
|
|
680
900
|
const usable = resolveModel(found.model.name, found.provider);
|
|
@@ -691,6 +911,13 @@ export class WebSession {
|
|
|
691
911
|
* approval gate and abort signal live.
|
|
692
912
|
*/
|
|
693
913
|
async runTool(name, args, toolMsg, token, apiMessages, approvalContext) {
|
|
914
|
+
if (this.mode === "pro" && (name === "call_pro_agent" || name === DELEGATE_TOOL)) {
|
|
915
|
+
const team = this.proTeam;
|
|
916
|
+
if (!team)
|
|
917
|
+
return "[Error: Pro models have not passed preflight]";
|
|
918
|
+
return team.call(String(args.role ?? "backend_output_model"), String(args.task ?? ""), `${typeof args.context === "string" ? args.context : ""}\n\nCurrent user request: ${approvalContext.prompt}\n` +
|
|
919
|
+
captureApprovalContext(apiMessages).context, 1);
|
|
920
|
+
}
|
|
694
921
|
if (name !== DELEGATE_TOOL)
|
|
695
922
|
return executeTool(name, args, this.dir);
|
|
696
923
|
return this.delegate(args, toolMsg, token, apiMessages, approvalContext);
|
|
@@ -738,7 +965,33 @@ export class WebSession {
|
|
|
738
965
|
}
|
|
739
966
|
// ---- compression -----------------------------------------------------
|
|
740
967
|
compressThreshold() {
|
|
741
|
-
|
|
968
|
+
if (this.mode === "pro") {
|
|
969
|
+
const setting = this.proState?.models.find((entry) => entry.role === "main_model");
|
|
970
|
+
const tier = loadProModels().tier.find((entry) => entry.name === setting?.tier);
|
|
971
|
+
const capacity = tier?.["model-setting"].find((entry) => entry.type === "main_model")?.context_window;
|
|
972
|
+
if (capacity)
|
|
973
|
+
return Math.max(1, Math.floor(capacity * 0.8));
|
|
974
|
+
}
|
|
975
|
+
return contextCompressionThreshold(this.provider, this.model, this.mode);
|
|
976
|
+
}
|
|
977
|
+
/** Trim old tool results in a long turn without changing the saved history. */
|
|
978
|
+
trimApiMessages(apiMessages) {
|
|
979
|
+
const threshold = this.compressThreshold();
|
|
980
|
+
if (threshold <= 0 || this.lastPromptTokens < threshold)
|
|
981
|
+
return apiMessages;
|
|
982
|
+
const toolIdx = [];
|
|
983
|
+
for (let i = 0; i < apiMessages.length; i++) {
|
|
984
|
+
if (apiMessages[i].role === "tool")
|
|
985
|
+
toolIdx.push(i);
|
|
986
|
+
}
|
|
987
|
+
const dropCount = toolIdx.length - COMPACT_KEEP;
|
|
988
|
+
if (dropCount <= 0)
|
|
989
|
+
return apiMessages;
|
|
990
|
+
const copy = apiMessages.slice();
|
|
991
|
+
for (let k = 0; k < dropCount; k++) {
|
|
992
|
+
copy[toolIdx[k]] = { ...copy[toolIdx[k]], content: "[tool output omitted to save context]" };
|
|
993
|
+
}
|
|
994
|
+
return copy;
|
|
742
995
|
}
|
|
743
996
|
async autoCompress(token) {
|
|
744
997
|
const threshold = this.compressThreshold();
|
|
@@ -760,7 +1013,7 @@ export class WebSession {
|
|
|
760
1013
|
this.publishLive();
|
|
761
1014
|
let summary = "";
|
|
762
1015
|
try {
|
|
763
|
-
summary = await new LLMClient(this.model, this.provider, this.intensity).summarize(transcript, {
|
|
1016
|
+
summary = await (this.proTeam?.client("main_model") ?? new LLMClient(this.model, this.provider, this.intensity)).summarize(transcript, {
|
|
764
1017
|
cancelled: () => token !== this.turnToken || this.paused,
|
|
765
1018
|
});
|
|
766
1019
|
}
|
|
@@ -813,7 +1066,7 @@ export class WebSession {
|
|
|
813
1066
|
if (chat.length === 0)
|
|
814
1067
|
return true;
|
|
815
1068
|
try {
|
|
816
|
-
this.sessionId = saveSession(chat, this.model.name, this.provider, this.intensity, this.dir, this.sessionId, this.tokens, this.context, this.approvalMode, this.plan);
|
|
1069
|
+
this.sessionId = saveSession(chat, this.model.name, this.provider, this.intensity, this.dir, this.sessionId, this.tokens, this.context, this.approvalMode, this.plan, this.mode, this.proPreset ?? undefined);
|
|
817
1070
|
this.saveError = null;
|
|
818
1071
|
return true;
|
|
819
1072
|
}
|