wave-agent-sdk 1.1.5 → 1.3.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/dist/agent.d.ts +128 -28
- package/dist/agent.js +201 -49
- package/dist/builtin/index.js +2 -0
- package/dist/builtin/plugins.js +11 -20
- package/dist/builtin/skills/settings.js +7 -20
- package/dist/builtin/skills/wave-daemon.d.ts +1 -0
- package/dist/builtin/skills/wave-daemon.js +194 -0
- package/dist/constants/images.d.ts +26 -0
- package/dist/constants/images.js +26 -0
- package/dist/constants/index.d.ts +16 -0
- package/dist/constants/index.js +16 -0
- package/dist/constants/memory.d.ts +26 -0
- package/dist/constants/memory.js +34 -0
- package/dist/constants/messages.d.ts +11 -0
- package/dist/constants/messages.js +11 -0
- package/dist/constants/plugins.d.ts +8 -0
- package/dist/constants/plugins.js +8 -0
- package/dist/constants/tools.d.ts +1 -0
- package/dist/constants/tools.js +1 -0
- package/dist/core/plugin.d.ts +54 -10
- package/dist/core/plugin.js +137 -23
- package/dist/core/session.d.ts +1 -1
- package/dist/core/session.js +1 -1
- package/dist/exec/catalog.d.ts +140 -0
- package/dist/exec/catalog.js +470 -0
- package/dist/exec/catalogAnnouncement.d.ts +89 -0
- package/dist/exec/catalogAnnouncement.js +293 -0
- package/dist/exec/constants.d.ts +51 -0
- package/dist/exec/constants.js +51 -0
- package/dist/exec/execRuntime.d.ts +55 -0
- package/dist/exec/execRuntime.js +217 -0
- package/dist/exec/workerSource.d.ts +28 -0
- package/dist/exec/workerSource.js +299 -0
- package/dist/host/index.d.ts +23 -0
- package/dist/host/index.js +23 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +8 -1
- package/dist/managers/MemoryRuleManager.d.ts +6 -0
- package/dist/managers/MemoryRuleManager.js +12 -0
- package/dist/managers/aiManager.d.ts +35 -25
- package/dist/managers/aiManager.js +204 -202
- package/dist/managers/backgroundTaskManager.js +14 -0
- package/dist/managers/bashModeManager.d.ts +33 -0
- package/dist/managers/bashModeManager.js +110 -0
- package/dist/managers/hookManager.d.ts +18 -0
- package/dist/managers/hookManager.js +37 -3
- package/dist/managers/liveConfigManager.d.ts +33 -0
- package/dist/managers/liveConfigManager.js +106 -11
- package/dist/managers/lspManager.d.ts +9 -0
- package/dist/managers/lspManager.js +47 -18
- package/dist/managers/mcpManager.d.ts +68 -10
- package/dist/managers/mcpManager.js +265 -15
- package/dist/managers/messageManager.d.ts +60 -18
- package/dist/managers/messageManager.js +170 -81
- package/dist/managers/permissionManager.d.ts +69 -0
- package/dist/managers/permissionManager.js +221 -78
- package/dist/managers/planManager.d.ts +9 -0
- package/dist/managers/planManager.js +19 -1
- package/dist/managers/pluginManager.d.ts +46 -2
- package/dist/managers/pluginManager.js +117 -11
- package/dist/managers/pluginScopeManager.d.ts +15 -2
- package/dist/managers/pluginScopeManager.js +20 -1
- package/dist/managers/skillManager.d.ts +50 -0
- package/dist/managers/skillManager.js +166 -12
- package/dist/managers/slashCommandManager.d.ts +10 -0
- package/dist/managers/slashCommandManager.js +44 -31
- package/dist/managers/subagentManager.d.ts +15 -0
- package/dist/managers/subagentManager.js +81 -9
- package/dist/managers/toolManager.d.ts +29 -3
- package/dist/managers/toolManager.js +87 -13
- package/dist/managers/workflowManager.js +6 -0
- package/dist/prompts/autoMemory.d.ts +9 -0
- package/dist/prompts/autoMemory.js +30 -31
- package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
- package/dist/prompts/autoMemoryExtraction.js +8 -111
- package/dist/prompts/index.d.ts +0 -1
- package/dist/prompts/index.js +0 -4
- package/dist/prompts/memoryTypes.d.ts +63 -0
- package/dist/prompts/memoryTypes.js +191 -0
- package/dist/services/GitService.d.ts +7 -0
- package/dist/services/GitService.js +23 -0
- package/dist/services/MarketplaceService.d.ts +101 -17
- package/dist/services/MarketplaceService.js +323 -102
- package/dist/services/artifactContent.d.ts +84 -0
- package/dist/services/artifactContent.js +204 -0
- package/dist/services/artifactSession.d.ts +6 -0
- package/dist/services/artifactSession.js +17 -0
- package/dist/services/autoMemoryService.js +5 -13
- package/dist/services/configurationService.d.ts +92 -9
- package/dist/services/configurationService.js +246 -64
- package/dist/services/contentSummarizer.d.ts +15 -0
- package/dist/services/contentSummarizer.js +45 -0
- package/dist/services/execAvailability.d.ts +9 -0
- package/dist/services/execAvailability.js +32 -0
- package/dist/services/fileWatcher.js +61 -6
- package/dist/services/initializationService.js +21 -17
- package/dist/services/interactionService.d.ts +9 -1
- package/dist/services/interactionService.js +28 -8
- package/dist/services/jsonlHandler.d.ts +98 -0
- package/dist/services/jsonlHandler.js +250 -12
- package/dist/services/memory.d.ts +17 -1
- package/dist/services/memory.js +44 -7
- package/dist/services/officialMarketplaceMirror.d.ts +85 -0
- package/dist/services/officialMarketplaceMirror.js +290 -0
- package/dist/services/pluginLoader.d.ts +12 -4
- package/dist/services/pluginLoader.js +38 -7
- package/dist/services/remoteSettingsService.js +20 -6
- package/dist/services/session.d.ts +74 -0
- package/dist/services/session.js +174 -16
- package/dist/services/sessionEntries.d.ts +2 -0
- package/dist/services/sessionEntries.js +20 -0
- package/dist/services/worktreeHooks.js +6 -1
- package/dist/stdio/index.d.ts +12 -0
- package/dist/stdio/index.js +12 -0
- package/dist/stdio/notificationRouter.d.ts +38 -0
- package/dist/stdio/notificationRouter.js +97 -0
- package/dist/stdio/rpcClient.d.ts +18 -0
- package/dist/stdio/rpcClient.js +10 -0
- package/dist/stdio/stdioAgent.d.ts +229 -0
- package/dist/stdio/stdioAgent.js +360 -0
- package/dist/tools/artifactTool.js +406 -273
- package/dist/tools/bashTool.js +10 -6
- package/dist/tools/editTool.js +6 -3
- package/dist/tools/execTool.d.ts +2 -0
- package/dist/tools/execTool.js +165 -0
- package/dist/tools/exitPlanMode.js +10 -2
- package/dist/tools/grepTool.js +7 -1
- package/dist/tools/readTool.js +30 -2
- package/dist/tools/types.d.ts +34 -8
- package/dist/tools/webFetchTool.js +15 -166
- package/dist/tools/workflowTool.js +40 -8
- package/dist/tools/writeTool.js +6 -3
- package/dist/types/agent.d.ts +24 -1
- package/dist/types/commands.d.ts +7 -0
- package/dist/types/configuration.d.ts +45 -2
- package/dist/types/hooks.d.ts +1 -0
- package/dist/types/hooks.js +19 -0
- package/dist/types/marketplace.d.ts +40 -2
- package/dist/types/mcp.d.ts +42 -0
- package/dist/types/messaging.d.ts +1 -8
- package/dist/types/permissions.d.ts +22 -0
- package/dist/types/permissions.js +17 -0
- package/dist/types/plugins.d.ts +26 -2
- package/dist/types/skills.d.ts +26 -0
- package/dist/utils/bashParser.d.ts +17 -0
- package/dist/utils/bashParser.js +72 -0
- package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
- package/dist/utils/bashStructure/bashLexer.js +676 -0
- package/dist/utils/bashStructure/bashParser.d.ts +144 -0
- package/dist/utils/bashStructure/bashParser.js +606 -0
- package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
- package/dist/utils/bashStructure/bashSemantics.js +477 -0
- package/dist/utils/bashStructure/index.d.ts +26 -0
- package/dist/utils/bashStructure/index.js +27 -0
- package/dist/utils/bashStructure/types.d.ts +62 -0
- package/dist/utils/bashStructure/types.js +47 -0
- package/dist/utils/constants.d.ts +10 -0
- package/dist/utils/constants.js +10 -0
- package/dist/utils/containerSetup.js +48 -6
- package/dist/utils/convertMessagesForAPI.d.ts +7 -1
- package/dist/utils/convertMessagesForAPI.js +64 -14
- package/dist/utils/fileChangeReminder.d.ts +20 -0
- package/dist/utils/fileChangeReminder.js +153 -0
- package/dist/utils/fileSearch.js +4 -3
- package/dist/utils/fileUtils.d.ts +44 -0
- package/dist/utils/fileUtils.js +118 -0
- package/dist/utils/frontmatterYaml.d.ts +33 -0
- package/dist/utils/frontmatterYaml.js +192 -0
- package/dist/utils/imageBudget.d.ts +85 -0
- package/dist/utils/imageBudget.js +109 -0
- package/dist/utils/imageDimensions.d.ts +83 -0
- package/dist/utils/imageDimensions.js +232 -0
- package/dist/utils/imageProcessor.d.ts +66 -0
- package/dist/utils/imageProcessor.js +84 -0
- package/dist/utils/imageRewrite.d.ts +29 -0
- package/dist/utils/imageRewrite.js +251 -0
- package/dist/utils/markdownParser.d.ts +5 -1
- package/dist/utils/markdownParser.js +9 -51
- package/dist/utils/mcpInstructions.d.ts +61 -0
- package/dist/utils/mcpInstructions.js +126 -0
- package/dist/utils/mcpUtils.d.ts +7 -0
- package/dist/utils/mcpUtils.js +11 -2
- package/dist/utils/memoryAge.d.ts +32 -0
- package/dist/utils/memoryAge.js +47 -0
- package/dist/utils/memoryEntrypoint.d.ts +20 -0
- package/dist/utils/memoryEntrypoint.js +49 -0
- package/dist/utils/memoryIndex.d.ts +30 -0
- package/dist/utils/memoryIndex.js +76 -0
- package/dist/utils/messageOperations.d.ts +6 -20
- package/dist/utils/messageOperations.js +40 -91
- package/dist/utils/nestedMemory.d.ts +22 -0
- package/dist/utils/nestedMemory.js +61 -0
- package/dist/utils/npmTarball.d.ts +19 -0
- package/dist/utils/npmTarball.js +92 -0
- package/dist/utils/pluginSource.d.ts +37 -0
- package/dist/utils/pluginSource.js +73 -0
- package/dist/utils/ripgrep.d.ts +18 -4
- package/dist/utils/ripgrep.js +56 -4
- package/dist/utils/runtimeDeps.d.ts +35 -0
- package/dist/utils/runtimeDeps.js +426 -0
- package/dist/utils/skillParser.js +22 -52
- package/dist/utils/subagentParser.js +48 -45
- package/dist/utils/tokenCalculation.js +0 -8
- package/dist/utils/userSettings.d.ts +90 -0
- package/dist/utils/userSettings.js +291 -0
- package/dist/utils/worktreeUtils.d.ts +2 -1
- package/dist/utils/worktreeUtils.js +64 -34
- package/package.json +12 -4
- package/dist/managers/bangManager.d.ts +0 -26
- package/dist/managers/bangManager.js +0 -78
|
@@ -14,7 +14,7 @@ import { HookManager } from "../managers/hookManager.js";
|
|
|
14
14
|
import { SkillManager } from "../managers/skillManager.js";
|
|
15
15
|
import { SlashCommandManager } from "../managers/slashCommandManager.js";
|
|
16
16
|
import { PluginManager } from "../managers/pluginManager.js";
|
|
17
|
-
import {
|
|
17
|
+
import { BashModeManager } from "../managers/bashModeManager.js";
|
|
18
18
|
import { CronManager } from "../managers/cronManager.js";
|
|
19
19
|
import { WorkflowManager } from "../managers/workflowManager.js";
|
|
20
20
|
import { MemoryRuleManager } from "../managers/MemoryRuleManager.js";
|
|
@@ -28,6 +28,8 @@ import { MemoryService } from "../services/memory.js";
|
|
|
28
28
|
import { AutoMemoryService } from "../services/autoMemoryService.js";
|
|
29
29
|
import { USER_MEMORY_FILE } from "./constants.js";
|
|
30
30
|
import { getGitMainRepoRoot } from "./gitUtils.js";
|
|
31
|
+
import { toPosixPath } from "./path.js";
|
|
32
|
+
import { EDIT_TOOL_NAME, WRITE_TOOL_NAME } from "../constants/tools.js";
|
|
31
33
|
import { AsyncWorkRegistry } from "./asyncWorkRegistry.js";
|
|
32
34
|
import { logger } from "./globalLogger.js";
|
|
33
35
|
import { authService } from "../services/authService.js";
|
|
@@ -211,6 +213,19 @@ export function setupAgentContainer(setupOptions) {
|
|
|
211
213
|
if (decision.newPermissionRule) {
|
|
212
214
|
await addPermissionRule(decision.newPermissionRule);
|
|
213
215
|
}
|
|
216
|
+
if (decision.newAdditionalDirectory) {
|
|
217
|
+
// Session-level only: the user approved this directory from the
|
|
218
|
+
// confirmation dialog (never persisted to settings).
|
|
219
|
+
permissionManager.addInstanceAdditionalDirectory(decision.newAdditionalDirectory);
|
|
220
|
+
// The dialog option is "允许本会话编辑 <目录名>/" — it has to cover
|
|
221
|
+
// later edits too, otherwise `default` mode would keep prompting for
|
|
222
|
+
// every Write/Edit in that directory. Forward slashes: minimatch
|
|
223
|
+
// treats the pattern `\` as an escape, while it normalises the input
|
|
224
|
+
// path's separators on Windows.
|
|
225
|
+
const pattern = `${toPosixPath(decision.newAdditionalDirectory)}/**`;
|
|
226
|
+
permissionManager.addInstanceAllowedRule(`${EDIT_TOOL_NAME}(${pattern})`);
|
|
227
|
+
permissionManager.addInstanceAllowedRule(`${WRITE_TOOL_NAME}(${pattern})`);
|
|
228
|
+
}
|
|
214
229
|
return decision;
|
|
215
230
|
}
|
|
216
231
|
: undefined;
|
|
@@ -240,22 +255,49 @@ export function setupAgentContainer(setupOptions) {
|
|
|
240
255
|
// Same gate for the builtin /artifact skill: refresh emits "refreshed"
|
|
241
256
|
// so slash-command registration follows enableArtifact. Awaited (via the
|
|
242
257
|
// awaited onReload) so skills are re-populated before Agent.create()
|
|
243
|
-
// returns —
|
|
244
|
-
//
|
|
245
|
-
// skill list to callers right after create().
|
|
258
|
+
// returns — otherwise callers right after create() could observe a
|
|
259
|
+
// mid-refresh skill list.
|
|
246
260
|
await skillManager.reloadFeatureGatedSkills().catch((error) => {
|
|
247
261
|
logger.error("Failed to reload feature-gated skills:", error);
|
|
248
262
|
});
|
|
249
263
|
},
|
|
250
264
|
});
|
|
251
265
|
container.register("LiveConfigManager", liveConfigManager);
|
|
266
|
+
// The turn-scoped configuration snapshot is held by LiveConfigManager (it owns
|
|
267
|
+
// the turn boundaries and the live reload) and read by ConfigurationService's
|
|
268
|
+
// resolve chain, so a mid-turn settings.json change applies from the next turn
|
|
269
|
+
// instead of shifting values under the running one.
|
|
270
|
+
configurationService.setTurnSnapshotSource(() => liveConfigManager.getTurnSnapshot());
|
|
252
271
|
// Wire up remote settings hot-update: when polling detects changed settings,
|
|
253
272
|
// reload configuration so admin changes propagate to the running agent.
|
|
254
273
|
// The callback strongly captures the per-agent LiveConfigManager, so it MUST
|
|
255
274
|
// be unsubscribed in destroy() via teardown — otherwise the module-level
|
|
256
275
|
// callback array pins the whole agent object graph.
|
|
276
|
+
// Managed plugin config (enabledPlugins / marketplaces) last applied to the
|
|
277
|
+
// plugin subsystem, keyed by a JSON signature: a synced change that touches
|
|
278
|
+
// neither field must not bounce every plugin's MCP servers.
|
|
279
|
+
const remoteAtSetup = remoteSettingsService.getRemoteSettingsSync();
|
|
280
|
+
let managedPluginSignature = JSON.stringify([
|
|
281
|
+
remoteAtSetup?.enabledPlugins ?? null,
|
|
282
|
+
remoteAtSetup?.marketplaces ?? null,
|
|
283
|
+
]);
|
|
257
284
|
teardowns.push(remoteSettingsService.onSettingsUpdate(async () => {
|
|
258
285
|
await liveConfigManager.reload();
|
|
286
|
+
// Managed settings also drive plugin installation and loading: a synced
|
|
287
|
+
// change to `enabledPlugins` / `marketplaces` must re-read the plugins in
|
|
288
|
+
// place, otherwise an admin push would only take effect after a restart
|
|
289
|
+
// (spec enterprise server-managed-config「托管配置下发插件市场与启用列表」
|
|
290
|
+
// 场景 1 与 3).
|
|
291
|
+
const remote = remoteSettingsService.getRemoteSettingsSync();
|
|
292
|
+
const signature = JSON.stringify([
|
|
293
|
+
remote?.enabledPlugins ?? null,
|
|
294
|
+
remote?.marketplaces ?? null,
|
|
295
|
+
]);
|
|
296
|
+
if (signature === managedPluginSignature) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
managedPluginSignature = signature;
|
|
300
|
+
await container.get("PluginManager")?.reloadAllPlugins();
|
|
259
301
|
}));
|
|
260
302
|
const subagentManager = new SubagentManager(container, {
|
|
261
303
|
workdir,
|
|
@@ -289,8 +331,8 @@ export function setupAgentContainer(setupOptions) {
|
|
|
289
331
|
slashCommandManager.initialize();
|
|
290
332
|
const pluginManager = new PluginManager(container, { workdir });
|
|
291
333
|
container.register("PluginManager", pluginManager);
|
|
292
|
-
const
|
|
293
|
-
container.register("
|
|
334
|
+
const bashModeManager = new BashModeManager(container, { workdir });
|
|
335
|
+
container.register("BashModeManager", bashModeManager);
|
|
294
336
|
const cronManager = new CronManager(container, messageManager.getSessionId());
|
|
295
337
|
container.register("CronManager", cronManager);
|
|
296
338
|
cronManager.start();
|
|
@@ -10,8 +10,14 @@ export interface ConvertMessagesOptions {
|
|
|
10
10
|
/**
|
|
11
11
|
* Convert message format to API call format, stopping when a compacted message is encountered.
|
|
12
12
|
* Messages with no meaningful content or tool calls are filtered out.
|
|
13
|
+
*
|
|
14
|
+
* Async because every outbound image is routed through `planOutboundImage`,
|
|
15
|
+
* which may have to re-encode an oversized image with the optional `sharp`
|
|
16
|
+
* codec (`utils/imageRewrite.ts`). Images that are already inside the budget —
|
|
17
|
+
* the common case — take a synchronous fast path and come out byte-identical.
|
|
18
|
+
*
|
|
13
19
|
* @param messages Message list
|
|
14
20
|
* @param options Optional conversion options (e.g. supportsVision)
|
|
15
21
|
* @returns Converted API message format list
|
|
16
22
|
*/
|
|
17
|
-
export declare function convertMessagesForAPI(messages: Message[], options?: ConvertMessagesOptions): ChatCompletionMessageParam[]
|
|
23
|
+
export declare function convertMessagesForAPI(messages: Message[], options?: ConvertMessagesOptions): Promise<ChatCompletionMessageParam[]>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { imageFileCacheKey, planOutboundImage } from "./imageRewrite.js";
|
|
1
2
|
import { convertImageToBase64 } from "./messageOperations.js";
|
|
2
3
|
import { taskNotificationToXml } from "./notificationXml.js";
|
|
3
4
|
import { recoverTruncatedJson, stripAnsiColors } from "./stringUtils.js";
|
|
@@ -35,11 +36,17 @@ function safeToolArguments(args) {
|
|
|
35
36
|
/**
|
|
36
37
|
* Convert message format to API call format, stopping when a compacted message is encountered.
|
|
37
38
|
* Messages with no meaningful content or tool calls are filtered out.
|
|
39
|
+
*
|
|
40
|
+
* Async because every outbound image is routed through `planOutboundImage`,
|
|
41
|
+
* which may have to re-encode an oversized image with the optional `sharp`
|
|
42
|
+
* codec (`utils/imageRewrite.ts`). Images that are already inside the budget —
|
|
43
|
+
* the common case — take a synchronous fast path and come out byte-identical.
|
|
44
|
+
*
|
|
38
45
|
* @param messages Message list
|
|
39
46
|
* @param options Optional conversion options (e.g. supportsVision)
|
|
40
47
|
* @returns Converted API message format list
|
|
41
48
|
*/
|
|
42
|
-
export function convertMessagesForAPI(messages, options) {
|
|
49
|
+
export async function convertMessagesForAPI(messages, options) {
|
|
43
50
|
const supportsVision = options?.supportsVision !== false;
|
|
44
51
|
const recentMessages = [];
|
|
45
52
|
const startIndex = messages.length - 1;
|
|
@@ -70,7 +77,7 @@ export function convertMessagesForAPI(messages, options) {
|
|
|
70
77
|
if (toolBlocks.length > 0) {
|
|
71
78
|
// Collect image user messages to place after all tool messages
|
|
72
79
|
const imageUserMessages = [];
|
|
73
|
-
|
|
80
|
+
for (const toolBlock of toolBlocks) {
|
|
74
81
|
// Only add completed tool blocks (i.e., stage is 'end')
|
|
75
82
|
if (toolBlock.id && toolBlock.stage === "end") {
|
|
76
83
|
completedToolIds.add(toolBlock.id);
|
|
@@ -84,18 +91,34 @@ export function convertMessagesForAPI(messages, options) {
|
|
|
84
91
|
if (toolBlock.images && toolBlock.images.length > 0) {
|
|
85
92
|
if (supportsVision) {
|
|
86
93
|
const contentParts = [];
|
|
87
|
-
toolBlock.images
|
|
94
|
+
for (const image of toolBlock.images) {
|
|
88
95
|
const imageUrl = image.data.startsWith("data:")
|
|
89
96
|
? image.data
|
|
90
97
|
: `data:${image.mediaType || "image/png"};base64,${image.data}`;
|
|
98
|
+
// Tool-produced images (Read screenshots, MCP results, Exec
|
|
99
|
+
// output) go through the same outbound budget as attachments:
|
|
100
|
+
// an image the gateway would reject is shrunk, or omitted with
|
|
101
|
+
// an actionable note, instead of failing the whole request.
|
|
102
|
+
const plan = await planOutboundImage({
|
|
103
|
+
dataUrl: imageUrl,
|
|
104
|
+
cacheKey: imageUrl,
|
|
105
|
+
sourcePath: image.path,
|
|
106
|
+
});
|
|
107
|
+
if (plan.kind === "omit") {
|
|
108
|
+
contentParts.push({ type: "text", text: plan.note });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
91
111
|
contentParts.push({
|
|
92
112
|
type: "image_url",
|
|
93
113
|
image_url: {
|
|
94
|
-
url:
|
|
114
|
+
url: plan.dataUrl,
|
|
95
115
|
detail: "auto",
|
|
96
116
|
},
|
|
97
117
|
});
|
|
98
|
-
|
|
118
|
+
if (plan.note) {
|
|
119
|
+
contentParts.push({ type: "text", text: plan.note });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
99
122
|
imageUserMessages.push({
|
|
100
123
|
role: "user",
|
|
101
124
|
content: contentParts,
|
|
@@ -129,7 +152,7 @@ export function convertMessagesForAPI(messages, options) {
|
|
|
129
152
|
}
|
|
130
153
|
}
|
|
131
154
|
}
|
|
132
|
-
}
|
|
155
|
+
}
|
|
133
156
|
// Insert image user messages after all tool messages but before the
|
|
134
157
|
// assistant message (which will be unshifted next). Since tool messages
|
|
135
158
|
// were unshifted to the front, we splice images right after them.
|
|
@@ -218,7 +241,7 @@ export function convertMessagesForAPI(messages, options) {
|
|
|
218
241
|
else if (message.role === "user") {
|
|
219
242
|
// User messages converted to standard format
|
|
220
243
|
const contentParts = [];
|
|
221
|
-
message.blocks
|
|
244
|
+
for (const block of message.blocks) {
|
|
222
245
|
// Add text content - only if it has meaningful content
|
|
223
246
|
if (block.type === "text" &&
|
|
224
247
|
block.content &&
|
|
@@ -253,28 +276,55 @@ export function convertMessagesForAPI(messages, options) {
|
|
|
253
276
|
});
|
|
254
277
|
}
|
|
255
278
|
else {
|
|
256
|
-
block.imageUrls
|
|
279
|
+
for (const imageUrl of block.imageUrls) {
|
|
257
280
|
// Check if it's already base64, convert if not
|
|
258
281
|
const isDataUrl = imageUrl.startsWith("data:image/");
|
|
259
282
|
let finalImageUrl = imageUrl;
|
|
260
283
|
if (!isDataUrl) {
|
|
261
|
-
// If it's a file path, it needs to be converted to base64
|
|
284
|
+
// If it's a file path, it needs to be converted to base64.
|
|
285
|
+
// Unreadable/empty/unknown-format files come back as
|
|
286
|
+
// undefined: skip the image rather than send an empty payload.
|
|
287
|
+
let converted;
|
|
262
288
|
try {
|
|
263
|
-
|
|
289
|
+
converted = convertImageToBase64(imageUrl);
|
|
264
290
|
}
|
|
265
291
|
catch (error) {
|
|
266
292
|
logger.error("Failed to convert image path to base64:", imageUrl, error);
|
|
267
293
|
// Skip this image, do not add to content
|
|
268
|
-
|
|
294
|
+
continue;
|
|
269
295
|
}
|
|
296
|
+
if (!converted) {
|
|
297
|
+
logger.warn("Skipping unusable image file:", imageUrl);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
finalImageUrl = converted;
|
|
301
|
+
}
|
|
302
|
+
// Outbound budget: an image the vision gateway would reject, or
|
|
303
|
+
// one over our own size budget, is shrunk here (or replaced by an
|
|
304
|
+
// actionable note when it cannot be). See utils/imageRewrite.ts.
|
|
305
|
+
const plan = await planOutboundImage({
|
|
306
|
+
dataUrl: finalImageUrl,
|
|
307
|
+
cacheKey: isDataUrl
|
|
308
|
+
? finalImageUrl
|
|
309
|
+
: imageFileCacheKey(imageUrl),
|
|
310
|
+
sourcePath: isDataUrl ? undefined : imageUrl,
|
|
311
|
+
});
|
|
312
|
+
if (plan.kind === "omit") {
|
|
313
|
+
contentParts.push({ type: "text", text: plan.note });
|
|
314
|
+
continue;
|
|
270
315
|
}
|
|
271
316
|
contentParts.push({
|
|
272
317
|
type: "image_url",
|
|
273
318
|
image_url: {
|
|
274
|
-
url:
|
|
319
|
+
url: plan.dataUrl,
|
|
275
320
|
detail: "auto",
|
|
276
321
|
},
|
|
277
322
|
});
|
|
323
|
+
// Tell the model when the image it sees is not the full-size
|
|
324
|
+
// original, so it can map coordinates back.
|
|
325
|
+
if (plan.note) {
|
|
326
|
+
contentParts.push({ type: "text", text: plan.note });
|
|
327
|
+
}
|
|
278
328
|
// Aligned with Claude Code: when the image comes from a local
|
|
279
329
|
// file (not an inline dataURL), append its source path as text
|
|
280
330
|
// metadata so the model can reference the file with tools
|
|
@@ -285,7 +335,7 @@ export function convertMessagesForAPI(messages, options) {
|
|
|
285
335
|
text: `[Image source: ${imageUrl}]`,
|
|
286
336
|
});
|
|
287
337
|
}
|
|
288
|
-
}
|
|
338
|
+
}
|
|
289
339
|
}
|
|
290
340
|
}
|
|
291
341
|
// If there is a tool block in user message, add its result
|
|
@@ -302,7 +352,7 @@ export function convertMessagesForAPI(messages, options) {
|
|
|
302
352
|
text: `A background agent completed a task:\n${taskNotificationToXml(block)}`,
|
|
303
353
|
});
|
|
304
354
|
}
|
|
305
|
-
}
|
|
355
|
+
}
|
|
306
356
|
// Only add user message if there is meaningful content
|
|
307
357
|
if (contentParts.length > 0) {
|
|
308
358
|
// Filter out empty text parts
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReadFileState } from "../tools/types.js";
|
|
2
|
+
/** Max number of changed files reported in a single reminder. */
|
|
3
|
+
export declare const CHANGED_FILES_MAX_FILES = 10;
|
|
4
|
+
/** Max bytes of the changed-lines snippet rendered for a single file. */
|
|
5
|
+
export declare const CHANGED_FILES_SNIPPET_MAX_BYTES = 2048;
|
|
6
|
+
/** Max bytes of the whole reminder (all reported files combined). */
|
|
7
|
+
export declare const CHANGED_FILES_TOTAL_MAX_BYTES = 8192;
|
|
8
|
+
/**
|
|
9
|
+
* Build a compact "what changed" snippet from the new file content: trim the
|
|
10
|
+
* common prefix/suffix, then render the differing region with line numbers.
|
|
11
|
+
* A deletion-only region renders as a removal note. Truncates at a line
|
|
12
|
+
* boundary to `maxBytes`. Returns "" when the contents are identical.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildChangedSnippet(oldContent: string, newContent: string, maxBytes?: number): string;
|
|
15
|
+
/**
|
|
16
|
+
* Detect files that changed on disk since they were read and return a reminder
|
|
17
|
+
* body for the agent, or null when nothing changed. Refreshes the read state
|
|
18
|
+
* for every detected change (see the module comment).
|
|
19
|
+
*/
|
|
20
|
+
export declare function getChangedFilesReminder(readFileState: ReadFileState | undefined): Promise<string | null>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External file-change detection for the main agent loop (aligned with Claude
|
|
3
|
+
* Code's `changed_files` attachment).
|
|
4
|
+
*
|
|
5
|
+
* For every file recorded in the session read state by a *full* read/write,
|
|
6
|
+
* this compares the on-disk mtime against the mtime recorded at read time and,
|
|
7
|
+
* when the file is newer AND the content actually differs, produces a short
|
|
8
|
+
* changed-lines summary. The read state entry is then refreshed to the new
|
|
9
|
+
* mtime/hash/content so that (a) the same change is reported only once, and
|
|
10
|
+
* (b) a subsequent Edit/Write passes the staleness check — the agent has just
|
|
11
|
+
* been told the new content.
|
|
12
|
+
*
|
|
13
|
+
* The writers this surfaces are the ones other than the main agent: the
|
|
14
|
+
* auto-memory extraction fork, an external editor, another session. The main
|
|
15
|
+
* agent's own Write/Edit refresh the read state on write, so they never show up
|
|
16
|
+
* here.
|
|
17
|
+
*/
|
|
18
|
+
import { readFile, stat } from "node:fs/promises";
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
/** Max number of changed files reported in a single reminder. */
|
|
21
|
+
export const CHANGED_FILES_MAX_FILES = 10;
|
|
22
|
+
/** Max bytes of the changed-lines snippet rendered for a single file. */
|
|
23
|
+
export const CHANGED_FILES_SNIPPET_MAX_BYTES = 2048;
|
|
24
|
+
/** Max bytes of the whole reminder (all reported files combined). */
|
|
25
|
+
export const CHANGED_FILES_TOTAL_MAX_BYTES = 8192;
|
|
26
|
+
function sha256(text) {
|
|
27
|
+
return createHash("sha256").update(text).digest("hex");
|
|
28
|
+
}
|
|
29
|
+
/** UTF-8 byte length — CJK content is ~3 bytes/char, so `.length` would under-count. */
|
|
30
|
+
function byteLength(text) {
|
|
31
|
+
return Buffer.byteLength(text, "utf-8");
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build a compact "what changed" snippet from the new file content: trim the
|
|
35
|
+
* common prefix/suffix, then render the differing region with line numbers.
|
|
36
|
+
* A deletion-only region renders as a removal note. Truncates at a line
|
|
37
|
+
* boundary to `maxBytes`. Returns "" when the contents are identical.
|
|
38
|
+
*/
|
|
39
|
+
export function buildChangedSnippet(oldContent, newContent, maxBytes = CHANGED_FILES_SNIPPET_MAX_BYTES) {
|
|
40
|
+
const oldLines = oldContent.replace(/\r\n/g, "\n").split("\n");
|
|
41
|
+
const newLines = newContent.replace(/\r\n/g, "\n").split("\n");
|
|
42
|
+
const shared = Math.min(oldLines.length, newLines.length);
|
|
43
|
+
let prefix = 0;
|
|
44
|
+
while (prefix < shared && oldLines[prefix] === newLines[prefix])
|
|
45
|
+
prefix++;
|
|
46
|
+
let suffix = 0;
|
|
47
|
+
const maxSuffix = shared - prefix;
|
|
48
|
+
while (suffix < maxSuffix &&
|
|
49
|
+
oldLines[oldLines.length - 1 - suffix] ===
|
|
50
|
+
newLines[newLines.length - 1 - suffix]) {
|
|
51
|
+
suffix++;
|
|
52
|
+
}
|
|
53
|
+
const changedOld = oldLines.slice(prefix, oldLines.length - suffix);
|
|
54
|
+
const changedNew = newLines.slice(prefix, newLines.length - suffix);
|
|
55
|
+
if (changedOld.length === 0 && changedNew.length === 0)
|
|
56
|
+
return "";
|
|
57
|
+
if (changedNew.length === 0) {
|
|
58
|
+
const plural = changedOld.length === 1 ? "line" : "lines";
|
|
59
|
+
return `(${changedOld.length} ${plural} removed at line ${prefix + 1})`;
|
|
60
|
+
}
|
|
61
|
+
const rendered = [];
|
|
62
|
+
let used = 0;
|
|
63
|
+
let truncated = 0;
|
|
64
|
+
for (let i = 0; i < changedNew.length; i++) {
|
|
65
|
+
const line = `${String(prefix + i + 1).padStart(4, " ")} | ${changedNew[i]}`;
|
|
66
|
+
const cost = byteLength(line) + 1; // + trailing newline
|
|
67
|
+
if (used + cost > maxBytes) {
|
|
68
|
+
truncated = changedNew.length - i;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
rendered.push(line);
|
|
72
|
+
used += cost;
|
|
73
|
+
}
|
|
74
|
+
let snippet = rendered.join("\n");
|
|
75
|
+
if (truncated > 0) {
|
|
76
|
+
snippet += `\n... [${truncated} lines truncated] ...`;
|
|
77
|
+
}
|
|
78
|
+
return snippet;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Detect files that changed on disk since they were read and return a reminder
|
|
82
|
+
* body for the agent, or null when nothing changed. Refreshes the read state
|
|
83
|
+
* for every detected change (see the module comment).
|
|
84
|
+
*/
|
|
85
|
+
export async function getChangedFilesReminder(readFileState) {
|
|
86
|
+
if (!readFileState || readFileState.size === 0)
|
|
87
|
+
return null;
|
|
88
|
+
const blocks = [];
|
|
89
|
+
let totalBytes = 0;
|
|
90
|
+
for (const [filePath, state] of readFileState) {
|
|
91
|
+
if (blocks.length >= CHANGED_FILES_MAX_FILES)
|
|
92
|
+
break;
|
|
93
|
+
// Partial reads cache only a slice — no reliable diff baseline.
|
|
94
|
+
if (state.offset !== undefined || state.limit !== undefined)
|
|
95
|
+
continue;
|
|
96
|
+
let mtimeMs;
|
|
97
|
+
let newContent;
|
|
98
|
+
try {
|
|
99
|
+
const stats = await stat(filePath);
|
|
100
|
+
mtimeMs = stats.mtime.getTime();
|
|
101
|
+
// Not newer than the recorded mtime → untouched, or touched by the main
|
|
102
|
+
// agent's own Write/Edit (which refreshes the entry to the post-write mtime).
|
|
103
|
+
if (mtimeMs <= state.mtime)
|
|
104
|
+
continue;
|
|
105
|
+
newContent = await readFile(filePath, "utf-8");
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Deleted or transiently unreadable — leave the entry alone and move on.
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// Refresh the baseline so the same observation is not re-processed on every
|
|
112
|
+
// turn and a subsequent Edit passes the staleness check (the agent has just
|
|
113
|
+
// been told the new content).
|
|
114
|
+
const refresh = () => {
|
|
115
|
+
readFileState.set(filePath, {
|
|
116
|
+
mtime: mtimeMs,
|
|
117
|
+
hash: sha256(newContent),
|
|
118
|
+
source: "changed",
|
|
119
|
+
content: newContent,
|
|
120
|
+
offset: undefined,
|
|
121
|
+
limit: undefined,
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
const oldContent = state.content;
|
|
125
|
+
// No baseline, or the file was touched but its content is unchanged
|
|
126
|
+
// (git checkout / editor round-trip save / cloud sync / antivirus).
|
|
127
|
+
if (oldContent === undefined || oldContent === newContent) {
|
|
128
|
+
refresh();
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const snippet = buildChangedSnippet(oldContent, newContent);
|
|
132
|
+
if (!snippet) {
|
|
133
|
+
refresh();
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const block = `${filePath}\n${snippet}`;
|
|
137
|
+
if (totalBytes + byteLength(block) > CHANGED_FILES_TOTAL_MAX_BYTES) {
|
|
138
|
+
// Leave the entry un-refreshed so the change is reported on a later turn
|
|
139
|
+
// rather than silently dropped.
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
refresh();
|
|
143
|
+
blocks.push(block);
|
|
144
|
+
totalBytes += byteLength(block);
|
|
145
|
+
}
|
|
146
|
+
if (blocks.length === 0)
|
|
147
|
+
return null;
|
|
148
|
+
return [
|
|
149
|
+
"The following files were modified on disk since you last read them — by another process such as a background agent, an external editor, or another session. Your earlier read of these files is out of date; re-read a file before editing it if the changed lines below are not enough.",
|
|
150
|
+
"",
|
|
151
|
+
blocks.join("\n\n"),
|
|
152
|
+
].join("\n");
|
|
153
|
+
}
|
package/dist/utils/fileSearch.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { getRgPath } from "./ripgrep.js";
|
|
3
3
|
import fuzzysort from "fuzzysort";
|
|
4
4
|
import { logger } from "./globalLogger.js";
|
|
5
5
|
const EXCLUDED_FILES = [".git", ".DS_Store"];
|
|
@@ -7,12 +7,13 @@ const EXCLUDED_FILES = [".git", ".DS_Store"];
|
|
|
7
7
|
* Execute ripgrep to get all file paths
|
|
8
8
|
*/
|
|
9
9
|
async function getAllFiles(workingDirectory) {
|
|
10
|
-
|
|
10
|
+
const rgBinary = getRgPath();
|
|
11
|
+
if (!rgBinary) {
|
|
11
12
|
throw new Error("ripgrep is not available");
|
|
12
13
|
}
|
|
13
14
|
const rgArgs = ["--files", "--color=never", "--hidden"];
|
|
14
15
|
return new Promise((resolve, reject) => {
|
|
15
|
-
const child = spawn(
|
|
16
|
+
const child = spawn(rgBinary, rgArgs, {
|
|
16
17
|
cwd: workingDirectory,
|
|
17
18
|
stdio: ["ignore", "pipe", "pipe"],
|
|
18
19
|
});
|
|
@@ -13,6 +13,25 @@ export declare function readFirstLine(filePath: string): Promise<string>;
|
|
|
13
13
|
* @return {Promise<string[]>} - Array of non-empty lines (up to maxLines).
|
|
14
14
|
*/
|
|
15
15
|
export declare function readFirstNLines(filePath: string, maxLines: number): Promise<string[]>;
|
|
16
|
+
/**
|
|
17
|
+
* Streams a file line by line without holding it in memory.
|
|
18
|
+
*
|
|
19
|
+
* Splits on `\n` and hands each segment to `onLine`; a trailing `\r` is left
|
|
20
|
+
* for the callback's own `trim()`. When the file does not end with a newline,
|
|
21
|
+
* the trailing partial segment is delivered too, so callers can apply the same
|
|
22
|
+
* "interrupted append" tolerance `JsonlHandler.read()` uses — its
|
|
23
|
+
* `endsWithNewline` flag is exactly the return value here.
|
|
24
|
+
*
|
|
25
|
+
* Memory stays at one line: chunks are consumed as they arrive and are not
|
|
26
|
+
* accumulated.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} filePath - The path to the file.
|
|
29
|
+
* @param {(line: string) => void | Promise<void>} onLine - Called per segment, in file order.
|
|
30
|
+
* @return {Promise<{ endsWithNewline: boolean }>} - Whether the file ended with a newline (empty files count as true).
|
|
31
|
+
*/
|
|
32
|
+
export declare function forEachLine(filePath: string, onLine: (line: string) => void | Promise<void>): Promise<{
|
|
33
|
+
endsWithNewline: boolean;
|
|
34
|
+
}>;
|
|
16
35
|
/**
|
|
17
36
|
* Reads a file from the end and returns the last non-empty line.
|
|
18
37
|
*
|
|
@@ -26,6 +45,31 @@ export declare function readFirstNLines(filePath: string, maxLines: number): Pro
|
|
|
26
45
|
* @return {Promise<string>} - The last non-empty line of the file, or an empty string if no non-empty lines found.
|
|
27
46
|
*/
|
|
28
47
|
export declare function getLastLine(filePath: string, minLength?: number): Promise<string>;
|
|
48
|
+
/**
|
|
49
|
+
* Read up to `maxBytes` from the end of a file and return the lines it holds,
|
|
50
|
+
* oldest first. The window may start in the middle of a line, in which case
|
|
51
|
+
* that partial first line is dropped so every returned line is complete —
|
|
52
|
+
* callers parse them as JSON.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} filePath - The path to the file.
|
|
55
|
+
* @param {number} maxBytes - Size of the tail window (default 64KB).
|
|
56
|
+
* @return {Promise<string[]>} - Trailing non-empty lines, or [] if unreadable.
|
|
57
|
+
*/
|
|
58
|
+
export declare function readTailLines(filePath: string, maxBytes?: number): Promise<string[]>;
|
|
59
|
+
/**
|
|
60
|
+
* Synchronously read up to `maxBytes` from the end of a file and return the
|
|
61
|
+
* raw tail text (blank lines preserved). When the window starts mid-line the
|
|
62
|
+
* partial first line is dropped, so the result holds complete lines only.
|
|
63
|
+
*
|
|
64
|
+
* Synchronous on purpose: the caller (`BackgroundTaskManager.getOutput`) sits
|
|
65
|
+
* on the synchronous `getBackgroundTaskOutput` path that hosts and the CLI
|
|
66
|
+
* already consume. The window is capped, so the blocking read stays small.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} filePath - The path to the file.
|
|
69
|
+
* @param {number} maxBytes - Size of the tail window (default 64KB).
|
|
70
|
+
* @return {string} - Tail text, or "" when the file is empty or unreadable.
|
|
71
|
+
*/
|
|
72
|
+
export declare function readTailTextSync(filePath: string, maxBytes?: number): string;
|
|
29
73
|
/**
|
|
30
74
|
* Suggests similar paths if a file is not found.
|
|
31
75
|
*/
|