Files
Algis DumbrisandClaude Opus 4.6 8df22457ab feat: trust scores, claim semantics, state-change webhooks (011-trust-claims-triggers)
Trust scores: per (agent, action_type) pair, stored in agent_trust
table. Auto-adjusts when human reacts to AI agent messages (approve
+0.05, reject -0.1). Scores clamped [0.0, 1.0]. MCP get_trust action
+ REST API /api/trust/{agent}. Web UI shows trust progress bars on
agent detail pages.

Claim semantics: only one in_progress reaction per message enforced.
First agent to claim wins, duplicates rejected with clear error.

State-change webhooks: StateChangeNotifier interface fires
workflow.state_changed events through existing webhook infrastructure
when reactions change a message's derived workflow state.

Channel thresholds: publish_threshold and approve_threshold fields
on channels for configuring autonomy gates.

Migration 014_trust_claims.sql. 17 new test cases across trust
model + store.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 21:41:54 +02:00

84 lines
2.4 KiB
Go

package trust
import (
"context"
"fmt"
"log/slog"
)
// Service provides business logic for trust score management.
type Service struct {
store Store
logger *slog.Logger
}
// NewService creates a new trust service.
func NewService(store Store, logger *slog.Logger) *Service {
return &Service{
store: store,
logger: logger.With("component", "trust"),
}
}
// RecordApproval increases an agent's trust for an action type.
func (s *Service) RecordApproval(ctx context.Context, agentName, actionType string) (*TrustScore, error) {
ts, err := s.store.UpsertScore(ctx, agentName, actionType, ApprovalIncrement)
if err != nil {
return nil, fmt.Errorf("record approval: %w", err)
}
s.logger.Info("trust increased",
"agent", agentName,
"action", actionType,
"delta", ApprovalIncrement,
"new_score", ts.Score,
)
return ts, nil
}
// RecordRejection decreases an agent's trust for an action type.
func (s *Service) RecordRejection(ctx context.Context, agentName, actionType string) (*TrustScore, error) {
ts, err := s.store.UpsertScore(ctx, agentName, actionType, -RejectionDecrement)
if err != nil {
return nil, fmt.Errorf("record rejection: %w", err)
}
s.logger.Info("trust decreased",
"agent", agentName,
"action", actionType,
"delta", -RejectionDecrement,
"new_score", ts.Score,
)
return ts, nil
}
// GetScores returns all trust scores for an agent as a summary map.
func (s *Service) GetScores(ctx context.Context, agentName string) (AgentTrustSummary, error) {
scores, err := s.store.GetAllScores(ctx, agentName)
if err != nil {
return nil, fmt.Errorf("get scores: %w", err)
}
summary := make(AgentTrustSummary)
for _, ts := range scores {
summary[ts.ActionType] = ts.Score
}
return summary, nil
}
// GetScore returns the trust score for a specific (agent, action) pair.
func (s *Service) GetScore(ctx context.Context, agentName, actionType string) (float64, error) {
ts, err := s.store.GetScore(ctx, agentName, actionType)
if err != nil {
return 0, fmt.Errorf("get score: %w", err)
}
return ts.Score, nil
}
// CheckAutonomy returns whether an agent has sufficient trust for an action
// given a channel's threshold.
func (s *Service) CheckAutonomy(ctx context.Context, agentName, actionType string, threshold float64) (bool, float64, error) {
score, err := s.GetScore(ctx, agentName, actionType)
if err != nil {
return false, 0, err
}
return score >= threshold, score, nil
}