smart-context-mcp 1.6.2 → 1.7.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.
@@ -1,123 +1,13 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { persistMetrics } from '../metrics.js';
3
- import { countTokens } from '../tokenCounter.js';
4
- import { buildOperationalContextLines } from '../client-contract.js';
5
- import { smartSummary } from '../tools/smart-summary.js';
6
- import { smartTurn } from '../tools/smart-turn.js';
7
-
8
- const DEFAULT_EVENT = 'session_end';
9
- const START_MAX_TOKENS = 350;
10
- const END_MAX_TOKENS = 350;
11
- const SAFE_CONTINUITY_STATES = new Set(['aligned', 'resume']);
12
-
13
- const normalizeWhitespace = (value) => String(value ?? '').replace(/\s+/g, ' ').trim();
14
-
15
- const truncate = (value, maxLength = 160) => {
16
- const normalized = normalizeWhitespace(value);
17
- if (normalized.length <= maxLength) {
18
- return normalized;
19
- }
20
-
21
- if (maxLength <= 3) {
22
- return '';
23
- }
24
-
25
- return `${normalized.slice(0, maxLength - 3)}...`;
26
- };
27
-
28
- const extractNextStep = (value) => {
29
- const normalized = normalizeWhitespace(value);
30
- if (!normalized) {
31
- return '';
32
- }
33
-
34
- const explicitMatch = normalized.match(/(?:next step|siguiente paso)\s*[:\-]\s*([^.;\n]{12,180})/i);
35
- if (explicitMatch?.[1]) {
36
- return truncate(explicitMatch[1], 150);
37
- }
38
-
39
- return '';
40
- };
41
-
42
- const buildContextLines = (startResult) => {
43
- const context = buildOperationalContextLines(startResult, {
44
- sessionStart: false,
45
- maxLineLength: 120,
46
- maxLines: 8,
47
- maxChars: 560,
48
- });
49
- return context ? context.split('\n') : [];
50
- };
51
-
52
- export const buildWrappedPrompt = ({ prompt, startResult }) => {
53
- const lines = buildContextLines(startResult);
54
- if (lines.length === 0) {
55
- return prompt;
56
- }
57
-
58
- return [
59
- 'Use the persisted devctx project context below only if it is relevant to the user request.',
60
- ...lines.map((line) => `- ${line}`),
61
- '',
62
- 'User request:',
63
- prompt,
64
- ].join('\n');
65
- };
66
-
67
- const buildFreshSessionUpdate = (prompt) => {
68
- const preview = truncate(prompt, 140);
69
- return {
70
- goal: truncate(prompt, 120),
71
- status: 'planning',
72
- currentFocus: preview,
73
- pinnedContext: [preview],
74
- nextStep: 'Inspect the relevant code, validate task boundaries, and checkpoint the first concrete milestone.',
75
- };
76
- };
77
-
78
- const ensureIsolatedSession = async ({ prompt, sessionId, startResult }) => {
79
- if (sessionId || !startResult?.sessionId) {
80
- return {
81
- startResult,
82
- isolated: Boolean(startResult?.isolatedSession),
83
- previousSessionId: startResult?.previousSessionId ?? null,
84
- };
85
- }
86
-
87
- if (startResult?.isolatedSession) {
88
- return {
89
- startResult,
90
- isolated: true,
91
- previousSessionId: startResult.previousSessionId ?? null,
92
- };
93
- }
94
-
95
- if (SAFE_CONTINUITY_STATES.has(startResult.continuity?.state ?? '')) {
96
- return {
97
- startResult,
98
- isolated: false,
99
- };
100
- }
101
-
102
- const created = await smartSummary({
103
- action: 'update',
104
- update: buildFreshSessionUpdate(prompt),
105
- maxTokens: START_MAX_TOKENS,
106
- });
107
- const isolatedStart = await smartTurn({
108
- phase: 'start',
109
- sessionId: created.sessionId,
110
- prompt,
111
- ensureSession: false,
112
- maxTokens: START_MAX_TOKENS,
113
- });
114
-
115
- return {
116
- startResult: isolatedStart,
117
- isolated: true,
118
- previousSessionId: startResult.sessionId,
119
- };
120
- };
2
+ import {
3
+ buildWrappedPrompt,
4
+ computeContextOverhead,
5
+ finalizeManagedRun,
6
+ recordAgentWrapperMetric,
7
+ resolveManagedStart,
8
+ } from './base-orchestrator.js';
9
+ import { normalizeWhitespace } from './policy/event-policy.js';
10
+ import { detectClient } from '../utils/client-detection.js';
121
11
 
