// A tiny standalone libpanto-go app: one prompt in, streamed text out. // // Run with: // // PANTO_API_STYLE=openai_chat|anthropic_messages \ // PANTO_API_KEY=... \ // PANTO_BASE_URL=... \ // PANTO_MODEL=... \ // go run examples/simple-agent-go/main.go "list the files" package main import ( "encoding/json" "fmt" "os" "os/exec" "strings" panto "code.tjp.lol/pantograph.git/libpanto-go" ) func main() { panto.Init() defer panto.Deinit() apiStyle := env("PANTO_API_STYLE", "openai_chat") apiKey := requireEnv("PANTO_API_KEY") baseURL := requireEnv("PANTO_BASE_URL") model := requireEnv("PANTO_MODEL") maxTokens := uint32(4096) if v := os.Getenv("PANTO_MAX_TOKENS"); v != "" { fmt.Sscanf(v, "%d", &maxTokens) } prompt := strings.Join(os.Args[1:], " ") if prompt == "" { fmt.Fprintln(os.Stderr, "usage: simple-agent-go ") os.Exit(2) } var cfg panto.Config if strings.HasPrefix(apiStyle, "anthropic") { cfg.Provider = panto.ProviderConfig{ AnthropicMessages: &panto.AnthropicMessagesConfig{ APIKey: apiKey, BaseURL: baseURL, Model: model, MaxTokens: maxTokens, }, } } else { cfg.Provider = panto.ProviderConfig{ OpenAIChat: &panto.OpenAIChatConfig{ APIKey: apiKey, BaseURL: baseURL, Model: model, MaxTokens: maxTokens, }, } } agent, err := panto.NewAgent(cfg) if err != nil { panic(err) } defer agent.Close() agent.SetSystemPrompt("You are a very small coding agent.\nUse the bash tool when you need to inspect or change the local workspace.\nKeep final answers concise.") agent.RegisterTool(panto.Tool{ Decl: panto.ToolDecl{ Name: "bash", Description: "Run a shell command with bash -lc and return combined stdout/stderr plus the exit code.", SchemaJSON: `{ "type": "object", "properties": { "command": { "type": "string", "description": "The command to run." } }, "required": ["command"] }`, }, Invoke: func(input string) (panto.ResultParts, error) { var params map[string]any if err := json.Unmarshal([]byte(input), ¶ms); err != nil { return nil, fmt.Errorf("invalid input: %w", err) } command, ok := params["command"].(string) if !ok || command == "" { return nil, fmt.Errorf("bash.command must be a non-empty string") } out, err := runBash(command) if err != nil { return nil, err } return panto.ResultParts{{Text: out}}, nil }, }) stream, err := agent.Run(prompt) if err != nil { panic(err) } defer stream.Close() for ev := range stream.Iter() { switch ev.Tag { case panto.EventContentDelta: fmt.Print(ev.ContentDelta.Delta) os.Stdout.Sync() case panto.EventToolDispatchStart: fmt.Fprintf(os.Stderr, "\n[bash: running %d tool call(s)]\n", ev.ToolDispatchStart.Count) case panto.EventToolDispatchComplete: fmt.Fprint(os.Stderr, "[bash: done]\n") } } if err := stream.Err(); err != nil { panic(err) } fmt.Println() } func env(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } func requireEnv(key string) string { v := os.Getenv(key) if v == "" { panic("missing required env var: " + key) } return v } func runBash(command string) (string, error) { cmd := exec.Command("bash", "-lc", command) out, err := cmd.CombinedOutput() status := "unknown" if exitErr, ok := err.(*exec.ExitError); ok { status = fmt.Sprintf("%d", exitErr.ExitCode()) } else if err != nil { return "", err } return fmt.Sprintf("exit_code=%s\n%s", status, string(out)), nil }