@xingjisoft/neutrino 0.2.1 → 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 -436
- 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 +179 -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 +54 -38
- 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 +31 -4
- package/dist/web/server.js +611 -50
- package/dist/web/session.js +320 -20
- package/dist/web/settings.js +117 -24
- package/package.json +37 -37
- package/web/README.md +158 -16
- package/web/app.js +1603 -128
- package/web/i18n.js +382 -0
- package/web/index.html +289 -37
- package/web/markdown.js +710 -0
- package/web/neutrino-pro.gif +0 -0
- package/web/neutrino.png +0 -0
- package/web/pro.js +184 -0
- package/web/settings.js +322 -193
- package/web/style.css +1378 -42
package/dist/web/session.js
CHANGED
|
@@ -10,9 +10,13 @@
|
|
|
10
10
|
* registers itself in the live hub, so the regular `/api/live/*` endpoints
|
|
11
11
|
* (snapshot, stream, pause, resume, message) work unchanged.
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { findModel, contextCompressionThreshold, intensityForModel, loadConfig, loadPlannerPrompt, loadStepPrompt, loadSubAgentPrompt, normalizeApprovalMode, planFirstEnabled, loadModels, loadPrompt, saveSession, } from "../config.js";
|
|
14
15
|
import { COMPACT_KEEP, planCompaction, transcriptFor, modelHistory, normalizeContext, } from "../compact.js";
|
|
15
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";
|
|
16
20
|
import { advancePlan, buildPlannerInput, lastRequestText, needsPlan, parsePlan, planMessage, planTranscript, renderStepDirective, resumePlan, stopPlan, } from "../llm/plan.js";
|
|
17
21
|
import { DELEGATE_TOOL, executeTool, getSubAgentToolDefinitions, getToolDefinitions } from "../llm/tools.js";
|
|
18
22
|
import { buildSubAgentTask, formatSubAgentResult, runSubAgent as runSubAgentTask, } from "../llm/subagent.js";
|
|
@@ -31,6 +35,49 @@ const TOOL_PENDING = "…";
|
|
|
31
35
|
const SUB_AGENT_TOOLS = getSubAgentToolDefinitions();
|
|
32
36
|
/** Marker for the model-only summary, separate from the original transcript. */
|
|
33
37
|
const SUMMARY_PREFIX = "Context was compressed. Summary of the earlier part:";
|
|
38
|
+
/**
|
|
39
|
+
* A fresh session id in the same shape `saveSession` generates, so a new
|
|
40
|
+
* browser conversation has a stable identity before its first message exists.
|
|
41
|
+
*/
|
|
42
|
+
export function newSessionId(now = new Date()) {
|
|
43
|
+
return `${now.toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Start an empty conversation in a caller-chosen working directory. Nothing is
|
|
47
|
+
* written to disk until the first message, so an abandoned new-chat dialog
|
|
48
|
+
* cannot litter the history with empty transcripts.
|
|
49
|
+
*/
|
|
50
|
+
export function createWebSession(cwd, selection, approvalMode, mode = "standard", proPreset) {
|
|
51
|
+
const now = new Date();
|
|
52
|
+
const source = {
|
|
53
|
+
id: newSessionId(now),
|
|
54
|
+
created_at: now.toISOString(),
|
|
55
|
+
updated_at: now.toISOString(),
|
|
56
|
+
model: selection.model.name,
|
|
57
|
+
provider: selection.provider,
|
|
58
|
+
intensity: selection.intensity,
|
|
59
|
+
cwd,
|
|
60
|
+
title: "",
|
|
61
|
+
messages: [],
|
|
62
|
+
tokens: { prompt: 0, completion: 0 },
|
|
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",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
34
81
|
function parseArguments(raw) {
|
|
35
82
|
const text = raw?.trim() ?? "";
|
|
36
83
|
if (!text)
|
|
@@ -63,15 +110,27 @@ export function resolveModel(modelName, providerName) {
|
|
|
63
110
|
return { ok: true, model };
|
|
64
111
|
}
|
|
65
112
|
/** Build a web session from a saved transcript, or explain why we cannot. */
|
|
66
|
-
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
|
+
}
|
|
67
124
|
const updated = selection ? { ...source, ...selection } : source;
|
|
68
125
|
const resolved = resolveModel(updated.model, updated.provider);
|
|
69
126
|
if (!resolved.ok)
|
|
70
127
|
return resolved;
|
|
71
|
-
return { ok: true, session: new WebSession(updated, resolved.model) };
|
|
128
|
+
return { ok: true, session: new WebSession(updated, resolved.model, mode) };
|
|
72
129
|
}
|
|
73
130
|
export class WebSession {
|
|
74
131
|
key = null;
|
|
132
|
+
/** Workspace this conversation belongs to; keeps Pro and standard separate. */
|
|
133
|
+
mode;
|
|
75
134
|
model;
|
|
76
135
|
provider;
|
|
77
136
|
intensity;
|
|
@@ -90,6 +149,10 @@ export class WebSession {
|
|
|
90
149
|
plan = null;
|
|
91
150
|
/** True while the planner agent is writing the steps. */
|
|
92
151
|
planning = false;
|
|
152
|
+
proPreset = null;
|
|
153
|
+
proTeam = null;
|
|
154
|
+
proState;
|
|
155
|
+
proAbort = null;
|
|
93
156
|
/** What the sub-agent is doing, or null when none is running. */
|
|
94
157
|
subagent = null;
|
|
95
158
|
saveError = null;
|
|
@@ -108,11 +171,16 @@ export class WebSession {
|
|
|
108
171
|
pendingApproval = null;
|
|
109
172
|
/** Reviewer count while an automated review runs, null otherwise. */
|
|
110
173
|
reviewing = null;
|
|
111
|
-
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());
|
|
112
178
|
this.model = model;
|
|
113
|
-
this.approvalMode = normalizeApprovalMode(source.approval_mode ?? loadConfig().approval_mode);
|
|
179
|
+
this.approvalMode = normalizeApprovalMode(source.approval_mode ?? loadConfig(this.mode).approval_mode);
|
|
114
180
|
this.provider = source.provider;
|
|
115
|
-
|
|
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);
|
|
116
184
|
this.dir = source.cwd;
|
|
117
185
|
this.sessionId = source.id;
|
|
118
186
|
this.messages = source.messages.slice();
|
|
@@ -137,6 +205,7 @@ export class WebSession {
|
|
|
137
205
|
if (this.disposed)
|
|
138
206
|
return;
|
|
139
207
|
this.disposed = true;
|
|
208
|
+
this.proAbort?.abort();
|
|
140
209
|
this.turnToken++;
|
|
141
210
|
this.busy = false;
|
|
142
211
|
if (this.pendingApproval) {
|
|
@@ -153,6 +222,8 @@ export class WebSession {
|
|
|
153
222
|
return this.dir;
|
|
154
223
|
}
|
|
155
224
|
liveConfigure(selection) {
|
|
225
|
+
if (this.mode === "pro")
|
|
226
|
+
return { ok: false, error: "choose a Pro preset to change professional models" };
|
|
156
227
|
if (this.disposed)
|
|
157
228
|
return { ok: false, error: "this session has ended" };
|
|
158
229
|
if (this.busy || this.pendingApproval) {
|
|
@@ -160,13 +231,37 @@ export class WebSession {
|
|
|
160
231
|
}
|
|
161
232
|
this.model = { ...selection.model };
|
|
162
233
|
this.provider = selection.provider;
|
|
163
|
-
this.intensity = selection.intensity;
|
|
234
|
+
this.intensity = intensityForModel(this.model, selection.intensity, loadConfig(this.mode).default_intensity);
|
|
164
235
|
this.save();
|
|
165
236
|
this.publishLive();
|
|
166
237
|
return { ok: true };
|
|
167
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
|
+
}
|
|
168
260
|
liveMeta() {
|
|
169
261
|
return {
|
|
262
|
+
...(this.mode === "pro" ? { pro: this.proState ?? {
|
|
263
|
+
preset: this.proPreset, checking: false, checkId: "", models: [], fallbacks: [],
|
|
264
|
+
} } : {}),
|
|
170
265
|
sessionId: this.sessionId,
|
|
171
266
|
title: this.title(),
|
|
172
267
|
provider: this.provider,
|
|
@@ -221,6 +316,7 @@ export class WebSession {
|
|
|
221
316
|
if (this.paused || (!this.busy && !this.queued))
|
|
222
317
|
return;
|
|
223
318
|
this.paused = true;
|
|
319
|
+
this.proAbort?.abort();
|
|
224
320
|
this.turnToken++;
|
|
225
321
|
// A queued decision would otherwise keep the promise (and the gate) alive.
|
|
226
322
|
if (this.pendingApproval) {
|
|
@@ -231,6 +327,13 @@ export class WebSession {
|
|
|
231
327
|
this.busy = false;
|
|
232
328
|
this.retainStreamingContent();
|
|
233
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
|
+
}
|
|
234
337
|
stopPlan(this.plan);
|
|
235
338
|
this.save();
|
|
236
339
|
this.publishLive();
|
|
@@ -267,6 +370,43 @@ export class WebSession {
|
|
|
267
370
|
this.save();
|
|
268
371
|
this.publishLive();
|
|
269
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* Start a fresh chat in the same working directory, mirroring the terminal's
|
|
375
|
+
* clear shortcut. The finished transcript is checkpointed first and stays on
|
|
376
|
+
* disk; this session simply moves on under a new id.
|
|
377
|
+
*/
|
|
378
|
+
liveClear() {
|
|
379
|
+
if (this.disposed)
|
|
380
|
+
return;
|
|
381
|
+
if (!this.checkpoint.flush())
|
|
382
|
+
return;
|
|
383
|
+
this.proAbort?.abort();
|
|
384
|
+
this.proTeam = null;
|
|
385
|
+
this.proState = undefined;
|
|
386
|
+
this.turnToken++;
|
|
387
|
+
if (this.pendingApproval) {
|
|
388
|
+
const pending = this.pendingApproval;
|
|
389
|
+
this.pendingApproval = null;
|
|
390
|
+
pending.resolve(false);
|
|
391
|
+
}
|
|
392
|
+
this.busy = false;
|
|
393
|
+
this.paused = false;
|
|
394
|
+
this.queued = null;
|
|
395
|
+
this.compressing = false;
|
|
396
|
+
this.retrying = null;
|
|
397
|
+
this.subagent = null;
|
|
398
|
+
this.reviewing = null;
|
|
399
|
+
this.streamingContent = null;
|
|
400
|
+
this.plan = null;
|
|
401
|
+
this.planning = false;
|
|
402
|
+
this.messages = [];
|
|
403
|
+
this.context = null;
|
|
404
|
+
this.lastPromptTokens = 0;
|
|
405
|
+
this.saveError = null;
|
|
406
|
+
this.sessionId = newSessionId();
|
|
407
|
+
this.publishLive();
|
|
408
|
+
this.touch();
|
|
409
|
+
}
|
|
270
410
|
// ---- turn driving ----------------------------------------------------
|
|
271
411
|
send(text) {
|
|
272
412
|
const trimmed = text.trim();
|
|
@@ -310,6 +450,8 @@ export class WebSession {
|
|
|
310
450
|
* pass, exactly as it did before planning existed.
|
|
311
451
|
*/
|
|
312
452
|
async runTurn(token, turnMessages, approvalContext) {
|
|
453
|
+
if (this.mode === "pro")
|
|
454
|
+
return this.runProTurn(token, turnMessages, approvalContext);
|
|
313
455
|
await this.autoCompress(token);
|
|
314
456
|
if (token !== this.turnToken || this.paused)
|
|
315
457
|
return;
|
|
@@ -341,6 +483,127 @@ export class WebSession {
|
|
|
341
483
|
}
|
|
342
484
|
this.onTurnFinished();
|
|
343
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
|
+
}
|
|
344
607
|
/**
|
|
345
608
|
* The transcript this turn may see: the messages it started with plus what it
|
|
346
609
|
* added since. A user message queued while the turn runs belongs to the next
|
|
@@ -366,7 +629,7 @@ export class WebSession {
|
|
|
366
629
|
* a plan must improve a turn, never be a new way for one to fail.
|
|
367
630
|
*/
|
|
368
631
|
async makePlan(token) {
|
|
369
|
-
if (!planFirstEnabled())
|
|
632
|
+
if (!planFirstEnabled(this.mode))
|
|
370
633
|
return null;
|
|
371
634
|
const request = lastRequestText(this.messages);
|
|
372
635
|
if (!request.trim())
|
|
@@ -424,8 +687,9 @@ export class WebSession {
|
|
|
424
687
|
buildApiMessages(messages = this.messages, directive = "") {
|
|
425
688
|
// A step directive extends the system prompt: the plan is an instruction, not
|
|
426
689
|
// something the user just said.
|
|
690
|
+
const prompt = this.mode === "pro" ? loadProPrompt("main_model") : loadPrompt();
|
|
427
691
|
const api = [
|
|
428
|
-
{ role: "system", content: directive ? `${
|
|
692
|
+
{ role: "system", content: directive ? `${prompt}\n\n${directive}` : prompt },
|
|
429
693
|
];
|
|
430
694
|
for (const m of modelHistory(messages, this.context)) {
|
|
431
695
|
if (m.role === "user" || m.role === "assistant") {
|
|
@@ -443,12 +707,13 @@ export class WebSession {
|
|
|
443
707
|
* the caller owns the turn's end because a planned turn runs this per step.
|
|
444
708
|
*/
|
|
445
709
|
async agentLoop(apiMessages, token, approvalContext) {
|
|
446
|
-
const tools = getToolDefinitions();
|
|
710
|
+
const tools = this.mode === "pro" ? getProToolDefinitions() : getToolDefinitions();
|
|
711
|
+
const team = this.proTeam;
|
|
447
712
|
try {
|
|
448
713
|
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
|
449
714
|
if (token !== this.turnToken || this.paused)
|
|
450
715
|
return false;
|
|
451
|
-
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);
|
|
452
717
|
let text = "";
|
|
453
718
|
this.streamingContent = "";
|
|
454
719
|
this.publishLive();
|
|
@@ -488,6 +753,7 @@ export class WebSession {
|
|
|
488
753
|
role: "assistant",
|
|
489
754
|
content: text,
|
|
490
755
|
reasoning_content: client.reasoningContent ?? "",
|
|
756
|
+
...(client.anthropicContent.length ? { anthropic_content: client.anthropicContent } : {}),
|
|
491
757
|
...(toolCalls.length > 0
|
|
492
758
|
? {
|
|
493
759
|
tool_calls: toolCalls.map((call) => ({
|
|
@@ -501,8 +767,13 @@ export class WebSession {
|
|
|
501
767
|
}
|
|
502
768
|
this.save();
|
|
503
769
|
this.publishLive();
|
|
504
|
-
if (toolCalls.length === 0)
|
|
505
|
-
|
|
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
|
+
}
|
|
506
777
|
for (const call of toolCalls) {
|
|
507
778
|
if (token !== this.turnToken || this.paused)
|
|
508
779
|
return false;
|
|
@@ -554,7 +825,9 @@ export class WebSession {
|
|
|
554
825
|
}
|
|
555
826
|
this.save();
|
|
556
827
|
}
|
|
557
|
-
|
|
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;
|
|
558
831
|
}
|
|
559
832
|
catch (err) {
|
|
560
833
|
if (token !== this.turnToken)
|
|
@@ -588,15 +861,28 @@ export class WebSession {
|
|
|
588
861
|
const risk = assessToolCall(tool, args, this.dir);
|
|
589
862
|
if (!risk)
|
|
590
863
|
return { approved: true, source: "prompt", reason: "routine call" };
|
|
864
|
+
const approvalToken = this.turnToken;
|
|
865
|
+
const team = this.proTeam;
|
|
591
866
|
const gate = new ApprovalGate({
|
|
592
867
|
askHuman: () => new Promise((resolve) => {
|
|
868
|
+
if (this.mode === "pro" && (approvalToken !== this.turnToken || this.paused || this.disposed)) {
|
|
869
|
+
resolve(false);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
593
872
|
this.pendingApproval = { tool, args, risk, resolve };
|
|
594
873
|
this.publishLive();
|
|
595
874
|
}),
|
|
596
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
|
+
} : {}),
|
|
597
881
|
autoApprovalEnabled: () => this.approvalMode === "auto",
|
|
598
|
-
extremeAutoApprovalEnabled: () => loadConfig().auto_approve_extreme_risk === true,
|
|
882
|
+
extremeAutoApprovalEnabled: () => loadConfig(this.mode).auto_approve_extreme_risk === true,
|
|
599
883
|
onReview: (info) => {
|
|
884
|
+
if (this.mode === "pro" && (approvalToken !== this.turnToken || this.paused || this.disposed))
|
|
885
|
+
return;
|
|
600
886
|
this.reviewing = info
|
|
601
887
|
? {
|
|
602
888
|
total: info.total, round: info.round ?? 1, rounds: info.rounds ?? 1,
|
|
@@ -610,7 +896,7 @@ export class WebSession {
|
|
|
610
896
|
}
|
|
611
897
|
/** Reviewer model: `approval_model` when set, otherwise the chat model. */
|
|
612
898
|
reviewerModel() {
|
|
613
|
-
const name = String(loadConfig().approval_model ?? "").trim();
|
|
899
|
+
const name = String(loadConfig(this.mode).approval_model ?? "").trim();
|
|
614
900
|
const found = name ? findModel(name) : null;
|
|
615
901
|
if (found) {
|
|
616
902
|
const usable = resolveModel(found.model.name, found.provider);
|
|
@@ -627,6 +913,13 @@ export class WebSession {
|
|
|
627
913
|
* approval gate and abort signal live.
|
|
628
914
|
*/
|
|
629
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
|
+
}
|
|
630
923
|
if (name !== DELEGATE_TOOL)
|
|
631
924
|
return executeTool(name, args, this.dir);
|
|
632
925
|
return this.delegate(args, toolMsg, token, apiMessages, approvalContext);
|
|
@@ -674,7 +967,14 @@ export class WebSession {
|
|
|
674
967
|
}
|
|
675
968
|
// ---- compression -----------------------------------------------------
|
|
676
969
|
compressThreshold() {
|
|
677
|
-
|
|
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);
|
|
678
978
|
}
|
|
679
979
|
async autoCompress(token) {
|
|
680
980
|
const threshold = this.compressThreshold();
|
|
@@ -696,7 +996,7 @@ export class WebSession {
|
|
|
696
996
|
this.publishLive();
|
|
697
997
|
let summary = "";
|
|
698
998
|
try {
|
|
699
|
-
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, {
|
|
700
1000
|
cancelled: () => token !== this.turnToken || this.paused,
|
|
701
1001
|
});
|
|
702
1002
|
}
|
|
@@ -725,7 +1025,7 @@ export class WebSession {
|
|
|
725
1025
|
return line.length > 80 ? `${line.slice(0, 77)}…` : line;
|
|
726
1026
|
}
|
|
727
1027
|
}
|
|
728
|
-
return "
|
|
1028
|
+
return "";
|
|
729
1029
|
}
|
|
730
1030
|
retainStreamingContent() {
|
|
731
1031
|
if (this.streamingContent) {
|
|
@@ -749,7 +1049,7 @@ export class WebSession {
|
|
|
749
1049
|
if (chat.length === 0)
|
|
750
1050
|
return true;
|
|
751
1051
|
try {
|
|
752
|
-
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);
|
|
753
1053
|
this.saveError = null;
|
|
754
1054
|
return true;
|
|
755
1055
|
}
|