From 133dae3d23f6cf62ea4ab32c4e12c70cb7ffdd3a Mon Sep 17 00:00:00 2001 From: Bernd Worsch Date: Wed, 1 Apr 2026 20:57:17 +0000 Subject: [PATCH] =?UTF-8?q?feat(WP-0012):=20IHF=20Phase=2011=20=E2=80=94?= =?UTF-8?q?=20Advanced=20AI=20Federation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- ARCHITECTURE-LAYERS.md | 41 +++-- Application/Helper/AgentBridge.hs | 116 +++++++++++++ Application/Helper/ModelRouter.hs | 41 +++++ .../1744156800-ihf-phase11-ai-federation.sql | 134 +++++++++++++++ Application/Schema.sql | 140 ++++++++++++++++ CLAUDE.md | 6 +- Web/Controller/AgentDelegations.hs | 74 +++++++++ Web/Controller/AgentRegistrations.hs | 122 ++++++++++++++ Web/Controller/AiGovernancePolicies.hs | 65 ++++++++ Web/Controller/CollectiveProposals.hs | 103 ++++++++++++ Web/Controller/DecisionRecords.hs | 68 +++++--- Web/Controller/ModelRoutingPolicies.hs | 47 ++++++ Web/Controller/RequirementCandidates.hs | 71 +++++--- Web/Controller/Widgets.hs | 118 +++++++++----- Web/FrontController.hs | 16 ++ Web/Routes.hs | 7 + Web/Types.hs | 39 +++++ Web/View/AgentDelegations/Index.hs | 50 ++++++ Web/View/AgentDelegations/Show.hs | 64 ++++++++ Web/View/AgentRegistrations/Edit.hs | 17 ++ Web/View/AgentRegistrations/Index.hs | 72 +++++++++ Web/View/AgentRegistrations/New.hs | 56 +++++++ Web/View/AgentRegistrations/Performance.hs | 7 + Web/View/AgentRegistrations/Show.hs | 153 ++++++++++++++++++ Web/View/AiGovernancePolicies/Index.hs | 63 ++++++++ Web/View/AiGovernancePolicies/New.hs | 57 +++++++ Web/View/CollectiveProposals/Index.hs | 47 ++++++ Web/View/CollectiveProposals/Show.hs | 58 +++++++ Web/View/ModelRoutingPolicies/Index.hs | 65 ++++++++ Web/View/ModelRoutingPolicies/New.hs | 55 +++++++ scripts/llm_bridge.py | 67 ++++++++ ...0012-ihf-phase11-advanced-ai-federation.md | 22 +-- 32 files changed, 1959 insertions(+), 102 deletions(-) create mode 100644 Application/Helper/AgentBridge.hs create mode 100644 Application/Helper/ModelRouter.hs create mode 100644 Application/Migration/1744156800-ihf-phase11-ai-federation.sql create mode 100644 Web/Controller/AgentDelegations.hs create mode 100644 Web/Controller/AgentRegistrations.hs create mode 100644 Web/Controller/AiGovernancePolicies.hs create mode 100644 Web/Controller/CollectiveProposals.hs create mode 100644 Web/Controller/ModelRoutingPolicies.hs create mode 100644 Web/View/AgentDelegations/Index.hs create mode 100644 Web/View/AgentDelegations/Show.hs create mode 100644 Web/View/AgentRegistrations/Edit.hs create mode 100644 Web/View/AgentRegistrations/Index.hs create mode 100644 Web/View/AgentRegistrations/New.hs create mode 100644 Web/View/AgentRegistrations/Performance.hs create mode 100644 Web/View/AgentRegistrations/Show.hs create mode 100644 Web/View/AiGovernancePolicies/Index.hs create mode 100644 Web/View/AiGovernancePolicies/New.hs create mode 100644 Web/View/CollectiveProposals/Index.hs create mode 100644 Web/View/CollectiveProposals/Show.hs create mode 100644 Web/View/ModelRoutingPolicies/Index.hs create mode 100644 Web/View/ModelRoutingPolicies/New.hs create mode 100755 scripts/llm_bridge.py diff --git a/ARCHITECTURE-LAYERS.md b/ARCHITECTURE-LAYERS.md index 6b592da..2eac04e 100644 --- a/ARCHITECTURE-LAYERS.md +++ b/ARCHITECTURE-LAYERS.md @@ -46,6 +46,9 @@ Value-realisation modules. Each module has a declared maturity. See - Requirements (promoted from candidates) — **Stable** - DeploymentRecord + OutcomeSignal — **Stable** - AgentProposal + AgentReviewRecord + ConfidenceAnnotation — **Beta** +- AgentRegistration + ModelRoutingPolicy — **Beta** +- AgentDelegation + CollectiveProposal + CollectiveProposalContribution — **Experimental** +- AiGovernancePolicy + AgentPerformanceRecord — **Beta** - Cross-framework adapter contracts (EnvelopeEmissionContract, InteractionReportingContract, WidgetAdapterSpec) — **Stable** - FrictionScore + BottleneckRecord — **Beta** @@ -116,6 +119,15 @@ version history), `PatternAdoption` (hub adoption records with pin/follow-latest `GovernanceTemplate` (reusable governance templates with category JSONB validated at controller), `GovernanceTemplateClone` (adoption records for governance templates). +Phase 11 adds the AI federation layer: `AgentRegistration` (named, versioned AI +agents with provider/model/trust_level), `ModelRoutingPolicy` (task_type → agent +routing rules per hub), `AgentDelegation` (bounded inter-agent subtask delegation +with token budget enforcement), `CollectiveProposal` + `CollectiveProposalContribution` +(multi-agent proposals with per-agent attribution and consensus detection), +`AiGovernancePolicy` (per-hub agent scope rules with allowed_actions JSONB), +`AgentPerformanceRecord` (30-day acceptance/confidence snapshots). +Integration via `scripts/llm_bridge.py` subprocess bridge to llm-connect Python library. + --- ## Dependency Rule @@ -136,27 +148,30 @@ Downward dependencies (Core → Functional) are **forbidden**. ## GAAF-2026 Scorecard -*Last updated: 2026-04-01 (post IHUB-WP-0011 — Phase 10 Hub Registry and Widget Marketplace)* +*Last updated: 2026-04-01 (post IHUB-WP-0012 — Phase 11 Advanced AI Federation)* | Layer | Score (0–5) | Weight | Weighted | Notes | |---|---|---|---|---| | Core | 3.8 | 30% | 1.14 | Contracts formalised; type registries anchor discriminators | -| Functional | 3.4 | 20% | 0.68 | API v2 covers hub registry + marketplace; OpenAPI spec updated | +| Functional | 3.6 | 20% | 0.72 | Multi-agent federation formalises AI collaboration; bridge + routing operational | | Customization | 3.2 | 15% | 0.48 | Manifest amendment workflow is formal per-hub config contract with migration | | Configuration | 3.2 | 10% | 0.32 | OAuth scopes validate against manifest; rate limits per consumer | -| Extensions | 3.8 | 10% | 0.38 | Hub Registry UI + API; widget pattern marketplace operational | +| Extensions | 3.9 | 10% | 0.39 | Agent registry + routing + governance policies expose AI surface via UI and API | | Cross-layer | 3.7 | 15% | 0.56 | Fitness functions in CI; contracts documented; layer map current | -| **Total** | | | **3.56** | Strong — Phase 10 exit criteria met | +| **Total** | | | **3.61** | Strong — Phase 11 exit criteria met | -**Interpretation:** 3.56 = Strong (≥3.5). Phase 10 exit target achieved. +**Interpretation:** 3.61 = Strong (≥3.5). Phase 11 exit target achieved. -*Customization layer improvement (2.5 → 3.2):* The `PatternAdoption` and -`CloneGovernanceTemplate` workflows require a manifest amendment draft when new -types are introduced, making the manifest a formal per-hub configuration contract -with an explicit activation gate. This is the specific GAAF-2026 Customization -criterion: formal, migration-backed per-hub configuration. +*Functional layer improvement (3.4 → 3.6):* AgentRegistration + ModelRoutingPolicy + +AgentDelegation + CollectiveProposal + AiGovernancePolicy formalise multi-agent +federation as first-class governed artifacts. Agent invocations are now routed +through the registry, policy-checked, and attributed with token counts. -*Previous scorecard (Phase 9):* 3.41 (Usable but vulnerable) +*Extensions layer improvement (3.8 → 3.9):* Agent Registry UI + API surface; +collective proposals expose multi-agent output with per-contributor attribution. +AiGovernancePolicy makes AI scope constraints explicit and operator-configurable. + +*Previous scorecard (Phase 10):* 3.56 (Strong) *Next review date: 2026-09-30* @@ -212,3 +227,7 @@ Run as part of the standard `test` command. | 2026-04-01 | widget_patterns.widget_type is a true FK to widget_type_registry | GAAF rule: no bare TEXT type discriminators; FK ensures patterns only reference registered types | | 2026-04-01 | governance_templates.categories validated at controller (JSONB array FK) | SQL cannot express array FK; controller validates each element against annotation_category_registry at write time | | 2026-04-01 | Manifest amendment gate on pattern adoption and template cloning | Adopting a cross-type-boundary artifact must go through the manifest activation flow to maintain GAAF compliance | +| 2026-04-01 | llm-connect via subprocess bridge (not direct Haskell FFI) | llm-connect is Python-only; subprocess bridge is the correct integration seam — avoids GHC/CPython FFI complexity | +| 2026-04-01 | trust_level/status/consensus_status as TEXT with CHECK constraints | GAAF rule: no bare TEXT discriminators; finite closed-set values suit CHECK over registry for these internal ADTs | +| 2026-04-01 | AiGovernancePolicy default = permit (no policy = allow propose) | Conservative default avoids silently blocking existing workflows after Phase 11 migration; operators add restrictions explicitly | +| 2026-04-01 | agent_proposals ALTER TABLE (not new table) for agent_registration_id | agent_proposals is a Core-adjacent table; extending it is cheaper and more traceable than a parallel Phase 11 table | diff --git a/Application/Helper/AgentBridge.hs b/Application/Helper/AgentBridge.hs new file mode 100644 index 0000000..7316850 --- /dev/null +++ b/Application/Helper/AgentBridge.hs @@ -0,0 +1,116 @@ +module Application.Helper.AgentBridge where + +-- IHF Phase 11 — Advanced AI Federation (IHUB-WP-0012) +-- Haskell wrapper around scripts/llm_bridge.py (llm-connect subprocess bridge). + +import IHP.Prelude +import IHP.ControllerPrelude +import Data.Aeson (object, (.=), encode, decode, Value, FromJSON(..), (.:), (.:?)) +import qualified Data.Aeson as A +import qualified Data.ByteString.Lazy as LBS +import System.Process (readProcessWithExitCode) +import System.Exit (ExitCode(..)) +import Generated.Types + +-- --------------------------------------------------------------------------- +-- Request / response types + +data BridgeRequest = BridgeRequest + { provider :: !Text + , model :: !Text + , systemPrompt :: !(Maybe Text) + , prompt :: !Text + , maxTokens :: !Int + , temperature :: !Double + } + +data BridgeResponse = BridgeResponse + { content :: !Text + , modelUsed :: !Text + , tokensIn :: !Int + , tokensOut :: !Int + , finishReason :: !Text + } deriving (Show) + +data BridgeError = BridgeError + { errorMessage :: !Text + , errorType :: !Text + } deriving (Show) + +instance FromJSON BridgeResponse where + parseJSON = A.withObject "BridgeResponse" \o -> BridgeResponse + <$> o .: "content" + <*> o .: "model" + <*> o .: "tokensIn" + <*> o .: "tokensOut" + <*> o .: "finishReason" + +instance FromJSON BridgeError where + parseJSON = A.withObject "BridgeError" \o -> BridgeError + <$> o .: "error" + <*> o .: "errorType" + +-- --------------------------------------------------------------------------- +-- Core bridge call + +-- | Invoke the llm_bridge.py subprocess with the given request. +callBridge :: BridgeRequest -> IO (Either BridgeError BridgeResponse) +callBridge req = do + let payload = LBS.toStrict . encode $ object + [ "provider" .= req.provider + , "model" .= req.model + , "systemPrompt" .= req.systemPrompt + , "prompt" .= req.prompt + , "maxTokens" .= req.maxTokens + , "temperature" .= req.temperature + ] + (exitCode, stdout, stderr) <- + readProcessWithExitCode "python3" ["scripts/llm_bridge.py"] (cs payload) + let outBytes = LBS.fromStrict (cs stdout) + case exitCode of + ExitSuccess -> + case decode outBytes of + Just v -> pure (Right v) + Nothing -> pure (Left (BridgeError "Unparseable bridge output" "ParseError")) + ExitFailure _ -> + case decode outBytes of + Just v -> pure (Left v) + Nothing -> pure (Left (BridgeError (cs stderr) "BridgeError")) + +-- | Call the bridge using an AgentRegistration record. +callAgent :: AgentRegistration -> Text -> IO (Either BridgeError BridgeResponse) +callAgent agent userPrompt = + callBridge BridgeRequest + { provider = agent.provider + , model = agent.modelName + , systemPrompt = agent.systemPrompt + , prompt = userPrompt + , maxTokens = 2000 + , temperature = 0.7 + } + +-- --------------------------------------------------------------------------- +-- AI governance policy check + +-- | Returns True if the agent is allowed to perform the 'propose' action on +-- the given artifact_type in this hub. +-- When no policy exists the default is permissive (True). +checkGovernancePolicy :: + (?modelContext :: ModelContext) => + Id Hub -> Id AgentRegistration -> Text -> IO Bool +checkGovernancePolicy hubId agentId artifactType = do + mPolicy <- query @AiGovernancePolicy + |> filterWhere (#hubId, hubId) + |> filterWhere (#agentRegistrationId, agentId) + |> filterWhere (#artifactType, artifactType) + |> filterWhere (#isActive, True) + |> fetchOneOrNothing + case mPolicy of + Nothing -> pure True + Just p -> pure ("propose" `elem` jsonArrayTexts p.allowedActions) + +-- | Extract Text values from a JSONB array. +jsonArrayTexts :: Value -> [Text] +jsonArrayTexts (A.Array vs) = + [ t | A.String t <- toList vs ] +jsonArrayTexts _ = [] diff --git a/Application/Helper/ModelRouter.hs b/Application/Helper/ModelRouter.hs new file mode 100644 index 0000000..bf14eea --- /dev/null +++ b/Application/Helper/ModelRouter.hs @@ -0,0 +1,41 @@ +module Application.Helper.ModelRouter where + +-- IHF Phase 11 — Advanced AI Federation (IHUB-WP-0012 T04) +-- Resolve the AgentRegistration to use for a hub + task type. + +import IHP.Prelude +import IHP.ControllerPrelude +import Generated.Types +import Database.PostgreSQL.Simple (Only(..)) + +-- | Resolve the highest-priority active AgentRegistration for the given hub +-- and task type. Returns Nothing if no matching policy exists (callers should +-- fall back gracefully or surface an error to the operator). +resolveAgent :: + (?modelContext :: ModelContext) => + Id Hub -> Text -> IO (Maybe AgentRegistration) +resolveAgent hubId taskType = do + rows <- sqlQuery + "SELECT mrp.agent_registration_id \ + \ FROM model_routing_policies mrp \ + \ WHERE mrp.hub_id = ? AND mrp.task_type = ? AND mrp.is_active = TRUE \ + \ ORDER BY mrp.priority DESC \ + \ LIMIT 1" + (hubId, taskType) + case rows of + [Only agentId] -> fetchOneOrNothing agentId + _ -> pure Nothing + +-- | Return all active AgentRegistrations for a hub + task_type ordered by +-- priority (highest first). Used by CollectiveProposals to fan out. +resolveAllAgents :: + (?modelContext :: ModelContext) => + Id Hub -> Text -> IO [AgentRegistration] +resolveAllAgents hubId taskType = do + rows <- sqlQuery + "SELECT mrp.agent_registration_id \ + \ FROM model_routing_policies mrp \ + \ WHERE mrp.hub_id = ? AND mrp.task_type = ? AND mrp.is_active = TRUE \ + \ ORDER BY mrp.priority DESC" + (hubId, taskType) + mapM (fetch . (\(Only i) -> i)) rows diff --git a/Application/Migration/1744156800-ihf-phase11-ai-federation.sql b/Application/Migration/1744156800-ihf-phase11-ai-federation.sql new file mode 100644 index 0000000..477336d --- /dev/null +++ b/Application/Migration/1744156800-ihf-phase11-ai-federation.sql @@ -0,0 +1,134 @@ +-- IHF Phase 11 — Advanced AI Federation (IHUB-WP-0012) +-- Migration timestamp: 1744156800 + +-- agent_registrations: named, versioned AI agents backed by llm-connect providers +CREATE TABLE agent_registrations ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + hub_id UUID NOT NULL REFERENCES hubs(id), + name TEXT NOT NULL, + slug TEXT NOT NULL UNIQUE, + description TEXT, + provider TEXT NOT NULL, + model_name TEXT NOT NULL, + trust_level TEXT NOT NULL DEFAULT 'advisory', + capabilities JSONB NOT NULL DEFAULT '[]', + system_prompt TEXT, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + version INTEGER NOT NULL DEFAULT 1, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + CHECK (trust_level IN ('advisory', 'elevated', 'autonomous')) +); + +CREATE INDEX agent_registrations_hub_id_idx ON agent_registrations (hub_id); +CREATE INDEX agent_registrations_slug_idx ON agent_registrations (slug); +CREATE INDEX agent_registrations_is_active_idx ON agent_registrations (is_active); + +-- model_routing_policies: task_type → agent selection rules per hub +CREATE TABLE model_routing_policies ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + hub_id UUID NOT NULL REFERENCES hubs(id), + task_type TEXT NOT NULL, + agent_registration_id UUID NOT NULL REFERENCES agent_registrations(id), + priority INTEGER NOT NULL DEFAULT 0, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + UNIQUE (hub_id, task_type, priority) +); + +CREATE INDEX model_routing_policies_hub_task_idx ON model_routing_policies (hub_id, task_type); + +-- agent_delegations: auditable inter-agent subtask delegation records +CREATE TABLE agent_delegations ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + delegating_agent_id UUID NOT NULL REFERENCES agent_registrations(id), + receiving_agent_id UUID NOT NULL REFERENCES agent_registrations(id), + parent_proposal_id UUID REFERENCES agent_proposals(id), + scope TEXT NOT NULL, + token_budget INTEGER NOT NULL DEFAULT 1000, + tokens_used INTEGER, + status TEXT NOT NULL DEFAULT 'pending', + result JSONB, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + completed_at TIMESTAMP WITH TIME ZONE, + CHECK (status IN ('pending', 'completed', 'failed', 'cancelled')) +); + +CREATE INDEX agent_delegations_delegating_idx ON agent_delegations (delegating_agent_id); +CREATE INDEX agent_delegations_receiving_idx ON agent_delegations (receiving_agent_id); +CREATE INDEX agent_delegations_parent_proposal_idx ON agent_delegations (parent_proposal_id); + +-- collective_proposals: multi-agent proposals with attribution +CREATE TABLE collective_proposals ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + title TEXT NOT NULL, + summary TEXT, + task_type TEXT NOT NULL, + consensus_status TEXT NOT NULL DEFAULT 'pending', + final_content JSONB, + source_widget_id UUID REFERENCES widgets(id), + source_candidate_id UUID REFERENCES requirement_candidates(id), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + CHECK (consensus_status IN ('pending', 'consensus', 'divergent')) +); + +CREATE INDEX collective_proposals_task_type_idx ON collective_proposals (task_type); +CREATE INDEX collective_proposals_consensus_status_idx ON collective_proposals (consensus_status); + +-- collective_proposal_contributions: per-agent contribution records +CREATE TABLE collective_proposal_contributions ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + collective_proposal_id UUID NOT NULL REFERENCES collective_proposals(id), + agent_registration_id UUID NOT NULL REFERENCES agent_registrations(id), + content JSONB NOT NULL, + tokens_in INTEGER, + tokens_out INTEGER, + model_used TEXT, + contributed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL +); + +CREATE INDEX collective_proposal_contributions_proposal_idx ON collective_proposal_contributions (collective_proposal_id); +CREATE INDEX collective_proposal_contributions_agent_idx ON collective_proposal_contributions (agent_registration_id); + +-- ai_governance_policies: per-hub rules controlling agent scope +CREATE TABLE ai_governance_policies ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + hub_id UUID NOT NULL REFERENCES hubs(id), + agent_registration_id UUID NOT NULL REFERENCES agent_registrations(id), + artifact_type TEXT NOT NULL, + allowed_actions JSONB NOT NULL DEFAULT '["read"]', + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL +); + +CREATE INDEX ai_governance_policies_hub_agent_idx ON ai_governance_policies (hub_id, agent_registration_id); +CREATE INDEX ai_governance_policies_is_active_idx ON ai_governance_policies (is_active); + +-- agent_performance_records: periodic snapshots of per-agent metrics +CREATE TABLE agent_performance_records ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + agent_registration_id UUID NOT NULL REFERENCES agent_registrations(id), + hub_id UUID NOT NULL REFERENCES hubs(id), + period_start TIMESTAMP WITH TIME ZONE NOT NULL, + period_end TIMESTAMP WITH TIME ZONE NOT NULL, + proposals_generated INTEGER NOT NULL DEFAULT 0, + proposals_accepted INTEGER NOT NULL DEFAULT 0, + proposals_rejected INTEGER NOT NULL DEFAULT 0, + proposals_revised INTEGER NOT NULL DEFAULT 0, + mean_confidence DOUBLE PRECISION, + calibration_score DOUBLE PRECISION, + computed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL +); + +CREATE INDEX agent_performance_records_agent_idx ON agent_performance_records (agent_registration_id); +CREATE INDEX agent_performance_records_period_idx ON agent_performance_records (period_start, period_end); + +-- Extend agent_proposals with agent_registration_id and token tracking +ALTER TABLE agent_proposals + ADD COLUMN agent_registration_id UUID REFERENCES agent_registrations(id), + ADD COLUMN tokens_in INTEGER, + ADD COLUMN tokens_out INTEGER; + +CREATE INDEX agent_proposals_agent_registration_idx ON agent_proposals (agent_registration_id); diff --git a/Application/Schema.sql b/Application/Schema.sql index e261d15..61ff39b 100644 --- a/Application/Schema.sql +++ b/Application/Schema.sql @@ -865,3 +865,143 @@ CREATE TABLE governance_template_clones ( CREATE INDEX governance_template_clones_template_idx ON governance_template_clones (governance_template_id); CREATE INDEX governance_template_clones_hub_idx ON governance_template_clones (cloning_hub_id); + +-- IHF Phase 11 — Advanced AI Federation (IHUB-WP-0012) + +-- agent_registrations: named, versioned AI agents backed by llm-connect providers +-- GAAF: trust_level CHECK constraint — no bare TEXT discriminator +CREATE TABLE agent_registrations ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + hub_id UUID NOT NULL REFERENCES hubs(id), + name TEXT NOT NULL, + slug TEXT NOT NULL UNIQUE, + description TEXT, + provider TEXT NOT NULL, + -- provider values: openrouter | gemini | openai | claude-code + model_name TEXT NOT NULL, + trust_level TEXT NOT NULL DEFAULT 'advisory', + capabilities JSONB NOT NULL DEFAULT '[]', + system_prompt TEXT, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + version INTEGER NOT NULL DEFAULT 1, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + CHECK (trust_level IN ('advisory', 'elevated', 'autonomous')) +); + +CREATE INDEX agent_registrations_hub_id_idx ON agent_registrations (hub_id); +CREATE INDEX agent_registrations_slug_idx ON agent_registrations (slug); +CREATE INDEX agent_registrations_is_active_idx ON agent_registrations (is_active); + +-- model_routing_policies: task_type → agent selection rules per hub +CREATE TABLE model_routing_policies ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + hub_id UUID NOT NULL REFERENCES hubs(id), + task_type TEXT NOT NULL, + agent_registration_id UUID NOT NULL REFERENCES agent_registrations(id), + priority INTEGER NOT NULL DEFAULT 0, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + UNIQUE (hub_id, task_type, priority) +); + +CREATE INDEX model_routing_policies_hub_task_idx ON model_routing_policies (hub_id, task_type); + +-- agent_delegations: auditable inter-agent subtask delegation records +-- GAAF: status CHECK constraint +CREATE TABLE agent_delegations ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + delegating_agent_id UUID NOT NULL REFERENCES agent_registrations(id), + receiving_agent_id UUID NOT NULL REFERENCES agent_registrations(id), + parent_proposal_id UUID REFERENCES agent_proposals(id), + scope TEXT NOT NULL, + token_budget INTEGER NOT NULL DEFAULT 1000, + tokens_used INTEGER, + status TEXT NOT NULL DEFAULT 'pending', + result JSONB, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + completed_at TIMESTAMP WITH TIME ZONE, + CHECK (status IN ('pending', 'completed', 'failed', 'cancelled')) +); + +CREATE INDEX agent_delegations_delegating_idx ON agent_delegations (delegating_agent_id); +CREATE INDEX agent_delegations_receiving_idx ON agent_delegations (receiving_agent_id); +CREATE INDEX agent_delegations_parent_proposal_idx ON agent_delegations (parent_proposal_id); + +-- collective_proposals: multi-agent proposals with attribution +-- GAAF: consensus_status CHECK constraint +CREATE TABLE collective_proposals ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + title TEXT NOT NULL, + summary TEXT, + task_type TEXT NOT NULL, + consensus_status TEXT NOT NULL DEFAULT 'pending', + final_content JSONB, + source_widget_id UUID REFERENCES widgets(id), + source_candidate_id UUID REFERENCES requirement_candidates(id), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + CHECK (consensus_status IN ('pending', 'consensus', 'divergent')) +); + +CREATE INDEX collective_proposals_task_type_idx ON collective_proposals (task_type); +CREATE INDEX collective_proposals_consensus_status_idx ON collective_proposals (consensus_status); + +-- collective_proposal_contributions: per-agent contribution records +CREATE TABLE collective_proposal_contributions ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + collective_proposal_id UUID NOT NULL REFERENCES collective_proposals(id), + agent_registration_id UUID NOT NULL REFERENCES agent_registrations(id), + content JSONB NOT NULL, + tokens_in INTEGER, + tokens_out INTEGER, + model_used TEXT, + contributed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL +); + +CREATE INDEX collective_proposal_contributions_proposal_idx ON collective_proposal_contributions (collective_proposal_id); +CREATE INDEX collective_proposal_contributions_agent_idx ON collective_proposal_contributions (agent_registration_id); + +-- ai_governance_policies: per-hub rules controlling agent scope +-- allowed_actions is JSONB array; elements validated at controller layer +-- (each element: read | propose | delegate | auto_apply) +CREATE TABLE ai_governance_policies ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + hub_id UUID NOT NULL REFERENCES hubs(id), + agent_registration_id UUID NOT NULL REFERENCES agent_registrations(id), + artifact_type TEXT NOT NULL, + allowed_actions JSONB NOT NULL DEFAULT '["read"]', + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL +); + +CREATE INDEX ai_governance_policies_hub_agent_idx ON ai_governance_policies (hub_id, agent_registration_id); +CREATE INDEX ai_governance_policies_is_active_idx ON ai_governance_policies (is_active); + +-- agent_performance_records: periodic snapshots of per-agent metrics +CREATE TABLE agent_performance_records ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + agent_registration_id UUID NOT NULL REFERENCES agent_registrations(id), + hub_id UUID NOT NULL REFERENCES hubs(id), + period_start TIMESTAMP WITH TIME ZONE NOT NULL, + period_end TIMESTAMP WITH TIME ZONE NOT NULL, + proposals_generated INTEGER NOT NULL DEFAULT 0, + proposals_accepted INTEGER NOT NULL DEFAULT 0, + proposals_rejected INTEGER NOT NULL DEFAULT 0, + proposals_revised INTEGER NOT NULL DEFAULT 0, + mean_confidence DOUBLE PRECISION, + calibration_score DOUBLE PRECISION, + computed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL +); + +CREATE INDEX agent_performance_records_agent_idx ON agent_performance_records (agent_registration_id); +CREATE INDEX agent_performance_records_period_idx ON agent_performance_records (period_start, period_end); + +-- Extend agent_proposals with agent_registration_id and token tracking (Phase 11) +ALTER TABLE agent_proposals + ADD COLUMN agent_registration_id UUID REFERENCES agent_registrations(id), + ADD COLUMN tokens_in INTEGER, + ADD COLUMN tokens_out INTEGER; + +CREATE INDEX agent_proposals_agent_registration_idx ON agent_proposals (agent_registration_id); diff --git a/CLAUDE.md b/CLAUDE.md index 43d14d4..36883e9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co **inter-hub** is the reference implementation of the **Interaction Hub Framework (IHF)** — a governed, observable interaction substrate for hub-based AI-enabled software systems. It treats every UI element as a governed artifact, creating a full traceability chain from rendered widget → user interaction → structured feedback → requirement candidate → decision record → implementation change → observed outcome. -**Current state:** Phases 1–10 complete (including GAAF Compliance Foundation and External API Surface). Phase 11 (Advanced AI Federation) is the active implementation target. +**Current state:** Phases 1–11 complete (including GAAF Compliance Foundation, External API Surface, Hub Registry, and Advanced AI Federation). Phase 12 (Platform Memory and Continuous Learning) is the active implementation target. For situational context, read `SCOPE.md`. For architecture depth, read `specs/InteractionHubFrameworkSpecification_v0.1.md`. @@ -108,9 +108,9 @@ Key rules: ## Active Workplan -Phase 11 (Advanced AI Federation) is the next target. Create workplan `workplans/IHUB-WP-0012-ihf-phase11-advanced-ai-federation.md` when ready. Use `/ralph-workplan` to drive implementation. +Phase 12 (Platform Memory and Continuous Learning) is the next target. Create workplan `workplans/IHUB-WP-0013-ihf-phase12-platform-memory.md` when ready. Use `/ralph-workplan` to drive implementation. -Completed workplans: IHUB-WP-0001 (Phase 1), IHUB-WP-0002 (Phase 2), IHUB-WP-0003 (Phase 3), IHUB-WP-0004 (Phase 4), IHUB-WP-0005 (Phase 5), IHUB-WP-0006 (Phase 6), IHUB-WP-0007 (Phase 7), IHUB-WP-0008 (Phase 8), IHUB-WP-0009 (GAAF Compliance Foundation), IHUB-WP-0010 (Phase 9 — External API Surface and Consumer SDKs), IHUB-WP-0011 (Phase 10 — Hub Registry and Widget Marketplace). +Completed workplans: IHUB-WP-0001 (Phase 1), IHUB-WP-0002 (Phase 2), IHUB-WP-0003 (Phase 3), IHUB-WP-0004 (Phase 4), IHUB-WP-0005 (Phase 5), IHUB-WP-0006 (Phase 6), IHUB-WP-0007 (Phase 7), IHUB-WP-0008 (Phase 8), IHUB-WP-0009 (GAAF Compliance Foundation), IHUB-WP-0010 (Phase 9 — External API Surface and Consumer SDKs), IHUB-WP-0011 (Phase 10 — Hub Registry and Widget Marketplace), IHUB-WP-0012 (Phase 11 — Advanced AI Federation). ## GAAF Architecture Rules (enforced from IHUB-WP-0009) diff --git a/Web/Controller/AgentDelegations.hs b/Web/Controller/AgentDelegations.hs new file mode 100644 index 0000000..daeefd7 --- /dev/null +++ b/Web/Controller/AgentDelegations.hs @@ -0,0 +1,74 @@ +module Web.Controller.AgentDelegations where + +-- IHF Phase 11 — Advanced AI Federation (IHUB-WP-0012 T06) + +import Web.Controller.Prelude +import Web.View.AgentDelegations.Index +import Web.View.AgentDelegations.Show +import Application.Helper.AgentBridge (callBridge, BridgeRequest(..)) + +instance Controller AgentDelegationsController where + + action AgentDelegationsAction = do + delegations <- query @AgentDelegation + |> orderByDesc #createdAt + |> fetch + render IndexView { .. } + + action ShowAgentDelegationAction { agentDelegationId } = do + delegation <- fetch agentDelegationId + delegatingAgent <- fetch delegation.delegatingAgentId + receivingAgent <- fetch delegation.receivingAgentId + mParentProposal <- case delegation.parentProposalId of + Nothing -> pure Nothing + Just pid -> fetchOneOrNothing pid + render ShowView { .. } + + action DelegateSubtaskAction { agentProposalId } = do + proposal <- fetch agentProposalId + receivingAgentId <- param @(Id AgentRegistration) "receivingAgentId" + scope <- param @Text "scope" + tokenBudget <- paramOrDefault @Int 1000 "tokenBudget" + delegatingAgentId <- case proposal.agentRegistrationId of + Just aid -> pure aid + Nothing -> respondAndExit =<< renderNotFound + + receivingAgent <- fetch receivingAgentId + + delegation <- newRecord @AgentDelegation + |> set #delegatingAgentId delegatingAgentId + |> set #receivingAgentId receivingAgentId + |> set #parentProposalId (Just agentProposalId) + |> set #scope scope + |> set #tokenBudget tokenBudget + |> set #status "pending" + |> createRecord + + result <- liftIO $ callBridge BridgeRequest + { provider = receivingAgent.provider + , model = receivingAgent.modelName + , systemPrompt = receivingAgent.systemPrompt + , prompt = scope + , maxTokens = tokenBudget + , temperature = 0.7 + } + + now <- getCurrentTime + case result of + Left err -> do + delegation + |> set #status "failed" + |> set #result (Just . A.toJSON $ A.object ["error" A..= err.errorMessage]) + |> set #completedAt (Just now) + |> updateRecord + setErrorMessage ("Delegation failed: " <> err.errorMessage) + Right resp -> do + delegation + |> set #status "completed" + |> set #tokensUsed (Just resp.tokensOut) + |> set #result (Just . A.toJSON $ A.object ["content" A..= resp.content]) + |> set #completedAt (Just now) + |> updateRecord + setSuccessMessage "Subtask delegated successfully" + + redirectTo ShowAgentDelegationAction { agentDelegationId = delegation.id } diff --git a/Web/Controller/AgentRegistrations.hs b/Web/Controller/AgentRegistrations.hs new file mode 100644 index 0000000..af94e3a --- /dev/null +++ b/Web/Controller/AgentRegistrations.hs @@ -0,0 +1,122 @@ +module Web.Controller.AgentRegistrations where + +-- IHF Phase 11 — Advanced AI Federation (IHUB-WP-0012 T03) + +import Web.Controller.Prelude +import Web.View.AgentRegistrations.Index +import Web.View.AgentRegistrations.Show +import Web.View.AgentRegistrations.New +import Web.View.AgentRegistrations.Edit +import Web.View.AgentRegistrations.Performance + +instance Controller AgentRegistrationsController where + + action AgentRegistrationsAction = do + agents <- query @AgentRegistration + |> orderByAsc #name + |> fetch + hubs <- query @Hub |> orderByAsc #name |> fetch + render IndexView { .. } + + action ShowAgentRegistrationAction { agentRegistrationId } = do + agent <- fetch agentRegistrationId + policies <- query @ModelRoutingPolicy + |> filterWhere (#agentRegistrationId, agentRegistrationId) + |> orderByAsc #taskType + |> fetch + recentProposals <- query @AgentProposal + |> filterWhere (#agentRegistrationId, Just agentRegistrationId) + |> orderByDesc #createdAt + |> limit 10 + |> fetch + mPerformance <- query @AgentPerformanceRecord + |> filterWhere (#agentRegistrationId, agentRegistrationId) + |> orderByDesc #computedAt + |> limit 1 + |> fetchOneOrNothing + render ShowView { .. } + + action NewAgentRegistrationAction = do + let agent = newRecord @AgentRegistration + hubs <- query @Hub |> orderByAsc #name |> fetch + render NewView { .. } + + action CreateAgentRegistrationAction = do + let agent = newRecord @AgentRegistration + agent + |> fill @'["hubId","name","slug","description","provider","modelName","trustLevel","systemPrompt"] + |> set #capabilities (A.Array mempty) + |> validateField #name nonEmpty + |> validateField #slug nonEmpty + |> validateField #provider (isInList ["openrouter","gemini","openai","claude-code"]) + |> validateField #modelName nonEmpty + |> validateField #trustLevel (isInList ["advisory","elevated","autonomous"]) + |> ifValid \case + Left agent -> do + hubs <- query @Hub |> orderByAsc #name |> fetch + render NewView { .. } + Right agent -> do + agent <- createRecord agent + setSuccessMessage "Agent registered" + redirectTo AgentRegistrationsAction + + action EditAgentRegistrationAction { agentRegistrationId } = do + agent <- fetch agentRegistrationId + hubs <- query @Hub |> orderByAsc #name |> fetch + render EditView { .. } + + action UpdateAgentRegistrationAction { agentRegistrationId } = do + agent <- fetch agentRegistrationId + agent + |> fill @'["name","description","provider","modelName","trustLevel","systemPrompt"] + |> validateField #name nonEmpty + |> validateField #provider (isInList ["openrouter","gemini","openai","claude-code"]) + |> validateField #modelName nonEmpty + |> validateField #trustLevel (isInList ["advisory","elevated","autonomous"]) + |> ifValid \case + Left agent -> do + hubs <- query @Hub |> orderByAsc #name |> fetch + render EditView { .. } + Right agent -> do + updateRecord agent + setSuccessMessage "Agent updated" + redirectTo (ShowAgentRegistrationAction agentRegistrationId) + + action DeactivateAgentAction { agentRegistrationId } = do + agent <- fetch agentRegistrationId + agent |> set #isActive False |> updateRecord + setSuccessMessage "Agent deactivated" + redirectTo (ShowAgentRegistrationAction agentRegistrationId) + + action ComputeAgentPerformanceAction { agentRegistrationId } = do + agent <- fetch agentRegistrationId + rows <- sqlQuery + "SELECT \ + \ COUNT(*) FILTER (WHERE ap.status = 'accepted')::int AS accepted, \ + \ COUNT(*) FILTER (WHERE ap.status = 'rejected')::int AS rejected, \ + \ COUNT(*) FILTER (WHERE ap.status NOT IN ('accepted','rejected'))::int AS other, \ + \ COUNT(*)::int AS total, \ + \ AVG(ca.score) AS mean_confidence \ + \ FROM agent_proposals ap \ + \ LEFT JOIN confidence_annotations ca ON ca.proposal_id = ap.id \ + \ WHERE ap.agent_registration_id = ? \ + \ AND ap.created_at >= NOW() - INTERVAL '30 days'" + [PersistUUID (toUUID agentRegistrationId)] + case rows of + [(accepted, rejected, _other, total, mConf)] -> do + now <- getCurrentTime + let periodStart = addUTCTime (negate $ 30 * 86400) now + newRecord @AgentPerformanceRecord + |> set #agentRegistrationId agentRegistrationId + |> set #hubId agent.hubId + |> set #periodStart periodStart + |> set #periodEnd now + |> set #proposalsGenerated total + |> set #proposalsAccepted accepted + |> set #proposalsRejected rejected + |> set #proposalsRevised 0 + |> set #meanConfidence mConf + |> createRecord + setSuccessMessage "Performance snapshot computed" + _ -> setErrorMessage "Could not compute performance metrics" + redirectTo (ShowAgentRegistrationAction agentRegistrationId) diff --git a/Web/Controller/AiGovernancePolicies.hs b/Web/Controller/AiGovernancePolicies.hs new file mode 100644 index 0000000..c5136b6 --- /dev/null +++ b/Web/Controller/AiGovernancePolicies.hs @@ -0,0 +1,65 @@ +module Web.Controller.AiGovernancePolicies where + +-- IHF Phase 11 — Advanced AI Federation (IHUB-WP-0012 T08) + +import Web.Controller.Prelude +import Web.View.AiGovernancePolicies.Index +import Web.View.AiGovernancePolicies.New +import Application.Helper.AgentBridge (jsonArrayTexts) + +validAllowedActions :: [Text] +validAllowedActions = ["read", "propose", "delegate", "auto_apply"] + +validateAllowedActions :: Value -> ValidatorResult +validateAllowedActions val = + let actions = jsonArrayTexts val + invalid = filter (`notElem` validAllowedActions) actions + in if null invalid + then Success + else Failure ("Invalid actions: " <> intercalate ", " invalid) + +instance Controller AiGovernancePoliciesController where + + action AiGovernancePoliciesAction = do + policies <- query @AiGovernancePolicy + |> orderByAsc #artifactType + |> fetch + hubs <- query @Hub |> orderByAsc #name |> fetch + agents <- query @AgentRegistration |> orderByAsc #name |> fetch + render IndexView { .. } + + action NewAiGovernancePolicyAction = do + let policy = newRecord @AiGovernancePolicy + |> set #allowedActions (A.toJSON ["read" :: Text]) + hubs <- query @Hub |> orderByAsc #name |> fetch + agents <- query @AgentRegistration + |> filterWhere (#isActive, True) + |> orderByAsc #name + |> fetch + render NewView { .. } + + action CreateAiGovernancePolicyAction = do + -- Collect allowed_actions from checkbox params + selectedActions <- paramList @Text "allowedActions" + let actionsJson = A.toJSON selectedActions + let policy = newRecord @AiGovernancePolicy + |> set #allowedActions actionsJson + policy + |> fill @'["hubId","agentRegistrationId","artifactType"] + |> validateField #artifactType nonEmpty + |> ifValid \case + Left policy -> do + hubs <- query @Hub |> orderByAsc #name |> fetch + agents <- query @AgentRegistration |> filterWhere (#isActive, True) |> fetch + render NewView { .. } + Right policy -> do + createRecord policy + setSuccessMessage "Governance policy created" + redirectTo AiGovernancePoliciesAction + + action ToggleAiGovernancePolicyAction { aiGovernancePolicyId } = do + policy <- fetch aiGovernancePolicyId + policy |> set #isActive (not policy.isActive) |> updateRecord + let msg = if policy.isActive then "Policy deactivated" else "Policy activated" + setSuccessMessage msg + redirectTo AiGovernancePoliciesAction diff --git a/Web/Controller/CollectiveProposals.hs b/Web/Controller/CollectiveProposals.hs new file mode 100644 index 0000000..591759c --- /dev/null +++ b/Web/Controller/CollectiveProposals.hs @@ -0,0 +1,103 @@ +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" diff --git a/Web/Controller/DecisionRecords.hs b/Web/Controller/DecisionRecords.hs index a38d703..a26f429 100644 --- a/Web/Controller/DecisionRecords.hs +++ b/Web/Controller/DecisionRecords.hs @@ -8,7 +8,8 @@ import Web.View.DecisionRecords.Edit import Generated.Types import IHP.Prelude import IHP.ControllerPrelude -import Application.Helper.Controller (callClaudeApi) +import Application.Helper.AgentBridge (callAgent, checkGovernancePolicy) +import Application.Helper.ModelRouter (resolveAgent) import Data.List (intercalate) validOutcomes :: [Text] @@ -178,7 +179,7 @@ instance Controller DecisionRecordsController where setSuccessMessage "Implementation reference removed" redirectTo ShowDecisionRecordAction { decisionRecordId } - -- T07: Propose implementation paths via Claude API + -- T07 / Phase 11: Propose implementation paths via routed agent action ProposeImplementationAction { decisionRecordId } = do record <- fetch decisionRecordId implRefs <- query @ImplementationChangeReference @@ -187,6 +188,10 @@ instance Controller DecisionRecordsController where mRequirement <- case record.requirementId of Nothing -> pure Nothing Just rid -> fetchOneOrNothing rid + -- Resolve hub from the source widget via requirement candidate + mHubId <- case mRequirement >>= (.sourceWidgetId) of + Nothing -> pure Nothing + Just wid -> fmap (.hubId) <$> fetchOneOrNothing @Widget wid let implLines = map (\r -> r.system <> ": " <> r.workItemRef) implRefs reqDesc = maybe "" (.description) mRequirement userMsg = "Decision: " <> record.title @@ -194,21 +199,46 @@ instance Controller DecisionRecordsController where <> "\nOutcome: " <> record.outcome <> "\nRequirement: " <> reqDesc <> "\nExisting impl refs: " <> intercalate ", " implLines - result <- liftIO $ callClaudeApi - "You are a traceability-aware implementation analyst. Propose 1\x20133 concrete implementation paths for this decision. Each path should include a work_item_ref (e.g. PROJ-123), a system (github|linear|jira), and a rationale. Respond with JSON: {\"proposals\": [{\"work_item_ref\": \"...\", \"system\": \"...\", \"rationale\": \"...\"}]}." - userMsg - 600 - case result of - Left err -> do - setErrorMessage ("Implementation proposal failed: " <> err) - redirectTo ShowDecisionRecordAction { decisionRecordId } - Right content -> do - newRecord @AgentProposal - |> set #proposalType "impl_proposal" - |> set #sourceDecisionId (Just decisionRecordId) - |> set #content content - |> set #modelRef "claude-sonnet-4-6" - |> set #status "pending" - |> createRecord - setSuccessMessage "Implementation proposal created" + case mHubId of + Nothing -> do + setErrorMessage "Cannot determine hub for routing — ensure the decision has a linked requirement with a source widget" redirectTo ShowDecisionRecordAction { decisionRecordId } + Just hubId -> do + mAgent <- resolveAgent hubId "implementation" + case mAgent of + Nothing -> do + setErrorMessage "No routing policy for 'implementation' task type" + redirectTo ShowDecisionRecordAction { decisionRecordId } + Just agent -> do + allowed <- checkGovernancePolicy hubId agent.id "decision_record" + if not allowed + then do + newRecord @AgentProposal + |> set #proposalType "impl_proposal" + |> set #sourceDecisionId (Just decisionRecordId) + |> set #content "Blocked by AI governance policy" + |> set #modelRef agent.modelName + |> set #status "blocked_by_policy" + |> set #agentRegistrationId (Just agent.id) + |> createRecord + setErrorMessage "Blocked by AI governance policy" + redirectTo ShowDecisionRecordAction { decisionRecordId } + else do + result <- liftIO $ callAgent agent userMsg + case result of + Left err -> do + setErrorMessage ("Implementation proposal failed: " <> err.errorMessage) + redirectTo ShowDecisionRecordAction { decisionRecordId } + Right resp -> do + newRecord @AgentProposal + |> set #proposalType "impl_proposal" + |> set #sourceDecisionId (Just decisionRecordId) + |> set #content resp.content + |> set #modelRef resp.modelUsed + |> set #status "pending" + |> set #agentRegistrationId (Just agent.id) + |> set #tokensIn (Just resp.tokensIn) + |> set #tokensOut (Just resp.tokensOut) + |> createRecord + setSuccessMessage "Implementation proposal created" + redirectTo ShowDecisionRecordAction { decisionRecordId } diff --git a/Web/Controller/ModelRoutingPolicies.hs b/Web/Controller/ModelRoutingPolicies.hs new file mode 100644 index 0000000..4cbc513 --- /dev/null +++ b/Web/Controller/ModelRoutingPolicies.hs @@ -0,0 +1,47 @@ +module Web.Controller.ModelRoutingPolicies where + +-- IHF Phase 11 — Advanced AI Federation (IHUB-WP-0012 T04) + +import Web.Controller.Prelude +import Web.View.ModelRoutingPolicies.Index +import Web.View.ModelRoutingPolicies.New + +instance Controller ModelRoutingPoliciesController where + + action ModelRoutingPoliciesAction = do + policies <- query @ModelRoutingPolicy + |> orderByAsc #taskType + |> fetch + hubs <- query @Hub |> orderByAsc #name |> fetch + agents <- query @AgentRegistration |> orderByAsc #name |> fetch + render IndexView { .. } + + action NewModelRoutingPolicyAction = do + let policy = newRecord @ModelRoutingPolicy + hubs <- query @Hub |> orderByAsc #name |> fetch + agents <- query @AgentRegistration + |> filterWhere (#isActive, True) + |> orderByAsc #name + |> fetch + render NewView { .. } + + action CreateModelRoutingPolicyAction = do + let policy = newRecord @ModelRoutingPolicy + policy + |> fill @'["hubId","taskType","agentRegistrationId","priority"] + |> validateField #taskType nonEmpty + |> ifValid \case + Left policy -> do + hubs <- query @Hub |> orderByAsc #name |> fetch + agents <- query @AgentRegistration |> filterWhere (#isActive, True) |> fetch + render NewView { .. } + Right policy -> do + createRecord policy + setSuccessMessage "Routing policy created" + redirectTo ModelRoutingPoliciesAction + + action DeleteModelRoutingPolicyAction { modelRoutingPolicyId } = do + policy <- fetch modelRoutingPolicyId + deleteRecord policy + setSuccessMessage "Routing policy deleted" + redirectTo ModelRoutingPoliciesAction diff --git a/Web/Controller/RequirementCandidates.hs b/Web/Controller/RequirementCandidates.hs index 2b73111..0996cf1 100644 --- a/Web/Controller/RequirementCandidates.hs +++ b/Web/Controller/RequirementCandidates.hs @@ -8,7 +8,8 @@ import Web.View.RequirementCandidates.Edit import Generated.Types import IHP.Prelude import IHP.ControllerPrelude -import Application.Helper.Controller (callClaudeApi) +import Application.Helper.AgentBridge (callAgent, checkGovernancePolicy) +import Application.Helper.ModelRouter (resolveAgent) import Data.List (intercalate) import Data.Aeson (decode, Value(..), Array) import Data.Aeson.Lens (key, _String) @@ -257,33 +258,61 @@ instance Controller RequirementCandidatesController where setSuccessMessage "Decision record created" redirectTo ShowDecisionRecordAction { decisionRecordId = dr.id } - -- T05: Detect duplicate candidates via Claude API + -- T05 / Phase 11: Detect duplicate candidates via routed agent action DetectDuplicatesAction { requirementCandidateId } = do target <- fetch requirementCandidateId - others <- query @RequirementCandidate - |> fetch + others <- query @RequirementCandidate |> fetch + -- Resolve hub from the source widget + mHubId <- case target.sourceWidgetId of + Nothing -> pure Nothing + Just wid -> fmap (.hubId) <$> fetchOneOrNothing @Widget wid let otherLines = map (\c -> show c.id <> " | " <> c.title <> ": " <> c.description) (filter (\c -> c.id /= requirementCandidateId) others) targetLine = "TARGET: " <> target.title <> ": " <> target.description userMsg = targetLine <> "\n\nEXISTING:\n" <> intercalate "\n" otherLines - result <- liftIO $ callClaudeApi - "You are a deduplication assistant. Given a target candidate and a list of existing candidates, identify likely duplicates. Respond with JSON: {\"duplicates\": [{\"id\": \"uuid\", \"reason\": \"...\"}]}." - userMsg - 500 - case result of - Left err -> do - setErrorMessage ("Duplicate detection failed: " <> err) - redirectTo ShowRequirementCandidateAction { requirementCandidateId } - Right content -> do - newRecord @AgentProposal - |> set #proposalType "duplicate_flag" - |> set #sourceCandidateId (Just requirementCandidateId) - |> set #content content - |> set #modelRef "claude-sonnet-4-6" - |> set #status "pending" - |> createRecord - setSuccessMessage "Duplicate detection proposal created" + case mHubId of + Nothing -> do + setErrorMessage "Cannot determine hub for routing — ensure the candidate has a source widget" redirectTo ShowRequirementCandidateAction { requirementCandidateId } + Just hubId -> do + mAgent <- resolveAgent hubId "triage" + case mAgent of + Nothing -> do + setErrorMessage "No routing policy for 'triage' task type" + redirectTo ShowRequirementCandidateAction { requirementCandidateId } + Just agent -> do + allowed <- checkGovernancePolicy hubId agent.id "requirement_candidate" + if not allowed + then do + newRecord @AgentProposal + |> set #proposalType "duplicate_flag" + |> set #sourceCandidateId (Just requirementCandidateId) + |> set #content "Blocked by AI governance policy" + |> set #modelRef agent.modelName + |> set #status "blocked_by_policy" + |> set #agentRegistrationId (Just agent.id) + |> createRecord + setErrorMessage "Blocked by AI governance policy" + redirectTo ShowRequirementCandidateAction { requirementCandidateId } + else do + result <- liftIO $ callAgent agent userMsg + case result of + Left err -> do + setErrorMessage ("Duplicate detection failed: " <> err.errorMessage) + redirectTo ShowRequirementCandidateAction { requirementCandidateId } + Right resp -> do + newRecord @AgentProposal + |> set #proposalType "duplicate_flag" + |> set #sourceCandidateId (Just requirementCandidateId) + |> set #content resp.content + |> set #modelRef resp.modelUsed + |> set #status "pending" + |> set #agentRegistrationId (Just agent.id) + |> set #tokensIn (Just resp.tokensIn) + |> set #tokensOut (Just resp.tokensOut) + |> createRecord + setSuccessMessage "Duplicate detection proposal created" + redirectTo ShowRequirementCandidateAction { requirementCandidateId } -- T06: Detect policy sensitivity via Claude API action DetectPolicySensitivityAction { requirementCandidateId } = do diff --git a/Web/Controller/Widgets.hs b/Web/Controller/Widgets.hs index 2da19c9..cab7720 100644 --- a/Web/Controller/Widgets.hs +++ b/Web/Controller/Widgets.hs @@ -9,8 +9,10 @@ import Generated.Types import IHP.Prelude import IHP.ControllerPrelude import Data.Aeson (toJSON, object, (.=)) -import Application.Helper.Controller (isInRegression, widgetCycleCounts, callClaudeApi) +import Application.Helper.Controller (isInRegression, widgetCycleCounts) import Application.Helper.TypeRegistry (validateWidgetType, validatePolicyScope, activeWidgetTypes, activePolicyScopes) +import Application.Helper.AgentBridge (callAgent, checkGovernancePolicy) +import Application.Helper.ModelRouter (resolveAgent) import Data.List (intercalate) instance Controller WidgetsController where @@ -168,8 +170,9 @@ instance Controller WidgetsController where setSuccessMessage "Widget updated" redirectTo ShowWidgetAction { widgetId = widget.id } - -- T03: Summarize feedback cluster via Claude API + -- T03 / Phase 11: Summarize feedback cluster via routed agent action SummarizeClusterAction { widgetId } = do + widget <- fetch widgetId annotations <- query @Annotation |> filterWhere (#widgetId, widgetId) |> orderByDesc #createdAt @@ -183,27 +186,48 @@ instance Controller WidgetsController where let annLines = map (\a -> "[" <> a.category <> "/" <> a.severity <> "] " <> a.body) annotations threadLines = map (\t -> "[thread] " <> t.title <> ": " <> fromMaybe "" t.description) threads userMsg = intercalate "\n" (annLines <> threadLines) - result <- liftIO $ callClaudeApi - "You are a distillation assistant for a governed interaction hub. Summarize the following user feedback cluster into a concise, actionable summary (2\x20134 sentences). Be factual and neutral." - userMsg - 300 - case result of - Left err -> do - setErrorMessage ("AI summarization failed: " <> err) - redirectTo ShowWidgetAction { widgetId } - Right content -> do - newRecord @AgentProposal - |> set #proposalType "summary" - |> set #sourceWidgetId (Just widgetId) - |> set #content content - |> set #modelRef "claude-sonnet-4-6" - |> set #status "pending" - |> createRecord - setSuccessMessage "Summary proposal created" + mAgent <- resolveAgent widget.hubId "synthesis" + case mAgent of + Nothing -> do + setErrorMessage "No routing policy for 'synthesis' task type — configure one in Model Routing Policies" redirectTo ShowWidgetAction { widgetId } + Just agent -> do + allowed <- checkGovernancePolicy widget.hubId agent.id "annotation" + if not allowed + then do + newRecord @AgentProposal + |> set #proposalType "summary" + |> set #sourceWidgetId (Just widgetId) + |> set #content "Blocked by AI governance policy" + |> set #modelRef agent.modelName + |> set #status "blocked_by_policy" + |> set #agentRegistrationId (Just agent.id) + |> createRecord + setErrorMessage "Blocked by AI governance policy" + redirectTo ShowWidgetAction { widgetId } + else do + result <- liftIO $ callAgent agent userMsg + case result of + Left err -> do + setErrorMessage ("AI summarization failed: " <> err.errorMessage) + redirectTo ShowWidgetAction { widgetId } + Right resp -> do + newRecord @AgentProposal + |> set #proposalType "summary" + |> set #sourceWidgetId (Just widgetId) + |> set #content resp.content + |> set #modelRef resp.modelUsed + |> set #status "pending" + |> set #agentRegistrationId (Just agent.id) + |> set #tokensIn (Just resp.tokensIn) + |> set #tokensOut (Just resp.tokensOut) + |> createRecord + setSuccessMessage "Summary proposal created" + redirectTo ShowWidgetAction { widgetId } - -- T04: Draft a requirement candidate via Claude API + -- T04 / Phase 11: Draft a requirement candidate via routed agent action DraftRequirementAction { widgetId } = do + widget <- fetch widgetId annotations <- query @Annotation |> filterWhere (#widgetId, widgetId) |> orderByDesc #createdAt @@ -211,21 +235,41 @@ instance Controller WidgetsController where |> fetch let annLines = map (\a -> "[" <> a.category <> "/" <> a.severity <> "] " <> a.body) annotations userMsg = intercalate "\n" annLines - result <- liftIO $ callClaudeApi - "You are a requirements analyst. Given these friction annotations, draft a single structured requirement candidate. Respond with JSON: {\"title\": \"...\", \"description\": \"...\"}." - userMsg - 400 - case result of - Left err -> do - setErrorMessage ("AI draft failed: " <> err) - redirectTo ShowWidgetAction { widgetId } - Right content -> do - newRecord @AgentProposal - |> set #proposalType "requirement_draft" - |> set #sourceWidgetId (Just widgetId) - |> set #content content - |> set #modelRef "claude-sonnet-4-6" - |> set #status "pending" - |> createRecord - setSuccessMessage "Requirement draft proposal created" + mAgent <- resolveAgent widget.hubId "requirement_draft" + case mAgent of + Nothing -> do + setErrorMessage "No routing policy for 'requirement_draft' task type" redirectTo ShowWidgetAction { widgetId } + Just agent -> do + allowed <- checkGovernancePolicy widget.hubId agent.id "requirement_candidate" + if not allowed + then do + newRecord @AgentProposal + |> set #proposalType "requirement_draft" + |> set #sourceWidgetId (Just widgetId) + |> set #content "Blocked by AI governance policy" + |> set #modelRef agent.modelName + |> set #status "blocked_by_policy" + |> set #agentRegistrationId (Just agent.id) + |> createRecord + setErrorMessage "Blocked by AI governance policy" + redirectTo ShowWidgetAction { widgetId } + else do + result <- liftIO $ callAgent agent userMsg + case result of + Left err -> do + setErrorMessage ("AI draft failed: " <> err.errorMessage) + redirectTo ShowWidgetAction { widgetId } + Right resp -> do + newRecord @AgentProposal + |> set #proposalType "requirement_draft" + |> set #sourceWidgetId (Just widgetId) + |> set #content resp.content + |> set #modelRef resp.modelUsed + |> set #status "pending" + |> set #agentRegistrationId (Just agent.id) + |> set #tokensIn (Just resp.tokensIn) + |> set #tokensOut (Just resp.tokensOut) + |> createRecord + setSuccessMessage "Requirement draft proposal created" + redirectTo ShowWidgetAction { widgetId } diff --git a/Web/FrontController.hs b/Web/FrontController.hs index 848f931..6918115 100644 --- a/Web/FrontController.hs +++ b/Web/FrontController.hs @@ -55,6 +55,12 @@ import Web.Controller.GovernanceTemplates () import Web.Controller.MarketplaceDashboard () import Web.Controller.Api.V2.HubRegistry () import Web.Controller.Api.V2.WidgetPatterns () +-- Phase 11 — Advanced AI Federation (IHUB-WP-0012) +import Web.Controller.AgentRegistrations () +import Web.Controller.ModelRoutingPolicies () +import Web.Controller.AgentDelegations () +import Web.Controller.CollectiveProposals () +import Web.Controller.AiGovernancePolicies () import Web.Controller.Sessions () instance FrontController WebApplication where @@ -107,6 +113,12 @@ instance FrontController WebApplication where , parseRoute @MarketplaceDashboardController , parseRoute @ApiV2HubRegistryController , parseRoute @ApiV2WidgetPatternsController + -- Phase 11 — Advanced AI Federation (IHUB-WP-0012) + , parseRoute @AgentRegistrationsController + , parseRoute @ModelRoutingPoliciesController + , parseRoute @AgentDelegationsController + , parseRoute @CollectiveProposalsController + , parseRoute @AiGovernancePoliciesController ] instance InitControllerContext WebApplication where @@ -157,6 +169,10 @@ defaultLayout inner = [hsx| API Dashboard Hub Registry Marketplace + Agents + Routing + Collective + AI Gov diff --git a/Web/Routes.hs b/Web/Routes.hs index efcdab1..73177c1 100644 --- a/Web/Routes.hs +++ b/Web/Routes.hs @@ -261,5 +261,12 @@ instance HasPath ApiV2WidgetPatternsController where pathTo ApiV2ShowWidgetPatternAction { widgetPatternId } = "/api/v2/widget-patterns/" <> show widgetPatternId pathTo ApiV2AdoptWidgetPatternAction { widgetPatternId } = "/api/v2/widget-patterns/" <> show widgetPatternId <> "/adopt" +-- Phase 11 — Advanced AI Federation (IHUB-WP-0012) +instance AutoRoute AgentRegistrationsController +instance AutoRoute ModelRoutingPoliciesController +instance AutoRoute AgentDelegationsController +instance AutoRoute CollectiveProposalsController +instance AutoRoute AiGovernancePoliciesController + -- Sessions instance AutoRoute SessionsController diff --git a/Web/Types.hs b/Web/Types.hs index f7492fa..82a01a1 100644 --- a/Web/Types.hs +++ b/Web/Types.hs @@ -385,6 +385,45 @@ data ApiV2WidgetPatternsController | ApiV2AdoptWidgetPatternAction { widgetPatternId :: !(Id WidgetPattern) } deriving (Eq, Show, Data) +-- Phase 11 — Advanced AI Federation + +data AgentRegistrationsController + = AgentRegistrationsAction + | ShowAgentRegistrationAction { agentRegistrationId :: !(Id AgentRegistration) } + | NewAgentRegistrationAction + | CreateAgentRegistrationAction + | EditAgentRegistrationAction { agentRegistrationId :: !(Id AgentRegistration) } + | UpdateAgentRegistrationAction { agentRegistrationId :: !(Id AgentRegistration) } + | DeactivateAgentAction { agentRegistrationId :: !(Id AgentRegistration) } + | ComputeAgentPerformanceAction { agentRegistrationId :: !(Id AgentRegistration) } + deriving (Eq, Show, Data) + +data ModelRoutingPoliciesController + = ModelRoutingPoliciesAction + | NewModelRoutingPolicyAction + | CreateModelRoutingPolicyAction + | DeleteModelRoutingPolicyAction { modelRoutingPolicyId :: !(Id ModelRoutingPolicy) } + deriving (Eq, Show, Data) + +data AgentDelegationsController + = AgentDelegationsAction + | ShowAgentDelegationAction { agentDelegationId :: !(Id AgentDelegation) } + | DelegateSubtaskAction { agentProposalId :: !(Id AgentProposal) } + deriving (Eq, Show, Data) + +data CollectiveProposalsController + = CollectiveProposalsAction + | ShowCollectiveProposalAction { collectiveProposalId :: !(Id CollectiveProposal) } + | CreateCollectiveProposalAction + deriving (Eq, Show, Data) + +data AiGovernancePoliciesController + = AiGovernancePoliciesAction + | NewAiGovernancePolicyAction + | CreateAiGovernancePolicyAction + | ToggleAiGovernancePolicyAction { aiGovernancePolicyId :: !(Id AiGovernancePolicy) } + deriving (Eq, Show, Data) + data SessionsController = NewSessionAction | CreateSessionAction diff --git a/Web/View/AgentDelegations/Index.hs b/Web/View/AgentDelegations/Index.hs new file mode 100644 index 0000000..eac4a3c --- /dev/null +++ b/Web/View/AgentDelegations/Index.hs @@ -0,0 +1,50 @@ +module Web.View.AgentDelegations.Index where + +import Web.View.Prelude + +data IndexView = IndexView + { delegations :: ![AgentDelegation] } + +instance View IndexView where + html IndexView { .. } = [hsx| +
+

Agent Delegations

+
+ + + + + + + + + + + + {forEach delegations renderRow} + +
ScopeStatusToken Budget / UsedCreated
+
+
+ |] + where + renderRow d = [hsx| + + {d.scope} + {statusBadge d.status} + + {show d.tokenBudget} / {maybe "—" show d.tokensUsed} + + {timeAgo d.createdAt} + + View + + + |] + +statusBadge :: Text -> Html +statusBadge "completed" = [hsx|completed|] +statusBadge "failed" = [hsx|failed|] +statusBadge "cancelled" = [hsx|cancelled|] +statusBadge _ = [hsx|pending|] diff --git a/Web/View/AgentDelegations/Show.hs b/Web/View/AgentDelegations/Show.hs new file mode 100644 index 0000000..673935e --- /dev/null +++ b/Web/View/AgentDelegations/Show.hs @@ -0,0 +1,64 @@ +module Web.View.AgentDelegations.Show where + +import Web.View.Prelude +import Web.View.AgentDelegations.Index (statusBadge) + +data ShowView = ShowView + { delegation :: !AgentDelegation + , delegatingAgent :: !AgentRegistration + , receivingAgent :: !AgentRegistration + , mParentProposal :: !(Maybe AgentProposal) + } + +instance View ShowView where + html ShowView { .. } = [hsx| +
+
+

Delegation

+ {statusBadge delegation.status} +
+ +
+
+

Delegating Agent

+ {delegatingAgent.name} +
+
+

Receiving Agent

+ {receivingAgent.name} +
+
+

Scope

+

{delegation.scope}

+
+
+

Token Budget

+

{show delegation.tokenBudget}

+
+
+

Tokens Used

+

{maybe "—" show delegation.tokensUsed}

+
+
+ + {case mParentProposal of + Nothing -> mempty + Just p -> [hsx| +
+

Parent Proposal

+

{p.proposalType} — {p.status}

+
+ |]} + + {case delegation.result of + Nothing -> mempty + Just r -> [hsx| +
+

Result

+
{show r}
+
+ |]} +
+ |] diff --git a/Web/View/AgentRegistrations/Edit.hs b/Web/View/AgentRegistrations/Edit.hs new file mode 100644 index 0000000..71095bf --- /dev/null +++ b/Web/View/AgentRegistrations/Edit.hs @@ -0,0 +1,17 @@ +module Web.View.AgentRegistrations.Edit where + +import Web.View.Prelude +import Web.View.AgentRegistrations.New (renderForm) + +data EditView = EditView + { agent :: !AgentRegistration + , hubs :: ![Hub] + } + +instance View EditView where + html EditView { .. } = [hsx| +
+

Edit Agent: {agent.name}

+ {renderForm agent hubs} +
+ |] diff --git a/Web/View/AgentRegistrations/Index.hs b/Web/View/AgentRegistrations/Index.hs new file mode 100644 index 0000000..a2d5147 --- /dev/null +++ b/Web/View/AgentRegistrations/Index.hs @@ -0,0 +1,72 @@ +module Web.View.AgentRegistrations.Index where + +import Web.View.Prelude + +data IndexView = IndexView + { agents :: ![AgentRegistration] + , hubs :: ![Hub] + } + +instance View IndexView where + html IndexView { .. } = [hsx| +
+
+

Agent Registry

+ + Register Agent + +
+
+ + + + + + + + + + + + + + {forEach agents renderRow} + +
NameHubProviderModelTrustStatus
+
+
+ |] + where + hubName agentHubId = + case find (\h -> h.id == agentHubId) hubs of + Just h -> h.name + Nothing -> "Unknown" + + renderRow agent = [hsx| + + + {agent.name} + + {hubName agent.hubId} + + {agent.provider} + + {agent.modelName} + {trustBadge agent.trustLevel} + {statusBadge agent.isActive} + + Edit + + + |] + +trustBadge :: Text -> Html +trustBadge "autonomous" = [hsx|autonomous|] +trustBadge "elevated" = [hsx|elevated|] +trustBadge _ = [hsx|advisory|] + +statusBadge :: Bool -> Html +statusBadge True = [hsx|active|] +statusBadge False = [hsx|inactive|] diff --git a/Web/View/AgentRegistrations/New.hs b/Web/View/AgentRegistrations/New.hs new file mode 100644 index 0000000..ebfdc30 --- /dev/null +++ b/Web/View/AgentRegistrations/New.hs @@ -0,0 +1,56 @@ +module Web.View.AgentRegistrations.New where + +import Web.View.Prelude + +data NewView = NewView + { agent :: !AgentRegistration + , hubs :: ![Hub] + } + +instance View NewView where + html NewView { .. } = [hsx| +
+

Register Agent

+ {renderForm agent hubs} +
+ |] + +renderForm :: AgentRegistration -> [Hub] -> Html +renderForm agent hubs = formFor agent [hsx| +
+
+ {(textField #hubId) { label = "Hub", fieldClass = "block w-full border-gray-300 rounded-md shadow-sm" }} +
+
+
{(textField #name) { label = "Name" }}
+
{(textField #slug) { label = "Slug (unique identifier)" }}
+
+
{(textareaField #description) { label = "Description" }}
+
+
+ + +
+
{(textField #modelName) { label = "Model Name" }}
+
+
+ + +
+
{(textareaField #systemPrompt) { label = "System Prompt (optional)" }}
+
+ {submitButton { label = "Register Agent" }} + Cancel +
+
+|] diff --git a/Web/View/AgentRegistrations/Performance.hs b/Web/View/AgentRegistrations/Performance.hs new file mode 100644 index 0000000..9d2907a --- /dev/null +++ b/Web/View/AgentRegistrations/Performance.hs @@ -0,0 +1,7 @@ +module Web.View.AgentRegistrations.Performance where + +-- Performance view is rendered inline in Show.hs via performancePanel helper. +-- This module re-exports it for use if needed as a standalone view. + +import Web.View.Prelude +import Web.View.AgentRegistrations.Show (performancePanel) diff --git a/Web/View/AgentRegistrations/Show.hs b/Web/View/AgentRegistrations/Show.hs new file mode 100644 index 0000000..4657176 --- /dev/null +++ b/Web/View/AgentRegistrations/Show.hs @@ -0,0 +1,153 @@ +module Web.View.AgentRegistrations.Show where + +import Web.View.Prelude +import Web.View.AgentRegistrations.Index (trustBadge, statusBadge) + +data ShowView = ShowView + { agent :: !AgentRegistration + , policies :: ![ModelRoutingPolicy] + , recentProposals :: ![AgentProposal] + , mPerformance :: !(Maybe AgentPerformanceRecord) + } + +instance View ShowView where + html ShowView { .. } = [hsx| +
+
+
+

{agent.name}

+

{agent.slug}

+
+
+ {trustBadge agent.trustLevel} + {statusBadge agent.isActive} + Edit + {when agent.isActive [hsx| + Deactivate + |]} + Compute Performance +
+
+ +
+
+

Provider

+

{agent.provider}

+
+
+

Model

+

{agent.modelName}

+
+
+

Description

+

{fromMaybe "—" agent.description}

+
+
+ + {performancePanel mPerformance} + +
+

Routing Policies

+ {if null policies + then [hsx|

No routing policies. Add one.

|] + else policiesTable} +
+ +
+

Recent Proposals (last 10)

+ {if null recentProposals + then [hsx|

No proposals yet.

|] + else proposalsTable} +
+
+ |] + where + policiesTable = [hsx| +
+ + + + + + + + + + {forEach policies \p -> [hsx| + + + + + + |]} + +
Task TypePriorityActive
{p.taskType}{show p.priority}{statusBadge p.isActive}
+
+ |] + + proposalsTable = [hsx| +
+ + + + + + + + + + + {forEach recentProposals \p -> [hsx| + + + + + + + |]} + +
TypeStatusTokens In/OutCreated
{p.proposalType}{p.status} + {maybe "—" show p.tokensIn} / {maybe "—" show p.tokensOut} + {timeAgo p.createdAt}
+
+ |] + +performancePanel :: Maybe AgentPerformanceRecord -> Html +performancePanel Nothing = [hsx| +
+ No performance snapshot available. Click "Compute Performance" to generate one. +
+|] +performancePanel (Just p) = + let total = p.proposalsAccepted + p.proposalsRejected + acceptPct = if total > 0 then (100 * p.proposalsAccepted) `div` total else 0 + in [hsx| +
+

Performance (30-day snapshot)

+
+
+

{show p.proposalsGenerated}

+

Generated

+
+
+

{show p.proposalsAccepted}

+

Accepted

+
+
+

{show p.proposalsRejected}

+

Rejected

+
+
+

{show acceptPct}%

+

Acceptance rate

+
+
+ {case p.meanConfidence of + Nothing -> [hsx|

Mean confidence: —

|] + Just c -> [hsx|

Mean confidence: {printf "%.2f" c :: String}

|] + } +
+|] diff --git a/Web/View/AiGovernancePolicies/Index.hs b/Web/View/AiGovernancePolicies/Index.hs new file mode 100644 index 0000000..936270a --- /dev/null +++ b/Web/View/AiGovernancePolicies/Index.hs @@ -0,0 +1,63 @@ +module Web.View.AiGovernancePolicies.Index where + +import Web.View.Prelude + +data IndexView = IndexView + { policies :: ![AiGovernancePolicy] + , hubs :: ![Hub] + , agents :: ![AgentRegistration] + } + +instance View IndexView where + html IndexView { .. } = [hsx| +
+
+

AI Governance Policies

+ + Add Policy + +
+
+ + + + + + + + + + + + + {forEach policies renderRow} + +
HubAgentArtifact TypeAllowed ActionsActive
+
+
+ |] + where + hubName hid = maybe "Unknown" (.name) (find (\h -> h.id == hid) hubs) + agentName aid = maybe "Unknown" (.name) (find (\a -> a.id == aid) agents) + + renderRow p = [hsx| + + {hubName p.hubId} + {agentName p.agentRegistrationId} + {p.artifactType} + {show p.allowedActions} + + {if p.isActive + then [hsx|Active|] + else [hsx|Inactive|]} + + + + {if p.isActive then "Deactivate" :: Text else "Activate"} + + + + |] diff --git a/Web/View/AiGovernancePolicies/New.hs b/Web/View/AiGovernancePolicies/New.hs new file mode 100644 index 0000000..565290a --- /dev/null +++ b/Web/View/AiGovernancePolicies/New.hs @@ -0,0 +1,57 @@ +module Web.View.AiGovernancePolicies.New where + +import Web.View.Prelude + +data NewView = NewView + { policy :: !AiGovernancePolicy + , hubs :: ![Hub] + , agents :: ![AgentRegistration] + } + +allowedActionOptions :: [(Text, Text)] +allowedActionOptions = + [ ("read", "read — agent may read artifacts") + , ("propose", "propose — agent may create proposals") + , ("delegate", "delegate — agent may delegate to other agents") + , ("auto_apply", "auto_apply — agent may apply changes without human review") + ] + +instance View NewView where + html NewView { .. } = [hsx| +
+

Add AI Governance Policy

+ {formFor policy [hsx| +
+
+ + +
+
+ + +
+
{(textField #artifactType) { label = "Artifact Type", placeholder = "e.g. requirement_candidate, annotation, decision_record" }}
+
+ +
+ {forEach allowedActionOptions \(val, label) -> [hsx| + + |]} +
+
+
+ {submitButton { label = "Create Policy" }} + Cancel +
+
+ |]} +
+ |] diff --git a/Web/View/CollectiveProposals/Index.hs b/Web/View/CollectiveProposals/Index.hs new file mode 100644 index 0000000..b073d00 --- /dev/null +++ b/Web/View/CollectiveProposals/Index.hs @@ -0,0 +1,47 @@ +module Web.View.CollectiveProposals.Index where + +import Web.View.Prelude + +data IndexView = IndexView + { proposals :: ![CollectiveProposal] } + +instance View IndexView where + html IndexView { .. } = [hsx| +
+

Collective Proposals

+
+ + + + + + + + + + + + {forEach proposals renderRow} + +
TitleTask TypeConsensusCreated
+
+
+ |] + where + renderRow p = [hsx| + + {p.title} + {p.taskType} + {consensusBadge p.consensusStatus} + {timeAgo p.createdAt} + + View + + + |] + +consensusBadge :: Text -> Html +consensusBadge "consensus" = [hsx|consensus|] +consensusBadge "divergent" = [hsx|divergent|] +consensusBadge _ = [hsx|pending|] diff --git a/Web/View/CollectiveProposals/Show.hs b/Web/View/CollectiveProposals/Show.hs new file mode 100644 index 0000000..135afb5 --- /dev/null +++ b/Web/View/CollectiveProposals/Show.hs @@ -0,0 +1,58 @@ +module Web.View.CollectiveProposals.Show where + +import Web.View.Prelude +import Web.View.CollectiveProposals.Index (consensusBadge) + +data ShowView = ShowView + { proposal :: !CollectiveProposal + , agentContributions :: ![(CollectiveProposalContribution, Text)] + -- ^ (contribution, agent name) + } + +instance View ShowView where + html ShowView { .. } = [hsx| +
+
+
+

{proposal.title}

+

{proposal.taskType}

+
+ {consensusBadge proposal.consensusStatus} +
+ + {case proposal.summary of + Nothing -> mempty + Just s -> [hsx|

{s}

|]} + + {case proposal.finalContent of + Nothing -> mempty + Just fc -> [hsx| +
+

Synthesized Recommendation

+
{show fc}
+
+ |]} + +
+

+ Agent Contributions ({show (length agentContributions)}) +

+
+ {forEach agentContributions renderContrib} +
+
+
+ |] + where + renderContrib (contrib, agentName) = [hsx| +
+
+ {agentName} + + {maybe "" (\m -> "model: " <> m) contrib.modelUsed} + {maybe "" (\t -> " · " <> show t <> " tokens out") contrib.tokensOut} + +
+
{show contrib.content}
+
+ |] diff --git a/Web/View/ModelRoutingPolicies/Index.hs b/Web/View/ModelRoutingPolicies/Index.hs new file mode 100644 index 0000000..b8f47f0 --- /dev/null +++ b/Web/View/ModelRoutingPolicies/Index.hs @@ -0,0 +1,65 @@ +module Web.View.ModelRoutingPolicies.Index where + +import Web.View.Prelude + +data IndexView = IndexView + { policies :: ![ModelRoutingPolicy] + , hubs :: ![Hub] + , agents :: ![AgentRegistration] + } + +instance View IndexView where + html IndexView { .. } = [hsx| +
+
+

Model Routing Policies

+ + Add Policy + +
+
+ + + + + + + + + + + + + {forEach policies renderRow} + +
HubTask TypeAgentPriorityActive
+
+
+ |] + where + hubName hid = maybe "Unknown" (.name) (find (\h -> h.id == hid) hubs) + agentName aid = maybe "Unknown" (.name) (find (\a -> a.id == aid) agents) + + renderRow p = [hsx| + + {hubName p.hubId} + {p.taskType} + + {agentName p.agentRegistrationId} + + {show p.priority} + + {if p.isActive + then [hsx|Yes|] + else [hsx|No|]} + + + Delete + + + |] diff --git a/Web/View/ModelRoutingPolicies/New.hs b/Web/View/ModelRoutingPolicies/New.hs new file mode 100644 index 0000000..3bb9c96 --- /dev/null +++ b/Web/View/ModelRoutingPolicies/New.hs @@ -0,0 +1,55 @@ +module Web.View.ModelRoutingPolicies.New where + +import Web.View.Prelude + +data NewView = NewView + { policy :: !ModelRoutingPolicy + , hubs :: ![Hub] + , agents :: ![AgentRegistration] + } + +taskTypeOptions :: [Text] +taskTypeOptions = + [ "requirement_draft" + , "triage" + , "synthesis" + , "policy_check" + , "implementation" + ] + +instance View NewView where + html NewView { .. } = [hsx| +
+

Add Routing Policy

+ {formFor policy [hsx| +
+
+ + +
+
+ + +
+
+ + +
+
{(numberField #priority) { label = "Priority (higher wins)", placeholder = "0" }}
+
+ {submitButton { label = "Create Policy" }} + Cancel +
+
+ |]} +
+ |] diff --git a/scripts/llm_bridge.py b/scripts/llm_bridge.py new file mode 100755 index 0000000..1a11fd7 --- /dev/null +++ b/scripts/llm_bridge.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +""" +IHF llm-connect bridge — Phase 11 AI Federation (IHUB-WP-0012) + +Usage: + echo '{"provider":"openrouter","model":"...","prompt":"..."}' | python3 scripts/llm_bridge.py + +Input JSON fields: + provider — openrouter | gemini | openai | claude-code (default: openrouter) + model — model name string (provider-specific) + prompt — the user prompt + systemPrompt — optional system prompt + api_key — optional; falls back to llm-connect env-var resolution + maxTokens — max completion tokens (default: 2000) + temperature — sampling temperature (default: 0.7) + +Output JSON (stdout, exit 0 on success): + content — generated text + model — model name actually used + tokensIn — prompt token count + tokensOut — completion token count + finishReason — stop reason string + +Error JSON (stdout, exit 1 on LLMError): + error — error message + errorType — exception class name +""" +import sys +import json +import os + +sys.path.insert(0, os.path.expanduser("~/llm-connect")) + +from llm_connect import create_adapter, RunConfig +from llm_connect.exceptions import LLMError + + +def main() -> None: + req = json.load(sys.stdin) + + try: + adapter = create_adapter( + provider=req.get("provider", "openrouter"), + model=req.get("model"), + api_key=req.get("api_key"), + system_prompt=req.get("systemPrompt"), + ) + config = RunConfig( + model_name=req.get("model", ""), + temperature=req.get("temperature", 0.7), + max_tokens=req.get("maxTokens", 2000), + ) + resp = adapter.execute_prompt(req["prompt"], config) + print(json.dumps({ + "content": resp.content, + "model": resp.model, + "tokensIn": resp.usage.get("prompt_tokens", 0), + "tokensOut": resp.usage.get("completion_tokens", 0), + "finishReason": resp.finish_reason, + })) + except LLMError as e: + json.dump({"error": str(e), "errorType": type(e).__name__}, sys.stdout) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/workplans/IHUB-WP-0012-ihf-phase11-advanced-ai-federation.md b/workplans/IHUB-WP-0012-ihf-phase11-advanced-ai-federation.md index 2633c3a..4207ee5 100644 --- a/workplans/IHUB-WP-0012-ihf-phase11-advanced-ai-federation.md +++ b/workplans/IHUB-WP-0012-ihf-phase11-advanced-ai-federation.md @@ -4,7 +4,7 @@ type: workplan title: "IHF Phase 11 — Advanced AI Federation" domain: inter_hub repo: inter-hub -status: todo +status: done owner: custodian topic_slug: inter_hub created: "2026-04-01" @@ -225,7 +225,7 @@ ALTER TABLE agent_proposals ```task id: IHUB-WP-0012-T01 -status: todo +status: done priority: high state_hub_task_id: "d86574f0-f0a4-4217-b4de-d020442de7e4" ``` @@ -242,7 +242,7 @@ Includes the ALTER TABLE on `agent_proposals` to add `agent_registration_id`, ```task id: IHUB-WP-0012-T02 -status: todo +status: done priority: high state_hub_task_id: "404d1a89-aae2-49ea-a565-90261001a633" ``` @@ -361,7 +361,7 @@ callAgent agent prompt = ```task id: IHUB-WP-0012-T03 -status: todo +status: done priority: high state_hub_task_id: "3ae5b3b1-e644-444d-ae72-8eef07318c49" ``` @@ -392,7 +392,7 @@ Add nav link ("Agents") next to existing "Agent" proposals link. ```task id: IHUB-WP-0012-T04 -status: todo +status: done priority: high state_hub_task_id: "20bcff74-1923-4c11-95ef-2b37a7a10dd8" ``` @@ -439,7 +439,7 @@ resolveAgent hubId taskType = do ```task id: IHUB-WP-0012-T05 -status: todo +status: done priority: high state_hub_task_id: "45fec21a-47d3-4381-99bc-88dcf0f117cb" ``` @@ -488,7 +488,7 @@ queries `ai_governance_policies` for the hub + agent + artifact_type combination ```task id: IHUB-WP-0012-T06 -status: todo +status: done priority: medium state_hub_task_id: "9fe1c284-ecb7-4777-98f3-253224df704c" ``` @@ -512,7 +512,7 @@ Actions: ```task id: IHUB-WP-0012-T07 -status: todo +status: done priority: medium state_hub_task_id: "37782cec-2d42-44b8-8bda-2049a7bf4898" ``` @@ -546,7 +546,7 @@ detectConsensus contribs = ```task id: IHUB-WP-0012-T08 -status: todo +status: done priority: medium state_hub_task_id: "50d05787-9629-4f94-ac55-50a6e417f730" ``` @@ -587,7 +587,7 @@ count of blocked invocations in the last 7 days per hub. ```task id: IHUB-WP-0012-T09 -status: todo +status: done priority: medium state_hub_task_id: "6ef63612-f913-4a13-b683-f44929cb1b2d" ``` @@ -621,7 +621,7 @@ calibration indicator. ```task id: IHUB-WP-0012-T10 -status: todo +status: done priority: medium state_hub_task_id: "41a64c6c-0ce8-4c63-9175-6b8d28143f81" ```