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
50 changes: 31 additions & 19 deletions internal/tool/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,38 +26,50 @@ func (BatchTool) Description() string {
return "Run several read-only tool calls in one turn (fan-out research). Calls must be read-only tools."
}

func (BatchTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"calls": map[string]interface{}{
"type": "array",
"description": "Read-only tool calls to execute in sequence",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"tool": map[string]interface{}{"type": "string", "description": "Read-only tool name (e.g. Read, Grep, Glob, LS, CodeSearch)"},
"input": map[string]interface{}{"type": "object", "description": "Tool input object"},
// BatchInput is the typed input for BatchTool.
type BatchInput struct {
Calls []batchCall `json:"calls"`
}

// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (BatchTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"calls": {
Type: "array",
Description: "Read-only tool calls to execute in sequence",
Items: &SchemaProperty{
Type: "object",
Properties: map[string]SchemaProperty{
"tool": {Type: "string", Description: "Read-only tool name (e.g. Read, Grep, Glob, LS, CodeSearch)"},
"input": {Type: "object", Description: "Tool input object"},
},
"required": []string{"tool"},
Required: []string{"tool"},
},
},
},
"required": []string{"calls"},
Required: []string{"calls"},
}
}

func (BatchTool) Parameters() map[string]interface{} {
return batchSchema.ToJSONSchema()
}

// batchSchema is the single source of truth for Batch's input schema.
var batchSchema = BatchTool{}.Schema()

type batchCall struct {
Tool string `json:"tool"`
Input json.RawMessage `json:"input"`
}

