smart-context-mcp 1.6.2 → 1.7.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.
@@ -1,123 +1,12 @@
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';
121
10
 
122
11
  const runChildProcess = ({ command, args, env, stdinText, streamOutput }) => new Promise((resolve, reject) => {
123
12
  const child = spawn(command, args, {
@@ -154,34 +43,6 @@ const runChildProcess = ({ command, args, env, stdinText, streamOutput }) => new
154
43
  }
155
44
  });
156
45
 
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
46
  export const runHeadlessWrapper = async ({
186
47
  client = 'generic',
187
48
  prompt,
@@ -203,35 +64,26 @@ export const runHeadlessWrapper = async ({
203
64
  throw new Error('command is required unless dryRun=true');
204
65
  }
205
66
 
206
- const start = preparedStartResult ?? await smartTurn({
207
- phase: 'start',
208
- sessionId,
67
+ const sessionResolution = await resolveManagedStart({
209
68
  prompt,
69
+ sessionId,
70
+ preparedStartResult,
210
71
  ensureSession: true,
211
- maxTokens: START_MAX_TOKENS,
72
+ allowIsolation: true,
212
73
  });
213
- const sessionResolution = await ensureIsolatedSession({ prompt, sessionId, startResult: start });
214
74
  const effectiveStart = sessionResolution.startResult;
215
75
  const wrappedPrompt = buildWrappedPrompt({ prompt, startResult: effectiveStart });
216
- const overheadTokens = Math.max(0, countTokens(wrappedPrompt) - countTokens(prompt));
76
+ const overheadTokens = computeContextOverhead({ prompt, wrappedPrompt });
217
77
 
218
- await persistMetrics({
219
- tool: 'agent_wrapper',
220
- action: `${client}:start`,
78
+ await recordAgentWrapperMetric({
79
+ phase: 'start',
80
+ client,
221
81
  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(),
82
+ dryRun,
83
+ overheadTokens,
84
+ isolatedSession: sessionResolution.isolated,
85
+ previousSessionId: sessionResolution.previousSessionId ?? null,
86
+ autoStarted: sessionResolution.autoStarted,
235
87
  });
236
88
 
237
89
  const finalArgs = stdinPrompt ? [...args] : [...args, wrappedPrompt];
@@ -262,32 +114,21 @@ export const runHeadlessWrapper = async ({
262
114
  streamOutput,
263
115
  });
264
116
 
265
- const resolvedEvent = inferEndEvent({ requestedEvent: event, childResult });
266
- const end = await smartTurn({
267
- phase: 'end',
117
+ const { resolvedEvent, endResult } = await finalizeManagedRun({
118
+ prompt,
119
+ childResult,
268
120
  sessionId: effectiveStart.sessionId ?? sessionId ?? undefined,
269
- event: resolvedEvent,
270
- update: buildEndUpdate({ prompt, childResult }),
271
- maxTokens: END_MAX_TOKENS,
121
+ requestedEvent: event,
272
122
  });
273
123
 
274
- await persistMetrics({
275
- tool: 'agent_wrapper',
276
- action: `${client}:end`,
124
+ await recordAgentWrapperMetric({
125
+ phase: 'end',
126
+ client,
277
127
  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(),
128
+ isolatedSession: sessionResolution.isolated,
129
+ exitCode: childResult.exitCode,
130
+ event: resolvedEvent,
131
+ autoStarted: sessionResolution.autoStarted,
291
132
  });
292
133
 
293
134
  return {
@@ -301,7 +142,7 @@ export const runHeadlessWrapper = async ({
301
142
  stdout: childResult.stdout,
302
143
  stderr: childResult.stderr,
303
144
  start: effectiveStart,
304
- end,
145
+ end: endResult,
305
146
  sessionId: effectiveStart.sessionId ?? sessionId ?? null,
306
147
  isolatedSession: sessionResolution.isolated,
307
148
  };
@@ -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
+ };