diff --git a/README.md b/README.md index cd48452d..e6a75618 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/lib/httpapi/server.go b/lib/httpapi/server.go index 18f2bf40..9d8a0412 100644 --- a/lib/httpapi/server.go +++ b/lib/httpapi/server.go @@ -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 { diff --git a/lib/msgfmt/message_box.go b/lib/msgfmt/message_box.go index 13efaf11..53d9925f 100644 --- a/lib/msgfmt/message_box.go +++ b/lib/msgfmt/message_box.go @@ -1,6 +1,7 @@ package msgfmt import ( + "log/slog" "strings" ) @@ -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, "●") { + // 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") +} diff --git a/lib/msgfmt/msgfmt.go b/lib/msgfmt/msgfmt.go index 3fccc2c3..ebe8805d 100644 --- a/lib/msgfmt/msgfmt.go +++ b/lib/msgfmt/msgfmt.go @@ -1,6 +1,7 @@ package msgfmt import ( + "log/slog" "strings" ) @@ -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) @@ -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: diff --git a/lib/msgfmt/msgfmt_test.go b/lib/msgfmt/msgfmt_test.go index 780a3954..92dc6b3e 100644 --- a/lib/msgfmt/msgfmt_test.go +++ b/lib/msgfmt/msgfmt_test.go @@ -2,6 +2,7 @@ package msgfmt import ( "embed" + "log/slog" "path" "strings" "testing" @@ -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())) }) } }) diff --git a/lib/msgfmt/testdata/format/claude/remove-task-tool-call/expected.txt b/lib/msgfmt/testdata/format/claude/remove-task-tool-call/expected.txt new file mode 100644 index 00000000..f06f58f6 --- /dev/null +++ b/lib/msgfmt/testdata/format/claude/remove-task-tool-call/expected.txt @@ -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 + + +
+ + +