refactor(runtime): per-capability service traits (ADR-0002)#21
Merged
Conversation
ADR-0002 lays out a four-phase migration from the parallel RuntimeContext + _mqtt_publish patterns to one shape: per-kind Capability Traits with live Service Registries. Phase 1 lands the LLM trait and its two adapters without touching the existing Llm component — pure addition. - runtime/services/llm.rs: LlmProvider async trait, LlmRequest / LlmResponse / LlmError value types, LlmRegistry keyed by id. - Production adapter HttpLlmProvider shares one reqwest::Client per instance (connection-pool reuse). Empty api_key skips the bearer header for local Ollama. - Test adapter RecordingLlmProvider records every inbound request and returns scripted outcomes from a FIFO queue — the second adapter that makes the trait a real seam, mirroring BoardHandle + TestIoLoop. - 8 unit tests cover the recording adapter, registry roundtrip, missing ids, atomic sync, and same-id replacement semantics. - CONTEXT.md gains Capability Trait, Service Registry, and LLM Provider entries; Runtime Context marked deprecated, pointing at the ADR. async-trait = "0.1" added for dyn-safe async-in-trait under Rust 2021. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…time
ADR-0002 Phase 2. The Llm component now holds Arc<LlmRegistry> instead of
a build-time credential snapshot. LlmRegistry replaces the dead LlmManager;
the parallel sync paths (llm_sync_providers and flow_update's providers
field) both feed the same registry, so credential rotation takes effect
on the next dispatch — no flow_update re-fire required.
- Llm::new(id, config, Arc<LlmRegistry>); spawn_generate resolves the
provider by id inside the spawned task and emits an "error" event when
the id is missing from the registry.
- LlmConfig sheds base_url/api_key; credentials live on the registry's
HttpLlmProvider entries now.
- RuntimeContext holds the registry (single field, Arc-cloned cheaply);
ProviderEntry deletes — pending_flow's snapshot now carries the live
registry handle, killing the stale-credential hazard the ADR called out.
- AppState swaps LlmManager for Arc<LlmRegistry>; llm_sync_providers and
flow_update both map FrontendProviderConfig → Arc<HttpLlmProvider> and
call LlmRegistry::sync.
- llm/manager.rs and llm/provider.rs delete; llm/mod.rs is now just the
Tauri command module.
- Four new Llm component tests against RecordingLlmProvider cover the
happy path, missing-provider error path, system-prompt forwarding, and
{{var}} template substitution.
- CONTEXT.md § Runtime Context updated to reflect the registry-carrying
shape.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ADR-0002 Phase 3. Mqtt and Figma components now hold Arc<dyn MqttPublisher>
and call publish() directly from their dispatch arms via a spawned Tokio
task. The _mqtt_publish reserved-event pattern is gone: components no
longer emit a JSON-encoded publish request for lib.rs to parse, no
dedicated handler thread, no mqtt_publish_tx channel, no MqttPublishRequest
struct.
- runtime/services/mqtt.rs: trait MqttPublisher with publish() returning
MqttPublishError (BrokerNotConnected | PublishFailed). impl on the
existing MqttManager translates the legacy String error into the typed
variant. RecordingMqttPublisher records (broker_id, topic, payload,
retain) tuples and pops scripted errors from a FIFO queue — the second
adapter that makes the trait a real seam.
- Mqtt component: new(id, config, Arc<dyn MqttPublisher>); dispatch
("trigger") encodes the args via Self::encode_payload and spawns the
async publish, logging failures. Subscribe path unchanged.
- Figma component: new(id, config, Arc<dyn MqttPublisher>); publish() is
now a spawn-and-go that calls publisher.publish on the set_topic. All
dispatch arms (true/false/toggle/set/increment/decrement/reset/red/
green/blue/opacity) keep their semantics; only the wire path changes.
- RuntimeContext gains a mqtt_publisher: Arc<dyn MqttPublisher> field
alongside llm_registry; AppState exposes the same Arc to the rest of
the app (built once from mqtt_manager.clone() so the publisher and the
manager share one broker pool).
- lib.rs loses MqttPublishRequest, mqtt_publish_tx/rx, the _mqtt_publish
intercept branch (~30 lines), and the dedicated publish handler thread
(~25 lines). The event-forwarding thread is now a plain
emit-to-frontend + process_event loop.
- 9 new tests cover RecordingMqttPublisher, Mqtt publish dispatch +
subscribe-rejects-trigger + payload encoding, and Figma dispatch arms
against the recorder.
- CONTEXT.md gains an "MQTT Publisher" entry; Runtime Context updated
to list both registries on the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…s, retire RuntimeContext ADR-0002 Phase 4. The `ComponentBuilder` trait now declares an associated `type Deps: FromServices`; the component registry's factory projects each impl's slice of the shared `RuntimeServices` bundle at build time. RuntimeContext deletes — its role is taken over by RuntimeServices, which lives beside the capability traits in runtime/services. - runtime/services/mod.rs: pub struct RuntimeServices holding Arc<LlmRegistry> + Arc<dyn MqttPublisher>; pub trait FromServices with impls for () (no-deps default), Arc<LlmRegistry>, Arc<dyn MqttPublisher>. Three tests cover the projection identity-Arc semantics. - runtime/component.rs: ComponentBuilder gains `type Deps: FromServices`; build signature changes from (id, config, &RuntimeContext) to (id, config, Self::Deps). - runtime/registry.rs: Factory type takes &RuntimeServices; make_factory projects deps via <B::Deps as FromServices>::from_services(services). - runtime/builders.rs: 29 builders declare `type Deps = ();` and take _deps: Self::Deps; Llm declares Arc<LlmRegistry>; Mqtt/Figma declare Arc<dyn MqttPublisher>. Each build body reduces to Self::new(id, config, deps) or Self::new(id, config). - runtime/mod.rs / lib.rs / runtime/commands.rs: drop RuntimeContext imports, rename ctx → services everywhere, pending_flow now carries (FlowUpdate, RuntimeServices). - runtime/context.rs deleted. - CONTEXT.md: Runtime Context section deletes; Runtime Services and Component Deps sections added. 66 lib tests pass (+3 FromServices). Full sweep across all 9 test binaries passes. Clippy clean (the one pre-existing manual_async_fn warning in board/receipt.rs is unrelated). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two pre-existing / freshly-introduced lints in the runtime tree:
- board/receipt.rs::CommandReceipt::into_future used the manual
impl-Future + async move {} pattern that clippy::manual_async_fn now
flags. Converted to plain `pub async fn into_future(self) -> Result<..>`;
the future is still Send + 'static because the body's only captured
state is the oneshot::Receiver.
- The new external/{llm,mqtt,figma} tests built configs via
`let mut config = FooConfig::default(); config.field = value;` — clippy
field_reassign_with_default. Rewrote each as struct-literal init with
`..FooConfig::default()` to fill the unspecified fields.
cargo clippy --all-targets -- -D warnings is now clean.
cargo test --lib continues to pass (66/66).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Land ADR-0002 — replaces the ad-hoc external-service patterns (
RuntimeContext's build-time snapshot for LLM;_mqtt_publishevent-out + lib.rs interceptor for MQTT/Figma) with one shape: per-capability traits, live service registries, typedComponentBuilder::Deps.Future external kinds (HTTP webhook, OSC, WebSocket, ...) add one trait + one registry + one
RuntimeServicesfield + oneFromServicesimpl. Zero churn in the 29 unrelated builders.Phases
Five commits, each compiles and ships:
LlmProvidertrait,LlmRegistry,HttpLlmProvider,RecordingLlmProvider. Pure addition; existingLlmcomponent untouched.Llmmigrates to dispatch-time registry lookup.LlmConfigshedsbase_url/api_key. DeadLlmManager+llm/provider.rsdeleted. Stale-credential hazard inpending_floweliminated.MqttPublishertrait +RecordingMqttPublisher.MqttManagerimplements the production adapter.Mqtt/Figmapublish directly._mqtt_publishinterceptor,MqttPublishRequest,mqtt_publish_tx/rx, dedicated publish-handler thread all retired fromlib.rs.RuntimeServices+FromServices.ComponentBuildergainstype Deps: FromServices. 29 builders declaretype Deps = ();Llm/Mqtt/Figmadeclare what they need.RuntimeContextandruntime/context.rsdeleted.manual_async_fnonCommandReceipt::into_futureplusfield_reassign_with_defaultin the new tests;cargo clippy --all-targets -- -D warningsis now clean.Test plan
cargo build --lib— cleancargo test --lib— 66 / 66 pass (+27 new across phases; 24 phase tests + 3 FromServices tests)cargo test— all 9 binaries greencargo clippy --all-targets -- -D warnings— cleanDocs
docs/adr/0002-per-capability-service-traits.md— decision record, five sub-decisions, four-phase rolloutCONTEXT.md— new entries:Capability Trait,Service Registry,LLM Provider,MQTT Publisher,Runtime Services,Component Deps; removedRuntime Context🤖 Generated with Claude Code