neoctl 0.2.38 → 0.2.40

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 (62) hide show
  1. package/dist/context/context-manager.js +3 -2
  2. package/dist/context/context-manager.js.map +1 -1
  3. package/dist/context/prompt-config.test.js +4 -0
  4. package/dist/context/prompt-config.test.js.map +1 -1
  5. package/dist/context/prompts.d.ts +3 -0
  6. package/dist/context/prompts.js +10 -4
  7. package/dist/context/prompts.js.map +1 -1
  8. package/dist/context/system.md +1 -1
  9. package/dist/core/query-engine.d.ts +5 -2
  10. package/dist/core/query-engine.js +31 -13
  11. package/dist/core/query-engine.js.map +1 -1
  12. package/dist/core/query.js +2 -2
  13. package/dist/core/query.js.map +1 -1
  14. package/dist/core/session-prompt-inheritance.test.d.ts +1 -0
  15. package/dist/core/session-prompt-inheritance.test.js +132 -0
  16. package/dist/core/session-prompt-inheritance.test.js.map +1 -0
  17. package/dist/core/session-settings-prompt.d.ts +12 -4
  18. package/dist/core/session-settings-prompt.js +50 -27
  19. package/dist/core/session-settings-prompt.js.map +1 -1
  20. package/dist/core/session-settings-prompt.test.js +31 -22
  21. package/dist/core/session-settings-prompt.test.js.map +1 -1
  22. package/dist/execution/docker.d.ts +9 -0
  23. package/dist/execution/docker.js +75 -0
  24. package/dist/execution/docker.js.map +1 -0
  25. package/dist/execution/docker.test.d.ts +1 -0
  26. package/dist/execution/docker.test.js +57 -0
  27. package/dist/execution/docker.test.js.map +1 -0
  28. package/dist/execution/filesystem.d.ts +7 -0
  29. package/dist/execution/filesystem.js +76 -0
  30. package/dist/execution/filesystem.js.map +1 -0
  31. package/dist/execution/process.d.ts +8 -0
  32. package/dist/execution/process.js +62 -0
  33. package/dist/execution/process.js.map +1 -0
  34. package/dist/plugins/plugin-system.js +3 -0
  35. package/dist/plugins/plugin-system.js.map +1 -1
  36. package/dist/session/session-export.d.ts +4 -0
  37. package/dist/session/session-export.js.map +1 -1
  38. package/dist/session/session-store.js +4 -6
  39. package/dist/session/session-store.js.map +1 -1
  40. package/dist/session/tool-result-memory.js +9 -1
  41. package/dist/session/tool-result-memory.js.map +1 -1
  42. package/dist/tools/builtins/edit-tool.js +1 -1
  43. package/dist/tools/builtins/edit-tool.js.map +1 -1
  44. package/dist/tools/builtins/exec-process-manager.js +17 -11
  45. package/dist/tools/builtins/exec-process-manager.js.map +1 -1
  46. package/dist/tools/builtins/exec-tool.js +6 -3
  47. package/dist/tools/builtins/exec-tool.js.map +1 -1
  48. package/dist/tools/builtins/filesystem-tools.js +1 -1
  49. package/dist/tools/builtins/filesystem-tools.js.map +1 -1
  50. package/dist/tools/builtins/grep-tool.js +11 -4
  51. package/dist/tools/builtins/grep-tool.js.map +1 -1
  52. package/dist/tools/builtins/image-generation-tool.js +19 -6
  53. package/dist/tools/builtins/image-generation-tool.js.map +1 -1
  54. package/dist/tools/builtins/image-loader-tool.js +5 -3
  55. package/dist/tools/builtins/image-loader-tool.js.map +1 -1
  56. package/dist/web/index.d.ts +25 -8
  57. package/dist/web/index.js +126 -46
  58. package/dist/web/index.js.map +1 -1
  59. package/dist/web/session-prompt-integration.test.js +1 -1
  60. package/dist/web/session-prompt-integration.test.js.map +1 -1
  61. package/docs/prompt-config.md +19 -4
  62. package/package.json +1 -1
package/dist/web/index.js CHANGED
@@ -5,6 +5,7 @@ import http from "node:http";
5
5
  import fs from "node:fs/promises";
