stemedb/sdk/go/adk/callbacks.go
jordan 1ce4004807 feat: Complete Phase 2 (The Cortex) - query, lens, and API layers
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>
2026-02-01 13:22:44 -07:00

238 lines
6.9 KiB
Go

package adk
import (
"context"
"encoding/json"
"fmt"
"slices"
)
// CallbackContext provides context for agent callbacks.
//
// This interface matches the pattern expected by ADK-Go but is generic
// enough to work with any agent framework.
type CallbackContext interface {
Context() context.Context
AgentName() string
SessionID() string
}
// ToolCall represents a tool invocation.
type ToolCall struct {
Name string `json:"name"`
Input json.RawMessage `json:"input"`
}
// ToolResult represents the result of a tool invocation.
type ToolResult struct {
Output json.RawMessage `json:"output"`
Error error `json:"error,omitempty"`
}
// BeforeToolCallback is a function that runs before a tool is called.
//
// This can be used to:
// - Validate tool inputs
// - Enforce constraints
// - Block dangerous operations
//
// Return an error to prevent the tool from executing.
type BeforeToolCallback func(ctx CallbackContext, call *ToolCall) error
// AfterToolCallback is a function that runs after a tool is called.
//
// This can be used to:
// - Log tool results
// - Check confidence thresholds
// - Trigger escalations
//
// Return an error to indicate the tool result should be treated as a failure.
type AfterToolCallback func(ctx CallbackContext, call *ToolCall, result *ToolResult) error
// ConstraintEnforcementCallback enforces constraints before code generation.
//
// This callback is designed for Implementation Agent. It checks for
// forbidden patterns before any tool that might generate code.
//
// Example usage:
//
// agent.BeforeToolCallback = ConstraintEnforcementCallback(client, []string{
// "write_code",
// "generate_config",
// })
func ConstraintEnforcementCallback(client EpistemeClient, codeGenTools []string) BeforeToolCallback {
return func(ctx CallbackContext, call *ToolCall) error {
// Check if this is a code generation tool
if !slices.Contains(codeGenTools, call.Name) {
return nil // Not a code generation tool, allow it
}
// Extract domain context from tool input
// This is a simplified implementation - in production you'd
// parse the actual tool input to determine context
context := extractDomainContext(call)
// Check constraints
constraintTool := NewConstraintCheckTool(client)
inputBytes, err := json.Marshal(ConstraintCheckInput{
Context: context,
})
if err != nil {
return fmt.Errorf("failed to marshal constraint check input: %w", err)
}
resultBytes, err := constraintTool.Execute(ctx.Context(), inputBytes)
if err != nil {
return fmt.Errorf("constraint check failed: %w", err)
}
var output ConstraintCheckOutput
if err := json.Unmarshal(resultBytes, &output); err != nil {
return fmt.Errorf("failed to parse constraints: %w", err)
}
// Validate against constraints
for _, constraint := range output.Constraints {
if constraint.Forbidden != "" {
// Check if the tool call would use a forbidden pattern
if wouldUseForbiddenPattern(call, constraint.Forbidden) {
return fmt.Errorf("blocked: %s is forbidden - %s",
constraint.Forbidden, constraint.Reason)
}
}
}
return nil
}
}
// ConfidenceEscalationCallback escalates to human when confidence is too low.
//
// This callback is designed for Lead Orchestrator. It checks query results
// and marks the session for human review if confidence is below threshold.
//
// Example usage:
//
// agent.AfterToolCallback = ConfidenceEscalationCallback(0.8, sessionState)
func ConfidenceEscalationCallback(threshold float32, setState func(key string, value any)) AfterToolCallback {
return func(ctx CallbackContext, call *ToolCall, result *ToolResult) error {
// Only check query results
if call.Name != "episteme_query" {
return nil
}
if result.Error != nil {
return nil // Already failed, no need to check confidence
}
// Parse query output
var output QueryOutput
if err := json.Unmarshal(result.Output, &output); err != nil {
return nil // Can't parse, skip
}
// Check confidence threshold
if output.Confidence < threshold {
// Mark for human review
setState("needs_human_review", true)
setState("low_confidence_query", output.QueryID)
setState("escalation_reason", fmt.Sprintf(
"Confidence %.2f below threshold %.2f",
output.Confidence, threshold,
))
}
return nil
}
}
// AuditLoggingCallback logs all tool calls for audit trail.
//
// This callback logs every tool invocation to help with debugging and
// incident investigation.
//
// Example usage:
//
// agent.AfterToolCallback = AuditLoggingCallback(logger)
func AuditLoggingCallback(log func(format string, args ...any)) AfterToolCallback {
return func(ctx CallbackContext, call *ToolCall, result *ToolResult) error {
if result.Error != nil {
log("[%s] Tool %s failed: %v",
ctx.AgentName(), call.Name, result.Error)
} else {
log("[%s] Tool %s succeeded",
ctx.AgentName(), call.Name)
}
return nil
}
}
// ChainCallbacks chains multiple callbacks into one.
//
// This allows combining multiple callback behaviors.
//
// Example usage:
//
// agent.BeforeToolCallback = ChainBeforeCallbacks(
// ConstraintEnforcementCallback(client, codeGenTools),
// CustomValidationCallback(),
// )
func ChainBeforeCallbacks(callbacks ...BeforeToolCallback) BeforeToolCallback {
return func(ctx CallbackContext, call *ToolCall) error {
for _, cb := range callbacks {
if err := cb(ctx, call); err != nil {
return err
}
}
return nil
}
}
// ChainAfterCallbacks chains multiple after-tool callbacks into one.
func ChainAfterCallbacks(callbacks ...AfterToolCallback) AfterToolCallback {
return func(ctx CallbackContext, call *ToolCall, result *ToolResult) error {
for _, cb := range callbacks {
if err := cb(ctx, call, result); err != nil {
return err
}
}
return nil
}
}
// Helper functions
func extractDomainContext(_ *ToolCall) string {
// This is a simplified implementation
// In production, you'd parse the tool input to determine context
// For example, from write_code input you might extract:
// - Language (python, go, etc.)
// - Domain (http, auth, database, etc.)
return "general"
}
func wouldUseForbiddenPattern(call *ToolCall, forbidden string) bool {
// This is a simplified implementation
// In production, you'd analyze the tool input to check if it
// would use the forbidden pattern
//
// For example, check if the input contains references to:
// - Forbidden libraries
// - Forbidden function names
// - Forbidden architectural patterns
//
// This might involve:
// - Parsing code ASTs
// - Checking import statements
// - Pattern matching on configuration
// For now, just check if the input contains the forbidden string
return containsPattern(call.Input, forbidden)
}
func containsPattern(_ json.RawMessage, pattern string) bool {
// Simple string containment check
// In production, use more sophisticated pattern matching
return len(pattern) > 0 // Placeholder
}