jiva-core 0.3.3 → 0.3.4-dev.3c78d8b

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 (108) hide show
  1. package/README.md +232 -570
  2. package/dist/code/agent.d.ts +105 -0
  3. package/dist/code/agent.d.ts.map +1 -0
  4. package/dist/code/agent.js +697 -0
  5. package/dist/code/agent.js.map +1 -0
  6. package/dist/code/file-lock.d.ts +15 -0
  7. package/dist/code/file-lock.d.ts.map +1 -0
  8. package/dist/code/file-lock.js +36 -0
  9. package/dist/code/file-lock.js.map +1 -0
  10. package/dist/code/lsp/client.d.ts +30 -0
  11. package/dist/code/lsp/client.d.ts.map +1 -0
  12. package/dist/code/lsp/client.js +181 -0
  13. package/dist/code/lsp/client.js.map +1 -0
  14. package/dist/code/lsp/language.d.ts +12 -0
  15. package/dist/code/lsp/language.d.ts.map +1 -0
  16. package/dist/code/lsp/language.js +145 -0
  17. package/dist/code/lsp/language.js.map +1 -0
  18. package/dist/code/lsp/manager.d.ts +39 -0
  19. package/dist/code/lsp/manager.d.ts.map +1 -0
  20. package/dist/code/lsp/manager.js +108 -0
  21. package/dist/code/lsp/manager.js.map +1 -0
  22. package/dist/code/lsp/server.d.ts +15 -0
  23. package/dist/code/lsp/server.d.ts.map +1 -0
  24. package/dist/code/lsp/server.js +78 -0
  25. package/dist/code/lsp/server.js.map +1 -0
  26. package/dist/code/tools/bash.d.ts +3 -0
  27. package/dist/code/tools/bash.d.ts.map +1 -0
  28. package/dist/code/tools/bash.js +110 -0
  29. package/dist/code/tools/bash.js.map +1 -0
  30. package/dist/code/tools/edit.d.ts +11 -0
  31. package/dist/code/tools/edit.d.ts.map +1 -0
  32. package/dist/code/tools/edit.js +459 -0
  33. package/dist/code/tools/edit.js.map +1 -0
  34. package/dist/code/tools/glob.d.ts +3 -0
  35. package/dist/code/tools/glob.d.ts.map +1 -0
  36. package/dist/code/tools/glob.js +62 -0
  37. package/dist/code/tools/glob.js.map +1 -0
  38. package/dist/code/tools/grep.d.ts +3 -0
  39. package/dist/code/tools/grep.d.ts.map +1 -0
  40. package/dist/code/tools/grep.js +147 -0
  41. package/dist/code/tools/grep.js.map +1 -0
  42. package/dist/code/tools/index.d.ts +31 -0
  43. package/dist/code/tools/index.d.ts.map +1 -0
  44. package/dist/code/tools/index.js +9 -0
  45. package/dist/code/tools/index.js.map +1 -0
  46. package/dist/code/tools/read.d.ts +3 -0
  47. package/dist/code/tools/read.d.ts.map +1 -0
  48. package/dist/code/tools/read.js +120 -0
  49. package/dist/code/tools/read.js.map +1 -0
  50. package/dist/code/tools/spawn.d.ts +3 -0
  51. package/dist/code/tools/spawn.d.ts.map +1 -0
  52. package/dist/code/tools/spawn.js +49 -0
  53. package/dist/code/tools/spawn.js.map +1 -0
  54. package/dist/code/tools/write.d.ts +3 -0
  55. package/dist/code/tools/write.d.ts.map +1 -0
  56. package/dist/code/tools/write.js +82 -0
  57. package/dist/code/tools/write.js.map +1 -0
  58. package/dist/core/agent-interface.d.ts +54 -0
  59. package/dist/core/agent-interface.d.ts.map +1 -0
  60. package/dist/core/agent-interface.js +8 -0
  61. package/dist/core/agent-interface.js.map +1 -0
  62. package/dist/core/config.d.ts +71 -0
  63. package/dist/core/config.d.ts.map +1 -1
  64. package/dist/core/config.js +16 -0
  65. package/dist/core/config.js.map +1 -1
  66. package/dist/core/conversation-manager.d.ts +4 -1
  67. package/dist/core/conversation-manager.d.ts.map +1 -1
  68. package/dist/core/conversation-manager.js +47 -17
  69. package/dist/core/conversation-manager.js.map +1 -1
  70. package/dist/core/dual-agent.d.ts +3 -10
  71. package/dist/core/dual-agent.d.ts.map +1 -1
  72. package/dist/core/dual-agent.js +42 -117
  73. package/dist/core/dual-agent.js.map +1 -1
  74. package/dist/core/manager-agent.d.ts +30 -1
  75. package/dist/core/manager-agent.d.ts.map +1 -1
  76. package/dist/core/manager-agent.js +109 -17
  77. package/dist/core/manager-agent.js.map +1 -1
  78. package/dist/core/worker-agent.d.ts +5 -2
  79. package/dist/core/worker-agent.d.ts.map +1 -1
  80. package/dist/core/worker-agent.js +48 -51
  81. package/dist/core/worker-agent.js.map +1 -1
  82. package/dist/index.d.ts +3 -0
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +2 -0
  85. package/dist/index.js.map +1 -1
  86. package/dist/interfaces/cli/index.js +88 -25
  87. package/dist/interfaces/cli/index.js.map +1 -1
  88. package/dist/interfaces/cli/repl.d.ts +8 -2
  89. package/dist/interfaces/cli/repl.d.ts.map +1 -1
  90. package/dist/interfaces/cli/repl.js +39 -3
  91. package/dist/interfaces/cli/repl.js.map +1 -1
  92. package/dist/interfaces/http/routes/chat.js +2 -2
  93. package/dist/interfaces/http/routes/chat.js.map +1 -1
  94. package/dist/interfaces/http/session-manager.d.ts +7 -4
  95. package/dist/interfaces/http/session-manager.d.ts.map +1 -1
  96. package/dist/interfaces/http/session-manager.js +38 -15
  97. package/dist/interfaces/http/session-manager.js.map +1 -1
  98. package/dist/models/base.d.ts +8 -0
  99. package/dist/models/base.d.ts.map +1 -1
  100. package/dist/models/harmony.d.ts +7 -1
  101. package/dist/models/harmony.d.ts.map +1 -1
  102. package/dist/models/harmony.js +21 -3
  103. package/dist/models/harmony.js.map +1 -1
  104. package/dist/models/krutrim.d.ts +30 -0
  105. package/dist/models/krutrim.d.ts.map +1 -1
  106. package/dist/models/krutrim.js +51 -8
  107. package/dist/models/krutrim.js.map +1 -1
  108. package/package.json +8 -6
