wave-agent-sdk 0.13.6 → 0.14.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.map +1 -1
- package/dist/agent.js +4 -2
- package/dist/managers/aiManager.d.ts +3 -0
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +93 -8
- package/dist/managers/messageManager.d.ts +15 -0
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +52 -2
- package/dist/managers/permissionManager.d.ts +4 -0
- package/dist/managers/permissionManager.d.ts.map +1 -1
- package/dist/managers/permissionManager.js +6 -0
- package/dist/managers/subagentManager.d.ts.map +1 -1
- package/dist/managers/subagentManager.js +23 -17
- package/dist/prompts/index.d.ts +2 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +50 -25
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +11 -1
- package/dist/tools/agentTool.d.ts.map +1 -1
- package/dist/tools/agentTool.js +14 -2
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +27 -5
- package/dist/tools/types.d.ts +1 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/webFetchTool.d.ts.map +1 -1
- package/dist/tools/webFetchTool.js +202 -78
- package/dist/types/messaging.d.ts +1 -0
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/utils/convertMessagesForAPI.js +1 -1
- package/dist/utils/groupMessagesByApiRound.d.ts +24 -0
- package/dist/utils/groupMessagesByApiRound.d.ts.map +1 -0
- package/dist/utils/groupMessagesByApiRound.js +97 -0
- package/dist/utils/messageOperations.d.ts +1 -0
- package/dist/utils/messageOperations.d.ts.map +1 -1
- package/dist/utils/microcompact.d.ts +7 -0
- package/dist/utils/microcompact.d.ts.map +1 -0
- package/dist/utils/microcompact.js +78 -0
- package/package.json +2 -1
- package/src/agent.ts +4 -2
- package/src/managers/aiManager.ts +117 -15
- package/src/managers/messageManager.ts +64 -2
- package/src/managers/permissionManager.ts +7 -0
- package/src/managers/subagentManager.ts +28 -24
- package/src/prompts/index.ts +51 -25
- package/src/services/aiService.ts +14 -1
- package/src/tools/agentTool.ts +14 -2
- package/src/tools/bashTool.ts +27 -5
- package/src/tools/types.ts +1 -0
- package/src/tools/webFetchTool.ts +276 -86
- package/src/types/messaging.ts +1 -0
- package/src/utils/convertMessagesForAPI.ts +1 -1
- package/src/utils/groupMessagesByApiRound.ts +120 -0
- package/src/utils/messageOperations.ts +1 -0
- package/src/utils/microcompact.ts +101 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Message } from "../types/index.js";
|
|
2
|
+
export interface ApiRound {
|
|
3
|
+
messages: Message[];
|
|
4
|
+
estimatedTokens: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Groups messages into "API rounds" — each round corresponds to one API
|
|
8
|
+
* call-response cycle. This is critical because in agentic sessions with a
|
|
9
|
+
* single user prompt, Wave creates a new Message per API round (each recursive
|
|
10
|
+
* sendAIMessage call creates a new assistant message).
|
|
11
|
+
*
|
|
12
|
+
* Boundaries:
|
|
13
|
+
* - A new `role: "user"` message starts a new round.
|
|
14
|
+
* - A new `role: "assistant"` message with a different `id` starts a new round.
|
|
15
|
+
* - A message with a `compress` block is pushed as its own round and starts a
|
|
16
|
+
* new round after it.
|
|
17
|
+
*/
|
|
18
|
+
export declare function groupMessagesByApiRound(messages: Message[]): ApiRound[];
|
|
19
|
+
/**
|
|
20
|
+
* Returns the last `roundCount` complete API rounds as a flat message array.
|
|
21
|
+
* Never splits a tool_use/tool_result pair. If fewer rounds exist, returns all.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getLastApiRounds(messages: Message[], roundCount: number): Message[];
|
|
24
|
+
//# sourceMappingURL=groupMessagesByApiRound.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"groupMessagesByApiRound.d.ts","sourceRoot":"","sources":["../../src/utils/groupMessagesByApiRound.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,QAAQ,EAAE,CA8DvE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,OAAO,EAAE,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,EAAE,CAIX"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Groups messages into "API rounds" — each round corresponds to one API
|
|
3
|
+
* call-response cycle. This is critical because in agentic sessions with a
|
|
4
|
+
* single user prompt, Wave creates a new Message per API round (each recursive
|
|
5
|
+
* sendAIMessage call creates a new assistant message).
|
|
6
|
+
*
|
|
7
|
+
* Boundaries:
|
|
8
|
+
* - A new `role: "user"` message starts a new round.
|
|
9
|
+
* - A new `role: "assistant"` message with a different `id` starts a new round.
|
|
10
|
+
* - A message with a `compress` block is pushed as its own round and starts a
|
|
11
|
+
* new round after it.
|
|
12
|
+
*/
|
|
13
|
+
export function groupMessagesByApiRound(messages) {
|
|
14
|
+
const rounds = [];
|
|
15
|
+
let currentRound = [];
|
|
16
|
+
let lastAssistantId;
|
|
17
|
+
for (const msg of messages) {
|
|
18
|
+
let startNewRound = false;
|
|
19
|
+
if (msg.role === "user") {
|
|
20
|
+
startNewRound = true;
|
|
21
|
+
}
|
|
22
|
+
else if (msg.role === "assistant") {
|
|
23
|
+
// Compress block is always its own round
|
|
24
|
+
const hasCompress = msg.blocks.some((b) => b.type === "compress");
|
|
25
|
+
if (hasCompress) {
|
|
26
|
+
startNewRound = true;
|
|
27
|
+
}
|
|
28
|
+
else if (msg.id !== lastAssistantId) {
|
|
29
|
+
// New assistant id starts a new round.
|
|
30
|
+
// Exception: if the current round is [user] (first assistant after a
|
|
31
|
+
// user prompt in a normal conversation), keep them together as one
|
|
32
|
+
// round. But if we already have assistant(s) in this round (agentic
|
|
33
|
+
// tool loop), the new id starts a new round.
|
|
34
|
+
const roundHasOtherAssistant = currentRound.some((m) => m.role === "assistant" && m.id !== msg.id);
|
|
35
|
+
if (roundHasOtherAssistant) {
|
|
36
|
+
startNewRound = true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
lastAssistantId = msg.id;
|
|
40
|
+
}
|
|
41
|
+
if (startNewRound && currentRound.length > 0) {
|
|
42
|
+
rounds.push({
|
|
43
|
+
messages: currentRound,
|
|
44
|
+
estimatedTokens: estimateTokens(currentRound),
|
|
45
|
+
});
|
|
46
|
+
currentRound = [];
|
|
47
|
+
}
|
|
48
|
+
currentRound.push(msg);
|
|
49
|
+
// After pushing a compress message as its own round, flush immediately
|
|
50
|
+
if (msg.role === "assistant" &&
|
|
51
|
+
msg.blocks.some((b) => b.type === "compress")) {
|
|
52
|
+
rounds.push({
|
|
53
|
+
messages: currentRound,
|
|
54
|
+
estimatedTokens: estimateTokens(currentRound),
|
|
55
|
+
});
|
|
56
|
+
currentRound = [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (currentRound.length > 0) {
|
|
60
|
+
rounds.push({
|
|
61
|
+
messages: currentRound,
|
|
62
|
+
estimatedTokens: estimateTokens(currentRound),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return rounds;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Returns the last `roundCount` complete API rounds as a flat message array.
|
|
69
|
+
* Never splits a tool_use/tool_result pair. If fewer rounds exist, returns all.
|
|
70
|
+
*/
|
|
71
|
+
export function getLastApiRounds(messages, roundCount) {
|
|
72
|
+
const rounds = groupMessagesByApiRound(messages);
|
|
73
|
+
const lastRounds = rounds.slice(-roundCount);
|
|
74
|
+
return lastRounds.flatMap((r) => r.messages);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Roughly estimate token count from character count (~4 chars per token).
|
|
78
|
+
*/
|
|
79
|
+
function estimateTokens(messages) {
|
|
80
|
+
let chars = 0;
|
|
81
|
+
for (const msg of messages) {
|
|
82
|
+
for (const block of msg.blocks) {
|
|
83
|
+
if ("content" in block && typeof block.content === "string") {
|
|
84
|
+
chars += block.content.length;
|
|
85
|
+
}
|
|
86
|
+
if (block.type === "tool" &&
|
|
87
|
+
block.parameters &&
|
|
88
|
+
typeof block.parameters === "string") {
|
|
89
|
+
chars += block.parameters.length;
|
|
90
|
+
}
|
|
91
|
+
if (block.type === "tool" && block.result) {
|
|
92
|
+
chars += block.result.length;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return Math.ceil(chars / 4);
|
|
97
|
+
}
|
|
@@ -41,6 +41,7 @@ export interface UpdateToolBlockParams {
|
|
|
41
41
|
compactParams?: string;
|
|
42
42
|
parametersChunk?: string;
|
|
43
43
|
isManuallyBackgrounded?: boolean;
|
|
44
|
+
timestamp?: number;
|
|
44
45
|
}
|
|
45
46
|
export type AgentToolBlockUpdateParams = Omit<UpdateToolBlockParams, "messages">;
|
|
46
47
|
export interface AddErrorBlockParams {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messageOperations.d.ts","sourceRoot":"","sources":["../../src/utils/messageOperations.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,OAAO,EACP,KAAK,EAGN,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGlD,OAAO,EAAE,qCAAqC,EAAE,MAAM,qBAAqB,CAAC;AAI5E,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAGD,MAAM,WAAW,oBAAqB,SAAQ,iBAAiB;IAC7D,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,OAAO,GAAG,WAAW,GAAG,SAAS,GAAG,KAAK,CAAC;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sBAAsB,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"messageOperations.d.ts","sourceRoot":"","sources":["../../src/utils/messageOperations.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,OAAO,EACP,KAAK,EAGN,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGlD,OAAO,EAAE,qCAAqC,EAAE,MAAM,qBAAqB,CAAC;AAI5E,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAGD,MAAM,WAAW,oBAAqB,SAAQ,iBAAiB;IAC7D,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,OAAO,GAAG,WAAW,GAAG,SAAS,GAAG,KAAK,CAAC;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAGD,MAAM,MAAM,0BAA0B,GAAG,IAAI,CAC3C,qBAAqB,EACrB,UAAU,CACX,CAAC;AAEF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAI,WAAW,MAAM,KAAG,MAmCxD,CAAC;AAEF,eAAO,MAAM,iBAAiB,QAAO,MAA+B,CAAC;AAGrE,eAAO,MAAM,wBAAwB,GAAI,0EAQtC,oBAAoB,KAAG,OAAO,EA6BhC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,2BAA2B,GACtC,UAAU,OAAO,EAAE,EACnB,IAAI,MAAM,EACV,QAAQ,OAAO,CAAC,iBAAiB,CAAC,KACjC,OAAO,EAwBT,CAAC;AAGF,eAAO,MAAM,6BAA6B,GACxC,UAAU,OAAO,EAAE,EACnB,UAAU,MAAM,EAChB,YAAY,qCAAqC,EAAE,EACnD,QAAQ,KAAK,EACb,mBAAmB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACzC,OAAO,EA+BT,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,+BAA+B,GAC1C,UAAU,OAAO,EAAE,EACnB,WAAW,MAAM,EACjB,QAAQ,IAAI,CAAC,0BAA0B,EAAE,IAAI,CAAC,KAC7C;IAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAuB5C,CAAC;AAGF,eAAO,MAAM,wBAAwB,GAAI,6KAgBtC,qBAAqB,KAAG,OAAO,EAsFjC,CAAC;AAGF,eAAO,MAAM,sBAAsB,GAAI,sBAGpC,mBAAmB,KAAG,OAAO,EAgC/B,CAAC;AAGF,eAAO,MAAM,cAAc,GAAI,wBAG5B,aAAa,KAAG,OAAO,EAgBzB,CAAC;AAGF,eAAO,MAAM,mBAAmB,GAAI,gCAIjC,gBAAgB,KAAG,OAAO,EAmB5B,CAAC;AAGF,eAAO,MAAM,qBAAqB,GAAI,0CAKnC,kBAAkB,KAAG,OAAO,EAuB9B,CAAC;AAEF;;GAEG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAU3D;AAED;;;GAGG;AACH,eAAO,MAAM,qBAAqB,GAAI,UAAU,OAAO,EAAE,KAAG,OAAO,EASlE,CAAC;AAEF;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAoBtD;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,MAAM,CAUR;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAoB1D;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC;IAC5B,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,gCAAgC,GAAI,8DAO9C,4BAA4B,KAAG,OAAO,EAiBxC,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Message } from "../types/messaging.js";
|
|
2
|
+
export interface MicrocompactOptions {
|
|
3
|
+
timeThresholdMS: number;
|
|
4
|
+
recentResultsToKeep: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function microcompactMessages(messages: Message[], options: MicrocompactOptions): Message[];
|
|
7
|
+
//# sourceMappingURL=microcompact.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"microcompact.d.ts","sourceRoot":"","sources":["../../src/utils/microcompact.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAa,MAAM,uBAAuB,CAAC;AAEhE,MAAM,WAAW,mBAAmB;IAClC,eAAe,EAAE,MAAM,CAAC;IACxB,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAID,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,EAAE,CAwFX"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const CLEARED_RESULT = "[Old tool result content cleared]";
|
|
2
|
+
export function microcompactMessages(messages, options) {
|
|
3
|
+
const { timeThresholdMS, recentResultsToKeep } = options;
|
|
4
|
+
// 1. Find the latest tool block timestamp across all assistant messages
|
|
5
|
+
let lastAssistantTime = 0;
|
|
6
|
+
for (const msg of messages) {
|
|
7
|
+
if (msg.role === "assistant") {
|
|
8
|
+
for (const block of msg.blocks) {
|
|
9
|
+
if (block.type === "tool" && block.stage === "end" && block.timestamp) {
|
|
10
|
+
if (block.timestamp > lastAssistantTime) {
|
|
11
|
+
lastAssistantTime = block.timestamp;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// 2. If no prior assistant messages with completed tools, return unchanged
|
|
18
|
+
if (lastAssistantTime === 0) {
|
|
19
|
+
return messages;
|
|
20
|
+
}
|
|
21
|
+
// 3. If within threshold, return unchanged
|
|
22
|
+
if (Date.now() - lastAssistantTime < timeThresholdMS) {
|
|
23
|
+
return messages;
|
|
24
|
+
}
|
|
25
|
+
const toolRefs = [];
|
|
26
|
+
for (let mi = 0; mi < messages.length; mi++) {
|
|
27
|
+
const msg = messages[mi];
|
|
28
|
+
if (msg.role === "assistant") {
|
|
29
|
+
for (let bi = 0; bi < msg.blocks.length; bi++) {
|
|
30
|
+
const block = msg.blocks[bi];
|
|
31
|
+
if (block.type === "tool" && block.stage === "end" && block.timestamp) {
|
|
32
|
+
toolRefs.push({
|
|
33
|
+
msgIndex: mi,
|
|
34
|
+
blockIndex: bi,
|
|
35
|
+
timestamp: block.timestamp,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
toolRefs.sort((a, b) => b.timestamp - a.timestamp);
|
|
42
|
+
// 5. Mark the top N as "keep"
|
|
43
|
+
const keepSet = new Set();
|
|
44
|
+
for (let i = 0; i < Math.min(recentResultsToKeep, toolRefs.length); i++) {
|
|
45
|
+
const ref = toolRefs[i];
|
|
46
|
+
keepSet.add(`${ref.msgIndex}:${ref.blockIndex}`);
|
|
47
|
+
}
|
|
48
|
+
// 6. Deep-copy messages and clear result + shortResult on non-kept blocks
|
|
49
|
+
const result = messages.map((msg) => ({
|
|
50
|
+
...msg,
|
|
51
|
+
blocks: msg.blocks.map((block) => {
|
|
52
|
+
if (block.type === "tool" && block.stage === "end" && block.timestamp) {
|
|
53
|
+
return { ...block };
|
|
54
|
+
}
|
|
55
|
+
return block;
|
|
56
|
+
}),
|
|
57
|
+
}));
|
|
58
|
+
// Clear non-kept tool blocks
|
|
59
|
+
for (const ref of toolRefs) {
|
|
60
|
+
const key = `${ref.msgIndex}:${ref.blockIndex}`;
|
|
61
|
+
if (!keepSet.has(key)) {
|
|
62
|
+
result[ref.msgIndex] = {
|
|
63
|
+
...result[ref.msgIndex],
|
|
64
|
+
blocks: result[ref.msgIndex].blocks.map((b, idx) => {
|
|
65
|
+
if (idx === ref.blockIndex && b.type === "tool") {
|
|
66
|
+
return {
|
|
67
|
+
...b,
|
|
68
|
+
result: CLEARED_RESULT,
|
|
69
|
+
shortResult: undefined,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return b;
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-agent-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "SDK for building AI-powered development tools and agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"cron-parser": "^5.5.0",
|
|
34
34
|
"fuzzysort": "^3.1.0",
|
|
35
35
|
"glob": "^13.0.0",
|
|
36
|
+
"lru-cache": "^11.3.5",
|
|
36
37
|
"minimatch": "^10.0.3",
|
|
37
38
|
"openai": "^5.12.2",
|
|
38
39
|
"turndown": "^7.2.2"
|
package/src/agent.ts
CHANGED
|
@@ -579,11 +579,13 @@ export class Agent {
|
|
|
579
579
|
|
|
580
580
|
/** Unified interrupt method, interrupts both AI messages and command execution */
|
|
581
581
|
public abortMessage(): void {
|
|
582
|
+
// Clear queue first to prevent processQueuedMessage from dequeuing
|
|
583
|
+
// when abortAIMessage triggers onLoadingChange(false)
|
|
584
|
+
this.messageQueue.clear();
|
|
585
|
+
this.options.callbacks?.onQueuedMessagesChange?.(this.queuedMessages);
|
|
582
586
|
this.abortAIMessage(); // This will abort tools including Agent tool (subagents)
|
|
583
587
|
this.abortBashCommand();
|
|
584
588
|
this.abortSlashCommand();
|
|
585
|
-
this.messageQueue.clear();
|
|
586
|
-
this.options.callbacks?.onQueuedMessagesChange?.(this.queuedMessages);
|
|
587
589
|
}
|
|
588
590
|
|
|
589
591
|
/** Interrupt bash command execution */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type CallAgentOptions } from "../services/aiService.js";
|
|
2
2
|
import * as aiService from "../services/aiService.js";
|
|
3
3
|
import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
|
|
4
|
+
import { microcompactMessages } from "../utils/microcompact.js";
|
|
4
5
|
import { parseTaskNotificationXml } from "../utils/notificationXml.js";
|
|
5
6
|
import { calculateComprehensiveTotalTokens } from "../utils/tokenCalculation.js";
|
|
6
7
|
import * as fs from "node:fs/promises";
|
|
@@ -15,7 +16,6 @@ import type { ToolManager } from "./toolManager.js";
|
|
|
15
16
|
import type { ToolContext, ToolResult } from "../tools/types.js";
|
|
16
17
|
import type { MessageManager } from "./messageManager.js";
|
|
17
18
|
import type { BackgroundTaskManager } from "./backgroundTaskManager.js";
|
|
18
|
-
import type { NotificationQueue } from "./notificationQueue.js";
|
|
19
19
|
import { ChatCompletionMessageFunctionToolCall } from "openai/resources.js";
|
|
20
20
|
import type { HookManager } from "./hookManager.js";
|
|
21
21
|
import type { ExtendedHookExecutionContext } from "../types/hooks.js";
|
|
@@ -25,6 +25,7 @@ import type { SkillManager } from "./skillManager.js";
|
|
|
25
25
|
import { buildSystemPrompt } from "../prompts/index.js";
|
|
26
26
|
import { Container } from "../utils/container.js";
|
|
27
27
|
import { ConfigurationService } from "../services/configurationService.js";
|
|
28
|
+
import type { NotificationQueue } from "./notificationQueue.js";
|
|
28
29
|
|
|
29
30
|
import { logger } from "../utils/globalLogger.js";
|
|
30
31
|
|
|
@@ -51,11 +52,13 @@ export class AIManager {
|
|
|
51
52
|
onLoadingChange?: (loading: boolean) => void;
|
|
52
53
|
private toolAbortController: AbortController | null = null;
|
|
53
54
|
private workdir: string;
|
|
55
|
+
private originalWorkdir: string;
|
|
54
56
|
private systemPrompt?: string;
|
|
55
57
|
private subagentType?: string; // Store subagent type for hook context
|
|
56
58
|
private stream: boolean; // Streaming mode flag
|
|
57
59
|
private modelOverride?: string;
|
|
58
60
|
private _onCwdChange?: (newCwd: string) => void; // Store callback for CWD changes
|
|
61
|
+
private consecutiveCompressionFailures: number = 0;
|
|
59
62
|
|
|
60
63
|
// Service overrides
|
|
61
64
|
constructor(
|
|
@@ -63,6 +66,7 @@ export class AIManager {
|
|
|
63
66
|
options: AIManagerOptions,
|
|
64
67
|
) {
|
|
65
68
|
this.workdir = options.workdir;
|
|
69
|
+
this.originalWorkdir = options.workdir;
|
|
66
70
|
this.systemPrompt = options.systemPrompt;
|
|
67
71
|
this.subagentType = options.subagentType; // Store subagent type
|
|
68
72
|
this.stream = options.stream ?? true; // Default to true if not specified
|
|
@@ -165,6 +169,10 @@ export class AIManager {
|
|
|
165
169
|
return this.workdir;
|
|
166
170
|
}
|
|
167
171
|
|
|
172
|
+
public getOriginalWorkdir(): string {
|
|
173
|
+
return this.originalWorkdir;
|
|
174
|
+
}
|
|
175
|
+
|
|
168
176
|
public setOnCwdChange(callback: (newCwd: string) => void): void {
|
|
169
177
|
this._onCwdChange = callback;
|
|
170
178
|
}
|
|
@@ -234,6 +242,7 @@ export class AIManager {
|
|
|
234
242
|
if (toolPlugin?.formatCompactParams) {
|
|
235
243
|
const context: ToolContext = {
|
|
236
244
|
workdir: this.workdir,
|
|
245
|
+
originalWorkdir: this.originalWorkdir,
|
|
237
246
|
taskManager: this.taskManager,
|
|
238
247
|
};
|
|
239
248
|
return toolPlugin.formatCompactParams(toolArgs, context);
|
|
@@ -248,7 +257,6 @@ export class AIManager {
|
|
|
248
257
|
private async handleTokenUsageAndCompression(
|
|
249
258
|
usage: Usage | undefined,
|
|
250
259
|
abortController: AbortController,
|
|
251
|
-
model?: string,
|
|
252
260
|
): Promise<void> {
|
|
253
261
|
if (!usage) return;
|
|
254
262
|
|
|
@@ -272,6 +280,14 @@ export class AIManager {
|
|
|
272
280
|
|
|
273
281
|
// If there are messages to compress, perform compression
|
|
274
282
|
if (messagesToCompress.length > 0) {
|
|
283
|
+
// Circuit breaker: skip compression after 3 consecutive failures
|
|
284
|
+
if (this.consecutiveCompressionFailures >= 3) {
|
|
285
|
+
logger?.warn(
|
|
286
|
+
`Skipping compression: ${this.consecutiveCompressionFailures} consecutive failures`,
|
|
287
|
+
);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
275
291
|
const recentChatMessages = convertMessagesForAPI(messagesToCompress);
|
|
276
292
|
|
|
277
293
|
// Save session before compression to preserve original messages
|
|
@@ -284,7 +300,7 @@ export class AIManager {
|
|
|
284
300
|
modelConfig: this.getModelConfig(),
|
|
285
301
|
messages: recentChatMessages,
|
|
286
302
|
abortSignal: abortController.signal,
|
|
287
|
-
model:
|
|
303
|
+
model: this.getModelConfig().fastModel,
|
|
288
304
|
});
|
|
289
305
|
|
|
290
306
|
// Handle usage tracking for compression operations
|
|
@@ -294,14 +310,91 @@ export class AIManager {
|
|
|
294
310
|
prompt_tokens: compressionResult.usage.prompt_tokens,
|
|
295
311
|
completion_tokens: compressionResult.usage.completion_tokens,
|
|
296
312
|
total_tokens: compressionResult.usage.total_tokens,
|
|
297
|
-
model:
|
|
313
|
+
model: this.getModelConfig().fastModel,
|
|
298
314
|
operation_type: "compress",
|
|
299
315
|
};
|
|
300
316
|
}
|
|
301
317
|
|
|
318
|
+
// Build post-compact context restoration
|
|
319
|
+
const POST_COMPACT_TOKEN_BUDGET = 50_000;
|
|
320
|
+
const POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000;
|
|
321
|
+
const POST_COMPACT_MAX_FILES_TO_RESTORE = 5;
|
|
322
|
+
const contextParts: string[] = [];
|
|
323
|
+
|
|
324
|
+
// 1. File context restoration
|
|
325
|
+
const recentFiles = this.messageManager.getRecentFileReads(
|
|
326
|
+
POST_COMPACT_MAX_FILES_TO_RESTORE,
|
|
327
|
+
POST_COMPACT_MAX_TOKENS_PER_FILE,
|
|
328
|
+
);
|
|
329
|
+
let usedTokens = 0;
|
|
330
|
+
for (const file of recentFiles) {
|
|
331
|
+
const fileTokens = Math.ceil(file.content.length / 4);
|
|
332
|
+
if (usedTokens + fileTokens > POST_COMPACT_MAX_TOKENS_PER_FILE)
|
|
333
|
+
continue;
|
|
334
|
+
if (fileTokens > 0) usedTokens += fileTokens;
|
|
335
|
+
contextParts.push(
|
|
336
|
+
`\n\n## ${file.path}\n\`\`\`\n${file.content}\n\`\`\``,
|
|
337
|
+
);
|
|
338
|
+
if (contextParts.length >= POST_COMPACT_MAX_FILES_TO_RESTORE) break;
|
|
339
|
+
if (usedTokens >= POST_COMPACT_TOKEN_BUDGET) break;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 2. Working directory
|
|
343
|
+
contextParts.push(
|
|
344
|
+
`\n\n[Working Directory]\nCurrent working directory: ${this.workdir}`,
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
// 3. Plan mode context
|
|
348
|
+
const currentMode = this.permissionManager?.getCurrentEffectiveMode(
|
|
349
|
+
this.getModelConfig().permissionMode,
|
|
350
|
+
);
|
|
351
|
+
if (currentMode === "plan") {
|
|
352
|
+
const planFilePath = this.permissionManager?.getPlanFilePath();
|
|
353
|
+
if (planFilePath) {
|
|
354
|
+
let planExists = false;
|
|
355
|
+
try {
|
|
356
|
+
await fs.access(planFilePath);
|
|
357
|
+
planExists = true;
|
|
358
|
+
} catch {
|
|
359
|
+
// Plan file doesn't exist yet
|
|
360
|
+
}
|
|
361
|
+
contextParts.push(
|
|
362
|
+
`\n\n[Plan Mode]\nYou are in plan mode. Plan file: ${planFilePath} (exists: ${planExists})`,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 4. Skills context
|
|
368
|
+
const skills =
|
|
369
|
+
this.skillManager
|
|
370
|
+
?.getAvailableSkills()
|
|
371
|
+
.filter((s) => !s.disableModelInvocation) || [];
|
|
372
|
+
if (skills.length > 0) {
|
|
373
|
+
const skillList = skills
|
|
374
|
+
.map((s) => `- ${s.name}: ${s.description || ""}`)
|
|
375
|
+
.join("\n");
|
|
376
|
+
contextParts.push(`\n\n[Available Skills]\n${skillList}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 5. Background agents status
|
|
380
|
+
const agents = this.backgroundTaskManager?.getAllTasks() || [];
|
|
381
|
+
if (agents.length > 0) {
|
|
382
|
+
const agentList = agents
|
|
383
|
+
.map((a) => `- Agent "${a.description}": ${a.status}`)
|
|
384
|
+
.join("\n");
|
|
385
|
+
contextParts.push(`\n\n[Background Tasks]\n${agentList}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Merge context restoration into summary
|
|
389
|
+
const enhancedSummary =
|
|
390
|
+
compressionResult.content +
|
|
391
|
+
(contextParts.length > 0
|
|
392
|
+
? `\n\n[Context Restoration]` + contextParts.join("")
|
|
393
|
+
: "");
|
|
394
|
+
|
|
302
395
|
// Execute message reconstruction and sessionId update after compression
|
|
303
396
|
this.messageManager.compressMessagesAndUpdateSession(
|
|
304
|
-
|
|
397
|
+
enhancedSummary,
|
|
305
398
|
compressionUsage,
|
|
306
399
|
);
|
|
307
400
|
|
|
@@ -313,8 +406,13 @@ export class AIManager {
|
|
|
313
406
|
logger?.debug(
|
|
314
407
|
`Successfully compressed ${messagesToCompress.length} messages and updated session`,
|
|
315
408
|
);
|
|
409
|
+
this.consecutiveCompressionFailures = 0;
|
|
316
410
|
} catch (compressError) {
|
|
317
|
-
|
|
411
|
+
this.consecutiveCompressionFailures++;
|
|
412
|
+
logger?.error(
|
|
413
|
+
`Failed to compress messages (${this.consecutiveCompressionFailures} consecutive):`,
|
|
414
|
+
compressError,
|
|
415
|
+
);
|
|
318
416
|
this.messageManager.addErrorBlock(
|
|
319
417
|
`Failed to compress conversation history: ${compressError instanceof Error ? compressError.message : String(compressError)}. You may encounter context limit issues.`,
|
|
320
418
|
);
|
|
@@ -403,10 +501,13 @@ export class AIManager {
|
|
|
403
501
|
toolAbortController = this.toolAbortController!;
|
|
404
502
|
}
|
|
405
503
|
|
|
406
|
-
// Get recent message history
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
504
|
+
// Get recent message history with microcompact applied
|
|
505
|
+
const rawMessages = this.messageManager.getMessages();
|
|
506
|
+
const microcompactedMessages = microcompactMessages(rawMessages, {
|
|
507
|
+
timeThresholdMS: 30 * 60 * 1000, // 30 minutes
|
|
508
|
+
recentResultsToKeep: 3,
|
|
509
|
+
});
|
|
510
|
+
const recentMessages = convertMessagesForAPI(microcompactedMessages);
|
|
410
511
|
|
|
411
512
|
try {
|
|
412
513
|
// Get combined memory content
|
|
@@ -472,6 +573,7 @@ export class AIManager {
|
|
|
472
573
|
filteredToolPlugins,
|
|
473
574
|
{
|
|
474
575
|
workdir: this.workdir,
|
|
576
|
+
originalWorkdir: this.originalWorkdir,
|
|
475
577
|
memory: combinedMemory,
|
|
476
578
|
language: this.getLanguage(),
|
|
477
579
|
isSubagent: !!this.subagentType,
|
|
@@ -658,6 +760,7 @@ export class AIManager {
|
|
|
658
760
|
stage: "end",
|
|
659
761
|
name: toolName,
|
|
660
762
|
compactParams: "",
|
|
763
|
+
timestamp: Date.now(),
|
|
661
764
|
});
|
|
662
765
|
return;
|
|
663
766
|
}
|
|
@@ -710,6 +813,7 @@ export class AIManager {
|
|
|
710
813
|
abortSignal: toolAbortController.signal,
|
|
711
814
|
backgroundTaskManager: this.backgroundTaskManager,
|
|
712
815
|
workdir: this.workdir,
|
|
816
|
+
originalWorkdir: this.originalWorkdir,
|
|
713
817
|
messageId: this.messageManager.getMessages().slice(-1)[0]?.id,
|
|
714
818
|
sessionId: this.messageManager.getSessionId(),
|
|
715
819
|
toolCallId: toolId,
|
|
@@ -774,6 +878,7 @@ export class AIManager {
|
|
|
774
878
|
shortResult: toolResult.shortResult,
|
|
775
879
|
isManuallyBackgrounded: toolResult.isManuallyBackgrounded,
|
|
776
880
|
startLineNumber: toolResult.startLineNumber,
|
|
881
|
+
timestamp: Date.now(),
|
|
777
882
|
});
|
|
778
883
|
|
|
779
884
|
// Execute PostToolUse hooks after successful tool completion
|
|
@@ -799,6 +904,7 @@ export class AIManager {
|
|
|
799
904
|
name: toolName,
|
|
800
905
|
compactParams,
|
|
801
906
|
isManuallyBackgrounded: false,
|
|
907
|
+
timestamp: Date.now(),
|
|
802
908
|
});
|
|
803
909
|
}
|
|
804
910
|
},
|
|
@@ -809,11 +915,7 @@ export class AIManager {
|
|
|
809
915
|
}
|
|
810
916
|
|
|
811
917
|
// Handle token statistics and message compression
|
|
812
|
-
await this.handleTokenUsageAndCompression(
|
|
813
|
-
result.usage,
|
|
814
|
-
abortController,
|
|
815
|
-
model,
|
|
816
|
-
);
|
|
918
|
+
await this.handleTokenUsageAndCompression(result.usage, abortController);
|
|
817
919
|
|
|
818
920
|
// Finalize text/reasoning blocks for the final response (no tools)
|
|
819
921
|
this.messageManager.finalizeStreamingBlocks();
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
generateMessageId,
|
|
17
17
|
} from "../utils/messageOperations.js";
|
|
18
18
|
import type { Message, Usage } from "../types/index.js";
|
|
19
|
+
import { getLastApiRounds } from "../utils/groupMessagesByApiRound.js";
|
|
19
20
|
import { join, isAbsolute, relative } from "path";
|
|
20
21
|
import {
|
|
21
22
|
appendMessages,
|
|
@@ -89,6 +90,8 @@ export class MessageManager {
|
|
|
89
90
|
private transcriptPath: string; // Cached transcript path
|
|
90
91
|
private savedMessageCount: number; // Track how many messages have been saved to prevent duplication
|
|
91
92
|
private filesInContext: Set<string> = new Set(); // Track files mentioned in the conversation
|
|
93
|
+
private recentFileReads: Map<string, { content: string; timestamp: number }> =
|
|
94
|
+
new Map(); // Track file read contents
|
|
92
95
|
private sessionType: "main" | "subagent";
|
|
93
96
|
private subagentType?: string;
|
|
94
97
|
private _usages: Usage[] = [];
|
|
@@ -266,11 +269,13 @@ export class MessageManager {
|
|
|
266
269
|
const newMessages = messages.slice(oldLength);
|
|
267
270
|
for (const message of newMessages) {
|
|
268
271
|
this.addPathsFromMessage(message);
|
|
272
|
+
this.extractFileReadsFromMessage(message);
|
|
269
273
|
}
|
|
270
274
|
|
|
271
275
|
// Also check if the last message was updated (common for tool blocks)
|
|
272
276
|
if (messages.length > 0 && messages.length === oldLength) {
|
|
273
277
|
this.addPathsFromMessage(messages[messages.length - 1]);
|
|
278
|
+
this.extractFileReadsFromMessage(messages[messages.length - 1]);
|
|
274
279
|
}
|
|
275
280
|
|
|
276
281
|
this.callbacks.onMessagesChange?.([...messages]);
|
|
@@ -495,8 +500,8 @@ export class MessageManager {
|
|
|
495
500
|
compressedContent: string,
|
|
496
501
|
usage?: Usage,
|
|
497
502
|
): void {
|
|
498
|
-
// Get last
|
|
499
|
-
const lastThreeMessages = this.messages
|
|
503
|
+
// Get last 2 API rounds to preserve (structurally safe boundary)
|
|
504
|
+
const lastThreeMessages = getLastApiRounds(this.messages, 2);
|
|
500
505
|
|
|
501
506
|
// Create compressed message
|
|
502
507
|
const compressMessage: Message = {
|
|
@@ -994,4 +999,61 @@ export class MessageManager {
|
|
|
994
999
|
|
|
995
1000
|
return paths;
|
|
996
1001
|
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Extract file read contents from tool result blocks in a message.
|
|
1005
|
+
*/
|
|
1006
|
+
private extractFileReadsFromMessage(message: Message): void {
|
|
1007
|
+
for (const block of message.blocks) {
|
|
1008
|
+
if (
|
|
1009
|
+
block.type === "tool" &&
|
|
1010
|
+
block.name === "read" &&
|
|
1011
|
+
block.stage === "end" &&
|
|
1012
|
+
block.result &&
|
|
1013
|
+
block.parameters
|
|
1014
|
+
) {
|
|
1015
|
+
let filePath: string | undefined;
|
|
1016
|
+
try {
|
|
1017
|
+
const params = JSON.parse(block.parameters) as Record<
|
|
1018
|
+
string,
|
|
1019
|
+
unknown
|
|
1020
|
+
>;
|
|
1021
|
+
filePath = params.file_path as string | undefined;
|
|
1022
|
+
} catch {
|
|
1023
|
+
// Ignore parse errors
|
|
1024
|
+
}
|
|
1025
|
+
if (filePath) {
|
|
1026
|
+
this.recentFileReads.set(filePath, {
|
|
1027
|
+
content: block.result,
|
|
1028
|
+
timestamp: Date.now(),
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Get recent file read contents, sorted by timestamp (newest first).
|
|
1037
|
+
* @param maxFiles - Maximum number of files to return
|
|
1038
|
+
* @param maxTokensPerFile - Maximum tokens per file (~4 chars/token)
|
|
1039
|
+
* @returns Array of { path, content } sorted by recency
|
|
1040
|
+
*/
|
|
1041
|
+
public getRecentFileReads(
|
|
1042
|
+
maxFiles = 5,
|
|
1043
|
+
maxTokensPerFile = 5000,
|
|
1044
|
+
): Array<{ path: string; content: string }> {
|
|
1045
|
+
const sorted = Array.from(this.recentFileReads.entries())
|
|
1046
|
+
.sort(([, a], [, b]) => b.timestamp - a.timestamp)
|
|
1047
|
+
.slice(0, maxFiles);
|
|
1048
|
+
|
|
1049
|
+
const result: Array<{ path: string; content: string }> = [];
|
|
1050
|
+
for (const [path, { content }] of sorted) {
|
|
1051
|
+
const truncated =
|
|
1052
|
+
content.length > maxTokensPerFile * 4
|
|
1053
|
+
? content.slice(0, maxTokensPerFile * 4)
|
|
1054
|
+
: content;
|
|
1055
|
+
result.push({ path, content: truncated });
|
|
1056
|
+
}
|
|
1057
|
+
return result;
|
|
1058
|
+
}
|
|
997
1059
|
}
|
|
@@ -315,6 +315,13 @@ export class PermissionManager {
|
|
|
315
315
|
return this.planFilePath;
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Public wrapper for isInsideSafeZone to check if a path is in the safe zone
|
|
320
|
+
*/
|
|
321
|
+
public isPathInSafeZone(targetPath: string): boolean {
|
|
322
|
+
return this.isInsideSafeZone(targetPath).isInside;
|
|
323
|
+
}
|
|
324
|
+
|
|
318
325
|
/**
|
|
319
326
|
* Check if a path is inside the Safe Zone (workdir + additionalDirectories)
|
|
320
327
|
*/
|