Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ pnpm-debug.log*

.DS_Store

.a2a-server.pid

# Python / DeepEval (eval-harness/deepeval)
eval-harness/deepeval/.venv/
eval-harness/deepeval/.pytest_cache/
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ import (
"github.com/agenticenv/agent-sdk-go/pkg/conversation/inmem"
)

conv := inmem.NewInMemoryConversation(inmem.WithMaxSize(100))
conv := inmem.NewConversation(inmem.WithMaxSize(100))
a, _ := agent.NewAgent(
agent.WithTemporalConfig(...),
agent.WithLLMClient(...),
Expand All @@ -415,13 +415,13 @@ a.Run(ctx, "What's my name?", opts)
**Remote workers** — configure Redis on both processes; set `SaveOnIteration: true` on the worker when live updates are needed:

```go
convW, _ := redis.NewRedisConversation(redis.WithAddr("localhost:6379"))
convW, _ := redis.NewConversation(redis.WithAddr("localhost:6379"))
w, _ := agent.NewAgentWorker(
agent.WithTemporalConfig(...),
agent.WithConversation(conversation.Config{Conversation: convW, Size: 20, SaveOnIteration: true}),
)

convA, _ := redis.NewRedisConversation(redis.WithAddr("localhost:6379"))
convA, _ := redis.NewConversation(redis.WithAddr("localhost:6379"))
a, _ := agent.NewAgent(
agent.WithTemporalConfig(...),
agent.DisableLocalWorker(),
Expand Down
2 changes: 1 addition & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func main() {
log.Fatalf("mcp config: %v", err)
}

conv := inmem.NewInMemoryConversation(inmem.WithMaxSize(100))
conv := inmem.NewConversation(inmem.WithMaxSize(100))

// Single stdin reader: avoids conflict between main loop and approval handler after timeout
lineCh := make(chan string)
Expand Down
2 changes: 1 addition & 1 deletion docs/advanced/worker-separation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ This starts Temporal's remote event path for out-of-process event delivery.
In-memory conversation **fails at build time** with remote workers. Use Redis or another distributed backend on both processes:

```go
conv, _ := redis.NewRedisConversation(redis.WithAddr("localhost:6379"))
conv, _ := redis.NewConversation(redis.WithAddr("localhost:6379"))
convCfg := conversation.Config{Conversation: conv, Size: 20, SaveOnIteration: true}

w, _ := agent.NewAgentWorker(/* ... */, agent.WithConversation(convCfg))
Expand Down
2 changes: 1 addition & 1 deletion docs/features/conversation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
"github.com/agenticenv/agent-sdk-go/pkg/conversation/inmem"
)

conv := inmem.NewInMemoryConversation(inmem.WithMaxSize(100))
conv := inmem.NewConversation(inmem.WithMaxSize(100))

