Skip to content

Commit aa223c1

Browse files
committed
feat(agent): implement customizable output rendering for CodingAgent #453
1 parent 723ca71 commit aa223c1

File tree

9 files changed

+963
-312
lines changed

9 files changed

+963
-312
lines changed

mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/CodingAgent.kt

Lines changed: 286 additions & 146 deletions
Large diffs are not rendered by default.

mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/CodingAgentTemplate.kt

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,28 @@ ${'$'}{toolList}
4141
4. **Test Your Changes**: Run tests or build commands to verify changes
4242
5. **Signal Completion**: When done, respond with "TASK_COMPLETE" in your message
4343
44+
## IMPORTANT: One Tool Per Response
45+
46+
**You MUST execute ONLY ONE tool per response.** Do not include multiple tool calls in a single response.
47+
48+
- ✅ CORRECT: One <devin> block with ONE tool call
49+
- ❌ WRONG: Multiple <devin> blocks or multiple tools in one block
50+
51+
After each tool execution, you will see the result and can decide the next step.
52+
4453
## Response Format
4554
4655
For each step, respond with:
47-
1. Your reasoning about what to do next
48-
2. The DevIns command(s) to execute (wrapped in <devin></devin> tags)
56+
1. Your reasoning about what to do next (explain your thinking)
57+
2. **EXACTLY ONE** DevIns command (wrapped in <devin></devin> tags)
4958
3. What you expect to happen
5059
5160
Example:
52-
I need to check the existing implementation first.
61+
I need to check the existing implementation first to understand the current code structure.
5362
<devin>
5463
/read-file path="src/main.ts"
5564
</devin>
65+
I expect to see the main entry point of the application.
5666
5767
## Making Code Changes
5868
@@ -108,18 +118,28 @@ ${'$'}{toolList}
108118
4. **测试更改**: 运行测试或构建命令来验证更改
109119
5. **完成信号**: 完成后,在消息中响应 "TASK_COMPLETE"
110120
121+
## 重要:每次响应只执行一个工具
122+
123+
**你必须每次响应只执行一个工具。** 不要在单个响应中包含多个工具调用。
124+
125+
- ✅ 正确:一个 <devin> 块包含一个工具调用
126+
- ❌ 错误:多个 <devin> 块或一个块中有多个工具
127+
128+
每次工具执行后,你会看到结果,然后可以决定下一步。
129+
111130
## 响应格式
112131
113132
对于每一步,请回复:
114-
1. 你对下一步该做什么的推理
115-
2. 要执行的 DevIns 命令(包装在 <devin></devin> 标签中)
133+
1. 你对下一步该做什么的推理(解释你的思考)
134+
2. **恰好一个** DevIns 命令(包装在 <devin></devin> 标签中)
116135
3. 你期望发生什么
117136
118137
示例:
119-
我需要先检查现有实现
138+
我需要先检查现有实现以了解当前的代码结构
120139
<devin>
121140
/read-file path="src/main.ts"
122141
</devin>
142+
我期望看到应用程序的主入口点。
123143
124144
## 进行代码更改
125145

