feat(WP-0012): IHF Phase 11 — Advanced AI Federation
Some checks failed
Test / test (push) Has been cancelled

- 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>
This commit is contained in:
2026-04-01 20:57:17 +00:00
parent 4e4e994659
commit 133dae3d23
32 changed files with 1959 additions and 102 deletions

View File

@@ -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 (05) | 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 |

View File

@@ -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 _ = []

View File

@@ -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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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 110 complete (including GAAF Compliance Foundation and External API Surface). Phase 11 (Advanced AI Federation) is the active implementation target.
**Current state:** Phases 111 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)

View File

@@ -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 }

View File

@@ -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)

View File

@@ -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

View File

@@ -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"

View File

@@ -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 }

View File

@@ -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

View File

@@ -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

View File

@@ -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 }

View File

@@ -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|
<a href={ShowApiDashboardAction} class="text-sm text-gray-600 hover:text-gray-900">API Dashboard</a>
<a href={HubRegistryAction} class="text-sm text-gray-600 hover:text-gray-900">Hub Registry</a>
<a href={MarketplaceDashboardAction} class="text-sm text-gray-600 hover:text-gray-900">Marketplace</a>
<a href={AgentRegistrationsAction} class="text-sm text-gray-600 hover:text-gray-900">Agents</a>
<a href={ModelRoutingPoliciesAction} class="text-sm text-gray-600 hover:text-gray-900">Routing</a>
<a href={CollectiveProposalsAction} class="text-sm text-gray-600 hover:text-gray-900">Collective</a>
<a href={AiGovernancePoliciesAction} class="text-sm text-gray-600 hover:text-gray-900">AI Gov</a>
<div class="ml-auto">
<a href={DeleteSessionAction} class="text-sm text-gray-500 hover:text-gray-700">Sign out</a>
</div>

View File

@@ -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

View File

@@ -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

View File

@@ -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|
<div class="p-6">
<h1 class="text-2xl font-bold text-gray-900 mb-6">Agent Delegations</h1>
<div class="bg-white shadow rounded-lg overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Scope</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Token Budget / Used</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Created</th>
<th class="px-6 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{forEach delegations renderRow}
</tbody>
</table>
</div>
</div>
|]
where
renderRow d = [hsx|
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 text-sm text-gray-700">{d.scope}</td>
<td class="px-6 py-4">{statusBadge d.status}</td>
<td class="px-6 py-4 text-sm text-gray-500">
{show d.tokenBudget} / {maybe "" show d.tokensUsed}
</td>
<td class="px-6 py-4 text-sm text-gray-500">{timeAgo d.createdAt}</td>
<td class="px-6 py-4 text-right">
<a href={ShowAgentDelegationAction d.id}
class="text-sm text-blue-600 hover:text-blue-800">View</a>
</td>
</tr>
|]
statusBadge :: Text -> Html
statusBadge "completed" = [hsx|<span class="px-2 py-0.5 text-xs rounded-full bg-green-100 text-green-800">completed</span>|]
statusBadge "failed" = [hsx|<span class="px-2 py-0.5 text-xs rounded-full bg-red-100 text-red-800">failed</span>|]
statusBadge "cancelled" = [hsx|<span class="px-2 py-0.5 text-xs rounded-full bg-gray-100 text-gray-500">cancelled</span>|]
statusBadge _ = [hsx|<span class="px-2 py-0.5 text-xs rounded-full bg-yellow-100 text-yellow-800">pending</span>|]

View File

@@ -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|
<div class="p-6 space-y-6 max-w-3xl">
<div class="flex justify-between items-start">
<h1 class="text-2xl font-bold text-gray-900">Delegation</h1>
{statusBadge delegation.status}
</div>
<div class="bg-gray-50 rounded-lg p-4 grid grid-cols-2 gap-4">
<div>
<p class="text-xs text-gray-500">Delegating Agent</p>
<a href={ShowAgentRegistrationAction delegatingAgent.id}
class="text-sm font-medium text-blue-600 hover:text-blue-800">{delegatingAgent.name}</a>
</div>
<div>
<p class="text-xs text-gray-500">Receiving Agent</p>
<a href={ShowAgentRegistrationAction receivingAgent.id}
class="text-sm font-medium text-blue-600 hover:text-blue-800">{receivingAgent.name}</a>
</div>
<div class="col-span-2">
<p class="text-xs text-gray-500">Scope</p>
<p class="text-sm">{delegation.scope}</p>
</div>
<div>
<p class="text-xs text-gray-500">Token Budget</p>
<p class="text-sm">{show delegation.tokenBudget}</p>
</div>
<div>
<p class="text-xs text-gray-500">Tokens Used</p>
<p class="text-sm">{maybe "" show delegation.tokensUsed}</p>
</div>
</div>
{case mParentProposal of
Nothing -> mempty
Just p -> [hsx|
<div>
<p class="text-xs text-gray-500 mb-1">Parent Proposal</p>
<p class="text-sm font-mono text-gray-600">{p.proposalType} {p.status}</p>
</div>
|]}
{case delegation.result of
Nothing -> mempty
Just r -> [hsx|
<div>
<h2 class="text-lg font-semibold text-gray-800 mb-2">Result</h2>
<pre class="bg-gray-100 rounded p-4 text-sm overflow-auto">{show r}</pre>
</div>
|]}
</div>
|]

