diff --git a/internal/runtime/executor/kiro_executor.go b/internal/runtime/executor/kiro_executor.go index 59ba9950..7c10d03e 100644 --- a/internal/runtime/executor/kiro_executor.go +++ b/internal/runtime/executor/kiro_executor.go @@ -2372,8 +2372,8 @@ func (e *KiroExecutor) extractEventTypeFromBytes(headers []byte) string { func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out chan<- cliproxyexecutor.StreamChunk, targetFormat sdktranslator.Format, model string, originalReq, claudeBody []byte, reporter *usageReporter, thinkingEnabled bool) { reader := bufio.NewReaderSize(body, 20*1024*1024) // 20MB buffer to match other providers var totalUsage usage.Detail - var hasToolUses bool // Track if any tool uses were emitted - var upstreamStopReason string // Track stop_reason from upstream events + var hasToolUses bool // Track if any tool uses were emitted + var upstreamStopReason string // Track stop_reason from upstream events // Tool use state tracking for input buffering and deduplication processedIDs := make(map[string]bool) @@ -3151,12 +3151,92 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out _ = signature // Signature can be used for verification if needed case "toolUseEvent": + // Debug: log raw toolUseEvent payload for large tool inputs + if log.IsLevelEnabled(log.DebugLevel) { + payloadStr := string(payload) + if len(payloadStr) > 500 { + payloadStr = payloadStr[:500] + "...[truncated]" + } + log.Debugf("kiro: raw toolUseEvent payload (%d bytes): %s", len(payload), payloadStr) + } // Handle dedicated tool use events with input buffering completedToolUses, newState := kiroclaude.ProcessToolUseEvent(event, currentToolUse, processedIDs) currentToolUse = newState // Emit completed tool uses for _, tu := range completedToolUses { + // Check for truncated write marker - emit as a Bash tool that echoes the error + // This way Claude Code will execute it, see the error, and the agent can retry + if tu.Name == "__truncated_write__" { + filePath := "" + if fp, ok := tu.Input["file_path"].(string); ok && fp != "" { + filePath = fp + } + + // Create a Bash tool that echoes the error message + // This will be executed by Claude Code and the agent will see the result + var errorMsg string + if filePath != "" { + errorMsg = fmt.Sprintf("echo '[WRITE TOOL ERROR] The file content for \"%s\" is too large to be transmitted by the upstream API. You MUST retry by writing the file in smaller chunks: First use Write to create the file with the first 700 lines, then use multiple Edit operations to append the remaining content in chunks of ~700 lines each.'", filePath) + } else { + errorMsg = "echo '[WRITE TOOL ERROR] The file content is too large to be transmitted by the upstream API. The Write tool input was truncated. You MUST retry by writing the file in smaller chunks: First use Write to create the file with the first 700 lines, then use multiple Edit operations to append the remaining content in chunks of ~700 lines each.'" + } + + log.Warnf("kiro: converting truncated write to Bash echo for file: %s", filePath) + + hasToolUses = true + + // Close text block if open + if isTextBlockOpen && contentBlockIndex >= 0 { + blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex) + sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam) + for _, chunk := range sseData { + if chunk != "" { + out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")} + } + } + isTextBlockOpen = false + } + + contentBlockIndex++ + + // Emit as Bash tool_use + blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "tool_use", tu.ToolUseID, "Bash") + sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam) + for _, chunk := range sseData { + if chunk != "" { + out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")} + } + } + + // Emit the Bash command as input + bashInput := map[string]interface{}{ + "command": errorMsg, + } + inputJSON, err := json.Marshal(bashInput) + if err != nil { + log.Errorf("kiro: failed to marshal bash input for truncated write error: %v", err) + continue + } + inputDelta := kiroclaude.BuildClaudeInputJsonDeltaEvent(string(inputJSON), contentBlockIndex) + sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, inputDelta, &translatorParam) + for _, chunk := range sseData { + if chunk != "" { + out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")} + } + } + + blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex) + sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam) + for _, chunk := range sseData { + if chunk != "" { + out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")} + } + } + + continue // Skip the normal tool_use emission + } + hasToolUses = true // Close text block if open diff --git a/internal/translator/kiro/claude/kiro_claude_tools.go b/internal/translator/kiro/claude/kiro_claude_tools.go index 93ede875..6020a8a4 100644 --- a/internal/translator/kiro/claude/kiro_claude_tools.go +++ b/internal/translator/kiro/claude/kiro_claude_tools.go @@ -395,6 +395,17 @@ func ProcessToolUseEvent(event map[string]interface{}, currentToolUse *ToolUseSt isStop = stop } + // Debug: log when stop event arrives + if isStop { + log.Debugf("kiro: toolUseEvent stop=true received for tool %s (ID: %s), currentToolUse buffer len: %d", + toolName, toolUseID, func() int { + if currentToolUse != nil { + return currentToolUse.InputBuffer.Len() + } + return -1 + }()) + } + // Get input - can be string (fragment) or object (complete) var inputFragment string var inputMap map[string]interface{} @@ -466,12 +477,92 @@ func ProcessToolUseEvent(event map[string]interface{}, currentToolUse *ToolUseSt if isStop && currentToolUse != nil { fullInput := currentToolUse.InputBuffer.String() + // Check for Write tool with empty or missing input - this happens when Kiro API + // completely skips sending input for large file writes + if currentToolUse.Name == "Write" && len(strings.TrimSpace(fullInput)) == 0 { + log.Warnf("kiro: Write tool received no input from upstream API. The file content may be too large to transmit.") + // Return nil to skip this tool use - it will be handled as a truncation error + // The caller should emit a text block explaining the error instead + if processedIDs != nil { + processedIDs[currentToolUse.ToolUseID] = true + } + log.Infof("kiro: skipping Write tool use %s due to empty input (content too large)", currentToolUse.ToolUseID) + // Return a special marker tool use that indicates truncation + toolUse := KiroToolUse{ + ToolUseID: currentToolUse.ToolUseID, + Name: "__truncated_write__", // Special marker name + Input: map[string]interface{}{ + "error": "Write tool input was not transmitted by upstream API. The file content is too large.", + }, + } + toolUses = append(toolUses, toolUse) + return toolUses, nil + } + // Repair and parse the accumulated JSON repairedJSON := RepairJSON(fullInput) var finalInput map[string]interface{} if err := json.Unmarshal([]byte(repairedJSON), &finalInput); err != nil { log.Warnf("kiro: failed to parse accumulated tool input: %v, raw: %s", err, fullInput) finalInput = make(map[string]interface{}) + + // Check if this is a Write tool with truncated input (missing content field) + // This happens when the Kiro API truncates large tool inputs + if currentToolUse.Name == "Write" && strings.Contains(fullInput, "file_path") && !strings.Contains(fullInput, "content") { + log.Warnf("kiro: Write tool input was truncated by upstream API (content field missing). The file content may be too large.") + // Extract file_path if possible for error context + filePath := "" + if idx := strings.Index(fullInput, "file_path"); idx >= 0 { + // Try to extract the file path value + rest := fullInput[idx:] + if colonIdx := strings.Index(rest, ":"); colonIdx >= 0 { + rest = strings.TrimSpace(rest[colonIdx+1:]) + if len(rest) > 0 && rest[0] == '"' { + rest = rest[1:] + if endQuote := strings.Index(rest, "\""); endQuote >= 0 { + filePath = rest[:endQuote] + } + } + } + } + if processedIDs != nil { + processedIDs[currentToolUse.ToolUseID] = true + } + // Return a special marker tool use that indicates truncation + toolUse := KiroToolUse{ + ToolUseID: currentToolUse.ToolUseID, + Name: "__truncated_write__", // Special marker name + Input: map[string]interface{}{ + "error": "Write tool content was truncated by upstream API. The file content is too large.", + "file_path": filePath, + }, + } + toolUses = append(toolUses, toolUse) + return toolUses, nil + } + } + + // Additional check: Write tool parsed successfully but missing content field + if currentToolUse.Name == "Write" { + if _, hasContent := finalInput["content"]; !hasContent { + if filePath, hasPath := finalInput["file_path"]; hasPath { + log.Warnf("kiro: Write tool input missing 'content' field, likely truncated by upstream API") + if processedIDs != nil { + processedIDs[currentToolUse.ToolUseID] = true + } + // Return a special marker tool use that indicates truncation + toolUse := KiroToolUse{ + ToolUseID: currentToolUse.ToolUseID, + Name: "__truncated_write__", // Special marker name + Input: map[string]interface{}{ + "error": "Write tool content field was missing. The file content is too large.", + "file_path": filePath, + }, + } + toolUses = append(toolUses, toolUse) + return toolUses, nil + } + } } toolUse := KiroToolUse{