neoctl 0.2.37 → 0.2.39

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 (98) hide show
  1. package/dist/agents/agent-definition.d.ts +2 -0
  2. package/dist/agents/agent-definition.js +10 -0
  3. package/dist/agents/agent-definition.js.map +1 -1
  4. package/dist/agents/agent-tool.js +4 -0
  5. package/dist/agents/agent-tool.js.map +1 -1
  6. package/dist/agents/coordination-boundary.test.d.ts +1 -0
  7. package/dist/agents/coordination-boundary.test.js +58 -0
  8. package/dist/agents/coordination-boundary.test.js.map +1 -0
  9. package/dist/context/context-manager.d.ts +4 -0
  10. package/dist/context/context-manager.js +6 -2
  11. package/dist/context/context-manager.js.map +1 -1
  12. package/dist/context/prompt-config.d.ts +35 -0
  13. package/dist/context/prompt-config.js +151 -0
  14. package/dist/context/prompt-config.js.map +1 -0
  15. package/dist/context/prompt-config.test.d.ts +1 -0
  16. package/dist/context/prompt-config.test.js +128 -0
  17. package/dist/context/prompt-config.test.js.map +1 -0
  18. package/dist/context/prompts.d.ts +4 -1
  19. package/dist/context/prompts.js +13 -30
  20. package/dist/context/prompts.js.map +1 -1
  21. package/dist/context/smoke-context.js +3 -1
  22. package/dist/context/smoke-context.js.map +1 -1
  23. package/dist/context/system.md +16 -0
  24. package/dist/core/query-engine.d.ts +30 -1
  25. package/dist/core/query-engine.js +321 -111
  26. package/dist/core/query-engine.js.map +1 -1
  27. package/dist/core/query-session-settings.test.d.ts +1 -0
  28. package/dist/core/query-session-settings.test.js +252 -0
  29. package/dist/core/query-session-settings.test.js.map +1 -0
  30. package/dist/core/query.d.ts +16 -0
  31. package/dist/core/query.js +32 -2
  32. package/dist/core/query.js.map +1 -1
  33. package/dist/core/run-agent.js +3 -5
  34. package/dist/core/run-agent.js.map +1 -1
  35. package/dist/core/session-prompt-inheritance.test.d.ts +1 -0
  36. package/dist/core/session-prompt-inheritance.test.js +132 -0
  37. package/dist/core/session-prompt-inheritance.test.js.map +1 -0
  38. package/dist/core/session-settings-prompt.d.ts +43 -0
  39. package/dist/core/session-settings-prompt.js +84 -0
  40. package/dist/core/session-settings-prompt.js.map +1 -0
  41. package/dist/core/session-settings-prompt.test.d.ts +1 -0
  42. package/dist/core/session-settings-prompt.test.js +266 -0
  43. package/dist/core/session-settings-prompt.test.js.map +1 -0
  44. package/dist/index.d.ts +3 -0
  45. package/dist/index.js +2 -0
  46. package/dist/index.js.map +1 -1
  47. package/dist/model/model-metadata.json +1 -3
  48. package/dist/model/openai-responses-instructions.test.d.ts +1 -0
  49. package/dist/model/openai-responses-instructions.test.js +64 -0
  50. package/dist/model/openai-responses-instructions.test.js.map +1 -0
  51. package/dist/model/openai-responses-mapper.js +5 -16
  52. package/dist/model/openai-responses-mapper.js.map +1 -1
  53. package/dist/model/smoke-responses-mapper.js +8 -5
  54. package/dist/model/smoke-responses-mapper.js.map +1 -1
  55. package/dist/plugins/plugin-system.js +3 -0
  56. package/dist/plugins/plugin-system.js.map +1 -1
  57. package/dist/session/session-export.d.ts +10 -0
  58. package/dist/session/session-export.js.map +1 -1
  59. package/dist/session/session-store.d.ts +11 -0
  60. package/dist/session/session-store.js +23 -0
  61. package/dist/session/session-store.js.map +1 -1
  62. package/dist/tools/builtins/exec-process-manager.js +4 -1
  63. package/dist/tools/builtins/exec-process-manager.js.map +1 -1
  64. package/dist/tools/terminal-background-history.test.d.ts +1 -0
  65. package/dist/tools/terminal-background-history.test.js +77 -0
  66. package/dist/tools/terminal-background-history.test.js.map +1 -0
  67. package/dist/tools/terminal-output-store.d.ts +4 -0
  68. package/dist/tools/terminal-output-store.js +18 -0
  69. package/dist/tools/terminal-output-store.js.map +1 -1
  70. package/dist/web/index.d.ts +26 -1
  71. package/dist/web/index.js +150 -18
  72. package/dist/web/index.js.map +1 -1
  73. package/dist/web/prompt-config-protocol.d.ts +4 -0
  74. package/dist/web/prompt-config-protocol.js +57 -0
  75. package/dist/web/prompt-config-protocol.js.map +1 -0
  76. package/dist/web/prompt-config-protocol.test.d.ts +1 -0
  77. package/dist/web/prompt-config-protocol.test.js +78 -0
  78. package/dist/web/prompt-config-protocol.test.js.map +1 -0
  79. package/dist/web/runtime-context-protocol.d.ts +1 -0
  80. package/dist/web/runtime-context-protocol.js +1 -0
  81. package/dist/web/runtime-context-protocol.js.map +1 -1
  82. package/dist/web/session-prompt-integration.test.d.ts +1 -0
  83. package/dist/web/session-prompt-integration.test.js +57 -0
  84. package/dist/web/session-prompt-integration.test.js.map +1 -0
  85. package/dist/web/session-prompt-protocol.d.ts +8 -0
  86. package/dist/web/session-prompt-protocol.js +11 -0
  87. package/dist/web/session-prompt-protocol.js.map +1 -0
  88. package/dist/web/session-settings.test.d.ts +1 -0
  89. package/dist/web/session-settings.test.js +254 -0
  90. package/dist/web/session-settings.test.js.map +1 -0
  91. package/dist/web/smoke-terminal-output-http.js +10 -5
  92. package/dist/web/smoke-terminal-output-http.js.map +1 -1
  93. package/dist/web/smoke-web-terminal-tasks.js +100 -3
  94. package/dist/web/smoke-web-terminal-tasks.js.map +1 -1
  95. package/docs/prompt-config.md +86 -0
  96. package/package.json +4 -3
  97. package/scripts/build-standalone.mjs +3 -0
  98. package/scripts/copy-prompt-resources.mjs +5 -0