6
6
  import path from "node:path";
7
7
  import { existsSync, readFileSync } from "node:fs";
8
+ import { dockerEnabled } from "../execution/docker.js";
8
9
  import { fileURLToPath } from "node:url";
9
10
  import { createRequire } from "node:module";
10
11
  import { readSessionPrompt, updateSessionPrompt, SessionPromptError } from "./session-prompt-protocol.js";
@@ -104,7 +105,7 @@ const DEFAULT_WEB_RUNTIME_KEY = "__default__";
104
105
  export async function runWebServer(argv = process.argv.slice(2), runtimeOptions = {}) {
105
106
  const options = parseWebArgs(argv);
106
107
  const router = await createWebRuntimeRouter(runtimeOptions);
107
- const server = http.createServer((req, res) => void route(req, res, router));
108
+ const server = http.createServer((req, res) => void handleWebRequest(req, res, router));
108
109
  await new Promise((resolve) => server.listen(options.port, options.host, resolve));
109
110
  const address = server.address();
110
111
  const actualPort = typeof address === "object" && address ? address.port : options.port;
@@ -194,6 +195,14 @@ export async function createWebRuntime(options = {}) {
194
195
  modelGateway,
195
196
  tools,
196
197
  contextManagerFactory: (cwd) => new DefaultContextManager({ cwd }),
198
+ refreshTools: () => {
199
+ if (!options.resolveGlobalToolOverrides)
200
+ return;
201
+ if (runtime)
202
+ refreshGlobalToolOverrides(runtime);
203
+ else
204
+ applyToolOverrides(tools, toolCatalog, normalizeToolOverrides(options.resolveGlobalToolOverrides(), toolCatalog), sessionToolOverrides);
205
+ },
197
206
  additionalPromptSections: [
198
207
  ...(options.externalPromptSections ?? []),
199
208
  ...activePlugins.flatMap((plugin) => plugin.promptSections ?? []),
@@ -207,7 +216,7 @@ export async function createWebRuntime(options = {}) {
207
216
  session: {
208
217
  enabled: process.env.AGENT_SESSION_TRANSCRIPT !== "0",
209
218
  sessionId: options.sessionId ?? process.env.AGENT_SESSION_ID,
210
- rootDir: process.env.AGENT_SESSION_DIR,
219
+ rootDir: options.sessionRootDir ?? process.env.AGENT_SESSION_DIR,
211
220
  resume: options.resume ?? parseResumeFlag(process.env.AGENT_SESSION_RESUME),
212
221
  toolResultThresholdChars: process.env.AGENT_TOOL_RESULT_THRESHOLD_CHARS ? Number(process.env.AGENT_TOOL_RESULT_THRESHOLD_CHARS) : undefined,
213
222
  },
@@ -242,6 +251,7 @@ export async function createWebRuntime(options = {}) {
242
251
  globalOverrides: globalToolOverrides,
243
252
  sessionOverrides: sessionToolOverrides,
244
253
  persistGlobal: options.persistGlobalToolOverrides,
254
+ resolveGlobal: options.resolveGlobalToolOverrides,
245
255
  persistSession: options.persistSessionToolOverrides,
246
256
  resolveSession: options.resolveSessionToolOverrides,
247
257
  },
@@ -279,6 +289,14 @@ function applyToolOverrides(registry, catalog, globalOverrides, sessionOverrides
279
289
  for (const tool of catalog)
280
290
  registry.setEnabled(tool.name, sessionOverrides[tool.name] ?? globalOverrides[tool.name] ?? true);
281
291
  }
292
+ /** Called only at a safe turn boundary or while idle, never during tool execution. */
293
+ export function refreshGlobalToolOverrides(runtime) {
294
+ const support = runtime.toolSupport;
295
+ if (!support?.resolveGlobal)
296
+ return;
297
+ support.globalOverrides = normalizeToolOverrides(support.resolveGlobal(), support.catalog);
298
+ applyToolOverrides(runtime.tools, support.catalog, support.globalOverrides, support.sessionOverrides);
299
+ }
282
300
  /** Bind only the foreground view; never stop work owned by another session. */
283
301
  export function activateSession(runtime) {
284
302
  const sessionDir = runtime.engine.snapshot().session?.sessionDir || undefined;
@@ -367,6 +385,14 @@ export class WebRuntimeRouter {
367
385
  async snapshot(scope = {}, includeCatalog = true) {
368
386
  return (await this.get(scope)).snapshot(includeCatalog);
369
387
  }
388
+ async reloadModelConfig() {
389
+ const repls = await Promise.all([...this.repls.values()]);
390
+ await Promise.all(repls.map(repl => repl.reloadModelConfig()));
391
+ }
392
+ async reloadGlobalTools(overrides) {
393
+ const repls = await Promise.all([...this.repls.values()]);
394
+ await Promise.all(repls.map(repl => repl.reloadGlobalTools(overrides)));
395
+ }
370
396
  activeScopes() {
371
397
  return [...this.repls.keys()];
372
398
  }
@@ -427,6 +453,8 @@ export class WebRepl {
427
453
  queuedAttachments;
428
454
  foregroundRun;
429
455
  foregroundRunToken = 0;
456
+ stoppingRun;
457
+ immediateSend;
430
458
  pendingUserImageEchoIds;
431
459
  pendingUserImageEchoMessageId;
432
460
  runtimeContextRevision = 0;
@@ -561,6 +589,7 @@ export class WebRepl {
561
589
  };
562
590
  }
563
591
  globalTools() {
592
+ this.runtime.engine.refreshToolsIfIdle();
564
593
  const support = this.runtime.toolSupport;
565
594
  if (!support)
566
595
  return { items: [] };
@@ -572,6 +601,7 @@ export class WebRepl {
572
601
  };
573
602
  }
574
603
  sessionTools() {
604
+ this.runtime.engine.refreshToolsIfIdle();
575
605
  const support = this.runtime.toolSupport;
576
606
  const sessionId = this.runtime.engine.snapshot().session?.sessionId;
577
607
  if (!support)
@@ -608,18 +638,25 @@ export class WebRepl {
608
638
  if (!normalized.ok)
609
639
  return normalized;
610
640
  try {
611
- support.globalOverrides = normalized.value;
612
- this.syncPluginToolRegistration();
613
- applyToolOverrides(this.runtime.tools, support.catalog, support.globalOverrides, support.sessionOverrides);
641
+ // Persist before publishing so a failed write never changes active capabilities.
614
642
  if (support.persistGlobal)
615
- await support.persistGlobal(support.globalOverrides);
616
- await this.refreshToolConfiguration();
643
+ await support.persistGlobal(normalized.value);
644
+ await this.reloadGlobalTools(normalized.value);
617
645
  return { ok: true, state: this.globalTools() };
618
646
  }
619
647
  catch (error) {
620
648
  return actionFailure("TOOL_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
621
649
  }
622
650
  }
651
+ async reloadGlobalTools(overrides) {
652
+ const support = this.runtime.toolSupport;
653
+ if (!support)
654
+ return;
655
+ support.globalOverrides = normalizeToolOverrides(overrides, support.catalog);
656
+ this.syncPluginToolRegistration();
657
+ applyToolOverrides(this.runtime.tools, support.catalog, support.globalOverrides, support.sessionOverrides);
658
+ await this.refreshToolConfiguration();
659
+ }
623
660
  async setSessionTools(value) {
624
661
  if (this.busy)
625
662
  return actionFailure("TOOL_UPDATE_BLOCKED", "cannot change session tools while a response is running");
@@ -710,12 +747,14 @@ export class WebRepl {
710
747
  const support = this.runtime.toolSupport;
711
748
  if (!support)
712
749
  return;
713
- support.sessionOverrides = normalizeToolOverrides(overrides, support.catalog);
714
- this.syncPluginToolRegistration();
715
- applyToolOverrides(this.runtime.tools, support.catalog, support.globalOverrides, support.sessionOverrides);
750
+ const normalized = normalizeToolOverrides(overrides, support.catalog);
716
751
  const sessionId = this.runtime.engine.snapshot().session?.sessionId;
717
752
  if (persist && sessionId && support.persistSession)
718
- await support.persistSession(sessionId, support.sessionOverrides);
753
+ await support.persistSession(sessionId, normalized);
754
+ support.sessionOverrides = normalized;
755
+ this.runtime.engine.refreshToolsIfIdle();
756
+ this.syncPluginToolRegistration();
757
+ applyToolOverrides(this.runtime.tools, support.catalog, support.globalOverrides, support.sessionOverrides);
719
758
  await this.refreshToolConfiguration();
720
759
  }
721
760
  syncPluginToolRegistration() {
@@ -764,6 +803,8 @@ export class WebRepl {
764
803
  this.publishRuntimeContext();
765
804
  }
766
805
  async submit(text, attachments = []) {
806
+ if (this.stoppingRun || this.immediateSend)
807
+ return { ok: false, error: "正在停止上一轮请求,请稍后发送" };
767
808
  const trimmed = text.trim();
768
809
  if (!trimmed && attachments.length === 0)
769
810
  return { ok: true };
@@ -783,7 +824,11 @@ export class WebRepl {
783
824
  this.startRun(text, attachments);
784
825
  return { ok: true };
785
826
  }
786
- submitImmediately(text, attachments = []) {
827
+ async submitImmediately(text, attachments = []) {
828
+ if (this.immediateSend)
829
+ return { ok: false, error: "正在发送,请勿重复提交" };
830
+ if (this.stoppingRun)
831
+ return { ok: false, error: "正在停止上一轮请求,请稍后发送" };
787
832
  if (!this.busy) {
788
833
  this.startRun(text, attachments);
789
834
  return { ok: true, interrupted: false };
@@ -792,15 +837,21 @@ export class WebRepl {
792
837
  return this.sendQueuedNow();
793
838
  }
794
839
  sendQueuedNow() {
840
+ if (this.immediateSend)
841
+ return this.immediateSend;
795
842
  const queuedText = this.queuedInput;
796
843
  const queuedAttachments = this.queuedAttachments ?? [];
797
844
  if (queuedText === undefined)
798
- return { ok: true, interrupted: false };
799
- this.queuedInput = undefined;
800
- this.queuedAttachments = undefined;
801
- const interrupted = this.stopForegroundRun("Interrupted to send queued web input");
802
- this.startRun(queuedText, queuedAttachments);
803
- return { ok: true, interrupted };
845
+ return Promise.resolve({ ok: true, interrupted: false });
846
+ const pending = (async () => {
847
+ const interrupted = await this.stopForegroundRun("Interrupted to send queued web input", true);
848
+ this.startRun(queuedText, queuedAttachments);
849
+ return { ok: true, interrupted };
850
+ })();
851
+ this.immediateSend = pending;
852
+ void pending.finally(() => { if (this.immediateSend === pending)
853
+ this.immediateSend = undefined; }).catch(() => undefined);
854
+ return pending;
804
855
  }
805
856
  enqueueInput(text, attachments) {
806
857
  this.queuedInput = mergeWebQueuedInput(this.queuedInput, text);
@@ -916,12 +967,13 @@ export class WebRepl {
916
967
  source: payload.source,
917
968
  });
918
969
  const usage = payload.usage?.trim();
919
- if (!shouldClear && usage) {
970
+ if (!shouldClear) {
920
971
  this.append({
921
972
  kind: "meta",
922
973
  title: "提示词用法",
923
- text: usage,
974
+ text: usage || "提示词已应用。请在下方输入框中描述你的需求,按提示词要求补充材料后发送;它会用于后续对话。可在提示词管理中切换或清除。",
924
975
  format: "markdown",
976
+ presentationLevel: "primary",
925
977
  });
926
978
  }
927
979
  this.broadcastSync();
@@ -1066,6 +1118,9 @@ export class WebRepl {
1066
1118
  const provider = parseLoginProvider(providerValue);
1067
1119
  if (!provider)
1068
1120
  return actionFailure("LOGIN_INVALID", "provider must be openai");
1121
+ if (dockerEnabled() && !values.apiKey?.trim()) {
1122
+ values = { ...values, apiKey: parseEnvFileSafe(this.runtime.envPath).OPENAI_API_KEY || process.env.OPENAI_API_KEY || "" };
1123
+ }
1069
1124
  const payload = { ...createLoginFormPayload(this.runtime.envPath, provider), provider, values };
1070
1125
  const validationError = validateLoginFormPayload(payload);
1071
1126
  if (validationError)
@@ -1073,18 +1128,7 @@ export class WebRepl {
1073
1128
  try {
1074
1129
  await saveLoginPayloadToEnv(payload);
1075
1130
  applyLoginPayloadToProcessEnv(payload);
1076
- const config = readModelProviderConfig(process.env);
1077
- if (!config)
1078
- throw new Error("Saved provider config could not be loaded from environment.");
1079
- const innerGateway = createModelGatewayFromConfig(config);
1080
- this.runtime.modelGateway.setInner(innerGateway);
1081
- this.runtime.agentRuntime.modelGateway = this.runtime.modelGateway;
1082
- this.runtime.engine.setModelProvider({ modelGateway: this.runtime.modelGateway, model: config.model, reasoning: config.defaultReasoning });
1083
- syncImageGenerationTool(this.runtime, config.provider);
1084
- this.runtime.defaultReasoning = config.defaultReasoning;
1085
- const metrics = await this.runtime.engine.contextMetrics();
1086
- this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
1087
- this.publishRuntimeContext();
1131
+ await this.reloadModelConfig();
1088
1132
  return { ok: true };
1089
1133
  }
1090
1134
  catch (error) {
@@ -1092,8 +1136,27 @@ export class WebRepl {
1092
1136
  return actionFailure("LOGIN_SAVE_FAILED", message);
1093
1137
  }
1094
1138
  }
1095
- interrupt() {
1096
- const interrupted = this.stopForegroundRun("Interrupted from neo web");
1139
+ async reloadModelConfig() {
1140
+ const config = readModelProviderConfig(process.env);
1141
+ if (!config)
1142
+ throw new Error("Saved provider config could not be loaded from environment.");
1143
+ const innerGateway = createModelGatewayFromConfig(config);
1144
+ this.runtime.modelGateway.setInner(innerGateway);
1145
+ this.runtime.agentRuntime.modelGateway = this.runtime.modelGateway;
1146
+ this.runtime.engine.setModelProvider({ modelGateway: this.runtime.modelGateway, model: config.model, reasoning: config.defaultReasoning });
1147
+ for (const run of this.backgroundSessionRuns.values()) {
1148
+ run.engine.setModelProvider({ modelGateway: this.runtime.modelGateway, model: config.model, reasoning: config.defaultReasoning });
1149
+ }
1150
+ syncImageGenerationTool(this.runtime, config.provider);
1151
+ this.runtime.defaultReasoning = config.defaultReasoning;
1152
+ const metrics = await this.runtime.engine.contextMetrics();
1153
+ this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
1154
+ this.publishRuntimeContext();
1155
+ }
1156
+ async interrupt() {
1157
+ if (this.immediateSend)
1158
+ await this.immediateSend;
1159
+ const interrupted = await this.stopForegroundRun("Interrupted from neo web");
1097
1160
  return { ok: true, interrupted };
1098
1161
  }
1099
1162
  cancelQueue() {
@@ -1146,15 +1209,15 @@ export class WebRepl {
1146
1209
  this.assistantLineId = undefined;
1147
1210
  this.finalizedThinkingLineId = undefined;
1148
1211
  }
1149
- stopForegroundRun(reason) {
1212
+ stopForegroundRun(reason, keepBusy = false) {
1213
+ if (this.stoppingRun)
1214
+ return this.stoppingRun;
1150
1215
  const controller = this.activeAbortController;
1216
+ const run = this.foregroundRun;
1151
1217
  const runWasActive = this.busy || Boolean(controller && !controller.signal.aborted);
1152
1218
  this.foregroundRunToken += 1;
1153
- this.foregroundRun = undefined;
1154
- this.runtime.usage.reset();
1155
1219
  if (controller && !controller.signal.aborted)
1156
1220
  controller.abort(reason);
1157
- this.activeAbortController = undefined;
1158
1221
  this.interruptArmed = false;
1159
1222
  this.queuedInput = undefined;
1160
1223
  this.queuedAttachments = undefined;
@@ -1162,10 +1225,26 @@ export class WebRepl {
1162
1225
  this.pendingUserImageEchoMessageId = undefined;
1163
1226
  this.finalizeForegroundView();
1164
1227
  this.cancelLiveToolLines(reason);
1165
- this.busy = false;
1166
- this.status = { ...this.status, phase: "ready", detail: undefined, currentTool: undefined, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined };
1228
+ this.busy = true;
1229
+ this.status = { ...this.status, phase: "running", detail: "stopping", currentTool: undefined, retryCooldownUntil: undefined };
1167
1230
  this.broadcastSync();
1168
- return runWasActive;
1231
+ const pending = (async () => {
1232
+ // Abort is cooperative: the engine still owns its run until the iterator settles.
1233
+ await run;
1234
+ if (this.foregroundRun === run)
1235
+ this.foregroundRun = undefined;
1236
+ if (this.activeAbortController === controller)
1237
+ this.activeAbortController = undefined;
1238
+ this.runtime.usage.reset();
1239
+ this.busy = keepBusy;
1240
+ this.status = { ...this.status, phase: keepBusy ? "running" : "ready", detail: undefined, currentTool: undefined, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined };
1241
+ this.broadcastSync();
1242
+ return runWasActive;
1243
+ })();
1244
+ this.stoppingRun = pending;
1245
+ void pending.finally(() => { if (this.stoppingRun === pending)
1246
+ this.stoppingRun = undefined; }).catch(() => undefined);
1247
+ return pending;
1169
1248
  }
1170
1249
  agentTaskSnapshot(task) {
1171
1250
  const terminal = this.runtime.taskStore.isTerminal(task);
@@ -1993,7 +2072,8 @@ function reqKeepAlive(res) {
1993
2072
  timer.unref?.();
1994
2073
  res.on("close", () => clearInterval(timer));
1995
2074
  }
1996
- async function route(req, res, router) {
2075
+ /** Embed runtime HTTP routes behind an application's own authentication/authorization layer. */
2076
+ export async function handleWebRequest(req, res, router) {
1997
2077
  const url = new URL(req.url ?? "/", "http://localhost");
1998
2078
  try {
1999
2079
  if (req.method === "GET" && url.pathname === "/")
@@ -2146,14 +2226,14 @@ async function route(req, res, router) {
2146
2226
  }
2147
2227
  if (req.method === "POST" && url.pathname === "/api/submit-now") {
2148
2228
  const body = await readJsonBody(req);
2149
- return sendJson(res, repl.submitImmediately(String(body.text ?? ""), sanitizeWebAttachments(body.attachments)));
2229
+ return sendJson(res, await repl.submitImmediately(String(body.text ?? ""), sanitizeWebAttachments(body.attachments)));
2150
2230
  }
2151
2231
  if (req.method === "POST" && url.pathname === "/api/interrupt")
2152
- return sendJson(res, repl.interrupt());
2232
+ return sendJson(res, await repl.interrupt());
2153
2233
  if (req.method === "POST" && url.pathname === "/api/queue/cancel")
2154
2234
  return sendJson(res, repl.cancelQueue());
2155
2235
  if (req.method === "POST" && url.pathname === "/api/queue/send-now")
2156
- return sendJson(res, repl.sendQueuedNow());
2236
+ return sendJson(res, await repl.sendQueuedNow());
2157
2237
  if (req.method === "GET" && url.pathname === "/api/sessions")
2158
2238
  return sendJson(res, await repl.listSessions());
2159
2239
  if (req.method === "POST" && url.pathname === "/api/sessions/resume") {
@@ -2631,7 +2711,7 @@ function createLoginFormPayload(envPath, provider) {
2631
2711
  function loginValuesForProvider(provider, env) {
2632
2712
  const values = {};
2633
2713
  for (const field of LOGIN_FIELD_DEFINITIONS[provider])
2634
- values[field.key] = env[field.envKey] ?? "";
2714
+ values[field.key] = dockerEnabled() && field.secret ? "" : env[field.envKey] ?? "";
2635
2715
  if (!values.baseUrl)
2636
2716
  values.baseUrl = defaultBaseUrlForLoginProvider(provider);
2637
2717
  if (!values.model)