glitool 2.0.4 → 2.1.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/dist/agent.js CHANGED
@@ -1,32 +1,26 @@
1
1
  import { writeFileTool, listFilesTool, readFileTool, searchCodeTool, editFileTool, bashTool, readBackgroundOutputTool, webFetchTool, } from "./tools/index.js";
2
2
  import { AIMessage, BaseMessage, HumanMessage, SystemMessage } from "@langchain/core/messages";
3
- import { StructuredTool } from "@langchain/core/tools";
4
3
  import { createReactAgent } from '@langchain/langgraph/prebuilt';
5
4
  import { ChatOpenAI } from '@langchain/openai';
6
5
  import { loadSession, loadSummary, saveSession, generateAndSaveSummary } from "./memory.js";
7
- import { loadConfig } from "./config.js";
8
6
  import { loadProjectMemory } from "./projectMemory.js";
9
7
  import { config as loadEnv } from 'dotenv';
10
8
  import { fileURLToPath } from 'url';
11
9
  import { dirname, join } from 'path';
12
10
  import { route, stripExplicitPrefix } from './llm/router.js';
13
11
  import { logRouting } from './llm/telemetry.js';
14
- import { runAgentGraph } from "./agents/graph.js";
15
- import { runReviewer } from "./agents/reviewer-agent.js";
16
12
  import os from 'os';
17
13
  import { cleanupAll } from "./tools/processRegistry.js";
18
- import { runPlanningAgent } from "./agents/planningAgent.js";
19
- import { runDebugger } from "./agents/debugger.js";
20
- import { runRefactorer } from "./agents/refactorer.js";
21
- import { runGitAgent } from "./agents/git-agent.js";
22
- import { makeLlm, startNewRequest } from './llm/factory.js';
14
+ import { makeLlm, startNewRequest, getResolvedModelForCurrentRequest } from './llm/factory.js';
23
15
  import { emit } from './monitor.js';
16
+ import { runClarifier } from './clarifier.js';
17
+ import { setClarificationHandler } from './clarificationHandler.js';
18
+ import { execSync } from 'child_process';
19
+ import { runExecutor } from "./agents/executor.js";
24
20
  const __filename = fileURLToPath(import.meta.url);
25
21
  const __dirname = dirname(__filename);
26
22
  loadEnv({ path: join(os.homedir(), '.glitool', '.env') });
27
23
  const MAX_HISTORY_CHARS = 60_000;
28
- // const simpleLlm = makeLlm('meta-llama/Llama-3.3-70B-Instruct-Turbo');
29
- // const simpleLlm = makeLlm('meta-llama/Llama-3.3-70B-Instruct-Turbo');
30
24
  function createLlm(model) {
31
25
  return makeLlm(model);
32
26
  }
@@ -37,8 +31,7 @@ function createLlm(model) {
37
31
  export function getDefaultLlm() {
38
32
  return createLlm('meta-llama/Llama-3.3-70B-Instruct-Turbo');
39
33
  }
40
- // const config = loadConfig();
41
- const tools = [listFilesTool, readFileTool, searchCodeTool, writeFileTool, editFileTool, bashTool, readBackgroundOutputTool, webFetchTool];
34
+ const chatFallbackTools = [listFilesTool, readFileTool, searchCodeTool, writeFileTool, editFileTool, bashTool, readBackgroundOutputTool, webFetchTool];
42
35
  process.on('exit', cleanupAll);
43
36
  process.on('SIGINT', () => { cleanupAll(); process.exit(0); });
44
37
  process.on('SIGTERM', () => { cleanupAll(); process.exit(0); });
@@ -49,13 +42,28 @@ export function clearSession() {
49
42
  }
50
43
  const MAX_SUMMARY_CHARS = 2_000;
51
44
  const MAX_PROJECT_FACTS_CHARS = 3_000;
45
+ function getGitContext() {
46
+ try {
47
+ const status = execSync('git --no-optional-locks status --short --branch', {
48
+ cwd: process.cwd(),
49
+ timeout: 3000,
50
+ stdio: ['pipe', 'pipe', 'pipe'],
51
+ }).toString().trim();
52
+ if (!status)
53
+ return '';
54
+ return `\n\n## Git State\n${status}`;
55
+ }
56
+ catch {
57
+ return '';
58
+ }
59
+ }
52
60
  function buildSystemPrompt() {
53
61
  let summary = loadSummary();
54
62
  const project = loadProjectMemory();
55
63
  if (!summary) {
56
64
  const rawSession = loadSession();
57
65
  if (rawSession.length > 4) {
58
- generateAndSaveSummary(rawSession, getDefaultLlm());
66
+ generateAndSaveSummary(rawSession, getDefaultLlm()).catch(() => { });
59
67
  summary = loadSummary();
60
68
  }
61
69
  }
@@ -70,6 +78,8 @@ You can:
70
78
 
71
79
  Be concise. Default to plain conversation. Only call tools when the request clearly needs them.
72
80
 
81
+ For greetings or small talk (e.g. "hi", "hey", "thanks"), reply warmly in one short line and invite the user to say what they're working on. Don't ask them to clarify a "request" — there isn't one.
82
+
73
83
  When the user asks to read, show, or display a specific file → call readFile.
74
84
  For "read <name>" shorthand, pass the bare name; the tool searches the project automatically.
75
85
  Don't claim a file is missing without verifying via listFiles or readFile first.
@@ -95,6 +105,9 @@ Style:
95
105
  : json;
96
106
  prompt += `\n\nProject facts:\n${capped}`;
97
107
  }
108
+ const gitContext = getGitContext();
109
+ if (gitContext)
110
+ prompt += gitContext;
98
111
  return prompt;
99
112
  }
