From c7a413a9467b2b0ded63eadfe6d7828c1f127711 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Wed, 15 Jul 2026 11:17:28 +0530 Subject: [PATCH 1/6] Implement gRPC server for the existing YaadService proto MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit api/proto/yaad.proto defined the full YaadService (14 RPCs incl. 2 streaming) but had no generated Go code and no server implementation — PLAN.md and ARCHITECTURE.md both marked this explicitly TODO. Generates Go code via buf (new buf.gen.yaml) and adds GrpcServer, a thin adapter over *engine.Engine following the same pattern as RESTServer/MCPServer. WatchMemories/WatchStale bridge to the existing SSEBroadcaster used by REST's SSE streaming, so there's one subscription mechanism, not two. Verified with a real bufconn-based in-process client/server exercising every RPC, including a live streaming delivery test. --- api/proto/buf.gen.yaml | 8 + api/proto/yaad.pb.go | 1944 ++++++++++++++++++++++++++++++++++ api/proto/yaad_grpc.pb.go | 654 ++++++++++++ go.mod | 4 + go.sum | 12 + internal/server/grpc.go | 415 ++++++++ internal/server/grpc_test.go | 270 +++++ 7 files changed, 3307 insertions(+) create mode 100644 api/proto/buf.gen.yaml create mode 100644 api/proto/yaad.pb.go create mode 100644 api/proto/yaad_grpc.pb.go create mode 100644 internal/server/grpc.go create mode 100644 internal/server/grpc_test.go diff --git a/api/proto/buf.gen.yaml b/api/proto/buf.gen.yaml new file mode 100644 index 0000000..c5ca49b --- /dev/null +++ b/api/proto/buf.gen.yaml @@ -0,0 +1,8 @@ +version: v2 +plugins: + - local: protoc-gen-go + out: . + opt: paths=source_relative + - local: protoc-gen-go-grpc + out: . + opt: paths=source_relative diff --git a/api/proto/yaad.pb.go b/api/proto/yaad.pb.go new file mode 100644 index 0000000..1c7b2f0 --- /dev/null +++ b/api/proto/yaad.pb.go @@ -0,0 +1,1944 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: yaad.proto + +package yaadproto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type RememberRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` + Category string `protobuf:"bytes,2,opt,name=category,proto3" json:"category,omitempty"` + Scope string `protobuf:"bytes,3,opt,name=scope,proto3" json:"scope,omitempty"` + Project string `protobuf:"bytes,4,opt,name=project,proto3" json:"project,omitempty"` + Tags []string `protobuf:"bytes,5,rep,name=tags,proto3" json:"tags,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RememberRequest) Reset() { + *x = RememberRequest{} + mi := &file_yaad_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RememberRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RememberRequest) ProtoMessage() {} + +func (x *RememberRequest) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RememberRequest.ProtoReflect.Descriptor instead. +func (*RememberRequest) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{0} +} + +func (x *RememberRequest) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *RememberRequest) GetCategory() string { + if x != nil { + return x.Category + } + return "" +} + +func (x *RememberRequest) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + +func (x *RememberRequest) GetProject() string { + if x != nil { + return x.Project + } + return "" +} + +func (x *RememberRequest) GetTags() []string { + if x != nil { + return x.Tags + } + return nil +} + +type RememberResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + Created bool `protobuf:"varint,2,opt,name=created,proto3" json:"created,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RememberResponse) Reset() { + *x = RememberResponse{} + mi := &file_yaad_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RememberResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RememberResponse) ProtoMessage() {} + +func (x *RememberResponse) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RememberResponse.ProtoReflect.Descriptor instead. +func (*RememberResponse) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{1} +} + +func (x *RememberResponse) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *RememberResponse) GetCreated() bool { + if x != nil { + return x.Created + } + return false +} + +type RecallRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + TokenBudget int32 `protobuf:"varint,2,opt,name=token_budget,json=tokenBudget,proto3" json:"token_budget,omitempty"` + Scope string `protobuf:"bytes,3,opt,name=scope,proto3" json:"scope,omitempty"` + Project string `protobuf:"bytes,4,opt,name=project,proto3" json:"project,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RecallRequest) Reset() { + *x = RecallRequest{} + mi := &file_yaad_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RecallRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RecallRequest) ProtoMessage() {} + +func (x *RecallRequest) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RecallRequest.ProtoReflect.Descriptor instead. +func (*RecallRequest) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{2} +} + +func (x *RecallRequest) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +func (x *RecallRequest) GetTokenBudget() int32 { + if x != nil { + return x.TokenBudget + } + return 0 +} + +func (x *RecallRequest) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + +func (x *RecallRequest) GetProject() string { + if x != nil { + return x.Project + } + return "" +} + +type RecallResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Context string `protobuf:"bytes,1,opt,name=context,proto3" json:"context,omitempty"` + Nodes []*MemoryNode `protobuf:"bytes,2,rep,name=nodes,proto3" json:"nodes,omitempty"` + TokensUsed int32 `protobuf:"varint,3,opt,name=tokens_used,json=tokensUsed,proto3" json:"tokens_used,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RecallResponse) Reset() { + *x = RecallResponse{} + mi := &file_yaad_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RecallResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RecallResponse) ProtoMessage() {} + +func (x *RecallResponse) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RecallResponse.ProtoReflect.Descriptor instead. +func (*RecallResponse) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{3} +} + +func (x *RecallResponse) GetContext() string { + if x != nil { + return x.Context + } + return "" +} + +func (x *RecallResponse) GetNodes() []*MemoryNode { + if x != nil { + return x.Nodes + } + return nil +} + +func (x *RecallResponse) GetTokensUsed() int32 { + if x != nil { + return x.TokensUsed + } + return 0 +} + +type ContextRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + MaxTokens int32 `protobuf:"varint,2,opt,name=max_tokens,json=maxTokens,proto3" json:"max_tokens,omitempty"` + Project string `protobuf:"bytes,3,opt,name=project,proto3" json:"project,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContextRequest) Reset() { + *x = ContextRequest{} + mi := &file_yaad_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContextRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContextRequest) ProtoMessage() {} + +func (x *ContextRequest) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContextRequest.ProtoReflect.Descriptor instead. +func (*ContextRequest) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{4} +} + +func (x *ContextRequest) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +func (x *ContextRequest) GetMaxTokens() int32 { + if x != nil { + return x.MaxTokens + } + return 0 +} + +func (x *ContextRequest) GetProject() string { + if x != nil { + return x.Project + } + return "" +} + +type ContextResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Context string `protobuf:"bytes,1,opt,name=context,proto3" json:"context,omitempty"` + TokensUsed int32 `protobuf:"varint,2,opt,name=tokens_used,json=tokensUsed,proto3" json:"tokens_used,omitempty"` + Sources []string `protobuf:"bytes,3,rep,name=sources,proto3" json:"sources,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContextResponse) Reset() { + *x = ContextResponse{} + mi := &file_yaad_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContextResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContextResponse) ProtoMessage() {} + +func (x *ContextResponse) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContextResponse.ProtoReflect.Descriptor instead. +func (*ContextResponse) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{5} +} + +func (x *ContextResponse) GetContext() string { + if x != nil { + return x.Context + } + return "" +} + +func (x *ContextResponse) GetTokensUsed() int32 { + if x != nil { + return x.TokensUsed + } + return 0 +} + +func (x *ContextResponse) GetSources() []string { + if x != nil { + return x.Sources + } + return nil +} + +type ForgetRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ForgetRequest) Reset() { + *x = ForgetRequest{} + mi := &file_yaad_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ForgetRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ForgetRequest) ProtoMessage() {} + +func (x *ForgetRequest) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ForgetRequest.ProtoReflect.Descriptor instead. +func (*ForgetRequest) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{6} +} + +func (x *ForgetRequest) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +type ForgetResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ForgetResponse) Reset() { + *x = ForgetResponse{} + mi := &file_yaad_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ForgetResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ForgetResponse) ProtoMessage() {} + +func (x *ForgetResponse) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ForgetResponse.ProtoReflect.Descriptor instead. +func (*ForgetResponse) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{7} +} + +func (x *ForgetResponse) GetDeleted() bool { + if x != nil { + return x.Deleted + } + return false +} + +type LinkRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + FromId string `protobuf:"bytes,1,opt,name=from_id,json=fromId,proto3" json:"from_id,omitempty"` + ToId string `protobuf:"bytes,2,opt,name=to_id,json=toId,proto3" json:"to_id,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinkRequest) Reset() { + *x = LinkRequest{} + mi := &file_yaad_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinkRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinkRequest) ProtoMessage() {} + +func (x *LinkRequest) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinkRequest.ProtoReflect.Descriptor instead. +func (*LinkRequest) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{8} +} + +func (x *LinkRequest) GetFromId() string { + if x != nil { + return x.FromId + } + return "" +} + +func (x *LinkRequest) GetToId() string { + if x != nil { + return x.ToId + } + return "" +} + +func (x *LinkRequest) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +type LinkResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + EdgeId string `protobuf:"bytes,1,opt,name=edge_id,json=edgeId,proto3" json:"edge_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinkResponse) Reset() { + *x = LinkResponse{} + mi := &file_yaad_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinkResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinkResponse) ProtoMessage() {} + +func (x *LinkResponse) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinkResponse.ProtoReflect.Descriptor instead. +func (*LinkResponse) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{9} +} + +func (x *LinkResponse) GetEdgeId() string { + if x != nil { + return x.EdgeId + } + return "" +} + +type UnlinkRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + EdgeId string `protobuf:"bytes,1,opt,name=edge_id,json=edgeId,proto3" json:"edge_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UnlinkRequest) Reset() { + *x = UnlinkRequest{} + mi := &file_yaad_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UnlinkRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnlinkRequest) ProtoMessage() {} + +func (x *UnlinkRequest) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnlinkRequest.ProtoReflect.Descriptor instead. +func (*UnlinkRequest) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{10} +} + +func (x *UnlinkRequest) GetEdgeId() string { + if x != nil { + return x.EdgeId + } + return "" +} + +type UnlinkResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UnlinkResponse) Reset() { + *x = UnlinkResponse{} + mi := &file_yaad_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UnlinkResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnlinkResponse) ProtoMessage() {} + +func (x *UnlinkResponse) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnlinkResponse.ProtoReflect.Descriptor instead. +func (*UnlinkResponse) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{11} +} + +func (x *UnlinkResponse) GetDeleted() bool { + if x != nil { + return x.Deleted + } + return false +} + +type SubgraphRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + Depth int32 `protobuf:"varint,2,opt,name=depth,proto3" json:"depth,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubgraphRequest) Reset() { + *x = SubgraphRequest{} + mi := &file_yaad_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubgraphRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubgraphRequest) ProtoMessage() {} + +func (x *SubgraphRequest) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubgraphRequest.ProtoReflect.Descriptor instead. +func (*SubgraphRequest) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{12} +} + +func (x *SubgraphRequest) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *SubgraphRequest) GetDepth() int32 { + if x != nil { + return x.Depth + } + return 0 +} + +type SubgraphResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Nodes []*MemoryNode `protobuf:"bytes,1,rep,name=nodes,proto3" json:"nodes,omitempty"` + Edges []*MemoryEdge `protobuf:"bytes,2,rep,name=edges,proto3" json:"edges,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubgraphResponse) Reset() { + *x = SubgraphResponse{} + mi := &file_yaad_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubgraphResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubgraphResponse) ProtoMessage() {} + +func (x *SubgraphResponse) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubgraphResponse.ProtoReflect.Descriptor instead. +func (*SubgraphResponse) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{13} +} + +func (x *SubgraphResponse) GetNodes() []*MemoryNode { + if x != nil { + return x.Nodes + } + return nil +} + +func (x *SubgraphResponse) GetEdges() []*MemoryEdge { + if x != nil { + return x.Edges + } + return nil +} + +type ImpactRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ChangedFiles []string `protobuf:"bytes,1,rep,name=changed_files,json=changedFiles,proto3" json:"changed_files,omitempty"` + Project string `protobuf:"bytes,2,opt,name=project,proto3" json:"project,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ImpactRequest) Reset() { + *x = ImpactRequest{} + mi := &file_yaad_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ImpactRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ImpactRequest) ProtoMessage() {} + +func (x *ImpactRequest) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ImpactRequest.ProtoReflect.Descriptor instead. +func (*ImpactRequest) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{14} +} + +func (x *ImpactRequest) GetChangedFiles() []string { + if x != nil { + return x.ChangedFiles + } + return nil +} + +func (x *ImpactRequest) GetProject() string { + if x != nil { + return x.Project + } + return "" +} + +type ImpactResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + AffectedNodes []string `protobuf:"bytes,1,rep,name=affected_nodes,json=affectedNodes,proto3" json:"affected_nodes,omitempty"` + StaleNodes []string `protobuf:"bytes,2,rep,name=stale_nodes,json=staleNodes,proto3" json:"stale_nodes,omitempty"` + Summary string `protobuf:"bytes,3,opt,name=summary,proto3" json:"summary,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ImpactResponse) Reset() { + *x = ImpactResponse{} + mi := &file_yaad_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ImpactResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ImpactResponse) ProtoMessage() {} + +func (x *ImpactResponse) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ImpactResponse.ProtoReflect.Descriptor instead. +func (*ImpactResponse) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{15} +} + +func (x *ImpactResponse) GetAffectedNodes() []string { + if x != nil { + return x.AffectedNodes + } + return nil +} + +func (x *ImpactResponse) GetStaleNodes() []string { + if x != nil { + return x.StaleNodes + } + return nil +} + +func (x *ImpactResponse) GetSummary() string { + if x != nil { + return x.Summary + } + return "" +} + +type SessionStartRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + Project string `protobuf:"bytes,2,opt,name=project,proto3" json:"project,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SessionStartRequest) Reset() { + *x = SessionStartRequest{} + mi := &file_yaad_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SessionStartRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SessionStartRequest) ProtoMessage() {} + +func (x *SessionStartRequest) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SessionStartRequest.ProtoReflect.Descriptor instead. +func (*SessionStartRequest) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{16} +} + +func (x *SessionStartRequest) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *SessionStartRequest) GetProject() string { + if x != nil { + return x.Project + } + return "" +} + +type SessionStartResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Context string `protobuf:"bytes,1,opt,name=context,proto3" json:"context,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SessionStartResponse) Reset() { + *x = SessionStartResponse{} + mi := &file_yaad_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SessionStartResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SessionStartResponse) ProtoMessage() {} + +func (x *SessionStartResponse) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SessionStartResponse.ProtoReflect.Descriptor instead. +func (*SessionStartResponse) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{17} +} + +func (x *SessionStartResponse) GetContext() string { + if x != nil { + return x.Context + } + return "" +} + +type SessionEndRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + Summary string `protobuf:"bytes,2,opt,name=summary,proto3" json:"summary,omitempty"` + Success bool `protobuf:"varint,3,opt,name=success,proto3" json:"success,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SessionEndRequest) Reset() { + *x = SessionEndRequest{} + mi := &file_yaad_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SessionEndRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SessionEndRequest) ProtoMessage() {} + +func (x *SessionEndRequest) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SessionEndRequest.ProtoReflect.Descriptor instead. +func (*SessionEndRequest) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{18} +} + +func (x *SessionEndRequest) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *SessionEndRequest) GetSummary() string { + if x != nil { + return x.Summary + } + return "" +} + +func (x *SessionEndRequest) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +type SessionEndResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SessionEndResponse) Reset() { + *x = SessionEndResponse{} + mi := &file_yaad_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SessionEndResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SessionEndResponse) ProtoMessage() {} + +func (x *SessionEndResponse) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SessionEndResponse.ProtoReflect.Descriptor instead. +func (*SessionEndResponse) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{19} +} + +type HealthRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HealthRequest) Reset() { + *x = HealthRequest{} + mi := &file_yaad_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HealthRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthRequest) ProtoMessage() {} + +func (x *HealthRequest) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HealthRequest.ProtoReflect.Descriptor instead. +func (*HealthRequest) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{20} +} + +type HealthResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` + NodeCount int32 `protobuf:"varint,2,opt,name=node_count,json=nodeCount,proto3" json:"node_count,omitempty"` + EdgeCount int32 `protobuf:"varint,3,opt,name=edge_count,json=edgeCount,proto3" json:"edge_count,omitempty"` + Version string `protobuf:"bytes,4,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HealthResponse) Reset() { + *x = HealthResponse{} + mi := &file_yaad_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HealthResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthResponse) ProtoMessage() {} + +func (x *HealthResponse) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HealthResponse.ProtoReflect.Descriptor instead. +func (*HealthResponse) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{21} +} + +func (x *HealthResponse) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *HealthResponse) GetNodeCount() int32 { + if x != nil { + return x.NodeCount + } + return 0 +} + +func (x *HealthResponse) GetEdgeCount() int32 { + if x != nil { + return x.EdgeCount + } + return 0 +} + +func (x *HealthResponse) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +type GraphStatsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GraphStatsRequest) Reset() { + *x = GraphStatsRequest{} + mi := &file_yaad_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GraphStatsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GraphStatsRequest) ProtoMessage() {} + +func (x *GraphStatsRequest) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GraphStatsRequest.ProtoReflect.Descriptor instead. +func (*GraphStatsRequest) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{22} +} + +type GraphStatsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + TotalNodes int32 `protobuf:"varint,1,opt,name=total_nodes,json=totalNodes,proto3" json:"total_nodes,omitempty"` + TotalEdges int32 `protobuf:"varint,2,opt,name=total_edges,json=totalEdges,proto3" json:"total_edges,omitempty"` + NodesByType map[string]int32 `protobuf:"bytes,3,rep,name=nodes_by_type,json=nodesByType,proto3" json:"nodes_by_type,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"` + Communities int32 `protobuf:"varint,4,opt,name=communities,proto3" json:"communities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GraphStatsResponse) Reset() { + *x = GraphStatsResponse{} + mi := &file_yaad_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GraphStatsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GraphStatsResponse) ProtoMessage() {} + +func (x *GraphStatsResponse) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GraphStatsResponse.ProtoReflect.Descriptor instead. +func (*GraphStatsResponse) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{23} +} + +func (x *GraphStatsResponse) GetTotalNodes() int32 { + if x != nil { + return x.TotalNodes + } + return 0 +} + +func (x *GraphStatsResponse) GetTotalEdges() int32 { + if x != nil { + return x.TotalEdges + } + return 0 +} + +func (x *GraphStatsResponse) GetNodesByType() map[string]int32 { + if x != nil { + return x.NodesByType + } + return nil +} + +func (x *GraphStatsResponse) GetCommunities() int32 { + if x != nil { + return x.Communities + } + return 0 +} + +type WatchMemoriesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Project string `protobuf:"bytes,1,opt,name=project,proto3" json:"project,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WatchMemoriesRequest) Reset() { + *x = WatchMemoriesRequest{} + mi := &file_yaad_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WatchMemoriesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WatchMemoriesRequest) ProtoMessage() {} + +func (x *WatchMemoriesRequest) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WatchMemoriesRequest.ProtoReflect.Descriptor instead. +func (*WatchMemoriesRequest) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{24} +} + +func (x *WatchMemoriesRequest) GetProject() string { + if x != nil { + return x.Project + } + return "" +} + +type WatchStaleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Project string `protobuf:"bytes,1,opt,name=project,proto3" json:"project,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WatchStaleRequest) Reset() { + *x = WatchStaleRequest{} + mi := &file_yaad_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WatchStaleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WatchStaleRequest) ProtoMessage() {} + +func (x *WatchStaleRequest) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WatchStaleRequest.ProtoReflect.Descriptor instead. +func (*WatchStaleRequest) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{25} +} + +func (x *WatchStaleRequest) GetProject() string { + if x != nil { + return x.Project + } + return "" +} + +type MemoryEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // "created", "updated", "deleted" + NodeId string `protobuf:"bytes,2,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + Content string `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"` + Timestamp int64 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MemoryEvent) Reset() { + *x = MemoryEvent{} + mi := &file_yaad_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MemoryEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MemoryEvent) ProtoMessage() {} + +func (x *MemoryEvent) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MemoryEvent.ProtoReflect.Descriptor instead. +func (*MemoryEvent) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{26} +} + +func (x *MemoryEvent) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *MemoryEvent) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *MemoryEvent) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *MemoryEvent) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +type StaleEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` + FilePath string `protobuf:"bytes,3,opt,name=file_path,json=filePath,proto3" json:"file_path,omitempty"` + Timestamp int64 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StaleEvent) Reset() { + *x = StaleEvent{} + mi := &file_yaad_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StaleEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StaleEvent) ProtoMessage() {} + +func (x *StaleEvent) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StaleEvent.ProtoReflect.Descriptor instead. +func (*StaleEvent) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{27} +} + +func (x *StaleEvent) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *StaleEvent) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *StaleEvent) GetFilePath() string { + if x != nil { + return x.FilePath + } + return "" +} + +func (x *StaleEvent) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +type MemoryNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + Content string `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"` + Summary string `protobuf:"bytes,4,opt,name=summary,proto3" json:"summary,omitempty"` + Confidence float32 `protobuf:"fixed32,5,opt,name=confidence,proto3" json:"confidence,omitempty"` + CreatedAt int64 `protobuf:"varint,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + AccessedAt int64 `protobuf:"varint,7,opt,name=accessed_at,json=accessedAt,proto3" json:"accessed_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MemoryNode) Reset() { + *x = MemoryNode{} + mi := &file_yaad_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MemoryNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MemoryNode) ProtoMessage() {} + +func (x *MemoryNode) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MemoryNode.ProtoReflect.Descriptor instead. +func (*MemoryNode) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{28} +} + +func (x *MemoryNode) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *MemoryNode) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *MemoryNode) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *MemoryNode) GetSummary() string { + if x != nil { + return x.Summary + } + return "" +} + +func (x *MemoryNode) GetConfidence() float32 { + if x != nil { + return x.Confidence + } + return 0 +} + +func (x *MemoryNode) GetCreatedAt() int64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +func (x *MemoryNode) GetAccessedAt() int64 { + if x != nil { + return x.AccessedAt + } + return 0 +} + +type MemoryEdge struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + FromId string `protobuf:"bytes,2,opt,name=from_id,json=fromId,proto3" json:"from_id,omitempty"` + ToId string `protobuf:"bytes,3,opt,name=to_id,json=toId,proto3" json:"to_id,omitempty"` + Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"` + Weight float32 `protobuf:"fixed32,5,opt,name=weight,proto3" json:"weight,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MemoryEdge) Reset() { + *x = MemoryEdge{} + mi := &file_yaad_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MemoryEdge) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MemoryEdge) ProtoMessage() {} + +func (x *MemoryEdge) ProtoReflect() protoreflect.Message { + mi := &file_yaad_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MemoryEdge.ProtoReflect.Descriptor instead. +func (*MemoryEdge) Descriptor() ([]byte, []int) { + return file_yaad_proto_rawDescGZIP(), []int{29} +} + +func (x *MemoryEdge) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *MemoryEdge) GetFromId() string { + if x != nil { + return x.FromId + } + return "" +} + +func (x *MemoryEdge) GetToId() string { + if x != nil { + return x.ToId + } + return "" +} + +func (x *MemoryEdge) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *MemoryEdge) GetWeight() float32 { + if x != nil { + return x.Weight + } + return 0 +} + +var File_yaad_proto protoreflect.FileDescriptor + +const file_yaad_proto_rawDesc = "" + + "\n" + + "\n" + + "yaad.proto\x12\x04yaad\"\x8b\x01\n" + + "\x0fRememberRequest\x12\x18\n" + + "\acontent\x18\x01 \x01(\tR\acontent\x12\x1a\n" + + "\bcategory\x18\x02 \x01(\tR\bcategory\x12\x14\n" + + "\x05scope\x18\x03 \x01(\tR\x05scope\x12\x18\n" + + "\aproject\x18\x04 \x01(\tR\aproject\x12\x12\n" + + "\x04tags\x18\x05 \x03(\tR\x04tags\"E\n" + + "\x10RememberResponse\x12\x17\n" + + "\anode_id\x18\x01 \x01(\tR\x06nodeId\x12\x18\n" + + "\acreated\x18\x02 \x01(\bR\acreated\"x\n" + + "\rRecallRequest\x12\x14\n" + + "\x05query\x18\x01 \x01(\tR\x05query\x12!\n" + + "\ftoken_budget\x18\x02 \x01(\x05R\vtokenBudget\x12\x14\n" + + "\x05scope\x18\x03 \x01(\tR\x05scope\x12\x18\n" + + "\aproject\x18\x04 \x01(\tR\aproject\"s\n" + + "\x0eRecallResponse\x12\x18\n" + + "\acontext\x18\x01 \x01(\tR\acontext\x12&\n" + + "\x05nodes\x18\x02 \x03(\v2\x10.yaad.MemoryNodeR\x05nodes\x12\x1f\n" + + "\vtokens_used\x18\x03 \x01(\x05R\n" + + "tokensUsed\"_\n" + + "\x0eContextRequest\x12\x14\n" + + "\x05query\x18\x01 \x01(\tR\x05query\x12\x1d\n" + + "\n" + + "max_tokens\x18\x02 \x01(\x05R\tmaxTokens\x12\x18\n" + + "\aproject\x18\x03 \x01(\tR\aproject\"f\n" + + "\x0fContextResponse\x12\x18\n" + + "\acontext\x18\x01 \x01(\tR\acontext\x12\x1f\n" + + "\vtokens_used\x18\x02 \x01(\x05R\n" + + "tokensUsed\x12\x18\n" + + "\asources\x18\x03 \x03(\tR\asources\"(\n" + + "\rForgetRequest\x12\x17\n" + + "\anode_id\x18\x01 \x01(\tR\x06nodeId\"*\n" + + "\x0eForgetResponse\x12\x18\n" + + "\adeleted\x18\x01 \x01(\bR\adeleted\"O\n" + + "\vLinkRequest\x12\x17\n" + + "\afrom_id\x18\x01 \x01(\tR\x06fromId\x12\x13\n" + + "\x05to_id\x18\x02 \x01(\tR\x04toId\x12\x12\n" + + "\x04type\x18\x03 \x01(\tR\x04type\"'\n" + + "\fLinkResponse\x12\x17\n" + + "\aedge_id\x18\x01 \x01(\tR\x06edgeId\"(\n" + + "\rUnlinkRequest\x12\x17\n" + + "\aedge_id\x18\x01 \x01(\tR\x06edgeId\"*\n" + + "\x0eUnlinkResponse\x12\x18\n" + + "\adeleted\x18\x01 \x01(\bR\adeleted\"@\n" + + "\x0fSubgraphRequest\x12\x17\n" + + "\anode_id\x18\x01 \x01(\tR\x06nodeId\x12\x14\n" + + "\x05depth\x18\x02 \x01(\x05R\x05depth\"b\n" + + "\x10SubgraphResponse\x12&\n" + + "\x05nodes\x18\x01 \x03(\v2\x10.yaad.MemoryNodeR\x05nodes\x12&\n" + + "\x05edges\x18\x02 \x03(\v2\x10.yaad.MemoryEdgeR\x05edges\"N\n" + + "\rImpactRequest\x12#\n" + + "\rchanged_files\x18\x01 \x03(\tR\fchangedFiles\x12\x18\n" + + "\aproject\x18\x02 \x01(\tR\aproject\"r\n" + + "\x0eImpactResponse\x12%\n" + + "\x0eaffected_nodes\x18\x01 \x03(\tR\raffectedNodes\x12\x1f\n" + + "\vstale_nodes\x18\x02 \x03(\tR\n" + + "staleNodes\x12\x18\n" + + "\asummary\x18\x03 \x01(\tR\asummary\"N\n" + + "\x13SessionStartRequest\x12\x1d\n" + + "\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12\x18\n" + + "\aproject\x18\x02 \x01(\tR\aproject\"0\n" + + "\x14SessionStartResponse\x12\x18\n" + + "\acontext\x18\x01 \x01(\tR\acontext\"f\n" + + "\x11SessionEndRequest\x12\x1d\n" + + "\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12\x18\n" + + "\asummary\x18\x02 \x01(\tR\asummary\x12\x18\n" + + "\asuccess\x18\x03 \x01(\bR\asuccess\"\x14\n" + + "\x12SessionEndResponse\"\x0f\n" + + "\rHealthRequest\"\x80\x01\n" + + "\x0eHealthResponse\x12\x16\n" + + "\x06status\x18\x01 \x01(\tR\x06status\x12\x1d\n" + + "\n" + + "node_count\x18\x02 \x01(\x05R\tnodeCount\x12\x1d\n" + + "\n" + + "edge_count\x18\x03 \x01(\x05R\tedgeCount\x12\x18\n" + + "\aversion\x18\x04 \x01(\tR\aversion\"\x13\n" + + "\x11GraphStatsRequest\"\x87\x02\n" + + "\x12GraphStatsResponse\x12\x1f\n" + + "\vtotal_nodes\x18\x01 \x01(\x05R\n" + + "totalNodes\x12\x1f\n" + + "\vtotal_edges\x18\x02 \x01(\x05R\n" + + "totalEdges\x12M\n" + + "\rnodes_by_type\x18\x03 \x03(\v2).yaad.GraphStatsResponse.NodesByTypeEntryR\vnodesByType\x12 \n" + + "\vcommunities\x18\x04 \x01(\x05R\vcommunities\x1a>\n" + + "\x10NodesByTypeEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\x05R\x05value:\x028\x01\"0\n" + + "\x14WatchMemoriesRequest\x12\x18\n" + + "\aproject\x18\x01 \x01(\tR\aproject\"-\n" + + "\x11WatchStaleRequest\x12\x18\n" + + "\aproject\x18\x01 \x01(\tR\aproject\"r\n" + + "\vMemoryEvent\x12\x12\n" + + "\x04type\x18\x01 \x01(\tR\x04type\x12\x17\n" + + "\anode_id\x18\x02 \x01(\tR\x06nodeId\x12\x18\n" + + "\acontent\x18\x03 \x01(\tR\acontent\x12\x1c\n" + + "\ttimestamp\x18\x04 \x01(\x03R\ttimestamp\"x\n" + + "\n" + + "StaleEvent\x12\x17\n" + + "\anode_id\x18\x01 \x01(\tR\x06nodeId\x12\x16\n" + + "\x06reason\x18\x02 \x01(\tR\x06reason\x12\x1b\n" + + "\tfile_path\x18\x03 \x01(\tR\bfilePath\x12\x1c\n" + + "\ttimestamp\x18\x04 \x01(\x03R\ttimestamp\"\xc4\x01\n" + + "\n" + + "MemoryNode\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04type\x18\x02 \x01(\tR\x04type\x12\x18\n" + + "\acontent\x18\x03 \x01(\tR\acontent\x12\x18\n" + + "\asummary\x18\x04 \x01(\tR\asummary\x12\x1e\n" + + "\n" + + "confidence\x18\x05 \x01(\x02R\n" + + "confidence\x12\x1d\n" + + "\n" + + "created_at\x18\x06 \x01(\x03R\tcreatedAt\x12\x1f\n" + + "\vaccessed_at\x18\a \x01(\x03R\n" + + "accessedAt\"v\n" + + "\n" + + "MemoryEdge\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x17\n" + + "\afrom_id\x18\x02 \x01(\tR\x06fromId\x12\x13\n" + + "\x05to_id\x18\x03 \x01(\tR\x04toId\x12\x12\n" + + "\x04type\x18\x04 \x01(\tR\x04type\x12\x16\n" + + "\x06weight\x18\x05 \x01(\x02R\x06weight2\xb9\x06\n" + + "\vYaadService\x129\n" + + "\bRemember\x12\x15.yaad.RememberRequest\x1a\x16.yaad.RememberResponse\x123\n" + + "\x06Recall\x12\x13.yaad.RecallRequest\x1a\x14.yaad.RecallResponse\x126\n" + + "\aContext\x12\x14.yaad.ContextRequest\x1a\x15.yaad.ContextResponse\x123\n" + + "\x06Forget\x12\x13.yaad.ForgetRequest\x1a\x14.yaad.ForgetResponse\x12-\n" + + "\x04Link\x12\x11.yaad.LinkRequest\x1a\x12.yaad.LinkResponse\x123\n" + + "\x06Unlink\x12\x13.yaad.UnlinkRequest\x1a\x14.yaad.UnlinkResponse\x129\n" + + "\bSubgraph\x12\x15.yaad.SubgraphRequest\x1a\x16.yaad.SubgraphResponse\x123\n" + + "\x06Impact\x12\x13.yaad.ImpactRequest\x1a\x14.yaad.ImpactResponse\x12E\n" + + "\fSessionStart\x12\x19.yaad.SessionStartRequest\x1a\x1a.yaad.SessionStartResponse\x12?\n" + + "\n" + + "SessionEnd\x12\x17.yaad.SessionEndRequest\x1a\x18.yaad.SessionEndResponse\x123\n" + + "\x06Health\x12\x13.yaad.HealthRequest\x1a\x14.yaad.HealthResponse\x12?\n" + + "\n" + + "GraphStats\x12\x17.yaad.GraphStatsRequest\x1a\x18.yaad.GraphStatsResponse\x12@\n" + + "\rWatchMemories\x12\x1a.yaad.WatchMemoriesRequest\x1a\x11.yaad.MemoryEvent0\x01\x129\n" + + "\n" + + "WatchStale\x12\x17.yaad.WatchStaleRequest\x1a\x10.yaad.StaleEvent0\x01B0Z.github.com/GrayCodeAI/yaad/api/proto;yaadprotob\x06proto3" + +var ( + file_yaad_proto_rawDescOnce sync.Once + file_yaad_proto_rawDescData []byte +) + +func file_yaad_proto_rawDescGZIP() []byte { + file_yaad_proto_rawDescOnce.Do(func() { + file_yaad_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_yaad_proto_rawDesc), len(file_yaad_proto_rawDesc))) + }) + return file_yaad_proto_rawDescData +} + +var file_yaad_proto_msgTypes = make([]protoimpl.MessageInfo, 31) +var file_yaad_proto_goTypes = []any{ + (*RememberRequest)(nil), // 0: yaad.RememberRequest + (*RememberResponse)(nil), // 1: yaad.RememberResponse + (*RecallRequest)(nil), // 2: yaad.RecallRequest + (*RecallResponse)(nil), // 3: yaad.RecallResponse + (*ContextRequest)(nil), // 4: yaad.ContextRequest + (*ContextResponse)(nil), // 5: yaad.ContextResponse + (*ForgetRequest)(nil), // 6: yaad.ForgetRequest + (*ForgetResponse)(nil), // 7: yaad.ForgetResponse + (*LinkRequest)(nil), // 8: yaad.LinkRequest + (*LinkResponse)(nil), // 9: yaad.LinkResponse + (*UnlinkRequest)(nil), // 10: yaad.UnlinkRequest + (*UnlinkResponse)(nil), // 11: yaad.UnlinkResponse + (*SubgraphRequest)(nil), // 12: yaad.SubgraphRequest + (*SubgraphResponse)(nil), // 13: yaad.SubgraphResponse + (*ImpactRequest)(nil), // 14: yaad.ImpactRequest + (*ImpactResponse)(nil), // 15: yaad.ImpactResponse + (*SessionStartRequest)(nil), // 16: yaad.SessionStartRequest + (*SessionStartResponse)(nil), // 17: yaad.SessionStartResponse + (*SessionEndRequest)(nil), // 18: yaad.SessionEndRequest + (*SessionEndResponse)(nil), // 19: yaad.SessionEndResponse + (*HealthRequest)(nil), // 20: yaad.HealthRequest + (*HealthResponse)(nil), // 21: yaad.HealthResponse + (*GraphStatsRequest)(nil), // 22: yaad.GraphStatsRequest + (*GraphStatsResponse)(nil), // 23: yaad.GraphStatsResponse + (*WatchMemoriesRequest)(nil), // 24: yaad.WatchMemoriesRequest + (*WatchStaleRequest)(nil), // 25: yaad.WatchStaleRequest + (*MemoryEvent)(nil), // 26: yaad.MemoryEvent + (*StaleEvent)(nil), // 27: yaad.StaleEvent + (*MemoryNode)(nil), // 28: yaad.MemoryNode + (*MemoryEdge)(nil), // 29: yaad.MemoryEdge + nil, // 30: yaad.GraphStatsResponse.NodesByTypeEntry +} +var file_yaad_proto_depIdxs = []int32{ + 28, // 0: yaad.RecallResponse.nodes:type_name -> yaad.MemoryNode + 28, // 1: yaad.SubgraphResponse.nodes:type_name -> yaad.MemoryNode + 29, // 2: yaad.SubgraphResponse.edges:type_name -> yaad.MemoryEdge + 30, // 3: yaad.GraphStatsResponse.nodes_by_type:type_name -> yaad.GraphStatsResponse.NodesByTypeEntry + 0, // 4: yaad.YaadService.Remember:input_type -> yaad.RememberRequest + 2, // 5: yaad.YaadService.Recall:input_type -> yaad.RecallRequest + 4, // 6: yaad.YaadService.Context:input_type -> yaad.ContextRequest + 6, // 7: yaad.YaadService.Forget:input_type -> yaad.ForgetRequest + 8, // 8: yaad.YaadService.Link:input_type -> yaad.LinkRequest + 10, // 9: yaad.YaadService.Unlink:input_type -> yaad.UnlinkRequest + 12, // 10: yaad.YaadService.Subgraph:input_type -> yaad.SubgraphRequest + 14, // 11: yaad.YaadService.Impact:input_type -> yaad.ImpactRequest + 16, // 12: yaad.YaadService.SessionStart:input_type -> yaad.SessionStartRequest + 18, // 13: yaad.YaadService.SessionEnd:input_type -> yaad.SessionEndRequest + 20, // 14: yaad.YaadService.Health:input_type -> yaad.HealthRequest + 22, // 15: yaad.YaadService.GraphStats:input_type -> yaad.GraphStatsRequest + 24, // 16: yaad.YaadService.WatchMemories:input_type -> yaad.WatchMemoriesRequest + 25, // 17: yaad.YaadService.WatchStale:input_type -> yaad.WatchStaleRequest + 1, // 18: yaad.YaadService.Remember:output_type -> yaad.RememberResponse + 3, // 19: yaad.YaadService.Recall:output_type -> yaad.RecallResponse + 5, // 20: yaad.YaadService.Context:output_type -> yaad.ContextResponse + 7, // 21: yaad.YaadService.Forget:output_type -> yaad.ForgetResponse + 9, // 22: yaad.YaadService.Link:output_type -> yaad.LinkResponse + 11, // 23: yaad.YaadService.Unlink:output_type -> yaad.UnlinkResponse + 13, // 24: yaad.YaadService.Subgraph:output_type -> yaad.SubgraphResponse + 15, // 25: yaad.YaadService.Impact:output_type -> yaad.ImpactResponse + 17, // 26: yaad.YaadService.SessionStart:output_type -> yaad.SessionStartResponse + 19, // 27: yaad.YaadService.SessionEnd:output_type -> yaad.SessionEndResponse + 21, // 28: yaad.YaadService.Health:output_type -> yaad.HealthResponse + 23, // 29: yaad.YaadService.GraphStats:output_type -> yaad.GraphStatsResponse + 26, // 30: yaad.YaadService.WatchMemories:output_type -> yaad.MemoryEvent + 27, // 31: yaad.YaadService.WatchStale:output_type -> yaad.StaleEvent + 18, // [18:32] is the sub-list for method output_type + 4, // [4:18] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_yaad_proto_init() } +func file_yaad_proto_init() { + if File_yaad_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_yaad_proto_rawDesc), len(file_yaad_proto_rawDesc)), + NumEnums: 0, + NumMessages: 31, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_yaad_proto_goTypes, + DependencyIndexes: file_yaad_proto_depIdxs, + MessageInfos: file_yaad_proto_msgTypes, + }.Build() + File_yaad_proto = out.File + file_yaad_proto_goTypes = nil + file_yaad_proto_depIdxs = nil +} diff --git a/api/proto/yaad_grpc.pb.go b/api/proto/yaad_grpc.pb.go new file mode 100644 index 0000000..e4295d1 --- /dev/null +++ b/api/proto/yaad_grpc.pb.go @@ -0,0 +1,654 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: yaad.proto + +package yaadproto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + YaadService_Remember_FullMethodName = "/yaad.YaadService/Remember" + YaadService_Recall_FullMethodName = "/yaad.YaadService/Recall" + YaadService_Context_FullMethodName = "/yaad.YaadService/Context" + YaadService_Forget_FullMethodName = "/yaad.YaadService/Forget" + YaadService_Link_FullMethodName = "/yaad.YaadService/Link" + YaadService_Unlink_FullMethodName = "/yaad.YaadService/Unlink" + YaadService_Subgraph_FullMethodName = "/yaad.YaadService/Subgraph" + YaadService_Impact_FullMethodName = "/yaad.YaadService/Impact" + YaadService_SessionStart_FullMethodName = "/yaad.YaadService/SessionStart" + YaadService_SessionEnd_FullMethodName = "/yaad.YaadService/SessionEnd" + YaadService_Health_FullMethodName = "/yaad.YaadService/Health" + YaadService_GraphStats_FullMethodName = "/yaad.YaadService/GraphStats" + YaadService_WatchMemories_FullMethodName = "/yaad.YaadService/WatchMemories" + YaadService_WatchStale_FullMethodName = "/yaad.YaadService/WatchStale" +) + +// YaadServiceClient is the client API for YaadService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// YaadService is the gRPC interface for yaad's memory graph. +type YaadServiceClient interface { + // Remember stores a new memory node. + Remember(ctx context.Context, in *RememberRequest, opts ...grpc.CallOption) (*RememberResponse, error) + // Recall retrieves relevant memories for a query. + Recall(ctx context.Context, in *RecallRequest, opts ...grpc.CallOption) (*RecallResponse, error) + // Context builds a context pack for a query within a token budget. + Context(ctx context.Context, in *ContextRequest, opts ...grpc.CallOption) (*ContextResponse, error) + // Forget removes a memory node. + Forget(ctx context.Context, in *ForgetRequest, opts ...grpc.CallOption) (*ForgetResponse, error) + // Link creates an edge between two nodes. + Link(ctx context.Context, in *LinkRequest, opts ...grpc.CallOption) (*LinkResponse, error) + // Unlink removes an edge. + Unlink(ctx context.Context, in *UnlinkRequest, opts ...grpc.CallOption) (*UnlinkResponse, error) + // Subgraph retrieves a subgraph around a node. + Subgraph(ctx context.Context, in *SubgraphRequest, opts ...grpc.CallOption) (*SubgraphResponse, error) + // Impact analyzes what memories are affected by a file change. + Impact(ctx context.Context, in *ImpactRequest, opts ...grpc.CallOption) (*ImpactResponse, error) + // SessionStart notifies yaad of a new session. + SessionStart(ctx context.Context, in *SessionStartRequest, opts ...grpc.CallOption) (*SessionStartResponse, error) + // SessionEnd notifies yaad that a session ended. + SessionEnd(ctx context.Context, in *SessionEndRequest, opts ...grpc.CallOption) (*SessionEndResponse, error) + // Health returns the health status of the yaad service. + Health(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*HealthResponse, error) + // GraphStats returns statistics about the memory graph. + GraphStats(ctx context.Context, in *GraphStatsRequest, opts ...grpc.CallOption) (*GraphStatsResponse, error) + // WatchMemories streams memory changes in real-time. + WatchMemories(ctx context.Context, in *WatchMemoriesRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[MemoryEvent], error) + // WatchStale streams stale memory notifications. + WatchStale(ctx context.Context, in *WatchStaleRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StaleEvent], error) +} + +type yaadServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewYaadServiceClient(cc grpc.ClientConnInterface) YaadServiceClient { + return &yaadServiceClient{cc} +} + +func (c *yaadServiceClient) Remember(ctx context.Context, in *RememberRequest, opts ...grpc.CallOption) (*RememberResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RememberResponse) + err := c.cc.Invoke(ctx, YaadService_Remember_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *yaadServiceClient) Recall(ctx context.Context, in *RecallRequest, opts ...grpc.CallOption) (*RecallResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RecallResponse) + err := c.cc.Invoke(ctx, YaadService_Recall_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *yaadServiceClient) Context(ctx context.Context, in *ContextRequest, opts ...grpc.CallOption) (*ContextResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ContextResponse) + err := c.cc.Invoke(ctx, YaadService_Context_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *yaadServiceClient) Forget(ctx context.Context, in *ForgetRequest, opts ...grpc.CallOption) (*ForgetResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ForgetResponse) + err := c.cc.Invoke(ctx, YaadService_Forget_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *yaadServiceClient) Link(ctx context.Context, in *LinkRequest, opts ...grpc.CallOption) (*LinkResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(LinkResponse) + err := c.cc.Invoke(ctx, YaadService_Link_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *yaadServiceClient) Unlink(ctx context.Context, in *UnlinkRequest, opts ...grpc.CallOption) (*UnlinkResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UnlinkResponse) + err := c.cc.Invoke(ctx, YaadService_Unlink_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *yaadServiceClient) Subgraph(ctx context.Context, in *SubgraphRequest, opts ...grpc.CallOption) (*SubgraphResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SubgraphResponse) + err := c.cc.Invoke(ctx, YaadService_Subgraph_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *yaadServiceClient) Impact(ctx context.Context, in *ImpactRequest, opts ...grpc.CallOption) (*ImpactResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ImpactResponse) + err := c.cc.Invoke(ctx, YaadService_Impact_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *yaadServiceClient) SessionStart(ctx context.Context, in *SessionStartRequest, opts ...grpc.CallOption) (*SessionStartResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SessionStartResponse) + err := c.cc.Invoke(ctx, YaadService_SessionStart_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *yaadServiceClient) SessionEnd(ctx context.Context, in *SessionEndRequest, opts ...grpc.CallOption) (*SessionEndResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SessionEndResponse) + err := c.cc.Invoke(ctx, YaadService_SessionEnd_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *yaadServiceClient) Health(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*HealthResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(HealthResponse) + err := c.cc.Invoke(ctx, YaadService_Health_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *yaadServiceClient) GraphStats(ctx context.Context, in *GraphStatsRequest, opts ...grpc.CallOption) (*GraphStatsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GraphStatsResponse) + err := c.cc.Invoke(ctx, YaadService_GraphStats_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *yaadServiceClient) WatchMemories(ctx context.Context, in *WatchMemoriesRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[MemoryEvent], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &YaadService_ServiceDesc.Streams[0], YaadService_WatchMemories_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[WatchMemoriesRequest, MemoryEvent]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type YaadService_WatchMemoriesClient = grpc.ServerStreamingClient[MemoryEvent] + +func (c *yaadServiceClient) WatchStale(ctx context.Context, in *WatchStaleRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StaleEvent], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &YaadService_ServiceDesc.Streams[1], YaadService_WatchStale_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[WatchStaleRequest, StaleEvent]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type YaadService_WatchStaleClient = grpc.ServerStreamingClient[StaleEvent] + +// YaadServiceServer is the server API for YaadService service. +// All implementations must embed UnimplementedYaadServiceServer +// for forward compatibility. +// +// YaadService is the gRPC interface for yaad's memory graph. +type YaadServiceServer interface { + // Remember stores a new memory node. + Remember(context.Context, *RememberRequest) (*RememberResponse, error) + // Recall retrieves relevant memories for a query. + Recall(context.Context, *RecallRequest) (*RecallResponse, error) + // Context builds a context pack for a query within a token budget. + Context(context.Context, *ContextRequest) (*ContextResponse, error) + // Forget removes a memory node. + Forget(context.Context, *ForgetRequest) (*ForgetResponse, error) + // Link creates an edge between two nodes. + Link(context.Context, *LinkRequest) (*LinkResponse, error) + // Unlink removes an edge. + Unlink(context.Context, *UnlinkRequest) (*UnlinkResponse, error) + // Subgraph retrieves a subgraph around a node. + Subgraph(context.Context, *SubgraphRequest) (*SubgraphResponse, error) + // Impact analyzes what memories are affected by a file change. + Impact(context.Context, *ImpactRequest) (*ImpactResponse, error) + // SessionStart notifies yaad of a new session. + SessionStart(context.Context, *SessionStartRequest) (*SessionStartResponse, error) + // SessionEnd notifies yaad that a session ended. + SessionEnd(context.Context, *SessionEndRequest) (*SessionEndResponse, error) + // Health returns the health status of the yaad service. + Health(context.Context, *HealthRequest) (*HealthResponse, error) + // GraphStats returns statistics about the memory graph. + GraphStats(context.Context, *GraphStatsRequest) (*GraphStatsResponse, error) + // WatchMemories streams memory changes in real-time. + WatchMemories(*WatchMemoriesRequest, grpc.ServerStreamingServer[MemoryEvent]) error + // WatchStale streams stale memory notifications. + WatchStale(*WatchStaleRequest, grpc.ServerStreamingServer[StaleEvent]) error + mustEmbedUnimplementedYaadServiceServer() +} + +// UnimplementedYaadServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedYaadServiceServer struct{} + +func (UnimplementedYaadServiceServer) Remember(context.Context, *RememberRequest) (*RememberResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Remember not implemented") +} +func (UnimplementedYaadServiceServer) Recall(context.Context, *RecallRequest) (*RecallResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Recall not implemented") +} +func (UnimplementedYaadServiceServer) Context(context.Context, *ContextRequest) (*ContextResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Context not implemented") +} +func (UnimplementedYaadServiceServer) Forget(context.Context, *ForgetRequest) (*ForgetResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Forget not implemented") +} +func (UnimplementedYaadServiceServer) Link(context.Context, *LinkRequest) (*LinkResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Link not implemented") +} +func (UnimplementedYaadServiceServer) Unlink(context.Context, *UnlinkRequest) (*UnlinkResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Unlink not implemented") +} +func (UnimplementedYaadServiceServer) Subgraph(context.Context, *SubgraphRequest) (*SubgraphResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Subgraph not implemented") +} +func (UnimplementedYaadServiceServer) Impact(context.Context, *ImpactRequest) (*ImpactResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Impact not implemented") +} +func (UnimplementedYaadServiceServer) SessionStart(context.Context, *SessionStartRequest) (*SessionStartResponse, error) { + return nil, status.Error(codes.Unimplemented, "method SessionStart not implemented") +} +func (UnimplementedYaadServiceServer) SessionEnd(context.Context, *SessionEndRequest) (*SessionEndResponse, error) { + return nil, status.Error(codes.Unimplemented, "method SessionEnd not implemented") +} +func (UnimplementedYaadServiceServer) Health(context.Context, *HealthRequest) (*HealthResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Health not implemented") +} +func (UnimplementedYaadServiceServer) GraphStats(context.Context, *GraphStatsRequest) (*GraphStatsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GraphStats not implemented") +} +func (UnimplementedYaadServiceServer) WatchMemories(*WatchMemoriesRequest, grpc.ServerStreamingServer[MemoryEvent]) error { + return status.Error(codes.Unimplemented, "method WatchMemories not implemented") +} +func (UnimplementedYaadServiceServer) WatchStale(*WatchStaleRequest, grpc.ServerStreamingServer[StaleEvent]) error { + return status.Error(codes.Unimplemented, "method WatchStale not implemented") +} +func (UnimplementedYaadServiceServer) mustEmbedUnimplementedYaadServiceServer() {} +func (UnimplementedYaadServiceServer) testEmbeddedByValue() {} + +// UnsafeYaadServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to YaadServiceServer will +// result in compilation errors. +type UnsafeYaadServiceServer interface { + mustEmbedUnimplementedYaadServiceServer() +} + +func RegisterYaadServiceServer(s grpc.ServiceRegistrar, srv YaadServiceServer) { + // If the following call panics, it indicates UnimplementedYaadServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&YaadService_ServiceDesc, srv) +} + +func _YaadService_Remember_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RememberRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(YaadServiceServer).Remember(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: YaadService_Remember_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(YaadServiceServer).Remember(ctx, req.(*RememberRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _YaadService_Recall_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RecallRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(YaadServiceServer).Recall(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: YaadService_Recall_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(YaadServiceServer).Recall(ctx, req.(*RecallRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _YaadService_Context_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ContextRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(YaadServiceServer).Context(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: YaadService_Context_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(YaadServiceServer).Context(ctx, req.(*ContextRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _YaadService_Forget_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ForgetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(YaadServiceServer).Forget(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: YaadService_Forget_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(YaadServiceServer).Forget(ctx, req.(*ForgetRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _YaadService_Link_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LinkRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(YaadServiceServer).Link(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: YaadService_Link_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(YaadServiceServer).Link(ctx, req.(*LinkRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _YaadService_Unlink_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UnlinkRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(YaadServiceServer).Unlink(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: YaadService_Unlink_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(YaadServiceServer).Unlink(ctx, req.(*UnlinkRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _YaadService_Subgraph_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SubgraphRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(YaadServiceServer).Subgraph(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: YaadService_Subgraph_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(YaadServiceServer).Subgraph(ctx, req.(*SubgraphRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _YaadService_Impact_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ImpactRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(YaadServiceServer).Impact(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: YaadService_Impact_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(YaadServiceServer).Impact(ctx, req.(*ImpactRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _YaadService_SessionStart_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SessionStartRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(YaadServiceServer).SessionStart(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: YaadService_SessionStart_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(YaadServiceServer).SessionStart(ctx, req.(*SessionStartRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _YaadService_SessionEnd_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SessionEndRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(YaadServiceServer).SessionEnd(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: YaadService_SessionEnd_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(YaadServiceServer).SessionEnd(ctx, req.(*SessionEndRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _YaadService_Health_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HealthRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(YaadServiceServer).Health(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: YaadService_Health_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(YaadServiceServer).Health(ctx, req.(*HealthRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _YaadService_GraphStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GraphStatsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(YaadServiceServer).GraphStats(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: YaadService_GraphStats_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(YaadServiceServer).GraphStats(ctx, req.(*GraphStatsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _YaadService_WatchMemories_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(WatchMemoriesRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(YaadServiceServer).WatchMemories(m, &grpc.GenericServerStream[WatchMemoriesRequest, MemoryEvent]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type YaadService_WatchMemoriesServer = grpc.ServerStreamingServer[MemoryEvent] + +func _YaadService_WatchStale_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(WatchStaleRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(YaadServiceServer).WatchStale(m, &grpc.GenericServerStream[WatchStaleRequest, StaleEvent]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type YaadService_WatchStaleServer = grpc.ServerStreamingServer[StaleEvent] + +// YaadService_ServiceDesc is the grpc.ServiceDesc for YaadService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var YaadService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "yaad.YaadService", + HandlerType: (*YaadServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Remember", + Handler: _YaadService_Remember_Handler, + }, + { + MethodName: "Recall", + Handler: _YaadService_Recall_Handler, + }, + { + MethodName: "Context", + Handler: _YaadService_Context_Handler, + }, + { + MethodName: "Forget", + Handler: _YaadService_Forget_Handler, + }, + { + MethodName: "Link", + Handler: _YaadService_Link_Handler, + }, + { + MethodName: "Unlink", + Handler: _YaadService_Unlink_Handler, + }, + { + MethodName: "Subgraph", + Handler: _YaadService_Subgraph_Handler, + }, + { + MethodName: "Impact", + Handler: _YaadService_Impact_Handler, + }, + { + MethodName: "SessionStart", + Handler: _YaadService_SessionStart_Handler, + }, + { + MethodName: "SessionEnd", + Handler: _YaadService_SessionEnd_Handler, + }, + { + MethodName: "Health", + Handler: _YaadService_Health_Handler, + }, + { + MethodName: "GraphStats", + Handler: _YaadService_GraphStats_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "WatchMemories", + Handler: _YaadService_WatchMemories_Handler, + ServerStreams: true, + }, + { + StreamName: "WatchStale", + Handler: _YaadService_WatchStale_Handler, + ServerStreams: true, + }, + }, + Metadata: "yaad.proto", +} diff --git a/go.mod b/go.mod index d32b1ec..346acad 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,8 @@ require ( go.opentelemetry.io/otel/metric v1.44.0 go.opentelemetry.io/otel/sdk/metric v1.44.0 golang.org/x/text v0.37.0 + google.golang.org/grpc v1.82.0 + google.golang.org/protobuf v1.36.11 modernc.org/sqlite v1.51.0 ) @@ -29,7 +31,9 @@ require ( go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/sdk v1.44.0 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect + golang.org/x/net v0.53.0 // indirect golang.org/x/sys v0.45.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect modernc.org/libc v1.72.5 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go.sum b/go.sum index 3d2aacf..36097c2 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= @@ -65,6 +67,8 @@ go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/ go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= @@ -73,6 +77,14 @@ golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= +google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY= diff --git a/internal/server/grpc.go b/internal/server/grpc.go new file mode 100644 index 0000000..316bfe6 --- /dev/null +++ b/internal/server/grpc.go @@ -0,0 +1,415 @@ +package server + +import ( + "context" + "fmt" + "net" + "strings" + "time" + + yaadversion "github.com/GrayCodeAI/yaad" + yaadproto "github.com/GrayCodeAI/yaad/api/proto" + "github.com/GrayCodeAI/yaad/engine" + "github.com/GrayCodeAI/yaad/graph" + "github.com/GrayCodeAI/yaad/storage" + "github.com/google/uuid" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// GrpcServer implements yaadproto.YaadServiceServer, the gRPC counterpart to +// RESTServer and MCPServer — all three are thin adapters over the same +// *engine.Engine. Unlike REST/MCP, streaming (WatchMemories/WatchStale) is +// only available if a broadcaster is supplied; without one those two RPCs +// return Unavailable, mirroring REST's 503 when streaming isn't enabled. +type GrpcServer struct { + yaadproto.UnimplementedYaadServiceServer + + eng *engine.Engine + broadcaster *SSEBroadcaster // nil = WatchMemories/WatchStale disabled + limiter *engine.RateLimiter // rate limiter for destructive operations, mirrors RESTServer + + srv *grpc.Server +} + +// NewGrpcServer creates a gRPC server backed by eng. broadcaster may be nil, +// in which case WatchMemories and WatchStale return Unavailable. +func NewGrpcServer(eng *engine.Engine, broadcaster *SSEBroadcaster) *GrpcServer { + return &GrpcServer{ + eng: eng, + broadcaster: broadcaster, + limiter: engine.NewRateLimiter(10, 20), // 10 req/sec, burst 20 — matches RESTServer + } +} + +// ListenAndServe starts the gRPC server on addr. Blocks until the server +// stops (via Shutdown or a fatal error). +func (s *GrpcServer) ListenAndServe(addr string) error { + lis, err := net.Listen("tcp", addr) + if err != nil { + return fmt.Errorf("yaad grpc: listen on %s: %w", addr, err) + } + s.srv = grpc.NewServer() + yaadproto.RegisterYaadServiceServer(s.srv, s) + fmt.Printf("yaad gRPC API listening on %s\n", addr) + return s.srv.Serve(lis) +} + +// Shutdown gracefully stops the gRPC server. Unlike grpc.Server.GracefulStop +// (which has no timeout), this respects ctx's deadline by falling back to a +// hard Stop if graceful shutdown doesn't finish in time. +func (s *GrpcServer) Shutdown(ctx context.Context) error { + if s.srv == nil { + return nil + } + done := make(chan struct{}) + go func() { + s.srv.GracefulStop() + close(done) + }() + select { + case <-done: + return nil + case <-ctx.Done(): + s.srv.Stop() + return ctx.Err() + } +} + +func (s *GrpcServer) rateLimit() error { + if s.limiter != nil && !s.limiter.Allow() { + return status.Error(codes.ResourceExhausted, "rate limit exceeded, try again later") + } + return nil +} + +// --- unary RPCs ------------------------------------------------------- + +func (s *GrpcServer) Remember(ctx context.Context, req *yaadproto.RememberRequest) (*yaadproto.RememberResponse, error) { + in := engine.RememberInput{ + Content: req.GetContent(), + Type: req.GetCategory(), + Scope: req.GetScope(), + Project: req.GetProject(), + Tags: strings.Join(req.GetTags(), ","), + } + if in.Type != "" && !engine.IsValidNodeType(in.Type) { + return nil, status.Errorf(codes.InvalidArgument, "invalid node type: %q", in.Type) + } + node, err := s.eng.Remember(ctx, in) + if err != nil { + return nil, status.Errorf(codes.Internal, "remember: %v", err) + } + return &yaadproto.RememberResponse{ + NodeId: node.ID, + Created: node.Version == 1, // see engine.Remember: Version 1 = newly created, >1 = upsert/update + }, nil +} + +func (s *GrpcServer) Recall(ctx context.Context, req *yaadproto.RecallRequest) (*yaadproto.RecallResponse, error) { + result, err := s.eng.Recall(ctx, engine.RecallOpts{ + Query: req.GetQuery(), + Budget: int(req.GetTokenBudget()), + Project: req.GetProject(), + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "recall: %v", err) + } + text := renderContextText(result.Nodes) + return &yaadproto.RecallResponse{ + Context: text, + Nodes: nodesToProto(result.Nodes), + TokensUsed: roughTokenCount(text), + }, nil +} + +func (s *GrpcServer) Context(ctx context.Context, req *yaadproto.ContextRequest) (*yaadproto.ContextResponse, error) { + result, err := s.eng.Context(ctx, req.GetProject()) + if err != nil { + return nil, status.Errorf(codes.Internal, "context: %v", err) + } + text := renderContextText(result.Nodes) + sources := make([]string, 0, len(result.Nodes)) + for _, n := range result.Nodes { + sources = append(sources, n.ID) + } + return &yaadproto.ContextResponse{ + Context: text, + TokensUsed: roughTokenCount(text), + Sources: sources, + }, nil +} + +func (s *GrpcServer) Forget(ctx context.Context, req *yaadproto.ForgetRequest) (*yaadproto.ForgetResponse, error) { + if err := s.rateLimit(); err != nil { + return nil, err + } + if err := s.eng.Forget(ctx, req.GetNodeId()); err != nil { + return nil, status.Errorf(codes.NotFound, "forget: %v", err) + } + return &yaadproto.ForgetResponse{Deleted: true}, nil +} + +func (s *GrpcServer) Link(ctx context.Context, req *yaadproto.LinkRequest) (*yaadproto.LinkResponse, error) { + if req.GetType() == "" { + return nil, status.Error(codes.InvalidArgument, "edge type is required") + } + if !graph.IsValidEdgeType(req.GetType()) { + return nil, status.Errorf(codes.InvalidArgument, "invalid edge type: %q", req.GetType()) + } + edge := &storage.Edge{ + ID: uuid.New().String(), + FromID: req.GetFromId(), + ToID: req.GetToId(), + Type: req.GetType(), + } + if err := s.eng.Graph().AddEdge(ctx, edge); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "link: %v", err) + } + return &yaadproto.LinkResponse{EdgeId: edge.ID}, nil +} + +func (s *GrpcServer) Unlink(ctx context.Context, req *yaadproto.UnlinkRequest) (*yaadproto.UnlinkResponse, error) { + if err := s.rateLimit(); err != nil { + return nil, err + } + if err := s.eng.Graph().RemoveEdge(ctx, req.GetEdgeId()); err != nil { + return nil, status.Errorf(codes.NotFound, "unlink: %v", err) + } + return &yaadproto.UnlinkResponse{Deleted: true}, nil +} + +func (s *GrpcServer) Subgraph(ctx context.Context, req *yaadproto.SubgraphRequest) (*yaadproto.SubgraphResponse, error) { + depth := int(req.GetDepth()) + if depth <= 0 { + depth = 2 + } + if depth > maxGraphDepth { + return nil, status.Errorf(codes.InvalidArgument, "depth exceeds maximum of %d", maxGraphDepth) + } + sg, err := s.eng.Graph().ExtractSubgraph(ctx, req.GetNodeId(), depth) + if err != nil { + return nil, status.Errorf(codes.Internal, "subgraph: %v", err) + } + return &yaadproto.SubgraphResponse{ + Nodes: nodesToProto(sg.Nodes), + Edges: edgesToProto(sg.Edges), + }, nil +} + +// Impact maps to the same graph-traversal impact analysis REST exposes at +// GET /yaad/impact/{file} (Graph().Impact), not the separate node-centric +// POST /yaad/impact/analyze (Engine.AnalyzeImpact) — the proto request shape +// (repeated changed_files + project) matches the file-based traversal, not +// AnalyzeImpact's single node_id. +func (s *GrpcServer) Impact(ctx context.Context, req *yaadproto.ImpactRequest) (*yaadproto.ImpactResponse, error) { + const depth = 3 + seen := make(map[string]bool) + var affected []string + for _, file := range req.GetChangedFiles() { + ids, err := s.eng.Graph().Impact(ctx, file, depth) + if err != nil { + return nil, status.Errorf(codes.Internal, "impact: %v", err) + } + for _, id := range ids { + if !seen[id] { + seen[id] = true + affected = append(affected, id) + } + } + } + summary := fmt.Sprintf("%d file(s) changed, %d memory node(s) potentially affected", len(req.GetChangedFiles()), len(affected)) + return &yaadproto.ImpactResponse{ + AffectedNodes: affected, + // StaleNodes is left empty: the graph-traversal impact path doesn't + // itself compute staleness (that's StalenessManager's job, exposed + // via WatchStale) — reporting a guess here would be worse than + // reporting nothing. + Summary: summary, + }, nil +} + +func (s *GrpcServer) SessionStart(ctx context.Context, req *yaadproto.SessionStartRequest) (*yaadproto.SessionStartResponse, error) { + sess := &storage.Session{ + ID: req.GetSessionId(), + Project: req.GetProject(), + StartedAt: time.Now(), + } + if err := s.eng.Store().CreateSession(ctx, sess); err != nil { + return nil, status.Errorf(codes.Internal, "session start: %v", err) + } + result, err := s.eng.Context(ctx, req.GetProject()) + if err != nil { + // Best-effort, matching REST: session creation already succeeded. + return &yaadproto.SessionStartResponse{}, nil + } + return &yaadproto.SessionStartResponse{Context: renderContextText(result.Nodes)}, nil +} + +func (s *GrpcServer) SessionEnd(ctx context.Context, req *yaadproto.SessionEndRequest) (*yaadproto.SessionEndResponse, error) { + summary := req.GetSummary() + if !req.GetSuccess() && summary != "" { + summary = "[unsuccessful] " + summary + } + if err := s.eng.Store().EndSession(ctx, req.GetSessionId(), summary); err != nil { + return nil, status.Errorf(codes.Internal, "session end: %v", err) + } + return &yaadproto.SessionEndResponse{}, nil +} + +func (s *GrpcServer) Health(ctx context.Context, _ *yaadproto.HealthRequest) (*yaadproto.HealthResponse, error) { + if _, err := s.eng.Store().ListNodes(ctx, storage.NodeFilter{}); err != nil { + return &yaadproto.HealthResponse{Status: "error"}, nil + } + stats, err := s.eng.GetMemoryStats(ctx) + if err != nil { + return &yaadproto.HealthResponse{Status: "error"}, nil + } + return &yaadproto.HealthResponse{ + Status: "ok", + NodeCount: int32(stats.TotalNodes), //nolint:gosec // node/edge counts fit comfortably in int32 for any real deployment + EdgeCount: int32(stats.TotalEdges), //nolint:gosec + Version: yaadversion.String(), + }, nil +} + +func (s *GrpcServer) GraphStats(ctx context.Context, _ *yaadproto.GraphStatsRequest) (*yaadproto.GraphStatsResponse, error) { + stats, err := s.eng.GetMemoryStats(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "graph stats: %v", err) + } + byType := make(map[string]int32, len(stats.NodesByType)) + for k, v := range stats.NodesByType { + byType[k] = int32(v) //nolint:gosec // per-type node counts fit comfortably in int32 + } + return &yaadproto.GraphStatsResponse{ + TotalNodes: int32(stats.TotalNodes), //nolint:gosec + TotalEdges: int32(stats.TotalEdges), //nolint:gosec + NodesByType: byType, + // Communities isn't computed by GetMemoryStats today — left at the + // zero value rather than approximated. + }, nil +} + +// --- streaming RPCs ----------------------------------------------------- + +func (s *GrpcServer) WatchMemories(req *yaadproto.WatchMemoriesRequest, stream yaadproto.YaadService_WatchMemoriesServer) error { + if s.broadcaster == nil { + return status.Error(codes.Unavailable, "streaming not enabled") + } + ch, unsub := s.broadcaster.subscribeMemory() + defer unsub() + + ctx := stream.Context() + for { + select { + case <-ctx.Done(): + return ctx.Err() + case evt, ok := <-ch: + if !ok { + return nil + } + if err := stream.Send(&yaadproto.MemoryEvent{ + Type: string(evt.Type), + NodeId: evt.NodeID, + Content: evt.Content, + Timestamp: evt.Timestamp, + }); err != nil { + return err + } + } + } +} + +func (s *GrpcServer) WatchStale(req *yaadproto.WatchStaleRequest, stream yaadproto.YaadService_WatchStaleServer) error { + if s.broadcaster == nil { + return status.Error(codes.Unavailable, "streaming not enabled") + } + ch, unsub := s.broadcaster.subscribeStale() + defer unsub() + + ctx := stream.Context() + for { + select { + case <-ctx.Done(): + return ctx.Err() + case evt, ok := <-ch: + if !ok { + return nil + } + if err := stream.Send(&yaadproto.StaleEvent{ + NodeId: evt.NodeID, + Reason: evt.Reason, + FilePath: evt.FilePath, + Timestamp: evt.Timestamp, + }); err != nil { + return err + } + } + } +} + +// --- conversion helpers --------------------------------------------------- + +func nodesToProto(nodes []*storage.Node) []*yaadproto.MemoryNode { + out := make([]*yaadproto.MemoryNode, 0, len(nodes)) + for _, n := range nodes { + out = append(out, &yaadproto.MemoryNode{ + Id: n.ID, + Type: n.Type, + Content: n.Content, + Summary: n.Summary, + Confidence: float32(n.Confidence), + CreatedAt: n.CreatedAt.Unix(), + AccessedAt: n.AccessedAt.Unix(), + }) + } + return out +} + +func edgesToProto(edges []*storage.Edge) []*yaadproto.MemoryEdge { + out := make([]*yaadproto.MemoryEdge, 0, len(edges)) + for _, e := range edges { + out = append(out, &yaadproto.MemoryEdge{ + Id: e.ID, + FromId: e.FromID, + ToId: e.ToID, + Type: e.Type, + Weight: float32(e.Weight), + }) + } + return out +} + +// renderContextText synthesizes the "context" text blob the .proto's +// RecallResponse/ContextResponse/SessionStartResponse promise. No existing +// transport (REST, MCP) does this today — both return the structured +// RecallResult{Nodes, Edges} as-is — so this is new, minimal rendering +// logic, not a port of an existing formatter. +func renderContextText(nodes []*storage.Node) string { + if len(nodes) == 0 { + return "" + } + var sb strings.Builder + for i, n := range nodes { + if i > 0 { + sb.WriteString("\n\n") + } + if n.Summary != "" { + sb.WriteString(n.Summary) + sb.WriteString(": ") + } + sb.WriteString(n.Content) + } + return sb.String() +} + +// roughTokenCount is a rough chars/4 heuristic, not a real tokenizer — good +// enough for the proto's tokens_used field, which no existing transport +// populates today either. Do not use this for real budget enforcement +// (Engine.Context/TrimToTokenBudget already do that internally). +func roughTokenCount(s string) int32 { + return int32(len(s) / 4) //nolint:gosec // bounded by response size limits well under int32 range +} diff --git a/internal/server/grpc_test.go b/internal/server/grpc_test.go new file mode 100644 index 0000000..dbecff4 --- /dev/null +++ b/internal/server/grpc_test.go @@ -0,0 +1,270 @@ +package server + +import ( + "context" + "net" + "path/filepath" + "testing" + "time" + + yaadproto "github.com/GrayCodeAI/yaad/api/proto" + "github.com/GrayCodeAI/yaad/engine" + "github.com/GrayCodeAI/yaad/graph" + "github.com/GrayCodeAI/yaad/storage" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/test/bufconn" +) + +// setupGrpcServer mirrors setupMCPServer (mcp_test.go): a *GrpcServer backed +// by a temporary SQLite store, served in-memory over bufconn so no real +// network port is used. +func setupGrpcServer(t *testing.T, withBroadcaster bool) (yaadproto.YaadServiceClient, *engine.Engine, func()) { + t.Helper() + dir := t.TempDir() + store, err := storage.NewStore(filepath.Join(dir, "test.db")) + if err != nil { + t.Fatal(err) + } + eng := engine.New(store, graph.New(store, store.DB())) + + var broadcaster *SSEBroadcaster + var stopBroadcast func() + if withBroadcaster { + broadcaster = NewSSEBroadcaster(eng) + bctx, cancel := context.WithCancel(context.Background()) + go broadcaster.Start(bctx) + stopBroadcast = cancel + } + + grpcSrv := NewGrpcServer(eng, broadcaster) + + lis := bufconn.Listen(1024 * 1024) + s := grpc.NewServer() + yaadproto.RegisterYaadServiceServer(s, grpcSrv) + go func() { _ = s.Serve(lis) }() + + conn, err := grpc.NewClient( + "passthrough:///bufnet", + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return lis.Dial() }), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + t.Fatal(err) + } + client := yaadproto.NewYaadServiceClient(conn) + + cleanup := func() { + _ = conn.Close() + s.Stop() + if stopBroadcast != nil { + stopBroadcast() + } + eng.Close() + _ = store.Close() + } + return client, eng, cleanup +} + +func TestGrpc_RememberAndRecall(t *testing.T) { + client, _, cleanup := setupGrpcServer(t, false) + defer cleanup() + ctx := context.Background() + + rememberResp, err := client.Remember(ctx, &yaadproto.RememberRequest{ + Content: "the deploy script lives in scripts/deploy.sh", + Category: "convention", + Scope: "project", + Project: "proj-a", + Tags: []string{"deploy", "scripts"}, + }) + if err != nil { + t.Fatalf("Remember: %v", err) + } + if rememberResp.GetNodeId() == "" { + t.Fatal("Remember returned empty node_id") + } + if !rememberResp.GetCreated() { + t.Error("Remember: Created = false, want true for a new node") + } + + recallResp, err := client.Recall(ctx, &yaadproto.RecallRequest{ + Query: "deploy script", + Project: "proj-a", + }) + if err != nil { + t.Fatalf("Recall: %v", err) + } + if len(recallResp.GetNodes()) == 0 { + t.Error("Recall returned no nodes for a query matching the remembered content") + } + found := false + for _, n := range recallResp.GetNodes() { + if n.GetId() == rememberResp.GetNodeId() { + found = true + } + } + if !found { + t.Error("Recall didn't return the just-remembered node") + } +} + +func TestGrpc_RememberInvalidCategory(t *testing.T) { + client, _, cleanup := setupGrpcServer(t, false) + defer cleanup() + + _, err := client.Remember(context.Background(), &yaadproto.RememberRequest{ + Content: "x", + Category: "not-a-real-type", + Project: "proj-a", + }) + if err == nil { + t.Fatal("expected an error for an invalid category/type, got nil") + } +} + +func TestGrpc_LinkForgetUnlink(t *testing.T) { + client, _, cleanup := setupGrpcServer(t, false) + defer cleanup() + ctx := context.Background() + + a, err := client.Remember(ctx, &yaadproto.RememberRequest{Content: "node a", Category: "convention", Project: "p"}) + if err != nil { + t.Fatalf("Remember(a): %v", err) + } + b, err := client.Remember(ctx, &yaadproto.RememberRequest{Content: "node b", Category: "convention", Project: "p"}) + if err != nil { + t.Fatalf("Remember(b): %v", err) + } + + link, err := client.Link(ctx, &yaadproto.LinkRequest{ + FromId: a.GetNodeId(), + ToId: b.GetNodeId(), + Type: "relates_to", + }) + if err != nil { + t.Fatalf("Link: %v", err) + } + if link.GetEdgeId() == "" { + t.Fatal("Link returned empty edge_id") + } + + sg, err := client.Subgraph(ctx, &yaadproto.SubgraphRequest{NodeId: a.GetNodeId(), Depth: 2}) + if err != nil { + t.Fatalf("Subgraph: %v", err) + } + if len(sg.GetEdges()) == 0 { + t.Error("Subgraph returned no edges after Link") + } + + if _, err := client.Unlink(ctx, &yaadproto.UnlinkRequest{EdgeId: link.GetEdgeId()}); err != nil { + t.Fatalf("Unlink: %v", err) + } + + forgetResp, err := client.Forget(ctx, &yaadproto.ForgetRequest{NodeId: a.GetNodeId()}) + if err != nil { + t.Fatalf("Forget: %v", err) + } + if !forgetResp.GetDeleted() { + t.Error("Forget: Deleted = false, want true") + } +} + +func TestGrpc_LinkInvalidEdgeType(t *testing.T) { + client, _, cleanup := setupGrpcServer(t, false) + defer cleanup() + + _, err := client.Link(context.Background(), &yaadproto.LinkRequest{ + FromId: "a", ToId: "b", Type: "not-a-real-edge-type", + }) + if err == nil { + t.Fatal("expected an error for an invalid edge type, got nil") + } +} + +func TestGrpc_HealthAndGraphStats(t *testing.T) { + client, _, cleanup := setupGrpcServer(t, false) + defer cleanup() + ctx := context.Background() + + if _, err := client.Remember(ctx, &yaadproto.RememberRequest{Content: "x", Category: "convention", Project: "p"}); err != nil { + t.Fatalf("Remember: %v", err) + } + + health, err := client.Health(ctx, &yaadproto.HealthRequest{}) + if err != nil { + t.Fatalf("Health: %v", err) + } + if health.GetStatus() != "ok" { + t.Errorf("Health.Status = %q, want ok", health.GetStatus()) + } + if health.GetNodeCount() < 1 { + t.Errorf("Health.NodeCount = %d, want >= 1", health.GetNodeCount()) + } + + stats, err := client.GraphStats(ctx, &yaadproto.GraphStatsRequest{}) + if err != nil { + t.Fatalf("GraphStats: %v", err) + } + if stats.GetTotalNodes() < 1 { + t.Errorf("GraphStats.TotalNodes = %d, want >= 1", stats.GetTotalNodes()) + } +} + +func TestGrpc_SessionStartEnd(t *testing.T) { + client, _, cleanup := setupGrpcServer(t, false) + defer cleanup() + ctx := context.Background() + + if _, err := client.SessionStart(ctx, &yaadproto.SessionStartRequest{SessionId: "sess-1", Project: "p"}); err != nil { + t.Fatalf("SessionStart: %v", err) + } + if _, err := client.SessionEnd(ctx, &yaadproto.SessionEndRequest{SessionId: "sess-1", Summary: "done", Success: true}); err != nil { + t.Fatalf("SessionEnd: %v", err) + } +} + +func TestGrpc_WatchMemoriesWithoutBroadcaster(t *testing.T) { + client, _, cleanup := setupGrpcServer(t, false) + defer cleanup() + + stream, err := client.WatchMemories(context.Background(), &yaadproto.WatchMemoriesRequest{}) + if err != nil { + t.Fatalf("WatchMemories (open): %v", err) + } + if _, err := stream.Recv(); err == nil { + t.Fatal("expected an Unavailable error when no broadcaster is configured, got nil") + } +} + +func TestGrpc_WatchMemoriesDeliversEvent(t *testing.T) { + client, _, cleanup := setupGrpcServer(t, true) + defer cleanup() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + stream, err := client.WatchMemories(ctx, &yaadproto.WatchMemoriesRequest{}) + if err != nil { + t.Fatalf("WatchMemories: %v", err) + } + + // Give the stream a moment to subscribe before triggering the event. + time.Sleep(50 * time.Millisecond) + + if _, err := client.Remember(ctx, &yaadproto.RememberRequest{Content: "watched", Category: "convention", Project: "p"}); err != nil { + t.Fatalf("Remember: %v", err) + } + + evt, err := stream.Recv() + if err != nil { + t.Fatalf("stream.Recv: %v", err) + } + if evt.GetType() != "created" { + t.Errorf("event Type = %q, want created", evt.GetType()) + } + if evt.GetContent() != "watched" { + t.Errorf("event Content = %q, want %q", evt.GetContent(), "watched") + } +} From 00b44c29aeac39ac9006757aca735acc2b2a21f0 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Wed, 15 Jul 2026 11:17:37 +0530 Subject: [PATCH 2/6] Ingest LoCoMo-10 external benchmark, publish first honest baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit internal/bench's harness (R@K/MRR) already worked but had only an 18-question self-seeded QA set, no external-corpus coverage. Adds benchmark/locomo, a data adapter for the public LoCoMo-10 dataset (snap-research/locomo), and benchmark/cmd/locomobench to run it — the harness itself is reused unmodified. Ran it for real: 1,540 non-adversarial QA pairs across 10 conversations, R@1 3.1%/R@10 19.1%/MRR 0.072, published under benchmark/results/yaad-locomo/2026-07-15/. Numbers are structurally lower than yaad-longmem-core's self-seeded baseline by design, not regression — see that run's notes.md for why (substring-match proxy vs. LoCoMo's real LLM-judged generation metric, paraphrased/computed answers). --- benchmark/README.md | 13 +- benchmark/cmd/locomobench/main.go | 122 +++++++++++++++ benchmark/locomo/adapter.go | 73 +++++++++ benchmark/locomo/locomo.go | 147 ++++++++++++++++++ benchmark/locomo/locomo_test.go | 134 ++++++++++++++++ benchmark/manifests/yaad-locomo.yaml | 28 ++++ benchmark/results/README.md | 1 + .../results/yaad-locomo/2026-07-15/notes.md | 49 ++++++ .../results/yaad-locomo/2026-07-15/report.md | 49 ++++++ .../results/yaad-locomo/2026-07-15/result.txt | 115 ++++++++++++++ benchmark/results/yaad-locomo/README.md | 13 ++ 11 files changed, 742 insertions(+), 2 deletions(-) create mode 100644 benchmark/cmd/locomobench/main.go create mode 100644 benchmark/locomo/adapter.go create mode 100644 benchmark/locomo/locomo.go create mode 100644 benchmark/locomo/locomo_test.go create mode 100644 benchmark/manifests/yaad-locomo.yaml create mode 100644 benchmark/results/yaad-locomo/2026-07-15/notes.md create mode 100644 benchmark/results/yaad-locomo/2026-07-15/report.md create mode 100644 benchmark/results/yaad-locomo/2026-07-15/result.txt create mode 100644 benchmark/results/yaad-locomo/README.md diff --git a/benchmark/README.md b/benchmark/README.md index 99b14ab..85df370 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -13,7 +13,8 @@ It complements: - `yaad-longmem-core` - `yaad-microbench` -- `yaad-locomo-beam-publication` (planned) +- `yaad-locomo` (LoCoMo-10 external corpus; see caveats in its published run's `notes.md`) +- `yaad-locomo-beam-publication` (planned — broader BEAM-benchmark coverage in addition to LoCoMo; `yaad-locomo` above covers LoCoMo only) See the Hawk-side benchmark registry at `hawk/docs/benchmarks/SUITES.md`. @@ -21,16 +22,19 @@ See the Hawk-side benchmark registry at `hawk/docs/benchmarks/SUITES.md`. - `yaad-longmem-core`: `results/yaad-longmem-core/2026-06-27/` - `yaad-microbench`: `results/yaad-microbench/2026-06-27/` +- `yaad-locomo`: `results/yaad-locomo/2026-07-15/` ## Current runnable commands ```bash /bin/zsh -lc 'GOCACHE=$PWD/.gocache go run ./benchmark/cmd/longmemreport --suite default' +/bin/zsh -lc 'GOCACHE=$PWD/.gocache go run ./benchmark/cmd/locomobench' go test ./benchmark/ -bench=. -benchmem -benchtime=1s go test ./benchmark/ -bench=BenchmarkRemember -benchmem go test ./benchmark/ -bench=BenchmarkRecall -benchmem go test ./benchmark/ -bench=BenchmarkGraphBFS -benchmem /bin/zsh -lc 'GOCACHE=$PWD/.gocache go test ./internal/bench -count=1' +/bin/zsh -lc 'GOCACHE=$PWD/.gocache go test ./benchmark/locomo -count=1' ``` ## Publication layout @@ -52,11 +56,16 @@ benchmark/ report.md result.txt notes.md + yaad-locomo/ + 2026-07-15/ + report.md + result.txt + notes.md ``` ## Required metrics -### `yaad-longmem-core` +### `yaad-longmem-core`, `yaad-locomo` - R@1 - R@3 diff --git a/benchmark/cmd/locomobench/main.go b/benchmark/cmd/locomobench/main.go new file mode 100644 index 0000000..e208499 --- /dev/null +++ b/benchmark/cmd/locomobench/main.go @@ -0,0 +1,122 @@ +// Command locomobench runs yaad's retrieval-quality harness against the +// public LoCoMo-10 long-term conversational memory benchmark, as opposed to +// longmemreport's first-party self-seeded QA set. Each of LoCoMo-10's 10 +// conversations is benchmarked against its own freshly seeded engine (see +// locomo.SeedInputs for why), then results are aggregated. +package main + +import ( + "context" + "flag" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/GrayCodeAI/yaad/benchmark/locomo" + "github.com/GrayCodeAI/yaad/engine" + "github.com/GrayCodeAI/yaad/graph" + bench "github.com/GrayCodeAI/yaad/internal/bench" + "github.com/GrayCodeAI/yaad/storage" +) + +func main() { + var ( + depth = flag.Int("depth", 2, "recall depth") + limit = flag.Int("limit", 10, "recall limit") + datasetURL = flag.String("dataset-url", locomo.DatasetURL, "LoCoMo-10 dataset URL to download") + cacheFile = flag.String("cache", "", "local path to a previously downloaded LoCoMo-10 JSON file (skips download if present)") + ) + flag.Parse() + + ctx := context.Background() + + datasetPath := *cacheFile + if datasetPath == "" { + dir := mustTempDir() + datasetPath = filepath.Join(dir, "locomo10.json") + } + if _, err := os.Stat(datasetPath); err != nil { + fmt.Fprintf(os.Stderr, "Downloading LoCoMo-10 dataset from %s...\n", *datasetURL) + must(locomo.Download(*datasetURL, datasetPath)) + } + + samples, err := locomo.Load(datasetPath) + must(err) + + fmt.Printf("Suite: yaad-locomo\n") + fmt.Printf("Samples: %d\n", len(samples)) + fmt.Printf("Depth: %d\n", *depth) + fmt.Printf("Limit: %d\n", *limit) + fmt.Printf("Timestamp: %s\n\n", time.Now().UTC().Format(time.RFC3339)) + + agg := &bench.Result{HitAtK: map[int]int{1: 0, 3: 0, 5: 0, 10: 0}} + var mrrWeighted, tokensWeighted float64 + + for _, sample := range samples { + result, err := runSample(ctx, sample, *depth, *limit) + if err != nil { + fmt.Fprintf(os.Stderr, "sample %s: %v\n", sample.SampleID, err) + continue + } + fmt.Printf("--- %s (%d questions) ---\n", sample.SampleID, result.Total) + fmt.Print(result.String()) + fmt.Println() + + agg.Total += result.Total + for _, k := range []int{1, 3, 5, 10} { + agg.HitAtK[k] += result.HitAtK[k] + } + mrrWeighted += result.MRR * float64(result.Total) + tokensWeighted += result.AvgTokens * float64(result.Total) + agg.Duration += result.Duration + } + + if agg.Total > 0 { + agg.MRR = mrrWeighted / float64(agg.Total) + agg.AvgTokens = tokensWeighted / float64(agg.Total) + } + + fmt.Println("=== Aggregate across all samples ===") + fmt.Print(agg.String()) +} + +// runSample seeds a fresh, isolated engine with one LoCoMo sample's +// dialogue turns and runs bench.Run against that sample's QA pairs. +func runSample(ctx context.Context, sample locomo.Sample, depth, limit int) (*bench.Result, error) { + dir, err := os.MkdirTemp("", "yaad-locomo-*") + if err != nil { + return nil, err + } + defer func() { _ = os.RemoveAll(dir) }() + + store, err := storage.NewStore(filepath.Join(dir, "locomo.db")) + if err != nil { + return nil, err + } + defer func() { _ = store.Close() }() + + eng := engine.New(store, graph.New(store, store.DB())) + defer eng.Close() + + for _, in := range sample.SeedInputs() { + if _, err := eng.Remember(ctx, in); err != nil { + return nil, fmt.Errorf("seed: %w", err) + } + } + + qas := sample.ToQAs() + return bench.Run(ctx, eng, qas, depth, limit), nil +} + +func must(err error) { + if err != nil { + panic(err) + } +} + +func mustTempDir() string { + dir, err := os.MkdirTemp("", "yaad-locomo-cache-*") + must(err) + return dir +} diff --git a/benchmark/locomo/adapter.go b/benchmark/locomo/adapter.go new file mode 100644 index 0000000..cc510ab --- /dev/null +++ b/benchmark/locomo/adapter.go @@ -0,0 +1,73 @@ +package locomo + +import ( + "fmt" + + "github.com/GrayCodeAI/yaad/engine" + bench "github.com/GrayCodeAI/yaad/internal/bench" +) + +// SeedInputs converts a Sample's dialogue turns into engine.RememberInput +// values, one per turn (": "), tagged with project=sample_id. +// The project tag is informational only: bench.Run's Recall calls don't +// filter by project, so true isolation between LoCoMo-10's 10 independent +// conversations comes from running each sample against its own freshly +// seeded *engine.Engine (see locomobench/main.go), not from this tag — +// seeding multiple samples into one shared engine would let recall answer +// one conversation's questions from another's dialogue, which is a +// correctness bug, not a harder benchmark. Type "session" is the closest +// fit among yaad's node types (convention/decision/bug/spec/task/ +// preference/skill/file/entity/session) for raw conversational content — +// the others are coding-specific and don't apply to LoCoMo's chat +// transcripts. +func (s Sample) SeedInputs() []engine.RememberInput { + var inputs []engine.RememberInput + for _, sess := range s.Sessions() { + for _, turn := range sess.Turns { + inputs = append(inputs, engine.RememberInput{ + Type: "session", + Content: fmt.Sprintf("%s: %s", turn.Speaker, turn.Text), + Scope: "project", + Project: s.SampleID, + Tags: "locomo," + turn.DiaID, + }) + } + } + return inputs +} + +// ToQAs converts a Sample's QA pairs into bench.QA, matching by expected +// content (the LoCoMo answer text) rather than by node ID, since LoCoMo's +// answers are free-text, not references to specific ingested nodes. +// +// Category-5 (adversarial/unanswerable) questions are skipped: they carry +// no "answer" field, only "adversarial_answer" (real-world context for why +// the question is unanswerable) — bench's substring-match harness has no +// way to score "correctly recalled nothing," so including them would just +// be permanent misses inflating the miss count without measuring anything +// real. +// +// This is a real, documented harness limitation, not a LoCoMo-specific one: +// LoCoMo's own official evaluation grades answers via an LLM judge against +// free-text generation, not a substring-match-in-top-K retrieval hit; a +// substring match is a materially weaker (but network/API-key-free, fully +// offline) proxy. Treat these numbers as a retrieval-hit proxy score, not +// as directly comparable to published LoCoMo leaderboard numbers, which +// score generation quality, not retrieval. +func (s Sample) ToQAs() []bench.QA { + var qas []bench.QA + for _, qa := range s.QA { + if qa.Category == categoryAdversarial { + continue + } + answer := qa.AnswerText() + if answer == "" { + continue + } + qas = append(qas, bench.QA{ + Question: qa.Question, + ExpectedContent: answer, + }) + } + return qas +} diff --git a/benchmark/locomo/locomo.go b/benchmark/locomo/locomo.go new file mode 100644 index 0000000..cf9312b --- /dev/null +++ b/benchmark/locomo/locomo.go @@ -0,0 +1,147 @@ +// Package locomo adapts the public LoCoMo-10 long-term conversational +// memory benchmark (snap-research/locomo) into internal/bench's QA harness, +// so yaad's retrieval quality can be measured against a standard external +// corpus rather than only the first-party self-seeded QA set in +// internal/bench.go's DefaultQAs/CodingBenchQAs. +// +// This package only adapts data — it does not modify internal/bench, whose +// Run/QA/Result types are reused as-is. +package locomo + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" +) + +// DatasetURL is the public LoCoMo-10 dataset file. Exported so callers (the +// locomobench command) can override it, e.g. to point at a local mirror. +const DatasetURL = "https://raw.githubusercontent.com/snap-research/locomo/main/data/locomo10.json" + +// categoryAdversarial is LoCoMo's "unanswerable" question category: these +// QAs have no "answer" field (only "adversarial_answer", the real-world +// context explaining why the question can't be answered from the +// conversation) and are excluded by ToQAs — a substring-match retrieval-hit +// harness has no way to score "the model correctly recalled nothing." +const categoryAdversarial = 5 + +// Turn is a single dialogue line within a session. +type Turn struct { + Speaker string `json:"speaker"` + DiaID string `json:"dia_id"` + Text string `json:"text"` +} + +// QAPair is one question/answer/evidence entry from a LoCoMo sample. +type QAPair struct { + Question string `json:"question"` + Answer any `json:"answer"` // string or number depending on the question; see AnswerText + Evidence []string `json:"evidence"` + Category int `json:"category"` +} + +// AnswerText renders Answer as a string regardless of its underlying JSON +// type (LoCoMo mixes string and numeric answers, e.g. ages). +func (q QAPair) AnswerText() string { + switch v := q.Answer.(type) { + case string: + return v + case nil: + return "" + default: + return fmt.Sprint(v) + } +} + +// Sample is one LoCoMo conversation: up to ~35 sessions between two +// speakers, plus QA pairs whose evidence cites specific dialogue turns. +// Conversation is left as a raw map because session keys are numbered +// ("session_1", "session_1_date_time", "session_2", ...) rather than a +// fixed schema — Sessions() below extracts them in order. +type Sample struct { + SampleID string `json:"sample_id"` + QA []QAPair `json:"qa"` + Conversation map[string]any `json:"conversation"` +} + +// Session is one numbered session's turns plus its date/time label. +type Session struct { + Number int + DateTime string + Turns []Turn +} + +// Sessions extracts s.Conversation's numbered sessions in order. LoCoMo +// samples have a variable number of sessions (not fixed across samples), so +// this scans session_1..session_N until a number is missing rather than +// assuming a fixed count. +func (s Sample) Sessions() []Session { + var sessions []Session + for n := 1; ; n++ { + key := fmt.Sprintf("session_%d", n) + raw, ok := s.Conversation[key] + if !ok { + break + } + data, err := json.Marshal(raw) + if err != nil { + break + } + var turns []Turn + if err := json.Unmarshal(data, &turns); err != nil { + break + } + dateTime, _ := s.Conversation[key+"_date_time"].(string) + sessions = append(sessions, Session{Number: n, DateTime: dateTime, Turns: turns}) + } + return sessions +} + +// Load parses a local LoCoMo-10 JSON file (as saved by Download). +func Load(path string) ([]Sample, error) { + f, err := os.Open(path) //nolint:gosec // path is operator-supplied (CLI flag / test fixture), not untrusted input + if err != nil { + return nil, fmt.Errorf("locomo: open %s: %w", path, err) + } + defer func() { _ = f.Close() }() + return Parse(f) +} + +// Parse decodes a LoCoMo-10 dataset from r. +func Parse(r io.Reader) ([]Sample, error) { + var samples []Sample + if err := json.NewDecoder(r).Decode(&samples); err != nil { + return nil, fmt.Errorf("locomo: decode dataset: %w", err) + } + return samples, nil +} + +// Download fetches url (typically DatasetURL) and saves it to destPath, +// so a benchmark run doesn't depend on network access at eval time and the +// downloaded copy can be inspected/reused. +func Download(url, destPath string) error { + req, err := http.NewRequest(http.MethodGet, url, nil) //nolint:noctx // one-shot CLI download, no cancellation needed + if err != nil { + return fmt.Errorf("locomo: build request: %w", err) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("locomo: download %s: %w", url, err) + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("locomo: download %s: unexpected status %d", url, resp.StatusCode) + } + + out, err := os.Create(destPath) //nolint:gosec // destPath is operator-supplied (CLI flag), not untrusted input + if err != nil { + return fmt.Errorf("locomo: create %s: %w", destPath, err) + } + defer func() { _ = out.Close() }() + if _, err := io.Copy(out, resp.Body); err != nil { //nolint:gosec // dataset size is bounded (a few MB); no DoS concern for a manually-invoked benchmark CLI + return fmt.Errorf("locomo: write %s: %w", destPath, err) + } + return nil +} diff --git a/benchmark/locomo/locomo_test.go b/benchmark/locomo/locomo_test.go new file mode 100644 index 0000000..8128511 --- /dev/null +++ b/benchmark/locomo/locomo_test.go @@ -0,0 +1,134 @@ +package locomo + +import ( + "strings" + "testing" +) + +// fixtureJSON is a small, hand-written stand-in for LoCoMo-10's real shape +// (one sample, two sessions, a mix of QA categories including adversarial) +// so tests don't depend on network access or the multi-MB real dataset. +const fixtureJSON = `[ + { + "sample_id": "conv-test-1", + "qa": [ + {"question": "What did Alice go to yesterday?", "answer": "a support group", "evidence": ["D1:2"], "category": 1}, + {"question": "How old is Bob's dog?", "answer": 3, "evidence": ["D2:1"], "category": 2}, + {"question": "What did Alice realize?", "evidence": ["D1:2"], "category": 5, "adversarial_answer": "self-care matters"} + ], + "conversation": { + "speaker_a": "Alice", + "speaker_b": "Bob", + "session_1_date_time": "1:00 pm on 1 May, 2023", + "session_1": [ + {"speaker": "Alice", "dia_id": "D1:1", "text": "Hey Bob!"}, + {"speaker": "Alice", "dia_id": "D1:2", "text": "I went to a support group yesterday."} + ], + "session_2_date_time": "2:00 pm on 2 May, 2023", + "session_2": [ + {"speaker": "Bob", "dia_id": "D2:1", "text": "My dog is 3 years old."} + ] + } + } +]` + +func TestParse(t *testing.T) { + samples, err := Parse(strings.NewReader(fixtureJSON)) + if err != nil { + t.Fatalf("Parse: %v", err) + } + if len(samples) != 1 { + t.Fatalf("len(samples) = %d, want 1", len(samples)) + } + s := samples[0] + if s.SampleID != "conv-test-1" { + t.Errorf("SampleID = %q, want conv-test-1", s.SampleID) + } + if len(s.QA) != 3 { + t.Fatalf("len(QA) = %d, want 3", len(s.QA)) + } +} + +func TestSample_Sessions(t *testing.T) { + samples, err := Parse(strings.NewReader(fixtureJSON)) + if err != nil { + t.Fatalf("Parse: %v", err) + } + sessions := samples[0].Sessions() + if len(sessions) != 2 { + t.Fatalf("len(sessions) = %d, want 2", len(sessions)) + } + if sessions[0].Number != 1 || sessions[0].DateTime != "1:00 pm on 1 May, 2023" { + t.Errorf("sessions[0] = %+v, unexpected", sessions[0]) + } + if len(sessions[0].Turns) != 2 { + t.Fatalf("len(sessions[0].Turns) = %d, want 2", len(sessions[0].Turns)) + } + if sessions[0].Turns[1].Text != "I went to a support group yesterday." { + t.Errorf("sessions[0].Turns[1].Text = %q, unexpected", sessions[0].Turns[1].Text) + } + if len(sessions[1].Turns) != 1 { + t.Fatalf("len(sessions[1].Turns) = %d, want 1", len(sessions[1].Turns)) + } +} + +func TestQAPair_AnswerText(t *testing.T) { + samples, err := Parse(strings.NewReader(fixtureJSON)) + if err != nil { + t.Fatalf("Parse: %v", err) + } + qa := samples[0].QA + if got := qa[0].AnswerText(); got != "a support group" { + t.Errorf("qa[0].AnswerText() = %q, want %q", got, "a support group") + } + if got := qa[1].AnswerText(); got != "3" { + t.Errorf("qa[1].AnswerText() (numeric answer) = %q, want %q", got, "3") + } + if got := qa[2].AnswerText(); got != "" { + t.Errorf("qa[2].AnswerText() (adversarial, no answer field) = %q, want empty", got) + } +} + +func TestSample_SeedInputs(t *testing.T) { + samples, err := Parse(strings.NewReader(fixtureJSON)) + if err != nil { + t.Fatalf("Parse: %v", err) + } + inputs := samples[0].SeedInputs() + if len(inputs) != 3 { + t.Fatalf("len(inputs) = %d, want 3 (2 turns in session 1 + 1 in session 2)", len(inputs)) + } + for _, in := range inputs { + if in.Type != "session" { + t.Errorf("input Type = %q, want session", in.Type) + } + if in.Project != "conv-test-1" { + t.Errorf("input Project = %q, want conv-test-1", in.Project) + } + } + if inputs[1].Content != "Alice: I went to a support group yesterday." { + t.Errorf("inputs[1].Content = %q, unexpected", inputs[1].Content) + } +} + +func TestSample_ToQAs_SkipsAdversarial(t *testing.T) { + samples, err := Parse(strings.NewReader(fixtureJSON)) + if err != nil { + t.Fatalf("Parse: %v", err) + } + qas := samples[0].ToQAs() + if len(qas) != 2 { + t.Fatalf("len(qas) = %d, want 2 (category-5 adversarial question excluded)", len(qas)) + } + for _, qa := range qas { + if qa.ExpectedContent == "" { + t.Errorf("qa %q has empty ExpectedContent", qa.Question) + } + } + if qas[0].ExpectedContent != "a support group" { + t.Errorf("qas[0].ExpectedContent = %q, want %q", qas[0].ExpectedContent, "a support group") + } + if qas[1].ExpectedContent != "3" { + t.Errorf("qas[1].ExpectedContent = %q, want %q", qas[1].ExpectedContent, "3") + } +} diff --git a/benchmark/manifests/yaad-locomo.yaml b/benchmark/manifests/yaad-locomo.yaml new file mode 100644 index 0000000..7613728 --- /dev/null +++ b/benchmark/manifests/yaad-locomo.yaml @@ -0,0 +1,28 @@ +suite: yaad-locomo +status: shipped +published: true +kind: retrieval-quality +owner: yaad +source: + - yaad/benchmark/locomo/locomo.go + - yaad/benchmark/locomo/adapter.go + - yaad/benchmark/cmd/locomobench/main.go +runner: + command: /bin/zsh -lc 'GOCACHE=$PWD/.gocache go run ./benchmark/cmd/locomobench' +result_format: + primary: text_or_markdown_report +metrics: + - r_at_1 + - r_at_3 + - r_at_5 + - r_at_10 + - mrr + - avg_tokens_per_query + - duration +publication_dir: yaad/benchmark/results/yaad-locomo +notes: + - First external-corpus retrieval-quality suite (LoCoMo-10, snap-research/locomo). + - Scores a substring-match retrieval-hit proxy, not LoCoMo's official LLM-judged + generation metric — see the published run's notes.md for details. + - A separate planned suite id, yaad-locomo-beam-publication, covers eventual + BEAM-benchmark coverage in addition to LoCoMo; this suite is LoCoMo only. diff --git a/benchmark/results/README.md b/benchmark/results/README.md index 1d96280..2d2a760 100644 --- a/benchmark/results/README.md +++ b/benchmark/results/README.md @@ -18,3 +18,4 @@ Current published runs: - `yaad-longmem-core/2026-06-27/` - `yaad-microbench/2026-06-27/` +- `yaad-locomo/2026-07-15/` diff --git a/benchmark/results/yaad-locomo/2026-07-15/notes.md b/benchmark/results/yaad-locomo/2026-07-15/notes.md new file mode 100644 index 0000000..a5fbb89 --- /dev/null +++ b/benchmark/results/yaad-locomo/2026-07-15/notes.md @@ -0,0 +1,49 @@ +# Provenance Notes + +- Command: + `/bin/zsh -lc 'GOCACHE=$PWD/.gocache go run ./benchmark/cmd/locomobench --cache /tmp/locomo10_cache.json'` + (the `--cache` flag is optional; omitting it downloads the dataset from + `locomo.DatasetURL` — `https://raw.githubusercontent.com/snap-research/locomo/main/data/locomo10.json` + — on first run) +- Verification: + `/bin/zsh -lc 'GOCACHE=$PWD/.gocache go test ./benchmark/locomo -count=1'` +- Environment: + local workspace run on `2026-07-15`, dataset fetched live from the public + `snap-research/locomo` GitHub repo (10 samples, 1,984 total QA pairs) +- Dataset: LoCoMo-10 (`snap-research/locomo`), the standard external + long-term conversational memory benchmark — this is the first run against + an external corpus; the prior `yaad-longmem-core` baseline uses a + first-party, self-seeded 18-question QA set. + +## Caveats — read before comparing these numbers to anything else + +1. **This is a retrieval-hit proxy score, not LoCoMo's official metric.** + LoCoMo's own published evaluation grades free-text answer *generation* + via an LLM judge. `internal/bench.Run` (reused unmodified here) instead + checks whether the expected answer text appears as a case-insensitive + substring of a retrieved node's content within the top-K. These are + related but genuinely different measurements — do not read these numbers + as comparable to a LoCoMo leaderboard entry. +2. **Category 5 (adversarial/unanswerable) questions are excluded.** 444 of + the ~1,984 total QA pairs have no `answer` field (only + `adversarial_answer`, explaining why the question is unanswerable from + the conversation) — a substring-match harness has no way to score + "correctly recalled nothing," so scoring them would just inflate the + miss count without measuring anything real. 1,540 non-adversarial QA + pairs were evaluated. +3. **The much lower scores vs. `yaad-longmem-core` (R@1 3.1% vs. 66.7%) are + expected, not a regression.** The self-seeded baseline's answers are + near-verbatim substrings of their source memories by construction; real + LoCoMo answers are frequently paraphrased, computed (e.g. relative + dates), or require synthesizing multiple dialogue turns — exactly the + kind of question a substring-match proxy underscores relative to a real + generation-based judge. Treat this run as establishing a first honest + external-corpus baseline, not as evidence retrieval quality regressed. +4. **Each of the 10 LoCoMo conversations is benchmarked against its own + freshly seeded engine**, not a shared one — see + `benchmark/locomo/adapter.go`'s `SeedInputs` doc comment for why (cross- + conversation recall would be a correctness bug, not a harder benchmark). +5. The broader `yaad-locomo-beam-publication` suite id already named as + "planned" in `benchmark/README.md` implies eventual BEAM-benchmark + coverage too — this run covers LoCoMo only. `BEAM` ingestion was not + attempted in this pass. diff --git a/benchmark/results/yaad-locomo/2026-07-15/report.md b/benchmark/results/yaad-locomo/2026-07-15/report.md new file mode 100644 index 0000000..dafe55f --- /dev/null +++ b/benchmark/results/yaad-locomo/2026-07-15/report.md @@ -0,0 +1,49 @@ +# Yaad LoCoMo-10 External Baseline + +## Metadata + +- Suite: `yaad-locomo` +- Date: `2026-07-15` +- Dataset: LoCoMo-10 (`snap-research/locomo`), 10 conversations, 1,540 non-adversarial QA pairs evaluated +- Model: none +- Provider: none +- Commit: current workspace snapshot +- Command: `/bin/zsh -lc 'GOCACHE=$PWD/.gocache go run ./benchmark/cmd/locomobench --cache /tmp/locomo10_cache.json'` + +## Headline Metrics (aggregate across all 10 samples) + +- R@1: `3.1%` +- R@3: `8.6%` +- R@5: `13.2%` +- R@10: `19.1%` +- MRR: `0.072` +- Avg tokens/query: `293` +- Duration: `3.60s` (1,540 questions, 10 isolated engines) + +## Per-sample range + +R@1 ranged `0.0%`–`4.6%` and R@10 ranged `8.6%`–`27.0%` across the 10 conversations (see `result.txt` for the full per-sample breakdown) — no single conversation dominates the aggregate. + +## Notes + +- This is the first committed run against a **standard external corpus** + rather than yaad's first-party self-seeded QA set — see `notes.md` for + why the numbers are structurally lower than `yaad-longmem-core`'s and + what they do and don't measure. +- The harness (`internal/bench.Run`) is reused completely unmodified; only + a new data adapter (`benchmark/locomo/`) was added to convert LoCoMo-10's + conversation/QA format into `bench.QA` + seed inputs. +- The report is generated from 10 independent local SQLite-backed memory + graphs (one per LoCoMo conversation) with no external model dependency — + the dataset itself is fetched from `snap-research/locomo` on GitHub, but + no LLM/API calls are made during the benchmark run. + +## Comparison Summary + +- Previous baseline: none committed for this suite (first `yaad-locomo` run) +- Change since baseline: initial published baseline +- Interpretation: establishes yaad's first honest external-corpus retrieval + number. The gap to `yaad-longmem-core`'s numbers is expected given the + harness's substring-match scoring (see `notes.md` caveat 1) and is not + read as a quality regression — it's a materially harder, more realistic + measurement than the self-seeded set. diff --git a/benchmark/results/yaad-locomo/2026-07-15/result.txt b/benchmark/results/yaad-locomo/2026-07-15/result.txt new file mode 100644 index 0000000..3ecd07a --- /dev/null +++ b/benchmark/results/yaad-locomo/2026-07-15/result.txt @@ -0,0 +1,115 @@ +Suite: yaad-locomo +Samples: 10 +Depth: 2 +Limit: 10 +Timestamp: 2026-07-15T05:41:56Z + +--- conv-26 (152 questions) --- +Benchmark Results (152 questions) + R@1: 0.7% + R@3: 3.3% + R@5: 6.6% + R@10: 8.6% + MRR: 0.029 + Avg tokens/query: 313 + Duration: 269.12075ms + +--- conv-30 (81 questions) --- +Benchmark Results (81 questions) + R@1: 0.0% + R@3: 0.0% + R@5: 4.9% + R@10: 11.1% + MRR: 0.021 + Avg tokens/query: 275 + Duration: 154.646875ms + +--- conv-41 (152 questions) --- +Benchmark Results (152 questions) + R@1: 4.6% + R@3: 11.2% + R@5: 15.8% + R@10: 27.0% + MRR: 0.099 + Avg tokens/query: 323 + Duration: 415.637708ms + +--- conv-42 (199 questions) --- +Benchmark Results (199 questions) + R@1: 4.5% + R@3: 10.6% + R@5: 13.1% + R@10: 17.6% + MRR: 0.083 + Avg tokens/query: 279 + Duration: 508.105875ms + +--- conv-43 (178 questions) --- +Benchmark Results (178 questions) + R@1: 2.2% + R@3: 6.7% + R@5: 12.4% + R@10: 20.8% + MRR: 0.067 + Avg tokens/query: 273 + Duration: 431.487834ms + +--- conv-44 (123 questions) --- +Benchmark Results (123 questions) + R@1: 0.8% + R@3: 6.5% + R@5: 13.8% + R@10: 22.8% + MRR: 0.058 + Avg tokens/query: 317 + Duration: 285.4295ms + +--- conv-47 (150 questions) --- +Benchmark Results (150 questions) + R@1: 4.0% + R@3: 10.7% + R@5: 15.3% + R@10: 19.3% + MRR: 0.081 + Avg tokens/query: 251 + Duration: 336.833917ms + +--- conv-48 (191 questions) --- +Benchmark Results (191 questions) + R@1: 4.2% + R@3: 9.4% + R@5: 13.1% + R@10: 18.3% + MRR: 0.079 + Avg tokens/query: 283 + Duration: 451.792ms + +--- conv-49 (156 questions) --- +Benchmark Results (156 questions) + R@1: 4.5% + R@3: 11.5% + R@5: 16.7% + R@10: 20.5% + MRR: 0.090 + Avg tokens/query: 289 + Duration: 367.693416ms + +--- conv-50 (158 questions) --- +Benchmark Results (158 questions) + R@1: 2.5% + R@3: 10.8% + R@5: 16.5% + R@10: 22.2% + MRR: 0.082 + Avg tokens/query: 326 + Duration: 382.26175ms + +=== Aggregate across all samples === +Benchmark Results (1540 questions) + R@1: 3.1% + R@3: 8.6% + R@5: 13.2% + R@10: 19.1% + MRR: 0.072 + Avg tokens/query: 293 + Duration: 3.603009625s diff --git a/benchmark/results/yaad-locomo/README.md b/benchmark/results/yaad-locomo/README.md new file mode 100644 index 0000000..554879b --- /dev/null +++ b/benchmark/results/yaad-locomo/README.md @@ -0,0 +1,13 @@ +# `yaad-locomo` Published Runs + +Current published runs: + +- `2026-07-15/` + +Each published run includes: + +- `report.md` +- `result.txt` or `result.json` +- `notes.md` + +Reference manifest: `../../manifests/yaad-locomo.yaml` From aa8b25b61c850a390d8bfa3e9d1a0d5f319c3fc2 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Wed, 15 Jul 2026 17:36:21 +0530 Subject: [PATCH 3/6] refactor(yaad): migrate MCP server onto hawk-mcpkit yaad re-implemented (transport wiring, HTTP-level auth middleware, the strArg/jsonResult handlers) that hawk-mcpkit already provides. Replace the raw *mcpserver.MCPServer with a *mcpkit.Server and route every tool/prompt/resource registration through it. WithAPIKey now uses kit.WithHTTPToken (HTTP-level bearer/apikey gating), transports delegate to kit.ServeStdio/ServeHTTP/ServeHTTPWithShutdown, and the duplicated auth middleware, buildHTTPServer, body cap, and helper functions are deleted. Handler signatures and behavior are unchanged; strArg/intArgOr stay as small unexported yaad helpers. --- go.mod | 3 + internal/server/mcp_concurrency_rest_test.go | 9 +- internal/server/mcp_core.go | 117 +++++------------- internal/server/mcp_graph.go | 49 ++++---- internal/server/mcp_memory.go | 121 ++++++++++--------- internal/server/mcp_prompts.go | 16 +-- internal/server/mcp_resources.go | 20 +-- 7 files changed, 138 insertions(+), 197 deletions(-) diff --git a/go.mod b/go.mod index 346acad..ab206aa 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.26.5 require ( github.com/BurntSushi/toml v1.6.0 + github.com/GrayCodeAI/hawk-mcpkit v0.0.0 github.com/google/uuid v1.6.0 github.com/mark3labs/mcp-go v0.49.0 github.com/tiktoken-go/tokenizer v0.8.0 @@ -38,3 +39,5 @@ require ( modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) + +replace github.com/GrayCodeAI/hawk-mcpkit => ../hawk-mcpkit diff --git a/internal/server/mcp_concurrency_rest_test.go b/internal/server/mcp_concurrency_rest_test.go index 072cae7..1f07cbe 100644 --- a/internal/server/mcp_concurrency_rest_test.go +++ b/internal/server/mcp_concurrency_rest_test.go @@ -442,11 +442,10 @@ func TestMCPToolRegistration(t *testing.T) { srv, _, cleanup := setupMCPServer(t) defer cleanup() - // The MCP server registers tools via AddTool. We verify by checking that - // calling each known tool handler does not panic and that the server struct - // was constructed successfully. - if srv.server == nil { - t.Fatal("expected non-nil mcp-go server") + // The MCP server registers tools via mcpkit. We verify by checking that + // the wrapper and the engine were both constructed successfully. + if srv.srv == nil { + t.Fatal("expected non-nil mcpkit server") } if srv.eng == nil { t.Fatal("expected non-nil engine") diff --git a/internal/server/mcp_core.go b/internal/server/mcp_core.go index 8702cc7..566c778 100644 --- a/internal/server/mcp_core.go +++ b/internal/server/mcp_core.go @@ -2,46 +2,36 @@ package server import ( "context" - "encoding/json" - "net/http" - "os" - "strings" - "time" + "github.com/GrayCodeAI/hawk-mcpkit" "github.com/GrayCodeAI/yaad/engine" "github.com/mark3labs/mcp-go/mcp" mcpserver "github.com/mark3labs/mcp-go/server" ) -// maxMCPRequestBodySize caps the body of an MCP-over-HTTP request, matching -// the REST server's maxRequestBodySize so both HTTP surfaces have the same -// resource-exhaustion protection. -const maxMCPRequestBodySize = 1 << 20 // 1 MB - -// MCPServer wraps the MCP protocol server for Hawk integration. +// MCPServer wraps the shared mcpkit MCP scaffolding with yaad's tool, +// resource, and prompt registrations plus the engine that backs them. type MCPServer struct { - eng *engine.Engine - server *mcpserver.MCPServer - apiKey string // if set, require Bearer / X-API-Key on HTTP requests + eng *engine.Engine + srv *mcpkit.Server } -// WithAPIKey sets the API key required for authenticated HTTP requests -// (Bearer / X-API-Key). Has no effect on ServeStdio, which is trusted by -// virtue of being a local subprocess pipe. +// WithAPIKey makes HTTP requests require a matching Bearer or X-API-Key +// token before reaching any MCP surface (initialize, resources, prompts, +// tools). Has no effect on ServeStdio, which is a trusted local subprocess +// pipe. func (s *MCPServer) WithAPIKey(key string) *MCPServer { - s.apiKey = key + s.srv.WithHTTPToken(key) return s } // NewMCPServer creates an MCP server with all yaad tools registered. func NewMCPServer(eng *engine.Engine) *MCPServer { s := &MCPServer{eng: eng} - s.server = mcpserver.NewMCPServer( - "yaad", "0.1.0", - mcpserver.WithToolCapabilities(true), - mcpserver.WithResourceCapabilities(true, false), - mcpserver.WithPromptCapabilities(true), - ) + // yaad exposes a resource *list* rather than a set of subscribable + // resources, so it opts into resource-list-changed updates (the + // second arg) — overriding mcpkit's read-only default. + s.srv = mcpkit.New("yaad", "0.1.0", mcpserver.WithResourceCapabilities(true, false)) s.registerTools() s.registerResources() s.registerPrompts() @@ -50,71 +40,31 @@ func NewMCPServer(eng *engine.Engine) *MCPServer { // ServeStdio starts the MCP server on stdin/stdout. func (s *MCPServer) ServeStdio() error { - stdio := mcpserver.NewStdioServer(s.server) - return stdio.Listen(context.Background(), os.Stdin, os.Stdout) + return s.srv.ServeStdio() } // ServeHTTP starts the MCP server on a streamable HTTP endpoint. // The default endpoint path is /mcp. Clients connect to http:///mcp. func (s *MCPServer) ServeHTTP(addr string) error { - httpServer, err := s.buildHTTPServer(addr) - if err != nil { - return err - } - return httpServer.Start(addr) + return s.srv.ServeHTTP(addr) } // ServeHTTPWithShutdown starts the MCP server on a streamable HTTP endpoint -// and returns the server so the caller can invoke Shutdown for graceful teardown. +// and returns the server so the caller can invoke Shutdown for graceful +// teardown. func (s *MCPServer) ServeHTTPWithShutdown(addr string) (*mcpserver.StreamableHTTPServer, error) { - httpServer, err := s.buildHTTPServer(addr) - if err != nil { - return nil, err - } - err = httpServer.Start(addr) - return httpServer, err + return s.srv.ServeHTTPWithShutdown(addr) } -// buildHTTPServer wires the streamable MCP transport behind the same minimum -// HTTP hardening the REST server gets: a body-size cap, an optional API-key -// check, and a slowloris-resistant header read timeout. It deliberately does -// NOT impose an overall request/response deadline (unlike the REST server's -// http.TimeoutHandler) because MCP's GET transport is a long-lived SSE -// stream; a blanket timeout would sever legitimate long-running sessions. -func (s *MCPServer) buildHTTPServer(addr string) (*mcpserver.StreamableHTTPServer, error) { - const endpointPath = "/mcp" - mux := http.NewServeMux() - httpServer := mcpserver.NewStreamableHTTPServer(s.server, mcpserver.WithStreamableHTTPServer(&http.Server{ - Addr: addr, - Handler: mux, - ReadHeaderTimeout: 10 * time.Second, - })) - mux.Handle(endpointPath, s.authMiddleware(httpServer)) - return httpServer, nil -} - -// authMiddleware caps the request body and, if an API key is configured, -// requires it via Bearer or X-API-Key before delegating to next. -func (s *MCPServer) authMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - r.Body = http.MaxBytesReader(w, r.Body, maxMCPRequestBodySize) - if s.apiKey != "" { - token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") - if token == "" { - token = r.Header.Get("X-API-Key") - } - if !constantTimeEqual(token, s.apiKey) { - http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) - return - } - } - next.ServeHTTP(w, r) - }) +// mcp returns the underlying mcp-go server so the progress notification +// helper can reach mcp-go's send path directly. +func (s *MCPServer) mcp() *mcpserver.MCPServer { + return s.srv.MCP() } func (s *MCPServer) registerTools() { add := func(tool mcp.Tool, handler mcpserver.ToolHandlerFunc) { - s.server.AddTool(tool, handler) + s.srv.AddTool(tool, handler) } // yaad_remember @@ -344,16 +294,11 @@ func sendProgress(ctx context.Context, srv *mcpserver.MCPServer, token mcp.Progr } // --- helpers --- - -func strArg(req mcp.CallToolRequest, key string) string { - if v, ok := req.GetArguments()[key].(string); ok { - return v - } - return "" -} +// strArgOr / intArgOr are yaad-specific extraction helpers not carried by +// mcpkit (mcpkit exports the base StrArg). Kept as small unexported helpers. func strArgOr(req mcp.CallToolRequest, key, def string) string { - if v := strArg(req, key); v != "" { + if v := mcpkit.StrArg(req, key); v != "" { return v } return def @@ -365,11 +310,3 @@ func intArgOr(req mcp.CallToolRequest, key string, def int) int { } return def } - -func jsonResult(v any) (*mcp.CallToolResult, error) { - b, err := json.MarshalIndent(v, "", " ") - if err != nil { - return nil, err - } - return mcp.NewToolResultText(string(b)), nil -} diff --git a/internal/server/mcp_graph.go b/internal/server/mcp_graph.go index 09da67d..3273c87 100644 --- a/internal/server/mcp_graph.go +++ b/internal/server/mcp_graph.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/GrayCodeAI/hawk-mcpkit" "github.com/GrayCodeAI/yaad/engine" "github.com/GrayCodeAI/yaad/graph" "github.com/GrayCodeAI/yaad/storage" @@ -14,7 +15,7 @@ import ( // --- Graph tool handlers --- func (s *MCPServer) handleLink(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - edgeType := strArg(req, "type") + edgeType := mcpkit.StrArg(req, "type") if edgeType == "" { return nil, fmt.Errorf("edge type is required") } @@ -22,8 +23,8 @@ func (s *MCPServer) handleLink(ctx context.Context, req mcp.CallToolRequest) (*m return nil, fmt.Errorf("invalid edge type: %q", edgeType) } edge := &storage.Edge{ - FromID: strArg(req, "from_id"), - ToID: strArg(req, "to_id"), + FromID: mcpkit.StrArg(req, "from_id"), + ToID: mcpkit.StrArg(req, "to_id"), Type: edgeType, Weight: 1.0, } @@ -31,11 +32,11 @@ func (s *MCPServer) handleLink(ctx context.Context, req mcp.CallToolRequest) (*m if err := s.eng.Graph().AddEdge(ctx, edge); err != nil { return nil, err } - return jsonResult(edge) + return mcpkit.JSONResult(edge) } func (s *MCPServer) handleUnlink(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if err := s.eng.Graph().RemoveEdge(ctx, strArg(req, "id")); err != nil { + if err := s.eng.Graph().RemoveEdge(ctx, mcpkit.StrArg(req, "id")); err != nil { return nil, err } return mcp.NewToolResultText("unlinked"), nil @@ -46,11 +47,11 @@ func (s *MCPServer) handleSubgraph(ctx context.Context, req mcp.CallToolRequest) if depth <= 0 || depth > 5 { depth = 2 } - sg, err := s.eng.Graph().ExtractSubgraph(ctx, strArg(req, "id"), depth) + sg, err := s.eng.Graph().ExtractSubgraph(ctx, mcpkit.StrArg(req, "id"), depth) if err != nil { return nil, err } - return jsonResult(sg) + return mcpkit.JSONResult(sg) } func (s *MCPServer) handleImpact(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -60,14 +61,14 @@ func (s *MCPServer) handleImpact(ctx context.Context, req mcp.CallToolRequest) ( depth = 3 } - sendProgress(ctx, s.server, token, 1, 3, "tracing impact graph") + sendProgress(ctx, s.mcp(), token, 1, 3, "tracing impact graph") - ids, err := s.eng.Graph().Impact(ctx, strArg(req, "file"), depth) + ids, err := s.eng.Graph().Impact(ctx, mcpkit.StrArg(req, "file"), depth) if err != nil { return nil, err } - sendProgress(ctx, s.server, token, 2, 3, "resolving affected nodes") + sendProgress(ctx, s.mcp(), token, 2, 3, "resolving affected nodes") // Resolve IDs to nodes var nodes []*storage.Node @@ -78,14 +79,14 @@ func (s *MCPServer) handleImpact(ctx context.Context, req mcp.CallToolRequest) ( } } - sendProgress(ctx, s.server, token, 3, 3, "impact analysis complete") - return jsonResult(nodes) + sendProgress(ctx, s.mcp(), token, 3, 3, "impact analysis complete") + return mcpkit.JSONResult(nodes) } func (s *MCPServer) handleCommunities(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { token := req.Params.Meta.ProgressToken - sendProgress(ctx, s.server, token, 1, 3, "detecting memory communities") + sendProgress(ctx, s.mcp(), token, 1, 3, "detecting memory communities") cd := engine.NewCommunityDetector(s.eng.Store()) communities, err := cd.Detect(ctx, 10) @@ -93,11 +94,11 @@ func (s *MCPServer) handleCommunities(ctx context.Context, req mcp.CallToolReque return nil, fmt.Errorf("community detection failed: %w", err) } - sendProgress(ctx, s.server, token, 2, 3, "summarizing clusters") + sendProgress(ctx, s.mcp(), token, 2, 3, "summarizing clusters") communities = cd.Summarize(ctx, communities) - sendProgress(ctx, s.server, token, 3, 3, "communities detected") + sendProgress(ctx, s.mcp(), token, 3, 3, "communities detected") type communityOut struct { ID int `json:"id"` @@ -109,22 +110,22 @@ func (s *MCPServer) handleCommunities(ctx context.Context, req mcp.CallToolReque for i, c := range communities { out[i] = communityOut{ID: c.ID, Size: c.Size, Summary: c.Summary, Types: c.Types} } - return jsonResult(out) + return mcpkit.JSONResult(out) } func (s *MCPServer) handleHierarchy(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { token := req.Params.Meta.ProgressToken - sendProgress(ctx, s.server, token, 1, 2, "building hierarchical memory") + sendProgress(ctx, s.mcp(), token, 1, 2, "building hierarchical memory") hm := engine.NewHierarchicalMemory(s.eng.Store()) if err := hm.Build(ctx); err != nil { return nil, fmt.Errorf("hierarchy build failed: %w", err) } - sendProgress(ctx, s.server, token, 2, 2, "hierarchy ready") + sendProgress(ctx, s.mcp(), token, 2, 2, "hierarchy ready") - query := strArg(req, "query") + query := mcpkit.StrArg(req, "query") level := intArgOr(req, "level", -1) if level < 0 && query != "" { @@ -136,7 +137,7 @@ func (s *MCPServer) handleHierarchy(ctx context.Context, req mcp.CallToolRequest if query != "" { clusters := hm.RetrieveAtLevel(query, level) - return jsonResult(map[string]interface{}{ + return mcpkit.JSONResult(map[string]interface{}{ "level": level, "query": query, "clusters": clusters, @@ -149,19 +150,19 @@ func (s *MCPServer) handleHierarchy(ctx context.Context, req mcp.CallToolRequest func (s *MCPServer) handleSparsify(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { token := req.Params.Meta.ProgressToken - sendProgress(ctx, s.server, token, 1, 3, "analyzing memory graph") + sendProgress(ctx, s.mcp(), token, 1, 3, "analyzing memory graph") sp := engine.NewSparsifier(s.eng.Store()) - sendProgress(ctx, s.server, token, 2, 3, "merging duplicates and pruning orphans") + sendProgress(ctx, s.mcp(), token, 2, 3, "merging duplicates and pruning orphans") result, err := sp.Run(ctx) if err != nil { return nil, fmt.Errorf("sparsification failed: %w", err) } - sendProgress(ctx, s.server, token, 3, 3, "sparsification complete") - return jsonResult(map[string]interface{}{ + sendProgress(ctx, s.mcp(), token, 3, 3, "sparsification complete") + return mcpkit.JSONResult(map[string]interface{}{ "merged": result.Merged, "compressed": result.Compressed, "pruned": result.Pruned, diff --git a/internal/server/mcp_memory.go b/internal/server/mcp_memory.go index d0e835d..d4f276d 100644 --- a/internal/server/mcp_memory.go +++ b/internal/server/mcp_memory.go @@ -7,6 +7,7 @@ import ( "os" "time" + "github.com/GrayCodeAI/hawk-mcpkit" "github.com/GrayCodeAI/yaad/engine" gitwatch "github.com/GrayCodeAI/yaad/git" intentpkg "github.com/GrayCodeAI/yaad/intent" @@ -24,18 +25,18 @@ func (s *MCPServer) handleRemember(ctx context.Context, req mcp.CallToolRequest) return nil, fmt.Errorf("invalid node type: %q", typ) } in := engine.RememberInput{ - Content: strArg(req, "content"), + Content: mcpkit.StrArg(req, "content"), Type: typ, - Summary: strArg(req, "summary"), - Tags: strArg(req, "tags"), - Project: strArg(req, "project"), + Summary: mcpkit.StrArg(req, "summary"), + Tags: mcpkit.StrArg(req, "tags"), + Project: mcpkit.StrArg(req, "project"), Scope: strArgOr(req, "scope", "project"), } node, err := s.eng.Remember(ctx, in) if err != nil { return nil, err } - return jsonResult(node) + return mcpkit.JSONResult(node) } func (s *MCPServer) handleRecall(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -49,48 +50,48 @@ func (s *MCPServer) handleRecall(ctx context.Context, req mcp.CallToolRequest) ( limit = 10 } - sendProgress(ctx, s.server, token, 1, 3, "running BM25 seed search") + sendProgress(ctx, s.mcp(), token, 1, 3, "running BM25 seed search") result, err := s.eng.Recall(ctx, engine.RecallOpts{ - Query: strArg(req, "query"), + Query: mcpkit.StrArg(req, "query"), Depth: depth, Limit: limit, - Type: strArg(req, "type"), - Project: strArg(req, "project"), + Type: mcpkit.StrArg(req, "type"), + Project: mcpkit.StrArg(req, "project"), }) if err != nil { return nil, err } - sendProgress(ctx, s.server, token, 3, 3, "recall complete") - return jsonResult(result) + sendProgress(ctx, s.mcp(), token, 3, 3, "recall complete") + return mcpkit.JSONResult(result) } func (s *MCPServer) handleContext(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { token := req.Params.Meta.ProgressToken - sendProgress(ctx, s.server, token, 1, 2, "assembling hot-tier subgraph") + sendProgress(ctx, s.mcp(), token, 1, 2, "assembling hot-tier subgraph") - result, err := s.eng.Context(ctx, strArg(req, "project")) + result, err := s.eng.Context(ctx, mcpkit.StrArg(req, "project")) if err != nil { return nil, err } - sendProgress(ctx, s.server, token, 2, 2, "context ready") - return jsonResult(result) + sendProgress(ctx, s.mcp(), token, 2, 2, "context ready") + return mcpkit.JSONResult(result) } func (s *MCPServer) handleForget(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if err := s.eng.Forget(ctx, strArg(req, "id")); err != nil { + if err := s.eng.Forget(ctx, mcpkit.StrArg(req, "id")); err != nil { return nil, err } return mcp.NewToolResultText("forgotten"), nil } func (s *MCPServer) handleFeedback(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - id := strArg(req, "id") - action := strArg(req, "action") - content := strArg(req, "content") + id := mcpkit.StrArg(req, "id") + action := mcpkit.StrArg(req, "action") + content := mcpkit.StrArg(req, "content") if err := s.eng.Feedback(ctx, id, engine.FeedbackAction(action), content); err != nil { return nil, err } @@ -98,7 +99,7 @@ func (s *MCPServer) handleFeedback(ctx context.Context, req mcp.CallToolRequest) } func (s *MCPServer) handlePin(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - node, err := s.eng.Store().GetNode(ctx, strArg(req, "id")) + node, err := s.eng.Store().GetNode(ctx, mcpkit.StrArg(req, "id")) if err != nil { return nil, err } @@ -120,58 +121,58 @@ func (s *MCPServer) handlePin(ctx context.Context, req mcp.CallToolRequest) (*mc func (s *MCPServer) handleCompact(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { token := req.Params.Meta.ProgressToken - sendProgress(ctx, s.server, token, 1, 3, "identifying low-confidence memories") + sendProgress(ctx, s.mcp(), token, 1, 3, "identifying low-confidence memories") - n, err := s.eng.Compact(ctx, strArg(req, "project")) + n, err := s.eng.Compact(ctx, mcpkit.StrArg(req, "project")) if err != nil { return nil, err } - sendProgress(ctx, s.server, token, 3, 3, "compaction complete") + sendProgress(ctx, s.mcp(), token, 3, 3, "compaction complete") return mcp.NewToolResultText(fmt.Sprintf("compacted %d nodes", n)), nil } func (s *MCPServer) handleMentalModel(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { token := req.Params.Meta.ProgressToken - sendProgress(ctx, s.server, token, 1, 2, "building mental model") + sendProgress(ctx, s.mcp(), token, 1, 2, "building mental model") - model, err := s.eng.MentalModel(ctx, strArg(req, "project")) + model, err := s.eng.MentalModel(ctx, mcpkit.StrArg(req, "project")) if err != nil { return nil, err } - sendProgress(ctx, s.server, token, 2, 2, "mental model ready") + sendProgress(ctx, s.mcp(), token, 2, 2, "mental model ready") return mcp.NewToolResultText(model.Format()), nil } func (s *MCPServer) handleProactive(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { token := req.Params.Meta.ProgressToken - sendProgress(ctx, s.server, token, 1, 3, "building hybrid search index") + sendProgress(ctx, s.mcp(), token, 1, 3, "building hybrid search index") hs := engine.NewHybridSearch(s.eng.Store(), s.eng.Graph(), nil) pc := engine.NewProactiveContext(s.eng, hs) budget := intArgOr(req, "budget", 2000) - sendProgress(ctx, s.server, token, 2, 3, "predicting context") + sendProgress(ctx, s.mcp(), token, 2, 3, "predicting context") - nodes, err := pc.Predict(ctx, strArg(req, "project"), budget) + nodes, err := pc.Predict(ctx, mcpkit.StrArg(req, "project"), budget) if err != nil { return nil, err } - sendProgress(ctx, s.server, token, 3, 3, "proactive context ready") + sendProgress(ctx, s.mcp(), token, 3, 3, "proactive context ready") return mcp.NewToolResultText(engine.FormatContext(nodes)), nil } func (s *MCPServer) handleHybridRecall(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { token := req.Params.Meta.ProgressToken - sendProgress(ctx, s.server, token, 1, 4, "running hybrid search (BM25 + vector + graph)") + sendProgress(ctx, s.mcp(), token, 1, 4, "running hybrid search (BM25 + vector + graph)") hs := engine.NewHybridSearch(s.eng.Store(), s.eng.Graph(), nil) - scored, err := hs.Search(ctx, strArg(req, "query"), engine.RecallOpts{ + scored, err := hs.Search(ctx, mcpkit.StrArg(req, "query"), engine.RecallOpts{ Depth: intArgOr(req, "depth", 2), Limit: intArgOr(req, "limit", 10), }) @@ -179,23 +180,23 @@ func (s *MCPServer) handleHybridRecall(ctx context.Context, req mcp.CallToolRequ return nil, err } - sendProgress(ctx, s.server, token, 3, 4, "reranking results with RRF fusion") + sendProgress(ctx, s.mcp(), token, 3, 4, "reranking results with RRF fusion") reranked := engine.Rerank(ctx, scored, s.eng.Store()) - sendProgress(ctx, s.server, token, 4, 4, "hybrid recall complete") - return jsonResult(reranked) + sendProgress(ctx, s.mcp(), token, 4, 4, "hybrid recall complete") + return mcpkit.JSONResult(reranked) } func (s *MCPServer) handleTaskUpdate(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - node, err := s.eng.Store().GetNode(ctx, strArg(req, "id")) + node, err := s.eng.Store().GetNode(ctx, mcpkit.StrArg(req, "id")) if err != nil { return nil, err } if err := s.eng.Store().SaveVersion(ctx, node.ID, node.Content, "agent", "task update"); err != nil { return nil, fmt.Errorf("save version: %w", err) } - node.Content = strArg(req, "content") + node.Content = mcpkit.StrArg(req, "content") node.Version++ if err := s.eng.Store().UpdateNode(ctx, node); err != nil { return nil, err @@ -205,25 +206,25 @@ func (s *MCPServer) handleTaskUpdate(ctx context.Context, req mcp.CallToolReques sig := mi.Sign(node) _ = s.eng.Store().SaveSignature(ctx, node.ID, sig) } - return jsonResult(node) + return mcpkit.JSONResult(node) } func (s *MCPServer) handleSessions(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - sessions, err := s.eng.Store().ListSessions(ctx, strArg(req, "project"), intArgOr(req, "limit", 10)) + sessions, err := s.eng.Store().ListSessions(ctx, mcpkit.StrArg(req, "project"), intArgOr(req, "limit", 10)) if err != nil { return nil, err } - return jsonResult(sessions) + return mcpkit.JSONResult(sessions) } func (s *MCPServer) handleStale(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { token := req.Params.Meta.ProgressToken - project := strArg(req, "project") + project := mcpkit.StrArg(req, "project") if project == "" { project, _ = os.Getwd() } - sendProgress(ctx, s.server, token, 1, 3, "initializing git watcher") + sendProgress(ctx, s.mcp(), token, 1, 3, "initializing git watcher") // gitwatch.New is lightweight (path validation + struct creation only). // No caching since the project directory varies per request. @@ -232,7 +233,7 @@ func (s *MCPServer) handleStale(ctx context.Context, req mcp.CallToolRequest) (* return nil, err } - sendProgress(ctx, s.server, token, 2, 3, "scanning for stale memories") + sendProgress(ctx, s.mcp(), token, 2, 3, "scanning for stale memories") since := time.Now().Add(-7 * 24 * time.Hour) reports, err := watcher.StalesSince(ctx, since) @@ -240,18 +241,18 @@ func (s *MCPServer) handleStale(ctx context.Context, req mcp.CallToolRequest) (* return nil, err } - sendProgress(ctx, s.server, token, 3, 3, "staleness check complete") + sendProgress(ctx, s.mcp(), token, 3, 3, "staleness check complete") if len(reports) == 0 { return mcp.NewToolResultText("No stale memories detected in the last 7 days."), nil } - return jsonResult(reports) + return mcpkit.JSONResult(reports) } func (s *MCPServer) handleIntent(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - query := strArg(req, "query") + query := mcpkit.StrArg(req, "query") i := intentpkg.Classify(query) weights := intentpkg.Weights(i) - return jsonResult(map[string]any{ + return mcpkit.JSONResult(map[string]any{ "query": query, "intent": i.String(), "weights": weights, @@ -259,20 +260,20 @@ func (s *MCPServer) handleIntent(ctx context.Context, req mcp.CallToolRequest) ( } func (s *MCPServer) handleProfile(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - p, err := s.eng.Profile(ctx, strArg(req, "project")) + p, err := s.eng.Profile(ctx, mcpkit.StrArg(req, "project")) if err != nil { return nil, err } - return jsonResult(map[string]any{ + return mcpkit.JSONResult(map[string]any{ "profile": p, "formatted": p.Format(), }) } func (s *MCPServer) handleSkillStore(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - name := strArg(req, "name") - desc := strArg(req, "description") - stepsJSON := strArg(req, "steps") + name := mcpkit.StrArg(req, "name") + desc := mcpkit.StrArg(req, "description") + stepsJSON := mcpkit.StrArg(req, "steps") var stepDescs []string if err := json.Unmarshal([]byte(stepsJSON), &stepDescs); err != nil { return nil, fmt.Errorf("steps must be a JSON array of strings: %w", err) @@ -292,7 +293,7 @@ func (s *MCPServer) handleSkillStore(ctx context.Context, req mcp.CallToolReques func (s *MCPServer) handleSkillGet(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { project, _ := os.Getwd() - sk, err := skill.Load(ctx, s.eng.Store(), strArg(req, "name"), project) + sk, err := skill.Load(ctx, s.eng.Store(), mcpkit.StrArg(req, "name"), project) if err != nil { return nil, err } @@ -300,7 +301,7 @@ func (s *MCPServer) handleSkillGet(ctx context.Context, req mcp.CallToolRequest) } func (s *MCPServer) handleSessionRecap(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - project := strArg(req, "project") + project := mcpkit.StrArg(req, "project") sessions, err := s.eng.Store().ListSessions(ctx, project, 1) if err != nil { return nil, err @@ -323,11 +324,11 @@ func (s *MCPServer) handleSessionRecap(ctx context.Context, req mcp.CallToolRequ } func (s *MCPServer) handleStatus(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - st, err := s.eng.Status(ctx, strArg(req, "project")) + st, err := s.eng.Status(ctx, mcpkit.StrArg(req, "project")) if err != nil { return nil, err } - return jsonResult(st) + return mcpkit.JSONResult(st) } func (s *MCPServer) handleVerify(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -338,7 +339,7 @@ func (s *MCPServer) handleVerify(ctx context.Context, req mcp.CallToolRequest) ( return nil, fmt.Errorf("integrity checker not configured; initialize with NewMemoryIntegrity and wire via Engine.WithIntegrity") } - sendProgress(ctx, s.server, token, 1, 3, "loading memory nodes") + sendProgress(ctx, s.mcp(), token, 1, 3, "loading memory nodes") nodes, err := s.eng.Store().ListNodes(ctx, storage.NodeFilter{Limit: 1000}) if err != nil { @@ -351,12 +352,12 @@ func (s *MCPServer) handleVerify(ctx context.Context, req mcp.CallToolRequest) ( return nil, fmt.Errorf("load signatures: %w", err) } - sendProgress(ctx, s.server, token, 2, 3, "verifying integrity signatures") + sendProgress(ctx, s.mcp(), token, 2, 3, "verifying integrity signatures") // Compare stored signatures against recomputed ones tampered := mi.VerifyBatch(nodes, storedSigs) - sendProgress(ctx, s.server, token, 3, 3, "verification complete") + sendProgress(ctx, s.mcp(), token, 3, 3, "verification complete") // Count nodes that have signatures stored signed := 0 @@ -390,5 +391,5 @@ func (s *MCPServer) handleVerify(ctx context.Context, req mcp.CallToolRequest) ( result["message"] = fmt.Sprintf("All %d signed memories verified. No tampering detected.", signed) } - return jsonResult(result) + return mcpkit.JSONResult(result) } diff --git a/internal/server/mcp_prompts.go b/internal/server/mcp_prompts.go index a66a60c..13b8455 100644 --- a/internal/server/mcp_prompts.go +++ b/internal/server/mcp_prompts.go @@ -14,7 +14,7 @@ import ( // --- prompts --- func (s *MCPServer) registerPrompts() { - s.server.AddPrompt(mcp.NewPrompt( + s.srv.AddPrompt(mcp.NewPrompt( "recall_context", mcp.WithPromptDescription("Search memory and format as injectable context"), mcp.WithArgument("query", mcp.ArgumentDescription("Search query"), mcp.RequiredArgument()), @@ -22,7 +22,7 @@ func (s *MCPServer) registerPrompts() { mcp.WithArgument("depth", mcp.ArgumentDescription("Graph expansion depth (default 2)")), ), s.handlePromptRecallContext) - s.server.AddPrompt(mcp.NewPrompt( + s.srv.AddPrompt(mcp.NewPrompt( "session_handoff", mcp.WithPromptDescription("Generate handoff summary for session continuation"), mcp.WithArgument("project", mcp.ArgumentDescription("Project path")), @@ -30,14 +30,14 @@ func (s *MCPServer) registerPrompts() { // Task #153: Additional MCP prompts - s.server.AddPrompt(mcp.NewPrompt( + s.srv.AddPrompt(mcp.NewPrompt( "summarize_context", mcp.WithPromptDescription("Generate a concise summary of the current memory context for a project"), mcp.WithArgument("project", mcp.ArgumentDescription("Project path")), mcp.WithArgument("max_tokens", mcp.ArgumentDescription("Maximum tokens for summary (default 500)")), ), s.handlePromptSummarizeContext) - s.server.AddPrompt(mcp.NewPrompt( + s.srv.AddPrompt(mcp.NewPrompt( "memory_search", mcp.WithPromptDescription("Search memories and format results with relevance ranking"), mcp.WithArgument("query", mcp.ArgumentDescription("Search query"), mcp.RequiredArgument()), @@ -45,27 +45,27 @@ func (s *MCPServer) registerPrompts() { mcp.WithArgument("project", mcp.ArgumentDescription("Project path filter")), ), s.handlePromptMemorySearch) - s.server.AddPrompt(mcp.NewPrompt( + s.srv.AddPrompt(mcp.NewPrompt( "knowledge_graph", mcp.WithPromptDescription("Get an overview of the memory knowledge graph structure and key nodes"), mcp.WithArgument("project", mcp.ArgumentDescription("Project path")), mcp.WithArgument("focus", mcp.ArgumentDescription("Focus area: all|conventions|decisions|tasks|bugs")), ), s.handlePromptKnowledgeGraph) - s.server.AddPrompt(mcp.NewPrompt( + s.srv.AddPrompt(mcp.NewPrompt( "skill_replay", mcp.WithPromptDescription("Retrieve and format a stored procedural skill for execution"), mcp.WithArgument("name", mcp.ArgumentDescription("Skill name"), mcp.RequiredArgument()), mcp.WithArgument("project", mcp.ArgumentDescription("Project path")), ), s.handlePromptSkillReplay) - s.server.AddPrompt(mcp.NewPrompt( + s.srv.AddPrompt(mcp.NewPrompt( "session_recap", mcp.WithPromptDescription("Show what was captured in the last session to pick up where you left off"), mcp.WithArgument("project", mcp.ArgumentDescription("Project path")), ), s.handlePromptSessionRecap) - s.server.AddPrompt(mcp.NewPrompt( + s.srv.AddPrompt(mcp.NewPrompt( "impact_analysis", mcp.WithPromptDescription("Analyze what memories would be affected by a file change"), mcp.WithArgument("file", mcp.ArgumentDescription("File path to analyze"), mcp.RequiredArgument()), diff --git a/internal/server/mcp_resources.go b/internal/server/mcp_resources.go index 558d04b..b956889 100644 --- a/internal/server/mcp_resources.go +++ b/internal/server/mcp_resources.go @@ -17,21 +17,21 @@ import ( // --- resources --- func (s *MCPServer) registerResources() { - s.server.AddResource(mcp.Resource{ + s.srv.AddResource(mcp.Resource{ URI: "yaad://context", Name: "context", Description: "Hot-tier context nodes for session injection", MIMEType: "application/json", }, s.handleResourceContext) - s.server.AddResource(mcp.Resource{ + s.srv.AddResource(mcp.Resource{ URI: "yaad://graph/stats", Name: "graph_stats", Description: "Node/edge counts and health metrics", MIMEType: "application/json", }, s.handleResourceStats) - s.server.AddResource(mcp.Resource{ + s.srv.AddResource(mcp.Resource{ URI: "yaad://stale", Name: "stale", Description: "Potentially stale memory nodes based on git changes", @@ -40,49 +40,49 @@ func (s *MCPServer) registerResources() { // Task #152: Additional MCP resources - s.server.AddResource(mcp.Resource{ + s.srv.AddResource(mcp.Resource{ URI: "yaad://memory/{id}", Name: "memory", Description: "Retrieve a specific memory node by ID", MIMEType: "application/json", }, s.handleResourceMemory) - s.server.AddResource(mcp.Resource{ + s.srv.AddResource(mcp.Resource{ URI: "yaad://skills", Name: "skills", Description: "List all stored procedural skills", MIMEType: "application/json", }, s.handleResourceSkills) - s.server.AddResource(mcp.Resource{ + s.srv.AddResource(mcp.Resource{ URI: "yaad://profile", Name: "profile", Description: "User/project profile with static facts and dynamic context", MIMEType: "application/json", }, s.handleResourceProfile) - s.server.AddResource(mcp.Resource{ + s.srv.AddResource(mcp.Resource{ URI: "yaad://mental-model", Name: "mental_model", Description: "Auto-evolving project summary: conventions, decisions, architecture", MIMEType: "application/json", }, s.handleResourceMentalModel) - s.server.AddResource(mcp.Resource{ + s.srv.AddResource(mcp.Resource{ URI: "yaad://sessions", Name: "sessions", Description: "Recent session history and summaries", MIMEType: "application/json", }, s.handleResourceSessions) - s.server.AddResource(mcp.Resource{ + s.srv.AddResource(mcp.Resource{ URI: "yaad://export", Name: "export", Description: "Export memory graph as interactive HTML visualization", MIMEType: "text/html", }, s.handleResourceExport) - s.server.AddResource(mcp.Resource{ + s.srv.AddResource(mcp.Resource{ URI: "yaad://communities", Name: "communities", Description: "Memory communities and thematic clusters", From 7a796a0aee63ea31df70810e3f592f4f404244df Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Wed, 15 Jul 2026 17:46:35 +0530 Subject: [PATCH 4/6] fix(yaad): correct mcpkit replace path for external submodule layout From hawk/external/yaad the sibling hawk-mcpkit resolves at ../../../hawk-mcpkit (the sibling copy uses ../hawk-mcpkit). --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index ab206aa..989ca83 100644 --- a/go.mod +++ b/go.mod @@ -40,4 +40,4 @@ require ( modernc.org/memory v1.11.0 // indirect ) -replace github.com/GrayCodeAI/hawk-mcpkit => ../hawk-mcpkit +replace github.com/GrayCodeAI/hawk-mcpkit => ../../../hawk-mcpkit From c6c5b041589bfad86dc704193c5166e7f2e7e704 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Thu, 16 Jul 2026 00:07:57 +0530 Subject: [PATCH 5/6] fix(yaad): use sibling mcpkit path for external submodule layout --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 989ca83..ab206aa 100644 --- a/go.mod +++ b/go.mod @@ -40,4 +40,4 @@ require ( modernc.org/memory v1.11.0 // indirect ) -replace github.com/GrayCodeAI/hawk-mcpkit => ../../../hawk-mcpkit +replace github.com/GrayCodeAI/hawk-mcpkit => ../hawk-mcpkit From 7556d22e1acfa69550b45afaad40b33859263454 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Thu, 16 Jul 2026 00:44:42 +0530 Subject: [PATCH 6/6] ci: clone sibling hawk-mcpkit for mcpkit dependency --- .github/workflows/ci.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f0f346..9b8c72a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,8 @@ jobs: cache: true - name: Clone tok (workspace dep) run: git clone --depth=1 https://github.com/GrayCodeAI/tok.git ../tok + - name: Clone hawk-mcpkit (workspace dep) + run: git clone --depth=1 https://github.com/GrayCodeAI/hawk-mcpkit.git ../hawk-mcpkit - name: Boundary guard run: bash ./scripts/check-ecosystem-boundaries.sh - name: gofumpt diff @@ -80,6 +82,8 @@ jobs: cache: true - name: Clone tok (workspace dep) run: git clone --depth=1 https://github.com/GrayCodeAI/tok.git ../tok + - name: Clone hawk-mcpkit (workspace dep) + run: git clone --depth=1 https://github.com/GrayCodeAI/hawk-mcpkit.git ../hawk-mcpkit - name: Boundary guard run: bash ./scripts/check-ecosystem-boundaries.sh - uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v7.0.0 @@ -103,6 +107,8 @@ jobs: cache: true - name: Clone tok (workspace dep) run: git clone --depth=1 https://github.com/GrayCodeAI/tok.git ../tok + - name: Clone hawk-mcpkit (workspace dep) + run: git clone --depth=1 https://github.com/GrayCodeAI/hawk-mcpkit.git ../hawk-mcpkit - name: Boundary guard run: bash ./scripts/check-ecosystem-boundaries.sh - name: Tidy check @@ -146,6 +152,8 @@ jobs: cache: true - name: Clone tok (workspace dep) run: git clone --depth=1 https://github.com/GrayCodeAI/tok.git ../tok + - name: Clone hawk-mcpkit (workspace dep) + run: git clone --depth=1 https://github.com/GrayCodeAI/hawk-mcpkit.git ../hawk-mcpkit - name: govulncheck run: | go install golang.org/x/vuln/cmd/govulncheck@v1.1.4 @@ -170,6 +178,8 @@ jobs: cache: true - name: Clone tok (workspace dep) run: git clone --depth=1 https://github.com/GrayCodeAI/tok.git ../tok + - name: Clone hawk-mcpkit (workspace dep) + run: git clone --depth=1 https://github.com/GrayCodeAI/hawk-mcpkit.git ../hawk-mcpkit - name: deadcode run: | go install golang.org/x/tools/cmd/deadcode@v0.30.0 @@ -205,6 +215,8 @@ jobs: cache: true - name: Clone tok (workspace dep) run: git clone --depth=1 https://github.com/GrayCodeAI/tok.git ../tok + - name: Clone hawk-mcpkit (workspace dep) + run: git clone --depth=1 https://github.com/GrayCodeAI/hawk-mcpkit.git ../hawk-mcpkit - name: Run fuzz targets run: | go test -fuzz=FuzzContentHash -fuzztime=60s ./engine @@ -236,6 +248,8 @@ jobs: cache: true - name: Clone tok (workspace dep) run: git clone --depth=1 https://github.com/GrayCodeAI/tok.git ../tok + - name: Clone hawk-mcpkit (workspace dep) + run: git clone --depth=1 https://github.com/GrayCodeAI/hawk-mcpkit.git ../hawk-mcpkit - name: Build env: GOOS: ${{ matrix.goos }}