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
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { InMemoryAppState } from "../app/app-state.js";
2
3
  import { InMemoryAppPromptStore } from "../app/app-prompt.js";
3
4
  import { AppPromptContextManager, DefaultContextManager, readProjectMemoryDocuments } from "../context/context-manager.js";
@@ -14,6 +15,7 @@ import { GENERAL_PURPOSE_AGENT } from "../agents/agent-definition.js";
14
15
  import { SessionStore } from "../session/session-store.js";
15
16
  import { buildPromptCacheDiagnostics } from "./prompt-cache-telemetry.js";
16
17
  import { computeStaticTokens } from "./context-metrics.js";
18
+ import { applySessionPrompt, sessionPromptMode, createSessionPromptSnapshot, parseSessionPromptUpdate, SessionPromptError } from "./session-settings-prompt.js";
17
19
  const DEFAULT_SESSION_TITLE_DELAY_MS = 5000;
18
20
  export class QueryEngine {
19
21
  options;
@@ -25,14 +27,23 @@ export class QueryEngine {
25
27
  currentModel;
26
28
  currentReasoning;
27
29
  currentFastMode = false;
30
+ running = false;
31
+ pendingSessionSettings = {};
32
+ sessionPromptState;
33
+ activeSessionPrompt = null;
34
+ ephemeralSessionKey = randomUUID();
35
+ // Idle mutations and the next generator share a barrier. Never compact alongside a request.
36
+ settingsMutation = Promise.resolve();
28
37
  currentContextWindowTokens;
29
38
  currentModelGateway;
30
39
  sessionInitialized = false;
40
+ sessionInitialization;
31
41
  titleSchedulerVersion = 0;
32
42
  titleAgentRun;
33
43
  sessionTitleListeners = new Set();
34
44
  appPromptStore;
35
45
  contextManager;
46
+ baselineContextManager;
36
47
  additionalPromptContextManager;
37
48
  pendingCwdTransitionPaths;
38
49
  pendingRequestContext;
@@ -49,7 +60,12 @@ export class QueryEngine {
49
60
  ?? options.contextManager
50
61
  ?? new DefaultContextManager({ cwd: options.cwd });
51
62
  this.additionalPromptContextManager = new AdditionalPromptContextManager(baseContextManager, options.additionalPromptSections);
52
- this.contextManager = new AppPromptContextManager(this.additionalPromptContextManager, this.appPromptStore);
63
+ this.baselineContextManager = new AppPromptContextManager(this.additionalPromptContextManager, this.appPromptStore);
64
+ this.contextManager = { build: async (input) => {
65
+ // Capture before any asynchronous baseline work: an in-flight request never observes a later edit.
66
+ const content = this.activeSessionPrompt;
67
+ return applySessionPrompt(await this.baselineContextManager.build(input), content);
68
+ } };
53
69
  this.pendingCwdTransitionPaths = normalizeCwdTransitionPaths(options.cwdTransitionPaths);
54
70
  }
55
71
  forkForSession(sessionId, resume = true) {
@@ -77,25 +93,39 @@ export class QueryEngine {
77
93
  this.pendingRequestContextConsumed = onConsumed;
78
94
  }
79
95
  async initialize() {
96
+ if (this.sessionInitialization)
97
+ return this.sessionInitialization;
80
98
  if (this.sessionInitialized)
81
99
  return;
82
- this.sessionInitialized = true;
83
- if (this.options.session?.enabled === false)
84
- return;
85
- if (!this.options.session)
86
- return;
87
- await this.openSession({
88
- sessionId: this.options.session.sessionId,
89
- resume: this.options.session.resume,
90
- });
100
+ const initialization = (async () => {
101
+ if (this.options.session?.enabled !== false && this.options.session) {
102
+ await this.openSession({ sessionId: this.options.session.sessionId, resume: this.options.session.resume });
103
+ }
104
+ this.sessionInitialized = true;
105
+ })();
106
+ this.sessionInitialization = initialization;
107
+ try {
108
+ await initialization;
109
+ }
110
+ finally {
111
+ this.sessionInitialization = undefined;
112
+ }
91
113
  }
92
114
  async newSession() {
93
- this.sessionInitialized = true;
94
- await this.openSession({ resume: false });
95
- const snapshot = this.sessionStore?.snapshot();
96
- if (!snapshot)
97
- throw new Error("session transcripts are disabled");
98
- return snapshot;
115
+ if (this.running)
116
+ throw new Error("cannot switch session while query engine is running");
117
+ const mutation = this.settingsMutation.then(async () => {
118
+ await this.sessionInitialization;
119
+ await this.openSession({ resume: false });
120
+ this.sessionInitialized = true;
121
+ const snapshot = this.sessionStore?.snapshot();
122
+ if (!snapshot)
123
+ throw new Error("session transcripts are disabled");
124
+ return snapshot;
125
+ });
126
+ // Session switches share the editor barrier: stale saves can never land in a new session.
127
+ this.settingsMutation = mutation.catch(() => undefined);
128
+ return mutation;
99
129
  }
100
130
  async listSessions(limit = 10) {
101
131
  if (this.options.session?.enabled === false || !this.options.session)
@@ -121,92 +151,250 @@ export class QueryEngine {
121
151
  });
122
152
  }
123
153
  async *sendUserText(text, options = {}) {
124
- await this.initialize();
125
- const cwdTransitionPaths = this.pendingCwdTransitionPaths;
126
- const pendingRequestContext = this.pendingRequestContext;
127
- const pendingRequestContextConsumed = this.pendingRequestContextConsumed;
128
- this.pendingCwdTransitionPaths = undefined;
129
- this.options.cwdTransitionPaths = undefined;
130
- if (cwdTransitionPaths?.length)
131
- void this.options.onCwdTransitionConsumed?.();
132
- const userMessage = options.blocks
133
- ? await this.persistMessageImages({
134
- ...createTextMessage("user", options.displayText ?? text),
135
- blocks: options.blocks,
136
- })
137
- : createTextMessage("user", text);
138
- this.history.push(userMessage);
139
- this.sessionStore?.recordMessage(userMessage);
140
- this.scheduleSessionTitleCheck();
141
- const initMessage = createSystemInitMessage({
142
- agentId: this.agentId,
143
- tools: this.options.tools.names(),
144
- model: this.currentModel,
145
- commands: [...(this.options.commands ?? [])],
146
- agents: [...(this.options.agents ?? [])],
147
- skills: [...(this.options.skills ?? [])],
148
- plugins: [...(this.options.plugins ?? [])],
149
- });
150
- initMessage.metadata = {
151
- ...initMessage.metadata,
152
- reasoning: cloneReasoningConfig(this.currentReasoning),
153
- };
154
- yield {
155
- type: "message",
156
- message: initMessage,
157
- };
158
- this.sessionStore?.recordMessage(initMessage);
159
- const queryOptions = {
160
- agentId: this.agentId,
161
- model: this.currentModel,
162
- reasoning: cloneReasoningConfig(this.currentReasoning),
163
- queryOrigin: this.options.queryOrigin ?? "repl",
164
- serviceTier: this.currentFastMode ? "priority" : undefined,
165
- maxOutputTokensOverride: this.options.maxOutputTokensOverride,
166
- contextWindowTokensOverride: this.currentContextWindowTokens,
167
- maxTurns: this.options.maxTurns,
168
- workspaceCwd: this.options.cwd,
169
- requestContext: pendingRequestContext || cwdTransitionPaths?.length
170
- ? {
171
- ...pendingRequestContext,
172
- ...(cwdTransitionPaths?.length
173
- ? { cwdTransition: { paths: cwdTransitionPaths, current: cwdTransitionPaths.at(-1) } }
174
- : {}),
154
+ if (this.running)
155
+ throw new Error("query engine is already running");
156
+ this.running = true;
157
+ try {
158
+ await this.settingsMutation;
159
+ await this.initialize();
160
+ const cwdTransitionPaths = this.pendingCwdTransitionPaths;
161
+ const pendingRequestContext = this.pendingRequestContext;
162
+ const pendingRequestContextConsumed = this.pendingRequestContextConsumed;
163
+ this.pendingCwdTransitionPaths = undefined;
164
+ this.options.cwdTransitionPaths = undefined;
165
+ if (cwdTransitionPaths?.length)
166
+ void this.options.onCwdTransitionConsumed?.();
167
+ const userMessage = options.blocks
168
+ ? await this.persistMessageImages({
169
+ ...createTextMessage("user", options.displayText ?? text),
170
+ blocks: options.blocks,
171
+ })
172
+ : createTextMessage("user", text);
173
+ this.history.push(userMessage);
174
+ this.sessionStore?.recordMessage(userMessage);
175
+ this.scheduleSessionTitleCheck();
176
+ const initMessage = createSystemInitMessage({
177
+ agentId: this.agentId,
178
+ tools: this.options.tools.names(),
179
+ model: this.currentModel,
180
+ commands: [...(this.options.commands ?? [])],
181
+ agents: [...(this.options.agents ?? [])],
182
+ skills: [...(this.options.skills ?? [])],
183
+ plugins: [...(this.options.plugins ?? [])],
184
+ });
185
+ initMessage.metadata = {
186
+ ...initMessage.metadata,
187
+ reasoning: cloneReasoningConfig(this.currentReasoning),
188
+ };
189
+ yield {
190
+ type: "message",
191
+ message: initMessage,
192
+ };
193
+ this.sessionStore?.recordMessage(initMessage);
194
+ const queryOptions = {
195
+ agentId: this.agentId,
196
+ model: this.currentModel,
197
+ reasoning: cloneReasoningConfig(this.currentReasoning),
198
+ queryOrigin: this.options.queryOrigin ?? "repl",
199
+ serviceTier: this.currentFastMode ? "priority" : undefined,
200
+ maxOutputTokensOverride: this.options.maxOutputTokensOverride,
201
+ contextWindowTokensOverride: this.currentContextWindowTokens,
202
+ maxTurns: this.options.maxTurns,
203
+ workspaceCwd: this.options.cwd,
204
+ requestContext: pendingRequestContext || cwdTransitionPaths?.length
205
+ ? {
206
+ ...pendingRequestContext,
207
+ ...(cwdTransitionPaths?.length
208
+ ? { cwdTransition: { paths: cwdTransitionPaths, current: cwdTransitionPaths.at(-1) } }
209
+ : {}),
210
+ }
211
+ : undefined,
212
+ onRequestContextConsumed: pendingRequestContext ? () => {
213
+ pendingRequestContextConsumed?.();
214
+ if (this.pendingRequestContext === pendingRequestContext) {
215
+ this.pendingRequestContext = undefined;
216
+ this.pendingRequestContextConsumed = undefined;
217
+ }
218
+ } : undefined,
219
+ abortSignal: options.abortSignal,
220
+ stopAfterTurn: options.stopAfterTurn,
221
+ beforeTurn: (messages) => {
222
+ this.options.refreshTools?.();
223
+ return this.consumeSessionSettings(messages);
224
+ },
225
+ };
226
+ const stream = query(this.history, {
227
+ ...this.options,
228
+ contextManager: this.contextManager,
229
+ modelGateway: this.currentModelGateway,
230
+ taskNotificationSource: this.options.taskNotificationSource,
231
+ toolResultMemory: this.sessionStore?.toolResultMemory,
232
+ session: this.sessionStore ? { sessionId: this.sessionStore.sessionId, sessionDir: this.sessionStore.sessionDir, rootDir: this.options.session?.rootDir } : undefined,
233
+ recordContentReplacements: (records) => this.sessionStore?.recordContentReplacements(records),
234
+ exportToolCalls: (calls) => this.recordSyntheticToolCalls(calls),
235
+ applyCompaction: (result) => this.applyCompactionResult(result),
236
+ }, queryOptions);
237
+ for await (const event of stream) {
238
+ if (event.type === "message") {
239
+ const message = await this.persistMessageImages(event.message);
240
+ this.history.push(message);
241
+ this.sessionStore?.recordMessage(message);
242
+ yield { ...event, message };
243
+ continue;
175
244
  }
176
- : undefined,
177
- onRequestContextConsumed: pendingRequestContext ? () => {
178
- pendingRequestContextConsumed?.();
179
- if (this.pendingRequestContext === pendingRequestContext) {
180
- this.pendingRequestContext = undefined;
181
- this.pendingRequestContextConsumed = undefined;
245
+ if (event.type === "terminal") {
246
+ this.lastTerminalReason = event.reason;
247
+ // The final assistant/tool messages have already been recorded by this wrapper.
248
+ const hadPending = Object.keys(this.pendingSessionSettings).length > 0;
249
+ for (const result of await this.flushSessionSettings()) {
250
+ if (result.report)
251
+ yield { type: "context.compacted", compaction: result.report };
252
+ }
253
+ if (hadPending)
254
+ yield { type: "context.metrics", metrics: await this.contextMetrics() };
182
255
  }
183
- } : undefined,
184
- abortSignal: options.abortSignal,
185
- stopAfterTurn: options.stopAfterTurn,
186
- };
187
- const stream = query(this.history, {
188
- ...this.options,
189
- contextManager: this.contextManager,
190
- modelGateway: this.currentModelGateway,
191
- taskNotificationSource: this.options.taskNotificationSource,
192
- toolResultMemory: this.sessionStore?.toolResultMemory,
193
- session: this.sessionStore ? { sessionId: this.sessionStore.sessionId, sessionDir: this.sessionStore.sessionDir, rootDir: this.options.session?.rootDir } : undefined,
194
- recordContentReplacements: (records) => this.sessionStore?.recordContentReplacements(records),
195
- exportToolCalls: (calls) => this.recordSyntheticToolCalls(calls),
196
- applyCompaction: (result) => this.applyCompactionResult(result),
197
- }, queryOptions);
198
- for await (const event of stream) {
199
- if (event.type === "message") {
200
- const message = await this.persistMessageImages(event.message);
201
- this.history.push(message);
202
- this.sessionStore?.recordMessage(message);
203
- yield { ...event, message };
204
- continue;
256
+ yield event;
205
257
  }
206
- if (event.type === "terminal")
207
- this.lastTerminalReason = event.reason;
208
- yield event;
209
258
  }
259
+ finally {
260
+ // Covers initialization errors, aborts, exceptions and consumer generator.return().
261
+ // Do not yield in finally: a closing consumer may never resume the generator.
262
+ try {
263
+ do {
264
+ await this.flushSessionSettings();
265
+ } while (Object.keys(this.pendingSessionSettings).length);
266
+ }
267
+ finally {
268
+ this.running = false;
269
+ }
270
+ }
271
+ }
272
+ /** Queue instance-local settings; no prompt messages or provider-global mutation. */
273
+ async updateSessionSettings(patch) {
274
+ const normalized = {};
275
+ if (Object.hasOwn(patch, "model"))
276
+ normalized.model = patch.model?.trim() || undefined;
277
+ if (Object.hasOwn(patch, "reasoning"))
278
+ normalized.reasoning = cloneReasoningConfig(patch.reasoning);
279
+ if (Object.hasOwn(patch, "fastMode"))
280
+ normalized.fastMode = patch.fastMode === true;
281
+ if (Object.hasOwn(patch, "contextWindowTokens")) {
282
+ const tokens = normalizePositiveInteger(patch.contextWindowTokens);
283
+ if (!tokens)
284
+ throw new Error("context window tokens must be a positive integer");
285
+ normalized.contextWindowTokens = tokens;
286
+ }
287
+ if (patch.compact)
288
+ normalized.compact = true;
289
+ if (this.running) {
290
+ this.pendingSessionSettings = { ...this.pendingSessionSettings, ...normalized };
291
+ return { deferred: true };
292
+ }
293
+ const mutation = this.settingsMutation.then(async () => {
294
+ await this.initialize();
295
+ const update = await this.applySessionSettings(normalized, this.getHistoryMessages());
296
+ if (update?.compaction)
297
+ this.applyCompactionResult(update.compaction);
298
+ return { deferred: false, ...(update?.compaction ? { compaction: update.compaction } : {}) };
299
+ });
300
+ // A rejected mutation must not poison subsequent settings or user input.
301
+ this.settingsMutation = mutation.catch(() => undefined);
302
+ return mutation;
303
+ }
304
+ /** Editor state includes accepted queued changes; runtime exports still describe the active turn. */
305
+ async getSessionPrompt() {
306
+ await this.settingsMutation;
307
+ await this.initialize();
308
+ const snapshot = await this.buildPromptExportSnapshot(this.getHistoryMessages());
309
+ return snapshot.sessionPromptConfig;
310
+ }
311
+ async updateSessionPrompt(input) {
312
+ const update = parseSessionPromptUpdate(input);
313
+ const mutation = this.settingsMutation.then(async () => {
314
+ await this.initialize();
315
+ const key = this.sessionPromptKey();
316
+ const snapshot = await this.buildPromptExportSnapshot(this.getHistoryMessages());
317
+ const current = snapshot.sessionPromptConfig;
318
+ if (key !== this.sessionPromptKey() || update.revision !== current.revision) {
319
+ throw new SessionPromptError("session prompt changed; reload before saving", 409);
320
+ }
321
+ if (!update.reset && update.mode === "legacy_full_override" && current.mode !== "legacy_full_override") {
322
+ throw new SessionPromptError("full replacement is only supported for existing legacy prompts", 400);
323
+ }
324
+ const state = { content: update.reset ? null : update.content, mode: update.reset ? "inherit" : update.mode ?? (current.mode === "legacy_full_override" ? "legacy_full_override" : "append"), revision: randomUUID() };
325
+ const nextSnapshot = (await this.buildPromptExportSnapshot(this.getHistoryMessages(), state)).sessionPromptConfig;
326
+ // Persist before accepting, including busy edits, so same-session resume cannot lose them.
327
+ this.sessionStore?.recordSessionPrompt(state);
328
+ this.sessionPromptState = state;
329
+ const deferred = this.running;
330
+ if (deferred)
331
+ this.pendingSessionSettings = { ...this.pendingSessionSettings, sessionPrompt: state };
332
+ else
333
+ this.activeSessionPrompt = state;
334
+ return { ok: true, ...nextSnapshot, deferred };
335
+ });
336
+ this.settingsMutation = mutation.catch(() => undefined);
337
+ return mutation;
338
+ }
339
+ sessionPromptKey() {
340
+ return this.sessionStore ? `${this.sessionStore.sessionDir}:${this.agentId}` : this.ephemeralSessionKey;
341
+ }
342
+ getPendingSessionSettings() {
343
+ const patch = { ...this.pendingSessionSettings };
344
+ if (Object.hasOwn(patch, "reasoning"))
345
+ patch.reasoning = cloneReasoningConfig(patch.reasoning);
346
+ if (patch.sessionPrompt)
347
+ patch.sessionPrompt = { ...patch.sessionPrompt };
348
+ return patch;
349
+ }
350
+ async consumeSessionSettings(messages) {
351
+ const patch = this.pendingSessionSettings;
352
+ if (!Object.keys(patch).length)
353
+ return undefined;
354
+ this.pendingSessionSettings = {};
355
+ return this.applySessionSettings(patch, messages);
356
+ }
357
+ async applySessionSettings(patch, messages) {
358
+ const changed = {};
359
+ if (Object.hasOwn(patch, "sessionPrompt"))
360
+ this.activeSessionPrompt = patch.sessionPrompt ?? null;
361
+ if (Object.hasOwn(patch, "model") && patch.model !== this.currentModel) {
362
+ this.currentModel = patch.model;
363
+ changed.model = patch.model;
364
+ }
365
+ if (Object.hasOwn(patch, "reasoning") && !sameReasoningConfig(patch.reasoning, this.currentReasoning)) {
366
+ this.currentReasoning = cloneReasoningConfig(patch.reasoning);
367
+ changed.reasoning = cloneReasoningConfig(patch.reasoning);
368
+ }
369
+ if (Object.hasOwn(patch, "fastMode") && patch.fastMode !== this.currentFastMode) {
370
+ this.currentFastMode = patch.fastMode === true;
371
+ this.sessionStore?.recordFastMode(this.currentFastMode);
372
+ changed.fastMode = this.currentFastMode;
373
+ }
374
+ if (Object.hasOwn(patch, "contextWindowTokens") && patch.contextWindowTokens !== this.currentContextWindowTokens) {
375
+ this.currentContextWindowTokens = patch.contextWindowTokens;
376
+ this.sessionStore?.recordContextWindowTokens(this.currentContextWindowTokens);
377
+ changed.contextWindowTokens = this.currentContextWindowTokens;
378
+ }
379
+ let compaction;
380
+ if (patch.compact) {
381
+ const compactor = this.options.compactor ?? new ModelDrivenCompactor(this.currentModelGateway);
382
+ const budget = { ...this.options.contextBudget, contextWindowTokens: this.currentContextWindowTokens ?? this.options.contextBudget?.contextWindowTokens };
383
+ compaction = withCompactionReport(await (compactor.manualCompact?.(messages, budget) ?? compactor.compact(messages, budget)), messages.length);
384
+ }
385
+ return Object.keys(changed).length || compaction ? { settings: changed, compaction } : undefined;
386
+ }
387
+ async flushSessionSettings() {
388
+ const results = [];
389
+ while (Object.keys(this.pendingSessionSettings).length) {
390
+ const update = await this.consumeSessionSettings(this.getHistoryMessages());
391
+ if (update?.compaction) {
392
+ this.applyCompactionResult(update.compaction);
393
+ if (update.compaction.changed)
394
+ results.push(update.compaction);
395
+ }
396
+ }
397
+ return results;
210
398
  }
211
399
  setModel(model, reasoning, updateReasoning = false) {
212
400
  this.currentModel = model?.trim() || undefined;
@@ -264,6 +452,9 @@ export class QueryEngine {
264
452
  return this.appPromptStore.snapshot();
265
453
  }
266
454
  reset() {
455
+ if (this.running)
456
+ throw new Error("cannot reset while query engine is running");
457
+ this.pendingSessionSettings = {};
267
458
  this.history.length = 0;
268
459
  this.lastTerminalReason = undefined;
269
460
  this.cancelPendingTitleWork();
@@ -271,17 +462,10 @@ export class QueryEngine {
271
462
  this.notifySessionTitleChange(this.sessionStore?.snapshot());
272
463
  }
273
464
  async compact(options = {}) {
274
- await this.initialize();
275
465
  if (options.abortSignal?.aborted)
276
466
  return { messages: this.getHistoryMessages(), changed: false, reason: "none" };
277
- const compactor = this.options.compactor ?? new ModelDrivenCompactor(this.currentModelGateway);
278
- const contextBudget = {
279
- ...this.options.contextBudget,
280
- contextWindowTokens: this.currentContextWindowTokens ?? this.options.contextBudget?.contextWindowTokens,
281
- };
282
- const result = withCompactionReport(await (compactor.manualCompact?.(this.history, contextBudget) ?? compactor.compact(this.history, contextBudget)), this.history.length);
283
- this.applyCompactionResult(result);
284
- return result;
467
+ const result = await this.updateSessionSettings({ compact: true });
468
+ return result.compaction ?? { messages: this.getHistoryMessages(), changed: false, reason: "none" };
285
469
  }
286
470
  async pureCompact(options = {}) {
287
471
  await this.initialize();
@@ -353,7 +537,12 @@ export class QueryEngine {
353
537
  await this.initialize();
354
538
  return this.buildPromptExportSnapshot(this.getHistoryMessages());
355
539
  }
356
- async buildPromptExportSnapshot(messages) {
540
+ refreshToolsIfIdle() {
541
+ if (!this.running)
542
+ this.options.refreshTools?.();
543
+ }
544
+ async buildPromptExportSnapshot(messages, desiredState = this.sessionPromptState) {
545
+ this.refreshToolsIfIdle();
357
546
  const toolContext = {
358
547
  agentId: this.agentId,
359
548
  tools: this.options.tools,
@@ -376,13 +565,16 @@ export class QueryEngine {
376
565
  emit: () => undefined,
377
566
  };
378
567
  const initialToolDefinitions = this.options.tools.definitions(toolContext);
379
- const context = await this.contextManager.build({
568
+ const activePrompt = this.activeSessionPrompt;
569
+ const baseline = await this.baselineContextManager.build({
380
570
  agentId: this.agentId,
381
571
  messages,
382
572
  enabledTools: initialToolDefinitions.map((tool) => tool.name),
383
573
  toolUseContext: toolContext,
384
574
  });
385
- const toolDefinitions = this.options.tools.definitions(toolContext);
575
+ const context = applySessionPrompt(baseline, activePrompt);
576
+ const desiredPrompt = createSessionPromptSnapshot(this.sessionPromptKey(), desiredState, baseline);
577
+ const toolDefinitions = initialToolDefinitions;
386
578
  const messagesWithUserContext = applyRuntimeContextForPromptCache([], context.userContext, {});
387
579
  const userContextPrompt = messagesWithUserContext[0]?.blocks
388
580
  .filter((block) => block.type === "text")
@@ -392,7 +584,17 @@ export class QueryEngine {
392
584
  model: this.currentModel,
393
585
  reasoning: cloneReasoningConfig(this.currentReasoning),
394
586
  systemPrompt: context.systemPrompt,
395
- baseSystemPrompt: context.systemPrompt,
587
+ baseSystemPrompt: baseline.systemPrompt,
588
+ sessionPromptConfig: desiredPrompt,
589
+ sessionPrompt: {
590
+ mode: sessionPromptMode(activePrompt),
591
+ pendingMode: desiredPrompt.mode,
592
+ effectiveRevision: createSessionPromptSnapshot(this.sessionPromptKey(), activePrompt ?? undefined, baseline).effectiveRevision,
593
+ override: sessionPromptMode(activePrompt) !== "inherit",
594
+ revision: desiredPrompt.revision,
595
+ deferred: Object.hasOwn(this.pendingSessionSettings, "sessionPrompt"),
596
+ pendingOverride: desiredPrompt.override,
597
+ },
396
598
  promptSections: context.promptSections,
397
599
  userContext: context.userContext,
398
600
  systemContext: context.systemContext,
@@ -449,6 +651,9 @@ export class QueryEngine {
449
651
  resume: options.resume,
450
652
  toolResultThresholdChars: this.options.session.toolResultThresholdChars,
451
653
  });
654
+ this.pendingSessionSettings = {};
655
+ this.sessionPromptState = this.sessionStore.getSessionPrompt();
656
+ this.activeSessionPrompt = this.sessionPromptState ?? null;
452
657
  this.history.length = 0;
453
658
  if (options.resume)
454
659
  this.history.push(...this.sessionStore.getInitialMessages());
@@ -609,6 +814,11 @@ function resolveSessionTitleDelayMs() {
609
814
  }
610
815
  return DEFAULT_SESSION_TITLE_DELAY_MS;
611
816
  }
817
+ function sameReasoningConfig(a, b) {
818
+ if (a === null || b === null || a === undefined || b === undefined)
819
+ return a === b;
820
+ return a.effort === b.effort && a.summary === b.summary;
821
+ }
612
822
  function cloneReasoningConfig(reasoning) {
613
823
  if (reasoning === null)
614
824
  return null;