wave-agent-sdk 0.18.3 → 0.18.5
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/skills/settings/ENV.md +1 -1
- package/builtin/skills/settings/MEMORY.md +76 -0
- package/builtin/skills/settings/SKILL.md +4 -4
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +0 -2
- package/dist/managers/MemoryRuleManager.d.ts +9 -0
- package/dist/managers/MemoryRuleManager.d.ts.map +1 -1
- package/dist/managers/MemoryRuleManager.js +18 -0
- package/dist/managers/aiManager.d.ts +1 -1
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +370 -345
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +22 -15
- package/dist/managers/messageManager.d.ts +32 -13
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +82 -76
- package/dist/prompts/index.d.ts +10 -4
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +21 -20
- package/dist/services/aiService.d.ts +2 -1
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +37 -10
- package/dist/services/memory.d.ts +1 -0
- package/dist/services/memory.d.ts.map +1 -1
- package/dist/services/memory.js +35 -17
- package/dist/tools/agentTool.d.ts.map +1 -1
- package/dist/tools/agentTool.js +24 -2
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +17 -10
- package/dist/tools/editTool.d.ts.map +1 -1
- package/dist/tools/editTool.js +7 -6
- package/dist/tools/readTool.js +2 -2
- package/dist/tools/writeTool.js +2 -2
- package/dist/utils/constants.d.ts +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +1 -0
- package/dist/utils/gitUtils.js +6 -6
- package/dist/utils/openaiClient.d.ts.map +1 -1
- package/dist/utils/openaiClient.js +33 -32
- package/dist/utils/path.d.ts +7 -0
- package/dist/utils/path.d.ts.map +1 -1
- package/dist/utils/path.js +9 -0
- package/dist/utils/taskReminder.d.ts +2 -3
- package/dist/utils/taskReminder.d.ts.map +1 -1
- package/dist/utils/taskReminder.js +7 -18
- package/dist/utils/worktreeUtils.d.ts.map +1 -1
- package/dist/utils/worktreeUtils.js +19 -13
- package/package.json +1 -1
- package/scripts/install_ripgrep.js +21 -1
- package/src/agent.ts +0 -5
- package/src/managers/MemoryRuleManager.ts +23 -0
- package/src/managers/aiManager.ts +473 -434
- package/src/managers/mcpManager.ts +26 -15
- package/src/managers/messageManager.ts +99 -82
- package/src/prompts/index.ts +33 -24
- package/src/services/aiService.ts +39 -12
- package/src/services/memory.ts +34 -16
- package/src/tools/agentTool.ts +24 -2
- package/src/tools/bashTool.ts +18 -9
- package/src/tools/editTool.ts +7 -9
- package/src/tools/readTool.ts +2 -2
- package/src/tools/writeTool.ts +2 -2
- package/src/utils/constants.ts +1 -1
- package/src/utils/containerSetup.ts +1 -0
- package/src/utils/gitUtils.ts +6 -6
- package/src/utils/openaiClient.ts +40 -31
- package/src/utils/path.ts +10 -0
- package/src/utils/taskReminder.ts +7 -19
- package/src/utils/worktreeUtils.ts +46 -28
- package/builtin/skills/settings/MEMORY_RULES.md +0 -60
- package/vendor/ripgrep/linux-aarch64/rg +0 -0
- package/vendor/ripgrep/macos-aarch64/rg +0 -0
- package/vendor/ripgrep/macos-x86_64/rg +0 -0
- package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
- package/vendor/ripgrep/windows-x86_64/rg.exe +0 -0
package/src/services/memory.ts
CHANGED
|
@@ -8,24 +8,26 @@ import { getGitCommonDir } from "../utils/gitUtils.js";
|
|
|
8
8
|
import { pathEncoder } from "../utils/pathEncoder.js";
|
|
9
9
|
|
|
10
10
|
export class MemoryService {
|
|
11
|
-
private _cachedProjectMemory: string =
|
|
12
|
-
private _cachedUserMemory: string =
|
|
11
|
+
private _cachedProjectMemory: string | null = null;
|
|
12
|
+
private _cachedUserMemory: string | null = null;
|
|
13
13
|
private _cachedCombinedMemory: string | null = null;
|
|
14
|
+
private _cachedAutoMemoryContent: string | null = null;
|
|
14
15
|
|
|
15
16
|
constructor(private container: Container) {}
|
|
16
17
|
|
|
17
18
|
public get cachedProjectMemory(): string {
|
|
18
|
-
return this._cachedProjectMemory;
|
|
19
|
+
return this._cachedProjectMemory ?? "";
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
public get cachedUserMemory(): string {
|
|
22
|
-
return this._cachedUserMemory;
|
|
23
|
+
return this._cachedUserMemory ?? "";
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
public clearCache(): void {
|
|
26
|
-
this._cachedProjectMemory =
|
|
27
|
-
this._cachedUserMemory =
|
|
27
|
+
this._cachedProjectMemory = null;
|
|
28
|
+
this._cachedUserMemory = null;
|
|
28
29
|
this._cachedCombinedMemory = null;
|
|
30
|
+
this._cachedAutoMemoryContent = null;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/**
|
|
@@ -76,15 +78,20 @@ export class MemoryService {
|
|
|
76
78
|
* Get the first 200 lines of MEMORY.md.
|
|
77
79
|
*/
|
|
78
80
|
async getAutoMemoryContent(workdir: string): Promise<string> {
|
|
81
|
+
if (this._cachedAutoMemoryContent !== null) {
|
|
82
|
+
return this._cachedAutoMemoryContent;
|
|
83
|
+
}
|
|
79
84
|
const memoryDir = this.getAutoMemoryDirectory(workdir);
|
|
80
85
|
const memoryFile = path.join(memoryDir, "MEMORY.md");
|
|
81
86
|
|
|
82
87
|
try {
|
|
83
88
|
const content = await fs.readFile(memoryFile, "utf-8");
|
|
84
89
|
const lines = content.split("\n").slice(0, 200);
|
|
85
|
-
|
|
90
|
+
this._cachedAutoMemoryContent = lines.join("\n");
|
|
91
|
+
return this._cachedAutoMemoryContent;
|
|
86
92
|
} catch (error) {
|
|
87
93
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
94
|
+
this._cachedAutoMemoryContent = "";
|
|
88
95
|
return "";
|
|
89
96
|
}
|
|
90
97
|
logger.error("Failed to read auto-memory content:", error);
|
|
@@ -123,6 +130,9 @@ export class MemoryService {
|
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
async getUserMemoryContent(): Promise<string> {
|
|
133
|
+
if (this._cachedUserMemory !== null) {
|
|
134
|
+
return this._cachedUserMemory;
|
|
135
|
+
}
|
|
126
136
|
try {
|
|
127
137
|
await this.ensureUserMemoryFile();
|
|
128
138
|
const content = await fs.readFile(USER_MEMORY_FILE, "utf-8");
|
|
@@ -145,13 +155,15 @@ export class MemoryService {
|
|
|
145
155
|
contentLength: claudeContent.length,
|
|
146
156
|
},
|
|
147
157
|
);
|
|
148
|
-
|
|
158
|
+
this._cachedUserMemory = claudeContent;
|
|
159
|
+
return this._cachedUserMemory;
|
|
149
160
|
} catch {
|
|
150
161
|
// CLAUDE.md doesn't exist or can't be read, return the default template
|
|
151
162
|
}
|
|
152
163
|
}
|
|
153
164
|
|
|
154
|
-
|
|
165
|
+
this._cachedUserMemory = content;
|
|
166
|
+
return this._cachedUserMemory;
|
|
155
167
|
} catch (error) {
|
|
156
168
|
logger.error("Failed to read user memory:", error);
|
|
157
169
|
return "";
|
|
@@ -159,6 +171,9 @@ export class MemoryService {
|
|
|
159
171
|
}
|
|
160
172
|
|
|
161
173
|
async readMemoryFile(workdir: string): Promise<string> {
|
|
174
|
+
if (this._cachedProjectMemory !== null) {
|
|
175
|
+
return this._cachedProjectMemory;
|
|
176
|
+
}
|
|
162
177
|
const memoryFilePath = path.join(workdir, "AGENTS.md");
|
|
163
178
|
try {
|
|
164
179
|
const content = await fs.readFile(memoryFilePath, "utf-8");
|
|
@@ -166,7 +181,8 @@ export class MemoryService {
|
|
|
166
181
|
memoryFilePath,
|
|
167
182
|
contentLength: content.length,
|
|
168
183
|
});
|
|
169
|
-
|
|
184
|
+
this._cachedProjectMemory = content;
|
|
185
|
+
return this._cachedProjectMemory;
|
|
170
186
|
} catch (error) {
|
|
171
187
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
172
188
|
// Fallback to CLAUDE.md
|
|
@@ -177,12 +193,14 @@ export class MemoryService {
|
|
|
177
193
|
memoryFilePath: claudeMemoryPath,
|
|
178
194
|
contentLength: content.length,
|
|
179
195
|
});
|
|
180
|
-
|
|
196
|
+
this._cachedProjectMemory = content;
|
|
197
|
+
return this._cachedProjectMemory;
|
|
181
198
|
} catch (claudeError) {
|
|
182
199
|
if ((claudeError as NodeJS.ErrnoException).code === "ENOENT") {
|
|
183
200
|
logger.debug("Neither AGENTS.md nor CLAUDE.md found", {
|
|
184
201
|
memoryFilePath,
|
|
185
202
|
});
|
|
203
|
+
this._cachedProjectMemory = "";
|
|
186
204
|
return "";
|
|
187
205
|
}
|
|
188
206
|
logger.error("Failed to read CLAUDE.md fallback", {
|
|
@@ -201,14 +219,14 @@ export class MemoryService {
|
|
|
201
219
|
if (this._cachedCombinedMemory !== null) {
|
|
202
220
|
return this._cachedCombinedMemory;
|
|
203
221
|
}
|
|
204
|
-
|
|
205
|
-
|
|
222
|
+
const projectMemory = await this.readMemoryFile(workdir);
|
|
223
|
+
const userMemory = await this.getUserMemoryContent();
|
|
206
224
|
|
|
207
225
|
let combined = "";
|
|
208
|
-
if (
|
|
209
|
-
if (
|
|
226
|
+
if (projectMemory.trim()) combined += projectMemory;
|
|
227
|
+
if (userMemory.trim()) {
|
|
210
228
|
if (combined) combined += "\n\n";
|
|
211
|
-
combined +=
|
|
229
|
+
combined += userMemory;
|
|
212
230
|
}
|
|
213
231
|
this._cachedCombinedMemory = combined;
|
|
214
232
|
return combined;
|
package/src/tools/agentTool.ts
CHANGED
|
@@ -266,10 +266,32 @@ When using the Agent tool, you must specify a subagent_type parameter to select
|
|
|
266
266
|
});
|
|
267
267
|
} catch (error) {
|
|
268
268
|
if (!isBackgrounded) {
|
|
269
|
+
// Extract content from the subagent's last assistant message.
|
|
270
|
+
// aiManager.sendAIMessage() catch block already added an error
|
|
271
|
+
// block via addErrorBlock(), so the error info is in the message
|
|
272
|
+
// history — just return it directly.
|
|
273
|
+
let content = "";
|
|
274
|
+
try {
|
|
275
|
+
const messages = instance.messageManager.getMessages();
|
|
276
|
+
const lastAssistant = messages
|
|
277
|
+
.filter((m) => m.role === "assistant")
|
|
278
|
+
.pop();
|
|
279
|
+
if (lastAssistant) {
|
|
280
|
+
content = lastAssistant.blocks
|
|
281
|
+
.filter((b) => b.type === "text" || b.type === "error")
|
|
282
|
+
.map((b) => (b as { content: string }).content)
|
|
283
|
+
.join("\n")
|
|
284
|
+
.trim();
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
// Ignore errors when extracting messages
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const errorMsg = `Agent delegation failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
269
291
|
resolve({
|
|
270
292
|
success: false,
|
|
271
|
-
content:
|
|
272
|
-
error:
|
|
293
|
+
content: content || errorMsg,
|
|
294
|
+
error: errorMsg,
|
|
273
295
|
shortResult: "Delegation error",
|
|
274
296
|
});
|
|
275
297
|
}
|
package/src/tools/bashTool.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as path from "path";
|
|
|
4
4
|
import * as os from "os";
|
|
5
5
|
import { logger } from "../utils/globalLogger.js";
|
|
6
6
|
import { resolveShellPath } from "../utils/shellResolver.js";
|
|
7
|
+
import { toPosixPath } from "../utils/path.js";
|
|
7
8
|
import { stripAnsiColors } from "../utils/stringUtils.js";
|
|
8
9
|
import { processToolResult } from "../utils/toolResultStorage.js";
|
|
9
10
|
import { BASH_MAX_OUTPUT_CHARS } from "../constants/toolLimits.js";
|
|
@@ -246,7 +247,8 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
246
247
|
os.tmpdir(),
|
|
247
248
|
`wave_cwd_${Date.now()}_${Math.random().toString(36).substring(2, 11)}.tmp`,
|
|
248
249
|
);
|
|
249
|
-
const
|
|
250
|
+
const tempCwdFileForBash = toPosixPath(tempCwdFile);
|
|
251
|
+
const wrappedCommand = `${command} && pwd -P >| ${tempCwdFileForBash}`;
|
|
250
252
|
|
|
251
253
|
const child: ChildProcess = spawn(wrappedCommand, {
|
|
252
254
|
shell: shellPath || true,
|
|
@@ -264,6 +266,17 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
264
266
|
let isBackgrounded = false;
|
|
265
267
|
let isFinished = false;
|
|
266
268
|
|
|
269
|
+
// Best-effort cleanup of the temp CWD file — used by abort/error/exit paths
|
|
270
|
+
const cleanupTempFile = () => {
|
|
271
|
+
try {
|
|
272
|
+
if (fs.existsSync(tempCwdFile)) {
|
|
273
|
+
fs.unlinkSync(tempCwdFile);
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
// ignore — best-effort cleanup
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
267
280
|
const updateRealtimeResults = () => {
|
|
268
281
|
if (isAborted || isBackgrounded || isFinished) return;
|
|
269
282
|
|
|
@@ -420,6 +433,8 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
420
433
|
}
|
|
421
434
|
}
|
|
422
435
|
|
|
436
|
+
cleanupTempFile();
|
|
437
|
+
|
|
423
438
|
const processedOutput = processToolResult(
|
|
424
439
|
outputBuffer + (errorBuffer ? "\n" + errorBuffer : ""),
|
|
425
440
|
BASH_MAX_OUTPUT_CHARS,
|
|
@@ -491,14 +506,7 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
491
506
|
);
|
|
492
507
|
newCwd = undefined;
|
|
493
508
|
} finally {
|
|
494
|
-
|
|
495
|
-
try {
|
|
496
|
-
if (fs.existsSync(tempCwdFile)) {
|
|
497
|
-
fs.unlinkSync(tempCwdFile);
|
|
498
|
-
}
|
|
499
|
-
} catch (fileError) {
|
|
500
|
-
logger.error("Failed to clean up temp CWD file:", fileError);
|
|
501
|
-
}
|
|
509
|
+
cleanupTempFile();
|
|
502
510
|
}
|
|
503
511
|
|
|
504
512
|
// If CWD changed, call the onCwdChange callback and add notification
|
|
@@ -560,6 +568,7 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
560
568
|
if (timeoutHandle) {
|
|
561
569
|
clearTimeout(timeoutHandle);
|
|
562
570
|
}
|
|
571
|
+
cleanupTempFile();
|
|
563
572
|
resolve({
|
|
564
573
|
success: false,
|
|
565
574
|
content: "",
|
package/src/tools/editTool.ts
CHANGED
|
@@ -109,14 +109,14 @@ Usage:
|
|
|
109
109
|
};
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
//
|
|
113
|
-
context.messageManager?.
|
|
112
|
+
// Trigger conditional rule loading for this file
|
|
113
|
+
context.messageManager?.triggerFileRead(filePath);
|
|
114
114
|
|
|
115
|
-
// Enforce read-before-edit: the file must have been read first
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
) {
|
|
115
|
+
// Enforce read-before-edit: the file must have been read or written first.
|
|
116
|
+
// readFileState is populated by Read, Write, and Edit tools — single source
|
|
117
|
+
// of truth, aligned with Claude Code's readFileState approach.
|
|
118
|
+
const resolvedPath = resolvePath(filePath, context.workdir);
|
|
119
|
+
if (!context.readFileState?.has(resolvedPath)) {
|
|
120
120
|
return {
|
|
121
121
|
success: false,
|
|
122
122
|
content: "",
|
|
@@ -125,8 +125,6 @@ Usage:
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
try {
|
|
128
|
-
const resolvedPath = resolvePath(filePath, context.workdir);
|
|
129
|
-
|
|
130
128
|
// Read file content
|
|
131
129
|
let originalContent: string;
|
|
132
130
|
try {
|
package/src/tools/readTool.ts
CHANGED
|
@@ -195,8 +195,8 @@ Usage:
|
|
|
195
195
|
};
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
//
|
|
199
|
-
context.messageManager?.
|
|
198
|
+
// Trigger conditional rule loading for this file
|
|
199
|
+
context.messageManager?.triggerFileRead(filePath);
|
|
200
200
|
|
|
201
201
|
// Check for binary document formats
|
|
202
202
|
if (isBinaryDocument(filePath)) {
|
package/src/tools/writeTool.ts
CHANGED
|
@@ -68,8 +68,8 @@ Usage:
|
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
//
|
|
72
|
-
context.messageManager?.
|
|
71
|
+
// Trigger conditional rule loading for this file
|
|
72
|
+
context.messageManager?.triggerFileRead(filePath);
|
|
73
73
|
|
|
74
74
|
try {
|
|
75
75
|
const resolvedPath = resolvePath(filePath, context.workdir);
|
package/src/utils/constants.ts
CHANGED
|
@@ -30,4 +30,4 @@ export const USER_MEMORY_FILE = path.join(DATA_DIRECTORY, "AGENTS.md");
|
|
|
30
30
|
* AI related constants
|
|
31
31
|
*/
|
|
32
32
|
export const DEFAULT_WAVE_MAX_INPUT_TOKENS = 200000; // Default token limit
|
|
33
|
-
export const DEFAULT_WAVE_MAX_OUTPUT_TOKENS =
|
|
33
|
+
export const DEFAULT_WAVE_MAX_OUTPUT_TOKENS = 32000; // Default output token limit (aligned with Claude Code)
|
package/src/utils/gitUtils.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import * as fsSync from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Check if a directory is a git repository
|
|
@@ -31,7 +31,7 @@ export function isGitRepository(dirPath: string): string {
|
|
|
31
31
|
*/
|
|
32
32
|
export function getGitRepoRoot(cwd: string): string {
|
|
33
33
|
try {
|
|
34
|
-
return
|
|
34
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
35
35
|
cwd,
|
|
36
36
|
encoding: "utf8",
|
|
37
37
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -48,7 +48,7 @@ export function getGitRepoRoot(cwd: string): string {
|
|
|
48
48
|
*/
|
|
49
49
|
export function getGitCommonDir(cwd: string): string {
|
|
50
50
|
try {
|
|
51
|
-
const commonDir =
|
|
51
|
+
const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
|
52
52
|
cwd,
|
|
53
53
|
encoding: "utf8",
|
|
54
54
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -66,7 +66,7 @@ export function getGitCommonDir(cwd: string): string {
|
|
|
66
66
|
*/
|
|
67
67
|
export function getGitMainRepoRoot(cwd: string): string {
|
|
68
68
|
try {
|
|
69
|
-
const output =
|
|
69
|
+
const output = execFileSync("git", ["worktree", "list", "--porcelain"], {
|
|
70
70
|
cwd,
|
|
71
71
|
encoding: "utf8",
|
|
72
72
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -229,7 +229,7 @@ export function getDefaultRemoteBranch(cwd: string): string {
|
|
|
229
229
|
*/
|
|
230
230
|
export function hasUncommittedChanges(cwd: string): boolean {
|
|
231
231
|
try {
|
|
232
|
-
const status =
|
|
232
|
+
const status = execFileSync("git", ["status", "--porcelain"], {
|
|
233
233
|
cwd,
|
|
234
234
|
encoding: "utf8",
|
|
235
235
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -249,7 +249,7 @@ export function hasUncommittedChanges(cwd: string): boolean {
|
|
|
249
249
|
export function hasNewCommits(cwd: string, baseBranch?: string): boolean {
|
|
250
250
|
try {
|
|
251
251
|
const range = baseBranch ? `${baseBranch}..HEAD` : "@{u}..HEAD";
|
|
252
|
-
const log =
|
|
252
|
+
const log = execFileSync("git", ["log", range, "--oneline"], {
|
|
253
253
|
cwd,
|
|
254
254
|
encoding: "utf8",
|
|
255
255
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -7,6 +7,28 @@ import {
|
|
|
7
7
|
import { GatewayConfig } from "../types/config.js";
|
|
8
8
|
import { logger } from "./globalLogger.js";
|
|
9
9
|
|
|
10
|
+
const MAX_RETRIES = 10;
|
|
11
|
+
const BASE_DELAY_MS = 500;
|
|
12
|
+
const MAX_DELAY_MS = 32000;
|
|
13
|
+
|
|
14
|
+
function getRetryDelay(
|
|
15
|
+
attempt: number,
|
|
16
|
+
retryAfterHeader: string | null,
|
|
17
|
+
): number {
|
|
18
|
+
if (retryAfterHeader) {
|
|
19
|
+
const seconds = parseInt(retryAfterHeader, 10);
|
|
20
|
+
if (!isNaN(seconds)) {
|
|
21
|
+
return seconds * 1000;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const baseDelay = Math.min(
|
|
25
|
+
BASE_DELAY_MS * Math.pow(2, attempt - 1),
|
|
26
|
+
MAX_DELAY_MS,
|
|
27
|
+
);
|
|
28
|
+
const jitter = Math.random() * 0.25 * baseDelay;
|
|
29
|
+
return baseDelay + jitter;
|
|
30
|
+
}
|
|
31
|
+
|
|
10
32
|
type CreateParams =
|
|
11
33
|
| ChatCompletionCreateParamsNonStreaming
|
|
12
34
|
| ChatCompletionCreateParamsStreaming;
|
|
@@ -88,12 +110,11 @@ export class OpenAIClient {
|
|
|
88
110
|
|
|
89
111
|
const fetchFn = (customFetch as typeof fetch) || fetch;
|
|
90
112
|
let lastError: (Error & { status?: number; body?: unknown }) | undefined;
|
|
91
|
-
|
|
92
|
-
const initialDelay = 2000;
|
|
113
|
+
let lastRetryAfter: string | null = null;
|
|
93
114
|
|
|
94
|
-
for (let attempt = 0; attempt <=
|
|
115
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
95
116
|
if (attempt > 0) {
|
|
96
|
-
const delay =
|
|
117
|
+
const delay = getRetryDelay(attempt, lastRetryAfter);
|
|
97
118
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
98
119
|
}
|
|
99
120
|
|
|
@@ -110,12 +131,13 @@ export class OpenAIClient {
|
|
|
110
131
|
if (e instanceof Error && e.name === "AbortError") {
|
|
111
132
|
throw e;
|
|
112
133
|
}
|
|
113
|
-
if (attempt <
|
|
134
|
+
if (attempt < MAX_RETRIES) {
|
|
114
135
|
logger.warn("OpenAI API network error, retrying...", {
|
|
115
136
|
attempt: attempt + 1,
|
|
116
137
|
error: e,
|
|
117
138
|
});
|
|
118
139
|
lastError = e as Error;
|
|
140
|
+
lastRetryAfter = null;
|
|
119
141
|
continue;
|
|
120
142
|
}
|
|
121
143
|
throw e;
|
|
@@ -144,45 +166,28 @@ export class OpenAIClient {
|
|
|
144
166
|
}
|
|
145
167
|
}
|
|
146
168
|
|
|
147
|
-
let
|
|
169
|
+
let responseText = "";
|
|
148
170
|
try {
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
errorBody = JSON.parse(text);
|
|
152
|
-
} catch (e) {
|
|
153
|
-
logger.error("Failed to parse error response body as JSON", {
|
|
154
|
-
error: e,
|
|
155
|
-
text,
|
|
156
|
-
});
|
|
157
|
-
errorBody = text;
|
|
158
|
-
}
|
|
171
|
+
responseText = await response.text();
|
|
159
172
|
} catch (e) {
|
|
160
173
|
logger.error("Failed to read error response text", { error: e });
|
|
161
|
-
errorBody = {};
|
|
162
174
|
}
|
|
163
175
|
|
|
164
176
|
const error = new Error(
|
|
165
|
-
|
|
166
|
-
errorBody !== null &&
|
|
167
|
-
"error" in errorBody &&
|
|
168
|
-
typeof (errorBody as { error: unknown }).error === "object" &&
|
|
169
|
-
(errorBody as { error: object }).error !== null &&
|
|
170
|
-
"message" in (errorBody as { error: { message: unknown } }).error
|
|
171
|
-
? String((errorBody as { error: { message: string } }).error.message)
|
|
172
|
-
: typeof errorBody === "string"
|
|
173
|
-
? errorBody
|
|
174
|
-
: response.statusText,
|
|
177
|
+
`HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}: ${responseText}`,
|
|
175
178
|
) as Error & { status?: number; body?: unknown };
|
|
176
179
|
error.status = response.status;
|
|
177
|
-
error.body =
|
|
180
|
+
error.body = responseText;
|
|
178
181
|
|
|
179
182
|
const retryableStatus =
|
|
180
183
|
response.status === 429 ||
|
|
181
184
|
(response.status >= 500 && response.status !== 501);
|
|
182
|
-
if (retryableStatus && attempt <
|
|
185
|
+
if (retryableStatus && attempt < MAX_RETRIES) {
|
|
186
|
+
lastRetryAfter = response.headers.get("retry-after");
|
|
183
187
|
logger.warn("OpenAI API error, retrying...", {
|
|
184
188
|
attempt: attempt + 1,
|
|
185
189
|
status: response.status,
|
|
190
|
+
retryAfter: lastRetryAfter,
|
|
186
191
|
});
|
|
187
192
|
lastError = error;
|
|
188
193
|
continue;
|
|
@@ -191,7 +196,7 @@ export class OpenAIClient {
|
|
|
191
196
|
logger.error("OpenAI API Error:", {
|
|
192
197
|
status: response.status,
|
|
193
198
|
statusText: response.statusText,
|
|
194
|
-
|
|
199
|
+
body: responseText,
|
|
195
200
|
});
|
|
196
201
|
throw error;
|
|
197
202
|
}
|
|
@@ -222,12 +227,16 @@ export class OpenAIClient {
|
|
|
222
227
|
|
|
223
228
|
const data = trimmedLine.slice(5).trim();
|
|
224
229
|
if (data === "[DONE]") return;
|
|
230
|
+
if (!data) continue; // Skip empty data lines (keepalive heartbeats)
|
|
225
231
|
|
|
226
232
|
try {
|
|
227
233
|
const json = JSON.parse(data);
|
|
228
234
|
yield json as ChatCompletionChunk;
|
|
229
235
|
} catch (e) {
|
|
230
|
-
logger.error("Failed to parse stream chunk", {
|
|
236
|
+
logger.error("Failed to parse stream chunk", {
|
|
237
|
+
error: e instanceof Error ? e.message : String(e),
|
|
238
|
+
data,
|
|
239
|
+
});
|
|
231
240
|
}
|
|
232
241
|
}
|
|
233
242
|
}
|
package/src/utils/path.ts
CHANGED
|
@@ -54,3 +54,13 @@ export function getDisplayPath(filePath: string, workdir: string): string {
|
|
|
54
54
|
}
|
|
55
55
|
return filePath;
|
|
56
56
|
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Convert backslashes to forward slashes for shell compatibility on Windows.
|
|
60
|
+
* On Windows, os.tmpdir() returns paths with backslashes (e.g., C:\Users\foo\Temp),
|
|
61
|
+
* which are treated as escape characters when interpolated into shell command strings.
|
|
62
|
+
* Returns the path as-is on non-Windows platforms.
|
|
63
|
+
*/
|
|
64
|
+
export function toPosixPath(p: string): string {
|
|
65
|
+
return process.platform === "win32" ? p.replace(/\\/g, "/") : p;
|
|
66
|
+
}
|
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
import type { Message } from "../types/messaging.js";
|
|
2
2
|
import type { Task } from "../types/tasks.js";
|
|
3
|
-
import type { ChatCompletionMessageParam } from "openai/resources.js";
|
|
4
3
|
|
|
5
4
|
export const TASK_REMINDER_CONFIG = {
|
|
6
5
|
TURNS_SINCE_WRITE: 10,
|
|
7
6
|
TURNS_BETWEEN_REMINDERS: 10,
|
|
8
7
|
};
|
|
9
8
|
|
|
10
|
-
const TASK_MANAGEMENT_TOOLS = new Set([
|
|
11
|
-
"TaskCreate",
|
|
12
|
-
"TaskUpdate",
|
|
13
|
-
"TaskList",
|
|
14
|
-
"TaskGet",
|
|
15
|
-
]);
|
|
9
|
+
const TASK_MANAGEMENT_TOOLS = new Set(["TaskCreate", "TaskUpdate"]);
|
|
16
10
|
const TASK_REMINDER_MARKER = "<!-- task-reminder -->";
|
|
17
11
|
|
|
18
12
|
function isQualifyingAssistantMessage(message: Message): boolean {
|
|
@@ -66,43 +60,37 @@ export function getTaskReminderTurnCounts(messages: Message[]): {
|
|
|
66
60
|
export function buildTaskReminderText(tasks: Task[]): string {
|
|
67
61
|
let text = `${TASK_REMINDER_MARKER}\n`;
|
|
68
62
|
text +=
|
|
69
|
-
"
|
|
63
|
+
"The task tools haven't been used recently. If you're working on tasks that would benefit from tracking progress, consider using TaskCreate to add new tasks and TaskUpdate to update task status (set to in_progress when starting, completed when done). Also consider cleaning up the task list if it has become stale. Only use these if relevant to the current work. This is just a gentle reminder - ignore if not applicable. Make sure that you never mention this reminder to the user";
|
|
70
64
|
|
|
71
65
|
if (tasks.length > 0) {
|
|
72
|
-
text += "\n";
|
|
66
|
+
text += "\n\nHere are the existing tasks:\n";
|
|
73
67
|
for (const task of tasks) {
|
|
74
68
|
text += `#${task.id} [${task.status}] ${task.subject}\n`;
|
|
75
69
|
}
|
|
76
|
-
} else {
|
|
77
|
-
text += "The task list is currently empty.\n";
|
|
78
70
|
}
|
|
79
71
|
|
|
80
72
|
return text;
|
|
81
73
|
}
|
|
82
74
|
|
|
83
75
|
export function maybeInjectTaskReminder(
|
|
84
|
-
messages: ChatCompletionMessageParam[],
|
|
85
76
|
turnCounts: {
|
|
86
77
|
turnsSinceLastTaskManagement: number;
|
|
87
78
|
turnsSinceLastReminder: number;
|
|
88
79
|
},
|
|
89
80
|
tasks: Task[],
|
|
90
|
-
):
|
|
81
|
+
): string | null {
|
|
91
82
|
if (
|
|
92
83
|
turnCounts.turnsSinceLastTaskManagement <
|
|
93
84
|
TASK_REMINDER_CONFIG.TURNS_SINCE_WRITE
|
|
94
85
|
) {
|
|
95
|
-
return;
|
|
86
|
+
return null;
|
|
96
87
|
}
|
|
97
88
|
if (
|
|
98
89
|
turnCounts.turnsSinceLastReminder <
|
|
99
90
|
TASK_REMINDER_CONFIG.TURNS_BETWEEN_REMINDERS
|
|
100
91
|
) {
|
|
101
|
-
return;
|
|
92
|
+
return null;
|
|
102
93
|
}
|
|
103
94
|
|
|
104
|
-
|
|
105
|
-
role: "user",
|
|
106
|
-
content: buildTaskReminderText(tasks),
|
|
107
|
-
} as ChatCompletionMessageParam);
|
|
95
|
+
return buildTaskReminderText(tasks);
|
|
108
96
|
}
|