122
12
  const runChildProcess = ({ command, args, env, stdinText, streamOutput }) => new Promise((resolve, reject) => {
123
13
  const child = spawn(command, args, {
@@ -154,36 +44,8 @@ const runChildProcess = ({ command, args, env, stdinText, streamOutput }) => new
154
44
  }
155
45
  });
156
46
 
157
- const buildEndUpdate = ({ prompt, childResult }) => {
158
- const combinedOutput = [childResult.stdout, childResult.stderr].filter(Boolean).join('\n');
159
- const nextStep = extractNextStep(combinedOutput);
160
- const update = {
161
- currentFocus: truncate(prompt, 140),
162
- };
163
-
164
- if (nextStep) {
165
- update.nextStep = nextStep;
166
- } else if (childResult.exitCode === 0) {
167
- update.nextStep = 'Review the latest headless agent output and checkpoint any concrete file changes before continuing.';
168
- } else {
169
- update.status = 'blocked';
170
- update.whyBlocked = `Headless agent command exited with code ${childResult.exitCode}.`;
171
- update.nextStep = 'Review the headless agent stderr/output and rerun the command once the issue is fixed.';
172
- }
173
-
174
- return update;
175
- };
176
-
177
- const inferEndEvent = ({ requestedEvent, childResult }) => {
178
- if (requestedEvent) {
179
- return requestedEvent;
180
- }
181
-
182
- return childResult.exitCode === 0 ? DEFAULT_EVENT : 'blocker';
183
- };
184
-
185
47
  export const runHeadlessWrapper = async ({
186
- client = 'generic',
48
+ client = null,
187
49
  prompt,
188
50
  command,
189
51
  args = [],
@@ -195,6 +57,8 @@ export const runHeadlessWrapper = async ({
195
57
  runCommand = runChildProcess,
196
58
  preparedStartResult = null,
197
59
  } = {}) => {
60
+ const resolvedClient = client ?? detectClient();
61
+
198
62
  if (!normalizeWhitespace(prompt)) {
199
63
  throw new Error('prompt is required');
200
64
  }
@@ -203,41 +67,32 @@ export const runHeadlessWrapper = async ({
203
67
  throw new Error('command is required unless dryRun=true');
204
68
  }
205
69
 
206
- const start = preparedStartResult ?? await smartTurn({
207
- phase: 'start',
208
- sessionId,
70
+ const sessionResolution = await resolveManagedStart({
209
71
  prompt,
72
+ sessionId,
73
+ preparedStartResult,
210
74
  ensureSession: true,
211
- maxTokens: START_MAX_TOKENS,
75
+ allowIsolation: true,
212
76
  });
213
- const sessionResolution = await ensureIsolatedSession({ prompt, sessionId, startResult: start });
214
77
  const effectiveStart = sessionResolution.startResult;
215
78
  const wrappedPrompt = buildWrappedPrompt({ prompt, startResult: effectiveStart });
216
- const overheadTokens = Math.max(0, countTokens(wrappedPrompt) - countTokens(prompt));
79
+ const overheadTokens = computeContextOverhead({ prompt, wrappedPrompt });
217
80
 
218
- await persistMetrics({
219
- tool: 'agent_wrapper',
220
- action: `${client}:start`,
81
+ await recordAgentWrapperMetric({
82
+ phase: 'start',
83
+ client: resolvedClient,
221
84
  sessionId: effectiveStart.sessionId ?? null,
222
- rawTokens: 0,
223
- compressedTokens: 0,
224
- savedTokens: 0,
225
- savingsPct: 0,
226
- metadata: {
227
- isContextOverhead: overheadTokens > 0,
228
- overheadTokens,
229
- client,
230
- dryRun,
231
- isolatedSession: sessionResolution.isolated,
232
- previousSessionId: sessionResolution.previousSessionId ?? null,
233
- },
234
- timestamp: new Date().toISOString(),
85
+ dryRun,
86
+ overheadTokens,
87
+ isolatedSession: sessionResolution.isolated,
88
+ previousSessionId: sessionResolution.previousSessionId ?? null,
89
+ autoStarted: sessionResolution.autoStarted,
235
90
  });
236
91
 
237
92
  const finalArgs = stdinPrompt ? [...args] : [...args, wrappedPrompt];
238
93
  if (dryRun) {
239
94
  return {
240
- client,
95
+ client: resolvedClient,
241
96
  dryRun: true,
242
97
  command,
243
98
  args: finalArgs,
@@ -262,36 +117,25 @@ export const runHeadlessWrapper = async ({
262
117
  streamOutput,
263
118
  });
264
119
 
265
- const resolvedEvent = inferEndEvent({ requestedEvent: event, childResult });
266
- const end = await smartTurn({
267
- phase: 'end',
120
+ const { resolvedEvent, endResult } = await finalizeManagedRun({
121
+ prompt,
122
+ childResult,
268
123
  sessionId: effectiveStart.sessionId ?? sessionId ?? undefined,
269
- event: resolvedEvent,
270
- update: buildEndUpdate({ prompt, childResult }),
271
- maxTokens: END_MAX_TOKENS,
124
+ requestedEvent: event,
272
125
  });
273
126
 
274
- await persistMetrics({
275
- tool: 'agent_wrapper',
276
- action: `${client}:end`,
127
+ await recordAgentWrapperMetric({
128
+ phase: 'end',
129
+ client: resolvedClient,
277
130
  sessionId: effectiveStart.sessionId ?? null,
278
- rawTokens: 0,
279
- compressedTokens: 0,
280
- savedTokens: 0,
281
- savingsPct: 0,
282
- metadata: {
283
- client,
284
- exitCode: childResult.exitCode,
285
- event: resolvedEvent,
286
- isContextOverhead: false,
287
- overheadTokens: 0,
288
- isolatedSession: sessionResolution.isolated,
289
- },
290
- timestamp: new Date().toISOString(),
131
+ isolatedSession: sessionResolution.isolated,
132
+ exitCode: childResult.exitCode,
133
+ event: resolvedEvent,
134
+ autoStarted: sessionResolution.autoStarted,
291
135
  });
292
136
 
293
137
  return {
294
- client,
138
+ client: resolvedClient,
295
139
  command,
296
140
  args: finalArgs,
297
141
  wrappedPrompt,
@@ -301,7 +145,7 @@ export const runHeadlessWrapper = async ({
301
145
  stdout: childResult.stdout,
302
146
  stderr: childResult.stderr,
303
147
  start: effectiveStart,
304
- end,
148
+ end: endResult,
305
149
  sessionId: effectiveStart.sessionId ?? sessionId ?? null,
306
150
  isolatedSession: sessionResolution.isolated,
307
151
  };
@@ -0,0 +1,297 @@
1
+ import { smartContext } from '../../tools/smart-context.js';
2
+ import { smartSearch } from '../../tools/smart-search.js';
3
+
4
+ export const SAFE_CONTINUITY_STATES = new Set(['aligned', 'resume']);
5
+
6
+ export const MAX_TOP_FILES = 3;
7
+ export const MAX_PREFLIGHT_HINTS = 2;
8
+ export const MAX_FOCUS_LENGTH = 140;
9
+ export const MAX_GOAL_LENGTH = 120;
10
+ export const MAX_NEXT_STEP_LENGTH = 150;
11
+ export const DEFAULT_TRUNCATE_LENGTH = 160;
12
+ export const MIN_NEXT_STEP_LENGTH = 12;
13
+ export const MAX_NEXT_STEP_CAPTURE_LENGTH = 180;
14
+ export const MAX_RECOMMENDED_TOOLS = 3;
15
+
16
+ export const normalizeWhitespace = (value) => String(value ?? '').replace(/\s+/g, ' ').trim();
17
+
18
+ export const truncate = (value, maxLength = DEFAULT_TRUNCATE_LENGTH) => {
19
+ const normalized = normalizeWhitespace(value);
20
+ if (normalized.length <= maxLength) {
21
+ return normalized;
22
+ }
23
+
24
+ if (maxLength <= 3) {
25
+ return '';
26
+ }
27
+
28
+ return `${normalized.slice(0, maxLength - 3)}...`;
29
+ };
30
+
31
+ const asArray = (value) => Array.isArray(value) ? value : [];
32
+
33
+ export const uniqueCompact = (values) => [...new Set(
34
+ asArray(values)
35
+ .map((value) => normalizeWhitespace(value))
36
+ .filter(Boolean),
37
+ )];
38
+
39
+ export const extractContextTopFiles = (topFiles) => uniqueCompact(asArray(topFiles).map((item) => {
40
+ if (typeof item === 'string') {
41
+ return item;
42
+ }
43
+
44
+ return item?.file ?? item?.path ?? '';
45
+ })).slice(0, MAX_TOP_FILES);
46
+
47
+ export const extractPreflightTopFiles = (preflightResult) => {
48
+ if (!preflightResult) {
49
+ return [];
50
+ }
51
+
52
+ if (preflightResult.tool === 'smart_context') {
53
+ return uniqueCompact(asArray(preflightResult.result?.context).map((item) => item?.file).filter(Boolean)).slice(0, MAX_TOP_FILES);
54
+ }
55
+
56
+ if (preflightResult.tool === 'smart_search') {
57
+ return extractContextTopFiles(preflightResult.result?.topFiles);
58
+ }
59
+
60
+ return [];
61
+ };
62
+
63
+ export const extractPreflightHints = (preflightResult) => {
64
+ if (!preflightResult) {
65
+ return [];
66
+ }
67
+
68
+ if (preflightResult.tool === 'smart_context') {
69
+ return uniqueCompact(preflightResult.result?.hints).slice(0, MAX_PREFLIGHT_HINTS);
70
+ }
71
+
72
+ if (preflightResult.tool === 'smart_search') {
73
+ const totalMatches = Number(preflightResult.result?.totalMatches ?? 0);
74
+ if (totalMatches > 0) {
75
+ return [`${totalMatches} search match(es) surfaced for the workflow target`];
76
+ }
77
+ }
78
+
79
+ return [];
80
+ };
81
+
82
+ export const buildPreflightSummary = (preflightResult) => {
83
+ if (!preflightResult) {
84
+ return null;
85
+ }
86
+
87
+ return {
88
+ tool: preflightResult.tool,
89
+ topFiles: extractPreflightTopFiles(preflightResult),
90
+ hints: extractPreflightHints(preflightResult),
91
+ totalMatches: Number(preflightResult.result?.totalMatches ?? 0),
92
+ };
93
+ };
94
+
95
+ export const buildPreflightTask = ({ workflowProfile, prompt, startResult }) => {
96
+ if (!workflowProfile || typeof workflowProfile !== 'object') {
97
+ return '';
98
+ }
99
+
100
+ const normalizedPrompt = normalizeWhitespace(prompt);
101
+ const persistedNextStep = normalizeWhitespace(startResult?.summary?.nextStep);
102
+ const currentFocus = normalizeWhitespace(startResult?.summary?.currentFocus);
103
+ const refreshedTopFiles = extractContextTopFiles(startResult?.refreshedContext?.topFiles);
104
+
105
+ if (workflowProfile.commandName === 'continue' || workflowProfile.commandName === 'resume') {
106
+ if (persistedNextStep) {
107
+ return persistedNextStep;
108
+ }
109
+ if (currentFocus) {
110
+ return currentFocus;
111
+ }
112
+ }
113
+
114
+ if (workflowProfile.commandName === 'task' && currentFocus && persistedNextStep) {
115
+ return `${currentFocus}. ${persistedNextStep}`;
116
+ }
117
+
118
+ if (normalizedPrompt) {
119
+ return normalizedPrompt;
120
+ }
121
+
122
+ if (refreshedTopFiles.length > 0) {
123
+ return `Inspect ${refreshedTopFiles.join(', ')} and continue the persisted task`;
124
+ }
125
+
126
+ return workflowProfile.label;
127
+ };
128
+
129
+ export const runWorkflowPreflight = async ({
130
+ workflowProfile,
131
+ prompt,
132
+ startResult,
133
+ contextTool = smartContext,
134
+ searchTool = smartSearch,
135
+ }) => {
136
+ const preflight = workflowProfile.preflight;
137
+ if (!preflight) {
138
+ return null;
139
+ }
140
+
141
+ const preflightTask = buildPreflightTask({ workflowProfile, prompt, startResult });
142
+
143
+ if (preflight.tool === 'smart_context') {
144
+ const request = {
145
+ task: preflightTask,
146
+ detail: preflight.detail ?? 'minimal',
147
+ include: preflight.include ?? ['hints'],
148
+ maxTokens: preflight.maxTokens ?? 1200,
149
+ };
150
+ const result = await contextTool(request);
151
+ return {
152
+ tool: 'smart_context',
153
+ request,
154
+ result,
155
+ };
156
+ }
157
+
158
+ if (preflight.tool === 'smart_search') {
159
+ const request = {
160
+ query: preflightTask,
161
+ intent: preflight.intent ?? workflowProfile.workflowIntent,
162
+ };
163
+ const result = await searchTool(request);
164
+ return {
165
+ tool: 'smart_search',
166
+ request,
167
+ result,
168
+ };
169
+ }
170
+
171
+ return null;
172
+ };
173
+
174
+ export const buildContinuityGuidance = ({ startResult }) => {
175
+ const continuityState = startResult?.continuity?.state ?? 'unknown';
176
+ const lines = [`- Continuity: ${continuityState}`];
177
+ const nextStep = normalizeWhitespace(startResult?.summary?.nextStep);
178
+ const currentFocus = normalizeWhitespace(startResult?.summary?.currentFocus);
179
+ const refreshedTopFiles = extractContextTopFiles(startResult?.refreshedContext?.topFiles);
180
+ const recommendedNextTools = asArray(startResult?.recommendedPath?.nextTools)
181
+ .map((tool) => normalizeWhitespace(tool))
182
+ .filter(Boolean)
183
+ .slice(0, MAX_RECOMMENDED_TOOLS);
184
+
185
+ if (currentFocus) {
186
+ lines.push(`- Persisted focus: ${truncate(currentFocus, MAX_FOCUS_LENGTH)}`);
187
+ }
188
+
189
+ if (nextStep) {
190
+ lines.push(`- Persisted next step: ${truncate(nextStep, MAX_FOCUS_LENGTH)}`);
191
+ }
192
+
193
+ if (refreshedTopFiles.length > 0) {
194
+ lines.push(`- Refreshed top files: ${refreshedTopFiles.join(', ')}`);
195
+ }
196
+
197
+ if (recommendedNextTools.length > 0) {
198
+ lines.push(`- smart_turn suggested: ${recommendedNextTools.join(' -> ')}`);
199
+ }
200
+
201
+ if (startResult?.isolatedSession) {
202
+ lines.push('- Session handling: smart_turn already isolated this work from the previous session; revalidate before assuming old focus.');
203
+ } else if (continuityState === 'aligned' || continuityState === 'resume') {
204
+ lines.push('- Session handling: reuse the active session context and stay close to the persisted next step unless the task proves otherwise.');
205
+ } else if (continuityState === 'possible_shift' || continuityState === 'context_mismatch') {
206
+ lines.push('- Session handling: treat this as a shifted slice, validate the working set early, and avoid silent context reuse.');
207
+ }
208
+
209
+ return lines;
210
+ };
211
+
212
+ export const buildWorkflowPromptWithPolicy = ({
213
+ prompt,
214
+ workflowProfile,
215
+ preflightSummary,
216
+ startResult,
217
+ }) => {
218
+ const lines = [
219
+ prompt,
220
+ '',
221
+ 'Workflow policy:',
222
+ `- Mode: ${workflowProfile.policyMode}`,
223
+ `- Intent: ${workflowProfile.workflowIntent}`,
224
+ `- Prefer this tool order: ${workflowProfile.nextTools.join(' -> ')}`,
225
+ ];
226
+
227
+ if (workflowProfile.checkpointStrategy) {
228
+ lines.push(`- Checkpoint rule: ${workflowProfile.checkpointStrategy}`);
229
+ }
230
+
231
+ lines.push(...buildContinuityGuidance({ startResult }));
232
+
233
+ if (preflightSummary?.tool) {
234
+ lines.push(`- Preflight: ${preflightSummary.tool}`);
235
+ }
236
+
237
+ if (preflightSummary?.topFiles?.length) {
238
+ lines.push(`- Focus files: ${preflightSummary.topFiles.join(', ')}`);
239
+ }
240
+
241
+ if (preflightSummary?.hints?.length) {
242
+ lines.push(`- Signals: ${preflightSummary.hints.map((hint) => truncate(hint, 120)).join(' | ')}`);
243
+ }
244
+
245
+ return lines.join('\n');
246
+ };
247
+
248
+ export const buildWorkflowPolicyPayload = ({ commandName, workflowProfile, preflightSummary }) => ({
249
+ commandName,
250
+ label: workflowProfile.label,
251
+ policyMode: workflowProfile.policyMode,
252
+ intent: workflowProfile.workflowIntent,
253
+ specialized: workflowProfile.specialized,
254
+ nextTools: [...workflowProfile.nextTools],
255
+ checkpointStrategy: workflowProfile.checkpointStrategy,
256
+ preflight: preflightSummary,
257
+ });
258
+
259
+ export const extractNextStep = (value) => {
260
+ const normalized = normalizeWhitespace(value);
261
+ if (!normalized) {
262
+ return '';
263
+ }
264
+
265
+ const explicitMatch = normalized.match(new RegExp(
266
+ `(?:next step|siguiente paso)\\s*[:\\-]\\s*([^.;\\n]{${MIN_NEXT_STEP_LENGTH},${MAX_NEXT_STEP_CAPTURE_LENGTH}})`,
267
+ 'i',
268
+ ));
269
+ if (explicitMatch?.[1]) {
270
+ return truncate(explicitMatch[1], MAX_NEXT_STEP_LENGTH);
271
+ }
272
+
273
+ return '';
274
+ };
275
+
276
+ export const buildTaskRunnerAutomaticity = ({
277
+ isWorkflowCommand = false,
278
+ startResult = null,
279
+ endResult = null,
280
+ workflowPolicy = null,
281
+ usedWrapper = false,
282
+ overheadTokens = 0,
283
+ managedByBaseOrchestrator = false,
284
+ }) => {
285
+ const safeOverheadTokens = Number.isFinite(overheadTokens) ? Math.max(0, overheadTokens) : 0;
286
+ const checkpointPersisted = Boolean(endResult && !endResult.checkpoint?.skipped && !endResult.checkpoint?.blocked);
287
+
288
+ return {
289
+ managedByBaseOrchestrator,
290
+ autoStartTriggered: isWorkflowCommand && Boolean(startResult),
291
+ autoPreflightTriggered: isWorkflowCommand && Boolean(workflowPolicy?.preflight?.tool),
292
+ autoCheckpointTriggered: checkpointPersisted,
293
+ autoWrappedPrompt: usedWrapper && safeOverheadTokens > 0,
294
+ isolatedSession: Boolean(startResult?.isolatedSession),
295
+ contextOverheadTokens: safeOverheadTokens,
296
+ };
297
+ };