agent: resolve prompt from file paths and OLLIE_PROMPTS_PATH
This commit is contained in:
parent
663311daa0
commit
05e43b9a7c
|
|
@ -65,9 +65,11 @@ func BuildRuntime(cfg *config.Config, d tools.Dispatcher, cwd string, env []stri
|
||||||
for k, v := range cfg.Hooks {
|
for k, v := range cfg.Hooks {
|
||||||
hooks[k] = []string(v)
|
hooks[k] = []string(v)
|
||||||
}
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "[DEBUG BuildRuntime] cfg.Prompt.IsExec=%v len(Value)=%d\n", cfg.Prompt.IsExec, len(cfg.Prompt.Value))
|
||||||
if resolved, err := resolvePrompt(cfg.Prompt, cwd, env); err != nil {
|
if resolved, err := resolvePrompt(cfg.Prompt, cwd, env); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "resolve prompt: %v\n", err)
|
fmt.Fprintf(os.Stderr, "resolve prompt: %v\n", err)
|
||||||
} else {
|
} else {
|
||||||
|
fmt.Fprintf(os.Stderr, "[DEBUG BuildRuntime] preamble len=%d\n", len(resolved))
|
||||||
preamble = resolved
|
preamble = resolved
|
||||||
}
|
}
|
||||||
genParams = backend.GenerationParams{
|
genParams = backend.GenerationParams{
|
||||||
|
|
|
||||||
|
|
@ -2473,6 +2473,29 @@ func TestBuildRuntime_ExecPrompt(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildRuntime_ExecPromptFileResolution(t *testing.T) {
|
||||||
|
dir := setupCfgDir(t)
|
||||||
|
// Create a prompts directory with a test prompt file.
|
||||||
|
promptsDir := filepath.Join(dir, "prompts")
|
||||||
|
os.MkdirAll(promptsDir, 0o755)
|
||||||
|
os.WriteFile(filepath.Join(promptsDir, "test-prompt.md"), []byte("Hello ${PRIME_PLATFORM}"), 0o644)
|
||||||
|
t.Setenv("OLLIE_PROMPTS_PATH", promptsDir)
|
||||||
|
|
||||||
|
d := tools.NewDispatcher()
|
||||||
|
cfg := &config.Config{Prompt: config.Prompt{
|
||||||
|
Value: []string{"test-prompt", "echo extra"},
|
||||||
|
IsExec: true,
|
||||||
|
}}
|
||||||
|
cwd := t.TempDir()
|
||||||
|
rt := BuildRuntime(cfg, d, cwd, PromptEnv(cwd))
|
||||||
|
if !strings.Contains(rt.Preamble, "Hello linux") {
|
||||||
|
t.Errorf("expected file content with expanded vars, got %q", rt.Preamble)
|
||||||
|
}
|
||||||
|
if !strings.Contains(rt.Preamble, "extra") {
|
||||||
|
t.Errorf("expected shell fallback output, got %q", rt.Preamble)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildRuntime_ExecDispatchSuccess(t *testing.T) {
|
func TestBuildRuntime_ExecDispatchSuccess(t *testing.T) {
|
||||||
setupCfgDir(t)
|
setupCfgDir(t)
|
||||||
d := &mockDispatcher{
|
d := &mockDispatcher{
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ func PromptEnv(cwd string) []string {
|
||||||
return []string{
|
return []string{
|
||||||
"PRIME_CWD=" + cwd,
|
"PRIME_CWD=" + cwd,
|
||||||
"PRIME_PLATFORM=" + platform,
|
"PRIME_PLATFORM=" + platform,
|
||||||
|
"PRIME_DATE=" + time.Now().Format("2006-01-02"),
|
||||||
"PRIME_IS_GIT_REPO=" + isGitRepo,
|
"PRIME_IS_GIT_REPO=" + isGitRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -55,27 +56,52 @@ func resolvePrompt(p config.Prompt, cwd string, env []string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveExecPrompt(cmds []string, cwd string, env []string) (string, error) {
|
func resolveExecPrompt(cmds []string, cwd string, env []string) (string, error) {
|
||||||
|
// Build env map for variable expansion.
|
||||||
|
fullEnv := append(os.Environ(), env...)
|
||||||
|
envMap := make(map[string]string, len(fullEnv))
|
||||||
|
for _, e := range fullEnv {
|
||||||
|
if k, v, ok := strings.Cut(e, "="); ok {
|
||||||
|
envMap[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expand := func(s string) string {
|
||||||
|
return os.Expand(s, func(key string) string {
|
||||||
|
return envMap[key]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
var parts []string
|
var parts []string
|
||||||
for _, cmdStr := range cmds {
|
fmt.Fprintf(os.Stderr, "[DEBUG resolveExecPrompt] %d entries\n", len(cmds))
|
||||||
cmdStr = strings.TrimSpace(cmdStr)
|
for _, entry := range cmds {
|
||||||
if cmdStr == "" {
|
entry = strings.TrimSpace(entry)
|
||||||
|
if entry == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "[DEBUG resolveExecPrompt] entry=%q\n", entry)
|
||||||
|
// Try resolving as a prompt file first.
|
||||||
|
if content, ok := resolvePromptFile(entry, envMap, expand); ok {
|
||||||
|
fmt.Fprintf(os.Stderr, "[DEBUG resolveExecPrompt] -> file ok, len=%d\n", len(content))
|
||||||
|
if content != "" {
|
||||||
|
parts = append(parts, content)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Fall back to shell execution.
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
cmd := exec.CommandContext(ctx, "sh", "-c", cmdStr)
|
cmd := exec.CommandContext(ctx, "sh", "-c", entry)
|
||||||
if cwd != "" {
|
if cwd != "" {
|
||||||
if info, err := os.Stat(cwd); err == nil && info.IsDir() {
|
if info, err := os.Stat(cwd); err == nil && info.IsDir() {
|
||||||
cmd.Dir = cwd
|
cmd.Dir = cwd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cmd.Env = append(os.Environ(), env...)
|
cmd.Env = fullEnv
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
cmd.Stdout = &stdout
|
cmd.Stdout = &stdout
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
cancel()
|
cancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("prompt command %q failed: %v: %s", cmdStr, err, stderr.String())
|
return "", fmt.Errorf("prompt command %q failed: %v: %s", entry, err, stderr.String())
|
||||||
}
|
}
|
||||||
if out := strings.TrimRight(stdout.String(), "\n"); out != "" {
|
if out := strings.TrimRight(stdout.String(), "\n"); out != "" {
|
||||||
parts = append(parts, out)
|
parts = append(parts, out)
|
||||||
|
|
@ -84,6 +110,61 @@ func resolveExecPrompt(cmds []string, cwd string, env []string) (string, error)
|
||||||
return strings.Join(parts, "\n"), nil
|
return strings.Join(parts, "\n"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolvePromptFile attempts to resolve entry as a prompt file.
|
||||||
|
// It first checks if the entry (after env expansion) is an absolute path to an
|
||||||
|
// existing file. Otherwise it searches OLLIE_PROMPTS_PATH directories for
|
||||||
|
// entry and entry.md. Returns the file content (with env vars expanded) and
|
||||||
|
// true if found.
|
||||||
|
func resolvePromptFile(entry string, envMap map[string]string, expand func(string) string) (string, bool) {
|
||||||
|
expanded := expand(entry)
|
||||||
|
|
||||||
|
// If it contains shell metacharacters, it's a command, not a file name.
|
||||||
|
if strings.ContainsAny(expanded, "|&;<>()$`\\\"' \t") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try as absolute path.
|
||||||
|
if filepath.IsAbs(expanded) {
|
||||||
|
if info, err := os.Stat(expanded); err == nil && !info.IsDir() {
|
||||||
|
return readAndExpand(expanded, expand)
|
||||||
|
}
|
||||||
|
// Try with .md suffix.
|
||||||
|
if info, err := os.Stat(expanded + ".md"); err == nil && !info.IsDir() {
|
||||||
|
return readAndExpand(expanded+".md", expand)
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search OLLIE_PROMPTS_PATH.
|
||||||
|
promptsPath := envMap["OLLIE_PROMPTS_PATH"]
|
||||||
|
if promptsPath == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
for _, dir := range filepath.SplitList(promptsPath) {
|
||||||
|
if dir == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, candidate := range []string{
|
||||||
|
filepath.Join(dir, expanded),
|
||||||
|
filepath.Join(dir, expanded+".md"),
|
||||||
|
} {
|
||||||
|
if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
|
||||||
|
return readAndExpand(candidate, expand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAndExpand(path string, expand func(string) string) (string, bool) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
content := strings.TrimRight(expand(string(data)), "\n")
|
||||||
|
return content, true
|
||||||
|
}
|
||||||
|
|
||||||
func resolveStringPrompt(prompt, cwd string, env []string) (string, error) {
|
func resolveStringPrompt(prompt, cwd string, env []string) (string, error) {
|
||||||
if prompt == "" {
|
if prompt == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
|
|
|
||||||
Reference in New Issue