wave-agent-sdk 0.19.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/builtin/plugins/sdd/.wave-plugin/plugin.json +8 -0
- package/builtin/plugins/sdd/hooks/hooks.json +14 -0
- package/builtin/plugins/sdd/scripts/session-start.js +24 -0
- package/builtin/plugins/sdd/scripts/spec-count.js +77 -0
- package/builtin/plugins/sdd/skills/specify/SKILL.md +47 -0
- package/builtin/plugins/sdd/skills/specify/templates/spec-template.md +47 -0
- package/builtin/skills/settings/ENV.md +15 -9
- package/builtin/skills/settings/HOOKS.md +27 -2
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +26 -12
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/managers/aiManager.d.ts +25 -0
- package/dist/managers/aiManager.js +172 -51
- package/dist/managers/backgroundTaskManager.d.ts +6 -0
- package/dist/managers/backgroundTaskManager.js +11 -0
- package/dist/managers/bangManager.d.ts +6 -0
- package/dist/managers/bangManager.js +11 -0
- package/dist/managers/hookManager.d.ts +8 -2
- package/dist/managers/hookManager.js +14 -4
- package/dist/managers/mcpManager.d.ts +18 -4
- package/dist/managers/mcpManager.js +40 -18
- package/dist/managers/permissionManager.d.ts +7 -0
- package/dist/managers/permissionManager.js +102 -142
- package/dist/managers/pluginManager.d.ts +7 -0
- package/dist/managers/pluginManager.js +31 -0
- package/dist/managers/toolManager.js +5 -0
- package/dist/prompts/index.d.ts +12 -1
- package/dist/prompts/index.js +133 -45
- package/dist/services/aiService.d.ts +1 -17
- package/dist/services/aiService.js +3 -85
- package/dist/services/configurationService.d.ts +21 -2
- package/dist/services/configurationService.js +72 -23
- package/dist/services/initializationService.js +14 -4
- package/dist/services/interactionService.js +35 -7
- package/dist/services/remoteSettingsService.d.ts +12 -0
- package/dist/services/remoteSettingsService.js +15 -1
- package/dist/services/session.d.ts +3 -1
- package/dist/services/session.js +12 -4
- package/dist/services/taskManager.d.ts +1 -0
- package/dist/services/taskManager.js +41 -6
- package/dist/tools/bashTool.js +1 -0
- package/dist/tools/editTool.js +24 -10
- package/dist/tools/enterWorktreeTool.js +14 -3
- package/dist/tools/exitWorktreeTool.js +11 -10
- package/dist/tools/grepTool.js +8 -2
- package/dist/tools/types.d.ts +7 -0
- package/dist/tools/writeTool.js +36 -0
- package/dist/types/config.d.ts +2 -0
- package/dist/types/hooks.d.ts +2 -2
- package/dist/utils/bashParser.d.ts +25 -0
- package/dist/utils/bashParser.js +103 -0
- package/dist/utils/configPaths.d.ts +4 -0
- package/dist/utils/configPaths.js +6 -0
- package/dist/utils/containerSetup.js +1 -1
- package/dist/utils/fileSearch.js +4 -2
- package/dist/utils/openaiClient.js +2 -1
- package/dist/utils/pathEncoder.js +7 -2
- package/dist/utils/worktreeUtils.d.ts +17 -0
- package/dist/utils/worktreeUtils.js +339 -1
- package/package.json +1 -1
- package/src/agent.ts +26 -12
- package/src/index.ts +1 -0
- package/src/managers/aiManager.ts +238 -66
- package/src/managers/backgroundTaskManager.ts +15 -0
- package/src/managers/bangManager.ts +15 -0
- package/src/managers/hookManager.ts +20 -5
- package/src/managers/mcpManager.ts +60 -18
- package/src/managers/permissionManager.ts +116 -168
- package/src/managers/pluginManager.ts +29 -0
- package/src/managers/toolManager.ts +7 -0
- package/src/prompts/index.ts +144 -37
- package/src/services/aiService.ts +9 -128
- package/src/services/configurationService.ts +84 -23
- package/src/services/initializationService.ts +17 -4
- package/src/services/interactionService.ts +49 -6
- package/src/services/remoteSettingsService.ts +16 -1
- package/src/services/session.ts +18 -4
- package/src/services/taskManager.ts +56 -8
- package/src/tools/bashTool.ts +1 -0
- package/src/tools/editTool.ts +29 -11
- package/src/tools/enterWorktreeTool.ts +19 -2
- package/src/tools/exitWorktreeTool.ts +15 -12
- package/src/tools/grepTool.ts +11 -2
- package/src/tools/types.ts +7 -0
- package/src/tools/writeTool.ts +43 -0
- package/src/types/config.ts +2 -0
- package/src/types/hooks.ts +2 -2
- package/src/utils/bashParser.ts +106 -0
- package/src/utils/configPaths.ts +7 -0
- package/src/utils/containerSetup.ts +3 -1
- package/src/utils/fileSearch.ts +6 -2
- package/src/utils/openaiClient.ts +2 -0
- package/src/utils/pathEncoder.ts +7 -2
- package/src/utils/worktreeUtils.ts +401 -1
package/src/prompts/index.ts
CHANGED
|
@@ -190,53 +190,160 @@ export interface SystemPromptBlock {
|
|
|
190
190
|
cacheable: boolean;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
|
|
193
|
+
// Aggressive no-tools preamble, aligned with Claude Code's NO_TOOLS_PREAMBLE.
|
|
194
|
+
// The fork path inherits the main conversation's full tool set (required for
|
|
195
|
+
// cache-key match), so the instruction must be explicit about rejection
|
|
196
|
+
// consequences to prevent wasted turns.
|
|
197
|
+
const COMPACT_NO_TOOLS_PREAMBLE = `CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
|
|
194
198
|
|
|
195
|
-
|
|
196
|
-
-
|
|
197
|
-
-
|
|
198
|
-
-
|
|
199
|
-
- Check for accuracy and completeness — ensure nothing critical is missing
|
|
199
|
+
- Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
|
|
200
|
+
- You already have all the context you need in the conversation above.
|
|
201
|
+
- Tool calls will be REJECTED and will waste your only turn — you will fail the task.
|
|
202
|
+
- Your entire response must be plain text: an <analysis> block followed by a <summary> block.
|
|
200
203
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
## Primary Request and Intent
|
|
204
|
-
- The user's core request and success criteria
|
|
205
|
-
- Clarifications, constraints, or scope changes
|
|
206
|
-
|
|
207
|
-
## Key Technical Concepts
|
|
208
|
-
- Frameworks, libraries, patterns, architectural decisions
|
|
204
|
+
`;
|
|
209
205
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
206
|
+
// Aligned with Claude Code's DETAILED_ANALYSIS_INSTRUCTION_BASE. The
|
|
207
|
+
// <analysis> block is a drafting scratchpad that formatCompactSummary()
|
|
208
|
+
// strips before the summary reaches context.
|
|
209
|
+
const COMPACT_DETAILED_ANALYSIS_INSTRUCTION = `Before providing your final summary, wrap your analysis in <analysis> tags to organize your thoughts and ensure you've covered all necessary points. In your analysis process:
|
|
210
|
+
|
|
211
|
+
1. Chronologically analyze each message and section of the conversation. For each section thoroughly identify:
|
|
212
|
+
- The user's explicit requests and intents
|
|
213
|
+
- Your approach to addressing the user's requests
|
|
214
|
+
- Key decisions, technical concepts and code patterns
|
|
215
|
+
- Specific details like:
|
|
216
|
+
- file names
|
|
217
|
+
- full code snippets
|
|
218
|
+
- function signatures
|
|
219
|
+
- file edits
|
|
220
|
+
- Errors that you ran into and how you fixed them
|
|
221
|
+
- Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
|
|
222
|
+
2. Double-check for technical accuracy and completeness, addressing each required element thoroughly.`;
|
|
223
|
+
|
|
224
|
+
// Aligned with Claude Code's BASE_COMPACT_PROMPT (9 sections + example).
|
|
225
|
+
const BASE_COMPACT_PROMPT = `Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
|
|
226
|
+
This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing development work without losing context.
|
|
227
|
+
|
|
228
|
+
${COMPACT_DETAILED_ANALYSIS_INSTRUCTION}
|
|
229
|
+
|
|
230
|
+
Your summary should include the following sections:
|
|
231
|
+
|
|
232
|
+
1. Primary Request and Intent: Capture all of the user's explicit requests and intents in detail
|
|
233
|
+
2. Key Technical Concepts: List all important technical concepts, technologies, and frameworks discussed.
|
|
234
|
+
3. Files and Code Sections: Enumerate specific files and code sections examined, modified, or created. Pay special attention to the most recent messages and include full code snippets where applicable and include a summary of why this file read or edit is important.
|
|
235
|
+
4. Errors and fixes: List all errors that you ran into, and how you fixed them. Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
|
|
236
|
+
5. Problem Solving: Document problems solved and any ongoing troubleshooting efforts.
|
|
237
|
+
6. All user messages: List ALL user messages that are not tool results. These are critical for understanding the users' feedback and changing intent.
|
|
238
|
+
7. Pending Tasks: Outline any pending tasks that you have explicitly been asked to work on.
|
|
239
|
+
8. Current Work: Describe in detail precisely what was being worked on immediately before this summary request, paying special attention to the most recent messages from both user and assistant. Include file names and code snippets where applicable.
|
|
240
|
+
9. Optional Next Step: List the next step that you will take that is related to the most recent work you were doing. IMPORTANT: ensure that this step is DIRECTLY in line with the user's most recent explicit requests, and the task you were working on immediately before this summary request. If your last task was concluded, then only list next steps if they are explicitly in line with the users request. Do not start on tangential requests or really old requests that were already completed without confirming with the user first.
|
|
241
|
+
If there is a next step, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no drift in task interpretation.
|
|
242
|
+
|
|
243
|
+
Here's an example of how your output should be structured:
|
|
244
|
+
|
|
245
|
+
<example>
|
|
246
|
+
<analysis>
|
|
247
|
+
[Your thought process, ensuring all points are covered thoroughly and accurately]
|
|
248
|
+
</analysis>
|
|
249
|
+
|
|
250
|
+
<summary>
|
|
251
|
+
1. Primary Request and Intent:
|
|
252
|
+
[Detailed description]
|
|
253
|
+
|
|
254
|
+
2. Key Technical Concepts:
|
|
255
|
+
- [Concept 1]
|
|
256
|
+
- [Concept 2]
|
|
257
|
+
- [...]
|
|
258
|
+
|
|
259
|
+
3. Files and Code Sections:
|
|
260
|
+
- [File Name 1]
|
|
261
|
+
- [Summary of why this file is important]
|
|
262
|
+
- [Summary of the changes made to this file, if any]
|
|
263
|
+
- [Important Code Snippet]
|
|
264
|
+
- [File Name 2]
|
|
265
|
+
- [Important Code Snippet]
|
|
266
|
+
- [...]
|
|
267
|
+
|
|
268
|
+
4. Errors and fixes:
|
|
269
|
+
- [Detailed description of error 1]:
|
|
270
|
+
- [How you fixed the error]
|
|
271
|
+
- [User feedback on the error if any]
|
|
272
|
+
- [...]
|
|
273
|
+
|
|
274
|
+
5. Problem Solving:
|
|
275
|
+
[Description of solved problems and ongoing troubleshooting]
|
|
276
|
+
|
|
277
|
+
6. All user messages:
|
|
278
|
+
- [Detailed non tool use user message]
|
|
279
|
+
- [...]
|
|
280
|
+
|
|
281
|
+
7. Pending Tasks:
|
|
282
|
+
- [Task 1]
|
|
283
|
+
- [Task 2]
|
|
284
|
+
- [...]
|
|
285
|
+
|
|
286
|
+
8. Current Work:
|
|
287
|
+
[Precise description of current work]
|
|
288
|
+
|
|
289
|
+
9. Optional Next Step:
|
|
290
|
+
[Optional Next step to take]
|
|
291
|
+
|
|
292
|
+
</summary>
|
|
293
|
+
</example>
|
|
294
|
+
|
|
295
|
+
Please provide your summary based on the conversation so far, following this structure and ensuring precision and thoroughness in your response.
|
|
296
|
+
|
|
297
|
+
There may be additional summarization instructions provided in the included context. If so, remember to follow these instructions when creating the above summary.`;
|
|
298
|
+
|
|
299
|
+
const COMPACT_NO_TOOLS_TRAILER =
|
|
300
|
+
"\n\nREMINDER: Do NOT call any tools. Respond with plain text only — " +
|
|
301
|
+
"an <analysis> block followed by a <summary> block. " +
|
|
302
|
+
"Tool calls will be rejected and you will fail the task.";
|
|
214
303
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
304
|
+
/**
|
|
305
|
+
* Builds the compact instruction sent as the trailing user message on the
|
|
306
|
+
* fork path. Aligned with Claude Code's getCompactPrompt().
|
|
307
|
+
*/
|
|
308
|
+
export function getCompactPrompt(customInstructions?: string): string {
|
|
309
|
+
let prompt = COMPACT_NO_TOOLS_PREAMBLE + BASE_COMPACT_PROMPT;
|
|
218
310
|
|
|
219
|
-
|
|
220
|
-
|
|
311
|
+
if (customInstructions && customInstructions.trim() !== "") {
|
|
312
|
+
prompt += `\n\nAdditional Instructions:\n${customInstructions}`;
|
|
313
|
+
}
|
|
221
314
|
|
|
222
|
-
|
|
223
|
-
- Complete list of all user messages (non-tool content)
|
|
224
|
-
- Preserve exact wording where load-bearing
|
|
315
|
+
prompt += COMPACT_NO_TOOLS_TRAILER;
|
|
225
316
|
|
|
226
|
-
|
|
227
|
-
|
|
317
|
+
return prompt;
|
|
318
|
+
}
|
|
228
319
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
320
|
+
/**
|
|
321
|
+
* Formats the compact summary by stripping the <analysis> drafting scratchpad
|
|
322
|
+
* and extracting the <summary> section. Raw text passes through unchanged
|
|
323
|
+
* when no <summary> tag is present. Aligned with Claude Code's
|
|
324
|
+
* formatCompactSummary().
|
|
325
|
+
*/
|
|
326
|
+
export function formatCompactSummary(summary: string): string {
|
|
327
|
+
let formattedSummary = summary;
|
|
328
|
+
|
|
329
|
+
formattedSummary = formattedSummary.replace(
|
|
330
|
+
/<analysis>[\s\S]*?<\/analysis>/,
|
|
331
|
+
"",
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const summaryMatch = formattedSummary.match(/<summary>([\s\S]*?)<\/summary>/);
|
|
335
|
+
if (summaryMatch) {
|
|
336
|
+
const content = summaryMatch[1] || "";
|
|
337
|
+
formattedSummary = formattedSummary.replace(
|
|
338
|
+
/<summary>[\s\S]*?<\/summary>/,
|
|
339
|
+
`Summary:\n${content.trim()}`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
232
342
|
|
|
233
|
-
|
|
234
|
-
- Immediate next action needed
|
|
235
|
-
- Include verbatim quotes from recent conversation if relevant
|
|
343
|
+
formattedSummary = formattedSummary.replace(/\n\n+/g, "\n\n");
|
|
236
344
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
Wrap your summary in <summary></summary> tags.`;
|
|
345
|
+
return formattedSummary.trim();
|
|
346
|
+
}
|
|
240
347
|
|
|
241
348
|
export const WEB_CONTENT_SYSTEM_PROMPT = `You are a helpful assistant that extracts information from web content. The content is provided in Markdown format.`;
|
|
242
349
|
export const BTW_SYSTEM_PROMPT = `You are a helpful assistant. Answer the user's side question based on the conversation history.
|
|
@@ -24,7 +24,6 @@ import * as fs from "fs";
|
|
|
24
24
|
import * as path from "path";
|
|
25
25
|
|
|
26
26
|
import {
|
|
27
|
-
COMPACT_MESSAGES_SYSTEM_PROMPT,
|
|
28
27
|
WEB_CONTENT_SYSTEM_PROMPT,
|
|
29
28
|
BTW_SYSTEM_PROMPT,
|
|
30
29
|
type SystemPromptBlock,
|
|
@@ -169,6 +168,12 @@ export interface CallAgentOptions {
|
|
|
169
168
|
| "required"
|
|
170
169
|
| { type: "function"; function: { name: string } }; // Force tool selection
|
|
171
170
|
|
|
171
|
+
// Force SSE streaming independent of callback presence. Long-running
|
|
172
|
+
// non-interactive calls (e.g. the compaction fork) need streaming so a
|
|
173
|
+
// slow reasoning model isn't killed by a gateway idle timeout before the
|
|
174
|
+
// first byte arrives.
|
|
175
|
+
stream?: boolean;
|
|
176
|
+
|
|
172
177
|
// NEW: Streaming callbacks
|
|
173
178
|
onContentUpdate?: (content: string) => void;
|
|
174
179
|
onToolUpdate?: (toolCall: {
|
|
@@ -319,11 +324,9 @@ export async function callAgent(
|
|
|
319
324
|
});
|
|
320
325
|
|
|
321
326
|
// Determine if streaming is needed
|
|
322
|
-
const isStreaming =
|
|
323
|
-
|
|
324
|
-
onToolUpdate ||
|
|
325
|
-
onReasoningUpdate
|
|
326
|
-
);
|
|
327
|
+
const isStreaming =
|
|
328
|
+
options.stream === true ||
|
|
329
|
+
!!(onContentUpdate || onToolUpdate || onReasoningUpdate);
|
|
327
330
|
|
|
328
331
|
// Prepare API call parameters
|
|
329
332
|
createParams = {
|
|
@@ -787,128 +790,6 @@ async function processStreamingResponse(
|
|
|
787
790
|
return result;
|
|
788
791
|
}
|
|
789
792
|
|
|
790
|
-
export interface CompactMessagesOptions {
|
|
791
|
-
// Resolved configuration
|
|
792
|
-
gatewayConfig: GatewayConfig;
|
|
793
|
-
modelConfig: ModelConfig;
|
|
794
|
-
|
|
795
|
-
// Existing parameters
|
|
796
|
-
messages: ChatCompletionMessageParam[];
|
|
797
|
-
abortSignal?: AbortSignal;
|
|
798
|
-
model?: string;
|
|
799
|
-
customInstructions?: string;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
export interface CompactMessagesResult {
|
|
803
|
-
content: string;
|
|
804
|
-
usage?: {
|
|
805
|
-
prompt_tokens: number;
|
|
806
|
-
completion_tokens: number;
|
|
807
|
-
total_tokens: number;
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
export async function compactMessages(
|
|
812
|
-
options: CompactMessagesOptions,
|
|
813
|
-
): Promise<CompactMessagesResult> {
|
|
814
|
-
const { gatewayConfig, modelConfig, messages, abortSignal } = options;
|
|
815
|
-
|
|
816
|
-
// Validate model config at call time
|
|
817
|
-
validateModelConfig(modelConfig);
|
|
818
|
-
|
|
819
|
-
// Apply global 1 QPS rate limit
|
|
820
|
-
if (
|
|
821
|
-
process.env.NODE_ENV !== "test" ||
|
|
822
|
-
modelConfig.model === "rate-limit-test"
|
|
823
|
-
) {
|
|
824
|
-
await acquireSlot(abortSignal);
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
// Strip images from messages before compact API call to reduce token usage
|
|
828
|
-
const cleanedMessages = messages.map((msg) => {
|
|
829
|
-
// Handle user/assistant messages with array content
|
|
830
|
-
if (Array.isArray(msg.content)) {
|
|
831
|
-
const textParts = msg.content.filter(
|
|
832
|
-
(part) => part.type === "text",
|
|
833
|
-
) as import("openai/resources.js").ChatCompletionContentPartText[];
|
|
834
|
-
const text = textParts.map((p) => p.text).join("\n");
|
|
835
|
-
return { ...msg, content: text || "(empty message)" };
|
|
836
|
-
}
|
|
837
|
-
return msg;
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
// Create OpenAI client with injected configuration
|
|
841
|
-
const openai = new OpenAIClient({
|
|
842
|
-
apiKey: gatewayConfig.apiKey,
|
|
843
|
-
baseURL: gatewayConfig.baseURL,
|
|
844
|
-
defaultHeaders: gatewayConfig.defaultHeaders,
|
|
845
|
-
fetchOptions: gatewayConfig.fetchOptions,
|
|
846
|
-
fetch: gatewayConfig.fetch,
|
|
847
|
-
});
|
|
848
|
-
|
|
849
|
-
// When a fast model override is provided, use the fast model's options
|
|
850
|
-
// (if configured); otherwise fall back to the agent model's options.
|
|
851
|
-
const activeExtraParams = options.model
|
|
852
|
-
? modelConfig.fastModelOptions || {}
|
|
853
|
-
: modelConfig.options || {};
|
|
854
|
-
|
|
855
|
-
const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
|
|
856
|
-
temperature: 0.1,
|
|
857
|
-
max_tokens: 8192,
|
|
858
|
-
...activeExtraParams,
|
|
859
|
-
});
|
|
860
|
-
|
|
861
|
-
try {
|
|
862
|
-
const response = await openai.chat.completions.create(
|
|
863
|
-
{
|
|
864
|
-
...openaiModelConfig,
|
|
865
|
-
messages: [
|
|
866
|
-
{
|
|
867
|
-
role: "system",
|
|
868
|
-
content: COMPACT_MESSAGES_SYSTEM_PROMPT,
|
|
869
|
-
},
|
|
870
|
-
...cleanedMessages,
|
|
871
|
-
{
|
|
872
|
-
role: "user",
|
|
873
|
-
content: options.customInstructions
|
|
874
|
-
? `Please create a detailed summary of the conversation so far. Pay special attention to these instructions: ${options.customInstructions}`
|
|
875
|
-
: `Please create a detailed summary of the conversation so far.`,
|
|
876
|
-
},
|
|
877
|
-
],
|
|
878
|
-
},
|
|
879
|
-
{
|
|
880
|
-
signal: abortSignal,
|
|
881
|
-
},
|
|
882
|
-
);
|
|
883
|
-
|
|
884
|
-
const content = response.choices[0]?.message?.content?.trim();
|
|
885
|
-
if (!content) {
|
|
886
|
-
throw new Error(
|
|
887
|
-
"Failed to compact conversation history: Empty response from AI",
|
|
888
|
-
);
|
|
889
|
-
}
|
|
890
|
-
const usage = response.usage
|
|
891
|
-
? {
|
|
892
|
-
prompt_tokens: response.usage.prompt_tokens,
|
|
893
|
-
completion_tokens: response.usage.completion_tokens,
|
|
894
|
-
total_tokens: response.usage.total_tokens,
|
|
895
|
-
}
|
|
896
|
-
: undefined;
|
|
897
|
-
|
|
898
|
-
return {
|
|
899
|
-
content,
|
|
900
|
-
usage,
|
|
901
|
-
};
|
|
902
|
-
} catch (error) {
|
|
903
|
-
if ((error as Error).name === "AbortError") {
|
|
904
|
-
logger.info("Compaction request was aborted");
|
|
905
|
-
throw new Error("Compaction request was aborted");
|
|
906
|
-
}
|
|
907
|
-
logger.error("Failed to compact messages:", error);
|
|
908
|
-
throw error;
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
|
|
912
793
|
export interface ProcessWebContentOptions {
|
|
913
794
|
// Resolved configuration
|
|
914
795
|
gatewayConfig: GatewayConfig;
|
|
@@ -61,6 +61,10 @@ export class ConfigurationService {
|
|
|
61
61
|
private currentConfiguration: WaveConfiguration | null = null;
|
|
62
62
|
private options: AgentOptions = {};
|
|
63
63
|
private _configuredEnvKeys = new Set<string>();
|
|
64
|
+
// Per-session environment snapshot: settings.json `env` is stored here (NOT
|
|
65
|
+
// written to process.env) so multiple sessions in one `wave --stdio` process
|
|
66
|
+
// don't cross-pollute. Resolve methods read `this.envSnapshot ?? process.env`.
|
|
67
|
+
private envSnapshot: Record<string, string> = {};
|
|
64
68
|
|
|
65
69
|
/**
|
|
66
70
|
* Set agent options for configuration resolution
|
|
@@ -69,6 +73,28 @@ export class ConfigurationService {
|
|
|
69
73
|
this.options = options;
|
|
70
74
|
}
|
|
71
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Returns a copy of the per-session environment snapshot (settings.json `env`).
|
|
78
|
+
* Priority over OS env; does NOT include OS env. For subprocess spawning use
|
|
79
|
+
* {@link getMergedEnv} instead.
|
|
80
|
+
*/
|
|
81
|
+
getEnvSnapshot(): Record<string, string> {
|
|
82
|
+
return { ...this.envSnapshot };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Returns OS env merged with the session snapshot (snapshot wins). Use this
|
|
87
|
+
* when spawning user-facing subprocesses (bash, hooks, bang, background, MCP)
|
|
88
|
+
* so they inherit both OS env and the session's settings env.
|
|
89
|
+
*/
|
|
90
|
+
getMergedEnv(): Record<string, string> {
|
|
91
|
+
return Object.fromEntries(
|
|
92
|
+
Object.entries({ ...process.env, ...this.envSnapshot }).filter(
|
|
93
|
+
([, v]) => v !== undefined,
|
|
94
|
+
),
|
|
95
|
+
) as Record<string, string>;
|
|
96
|
+
}
|
|
97
|
+
|
|
72
98
|
// Core loading operations
|
|
73
99
|
|
|
74
100
|
/**
|
|
@@ -384,16 +410,33 @@ export class ConfigurationService {
|
|
|
384
410
|
// Utility operations
|
|
385
411
|
|
|
386
412
|
/**
|
|
387
|
-
*
|
|
388
|
-
*
|
|
413
|
+
* Store environment variables from configuration into the per-session
|
|
414
|
+
* snapshot (NOT process.env). Settings `env` shadows OS env for this session
|
|
415
|
+
* only — multiple sessions in one stdio process stay isolated.
|
|
416
|
+
*
|
|
417
|
+
* Exception: `WAVE_SERVER_URL` is also mirrored to `process.env` because the
|
|
418
|
+
* process-level singletons (AuthService, remoteSettingsService background
|
|
419
|
+
* fetch) need to read it and don't hold a per-session snapshot. Same value
|
|
420
|
+
* across sessions ⇒ no cross-pollution. See docs/specs/core/agent-config.md.
|
|
389
421
|
*/
|
|
390
422
|
setEnvironmentVars(env: Record<string, string>): void {
|
|
391
423
|
for (const [key, value] of Object.entries(env)) {
|
|
392
|
-
if (
|
|
424
|
+
if (
|
|
425
|
+
process.env[key] !== undefined &&
|
|
426
|
+
!this._configuredEnvKeys.has(key) &&
|
|
427
|
+
process.env[key] !== value
|
|
428
|
+
) {
|
|
393
429
|
logger.warn(`Overriding environment variable: ${key}`);
|
|
394
430
|
}
|
|
395
|
-
|
|
431
|
+
this.envSnapshot[key] = value;
|
|
396
432
|
this._configuredEnvKeys.add(key);
|
|
433
|
+
// WAVE_SERVER_URL is consumed by process-level singletons (AuthService,
|
|
434
|
+
// remoteSettingsService) that can't see the per-session snapshot — mirror
|
|
435
|
+
// it to process.env so they read the settings value. Same value across
|
|
436
|
+
// sessions ⇒ no last-session-wins cross-pollution.
|
|
437
|
+
if (key === "WAVE_SERVER_URL") {
|
|
438
|
+
process.env[key] = value;
|
|
439
|
+
}
|
|
397
440
|
}
|
|
398
441
|
}
|
|
399
442
|
|
|
@@ -437,8 +480,11 @@ export class ConfigurationService {
|
|
|
437
480
|
fetchOptions?: ClientOptions["fetchOptions"],
|
|
438
481
|
fetch?: ClientOptions["fetch"],
|
|
439
482
|
): GatewayConfig {
|
|
440
|
-
// Check for SSO token first - if present and server URL is available, use SSO mode
|
|
441
|
-
// Server URL resolution: options > process.env > default
|
|
483
|
+
// Check for SSO token first - if present and server URL is available, use SSO mode.
|
|
484
|
+
// Server URL resolution: options.serverUrl > process.env.WAVE_SERVER_URL > default.
|
|
485
|
+
// settings.json `env` WAVE_SERVER_URL is mirrored to process.env by
|
|
486
|
+
// setEnvironmentVars (so process-level singletons AuthService / remoteSettings
|
|
487
|
+
// can read it), then read here. See docs/specs/core/agent-config.md.
|
|
442
488
|
const ssoToken = this.readSSOToken();
|
|
443
489
|
const serverUrl =
|
|
444
490
|
this.options.serverUrl ||
|
|
@@ -469,7 +515,8 @@ export class ConfigurationService {
|
|
|
469
515
|
} else if (this.options.apiKey !== undefined) {
|
|
470
516
|
resolvedApiKey = this.options.apiKey;
|
|
471
517
|
} else {
|
|
472
|
-
resolvedApiKey =
|
|
518
|
+
resolvedApiKey =
|
|
519
|
+
this.envSnapshot.WAVE_API_KEY ?? process.env.WAVE_API_KEY;
|
|
473
520
|
}
|
|
474
521
|
|
|
475
522
|
// Resolve base URL: override > options > env (settings.json) > process.env
|
|
@@ -480,15 +527,18 @@ export class ConfigurationService {
|
|
|
480
527
|
} else if (this.options.baseURL !== undefined) {
|
|
481
528
|
resolvedBaseURL = this.options.baseURL;
|
|
482
529
|
} else {
|
|
483
|
-
resolvedBaseURL =
|
|
530
|
+
resolvedBaseURL =
|
|
531
|
+
this.envSnapshot.WAVE_BASE_URL ?? process.env.WAVE_BASE_URL;
|
|
484
532
|
}
|
|
485
533
|
|
|
486
534
|
// Fallback to process.env if still not resolved (for dynamic updates in tests)
|
|
487
535
|
if (resolvedApiKey === undefined) {
|
|
488
|
-
resolvedApiKey =
|
|
536
|
+
resolvedApiKey =
|
|
537
|
+
this.envSnapshot.WAVE_API_KEY ?? process.env.WAVE_API_KEY;
|
|
489
538
|
}
|
|
490
539
|
if (!resolvedBaseURL) {
|
|
491
|
-
resolvedBaseURL =
|
|
540
|
+
resolvedBaseURL =
|
|
541
|
+
this.envSnapshot.WAVE_BASE_URL ?? process.env.WAVE_BASE_URL;
|
|
492
542
|
}
|
|
493
543
|
|
|
494
544
|
// Treat empty string as not provided
|
|
@@ -497,7 +547,10 @@ export class ConfigurationService {
|
|
|
497
547
|
}
|
|
498
548
|
|
|
499
549
|
// Resolve custom headers from environment: env (settings.json) > process.env
|
|
500
|
-
const envCustomHeaders =
|
|
550
|
+
const envCustomHeaders =
|
|
551
|
+
this.envSnapshot.WAVE_CUSTOM_HEADERS ??
|
|
552
|
+
process.env.WAVE_CUSTOM_HEADERS ??
|
|
553
|
+
"";
|
|
501
554
|
const parsedEnvHeaders = parseCustomHeaders(envCustomHeaders);
|
|
502
555
|
|
|
503
556
|
// Merge headers: env headers < options < override
|
|
@@ -539,11 +592,13 @@ export class ConfigurationService {
|
|
|
539
592
|
model ||
|
|
540
593
|
this.options.model ||
|
|
541
594
|
this.currentConfiguration?.model ||
|
|
542
|
-
process.env.WAVE_MODEL;
|
|
595
|
+
(this.envSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
|
|
543
596
|
|
|
544
597
|
// Resolve fast model: override > options > process.env (includes settings.json env)
|
|
545
598
|
const resolvedFastModel =
|
|
546
|
-
fastModel ||
|
|
599
|
+
fastModel ||
|
|
600
|
+
this.options.fastModel ||
|
|
601
|
+
(this.envSnapshot.WAVE_FAST_MODEL ?? process.env.WAVE_FAST_MODEL);
|
|
547
602
|
|
|
548
603
|
// Resolve max output tokens
|
|
549
604
|
const resolvedMaxTokens = this.resolveMaxOutputTokens(maxTokens);
|
|
@@ -596,8 +651,10 @@ export class ConfigurationService {
|
|
|
596
651
|
return this.options.maxInputTokens;
|
|
597
652
|
}
|
|
598
653
|
|
|
599
|
-
// Try env (settings.json) first, then process.env
|
|
600
|
-
const envMaxInputTokens =
|
|
654
|
+
// Try env (settings.json snapshot) first, then process.env
|
|
655
|
+
const envMaxInputTokens =
|
|
656
|
+
this.envSnapshot.WAVE_MAX_INPUT_TOKENS ??
|
|
657
|
+
process.env.WAVE_MAX_INPUT_TOKENS;
|
|
601
658
|
if (envMaxInputTokens) {
|
|
602
659
|
const parsed = parseInt(envMaxInputTokens, 10);
|
|
603
660
|
if (!isNaN(parsed)) {
|
|
@@ -645,9 +702,9 @@ export class ConfigurationService {
|
|
|
645
702
|
return this.currentConfiguration.autoMemoryEnabled;
|
|
646
703
|
}
|
|
647
704
|
|
|
648
|
-
// 2. WAVE_DISABLE_AUTO_MEMORY environment variable
|
|
705
|
+
// 2. WAVE_DISABLE_AUTO_MEMORY environment variable (settings snapshot > OS env)
|
|
649
706
|
const disableAutoMemory =
|
|
650
|
-
|
|
707
|
+
this.envSnapshot.WAVE_DISABLE_AUTO_MEMORY ??
|
|
651
708
|
process.env.WAVE_DISABLE_AUTO_MEMORY;
|
|
652
709
|
if (disableAutoMemory === "1" || disableAutoMemory === "true") {
|
|
653
710
|
return false;
|
|
@@ -681,9 +738,9 @@ export class ConfigurationService {
|
|
|
681
738
|
return this.currentConfiguration.autoMemoryFrequency;
|
|
682
739
|
}
|
|
683
740
|
|
|
684
|
-
// 2. WAVE_AUTO_MEMORY_FREQUENCY environment variable
|
|
741
|
+
// 2. WAVE_AUTO_MEMORY_FREQUENCY environment variable (settings snapshot > OS env)
|
|
685
742
|
const envFrequency =
|
|
686
|
-
|
|
743
|
+
this.envSnapshot.WAVE_AUTO_MEMORY_FREQUENCY ??
|
|
687
744
|
process.env.WAVE_AUTO_MEMORY_FREQUENCY;
|
|
688
745
|
if (envFrequency) {
|
|
689
746
|
const parsed = parseInt(envFrequency, 10);
|
|
@@ -713,8 +770,10 @@ export class ConfigurationService {
|
|
|
713
770
|
return this.options.maxTokens;
|
|
714
771
|
}
|
|
715
772
|
|
|
716
|
-
// Try env (settings.json) first, then process.env
|
|
717
|
-
const envMaxOutputTokens =
|
|
773
|
+
// Try env (settings.json snapshot) first, then process.env
|
|
774
|
+
const envMaxOutputTokens =
|
|
775
|
+
this.envSnapshot.WAVE_MAX_OUTPUT_TOKENS ??
|
|
776
|
+
process.env.WAVE_MAX_OUTPUT_TOKENS;
|
|
718
777
|
if (envMaxOutputTokens) {
|
|
719
778
|
const parsed = parseInt(envMaxOutputTokens, 10);
|
|
720
779
|
if (!isNaN(parsed) && parsed > 0) {
|
|
@@ -761,8 +820,10 @@ export class ConfigurationService {
|
|
|
761
820
|
getConfiguredModels(): string[] {
|
|
762
821
|
const models = new Set<string>();
|
|
763
822
|
|
|
764
|
-
// Add current model from options or environment
|
|
765
|
-
const currentModel =
|
|
823
|
+
// Add current model from options or environment (settings snapshot > OS env)
|
|
824
|
+
const currentModel =
|
|
825
|
+
this.options.model ||
|
|
826
|
+
(this.envSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
|
|
766
827
|
if (currentModel) {
|
|
767
828
|
models.add(currentModel);
|
|
768
829
|
}
|
|
@@ -126,13 +126,17 @@ export class InitializationService {
|
|
|
126
126
|
// Don't throw error to prevent app startup failure
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
//
|
|
130
|
-
// Must happen BEFORE loadMergedConfiguration so
|
|
129
|
+
// Load remote settings disk cache synchronously.
|
|
130
|
+
// Must happen BEFORE loadMergedConfiguration so cached managed settings
|
|
131
|
+
// (env, model, disallowedTools) are merged into the config. Settings `env`
|
|
132
|
+
// is stored in the per-session env snapshot (NOT process.env), except
|
|
133
|
+
// WAVE_SERVER_URL which is mirrored to process.env so the network fetch
|
|
134
|
+
// (below) can read it via authService.getServerUrl(); no race.
|
|
131
135
|
try {
|
|
132
136
|
const phaseStart = performance.now();
|
|
133
137
|
await remoteSettingsService.initialize();
|
|
134
138
|
logger?.debug(
|
|
135
|
-
`Initialization Phase [Remote Settings] took ${(performance.now() - phaseStart).toFixed(2)}ms`,
|
|
139
|
+
`Initialization Phase [Remote Settings Cache] took ${(performance.now() - phaseStart).toFixed(2)}ms`,
|
|
136
140
|
);
|
|
137
141
|
} catch (error) {
|
|
138
142
|
logger?.error("Failed to initialize remote settings:", error);
|
|
@@ -190,6 +194,13 @@ export class InitializationService {
|
|
|
190
194
|
// Don't throw error to prevent app startup failure
|
|
191
195
|
}
|
|
192
196
|
|
|
197
|
+
// Start remote settings network fetch + polling now that the config is
|
|
198
|
+
// merged. Settings `env` WAVE_SERVER_URL was mirrored to process.env by
|
|
199
|
+
// loadMergedConfiguration → setEnvironmentVars, so the fetch reads it via
|
|
200
|
+
// authService.getServerUrl(); fire-and-forget, failures fall back to the
|
|
201
|
+
// cached/merged settings.
|
|
202
|
+
remoteSettingsService.startBackgroundFetch();
|
|
203
|
+
|
|
193
204
|
// Execute SessionStart hooks
|
|
194
205
|
try {
|
|
195
206
|
const phaseStart = performance.now();
|
|
@@ -237,7 +248,9 @@ export class InitializationService {
|
|
|
237
248
|
cwd: workdir,
|
|
238
249
|
worktreeName: agentOptions.worktreeName,
|
|
239
250
|
env: Object.fromEntries(
|
|
240
|
-
Object.entries(
|
|
251
|
+
Object.entries(configurationService.getMergedEnv()).filter(
|
|
252
|
+
(e) => e[1] !== undefined,
|
|
253
|
+
),
|
|
241
254
|
) as Record<string, string>,
|
|
242
255
|
});
|
|
243
256
|
|