Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/ci-lite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,11 +231,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.
Expand Down
14 changes: 13 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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"]
Expand Down
12 changes: 11 additions & 1 deletion app/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 2 additions & 0 deletions src/openhuman/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/openhuman/tools/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,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(
Comment thread
M3gA-Mind marked this conversation as resolved.
root_config,
action_dir,
Expand Down
34 changes: 34 additions & 0 deletions src/openhuman/tools/ops_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
27 changes: 11 additions & 16 deletions tests/raw_coverage/agent_session_round24_raw_coverage_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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;
Expand All @@ -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, .. }
)));
}

Expand Down
Loading