wave-agent-sdk 0.17.9 → 0.17.11
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/dist/managers/aiManager.d.ts +11 -4
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +232 -200
- package/dist/managers/backgroundTaskManager.js +1 -1
- package/dist/managers/bangManager.js +1 -1
- package/dist/managers/hookManager.js +2 -4
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +1 -3
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +1 -8
- package/dist/managers/toolManager.d.ts +6 -0
- package/dist/managers/toolManager.d.ts.map +1 -1
- package/dist/managers/toolManager.js +17 -12
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +20 -12
- package/dist/services/authService.d.ts +0 -1
- package/dist/services/authService.d.ts.map +1 -1
- package/dist/services/authService.js +1 -29
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +13 -0
- package/dist/services/fileWatcher.d.ts +2 -0
- package/dist/services/fileWatcher.d.ts.map +1 -1
- package/dist/services/fileWatcher.js +12 -1
- package/dist/services/jsonlHandler.d.ts +1 -1
- package/dist/services/jsonlHandler.d.ts.map +1 -1
- package/dist/services/jsonlHandler.js +7 -22
- package/dist/services/session.d.ts +2 -9
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +16 -46
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +3 -1
- package/dist/tools/editTool.d.ts.map +1 -1
- package/dist/tools/editTool.js +28 -1
- package/dist/tools/enterPlanMode.d.ts.map +1 -1
- package/dist/tools/enterPlanMode.js +8 -0
- package/dist/tools/exitPlanMode.d.ts.map +1 -1
- package/dist/tools/exitPlanMode.js +8 -0
- package/dist/tools/readTool.d.ts.map +1 -1
- package/dist/tools/readTool.js +18 -10
- package/dist/tools/types.d.ts +8 -2
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/writeTool.d.ts.map +1 -1
- package/dist/tools/writeTool.js +14 -1
- package/dist/types/agent.d.ts +0 -2
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/utils/cacheControlUtils.d.ts +20 -15
- package/dist/utils/cacheControlUtils.d.ts.map +1 -1
- package/dist/utils/cacheControlUtils.js +69 -66
- 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 -6
- package/dist/utils/taskReminder.d.ts +17 -0
- package/dist/utils/taskReminder.d.ts.map +1 -0
- package/dist/utils/taskReminder.js +76 -0
- package/package.json +1 -1
- package/src/managers/aiManager.ts +326 -262
- package/src/managers/backgroundTaskManager.ts +1 -1
- package/src/managers/bangManager.ts +1 -1
- package/src/managers/hookManager.ts +6 -6
- package/src/managers/mcpManager.ts +2 -4
- package/src/managers/messageManager.ts +1 -9
- package/src/managers/toolManager.ts +18 -12
- package/src/services/aiService.ts +23 -12
- package/src/services/authService.ts +1 -31
- package/src/services/configurationService.ts +21 -0
- package/src/services/fileWatcher.ts +15 -1
- package/src/services/jsonlHandler.ts +7 -27
- package/src/services/session.ts +16 -57
- package/src/tools/bashTool.ts +3 -1
- package/src/tools/editTool.ts +31 -1
- package/src/tools/enterPlanMode.ts +9 -0
- package/src/tools/exitPlanMode.ts +10 -0
- package/src/tools/readTool.ts +20 -10
- package/src/tools/types.ts +15 -3
- package/src/tools/writeTool.ts +15 -1
- package/src/types/agent.ts +0 -2
- package/src/types/config.ts +1 -0
- package/src/utils/cacheControlUtils.ts +77 -95
- package/src/utils/constants.ts +1 -1
- package/src/utils/containerSetup.ts +3 -7
- package/src/utils/taskReminder.ts +108 -0
package/src/tools/readTool.ts
CHANGED
|
@@ -244,18 +244,26 @@ Usage:
|
|
|
244
244
|
|
|
245
245
|
const stats = await stat(actualFilePath);
|
|
246
246
|
|
|
247
|
-
// Deduplication
|
|
247
|
+
// Deduplication: only dedup if the exact same range was read before
|
|
248
248
|
if (context.readFileState) {
|
|
249
249
|
const state = context.readFileState.get(actualFilePath);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
250
|
+
// Only dedup entries from a prior Read with explicit offset (not Edit/Write entries)
|
|
251
|
+
if (
|
|
252
|
+
state &&
|
|
253
|
+
state.mtime === stats.mtime.getTime() &&
|
|
254
|
+
state.offset !== undefined
|
|
255
|
+
) {
|
|
256
|
+
const rangeMatch = state.offset === offset && state.limit === limit;
|
|
257
|
+
if (rangeMatch) {
|
|
258
|
+
return {
|
|
259
|
+
success: true,
|
|
260
|
+
content: `File ${filePath} has not changed since last read. The content from the earlier Read tool_result in this conversation is still current — refer to that instead of re-reading.`,
|
|
261
|
+
shortResult: "File unchanged",
|
|
262
|
+
metadata: {
|
|
263
|
+
type: "file_unchanged",
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
259
267
|
}
|
|
260
268
|
}
|
|
261
269
|
|
|
@@ -287,6 +295,8 @@ Usage:
|
|
|
287
295
|
context.readFileState.set(actualFilePath, {
|
|
288
296
|
mtime: stats.mtime.getTime(),
|
|
289
297
|
hash,
|
|
298
|
+
offset, // undefined for full reads
|
|
299
|
+
limit, // undefined for full reads
|
|
290
300
|
});
|
|
291
301
|
}
|
|
292
302
|
|
package/src/tools/types.ts
CHANGED
|
@@ -31,6 +31,12 @@ export interface ToolPlugin {
|
|
|
31
31
|
workdir?: string;
|
|
32
32
|
isSubagent?: boolean;
|
|
33
33
|
}) => string;
|
|
34
|
+
/**
|
|
35
|
+
* Whether this tool is safe to run in parallel with other tools.
|
|
36
|
+
* Default (undefined) = true (parallel). Set to false for tools that
|
|
37
|
+
* perform read-modify-write on shared resources (e.g. Edit, Write).
|
|
38
|
+
*/
|
|
39
|
+
isConcurrencySafe?: boolean;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
export interface ToolResult {
|
|
@@ -104,15 +110,21 @@ export interface ToolContext {
|
|
|
104
110
|
maxTokens: number;
|
|
105
111
|
};
|
|
106
112
|
/** State of files read in the current session for deduplication */
|
|
107
|
-
readFileState?: Map<
|
|
113
|
+
readFileState?: Map<
|
|
114
|
+
string,
|
|
115
|
+
{
|
|
116
|
+
mtime: number;
|
|
117
|
+
hash: string;
|
|
118
|
+
offset?: number; // undefined = full read or Edit/Write entry (never dedup)
|
|
119
|
+
limit?: number;
|
|
120
|
+
}
|
|
121
|
+
>;
|
|
108
122
|
/** Hook manager instance for executing hooks */
|
|
109
123
|
hookManager?: import("../managers/hookManager.js").HookManager;
|
|
110
124
|
/** Callback to notify when the current working directory changes */
|
|
111
125
|
onCwdChange?: (newCwd: string) => void;
|
|
112
126
|
/** Original working directory (before any cd changes) for CWD reset */
|
|
113
127
|
originalWorkdir?: string;
|
|
114
|
-
/** Merged environment variables (process.env + agent env) for child processes */
|
|
115
|
-
env?: Record<string, string>;
|
|
116
128
|
/** Workflow manager instance for workflow orchestration */
|
|
117
129
|
workflowManager?: import("../managers/workflowManager.js").WorkflowManager;
|
|
118
130
|
}
|
package/src/tools/writeTool.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
1
|
+
import { readFile, writeFile, mkdir, stat } from "fs/promises";
|
|
2
|
+
import { createHash } from "crypto";
|
|
2
3
|
import { dirname } from "path";
|
|
3
4
|
import { logger } from "../utils/globalLogger.js";
|
|
4
5
|
import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
|
|
@@ -10,6 +11,7 @@ import { READ_TOOL_NAME } from "../constants/tools.js";
|
|
|
10
11
|
*/
|
|
11
12
|
export const writeTool: ToolPlugin = {
|
|
12
13
|
name: "Write",
|
|
14
|
+
isConcurrencySafe: false,
|
|
13
15
|
config: {
|
|
14
16
|
type: "function",
|
|
15
17
|
function: {
|
|
@@ -164,6 +166,18 @@ Usage:
|
|
|
164
166
|
};
|
|
165
167
|
}
|
|
166
168
|
|
|
169
|
+
// Update readFileState so subsequent serialized edits pass staleness check
|
|
170
|
+
if (context.readFileState) {
|
|
171
|
+
const newStats = await stat(resolvedPath);
|
|
172
|
+
const hash = createHash("sha256").update(content).digest("hex");
|
|
173
|
+
context.readFileState.set(resolvedPath, {
|
|
174
|
+
mtime: newStats.mtime.getTime(),
|
|
175
|
+
hash,
|
|
176
|
+
offset: undefined,
|
|
177
|
+
limit: undefined,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
167
181
|
const shortResult = isExistingFile ? "File overwritten" : "File created";
|
|
168
182
|
|
|
169
183
|
const lines = content.split("\n").length;
|
package/src/types/agent.ts
CHANGED
|
@@ -97,8 +97,6 @@ export interface AgentOptions {
|
|
|
97
97
|
* File-based hooks (from config.json/.waverc.json) merge on top of these.
|
|
98
98
|
*/
|
|
99
99
|
hooks?: PartialHookConfiguration;
|
|
100
|
-
/** Per-agent environment variables, merged on top of process.env for bash, MCP, and hooks */
|
|
101
|
-
env?: Record<string, string>;
|
|
102
100
|
[key: string]: unknown;
|
|
103
101
|
}
|
|
104
102
|
|
package/src/types/config.ts
CHANGED
|
@@ -10,7 +10,6 @@ import type {
|
|
|
10
10
|
ChatCompletionMessageParam,
|
|
11
11
|
ChatCompletionContentPart,
|
|
12
12
|
ChatCompletionContentPartText,
|
|
13
|
-
ChatCompletionFunctionTool,
|
|
14
13
|
CompletionUsage,
|
|
15
14
|
} from "openai/resources";
|
|
16
15
|
import { logger } from "./globalLogger.js";
|
|
@@ -36,16 +35,6 @@ export interface ClaudeChatCompletionContentPartText
|
|
|
36
35
|
cache_control?: CacheControl;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
/**
|
|
40
|
-
* Extended tool definition with cache control support
|
|
41
|
-
*/
|
|
42
|
-
export interface ClaudeChatCompletionFunctionTool
|
|
43
|
-
extends ChatCompletionFunctionTool {
|
|
44
|
-
type: "function";
|
|
45
|
-
function: ChatCompletionFunctionTool["function"];
|
|
46
|
-
cache_control?: CacheControl;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
38
|
/**
|
|
50
39
|
* Extended prompt_tokens_details with cache_creation_input_tokens
|
|
51
40
|
* Some models (e.g. Gemini, DeepSeek) return this field inside prompt_tokens_details
|
|
@@ -229,68 +218,53 @@ export function addCacheControlToContent(
|
|
|
229
218
|
}
|
|
230
219
|
|
|
231
220
|
/**
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
221
|
+
* Counts the total number of content blocks across all messages.
|
|
222
|
+
* Each element in a message's content array counts as one block.
|
|
223
|
+
* String content counts as one block. Null/undefined content counts as zero
|
|
224
|
+
* (e.g. assistant messages with only tool_calls).
|
|
225
|
+
* @param messages - Array of chat messages
|
|
226
|
+
* @returns Total content block count
|
|
235
227
|
*/
|
|
236
|
-
export function
|
|
237
|
-
|
|
238
|
-
):
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
logger.warn("Invalid tool structure detected, skipping:", tool);
|
|
248
|
-
return false;
|
|
249
|
-
}
|
|
250
|
-
if (tool.type !== "function" || !tool.function) {
|
|
251
|
-
logger.warn(
|
|
252
|
-
"Tool is not a function type or missing function property:",
|
|
253
|
-
tool,
|
|
254
|
-
);
|
|
255
|
-
return false;
|
|
228
|
+
export function countContentBlocks(
|
|
229
|
+
messages: ChatCompletionMessageParam[],
|
|
230
|
+
): number {
|
|
231
|
+
let count = 0;
|
|
232
|
+
for (const message of messages) {
|
|
233
|
+
if (!message || typeof message !== "object") continue;
|
|
234
|
+
const content = message.content;
|
|
235
|
+
if (typeof content === "string") {
|
|
236
|
+
count += 1;
|
|
237
|
+
} else if (Array.isArray(content)) {
|
|
238
|
+
count += content.length;
|
|
256
239
|
}
|
|
257
|
-
return true;
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
if (validTools.length === 0) {
|
|
261
|
-
logger.warn("No valid tools found for cache control");
|
|
262
|
-
return [];
|
|
263
240
|
}
|
|
264
|
-
|
|
265
|
-
// Create a copy of the valid tools array
|
|
266
|
-
const result = validTools.map((tool) => ({
|
|
267
|
-
...tool,
|
|
268
|
-
})) as ClaudeChatCompletionFunctionTool[];
|
|
269
|
-
|
|
270
|
-
// Add cache control to the last tool only
|
|
271
|
-
const lastIndex = result.length - 1;
|
|
272
|
-
result[lastIndex] = {
|
|
273
|
-
...result[lastIndex],
|
|
274
|
-
cache_control: { type: "ephemeral" },
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
return result;
|
|
241
|
+
return count;
|
|
278
242
|
}
|
|
279
243
|
|
|
280
244
|
/**
|
|
281
|
-
* Transforms messages for
|
|
245
|
+
* Transforms messages for explicit cache control.
|
|
246
|
+
*
|
|
247
|
+
* Cache breakpoints (following Claude Code's "last message marker" strategy):
|
|
248
|
+
* 1. System message — always marked (stable prefix)
|
|
249
|
+
* 2. Last message — the last user/assistant message with content is marked.
|
|
250
|
+
* The API scans backward from this marker within 20 blocks. Since each
|
|
251
|
+
* turn adds ~2 blocks (< 20), the previous request's marker position
|
|
252
|
+
* always falls within the scan window, ensuring cache hits.
|
|
253
|
+
*
|
|
254
|
+
* Tools are marked separately via addCacheControlToLastTool (called by aiService).
|
|
255
|
+
*
|
|
282
256
|
* @param messages - Original OpenAI message array
|
|
283
257
|
* @param modelName - Model name for cache detection
|
|
284
258
|
* @returns Messages with cache control markers applied
|
|
285
259
|
*/
|
|
286
|
-
export function
|
|
260
|
+
export function transformMessagesForExplicitCache(
|
|
287
261
|
messages: ChatCompletionMessageParam[],
|
|
288
262
|
modelName: string,
|
|
289
263
|
): ChatCompletionMessageParam[] {
|
|
290
264
|
// Validate inputs
|
|
291
265
|
if (!messages || !Array.isArray(messages)) {
|
|
292
266
|
logger.warn(
|
|
293
|
-
"Invalid messages array provided to
|
|
267
|
+
"Invalid messages array provided to transformMessagesForExplicitCache",
|
|
294
268
|
);
|
|
295
269
|
return [];
|
|
296
270
|
}
|
|
@@ -307,60 +281,68 @@ export function transformMessagesForClaudeCache(
|
|
|
307
281
|
// Find first system message index
|
|
308
282
|
const firstSystemIndex = messages.findIndex((m) => m.role === "system");
|
|
309
283
|
|
|
310
|
-
//
|
|
311
|
-
|
|
284
|
+
// Determine which message indices should receive cache_control markers
|
|
285
|
+
const cacheIndices = new Set<number>();
|
|
286
|
+
|
|
287
|
+
// Marker 1: First system message (always — stable prefix)
|
|
288
|
+
if (firstSystemIndex !== -1) {
|
|
289
|
+
cacheIndices.add(firstSystemIndex);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Marker 2: Last message with content (user or assistant).
|
|
293
|
+
// This marker moves each turn (~2 blocks), but since the move is < 20 blocks,
|
|
294
|
+
// the previous marker position is always within the API's 20-block scan window.
|
|
295
|
+
// This ensures the entire conversation prefix is cached.
|
|
312
296
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
313
|
-
|
|
314
|
-
|
|
297
|
+
const msg = messages[i];
|
|
298
|
+
if (!msg || typeof msg !== "object") continue;
|
|
299
|
+
if (i === firstSystemIndex) continue;
|
|
300
|
+
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
|
301
|
+
|
|
302
|
+
const content = msg.content;
|
|
303
|
+
const hasContent =
|
|
304
|
+
(typeof content === "string" && content.length > 0) ||
|
|
305
|
+
(Array.isArray(content) && content.length > 0);
|
|
306
|
+
|
|
307
|
+
if (hasContent) {
|
|
308
|
+
cacheIndices.add(i);
|
|
315
309
|
break;
|
|
316
310
|
}
|
|
317
311
|
}
|
|
318
312
|
|
|
313
|
+
// Apply cache_control markers to selected messages
|
|
319
314
|
const result = messages.map((message, index) => {
|
|
320
315
|
// Validate message structure
|
|
321
316
|
if (!message || typeof message !== "object" || !message.role) {
|
|
322
317
|
logger.warn("Invalid message structure at index", index, ":", message);
|
|
323
|
-
return message;
|
|
318
|
+
return message;
|
|
324
319
|
}
|
|
325
320
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const content =
|
|
329
|
-
(message.content as string | ChatCompletionContentPart[]) || "";
|
|
330
|
-
// If content is already an array, check if it already has cache control
|
|
331
|
-
if (Array.isArray(content)) {
|
|
332
|
-
const hasCacheControl = content.some(
|
|
333
|
-
(part) =>
|
|
334
|
-
part.type === "text" &&
|
|
335
|
-
(part as ClaudeChatCompletionContentPartText).cache_control,
|
|
336
|
-
);
|
|
337
|
-
if (hasCacheControl) {
|
|
338
|
-
return message;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const transformedContent = addCacheControlToContent(content, true);
|
|
343
|
-
|
|
344
|
-
return {
|
|
345
|
-
...message,
|
|
346
|
-
content: transformedContent,
|
|
347
|
-
} as ChatCompletionMessageParam;
|
|
321
|
+
if (!cacheIndices.has(index)) {
|
|
322
|
+
return message;
|
|
348
323
|
}
|
|
349
324
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const content =
|
|
353
|
-
(message.content as string | ChatCompletionContentPart[]) || "";
|
|
354
|
-
const transformedContent = addCacheControlToContent(content, true);
|
|
325
|
+
const content =
|
|
326
|
+
(message.content as string | ChatCompletionContentPart[]) || "";
|
|
355
327
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
328
|
+
// Idempotency: skip if content already has cache_control (system message)
|
|
329
|
+
if (message.role === "system" && Array.isArray(content)) {
|
|
330
|
+
const hasCacheControl = content.some(
|
|
331
|
+
(part) =>
|
|
332
|
+
part.type === "text" &&
|
|
333
|
+
(part as ClaudeChatCompletionContentPartText).cache_control,
|
|
334
|
+
);
|
|
335
|
+
if (hasCacheControl) {
|
|
336
|
+
return message;
|
|
337
|
+
}
|
|
360
338
|
}
|
|
361
339
|
|
|
362
|
-
|
|
363
|
-
|
|
340
|
+
const transformedContent = addCacheControlToContent(content, true);
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
...message,
|
|
344
|
+
content: transformedContent,
|
|
345
|
+
} as ChatCompletionMessageParam;
|
|
364
346
|
});
|
|
365
347
|
|
|
366
348
|
return result;
|
package/src/utils/constants.ts
CHANGED
|
@@ -29,5 +29,5 @@ export const USER_MEMORY_FILE = path.join(DATA_DIRECTORY, "AGENTS.md");
|
|
|
29
29
|
/**
|
|
30
30
|
* AI related constants
|
|
31
31
|
*/
|
|
32
|
-
export const DEFAULT_WAVE_MAX_INPUT_TOKENS =
|
|
32
|
+
export const DEFAULT_WAVE_MAX_INPUT_TOKENS = 200000; // Default token limit
|
|
33
33
|
export const DEFAULT_WAVE_MAX_OUTPUT_TOKENS = 16384; // Default output token limit
|
|
@@ -83,12 +83,6 @@ export function setupAgentContainer(
|
|
|
83
83
|
container.register("AgentOptions", options);
|
|
84
84
|
container.register("Workdir", workdir);
|
|
85
85
|
|
|
86
|
-
const mergedEnv: Record<string, string> = {
|
|
87
|
-
...(process.env as Record<string, string>),
|
|
88
|
-
...(options.env || {}),
|
|
89
|
-
};
|
|
90
|
-
container.register("MergedEnv", mergedEnv);
|
|
91
|
-
|
|
92
86
|
if (options.worktreeName) {
|
|
93
87
|
container.register("WorktreeName", options.worktreeName);
|
|
94
88
|
container.register("MainRepoRoot", getGitMainRepoRoot(workdir));
|
|
@@ -227,7 +221,9 @@ export function setupAgentContainer(
|
|
|
227
221
|
toolName: context.toolName,
|
|
228
222
|
toolInput: context.toolInput,
|
|
229
223
|
planFilePath: permissionManager.getPlanFilePath(),
|
|
230
|
-
env:
|
|
224
|
+
env: Object.fromEntries(
|
|
225
|
+
Object.entries(process.env).filter((e) => e[1] !== undefined),
|
|
226
|
+
) as Record<string, string>,
|
|
231
227
|
});
|
|
232
228
|
|
|
233
229
|
if (results.length > 0) {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Message } from "../types/messaging.js";
|
|
2
|
+
import type { Task } from "../types/tasks.js";
|
|
3
|
+
import type { ChatCompletionMessageParam } from "openai/resources.js";
|
|
4
|
+
|
|
5
|
+
export const TASK_REMINDER_CONFIG = {
|
|
6
|
+
TURNS_SINCE_WRITE: 10,
|
|
7
|
+
TURNS_BETWEEN_REMINDERS: 10,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const TASK_MANAGEMENT_TOOLS = new Set([
|
|
11
|
+
"TaskCreate",
|
|
12
|
+
"TaskUpdate",
|
|
13
|
+
"TaskList",
|
|
14
|
+
"TaskGet",
|
|
15
|
+
]);
|
|
16
|
+
const TASK_REMINDER_MARKER = "<!-- task-reminder -->";
|
|
17
|
+
|
|
18
|
+
function isQualifyingAssistantMessage(message: Message): boolean {
|
|
19
|
+
if (message.role !== "assistant") return false;
|
|
20
|
+
if (message.isMeta) return false;
|
|
21
|
+
return message.blocks.some((block) => block.type !== "reasoning");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getTaskReminderTurnCounts(messages: Message[]): {
|
|
25
|
+
turnsSinceLastTaskManagement: number;
|
|
26
|
+
turnsSinceLastReminder: number;
|
|
27
|
+
} {
|
|
28
|
+
let assistantTurnCount = 0;
|
|
29
|
+
let turnsSinceLastTaskManagement: number | undefined;
|
|
30
|
+
let turnsSinceLastReminder: number | undefined;
|
|
31
|
+
|
|
32
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
33
|
+
const message = messages[i];
|
|
34
|
+
|
|
35
|
+
// Check blocks for task management tools and reminder markers
|
|
36
|
+
for (const block of message.blocks) {
|
|
37
|
+
if (
|
|
38
|
+
turnsSinceLastTaskManagement === undefined &&
|
|
39
|
+
block.type === "tool" &&
|
|
40
|
+
block.name &&
|
|
41
|
+
TASK_MANAGEMENT_TOOLS.has(block.name)
|
|
42
|
+
) {
|
|
43
|
+
turnsSinceLastTaskManagement = assistantTurnCount;
|
|
44
|
+
}
|
|
45
|
+
if (
|
|
46
|
+
turnsSinceLastReminder === undefined &&
|
|
47
|
+
block.type === "text" &&
|
|
48
|
+
block.content.includes(TASK_REMINDER_MARKER)
|
|
49
|
+
) {
|
|
50
|
+
turnsSinceLastReminder = assistantTurnCount;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (isQualifyingAssistantMessage(message)) {
|
|
55
|
+
assistantTurnCount++;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
turnsSinceLastTaskManagement:
|
|
61
|
+
turnsSinceLastTaskManagement ?? assistantTurnCount,
|
|
62
|
+
turnsSinceLastReminder: turnsSinceLastReminder ?? assistantTurnCount,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function buildTaskReminderText(tasks: Task[]): string {
|
|
67
|
+
let text = `${TASK_REMINDER_MARKER}\n`;
|
|
68
|
+
text +=
|
|
69
|
+
"Here is a gentle reminder about the task list. The user may not have explicitly asked for the following to be done, but you should check the task list to see if any tasks need attention.\n";
|
|
70
|
+
|
|
71
|
+
if (tasks.length > 0) {
|
|
72
|
+
text += "\n";
|
|
73
|
+
for (const task of tasks) {
|
|
74
|
+
text += `#${task.id} [${task.status}] ${task.subject}\n`;
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
text += "The task list is currently empty.\n";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return text;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function maybeInjectTaskReminder(
|
|
84
|
+
messages: ChatCompletionMessageParam[],
|
|
85
|
+
turnCounts: {
|
|
86
|
+
turnsSinceLastTaskManagement: number;
|
|
87
|
+
turnsSinceLastReminder: number;
|
|
88
|
+
},
|
|
89
|
+
tasks: Task[],
|
|
90
|
+
): void {
|
|
91
|
+
if (
|
|
92
|
+
turnCounts.turnsSinceLastTaskManagement <
|
|
93
|
+
TASK_REMINDER_CONFIG.TURNS_SINCE_WRITE
|
|
94
|
+
) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (
|
|
98
|
+
turnCounts.turnsSinceLastReminder <
|
|
99
|
+
TASK_REMINDER_CONFIG.TURNS_BETWEEN_REMINDERS
|
|
100
|
+
) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
messages.push({
|
|
105
|
+
role: "user",
|
|
106
|
+
content: buildTaskReminderText(tasks),
|
|
107
|
+
} as ChatCompletionMessageParam);
|
|
108
|
+
}
|