ADR 007: Skill Provisioning vs Binding, Model Gateway, and Evre 2 Configuration Placement
- Status: Accepted
- Date: 2026-08-07
- Deciders: Software Architecture Team
Context and Problem Statement
Evre 2 introduces Guardrails, MCP, and A2A capabilities, and raises two structural questions:
-
Skills: The existing
SkillDefinitionCRD model andSkillRegistryconflate two concerns — how a skill is provided (compiled-in classes, external JARs dropped into a folder) and how an agent binds to it (which skills, with which parameters). The sealedSkillinterface (permits CalculatorSkill, WebSearchSkill, WeatherSkill, CustomSkill) prevents external JARs from contributing implementations at compile time, andCustomSkillis a placeholder fallback, not real extensibility. -
Model management:
LlmProviderSpecis a clean transport definition (providerType, baseUrl, apiKeySecretRef, defaultModel, temperature, maxTokens). Model cost, fallback chains, and budget policies are routing/policy concerns that do not belong to a single provider definition.
Additionally, the placement of Guardrail policy, MCP server definitions, and A2A capability across CRDs must be decided consistently with the existing providerRef reference pattern.
Decision Drivers
- Extensibility: Anyone must be able to add a skill by dropping a JAR into a mounted folder — no code change, no recompile.
- Declarative Contracts: CRDs declare what (policy, references), modules implement how.
- Pattern Consistency: MCP servers are external resources like
LlmProvider; agents should reference them via the same*Refpattern already used for providers. - Separation of Routing from Transport: Fallback, budget, and cost tracking operate across providers and must not pollute the provider transport spec.
- Security: Guardrail policy is per-agent (PII masking modes differ by domain), but enforcement is a platform pipeline.
Considered Options
Skills
- Inline everything in
SkillDefinition(Status Quo): Skill definitions carry both provisioning hints and binding config. Sealed interface stays; external JARs impossible without recompilingtuluat-engine. - Split Provisioning vs Binding: A
SkillProviderSPI discovered viaServiceLoaderfrom mounted JAR folders (skillSources), plus the existingSkillDefinitionkept purely as the per-agent binding (name, enabled, parameters).Skillbecomes non-sealed; built-ins register in code, external skills register via SPI.
Model Management
- Extend
LlmProviderSpec: Add fallbacks, budget, cost to the provider CRD. - Dedicated
ModelGatewaylayer: KeepLlmProvideras transport; add a gateway service intuluat-enginethat resolves routes, applies fallback ordering, enforces budgets, and records cost telemetry. Pricing metadata (costPer1kInputTokens,costPer1kOutputTokens) remains on the provider as data, not policy.
Decision Outcome
Chosen Option: Option 2 for both.
1. Skill Provisioning vs Binding
Skillinterface is unsealed so external JARs can implement it (compile-time requirement for SPI extensibility).- New SPI:
SkillProviderincom.tuluat.engine.skill.spi— implementations returnSkillinstances. Built-in skills (Calculator, WebSearch, Weather) register via a defaultSkillProviderinSkillRegistry; external providers register viaServiceLoader. - JAR-drop support:
SkillJarLoaderscans configured skill folders (skillSources), loads*.jarwith an isolatedURLClassLoader, discoversSkillProviderimplementations, and registers them. Optional folder watch enables hot-reload. - CRD binding unchanged:
AiAgentSpec.skills: List<SkillDefinition>remains the per-agent binding (name, description, enabled, parameters). A skill is provided by any source and bound by the agent spec.
2. Model Gateway
LlmProviderSpecadditions (data only):costPer1kInputTokens/costPer1kOutputTokens(pricing metadata)- New
ModelGatewayservice intuluat-engine: - Route resolution: primary provider/model → ordered fallback list
- Budget enforcement (per agent/global, limit over a period)
- Cost estimation from usage + pricing metadata, emitted to
UsageStatsandWorkflowTelemetryService - External gateway escape hatch:
LlmProviderSpec.baseUrlalready accepts arbitrary URLs (e.g. LiteLLM), so the gateway can front a third-party router with no architectural change.
3. Configuration Placement Summary
| Feature | Location | Pattern |
|---|---|---|
| Guardrail policy (PII masking, injection defense, min confidence) | AiAgentSpec.guardrails (optional block; platform defaults applied when absent) |
Declarative policy, engine-enforced |
| Post-execution output JSON Schema | NodeDefinition.outputSchema (workflow node contract) |
Node contract, not agent identity |
| MCP server definition | New McpServer CRD in tuluat-crd-domain |
Mirrors LlmProvider |
| MCP references from agents | AiAgentSpec.mcpServers: List<McpServerRef> |
Mirrors providerRef |
| MCP runtime | McpClientRegistryImpl in tuluat-protocols |
Operator reconciles McpServer CRDs into client connections |
| A2A adapter | tuluat-protocols, platform level (gRPC/REST discovery + remote execution) |
Gateway capability |
| A2A agent opt-in | AiAgentSpec.a2a (optional { enabled, remoteDiscovery }) |
Declaration only |
Positives
- JAR-drop extensibility: Third parties extend the platform without recompiling or forking
tuluat-engine. - Clean seam: CRDs remain declarative contracts; implementation stays in
tuluat-guardrails,tuluat-protocols, andtuluat-engine. - Consistent reference pattern:
McpServerRef/skillSources/a2aall follow theproviderRefidiom. - Realistic fallback testing: The WireMock stub provider can act as primary with a real provider as fallback in KinD E2E.
Negatives
- Sealed-interface loss: Removing
sealedfromSkillsacrifices a compile-time exhaustiveness guarantee in exchange for extensibility (mitigated by the SPI contract and registry isolation). - JAR classloader hygiene:
URLClassLoaderisolation requires care to avoid leaks on hot-reload (folder watcher must close loaders). - Gateway complexity: Budget/fallback logic adds a layer between
AgentExecutionServiceand providers; must be covered by unit tests.