Host / plugin-management foundation for the Jarvis ecosystem (speech-to-speech-mobile, s2s-llm, s2s-context, s2s-tools, s2s-agent).
Replaces hardcoded LlamaLanguageModel(...)/SqliteContextEngine(...)
construction in host apps with a plugin registry + typed provider/factory
model + composition resolver. Neither s2s-agent nor
speech-to-speech-mobile depend on this module or know it exists — they
consume LanguageModel/ContextEngine/Tools exactly as before.
- Not a plugin marketplace, no remote catalog, no payments/ratings.
- Not a runtime arbitrary-code-installation mechanism — Android/Play Store
don't allow that, and this module doesn't pretend otherwise. Every
plugin registered here is compiled into the host app (
PluginSource.BUILT_IN/BUNDLED). - Not sandboxing —
PluginSource.EXTERNAL/MCPexist as enum values for a future phase; no discovery, IPC, or isolation mechanism is implemented. - Not MCP — same as above, a future
Toolsprovider, not built here. - Not another agent — zero orchestration/execution logic;
AgentRuntime(ins2s-agent) remains the only execution loop.
PluginDescriptor/PluginType/PluginSource/PluginAvailability— inspectable metadata.PluginProvider<T>— typed factory (PluginProvider<LanguageModel>, etc.), noAny/casts.PluginState—DISCOVERED → CONFIGURED → ENABLED/DISABLED,FAILEDon incompatible version, missing permission, unavailable implementation, or a creation error.PluginConfigStore—InMemoryPluginConfigStore(tests) /SharedPreferencesPluginConfigStore(persists across restarts).PluginRegistry— register/unregister/find/list/enable/disable/select, idempotent registration.HostComposer— resolves the currently enabled+selected plugins into aComposedCapabilities(languageModel, contextEngine, tools).
PluginRegistry deliberately keeps these separate — a plugin can be any
combination of them, and conflating any two produces wrong UI/composition
behavior:
| Concept | How to check | Independent of |
|---|---|---|
| Known | registry.find(id) != null |
everything else |
| Available | descriptor.availability != UNAVAILABLE |
enabled/selected — an unavailable plugin can still be "enabled" in config, it just can never compose |
| Enabled | registry.isEnabled(id) |
selection — two plugins of the same PluginType can both be enabled with only one selected |
| Selected | registry.getSelected(type) == id |
enabled — selecting a disabled plugin doesn't implicitly enable it |
| Composable right now | registry.canCompose(id, grantedPermissions) |
doesn't instantiate anything — checks known+available+enabled+permissions without calling PluginProvider.create() |
PluginRegistry.state(id) folds all of these into one PluginState for
simple UI display; canCompose() is the non-instantiating check to use
when you need a yes/no answer without side effects (e.g. before showing an
"Enable" toggle as available).
val registry = PluginRegistry(SharedPreferencesPluginConfigStore(context))
registry.register(
PluginDescriptor("llama-cpp", PluginType.LANGUAGE_MODEL, "Llama.cpp", "0.2.0"),
PluginProvider<LanguageModel> { config -> LlamaLanguageModel(LlamaConfig(), config["modelPath"]!!) },
)
registry.setEnabled("llama-cpp", true)
registry.select("llama-cpp", PluginType.LANGUAGE_MODEL)
registry.setConfig("llama-cpp", PluginConfig(mapOf("modelPath" to "/data/model.gguf")))
// ... register CONTEXT_ENGINE and TOOLS plugins the same way ...
val composed = HostComposer(registry).resolve().getOrThrow()
val engine = S2SEngine(context, config, languageModel = composed.languageModel, history = composed.contextEngine, sessionId = sessionId)Switching implementations later needs no code change downstream — only
registry.select("remote", PluginType.LANGUAGE_MODEL) followed by a fresh
HostComposer(registry).resolve() call.
- Disabling the currently-active plugin does not reach into an already-running
AgentRuntime/S2SEngine— an in-flight instance keeps running against whatever it was constructed with; only the nextresolve()call sees the change. No live-swap mechanism exists. PluginRegistry.select/setEnabled/setConfigallrequire()-throw on an unknownpluginIdrather than returning aResult— acceptable for a host's own programming error, not meant to handle a plugin registered by someone else at a different point in the app's lifecycle.- No signature/identity verification for
PluginSource.EXTERNAL— not needed yet since nothing external is discoverable in this phase, but a real implementation must add it before that source type does anything.
Published to JitPack. Note: due to an indexing quirk on first publish,
s2s-host resolves under com.github.Loyality7:s2s-host (capitalized) for
some earlier versions — check which coordinate actually resolves for the
version you're pinning (https://jitpack.io/com/github/<casing>/s2s-host/<version>/)
before depending on it.