mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
Co-authored-by: M3gA-Mind <elvin@tinyhumans.ai>
This commit is contained in:
@@ -354,8 +354,10 @@ jobs:
|
||||
# Feature-gate smoke: proves the core still compiles with a domain gate turned
|
||||
# OFF. The disabled build is the ONLY thing that catches stub-facade signature
|
||||
# drift (see AGENTS.md "Compile-time domain gates"), so it must run in CI, not
|
||||
# just locally. Pathfinder lane for #4803 (voice); extend the --features list
|
||||
# as sibling gates (#4797–#4802, #4804) land.
|
||||
# just locally. Pathfinder lane for #4803 (voice); the explicit --features
|
||||
# list (only tokenjuice-treesitter) turns every default gate OFF, so it also
|
||||
# covers #4804 (media) and each sibling gate as it lands — no edit needed
|
||||
# unless a new gate must stay ON here.
|
||||
rust-feature-gate-smoke:
|
||||
name: Rust Feature-Gate Smoke (gates off)
|
||||
needs: [changes]
|
||||
@@ -382,7 +384,7 @@ jobs:
|
||||
cache-on-failure: true
|
||||
shared-key: pr-rust-feature-gate-smoke
|
||||
|
||||
- name: Check core builds with the voice gate disabled
|
||||
- name: Check core builds with the voice + media gates disabled
|
||||
run: bash scripts/ci-cancel-aware.sh cargo check --manifest-path Cargo.toml --no-default-features --features tokenjuice-treesitter
|
||||
|
||||
rust-core-coverage:
|
||||
|
||||
@@ -233,11 +233,14 @@ GGML_NATIVE=OFF cargo check --manifest-path Cargo.toml \
|
||||
| Feature | Default | Gates | Drops deps |
|
||||
| ------- | ------- | ----- | ---------- |
|
||||
| `voice` | ON | `openhuman::voice` + `openhuman::audio_toolkit` domains — STT/TTS providers, dictation server, always-on listening, podcast audio + email | `hound`, `lettre` |
|
||||
| `media` | ON | `openhuman::media_generation` (the `media_generate_*` agent tools) + `openhuman::image` scaffold | none (surface-only) |
|
||||
|
||||
**Facade pattern (pathfinder for the other gates).** `pub mod voice;` is **always compiled** as a facade: the real submodules are `#[cfg(feature = "voice")]`, and a `#[cfg(not(feature = "voice"))] mod stub;` (`src/openhuman/voice/stub.rs`) re-exposes the same public surface that always-on / other-gated callers use (`server`, `dictation_listener`, `streaming`, `reply_speech`, `cloud_transcribe`, `cli`, `create_stt_provider`, `effective_stt_provider`, `publish_ptt_transcript_committed`) with no-op / `None` / disabled-error bodies. Callers therefore do **not** need per-call `#[cfg]`. When voice is off: the voice/audio controllers are unregistered (unknown-method over `/rpc`, absent from `/schema`), the `audio_generate_podcast` agent tools are absent, and `openhuman voice` returns a "voice disabled" error. Stub signatures must match the real ones exactly — the disabled build (`--no-default-features --features tokenjuice-treesitter`) is the **only** thing that catches drift, so run it before pushing any change to the voice surface.
|
||||
|
||||
**Scope note:** the `voice` gate does **not** drop `whisper-rs` / `llama` / `cpal`. Those live in the inference domain (`src/openhuman/inference/local/service/whisper_engine.rs`; `cpal` is shared with accessibility) and await a separate future `inference` gate. The issue-level DoD line claiming whisper is dropped is superseded by this scope correction.
|
||||
|
||||
**Leaf-gate variant (`media`, #4804).** Unlike `voice`, the `media` gate needs **no** stub facade: `media_generation` has a single caller (the `build_media_tools` call in `src/openhuman/tools/ops.rs`, itself `#[cfg(feature = "media")]`) and `openhuman::image` is unwired scaffold (#2997), so both modules are simply `#[cfg(feature = "media")] pub mod …`. It is a **surface-only** gate: media generation is backend-proxied (`reqwest`, shared) and the `image` crate is shared with channel upload, so no exclusive deps are shed — the issue's "sheds media processing dependencies" / "controllers unregistered" DoD lines are superseded (Media is agent-tools-only; no controller/store/subscriber is tagged `Media`). When a gated domain is a true leaf, prefer this over the facade+stub.
|
||||
|
||||
### Event bus (`src/core/event_bus/`)
|
||||
|
||||
Typed pub/sub + native request/response. Both singletons — use module-level functions.
|
||||
|
||||
+13
-1
@@ -334,7 +334,7 @@ tokio = { version = "1", features = ["test-util"] }
|
||||
proptest = "1"
|
||||
|
||||
[features]
|
||||
default = ["tokenjuice-treesitter", "voice"]
|
||||
default = ["tokenjuice-treesitter", "voice", "media"]
|
||||
# AST-aware code compression (tree-sitter Rust/TS/Python grammars; C build).
|
||||
# On by default; disable to fall back to the brace-depth heuristic.
|
||||
tokenjuice-treesitter = [
|
||||
@@ -351,6 +351,18 @@ tokenjuice-treesitter = [
|
||||
# inference domain (shared with accessibility for cpal) and await a separate
|
||||
# `inference` gate.
|
||||
voice = ["dep:hound", "dep:lettre"]
|
||||
# Media-generation + image domains: the `media_generate_*` agent tools
|
||||
# (image/video via GMI through the backend) and the `openhuman::image` tool
|
||||
# contracts scaffold. Default-ON. Slim builds opt out via
|
||||
# `--no-default-features --features "<explicit list without media>"`.
|
||||
# Composes with the runtime `DomainSet::media` flag (#4796).
|
||||
# NOTE: this gate sheds NO exclusive dependencies — media generation is
|
||||
# backend-proxied (reqwest, shared) and the `image` crate is shared with
|
||||
# channel media upload. It is a surface-only gate (drops the tool code +
|
||||
# module from the compile), not a dependency-shedding one. There are no
|
||||
# controllers / stores / subscribers tagged `Media` (agent tools only), and
|
||||
# `openhuman::image` is currently unwired scaffold (added #2997).
|
||||
media = []
|
||||
sandbox-landlock = ["dep:landlock"]
|
||||
sandbox-bubblewrap = []
|
||||
peripheral-rpi = ["dep:rppal"]
|
||||
|
||||
@@ -132,7 +132,17 @@ cef = { version = "=146.4.1", default-features = false }
|
||||
# by tying the core's lifetime to the GUI process. The existing port-7788
|
||||
# probe in `core_process::ensure_running` still attaches to a running
|
||||
# `openhuman-core run` harness when one is already listening.
|
||||
openhuman_core = { path = "../..", package = "openhuman", default-features = false }
|
||||
#
|
||||
# `default-features = false` (set in #1061, before the compile-time domain
|
||||
# gates existed) means the embedded core does NOT inherit the root crate's
|
||||
# default gate set, so each default-ON gate must be forwarded explicitly to
|
||||
# keep the shipped desktop build byte-identical (AGENTS.md "Compile-time
|
||||
# domain gates"). `media` re-registers the `media_generate_*` agent tools
|
||||
# that #4804 moved behind `#[cfg(feature = "media")]`; it sheds no deps, so
|
||||
# this only restores the pre-gate desktop tool surface.
|
||||
openhuman_core = { path = "../..", package = "openhuman", default-features = false, features = [
|
||||
"media",
|
||||
] }
|
||||
tinyjuice = { version = "0.2.1", default-features = false }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
|
||||
@@ -57,6 +57,7 @@ pub mod harness_init;
|
||||
pub mod health;
|
||||
pub mod heartbeat;
|
||||
pub mod http_host;
|
||||
#[cfg(feature = "media")]
|
||||
pub mod image;
|
||||
pub mod inference;
|
||||
pub mod integrations;
|
||||
@@ -68,6 +69,7 @@ pub mod mcp_audit;
|
||||
pub mod mcp_client;
|
||||
pub mod mcp_registry;
|
||||
pub mod mcp_server;
|
||||
#[cfg(feature = "media")]
|
||||
pub mod media_generation;
|
||||
pub mod meet;
|
||||
pub mod meet_agent;
|
||||
|
||||
@@ -875,6 +875,9 @@ pub fn all_tools_with_runtime(
|
||||
|
||||
// Media generation (image/video via GMI through the backend). Skipped when
|
||||
// no integration client is configured; artifacts land under `action_dir`.
|
||||
// Gated by the `media` compile-time feature (#4804); absent from slim
|
||||
// builds. Runtime `DomainSet::media` (#4796) still gates it when compiled.
|
||||
#[cfg(feature = "media")]
|
||||
tools.extend(crate::openhuman::media_generation::build_media_tools(
|
||||
root_config,
|
||||
action_dir,
|
||||
|
||||
@@ -260,6 +260,40 @@ fn all_tools_always_registers_curl() {
|
||||
);
|
||||
}
|
||||
|
||||
// Compile-time `media` feature gate (#4804). The media-generation agent tools
|
||||
// (`media_generate_*`) are present only when the `media` feature is compiled
|
||||
// in AND an integration client is configured. The disabled build proves the
|
||||
// module + its single call site drop out entirely (leaf gate, no stub facade).
|
||||
#[cfg(feature = "media")]
|
||||
#[test]
|
||||
fn media_tools_registered_when_feature_on() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let cfg = integration_test_config(&tmp, "http://127.0.0.1:1");
|
||||
store_test_session_token(&cfg);
|
||||
let tools = integration_tools_for_config(&tmp, &cfg);
|
||||
let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
|
||||
assert!(
|
||||
names.contains(&"media_generate_image"),
|
||||
"media tools must register with the `media` feature on + an integration \
|
||||
client; got: {names:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "media"))]
|
||||
#[test]
|
||||
fn media_tools_absent_when_feature_off() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let cfg = integration_test_config(&tmp, "http://127.0.0.1:1");
|
||||
store_test_session_token(&cfg);
|
||||
let tools = integration_tools_for_config(&tmp, &cfg);
|
||||
let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
|
||||
assert!(
|
||||
!names.iter().any(|n| n.starts_with("media_")),
|
||||
"no `media_*` tools may be registered when the `media` feature is off; \
|
||||
got: {names:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_tools_registers_gitbooks_when_enabled() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
@@ -411,17 +411,13 @@ async fn max_iteration_checkpoint_uses_deterministic_fallback_and_hooks() {
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let hook_calls = Arc::new(AtomicUsize::new(0));
|
||||
let hook_contexts = Arc::new(Mutex::new(Vec::new()));
|
||||
// The wrap-up model call yields nothing (empty text + no streamed deltas), so
|
||||
// `summarize_turn_wrapup` returns an empty summary and the cap path falls back
|
||||
// to `build_deterministic_checkpoint` — the exact path this test covers. (A
|
||||
// non-empty wrap-up would instead be surfaced verbatim as the answer.)
|
||||
let provider = ScriptedProvider::with_stream(
|
||||
vec![
|
||||
xml_tool_response("alpha"),
|
||||
text_response(
|
||||
"<tool_call>{\"name\":\"round24_echo\",\"arguments\":{\"value\":\"again\"}}</tool_call>",
|
||||
None,
|
||||
),
|
||||
],
|
||||
vec![ProviderDelta::TextDelta {
|
||||
delta: "checkpoint delta".to_string(),
|
||||
}],
|
||||
vec![xml_tool_response("alpha"), text_response("", None)],
|
||||
vec![],
|
||||
);
|
||||
|
||||
let mut agent = Agent::builder()
|
||||
@@ -454,7 +450,7 @@ async fn max_iteration_checkpoint_uses_deterministic_fallback_and_hooks() {
|
||||
let answer = agent.turn("hit the cap").await.unwrap();
|
||||
|
||||
assert!(answer.contains("I reached the tool-call limit for this turn (1 steps)"));
|
||||
// The unified TurnEngine digest uses `- round24_echo [ok]: ...` format (no backticks).
|
||||
// The deterministic checkpoint lists each executed tool, e.g. ``- `round24_echo` — ok``.
|
||||
assert!(answer.contains("round24_echo"));
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 1);
|
||||
wait_for_hook_calls(&hook_calls, 1).await;
|
||||
@@ -480,12 +476,11 @@ async fn max_iteration_checkpoint_uses_deterministic_fallback_and_hooks() {
|
||||
while let Ok(event) = progress_rx.try_recv() {
|
||||
streamed.push(event);
|
||||
}
|
||||
assert!(streamed.iter().any(|event| matches!(
|
||||
// The wrap-up produced no text, so no iteration-2 wrap-up delta is streamed;
|
||||
// the deterministic fallback (asserted above) becomes the answer instead.
|
||||
assert!(!streamed.iter().any(|event| matches!(
|
||||
event,
|
||||
openhuman_core::openhuman::agent::progress::AgentProgress::TextDelta {
|
||||
delta,
|
||||
iteration: 2
|
||||
} if delta == "checkpoint delta"
|
||||
openhuman_core::openhuman::agent::progress::AgentProgress::TextDelta { iteration: 2, .. }
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user