100
113
  const systemPrompt = await buildSystemPrompt();
@@ -165,14 +178,16 @@ function trimHistory(messages) {
165
178
  }
166
179
  return kept;
167
180
  }
181
+ // Keyed by the model the SERVER actually runs, not the CLI's role hint.
182
+ // Together.ai pricing per million tokens (May 2026).
168
183
  const COST_PER_TOKEN = {
169
- 'gpt-4o-mini': { input: 0.15 / 1_000_000, output: 0.60 / 1_000_000 },
170
- 'gpt-5.4-mini': { input: 0.75 / 1_000_000, output: 4.50 / 1_000_000 },
171
- 'gpt-5.4': { input: 2.50 / 1_000_000, output: 15.00 / 1_000_000 },
172
- 'gpt-5.5': { input: 5.00 / 1_000_000, output: 30.00 / 1_000_000 },
184
+ 'MiniMaxAI/MiniMax-M2.7': { input: 0.30 / 1_000_000, output: 1.20 / 1_000_000 },
185
+ 'deepseek-ai/DeepSeek-V4-Pro': { input: 2.10 / 1_000_000, output: 4.40 / 1_000_000 },
186
+ 'moonshotai/Kimi-K2.6': { input: 1.20 / 1_000_000, output: 4.50 / 1_000_000 },
187
+ 'Qwen/Qwen2.5-7B-Instruct-Turbo': { input: 0.30 / 1_000_000, output: 0.30 / 1_000_000 },
173
188
  };
174
189
  function estimateCost(model, inputTokens, outputTokens) {
175
- const rates = COST_PER_TOKEN[model] ?? COST_PER_TOKEN['gpt-4o-mini'];
190
+ const rates = COST_PER_TOKEN[model] ?? COST_PER_TOKEN['MiniMaxAI/MiniMax-M2.7'];
176
191
  return inputTokens * rates.input + outputTokens * rates.output;
177
192
  }
178
193
  function extractTarget(args) {
@@ -193,13 +208,33 @@ function extractTarget(args) {
193
208
  }
194
209
  return String(first ?? '');
195
210
  }