func (BatchTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var p struct {
Calls []batchCall `json:"calls"`
}
if err := json.Unmarshal(input, &p); err != nil {
return "", fmt.Errorf("batch: %w", err)
p, err := DecodeInput[BatchInput]("Batch", input)
if err != nil {
return "", err
}
if len(p.Calls) == 0 {
return "", errors.New("batch: calls is required and must not be empty")
Expand Down
106 changes: 55 additions & 51 deletions internal/tool/cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,49 +220,43 @@ func (CronCreateTool) Description() string {
return "Schedule a prompt to run at a future time — either recurring on a cron schedule, or once at a specific time."
}

func (CronCreateTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"schedule": map[string]interface{}{
"type": "string",
"description": "5-field cron expression in user's local timezone: minute hour day-of-month month day-of-week",
},
"prompt": map[string]interface{}{
"type": "string",
"description": "The prompt to enqueue when the schedule fires",
},
"recurring": map[string]interface{}{
"type": "boolean",
"description": "If true, repeats on schedule. If false, fires once then auto-deletes (default: true)",
},
"durable": map[string]interface{}{
"type": "boolean",
"description": "If true, persists to disk and survives session restarts (default: false)",
},
"max_runs": map[string]interface{}{
"type": "integer",
"description": "Stop after this many fires (0 = unlimited). Useful for /loop-style caps.",
},
"expires_in_sec": map[string]interface{}{
"type": "integer",
"description": "Auto-delete job after this many seconds from creation (0 = never).",
},
// CronCreateInput is the typed input for CronCreateTool.
type CronCreateInput struct {
Schedule string `json:"schedule"`
Prompt string `json:"prompt"`
Recurring *bool `json:"recurring"`
Durable *bool `json:"durable"`
MaxRuns int `json:"max_runs"`
ExpiresInSec int `json:"expires_in_sec"`
}

// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (CronCreateTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"schedule": {Type: "string", Description: "5-field cron expression in user's local timezone: minute hour day-of-month month day-of-week"},
"prompt": {Type: "string", Description: "The prompt to enqueue when the schedule fires"},
"recurring": {Type: "boolean", Description: "If true, repeats on schedule. If false, fires once then auto-deletes (default: true)"},
"durable": {Type: "boolean", Description: "If true, persists to disk and survives session restarts (default: false)"},
"max_runs": {Type: "integer", Description: "Stop after this many fires (0 = unlimited). Useful for /loop-style caps."},
"expires_in_sec": {Type: "integer", Description: "Auto-delete job after this many seconds from creation (0 = never)."},
},
"required": []string{"schedule", "prompt"},
Required: []string{"schedule", "prompt"},
}
}

func (CronCreateTool) Parameters() map[string]interface{} {
return cronCreateSchema.ToJSONSchema()
}

// cronCreateSchema is the single source of truth for CronCreate's input schema.
var cronCreateSchema = CronCreateTool{}.Schema()

func (CronCreateTool) Execute(_ context.Context, input json.RawMessage) (string, error) {
var p struct {
Schedule string `json:"schedule"`
Prompt string `json:"prompt"`
Recurring *bool `json:"recurring"`
Durable *bool `json:"durable"`
MaxRuns int `json:"max_runs"`
ExpiresInSec int `json:"expires_in_sec"`
}
if err := json.Unmarshal(input, &p); err != nil {
p, err := DecodeInput[CronCreateInput]("CronCreate", input)
if err != nil {
return "", err
}
if p.Schedule == "" {
Expand Down Expand Up @@ -317,24 +311,34 @@ type CronDeleteTool struct{}
func (CronDeleteTool) Name() string { return "CronDelete" }
func (CronDeleteTool) Aliases() []string { return []string{"cron_delete"} }
func (CronDeleteTool) Description() string { return "Remove a scheduled cron job" }
func (CronDeleteTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"id": map[string]interface{}{
"type": "string",
"description": "The cron job ID to delete",
},

// CronDeleteInput is the typed input for CronDeleteTool.
type CronDeleteInput struct {
ID string `json:"id"`
}

// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (CronDeleteTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"id": {Type: "string", Description: "The cron job ID to delete"},
},
"required": []string{"id"},
Required: []string{"id"},
}
}

func (CronDeleteTool) Parameters() map[string]interface{} {
return cronDeleteSchema.ToJSONSchema()
}

// cronDeleteSchema is the single source of truth for CronDelete's input schema.
var cronDeleteSchema = CronDeleteTool{}.Schema()

func (CronDeleteTool) Execute(_ context.Context, input json.RawMessage) (string, error) {
var p struct {
ID string `json:"id"`
}
if err := json.Unmarshal(input, &p); err != nil {
p, err := DecodeInput[CronDeleteInput]("CronDelete", input)
if err != nil {
return "", err
}
if !globalCronScheduler.Delete(p.ID) {
Expand Down
80 changes: 52 additions & 28 deletions internal/tool/extra.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,37 @@ func (NotebookEditTool) Description() string {
return "Edit a Jupyter notebook cell. Specify the notebook path, cell number, and new source."
}

func (NotebookEditTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"path": map[string]interface{}{"type": "string", "description": "Notebook file path (.ipynb)"},
"cell_number": map[string]interface{}{"type": "integer", "description": "Cell number (0-based)"},
"new_source": map[string]interface{}{"type": "string", "description": "New cell source content"},
// NotebookEditInput is the typed input for NotebookEditTool.
type NotebookEditInput struct {
Path string `json:"path"`
CellNumber int `json:"cell_number"`
NewSource string `json:"new_source"`
}

// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (NotebookEditTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"path": {Type: "string", Description: "Notebook file path (.ipynb)"},
"cell_number": {Type: "integer", Description: "Cell number (0-based)"},
"new_source": {Type: "string", Description: "New cell source content"},
},
"required": []string{"path", "cell_number", "new_source"},
Required: []string{"path", "cell_number", "new_source"},
}
}

func (NotebookEditTool) Parameters() map[string]interface{} {
return notebookEditSchema.ToJSONSchema()
}

// notebookEditSchema is the single source of truth for NotebookEdit's input schema.
var notebookEditSchema = NotebookEditTool{}.Schema()

func (NotebookEditTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var p struct {
Path string `json:"path"`
CellNumber int `json:"cell_number"`
NewSource string `json:"new_source"`
}
if err := json.Unmarshal(input, &p); err != nil {
p, err := DecodeInput[NotebookEditInput]("NotebookEdit", input)
if err != nil {
return "", err
}
if err := validatePathAllowed(ctx, p.Path); err != nil {
Expand Down Expand Up @@ -92,25 +104,37 @@ func (ConfigTool) Description() string {
return "Read or modify rho configuration settings."
}

func (ConfigTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{"type": "string", "enum": []string{"get", "set"}, "description": "Action"},
"key": map[string]interface{}{"type": "string", "description": "Setting key"},
"value": map[string]interface{}{"type": "string", "description": "Setting value (for set)"},
// ConfigInput is the typed input for ConfigTool.
type ConfigInput struct {
Action string `json:"action"`
Key string `json:"key"`
Value string `json:"value"`
}

// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (ConfigTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"action": {Type: "string", Description: "Action", Enum: []interface{}{"get", "set"}},
"key": {Type: "string", Description: "Setting key"},
"value": {Type: "string", Description: "Setting value (for set)"},
},
"required": []string{"action", "key"},
Required: []string{"action", "key"},
}
}

func (ConfigTool) Parameters() map[string]interface{} {
return configSchema.ToJSONSchema()
}

// configSchema is the single source of truth for Config's input schema.
var configSchema = ConfigTool{}.Schema()

func (ConfigTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var p struct {
Action string `json:"action"`
Key string `json:"key"`
Value string `json:"value"`
}
if err := json.Unmarshal(input, &p); err != nil {
p, err := DecodeInput[ConfigInput]("Config", input)
if err != nil {
return "", err
}
tc := GetToolContext(ctx)
Expand Down
57 changes: 28 additions & 29 deletions internal/tool/git_history.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,40 +21,39 @@ func (GitHistoryTool) Description() string {
return "Mine git history for co-change patterns, file history, and code ownership insights."
}

func (GitHistoryTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"type": "string",
"enum": []string{"history", "cochange", "owners", "blame"},
"description": "Action: history (commit log), cochange (frequently changed together), owners (top contributors), blame (line-level ownership)",
},
"file": map[string]interface{}{
"type": "string",
"description": "File to analyze",
},
"limit": map[string]interface{}{
"type": "integer",
"description": "Max results (default: 20)",
},
"root": map[string]interface{}{
"type": "string",
"description": "Project root directory (default: current dir)",
},
// GitHistoryInput is the typed input for GitHistoryTool.
type GitHistoryInput struct {
Action string `json:"action"`
File string `json:"file"`
Limit int `json:"limit"`
Root string `json:"root"`
}

// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (GitHistoryTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"action": {Type: "string", Description: "Action: history (commit log), cochange (frequently changed together), owners (top contributors), blame (line-level ownership)", Enum: []interface{}{"history", "cochange", "owners", "blame"}},
"file": {Type: "string", Description: "File to analyze"},
"limit": {Type: "integer", Description: "Max results (default: 20)"},
"root": {Type: "string", Description: "Project root directory (default: current dir)"},
},
"required": []string{"action"},
Required: []string{"action"},
}
}

func (GitHistoryTool) Parameters() map[string]interface{} {
return gitHistorySchema.ToJSONSchema()
}

// gitHistorySchema is the single source of truth for GitHistory's input schema.
var gitHistorySchema = GitHistoryTool{}.Schema()

func (GitHistoryTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var p struct {
Action string `json:"action"`
File string `json:"file"`
Limit int `json:"limit"`
Root string `json:"root"`
}
if err := json.Unmarshal(input, &p); err != nil {
p, err := DecodeInput[GitHistoryInput]("GitHistory", input)
if err != nil {
return "", err
}

Expand Down
Loading
Loading