feat(kiro): enhance Kiro executor stability and compatibility

## Changes Overview
This commit includes multiple improvements to the Kiro executor for better
stability, API compatibility, and code quality.

## Detailed Changes

### 1. Output Token Calculation Improvement (lines 317-330)
- Replace simple len(content)/4 estimation with tiktoken-based calculation
- Add fallback to character count estimation if tiktoken fails
- Improves token counting accuracy for usage tracking

### 2. Stream Handler Panic Recovery (lines 528-533)
- Add defer/recover block in streamToChannel goroutine
- Prevents single request crashes from affecting the entire service

### 3. Struct Field Reordering (lines 670-673)
- Reorder kiroToolResult struct fields: Content, Status, ToolUseID
- Ensures consistency with API expectations

### 4. Message Merging Function (lines 778-780, 2356-2483)
- Add mergeAdjacentMessages() to combine consecutive messages with same role
- Add helper functions: mergeMessageContent(), blockToMap(), createMergedMessage()
- Required by Kiro API which doesn't allow adjacent messages from same role

### 5. Empty Content Handling (lines 791-800)
- Add default content for empty history messages
- User messages with tool results: "Tool results provided."
- User messages without tool results: "Continue"

### 6. Assistant Last Message Handling (lines 811-830)
- Detect when last message is from assistant
- Create synthetic "Continue" user message to satisfy Kiro API requirements
- Kiro API requires currentMessage to be userInputMessage type

### 7. Duplicate Content Event Detection (lines 1650-1660)
- Track lastContentEvent to detect duplicate streaming events
- Skip redundant events to prevent duplicate content in responses
- Based on AIClient-2-API implementation for Kiro

### 8. Streaming Token Calculation Enhancement (lines 1785-1817)
- Add accumulatedContent buffer for streaming token calculation
- Use tiktoken for accurate output token counting during streaming
- Add fallback to character count estimation with proper logging

### 9. JSON Repair Enhancement (lines 2665-2818)
- Implement conservative JSON repair strategy
- First try to parse JSON directly - if valid, return unchanged
- Add bracket balancing detection and repair
- Only repair when necessary to avoid corrupting valid JSON
- Validate repaired JSON before returning

### 10. HELIOS_CHK Filtering Removal (lines 2500-2504, 3004-3039)
- Remove filterHeliosDebugInfo function
- Remove heliosDebugPattern regex
- HELIOS_CHK fields now handled by client-side processing

### 11. Comment Translation
- Translate Chinese comments to English for code consistency
- Affected areas: token calculation, buffer handling, message processing
This commit is contained in:
Ravens2121
2025-12-11 08:10:11 +08:00
parent f302be5ce6
commit 6133bac226

View File

