Skip to content

refactor(runtime): per-capability service traits (ADR-0002)#21

Merged
xiduzo merged 5 commits into
mainfrom
refactor/runtime-service-traits
May 17, 2026
Merged

refactor(runtime): per-capability service traits (ADR-0002)#21
xiduzo merged 5 commits into
mainfrom
refactor/runtime-service-traits

Conversation

@xiduzo
Copy link
Copy Markdown
Owner

@xiduzo xiduzo commented May 17, 2026

Summary

Land ADR-0002 — replaces the ad-hoc external-service patterns (RuntimeContext's build-time snapshot for LLM; _mqtt_publish event-out + lib.rs interceptor for MQTT/Figma) with one shape: per-capability traits, live service registries, typed ComponentBuilder::Deps.

Future external kinds (HTTP webhook, OSC, WebSocket, ...) add one trait + one registry + one RuntimeServices field + one FromServices impl. Zero churn in the 29 unrelated builders.

Phases

Five commits, each compiles and ships:

  • P1LlmProvider trait, LlmRegistry, HttpLlmProvider, RecordingLlmProvider. Pure addition; existing Llm component untouched.
  • P2Llm migrates to dispatch-time registry lookup. LlmConfig sheds base_url/api_key. Dead LlmManager + llm/provider.rs deleted. Stale-credential hazard in pending_flow eliminated.
  • P3MqttPublisher trait + RecordingMqttPublisher. MqttManager implements the production adapter. Mqtt/Figma publish directly. _mqtt_publish interceptor, MqttPublishRequest, mqtt_publish_tx/rx, dedicated publish-handler thread all retired from lib.rs.
  • P4RuntimeServices + FromServices. ComponentBuilder gains type Deps: FromServices. 29 builders declare type Deps = (); Llm/Mqtt/Figma declare what they need. RuntimeContext and runtime/context.rs deleted.
  • Clippy — fix manual_async_fn on CommandReceipt::into_future plus field_reassign_with_default in the new tests; cargo clippy --all-targets -- -D warnings is now clean.

Test plan

  • cargo build --lib — clean
  • cargo test --lib — 66 / 66 pass (+27 new across phases; 24 phase tests + 3 FromServices tests)
  • cargo test — all 9 binaries green
  • cargo clippy --all-targets -- -D warnings — clean
  • Manual smoke against real Ollama + an MQTT broker once merged

Docs

  • docs/adr/0002-per-capability-service-traits.md — decision record, five sub-decisions, four-phase rollout
  • CONTEXT.md — new entries: Capability Trait, Service Registry, LLM Provider, MQTT Publisher, Runtime Services, Component Deps; removed Runtime Context

🤖 Generated with Claude Code

xiduzo and others added 5 commits May 17, 2026 00:50
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>
@xiduzo xiduzo merged commit 1887f1e into main May 17, 2026
6 of 7 checks passed
@xiduzo xiduzo deleted the refactor/runtime-service-traits branch May 17, 2026 09:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant