This commit adds the read path (Cortex) to complement the write path (Spine): ## Crates - stemedb-api: HTTP API with axum + utoipa OpenAPI - /v1/assert, /v1/query, /v1/epoch, /v1/skeptic, /v1/trace, /v1/audit - Metered endpoints with quota enforcement - Ed25519 signature verification - stemedb-lens: Truth resolution lenses - RecencyLens, ConsensusLens, ConfidenceLens - VoteAwareConsensusLens (Ballot Box pattern) - TrustAwareAuthorityLens (The Hive pattern) - SkepticLens (conflict analysis) - EpochAwareLens (paradigm-safe queries) - stemedb-query: Query engine with materialized views ## Storage Extensions - VoteStore: Vote aggregation with cached counts - TrustRankStore: Agent reputation with decay - AuditStore: Query audit trail - IndexStore: SP/P/S index structures - SupersessionStore: Epoch supersession chains ## SDKs - sdk/go/steme: Go HTTP client with Ed25519 signing - sdk/go/adk: ADK-Go tools for AI agents ## Documentation - Updated CLAUDE.md, architecture.md, roadmap.md - New ai-lookup entries for all services - Use case docs for consumer health intelligence - Arena roadmap for simulation advancement Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
632 lines
17 KiB
Go
632 lines
17 KiB
Go
// Package adk provides ADK-Go tool wrappers for StemeDB.
|
|
//
|
|
// This package wraps the base StemeDB SDK with tool definitions suitable
|
|
// for Google's Agent Development Kit (ADK-Go). It provides:
|
|
//
|
|
// - QueryTool - Query with lens-based resolution
|
|
// - AssertTool - Create assertions with confidence
|
|
// - ConstraintCheckTool - Pre-flight validation
|
|
// - TraceTool - Audit trail queries
|
|
// - SupersedeTool - Epoch/correction management
|
|
//
|
|
// Since ADK-Go is not yet publicly released, these tools use interface-based
|
|
// design and can be adapted to any ADK implementation.
|
|
//
|
|
// Example usage:
|
|
//
|
|
// client := steme.NewClient("http://localhost:3000", signer)
|
|
// tools := adk.AllTools(client)
|
|
//
|
|
// // Use tools with your ADK agent configuration
|
|
// agent := configureAgent(tools)
|
|
package adk
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/orchard9/stemedb-go/steme"
|
|
)
|
|
|
|
// Tool represents a generic ADK tool interface.
|
|
//
|
|
// This interface is designed to be compatible with any ADK implementation.
|
|
type Tool interface {
|
|
Name() string
|
|
Description() string
|
|
Execute(ctx context.Context, input []byte) ([]byte, error)
|
|
}
|
|
|
|
// EpistemeClient defines the interface for StemeDB operations.
|
|
//
|
|
// This interface allows for easy mocking and testing.
|
|
type EpistemeClient interface {
|
|
Query(ctx context.Context, params steme.QueryParams) (*steme.QueryResult, error)
|
|
Assert(ctx context.Context, assertion steme.Assertion) (string, error)
|
|
Trace(ctx context.Context, params steme.TraceParams) (*steme.TraceResult, error)
|
|
Supersede(ctx context.Context, params steme.SupersedeParams) (*steme.SupersedeResult, error)
|
|
}
|
|
|
|
// QueryTool provides lens-based knowledge retrieval.
|
|
//
|
|
// Used by all agents to retrieve current knowledge with conflict resolution.
|
|
type QueryTool struct {
|
|
client EpistemeClient
|
|
}
|
|
|
|
// NewQueryTool creates a new Query tool.
|
|
func NewQueryTool(client EpistemeClient) *QueryTool {
|
|
return &QueryTool{client: client}
|
|
}
|
|
|
|
// Name returns the tool name.
|
|
func (t *QueryTool) Name() string {
|
|
return "episteme_query"
|
|
}
|
|
|
|
// Description returns the tool description.
|
|
func (t *QueryTool) Description() string {
|
|
return "Query Episteme knowledge graph with lens-based conflict resolution. " +
|
|
"Returns resolved value with confidence score and source provenance. " +
|
|
"CRITICAL: Always filter by lifecycle=approved for production decisions."
|
|
}
|
|
|
|
// Execute performs the query operation.
|
|
func (t *QueryTool) Execute(ctx context.Context, input []byte) ([]byte, error) {
|
|
var params QueryInput
|
|
if err := json.Unmarshal(input, ¶ms); err != nil {
|
|
return nil, fmt.Errorf("invalid query input: %w", err)
|
|
}
|
|
|
|
// Build StemeDB query params
|
|
builder := steme.NewQuery()
|
|
|
|
if params.Subject != "" {
|
|
builder.WithSubject(params.Subject)
|
|
}
|
|
|
|
if params.Predicate != "" {
|
|
builder.WithPredicate(params.Predicate)
|
|
}
|
|
|
|
if params.Lens != "" {
|
|
lens := parseLen(params.Lens)
|
|
builder.WithLens(lens)
|
|
}
|
|
|
|
if params.Lifecycle != "" {
|
|
lifecycle := parseLifecycle(params.Lifecycle)
|
|
builder.WithLifecycle(lifecycle)
|
|
}
|
|
|
|
// Execute query
|
|
result, err := t.client.Query(ctx, builder.Build())
|
|
if err != nil {
|
|
errorOutput, marshalErr := json.Marshal(QueryOutput{
|
|
Error: err.Error(),
|
|
})
|
|
if marshalErr != nil {
|
|
return nil, fmt.Errorf("query failed: %w", err)
|
|
}
|
|
return errorOutput, nil
|
|
}
|
|
|
|
// Convert to QueryOutput format
|
|
output := convertQueryResult(result, params.MinConfidence)
|
|
|
|
outputBytes, err := json.Marshal(output)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal query output: %w", err)
|
|
}
|
|
return outputBytes, nil
|
|
}
|
|
|
|
// AssertTool stores knowledge with lifecycle and confidence.
|
|
//
|
|
// Used by Research Agent and Human Supervisor to store assertions.
|
|
type AssertTool struct {
|
|
client EpistemeClient
|
|
}
|
|
|
|
// NewAssertTool creates a new Assert tool.
|
|
func NewAssertTool(client EpistemeClient) *AssertTool {
|
|
return &AssertTool{client: client}
|
|
}
|
|
|
|
// Name returns the tool name.
|
|
func (t *AssertTool) Name() string {
|
|
return "episteme_assert"
|
|
}
|
|
|
|
// Description returns the tool description.
|
|
func (t *AssertTool) Description() string {
|
|
return "Store knowledge assertion in Episteme. " +
|
|
"Always include source_hash for provenance. " +
|
|
"Use confidence to express uncertainty. " +
|
|
"Mark as lifecycle=proposed unless explicitly approved."
|
|
}
|
|
|
|
// Execute performs the assert operation.
|
|
func (t *AssertTool) Execute(ctx context.Context, input []byte) ([]byte, error) {
|
|
var params AssertInput
|
|
if err := json.Unmarshal(input, ¶ms); err != nil {
|
|
return nil, fmt.Errorf("invalid assert input: %w", err)
|
|
}
|
|
|
|
// Build assertion
|
|
builder := steme.NewAssertion(params.Subject, params.Predicate)
|
|
|
|
// Set object value based on type
|
|
if err := setObjectValue(builder, params.Object); err != nil {
|
|
errorOutput, marshalErr := json.Marshal(AssertOutput{
|
|
Success: false,
|
|
Error: err.Error(),
|
|
})
|
|
if marshalErr != nil {
|
|
return nil, fmt.Errorf("invalid object value: %w", err)
|
|
}
|
|
return errorOutput, nil
|
|
}
|
|
|
|
builder.WithConfidence(float64(params.Confidence)).
|
|
WithSourceHash(params.SourceHash)
|
|
|
|
if params.Lifecycle != "" {
|
|
lifecycle := parseLifecycle(params.Lifecycle)
|
|
builder.WithLifecycle(lifecycle)
|
|
}
|
|
|
|
if params.ParentHash != "" {
|
|
builder.WithParentHash(params.ParentHash)
|
|
}
|
|
|
|
// Execute assertion
|
|
hash, err := t.client.Assert(ctx, builder.Build())
|
|
if err != nil {
|
|
errorOutput, marshalErr := json.Marshal(AssertOutput{
|
|
Success: false,
|
|
Error: err.Error(),
|
|
})
|
|
if marshalErr != nil {
|
|
return nil, fmt.Errorf("assertion failed: %w", err)
|
|
}
|
|
return errorOutput, nil
|
|
}
|
|
|
|
outputBytes, err := json.Marshal(AssertOutput{
|
|
Hash: hash,
|
|
Success: true,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal assert output: %w", err)
|
|
}
|
|
return outputBytes, nil
|
|
}
|
|
|
|
// ConstraintCheckTool provides pre-flight validation for Implementation Agent.
|
|
//
|
|
// Returns must-use and forbidden patterns for a given context.
|
|
type ConstraintCheckTool struct {
|
|
client EpistemeClient
|
|
}
|
|
|
|
// NewConstraintCheckTool creates a new ConstraintCheck tool.
|
|
func NewConstraintCheckTool(client EpistemeClient) *ConstraintCheckTool {
|
|
return &ConstraintCheckTool{client: client}
|
|
}
|
|
|
|
// Name returns the tool name.
|
|
func (t *ConstraintCheckTool) Name() string {
|
|
return "episteme_constraint_check"
|
|
}
|
|
|
|
// Description returns the tool description.
|
|
func (t *ConstraintCheckTool) Description() string {
|
|
return "Check for must-use and forbidden patterns before code generation. " +
|
|
"Returns constraints with explanations for contrastive learning. " +
|
|
"Implementation Agent MUST call this before writing code."
|
|
}
|
|
|
|
// Execute performs the constraint check.
|
|
func (t *ConstraintCheckTool) Execute(ctx context.Context, input []byte) ([]byte, error) {
|
|
var params ConstraintCheckInput
|
|
if err := json.Unmarshal(input, ¶ms); err != nil {
|
|
return nil, fmt.Errorf("invalid constraint check input: %w", err)
|
|
}
|
|
|
|
// Query for constraints in the given context
|
|
// This queries for assertions with predicate "must_use" or "forbidden"
|
|
builder := steme.NewQuery().
|
|
WithSubject(params.Context).
|
|
WithLifecycle(steme.LifecycleApproved). // Only approved constraints
|
|
WithLens(steme.LensAuthority)
|
|
|
|
result, err := t.client.Query(ctx, builder.Build())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("constraint query failed: %w", err)
|
|
}
|
|
|
|
// Convert assertions to constraints
|
|
constraints := convertToConstraints(result)
|
|
|
|
outputBytes, err := json.Marshal(ConstraintCheckOutput{
|
|
Constraints: constraints,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal constraint output: %w", err)
|
|
}
|
|
return outputBytes, nil
|
|
}
|
|
|
|
// TraceTool provides audit trail queries for incident investigation.
|
|
//
|
|
// Used by On-Call SRE and Human Supervisor to trace agent decisions.
|
|
type TraceTool struct {
|
|
client EpistemeClient
|
|
}
|
|
|
|
// NewTraceTool creates a new Trace tool.
|
|
func NewTraceTool(client EpistemeClient) *TraceTool {
|
|
return &TraceTool{client: client}
|
|
}
|
|
|
|
// Name returns the tool name.
|
|
func (t *TraceTool) Name() string {
|
|
return "episteme_trace"
|
|
}
|
|
|
|
// Description returns the tool description.
|
|
func (t *TraceTool) Description() string {
|
|
return "Trace agent queries for incident investigation. " +
|
|
"Shows what an agent queried, when, and what values were returned. " +
|
|
"CRITICAL for debugging production incidents."
|
|
}
|
|
|
|
// Execute performs the trace operation.
|
|
func (t *TraceTool) Execute(ctx context.Context, input []byte) ([]byte, error) {
|
|
var params TraceInput
|
|
if err := json.Unmarshal(input, ¶ms); err != nil {
|
|
return nil, fmt.Errorf("invalid trace input: %w", err)
|
|
}
|
|
|
|
// Build StemeDB trace params
|
|
traceParams := steme.TraceParams{
|
|
AgentID: params.AgentID,
|
|
From: params.From,
|
|
To: params.To,
|
|
Subject: params.Subject,
|
|
Limit: 100, // Default limit
|
|
}
|
|
|
|
// Execute trace
|
|
result, err := t.client.Trace(ctx, traceParams)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("trace query failed: %w", err)
|
|
}
|
|
|
|
// Convert to TraceOutput format
|
|
output := convertTraceResult(result)
|
|
|
|
outputBytes, err := json.Marshal(output)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal trace output: %w", err)
|
|
}
|
|
return outputBytes, nil
|
|
}
|
|
|
|
// SupersedeTool provides error correction with cascade tracking.
|
|
//
|
|
// Used by Human Supervisor to correct bad assertions.
|
|
type SupersedeTool struct {
|
|
client EpistemeClient
|
|
}
|
|
|
|
// NewSupersedeTool creates a new Supersede tool.
|
|
func NewSupersedeTool(client EpistemeClient) *SupersedeTool {
|
|
return &SupersedeTool{client: client}
|
|
}
|
|
|
|
// Name returns the tool name.
|
|
func (t *SupersedeTool) Name() string {
|
|
return "episteme_supersede"
|
|
}
|
|
|
|
// Description returns the tool description.
|
|
func (t *SupersedeTool) Description() string {
|
|
return "Supersede a bad assertion with correction tracking. " +
|
|
"Shows cascade impact on downstream assertions. " +
|
|
"Used for incident remediation and knowledge correction."
|
|
}
|
|
|
|
// Execute performs the supersede operation.
|
|
func (t *SupersedeTool) Execute(ctx context.Context, input []byte) ([]byte, error) {
|
|
var params SupersedeInput
|
|
if err := json.Unmarshal(input, ¶ms); err != nil {
|
|
return nil, fmt.Errorf("invalid supersede input: %w", err)
|
|
}
|
|
|
|
// Convert SupersessionType
|
|
superType := parseSupersessionType(params.Type)
|
|
|
|
// Build SupersedeParams
|
|
supersedeParams := steme.SupersedeParams{
|
|
TargetHash: params.Hash,
|
|
SupersessionType: superType,
|
|
Reason: params.Reason,
|
|
NewHash: params.NewHash,
|
|
AgentID: params.AgentID,
|
|
Signature: params.Signature,
|
|
}
|
|
|
|
// Execute supersede
|
|
result, err := t.client.Supersede(ctx, supersedeParams)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("supersede failed: %w", err)
|
|
}
|
|
|
|
// Convert to SupersedeOutput format
|
|
output := SupersedeOutput{
|
|
Success: true,
|
|
TargetHash: result.TargetHash,
|
|
SupersessionType: string(result.SupersessionType),
|
|
Timestamp: result.Timestamp,
|
|
}
|
|
|
|
outputBytes, err := json.Marshal(output)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal supersede output: %w", err)
|
|
}
|
|
return outputBytes, nil
|
|
}
|
|
|
|
// AllTools returns all available Episteme tools.
|
|
//
|
|
// Use this for agent configuration:
|
|
//
|
|
// tools := adk.AllTools(client)
|
|
func AllTools(client EpistemeClient) []Tool {
|
|
return []Tool{
|
|
NewQueryTool(client),
|
|
NewAssertTool(client),
|
|
NewConstraintCheckTool(client),
|
|
NewTraceTool(client),
|
|
NewSupersedeTool(client),
|
|
}
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func parseLen(lens string) steme.Lens {
|
|
switch lens {
|
|
case "recency":
|
|
return steme.LensRecency
|
|
case "consensus":
|
|
return steme.LensConsensus
|
|
case "authority":
|
|
return steme.LensAuthority
|
|
case "vote_aware_consensus":
|
|
return steme.LensVoteAwareConsensus
|
|
case "trust_aware_authority":
|
|
return steme.LensTrustAwareAuthority
|
|
default:
|
|
return steme.LensConsensus // Default to consensus
|
|
}
|
|
}
|
|
|
|
func parseLifecycle(lifecycle string) steme.LifecycleStage {
|
|
switch lifecycle {
|
|
case "proposed":
|
|
return steme.LifecycleProposed
|
|
case "under_review":
|
|
return steme.LifecycleUnderReview
|
|
case "approved":
|
|
return steme.LifecycleApproved
|
|
case "deprecated":
|
|
return steme.LifecycleDeprecated
|
|
case "rejected":
|
|
return steme.LifecycleRejected
|
|
default:
|
|
return steme.LifecycleProposed // Default to proposed
|
|
}
|
|
}
|
|
|
|
func parseSupersessionType(t string) steme.SupersessionType {
|
|
switch t {
|
|
case "Invalidate", "invalidate":
|
|
return steme.SupersessionInvalidate
|
|
case "Temporal", "temporal":
|
|
return steme.SupersessionTemporal
|
|
case "Refinement", "refinement":
|
|
return steme.SupersessionRefinement
|
|
case "RequiresReview", "requires_review":
|
|
return steme.SupersessionRequiresReview
|
|
case "Additive", "additive":
|
|
return steme.SupersessionAdditive
|
|
default:
|
|
return steme.SupersessionInvalidate // Default to invalidate
|
|
}
|
|
}
|
|
|
|
func setObjectValue(builder *steme.AssertionBuilder, obj any) error {
|
|
switch v := obj.(type) {
|
|
case string:
|
|
builder.WithText(v)
|
|
case float64:
|
|
builder.WithNumber(v)
|
|
case bool:
|
|
builder.WithBoolean(v)
|
|
case map[string]any:
|
|
// Handle ObjectValue from JSON
|
|
if typeStr, ok := v["type"].(string); ok {
|
|
if value, ok := v["value"]; ok {
|
|
switch typeStr {
|
|
case "Text":
|
|
if str, ok := value.(string); ok {
|
|
builder.WithText(str)
|
|
}
|
|
case "Number":
|
|
if num, ok := value.(float64); ok {
|
|
builder.WithNumber(num)
|
|
}
|
|
case "Boolean":
|
|
if b, ok := value.(bool); ok {
|
|
builder.WithBoolean(b)
|
|
}
|
|
case "Reference":
|
|
if ref, ok := value.(string); ok {
|
|
builder.WithReference(ref)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported object type: %T", obj)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func convertQueryResult(result *steme.QueryResult, minConfidence float32) QueryOutput {
|
|
if len(result.Assertions) == 0 {
|
|
return QueryOutput{
|
|
Error: "no results found",
|
|
}
|
|
}
|
|
|
|
// Take the first result (lens already applied resolution)
|
|
assertion := result.Assertions[0]
|
|
|
|
// Check confidence threshold
|
|
confidence := float32(assertion.Confidence)
|
|
if minConfidence > 0 && confidence < minConfidence {
|
|
return QueryOutput{
|
|
Value: assertion.Object.Value,
|
|
Confidence: confidence,
|
|
Lifecycle: string(assertion.Lifecycle),
|
|
QueryID: generateQueryID(),
|
|
Error: fmt.Sprintf("confidence %.2f below threshold %.2f", confidence, minConfidence),
|
|
}
|
|
}
|
|
|
|
// Convert sources
|
|
sources := make([]Source, 0, len(result.Assertions))
|
|
for _, a := range result.Assertions {
|
|
sources = append(sources, Source{
|
|
Hash: a.Hash,
|
|
SourceHash: a.SourceHash,
|
|
Weight: float32(a.Confidence),
|
|
})
|
|
}
|
|
|
|
return QueryOutput{
|
|
Value: assertion.Object.Value,
|
|
Confidence: confidence,
|
|
Lifecycle: string(assertion.Lifecycle),
|
|
Sources: sources,
|
|
QueryID: generateQueryID(),
|
|
}
|
|
}
|
|
|
|
func convertToConstraints(result *steme.QueryResult) []Constraint {
|
|
constraints := make([]Constraint, 0)
|
|
|
|
for _, assertion := range result.Assertions {
|
|
constraint := Constraint{
|
|
Subject: assertion.Subject,
|
|
}
|
|
|
|
// Determine if this is a must-use or forbidden constraint
|
|
// based on predicate
|
|
switch assertion.Predicate {
|
|
case "must_use":
|
|
if text, ok := assertion.Object.Value.(string); ok {
|
|
constraint.MustUse = text
|
|
}
|
|
case "forbidden":
|
|
if text, ok := assertion.Object.Value.(string); ok {
|
|
constraint.Forbidden = text
|
|
}
|
|
}
|
|
|
|
// Extract reason from parent assertion if available
|
|
// (This is a simplified implementation)
|
|
constraint.Reason = "Approved organizational pattern"
|
|
|
|
constraints = append(constraints, constraint)
|
|
}
|
|
|
|
return constraints
|
|
}
|
|
|
|
func generateQueryID() string {
|
|
// Generate a unique query ID for audit trail
|
|
// In production, this would be generated server-side
|
|
return fmt.Sprintf("query_%d", time.Now().UnixNano())
|
|
}
|
|
|
|
func convertTraceResult(result *steme.TraceResult) TraceOutput {
|
|
queries := make([]QueryTrace, 0, len(result.Audits))
|
|
|
|
for _, audit := range result.Audits {
|
|
// Extract subject and predicate from params
|
|
subject := ""
|
|
if audit.Params.Subject != nil {
|
|
subject = *audit.Params.Subject
|
|
}
|
|
|
|
predicate := ""
|
|
if audit.Params.Predicate != nil {
|
|
predicate = *audit.Params.Predicate
|
|
}
|
|
|
|
// Extract lens (convert to lowercase for consistency)
|
|
lens := "consensus" // Default
|
|
if audit.Params.Lens != nil {
|
|
lensVal := string(*audit.Params.Lens)
|
|
// Convert "Consensus" -> "consensus", etc.
|
|
switch lensVal {
|
|
case "Recency":
|
|
lens = "recency"
|
|
case "Consensus":
|
|
lens = "consensus"
|
|
case "Authority":
|
|
lens = "authority"
|
|
case "VoteAwareConsensus":
|
|
lens = "vote_aware_consensus"
|
|
case "TrustAwareAuthority":
|
|
lens = "trust_aware_authority"
|
|
default:
|
|
lens = "consensus"
|
|
}
|
|
}
|
|
|
|
// Extract result value (simplified - just use hash if available)
|
|
resultValue := ""
|
|
if audit.ResultHash != nil {
|
|
resultValue = *audit.ResultHash
|
|
}
|
|
|
|
// Extract contributing assertion hashes
|
|
contributing := make([]string, 0, len(audit.ContributingAssertions))
|
|
for _, ca := range audit.ContributingAssertions {
|
|
contributing = append(contributing, ca.AssertionHash)
|
|
}
|
|
|
|
queries = append(queries, QueryTrace{
|
|
QueryID: audit.QueryID,
|
|
Timestamp: fmt.Sprintf("%d", audit.Timestamp),
|
|
Subject: subject,
|
|
Predicate: predicate,
|
|
Lens: lens,
|
|
Result: resultValue,
|
|
Confidence: audit.ResultConfidence,
|
|
Contributing: contributing,
|
|
})
|
|
}
|
|
|
|
return TraceOutput{
|
|
Queries: queries,
|
|
}
|
|
}
|