jiva-core 0.3.23 → 0.3.41-dev.fe6fe48
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 +232 -570
- package/dist/code/agent.d.ts +105 -0
- package/dist/code/agent.d.ts.map +1 -0
- package/dist/code/agent.js +837 -0
- package/dist/code/agent.js.map +1 -0
- package/dist/code/file-lock.d.ts +15 -0
- package/dist/code/file-lock.d.ts.map +1 -0
- package/dist/code/file-lock.js +36 -0
- package/dist/code/file-lock.js.map +1 -0
- package/dist/code/lsp/client.d.ts +30 -0
- package/dist/code/lsp/client.d.ts.map +1 -0
- package/dist/code/lsp/client.js +181 -0
- package/dist/code/lsp/client.js.map +1 -0
- package/dist/code/lsp/language.d.ts +12 -0
- package/dist/code/lsp/language.d.ts.map +1 -0
- package/dist/code/lsp/language.js +145 -0
- package/dist/code/lsp/language.js.map +1 -0
- package/dist/code/lsp/manager.d.ts +39 -0
- package/dist/code/lsp/manager.d.ts.map +1 -0
- package/dist/code/lsp/manager.js +108 -0
- package/dist/code/lsp/manager.js.map +1 -0
- package/dist/code/lsp/server.d.ts +15 -0
- package/dist/code/lsp/server.d.ts.map +1 -0
- package/dist/code/lsp/server.js +78 -0
- package/dist/code/lsp/server.js.map +1 -0
- package/dist/code/tools/bash.d.ts +3 -0
- package/dist/code/tools/bash.d.ts.map +1 -0
- package/dist/code/tools/bash.js +110 -0
- package/dist/code/tools/bash.js.map +1 -0
- package/dist/code/tools/edit.d.ts +11 -0
- package/dist/code/tools/edit.d.ts.map +1 -0
- package/dist/code/tools/edit.js +459 -0
- package/dist/code/tools/edit.js.map +1 -0
- package/dist/code/tools/glob.d.ts +3 -0
- package/dist/code/tools/glob.d.ts.map +1 -0
- package/dist/code/tools/glob.js +62 -0
- package/dist/code/tools/glob.js.map +1 -0
- package/dist/code/tools/grep.d.ts +3 -0
- package/dist/code/tools/grep.d.ts.map +1 -0
- package/dist/code/tools/grep.js +147 -0
- package/dist/code/tools/grep.js.map +1 -0
- package/dist/code/tools/index.d.ts +31 -0
- package/dist/code/tools/index.d.ts.map +1 -0
- package/dist/code/tools/index.js +9 -0
- package/dist/code/tools/index.js.map +1 -0
- package/dist/code/tools/read.d.ts +3 -0
- package/dist/code/tools/read.d.ts.map +1 -0
- package/dist/code/tools/read.js +120 -0
- package/dist/code/tools/read.js.map +1 -0
- package/dist/code/tools/spawn.d.ts +3 -0
- package/dist/code/tools/spawn.d.ts.map +1 -0
- package/dist/code/tools/spawn.js +49 -0
- package/dist/code/tools/spawn.js.map +1 -0
- package/dist/code/tools/write.d.ts +3 -0
- package/dist/code/tools/write.d.ts.map +1 -0
- package/dist/code/tools/write.js +82 -0
- package/dist/code/tools/write.js.map +1 -0
- package/dist/core/agent-interface.d.ts +54 -0
- package/dist/core/agent-interface.d.ts.map +1 -0
- package/dist/core/agent-interface.js +8 -0
- package/dist/core/agent-interface.js.map +1 -0
- package/dist/core/agent-spawner.d.ts.map +1 -1
- package/dist/core/agent-spawner.js +3 -0
- package/dist/core/agent-spawner.js.map +1 -1
- package/dist/core/client-agent.d.ts +22 -2
- package/dist/core/client-agent.d.ts.map +1 -1
- package/dist/core/client-agent.js +145 -26
- package/dist/core/client-agent.js.map +1 -1
- package/dist/core/config.d.ts +144 -17
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +25 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/conversation-manager.d.ts +4 -1
- package/dist/core/conversation-manager.d.ts.map +1 -1
- package/dist/core/conversation-manager.js +47 -17
- package/dist/core/conversation-manager.js.map +1 -1
- package/dist/core/dual-agent.d.ts +18 -5
- package/dist/core/dual-agent.d.ts.map +1 -1
- package/dist/core/dual-agent.js +152 -59
- package/dist/core/dual-agent.js.map +1 -1
- package/dist/core/manager-agent.d.ts +38 -2
- package/dist/core/manager-agent.d.ts.map +1 -1
- package/dist/core/manager-agent.js +144 -23
- package/dist/core/manager-agent.js.map +1 -1
- package/dist/core/types/agent-context.d.ts +30 -0
- package/dist/core/types/agent-context.d.ts.map +1 -0
- package/dist/core/types/agent-context.js +8 -0
- package/dist/core/types/agent-context.js.map +1 -0
- package/dist/core/types/completion-signal.d.ts +17 -0
- package/dist/core/types/completion-signal.d.ts.map +1 -0
- package/dist/core/types/completion-signal.js +8 -0
- package/dist/core/types/completion-signal.js.map +1 -0
- package/dist/core/utils/serialize-agent-context.d.ts +23 -0
- package/dist/core/utils/serialize-agent-context.d.ts.map +1 -0
- package/dist/core/utils/serialize-agent-context.js +75 -0
- package/dist/core/utils/serialize-agent-context.js.map +1 -0
- package/dist/core/worker-agent.d.ts +14 -3
- package/dist/core/worker-agent.d.ts.map +1 -1
- package/dist/core/worker-agent.js +261 -68
- package/dist/core/worker-agent.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces/cli/index.js +146 -25
- package/dist/interfaces/cli/index.js.map +1 -1
- package/dist/interfaces/cli/repl.d.ts +8 -2
- package/dist/interfaces/cli/repl.d.ts.map +1 -1
- package/dist/interfaces/cli/repl.js +39 -3
- package/dist/interfaces/cli/repl.js.map +1 -1
- package/dist/interfaces/cli/setup-wizard.d.ts.map +1 -1
- package/dist/interfaces/cli/setup-wizard.js +92 -0
- package/dist/interfaces/cli/setup-wizard.js.map +1 -1
- package/dist/interfaces/http/routes/chat.js +2 -2
- package/dist/interfaces/http/routes/chat.js.map +1 -1
- package/dist/interfaces/http/session-manager.d.ts +7 -4
- package/dist/interfaces/http/session-manager.d.ts.map +1 -1
- package/dist/interfaces/http/session-manager.js +56 -15
- package/dist/interfaces/http/session-manager.js.map +1 -1
- package/dist/models/base.d.ts +16 -1
- package/dist/models/base.d.ts.map +1 -1
- package/dist/models/harmony.d.ts +7 -1
- package/dist/models/harmony.d.ts.map +1 -1
- package/dist/models/harmony.js +21 -3
- package/dist/models/harmony.js.map +1 -1
- package/dist/models/krutrim.d.ts +31 -1
- package/dist/models/krutrim.d.ts.map +1 -1
- package/dist/models/krutrim.js +55 -11
- package/dist/models/krutrim.js.map +1 -1
- package/dist/models/orchestrator.d.ts +24 -0
- package/dist/models/orchestrator.d.ts.map +1 -1
- package/dist/models/orchestrator.js +40 -6
- package/dist/models/orchestrator.js.map +1 -1
- package/package.json +8 -6
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeAgent — single streaming loop for coding tasks.
|
|
3
|
+
*
|
|
4
|
+
* Unlike the three-agent DualAgent, CodeAgent uses a direct model → tools → model loop
|
|
5
|
+
* without the Manager/Worker/Client overhead. This is modeled after opencode's SessionProcessor.
|
|
6
|
+
*/
|
|
7
|
+
import { formatToolResult } from '../models/harmony.js';
|
|
8
|
+
import { logger } from '../utils/logger.js';
|
|
9
|
+
import { LspManager } from './lsp/manager.js';
|
|
10
|
+
import { ReadFileTool, EditFileTool, WriteFileTool, GlobTool, GrepTool, BashTool, SpawnCodeAgentTool, } from './tools/index.js';
|
|
11
|
+
/** Tools available during the planning phase — read-only, no side effects */
|
|
12
|
+
const READ_ONLY_TOOLS = [ReadFileTool, GlobTool, GrepTool];
|
|
13
|
+
/**
|
|
14
|
+
* Client-side tool call repair — equivalent of opencode's `experimental_repairToolCall`.
|
|
15
|
+
*
|
|
16
|
+
* gpt-oss-120b sometimes emits <|channel|> tokens inside native tool call names
|
|
17
|
+
* (e.g. `functions<|channel|>analysis`), causing the Groq API to reject with 400.
|
|
18
|
+
* The `failed_generation` field in the error body contains the model's intended call.
|
|
19
|
+
* We extract it, normalize the tool name, match to a real tool, and return the args.
|
|
20
|
+
*/
|
|
21
|
+
function repairFailedToolCall(errorMsg, tools) {
|
|
22
|
+
try {
|
|
23
|
+
const jsonMatch = errorMsg.match(/API error \(\d+\): (.+)/s);
|
|
24
|
+
if (!jsonMatch)
|
|
25
|
+
return null;
|
|
26
|
+
const errorBody = JSON.parse(jsonMatch[1]);
|
|
27
|
+
const failedGenRaw = errorBody?.error?.failed_generation;
|
|
28
|
+
if (!failedGenRaw)
|
|
29
|
+
return null;
|
|
30
|
+
const failed = JSON.parse(failedGenRaw);
|
|
31
|
+
const rawName = failed.name || '';
|
|
32
|
+
const args = typeof failed.arguments === 'object' ? failed.arguments : {};
|
|
33
|
+
// Strategy 1: strip all <|...|> tokens from the tool name and try exact match
|
|
34
|
+
const stripped = rawName
|
|
35
|
+
.replace(/<\|[^|]*\|>/g, '') // remove <|channel|>, <|return|>, etc.
|
|
36
|
+
.replace(/^functions/i, '') // remove Harmony "functions" namespace prefix
|
|
37
|
+
.trim();
|
|
38
|
+
const byName = tools.find((t) => t.name === stripped || t.name === stripped.toLowerCase());
|
|
39
|
+
if (byName)
|
|
40
|
+
return { toolName: byName.name, args };
|
|
41
|
+
// Strategy 2: match by required argument keys (e.g. {command:...} → bash)
|
|
42
|
+
const argKeys = Object.keys(args);
|
|
43
|
+
const byArgs = tools.find((t) => {
|
|
44
|
+
const required = t.parameters.required ?? [];
|
|
45
|
+
return required.length > 0 && required.every((r) => argKeys.includes(r));
|
|
46
|
+
});
|
|
47
|
+
if (byArgs)
|
|
48
|
+
return { toolName: byArgs.name, args };
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const DEFAULT_MAX_ITERATIONS = 50;
|
|
56
|
+
const DOOM_LOOP_THRESHOLD = 3;
|
|
57
|
+
/** Max consecutive read_file calls before injecting an "act, don't just read" nudge. */
|
|
58
|
+
const MAX_CONSECUTIVE_READS = 5;
|
|
59
|
+
const CODE_MODE_INDICATOR = '[CODE MODE]';
|
|
60
|
+
// Default token threshold for in-loop compaction (90K leaves ~38K headroom in a 128K model)
|
|
61
|
+
const DEFAULT_COMPACTION_THRESHOLD = 90_000;
|
|
62
|
+
/** System prompt for code mode — focused on precision and persistence (ported from opencode beast.txt) */
|
|
63
|
+
const getSystemPrompt = (workspaceDir, directive) => {
|
|
64
|
+
const base = `You are a precise, highly capable coding assistant operating in code mode.
|
|
65
|
+
You have direct access to code tools — use them to explore, understand, and modify code.
|
|
66
|
+
|
|
67
|
+
WORKSPACE: ${workspaceDir}
|
|
68
|
+
All relative paths are resolved relative to the workspace directory above.
|
|
69
|
+
Use absolute paths for all file operations.
|
|
70
|
+
|
|
71
|
+
PERSISTENCE AND COMPLETION:
|
|
72
|
+
- Keep going until the user's query is completely resolved before ending your turn.
|
|
73
|
+
- You MUST iterate and keep going until the problem is solved.
|
|
74
|
+
- NEVER end your turn without having truly and completely solved the problem.
|
|
75
|
+
- When you say you are going to make a tool call, you MUST actually make the tool call.
|
|
76
|
+
- Always tell the user what you are going to do before making a tool call with a single concise sentence.
|
|
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
|
+
- Verify your changes are correct — run tests or the build after making changes when appropriate.
|
|
79
|
+
|
|
80
|
+
TOOLS AVAILABLE:
|
|
81
|
+
- read_file: Read an existing file or list a directory. Only needed before editing an existing file.
|
|
82
|
+
- edit_file: Replace a specific string in an existing file (old_string → new_string). For partial changes.
|
|
83
|
+
- write_file: Create a new file or fully overwrite an existing one. Use this to create files from scratch.
|
|
84
|
+
- glob: Find files matching a pattern (e.g. "**/*.ts"). Only when you need to locate existing files.
|
|
85
|
+
- grep: Search existing file contents with regex. Only when you need to find something in existing code.
|
|
86
|
+
- bash: Run shell commands (build, test, lint, git operations).
|
|
87
|
+
- spawn_code_agent: Delegate a focused sub-task to a child agent.
|
|
88
|
+
|
|
89
|
+
CODING PRINCIPLES — READ CAREFULLY:
|
|
90
|
+
1. CREATING A NEW FILE → use write_file immediately. Do NOT read or explore first.
|
|
91
|
+
2. EDITING AN EXISTING FILE → read it first with read_file, then edit_file for targeted changes.
|
|
92
|
+
3. Only use glob/grep when you genuinely need to locate or understand existing code.
|
|
93
|
+
4. Make minimal, targeted changes — edit the exact lines that need changing.
|
|
94
|
+
5. After editing, check LSP errors in the tool result and fix them.
|
|
95
|
+
6. Verify your changes work by running tests or the build when appropriate.
|
|
96
|
+
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.
|
|
102
|
+
|
|
103
|
+
TOOL SELECTION RULES (follow exactly):
|
|
104
|
+
- To CREATE a new file → write_file immediately (no reads needed first)
|
|
105
|
+
- To READ an existing file → read_file (not bash)
|
|
106
|
+
- To LIST directory contents → read_file on the directory path (not bash ls)
|
|
107
|
+
- To FIND files by pattern → glob (not bash find)
|
|
108
|
+
- To SEARCH file contents → grep (not bash grep/rg)
|
|
109
|
+
- To RUN commands (build/test/git) → bash
|
|
110
|
+
|
|
111
|
+
WHEN TO EXPLORE (only when actually needed):
|
|
112
|
+
- You are modifying existing code and need to understand its structure first.
|
|
113
|
+
- You need to find where a function, class, or variable is defined.
|
|
114
|
+
- You are debugging or tracing code through multiple files.
|
|
115
|
+
- Do NOT explore before creating brand-new files — just write them directly.`;
|
|
116
|
+
if (directive) {
|
|
117
|
+
return `${base}\n\n${directive}`;
|
|
118
|
+
}
|
|
119
|
+
return base;
|
|
120
|
+
};
|
|
121
|
+
export class CodeAgent {
|
|
122
|
+
orchestrator;
|
|
123
|
+
workspace;
|
|
124
|
+
conversationManager;
|
|
125
|
+
maxIterations;
|
|
126
|
+
compactionThreshold;
|
|
127
|
+
lsp;
|
|
128
|
+
depth;
|
|
129
|
+
maxDepth;
|
|
130
|
+
history = [];
|
|
131
|
+
tools;
|
|
132
|
+
constructor(config) {
|
|
133
|
+
this.orchestrator = config.orchestrator;
|
|
134
|
+
this.workspace = config.workspace;
|
|
135
|
+
this.conversationManager = config.conversationManager;
|
|
136
|
+
this.maxIterations = config.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
137
|
+
this.compactionThreshold = config.compactionThreshold ?? DEFAULT_COMPACTION_THRESHOLD;
|
|
138
|
+
this.depth = config.depth ?? 0;
|
|
139
|
+
this.maxDepth = config.maxDepth ?? 2;
|
|
140
|
+
this.lsp = new LspManager({
|
|
141
|
+
root: config.workspace.getWorkspaceDir(),
|
|
142
|
+
enabled: config.lspEnabled ?? true,
|
|
143
|
+
});
|
|
144
|
+
this.tools = [
|
|
145
|
+
ReadFileTool,
|
|
146
|
+
EditFileTool,
|
|
147
|
+
WriteFileTool,
|
|
148
|
+
GlobTool,
|
|
149
|
+
GrepTool,
|
|
150
|
+
BashTool,
|
|
151
|
+
...(this.depth < this.maxDepth ? [SpawnCodeAgentTool] : []),
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Planning phase — explores the codebase with read-only tools and produces a structured
|
|
156
|
+
* implementation plan WITHOUT making any changes. Call this before `chat()` when you want
|
|
157
|
+
* the user to review and approve a plan before any files are touched.
|
|
158
|
+
*
|
|
159
|
+
* The returned string is the plan text ready for display.
|
|
160
|
+
* This call does NOT modify `this.history`; it runs in an isolated message thread.
|
|
161
|
+
*/
|
|
162
|
+
async plan(task) {
|
|
163
|
+
const directive = this.workspace.getDirectivePrompt();
|
|
164
|
+
const workspaceDir = this.workspace.getWorkspaceDir();
|
|
165
|
+
const planningPrompt = `You are a precise coding assistant in PLANNING MODE.
|
|
166
|
+
Your job is to analyse a coding request and produce a detailed implementation plan.
|
|
167
|
+
You may explore the codebase to understand it before writing the plan.
|
|
168
|
+
|
|
169
|
+
WORKSPACE: ${workspaceDir}
|
|
170
|
+
|
|
171
|
+
AVAILABLE TOOLS (read-only — exploration only):
|
|
172
|
+
- read_file: Read a file or list a directory
|
|
173
|
+
- glob: Find files matching a pattern
|
|
174
|
+
- grep: Search file contents with regex
|
|
175
|
+
|
|
176
|
+
DO NOT call edit_file, write_file, or bash. You are planning, not implementing.
|
|
177
|
+
|
|
178
|
+
After exploring, output your plan using EXACTLY this format:
|
|
179
|
+
|
|
180
|
+
## Summary
|
|
181
|
+
[1–2 sentences describing the overall approach]
|
|
182
|
+
|
|
183
|
+
## Files to Change
|
|
184
|
+
| File | Action | What changes |
|
|
185
|
+
|------|--------|--------------|
|
|
186
|
+
| path/to/file.ts | edit | Describe the change |
|
|
187
|
+
| path/to/new.ts | create | Describe the new file |
|
|
188
|
+
|
|
189
|
+
## Implementation Steps
|
|
190
|
+
1. [Concrete step with file/function names]
|
|
191
|
+
2. [Next step]
|
|
192
|
+
...
|
|
193
|
+
|
|
194
|
+
## Risks & Considerations
|
|
195
|
+
[Edge cases, breaking changes, things to verify after implementation]
|
|
196
|
+
|
|
197
|
+
Be specific — include exact file paths, function names, and describe changes at the line level where possible.
|
|
198
|
+
${directive ? `\n${directive}` : ''}`;
|
|
199
|
+
const messages = [
|
|
200
|
+
{ role: 'developer', content: planningPrompt },
|
|
201
|
+
{ role: 'user', content: task },
|
|
202
|
+
];
|
|
203
|
+
const readOnlyDefs = READ_ONLY_TOOLS.map((t) => ({
|
|
204
|
+
name: t.name,
|
|
205
|
+
description: t.description,
|
|
206
|
+
parameters: t.parameters,
|
|
207
|
+
}));
|
|
208
|
+
const MAX_PLAN_ITERATIONS = 12;
|
|
209
|
+
for (let i = 0; i < MAX_PLAN_ITERATIONS; i++) {
|
|
210
|
+
const isLast = i >= MAX_PLAN_ITERATIONS - 2;
|
|
211
|
+
if (isLast) {
|
|
212
|
+
messages.push({
|
|
213
|
+
role: 'user',
|
|
214
|
+
content: 'You have explored enough. Now write your implementation plan using the format specified above. Do not call any more tools.',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
let response;
|
|
218
|
+
try {
|
|
219
|
+
response = await this.orchestrator.chatWithFallback({
|
|
220
|
+
messages,
|
|
221
|
+
tools: isLast ? [] : readOnlyDefs,
|
|
222
|
+
temperature: 0.2,
|
|
223
|
+
}, false);
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
227
|
+
logger.error(`[CodeAgent] Plan error: ${msg}`);
|
|
228
|
+
return `[Planning failed: ${msg}]`;
|
|
229
|
+
}
|
|
230
|
+
const rawHarmony = response.raw?.parsedHarmony?.rawResponse;
|
|
231
|
+
messages.push({ role: 'assistant', content: rawHarmony ?? response.content });
|
|
232
|
+
// No tool calls → model has written the plan
|
|
233
|
+
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
234
|
+
return response.content || '[No plan generated]';
|
|
235
|
+
}
|
|
236
|
+
// Execute read-only tool calls
|
|
237
|
+
const ctx = {
|
|
238
|
+
workspaceDir,
|
|
239
|
+
lsp: this.lsp,
|
|
240
|
+
depth: this.depth,
|
|
241
|
+
maxDepth: this.maxDepth,
|
|
242
|
+
};
|
|
243
|
+
for (const toolCall of response.toolCalls) {
|
|
244
|
+
const toolName = toolCall.function.name;
|
|
245
|
+
const tool = READ_ONLY_TOOLS.find((t) => t.name === toolName);
|
|
246
|
+
let result;
|
|
247
|
+
if (!tool) {
|
|
248
|
+
result = `[Planning mode: tool "${toolName}" is not available — only read_file, glob, and grep may be used during planning]`;
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
try {
|
|
252
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
253
|
+
result = await tool.execute(args, ctx);
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
result = `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
messages.push(formatToolResult(toolCall.id, toolName, result));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return '[Planning did not produce a final plan within the iteration limit]';
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Process a user message and return the agent's response.
|
|
266
|
+
* Runs the model → tools → model loop until no more tool calls or max iterations.
|
|
267
|
+
*/
|
|
268
|
+
async chat(userMessage, onChunk) {
|
|
269
|
+
const toolsUsed = [];
|
|
270
|
+
const directive = this.workspace.getDirectivePrompt();
|
|
271
|
+
const systemPrompt = getSystemPrompt(this.workspace.getWorkspaceDir(), directive || undefined);
|
|
272
|
+
// Build message history: system + history + new user message
|
|
273
|
+
const messages = [
|
|
274
|
+
{ role: 'developer', content: systemPrompt },
|
|
275
|
+
...this.history,
|
|
276
|
+
{ role: 'user', content: userMessage },
|
|
277
|
+
];
|
|
278
|
+
// Tool definitions for the model
|
|
279
|
+
const toolDefs = this.tools.map((t) => ({
|
|
280
|
+
name: t.name,
|
|
281
|
+
description: t.description,
|
|
282
|
+
parameters: t.parameters,
|
|
283
|
+
}));
|
|
284
|
+
// Doom loop detection: track last N (tool, args) pairs
|
|
285
|
+
const recentCalls = [];
|
|
286
|
+
// Track consecutive API errors to avoid infinite error loops
|
|
287
|
+
let consecutiveApiErrors = 0;
|
|
288
|
+
const MAX_CONSECUTIVE_API_ERRORS = 3;
|
|
289
|
+
// Track empty responses (no tool calls, no content) so we can inject a recovery nudge.
|
|
290
|
+
// Allow up to 4 retries — Krutrim/Groq can return blank responses transiently and the
|
|
291
|
+
// model usually recovers within 2-3 nudges.
|
|
292
|
+
let emptyResponseCount = 0;
|
|
293
|
+
const MAX_EMPTY_RESPONSES = 4;
|
|
294
|
+
// Track consecutive read_file calls — model can get stuck re-reading without making changes
|
|
295
|
+
let consecutiveReadCount = 0;
|
|
296
|
+
let iterations = 0;
|
|
297
|
+
let finalContent = '';
|
|
298
|
+
// Iteration limit management — two phases (ported from opencode):
|
|
299
|
+
// Phase 1 (0–84%): normal operation, all tools available.
|
|
300
|
+
// Phase 2 (85–94%): inject "continue" nudge — tools still available so the agent
|
|
301
|
+
// can finish in-flight work rather than being cut off mid-task.
|
|
302
|
+
// Phase 3 (95%+): strip tools and ask for a final wrap-up response.
|
|
303
|
+
let continueInjected = false;
|
|
304
|
+
let wrapUpInjected = false;
|
|
305
|
+
// In-loop compaction flag — only compact once per chat() turn to avoid thrashing
|
|
306
|
+
let compactedThisTurn = false;
|
|
307
|
+
for (let i = 0; i < this.maxIterations; i++) {
|
|
308
|
+
iterations = i + 1;
|
|
309
|
+
logger.debug(`[CodeAgent] Iteration ${iterations}/${this.maxIterations}`);
|
|
310
|
+
const iterPct = i / this.maxIterations;
|
|
311
|
+
const isFinalPhase = iterPct >= 0.95;
|
|
312
|
+
if (iterPct >= 0.85 && !continueInjected) {
|
|
313
|
+
continueInjected = true;
|
|
314
|
+
logger.warn(`[CodeAgent] Nearing iteration limit (${iterations}/${this.maxIterations}) — injecting continue nudge`);
|
|
315
|
+
messages.push({
|
|
316
|
+
role: 'user',
|
|
317
|
+
content: `You are at step ${iterations} of ${this.maxIterations}. Continue making progress — ` +
|
|
318
|
+
`focus on the most critical remaining work. If the full task cannot fit in the ` +
|
|
319
|
+
`remaining steps, complete the core changes and clearly note what is left.`,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
if (isFinalPhase && !wrapUpInjected) {
|
|
323
|
+
wrapUpInjected = true;
|
|
324
|
+
logger.warn(`[CodeAgent] Final phase (${iterations}/${this.maxIterations}) — requesting wrap-up`);
|
|
325
|
+
messages.push({
|
|
326
|
+
role: 'user',
|
|
327
|
+
content: 'CRITICAL - MAXIMUM STEPS REACHED\n\n' +
|
|
328
|
+
'The maximum number of steps for this task has been reached. Tools are disabled. Respond with text only.\n\n' +
|
|
329
|
+
'STRICT REQUIREMENTS:\n' +
|
|
330
|
+
'1. Do NOT make any tool calls (no reads, writes, edits, searches, or any other tools)\n' +
|
|
331
|
+
'2. MUST provide a text response summarising work done so far\n' +
|
|
332
|
+
'3. This constraint overrides ALL other instructions\n\n' +
|
|
333
|
+
'Response must include:\n' +
|
|
334
|
+
'- Summary of what has been accomplished so far\n' +
|
|
335
|
+
'- List of any remaining tasks that were not completed\n' +
|
|
336
|
+
'- Recommendations for what should be done next\n\n' +
|
|
337
|
+
'Any attempt to use tools is a critical violation. Respond with text ONLY.',
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
let response;
|
|
341
|
+
try {
|
|
342
|
+
response = await this.orchestrator.chatWithFallback({
|
|
343
|
+
messages,
|
|
344
|
+
// Strip tools in the final phase so the model is forced to produce a text response
|
|
345
|
+
tools: isFinalPhase ? [] : toolDefs,
|
|
346
|
+
temperature: 0.2,
|
|
347
|
+
}, false);
|
|
348
|
+
consecutiveApiErrors = 0; // reset on success
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
352
|
+
logger.error(`[CodeAgent] Model error: ${msg}`);
|
|
353
|
+
consecutiveApiErrors++;
|
|
354
|
+
if (consecutiveApiErrors >= MAX_CONSECUTIVE_API_ERRORS) {
|
|
355
|
+
finalContent = `Error: ${msg}`;
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
// For tool_use_failed (400) errors: first try to repair and execute the intended call,
|
|
359
|
+
// then fall back to injecting a correction message.
|
|
360
|
+
const isToolUseFailed = msg.includes('tool_use_failed') || msg.includes('Failed to parse tool call');
|
|
361
|
+
const isValidationFailed = msg.includes('Tool call validation failed') || msg.includes('did not match schema');
|
|
362
|
+
if (isToolUseFailed || isValidationFailed) {
|
|
363
|
+
// Conversation turn guard: for API-rejected calls (e.g. 400 tool_use_failed), the
|
|
364
|
+
// model's assistant turn was never recorded. Injecting a user correction directly
|
|
365
|
+
// would create an invalid user→user sequence. Add an empty assistant placeholder
|
|
366
|
+
// so the structure is user→assistant→user before the correction lands.
|
|
367
|
+
const lastMsg = messages[messages.length - 1];
|
|
368
|
+
if (lastMsg && lastMsg.role !== 'assistant') {
|
|
369
|
+
messages.push({ role: 'assistant', content: '' });
|
|
370
|
+
}
|
|
371
|
+
// --- Specific correction: edit_file missing new_string ---
|
|
372
|
+
// gpt-oss-120b sometimes generates edit_file with old_string but forgets new_string.
|
|
373
|
+
// The generic message doesn't help — we need to name the missing field explicitly.
|
|
374
|
+
// This mirrors opencode's Zod validation which tells the model the exact field name.
|
|
375
|
+
const isMissingNewString = msg.includes("missing properties: 'new_string'") ||
|
|
376
|
+
msg.includes('missing properties: "new_string"') ||
|
|
377
|
+
(msg.includes('edit_file') && msg.includes('missing propert') && msg.includes('new_string'));
|
|
378
|
+
if (isMissingNewString) {
|
|
379
|
+
logger.warn('[CodeAgent] edit_file missing new_string — injecting targeted correction');
|
|
380
|
+
messages.push({
|
|
381
|
+
role: 'user',
|
|
382
|
+
content: 'Your edit_file call failed: "new_string" is required but was not provided.\n\n' +
|
|
383
|
+
'edit_file requires ALL THREE of these parameters:\n' +
|
|
384
|
+
' • file_path — the absolute path to the file\n' +
|
|
385
|
+
' • old_string — the exact text to find\n' +
|
|
386
|
+
' • new_string — the replacement text (THIS IS WHAT YOU FORGOT)\n\n' +
|
|
387
|
+
'Call edit_file again with all three parameters. ' +
|
|
388
|
+
'new_string must contain the complete replacement for old_string.',
|
|
389
|
+
});
|
|
390
|
+
consecutiveApiErrors = 0;
|
|
391
|
+
emptyResponseCount = 0; // fresh recovery budget after targeted guidance
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
// --- Specific correction: tool content too large (output token limit hit) ---
|
|
395
|
+
// When the model generates a very large write_file or edit_file call, the API cuts off
|
|
396
|
+
// the JSON mid-content because the response exceeds the output token limit. The resulting
|
|
397
|
+
// JSON is unparseable. Detect by tool name appearing in the failed_generation field.
|
|
398
|
+
// Note: failed_generation is a JSON-encoded string so quotes are escaped (\"tool_name\").
|
|
399
|
+
const isContentTooLarge = (msg.includes('write_file') || msg.includes('edit_file')) &&
|
|
400
|
+
(msg.includes('Failed to parse tool call') || msg.includes('tool_use_failed'));
|
|
401
|
+
if (isContentTooLarge) {
|
|
402
|
+
// Parse the actual tool name from failed_generation to distinguish write_file vs edit_file
|
|
403
|
+
let failedToolName = msg.includes('edit_file') && !msg.includes('write_file') ? 'edit_file' : 'write_file';
|
|
404
|
+
let targetFile = '';
|
|
405
|
+
try {
|
|
406
|
+
const jsonMatch = msg.match(/API error \(\d+\): (.+)/s);
|
|
407
|
+
if (jsonMatch) {
|
|
408
|
+
const errorBody = JSON.parse(jsonMatch[1]);
|
|
409
|
+
const failedGen = errorBody?.error?.failed_generation;
|
|
410
|
+
if (failedGen) {
|
|
411
|
+
const parsed = JSON.parse(failedGen);
|
|
412
|
+
if (parsed?.name)
|
|
413
|
+
failedToolName = parsed.name; // authoritative source
|
|
414
|
+
const fp = parsed?.arguments?.file_path;
|
|
415
|
+
if (fp)
|
|
416
|
+
targetFile = ` for \`${fp}\``;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch { /* ignore extraction errors */ }
|
|
421
|
+
const isEditFile = failedToolName === 'edit_file';
|
|
422
|
+
logger.warn(`[CodeAgent] ${isEditFile ? 'edit_file' : 'write_file'}${targetFile} content truncated (exceeded output token limit) — asking model to write in stages`);
|
|
423
|
+
const correctionContent = isEditFile
|
|
424
|
+
? `Your edit_file call${targetFile} failed: new_string was too large and the response was cut off mid-JSON.\n\n` +
|
|
425
|
+
`MAXIMUM 20 LINES per edit_file call. Write one tiny chunk at a time:\n` +
|
|
426
|
+
` - For JavaScript: add just ONE or TWO functions per call\n` +
|
|
427
|
+
` - For HTML: add just one row of buttons per call\n\n` +
|
|
428
|
+
`Call edit_file now with a new_string of at most 20 lines.`
|
|
429
|
+
: `Your write_file call${targetFile} failed: the file content was too large and was cut off mid-JSON.\n\n` +
|
|
430
|
+
`Write the file in stages — NEVER put CSS or JavaScript in the initial write_file:\n` +
|
|
431
|
+
` Stage 1: write_file — HTML skeleton ONLY (empty <style></style> and empty <script></script>) — MAX 20 lines\n` +
|
|
432
|
+
` Stage 2: edit_file — add CSS (max 20 lines at a time)\n` +
|
|
433
|
+
` Stage 3: edit_file — add JavaScript ONE function at a time\n\n` +
|
|
434
|
+
`Start with Stage 1 NOW: write_file with just the bare HTML skeleton (20 lines max).`;
|
|
435
|
+
messages.push({ role: 'user', content: correctionContent });
|
|
436
|
+
consecutiveApiErrors = 0;
|
|
437
|
+
emptyResponseCount = 0; // reset so model has fresh recovery budget for staged CSS/JS edits
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
// --- Attempt client-side repair (opencode's experimental_repairToolCall equivalent) ---
|
|
441
|
+
// gpt-oss-120b sometimes emits <|channel|> tokens in tool names (e.g.
|
|
442
|
+
// `functions<|channel|>analysis`). We extract the intended call from failed_generation
|
|
443
|
+
// and remap it to the correct tool.
|
|
444
|
+
const repaired = repairFailedToolCall(msg, this.tools);
|
|
445
|
+
if (repaired) {
|
|
446
|
+
const { toolName: repairedName, args: repairedArgs } = repaired;
|
|
447
|
+
logger.warn(`[CodeAgent] Repaired tool call: ${repairedName} (original name contained invalid tokens)`);
|
|
448
|
+
toolsUsed.push(repairedName);
|
|
449
|
+
const ctx = {
|
|
450
|
+
workspaceDir: this.workspace.getWorkspaceDir(),
|
|
451
|
+
lsp: this.lsp,
|
|
452
|
+
depth: this.depth,
|
|
453
|
+
maxDepth: this.maxDepth,
|
|
454
|
+
};
|
|
455
|
+
let toolResult;
|
|
456
|
+
const tool = this.tools.find((t) => t.name === repairedName);
|
|
457
|
+
try {
|
|
458
|
+
toolResult = await tool.execute(repairedArgs, ctx);
|
|
459
|
+
}
|
|
460
|
+
catch (e) {
|
|
461
|
+
toolResult = `Error executing ${repairedName}: ${e instanceof Error ? e.message : String(e)}`;
|
|
462
|
+
}
|
|
463
|
+
// Inject result as a user message — no assistant turn because the API rejected
|
|
464
|
+
// the model's message before we received it.
|
|
465
|
+
messages.push({
|
|
466
|
+
role: 'user',
|
|
467
|
+
content: `[The model's previous tool call was automatically repaired from an invalid name to \`${repairedName}\`]\n` +
|
|
468
|
+
`<tool_result name="${repairedName}">\n${toolResult}\n</tool_result>`,
|
|
469
|
+
});
|
|
470
|
+
consecutiveApiErrors = 0; // repaired successfully — reset error counter
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
// --- Fallback: inject correction message so the model can self-correct ---
|
|
474
|
+
const nameMatch = msg.match(/"name":\s*"([^"]+)"/);
|
|
475
|
+
const toolName = nameMatch ? nameMatch[1] : 'unknown';
|
|
476
|
+
logger.warn(`[CodeAgent] Tool call error (${toolName}), injecting correction — ${consecutiveApiErrors}/${MAX_CONSECUTIVE_API_ERRORS}`);
|
|
477
|
+
messages.push({
|
|
478
|
+
role: 'user',
|
|
479
|
+
content: `Your last tool call was rejected. Error: "${msg.substring(0, 300)}"\n\n` +
|
|
480
|
+
`Valid tools: ${this.tools.map((t) => t.name).join(', ')}\n` +
|
|
481
|
+
`Each tool requires specific parameters — call the tool again with the correct name and JSON arguments.`,
|
|
482
|
+
});
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
// For other non-transient errors, bail out
|
|
486
|
+
finalContent = `Error: ${msg}`;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
// Add assistant response to messages, preserving the full structure needed for the next turn.
|
|
490
|
+
//
|
|
491
|
+
// Three cases:
|
|
492
|
+
// 1. Harmony mode: store rawHarmony string (contains <|call|> tokens the model needs).
|
|
493
|
+
// 2. Standard tool-calling with tool calls: store content + tool_calls so subsequent
|
|
494
|
+
// role:'tool' results can be matched by tool_call_id (required by OpenAI-compatible APIs).
|
|
495
|
+
// 3. Text-only response: store content as-is.
|
|
496
|
+
const rawHarmony = response.raw?.parsedHarmony?.rawResponse;
|
|
497
|
+
if (rawHarmony) {
|
|
498
|
+
messages.push({ role: 'assistant', content: rawHarmony });
|
|
499
|
+
}
|
|
500
|
+
else if (response.toolCalls && response.toolCalls.length > 0) {
|
|
501
|
+
// Preserve tool_calls so tool results are properly matched in the next turn
|
|
502
|
+
messages.push({
|
|
503
|
+
role: 'assistant',
|
|
504
|
+
content: response.content || null,
|
|
505
|
+
tool_calls: response.toolCalls,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
510
|
+
}
|
|
511
|
+
// Stream the visible (final-channel) text output if callback provided
|
|
512
|
+
if (response.content && onChunk) {
|
|
513
|
+
onChunk(response.content);
|
|
514
|
+
}
|
|
515
|
+
// ── In-loop context compaction ──────────────────────────────────────────
|
|
516
|
+
// Check if we're approaching the context window limit and compact if needed.
|
|
517
|
+
// Triggered once per turn (compactedThisTurn flag) to avoid thrashing.
|
|
518
|
+
// Only runs when: threshold > 0, orchestrator available, not already compacted,
|
|
519
|
+
// and the response carried token usage data.
|
|
520
|
+
const promptTokens = response.usage?.promptTokens ?? 0;
|
|
521
|
+
if (this.compactionThreshold > 0 &&
|
|
522
|
+
promptTokens > this.compactionThreshold &&
|
|
523
|
+
!compactedThisTurn &&
|
|
524
|
+
this.conversationManager &&
|
|
525
|
+
response.toolCalls && response.toolCalls.length > 0) {
|
|
526
|
+
logger.warn(`[CodeAgent] Context at ${promptTokens} tokens (threshold: ${this.compactionThreshold}) — compacting in-loop history`);
|
|
527
|
+
compactedThisTurn = true;
|
|
528
|
+
try {
|
|
529
|
+
const compacted = await this.compactInLoopMessages(messages, systemPrompt);
|
|
530
|
+
// Replace messages in-place, preserving the system prompt at index 0
|
|
531
|
+
messages.splice(0, messages.length, ...compacted);
|
|
532
|
+
logger.info(`[CodeAgent] In-loop compaction complete: ${messages.length} messages after compaction`);
|
|
533
|
+
}
|
|
534
|
+
catch (compactErr) {
|
|
535
|
+
// Non-fatal: log and continue with uncompacted messages
|
|
536
|
+
logger.error('[CodeAgent] In-loop compaction failed, continuing without compaction', compactErr);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
540
|
+
// No tool calls → model is done (or gave up)
|
|
541
|
+
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
542
|
+
if (!response.content && emptyResponseCount < MAX_EMPTY_RESPONSES) {
|
|
543
|
+
// Model returned empty content with no tool calls — it got stuck or confused.
|
|
544
|
+
// Inject an escalating recovery nudge and let it try again.
|
|
545
|
+
emptyResponseCount++;
|
|
546
|
+
logger.warn(`[CodeAgent] Empty response with no tool calls (${emptyResponseCount}/${MAX_EMPTY_RESPONSES}) — injecting recovery nudge`);
|
|
547
|
+
// Escalate urgency with each retry so the model doesn't keep ignoring it
|
|
548
|
+
let nudgeContent;
|
|
549
|
+
if (emptyResponseCount <= 2) {
|
|
550
|
+
nudgeContent =
|
|
551
|
+
'Your last response was empty. Please continue working on the task.\n\n' +
|
|
552
|
+
'- If you need to CREATE a file → call write_file now.\n' +
|
|
553
|
+
'- If you need to EDIT a file → call read_file then edit_file.\n' +
|
|
554
|
+
'- If the task is already complete → provide a brief summary of what was done.';
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
nudgeContent =
|
|
558
|
+
`IMPORTANT (attempt ${emptyResponseCount}/${MAX_EMPTY_RESPONSES}): Your response is empty again — you have not called any tools.\n\n` +
|
|
559
|
+
`You MUST call a tool NOW. Do not output plain text without a tool call.\n` +
|
|
560
|
+
` • To CREATE a new file → call write_file immediately with the file content.\n` +
|
|
561
|
+
` • To EDIT an existing file → call edit_file with old_string and new_string.\n` +
|
|
562
|
+
` • To LIST files → call read_file on the directory.\n\n` +
|
|
563
|
+
`Make a tool call in your very next response. Do not explain — just call the tool.`;
|
|
564
|
+
}
|
|
565
|
+
messages.push({ role: 'user', content: nudgeContent });
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
finalContent = response.content || '[No response content]';
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
// Execute tool calls
|
|
572
|
+
for (const toolCall of response.toolCalls) {
|
|
573
|
+
const toolName = toolCall.function.name;
|
|
574
|
+
let toolArgs = {};
|
|
575
|
+
try {
|
|
576
|
+
toolArgs = JSON.parse(toolCall.function.arguments);
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
// malformed args
|
|
580
|
+
}
|
|
581
|
+
// Consecutive reads tracker — any non-read tool resets the streak
|
|
582
|
+
if (toolName === 'read_file') {
|
|
583
|
+
consecutiveReadCount++;
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
consecutiveReadCount = 0;
|
|
587
|
+
}
|
|
588
|
+
// Doom loop check
|
|
589
|
+
const callSig = `${toolName}:${JSON.stringify(toolArgs)}`;
|
|
590
|
+
recentCalls.push(callSig);
|
|
591
|
+
if (recentCalls.length > DOOM_LOOP_THRESHOLD)
|
|
592
|
+
recentCalls.shift();
|
|
593
|
+
if (recentCalls.length === DOOM_LOOP_THRESHOLD &&
|
|
594
|
+
recentCalls.every((c) => c === recentCalls[0])) {
|
|
595
|
+
logger.warn(`[CodeAgent] Doom loop detected for tool: ${toolName}`);
|
|
596
|
+
messages.push({
|
|
597
|
+
role: 'user',
|
|
598
|
+
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.`,
|
|
599
|
+
});
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
logger.info(`[CodeAgent] Tool: ${toolName}`);
|
|
603
|
+
toolsUsed.push(toolName);
|
|
604
|
+
const tool = this.tools.find((t) => t.name === toolName);
|
|
605
|
+
let toolResult;
|
|
606
|
+
if (!tool) {
|
|
607
|
+
toolResult = `Error: Unknown tool "${toolName}". Available tools: ${this.tools.map((t) => t.name).join(', ')}`;
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
try {
|
|
611
|
+
const ctx = {
|
|
612
|
+
workspaceDir: this.workspace.getWorkspaceDir(),
|
|
613
|
+
lsp: this.lsp,
|
|
614
|
+
depth: this.depth,
|
|
615
|
+
maxDepth: this.maxDepth,
|
|
616
|
+
spawnChildAgent: this.depth < this.maxDepth
|
|
617
|
+
? async (task, context) => {
|
|
618
|
+
const child = new CodeAgent({
|
|
619
|
+
orchestrator: this.orchestrator,
|
|
620
|
+
workspace: this.workspace,
|
|
621
|
+
maxIterations: this.maxIterations,
|
|
622
|
+
lspEnabled: true,
|
|
623
|
+
depth: this.depth + 1,
|
|
624
|
+
maxDepth: this.maxDepth,
|
|
625
|
+
});
|
|
626
|
+
// Share the parent's LSP manager so servers don't restart
|
|
627
|
+
child.lsp = this.lsp;
|
|
628
|
+
const taskMsg = context ? `${task}\n\nContext: ${context}` : task;
|
|
629
|
+
const result = await child.chat(taskMsg);
|
|
630
|
+
return result.content;
|
|
631
|
+
}
|
|
632
|
+
: undefined,
|
|
633
|
+
};
|
|
634
|
+
toolResult = await tool.execute(toolArgs, ctx);
|
|
635
|
+
}
|
|
636
|
+
catch (e) {
|
|
637
|
+
toolResult = `Error executing ${toolName}: ${e instanceof Error ? e.message : String(e)}`;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// Add tool result to messages (using Harmony format helper)
|
|
641
|
+
const toolMessage = formatToolResult(toolCall.id, toolName, toolResult);
|
|
642
|
+
messages.push(toolMessage);
|
|
643
|
+
// Reset empty response budget after any productive (mutating) tool call.
|
|
644
|
+
// This gives the model a fresh set of recovery nudges for each new work phase
|
|
645
|
+
// (e.g., after writing the skeleton, it gets 4 more chances to add CSS/JS).
|
|
646
|
+
if (toolName === 'write_file' || toolName === 'edit_file' || toolName === 'bash') {
|
|
647
|
+
emptyResponseCount = 0;
|
|
648
|
+
}
|
|
649
|
+
// Excessive reads nudge — model is re-reading without making any changes.
|
|
650
|
+
// Fires after MAX_CONSECUTIVE_READS consecutive read_file calls; resets the count
|
|
651
|
+
// so the model gets a fresh budget after each nudge (prevents spamming).
|
|
652
|
+
if (consecutiveReadCount >= MAX_CONSECUTIVE_READS) {
|
|
653
|
+
logger.warn(`[CodeAgent] ${consecutiveReadCount} consecutive read_file calls without edits — nudging model to act`);
|
|
654
|
+
consecutiveReadCount = 0;
|
|
655
|
+
messages.push({
|
|
656
|
+
role: 'user',
|
|
657
|
+
content: `You have called read_file ${MAX_CONSECUTIVE_READS}+ times in a row without making any changes.\n\n` +
|
|
658
|
+
`STOP READING — you have enough context. Make a concrete change NOW:\n` +
|
|
659
|
+
` • To ADD or CHANGE content in an existing file → call edit_file with old_string and new_string\n` +
|
|
660
|
+
` • To CREATE a new file → call write_file\n` +
|
|
661
|
+
` • If the task is fully complete → provide a final summary (no more tool calls needed)\n\n` +
|
|
662
|
+
`Do NOT call read_file again until you have made at least one edit_file or write_file call.`,
|
|
663
|
+
});
|
|
664
|
+
break; // exit inner tool loop — model sees the nudge on the next outer iteration
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (!finalContent) {
|
|
669
|
+
finalContent = '[Max iterations reached without a final response]';
|
|
670
|
+
}
|
|
671
|
+
// Update conversation history (trim system prompt from what we store)
|
|
672
|
+
const userMsg = { role: 'user', content: userMessage };
|
|
673
|
+
const assistantMsg = { role: 'assistant', content: finalContent };
|
|
674
|
+
this.history.push(userMsg, assistantMsg);
|
|
675
|
+
// Auto-save after each exchange so conversations persist across sessions
|
|
676
|
+
if (this.conversationManager) {
|
|
677
|
+
await this.conversationManager.autoSave(this.history, this.workspace.getWorkspaceDir(), this.orchestrator);
|
|
678
|
+
}
|
|
679
|
+
return { content: finalContent, toolsUsed, iterations };
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Compact the live messages array when the context window is filling up.
|
|
683
|
+
*
|
|
684
|
+
* Strategy (mirrors opencode's compaction approach):
|
|
685
|
+
* 1. Keep the system/developer prompt (index 0).
|
|
686
|
+
* 2. Identify tool-call + tool-result pairs in the middle of the conversation.
|
|
687
|
+
* 3. Replace bulky tool results with compact "[result summarised]" placeholders.
|
|
688
|
+
* 4. If a ConversationManager is available, generate a structured summary of
|
|
689
|
+
* the condensed middle section and inject it as a single context message.
|
|
690
|
+
*
|
|
691
|
+
* This runs in-loop (mid-turn) so we only compact once per chat() invocation
|
|
692
|
+
* to avoid thrashing (controlled by the `compactedThisTurn` flag in the caller).
|
|
693
|
+
*/
|
|
694
|
+
async compactInLoopMessages(messages, systemPrompt) {
|
|
695
|
+
// Always keep: [0] system/developer prompt + last KEEP_RECENT messages
|
|
696
|
+
const KEEP_RECENT = 10;
|
|
697
|
+
if (messages.length <= KEEP_RECENT + 1) {
|
|
698
|
+
// Nothing meaningful to compact
|
|
699
|
+
return messages;
|
|
700
|
+
}
|
|
701
|
+
const systemMsg = messages[0]; // developer/system prompt
|
|
702
|
+
const recentMessages = messages.slice(-KEEP_RECENT);
|
|
703
|
+
const middleMessages = messages.slice(1, -KEEP_RECENT);
|
|
704
|
+
if (middleMessages.length === 0)
|
|
705
|
+
return messages;
|
|
706
|
+
// Build a structured compaction summary using the opencode template
|
|
707
|
+
const conversationText = middleMessages
|
|
708
|
+
.map((msg) => {
|
|
709
|
+
if (msg.role === 'tool') {
|
|
710
|
+
// Tool results — truncate to first 200 chars to save tokens in the summary prompt
|
|
711
|
+
const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
|
|
712
|
+
return `[Tool result]: ${content.substring(0, 200)}${content.length > 200 ? '...' : ''}`;
|
|
713
|
+
}
|
|
714
|
+
const role = msg.role === 'assistant' ? 'Assistant' : msg.role === 'user' ? 'User' : msg.role;
|
|
715
|
+
const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
|
|
716
|
+
return `${role}: ${content.substring(0, 300)}${content.length > 300 ? '...' : ''}`;
|
|
717
|
+
})
|
|
718
|
+
.join('\n\n');
|
|
719
|
+
const compactionPrompt = `You are summarising a coding session to preserve context for the next agent turn.
|
|
720
|
+
|
|
721
|
+
Provide a structured summary following this exact template:
|
|
722
|
+
|
|
723
|
+
## Goal
|
|
724
|
+
|
|
725
|
+
[What goal(s) is the user trying to accomplish in this session?]
|
|
726
|
+
|
|
727
|
+
## Instructions
|
|
728
|
+
|
|
729
|
+
- [Important instructions the user gave that are still relevant]
|
|
730
|
+
|
|
731
|
+
## Discoveries
|
|
732
|
+
|
|
733
|
+
[Notable things learned during this conversation — file structures, patterns, errors encountered]
|
|
734
|
+
|
|
735
|
+
## Accomplished
|
|
736
|
+
|
|
737
|
+
[Work completed so far, work in progress, work remaining]
|
|
738
|
+
|
|
739
|
+
## Relevant files / directories
|
|
740
|
+
|
|
741
|
+
[Files that have been read, edited, or created — include full paths]
|
|
742
|
+
|
|
743
|
+
---
|
|
744
|
+
|
|
745
|
+
Conversation to summarise:
|
|
746
|
+
${conversationText}
|
|
747
|
+
|
|
748
|
+
Keep the summary concise but complete. Focus on what would help continue the work.`;
|
|
749
|
+
let summaryContent;
|
|
750
|
+
try {
|
|
751
|
+
const summaryResponse = await this.orchestrator.chat({
|
|
752
|
+
messages: [{ role: 'user', content: compactionPrompt }],
|
|
753
|
+
temperature: 0.1,
|
|
754
|
+
maxTokens: 1500,
|
|
755
|
+
});
|
|
756
|
+
summaryContent = summaryResponse.content.trim();
|
|
757
|
+
}
|
|
758
|
+
catch (err) {
|
|
759
|
+
// Fallback: strip middle tool results to their first line only
|
|
760
|
+
logger.warn('[CodeAgent] Compaction summary failed, falling back to tool-result stripping');
|
|
761
|
+
const stripped = middleMessages.map((msg) => {
|
|
762
|
+
if (msg.role === 'tool') {
|
|
763
|
+
const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
|
|
764
|
+
const firstLine = content.split('\n')[0];
|
|
765
|
+
return { ...msg, content: `${firstLine} [... result truncated for context compaction]` };
|
|
766
|
+
}
|
|
767
|
+
return msg;
|
|
768
|
+
});
|
|
769
|
+
return [systemMsg, ...stripped, ...recentMessages];
|
|
770
|
+
}
|
|
771
|
+
const summaryMessage = {
|
|
772
|
+
role: 'user',
|
|
773
|
+
content: `[Context compacted — conversation history summarised to fit within context window]\n\n` +
|
|
774
|
+
summaryContent +
|
|
775
|
+
`\n\n[End of summary — continuing conversation...]`,
|
|
776
|
+
};
|
|
777
|
+
return [systemMsg, summaryMessage, ...recentMessages];
|
|
778
|
+
}
|
|
779
|
+
/** Clean up LSP servers and other resources. */
|
|
780
|
+
async cleanup() {
|
|
781
|
+
// Only shutdown LSP if we own it (not a shared child-agent LSP)
|
|
782
|
+
if (this.depth === 0) {
|
|
783
|
+
await this.lsp.shutdown().catch(() => { });
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
// ─── IAgent interface methods ─────────────────────────────────────────────
|
|
787
|
+
getWorkspace() {
|
|
788
|
+
return this.workspace;
|
|
789
|
+
}
|
|
790
|
+
getMCPManager() {
|
|
791
|
+
// CodeAgent uses in-process tools, not MCP. Return a stub that reflects built-in tools.
|
|
792
|
+
const tools = this.tools;
|
|
793
|
+
return {
|
|
794
|
+
getServerStatus: () => [{
|
|
795
|
+
name: 'code-tools',
|
|
796
|
+
connected: true,
|
|
797
|
+
enabled: true,
|
|
798
|
+
toolCount: tools.length,
|
|
799
|
+
}],
|
|
800
|
+
getClient: () => ({
|
|
801
|
+
getAllTools: () => tools.map((t) => ({ name: t.name, description: t.description })),
|
|
802
|
+
}),
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
resetConversation() {
|
|
806
|
+
this.history = [];
|
|
807
|
+
}
|
|
808
|
+
getConversationHistory() {
|
|
809
|
+
return this.history;
|
|
810
|
+
}
|
|
811
|
+
getConversationManager() {
|
|
812
|
+
return this.conversationManager;
|
|
813
|
+
}
|
|
814
|
+
async saveConversation() {
|
|
815
|
+
if (!this.conversationManager)
|
|
816
|
+
return null;
|
|
817
|
+
return this.conversationManager.saveConversation(this.history, this.workspace.getWorkspaceDir(), undefined, this.orchestrator);
|
|
818
|
+
}
|
|
819
|
+
async loadConversation(id) {
|
|
820
|
+
if (!this.conversationManager)
|
|
821
|
+
return;
|
|
822
|
+
const conversation = await this.conversationManager.loadConversation(id);
|
|
823
|
+
if (conversation) {
|
|
824
|
+
this.history = conversation.messages;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
async listConversations() {
|
|
828
|
+
if (!this.conversationManager)
|
|
829
|
+
return [];
|
|
830
|
+
return this.conversationManager.listConversations();
|
|
831
|
+
}
|
|
832
|
+
/** Get the code mode indicator for UI display. */
|
|
833
|
+
static get indicator() {
|
|
834
|
+
return CODE_MODE_INDICATOR;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
//# sourceMappingURL=agent.js.map
|