summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--examples/simple-agent-go/go.mod7
-rw-r--r--examples/simple-agent-go/main.go152
-rw-r--r--libpanto-go/go.mod2
-rw-r--r--libpanto-go/panto.go41
-rw-r--r--libpanto-go/panto_test.go34
5 files changed, 222 insertions, 14 deletions
diff --git a/examples/simple-agent-go/go.mod b/examples/simple-agent-go/go.mod
new file mode 100644
index 0000000..19a479c
--- /dev/null
+++ b/examples/simple-agent-go/go.mod
@@ -0,0 +1,7 @@
+module simple-agent-go
+
+go 1.24
+
+require code.tjp.lol/pantograph.git/libpanto-go v0.0.0
+
+replace code.tjp.lol/pantograph.git/libpanto-go => ../../libpanto-go
diff --git a/examples/simple-agent-go/main.go b/examples/simple-agent-go/main.go
new file mode 100644
index 0000000..c733cd7
--- /dev/null
+++ b/examples/simple-agent-go/main.go
@@ -0,0 +1,152 @@
+// 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 <prompt>")
+ 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), &params); 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
+}
diff --git a/libpanto-go/go.mod b/libpanto-go/go.mod
index 805022f..c8ccb75 100644
--- a/libpanto-go/go.mod
+++ b/libpanto-go/go.mod
@@ -1,3 +1,3 @@
-module github.com/travisjeffery/pantograph/libpanto-go
+module code.tjp.lol/pantograph.git/libpanto-go
go 1.24
diff --git a/libpanto-go/panto.go b/libpanto-go/panto.go
index 786ff1c..5644b61 100644
--- a/libpanto-go/panto.go
+++ b/libpanto-go/panto.go
@@ -202,7 +202,12 @@ type (
)
type (
- Agent struct{ ptr *C.PantoAgent }
+ Agent struct {
+ ptr *C.PantoAgent
+ retainedSession *Session
+ retainedConv *Conversation
+ retainedStoreCloser func()
+ }
Stream struct {
ptr *C.PantoStream
err error
@@ -564,16 +569,7 @@ func (b ContentBlock) Snapshot() OwnedContentBlock {
}
func NewAgent(cfg Config) (*Agent, error) {
- st, err := NewNullStore()
- if err != nil {
- return nil, err
- }
- sess, err := CreateSession(st)
- st.Close()
- if err != nil {
- return nil, err
- }
- return NewAgentWithSession(cfg, sess, nil)
+ return NewAgentWithSession(cfg, nil, nil)
}
func NewAgentWithSession(cfg Config, session *Session, conv *Conversation) (*Agent, error) {
@@ -582,6 +578,11 @@ func NewAgentWithSession(cfg Config, session *Session, conv *Conversation) (*Age
return nil, err
}
defer strings.free()
+ var csession *C.PantoSession
+ if session != nil {
+ csession = session.ptr
+ runtime.SetFinalizer(session, nil)
+ }
var cconv *C.PantoConversation
if conv != nil {
cconv = conv.ptr
@@ -589,10 +590,10 @@ func NewAgentWithSession(cfg Config, session *Session, conv *Conversation) (*Age
runtime.SetFinalizer(conv, nil)
}
var out *C.PantoAgent
- if C.panto_agent_create(&ccfg, session.ptr, cconv, &out) != C.PANTO_OK {
+ if C.panto_agent_create(&ccfg, csession, cconv, &out) != C.PANTO_OK {
return nil, lastError()
}
- a := &Agent{out}
+ a := &Agent{ptr: out, retainedSession: session, retainedConv: conv}
runtime.SetFinalizer(a, (*Agent).Close)
return a, nil
}
@@ -602,6 +603,20 @@ func (a *Agent) Close() {
C.panto_agent_destroy(a.ptr)
a.ptr = nil
}
+ if a != nil {
+ if a.retainedSession != nil {
+ a.retainedSession.ptr = nil
+ a.retainedSession = nil
+ }
+ if a.retainedConv != nil {
+ a.retainedConv.ptr = nil
+ a.retainedConv = nil
+ }
+ if a.retainedStoreCloser != nil {
+ a.retainedStoreCloser()
+ a.retainedStoreCloser = nil
+ }
+ }
}
func (a *Agent) SessionID() string { return goSlice(C.panto_agent_session_id(a.ptr)) }
func (a *Agent) Conversation() *Conversation {
diff --git a/libpanto-go/panto_test.go b/libpanto-go/panto_test.go
new file mode 100644
index 0000000..ef21f1e
--- /dev/null
+++ b/libpanto-go/panto_test.go
@@ -0,0 +1,34 @@
+package panto
+
+import "testing"
+
+func testConfig() Config {
+ return Config{
+ Provider: ProviderConfig{
+ OpenAIChat: &OpenAIChatConfig{
+ APIKey: "test-key",
+ BaseURL: "http://localhost",
+ Model: "test-model",
+ MaxTokens: 16,
+ },
+ },
+ }
+}
+
+func TestNewAgentSetSystemPromptDoesNotCrash(t *testing.T) {
+ Init()
+ defer Deinit()
+
+ a, err := NewAgent(testConfig())
+ if err != nil {
+ t.Fatalf("NewAgent: %v", err)
+ }
+ defer a.Close()
+
+ if err := a.SetSystemPrompt("be brief"); err != nil {
+ t.Fatalf("SetSystemPrompt: %v", err)
+ }
+ if err := a.AddSystemMessage("also be accurate"); err != nil {
+ t.Fatalf("AddSystemMessage: %v", err)
+ }
+}