@xingjisoft/neutrino 0.6.0 → 0.7.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 (169) hide show
  1. package/README.md +6 -1
  2. package/assets/fast-planner.txt +57 -0
  3. package/assets/fast-replanner.txt +28 -0
  4. package/dist/agents.d.ts +25 -0
  5. package/dist/app.d.ts +206 -0
  6. package/dist/app.js +15 -0
  7. package/dist/background.d.ts +96 -0
  8. package/dist/bin.d.ts +2 -0
  9. package/dist/bin.js +40 -3
  10. package/dist/browser.d.ts +18 -0
  11. package/dist/clipboard.d.ts +16 -0
  12. package/dist/colors.d.ts +29 -0
  13. package/dist/compact.d.ts +52 -0
  14. package/dist/config.d.ts +221 -0
  15. package/dist/config.js +22 -7
  16. package/dist/cwd.d.ts +19 -0
  17. package/dist/fast/artifacts.d.ts +26 -0
  18. package/dist/fast/artifacts.js +109 -0
  19. package/dist/fast/engine.d.ts +78 -0
  20. package/dist/fast/engine.js +657 -0
  21. package/dist/fast/evidence.d.ts +21 -0
  22. package/dist/fast/evidence.js +68 -0
  23. package/dist/fast/planner.d.ts +10 -0
  24. package/dist/fast/planner.js +51 -0
  25. package/dist/fast/pool.d.ts +17 -0
  26. package/dist/fast/pool.js +78 -0
  27. package/dist/fast/scheduler.d.ts +54 -0
  28. package/dist/fast/scheduler.js +383 -0
  29. package/dist/fast/types.d.ts +100 -0
  30. package/dist/fast/types.js +21 -0
  31. package/dist/fast/worker.d.ts +23 -0
  32. package/dist/fast/worker.js +185 -0
  33. package/dist/header.d.ts +3 -0
  34. package/dist/index.d.ts +5 -0
  35. package/dist/llm/approval.d.ts +204 -0
  36. package/dist/llm/approval.js +14 -3
  37. package/dist/llm/client.d.ts +118 -0
  38. package/dist/llm/client.js +57 -8
  39. package/dist/llm/codeExecutor.d.ts +14 -0
  40. package/dist/llm/codemode.d.ts +15 -0
  41. package/dist/llm/context.d.ts +29 -0
  42. package/dist/llm/context.js +17 -4
  43. package/dist/llm/diagnostics.d.ts +91 -0
  44. package/dist/llm/diagnostics.js +242 -0
  45. package/dist/llm/plan.d.ts +148 -0
  46. package/dist/llm/retry.d.ts +80 -0
  47. package/dist/llm/risk.d.ts +21 -0
  48. package/dist/llm/risk.js +7 -0
  49. package/dist/llm/runtime.d.ts +189 -0
  50. package/dist/llm/runtime.js +178 -24
  51. package/dist/llm/search.d.ts +2 -0
  52. package/dist/llm/searchNetwork.d.ts +8 -0
  53. package/dist/llm/subagent.d.ts +112 -0
  54. package/dist/llm/subagent.js +25 -12
  55. package/dist/llm/tools.d.ts +20 -0
  56. package/dist/llm/tools.js +4 -1
  57. package/dist/llm/workspace.d.ts +6 -0
  58. package/dist/llm/workspace.js +271 -0
  59. package/dist/markdown.d.ts +58 -0
  60. package/dist/migration.d.ts +8 -0
  61. package/dist/persistence.d.ts +14 -0
  62. package/dist/plugins/catalog.d.ts +20 -0
  63. package/dist/plugins/catalog.js +45 -0
  64. package/dist/plugins/cli.d.ts +1 -0
  65. package/dist/plugins/cli.js +106 -0
  66. package/dist/plugins/host.d.ts +121 -0
  67. package/dist/plugins/host.js +450 -0
  68. package/dist/plugins/manager.d.ts +24 -0
  69. package/dist/plugins/manager.js +288 -0
  70. package/dist/plugins/package.d.ts +5 -0
  71. package/dist/plugins/package.js +53 -0
  72. package/dist/plugins/runner.d.ts +1 -0
  73. package/dist/plugins/runner.js +227 -0
  74. package/dist/plugins/session.d.ts +12 -0
  75. package/dist/plugins/session.js +97 -0
  76. package/dist/plugins/terminal.d.ts +22 -0
  77. package/dist/plugins/terminal.js +33 -0
  78. package/dist/plugins/types.d.ts +178 -0
  79. package/dist/plugins/types.js +6 -0
  80. package/dist/plugins/validation.d.ts +14 -0
  81. package/dist/plugins/validation.js +174 -0
  82. package/dist/plugins/web-sdk.d.ts +58 -0
  83. package/dist/plugins/web-sdk.js +1 -0
  84. package/dist/pro/agents.d.ts +34 -0
  85. package/dist/pro/agents.js +1 -1
  86. package/dist/pro/config.d.ts +75 -0
  87. package/dist/pro/config.js +3 -2
  88. package/dist/pro/preflight.d.ts +21 -0
  89. package/dist/sanitize.d.ts +34 -0
  90. package/dist/screens/agentConversation.d.ts +28 -0
  91. package/dist/screens/agentHistory.d.ts +25 -0
  92. package/dist/screens/agentSelect.d.ts +16 -0
  93. package/dist/screens/chat.d.ts +292 -0
  94. package/dist/screens/chat.js +179 -65
  95. package/dist/screens/history.d.ts +23 -0
  96. package/dist/screens/modelSelect.d.ts +29 -0
  97. package/dist/screens/plugins.d.ts +29 -0
  98. package/dist/screens/plugins.js +196 -0
  99. package/dist/screens/settings.d.ts +42 -0
  100. package/dist/screens/start.d.ts +43 -0
  101. package/dist/screens/start.js +25 -17
  102. package/dist/screens/updatePrompt.d.ts +41 -0
  103. package/dist/selection.d.ts +75 -0
  104. package/dist/state.d.ts +26 -0
  105. package/dist/terminal/buffer.d.ts +47 -0
  106. package/dist/terminal/input.d.ts +70 -0
  107. package/dist/terminal/renderer.d.ts +43 -0
  108. package/dist/terminal/restore.d.ts +41 -0
  109. package/dist/terminal/screen.d.ts +57 -0
  110. package/dist/tray.d.ts +14 -0
  111. package/dist/ui.d.ts +123 -0
  112. package/dist/update.d.ts +71 -0
  113. package/dist/updateWatch.d.ts +43 -0
  114. package/dist/web/approvalQueue.d.ts +8 -0
  115. package/dist/web/approvalQueue.js +59 -0
  116. package/dist/web/auth.d.ts +14 -0
  117. package/dist/web/clientState.d.ts +16 -0
  118. package/dist/web/folderPicker.d.ts +34 -0
  119. package/dist/web/folderPicker.js +544 -0
  120. package/dist/web/hub.d.ts +213 -0
  121. package/dist/web/hub.js +14 -2
  122. package/dist/web/localApi.d.ts +11 -0
  123. package/dist/web/localApi.js +260 -0
  124. package/dist/web/plugins.d.ts +8 -0
  125. package/dist/web/plugins.js +64 -0
  126. package/dist/web/server.d.ts +81 -0
  127. package/dist/web/server.js +127 -19
  128. package/dist/web/service.d.ts +7 -0
  129. package/dist/web/service.js +1 -0
  130. package/dist/web/session.d.ts +176 -0
  131. package/dist/web/session.js +170 -21
  132. package/dist/web/settings.d.ts +65 -0
  133. package/dist/web/terminalBridge.d.ts +15 -0
  134. package/dist/web/terminalBridge.js +8 -2
  135. package/dist/width.d.ts +6 -0
  136. package/docs/plugins/README.md +126 -0
  137. package/docs/plugins/agent-sdk.md +92 -0
  138. package/docs/plugins/architecture.md +64 -0
  139. package/docs/plugins/compatibility.md +34 -0
  140. package/docs/plugins/development.md +55 -0
  141. package/docs/plugins/manifest.md +59 -0
  142. package/docs/plugins/manifest.schema.json +286 -0
  143. package/docs/plugins/security.md +53 -0
  144. package/docs/plugins/skills-prompts.md +69 -0
  145. package/docs/plugins/ui-sdk.md +109 -0
  146. package/examples/local-api.py +105 -0
  147. package/examples/plugins/cards/card.html +6 -0
  148. package/examples/plugins/cards/neutrino.plugin.json +42 -0
  149. package/examples/plugins/composer/editor.html +10 -0
  150. package/examples/plugins/composer/neutrino.plugin.json +10 -0
  151. package/examples/plugins/function-tools/agent.js +32 -0
  152. package/examples/plugins/function-tools/neutrino.plugin.json +15 -0
  153. package/examples/plugins/menus-tabs/neutrino.plugin.json +15 -0
  154. package/examples/plugins/menus-tabs/panel.html +6 -0
  155. package/examples/plugins/skills-prompts/neutrino.plugin.json +13 -0
  156. package/examples/plugins/skills-prompts/prompts/evidence.md +1 -0
  157. package/examples/plugins/skills-prompts/prompts/reviewer.md +1 -0
  158. package/examples/plugins/skills-prompts/skills/review/SKILL.md +8 -0
  159. package/examples/plugins/theme/neutrino.plugin.json +12 -0
  160. package/package.json +66 -42
  161. package/web/LOCAL-API.md +65 -0
  162. package/web/README.md +30 -8
  163. package/web/app.js +256 -67
  164. package/web/i18n.js +18 -3
  165. package/web/index.html +3 -1
  166. package/web/plugins.css +10 -0
  167. package/web/plugins.js +230 -0
  168. package/web/style.css +12 -3
  169. package/web/turns.js +7 -8
