@csmedeiros/codemax 1.0.3 → 1.0.7
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 +4 -20
- package/launcher.js +28 -0
- package/package.json +15 -121
- package/LICENSE +0 -21
- package/dist/commands/cursorRules.d.ts +0 -15
- package/dist/commands/cursorRules.js +0 -118
- package/dist/commands/slashCommands.d.ts +0 -36
- package/dist/commands/slashCommands.js +0 -236
- package/dist/configuration/configManager.d.ts +0 -32
- package/dist/configuration/configManager.js +0 -71
- package/dist/configuration/modelContextWindows.d.ts +0 -3
- package/dist/configuration/modelContextWindows.js +0 -13
- package/dist/conversation/agentGraph.d.ts +0 -203
- package/dist/conversation/agentGraph.js +0 -433
- package/dist/conversation/agentTurn.d.ts +0 -40
- package/dist/conversation/agentTurn.js +0 -252
- package/dist/conversation/chatHistory.d.ts +0 -24
- package/dist/conversation/chatHistory.js +0 -251
- package/dist/conversation/compactionUtils.d.ts +0 -17
- package/dist/conversation/compactionUtils.js +0 -57
- package/dist/conversation/prompts/planPrompt.d.ts +0 -1
- package/dist/conversation/prompts/planPrompt.js +0 -12
- package/dist/conversation/prompts/systemPrompt.d.ts +0 -29
- package/dist/conversation/prompts/systemPrompt.js +0 -149
- package/dist/entry/cli.d.ts +0 -2
- package/dist/entry/cli.js +0 -24
- package/dist/observability/langfuseTracing.d.ts +0 -5
- package/dist/observability/langfuseTracing.js +0 -76
- package/dist/shared/types.d.ts +0 -25
- package/dist/shared/types.js +0 -1
- package/dist/terminal/app.d.ts +0 -2
- package/dist/terminal/app.js +0 -1236
- package/dist/terminal/components.d.ts +0 -18
- package/dist/terminal/components.js +0 -43
- package/dist/terminal/markdown.d.ts +0 -4
- package/dist/terminal/markdown.js +0 -47
- package/dist/terminal/screens/compactionSettings.d.ts +0 -6
- package/dist/terminal/screens/compactionSettings.js +0 -66
- package/dist/terminal/screens/modelSettings.d.ts +0 -10
- package/dist/terminal/screens/modelSettings.js +0 -76
- package/dist/terminal/textField.d.ts +0 -7
- package/dist/terminal/textField.js +0 -136
- package/dist/terminal/theme.d.ts +0 -23
- package/dist/terminal/theme.js +0 -23
- package/dist/tooling/mcpConfig.d.ts +0 -40
- package/dist/tooling/mcpConfig.js +0 -49
- package/dist/tooling/planControlChannel.d.ts +0 -2
- package/dist/tooling/planControlChannel.js +0 -21
- package/dist/tooling/toolConfig.d.ts +0 -42
- package/dist/tooling/toolConfig.js +0 -138
- package/dist/tooling/toolUiCallback.d.ts +0 -21
- package/dist/tooling/toolUiCallback.js +0 -268
- package/dist/tooling/tools.d.ts +0 -216
- package/dist/tooling/tools.js +0 -614
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import { HumanMessage, AIMessage, ToolMessage } from '@langchain/core/messages';
|
|
2
|
-
import { appendFileSync } from 'node:fs';
|
|
3
|
-
import { Command } from '@langchain/langgraph';
|
|
4
|
-
import { agent } from './agentGraph.js';
|
|
5
|
-
import { createToolUiCallbackHandlerWithSink } from '../tooling/toolUiCallback.js';
|
|
6
|
-
import { getLangfuseCallbacksForTurn } from '../observability/langfuseTracing.js';
|
|
7
|
-
import { getConfig } from '../configuration/configManager.js';
|
|
8
|
-
function debugEventsEnabled() {
|
|
9
|
-
return getConfig().debugEvents;
|
|
10
|
-
}
|
|
11
|
-
function debugLog(message) {
|
|
12
|
-
if (!debugEventsEnabled())
|
|
13
|
-
return;
|
|
14
|
-
const ts = new Date().toISOString();
|
|
15
|
-
const line = `[CodeMax][events][${ts}] ${message}\n`;
|
|
16
|
-
// Writing to stderr corrupts the Ink TUI. Only write to file.
|
|
17
|
-
try {
|
|
18
|
-
const filePath = (process.env['CODEMAX_DEBUG_LOG_FILE'] ?? '').trim() ||
|
|
19
|
-
'/tmp/codemax-debug.log';
|
|
20
|
-
appendFileSync(filePath, line, { encoding: 'utf8' });
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
// ignore
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
function messageContentToPlainText(content) {
|
|
27
|
-
if (typeof content === 'string')
|
|
28
|
-
return { text: content };
|
|
29
|
-
if (!content)
|
|
30
|
-
return { text: '' };
|
|
31
|
-
if (typeof content === 'object' && !Array.isArray(content)) {
|
|
32
|
-
const obj = content;
|
|
33
|
-
if (obj['type'] === 'text' && typeof obj['text'] === 'string') {
|
|
34
|
-
return { text: obj['text'] };
|
|
35
|
-
}
|
|
36
|
-
if ((obj['type'] === 'thinking' || obj['type'] === 'reasoning') &&
|
|
37
|
-
typeof obj['text'] === 'string') {
|
|
38
|
-
return { text: obj['text'], isThinking: true };
|
|
39
|
-
}
|
|
40
|
-
if (typeof obj['content'] === 'string')
|
|
41
|
-
return { text: obj['content'] };
|
|
42
|
-
return { text: '' };
|
|
43
|
-
}
|
|
44
|
-
if (!Array.isArray(content))
|
|
45
|
-
return { text: '' };
|
|
46
|
-
let fullText = '';
|
|
47
|
-
let anyThinking = false;
|
|
48
|
-
for (const part of content) {
|
|
49
|
-
if (typeof part === 'string') {
|
|
50
|
-
fullText += part;
|
|
51
|
-
}
|
|
52
|
-
else if (part && typeof part === 'object') {
|
|
53
|
-
const obj = part;
|
|
54
|
-
if (obj['type'] === 'text' && typeof obj['text'] === 'string') {
|
|
55
|
-
fullText += obj['text'];
|
|
56
|
-
}
|
|
57
|
-
else if ((obj['type'] === 'thinking' || obj['type'] === 'reasoning') &&
|
|
58
|
-
typeof obj['text'] === 'string') {
|
|
59
|
-
fullText += obj['text'];
|
|
60
|
-
anyThinking = true;
|
|
61
|
-
}
|
|
62
|
-
else if (typeof obj['content'] === 'string') {
|
|
63
|
-
fullText += obj['content'];
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return { text: fullText, isThinking: anyThinking };
|
|
68
|
-
}
|
|
69
|
-
export async function* streamAgentTurn(options) {
|
|
70
|
-
const queue = [];
|
|
71
|
-
const waiters = [];
|
|
72
|
-
let completed = false;
|
|
73
|
-
const push = (event) => {
|
|
74
|
-
debugLog(`push ${event.type}`);
|
|
75
|
-
queue.push(event);
|
|
76
|
-
const resolve = waiters.shift();
|
|
77
|
-
resolve?.();
|
|
78
|
-
};
|
|
79
|
-
const callbacks = [
|
|
80
|
-
createToolUiCallbackHandlerWithSink(e => {
|
|
81
|
-
debugLog(`callback ${e.type} tool=${e.toolName ?? ''} input_len=${(e.input ?? '').length} output_len=${(e.output ?? '').length}`);
|
|
82
|
-
// Re-emit into our turn event stream.
|
|
83
|
-
if (e.type === 'on_tool_start')
|
|
84
|
-
push({ type: 'on_tool_start', toolName: e.toolName, input: e.input });
|
|
85
|
-
else if (e.type === 'on_tool_end')
|
|
86
|
-
push({ type: 'on_tool_end', toolName: e.toolName, output: e.output });
|
|
87
|
-
else
|
|
88
|
-
push({ type: 'on_tool_error', error: e.error });
|
|
89
|
-
}),
|
|
90
|
-
...(options.tracingEnabled
|
|
91
|
-
? getLangfuseCallbacksForTurn(options.sessionId)
|
|
92
|
-
: []),
|
|
93
|
-
];
|
|
94
|
-
if (options.prompt) {
|
|
95
|
-
push({ type: 'on_chat_start' });
|
|
96
|
-
push({ type: 'status', text: `Searching... (${options.threadId})` });
|
|
97
|
-
}
|
|
98
|
-
const run = (async () => {
|
|
99
|
-
try {
|
|
100
|
-
debugLog(`agent.stream start thread_id=${options.threadId}`);
|
|
101
|
-
const initialState = await agent.getState({
|
|
102
|
-
configurable: { thread_id: options.threadId },
|
|
103
|
-
});
|
|
104
|
-
if (initialState.values?.todos) {
|
|
105
|
-
push({ type: 'on_state_update', todos: initialState.values.todos });
|
|
106
|
-
}
|
|
107
|
-
const input = options.resumeResponse
|
|
108
|
-
? new Command({
|
|
109
|
-
resume: options.resumeResponse,
|
|
110
|
-
update: { toolExecutionMode: options.toolExecutionMode },
|
|
111
|
-
})
|
|
112
|
-
: {
|
|
113
|
-
messages: [new HumanMessage(options.prompt)],
|
|
114
|
-
toolExecutionMode: options.toolExecutionMode,
|
|
115
|
-
activeSkill: options.activeSkill,
|
|
116
|
-
};
|
|
117
|
-
const stream = await agent.stream(input, {
|
|
118
|
-
streamMode: 'messages',
|
|
119
|
-
configurable: { thread_id: options.threadId },
|
|
120
|
-
callbacks,
|
|
121
|
-
signal: options.signal,
|
|
122
|
-
recursionLimit: getConfig().recursionLimit,
|
|
123
|
-
});
|
|
124
|
-
let inThinking = false;
|
|
125
|
-
const emitThinking = (text) => {
|
|
126
|
-
if (!text)
|
|
127
|
-
return;
|
|
128
|
-
const prefix = inThinking ? '' : '> [Thinking] ';
|
|
129
|
-
inThinking = true;
|
|
130
|
-
const formatted = (prefix + text).replace(/\n/g, '\n> ');
|
|
131
|
-
push({ type: 'on_chat_delta', text: formatted });
|
|
132
|
-
};
|
|
133
|
-
const emitContent = (text) => {
|
|
134
|
-
if (!text)
|
|
135
|
-
return;
|
|
136
|
-
const prefix = inThinking ? '\n\n' : '';
|
|
137
|
-
inThinking = false;
|
|
138
|
-
push({ type: 'on_chat_delta', text: prefix + text });
|
|
139
|
-
};
|
|
140
|
-
for await (const chunk of stream) {
|
|
141
|
-
// Normalize chunk to a message object
|
|
142
|
-
let msg = null;
|
|
143
|
-
if (Array.isArray(chunk)) {
|
|
144
|
-
msg = chunk[0];
|
|
145
|
-
}
|
|
146
|
-
else if (chunk && typeof chunk === 'object') {
|
|
147
|
-
msg = chunk;
|
|
148
|
-
}
|
|
149
|
-
if (msg) {
|
|
150
|
-
// Only push deltas for assistant messages (AIMessage chunks)
|
|
151
|
-
// ToolMessages and HumanMessages should not be streamed to the chat buffer
|
|
152
|
-
const isAI = msg instanceof AIMessage ||
|
|
153
|
-
msg._getType() === 'ai' ||
|
|
154
|
-
(msg.tool_calls && !msg.tool_call_id);
|
|
155
|
-
if (isAI) {
|
|
156
|
-
const reasoning = msg.additional_kwargs?.reasoning_content ??
|
|
157
|
-
msg.additional_kwargs?.reasoning;
|
|
158
|
-
if (typeof reasoning === 'string' && reasoning.length > 0) {
|
|
159
|
-
emitThinking(reasoning);
|
|
160
|
-
}
|
|
161
|
-
const content = msg.content;
|
|
162
|
-
const { text, isThinking } = messageContentToPlainText(content);
|
|
163
|
-
if (text) {
|
|
164
|
-
if (isThinking)
|
|
165
|
-
emitThinking(text);
|
|
166
|
-
else
|
|
167
|
-
emitContent(text);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
// Check for interrupts after the stream finishes.
|
|
173
|
-
// 'messages' stream mode often doesn't yield the interrupt itself.
|
|
174
|
-
const state = await agent.getState({
|
|
175
|
-
configurable: { thread_id: options.threadId },
|
|
176
|
-
});
|
|
177
|
-
if (state.values?.todos) {
|
|
178
|
-
push({ type: 'on_state_update', todos: state.values.todos });
|
|
179
|
-
}
|
|
180
|
-
if (state.tasks && state.tasks.length > 0) {
|
|
181
|
-
const lastTask = state.tasks[state.tasks.length - 1];
|
|
182
|
-
const firstInterrupt = lastTask?.interrupts?.[0];
|
|
183
|
-
if (firstInterrupt) {
|
|
184
|
-
debugLog(`detected interrupt in state thread_id=${options.threadId}`);
|
|
185
|
-
push({ type: 'on_interrupt', payload: firstInterrupt.value });
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
push({ type: 'on_chat_end' });
|
|
190
|
-
push({ type: 'status', text: `Ready · thread ${options.threadId}` });
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
const isAbort = error instanceof Error && error.name === 'AbortError';
|
|
194
|
-
if (!isAbort) {
|
|
195
|
-
process.stderr.write(`\n\n[CodeMax] CRITICAL ERROR: ${error?.message || error}\n\n`);
|
|
196
|
-
}
|
|
197
|
-
debugLog(`agent.stream ERROR thread_id=${options.threadId} error=${error}`);
|
|
198
|
-
if (isAbort) {
|
|
199
|
-
debugLog(`agent.stream aborted thread_id=${options.threadId}`);
|
|
200
|
-
// Cleanup incomplete tool calls from checkpoint history.
|
|
201
|
-
try {
|
|
202
|
-
const history = await agent.getState({
|
|
203
|
-
configurable: { thread_id: options.threadId },
|
|
204
|
-
});
|
|
205
|
-
const messages = history.values.messages ?? [];
|
|
206
|
-
const cleaned = [];
|
|
207
|
-
const toolMessageIds = new Set(messages
|
|
208
|
-
.filter((m) => m instanceof ToolMessage || m._getType() === 'tool')
|
|
209
|
-
.map((m) => m.tool_call_id));
|
|
210
|
-
for (const m of messages) {
|
|
211
|
-
if (m instanceof AIMessage || m._getType() === 'ai') {
|
|
212
|
-
const toolCalls = m.tool_calls ?? [];
|
|
213
|
-
const hasOrphan = toolCalls.some(tc => !toolMessageIds.has(tc.id));
|
|
214
|
-
if (hasOrphan) {
|
|
215
|
-
debugLog(`cleaning up AIMessage with orphan tool calls id=${m.id}`);
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
cleaned.push(m);
|
|
220
|
-
}
|
|
221
|
-
if (cleaned.length !== messages.length) {
|
|
222
|
-
await agent.updateState({ configurable: { thread_id: options.threadId } }, { messages: cleaned }, 'callModel');
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
catch (cleanupError) {
|
|
226
|
-
debugLog(`failed to cleanup history: ${cleanupError}`);
|
|
227
|
-
}
|
|
228
|
-
push({ type: 'status', text: 'Paused' });
|
|
229
|
-
}
|
|
230
|
-
else {
|
|
231
|
-
push({
|
|
232
|
-
type: 'on_tool_error',
|
|
233
|
-
error: error instanceof Error ? error.message : String(error),
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
finally {
|
|
238
|
-
debugLog(`agent.stream done thread_id=${options.threadId}`);
|
|
239
|
-
completed = true;
|
|
240
|
-
const resolve = waiters.shift();
|
|
241
|
-
resolve?.();
|
|
242
|
-
}
|
|
243
|
-
})();
|
|
244
|
-
while (!completed || queue.length) {
|
|
245
|
-
if (!queue.length) {
|
|
246
|
-
await new Promise(resolve => waiters.push(resolve));
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
yield queue.shift();
|
|
250
|
-
}
|
|
251
|
-
await run;
|
|
252
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { type TranscriptItem } from '../shared/types.js';
|
|
2
|
-
export type HistoryMessage = {
|
|
3
|
-
role: 'user' | 'assistant';
|
|
4
|
-
content: string;
|
|
5
|
-
};
|
|
6
|
-
export type ThreadSnapshot = {
|
|
7
|
-
messages: HistoryMessage[];
|
|
8
|
-
todos: Array<{
|
|
9
|
-
id: string;
|
|
10
|
-
task: string;
|
|
11
|
-
status: 'pending' | 'in_progress' | 'completed';
|
|
12
|
-
}>;
|
|
13
|
-
};
|
|
14
|
-
export declare function loadThreadSnapshot(threadId: string): ThreadSnapshot;
|
|
15
|
-
export declare function loadChatHistory(threadId: string): TranscriptItem[];
|
|
16
|
-
export type ChatEntry = {
|
|
17
|
-
threadId: string;
|
|
18
|
-
name: string | null;
|
|
19
|
-
createdAt: number;
|
|
20
|
-
};
|
|
21
|
-
export declare function initChatNames(): void;
|
|
22
|
-
export declare function listChats(): ChatEntry[];
|
|
23
|
-
export declare function saveThreadName(threadId: string, name: string): void;
|
|
24
|
-
export declare function generateThreadName(firstUserMessage: string): Promise<string>;
|
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
import { HumanMessage } from '@langchain/core/messages';
|
|
2
|
-
import { db, model, COMPACTION_SUMMARY_SENTINEL } from './agentGraph.js';
|
|
3
|
-
import { resolveToolUi, buildToolUiVars, renderToolTemplate, } from '../tooling/toolConfig.js';
|
|
4
|
-
function loadFullHistoryMessages(threadId) {
|
|
5
|
-
try {
|
|
6
|
-
const row = db
|
|
7
|
-
.prepare(`SELECT messages_json FROM chat_messages_full WHERE thread_id = ?`)
|
|
8
|
-
.get(threadId);
|
|
9
|
-
if (!row)
|
|
10
|
-
return null;
|
|
11
|
-
const stored = JSON.parse(row.messages_json);
|
|
12
|
-
// Re-shape storage format into the same shape loaders expect (id-tagged LangChain JSON).
|
|
13
|
-
return stored.map(m => ({
|
|
14
|
-
id: [
|
|
15
|
-
'langchain_core',
|
|
16
|
-
'messages',
|
|
17
|
-
m.type === 'human'
|
|
18
|
-
? 'HumanMessage'
|
|
19
|
-
: m.type === 'ai'
|
|
20
|
-
? 'AIMessage'
|
|
21
|
-
: m.type === 'tool'
|
|
22
|
-
? 'ToolMessage'
|
|
23
|
-
: m.type === 'system'
|
|
24
|
-
? 'SystemMessage'
|
|
25
|
-
: 'BaseMessage',
|
|
26
|
-
],
|
|
27
|
-
kwargs: {
|
|
28
|
-
content: m.content,
|
|
29
|
-
tool_calls: m.tool_calls,
|
|
30
|
-
tool_call_id: m.tool_call_id,
|
|
31
|
-
name: m.name,
|
|
32
|
-
},
|
|
33
|
-
}));
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
function isCompactionSummaryMessage(msg) {
|
|
40
|
-
const c = msg?.kwargs?.content;
|
|
41
|
-
const text = Array.isArray(c)
|
|
42
|
-
? c
|
|
43
|
-
.map((b) => (typeof b === 'string' ? b : b?.text ?? ''))
|
|
44
|
-
.join('')
|
|
45
|
-
: String(c ?? '');
|
|
46
|
-
return text.startsWith(COMPACTION_SUMMARY_SENTINEL);
|
|
47
|
-
}
|
|
48
|
-
export function loadThreadSnapshot(threadId) {
|
|
49
|
-
try {
|
|
50
|
-
const row = db
|
|
51
|
-
.prepare(`SELECT checkpoint FROM checkpoints WHERE thread_id = ? AND checkpoint_ns = '' ORDER BY checkpoint_id DESC LIMIT 1`)
|
|
52
|
-
.get(threadId);
|
|
53
|
-
if (!row)
|
|
54
|
-
return { messages: [], todos: [] };
|
|
55
|
-
const parsed = JSON.parse(row.checkpoint);
|
|
56
|
-
const fullHistory = loadFullHistoryMessages(threadId);
|
|
57
|
-
const rawMessages = (fullHistory ?? parsed.channel_values?.messages ?? []).filter((m) => !isCompactionSummaryMessage(m));
|
|
58
|
-
const todos = parsed.channel_values?.todos ?? [];
|
|
59
|
-
const messages = [];
|
|
60
|
-
for (const msg of rawMessages) {
|
|
61
|
-
const msgType = Array.isArray(msg.id)
|
|
62
|
-
? msg.id[msg.id.length - 1]
|
|
63
|
-
: '';
|
|
64
|
-
const rawContent = msg.kwargs?.content;
|
|
65
|
-
if (msgType === 'HumanMessage') {
|
|
66
|
-
const content = Array.isArray(rawContent)
|
|
67
|
-
? rawContent
|
|
68
|
-
.filter((b) => b.type === 'text')
|
|
69
|
-
.map((b) => b.text ?? '')
|
|
70
|
-
.join('')
|
|
71
|
-
: String(rawContent ?? '');
|
|
72
|
-
messages.push({ role: 'user', content });
|
|
73
|
-
}
|
|
74
|
-
else if (msgType === 'AIMessage' || msgType === 'AIMessageChunk') {
|
|
75
|
-
const content = Array.isArray(rawContent)
|
|
76
|
-
? rawContent
|
|
77
|
-
.filter((b) => b.type === 'text')
|
|
78
|
-
.map((b) => b.text ?? '')
|
|
79
|
-
.join('')
|
|
80
|
-
: String(rawContent ?? '');
|
|
81
|
-
if (content)
|
|
82
|
-
messages.push({ role: 'assistant', content });
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return { messages, todos };
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
return { messages: [], todos: [] };
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
export function loadChatHistory(threadId) {
|
|
92
|
-
try {
|
|
93
|
-
const row = db
|
|
94
|
-
.prepare(`SELECT checkpoint FROM checkpoints WHERE thread_id = ? AND checkpoint_ns = '' ORDER BY checkpoint_id DESC LIMIT 1`)
|
|
95
|
-
.get(threadId);
|
|
96
|
-
if (!row)
|
|
97
|
-
return [];
|
|
98
|
-
const parsed = JSON.parse(row.checkpoint);
|
|
99
|
-
const fullHistory = loadFullHistoryMessages(threadId);
|
|
100
|
-
const rawMessages = (fullHistory ?? parsed.channel_values?.messages ?? []).filter((m) => !isCompactionSummaryMessage(m));
|
|
101
|
-
// Build map of tool_call_id -> tool result content for look-ahead
|
|
102
|
-
const toolResultMap = new Map();
|
|
103
|
-
for (const msg of rawMessages) {
|
|
104
|
-
const msgType = Array.isArray(msg.id)
|
|
105
|
-
? msg.id[msg.id.length - 1]
|
|
106
|
-
: '';
|
|
107
|
-
if (msgType === 'ToolMessage') {
|
|
108
|
-
const toolCallId = msg.kwargs?.tool_call_id ?? '';
|
|
109
|
-
const rawContent = msg.kwargs?.content;
|
|
110
|
-
const content = typeof rawContent === 'string'
|
|
111
|
-
? rawContent
|
|
112
|
-
: JSON.stringify(rawContent ?? '');
|
|
113
|
-
if (toolCallId)
|
|
114
|
-
toolResultMap.set(toolCallId, content);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
const items = [];
|
|
118
|
-
let counter = 0;
|
|
119
|
-
for (const msg of rawMessages) {
|
|
120
|
-
const msgType = Array.isArray(msg.id)
|
|
121
|
-
? msg.id[msg.id.length - 1]
|
|
122
|
-
: '';
|
|
123
|
-
const rawContent = msg.kwargs?.content;
|
|
124
|
-
if (msgType === 'HumanMessage') {
|
|
125
|
-
const text = Array.isArray(rawContent)
|
|
126
|
-
? rawContent
|
|
127
|
-
.filter((b) => b.type === 'text')
|
|
128
|
-
.map((b) => String(b.text ?? ''))
|
|
129
|
-
.join('')
|
|
130
|
-
: String(rawContent ?? '');
|
|
131
|
-
items.push({ id: `hist-${counter++}`, role: 'user', text });
|
|
132
|
-
}
|
|
133
|
-
else if (msgType === 'AIMessage' || msgType === 'AIMessageChunk') {
|
|
134
|
-
const text = Array.isArray(rawContent)
|
|
135
|
-
? rawContent
|
|
136
|
-
.filter((b) => b.type === 'text')
|
|
137
|
-
.map((b) => String(b.text ?? ''))
|
|
138
|
-
.join('')
|
|
139
|
-
: String(rawContent ?? '');
|
|
140
|
-
if (text) {
|
|
141
|
-
items.push({
|
|
142
|
-
id: `hist-${counter++}`,
|
|
143
|
-
role: 'assistant',
|
|
144
|
-
text,
|
|
145
|
-
timestamp: '',
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
// Emit tool start + end items for each tool_call on this AI message
|
|
149
|
-
const toolCalls = msg.kwargs?.tool_calls ?? [];
|
|
150
|
-
for (const tc of toolCalls) {
|
|
151
|
-
const toolName = tc.name ?? 'tool';
|
|
152
|
-
const input = JSON.stringify(tc.args ?? {});
|
|
153
|
-
const output = toolResultMap.get(tc.id ?? '') ?? '';
|
|
154
|
-
const cfg = resolveToolUi(toolName);
|
|
155
|
-
const vars = buildToolUiVars({
|
|
156
|
-
toolName,
|
|
157
|
-
input,
|
|
158
|
-
output,
|
|
159
|
-
toolCallId: tc.id,
|
|
160
|
-
runId: `hist-${counter}`,
|
|
161
|
-
maxInputPreview: cfg.maxInputPreview,
|
|
162
|
-
maxOutputPreview: cfg.maxOutputPreview,
|
|
163
|
-
placeholders: cfg.placeholders,
|
|
164
|
-
});
|
|
165
|
-
items.push({
|
|
166
|
-
id: `hist-${counter++}-start`,
|
|
167
|
-
role: 'tool',
|
|
168
|
-
kind: 'start',
|
|
169
|
-
toolName,
|
|
170
|
-
text: renderToolTemplate(cfg.onCallTemplate, vars),
|
|
171
|
-
});
|
|
172
|
-
if (output) {
|
|
173
|
-
items.push({
|
|
174
|
-
id: `hist-${counter++}-end`,
|
|
175
|
-
role: 'tool',
|
|
176
|
-
kind: 'end',
|
|
177
|
-
toolName,
|
|
178
|
-
text: renderToolTemplate(cfg.onResultTemplate, vars),
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
// ToolMessage entries are consumed via toolResultMap; skip standalone
|
|
184
|
-
}
|
|
185
|
-
return items;
|
|
186
|
-
}
|
|
187
|
-
catch {
|
|
188
|
-
return [];
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
let chatNamesInitialized = false;
|
|
192
|
-
export function initChatNames() {
|
|
193
|
-
db.exec(`
|
|
194
|
-
CREATE TABLE IF NOT EXISTS chat_names (
|
|
195
|
-
thread_id TEXT PRIMARY KEY,
|
|
196
|
-
name TEXT NOT NULL,
|
|
197
|
-
created_at INTEGER NOT NULL
|
|
198
|
-
)
|
|
199
|
-
`);
|
|
200
|
-
}
|
|
201
|
-
export function listChats() {
|
|
202
|
-
if (!chatNamesInitialized) {
|
|
203
|
-
initChatNames();
|
|
204
|
-
chatNamesInitialized = true;
|
|
205
|
-
}
|
|
206
|
-
try {
|
|
207
|
-
const rows = db
|
|
208
|
-
.prepare(`SELECT DISTINCT c.thread_id, cn.name, cn.created_at
|
|
209
|
-
FROM checkpoints c
|
|
210
|
-
LEFT JOIN chat_names cn ON cn.thread_id = c.thread_id
|
|
211
|
-
ORDER BY COALESCE(cn.created_at, 0) DESC`)
|
|
212
|
-
.all();
|
|
213
|
-
return rows.map(row => ({
|
|
214
|
-
threadId: row.thread_id,
|
|
215
|
-
name: row.name ?? null,
|
|
216
|
-
createdAt: row.created_at ?? 0,
|
|
217
|
-
}));
|
|
218
|
-
}
|
|
219
|
-
catch {
|
|
220
|
-
return [];
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
export function saveThreadName(threadId, name) {
|
|
224
|
-
if (!chatNamesInitialized) {
|
|
225
|
-
initChatNames();
|
|
226
|
-
chatNamesInitialized = true;
|
|
227
|
-
}
|
|
228
|
-
db.prepare(`INSERT INTO chat_names (thread_id, name, created_at) VALUES (?, ?, ?)
|
|
229
|
-
ON CONFLICT(thread_id) DO UPDATE SET name = excluded.name`).run(threadId, name, Date.now());
|
|
230
|
-
}
|
|
231
|
-
export async function generateThreadName(firstUserMessage) {
|
|
232
|
-
try {
|
|
233
|
-
const response = await model.invoke([
|
|
234
|
-
new HumanMessage(`Name this conversation in 6 words or fewer, title case, no punctuation: ${firstUserMessage}`),
|
|
235
|
-
]);
|
|
236
|
-
let text;
|
|
237
|
-
if (Array.isArray(response.content)) {
|
|
238
|
-
text = response.content
|
|
239
|
-
.map(b => (typeof b === 'string' ? b : b.text ?? ''))
|
|
240
|
-
.join('')
|
|
241
|
-
.trim();
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
text = String(response.content).trim();
|
|
245
|
-
}
|
|
246
|
-
return text || `Conversation ${Date.now()}`;
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
return `Conversation ${Date.now()}`;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure, side-effect-free helpers for context compaction.
|
|
3
|
-
* No database, no LLM, no graph — safe to import in tests.
|
|
4
|
-
*/
|
|
5
|
-
import { type BaseMessage } from '@langchain/core/messages';
|
|
6
|
-
export declare function estimateTokens(messages: BaseMessage[]): number;
|
|
7
|
-
export declare function shouldCompactMessages(messages: BaseMessage[], opts: {
|
|
8
|
-
threshold: number;
|
|
9
|
-
contextWindow: number;
|
|
10
|
-
}): boolean;
|
|
11
|
-
/**
|
|
12
|
-
* Returns the index at which the kept tail begins.
|
|
13
|
-
* Messages before keepStart will be summarized and removed.
|
|
14
|
-
* Guarantees the boundary never splits an AIMessage-with-tool-calls
|
|
15
|
-
* from its following ToolMessages.
|
|
16
|
-
*/
|
|
17
|
-
export declare function computeKeepStart(messages: BaseMessage[], tokensToKeep: number): number;
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure, side-effect-free helpers for context compaction.
|
|
3
|
-
* No database, no LLM, no graph — safe to import in tests.
|
|
4
|
-
*/
|
|
5
|
-
// Char/4 token estimation — no native tiktoken dep, sufficient for thresholding.
|
|
6
|
-
export function estimateTokens(messages) {
|
|
7
|
-
let chars = 0;
|
|
8
|
-
for (const m of messages) {
|
|
9
|
-
const c = m.content;
|
|
10
|
-
if (typeof c === 'string') {
|
|
11
|
-
chars += c.length;
|
|
12
|
-
}
|
|
13
|
-
else if (Array.isArray(c)) {
|
|
14
|
-
for (const block of c) {
|
|
15
|
-
if (typeof block === 'string')
|
|
16
|
-
chars += block.length;
|
|
17
|
-
else if (block && typeof block === 'object' && 'text' in block) {
|
|
18
|
-
chars += String(block.text || '').length;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
const toolCalls = m.tool_calls;
|
|
23
|
-
if (toolCalls && toolCalls.length > 0) {
|
|
24
|
-
for (const tc of toolCalls) {
|
|
25
|
-
chars += JSON.stringify(tc.args ?? {}).length + (tc.name?.length ?? 0);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return Math.ceil(chars / 4);
|
|
30
|
-
}
|
|
31
|
-
export function shouldCompactMessages(messages, opts) {
|
|
32
|
-
const used = estimateTokens(messages);
|
|
33
|
-
return used >= opts.threshold * opts.contextWindow;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Returns the index at which the kept tail begins.
|
|
37
|
-
* Messages before keepStart will be summarized and removed.
|
|
38
|
-
* Guarantees the boundary never splits an AIMessage-with-tool-calls
|
|
39
|
-
* from its following ToolMessages.
|
|
40
|
-
*/
|
|
41
|
-
export function computeKeepStart(messages, tokensToKeep) {
|
|
42
|
-
let runningTokens = 0;
|
|
43
|
-
let keepStart = messages.length;
|
|
44
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
45
|
-
const t = estimateTokens([messages[i]]);
|
|
46
|
-
if (runningTokens + t > tokensToKeep)
|
|
47
|
-
break;
|
|
48
|
-
runningTokens += t;
|
|
49
|
-
keepStart = i;
|
|
50
|
-
}
|
|
51
|
-
// Advance past any ToolMessages to avoid breaking AIMessage+ToolMessage pairs.
|
|
52
|
-
while (keepStart < messages.length &&
|
|
53
|
-
messages[keepStart]._getType() === 'tool') {
|
|
54
|
-
keepStart++;
|
|
55
|
-
}
|
|
56
|
-
return keepStart;
|
|
57
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const PLAN_MODE_PROMPT = "<plan_mode>\nYou are operating in PLAN MODE. Your only job is to produce a written implementation plan \u2014 do NOT write, edit, or execute any files or shell commands.\n\nRULES:\n1. Use the `writePlanTool` to output your plan. Do not use `writeFileTool`, `editFileTool`, or `shellTool`.\n2. Analyse the user's request thoroughly before writing. Ask clarifying questions if requirements are ambiguous.\n3. The plan must be a complete, step-by-step implementation guide in Markdown.\n4. After calling `writePlanTool`, wait for user feedback or acceptance from the browser review page.\n5. If you receive a message starting with \"Received user feedback:\", revise and re-call `writePlanTool` with the updated plan.\n\nYou are blocked from modifying the codebase in plan mode. Focus exclusively on producing the plan document.\n</plan_mode>";
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export const PLAN_MODE_PROMPT = `<plan_mode>
|
|
2
|
-
You are operating in PLAN MODE. Your only job is to produce a written implementation plan — do NOT write, edit, or execute any files or shell commands.
|
|
3
|
-
|
|
4
|
-
RULES:
|
|
5
|
-
1. Use the \`writePlanTool\` to output your plan. Do not use \`writeFileTool\`, \`editFileTool\`, or \`shellTool\`.
|
|
6
|
-
2. Analyse the user's request thoroughly before writing. Ask clarifying questions if requirements are ambiguous.
|
|
7
|
-
3. The plan must be a complete, step-by-step implementation guide in Markdown.
|
|
8
|
-
4. After calling \`writePlanTool\`, wait for user feedback or acceptance from the browser review page.
|
|
9
|
-
5. If you receive a message starting with "Received user feedback:", revise and re-call \`writePlanTool\` with the updated plan.
|
|
10
|
-
|
|
11
|
-
You are blocked from modifying the codebase in plan mode. Focus exclusively on producing the plan document.
|
|
12
|
-
</plan_mode>`;
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* System prompts for the LangChain agent.
|
|
3
|
-
*
|
|
4
|
-
* Customize these prompts to change your agent's behavior and personality.
|
|
5
|
-
*/
|
|
6
|
-
import type { BaseMessage } from '@langchain/core/messages';
|
|
7
|
-
/**
|
|
8
|
-
* Marks ephemeral per-turn system messages that carry the host date/time; stripped before persisting thread history.
|
|
9
|
-
*/
|
|
10
|
-
export declare const CODEMAX_TURN_DATETIME_MARKER: "<!--codemax-datetime-context-->";
|
|
11
|
-
/**
|
|
12
|
-
* Current local date/time using the host OS time zone and locale (via Intl).
|
|
13
|
-
*/
|
|
14
|
-
export declare function formatLocalDateTimeContext(): string;
|
|
15
|
-
/**
|
|
16
|
-
* Content for a single-turn HumanMessage injected before each agent invocation.
|
|
17
|
-
* Using a user message (not system) keeps the system prompt stable for prompt caching.
|
|
18
|
-
*/
|
|
19
|
-
export declare function buildCodemaxTurnDatetimeUserContent(): string;
|
|
20
|
-
/**
|
|
21
|
-
* Removes the ephemeral datetime user message from graph output so REPL history does not accumulate it.
|
|
22
|
-
*/
|
|
23
|
-
export declare function stripCodemaxTurnDatetimeFromMessages(messages: BaseMessage[]): BaseMessage[];
|
|
24
|
-
/**
|
|
25
|
-
* The main system prompt that defines the agent's behavior.
|
|
26
|
-
* This is passed to createAgent as the systemPrompt parameter.
|
|
27
|
-
*/
|
|
28
|
-
export declare const COMPACT_SUMMARY_PROMPT = "You are compacting a long agentic coding session. Produce a detailed summary preserving:\n- The user's overarching goals and the current task being worked on.\n- All file paths read, written, or modified and their purposes.\n- Key decisions and their rationale.\n- Current todo state and in-progress work.\n- Important tool results (errors, search hits, file contents that informed decisions).\n- Open questions and unresolved blockers.\n\nBe specific \u2014 preserve exact filenames, function names, identifiers, command outputs, and error messages. Format as structured Markdown with clear sections. Do not omit details that would be needed to continue the work seamlessly.";
|
|
29
|
-
export declare const SYSTEM_PROMPT = "\n<role>\n\nYou are CodeMax, the next-generation code agent.\nYou are responsible for solving software development tasks by generating code, explanations, and reasoning steps.\n\n</role>\n\n<general_instructions>\n\n- When the conversation includes a system message with the host's current local date and time (and time zone), use it for anything that depends on \"now\", \"today\", or scheduling.\n- You will receive a task and you will need to solve it.\n- The first step is to understand the task and the context by reading the codebase documentation and codebase, and write a TO-DO list of the steps you need to take to solve the task using the 'todoTool'.\n- As you progress through the task, you must update the status of the items in the TO-DO list using the 'todoTool'.\n\n</general_instructions>\n\n<rules>\n\nWhen rules are injected into this conversation, you MUST follow them strictly and without exception.\nRules override your default behavior. They are not suggestions \u2014 treat them as hard constraints.\nIf a rule conflicts with a general instruction, the rule wins.\n\n</rules>\n\n<file_reading>\n\n- NEVER read an entire file unless you are certain you need every line.\n- Always use 'startLine' and 'endLine' to read only the portion you need.\n- If you do not know how many lines a file has or where the relevant section is, start by reading lines 1\u201320 to orient yourself, then read further ranges as needed.\n- Prefer multiple small reads over one large read.\n\n</file_reading>\n\n<todo_tool>\n - Use the 'todoTool' to manage your task list.\n - Actions: 'add', 'update', 'remove', 'list'.\n - Statuses: 'pending', 'in_progress', 'completed'.\n - When starting a task, 'add' all the steps you identified.\n - When you start working on a step, 'update' its status to 'in_progress'.\n - When you finish a step, 'update' its status to 'completed'.\n</todo_tool>\n\n<subagents>\n\n - You are able to use subagents to help you solve tasks.\n - You can delegate tasks for subagents to solve.\n - For delegation, you must use the 'subagent' tool.\n - The subagent tool will return a report of the task completion.\n - The report will be a JSON file with the following properties:\n - 'status': 'success' or 'error'\n - 'message': a short description of the result\n - 'data': the data returned by the subagent\n - 'files': a list of dictionaries with the following properties:\n - 'path': the path of the file\n - 'content': a short description of what he did in the file\n</subagents>\n\n\n\n<response_format>\n\n- You must NEVER answer with emojis.\n- You must ALWAYS answer with markdown headings and markdown bullet points.\n- You must ALWAYS answer with a clear and concise response.\n- Your answers must be ALWAYS about what the user wants to know about or what you have done in your work.\n- NEVER answer with unnecessary facts or anything that doesn't relate to the user's request, your answer, or what the user wants to know.\n\n</response_format>\n";
|