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/dist/prompts/index.js
CHANGED
|
@@ -149,53 +149,141 @@ This is critical - your turn should only end with either using the ${ASK_USER_QU
|
|
|
149
149
|
NOTE: At any point in time through this workflow you should feel free to ask the user questions or clarifications using the ${ASK_USER_QUESTION_TOOL_NAME} tool. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins.`;
|
|
150
150
|
}
|
|
151
151
|
export const DEFAULT_SYSTEM_PROMPT = BASE_SYSTEM_PROMPT;
|
|
152
|
-
|
|
152
|
+
// Aggressive no-tools preamble, aligned with Claude Code's NO_TOOLS_PREAMBLE.
|
|
153
|
+
// The fork path inherits the main conversation's full tool set (required for
|
|
154
|
+
// cache-key match), so the instruction must be explicit about rejection
|
|
155
|
+
// consequences to prevent wasted turns.
|
|
156
|
+
const COMPACT_NO_TOOLS_PREAMBLE = `CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
|
|
153
157
|
|
|
154
|
-
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
-
|
|
158
|
-
- Check for accuracy and completeness — ensure nothing critical is missing
|
|
158
|
+
- Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
|
|
159
|
+
- You already have all the context you need in the conversation above.
|
|
160
|
+
- Tool calls will be REJECTED and will waste your only turn — you will fail the task.
|
|
161
|
+
- Your entire response must be plain text: an <analysis> block followed by a <summary> block.
|
|
159
162
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
-
|
|
171
|
-
-
|
|
172
|
-
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
- Errors
|
|
176
|
-
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
163
|
+
`;
|
|
164
|
+
// Aligned with Claude Code's DETAILED_ANALYSIS_INSTRUCTION_BASE. The
|
|
165
|
+
// <analysis> block is a drafting scratchpad that formatCompactSummary()
|
|
166
|
+
// strips before the summary reaches context.
|
|
167
|
+
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:
|
|
168
|
+
|
|
169
|
+
1. Chronologically analyze each message and section of the conversation. For each section thoroughly identify:
|
|
170
|
+
- The user's explicit requests and intents
|
|
171
|
+
- Your approach to addressing the user's requests
|
|
172
|
+
- Key decisions, technical concepts and code patterns
|
|
173
|
+
- Specific details like:
|
|
174
|
+
- file names
|
|
175
|
+
- full code snippets
|
|
176
|
+
- function signatures
|
|
177
|
+
- file edits
|
|
178
|
+
- Errors that you ran into and how you fixed them
|
|
179
|
+
- Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
|
|
180
|
+
2. Double-check for technical accuracy and completeness, addressing each required element thoroughly.`;
|
|
181
|
+
// Aligned with Claude Code's BASE_COMPACT_PROMPT (9 sections + example).
|
|
182
|
+
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.
|
|
183
|
+
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.
|
|
184
|
+
|
|
185
|
+
${COMPACT_DETAILED_ANALYSIS_INSTRUCTION}
|
|
186
|
+
|
|
187
|
+
Your summary should include the following sections:
|
|
188
|
+
|
|
189
|
+
1. Primary Request and Intent: Capture all of the user's explicit requests and intents in detail
|
|
190
|
+
2. Key Technical Concepts: List all important technical concepts, technologies, and frameworks discussed.
|
|
191
|
+
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.
|
|
192
|
+
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.
|
|
193
|
+
5. Problem Solving: Document problems solved and any ongoing troubleshooting efforts.
|
|
194
|
+
6. All user messages: List ALL user messages that are not tool results. These are critical for understanding the users' feedback and changing intent.
|
|
195
|
+
7. Pending Tasks: Outline any pending tasks that you have explicitly been asked to work on.
|
|
196
|
+
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.
|
|
197
|
+
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.
|
|
198
|
+
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.
|
|
199
|
+
|
|
200
|
+
Here's an example of how your output should be structured:
|
|
201
|
+
|
|
202
|
+
<example>
|
|
203
|
+
<analysis>
|
|
204
|
+
[Your thought process, ensuring all points are covered thoroughly and accurately]
|
|
205
|
+
</analysis>
|
|
206
|
+
|
|
207
|
+
<summary>
|
|
208
|
+
1. Primary Request and Intent:
|
|
209
|
+
[Detailed description]
|
|
210
|
+
|
|
211
|
+
2. Key Technical Concepts:
|
|
212
|
+
- [Concept 1]
|
|
213
|
+
- [Concept 2]
|
|
214
|
+
- [...]
|
|
215
|
+
|
|
216
|
+
3. Files and Code Sections:
|
|
217
|
+
- [File Name 1]
|
|
218
|
+
- [Summary of why this file is important]
|
|
219
|
+
- [Summary of the changes made to this file, if any]
|
|
220
|
+
- [Important Code Snippet]
|
|
221
|
+
- [File Name 2]
|
|
222
|
+
- [Important Code Snippet]
|
|
223
|
+
- [...]
|
|
224
|
+
|
|
225
|
+
4. Errors and fixes:
|
|
226
|
+
- [Detailed description of error 1]:
|
|
227
|
+
- [How you fixed the error]
|
|
228
|
+
- [User feedback on the error if any]
|
|
229
|
+
- [...]
|
|
230
|
+
|
|
231
|
+
5. Problem Solving:
|
|
232
|
+
[Description of solved problems and ongoing troubleshooting]
|
|
233
|
+
|
|
234
|
+
6. All user messages:
|
|
235
|
+
- [Detailed non tool use user message]
|
|
236
|
+
- [...]
|
|
237
|
+
|
|
238
|
+
7. Pending Tasks:
|
|
239
|
+
- [Task 1]
|
|
240
|
+
- [Task 2]
|
|
241
|
+
- [...]
|
|
242
|
+
|
|
243
|
+
8. Current Work:
|
|
244
|
+
[Precise description of current work]
|
|
245
|
+
|
|
246
|
+
9. Optional Next Step:
|
|
247
|
+
[Optional Next step to take]
|
|
248
|
+
|
|
249
|
+
</summary>
|
|
250
|
+
</example>
|
|
251
|
+
|
|
252
|
+
Please provide your summary based on the conversation so far, following this structure and ensuring precision and thoroughness in your response.
|
|
253
|
+
|
|
254
|
+
There may be additional summarization instructions provided in the included context. If so, remember to follow these instructions when creating the above summary.`;
|
|
255
|
+
const COMPACT_NO_TOOLS_TRAILER = "\n\nREMINDER: Do NOT call any tools. Respond with plain text only — " +
|
|
256
|
+
"an <analysis> block followed by a <summary> block. " +
|
|
257
|
+
"Tool calls will be rejected and you will fail the task.";
|
|
258
|
+
/**
|
|
259
|
+
* Builds the compact instruction sent as the trailing user message on the
|
|
260
|
+
* fork path. Aligned with Claude Code's getCompactPrompt().
|
|
261
|
+
*/
|
|
262
|
+
export function getCompactPrompt(customInstructions) {
|
|
263
|
+
let prompt = COMPACT_NO_TOOLS_PREAMBLE + BASE_COMPACT_PROMPT;
|
|
264
|
+
if (customInstructions && customInstructions.trim() !== "") {
|
|
265
|
+
prompt += `\n\nAdditional Instructions:\n${customInstructions}`;
|
|
266
|
+
}
|
|
267
|
+
prompt += COMPACT_NO_TOOLS_TRAILER;
|
|
268
|
+
return prompt;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Formats the compact summary by stripping the <analysis> drafting scratchpad
|
|
272
|
+
* and extracting the <summary> section. Raw text passes through unchanged
|
|
273
|
+
* when no <summary> tag is present. Aligned with Claude Code's
|
|
274
|
+
* formatCompactSummary().
|
|
275
|
+
*/
|
|
276
|
+
export function formatCompactSummary(summary) {
|
|
277
|
+
let formattedSummary = summary;
|
|
278
|
+
formattedSummary = formattedSummary.replace(/<analysis>[\s\S]*?<\/analysis>/, "");
|
|
279
|
+
const summaryMatch = formattedSummary.match(/<summary>([\s\S]*?)<\/summary>/);
|
|
280
|
+
if (summaryMatch) {
|
|
281
|
+
const content = summaryMatch[1] || "";
|
|
282
|
+
formattedSummary = formattedSummary.replace(/<summary>[\s\S]*?<\/summary>/, `Summary:\n${content.trim()}`);
|
|
283
|
+
}
|
|
284
|
+
formattedSummary = formattedSummary.replace(/\n\n+/g, "\n\n");
|
|
285
|
+
return formattedSummary.trim();
|
|
286
|
+
}
|
|
199
287
|
export const WEB_CONTENT_SYSTEM_PROMPT = `You are a helpful assistant that extracts information from web content. The content is provided in Markdown format.`;
|
|
200
288
|
export const BTW_SYSTEM_PROMPT = `You are a helpful assistant. Answer the user's side question based on the conversation history.
|
|
201
289
|
Do NOT say things like "Let me try...", "I'll now...", "Let me check...", or promise to take any action.
|
|
@@ -24,6 +24,7 @@ export interface CallAgentOptions {
|
|
|
24
24
|
name: string;
|
|
25
25
|
};
|
|
26
26
|
};
|
|
27
|
+
stream?: boolean;
|
|
27
28
|
onContentUpdate?: (content: string) => void;
|
|
28
29
|
onToolUpdate?: (toolCall: {
|
|
29
30
|
id: string;
|
|
@@ -44,23 +45,6 @@ export interface CallAgentResult {
|
|
|
44
45
|
additionalFields?: Record<string, unknown>;
|
|
45
46
|
}
|
|
46
47
|
export declare function callAgent(options: CallAgentOptions): Promise<CallAgentResult>;
|
|
47
|
-
export interface CompactMessagesOptions {
|
|
48
|
-
gatewayConfig: GatewayConfig;
|
|
49
|
-
modelConfig: ModelConfig;
|
|
50
|
-
messages: ChatCompletionMessageParam[];
|
|
51
|
-
abortSignal?: AbortSignal;
|
|
52
|
-
model?: string;
|
|
53
|
-
customInstructions?: string;
|
|
54
|
-
}
|
|
55
|
-
export interface CompactMessagesResult {
|
|
56
|
-
content: string;
|
|
57
|
-
usage?: {
|
|
58
|
-
prompt_tokens: number;
|
|
59
|
-
completion_tokens: number;
|
|
60
|
-
total_tokens: number;
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
export declare function compactMessages(options: CompactMessagesOptions): Promise<CompactMessagesResult>;
|
|
64
48
|
export interface ProcessWebContentOptions {
|
|
65
49
|
gatewayConfig: GatewayConfig;
|
|
66
50
|
modelConfig: ModelConfig;
|
|
@@ -7,7 +7,7 @@ import { supportsPromptCaching } from "../utils/modelCapabilities.js";
|
|
|
7
7
|
import * as os from "os";
|
|
8
8
|
import * as fs from "fs";
|
|
9
9
|
import * as path from "path";
|
|
10
|
-
import {
|
|
10
|
+
import { WEB_CONTENT_SYSTEM_PROMPT, BTW_SYSTEM_PROMPT, } from "../prompts/index.js";
|
|
11
11
|
import { GOAL_EVALUATION_SYSTEM_PROMPT } from "../constants/goalPrompts.js";
|
|
12
12
|
// Global rate limiter state for 1 QPS
|
|
13
13
|
let nextAllowedTime = 0;
|
|
@@ -149,9 +149,8 @@ export async function callAgent(options) {
|
|
|
149
149
|
...(modelConfig.options || {}),
|
|
150
150
|
});
|
|
151
151
|
// Determine if streaming is needed
|
|
152
|
-
const isStreaming =
|
|
153
|
-
onToolUpdate ||
|
|
154
|
-
onReasoningUpdate);
|
|
152
|
+
const isStreaming = options.stream === true ||
|
|
153
|
+
!!(onContentUpdate || onToolUpdate || onReasoningUpdate);
|
|
155
154
|
// Prepare API call parameters
|
|
156
155
|
createParams = {
|
|
157
156
|
...openaiModelConfig,
|
|
@@ -488,87 +487,6 @@ async function processStreamingResponse(stream, onContentUpdate, onToolUpdate, o
|
|
|
488
487
|
}
|
|
489
488
|
return result;
|
|
490
489
|
}
|
|
491
|
-
export async function compactMessages(options) {
|
|
492
|
-
const { gatewayConfig, modelConfig, messages, abortSignal } = options;
|
|
493
|
-
// Validate model config at call time
|
|
494
|
-
validateModelConfig(modelConfig);
|
|
495
|
-
// Apply global 1 QPS rate limit
|
|
496
|
-
if (process.env.NODE_ENV !== "test" ||
|
|
497
|
-
modelConfig.model === "rate-limit-test") {
|
|
498
|
-
await acquireSlot(abortSignal);
|
|
499
|
-
}
|
|
500
|
-
// Strip images from messages before compact API call to reduce token usage
|
|
501
|
-
const cleanedMessages = messages.map((msg) => {
|
|
502
|
-
// Handle user/assistant messages with array content
|
|
503
|
-
if (Array.isArray(msg.content)) {
|
|
504
|
-
const textParts = msg.content.filter((part) => part.type === "text");
|
|
505
|
-
const text = textParts.map((p) => p.text).join("\n");
|
|
506
|
-
return { ...msg, content: text || "(empty message)" };
|
|
507
|
-
}
|
|
508
|
-
return msg;
|
|
509
|
-
});
|
|
510
|
-
// Create OpenAI client with injected configuration
|
|
511
|
-
const openai = new OpenAIClient({
|
|
512
|
-
apiKey: gatewayConfig.apiKey,
|
|
513
|
-
baseURL: gatewayConfig.baseURL,
|
|
514
|
-
defaultHeaders: gatewayConfig.defaultHeaders,
|
|
515
|
-
fetchOptions: gatewayConfig.fetchOptions,
|
|
516
|
-
fetch: gatewayConfig.fetch,
|
|
517
|
-
});
|
|
518
|
-
// When a fast model override is provided, use the fast model's options
|
|
519
|
-
// (if configured); otherwise fall back to the agent model's options.
|
|
520
|
-
const activeExtraParams = options.model
|
|
521
|
-
? modelConfig.fastModelOptions || {}
|
|
522
|
-
: modelConfig.options || {};
|
|
523
|
-
const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
|
|
524
|
-
temperature: 0.1,
|
|
525
|
-
max_tokens: 8192,
|
|
526
|
-
...activeExtraParams,
|
|
527
|
-
});
|
|
528
|
-
try {
|
|
529
|
-
const response = await openai.chat.completions.create({
|
|
530
|
-
...openaiModelConfig,
|
|
531
|
-
messages: [
|
|
532
|
-
{
|
|
533
|
-
role: "system",
|
|
534
|
-
content: COMPACT_MESSAGES_SYSTEM_PROMPT,
|
|
535
|
-
},
|
|
536
|
-
...cleanedMessages,
|
|
537
|
-
{
|
|
538
|
-
role: "user",
|
|
539
|
-
content: options.customInstructions
|
|
540
|
-
? `Please create a detailed summary of the conversation so far. Pay special attention to these instructions: ${options.customInstructions}`
|
|
541
|
-
: `Please create a detailed summary of the conversation so far.`,
|
|
542
|
-
},
|
|
543
|
-
],
|
|
544
|
-
}, {
|
|
545
|
-
signal: abortSignal,
|
|
546
|
-
});
|
|
547
|
-
const content = response.choices[0]?.message?.content?.trim();
|
|
548
|
-
if (!content) {
|
|
549
|
-
throw new Error("Failed to compact conversation history: Empty response from AI");
|
|
550
|
-
}
|
|
551
|
-
const usage = response.usage
|
|
552
|
-
? {
|
|
553
|
-
prompt_tokens: response.usage.prompt_tokens,
|
|
554
|
-
completion_tokens: response.usage.completion_tokens,
|
|
555
|
-
total_tokens: response.usage.total_tokens,
|
|
556
|
-
}
|
|
557
|
-
: undefined;
|
|
558
|
-
return {
|
|
559
|
-
content,
|
|
560
|
-
usage,
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
catch (error) {
|
|
564
|
-
if (error.name === "AbortError") {
|
|
565
|
-
logger.info("Compaction request was aborted");
|
|
566
|
-
throw new Error("Compaction request was aborted");
|
|
567
|
-
}
|
|
568
|
-
logger.error("Failed to compact messages:", error);
|
|
569
|
-
throw error;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
490
|
export async function processWebContent(options) {
|
|
573
491
|
const { gatewayConfig, modelConfig, content, prompt, abortSignal } = options;
|
|
574
492
|
// Validate model config at call time
|
|
@@ -18,10 +18,23 @@ export declare class ConfigurationService {
|
|
|
18
18
|
private currentConfiguration;
|
|
19
19
|
private options;
|
|
20
20
|
private _configuredEnvKeys;
|
|
21
|
+
private envSnapshot;
|
|
21
22
|
/**
|
|
22
23
|
* Set agent options for configuration resolution
|
|
23
24
|
*/
|
|
24
25
|
setOptions(options: AgentOptions): void;
|
|
26
|
+
/**
|
|
27
|
+
* Returns a copy of the per-session environment snapshot (settings.json `env`).
|
|
28
|
+
* Priority over OS env; does NOT include OS env. For subprocess spawning use
|
|
29
|
+
* {@link getMergedEnv} instead.
|
|
30
|
+
*/
|
|
31
|
+
getEnvSnapshot(): Record<string, string>;
|
|
32
|
+
/**
|
|
33
|
+
* Returns OS env merged with the session snapshot (snapshot wins). Use this
|
|
34
|
+
* when spawning user-facing subprocesses (bash, hooks, bang, background, MCP)
|
|
35
|
+
* so they inherit both OS env and the session's settings env.
|
|
36
|
+
*/
|
|
37
|
+
getMergedEnv(): Record<string, string>;
|
|
25
38
|
/**
|
|
26
39
|
* Load and merge configuration with comprehensive validation
|
|
27
40
|
*/
|
|
@@ -35,8 +48,14 @@ export declare class ConfigurationService {
|
|
|
35
48
|
*/
|
|
36
49
|
validateConfigurationFile(filePath: string): ValidationResult;
|
|
37
50
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
51
|
+
* Store environment variables from configuration into the per-session
|
|
52
|
+
* snapshot (NOT process.env). Settings `env` shadows OS env for this session
|
|
53
|
+
* only — multiple sessions in one stdio process stay isolated.
|
|
54
|
+
*
|
|
55
|
+
* Exception: `WAVE_SERVER_URL` is also mirrored to `process.env` because the
|
|
56
|
+
* process-level singletons (AuthService, remoteSettingsService background
|
|
57
|
+
* fetch) need to read it and don't hold a per-session snapshot. Same value
|
|
58
|
+
* across sessions ⇒ no cross-pollution. See docs/specs/core/agent-config.md.
|
|
40
59
|
*/
|
|
41
60
|
setEnvironmentVars(env: Record<string, string>): void;
|
|
42
61
|
/**
|
|
@@ -28,6 +28,10 @@ export class ConfigurationService {
|
|
|
28
28
|
this.currentConfiguration = null;
|
|
29
29
|
this.options = {};
|
|
30
30
|
this._configuredEnvKeys = new Set();
|
|
31
|
+
// Per-session environment snapshot: settings.json `env` is stored here (NOT
|
|
32
|
+
// written to process.env) so multiple sessions in one `wave --stdio` process
|
|
33
|
+
// don't cross-pollute. Resolve methods read `this.envSnapshot ?? process.env`.
|
|
34
|
+
this.envSnapshot = {};
|
|
31
35
|
}
|
|
32
36
|
/**
|
|
33
37
|
* Set agent options for configuration resolution
|
|
@@ -35,6 +39,22 @@ export class ConfigurationService {
|
|
|
35
39
|
setOptions(options) {
|
|
36
40
|
this.options = options;
|
|
37
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Returns a copy of the per-session environment snapshot (settings.json `env`).
|
|
44
|
+
* Priority over OS env; does NOT include OS env. For subprocess spawning use
|
|
45
|
+
* {@link getMergedEnv} instead.
|
|
46
|
+
*/
|
|
47
|
+
getEnvSnapshot() {
|
|
48
|
+
return { ...this.envSnapshot };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Returns OS env merged with the session snapshot (snapshot wins). Use this
|
|
52
|
+
* when spawning user-facing subprocesses (bash, hooks, bang, background, MCP)
|
|
53
|
+
* so they inherit both OS env and the session's settings env.
|
|
54
|
+
*/
|
|
55
|
+
getMergedEnv() {
|
|
56
|
+
return Object.fromEntries(Object.entries({ ...process.env, ...this.envSnapshot }).filter(([, v]) => v !== undefined));
|
|
57
|
+
}
|
|
38
58
|
// Core loading operations
|
|
39
59
|
/**
|
|
40
60
|
* Load and merge configuration with comprehensive validation
|
|
@@ -292,16 +312,31 @@ export class ConfigurationService {
|
|
|
292
312
|
}
|
|
293
313
|
// Utility operations
|
|
294
314
|
/**
|
|
295
|
-
*
|
|
296
|
-
*
|
|
315
|
+
* Store environment variables from configuration into the per-session
|
|
316
|
+
* snapshot (NOT process.env). Settings `env` shadows OS env for this session
|
|
317
|
+
* only — multiple sessions in one stdio process stay isolated.
|
|
318
|
+
*
|
|
319
|
+
* Exception: `WAVE_SERVER_URL` is also mirrored to `process.env` because the
|
|
320
|
+
* process-level singletons (AuthService, remoteSettingsService background
|
|
321
|
+
* fetch) need to read it and don't hold a per-session snapshot. Same value
|
|
322
|
+
* across sessions ⇒ no cross-pollution. See docs/specs/core/agent-config.md.
|
|
297
323
|
*/
|
|
298
324
|
setEnvironmentVars(env) {
|
|
299
325
|
for (const [key, value] of Object.entries(env)) {
|
|
300
|
-
if (process.env[key] !== undefined &&
|
|
326
|
+
if (process.env[key] !== undefined &&
|
|
327
|
+
!this._configuredEnvKeys.has(key) &&
|
|
328
|
+
process.env[key] !== value) {
|
|
301
329
|
logger.warn(`Overriding environment variable: ${key}`);
|
|
302
330
|
}
|
|
303
|
-
|
|
331
|
+
this.envSnapshot[key] = value;
|
|
304
332
|
this._configuredEnvKeys.add(key);
|
|
333
|
+
// WAVE_SERVER_URL is consumed by process-level singletons (AuthService,
|
|
334
|
+
// remoteSettingsService) that can't see the per-session snapshot — mirror
|
|
335
|
+
// it to process.env so they read the settings value. Same value across
|
|
336
|
+
// sessions ⇒ no last-session-wins cross-pollution.
|
|
337
|
+
if (key === "WAVE_SERVER_URL") {
|
|
338
|
+
process.env[key] = value;
|
|
339
|
+
}
|
|
305
340
|
}
|
|
306
341
|
}
|
|
307
342
|
// =============================================================================
|
|
@@ -337,8 +372,11 @@ export class ConfigurationService {
|
|
|
337
372
|
* @returns Resolved model configuration (model/fastModel may be undefined if not yet configured)
|
|
338
373
|
*/
|
|
339
374
|
resolveGatewayConfig(apiKey, baseURL, defaultHeaders, fetchOptions, fetch) {
|
|
340
|
-
// Check for SSO token first - if present and server URL is available, use SSO mode
|
|
341
|
-
// Server URL resolution: options > process.env > default
|
|
375
|
+
// Check for SSO token first - if present and server URL is available, use SSO mode.
|
|
376
|
+
// Server URL resolution: options.serverUrl > process.env.WAVE_SERVER_URL > default.
|
|
377
|
+
// settings.json `env` WAVE_SERVER_URL is mirrored to process.env by
|
|
378
|
+
// setEnvironmentVars (so process-level singletons AuthService / remoteSettings
|
|
379
|
+
// can read it), then read here. See docs/specs/core/agent-config.md.
|
|
342
380
|
const ssoToken = this.readSSOToken();
|
|
343
381
|
const serverUrl = this.options.serverUrl ||
|
|
344
382
|
process.env.WAVE_SERVER_URL ||
|
|
@@ -366,7 +404,8 @@ export class ConfigurationService {
|
|
|
366
404
|
resolvedApiKey = this.options.apiKey;
|
|
367
405
|
}
|
|
368
406
|
else {
|
|
369
|
-
resolvedApiKey =
|
|
407
|
+
resolvedApiKey =
|
|
408
|
+
this.envSnapshot.WAVE_API_KEY ?? process.env.WAVE_API_KEY;
|
|
370
409
|
}
|
|
371
410
|
// Resolve base URL: override > options > env (settings.json) > process.env
|
|
372
411
|
// Note: Explicitly provided empty strings should be treated as invalid, not fall back to env
|
|
@@ -378,21 +417,26 @@ export class ConfigurationService {
|
|
|
378
417
|
resolvedBaseURL = this.options.baseURL;
|
|
379
418
|
}
|
|
380
419
|
else {
|
|
381
|
-
resolvedBaseURL =
|
|
420
|
+
resolvedBaseURL =
|
|
421
|
+
this.envSnapshot.WAVE_BASE_URL ?? process.env.WAVE_BASE_URL;
|
|
382
422
|
}
|
|
383
423
|
// Fallback to process.env if still not resolved (for dynamic updates in tests)
|
|
384
424
|
if (resolvedApiKey === undefined) {
|
|
385
|
-
resolvedApiKey =
|
|
425
|
+
resolvedApiKey =
|
|
426
|
+
this.envSnapshot.WAVE_API_KEY ?? process.env.WAVE_API_KEY;
|
|
386
427
|
}
|
|
387
428
|
if (!resolvedBaseURL) {
|
|
388
|
-
resolvedBaseURL =
|
|
429
|
+
resolvedBaseURL =
|
|
430
|
+
this.envSnapshot.WAVE_BASE_URL ?? process.env.WAVE_BASE_URL;
|
|
389
431
|
}
|
|
390
432
|
// Treat empty string as not provided
|
|
391
433
|
if (resolvedBaseURL?.trim() === "") {
|
|
392
434
|
resolvedBaseURL = undefined;
|
|
393
435
|
}
|
|
394
436
|
// Resolve custom headers from environment: env (settings.json) > process.env
|
|
395
|
-
const envCustomHeaders =
|
|
437
|
+
const envCustomHeaders = this.envSnapshot.WAVE_CUSTOM_HEADERS ??
|
|
438
|
+
process.env.WAVE_CUSTOM_HEADERS ??
|
|
439
|
+
"";
|
|
396
440
|
const parsedEnvHeaders = parseCustomHeaders(envCustomHeaders);
|
|
397
441
|
// Merge headers: env headers < options < override
|
|
398
442
|
const resolvedHeaders = {
|
|
@@ -424,9 +468,11 @@ export class ConfigurationService {
|
|
|
424
468
|
const resolvedAgentModel = model ||
|
|
425
469
|
this.options.model ||
|
|
426
470
|
this.currentConfiguration?.model ||
|
|
427
|
-
process.env.WAVE_MODEL;
|
|
471
|
+
(this.envSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
|
|
428
472
|
// Resolve fast model: override > options > process.env (includes settings.json env)
|
|
429
|
-
const resolvedFastModel = fastModel ||
|
|
473
|
+
const resolvedFastModel = fastModel ||
|
|
474
|
+
this.options.fastModel ||
|
|
475
|
+
(this.envSnapshot.WAVE_FAST_MODEL ?? process.env.WAVE_FAST_MODEL);
|
|
430
476
|
// Resolve max output tokens
|
|
431
477
|
const resolvedMaxTokens = this.resolveMaxOutputTokens(maxTokens);
|
|
432
478
|
const baseConfig = {
|
|
@@ -468,8 +514,9 @@ export class ConfigurationService {
|
|
|
468
514
|
if (this.options.maxInputTokens !== undefined) {
|
|
469
515
|
return this.options.maxInputTokens;
|
|
470
516
|
}
|
|
471
|
-
// Try env (settings.json) first, then process.env
|
|
472
|
-
const envMaxInputTokens =
|
|
517
|
+
// Try env (settings.json snapshot) first, then process.env
|
|
518
|
+
const envMaxInputTokens = this.envSnapshot.WAVE_MAX_INPUT_TOKENS ??
|
|
519
|
+
process.env.WAVE_MAX_INPUT_TOKENS;
|
|
473
520
|
if (envMaxInputTokens) {
|
|
474
521
|
const parsed = parseInt(envMaxInputTokens, 10);
|
|
475
522
|
if (!isNaN(parsed)) {
|
|
@@ -510,8 +557,8 @@ export class ConfigurationService {
|
|
|
510
557
|
if (this.currentConfiguration?.autoMemoryEnabled !== undefined) {
|
|
511
558
|
return this.currentConfiguration.autoMemoryEnabled;
|
|
512
559
|
}
|
|
513
|
-
// 2. WAVE_DISABLE_AUTO_MEMORY environment variable
|
|
514
|
-
const disableAutoMemory =
|
|
560
|
+
// 2. WAVE_DISABLE_AUTO_MEMORY environment variable (settings snapshot > OS env)
|
|
561
|
+
const disableAutoMemory = this.envSnapshot.WAVE_DISABLE_AUTO_MEMORY ??
|
|
515
562
|
process.env.WAVE_DISABLE_AUTO_MEMORY;
|
|
516
563
|
if (disableAutoMemory === "1" || disableAutoMemory === "true") {
|
|
517
564
|
return false;
|
|
@@ -541,8 +588,8 @@ export class ConfigurationService {
|
|
|
541
588
|
if (this.currentConfiguration?.autoMemoryFrequency !== undefined) {
|
|
542
589
|
return this.currentConfiguration.autoMemoryFrequency;
|
|
543
590
|
}
|
|
544
|
-
// 2. WAVE_AUTO_MEMORY_FREQUENCY environment variable
|
|
545
|
-
const envFrequency =
|
|
591
|
+
// 2. WAVE_AUTO_MEMORY_FREQUENCY environment variable (settings snapshot > OS env)
|
|
592
|
+
const envFrequency = this.envSnapshot.WAVE_AUTO_MEMORY_FREQUENCY ??
|
|
546
593
|
process.env.WAVE_AUTO_MEMORY_FREQUENCY;
|
|
547
594
|
if (envFrequency) {
|
|
548
595
|
const parsed = parseInt(envFrequency, 10);
|
|
@@ -568,8 +615,9 @@ export class ConfigurationService {
|
|
|
568
615
|
if (this.options.maxTokens !== undefined) {
|
|
569
616
|
return this.options.maxTokens;
|
|
570
617
|
}
|
|
571
|
-
// Try env (settings.json) first, then process.env
|
|
572
|
-
const envMaxOutputTokens =
|
|
618
|
+
// Try env (settings.json snapshot) first, then process.env
|
|
619
|
+
const envMaxOutputTokens = this.envSnapshot.WAVE_MAX_OUTPUT_TOKENS ??
|
|
620
|
+
process.env.WAVE_MAX_OUTPUT_TOKENS;
|
|
573
621
|
if (envMaxOutputTokens) {
|
|
574
622
|
const parsed = parseInt(envMaxOutputTokens, 10);
|
|
575
623
|
if (!isNaN(parsed) && parsed > 0) {
|
|
@@ -610,8 +658,9 @@ export class ConfigurationService {
|
|
|
610
658
|
*/
|
|
611
659
|
getConfiguredModels() {
|
|
612
660
|
const models = new Set();
|
|
613
|
-
// Add current model from options or environment
|
|
614
|
-
const currentModel = this.options.model ||
|
|
661
|
+
// Add current model from options or environment (settings snapshot > OS env)
|
|
662
|
+
const currentModel = this.options.model ||
|
|
663
|
+
(this.envSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
|
|
615
664
|
if (currentModel) {
|
|
616
665
|
models.add(currentModel);
|
|
617
666
|
}
|
|
@@ -44,12 +44,16 @@ export class InitializationService {
|
|
|
44
44
|
logger?.error("Failed to initialize MCP servers:", error);
|
|
45
45
|
// Don't throw error to prevent app startup failure
|
|
46
46
|
}
|
|
47
|
-
//
|
|
48
|
-
// Must happen BEFORE loadMergedConfiguration so
|
|
47
|
+
// Load remote settings disk cache synchronously.
|
|
48
|
+
// Must happen BEFORE loadMergedConfiguration so cached managed settings
|
|
49
|
+
// (env, model, disallowedTools) are merged into the config. Settings `env`
|
|
50
|
+
// is stored in the per-session env snapshot (NOT process.env), except
|
|
51
|
+
// WAVE_SERVER_URL which is mirrored to process.env so the network fetch
|
|
52
|
+
// (below) can read it via authService.getServerUrl(); no race.
|
|
49
53
|
try {
|
|
50
54
|
const phaseStart = performance.now();
|
|
51
55
|
await remoteSettingsService.initialize();
|
|
52
|
-
logger?.debug(`Initialization Phase [Remote Settings] took ${(performance.now() - phaseStart).toFixed(2)}ms`);
|
|
56
|
+
logger?.debug(`Initialization Phase [Remote Settings Cache] took ${(performance.now() - phaseStart).toFixed(2)}ms`);
|
|
53
57
|
}
|
|
54
58
|
catch (error) {
|
|
55
59
|
logger?.error("Failed to initialize remote settings:", error);
|
|
@@ -89,6 +93,12 @@ export class InitializationService {
|
|
|
89
93
|
logger?.error("Failed to initialize hooks system:", error);
|
|
90
94
|
// Don't throw error to prevent app startup failure
|
|
91
95
|
}
|
|
96
|
+
// Start remote settings network fetch + polling now that the config is
|
|
97
|
+
// merged. Settings `env` WAVE_SERVER_URL was mirrored to process.env by
|
|
98
|
+
// loadMergedConfiguration → setEnvironmentVars, so the fetch reads it via
|
|
99
|
+
// authService.getServerUrl(); fire-and-forget, failures fall back to the
|
|
100
|
+
// cached/merged settings.
|
|
101
|
+
remoteSettingsService.startBackgroundFetch();
|
|
92
102
|
// Execute SessionStart hooks
|
|
93
103
|
try {
|
|
94
104
|
const phaseStart = performance.now();
|
|
@@ -124,7 +134,7 @@ export class InitializationService {
|
|
|
124
134
|
transcriptPath: messageManager.getTranscriptPath(),
|
|
125
135
|
cwd: workdir,
|
|
126
136
|
worktreeName: agentOptions.worktreeName,
|
|
127
|
-
env: Object.fromEntries(Object.entries(
|
|
137
|
+
env: Object.fromEntries(Object.entries(configurationService.getMergedEnv()).filter((e) => e[1] !== undefined)),
|
|
128
138
|
});
|
|
129
139
|
// Process hook results
|
|
130
140
|
hookManager.processHookResults("WorktreeCreate", hookResults, messageManager);
|