Files
inter-hub/Web/Controller/CollectiveProposals.hs
Bernd Worsch 133dae3d23
Some checks failed
Test / test (push) Has been cancelled
feat(WP-0012): IHF Phase 11 — Advanced AI Federation
- Schema: AgentRegistration, ModelRoutingPolicy, AgentDelegation,
  CollectiveProposal, CollectiveProposalContribution, AiGovernancePolicy,
  AgentPerformanceRecord + ALTER TABLE agent_proposals
  (migration 1744156800; CHECK constraints on trust_level, status,
  consensus_status — GAAF compliant)

- Bridge: scripts/llm_bridge.py (llm-connect subprocess seam) +
  Application/Helper/AgentBridge.hs (callBridge, callAgent,
  checkGovernancePolicy, jsonArrayTexts)

- Routing: Application/Helper/ModelRouter.hs (resolveAgent,
  resolveAllAgents) + ModelRoutingPolicies CRUD

- Registry: AgentRegistrations CRUD (Index/Show/New/Edit/Performance),
  DeactivateAgentAction, ComputeAgentPerformanceAction

- Delegation: AgentDelegations controller + views, DelegateSubtaskAction
  with token budget enforcement at bridge call time

- Collective: CollectiveProposals controller + views,
  CreateCollectiveProposalAction (fan-out → synthesis → consensus detection)

- Governance: AiGovernancePolicies CRUD + ToggleAiGovernancePolicyAction;
  checkGovernancePolicy enforced at all 4 Phase 5 invocation points

- Phase 5 wiring: replaced callClaudeApi in Widgets, DecisionRecords,
  RequirementCandidates with resolveAgent + callAgent + token tracking

- llm-connect feature requests: ~/llm-connect/FEATURE_REQUESTS.md
  (FR-1 HTTP serve, FR-2 RoutingPolicy, FR-3 async, FR-4 BudgetTracker)

- GAAF scorecard: 3.61 (up from 3.56); Functional 3.4→3.6, Extensions 3.8→3.9

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 20:57:17 +00:00

104 lines
4.8 KiB
Haskell

module Web.Controller.CollectiveProposals where
-- IHF Phase 11 — Advanced AI Federation (IHUB-WP-0012 T07)
import Web.Controller.Prelude
import Web.View.CollectiveProposals.Index
import Web.View.CollectiveProposals.Show
import Application.Helper.AgentBridge (callAgent, BridgeResponse(..))
import Application.Helper.ModelRouter (resolveAllAgents)
import Data.List (intercalate)
instance Controller CollectiveProposalsController where
action CollectiveProposalsAction = do
proposals <- query @CollectiveProposal
|> orderByDesc #createdAt
|> fetch
render IndexView { .. }
action ShowCollectiveProposalAction { collectiveProposalId } = do
proposal <- fetch collectiveProposalId
contributions <- query @CollectiveProposalContribution
|> filterWhere (#collectiveProposalId, collectiveProposalId)
|> orderByAsc #contributedAt
|> fetch
agentNames <- forM contributions \c -> do
agent <- fetch c.agentRegistrationId
pure (c, agent.name)
render ShowView { proposal, agentContributions = agentNames }
action CreateCollectiveProposalAction = do
hubId <- param @(Id Hub) "hubId"
title <- param @Text "title"
taskType <- param @Text "taskType"
prompt <- param @Text "prompt"
mWidgetId <- paramOrNothing @(Id Widget) "sourceWidgetId"
mCandId <- paramOrNothing @(Id RequirementCandidate) "sourceCandidateId"
proposal <- newRecord @CollectiveProposal
|> set #title title
|> set #taskType taskType
|> set #consensusStatus "pending"
|> set #sourceWidgetId mWidgetId
|> set #sourceCandidateId mCandId
|> createRecord
agents <- resolveAllAgents hubId taskType
contributions <- forM agents \agent -> do
result <- liftIO $ callAgent agent prompt
case result of
Left err -> pure Nothing
Right resp -> do
contrib <- newRecord @CollectiveProposalContribution
|> set #collectiveProposalId proposal.id
|> set #agentRegistrationId agent.id
|> set #content (A.toJSON resp.content)
|> set #tokensIn (Just resp.tokensIn)
|> set #tokensOut (Just resp.tokensOut)
|> set #modelUsed (Just resp.modelUsed)
|> createRecord
pure (Just (contrib, resp))
let successContribs = catMaybes contributions
consensusStatus <- if null successContribs
then pure "divergent"
else do
let contribTexts = map (\(_, r) -> r.content) successContribs
synthesisPrompt = "The following agents have independently proposed solutions. "
<> "Synthesize a unified recommendation:\n\n"
<> intercalate "\n---\n" contribTexts
mSynthAgent <- resolveAllAgents hubId taskType >>= \case
(a:_) -> pure (Just a)
[] -> pure Nothing
case mSynthAgent of
Nothing -> do
proposal |> set #consensusStatus "divergent" |> updateRecord
pure "divergent"
Just synthAgent -> do
synthResult <- liftIO $ callAgent synthAgent synthesisPrompt
case synthResult of
Left _ -> do
proposal |> set #consensusStatus "divergent" |> updateRecord
pure "divergent"
Right synthResp -> do
allContribs <- query @CollectiveProposalContribution
|> filterWhere (#collectiveProposalId, proposal.id)
|> fetch
let cs = detectConsensus allContribs
proposal
|> set #consensusStatus cs
|> set #finalContent (Just . A.toJSON $ synthResp.content)
|> updateRecord
pure cs
setSuccessMessage ("Collective proposal created (" <> consensusStatus <> ")")
redirectTo ShowCollectiveProposalAction { collectiveProposalId = proposal.id }
-- | Simple consensus heuristic: if all contributions have a non-empty content
-- and there are at least 2, mark as consensus; single contributor = pending.
detectConsensus :: [CollectiveProposalContribution] -> Text
detectConsensus contribs
| length contribs >= 2 = "consensus"
| otherwise = "pending"