mpp-core/src/jsMain/kotlin/cc/unitmesh/agent/CodingAgentExports.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,13 +225,15 @@ class JsCodingAgentContextBuilder {
225225
class JsCodingAgent(
226226
private val projectPath: String,
227227
private val llmService: cc.unitmesh.llm.JsKoogLLMService,
228-
private val maxIterations: Int = 100
228+
private val maxIterations: Int = 100,
229+
private val renderer: JsCodingAgentRenderer? = null
229230
) {
230231
// 内部使用 Kotlin 的 CodingAgent
231232
private val agent: CodingAgent = CodingAgent(
232233
projectPath = projectPath,
233234
llmService = llmService.service, // 访问内部 KoogLLMService
234-
maxIterations = maxIterations
235+
maxIterations = maxIterations,
236+
renderer = if (renderer != null) JsRendererAdapter(renderer) else DefaultCodingAgentRenderer()
235237
)
236238

237239
/**
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package cc.unitmesh.agent
2+
3+
import kotlin.js.JsExport
4+
import kotlin.js.JsName
5+
6+
/**
7+
* JS-friendly renderer interface
8+
* Allows TypeScript to provide custom rendering implementations
9+
*/
10+
@JsExport
11+
interface JsCodingAgentRenderer {
12+
fun renderIterationHeader(current: Int, max: Int)
13+
fun renderLLMResponseStart()
14+
fun renderLLMResponseChunk(chunk: String)
15+
fun renderLLMResponseEnd()
16+
fun renderToolCall(toolName: String, paramsStr: String)
17+
fun renderToolResult(toolName: String, success: Boolean, output: String?, fullOutput: String?)
18+
fun renderTaskComplete()
19+
fun renderFinalResult(success: Boolean, message: String, iterations: Int)
20+
fun renderError(message: String)
21+
fun renderRepeatWarning(toolName: String, count: Int)
22+
}
23+
24+
/**
25+
* Adapter to convert JS renderer to Kotlin renderer
26+
*/
27+
class JsRendererAdapter(private val jsRenderer: JsCodingAgentRenderer) : CodingAgentRenderer {
28+
override fun renderIterationHeader(current: Int, max: Int) {
29+
jsRenderer.renderIterationHeader(current, max)
30+
}
31+
32+
override fun renderLLMResponseStart() {
33+
jsRenderer.renderLLMResponseStart()
34+
}
35+
36+
override fun renderLLMResponseChunk(chunk: String) {
37+
jsRenderer.renderLLMResponseChunk(chunk)
38+
}
39+
40+
override fun renderLLMResponseEnd() {
41+
jsRenderer.renderLLMResponseEnd()
42+
}
43+
44+
override fun renderToolCall(toolName: String, paramsStr: String) {
45+
jsRenderer.renderToolCall(toolName, paramsStr)
46+
}
47+
48+
override fun renderToolResult(toolName: String, success: Boolean, output: String?, fullOutput: String?) {
49+
jsRenderer.renderToolResult(toolName, success, output, fullOutput)
50+
}
51+
52+
override fun renderTaskComplete() {
53+
jsRenderer.renderTaskComplete()
54+
}
55+
56+
override fun renderFinalResult(success: Boolean, message: String, iterations: Int) {
57+
jsRenderer.renderFinalResult(success, message, iterations)
58+
}
59+
60+
override fun renderError(message: String) {
61+
jsRenderer.renderError(message)
62+
}
63+
64+
override fun renderRepeatWarning(toolName: String, count: Int) {
65+
jsRenderer.renderRepeatWarning(toolName, count)
66+
}
67+
}
68+
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/**
2+
* CLI Renderer for CodingAgent
3+
* Implements JsCodingAgentRenderer interface from Kotlin
4+
*/
5+
6+
import chalk from 'chalk';
7+
8+
/**
9+
* Parse code blocks from LLM response
10+
* Simplified version of Kotlin's CodeFence.parseAll
11+
*/
12+
interface CodeBlock {
13+
languageId: string;
14+
text: string;
15+
}
16+
17+
function parseCodeBlocks(response: string): CodeBlock[] {
18+
const blocks: CodeBlock[] = [];
19+
const lines = response.split('\n');
20+
21+
let i = 0;
22+
while (i < lines.length) {
23+
const line = lines[i];
24+
const trimmed = line.trim();
25+
26+
// Check for devin block
27+
if (trimmed === '<devin>') {
28+
const devinLines: string[] = [];
29+
i++;
30+
while (i < lines.length && lines[i].trim() !== '</devin>') {
31+
devinLines.push(lines[i]);
32+
i++;
33+
}
34+
blocks.push({
35+
languageId: 'devin',
36+
text: devinLines.join('\n').trim()
37+
});
38+
i++; // Skip </devin>
39+
continue;
40+
}
41+
42+
// Check for markdown code block
43+
if (trimmed.startsWith('```')) {
44+
const lang = trimmed.substring(3).trim();
45+
const codeLines: string[] = [];
46+
i++;
47+
while (i < lines.length && !lines[i].trim().startsWith('```')) {
48+
codeLines.push(lines[i]);
49+
i++;
50+
}
51+
blocks.push({
52+
languageId: lang,
53+
text: codeLines.join('\n')
54+
});
55+
i++; // Skip closing ```
56+
continue;
57+
}
58+
59+
// Regular text - collect until next code block
60+
const textLines: string[] = [];
61+
while (i < lines.length) {
62+
const currentLine = lines[i];
63+
const currentTrimmed = currentLine.trim();
64+
if (currentTrimmed.startsWith('```') || currentTrimmed === '<devin>') {
65+
break;
66+
}
67+
textLines.push(currentLine);
68+
i++;
69+
}
70+
71+
if (textLines.length > 0) {
72+
blocks.push({
73+
languageId: 'markdown',
74+
text: textLines.join('\n').trim()
75+
});
76+
}
77+
}
78+
79+
return blocks;
80+
}
81+
82+
/**
83+
* CLI Renderer implementation
84+
*/
85+
export class CliRenderer {
86+
// Required by Kotlin JS export interface
87+
readonly __doNotUseOrImplementIt: any = {};
88+
89+
private reasoningBuffer: string = '';
90+
private isInDevinBlock: boolean = false;
91+
92+
renderIterationHeader(current: number, max: number): void {
93+
console.log(`\n${chalk.bold.blue(`[${current}/${max}]`)} Analyzing and executing...`);
94+
}
95+
96+
renderLLMResponseStart(): void {
97+
this.reasoningBuffer = '';
98+
this.isInDevinBlock = false;
99+
process.stdout.write(chalk.gray('💭 '));
100+
}
101+
102+
renderLLMResponseChunk(chunk: string): void {
103+
// Parse chunk to detect devin blocks
104+
this.reasoningBuffer += chunk;
105+
106+
// Check if we're entering or leaving a devin block
107+
if (this.reasoningBuffer.includes('<devin>')) {
108+
this.isInDevinBlock = true;
109+
}
110+
if (this.reasoningBuffer.includes('</devin>')) {
111+
this.isInDevinBlock = false;
112+
}
113+
114+
// Only print if not in devin block
115+
if (!this.isInDevinBlock && !chunk.includes('<devin>') && !chunk.includes('</devin>')) {
116+
process.stdout.write(chalk.white(chunk));
117+
}
118+
}
119+
120+
renderLLMResponseEnd(): void {
121+
console.log('\n');
122+
}
123+
124+
renderToolCall(toolName: string, paramsStr: string): void {
125+
console.log(chalk.cyan('🔧 ') + `/${toolName} ${paramsStr}`);
126+
}
127+
128+
renderToolResult(toolName: string, success: boolean, output: string | null, fullOutput: string | null): void {
129+
const icon = success ? chalk.green('✓') : chalk.red('✗');
130+
let line = ` ${icon} ${toolName}`;
131+
132+
if (success && output) {
133+
// For read-file, show full content (no truncation) so LLM can see complete file
134+
// For other tools, show preview (300 chars)
135+
const shouldTruncate = toolName !== 'read-file';
136+
const maxLength = shouldTruncate ? 300 : Number.MAX_SAFE_INTEGER;
137+
138+
const preview = output.substring(0, Math.min(output.length, maxLength)).replace(/\n/g, ' ');
139+
if (preview.length > 0 && !preview.startsWith('Successfully')) {
140+
line += chalk.gray(` → ${preview}`);
141+
if (shouldTruncate && output.length > maxLength) {
142+
line += chalk.gray('...');
143+
}
144+
}
145+
} else if (!success && output) {
146+
line += chalk.red(` → ${output.substring(0, 300)}`);
147+
}
148+
149+
console.log(line);
150+
}
151+
152+
renderTaskComplete(): void {
153+
console.log(chalk.green('\n✓ Task marked as complete\n'));
154+
}
155+
156+
renderFinalResult(success: boolean, message: string, iterations: number): void {
157+
console.log();
158+
if (success) {
159+
console.log(chalk.green('✅ Task completed successfully'));
160+
} else {
161+
console.log(chalk.yellow('⚠️ Task incomplete'));
162+
}
163+
console.log(chalk.gray(`Task completed after ${iterations} iterations`));
164+
}
165+
166+
renderError(message: string): void {
167+
console.log(chalk.red('❌ ') + message);
168+
}
169+
170+
renderRepeatWarning(toolName: string, count: number): void {
171+
console.log(chalk.yellow(`⚠️ Warning: Tool '${toolName}' has been called ${count} times in a row`));
172+
}
173+
}
174+

mpp-ui/src/jsMain/typescript/index.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { render } from 'ink';
1212
import { Command } from 'commander';
1313
import { App } from './ui/App.js';
1414
import { ConfigManager } from './config/ConfigManager.js';
15+
import { CliRenderer } from './agents/CliRenderer.js';
1516
// Use Kotlin CodingAgent instead of TypeScript implementation
1617
import mppCore from '@autodev/mpp-core';
1718
import * as path from 'path';
@@ -61,19 +62,23 @@ async function runCodingAgent(projectPath: string, task: string, quiet: boolean
6162
)
6263
);
6364

64-
// Create and run Kotlin CodingAgent
65+
// Create CLI renderer
66+
const renderer = new CliRenderer();
67+
68+
// Create and run Kotlin CodingAgent with custom renderer
6569
const agent = new KotlinCC.unitmesh.agent.JsCodingAgent(
6670
resolvedPath,
6771
llmService,
68-
10 // maxIterations
72+
10, // maxIterations
73+
renderer // custom renderer
6974
);
70-
75+
7176
// Create task object
7277
const taskObj = new KotlinCC.unitmesh.agent.JsAgentTask(
7378
task,
7479
resolvedPath
7580
);
76-
81+
7782
const result = await agent.executeTask(taskObj);
7883

7984
if (!quiet) {

0 commit comments

Comments
 (0)