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.
- package/README.md +37 -5
- package/dist/code/agent.d.ts +16 -0
- package/dist/code/agent.d.ts.map +1 -1
- package/dist/code/agent.js +317 -186
- package/dist/code/agent.js.map +1 -1
- package/dist/core/config.d.ts +30 -16
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +2 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/worker-agent.d.ts +0 -8
- package/dist/core/worker-agent.d.ts.map +1 -1
- package/dist/core/worker-agent.js +359 -228
- package/dist/core/worker-agent.js.map +1 -1
- package/dist/core/workspace.d.ts.map +1 -1
- package/dist/core/workspace.js +2 -0
- package/dist/core/workspace.js.map +1 -1
- package/dist/interfaces/cli/index.js +33 -3
- package/dist/interfaces/cli/index.js.map +1 -1
- package/dist/interfaces/cli/repl.d.ts +6 -0
- package/dist/interfaces/cli/repl.d.ts.map +1 -1
- package/dist/interfaces/cli/repl.js +45 -3
- package/dist/interfaces/cli/repl.js.map +1 -1
- package/dist/interfaces/cli/setup-wizard.js +12 -12
- package/dist/interfaces/cli/setup-wizard.js.map +1 -1
- package/dist/models/model-client.d.ts.map +1 -1
- package/dist/models/model-client.js +19 -3
- package/dist/models/model-client.js.map +1 -1
- package/dist/personas/persona-manager.d.ts +14 -2
- package/dist/personas/persona-manager.d.ts.map +1 -1
- package/dist/personas/persona-manager.js +45 -14
- package/dist/personas/persona-manager.js.map +1 -1
- package/dist/storage/local-provider.d.ts.map +1 -1
- package/dist/storage/local-provider.js +2 -0
- package/dist/storage/local-provider.js.map +1 -1
- package/package.json +1 -1
package/dist/code/agent.js
CHANGED
|
@@ -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
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
450
|
-
` -
|
|
451
|
-
` -
|
|
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
|
-
`
|
|
455
|
-
` Stage 1: write_file —
|
|
456
|
-
` Stage 2
|
|
457
|
-
`
|
|
458
|
-
`Start with Stage 1 NOW: write_file with just the bare
|
|
519
|
+
`Use the skeleton-first approach — write 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
|
-
//
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
620
|
-
|
|
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: '
|
|
629
|
-
content:
|
|
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
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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 (
|
|
668
|
-
|
|
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
|
-
//
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
//
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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 =
|
|
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
|
-
|
|
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: () => [{
|