Implements US1, US2, and US3 of spec 016 by layering a marketplace service on top of existing primitives rather than reinventing them: - Capability manifests (US2) reuse the wiki subsystem. Each agent publishes a per-agent article at slug "agent-<name>" and gets versioning, revision history, and FTS search for free. - Auction channels (US1) reuse the existing auction channel type, swarm service, and task/bid store. post_auction / bid / award wrap post_task / bid_task / accept_bid and attach marketplace metadata (max_budget_tokens, domains, estimated_tokens, confidence, approach) in the task.requirements and bid.capabilities JSON blobs. Award converts the auction into a claim by DM'ing the winner at priority 8 with task_id metadata, so the existing claim/process/done lifecycle takes over with zero new machinery. - Reputation ledger (US3) adds migration 018_agent_marketplace.sql with a new agent_reputation table keyed by (agent_name, domain). mark_task_done completes the task via the swarm service and writes one ledger row per declared domain using the reported actual_tokens and success_score. query_reputation returns a rolled-up summary plus recent entries for a given (agent, domain) pair — reputation is always a vector, never a global score (FR-013). Also: - Adds the "awarded" reaction type (FR-008) alongside existing approve/ reject/in_progress/done/published. Migration 018 widens the reactions CHECK constraint via a table rebuild. - 6 new actions added to the action registry (post_auction, bid, award, mark_task_done, read_skill_card, query_reputation) so the search tool can discover them and the execute tool can dispatch them. - New internal/marketplace package (store.go + service.go). - New internal/mcp/marketplace.go bridge handlers. - New internal/mcp/marketplace_test.go covers the full auction lifecycle, capability manifest publish/read/update, self-bid rejection, non-auction channel rejection, and reputation summary aggregation. Out of scope for MVP (deferred per spec prompt): US4 reflection loop, tombstoning FR-020a/b, multi-owner quorums, auto-escalation on zero bids, bootstrap exploration credit, epsilon-greedy selection, and the hard-stop budget enforcement daemon (only soft recording of estimated vs actual is included). All existing tests pass; new marketplace tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
121 lines
3.3 KiB
Go
121 lines
3.3 KiB
Go
// Package reactions provides message reaction types, storage, and workflow state logic.
|
|
package reactions
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// Valid reaction types.
|
|
const (
|
|
ReactionApprove = "approve"
|
|
ReactionReject = "reject"
|
|
ReactionInProgress = "in_progress"
|
|
ReactionDone = "done"
|
|
ReactionPublished = "published"
|
|
// ReactionAwarded is used on auction-channel bid messages to designate
|
|
// the winning bid (spec 016, FR-008). Added in feature 016.
|
|
ReactionAwarded = "awarded"
|
|
)
|
|
|
|
// Workflow states (derived from reactions).
|
|
const (
|
|
StateProposed = "proposed"
|
|
StateApproved = "approved"
|
|
StateInProgress = "in_progress"
|
|
StateRejected = "rejected"
|
|
StateDone = "done"
|
|
StatePublished = "published"
|
|
StateAwarded = "awarded"
|
|
)
|
|
|
|
// reactionPriority maps reaction types to their priority for state derivation.
|
|
// Higher number = higher priority = wins for badge display.
|
|
var reactionPriority = map[string]int{
|
|
ReactionApprove: 2,
|
|
ReactionInProgress: 3,
|
|
ReactionReject: 4,
|
|
ReactionAwarded: 5,
|
|
ReactionDone: 6,
|
|
ReactionPublished: 7,
|
|
}
|
|
|
|
// reactionToState maps reaction types to workflow states.
|
|
var reactionToState = map[string]string{
|
|
ReactionApprove: StateApproved,
|
|
ReactionReject: StateRejected,
|
|
ReactionInProgress: StateInProgress,
|
|
ReactionAwarded: StateAwarded,
|
|
ReactionDone: StateDone,
|
|
ReactionPublished: StatePublished,
|
|
}
|
|
|
|
// TerminalStates are states that should not trigger stalemate checks.
|
|
var TerminalStates = map[string]bool{
|
|
StateRejected: true,
|
|
StateDone: true,
|
|
StatePublished: true,
|
|
}
|
|
|
|
// MaxReactionsPerMessage is the safety limit.
|
|
const MaxReactionsPerMessage = 100
|
|
|
|
// Sentinel errors.
|
|
var (
|
|
ErrInvalidReaction = errors.New("invalid reaction type: must be one of approve, reject, in_progress, awarded, done, published")
|
|
ErrReactionLimit = errors.New("maximum reactions per message (100) reached")
|
|
ErrNotMember = errors.New("only channel members can react to messages")
|
|
)
|
|
|
|
// Reaction represents a single reaction on a message.
|
|
type Reaction struct {
|
|
ID int64 `json:"id"`
|
|
MessageID int64 `json:"message_id"`
|
|
AgentName string `json:"agent_name"`
|
|
Reaction string `json:"reaction"`
|
|
Metadata json.RawMessage `json:"metadata"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// ValidReactions is the set of allowed reaction types.
|
|
var ValidReactions = map[string]bool{
|
|
ReactionApprove: true,
|
|
ReactionReject: true,
|
|
ReactionInProgress: true,
|
|
ReactionAwarded: true,
|
|
ReactionDone: true,
|
|
ReactionPublished: true,
|
|
}
|
|
|
|
// IsValidReaction returns true if the reaction type is valid.
|
|
func IsValidReaction(r string) bool {
|
|
return ValidReactions[r]
|
|
}
|
|
|
|
// ComputeWorkflowState derives the workflow state from a list of reactions.
|
|
// Returns "proposed" if no reactions exist.
|
|
func ComputeWorkflowState(reactions []*Reaction) string {
|
|
if len(reactions) == 0 {
|
|
return StateProposed
|
|
}
|
|
|
|
highestPriority := 0
|
|
highestState := StateProposed
|
|
|
|
for _, r := range reactions {
|
|
p, ok := reactionPriority[r.Reaction]
|
|
if ok && p > highestPriority {
|
|
highestPriority = p
|
|
highestState = reactionToState[r.Reaction]
|
|
}
|
|
}
|
|
|
|
return highestState
|
|
}
|
|
|
|
// IsTerminalState returns true if the state should not trigger stalemate checks.
|
|
func IsTerminalState(state string) bool {
|
|
return TerminalStates[state]
|
|
}
|