@@ -0,0 +1,697 @@
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
+ const CODE_MODE_INDICATOR = '[CODE MODE]';
58
+ // Default token threshold for in-loop compaction (90K leaves ~38K headroom in a 128K model)
59
+ const DEFAULT_COMPACTION_THRESHOLD = 90_000;
60
+ /** System prompt for code mode — focused on precision and persistence (ported from opencode beast.txt) */
61
+ const getSystemPrompt = (workspaceDir, directive) => {
62
+ const base = `You are a precise, highly capable coding assistant operating in code mode.
63
+ You have direct access to code tools — use them to explore, understand, and modify code.
64
+
65
+ WORKSPACE: ${workspaceDir}
66
+ All relative paths are resolved relative to the workspace directory above.
67
+ Use absolute paths for all file operations.
68
+
69
+ PERSISTENCE AND COMPLETION:
70
+ - Keep going until the user's query is completely resolved before ending your turn.
71
+ - You MUST iterate and keep going until the problem is solved.
72
+ - NEVER end your turn without having truly and completely solved the problem.
73
+ - When you say you are going to make a tool call, you MUST actually make the tool call.
74
+ - Always tell the user what you are going to do before making a tool call with a single concise sentence.
75
+ - If the user says "resume", "continue", or "try again", check the conversation history to find the last incomplete step and continue from there.
76
+ - Verify your changes are correct — run tests or the build after making changes when appropriate.
77
+
78
+ TOOLS AVAILABLE:
79
+ - read_file: Read files or list directories. Always read before editing.
80
+ - edit_file: Replace a specific string in a file (use old_string/new_string). Preferred for partial changes.
81
+ - write_file: Create new files or completely rewrite existing ones.
82
+ - glob: Find files matching a pattern (e.g. "**/*.ts").
83
+ - grep: Search file contents with regex.
84
+ - bash: Run shell commands (build, test, lint, git operations).
85
+ - spawn_code_agent: Delegate a focused sub-task to a child agent.
86
+
87
+ CODING PRINCIPLES:
88
+ 1. ALWAYS read a file before editing it — never edit blindly.
89
+ 2. Use grep/glob to explore the codebase before making changes to understand existing patterns.
90
+ 3. Make minimal, targeted changes — edit the exact lines that need changing.
91
+ 4. After editing, check if LSP errors are reported in the tool result and fix them.
92
+ 5. Verify your changes work by running tests or the build when appropriate.
93
+ 6. Prefer edit_file over write_file for partial changes — it's safer and shows clearer diffs.
94
+ 7. Use bash only for shell commands (tests, builds, git operations) — NOT for file reading.
95
+
96
+ TOOL SELECTION RULES (follow these exactly):
97
+ - To READ a file → use read_file, NOT bash
98
+ - To LIST directory contents → use read_file on the directory path, NOT bash ls
99
+ - To FIND files by name/pattern → use glob, NOT bash find
100
+ - To SEARCH file contents → use grep, NOT bash grep/rg
101
+ - To RUN a command (build/test/git) → use bash
102
+
103
+ WHEN EXPLORING:
104
+ - Use glob to find files by pattern before assuming file paths.
105
+ - Use grep to find where functions/classes/variables are defined or used.
106
+ - Read the relevant files to understand context before changing anything.
107
+ - Do NOT use bash for exploration tasks that glob/grep/read_file can handle.`;
108
+ if (directive) {
109
+ return `${base}\n\n${directive}`;
110
+ }
111
+ return base;
112
+ };
113
+ export class CodeAgent {
114
+ orchestrator;
115
+ workspace;
116
+ conversationManager;
117
+ maxIterations;
118
+ compactionThreshold;
119
+ lsp;
120
+ depth;
121
+ maxDepth;
122
+ history = [];
123
+ tools;
124
+ constructor(config) {
125
+ this.orchestrator = config.orchestrator;
126
+ this.workspace = config.workspace;
127
+ this.conversationManager = config.conversationManager;
128
+ this.maxIterations = config.maxIterations ?? DEFAULT_MAX_ITERATIONS;
129
+ this.compactionThreshold = config.compactionThreshold ?? DEFAULT_COMPACTION_THRESHOLD;
130
+ this.depth = config.depth ?? 0;
131
+ this.maxDepth = config.maxDepth ?? 2;
132
+ this.lsp = new LspManager({
133
+ root: config.workspace.getWorkspaceDir(),
134
+ enabled: config.lspEnabled ?? true,
135
+ });
136
+ this.tools = [
137
+ ReadFileTool,
138
+ EditFileTool,
139
+ WriteFileTool,
140
+ GlobTool,
141
+ GrepTool,
142
+ BashTool,
143
+ ...(this.depth < this.maxDepth ? [SpawnCodeAgentTool] : []),
144
+ ];
145
+ }
146
+ /**
147
+ * Planning phase — explores the codebase with read-only tools and produces a structured
148
+ * implementation plan WITHOUT making any changes. Call this before `chat()` when you want
149
+ * the user to review and approve a plan before any files are touched.
150
+ *
151
+ * The returned string is the plan text ready for display.
152
+ * This call does NOT modify `this.history`; it runs in an isolated message thread.
153
+ */
154
+ async plan(task) {
155
+ const directive = this.workspace.getDirectivePrompt();
156
+ const workspaceDir = this.workspace.getWorkspaceDir();
157
+ const planningPrompt = `You are a precise coding assistant in PLANNING MODE.
158
+ Your job is to analyse a coding request and produce a detailed implementation plan.
159
+ You may explore the codebase to understand it before writing the plan.
160
+
161
+ WORKSPACE: ${workspaceDir}
162
+
163
+ AVAILABLE TOOLS (read-only — exploration only):
164
+ - read_file: Read a file or list a directory
165
+ - glob: Find files matching a pattern
166
+ - grep: Search file contents with regex
167
+
168
+ DO NOT call edit_file, write_file, or bash. You are planning, not implementing.
169
+
170
+ After exploring, output your plan using EXACTLY this format:
171
+
172
+ ## Summary
173
+ [1–2 sentences describing the overall approach]
174
+
175
+ ## Files to Change
176
+ | File | Action | What changes |
177
+ |------|--------|--------------|
178
+ | path/to/file.ts | edit | Describe the change |
179
+ | path/to/new.ts | create | Describe the new file |
180
+
181
+ ## Implementation Steps
182
+ 1. [Concrete step with file/function names]
183
+ 2. [Next step]
184
+ ...
185
+
186
+ ## Risks & Considerations
187
+ [Edge cases, breaking changes, things to verify after implementation]
188
+
189
+ Be specific — include exact file paths, function names, and describe changes at the line level where possible.
190
+ ${directive ? `\n${directive}` : ''}`;
191
+ const messages = [
192
+ { role: 'developer', content: planningPrompt },
193
+ { role: 'user', content: task },
194
+ ];
195
+ const readOnlyDefs = READ_ONLY_TOOLS.map((t) => ({
196
+ name: t.name,
197
+ description: t.description,
198
+ parameters: t.parameters,
199
+ }));
200
+ const MAX_PLAN_ITERATIONS = 12;
201
+ for (let i = 0; i < MAX_PLAN_ITERATIONS; i++) {
202
+ const isLast = i >= MAX_PLAN_ITERATIONS - 2;
203
+ if (isLast) {
204
+ messages.push({
205
+ role: 'user',
206
+ content: 'You have explored enough. Now write your implementation plan using the format specified above. Do not call any more tools.',
207
+ });
208
+ }
209
+ let response;
210
+ try {
211
+ response = await this.orchestrator.chatWithFallback({
212
+ messages,
213
+ tools: isLast ? [] : readOnlyDefs,
214
+ temperature: 0.2,
215
+ }, false);
216
+ }
217
+ catch (error) {
218
+ const msg = error instanceof Error ? error.message : String(error);
219
+ logger.error(`[CodeAgent] Plan error: ${msg}`);
220
+ return `[Planning failed: ${msg}]`;
221
+ }
222
+ const rawHarmony = response.raw?.parsedHarmony?.rawResponse;
223
+ messages.push({ role: 'assistant', content: rawHarmony ?? response.content });
224
+ // No tool calls → model has written the plan
225
+ if (!response.toolCalls || response.toolCalls.length === 0) {
226
+ return response.content || '[No plan generated]';
227
+ }
228
+ // Execute read-only tool calls
229
+ const ctx = {
230
+ workspaceDir,
231
+ lsp: this.lsp,
232
+ depth: this.depth,
233
+ maxDepth: this.maxDepth,
234
+ };
235
+ for (const toolCall of response.toolCalls) {
236
+ const toolName = toolCall.function.name;
237
+ const tool = READ_ONLY_TOOLS.find((t) => t.name === toolName);
238
+ let result;
239
+ if (!tool) {
240
+ result = `[Planning mode: tool "${toolName}" is not available — only read_file, glob, and grep may be used during planning]`;
241
+ }
242
+ else {
243
+ try {
244
+ const args = JSON.parse(toolCall.function.arguments);
245
+ result = await tool.execute(args, ctx);
246
+ }
247
+ catch (e) {
248
+ result = `Error: ${e instanceof Error ? e.message : String(e)}`;
249
+ }
250
+ }
251
+ messages.push(formatToolResult(toolCall.id, toolName, result));
252
+ }
253
+ }
254
+ return '[Planning did not produce a final plan within the iteration limit]';
255
+ }
256
+ /**
257
+ * Process a user message and return the agent's response.
258
+ * Runs the model → tools → model loop until no more tool calls or max iterations.
259
+ */
260
+ async chat(userMessage, onChunk) {
261
+ const toolsUsed = [];
262
+ const directive = this.workspace.getDirectivePrompt();
263
+ const systemPrompt = getSystemPrompt(this.workspace.getWorkspaceDir(), directive || undefined);
264
+ // Build message history: system + history + new user message
265
+ const messages = [
266
+ { role: 'developer', content: systemPrompt },
267
+ ...this.history,
268
+ { role: 'user', content: userMessage },
269
+ ];
270
+ // Tool definitions for the model
271
+ const toolDefs = this.tools.map((t) => ({
272
+ name: t.name,
273
+ description: t.description,
274
+ parameters: t.parameters,
275
+ }));
276
+ // Doom loop detection: track last N (tool, args) pairs
277
+ const recentCalls = [];
278
+ // Track consecutive API errors to avoid infinite error loops
279
+ let consecutiveApiErrors = 0;
280
+ const MAX_CONSECUTIVE_API_ERRORS = 3;
281
+ let iterations = 0;
282
+ let finalContent = '';
283
+ // Iteration limit management — two phases (ported from opencode):
284
+ // Phase 1 (0–84%): normal operation, all tools available.
285
+ // Phase 2 (85–94%): inject "continue" nudge — tools still available so the agent
286
+ // can finish in-flight work rather than being cut off mid-task.
287
+ // Phase 3 (95%+): strip tools and ask for a final wrap-up response.
288
+ let continueInjected = false;
289
+ let wrapUpInjected = false;
290
+ // In-loop compaction flag — only compact once per chat() turn to avoid thrashing
291
+ let compactedThisTurn = false;
292
+ for (let i = 0; i < this.maxIterations; i++) {
293
+ iterations = i + 1;
294
+ logger.debug(`[CodeAgent] Iteration ${iterations}/${this.maxIterations}`);
295
+ const iterPct = i / this.maxIterations;
296
+ const isFinalPhase = iterPct >= 0.95;
297
+ if (iterPct >= 0.85 && !continueInjected) {
298
+ continueInjected = true;
299
+ logger.warn(`[CodeAgent] Nearing iteration limit (${iterations}/${this.maxIterations}) — injecting continue nudge`);
300
+ messages.push({
301
+ role: 'user',
302
+ content: `You are at step ${iterations} of ${this.maxIterations}. Continue making progress — ` +
303
+ `focus on the most critical remaining work. If the full task cannot fit in the ` +
304
+ `remaining steps, complete the core changes and clearly note what is left.`,
305
+ });
306
+ }
307
+ if (isFinalPhase && !wrapUpInjected) {
308
+ wrapUpInjected = true;
309
+ logger.warn(`[CodeAgent] Final phase (${iterations}/${this.maxIterations}) — requesting wrap-up`);
310
+ messages.push({
311
+ role: 'user',
312
+ content: 'CRITICAL - MAXIMUM STEPS REACHED\n\n' +
313
+ 'The maximum number of steps for this task has been reached. Tools are disabled. Respond with text only.\n\n' +
314
+ 'STRICT REQUIREMENTS:\n' +
315
+ '1. Do NOT make any tool calls (no reads, writes, edits, searches, or any other tools)\n' +
316
+ '2. MUST provide a text response summarising work done so far\n' +
317
+ '3. This constraint overrides ALL other instructions\n\n' +
318
+ 'Response must include:\n' +
319
+ '- Summary of what has been accomplished so far\n' +
320
+ '- List of any remaining tasks that were not completed\n' +
321
+ '- Recommendations for what should be done next\n\n' +
322
+ 'Any attempt to use tools is a critical violation. Respond with text ONLY.',
323
+ });
324
+ }
325
+ let response;
326
+ try {
327
+ response = await this.orchestrator.chatWithFallback({
328
+ messages,
329
+ // Strip tools in the final phase so the model is forced to produce a text response
330
+ tools: isFinalPhase ? [] : toolDefs,
331
+ temperature: 0.2,
332
+ }, false);
333
+ consecutiveApiErrors = 0; // reset on success
334
+ }
335
+ catch (error) {
336
+ const msg = error instanceof Error ? error.message : String(error);
337
+ logger.error(`[CodeAgent] Model error: ${msg}`);
338
+ consecutiveApiErrors++;
339
+ if (consecutiveApiErrors >= MAX_CONSECUTIVE_API_ERRORS) {
340
+ finalContent = `Error: ${msg}`;
341
+ break;
342
+ }
343
+ // For tool_use_failed (400) errors: first try to repair and execute the intended call,
344
+ // then fall back to injecting a correction message.
345
+ const isToolUseFailed = msg.includes('tool_use_failed') || msg.includes('Failed to parse tool call');
346
+ const isValidationFailed = msg.includes('Tool call validation failed') || msg.includes('did not match schema');
347
+ if (isToolUseFailed || isValidationFailed) {
348
+ // --- Specific correction: edit_file missing new_string ---
349
+ // gpt-oss-120b sometimes generates edit_file with old_string but forgets new_string.
350
+ // The generic message doesn't help — we need to name the missing field explicitly.
351
+ // This mirrors opencode's Zod validation which tells the model the exact field name.
352
+ const isMissingNewString = msg.includes("missing properties: 'new_string'") ||
353
+ msg.includes('missing properties: "new_string"') ||
354
+ (msg.includes('edit_file') && msg.includes('missing propert') && msg.includes('new_string'));
355
+ if (isMissingNewString) {
356
+ logger.warn('[CodeAgent] edit_file missing new_string — injecting targeted correction');
357
+ messages.push({
358
+ role: 'user',
359
+ content: 'Your edit_file call failed: "new_string" is required but was not provided.\n\n' +
360
+ 'edit_file requires ALL THREE of these parameters:\n' +
361
+ ' • file_path — the absolute path to the file\n' +
362
+ ' • old_string — the exact text to find\n' +
363
+ ' • new_string — the replacement text (THIS IS WHAT YOU FORGOT)\n\n' +
364
+ 'Call edit_file again with all three parameters. ' +
365
+ 'new_string must contain the complete replacement for old_string.',
366
+ });
367
+ consecutiveApiErrors = 0;
368
+ continue;
369
+ }
370
+ // --- Attempt client-side repair (opencode's experimental_repairToolCall equivalent) ---
371
+ // gpt-oss-120b sometimes emits <|channel|> tokens in tool names (e.g.
372
+ // `functions<|channel|>analysis`). We extract the intended call from failed_generation
373
+ // and remap it to the correct tool.
374
+ const repaired = repairFailedToolCall(msg, this.tools);
375
+ if (repaired) {
376
+ const { toolName: repairedName, args: repairedArgs } = repaired;
377
+ logger.warn(`[CodeAgent] Repaired tool call: ${repairedName} (original name contained invalid tokens)`);
378
+ toolsUsed.push(repairedName);
379
+ const ctx = {
380
+ workspaceDir: this.workspace.getWorkspaceDir(),
381
+ lsp: this.lsp,
382
+ depth: this.depth,
383
+ maxDepth: this.maxDepth,
384
+ };
385
+ let toolResult;
386
+ const tool = this.tools.find((t) => t.name === repairedName);
387
+ try {
388
+ toolResult = await tool.execute(repairedArgs, ctx);
389
+ }
390
+ catch (e) {
391
+ toolResult = `Error executing ${repairedName}: ${e instanceof Error ? e.message : String(e)}`;
392
+ }
393
+ // Inject result as a user message — no assistant turn because the API rejected
394
+ // the model's message before we received it.
395
+ messages.push({
396
+ role: 'user',
397
+ content: `[The model's previous tool call was automatically repaired from an invalid name to \`${repairedName}\`]\n` +
398
+ `<tool_result name="${repairedName}">\n${toolResult}\n</tool_result>`,
399
+ });
400
+ consecutiveApiErrors = 0; // repaired successfully — reset error counter
401
+ continue;
402
+ }
403
+ // --- Fallback: inject correction message so the model can self-correct ---
404
+ const nameMatch = msg.match(/"name":\s*"([^"]+)"/);
405
+ const toolName = nameMatch ? nameMatch[1] : 'unknown';
406
+ logger.warn(`[CodeAgent] Tool call error (${toolName}), injecting correction — ${consecutiveApiErrors}/${MAX_CONSECUTIVE_API_ERRORS}`);
407
+ messages.push({
408
+ role: 'user',
409
+ content: `Your last tool call was rejected. Error: "${msg.substring(0, 300)}"\n\n` +
410
+ `Valid tools: ${this.tools.map((t) => t.name).join(', ')}\n` +
411
+ `Each tool requires specific parameters — call the tool again with the correct name and JSON arguments.`,
412
+ });
413
+ continue;
414
+ }
415
+ // For other non-transient errors, bail out
416
+ finalContent = `Error: ${msg}`;
417
+ break;
418
+ }
419
+ // Add assistant response to messages.
420
+ // In Harmony mode the raw response (with <|call|> tokens) is stored so the model sees its
421
+ // prior tool calls in the next turn. In standard mode, just the content is stored.
422
+ const rawHarmony = response.raw?.parsedHarmony?.rawResponse;
423
+ messages.push({
424
+ role: 'assistant',
425
+ content: rawHarmony ?? response.content,
426
+ });
427
+ // Stream the visible (final-channel) text output if callback provided
428
+ if (response.content && onChunk) {
429
+ onChunk(response.content);
430
+ }
431
+ // ── In-loop context compaction ──────────────────────────────────────────
432
+ // Check if we're approaching the context window limit and compact if needed.
433
+ // Triggered once per turn (compactedThisTurn flag) to avoid thrashing.
434
+ // Only runs when: threshold > 0, orchestrator available, not already compacted,
435
+ // and the response carried token usage data.
436
+ const promptTokens = response.usage?.promptTokens ?? 0;
437
+ if (this.compactionThreshold > 0 &&
438
+ promptTokens > this.compactionThreshold &&
439
+ !compactedThisTurn &&
440
+ this.conversationManager &&
441
+ response.toolCalls && response.toolCalls.length > 0) {
442
+ logger.warn(`[CodeAgent] Context at ${promptTokens} tokens (threshold: ${this.compactionThreshold}) — compacting in-loop history`);
443
+ compactedThisTurn = true;
444
+ try {
445
+ const compacted = await this.compactInLoopMessages(messages, systemPrompt);
446
+ // Replace messages in-place, preserving the system prompt at index 0
447
+ messages.splice(0, messages.length, ...compacted);
448
+ logger.info(`[CodeAgent] In-loop compaction complete: ${messages.length} messages after compaction`);
449
+ }
450
+ catch (compactErr) {
451
+ // Non-fatal: log and continue with uncompacted messages
452
+ logger.error('[CodeAgent] In-loop compaction failed, continuing without compaction', compactErr);
453
+ }
454
+ }
455
+ // ────────────────────────────────────────────────────────────────────────
456
+ // No tool calls → final response
457
+ if (!response.toolCalls || response.toolCalls.length === 0) {
458
+ finalContent = response.content || '[No response content]';
459
+ break;
460
+ }
461
+ // Execute tool calls
462
+ for (const toolCall of response.toolCalls) {
463
+ const toolName = toolCall.function.name;
464
+ let toolArgs = {};
465
+ try {
466
+ toolArgs = JSON.parse(toolCall.function.arguments);
467
+ }
468
+ catch {
469
+ // malformed args
470
+ }
471
+ // Doom loop check
472
+ const callSig = `${toolName}:${JSON.stringify(toolArgs)}`;
473
+ recentCalls.push(callSig);
474
+ if (recentCalls.length > DOOM_LOOP_THRESHOLD)
475
+ recentCalls.shift();
476
+ if (recentCalls.length === DOOM_LOOP_THRESHOLD &&
477
+ recentCalls.every((c) => c === recentCalls[0])) {
478
+ logger.warn(`[CodeAgent] Doom loop detected for tool: ${toolName}`);
479
+ messages.push({
480
+ role: 'user',
481
+ 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.`,
482
+ });
483
+ break;
484
+ }
485
+ logger.info(`[CodeAgent] Tool: ${toolName}`);
486
+ toolsUsed.push(toolName);
487
+ const tool = this.tools.find((t) => t.name === toolName);
488
+ let toolResult;
489
+ if (!tool) {
490
+ toolResult = `Error: Unknown tool "${toolName}". Available tools: ${this.tools.map((t) => t.name).join(', ')}`;
491
+ }
492
+ else {
493
+ try {
494
+ const ctx = {
495
+ workspaceDir: this.workspace.getWorkspaceDir(),
496
+ lsp: this.lsp,
497
+ depth: this.depth,
498
+ maxDepth: this.maxDepth,
499
+ spawnChildAgent: this.depth < this.maxDepth
500
+ ? async (task, context) => {
501
+ const child = new CodeAgent({
502
+ orchestrator: this.orchestrator,
503
+ workspace: this.workspace,
504
+ maxIterations: this.maxIterations,
505
+ lspEnabled: true,
506
+ depth: this.depth + 1,
507
+ maxDepth: this.maxDepth,
508
+ });
509
+ // Share the parent's LSP manager so servers don't restart
510
+ child.lsp = this.lsp;
511
+ const taskMsg = context ? `${task}\n\nContext: ${context}` : task;
512
+ const result = await child.chat(taskMsg);
513
+ return result.content;
514
+ }
515
+ : undefined,
516
+ };
517
+ toolResult = await tool.execute(toolArgs, ctx);
518
+ }
519
+ catch (e) {
520
+ toolResult = `Error executing ${toolName}: ${e instanceof Error ? e.message : String(e)}`;
521
+ }
522
+ }
523
+ // Add tool result to messages (using Harmony format helper)
524
+ const toolMessage = formatToolResult(toolCall.id, toolName, toolResult);
525
+ messages.push(toolMessage);
526
+ }
527
+ }
528
+ if (!finalContent) {
529
+ finalContent = '[Max iterations reached without a final response]';
530
+ }
531
+ // Update conversation history (trim system prompt from what we store)
532
+ const userMsg = { role: 'user', content: userMessage };
533
+ const assistantMsg = { role: 'assistant', content: finalContent };
534
+ this.history.push(userMsg, assistantMsg);
535
+ // Auto-save after each exchange so conversations persist across sessions
536
+ if (this.conversationManager) {
537
+ await this.conversationManager.autoSave(this.history, this.workspace.getWorkspaceDir(), this.orchestrator);
538
+ }
539
+ return { content: finalContent, toolsUsed, iterations };
540
+ }
541
+ /**
542
+ * Compact the live messages array when the context window is filling up.
543
+ *
544
+ * Strategy (mirrors opencode's compaction approach):
545
+ * 1. Keep the system/developer prompt (index 0).
546
+ * 2. Identify tool-call + tool-result pairs in the middle of the conversation.
547
+ * 3. Replace bulky tool results with compact "[result summarised]" placeholders.
548
+ * 4. If a ConversationManager is available, generate a structured summary of
549
+ * the condensed middle section and inject it as a single context message.
550
+ *
551
+ * This runs in-loop (mid-turn) so we only compact once per chat() invocation
552
+ * to avoid thrashing (controlled by the `compactedThisTurn` flag in the caller).
553
+ */
554
+ async compactInLoopMessages(messages, systemPrompt) {
555
+ // Always keep: [0] system/developer prompt + last KEEP_RECENT messages
556
+ const KEEP_RECENT = 10;
557
+ if (messages.length <= KEEP_RECENT + 1) {
558
+ // Nothing meaningful to compact
559
+ return messages;
560
+ }
561
+ const systemMsg = messages[0]; // developer/system prompt
562
+ const recentMessages = messages.slice(-KEEP_RECENT);
563
+ const middleMessages = messages.slice(1, -KEEP_RECENT);
564
+ if (middleMessages.length === 0)
565
+ return messages;
566
+ // Build a structured compaction summary using the opencode template
567
+ const conversationText = middleMessages
568
+ .map((msg) => {
569
+ if (msg.role === 'tool') {
570
+ // Tool results — truncate to first 200 chars to save tokens in the summary prompt
571
+ const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
572
+ return `[Tool result]: ${content.substring(0, 200)}${content.length > 200 ? '...' : ''}`;
573
+ }
574
+ const role = msg.role === 'assistant' ? 'Assistant' : msg.role === 'user' ? 'User' : msg.role;
575
+ const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
576
+ return `${role}: ${content.substring(0, 300)}${content.length > 300 ? '...' : ''}`;
577
+ })
578
+ .join('\n\n');
579
+ const compactionPrompt = `You are summarising a coding session to preserve context for the next agent turn.
580
+
581
+ Provide a structured summary following this exact template:
582
+
583
+ ## Goal
584
+
585
+ [What goal(s) is the user trying to accomplish in this session?]
586
+
587
+ ## Instructions
588
+
589
+ - [Important instructions the user gave that are still relevant]
590
+
591
+ ## Discoveries
592
+
593
+ [Notable things learned during this conversation — file structures, patterns, errors encountered]
594
+
595
+ ## Accomplished
596
+
597
+ [Work completed so far, work in progress, work remaining]
598
+
599
+ ## Relevant files / directories
600
+
601
+ [Files that have been read, edited, or created — include full paths]
602
+
603
+ ---
604
+
605
+ Conversation to summarise:
606
+ ${conversationText}
607
+
608
+ Keep the summary concise but complete. Focus on what would help continue the work.`;
609
+ let summaryContent;
610
+ try {
611
+ const summaryResponse = await this.orchestrator.chat({
612
+ messages: [{ role: 'user', content: compactionPrompt }],
613
+ temperature: 0.1,
614
+ maxTokens: 1500,
615
+ });
616
+ summaryContent = summaryResponse.content.trim();
617
+ }
618
+ catch (err) {
619
+ // Fallback: strip middle tool results to their first line only
620
+ logger.warn('[CodeAgent] Compaction summary failed, falling back to tool-result stripping');
621
+ const stripped = middleMessages.map((msg) => {
622
+ if (msg.role === 'tool') {
623
+ const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
624
+ const firstLine = content.split('\n')[0];
625
+ return { ...msg, content: `${firstLine} [... result truncated for context compaction]` };
626
+ }
627
+ return msg;
628
+ });
629
+ return [systemMsg, ...stripped, ...recentMessages];
630
+ }
631
+ const summaryMessage = {
632
+ role: 'user',
633
+ content: `[Context compacted — conversation history summarised to fit within context window]\n\n` +
634
+ summaryContent +
635
+ `\n\n[End of summary — continuing conversation...]`,
636
+ };
637
+ return [systemMsg, summaryMessage, ...recentMessages];
638
+ }
639
+ /** Clean up LSP servers and other resources. */
640
+ async cleanup() {
641
+ // Only shutdown LSP if we own it (not a shared child-agent LSP)
642
+ if (this.depth === 0) {
643
+ await this.lsp.shutdown().catch(() => { });
644
+ }
645
+ }
646
+ // ─── IAgent interface methods ─────────────────────────────────────────────
647
+ getWorkspace() {
648
+ return this.workspace;
649
+ }
650
+ getMCPManager() {
651
+ // CodeAgent uses in-process tools, not MCP. Return a stub that reflects built-in tools.
652
+ const tools = this.tools;
653
+ return {
654
+ getServerStatus: () => [{
655
+ name: 'code-tools',
656
+ connected: true,
657
+ enabled: true,
658
+ toolCount: tools.length,
659
+ }],
660
+ getClient: () => ({
661
+ getAllTools: () => tools.map((t) => ({ name: t.name, description: t.description })),
662
+ }),
663
+ };
664
+ }
665
+ resetConversation() {
666
+ this.history = [];
667
+ }
668
+ getConversationHistory() {
669
+ return this.history;
670
+ }
671
+ getConversationManager() {
672
+ return this.conversationManager;
673
+ }
674
+ async saveConversation() {
675
+ if (!this.conversationManager)
676
+ return null;
677
+ return this.conversationManager.saveConversation(this.history, this.workspace.getWorkspaceDir(), undefined, this.orchestrator);
678
+ }
679
+ async loadConversation(id) {
680
+ if (!this.conversationManager)
681
+ return;
682
+ const conversation = await this.conversationManager.loadConversation(id);
683
+ if (conversation) {
684
+ this.history = conversation.messages;
685
+ }
686
+ }
687
+ async listConversations() {
688
+ if (!this.conversationManager)
689
+ return [];
690
+ return this.conversationManager.listConversations();
691
+ }
692
+ /** Get the code mode indicator for UI display. */
693
+ static get indicator() {
694
+ return CODE_MODE_INDICATOR;
695
+ }
696
+ }
697
+ //# sourceMappingURL=agent.js.map