View File

@@ -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|
<div class="p-6 max-w-2xl">
<h1 class="text-2xl font-bold text-gray-900 mb-6">Edit Agent: {agent.name}</h1>
{renderForm agent hubs}
</div>
|]

View File

@@ -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|
<div class="p-6">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-900">Agent Registry</h1>
<a href={NewAgentRegistrationAction}
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium">
Register Agent
</a>
</div>
<div class="bg-white shadow rounded-lg overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Hub</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Provider</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Model</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Trust</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th class="px-6 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{forEach agents renderRow}
</tbody>
</table>
</div>
</div>
|]
where
hubName agentHubId =
case find (\h -> h.id == agentHubId) hubs of
Just h -> h.name
Nothing -> "Unknown"
renderRow agent = [hsx|
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 text-sm font-medium text-gray-900">
<a href={ShowAgentRegistrationAction agent.id} class="hover:text-blue-600">{agent.name}</a>
</td>
<td class="px-6 py-4 text-sm text-gray-500">{hubName agent.hubId}</td>
<td class="px-6 py-4 text-sm text-gray-500">
<span class="font-mono bg-gray-100 px-2 py-0.5 rounded text-xs">{agent.provider}</span>
</td>
<td class="px-6 py-4 text-sm text-gray-500 font-mono text-xs">{agent.modelName}</td>
<td class="px-6 py-4">{trustBadge agent.trustLevel}</td>
<td class="px-6 py-4">{statusBadge agent.isActive}</td>
<td class="px-6 py-4 text-right">
<a href={EditAgentRegistrationAction agent.id}
class="text-sm text-blue-600 hover:text-blue-800">Edit</a>
</td>
</tr>
|]
trustBadge :: Text -> Html
trustBadge "autonomous" = [hsx|<span class="px-2 py-0.5 text-xs rounded-full bg-red-100 text-red-800">autonomous</span>|]
trustBadge "elevated" = [hsx|<span class="px-2 py-0.5 text-xs rounded-full bg-yellow-100 text-yellow-800">elevated</span>|]
trustBadge _ = [hsx|<span class="px-2 py-0.5 text-xs rounded-full bg-gray-100 text-gray-700">advisory</span>|]
statusBadge :: Bool -> Html
statusBadge True = [hsx|<span class="px-2 py-0.5 text-xs rounded-full bg-green-100 text-green-800">active</span>|]
statusBadge False = [hsx|<span class="px-2 py-0.5 text-xs rounded-full bg-gray-100 text-gray-500">inactive</span>|]

View File

