diff --git a/pkg/brain/actions.go b/pkg/brain/actions.go index 3d572e5..f449cbb 100644 --- a/pkg/brain/actions.go +++ b/pkg/brain/actions.go @@ -42,6 +42,7 @@ func (s *DirectSubsystem) handleRemember(ctx context.Context, options core.Optio Content: actionStringValue(options, "content"), Type: actionStringValue(options, "type"), Tags: actionStringSliceValue(options, "tags"), + Org: actionStringValue(options, "org"), Project: actionStringValue(options, "project"), Confidence: actionFloatValue(options, "confidence"), Supersedes: actionStringValue(options, "supersedes"), @@ -98,6 +99,7 @@ func (s *DirectSubsystem) handleForget(ctx context.Context, options core.Options // )) func (s *DirectSubsystem) handleList(ctx context.Context, options core.Options) core.Result { input := ListInput{ + Org: actionStringValue(options, "org"), Project: actionStringValue(options, "project"), Type: actionStringValue(options, "type"), AgentID: actionStringValue(options, "agent_id", "agent"), @@ -163,6 +165,9 @@ func (s *DirectSubsystem) handleConversation(ctx context.Context, options core.O func recallFilterFromOptions(options core.Options) RecallFilter { filter := recallFilterValue(actionOptionValue(options, "filter")) + if filter.Org == "" { + filter.Org = actionStringValue(options, "org") + } if filter.Project == "" { filter.Project = actionStringValue(options, "project") } @@ -187,6 +192,7 @@ func recallFilterValue(value any) RecallFilter { Project: actionStringFromAny(typed["project"]), Type: typed["type"], AgentID: actionStringFromAny(typed["agent_id"]), + Org: actionStringFromAny(typed["org"]), MinConfidence: actionFloatFromAny(typed["min_confidence"]), } case map[string]string: @@ -194,6 +200,7 @@ func recallFilterValue(value any) RecallFilter { Project: actionStringFromAny(typed["project"]), Type: typed["type"], AgentID: actionStringFromAny(typed["agent_id"]), + Org: actionStringFromAny(typed["org"]), } default: if text := actionStringFromAny(value); text != "" { diff --git a/pkg/brain/actions_test.go b/pkg/brain/actions_test.go index 8b615cc..53b0210 100644 --- a/pkg/brain/actions_test.go +++ b/pkg/brain/actions_test.go @@ -37,6 +37,7 @@ func TestActions_HandleList_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "GET", r.Method) assert.Equal(t, "/v1/brain/list", r.URL.Path) + assert.Equal(t, "core", r.URL.Query().Get("org")) assert.Equal(t, "agent", r.URL.Query().Get("project")) assert.Equal(t, "decision", r.URL.Query().Get("type")) assert.Equal(t, "cladius", r.URL.Query().Get("agent_id")) @@ -69,6 +70,7 @@ func TestActions_HandleList_Good(t *testing.T) { require.True(t, result.OK) actionResult := c.Action("brain.list").Run(context.Background(), core.NewOptions( + core.Option{Key: "org", Value: "core"}, core.Option{Key: "project", Value: "agent"}, core.Option{Key: "type", Value: "decision"}, core.Option{Key: "agent_id", Value: "cladius"}, @@ -103,7 +105,7 @@ func TestActions_HandleList_Bad(t *testing.T) { require.False(t, actionResult.OK) err, ok := actionResult.Value.(error) require.True(t, ok) - assert.Contains(t, err.Error(), "API call failed") + assert.Contains(t, err.Error(), "upstream returned 500") } func TestActions_HandleRecall_Ugly_FilterMap(t *testing.T) { @@ -115,6 +117,7 @@ func TestActions_HandleRecall_Ugly_FilterMap(t *testing.T) { require.True(t, core.JSONUnmarshalString(core.ReadAll(r.Body).Value.(string), &body).OK) assert.Equal(t, "architecture", body["query"]) assert.Equal(t, float64(3), body["top_k"]) + assert.Equal(t, "core", body["org"]) assert.Equal(t, "agent", body["project"]) assert.Equal(t, "decision", body["type"]) assert.Equal(t, "clotho", body["agent_id"]) @@ -136,6 +139,7 @@ func TestActions_HandleRecall_Ugly_FilterMap(t *testing.T) { core.Option{Key: "query", Value: "architecture"}, core.Option{Key: "top_k", Value: 3}, core.Option{Key: "filter", Value: map[string]any{ + "org": "core", "project": "agent", "type": "decision", "agent_id": "clotho", diff --git a/pkg/brain/direct.go b/pkg/brain/direct.go index 06d3032..d84e331 100644 --- a/pkg/brain/direct.go +++ b/pkg/brain/direct.go @@ -9,6 +9,7 @@ import ( "dappco.re/go/agent/pkg/agentic" core "dappco.re/go/core" coremcp "dappco.re/go/mcp/pkg/mcp" + brainclient "dappco.re/go/mcp/pkg/mcp/brain/client" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -16,8 +17,9 @@ import ( // core.Println(subsystem.Name()) // "brain" type DirectSubsystem struct { *core.ServiceRuntime[DirectOptions] - apiURL string - apiKey string + apiURL string + apiKey string + apiClient *brainclient.Client } var _ coremcp.Subsystem = (*DirectSubsystem)(nil) @@ -48,8 +50,9 @@ func NewDirect() *DirectSubsystem { } return &DirectSubsystem{ - apiURL: apiURL, - apiKey: apiKey, + apiURL: apiURL, + apiKey: apiKey, + apiClient: newBrainClient(apiURL, apiKey), } } @@ -93,37 +96,10 @@ func brainKeyPath(home string) string { } func (s *DirectSubsystem) apiCall(ctx context.Context, method, path string, body any) core.Result { - if s.apiKey == "" { - return core.Result{ - Value: core.E("brain.apiCall", "no API key (set CORE_BRAIN_KEY or create ~/.claude/brain.key)", nil), - OK: false, - } + result, err := s.client().Call(ctx, method, path, body) + if err != nil { + return core.Result{Value: err, OK: false} } - - requestURL := core.Concat(s.apiURL, path) - var bodyStr string - if body != nil { - bodyStr = core.JSONMarshalString(body) - } - requestResult := agentic.HTTPDo(ctx, method, requestURL, bodyStr, s.apiKey, "Bearer") - if !requestResult.OK { - core.Error("brain API call failed", "method", method, "path", path) - if err, ok := requestResult.Value.(error); ok { - return core.Result{Value: core.E("brain.apiCall", "API call failed", err), OK: false} - } - if responseBody, ok := requestResult.Value.(string); ok && responseBody != "" { - return core.Result{Value: core.E("brain.apiCall", core.Concat("API call failed: ", core.Trim(responseBody)), nil), OK: false} - } - return core.Result{Value: core.E("brain.apiCall", "API call failed", nil), OK: false} - } - - var result map[string]any - if parseResult := core.JSONUnmarshalString(requestResult.Value.(string), &result); !parseResult.OK { - core.Error("brain API response parse failed", "method", method, "path", path) - err, _ := parseResult.Value.(error) - return core.Result{Value: core.E("brain.apiCall", "parse response", err), OK: false} - } - return core.Result{Value: result, OK: true} } @@ -132,11 +108,12 @@ func (s *DirectSubsystem) remember(ctx context.Context, _ *mcp.CallToolRequest, "content": input.Content, "type": input.Type, "tags": input.Tags, + "org": input.Org, "project": input.Project, "confidence": input.Confidence, "supersedes": input.Supersedes, "expires_in": input.ExpiresIn, - "agent_id": agentic.AgentName(), + "agent_id": directAgentID(), }) if !result.OK { err, _ := result.Value.(error) @@ -162,6 +139,9 @@ func (s *DirectSubsystem) recall(ctx context.Context, _ *mcp.CallToolRequest, in if input.Filter.Project != "" { body["project"] = input.Filter.Project } + if input.Filter.Org != "" { + body["org"] = input.Filter.Org + } if input.Filter.Type != nil { body["type"] = input.Filter.Type } @@ -204,6 +184,9 @@ func (s *DirectSubsystem) forget(ctx context.Context, _ *mcp.CallToolRequest, in func (s *DirectSubsystem) list(ctx context.Context, _ *mcp.CallToolRequest, input ListInput) (*mcp.CallToolResult, ListOutput, error) { var params []string + if input.Org != "" { + params = append(params, core.Concat("org=", core.URLEncode(input.Org))) + } if input.Project != "" { params = append(params, core.Concat("project=", core.URLEncode(input.Project))) } @@ -238,6 +221,29 @@ func (s *DirectSubsystem) list(ctx context.Context, _ *mcp.CallToolRequest, inpu }, nil } +func (s *DirectSubsystem) client() *brainclient.Client { + if s.apiClient == nil { + s.apiClient = newBrainClient(s.apiURL, s.apiKey) + } + return s.apiClient +} + +func newBrainClient(apiURL, apiKey string) *brainclient.Client { + return brainclient.New(brainclient.Options{ + URL: apiURL, + Key: apiKey, + Org: core.Trim(core.Env("CORE_BRAIN_ORG")), + AgentID: directAgentID(), + }) +} + +func directAgentID() string { + if configured := core.Trim(core.Env("CORE_BRAIN_AGENT_ID")); configured != "" { + return configured + } + return agentic.AgentName() +} + func memoriesFromPayload(payload map[string]any) []Memory { var memories []Memory source := payloadMap(payload) diff --git a/pkg/brain/direct_test.go b/pkg/brain/direct_test.go index f130b73..f4f0012 100644 --- a/pkg/brain/direct_test.go +++ b/pkg/brain/direct_test.go @@ -7,15 +7,27 @@ import ( "net/http" "net/http/httptest" "testing" + "time" core "dappco.re/go/core" + brainclient "dappco.re/go/mcp/pkg/mcp/brain/client" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // newTestDirect returns a DirectSubsystem wired to the given test server. func newTestDirect(srv *httptest.Server) *DirectSubsystem { - return &DirectSubsystem{apiURL: srv.URL, apiKey: "test-key"} + return &DirectSubsystem{ + apiURL: srv.URL, + apiKey: "test-key", + apiClient: brainclient.New(brainclient.Options{ + URL: srv.URL, + Key: "test-key", + HTTPClient: srv.Client(), + MaxAttempts: 1, + BaseDelay: time.Nanosecond, + }), + } } // jsonHandler returns an http.Handler that responds with the given JSON payload. @@ -151,7 +163,7 @@ func TestDirect_ApiCall_Bad_ServerError(t *testing.T) { require.False(t, result.OK) err, _ := result.Value.(error) require.Error(t, err) - assert.Contains(t, err.Error(), "API call failed") + assert.Contains(t, err.Error(), "upstream returned 500") } func TestDirect_ApiCall_Bad_InvalidJSON(t *testing.T) { @@ -169,12 +181,21 @@ func TestDirect_ApiCall_Bad_InvalidJSON(t *testing.T) { } func TestDirect_ApiCall_Bad_ConnectionRefused(t *testing.T) { - sub := &DirectSubsystem{apiURL: "http://127.0.0.1:1", apiKey: "test-key"} + sub := &DirectSubsystem{ + apiURL: "http://127.0.0.1:1", + apiKey: "test-key", + apiClient: brainclient.New(brainclient.Options{ + URL: "http://127.0.0.1:1", + Key: "test-key", + MaxAttempts: 1, + BaseDelay: time.Nanosecond, + }), + } result := sub.apiCall(context.Background(), "GET", "/v1/test", nil) require.False(t, result.OK) err, _ := result.Value.(error) require.Error(t, err) - assert.Contains(t, err.Error(), "API call failed") + assert.Contains(t, err.Error(), "request failed") } func TestDirect_ApiCall_Bad_BadRequest(t *testing.T) { @@ -185,12 +206,14 @@ func TestDirect_ApiCall_Bad_BadRequest(t *testing.T) { require.False(t, result.OK) err, _ := result.Value.(error) require.Error(t, err) - assert.Contains(t, err.Error(), "API call failed") + assert.Contains(t, err.Error(), "upstream returned 400") } // --- remember --- func TestDirect_Remember_Good(t *testing.T) { + t.Setenv("CORE_BRAIN_AGENT_ID", "codex") + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) assert.Equal(t, "/v1/brain/remember", r.URL.Path) @@ -199,6 +222,8 @@ func TestDirect_Remember_Good(t *testing.T) { core.JSONUnmarshalString(core.ReadAll(r.Body).Value.(string), &body) assert.Equal(t, "test content", body["content"]) assert.Equal(t, "observation", body["type"]) + assert.Equal(t, "core", body["org"]) + assert.Equal(t, "codex", body["agent_id"]) w.Header().Set("Content-Type", "application/json") w.Write([]byte(core.JSONMarshalString(map[string]any{ @@ -211,6 +236,7 @@ func TestDirect_Remember_Good(t *testing.T) { Content: "test content", Type: "observation", Tags: []string{"test"}, + Org: "core", Project: "core", }) require.NoError(t, err) @@ -254,6 +280,7 @@ func TestDirect_Recall_Good_WithMemories(t *testing.T) { var body map[string]any core.JSONUnmarshalString(core.ReadAll(r.Body).Value.(string), &body) assert.Equal(t, "architecture", body["query"]) + assert.Equal(t, "core", body["org"]) w.Header().Set("Content-Type", "application/json") w.Write([]byte(core.JSONMarshalString(map[string]any{ @@ -287,6 +314,9 @@ func TestDirect_Recall_Good_WithMemories(t *testing.T) { _, out, err := newTestDirect(srv).recall(context.Background(), nil, RecallInput{ Query: "architecture", TopK: 5, + Filter: RecallFilter{ + Org: "core", + }, }) require.NoError(t, err) assert.True(t, out.Success) @@ -331,6 +361,7 @@ func TestDirect_Recall_Good_WithFilters(t *testing.T) { var body map[string]any core.JSONUnmarshalString(core.ReadAll(r.Body).Value.(string), &body) assert.Equal(t, "cladius", body["agent_id"]) + assert.Equal(t, "core", body["org"]) assert.Equal(t, "eaas", body["project"]) assert.Equal(t, "decision", body["type"]) @@ -346,6 +377,7 @@ func TestDirect_Recall_Good_WithFilters(t *testing.T) { TopK: 5, Filter: RecallFilter{ AgentID: "cladius", + Org: "core", Project: "eaas", Type: "decision", }, @@ -412,6 +444,7 @@ func TestDirect_List_Good_WithMemories(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "GET", r.Method) assert.Equal(t, "/v1/brain/list", r.URL.Path) + assert.Equal(t, "core", r.URL.Query().Get("org")) assert.Equal(t, "agent", r.URL.Query().Get("project")) assert.Equal(t, "decision", r.URL.Query().Get("type")) assert.Equal(t, "codex", r.URL.Query().Get("agent_id")) @@ -453,6 +486,7 @@ func TestDirect_List_Good_WithMemories(t *testing.T) { defer srv.Close() _, out, err := newTestDirect(srv).list(context.Background(), nil, ListInput{ + Org: "core", Project: "agent", Type: "decision", AgentID: "codex",