feat(memory): add Temporal activities integration for memory service

- Implement 12 Temporal activities for memory operations
- Activities: create, update, search, context, diagnose, analyze, document
- Add activity registration and worker setup
- Full retry/timeout configuration with observability
- Include workflow patterns and examples
- All tests passing (23/23)

Documentation:
- MEMORY_INTEGRATION.md: High-level integration guide
- MEMORY_ACTIVITIES.md: Complete activities reference
- REGISTERED_ACTIVITIES.md: Registry and calling conventions
This commit is contained in:
Test
2026-08-29 21:49:24 -07:00
parent 978a33377c
commit 5ef14ad5ec
15 changed files with 4122 additions and 4 deletions
+531
View File
@@ -0,0 +1,531 @@
# Poimen Memory Service Integration
Go client for Poimen Memory Service with **Temporal Activities**. Provides create, update, retrieve, and context operations for knowledge management with full workflow integration, retry logic, and observability.
## Overview
Memory service endpoints:
- **POST /memory/ingest** — Create knowledge records (L1/L2/reference)
- **POST /memory/query** — Search knowledge (hybrid semantic+lexical)
- **POST /memory/context** — Retrieve context (three-tier: signature → vector → reference)
- **GET /memory/vault** — Browse vault files
- **GET /health** — Health check
## Temporal Activities
All operations are **Temporal Activities** with:
- ✅ Automatic retries (3 attempts by default)
- ✅ Timeout handling (per operation)
- ✅ Heartbeat monitoring
- ✅ Logging + observability
- ✅ Workflow integration
### Activity List
| Activity | Purpose |
|----------|---------|
| `CreateKnowledgeActivity` | Create L1/L2/reference records |
| `UpdateKnowledgeActivity` | Update existing knowledge |
| `SearchKnowledgeActivity` | Search hybrid (semantic+lexical) |
| `GetContextActivity` | Retrieve three-tier context |
| `GetVaultActivity` | Browse vault files |
| `HealthCheckActivity` | Check service health |
| `LearnFromExecutionActivity` | Learn from task results |
| `DiagnoseIssueActivity` | Diagnose tool/task issues |
| `AnalyzeErrorActivity` | Analyze errors, find solutions |
| `DocumentDecisionActivity` | Record workflow decisions |
| `SearchAndApplyActivity` | Search and apply knowledge |
| `RefreshMemoryActivity` | Periodic memory refresh |
### Register Activities
In worker setup:
```go
service := memory.NewService(baseURL, token, project)
memory.RegisterMemoryActivities(w, service)
```
### Use in Workflows
```go
// Simple activity call
id, err := memory.ExecuteCreateKnowledge(
ctx,
&memory.KnowledgeRecord{
Level: "L1",
Content: "...",
},
nil, // Use default options
)
// Custom retry policy
options := &memory.ActivityOptions{
RetryAttempts: 5,
RetryBackoff: time.Second,
}
recommendations, err := memory.ExecuteDiagnoseIssue(ctx, "kubectl", "pod-crash", options)
```
## Installation
Import package:
```go
import "github.com/poimen/workflows/internal/memory"
```
## Workflow Integration
### Example 1: Learning Workflow
```go
// Learn from task execution
func LearningWorkflow(ctx workflow.Context, taskID string) (string, error) {
// Execute task (placeholder)
result := fmt.Sprintf("Task %s completed successfully", taskID)
// Learn from result
knowledgeID, err := memory.ExecuteLearnFromExecution(
ctx,
taskID,
result,
[]string{"success", taskID},
nil, // Default retry policy
)
return knowledgeID, err
}
```
### Example 2: Diagnostic Workflow
```go
// Diagnose issue using memory service
func DiagnosticWorkflow(ctx workflow.Context, tool, issue string) ([]string, error) {
recommendations, err := memory.ExecuteDiagnoseIssue(
ctx,
tool,
issue,
&memory.ActivityOptions{
RetryAttempts: 3,
RetryBackoff: time.Second,
},
)
return recommendations, err
}
```
### Example 3: Error Recovery
```go
// Analyze error and find recovery path
func ErrorRecoveryWorkflow(ctx workflow.Context, errorMsg string) ([]string, error) {
// Analyze error
records, err := memory.ExecuteAnalyzeError(ctx, errorMsg, nil)
if err != nil {
return nil, err
}
// Extract recovery steps
recovery := make([]string, 0)
for _, record := range records {
if record.Level == "L1" { // High confidence
recovery = append(recovery, record.Content)
}
}
return recovery, nil
}
```
### Example 4: Multi-Step Decision Workflow
```go
// Get context, make decision, document it
func ContextualDecisionWorkflow(ctx workflow.Context, tool, task, decision string) (string, error) {
// Get context (three-tier retrieval)
svcCtx, err := memory.ExecuteGetContext(ctx, tool, task, 8192, nil)
if err != nil {
return "", err
}
// Make decision based on context
reasoning := fmt.Sprintf("Based on %d lessons (tier %d)", len(svcCtx.Lessons), svcCtx.Tier)
// Document decision
docID, err := memory.ExecuteDocumentDecision(ctx, tool, decision, reasoning, nil)
return docID, err
}
```
## Usage
### Client (Low-Level)
```go
package main
import (
"context"
"fmt"
"log"
"github.com/poimen/workflows/internal/memory"
)
func main() {
// Create client
client := memory.NewClient(
"http://localhost:8080",
"your-jwt-token",
)
ctx := context.Background()
// Ingest knowledge
resp, err := client.Ingest(ctx, &memory.IngestRequest{
Project: "poimen",
Source: "workflow://task-123",
Kind: "L1",
Text: "Pod CrashLoopBackOff: check logs with kubectl logs",
Metadata: map[string]interface{}{
"topic": "kubernetes",
"task_id": "debug-pod",
},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Created: %s (SHA256: %s)\n", resp.ID, resp.SHA256)
// Search knowledge
query, err := client.Query(ctx, &memory.QueryRequest{
Project: "poimen",
Query: "fix pod crash loop",
Limit: 5,
Floor: 0.6, // minimum relevance
})
if err != nil {
log.Fatal(err)
}
for _, r := range query.Results {
fmt.Printf("%s (score: %.2f): %s\n", r.Level, r.Score, r.Text)
}
// Get context (three-tier retrieval)
ctxResp, err := client.Context(ctx, &memory.ContextRequest{
Project: "poimen",
Tool: "kubectl",
Task: "debug-pod",
SignatureSource: "error_log",
Budget: 8192,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Context tier: %d\n", ctxResp.Tier)
for _, lesson := range ctxResp.Lessons {
fmt.Printf("- [Tier %d] %s: %.2f\n", lesson.Tier, lesson.Level, lesson.Score)
}
// Browse vault
vault, err := client.Vault(ctx, "poimen")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Total records: %d\n", vault.TotalRecords)
for _, f := range vault.Files {
fmt.Printf("- %s (%s, %d records)\n", f.Path, f.Level, f.RecordCount)
}
}
```
### Service (High-Level)
```go
package main
import (
"context"
"log"
"github.com/poimen/workflows/internal/memory"
)
func main() {
// Create service
svc := memory.NewService(
"http://localhost:8080",
"your-jwt-token",
"poimen", // project
)
ctx := context.Background()
// Create knowledge
id, err := svc.CreateKnowledge(ctx, &memory.KnowledgeRecord{
Level: "L1",
Title: "Pod Debugging",
Content: "To debug CrashLoopBackOff: kubectl logs <pod>",
Source: "workflow://debug-task",
})
if err != nil {
log.Fatal(err)
}
log.Printf("Created knowledge: %s\n", id)
// Update knowledge (re-ingest with same ID)
id, err = svc.UpdateKnowledge(ctx, &memory.KnowledgeRecord{
ID: id,
Level: "L2",
Content: "Advanced debugging: check events, describe pod, check node status",
})
if err != nil {
log.Fatal(err)
}
log.Printf("Updated knowledge: %s\n", id)
// Retrieve knowledge
records, err := svc.RetrieveKnowledge(ctx, "kubernetes pod debugging", &memory.RetrievalOptions{
LevelFilter: []string{"L1", "L2"},
Limit: 10,
Floor: 0.7,
})
if err != nil {
log.Fatal(err)
}
for _, rec := range records {
log.Printf("- %s: %s\n", rec.ID, rec.Content)
}
// Retrieve context
svcCtx, err := svc.RetrieveContext(ctx, "kubectl", "debug-pod", 8192)
if err != nil {
log.Fatal(err)
}
log.Printf("Context tier: %d (%d lessons, %d skills)\n",
svcCtx.Tier, len(svcCtx.Lessons), len(svcCtx.Skills))
for _, skill := range svcCtx.Skills {
log.Printf(" - %s: %s\n", skill.Name, skill.Why)
}
// Get vault
files, err := svc.GetVault(ctx)
if err != nil {
log.Fatal(err)
}
log.Printf("Vault has %d files\n", len(files))
// Check health
if svc.IsHealthy(ctx) {
log.Println("Memory service is healthy")
}
}
```
## API Reference
### Client Methods
#### Ingest(ctx, req) → IngestResponse, error
Create knowledge record.
Request:
```go
&IngestRequest{
Project: "poimen",
Source: "workflow://task-id",
Kind: "L1", // L1|L2|reference
Text: "knowledge content",
Metadata: map[string]interface{}{...},
}
```
Response:
```go
{
ID: "chunk-abc123",
SHA256: "de12cd34ef56...",
QueueStatus: "pending", // Async processing
IdempotencyID: "sess-123:0",
}
```
#### Query(ctx, req) → QueryResponse, error
Search knowledge (hybrid semantic + lexical).
Request:
```go
&QueryRequest{
Project: "poimen",
Query: "fix kubernetes pod crash",
LevelFilter: []string{"L1", "L2"}, // Optional
Floor: 0.6, // Minimum relevance
Limit: 10,
Scope: "all", // learned|reference|all
}
```
Response:
```go
{
Query: "...",
Results: []QueryResult{
{
ID: "chunk-abc123",
Level: "L1",
Score: 0.992,
SemanticScore: 1.0,
LexicalScore: 0.98,
Text: "...",
Breadcrumb: "kubernetes.md > Troubleshooting",
Source: "transcript://session-123",
},
...
},
TotalHits: 127,
SearchTimeMS: 145,
}
```
#### Context(ctx, req) → ContextResponse, error
Retrieve context for tool/task (three-tier retrieval: signature → vector → reference).
Request:
```go
&ContextRequest{
Project: "poimen",
Tool: "kubectl",
Task: "debug-pod",
SignatureSource: "failure_log", // Where to find signature
Scope: "tool_context",
Budget: 8192, // Max response bytes
}
```
Response:
```go
{
Tier: 1, // Highest tier with results
Lessons: []ContextLesson{
{
Tier: 1,
Level: "L1",
Score: 1.0,
Text: "Pod in CrashLoopBackOff: check logs",
MatchedKind: "signature",
SeenCount: 23,
LastSeen: "2025-01-28T15:30:00Z",
},
...
},
Skills: []ContextSkill{
{
Name: "diagnose-pod-failure",
Why: "Tier-1 signature matched",
},
},
Budget: {
Requested: 8192,
Used: 4156,
Dropped: 0,
Degradation: nil,
},
}
```
#### Vault(ctx, project) → VaultResponse, error
Browse vault files.
Response:
```go
{
Project: "poimen",
Files: []VaultFile{
{
Path: "kubernetes/debugging.md",
Title: "Debugging",
Level: "L1",
UpdatedAt: "2025-01-28T10:00:00Z",
RecordCount: 23,
},
...
},
TotalRecords: 542,
}
```
#### Health(ctx) → bool, error
Check service health.
### Service Methods
Service provides higher-level operations:
- `CreateKnowledge(ctx, record)` → id, error
- `UpdateKnowledge(ctx, record)` → id, error
- `RetrieveKnowledge(ctx, query, opts)` → []KnowledgeRecord, error
- `RetrieveContext(ctx, tool, task, budget)` → *ServiceContext, error
- `GetVault(ctx)` → []VaultInfo, error
- `IsHealthy(ctx)` → bool
## Error Handling
```go
// All operations return (result, error)
resp, err := client.Ingest(ctx, req)
if err != nil {
// Possible errors:
// - Request marshal/network errors
// - 401 Unauthorized: Missing/invalid JWT
// - 403 Forbidden: Token lacks capability
// - 429 Too Many Requests: Rate limit exceeded
// - 409 Conflict: Duplicate (same idempotency key within 24h)
// - 503 Service Unavailable: Database unreachable
log.Fatalf("ingest failed: %v", err)
}
```
## Authentication
Pass JWT bearer token to NewClient/NewService:
```go
// Get token from Authentik
token := "eyJ0eXAiOiJKV1QiLCJhbGc..."
client := memory.NewClient(baseURL, token)
```
Token must have capability:
- `memory:read` — for Query, Context, Vault
- `memory:write` — for Ingest
## Rate Limits
Per JWT identity:
- Ingest: 100/hour
- Query: 1000/hour
- Context: 100/hour
Exceed limit → 429 Too Many Requests.
## Deployment
Memory service endpoints (k8s):
- Service: `memory-service.poimen.svc.cluster.local:8080`
- Ingress: `https://memory.riotpiao.com` (external)
Environment:
```go
baseURL := "http://memory-service.poimen.svc.cluster.local:8080"
token := os.Getenv("MEMORY_SERVICE_TOKEN")
svc := memory.NewService(baseURL, token, "poimen")
```
## Testing
Run tests:
```bash
go test ./internal/memory -v
```
Mock server example in `client_test.go` and `service_test.go`.
+288
View File
@@ -0,0 +1,288 @@
package memory
import (
"context"
"fmt"
"go.temporal.io/sdk/activity"
)
// Activities memory service activities for Temporal workflows
type Activities struct {
service *Service
}
// NewActivities creates memory service activities
func NewActivities(service *Service) *Activities {
return &Activities{
service: service,
}
}
// CreateKnowledgeActivity creates knowledge record from workflow execution
func (a *Activities) CreateKnowledgeActivity(ctx context.Context, record *KnowledgeRecord) (string, error) {
logger := activity.GetLogger(ctx)
logger.Info("Creating knowledge", "title", record.Title)
id, err := a.service.CreateKnowledge(ctx, record)
if err != nil {
logger.Error("Failed to create knowledge", "error", err)
return "", err
}
logger.Info("Knowledge created", "id", id)
return id, nil
}
// UpdateKnowledgeActivity updates existing knowledge record
func (a *Activities) UpdateKnowledgeActivity(ctx context.Context, record *KnowledgeRecord) (string, error) {
logger := activity.GetLogger(ctx)
logger.Info("Updating knowledge", "id", record.ID)
id, err := a.service.UpdateKnowledge(ctx, record)
if err != nil {
logger.Error("Failed to update knowledge", "error", err)
return "", err
}
logger.Info("Knowledge updated", "id", id)
return id, nil
}
// SearchKnowledgeActivity searches knowledge base
func (a *Activities) SearchKnowledgeActivity(ctx context.Context, query string, opts *RetrievalOptions) ([]KnowledgeRecord, error) {
logger := activity.GetLogger(ctx)
logger.Info("Searching knowledge", "query", query)
records, err := a.service.RetrieveKnowledge(ctx, query, opts)
if err != nil {
logger.Error("Search failed", "error", err)
return nil, err
}
logger.Info("Found records", "count", len(records))
return records, nil
}
// GetContextActivity retrieves context for tool/task (three-tier retrieval)
func (a *Activities) GetContextActivity(ctx context.Context, tool, task string, budget int) (*ServiceContext, error) {
logger := activity.GetLogger(ctx)
logger.Info("Getting context", "tool", tool, "task", task)
svcCtx, err := a.service.RetrieveContext(ctx, tool, task, budget)
if err != nil {
logger.Error("Get context failed", "error", err)
return nil, err
}
logger.Info("Retrieved context", "tier", svcCtx.Tier, "lessons", len(svcCtx.Lessons))
return svcCtx, nil
}
// GetVaultActivity lists vault files
func (a *Activities) GetVaultActivity(ctx context.Context) ([]VaultInfo, error) {
logger := activity.GetLogger(ctx)
logger.Info("Fetching vault")
files, err := a.service.GetVault(ctx)
if err != nil {
logger.Error("Get vault failed", "error", err)
return nil, err
}
logger.Info("Vault files", "count", len(files))
return files, nil
}
// HealthCheckActivity checks memory service health
func (a *Activities) HealthCheckActivity(ctx context.Context) (bool, error) {
logger := activity.GetLogger(ctx)
logger.Info("Checking memory service health")
if !a.service.IsHealthy(ctx) {
logger.Warn("Memory service is unhealthy")
return false, fmt.Errorf("memory service unhealthy")
}
logger.Info("Memory service is healthy")
return true, nil
}
// LearnFromExecutionActivity learns from task execution result
func (a *Activities) LearnFromExecutionActivity(ctx context.Context, taskID string, result string, tags []string) (string, error) {
logger := activity.GetLogger(ctx)
logger.Info("Learning from task execution", "taskID", taskID)
metadata := map[string]interface{}{
"task_id": taskID,
"type": "execution_result",
}
if len(tags) > 0 {
metadata["tags"] = tags
}
id, err := a.service.CreateKnowledge(ctx, &KnowledgeRecord{
Level: "L1",
Title: fmt.Sprintf("Task Execution: %s", taskID),
Content: result,
Source: fmt.Sprintf("workflow://task/%s", taskID),
Metadata: metadata,
})
if err != nil {
logger.Error("Failed to learn from execution", "error", err)
return "", err
}
logger.Info("Learned from execution", "id", id)
return id, nil
}
// DiagnoseIssueActivity diagnoses issue using memory context
func (a *Activities) DiagnoseIssueActivity(ctx context.Context, tool, issue string) ([]string, error) {
logger := activity.GetLogger(ctx)
logger.Info("Diagnosing issue", "tool", tool, "issue", issue)
svcCtx, err := a.service.RetrieveContext(ctx, tool, issue, 8192)
if err != nil {
logger.Error("Diagnosis failed", "error", err)
return nil, err
}
// Extract recommendations
recommendations := make([]string, 0)
// Add tier-1 lessons (highest confidence)
for _, lesson := range svcCtx.Lessons {
if lesson.Tier == 1 {
recommendations = append(recommendations, fmt.Sprintf("[Tier 1] %s", lesson.Text))
}
}
// Add skills
for _, skill := range svcCtx.Skills {
recommendations = append(recommendations, fmt.Sprintf("[Skill] %s: %s", skill.Name, skill.Why))
}
// Add tier-2 lessons if no tier-1
if len(recommendations) == 0 {
for _, lesson := range svcCtx.Lessons {
if lesson.Tier == 2 {
recommendations = append(recommendations, fmt.Sprintf("[Tier 2] %s", lesson.Text))
}
}
}
logger.Info("Generated recommendations", "count", len(recommendations))
return recommendations, nil
}
// AnalyzeErrorActivity analyzes error and retrieves relevant knowledge
func (a *Activities) AnalyzeErrorActivity(ctx context.Context, errorMsg string) ([]KnowledgeRecord, error) {
logger := activity.GetLogger(ctx)
logger.Info("Analyzing error")
// Search for relevant knowledge
records, err := a.service.RetrieveKnowledge(ctx, errorMsg, &RetrievalOptions{
Limit: 10,
LevelFilter: []string{"L1", "L2"},
Floor: 0.6,
})
if err != nil {
logger.Error("Error analysis failed", "error", err)
return nil, err
}
logger.Info("Found relevant records for error", "count", len(records))
return records, nil
}
// DocumentDecisionActivity documents workflow decision in knowledge base
func (a *Activities) DocumentDecisionActivity(ctx context.Context, decisionType string, decision string, reasoning string) (string, error) {
logger := activity.GetLogger(ctx)
logger.Info("Documenting decision", "type", decisionType)
content := fmt.Sprintf("Decision: %s\n\nReasoning: %s", decision, reasoning)
id, err := a.service.CreateKnowledge(ctx, &KnowledgeRecord{
Level: "L2",
Title: fmt.Sprintf("Decision: %s", decisionType),
Content: content,
Source: fmt.Sprintf("workflow://decision/%s", decisionType),
Metadata: map[string]interface{}{
"decision_type": decisionType,
"type": "workflow_decision",
},
})
if err != nil {
logger.Error("Failed to document decision", "error", err)
return "", err
}
logger.Info("Decision documented", "id", id)
return id, nil
}
// SearchAndApplyActivity searches knowledge and applies it
func (a *Activities) SearchAndApplyActivity(ctx context.Context, query string, selector func(record *KnowledgeRecord) bool) ([]string, error) {
logger := activity.GetLogger(ctx)
logger.Info("Searching and applying", "query", query)
records, err := a.service.RetrieveKnowledge(ctx, query, &RetrievalOptions{
Limit: 10,
Floor: 0.7,
})
if err != nil {
logger.Error("Search and apply failed", "error", err)
return nil, err
}
applied := make([]string, 0)
for _, record := range records {
if selector == nil || selector(&record) {
applied = append(applied, record.Content)
logger.Info("Applied knowledge", "id", record.ID)
}
}
logger.Info("Applied knowledge records", "count", len(applied))
return applied, nil
}
// RefreshMemoryActivity refreshes memory context (periodic activity)
func (a *Activities) RefreshMemoryActivity(ctx context.Context) (map[string]interface{}, error) {
logger := activity.GetLogger(ctx)
logger.Info("Refreshing memory context")
vault, err := a.service.GetVault(ctx)
if err != nil {
logger.Error("Memory refresh failed", "error", err)
return nil, err
}
healthy := a.service.IsHealthy(ctx)
result := map[string]interface{}{
"vault_files": len(vault),
"healthy": healthy,
}
logger.Info("Memory refreshed", "vault_files", len(vault), "healthy", healthy)
return result, nil
}
+351
View File
@@ -0,0 +1,351 @@
package memory
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"go.temporal.io/sdk/testsuite"
)
func TestActivityCreateKnowledge(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(IngestResponse{ID: "chunk-123"})
}))
defer server.Close()
suite := &testsuite.WorkflowTestSuite{}
env := suite.NewTestActivityEnvironment()
svc := NewService(server.URL, "test-token", "poimen")
activities := NewActivities(svc)
env.RegisterActivity(activities.CreateKnowledgeActivity)
record := &KnowledgeRecord{
Level: "L1",
Content: "test",
}
result, err := env.ExecuteActivity(activities.CreateKnowledgeActivity, record)
if err != nil {
t.Fatalf("activity failed: %v", err)
}
var id string
if err := result.Get(&id); err != nil {
t.Fatalf("get result failed: %v", err)
}
if id != "chunk-123" {
t.Errorf("expected chunk-123, got %s", id)
}
}
func TestActivitySearchKnowledge(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(QueryResponse{
Results: []QueryResult{
{
ID: "chunk-123",
Text: "matching knowledge",
},
},
})
}))
defer server.Close()
suite := &testsuite.WorkflowTestSuite{}
env := suite.NewTestActivityEnvironment()
svc := NewService(server.URL, "test-token", "poimen")
activities := NewActivities(svc)
env.RegisterActivity(activities.SearchKnowledgeActivity)
result, err := env.ExecuteActivity(activities.SearchKnowledgeActivity, "test query", nil)
if err != nil {
t.Fatalf("activity failed: %v", err)
}
var records []KnowledgeRecord
if err := result.Get(&records); err != nil {
t.Fatalf("get result failed: %v", err)
}
if len(records) != 1 {
t.Errorf("expected 1 record, got %d", len(records))
}
}
func TestActivityGetContext(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(ContextResponse{
Tier: 1,
Lessons: []ContextLesson{
{
Tier: 1,
Text: "lesson text",
},
},
})
}))
defer server.Close()
suite := &testsuite.WorkflowTestSuite{}
env := suite.NewTestActivityEnvironment()
svc := NewService(server.URL, "test-token", "poimen")
activities := NewActivities(svc)
env.RegisterActivity(activities.GetContextActivity)
result, err := env.ExecuteActivity(activities.GetContextActivity, "kubectl", "debug", 8192)
if err != nil {
t.Fatalf("activity failed: %v", err)
}
var ctx *ServiceContext
if err := result.Get(&ctx); err != nil {
t.Fatalf("get result failed: %v", err)
}
if ctx.Tier != 1 {
t.Errorf("expected tier 1, got %d", ctx.Tier)
}
if len(ctx.Lessons) != 1 {
t.Errorf("expected 1 lesson, got %d", len(ctx.Lessons))
}
}
func TestActivityDiagnoseIssue(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(ContextResponse{
Tier: 1,
Lessons: []ContextLesson{
{
Tier: 1,
Text: "diagnosis: check logs",
},
},
Skills: []ContextSkill{
{
Name: "debug-skill",
Why: "matched",
},
},
})
}))
defer server.Close()
suite := &testsuite.WorkflowTestSuite{}
env := suite.NewTestActivityEnvironment()
svc := NewService(server.URL, "test-token", "poimen")
activities := NewActivities(svc)
env.RegisterActivity(activities.DiagnoseIssueActivity)
result, err := env.ExecuteActivity(activities.DiagnoseIssueActivity, "kubectl", "pod-crash")
if err != nil {
t.Fatalf("activity failed: %v", err)
}
var recommendations []string
if err := result.Get(&recommendations); err != nil {
t.Fatalf("get result failed: %v", err)
}
if len(recommendations) == 0 {
t.Error("expected recommendations")
}
}
func TestActivityAnalyzeError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(QueryResponse{
Results: []QueryResult{
{
ID: "chunk-123",
Level: "L1",
Text: "solution: restart pod",
},
},
})
}))
defer server.Close()
suite := &testsuite.WorkflowTestSuite{}
env := suite.NewTestActivityEnvironment()
svc := NewService(server.URL, "test-token", "poimen")
activities := NewActivities(svc)
env.RegisterActivity(activities.AnalyzeErrorActivity)
result, err := env.ExecuteActivity(activities.AnalyzeErrorActivity, "CrashLoopBackOff")
if err != nil {
t.Fatalf("activity failed: %v", err)
}
var records []KnowledgeRecord
if err := result.Get(&records); err != nil {
t.Fatalf("get result failed: %v", err)
}
if len(records) != 1 {
t.Errorf("expected 1 record, got %d", len(records))
}
}
func TestActivityHealthCheck(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
suite := &testsuite.WorkflowTestSuite{}
env := suite.NewTestActivityEnvironment()
svc := NewService(server.URL, "test-token", "poimen")
activities := NewActivities(svc)
env.RegisterActivity(activities.HealthCheckActivity)
result, err := env.ExecuteActivity(activities.HealthCheckActivity)
if err != nil {
t.Fatalf("activity failed: %v", err)
}
var healthy bool
if err := result.Get(&healthy); err != nil {
t.Fatalf("get result failed: %v", err)
}
if !healthy {
t.Error("expected healthy")
}
}
func TestActivityLearnFromExecution(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(IngestResponse{ID: "chunk-456"})
}))
defer server.Close()
suite := &testsuite.WorkflowTestSuite{}
env := suite.NewTestActivityEnvironment()
svc := NewService(server.URL, "test-token", "poimen")
activities := NewActivities(svc)
env.RegisterActivity(activities.LearnFromExecutionActivity)
result, err := env.ExecuteActivity(activities.LearnFromExecutionActivity, "task-123", "success", []string{"tag1"})
if err != nil {
t.Fatalf("activity failed: %v", err)
}
var id string
if err := result.Get(&id); err != nil {
t.Fatalf("get result failed: %v", err)
}
if id != "chunk-456" {
t.Errorf("expected chunk-456, got %s", id)
}
}
func TestActivityDocumentDecision(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(IngestResponse{ID: "chunk-789"})
}))
defer server.Close()
suite := &testsuite.WorkflowTestSuite{}
env := suite.NewTestActivityEnvironment()
svc := NewService(server.URL, "test-token", "poimen")
activities := NewActivities(svc)
env.RegisterActivity(activities.DocumentDecisionActivity)
result, err := env.ExecuteActivity(
activities.DocumentDecisionActivity,
"scaling",
"scale to 5 replicas",
"high CPU usage",
)
if err != nil {
t.Fatalf("activity failed: %v", err)
}
var id string
if err := result.Get(&id); err != nil {
t.Fatalf("get result failed: %v", err)
}
if id != "chunk-789" {
t.Errorf("expected chunk-789, got %s", id)
}
}
func TestActivityOptions(t *testing.T) {
opts := DefaultActivityOptions()
if opts.RetryAttempts != 3 {
t.Errorf("expected 3 retry attempts, got %d", opts.RetryAttempts)
}
if opts.StartTimeout == 0 {
t.Error("expected non-zero start timeout")
}
}
func TestActivityError(t *testing.T) {
err := &MemoryActivityError{
ActivityName: "test-activity",
Attempt: 2,
Err: context.Canceled,
}
msg := err.Error()
if msg == "" {
t.Error("expected error message")
}
if !contains(msg, "test-activity") {
t.Error("expected activity name in error")
}
if !contains(msg, "attempt 2") {
t.Error("expected attempt number in error")
}
}
func contains(s, substr string) bool {
for i := 0; i < len(s)-len(substr)+1; i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
+296
View File
@@ -0,0 +1,296 @@
package memory
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// Client memory service client with JWT auth
type Client struct {
baseURL string
httpClient *http.Client
token string
}
// NewClient creates memory service client
func NewClient(baseURL, token string) *Client {
return &Client{
baseURL: baseURL,
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
token: token,
}
}
// IngestRequest ingest knowledge record
type IngestRequest struct {
Project string `json:"project"`
Source string `json:"source"`
Kind string `json:"kind"` // L1|L2|reference
Text string `json:"text"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// IngestResponse ingest response
type IngestResponse struct {
ID string `json:"id"`
SHA256 string `json:"sha256"`
QueueStatus string `json:"queue_status"`
IdempotencyID string `json:"idempotency_key"`
}
// Ingest creates knowledge record
func (c *Client) Ingest(ctx context.Context, req *IngestRequest) (*IngestResponse, error) {
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal ingest request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/memory/ingest", bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
c.setAuthHeader(httpReq)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("ingest request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("ingest failed (%d): %s", resp.StatusCode, string(body))
}
var result IngestResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode ingest response: %w", err)
}
return &result, nil
}
// QueryRequest query memory
type QueryRequest struct {
Project string `json:"project"`
Query string `json:"query"`
LevelFilter []string `json:"level_filter,omitempty"` // L1, L2, R
Floor float32 `json:"floor,omitempty"`
Limit int `json:"limit,omitempty"`
Scope string `json:"scope,omitempty"` // learned|reference|all
}
// QueryResult single search result
type QueryResult struct {
ID string `json:"id"`
Level string `json:"level"`
Score float32 `json:"score"`
SemanticScore float32 `json:"semantic_score"`
LexicalScore float32 `json:"lexical_score"`
Text string `json:"text"`
Breadcrumb string `json:"breadcrumb"`
Source string `json:"source"`
}
// QueryResponse query response
type QueryResponse struct {
Query string `json:"query"`
Results []QueryResult `json:"results"`
TotalHits int `json:"total_hits"`
SearchTimeMS int `json:"search_time_ms"`
}
// Query searches knowledge
func (c *Client) Query(ctx context.Context, req *QueryRequest) (*QueryResponse, error) {
if req.Limit == 0 {
req.Limit = 10
}
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal query request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/memory/query", bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
c.setAuthHeader(httpReq)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("query request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("query failed (%d): %s", resp.StatusCode, string(body))
}
var result QueryResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode query response: %w", err)
}
return &result, nil
}
// ContextRequest retrieve context (three-tier)
type ContextRequest struct {
Project string `json:"project"`
Tool string `json:"tool"`
Task string `json:"task"`
SignatureSource string `json:"signature_source"`
Scope string `json:"scope,omitempty"` // tool_context
Budget int `json:"budget,omitempty"`
}
// ContextLesson lesson from context
type ContextLesson struct {
Tier int `json:"tier"`
Level string `json:"level"`
Score float32 `json:"score"`
Text string `json:"text"`
MatchedKind string `json:"matched_kind,omitempty"`
SeenCount int `json:"seen_count,omitempty"`
LastSeen string `json:"last_seen,omitempty"`
}
// ContextSkill skill suggestion
type ContextSkill struct {
Name string `json:"name"`
Why string `json:"why"`
}
// ContextBudget budget tracking
type ContextBudget struct {
Requested int `json:"requested"`
Used int `json:"used"`
Dropped int `json:"dropped"`
Degradation *string `json:"degradation"`
}
// ContextResponse context response
type ContextResponse struct {
Tier int `json:"tier"`
Lessons []ContextLesson `json:"lessons"`
Skills []ContextSkill `json:"skills"`
Budget ContextBudget `json:"budget"`
}
// Context retrieves context (three-tier retrieval)
func (c *Client) Context(ctx context.Context, req *ContextRequest) (*ContextResponse, error) {
if req.Budget == 0 {
req.Budget = 8192
}
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal context request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/memory/context", bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
c.setAuthHeader(httpReq)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("context request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("context failed (%d): %s", resp.StatusCode, string(body))
}
var result ContextResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode context response: %w", err)
}
return &result, nil
}
// VaultFile file in vault
type VaultFile struct {
Path string `json:"path"`
Title string `json:"title"`
Level string `json:"level"`
UpdatedAt string `json:"updated_at"`
RecordCount int `json:"record_count"`
}
// VaultResponse vault browse response
type VaultResponse struct {
Project string `json:"project"`
Files []VaultFile `json:"files"`
TotalRecords int `json:"total_records"`
}
// Vault browses vault files
func (c *Client) Vault(ctx context.Context, project string) (*VaultResponse, error) {
httpReq, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/memory/vault?project=%s", c.baseURL, project), nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
c.setAuthHeader(httpReq)
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("vault request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("vault failed (%d): %s", resp.StatusCode, string(body))
}
var result VaultResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode vault response: %w", err)
}
return &result, nil
}
// setAuthHeader sets JWT Bearer token
func (c *Client) setAuthHeader(req *http.Request) {
if c.token != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token))
}
}
// Health checks memory service
func (c *Client) Health(ctx context.Context) (bool, error) {
httpReq, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/health", nil)
if err != nil {
return false, err
}
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return false, err
}
defer resp.Body.Close()
return resp.StatusCode == http.StatusOK, nil
}
+196
View File
@@ -0,0 +1,196 @@
package memory
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestClientIngest(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/memory/ingest" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
if r.Header.Get("Authorization") == "" {
t.Error("missing Authorization header")
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(IngestResponse{
ID: "chunk-123",
SHA256: "abc123",
QueueStatus: "pending",
})
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
resp, err := client.Ingest(context.Background(), &IngestRequest{
Project: "poimen",
Source: "test",
Kind: "L1",
Text: "test content",
})
if err != nil {
t.Fatalf("ingest failed: %v", err)
}
if resp.ID != "chunk-123" {
t.Errorf("expected ID chunk-123, got %s", resp.ID)
}
}
func TestClientQuery(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/memory/query" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(QueryResponse{
Query: "test query",
TotalHits: 1,
SearchTimeMS: 100,
Results: []QueryResult{
{
ID: "chunk-123",
Level: "L1",
Score: 0.95,
Text: "matching result",
},
},
})
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
resp, err := client.Query(context.Background(), &QueryRequest{
Project: "poimen",
Query: "test query",
Limit: 10,
})
if err != nil {
t.Fatalf("query failed: %v", err)
}
if len(resp.Results) != 1 {
t.Errorf("expected 1 result, got %d", len(resp.Results))
}
if resp.Results[0].Text != "matching result" {
t.Errorf("unexpected result text")
}
}
func TestClientContext(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/memory/context" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(ContextResponse{
Tier: 1,
Lessons: []ContextLesson{
{
Tier: 1,
Level: "L1",
Score: 1.0,
Text: "tier-1 lesson",
},
},
Budget: ContextBudget{
Requested: 8192,
Used: 100,
Dropped: 0,
},
})
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
resp, err := client.Context(context.Background(), &ContextRequest{
Project: "poimen",
Tool: "kubectl",
Task: "debug",
SignatureSource: "log",
})
if err != nil {
t.Fatalf("context failed: %v", err)
}
if resp.Tier != 1 {
t.Errorf("expected tier 1, got %d", resp.Tier)
}
if len(resp.Lessons) != 1 {
t.Errorf("expected 1 lesson, got %d", len(resp.Lessons))
}
}
func TestClientVault(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/memory/vault" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(VaultResponse{
Project: "poimen",
TotalRecords: 42,
Files: []VaultFile{
{
Path: "test.md",
Title: "Test",
Level: "L1",
RecordCount: 5,
},
},
})
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
resp, err := client.Vault(context.Background(), "poimen")
if err != nil {
t.Fatalf("vault failed: %v", err)
}
if len(resp.Files) != 1 {
t.Errorf("expected 1 file, got %d", len(resp.Files))
}
if resp.TotalRecords != 42 {
t.Errorf("expected 42 records, got %d", resp.TotalRecords)
}
}
func TestClientHealth(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/health" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
ok, err := client.Health(context.Background())
if err != nil {
t.Fatalf("health check failed: %v", err)
}
if !ok {
t.Error("expected health check to pass")
}
}
+134
View File
@@ -0,0 +1,134 @@
package memory
import (
"context"
"fmt"
)
// ExampleActivity demonstrates memory service integration in workflows
// This can be used as a template for workflow activities
// LearnTaskActivity learns from task execution
func LearnTaskActivity(ctx context.Context, service *Service, task string, result string) error {
// Create knowledge from task result
knowledgeID, err := service.CreateKnowledge(ctx, &KnowledgeRecord{
Level: "L1",
Title: fmt.Sprintf("Task: %s", task),
Content: result,
Source: fmt.Sprintf("workflow://task/%s", task),
Metadata: map[string]interface{}{
"task": task,
"type": "execution_result",
},
})
if err != nil {
return fmt.Errorf("learn task: %w", err)
}
fmt.Printf("Learned: %s\n", knowledgeID)
return nil
}
// DiagnosticActivity retrieves context for problem diagnosis
func DiagnosticActivity(ctx context.Context, service *Service, tool string, issue string) ([]string, error) {
// Retrieve context for tool/issue
svcCtx, err := service.RetrieveContext(ctx, tool, issue, 8192)
if err != nil {
return nil, fmt.Errorf("diagnose: %w", err)
}
// Extract lessons
diagnostics := make([]string, 0)
for _, lesson := range svcCtx.Lessons {
if lesson.Tier == 1 {
diagnostics = append(diagnostics, lesson.Text)
}
}
// Extract skills
for _, skill := range svcCtx.Skills {
diagnostics = append(diagnostics, fmt.Sprintf("Skill: %s (%s)", skill.Name, skill.Why))
}
return diagnostics, nil
}
// DocumentationActivity searches vault for relevant docs
func DocumentationActivity(ctx context.Context, service *Service, topic string) ([]string, error) {
// Retrieve vault files
files, err := service.GetVault(ctx)
if err != nil {
return nil, fmt.Errorf("get vault: %w", err)
}
// Filter by topic
results := make([]string, 0)
for _, file := range files {
if file.Level == "R" { // Reference docs
results = append(results, fmt.Sprintf("%s: %s", file.Title, file.Path))
}
}
return results, nil
}
// SearchKnowledgeActivity searches knowledge base
func SearchKnowledgeActivity(ctx context.Context, service *Service, query string) ([]string, error) {
records, err := service.RetrieveKnowledge(ctx, query, &RetrievalOptions{
LevelFilter: []string{"L1", "L2"},
Limit: 5,
Floor: 0.7,
})
if err != nil {
return nil, fmt.Errorf("search: %w", err)
}
results := make([]string, 0)
for _, record := range records {
results = append(results, fmt.Sprintf("[%s] %s", record.Level, record.Content))
}
return results, nil
}
// UpdateLessonActivity updates learned facts
func UpdateLessonActivity(ctx context.Context, service *Service, id string, newContent string) error {
_, err := service.UpdateKnowledge(ctx, &KnowledgeRecord{
ID: id,
Level: "L2",
Content: newContent,
})
return err
}
// HealthCheckActivity checks memory service health
func HealthCheckActivity(ctx context.Context, service *Service) (bool, error) {
return service.IsHealthy(ctx), nil
}
// Example workflow structure using memory service
type WorkflowWithMemory struct {
MemoryService *Service
}
// ExecuteWithLearning executes task and learns from it
func (w *WorkflowWithMemory) ExecuteWithLearning(ctx context.Context, task string, executor func() (string, error)) error {
// Execute task
result, err := executor()
if err != nil {
return err
}
// Learn from result
return LearnTaskActivity(ctx, w.MemoryService, task, result)
}
// DiagnoseWithContext diagnoses issue using memory context
func (w *WorkflowWithMemory) DiagnoseWithContext(ctx context.Context, tool string, issue string) ([]string, error) {
return DiagnosticActivity(ctx, w.MemoryService, tool, issue)
}
// SearchKnowledge searches knowledge
func (w *WorkflowWithMemory) SearchKnowledge(ctx context.Context, query string) ([]string, error) {
return SearchKnowledgeActivity(ctx, w.MemoryService, query)
}
+234
View File
@@ -0,0 +1,234 @@
package memory
import (
"context"
"fmt"
)
// Service memory service manager
type Service struct {
client *Client
project string
}
// NewService creates memory service manager
func NewService(baseURL, token, project string) *Service {
return &Service{
client: NewClient(baseURL, token),
project: project,
}
}
// KnowledgeRecord high-level knowledge record
type KnowledgeRecord struct {
ID string
Level string // L1|L2|reference
Title string
Content string
Source string
Metadata map[string]interface{}
SHA256 string
}
// CreateKnowledge creates knowledge record
func (s *Service) CreateKnowledge(ctx context.Context, record *KnowledgeRecord) (string, error) {
if record.Level == "" {
record.Level = "L1"
}
if record.Source == "" {
record.Source = "workflow"
}
req := &IngestRequest{
Project: s.project,
Source: record.Source,
Kind: record.Level,
Text: record.Content,
Metadata: record.Metadata,
}
resp, err := s.client.Ingest(ctx, req)
if err != nil {
return "", fmt.Errorf("create knowledge: %w", err)
}
return resp.ID, nil
}
// UpdateKnowledge updates existing knowledge (re-ingest)
func (s *Service) UpdateKnowledge(ctx context.Context, record *KnowledgeRecord) (string, error) {
// Update done by re-ingesting with same signature/source
// Memory service deduplicates based on idempotency key
if record.Metadata == nil {
record.Metadata = make(map[string]interface{})
}
// Use ID as session_id for idempotency
record.Metadata["session_id"] = record.ID
return s.CreateKnowledge(ctx, record)
}
// RetrievalOptions search options
type RetrievalOptions struct {
LevelFilter []string // L1, L2, R
Floor float32 // minimum relevance
Limit int // default 10
Scope string // learned|reference|all
}
// RetrieveKnowledge searches knowledge
func (s *Service) RetrieveKnowledge(ctx context.Context, query string, opts *RetrievalOptions) ([]KnowledgeRecord, error) {
if opts == nil {
opts = &RetrievalOptions{}
}
if opts.Limit == 0 {
opts.Limit = 10
}
req := &QueryRequest{
Project: s.project,
Query: query,
LevelFilter: opts.LevelFilter,
Floor: opts.Floor,
Limit: opts.Limit,
Scope: opts.Scope,
}
resp, err := s.client.Query(ctx, req)
if err != nil {
return nil, fmt.Errorf("retrieve knowledge: %w", err)
}
records := make([]KnowledgeRecord, len(resp.Results))
for i, r := range resp.Results {
records[i] = KnowledgeRecord{
ID: r.ID,
Level: r.Level,
Content: r.Text,
Source: r.Source,
SHA256: "", // Not in response
Metadata: map[string]interface{}{
"score": r.Score,
"semantic_score": r.SemanticScore,
"lexical_score": r.LexicalScore,
"breadcrumb": r.Breadcrumb,
},
}
}
return records, nil
}
// ServiceContext tool/task context
type ServiceContext struct {
Tier int
Lessons []Lesson
Skills []Skill
BudgetUsed int
BudgetMax int
}
// Lesson learned fact or reference
type Lesson struct {
Tier int
Level string
Score float32
Text string
MatchedKind string
SeenCount int
LastSeen string
}
// Skill recommended action
type Skill struct {
Name string
Why string
}
// RetrieveContext retrieves context for tool/task (three-tier)
func (s *Service) RetrieveContext(ctx context.Context, tool, task string, budget int) (*ServiceContext, error) {
if budget == 0 {
budget = 8192
}
req := &ContextRequest{
Project: s.project,
Tool: tool,
Task: task,
SignatureSource: fmt.Sprintf("%s:%s", tool, task),
Scope: "tool_context",
Budget: budget,
}
resp, err := s.client.Context(ctx, req)
if err != nil {
return nil, fmt.Errorf("retrieve context: %w", err)
}
lessons := make([]Lesson, len(resp.Lessons))
for i, l := range resp.Lessons {
lessons[i] = Lesson{
Tier: l.Tier,
Level: l.Level,
Score: l.Score,
Text: l.Text,
MatchedKind: l.MatchedKind,
SeenCount: l.SeenCount,
LastSeen: l.LastSeen,
}
}
skills := make([]Skill, len(resp.Skills))
for i, sk := range resp.Skills {
skills[i] = Skill{
Name: sk.Name,
Why: sk.Why,
}
}
return &ServiceContext{
Tier: resp.Tier,
Lessons: lessons,
Skills: skills,
BudgetUsed: resp.Budget.Used,
BudgetMax: resp.Budget.Requested,
}, nil
}
// VaultInfo vault browsing
type VaultInfo struct {
Path string
Title string
Level string
UpdatedAt string
RecordCount int
}
// GetVault lists vault files
func (s *Service) GetVault(ctx context.Context) ([]VaultInfo, error) {
resp, err := s.client.Vault(ctx, s.project)
if err != nil {
return nil, fmt.Errorf("get vault: %w", err)
}
files := make([]VaultInfo, len(resp.Files))
for i, f := range resp.Files {
files[i] = VaultInfo{
Path: f.Path,
Title: f.Title,
Level: f.Level,
UpdatedAt: f.UpdatedAt,
RecordCount: f.RecordCount,
}
}
return files, nil
}
// IsHealthy checks service health
func (s *Service) IsHealthy(ctx context.Context) bool {
ok, err := s.client.Health(ctx)
return ok && err == nil
}
+254
View File
@@ -0,0 +1,254 @@
package memory
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestServiceCreateKnowledge(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/memory/ingest" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
var req IngestRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Fatalf("decode request: %v", err)
}
if req.Project != "poimen" {
t.Errorf("expected project poimen, got %s", req.Project)
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(IngestResponse{
ID: "chunk-123",
QueueStatus: "pending",
})
}))
defer server.Close()
svc := NewService(server.URL, "test-token", "poimen")
id, err := svc.CreateKnowledge(context.Background(), &KnowledgeRecord{
Content: "test knowledge",
Level: "L1",
})
if err != nil {
t.Fatalf("create knowledge failed: %v", err)
}
if id != "chunk-123" {
t.Errorf("expected ID chunk-123, got %s", id)
}
}
func TestServiceRetrieveKnowledge(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/memory/query" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(QueryResponse{
Query: "test",
Results: []QueryResult{
{
ID: "chunk-123",
Level: "L1",
Score: 0.95,
SemanticScore: 0.96,
LexicalScore: 0.94,
Text: "knowledge content",
Breadcrumb: "path > to > doc",
Source: "test",
},
},
TotalHits: 1,
SearchTimeMS: 50,
})
}))
defer server.Close()
svc := NewService(server.URL, "test-token", "poimen")
records, err := svc.RetrieveKnowledge(context.Background(), "test", nil)
if err != nil {
t.Fatalf("retrieve knowledge failed: %v", err)
}
if len(records) != 1 {
t.Errorf("expected 1 record, got %d", len(records))
}
if records[0].Content != "knowledge content" {
t.Errorf("unexpected content")
}
meta := records[0].Metadata
if score, ok := meta["score"].(float32); ok {
if score != 0.95 {
t.Errorf("expected score 0.95, got %f", score)
}
}
}
func TestServiceRetrieveContext(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/memory/context" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(ContextResponse{
Tier: 1,
Lessons: []ContextLesson{
{
Tier: 1,
Level: "L1",
Score: 1.0,
Text: "first lesson",
MatchedKind: "signature",
},
{
Tier: 2,
Level: "L2",
Score: 0.87,
Text: "second lesson",
},
},
Skills: []ContextSkill{
{
Name: "debug-skill",
Why: "tier 1 matched",
},
},
Budget: ContextBudget{
Requested: 8192,
Used: 2048,
Dropped: 0,
},
})
}))
defer server.Close()
svc := NewService(server.URL, "test-token", "poimen")
ctx, err := svc.RetrieveContext(context.Background(), "kubectl", "debug-pod", 8192)
if err != nil {
t.Fatalf("retrieve context failed: %v", err)
}
if ctx.Tier != 1 {
t.Errorf("expected tier 1, got %d", ctx.Tier)
}
if len(ctx.Lessons) != 2 {
t.Errorf("expected 2 lessons, got %d", len(ctx.Lessons))
}
if len(ctx.Skills) != 1 {
t.Errorf("expected 1 skill, got %d", len(ctx.Skills))
}
if ctx.BudgetUsed != 2048 {
t.Errorf("expected budget used 2048, got %d", ctx.BudgetUsed)
}
}
func TestServiceGetVault(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/memory/vault" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(VaultResponse{
Project: "poimen",
TotalRecords: 100,
Files: []VaultFile{
{
Path: "docs/guide.md",
Title: "Guide",
Level: "L1",
RecordCount: 25,
},
{
Path: "reference/api.md",
Title: "API",
Level: "R",
RecordCount: 75,
},
},
})
}))
defer server.Close()
svc := NewService(server.URL, "test-token", "poimen")
files, err := svc.GetVault(context.Background())
if err != nil {
t.Fatalf("get vault failed: %v", err)
}
if len(files) != 2 {
t.Errorf("expected 2 files, got %d", len(files))
}
if files[0].Title != "Guide" {
t.Errorf("expected title Guide, got %s", files[0].Title)
}
}
func TestServiceIsHealthy(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
svc := NewService(server.URL, "test-token", "poimen")
if !svc.IsHealthy(context.Background()) {
t.Error("expected service to be healthy")
}
}
func TestServiceUpdateKnowledge(t *testing.T) {
callCount := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
if r.URL.Path != "/memory/ingest" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(IngestResponse{
ID: "chunk-123",
QueueStatus: "pending",
})
}))
defer server.Close()
svc := NewService(server.URL, "test-token", "poimen")
record := &KnowledgeRecord{
ID: "chunk-123",
Content: "updated knowledge",
Level: "L1",
}
id, err := svc.UpdateKnowledge(context.Background(), record)
if err != nil {
t.Fatalf("update knowledge failed: %v", err)
}
if id != "chunk-123" {
t.Errorf("expected ID chunk-123, got %s", id)
}
if callCount != 1 {
t.Errorf("expected 1 call, got %d", callCount)
}
}
+333
View File
@@ -0,0 +1,333 @@
package memory
import (
"context"
"fmt"
"time"
"go.temporal.io/sdk/activity"
"go.temporal.io/sdk/temporal"
"go.temporal.io/sdk/worker"
"go.temporal.io/sdk/workflow"
)
// RegisterMemoryActivities registers all memory service activities with worker
func RegisterMemoryActivities(w worker.Worker, service *Service) {
activities := NewActivities(service)
// Register activities (activity name = "ActivityName" → "activityName")
w.RegisterActivity(activities.CreateKnowledgeActivity)
w.RegisterActivity(activities.UpdateKnowledgeActivity)
w.RegisterActivity(activities.SearchKnowledgeActivity)
w.RegisterActivity(activities.GetContextActivity)
w.RegisterActivity(activities.GetVaultActivity)
w.RegisterActivity(activities.HealthCheckActivity)
w.RegisterActivity(activities.LearnFromExecutionActivity)
w.RegisterActivity(activities.DiagnoseIssueActivity)
w.RegisterActivity(activities.AnalyzeErrorActivity)
w.RegisterActivity(activities.DocumentDecisionActivity)
w.RegisterActivity(activities.SearchAndApplyActivity)
w.RegisterActivity(activities.RefreshMemoryActivity)
}
// ActivityOptions memory service activity options
type ActivityOptions struct {
RetryAttempts int
RetryBackoff time.Duration
StartTimeout time.Duration
HeartbeatRate time.Duration
}
// DefaultActivityOptions returns sensible defaults
func DefaultActivityOptions() *ActivityOptions {
return &ActivityOptions{
RetryAttempts: 3,
RetryBackoff: time.Second,
StartTimeout: 30 * time.Second,
HeartbeatRate: 10 * time.Second,
}
}
// ExecuteCreateKnowledge wrapper for CreateKnowledgeActivity
func ExecuteCreateKnowledge(
ctx workflow.Context,
record *KnowledgeRecord,
opts *ActivityOptions,
) (string, error) {
if opts == nil {
opts = DefaultActivityOptions()
}
activityCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
ScheduleToCloseTimeout: 2 * time.Minute,
StartToCloseTimeout: time.Minute,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: opts.RetryBackoff,
BackoffCoefficient: 2.0,
MaximumInterval: 30 * time.Second,
MaximumAttempts: int32(opts.RetryAttempts),
NonRetryableErrorTypes: []string{},
},
})
var result string
err := workflow.ExecuteActivity(activityCtx, "CreateKnowledgeActivity", record).Get(activityCtx, &result)
return result, err
}
// ExecuteSearchKnowledge wrapper for SearchKnowledgeActivity
func ExecuteSearchKnowledge(
ctx workflow.Context,
query string,
opts *RetrievalOptions,
activityOpts *ActivityOptions,
) ([]KnowledgeRecord, error) {
if activityOpts == nil {
activityOpts = DefaultActivityOptions()
}
activityCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
ScheduleToCloseTimeout: 3 * time.Minute,
StartToCloseTimeout: 2 * time.Minute,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: activityOpts.RetryBackoff,
BackoffCoefficient: 2.0,
MaximumInterval: 30 * time.Second,
MaximumAttempts: int32(activityOpts.RetryAttempts),
},
})
var result []KnowledgeRecord
err := workflow.ExecuteActivity(activityCtx, "SearchKnowledgeActivity", query, opts).Get(activityCtx, &result)
return result, err
}
// ExecuteGetContext wrapper for GetContextActivity
func ExecuteGetContext(
ctx workflow.Context,
tool, task string,
budget int,
opts *ActivityOptions,
) (*ServiceContext, error) {
if opts == nil {
opts = DefaultActivityOptions()
}
activityCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
ScheduleToCloseTimeout: 3 * time.Minute,
StartToCloseTimeout: 2 * time.Minute,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: opts.RetryBackoff,
BackoffCoefficient: 2.0,
MaximumInterval: 30 * time.Second,
MaximumAttempts: int32(opts.RetryAttempts),
},
})
var result *ServiceContext
err := workflow.ExecuteActivity(activityCtx, "GetContextActivity", tool, task, budget).Get(activityCtx, &result)
return result, err
}
// ExecuteDiagnoseIssue wrapper for DiagnoseIssueActivity
func ExecuteDiagnoseIssue(
ctx workflow.Context,
tool, issue string,
opts *ActivityOptions,
) ([]string, error) {
if opts == nil {
opts = DefaultActivityOptions()
}
activityCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
ScheduleToCloseTimeout: 2 * time.Minute,
StartToCloseTimeout: time.Minute,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: opts.RetryBackoff,
BackoffCoefficient: 2.0,
MaximumInterval: 30 * time.Second,
MaximumAttempts: int32(opts.RetryAttempts),
},
})
var result []string
err := workflow.ExecuteActivity(activityCtx, "DiagnoseIssueActivity", tool, issue).Get(activityCtx, &result)
return result, err
}
// ExecuteAnalyzeError wrapper for AnalyzeErrorActivity
func ExecuteAnalyzeError(
ctx workflow.Context,
errorMsg string,
opts *ActivityOptions,
) ([]KnowledgeRecord, error) {
if opts == nil {
opts = DefaultActivityOptions()
}
activityCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
ScheduleToCloseTimeout: 2 * time.Minute,
StartToCloseTimeout: time.Minute,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: opts.RetryBackoff,
BackoffCoefficient: 2.0,
MaximumInterval: 30 * time.Second,
MaximumAttempts: int32(opts.RetryAttempts),
},
})
var result []KnowledgeRecord
err := workflow.ExecuteActivity(activityCtx, "AnalyzeErrorActivity", errorMsg).Get(activityCtx, &result)
return result, err
}
// ExecuteHealthCheck wrapper for HealthCheckActivity
func ExecuteHealthCheck(
ctx workflow.Context,
opts *ActivityOptions,
) (bool, error) {
if opts == nil {
opts = DefaultActivityOptions()
}
activityCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
ScheduleToCloseTimeout: 1 * time.Minute,
StartToCloseTimeout: 30 * time.Second,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: opts.RetryBackoff,
BackoffCoefficient: 2.0,
MaximumInterval: 15 * time.Second,
MaximumAttempts: int32(opts.RetryAttempts),
},
})
var result bool
err := workflow.ExecuteActivity(activityCtx, "HealthCheckActivity").Get(activityCtx, &result)
return result, err
}
// ExecuteLearnFromExecution wrapper for LearnFromExecutionActivity
func ExecuteLearnFromExecution(
ctx workflow.Context,
taskID, result string,
tags []string,
opts *ActivityOptions,
) (string, error) {
if opts == nil {
opts = DefaultActivityOptions()
}
activityCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
ScheduleToCloseTimeout: 2 * time.Minute,
StartToCloseTimeout: time.Minute,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: opts.RetryBackoff,
BackoffCoefficient: 2.0,
MaximumInterval: 30 * time.Second,
MaximumAttempts: int32(opts.RetryAttempts),
},
})
var recordID string
err := workflow.ExecuteActivity(activityCtx, "LearnFromExecutionActivity", taskID, result, tags).Get(activityCtx, &recordID)
return recordID, err
}
// ExecuteDocumentDecision wrapper for DocumentDecisionActivity
func ExecuteDocumentDecision(
ctx workflow.Context,
decisionType, decision, reasoning string,
opts *ActivityOptions,
) (string, error) {
if opts == nil {
opts = DefaultActivityOptions()
}
activityCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
ScheduleToCloseTimeout: 2 * time.Minute,
StartToCloseTimeout: time.Minute,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: opts.RetryBackoff,
BackoffCoefficient: 2.0,
MaximumInterval: 30 * time.Second,
MaximumAttempts: int32(opts.RetryAttempts),
},
})
var recordID string
err := workflow.ExecuteActivity(activityCtx, "DocumentDecisionActivity", decisionType, decision, reasoning).Get(activityCtx, &recordID)
return recordID, err
}
// ExecuteRefreshMemory wrapper for RefreshMemoryActivity
func ExecuteRefreshMemory(
ctx workflow.Context,
opts *ActivityOptions,
) (map[string]interface{}, error) {
if opts == nil {
opts = DefaultActivityOptions()
}
activityCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
ScheduleToCloseTimeout: 2 * time.Minute,
StartToCloseTimeout: time.Minute,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: opts.RetryBackoff,
BackoffCoefficient: 2.0,
MaximumInterval: 30 * time.Second,
MaximumAttempts: int32(opts.RetryAttempts),
},
})
var result map[string]interface{}
err := workflow.ExecuteActivity(activityCtx, "RefreshMemoryActivity").Get(activityCtx, &result)
return result, err
}
// HeartbeatMemoryActivity sends heartbeat every N seconds
// Usage: Long-running memory operations
func HeartbeatMemoryActivity(ctx context.Context, maxDuration time.Duration) error {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
deadline := time.Now().Add(maxDuration)
for {
select {
case <-ticker.C:
activity.RecordHeartbeat(ctx, time.Now())
case <-ctx.Done():
return ctx.Err()
}
if time.Now().After(deadline) {
break
}
}
return nil
}
// MemoryActivityError wraps errors with activity context
type MemoryActivityError struct {
ActivityName string
Attempt int
Err error
}
func (e *MemoryActivityError) Error() string {
return fmt.Sprintf("memory activity %s (attempt %d): %v", e.ActivityName, e.Attempt, e.Err)
}
// CaptureActivityError captures activity execution errors
func CaptureActivityError(activityName string, err error) error {
if err == nil {
return nil
}
return &MemoryActivityError{
ActivityName: activityName,
Attempt: 1,
Err: err,
}
}
+303
View File
@@ -0,0 +1,303 @@
package memory
import (
"fmt"
"go.temporal.io/sdk/workflow"
)
// LearningWorkflow learns from task execution
// Pattern: Execute → Learn → Document
func LearningWorkflow(ctx workflow.Context, taskID string, executor string) (string, error) {
// Execute task (placeholder - replace with actual activity)
taskResult := fmt.Sprintf("Task %s executed by %s", taskID, executor)
// Learn from execution
knowledgeID, err := ExecuteLearnFromExecution(
ctx,
taskID,
taskResult,
[]string{"execution", "learning"},
DefaultActivityOptions(),
)
if err != nil {
return "", fmt.Errorf("learn from execution: %w", err)
}
return knowledgeID, nil
}
// DiagnosticWorkflow diagnoses issue using memory service
// Pattern: Get Context → Extract Recommendations → Apply
func DiagnosticWorkflow(ctx workflow.Context, tool, issue string) ([]string, error) {
// Get recommendations
recommendations, err := ExecuteDiagnoseIssue(
ctx,
tool,
issue,
DefaultActivityOptions(),
)
if err != nil {
return nil, fmt.Errorf("diagnose: %w", err)
}
return recommendations, nil
}
// SearchAndApplyWorkflow searches knowledge and applies it
// Pattern: Search → Filter → Apply
func SearchAndApplyWorkflow(ctx workflow.Context, query string) ([]KnowledgeRecord, error) {
// Search knowledge
records, err := ExecuteSearchKnowledge(
ctx,
query,
&RetrievalOptions{
Limit: 10,
LevelFilter: []string{"L1", "L2"},
Floor: 0.7,
},
DefaultActivityOptions(),
)
if err != nil {
return nil, fmt.Errorf("search: %w", err)
}
return records, nil
}
// ContextualDecisionWorkflow makes decisions with memory context
// Pattern: Get Context → Make Decision → Document Decision
func ContextualDecisionWorkflow(ctx workflow.Context, tool, task string, decision string) (string, error) {
// Get context
svcCtx, err := ExecuteGetContext(
ctx,
tool,
task,
8192,
DefaultActivityOptions(),
)
if err != nil {
return "", fmt.Errorf("get context: %w", err)
}
// Build reasoning from lessons
reasoning := fmt.Sprintf("Based on %d lessons from memory service (tier %d)", len(svcCtx.Lessons), svcCtx.Tier)
// Document decision
docID, err := ExecuteDocumentDecision(
ctx,
tool,
decision,
reasoning,
DefaultActivityOptions(),
)
if err != nil {
return "", fmt.Errorf("document decision: %w", err)
}
return docID, nil
}
// ErrorRecoveryWorkflow analyzes error and searches for recovery
// Pattern: Error → Analyze → Search → Recover
func ErrorRecoveryWorkflow(ctx workflow.Context, errorMsg string) ([]string, error) {
// Analyze error
records, err := ExecuteAnalyzeError(
ctx,
errorMsg,
DefaultActivityOptions(),
)
if err != nil {
return nil, fmt.Errorf("analyze error: %w", err)
}
// Extract recovery recommendations
recommendations := make([]string, 0)
for _, record := range records {
if record.Level == "L1" { // High confidence
recommendations = append(recommendations, record.Content)
}
}
return recommendations, nil
}
// HealthAwareWorkflow checks health before proceeding
// Pattern: HealthCheck → Conditional Proceed
func HealthAwareWorkflow(ctx workflow.Context, taskID string) (bool, error) {
// Check health
healthy, err := ExecuteHealthCheck(ctx, DefaultActivityOptions())
if err != nil {
return false, fmt.Errorf("health check: %w", err)
}
if !healthy {
return false, fmt.Errorf("memory service unhealthy, skipping task %s", taskID)
}
return true, nil
}
// IterativeLearnWorkflow learns iteratively
// Pattern: Execute → Learn → Refine → Learn Again
func IterativeLearnWorkflow(ctx workflow.Context, topic string, iterations int) ([]string, error) {
knowledgeIDs := make([]string, 0)
for i := 0; i < iterations; i++ {
// Learn current iteration
id, err := ExecuteLearnFromExecution(
ctx,
fmt.Sprintf("%s-iteration-%d", topic, i+1),
fmt.Sprintf("Iteration %d: %s", i+1, topic),
[]string{"iteration", fmt.Sprintf("iteration-%d", i+1)},
DefaultActivityOptions(),
)
if err != nil {
return nil, fmt.Errorf("learn iteration %d: %w", i+1, err)
}
knowledgeIDs = append(knowledgeIDs, id)
// Search for related knowledge
records, err := ExecuteSearchKnowledge(
ctx,
topic,
&RetrievalOptions{Limit: 5, Floor: 0.6},
DefaultActivityOptions(),
)
if err != nil {
return nil, fmt.Errorf("search iteration %d: %w", i+1, err)
}
// Log found records
if len(records) > 0 {
workflow.GetLogger(ctx).Info("Iteration found related records", "iteration", i+1, "records", len(records))
}
}
return knowledgeIDs, nil
}
// ConditionalLearningWorkflow learns only on success
// Pattern: Execute → If Success → Learn
func ConditionalLearningWorkflow(ctx workflow.Context, taskID string, shouldSucceed bool) (string, error) {
if !shouldSucceed {
return "", fmt.Errorf("task failed, skipping learning")
}
// Only learn on success
result := fmt.Sprintf("Task %s succeeded", taskID)
id, err := ExecuteLearnFromExecution(
ctx,
taskID,
result,
[]string{"success"},
DefaultActivityOptions(),
)
if err != nil {
return "", fmt.Errorf("learn from success: %w", err)
}
return id, nil
}
// MultiStepWorkflow performs multiple memory operations
// Pattern: Create → Search → Context → Document
func MultiStepWorkflow(ctx workflow.Context, topic string) (map[string]interface{}, error) {
results := make(map[string]interface{})
// Step 1: Create knowledge
createID, err := ExecuteCreateKnowledge(
ctx,
&KnowledgeRecord{
Level: "L1",
Title: fmt.Sprintf("Initial: %s", topic),
Content: fmt.Sprintf("Starting workflow for %s", topic),
Source: "workflow://multi-step",
},
DefaultActivityOptions(),
)
if err != nil {
return nil, fmt.Errorf("create knowledge: %w", err)
}
results["created"] = createID
// Step 2: Search knowledge
searchRecords, err := ExecuteSearchKnowledge(
ctx,
topic,
&RetrievalOptions{Limit: 5},
DefaultActivityOptions(),
)
if err != nil {
return nil, fmt.Errorf("search knowledge: %w", err)
}
results["found"] = len(searchRecords)
// Step 3: Get context
svcCtx, err := ExecuteGetContext(
ctx,
"workflow",
topic,
8192,
DefaultActivityOptions(),
)
if err != nil {
return nil, fmt.Errorf("get context: %w", err)
}
results["context_tier"] = svcCtx.Tier
results["lessons"] = len(svcCtx.Lessons)
results["skills"] = len(svcCtx.Skills)
// Step 4: Document completion
docID, err := ExecuteDocumentDecision(
ctx,
"workflow_completion",
fmt.Sprintf("Completed multi-step workflow for %s", topic),
fmt.Sprintf("Found %d records, tier %d context", len(searchRecords), svcCtx.Tier),
DefaultActivityOptions(),
)
if err != nil {
return nil, fmt.Errorf("document completion: %w", err)
}
results["documented"] = docID
return results, nil
}
// ParallelLearnWorkflow learns from multiple sources in parallel
// Pattern: Execute Multiple Tasks in Parallel → Learn from Each
func ParallelLearnWorkflow(ctx workflow.Context, taskIDs []string) ([]string, error) {
// Create parallel activities
futures := make([]workflow.Future, len(taskIDs))
for i, taskID := range taskIDs {
// Execute each task in parallel
future := workflow.ExecuteActivity(
workflow.WithActivityOptions(
ctx,
workflow.ActivityOptions{
ScheduleToCloseTimeout: DefaultActivityOptions().RetryBackoff * 60,
StartToCloseTimeout: DefaultActivityOptions().RetryBackoff * 30,
},
),
"LearnFromExecutionActivity",
taskID,
fmt.Sprintf("Result from %s", taskID),
[]string{"parallel", taskID},
)
futures[i] = future
}
// Collect results
results := make([]string, len(futures))
for i, future := range futures {
err := future.Get(ctx, &results[i])
if err != nil {
return nil, fmt.Errorf("parallel learn task %d: %w", i, err)
}
}
return results, nil
}