@j0hanz/cortex-mcp 1.3.0 → 1.5.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.
Files changed (100) hide show
  1. package/dist/engine/config.d.ts +5 -5
  2. package/dist/engine/config.js +14 -5
  3. package/dist/engine/context.d.ts +0 -2
  4. package/dist/engine/context.js +0 -4
  5. package/dist/engine/events.d.ts +1 -2
  6. package/dist/engine/events.js +2 -4
  7. package/dist/engine/heuristics.d.ts +4 -0
  8. package/dist/engine/heuristics.js +65 -0
  9. package/dist/engine/reasoner.d.ts +7 -2
  10. package/dist/engine/reasoner.js +41 -103
  11. package/dist/engine/session-store.d.ts +6 -2
  12. package/dist/engine/session-store.js +78 -27
  13. package/dist/index.d.ts +0 -1
  14. package/dist/index.js +0 -1
  15. package/dist/lib/concurrency.d.ts +5 -0
  16. package/dist/lib/concurrency.js +17 -0
  17. package/dist/lib/errors.d.ts +23 -8
  18. package/dist/lib/errors.js +47 -6
  19. package/dist/lib/formatting.d.ts +13 -1
  20. package/dist/lib/formatting.js +18 -1
  21. package/dist/lib/prompt-contracts.d.ts +6 -0
  22. package/dist/lib/prompt-contracts.js +35 -0
  23. package/dist/lib/text.d.ts +0 -1
  24. package/dist/lib/text.js +0 -1
  25. package/dist/lib/tool-contracts.d.ts +15 -0
  26. package/dist/lib/tool-contracts.js +93 -0
  27. package/dist/lib/tool-response.d.ts +4 -1
  28. package/dist/lib/tool-response.js +3 -1
  29. package/dist/lib/types.d.ts +18 -1
  30. package/dist/lib/types.js +8 -2
  31. package/dist/lib/validators.d.ts +7 -2
  32. package/dist/lib/validators.js +30 -8
  33. package/dist/prompts/index.d.ts +0 -1
  34. package/dist/prompts/index.js +171 -74
  35. package/dist/prompts/templates.d.ts +2 -0
  36. package/dist/prompts/templates.js +106 -0
  37. package/dist/resources/index.d.ts +0 -1
  38. package/dist/resources/index.js +40 -63
  39. package/dist/resources/instructions.d.ts +1 -0
  40. package/dist/resources/instructions.js +103 -0
  41. package/dist/resources/tool-catalog.d.ts +1 -0
  42. package/dist/resources/tool-catalog.js +20 -0
  43. package/dist/resources/tool-info.d.ts +2 -0
  44. package/dist/resources/tool-info.js +35 -0
  45. package/dist/resources/workflows.d.ts +1 -0
  46. package/dist/resources/workflows.js +61 -0
  47. package/dist/schemas/inputs.d.ts +7 -2
  48. package/dist/schemas/inputs.js +39 -4
  49. package/dist/schemas/outputs.d.ts +32 -26
  50. package/dist/schemas/outputs.js +17 -14
  51. package/dist/server.d.ts +0 -1
  52. package/dist/server.js +1 -4
  53. package/dist/tools/index.d.ts +0 -1
  54. package/dist/tools/index.js +0 -1
  55. package/dist/tools/reasoning-think.d.ts +0 -1
  56. package/dist/tools/reasoning-think.js +211 -118
  57. package/package.json +9 -10
  58. package/dist/engine/config.d.ts.map +0 -1
  59. package/dist/engine/config.js.map +0 -1
  60. package/dist/engine/context.d.ts.map +0 -1
  61. package/dist/engine/context.js.map +0 -1
  62. package/dist/engine/events.d.ts.map +0 -1
  63. package/dist/engine/events.js.map +0 -1
  64. package/dist/engine/reasoner.d.ts.map +0 -1
  65. package/dist/engine/reasoner.js.map +0 -1
  66. package/dist/engine/session-store.d.ts.map +0 -1
  67. package/dist/engine/session-store.js.map +0 -1
  68. package/dist/index.d.ts.map +0 -1
  69. package/dist/index.js.map +0 -1
  70. package/dist/instructions.md +0 -148
  71. package/dist/lib/errors.d.ts.map +0 -1
  72. package/dist/lib/errors.js.map +0 -1
  73. package/dist/lib/formatting.d.ts.map +0 -1
  74. package/dist/lib/formatting.js.map +0 -1
  75. package/dist/lib/instructions.d.ts +0 -5
  76. package/dist/lib/instructions.d.ts.map +0 -1
  77. package/dist/lib/instructions.js +0 -30
  78. package/dist/lib/instructions.js.map +0 -1
  79. package/dist/lib/text.d.ts.map +0 -1
  80. package/dist/lib/text.js.map +0 -1
  81. package/dist/lib/tool-response.d.ts.map +0 -1
  82. package/dist/lib/tool-response.js.map +0 -1
  83. package/dist/lib/types.d.ts.map +0 -1
  84. package/dist/lib/types.js.map +0 -1
  85. package/dist/lib/validators.d.ts.map +0 -1
  86. package/dist/lib/validators.js.map +0 -1
  87. package/dist/prompts/index.d.ts.map +0 -1
  88. package/dist/prompts/index.js.map +0 -1
  89. package/dist/resources/index.d.ts.map +0 -1
  90. package/dist/resources/index.js.map +0 -1
  91. package/dist/schemas/inputs.d.ts.map +0 -1
  92. package/dist/schemas/inputs.js.map +0 -1
  93. package/dist/schemas/outputs.d.ts.map +0 -1
  94. package/dist/schemas/outputs.js.map +0 -1
  95. package/dist/server.d.ts.map +0 -1
  96. package/dist/server.js.map +0 -1
  97. package/dist/tools/index.d.ts.map +0 -1
  98. package/dist/tools/index.js.map +0 -1
  99. package/dist/tools/reasoning-think.d.ts.map +0 -1
  100. package/dist/tools/reasoning-think.js.map +0 -1
