smart-context-mcp 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +239 -20
- package/package.json +10 -7
- package/scripts/init-clients.js +56 -27
- package/scripts/report-metrics.js +5 -0
- package/scripts/report-workflow-metrics.js +255 -0
- package/src/analytics/adoption.js +197 -0
- package/src/decision-explainer.js +168 -0
- package/src/missed-opportunities.js +255 -0
- package/src/server.js +66 -0
- package/src/storage/sqlite.js +30 -1
- package/src/tools/smart-context.js +33 -1
- package/src/tools/smart-metrics.js +7 -0
- package/src/tools/smart-read-batch.js +9 -0
- package/src/tools/smart-read.js +56 -1
- package/src/tools/smart-search.js +31 -0
- package/src/tools/smart-shell.js +62 -9
- package/src/tools/smart-summary.js +46 -2
- package/src/tools/smart-turn.js +1 -0
- package/src/usage-feedback.js +179 -0
- package/src/workflow-tracker-stub.js +53 -0
- package/src/workflow-tracker.js +410 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Missed opportunities detector - identifies when devctx should have been used but wasn't
|
|
3
|
+
*
|
|
4
|
+
* Analyzes session metrics to detect patterns where devctx would have helped.
|
|
5
|
+
*
|
|
6
|
+
* Enable with environment variable: DEVCTX_DETECT_MISSED=true
|
|
7
|
+
*
|
|
8
|
+
* Detection heuristics (based on session metrics):
|
|
9
|
+
* - Low devctx adoption in complex sessions (many operations, few devctx calls)
|
|
10
|
+
* - Sessions with 0 devctx usage but high operation count
|
|
11
|
+
* - Inferred complexity vs actual devctx usage
|
|
12
|
+
*
|
|
13
|
+
* Note: We can't intercept native tool calls in real-time, so we analyze
|
|
14
|
+
* patterns from metrics after the fact.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const sessionActivity = {
|
|
18
|
+
devctxOperations: 0,
|
|
19
|
+
totalOperations: 0, // Estimated from devctx calls + time-based heuristic
|
|
20
|
+
lastDevctxCall: 0,
|
|
21
|
+
sessionStart: Date.now(),
|
|
22
|
+
enabled: false,
|
|
23
|
+
warnings: [],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const DEVCTX_TOOLS = new Set([
|
|
27
|
+
'smart_read',
|
|
28
|
+
'smart_search',
|
|
29
|
+
'smart_context',
|
|
30
|
+
'smart_shell',
|
|
31
|
+
'smart_summary',
|
|
32
|
+
'smart_turn',
|
|
33
|
+
'smart_read_batch',
|
|
34
|
+
'build_index',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if missed opportunity detection is enabled
|
|
39
|
+
*/
|
|
40
|
+
export const isMissedDetectionEnabled = () => {
|
|
41
|
+
const envValue = process.env.DEVCTX_DETECT_MISSED?.toLowerCase();
|
|
42
|
+
const enabled = envValue === 'true' || envValue === '1' || envValue === 'yes';
|
|
43
|
+
sessionActivity.enabled = enabled;
|
|
44
|
+
return enabled;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Record devctx tool usage
|
|
49
|
+
*/
|
|
50
|
+
export const recordDevctxOperation = () => {
|
|
51
|
+
if (!isMissedDetectionEnabled()) return;
|
|
52
|
+
|
|
53
|
+
sessionActivity.devctxOperations += 1;
|
|
54
|
+
sessionActivity.totalOperations += 1;
|
|
55
|
+
sessionActivity.lastDevctxCall = Date.now();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Estimate total operations based on time and activity
|
|
60
|
+
* Heuristic: If no devctx calls for >2 minutes, likely agent is using native tools
|
|
61
|
+
*/
|
|
62
|
+
const estimateTotalOperations = () => {
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
const sessionDuration = now - sessionActivity.sessionStart;
|
|
65
|
+
const timeSinceLastDevctx = now - sessionActivity.lastDevctxCall;
|
|
66
|
+
|
|
67
|
+
// If session is active (recent devctx calls), estimate conservatively
|
|
68
|
+
if (timeSinceLastDevctx < 2 * 60 * 1000) {
|
|
69
|
+
return sessionActivity.totalOperations;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// If long gap without devctx, estimate agent is using native tools
|
|
73
|
+
// Heuristic: ~1 operation per 10 seconds of activity
|
|
74
|
+
const estimatedNativeOps = Math.floor(timeSinceLastDevctx / 10000);
|
|
75
|
+
return sessionActivity.totalOperations + estimatedNativeOps;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Analyze session and detect missed opportunities
|
|
80
|
+
*/
|
|
81
|
+
export const analyzeMissedOpportunities = () => {
|
|
82
|
+
if (!isMissedDetectionEnabled()) return null;
|
|
83
|
+
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
const sessionDuration = now - sessionActivity.sessionStart;
|
|
86
|
+
const timeSinceLastDevctx = now - sessionActivity.lastDevctxCall;
|
|
87
|
+
const estimatedTotal = estimateTotalOperations();
|
|
88
|
+
|
|
89
|
+
const opportunities = [];
|
|
90
|
+
|
|
91
|
+
// Detection 1: Long session with no devctx usage
|
|
92
|
+
if (sessionDuration > 5 * 60 * 1000 && sessionActivity.devctxOperations === 0) {
|
|
93
|
+
opportunities.push({
|
|
94
|
+
type: 'no_devctx_usage',
|
|
95
|
+
severity: 'high',
|
|
96
|
+
reason: 'Session active for >5 minutes with 0 devctx calls. Agent may not be using devctx.',
|
|
97
|
+
suggestion: 'Use forcing prompt or check if MCP is active',
|
|
98
|
+
estimatedSavings: estimatedTotal * 10000, // Estimate ~10K tokens per operation
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Detection 2: Low devctx adoption in active session
|
|
103
|
+
const devctxRatio = estimatedTotal > 0 ? sessionActivity.devctxOperations / estimatedTotal : 0;
|
|
104
|
+
if (estimatedTotal >= 10 && devctxRatio < 0.3) {
|
|
105
|
+
opportunities.push({
|
|
106
|
+
type: 'low_devctx_adoption',
|
|
107
|
+
severity: 'medium',
|
|
108
|
+
reason: `Low devctx adoption: ${sessionActivity.devctxOperations}/${estimatedTotal} operations (${Math.round(devctxRatio * 100)}%). Target: >50%.`,
|
|
109
|
+
suggestion: 'Agent may be using native tools. Consider forcing prompt.',
|
|
110
|
+
estimatedSavings: (estimatedTotal - sessionActivity.devctxOperations) * 8000,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Detection 3: Long gap without devctx (agent switched to native tools)
|
|
115
|
+
if (sessionActivity.devctxOperations > 0 && timeSinceLastDevctx > 3 * 60 * 1000) {
|
|
116
|
+
const minutesSince = Math.round(timeSinceLastDevctx / 60000);
|
|
117
|
+
opportunities.push({
|
|
118
|
+
type: 'devctx_usage_dropped',
|
|
119
|
+
severity: 'medium',
|
|
120
|
+
reason: `No devctx calls for ${minutesSince} minutes. Agent may have switched to native tools.`,
|
|
121
|
+
suggestion: 'Re-apply forcing prompt if task is still complex',
|
|
122
|
+
estimatedSavings: Math.floor(timeSinceLastDevctx / 10000) * 5000,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Detection 4: Session too short to analyze
|
|
127
|
+
if (sessionDuration < 60 * 1000 && opportunities.length === 0) {
|
|
128
|
+
return {
|
|
129
|
+
opportunities: [],
|
|
130
|
+
message: 'Session too short to analyze (<1 minute)',
|
|
131
|
+
devctxOperations: sessionActivity.devctxOperations,
|
|
132
|
+
estimatedTotal,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
opportunities,
|
|
138
|
+
devctxOperations: sessionActivity.devctxOperations,
|
|
139
|
+
estimatedTotal,
|
|
140
|
+
devctxRatio: Math.round(devctxRatio * 100),
|
|
141
|
+
sessionDuration: Math.round(sessionDuration / 1000),
|
|
142
|
+
totalEstimatedSavings: opportunities.reduce((sum, opp) => sum + (opp.estimatedSavings || 0), 0),
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Format missed opportunities as markdown
|
|
148
|
+
*/
|
|
149
|
+
export const formatMissedOpportunities = () => {
|
|
150
|
+
if (!isMissedDetectionEnabled()) return '';
|
|
151
|
+
|
|
152
|
+
const analysis = analyzeMissedOpportunities();
|
|
153
|
+
if (!analysis) return '';
|
|
154
|
+
|
|
155
|
+
// Don't show if session is too short or no opportunities
|
|
156
|
+
if (analysis.message || analysis.opportunities.length === 0) {
|
|
157
|
+
return '';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const lines = [];
|
|
161
|
+
lines.push('');
|
|
162
|
+
lines.push('---');
|
|
163
|
+
lines.push('');
|
|
164
|
+
lines.push('⚠️ **Missed devctx opportunities detected:**');
|
|
165
|
+
lines.push('');
|
|
166
|
+
|
|
167
|
+
// Show session stats
|
|
168
|
+
lines.push(`**Session stats:**`);
|
|
169
|
+
lines.push(`- Duration: ${analysis.sessionDuration}s`);
|
|
170
|
+
lines.push(`- devctx operations: ${analysis.devctxOperations}`);
|
|
171
|
+
lines.push(`- Estimated total operations: ${analysis.estimatedTotal}`);
|
|
172
|
+
lines.push(`- devctx adoption: ${analysis.devctxRatio}%`);
|
|
173
|
+
lines.push('');
|
|
174
|
+
|
|
175
|
+
for (const opp of analysis.opportunities) {
|
|
176
|
+
const severityIcon = opp.severity === 'high' ? '🔴' : '🟡';
|
|
177
|
+
lines.push(`${severityIcon} **${opp.type.replace(/_/g, ' ')}**`);
|
|
178
|
+
lines.push(`- **Issue:** ${opp.reason}`);
|
|
179
|
+
lines.push(`- **Suggestion:** ${opp.suggestion}`);
|
|
180
|
+
|
|
181
|
+
if (opp.estimatedSavings) {
|
|
182
|
+
lines.push(`- **Potential savings:** ~${formatTokens(opp.estimatedSavings)}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
lines.push('');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (analysis.totalEstimatedSavings > 0) {
|
|
189
|
+
lines.push(`**Total potential savings:** ~${formatTokens(analysis.totalEstimatedSavings)}`);
|
|
190
|
+
lines.push('');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
lines.push('**How to fix:**');
|
|
194
|
+
lines.push('1. Use forcing prompt: `Use devctx: smart_turn(start) → smart_context/smart_search → smart_read → smart_turn(end)`');
|
|
195
|
+
lines.push('2. Check if index is built: `ls .devctx/index.json`');
|
|
196
|
+
lines.push('3. Verify MCP is active in Cursor settings');
|
|
197
|
+
lines.push('');
|
|
198
|
+
lines.push('*To disable: `export DEVCTX_DETECT_MISSED=false`*');
|
|
199
|
+
|
|
200
|
+
return lines.join('\n');
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get session activity summary
|
|
205
|
+
*/
|
|
206
|
+
export const getSessionActivity = () => {
|
|
207
|
+
return {
|
|
208
|
+
devctxOperations: sessionActivity.devctxOperations,
|
|
209
|
+
totalOperations: sessionActivity.totalOperations,
|
|
210
|
+
estimatedTotal: estimateTotalOperations(),
|
|
211
|
+
sessionDuration: Date.now() - sessionActivity.sessionStart,
|
|
212
|
+
timeSinceLastDevctx: Date.now() - sessionActivity.lastDevctxCall,
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Reset session activity (for testing or manual reset)
|
|
218
|
+
*/
|
|
219
|
+
export const resetSessionActivity = () => {
|
|
220
|
+
sessionActivity.devctxOperations = 0;
|
|
221
|
+
sessionActivity.totalOperations = 0;
|
|
222
|
+
sessionActivity.lastDevctxCall = 0;
|
|
223
|
+
sessionActivity.sessionStart = Date.now();
|
|
224
|
+
sessionActivity.warnings = [];
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Testing helpers - expose internal state for test scenarios
|
|
229
|
+
*/
|
|
230
|
+
export const __testing__ = {
|
|
231
|
+
setSessionStart: (timestamp) => {
|
|
232
|
+
sessionActivity.sessionStart = timestamp;
|
|
233
|
+
},
|
|
234
|
+
setLastDevctxCall: (timestamp) => {
|
|
235
|
+
sessionActivity.lastDevctxCall = timestamp;
|
|
236
|
+
},
|
|
237
|
+
setTotalOperations: (count) => {
|
|
238
|
+
sessionActivity.totalOperations = count;
|
|
239
|
+
},
|
|
240
|
+
getSessionActivity: () => sessionActivity,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Format token count for display
|
|
245
|
+
*/
|
|
246
|
+
const formatTokens = (tokens) => {
|
|
247
|
+
if (tokens >= 1000000) {
|
|
248
|
+
return `${(tokens / 1000000).toFixed(1)}M tokens`;
|
|
249
|
+
}
|
|
250
|
+
if (tokens >= 1000) {
|
|
251
|
+
return `${(tokens / 1000).toFixed(1)}K tokens`;
|
|
252
|
+
}
|
|
253
|
+
return `${tokens} tokens`;
|
|
254
|
+
};
|
|
255
|
+
|
package/src/server.js
CHANGED
|
@@ -49,6 +49,72 @@ export const createDevctxServer = () => {
|
|
|
49
49
|
// Enable streaming progress notifications
|
|
50
50
|
setServerForStreaming(server);
|
|
51
51
|
|
|
52
|
+
// Register prompts
|
|
53
|
+
server.prompt(
|
|
54
|
+
'use-devctx',
|
|
55
|
+
'Force the agent to use devctx tools for the current task. Use this prompt at the start of your message to ensure devctx is used instead of native tools.',
|
|
56
|
+
{},
|
|
57
|
+
async () => ({
|
|
58
|
+
messages: [
|
|
59
|
+
{
|
|
60
|
+
role: 'user',
|
|
61
|
+
content: {
|
|
62
|
+
type: 'text',
|
|
63
|
+
text: 'Use devctx: smart_turn(start) → smart_context/smart_search → smart_read → smart_turn(end)',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
server.prompt(
|
|
71
|
+
'devctx-workflow',
|
|
72
|
+
'Complete devctx workflow template with all recommended steps. Includes session start, context building, file reading, and session end.',
|
|
73
|
+
{},
|
|
74
|
+
async () => ({
|
|
75
|
+
messages: [
|
|
76
|
+
{
|
|
77
|
+
role: 'user',
|
|
78
|
+
content: {
|
|
79
|
+
type: 'text',
|
|
80
|
+
text: `Follow this devctx workflow:
|
|
81
|
+
|
|
82
|
+
1. smart_turn(start) - Start session and recover previous context
|
|
83
|
+
2. smart_context(task) - Build complete context for the task
|
|
84
|
+
3. smart_search(query) - Search for specific patterns if needed
|
|
85
|
+
4. smart_read(file) - Read files with appropriate mode (outline/signatures/symbol)
|
|
86
|
+
5. smart_turn(end) - Save checkpoint for next session
|
|
87
|
+
|
|
88
|
+
Use devctx tools instead of native Read/Grep/Shell when possible.`,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
server.prompt(
|
|
96
|
+
'devctx-preflight',
|
|
97
|
+
'Preflight checklist before starting work. Ensures index is built and session is initialized.',
|
|
98
|
+
{},
|
|
99
|
+
async () => ({
|
|
100
|
+
messages: [
|
|
101
|
+
{
|
|
102
|
+
role: 'user',
|
|
103
|
+
content: {
|
|
104
|
+
type: 'text',
|
|
105
|
+
text: `Preflight checklist:
|
|
106
|
+
|
|
107
|
+
1. build_index(incremental=true) - Build/update symbol index
|
|
108
|
+
2. smart_turn(start) - Initialize session and recover context
|
|
109
|
+
3. Proceed with your task using devctx tools
|
|
110
|
+
|
|
111
|
+
This ensures optimal performance and context recovery.`,
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
|
|
52
118
|
server.tool(
|
|
53
119
|
'smart_read',
|
|
54
120
|
'Read a file with token-efficient modes. outline/signatures: compact structure (~90% savings). range: specific line range with line numbers. symbol: extract function/class/method by name (string or array for batch). full: file content capped at 12k chars. maxTokens: token budget — auto-selects the most detailed mode that fits (full -> outline -> signatures -> truncated). context=true (symbol mode only): includes callers, tests, and referenced types from the dependency graph; returns graphCoverage (imports/tests: full|partial|none) so the agent knows how reliable the cross-file context is. Responses are cached in memory per session and invalidated by file mtime; cached=true when served from cache. Every response includes a unified confidence block: { parser, truncated, cached, graphCoverage? }. Supports JS/TS, Python, Go, Rust, Java, C#, Kotlin, PHP, Swift, shell, Terraform, Dockerfile, SQL, JSON, TOML, YAML.',
|
package/src/storage/sqlite.js
CHANGED
|
@@ -5,7 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { projectRoot } from '../utils/runtime-config.js';
|
|
6
6
|
|
|
7
7
|
export const STATE_DB_FILENAME = 'state.sqlite';
|
|
8
|
-
export const SQLITE_SCHEMA_VERSION =
|
|
8
|
+
export const SQLITE_SCHEMA_VERSION = 5;
|
|
9
9
|
export const ACTIVE_SESSION_SCOPE = 'project';
|
|
10
10
|
export const EXPECTED_TABLES = [
|
|
11
11
|
'active_session',
|
|
@@ -16,6 +16,7 @@ export const EXPECTED_TABLES = [
|
|
|
16
16
|
'session_events',
|
|
17
17
|
'sessions',
|
|
18
18
|
'summary_cache',
|
|
19
|
+
'workflow_metrics',
|
|
19
20
|
];
|
|
20
21
|
|
|
21
22
|
const MIGRATIONS = [
|
|
@@ -145,6 +146,34 @@ const MIGRATIONS = [
|
|
|
145
146
|
ON context_access(session_id, timestamp DESC)`,
|
|
146
147
|
],
|
|
147
148
|
},
|
|
149
|
+
{
|
|
150
|
+
version: 5,
|
|
151
|
+
statements: [
|
|
152
|
+
`CREATE TABLE IF NOT EXISTS workflow_metrics (
|
|
153
|
+
workflow_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
154
|
+
workflow_type TEXT NOT NULL,
|
|
155
|
+
session_id TEXT,
|
|
156
|
+
start_time TEXT NOT NULL,
|
|
157
|
+
end_time TEXT,
|
|
158
|
+
duration_ms INTEGER,
|
|
159
|
+
tools_used_json TEXT NOT NULL DEFAULT '[]',
|
|
160
|
+
steps_count INTEGER NOT NULL DEFAULT 0,
|
|
161
|
+
raw_tokens INTEGER NOT NULL DEFAULT 0,
|
|
162
|
+
compressed_tokens INTEGER NOT NULL DEFAULT 0,
|
|
163
|
+
saved_tokens INTEGER NOT NULL DEFAULT 0,
|
|
164
|
+
savings_pct REAL NOT NULL DEFAULT 0,
|
|
165
|
+
baseline_tokens INTEGER NOT NULL DEFAULT 0,
|
|
166
|
+
vs_baseline_pct REAL NOT NULL DEFAULT 0,
|
|
167
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
168
|
+
created_at TEXT NOT NULL,
|
|
169
|
+
FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE SET NULL
|
|
170
|
+
)`,
|
|
171
|
+
`CREATE INDEX IF NOT EXISTS idx_workflow_metrics_type_created
|
|
172
|
+
ON workflow_metrics(workflow_type, created_at DESC)`,
|
|
173
|
+
`CREATE INDEX IF NOT EXISTS idx_workflow_metrics_session
|
|
174
|
+
ON workflow_metrics(session_id, created_at DESC)`,
|
|
175
|
+
],
|
|
176
|
+
},
|
|
148
177
|
];
|
|
149
178
|
|
|
150
179
|
let sqliteModulePromise = null;
|
|
@@ -11,6 +11,9 @@ import { resolveSafePath } from '../utils/fs.js';
|
|
|
11
11
|
import { countTokens } from '../tokenCounter.js';
|
|
12
12
|
import { persistMetrics } from '../metrics.js';
|
|
13
13
|
import { predictContextFiles, recordContextAccess } from '../context-patterns.js';
|
|
14
|
+
import { recordToolUsage } from '../usage-feedback.js';
|
|
15
|
+
import { recordDecision, DECISION_REASONS, EXPECTED_BENEFITS } from '../decision-explainer.js';
|
|
16
|
+
import { recordDevctxOperation } from '../missed-opportunities.js';
|
|
14
17
|
import {
|
|
15
18
|
getDetailedDiff,
|
|
16
19
|
analyzeChangeImpact,
|
|
@@ -1219,15 +1222,44 @@ export const smartContext = async ({
|
|
|
1219
1222
|
const contentItems = context.filter((item) => typeof item.content === 'string' && item.content.length > 0).length;
|
|
1220
1223
|
const primaryItem = context.find((item) => item.role === 'primary');
|
|
1221
1224
|
|
|
1225
|
+
const savedTokens = Math.max(0, totalRawTokens - totalCompressedTokens);
|
|
1226
|
+
|
|
1222
1227
|
await persistMetrics({
|
|
1223
1228
|
tool: 'smart_context',
|
|
1224
1229
|
target: `${root} :: ${task}`,
|
|
1225
1230
|
rawTokens: totalRawTokens,
|
|
1226
1231
|
compressedTokens: totalCompressedTokens,
|
|
1227
|
-
savedTokens
|
|
1232
|
+
savedTokens,
|
|
1228
1233
|
savingsPct,
|
|
1229
1234
|
timestamp: new Date().toISOString(),
|
|
1230
1235
|
});
|
|
1236
|
+
|
|
1237
|
+
// Record usage for feedback
|
|
1238
|
+
recordToolUsage({
|
|
1239
|
+
tool: 'smart_context',
|
|
1240
|
+
savedTokens,
|
|
1241
|
+
target: task,
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
// Record devctx operation for missed opportunity detection
|
|
1245
|
+
recordDevctxOperation();
|
|
1246
|
+
|
|
1247
|
+
// Record decision explanation
|
|
1248
|
+
let reason = DECISION_REASONS.TASK_CONTEXT;
|
|
1249
|
+
if (diff) {
|
|
1250
|
+
reason = DECISION_REASONS.DIFF_ANALYSIS;
|
|
1251
|
+
} else if (context.some(c => c.role === 'caller' || c.role === 'test')) {
|
|
1252
|
+
reason = DECISION_REASONS.RELATED_FILES;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
recordDecision({
|
|
1256
|
+
tool: 'smart_context',
|
|
1257
|
+
action: `build context for "${task}"`,
|
|
1258
|
+
reason,
|
|
1259
|
+
alternative: 'Multiple smart_read + smart_search calls',
|
|
1260
|
+
expectedBenefit: `${EXPECTED_BENEFITS.TOKEN_SAVINGS(savedTokens)}, ${EXPECTED_BENEFITS.COMPLETE_CONTEXT}`,
|
|
1261
|
+
context: `${context.length} files, ${totalCompressedTokens} tokens (${savingsPct}% compression)`,
|
|
1262
|
+
});
|
|
1231
1263
|
|
|
1232
1264
|
if (prefetch && context.length > 0) {
|
|
1233
1265
|
try {
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
readMetricsEntries,
|
|
15
15
|
resolveMetricsInput,
|
|
16
16
|
} from '../metrics.js';
|
|
17
|
+
import { analyzeAdoption } from '../analytics/adoption.js';
|
|
17
18
|
|
|
18
19
|
const WINDOW_MS = {
|
|
19
20
|
'24h': 24 * 60 * 60 * 1000,
|
|
@@ -197,6 +198,8 @@ export const smartMetrics = async ({
|
|
|
197
198
|
.filter((entry) => (tool ? entry.tool === tool : true))
|
|
198
199
|
.filter((entry) => (resolvedSessionId ? entry.sessionId === resolvedSessionId : true));
|
|
199
200
|
|
|
201
|
+
const adoption = analyzeAdoption(filteredEntries);
|
|
202
|
+
|
|
200
203
|
return {
|
|
201
204
|
filePath: resolved.storagePath,
|
|
202
205
|
storagePath: resolved.storagePath,
|
|
@@ -212,6 +215,7 @@ export const smartMetrics = async ({
|
|
|
212
215
|
},
|
|
213
216
|
invalidLines,
|
|
214
217
|
summary: aggregateMetrics(filteredEntries),
|
|
218
|
+
adoption,
|
|
215
219
|
latestEntries: buildLatestEntries(filteredEntries, latest),
|
|
216
220
|
};
|
|
217
221
|
}
|
|
@@ -229,6 +233,8 @@ export const smartMetrics = async ({
|
|
|
229
233
|
window,
|
|
230
234
|
});
|
|
231
235
|
|
|
236
|
+
const adoption = analyzeAdoption(entries);
|
|
237
|
+
|
|
232
238
|
return {
|
|
233
239
|
filePath: resolved.storagePath,
|
|
234
240
|
storagePath: resolved.storagePath,
|
|
@@ -244,6 +250,7 @@ export const smartMetrics = async ({
|
|
|
244
250
|
},
|
|
245
251
|
invalidLines,
|
|
246
252
|
summary: aggregateMetrics(entries),
|
|
253
|
+
adoption,
|
|
247
254
|
latestEntries: buildLatestEntries(entries, latest),
|
|
248
255
|
};
|
|
249
256
|
};
|
|
@@ -18,6 +18,15 @@ export const smartReadBatch = async ({ files, maxTokens }) => {
|
|
|
18
18
|
maxTokens: item.maxTokens,
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
if (readResult.error) {
|
|
22
|
+
results.push({
|
|
23
|
+
filePath: item.path,
|
|
24
|
+
mode: item.mode ?? 'outline',
|
|
25
|
+
error: readResult.error,
|
|
26
|
+
});
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
const itemTokens = countTokens(readResult.content);
|
|
22
31
|
|
|
23
32
|
if (maxTokens && totalTokens + itemTokens > maxTokens && results.length > 0) {
|
package/src/tools/smart-read.js
CHANGED
|
@@ -9,6 +9,9 @@ import { isDockerfile, readTextFile } from '../utils/fs.js';
|
|
|
9
9
|
import { projectRoot } from '../utils/paths.js';
|
|
10
10
|
import { truncate } from '../utils/text.js';
|
|
11
11
|
import { countTokens } from '../tokenCounter.js';
|
|
12
|
+
import { recordToolUsage } from '../usage-feedback.js';
|
|
13
|
+
import { recordDecision, DECISION_REASONS, EXPECTED_BENEFITS } from '../decision-explainer.js';
|
|
14
|
+
import { recordDevctxOperation } from '../missed-opportunities.js';
|
|
12
15
|
|
|
13
16
|
const execFile = promisify(execFileCb);
|
|
14
17
|
import { summarizeGo, summarizeRust, summarizeJava, summarizeShell, summarizeTerraform, summarizeDockerfile, summarizeSql, extractGoSymbol, extractRustSymbol, extractJavaSymbol, summarizeCsharp, extractCsharpSymbol, summarizeKotlin, extractKotlinSymbol, summarizePhp, extractPhpSymbol, summarizeSwift, extractSwiftSymbol } from './smart-read/additional-languages.js';
|
|
@@ -363,7 +366,27 @@ const formatContextSections = (sections) => {
|
|
|
363
366
|
};
|
|
364
367
|
|
|
365
368
|
export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine, symbol, maxTokens, context: includeContext }) => {
|
|
366
|
-
|
|
369
|
+
let fullPath, content;
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
const result = readTextFile(filePath);
|
|
373
|
+
fullPath = result.fullPath;
|
|
374
|
+
content = result.content;
|
|
375
|
+
} catch (error) {
|
|
376
|
+
const errorMessage = error.message || String(error);
|
|
377
|
+
return {
|
|
378
|
+
error: errorMessage,
|
|
379
|
+
filePath,
|
|
380
|
+
mode,
|
|
381
|
+
metrics: buildMetrics({
|
|
382
|
+
tool: 'smart_read',
|
|
383
|
+
target: filePath,
|
|
384
|
+
rawText: '',
|
|
385
|
+
compressedText: errorMessage,
|
|
386
|
+
}),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
367
390
|
const extension = path.extname(fullPath).toLowerCase();
|
|
368
391
|
const mtime = getFileMtime(fullPath);
|
|
369
392
|
|
|
@@ -433,6 +456,38 @@ export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine
|
|
|
433
456
|
});
|
|
434
457
|
|
|
435
458
|
await persistMetrics(metrics);
|
|
459
|
+
|
|
460
|
+
// Record usage for feedback
|
|
461
|
+
recordToolUsage({
|
|
462
|
+
tool: 'smart_read',
|
|
463
|
+
savedTokens: metrics.savedTokens,
|
|
464
|
+
target: path.relative(projectRoot, fullPath),
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Record devctx operation for missed opportunity detection
|
|
468
|
+
recordDevctxOperation();
|
|
469
|
+
|
|
470
|
+
// Record decision explanation
|
|
471
|
+
const lineCount = content.split('\n').length;
|
|
472
|
+
let reason = DECISION_REASONS.LARGE_FILE;
|
|
473
|
+
let expectedBenefit = EXPECTED_BENEFITS.TOKEN_SAVINGS(metrics.savedTokens);
|
|
474
|
+
|
|
475
|
+
if (mode === 'symbol') {
|
|
476
|
+
reason = DECISION_REASONS.SYMBOL_EXTRACTION;
|
|
477
|
+
} else if (validBudget && effectiveMode !== mode) {
|
|
478
|
+
reason = DECISION_REASONS.TOKEN_BUDGET;
|
|
479
|
+
} else if (lineCount < 100) {
|
|
480
|
+
reason = `File is small (${lineCount} lines), but using ${effectiveMode} mode for consistency`;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
recordDecision({
|
|
484
|
+
tool: 'smart_read',
|
|
485
|
+
action: `read ${path.relative(projectRoot, fullPath)} (${effectiveMode} mode)`,
|
|
486
|
+
reason,
|
|
487
|
+
alternative: 'Read (full file)',
|
|
488
|
+
expectedBenefit,
|
|
489
|
+
context: `${lineCount} lines, ${metrics.rawTokens} tokens → ${metrics.compressedTokens} tokens`,
|
|
490
|
+
});
|
|
436
491
|
|
|
437
492
|
const confidence = { parser, truncated, cached: cacheHit && !contextResult };
|
|
438
493
|
if (contextResult) confidence.graphCoverage = contextResult.graphCoverage;
|
|
@@ -8,6 +8,9 @@ import { loadIndex, queryIndex, queryRelated } from '../index.js';
|
|
|
8
8
|
import { projectRoot } from '../utils/paths.js';
|
|
9
9
|
import { isBinaryBuffer, isDockerfile, resolveSafePath } from '../utils/fs.js';
|
|
10
10
|
import { truncate } from '../utils/text.js';
|
|
11
|
+
import { recordToolUsage } from '../usage-feedback.js';
|
|
12
|
+
import { recordDecision, DECISION_REASONS, EXPECTED_BENEFITS } from '../decision-explainer.js';
|
|
13
|
+
import { recordDevctxOperation } from '../missed-opportunities.js';
|
|
11
14
|
|
|
12
15
|
const execFile = promisify(execFileCallback);
|
|
13
16
|
const supportedGlobs = [
|
|
@@ -381,6 +384,34 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
|
|
|
381
384
|
});
|
|
382
385
|
|
|
383
386
|
await persistMetrics(metrics);
|
|
387
|
+
|
|
388
|
+
// Record usage for feedback
|
|
389
|
+
recordToolUsage({
|
|
390
|
+
tool: 'smart_search',
|
|
391
|
+
savedTokens: metrics.savedTokens,
|
|
392
|
+
target: query,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Record devctx operation for missed opportunity detection
|
|
396
|
+
recordDevctxOperation();
|
|
397
|
+
|
|
398
|
+
// Record decision explanation
|
|
399
|
+
let reason = DECISION_REASONS.MULTIPLE_FILES;
|
|
400
|
+
if (validIntent) {
|
|
401
|
+
reason = DECISION_REASONS.INTENT_AWARE;
|
|
402
|
+
}
|
|
403
|
+
if (indexHits && indexHits.size > 0) {
|
|
404
|
+
reason = DECISION_REASONS.INDEX_BOOST;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
recordDecision({
|
|
408
|
+
tool: 'smart_search',
|
|
409
|
+
action: `search "${query}"${validIntent ? ` (intent: ${validIntent})` : ''}`,
|
|
410
|
+
reason,
|
|
411
|
+
alternative: 'Grep (unranked results)',
|
|
412
|
+
expectedBenefit: `${EXPECTED_BENEFITS.TOKEN_SAVINGS(metrics.savedTokens)}, ${EXPECTED_BENEFITS.BETTER_RANKING}`,
|
|
413
|
+
context: `${dedupedMatches.length} matches in ${groups.length} files, ranked by relevance`,
|
|
414
|
+
});
|
|
384
415
|
|
|
385
416
|
let retrievalConfidence = 'high';
|
|
386
417
|
if (provenance) {
|