Refactor Context Management to Use Memory Instead of Space
- Replaced all instances of `ctx.Space` with `ctx.Memory.Context` in the context management code, ensuring a more structured approach to handling temporary request-scoped data. - Updated related test cases to reflect the changes in context memory usage, enhancing the reliability and clarity of tests. - Removed the deprecated `Space` references and adjusted comments and documentation to align with the new memory management strategy.
This commit is contained in:
parent
51be1844d5
commit
632c0f5674
35 changed files with 2676 additions and 1604 deletions
|
|
@ -252,14 +252,14 @@ func (ast *Assistant) BufferUserInput(ctx *agentcontext.Context, inputMessages [
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateSpaceSnapshot updates the space snapshot in the buffer
|
// UpdateSpaceSnapshot updates the context memory snapshot in the buffer
|
||||||
// Should be called when space data changes
|
// Only captures Context-level memory (request-scoped temporary data) for recovery
|
||||||
func (ast *Assistant) UpdateSpaceSnapshot(ctx *agentcontext.Context) {
|
func (ast *Assistant) UpdateSpaceSnapshot(ctx *agentcontext.Context) {
|
||||||
if ctx.Buffer == nil || ctx.Space == nil {
|
if ctx.Buffer == nil || ctx.Memory == nil || ctx.Memory.Context == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshot := ctx.Space.Snapshot()
|
snapshot := ctx.Memory.Context.Snapshot()
|
||||||
ctx.Buffer.SetSpaceSnapshot(snapshot)
|
ctx.Buffer.SetSpaceSnapshot(snapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/yaoapp/gou/plan"
|
|
||||||
"github.com/yaoapp/yao/agent/assistant"
|
"github.com/yaoapp/yao/agent/assistant"
|
||||||
agentcontext "github.com/yaoapp/yao/agent/context"
|
agentcontext "github.com/yaoapp/yao/agent/context"
|
||||||
storetypes "github.com/yaoapp/yao/agent/store/types"
|
storetypes "github.com/yaoapp/yao/agent/store/types"
|
||||||
|
|
@ -431,15 +430,16 @@ func TestBufferStepTracking(t *testing.T) {
|
||||||
|
|
||||||
t.Run("BeginAndCompleteStep", func(t *testing.T) {
|
t.Run("BeginAndCompleteStep", func(t *testing.T) {
|
||||||
ctx := agentcontext.New(context.Background(), nil, "test_chat_step_001")
|
ctx := agentcontext.New(context.Background(), nil, "test_chat_step_001")
|
||||||
ctx.Space = plan.NewMemorySharedSpace()
|
|
||||||
|
|
||||||
// Enter stack and init buffer
|
// Enter stack and init buffer
|
||||||
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
||||||
defer done()
|
defer done()
|
||||||
ast.InitBuffer(ctx)
|
ast.InitBuffer(ctx)
|
||||||
|
|
||||||
// Set some space data
|
// Set some context memory data
|
||||||
ctx.Space.Set("test_key", "test_value")
|
if ctx.Memory != nil && ctx.Memory.Context != nil {
|
||||||
|
ctx.Memory.Context.Set("test_key", "test_value", 0)
|
||||||
|
}
|
||||||
|
|
||||||
// Begin a step
|
// Begin a step
|
||||||
step := ast.BeginStep(ctx, agentcontext.StepTypeLLM, map[string]interface{}{
|
step := ast.BeginStep(ctx, agentcontext.StepTypeLLM, map[string]interface{}{
|
||||||
|
|
@ -464,34 +464,34 @@ func TestBufferStepTracking(t *testing.T) {
|
||||||
t.Logf("✓ Step tracking works correctly")
|
t.Logf("✓ Step tracking works correctly")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("SpaceSnapshotCapture", func(t *testing.T) {
|
t.Run("ContextMemorySnapshotCapture", func(t *testing.T) {
|
||||||
ctx := agentcontext.New(context.Background(), nil, "test_chat_space_001")
|
ctx := agentcontext.New(context.Background(), nil, "test_chat_memory_001")
|
||||||
ctx.Space = plan.NewMemorySharedSpace()
|
|
||||||
|
|
||||||
// Enter stack and init buffer
|
// Enter stack and init buffer
|
||||||
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
||||||
defer done()
|
defer done()
|
||||||
ast.InitBuffer(ctx)
|
ast.InitBuffer(ctx)
|
||||||
|
|
||||||
// Set space data before step
|
// Set context memory data before step
|
||||||
ctx.Space.Set("key1", "value1")
|
require.NotNil(t, ctx.Memory)
|
||||||
ctx.Space.Set("key2", 123)
|
require.NotNil(t, ctx.Memory.Context)
|
||||||
|
ctx.Memory.Context.Set("key1", "value1", 0)
|
||||||
|
ctx.Memory.Context.Set("key2", 123, 0)
|
||||||
|
|
||||||
// Begin step (should capture space snapshot)
|
// Begin step (should capture context memory snapshot)
|
||||||
ast.BeginStep(ctx, agentcontext.StepTypeHookCreate, nil)
|
ast.BeginStep(ctx, agentcontext.StepTypeHookCreate, nil)
|
||||||
|
|
||||||
// Verify space snapshot was captured
|
// Verify context memory snapshot was captured
|
||||||
steps := ctx.Buffer.GetAllSteps()
|
steps := ctx.Buffer.GetAllSteps()
|
||||||
require.Len(t, steps, 1)
|
require.Len(t, steps, 1)
|
||||||
assert.NotNil(t, steps[0].SpaceSnapshot)
|
assert.NotNil(t, steps[0].SpaceSnapshot)
|
||||||
assert.Equal(t, "value1", steps[0].SpaceSnapshot["key1"])
|
assert.Equal(t, "value1", steps[0].SpaceSnapshot["key1"])
|
||||||
assert.Equal(t, 123, steps[0].SpaceSnapshot["key2"])
|
assert.Equal(t, 123, steps[0].SpaceSnapshot["key2"])
|
||||||
t.Logf("✓ Space snapshot captured: %v", steps[0].SpaceSnapshot)
|
t.Logf("✓ Context memory snapshot captured: %v", steps[0].SpaceSnapshot)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("MultipleSteps", func(t *testing.T) {
|
t.Run("MultipleSteps", func(t *testing.T) {
|
||||||
ctx := agentcontext.New(context.Background(), nil, "test_chat_multi_step")
|
ctx := agentcontext.New(context.Background(), nil, "test_chat_multi_step")
|
||||||
ctx.Space = plan.NewMemorySharedSpace()
|
|
||||||
|
|
||||||
// Enter stack and init buffer
|
// Enter stack and init buffer
|
||||||
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
||||||
|
|
@ -536,7 +536,6 @@ func TestFlushBuffer(t *testing.T) {
|
||||||
t.Run("FlushOnSuccess", func(t *testing.T) {
|
t.Run("FlushOnSuccess", func(t *testing.T) {
|
||||||
chatID := fmt.Sprintf("test_flush_success_%s", uuid.New().String()[:8])
|
chatID := fmt.Sprintf("test_flush_success_%s", uuid.New().String()[:8])
|
||||||
ctx := agentcontext.New(context.Background(), nil, chatID)
|
ctx := agentcontext.New(context.Background(), nil, chatID)
|
||||||
ctx.Space = plan.NewMemorySharedSpace()
|
|
||||||
|
|
||||||
// Enter stack and init buffer
|
// Enter stack and init buffer
|
||||||
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
||||||
|
|
@ -584,7 +583,6 @@ func TestFlushBuffer(t *testing.T) {
|
||||||
t.Run("FlushOnFailure", func(t *testing.T) {
|
t.Run("FlushOnFailure", func(t *testing.T) {
|
||||||
chatID := fmt.Sprintf("test_flush_fail_%s", uuid.New().String()[:8])
|
chatID := fmt.Sprintf("test_flush_fail_%s", uuid.New().String()[:8])
|
||||||
ctx := agentcontext.New(context.Background(), nil, chatID)
|
ctx := agentcontext.New(context.Background(), nil, chatID)
|
||||||
ctx.Space = plan.NewMemorySharedSpace()
|
|
||||||
|
|
||||||
// Enter stack and init buffer
|
// Enter stack and init buffer
|
||||||
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
||||||
|
|
@ -633,7 +631,6 @@ func TestFlushBuffer(t *testing.T) {
|
||||||
t.Run("FlushOnInterrupt", func(t *testing.T) {
|
t.Run("FlushOnInterrupt", func(t *testing.T) {
|
||||||
chatID := fmt.Sprintf("test_flush_interrupt_%s", uuid.New().String()[:8])
|
chatID := fmt.Sprintf("test_flush_interrupt_%s", uuid.New().String()[:8])
|
||||||
ctx := agentcontext.New(context.Background(), nil, chatID)
|
ctx := agentcontext.New(context.Background(), nil, chatID)
|
||||||
ctx.Space = plan.NewMemorySharedSpace()
|
|
||||||
|
|
||||||
// Enter stack and init buffer
|
// Enter stack and init buffer
|
||||||
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
||||||
|
|
@ -673,7 +670,6 @@ func TestFlushBuffer(t *testing.T) {
|
||||||
t.Run("FlushWithModeAndConnector", func(t *testing.T) {
|
t.Run("FlushWithModeAndConnector", func(t *testing.T) {
|
||||||
chatID := fmt.Sprintf("test_flush_mode_%s", uuid.New().String()[:8])
|
chatID := fmt.Sprintf("test_flush_mode_%s", uuid.New().String()[:8])
|
||||||
ctx := agentcontext.New(context.Background(), nil, chatID)
|
ctx := agentcontext.New(context.Background(), nil, chatID)
|
||||||
ctx.Space = plan.NewMemorySharedSpace()
|
|
||||||
|
|
||||||
// Enter stack with connector and mode options
|
// Enter stack with connector and mode options
|
||||||
opts := &agentcontext.Options{
|
opts := &agentcontext.Options{
|
||||||
|
|
|
||||||
|
|
@ -262,7 +262,7 @@ func TestDBAuthWheresFilter(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("NilAuthorizedReturnsNil", func(t *testing.T) {
|
t.Run("NilAuthorizedReturnsNil", func(t *testing.T) {
|
||||||
ctx := &agentContext.Context{Authorized: nil}
|
ctx := agentContext.New(context.Background(), nil, "test-chat")
|
||||||
wheres := assistant.BuildDBAuthWheres(ctx)
|
wheres := assistant.BuildDBAuthWheres(ctx)
|
||||||
|
|
||||||
assert.Nil(t, wheres, "Nil Authorized should return nil")
|
assert.Nil(t, wheres, "Nil Authorized should return nil")
|
||||||
|
|
@ -419,16 +419,15 @@ func TestKBSearchIntegration(t *testing.T) {
|
||||||
// ========== Helper Functions ==========
|
// ========== Helper Functions ==========
|
||||||
|
|
||||||
func createAuthContext(userID, teamID string, teamOnly, ownerOnly bool) *agentContext.Context {
|
func createAuthContext(userID, teamID string, teamOnly, ownerOnly bool) *agentContext.Context {
|
||||||
return &agentContext.Context{
|
authorized := &oauthtypes.AuthorizedInfo{
|
||||||
Authorized: &oauthtypes.AuthorizedInfo{
|
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
TeamID: teamID,
|
TeamID: teamID,
|
||||||
Constraints: oauthtypes.DataConstraints{
|
Constraints: oauthtypes.DataConstraints{
|
||||||
TeamOnly: teamOnly,
|
TeamOnly: teamOnly,
|
||||||
OwnerOnly: ownerOnly,
|
OwnerOnly: ownerOnly,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
return agentContext.New(context.Background(), authorized, "test-chat")
|
||||||
}
|
}
|
||||||
|
|
||||||
func createAuthCollection(ctx context.Context, t *testing.T, id, userID, teamID string, public bool, share string) {
|
func createAuthCollection(ctx context.Context, t *testing.T, id, userID, teamID string, public bool, share string) {
|
||||||
|
|
|
||||||
|
|
@ -457,8 +457,8 @@ func TestVision_CachedContent(t *testing.T) {
|
||||||
t.Logf("✓ Content successfully cached and reused: %d characters", len(cachedText))
|
t.Logf("✓ Content successfully cached and reused: %d characters", len(cachedText))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestVision_FileMetadataInSpace tests that file metadata is correctly passed to vision agent via ctx.Space
|
// TestVision_FileMetadataInMemory tests that file metadata is correctly passed to vision agent via ctx.Memory.Context
|
||||||
func TestVision_FileMetadataInSpace(t *testing.T) {
|
func TestVision_FileMetadataInMemory(t *testing.T) {
|
||||||
testutils.Prepare(t)
|
testutils.Prepare(t)
|
||||||
defer testutils.Clean(t)
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/yaoapp/gou/connector/openai"
|
"github.com/yaoapp/gou/connector/openai"
|
||||||
"github.com/yaoapp/gou/plan"
|
|
||||||
agentContext "github.com/yaoapp/yao/agent/context"
|
agentContext "github.com/yaoapp/yao/agent/context"
|
||||||
"github.com/yaoapp/yao/config"
|
"github.com/yaoapp/yao/config"
|
||||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
|
|
@ -27,31 +26,29 @@ func TestMain(m *testing.M) {
|
||||||
|
|
||||||
// newTestContext creates a Context for testing with commonly used fields pre-populated
|
// newTestContext creates a Context for testing with commonly used fields pre-populated
|
||||||
func newTestContext(capabilities *openai.Capabilities) *agentContext.Context {
|
func newTestContext(capabilities *openai.Capabilities) *agentContext.Context {
|
||||||
return &agentContext.Context{
|
authorized := &types.AuthorizedInfo{
|
||||||
Context: stdContext.Background(),
|
|
||||||
Space: plan.NewMemorySharedSpace(),
|
|
||||||
ChatID: "test-chat",
|
|
||||||
AssistantID: "test-assistant",
|
|
||||||
Locale: "en-us",
|
|
||||||
Theme: "light",
|
|
||||||
Client: agentContext.Client{
|
|
||||||
Type: "web",
|
|
||||||
UserAgent: "TestAgent/1.0",
|
|
||||||
IP: "127.0.0.1",
|
|
||||||
},
|
|
||||||
Referer: agentContext.RefererAPI,
|
|
||||||
Accept: agentContext.AcceptWebCUI,
|
|
||||||
Route: "",
|
|
||||||
Metadata: make(map[string]interface{}),
|
|
||||||
Capabilities: capabilities,
|
|
||||||
Authorized: &types.AuthorizedInfo{
|
|
||||||
Subject: "test-user",
|
Subject: "test-user",
|
||||||
ClientID: "test-client-id",
|
ClientID: "test-client-id",
|
||||||
UserID: "test-user-123",
|
UserID: "test-user-123",
|
||||||
TeamID: "test-team-456",
|
TeamID: "test-team-456",
|
||||||
TenantID: "test-tenant-789",
|
TenantID: "test-tenant-789",
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx := agentContext.New(stdContext.Background(), authorized, "test-chat")
|
||||||
|
ctx.AssistantID = "test-assistant"
|
||||||
|
ctx.Locale = "en-us"
|
||||||
|
ctx.Theme = "light"
|
||||||
|
ctx.Client = agentContext.Client{
|
||||||
|
Type: "web",
|
||||||
|
UserAgent: "TestAgent/1.0",
|
||||||
|
IP: "127.0.0.1",
|
||||||
|
}
|
||||||
|
ctx.Referer = agentContext.RefererAPI
|
||||||
|
ctx.Accept = agentContext.AcceptWebCUI
|
||||||
|
ctx.Route = ""
|
||||||
|
ctx.Metadata = make(map[string]interface{})
|
||||||
|
ctx.Capabilities = capabilities
|
||||||
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestImageHandler_CanHandle(t *testing.T) {
|
func TestImageHandler_CanHandle(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -49,15 +49,15 @@ func CallAgent(ctx *agentContext.Context, agentID string, message agentContext.M
|
||||||
}
|
}
|
||||||
|
|
||||||
// CallAgentWithFileInfo calls an agent to process content with file metadata
|
// CallAgentWithFileInfo calls an agent to process content with file metadata
|
||||||
// The file metadata is passed via ctx.Space for access by hooks (especially Next hook)
|
// The file metadata is passed via ctx.Memory.Context for access by hooks (especially Next hook)
|
||||||
// Uses Space instead of Metadata to avoid creating context copies and ensure proper cleanup
|
// Uses Memory.Context (request-scoped) to avoid creating context copies and ensure proper cleanup
|
||||||
//
|
//
|
||||||
// Space Keys (with agent ID as namespace prefix to avoid conflicts between different agents):
|
// Memory Keys (with agent ID as namespace prefix to avoid conflicts between different agents):
|
||||||
// - {agentID}:files_info - List of all files being processed by this agent (array)
|
// - {agentID}:files_info - List of all files being processed by this agent (array)
|
||||||
// - {agentID}:current_file - Currently processing file (single object)
|
// - {agentID}:current_file - Currently processing file (single object)
|
||||||
func CallAgentWithFileInfo(ctx *agentContext.Context, agentID string, message agentContext.Message, info *Info) (string, error) {
|
func CallAgentWithFileInfo(ctx *agentContext.Context, agentID string, message agentContext.Message, info *Info) (string, error) {
|
||||||
// Store file information in Space if available
|
// Store file information in Memory.Context if available
|
||||||
if info != nil && ctx.Space != nil {
|
if info != nil && ctx.Memory != nil && ctx.Memory.Context != nil {
|
||||||
fileInfo := map[string]interface{}{
|
fileInfo := map[string]interface{}{
|
||||||
"url": info.URL,
|
"url": info.URL,
|
||||||
"filename": info.Filename,
|
"filename": info.Filename,
|
||||||
|
|
@ -74,14 +74,14 @@ func CallAgentWithFileInfo(ctx *agentContext.Context, agentID string, message ag
|
||||||
fileInfo["file_id"] = info.FileID
|
fileInfo["file_id"] = info.FileID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use agent ID as namespace prefix for Space keys
|
// Use agent ID as namespace prefix for Memory keys
|
||||||
filesListKey := agentID + ":files_info"
|
filesListKey := agentID + ":files_info"
|
||||||
currentFileKey := agentID + ":current_file"
|
currentFileKey := agentID + ":current_file"
|
||||||
|
|
||||||
// Thread-safe: append current file to files list
|
// Thread-safe: append current file to files list
|
||||||
fileInfoMutex.Lock()
|
fileInfoMutex.Lock()
|
||||||
var filesList []map[string]interface{}
|
var filesList []map[string]interface{}
|
||||||
if existing, err := ctx.Space.Get(filesListKey); err == nil {
|
if existing, ok := ctx.Memory.Context.Get(filesListKey); ok {
|
||||||
// Convert existing data to []map[string]interface{}
|
// Convert existing data to []map[string]interface{}
|
||||||
if existingList, ok := existing.([]interface{}); ok {
|
if existingList, ok := existing.([]interface{}); ok {
|
||||||
for _, item := range existingList {
|
for _, item := range existingList {
|
||||||
|
|
@ -95,23 +95,23 @@ func CallAgentWithFileInfo(ctx *agentContext.Context, agentID string, message ag
|
||||||
}
|
}
|
||||||
// Append current file to list
|
// Append current file to list
|
||||||
filesList = append(filesList, fileInfo)
|
filesList = append(filesList, fileInfo)
|
||||||
ctx.Space.Set(filesListKey, filesList)
|
ctx.Memory.Context.Set(filesListKey, filesList, 0)
|
||||||
fileInfoMutex.Unlock()
|
fileInfoMutex.Unlock()
|
||||||
|
|
||||||
// Store current file in Space
|
// Store current file in Memory.Context
|
||||||
if err := ctx.Space.Set(currentFileKey, fileInfo); err != nil {
|
if err := ctx.Memory.Context.Set(currentFileKey, fileInfo, 0); err != nil {
|
||||||
log.Trace("[Content] Failed to set current file info in Space: %v", err)
|
log.Trace("[Content] Failed to set current file info in Memory.Context: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure cleanup after agent call completes
|
// Ensure cleanup after agent call completes
|
||||||
defer func() {
|
defer func() {
|
||||||
// Clean up current file
|
// Clean up current file
|
||||||
if err := ctx.Space.Delete(currentFileKey); err != nil {
|
if err := ctx.Memory.Context.Del(currentFileKey); err != nil {
|
||||||
log.Trace("[Content] Failed to delete current file info from Space: %v", err)
|
log.Trace("[Content] Failed to delete current file info from Memory.Context: %v", err)
|
||||||
}
|
}
|
||||||
// Clean up files list (reset for next call)
|
// Clean up files list (reset for next call)
|
||||||
if err := ctx.Space.Delete(filesListKey); err != nil {
|
if err := ctx.Memory.Context.Del(filesListKey); err != nil {
|
||||||
log.Trace("[Content] Failed to delete files list from Space: %v", err)
|
log.Trace("[Content] Failed to delete files list from Memory.Context: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ interface Context {
|
||||||
authorized: Record<string, any>; // Authorization data (empty object if not set)
|
authorized: Record<string, any>; // Authorization data (empty object if not set)
|
||||||
|
|
||||||
// Objects
|
// Objects
|
||||||
space: Space; // Shared data space for passing data between requests
|
memory: Memory; // Agent memory with four namespaces: user, team, chat, context
|
||||||
trace: Trace; // Trace object for debugging and monitoring
|
trace: Trace; // Trace object for debugging and monitoring
|
||||||
mcp: MCP; // MCP object for external tool/resource access
|
mcp: MCP; // MCP object for external tool/resource access
|
||||||
}
|
}
|
||||||
|
|
@ -1210,7 +1210,7 @@ Releases trace resources.
|
||||||
|
|
||||||
Trace spaces are visual containers for organizing trace nodes in the frontend UI. They help group related operations together for better presentation to users.
|
Trace spaces are visual containers for organizing trace nodes in the frontend UI. They help group related operations together for better presentation to users.
|
||||||
|
|
||||||
> **Note:** Trace spaces are purely for visual organization and presentation. They do not store data - use `ctx.space` for data storage between hooks.
|
> **Note:** Trace spaces are purely for visual organization and presentation. They do not store data - use `ctx.memory` for data storage between hooks.
|
||||||
|
|
||||||
#### `ctx.trace.CreateSpace(option)`
|
#### `ctx.trace.CreateSpace(option)`
|
||||||
|
|
||||||
|
|
@ -1248,138 +1248,372 @@ Retrieves a trace space by ID.
|
||||||
const search_space = ctx.trace.GetSpace("search-space-id");
|
const search_space = ctx.trace.GetSpace("search-space-id");
|
||||||
```
|
```
|
||||||
|
|
||||||
## Space API
|
## Memory API
|
||||||
|
|
||||||
The `ctx.space` object provides a shared data space for passing data between requests and agent calls. This is useful for storing temporary data that needs to be accessed across different hooks or nested agent calls.
|
The `ctx.memory` object provides a four-level hierarchical memory system for agent state management. Each level has different persistence and scope characteristics.
|
||||||
|
|
||||||
### Methods Summary
|
### Memory Namespaces
|
||||||
|
|
||||||
| Method | Description |
|
| Namespace | Scope | Persistence | Use Case |
|
||||||
| ----------------- | ------------------------------------- |
|
| -------------------- | ------------------- | ----------- | ------------------------------------------- |
|
||||||
| `Get(key)` | Get a value from the space |
|
| `ctx.memory.user` | Per user | Persistent | User preferences, settings, long-term state |
|
||||||
| `Set(key, value)` | Set a value in the space |
|
| `ctx.memory.team` | Per team | Persistent | Team-wide settings, shared configurations |
|
||||||
| `Delete(key)` | Delete a key from the space |
|
| `ctx.memory.chat` | Per chat session | Persistent | Chat-specific context, conversation state |
|
||||||
| `GetDel(key)` | Get a value and immediately delete it |
|
| `ctx.memory.context` | Per request context | Temporary | Request-scoped data, cleared on release |
|
||||||
|
|
||||||
### Methods
|
### Namespace Interface
|
||||||
|
|
||||||
#### `ctx.space.Get(key): any`
|
Each namespace (`user`, `team`, `chat`, `context`) provides the same interface:
|
||||||
|
|
||||||
Gets a value from the space.
|
```typescript
|
||||||
|
interface MemoryNamespace {
|
||||||
|
// Basic KV operations
|
||||||
|
Get(key: string): any; // Get a value
|
||||||
|
Set(key: string, value: any, ttl?: number): void; // Set a value with optional TTL (seconds)
|
||||||
|
Del(key: string): void; // Delete a key (supports wildcards: "prefix:*")
|
||||||
|
Has(key: string): boolean; // Check if key exists
|
||||||
|
GetDel(key: string): any; // Get and delete atomically
|
||||||
|
|
||||||
**Parameters:**
|
// Collection operations
|
||||||
|
Keys(): string[]; // Get all keys
|
||||||
|
Len(): number; // Get number of keys
|
||||||
|
Clear(): void; // Delete all keys
|
||||||
|
|
||||||
- `key`: String - The key to retrieve
|
// Atomic counter operations
|
||||||
|
Incr(key: string, delta?: number): number; // Increment (default delta=1)
|
||||||
|
Decr(key: string, delta?: number): number; // Decrement (default delta=1)
|
||||||
|
|
||||||
**Returns:**
|
// List operations
|
||||||
|
Push(key: string, values: any[]): number; // Append to list, returns new length
|
||||||
|
Pop(key: string): any; // Remove and return last element
|
||||||
|
Pull(key: string, count: number): any[]; // Remove and return last N elements
|
||||||
|
PullAll(key: string): any[]; // Remove and return all elements
|
||||||
|
AddToSet(key: string, values: any[]): number; // Add unique values to set
|
||||||
|
|
||||||
- `any`: The value, or `null` if not found
|
// Array access operations
|
||||||
|
ArrayLen(key: string): number; // Get array length
|
||||||
|
ArrayGet(key: string, index: number): any; // Get element at index
|
||||||
|
ArraySet(key: string, index: number, value: any): void; // Set element at index
|
||||||
|
ArraySlice(key: string, start: number, end: number): any[]; // Get slice
|
||||||
|
ArrayPage(key: string, page: number, size: number): any[]; // Paginated access
|
||||||
|
ArrayAll(key: string): any[]; // Get all elements
|
||||||
|
|
||||||
**Example:**
|
// Metadata
|
||||||
|
id: string; // Namespace ID
|
||||||
```javascript
|
space: string; // Space type: "user", "team", "chat", or "context"
|
||||||
const user_data = ctx.space.Get("user_data");
|
|
||||||
if (user_data) {
|
|
||||||
console.log("Found user:", user_data.name);
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `ctx.space.Set(key, value): void`
|
### Basic KV Operations
|
||||||
|
|
||||||
Sets a value in the space.
|
#### `Get(key): any`
|
||||||
|
|
||||||
**Parameters:**
|
Gets a value from the namespace.
|
||||||
|
|
||||||
- `key`: String - The key to set
|
|
||||||
- `value`: Any - The value to store
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
ctx.space.Set("user_data", { name: "John", id: 123 });
|
// User preferences
|
||||||
ctx.space.Set("processing_status", "started");
|
const theme = ctx.memory.user.Get("theme");
|
||||||
|
if (theme) {
|
||||||
|
console.log("User prefers:", theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chat context
|
||||||
|
const topic = ctx.memory.chat.Get("current_topic");
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `ctx.space.Delete(key): void`
|
#### `Set(key, value, ttl?): void`
|
||||||
|
|
||||||
Deletes a key from the space.
|
Sets a value with optional TTL (time-to-live in seconds).
|
||||||
|
|
||||||
**Parameters:**
|
|
||||||
|
|
||||||
- `key`: String - The key to delete
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
ctx.space.Delete("temp_data");
|
// Persistent user setting
|
||||||
|
ctx.memory.user.Set("language", "en");
|
||||||
|
|
||||||
|
// Team configuration
|
||||||
|
ctx.memory.team.Set("api_key", "sk-xxx");
|
||||||
|
|
||||||
|
// Chat state
|
||||||
|
ctx.memory.chat.Set("last_query", "What is AI?");
|
||||||
|
|
||||||
|
// Temporary context data with 5 minute TTL
|
||||||
|
ctx.memory.context.Set("temp_result", { data: "..." }, 300);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `ctx.space.GetDel(key): any`
|
#### `Del(key): void`
|
||||||
|
|
||||||
Gets a value and immediately deletes it. Convenient for one-time use data.
|
Deletes a key. Supports wildcard patterns with `*`.
|
||||||
|
|
||||||
**Parameters:**
|
|
||||||
|
|
||||||
- `key`: String - The key to retrieve and delete
|
|
||||||
|
|
||||||
**Returns:**
|
|
||||||
|
|
||||||
- `any`: The value, or `null` if not found
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Store file metadata in parent agent
|
// Delete single key
|
||||||
ctx.space.Set("file_metadata", { name: "report.pdf", size: 1024 });
|
ctx.memory.user.Del("old_setting");
|
||||||
|
|
||||||
// In child agent, get and consume the data
|
// Delete with wildcard pattern
|
||||||
const metadata = ctx.space.GetDel("file_metadata");
|
ctx.memory.chat.Del("cache:*"); // Deletes all keys starting with "cache:"
|
||||||
// metadata is now deleted from space
|
```
|
||||||
|
|
||||||
|
#### `Has(key): boolean`
|
||||||
|
|
||||||
|
Checks if a key exists.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
if (ctx.memory.user.Has("onboarding_complete")) {
|
||||||
|
// Skip onboarding
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `GetDel(key): any`
|
||||||
|
|
||||||
|
Atomically gets and deletes a value. Useful for one-time tokens.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const token = ctx.memory.context.GetDel("one_time_token");
|
||||||
|
if (token) {
|
||||||
|
// Use token (it's now deleted)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Collection Operations
|
||||||
|
|
||||||
|
#### `Keys(): string[]`
|
||||||
|
|
||||||
|
Returns all keys in the namespace.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const userKeys = ctx.memory.user.Keys();
|
||||||
|
console.log("User has", userKeys.length, "stored values");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `Len(): number`
|
||||||
|
|
||||||
|
Returns the number of keys.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const count = ctx.memory.chat.Len();
|
||||||
|
console.log("Chat has", count, "stored values");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `Clear(): void`
|
||||||
|
|
||||||
|
Deletes all keys in the namespace.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Clear temporary context data
|
||||||
|
ctx.memory.context.Clear();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Atomic Counter Operations
|
||||||
|
|
||||||
|
#### `Incr(key, delta?): number`
|
||||||
|
|
||||||
|
Atomically increments a counter. Returns the new value.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Simple counter
|
||||||
|
const views = ctx.memory.user.Incr("page_views");
|
||||||
|
console.log("Total views:", views);
|
||||||
|
|
||||||
|
// Increment by custom amount
|
||||||
|
const points = ctx.memory.user.Incr("points", 10);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `Decr(key, delta?): number`
|
||||||
|
|
||||||
|
Atomically decrements a counter. Returns the new value.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const remaining = ctx.memory.user.Decr("credits");
|
||||||
|
if (remaining < 0) {
|
||||||
|
throw new Error("Insufficient credits");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### List Operations
|
||||||
|
|
||||||
|
#### `Push(key, values): number`
|
||||||
|
|
||||||
|
Appends values to a list. Returns new length.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const len = ctx.memory.chat.Push("history", [
|
||||||
|
{ role: "user", content: "Hello" },
|
||||||
|
{ role: "assistant", content: "Hi there!" },
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `Pop(key): any`
|
||||||
|
|
||||||
|
Removes and returns the last element.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const lastItem = ctx.memory.chat.Pop("pending_tasks");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `Pull(key, count): any[]`
|
||||||
|
|
||||||
|
Removes and returns the last N elements.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const recentItems = ctx.memory.chat.Pull("notifications", 5);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `PullAll(key): any[]`
|
||||||
|
|
||||||
|
Removes and returns all elements.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const allTasks = ctx.memory.context.PullAll("batch_queue");
|
||||||
|
// Process all tasks, queue is now empty
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `AddToSet(key, values): number`
|
||||||
|
|
||||||
|
Adds unique values to a set (no duplicates). Returns new size.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
ctx.memory.user.AddToSet("visited_pages", ["/home", "/about"]);
|
||||||
|
ctx.memory.user.AddToSet("visited_pages", ["/home", "/contact"]); // "/home" not added again
|
||||||
|
```
|
||||||
|
|
||||||
|
### Array Access Operations
|
||||||
|
|
||||||
|
#### `ArrayLen(key): number`
|
||||||
|
|
||||||
|
Gets the length of an array.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const historyLen = ctx.memory.chat.ArrayLen("messages");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `ArrayGet(key, index): any`
|
||||||
|
|
||||||
|
Gets an element at a specific index.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const firstMessage = ctx.memory.chat.ArrayGet("messages", 0);
|
||||||
|
const lastMessage = ctx.memory.chat.ArrayGet("messages", -1); // Negative index
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `ArraySet(key, index, value): void`
|
||||||
|
|
||||||
|
Sets an element at a specific index.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
ctx.memory.chat.ArraySet("messages", 0, { role: "system", content: "Updated" });
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `ArraySlice(key, start, end): any[]`
|
||||||
|
|
||||||
|
Gets a slice of the array.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const recent = ctx.memory.chat.ArraySlice("messages", -10, -1); // Last 10 messages
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `ArrayPage(key, page, size): any[]`
|
||||||
|
|
||||||
|
Gets a page of elements (1-indexed pages).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const page1 = ctx.memory.chat.ArrayPage("messages", 1, 20); // First 20 messages
|
||||||
|
const page2 = ctx.memory.chat.ArrayPage("messages", 2, 20); // Next 20 messages
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `ArrayAll(key): any[]`
|
||||||
|
|
||||||
|
Gets all elements of the array.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const allMessages = ctx.memory.chat.ArrayAll("messages");
|
||||||
```
|
```
|
||||||
|
|
||||||
### Use Cases
|
### Use Cases
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Use case 1: Pass data between hooks
|
// Use case 1: User preferences (persistent across sessions)
|
||||||
function Create(ctx, messages) {
|
function Create(ctx, messages) {
|
||||||
// Store data for later use
|
// Load user preferences
|
||||||
ctx.space.Set("original_query", messages[0].content);
|
const locale = ctx.memory.user.Get("preferred_locale") || "en";
|
||||||
|
const style = ctx.memory.user.Get("response_style") || "concise";
|
||||||
|
|
||||||
|
return {
|
||||||
|
messages,
|
||||||
|
locale: locale,
|
||||||
|
metadata: { style: style },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use case 2: Chat context (persistent within chat session)
|
||||||
|
function Next(ctx, payload) {
|
||||||
|
// Track conversation topics
|
||||||
|
const topics = ctx.memory.chat.Get("discussed_topics") || [];
|
||||||
|
const newTopic = extractTopic(payload.completion.content);
|
||||||
|
|
||||||
|
if (newTopic && !topics.includes(newTopic)) {
|
||||||
|
topics.push(newTopic);
|
||||||
|
ctx.memory.chat.Set("discussed_topics", topics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use case 3: Request-scoped data (cleared on context release)
|
||||||
|
function Create(ctx, messages) {
|
||||||
|
// Store temporary processing data
|
||||||
|
ctx.memory.context.Set("request_start", Date.now());
|
||||||
|
ctx.memory.context.Set("original_query", messages[0]?.content);
|
||||||
|
|
||||||
return { messages };
|
return { messages };
|
||||||
}
|
}
|
||||||
|
|
||||||
function Next(ctx, payload) {
|
function Next(ctx, payload) {
|
||||||
// Retrieve data from Create hook
|
// Retrieve temporary data
|
||||||
const query = ctx.space.Get("original_query");
|
const startTime = ctx.memory.context.Get("request_start");
|
||||||
console.log("Original query was:", query);
|
const duration = Date.now() - startTime;
|
||||||
|
console.log("Request took", duration, "ms");
|
||||||
|
|
||||||
|
// context memory is automatically cleared when ctx.Release() is called
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use case 2: Pass data to nested agent calls
|
// Use case 4: Team-wide settings
|
||||||
function Create(ctx, messages) {
|
function Create(ctx, messages) {
|
||||||
// Prepare context for child agent
|
// Check team quota
|
||||||
ctx.space.Set("parent_context", {
|
const used = ctx.memory.team.Incr("monthly_requests");
|
||||||
user_id: ctx.authorized.user_id,
|
const limit = ctx.memory.team.Get("monthly_limit") || 10000;
|
||||||
session_start: Date.now(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Call child agent...
|
if (used > limit) {
|
||||||
|
throw new Error("Team quota exceeded");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use case 3: One-time data consumption
|
return { messages };
|
||||||
function Next(ctx, payload) {
|
|
||||||
// Get and delete in one operation
|
|
||||||
const temp_data = ctx.space.GetDel("temp_processing_data");
|
|
||||||
if (temp_data) {
|
|
||||||
// Process and discard
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use case 5: Rate limiting with counters
|
||||||
|
function Create(ctx, messages) {
|
||||||
|
const key = `rate:${new Date().toISOString().slice(0, 13)}`; // Hourly bucket
|
||||||
|
const count = ctx.memory.user.Incr(key);
|
||||||
|
|
||||||
|
if (count > 100) {
|
||||||
|
throw new Error("Rate limit exceeded");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { messages };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Memory Lifecycle
|
||||||
|
|
||||||
|
| Namespace | Created When | Cleared When |
|
||||||
|
| --------- | ---------------- | --------------- |
|
||||||
|
| `user` | First access | Manual only |
|
||||||
|
| `team` | First access | Manual only |
|
||||||
|
| `chat` | First access | Manual only |
|
||||||
|
| `context` | Context creation | `ctx.Release()` |
|
||||||
|
|
||||||
**Notes:**
|
**Notes:**
|
||||||
|
|
||||||
- Space is shared across all hooks within the same request
|
- `user`, `team`, `chat` namespaces are persistent (backed by database)
|
||||||
- Space persists across nested agent calls (A2A)
|
- `context` namespace is temporary and cleared when the request context is released
|
||||||
- Values can be any JSON-serializable data
|
- All namespaces support TTL for automatic expiration
|
||||||
- Use `GetDel` for data that should only be consumed once
|
- Wildcard deletion (`Del("prefix:*")`) works on all namespaces
|
||||||
|
- Counter operations (`Incr`, `Decr`) are atomic
|
||||||
|
|
||||||
## MCP API
|
## MCP API
|
||||||
|
|
||||||
|
|
@ -1697,7 +1931,7 @@ interface UsesConfig {
|
||||||
```javascript
|
```javascript
|
||||||
function Create(ctx, messages) {
|
function Create(ctx, messages) {
|
||||||
// Store data for Next hook
|
// Store data for Next hook
|
||||||
ctx.space.Set("user_query", messages[0]?.content);
|
ctx.memory.context.Set("user_query", messages[0]?.content);
|
||||||
|
|
||||||
// Modify messages
|
// Modify messages
|
||||||
const enhanced_messages = messages.map((msg) => ({
|
const enhanced_messages = messages.map((msg) => ({
|
||||||
|
|
@ -1873,7 +2107,7 @@ See the [Agent Execution Lifecycle](#agent-execution-lifecycle) diagram above fo
|
||||||
- **Hooks can send messages directly** via `ctx.Send()`, `ctx.SendStream()`, etc.
|
- **Hooks can send messages directly** via `ctx.Send()`, `ctx.SendStream()`, etc.
|
||||||
- **Create Hook** runs before LLM call (if any), can modify messages and configure the request
|
- **Create Hook** runs before LLM call (if any), can modify messages and configure the request
|
||||||
- **Next Hook** runs after LLM call and tool execution (if any), can post-process or delegate
|
- **Next Hook** runs after LLM call and tool execution (if any), can post-process or delegate
|
||||||
- Use `ctx.space` to pass data between Create and Next hooks
|
- Use `ctx.memory.context` to pass data between Create and Next hooks within a request
|
||||||
|
|
||||||
## Complete Example
|
## Complete Example
|
||||||
|
|
||||||
|
|
@ -1891,9 +2125,9 @@ function Create(ctx, messages) {
|
||||||
// Extract user query from the last message
|
// Extract user query from the last message
|
||||||
const user_query = messages[messages.length - 1]?.content || "";
|
const user_query = messages[messages.length - 1]?.content || "";
|
||||||
|
|
||||||
// Store data in space for use in Next hook
|
// Store data in context memory for use in Next hook
|
||||||
ctx.space.Set("original_query", user_query);
|
ctx.memory.context.Set("original_query", user_query);
|
||||||
ctx.space.Set("request_time", Date.now());
|
ctx.memory.context.Set("request_time", Date.now());
|
||||||
|
|
||||||
// Add trace node to show processing in UI
|
// Add trace node to show processing in UI
|
||||||
const create_node = ctx.trace.Add(
|
const create_node = ctx.trace.Add(
|
||||||
|
|
@ -1943,9 +2177,9 @@ function Create(ctx, messages) {
|
||||||
function Next(ctx, payload) {
|
function Next(ctx, payload) {
|
||||||
const { messages, completion, tools, error } = payload;
|
const { messages, completion, tools, error } = payload;
|
||||||
|
|
||||||
// Retrieve data from Create hook via space
|
// Retrieve data from Create hook via context memory
|
||||||
const original_query = ctx.space.Get("original_query");
|
const original_query = ctx.memory.context.Get("original_query");
|
||||||
const request_time = ctx.space.Get("request_time");
|
const request_time = ctx.memory.context.Get("request_time");
|
||||||
const duration = Date.now() - request_time;
|
const duration = Date.now() - request_time;
|
||||||
|
|
||||||
// Create trace node for Next hook processing
|
// Create trace node for Next hook processing
|
||||||
|
|
@ -2038,7 +2272,7 @@ function Next(ctx, payload) {
|
||||||
4. **Logging Levels**: Use appropriate log levels (Debug for development, Info for progress, Error for failures)
|
4. **Logging Levels**: Use appropriate log levels (Debug for development, Info for progress, Error for failures)
|
||||||
5. **Message IDs**: Let the system auto-generate message IDs unless you need specific tracking
|
5. **Message IDs**: Let the system auto-generate message IDs unless you need specific tracking
|
||||||
6. **Parallel Operations**: Use `Trace.Parallel()` for concurrent operations to maintain trace clarity
|
6. **Parallel Operations**: Use `Trace.Parallel()` for concurrent operations to maintain trace clarity
|
||||||
7. **Space Usage**: Use `ctx.space` for passing data between hooks and nested agent calls
|
7. **Memory Usage**: Use `ctx.memory.context` for request-scoped data, `ctx.memory.chat` for chat state, `ctx.memory.user` for user preferences
|
||||||
8. **Streaming Messages**: Use `SendStream()` + `Append()` + `End()` for streaming output; use `Send()` for complete messages
|
8. **Streaming Messages**: Use `SendStream()` + `Append()` + `End()` for streaming output; use `Send()` for complete messages
|
||||||
9. **Block Grouping**: Only use Block IDs when you need to group multiple messages together (e.g., LLM output + follow-up card)
|
9. **Block Grouping**: Only use Block IDs when you need to group multiple messages together (e.g., LLM output + follow-up card)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/yaoapp/gou/plan"
|
"github.com/yaoapp/yao/agent/memory"
|
||||||
"github.com/yaoapp/yao/agent/output/message"
|
"github.com/yaoapp/yao/agent/output/message"
|
||||||
"github.com/yaoapp/yao/config"
|
"github.com/yaoapp/yao/config"
|
||||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
|
|
@ -27,11 +27,21 @@ func New(parent context.Context, authorized *types.AuthorizedInfo, chatID string
|
||||||
|
|
||||||
contextID := generateContextID()
|
contextID := generateContextID()
|
||||||
|
|
||||||
|
// Extract user and team IDs from authorized info
|
||||||
|
var userID, teamID string
|
||||||
|
if authorized != nil {
|
||||||
|
userID = authorized.UserID
|
||||||
|
teamID = authorized.TeamID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create memory instance using global manager
|
||||||
|
mem, _ := memory.GetMemory(userID, teamID, chatID, contextID)
|
||||||
|
|
||||||
ctx := &Context{
|
ctx := &Context{
|
||||||
Context: parent,
|
Context: parent,
|
||||||
ID: contextID, // Generate unique ID for the context
|
ID: contextID, // Generate unique ID for the context
|
||||||
Authorized: authorized, // Set authorized info
|
Authorized: authorized, // Set authorized info
|
||||||
Space: plan.NewMemorySharedSpace(),
|
Memory: mem,
|
||||||
ChatID: chatID,
|
ChatID: chatID,
|
||||||
IDGenerator: message.NewIDGenerator(), // Initialize ID generator for this context
|
IDGenerator: message.NewIDGenerator(), // Initialize ID generator for this context
|
||||||
messageMetadata: newMessageMetadataStore(), // Initialize message metadata store
|
messageMetadata: newMessageMetadataStore(), // Initialize message metadata store
|
||||||
|
|
@ -78,14 +88,15 @@ func (ctx *Context) Release() {
|
||||||
ctx.trace = nil
|
ctx.trace = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear space
|
// Clear context-level memory only (request-scoped temporary data)
|
||||||
if ctx.Space != nil {
|
// User, Team, Chat level memory is persistent and should NOT be cleared
|
||||||
|
if ctx.Memory != nil && ctx.Memory.Context != nil {
|
||||||
if ctx.Logger != nil {
|
if ctx.Logger != nil {
|
||||||
ctx.Logger.Cleanup("Space")
|
ctx.Logger.Cleanup("Memory.Context")
|
||||||
}
|
}
|
||||||
ctx.Space.Clear()
|
ctx.Memory.Context.Clear()
|
||||||
ctx.Space = nil
|
|
||||||
}
|
}
|
||||||
|
ctx.Memory = nil
|
||||||
|
|
||||||
// Clear stacks
|
// Clear stacks
|
||||||
if ctx.Stacks != nil {
|
if ctx.Stacks != nil {
|
||||||
|
|
@ -379,9 +390,9 @@ func (ctx *Context) BeginStep(stepType string, input map[string]interface{}) *Bu
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update space snapshot before starting step
|
// Update context memory snapshot before starting step (for recovery)
|
||||||
if ctx.Space != nil {
|
if ctx.Memory != nil && ctx.Memory.Context != nil {
|
||||||
ctx.Buffer.SetSpaceSnapshot(ctx.Space.Snapshot())
|
ctx.Buffer.SetSpaceSnapshot(ctx.Memory.Context.Snapshot())
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.Buffer.BeginStep(stepType, input, ctx.Stack)
|
return ctx.Buffer.BeginStep(stepType, input, ctx.Stack)
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,7 @@ func TestGetCompletionRequest(t *testing.T) {
|
||||||
assert.Equal(t, tt.expectedReferer, ctx.Referer)
|
assert.Equal(t, tt.expectedReferer, ctx.Referer)
|
||||||
assert.Equal(t, tt.expectedAccept, ctx.Accept)
|
assert.Equal(t, tt.expectedAccept, ctx.Accept)
|
||||||
assert.Equal(t, tt.expectedAssistantID, ctx.AssistantID)
|
assert.Equal(t, tt.expectedAssistantID, ctx.AssistantID)
|
||||||
assert.NotNil(t, ctx.Space)
|
assert.NotNil(t, ctx.Memory)
|
||||||
assert.NotNil(t, ctx.Cache)
|
assert.NotNil(t, ctx.Cache)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -227,7 +227,7 @@ func TestContextNew_WithAuthorized(t *testing.T) {
|
||||||
|
|
||||||
assert.NotNil(t, ctx)
|
assert.NotNil(t, ctx)
|
||||||
assert.Equal(t, "test-chat-id", ctx.ChatID)
|
assert.Equal(t, "test-chat-id", ctx.ChatID)
|
||||||
assert.NotNil(t, ctx.Space)
|
assert.NotNil(t, ctx.Memory)
|
||||||
assert.NotNil(t, ctx.IDGenerator)
|
assert.NotNil(t, ctx.IDGenerator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
package context
|
package context
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/yaoapp/gou/runtime/v8/bridge"
|
"github.com/yaoapp/gou/runtime/v8/bridge"
|
||||||
|
"github.com/yaoapp/yao/agent/memory"
|
||||||
"github.com/yaoapp/yao/agent/output"
|
"github.com/yaoapp/yao/agent/output"
|
||||||
"github.com/yaoapp/yao/agent/output/message"
|
"github.com/yaoapp/yao/agent/output/message"
|
||||||
traceJsapi "github.com/yaoapp/yao/trace/jsapi"
|
traceJsapi "github.com/yaoapp/yao/trace/jsapi"
|
||||||
|
|
@ -136,11 +139,11 @@ func (ctx *Context) NewObject(v8ctx *v8go.Context) (*v8go.Value, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Space object - create a JavaScript object with Get/Set/Delete methods
|
// Memory object - create a JavaScript object with User/Team/Chat/Context namespaces
|
||||||
if ctx.Space != nil {
|
if ctx.Memory != nil {
|
||||||
spaceObj := ctx.createSpaceObject(v8ctx)
|
memoryObj := ctx.createMemoryObject(v8ctx)
|
||||||
obj.Set("space", spaceObj)
|
obj.Set("memory", memoryObj)
|
||||||
spaceObj.Release()
|
memoryObj.Release()
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance.Value, nil
|
return instance.Value, nil
|
||||||
|
|
@ -718,26 +721,57 @@ func (ctx *Context) endBlockMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// createSpaceObject creates a Space object for JavaScript access
|
// createMemoryObject creates a Memory object for JavaScript access
|
||||||
// Space is a shared data space for passing data between requests and calls
|
// Memory provides four namespaces: User, Team, Chat, Context
|
||||||
func (ctx *Context) createSpaceObject(v8ctx *v8go.Context) *v8go.Value {
|
// Each namespace supports: Get, Set, Del, Has, Keys, Len, Clear, Incr, Decr
|
||||||
|
func (ctx *Context) createMemoryObject(v8ctx *v8go.Context) *v8go.Value {
|
||||||
iso := v8ctx.Isolate()
|
iso := v8ctx.Isolate()
|
||||||
spaceObj, _ := v8ctx.RunScript("({})", "space-init")
|
objTpl := v8go.NewObjectTemplate(iso)
|
||||||
obj, _ := spaceObj.AsObject()
|
obj, _ := objTpl.NewInstance(v8ctx)
|
||||||
|
|
||||||
// Get method: space.Get(key)
|
// Create namespace accessors
|
||||||
getFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
if ctx.Memory.User != nil {
|
||||||
if ctx.Space == nil {
|
userObj := ctx.createNamespaceObject(v8ctx, ctx.Memory.User)
|
||||||
return v8go.Null(iso)
|
obj.Set("user", userObj)
|
||||||
|
userObj.Release()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ctx.Memory.Team != nil {
|
||||||
|
teamObj := ctx.createNamespaceObject(v8ctx, ctx.Memory.Team)
|
||||||
|
obj.Set("team", teamObj)
|
||||||
|
teamObj.Release()
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.Memory.Chat != nil {
|
||||||
|
chatObj := ctx.createNamespaceObject(v8ctx, ctx.Memory.Chat)
|
||||||
|
obj.Set("chat", chatObj)
|
||||||
|
chatObj.Release()
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.Memory.Context != nil {
|
||||||
|
contextObj := ctx.createNamespaceObject(v8ctx, ctx.Memory.Context)
|
||||||
|
obj.Set("context", contextObj)
|
||||||
|
contextObj.Release()
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// createNamespaceObject creates a namespace object with KV store methods
|
||||||
|
func (ctx *Context) createNamespaceObject(v8ctx *v8go.Context, ns *memory.Namespace) *v8go.Value {
|
||||||
|
iso := v8ctx.Isolate()
|
||||||
|
objTpl := v8go.NewObjectTemplate(iso)
|
||||||
|
obj, _ := objTpl.NewInstance(v8ctx)
|
||||||
|
|
||||||
|
// Get method: ns.Get(key)
|
||||||
|
getFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
if len(info.Args()) < 1 {
|
if len(info.Args()) < 1 {
|
||||||
return bridge.JsException(info.Context(), "Get requires a key argument")
|
return bridge.JsException(info.Context(), "Get requires a key argument")
|
||||||
}
|
}
|
||||||
|
|
||||||
key := info.Args()[0].String()
|
key := info.Args()[0].String()
|
||||||
value, err := ctx.Space.Get(key)
|
value, ok := ns.Get(key)
|
||||||
if err != nil {
|
if !ok {
|
||||||
return v8go.Null(iso)
|
return v8go.Null(iso)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -751,12 +785,8 @@ func (ctx *Context) createSpaceObject(v8ctx *v8go.Context) *v8go.Value {
|
||||||
getFuncVal := getFunc.GetFunction(v8ctx)
|
getFuncVal := getFunc.GetFunction(v8ctx)
|
||||||
obj.Set("Get", getFuncVal.Value)
|
obj.Set("Get", getFuncVal.Value)
|
||||||
|
|
||||||
// Set method: space.Set(key, value)
|
// Set method: ns.Set(key, value, ttl?)
|
||||||
setFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
setFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
if ctx.Space == nil {
|
|
||||||
return bridge.JsException(info.Context(), "Space is not available")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(info.Args()) < 2 {
|
if len(info.Args()) < 2 {
|
||||||
return bridge.JsException(info.Context(), "Set requires key and value arguments")
|
return bridge.JsException(info.Context(), "Set requires key and value arguments")
|
||||||
}
|
}
|
||||||
|
|
@ -767,7 +797,14 @@ func (ctx *Context) createSpaceObject(v8ctx *v8go.Context) *v8go.Value {
|
||||||
return bridge.JsException(info.Context(), "Failed to convert value: "+err.Error())
|
return bridge.JsException(info.Context(), "Failed to convert value: "+err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ctx.Space.Set(key, value); err != nil {
|
// Optional TTL in milliseconds (third argument)
|
||||||
|
var ttl time.Duration
|
||||||
|
if len(info.Args()) >= 3 && info.Args()[2].IsNumber() {
|
||||||
|
ttlMs := info.Args()[2].Integer()
|
||||||
|
ttl = time.Duration(ttlMs) * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ns.Set(key, value, ttl); err != nil {
|
||||||
return bridge.JsException(info.Context(), "Failed to set value: "+err.Error())
|
return bridge.JsException(info.Context(), "Failed to set value: "+err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -776,50 +813,128 @@ func (ctx *Context) createSpaceObject(v8ctx *v8go.Context) *v8go.Value {
|
||||||
setFuncVal := setFunc.GetFunction(v8ctx)
|
setFuncVal := setFunc.GetFunction(v8ctx)
|
||||||
obj.Set("Set", setFuncVal.Value)
|
obj.Set("Set", setFuncVal.Value)
|
||||||
|
|
||||||
// Delete method: space.Delete(key)
|
// Del method: ns.Del(key)
|
||||||
delFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
delFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
if ctx.Space == nil {
|
|
||||||
return bridge.JsException(info.Context(), "Space is not available")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(info.Args()) < 1 {
|
if len(info.Args()) < 1 {
|
||||||
return bridge.JsException(info.Context(), "Delete requires a key argument")
|
return bridge.JsException(info.Context(), "Del requires a key argument")
|
||||||
}
|
}
|
||||||
|
|
||||||
key := info.Args()[0].String()
|
key := info.Args()[0].String()
|
||||||
if err := ctx.Space.Delete(key); err != nil {
|
if err := ns.Del(key); err != nil {
|
||||||
return bridge.JsException(info.Context(), "Failed to delete key: "+err.Error())
|
return bridge.JsException(info.Context(), "Failed to delete key: "+err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return v8go.Undefined(iso)
|
return v8go.Undefined(iso)
|
||||||
})
|
})
|
||||||
delFuncVal := delFunc.GetFunction(v8ctx)
|
delFuncVal := delFunc.GetFunction(v8ctx)
|
||||||
obj.Set("Delete", delFuncVal.Value)
|
obj.Set("Del", delFuncVal.Value)
|
||||||
|
|
||||||
// GetDel method: space.GetDel(key) - Get value and delete immediately
|
// Has method: ns.Has(key)
|
||||||
// Convenient for one-time use data (e.g., file metadata passed between agents)
|
hasFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
getDelFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
if len(info.Args()) < 1 {
|
||||||
if ctx.Space == nil {
|
return bridge.JsException(info.Context(), "Has requires a key argument")
|
||||||
return v8go.Null(iso)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
key := info.Args()[0].String()
|
||||||
|
exists := ns.Has(key)
|
||||||
|
|
||||||
|
jsValue, _ := v8go.NewValue(iso, exists)
|
||||||
|
return jsValue
|
||||||
|
})
|
||||||
|
hasFuncVal := hasFunc.GetFunction(v8ctx)
|
||||||
|
obj.Set("Has", hasFuncVal.Value)
|
||||||
|
|
||||||
|
// Keys method: ns.Keys()
|
||||||
|
keysFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
|
keys := ns.Keys()
|
||||||
|
jsValue, err := bridge.JsValue(info.Context(), keys)
|
||||||
|
if err != nil {
|
||||||
|
return bridge.JsException(info.Context(), "Failed to get keys: "+err.Error())
|
||||||
|
}
|
||||||
|
return jsValue
|
||||||
|
})
|
||||||
|
keysFuncVal := keysFunc.GetFunction(v8ctx)
|
||||||
|
obj.Set("Keys", keysFuncVal.Value)
|
||||||
|
|
||||||
|
// Len method: ns.Len()
|
||||||
|
lenFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
|
length := ns.Len()
|
||||||
|
// Use int32 for JavaScript Number (int64 becomes BigInt which is incompatible)
|
||||||
|
jsValue, _ := v8go.NewValue(iso, int32(length))
|
||||||
|
return jsValue
|
||||||
|
})
|
||||||
|
lenFuncVal := lenFunc.GetFunction(v8ctx)
|
||||||
|
obj.Set("Len", lenFuncVal.Value)
|
||||||
|
|
||||||
|
// Clear method: ns.Clear()
|
||||||
|
clearFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
|
ns.Clear()
|
||||||
|
return v8go.Undefined(iso)
|
||||||
|
})
|
||||||
|
clearFuncVal := clearFunc.GetFunction(v8ctx)
|
||||||
|
obj.Set("Clear", clearFuncVal.Value)
|
||||||
|
|
||||||
|
// Incr method: ns.Incr(key, delta?)
|
||||||
|
incrFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
|
if len(info.Args()) < 1 {
|
||||||
|
return bridge.JsException(info.Context(), "Incr requires a key argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
key := info.Args()[0].String()
|
||||||
|
delta := int64(1)
|
||||||
|
if len(info.Args()) >= 2 && info.Args()[1].IsNumber() {
|
||||||
|
delta = info.Args()[1].Integer()
|
||||||
|
}
|
||||||
|
|
||||||
|
newValue, err := ns.Incr(key, delta)
|
||||||
|
if err != nil {
|
||||||
|
return bridge.JsException(info.Context(), "Failed to increment: "+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use int32 for JavaScript Number (int64 becomes BigInt which is incompatible with ===)
|
||||||
|
// For counters, int32 range (-2^31 to 2^31-1) is sufficient
|
||||||
|
jsValue, _ := v8go.NewValue(iso, int32(newValue))
|
||||||
|
return jsValue
|
||||||
|
})
|
||||||
|
incrFuncVal := incrFunc.GetFunction(v8ctx)
|
||||||
|
obj.Set("Incr", incrFuncVal.Value)
|
||||||
|
|
||||||
|
// Decr method: ns.Decr(key, delta?)
|
||||||
|
decrFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
|
if len(info.Args()) < 1 {
|
||||||
|
return bridge.JsException(info.Context(), "Decr requires a key argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
key := info.Args()[0].String()
|
||||||
|
delta := int64(1)
|
||||||
|
if len(info.Args()) >= 2 && info.Args()[1].IsNumber() {
|
||||||
|
delta = info.Args()[1].Integer()
|
||||||
|
}
|
||||||
|
|
||||||
|
newValue, err := ns.Decr(key, delta)
|
||||||
|
if err != nil {
|
||||||
|
return bridge.JsException(info.Context(), "Failed to decrement: "+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use int32 for JavaScript Number (int64 becomes BigInt which is incompatible with ===)
|
||||||
|
jsValue, _ := v8go.NewValue(iso, int32(newValue))
|
||||||
|
return jsValue
|
||||||
|
})
|
||||||
|
decrFuncVal := decrFunc.GetFunction(v8ctx)
|
||||||
|
obj.Set("Decr", decrFuncVal.Value)
|
||||||
|
|
||||||
|
// GetDel method: ns.GetDel(key) - Get value and delete immediately
|
||||||
|
getDelFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
if len(info.Args()) < 1 {
|
if len(info.Args()) < 1 {
|
||||||
return bridge.JsException(info.Context(), "GetDel requires a key argument")
|
return bridge.JsException(info.Context(), "GetDel requires a key argument")
|
||||||
}
|
}
|
||||||
|
|
||||||
key := info.Args()[0].String()
|
key := info.Args()[0].String()
|
||||||
|
value, ok := ns.GetDel(key)
|
||||||
// Get value first
|
if !ok {
|
||||||
value, err := ctx.Space.Get(key)
|
|
||||||
if err != nil {
|
|
||||||
return v8go.Null(iso)
|
return v8go.Null(iso)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete immediately after getting
|
|
||||||
// Ignore delete errors (key might not exist)
|
|
||||||
ctx.Space.Delete(key)
|
|
||||||
|
|
||||||
// Convert to JavaScript value
|
|
||||||
jsValue, err := bridge.JsValue(info.Context(), value)
|
jsValue, err := bridge.JsValue(info.Context(), value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return v8go.Null(iso)
|
return v8go.Null(iso)
|
||||||
|
|
@ -830,7 +945,7 @@ func (ctx *Context) createSpaceObject(v8ctx *v8go.Context) *v8go.Value {
|
||||||
getDelFuncVal := getDelFunc.GetFunction(v8ctx)
|
getDelFuncVal := getDelFunc.GetFunction(v8ctx)
|
||||||
obj.Set("GetDel", getDelFuncVal.Value)
|
obj.Set("GetDel", getDelFuncVal.Value)
|
||||||
|
|
||||||
return spaceObj
|
return obj.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendGroupMethod implements ctx.SendGroup(group)
|
// sendGroupMethod implements ctx.SendGroup(group)
|
||||||
|
|
|
||||||
|
|
@ -11,21 +11,23 @@ import (
|
||||||
"github.com/yaoapp/yao/test"
|
"github.com/yaoapp/yao/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// newMCPTestContext creates a test context for MCP testing
|
||||||
|
func newMCPTestContext() *context.Context {
|
||||||
|
ctx := context.New(stdContext.Background(), nil, "test-chat-id")
|
||||||
|
ctx.AssistantID = "test-assistant-id"
|
||||||
|
ctx.Locale = "en"
|
||||||
|
ctx.Referer = context.RefererAPI
|
||||||
|
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
||||||
|
ctx.Stack = stack
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
// TestMCPListResources tests MCP.ListResources from JavaScript
|
// TestMCPListResources tests MCP.ListResources from JavaScript
|
||||||
func TestMCPListResources(t *testing.T) {
|
func TestMCPListResources(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
// Initialize context with trace
|
ctx := newMCPTestContext()
|
||||||
ctx := &context.Context{
|
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
|
||||||
ctx.Stack = stack
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
@ -62,15 +64,7 @@ func TestMCPReadResource(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
ctx := &context.Context{
|
ctx := newMCPTestContext()
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
|
||||||
ctx.Stack = stack
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
@ -105,15 +99,7 @@ func TestMCPListTools(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
ctx := &context.Context{
|
ctx := newMCPTestContext()
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
|
||||||
ctx.Stack = stack
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
@ -152,15 +138,7 @@ func TestMCPCallTool(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
ctx := &context.Context{
|
ctx := newMCPTestContext()
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
|
||||||
ctx.Stack = stack
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
@ -195,15 +173,7 @@ func TestMCPCallTools(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
ctx := &context.Context{
|
ctx := newMCPTestContext()
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
|
||||||
ctx.Stack = stack
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
@ -243,15 +213,7 @@ func TestMCPCallToolsParallel(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
ctx := &context.Context{
|
ctx := newMCPTestContext()
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
|
||||||
ctx.Stack = stack
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
@ -291,15 +253,7 @@ func TestMCPListPrompts(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
ctx := &context.Context{
|
ctx := newMCPTestContext()
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
|
||||||
ctx.Stack = stack
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
@ -336,15 +290,7 @@ func TestMCPGetPrompt(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
ctx := &context.Context{
|
ctx := newMCPTestContext()
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
|
||||||
ctx.Stack = stack
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
@ -379,15 +325,7 @@ func TestMCPListSamples(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
ctx := &context.Context{
|
ctx := newMCPTestContext()
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
|
||||||
ctx.Stack = stack
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
@ -422,15 +360,7 @@ func TestMCPGetSample(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
ctx := &context.Context{
|
ctx := newMCPTestContext()
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
|
||||||
ctx.Stack = stack
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
@ -467,15 +397,7 @@ func TestMCPJsApiWithTrace(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
ctx := &context.Context{
|
ctx := newMCPTestContext()
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
|
||||||
ctx.Stack = stack
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
|
||||||
561
agent/context/jsapi_memory_test.go
Normal file
561
agent/context/jsapi_memory_test.go
Normal file
|
|
@ -0,0 +1,561 @@
|
||||||
|
package context_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdContext "context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
v8 "github.com/yaoapp/gou/runtime/v8"
|
||||||
|
"github.com/yaoapp/yao/agent/context"
|
||||||
|
"github.com/yaoapp/yao/agent/memory"
|
||||||
|
"github.com/yaoapp/yao/config"
|
||||||
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
|
"github.com/yaoapp/yao/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMemoryUserNamespace(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
mem, err := memory.New(nil, "user1", "team1", "chat1", "ctx1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx := &context.Context{
|
||||||
|
ChatID: "test-chat-id",
|
||||||
|
AssistantID: "test-assistant-id",
|
||||||
|
Locale: "en",
|
||||||
|
Context: stdContext.Background(),
|
||||||
|
Memory: mem,
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
|
function test(ctx) {
|
||||||
|
try {
|
||||||
|
// Set values in user namespace
|
||||||
|
ctx.memory.user.Set("name", "John");
|
||||||
|
ctx.memory.user.Set("age", 30);
|
||||||
|
ctx.memory.user.Set("active", true);
|
||||||
|
|
||||||
|
// Get values back
|
||||||
|
const name = ctx.memory.user.Get("name");
|
||||||
|
const age = ctx.memory.user.Get("age");
|
||||||
|
const active = ctx.memory.user.Get("active");
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
if (name !== "John") throw new Error("Name mismatch");
|
||||||
|
if (age !== 30) throw new Error("Age mismatch");
|
||||||
|
if (active !== true) throw new Error("Active mismatch");
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
name: name,
|
||||||
|
age: age,
|
||||||
|
active: active
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}`, ctx)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
result := res.(map[string]interface{})
|
||||||
|
assert.True(t, result["success"].(bool))
|
||||||
|
assert.Equal(t, "John", result["name"])
|
||||||
|
assert.Equal(t, float64(30), result["age"])
|
||||||
|
assert.Equal(t, true, result["active"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryTeamNamespace(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
mem, err := memory.New(nil, "user1", "team1", "chat1", "ctx1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx := &context.Context{
|
||||||
|
ChatID: "test-chat-id",
|
||||||
|
AssistantID: "test-assistant-id",
|
||||||
|
Locale: "en",
|
||||||
|
Context: stdContext.Background(),
|
||||||
|
Memory: mem,
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
|
function test(ctx) {
|
||||||
|
try {
|
||||||
|
// Set team-wide settings
|
||||||
|
ctx.memory.team.Set("settings", { theme: "dark", language: "en" });
|
||||||
|
|
||||||
|
// Get back
|
||||||
|
const settings = ctx.memory.team.Get("settings");
|
||||||
|
|
||||||
|
if (settings.theme !== "dark") throw new Error("Theme mismatch");
|
||||||
|
if (settings.language !== "en") throw new Error("Language mismatch");
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
settings: settings
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}`, ctx)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
result := res.(map[string]interface{})
|
||||||
|
assert.True(t, result["success"].(bool))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryChatNamespace(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
mem, err := memory.New(nil, "user1", "team1", "chat1", "ctx1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx := &context.Context{
|
||||||
|
ChatID: "test-chat-id",
|
||||||
|
AssistantID: "test-assistant-id",
|
||||||
|
Locale: "en",
|
||||||
|
Context: stdContext.Background(),
|
||||||
|
Memory: mem,
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
|
function test(ctx) {
|
||||||
|
try {
|
||||||
|
// Set chat context
|
||||||
|
ctx.memory.chat.Set("topic", "AI Discussion");
|
||||||
|
ctx.memory.chat.Set("participants", ["Alice", "Bob"]);
|
||||||
|
|
||||||
|
// Get back
|
||||||
|
const topic = ctx.memory.chat.Get("topic");
|
||||||
|
const participants = ctx.memory.chat.Get("participants");
|
||||||
|
|
||||||
|
if (topic !== "AI Discussion") throw new Error("Topic mismatch");
|
||||||
|
if (participants.length !== 2) throw new Error("Participants mismatch");
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
topic: topic,
|
||||||
|
participants: participants
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}`, ctx)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
result := res.(map[string]interface{})
|
||||||
|
assert.True(t, result["success"].(bool))
|
||||||
|
assert.Equal(t, "AI Discussion", result["topic"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryContextNamespace(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
mem, err := memory.New(nil, "user1", "team1", "chat1", "ctx1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx := &context.Context{
|
||||||
|
ChatID: "test-chat-id",
|
||||||
|
AssistantID: "test-assistant-id",
|
||||||
|
Locale: "en",
|
||||||
|
Context: stdContext.Background(),
|
||||||
|
Memory: mem,
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
|
function test(ctx) {
|
||||||
|
try {
|
||||||
|
// Set temporary context data
|
||||||
|
ctx.memory.context.Set("temp_result", { step: 1, data: "processing" });
|
||||||
|
|
||||||
|
// Get back
|
||||||
|
const result = ctx.memory.context.Get("temp_result");
|
||||||
|
|
||||||
|
if (result.step !== 1) throw new Error("Step mismatch");
|
||||||
|
if (result.data !== "processing") throw new Error("Data mismatch");
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
result: result
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}`, ctx)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
result := res.(map[string]interface{})
|
||||||
|
assert.True(t, result["success"].(bool))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryHasAndDel(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
mem, err := memory.New(nil, "user1", "team1", "chat1", "ctx1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx := &context.Context{
|
||||||
|
ChatID: "test-chat-id",
|
||||||
|
AssistantID: "test-assistant-id",
|
||||||
|
Locale: "en",
|
||||||
|
Context: stdContext.Background(),
|
||||||
|
Memory: mem,
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
|
function test(ctx) {
|
||||||
|
try {
|
||||||
|
// Set a value
|
||||||
|
ctx.memory.user.Set("key", "value");
|
||||||
|
|
||||||
|
// Check Has
|
||||||
|
const hasBefore = ctx.memory.user.Has("key");
|
||||||
|
if (!hasBefore) throw new Error("Should have key before delete");
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
ctx.memory.user.Del("key");
|
||||||
|
|
||||||
|
// Check Has again
|
||||||
|
const hasAfter = ctx.memory.user.Has("key");
|
||||||
|
if (hasAfter) throw new Error("Should not have key after delete");
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
hasBefore: hasBefore,
|
||||||
|
hasAfter: hasAfter
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}`, ctx)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
result := res.(map[string]interface{})
|
||||||
|
assert.True(t, result["success"].(bool))
|
||||||
|
assert.True(t, result["hasBefore"].(bool))
|
||||||
|
assert.False(t, result["hasAfter"].(bool))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryIncrDecr(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
mem, err := memory.New(nil, "user1", "team1", "chat1", "ctx1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx := &context.Context{
|
||||||
|
ChatID: "test-chat-id",
|
||||||
|
AssistantID: "test-assistant-id",
|
||||||
|
Locale: "en",
|
||||||
|
Context: stdContext.Background(),
|
||||||
|
Memory: mem,
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
|
function test(ctx) {
|
||||||
|
try {
|
||||||
|
// Incr on non-existent key
|
||||||
|
const v1 = ctx.memory.user.Incr("counter");
|
||||||
|
if (v1 !== 1) throw new Error("First incr should be 1, got " + v1);
|
||||||
|
|
||||||
|
// Incr with delta
|
||||||
|
const v2 = ctx.memory.user.Incr("counter", 5);
|
||||||
|
if (v2 !== 6) throw new Error("Second incr should be 6, got " + v2);
|
||||||
|
|
||||||
|
// Decr
|
||||||
|
const v3 = ctx.memory.user.Decr("counter", 2);
|
||||||
|
if (v3 !== 4) throw new Error("Decr should be 4, got " + v3);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
v1: v1,
|
||||||
|
v2: v2,
|
||||||
|
v3: v3
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}`, ctx)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
result := res.(map[string]interface{})
|
||||||
|
assert.True(t, result["success"].(bool))
|
||||||
|
assert.Equal(t, float64(1), result["v1"])
|
||||||
|
assert.Equal(t, float64(6), result["v2"])
|
||||||
|
assert.Equal(t, float64(4), result["v3"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryKeysAndLen(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
// Use unique IDs to avoid data pollution from other tests
|
||||||
|
mem, err := memory.New(nil, "user-keys-len", "team-keys-len", "chat-keys-len", "ctx-keys-len")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx := &context.Context{
|
||||||
|
ChatID: "test-chat-id",
|
||||||
|
AssistantID: "test-assistant-id",
|
||||||
|
Locale: "en",
|
||||||
|
Context: stdContext.Background(),
|
||||||
|
Memory: mem,
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
|
function test(ctx) {
|
||||||
|
try {
|
||||||
|
// Set multiple values
|
||||||
|
ctx.memory.user.Set("a", 1);
|
||||||
|
ctx.memory.user.Set("b", 2);
|
||||||
|
ctx.memory.user.Set("c", 3);
|
||||||
|
|
||||||
|
// Get keys
|
||||||
|
const keys = ctx.memory.user.Keys();
|
||||||
|
if (keys.length !== 3) throw new Error("Should have 3 keys, got " + keys.length);
|
||||||
|
|
||||||
|
// Get len
|
||||||
|
const len = ctx.memory.user.Len();
|
||||||
|
if (len !== 3) throw new Error("Len should be 3, got " + len);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
keys: keys,
|
||||||
|
len: len
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}`, ctx)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
result := res.(map[string]interface{})
|
||||||
|
if !result["success"].(bool) {
|
||||||
|
t.Fatalf("Test failed: %v", result["error"])
|
||||||
|
}
|
||||||
|
assert.Equal(t, float64(3), result["len"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryClear(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
mem, err := memory.New(nil, "user1", "team1", "chat1", "ctx1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx := &context.Context{
|
||||||
|
ChatID: "test-chat-id",
|
||||||
|
AssistantID: "test-assistant-id",
|
||||||
|
Locale: "en",
|
||||||
|
Context: stdContext.Background(),
|
||||||
|
Memory: mem,
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
|
function test(ctx) {
|
||||||
|
try {
|
||||||
|
// Set values
|
||||||
|
ctx.memory.user.Set("a", 1);
|
||||||
|
ctx.memory.user.Set("b", 2);
|
||||||
|
|
||||||
|
// Clear
|
||||||
|
ctx.memory.user.Clear();
|
||||||
|
|
||||||
|
// Check len
|
||||||
|
const len = ctx.memory.user.Len();
|
||||||
|
if (len !== 0) throw new Error("Len should be 0 after clear, got " + len);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
len: len
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}`, ctx)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
result := res.(map[string]interface{})
|
||||||
|
assert.True(t, result["success"].(bool))
|
||||||
|
assert.Equal(t, float64(0), result["len"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryGetDel(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
mem, err := memory.New(nil, "user1", "team1", "chat1", "ctx1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx := &context.Context{
|
||||||
|
ChatID: "test-chat-id",
|
||||||
|
AssistantID: "test-assistant-id",
|
||||||
|
Locale: "en",
|
||||||
|
Context: stdContext.Background(),
|
||||||
|
Memory: mem,
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
|
function test(ctx) {
|
||||||
|
try {
|
||||||
|
// Set a one-time value
|
||||||
|
ctx.memory.user.Set("token", "secret123");
|
||||||
|
|
||||||
|
// GetDel
|
||||||
|
const value = ctx.memory.user.GetDel("token");
|
||||||
|
if (value !== "secret123") throw new Error("Value mismatch");
|
||||||
|
|
||||||
|
// Should be deleted
|
||||||
|
const after = ctx.memory.user.Get("token");
|
||||||
|
if (after !== null) throw new Error("Should be null after GetDel");
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: value,
|
||||||
|
after: after
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}`, ctx)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
result := res.(map[string]interface{})
|
||||||
|
assert.True(t, result["success"].(bool))
|
||||||
|
assert.Equal(t, "secret123", result["value"])
|
||||||
|
assert.Nil(t, result["after"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryIsolation(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
// Create two different memory instances
|
||||||
|
mem1, err := memory.New(nil, "user1", "", "", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
mem2, err := memory.New(nil, "user2", "", "", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx1 := &context.Context{
|
||||||
|
ChatID: "chat1",
|
||||||
|
Context: stdContext.Background(),
|
||||||
|
Memory: mem1,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx2 := &context.Context{
|
||||||
|
ChatID: "chat2",
|
||||||
|
Context: stdContext.Background(),
|
||||||
|
Memory: mem2,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set value in user1
|
||||||
|
res1, err := v8.Call(v8.CallOptions{}, `
|
||||||
|
function test(ctx) {
|
||||||
|
ctx.memory.user.Set("key", "user1_value");
|
||||||
|
return ctx.memory.user.Get("key");
|
||||||
|
}`, ctx1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "user1_value", res1)
|
||||||
|
|
||||||
|
// Set value in user2
|
||||||
|
res2, err := v8.Call(v8.CallOptions{}, `
|
||||||
|
function test(ctx) {
|
||||||
|
ctx.memory.user.Set("key", "user2_value");
|
||||||
|
return ctx.memory.user.Get("key");
|
||||||
|
}`, ctx2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "user2_value", res2)
|
||||||
|
|
||||||
|
// Verify user1 still has its own value
|
||||||
|
res3, err := v8.Call(v8.CallOptions{}, `
|
||||||
|
function test(ctx) {
|
||||||
|
return ctx.memory.user.Get("key");
|
||||||
|
}`, ctx1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "user1_value", res3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryNoMemory(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
ctx := &context.Context{
|
||||||
|
ChatID: "test-chat-id",
|
||||||
|
Context: stdContext.Background(),
|
||||||
|
Memory: nil, // No memory
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
|
function test(ctx) {
|
||||||
|
try {
|
||||||
|
const hasMemory = ctx.memory !== undefined && ctx.memory !== null;
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
hasMemory: hasMemory
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}`, ctx)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
result := res.(map[string]interface{})
|
||||||
|
assert.True(t, result["success"].(bool))
|
||||||
|
assert.False(t, result["hasMemory"].(bool))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryWithAuthorizedInfo(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
// Use context.New to create context with authorized info
|
||||||
|
authorized := &types.AuthorizedInfo{
|
||||||
|
UserID: "user123",
|
||||||
|
TeamID: "team456",
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.New(stdContext.Background(), authorized, "chat789")
|
||||||
|
defer ctx.Release()
|
||||||
|
|
||||||
|
// Verify memory was created with correct IDs
|
||||||
|
require.NotNil(t, ctx.Memory)
|
||||||
|
require.NotNil(t, ctx.Memory.User)
|
||||||
|
require.NotNil(t, ctx.Memory.Team)
|
||||||
|
require.NotNil(t, ctx.Memory.Chat)
|
||||||
|
require.NotNil(t, ctx.Memory.Context)
|
||||||
|
|
||||||
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
|
function test(ctx) {
|
||||||
|
try {
|
||||||
|
// Set values in different namespaces
|
||||||
|
ctx.memory.user.Set("pref", "dark");
|
||||||
|
ctx.memory.team.Set("setting", "shared");
|
||||||
|
ctx.memory.chat.Set("topic", "test");
|
||||||
|
ctx.memory.context.Set("temp", "data");
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
user: ctx.memory.user.Get("pref"),
|
||||||
|
team: ctx.memory.team.Get("setting"),
|
||||||
|
chat: ctx.memory.chat.Get("topic"),
|
||||||
|
context: ctx.memory.context.Get("temp")
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}`, ctx)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
result := res.(map[string]interface{})
|
||||||
|
assert.True(t, result["success"].(bool))
|
||||||
|
assert.Equal(t, "dark", result["user"])
|
||||||
|
assert.Equal(t, "shared", result["team"])
|
||||||
|
assert.Equal(t, "test", result["chat"])
|
||||||
|
assert.Equal(t, "data", result["context"])
|
||||||
|
}
|
||||||
|
|
@ -7,27 +7,26 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
v8 "github.com/yaoapp/gou/runtime/v8"
|
v8 "github.com/yaoapp/gou/runtime/v8"
|
||||||
"github.com/yaoapp/yao/agent/context"
|
"github.com/yaoapp/yao/agent/context"
|
||||||
"github.com/yaoapp/yao/agent/output/message"
|
|
||||||
"github.com/yaoapp/yao/config"
|
"github.com/yaoapp/yao/config"
|
||||||
"github.com/yaoapp/yao/test"
|
"github.com/yaoapp/yao/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// newReleaseTestContext creates a test context for release testing
|
||||||
|
func newReleaseTestContext() *context.Context {
|
||||||
|
ctx := context.New(stdContext.Background(), nil, "test-chat-id")
|
||||||
|
ctx.AssistantID = "test-assistant-id"
|
||||||
|
ctx.Referer = context.RefererAPI
|
||||||
|
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
||||||
|
ctx.Stack = stack
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
// TestContextRelease tests explicit Release() method on Context
|
// TestContextRelease tests explicit Release() method on Context
|
||||||
func TestContextRelease(t *testing.T) {
|
func TestContextRelease(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
cxt := &context.Context{
|
cxt := newReleaseTestContext()
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
IDGenerator: message.NewIDGenerator(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize stack and trace
|
|
||||||
stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{})
|
|
||||||
cxt.Stack = stack
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
@ -71,17 +70,7 @@ func TestTraceRelease(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
cxt := &context.Context{
|
cxt := newReleaseTestContext()
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
IDGenerator: message.NewIDGenerator(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize stack and trace
|
|
||||||
stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{})
|
|
||||||
cxt.Stack = stack
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
@ -134,17 +123,7 @@ func TestContextReleaseWithTrace(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
cxt := &context.Context{
|
cxt := newReleaseTestContext()
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
IDGenerator: message.NewIDGenerator(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize stack and trace
|
|
||||||
stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{})
|
|
||||||
cxt.Stack = stack
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
@ -183,17 +162,7 @@ func TestTryFinallyPattern(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
cxt := &context.Context{
|
cxt := newReleaseTestContext()
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
IDGenerator: message.NewIDGenerator(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize stack and trace
|
|
||||||
stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{})
|
|
||||||
cxt.Stack = stack
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
@ -237,12 +206,8 @@ func TestNoOpTraceRelease(t *testing.T) {
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
// Context without trace initialization
|
// Context without trace initialization
|
||||||
cxt := &context.Context{
|
cxt := context.New(stdContext.Background(), nil, "test-chat-id")
|
||||||
ChatID: "test-chat-id",
|
cxt.AssistantID = "test-assistant-id"
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
IDGenerator: message.NewIDGenerator(),
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
@ -286,17 +251,7 @@ func TestTryFinallyPatternWithError(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
cxt := &context.Context{
|
cxt := newReleaseTestContext()
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
IDGenerator: message.NewIDGenerator(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize stack and trace
|
|
||||||
stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{})
|
|
||||||
cxt.Stack = stack
|
|
||||||
|
|
||||||
_, err := v8.Call(v8.CallOptions{}, `
|
_, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
|
||||||
|
|
@ -1,817 +0,0 @@
|
||||||
package context_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
stdContext "context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/yaoapp/gou/plan"
|
|
||||||
v8 "github.com/yaoapp/gou/runtime/v8"
|
|
||||||
"github.com/yaoapp/yao/agent/context"
|
|
||||||
"github.com/yaoapp/yao/config"
|
|
||||||
"github.com/yaoapp/yao/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestSpaceSetAndGet tests ctx.space.Set and ctx.space.Get
|
|
||||||
func TestSpaceSetAndGet(t *testing.T) {
|
|
||||||
test.Prepare(t, config.Conf)
|
|
||||||
defer test.Clean()
|
|
||||||
|
|
||||||
ctx := &context.Context{
|
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Space: plan.NewMemorySharedSpace(),
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
|
||||||
function test(ctx) {
|
|
||||||
try {
|
|
||||||
// Set various types of values
|
|
||||||
ctx.space.Set("string_key", "hello world");
|
|
||||||
ctx.space.Set("number_key", 42);
|
|
||||||
ctx.space.Set("boolean_key", true);
|
|
||||||
ctx.space.Set("object_key", { name: "test", value: 123 });
|
|
||||||
ctx.space.Set("array_key", [1, 2, 3, 4, 5]);
|
|
||||||
|
|
||||||
// Get values back
|
|
||||||
const str = ctx.space.Get("string_key");
|
|
||||||
const num = ctx.space.Get("number_key");
|
|
||||||
const bool = ctx.space.Get("boolean_key");
|
|
||||||
const obj = ctx.space.Get("object_key");
|
|
||||||
const arr = ctx.space.Get("array_key");
|
|
||||||
|
|
||||||
// Verify values
|
|
||||||
if (str !== "hello world") throw new Error("String mismatch");
|
|
||||||
if (num !== 42) throw new Error("Number mismatch");
|
|
||||||
if (bool !== true) throw new Error("Boolean mismatch");
|
|
||||||
if (obj.name !== "test" || obj.value !== 123) throw new Error("Object mismatch");
|
|
||||||
if (arr.length !== 5 || arr[0] !== 1) throw new Error("Array mismatch");
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
str: str,
|
|
||||||
num: num,
|
|
||||||
bool: bool,
|
|
||||||
obj: obj,
|
|
||||||
arr: arr
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}`, ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Call failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, ok := res.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected map result, got %T", res)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !result["success"].(bool) {
|
|
||||||
t.Fatalf("Test failed: %v", result["error"])
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, true, result["success"], "Space Set/Get should succeed")
|
|
||||||
assert.Equal(t, "hello world", result["str"], "String should match")
|
|
||||||
assert.Equal(t, float64(42), result["num"], "Number should match")
|
|
||||||
assert.Equal(t, true, result["bool"], "Boolean should match")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSpaceGetNonExistentKey tests getting a non-existent key
|
|
||||||
func TestSpaceGetNonExistentKey(t *testing.T) {
|
|
||||||
test.Prepare(t, config.Conf)
|
|
||||||
defer test.Clean()
|
|
||||||
|
|
||||||
ctx := &context.Context{
|
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Space: plan.NewMemorySharedSpace(),
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
|
||||||
function test(ctx) {
|
|
||||||
try {
|
|
||||||
// Get non-existent key should return null/undefined
|
|
||||||
const value = ctx.space.Get("non_existent_key");
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
value: value,
|
|
||||||
is_null: value === null,
|
|
||||||
is_undefined: value === undefined
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}`, ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Call failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, ok := res.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected map result, got %T", res)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, true, result["success"], "Get non-existent key should succeed")
|
|
||||||
// JavaScript null is returned as nil in Go
|
|
||||||
assert.True(t, result["is_null"].(bool) || result["is_undefined"].(bool), "Non-existent key should return null or undefined")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSpaceDelete tests ctx.space.Delete
|
|
||||||
func TestSpaceDelete(t *testing.T) {
|
|
||||||
test.Prepare(t, config.Conf)
|
|
||||||
defer test.Clean()
|
|
||||||
|
|
||||||
ctx := &context.Context{
|
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Space: plan.NewMemorySharedSpace(),
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
|
||||||
function test(ctx) {
|
|
||||||
try {
|
|
||||||
// Set a value
|
|
||||||
ctx.space.Set("delete_me", "temporary value");
|
|
||||||
|
|
||||||
// Verify it exists
|
|
||||||
const before = ctx.space.Get("delete_me");
|
|
||||||
if (before !== "temporary value") throw new Error("Value not set correctly");
|
|
||||||
|
|
||||||
// Delete it
|
|
||||||
ctx.space.Delete("delete_me");
|
|
||||||
|
|
||||||
// Verify it's gone
|
|
||||||
const after = ctx.space.Get("delete_me");
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
before: before,
|
|
||||||
after: after,
|
|
||||||
is_deleted: after === null || after === undefined
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}`, ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Call failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, ok := res.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected map result, got %T", res)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !result["success"].(bool) {
|
|
||||||
t.Fatalf("Test failed: %v", result["error"])
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, true, result["success"], "Space Delete should succeed")
|
|
||||||
assert.Equal(t, "temporary value", result["before"], "Value should exist before delete")
|
|
||||||
assert.Equal(t, true, result["is_deleted"], "Value should be deleted")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSpaceDeleteNonExistentKey tests deleting a non-existent key
|
|
||||||
func TestSpaceDeleteNonExistentKey(t *testing.T) {
|
|
||||||
test.Prepare(t, config.Conf)
|
|
||||||
defer test.Clean()
|
|
||||||
|
|
||||||
ctx := &context.Context{
|
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Space: plan.NewMemorySharedSpace(),
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
|
||||||
function test(ctx) {
|
|
||||||
try {
|
|
||||||
// Delete non-existent key should not throw error
|
|
||||||
ctx.space.Delete("non_existent_key");
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}`, ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Call failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, ok := res.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected map result, got %T", res)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, true, result["success"], "Delete non-existent key should not throw error")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSpaceWithNamespace tests using Space with namespace prefixes (like agent IDs)
|
|
||||||
func TestSpaceWithNamespace(t *testing.T) {
|
|
||||||
test.Prepare(t, config.Conf)
|
|
||||||
defer test.Clean()
|
|
||||||
|
|
||||||
ctx := &context.Context{
|
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Space: plan.NewMemorySharedSpace(),
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
|
||||||
function test(ctx) {
|
|
||||||
try {
|
|
||||||
// Simulate namespace pattern used in voucher assistant
|
|
||||||
const agentID = "workers.voucher";
|
|
||||||
|
|
||||||
// Set files_info with namespace
|
|
||||||
const filesInfo = [
|
|
||||||
{
|
|
||||||
file_id: "abc123",
|
|
||||||
filename: "test.png",
|
|
||||||
content_type: "image/png",
|
|
||||||
file_type: "image",
|
|
||||||
source: "uploader"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
ctx.space.Set(agentID + ":files_info", filesInfo);
|
|
||||||
|
|
||||||
// Set current_file with namespace
|
|
||||||
const currentFile = {
|
|
||||||
file_id: "abc123",
|
|
||||||
filename: "test.png",
|
|
||||||
content_type: "image/png"
|
|
||||||
};
|
|
||||||
ctx.space.Set(agentID + ":current_file", currentFile);
|
|
||||||
|
|
||||||
// Read back with namespace
|
|
||||||
const retrievedFiles = ctx.space.Get(agentID + ":files_info");
|
|
||||||
const retrievedCurrent = ctx.space.Get(agentID + ":current_file");
|
|
||||||
|
|
||||||
// Verify
|
|
||||||
if (!Array.isArray(retrievedFiles)) throw new Error("files_info should be array");
|
|
||||||
if (retrievedFiles.length !== 1) throw new Error("files_info length mismatch");
|
|
||||||
if (retrievedFiles[0].file_id !== "abc123") throw new Error("file_id mismatch");
|
|
||||||
if (retrievedCurrent.filename !== "test.png") throw new Error("filename mismatch");
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
files_count: retrievedFiles.length,
|
|
||||||
file_id: retrievedFiles[0].file_id,
|
|
||||||
current_filename: retrievedCurrent.filename
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}`, ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Call failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, ok := res.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected map result, got %T", res)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !result["success"].(bool) {
|
|
||||||
t.Fatalf("Test failed: %v", result["error"])
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, true, result["success"], "Namespace operations should succeed")
|
|
||||||
assert.Equal(t, float64(1), result["files_count"], "Should have 1 file")
|
|
||||||
assert.Equal(t, "abc123", result["file_id"], "File ID should match")
|
|
||||||
assert.Equal(t, "test.png", result["current_filename"], "Filename should match")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSpaceComplexData tests Space with complex nested data structures
|
|
||||||
func TestSpaceComplexData(t *testing.T) {
|
|
||||||
test.Prepare(t, config.Conf)
|
|
||||||
defer test.Clean()
|
|
||||||
|
|
||||||
ctx := &context.Context{
|
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Space: plan.NewMemorySharedSpace(),
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
|
||||||
function test(ctx) {
|
|
||||||
try {
|
|
||||||
// Complex nested structure
|
|
||||||
const complexData = {
|
|
||||||
metadata: {
|
|
||||||
assistant_id: "tests.vision-helper",
|
|
||||||
has_files_info: true,
|
|
||||||
files_count: 2
|
|
||||||
},
|
|
||||||
files_info: [
|
|
||||||
{
|
|
||||||
file_id: "file1",
|
|
||||||
filename: "image1.png",
|
|
||||||
content_type: "image/png",
|
|
||||||
metadata: {
|
|
||||||
size: 1024,
|
|
||||||
created: Date.now()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
file_id: "file2",
|
|
||||||
filename: "image2.jpg",
|
|
||||||
content_type: "image/jpeg",
|
|
||||||
metadata: {
|
|
||||||
size: 2048,
|
|
||||||
created: Date.now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
tags: ["vision", "test", "multi-file"]
|
|
||||||
};
|
|
||||||
|
|
||||||
ctx.space.Set("complex_data", complexData);
|
|
||||||
|
|
||||||
// Retrieve and verify
|
|
||||||
const retrieved = ctx.space.Get("complex_data");
|
|
||||||
|
|
||||||
if (!retrieved) throw new Error("Data not retrieved");
|
|
||||||
if (!retrieved.metadata) throw new Error("Metadata missing");
|
|
||||||
if (retrieved.metadata.files_count !== 2) throw new Error("Files count mismatch");
|
|
||||||
if (!Array.isArray(retrieved.files_info)) throw new Error("files_info not array");
|
|
||||||
if (retrieved.files_info.length !== 2) throw new Error("files_info length mismatch");
|
|
||||||
if (!Array.isArray(retrieved.tags)) throw new Error("tags not array");
|
|
||||||
if (retrieved.tags[0] !== "vision") throw new Error("tags mismatch");
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
files_count: retrieved.files_info.length,
|
|
||||||
first_file_id: retrieved.files_info[0].file_id,
|
|
||||||
second_filename: retrieved.files_info[1].filename,
|
|
||||||
tags: retrieved.tags
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}`, ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Call failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, ok := res.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected map result, got %T", res)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !result["success"].(bool) {
|
|
||||||
t.Fatalf("Test failed: %v", result["error"])
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, true, result["success"], "Complex data operations should succeed")
|
|
||||||
assert.Equal(t, float64(2), result["files_count"], "Should have 2 files")
|
|
||||||
assert.Equal(t, "file1", result["first_file_id"], "First file ID should match")
|
|
||||||
assert.Equal(t, "image2.jpg", result["second_filename"], "Second filename should match")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSpaceOverwrite tests overwriting existing values
|
|
||||||
func TestSpaceOverwrite(t *testing.T) {
|
|
||||||
test.Prepare(t, config.Conf)
|
|
||||||
defer test.Clean()
|
|
||||||
|
|
||||||
ctx := &context.Context{
|
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Space: plan.NewMemorySharedSpace(),
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
|
||||||
function test(ctx) {
|
|
||||||
try {
|
|
||||||
// Set initial value
|
|
||||||
ctx.space.Set("counter", 1);
|
|
||||||
const first = ctx.space.Get("counter");
|
|
||||||
|
|
||||||
// Overwrite with new value
|
|
||||||
ctx.space.Set("counter", 2);
|
|
||||||
const second = ctx.space.Get("counter");
|
|
||||||
|
|
||||||
// Overwrite again
|
|
||||||
ctx.space.Set("counter", 3);
|
|
||||||
const third = ctx.space.Get("counter");
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
first: first,
|
|
||||||
second: second,
|
|
||||||
third: third
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}`, ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Call failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, ok := res.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected map result, got %T", res)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !result["success"].(bool) {
|
|
||||||
t.Fatalf("Test failed: %v", result["error"])
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, true, result["success"], "Overwrite operations should succeed")
|
|
||||||
assert.Equal(t, float64(1), result["first"], "First value should be 1")
|
|
||||||
assert.Equal(t, float64(2), result["second"], "Second value should be 2")
|
|
||||||
assert.Equal(t, float64(3), result["third"], "Third value should be 3")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSpaceNoSpace tests behavior when Space is nil
|
|
||||||
func TestSpaceNoSpace(t *testing.T) {
|
|
||||||
test.Prepare(t, config.Conf)
|
|
||||||
defer test.Clean()
|
|
||||||
|
|
||||||
ctx := &context.Context{
|
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Space: nil, // No Space
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
|
||||||
function test(ctx) {
|
|
||||||
try {
|
|
||||||
// ctx.space should be undefined when Space is nil
|
|
||||||
const hasSpace = ctx.space !== undefined && ctx.space !== null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
has_space: hasSpace
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}`, ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Call failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, ok := res.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected map result, got %T", res)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, true, result["success"], "Should handle nil Space gracefully")
|
|
||||||
assert.Equal(t, false, result["has_space"], "Should not have space when Space is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSpaceErrorHandling tests error handling in Space methods
|
|
||||||
func TestSpaceErrorHandling(t *testing.T) {
|
|
||||||
test.Prepare(t, config.Conf)
|
|
||||||
defer test.Clean()
|
|
||||||
|
|
||||||
ctx := &context.Context{
|
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Space: plan.NewMemorySharedSpace(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test Set without key
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
|
||||||
function test(ctx) {
|
|
||||||
try {
|
|
||||||
// Set without proper arguments should throw
|
|
||||||
ctx.space.Set();
|
|
||||||
return { success: false, error: "Should have thrown" };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: true, caught_error: error.message };
|
|
||||||
}
|
|
||||||
}`, ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Call failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, ok := res.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected map result, got %T", res)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, true, result["success"], "Should catch Set error")
|
|
||||||
|
|
||||||
// Test Get without key
|
|
||||||
res, err = v8.Call(v8.CallOptions{}, `
|
|
||||||
function test(ctx) {
|
|
||||||
try {
|
|
||||||
// Get without key should throw
|
|
||||||
ctx.space.Get();
|
|
||||||
return { success: false, error: "Should have thrown" };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: true, caught_error: error.message };
|
|
||||||
}
|
|
||||||
}`, ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Call failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, ok = res.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected map result, got %T", res)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, true, result["success"], "Should catch Get error")
|
|
||||||
|
|
||||||
// Test Delete without key
|
|
||||||
res, err = v8.Call(v8.CallOptions{}, `
|
|
||||||
function test(ctx) {
|
|
||||||
try {
|
|
||||||
// Delete without key should throw
|
|
||||||
ctx.space.Delete();
|
|
||||||
return { success: false, error: "Should have thrown" };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: true, caught_error: error.message };
|
|
||||||
}
|
|
||||||
}`, ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Call failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, ok = res.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected map result, got %T", res)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, true, result["success"], "Should catch Delete error")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSpaceGetDel tests ctx.space.GetDel method
|
|
||||||
func TestSpaceGetDel(t *testing.T) {
|
|
||||||
test.Prepare(t, config.Conf)
|
|
||||||
defer test.Clean()
|
|
||||||
|
|
||||||
ctx := &context.Context{
|
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Space: plan.NewMemorySharedSpace(),
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
|
||||||
function test(ctx) {
|
|
||||||
try {
|
|
||||||
// Set a one-time use value
|
|
||||||
ctx.space.Set("one_time_token", "secret_token_12345");
|
|
||||||
|
|
||||||
// Verify it exists before GetDel
|
|
||||||
const before = ctx.space.Get("one_time_token");
|
|
||||||
if (before !== "secret_token_12345") throw new Error("Value not set");
|
|
||||||
|
|
||||||
// Use GetDel - should get value and delete automatically
|
|
||||||
const value = ctx.space.GetDel("one_time_token");
|
|
||||||
|
|
||||||
// Verify value was retrieved
|
|
||||||
if (value !== "secret_token_12345") throw new Error("GetDel returned wrong value");
|
|
||||||
|
|
||||||
// Verify key was deleted
|
|
||||||
const after = ctx.space.Get("one_time_token");
|
|
||||||
if (after !== null && after !== undefined) throw new Error("Key should be deleted after GetDel");
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
value: value,
|
|
||||||
is_deleted: after === null || after === undefined
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}`, ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Call failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, ok := res.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected map result, got %T", res)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !result["success"].(bool) {
|
|
||||||
t.Fatalf("Test failed: %v", result["error"])
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, true, result["success"], "GetDel should succeed")
|
|
||||||
assert.Equal(t, "secret_token_12345", result["value"], "GetDel should return correct value")
|
|
||||||
assert.Equal(t, true, result["is_deleted"], "Key should be deleted after GetDel")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSpaceGetDelNonExistentKey tests GetDel on non-existent key
|
|
||||||
func TestSpaceGetDelNonExistentKey(t *testing.T) {
|
|
||||||
test.Prepare(t, config.Conf)
|
|
||||||
defer test.Clean()
|
|
||||||
|
|
||||||
ctx := &context.Context{
|
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Space: plan.NewMemorySharedSpace(),
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
|
||||||
function test(ctx) {
|
|
||||||
try {
|
|
||||||
// GetDel on non-existent key should return null/undefined
|
|
||||||
const value = ctx.space.GetDel("non_existent_key");
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
value: value,
|
|
||||||
is_null: value === null || value === undefined
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}`, ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Call failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, ok := res.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected map result, got %T", res)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, true, result["success"], "GetDel on non-existent key should not throw")
|
|
||||||
assert.Equal(t, true, result["is_null"], "GetDel on non-existent key should return null")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSpaceGetDelComplexData tests GetDel with complex data structures
|
|
||||||
func TestSpaceGetDelComplexData(t *testing.T) {
|
|
||||||
test.Prepare(t, config.Conf)
|
|
||||||
defer test.Clean()
|
|
||||||
|
|
||||||
ctx := &context.Context{
|
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Space: plan.NewMemorySharedSpace(),
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
|
||||||
function test(ctx) {
|
|
||||||
try {
|
|
||||||
// Set complex file info data (like voucher assistant use case)
|
|
||||||
const filesInfo = [
|
|
||||||
{
|
|
||||||
file_id: "file123",
|
|
||||||
filename: "invoice.pdf",
|
|
||||||
content_type: "application/pdf",
|
|
||||||
file_type: "pdf",
|
|
||||||
source: "uploader",
|
|
||||||
uploader_name: "__yao.attachment"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
file_id: "file456",
|
|
||||||
filename: "receipt.png",
|
|
||||||
content_type: "image/png",
|
|
||||||
file_type: "image",
|
|
||||||
source: "uploader",
|
|
||||||
uploader_name: "__yao.attachment"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
ctx.space.Set("workers.voucher:files_info", filesInfo);
|
|
||||||
|
|
||||||
// Use GetDel to retrieve and clean up
|
|
||||||
const retrieved = ctx.space.GetDel("workers.voucher:files_info");
|
|
||||||
|
|
||||||
// Verify data integrity
|
|
||||||
if (!Array.isArray(retrieved)) throw new Error("Should be array");
|
|
||||||
if (retrieved.length !== 2) throw new Error("Length mismatch");
|
|
||||||
if (retrieved[0].file_id !== "file123") throw new Error("First file_id mismatch");
|
|
||||||
if (retrieved[1].filename !== "receipt.png") throw new Error("Second filename mismatch");
|
|
||||||
|
|
||||||
// Verify it's deleted
|
|
||||||
const after = ctx.space.Get("workers.voucher:files_info");
|
|
||||||
if (after !== null && after !== undefined) throw new Error("Should be deleted");
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
files_count: retrieved.length,
|
|
||||||
first_file_id: retrieved[0].file_id,
|
|
||||||
is_deleted: after === null || after === undefined
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}`, ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Call failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, ok := res.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected map result, got %T", res)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !result["success"].(bool) {
|
|
||||||
t.Fatalf("Test failed: %v", result["error"])
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, true, result["success"], "GetDel with complex data should succeed")
|
|
||||||
assert.Equal(t, float64(2), result["files_count"], "Should have 2 files")
|
|
||||||
assert.Equal(t, "file123", result["first_file_id"], "File ID should match")
|
|
||||||
assert.Equal(t, true, result["is_deleted"], "Should be deleted after GetDel")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSpaceGetDelMultipleCalls tests that GetDel only works once
|
|
||||||
func TestSpaceGetDelMultipleCalls(t *testing.T) {
|
|
||||||
test.Prepare(t, config.Conf)
|
|
||||||
defer test.Clean()
|
|
||||||
|
|
||||||
ctx := &context.Context{
|
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "en",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Space: plan.NewMemorySharedSpace(),
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
|
||||||
function test(ctx) {
|
|
||||||
try {
|
|
||||||
// Set a value
|
|
||||||
ctx.space.Set("single_use", "use_me_once");
|
|
||||||
|
|
||||||
// First GetDel should work
|
|
||||||
const first = ctx.space.GetDel("single_use");
|
|
||||||
if (first !== "use_me_once") throw new Error("First GetDel failed");
|
|
||||||
|
|
||||||
// Second GetDel should return null (already deleted)
|
|
||||||
const second = ctx.space.GetDel("single_use");
|
|
||||||
if (second !== null && second !== undefined) throw new Error("Second GetDel should return null");
|
|
||||||
|
|
||||||
// Third GetDel should also return null
|
|
||||||
const third = ctx.space.GetDel("single_use");
|
|
||||||
if (third !== null && third !== undefined) throw new Error("Third GetDel should return null");
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
first: first,
|
|
||||||
second_is_null: second === null || second === undefined,
|
|
||||||
third_is_null: third === null || third === undefined
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}`, ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Call failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, ok := res.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected map result, got %T", res)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !result["success"].(bool) {
|
|
||||||
t.Fatalf("Test failed: %v", result["error"])
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, true, result["success"], "Multiple GetDel calls should work correctly")
|
|
||||||
assert.Equal(t, "use_me_once", result["first"], "First GetDel should return value")
|
|
||||||
assert.Equal(t, true, result["second_is_null"], "Second GetDel should return null")
|
|
||||||
assert.Equal(t, true, result["third_is_null"], "Third GetDel should return null")
|
|
||||||
}
|
|
||||||
|
|
@ -12,11 +12,20 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
v8 "github.com/yaoapp/gou/runtime/v8"
|
v8 "github.com/yaoapp/gou/runtime/v8"
|
||||||
"github.com/yaoapp/yao/agent/context"
|
"github.com/yaoapp/yao/agent/context"
|
||||||
"github.com/yaoapp/yao/agent/output/message"
|
|
||||||
"github.com/yaoapp/yao/config"
|
"github.com/yaoapp/yao/config"
|
||||||
"github.com/yaoapp/yao/test"
|
"github.com/yaoapp/yao/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// newStressTestContext creates a test context for stress testing
|
||||||
|
func newStressTestContext(chatID string) *context.Context {
|
||||||
|
ctx := context.New(stdContext.Background(), nil, chatID)
|
||||||
|
ctx.AssistantID = "test-assistant"
|
||||||
|
ctx.Referer = context.RefererAPI
|
||||||
|
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
||||||
|
ctx.Stack = stack
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
// TestStressContextCreationAndRelease tests massive context creation and cleanup
|
// TestStressContextCreationAndRelease tests massive context creation and cleanup
|
||||||
func TestStressContextCreationAndRelease(t *testing.T) {
|
func TestStressContextCreationAndRelease(t *testing.T) {
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
|
|
@ -30,17 +39,7 @@ func TestStressContextCreationAndRelease(t *testing.T) {
|
||||||
startMemory := getMemStats()
|
startMemory := getMemStats()
|
||||||
|
|
||||||
for i := 0; i < iterations; i++ {
|
for i := 0; i < iterations; i++ {
|
||||||
cxt := &context.Context{
|
cxt := newStressTestContext(fmt.Sprintf("chat-%d", i))
|
||||||
ChatID: fmt.Sprintf("chat-%d", i),
|
|
||||||
AssistantID: "test-assistant",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
IDGenerator: message.NewIDGenerator(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize stack and trace
|
|
||||||
cxt.Referer = context.RefererAPI
|
|
||||||
stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{})
|
|
||||||
cxt.Stack = stack
|
|
||||||
|
|
||||||
_, err := v8.Call(v8.CallOptions{}, `
|
_, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
@ -107,17 +106,7 @@ func TestStressTraceOperations(t *testing.T) {
|
||||||
|
|
||||||
for i := 0; i < iterations; i++ {
|
for i := 0; i < iterations; i++ {
|
||||||
// Create new context for each iteration to avoid context cancellation issues
|
// Create new context for each iteration to avoid context cancellation issues
|
||||||
cxt := &context.Context{
|
cxt := newStressTestContext(fmt.Sprintf("stress-test-chat-%d", i))
|
||||||
ChatID: fmt.Sprintf("stress-test-chat-%d", i),
|
|
||||||
AssistantID: "test-assistant",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
IDGenerator: message.NewIDGenerator(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize stack and trace
|
|
||||||
stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{})
|
|
||||||
cxt.Stack = stack
|
|
||||||
_, err := v8.Call(v8.CallOptions{}, fmt.Sprintf(`
|
_, err := v8.Call(v8.CallOptions{}, fmt.Sprintf(`
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
const trace = ctx.trace
|
const trace = ctx.trace
|
||||||
|
|
@ -185,16 +174,7 @@ func TestStressMCPOperations(t *testing.T) {
|
||||||
|
|
||||||
iterations := 500
|
iterations := 500
|
||||||
|
|
||||||
cxt := &context.Context{
|
cxt := newStressTestContext("mcp-stress-test")
|
||||||
ChatID: "mcp-stress-test",
|
|
||||||
AssistantID: "test-assistant",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
IDGenerator: message.NewIDGenerator(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
|
|
||||||
stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{})
|
|
||||||
cxt.Stack = stack
|
|
||||||
|
|
||||||
startMemory := getMemStats()
|
startMemory := getMemStats()
|
||||||
|
|
||||||
|
|
@ -269,16 +249,7 @@ func TestStressConcurrentContexts(t *testing.T) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
for i := 0; i < iterationsPerGoroutine; i++ {
|
for i := 0; i < iterationsPerGoroutine; i++ {
|
||||||
cxt := &context.Context{
|
cxt := newStressTestContext(fmt.Sprintf("chat-%d-%d", goroutineID, i))
|
||||||
ChatID: fmt.Sprintf("chat-%d-%d", goroutineID, i),
|
|
||||||
AssistantID: "test-assistant",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
IDGenerator: message.NewIDGenerator(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
|
|
||||||
stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{})
|
|
||||||
cxt.Stack = stack
|
|
||||||
|
|
||||||
_, err := v8.Call(v8.CallOptions{}, `
|
_, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
@ -345,12 +316,8 @@ func TestStressNoOpTracePerformance(t *testing.T) {
|
||||||
iterations := 1000
|
iterations := 1000
|
||||||
|
|
||||||
// Context without trace initialization (no-op trace)
|
// Context without trace initialization (no-op trace)
|
||||||
cxt := &context.Context{
|
cxt := context.New(stdContext.Background(), nil, "noop-stress-test")
|
||||||
ChatID: "noop-stress-test",
|
cxt.AssistantID = "test-assistant"
|
||||||
AssistantID: "test-assistant",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
IDGenerator: message.NewIDGenerator(),
|
|
||||||
}
|
|
||||||
|
|
||||||
startMemory := getMemStats()
|
startMemory := getMemStats()
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
|
@ -420,16 +387,7 @@ func TestStressReleasePatterns(t *testing.T) {
|
||||||
startMemory := getMemStats()
|
startMemory := getMemStats()
|
||||||
|
|
||||||
for i := 0; i < iterations; i++ {
|
for i := 0; i < iterations; i++ {
|
||||||
cxt := &context.Context{
|
cxt := newStressTestContext(fmt.Sprintf("manual-%d", i))
|
||||||
ChatID: fmt.Sprintf("manual-%d", i),
|
|
||||||
AssistantID: "test-assistant",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
IDGenerator: message.NewIDGenerator(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
|
|
||||||
stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{})
|
|
||||||
cxt.Stack = stack
|
|
||||||
|
|
||||||
_, err := v8.Call(v8.CallOptions{}, `
|
_, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
@ -459,16 +417,7 @@ func TestStressReleasePatterns(t *testing.T) {
|
||||||
startMemory := getMemStats()
|
startMemory := getMemStats()
|
||||||
|
|
||||||
for i := 0; i < iterations; i++ {
|
for i := 0; i < iterations; i++ {
|
||||||
cxt := &context.Context{
|
cxt := newStressTestContext(fmt.Sprintf("gc-%d", i))
|
||||||
ChatID: fmt.Sprintf("gc-%d", i),
|
|
||||||
AssistantID: "test-assistant",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
IDGenerator: message.NewIDGenerator(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
|
|
||||||
stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{})
|
|
||||||
cxt.Stack = stack
|
|
||||||
|
|
||||||
_, err := v8.Call(v8.CallOptions{}, `
|
_, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
@ -500,16 +449,7 @@ func TestStressReleasePatterns(t *testing.T) {
|
||||||
startMemory := getMemStats()
|
startMemory := getMemStats()
|
||||||
|
|
||||||
for i := 0; i < iterations; i++ {
|
for i := 0; i < iterations; i++ {
|
||||||
cxt := &context.Context{
|
cxt := newStressTestContext(fmt.Sprintf("separate-%d", i))
|
||||||
ChatID: fmt.Sprintf("separate-%d", i),
|
|
||||||
AssistantID: "test-assistant",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
IDGenerator: message.NewIDGenerator(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
|
|
||||||
stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{})
|
|
||||||
cxt.Stack = stack
|
|
||||||
|
|
||||||
_, err := v8.Call(v8.CallOptions{}, `
|
_, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
|
|
@ -546,16 +486,7 @@ func TestStressLongRunningTrace(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
cxt := &context.Context{
|
cxt := newStressTestContext("long-running-test")
|
||||||
ChatID: "long-running-test",
|
|
||||||
AssistantID: "test-assistant",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
IDGenerator: message.NewIDGenerator(),
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
|
|
||||||
stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{})
|
|
||||||
cxt.Stack = stack
|
|
||||||
|
|
||||||
startMemory := getMemStats()
|
startMemory := getMemStats()
|
||||||
operations := 100
|
operations := 100
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import (
|
||||||
v8 "github.com/yaoapp/gou/runtime/v8"
|
v8 "github.com/yaoapp/gou/runtime/v8"
|
||||||
"github.com/yaoapp/gou/runtime/v8/bridge"
|
"github.com/yaoapp/gou/runtime/v8/bridge"
|
||||||
"github.com/yaoapp/yao/agent/context"
|
"github.com/yaoapp/yao/agent/context"
|
||||||
"github.com/yaoapp/yao/agent/output/message"
|
|
||||||
"github.com/yaoapp/yao/config"
|
"github.com/yaoapp/yao/config"
|
||||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
"github.com/yaoapp/yao/test"
|
"github.com/yaoapp/yao/test"
|
||||||
|
|
@ -23,12 +22,8 @@ func TestJsValue(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
cxt := &context.Context{
|
cxt := context.New(stdContext.Background(), nil, "ChatID-123456")
|
||||||
ChatID: "ChatID-123456",
|
cxt.AssistantID = "AssistantID-1234"
|
||||||
AssistantID: "AssistantID-1234",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
IDGenerator: message.NewIDGenerator(),
|
|
||||||
}
|
|
||||||
|
|
||||||
v8.RegisterFunction("testContextJsvalue", testContextJsvalueEmbed)
|
v8.RegisterFunction("testContextJsvalue", testContextJsvalueEmbed)
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
|
|
@ -91,12 +86,8 @@ func TestJsValueConcurrent(t *testing.T) {
|
||||||
chatID := fmt.Sprintf("ChatID-%d-%d", routineID, j)
|
chatID := fmt.Sprintf("ChatID-%d-%d", routineID, j)
|
||||||
assistantID := fmt.Sprintf("AssistantID-%d-%d", routineID, j)
|
assistantID := fmt.Sprintf("AssistantID-%d-%d", routineID, j)
|
||||||
|
|
||||||
cxt := &context.Context{
|
cxt := context.New(stdContext.Background(), nil, chatID)
|
||||||
ChatID: chatID,
|
cxt.AssistantID = assistantID
|
||||||
AssistantID: assistantID,
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
IDGenerator: message.NewIDGenerator(),
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(cxt) {
|
function test(cxt) {
|
||||||
|
|
@ -150,12 +141,8 @@ func TestJsValueRegistrationAndCleanup(t *testing.T) {
|
||||||
// Create multiple contexts and verify registration
|
// Create multiple contexts and verify registration
|
||||||
contextCount := 5
|
contextCount := 5
|
||||||
for i := 0; i < contextCount; i++ {
|
for i := 0; i < contextCount; i++ {
|
||||||
cxt := &context.Context{
|
cxt := context.New(stdContext.Background(), nil, fmt.Sprintf("ChatID-%d", i))
|
||||||
ChatID: fmt.Sprintf("ChatID-%d", i),
|
cxt.AssistantID = fmt.Sprintf("AssistantID-%d", i)
|
||||||
AssistantID: fmt.Sprintf("AssistantID-%d", i),
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
IDGenerator: message.NewIDGenerator(),
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := v8.Call(v8.CallOptions{}, `
|
_, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(cxt) {
|
function test(cxt) {
|
||||||
|
|
@ -219,26 +206,7 @@ func TestJsValueAllFields(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
cxt := &context.Context{
|
authInfo := &types.AuthorizedInfo{
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Locale: "zh-cn",
|
|
||||||
Theme: "dark",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
Client: context.Client{
|
|
||||||
Type: "web",
|
|
||||||
UserAgent: "Mozilla/5.0",
|
|
||||||
IP: "127.0.0.1",
|
|
||||||
},
|
|
||||||
Referer: "api",
|
|
||||||
Accept: "cui-web",
|
|
||||||
Route: "/dashboard/home",
|
|
||||||
Metadata: map[string]interface{}{
|
|
||||||
"key1": "value1",
|
|
||||||
"key2": 123,
|
|
||||||
"key3": true,
|
|
||||||
},
|
|
||||||
Authorized: &types.AuthorizedInfo{
|
|
||||||
Subject: "test-user",
|
Subject: "test-user",
|
||||||
ClientID: "test-client",
|
ClientID: "test-client",
|
||||||
UserID: "user-123",
|
UserID: "user-123",
|
||||||
|
|
@ -253,7 +221,24 @@ func TestJsValueAllFields(t *testing.T) {
|
||||||
"region": "us-west",
|
"region": "us-west",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
|
|
||||||
|
cxt := context.New(stdContext.Background(), authInfo, "test-chat-id")
|
||||||
|
cxt.AssistantID = "test-assistant-id"
|
||||||
|
cxt.Locale = "zh-cn"
|
||||||
|
cxt.Theme = "dark"
|
||||||
|
cxt.Client = context.Client{
|
||||||
|
Type: "web",
|
||||||
|
UserAgent: "Mozilla/5.0",
|
||||||
|
IP: "127.0.0.1",
|
||||||
|
}
|
||||||
|
cxt.Referer = "api"
|
||||||
|
cxt.Accept = "cui-web"
|
||||||
|
cxt.Route = "/dashboard/home"
|
||||||
|
cxt.Metadata = map[string]interface{}{
|
||||||
|
"key1": "value1",
|
||||||
|
"key2": 123,
|
||||||
|
"key3": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
v8.RegisterFunction("testAllFields", testAllFieldsEmbed)
|
v8.RegisterFunction("testAllFields", testAllFieldsEmbed)
|
||||||
|
|
@ -404,14 +389,10 @@ func TestJsValueTrace(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
cxt := &context.Context{
|
cxt := context.New(stdContext.Background(), nil, "test-chat-id")
|
||||||
ChatID: "test-chat-id",
|
cxt.AssistantID = "test-assistant-id"
|
||||||
AssistantID: "test-assistant-id",
|
cxt.Stack = &context.Stack{
|
||||||
Stack: &context.Stack{
|
|
||||||
TraceID: "test-trace-id",
|
TraceID: "test-trace-id",
|
||||||
},
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
IDGenerator: message.NewIDGenerator(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
|
|
@ -470,21 +451,17 @@ func TestJsValueAuthorizedAndMetadata(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
cxt := &context.Context{
|
authInfo := &types.AuthorizedInfo{
|
||||||
ChatID: "test-chat-id",
|
|
||||||
AssistantID: "test-assistant-id",
|
|
||||||
Context: stdContext.Background(),
|
|
||||||
IDGenerator: message.NewIDGenerator(),
|
|
||||||
Authorized: &types.AuthorizedInfo{
|
|
||||||
UserID: "user-123",
|
UserID: "user-123",
|
||||||
TenantID: "tenant-456",
|
TenantID: "tenant-456",
|
||||||
ClientID: "client-789",
|
ClientID: "client-789",
|
||||||
},
|
}
|
||||||
Metadata: map[string]interface{}{
|
cxt := context.New(stdContext.Background(), authInfo, "test-chat-id")
|
||||||
|
cxt.AssistantID = "test-assistant-id"
|
||||||
|
cxt.Metadata = map[string]interface{}{
|
||||||
"request_id": "req-001",
|
"request_id": "req-001",
|
||||||
"source": "api",
|
"source": "api",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
v8.RegisterFunction("testAuthorizedMetadata", testAuthorizedMetadataEmbed)
|
v8.RegisterFunction("testAuthorizedMetadata", testAuthorizedMetadataEmbed)
|
||||||
|
|
@ -573,14 +550,9 @@ func TestJsValueAuthorizedNil(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
cxt := &context.Context{
|
cxt := context.New(stdContext.Background(), nil, "test-chat-id")
|
||||||
ChatID: "test-chat-id",
|
cxt.AssistantID = "test-assistant-id"
|
||||||
AssistantID: "test-assistant-id",
|
cxt.Metadata = nil // Explicitly nil (should be empty object)
|
||||||
Context: stdContext.Background(),
|
|
||||||
IDGenerator: message.NewIDGenerator(),
|
|
||||||
Authorized: nil, // Explicitly nil
|
|
||||||
Metadata: nil, // Explicitly nil (should be empty object)
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(cxt) {
|
function test(cxt) {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/yaoapp/gou/mcp/types"
|
"github.com/yaoapp/gou/mcp/types"
|
||||||
"github.com/yaoapp/gou/plan"
|
|
||||||
"github.com/yaoapp/yao/agent/context"
|
"github.com/yaoapp/yao/agent/context"
|
||||||
"github.com/yaoapp/yao/config"
|
"github.com/yaoapp/yao/config"
|
||||||
"github.com/yaoapp/yao/test"
|
"github.com/yaoapp/yao/test"
|
||||||
|
|
@ -13,15 +12,10 @@ import (
|
||||||
|
|
||||||
// newTestMCPContext creates a test context
|
// newTestMCPContext creates a test context
|
||||||
func newTestMCPContext() *context.Context {
|
func newTestMCPContext() *context.Context {
|
||||||
ctx := &context.Context{
|
ctx := context.New(stdContext.Background(), nil, "test-chat")
|
||||||
Context: stdContext.Background(),
|
ctx.AssistantID = "test-assistant"
|
||||||
Space: plan.NewMemorySharedSpace(),
|
ctx.Locale = "en"
|
||||||
ID: "test-context",
|
ctx.Referer = context.RefererAPI
|
||||||
ChatID: "test-chat",
|
|
||||||
AssistantID: "test-assistant",
|
|
||||||
Locale: "en",
|
|
||||||
Referer: context.RefererAPI,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize stack and trace
|
// Initialize stack and trace
|
||||||
stack, traceID, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
stack, traceID, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/yaoapp/gou/connector/openai"
|
"github.com/yaoapp/gou/connector/openai"
|
||||||
"github.com/yaoapp/gou/plan"
|
|
||||||
"github.com/yaoapp/gou/store"
|
"github.com/yaoapp/gou/store"
|
||||||
|
"github.com/yaoapp/yao/agent/memory"
|
||||||
"github.com/yaoapp/yao/agent/output"
|
"github.com/yaoapp/yao/agent/output"
|
||||||
"github.com/yaoapp/yao/agent/output/message"
|
"github.com/yaoapp/yao/agent/output/message"
|
||||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
|
|
@ -226,7 +226,7 @@ type Context struct {
|
||||||
|
|
||||||
// External
|
// External
|
||||||
ID string `json:"id"` // Context ID for external interrupt identification
|
ID string `json:"id"` // Context ID for external interrupt identification
|
||||||
Space plan.Space `json:"-"` // Shared data space, it will be used to share data between the request and the call
|
Memory *memory.Memory `json:"-"` // Agent memory with four spaces: User, Team, Chat, Context
|
||||||
Cache store.Store `json:"-"` // Cache store, it will be used to store the message cache, default is "__yao.agent.cache"
|
Cache store.Store `json:"-"` // Cache store, it will be used to store the message cache, default is "__yao.agent.cache"
|
||||||
Stack *Stack `json:"-"` // Stack, current active stack of the request
|
Stack *Stack `json:"-"` // Stack, current active stack of the request
|
||||||
Stacks map[string]*Stack `json:"-"` // Stacks, all stacks in this request (for trace logging)
|
Stacks map[string]*Stack `json:"-"` // Stacks, all stacks in this request (for trace logging)
|
||||||
|
|
|
||||||
58
agent/memory/interfaces.go
Normal file
58
agent/memory/interfaces.go
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
package memory
|
||||||
|
|
||||||
|
import "github.com/yaoapp/gou/store"
|
||||||
|
|
||||||
|
// Manager defines the interface for managing agent memory
|
||||||
|
type Manager interface {
|
||||||
|
// Memory returns the memory instance for given identifiers
|
||||||
|
Memory(userID, teamID, chatID, contextID string) (*Memory, error)
|
||||||
|
|
||||||
|
// Close closes all stores and releases resources
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accessor defines the interface for accessing memory from agent context
|
||||||
|
// This is the primary interface used by agent hooks and tools
|
||||||
|
type Accessor interface {
|
||||||
|
// User returns the user-level memory namespace
|
||||||
|
User() NamespaceAccessor
|
||||||
|
|
||||||
|
// Team returns the team-level memory namespace
|
||||||
|
Team() NamespaceAccessor
|
||||||
|
|
||||||
|
// Chat returns the chat-level memory namespace
|
||||||
|
Chat() NamespaceAccessor
|
||||||
|
|
||||||
|
// Context returns the context-level memory namespace
|
||||||
|
Context() NamespaceAccessor
|
||||||
|
|
||||||
|
// Space returns a memory namespace by space type
|
||||||
|
Space(space Space) NamespaceAccessor
|
||||||
|
|
||||||
|
// Stats returns memory statistics
|
||||||
|
Stats() *Stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// NamespaceAccessor defines the interface for accessing a single memory namespace
|
||||||
|
// Embeds store.Store for all KV and list operations
|
||||||
|
type NamespaceAccessor interface {
|
||||||
|
store.Store
|
||||||
|
|
||||||
|
// GetID returns the namespace identifier (user_id, team_id, chat_id, or context_id)
|
||||||
|
GetID() string
|
||||||
|
|
||||||
|
// GetSpace returns the space type of this namespace
|
||||||
|
GetSpace() Space
|
||||||
|
|
||||||
|
// Stats returns statistics for this namespace
|
||||||
|
Stats() *NamespaceStats
|
||||||
|
}
|
||||||
|
|
||||||
|
// Factory defines the interface for creating memory instances
|
||||||
|
type Factory interface {
|
||||||
|
// Create creates a new memory instance with the given configuration
|
||||||
|
Create(config *Config) (Manager, error)
|
||||||
|
|
||||||
|
// CreateWithDefaults creates a new memory instance with default configuration
|
||||||
|
CreateWithDefaults() (Manager, error)
|
||||||
|
}
|
||||||
98
agent/memory/manager.go
Normal file
98
agent/memory/manager.go
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Global manager instance
|
||||||
|
var globalManager Manager
|
||||||
|
|
||||||
|
// Init initializes the global memory manager with the given configuration
|
||||||
|
// Called by agent.Load() after loading agent DSL
|
||||||
|
func Init(config *Config) {
|
||||||
|
globalManager = NewManager(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMemory returns a memory instance for the given identifiers using the global manager
|
||||||
|
// This is the main entry point for creating Memory instances from agent/context
|
||||||
|
func GetMemory(userID, teamID, chatID, contextID string) (*Memory, error) {
|
||||||
|
if globalManager == nil {
|
||||||
|
// Initialize with defaults if not configured
|
||||||
|
globalManager = NewManagerWithDefaults()
|
||||||
|
}
|
||||||
|
return globalManager.Memory(userID, teamID, chatID, contextID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the global manager and releases resources
|
||||||
|
func Close() error {
|
||||||
|
if globalManager != nil {
|
||||||
|
err := globalManager.Close()
|
||||||
|
globalManager = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultManager is the default memory manager implementation
|
||||||
|
type DefaultManager struct {
|
||||||
|
config *Config
|
||||||
|
memories sync.Map // map[string]*Memory, key is composite of userID:teamID:chatID:contextID
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManager creates a new memory manager with the given configuration
|
||||||
|
func NewManager(config *Config) Manager {
|
||||||
|
if config == nil {
|
||||||
|
config = &Config{}
|
||||||
|
}
|
||||||
|
return &DefaultManager{
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManagerWithDefaults creates a new memory manager with default configuration
|
||||||
|
func NewManagerWithDefaults() Manager {
|
||||||
|
return NewManager(&Config{
|
||||||
|
User: DefaultUserStore,
|
||||||
|
Team: DefaultTeamStore,
|
||||||
|
Chat: DefaultChatStore,
|
||||||
|
Context: DefaultContextStore,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// memoryKey generates a unique key for the memory instance
|
||||||
|
func memoryKey(userID, teamID, chatID, contextID string) string {
|
||||||
|
return userID + ":" + teamID + ":" + chatID + ":" + contextID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory returns the memory instance for given identifiers
|
||||||
|
func (m *DefaultManager) Memory(userID, teamID, chatID, contextID string) (*Memory, error) {
|
||||||
|
key := memoryKey(userID, teamID, chatID, contextID)
|
||||||
|
|
||||||
|
// Check if memory already exists
|
||||||
|
if val, ok := m.memories.Load(key); ok {
|
||||||
|
return val.(*Memory), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new memory instance
|
||||||
|
mem, err := New(m.config, userID, teamID, chatID, contextID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store and return (use LoadOrStore for thread safety)
|
||||||
|
actual, _ := m.memories.LoadOrStore(key, mem)
|
||||||
|
return actual.(*Memory), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes all stores and releases resources
|
||||||
|
func (m *DefaultManager) Close() error {
|
||||||
|
// Clear all cached memory instances
|
||||||
|
m.memories.Range(func(key, value interface{}) bool {
|
||||||
|
m.memories.Delete(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure DefaultManager implements Manager
|
||||||
|
var _ Manager = (*DefaultManager)(nil)
|
||||||
185
agent/memory/memory.go
Normal file
185
agent/memory/memory.go
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yaoapp/gou/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Default TTL values for each memory space
|
||||||
|
const (
|
||||||
|
DefaultUserTTL = 0 // No expiration for user-level memory
|
||||||
|
DefaultTeamTTL = 0 // No expiration for team-level memory
|
||||||
|
DefaultChatTTL = 24 * time.Hour // 24 hours for chat-level memory
|
||||||
|
DefaultContextTTL = 30 * time.Minute // 30 minutes for context-level memory
|
||||||
|
)
|
||||||
|
|
||||||
|
// New creates a new Memory instance with the given configuration and identifiers
|
||||||
|
func New(cfg *Config, userID, teamID, chatID, contextID string) (*Memory, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
cfg = &Config{}
|
||||||
|
}
|
||||||
|
|
||||||
|
m := &Memory{
|
||||||
|
UserID: userID,
|
||||||
|
TeamID: teamID,
|
||||||
|
ChatID: chatID,
|
||||||
|
ContextID: contextID,
|
||||||
|
Config: cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize user namespace
|
||||||
|
if userID != "" {
|
||||||
|
ns, err := newNamespace(SpaceUser, userID, cfg.User, DefaultUserTTL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create user namespace: %w", err)
|
||||||
|
}
|
||||||
|
m.User = ns
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize team namespace
|
||||||
|
if teamID != "" {
|
||||||
|
ns, err := newNamespace(SpaceTeam, teamID, cfg.Team, DefaultTeamTTL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create team namespace: %w", err)
|
||||||
|
}
|
||||||
|
m.Team = ns
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize chat namespace
|
||||||
|
if chatID != "" {
|
||||||
|
ns, err := newNamespace(SpaceChat, chatID, cfg.Chat, DefaultChatTTL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create chat namespace: %w", err)
|
||||||
|
}
|
||||||
|
m.Chat = ns
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize context namespace
|
||||||
|
if contextID != "" {
|
||||||
|
ns, err := newNamespace(SpaceContext, contextID, cfg.Context, DefaultContextTTL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create context namespace: %w", err)
|
||||||
|
}
|
||||||
|
m.Context = ns
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// newNamespace creates a new Namespace with the given parameters
|
||||||
|
func newNamespace(space Space, id, storeID string, defaultTTL time.Duration) (*Namespace, error) {
|
||||||
|
// Use default store ID if not specified
|
||||||
|
if storeID == "" {
|
||||||
|
switch space {
|
||||||
|
case SpaceUser:
|
||||||
|
storeID = DefaultUserStore
|
||||||
|
case SpaceTeam:
|
||||||
|
storeID = DefaultTeamStore
|
||||||
|
case SpaceChat:
|
||||||
|
storeID = DefaultChatStore
|
||||||
|
case SpaceContext:
|
||||||
|
storeID = DefaultContextStore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get store instance
|
||||||
|
s, err := store.Get(storeID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get store %s: %w", storeID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Namespace{
|
||||||
|
Space: space,
|
||||||
|
ID: id,
|
||||||
|
Store: s,
|
||||||
|
StoreID: storeID,
|
||||||
|
Prefix: fmt.Sprintf("%s:%s:", space, id),
|
||||||
|
Default: defaultTTL,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser returns the user-level memory namespace accessor
|
||||||
|
func (m *Memory) GetUser() NamespaceAccessor {
|
||||||
|
if m.User == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return m.User
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTeam returns the team-level memory namespace accessor
|
||||||
|
func (m *Memory) GetTeam() NamespaceAccessor {
|
||||||
|
if m.Team == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return m.Team
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChat returns the chat-level memory namespace accessor
|
||||||
|
func (m *Memory) GetChat() NamespaceAccessor {
|
||||||
|
if m.Chat == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return m.Chat
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetContext returns the context-level memory namespace accessor
|
||||||
|
func (m *Memory) GetContext() NamespaceAccessor {
|
||||||
|
if m.Context == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return m.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSpace returns a memory namespace by space type
|
||||||
|
func (m *Memory) GetSpace(space Space) NamespaceAccessor {
|
||||||
|
switch space {
|
||||||
|
case SpaceUser:
|
||||||
|
return m.GetUser()
|
||||||
|
case SpaceTeam:
|
||||||
|
return m.GetTeam()
|
||||||
|
case SpaceChat:
|
||||||
|
return m.GetChat()
|
||||||
|
case SpaceContext:
|
||||||
|
return m.GetContext()
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStats returns memory statistics for all namespaces
|
||||||
|
func (m *Memory) GetStats() *Stats {
|
||||||
|
stats := &Stats{}
|
||||||
|
|
||||||
|
if m.User != nil {
|
||||||
|
stats.User = m.User.Stats()
|
||||||
|
}
|
||||||
|
if m.Team != nil {
|
||||||
|
stats.Team = m.Team.Stats()
|
||||||
|
}
|
||||||
|
if m.Chat != nil {
|
||||||
|
stats.Chat = m.Chat.Stats()
|
||||||
|
}
|
||||||
|
if m.Context != nil {
|
||||||
|
stats.Context = m.Context.Stats()
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear clears all memory in all namespaces for this memory instance
|
||||||
|
func (m *Memory) Clear() {
|
||||||
|
if m.User != nil {
|
||||||
|
m.User.Clear()
|
||||||
|
}
|
||||||
|
if m.Team != nil {
|
||||||
|
m.Team.Clear()
|
||||||
|
}
|
||||||
|
if m.Chat != nil {
|
||||||
|
m.Chat.Clear()
|
||||||
|
}
|
||||||
|
if m.Context != nil {
|
||||||
|
m.Context.Clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
391
agent/memory/memory_test.go
Normal file
391
agent/memory/memory_test.go
Normal file
|
|
@ -0,0 +1,391 @@
|
||||||
|
package memory_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yaoapp/yao/agent/memory"
|
||||||
|
"github.com/yaoapp/yao/config"
|
||||||
|
"github.com/yaoapp/yao/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMemoryNew(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
// Create memory with default stores
|
||||||
|
mem, err := memory.New(nil, "user1", "team1", "chat1", "ctx1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, mem)
|
||||||
|
|
||||||
|
// Verify all namespaces are initialized
|
||||||
|
assert.NotNil(t, mem.User)
|
||||||
|
assert.NotNil(t, mem.Team)
|
||||||
|
assert.NotNil(t, mem.Chat)
|
||||||
|
assert.NotNil(t, mem.Context)
|
||||||
|
|
||||||
|
// Verify IDs
|
||||||
|
assert.Equal(t, "user1", mem.UserID)
|
||||||
|
assert.Equal(t, "team1", mem.TeamID)
|
||||||
|
assert.Equal(t, "chat1", mem.ChatID)
|
||||||
|
assert.Equal(t, "ctx1", mem.ContextID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryPartialIDs(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
// Create memory with only user and chat
|
||||||
|
mem, err := memory.New(nil, "user1", "", "chat1", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, mem)
|
||||||
|
|
||||||
|
// Only user and chat namespaces should be initialized
|
||||||
|
assert.NotNil(t, mem.User)
|
||||||
|
assert.Nil(t, mem.Team)
|
||||||
|
assert.NotNil(t, mem.Chat)
|
||||||
|
assert.Nil(t, mem.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamespaceBasicOperations(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
mem, err := memory.New(nil, "user1", "team1", "chat1", "ctx1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Test User namespace
|
||||||
|
t.Run("User namespace", func(t *testing.T) {
|
||||||
|
ns := mem.GetUser()
|
||||||
|
require.NotNil(t, ns)
|
||||||
|
|
||||||
|
// Set and Get
|
||||||
|
err := ns.Set("name", "John", 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
val, ok := ns.Get("name")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "John", val)
|
||||||
|
|
||||||
|
// Has
|
||||||
|
assert.True(t, ns.Has("name"))
|
||||||
|
assert.False(t, ns.Has("nonexistent"))
|
||||||
|
|
||||||
|
// Del
|
||||||
|
err = ns.Del("name")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, ns.Has("name"))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test Team namespace
|
||||||
|
t.Run("Team namespace", func(t *testing.T) {
|
||||||
|
ns := mem.GetTeam()
|
||||||
|
require.NotNil(t, ns)
|
||||||
|
|
||||||
|
err := ns.Set("setting", "value", 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
val, ok := ns.Get("setting")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "value", val)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test Chat namespace
|
||||||
|
t.Run("Chat namespace", func(t *testing.T) {
|
||||||
|
ns := mem.GetChat()
|
||||||
|
require.NotNil(t, ns)
|
||||||
|
|
||||||
|
err := ns.Set("topic", "AI", 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
val, ok := ns.Get("topic")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "AI", val)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test Context namespace
|
||||||
|
t.Run("Context namespace", func(t *testing.T) {
|
||||||
|
ns := mem.GetContext()
|
||||||
|
require.NotNil(t, ns)
|
||||||
|
|
||||||
|
err := ns.Set("temp", "data", 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
val, ok := ns.Get("temp")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "data", val)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamespaceIsolation(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
// Create two memory instances with different user IDs
|
||||||
|
mem1, err := memory.New(nil, "user1", "", "", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
mem2, err := memory.New(nil, "user2", "", "", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Set value in user1's namespace
|
||||||
|
err = mem1.GetUser().Set("key", "user1_value", 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Set value in user2's namespace
|
||||||
|
err = mem2.GetUser().Set("key", "user2_value", 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify isolation
|
||||||
|
val1, ok := mem1.GetUser().Get("key")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "user1_value", val1)
|
||||||
|
|
||||||
|
val2, ok := mem2.GetUser().Get("key")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "user2_value", val2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamespaceIncrDecr(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
mem, err := memory.New(nil, "user1", "", "", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ns := mem.GetUser()
|
||||||
|
|
||||||
|
// Incr on non-existent key
|
||||||
|
val, err := ns.Incr("counter", 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(1), val)
|
||||||
|
|
||||||
|
// Incr again
|
||||||
|
val, err = ns.Incr("counter", 5)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(6), val)
|
||||||
|
|
||||||
|
// Decr
|
||||||
|
val, err = ns.Decr("counter", 2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(4), val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamespaceListOperations(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
mem, err := memory.New(nil, "user1", "", "", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ns := mem.GetUser()
|
||||||
|
|
||||||
|
// Push values
|
||||||
|
err = ns.Push("list", "a", "b", "c")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// ArrayLen
|
||||||
|
assert.Equal(t, 3, ns.ArrayLen("list"))
|
||||||
|
|
||||||
|
// ArrayAll
|
||||||
|
all, err := ns.ArrayAll("list")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, all, 3)
|
||||||
|
|
||||||
|
// Pop from end
|
||||||
|
val, err := ns.Pop("list", 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "c", val)
|
||||||
|
|
||||||
|
// ArrayLen after pop
|
||||||
|
assert.Equal(t, 2, ns.ArrayLen("list"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamespaceSetOperations(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
mem, err := memory.New(nil, "user1", "", "", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ns := mem.GetUser()
|
||||||
|
|
||||||
|
// AddToSet
|
||||||
|
err = ns.AddToSet("tags", "go", "rust", "go") // "go" should only appear once
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
all, err := ns.ArrayAll("tags")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, all, 2) // Only "go" and "rust"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamespaceTTL(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
mem, err := memory.New(nil, "", "", "", "ctx1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ns := mem.GetContext()
|
||||||
|
|
||||||
|
// Set with short TTL
|
||||||
|
err = ns.Set("temp", "value", 100*time.Millisecond)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Should exist immediately
|
||||||
|
val, ok := ns.Get("temp")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "value", val)
|
||||||
|
|
||||||
|
// Wait for expiration
|
||||||
|
time.Sleep(150 * time.Millisecond)
|
||||||
|
|
||||||
|
// Should be expired
|
||||||
|
_, ok = ns.Get("temp")
|
||||||
|
assert.False(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryClear(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
mem, err := memory.New(nil, "user1", "team1", "chat1", "ctx1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Set values in all namespaces
|
||||||
|
mem.GetUser().Set("key", "user_value", 0)
|
||||||
|
mem.GetTeam().Set("key", "team_value", 0)
|
||||||
|
mem.GetChat().Set("key", "chat_value", 0)
|
||||||
|
mem.GetContext().Set("key", "ctx_value", 0)
|
||||||
|
|
||||||
|
// Clear all
|
||||||
|
mem.Clear()
|
||||||
|
|
||||||
|
// All should be empty
|
||||||
|
_, ok := mem.GetUser().Get("key")
|
||||||
|
assert.False(t, ok)
|
||||||
|
_, ok = mem.GetTeam().Get("key")
|
||||||
|
assert.False(t, ok)
|
||||||
|
_, ok = mem.GetChat().Get("key")
|
||||||
|
assert.False(t, ok)
|
||||||
|
_, ok = mem.GetContext().Get("key")
|
||||||
|
assert.False(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryStats(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
mem, err := memory.New(nil, "user1", "team1", "chat1", "ctx1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Set some values
|
||||||
|
mem.GetUser().Set("k1", "v1", 0)
|
||||||
|
mem.GetUser().Set("k2", "v2", 0)
|
||||||
|
mem.GetTeam().Set("k1", "v1", 0)
|
||||||
|
|
||||||
|
stats := mem.GetStats()
|
||||||
|
require.NotNil(t, stats)
|
||||||
|
|
||||||
|
assert.Equal(t, 2, stats.User.KeyCount)
|
||||||
|
assert.Equal(t, 1, stats.Team.KeyCount)
|
||||||
|
assert.Equal(t, 0, stats.Chat.KeyCount)
|
||||||
|
assert.Equal(t, 0, stats.Context.KeyCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManager(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
mgr := memory.NewManagerWithDefaults()
|
||||||
|
defer mgr.Close()
|
||||||
|
|
||||||
|
// Get memory instance
|
||||||
|
mem1, err := mgr.Memory("user1", "team1", "chat1", "ctx1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, mem1)
|
||||||
|
|
||||||
|
// Set a value
|
||||||
|
err = mem1.GetUser().Set("key", "value", 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Get same memory instance again
|
||||||
|
mem2, err := mgr.Memory("user1", "team1", "chat1", "ctx1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Should be the same instance (cached)
|
||||||
|
val, ok := mem2.GetUser().Get("key")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "value", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSpace(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
mem, err := memory.New(nil, "user1", "team1", "chat1", "ctx1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Test GetSpace
|
||||||
|
assert.NotNil(t, mem.GetSpace(memory.SpaceUser))
|
||||||
|
assert.NotNil(t, mem.GetSpace(memory.SpaceTeam))
|
||||||
|
assert.NotNil(t, mem.GetSpace(memory.SpaceChat))
|
||||||
|
assert.NotNil(t, mem.GetSpace(memory.SpaceContext))
|
||||||
|
|
||||||
|
// Invalid space
|
||||||
|
assert.Nil(t, mem.GetSpace(memory.Space("invalid")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamespaceGetMultiSetMulti(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
mem, err := memory.New(nil, "user1", "", "", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ns := mem.GetUser()
|
||||||
|
|
||||||
|
// SetMulti
|
||||||
|
ns.SetMulti(map[string]interface{}{
|
||||||
|
"a": 1,
|
||||||
|
"b": 2,
|
||||||
|
"c": 3,
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
// GetMulti
|
||||||
|
result := ns.GetMulti([]string{"a", "b", "c"})
|
||||||
|
assert.Equal(t, 1, result["a"])
|
||||||
|
assert.Equal(t, 2, result["b"])
|
||||||
|
assert.Equal(t, 3, result["c"])
|
||||||
|
|
||||||
|
// DelMulti
|
||||||
|
ns.DelMulti([]string{"a", "b"})
|
||||||
|
assert.False(t, ns.Has("a"))
|
||||||
|
assert.False(t, ns.Has("b"))
|
||||||
|
assert.True(t, ns.Has("c"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamespaceGetDel(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
mem, err := memory.New(nil, "user1", "", "", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ns := mem.GetUser()
|
||||||
|
|
||||||
|
// Set a value
|
||||||
|
err = ns.Set("key", "value", 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// GetDel
|
||||||
|
val, ok := ns.GetDel("key")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "value", val)
|
||||||
|
|
||||||
|
// Should be deleted
|
||||||
|
_, ok = ns.Get("key")
|
||||||
|
assert.False(t, ok)
|
||||||
|
}
|
||||||
238
agent/memory/namespace.go
Normal file
238
agent/memory/namespace.go
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure Namespace implements NamespaceAccessor
|
||||||
|
var _ NamespaceAccessor = (*Namespace)(nil)
|
||||||
|
|
||||||
|
// GetID returns the namespace identifier
|
||||||
|
func (ns *Namespace) GetID() string {
|
||||||
|
return ns.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSpace returns the space type of this namespace
|
||||||
|
func (ns *Namespace) GetSpace() Space {
|
||||||
|
return ns.Space
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefixKey adds the namespace prefix to a key
|
||||||
|
func (ns *Namespace) prefixKey(key string) string {
|
||||||
|
return ns.Prefix + key
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a value by key
|
||||||
|
func (ns *Namespace) Get(key string) (interface{}, bool) {
|
||||||
|
return ns.Store.Get(ns.prefixKey(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set stores a value with the default TTL for this namespace
|
||||||
|
func (ns *Namespace) Set(key string, value interface{}, ttl time.Duration) error {
|
||||||
|
if ttl == 0 {
|
||||||
|
ttl = ns.Default
|
||||||
|
}
|
||||||
|
return ns.Store.Set(ns.prefixKey(key), value, ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has checks if a key exists
|
||||||
|
func (ns *Namespace) Has(key string) bool {
|
||||||
|
return ns.Store.Has(ns.prefixKey(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Del deletes a key (supports wildcards)
|
||||||
|
func (ns *Namespace) Del(key string) error {
|
||||||
|
return ns.Store.Del(ns.prefixKey(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys returns all keys in this namespace
|
||||||
|
func (ns *Namespace) Keys() []string {
|
||||||
|
allKeys := ns.Store.Keys()
|
||||||
|
prefixLen := len(ns.Prefix)
|
||||||
|
|
||||||
|
// Filter keys that belong to this namespace
|
||||||
|
var result []string
|
||||||
|
for _, key := range allKeys {
|
||||||
|
if len(key) >= prefixLen && key[:prefixLen] == ns.Prefix {
|
||||||
|
result = append(result, key[prefixLen:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of keys in this namespace
|
||||||
|
func (ns *Namespace) Len() int {
|
||||||
|
return len(ns.Keys())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear deletes all keys in this namespace
|
||||||
|
func (ns *Namespace) Clear() {
|
||||||
|
ns.Store.Del(ns.Prefix + "*")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSet retrieves a value and sets a new value if not exists
|
||||||
|
func (ns *Namespace) GetSet(key string, ttl time.Duration, getValue func(key string) (interface{}, error)) (interface{}, error) {
|
||||||
|
if ttl == 0 {
|
||||||
|
ttl = ns.Default
|
||||||
|
}
|
||||||
|
return ns.Store.GetSet(ns.prefixKey(key), ttl, getValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDel retrieves a value and deletes it atomically
|
||||||
|
func (ns *Namespace) GetDel(key string) (interface{}, bool) {
|
||||||
|
return ns.Store.GetDel(ns.prefixKey(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMulti retrieves multiple values by keys
|
||||||
|
func (ns *Namespace) GetMulti(keys []string) map[string]interface{} {
|
||||||
|
prefixedKeys := make([]string, len(keys))
|
||||||
|
for i, key := range keys {
|
||||||
|
prefixedKeys[i] = ns.prefixKey(key)
|
||||||
|
}
|
||||||
|
result := ns.Store.GetMulti(prefixedKeys)
|
||||||
|
|
||||||
|
// Remove prefix from result keys
|
||||||
|
unprefixed := make(map[string]interface{})
|
||||||
|
prefixLen := len(ns.Prefix)
|
||||||
|
for k, v := range result {
|
||||||
|
if len(k) > prefixLen {
|
||||||
|
unprefixed[k[prefixLen:]] = v
|
||||||
|
} else {
|
||||||
|
unprefixed[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unprefixed
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMulti stores multiple values
|
||||||
|
func (ns *Namespace) SetMulti(values map[string]interface{}, ttl time.Duration) {
|
||||||
|
if ttl == 0 {
|
||||||
|
ttl = ns.Default
|
||||||
|
}
|
||||||
|
prefixed := make(map[string]interface{})
|
||||||
|
for k, v := range values {
|
||||||
|
prefixed[ns.prefixKey(k)] = v
|
||||||
|
}
|
||||||
|
ns.Store.SetMulti(prefixed, ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DelMulti deletes multiple keys
|
||||||
|
func (ns *Namespace) DelMulti(keys []string) {
|
||||||
|
prefixedKeys := make([]string, len(keys))
|
||||||
|
for i, key := range keys {
|
||||||
|
prefixedKeys[i] = ns.prefixKey(key)
|
||||||
|
}
|
||||||
|
ns.Store.DelMulti(prefixedKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSetMulti retrieves multiple values and sets new values if not exists
|
||||||
|
func (ns *Namespace) GetSetMulti(keys []string, ttl time.Duration, getValue func(key string) (interface{}, error)) map[string]interface{} {
|
||||||
|
if ttl == 0 {
|
||||||
|
ttl = ns.Default
|
||||||
|
}
|
||||||
|
prefixedKeys := make([]string, len(keys))
|
||||||
|
for i, key := range keys {
|
||||||
|
prefixedKeys[i] = ns.prefixKey(key)
|
||||||
|
}
|
||||||
|
result := ns.Store.GetSetMulti(prefixedKeys, ttl, getValue)
|
||||||
|
|
||||||
|
// Remove prefix from result keys
|
||||||
|
unprefixed := make(map[string]interface{})
|
||||||
|
prefixLen := len(ns.Prefix)
|
||||||
|
for k, v := range result {
|
||||||
|
if len(k) > prefixLen {
|
||||||
|
unprefixed[k[prefixLen:]] = v
|
||||||
|
} else {
|
||||||
|
unprefixed[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unprefixed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Incr increments a numeric value
|
||||||
|
func (ns *Namespace) Incr(key string, delta int64) (int64, error) {
|
||||||
|
return ns.Store.Incr(ns.prefixKey(key), delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decr decrements a numeric value
|
||||||
|
func (ns *Namespace) Decr(key string, delta int64) (int64, error) {
|
||||||
|
return ns.Store.Decr(ns.prefixKey(key), delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push appends values to a list
|
||||||
|
func (ns *Namespace) Push(key string, values ...interface{}) error {
|
||||||
|
return ns.Store.Push(ns.prefixKey(key), values...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop removes and returns an element from a list
|
||||||
|
func (ns *Namespace) Pop(key string, position int) (interface{}, error) {
|
||||||
|
return ns.Store.Pop(ns.prefixKey(key), position)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull removes the first occurrence of a value from a list
|
||||||
|
func (ns *Namespace) Pull(key string, value interface{}) error {
|
||||||
|
return ns.Store.Pull(ns.prefixKey(key), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PullAll removes all occurrences of values from a list
|
||||||
|
func (ns *Namespace) PullAll(key string, values []interface{}) error {
|
||||||
|
return ns.Store.PullAll(ns.prefixKey(key), values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddToSet adds values to a set (no duplicates)
|
||||||
|
func (ns *Namespace) AddToSet(key string, values ...interface{}) error {
|
||||||
|
return ns.Store.AddToSet(ns.prefixKey(key), values...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArrayLen returns the length of a list
|
||||||
|
func (ns *Namespace) ArrayLen(key string) int {
|
||||||
|
return ns.Store.ArrayLen(ns.prefixKey(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArrayGet retrieves an element from a list by index
|
||||||
|
func (ns *Namespace) ArrayGet(key string, index int) (interface{}, error) {
|
||||||
|
return ns.Store.ArrayGet(ns.prefixKey(key), index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArraySet sets an element in a list by index
|
||||||
|
func (ns *Namespace) ArraySet(key string, index int, value interface{}) error {
|
||||||
|
return ns.Store.ArraySet(ns.prefixKey(key), index, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArraySlice returns a slice of a list
|
||||||
|
func (ns *Namespace) ArraySlice(key string, skip, limit int) ([]interface{}, error) {
|
||||||
|
return ns.Store.ArraySlice(ns.prefixKey(key), skip, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArrayPage returns a page of a list
|
||||||
|
func (ns *Namespace) ArrayPage(key string, page, pageSize int) ([]interface{}, error) {
|
||||||
|
return ns.Store.ArrayPage(ns.prefixKey(key), page, pageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArrayAll returns all elements of a list
|
||||||
|
func (ns *Namespace) ArrayAll(key string) ([]interface{}, error) {
|
||||||
|
return ns.Store.ArrayAll(ns.prefixKey(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats returns statistics for this namespace
|
||||||
|
func (ns *Namespace) Stats() *NamespaceStats {
|
||||||
|
return &NamespaceStats{
|
||||||
|
Space: ns.Space,
|
||||||
|
ID: ns.ID,
|
||||||
|
KeyCount: ns.Len(),
|
||||||
|
StoreID: ns.StoreID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot returns all key-value pairs in this namespace
|
||||||
|
// Used for recovery/resume functionality
|
||||||
|
func (ns *Namespace) Snapshot() map[string]interface{} {
|
||||||
|
keys := ns.Keys()
|
||||||
|
snapshot := make(map[string]interface{}, len(keys))
|
||||||
|
for _, key := range keys {
|
||||||
|
if value, ok := ns.Get(key); ok {
|
||||||
|
snapshot[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return snapshot
|
||||||
|
}
|
||||||
99
agent/memory/types.go
Normal file
99
agent/memory/types.go
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yaoapp/gou/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Space defines the memory space type
|
||||||
|
type Space string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SpaceUser user-level memory, persists across all chats for a user
|
||||||
|
// Use case: user preferences, long-term knowledge, personal settings
|
||||||
|
SpaceUser Space = "user"
|
||||||
|
|
||||||
|
// SpaceTeam team-level memory, shared across all users in a team
|
||||||
|
// Use case: team knowledge, shared settings, collaborative data
|
||||||
|
SpaceTeam Space = "team"
|
||||||
|
|
||||||
|
// SpaceChat chat-level memory, persists within a single chat session
|
||||||
|
// Use case: conversation context, chat-specific settings, accumulated knowledge
|
||||||
|
SpaceChat Space = "chat"
|
||||||
|
|
||||||
|
// SpaceContext context-level memory, temporary within a single request context
|
||||||
|
// Use case: intermediate results, temporary variables, request-scoped cache
|
||||||
|
SpaceContext Space = "context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config represents the memory configuration
|
||||||
|
// Each field is a Store ID referencing gou/store, empty string uses built-in default
|
||||||
|
// All spaces use xun-based storage by default for persistence and reliability
|
||||||
|
type Config struct {
|
||||||
|
User string `json:"user,omitempty" yaml:"user,omitempty"` // Store ID for user-level memory (default: xun-based)
|
||||||
|
Team string `json:"team,omitempty" yaml:"team,omitempty"` // Store ID for team-level memory (default: xun-based)
|
||||||
|
Chat string `json:"chat,omitempty" yaml:"chat,omitempty"` // Store ID for chat-level memory (default: xun-based)
|
||||||
|
Context string `json:"context,omitempty" yaml:"context,omitempty"` // Store ID for context-level memory (default: xun-based, shorter TTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultStoreID constants for built-in stores
|
||||||
|
const (
|
||||||
|
DefaultUserStore = "__yao.agent.memory.user"
|
||||||
|
DefaultTeamStore = "__yao.agent.memory.team"
|
||||||
|
DefaultChatStore = "__yao.agent.memory.chat"
|
||||||
|
DefaultContextStore = "__yao.agent.memory.context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Entry represents a memory entry
|
||||||
|
type Entry struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value interface{} `json:"value"`
|
||||||
|
Space Space `json:"space"`
|
||||||
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||||
|
TTL time.Duration `json:"ttl,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Namespace represents a memory namespace for a specific space
|
||||||
|
type Namespace struct {
|
||||||
|
Space Space `json:"space"`
|
||||||
|
ID string `json:"id"` // UserID, TeamID, ChatID, or ContextID depending on space
|
||||||
|
Store store.Store `json:"-"` // Underlying store
|
||||||
|
StoreID string `json:"-"` // Store ID
|
||||||
|
Prefix string `json:"-"` // Computed key prefix (e.g., "user:123:", "team:456:")
|
||||||
|
Default time.Duration `json:"-"` // Default TTL for this namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory represents the complete memory system for an agent
|
||||||
|
// It manages four separate namespaces: User, Team, Chat, and Context
|
||||||
|
type Memory struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
TeamID string `json:"team_id"`
|
||||||
|
ChatID string `json:"chat_id"`
|
||||||
|
ContextID string `json:"context_id"`
|
||||||
|
|
||||||
|
User *Namespace `json:"-"` // User-level memory namespace
|
||||||
|
Team *Namespace `json:"-"` // Team-level memory namespace
|
||||||
|
Chat *Namespace `json:"-"` // Chat-level memory namespace
|
||||||
|
Context *Namespace `json:"-"` // Context-level memory namespace
|
||||||
|
Config *Config `json:"-"` // Memory configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats represents memory statistics
|
||||||
|
type Stats struct {
|
||||||
|
User *NamespaceStats `json:"user,omitempty"`
|
||||||
|
Team *NamespaceStats `json:"team,omitempty"`
|
||||||
|
Chat *NamespaceStats `json:"chat,omitempty"`
|
||||||
|
Context *NamespaceStats `json:"context,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NamespaceStats represents statistics for a single memory namespace
|
||||||
|
type NamespaceStats struct {
|
||||||
|
Space Space `json:"space"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
KeyCount int `json:"key_count"`
|
||||||
|
StoreID string `json:"store_id"`
|
||||||
|
}
|
||||||
|
|
@ -312,7 +312,7 @@ func TestSetJSAPIFactory(t *testing.T) {
|
||||||
require.NotNil(t, context.SearchAPIFactory)
|
require.NotNil(t, context.SearchAPIFactory)
|
||||||
|
|
||||||
// Create a mock context
|
// Create a mock context
|
||||||
ctx := &context.Context{}
|
ctx := context.New(nil, nil, "test-chat")
|
||||||
|
|
||||||
// Get search API
|
// Get search API
|
||||||
searchAPI := context.SearchAPIFactory(ctx)
|
searchAPI := context.SearchAPIFactory(ctx)
|
||||||
|
|
@ -337,7 +337,8 @@ func TestSetJSAPIFactory_WithGetter(t *testing.T) {
|
||||||
require.NotNil(t, context.SearchAPIFactory)
|
require.NotNil(t, context.SearchAPIFactory)
|
||||||
|
|
||||||
// Create a context with assistant ID
|
// Create a context with assistant ID
|
||||||
ctx := &context.Context{AssistantID: "test-assistant"}
|
ctx := context.New(nil, nil, "test-chat")
|
||||||
|
ctx.AssistantID = "test-assistant"
|
||||||
|
|
||||||
// Get search API
|
// Get search API
|
||||||
searchAPI := context.SearchAPIFactory(ctx)
|
searchAPI := context.SearchAPIFactory(ctx)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/yaoapp/gou/plan"
|
|
||||||
agentContext "github.com/yaoapp/yao/agent/context"
|
agentContext "github.com/yaoapp/yao/agent/context"
|
||||||
"github.com/yaoapp/yao/config"
|
"github.com/yaoapp/yao/config"
|
||||||
"github.com/yaoapp/yao/test"
|
"github.com/yaoapp/yao/test"
|
||||||
|
|
@ -14,15 +13,10 @@ import (
|
||||||
|
|
||||||
// newTestContext creates a test context for MCP testing
|
// newTestContext creates a test context for MCP testing
|
||||||
func newTestContext() *agentContext.Context {
|
func newTestContext() *agentContext.Context {
|
||||||
ctx := &agentContext.Context{
|
ctx := agentContext.New(stdContext.Background(), nil, "test-chat")
|
||||||
Context: stdContext.Background(),
|
ctx.AssistantID = "test-assistant"
|
||||||
Space: plan.NewMemorySharedSpace(),
|
ctx.Locale = "en"
|
||||||
ID: "test-querydsl",
|
ctx.Referer = agentContext.RefererAPI
|
||||||
ChatID: "test-chat",
|
|
||||||
AssistantID: "test-assistant",
|
|
||||||
Locale: "en",
|
|
||||||
Referer: agentContext.RefererAPI,
|
|
||||||
}
|
|
||||||
stack, _, _ := agentContext.EnterStack(ctx, "test-assistant", &agentContext.Options{})
|
stack, _, _ := agentContext.EnterStack(ctx, "test-assistant", &agentContext.Options{})
|
||||||
ctx.Stack = stack
|
ctx.Stack = stack
|
||||||
return ctx
|
return ctx
|
||||||
|
|
|
||||||
413
data/bindata.go
413
data/bindata.go
File diff suppressed because it is too large
Load diff
|
|
@ -19,8 +19,11 @@ var systemStores = map[string]string{
|
||||||
"__yao.oauth.store": "yao/stores/oauth/store.xun.yao", // for OAuth data store
|
"__yao.oauth.store": "yao/stores/oauth/store.xun.yao", // for OAuth data store
|
||||||
"__yao.oauth.cache": "yao/stores/oauth/cache.lru.yao", // for OAuth cache store
|
"__yao.oauth.cache": "yao/stores/oauth/cache.lru.yao", // for OAuth cache store
|
||||||
"__yao.oauth.client": "yao/stores/oauth/client.xun.yao", // for OAuth client store
|
"__yao.oauth.client": "yao/stores/oauth/client.xun.yao", // for OAuth client store
|
||||||
"__yao.agent.memory": "yao/stores/agent/memory.xun.yao", // for agent memory store (for agent memory)
|
"__yao.agent.memory.user": "yao/stores/agent/memory/user.xun.yao", // for agent user-level memory
|
||||||
"__yao.agent.cache": "yao/stores/agent/cache.lru.yao", // for agent cache store (for agent cache)
|
"__yao.agent.memory.team": "yao/stores/agent/memory/team.xun.yao", // for agent team-level memory
|
||||||
|
"__yao.agent.memory.chat": "yao/stores/agent/memory/chat.xun.yao", // for agent chat-level memory
|
||||||
|
"__yao.agent.memory.context": "yao/stores/agent/memory/context.xun.yao", // for agent context-level memory
|
||||||
|
"__yao.agent.cache": "yao/stores/agent/cache.lru.yao", // for agent cache store
|
||||||
"__yao.kb.store": "yao/stores/kb/store.xun.yao", // for knowledge base store
|
"__yao.kb.store": "yao/stores/kb/store.xun.yao", // for knowledge base store
|
||||||
"__yao.kb.cache": "yao/stores/kb/cache.lru.yao", // for knowledge base cache store
|
"__yao.kb.cache": "yao/stores/kb/cache.lru.yao", // for knowledge base cache store
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,10 @@ func check(t *testing.T) {
|
||||||
assert.True(t, ids["__yao.oauth.store"])
|
assert.True(t, ids["__yao.oauth.store"])
|
||||||
assert.True(t, ids["__yao.oauth.client"])
|
assert.True(t, ids["__yao.oauth.client"])
|
||||||
assert.True(t, ids["__yao.oauth.cache"])
|
assert.True(t, ids["__yao.oauth.cache"])
|
||||||
assert.True(t, ids["__yao.agent.memory"])
|
assert.True(t, ids["__yao.agent.memory.user"])
|
||||||
|
assert.True(t, ids["__yao.agent.memory.team"])
|
||||||
|
assert.True(t, ids["__yao.agent.memory.chat"])
|
||||||
|
assert.True(t, ids["__yao.agent.memory.context"])
|
||||||
assert.True(t, ids["__yao.agent.cache"])
|
assert.True(t, ids["__yao.agent.cache"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -225,7 +225,10 @@ var testSystemStores = map[string]string{
|
||||||
"__yao.oauth.store": "yao/stores/oauth/store.xun.yao",
|
"__yao.oauth.store": "yao/stores/oauth/store.xun.yao",
|
||||||
"__yao.oauth.client": "yao/stores/oauth/client.xun.yao",
|
"__yao.oauth.client": "yao/stores/oauth/client.xun.yao",
|
||||||
"__yao.oauth.cache": "yao/stores/oauth/cache.lru.yao",
|
"__yao.oauth.cache": "yao/stores/oauth/cache.lru.yao",
|
||||||
"__yao.agent.memory": "yao/stores/agent/memory.xun.yao",
|
"__yao.agent.memory.user": "yao/stores/agent/memory/user.xun.yao",
|
||||||
|
"__yao.agent.memory.team": "yao/stores/agent/memory/team.xun.yao",
|
||||||
|
"__yao.agent.memory.chat": "yao/stores/agent/memory/chat.xun.yao",
|
||||||
|
"__yao.agent.memory.context": "yao/stores/agent/memory/context.xun.yao",
|
||||||
"__yao.agent.cache": "yao/stores/agent/cache.lru.yao",
|
"__yao.agent.cache": "yao/stores/agent/cache.lru.yao",
|
||||||
"__yao.kb.store": "yao/stores/kb/store.xun.yao",
|
"__yao.kb.store": "yao/stores/kb/store.xun.yao",
|
||||||
"__yao.kb.cache": "yao/stores/kb/cache.lru.yao",
|
"__yao.kb.cache": "yao/stores/kb/cache.lru.yao",
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"label": "Agent Memory Store",
|
|
||||||
"description": "Database-backed store for agent memory persistence and long-term storage",
|
|
||||||
"tags": ["agent", "memory", "xun", "ai"],
|
|
||||||
"readonly": false,
|
|
||||||
"builtin": true,
|
|
||||||
"sort": 20,
|
|
||||||
"name": "Agent Memory Store",
|
|
||||||
"type": "xun",
|
|
||||||
"option": {
|
|
||||||
"type": "xun",
|
|
||||||
"table": "__yao_agent_memory",
|
|
||||||
"connector": "default"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
18
yao/stores/agent/memory/chat.xun.yao
Normal file
18
yao/stores/agent/memory/chat.xun.yao
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"label": "Agent Memory - Chat",
|
||||||
|
"description": "Chat-level memory store for conversation context, chat-specific settings, and accumulated knowledge",
|
||||||
|
"tags": ["agent", "memory", "chat", "xun"],
|
||||||
|
"readonly": false,
|
||||||
|
"builtin": true,
|
||||||
|
"sort": 22,
|
||||||
|
"name": "Agent Memory Chat Store",
|
||||||
|
"type": "xun",
|
||||||
|
"option": {
|
||||||
|
"type": "xun",
|
||||||
|
"table": "__yao_agent_memory_chat",
|
||||||
|
"connector": "default",
|
||||||
|
"cache_size": 10240,
|
||||||
|
"persist_interval": 30,
|
||||||
|
"cleanup_interval": 60
|
||||||
|
}
|
||||||
|
}
|
||||||
18
yao/stores/agent/memory/context.xun.yao
Normal file
18
yao/stores/agent/memory/context.xun.yao
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"label": "Agent Memory - Context",
|
||||||
|
"description": "Context-level memory store for intermediate results, temporary variables, and request-scoped cache",
|
||||||
|
"tags": ["agent", "memory", "context", "xun"],
|
||||||
|
"readonly": false,
|
||||||
|
"builtin": true,
|
||||||
|
"sort": 23,
|
||||||
|
"name": "Agent Memory Context Store",
|
||||||
|
"type": "xun",
|
||||||
|
"option": {
|
||||||
|
"type": "xun",
|
||||||
|
"table": "__yao_agent_memory_context",
|
||||||
|
"connector": "default",
|
||||||
|
"cache_size": 10240,
|
||||||
|
"persist_interval": 10,
|
||||||
|
"cleanup_interval": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
18
yao/stores/agent/memory/team.xun.yao
Normal file
18
yao/stores/agent/memory/team.xun.yao
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"label": "Agent Memory - Team",
|
||||||
|
"description": "Team-level memory store for team knowledge, shared settings, and collaborative data",
|
||||||
|
"tags": ["agent", "memory", "team", "xun"],
|
||||||
|
"readonly": false,
|
||||||
|
"builtin": true,
|
||||||
|
"sort": 21,
|
||||||
|
"name": "Agent Memory Team Store",
|
||||||
|
"type": "xun",
|
||||||
|
"option": {
|
||||||
|
"type": "xun",
|
||||||
|
"table": "__yao_agent_memory_team",
|
||||||
|
"connector": "default",
|
||||||
|
"cache_size": 10240,
|
||||||
|
"persist_interval": 60,
|
||||||
|
"cleanup_interval": 1440
|
||||||
|
}
|
||||||
|
}
|
||||||
19
yao/stores/agent/memory/user.xun.yao
Normal file
19
yao/stores/agent/memory/user.xun.yao
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"label": "Agent Memory - User",
|
||||||
|
"description": "User-level memory store for user preferences, long-term knowledge, and personal settings",
|
||||||
|
"tags": ["agent", "memory", "user", "xun"],
|
||||||
|
"readonly": false,
|
||||||
|
"builtin": true,
|
||||||
|
"sort": 20,
|
||||||
|
"name": "Agent Memory User Store",
|
||||||
|
"type": "xun",
|
||||||
|
"option": {
|
||||||
|
"type": "xun",
|
||||||
|
"table": "__yao_agent_memory_user",
|
||||||
|
"connector": "default",
|
||||||
|
"cache_size": 10240,
|
||||||
|
"persist_interval": 60,
|
||||||
|
"cleanup_interval": 1440
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Reference in a new issue