Files
kit/internal/core/write.go
T
Ed Zynda c94edc929b feat: add rich tool metadata to SDK and extension events (Gaps 1-8)
Thread ToolCallID, ToolKind, ParsedArgs, FileDiff metadata, StopReason,
SessionID, and StructuredMessages across the SDK event bus, extension
wrapper, app bridge, hooks, and ACP server layers.

- Gap 1: ToolCallID from Fantasy's ToolCallContent threaded end-to-end
- Gap 2: ToolKind via static lookup (execute/edit/read/search/agent)
- Gap 3+4: FileDiffInfo with DiffBlocks via fantasy.ToolResponse.Metadata
- Gap 5: StopReason from Fantasy FinishReason on TurnEndEvent/TurnResult
- Gap 6: Subagent sessions now opt-out (NoSession); SessionID in JSON output
- Gap 7: GetStructuredMessages() returns typed ContentParts
- Gap 8: ParsedArgs map[string]any on tool events for convenience

Edit/write tools attach structured diff metadata. ACP server uses real
ToolCallIDs. Extension and SDK events kept in sync with matching fields.
2026-03-16 11:10:05 +03:00

99 lines
2.9 KiB
Go

package core
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"charm.land/fantasy"
)
type writeArgs struct {
Path string `json:"path"`
Content string `json:"content"`
}
// NewWriteTool creates the write core tool.
func NewWriteTool(opts ...ToolOption) fantasy.AgentTool {
cfg := ApplyOptions(opts)
return &coreTool{
info: fantasy.ToolInfo{
Name: "write",
Description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.",
Parameters: map[string]any{
"path": map[string]any{
"type": "string",
"description": "Path to the file to write (relative or absolute)",
},
"content": map[string]any{
"type": "string",
"description": "Content to write to the file",
},
},
Required: []string{"path", "content"},
},
handler: func(ctx context.Context, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
return executeWrite(ctx, call, cfg.WorkDir)
},
}
}
func executeWrite(ctx context.Context, call fantasy.ToolCall, workDir string) (fantasy.ToolResponse, error) {
var args writeArgs
if err := parseArgs(call.Input, &args); err != nil {
return fantasy.NewTextErrorResponse("path and content parameters are 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
}
// Read existing content before writing (for diff metadata).
var beforeContent string
isNew := true
if existing, readErr := os.ReadFile(absPath); readErr == nil {
beforeContent = string(existing)
isNew = false
}
// Create parent directories
dir := filepath.Dir(absPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fantasy.NewTextErrorResponse(fmt.Sprintf("failed to create directories: %v", err)), nil
}
if err := os.WriteFile(absPath, []byte(args.Content), 0644); err != nil {
return fantasy.NewTextErrorResponse(fmt.Sprintf("failed to write file: %v", err)), nil
}
resp := fantasy.NewTextResponse(fmt.Sprintf("Wrote %d bytes to %s", len(args.Content), args.Path))
return fantasy.WithResponseMetadata(resp, writeDiffMeta(absPath, beforeContent, args.Content, isNew)), nil
}
// writeDiffMeta builds the structured metadata attached to write tool responses.
func writeDiffMeta(path, beforeContent, afterContent string, isNew bool) map[string]any {
additions := strings.Count(afterContent, "\n") + 1
deletions := 0
if !isNew {
deletions = strings.Count(beforeContent, "\n") + 1
}
return map[string]any{
"file_diffs": []map[string]any{{
"path": path,
"additions": additions,
"deletions": deletions,
"is_new": isNew,
"diff_blocks": []map[string]any{{
"old_text": beforeContent,
"new_text": afterContent,
}},
}},
}
}