NewInMemoryConversationStore
Go · package github.com/muthuishere/toolnexus/golang · SPEC §8 · golang/client.go
type ConversationStore interface { Get(id string) ([]any, error) Save(id string, messages []any) error}
func NewInMemoryConversationStore() *InMemoryConversationStoreConversationStore is where Client.Ask(ctx, prompt, tk, id) remembers a transcript by id — two
methods, Get (returns nil when id has no stored history yet) and Save. CreateClient uses
NewInMemoryConversationStore() when ClientOptions.Store is left nil; supply your own
implementation to persist conversations across process restarts (a file, Redis, a database table).
When to use it
Section titled “When to use it”The default (in-memory) store is right for anything that lives inside one process’s lifetime — a
CLI session, a short-lived worker. Reach for a custom ConversationStore when the id arrives with a
request (a web handler keyed by user id, an A2A peer keyed by contextId) and the transcript must
survive the process restarting, or be shared across processes.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”Examples 2 and 3 point CreateClient at a local httptest.Server — no real network call, no real
API key. The stub server echoes back the number of messages it received, so a growing count proves
history round-tripped through the store.
1. The smallest useful call — Get/Save roundtrip
Section titled “1. The smallest useful call — Get/Save roundtrip”NewInMemoryConversationStore works standalone too — it’s just the interface.
package main
import ( "fmt" "log"
toolnexus "github.com/muthuishere/toolnexus/golang")
func main() { store := toolnexus.NewInMemoryConversationStore()
// Nothing stored yet ⇒ nil, no error. got, err := store.Get("u1") if err != nil || got != nil { log.Fatalf("expected (nil, nil) for an unknown id, got (%v, %v)", got, err) }
if err := store.Save("u1", []any{map[string]any{"role": "user", "content": "hi"}}); err != nil { log.Fatal(err) } got, err = store.Get("u1") if err != nil { log.Fatal(err) } if len(got) != 1 { log.Fatalf("expected 1 stored message, got %d", len(got)) }
fmt.Println("ok: roundtripped", len(got), "message(s)")}2. The realistic case — Client.Ask remembers by id through the default store
Section titled “2. The realistic case — Client.Ask remembers by id through the default store”package main
import ( "context" "encoding/json" "fmt" "log" "net/http" "net/http/httptest"
toolnexus "github.com/muthuishere/toolnexus/golang")
func main() { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var body struct { Messages []any `json:"messages"` } _ = json.NewDecoder(r.Body).Decode(&body) w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ "choices": []any{map[string]any{"message": map[string]any{"role": "assistant", "content": fmt.Sprint(len(body.Messages))}}}, }) })) defer srv.Close()
// No skills/system prompt, so the count is exactly the turns. tk, err := toolnexus.CreateToolkit(context.Background(), toolnexus.Options{Builtins: false}) if err != nil { log.Fatal(err) } defer tk.Close()
client := toolnexus.CreateClient(toolnexus.ClientOptions{BaseURL: srv.URL, Style: toolnexus.StyleOpenAI, Model: "m", APIKey: "test-key"}) ctx := context.Background()
first, err := client.Ask(ctx, "first", tk, "c1") if err != nil { log.Fatal(err) } if first.Text != "1" { log.Fatalf("first turn: text = %q, want 1", first.Text) } second, err := client.Ask(ctx, "second", tk, "c1") if err != nil { log.Fatal(err) } // user(first) + assistant(1) + user(second) = 3 messages remembered via the store. if second.Text != "3" { log.Fatalf("same id remembers: text = %q, want 3", second.Text) }
fmt.Println("ok:", first.Text, "->", second.Text)}3. The full surface — a custom store, plugged in via ClientOptions.Store
Section titled “3. The full surface — a custom store, plugged in via ClientOptions.Store”Implement the two-method interface yourself for a file/db-backed store; Client.ConversationStore()
returns whatever instance is in effect (the one you supplied, or the default) so callers never need
a shadow copy.
package main
import ( "context" "encoding/json" "fmt" "log" "net/http" "net/http/httptest" "sync"
toolnexus "github.com/muthuishere/toolnexus/golang")
// fileLikeStore stands in for a durable store (file/db/Redis) — same two methods.type fileLikeStore struct { mu sync.Mutex calls []string backing map[string][]any}
func (s *fileLikeStore) Get(id string) ([]any, error) { s.mu.Lock() defer s.mu.Unlock() s.calls = append(s.calls, "get:"+id) return s.backing[id], nil}
func (s *fileLikeStore) Save(id string, messages []any) error { s.mu.Lock() defer s.mu.Unlock() s.calls = append(s.calls, "save:"+id) s.backing[id] = messages return nil}
func main() { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ "choices": []any{map[string]any{"message": map[string]any{"role": "assistant", "content": "ack"}}}, }) })) defer srv.Close()
tk, err := toolnexus.CreateToolkit(context.Background(), toolnexus.Options{Builtins: false}) if err != nil { log.Fatal(err) } defer tk.Close()
store := &fileLikeStore{backing: map[string][]any{}} client := toolnexus.CreateClient(toolnexus.ClientOptions{ BaseURL: srv.URL, Style: toolnexus.StyleOpenAI, Model: "m", APIKey: "test-key", Store: store, })
// The instance you get back is the exact one you supplied. if client.ConversationStore() != toolnexus.ConversationStore(store) { log.Fatal("ConversationStore() should return the supplied instance") }
if _, err := client.Ask(context.Background(), "hi", tk, "u1"); err != nil { log.Fatal(err) } if len(store.calls) != 2 || store.calls[0] != "get:u1" || store.calls[1] != "save:u1" { log.Fatalf("custom store calls = %v, want [get:u1 save:u1]", store.calls) } if _, ok := store.backing["u1"]; !ok { log.Fatal("custom store did not persist the transcript for u1") }
fmt.Println("ok: custom store calls =", store.calls)}ConversationStore surface
Section titled “ConversationStore surface”| Member | What it does |
|---|---|
Get(id string) ([]any, error) |
Returns the stored transcript for id, or nil (no error) when none exists. |
Save(id string, messages []any) error |
Persists the (updated) transcript for id. |
NewInMemoryConversationStore() *InMemoryConversationStore |
The default provider — process-lifetime memory, safe for concurrent use, copies on get/save so callers can’t mutate stored slices. |
ClientOptions.Store ConversationStore |
Supply your own implementation here; nil ⇒ the in-memory default. |
Client.ConversationStore() ConversationStore |
Returns whichever instance is in effect — the supplied one, or the default the client created. |
See also
Section titled “See also”CreateClient— The unified client: system prompt, skills injection, parallel and chained tool calls, retries, memory.Client.Run— Send a prompt, let the loop call tools until the model stops, get a RunResult.Client.Stream— The streaming loop: text deltas, tool-call events, and suspension events as they happen.Conversation— The in-process alternative when you don’t need a lookup key or durability.