@nghyane/arcane 0.1.28 → 0.1.30
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/CHANGELOG.md +7 -0
- package/package.json +4 -4
- package/src/cli/config-cli.ts +1 -1
- package/src/config/settings-schema.ts +19 -27
- package/src/config/settings.ts +3 -4
- package/src/extensibility/custom-tools/types.ts +0 -12
- package/src/extensibility/extensions/index.ts +0 -5
- package/src/extensibility/extensions/runner.ts +6 -26
- package/src/extensibility/extensions/types.ts +1 -77
- package/src/extensibility/hooks/runner.ts +5 -24
- package/src/extensibility/hooks/types.ts +1 -77
- package/src/index.ts +2 -13
- package/src/modes/components/footer.ts +4 -11
- package/src/modes/components/index.ts +0 -1
- package/src/modes/components/status-line/segments.ts +1 -2
- package/src/modes/components/status-line/types.ts +0 -1
- package/src/modes/components/status-line.ts +0 -6
- package/src/modes/components/tree-selector.ts +0 -8
- package/src/modes/controllers/command-controller.ts +2 -98
- package/src/modes/controllers/event-controller.ts +46 -52
- package/src/modes/controllers/extension-ui-controller.ts +0 -42
- package/src/modes/controllers/input-controller.ts +0 -23
- package/src/modes/controllers/selector-controller.ts +0 -5
- package/src/modes/interactive-mode.ts +3 -24
- package/src/modes/print-mode.ts +0 -16
- package/src/modes/rpc/rpc-client.ts +0 -16
- package/src/modes/rpc/rpc-mode.ts +0 -32
- package/src/modes/rpc/rpc-types.ts +0 -9
- package/src/modes/types.ts +1 -13
- package/src/modes/utils/ui-helpers.ts +2 -118
- package/src/prompts/agents/librarian.md +7 -12
- package/src/sdk.ts +0 -15
- package/src/session/agent-session.ts +89 -650
- package/src/session/compaction/branch-summarization.ts +5 -13
- package/src/session/compaction/index.ts +0 -1
- package/src/session/compaction/utils.ts +94 -2
- package/src/session/messages.ts +0 -37
- package/src/session/retry-utils.ts +1 -1
- package/src/session/session-manager.ts +8 -108
- package/src/session/session-types.ts +4 -25
- package/src/session/stats.ts +2 -39
- package/src/slash-commands/builtin-registry.ts +0 -11
- package/src/task/executor.ts +0 -8
- package/src/tools/create-tools.ts +3 -0
- package/src/tools/github-fs.ts +195 -0
- package/src/tools/github-utils.ts +35 -0
- package/src/tools/github.ts +35 -123
- package/src/tools/index.ts +1 -0
- package/examples/hooks/custom-compaction.ts +0 -116
- package/src/modes/components/compaction-summary-message.ts +0 -59
- package/src/prompts/compaction/compaction-short-summary.md +0 -9
- package/src/prompts/compaction/compaction-summary-context.md +0 -5
- package/src/prompts/compaction/compaction-summary.md +0 -41
- package/src/prompts/compaction/compaction-turn-prefix.md +0 -17
- package/src/prompts/compaction/compaction-update-summary.md +0 -45
- package/src/session/compaction/compaction.ts +0 -864
- package/src/session/compaction/pruning.ts +0 -91
|
@@ -10,17 +10,12 @@ import { completeSimple } from "@nghyane/arcane-ai";
|
|
|
10
10
|
import { renderPromptTemplate } from "../../config/prompt-templates";
|
|
11
11
|
import branchSummaryPrompt from "../../prompts/compaction/branch-summary.md" with { type: "text" };
|
|
12
12
|
import branchSummaryPreamble from "../../prompts/compaction/branch-summary-preamble.md" with { type: "text" };
|
|
13
|
-
import {
|
|
14
|
-
convertToLlm,
|
|
15
|
-
createBranchSummaryMessage,
|
|
16
|
-
createCompactionSummaryMessage,
|
|
17
|
-
createCustomMessage,
|
|
18
|
-
} from "../../session/messages";
|
|
13
|
+
import { convertToLlm, createBranchSummaryMessage, createCustomMessage } from "../../session/messages";
|
|
19
14
|
import type { ReadonlySessionManager, SessionEntry } from "../../session/session-manager";
|
|
20
|
-
import { estimateTokens } from "./compaction";
|
|
21
15
|
import {
|
|
22
16
|
computeFileLists,
|
|
23
17
|
createFileOps,
|
|
18
|
+
estimateTokens,
|
|
24
19
|
extractFileOpsFromMessage,
|
|
25
20
|
type FileOperations,
|
|
26
21
|
SUMMARIZATION_SYSTEM_PROMPT,
|
|
@@ -85,7 +80,7 @@ export interface GenerateBranchSummaryOptions {
|
|
|
85
80
|
* Collect entries that should be summarized when navigating from one position to another.
|
|
86
81
|
*
|
|
87
82
|
* Walks from oldLeafId back to the common ancestor with targetId, collecting entries
|
|
88
|
-
* along the way.
|
|
83
|
+
* along the way.
|
|
89
84
|
* summaries become context.
|
|
90
85
|
*
|
|
91
86
|
* @param session - Session manager (read-only access)
|
|
@@ -139,7 +134,7 @@ export function collectEntriesForBranchSummary(
|
|
|
139
134
|
|
|
140
135
|
/**
|
|
141
136
|
* Extract AgentMessage from a session entry.
|
|
142
|
-
*
|
|
137
|
+
* Extract AgentMessage from a session entry.
|
|
143
138
|
*/
|
|
144
139
|
function getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {
|
|
145
140
|
switch (entry.type) {
|
|
@@ -154,9 +149,6 @@ function getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {
|
|
|
154
149
|
case "branch_summary":
|
|
155
150
|
return createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);
|
|
156
151
|
|
|
157
|
-
case "compaction":
|
|
158
|
-
return createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp, entry.shortSummary);
|
|
159
|
-
|
|
160
152
|
// These don't contribute to conversation content
|
|
161
153
|
case "thinking_level_change":
|
|
162
154
|
case "model_change":
|
|
@@ -216,7 +208,7 @@ export function prepareBranchEntries(entries: SessionEntry[], tokenBudget: numbe
|
|
|
216
208
|
// Check budget before adding
|
|
217
209
|
if (tokenBudget > 0 && totalTokens + tokens > tokenBudget) {
|
|
218
210
|
// If this is a summary entry, try to fit it anyway as it's important context
|
|
219
|
-
if (entry.type === "
|
|
211
|
+
if (entry.type === "branch_summary") {
|
|
220
212
|
if (totalTokens < tokenBudget * 0.9) {
|
|
221
213
|
messages.unshift(message);
|
|
222
214
|
totalTokens += tokens;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared utilities for
|
|
2
|
+
* Shared utilities for branch summarization.
|
|
3
3
|
*/
|
|
4
4
|
import type { AgentMessage } from "@nghyane/arcane-agent";
|
|
5
|
-
import type { Message } from "@nghyane/arcane-ai";
|
|
5
|
+
import type { AssistantMessage, Message, Usage } from "@nghyane/arcane-ai";
|
|
6
6
|
import { renderPromptTemplate } from "../../config/prompt-templates";
|
|
7
7
|
import fileOperationsTemplate from "../../prompts/system/file-operations.md" with { type: "text" };
|
|
8
8
|
import summarizationSystemPrompt from "../../prompts/system/summarization-system.md" with { type: "text" };
|
|
@@ -169,3 +169,95 @@ export function serializeConversation(messages: Message[]): string {
|
|
|
169
169
|
// ============================================================================
|
|
170
170
|
|
|
171
171
|
export const SUMMARIZATION_SYSTEM_PROMPT = renderPromptTemplate(summarizationSystemPrompt);
|
|
172
|
+
|
|
173
|
+
// ============================================================================
|
|
174
|
+
// Token Estimation
|
|
175
|
+
// ============================================================================
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Estimate token count for a message using chars/4 heuristic.
|
|
179
|
+
* This is conservative (overestimates tokens).
|
|
180
|
+
*/
|
|
181
|
+
export function estimateTokens(message: AgentMessage): number {
|
|
182
|
+
let chars = 0;
|
|
183
|
+
|
|
184
|
+
switch (message.role) {
|
|
185
|
+
case "user": {
|
|
186
|
+
const content = (message as { content: string | Array<{ type: string; text?: string }> }).content;
|
|
187
|
+
if (typeof content === "string") {
|
|
188
|
+
chars = content.length;
|
|
189
|
+
} else if (Array.isArray(content)) {
|
|
190
|
+
for (const block of content) {
|
|
191
|
+
if (block.type === "text" && block.text) {
|
|
192
|
+
chars += block.text.length;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return Math.ceil(chars / 4);
|
|
197
|
+
}
|
|
198
|
+
case "assistant": {
|
|
199
|
+
const assistant = message as AssistantMessage;
|
|
200
|
+
for (const block of assistant.content) {
|
|
201
|
+
if (block.type === "text") {
|
|
202
|
+
chars += block.text.length;
|
|
203
|
+
} else if (block.type === "thinking") {
|
|
204
|
+
chars += block.thinking.length;
|
|
205
|
+
} else if (block.type === "toolCall") {
|
|
206
|
+
chars += block.name.length + JSON.stringify(block.arguments).length;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return Math.ceil(chars / 4);
|
|
210
|
+
}
|
|
211
|
+
case "hookMessage":
|
|
212
|
+
case "toolResult": {
|
|
213
|
+
if (typeof message.content === "string") {
|
|
214
|
+
chars = message.content.length;
|
|
215
|
+
} else {
|
|
216
|
+
for (const block of message.content) {
|
|
217
|
+
if (block.type === "text" && block.text) {
|
|
218
|
+
chars += block.text.length;
|
|
219
|
+
}
|
|
220
|
+
if (block.type === "image") {
|
|
221
|
+
chars += 4800; // Estimate images as 4000 chars, or 1200 tokens
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return Math.ceil(chars / 4);
|
|
226
|
+
}
|
|
227
|
+
case "bashExecution": {
|
|
228
|
+
chars = message.command.length + message.output.length;
|
|
229
|
+
return Math.ceil(chars / 4);
|
|
230
|
+
}
|
|
231
|
+
case "branchSummary": {
|
|
232
|
+
chars = message.summary.length;
|
|
233
|
+
return Math.ceil(chars / 4);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ============================================================================
|
|
241
|
+
// Context Token Calculation
|
|
242
|
+
// ============================================================================
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Calculate context tokens from usage report.
|
|
246
|
+
* Uses the native totalTokens field when available, falls back to computing from components.
|
|
247
|
+
*/
|
|
248
|
+
export function calculateContextTokens(usage: Usage): number {
|
|
249
|
+
return usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get the last assistant message's usage data.
|
|
254
|
+
*/
|
|
255
|
+
export function getLastAssistantUsage(messages: AgentMessage[]): Usage | undefined {
|
|
256
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
257
|
+
const msg = messages[i];
|
|
258
|
+
if (msg.role === "assistant") {
|
|
259
|
+
return (msg as AssistantMessage).usage;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
package/src/session/messages.ts
CHANGED
|
@@ -8,11 +8,9 @@ import type { AgentMessage } from "@nghyane/arcane-agent";
|
|
|
8
8
|
import type { ImageContent, Message, TextContent, ToolResultMessage } from "@nghyane/arcane-ai";
|
|
9
9
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
10
10
|
import branchSummaryContextPrompt from "../prompts/compaction/branch-summary-context.md" with { type: "text" };
|
|
11
|
-
import compactionSummaryContextPrompt from "../prompts/compaction/compaction-summary-context.md" with { type: "text" };
|
|
12
11
|
import type { OutputMeta } from "../tools/output-meta";
|
|
13
12
|
import { formatOutputNotice } from "../tools/output-meta";
|
|
14
13
|
|
|
15
|
-
const COMPACTION_SUMMARY_TEMPLATE = compactionSummaryContextPrompt;
|
|
16
14
|
const BRANCH_SUMMARY_TEMPLATE = branchSummaryContextPrompt;
|
|
17
15
|
|
|
18
16
|
export const SKILL_PROMPT_MESSAGE_TYPE = "skill-prompt";
|
|
@@ -97,14 +95,6 @@ export interface BranchSummaryMessage {
|
|
|
97
95
|
timestamp: number;
|
|
98
96
|
}
|
|
99
97
|
|
|
100
|
-
export interface CompactionSummaryMessage {
|
|
101
|
-
role: "compactionSummary";
|
|
102
|
-
summary: string;
|
|
103
|
-
shortSummary?: string;
|
|
104
|
-
tokensBefore: number;
|
|
105
|
-
timestamp: number;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
98
|
/**
|
|
109
99
|
* Message type for auto-read file mentions via @filepath syntax.
|
|
110
100
|
*/
|
|
@@ -132,7 +122,6 @@ declare module "@nghyane/arcane-agent" {
|
|
|
132
122
|
custom: CustomMessage;
|
|
133
123
|
hookMessage: HookMessage;
|
|
134
124
|
branchSummary: BranchSummaryMessage;
|
|
135
|
-
compactionSummary: CompactionSummaryMessage;
|
|
136
125
|
fileMention: FileMentionMessage;
|
|
137
126
|
}
|
|
138
127
|
}
|
|
@@ -184,21 +173,6 @@ export function createBranchSummaryMessage(summary: string, fromId: string, time
|
|
|
184
173
|
};
|
|
185
174
|
}
|
|
186
175
|
|
|
187
|
-
export function createCompactionSummaryMessage(
|
|
188
|
-
summary: string,
|
|
189
|
-
tokensBefore: number,
|
|
190
|
-
timestamp: string,
|
|
191
|
-
shortSummary?: string,
|
|
192
|
-
): CompactionSummaryMessage {
|
|
193
|
-
return {
|
|
194
|
-
role: "compactionSummary",
|
|
195
|
-
summary,
|
|
196
|
-
shortSummary,
|
|
197
|
-
tokensBefore,
|
|
198
|
-
timestamp: new Date(timestamp).getTime(),
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
176
|
/** Convert CustomMessageEntry to AgentMessage format */
|
|
203
177
|
export function createCustomMessage(
|
|
204
178
|
customType: string,
|
|
@@ -267,17 +241,6 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
|
|
|
267
241
|
],
|
|
268
242
|
timestamp: m.timestamp,
|
|
269
243
|
};
|
|
270
|
-
case "compactionSummary":
|
|
271
|
-
return {
|
|
272
|
-
role: "user",
|
|
273
|
-
content: [
|
|
274
|
-
{
|
|
275
|
-
type: "text" as const,
|
|
276
|
-
text: renderPromptTemplate(COMPACTION_SUMMARY_TEMPLATE, { summary: m.summary }),
|
|
277
|
-
},
|
|
278
|
-
],
|
|
279
|
-
timestamp: m.timestamp,
|
|
280
|
-
};
|
|
281
244
|
case "fileMention": {
|
|
282
245
|
const fileContents = m.files
|
|
283
246
|
.map(file => {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Matches: overloaded, rate limit, usage limit, 429, 5xx, connection errors.
|
|
9
9
|
*/
|
|
10
10
|
export function isRetryableErrorMessage(errorMessage: string): boolean {
|
|
11
|
-
return /overloaded|rate.?limit|usage.?limit|too many requests|429|500|502|503|504|service.?unavailable|server error|internal error|connection.?error|unable to connect|fetch failed|retry delay/i.test(
|
|
11
|
+
return /overloaded|rate.?limit|usage.?limit|too many requests|429|500|502|503|504|service.?unavailable|server error|internal error|connection.?error|unable to connect|fetch failed|retry delay|json parse error/i.test(
|
|
12
12
|
errorMessage,
|
|
13
13
|
);
|
|
14
14
|
}
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
type BashExecutionMessage,
|
|
12
12
|
type CustomMessage,
|
|
13
13
|
createBranchSummaryMessage,
|
|
14
|
-
createCompactionSummaryMessage,
|
|
15
14
|
createCustomMessage,
|
|
16
15
|
type FileMentionMessage,
|
|
17
16
|
type HookMessage,
|
|
@@ -24,7 +23,6 @@ export * from "./session-types";
|
|
|
24
23
|
|
|
25
24
|
import {
|
|
26
25
|
type BranchSummaryEntry,
|
|
27
|
-
type CompactionEntry,
|
|
28
26
|
CURRENT_SESSION_VERSION,
|
|
29
27
|
type CustomEntry,
|
|
30
28
|
type CustomMessageEntry,
|
|
@@ -85,18 +83,6 @@ function migrateV1ToV2(entries: FileEntry[]): void {
|
|
|
85
83
|
entry.id = generateId(ids);
|
|
86
84
|
entry.parentId = prevId;
|
|
87
85
|
prevId = entry.id;
|
|
88
|
-
|
|
89
|
-
// Convert firstKeptEntryIndex to firstKeptEntryId for compaction
|
|
90
|
-
if (entry.type === "compaction") {
|
|
91
|
-
const comp = entry as CompactionEntry & { firstKeptEntryIndex?: number };
|
|
92
|
-
if (typeof comp.firstKeptEntryIndex === "number") {
|
|
93
|
-
const targetEntry = entries[comp.firstKeptEntryIndex];
|
|
94
|
-
if (targetEntry && targetEntry.type !== "session") {
|
|
95
|
-
comp.firstKeptEntryId = targetEntry.id;
|
|
96
|
-
}
|
|
97
|
-
delete comp.firstKeptEntryIndex;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
86
|
}
|
|
101
87
|
}
|
|
102
88
|
|
|
@@ -199,20 +185,10 @@ function migrateHomeSessionDirs(): void {
|
|
|
199
185
|
}
|
|
200
186
|
}
|
|
201
187
|
|
|
202
|
-
/** Exported for compaction.test.ts */
|
|
203
188
|
export function parseSessionEntries(content: string): FileEntry[] {
|
|
204
189
|
return parseJsonlLenient<FileEntry>(content);
|
|
205
190
|
}
|
|
206
191
|
|
|
207
|
-
export function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEntry | null {
|
|
208
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
209
|
-
if (entries[i].type === "compaction") {
|
|
210
|
-
return entries[i] as CompactionEntry;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
192
|
function toError(value: unknown): Error {
|
|
217
193
|
return value instanceof Error ? value : new Error(String(value));
|
|
218
194
|
}
|
|
@@ -220,7 +196,7 @@ function toError(value: unknown): Error {
|
|
|
220
196
|
/**
|
|
221
197
|
* Build the session context from entries using tree traversal.
|
|
222
198
|
* If leafId is provided, walks from that entry to root.
|
|
223
|
-
* Handles
|
|
199
|
+
* Handles branch summaries along the path.
|
|
224
200
|
*/
|
|
225
201
|
export function buildSessionContext(
|
|
226
202
|
entries: SessionEntry[],
|
|
@@ -261,10 +237,9 @@ export function buildSessionContext(
|
|
|
261
237
|
current = current.parentId ? byId.get(current.parentId) : undefined;
|
|
262
238
|
}
|
|
263
239
|
|
|
264
|
-
// Extract settings
|
|
240
|
+
// Extract settings
|
|
265
241
|
let thinkingLevel = "off";
|
|
266
242
|
const models: Record<string, string> = {};
|
|
267
|
-
let compaction: CompactionEntry | null = null;
|
|
268
243
|
const injectedTtsrRulesSet = new Set<string>();
|
|
269
244
|
let mode = "none";
|
|
270
245
|
let modeData: Record<string, unknown> | undefined;
|
|
@@ -281,8 +256,6 @@ export function buildSessionContext(
|
|
|
281
256
|
} else if (entry.type === "message" && entry.message.role === "assistant") {
|
|
282
257
|
// Infer default model from assistant messages
|
|
283
258
|
models.default = `${entry.message.provider}/${entry.message.model}`;
|
|
284
|
-
} else if (entry.type === "compaction") {
|
|
285
|
-
compaction = entry;
|
|
286
259
|
} else if (entry.type === "ttsr_injection") {
|
|
287
260
|
// Collect injected TTSR rule names
|
|
288
261
|
for (const ruleName of entry.injectedRules) {
|
|
@@ -296,14 +269,10 @@ export function buildSessionContext(
|
|
|
296
269
|
|
|
297
270
|
const injectedTtsrRules = Array.from(injectedTtsrRulesSet);
|
|
298
271
|
|
|
299
|
-
// Build messages
|
|
300
|
-
// When there's a compaction, we need to:
|
|
301
|
-
// 1. Emit summary first (entry = compaction)
|
|
302
|
-
// 2. Emit kept messages (from firstKeptEntryId up to compaction)
|
|
303
|
-
// 3. Emit messages after compaction
|
|
272
|
+
// Build messages
|
|
304
273
|
const messages: AgentMessage[] = [];
|
|
305
274
|
|
|
306
|
-
const
|
|
275
|
+
for (const entry of path) {
|
|
307
276
|
if (entry.type === "message") {
|
|
308
277
|
messages.push(entry.message);
|
|
309
278
|
} else if (entry.type === "custom_message") {
|
|
@@ -313,44 +282,6 @@ export function buildSessionContext(
|
|
|
313
282
|
} else if (entry.type === "branch_summary" && entry.summary) {
|
|
314
283
|
messages.push(createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp));
|
|
315
284
|
}
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
if (compaction) {
|
|
319
|
-
// Emit summary first
|
|
320
|
-
messages.push(
|
|
321
|
-
createCompactionSummaryMessage(
|
|
322
|
-
compaction.summary,
|
|
323
|
-
compaction.tokensBefore,
|
|
324
|
-
compaction.timestamp,
|
|
325
|
-
compaction.shortSummary,
|
|
326
|
-
),
|
|
327
|
-
);
|
|
328
|
-
|
|
329
|
-
// Find compaction index in path
|
|
330
|
-
const compactionIdx = path.findIndex(e => e.type === "compaction" && e.id === compaction.id);
|
|
331
|
-
|
|
332
|
-
// Emit kept messages (before compaction, starting from firstKeptEntryId)
|
|
333
|
-
let foundFirstKept = false;
|
|
334
|
-
for (let i = 0; i < compactionIdx; i++) {
|
|
335
|
-
const entry = path[i];
|
|
336
|
-
if (entry.id === compaction.firstKeptEntryId) {
|
|
337
|
-
foundFirstKept = true;
|
|
338
|
-
}
|
|
339
|
-
if (foundFirstKept) {
|
|
340
|
-
appendMessage(entry);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Emit messages after compaction
|
|
345
|
-
for (let i = compactionIdx + 1; i < path.length; i++) {
|
|
346
|
-
const entry = path[i];
|
|
347
|
-
appendMessage(entry);
|
|
348
|
-
}
|
|
349
|
-
} else {
|
|
350
|
-
// No compaction - emit all messages, handle branch summaries and custom messages
|
|
351
|
-
for (const entry of path) {
|
|
352
|
-
appendMessage(entry);
|
|
353
|
-
}
|
|
354
285
|
}
|
|
355
286
|
|
|
356
287
|
return { messages, thinkingLevel, models, injectedTtsrRules, mode, modeData };
|
|
@@ -858,7 +789,7 @@ export async function getRecentSessions(
|
|
|
858
789
|
* modifying history.
|
|
859
790
|
*
|
|
860
791
|
* Use buildSessionContext() to get the resolved message list for the LLM, which
|
|
861
|
-
* handles
|
|
792
|
+
* handles branch summaries and follows the path from root to current leaf.
|
|
862
793
|
*/
|
|
863
794
|
export interface UsageStatistics {
|
|
864
795
|
input: number;
|
|
@@ -908,10 +839,6 @@ async function collectSessionsFromFiles(files: string[], storage: SessionStorage
|
|
|
908
839
|
for (let i = 1; i < entries.length; i++) {
|
|
909
840
|
const entry = entries[i] as { type?: string; message?: Message; shortSummary?: string };
|
|
910
841
|
|
|
911
|
-
if (entry.type === "compaction" && typeof entry.shortSummary === "string") {
|
|
912
|
-
shortSummary = entry.shortSummary;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
842
|
if (entry.type === "message" && entry.message) {
|
|
916
843
|
messageCount++;
|
|
917
844
|
|
|
@@ -1448,10 +1375,10 @@ export class SessionManager {
|
|
|
1448
1375
|
}
|
|
1449
1376
|
|
|
1450
1377
|
/** Append a message as child of current leaf, then advance leaf. Returns entry id.
|
|
1451
|
-
* Does not allow writing
|
|
1378
|
+
* Does not allow writing BranchSummaryMessage directly.
|
|
1452
1379
|
* Reason: we want these to be top-level entries in the session, not message session entries,
|
|
1453
1380
|
* so it is easier to find them.
|
|
1454
|
-
* These need to be appended via
|
|
1381
|
+
* These need to be appended via appendBranchSummary() methods.
|
|
1455
1382
|
*/
|
|
1456
1383
|
appendMessage(
|
|
1457
1384
|
message:
|
|
@@ -1531,33 +1458,6 @@ export class SessionManager {
|
|
|
1531
1458
|
return entry.id;
|
|
1532
1459
|
}
|
|
1533
1460
|
|
|
1534
|
-
/** Append a compaction summary as child of current leaf, then advance leaf. Returns entry id. */
|
|
1535
|
-
appendCompaction<T = unknown>(
|
|
1536
|
-
summary: string,
|
|
1537
|
-
shortSummary: string | undefined,
|
|
1538
|
-
firstKeptEntryId: string,
|
|
1539
|
-
tokensBefore: number,
|
|
1540
|
-
details?: T,
|
|
1541
|
-
fromExtension?: boolean,
|
|
1542
|
-
preserveData?: Record<string, unknown>,
|
|
1543
|
-
): string {
|
|
1544
|
-
const entry: CompactionEntry<T> = {
|
|
1545
|
-
type: "compaction",
|
|
1546
|
-
id: generateId(this.#byId),
|
|
1547
|
-
parentId: this.#leafId,
|
|
1548
|
-
timestamp: new Date().toISOString(),
|
|
1549
|
-
summary,
|
|
1550
|
-
shortSummary,
|
|
1551
|
-
firstKeptEntryId,
|
|
1552
|
-
tokensBefore,
|
|
1553
|
-
details,
|
|
1554
|
-
fromExtension,
|
|
1555
|
-
preserveData,
|
|
1556
|
-
};
|
|
1557
|
-
this.#appendEntry(entry);
|
|
1558
|
-
return entry.id;
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
1461
|
/** Append a custom entry (for extensions) as child of current leaf, then advance leaf. Returns entry id. */
|
|
1562
1462
|
appendCustomEntry(customType: string, data?: unknown): string {
|
|
1563
1463
|
const entry: CustomEntry = {
|
|
@@ -1726,7 +1626,7 @@ export class SessionManager {
|
|
|
1726
1626
|
|
|
1727
1627
|
/**
|
|
1728
1628
|
* Walk from entry to root, returning all entries in path order.
|
|
1729
|
-
* Includes all entry types (messages,
|
|
1629
|
+
* Includes all entry types (messages, model changes, etc.).
|
|
1730
1630
|
* Use buildSessionContext() to get the resolved messages for the LLM.
|
|
1731
1631
|
*/
|
|
1732
1632
|
getBranch(fromId?: string): SessionEntry[] {
|
|
@@ -11,7 +11,6 @@ import type { Skill, SkillWarning } from "../extensibility/skills";
|
|
|
11
11
|
import type { FileSlashCommand } from "../extensibility/slash-commands";
|
|
12
12
|
import type { SecretObfuscator } from "../secrets/obfuscator";
|
|
13
13
|
import type { TodoItem } from "../tools/todo-write";
|
|
14
|
-
import type { CompactionResult } from "./compaction";
|
|
15
14
|
import type { SessionManager } from "./session-manager";
|
|
16
15
|
|
|
17
16
|
export const CURRENT_SESSION_VERSION = 3;
|
|
@@ -55,20 +54,6 @@ export interface ModelChangeEntry extends SessionEntryBase {
|
|
|
55
54
|
role?: string;
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
export interface CompactionEntry<T = unknown> extends SessionEntryBase {
|
|
59
|
-
type: "compaction";
|
|
60
|
-
summary: string;
|
|
61
|
-
shortSummary?: string;
|
|
62
|
-
firstKeptEntryId: string;
|
|
63
|
-
tokensBefore: number;
|
|
64
|
-
/** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */
|
|
65
|
-
details?: T;
|
|
66
|
-
/** Hook-provided data to persist across compaction */
|
|
67
|
-
preserveData?: Record<string, unknown>;
|
|
68
|
-
/** True if generated by an extension, undefined/false if pi-generated (backward compatible) */
|
|
69
|
-
fromExtension?: boolean;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
57
|
export interface BranchSummaryEntry<T = unknown> extends SessionEntryBase {
|
|
73
58
|
type: "branch_summary";
|
|
74
59
|
fromId: string;
|
|
@@ -154,7 +139,6 @@ export type SessionEntry =
|
|
|
154
139
|
| SessionMessageEntry
|
|
155
140
|
| ThinkingLevelChangeEntry
|
|
156
141
|
| ModelChangeEntry
|
|
157
|
-
| CompactionEntry
|
|
158
142
|
| BranchSummaryEntry
|
|
159
143
|
| CustomEntry
|
|
160
144
|
| CustomMessageEntry
|
|
@@ -209,18 +193,13 @@ export interface SessionInfo {
|
|
|
209
193
|
/** Session-specific events that extend the core AgentEvent */
|
|
210
194
|
export type AgentSessionEvent =
|
|
211
195
|
| AgentEvent
|
|
212
|
-
| { type: "auto_compaction_start"; reason: "threshold" | "overflow" }
|
|
213
|
-
| {
|
|
214
|
-
type: "auto_compaction_end";
|
|
215
|
-
result: CompactionResult | undefined;
|
|
216
|
-
aborted: boolean;
|
|
217
|
-
willRetry: boolean;
|
|
218
|
-
errorMessage?: string;
|
|
219
|
-
}
|
|
220
196
|
| { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
|
|
221
197
|
| { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }
|
|
222
198
|
| { type: "ttsr_triggered"; rules: Rule[] }
|
|
223
|
-
| { type: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number }
|
|
199
|
+
| { type: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number }
|
|
200
|
+
| { type: "context_warning"; percent: number; tokens: number; contextWindow: number }
|
|
201
|
+
| { type: "auto_handoff_start"; percent: number }
|
|
202
|
+
| { type: "auto_handoff_end"; success: boolean; error?: string };
|
|
224
203
|
|
|
225
204
|
/** Listener function for agent session events */
|
|
226
205
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
package/src/session/stats.ts
CHANGED
|
@@ -5,9 +5,8 @@ import { exportSessionToHtml } from "../export/html";
|
|
|
5
5
|
import type { ContextUsage } from "../extensibility/extensions/types";
|
|
6
6
|
import { getCurrentThemeName } from "../theme/theme";
|
|
7
7
|
import { calculateContextTokens, estimateTokens } from "./compaction";
|
|
8
|
-
import type {
|
|
8
|
+
import type { FileMentionMessage } from "./messages";
|
|
9
9
|
import type { SessionManager } from "./session-manager";
|
|
10
|
-
import { getLatestCompactionEntry } from "./session-manager";
|
|
11
10
|
import type { SessionStats } from "./session-types";
|
|
12
11
|
|
|
13
12
|
/**
|
|
@@ -82,42 +81,12 @@ export function getSessionStats(
|
|
|
82
81
|
/**
|
|
83
82
|
* Get current context usage statistics.
|
|
84
83
|
*/
|
|
85
|
-
export function getContextUsage(
|
|
86
|
-
model: Model | undefined,
|
|
87
|
-
messages: AgentMessage[],
|
|
88
|
-
sessionManager: SessionManager,
|
|
89
|
-
): ContextUsage | undefined {
|
|
84
|
+
export function getContextUsage(model: Model | undefined, messages: AgentMessage[]): ContextUsage | undefined {
|
|
90
85
|
if (!model) return undefined;
|
|
91
86
|
|
|
92
87
|
const contextWindow = model.contextWindow ?? 0;
|
|
93
88
|
if (contextWindow <= 0) return undefined;
|
|
94
89
|
|
|
95
|
-
const branchEntries = sessionManager.getBranch();
|
|
96
|
-
const latestCompaction = getLatestCompactionEntry(branchEntries);
|
|
97
|
-
|
|
98
|
-
if (latestCompaction) {
|
|
99
|
-
const compactionIndex = branchEntries.lastIndexOf(latestCompaction);
|
|
100
|
-
let hasPostCompactionUsage = false;
|
|
101
|
-
for (let i = compactionIndex + 1; i < branchEntries.length; i++) {
|
|
102
|
-
const entry = branchEntries[i];
|
|
103
|
-
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
104
|
-
const msg = entry.message as AssistantMessage;
|
|
105
|
-
if (msg.usage) {
|
|
106
|
-
hasPostCompactionUsage = true;
|
|
107
|
-
break;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (!hasPostCompactionUsage) {
|
|
113
|
-
return {
|
|
114
|
-
tokens: 0,
|
|
115
|
-
contextWindow,
|
|
116
|
-
percent: 0,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
90
|
const { tokens } = estimateContextTokensFromMessages(messages);
|
|
122
91
|
const percent = Math.round((tokens / contextWindow) * 100);
|
|
123
92
|
|
|
@@ -374,12 +343,6 @@ export function formatCompactContext(messages: AgentMessage[]): string {
|
|
|
374
343
|
const paths = fileMsg.files.map(f => f.path).join(", ");
|
|
375
344
|
lines.push(`[Files referenced: ${paths}]`);
|
|
376
345
|
lines.push("");
|
|
377
|
-
} else if (msg.role === "compactionSummary") {
|
|
378
|
-
const compactMsg = msg as CompactionSummaryMessage;
|
|
379
|
-
lines.push("## Earlier Context (Summarized)");
|
|
380
|
-
lines.push("");
|
|
381
|
-
lines.push(compactMsg.summary);
|
|
382
|
-
lines.push("");
|
|
383
346
|
}
|
|
384
347
|
}
|
|
385
348
|
|
|
@@ -302,17 +302,6 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
302
302
|
await runtime.ctx.handleClearCommand();
|
|
303
303
|
},
|
|
304
304
|
},
|
|
305
|
-
{
|
|
306
|
-
name: "compact",
|
|
307
|
-
description: "Manually compact the session context",
|
|
308
|
-
inlineHint: "[focus instructions]",
|
|
309
|
-
allowArgs: true,
|
|
310
|
-
handle: async (command, runtime) => {
|
|
311
|
-
const customInstructions = command.args || undefined;
|
|
312
|
-
runtime.ctx.editor.setText("");
|
|
313
|
-
await runtime.ctx.handleCompactCommand(customInstructions);
|
|
314
|
-
},
|
|
315
|
-
},
|
|
316
305
|
{
|
|
317
306
|
name: "handoff",
|
|
318
307
|
description: "Hand off session context to a new session",
|
package/src/task/executor.ts
CHANGED
|
@@ -417,14 +417,6 @@ export async function runAgent(options: ExecutorOptions): Promise<SingleResult>
|
|
|
417
417
|
shutdown: () => {},
|
|
418
418
|
getContextUsage: () => session.getContextUsage(),
|
|
419
419
|
getSystemPrompt: () => session.systemPrompt,
|
|
420
|
-
compact: async instructionsOrOptions => {
|
|
421
|
-
const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
|
|
422
|
-
const compactOptions =
|
|
423
|
-
instructionsOrOptions && typeof instructionsOrOptions === "object"
|
|
424
|
-
? instructionsOrOptions
|
|
425
|
-
: undefined;
|
|
426
|
-
await session.compact(instructions, compactOptions);
|
|
427
|
-
},
|
|
428
420
|
},
|
|
429
421
|
);
|
|
430
422
|
await extensionRunner.emit({ type: "session_start" });
|
|
@@ -13,6 +13,7 @@ import { FetchTool } from "./fetch";
|
|
|
13
13
|
import { FindTool } from "./find";
|
|
14
14
|
import { FindThreadTool } from "./find-thread";
|
|
15
15
|
import { GitHubTool } from "./github";
|
|
16
|
+
import { GitHubFsTool } from "./github-fs";
|
|
16
17
|
import { GrepTool } from "./grep";
|
|
17
18
|
import type { ToolSession } from "./index";
|
|
18
19
|
import { librarianConfig } from "./librarian";
|
|
@@ -44,6 +45,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
44
45
|
find: s => new FindTool(s),
|
|
45
46
|
explore: s => new SubagentTool(s, exploreConfig),
|
|
46
47
|
github: s => new GitHubTool(s),
|
|
48
|
+
github_fs: s => new GitHubFsTool(s),
|
|
47
49
|
grep: s => new GrepTool(s),
|
|
48
50
|
librarian: s => new SubagentTool(s, librarianConfig),
|
|
49
51
|
lsp: LspTool.createIf,
|
|
@@ -132,6 +134,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
132
134
|
if (name === "librarian") return session.settings.get("librarian.enabled");
|
|
133
135
|
if (name === "oracle") return session.settings.get("oracle.enabled");
|
|
134
136
|
if (name === "github") return session.settings.get("github.enabled");
|
|
137
|
+
if (name === "github_fs") return session.isSubagent && session.settings.get("github.enabled");
|
|
135
138
|
if (name === "search_code") return session.isSubagent;
|
|
136
139
|
if (name === "task") {
|
|
137
140
|
return !session.isSubagent;
|