@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 ADDED
@@ -0,0 +1,204 @@
1
+ # @dex-ai/context
2
+
3
+ Context management extension for Dex — tracks token usage, provides a visual `/context` command, enforces context budgets, and guides the LLM to avoid flooding the context window.
4
+
5
+ ## What It Does
6
+
7
+ ```
8
+ Context Usage
9
+
10
+ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ Total Usage 102k ( 51.1%)
11
+ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■
12
+ ■ ■ ■ ■ ■ ■ ■ ■ □ □ ■ System Prompt 1k ( 0.7%)
13
+ □ □ □ □ □ □ □ □ □ □ ■ System Tools 2k ( 0.9%)
14
+ □ □ □ □ □ □ □ □ □ □ ■ Tool Results 96k ( 48.1%)
15
+ ■ Messages 3k ( 1.5%)
16
+ □ Available 98k ( 48.9%)
17
+ ```
18
+
19
+ ### Features
20
+
21
+ 1. **Real-time context tracking** — estimates token usage across categories (system prompt, tools, messages, tool calls/results, images, files, reasoning) using fast heuristics calibrated against cl100k_base.
22
+
23
+ 2. **`/context` command** — visual grid + percentage breakdown showing exactly where context is being consumed, with color-coded categories.
24
+
25
+ 3. **Threshold warnings** — automatically injects guidance to the LLM when context usage exceeds configurable thresholds (default: warn at 75%, critical at 90%).
26
+
27
+ 4. **Context-awareness skill** — teaches the LLM patterns for keeping outputs small (targeted reads, search-before-read, output truncation).
28
+
29
+ 5. **Tool output tracking** — records per-tool output sizes to identify the biggest context consumers.
30
+
31
+ ## Usage
32
+
33
+ ```typescript
34
+ import { contextExtension } from "@dex-ai/context";
35
+ import { Agent } from "@dex-ai/sdk";
36
+
37
+ const agent = await Agent.create({
38
+ provider: "anthropic",
39
+ model: "claude-sonnet-4-20250514",
40
+ extensions: [
41
+ providerExt,
42
+ contextExtension(), // sensible defaults
43
+ ],
44
+ });
45
+ ```
46
+
47
+ ### With Options
48
+
49
+ ```typescript
50
+ contextExtension({
51
+ maxTokens: 128_000, // override context window (auto-detected from model)
52
+ warnAt: 70, // warn threshold (default: 75%)
53
+ criticalAt: 85, // critical threshold (default: 90%)
54
+ injectGuidance: true, // inject context-awareness skill (default: true)
55
+ trackToolOutputs: true, // track per-tool output sizes (default: true)
56
+ largeOutputThreshold: 4_000, // tokens above which an output is "large"
57
+ });
58
+ ```
59
+
60
+ ## `/context` Command Integration
61
+
62
+ The extension stores accessor functions in `AgentContext.state` that the host (CLI/TUI) can invoke:
63
+
64
+ ```typescript
65
+ // In your host/CLI command handler:
66
+ const getFormatted = agent.context.state.get(
67
+ "context:getFormatted",
68
+ ) as () => string;
69
+ const getPlain = agent.context.state.get("context:getPlain") as () => string;
70
+ const getSnapshot = agent.context.state.get(
71
+ "context:getSnapshot",
72
+ ) as () => ContextSnapshot;
73
+
74
+ // For TUI rendering (ANSI color codes):
75
+ console.log(getFormatted());
76
+
77
+ // For plain text (no ANSI):
78
+ console.log(getPlain());
79
+
80
+ // For programmatic access:
81
+ const snapshot = getSnapshot();
82
+ console.log(
83
+ `${snapshot.usagePercent}% used, ${snapshot.availableTokens} remaining`,
84
+ );
85
+ ```
86
+
87
+ ## How It Works
88
+
89
+ ### Token Estimation
90
+
91
+ Uses character-based heuristics calibrated for modern tokenizers:
92
+
93
+ - English text: ~4 chars per token
94
+ - Code: ~3.5 chars per token
95
+ - JSON/structured: ~3 chars per token
96
+ - Images: Anthropic tile-based estimation
97
+
98
+ This is intentionally approximate (±10%). Real token counts come from the provider's `usage` response and are used to calibrate the estimates.
99
+
100
+ ### Category Tracking
101
+
102
+ | Category | What's counted |
103
+ | ------------- | ---------------------------------------------------- |
104
+ | System Prompt | System messages + injected skills |
105
+ | System Tools | Tool definitions (name + description + JSON Schema) |
106
+ | Tool Calls | Assistant tool invocations (name + serialized input) |
107
+ | Tool Results | Tool outputs (text, JSON, rich content) |
108
+ | Messages | User + assistant text messages |
109
+ | Images | Image content (resolution-based estimation) |
110
+ | Files | File attachments (size-based estimation) |
111
+ | Reasoning | Chain-of-thought / extended thinking |
112
+
113
+ ### Threshold Behavior
114
+
115
+ When context usage exceeds a threshold, the extension injects a concise system message into the next model request:
116
+
117
+ - **Warning (75%)**: `"⚠️ Context usage: 76%. Be concise. Avoid large reads/outputs."`
118
+ - **Critical (90%)**: `"🚨 Context nearly full: 91% used. Complete the current task as concisely as possible."`
119
+
120
+ These are injected once per threshold crossing (not on every model call).
121
+
122
+ ### Context-Awareness Skill
123
+
124
+ When `injectGuidance: true` (default), a skill is added to the system prompt teaching the LLM:
125
+
126
+ - Prefer targeted reads (line ranges) over full file reads
127
+ - Use search before read to find relevant sections
128
+ - Truncate bash output with `head`/`tail`/`grep`
129
+ - Avoid redundant re-reads of files already in context
130
+ - Be more concise when context is running low
131
+
132
+ ## Comparison with context-mode
133
+
134
+ | Feature | context-mode | @dex-ai/context |
135
+ | ------------------- | ---------------------- | ------------------------------------------------- |
136
+ | Token tracking | ❌ (defers to host) | ✅ Built-in estimation |
137
+ | Visual display | ❌ | ✅ Grid + categories |
138
+ | Hard-blocks tools | ✅ (blocks curl/fetch) | ❌ (guidance-based) |
139
+ | Sandboxed execution | ✅ (separate process) | ❌ (not needed — Dex tools already handle this) |
140
+ | FTS5 knowledge base | ✅ (SQLite) | ❌ (out of scope — use @dex-ai/knowledge) |
141
+ | Session persistence | ✅ (SessionDB) | ❌ (out of scope — use @dex-ai/session-extension) |
142
+ | Native dependencies | better-sqlite3 | None |
143
+ | Weight | 3.6 MB | ~15 KB |
144
+
145
+ **Philosophy difference**: context-mode hard-blocks certain tool calls to prevent context flooding. @dex-ai/context takes a guidance approach — it teaches the LLM good patterns and warns when budgets are exceeded, but trusts the model to make the right choice. This works better with modern models that can follow instructions.
146
+
147
+ ## API
148
+
149
+ ### `contextExtension(opts?): Extension`
150
+
151
+ Factory function — creates the extension.
152
+
153
+ ### `estimateTokens(text: string): number`
154
+
155
+ Estimate token count for a string.
156
+
157
+ ### `formatContextUsage(snapshot: ContextSnapshot): string`
158
+
159
+ Format a snapshot with ANSI colors for terminal display.
160
+
161
+ ### `formatContextUsagePlain(snapshot: ContextSnapshot): string`
162
+
163
+ Format a snapshot as plain text (no colors).
164
+
165
+ ### Types
166
+
167
+ ```typescript
168
+ interface ContextSnapshot {
169
+ timestamp: number;
170
+ totalTokens: number;
171
+ maxTokens: number;
172
+ usagePercent: number;
173
+ categories: CategoryUsage[];
174
+ availableTokens: number;
175
+ }
176
+
177
+ interface CategoryUsage {
178
+ category: ContextCategory;
179
+ tokens: number;
180
+ percent: number;
181
+ }
182
+
183
+ type ContextCategory =
184
+ | "system-prompt"
185
+ | "system-tools"
186
+ | "messages"
187
+ | "tool-calls"
188
+ | "tool-results"
189
+ | "images"
190
+ | "files"
191
+ | "reasoning";
192
+ ```
193
+
194
+ ## Development
195
+
196
+ ```bash
197
+ bun install
198
+ bun run typecheck
199
+ bun test
200
+ ```
201
+
202
+ ## License
203
+
204
+ MIT
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@dex-ai/context",
3
+ "version": "0.7.16",
4
+ "description": "Index-and-pointer context management — indexes all tool outputs into FTS5 knowledge base for zero-loss compression. Provides ctx_search for on-demand retrieval. Zero config.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./src/index.ts",
9
+ "default": "./src/index.ts"
10
+ }
11
+ },
12
+ "files": [
13
+ "src"
14
+ ],
15
+ "scripts": {
16
+ "typecheck": "tsc --noEmit",
17
+ "test": "bun test",
18
+ "changeset": "changeset",
19
+ "version": "changeset version",
20
+ "release": "changeset publish"
21
+ },
22
+ "dependencies": {
23
+ "@dex-ai/sdk": "^0.1.22",
24
+ "zod": "^3.23.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/bun": "latest",
28
+ "typescript": "^5.6.3",
29
+ "@changesets/cli": "^2.29.0"
30
+ },
31
+ "sideEffects": false,
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "registry": "https://registry.npmjs.org/"
35
+ }
36
+ }
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Event Log — passive extraction of structured events from tool results.
3
+ *
4
+ * This module observes tool results without modifying them. It extracts
5
+ * structured session events (file ops, commands, errors) that are used
6
+ * to build resume snapshots when context pressure requires compression.
7
+ *
8
+ * Zero modification. Zero truncation. Pure observation.
9
+ */
10
+
11
+ import { estimateTokens } from "./tokenizer";
12
+
13
+ /* ── Types ─────────────────────────────────────────────── */
14
+
15
+ export type EventCategory =
16
+ | "file"
17
+ | "command"
18
+ | "error"
19
+ | "search"
20
+ | "decision";
21
+
22
+ export interface SessionEvent {
23
+ type: string;
24
+ category: EventCategory;
25
+ /** Compact data payload — file path, command, error message */
26
+ data: string;
27
+ /** Unix ms timestamp */
28
+ timestamp: number;
29
+ }
30
+
31
+ /* ── Event Log ─────────────────────────────────────────── */
32
+
33
+ export class EventLog {
34
+ private events: SessionEvent[] = [];
35
+
36
+ append(event: SessionEvent): void {
37
+ this.events.push(event);
38
+ }
39
+
40
+ appendAll(events: SessionEvent[]): void {
41
+ this.events.push(...events);
42
+ }
43
+
44
+ getAll(): ReadonlyArray<SessionEvent> {
45
+ return this.events;
46
+ }
47
+
48
+ getByCategory(category: EventCategory): SessionEvent[] {
49
+ return this.events.filter((e) => e.category === category);
50
+ }
51
+
52
+ get length(): number {
53
+ return this.events.length;
54
+ }
55
+
56
+ clear(): void {
57
+ this.events = [];
58
+ }
59
+ }
60
+
61
+ /* ── Event Extraction ──────────────────────────────────── */
62
+
63
+ export interface ToolResultInput {
64
+ toolName: string;
65
+ input: Record<string, unknown> | undefined;
66
+ outputText: string | null;
67
+ isError: boolean;
68
+ }
69
+
70
+ /**
71
+ * Extract structured events from a tool result.
72
+ * Returns 0 or more events depending on the tool type.
73
+ */
74
+ export function extractEvents(result: ToolResultInput): SessionEvent[] {
75
+ const now = Date.now();
76
+ const events: SessionEvent[] = [];
77
+
78
+ switch (result.toolName) {
79
+ case "read": {
80
+ const path = str(result.input?.path);
81
+ if (path) {
82
+ events.push({
83
+ type: "file_read",
84
+ category: "file",
85
+ data: path,
86
+ timestamp: now,
87
+ });
88
+ }
89
+ break;
90
+ }
91
+
92
+ case "write": {
93
+ const path = str(result.input?.path);
94
+ if (path) {
95
+ events.push({
96
+ type: "file_write",
97
+ category: "file",
98
+ data: path,
99
+ timestamp: now,
100
+ });
101
+ }
102
+ break;
103
+ }
104
+
105
+ case "edit": {
106
+ const path = str(result.input?.path);
107
+ if (path) {
108
+ events.push({
109
+ type: "file_edit",
110
+ category: "file",
111
+ data: path,
112
+ timestamp: now,
113
+ });
114
+ }
115
+ break;
116
+ }
117
+
118
+ case "bash": {
119
+ const command = str(result.input?.command);
120
+ if (command) {
121
+ const summary = summarizeBashCommand(command, result.outputText);
122
+ events.push({
123
+ type: "bash_command",
124
+ category: "command",
125
+ data: summary,
126
+ timestamp: now,
127
+ });
128
+
129
+ // Also extract errors
130
+ if (result.isError || detectBashError(result.outputText)) {
131
+ const errorLine = extractErrorLine(result.outputText);
132
+ if (errorLine) {
133
+ events.push({
134
+ type: "error",
135
+ category: "error",
136
+ data: `${truncStr(command, 50)}: ${errorLine}`,
137
+ timestamp: now,
138
+ });
139
+ }
140
+ }
141
+ }
142
+ break;
143
+ }
144
+
145
+ case "search": {
146
+ const pattern = str(result.input?.pattern);
147
+ const mode = str(result.input?.mode) || "grep";
148
+ if (pattern) {
149
+ events.push({
150
+ type: "search",
151
+ category: "search",
152
+ data: `${mode} "${truncStr(pattern, 40)}"`,
153
+ timestamp: now,
154
+ });
155
+ }
156
+ break;
157
+ }
158
+
159
+ case "lsp_navigation": {
160
+ const op = str(result.input?.operation);
161
+ const file = str(result.input?.filePath);
162
+ if (op && file) {
163
+ events.push({
164
+ type: "lsp",
165
+ category: "search",
166
+ data: `lsp:${op} ${basename(file)}`,
167
+ timestamp: now,
168
+ });
169
+ }
170
+ break;
171
+ }
172
+ }
173
+
174
+ return events;
175
+ }
176
+
177
+ /* ── Helpers ───────────────────────────────────────────── */
178
+
179
+ function str(v: unknown): string {
180
+ return typeof v === "string" ? v : "";
181
+ }
182
+
183
+ function truncStr(s: string, max: number): string {
184
+ const cleaned = s.replace(/\s+/g, " ").trim();
185
+ return cleaned.length > max ? cleaned.slice(0, max - 1) + "…" : cleaned;
186
+ }
187
+
188
+ function basename(path: string): string {
189
+ const parts = path.split("/");
190
+ return parts[parts.length - 1] || path;
191
+ }
192
+
193
+ function summarizeBashCommand(command: string, output: string | null): string {
194
+ const cmd = truncStr(command, 60);
195
+
196
+ if (!output) return cmd;
197
+
198
+ // Try to extract test results
199
+ const testMatch = output.match(/(\d+)\s*pass(?:ed|ing)?/i);
200
+ const failMatch = output.match(/(\d+)\s*fail(?:ed|ing|ure)?/i);
201
+ if (testMatch || failMatch) {
202
+ const parts: string[] = [];
203
+ if (testMatch) parts.push(`${testMatch[1]} pass`);
204
+ if (failMatch) parts.push(`${failMatch[1]} fail`);
205
+ return `${cmd} → ${parts.join(", ")}`;
206
+ }
207
+
208
+ // Check for publish/deploy output
209
+ const publishMatch = output.match(/\+\s*(@[\w/-]+@[\d.]+)/);
210
+ if (publishMatch) return `${cmd} → ${publishMatch[1]}`;
211
+
212
+ // Exit code
213
+ const exitMatch = output.match(/\[exit code: (\d+)\]/);
214
+ if (exitMatch && exitMatch[1] !== "0") {
215
+ return `${cmd} → exit=${exitMatch[1]}`;
216
+ }
217
+
218
+ // Short output — include directly
219
+ const lines = output.trim().split("\n");
220
+ if (lines.length <= 2 && output.trim().length < 80) {
221
+ return `${cmd} → ${output.trim()}`;
222
+ }
223
+
224
+ return `${cmd} (${lines.length} lines)`;
225
+ }
226
+
227
+ function detectBashError(output: string | null): boolean {
228
+ if (!output) return false;
229
+ // Non-zero exit code is a definitive error signal
230
+ if (/\[exit code: [1-9]\]/.test(output)) return true;
231
+ return false;
232
+ }
233
+
234
+ function extractErrorLine(output: string | null): string | null {
235
+ if (!output) return null;
236
+ const lines = output.split("\n");
237
+ // Search from end for error patterns
238
+ for (let i = lines.length - 1; i >= Math.max(0, lines.length - 20); i--) {
239
+ const line = lines[i]!.trim();
240
+ if (!line) continue;
241
+ if (/error|Error|ERR!|FAIL|panic|exception/i.test(line)) {
242
+ return truncStr(line, 100);
243
+ }
244
+ }
245
+ return null;
246
+ }