@@ -1,21 +1,21 @@
1
- import type { LevelConfig, ReasoningLevel } from '../lib/types.js';
1
+ import { type LevelConfig, type ReasoningLevel } from '../lib/types.js';
2
2
  export declare const LEVEL_CONFIGS: {
3
3
  readonly basic: {
4
+ readonly tokenBudget: 2048;
4
5
  readonly minThoughts: 3;
5
6
  readonly maxThoughts: 5;
6
- readonly tokenBudget: 2048;
7
7
  };
8
8
  readonly normal: {
9
+ readonly tokenBudget: 8192;
9
10
  readonly minThoughts: 6;
10
11
  readonly maxThoughts: 10;
11
- readonly tokenBudget: 8192;
12
12
  };
13
13
  readonly high: {
14
+ readonly tokenBudget: 32768;
14
15
  readonly minThoughts: 15;
15
16
  readonly maxThoughts: 25;
16
- readonly tokenBudget: 32768;
17
17
  };
18
18
  };
19
19
  export declare function getLevelConfig(level: ReasoningLevel): LevelConfig;
20
20
  export declare function assertTargetThoughtsInRange(level: ReasoningLevel, targetThoughts: number): void;
21
- //# sourceMappingURL=config.d.ts.map
21
+ export declare function getLevelDescriptionString(): string;
@@ -1,8 +1,10 @@
1
+ import { InvalidThoughtCountError } from '../lib/errors.js';
2
+ import { LEVEL_BOUNDS, } from '../lib/types.js';
1
3
  import { getTargetThoughtsError } from '../lib/validators.js';
2
4
  export const LEVEL_CONFIGS = {
3
- basic: { minThoughts: 3, maxThoughts: 5, tokenBudget: 2048 },
4
- normal: { minThoughts: 6, maxThoughts: 10, tokenBudget: 8192 },
5
- high: { minThoughts: 15, maxThoughts: 25, tokenBudget: 32768 },
5
+ basic: { ...LEVEL_BOUNDS.basic, tokenBudget: 2048 },
6
+ normal: { ...LEVEL_BOUNDS.normal, tokenBudget: 8192 },
7
+ high: { ...LEVEL_BOUNDS.high, tokenBudget: 32768 },
6
8
  };