package/dist/web/index.js CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { subagentHeader, subagentStatusFacts } from "./status-semantics.js";
3
+ import { handlePromptConfigRequest } from "./prompt-config-protocol.js";
3
4
  import http from "node:http";
4
5
  import fs from "node:fs/promises";
5
6
  import path from "node:path";
6
7
  import { existsSync, readFileSync } from "node:fs";
7
8
  import { fileURLToPath } from "node:url";
8
9
  import { createRequire } from "node:module";
10
+ import { readSessionPrompt, updateSessionPrompt, SessionPromptError } from "./session-prompt-protocol.js";
9
11
  import { QueryEngine } from "../core/query-engine.js";
10
12
  import { InMemoryAppPromptStore } from "../app/app-prompt.js";
11
13
  import { InMemoryAppState } from "../app/app-state.js";
@@ -192,6 +194,14 @@ export async function createWebRuntime(options = {}) {
192
194
  modelGateway,
193
195
  tools,
194
196
  contextManagerFactory: (cwd) => new DefaultContextManager({ cwd }),
197
+ refreshTools: () => {
198
+ if (!options.resolveGlobalToolOverrides)
199
+ return;
200
+ if (runtime)
201
+ refreshGlobalToolOverrides(runtime);
202
+ else
203
+ applyToolOverrides(tools, toolCatalog, normalizeToolOverrides(options.resolveGlobalToolOverrides(), toolCatalog), sessionToolOverrides);
204
+ },
195
205
  additionalPromptSections: [
196
206
  ...(options.externalPromptSections ?? []),
197
207
  ...activePlugins.flatMap((plugin) => plugin.promptSections ?? []),
@@ -240,6 +250,7 @@ export async function createWebRuntime(options = {}) {
240
250
  globalOverrides: globalToolOverrides,
241
251
  sessionOverrides: sessionToolOverrides,
242
252
  persistGlobal: options.persistGlobalToolOverrides,
253
+ resolveGlobal: options.resolveGlobalToolOverrides,
243
254
  persistSession: options.persistSessionToolOverrides,
244
255
  resolveSession: options.resolveSessionToolOverrides,
245
256
  },
@@ -277,6 +288,14 @@ function applyToolOverrides(registry, catalog, globalOverrides, sessionOverrides
277
288
  for (const tool of catalog)
278
289
  registry.setEnabled(tool.name, sessionOverrides[tool.name] ?? globalOverrides[tool.name] ?? true);
279
290
  }
291
+ /** Called only at a safe turn boundary or while idle, never during tool execution. */
292
+ export function refreshGlobalToolOverrides(runtime) {
293
+ const support = runtime.toolSupport;
294
+ if (!support?.resolveGlobal)
295
+ return;
296
+ support.globalOverrides = normalizeToolOverrides(support.resolveGlobal(), support.catalog);
297
+ applyToolOverrides(runtime.tools, support.catalog, support.globalOverrides, support.sessionOverrides);
298
+ }
280
299
  /** Bind only the foreground view; never stop work owned by another session. */
281
300
  export function activateSession(runtime) {
282
301
  const sessionDir = runtime.engine.snapshot().session?.sessionDir || undefined;
@@ -506,6 +525,12 @@ export class WebRepl {
506
525
  snapshot(includeCatalog = false) {
507
526
  const backgroundTasks = this.backgroundTasks();
508
527
  const engineSnapshot = this.runtime.engine.snapshot();
528
+ const pendingSessionSettings = this.runtime.engine.getPendingSessionSettings?.() ?? {};
529
+ // Include retained reasoning alongside a queued model. Explicit undefined is
530
+ // the default reset and is intentionally omitted by JSON serialization.
531
+ if (Object.hasOwn(pendingSessionSettings, "model") && !Object.hasOwn(pendingSessionSettings, "reasoning")) {
532
+ pendingSessionSettings.reasoning = engineSnapshot.reasoning;
533
+ }
509
534
  return {
510
535
  lines: this.lines.map(line => line.toolError ? { ...line, toolError: this.runtime.engine.redactDisplayValue?.(line.toolError) ?? line.toolError } : line),
511
536
  status: this.status,
@@ -519,6 +544,7 @@ export class WebRepl {
519
544
  runningSessionIds: [...this.backgroundSessionRuns.keys()],
520
545
  session: engineSnapshot.session,
521
546
  modelSettings: { model: engineSnapshot.model, reasoning: engineSnapshot.reasoning },
547
+ pendingSessionSettings,
522
548
  fastMode: this.runtime.engine.isFastMode(),
523
549
  appPrompt: this.runtime.engine.getAppPrompt(),
524
550
  catalog: includeCatalog ? webCatalog(this.runtime) : undefined,
@@ -552,6 +578,7 @@ export class WebRepl {
552
578
  };
553
579
  }
554
580
  globalTools() {
581
+ this.runtime.engine.refreshToolsIfIdle();
555
582
  const support = this.runtime.toolSupport;
556
583
  if (!support)
557
584
  return { items: [] };
@@ -563,6 +590,7 @@ export class WebRepl {
563
590
  };
564
591
  }
565
592
  sessionTools() {
593
+ this.runtime.engine.refreshToolsIfIdle();
566
594
  const support = this.runtime.toolSupport;
567
595
  const sessionId = this.runtime.engine.snapshot().session?.sessionId;
568
596
  if (!support)
@@ -599,11 +627,12 @@ export class WebRepl {
599
627
  if (!normalized.ok)
600
628
  return normalized;
601
629
  try {
630
+ // Persist before publishing so a failed write never changes active capabilities.
631
+ if (support.persistGlobal)
632
+ await support.persistGlobal(normalized.value);
602
633
  support.globalOverrides = normalized.value;
603
634
  this.syncPluginToolRegistration();
604
635
  applyToolOverrides(this.runtime.tools, support.catalog, support.globalOverrides, support.sessionOverrides);
605
- if (support.persistGlobal)
606
- await support.persistGlobal(support.globalOverrides);
607
636
  await this.refreshToolConfiguration();
608
637
  return { ok: true, state: this.globalTools() };
609
638
  }
@@ -701,12 +730,14 @@ export class WebRepl {
701
730
  const support = this.runtime.toolSupport;
702
731
  if (!support)
703
732
  return;
704
- support.sessionOverrides = normalizeToolOverrides(overrides, support.catalog);
705
- this.syncPluginToolRegistration();
706
- applyToolOverrides(this.runtime.tools, support.catalog, support.globalOverrides, support.sessionOverrides);
733
+ const normalized = normalizeToolOverrides(overrides, support.catalog);
707
734
  const sessionId = this.runtime.engine.snapshot().session?.sessionId;
708
735
  if (persist && sessionId && support.persistSession)
709
- await support.persistSession(sessionId, support.sessionOverrides);
736
+ await support.persistSession(sessionId, normalized);
737
+ support.sessionOverrides = normalized;
738
+ this.runtime.engine.refreshToolsIfIdle();
739
+ this.syncPluginToolRegistration();
740
+ applyToolOverrides(this.runtime.tools, support.catalog, support.globalOverrides, support.sessionOverrides);
710
741
  await this.refreshToolConfiguration();
711
742
  }
712
743
  syncPluginToolRegistration() {
@@ -950,35 +981,105 @@ export class WebRepl {
950
981
  }
951
982
  return undefined;
952
983
  }
984
+ async refreshSessionSettingMetrics(engine) {
985
+ const metrics = await engine.contextMetrics();
986
+ // A request can finish after its owner session was detached/switched.
987
+ if (this.runtime.engine !== engine)
988
+ return;
989
+ this.runtime.initialMetrics = metrics;
990
+ this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
991
+ }
953
992
  async setFastMode(enabled) {
993
+ const engine = this.runtime.engine;
954
994
  try {
955
- const fastMode = await this.runtime.engine.setFastMode(enabled);
956
- this.broadcastSync();
957
- return { ok: true, fastMode };
995
+ const { deferred } = await engine.updateSessionSettings({ fastMode: enabled });
996
+ if (this.runtime.engine === engine)
997
+ this.broadcastSync();
998
+ return { ok: true, fastMode: enabled, deferred };
958
999
  }
959
1000
  catch (error) {
960
1001
  return actionFailure("FAST_MODE_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
961
1002
  }
962
1003
  }
963
1004
  async setContextWindowK(value) {
964
- if (this.busy)
965
- return actionFailure("CONTEXT_WINDOW_UPDATE_BLOCKED", "cannot change context window while a response is running");
966
- if (typeof value !== "string" || !/^\d+$/u.test(value) || Number(value) <= 0) {
1005
+ if (typeof value !== "string" || !/^\d+$/u.test(value) || Number(value) <= 0 || !Number.isSafeInteger(Number(value) * 1000)) {
967
1006
  return actionFailure("CONTEXT_WINDOW_INVALID", "context window must be a positive integer in k");
968
1007
  }
969
1008
  const contextWindowK = Number(value);
970
1009
  const contextWindowTokens = contextWindowK * 1000;
1010
+ const engine = this.runtime.engine;
971
1011
  try {
972
- await this.runtime.engine.setContextWindowTokensOverride(contextWindowTokens);
973
- const metrics = await this.runtime.engine.contextMetrics();
974
- this.runtime.initialMetrics = metrics;
975
- this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
976
- return { ok: true, contextWindowK, contextWindowTokens };
1012
+ const { deferred } = await engine.updateSessionSettings({ contextWindowTokens });
1013
+ if (!deferred)
1014
+ await this.refreshSessionSettingMetrics(engine);
1015
+ if (this.runtime.engine === engine)
1016
+ this.broadcastSync();
1017
+ return { ok: true, contextWindowK, contextWindowTokens, deferred };
977
1018
  }
978
1019
  catch (error) {
979
1020
  return actionFailure("CONTEXT_WINDOW_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
980
1021
  }
981
1022
  }
1023
+ async setSessionModel(model, reasoning) {
1024
+ if (typeof model !== "string" || !model.trim() || model.length > 256 || /[\s\x00-\x1f]/u.test(model.trim())) {
1025
+ return actionFailure("MODEL_INVALID", "invalid model");
1026
+ }
1027
+ if (reasoning !== undefined && (typeof reasoning !== "string" || !["none", "minimal", "low", "medium", "high", "xhigh", "max", "default", "off"].includes(reasoning))) {
1028
+ return actionFailure("MODEL_INVALID", "invalid reasoning effort");
1029
+ }
1030
+ const engine = this.runtime.engine;
1031
+ const nextModel = model.trim();
1032
+ const current = { ...engine.getModelSettings(), ...engine.getPendingSessionSettings() };
1033
+ const argument = reasoning;
1034
+ const validationError = validateModelReasoningArgument(nextModel, argument);
1035
+ if (validationError)
1036
+ return actionFailure("MODEL_INVALID", validationError);
1037
+ const update = resolveModelReasoningUpdate(argument, current.reasoning, nextModel, nextModel !== current.model);
1038
+ try {
1039
+ // Current engine only: no env write, provider replacement, transcript message,
1040
+ // runtime-context injection or prompt-cache prefix change for UI settings.
1041
+ const { deferred } = await engine.updateSessionSettings({ model: nextModel, ...(update.update ? { reasoning: update.reasoning } : {}) });
1042
+ if (!deferred)
1043
+ await this.refreshSessionSettingMetrics(engine);
1044
+ if (this.runtime.engine === engine)
1045
+ this.broadcastSync();
1046
+ return { ok: true, deferred, modelSettings: { model: nextModel, reasoning: update.reasoning } };
1047
+ }
1048
+ catch (error) {
1049
+ return actionFailure("MODEL_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
1050
+ }
1051
+ }
1052
+ sessionPrompt() {
1053
+ return readSessionPrompt(this.runtime.engine);
1054
+ }
1055
+ async saveSessionPrompt(body) {
1056
+ const engine = this.runtime.engine;
1057
+ const result = await updateSessionPrompt(engine, body);
1058
+ if (this.runtime.engine === engine) {
1059
+ if (!result.deferred)
1060
+ await this.refreshSessionSettingMetrics(engine);
1061
+ this.publishRuntimeContext();
1062
+ this.broadcastSync();
1063
+ }
1064
+ return result;
1065
+ }
1066
+ async compactSession() {
1067
+ const engine = this.runtime.engine;
1068
+ try {
1069
+ const result = await engine.updateSessionSettings({ compact: true });
1070
+ if (this.runtime.engine === engine) {
1071
+ if (result.compaction?.report)
1072
+ this.appendCompaction(result.compaction.report);
1073
+ if (!result.deferred)
1074
+ await this.refreshSessionSettingMetrics(engine);
1075
+ this.broadcastSync();
1076
+ }
1077
+ return { ok: true, deferred: result.deferred };
1078
+ }
1079
+ catch (error) {
1080
+ return actionFailure("COMPACTION_FAILED", error instanceof Error ? error.message : String(error));
1081
+ }
1082
+ }
982
1083
  loginForm(providerValue) {
983
1084
  const provider = parseLoginProvider(providerValue);
984
1085
  return createLoginFormPayload(this.runtime.envPath, provider);
@@ -1139,7 +1240,9 @@ export class WebRepl {
1139
1240
  break;
1140
1241
  offset = history.value.nextOffset;
1141
1242
  } while (true);
1142
- return records.reverse().map(({ record }) => ({
1243
+ // Output history includes foreground commands too. Only an explicit durable
1244
+ // background transition belongs in this UI; legacy records are not inferred.
1245
+ return records.filter(({ record }) => record.metadata.backgrounded === true).reverse().map(({ record }) => ({
1143
1246
  kind: "terminal", taskId: `terminal:${record.runId}`, type: "终端",
1144
1247
  ownerSessionId: session.sessionId, sessionId: record.runId,
1145
1248
  status: record.lifecycle === "lost" ? "lost" : record.exit?.status ?? "unknown",
@@ -1923,6 +2026,8 @@ async function route(req, res, router) {
1923
2026
  return sendFile(res, highlightAssetPath, "text/javascript; charset=utf-8");
1924
2027
  if (req.method === "GET" && url.pathname === "/vendor/highlight-theme.css")
1925
2028
  return sendFile(res, highlightThemeAssetPath, "text/css; charset=utf-8");
2029
+ if (url.pathname === "/api/prompt-config")
2030
+ return handlePromptConfigRequest(req, res);
1926
2031
  if (req.method === "GET" && url.pathname === "/api/client-info")
1927
2032
  return sendJson(res, router.clientInfo());
1928
2033
  if (req.method === "POST" && url.pathname === "/api/client-reload") {
@@ -2023,6 +2128,32 @@ async function route(req, res, router) {
2023
2128
  const body = await readJsonBody(req);
2024
2129
  return sendJson(res, repl.setAppPrompt(body));
2025
2130
  }
2131
+ if (url.pathname === "/api/session-prompt" && (req.method === "GET" || req.method === "POST")) {
2132
+ res.setHeader("Cache-Control", "no-store");
2133
+ try {
2134
+ if (req.method === "GET")
2135
+ return sendJson(res, await repl.sessionPrompt());
2136
+ let body;
2137
+ try {
2138
+ body = await readJsonBody(req);
2139
+ }
2140
+ catch {
2141
+ return sendJson(res, actionFailure("SESSION_PROMPT_INVALID", "invalid JSON body"), 400);
2142
+ }
2143
+ return sendJson(res, await repl.saveSessionPrompt(body));
2144
+ }
2145
+ catch (error) {
2146
+ const status = error instanceof SessionPromptError ? error.statusCode : 500;
2147
+ const code = status === 409 ? "SESSION_PROMPT_CONFLICT" : status === 400 ? "SESSION_PROMPT_INVALID" : "SESSION_PROMPT_FAILED";
2148
+ return sendJson(res, actionFailure(code, error instanceof Error ? error.message : String(error)), status);
2149
+ }
2150
+ }
2151
+ if (req.method === "POST" && url.pathname === "/api/session-model") {
2152
+ const body = await readJsonBody(req);
2153
+ return sendJson(res, await repl.setSessionModel(body.model, body.reasoning));
2154
+ }
2155
+ if (req.method === "POST" && url.pathname === "/api/compact")
2156
+ return sendJson(res, await repl.compactSession());
2026
2157
  if (req.method === "POST" && url.pathname === "/api/fast-mode") {
2027
2158
  const body = await readJsonBody(req);
2028
2159
  return sendJson(res, await repl.setFastMode(body.enabled === true));
@@ -2149,6 +2280,7 @@ function webCatalog(runtime) {
2149
2280
  return {
2150
2281
  commands: replCommandDefinitions,
2151
2282
  modelIds,
2283
+ modelReasoning: Object.fromEntries(modelIds.map(model => [model, reasoningEffortsForModel(model) ?? []])),
2152
2284
  reasoning: ["none", "minimal", "low", "medium", "high", "xhigh", "max", "default", "off"],
2153
2285
  envPath: runtime.envPath,
2154
2286
  };