diff --git a/workplans/IHUB-WP-0012-ihf-phase11-advanced-ai-federation.md b/workplans/IHUB-WP-0012-ihf-phase11-advanced-ai-federation.md new file mode 100644 index 0000000..2633c3a --- /dev/null +++ b/workplans/IHUB-WP-0012-ihf-phase11-advanced-ai-federation.md @@ -0,0 +1,647 @@ +--- +id: IHUB-WP-0012 +type: workplan +title: "IHF Phase 11 — Advanced AI Federation" +domain: inter_hub +repo: inter-hub +status: todo +owner: custodian +topic_slug: inter_hub +created: "2026-04-01" +updated: "2026-04-01" +state_hub_sync: done +state_hub_workstream_id: "b6d8058b-b786-4cb8-81d4-19067b2684f8" +--- + +# IHF Phase 11 — Advanced AI Federation + +## Goal + +Move AI assistance from single-model, single-agent operation (Phase 5) to a +federated, multi-model, multi-agent architecture. Phase 11 introduces agent +specialisation, model routing, inter-agent delegation, collective proposals, +and AI governance policies — while keeping humans firmly in control of final +decisions. + +## Background + +Phases 1–10 and IHUB-WP-0012 entry gates are satisfied: + +- Phase 5 `AgentProposal`, `AgentReviewRecord`, `ConfidenceAnnotation` + operational ✓ +- `HubCapabilityManifest` and type registries in place (governance policies + attach per-hub) ✓ +- `/api/v2/` surface live (agent registry will expose an API endpoint) ✓ +- GAAF scorecard at 3.56 (Strong) ✓ +- `llm-connect` available at `~/llm-connect` as a Python subprocess library ✓ + +Reference: `specs/InteractionHubFrameworkSpecification_v0.2.md` §Phase 11. + +## llm-connect Integration Architecture + +**llm-connect** (`~/llm-connect`) is a Python library with pluggable LLM +adapters for OpenRouter, Gemini, OpenAI, and Claude Code CLI. It is not a +Haskell library; the integration is via a thin **Python subprocess bridge**. + +### Bridge pattern + +``` +IHP controller action + → AgentBridge.callLlmBridge (Haskell) + → System.Process: python3 scripts/llm_bridge.py + → llm_connect.create_adapter(provider, model, api_key, system_prompt) + → adapter.execute_prompt(prompt, RunConfig(...)) + → JSON stdout: {content, model, tokens_in, tokens_out, finish_reason} + ← AgentResponse | AgentError +``` + +`scripts/llm_bridge.py` is the sole integration seam. Each `AgentRegistration` +record stores the provider, model name, and system prompt that are passed to the +bridge. The API key is resolved from environment variables at bridge call time +(following llm-connect's key resolution order: env var → key file → error). + +### Supported providers via llm-connect + +| Provider | llm-connect adapter | Notes | +|---|---|---| +| `openrouter` | `OpenRouterAdapter` | Multi-model routing; recommended for production | +| `gemini` | `GeminiAdapter` | Free tier available; Google AI Studio key | +| `openai` | `OpenAIAdapter` | GPT-4o and family | +| `claude-code` | `ClaudeCodeAdapter` | Shells out to `claude --print` CLI | + +### Known llm-connect limitations (feature requests raised in T10) + +| Gap | Impact on Phase 11 | Feature request | +|---|---|---| +| No HTTP serve mode | Process spawn overhead on every agent call | FR-1: `--serve` mode (local JSON-RPC HTTP server) | +| No routing policy API | ModelRoutingPolicy implemented in Haskell only | FR-2: `RoutingPolicy` class for declarative provider/model selection | +| Synchronous only | Collective proposals invoke agents sequentially | FR-3: `async_execute_prompt()` for concurrent execution | +| No token budget ledger | Delegation budget is checked by Haskell, not enforced by bridge | FR-4: `BudgetTracker` that spans a delegation chain | + +These limitations do not block Phase 11 — they affect performance and +architectural elegance. The feature requests are filed against llm-connect as +a separate deliverable (T10). + +## GAAF Architectural Constraints + +1. `agent_registrations.trust_level` must carry a CHECK constraint + (`advisory`, `elevated`, `autonomous`) — no bare TEXT discriminator. +2. `agent_delegations.status` must carry a CHECK constraint + (`pending`, `completed`, `failed`, `cancelled`). +3. `collective_proposals.consensus_status` must carry a CHECK constraint + (`pending`, `consensus`, `divergent`). +4. `ai_governance_policies.allowed_actions` is a JSONB array — validated at + the controller layer (each element must be one of: `read`, `propose`, + `delegate`, `auto_apply`). +5. Core tables remain frozen — `agent_proposals` gains `agent_registration_id` + and token columns via ALTER TABLE, with a corresponding update to + `/contracts/core/`. +6. Append-only invariant on `interaction_events` and `outcome_signals` + is permanent and unaffected by this phase. + +## Data Artifacts Introduced + +`AgentRegistration`, `ModelRoutingPolicy`, `AgentDelegation`, +`CollectiveProposal`, `CollectiveProposalContribution`, +`AiGovernancePolicy`, `AgentPerformanceRecord` + +### Schema additions + +```sql +-- 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, -- 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')) +); + +-- 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) +); + +-- 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')) +); + +-- 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')) +); + +-- 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 +); + +-- 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 +); + +-- 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 +); + +-- 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; +``` + +--- + +## Tasks + +### T01 — Schema: all Phase 11 tables + migration + +```task +id: IHUB-WP-0012-T01 +status: todo +priority: high +state_hub_task_id: "d86574f0-f0a4-4217-b4de-d020442de7e4" +``` + +Add all Phase 11 tables to `Application/Schema.sql` and write migration +`Application/Migration/-ihf-phase11-ai-federation.sql`. + +Includes the ALTER TABLE on `agent_proposals` to add `agent_registration_id`, +`tokens_in`, `tokens_out`. Run `migrate` after writing. + +--- + +### T02 — llm-connect bridge: scripts/llm_bridge.py + Application/Helper/AgentBridge.hs + +```task +id: IHUB-WP-0012-T02 +status: todo +priority: high +state_hub_task_id: "404d1a89-aae2-49ea-a565-90261001a633" +``` + +**`scripts/llm_bridge.py`** — thin wrapper around llm-connect: + +```python +#!/usr/bin/env python3 +# Usage: echo '{"provider":"openrouter","model":"...","prompt":"..."}' | python3 scripts/llm_bridge.py +import sys, json, 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(): + 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() +``` + +**`Application/Helper/AgentBridge.hs`** — Haskell wrapper: + +```haskell +module Application.Helper.AgentBridge where + +import IHP.Prelude +import Data.Aeson (object, (.=), encode, decode) +import qualified Data.ByteString.Lazy as LBS +import System.Process (readProcessWithExitCode) +import System.Exit (ExitCode(..)) +import Generated.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) + +callBridge :: BridgeRequest -> IO (Either BridgeError BridgeResponse) +callBridge req = do + let input = cs . 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"] input + 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 bridge using an AgentRegistration record. +callAgent :: AgentRegistration -> Text -> IO (Either BridgeError BridgeResponse) +callAgent agent prompt = + callBridge BridgeRequest + { provider = agent.provider + , model = agent.modelName + , systemPrompt = agent.systemPrompt + , prompt + , maxTokens = 2000 + , temperature = 0.7 + } +``` + +--- + +### T03 — Agent Registry: AgentRegistration CRUD + capability/trust UI + +```task +id: IHUB-WP-0012-T03 +status: todo +priority: high +state_hub_task_id: "3ae5b3b1-e644-444d-ae72-8eef07318c49" +``` + +**Controller:** `Web/Controller/AgentRegistrations.hs` + +Actions: +- `AgentRegistrationsAction` — list all agents with hub name, trust level, + provider, active/inactive +- `ShowAgentRegistrationAction { agentRegistrationId }` — detail with routing + policies, recent proposals, performance record +- `NewAgentRegistrationAction` / `CreateAgentRegistrationAction` — form with + hub selector, provider dropdown (openrouter / gemini / openai / claude-code), + model name, trust level selector, capabilities multi-tag input, system prompt + textarea +- `EditAgentRegistrationAction` / `UpdateAgentRegistrationAction` +- `DeactivateAgentAction { agentRegistrationId }` — set `is_active = False` + +Provider dropdown options map to the four llm-connect adapters. + +**Views:** `Web/View/AgentRegistrations/{Index,Show,New,Edit}.hs` + +Add nav link ("Agents") next to existing "Agent" proposals link. + +--- + +### T04 — Model routing: ModelRoutingPolicy CRUD + Application/Helper/ModelRouter.hs + +```task +id: IHUB-WP-0012-T04 +status: todo +priority: high +state_hub_task_id: "20bcff74-1923-4c11-95ef-2b37a7a10dd8" +``` + +**Controller:** `Web/Controller/ModelRoutingPolicies.hs` + +Actions: +- `ModelRoutingPoliciesAction` — list by hub, grouped by task_type +- `NewModelRoutingPolicyAction` / `CreateModelRoutingPolicyAction` — hub, + task_type selector (requirement_draft / triage / synthesis / policy_check / + implementation), agent selector, priority +- `DeleteModelRoutingPolicyAction { modelRoutingPolicyId }` + +**`Application/Helper/ModelRouter.hs`:** + +```haskell +module Application.Helper.ModelRouter where + +import IHP.Prelude +import IHP.ControllerPrelude +import Generated.Types + +-- | Resolve the AgentRegistration to use for a hub + task type. +-- Selects the highest-priority active policy for the hub. +-- Returns Nothing if no policy matches (caller should fall back or error). +resolveAgent :: + (?modelContext :: ModelContext) => + Id Hub -> Text -> IO (Maybe AgentRegistration) +resolveAgent hubId taskType = do + mPolicy <- 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 mPolicy of + [Only agentId] -> fetchOneOrNothing agentId + _ -> pure Nothing +``` + +--- + +### T05 — Agent invocation: route + invoke via bridge, replace Phase 5 hard-coded calls + +```task +id: IHUB-WP-0012-T05 +status: todo +priority: high +state_hub_task_id: "45fec21a-47d3-4381-99bc-88dcf0f117cb" +``` + +Update the four existing agent invocation points to use the routing + bridge: + +1. `SummarizeClusterAction` in `Web/Controller/Widgets.hs` +2. `DraftRequirementAction` in `Web/Controller/Widgets.hs` +3. `ProposeImplementationAction` in `Web/Controller/DecisionRecords.hs` +4. `DetectDuplicatesAction` in `Web/Controller/RequirementCandidates.hs` + +**Pattern for each:** +```haskell +mAgent <- resolveAgent hubId "requirement_draft" +case mAgent of + Nothing -> setErrorMessage "No routing policy for this task type" + Just agent -> do + -- Check AI governance policy before invocation + allowed <- checkGovernancePolicy hubId agent.id "requirement_candidate" + if not allowed + then do + newRecord @AgentProposal + |> set #status "blocked_by_policy" + |> set #agentRegistrationId (Just agent.id) + |> createRecord + setErrorMessage "Blocked by AI governance policy" + else do + result <- callAgent agent prompt + case result of + Left err -> setErrorMessage err.errorMessage + Right resp -> do + newRecord @AgentProposal + |> set #content resp.content + |> set #agentRegistrationId (Just agent.id) + |> set #tokensIn (Just resp.tokensIn) + |> set #tokensOut (Just resp.tokensOut) + |> createRecord +``` + +`checkGovernancePolicy` defined in `Application/Helper/AgentBridge.hs`: +queries `ai_governance_policies` for the hub + agent + artifact_type combination. + +--- + +### T06 — Inter-agent delegation: AgentDelegation records + bounded sub-task invocation + +```task +id: IHUB-WP-0012-T06 +status: todo +priority: medium +state_hub_task_id: "9fe1c284-ecb7-4777-98f3-253224df704c" +``` + +**Controller:** `Web/Controller/AgentDelegations.hs` + +Actions: +- `AgentDelegationsAction` — list delegations for a proposal +- `ShowAgentDelegationAction { agentDelegationId }` — detail with result +- `DelegateSubtaskAction { agentProposalId }` — form: pick receiving agent, + scope description, token budget; on submit: create `AgentDelegation` record, + call bridge with `maxTokens = delegation.tokenBudget`, store result + + `tokensUsed`, set `status = completed | failed` + +**Delegation tree on proposal Show page:** recursive query over +`parent_proposal_id` to render the full provenance chain. + +--- + +### T07 — Collective proposals: multi-agent attribution + consensus detection + +```task +id: IHUB-WP-0012-T07 +status: todo +priority: medium +state_hub_task_id: "37782cec-2d42-44b8-8bda-2049a7bf4898" +``` + +**Controller:** `Web/Controller/CollectiveProposals.hs` + +Actions: +- `CollectiveProposalsAction` — list by hub +- `ShowCollectiveProposalAction { collectiveProposalId }` — side-by-side + agent contributions + consensus/divergent status + human review buttons +- `CreateCollectiveProposalAction` — invokes all `is_active` agents for the + task_type in `model_routing_policies` priority order; creates a + `CollectiveProposalContribution` per agent; then runs a synthesis step + (invoke the highest-priority agent with all contributions as context) to + produce `final_content` + +**Consensus detection:** +```haskell +detectConsensus :: [CollectiveProposalContribution] -> Text +detectConsensus contribs = + -- Simple heuristic: if all contributions contain the same top-level + -- recommendation key, mark as consensus; otherwise divergent + if allSameRecommendation contribs then "consensus" else "divergent" +``` + +**Views:** `Web/View/CollectiveProposals/{Index,Show}.hs` + +--- + +### T08 — AI governance policies: AiGovernancePolicy CRUD + enforcement at invocation + +```task +id: IHUB-WP-0012-T08 +status: todo +priority: medium +state_hub_task_id: "50d05787-9629-4f94-ac55-50a6e417f730" +``` + +**Controller:** `Web/Controller/AiGovernancePolicies.hs` + +Actions: +- `AiGovernancePoliciesAction` — list by hub +- `NewAiGovernancePolicyAction` / `CreateAiGovernancePolicyAction` — form: + hub, agent, artifact_type, allowed_actions checkboxes (read / propose / + delegate / auto_apply) +- `ToggleAiGovernancePolicyAction { aiGovernancePolicyId }` — flip `is_active` + +**`checkGovernancePolicy` in `Application/Helper/AgentBridge.hs`:** + +```haskell +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 -- no policy = permit by default + Just p -> pure ("propose" `elem` jsonArrayTexts p.allowedActions) +``` + +**Governance compliance panel** added to existing `GovernanceDashboardAction`: +count of blocked invocations in the last 7 days per hub. + +--- + +### T09 — Agent performance dashboard + +```task +id: IHUB-WP-0012-T09 +status: todo +priority: medium +state_hub_task_id: "6ef63612-f913-4a13-b683-f44929cb1b2d" +``` + +**Action added to `AgentRegistrationsController`:** +`ComputeAgentPerformanceAction { agentRegistrationId }` — computes a 30-day +snapshot and writes `AgentPerformanceRecord`: + +```sql +SELECT + COUNT(*) FILTER (WHERE ap.status = 'accepted') AS accepted, + COUNT(*) FILTER (WHERE ap.status = 'rejected') AS rejected, + COUNT(*) FILTER (WHERE ap.status NOT IN ('accepted','rejected')) AS other, + 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' +``` + +Calibration score = Pearson correlation between `ca.score` and the binary +accept/reject outcome. + +**View:** `Web/View/AgentRegistrations/Performance.hs` — panel per agent with +progress bars for acceptance/rejection ratio, mean confidence gauge, +calibration indicator. + +--- + +### T10 — llm-connect feature requests + scorecard + CLAUDE.md + +```task +id: IHUB-WP-0012-T10 +status: todo +priority: medium +state_hub_task_id: "41a64c6c-0ce8-4c63-9175-6b8d28143f81" +``` + +**File feature requests against `~/llm-connect`** (as GitHub issues or local +`FEATURE_REQUESTS.md` in that repo): + +| # | Title | Why needed | Proposed API | +|---|---|---|---| +| FR-1 | HTTP/JSON-RPC serve mode | Avoid process spawn per call in production | `python -m llm_connect.server --port 9999`; IHP calls `POST localhost:9999/execute` | +| FR-2 | RoutingPolicy class | Declarative provider/model selection beyond `model_name` in RunConfig | `RoutingPolicy(rules=[{task_type: "triage", prefer: [{provider: "openrouter", model: "claude-haiku-4-5"}], max_cost_per_1k: 0.5}])` | +| FR-3 | `async_execute_prompt()` | Collective proposals need parallel agent invocation | Standard asyncio coroutine interface matching `execute_prompt` signature | +| FR-4 | `BudgetTracker` for delegation chains | Token budget enforcement across nested delegations | `BudgetTracker(total=4000)` passed through `RunConfig`; raises `LLMBudgetExceededError` | + +**ARCHITECTURE-LAYERS.md scorecard update:** +- Functional: 3.4 → 3.6 (multi-agent federation formalises AI collaboration) +- Extensions: 3.8 → 3.9 (agent registry exposes an API surface) +- Target overall: ≥3.7 + +**CLAUDE.md:** move IHUB-WP-0012 to completed; set active → IHUB-WP-0013 +(Phase 12 — Platform Memory and Continuous Learning). + +**Commit all changes** and mark workplan `status: done`.