a, err := agent.NewAgent(
agent.WithLLMClient(llmClient),
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/streaming.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ Handle tool approvals with [`WithApprovalHandler`](/features/approvals) — the
Pass `ConversationOptions` to share history across turns while streaming. The same session ID links messages across calls:

```go
conv := inmem.NewInMemoryConversation(inmem.WithMaxSize(100))
conv := inmem.NewConversation(inmem.WithMaxSize(100))

a, err := agent.NewAgent(
agent.WithLLMClient(llmClient),
Expand Down
2 changes: 1 addition & 1 deletion examples/agent_with_conversation/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func main() {
}

// Redis conversation (start with task infra:redis:up or docker compose redis service).
conv, err := redis.NewRedisConversation(
conv, err := redis.NewConversation(
redis.WithAddr(redisAddrFromEnv()),
redis.WithMaxSize(100),
)
Expand Down
2 changes: 1 addition & 1 deletion examples/agent_with_stream_conversation/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func main() {
log.Fatalf("failed to create LLM client: %v", err)
}

conv := inmem.NewInMemoryConversation(inmem.WithMaxSize(100))
conv := inmem.NewConversation(inmem.WithMaxSize(100))

reg := agent.NewToolRegistry()
if err := agent.RegisterTools(reg,
Expand Down
4 changes: 2 additions & 2 deletions pkg/agent/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -452,8 +452,8 @@ func WithDisableFingerprintCheck(disable bool) Option {
// Pass [conversation.DefaultConfig] for SDK defaults or a custom [conversation.Config].
//
// Choose implementation based on deployment:
// - Single process: use inmem.NewInMemoryConversation
// - Remote workers: use redis.NewRedisConversation (in-memory cannot be used across processes)
// - Single process: use inmem.NewConversation
// - Remote workers: use redis.NewConversation (in-memory cannot be used across processes)
//
// The application owns Clear on the conversation when required; the agent runtime does not call Clear.
// Agent and worker must use the same conversation config and ID when using remote workers.
Expand Down
2 changes: 1 addition & 1 deletion pkg/conversation/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func (c Config) Validate() error {
// ValidateDistributed returns an error when conv is not distributed but remote workers require it.
func ValidateDistributed(conv interfaces.Conversation, remoteWorkers bool) error {
if remoteWorkers && conv != nil && !conv.IsDistributed() {
return errors.New("in-memory conversation cannot be used with remote workers: use distributed storage such as redis.NewRedisConversation")
return errors.New("in-memory conversation cannot be used with remote workers: use distributed storage such as redis.NewConversation")
}
return nil
}
Expand Down
8 changes: 4 additions & 4 deletions pkg/conversation/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

func TestDefaultConfig(t *testing.T) {
conv := inmem.NewInMemoryConversation()
conv := inmem.NewConversation()
cfg := conversation.DefaultConfig(conv)
if err := cfg.Validate(); err != nil {
t.Fatal(err)
Expand All @@ -21,7 +21,7 @@ func TestDefaultConfig(t *testing.T) {
}

func TestConfig_WithDefaults(t *testing.T) {
cfg := (conversation.Config{Conversation: inmem.NewInMemoryConversation()}).WithDefaults()
cfg := (conversation.Config{Conversation: inmem.NewConversation()}).WithDefaults()
if cfg.Size != conversation.DefaultSize {
t.Fatalf("size = %d", cfg.Size)
}
Expand All @@ -34,15 +34,15 @@ func TestConfig_Validate_missingConversation(t *testing.T) {
}

func TestConfig_ListOptions(t *testing.T) {
cfg := conversation.DefaultConfig(inmem.NewInMemoryConversation())
cfg := conversation.DefaultConfig(inmem.NewConversation())
opts := cfg.ListOptions()
if len(opts) != 1 {
t.Fatalf("opts len = %d", len(opts))
}
}

func TestValidateDistributed_inmemRemoteWorkers(t *testing.T) {
conv := inmem.NewInMemoryConversation()
conv := inmem.NewConversation()
if err := conversation.ValidateDistributed(conv, true); err == nil {
t.Fatal("expected distributed error")
}
Expand Down
10 changes: 8 additions & 2 deletions pkg/conversation/inmem/conversation.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ type InMemoryConversation struct {
mu sync.RWMutex
}

func NewInMemoryConversation(options ...Option) *InMemoryConversation {
// NewConversation creates a new in-memory conversation store.
func NewConversation(opts ...Option) *InMemoryConversation {
c := &InMemoryConversation{
messages: make(map[string][]interfaces.Message),
maxSize: 100,
}
for _, option := range options {
for _, option := range opts {
option(c)
}
if c.maxSize <= 0 {
Expand All @@ -29,6 +30,11 @@ func NewInMemoryConversation(options ...Option) *InMemoryConversation {
return c
}

// Deprecated: use NewConversation instead.
func NewInMemoryConversation(opts ...Option) *InMemoryConversation {
return NewConversation(opts...)
}

type Option func(*InMemoryConversation)

// WithMaxSize sets the maximum number of messages to store per conversation.
Expand Down
22 changes: 11 additions & 11 deletions pkg/conversation/inmem/conversation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,25 @@ import (
)

func TestInMemoryConversation_IsDistributed(t *testing.T) {
c := NewInMemoryConversation()
c := NewConversation()
if c.IsDistributed() {
t.Error("in-memory should not be distributed")
}
}

func TestNewInMemoryConversation_MaxSizeDefault(t *testing.T) {
c := NewInMemoryConversation(WithMaxSize(0))
func TestNewConversation_MaxSizeDefault(t *testing.T) {
c := NewConversation(WithMaxSize(0))
if c.maxSize != 100 {
t.Fatalf("maxSize = %d, want 100", c.maxSize)
}
c2 := NewInMemoryConversation(WithMaxSize(-1))
c2 := NewConversation(WithMaxSize(-1))
if c2.maxSize != 100 {
t.Fatalf("maxSize = %d, want 100", c2.maxSize)
}
}

func TestInMemoryConversation_AddMessage_EmptyID(t *testing.T) {
c := NewInMemoryConversation()
c := NewConversation()
ctx := context.Background()
if err := c.AddMessage(ctx, "", interfaces.Message{Role: interfaces.MessageRoleUser, Content: "x"}); err != nil {
t.Fatal(err)
Expand All @@ -41,7 +41,7 @@ func TestInMemoryConversation_AddMessage_EmptyID(t *testing.T) {
}

func TestInMemoryConversation_ListMessages_EmptyID(t *testing.T) {
c := NewInMemoryConversation()
c := NewConversation()
msgs, err := c.ListMessages(context.Background(), "")
if err != nil {
t.Fatal(err)
Expand All @@ -52,7 +52,7 @@ func TestInMemoryConversation_ListMessages_EmptyID(t *testing.T) {
}

func TestInMemoryConversation_TrimToMaxSize(t *testing.T) {
c := NewInMemoryConversation(WithMaxSize(3))
c := NewConversation(WithMaxSize(3))
ctx := context.Background()
id := "conv-1"
for i := 0; i < 5; i++ {
Expand All @@ -73,7 +73,7 @@ func TestInMemoryConversation_TrimToMaxSize(t *testing.T) {
}

func TestInMemoryConversation_ListMessages_LimitOffset(t *testing.T) {
c := NewInMemoryConversation()
c := NewConversation()
ctx := context.Background()
id := "c"
for _, ch := range []string{"a", "b", "c", "d", "e"} {
Expand All @@ -100,7 +100,7 @@ func TestInMemoryConversation_ListMessages_LimitOffset(t *testing.T) {
}

func TestInMemoryConversation_ListMessages_RoleFilter(t *testing.T) {
c := NewInMemoryConversation()
c := NewConversation()
ctx := context.Background()
id := "c"
if err := c.AddMessage(ctx, id, interfaces.Message{Role: interfaces.MessageRoleSystem, Content: "sys"}); err != nil {
Expand All @@ -119,7 +119,7 @@ func TestInMemoryConversation_ListMessages_RoleFilter(t *testing.T) {
}

func TestInMemoryConversation_Clear(t *testing.T) {
c := NewInMemoryConversation()
c := NewConversation()
ctx := context.Background()
id := "x"
if err := c.AddMessage(ctx, id, interfaces.Message{Role: interfaces.MessageRoleUser, Content: "1"}); err != nil {
Expand All @@ -138,7 +138,7 @@ func TestInMemoryConversation_Clear(t *testing.T) {
}

func TestInMemoryConversation_Clear_EmptyID(t *testing.T) {
c := NewInMemoryConversation()
c := NewConversation()
if err := c.Clear(context.Background(), ""); err != nil {
t.Fatal(err)
}
Expand Down
9 changes: 7 additions & 2 deletions pkg/conversation/redis/conversation.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ func (c *RedisConversation) getKey(id string) string {
return fmt.Sprintf("%s:%s:messages", p, id)
}

// NewRedisConversation creates a Redis-backed conversation from options.
// NewConversation creates a Redis-backed conversation from options.
// Use WithClient to provide your own client; otherwise addr is required to create a client.
// Call Close() when done if you did not use WithClient.
func NewRedisConversation(opts ...Option) (*RedisConversation, error) {
func NewConversation(opts ...Option) (*RedisConversation, error) {
c := &RedisConversation{maxSize: 100}
for _, opt := range opts {
opt(c)
Expand Down Expand Up @@ -106,6 +106,11 @@ func NewRedisConversation(opts ...Option) (*RedisConversation, error) {
return c, nil
}

// Deprecated: use NewConversation instead.
func NewRedisConversation(opts ...Option) (*RedisConversation, error) {
return NewConversation(opts...)
}

// Close releases the Redis connection only when we own it (not using WithClient).
func (c *RedisConversation) Close() error {
if c.ownClient && c.client != nil {
Expand Down
18 changes: 9 additions & 9 deletions pkg/conversation/redis/conversation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,27 @@ func newTestConversation(t *testing.T, opts ...Option) (*RedisConversation, *min
t.Helper()
s := miniredis.RunT(t)
base := []Option{WithAddr(s.Addr())}
c, err := NewRedisConversation(append(base, opts...)...)
c, err := NewConversation(append(base, opts...)...)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = c.Close() })
return c, s
}

func TestNewRedisConversation_RequiresAddr(t *testing.T) {
_, err := NewRedisConversation()
func TestNewConversation_RequiresAddr(t *testing.T) {
_, err := NewConversation()
if err == nil || err.Error() != "addr is required when not using WithClient" {
t.Fatalf("got %v", err)
}
}

func TestNewRedisConversation_WithClient_CloseDoesNotOwn(t *testing.T) {
func TestNewConversation_WithClient_CloseDoesNotOwn(t *testing.T) {
s := miniredis.RunT(t)
rdb := goredis.NewClient(&goredis.Options{Addr: s.Addr()})
t.Cleanup(func() { _ = rdb.Close() })

c, err := NewRedisConversation(WithClient(rdb))
c, err := NewConversation(WithClient(rdb))
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -170,7 +170,7 @@ func TestRedisConversation_WithKeyPrefix(t *testing.T) {
s := miniredis.RunT(t)
rdb := goredis.NewClient(&goredis.Options{Addr: s.Addr()})
t.Cleanup(func() { _ = rdb.Close() })
c, err := NewRedisConversation(WithAddr(s.Addr()), WithKeyPrefix("myapp"))
c, err := NewConversation(WithAddr(s.Addr()), WithKeyPrefix("myapp"))
if err != nil {
t.Fatal(err)
}
Expand All @@ -194,7 +194,7 @@ func TestRedisConversation_ListMessages_UnmarshalError(t *testing.T) {
s := miniredis.RunT(t)
rdb := goredis.NewClient(&goredis.Options{Addr: s.Addr()})
t.Cleanup(func() { _ = rdb.Close() })
c, err := NewRedisConversation(WithClient(rdb))
c, err := NewConversation(WithClient(rdb))
if err != nil {
t.Fatal(err)
}
Expand All @@ -212,7 +212,7 @@ func TestRedisConversation_ListMessages_UnmarshalError(t *testing.T) {

func TestRedisConversation_WithMaxSizeZeroUsesDefault(t *testing.T) {
s := miniredis.RunT(t)
c, err := NewRedisConversation(WithAddr(s.Addr()), WithMaxSize(0))
c, err := NewConversation(WithAddr(s.Addr()), WithMaxSize(0))
if err != nil {
t.Fatal(err)
}
Expand All @@ -233,7 +233,7 @@ func TestRoleIn(t *testing.T) {

func TestRedisConversation_TTLExpiresKey(t *testing.T) {
s := miniredis.RunT(t)
c, err := NewRedisConversation(WithAddr(s.Addr()), WithTTL(10*time.Minute))
c, err := NewConversation(WithAddr(s.Addr()), WithTTL(10*time.Minute))
if err != nil {
t.Fatal(err)
}
Expand Down
Loading