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.
- package/dist/agents/agent-definition.d.ts +2 -0
- package/dist/agents/agent-definition.js +10 -0
- package/dist/agents/agent-definition.js.map +1 -1
- package/dist/agents/agent-tool.js +4 -0
- package/dist/agents/agent-tool.js.map +1 -1
- package/dist/agents/coordination-boundary.test.d.ts +1 -0
- package/dist/agents/coordination-boundary.test.js +58 -0
- package/dist/agents/coordination-boundary.test.js.map +1 -0
- package/dist/context/context-manager.d.ts +4 -0
- package/dist/context/context-manager.js +6 -2
- package/dist/context/context-manager.js.map +1 -1
- package/dist/context/prompt-config.d.ts +35 -0
- package/dist/context/prompt-config.js +151 -0
- package/dist/context/prompt-config.js.map +1 -0
- package/dist/context/prompt-config.test.d.ts +1 -0
- package/dist/context/prompt-config.test.js +128 -0
- package/dist/context/prompt-config.test.js.map +1 -0
- package/dist/context/prompts.d.ts +4 -1
- package/dist/context/prompts.js +13 -30
- package/dist/context/prompts.js.map +1 -1
- package/dist/context/smoke-context.js +3 -1
- package/dist/context/smoke-context.js.map +1 -1
- package/dist/context/system.md +16 -0
- package/dist/core/query-engine.d.ts +30 -1
- package/dist/core/query-engine.js +321 -111
- package/dist/core/query-engine.js.map +1 -1
- package/dist/core/query-session-settings.test.d.ts +1 -0
- package/dist/core/query-session-settings.test.js +252 -0
- package/dist/core/query-session-settings.test.js.map +1 -0
- package/dist/core/query.d.ts +16 -0
- package/dist/core/query.js +32 -2
- package/dist/core/query.js.map +1 -1
- package/dist/core/run-agent.js +3 -5
- package/dist/core/run-agent.js.map +1 -1
- package/dist/core/session-prompt-inheritance.test.d.ts +1 -0
- package/dist/core/session-prompt-inheritance.test.js +132 -0
- package/dist/core/session-prompt-inheritance.test.js.map +1 -0
- package/dist/core/session-settings-prompt.d.ts +43 -0
- package/dist/core/session-settings-prompt.js +84 -0
- package/dist/core/session-settings-prompt.js.map +1 -0
- package/dist/core/session-settings-prompt.test.d.ts +1 -0
- package/dist/core/session-settings-prompt.test.js +266 -0
- package/dist/core/session-settings-prompt.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/model/model-metadata.json +1 -3
- package/dist/model/openai-responses-instructions.test.d.ts +1 -0
- package/dist/model/openai-responses-instructions.test.js +64 -0
- package/dist/model/openai-responses-instructions.test.js.map +1 -0
- package/dist/model/openai-responses-mapper.js +5 -16
- package/dist/model/openai-responses-mapper.js.map +1 -1
- package/dist/model/smoke-responses-mapper.js +8 -5
- package/dist/model/smoke-responses-mapper.js.map +1 -1
- package/dist/plugins/plugin-system.js +3 -0
- package/dist/plugins/plugin-system.js.map +1 -1
- package/dist/session/session-export.d.ts +10 -0
- package/dist/session/session-export.js.map +1 -1
- package/dist/session/session-store.d.ts +11 -0
- package/dist/session/session-store.js +23 -0
- package/dist/session/session-store.js.map +1 -1
- package/dist/tools/builtins/exec-process-manager.js +4 -1
- package/dist/tools/builtins/exec-process-manager.js.map +1 -1
- package/dist/tools/terminal-background-history.test.d.ts +1 -0
- package/dist/tools/terminal-background-history.test.js +77 -0
- package/dist/tools/terminal-background-history.test.js.map +1 -0
- package/dist/tools/terminal-output-store.d.ts +4 -0
- package/dist/tools/terminal-output-store.js +18 -0
- package/dist/tools/terminal-output-store.js.map +1 -1
- package/dist/web/index.d.ts +26 -1
- package/dist/web/index.js +150 -18
- package/dist/web/index.js.map +1 -1
- package/dist/web/prompt-config-protocol.d.ts +4 -0
- package/dist/web/prompt-config-protocol.js +57 -0
- package/dist/web/prompt-config-protocol.js.map +1 -0
- package/dist/web/prompt-config-protocol.test.d.ts +1 -0
- package/dist/web/prompt-config-protocol.test.js +78 -0
- package/dist/web/prompt-config-protocol.test.js.map +1 -0
- package/dist/web/runtime-context-protocol.d.ts +1 -0
- package/dist/web/runtime-context-protocol.js +1 -0
- package/dist/web/runtime-context-protocol.js.map +1 -1
- package/dist/web/session-prompt-integration.test.d.ts +1 -0
- package/dist/web/session-prompt-integration.test.js +57 -0
- package/dist/web/session-prompt-integration.test.js.map +1 -0
- package/dist/web/session-prompt-protocol.d.ts +8 -0
- package/dist/web/session-prompt-protocol.js +11 -0
- package/dist/web/session-prompt-protocol.js.map +1 -0
- package/dist/web/session-settings.test.d.ts +1 -0
- package/dist/web/session-settings.test.js +254 -0
- package/dist/web/session-settings.test.js.map +1 -0
- package/dist/web/smoke-terminal-output-http.js +10 -5
- package/dist/web/smoke-terminal-output-http.js.map +1 -1
- package/dist/web/smoke-web-terminal-tasks.js +100 -3
- package/dist/web/smoke-web-terminal-tasks.js.map +1 -1
- package/docs/prompt-config.md +86 -0
- package/package.json +4 -3
- package/scripts/build-standalone.mjs +3 -0
- 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.
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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.
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
this.
|
|
181
|
-
|
|
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
|
-
|
|
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
|
|
278
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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;
|