mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-13 19:20:06 +00:00
fd960921ca
- Remove deprecated GenerateWithLoopAndStreaming and TreeManager AppendFantasyMessage / AddFantasyMessages / GetFantasyMessages to close the SDK leakage caused by the kit.TreeManager type alias - Switch extensionAPI method signatures to local Extension* aliases so pkg.go.dev signatures no longer expose internal package names - Bundle runNormalMode dependencies into a runModeDeps struct, shrinking the runNonInteractive and runInteractive call sites from 40+ positional args to (ctx, deps) - Add generic subscribeTyped[E Event] helper and collapse ~30 typed OnXxx wrappers in pkg/kit/events.go onto it (public signatures unchanged) - Extract setupBashPipes / interpretBashExit in internal/core/bash.go to deduplicate the buffered and streaming execution paths - Extract resolveAutoRouteAPIKey and wrapProviderErr helpers in internal/models/providers.go and uniformly apply them across every createXxxProvider site - Reimplement internal/extensions/watcher.go as a thin wrapper over the general-purpose internal/watcher.ContentWatcher, eliminating ~130 LOC of duplicated fsnotify logic while preserving the existing test API - Add ctx.Err() pre-flight checks in executeRead / Write / Edit / Ls so cancellation actually short-circuits pure file-IO tools
138 lines
3.8 KiB
Go
138 lines
3.8 KiB
Go
package core
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"charm.land/fantasy"
|
|
)
|
|
|
|
type readArgs struct {
|
|
Path string `json:"path"`
|
|
Offset int `json:"offset,omitempty"`
|
|
Limit int `json:"limit,omitempty"`
|
|
}
|
|
|
|
// NewReadTool creates the read core tool.
|
|
func NewReadTool(opts ...ToolOption) fantasy.AgentTool {
|
|
cfg := ApplyOptions(opts)
|
|
return &coreTool{
|
|
info: fantasy.ToolInfo{
|
|
Name: "read",
|
|
Description: "Read the contents of a file. Output is truncated to 2000 lines or 50KB. Use offset/limit for large files. Use offset to continue reading until complete.",
|
|
Parameters: map[string]any{
|
|
"path": map[string]any{
|
|
"type": "string",
|
|
"description": "Path to the file to read (relative or absolute)",
|
|
},
|
|
"offset": map[string]any{
|
|
"type": "number",
|
|
"description": "Line number to start reading from (1-indexed)",
|
|
},
|
|
"limit": map[string]any{
|
|
"type": "number",
|
|
"description": "Maximum number of lines to read",
|
|
},
|
|
},
|
|
Required: []string{"path"},
|
|
Parallel: true,
|
|
},
|
|
handler: func(ctx context.Context, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
|
|
return executeRead(ctx, call, cfg.WorkDir)
|
|
},
|
|
}
|
|
}
|
|
|
|
func executeRead(ctx context.Context, call fantasy.ToolCall, workDir string) (fantasy.ToolResponse, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return fantasy.ToolResponse{}, err
|
|
}
|
|
var args readArgs
|
|
if err := parseArgs(call.Input, &args); err != nil {
|
|
return fantasy.NewTextErrorResponse("path parameter is required"), nil
|
|
}
|
|
if args.Path == "" {
|
|
return fantasy.NewTextErrorResponse("path parameter is required"), nil
|
|
}
|
|
|
|
absPath, err := resolvePathWithWorkDir(args.Path, workDir)
|
|
if err != nil {
|
|
return fantasy.NewTextErrorResponse(fmt.Sprintf("invalid path: %v", err)), nil
|
|
}
|
|
|
|
// Check if path is a directory
|
|
info, err := os.Stat(absPath)
|
|
if err != nil {
|
|
return fantasy.NewTextErrorResponse(fmt.Sprintf("cannot access '%s': %v", args.Path, err)), nil
|
|
}
|
|
|
|
if info.IsDir() {
|
|
return fantasy.NewTextErrorResponse(fmt.Sprintf("'%s' is a directory, not a file. Use the ls tool to list directory contents.", args.Path)), nil
|
|
}
|
|
|
|
content, err := os.ReadFile(absPath)
|
|
if err != nil {
|
|
return fantasy.NewTextErrorResponse(fmt.Sprintf("failed to read file: %v", err)), nil
|
|
}
|
|
|
|
lines := strings.Split(string(content), "\n")
|
|
totalLines := len(lines)
|
|
|
|
// Apply offset (1-indexed)
|
|
offset := 0
|
|
if args.Offset > 0 {
|
|
offset = args.Offset - 1
|
|
if offset >= totalLines {
|
|
return fantasy.NewTextResponse(fmt.Sprintf("offset %d exceeds file length (%d lines)", args.Offset, totalLines)), nil
|
|
}
|
|
lines = lines[offset:]
|
|
}
|
|
|
|
// Apply limit
|
|
maxLines := defaultMaxLines
|
|
if args.Limit > 0 {
|
|
maxLines = args.Limit
|
|
}
|
|
if len(lines) > maxLines {
|
|
lines = lines[:maxLines]
|
|
}
|
|
|
|
// Number lines
|
|
var result strings.Builder
|
|
for i, line := range lines {
|
|
lineNum := offset + i + 1
|
|
fmt.Fprintf(&result, "%d: %s\n", lineNum, line)
|
|
}
|
|
|
|
output := result.String()
|
|
tr := truncateHead(output, 0, defaultMaxBytes)
|
|
|
|
// Add truncation notice
|
|
if len(lines) < totalLines-offset {
|
|
tr.Content += fmt.Sprintf("\n[showing lines %d-%d of %d total. Use offset=%d to continue reading]",
|
|
offset+1, offset+len(lines), totalLines, offset+len(lines)+1)
|
|
}
|
|
|
|
return fantasy.NewTextResponse(tr.Content), nil
|
|
}
|
|
|
|
// resolvePathWithWorkDir resolves a path to an absolute path relative to the
|
|
// given workDir. If workDir is empty, os.Getwd() is used.
|
|
func resolvePathWithWorkDir(path, workDir string) (string, error) {
|
|
if filepath.IsAbs(path) {
|
|
return filepath.Clean(path), nil
|
|
}
|
|
baseDir := workDir
|
|
if baseDir == "" {
|
|
var err error
|
|
baseDir, err = os.Getwd()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get working directory: %w", err)
|
|
}
|
|
}
|
|
return filepath.Clean(filepath.Join(baseDir, path)), nil
|
|
}
|