@j0hanz/cortex-mcp 1.5.0 → 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,5 +1,4 @@
1
1
  export interface EngineContext {
2
2
  readonly sessionId: string;
3
- readonly abortSignal?: AbortSignal;
4
3
  }
5
4
  export declare function runWithContext<T>(ctx: EngineContext, fn: () => T): T;
@@ -26,6 +26,12 @@ interface EngineEvents {
26
26
  sessionId: string;
27
27
  level: ReasoningLevel;
28
28
  }];
29
+ 'session:completed': [{
30
+ sessionId: string;
31
+ }];
32
+ 'session:cancelled': [{
33
+ sessionId: string;
34
+ }];
29
35
  'session:expired': [{
30
36
  sessionId: string;
31
37
  }];
@@ -13,6 +13,6 @@ interface ReasonOptions {
13
13
  isConclusion?: boolean;
14
14
  rollbackToStep?: number;
15
15
  abortSignal?: AbortSignal;
16
- onProgress?: (progress: number, total: number) => void | Promise<void>;
16
+ onProgress?: (progress: number, total: number, stepSummary?: string) => void | Promise<void>;
17
17
  }
18
18
  export declare function reason(query: string, level: ReasoningLevel | undefined, options?: ReasonOptions): Promise<Readonly<Session>>;
