///|
/// Manifest aggregation — merges `Array[&Extension]` into a single
/// `AggregatedPorts` value for the agent constructor.
///|
/// Flattened port bundle ready for the agent constructor. The `model` field
/// is a single value (multi-model routing is solved in-extension), and `ui`
/// is wrapped into one UiPort reference (the agent never sees a bare array).
priv struct AggregatedPorts {
model : &@port.ModelPort
tools : Array[&@port.ToolProvider]
sessions : Array[&@port.SessionStore]
observers : Array[&@port.Observer]
hooks : Array[&@port.Hook]
memory : Array[&@port.MemoryPort]
lifecycle : Array[&@port.Lifecycle]
commands : Array[&@port.CommandPort]
ui : &@port.UiPort
/// Contributor paired with its manifest id (used as the prompt section
/// header so the assembled prompt names its source).
prompt_contributors : Array[(String, &@port.SystemPromptContributor)]
}
///|
/// Aggregate every extension's manifest into one port bundle.
/// Order follows extension array order; collisions fail fast.
fn aggregate_extensions(
exts : Array[&@port.Extension],
) -> AggregatedPorts raise @error.CompositionError {
if exts.length() == 0 {
raise @error.CompositionError::EmptyManifests
}
// Per-port collectors.
let models : Array[&@port.ModelPort] = []
let model_manifests : Array[String] = []
let tools : Array[&@port.ToolProvider] = []
let tool_manifests : Map[String, Array[String]] = Map::from_array([])
let sessions : Array[&@port.SessionStore] = []
let observers : Array[&@port.Observer] = []
let hooks : Array[&@port.Hook] = []
let memory : Array[&@port.MemoryPort] = []
let lifecycle : Array[&@port.Lifecycle] = []
let commands : Array[&@port.CommandPort] = []
let command_manifests : Map[String, Array[String]] = Map::from_array([])
let ui : Array[&@port.UiPort] = []
let prompt_contributors : Array[(String, &@port.SystemPromptContributor)] = []
for ext in exts {
let manifest = ext.manifest()
let mid = manifest.id
if mid == "" {
raise @error.CompositionError::ManifestSchemaError(
manifest_id="",
detail="extension returned a manifest with empty id",
)
}
// models: collect for later cardinality check
for m in manifest.models {
models.push(m)
model_manifests.push(mid)
}
// tools: collect + collision check by tool name
for provider in manifest.tools {
for tool_def in provider.list_tools() {
let name = tool_def.name.to_string()
let source_label = match tool_def.provenance {
Some(s) if s != "" => s
_ =>
"ToolDef.provenance is empty for tool '" +
name +
"' in manifest '" +
mid +
"' (set ToolDef.provenance to a stable provider id to disambiguate)"
}
if tool_manifests.contains(name) {
let existing = tool_manifests[name]
existing.push(mid)
raise @error.CompositionError::ToolCollision(
name,
"tool collision; first declaration: " +
existing[0] +
"; conflicting declaration: " +
source_label,
manifests=existing,
)
} else {
tool_manifests[name] = [source_label]
}
}
tools.push(provider)
}
// commands: collect + collision check by command id
for cmd in manifest.commands {
for def in cmd.commands() {
let cid = def.id
if command_manifests.contains(cid) {
let existing = command_manifests[cid]
existing.push(mid)
raise @error.CompositionError::CommandCollision(
cid,
manifests=existing,
)
} else {
command_manifests[cid] = [mid]
}
}
commands.push(cmd)
}
// simple concat ports
for s in manifest.sessions {
sessions.push(s)
}
for o in manifest.observers {
observers.push(o)
}
for h in manifest.hooks {
hooks.push(h)
}
for m in manifest.memory {
memory.push(m)
}
for l in manifest.lifecycle {
lifecycle.push(l)
}
for u in manifest.ui {
ui.push(u)
}
for p in manifest.prompt_contributors {
prompt_contributors.push((mid, p))
}
}
// Model cardinality: exactly 1.
match models.length() {
0 => raise @error.CompositionError::MissingModel
1 => ()
_ =>
raise @error.CompositionError::MultipleModels(manifests=model_manifests)
}
// UI cardinality: 0 → NoopUiPort, 1 → passthrough, 2+ → CompositeUiPort.
let ui_ref : &@port.UiPort = match ui.length() {
0 => {
let n : &@port.UiPort = NoopUiPort()
n
}
1 => {
let u : &@port.UiPort = ui[0]
u
}
_ => {
let c : &@port.UiPort = CompositeUiPort(ui.copy())
c
}
}
{
model: models[0],
tools,
sessions,
observers,
hooks,
memory,
lifecycle,
commands,
ui: ui_ref,
prompt_contributors,
}
}