@@ -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|
<div class="p-6 max-w-2xl">
<h1 class="text-2xl font-bold text-gray-900 mb-6">Register Agent</h1>
{renderForm agent hubs}
</div>
|]
renderForm :: AgentRegistration -> [Hub] -> Html
renderForm agent hubs = formFor agent [hsx|
<div class="space-y-4">
<div>
{(textField #hubId) { label = "Hub", fieldClass = "block w-full border-gray-300 rounded-md shadow-sm" }}
</div>
<div class="grid grid-cols-2 gap-4">
<div>{(textField #name) { label = "Name" }}</div>
<div>{(textField #slug) { label = "Slug (unique identifier)" }}</div>
</div>
<div>{(textareaField #description) { label = "Description" }}</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Provider</label>
<select name="provider" class="block w-full border-gray-300 rounded-md shadow-sm">
<option value="openrouter">openrouter</option>
<option value="gemini">gemini</option>
<option value="openai">openai</option>
<option value="claude-code">claude-code</option>
</select>
</div>
<div>{(textField #modelName) { label = "Model Name" }}</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Trust Level</label>
<select name="trustLevel" class="block w-full border-gray-300 rounded-md shadow-sm">
<option value="advisory">advisory (default)</option>
<option value="elevated">elevated</option>
<option value="autonomous">autonomous</option>
</select>
</div>
<div>{(textareaField #systemPrompt) { label = "System Prompt (optional)" }}</div>
<div class="flex gap-3 pt-2">
{submitButton { label = "Register Agent" }}
<a href={AgentRegistrationsAction}
class="px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-md text-sm">Cancel</a>
</div>
</div>
|]

View File

@@ -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)

View File

@@ -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|
<div class="p-6 space-y-6">
<div class="flex justify-between items-start">
<div>
<h1 class="text-2xl font-bold text-gray-900">{agent.name}</h1>
<p class="text-sm text-gray-500 mt-1 font-mono">{agent.slug}</p>
</div>
<div class="flex gap-2">
{trustBadge agent.trustLevel}
{statusBadge agent.isActive}
<a href={EditAgentRegistrationAction agent.id}
class="px-3 py-1 text-sm bg-gray-100 hover:bg-gray-200 rounded">Edit</a>
{when agent.isActive [hsx|
<a href={DeactivateAgentAction agent.id}
class="px-3 py-1 text-sm bg-red-50 text-red-700 hover:bg-red-100 rounded">Deactivate</a>
|]}
<a href={ComputeAgentPerformanceAction agent.id}
class="px-3 py-1 text-sm bg-blue-50 text-blue-700 hover:bg-blue-100 rounded">Compute Performance</a>
</div>
</div>
<div class="grid grid-cols-2 gap-4 bg-gray-50 rounded-lg p-4">
<div>
<p class="text-xs text-gray-500">Provider</p>
<p class="font-mono text-sm">{agent.provider}</p>
</div>
<div>
<p class="text-xs text-gray-500">Model</p>
<p class="font-mono text-sm">{agent.modelName}</p>
</div>
<div class="col-span-2">
<p class="text-xs text-gray-500">Description</p>
<p class="text-sm">{fromMaybe "" agent.description}</p>
</div>
</div>
{performancePanel mPerformance}
<div>
<h2 class="text-lg font-semibold text-gray-800 mb-3">Routing Policies</h2>
{if null policies
then [hsx|<p class="text-sm text-gray-500">No routing policies. <a href={NewModelRoutingPolicyAction} class="text-blue-600">Add one</a>.</p>|]
else policiesTable}
</div>
<div>
<h2 class="text-lg font-semibold text-gray-800 mb-3">Recent Proposals (last 10)</h2>
{if null recentProposals
then [hsx|<p class="text-sm text-gray-500">No proposals yet.</p>|]
else proposalsTable}
</div>
</div>
|]
where
policiesTable = [hsx|
<div class="bg-white shadow rounded-lg overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Task Type</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Priority</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Active</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{forEach policies \p -> [hsx|
<tr>
<td class="px-4 py-3 text-sm font-mono">{p.taskType}</td>
<td class="px-4 py-3 text-sm">{show p.priority}</td>
<td class="px-4 py-3">{statusBadge p.isActive}</td>
</tr>
|]}
</tbody>
</table>
</div>
|]
proposalsTable = [hsx|
<div class="bg-white shadow rounded-lg overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Type</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Tokens In/Out</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Created</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{forEach recentProposals \p -> [hsx|
<tr>
<td class="px-4 py-3 text-sm font-mono">{p.proposalType}</td>
<td class="px-4 py-3 text-sm">{p.status}</td>
<td class="px-4 py-3 text-sm text-gray-500">
{maybe "" show p.tokensIn} / {maybe "" show p.tokensOut}
</td>
<td class="px-4 py-3 text-sm text-gray-500">{timeAgo p.createdAt}</td>
</tr>
|]}
</tbody>
</table>
</div>
|]
performancePanel :: Maybe AgentPerformanceRecord -> Html
performancePanel Nothing = [hsx|
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 text-sm text-yellow-800">
No performance snapshot available. Click "Compute Performance" to generate one.
</div>
|]
performancePanel (Just p) =
let total = p.proposalsAccepted + p.proposalsRejected
acceptPct = if total > 0 then (100 * p.proposalsAccepted) `div` total else 0
in [hsx|
<div class="bg-white shadow rounded-lg p-4">
<h2 class="text-lg font-semibold text-gray-800 mb-3">Performance (30-day snapshot)</h2>
<div class="grid grid-cols-4 gap-4">
<div class="text-center">
<p class="text-2xl font-bold text-gray-900">{show p.proposalsGenerated}</p>
<p class="text-xs text-gray-500">Generated</p>
</div>
<div class="text-center">
<p class="text-2xl font-bold text-green-600">{show p.proposalsAccepted}</p>
<p class="text-xs text-gray-500">Accepted</p>
</div>
<div class="text-center">
<p class="text-2xl font-bold text-red-500">{show p.proposalsRejected}</p>
<p class="text-xs text-gray-500">Rejected</p>
</div>
<div class="text-center">
<p class="text-2xl font-bold text-blue-600">{show acceptPct}%</p>
<p class="text-xs text-gray-500">Acceptance rate</p>
</div>
</div>
{case p.meanConfidence of
Nothing -> [hsx|<p class="mt-3 text-sm text-gray-400">Mean confidence: </p>|]
Just c -> [hsx|<p class="mt-3 text-sm text-gray-600">Mean confidence: {printf "%.2f" c :: String}</p>|]
}
</div>
|]

View File

@@ -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|
<div class="p-6">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-900">AI Governance Policies</h1>
<a href={NewAiGovernancePolicyAction}
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium">
Add Policy
</a>
</div>
<div class="bg-white shadow rounded-lg overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Hub</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Agent</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Artifact Type</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Allowed Actions</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Active</th>
<th class="px-6 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{forEach policies renderRow}
</tbody>
</table>
</div>
</div>
|]
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|
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 text-sm text-gray-700">{hubName p.hubId}</td>
<td class="px-6 py-4 text-sm text-gray-700">{agentName p.agentRegistrationId}</td>
<td class="px-6 py-4 text-sm font-mono">{p.artifactType}</td>
<td class="px-6 py-4 text-sm text-gray-600">{show p.allowedActions}</td>
<td class="px-6 py-4">
{if p.isActive
then [hsx|<span class="text-green-600 text-sm">Active</span>|]
else [hsx|<span class="text-gray-400 text-sm">Inactive</span>|]}
</td>
<td class="px-6 py-4 text-right">
<a href={ToggleAiGovernancePolicyAction p.id}
class="text-sm text-blue-600 hover:text-blue-800"
data-method="POST">
{if p.isActive then "Deactivate" :: Text else "Activate"}
</a>
</td>
</tr>
|]

View File

@@ -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|
<div class="p-6 max-w-xl">
<h1 class="text-2xl font-bold text-gray-900 mb-6">Add AI Governance Policy</h1>
{formFor policy [hsx|
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Hub</label>
<select name="hubId" class="block w-full border-gray-300 rounded-md shadow-sm text-sm">
{forEach hubs \h -> [hsx|<option value={show h.id}>{h.name}</option>|]}
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Agent</label>
<select name="agentRegistrationId" class="block w-full border-gray-300 rounded-md shadow-sm text-sm">
{forEach agents \a -> [hsx|<option value={show a.id}>{a.name}</option>|]}
</select>
</div>
<div>{(textField #artifactType) { label = "Artifact Type", placeholder = "e.g. requirement_candidate, annotation, decision_record" }}</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Allowed Actions</label>
<div class="space-y-2">
{forEach allowedActionOptions \(val, label) -> [hsx|
<label class="flex items-center gap-2 text-sm">
<input type="checkbox" name="allowedActions" value={val} class="rounded" />
<span>{label}</span>
</label>
|]}
</div>
</div>
<div class="flex gap-3 pt-2">
{submitButton { label = "Create Policy" }}
<a href={AiGovernancePoliciesAction}
class="px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-md text-sm">Cancel</a>
</div>
</div>
|]}
</div>
|]

View File

@@ -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|
<div class="p-6">
<h1 class="text-2xl font-bold text-gray-900 mb-6">Collective Proposals</h1>
<div class="bg-white shadow rounded-lg overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Title</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Task Type</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Consensus</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Created</th>
<th class="px-6 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{forEach proposals renderRow}
</tbody>
</table>
</div>
</div>
|]
where
renderRow p = [hsx|
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 text-sm font-medium text-gray-900">{p.title}</td>
<td class="px-6 py-4 text-sm font-mono text-gray-500">{p.taskType}</td>
<td class="px-6 py-4">{consensusBadge p.consensusStatus}</td>
<td class="px-6 py-4 text-sm text-gray-500">{timeAgo p.createdAt}</td>
<td class="px-6 py-4 text-right">
<a href={ShowCollectiveProposalAction p.id}
class="text-sm text-blue-600 hover:text-blue-800">View</a>
</td>
</tr>
|]
consensusBadge :: Text -> Html
consensusBadge "consensus" = [hsx|<span class="px-2 py-0.5 text-xs rounded-full bg-green-100 text-green-800">consensus</span>|]
consensusBadge "divergent" = [hsx|<span class="px-2 py-0.5 text-xs rounded-full bg-orange-100 text-orange-800">divergent</span>|]
consensusBadge _ = [hsx|<span class="px-2 py-0.5 text-xs rounded-full bg-gray-100 text-gray-500">pending</span>|]

View File

@@ -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|
<div class="p-6 space-y-6 max-w-4xl">
<div class="flex justify-between items-start">
<div>
<h1 class="text-2xl font-bold text-gray-900">{proposal.title}</h1>
<p class="text-sm font-mono text-gray-500 mt-1">{proposal.taskType}</p>
</div>
{consensusBadge proposal.consensusStatus}
</div>
{case proposal.summary of
Nothing -> mempty
Just s -> [hsx|<p class="text-gray-700">{s}</p>|]}
{case proposal.finalContent of
Nothing -> mempty
Just fc -> [hsx|
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
<h2 class="text-sm font-semibold text-green-800 mb-2">Synthesized Recommendation</h2>
<pre class="text-sm text-green-900 whitespace-pre-wrap">{show fc}</pre>
</div>
|]}
<div>
<h2 class="text-lg font-semibold text-gray-800 mb-3">
Agent Contributions ({show (length agentContributions)})
</h2>
<div class="grid gap-4">
{forEach agentContributions renderContrib}
</div>
</div>
</div>
|]
where
renderContrib (contrib, agentName) = [hsx|
<div class="bg-white shadow rounded-lg p-4">
<div class="flex justify-between items-center mb-2">
<span class="text-sm font-medium text-gray-800">{agentName}</span>
<span class="text-xs text-gray-400">
{maybe "" (\m -> "model: " <> m) contrib.modelUsed}
{maybe "" (\t -> " · " <> show t <> " tokens out") contrib.tokensOut}
</span>
</div>
<pre class="text-sm text-gray-700 whitespace-pre-wrap bg-gray-50 rounded p-3">{show contrib.content}</pre>
</div>
|]

View File

@@ -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|
<div class="p-6">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-900">Model Routing Policies</h1>
<a href={NewModelRoutingPolicyAction}
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium">
Add Policy
</a>
</div>
<div class="bg-white shadow rounded-lg overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Hub</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Task Type</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Agent</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Priority</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Active</th>
<th class="px-6 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{forEach policies renderRow}
</tbody>
</table>
</div>
</div>
|]
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|
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 text-sm text-gray-700">{hubName p.hubId}</td>
<td class="px-6 py-4 text-sm font-mono">{p.taskType}</td>
<td class="px-6 py-4 text-sm text-gray-700">
<a href={ShowAgentRegistrationAction p.agentRegistrationId}
class="hover:text-blue-600">{agentName p.agentRegistrationId}</a>
</td>
<td class="px-6 py-4 text-sm text-gray-500">{show p.priority}</td>
<td class="px-6 py-4 text-sm">
{if p.isActive
then [hsx|<span class="text-green-600">Yes</span>|]
else [hsx|<span class="text-gray-400">No</span>|]}
</td>
<td class="px-6 py-4 text-right">
<a href={DeleteModelRoutingPolicyAction p.id}
class="text-sm text-red-600 hover:text-red-800"
data-method="DELETE"
data-confirm="Delete this routing policy?">Delete</a>
</td>
</tr>
|]

View File

@@ -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|
<div class="p-6 max-w-xl">
<h1 class="text-2xl font-bold text-gray-900 mb-6">Add Routing Policy</h1>
{formFor policy [hsx|
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Hub</label>
<select name="hubId" class="block w-full border-gray-300 rounded-md shadow-sm text-sm">
{forEach hubs \h -> [hsx|<option value={show h.id}>{h.name}</option>|]}
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Task Type</label>
<select name="taskType" class="block w-full border-gray-300 rounded-md shadow-sm text-sm">
{forEach taskTypeOptions \t -> [hsx|<option value={t}>{t}</option>|]}
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Agent</label>
<select name="agentRegistrationId" class="block w-full border-gray-300 rounded-md shadow-sm text-sm">
{forEach agents \a -> [hsx|
<option value={show a.id}>{a.name} ({a.provider} / {a.modelName})</option>
|]}
</select>
</div>
<div>{(numberField #priority) { label = "Priority (higher wins)", placeholder = "0" }}</div>
<div class="flex gap-3 pt-2">
{submitButton { label = "Create Policy" }}
<a href={ModelRoutingPoliciesAction}
class="px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-md text-sm">Cancel</a>
</div>
</div>
|]}
</div>
|]

67
scripts/llm_bridge.py Executable file
View File

@@ -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()

View File

@@ -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"
```