Repository navigation
Expand file tree
/
Copy pathcli.go
More file actions
105 lines (90 loc) · 3.35 KB
/
Copy pathcli.go
File metadata and controls
105 lines (90 loc) · 3.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// SPDX-License-Identifier: MIT
package executor
import (
"context"
"fmt"
"os/exec"
"strings"
"time"
"github.com/ankit373/hydra/internal/sandbox"
"github.com/ankit373/hydra/internal/util"
)
// CLIExecutor runs a prompt via a subprocess CLI tool.
type CLIExecutor struct{}
func (e *CLIExecutor) Execute(ctx context.Context, req Request) (*Response, error) {
tmpl, ok := cliTemplates[req.Head.Provider]
if !ok {
tmpl, ok = cliTemplates[req.Head.ID]
}
if !ok {
return nil, fmt.Errorf("no CLI template for head %q (provider %q)", req.Head.ID, req.Head.Provider)
}
args := tmpl.buildArgs(req.Prompt)
cmd := sandbox.Harden(exec.CommandContext(ctx, req.Head.Executable, args...))
env, closeGate, err := gateIfLocalOnly(ctx, req.Head, headEnv(req.Head))
if err != nil {
return nil, err
}
defer closeGate()
cmd.Env = env
if err := sandbox.WithLimits(cmd, req.MaxMemoryMB, req.MaxCPUSeconds); err != nil {
return nil, err
}
if tmpl.stdinPrompt {
cmd.Stdin = strings.NewReader(req.Prompt)
}
stdout := util.NewAccumulator(0)
stderr := util.NewAccumulator(64 << 10)
cmd.Stdout = stdout
cmd.Stderr = stderr
start := time.Now()
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("cli exec %s: %w, stderr: %s", req.Head.ID, err, stderr.String())
}
output := strings.TrimSpace(stdout.String())
promptTokens := EstimateTokens(req.Prompt)
responseTokens := EstimateTokens(output)
writeTokenSidecar(req.Head.ID, "cli", "estimate", promptTokens, responseTokens)
return &Response{
Output: output,
Duration: measured(time.Since(start)),
Model: req.Head.ID,
InputTokens: promptTokens,
OutputTokens: responseTokens,
Truncated: stdout.Truncated(),
// CLI tools (claude, codex, cursor, ...) report no token usage, these
// are char/4 estimates, same heuristic as the agy executor.
TokensEstimated: true,
}, nil
}
// cliTemplate describes how to invoke a specific AI CLI tool.
type cliTemplate struct {
args []string // positional args before the prompt; use "" as prompt placeholder
stdinPrompt bool // if true, prompt is passed via stdin instead of args
}
func (t cliTemplate) buildArgs(prompt string) []string {
var out []string
for _, a := range t.args {
if a == "" {
out = append(out, prompt)
} else {
out = append(out, a)
}
}
return out
}
// cliTemplates maps provider ID (or head ID) to invocation template.
// To add a new CLI tool, add an entry here and in capabilities/data.json.
var cliTemplates = map[string]cliTemplate{
"anthropic": {args: []string{"--print", ""}}, // claude --print "<prompt>"
"openai": {args: []string{"exec", ""}}, // codex exec "<prompt>", bare codex launches its interactive TUI (#491)
"google": {args: []string{""}}, // gemini "<prompt>"
"antigravity": {args: []string{"--print", ""}}, // agy --print "<prompt>", bare agy launches its interactive TUI and, unlike codex, still exits 0 (#492)
"cursor": {args: []string{"--stdio"}, stdinPrompt: true},
"amazon": {args: []string{""}}, // kiro "<prompt>"
"codeium": {args: []string{""}}, // windsurf "<prompt>"
"github": {args: []string{"copilot", "suggest", ""}}, // gh-copilot
"sourcegraph": {args: []string{"ask", ""}}, // cody ask "<prompt>"
"continue": {stdinPrompt: true},
"amp": {args: []string{""}},
}