@@ -315,9 +315,18 @@ func (e *KiroExecutor) executeWithRetry(ctx context.Context, auth *cliproxyauth.
}
}
if len(content) > 0 {
usageInfo.OutputTokens = int64(len(content) / 4)
// Use tiktoken for more accurate output token calculation
if enc, encErr := tokenizerForModel(req.Model); encErr == nil {
if tokenCount, countErr := enc.Count(content); countErr == nil {
usageInfo.OutputTokens = int64(tokenCount)
}
}
// Fallback to character count estimation if tiktoken fails
if usageInfo.OutputTokens == 0 {
usageInfo.OutputTokens = 1
usageInfo.OutputTokens = int64(len(content) / 4)
if usageInfo.OutputTokens == 0 {
usageInfo.OutputTokens = 1
}
}
}
usageInfo.TotalTokens = usageInfo.InputTokens + usageInfo.OutputTokens
@@ -519,6 +528,12 @@ func (e *KiroExecutor) executeStreamWithRetry(ctx context.Context, auth *cliprox
go func(resp *http.Response) {
defer close(out)
defer func() {
if r := recover(); r != nil {
log.Errorf("kiro: panic in stream handler: %v", r)
out <- cliproxyexecutor.StreamChunk{Err: fmt.Errorf("internal error: %v", r)}
}
}()
defer func() {
if errClose := resp.Body.Close(); errClose != nil {
log.Errorf("response body close error: %v", errClose)
@@ -655,9 +670,9 @@ type kiroUserInputMessageContext struct {
}
type kiroToolResult struct {
ToolUseID string `json:"toolUseId"`
Content []kiroTextContent `json:"content"`
Status string `json:"status"`
ToolUseID string `json:"toolUseId"`
}
type kiroTextContent struct {
@@ -763,7 +778,9 @@ func (e *KiroExecutor) buildKiroPayload(claudeBody []byte, modelID, profileArn,
var currentUserMsg *kiroUserInputMessage
var currentToolResults []kiroToolResult
messagesArray := messages.Array()
// Merge adjacent messages with the same role before processing
// This reduces API call complexity and improves compatibility
messagesArray := mergeAdjacentMessages(messages.Array())
for i, msg := range messagesArray {
role := msg.Get("role").String()
isLastMessage := i == len(messagesArray)-1
@@ -774,6 +791,14 @@ func (e *KiroExecutor) buildKiroPayload(claudeBody []byte, modelID, profileArn,
currentUserMsg = &userMsg
currentToolResults = toolResults
} else {
// CRITICAL: Kiro API requires content to be non-empty for history messages too
if strings.TrimSpace(userMsg.Content) == "" {
if len(toolResults) > 0 {
userMsg.Content = "Tool results provided."
} else {
userMsg.Content = "Continue"
}
}
// For history messages, embed tool results in context
if len(toolResults) > 0 {
userMsg.UserInputMessageContext = &kiroUserInputMessageContext{
@@ -786,9 +811,24 @@ func (e *KiroExecutor) buildKiroPayload(claudeBody []byte, modelID, profileArn,
}
} else if role == "assistant" {
assistantMsg := e.buildAssistantMessageStruct(msg)
history = append(history, kiroHistoryMessage{
AssistantResponseMessage: &assistantMsg,
})
// If this is the last message and it's an assistant message,
// we need to add it to history and create a "Continue" user message
// because Kiro API requires currentMessage to be userInputMessage type
if isLastMessage {
history = append(history, kiroHistoryMessage{
AssistantResponseMessage: &assistantMsg,
})
// Create a "Continue" user message as currentMessage
currentUserMsg = &kiroUserInputMessage{
Content: "Continue",
ModelID: modelID,
Origin: origin,
}
} else {
history = append(history, kiroHistoryMessage{
AssistantResponseMessage: &assistantMsg,
})
}
}
}
@@ -805,7 +845,35 @@ func (e *KiroExecutor) buildKiroPayload(claudeBody []byte, modelID, profileArn,
// Add the actual user message
contentBuilder.WriteString(currentUserMsg.Content)
currentUserMsg.Content = contentBuilder.String()
finalContent := contentBuilder.String()
// CRITICAL: Kiro API requires content to be non-empty, even when toolResults are present
// If content is empty or only whitespace, provide a default message
if strings.TrimSpace(finalContent) == "" {
if len(currentToolResults) > 0 {
finalContent = "Tool results provided."
} else {
finalContent = "Continue"
}
log.Debugf("kiro: content was empty, using default: %s", finalContent)
}
currentUserMsg.Content = finalContent
// Deduplicate currentToolResults before adding to context
// Kiro API does not accept duplicate toolUseIds
if len(currentToolResults) > 0 {
seenIDs := make(map[string]bool)
uniqueToolResults := make([]kiroToolResult, 0, len(currentToolResults))
for _, tr := range currentToolResults {
if !seenIDs[tr.ToolUseID] {
seenIDs[tr.ToolUseID] = true
uniqueToolResults = append(uniqueToolResults, tr)
} else {
log.Debugf("kiro: skipping duplicate toolResult in currentMessage: %s", tr.ToolUseID)
}
}
currentToolResults = uniqueToolResults
}
// Build userInputMessageContext with tools and tool results
if len(kiroTools) > 0 || len(currentToolResults) > 0 {
@@ -855,11 +923,15 @@ func (e *KiroExecutor) buildKiroPayload(claudeBody []byte, modelID, profileArn,
// buildUserMessageStruct builds a user message and extracts tool results
// origin parameter determines which quota to use: "CLI" for Amazon Q, "AI_EDITOR" for Kiro IDE.
// IMPORTANT: Kiro API does not accept duplicate toolUseIds, so we deduplicate here.
func (e *KiroExecutor) buildUserMessageStruct(msg gjson.Result, modelID, origin string) (kiroUserInputMessage, []kiroToolResult) {
content := msg.Get("content")
var contentBuilder strings.Builder
var toolResults []kiroToolResult
var images []kiroImage
// Track seen toolUseIds to deduplicate - Kiro API rejects duplicate toolUseIds
seenToolUseIDs := make(map[string]bool)
if content.IsArray() {
for _, part := range content.Array() {
@@ -889,6 +961,14 @@ func (e *KiroExecutor) buildUserMessageStruct(msg gjson.Result, modelID, origin
case "tool_result":
// Extract tool result for API
toolUseID := part.Get("tool_use_id").String()
// Skip duplicate toolUseIds - Kiro API does not accept duplicates
if seenToolUseIDs[toolUseID] {
log.Debugf("kiro: skipping duplicate tool_result with toolUseId: %s", toolUseID)
continue
}
seenToolUseIDs[toolUseID] = true
isError := part.Get("is_error").Bool()
resultContent := part.Get("content")
@@ -1049,6 +1129,37 @@ func (e *KiroExecutor) parseEventStream(body io.Reader) (string, []kiroToolUse,
continue
}
// DIAGNOSTIC: Log all received event types for debugging
log.Debugf("kiro: parseEventStream received event type: %s", eventType)
if log.IsLevelEnabled(log.TraceLevel) {
log.Tracef("kiro: parseEventStream event payload: %s", string(payload))
}
// Check for error/exception events in the payload (Kiro API may return errors with HTTP 200)
// These can appear as top-level fields or nested within the event
if errType, hasErrType := event["_type"].(string); hasErrType {
// AWS-style error: {"_type": "com.amazon.aws.codewhisperer#ValidationException", "message": "..."}
errMsg := ""
if msg, ok := event["message"].(string); ok {
errMsg = msg
}
log.Errorf("kiro: received AWS error in event stream: type=%s, message=%s", errType, errMsg)
return "", nil, usageInfo, fmt.Errorf("kiro API error: %s - %s", errType, errMsg)
}
if errType, hasErrType := event["type"].(string); hasErrType && (errType == "error" || errType == "exception") {
// Generic error event
errMsg := ""
if msg, ok := event["message"].(string); ok {
errMsg = msg
} else if errObj, ok := event["error"].(map[string]interface{}); ok {
if msg, ok := errObj["message"].(string); ok {
errMsg = msg
}
}
log.Errorf("kiro: received error event in stream: type=%s, message=%s", errType, errMsg)
return "", nil, usageInfo, fmt.Errorf("kiro API error: %s", errMsg)
}
// Handle different event types
switch eventType {
case "assistantResponseEvent":
@@ -1142,11 +1253,6 @@ func (e *KiroExecutor) parseEventStream(body io.Reader) (string, []kiroToolUse,
// Deduplicate all tool uses
toolUses = deduplicateToolUses(toolUses)
// OPTIONAL: Filter out [HELIOS_CHK] debug blocks from Kiro/Amazon Q internal systems
// These blocks contain internal state tracking information that should not be exposed to users.
// To enable filtering, uncomment the following line:
// cleanedContent = filterHeliosDebugInfo(cleanedContent)
return cleanedContent, toolUses, usageInfo, nil
}
@@ -1267,8 +1373,9 @@ func (e *KiroExecutor) buildClaudeResponse(content string, toolUses []kiroToolUs
// streamToChannel converts AWS Event Stream to channel-based streaming.
// Supports tool calling - emits tool_use content blocks when tools are used.
// Includes embedded [Called ...] tool call parsing and input buffering for toolUseEvent.
// Implements duplicate content filtering using lastContentEvent detection (based on AIClient-2-API).
func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out chan<- cliproxyexecutor.StreamChunk, targetFormat sdktranslator.Format, model string, originalReq, claudeBody []byte, reporter *usageReporter) {
reader := bufio.NewReader(body)
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
@@ -1276,6 +1383,15 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
processedIDs := make(map[string]bool)
var currentToolUse *toolUseState
// Duplicate content detection - tracks last content event to filter duplicates
// Based on AIClient-2-API implementation for Kiro
var lastContentEvent string
// Streaming token calculation - accumulate content for real-time token counting
// Based on AIClient-2-API implementation
var accumulatedContent strings.Builder
accumulatedContent.Grow(4096) // Pre-allocate 4KB capacity to reduce reallocations
// Translator param for maintaining tool call state across streaming events
// IMPORTANT: This must persist across all TranslateStream calls
var translatorParam any
@@ -1408,6 +1524,39 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
continue
}
// DIAGNOSTIC: Log all received event types for debugging
log.Debugf("kiro: streamToChannel received event type: %s", eventType)
if log.IsLevelEnabled(log.TraceLevel) {
log.Tracef("kiro: streamToChannel event payload: %s", string(payload))
}
// Check for error/exception events in the payload (Kiro API may return errors with HTTP 200)
// These can appear as top-level fields or nested within the event
if errType, hasErrType := event["_type"].(string); hasErrType {
// AWS-style error: {"_type": "com.amazon.aws.codewhisperer#ValidationException", "message": "..."}
errMsg := ""
if msg, ok := event["message"].(string); ok {
errMsg = msg
}
log.Errorf("kiro: received AWS error in stream: type=%s, message=%s", errType, errMsg)
out <- cliproxyexecutor.StreamChunk{Err: fmt.Errorf("kiro API error: %s - %s", errType, errMsg)}
return
}
if errType, hasErrType := event["type"].(string); hasErrType && (errType == "error" || errType == "exception") {
// Generic error event
errMsg := ""
if msg, ok := event["message"].(string); ok {
errMsg = msg
} else if errObj, ok := event["error"].(map[string]interface{}); ok {
if msg, ok := errObj["message"].(string); ok {
errMsg = msg
}
}
log.Errorf("kiro: received error event in stream: type=%s, message=%s", errType, errMsg)
out <- cliproxyexecutor.StreamChunk{Err: fmt.Errorf("kiro API error: %s", errMsg)}
return
}
// Send message_start on first event
if !messageStartSent {
msgStart := e.buildClaudeMessageStartEvent(model, totalUsage.InputTokens)
@@ -1452,9 +1601,19 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
}
}
// Handle text content
// Handle text content with duplicate detection
if contentDelta != "" {
// Check for duplicate content - skip if identical to last content event
// Based on AIClient-2-API implementation for Kiro
if contentDelta == lastContentEvent {
log.Debugf("kiro: skipping duplicate content event (len: %d)", len(contentDelta))
continue
}
lastContentEvent = contentDelta
outputLen += len(contentDelta)
// Accumulate content for streaming token calculation
accumulatedContent.WriteString(contentDelta)
// Start text content block if needed
if !isTextBlockOpen {
contentBlockIndex++
@@ -1626,8 +1785,32 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
}
}
// Fallback for output tokens if not received from upstream
if totalUsage.OutputTokens == 0 && outputLen > 0 {
// Streaming token calculation - calculate output tokens from accumulated content
// This provides more accurate token counting than simple character division
if totalUsage.OutputTokens == 0 && accumulatedContent.Len() > 0 {
// Try to use tiktoken for accurate counting
if enc, err := tokenizerForModel(model); err == nil {
if tokenCount, countErr := enc.Count(accumulatedContent.String()); countErr == nil {
totalUsage.OutputTokens = int64(tokenCount)
log.Debugf("kiro: streamToChannel calculated output tokens using tiktoken: %d", totalUsage.OutputTokens)
} else {
// Fallback on count error: estimate from character count
totalUsage.OutputTokens = int64(accumulatedContent.Len() / 4)
if totalUsage.OutputTokens == 0 {
totalUsage.OutputTokens = 1
}
log.Debugf("kiro: streamToChannel tiktoken count failed, estimated from chars: %d", totalUsage.OutputTokens)
}
} else {
// Fallback: estimate from character count (roughly 4 chars per token)
totalUsage.OutputTokens = int64(accumulatedContent.Len() / 4)
if totalUsage.OutputTokens == 0 {
totalUsage.OutputTokens = 1
}
log.Debugf("kiro: streamToChannel estimated output tokens from chars: %d (content len: %d)", totalUsage.OutputTokens, accumulatedContent.Len())
}
} else if totalUsage.OutputTokens == 0 && outputLen > 0 {
// Legacy fallback using outputLen
totalUsage.OutputTokens = int64(outputLen / 4)
if totalUsage.OutputTokens == 0 {
totalUsage.OutputTokens = 1
@@ -2173,6 +2356,128 @@ func (e *KiroExecutor) isTokenExpired(accessToken string) bool {
return isExpired
}
// ============================================================================
// Message Merging Support - Merge adjacent messages with the same role
// Based on AIClient-2-API implementation
// ============================================================================
// mergeAdjacentMessages merges adjacent messages with the same role.
// This reduces API call complexity and improves compatibility.
// Based on AIClient-2-API implementation.
func mergeAdjacentMessages(messages []gjson.Result) []gjson.Result {
if len(messages) <= 1 {
return messages
}
var merged []gjson.Result
for _, msg := range messages {
if len(merged) == 0 {
merged = append(merged, msg)
continue
}
lastMsg := merged[len(merged)-1]
currentRole := msg.Get("role").String()
lastRole := lastMsg.Get("role").String()
if currentRole == lastRole {
// Merge content from current message into last message
mergedContent := mergeMessageContent(lastMsg, msg)
// Create a new merged message JSON
mergedMsg := createMergedMessage(lastRole, mergedContent)
merged[len(merged)-1] = gjson.Parse(mergedMsg)
} else {
merged = append(merged, msg)
}
}
return merged
}
// mergeMessageContent merges the content of two messages with the same role.
// Handles both string content and array content (with text, tool_use, tool_result blocks).
func mergeMessageContent(msg1, msg2 gjson.Result) string {
content1 := msg1.Get("content")
content2 := msg2.Get("content")
// Extract content blocks from both messages
var blocks1, blocks2 []map[string]interface{}
if content1.IsArray() {
for _, block := range content1.Array() {
blocks1 = append(blocks1, blockToMap(block))
}
} else if content1.Type == gjson.String {
blocks1 = append(blocks1, map[string]interface{}{
"type": "text",
"text": content1.String(),
})
}
if content2.IsArray() {
for _, block := range content2.Array() {
blocks2 = append(blocks2, blockToMap(block))
}
} else if content2.Type == gjson.String {
blocks2 = append(blocks2, map[string]interface{}{
"type": "text",
"text": content2.String(),
})
}
// Merge text blocks if both end/start with text
if len(blocks1) > 0 && len(blocks2) > 0 {
if blocks1[len(blocks1)-1]["type"] == "text" && blocks2[0]["type"] == "text" {
// Merge the last text block of msg1 with the first text block of msg2
text1 := blocks1[len(blocks1)-1]["text"].(string)
text2 := blocks2[0]["text"].(string)
blocks1[len(blocks1)-1]["text"] = text1 + "\n" + text2
blocks2 = blocks2[1:] // Remove the merged block from blocks2
}
}
// Combine all blocks
allBlocks := append(blocks1, blocks2...)
// Convert to JSON
result, _ := json.Marshal(allBlocks)
return string(result)
}
// blockToMap converts a gjson.Result block to a map[string]interface{}
func blockToMap(block gjson.Result) map[string]interface{} {
result := make(map[string]interface{})
block.ForEach(func(key, value gjson.Result) bool {
if value.IsObject() {
result[key.String()] = blockToMap(value)
} else if value.IsArray() {
var arr []interface{}
for _, item := range value.Array() {
if item.IsObject() {
arr = append(arr, blockToMap(item))
} else {
arr = append(arr, item.Value())
}
}
result[key.String()] = arr
} else {
result[key.String()] = value.Value()
}
return true
})
return result
}
// createMergedMessage creates a JSON string for a merged message
func createMergedMessage(role string, content string) string {
msg := map[string]interface{}{
"role": role,
"content": json.RawMessage(content),
}
result, _ := json.Marshal(msg)
return string(result)
}
// ============================================================================
// Tool Calling Support - Embedded tool call parsing and input buffering
// Based on amq2api and AIClient-2-API implementations
@@ -2195,12 +2500,6 @@ var (
whitespaceCollapsePattern = regexp.MustCompile(`\s+`)
// trailingCommaPattern matches trailing commas before closing braces/brackets
trailingCommaPattern = regexp.MustCompile(`,\s*([}\]])`)
// heliosDebugPattern matches [HELIOS_CHK] debug blocks from Kiro/Amazon Q internal systems
// These blocks contain internal state tracking information that should not be exposed to users
// Format: [HELIOS_CHK]\nPhase: ...\nHelios: ...\nAction: ...\nTask: ...\nContext-Map: ...\nNext: ...
// The pattern matches from [HELIOS_CHK] to the end of the "Next:" line (which may span multiple lines until double newline)
heliosDebugPattern = regexp.MustCompile(`(?s)\[HELIOS_CHK\].*?(?:Next:.*?)(?:\n\n|\z)`)
)
// parseEmbeddedToolCalls extracts [Called tool_name with args: {...}] format from text.
@@ -2366,17 +2665,154 @@ func findMatchingBracket(text string, startPos int) int {
}
// repairJSON attempts to fix common JSON issues that may occur in tool call arguments.
// Based on AIClient-2-API's JSON repair implementation.
// Based on AIClient-2-API's JSON repair implementation with a more conservative strategy.
//
// Conservative repair strategy:
// 1. First try to parse JSON directly - if valid, return as-is
// 2. Only attempt repair if parsing fails
// 3. After repair, validate the result - if still invalid, return original
//
// Handles incomplete JSON by balancing brackets and removing trailing incomplete content.
// Uses pre-compiled regex patterns for performance.
func repairJSON(raw string) string {
func repairJSON(jsonString string) string {
// Handle empty or invalid input
if jsonString == "" {
return "{}"
}
str := strings.TrimSpace(jsonString)
if str == "" {
return "{}"
}
// CONSERVATIVE STRATEGY: First try to parse directly
// If the JSON is already valid, return it unchanged
var testParse interface{}
if err := json.Unmarshal([]byte(str), &testParse); err == nil {
log.Debugf("kiro: repairJSON - JSON is already valid, returning unchanged")
return str
}
log.Debugf("kiro: repairJSON - JSON parse failed, attempting repair")
originalStr := str // Keep original for fallback
// First, escape unescaped newlines/tabs within JSON string values
repaired := escapeNewlinesInStrings(raw)
str = escapeNewlinesInStrings(str)
// Remove trailing commas before closing braces/brackets
repaired = trailingCommaPattern.ReplaceAllString(repaired, "$1")
// Note: unquotedKeyPattern removed - it incorrectly matches content inside
// JSON string values (e.g., "classDef fill:#1a1a2e,stroke:#00d4ff" would have
// ",stroke:" incorrectly treated as an unquoted key)
return repaired
str = trailingCommaPattern.ReplaceAllString(str, "$1")
// Calculate bracket balance to detect incomplete JSON
braceCount := 0 // {} balance
bracketCount := 0 // [] balance
inString := false
escape := false
lastValidIndex := -1
for i := 0; i < len(str); i++ {
char := str[i]
// Handle escape sequences
if escape {
escape = false
continue
}
if char == '\\' {
escape = true
continue
}
// Handle string boundaries
if char == '"' {
inString = !inString
continue
}
// Skip characters inside strings (they don't affect bracket balance)
if inString {
continue
}
// Track bracket balance
switch char {
case '{':
braceCount++
case '}':
braceCount--
case '[':
bracketCount++
case ']':
bracketCount--
}
// Record last valid position (where brackets are balanced or positive)
if braceCount >= 0 && bracketCount >= 0 {
lastValidIndex = i
}
}
// If brackets are unbalanced, try to repair
if braceCount > 0 || bracketCount > 0 {
// Truncate to last valid position if we have incomplete content
if lastValidIndex > 0 && lastValidIndex < len(str)-1 {
// Check if truncation would help (only truncate if there's trailing garbage)
truncated := str[:lastValidIndex+1]
// Recount brackets after truncation
braceCount = 0
bracketCount = 0
inString = false
escape = false
for i := 0; i < len(truncated); i++ {
char := truncated[i]
if escape {
escape = false
continue
}
if char == '\\' {
escape = true
continue
}
if char == '"' {
inString = !inString
continue
}
if inString {
continue
}
switch char {
case '{':
braceCount++
case '}':
braceCount--
case '[':
bracketCount++
case ']':
bracketCount--
}
}
str = truncated
}
// Add missing closing brackets
for braceCount > 0 {
str += "}"
braceCount--
}
for bracketCount > 0 {
str += "]"
bracketCount--
}
}
// CONSERVATIVE STRATEGY: Validate repaired JSON
// If repair didn't produce valid JSON, return original string
if err := json.Unmarshal([]byte(str), &testParse); err != nil {
log.Warnf("kiro: repairJSON - repair failed to produce valid JSON, returning original")
return originalStr
}
log.Debugf("kiro: repairJSON - successfully repaired JSON")
return str
}
// escapeNewlinesInStrings escapes literal newlines, tabs, and other control characters
@@ -2568,39 +3004,3 @@ func deduplicateToolUses(toolUses []kiroToolUse) []kiroToolUse {
return unique
}
// filterHeliosDebugInfo removes [HELIOS_CHK] debug blocks from Kiro/Amazon Q responses.
// These blocks contain internal state tracking information from the Helios system
// that should not be exposed to end users.
//
// The [HELIOS_CHK] block format typically looks like:
//
// [HELIOS_CHK]
// Phase: E (Evaluate & Evolve)
// Helios: [P1_Intent: OK] [P2_Research: OK] [P3_Strategy: OK]
// Action: [TEXT_RESPONSE]
// Task: #T001 - Some task description
// Context-Map: [SYNCED]
// Next: Some next action description...
//
// This function is currently DISABLED (commented out in callers) pending further testing.
// To enable, uncomment the filterHeliosDebugInfo calls in parseEventStream() and streamToChannel().
func filterHeliosDebugInfo(content string) string {
if !strings.Contains(content, "[HELIOS_CHK]") {
return content
}
// Remove [HELIOS_CHK] blocks
filtered := heliosDebugPattern.ReplaceAllString(content, "")
// Clean up any resulting double newlines or leading/trailing whitespace
filtered = strings.TrimSpace(filtered)
// Log when filtering occurs for debugging purposes
if filtered != content {
log.Debugf("kiro: filtered HELIOS debug info from response (original len: %d, filtered len: %d)",
len(content), len(filtered))
}
return filtered
}