chore(WP-0012): create Phase 11 workplan — Hub Registry and Widget Marketplace
Some checks failed
Test / test (push) Has been cancelled

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-01 20:32:04 +00:00
parent 6e8972f828
commit 4e4e994659

View File

@@ -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 110 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/<timestamp>-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`.