@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.
Files changed (43) hide show
  1. package/README.md +312 -436
  2. package/assets/pro/backend_output_model.txt +5 -0
  3. package/assets/pro/claude-module-prompt.txt +65 -0
  4. package/assets/pro/common.txt +9 -0
  5. package/assets/pro/confirm_model.txt +5 -0
  6. package/assets/pro/front_output_model.txt +9 -0
  7. package/assets/pro/genimage_model.txt +1 -0
  8. package/assets/pro/main_model.txt +5 -0
  9. package/assets/pro/model-template.json +376 -0
  10. package/assets/pro/modeling_model.txt +5 -0
  11. package/assets/pro/plan_model.txt +8 -0
  12. package/assets/pro/thinking_model.txt +3 -0
  13. package/dist/app.js +43 -7
  14. package/dist/bin.js +5 -2
  15. package/dist/browser.js +57 -0
  16. package/dist/config.js +179 -26
  17. package/dist/index.js +1 -1
  18. package/dist/llm/approval.js +6 -2
  19. package/dist/llm/client.js +197 -28
  20. package/dist/llm/subagent.js +37 -1
  21. package/dist/pro/agents.js +104 -0
  22. package/dist/pro/config.js +88 -0
  23. package/dist/pro/preflight.js +172 -0
  24. package/dist/screens/chat.js +54 -38
  25. package/dist/screens/modelSelect.js +15 -4
  26. package/dist/screens/settings.js +9 -7
  27. package/dist/screens/start.js +48 -15
  28. package/dist/ui.js +58 -0
  29. package/dist/web/hub.js +31 -4
  30. package/dist/web/server.js +611 -50
  31. package/dist/web/session.js +320 -20
  32. package/dist/web/settings.js +117 -24
  33. package/package.json +37 -37
  34. package/web/README.md +158 -16
  35. package/web/app.js +1603 -128
  36. package/web/i18n.js +382 -0
  37. package/web/index.html +289 -37
  38. package/web/markdown.js +710 -0
  39. package/web/neutrino-pro.gif +0 -0
  40. package/web/neutrino.png +0 -0
  41. package/web/pro.js +184 -0
  42. package/web/settings.js +322 -193
  43. package/web/style.css +1378 -42
@@ -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 { findModel, contextCompressionThreshold, loadConfig, loadPlannerPrompt, loadStepPrompt, loadSubAgentPrompt, normalizeApprovalMode, planFirstEnabled, loadModels, loadPrompt, saveSession, } from "../config.js";
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
- this.intensity = source.intensity;
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 ? `${loadPrompt()}\n\n${directive}` : loadPrompt() },
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
- break;
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
- return true;
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
- return contextCompressionThreshold(this.provider, this.model);
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 "Resumed chat";
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
  }