196
- export async function chat(userInput, onToolCall, onStatus, onToken, onEscalation, onUsage, onStageEvent) {
211
+ export async function chat(userInput, onToolCall, onStatus, onToken, onUsage, onStageEvent, onClarificationNeeded) {
197
212
  startNewRequest();
198
213
  emit('user_prompt', { text: userInput });
199
214
  const decision = await route(userInput, sessionMessages.slice(-6));
200
215
  emit('router', { domain: decision.domain, tier: decision.tier, model: decision.recommendedModel, reason: decision.reason });
201
216
  logRouting(userInput, decision);
202
217
  const cleanedInput = decision.source === 'explicit' ? stripExplicitPrefix(userInput) : userInput;
218
+ // Register the askUser tool callback so the executor can pause mid-execution and ask the user
219
+ if (onClarificationNeeded) {
220
+ setClarificationHandler(onClarificationNeeded);
221
+ }
222
+ // Fast codebase grep — no LLM, just gives the executor a head start
223
+ const { codeContext } = await runClarifier(cleanedInput, decision.domain, sessionMessages.length);
224
+ let finalInput = cleanedInput;
225
+ if (codeContext) {
226
+ finalInput = `${cleanedInput}\n\n[Codebase search context]:\n${codeContext}`;
227
+ }
228
+ emit('clarifier', { questions: [], skipped: true });
229
+ emit('memory', {
230
+ session_messages: sessionMessages.length,
231
+ has_summary: !!loadSummary(),
232
+ has_project: !!loadProjectMemory(),
233
+ recent: sessionMessages.slice(-4).map(m => ({
234
+ role: m._getType(),
235
+ text: (typeof m.content === 'string' ? m.content : JSON.stringify(m.content)).slice(0, 150)
236
+ }))
237
+ });
203
238
  sessionMessages.push(new HumanMessage(cleanedInput));
204
239
  const shortcut = await tryDirectReadShortcut(cleanedInput, onToolCall);
205
240
  if (shortcut !== null) {
@@ -207,93 +242,37 @@ export async function chat(userInput, onToolCall, onStatus, onToken, onEscalatio
207
242
  saveSession(sessionMessages);
208
243
  return shortcut;
209
244
  }
210
- if (decision.domain === 'planning') {
211
- emit('agent', { name: 'planning' });
212
- onStatus?.('Planning...');
213
- const result = await runPlanningAgent(cleanedInput, (inputTokens, outputTokens) => {
214
- onUsage?.(inputTokens + outputTokens, estimateCost('gpt-5.4', inputTokens, outputTokens));
215
- });
216
- sessionMessages.push(new AIMessage(result));
217
- saveSession(sessionMessages);
218
- return result;
219
- }
220
- if (decision.domain === 'review') {
221
- emit('agent', { name: 'reviewer' });
222
- onStageEvent?.({ type: 'stage_start', stage: 'reviewer' });
223
- const result = await runReviewer(cleanedInput, (name, args) => {
224
- onStageEvent?.({ type: 'tool', stage: 'reviewer', tool: name, target: extractTarget(args) });
225
- onToolCall(name, args);
226
- }, decision.recommendedModel);
227
- onStageEvent?.({ type: 'stage_done', stage: 'reviewer' });
228
- sessionMessages.push(new AIMessage(result));
229
- saveSession(sessionMessages);
230
- return result;
231
- }
232
- if (decision.domain === 'debugging') {
233
- emit('agent', { name: 'debugger' });
234
- onStageEvent?.({ type: 'stage_start', stage: 'debugger' });
235
- const result = await runDebugger(cleanedInput, (name, args) => {
236
- onStageEvent?.({ type: 'tool', stage: 'debugger', tool: name, target: extractTarget(args) });
245
+ const EXECUTOR_DOMAINS = new Set([
246
+ 'coding', 'debugging', 'refactoring', 'git', 'planning', 'review'
247
+ ]);
248
+ if (EXECUTOR_DOMAINS.has(decision.domain)) {
249
+ emit('agent', { name: `executor:${decision.domain}` });
250
+ onStageEvent?.({ type: 'stage_start', stage: decision.domain });
251
+ const result = await runExecutor(finalInput, decision.domain, decision.recommendedModel, (name, args) => {
252
+ onStageEvent?.({ type: 'tool', stage: decision.domain, tool: name, target: extractTarget(args) });
237
253
  onToolCall(name, args);
238
- }, decision.recommendedModel);
239
- onStageEvent?.({ type: 'stage_done', stage: 'debugger' });
254
+ }, onStatus, trimHistory(sessionMessages));
255
+ onStageEvent?.({ type: 'stage_done', stage: decision.domain });
240
256
  sessionMessages.push(new AIMessage(result));
241
257
  saveSession(sessionMessages);
258
+ emit('response', { text: result });
259
+ emit('done', { total_tokens: 0 }); // no token counting in executor path yet
242
260
  return result;
243
261
  }
244
- if (decision.domain === 'refactoring') {
245
- emit('agent', { name: 'refactorer' });
246
- onStageEvent?.({ type: 'stage_start', stage: 'refactorer' });
247
- const result = await runRefactorer(cleanedInput, (name, args) => {
248
- onStageEvent?.({ type: 'tool', stage: 'refactorer', tool: name, target: extractTarget(args) });
249
- onToolCall(name, args);
250
- }, decision.recommendedModel);
251
- onStageEvent?.({ type: 'stage_done', stage: 'refactorer' });
252
- sessionMessages.push(new AIMessage(result));
253
- saveSession(sessionMessages);
254
- return result;
255
- }
256
- if (decision.domain === 'git') {
257
- emit('agent', { name: 'git' });
258
- onStageEvent?.({ type: 'stage_start', stage: 'git_agent' });
259
- const result = await runGitAgent(cleanedInput, (name, args) => {
260
- onStageEvent?.({ type: 'tool', stage: 'git_agent', tool: name, target: extractTarget(args) });
261
- onToolCall(name, args);
262
- }, decision.recommendedModel);
263
- onStageEvent?.({ type: 'stage_done', stage: 'git_agent' });
264
- sessionMessages.push(new AIMessage(result));
265
- saveSession(sessionMessages);
266
- return result;
267
- }
268
- if (decision.domain === 'coding') {
269
- emit('agent', { name: 'coder' });
270
- const graphResult = await runAgentGraph(cleanedInput, buildSystemPrompt(), onToolCall, onStatus ?? (() => { }), decision, onStageEvent // ← add this
271
- );
272
- if (graphResult.escalated && onEscalation) {
273
- onEscalation({
274
- userMessage: graphResult.userMessage,
275
- plan: graphResult.plan,
276
- trajectory: graphResult.trajectory,
277
- finalOutput: graphResult.finalOutput ?? '',
278
- });
279
- }
280
- if (graphResult.finalOutput) {
281
- sessionMessages.push(new AIMessage(graphResult.finalOutput));
282
- saveSession(sessionMessages);
283
- return graphResult.finalOutput;
284
- }
285
- }
286
262
  emit('agent', { name: 'chat' });
263
+ const chatTools = decision.domain === 'chat' ? [] : chatFallbackTools;
287
264
  const simpleAgent = createReactAgent({
288
265
  llm: createLlm(decision.recommendedModel),
289
- tools,
266
+ tools: chatTools,
290
267
  stateModifier: new SystemMessage(systemPrompt)
291
268
  });
292
269
  const trimmed = trimHistory(sessionMessages);
270
+ emit('system_prompt', { agent: 'chat', text: systemPrompt.slice(0, 600) });
293
271
  const eventStrem = simpleAgent.streamEvents({ messages: trimmed }, { version: 'v2' });
294
272
  let finalResponse = '';
295
273
  let totalInputTokens = 0;
296
274
  let totalOutputTokens = 0;
275
+ let resolvedModel = null;
297
276
  for await (const { event, data, name: eventName } of eventStrem) {
298
277
  if (event === 'on_chat_model_stream') {
299
278
  const chunk = data.chunk;
@@ -308,27 +287,49 @@ export async function chat(userInput, onToolCall, onStatus, onToken, onEscalatio
308
287
  onToken?.(token);
309
288
  finalResponse += token;
310
289
  }
311
- // const token = data.chunk?.content;
312
- // if(token && typeof token === 'string'){
313
- // onToken?.(token);
314
- // finalResponse += token;
315
- // }
316
290
  }
317
291
  if (event === 'on_tool_start') {
318
292
  onToolCall(eventName, data.input);
319
293
  emit('tool_call', { name: eventName, input: data.input });
320
294
  }
295
+ if (event === 'on_tool_end') {
296
+ const out = typeof data.output === 'string'
297
+ ? data.output
298
+ : JSON.stringify(data.output ?? '');
299
+ emit('tool_response', { name: eventName, output: out.slice(0, 1000) });
300
+ }
321
301
  if (event === 'on_chat_model_end') {
322
302
  const usage = data.output?.usage_metadata;
303
+ // Prefer the X-Glitool-Resolved-Model header (captured via fetch wrapper).
304
+ // Falls back to LangChain's response_metadata.model_name, which on
305
+ // ChatOpenAI reports the REQUEST hint (the role), not the resolved name.
306
+ const fromHeader = getResolvedModelForCurrentRequest();
307
+ const fromMeta = data.output?.response_metadata?.model_name;
308
+ if (fromHeader)
309
+ resolvedModel = fromHeader;
310
+ else if (fromMeta)
311
+ resolvedModel = fromMeta;
323
312
  if (usage) {
324
313
  totalInputTokens += usage.input_tokens ?? 0;
325
314
  totalOutputTokens += usage.output_tokens ?? 0;
326
- emit('llm_call', { tokens_in: usage.input_tokens ?? 0, tokens_out: usage.output_tokens ?? 0 });
315
+ emit('llm_call', {
316
+ tokens_in: usage.input_tokens ?? 0,
317
+ tokens_out: usage.output_tokens ?? 0,
318
+ model: resolvedModel ?? decision.recommendedModel,
319
+ });
320
+ }
321
+ const output = data.output;
322
+ let msgText = '';
323
+ if (typeof output?.content === 'string') {
324
+ msgText = output.content;
325
+ }
326
+ else if (Array.isArray(output?.content)) {
327
+ msgText = output.content.filter((c) => c.type === 'text').map((c) => c.text ?? '').join('');
327
328
  }
328
- if (!finalResponse) {
329
- const output = data.output;
330
- if (typeof output?.content === 'string') {
331
- finalResponse = output.content;
329
+ if (msgText) {
330
+ emit('llm_message', { text: msgText.slice(0, 800) });
331
+ if (!finalResponse) {
332
+ finalResponse = msgText;
332
333
  onToken?.(finalResponse);
333
334
  }
334
335
  }
@@ -338,7 +339,7 @@ export async function chat(userInput, onToolCall, onStatus, onToken, onEscalatio
338
339
  sessionMessages.push(new AIMessage(finalResponse));
339
340
  }
340
341
  if (onUsage && (totalInputTokens + totalOutputTokens) > 0) {
341
- const model = decision.recommendedModel;
342
+ const model = resolvedModel ?? decision.recommendedModel;
342
343
  onUsage(totalInputTokens + totalOutputTokens, estimateCost(model, totalInputTokens, totalOutputTokens));
343
344
  }
344
345
  saveSession(sessionMessages);
@@ -2,30 +2,39 @@ import { createReactAgent } from "@langchain/langgraph/prebuilt";
2
2
  import { makeLlm } from '../llm/factory.js';
3
3
  import { SystemMessage, HumanMessage, BaseMessage } from "@langchain/core/messages";
4
4
  import { StructuredTool } from "@langchain/core/tools";
5
- import { listFilesTool, readFileTool, searchCodeTool, editFileTool, writeFileTool, bashTool } from '../tools/index.js';
5
+ import { listFilesTool, readFileTool, searchCodeTool, editFileTool, writeFileTool, bashTool, readBackgroundOutputTool } from '../tools/index.js';
6
6
  import { scoreRisk, getRiskMessage } from "../trust/riskScorer.js";
7
7
  import { log } from "../logger.js";
8
+ import { emit } from '../monitor.js';
8
9
  export async function runCoder(plan, userMessage, onToolCall, model, onReasoning) {
9
10
  const coderLlm = makeLlm(model);
11
+ emit('system_prompt', { agent: 'coder', text: plan.slice(0, 300) });
12
+ emit('enhanced_prompt', { text: userMessage.slice(0, 600) });
10
13
  const coderAgent = createReactAgent({
11
14
  llm: coderLlm,
12
- tools: [listFilesTool, readFileTool, searchCodeTool, editFileTool, writeFileTool, bashTool],
15
+ tools: [listFilesTool, readFileTool, searchCodeTool, editFileTool, writeFileTool, bashTool, readBackgroundOutputTool],
13
16
  stateModifier: new SystemMessage(`You are a coding execution agent. Execute the given plan step by step using tools.
14
17
 
18
+ Available tools: listFiles, readFile, searchCode, editFile, writeFile, bash, read_background_output. These are the ONLY tools that exist — do not call any other tool name (no "runShell", "exec", "shell", etc.).
19
+
15
20
  GROUNDING RULES — these are not optional:
16
21
 
22
+
17
23
  1. BEFORE editing any file, READ it first with readFile to confirm structure.
18
24
  2. PREFER searchCode over readFile for navigation. Read whole files only when you'll actually edit them.
19
25
  3. For UI features (slash commands, menus, palettes), search src/ui/, src/components/, src/cli/ first — don't trust the plan's filename blindly.
20
26
  4. After every editFile, if the tool returned an error, STOP and read the file again. Do not retry with guesses.
21
- 5. You MAY create package.json or tsconfig.json when building a new project from scratch. Never add dependencies to an EXISTING package.json unless explicitly asked. Never run npm install via bash.
22
- 6. Maximum 5 file reads per task. If you need more, you're doing it wrong use searchCode instead.
23
- 7. If you can't safely complete the task, STOP and return a failure message. Do not invent.
27
+ 5. When building a new project from scratch you MAY create package.json/tsconfig.json. Never add dependencies to an EXISTING package.json unless explicitly asked.
28
+ 6. Shell commands MUST be non-interactive you have no keyboard, so any command that opens a prompt/wizard will hang and be killed. For scaffolders, pass flags that skip prompts and use defaults: e.g. "npx create-next-app@latest my-app --yes". Give scaffold/install commands a generous timeout (120000) since they download packages.
29
+ 7. When a scaffolder creates a new subdirectory (e.g. "create-next-app my-app" makes ./my-app), ALL subsequent bash commands for that project MUST set cwd to that subdirectory: bash({ command: "npm install", cwd: "my-app" }). Otherwise "npm run dev"/"npm install" run in the parent and fail with "Missing script" or ENOENT.
30
+ 8. Maximum 5 file reads per task. If you need more, you're doing it wrong — use searchCode instead.
31
+ 9. If you can't safely complete the task, STOP and return a failure message. Do not invent.
24
32
 
25
33
  Be surgical, not exhaustive. Most tasks need 2-4 tool calls, not 15. The validator will catch broken output — you don't need to over-verify.
26
34
 
27
35
  Response style:
28
- - Your final text should be 1-3 sentences summarizing what files you changed and why.
36
+ - REPORT OUTCOMES FAITHFULLY. If a tool returned an error or you could not verify a step worked, say so explicitly. Never claim success for steps that failed, were killed, or you could not confirm. "I tried X but the tool returned <error>" is correct. "X is complete" when you have no proof is a LIE — do not do this.
37
+ - Your final text should be 1-3 sentences summarizing what files you actually changed and why.
29
38
  - Do NOT paste file contents in the response — the files are on disk; the user can read them.
30
39
  - The validator runs tsc + ESLint after you finish — no need to verify those yourself.
31
40
  - If a step is impossible (binary file, command blocked, etc.), say so explicitly and stop.
@@ -37,9 +46,14 @@ Response style:
37
46
  for await (const chunk of stream) {
38
47
  if (blocked)
39
48
  break;
40
- // 'updates' mode gives one complete message per graph step.
41
- // Agent node = LLM output (reasoning or tool call decision).
42
- // Tools node = tool results — no useful trace info, skip.
49
+ // Tool results node
50
+ const toolMsgs = chunk.tools?.messages;
51
+ if (toolMsgs?.length) {
52
+ for (const msg of toolMsgs) {
53
+ const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content ?? '');
54
+ emit('tool_response', { name: msg.name ?? 'tool', output: content.slice(0, 1000) });
55
+ }
56
+ }
43
57
  const agentMsgs = chunk.agent?.messages;
44
58
  if (!agentMsgs?.length) {
45
59
  log('coder:chunk', { keys: Object.keys(chunk).join(',') });
@@ -56,15 +70,18 @@ Response style:
56
70
  getRiskMessage(toolCall.name, risk, toolCall.args);
57
71
  if (risk === 'high') {
58
72
  onToolCall(toolCall.name, toolCall.args);
73
+ emit('tool_call', { name: toolCall.name, input: toolCall.args });
59
74
  result = `Blocked: I cannot write to sensitive files like ${toolCall.args?.filePath}.`;
60
75
  blocked = true;
61
76
  break;
62
77
  }
63
78
  onToolCall(toolCall.name, toolCall.args);
79
+ emit('tool_call', { name: toolCall.name, input: toolCall.args });
64
80
  }
65
81
  else if (text) {
66
82
  onReasoning?.(text);
67
83
  result = text;
84
+ emit('llm_message', { text: text.slice(0, 800) });
68
85
  }
69
86
  }
70
87
  log('coder:chunk', { keys: Object.keys(chunk).join(',') });
@@ -1,6 +1,7 @@
1
1
  import { makeLlm } from '../llm/factory.js';
2
2
  import { createReactAgent } from '@langchain/langgraph/prebuilt';
3
3
  import { SystemMessage, HumanMessage } from '@langchain/core/messages';
4
+ import { emit } from '../monitor.js';
4
5
  import { listFilesTool, readFileTool, searchCodeTool, bashTool, editFileTool, } from '../tools/index.js';
5
6
  const DEBUG_SYSTEM_PROMPT = `You are a debugging agent. You investigate first, then patch.
6
7
 
@@ -83,6 +84,8 @@ export async function runDebugger(userMessage, onToolCall, model) {
83
84
  bashTool,
84
85
  editFileTool,
85
86
  ];
87
+ emit('system_prompt', { agent: 'debugger', text: DEBUG_SYSTEM_PROMPT.slice(0, 600) });
88
+ emit('enhanced_prompt', { text: userMessage.slice(0, 600) });
86
89
  const agent = createReactAgent({
87
90
  llm,
88
91
  tools,
@@ -93,17 +96,26 @@ export async function runDebugger(userMessage, onToolCall, model) {
93
96
  for await (const { event, data, name: eventName } of stream) {
94
97
  if (event === 'on_tool_start') {
95
98
  onToolCall(eventName, data.input);
99
+ emit('tool_call', { name: eventName, input: data.input });
100
+ }
101
+ if (event === 'on_tool_end') {
102
+ const out = typeof data.output === 'string'
103
+ ? data.output
104
+ : JSON.stringify(data.output ?? '');
105
+ emit('tool_response', { name: eventName, output: out.slice(0, 1000) });
96
106
  }
97
107
  if (event === 'on_chat_model_end') {
98
108
  const output = data.output;
109
+ let content = '';
99
110
  if (typeof output?.content === 'string') {
100
- finalText = output.content;
111
+ content = output.content;
101
112
  }
102
113
  else if (Array.isArray(output?.content)) {
103
- finalText = output.content
104
- .filter((c) => c.type === 'text')
105
- .map((c) => c.text ?? '')
106
- .join('');
114
+ content = output.content.filter((c) => c.type === 'text').map((c) => c.text ?? '').join('');
115
+ }
116
+ if (content) {
117
+ finalText = content;
118
+ emit('llm_message', { text: content.slice(0, 800) });
107
119
  }
108
120
  }
109
121
  }
@@ -0,0 +1,144 @@
1
+ import { createReactAgent } from '@langchain/langgraph/prebuilt';
2
+ import { makeLlm } from '../llm/factory.js';
3
+ import { SystemMessage, HumanMessage, BaseMessage } from '@langchain/core/messages';
4
+ import { emit } from '../monitor.js';
5
+ import { listFilesTool, readFileTool, searchCodeTool, editFileTool, writeFileTool, bashTool, readBackgroundOutputTool, webFetchTool, askUserTool, } from '../tools/index.js';
6
+ const DOMAIN_RULES = {
7
+ debugging: `
8
+ ## Debugging Rules
9
+ WORKFLOW — follow in order:
10
+ 1. REPRODUCE — run the failing command or read the file mentioned. Start immediately, do not ask first.
11
+ 2. DIAGNOSE — before touching anything, output:
12
+ ## Diagnosis
13
+ - Root cause: one sentence
14
+ - Where: file.ts:LINE
15
+ - Why it fails: short explanation
16
+ 3. PATCH — one minimal editFile. Smallest change that fixes the issue.
17
+ 4. VERIFY — re-run the exact command from step 1.
18
+
19
+ STOP CONDITIONS (after you have started investigating):
20
+ - Same tool called twice with identical input → stop and report
21
+ - editFile failed twice for the same file → read the file fresh, try once more or give up
22
+ - 10 tool calls used → wrap up with what you have
23
+
24
+ Final response format:
25
+ ## Diagnosis
26
+ ...
27
+ ## Fix
28
+ - file.ts:LINE — what changed
29
+ ## Verification
30
+ - ran: \`<command>\`
31
+ - result: pass | fail | not verified — [reason]`,
32
+ coding: `
33
+ ## Coding Rules
34
+ 1. Read package.json first — understand the project type and available scripts
35
+ 2. If you scaffold a new project into a subdirectory, ALL subsequent commands must use cwd pointing to that subdirectory
36
+ 3. BEFORE editing any file, READ it first to confirm its structure
37
+ 4. Do not add dependencies, helpers, or abstractions beyond what was explicitly asked
38
+ 5. Shell commands must be non-interactive — use --yes / --defaults flags for scaffolders
39
+ 6. After writing files, run the appropriate check (npx tsc --noEmit, npm run build)
40
+ 7. Maximum 5 file reads — use searchCode for navigation instead
41
+ 8. Run shell commands ONE AT A TIME — never call bash multiple times in the same step. Wait for each command's output before running the next.
42
+ 9. To scaffold Next.js: use \`npx create-next-app@latest <name> --yes\`. Do NOT use --use-app-dir (deprecated in Next.js 14+). The project name must be the first argument before any flags.
43
+ 10. For files over ~120 lines, write a SKELETON first with writeFile (just structure + imports + section headers as comments), then add each section with editFile. Never write a 200+ line file in one writeFile call — long generations get truncated mid-string and produce invalid syntax.`,
44
+ refactoring: `
45
+ ## Refactoring Rules
46
+ 1. Read ALL files you plan to touch before changing any of them
47
+ 2. Make changes one file at a time, run tsc after each
48
+ 3. Do not restructure, rename, or clean up anything beyond the stated scope
49
+ 4. Preserve all existing behaviour — only change what was asked`,
50
+ git: `
51
+ ## Git Rules
52
+ 1. Run git status first — understand current state before acting
53
+ 2. Confirm the branch name before any commit or push
54
+ 3. Stage specific files by name — never use git add .
55
+ 4. Never force push unless explicitly asked
56
+ 5. Show the user a diff summary before committing`,
57
+ planning: `
58
+ ## Planning Rules
59
+ Think through the full approach before writing any code.
60
+ Output a clear numbered plan first, then execute it yourself immediately after.
61
+ Do not stop after the plan — carry it out.
62
+ If a step fails, adapt — do not blindly continue to the next step.`,
63
+ review: `
64
+ ## Review Rules
65
+ Read all changed or relevant files. Report findings as:
66
+ - BUG: file.ts:LINE — what the issue is and why it breaks
67
+ - SECURITY: file.ts:LINE — what the vulnerability is
68
+ - COMPLEXITY: file.ts:LINE — what could be simplified and how
69
+ - MISSING: what edge case or error path is unhandled
70
+ Be specific. Do not report style preferences. Only report things that cause real problems.`,
71
+ };
72
+ const BASE_PROMPT = `You are Glitool's execution agent. You solve coding tasks directly using tools.
73
+
74
+ CORE RULES — not optional:
75
+ 1. READ BEFORE WRITING — always read a file before editing it
76
+ 2. SEARCH BEFORE READING — use searchCode to locate symbols, read only when you'll edit
77
+ 3. VERIFY HONESTLY — if verification failed or wasn't run, say so explicitly. Never claim success without evidence
78
+ 4. NO SCOPE CREEP — do not add abstractions, helpers, or cleanup beyond what was asked
79
+ 5. DIAGNOSE BEFORE SWITCHING — if a tool fails, understand why before trying something different
80
+ 6. NON-INTERACTIVE SHELL — commands that open prompts will hang. Use --yes flags for scaffolders
81
+ 7. CWD AWARENESS — if you scaffold a project into a subdirectory, use cwd for all subsequent commands
82
+ 8. STOP ON REPEATED FAILURE — if the same tool call fails twice with the same input, stop and report
83
+ 9. ACT, DON'T NARRATE — every message you emit MUST either (a) include a tool call, or (b) be your FINAL response after the task is genuinely complete. Mid-task messages with text only and no tool call ("Now let me set up the pages next…", "I'll create the components now…", "Portfolio created! Now let me…") will end the agent IMMEDIATELY — that text becomes the user's final answer and any planned next steps are lost. If you have more steps to do, just do them — call the next tool. Save commentary for ONE final message AFTER the last tool call.
84
+
85
+ TOOLS AVAILABLE: listFiles, readFile, searchCode, editFile, writeFile, bash, webFetch
86
+ Use bash for: running commands, checking output, starting servers
87
+ Use editFile for: modifying existing files (requires oldString + newString)
88
+ Use writeFile for: creating new files only
89
+
90
+ RESPONSE FORMAT:
91
+ - 2-4 sentences: what you changed, what command you ran to verify, whether it passed
92
+ - Do NOT paste file contents in your response
93
+ - If something failed, say exactly what failed and why`;
94
+ export async function runExecutor(userMessage, domain, model, onToolCall, onStatus, history = []) {
95
+ const domainRules = DOMAIN_RULES[domain] ?? '';
96
+ const systemPrompt = domainRules
97
+ ? `${BASE_PROMPT}\n\n${domainRules}`
98
+ : BASE_PROMPT;
99
+ emit('system_prompt', { agent: `executor:${domain}`, text: systemPrompt.slice(0, 600) });
100
+ emit('enhanced_prompt', { text: userMessage.slice(0, 600) });
101
+ onStatus?.(`${domain.charAt(0).toUpperCase() + domain.slice(1)}...`);
102
+ const llm = makeLlm(model);
103
+ const agent = createReactAgent({
104
+ llm,
105
+ tools: [
106
+ listFilesTool, readFileTool, searchCodeTool,
107
+ editFileTool, writeFileTool, bashTool,
108
+ readBackgroundOutputTool, webFetchTool, askUserTool,
109
+ ],
110
+ stateModifier: new SystemMessage(systemPrompt),
111
+ });
112
+ const stream = agent.streamEvents({ messages: [...history, new HumanMessage(userMessage)] }, { version: 'v2', recursionLimit: 50 });
113
+ let finalText = '';
114
+ for await (const { event, data, name: eventName } of stream) {
115
+ if (event === 'on_tool_start') {
116
+ onToolCall(eventName, data.input);
117
+ emit('tool_call', { name: eventName, input: data.input });
118
+ }
119
+ if (event === 'on_tool_end') {
120
+ const raw = data.output;
121
+ const out = typeof raw === 'string'
122
+ ? raw
123
+ : (raw?.content ?? JSON.stringify(raw ?? ''));
124
+ emit('tool_response', { name: eventName, output: String(out).slice(0, 1000) });
125
+ }
126
+ if (event === 'on_chat_model_end') {
127
+ const output = data.output;
128
+ let content = '';
129
+ if (typeof output?.content === 'string')
130
+ content = output.content;
131
+ else if (Array.isArray(output?.content)) {
132
+ content = output.content
133
+ .filter((c) => c.type === 'text')
134
+ .map((c) => c.text ?? '')
135
+ .join('');
136
+ }
137
+ if (content) {
138
+ finalText = content;
139
+ emit('llm_message', { text: content.slice(0, 800) });
140
+ }
141
+ }
142
+ }
143
+ return finalText || 'No output.';
144
+ }