diff --git a/internal/config/config.go b/internal/config/config.go index b8a70505da30a1e3f274e95ef89f606d7b6db9d4..4a8c47dd8686ca562b60c97efa2c15c31daf88ad 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -223,6 +223,15 @@ func loadConfig(cwd string, debug bool) (*Config, error) { } } + messagesPath := fmt.Sprintf("%s/%s", cfg.Options.DataDirectory, "messages") + + if _, err := os.Stat(messagesPath); os.IsNotExist(err) { + if err := os.MkdirAll(messagesPath, 0o756); err != nil { + return cfg, fmt.Errorf("failed to create directory: %w", err) + } + } + logging.MessageDir = messagesPath + sloggingFileWriter, err := os.OpenFile(loggingFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666) if err != nil { return cfg, fmt.Errorf("failed to open log file: %w", err) diff --git a/internal/fsext/fileutil.go b/internal/fsext/fileutil.go index 1726f916b07ac9ac0defdf7c06dae7a8768b30c5..cc430d73edb34cc5b81e1e36ecbad550bf4312fe 100644 --- a/internal/fsext/fileutil.go +++ b/internal/fsext/fileutil.go @@ -55,7 +55,7 @@ func GetRgSearchCmd(pattern, path, include string) *exec.Cmd { return nil } // Use -n to show line numbers and include the matched line - args := []string{"-n", pattern} + args := []string{"-H", "-n", pattern} if include != "" { args = append(args, "--glob", include) } diff --git a/internal/llm/agent/agent.go b/internal/llm/agent/agent.go index 5568c8a619287619900e8d5e5d5d44c2e85de446..ec4f160e650d4eafa497b92b77b8dc26c2aca40b 100644 --- a/internal/llm/agent/agent.go +++ b/internal/llm/agent/agent.go @@ -363,6 +363,7 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string, attac } func (a *agent) processGeneration(ctx context.Context, sessionID, content string, attachmentParts []message.ContentPart) AgentEvent { + cfg := config.Get() // List existing messages; if none, start title generation asynchronously. msgs, err := a.messages.List(ctx, sessionID) if err != nil { @@ -421,7 +422,13 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string } return a.err(fmt.Errorf("failed to process events: %w", err)) } - logging.Info("Result", "message", agentMessage.FinishReason(), "toolResults", toolResults) + if cfg.Options.Debug { + seqId := (len(msgHistory) + 1) / 2 + toolResultFilepath := logging.WriteToolResultsJson(sessionID, seqId, toolResults) + logging.Info("Result", "message", agentMessage.FinishReason(), "toolResults", "{}", "filepath", toolResultFilepath) + } else { + logging.Info("Result", "message", agentMessage.FinishReason(), "toolResults", toolResults) + } if (agentMessage.FinishReason() == message.FinishReasonToolUse) && toolResults != nil { // We are not done, we need to respond with the tool response msgHistory = append(msgHistory, agentMessage, *toolResults) @@ -445,6 +452,7 @@ func (a *agent) createUserMessage(ctx context.Context, sessionID, content string } func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msgHistory []message.Message) (message.Message, *message.Message, error) { + ctx = context.WithValue(ctx, tools.SessionIDContextKey, sessionID) eventChan := a.provider.StreamResponse(ctx, msgHistory, a.tools) assistantMsg, err := a.messages.Create(ctx, sessionID, message.CreateMessageParams{ @@ -459,7 +467,6 @@ func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msg // Add the session and message ID into the context if needed by tools. ctx = context.WithValue(ctx, tools.MessageIDContextKey, assistantMsg.ID) - ctx = context.WithValue(ctx, tools.SessionIDContextKey, sessionID) // Process each event in the stream. for event := range eventChan { @@ -491,10 +498,17 @@ func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msg default: // Continue processing var tool tools.BaseTool - for _, availableTools := range a.tools { - if availableTools.Info().Name == toolCall.Name { - tool = availableTools + for _, availableTool := range a.tools { + if availableTool.Info().Name == toolCall.Name { + tool = availableTool + break } + // Monkey patch for Copilot Sonnet-4 tool repetition obfuscation + // if strings.HasPrefix(toolCall.Name, availableTool.Info().Name) && + // strings.HasPrefix(toolCall.Name, availableTool.Info().Name+availableTool.Info().Name) { + // tool = availableTool + // break + // } } // Tool not found @@ -665,6 +679,7 @@ func (a *agent) Summarize(ctx context.Context, sessionID string) error { a.Publish(pubsub.CreatedEvent, event) return } + summarizeCtx = context.WithValue(summarizeCtx, tools.SessionIDContextKey, sessionID) if len(msgs) == 0 { event = AgentEvent{ diff --git a/internal/llm/agent/mcp-tools.go b/internal/llm/agent/mcp-tools.go index 1950324fa3ed4dbd9de358d18023247b0bb429e7..fed0c06196c600bb5ecc06d1f92a1f3a07f14b38 100644 --- a/internal/llm/agent/mcp-tools.go +++ b/internal/llm/agent/mcp-tools.go @@ -37,11 +37,15 @@ func (b *mcpTool) Name() string { } func (b *mcpTool) Info() tools.ToolInfo { + required := b.tool.InputSchema.Required + if required == nil { + required = make([]string, 0) + } return tools.ToolInfo{ Name: fmt.Sprintf("%s_%s", b.mcpName, b.tool.Name), Description: b.tool.Description, Parameters: b.tool.InputSchema.Properties, - Required: b.tool.InputSchema.Required, + Required: required, } } diff --git a/internal/logging/logger.go b/internal/logging/logger.go index 9c2cfb50f33d27d52b9acb3009859f3509484253..98e7b23ae3b4025acbcd2585a7cfd3f3c9230623 100644 --- a/internal/logging/logger.go +++ b/internal/logging/logger.go @@ -4,16 +4,33 @@ import ( "fmt" "log/slog" "os" + // "path/filepath" + "encoding/json" + "runtime" "runtime/debug" + "sync" "time" ) +func getCaller() string { + var caller string + if _, file, line, ok := runtime.Caller(2); ok { + // caller = fmt.Sprintf("%s:%d", filepath.Base(file), line) + caller = fmt.Sprintf("%s:%d", file, line) + } else { + caller = "unknown" + } + return caller +} func Info(msg string, args ...any) { - slog.Info(msg, args...) + source := getCaller() + slog.Info(msg, append([]any{"source", source}, args...)...) } func Debug(msg string, args ...any) { - slog.Debug(msg, args...) + // slog.Debug(msg, args...) + source := getCaller() + slog.Debug(msg, append([]any{"source", source}, args...)...) } func Warn(msg string, args ...any) { @@ -76,3 +93,115 @@ func RecoverPanic(name string, cleanup func()) { } } } + +// Message Logging for Debug +var MessageDir string + +func GetSessionPrefix(sessionId string) string { + return sessionId[:8] +} + +var sessionLogMutex sync.Mutex + +func AppendToSessionLogFile(sessionId string, filename string, content string) string { + if MessageDir == "" || sessionId == "" { + return "" + } + sessionPrefix := GetSessionPrefix(sessionId) + + sessionLogMutex.Lock() + defer sessionLogMutex.Unlock() + + sessionPath := fmt.Sprintf("%s/%s", MessageDir, sessionPrefix) + if _, err := os.Stat(sessionPath); os.IsNotExist(err) { + if err := os.MkdirAll(sessionPath, 0o766); err != nil { + Error("Failed to create session directory", "dirpath", sessionPath, "error", err) + return "" + } + } + + filePath := fmt.Sprintf("%s/%s", sessionPath, filename) + + f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + Error("Failed to open session log file", "filepath", filePath, "error", err) + return "" + } + defer f.Close() + + // Append chunk to file + _, err = f.WriteString(content) + if err != nil { + Error("Failed to write chunk to session log file", "filepath", filePath, "error", err) + return "" + } + return filePath +} + +func WriteRequestMessageJson(sessionId string, requestSeqId int, message any) string { + if MessageDir == "" || sessionId == "" || requestSeqId <= 0 { + return "" + } + msgJson, err := json.Marshal(message) + if err != nil { + Error("Failed to marshal message", "session_id", sessionId, "request_seq_id", requestSeqId, "error", err) + return "" + } + return WriteRequestMessage(sessionId, requestSeqId, string(msgJson)) +} + +func WriteRequestMessage(sessionId string, requestSeqId int, message string) string { + if MessageDir == "" || sessionId == "" || requestSeqId <= 0 { + return "" + } + filename := fmt.Sprintf("%d_request.json", requestSeqId) + + return AppendToSessionLogFile(sessionId, filename, message) +} + +func AppendToStreamSessionLogJson(sessionId string, requestSeqId int, jsonableChunk any) string { + if MessageDir == "" || sessionId == "" || requestSeqId <= 0 { + return "" + } + chunkJson, err := json.Marshal(jsonableChunk) + if err != nil { + Error("Failed to marshal message", "session_id", sessionId, "request_seq_id", requestSeqId, "error", err) + return "" + } + return AppendToStreamSessionLog(sessionId, requestSeqId, string(chunkJson)) +} + +func AppendToStreamSessionLog(sessionId string, requestSeqId int, chunk string) string { + if MessageDir == "" || sessionId == "" || requestSeqId <= 0 { + return "" + } + filename := fmt.Sprintf("%d_response_stream.log", requestSeqId) + return AppendToSessionLogFile(sessionId, filename, chunk) +} + +func WriteChatResponseJson(sessionId string, requestSeqId int, response any) string { + if MessageDir == "" || sessionId == "" || requestSeqId <= 0 { + return "" + } + responseJson, err := json.Marshal(response) + if err != nil { + Error("Failed to marshal response", "session_id", sessionId, "request_seq_id", requestSeqId, "error", err) + return "" + } + filename := fmt.Sprintf("%d_response.json", requestSeqId) + + return AppendToSessionLogFile(sessionId, filename, string(responseJson)) +} + +func WriteToolResultsJson(sessionId string, requestSeqId int, toolResults any) string { + if MessageDir == "" || sessionId == "" || requestSeqId <= 0 { + return "" + } + toolResultsJson, err := json.Marshal(toolResults) + if err != nil { + Error("Failed to marshal tool results", "session_id", sessionId, "request_seq_id", requestSeqId, "error", err) + return "" + } + filename := fmt.Sprintf("%d_tool_results.json", requestSeqId) + return AppendToSessionLogFile(sessionId, filename, string(toolResultsJson)) +} diff --git a/internal/logging/writer.go b/internal/logging/writer.go index 8775f3752d52f3141e1cf51a11a734c3c6e523b1..e821338658a316ad0ffb6178e42c046addbfd1ab 100644 --- a/internal/logging/writer.go +++ b/internal/logging/writer.go @@ -45,6 +45,7 @@ type writer struct{} func (w *writer) Write(p []byte) (int, error) { d := logfmt.NewDecoder(bytes.NewReader(p)) + for d.ScanRecord() { msg := LogMessage{ ID: fmt.Sprintf("%d", time.Now().UnixNano()),