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
|
||||
// Should be called when space data changes
|
||||
// UpdateSpaceSnapshot updates the context memory snapshot in the buffer
|
||||
// Only captures Context-level memory (request-scoped temporary data) for recovery
|
||||
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
|
||||
}
|
||||
|
||||
snapshot := ctx.Space.Snapshot()
|
||||
snapshot := ctx.Memory.Context.Snapshot()
|
||||
ctx.Buffer.SetSpaceSnapshot(snapshot)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import (
|
|||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yaoapp/gou/plan"
|
||||
"github.com/yaoapp/yao/agent/assistant"
|
||||
agentcontext "github.com/yaoapp/yao/agent/context"
|
||||
storetypes "github.com/yaoapp/yao/agent/store/types"
|
||||
|
|
@ -431,15 +430,16 @@ func TestBufferStepTracking(t *testing.T) {
|
|||
|
||||
t.Run("BeginAndCompleteStep", func(t *testing.T) {
|
||||
ctx := agentcontext.New(context.Background(), nil, "test_chat_step_001")
|
||||
ctx.Space = plan.NewMemorySharedSpace()
|
||||
|
||||
// Enter stack and init buffer
|
||||
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
||||
defer done()
|
||||
ast.InitBuffer(ctx)
|
||||
|
||||
// Set some space data
|
||||
ctx.Space.Set("test_key", "test_value")
|
||||
// Set some context memory data
|
||||
if ctx.Memory != nil && ctx.Memory.Context != nil {
|
||||
ctx.Memory.Context.Set("test_key", "test_value", 0)
|
||||
}
|
||||
|
||||
// Begin a step
|
||||
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.Run("SpaceSnapshotCapture", func(t *testing.T) {
|
||||
ctx := agentcontext.New(context.Background(), nil, "test_chat_space_001")
|
||||
ctx.Space = plan.NewMemorySharedSpace()
|
||||
t.Run("ContextMemorySnapshotCapture", func(t *testing.T) {
|
||||
ctx := agentcontext.New(context.Background(), nil, "test_chat_memory_001")
|
||||
|
||||
// Enter stack and init buffer
|
||||
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
||||
defer done()
|
||||
ast.InitBuffer(ctx)
|
||||
|
||||
// Set space data before step
|
||||
ctx.Space.Set("key1", "value1")
|
||||
ctx.Space.Set("key2", 123)
|
||||
// Set context memory data before step
|
||||
require.NotNil(t, ctx.Memory)
|
||||
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)
|
||||
|
||||
// Verify space snapshot was captured
|
||||
// Verify context memory snapshot was captured
|
||||
steps := ctx.Buffer.GetAllSteps()
|
||||
require.Len(t, steps, 1)
|
||||
assert.NotNil(t, steps[0].SpaceSnapshot)
|
||||
assert.Equal(t, "value1", steps[0].SpaceSnapshot["key1"])
|
||||
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) {
|
||||
ctx := agentcontext.New(context.Background(), nil, "test_chat_multi_step")
|
||||
ctx.Space = plan.NewMemorySharedSpace()
|
||||
|
||||
// Enter stack and init buffer
|
||||
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
||||
|
|
@ -536,7 +536,6 @@ func TestFlushBuffer(t *testing.T) {
|
|||
t.Run("FlushOnSuccess", func(t *testing.T) {
|
||||
chatID := fmt.Sprintf("test_flush_success_%s", uuid.New().String()[:8])
|
||||
ctx := agentcontext.New(context.Background(), nil, chatID)
|
||||
ctx.Space = plan.NewMemorySharedSpace()
|
||||
|
||||
// Enter stack and init buffer
|
||||
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
||||
|
|
@ -584,7 +583,6 @@ func TestFlushBuffer(t *testing.T) {
|
|||
t.Run("FlushOnFailure", func(t *testing.T) {
|
||||
chatID := fmt.Sprintf("test_flush_fail_%s", uuid.New().String()[:8])
|
||||
ctx := agentcontext.New(context.Background(), nil, chatID)
|
||||
ctx.Space = plan.NewMemorySharedSpace()
|
||||
|
||||
// Enter stack and init buffer
|
||||
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
||||
|
|
@ -633,7 +631,6 @@ func TestFlushBuffer(t *testing.T) {
|
|||
t.Run("FlushOnInterrupt", func(t *testing.T) {
|
||||
chatID := fmt.Sprintf("test_flush_interrupt_%s", uuid.New().String()[:8])
|
||||
ctx := agentcontext.New(context.Background(), nil, chatID)
|
||||
ctx.Space = plan.NewMemorySharedSpace()
|
||||
|
||||
// Enter stack and init buffer
|
||||
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
||||
|
|
@ -673,7 +670,6 @@ func TestFlushBuffer(t *testing.T) {
|
|||
t.Run("FlushWithModeAndConnector", func(t *testing.T) {
|
||||
chatID := fmt.Sprintf("test_flush_mode_%s", uuid.New().String()[:8])
|
||||
ctx := agentcontext.New(context.Background(), nil, chatID)
|
||||
ctx.Space = plan.NewMemorySharedSpace()
|
||||
|
||||
// Enter stack with connector and mode options
|
||||
opts := &agentcontext.Options{
|
||||
|
|
|
|||
|
|
@ -262,7 +262,7 @@ func TestDBAuthWheresFilter(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)
|
||||
|
||||
assert.Nil(t, wheres, "Nil Authorized should return nil")
|
||||
|
|
@ -419,16 +419,15 @@ func TestKBSearchIntegration(t *testing.T) {
|
|||
// ========== Helper Functions ==========
|
||||
|
||||
func createAuthContext(userID, teamID string, teamOnly, ownerOnly bool) *agentContext.Context {
|
||||
return &agentContext.Context{
|
||||
Authorized: &oauthtypes.AuthorizedInfo{
|
||||
UserID: userID,
|
||||
TeamID: teamID,
|
||||
Constraints: oauthtypes.DataConstraints{
|
||||
TeamOnly: teamOnly,
|
||||
OwnerOnly: ownerOnly,
|
||||
},
|
||||
authorized := &oauthtypes.AuthorizedInfo{
|
||||
UserID: userID,
|
||||
TeamID: teamID,
|
||||
Constraints: oauthtypes.DataConstraints{
|
||||
TeamOnly: teamOnly,
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -457,8 +457,8 @@ func TestVision_CachedContent(t *testing.T) {
|
|||
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
|
||||
func TestVision_FileMetadataInSpace(t *testing.T) {
|
||||
// TestVision_FileMetadataInMemory tests that file metadata is correctly passed to vision agent via ctx.Memory.Context
|
||||
func TestVision_FileMetadataInMemory(t *testing.T) {
|
||||
testutils.Prepare(t)
|
||||
defer testutils.Clean(t)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/yaoapp/gou/connector/openai"
|
||||
"github.com/yaoapp/gou/plan"
|
||||
agentContext "github.com/yaoapp/yao/agent/context"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"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
|
||||
func newTestContext(capabilities *openai.Capabilities) *agentContext.Context {
|
||||
return &agentContext.Context{
|
||||
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",
|
||||
ClientID: "test-client-id",
|
||||
UserID: "test-user-123",
|
||||
TeamID: "test-team-456",
|
||||
TenantID: "test-tenant-789",
|
||||
},
|
||||
authorized := &types.AuthorizedInfo{
|
||||
Subject: "test-user",
|
||||
ClientID: "test-client-id",
|
||||
UserID: "test-user-123",
|
||||
TeamID: "test-team-456",
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -49,15 +49,15 @@ func CallAgent(ctx *agentContext.Context, agentID string, message agentContext.M
|
|||
}
|
||||
|
||||
// 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)
|
||||
// Uses Space instead of Metadata to avoid creating context copies and ensure proper cleanup
|
||||
// The file metadata is passed via ctx.Memory.Context for access by hooks (especially Next hook)
|
||||
// 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}:current_file - Currently processing file (single object)
|
||||
func CallAgentWithFileInfo(ctx *agentContext.Context, agentID string, message agentContext.Message, info *Info) (string, error) {
|
||||
// Store file information in Space if available
|
||||
if info != nil && ctx.Space != nil {
|
||||
// Store file information in Memory.Context if available
|
||||
if info != nil && ctx.Memory != nil && ctx.Memory.Context != nil {
|
||||
fileInfo := map[string]interface{}{
|
||||
"url": info.URL,
|
||||
"filename": info.Filename,
|
||||
|
|
@ -74,14 +74,14 @@ func CallAgentWithFileInfo(ctx *agentContext.Context, agentID string, message ag
|
|||
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"
|
||||
currentFileKey := agentID + ":current_file"
|
||||
|
||||
// Thread-safe: append current file to files list
|
||||
fileInfoMutex.Lock()
|
||||
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{}
|
||||
if existingList, ok := existing.([]interface{}); ok {
|
||||
for _, item := range existingList {
|
||||
|
|
@ -95,23 +95,23 @@ func CallAgentWithFileInfo(ctx *agentContext.Context, agentID string, message ag
|
|||
}
|
||||
// Append current file to list
|
||||
filesList = append(filesList, fileInfo)
|
||||
ctx.Space.Set(filesListKey, filesList)
|
||||
ctx.Memory.Context.Set(filesListKey, filesList, 0)
|
||||
fileInfoMutex.Unlock()
|
||||
|
||||
// Store current file in Space
|
||||
if err := ctx.Space.Set(currentFileKey, fileInfo); err != nil {
|
||||
log.Trace("[Content] Failed to set current file info in Space: %v", err)
|
||||
// Store current file in Memory.Context
|
||||
if err := ctx.Memory.Context.Set(currentFileKey, fileInfo, 0); err != nil {
|
||||
log.Trace("[Content] Failed to set current file info in Memory.Context: %v", err)
|
||||
}
|
||||
|
||||
// Ensure cleanup after agent call completes
|
||||
defer func() {
|
||||
// Clean up current file
|
||||
if err := ctx.Space.Delete(currentFileKey); err != nil {
|
||||
log.Trace("[Content] Failed to delete current file info from Space: %v", err)
|
||||
if err := ctx.Memory.Context.Del(currentFileKey); err != nil {
|
||||
log.Trace("[Content] Failed to delete current file info from Memory.Context: %v", err)
|
||||
}
|
||||
// Clean up files list (reset for next call)
|
||||
if err := ctx.Space.Delete(filesListKey); err != nil {
|
||||
log.Trace("[Content] Failed to delete files list from Space: %v", err)
|
||||
if err := ctx.Memory.Context.Del(filesListKey); err != nil {
|
||||
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)
|
||||
|
||||
// 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
|
||||
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.
|
||||
|
||||
> **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)`
|
||||
|
||||
|
|
@ -1248,138 +1248,372 @@ Retrieves a trace space by 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 |
|
||||
| ----------------- | ------------------------------------- |
|
||||
| `Get(key)` | Get a value from the space |
|
||||
| `Set(key, value)` | Set a value in the space |
|
||||
| `Delete(key)` | Delete a key from the space |
|
||||
| `GetDel(key)` | Get a value and immediately delete it |
|
||||
| Namespace | Scope | Persistence | Use Case |
|
||||
| -------------------- | ------------------- | ----------- | ------------------------------------------- |
|
||||
| `ctx.memory.user` | Per user | Persistent | User preferences, settings, long-term state |
|
||||
| `ctx.memory.team` | Per team | Persistent | Team-wide settings, shared configurations |
|
||||
| `ctx.memory.chat` | Per chat session | Persistent | Chat-specific context, conversation state |
|
||||
| `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:**
|
||||
|
||||
```javascript
|
||||
const user_data = ctx.space.Get("user_data");
|
||||
if (user_data) {
|
||||
console.log("Found user:", user_data.name);
|
||||
// Metadata
|
||||
id: string; // Namespace ID
|
||||
space: string; // Space type: "user", "team", "chat", or "context"
|
||||
}
|
||||
```
|
||||
|
||||
#### `ctx.space.Set(key, value): void`
|
||||
### Basic KV Operations
|
||||
|
||||
Sets a value in the space.
|
||||
#### `Get(key): any`
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `key`: String - The key to set
|
||||
- `value`: Any - The value to store
|
||||
|
||||
**Example:**
|
||||
Gets a value from the namespace.
|
||||
|
||||
```javascript
|
||||
ctx.space.Set("user_data", { name: "John", id: 123 });
|
||||
ctx.space.Set("processing_status", "started");
|
||||
// User preferences
|
||||
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.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `key`: String - The key to delete
|
||||
|
||||
**Example:**
|
||||
Sets a value with optional TTL (time-to-live in seconds).
|
||||
|
||||
```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.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `key`: String - The key to retrieve and delete
|
||||
|
||||
**Returns:**
|
||||
|
||||
- `any`: The value, or `null` if not found
|
||||
|
||||
**Example:**
|
||||
Deletes a key. Supports wildcard patterns with `*`.
|
||||
|
||||
```javascript
|
||||
// Store file metadata in parent agent
|
||||
ctx.space.Set("file_metadata", { name: "report.pdf", size: 1024 });
|
||||
// Delete single key
|
||||
ctx.memory.user.Del("old_setting");
|
||||
|
||||
// In child agent, get and consume the data
|
||||
const metadata = ctx.space.GetDel("file_metadata");
|
||||
// metadata is now deleted from space
|
||||
// Delete with wildcard pattern
|
||||
ctx.memory.chat.Del("cache:*"); // Deletes all keys starting with "cache:"
|
||||
```
|
||||
|
||||
#### `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
|
||||
|
||||
```javascript
|
||||
// Use case 1: Pass data between hooks
|
||||
// Use case 1: User preferences (persistent across sessions)
|
||||
function Create(ctx, messages) {
|
||||
// Store data for later use
|
||||
ctx.space.Set("original_query", messages[0].content);
|
||||
// Load user preferences
|
||||
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 };
|
||||
}
|
||||
|
||||
function Next(ctx, payload) {
|
||||
// Retrieve data from Create hook
|
||||
const query = ctx.space.Get("original_query");
|
||||
console.log("Original query was:", query);
|
||||
// Retrieve temporary data
|
||||
const startTime = ctx.memory.context.Get("request_start");
|
||||
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) {
|
||||
// Prepare context for child agent
|
||||
ctx.space.Set("parent_context", {
|
||||
user_id: ctx.authorized.user_id,
|
||||
session_start: Date.now(),
|
||||
});
|
||||
// Check team quota
|
||||
const used = ctx.memory.team.Incr("monthly_requests");
|
||||
const limit = ctx.memory.team.Get("monthly_limit") || 10000;
|
||||
|
||||
// Call child agent...
|
||||
if (used > limit) {
|
||||
throw new Error("Team quota exceeded");
|
||||
}
|
||||
|
||||
return { messages };
|
||||
}
|
||||
|
||||
// Use case 3: One-time data consumption
|
||||
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:**
|
||||
|
||||
- Space is shared across all hooks within the same request
|
||||
- Space persists across nested agent calls (A2A)
|
||||
- Values can be any JSON-serializable data
|
||||
- Use `GetDel` for data that should only be consumed once
|
||||
- `user`, `team`, `chat` namespaces are persistent (backed by database)
|
||||
- `context` namespace is temporary and cleared when the request context is released
|
||||
- All namespaces support TTL for automatic expiration
|
||||
- Wildcard deletion (`Del("prefix:*")`) works on all namespaces
|
||||
- Counter operations (`Incr`, `Decr`) are atomic
|
||||
|
||||
## MCP API
|
||||
|
||||
|
|
@ -1697,7 +1931,7 @@ interface UsesConfig {
|
|||
```javascript
|
||||
function Create(ctx, messages) {
|
||||
// Store data for Next hook
|
||||
ctx.space.Set("user_query", messages[0]?.content);
|
||||
ctx.memory.context.Set("user_query", messages[0]?.content);
|
||||
|
||||
// Modify messages
|
||||
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.
|
||||
- **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
|
||||
- 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
|
||||
|
||||
|
|
@ -1891,9 +2125,9 @@ function Create(ctx, messages) {
|
|||
// Extract user query from the last message
|
||||
const user_query = messages[messages.length - 1]?.content || "";
|
||||
|
||||
// Store data in space for use in Next hook
|
||||
ctx.space.Set("original_query", user_query);
|
||||
ctx.space.Set("request_time", Date.now());
|
||||
// Store data in context memory for use in Next hook
|
||||
ctx.memory.context.Set("original_query", user_query);
|
||||
ctx.memory.context.Set("request_time", Date.now());
|
||||
|
||||
// Add trace node to show processing in UI
|
||||
const create_node = ctx.trace.Add(
|
||||
|
|
@ -1943,9 +2177,9 @@ function Create(ctx, messages) {
|
|||
function Next(ctx, payload) {
|
||||
const { messages, completion, tools, error } = payload;
|
||||
|
||||
// Retrieve data from Create hook via space
|
||||
const original_query = ctx.space.Get("original_query");
|
||||
const request_time = ctx.space.Get("request_time");
|
||||
// Retrieve data from Create hook via context memory
|
||||
const original_query = ctx.memory.context.Get("original_query");
|
||||
const request_time = ctx.memory.context.Get("request_time");
|
||||
const duration = Date.now() - request_time;
|
||||
|
||||
// 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)
|
||||
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
|
||||
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
|
||||
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"
|
||||
"time"
|
||||
|
||||
"github.com/yaoapp/gou/plan"
|
||||
"github.com/yaoapp/yao/agent/memory"
|
||||
"github.com/yaoapp/yao/agent/output/message"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||
|
|
@ -27,11 +27,21 @@ func New(parent context.Context, authorized *types.AuthorizedInfo, chatID string
|
|||
|
||||
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{
|
||||
Context: parent,
|
||||
ID: contextID, // Generate unique ID for the context
|
||||
Authorized: authorized, // Set authorized info
|
||||
Space: plan.NewMemorySharedSpace(),
|
||||
Memory: mem,
|
||||
ChatID: chatID,
|
||||
IDGenerator: message.NewIDGenerator(), // Initialize ID generator for this context
|
||||
messageMetadata: newMessageMetadataStore(), // Initialize message metadata store
|
||||
|
|
@ -78,14 +88,15 @@ func (ctx *Context) Release() {
|
|||
ctx.trace = nil
|
||||
}
|
||||
|
||||
// Clear space
|
||||
if ctx.Space != nil {
|
||||
// Clear context-level memory only (request-scoped temporary data)
|
||||
// User, Team, Chat level memory is persistent and should NOT be cleared
|
||||
if ctx.Memory != nil && ctx.Memory.Context != nil {
|
||||
if ctx.Logger != nil {
|
||||
ctx.Logger.Cleanup("Space")
|
||||
ctx.Logger.Cleanup("Memory.Context")
|
||||
}
|
||||
ctx.Space.Clear()
|
||||
ctx.Space = nil
|
||||
ctx.Memory.Context.Clear()
|
||||
}
|
||||
ctx.Memory = nil
|
||||
|
||||
// Clear stacks
|
||||
if ctx.Stacks != nil {
|
||||
|
|
@ -379,9 +390,9 @@ func (ctx *Context) BeginStep(stepType string, input map[string]interface{}) *Bu
|
|||
return nil
|
||||
}
|
||||
|
||||
// Update space snapshot before starting step
|
||||
if ctx.Space != nil {
|
||||
ctx.Buffer.SetSpaceSnapshot(ctx.Space.Snapshot())
|
||||
// Update context memory snapshot before starting step (for recovery)
|
||||
if ctx.Memory != nil && ctx.Memory.Context != nil {
|
||||
ctx.Buffer.SetSpaceSnapshot(ctx.Memory.Context.Snapshot())
|
||||
}
|
||||
|
||||
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.expectedAccept, ctx.Accept)
|
||||
assert.Equal(t, tt.expectedAssistantID, ctx.AssistantID)
|
||||
assert.NotNil(t, ctx.Space)
|
||||
assert.NotNil(t, ctx.Memory)
|
||||
assert.NotNil(t, ctx.Cache)
|
||||
})
|
||||
}
|
||||
|
|
@ -227,7 +227,7 @@ func TestContextNew_WithAuthorized(t *testing.T) {
|
|||
|
||||
assert.NotNil(t, ctx)
|
||||
assert.Equal(t, "test-chat-id", ctx.ChatID)
|
||||
assert.NotNil(t, ctx.Space)
|
||||
assert.NotNil(t, ctx.Memory)
|
||||
assert.NotNil(t, ctx.IDGenerator)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"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/message"
|
||||
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
|
||||
if ctx.Space != nil {
|
||||
spaceObj := ctx.createSpaceObject(v8ctx)
|
||||
obj.Set("space", spaceObj)
|
||||
spaceObj.Release()
|
||||
// Memory object - create a JavaScript object with User/Team/Chat/Context namespaces
|
||||
if ctx.Memory != nil {
|
||||
memoryObj := ctx.createMemoryObject(v8ctx)
|
||||
obj.Set("memory", memoryObj)
|
||||
memoryObj.Release()
|
||||
}
|
||||
|
||||
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
|
||||
// Space is a shared data space for passing data between requests and calls
|
||||
func (ctx *Context) createSpaceObject(v8ctx *v8go.Context) *v8go.Value {
|
||||
// createMemoryObject creates a Memory object for JavaScript access
|
||||
// Memory provides four namespaces: User, Team, Chat, Context
|
||||
// Each namespace supports: Get, Set, Del, Has, Keys, Len, Clear, Incr, Decr
|
||||
func (ctx *Context) createMemoryObject(v8ctx *v8go.Context) *v8go.Value {
|
||||
iso := v8ctx.Isolate()
|
||||
spaceObj, _ := v8ctx.RunScript("({})", "space-init")
|
||||
obj, _ := spaceObj.AsObject()
|
||||
objTpl := v8go.NewObjectTemplate(iso)
|
||||
obj, _ := objTpl.NewInstance(v8ctx)
|
||||
|
||||
// Get method: space.Get(key)
|
||||
// Create namespace accessors
|
||||
if ctx.Memory.User != nil {
|
||||
userObj := ctx.createNamespaceObject(v8ctx, ctx.Memory.User)
|
||||
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 ctx.Space == nil {
|
||||
return v8go.Null(iso)
|
||||
}
|
||||
|
||||
if len(info.Args()) < 1 {
|
||||
return bridge.JsException(info.Context(), "Get requires a key argument")
|
||||
}
|
||||
|
||||
key := info.Args()[0].String()
|
||||
value, err := ctx.Space.Get(key)
|
||||
if err != nil {
|
||||
value, ok := ns.Get(key)
|
||||
if !ok {
|
||||
return v8go.Null(iso)
|
||||
}
|
||||
|
||||
|
|
@ -751,12 +785,8 @@ func (ctx *Context) createSpaceObject(v8ctx *v8go.Context) *v8go.Value {
|
|||
getFuncVal := getFunc.GetFunction(v8ctx)
|
||||
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 {
|
||||
if ctx.Space == nil {
|
||||
return bridge.JsException(info.Context(), "Space is not available")
|
||||
}
|
||||
|
||||
if len(info.Args()) < 2 {
|
||||
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())
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
|
|
@ -776,50 +813,128 @@ func (ctx *Context) createSpaceObject(v8ctx *v8go.Context) *v8go.Value {
|
|||
setFuncVal := setFunc.GetFunction(v8ctx)
|
||||
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 {
|
||||
if ctx.Space == nil {
|
||||
return bridge.JsException(info.Context(), "Space is not available")
|
||||
}
|
||||
|
||||
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()
|
||||
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 v8go.Undefined(iso)
|
||||
})
|
||||
delFuncVal := delFunc.GetFunction(v8ctx)
|
||||
obj.Set("Delete", delFuncVal.Value)
|
||||
obj.Set("Del", delFuncVal.Value)
|
||||
|
||||
// GetDel method: space.GetDel(key) - Get value and delete immediately
|
||||
// Convenient for one-time use data (e.g., file metadata passed between agents)
|
||||
getDelFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||
if ctx.Space == nil {
|
||||
return v8go.Null(iso)
|
||||
// Has method: ns.Has(key)
|
||||
hasFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||
if len(info.Args()) < 1 {
|
||||
return bridge.JsException(info.Context(), "Has requires a key argument")
|
||||
}
|
||||
|
||||
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 {
|
||||
return bridge.JsException(info.Context(), "GetDel requires a key argument")
|
||||
}
|
||||
|
||||
key := info.Args()[0].String()
|
||||
|
||||
// Get value first
|
||||
value, err := ctx.Space.Get(key)
|
||||
if err != nil {
|
||||
value, ok := ns.GetDel(key)
|
||||
if !ok {
|
||||
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)
|
||||
if err != nil {
|
||||
return v8go.Null(iso)
|
||||
|
|
@ -830,7 +945,7 @@ func (ctx *Context) createSpaceObject(v8ctx *v8go.Context) *v8go.Value {
|
|||
getDelFuncVal := getDelFunc.GetFunction(v8ctx)
|
||||
obj.Set("GetDel", getDelFuncVal.Value)
|
||||
|
||||
return spaceObj
|
||||
return obj.Value
|
||||
}
|
||||
|
||||
// sendGroupMethod implements ctx.SendGroup(group)
|
||||
|
|
|
|||
|
|
@ -11,21 +11,23 @@ import (
|
|||
"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
|
||||
func TestMCPListResources(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
// Initialize context with trace
|
||||
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
|
||||
ctx := newMCPTestContext()
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
|
|
@ -62,15 +64,7 @@ func TestMCPReadResource(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(),
|
||||
Referer: context.RefererAPI,
|
||||
}
|
||||
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
||||
ctx.Stack = stack
|
||||
ctx := newMCPTestContext()
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
|
|
@ -105,15 +99,7 @@ func TestMCPListTools(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(),
|
||||
Referer: context.RefererAPI,
|
||||
}
|
||||
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
||||
ctx.Stack = stack
|
||||
ctx := newMCPTestContext()
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
|
|
@ -152,15 +138,7 @@ func TestMCPCallTool(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(),
|
||||
Referer: context.RefererAPI,
|
||||
}
|
||||
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
||||
ctx.Stack = stack
|
||||
ctx := newMCPTestContext()
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
|
|
@ -195,15 +173,7 @@ func TestMCPCallTools(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(),
|
||||
Referer: context.RefererAPI,
|
||||
}
|
||||
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
||||
ctx.Stack = stack
|
||||
ctx := newMCPTestContext()
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
|
|
@ -243,15 +213,7 @@ func TestMCPCallToolsParallel(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(),
|
||||
Referer: context.RefererAPI,
|
||||
}
|
||||
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
||||
ctx.Stack = stack
|
||||
ctx := newMCPTestContext()
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
|
|
@ -291,15 +253,7 @@ func TestMCPListPrompts(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(),
|
||||
Referer: context.RefererAPI,
|
||||
}
|
||||
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
||||
ctx.Stack = stack
|
||||
ctx := newMCPTestContext()
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
|
|
@ -336,15 +290,7 @@ func TestMCPGetPrompt(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(),
|
||||
Referer: context.RefererAPI,
|
||||
}
|
||||
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
||||
ctx.Stack = stack
|
||||
ctx := newMCPTestContext()
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
|
|
@ -379,15 +325,7 @@ func TestMCPListSamples(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(),
|
||||
Referer: context.RefererAPI,
|
||||
}
|
||||
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
||||
ctx.Stack = stack
|
||||
ctx := newMCPTestContext()
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
|
|
@ -422,15 +360,7 @@ func TestMCPGetSample(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(),
|
||||
Referer: context.RefererAPI,
|
||||
}
|
||||
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
||||
ctx.Stack = stack
|
||||
ctx := newMCPTestContext()
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
|
|
@ -467,15 +397,7 @@ func TestMCPJsApiWithTrace(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(),
|
||||
Referer: context.RefererAPI,
|
||||
}
|
||||
stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
||||
ctx.Stack = stack
|
||||
ctx := newMCPTestContext()
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
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"
|
||||
v8 "github.com/yaoapp/gou/runtime/v8"
|
||||
"github.com/yaoapp/yao/agent/context"
|
||||
"github.com/yaoapp/yao/agent/output/message"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"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
|
||||
func TestContextRelease(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
cxt := &context.Context{
|
||||
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
|
||||
cxt := newReleaseTestContext()
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
|
|
@ -71,17 +70,7 @@ func TestTraceRelease(t *testing.T) {
|
|||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
cxt := &context.Context{
|
||||
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
|
||||
cxt := newReleaseTestContext()
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
|
|
@ -134,17 +123,7 @@ func TestContextReleaseWithTrace(t *testing.T) {
|
|||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
cxt := &context.Context{
|
||||
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
|
||||
cxt := newReleaseTestContext()
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
|
|
@ -183,17 +162,7 @@ func TestTryFinallyPattern(t *testing.T) {
|
|||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
cxt := &context.Context{
|
||||
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
|
||||
cxt := newReleaseTestContext()
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
|
|
@ -237,12 +206,8 @@ func TestNoOpTraceRelease(t *testing.T) {
|
|||
defer test.Clean()
|
||||
|
||||
// Context without trace initialization
|
||||
cxt := &context.Context{
|
||||
ChatID: "test-chat-id",
|
||||
AssistantID: "test-assistant-id",
|
||||
Context: stdContext.Background(),
|
||||
IDGenerator: message.NewIDGenerator(),
|
||||
}
|
||||
cxt := context.New(stdContext.Background(), nil, "test-chat-id")
|
||||
cxt.AssistantID = "test-assistant-id"
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
|
|
@ -286,17 +251,7 @@ func TestTryFinallyPatternWithError(t *testing.T) {
|
|||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
cxt := &context.Context{
|
||||
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
|
||||
cxt := newReleaseTestContext()
|
||||
|
||||
_, err := v8.Call(v8.CallOptions{}, `
|
||||
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"
|
||||
v8 "github.com/yaoapp/gou/runtime/v8"
|
||||
"github.com/yaoapp/yao/agent/context"
|
||||
"github.com/yaoapp/yao/agent/output/message"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"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
|
||||
func TestStressContextCreationAndRelease(t *testing.T) {
|
||||
if testing.Short() {
|
||||
|
|
@ -30,17 +39,7 @@ func TestStressContextCreationAndRelease(t *testing.T) {
|
|||
startMemory := getMemStats()
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
cxt := &context.Context{
|
||||
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
|
||||
cxt := newStressTestContext(fmt.Sprintf("chat-%d", i))
|
||||
|
||||
_, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
|
|
@ -107,17 +106,7 @@ func TestStressTraceOperations(t *testing.T) {
|
|||
|
||||
for i := 0; i < iterations; i++ {
|
||||
// Create new context for each iteration to avoid context cancellation issues
|
||||
cxt := &context.Context{
|
||||
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
|
||||
cxt := newStressTestContext(fmt.Sprintf("stress-test-chat-%d", i))
|
||||
_, err := v8.Call(v8.CallOptions{}, fmt.Sprintf(`
|
||||
function test(ctx) {
|
||||
const trace = ctx.trace
|
||||
|
|
@ -185,16 +174,7 @@ func TestStressMCPOperations(t *testing.T) {
|
|||
|
||||
iterations := 500
|
||||
|
||||
cxt := &context.Context{
|
||||
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
|
||||
cxt := newStressTestContext("mcp-stress-test")
|
||||
|
||||
startMemory := getMemStats()
|
||||
|
||||
|
|
@ -269,16 +249,7 @@ func TestStressConcurrentContexts(t *testing.T) {
|
|||
defer wg.Done()
|
||||
|
||||
for i := 0; i < iterationsPerGoroutine; i++ {
|
||||
cxt := &context.Context{
|
||||
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
|
||||
cxt := newStressTestContext(fmt.Sprintf("chat-%d-%d", goroutineID, i))
|
||||
|
||||
_, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
|
|
@ -345,12 +316,8 @@ func TestStressNoOpTracePerformance(t *testing.T) {
|
|||
iterations := 1000
|
||||
|
||||
// Context without trace initialization (no-op trace)
|
||||
cxt := &context.Context{
|
||||
ChatID: "noop-stress-test",
|
||||
AssistantID: "test-assistant",
|
||||
Context: stdContext.Background(),
|
||||
IDGenerator: message.NewIDGenerator(),
|
||||
}
|
||||
cxt := context.New(stdContext.Background(), nil, "noop-stress-test")
|
||||
cxt.AssistantID = "test-assistant"
|
||||
|
||||
startMemory := getMemStats()
|
||||
startTime := time.Now()
|
||||
|
|
@ -420,16 +387,7 @@ func TestStressReleasePatterns(t *testing.T) {
|
|||
startMemory := getMemStats()
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
cxt := &context.Context{
|
||||
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
|
||||
cxt := newStressTestContext(fmt.Sprintf("manual-%d", i))
|
||||
|
||||
_, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
|
|
@ -459,16 +417,7 @@ func TestStressReleasePatterns(t *testing.T) {
|
|||
startMemory := getMemStats()
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
cxt := &context.Context{
|
||||
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
|
||||
cxt := newStressTestContext(fmt.Sprintf("gc-%d", i))
|
||||
|
||||
_, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
|
|
@ -500,16 +449,7 @@ func TestStressReleasePatterns(t *testing.T) {
|
|||
startMemory := getMemStats()
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
cxt := &context.Context{
|
||||
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
|
||||
cxt := newStressTestContext(fmt.Sprintf("separate-%d", i))
|
||||
|
||||
_, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
|
|
@ -546,16 +486,7 @@ func TestStressLongRunningTrace(t *testing.T) {
|
|||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
cxt := &context.Context{
|
||||
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
|
||||
cxt := newStressTestContext("long-running-test")
|
||||
|
||||
startMemory := getMemStats()
|
||||
operations := 100
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import (
|
|||
v8 "github.com/yaoapp/gou/runtime/v8"
|
||||
"github.com/yaoapp/gou/runtime/v8/bridge"
|
||||
"github.com/yaoapp/yao/agent/context"
|
||||
"github.com/yaoapp/yao/agent/output/message"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||
"github.com/yaoapp/yao/test"
|
||||
|
|
@ -23,12 +22,8 @@ func TestJsValue(t *testing.T) {
|
|||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
cxt := &context.Context{
|
||||
ChatID: "ChatID-123456",
|
||||
AssistantID: "AssistantID-1234",
|
||||
Context: stdContext.Background(),
|
||||
IDGenerator: message.NewIDGenerator(),
|
||||
}
|
||||
cxt := context.New(stdContext.Background(), nil, "ChatID-123456")
|
||||
cxt.AssistantID = "AssistantID-1234"
|
||||
|
||||
v8.RegisterFunction("testContextJsvalue", testContextJsvalueEmbed)
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
|
|
@ -91,12 +86,8 @@ func TestJsValueConcurrent(t *testing.T) {
|
|||
chatID := fmt.Sprintf("ChatID-%d-%d", routineID, j)
|
||||
assistantID := fmt.Sprintf("AssistantID-%d-%d", routineID, j)
|
||||
|
||||
cxt := &context.Context{
|
||||
ChatID: chatID,
|
||||
AssistantID: assistantID,
|
||||
Context: stdContext.Background(),
|
||||
IDGenerator: message.NewIDGenerator(),
|
||||
}
|
||||
cxt := context.New(stdContext.Background(), nil, chatID)
|
||||
cxt.AssistantID = assistantID
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(cxt) {
|
||||
|
|
@ -150,12 +141,8 @@ func TestJsValueRegistrationAndCleanup(t *testing.T) {
|
|||
// Create multiple contexts and verify registration
|
||||
contextCount := 5
|
||||
for i := 0; i < contextCount; i++ {
|
||||
cxt := &context.Context{
|
||||
ChatID: fmt.Sprintf("ChatID-%d", i),
|
||||
AssistantID: fmt.Sprintf("AssistantID-%d", i),
|
||||
Context: stdContext.Background(),
|
||||
IDGenerator: message.NewIDGenerator(),
|
||||
}
|
||||
cxt := context.New(stdContext.Background(), nil, fmt.Sprintf("ChatID-%d", i))
|
||||
cxt.AssistantID = fmt.Sprintf("AssistantID-%d", i)
|
||||
|
||||
_, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(cxt) {
|
||||
|
|
@ -219,43 +206,41 @@ func TestJsValueAllFields(t *testing.T) {
|
|||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
cxt := &context.Context{
|
||||
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",
|
||||
ClientID: "test-client",
|
||||
UserID: "user-123",
|
||||
TeamID: "team-456",
|
||||
TenantID: "tenant-789",
|
||||
Constraints: types.DataConstraints{
|
||||
OwnerOnly: true,
|
||||
CreatorOnly: false,
|
||||
TeamOnly: true,
|
||||
Extra: map[string]interface{}{
|
||||
"department": "engineering",
|
||||
"region": "us-west",
|
||||
},
|
||||
authInfo := &types.AuthorizedInfo{
|
||||
Subject: "test-user",
|
||||
ClientID: "test-client",
|
||||
UserID: "user-123",
|
||||
TeamID: "team-456",
|
||||
TenantID: "tenant-789",
|
||||
Constraints: types.DataConstraints{
|
||||
OwnerOnly: true,
|
||||
CreatorOnly: false,
|
||||
TeamOnly: true,
|
||||
Extra: map[string]interface{}{
|
||||
"department": "engineering",
|
||||
"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)
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(cxt) {
|
||||
|
|
@ -404,14 +389,10 @@ func TestJsValueTrace(t *testing.T) {
|
|||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
cxt := &context.Context{
|
||||
ChatID: "test-chat-id",
|
||||
AssistantID: "test-assistant-id",
|
||||
Stack: &context.Stack{
|
||||
TraceID: "test-trace-id",
|
||||
},
|
||||
Context: stdContext.Background(),
|
||||
IDGenerator: message.NewIDGenerator(),
|
||||
cxt := context.New(stdContext.Background(), nil, "test-chat-id")
|
||||
cxt.AssistantID = "test-assistant-id"
|
||||
cxt.Stack = &context.Stack{
|
||||
TraceID: "test-trace-id",
|
||||
}
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
|
|
@ -470,21 +451,17 @@ func TestJsValueAuthorizedAndMetadata(t *testing.T) {
|
|||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
cxt := &context.Context{
|
||||
ChatID: "test-chat-id",
|
||||
AssistantID: "test-assistant-id",
|
||||
Context: stdContext.Background(),
|
||||
IDGenerator: message.NewIDGenerator(),
|
||||
Authorized: &types.AuthorizedInfo{
|
||||
UserID: "user-123",
|
||||
TenantID: "tenant-456",
|
||||
ClientID: "client-789",
|
||||
},
|
||||
Metadata: map[string]interface{}{
|
||||
"request_id": "req-001",
|
||||
"source": "api",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
authInfo := &types.AuthorizedInfo{
|
||||
UserID: "user-123",
|
||||
TenantID: "tenant-456",
|
||||
ClientID: "client-789",
|
||||
}
|
||||
cxt := context.New(stdContext.Background(), authInfo, "test-chat-id")
|
||||
cxt.AssistantID = "test-assistant-id"
|
||||
cxt.Metadata = map[string]interface{}{
|
||||
"request_id": "req-001",
|
||||
"source": "api",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
|
||||
v8.RegisterFunction("testAuthorizedMetadata", testAuthorizedMetadataEmbed)
|
||||
|
|
@ -573,14 +550,9 @@ func TestJsValueAuthorizedNil(t *testing.T) {
|
|||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
cxt := &context.Context{
|
||||
ChatID: "test-chat-id",
|
||||
AssistantID: "test-assistant-id",
|
||||
Context: stdContext.Background(),
|
||||
IDGenerator: message.NewIDGenerator(),
|
||||
Authorized: nil, // Explicitly nil
|
||||
Metadata: nil, // Explicitly nil (should be empty object)
|
||||
}
|
||||
cxt := context.New(stdContext.Background(), nil, "test-chat-id")
|
||||
cxt.AssistantID = "test-assistant-id"
|
||||
cxt.Metadata = nil // Explicitly nil (should be empty object)
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(cxt) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/yaoapp/gou/mcp/types"
|
||||
"github.com/yaoapp/gou/plan"
|
||||
"github.com/yaoapp/yao/agent/context"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/test"
|
||||
|
|
@ -13,15 +12,10 @@ import (
|
|||
|
||||
// newTestMCPContext creates a test context
|
||||
func newTestMCPContext() *context.Context {
|
||||
ctx := &context.Context{
|
||||
Context: stdContext.Background(),
|
||||
Space: plan.NewMemorySharedSpace(),
|
||||
ID: "test-context",
|
||||
ChatID: "test-chat",
|
||||
AssistantID: "test-assistant",
|
||||
Locale: "en",
|
||||
Referer: context.RefererAPI,
|
||||
}
|
||||
ctx := context.New(stdContext.Background(), nil, "test-chat")
|
||||
ctx.AssistantID = "test-assistant"
|
||||
ctx.Locale = "en"
|
||||
ctx.Referer = context.RefererAPI
|
||||
|
||||
// Initialize stack and trace
|
||||
stack, traceID, _ := context.EnterStack(ctx, "test-assistant", &context.Options{})
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/yaoapp/gou/connector/openai"
|
||||
"github.com/yaoapp/gou/plan"
|
||||
"github.com/yaoapp/gou/store"
|
||||
"github.com/yaoapp/yao/agent/memory"
|
||||
"github.com/yaoapp/yao/agent/output"
|
||||
"github.com/yaoapp/yao/agent/output/message"
|
||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||
|
|
@ -226,7 +226,7 @@ type Context struct {
|
|||
|
||||
// External
|
||||
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"
|
||||
Stack *Stack `json:"-"` // Stack, current active stack of the request
|
||||
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)
|
||||
|
||||
// Create a mock context
|
||||
ctx := &context.Context{}
|
||||
ctx := context.New(nil, nil, "test-chat")
|
||||
|
||||
// Get search API
|
||||
searchAPI := context.SearchAPIFactory(ctx)
|
||||
|
|
@ -337,7 +337,8 @@ func TestSetJSAPIFactory_WithGetter(t *testing.T) {
|
|||
require.NotNil(t, context.SearchAPIFactory)
|
||||
|
||||
// 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
|
||||
searchAPI := context.SearchAPIFactory(ctx)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yaoapp/gou/plan"
|
||||
agentContext "github.com/yaoapp/yao/agent/context"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/test"
|
||||
|
|
@ -14,15 +13,10 @@ import (
|
|||
|
||||
// newTestContext creates a test context for MCP testing
|
||||
func newTestContext() *agentContext.Context {
|
||||
ctx := &agentContext.Context{
|
||||
Context: stdContext.Background(),
|
||||
Space: plan.NewMemorySharedSpace(),
|
||||
ID: "test-querydsl",
|
||||
ChatID: "test-chat",
|
||||
AssistantID: "test-assistant",
|
||||
Locale: "en",
|
||||
Referer: agentContext.RefererAPI,
|
||||
}
|
||||
ctx := agentContext.New(stdContext.Background(), nil, "test-chat")
|
||||
ctx.AssistantID = "test-assistant"
|
||||
ctx.Locale = "en"
|
||||
ctx.Referer = agentContext.RefererAPI
|
||||
stack, _, _ := agentContext.EnterStack(ctx, "test-assistant", &agentContext.Options{})
|
||||
ctx.Stack = stack
|
||||
return ctx
|
||||
|
|
|
|||
415
data/bindata.go
415
data/bindata.go
File diff suppressed because it is too large
Load diff
|
|
@ -14,15 +14,18 @@ import (
|
|||
)
|
||||
|
||||
var systemStores = map[string]string{
|
||||
"__yao.store": "yao/stores/store.xun.yao", // for common data store
|
||||
"__yao.cache": "yao/stores/cache.lru.yao", // for common cache 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.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.cache": "yao/stores/agent/cache.lru.yao", // for agent cache store (for agent cache)
|
||||
"__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.store": "yao/stores/store.xun.yao", // for common data store
|
||||
"__yao.cache": "yao/stores/cache.lru.yao", // for common cache 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.client": "yao/stores/oauth/client.xun.yao", // for OAuth client store
|
||||
"__yao.agent.memory.user": "yao/stores/agent/memory/user.xun.yao", // for agent user-level memory
|
||||
"__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.cache": "yao/stores/kb/cache.lru.yao", // for knowledge base cache store
|
||||
}
|
||||
|
||||
// replaceVars replaces template variables in the JSON string
|
||||
|
|
|
|||
|
|
@ -43,7 +43,10 @@ func check(t *testing.T) {
|
|||
assert.True(t, ids["__yao.oauth.store"])
|
||||
assert.True(t, ids["__yao.oauth.client"])
|
||||
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"])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -220,15 +220,18 @@ var testSystemModels = map[string]string{
|
|||
}
|
||||
|
||||
var testSystemStores = map[string]string{
|
||||
"__yao.store": "yao/stores/store.xun.yao",
|
||||
"__yao.cache": "yao/stores/cache.lru.yao",
|
||||
"__yao.oauth.store": "yao/stores/oauth/store.xun.yao",
|
||||
"__yao.oauth.client": "yao/stores/oauth/client.xun.yao",
|
||||
"__yao.oauth.cache": "yao/stores/oauth/cache.lru.yao",
|
||||
"__yao.agent.memory": "yao/stores/agent/memory.xun.yao",
|
||||
"__yao.agent.cache": "yao/stores/agent/cache.lru.yao",
|
||||
"__yao.kb.store": "yao/stores/kb/store.xun.yao",
|
||||
"__yao.kb.cache": "yao/stores/kb/cache.lru.yao",
|
||||
"__yao.store": "yao/stores/store.xun.yao",
|
||||
"__yao.cache": "yao/stores/cache.lru.yao",
|
||||
"__yao.oauth.store": "yao/stores/oauth/store.xun.yao",
|
||||
"__yao.oauth.client": "yao/stores/oauth/client.xun.yao",
|
||||
"__yao.oauth.cache": "yao/stores/oauth/cache.lru.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.kb.store": "yao/stores/kb/store.xun.yao",
|
||||
"__yao.kb.cache": "yao/stores/kb/cache.lru.yao",
|
||||
}
|
||||
|
||||
func loadSystemStores(t *testing.T, cfg config.Config) error {
|
||||
|
|
|
|||
|
|
@ -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