Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 28 additions & 25 deletions internal/tool/browser.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,33 +117,36 @@ func (BrowserTool) Description() string {
return "Control a headless Chrome browser: navigate to URLs, click and type into elements, extract page text/HTML/title, and take screenshots."
}

func (BrowserTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"type": "string",
"enum": []string{"navigate", "content", "screenshot", "click", "type", "title", "location", "ax_snapshot", "close"},
"description": "Actions. navigate/content/screenshot/title/location as named. ax_snapshot: compressed accessibility tree with uid handles (query optional); click/type then accept uid from that snapshot instead of a CSS selector. close shuts the browser down.",
},
"url": map[string]interface{}{"type": "string", "description": "Target URL (http/https) for navigate/screenshot"},
"selector": map[string]interface{}{"type": "string", "description": "CSS selector for content/click/type and optional navigate wait"},
"uid": map[string]interface{}{"type": "string", "description": "Element uid from the last ax_snapshot; preferred over selector for click/type"},
"text": map[string]interface{}{"type": "string", "description": "Text to type (type action)"},
"clear": map[string]interface{}{"type": "boolean", "description": "Clear the field before typing"},
"path": map[string]interface{}{"type": "string", "description": "File path to save a screenshot to"},
"wait_ms": map[string]interface{}{"type": "number", "description": "Milliseconds to wait after navigation (default 800)"},
"html": map[string]interface{}{"type": "boolean", "description": "Return outer HTML instead of inner text (content action)"},
"max_chars": map[string]interface{}{
"type": "number", "description": "Truncate extracted content to this many characters (default 20000)",
},
// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (BrowserTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"action": {Type: "string", Enum: []interface{}{"navigate", "content", "screenshot", "click", "type", "title", "location", "ax_snapshot", "close"}, Description: "Actions. navigate/content/screenshot/title/location as named. ax_snapshot: compressed accessibility tree with uid handles (query optional); click/type then accept uid from that snapshot instead of a CSS selector. close shuts the browser down."},
"url": {Type: "string", Description: "Target URL (http/https) for navigate/screenshot"},
"selector": {Type: "string", Description: "CSS selector for content/click/type and optional navigate wait"},
"uid": {Type: "string", Description: "Element uid from the last ax_snapshot; preferred over selector for click/type"},
"text": {Type: "string", Description: "Text to type (type action)"},
"clear": {Type: "boolean", Description: "Clear the field before typing"},
"path": {Type: "string", Description: "File path to save a screenshot to"},
"wait_ms": {Type: "number", Description: "Milliseconds to wait after navigation (default 800)"},
"html": {Type: "boolean", Description: "Return outer HTML instead of inner text (content action)"},
"max_chars": {Type: "number", Description: "Truncate extracted content to this many characters (default 20000)"},
},
"required": []string{"action"},
Required: []string{"action"},
}
}

