jiva-core 0.3.43 → 0.3.44-dev.72622a6

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 (35) hide show
  1. package/README.md +37 -5
  2. package/dist/code/agent.d.ts +16 -0
  3. package/dist/code/agent.d.ts.map +1 -1
  4. package/dist/code/agent.js +317 -186
  5. package/dist/code/agent.js.map +1 -1
  6. package/dist/core/config.d.ts +30 -16
  7. package/dist/core/config.d.ts.map +1 -1
  8. package/dist/core/config.js +2 -0
  9. package/dist/core/config.js.map +1 -1
  10. package/dist/core/worker-agent.d.ts +0 -8
  11. package/dist/core/worker-agent.d.ts.map +1 -1
  12. package/dist/core/worker-agent.js +359 -228
  13. package/dist/core/worker-agent.js.map +1 -1
  14. package/dist/core/workspace.d.ts.map +1 -1
  15. package/dist/core/workspace.js +2 -0
  16. package/dist/core/workspace.js.map +1 -1
  17. package/dist/interfaces/cli/index.js +33 -3
  18. package/dist/interfaces/cli/index.js.map +1 -1
  19. package/dist/interfaces/cli/repl.d.ts +6 -0
  20. package/dist/interfaces/cli/repl.d.ts.map +1 -1
  21. package/dist/interfaces/cli/repl.js +45 -3
  22. package/dist/interfaces/cli/repl.js.map +1 -1
  23. package/dist/interfaces/cli/setup-wizard.js +12 -12
  24. package/dist/interfaces/cli/setup-wizard.js.map +1 -1
  25. package/dist/models/model-client.d.ts.map +1 -1
  26. package/dist/models/model-client.js +19 -3
  27. package/dist/models/model-client.js.map +1 -1
  28. package/dist/personas/persona-manager.d.ts +14 -2
  29. package/dist/personas/persona-manager.d.ts.map +1 -1
  30. package/dist/personas/persona-manager.js +45 -14
  31. package/dist/personas/persona-manager.js.map +1 -1
  32. package/dist/storage/local-provider.d.ts.map +1 -1
  33. package/dist/storage/local-provider.js +2 -0
  34. package/dist/storage/local-provider.js.map +1 -1
  35. package/package.json +1 -1
@@ -60,7 +60,7 @@ const CODE_MODE_INDICATOR = '[CODE MODE]';
60
60
  // Default token threshold for in-loop compaction (90K leaves ~38K headroom in a 128K model)
61
61
  const DEFAULT_COMPACTION_THRESHOLD = 90_000;
62
62
  /** System prompt for code mode — focused on precision and persistence (ported from opencode beast.txt) */