@@ -1,4 +1,6 @@
1
+ import { runPluginTurn, pluginCommand, pluginApprovalContext } from "../plugins/session.js";
1
2
  import path from "node:path";
3
+ import { ApprovalQueue } from "./approvalQueue.js";
2
4
  /**
3
5
  * Headless conversation runner for the web UI.
4
6
  *
@@ -12,9 +14,11 @@ import path from "node:path";
12
14
  * (snapshot, stream, pause, resume, message) work unchanged.
13
15
  */
14
16
  import { randomUUID } from "node:crypto";
17
+ import { FastAgentEngine } from "../fast/engine.js";
18
+ import { FAST_MODE, assertFastAvailable, fastSettings } from "../fast/types.js";
15
19
  import { findModel, contextCompressionThreshold, modelContextWindow, sessionDir, intensityForModel, loadConfig, loadPlannerPrompt, loadStepPrompt, loadSubAgentPrompt, normalizeApprovalMode, planFirstEnabled, loadModels, loadAgentPrompt, normalizeAgentMode, saveSession, } from "../config.js";
16
20
  import { COMPACT_KEEP, planCompaction, transcriptFor, modelHistory, normalizeContext, } from "../compact.js";
17
- import { TaskRuntime, currentTask, linkSignals, taskLimits, resumeTaskSnapshot } from "../llm/runtime.js";
21
+ import { TaskRuntime, TaskControlError, currentTask, linkSignals, taskLimits, taskBudgetScope, resumeTaskSnapshot } from "../llm/runtime.js";
18
22
  import { prepareContext, outputAllowance, finalTaskReport } from "../llm/context.js";