@@ -20,7 +20,7 @@ export async function reason(query, level, options) {
20
20
  const session = resolveSession(level, sessionId, query, targetThoughts);
21
21
  const config = getLevelConfig(session.level);
22
22
  const { totalThoughts } = session;
23
- return runWithContext({ sessionId: session.id, ...(abortSignal ? { abortSignal } : {}) }, () => withSessionLock(session.id, async () => {
23
+ return runWithContext({ sessionId: session.id }, () => withSessionLock(session.id, async () => {
24
24
  throwIfReasoningAborted(abortSignal);
25
25
  if (rollbackToStep !== undefined) {
26
26
  sessionStore.rollback(session.id, rollbackToStep);
@@ -53,7 +53,7 @@ export async function reason(query, level, options) {
53
53
  content: addedThought.content,
54
54
  });
55
55
  const updated = getSessionOrThrow(session.id);
56
- void emitBudgetExhaustedIfNeeded({
56
+ emitBudgetExhaustedIfNeeded({
57
57
  session: updated,
58
58
  tokenBudget: config.tokenBudget,
59
59
  generatedThoughts: addedThought.index + 1,
@@ -63,7 +63,7 @@ export async function reason(query, level, options) {
63
63
  sessionStore.markCompleted(session.id);
64
64
  }
65
65
  if (onProgress) {
66
- await onProgress(addedThought.index + 1, totalThoughts);
66
+ await onProgress(addedThought.index + 1, totalThoughts, stepSummary);
67
67
  throwIfReasoningAborted(abortSignal);
68
68
  }
69
69
  return getSessionOrThrow(session.id);
@@ -129,7 +129,7 @@ function resolveSession(level, sessionId, query, targetThoughts) {
129
129
  }
130
130
  const config = getLevelConfig(level);
131
131
  const totalThoughts = resolveThoughtCount(level, query, config, targetThoughts);
132
- const session = sessionStore.create(level, totalThoughts);
132
+ const session = sessionStore.create(level, totalThoughts, query);
133
133
  engineEvents.emit('session:created', {
134
134
  sessionId: session.id,
135
135
  level,
@@ -8,13 +8,15 @@ export declare class SessionStore {
8
8
  private oldestSessionId;
9
9
  private newestSessionId;
10
10
  private sortedSessionIdsCache;
11
- private readonly cleanupInterval;
11
+ private cleanupInterval;
12
12
  private readonly ttlMs;
13
13
  private readonly maxSessions;
14
14
  private readonly maxTotalTokens;
15
15
  private totalTokens;
16
16
  constructor(ttlMs?: number, maxSessions?: number, maxTotalTokens?: number);
17
- create(level: ReasoningLevel, totalThoughts?: number): Readonly<Session>;
17
+ ensureCleanupTimer(): void;
18
+ dispose(): void;
19
+ create(level: ReasoningLevel, totalThoughts?: number, query?: string): Readonly<Session>;
18
20
  get(id: string): Readonly<Session> | undefined;
19
21
  getSummary(id: string): Readonly<SessionSummary> | undefined;
20
22
  list(): Readonly<Session>[];
@@ -26,7 +28,7 @@ export declare class SessionStore {
26
28
  delete(id: string): boolean;
27
29
  addThought(sessionId: string, content: string, stepSummary?: string): Thought;
28
30
  rollback(sessionId: string, toIndex: number): void;
29
- reviseThought(sessionId: string, thoughtIndex: number, content: string): Thought;
31
+ reviseThought(sessionId: string, thoughtIndex: number, content: string, stepSummary?: string): Thought;
30
32
  markCompleted(sessionId: string): void;
31
33
  markCancelled(sessionId: string): void;
32
34
  private updateSessionStatus;
@@ -47,9 +49,9 @@ export declare class SessionStore {
47
49
  private snapshotThought;
48
50
  private snapshotSession;
49
51
  private snapshotSessionSummary;
50
- private emitSessionsListChanged;
51
- private emitSessionsResourceUpdated;
52
- private emitSessionsCollectionUpdated;
52
+ private emitListChanged;
53
+ private emitListResourceUpdated;
54
+ private emitCollectionUpdated;
53
55
  private emitSessionEvicted;
54
- private emitSessionResourcesUpdated;
56
+ private emitSessionResourceUpdated;
55
57
  }
@@ -1,11 +1,12 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { randomUUID } from 'node:crypto';
3
+ import { SessionNotFoundError } from '../lib/errors.js';
3
4
  import { getLevelConfig } from './config.js';
4
5
  import { engineEvents } from './events.js';
5
6
  export const DEFAULT_SESSION_TTL_MS = 30 * 60 * 1000; // 30 minutes
6
7
  export const DEFAULT_MAX_SESSIONS = 100;
7
8
  export const DEFAULT_MAX_TOTAL_TOKENS = 500_000;
8
- const TOKEN_ESTIMATE_DIVISOR = 4;
9
+ const TOKEN_ESTIMATE_DIVISOR = 3.5;
9
10
  const MIN_SWEEP_INTERVAL_MS = 10;
10
11
  const MAX_SWEEP_INTERVAL_MS = 60_000;
11
12
  function estimateTokens(text) {
@@ -33,13 +34,26 @@ export class SessionStore {
33
34
  this.ttlMs = ttlMs;
34
35
  this.maxSessions = maxSessions;
35
36
  this.maxTotalTokens = maxTotalTokens;
36
- const sweepInterval = resolveSweepInterval(ttlMs);
37
+ this.ensureCleanupTimer();
38
+ }
39
+ ensureCleanupTimer() {
40
+ if (this.cleanupInterval) {
41
+ return;
42
+ }
43
+ const sweepInterval = resolveSweepInterval(this.ttlMs);
37
44
  this.cleanupInterval = setInterval(() => {
38
45
  this.sweep();
39
46
  }, sweepInterval);
40
47
  this.cleanupInterval.unref();
41
48
  }
42
- create(level, totalThoughts) {
49
+ dispose() {
50
+ if (!this.cleanupInterval) {
51
+ return;
52
+ }
53
+ clearInterval(this.cleanupInterval);
54
+ this.cleanupInterval = undefined;
55
+ }
56
+ create(level, totalThoughts, query) {
43
57
  this.evictIfAtCapacity();
44
58
  const config = getLevelConfig(level);
45
59
  const now = Date.now();
@@ -53,12 +67,13 @@ export class SessionStore {
53
67
  tokensUsed: 0,
54
68
  createdAt: now,
55
69
  updatedAt: now,
70
+ ...(query !== undefined ? { query } : {}),
56
71
  };
57
72
  this.sessions.set(session.id, session);
58
73
  this.addToOrder(session.id);
59
74
  this.sortedSessionIdsCache = null;
60
- this.emitSessionsListChanged();
61
- this.emitSessionsResourceUpdated();
75
+ this.emitListChanged();
76
+ this.emitListResourceUpdated();
62
77
  return this.snapshotSession(session);
63
78
  }
64
79
  get(id) {
@@ -98,13 +113,13 @@ export class SessionStore {
98
113
  return false;
99
114
  }
100
115
  engineEvents.emit('session:deleted', { sessionId: id });
101
- this.emitSessionsCollectionUpdated();
116
+ this.emitCollectionUpdated();
102
117
  return true;
103
118
  }
104
119
  addThought(sessionId, content, stepSummary) {
105
120
  const session = this.sessions.get(sessionId);
106
121
  if (!session) {
107
- throw new Error(`Session not found: ${sessionId}`);
122
+ throw new SessionNotFoundError(sessionId);
108
123
  }
109
124
  const tokens = estimateTokens(content);
110
125
  this.evictForTokenHeadroom(tokens, sessionId);
@@ -124,7 +139,7 @@ export class SessionStore {
124
139
  rollback(sessionId, toIndex) {
125
140
  const session = this.sessions.get(sessionId);
126
141
  if (!session) {
127
- throw new Error(`Session not found: ${sessionId}`);
142
+ throw new SessionNotFoundError(sessionId);
128
143
  }
129
144
  // If toIndex is out of bounds or implies no change, return.
130
145
  // We keep thoughts up to and including toIndex.
@@ -141,10 +156,10 @@ export class SessionStore {
141
156
  this.totalTokens -= removedTokens;
142
157
  this.markSessionTouched(session);
143
158
  }
144
- reviseThought(sessionId, thoughtIndex, content) {
159
+ reviseThought(sessionId, thoughtIndex, content, stepSummary) {
145
160
  const session = this.sessions.get(sessionId);
146
161
  if (!session) {
147
- throw new Error(`Session not found: ${sessionId}`);
162
+ throw new SessionNotFoundError(sessionId);
148
163
  }
149
164
  const existing = session.thoughts[thoughtIndex];
150
165
  if (!existing) {
@@ -156,13 +171,14 @@ export class SessionStore {
156
171
  if (delta > 0) {
157
172
  this.evictForTokenHeadroom(delta, sessionId);
158
173
  }
174
+ const effectiveStepSummary = stepSummary ?? existing.stepSummary;
159
175
  const revised = {
160
176
  index: thoughtIndex,
161
177
  content,
162
178
  revision: existing.revision + 1,
163
179
  tokenCount: newTokens,
164
- ...(existing.stepSummary !== undefined
165
- ? { stepSummary: existing.stepSummary }
180
+ ...(effectiveStepSummary !== undefined
181
+ ? { stepSummary: effectiveStepSummary }
166
182
  : {}),
167
183
  };
168
184
  session.thoughts[thoughtIndex] = revised;
@@ -189,6 +205,12 @@ export class SessionStore {
189
205
  if (session?.status === 'active') {
190
206
  session.status = status;
191
207
  this.markSessionTouched(session);
208
+ if (status === 'completed') {
209
+ engineEvents.emit('session:completed', { sessionId });
210
+ }
211
+ else {
212
+ engineEvents.emit('session:cancelled', { sessionId });
213
+ }
192
214
  }
193
215
  }
194
216
  evictIfAtCapacity() {
@@ -241,7 +263,7 @@ export class SessionStore {
241
263
  currentId = nextId;
242
264
  }
243
265
  if (changed) {
244
- this.emitSessionsCollectionUpdated();
266
+ this.emitCollectionUpdated();
245
267
  }
246
268
  }
247
269
  buildSortedSessionIdsCache() {
@@ -349,7 +371,7 @@ export class SessionStore {
349
371
  session._cachedSummary = undefined;
350
372
  this.touchOrder(session.id);
351
373
  this.sortedSessionIdsCache = null;
352
- this.emitSessionResourcesUpdated(session.id);
374
+ this.emitSessionResourceUpdated(session.id);
353
375
  }
354
376
  getSessionIdsForIteration() {
355
377
  this.sortedSessionIdsCache ??= this.buildSortedSessionIdsCache();
@@ -392,6 +414,7 @@ export class SessionStore {
392
414
  tokensUsed: session.tokensUsed,
393
415
  createdAt: session.createdAt,
394
416
  updatedAt: session.updatedAt,
417
+ ...(session.query !== undefined ? { query: session.query } : {}),
395
418
  };
396
419
  Object.freeze(snapshot);
397
420
  Object.freeze(snapshot.thoughts);
@@ -417,27 +440,27 @@ export class SessionStore {
417
440
  session._cachedSummary = summary;
418
441
  return summary;
419
442
  }
420
- emitSessionsListChanged() {
443
+ emitListChanged() {
421
444
  engineEvents.emit('resources:changed', { uri: 'reasoning://sessions' });
422
445
  }
423
- emitSessionsResourceUpdated() {
446
+ emitListResourceUpdated() {
424
447
  engineEvents.emit('resource:updated', { uri: 'reasoning://sessions' });
425
448
  }
426
- emitSessionsCollectionUpdated() {
427
- this.emitSessionsListChanged();
428
- this.emitSessionsResourceUpdated();
449
+ emitCollectionUpdated() {
450
+ this.emitListChanged();
451
+ this.emitListResourceUpdated();
429
452
  }
430
453
  emitSessionEvicted(sessionId, reason) {
431
454
  engineEvents.emit('session:evicted', {
432
455
  sessionId,
433
456
  reason,
434
457
  });
435
- this.emitSessionsCollectionUpdated();
458
+ this.emitCollectionUpdated();
436
459
  }
437
- emitSessionResourcesUpdated(sessionId) {
460
+ emitSessionResourceUpdated(sessionId) {
438
461
  engineEvents.emit('resource:updated', {
439
462
  uri: `reasoning://sessions/${sessionId}`,
440
463
  });
441
- this.emitSessionsResourceUpdated();
464
+ this.emitListResourceUpdated();
442
465
  }
443
466
  }
@@ -30,12 +30,6 @@ const TOOL_CONTRACTS = [
30
30
  required: false,
31
31
  constraints: '1-100,000 chars',
32
32
  },
33
- {
34
- name: 'thoughts',
35
- type: 'string[]',
36
- required: false,
37
- constraints: 'optional',
38
- },
39
33
  {
40
34
  name: 'targetThoughts',
41
35
  type: 'number',
@@ -85,7 +79,7 @@ const TOOL_CONTRACTS = [
85
79
  constraints: 'optional',
86
80
  },
87
81
  ],
88
- outputShape: '{ok, result: {sessionId, level, status, thoughts[], generatedThoughts, requestedThoughts, totalThoughts, remainingThoughts, tokenBudget, tokensUsed, ttlMs, expiresAt, createdAt, updatedAt, summary}}',
82
+ outputShape: '{ok, result: {sessionId, query?, level, status, thoughts[], generatedThoughts, requestedThoughts, totalThoughts, remainingThoughts, tokenBudget, tokensUsed, ttlMs, expiresAt, createdAt, updatedAt, summary}}',
89
83
  },
90
84
  ];
91
85
  export function getToolContracts() {
@@ -40,6 +40,7 @@ export interface Thought {
40
40
  }
41
41
  export interface Session extends SessionBase {
42
42
  readonly thoughts: readonly Thought[];
43
+ readonly query?: string;
43
44
  }
44
45
  export interface SessionSummary extends SessionBase {
45
46
  readonly generatedThoughts: number;
@@ -9,4 +9,9 @@ export declare function getTargetThoughtsError(level: ReasoningLevel, targetThou
9
9
  * if the variable is absent or invalid. Values below `minimum` also fall back.
10
10
  */
11
11
  export declare function parsePositiveIntEnv(name: string, fallback: number, minimum?: number): number;
12
+ /**
13
+ * Parse a boolean from an environment variable, returning `fallback` when absent
14
+ * or when the value is not a recognized boolean literal.
15
+ */
16
+ export declare function parseBooleanEnv(name: string, fallback: boolean): boolean;
12
17
  export declare function collectPrefixMatches(candidates: readonly string[], value: string, limit: number): string[];
@@ -30,6 +30,24 @@ export function parsePositiveIntEnv(name, fallback, minimum = 1) {
30
30
  }
31
31
  return parsed;
32
32
  }
33
+ /**
34
+ * Parse a boolean from an environment variable, returning `fallback` when absent
35
+ * or when the value is not a recognized boolean literal.
36
+ */
37
+ export function parseBooleanEnv(name, fallback) {
38
+ const raw = process.env[name];
39
+ if (raw === undefined) {
40
+ return fallback;
41
+ }
42
+ const normalized = raw.trim().toLowerCase();
43
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) {
44
+ return true;
45
+ }
46
+ if (['0', 'false', 'no', 'off'].includes(normalized)) {
47
+ return false;
48
+ }
49
+ return fallback;
50
+ }
33
51
  export function collectPrefixMatches(candidates, value, limit) {
34
52
  const results = [];
35
53
  for (const candidate of candidates) {
@@ -40,17 +40,21 @@ function createTextPrompt(text) {
40
40
  function buildPromptText(args) {
41
41
  const { context, task, constraints, output } = args;
42
42
  return [
43
- '# Context',
43
+ '<context>',
44
44
  ...context,
45
+ '</context>',
45
46
  '',
46
- '# Task',
47
+ '<task>',
47
48
  ...task,
49
+ '</task>',
48
50
  '',
49
- '# Constraints',
51
+ '<constraints>',
50
52
  ...constraints.map((line) => `- ${line}`),
53
+ '</constraints>',
51
54
  '',
52
- '# Output',
55
+ '<output_format>',
53
56
  ...output,
57
+ '</output_format>',
54
58
  ].join('\n');
55
59
  }
56
60
  function buildStartReasoningPrompt(args) {
@@ -64,20 +68,20 @@ function buildStartReasoningPrompt(args) {
64
68
  : String(targetThoughts)}`,
65
69
  ],
66
70
  task: [
67
- `Start a new reasoning session using "${REASONING_TOOL_NAME}".`,
68
- 'Create the first step with a complete, concrete reasoning thought.',
71
+ `Start new reasoning session via "${REASONING_TOOL_NAME}".`,
72
+ 'Generate the first concrete reasoning step.',
69
73
  ],
70
74
  constraints: [
71
75
  THOUGHT_PARAMETER_GUIDANCE,
72
- 'Preserve sessionId from the response for continuation calls.',
73
- 'Continue until status is completed or remainingThoughts is 0.',
76
+ 'Preserve sessionId for continuation.',
77
+ 'Continue until status="completed" or remainingThoughts=0.',
74
78
  ],
75
79
  output: [
76
- 'Return the first tool call payload only.',
77
- 'Fields: query, level, thought, and optional targetThoughts.',
80
+ 'Return exactly one tool call payload.',
81
+ 'Required fields: query, level, thought.',
78
82
  ],
79
83
  });
80
- return `${base}\n\n---\n\n${getTemplate(level)}`;
84
+ return `${base}\n\n${getTemplate(level)}`;
81
85
  }
82
86
  function buildRetryReasoningPrompt(args) {
83
87
  const { query, level, targetThoughts } = args;
@@ -90,18 +94,18 @@ function buildRetryReasoningPrompt(args) {
90
94
  : String(targetThoughts)}`,
91
95
  ],
92
96
  task: [
93
- `Retry by calling "${REASONING_TOOL_NAME}" with an improved first thought.`,
97
+ `Retry calling "${REASONING_TOOL_NAME}" with an improved first thought.`,
94
98
  ],
95
99
  constraints: [
96
100
  THOUGHT_PARAMETER_GUIDANCE,
97
- 'Use a direct and specific thought with no filler language.',
101
+ 'Write a direct, specific thought. No filler.',
98
102
  ],
99
103
  output: [
100
- 'Return one tool call payload only.',
101
- 'Fields: query, level, thought, and optional targetThoughts.',
104
+ 'Return exactly one tool call payload.',
105
+ 'Required fields: query, level, thought.',
102
106
  ],
103
107
  });
104
- return `${base}\n\n---\n\n${getTemplate(level)}`;
108
+ return `${base}\n\n${getTemplate(level)}`;
105
109
  }
106
110
  function buildContinueReasoningPrompt(args) {
107
111
  const { sessionId, query, level, targetThoughts } = args;
@@ -119,17 +123,17 @@ function buildContinueReasoningPrompt(args) {
119
123
  : String(targetThoughts)}`,
120
124
  ],
121
125
  task: [
122
- `Continue the existing session using "${REASONING_TOOL_NAME}".`,
123
- 'Generate the next reasoning step only.',
126
+ `Continue session via "${REASONING_TOOL_NAME}".`,
127
+ 'Generate the next reasoning step.',
124
128
  ],
125
129
  constraints: [
126
130
  THOUGHT_PARAMETER_GUIDANCE,
127
- 'Keep the same sessionId in the call payload.',
128
- 'Prefer concise, concrete reasoning over meta commentary.',
131
+ 'Keep the same sessionId.',
132
+ 'Write concrete reasoning. No meta commentary.',
129
133
  ],
130
134
  output: [
131
- 'Return one continuation tool call payload only.',
132
- 'Fields: sessionId, thought, and optional query/level/targetThoughts.',
135
+ 'Return exactly one continuation tool call payload.',
136
+ 'Required fields: sessionId, thought.',
133
137
  ],
134
138
  });
135
139
  }