smart-context-mcp 1.19.0 → 1.20.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 +1 -1
- package/package.json +5 -2
- package/server.json +2 -2
- package/src/global-memory/store.js +101 -1
- package/src/orchestration/base-orchestrator.js +37 -1
- package/src/server.js +59 -15
- package/src/storage/sqlite.js +75 -1
- package/src/task-runner.js +4 -0
- package/src/tools/global-memory.js +12 -1
- package/src/tools/smart-context.js +18 -4
- package/src/tools/smart-read-batch.js +26 -3
- package/src/tools/smart-read.js +128 -15
- package/src/tools/smart-search.js +665 -57
- package/src/tools/smart-turn.js +88 -4
- package/src/utils/task-budget.js +116 -0
package/src/tools/smart-turn.js
CHANGED
|
@@ -11,10 +11,12 @@ import { persistMetrics } from '../metrics.js';
|
|
|
11
11
|
import { runStorageMaintenance } from '../storage/sqlite.js';
|
|
12
12
|
import { PRODUCT_QUALITY_ANALYTICS_KIND } from '../analytics/product-quality.js';
|
|
13
13
|
import { attachSafetyMetadata, buildMutationSafety } from '../utils/mutation-safety.js';
|
|
14
|
+
import { normalizeTokenBudget, peekRemainingBudget } from '../utils/task-budget.js';
|
|
14
15
|
import { smartContext } from './smart-context.js';
|
|
15
16
|
import { smartMetrics } from './smart-metrics.js';
|
|
16
17
|
import { smartSummary } from './smart-summary.js';
|
|
17
18
|
import { deriveStartActions, deriveEndActions } from '../turn/next-actions.js';
|
|
19
|
+
import { isSimpleTask } from '../orchestration/policy/event-policy.js';
|
|
18
20
|
|
|
19
21
|
const isStorageUnhealthy = (health) =>
|
|
20
22
|
health && health.status !== 'ok' && health.status !== null && health.status !== undefined;
|
|
@@ -30,6 +32,7 @@ const DEFAULT_END_MAX_TOKENS = 500;
|
|
|
30
32
|
const DEFAULT_END_EVENT = 'milestone';
|
|
31
33
|
const DEFAULT_REFRESH_CONTEXT_MAX_TOKENS = 1400;
|
|
32
34
|
const MAX_PROMPT_PREVIEW = 160;
|
|
35
|
+
const SIMPLE_TASK_SKIP_MAX_LENGTH = 40;
|
|
33
36
|
const REFRESHED_CONTEXT_FILE_LIMIT = 3;
|
|
34
37
|
const SAFE_CONTINUITY_STATES = new Set(['aligned', 'resume']);
|
|
35
38
|
const WORKFLOW_END_EVENTS = new Set(['milestone', 'task_complete', 'session_end', 'blocker']);
|
|
@@ -178,6 +181,46 @@ const hasMeaningfulPrompt = (prompt) => {
|
|
|
178
181
|
return normalized.length >= 20 && extractTerms(normalized).length >= 4;
|
|
179
182
|
};
|
|
180
183
|
|
|
184
|
+
const buildSimpleTaskStartResult = ({ prompt, tokenBudget, verbosity = DEFAULT_VERBOSITY }) => {
|
|
185
|
+
const normalizedTokenBudget = normalizeTokenBudget(tokenBudget);
|
|
186
|
+
const recommendedPath = verbosity === 'minimal'
|
|
187
|
+
? {
|
|
188
|
+
phase: 'start',
|
|
189
|
+
mode: 'simple_task_skip',
|
|
190
|
+
nextTools: ['smart_read', 'smart_search'],
|
|
191
|
+
next: 'smart_read: Skip smart_turn for this simple task and use lightweight read/search directly.',
|
|
192
|
+
}
|
|
193
|
+
: {
|
|
194
|
+
phase: 'start',
|
|
195
|
+
mode: 'simple_task_skip',
|
|
196
|
+
contextSource: 'direct_prompt',
|
|
197
|
+
continuityState: 'simple_task_skip',
|
|
198
|
+
ensureSessionRecommended: false,
|
|
199
|
+
autoCreated: false,
|
|
200
|
+
isolatedSession: false,
|
|
201
|
+
nextTools: ['smart_read', 'smart_search'],
|
|
202
|
+
nextActions: deriveStartActions({ prompt, mode: 'simple_task_skip', refreshedContext: null, summaryResult: null }),
|
|
203
|
+
next: 'smart_read: Skip smart_turn for this simple task and use lightweight read/search directly.',
|
|
204
|
+
instructions: 'smart_read: Skip smart_turn for this simple task and use lightweight read/search directly. | smart_search: Use only if the task grows beyond a single targeted read.',
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
phase: 'start',
|
|
209
|
+
skipSmartTurn: true,
|
|
210
|
+
continuity: {
|
|
211
|
+
state: 'simple_task_skip',
|
|
212
|
+
shouldReuseContext: false,
|
|
213
|
+
reason: 'Simple task heuristic skipped persisted continuity setup to avoid overhead.',
|
|
214
|
+
},
|
|
215
|
+
...(normalizedTokenBudget ? {
|
|
216
|
+
taskBudget: normalizedTokenBudget,
|
|
217
|
+
remainingBudget: peekRemainingBudget({ tokenBudget: normalizedTokenBudget }),
|
|
218
|
+
} : {}),
|
|
219
|
+
recommendedPath,
|
|
220
|
+
message: 'Simple task heuristic skipped smart_turn(start); use lightweight read/search flow unless the task grows.',
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
|
|
181
224
|
const buildAutoCreateUpdate = (prompt) => ({
|
|
182
225
|
goal: truncate(prompt, 120),
|
|
183
226
|
status: 'planning',
|
|
@@ -253,6 +296,28 @@ const shouldRefreshContext = ({ prompt, ensureSession, summaryResult, continuity
|
|
|
253
296
|
|| ['possible_shift', 'context_mismatch'].includes(continuity?.state)
|
|
254
297
|
);
|
|
255
298
|
|
|
299
|
+
const shouldIncludeSummaryInMinimal = ({ continuity, summaryResult, mutationSafety, autoCreated }) =>
|
|
300
|
+
Boolean(mutationSafety?.blocked)
|
|
301
|
+
|| Boolean(summaryResult?.ambiguous)
|
|
302
|
+
|| Boolean(autoCreated)
|
|
303
|
+
|| ['possible_shift', 'context_mismatch'].includes(continuity?.state);
|
|
304
|
+
|
|
305
|
+
const shouldIncludeRefreshedContextInMinimal = ({ continuity, summaryResult, mutationSafety, refreshedContext }) =>
|
|
306
|
+
Boolean(mutationSafety?.blocked)
|
|
307
|
+
|| Boolean(summaryResult?.ambiguous)
|
|
308
|
+
|| ['possible_shift', 'context_mismatch'].includes(continuity?.state)
|
|
309
|
+
|| Number(refreshedContext?.topFiles?.length ?? 0) > 0;
|
|
310
|
+
|
|
311
|
+
const compactSummaryForMinimal = (summary) => {
|
|
312
|
+
if (!summary) return null;
|
|
313
|
+
return {
|
|
314
|
+
...(summary.status ? { status: summary.status } : {}),
|
|
315
|
+
...(summary.goal ? { goal: summary.goal } : {}),
|
|
316
|
+
...(summary.currentFocus ? { currentFocus: summary.currentFocus } : {}),
|
|
317
|
+
...(summary.nextStep ? { nextStep: summary.nextStep } : {}),
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
|
|
256
321
|
const buildStartRecommendedPath = ({
|
|
257
322
|
prompt,
|
|
258
323
|
ensureSession,
|
|
@@ -444,6 +509,7 @@ const startTurn = async ({
|
|
|
444
509
|
taskId,
|
|
445
510
|
prompt,
|
|
446
511
|
maxTokens = DEFAULT_START_MAX_TOKENS,
|
|
512
|
+
tokenBudget,
|
|
447
513
|
ensureSession = false,
|
|
448
514
|
includeMetrics = false,
|
|
449
515
|
metricsWindow = '7d',
|
|
@@ -451,6 +517,11 @@ const startTurn = async ({
|
|
|
451
517
|
verbosity = DEFAULT_VERBOSITY,
|
|
452
518
|
} = {}) => {
|
|
453
519
|
const startTime = Date.now();
|
|
520
|
+
const normalizedTokenBudget = normalizeTokenBudget(tokenBudget);
|
|
521
|
+
|
|
522
|
+
if (!sessionId && !taskId && isSimpleTask(prompt) && normalizeWhitespace(prompt).length <= SIMPLE_TASK_SKIP_MAX_LENGTH) {
|
|
523
|
+
return buildSimpleTaskStartResult({ prompt, tokenBudget: normalizedTokenBudget, verbosity });
|
|
524
|
+
}
|
|
454
525
|
|
|
455
526
|
if (process.env.DEVCTX_DISABLE_BACKGROUND_TASKS !== 'true') {
|
|
456
527
|
triggerBackgroundIndexBuild({ root: projectRoot }).catch(() => {});
|
|
@@ -602,6 +673,8 @@ const startTurn = async ({
|
|
|
602
673
|
const compactTask = summaryResult.task && minimal
|
|
603
674
|
? { taskId: summaryResult.task.taskId, status: summaryResult.task.status }
|
|
604
675
|
: summaryResult.task ?? null;
|
|
676
|
+
const includeSummary = !minimal || shouldIncludeSummaryInMinimal({ continuity, summaryResult, mutationSafety, autoCreated });
|
|
677
|
+
const includeRefreshedContext = !minimal || shouldIncludeRefreshedContextInMinimal({ continuity, summaryResult, mutationSafety, refreshedContext });
|
|
605
678
|
const includeMessage = !minimal || mutationSafety?.blocked;
|
|
606
679
|
|
|
607
680
|
return attachSafetyMetadata({
|
|
@@ -613,8 +686,8 @@ const startTurn = async ({
|
|
|
613
686
|
...(minimal ? {} : { promptPreview: truncate(prompt, MAX_PROMPT_PREVIEW) }),
|
|
614
687
|
...(previousSessionId ? { previousSessionId } : {}),
|
|
615
688
|
continuity: compactContinuity,
|
|
616
|
-
...(summaryResult.summary ? { summary: summaryResult.summary } : {}),
|
|
617
|
-
...(refreshedContext ? { refreshedContext } : {}),
|
|
689
|
+
...(includeSummary && summaryResult.summary ? { summary: minimal ? compactSummaryForMinimal(summaryResult.summary) : summaryResult.summary } : {}),
|
|
690
|
+
...(includeRefreshedContext && refreshedContext ? { refreshedContext } : {}),
|
|
618
691
|
...(workflow ? { workflow } : {}),
|
|
619
692
|
...(!minimal && summaryResult.candidates ? { candidates: summaryResult.candidates } : {}),
|
|
620
693
|
...(summaryResult.ambiguous && summaryResult.recommendedSessionId ? { recommendedSessionId: summaryResult.recommendedSessionId } : {}),
|
|
@@ -632,6 +705,10 @@ const startTurn = async ({
|
|
|
632
705
|
? 'Created a new persisted session for this task prompt.'
|
|
633
706
|
: continuity.reason,
|
|
634
707
|
} : {}),
|
|
708
|
+
...(normalizedTokenBudget ? {
|
|
709
|
+
taskBudget: normalizedTokenBudget,
|
|
710
|
+
remainingBudget: peekRemainingBudget({ tokenBudget: normalizedTokenBudget }),
|
|
711
|
+
} : {}),
|
|
635
712
|
}, {
|
|
636
713
|
repoSafety: summaryResult.repoSafety ?? metrics?.repoSafety ?? null,
|
|
637
714
|
sideEffectsSuppressed: Boolean(summaryResult.sideEffectsSuppressed ?? metrics?.sideEffectsSuppressed),
|
|
@@ -649,12 +726,14 @@ const endTurn = async ({
|
|
|
649
726
|
update = {},
|
|
650
727
|
force = false,
|
|
651
728
|
maxTokens = DEFAULT_END_MAX_TOKENS,
|
|
729
|
+
tokenBudget,
|
|
652
730
|
includeMetrics = false,
|
|
653
731
|
metricsWindow = '7d',
|
|
654
732
|
latestMetrics = 5,
|
|
655
733
|
verbosity = DEFAULT_VERBOSITY,
|
|
656
734
|
} = {}) => {
|
|
657
735
|
const startTime = Date.now();
|
|
736
|
+
const normalizedTokenBudget = normalizeTokenBudget(tokenBudget);
|
|
658
737
|
const checkpoint = await smartSummary({
|
|
659
738
|
action: 'checkpoint',
|
|
660
739
|
sessionId,
|
|
@@ -741,8 +820,6 @@ const endTurn = async ({
|
|
|
741
820
|
? {
|
|
742
821
|
skipped: Boolean(checkpoint.skipped),
|
|
743
822
|
...(checkpoint.blocked !== undefined ? { blocked: checkpoint.blocked } : {}),
|
|
744
|
-
...(checkpoint.summary ? { summary: checkpoint.summary } : {}),
|
|
745
|
-
...(checkpoint.tokens !== undefined ? { tokens: checkpoint.tokens } : {}),
|
|
746
823
|
...(checkpoint.checkpoint ? {
|
|
747
824
|
checkpoint: {
|
|
748
825
|
event: checkpoint.checkpoint.event,
|
|
@@ -762,6 +839,10 @@ const endTurn = async ({
|
|
|
762
839
|
...(workflow ? { workflow } : {}),
|
|
763
840
|
...(metrics ? { metrics: summarizeMetrics(metrics) } : {}),
|
|
764
841
|
...(isStorageUnhealthy(checkpoint.storageHealth ?? metrics?.storageHealth) ? { storageHealth: checkpoint.storageHealth ?? metrics?.storageHealth } : {}),
|
|
842
|
+
...(normalizedTokenBudget ? {
|
|
843
|
+
taskBudget: normalizedTokenBudget,
|
|
844
|
+
remainingBudget: peekRemainingBudget({ tokenBudget: normalizedTokenBudget }),
|
|
845
|
+
} : {}),
|
|
765
846
|
recommendedPath,
|
|
766
847
|
...(includeMessage ? { message: mutationSafety?.blocked ? mutationSafety.message : checkpoint.message } : {}),
|
|
767
848
|
}, {
|
|
@@ -783,6 +864,7 @@ export const smartTurn = async ({
|
|
|
783
864
|
event,
|
|
784
865
|
force,
|
|
785
866
|
maxTokens,
|
|
867
|
+
tokenBudget,
|
|
786
868
|
ensureSession = false,
|
|
787
869
|
includeMetrics = false,
|
|
788
870
|
metricsWindow = '7d',
|
|
@@ -797,6 +879,7 @@ export const smartTurn = async ({
|
|
|
797
879
|
taskId,
|
|
798
880
|
prompt,
|
|
799
881
|
maxTokens,
|
|
882
|
+
tokenBudget,
|
|
800
883
|
ensureSession,
|
|
801
884
|
includeMetrics,
|
|
802
885
|
metricsWindow,
|
|
@@ -813,6 +896,7 @@ export const smartTurn = async ({
|
|
|
813
896
|
update,
|
|
814
897
|
force,
|
|
815
898
|
maxTokens,
|
|
899
|
+
tokenBudget,
|
|
816
900
|
includeMetrics,
|
|
817
901
|
metricsWindow,
|
|
818
902
|
latestMetrics,
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
export const normalizeTokenBudget = (value) => {
|
|
2
|
+
if (Number.isFinite(value) && value >= 1) {
|
|
3
|
+
return { maxTokens: Math.floor(value), shared: false };
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (!value || typeof value !== 'object') {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const maxTokens = Number.isFinite(value.maxTokens) && value.maxTokens >= 1
|
|
11
|
+
? Math.floor(value.maxTokens)
|
|
12
|
+
: null;
|
|
13
|
+
|
|
14
|
+
if (!maxTokens) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const normalized = {
|
|
19
|
+
maxTokens,
|
|
20
|
+
shared: Boolean(value.shared || value.id),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
if (typeof value.id === 'string' && value.id.trim()) {
|
|
24
|
+
normalized.id = value.id.trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return normalized;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const resolveBudgetMaxTokens = (maxTokens, tokenBudget) => {
|
|
31
|
+
if (Number.isFinite(maxTokens) && maxTokens >= 1) {
|
|
32
|
+
return maxTokens;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return normalizeTokenBudget(tokenBudget)?.maxTokens;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const sharedTaskBudgets = new Map();
|
|
39
|
+
|
|
40
|
+
const getSharedBudgetKey = (tokenBudget, fallbackKey = null) => {
|
|
41
|
+
const normalized = normalizeTokenBudget(tokenBudget);
|
|
42
|
+
if (!normalized?.shared) return null;
|
|
43
|
+
if (normalized.id) return normalized.id;
|
|
44
|
+
if (typeof fallbackKey === 'string' && fallbackKey.trim()) return fallbackKey.trim();
|
|
45
|
+
return null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const ensureSharedBudgetState = (normalizedTokenBudget, fallbackKey = null) => {
|
|
49
|
+
const sharedKey = getSharedBudgetKey(normalizedTokenBudget, fallbackKey);
|
|
50
|
+
if (!sharedKey) {
|
|
51
|
+
return { sharedKey: null, state: null };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const existing = sharedTaskBudgets.get(sharedKey);
|
|
55
|
+
if (!existing || existing.maxTokens !== normalizedTokenBudget.maxTokens) {
|
|
56
|
+
const next = {
|
|
57
|
+
maxTokens: normalizedTokenBudget.maxTokens,
|
|
58
|
+
remainingBudget: normalizedTokenBudget.maxTokens,
|
|
59
|
+
};
|
|
60
|
+
sharedTaskBudgets.set(sharedKey, next);
|
|
61
|
+
return { sharedKey, state: next };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { sharedKey, state: existing };
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const resolveTokenBudgetWindow = ({ maxTokens, tokenBudget, fallbackKey = null } = {}) => {
|
|
68
|
+
const normalized = normalizeTokenBudget(tokenBudget);
|
|
69
|
+
const explicitMaxTokens = Number.isFinite(maxTokens) && maxTokens >= 1 ? Math.floor(maxTokens) : null;
|
|
70
|
+
const baseMaxTokens = explicitMaxTokens ?? normalized?.maxTokens ?? null;
|
|
71
|
+
|
|
72
|
+
if (!normalized) {
|
|
73
|
+
return {
|
|
74
|
+
normalized: null,
|
|
75
|
+
sharedKey: null,
|
|
76
|
+
remainingBudget: null,
|
|
77
|
+
effectiveMaxTokens: baseMaxTokens,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { sharedKey, state } = ensureSharedBudgetState(normalized, fallbackKey);
|
|
82
|
+
const remainingBudget = sharedKey ? state.remainingBudget : normalized.maxTokens;
|
|
83
|
+
const effectiveMaxTokens = baseMaxTokens == null
|
|
84
|
+
? remainingBudget
|
|
85
|
+
: Math.min(baseMaxTokens, remainingBudget);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
normalized,
|
|
89
|
+
sharedKey,
|
|
90
|
+
remainingBudget,
|
|
91
|
+
effectiveMaxTokens,
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const peekRemainingBudget = ({ tokenBudget, fallbackKey = null } = {}) =>
|
|
96
|
+
resolveTokenBudgetWindow({ tokenBudget, fallbackKey }).remainingBudget;
|
|
97
|
+
|
|
98
|
+
export const consumeTokenBudget = ({ tokenBudget, usedTokens = 0, fallbackKey = null } = {}) => {
|
|
99
|
+
const normalized = normalizeTokenBudget(tokenBudget);
|
|
100
|
+
if (!normalized) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const consumed = Math.max(0, Math.floor(usedTokens));
|
|
105
|
+
const { sharedKey, state } = ensureSharedBudgetState(normalized, fallbackKey);
|
|
106
|
+
|
|
107
|
+
if (!sharedKey) {
|
|
108
|
+
return Math.max(0, normalized.maxTokens - consumed);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
state.remainingBudget = Math.max(0, state.remainingBudget - consumed);
|
|
112
|
+
sharedTaskBudgets.set(sharedKey, state);
|
|
113
|
+
return state.remainingBudget;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const clearTaskBudgets = () => sharedTaskBudgets.clear();
|