19
23
  import { FINISH_STEP_TOOL, FINISH_STEP_DEFINITION, finishPlanStep, preparePlan, settlePlan } from "../llm/plan.js";
20
24
  import { LLMClient, describeError } from "../llm/client.js";
@@ -34,7 +38,6 @@ const IDLE_DISPOSE_MS = 30 * 60 * 1000;
34
38
  /** Placeholder shown while a tool is still running. */
35
39
  const TOOL_PENDING = "…";
36
40
  /** A sub-agent gets the same tools minus delegation: one level, never a tree. */
37
- const SUB_AGENT_TOOLS = getSubAgentToolDefinitions();
38
41
  /** Marker for the model-only summary, separate from the original transcript. */
39
42
  const SUMMARY_PREFIX = "Context was compressed. Summary of the earlier part:";
40
43
  /**
@@ -50,6 +53,8 @@ export function newSessionId(now = new Date()) {
50
53
  * cannot litter the history with empty transcripts.
51
54
  */
52
55
  export function createWebSession(cwd, selection, approvalMode, mode = "standard", proPreset, agentMode = "standard") {
56
+ if (agentMode === FAST_MODE)
57
+ assertFastAvailable(loadConfig(mode));
53
58
  const now = new Date();
54
59
  const source = {
55
60
  id: newSessionId(now),
@@ -170,13 +175,19 @@ export class WebSession {
170
175
  lastTask;
171
176
  taskOutcome = "completed";
172
177
  turnToken = 0;
178
+ /** Set by a resume click: the next turn clears its tool calls without asking. */
179
+ autoApproveNextTurn = false;
180
+ autoApproveToken = null;
173
181
  agentMode = "standard";
174
182
  activeAgentMode = "standard";
183
+ fastState;
184
+ queuedAgentMode = null;
175
185
  turnAbort = null;
176
186
  lastPromptTokens = 0;
177
187
  disposed = false;
178
188
  idleTimer = null;
179
189
  /** Non-null while a tool call waits for the browser to answer. */
190
+ approvalQueue = new ApprovalQueue();
180
191
  pendingApproval = null;
181
192
  /** Reviewer count while an automated review runs, null otherwise. */
182
193
  reviewing = null;
@@ -197,6 +208,11 @@ export class WebSession {
197
208
  this.context = normalizeContext(source.context, this.messages.filter((m) => m.role !== "system").length);
198
209
  this.plan = resumePlan(source.plan ?? null);
199
210
  this.lastTask = resumeTaskSnapshot(source.task_run);
211
+ this.fastState = source.fast_run ? structuredClone(source.fast_run) : undefined;
212
+ if (this.fastState && !["completed", "failed", "cancelled"].includes(this.fastState.phase)) {
213
+ this.fastState.phase = "failed";
214
+ this.fastState.error = "Process restarted; artifacts preserved. Review partial state before resuming; tools are never replayed automatically.";
215
+ }
200
216
  this.tokens = {
201
217
  prompt: source.tokens?.prompt ?? 0,
202
218
  completion: source.tokens?.completion ?? 0,
@@ -216,6 +232,7 @@ export class WebSession {
216
232
  if (this.disposed)
217
233
  return;
218
234
  this.disposed = true;
235
+ this.approvalQueue.cancelWaiting();
219
236
  this.proAbort?.abort();
220
237
  this.turnAbort?.abort();
221
238
  this.turnToken++;
@@ -283,6 +300,7 @@ export class WebSession {
283
300
  approvalMode: this.approvalMode,
284
301
  agentMode: this.agentMode,
285
302
  activeAgentMode: this.busy ? this.activeAgentMode : this.agentMode,
303
+ ...(this.fastState ? { fast: this.fastState } : {}),
286
304
  task: this.taskRuntime?.snapshot() ?? this.lastTask,
287
305
  busy: this.busy,
288
306
  paused: this.paused,
@@ -295,7 +313,7 @@ export class WebSession {
295
313
  tokens: { prompt: this.tokens.prompt, completion: this.tokens.completion },
296
314
  subagent: this.subagent,
297
315
  approval: this.pendingApproval
298
- ? liveApprovalDetails(this.pendingApproval.tool, this.pendingApproval.args, this.pendingApproval.risk, this.dir)
316
+ ? { ...liveApprovalDetails(this.pendingApproval.tool, this.pendingApproval.args, this.pendingApproval.risk, this.dir), id: this.pendingApproval.id }
299
317
  : null,
300
318
  reviewing: this.reviewing,
301
319
  };
@@ -331,6 +349,8 @@ export class WebSession {
331
349
  livePause() {
332
350
  if (this.paused || (!this.busy && !this.queued))
333
351
  return;
352
+ if (this.fastState && this.activeAgentMode === FAST_MODE && this.busy)
353
+ this.fastState.phase = "cancelling";
334
354
  this.paused = true;
335
355
  this.proAbort?.abort();
336
356
  this.turnAbort?.abort();
@@ -358,14 +378,27 @@ export class WebSession {
358
378
  return;
359
379
  this.paused = false;
360
380
  this.publishLive();
361
- this.maybeFlushQueue();
381
+ if (this.busy)
382
+ return;
383
+ // Continue answers the approval the pause threw away: the turn that follows
384
+ // runs its tool calls through the gate instead of asking the human again.
385
+ const restart = !this.queued && lastRequestText(this.messages) !== "";
386
+ if (!this.queued && !restart)
387
+ return;
388
+ this.autoApproveNextTurn = true;
389
+ // A queued message starts the next turn on its own; otherwise the paused turn
390
+ // is restarted from the transcript the pause kept, partial answer included.
391
+ if (this.queued)
392
+ this.maybeFlushQueue();
393
+ else
394
+ this.startTurn();
362
395
  }
363
396
  liveSend(text) {
364
397
  this.send(text);
365
398
  }
366
- liveApprove(approved) {
399
+ liveApprove(approved, approvalId) {
367
400
  const pending = this.pendingApproval;
368
- if (!pending)
401
+ if (!pending || (approvalId !== undefined && pending.id !== approvalId))
369
402
  return false;
370
403
  this.pendingApproval = null;
371
404
  pending.resolve(approved);
@@ -375,6 +408,14 @@ export class WebSession {
375
408
  liveSetAgentMode(mode) {
376
409
  if (this.mode === "pro" || this.disposed)
377
410
  return { ok: false, error: "this session cannot change agent mode" };
411
+ if (mode === FAST_MODE) {
412
+ try {
413
+ assertFastAvailable(loadConfig(this.mode));
414
+ }
415
+ catch (error) {
416
+ return { ok: false, error: describeError(error) };
417
+ }
418
+ }
378
419
  this.agentMode = mode;
379
420
  this.save();
380
421
  this.publishLive();
@@ -410,6 +451,8 @@ export class WebSession {
410
451
  this.turnToken++;
411
452
  this.taskRuntime = null;
412
453
  this.lastTask = undefined;
454
+ this.fastState = undefined;
455
+ this.queuedAgentMode = null;
413
456
  if (this.pendingApproval) {
414
457
  const pending = this.pendingApproval;
415
458
  this.pendingApproval = null;
@@ -443,6 +486,7 @@ export class WebSession {
443
486
  this.save();
444
487
  if (this.busy || this.paused) {
445
488
  this.queued = trimmed;
489
+ this.queuedAgentMode = this.agentMode;
446
490
  this.publishLive();
447
491
  return;
448
492
  }
@@ -472,10 +516,13 @@ export class WebSession {
472
516
  const turnMessages = this.messages.slice();
473
517
  const approvalContext = captureApprovalContext(turnMessages);
474
518
  this.busy = true;
475
- this.activeAgentMode = this.agentMode;
519
+ this.activeAgentMode = this.queuedAgentMode ?? this.agentMode;
520
+ this.queuedAgentMode = null;
476
521
  this.turnAbort?.abort();
477
522
  this.turnAbort = new AbortController();
478
523
  const token = ++this.turnToken;
524
+ this.autoApproveToken = this.autoApproveNextTurn ? token : null;
525
+ this.autoApproveNextTurn = false;
479
526
  this.streamingContent = "";
480
527
  this.publishLive();
481
528
  void (async () => {
@@ -492,15 +539,26 @@ export class WebSession {
492
539
  */
493
540
  async runTurn(token, turnMessages, approvalContext) {
494
541
  const runtime = new TaskRuntime({
542
+ executionMode: this.mode === "pro" ? "pro" : this.activeAgentMode,
495
543
  request: approvalContext.prompt, priorUserRequests: approvalContext.priorUserRequests,
496
544
  cwd: this.dir, signal: this.turnAbort?.signal, limits: taskLimits(loadConfig(this.mode)),
545
+ budgetScope: taskBudgetScope(loadConfig(this.mode)),
497
546
  journalDir: path.join(sessionDir(this.mode), "tasks", path.basename(this.sessionId || "unsaved")),
498
547
  });
499
548
  this.taskRuntime = runtime;
500
549
  this.taskOutcome = "completed";
501
550
  let failed = false;
502
551
  try {
503
- await runtime.run(() => this.runTaskTurn(token, turnMessages, approvalContext));
552
+ await runPluginTurn(runtime, { profile: this.mode, cwd: this.dir, sessionId: this.sessionId ?? undefined, role: "main", request: approvalContext.prompt }, (name, args, signal) => this.requestApproval(name, args, approvalContext, signal), async () => {
553
+ const command = pluginCommand(approvalContext.prompt);
554
+ if (command) {
555
+ const result = await runtime.plugins.runCommand(command.name, command.args);
556
+ this.messages.push({ role: "assistant", content: typeof result === "string" ? result : JSON.stringify(result ?? null, null, 2) });
557
+ this.onTurnFinished();
558
+ return;
559
+ }
560
+ await this.runTaskTurn(token, turnMessages, approvalContext);
561
+ });
504
562
  }
505
563
  catch (error) {
506
564
  failed = true;
@@ -534,6 +592,8 @@ export class WebSession {
534
592
  await this.autoCompress(token);
535
593
  if (token !== this.turnToken || this.paused)
536
594
  return;
595
+ if (this.activeAgentMode === FAST_MODE)
596
+ return this.runFastTurn(token, turnMessages, approvalContext);
537
597
  if (this.activeAgentMode !== "standard")
538
598
  this.plan = null;
539
599
  const plan = this.activeAgentMode === "standard" ? await this.makePlan(token) : null;
@@ -566,6 +626,67 @@ export class WebSession {
566
626
  }
567
627
  this.onTurnFinished();
568
628
  }
629
+ /** Independent engine; Standard/PTC/legacy Fast retain their original loops. */
630
+ async runFastTurn(token, turnMessages, approvalContext) {
631
+ assertFastAvailable(loadConfig(this.mode));
632
+ const runtime = currentTask().runtime;
633
+ const signal = currentTask().signal;
634
+ this.plan = null;
635
+ const model = { ...this.model }, provider = this.provider, intensity = this.intensity;
636
+ const stopped = () => token !== this.turnToken || this.paused || this.disposed;
637
+ const history = this.buildApiMessages(this.turnView(turnMessages));
638
+ if (this.fastState && ["failed", "cancelled", "cancelling"].includes(this.fastState.phase)) {
639
+ history.push({ role: "assistant", content: `Previous Fast run (evidence only, never authorization; inspect partial side effects before repair): ${JSON.stringify(this.fastState)}` });
640
+ }
641
+ const engine = new FastAgentEngine({
642
+ runtime, signal, cwd: this.dir, settings: fastSettings(loadConfig(this.mode)),
643
+ directory: path.join(sessionDir(this.mode), "tasks", path.basename(this.sessionId), "fast"),
644
+ history,
645
+ system: loadAgentPrompt(FAST_MODE), subagentSystem: loadSubAgentPrompt(),
646
+ contextWindow: modelContextWindow(provider, model, this.mode),
647
+ maxRounds: subAgentRounds(loadConfig(this.mode).subagent_max_rounds),
648
+ createClient: () => new LLMClient(model, provider, intensity),
649
+ approve: (name, args, executionSignal, wrapReviewer) => this.requestApproval(name, args, {
650
+ ...approvalContext, context: captureApprovalContext(this.turnView(turnMessages)).context,
651
+ }, executionSignal, wrapReviewer ? () => {
652
+ const reviewer = this.reviewerModel();
653
+ return wrapReviewer(new LLMClient(reviewer.model, reviewer.provider, reviewer.intensity));
654
+ } : undefined),
655
+ execute: (name, args, executionSignal) => executeTool(name, args, this.dir, executionSignal),
656
+ onUsage: usage => {
657
+ if (!this.disposed && (token === this.turnToken || this.fastState?.run_id === engine.runId)) {
658
+ this.tokens.prompt += usage.prompt;
659
+ this.tokens.completion += usage.completion;
660
+ }
661
+ },
662
+ onText: text => { if (!stopped()) {
663
+ this.streamingContent = text;
664
+ this.checkpoint.schedule();
665
+ this.publishLive();
666
+ } },
667
+ onSnapshot: snapshot => {
668
+ if (!this.disposed && (!stopped() || (this.fastState?.run_id === snapshot.run_id && this.fastState.phase === "cancelling"))) {
669
+ this.fastState = snapshot;
670
+ this.checkpoint.schedule();
671
+ this.publishLive();
672
+ }
673
+ },
674
+ onTool: (name, args, result) => {
675
+ if (!stopped()) {
676
+ this.messages.push({ role: "tool", name, arguments: args, result });
677
+ this.save();
678
+ this.publishLive();
679
+ }
680
+ },
681
+ });
682
+ const result = await engine.run();
683
+ if (stopped())
684
+ return;
685
+ this.fastState = result.snapshot;
686
+ this.taskOutcome = result.status;
687
+ this.messages.push({ role: "assistant", content: result.report });
688
+ this.onTurnFinished();
689
+ }
569
690
  /** Freeze one resolved team per task, plan first, then give the whole brief to main. */
570
691
  async runProTurn(token, turnMessages, approvalContext) {
571
692
  const taskSignal = currentTask()?.signal;
@@ -723,6 +844,7 @@ export class WebSession {
723
844
  try {
724
845
  const client = new LLMClient(this.model, this.provider, this.intensity);
725
846
  const answer = await client.complete(loadPlannerPrompt(), buildPlannerInput({ request, cwd: this.dir, transcript: planTranscript(this.messages) }), {
847
+ ...(currentTask()?.runtime.diagnostics ? { diagnostic: { role: "planning" } } : {}),
726
848
  onRetry: (notice) => {
727
849
  this.retrying = notice;
728
850
  this.publishLive();
@@ -845,7 +967,7 @@ export class WebSession {
845
967
  return false;
846
968
  }
847
969
  if (client.outcome?.error?.code.endsWith("_budget") && currentTask()) {
848
- const report = await finalTaskReport(currentTask().runtime, () => team?.client("main_model") ?? new LLMClient(this.model, this.provider, this.intensity), usage => { this.tokens.prompt += usage.prompt; this.tokens.completion += usage.completion; });
970
+ const report = await finalTaskReport(currentTask().runtime, () => team?.client("main_model") ?? new LLMClient(this.model, this.provider, this.intensity), usage => { this.tokens.prompt += usage.prompt; this.tokens.completion += usage.completion; }, apiMessages);
849
971
  if (token !== this.turnToken || this.paused)
850
972
  return false;
851
973
  this.taskOutcome = "partial";
@@ -941,6 +1063,11 @@ export class WebSession {
941
1063
  result = await this.runTool(call.name, args, toolMsg, token, apiMessages, approvalContext);
942
1064
  }
943
1065
  catch (err) {
1066
+ if (err instanceof TaskControlError && err.code.endsWith("_budget")) {
1067
+ toolMsg.result = `[Not executed: ${describeError(err)}]`;
1068
+ this.save();
1069
+ throw err;
1070
+ }
944
1071
  if (token === this.turnToken)
945
1072
  this.taskOutcome = "failed";
946
1073
  result = `Error: ${err instanceof Error ? err.message : String(err)}`;
@@ -961,6 +1088,15 @@ export class WebSession {
961
1088
  catch (err) {
962
1089
  if (token !== this.turnToken)
963
1090
  return false;
1091
+ if (err instanceof TaskControlError && err.code.endsWith("_budget") && currentTask()) {
1092
+ const report = await finalTaskReport(currentTask().runtime, () => team?.client("main_model") ?? new LLMClient(this.model, this.provider, this.intensity), usage => { this.tokens.prompt += usage.prompt; this.tokens.completion += usage.completion; }, apiMessages);
1093
+ if (token !== this.turnToken || this.paused)
1094
+ return false;
1095
+ this.taskOutcome = "partial";
1096
+ this.streamingContent = null;
1097
+ this.messages.push({ role: "assistant", content: report });
1098
+ return false;
1099
+ }
964
1100
  if (this.streamingContent) {
965
1101
  this.messages.push({ role: "assistant", content: this.streamingContent });
966
1102
  }
@@ -976,6 +1112,7 @@ export class WebSession {
976
1112
  this.retrying = null;
977
1113
  this.busy = false;
978
1114
  this.paused = false;
1115
+ this.autoApproveToken = null;
979
1116
  this.streamingContent = null;
980
1117
  this.save();
981
1118
  this.publishLive();
@@ -986,11 +1123,15 @@ export class WebSession {
986
1123
  }
987
1124
  // ---- approval gate ---------------------------------------------------
988
1125
  /** Classify a tool call and, when it is risky, wait for a decision. */
989
- async requestApproval(tool, args, context = captureApprovalContext(this.messages), signal = currentTask()?.runtime.signal ?? this.turnAbort?.signal) {
1126
+ async requestApproval(tool, args, context = captureApprovalContext(this.messages), signal = currentTask()?.runtime.signal ?? this.turnAbort?.signal, createReviewerClient) {
1127
+ context = pluginApprovalContext(context);
990
1128
  const linked = linkSignals(signal, currentTask()?.signal);
991
1129
  signal = linked.signal;
992
1130
  try {
993
1131
  currentTask()?.runtime.assertTool(tool, args);
1132
+ const plugins = currentTask()?.runtime.plugins;
1133
+ if (plugins)
1134
+ await plugins.guardTool(tool, args);
994
1135
  }
995
1136
  catch (error) {
996
1137
  linked.dispose();
@@ -1004,21 +1145,29 @@ export class WebSession {
1004
1145
  const approvalToken = this.turnToken;
1005
1146
  const team = this.proTeam;
1006
1147
  const gate = new ApprovalGate({
1007
- askHuman: () => new Promise((resolve) => {
1008
- if (approvalToken !== this.turnToken || this.paused || this.disposed || signal?.aborted === true) {
1009
- resolve(false);
1010
- return;
1011
- }
1012
- this.pendingApproval = { tool, args, risk, resolve };
1013
- this.publishLive();
1014
- }),
1148
+ askHuman: () => {
1149
+ const requestedMode = this.approvalMode;
1150
+ return this.approvalQueue.run(() => new Promise((resolve) => {
1151
+ if (approvalToken !== this.turnToken || this.paused || this.disposed || signal?.aborted === true) {
1152
+ resolve(false);
1153
+ return;
1154
+ }
1155
+ if (requestedMode !== this.approvalMode) {
1156
+ resolve(null);
1157
+ return;
1158
+ }
1159
+ this.pendingApproval = { id: randomUUID(), tool, args, risk, resolve };
1160
+ this.publishLive();
1161
+ }), signal);
1162
+ },
1015
1163
  reviewer: () => this.reviewerModel(),
1164
+ ...(createReviewerClient ? { createReviewerClient } : {}),
1016
1165
  ...(this.mode === "pro" && team ? {
1017
1166
  createReviewerClient: () => team.client("confirm_model"),
1018
1167
  reviewerSystemPrompt: () => loadProPrompt("confirm_model"),
1019
1168
  } : {}),
1020
1169
  cancelled: () => approvalToken !== this.turnToken || this.paused || this.disposed || signal?.aborted === true,
1021
- autoApprovalEnabled: () => this.approvalMode === "auto",
1170
+ autoApprovalEnabled: () => this.approvalMode === "auto" || approvalToken === this.autoApproveToken,
1022
1171
  extremeAutoApprovalEnabled: () => loadConfig(this.mode).auto_approve_extreme_risk === true,
1023
1172
  onReview: (info) => {
1024
1173
  if (approvalToken !== this.turnToken || this.paused || this.disposed || signal?.aborted === true)
@@ -1194,7 +1343,7 @@ export class WebSession {
1194
1343
  createClient: () => new LLMClient(this.model, this.provider, this.intensity),
1195
1344
  system: loadSubAgentPrompt(),
1196
1345
  task: buildSubAgentTask({ task, context, cwd: this.dir, rounds: maxRounds }),
1197
- tools: SUB_AGENT_TOOLS,
1346
+ tools: getSubAgentToolDefinitions(),
1198
1347
  maxRounds,
1199
1348
  approve: (name, callArgs) => this.requestApproval(name, callArgs, approval, signal),
1200
1349
  execute: (name, callArgs) => name === DELEGATE_TOOL
@@ -1332,7 +1481,7 @@ export class WebSession {
1332
1481
  if (chat.length === 0)
1333
1482
  return true;
1334
1483
  try {
1335
- 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, this.agentMode, this.taskRuntime?.snapshot() ?? this.lastTask);
1484
+ 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, this.agentMode, this.taskRuntime?.snapshot() ?? this.lastTask, this.fastState);
1336
1485
  this.saveError = null;
1337
1486
  return true;
1338
1487
  }
@@ -0,0 +1,65 @@
1
+ import { type Mode } from "../config.js";
2
+ /**
3
+ * Public settings use a whitelist: API keys and cached credentials stay on disk.
4
+ *
5
+ * `mode` selects the workspace. The Pro workspace has its own defaults,
6
+ * language, plan and reviewer settings; the login password and the approval
7
+ * policy are layered in from the shared config by loadConfig(), so the two
8
+ * pages cannot drift apart on those.
9
+ */
10
+ export declare function getWebSettings(mode?: Mode): {
11
+ providers: {
12
+ name: string;
13
+ models: {
14
+ name: string;
15
+ configured: boolean;
16
+ context_window: number;
17
+ intensities: string[];
18
+ }[];
19
+ }[];
20
+ intensities: string[];
21
+ languages: {
22
+ value: "en" | "zh";
23
+ label: string;
24
+ }[];
25
+ selection: {
26
+ provider: string;
27
+ model: string;
28
+ intensity: string;
29
+ };
30
+ settings: {
31
+ default_intensity: string;
32
+ model_context_window: number;
33
+ approval_model: string;
34
+ auto_approve_extreme_risk: boolean;
35
+ sound_alerts: boolean;
36
+ language: "en" | "zh";
37
+ plan_first: boolean;
38
+ approval_mode: import("../config.js").ApprovalMode;
39
+ web_console: boolean;
40
+ web_lan: boolean;
41
+ background: boolean;
42
+ };
43
+ pro?: {
44
+ presets: {
45
+ name: string;
46
+ chinese_name: string;
47
+ "model-setting": {
48
+ type: "confirm_model" | "main_model" | "thinking_model" | "front_output_model" | "backend_output_model" | "genimage_model" | "plan_model" | "modeling_model";
49
+ provider: string;
50
+ model: string | null;
51
+ intensity: string;
52
+ context_window: number;
53
+ }[];
54
+ }[];
55
+ preset: string;
56
+ max_recursion_depth: number;
57
+ } | undefined;
58
+ update: {
59
+ current_version: string;
60
+ latest_version: string;
61
+ has_update: boolean;
62
+ };
63
+ };
64
+ /** Validate the complete patch before writing anything, preserving unrelated settings. */
65
+ export declare function updateWebSettings(value: unknown, mode?: Mode): void;
@@ -0,0 +1,15 @@
1
+ import type { TerminalBridgeInfo } from "../background.js";
2
+ /** A private loopback bridge exposes only the terminal's live hub. */
3
+ export declare function startTerminalBridge(): Promise<TerminalBridgeInfo & {
4
+ close(): void;
5
+ }>;
6
+ /** Mirror each attached terminal's live sessions into the Web service. */
7
+ export declare class TerminalBridges {
8
+ private entries;
9
+ private timer;
10
+ constructor();
11
+ add(owner: number, bridge: TerminalBridgeInfo): void;
12
+ remove(owner: number): void;
13
+ close(): void;
14
+ private poll;
15
+ }
@@ -66,10 +66,16 @@ export async function startTerminalBridge() {
66
66
  else if (action === "message") {
67
67
  if (typeof body.text !== "string" || !body.text.trim())
68
68
  throw new Error("empty message");
69
- hub.send(key, body.text);
69
+ if (body.agent_mode !== undefined && !isAgentMode(body.agent_mode))
70
+ throw new Error("invalid agent mode");
71
+ const sent = hub.sendConfigured(key, body.text, body.agent_mode);
72
+ if (!sent.ok) {
73
+ sendJson(res, 409, { error: sent.error });
74
+ return;
75
+ }
70
76
  }
71
77
  else if (action === "approve") {
72
- if (!hub.approve(key, body.approved === true)) {
78
+ if (!hub.approve(key, body.approved === true, typeof body.approval_id === "string" ? body.approval_id : undefined)) {
73
79
  sendJson(res, 409, { error: "no approval is pending" });
74
80
  return;
75
81
  }
@@ -0,0 +1,6 @@
1
+ export declare function wcwidth(cp: number): number;
2
+ export declare function displayWidth(text: string): number;
3
+ export declare function truncateByWidth(text: string, width: number): string;
4
+ export declare function ellipsize(text: string, width: number): string;
5
+ /** Trim the beginning of `text`, keeping the end (useful for long paths). */
6
+ export declare function ellipsizeStart(text: string, width: number): string;
@@ -0,0 +1,126 @@
1
+ # Neutrino 插件指南(API v1)
2
+
3
+ 插件可以同时提供 Agent 工具、用户命令、菜单、选项卡、输入框、消息与工具卡片、主题、skill 和提示词。安装插件不需要修改 Neutrino 源码。Standard 和 Pro 共用安装包,分别保存启用状态、配置和组件选择。
4
+
5
+ ## 快速开始
6
+
7
+ 从仓库根目录运行:
8
+
9
+ ```sh
10
+ npm run build
11
+ node dist/bin.js plugin install ./examples/plugins/function-tools
12
+ node dist/bin.js plugin enable community.text-tools
13
+ node dist/bin.js plugin doctor
14
+ ```
15
+
16
+ 在终端或浏览器的对话中输入以下命令,可直接得到结果,不请求模型:
17
+
18
+ ```text
19
+ /plugin community.text-tools/count {"text":"hello plugin"}
20
+ ```
21
+
22
+ 正常对话中,模型也可以调用此插件的文本统计和文件统计工具。工具使用 `p_<插件 ID 的散列>_<工具名>` 作为模型协议名;描述中保留完整的 `插件ID/工具名`。
23
+
24
+ 安装主题:
25
+
26
+ ```sh
27
+ neutrino plugin install ./examples/plugins/theme
28
+ neutrino plugin enable community.indigo-theme
29
+ ```
30
+
31
+ 安装自定义输入框后,在管理界面的 **组件提供者 → chat.composer** 中选择它。命令行也支持选择:
32
+
33
+ ```sh
34
+ neutrino plugin install ./examples/plugins/composer
35
+ neutrino plugin enable community.writing-composer
36
+ neutrino plugin provider chat.composer community.writing-composer/editor
37
+ ```
38
+
39
+ 恢复默认输入框:
40
+
41
+ ```sh
42
+ neutrino plugin provider chat.composer default
43
+ ```
44
+
45
+ ## 管理界面
46
+
47
+ 浏览器侧边栏的 **插件 / Plugins** 按钮打开管理界面。可安装本机目录或 `.neutrino-plugin` 包、启用/停用插件、编辑由 Schema 生成的配置表单、更新、回滚、卸载,以及选择输入框、消息、计划、工具卡片和人格提示词提供者。
48
+
49
+ “当前模式”修改当前 Standard/Pro 的默认配置;“当前项目”将启用状态和配置保存到该项目的覆盖层。安装、更新、回滚、卸载作用于共享包。组件提供者选择按 Standard/Pro 保存。
50
+
51
+ 终端起始页有 **Plugins** 入口,在其他页面按 `Ctrl+X` 也可打开。终端插件页支持:
52
+
53
+ | 按键 | 操作 |
54
+ | --- | --- |
55
+ | ↑ / ↓、Enter | 选择插件、查看详情 |
56
+ | E | 启用 / 停用 |
57
+ | I | 安装本机目录或插件包 |
58
+ | C | 编辑完整配置 JSON |
59
+ | U / R / D | 更新 / 回滚 / 卸载 |
60
+ | V | 选择该插件提供的替换组件 |
61
+ | M | 查看扩展页面的文本内容 |
62
+ | Tab | 切换 Standard / Pro |
63
+ | Esc | 返回 |
64
+
65
+ 启用原生插件时,终端要求按 Y 明确信任该版本。Web 使用独立的信任复选框。
66
+
67
+ ## 命令行
68
+
69
+ ```text
70
+ neutrino plugin help
71
+ neutrino plugin list [--profile standard|pro] [--workspace /absolute/project]
72
+ neutrino plugin inspect <id>
73
+ neutrino plugin create <new-directory>
74
+ neutrino plugin install <directory-or-bundle>
75
+ neutrino plugin enable <id> [--trust-native]
76
+ neutrino plugin disable <id>
77
+ neutrino plugin config <id> '<完整配置 JSON>'
78
+ neutrino plugin update <id>
79
+ neutrino plugin rollback <id>
80
+ neutrino plugin remove <id>
81
+ neutrino plugin provider <slot> <plugin-id/contribution-id|default>
82
+ neutrino plugin pack <directory> <output.neutrino-plugin>
83
+ neutrino plugin doctor
84
+ ```
85
+
86
+ 配置命令提交完整插件配置,未提交的字段回到 Schema 默认值。参数包含空格或 JSON 时,请使用所在 shell 的引号规则。更新从上次安装的本机来源重新读取,原文件夹更新或替换原插件包后执行即可。
87
+
88
+ ## 示例插件
89
+
90
+ | 目录 | ID | 验证的扩展方式 |
91
+ | --- | --- | --- |
92
+ | `examples/plugins/function-tools` | `community.text-tools` | 受限脚本工具、命令、只读文件访问、事件与存储 |
93
+ | `examples/plugins/menus-tabs` | `community.workbench` | 菜单、工作区选项卡、侧栏和终端回退 |
94
+ | `examples/plugins/composer` | `community.writing-composer` | 输入框替换、草稿版本检查、发送、工具栏 |
95
+ | `examples/plugins/cards` | `community.inspector-cards` | 工具、计划和消息渲染器 |
96
+ | `examples/plugins/theme` | `community.indigo-theme` | 无脚本 Web / TUI 主题 |
97
+ | `examples/plugins/skills-prompts` | `community.review-skills` | 按需 skill、规则片段和可选人格 |
98
+
99
+ 插件默认全部停用。卸载不删除旧会话,也不自动重放过去的工具调用。旧包文件会保留,以供运行中的回合继续使用;用户数据保存在独立的 `data` 目录。
100
+
101
+ ## 故障恢复
102
+
103
+ ```sh
104
+ neutrino plugin disable <id>
105
+ neutrino plugin doctor
106
+ neutrino --safe-mode --no-web
107
+ ```
108
+
109
+ 安全模式跳过所有外部插件。`--no-web` 可确保不连接到之前已启动、未处于安全模式的后台控制台。如需浏览器安全模式,先停止旧后台服务,再运行 `neutrino --safe-mode web`。
110
+
111
+ 运行中的回合固定包版本和配置,更新通常从下一回合生效。增加权限或更新原生代码时,插件会自动停用,需重新启用。Web 外观与组件配置会自动刷新。插件脚本激活失败时,该插件的注册项撤销,其他插件和内置功能继续运行;脚本执行失败会记录为该次调用的错误。
112
+
113
+ ## 文档目录与支持范围
114
+
115
+ - [架构、作用域与生命周期](architecture.md)
116
+ - [清单与配置格式](manifest.md),[编辑器 JSON Schema](manifest.schema.json)
117
+ - [Agent SDK 与事件契约](agent-sdk.md)
118
+ - [Web / TUI 界面 SDK](ui-sdk.md)
119
+ - [Skill 与提示词](skills-prompts.md)
120
+ - [权限、受限执行与原生信任](security.md)
121
+ - [开发、打包、测试与发布](development.md)
122
+ - [兼容性与迁移](compatibility.md)
123
+
124
+ API v1 支持本机目录和可校验的离线插件包。插件市场、直接拉取 npm/Git 依赖、模型供应商注册、任意替换 Agent 主循环,以及 DSH 插件二进制兼容不属于这一版的公开 API。可通过工具、命令、钩子、提示词和界面接口组合工作流。Web 可用 HTML/CSS/JavaScript 完整替换相应视图;TUI 提供菜单、文本页面、主题和文本渲染契约,浏览器组件可声明仅在 Web 生效。
125
+
126
+ 架构参考:[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/architecture.md)、[可逆注册与 Context](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/cordis-primer.md)。Neutrino 插件使用独立清单和 SDK。