token-pilot 0.12.0 → 0.14.1
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/.claude-plugin/hooks/hooks.json +9 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +30 -1
- package/README.md +28 -7
- package/dist/config/defaults.js +12 -0
- package/dist/core/architecture-fingerprint.d.ts +34 -0
- package/dist/core/architecture-fingerprint.js +127 -0
- package/dist/core/budget-planner.d.ts +21 -0
- package/dist/core/budget-planner.js +68 -0
- package/dist/core/confidence.d.ts +31 -0
- package/dist/core/confidence.js +99 -0
- package/dist/core/context-registry.d.ts +14 -0
- package/dist/core/context-registry.js +55 -0
- package/dist/core/decision-trace.d.ts +31 -0
- package/dist/core/decision-trace.js +45 -0
- package/dist/core/intent-classifier.d.ts +13 -0
- package/dist/core/intent-classifier.js +44 -0
- package/dist/core/policy-engine.d.ts +41 -0
- package/dist/core/policy-engine.js +76 -0
- package/dist/core/session-analytics.d.ts +8 -0
- package/dist/core/session-analytics.js +86 -7
- package/dist/core/session-cache.d.ts +74 -0
- package/dist/core/session-cache.js +162 -0
- package/dist/core/validation.d.ts +3 -0
- package/dist/core/validation.js +3 -0
- package/dist/git/file-watcher.d.ts +6 -0
- package/dist/git/file-watcher.js +18 -2
- package/dist/git/watcher.d.ts +3 -0
- package/dist/git/watcher.js +6 -0
- package/dist/handlers/code-audit.d.ts +7 -2
- package/dist/handlers/code-audit.js +19 -5
- package/dist/handlers/explore-area.d.ts +10 -0
- package/dist/handlers/explore-area.js +39 -13
- package/dist/handlers/find-unused.d.ts +3 -0
- package/dist/handlers/find-unused.js +3 -2
- package/dist/handlers/find-usages.d.ts +7 -0
- package/dist/handlers/find-usages.js +36 -5
- package/dist/handlers/module-info.d.ts +3 -0
- package/dist/handlers/module-info.js +22 -2
- package/dist/handlers/project-overview.d.ts +1 -1
- package/dist/handlers/project-overview.js +18 -2
- package/dist/handlers/read-for-edit.d.ts +3 -0
- package/dist/handlers/read-for-edit.js +185 -3
- package/dist/handlers/read-range.d.ts +1 -1
- package/dist/handlers/read-range.js +16 -1
- package/dist/handlers/read-symbol.d.ts +1 -1
- package/dist/handlers/read-symbol.js +26 -2
- package/dist/handlers/related-files.d.ts +11 -0
- package/dist/handlers/related-files.js +178 -42
- package/dist/handlers/smart-read-many.js +70 -16
- package/dist/handlers/smart-read.js +10 -1
- package/dist/handlers/test-summary.js +26 -3
- package/dist/hooks/installer.d.ts +12 -8
- package/dist/hooks/installer.js +24 -8
- package/dist/index.d.ts +16 -1
- package/dist/index.js +61 -55
- package/dist/server.js +395 -30
- package/dist/types.d.ts +12 -0
- package/package.json +5 -3
- package/start.sh +28 -27
- package/dist/handlers/class-hierarchy.d.ts +0 -11
- package/dist/handlers/class-hierarchy.js +0 -28
- package/dist/handlers/export-ast-index.d.ts +0 -22
- package/dist/handlers/export-ast-index.js +0 -175
- package/dist/handlers/find-implementations.d.ts +0 -11
- package/dist/handlers/find-implementations.js +0 -27
- package/dist/handlers/search-code.d.ts +0 -14
- package/dist/handlers/search-code.js +0 -32
|
@@ -47,6 +47,11 @@ export class ContextRegistry {
|
|
|
47
47
|
return false;
|
|
48
48
|
return entry.loaded.some(r => r.symbolName === symbolName);
|
|
49
49
|
}
|
|
50
|
+
/** Check if any region of a file has been loaded into context. */
|
|
51
|
+
hasAnyLoaded(path) {
|
|
52
|
+
const entry = this.entries.get(path);
|
|
53
|
+
return !!entry && entry.loaded.length > 0;
|
|
54
|
+
}
|
|
50
55
|
isStale(path, currentHash) {
|
|
51
56
|
const entry = this.entries.get(path);
|
|
52
57
|
if (!entry)
|
|
@@ -91,6 +96,56 @@ export class ContextRegistry {
|
|
|
91
96
|
lines.push('HINT: File unchanged since last read. Use read_symbol() to reload specific parts, or read_diff() to see changes.');
|
|
92
97
|
return lines.join('\n');
|
|
93
98
|
}
|
|
99
|
+
/** Check if file was loaded in full (type='full' region exists). */
|
|
100
|
+
isFullyLoaded(path) {
|
|
101
|
+
const entry = this.entries.get(path);
|
|
102
|
+
if (!entry)
|
|
103
|
+
return false;
|
|
104
|
+
return entry.loaded.some(r => r.type === 'full');
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Generate a compact dedup reminder for read_symbol.
|
|
108
|
+
* Fires when same symbol was already loaded OR full file is in context.
|
|
109
|
+
*/
|
|
110
|
+
symbolReminder(path, symbolName) {
|
|
111
|
+
const entry = this.entries.get(path);
|
|
112
|
+
if (!entry)
|
|
113
|
+
return '';
|
|
114
|
+
const elapsed = formatDuration(Date.now() - entry.loadedAt);
|
|
115
|
+
const symbolRegion = entry.loaded.find(r => r.symbolName === symbolName);
|
|
116
|
+
const fullRegion = entry.loaded.find(r => r.type === 'full');
|
|
117
|
+
if (fullRegion) {
|
|
118
|
+
const loc = symbolRegion ? ` Symbol at [L${symbolRegion.startLine}-${symbolRegion.endLine}].` : '';
|
|
119
|
+
return [
|
|
120
|
+
`DEDUP: "${symbolName}" in ${path} — full file already in context (loaded ${elapsed} ago, ${fullRegion.tokens} tokens, unchanged).${loc}`,
|
|
121
|
+
'HINT: File unchanged. No need to re-read. Use read_for_edit() if you need exact code for editing.',
|
|
122
|
+
].join('\n');
|
|
123
|
+
}
|
|
124
|
+
if (symbolRegion) {
|
|
125
|
+
return [
|
|
126
|
+
`DEDUP: "${symbolName}" in ${path} — already loaded ${elapsed} ago [L${symbolRegion.startLine}-${symbolRegion.endLine}] (${symbolRegion.tokens} tokens, unchanged).`,
|
|
127
|
+
'HINT: Symbol unchanged since last read. No need to re-read.',
|
|
128
|
+
].join('\n');
|
|
129
|
+
}
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Generate a compact dedup reminder for read_range.
|
|
134
|
+
* Only fires when full file is in context.
|
|
135
|
+
*/
|
|
136
|
+
rangeReminder(path, startLine, endLine) {
|
|
137
|
+
const entry = this.entries.get(path);
|
|
138
|
+
if (!entry)
|
|
139
|
+
return '';
|
|
140
|
+
const fullRegion = entry.loaded.find(r => r.type === 'full');
|
|
141
|
+
if (!fullRegion)
|
|
142
|
+
return '';
|
|
143
|
+
const elapsed = formatDuration(Date.now() - entry.loadedAt);
|
|
144
|
+
return [
|
|
145
|
+
`DEDUP: ${path} [L${startLine}-${endLine}] — full file already in context (loaded ${elapsed} ago, ${fullRegion.tokens} tokens, unchanged).`,
|
|
146
|
+
'HINT: File unchanged. No need to re-read. Use read_for_edit() if you need exact code for editing.',
|
|
147
|
+
].join('\n');
|
|
148
|
+
}
|
|
94
149
|
forget(path, symbolName) {
|
|
95
150
|
if (symbolName) {
|
|
96
151
|
const entry = this.entries.get(path);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision trace — captures pre/post-execution metadata for analytics.
|
|
3
|
+
* Provides instrumentation data for budget planner advisory.
|
|
4
|
+
*/
|
|
5
|
+
import type { ContextRegistry } from './context-registry.js';
|
|
6
|
+
import type { FileCache } from './file-cache.js';
|
|
7
|
+
export interface DecisionTrace {
|
|
8
|
+
fileSize?: number;
|
|
9
|
+
fileTotalLines?: number;
|
|
10
|
+
alreadyInContext: boolean;
|
|
11
|
+
estimatedCost: number;
|
|
12
|
+
actualCost: number;
|
|
13
|
+
cheaperAlternative?: string;
|
|
14
|
+
cheaperEstimate?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface BuildTraceOptions {
|
|
17
|
+
absPath?: string;
|
|
18
|
+
tool: string;
|
|
19
|
+
args: Record<string, unknown>;
|
|
20
|
+
contextRegistry: ContextRegistry;
|
|
21
|
+
fileCache: FileCache;
|
|
22
|
+
tokensReturned: number;
|
|
23
|
+
tokensWouldBe: number;
|
|
24
|
+
recentlyEdited?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build a decision trace for a tool call.
|
|
28
|
+
* Gathers file metadata, context state, and budget planner advice.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildDecisionTrace(opts: BuildTraceOptions): DecisionTrace;
|
|
31
|
+
//# sourceMappingURL=decision-trace.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision trace — captures pre/post-execution metadata for analytics.
|
|
3
|
+
* Provides instrumentation data for budget planner advisory.
|
|
4
|
+
*/
|
|
5
|
+
import { suggestCheaperAlternative } from './budget-planner.js';
|
|
6
|
+
/**
|
|
7
|
+
* Build a decision trace for a tool call.
|
|
8
|
+
* Gathers file metadata, context state, and budget planner advice.
|
|
9
|
+
*/
|
|
10
|
+
export function buildDecisionTrace(opts) {
|
|
11
|
+
const { absPath, tool, args, contextRegistry, fileCache, tokensReturned, tokensWouldBe } = opts;
|
|
12
|
+
let fileSize;
|
|
13
|
+
let fileTotalLines;
|
|
14
|
+
let alreadyInContext = false;
|
|
15
|
+
if (absPath) {
|
|
16
|
+
// Get file info from cache if available
|
|
17
|
+
const cached = fileCache.get(absPath);
|
|
18
|
+
if (cached) {
|
|
19
|
+
fileSize = cached.structure?.meta?.bytes;
|
|
20
|
+
fileTotalLines = cached.lines?.length ?? cached.structure?.meta?.lines;
|
|
21
|
+
}
|
|
22
|
+
alreadyInContext = contextRegistry.hasAnyLoaded(absPath);
|
|
23
|
+
}
|
|
24
|
+
const trace = {
|
|
25
|
+
fileSize,
|
|
26
|
+
fileTotalLines,
|
|
27
|
+
alreadyInContext,
|
|
28
|
+
estimatedCost: tokensWouldBe,
|
|
29
|
+
actualCost: tokensReturned,
|
|
30
|
+
};
|
|
31
|
+
// Budget planner advisory
|
|
32
|
+
const symbolKnown = !!(args.symbol || args.name);
|
|
33
|
+
const suggestion = suggestCheaperAlternative(tool, args, {
|
|
34
|
+
fileLines: fileTotalLines,
|
|
35
|
+
alreadyInContext,
|
|
36
|
+
symbolKnown,
|
|
37
|
+
recentlyEdited: opts.recentlyEdited ?? false,
|
|
38
|
+
});
|
|
39
|
+
if (suggestion) {
|
|
40
|
+
trace.cheaperAlternative = suggestion.tool;
|
|
41
|
+
trace.cheaperEstimate = suggestion.estimatedTokens;
|
|
42
|
+
}
|
|
43
|
+
return trace;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=decision-trace.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent classifier — maps tool + args to a task intent category.
|
|
3
|
+
* Used for per-intent analytics: which workflows save most tokens?
|
|
4
|
+
*/
|
|
5
|
+
export type Intent = 'edit' | 'debug' | 'explore' | 'review' | 'analyze' | 'search' | 'read';
|
|
6
|
+
/**
|
|
7
|
+
* Classify the intent of a tool call based on tool name and optional args.
|
|
8
|
+
* Returns a stable intent category for analytics grouping.
|
|
9
|
+
*/
|
|
10
|
+
export declare function classifyIntent(tool: string, _args?: Record<string, unknown>): Intent;
|
|
11
|
+
/** Get all known intents for iteration in reports. */
|
|
12
|
+
export declare const ALL_INTENTS: readonly Intent[];
|
|
13
|
+
//# sourceMappingURL=intent-classifier.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent classifier — maps tool + args to a task intent category.
|
|
3
|
+
* Used for per-intent analytics: which workflows save most tokens?
|
|
4
|
+
*/
|
|
5
|
+
const TOOL_INTENT_MAP = {
|
|
6
|
+
// Edit workflow
|
|
7
|
+
read_for_edit: 'edit',
|
|
8
|
+
// Review workflow
|
|
9
|
+
smart_diff: 'review',
|
|
10
|
+
smart_log: 'review',
|
|
11
|
+
read_diff: 'review',
|
|
12
|
+
// Explore workflow
|
|
13
|
+
project_overview: 'explore',
|
|
14
|
+
explore_area: 'explore',
|
|
15
|
+
outline: 'explore',
|
|
16
|
+
// Search workflow
|
|
17
|
+
find_usages: 'search',
|
|
18
|
+
related_files: 'search',
|
|
19
|
+
// Analyze workflow
|
|
20
|
+
code_audit: 'analyze',
|
|
21
|
+
find_unused: 'analyze',
|
|
22
|
+
module_info: 'analyze',
|
|
23
|
+
// Debug workflow
|
|
24
|
+
test_summary: 'debug',
|
|
25
|
+
// Read workflow (default for reading tools)
|
|
26
|
+
smart_read: 'read',
|
|
27
|
+
read_symbol: 'read',
|
|
28
|
+
read_range: 'read',
|
|
29
|
+
smart_read_many: 'read',
|
|
30
|
+
// Analytics (meta — classify as explore)
|
|
31
|
+
session_analytics: 'explore',
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Classify the intent of a tool call based on tool name and optional args.
|
|
35
|
+
* Returns a stable intent category for analytics grouping.
|
|
36
|
+
*/
|
|
37
|
+
export function classifyIntent(tool, _args) {
|
|
38
|
+
return TOOL_INTENT_MAP[tool] ?? 'read';
|
|
39
|
+
}
|
|
40
|
+
/** Get all known intents for iteration in reports. */
|
|
41
|
+
export const ALL_INTENTS = [
|
|
42
|
+
'edit', 'debug', 'explore', 'review', 'analyze', 'search', 'read',
|
|
43
|
+
];
|
|
44
|
+
//# sourceMappingURL=intent-classifier.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy engine — configurable team policies for consistent token savings.
|
|
3
|
+
* Phase 1: advisory-only warnings, no blocking.
|
|
4
|
+
* Track 10: Team Policy Mode
|
|
5
|
+
*/
|
|
6
|
+
export interface PolicyConfig {
|
|
7
|
+
/** Advisory hints when an expensive tool is used where a cheaper alternative exists */
|
|
8
|
+
preferCheapReads: boolean;
|
|
9
|
+
/** Track if read_for_edit was called before edit (advisory) */
|
|
10
|
+
requireReadForEditBeforeEdit: boolean;
|
|
11
|
+
/** Always cache project overview in session cache */
|
|
12
|
+
cacheProjectOverview: boolean;
|
|
13
|
+
/** Warn after N full-file reads in a session */
|
|
14
|
+
maxFullFileReads: number;
|
|
15
|
+
/** Warn when a single response exceeds this token threshold */
|
|
16
|
+
warnOnLargeReads: boolean;
|
|
17
|
+
/** Token threshold for large read warning */
|
|
18
|
+
largeReadThreshold: number;
|
|
19
|
+
}
|
|
20
|
+
export declare const DEFAULT_POLICIES: PolicyConfig;
|
|
21
|
+
export interface PolicyCheckContext {
|
|
22
|
+
fullFileReadsCount: number;
|
|
23
|
+
tokensReturned: number;
|
|
24
|
+
readForEditCalled?: Set<string>;
|
|
25
|
+
editTargetPath?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface PolicyAdvisory {
|
|
28
|
+
level: 'info' | 'warn';
|
|
29
|
+
message: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Check policy rules and return advisory messages.
|
|
33
|
+
* Returns null if no policy violation detected.
|
|
34
|
+
*/
|
|
35
|
+
export declare function checkPolicy(policy: PolicyConfig, tool: string, context: PolicyCheckContext): PolicyAdvisory | null;
|
|
36
|
+
/**
|
|
37
|
+
* Count how many full-file reads a tool represents.
|
|
38
|
+
* Returns 1 for full-read tools, 0 for targeted tools.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isFullReadTool(tool: string): boolean;
|
|
41
|
+
//# sourceMappingURL=policy-engine.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy engine — configurable team policies for consistent token savings.
|
|
3
|
+
* Phase 1: advisory-only warnings, no blocking.
|
|
4
|
+
* Track 10: Team Policy Mode
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_POLICIES = {
|
|
7
|
+
preferCheapReads: true,
|
|
8
|
+
requireReadForEditBeforeEdit: true,
|
|
9
|
+
cacheProjectOverview: true,
|
|
10
|
+
maxFullFileReads: 10,
|
|
11
|
+
warnOnLargeReads: true,
|
|
12
|
+
largeReadThreshold: 2000,
|
|
13
|
+
};
|
|
14
|
+
/** Full-file read tools that count toward maxFullFileReads */
|
|
15
|
+
const FULL_READ_TOOLS = new Set([
|
|
16
|
+
'smart_read',
|
|
17
|
+
'smart_read_many',
|
|
18
|
+
]);
|
|
19
|
+
/** Tools that indicate a cheaper alternative may exist */
|
|
20
|
+
const EXPENSIVE_TOOLS = {
|
|
21
|
+
smart_read: 'Consider read_symbol() or read_range() for targeted reads',
|
|
22
|
+
smart_read_many: 'Consider reading files individually with read_symbol()',
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Check policy rules and return advisory messages.
|
|
26
|
+
* Returns null if no policy violation detected.
|
|
27
|
+
*/
|
|
28
|
+
export function checkPolicy(policy, tool, context) {
|
|
29
|
+
// 1. Max full-file reads exceeded
|
|
30
|
+
if (policy.maxFullFileReads > 0 &&
|
|
31
|
+
FULL_READ_TOOLS.has(tool) &&
|
|
32
|
+
context.fullFileReadsCount >= policy.maxFullFileReads) {
|
|
33
|
+
return {
|
|
34
|
+
level: 'warn',
|
|
35
|
+
message: `POLICY: ${context.fullFileReadsCount} full-file reads this session (limit: ${policy.maxFullFileReads}). Consider read_symbol() or read_range() for targeted access.`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// 2. Large read warning
|
|
39
|
+
if (policy.warnOnLargeReads &&
|
|
40
|
+
context.tokensReturned > policy.largeReadThreshold) {
|
|
41
|
+
return {
|
|
42
|
+
level: 'info',
|
|
43
|
+
message: `POLICY: Large response (~${context.tokensReturned} tokens). Future reads on this file: use read_symbol() or read_range() for targeted access.`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// 3. Prefer cheap reads advisory
|
|
47
|
+
if (policy.preferCheapReads && EXPENSIVE_TOOLS[tool]) {
|
|
48
|
+
// Only advise when token count is high enough to matter
|
|
49
|
+
if (context.tokensReturned > 500) {
|
|
50
|
+
return {
|
|
51
|
+
level: 'info',
|
|
52
|
+
message: `POLICY: ${EXPENSIVE_TOOLS[tool]}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// 4. Require read_for_edit before edit
|
|
57
|
+
if (policy.requireReadForEditBeforeEdit &&
|
|
58
|
+
tool === 'edit' &&
|
|
59
|
+
context.editTargetPath &&
|
|
60
|
+
context.readForEditCalled &&
|
|
61
|
+
!context.readForEditCalled.has(context.editTargetPath)) {
|
|
62
|
+
return {
|
|
63
|
+
level: 'info',
|
|
64
|
+
message: `POLICY: Consider using read_for_edit("${context.editTargetPath}") before editing to get precise edit context.`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Count how many full-file reads a tool represents.
|
|
71
|
+
* Returns 1 for full-read tools, 0 for targeted tools.
|
|
72
|
+
*/
|
|
73
|
+
export function isFullReadTool(tool) {
|
|
74
|
+
return FULL_READ_TOOLS.has(tool);
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=policy-engine.js.map
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { ContextModeStatus } from '../integration/context-mode-detector.js';
|
|
2
|
+
import type { Intent } from './intent-classifier.js';
|
|
3
|
+
import type { DecisionTrace } from './decision-trace.js';
|
|
4
|
+
export type SavingsCategory = 'compression' | 'cache' | 'dedup' | 'none';
|
|
2
5
|
export interface ToolCall {
|
|
3
6
|
tool: string;
|
|
4
7
|
path?: string;
|
|
@@ -6,7 +9,12 @@ export interface ToolCall {
|
|
|
6
9
|
tokensWouldBe: number;
|
|
7
10
|
timestamp: number;
|
|
8
11
|
delegatedToContextMode?: boolean;
|
|
12
|
+
sessionCacheHit?: boolean;
|
|
13
|
+
savingsCategory?: SavingsCategory;
|
|
14
|
+
intent?: Intent;
|
|
15
|
+
decisionTrace?: DecisionTrace;
|
|
9
16
|
}
|
|
17
|
+
export type { Intent, DecisionTrace };
|
|
10
18
|
/**
|
|
11
19
|
* Tracks token savings and tool usage across a session.
|
|
12
20
|
* When context-mode is detected, includes unified reporting.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatDuration } from './format-duration.js';
|
|
2
|
+
import { ALL_INTENTS } from './intent-classifier.js';
|
|
2
3
|
/**
|
|
3
4
|
* Tracks token savings and tool usage across a session.
|
|
4
5
|
* When context-mode is detected, includes unified reporting.
|
|
@@ -24,10 +25,11 @@ export class SessionAnalytics {
|
|
|
24
25
|
// Group by tool
|
|
25
26
|
const byTool = new Map();
|
|
26
27
|
for (const c of this.calls) {
|
|
27
|
-
const existing = byTool.get(c.tool) ?? { count: 0, tokens: 0, saved: 0 };
|
|
28
|
+
const existing = byTool.get(c.tool) ?? { count: 0, tokens: 0, saved: 0, wouldBe: 0 };
|
|
28
29
|
existing.count++;
|
|
29
30
|
existing.tokens += c.tokensReturned;
|
|
30
31
|
existing.saved += Math.max(0, c.tokensWouldBe - c.tokensReturned);
|
|
32
|
+
existing.wouldBe += c.tokensWouldBe;
|
|
31
33
|
byTool.set(c.tool, existing);
|
|
32
34
|
}
|
|
33
35
|
const lines = [
|
|
@@ -39,8 +41,12 @@ export class SessionAnalytics {
|
|
|
39
41
|
'',
|
|
40
42
|
'By tool:',
|
|
41
43
|
];
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
const sortedTools = Array.from(byTool.entries()).sort((a, b) => b[1].saved - a[1].saved);
|
|
45
|
+
for (const [tool, stats] of sortedTools) {
|
|
46
|
+
const reduction = stats.wouldBe > 0
|
|
47
|
+
? Math.round((1 - stats.tokens / stats.wouldBe) * 100)
|
|
48
|
+
: 0;
|
|
49
|
+
lines.push(` ${tool}: ${stats.count} calls, ~${stats.tokens} tokens returned, ~${stats.saved} saved (${reduction}% reduction)`);
|
|
44
50
|
}
|
|
45
51
|
// Top files by savings
|
|
46
52
|
const byFile = new Map();
|
|
@@ -60,11 +66,48 @@ export class SessionAnalytics {
|
|
|
60
66
|
lines.push(` ${file}: ~${saved} tokens saved`);
|
|
61
67
|
}
|
|
62
68
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
const lowValueTools = sortedTools
|
|
70
|
+
.map(([tool, stats]) => ({
|
|
71
|
+
tool,
|
|
72
|
+
reduction: stats.wouldBe > 0 ? Math.round((1 - stats.tokens / stats.wouldBe) * 100) : 0,
|
|
73
|
+
count: stats.count,
|
|
74
|
+
}))
|
|
75
|
+
.filter((tool) => tool.reduction < 20);
|
|
76
|
+
if (lowValueTools.length > 0) {
|
|
66
77
|
lines.push('');
|
|
67
|
-
lines.push(
|
|
78
|
+
lines.push('Needs improvement:');
|
|
79
|
+
for (const tool of lowValueTools.slice(0, 5)) {
|
|
80
|
+
lines.push(` ${tool.tool}: only ${tool.reduction}% reduction across ${tool.count} call${tool.count === 1 ? '' : 's'}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Savings breakdown by category
|
|
84
|
+
const byCategory = { compression: 0, cache: 0, dedup: 0, none: 0 };
|
|
85
|
+
for (const c of this.calls) {
|
|
86
|
+
const cat = c.savingsCategory ?? 'none';
|
|
87
|
+
byCategory[cat] += Math.max(0, c.tokensWouldBe - c.tokensReturned);
|
|
88
|
+
}
|
|
89
|
+
if (totalWouldBe > totalReturned) {
|
|
90
|
+
lines.push('');
|
|
91
|
+
lines.push('Savings breakdown:');
|
|
92
|
+
if (byCategory.compression > 0)
|
|
93
|
+
lines.push(` Compression (AST/structured): ~${byCategory.compression} tokens`);
|
|
94
|
+
if (byCategory.cache > 0)
|
|
95
|
+
lines.push(` Cache hits (session cache): ~${byCategory.cache} tokens`);
|
|
96
|
+
if (byCategory.dedup > 0)
|
|
97
|
+
lines.push(` Dedup (already in context): ~${byCategory.dedup} tokens`);
|
|
98
|
+
}
|
|
99
|
+
// Session cache hits
|
|
100
|
+
const cacheHits = this.calls.filter(c => c.sessionCacheHit);
|
|
101
|
+
if (cacheHits.length > 0) {
|
|
102
|
+
const cacheTokensSaved = cacheHits.reduce((s, c) => s + Math.max(0, c.tokensWouldBe - c.tokensReturned), 0);
|
|
103
|
+
lines.push('');
|
|
104
|
+
lines.push(`Session cache: ${cacheHits.length} hits / ${this.calls.length} calls (${Math.round(cacheHits.length / this.calls.length * 100)}% hit rate, ~${cacheTokensSaved} tokens saved)`);
|
|
105
|
+
}
|
|
106
|
+
// Dedup reminders served
|
|
107
|
+
const dedupCalls = this.calls.filter(c => c.savingsCategory === 'dedup');
|
|
108
|
+
if (dedupCalls.length > 0) {
|
|
109
|
+
lines.push('');
|
|
110
|
+
lines.push(`Compact reminders/dedup: ${dedupCalls.length} calls (avoided full re-reads)`);
|
|
68
111
|
}
|
|
69
112
|
// Delegation stats
|
|
70
113
|
const delegated = this.calls.filter(c => c.delegatedToContextMode);
|
|
@@ -72,6 +115,42 @@ export class SessionAnalytics {
|
|
|
72
115
|
lines.push('');
|
|
73
116
|
lines.push(`Delegated to context-mode: ${delegated.length} calls`);
|
|
74
117
|
}
|
|
118
|
+
// Per-intent breakdown (Track 2)
|
|
119
|
+
const callsWithIntent = this.calls.filter(c => c.intent);
|
|
120
|
+
if (callsWithIntent.length > 0) {
|
|
121
|
+
const byIntent = new Map();
|
|
122
|
+
for (const c of callsWithIntent) {
|
|
123
|
+
const intent = c.intent;
|
|
124
|
+
const existing = byIntent.get(intent) ?? { count: 0, saved: 0 };
|
|
125
|
+
existing.count++;
|
|
126
|
+
existing.saved += Math.max(0, c.tokensWouldBe - c.tokensReturned);
|
|
127
|
+
byIntent.set(intent, existing);
|
|
128
|
+
}
|
|
129
|
+
lines.push('');
|
|
130
|
+
lines.push('Per-intent breakdown:');
|
|
131
|
+
for (const intent of ALL_INTENTS) {
|
|
132
|
+
const stats = byIntent.get(intent);
|
|
133
|
+
if (stats) {
|
|
134
|
+
lines.push(` ${intent}: ${stats.count} call${stats.count === 1 ? '' : 's'}, ~${stats.saved} tokens saved`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Decision insights (Track 0)
|
|
139
|
+
const tracedCalls = this.calls.filter(c => c.decisionTrace);
|
|
140
|
+
if (tracedCalls.length > 0) {
|
|
141
|
+
const alreadyInContextCount = tracedCalls.filter(c => c.decisionTrace.alreadyInContext).length;
|
|
142
|
+
const totalEstimated = tracedCalls.reduce((s, c) => s + c.decisionTrace.estimatedCost, 0);
|
|
143
|
+
const totalActual = tracedCalls.reduce((s, c) => s + c.decisionTrace.actualCost, 0);
|
|
144
|
+
const avgReduction = totalEstimated > 0 ? Math.round((1 - totalActual / totalEstimated) * 100) : 0;
|
|
145
|
+
const missedSavings = tracedCalls.filter(c => c.decisionTrace.cheaperAlternative).length;
|
|
146
|
+
lines.push('');
|
|
147
|
+
lines.push('Decision insights:');
|
|
148
|
+
lines.push(` Files already in context: ${alreadyInContextCount} of ${tracedCalls.length} calls (${Math.round(alreadyInContextCount / tracedCalls.length * 100)}%)`);
|
|
149
|
+
lines.push(` Avg cost reduction: ${avgReduction}% (estimated → actual)`);
|
|
150
|
+
if (missedSavings > 0) {
|
|
151
|
+
lines.push(` Missed savings opportunities: ${missedSavings} call${missedSavings === 1 ? '' : 's'} could have used cheaper tools`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
75
154
|
// Context-mode companion status
|
|
76
155
|
if (this.contextModeStatus.detected) {
|
|
77
156
|
lines.push('');
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export interface SessionCacheEntry {
|
|
2
|
+
/** Cached handler result */
|
|
3
|
+
result: {
|
|
4
|
+
content: Array<{
|
|
5
|
+
type: string;
|
|
6
|
+
text: string;
|
|
7
|
+
}>;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
/** Absolute file paths (or dir prefixes ending with '/') this result depends on */
|
|
11
|
+
fileDeps: Set<string>;
|
|
12
|
+
/** Invalidate when AST index rebuilds */
|
|
13
|
+
dependsOnAst: boolean;
|
|
14
|
+
/** Invalidate when git state changes (branch switch, new commits) */
|
|
15
|
+
dependsOnGit: boolean;
|
|
16
|
+
/** When this entry was cached */
|
|
17
|
+
cachedAt: number;
|
|
18
|
+
/** Estimated token count of the result */
|
|
19
|
+
tokenEstimate: number;
|
|
20
|
+
/** Original tokensWouldBe from the first (non-cached) call — for honest cache savings */
|
|
21
|
+
tokensWouldBe?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface SessionCacheDeps {
|
|
24
|
+
files?: string[];
|
|
25
|
+
dependsOnAst?: boolean;
|
|
26
|
+
dependsOnGit?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export declare class SessionCache {
|
|
29
|
+
private maxEntries;
|
|
30
|
+
private entries;
|
|
31
|
+
/** Reverse index: file path → set of cache keys that depend on it */
|
|
32
|
+
private fileDepsIndex;
|
|
33
|
+
private hits;
|
|
34
|
+
private misses;
|
|
35
|
+
private invalidations;
|
|
36
|
+
constructor(maxEntries: number);
|
|
37
|
+
/**
|
|
38
|
+
* Generate deterministic cache key from tool name + args.
|
|
39
|
+
* Sorts args keys for consistency regardless of insertion order.
|
|
40
|
+
*/
|
|
41
|
+
static makeCacheKey(tool: string, args: object): string;
|
|
42
|
+
/** Try to get a cached result. Returns null on miss. */
|
|
43
|
+
get(tool: string, args: object): SessionCacheEntry | null;
|
|
44
|
+
/** Store a result with its dependency metadata. */
|
|
45
|
+
set(tool: string, args: object, result: {
|
|
46
|
+
content: Array<{
|
|
47
|
+
type: string;
|
|
48
|
+
text: string;
|
|
49
|
+
}>;
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
}, deps: SessionCacheDeps, tokenEstimate: number, tokensWouldBe?: number): void;
|
|
52
|
+
/**
|
|
53
|
+
* Invalidate all entries that depend on any of the given files.
|
|
54
|
+
* Checks both exact path match and directory prefix match.
|
|
55
|
+
*/
|
|
56
|
+
invalidateByFiles(filePaths: string[]): number;
|
|
57
|
+
/** Invalidate all entries that depend on AST index state. */
|
|
58
|
+
invalidateByAst(): number;
|
|
59
|
+
/** Invalidate all entries that depend on git state. */
|
|
60
|
+
invalidateByGit(): number;
|
|
61
|
+
/** Clear all entries. */
|
|
62
|
+
invalidateAll(): void;
|
|
63
|
+
/** Cache statistics for analytics. */
|
|
64
|
+
stats(): {
|
|
65
|
+
entries: number;
|
|
66
|
+
hits: number;
|
|
67
|
+
misses: number;
|
|
68
|
+
hitRate: number;
|
|
69
|
+
invalidations: number;
|
|
70
|
+
};
|
|
71
|
+
private deleteEntry;
|
|
72
|
+
private evictOldest;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=session-cache.d.ts.map
|