63
- const getSystemPrompt = (workspaceDir, directive) => {
63
+ const _getSystemPromptBase = (workspaceDir, directive, skillsBlock, mcpToolNames) => {
64
64
  const base = `You are a precise, highly capable coding assistant operating in code mode.
65
65
  You have direct access to code tools — use them to explore, understand, and modify code.
66
66
 
@@ -76,6 +76,7 @@ PERSISTENCE AND COMPLETION:
76
76
  - Always tell the user what you are going to do before making a tool call with a single concise sentence.
77
77
  - If the user says "resume", "continue", or "try again", check the conversation history to find the last incomplete step and continue from there.
78
78
  - Verify your changes are correct — run tests or the build after making changes when appropriate.
79
+ - If a tool call fails or returns an error, DO NOT STOP. Analyse the error, try a different approach or arguments, and keep going. Errors are expected — your job is to recover and complete the task.
79
80
 
80
81
  TOOLS AVAILABLE:
81
82
  - read_file: Read an existing file or list a directory. Only needed before editing an existing file.
@@ -94,11 +95,19 @@ CODING PRINCIPLES — READ CAREFULLY:
94
95
  5. After editing, check LSP errors in the tool result and fix them.
95
96
  6. Verify your changes work by running tests or the build when appropriate.
96
97
  7. Use bash only for shell commands (tests, builds, git) — NOT for reading files.
97
- 8. LARGE FILES (100+ lines): write in stages never try to generate a complete large file in one call.
98
- - Stage 1: write_file with the skeleton/structure only (HTML tags, empty <style>, empty <script>)
99
- - Stage 2: edit_file to add CSS content into the <style> block
100
- - Stage 3: edit_file to add JS content into the <script> block
101
- - Each individual call must be short enough to fit in one model response.
98
+ 8. LARGE FILES ALWAYS WORK IN SMALL CHUNKS (applies to ALL file types: TS, Python, HTML, CSS, JSON…):
99
+ "Large" means any file or edit whose total new content exceeds ~80 lines.
100
+ BEFORE writing or editing: mentally estimate the line count. If > 80 lines, apply the rules below.
101
+ EDITING large files:
102
+ - Break every edit into chunks of 50–80 lines maximum.
103
+ - Never pass a new_string longer than ~80 lines to edit_file.
104
+ - Split large edits into multiple sequential edit_file calls, one section at a time.
105
+ CREATING new large files (skeleton-first approach — mandatory for any file > 80 lines):
106
+ - Stage 1: write_file with a skeleton/scaffold only — class/function stubs, empty bodies,
107
+ placeholder comments like "// TODO: implement X". Keep the skeleton under 60 lines.
108
+ - Stage 2+: edit_file to replace each placeholder/stub with the real implementation,
109
+ 50–80 lines per call. Never implement more than one function or section per call.
110
+ - Reason: model output longer than ~100 lines gets truncated mid-JSON, silently corrupting the file.
102
111
 
103
112
  TOOL SELECTION RULES (follow exactly):
104
113
  - To CREATE a new file → write_file immediately (no reads needed first)
@@ -113,15 +122,61 @@ WHEN TO EXPLORE (only when actually needed):
113
122
  - You need to find where a function, class, or variable is defined.
114
123
  - You are debugging or tracing code through multiple files.
115
124
  - Do NOT explore before creating brand-new files — just write them directly.`;
116
- if (directive) {
117
- return `${base}\n\n${directive}`;
125
+ const parts = [base];
126
+ if (mcpToolNames && mcpToolNames.length > 0) {
127
+ parts.push(`MCP TOOLS (external servers — call these like any other tool):\n` +
128
+ mcpToolNames.map((n) => `- ${n}`).join('\n') + '\n\n' +
129
+ `Use MCP tools when the built-in tools cannot satisfy the request (e.g. browser automation, database queries). ` +
130
+ `Prefer built-in tools for all file and shell operations.`);
118
131
  }
119
- return base;
132
+ if (skillsBlock)
133
+ parts.push(skillsBlock);
134
+ if (directive)
135
+ parts.push(directive);
136
+ return parts.join('\n\n');
120
137
  };
138
+ /**
139
+ * Wrap selected MCP server tools as ICodeTool adapters so CodeAgent can call them
140
+ * using the same dispatch path as built-in tools.
141
+ * Only servers listed in `serverNames` are exposed — keeps context lean.
142
+ */
143
+ function buildMCPAdapters(mcpManager, serverNames) {
144
+ if (!mcpManager || serverNames.length === 0)
145
+ return [];
146
+ const adapters = [];
147
+ const client = mcpManager.getClient();
148
+ for (const serverName of serverNames) {
149
+ const serverTools = client.getServerTools(serverName);
150
+ for (const tool of serverTools) {
151
+ // tool.name is already prefixed as "serverName__toolName" by MCPClient
152
+ adapters.push({
153
+ name: tool.name,
154
+ description: tool.description,
155
+ parameters: tool.parameters,
156
+ async execute(args) {
157
+ try {
158
+ const result = await client.executeTool(tool.name, args);
159
+ if (typeof result === 'string')
160
+ return result;
161
+ if (result && typeof result === 'object' && 'text' in result) {
162
+ return result.text;
163
+ }
164
+ return JSON.stringify(result, null, 2);
165
+ }
166
+ catch (e) {
167
+ return `Error: ${e instanceof Error ? e.message : String(e)}`;
168
+ }
169
+ },
170
+ });
171
+ }
172
+ }
173
+ return adapters;
174
+ }
121
175
  export class CodeAgent {
122
176
  orchestrator;
123
177
  workspace;
124
178
  conversationManager;
179
+ personaManager;
125
180
  maxIterations;
126
181
  compactionThreshold;
127
182
  lsp;
@@ -129,11 +184,14 @@ export class CodeAgent {
129
184
  maxDepth;
130
185
  history = [];
131
186
  tools;
187
+ _mcpManager;
188
+ _mcpServerNames = [];
132
189
  _stopped = false;
133
190
  constructor(config) {
134
191
  this.orchestrator = config.orchestrator;
135
192
  this.workspace = config.workspace;
136
193
  this.conversationManager = config.conversationManager;
194
+ this.personaManager = config.personaManager;
137
195
  this.maxIterations = config.maxIterations ?? DEFAULT_MAX_ITERATIONS;
138
196
  this.compactionThreshold = config.compactionThreshold ?? DEFAULT_COMPACTION_THRESHOLD;
139
197
  this.depth = config.depth ?? 0;
@@ -142,6 +200,8 @@ export class CodeAgent {
142
200
  root: config.workspace.getWorkspaceDir(),
143
201
  enabled: config.lspEnabled ?? true,
144
202
  });
203
+ this._mcpManager = config.mcpManager;
204
+ this._mcpServerNames = config.mcpServerNames ?? [];
145
205
  this.tools = [
146
206
  ReadFileTool,
147
207
  EditFileTool,
@@ -150,6 +210,7 @@ export class CodeAgent {
150
210
  GrepTool,
151
211
  BashTool,
152
212
  ...(this.depth < this.maxDepth ? [SpawnCodeAgentTool] : []),
213
+ ...buildMCPAdapters(config.mcpManager, config.mcpServerNames ?? []),
153
214
  ];
154
215
  }
155
216
  /**
@@ -269,7 +330,11 @@ ${directive ? `\n${directive}` : ''}`;
269
330
  async chat(userMessage, onChunk) {
270
331
  const toolsUsed = [];
271
332
  const directive = this.workspace.getDirectivePrompt();
272
- const systemPrompt = getSystemPrompt(this.workspace.getWorkspaceDir(), directive || undefined);
333
+ const skillsBlock = this.personaManager?.getSystemPromptAddition() || undefined;
334
+ const mcpToolNames = this._mcpManager
335
+ ? this.tools.filter((t) => t.name.includes('__')).map((t) => t.name)
336
+ : undefined;
337
+ const systemPrompt = _getSystemPromptBase(this.workspace.getWorkspaceDir(), directive || undefined, skillsBlock, mcpToolNames);
273
338
  // Build message history: system + history + new user message
274
339
  const messages = [
275
340
  { role: 'developer', content: systemPrompt },
@@ -446,16 +511,16 @@ ${directive ? `\n${directive}` : ''}`;
446
511
  logger.warn(`[CodeAgent] ${isEditFile ? 'edit_file' : 'write_file'}${targetFile} content truncated (exceeded output token limit) — asking model to write in stages`);
447
512
  const correctionContent = isEditFile
448
513
  ? `Your edit_file call${targetFile} failed: new_string was too large and the response was cut off mid-JSON.\n\n` +
449
- `MAXIMUM 20 LINES per edit_file call. Write one tiny chunk at a time:\n` +
450
- ` - For JavaScript: add just ONE or TWO functions per call\n` +
451
- ` - For HTML: add just one row of buttons per call\n\n` +
514
+ `MAXIMUM 20 LINES per edit_file call. Implement one function or section at a time:\n` +
515
+ ` - Split the change into smaller pieces and call edit_file once per piece.\n` +
516
+ ` - Never pass more than 20 lines as new_string.\n\n` +
452
517
  `Call edit_file now with a new_string of at most 20 lines.`
453
518
  : `Your write_file call${targetFile} failed: the file content was too large and was cut off mid-JSON.\n\n` +
454
- `Write the file in stages NEVER put CSS or JavaScript in the initial write_file:\n` +
455
- ` Stage 1: write_file — HTML skeleton ONLY (empty <style></style> and empty <script></script>) — MAX 20 lines\n` +
456
- ` Stage 2: edit_file — add CSS (max 20 lines at a time)\n` +
457
- ` Stage 3: edit_file add JavaScript ONE function at a time\n\n` +
458
- `Start with Stage 1 NOW: write_file with just the bare HTML skeleton (20 lines max).`;
519
+ `Use the skeleton-first approachwrite the file in stages:\n` +
520
+ ` Stage 1: write_file — skeleton/scaffold ONLY (stubs, empty function bodies, TODO placeholders) — MAX 20 lines\n` +
521
+ ` Stage 2+: edit_file — implement one function or section at a time (max 20 lines per call)\n` +
522
+ ` Never implement more than one major section per call.\n\n` +
523
+ `Start with Stage 1 NOW: write_file with just the bare skeleton (20 lines max).`;
459
524
  messages.push({ role: 'user', content: correctionContent });
460
525
  consecutiveToolCallErrors = 0;
461
526
  emptyResponseCount = 0; // reset so model has fresh recovery budget for staged CSS/JS edits
@@ -517,184 +582,229 @@ ${directive ? `\n${directive}` : ''}`;
517
582
  logger.warn(`[CodeAgent] API error ${consecutiveApiErrors}/${MAX_CONSECUTIVE_API_ERRORS}: ${msg}`);
518
583
  continue;
519
584
  }
520
- // Add assistant response to messages, preserving the full structure needed for the next turn.
521
- //
522
- // Three cases:
523
- // 1. Harmony mode: store rawHarmony string (contains <|call|> tokens the model needs).
524
- // 2. Standard tool-calling with tool calls: store content + tool_calls so subsequent
525
- // role:'tool' results can be matched by tool_call_id (required by OpenAI-compatible APIs).
526
- // 3. Text-only response: store content as-is.
527
- const rawHarmony = response.raw?.parsedHarmony?.rawResponse;
528
- if (rawHarmony) {
529
- messages.push({ role: 'assistant', content: rawHarmony });
530
- }
531
- else if (response.toolCalls && response.toolCalls.length > 0) {
532
- // Preserve tool_calls so tool results are properly matched in the next turn
533
- messages.push({
534
- role: 'assistant',
535
- content: response.content || null,
536
- tool_calls: response.toolCalls,
537
- });
538
- }
539
- else {
540
- messages.push({ role: 'assistant', content: response.content });
541
- }
542
- // Stream the visible (final-channel) text output if callback provided
543
- if (response.content && onChunk) {
544
- onChunk(response.content);
545
- }
546
- // ── In-loop context compaction ──────────────────────────────────────────
547
- // Check if we're approaching the context window limit and compact if needed.
548
- // Triggered once per turn (compactedThisTurn flag) to avoid thrashing.
549
- // Only runs when: threshold > 0, orchestrator available, not already compacted,
550
- // and the response carried token usage data.
551
- const promptTokens = response.usage?.promptTokens ?? 0;
552
- if (this.compactionThreshold > 0 &&
553
- promptTokens > this.compactionThreshold &&
554
- !compactedThisTurn &&
555
- this.conversationManager &&
556
- response.toolCalls && response.toolCalls.length > 0) {
557
- logger.warn(`[CodeAgent] Context at ${promptTokens} tokens (threshold: ${this.compactionThreshold}) — compacting in-loop history`);
558
- compactedThisTurn = true;
559
- try {
560
- const compacted = await this.compactInLoopMessages(messages, systemPrompt);
561
- // Replace messages in-place, preserving the system prompt at index 0
562
- messages.splice(0, messages.length, ...compacted);
563
- logger.info(`[CodeAgent] In-loop compaction complete: ${messages.length} messages after compaction`);
564
- }
565
- catch (compactErr) {
566
- // Non-fatal: log and continue with uncompacted messages
567
- logger.error('[CodeAgent] In-loop compaction failed, continuing without compaction', compactErr);
568
- }
569
- }
570
- // ────────────────────────────────────────────────────────────────────────
571
- // No tool calls → model is done (or gave up)
572
- if (!response.toolCalls || response.toolCalls.length === 0) {
573
- if (!response.content && emptyResponseCount < MAX_EMPTY_RESPONSES) {
574
- // Model returned empty content with no tool calls — it got stuck or confused.
575
- // Inject an escalating recovery nudge and let it try again.
576
- emptyResponseCount++;
577
- logger.warn(`[CodeAgent] Empty response with no tool calls (${emptyResponseCount}/${MAX_EMPTY_RESPONSES}) — injecting recovery nudge`);
578
- // Escalate urgency with each retry so the model doesn't keep ignoring it
579
- let nudgeContent;
580
- if (emptyResponseCount <= 2) {
581
- nudgeContent =
582
- 'Your last response was empty. Please continue working on the task.\n\n' +
583
- '- If you need to CREATE a file → call write_file now.\n' +
584
- '- If you need to EDIT a file → call read_file then edit_file.\n' +
585
- '- If the task is already complete → provide a brief summary of what was done.';
586
- }
587
- else {
588
- nudgeContent =
589
- `IMPORTANT (attempt ${emptyResponseCount}/${MAX_EMPTY_RESPONSES}): Your response is empty again — you have not called any tools.\n\n` +
590
- `You MUST call a tool NOW. Do not output plain text without a tool call.\n` +
591
- ` • To CREATE a new file → call write_file immediately with the file content.\n` +
592
- ` • To EDIT an existing file → call edit_file with old_string and new_string.\n` +
593
- ` • To LIST files → call read_file on the directory.\n\n` +
594
- `Make a tool call in your very next response. Do not explain — just call the tool.`;
595
- }
596
- messages.push({ role: 'user', content: nudgeContent });
597
- continue;
598
- }
599
- finalContent = response.content || '[No response content]';
600
- break;
601
- }
602
- // Execute tool calls
603
- for (const toolCall of response.toolCalls) {
604
- const toolName = toolCall.function.name;
605
- let toolArgs = {};
606
- try {
607
- toolArgs = JSON.parse(toolCall.function.arguments);
608
- }
609
- catch {
610
- // malformed args
611
- }
612
- // Consecutive reads tracker — any non-read tool resets the streak
613
- if (toolName === 'read_file') {
614
- consecutiveReadCount++;
615
- }
616
- else {
617
- consecutiveReadCount = 0;
585
+ // Wrap all post-API processing in a try/catch so an unexpected exception during tool
586
+ // execution or message construction doesn't crash the entire chat() call. Instead,
587
+ // inject the error as a user message and let the model recover on the next iteration.
588
+ let _postApiError = false;
589
+ try {
590
+ // Add assistant response to messages, preserving the full structure needed for the next turn.
591
+ //
592
+ // Three cases:
593
+ // 1. Harmony mode: store rawHarmony string (contains <|call|> tokens the model needs).
594
+ // 2. Standard tool-calling with tool calls: store content + tool_calls so subsequent
595
+ // role:'tool' results can be matched by tool_call_id (required by OpenAI-compatible APIs).
596
+ // 3. Text-only response: store content as-is.
597
+ const rawHarmony = response.raw?.parsedHarmony?.rawResponse;
598
+ if (rawHarmony) {
599
+ messages.push({ role: 'assistant', content: rawHarmony });
618
600
  }
619
- // Doom loop check
620
- const callSig = `${toolName}:${JSON.stringify(toolArgs)}`;
621
- recentCalls.push(callSig);
622
- if (recentCalls.length > DOOM_LOOP_THRESHOLD)
623
- recentCalls.shift();
624
- if (recentCalls.length === DOOM_LOOP_THRESHOLD &&
625
- recentCalls.every((c) => c === recentCalls[0])) {
626
- logger.warn(`[CodeAgent] Doom loop detected for tool: ${toolName}`);
601
+ else if (response.toolCalls && response.toolCalls.length > 0) {
602
+ // Preserve tool_calls so tool results are properly matched in the next turn
627
603
  messages.push({
628
- role: 'user',
629
- content: `STOP: You are calling \`${toolName}\` with the same arguments repeatedly. This action is not making progress. Stop and reassess your approach — try a different strategy or report what is blocking you.`,
604
+ role: 'assistant',
605
+ content: response.content || null,
606
+ tool_calls: response.toolCalls,
630
607
  });
631
- break;
632
- }
633
- logger.info(`[CodeAgent] Tool: ${toolName}`);
634
- toolsUsed.push(toolName);
635
- const tool = this.tools.find((t) => t.name === toolName);
636
- let toolResult;
637
- if (!tool) {
638
- toolResult = `Error: Unknown tool "${toolName}". Available tools: ${this.tools.map((t) => t.name).join(', ')}`;
639
608
  }
640
609
  else {
610
+ messages.push({ role: 'assistant', content: response.content });
611
+ }
612
+ // Stream the visible (final-channel) text output if callback provided
613
+ if (response.content && onChunk) {
614
+ onChunk(response.content);
615
+ }
616
+ // ── In-loop context compaction ──────────────────────────────────────────
617
+ // Check if we're approaching the context window limit and compact if needed.
618
+ // Triggered once per turn (compactedThisTurn flag) to avoid thrashing.
619
+ // Only runs when: threshold > 0, orchestrator available, not already compacted,
620
+ // and the response carried token usage data.
621
+ const promptTokens = response.usage?.promptTokens ?? 0;
622
+ if (this.compactionThreshold > 0 &&
623
+ promptTokens > this.compactionThreshold &&
624
+ !compactedThisTurn &&
625
+ this.conversationManager &&
626
+ response.toolCalls && response.toolCalls.length > 0) {
627
+ logger.warn(`[CodeAgent] Context at ${promptTokens} tokens (threshold: ${this.compactionThreshold}) — compacting in-loop history`);
628
+ compactedThisTurn = true;
641
629
  try {
642
- const ctx = {
643
- workspaceDir: this.workspace.getWorkspaceDir(),
644
- lsp: this.lsp,
645
- depth: this.depth,
646
- maxDepth: this.maxDepth,
647
- spawnChildAgent: this.depth < this.maxDepth
648
- ? async (task, context) => {
649
- const child = new CodeAgent({
650
- orchestrator: this.orchestrator,
651
- workspace: this.workspace,
652
- maxIterations: this.maxIterations,
653
- lspEnabled: true,
654
- depth: this.depth + 1,
655
- maxDepth: this.maxDepth,
656
- });
657
- // Share the parent's LSP manager so servers don't restart
658
- child.lsp = this.lsp;
659
- const taskMsg = context ? `${task}\n\nContext: ${context}` : task;
660
- const result = await child.chat(taskMsg);
661
- return result.content;
662
- }
663
- : undefined,
664
- };
665
- toolResult = await tool.execute(toolArgs, ctx);
630
+ const compacted = await this.compactInLoopMessages(messages, systemPrompt);
631
+ // Replace messages in-place, preserving the system prompt at index 0
632
+ messages.splice(0, messages.length, ...compacted);
633
+ logger.info(`[CodeAgent] In-loop compaction complete: ${messages.length} messages after compaction`);
666
634
  }
667
- catch (e) {
668
- toolResult = `Error executing ${toolName}: ${e instanceof Error ? e.message : String(e)}`;
635
+ catch (compactErr) {
636
+ // Non-fatal: log and continue with uncompacted messages
637
+ logger.error('[CodeAgent] In-loop compaction failed, continuing without compaction', compactErr);
669
638
  }
670
639
  }
671
- // Add tool result to messages (using Harmony format helper)
672
- const toolMessage = formatToolResult(toolCall.id, toolName, toolResult);
673
- messages.push(toolMessage);
674
- // Reset empty response budget after any productive (mutating) tool call.
675
- // This gives the model a fresh set of recovery nudges for each new work phase
676
- // (e.g., after writing the skeleton, it gets 4 more chances to add CSS/JS).
677
- if (toolName === 'write_file' || toolName === 'edit_file' || toolName === 'bash') {
678
- emptyResponseCount = 0;
640
+ // ────────────────────────────────────────────────────────────────────────
641
+ // No tool calls → model is done (or gave up)
642
+ if (!response.toolCalls || response.toolCalls.length === 0) {
643
+ // Detect a truncated XML tool call: the model started a <tool_call> block but the
644
+ // response was cut off before </tool_call> (token limit hit mid-content).
645
+ // The XML parser in parseHarmonyResponse requires the closing tag, so nothing was
646
+ // extracted but we can still detect the open tag and inject the staged-write correction.
647
+ const truncatedXml = response.content &&
648
+ response.content.includes('<tool_call>') &&
649
+ !response.content.includes('</tool_call>');
650
+ if (truncatedXml) {
651
+ const isEditFile = response.content.includes('edit_file') && !response.content.includes('write_file');
652
+ const fpMatch = response.content.match(/<arg_key>file_path<\/arg_key>\s*<arg_value>([^<]+)<\/arg_value>/);
653
+ const targetFile = fpMatch ? ` for \`${fpMatch[1]}\`` : '';
654
+ logger.warn(`[CodeAgent] Truncated XML tool call${targetFile} — injecting staged-writing correction`);
655
+ const correctionContent = isEditFile
656
+ ? `Your edit_file call${targetFile} failed: the response was cut off before the tool call completed.\n\n` +
657
+ `MAXIMUM 20 LINES per edit_file call. Implement one function or section at a time:\n` +
658
+ ` - Split the change into smaller pieces and call edit_file once per piece.\n` +
659
+ ` - Never pass more than 20 lines as new_string.\n\n` +
660
+ `Call edit_file now with a new_string of at most 20 lines.`
661
+ : `Your write_file call${targetFile} failed: the file content was too large and was cut off mid-response.\n\n` +
662
+ `Use the skeleton-first approach — write the file in stages:\n` +
663
+ ` Stage 1: write_file — skeleton/scaffold ONLY (stubs, empty function bodies, TODO placeholders) — MAX 20 lines\n` +
664
+ ` Stage 2+: edit_file — implement one function or section at a time (max 20 lines per call)\n` +
665
+ ` Never implement more than one major section per call.\n\n` +
666
+ `Start with Stage 1 NOW: write_file with just the bare skeleton (20 lines max).`;
667
+ messages.push({ role: 'user', content: correctionContent });
668
+ emptyResponseCount = 0;
669
+ continue;
670
+ }
671
+ if (!response.content && emptyResponseCount < MAX_EMPTY_RESPONSES) {
672
+ // Model returned empty content with no tool calls — it got stuck or confused.
673
+ // Inject an escalating recovery nudge and let it try again.
674
+ emptyResponseCount++;
675
+ logger.warn(`[CodeAgent] Empty response with no tool calls (${emptyResponseCount}/${MAX_EMPTY_RESPONSES}) — injecting recovery nudge`);
676
+ // Escalate urgency with each retry so the model doesn't keep ignoring it
677
+ let nudgeContent;
678
+ if (emptyResponseCount <= 2) {
679
+ nudgeContent =
680
+ 'Your last response was empty. Please continue working on the task.\n\n' +
681
+ '- If you need to CREATE a file → call write_file now.\n' +
682
+ '- If you need to EDIT a file → call read_file then edit_file.\n' +
683
+ '- If the task is already complete → provide a brief summary of what was done.';
684
+ }
685
+ else {
686
+ nudgeContent =
687
+ `IMPORTANT (attempt ${emptyResponseCount}/${MAX_EMPTY_RESPONSES}): Your response is empty again — you have not called any tools.\n\n` +
688
+ `You MUST call a tool NOW. Do not output plain text without a tool call.\n` +
689
+ ` • To CREATE a new file → call write_file immediately with the file content.\n` +
690
+ ` • To EDIT an existing file → call edit_file with old_string and new_string.\n` +
691
+ ` • To LIST files → call read_file on the directory.\n\n` +
692
+ `Make a tool call in your very next response. Do not explain — just call the tool.`;
693
+ }
694
+ messages.push({ role: 'user', content: nudgeContent });
695
+ continue;
696
+ }
697
+ finalContent = response.content || '[No response content]';
698
+ break;
679
699
  }
680
- // Excessive reads nudge — model is re-reading without making any changes.
681
- // Fires after MAX_CONSECUTIVE_READS consecutive read_file calls; resets the count
682
- // so the model gets a fresh budget after each nudge (prevents spamming).
683
- if (consecutiveReadCount >= MAX_CONSECUTIVE_READS) {
684
- logger.warn(`[CodeAgent] ${consecutiveReadCount} consecutive read_file calls without edits — nudging model to act`);
685
- consecutiveReadCount = 0;
686
- messages.push({
687
- role: 'user',
688
- content: `You have called read_file ${MAX_CONSECUTIVE_READS}+ times in a row without making any changes.\n\n` +
689
- `STOP READING — you have enough context. Make a concrete change NOW:\n` +
690
- ` • To ADD or CHANGE content in an existing file → call edit_file with old_string and new_string\n` +
691
- ` • To CREATE a new file → call write_file\n` +
692
- ` • If the task is fully complete → provide a final summary (no more tool calls needed)\n\n` +
693
- `Do NOT call read_file again until you have made at least one edit_file or write_file call.`,
694
- });
695
- break; // exit inner tool loop — model sees the nudge on the next outer iteration
700
+ // Execute tool calls
701
+ for (const toolCall of response.toolCalls) {
702
+ const toolName = toolCall.function.name;
703
+ let toolArgs = {};
704
+ try {
705
+ toolArgs = JSON.parse(toolCall.function.arguments);
706
+ }
707
+ catch {
708
+ // malformed args
709
+ }
710
+ // Consecutive reads tracker any non-read tool resets the streak
711
+ if (toolName === 'read_file') {
712
+ consecutiveReadCount++;
713
+ }
714
+ else {
715
+ consecutiveReadCount = 0;
716
+ }
717
+ // Doom loop check
718
+ const callSig = `${toolName}:${JSON.stringify(toolArgs)}`;
719
+ recentCalls.push(callSig);
720
+ if (recentCalls.length > DOOM_LOOP_THRESHOLD)
721
+ recentCalls.shift();
722
+ if (recentCalls.length === DOOM_LOOP_THRESHOLD &&
723
+ recentCalls.every((c) => c === recentCalls[0])) {
724
+ logger.warn(`[CodeAgent] Doom loop detected for tool: ${toolName}`);
725
+ messages.push({
726
+ role: 'user',
727
+ content: `STOP: You are calling \`${toolName}\` with the same arguments repeatedly. This action is not making progress. Stop and reassess your approach — try a different strategy or report what is blocking you.`,
728
+ });
729
+ break;
730
+ }
731
+ logger.info(`[CodeAgent] Tool: ${toolName}`);
732
+ toolsUsed.push(toolName);
733
+ const tool = this.tools.find((t) => t.name === toolName);
734
+ let toolResult;
735
+ if (!tool) {
736
+ toolResult = `Error: Unknown tool "${toolName}". Available tools: ${this.tools.map((t) => t.name).join(', ')}`;
737
+ }
738
+ else {
739
+ try {
740
+ const ctx = {
741
+ workspaceDir: this.workspace.getWorkspaceDir(),
742
+ lsp: this.lsp,
743
+ depth: this.depth,
744
+ maxDepth: this.maxDepth,
745
+ spawnChildAgent: this.depth < this.maxDepth
746
+ ? async (task, context) => {
747
+ const child = new CodeAgent({
748
+ orchestrator: this.orchestrator,
749
+ workspace: this.workspace,
750
+ maxIterations: this.maxIterations,
751
+ lspEnabled: true,
752
+ depth: this.depth + 1,
753
+ maxDepth: this.maxDepth,
754
+ });
755
+ // Share the parent's LSP manager so servers don't restart
756
+ child.lsp = this.lsp;
757
+ const taskMsg = context ? `${task}\n\nContext: ${context}` : task;
758
+ const result = await child.chat(taskMsg);
759
+ return result.content;
760
+ }
761
+ : undefined,
762
+ };
763
+ toolResult = await tool.execute(toolArgs, ctx);
764
+ }
765
+ catch (e) {
766
+ toolResult = `Error executing ${toolName}: ${e instanceof Error ? e.message : String(e)}`;
767
+ }
768
+ }
769
+ // Add tool result to messages (using Harmony format helper)
770
+ const toolMessage = formatToolResult(toolCall.id, toolName, toolResult);
771
+ messages.push(toolMessage);
772
+ // Reset empty response budget after any productive (mutating) tool call.
773
+ // This gives the model a fresh set of recovery nudges for each new work phase
774
+ // (e.g., after writing the skeleton, it gets 4 more chances to add CSS/JS).
775
+ if (toolName === 'write_file' || toolName === 'edit_file' || toolName === 'bash') {
776
+ emptyResponseCount = 0;
777
+ }
778
+ // Excessive reads nudge — model is re-reading without making any changes.
779
+ // Fires after MAX_CONSECUTIVE_READS consecutive read_file calls; resets the count
780
+ // so the model gets a fresh budget after each nudge (prevents spamming).
781
+ if (consecutiveReadCount >= MAX_CONSECUTIVE_READS) {
782
+ logger.warn(`[CodeAgent] ${consecutiveReadCount} consecutive read_file calls without edits — nudging model to act`);
783
+ consecutiveReadCount = 0;
784
+ messages.push({
785
+ role: 'user',
786
+ content: `You have called read_file ${MAX_CONSECUTIVE_READS}+ times in a row without making any changes.\n\n` +
787
+ `STOP READING — you have enough context. Make a concrete change NOW:\n` +
788
+ ` • To ADD or CHANGE content in an existing file → call edit_file with old_string and new_string\n` +
789
+ ` • To CREATE a new file → call write_file\n` +
790
+ ` • If the task is fully complete → provide a final summary (no more tool calls needed)\n\n` +
791
+ `Do NOT call read_file again until you have made at least one edit_file or write_file call.`,
792
+ });
793
+ break; // exit inner tool loop — model sees the nudge on the next outer iteration
794
+ }
696
795
  }
697
796
  }
797
+ catch (unexpectedError) {
798
+ _postApiError = true;
799
+ const errMsg = unexpectedError instanceof Error ? unexpectedError.message : String(unexpectedError);
800
+ logger.error(`[CodeAgent] Unexpected error during tool processing: ${errMsg}`);
801
+ messages.push({
802
+ role: 'user',
803
+ content: `An unexpected internal error occurred: "${errMsg}". Please try a different approach to continue the task.`,
804
+ });
805
+ }
806
+ if (_postApiError)
807
+ continue;
698
808
  }
699
809
  if (!finalContent) {
700
810
  finalContent = '[Max iterations reached without a final response]';
@@ -729,7 +839,7 @@ ${directive ? `\n${directive}` : ''}`;
729
839
  // Nothing meaningful to compact
730
840
  return messages;
731
841
  }
732
- const systemMsg = messages[0]; // developer/system prompt
842
+ const systemMsg = { role: 'developer', content: systemPrompt };
733
843
  const recentMessages = messages.slice(-KEEP_RECENT);
734
844
  const middleMessages = messages.slice(1, -KEEP_RECENT);
735
845
  if (middleMessages.length === 0)
@@ -823,7 +933,28 @@ Keep the summary concise but complete. Focus on what would help continue the wor
823
933
  return this.workspace;
824
934
  }
825
935
  getMCPManager() {
826
- // CodeAgent uses in-process tools, not MCP. Return a stub that reflects built-in tools.
936
+ if (this._mcpManager) {
937
+ const allowedNames = this._mcpServerNames;
938
+ const fullManager = this._mcpManager;
939
+ const builtinTools = this.tools.filter((t) => !t.name.includes('__'));
940
+ return {
941
+ getServerStatus: () => [
942
+ // Always report the built-in code tools as the first entry
943
+ { name: 'code-tools', connected: true, enabled: true, toolCount: builtinTools.length },
944
+ // Then the opted-in MCP servers only
945
+ ...fullManager.getServerStatus().filter((s) => allowedNames.includes(s.name)),
946
+ ],
947
+ getClient: () => ({
948
+ getAllTools: () => [
949
+ // Built-in code tools
950
+ ...builtinTools.map((t) => ({ name: t.name, description: t.description })),
951
+ // Opted-in MCP tools only
952
+ ...fullManager.getClient().getAllTools().filter((t) => allowedNames.some((n) => t.name.startsWith(`${n}__`))),
953
+ ],
954
+ }),
955
+ };
956
+ }
957
+ // Fallback stub when no MCP manager is configured — reflects built-in tools only.
827
958
  const tools = this.tools;
828
959
  return {
829
960
  getServerStatus: () => [{