// browserParams mirrors the declared JSON schema.
type browserParams struct {
func (BrowserTool) Parameters() map[string]interface{} {
return browserSchema.ToJSONSchema()
}

// browserSchema is the single source of truth for Browser's input schema.
var browserSchema = BrowserTool{}.Schema()

// BrowserInput is the typed input for BrowserTool.
type BrowserInput struct {
Action string `json:"action"`
URL string `json:"url"`
Selector string `json:"selector"`
Expand All @@ -157,8 +160,8 @@ type browserParams struct {
}

func (BrowserTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var p browserParams
if err := json.Unmarshal(input, &p); err != nil {
p, err := DecodeInput[BrowserInput]("Browser", input)
if err != nil {
return "", err
}
p.Action = strings.ToLower(strings.TrimSpace(p.Action))
Expand Down
35 changes: 17 additions & 18 deletions internal/tool/computer_use.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,28 +22,27 @@ func (ComputerUseTool) Description() string {
return "Operate the host desktop (snapshot UI, click, type, scroll, keypress, screenshot) via a pluggable backend. Requires a wired computer backend (see SetComputerBackend)."
}

func (ComputerUseTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"type": "string",
"enum": []string{"snapshot", "click", "type", "scroll", "press", "screenshot"},
"description": "snapshot: dump the UI; click: click an element/ref; type: enter text; scroll: scroll; press: send key chord; screenshot: capture screen.",
},
"target": map[string]interface{}{
"type": "string",
"description": "Element ref (e.g. @e1) or label for click/type/scroll.",
},
"text": map[string]interface{}{
"type": "string",
"description": "Text to type or key chord for press.",
},
// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (ComputerUseTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"action": {Type: "string", Enum: []interface{}{"snapshot", "click", "type", "scroll", "press", "screenshot"}, Description: "snapshot: dump the UI; click: click an element/ref; type: enter text; scroll: scroll; press: send key chord; screenshot: capture screen."},
"target": {Type: "string", Description: "Element ref (e.g. @e1) or label for click/type/scroll."},
"text": {Type: "string", Description: "Text to type or key chord for press."},
},
"required": []string{"action"},
Required: []string{"action"},
}
}

func (ComputerUseTool) Parameters() map[string]interface{} {
return computerUseSchema.ToJSONSchema()
}

// computerUseSchema is the single source of truth for ComputerUse's input schema.
var computerUseSchema = ComputerUseTool{}.Schema()

// ComputerBackend is the pluggable host-desktop automation backend.
type ComputerBackend interface {
// Name identifies the backend for provenance.
Expand Down
92 changes: 37 additions & 55 deletions internal/tool/media_generation.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ type MediaOptions struct {
DurationSec int `json:"duration_seconds,omitempty"`
}

// GenerateMediaInput is the typed input for GenerateMediaTool.
type GenerateMediaInput struct {
Kind string `json:"kind"`
Prompt string `json:"prompt"`
Source string `json:"source"`
AspectRatio string `json:"aspect_ratio"`
Resolution string `json:"resolution"`
Count int `json:"count"`
DurationSec int `json:"duration_seconds"`
OutputPath string `json:"output_path"`
}

// MediaAsset is the persisted, locally-available representation returned to the
// model and user.
type MediaAsset struct {
Expand Down Expand Up @@ -96,65 +108,36 @@ func (GenerateMediaTool) Description() string {
return "Generate an image or short video from a text prompt (and optionally edit an existing local image or URL). The generated asset is saved locally and its path is returned so you can reference it directly."
}

func (GenerateMediaTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"kind": map[string]interface{}{
"type": "string",
"enum": []string{"image", "video"},
"description": "The kind of media to generate.",
},
"prompt": map[string]interface{}{
"type": "string",
"description": "Text description of the media to generate.",
},
"source": map[string]interface{}{
"type": "string",
"description": "Optional local file path or URL used for image editing / image-to-video.",
},
"aspect_ratio": map[string]interface{}{
"type": "string",
"description": "Aspect ratio, e.g. 16:9, 1:1, 9:16.",
},
"resolution": map[string]interface{}{
"type": "string",
"description": "Resolution: images 1k or 2k; video 480p or 720p.",
},
"count": map[string]interface{}{
"type": "integer",
"minimum": 1,
"maximum": 4,
"description": "Number of images to generate (default 1).",
},
"duration_seconds": map[string]interface{}{
"type": "integer",
"minimum": 1,
"maximum": 15,
"description": "Video duration in seconds (default 5).",
},
"output_path": map[string]interface{}{
"type": "string",
"description": "Optional explicit output directory; defaults to the user state generated-media directory.",
},
// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (GenerateMediaTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"kind": {Type: "string", Enum: []interface{}{"image", "video"}, Description: "The kind of media to generate."},
"prompt": {Type: "string", Description: "Text description of the media to generate."},
"source": {Type: "string", Description: "Optional local file path or URL used for image editing / image-to-video."},
"aspect_ratio": {Type: "string", Description: "Aspect ratio, e.g. 16:9, 1:1, 9:16."},
"resolution": {Type: "string", Description: "Resolution: images 1k or 2k; video 480p or 720p."},
"count": {Type: "integer", Minimum: 1, Maximum: 4, Description: "Number of images to generate (default 1)."},
"duration_seconds": {Type: "integer", Minimum: 1, Maximum: 15, Description: "Video duration in seconds (default 5)."},
"output_path": {Type: "string", Description: "Optional explicit output directory; defaults to the user state generated-media directory."},
},
"required": []string{"kind", "prompt"},
Required: []string{"kind", "prompt"},
}
}

func (GenerateMediaTool) Parameters() map[string]interface{} {
return generateMediaSchema.ToJSONSchema()
}

// generateMediaSchema is the single source of truth for GenerateMedia's input schema.
var generateMediaSchema = GenerateMediaTool{}.Schema()

func (GenerateMediaTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var p struct {
Kind string `json:"kind"`
Prompt string `json:"prompt"`
Source string `json:"source"`
AspectRatio string `json:"aspect_ratio"`
Resolution string `json:"resolution"`
Count int `json:"count"`
DurationSec int `json:"duration_seconds"`
OutputPath string `json:"output_path"`
}
if err := json.Unmarshal(input, &p); err != nil {
return "", fmt.Errorf("invalid input: %w", err)
p, err := DecodeInput[GenerateMediaInput]("GenerateMedia", input)
if err != nil {
return "", err
}
p.Kind = strings.ToLower(strings.TrimSpace(p.Kind))
p.Prompt = strings.TrimSpace(p.Prompt)
Expand Down Expand Up @@ -203,7 +186,6 @@ func (GenerateMediaTool) Execute(ctx context.Context, input json.RawMessage) (st
}

var results []MediaResult
var err error
switch p.Kind {
case "image":
results, err = mediaEngine.GenerateImage(ctx, p.Prompt, source, opts)
Expand Down
44 changes: 27 additions & 17 deletions internal/tool/multiedit.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,38 @@ func (MultiEditTool) Description() string {
return "Apply multiple edits to a single file in one call. Each edit replaces an exact string match. Edits are applied sequentially."
}

func (MultiEditTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"file_path": map[string]interface{}{"type": "string", "description": "File path to edit"},
"edits": map[string]interface{}{
"type": "array",
"description": "Array of edit operations",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"old_string": map[string]interface{}{"type": "string", "description": "Exact string to find"},
"new_string": map[string]interface{}{"type": "string", "description": "Replacement string"},
"replace_all": map[string]interface{}{"type": "boolean", "description": "Replace all occurrences (default: first only)"},
// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (MultiEditTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"file_path": {Type: "string", Description: "File path to edit"},
"edits": {
Type: "array",
Description: "Array of edit operations",
Items: &SchemaProperty{
Type: "object",
Properties: map[string]SchemaProperty{
"old_string": {Type: "string", Description: "Exact string to find"},
"new_string": {Type: "string", Description: "Replacement string"},
"replace_all": {Type: "boolean", Description: "Replace all occurrences (default: first only)"},
},
},
},
},
}
}

type multiEditParams struct {
func (MultiEditTool) Parameters() map[string]interface{} {
return multiEditSchema.ToJSONSchema()
}

// multiEditSchema is the single source of truth for MultiEdit's input schema.
var multiEditSchema = MultiEditTool{}.Schema()

// MultiEditInput is the typed input for MultiEditTool.
type MultiEditInput struct {
FilePath string `json:"file_path"`
Edits []struct {
OldString string `json:"old_string"`
Expand All @@ -48,8 +58,8 @@ type multiEditParams struct {
}

func (MultiEditTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var p multiEditParams
if err := json.Unmarshal(input, &p); err != nil {
p, err := DecodeInput[MultiEditInput]("MultiEdit", input)
if err != nil {
return "", err
}
if p.FilePath == "" {
Expand Down
37 changes: 23 additions & 14 deletions internal/tool/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,31 +408,40 @@ func levenshteinDistance(a, b string) int {
// PatchTool implements the Tool interface for applying structured patches.
type PatchTool struct{}

// PatchInput is the typed input for PatchTool.
type PatchInput struct {
Patch string `json:"patch"`
}

func (PatchTool) Name() string { return "Patch" }

func (PatchTool) Description() string {
return "Apply a structured patch to one or more files. Supports context-anchored hunks for precise modifications."
}

func (PatchTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"patch": map[string]interface{}{
"type": "string",
"description": "Patch content in the *** Begin Patch format",
},
// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (PatchTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"patch": {Type: "string", Description: "Patch content in the *** Begin Patch format"},
},
"required": []interface{}{"patch"},
Required: []string{"patch"},
}
}

func (PatchTool) Parameters() map[string]interface{} {
return patchSchema.ToJSONSchema()
}

// patchSchema is the single source of truth for Patch's input schema.
var patchSchema = PatchTool{}.Schema()

func (PatchTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var params struct {
Patch string `json:"patch"`
}
if err := json.Unmarshal(input, &params); err != nil {
return "", fmt.Errorf("invalid input: %w", err)
params, err := DecodeInput[PatchInput]("Patch", input)
if err != nil {
return "", err
}
if params.Patch == "" {
return "", fmt.Errorf("patch content is required")
Expand Down
Loading
Loading