7
9
  export function getLevelConfig(level) {
8
10
  return LEVEL_CONFIGS[level];
@@ -12,6 +14,13 @@ export function assertTargetThoughtsInRange(level, targetThoughts) {
12
14
  if (!errorMessage) {
13
15
  return;
14
16
  }
15
- throw new Error(errorMessage);
17
+ throw new InvalidThoughtCountError(errorMessage);
18
+ }
19
+ export function getLevelDescriptionString() {
20
+ return Object.entries(LEVEL_CONFIGS)
21
+ .map(([level, config]) => {
22
+ const budgetK = Math.round(config.tokenBudget / 1024);
23
+ return `${level} (${config.minThoughts}–${config.maxThoughts} steps, ${budgetK}K budget)`;
24
+ })
25
+ .join(', ');
16
26
  }
17
- //# sourceMappingURL=config.js.map
@@ -3,5 +3,3 @@ export interface EngineContext {
3
3
  readonly abortSignal?: AbortSignal;
4
4
  }
5
5
  export declare function runWithContext<T>(ctx: EngineContext, fn: () => T): T;
6
- export declare function getContext(): EngineContext | undefined;
7
- //# sourceMappingURL=context.d.ts.map
@@ -3,7 +3,3 @@ const storage = new AsyncLocalStorage();
3
3
  export function runWithContext(ctx, fn) {
4
4
  return storage.run(ctx, fn);
5
5
  }
6
- export function getContext() {
7
- return storage.getStore();
8
- }
9
- //# sourceMappingURL=context.js.map
@@ -1,6 +1,6 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import type { ReasoningLevel } from '../lib/types.js';
3
- interface ThoughtBudgetExhaustedPayload {
3
+ export interface ThoughtBudgetExhaustedPayload {
4
4
  sessionId: string;
5
5
  tokensUsed: number;
6
6
  tokenBudget: number;
@@ -51,4 +51,3 @@ interface TypedEmitter<T> extends Omit<EventEmitter, 'on' | 'off' | 'emit'> {
51
51
  }
52
52
  export declare const engineEvents: TypedEmitter<EngineEvents>;
53
53
  export {};
54
- //# sourceMappingURL=events.d.ts.map
@@ -1,12 +1,10 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import { getErrorMessage } from '../lib/errors.js';
3
- const ENGINE_ERROR_LOG_PREFIX = '[engine]';
4
- const ENGINE_ERROR_PREFIX_WITH_SPACE = `${ENGINE_ERROR_LOG_PREFIX} `;
3
+ const ENGINE_ERROR_PREFIX = '[engine] ';
5
4
  export const engineEvents = new EventEmitter({
6
5
  captureRejections: true,
7
6
  });
8
7
  function logEngineError(err) {
9
- process.stderr.write(`${ENGINE_ERROR_PREFIX_WITH_SPACE}${getErrorMessage(err)}\n`);
8
+ process.stderr.write(`${ENGINE_ERROR_PREFIX}${getErrorMessage(err)}\n`);
10
9
  }
11
10
  engineEvents.on('error', logEngineError);
12
- //# sourceMappingURL=events.js.map
@@ -0,0 +1,4 @@
1
+ import type { LevelConfig, ReasoningLevel } from '../lib/types.js';
2
+ export declare function countSentences(queryText: string): number;
3
+ export declare function getStructureDensityScore(queryText: string): number;
4
+ export declare function resolveThoughtCount(level: ReasoningLevel, query: string, config: Pick<LevelConfig, 'minThoughts' | 'maxThoughts'>, targetThoughts?: number): number;
@@ -0,0 +1,65 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { createSegmenter } from '../lib/text.js';
3
+ import { assertTargetThoughtsInRange } from './config.js';
4
+ const NON_WHITESPACE = /\S/u;
5
+ const COMPLEXITY_KEYWORDS = /\b(compare|analy[sz]e|trade[- ]?off|design|plan|critique|evaluate|review|architecture)\b/i;
6
+ let _sentenceSegmenter;
7
+ let _sentenceSegmenterInitialized = false;
8
+ function getSentenceSegmenter() {
9
+ if (!_sentenceSegmenterInitialized) {
10
+ _sentenceSegmenter = createSegmenter('sentence');
11
+ _sentenceSegmenterInitialized = true;
12
+ }
13
+ return _sentenceSegmenter;
14
+ }
15
+ export function countSentences(queryText) {
16
+ const segmenter = getSentenceSegmenter();
17
+ if (!segmenter) {
18
+ return 0;
19
+ }
20
+ let count = 0;
21
+ for (const sentence of segmenter.segment(queryText)) {
22
+ if (NON_WHITESPACE.test(sentence.segment)) {
23
+ count++;
24
+ }
25
+ }
26
+ return count;
27
+ }
28
+ export function getStructureDensityScore(queryText) {
29
+ const sentenceCount = countSentences(queryText);
30
+ if (sentenceCount > 1) {
31
+ return (sentenceCount - 1) * 0.08;
32
+ }
33
+ let markerMatches = 0;
34
+ for (let index = 0; index < queryText.length; index++) {
35
+ switch (queryText.charCodeAt(index)) {
36
+ case 63: // ?
37
+ case 58: // :
38
+ case 59: // ;
39
+ case 44: // ,
40
+ case 10: // \n
41
+ markerMatches += 1;
42
+ break;
43
+ default:
44
+ break;
45
+ }
46
+ }
47
+ return markerMatches * 0.05;
48
+ }
49
+ export function resolveThoughtCount(level, query, config, targetThoughts) {
50
+ if (targetThoughts !== undefined) {
51
+ assertTargetThoughtsInRange(level, targetThoughts);
52
+ return targetThoughts;
53
+ }
54
+ if (config.minThoughts === config.maxThoughts) {
55
+ return config.minThoughts;
56
+ }
57
+ const queryText = query.trim();
58
+ const span = config.maxThoughts - config.minThoughts;
59
+ const queryByteLength = Buffer.byteLength(queryText, 'utf8');
60
+ const lengthScore = Math.min(1, queryByteLength / 400);
61
+ const structureScore = Math.min(0.4, getStructureDensityScore(queryText));
62
+ const keywordScore = COMPLEXITY_KEYWORDS.test(queryText) ? 0.25 : 0;
63
+ const score = Math.min(1, lengthScore + structureScore + keywordScore);
64
+ return config.minThoughts + Math.round(span * score);
65
+ }
@@ -5,9 +5,14 @@ export { sessionStore };
5
5
  interface ReasonOptions {
6
6
  sessionId?: string;
7
7
  targetThoughts?: number;
8
- thought: string;
8
+ thought?: string;
9
+ observation?: string;
10
+ hypothesis?: string;
11
+ evaluation?: string;
12
+ stepSummary?: string;
13
+ isConclusion?: boolean;
14
+ rollbackToStep?: number;
9
15
  abortSignal?: AbortSignal;
10
16
  onProgress?: (progress: number, total: number) => void | Promise<void>;
11
17
  }
12
18
  export declare function reason(query: string, level: ReasoningLevel | undefined, options?: ReasonOptions): Promise<Readonly<Session>>;
13
- //# sourceMappingURL=reasoner.d.ts.map
@@ -1,40 +1,39 @@
1
- import { Buffer } from 'node:buffer';
2
- import { createSegmenter } from '../lib/text.js';
3
- import { assertTargetThoughtsInRange, getLevelConfig } from './config.js';
1
+ import { InvalidRunModeArgsError, ReasoningAbortedError, SessionNotFoundError, } from '../lib/errors.js';
2
+ import { parsePositiveIntEnv } from '../lib/validators.js';
3
+ import { getLevelConfig } from './config.js';
4
4
  import { runWithContext } from './context.js';
5
5
  import { engineEvents } from './events.js';
6
- import { SessionStore } from './session-store.js';
7
- const DEFAULT_SESSION_TTL_MS = 30 * 60 * 1000;
8
- const DEFAULT_MAX_SESSIONS = 100;
9
- const DEFAULT_MAX_TOTAL_TOKENS = 500_000;
10
- const NON_WHITESPACE = /\S/u;
11
- const COMPLEXITY_KEYWORDS = /\b(compare|analy[sz]e|trade[- ]?off|design|plan|critique|evaluate|review|architecture)\b/i;
12
- function parsePositiveIntEnv(name, fallback, minimum = 1) {
13
- const raw = process.env[name];
14
- if (raw === undefined) {
15
- return fallback;
16
- }
17
- const parsed = Number.parseInt(raw, 10);
18
- if (!Number.isInteger(parsed) || parsed < minimum) {
19
- return fallback;
20
- }
21
- return parsed;
22
- }
6
+ import { resolveThoughtCount } from './heuristics.js';
7
+ import { DEFAULT_MAX_SESSIONS, DEFAULT_MAX_TOTAL_TOKENS, DEFAULT_SESSION_TTL_MS, SessionStore, } from './session-store.js';
23
8
  const sessionStore = new SessionStore(parsePositiveIntEnv('CORTEX_SESSION_TTL_MS', DEFAULT_SESSION_TTL_MS), parsePositiveIntEnv('CORTEX_MAX_SESSIONS', DEFAULT_MAX_SESSIONS), parsePositiveIntEnv('CORTEX_MAX_TOTAL_TOKENS', DEFAULT_MAX_TOTAL_TOKENS));
24
- const sentenceSegmenter = createSegmenter('sentence');
25
9
  const sessionLocks = new Map();
26
10
  export { sessionStore };
27
11
  export async function reason(query, level, options) {
28
- if (!options?.thought) {
29
- throw new Error('thought is required: provide your reasoning content');
12
+ const { sessionId, targetThoughts, thought, observation, hypothesis, evaluation, stepSummary, isConclusion, rollbackToStep, abortSignal, onProgress, } = options ?? {};
13
+ const hasContent = thought !== undefined ||
14
+ (observation !== undefined &&
15
+ hypothesis !== undefined &&
16
+ evaluation !== undefined);
17
+ if (!hasContent && rollbackToStep === undefined) {
18
+ throw new InvalidRunModeArgsError('Either thought (or observation/hypothesis/evaluation) or rollback_to_step is required');
30
19
  }
31
- const { sessionId, targetThoughts, thought, abortSignal, onProgress } = options;
32
20
  const session = resolveSession(level, sessionId, query, targetThoughts);
33
21
  const config = getLevelConfig(session.level);
34
22
  const { totalThoughts } = session;
35
23
  return runWithContext({ sessionId: session.id, ...(abortSignal ? { abortSignal } : {}) }, () => withSessionLock(session.id, async () => {
36
24
  throwIfReasoningAborted(abortSignal);
25
+ if (rollbackToStep !== undefined) {
26
+ sessionStore.rollback(session.id, rollbackToStep);
27
+ }
37
28
  const current = getSessionOrThrow(session.id);
29
+ let content = thought;
30
+ if (!content && observation) {
31
+ content = `**Observation:** ${observation}\n\n**Hypothesis:** ${hypothesis ?? ''}\n\n**Evaluation:** ${evaluation ?? ''}`;
32
+ }
33
+ if (!content) {
34
+ // Only rollback occurred
35
+ return current;
36
+ }
38
37
  if (emitBudgetExhaustedIfNeeded({
39
38
  session: current,
40
39
  tokenBudget: config.tokenBudget,
@@ -44,23 +43,23 @@ export async function reason(query, level, options) {
44
43
  return current;
45
44
  }
46
45
  const nextIndex = current.thoughts.length;
47
- if (nextIndex >= totalThoughts) {
46
+ if (nextIndex >= totalThoughts && !isConclusion) {
48
47
  return current;
49
48
  }
50
- const addedThought = sessionStore.addThought(session.id, thought);
49
+ const addedThought = sessionStore.addThought(session.id, content, stepSummary);
51
50
  engineEvents.emit('thought:added', {
52
51
  sessionId: session.id,
53
52
  index: addedThought.index,
54
53
  content: addedThought.content,
55
54
  });
56
55
  const updated = getSessionOrThrow(session.id);
57
- emitBudgetExhaustedIfNeeded({
56
+ void emitBudgetExhaustedIfNeeded({
58
57
  session: updated,
59
58
  tokenBudget: config.tokenBudget,
60
59
  generatedThoughts: addedThought.index + 1,
61
60
  requestedThoughts: totalThoughts,
62
61
  });
63
- if (updated.thoughts.length >= totalThoughts) {
62
+ if (isConclusion || updated.thoughts.length >= totalThoughts) {
64
63
  sessionStore.markCompleted(session.id);
65
64
  }
66
65
  if (onProgress) {
@@ -83,7 +82,7 @@ async function withSessionLock(sessionId, fn) {
83
82
  return await fn();
84
83
  }
85
84
  finally {
86
- release?.();
85
+ release();
87
86
  if (sessionLocks.get(sessionId) === currentTail) {
88
87
  sessionLocks.delete(sessionId);
89
88
  }
@@ -92,51 +91,41 @@ async function withSessionLock(sessionId, fn) {
92
91
  function getSessionOrThrow(sessionId) {
93
92
  const session = sessionStore.get(sessionId);
94
93
  if (!session) {
95
- throw new Error(`Session not found: ${sessionId}`);
94
+ throw new SessionNotFoundError(sessionId);
96
95
  }
97
96
  return session;
98
97
  }
99
- function emitBudgetExhausted(data) {
100
- engineEvents.emit('thought:budget-exhausted', data);
101
- }
102
- function createBudgetExhaustedPayload(args) {
98
+ function emitBudgetExhaustedIfNeeded(args) {
103
99
  const { session, tokenBudget, generatedThoughts, requestedThoughts } = args;
104
- return {
100
+ if (session.tokensUsed < tokenBudget) {
101
+ return false;
102
+ }
103
+ engineEvents.emit('thought:budget-exhausted', {
105
104
  sessionId: session.id,
106
105
  tokensUsed: session.tokensUsed,
107
106
  tokenBudget,
108
107
  generatedThoughts,
109
108
  requestedThoughts,
110
- };
111
- }
112
- function emitBudgetExhaustedIfNeeded(args) {
113
- const { session, tokenBudget } = args;
114
- if (session.tokensUsed < tokenBudget) {
115
- return false;
116
- }
117
- emitBudgetExhausted(createBudgetExhaustedPayload(args));
109
+ });
118
110
  return true;
119
111
  }
120
- function assertExistingSessionConstraints(existing, level, targetThoughts) {
121
- if (level !== undefined && existing.level !== level) {
122
- // Warning: ignoring provided level in favor of session level
123
- }
112
+ function assertExistingSessionConstraints(existing, targetThoughts) {
124
113
  if (targetThoughts !== undefined &&
125
114
  targetThoughts !== existing.totalThoughts) {
126
- throw new Error(`Cannot change targetThoughts on an existing session (current: ${String(existing.totalThoughts)}). Omit targetThoughts or pass ${String(existing.totalThoughts)}.`);
115
+ throw new InvalidRunModeArgsError(`Cannot change targetThoughts on an existing session (current: ${String(existing.totalThoughts)}). Omit targetThoughts or pass ${String(existing.totalThoughts)}.`);
127
116
  }
128
117
  }
129
118
  function resolveSession(level, sessionId, query, targetThoughts) {
130
119
  if (sessionId) {
131
120
  const existing = sessionStore.get(sessionId);
132
121
  if (!existing) {
133
- throw new Error(`Session not found: ${sessionId}`);
122
+ throw new SessionNotFoundError(sessionId);
134
123
  }
135
- assertExistingSessionConstraints(existing, level, targetThoughts);
124
+ assertExistingSessionConstraints(existing, targetThoughts);
136
125
  return existing;
137
126
  }
138
127
  if (level === undefined) {
139
- throw new Error('level is required for new sessions');
128
+ throw new InvalidRunModeArgsError('level is required for new sessions');
140
129
  }
141
130
  const config = getLevelConfig(level);
142
131
  const totalThoughts = resolveThoughtCount(level, query, config, targetThoughts);
@@ -147,56 +136,6 @@ function resolveSession(level, sessionId, query, targetThoughts) {
147
136
  });
148
137
  return session;
149
138
  }
150
- function resolveThoughtCount(level, query, config, targetThoughts) {
151
- if (targetThoughts !== undefined) {
152
- assertTargetThoughtsInRange(level, targetThoughts);
153
- return targetThoughts;
154
- }
155
- if (config.minThoughts === config.maxThoughts) {
156
- return config.minThoughts;
157
- }
158
- const queryText = query.trim();
159
- const span = config.maxThoughts - config.minThoughts;
160
- const queryByteLength = Buffer.byteLength(queryText, 'utf8');
161
- const lengthScore = Math.min(1, queryByteLength / 400);
162
- const structureScore = Math.min(0.4, getStructureDensityScore(queryText));
163
- const keywordScore = COMPLEXITY_KEYWORDS.test(queryText) ? 0.25 : 0;
164
- const score = Math.min(1, lengthScore + structureScore + keywordScore);
165
- return config.minThoughts + Math.round(span * score);
166
- }
167
- function countSentences(queryText) {
168
- if (!sentenceSegmenter) {
169
- return 0;
170
- }
171
- let count = 0;
172
- for (const sentence of sentenceSegmenter.segment(queryText)) {
173
- if (NON_WHITESPACE.test(sentence.segment)) {
174
- count++;
175
- }
176
- }
177
- return count;
178
- }
179
- function getStructureDensityScore(queryText) {
180
- const sentenceCount = countSentences(queryText);
181
- if (sentenceCount > 1) {
182
- return (sentenceCount - 1) * 0.08;
183
- }
184
- let markerMatches = 0;
185
- for (let index = 0; index < queryText.length; index++) {
186
- switch (queryText.charCodeAt(index)) {
187
- case 63: // ?
188
- case 58: // :
189
- case 59: // ;
190
- case 44: // ,
191
- case 10: // \n
192
- markerMatches += 1;
193
- break;
194
- default:
195
- break;
196
- }
197
- }
198
- return markerMatches * 0.05;
199
- }
200
139
  function throwIfReasoningAborted(signal) {
201
140
  if (!signal) {
202
141
  return;
@@ -205,7 +144,6 @@ function throwIfReasoningAborted(signal) {
205
144
  signal.throwIfAborted();
206
145
  }
207
146
  catch {
208
- throw new Error('Reasoning aborted');
147
+ throw new ReasoningAbortedError();
209
148
  }
210
149
  }
211
- //# sourceMappingURL=reasoner.js.map
@@ -1,4 +1,7 @@
1
1
  import type { ReasoningLevel, Session, SessionSummary, Thought } from '../lib/types.js';
2
+ export declare const DEFAULT_SESSION_TTL_MS: number;
3
+ export declare const DEFAULT_MAX_SESSIONS = 100;
4
+ export declare const DEFAULT_MAX_TOTAL_TOKENS = 500000;
2
5
  export declare class SessionStore {
3
6
  private readonly sessions;
4
7
  private readonly sessionOrder;
@@ -21,7 +24,8 @@ export declare class SessionStore {
21
24
  getExpiresAt(sessionId: string): number | undefined;
22
25
  getTotalTokensUsed(): number;
23
26
  delete(id: string): boolean;
24
- addThought(sessionId: string, content: string): Thought;
27
+ addThought(sessionId: string, content: string, stepSummary?: string): Thought;
28
+ rollback(sessionId: string, toIndex: number): void;
25
29
  reviseThought(sessionId: string, thoughtIndex: number, content: string): Thought;
26
30
  markCompleted(sessionId: string): void;
27
31
  markCancelled(sessionId: string): void;
@@ -39,6 +43,7 @@ export declare class SessionStore {
39
43
  private deleteSessionInternal;
40
44
  private markSessionTouched;
41
45
  private getSessionIdsForIteration;
46
+ private collectSessions;
42
47
  private snapshotThought;
43
48
  private snapshotSession;
44
49
  private snapshotSessionSummary;
@@ -48,4 +53,3 @@ export declare class SessionStore {
48
53
  private emitSessionEvicted;
49
54
  private emitSessionResourcesUpdated;
50
55
  }
51
- //# sourceMappingURL=session-store.d.ts.map
@@ -2,9 +2,9 @@ import { Buffer } from 'node:buffer';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { getLevelConfig } from './config.js';
4
4
  import { engineEvents } from './events.js';
5
- const DEFAULT_TTL_MS = 30 * 60 * 1000; // 30 minutes
6
- const DEFAULT_MAX_SESSIONS = 100;
7
- const DEFAULT_MAX_TOTAL_TOKENS = 500_000;
5
+ export const DEFAULT_SESSION_TTL_MS = 30 * 60 * 1000; // 30 minutes
6
+ export const DEFAULT_MAX_SESSIONS = 100;
7
+ export const DEFAULT_MAX_TOTAL_TOKENS = 500_000;
8
8
  const TOKEN_ESTIMATE_DIVISOR = 4;
9
9
  const MIN_SWEEP_INTERVAL_MS = 10;
10
10
  const MAX_SWEEP_INTERVAL_MS = 60_000;
@@ -12,6 +12,9 @@ function estimateTokens(text) {
12
12
  const byteLength = Buffer.byteLength(text, 'utf8');
13
13
  return Math.max(1, Math.ceil(byteLength / TOKEN_ESTIMATE_DIVISOR));
14
14
  }
15
+ function getThoughtTokenCount(thought) {
16
+ return thought.tokenCount ?? estimateTokens(thought.content);
17
+ }
15
18
  function resolveSweepInterval(ttlMs) {
16
19
  return Math.max(MIN_SWEEP_INTERVAL_MS, Math.min(MAX_SWEEP_INTERVAL_MS, ttlMs));
17
20
  }
@@ -26,7 +29,7 @@ export class SessionStore {
26
29
  maxSessions;
27
30
  maxTotalTokens;
28
31
  totalTokens = 0;
29
- constructor(ttlMs = DEFAULT_TTL_MS, maxSessions = DEFAULT_MAX_SESSIONS, maxTotalTokens = DEFAULT_MAX_TOTAL_TOKENS) {
32
+ constructor(ttlMs = DEFAULT_SESSION_TTL_MS, maxSessions = DEFAULT_MAX_SESSIONS, maxTotalTokens = DEFAULT_MAX_TOTAL_TOKENS) {
30
33
  this.ttlMs = ttlMs;
31
34
  this.maxSessions = maxSessions;
32
35
  this.maxTotalTokens = maxTotalTokens;
@@ -67,28 +70,14 @@ export class SessionStore {
67
70
  return session ? this.snapshotSessionSummary(session) : undefined;
68
71
  }
69
72
  list() {
70
- const sessions = [];
71
- for (const sessionId of this.getSessionIdsForIteration()) {
72
- const session = this.sessions.get(sessionId);
73
- if (session) {
74
- sessions.push(this.snapshotSession(session));
75
- }
76
- }
77
- return sessions;
73
+ return this.collectSessions(this.snapshotSession.bind(this));
78
74
  }
79
75
  listSessionIds() {
80
76
  this.sortedSessionIdsCache ??= this.buildSortedSessionIdsCache();
81
77
  return [...this.sortedSessionIdsCache];
82
78
  }
83
79
  listSummaries() {
84
- const summaries = [];
85
- for (const sessionId of this.getSessionIdsForIteration()) {
86
- const session = this.sessions.get(sessionId);
87
- if (session) {
88
- summaries.push(this.snapshotSessionSummary(session));
89
- }
90
- }
91
- return summaries;
80
+ return this.collectSessions(this.snapshotSessionSummary.bind(this));
92
81
  }
93
82
  getTtlMs() {
94
83
  return this.ttlMs;
@@ -112,7 +101,7 @@ export class SessionStore {
112
101
  this.emitSessionsCollectionUpdated();
113
102
  return true;
114
103
  }
115
- addThought(sessionId, content) {
104
+ addThought(sessionId, content, stepSummary) {
116
105
  const session = this.sessions.get(sessionId);
117
106
  if (!session) {
118
107
  throw new Error(`Session not found: ${sessionId}`);
@@ -123,6 +112,8 @@ export class SessionStore {
123
112
  index: session.thoughts.length,
124
113
  content,
125
114
  revision: 0,
115
+ tokenCount: tokens,
116
+ ...(stepSummary !== undefined ? { stepSummary } : {}),
126
117
  };
127
118
  session.thoughts.push(thought);
128
119
  session.tokensUsed += tokens;
@@ -130,6 +121,26 @@ export class SessionStore {
130
121
  this.markSessionTouched(session);
131
122
  return this.snapshotThought(thought);
132
123
  }
124
+ rollback(sessionId, toIndex) {
125
+ const session = this.sessions.get(sessionId);
126
+ if (!session) {
127
+ throw new Error(`Session not found: ${sessionId}`);
128
+ }
129
+ // If toIndex is out of bounds or implies no change, return.
130
+ // We keep thoughts up to and including toIndex.
131
+ if (toIndex < 0 || toIndex >= session.thoughts.length - 1) {
132
+ return;
133
+ }
134
+ const removedThoughts = session.thoughts.slice(toIndex + 1);
135
+ session.thoughts = session.thoughts.slice(0, toIndex + 1);
136
+ let removedTokens = 0;
137
+ for (const t of removedThoughts) {
138
+ removedTokens += getThoughtTokenCount(t);
139
+ }
140
+ session.tokensUsed -= removedTokens;
141
+ this.totalTokens -= removedTokens;
142
+ this.markSessionTouched(session);
143
+ }
133
144
  reviseThought(sessionId, thoughtIndex, content) {
134
145
  const session = this.sessions.get(sessionId);
135
146
  if (!session) {
@@ -139,7 +150,7 @@ export class SessionStore {
139
150
  if (!existing) {
140
151
  throw new Error(`Thought index ${String(thoughtIndex)} not found in session ${sessionId}`);
141
152
  }
142
- const oldTokens = estimateTokens(existing.content);
153
+ const oldTokens = getThoughtTokenCount(existing);
143
154
  const newTokens = estimateTokens(content);
144
155
  const delta = newTokens - oldTokens;
145
156
  if (delta > 0) {
@@ -149,12 +160,23 @@ export class SessionStore {
149
160
  index: thoughtIndex,
150
161
  content,
151
162
  revision: existing.revision + 1,
163
+ tokenCount: newTokens,
164
+ ...(existing.stepSummary !== undefined
165
+ ? { stepSummary: existing.stepSummary }
166
+ : {}),
152
167
  };
153
168
  session.thoughts[thoughtIndex] = revised;
154
169
  session.tokensUsed = session.tokensUsed - oldTokens + newTokens;
155
170
  this.totalTokens += delta;
156
171
  this.markSessionTouched(session);
157
- return this.snapshotThought(revised);
172
+ const snapshotted = this.snapshotThought(revised);
173
+ engineEvents.emit('thought:revised', {
174
+ sessionId,
175
+ index: snapshotted.index,
176
+ content: snapshotted.content,
177
+ revision: snapshotted.revision,
178
+ });
179
+ return snapshotted;
158
180
  }
159
181
  markCompleted(sessionId) {
160
182
  this.updateSessionStatus(sessionId, 'completed');
@@ -323,6 +345,8 @@ export class SessionStore {
323
345
  }
324
346
  markSessionTouched(session) {
325
347
  session.updatedAt = Date.now();
348
+ session._cachedSnapshot = undefined;
349
+ session._cachedSummary = undefined;
326
350
  this.touchOrder(session.id);
327
351
  this.sortedSessionIdsCache = null;
328
352
  this.emitSessionResourcesUpdated(session.id);
@@ -331,16 +355,34 @@ export class SessionStore {
331
355
  this.sortedSessionIdsCache ??= this.buildSortedSessionIdsCache();
332
356
  return this.sortedSessionIdsCache;
333
357
  }
358
+ collectSessions(mapSession) {
359
+ const collected = [];
360
+ for (const sessionId of this.getSessionIdsForIteration()) {
361
+ const session = this.sessions.get(sessionId);
362
+ if (session) {
363
+ collected.push(mapSession(session));
364
+ }
365
+ }
366
+ return collected;
367
+ }
334
368
  snapshotThought(thought) {
335
- return {
369
+ const t = {
336
370
  index: thought.index,
337
371
  content: thought.content,
338
372
  revision: thought.revision,
373
+ ...(thought.stepSummary !== undefined
374
+ ? { stepSummary: thought.stepSummary }
375
+ : {}),
339
376
  };
377
+ Object.freeze(t);
378
+ return t;
340
379
  }
341
380
  snapshotSession(session) {
381
+ if (session._cachedSnapshot) {
382
+ return session._cachedSnapshot;
383
+ }
342
384
  const thoughts = session.thoughts.map((thought) => this.snapshotThought(thought));
343
- return {
385
+ const snapshot = {
344
386
  id: session.id,
345
387
  level: session.level,
346
388
  status: session.status,
@@ -351,9 +393,16 @@ export class SessionStore {
351
393
  createdAt: session.createdAt,
352
394
  updatedAt: session.updatedAt,
353
395
  };
396
+ Object.freeze(snapshot);
397
+ Object.freeze(snapshot.thoughts);
398
+ session._cachedSnapshot = snapshot;
399
+ return snapshot;
354
400
  }
355
401
  snapshotSessionSummary(session) {
356
- return {
402
+ if (session._cachedSummary) {
403
+ return session._cachedSummary;
404
+ }
405
+ const summary = {
357
406
  id: session.id,
358
407
  level: session.level,
359
408
  status: session.status,
@@ -364,6 +413,9 @@ export class SessionStore {
364
413
  createdAt: session.createdAt,
365
414
  updatedAt: session.updatedAt,
366
415
  };
416
+ Object.freeze(summary);
417
+ session._cachedSummary = summary;
418
+ return summary;
367
419
  }
368
420
  emitSessionsListChanged() {
369
421
  engineEvents.emit('resources:changed', { uri: 'reasoning://sessions' });
@@ -389,4 +441,3 @@ export class SessionStore {
389
441
  this.emitSessionsResourceUpdated();
390
442
  }
391
443
  }
392
- //# sourceMappingURL=session-store.js.map
package/dist/index.d.ts CHANGED
@@ -1,3 +1,2 @@
1
1
  #!/usr/bin/env node
2
2
  export {};
3
- //# sourceMappingURL=index.d.ts.map