@xingjisoft/neutrino 0.3.0 → 0.4.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/README.md +312 -464
- 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/app.js +43 -7
- package/dist/bin.js +5 -2
- package/dist/browser.js +57 -0
- package/dist/config.js +170 -26
- 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/pro/agents.js +104 -0
- package/dist/pro/config.js +88 -0
- package/dist/pro/preflight.js +172 -0
- package/dist/screens/chat.js +14 -8
- package/dist/screens/modelSelect.js +15 -4
- package/dist/screens/settings.js +9 -7
- package/dist/screens/start.js +48 -15
- package/dist/ui.js +58 -0
- package/dist/web/hub.js +22 -4
- package/dist/web/server.js +279 -66
- package/dist/web/session.js +258 -22
- package/dist/web/settings.js +91 -24
- package/package.json +37 -37
- package/web/README.md +87 -13
- package/web/app.js +304 -14
- package/web/i18n.js +382 -345
- package/web/index.html +90 -14
- package/web/neutrino-pro.gif +0 -0
- package/web/neutrino.png +0 -0
- package/web/pro.js +184 -0
- package/web/settings.js +322 -247
- package/web/style.css +297 -0
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";
|
|
@@ -44,7 +47,7 @@ export function newSessionId(now = new Date()) {
|
|
|
44
47
|
* written to disk until the first message, so an abandoned new-chat dialog
|
|
45
48
|
* cannot litter the history with empty transcripts.
|
|
46
49
|
*/
|
|
47
|
-
export function createWebSession(cwd, selection, approvalMode) {
|
|
50
|
+
export function createWebSession(cwd, selection, approvalMode, mode = "standard", proPreset) {
|
|
48
51
|
const now = new Date();
|
|
49
52
|
const source = {
|
|
50
53
|
id: newSessionId(now),
|
|
@@ -57,9 +60,23 @@ export function createWebSession(cwd, selection, approvalMode) {
|
|
|
57
60
|
title: "",
|
|
58
61
|
messages: [],
|
|
59
62
|
tokens: { prompt: 0, completion: 0 },
|
|
60
|
-
approval_mode: normalizeApprovalMode(approvalMode ?? loadConfig().approval_mode),
|
|
63
|
+
approval_mode: normalizeApprovalMode(approvalMode ?? loadConfig(mode).approval_mode),
|
|
64
|
+
...(mode === "pro" ? { pro_preset: requireProPreset(proPreset ?? defaultProPreset()) } : {}),
|
|
65
|
+
};
|
|
66
|
+
return new WebSession(source, selection.model, mode);
|
|
67
|
+
}
|
|
68
|
+
/** A preset may need fallback; defer model credentials/availability to preflight. */
|
|
69
|
+
export function proInitialSelection(preset = defaultProPreset()) {
|
|
70
|
+
const tier = loadProModels().tier.find((entry) => entry.name === requireProPreset(preset));
|
|
71
|
+
const setting = tier["model-setting"].find((entry) => entry.type === "main_model");
|
|
72
|
+
const provider = setting?.provider ?? "";
|
|
73
|
+
const name = setting?.model ?? "";
|
|
74
|
+
const model = loadModels().providers.find((entry) => entry.name === provider)?.models.find((entry) => entry.name === name);
|
|
75
|
+
return {
|
|
76
|
+
provider,
|
|
77
|
+
model: model ? { ...model } : { name, url: "", api_key: "", supports_streaming: true },
|
|
78
|
+
intensity: setting?.intensity || "high",
|
|
61
79
|
};
|
|
62
|
-
return new WebSession(source, selection.model);
|
|
63
80
|
}
|
|
64
81
|
function parseArguments(raw) {
|
|
65
82
|
const text = raw?.trim() ?? "";
|
|
@@ -93,15 +110,27 @@ export function resolveModel(modelName, providerName) {
|
|
|
93
110
|
return { ok: true, model };
|
|
94
111
|
}
|
|
95
112
|
/** Build a web session from a saved transcript, or explain why we cannot. */
|
|
96
|
-
export function resumeSession(source, selection) {
|
|
113
|
+
export function resumeSession(source, selection, mode = "standard") {
|
|
114
|
+
if (mode === "pro") {
|
|
115
|
+
try {
|
|
116
|
+
const initial = proInitialSelection(source.pro_preset ?? defaultProPreset());
|
|
117
|
+
return { ok: true, session: new WebSession({ ...source, provider: initial.provider,
|
|
118
|
+
model: initial.model.name, intensity: initial.intensity }, initial.model, mode) };
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return { ok: false, error: describeError(error) };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
97
124
|
const updated = selection ? { ...source, ...selection } : source;
|
|
98
125
|
const resolved = resolveModel(updated.model, updated.provider);
|
|
99
126
|
if (!resolved.ok)
|
|
100
127
|
return resolved;
|
|
101
|
-
return { ok: true, session: new WebSession(updated, resolved.model) };
|
|
128
|
+
return { ok: true, session: new WebSession(updated, resolved.model, mode) };
|
|
102
129
|
}
|
|
103
130
|
export class WebSession {
|
|
104
131
|
key = null;
|
|
132
|
+
/** Workspace this conversation belongs to; keeps Pro and standard separate. */
|
|
133
|
+
mode;
|
|
105
134
|
model;
|
|
106
135
|
provider;
|
|
107
136
|
intensity;
|
|
@@ -120,6 +149,10 @@ export class WebSession {
|
|
|
120
149
|
plan = null;
|
|
121
150
|
/** True while the planner agent is writing the steps. */
|
|
122
151
|
planning = false;
|
|
152
|
+
proPreset = null;
|
|
153
|
+
proTeam = null;
|
|
154
|
+
proState;
|
|
155
|
+
proAbort = null;
|
|
123
156
|
/** What the sub-agent is doing, or null when none is running. */
|
|
124
157
|
subagent = null;
|
|
125
158
|
saveError = null;
|
|
@@ -138,11 +171,16 @@ export class WebSession {
|
|
|
138
171
|
pendingApproval = null;
|
|
139
172
|
/** Reviewer count while an automated review runs, null otherwise. */
|
|
140
173
|
reviewing = null;
|
|
141
|
-
constructor(source, model) {
|
|
174
|
+
constructor(source, model, mode = "standard") {
|
|
175
|
+
this.mode = mode;
|
|
176
|
+
if (mode === "pro")
|
|
177
|
+
this.proPreset = requireProPreset(source.pro_preset ?? defaultProPreset());
|
|
142
178
|
this.model = model;
|
|
143
|
-
this.approvalMode = normalizeApprovalMode(source.approval_mode ?? loadConfig().approval_mode);
|
|
179
|
+
this.approvalMode = normalizeApprovalMode(source.approval_mode ?? loadConfig(this.mode).approval_mode);
|
|
144
180
|
this.provider = source.provider;
|
|
145
|
-
|
|
181
|
+
// A transcript saved before the model's limits existed (or before the model
|
|
182
|
+
// itself was narrowed) must not replay an unsupported sampling level.
|
|
183
|
+
this.intensity = mode === "pro" ? source.intensity : intensityForModel(model, source.intensity, loadConfig(this.mode).default_intensity);
|
|
146
184
|
this.dir = source.cwd;
|
|
147
185
|
this.sessionId = source.id;
|
|
148
186
|
this.messages = source.messages.slice();
|
|
@@ -167,6 +205,7 @@ export class WebSession {
|
|
|
167
205
|
if (this.disposed)
|
|
168
206
|
return;
|
|
169
207
|
this.disposed = true;
|
|
208
|
+
this.proAbort?.abort();
|
|
170
209
|
this.turnToken++;
|
|
171
210
|
this.busy = false;
|
|
172
211
|
if (this.pendingApproval) {
|
|
@@ -183,6 +222,8 @@ export class WebSession {
|
|
|
183
222
|
return this.dir;
|
|
184
223
|
}
|
|
185
224
|
liveConfigure(selection) {
|
|
225
|
+
if (this.mode === "pro")
|
|
226
|
+
return { ok: false, error: "choose a Pro preset to change professional models" };
|
|
186
227
|
if (this.disposed)
|
|
187
228
|
return { ok: false, error: "this session has ended" };
|
|
188
229
|
if (this.busy || this.pendingApproval) {
|
|
@@ -190,13 +231,37 @@ export class WebSession {
|
|
|
190
231
|
}
|
|
191
232
|
this.model = { ...selection.model };
|
|
192
233
|
this.provider = selection.provider;
|
|
193
|
-
this.intensity = selection.intensity;
|
|
234
|
+
this.intensity = intensityForModel(this.model, selection.intensity, loadConfig(this.mode).default_intensity);
|
|
194
235
|
this.save();
|
|
195
236
|
this.publishLive();
|
|
196
237
|
return { ok: true };
|
|
197
238
|
}
|
|
239
|
+
liveSetProPreset(preset) {
|
|
240
|
+
if (this.mode !== "pro" || this.disposed)
|
|
241
|
+
return { ok: false, error: "this is not an active Pro session" };
|
|
242
|
+
if (this.busy || this.pendingApproval)
|
|
243
|
+
return { ok: false, error: "pause the current turn before changing presets" };
|
|
244
|
+
try {
|
|
245
|
+
this.proPreset = requireProPreset(preset);
|
|
246
|
+
const initial = proInitialSelection(this.proPreset);
|
|
247
|
+
this.model = initial.model;
|
|
248
|
+
this.provider = initial.provider;
|
|
249
|
+
this.intensity = initial.intensity;
|
|
250
|
+
this.proTeam = null;
|
|
251
|
+
this.proState = undefined;
|
|
252
|
+
this.save();
|
|
253
|
+
this.publishLive();
|
|
254
|
+
return { ok: true };
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
return { ok: false, error: describeError(error) };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
198
260
|
liveMeta() {
|
|
199
261
|
return {
|
|
262
|
+
...(this.mode === "pro" ? { pro: this.proState ?? {
|
|
263
|
+
preset: this.proPreset, checking: false, checkId: "", models: [], fallbacks: [],
|
|
264
|
+
} } : {}),
|
|
200
265
|
sessionId: this.sessionId,
|
|
201
266
|
title: this.title(),
|
|
202
267
|
provider: this.provider,
|
|
@@ -251,6 +316,7 @@ export class WebSession {
|
|
|
251
316
|
if (this.paused || (!this.busy && !this.queued))
|
|
252
317
|
return;
|
|
253
318
|
this.paused = true;
|
|
319
|
+
this.proAbort?.abort();
|
|
254
320
|
this.turnToken++;
|
|
255
321
|
// A queued decision would otherwise keep the promise (and the gate) alive.
|
|
256
322
|
if (this.pendingApproval) {
|
|
@@ -261,6 +327,13 @@ export class WebSession {
|
|
|
261
327
|
this.busy = false;
|
|
262
328
|
this.retainStreamingContent();
|
|
263
329
|
this.subagent = null;
|
|
330
|
+
if (this.proState)
|
|
331
|
+
this.proState.checking = false;
|
|
332
|
+
if (this.mode === "pro") {
|
|
333
|
+
this.reviewing = null;
|
|
334
|
+
this.retrying = null;
|
|
335
|
+
this.planning = false;
|
|
336
|
+
}
|
|
264
337
|
stopPlan(this.plan);
|
|
265
338
|
this.save();
|
|
266
339
|
this.publishLive();
|
|
@@ -307,6 +380,9 @@ export class WebSession {
|
|
|
307
380
|
return;
|
|
308
381
|
if (!this.checkpoint.flush())
|
|
309
382
|
return;
|
|
383
|
+
this.proAbort?.abort();
|
|
384
|
+
this.proTeam = null;
|
|
385
|
+
this.proState = undefined;
|
|
310
386
|
this.turnToken++;
|
|
311
387
|
if (this.pendingApproval) {
|
|
312
388
|
const pending = this.pendingApproval;
|
|
@@ -374,6 +450,8 @@ export class WebSession {
|
|
|
374
450
|
* pass, exactly as it did before planning existed.
|
|
375
451
|
*/
|
|
376
452
|
async runTurn(token, turnMessages, approvalContext) {
|
|
453
|
+
if (this.mode === "pro")
|
|
454
|
+
return this.runProTurn(token, turnMessages, approvalContext);
|
|
377
455
|
await this.autoCompress(token);
|
|
378
456
|
if (token !== this.turnToken || this.paused)
|
|
379
457
|
return;
|
|
@@ -405,6 +483,127 @@ export class WebSession {
|
|
|
405
483
|
}
|
|
406
484
|
this.onTurnFinished();
|
|
407
485
|
}
|
|
486
|
+
/** Freeze one resolved team per task, plan first, then give the whole brief to main. */
|
|
487
|
+
async runProTurn(token, turnMessages, approvalContext) {
|
|
488
|
+
const cancelled = () => token !== this.turnToken || this.paused || this.disposed;
|
|
489
|
+
const controller = new AbortController();
|
|
490
|
+
this.proAbort = controller;
|
|
491
|
+
this.proTeam = null;
|
|
492
|
+
this.plan = null;
|
|
493
|
+
this.proState = { preset: this.proPreset, checking: true, checkId: randomUUID(), models: [], fallbacks: [] };
|
|
494
|
+
this.publishLive();
|
|
495
|
+
try {
|
|
496
|
+
const resolved = await resolveProTeam(this.proPreset, {
|
|
497
|
+
signal: controller.signal,
|
|
498
|
+
onUsage: (usage) => {
|
|
499
|
+
if (!cancelled()) {
|
|
500
|
+
this.tokens.prompt += usage.prompt;
|
|
501
|
+
this.tokens.completion += usage.completion;
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
if (cancelled())
|
|
506
|
+
return;
|
|
507
|
+
this.proState.models = Object.values(resolved.models).map((entry) => ({
|
|
508
|
+
role: entry.role, provider: entry.provider, model: entry.model.name, intensity: entry.intensity, tier: entry.tier,
|
|
509
|
+
}));
|
|
510
|
+
this.proState.fallbacks = resolved.fallbacks;
|
|
511
|
+
this.proState.checking = false;
|
|
512
|
+
const team = new ProAgentTeam({
|
|
513
|
+
models: resolved.models, maxDepth: proMaxRecursionDepth(), cwd: this.dir, cancelled,
|
|
514
|
+
approve: (name, args) => this.requestApproval(name, args, {
|
|
515
|
+
...approvalContext, context: captureApprovalContext(this.turnView(turnMessages)).context,
|
|
516
|
+
}),
|
|
517
|
+
execute: (name, args) => executeTool(name, args, this.dir),
|
|
518
|
+
onProgress: (label) => { if (!cancelled()) {
|
|
519
|
+
this.subagent = label;
|
|
520
|
+
this.publishLive();
|
|
521
|
+
} },
|
|
522
|
+
onUsage: (usage) => {
|
|
523
|
+
if (!cancelled()) {
|
|
524
|
+
this.tokens.prompt += usage.prompt;
|
|
525
|
+
this.tokens.completion += usage.completion;
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
onRetry: (notice) => { if (!cancelled()) {
|
|
529
|
+
this.retrying = notice;
|
|
530
|
+
this.publishLive();
|
|
531
|
+
} },
|
|
532
|
+
});
|
|
533
|
+
this.proTeam = team;
|
|
534
|
+
const main = resolved.models.main_model;
|
|
535
|
+
this.model = main.model;
|
|
536
|
+
this.provider = main.provider;
|
|
537
|
+
this.intensity = main.intensity;
|
|
538
|
+
this.publishLive();
|
|
539
|
+
this.planning = true;
|
|
540
|
+
this.publishLive();
|
|
541
|
+
const planning = await team.call("plan_model", buildPlannerInput({
|
|
542
|
+
request: approvalContext.prompt, cwd: this.dir, transcript: planTranscript(turnMessages),
|
|
543
|
+
}), "Plan the task before execution. Do not modify files or execute commands.", 0, true);
|
|
544
|
+
if (cancelled())
|
|
545
|
+
return;
|
|
546
|
+
this.planning = false;
|
|
547
|
+
this.subagent = null;
|
|
548
|
+
this.retrying = null;
|
|
549
|
+
if (!planning.trim() || /\[Error:|\[Sub-agent .*without.*report/.test(planning)) {
|
|
550
|
+
throw new Error(`Pro planning failed: ${planning}`);
|
|
551
|
+
}
|
|
552
|
+
this.plan = parsePlan(planning);
|
|
553
|
+
if (!this.plan) {
|
|
554
|
+
// Standard planning skips single actions; Pro plans every task.
|
|
555
|
+
try {
|
|
556
|
+
const single = JSON.parse(planning.trim().replace(/^```(?:json)?\s*|\s*```$/g, ""));
|
|
557
|
+
if (single.steps?.length === 1 && typeof single.steps[0].title === "string" && single.steps[0].title.trim()) {
|
|
558
|
+
this.plan = { goal: String(single.goal ?? "").slice(0, 240), steps: [{
|
|
559
|
+
title: single.steps[0].title.slice(0, 160), detail: String(single.steps[0].detail ?? "").slice(0, 400), status: "pending",
|
|
560
|
+
}] };
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
catch { /* A useful prose plan can still guide the main agent. */ }
|
|
564
|
+
}
|
|
565
|
+
await this.autoCompress(token);
|
|
566
|
+
if (cancelled())
|
|
567
|
+
return;
|
|
568
|
+
this.messages.push({ role: "assistant", content: this.plan ? planMessage(this.plan) : planning });
|
|
569
|
+
this.save();
|
|
570
|
+
this.publishLive();
|
|
571
|
+
const directive = `The planning agent produced this plan for the current user request:\n${planning}\n\n` +
|
|
572
|
+
"Execute the complete task, using the appropriate professional agents for reasoning, frontend, backend and modeling work. Verify the result and report the outcome.";
|
|
573
|
+
if (this.plan?.steps.length)
|
|
574
|
+
this.setStep(0, "active");
|
|
575
|
+
const finished = await this.agentLoop(this.buildApiMessages(this.turnView(turnMessages), directive), token, approvalContext);
|
|
576
|
+
if (cancelled())
|
|
577
|
+
return;
|
|
578
|
+
if (this.plan) {
|
|
579
|
+
if (finished)
|
|
580
|
+
for (let i = 0; i < this.plan.steps.length; i++)
|
|
581
|
+
this.setStep(i, "done");
|
|
582
|
+
else
|
|
583
|
+
this.setStep(0, "failed");
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
if (cancelled())
|
|
588
|
+
return;
|
|
589
|
+
this.proState.checking = false;
|
|
590
|
+
this.proState.error = describeError(error);
|
|
591
|
+
if (error instanceof ProPreflightError)
|
|
592
|
+
this.proState.fallbacks = error.fallbacks;
|
|
593
|
+
throw error;
|
|
594
|
+
}
|
|
595
|
+
finally {
|
|
596
|
+
if (!cancelled()) {
|
|
597
|
+
this.planning = false;
|
|
598
|
+
this.subagent = null;
|
|
599
|
+
this.retrying = null;
|
|
600
|
+
this.proState.checking = false;
|
|
601
|
+
this.publishLive();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (!cancelled())
|
|
605
|
+
this.onTurnFinished();
|
|
606
|
+
}
|
|
408
607
|
/**
|
|
409
608
|
* The transcript this turn may see: the messages it started with plus what it
|
|
410
609
|
* added since. A user message queued while the turn runs belongs to the next
|
|
@@ -430,7 +629,7 @@ export class WebSession {
|
|
|
430
629
|
* a plan must improve a turn, never be a new way for one to fail.
|
|
431
630
|
*/
|
|
432
631
|
async makePlan(token) {
|
|
433
|
-
if (!planFirstEnabled())
|
|
632
|
+
if (!planFirstEnabled(this.mode))
|
|
434
633
|
return null;
|
|
435
634
|
const request = lastRequestText(this.messages);
|
|
436
635
|
if (!request.trim())
|
|
@@ -488,8 +687,9 @@ export class WebSession {
|
|
|
488
687
|
buildApiMessages(messages = this.messages, directive = "") {
|
|
489
688
|
// A step directive extends the system prompt: the plan is an instruction, not
|
|
490
689
|
// something the user just said.
|
|
690
|
+
const prompt = this.mode === "pro" ? loadProPrompt("main_model") : loadPrompt();
|
|
491
691
|
const api = [
|
|
492
|
-
{ role: "system", content: directive ? `${
|
|
692
|
+
{ role: "system", content: directive ? `${prompt}\n\n${directive}` : prompt },
|
|
493
693
|
];
|
|
494
694
|
for (const m of modelHistory(messages, this.context)) {
|
|
495
695
|
if (m.role === "user" || m.role === "assistant") {
|
|
@@ -507,12 +707,13 @@ export class WebSession {
|
|
|
507
707
|
* the caller owns the turn's end because a planned turn runs this per step.
|
|
508
708
|
*/
|
|
509
709
|
async agentLoop(apiMessages, token, approvalContext) {
|
|
510
|
-
const tools = getToolDefinitions();
|
|
710
|
+
const tools = this.mode === "pro" ? getProToolDefinitions() : getToolDefinitions();
|
|
711
|
+
const team = this.proTeam;
|
|
511
712
|
try {
|
|
512
713
|
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
|
513
714
|
if (token !== this.turnToken || this.paused)
|
|
514
715
|
return false;
|
|
515
|
-
const client = new LLMClient(this.model, this.provider, this.intensity);
|
|
716
|
+
const client = team ? team.client("main_model") : new LLMClient(this.model, this.provider, this.intensity);
|
|
516
717
|
let text = "";
|
|
517
718
|
this.streamingContent = "";
|
|
518
719
|
this.publishLive();
|
|
@@ -552,6 +753,7 @@ export class WebSession {
|
|
|
552
753
|
role: "assistant",
|
|
553
754
|
content: text,
|
|
554
755
|
reasoning_content: client.reasoningContent ?? "",
|
|
756
|
+
...(client.anthropicContent.length ? { anthropic_content: client.anthropicContent } : {}),
|
|
555
757
|
...(toolCalls.length > 0
|
|
556
758
|
? {
|
|
557
759
|
tool_calls: toolCalls.map((call) => ({
|
|
@@ -565,8 +767,13 @@ export class WebSession {
|
|
|
565
767
|
}
|
|
566
768
|
this.save();
|
|
567
769
|
this.publishLive();
|
|
568
|
-
if (toolCalls.length === 0)
|
|
569
|
-
|
|
770
|
+
if (toolCalls.length === 0) {
|
|
771
|
+
if (this.mode === "pro" && !text.trim()) {
|
|
772
|
+
this.messages.push({ role: "assistant", content: "[Error: Pro main model returned an empty completion.]" });
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
return !/\[Error:/.test(text);
|
|
776
|
+
}
|
|
570
777
|
for (const call of toolCalls) {
|
|
571
778
|
if (token !== this.turnToken || this.paused)
|
|
572
779
|
return false;
|
|
@@ -618,7 +825,9 @@ export class WebSession {
|
|
|
618
825
|
}
|
|
619
826
|
this.save();
|
|
620
827
|
}
|
|
621
|
-
|
|
828
|
+
if (this.mode === "pro")
|
|
829
|
+
this.messages.push({ role: "assistant", content: "[Error: Pro main agent reached its tool round limit before completion.]" });
|
|
830
|
+
return false;
|
|
622
831
|
}
|
|
623
832
|
catch (err) {
|
|
624
833
|
if (token !== this.turnToken)
|
|
@@ -652,15 +861,28 @@ export class WebSession {
|
|
|
652
861
|
const risk = assessToolCall(tool, args, this.dir);
|
|
653
862
|
if (!risk)
|
|
654
863
|
return { approved: true, source: "prompt", reason: "routine call" };
|
|
864
|
+
const approvalToken = this.turnToken;
|
|
865
|
+
const team = this.proTeam;
|
|
655
866
|
const gate = new ApprovalGate({
|
|
656
867
|
askHuman: () => new Promise((resolve) => {
|
|
868
|
+
if (this.mode === "pro" && (approvalToken !== this.turnToken || this.paused || this.disposed)) {
|
|
869
|
+
resolve(false);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
657
872
|
this.pendingApproval = { tool, args, risk, resolve };
|
|
658
873
|
this.publishLive();
|
|
659
874
|
}),
|
|
660
875
|
reviewer: () => this.reviewerModel(),
|
|
876
|
+
...(this.mode === "pro" && team ? {
|
|
877
|
+
createReviewerClient: () => team.client("confirm_model"),
|
|
878
|
+
reviewerSystemPrompt: () => loadProPrompt("confirm_model"),
|
|
879
|
+
cancelled: () => approvalToken !== this.turnToken || this.paused || this.disposed,
|
|
880
|
+
} : {}),
|
|
661
881
|
autoApprovalEnabled: () => this.approvalMode === "auto",
|
|
662
|
-
extremeAutoApprovalEnabled: () => loadConfig().auto_approve_extreme_risk === true,
|
|
882
|
+
extremeAutoApprovalEnabled: () => loadConfig(this.mode).auto_approve_extreme_risk === true,
|
|
663
883
|
onReview: (info) => {
|
|
884
|
+
if (this.mode === "pro" && (approvalToken !== this.turnToken || this.paused || this.disposed))
|
|
885
|
+
return;
|
|
664
886
|
this.reviewing = info
|
|
665
887
|
? {
|
|
666
888
|
total: info.total, round: info.round ?? 1, rounds: info.rounds ?? 1,
|
|
@@ -674,7 +896,7 @@ export class WebSession {
|
|
|
674
896
|
}
|
|
675
897
|
/** Reviewer model: `approval_model` when set, otherwise the chat model. */
|
|
676
898
|
reviewerModel() {
|
|
677
|
-
const name = String(loadConfig().approval_model ?? "").trim();
|
|
899
|
+
const name = String(loadConfig(this.mode).approval_model ?? "").trim();
|
|
678
900
|
const found = name ? findModel(name) : null;
|
|
679
901
|
if (found) {
|
|
680
902
|
const usable = resolveModel(found.model.name, found.provider);
|
|
@@ -691,6 +913,13 @@ export class WebSession {
|
|
|
691
913
|
* approval gate and abort signal live.
|
|
692
914
|
*/
|
|
693
915
|
async runTool(name, args, toolMsg, token, apiMessages, approvalContext) {
|
|
916
|
+
if (this.mode === "pro" && (name === "call_pro_agent" || name === DELEGATE_TOOL)) {
|
|
917
|
+
const team = this.proTeam;
|
|
918
|
+
if (!team)
|
|
919
|
+
return "[Error: Pro models have not passed preflight]";
|
|
920
|
+
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` +
|
|
921
|
+
captureApprovalContext(apiMessages).context, 1);
|
|
922
|
+
}
|
|
694
923
|
if (name !== DELEGATE_TOOL)
|
|
695
924
|
return executeTool(name, args, this.dir);
|
|
696
925
|
return this.delegate(args, toolMsg, token, apiMessages, approvalContext);
|
|
@@ -738,7 +967,14 @@ export class WebSession {
|
|
|
738
967
|
}
|
|
739
968
|
// ---- compression -----------------------------------------------------
|
|
740
969
|
compressThreshold() {
|
|
741
|
-
|
|
970
|
+
if (this.mode === "pro") {
|
|
971
|
+
const setting = this.proState?.models.find((entry) => entry.role === "main_model");
|
|
972
|
+
const tier = loadProModels().tier.find((entry) => entry.name === setting?.tier);
|
|
973
|
+
const capacity = tier?.["model-setting"].find((entry) => entry.type === "main_model")?.context_window;
|
|
974
|
+
if (capacity)
|
|
975
|
+
return Math.max(1, Math.floor(capacity * 0.8));
|
|
976
|
+
}
|
|
977
|
+
return contextCompressionThreshold(this.provider, this.model, this.mode);
|
|
742
978
|
}
|
|
743
979
|
async autoCompress(token) {
|
|
744
980
|
const threshold = this.compressThreshold();
|
|
@@ -760,7 +996,7 @@ export class WebSession {
|
|
|
760
996
|
this.publishLive();
|
|
761
997
|
let summary = "";
|
|
762
998
|
try {
|
|
763
|
-
summary = await new LLMClient(this.model, this.provider, this.intensity).summarize(transcript, {
|
|
999
|
+
summary = await (this.proTeam?.client("main_model") ?? new LLMClient(this.model, this.provider, this.intensity)).summarize(transcript, {
|
|
764
1000
|
cancelled: () => token !== this.turnToken || this.paused,
|
|
765
1001
|
});
|
|
766
1002
|
}
|
|
@@ -813,7 +1049,7 @@ export class WebSession {
|
|
|
813
1049
|
if (chat.length === 0)
|
|
814
1050
|
return true;
|
|
815
1051
|
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);
|
|
1052
|
+
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
1053
|
this.saveError = null;
|
|
818
1054
|
return true;
|
|
819
1055
|
}
|
package/dist/web/settings.js
CHANGED
|
@@ -1,28 +1,58 @@
|
|
|
1
|
-
import { DEFAULT_CONFIG, INTENSITY_LEVELS, WEB_LANGUAGES, contextWindow, isContextWindow, loadConfig, loadModels, normalizeApprovalMode, normalizeWebLanguage, resolveModelSelection, saveConfig, saveModels, } from "../config.js";
|
|
1
|
+
import { DEFAULT_CONFIG, INTENSITY_LEVELS, WEB_LANGUAGES, contextWindow, intensityForModel, isContextWindow, loadConfig, loadModels, modelContextWindow, modelContextWindowKey, modelIntensities, normalizeApprovalMode, normalizeIntensity, normalizeWebLanguage, resolveModelSelection, saveConfig, saveModels, } from "../config.js";
|
|
2
2
|
import { hashWebPassword, validateWebPassword } from "./auth.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import { defaultProPreset, loadProModels, proMaxRecursionDepth, requireProPreset } from "../pro/config.js";
|
|
4
|
+
/**
|
|
5
|
+
* Public settings use a whitelist: API keys and cached credentials stay on disk.
|
|
6
|
+
*
|
|
7
|
+
* `mode` selects the workspace. The Pro workspace has its own defaults,
|
|
8
|
+
* language, plan and reviewer settings; the login password and the approval
|
|
9
|
+
* policy are layered in from the shared config by loadConfig(), so the two
|
|
10
|
+
* pages cannot drift apart on those.
|
|
11
|
+
*/
|
|
12
|
+
export function getWebSettings(mode = "standard") {
|
|
13
|
+
const config = { ...DEFAULT_CONFIG, ...loadConfig(mode) };
|
|
14
|
+
const catalog = loadModels();
|
|
15
|
+
const providers = catalog.providers.map((provider) => ({
|
|
7
16
|
name: provider.name,
|
|
8
|
-
models: provider.models.map((model) => ({
|
|
17
|
+
models: provider.models.map((model) => ({
|
|
18
|
+
name: model.name,
|
|
19
|
+
configured: Boolean(model.api_key?.trim()),
|
|
20
|
+
context_window: modelContextWindow(provider.name, model, mode),
|
|
21
|
+
// The browser only offers these, so it cannot submit a level the model
|
|
22
|
+
// does not declare. Missing field = every intensity.
|
|
23
|
+
intensities: modelIntensities(model),
|
|
24
|
+
})),
|
|
9
25
|
}));
|
|
10
26
|
const preferred = providers.find((p) => p.name === config.selected_provider);
|
|
11
27
|
const provider = preferred ?? providers.find((p) => p.models.some((m) => m.configured)) ?? providers[0];
|
|
12
28
|
const model = provider?.models.find((m) => m.name === config.selected_model) ??
|
|
13
29
|
provider?.models.find((m) => m.configured) ?? provider?.models[0];
|
|
14
|
-
const
|
|
30
|
+
const configuredModel = provider && model
|
|
31
|
+
? catalog.providers.find((p) => p.name === provider.name)?.models.find((m) => m.name === model.name) ?? null
|
|
32
|
+
: null;
|
|
33
|
+
const intensity = intensityForModel(configuredModel, config.selected_intensity ?? config.default_intensity, config.default_intensity);
|
|
15
34
|
return {
|
|
35
|
+
...(mode === "pro" ? { pro: {
|
|
36
|
+
presets: loadProModels().tier.map((preset) => ({
|
|
37
|
+
name: preset.name, chinese_name: preset.chinese_name,
|
|
38
|
+
"model-setting": preset["model-setting"].map((setting) => ({
|
|
39
|
+
type: setting.type, provider: setting.provider, model: setting.model,
|
|
40
|
+
intensity: setting.intensity, context_window: setting.context_window,
|
|
41
|
+
})),
|
|
42
|
+
})),
|
|
43
|
+
preset: defaultProPreset(),
|
|
44
|
+
max_recursion_depth: proMaxRecursionDepth(),
|
|
45
|
+
} } : {}),
|
|
16
46
|
providers,
|
|
17
47
|
intensities: INTENSITY_LEVELS,
|
|
18
48
|
languages: WEB_LANGUAGES.map((value) => ({ value, label: value === "zh" ? "简体中文" : "English" })),
|
|
19
49
|
selection: {
|
|
20
50
|
provider: provider?.name ?? "",
|
|
21
51
|
model: model?.name ?? "",
|
|
22
|
-
intensity
|
|
52
|
+
intensity,
|
|
23
53
|
},
|
|
24
54
|
settings: {
|
|
25
|
-
default_intensity:
|
|
55
|
+
default_intensity: intensityForModel(configuredModel, config.default_intensity, "High"),
|
|
26
56
|
model_context_window: model?.context_window ?? contextWindow(undefined),
|
|
27
57
|
approval_model: String(config.approval_model ?? ""),
|
|
28
58
|
auto_approve_extreme_risk: config.auto_approve_extreme_risk === true,
|
|
@@ -31,27 +61,49 @@ export function getWebSettings() {
|
|
|
31
61
|
plan_first: config.plan_first !== false,
|
|
32
62
|
approval_mode: normalizeApprovalMode(config.approval_mode),
|
|
33
63
|
web_console: config.web_console !== false,
|
|
64
|
+
web_lan: config.web_lan === true,
|
|
34
65
|
},
|
|
35
66
|
};
|
|
36
67
|
}
|
|
37
68
|
/** Validate the complete patch before writing anything, preserving unrelated settings. */
|
|
38
|
-
export function updateWebSettings(value) {
|
|
69
|
+
export function updateWebSettings(value, mode = "standard") {
|
|
39
70
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
40
71
|
throw new Error("invalid settings body");
|
|
41
72
|
const body = value;
|
|
42
73
|
if (Object.keys(body).some((key) => key !== "selection" && key !== "settings"))
|
|
43
74
|
throw new Error("unknown settings field");
|
|
44
|
-
const config = loadConfig();
|
|
75
|
+
const config = loadConfig(mode);
|
|
76
|
+
// Resolve the requested model first: its declared intensities decide which
|
|
77
|
+
// default level can be saved with this patch. Settings that do not touch a
|
|
78
|
+
// model stay editable even when the catalogue is empty.
|
|
79
|
+
const chosen = body.selection !== undefined ? resolveModelSelection(body.selection, false) : undefined;
|
|
80
|
+
let target = chosen ?? null;
|
|
81
|
+
const settingsTarget = () => {
|
|
82
|
+
target ??= resolveModelSelection(getWebSettings(mode).selection, false);
|
|
83
|
+
return target;
|
|
84
|
+
};
|
|
45
85
|
if (body.settings !== undefined) {
|
|
46
86
|
if (!body.settings || typeof body.settings !== "object" || Array.isArray(body.settings))
|
|
47
87
|
throw new Error("invalid settings");
|
|
48
88
|
const patch = body.settings;
|
|
49
89
|
for (const [key, val] of Object.entries(patch)) {
|
|
50
|
-
if (key === "
|
|
51
|
-
|
|
90
|
+
if (key === "pro_preset" && mode === "pro") {
|
|
91
|
+
config.pro_preset = requireProPreset(val);
|
|
92
|
+
}
|
|
93
|
+
else if (key === "pro_max_recursion_depth" && mode === "pro") {
|
|
94
|
+
if (typeof val !== "number" || !Number.isSafeInteger(val) || val < 1 || val > 100) {
|
|
95
|
+
throw new Error("Pro recursion depth must be a whole number from 1 to 100");
|
|
96
|
+
}
|
|
97
|
+
config.pro_max_recursion_depth = val;
|
|
98
|
+
}
|
|
99
|
+
else if (key === "default_intensity") {
|
|
100
|
+
const level = normalizeIntensity(val);
|
|
101
|
+
if (!level)
|
|
52
102
|
throw new Error("invalid default intensity");
|
|
53
|
-
|
|
54
|
-
|
|
103
|
+
if (!modelIntensities(settingsTarget().model).includes(level))
|
|
104
|
+
throw new Error("this model does not support that thinking intensity");
|
|
105
|
+
config.default_intensity = level;
|
|
106
|
+
config.selected_intensity = level;
|
|
55
107
|
}
|
|
56
108
|
else if (key === "model_context_window") {
|
|
57
109
|
if (!isContextWindow(val))
|
|
@@ -93,6 +145,11 @@ export function updateWebSettings(value) {
|
|
|
93
145
|
throw new Error("web console must be a boolean");
|
|
94
146
|
config.web_console = val;
|
|
95
147
|
}
|
|
148
|
+
else if (key === "web_lan") {
|
|
149
|
+
if (typeof val !== "boolean")
|
|
150
|
+
throw new Error("LAN access must be a boolean");
|
|
151
|
+
config.web_lan = val;
|
|
152
|
+
}
|
|
96
153
|
else if (key === "web_password") {
|
|
97
154
|
validateWebPassword(val);
|
|
98
155
|
}
|
|
@@ -101,8 +158,7 @@ export function updateWebSettings(value) {
|
|
|
101
158
|
}
|
|
102
159
|
}
|
|
103
160
|
}
|
|
104
|
-
if (
|
|
105
|
-
const chosen = resolveModelSelection(body.selection, false);
|
|
161
|
+
if (chosen) {
|
|
106
162
|
config.selected_provider = chosen.provider;
|
|
107
163
|
config.selected_model = chosen.model.name;
|
|
108
164
|
config.selected_model_url = chosen.model.url;
|
|
@@ -116,12 +172,23 @@ export function updateWebSettings(value) {
|
|
|
116
172
|
config.web_password_hash = hashWebPassword(password);
|
|
117
173
|
const window = body.settings?.model_context_window;
|
|
118
174
|
if (window !== undefined) {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
175
|
+
const selected = settingsTarget();
|
|
176
|
+
if (mode === "pro") {
|
|
177
|
+
// Context capacity is part of a workspace's compression settings. Pro
|
|
178
|
+
// stores its own copy instead of editing the shared model catalog.
|
|
179
|
+
const current = config.model_context_windows;
|
|
180
|
+
const overrides = current && typeof current === "object" && !Array.isArray(current)
|
|
181
|
+
? { ...current }
|
|
182
|
+
: {};
|
|
183
|
+
overrides[modelContextWindowKey(selected.provider, selected.model.name)] = window;
|
|
184
|
+
config.model_context_windows = overrides;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
const models = loadModels();
|
|
188
|
+
const model = models.providers.find((p) => p.name === selected.provider).models.find((m) => m.name === selected.model.name);
|
|
189
|
+
model.context_window = window;
|
|
190
|
+
saveModels(models);
|
|
191
|
+
}
|
|
125
192
|
}
|
|
126
|
-
saveConfig(config);
|
|
193
|
+
saveConfig(config, mode);
|
|
127
194
|
}
|