@dex-ai/context 0.7.16
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/README.md +204 -0
- package/package.json +36 -0
- package/src/event-log.ts +246 -0
- package/src/extension.ts +1271 -0
- package/src/formatter.ts +127 -0
- package/src/index.ts +45 -0
- package/src/pressure.ts +61 -0
- package/src/search-tool.ts +230 -0
- package/src/snapshot.ts +240 -0
- package/src/store.ts +678 -0
- package/src/summarize.ts +206 -0
- package/src/tokenizer.ts +20 -0
- package/src/tracker.ts +159 -0
- package/src/types.ts +100 -0
package/src/summarize.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Result Summarization — converts tool results to compact 1-line summaries.
|
|
3
|
+
*
|
|
4
|
+
* Used in the warm zone to preserve the "what happened" without the raw data.
|
|
5
|
+
* Assistant messages are kept intact — only tool results get summarized.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Message, Content, ToolOutput } from "@dex-ai/sdk";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Summarize all tool results in a message to compact 1-liners.
|
|
12
|
+
* Returns a new message with tool-result content parts replaced by text summaries.
|
|
13
|
+
* Non-tool-result parts (text, reasoning) are preserved intact.
|
|
14
|
+
*/
|
|
15
|
+
export function summarizeToolResults(msg: Message): Message {
|
|
16
|
+
const newContent: Content[] = [];
|
|
17
|
+
|
|
18
|
+
for (const part of msg.content) {
|
|
19
|
+
if (part.type === "tool-result") {
|
|
20
|
+
// Replace with a compact text summary
|
|
21
|
+
const summary = summarizeResult(
|
|
22
|
+
part.toolName,
|
|
23
|
+
(part as any).input,
|
|
24
|
+
part.output,
|
|
25
|
+
);
|
|
26
|
+
newContent.push({
|
|
27
|
+
type: "tool-result",
|
|
28
|
+
toolCallId: part.toolCallId,
|
|
29
|
+
toolName: part.toolName,
|
|
30
|
+
output: { type: "text", value: summary },
|
|
31
|
+
} as Content);
|
|
32
|
+
} else {
|
|
33
|
+
newContent.push(part);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { ...msg, content: newContent };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Generate a 1-line summary for a tool result.
|
|
42
|
+
*/
|
|
43
|
+
function summarizeResult(
|
|
44
|
+
toolName: string,
|
|
45
|
+
input: Record<string, unknown> | undefined,
|
|
46
|
+
output: ToolOutput,
|
|
47
|
+
): string {
|
|
48
|
+
const text = extractText(output);
|
|
49
|
+
const isError =
|
|
50
|
+
output.type === "error-text" || output.type === "error-json";
|
|
51
|
+
|
|
52
|
+
if (isError) {
|
|
53
|
+
const errorLine = text ? firstMeaningfulLine(text) : "unknown error";
|
|
54
|
+
return `[${toolName} ERROR: ${truncate(errorLine, 80)}]`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
switch (toolName) {
|
|
58
|
+
case "read": {
|
|
59
|
+
const path = str(input?.path);
|
|
60
|
+
const lineCount = text ? text.split("\n").length : 0;
|
|
61
|
+
const range = formatRange(input);
|
|
62
|
+
return `[read ${path}${range}: ${lineCount} lines]`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
case "write": {
|
|
66
|
+
const path = str(input?.path);
|
|
67
|
+
return `[wrote ${path}]`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
case "edit": {
|
|
71
|
+
const path = str(input?.path);
|
|
72
|
+
const edits = Array.isArray(input?.edits) ? input.edits.length : 1;
|
|
73
|
+
return `[edited ${path}: ${edits} edit${edits !== 1 ? "s" : ""}]`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
case "bash": {
|
|
77
|
+
const cmd = truncate(str(input?.command), 50);
|
|
78
|
+
const outcome = summarizeBashOutput(text);
|
|
79
|
+
return `[bash: ${cmd}${outcome}]`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
case "search": {
|
|
83
|
+
const mode = str(input?.mode) || "grep";
|
|
84
|
+
const pattern = str(input?.pattern);
|
|
85
|
+
const matchCount = text ? text.split("\n").filter((l) => l.trim()).length : 0;
|
|
86
|
+
return `[search ${mode} "${truncate(pattern, 30)}": ${matchCount} results]`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
case "ast_grep_search": {
|
|
90
|
+
const pattern = str(input?.pattern);
|
|
91
|
+
const matchCount = text ? (text.match(/\d+ match/)?.[0] ?? `${text.split("\n").filter((l) => l.trim()).length} lines`) : "0 results";
|
|
92
|
+
return `[ast-grep "${truncate(pattern, 30)}": ${matchCount}]`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case "ast_grep_replace": {
|
|
96
|
+
const pattern = str(input?.pattern);
|
|
97
|
+
const applied = input?.apply ? "applied" : "dry-run";
|
|
98
|
+
return `[ast-grep replace "${truncate(pattern, 30)}": ${applied}]`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
case "lsp_navigation": {
|
|
102
|
+
const op = str(input?.operation);
|
|
103
|
+
const file = str(input?.filePath);
|
|
104
|
+
return `[lsp:${op} ${basename(file)}]`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case "lsp_diagnostics": {
|
|
108
|
+
const file = str(input?.filePath || (input?.filePaths as any)?.[0]);
|
|
109
|
+
const diagCount = text ? text.split("\n").filter((l) => l.trim()).length : 0;
|
|
110
|
+
return `[lsp:diagnostics ${basename(file)}: ${diagCount} issues]`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case "cd": {
|
|
114
|
+
const path = str(input?.path);
|
|
115
|
+
return `[cd ${path}]`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
default: {
|
|
119
|
+
// Generic: show tool name + output size
|
|
120
|
+
const lines = text ? text.split("\n").length : 0;
|
|
121
|
+
return `[${toolName}: ${lines} lines output]`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* ── Helpers ───────────────────────────────────────────── */
|
|
127
|
+
|
|
128
|
+
function extractText(output: ToolOutput): string | null {
|
|
129
|
+
switch (output.type) {
|
|
130
|
+
case "text":
|
|
131
|
+
case "error-text":
|
|
132
|
+
return output.value;
|
|
133
|
+
case "json":
|
|
134
|
+
case "error-json":
|
|
135
|
+
return JSON.stringify(output.value, null, 2);
|
|
136
|
+
case "content":
|
|
137
|
+
return (
|
|
138
|
+
output.value
|
|
139
|
+
.filter((p) => p.type === "text")
|
|
140
|
+
.map((p) => (p as any).text)
|
|
141
|
+
.join("\n") || null
|
|
142
|
+
);
|
|
143
|
+
default:
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function str(v: unknown): string {
|
|
149
|
+
return typeof v === "string" ? v : "";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function truncate(s: string, max: number): string {
|
|
153
|
+
const cleaned = s.replace(/\s+/g, " ").trim();
|
|
154
|
+
return cleaned.length > max ? cleaned.slice(0, max - 1) + "…" : cleaned;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function basename(path: string): string {
|
|
158
|
+
if (!path) return "";
|
|
159
|
+
const parts = path.split("/");
|
|
160
|
+
return parts[parts.length - 1] || path;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function formatRange(input: Record<string, unknown> | undefined): string {
|
|
164
|
+
if (!input) return "";
|
|
165
|
+
const start = input.lineStart as number | undefined;
|
|
166
|
+
const end = input.lineEnd as number | undefined;
|
|
167
|
+
if (start && end) return ` L${start}-${end}`;
|
|
168
|
+
if (start) return ` L${start}+`;
|
|
169
|
+
return "";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function summarizeBashOutput(text: string | null): string {
|
|
173
|
+
if (!text) return "";
|
|
174
|
+
|
|
175
|
+
// Test results
|
|
176
|
+
const passMatch = text.match(/(\d+)\s*pass(?:ed|ing)?/i);
|
|
177
|
+
const failMatch = text.match(/(\d+)\s*fail(?:ed|ing|ure)?/i);
|
|
178
|
+
if (passMatch || failMatch) {
|
|
179
|
+
const parts: string[] = [];
|
|
180
|
+
if (passMatch) parts.push(`${passMatch[1]} pass`);
|
|
181
|
+
if (failMatch) parts.push(`${failMatch[1]} fail`);
|
|
182
|
+
return ` → ${parts.join(", ")}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Non-zero exit
|
|
186
|
+
const exitMatch = text.match(/\[exit code: ([1-9]\d*)\]/);
|
|
187
|
+
if (exitMatch) return ` → exit=${exitMatch[1]}`;
|
|
188
|
+
|
|
189
|
+
// Short output
|
|
190
|
+
const trimmed = text.trim();
|
|
191
|
+
const lines = trimmed.split("\n");
|
|
192
|
+
if (lines.length <= 2 && trimmed.length < 60) {
|
|
193
|
+
return ` → ${trimmed}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return ` (${lines.length} lines)`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function firstMeaningfulLine(text: string): string {
|
|
200
|
+
const lines = text.split("\n");
|
|
201
|
+
for (const line of lines) {
|
|
202
|
+
const trimmed = line.trim();
|
|
203
|
+
if (trimmed && trimmed.length > 5) return trimmed;
|
|
204
|
+
}
|
|
205
|
+
return lines[0]?.trim() || "";
|
|
206
|
+
}
|
package/src/tokenizer.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fast token estimation. Calibrated against cl100k_base.
|
|
3
|
+
* ±10% accuracy — sufficient for budget tracking.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function estimateTokens(text: string): number {
|
|
7
|
+
if (!text) return 0;
|
|
8
|
+
// Detect content density from first 500 chars
|
|
9
|
+
const sample = text.slice(0, 500);
|
|
10
|
+
const symbols = (sample.match(/[{}[\]()=;:,<>|&!@#$%^*~`"']/g) || []).length;
|
|
11
|
+
const ratio = symbols / Math.max(sample.length, 1);
|
|
12
|
+
const charsPerToken = ratio > 0.12 ? 3.2 : ratio > 0.06 ? 3.6 : 4.0;
|
|
13
|
+
return Math.ceil(text.length / charsPerToken);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function estimateJsonTokens(value: unknown): number {
|
|
17
|
+
if (value == null) return 1;
|
|
18
|
+
const json = typeof value === "string" ? value : JSON.stringify(value);
|
|
19
|
+
return Math.ceil(json.length / 3);
|
|
20
|
+
}
|
package/src/tracker.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context tracker — computes token usage across categories.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Message, Content, ToolOutput, AnyTool } from "@dex-ai/sdk";
|
|
6
|
+
import type { ContextSnapshot, ContextCategory, CategoryUsage } from "./types";
|
|
7
|
+
import { estimateTokens, estimateJsonTokens } from "./tokenizer";
|
|
8
|
+
|
|
9
|
+
export class Tracker {
|
|
10
|
+
private maxTokens: number;
|
|
11
|
+
private _tokensSaved = 0;
|
|
12
|
+
private _compressions = 0;
|
|
13
|
+
|
|
14
|
+
constructor(maxTokens: number) {
|
|
15
|
+
this.maxTokens = maxTokens;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
setMaxTokens(n: number): void {
|
|
19
|
+
this.maxTokens = n;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
addSaved(tokens: number, _type: "compress"): void {
|
|
23
|
+
this._tokensSaved += tokens;
|
|
24
|
+
this._compressions++;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Compute a full snapshot from current messages + tools + system prompt.
|
|
29
|
+
*/
|
|
30
|
+
compute(
|
|
31
|
+
messages: ReadonlyArray<Message>,
|
|
32
|
+
tools: ReadonlyArray<AnyTool>,
|
|
33
|
+
systemPrompt?: string,
|
|
34
|
+
): ContextSnapshot {
|
|
35
|
+
return this._compute(messages, tools, systemPrompt);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Compute snapshot from an arbitrary message array (e.g., transformed request).
|
|
40
|
+
*/
|
|
41
|
+
computeFromMessages(
|
|
42
|
+
messages: ReadonlyArray<Message>,
|
|
43
|
+
tools: ReadonlyArray<AnyTool>,
|
|
44
|
+
systemPrompt?: string,
|
|
45
|
+
): ContextSnapshot {
|
|
46
|
+
return this._compute(messages, tools, systemPrompt);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private _compute(
|
|
50
|
+
messages: ReadonlyArray<Message>,
|
|
51
|
+
tools: ReadonlyArray<AnyTool>,
|
|
52
|
+
systemPrompt?: string,
|
|
53
|
+
): ContextSnapshot {
|
|
54
|
+
const systemPromptTokens = systemPrompt ? estimateTokens(systemPrompt) : 0;
|
|
55
|
+
let systemToolsTokens = 0;
|
|
56
|
+
let messagesTokens = 0;
|
|
57
|
+
let toolCallsTokens = 0;
|
|
58
|
+
let toolResultsTokens = 0;
|
|
59
|
+
let imagesTokens = 0;
|
|
60
|
+
let filesTokens = 0;
|
|
61
|
+
let reasoningTokens = 0;
|
|
62
|
+
|
|
63
|
+
// Tool definitions
|
|
64
|
+
for (const tool of tools) {
|
|
65
|
+
systemToolsTokens += estimateTokens(
|
|
66
|
+
`${tool.name}${tool.description ?? ""}`,
|
|
67
|
+
);
|
|
68
|
+
systemToolsTokens += estimateJsonTokens(tool.parameters);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Messages
|
|
72
|
+
for (const msg of messages) {
|
|
73
|
+
for (const part of msg.content) {
|
|
74
|
+
switch (part.type) {
|
|
75
|
+
case "text":
|
|
76
|
+
messagesTokens += estimateTokens(part.text);
|
|
77
|
+
break;
|
|
78
|
+
case "reasoning":
|
|
79
|
+
reasoningTokens += estimateTokens(part.text);
|
|
80
|
+
break;
|
|
81
|
+
case "tool-call":
|
|
82
|
+
toolCallsTokens +=
|
|
83
|
+
estimateTokens(part.toolName) + estimateJsonTokens(part.input);
|
|
84
|
+
break;
|
|
85
|
+
case "tool-result":
|
|
86
|
+
toolResultsTokens += outputTokens(part.output);
|
|
87
|
+
break;
|
|
88
|
+
case "image":
|
|
89
|
+
imagesTokens += 1000;
|
|
90
|
+
break;
|
|
91
|
+
case "file":
|
|
92
|
+
filesTokens += 500;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const total =
|
|
99
|
+
systemPromptTokens +
|
|
100
|
+
systemToolsTokens +
|
|
101
|
+
messagesTokens +
|
|
102
|
+
toolCallsTokens +
|
|
103
|
+
toolResultsTokens +
|
|
104
|
+
imagesTokens +
|
|
105
|
+
filesTokens +
|
|
106
|
+
reasoningTokens;
|
|
107
|
+
|
|
108
|
+
const categories: CategoryUsage[] = [];
|
|
109
|
+
const add = (cat: ContextCategory, tokens: number) => {
|
|
110
|
+
if (tokens > 0)
|
|
111
|
+
categories.push({
|
|
112
|
+
category: cat,
|
|
113
|
+
tokens,
|
|
114
|
+
percent: pct(tokens, this.maxTokens),
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
add("system-prompt", systemPromptTokens);
|
|
118
|
+
add("system-tools", systemToolsTokens);
|
|
119
|
+
add("tool-calls", toolCallsTokens);
|
|
120
|
+
add("tool-results", toolResultsTokens);
|
|
121
|
+
add("messages", messagesTokens);
|
|
122
|
+
add("images", imagesTokens);
|
|
123
|
+
add("files", filesTokens);
|
|
124
|
+
add("reasoning", reasoningTokens);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
timestamp: Date.now(),
|
|
128
|
+
totalTokens: total,
|
|
129
|
+
maxTokens: this.maxTokens,
|
|
130
|
+
usagePercent: pct(total, this.maxTokens),
|
|
131
|
+
categories,
|
|
132
|
+
availableTokens: Math.max(0, this.maxTokens - total),
|
|
133
|
+
tokensSaved: this._tokensSaved,
|
|
134
|
+
compressions: this._compressions,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function outputTokens(output: ToolOutput): number {
|
|
140
|
+
switch (output.type) {
|
|
141
|
+
case "text":
|
|
142
|
+
case "error-text":
|
|
143
|
+
return estimateTokens(output.value);
|
|
144
|
+
case "json":
|
|
145
|
+
case "error-json":
|
|
146
|
+
return estimateJsonTokens(output.value);
|
|
147
|
+
case "content":
|
|
148
|
+
return output.value.reduce(
|
|
149
|
+
(sum, p) => sum + (p.type === "text" ? estimateTokens(p.text) : 500),
|
|
150
|
+
0,
|
|
151
|
+
);
|
|
152
|
+
default:
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function pct(n: number, total: number): number {
|
|
158
|
+
return total > 0 ? Math.round((n / total) * 1000) / 10 : 0;
|
|
159
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the context management extension.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Context budget strategy — controls compression aggressiveness.
|
|
7
|
+
*
|
|
8
|
+
* All modes use a fixed 200k context budget and 80k send cap.
|
|
9
|
+
* The tier determines HOW EARLY compression kicks in:
|
|
10
|
+
*
|
|
11
|
+
* - "aggressive" — message-count-based aging with hard delete (most compression, least re-reads)
|
|
12
|
+
* - "strict" — same compression triggers, no hard delete
|
|
13
|
+
* - "balanced" — wider hot zone (more messages kept intact)
|
|
14
|
+
* - "relaxed" — widest hot zone (least compression, for short tasks)
|
|
15
|
+
*/
|
|
16
|
+
export type ContextBudget = "aggressive" | "strict" | "balanced" | "relaxed";
|
|
17
|
+
|
|
18
|
+
export type ContextCategory =
|
|
19
|
+
| "system-prompt"
|
|
20
|
+
| "system-tools"
|
|
21
|
+
| "messages"
|
|
22
|
+
| "tool-calls"
|
|
23
|
+
| "tool-results"
|
|
24
|
+
| "images"
|
|
25
|
+
| "files"
|
|
26
|
+
| "reasoning";
|
|
27
|
+
|
|
28
|
+
export interface ContextSnapshot {
|
|
29
|
+
readonly timestamp: number;
|
|
30
|
+
readonly totalTokens: number;
|
|
31
|
+
readonly maxTokens: number;
|
|
32
|
+
readonly usagePercent: number;
|
|
33
|
+
readonly categories: ReadonlyArray<CategoryUsage>;
|
|
34
|
+
readonly availableTokens: number;
|
|
35
|
+
/** Tokens saved this session by all mechanisms. */
|
|
36
|
+
readonly tokensSaved: number;
|
|
37
|
+
readonly compressions: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CategoryUsage {
|
|
41
|
+
readonly category: ContextCategory;
|
|
42
|
+
readonly tokens: number;
|
|
43
|
+
readonly percent: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ContextExtensionOptions {
|
|
47
|
+
/**
|
|
48
|
+
* Context budget strategy. Default: "strict"
|
|
49
|
+
*/
|
|
50
|
+
budget?: ContextBudget;
|
|
51
|
+
|
|
52
|
+
/** Explicit context window size. Overrides budget strategy. */
|
|
53
|
+
maxTokens?: number;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Number of recent turns to keep fully intact (all content preserved).
|
|
57
|
+
* Default: 3
|
|
58
|
+
*/
|
|
59
|
+
hotTurns?: number;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Number of turns in the warm zone (tool results summarized, messages kept).
|
|
63
|
+
* Turns older than hotTurns but within hotTurns + warmTurns.
|
|
64
|
+
* Default: 4
|
|
65
|
+
*/
|
|
66
|
+
warmTurns?: number;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Number of messages in the cold zone (legacy, unused in aggressive mode).
|
|
70
|
+
* @deprecated Use generation-aware thresholds in aggressive mode.
|
|
71
|
+
*/
|
|
72
|
+
coldMessages?: number;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Total message count threshold for hard deletion (legacy, unused in aggressive mode).
|
|
76
|
+
* @deprecated Use generation-aware thresholds in aggressive mode.
|
|
77
|
+
*/
|
|
78
|
+
deleteThreshold?: number;
|
|
79
|
+
|
|
80
|
+
/** Inject a context-awareness skill into the system prompt. Default: true */
|
|
81
|
+
injectSkill?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* ── Context Events ────────────────────────────────────── */
|
|
85
|
+
|
|
86
|
+
export type ContextEventType =
|
|
87
|
+
| "compress-warm"
|
|
88
|
+
| "compress-cold"
|
|
89
|
+
| "delete"
|
|
90
|
+
| "snapshot-built"
|
|
91
|
+
| "tier-advance";
|
|
92
|
+
|
|
93
|
+
export interface ContextEvent {
|
|
94
|
+
readonly type: ContextEventType;
|
|
95
|
+
readonly turn: number;
|
|
96
|
+
readonly timestamp: number;
|
|
97
|
+
readonly tokensSaved?: number;
|
|
98
|
+
readonly messagesAffected?: number;
|
|
99
|
+
readonly detail?: string;
|
|
100
|
+
}
|