Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ agentapi server -- goose
```

> [!NOTE]
> When using Codex, Opencode, Copilot, Gemini, Amp or CursorCLI, always specify the agent type explicitly (eg: `agentapi server --type=codex -- codex`), or message formatting may break.
> When using Claude, Codex, Opencode, Copilot, Gemini, Amp or CursorCLI, always specify the agent type explicitly (eg: `agentapi server --type=codex -- codex`), or message formatting may break.

An OpenAPI schema is available in [openapi.json](openapi.json).

Expand Down
2 changes: 1 addition & 1 deletion lib/httpapi/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ func NewServer(ctx context.Context, config ServerConfig) (*Server, error) {
humaConfig.Info.Description = "HTTP API for Claude Code, Goose, and Aider.\n\nhttps://github.com/coder/agentapi"
api := humachi.New(router, humaConfig)
formatMessage := func(message string, userInput string) string {
return mf.FormatAgentMessage(config.AgentType, message, userInput)
return mf.FormatAgentMessage(config.AgentType, message, userInput, logger)
}

isAgentReadyForInitialPrompt := func(message string) bool {
Expand Down
55 changes: 55 additions & 0 deletions lib/msgfmt/message_box.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package msgfmt

import (
"log/slog"
"strings"
)

Expand Down Expand Up @@ -100,3 +101,57 @@ func removeAmpMessageBox(msg string) string {
}
return formattedMsg
}

func removeClaudeReportTaskToolCall(msg string, logger *slog.Logger) string {
// Remove all tool calls that start with `● coder - coder_report_task (MCP)` till we encounter the next line starting with ●
lines := strings.Split(msg, "\n")

toolCallStartIdx := -1
newLineAfterToolCallIdx := -1

// Store all tool call start and end indices [[start, end], ...]
var toolCallIdxs [][]int

for i := 0; i < len(lines); i++ {
line := strings.TrimSpace(lines[i])

if strings.HasPrefix(line, "● coder - coder_report_task (MCP)") {
toolCallStartIdx = i
} else if toolCallStartIdx != -1 && strings.HasPrefix(line, "●") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible that ● coder - coder_report_task (MCP) is the last entry, and there won't be a next message with ? In which I assume we'd remove all the remaining lines?

Asking because I don't see handling for this case (unless I'm just missing it).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible that ● coder - coder_report_task (MCP) is the last entry, and there won't be a next message with ●? In which I assume we'd remove all the remaining lines?

@mafredri
Added the logic to handle this case. This handles the case where this is the last thing claude outputs is the tool call, and it also acts as a failsafe if the next new message is not prefixed with ●.
I'm checking for a newline after the tool-call and not directly using len(lines) in above case, because I never want to run into this: In which I assume we'd remove all the remaining lines?

// Store [start, end] pair
toolCallIdxs = append(toolCallIdxs, []int{toolCallStartIdx, i})

// Reset to find the next tool call
toolCallStartIdx = -1
newLineAfterToolCallIdx = -1
}
if len(line) == 0 && toolCallStartIdx != -1 && newLineAfterToolCallIdx == -1 {
newLineAfterToolCallIdx = i
}
}

// Handle the case where the last tool call goes till the end of the message
// And a failsafe when the next message is not prefixed with ●
if toolCallStartIdx != -1 && newLineAfterToolCallIdx != -1 {
toolCallIdxs = append(toolCallIdxs, []int{toolCallStartIdx, newLineAfterToolCallIdx})
}

// If no tool calls found, return original message
if len(toolCallIdxs) == 0 {
return msg
}

// Remove tool calls from the message
for i := len(toolCallIdxs) - 1; i >= 0; i-- {
idxPair := toolCallIdxs[i]
start, end := idxPair[0], idxPair[1]

// Capture the tool call content before removing it
toolCallContent := strings.Join(lines[start:end], "\n")
logger.Info("Removing tool call", "content", toolCallContent)

lines = append(lines[:start], lines[end:]...)
}

return strings.Join(lines, "\n")
}
13 changes: 11 additions & 2 deletions lib/msgfmt/msgfmt.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package msgfmt

import (
"log/slog"
"strings"
)

Expand Down Expand Up @@ -254,6 +255,14 @@ func formatGenericMessage(message string, userInput string, agentType AgentType)
return message
}

func formatClaudeMessage(message string, userInput string, logger *slog.Logger) string {
message = RemoveUserInput(message, userInput, AgentTypeClaude)
message = removeMessageBox(message)
message = removeClaudeReportTaskToolCall(message, logger)
message = trimEmptyLines(message)
return message
}

func formatCodexMessage(message string, userInput string) string {
message = RemoveUserInput(message, userInput, AgentTypeCodex)
message = removeCodexInputBox(message)
Expand All @@ -275,10 +284,10 @@ func formatAmpMessage(message string, userInput string) string {
return message
}

func FormatAgentMessage(agentType AgentType, message string, userInput string) string {
func FormatAgentMessage(agentType AgentType, message string, userInput string, logger *slog.Logger) string {
switch agentType {
case AgentTypeClaude:
return formatGenericMessage(message, userInput, agentType)
return formatClaudeMessage(message, userInput, logger)
case AgentTypeGoose:
return formatGenericMessage(message, userInput, agentType)
case AgentTypeAider:
Expand Down
3 changes: 2 additions & 1 deletion lib/msgfmt/msgfmt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package msgfmt

import (
"embed"
"log/slog"
"path"
"strings"
"testing"
Expand Down Expand Up @@ -233,7 +234,7 @@ func TestFormatAgentMessage(t *testing.T) {
assert.NoError(t, err)
expected, err := testdataDir.ReadFile(path.Join(dir, string(agentType), c.Name(), "expected.txt"))
assert.NoError(t, err)
assert.Equal(t, string(expected), FormatAgentMessage(agentType, string(msg), string(userInput)))
assert.Equal(t, string(expected), FormatAgentMessage(agentType, string(msg), string(userInput), slog.Default()))
})
}
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
● I'll build a snake game for you. Let me start by reporting my
progress and creating a task list.

● Now I'll create a complete snake game with HTML, CSS, and
JavaScript:

● Write(snake-game.html)
⎿ Wrote 344 lines to snake-game.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<title>Snake Game</title>
<style>
* {
margin: 0;
padding: 0;
… +334 lines (ctrl+o to expand)

● I've built a complete snake game for you! The game is saved
at /home/coder/snake-game.html.

Features:
- Classic snake gameplay with smooth controls
- Use arrow keys to control the snake
- Press SPACE to pause/unpause
- Score tracking with increasing difficulty (speed increases
every 5 points)
- Colorful gradient background and styled UI
- Game over screen with restart option
- Snake grows when eating food
- Collision detection for walls and self-collision

How to play:
Open the HTML file in your web browser and use the arrow keys
to move the snake. Collect the red food to grow and increase
your score!
72 changes: 72 additions & 0 deletions lib/msgfmt/testdata/format/claude/remove-task-tool-call/msg.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
> Build a snake game

● I'll build a snake game for you. Let me start by reporting my
progress and creating a task list.

● coder - coder_report_task (MCP)(summary: "Building a snake game
with HTML/CSS/JavaScript", link:
"", state: "working")
⎿ {
"message": "Thanks for reporting!"
}

● Now I'll create a complete snake game with HTML, CSS, and
JavaScript:

● Write(snake-game.html)
⎿ Wrote 344 lines to snake-game.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<title>Snake Game</title>
<style>
* {
margin: 0;
padding: 0;
… +334 lines (ctrl+o to expand)

● coder - coder_report_task (MCP)(summary: "Snake game created
successfully at snake-game.html",
link: "file:///home/coder/snake-ga
me.html", state: "working")
⎿ {
"message": "Thanks for reporting!"
}


● I've built a complete snake game for you! The game is saved
at /home/coder/snake-game.html.

Features:
- Classic snake gameplay with smooth controls
- Use arrow keys to control the snake
- Press SPACE to pause/unpause
- Score tracking with increasing difficulty (speed increases
every 5 points)
- Colorful gradient background and styled UI
- Game over screen with restart option
- Snake grows when eating food
- Collision detection for walls and self-collision

How to play:
Open the HTML file in your web browser and use the arrow keys
to move the snake. Collect the red food to grow and increase
your score!

● coder - coder_report_task (MCP)(summary: "Snake game created
successfully at snake-game.html",
link: "file:///home/coder/snake-ga
me.html", state: "working")
⎿ {
"message": "Thanks for reporting!"
}


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
? for shortcuts ⧉ In message_box.go

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Build a snake game