feat: integrate llm-connect FR-1/FR-3/FR-4 into IHF bridge
Some checks failed
Test / test (push) Has been cancelled

FR-3 (async_execute_prompt): CollectiveProposals now invokes all agents
concurrently via callAgentsBatch → single bridge subprocess with
asyncio.gather. Latency scales with slowest agent, not sum.

FR-4 (BudgetTracker): AgentDelegations passes tokenBudget to bridge;
llm-connect enforces it natively via BudgetTracker in RunConfig.
BudgetExceededError is a first-class BridgeError variant with total/
consumed/requested fields surfaced to the operator.

FR-1 (LLMServer passthrough): bridge accepts optional serverUrl field;
if present, calls POST {serverUrl}/execute instead of spawning a new
adapter. Infrastructure ready for hot-agent pre-warming (no schema
change required).

AgentBridge.hs: adds callAgentsBatch, callAgentWithBudget,
BudgetExceededError constructor, bridgeErrorMessage helper, defaultRequest,
requestToJson. All controllers updated to use bridgeErrorMessage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-01 22:48:29 +00:00
parent a400365d50
commit 674f5da0e1
7 changed files with 350 additions and 75 deletions

View File

@@ -1,11 +1,12 @@
module Web.Controller.CollectiveProposals where
-- IHF Phase 11 — Advanced AI Federation (IHUB-WP-0012 T07)
-- Updated: agents invoked concurrently via callAgentsBatch (FR-3 async).
import Web.Controller.Prelude
import Web.View.CollectiveProposals.Index
import Web.View.CollectiveProposals.Show
import Application.Helper.AgentBridge (callAgent, BridgeResponse(..))
import Application.Helper.AgentBridge (callAgent, callAgentsBatch, BridgeResponse(..))
import Application.Helper.ModelRouter (resolveAllAgents)
import Data.List (intercalate)
@@ -45,10 +46,15 @@ instance Controller CollectiveProposalsController where
|> createRecord
agents <- resolveAllAgents hubId taskType
contributions <- forM agents \agent -> do
result <- liftIO $ callAgent agent prompt
-- FR-3: invoke all agents concurrently in a single bridge subprocess call
-- instead of sequential forM. Latency now scales with the slowest agent,
-- not the sum of all agents.
results <- liftIO $ callAgentsBatch [(a, prompt) | a <- agents]
successContribs <- fmap catMaybes $ forM (zip agents results) \(agent, result) ->
case result of
Left err -> pure Nothing
Left _ -> pure Nothing
Right resp -> do
contrib <- newRecord @CollectiveProposalContribution
|> set #collectiveProposalId proposal.id
@@ -60,22 +66,21 @@ instance Controller CollectiveProposalsController where
|> createRecord
pure (Just (contrib, resp))
let successContribs = catMaybes contributions
consensusStatus <- if null successContribs
then pure "divergent"
then do
proposal |> set #consensusStatus "divergent" |> updateRecord
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
-- Synthesis uses the highest-priority agent (head of the list)
case agents of
[] -> do
proposal |> set #consensusStatus "divergent" |> updateRecord
pure "divergent"
Just synthAgent -> do
(synthAgent:_) -> do
synthResult <- liftIO $ callAgent synthAgent synthesisPrompt
case synthResult of
Left _ -> do
@@ -95,8 +100,7 @@ instance Controller CollectiveProposalsController where
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.
-- | Simple consensus heuristic: ≥2 successful contributions = consensus.
detectConsensus :: [CollectiveProposalContribution] -> Text
detectConsensus contribs
| length contribs >= 2 = "consensus"