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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- `zeph vault` CLI subcommands: `init` (generate age keypair), `set` (store secret), `get` (retrieve secret), `list` (show keys), `rm` (remove secret) (#598)
- Atomic file writes for vault operations with temp+rename strategy (#598)
- Default vault directory resolution via XDG_CONFIG_HOME / APPDATA / HOME (#598)
- Auto-update check via GitHub Releases API with configurable scheduler task (#588)
- `auto_update_check` config field (default: true) with `ZEPH_AUTO_UPDATE_CHECK` env override
- `TaskKind::UpdateCheck` variant and `UpdateCheckHandler` in zeph-scheduler
- One-shot update check at startup when scheduler feature is disabled
- `--init` wizard step for auto-update check configuration

### Fixed
- Restore `--vault`, `--vault-key`, `--vault-path` CLI flags lost during clap migration (#587)
Expand Down
54 changes: 54 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ ratatui = "0.30"
regex = "1.12"
reqwest = { version = "0.13", default-features = false }
rmcp = "0.15"
semver = "1.0.27"
scrape-core = "0.2.2"
subtle = "2.6"
rubato = "0.16"
Expand All @@ -56,6 +57,7 @@ sqlx = { version = "0.8", default-features = false, features = ["macros"] }
teloxide = { version = "0.17", default-features = false, features = ["rustls", "ctrlc_handler", "macros"] }
tempfile = "3"
testcontainers = "0.27"
wiremock = "0.6.5"
thiserror = "2.0"
tokenizers = { version = "0.22", default-features = false, features = ["fancy-regex"] }
tokio = "1"
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ Automatic prompt caching for Anthropic and OpenAI providers. Repeated system pro
- **Parallel context preparation** via `try_join!` — skills, memory, code context fetched concurrently
- **Byte-length token estimation** — fast approximation without tokenizer overhead
- **Config hot-reload** — change runtime parameters without restarting the agent
- **Auto-update check** — optional daily check against GitHub releases; notification delivered to the active channel (`ZEPH_AUTO_UPDATE_CHECK=false` to disable)
- **Pipeline API** — composable, type-safe step chains for LLM calls, vector retrieval, JSON extraction, and parallel execution

[Token efficiency deep dive →](https://bug-ops.github.io/zeph/architecture/token-efficiency.html)
Expand Down Expand Up @@ -382,7 +383,7 @@ Always compiled in: `openai`, `compatible`, `orchestrator`, `router`, `self-lear
| `daemon` | Component supervisor |
| `pdf` | PDF document loading for RAG |
| `stt` | Speech-to-text via OpenAI Whisper API |
| `scheduler` | Cron-based periodic tasks |
| `scheduler` | Cron-based periodic tasks; auto-update check runs daily at 09:00 |
| `otel` | OpenTelemetry OTLP export |
| `full` | Everything above |

Expand Down
18 changes: 18 additions & 0 deletions crates/zeph-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@ Core orchestration crate for the Zeph agent. Manages the main agent loop, bootst

**Re-exports:** `Agent`

## Configuration

Key `AgentConfig` fields (TOML section `[agent]`):

| Field | Type | Default | Env override | Description |
|-------|------|---------|--------------|-------------|
| `name` | string | `"zeph"` | — | Agent display name |
| `max_tool_iterations` | usize | `10` | — | Max tool calls per turn |
| `summary_model` | string? | `null` | — | Model used for context summarization |
| `auto_update_check` | bool | `true` | `ZEPH_AUTO_UPDATE_CHECK` | Check GitHub releases for a newer version on startup / via scheduler |

```toml
[agent]
auto_update_check = true # set to false to disable update notifications
```

Set `ZEPH_AUTO_UPDATE_CHECK=false` to disable without changing the config file.

## Usage

```toml
Expand Down
23 changes: 22 additions & 1 deletion crates/zeph-core/src/agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ pub struct Agent<C: Channel, T: ToolExecutor> {
cost_tracker: Option<CostTracker>,
cached_prompt_tokens: u64,
stt: Option<Box<dyn SpeechToText>>,
update_notify_rx: Option<mpsc::Receiver<String>>,
}

impl<C: Channel, T: ToolExecutor> Agent<C, T> {
Expand Down Expand Up @@ -253,6 +254,7 @@ impl<C: Channel, T: ToolExecutor> Agent<C, T> {
cost_tracker: None,
cached_prompt_tokens: initial_prompt_tokens,
stt: None,
update_notify_rx: None,
}
}

Expand All @@ -262,6 +264,12 @@ impl<C: Channel, T: ToolExecutor> Agent<C, T> {
self
}

#[must_use]
pub fn with_update_notifications(mut self, rx: mpsc::Receiver<String>) -> Self {
self.update_notify_rx = Some(rx);
self
}

#[must_use]
pub fn with_max_tool_iterations(mut self, max: usize) -> Self {
self.runtime.max_tool_iterations = max;
Expand Down Expand Up @@ -657,6 +665,12 @@ impl<C: Channel, T: ToolExecutor> Agent<C, T> {
self.reload_config();
continue;
}
Some(msg) = recv_optional(&mut self.update_notify_rx) => {
if let Err(e) = self.channel.send(&msg).await {
tracing::warn!("failed to send update notification: {e}");
}
continue;
}
};
let Some(msg) = incoming else { break };
self.drain_channel();
Expand Down Expand Up @@ -1088,7 +1102,14 @@ async fn shutdown_signal(rx: &mut watch::Receiver<bool>) {

async fn recv_optional<T>(rx: &mut Option<mpsc::Receiver<T>>) -> Option<T> {
match rx {
Some(rx) => rx.recv().await,
Some(inner) => {
if let Some(v) = inner.recv().await {
Some(v)
} else {
*rx = None;
std::future::pending().await
}
}
None => std::future::pending().await,
}
}
Expand Down
5 changes: 5 additions & 0 deletions crates/zeph-core/src/config/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@ impl Config {
});
stt.model = v;
}
if let Ok(v) = std::env::var("ZEPH_AUTO_UPDATE_CHECK")
&& let Ok(enabled) = v.parse::<bool>()
{
self.agent.auto_update_check = enabled;
}
if let Ok(v) = std::env::var("ZEPH_A2A_ENABLED")
&& let Ok(enabled) = v.parse::<bool>()
{
Expand Down
51 changes: 50 additions & 1 deletion crates/zeph-core/src/config/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use serial_test::serial;

use super::*;

const ENV_KEYS: [&str; 49] = [
const ENV_KEYS: [&str; 50] = [
"ZEPH_LLM_PROVIDER",
"ZEPH_LLM_BASE_URL",
"ZEPH_LLM_MODEL",
Expand Down Expand Up @@ -55,6 +55,7 @@ const ENV_KEYS: [&str; 49] = [
"ZEPH_INDEX_REPO_MAP_TOKENS",
"ZEPH_STT_PROVIDER",
"ZEPH_STT_MODEL",
"ZEPH_AUTO_UPDATE_CHECK",
];

fn clear_env() {
Expand Down Expand Up @@ -2367,3 +2368,51 @@ fn env_override_stt_provider_only() {
assert_eq!(stt.provider, "whisper");
assert_eq!(stt.model, "whisper-1");
}

#[test]
fn config_default_auto_update_check_is_true() {
let config = Config::default();
assert!(config.agent.auto_update_check);
}

#[test]
#[serial]
fn env_override_auto_update_check_false() {
clear_env();
let mut config = Config::default();
assert!(config.agent.auto_update_check);

unsafe { std::env::set_var("ZEPH_AUTO_UPDATE_CHECK", "false") };
config.apply_env_overrides();
unsafe { std::env::remove_var("ZEPH_AUTO_UPDATE_CHECK") };

assert!(!config.agent.auto_update_check);
}

#[test]
#[serial]
fn env_override_auto_update_check_true() {
clear_env();
let mut config = Config::default();
config.agent.auto_update_check = false;

unsafe { std::env::set_var("ZEPH_AUTO_UPDATE_CHECK", "true") };
config.apply_env_overrides();
unsafe { std::env::remove_var("ZEPH_AUTO_UPDATE_CHECK") };

assert!(config.agent.auto_update_check);
}

#[test]
#[serial]
fn env_override_auto_update_check_invalid_ignored() {
clear_env();
let mut config = Config::default();
assert!(config.agent.auto_update_check);

unsafe { std::env::set_var("ZEPH_AUTO_UPDATE_CHECK", "not-a-bool") };
config.apply_env_overrides();
unsafe { std::env::remove_var("ZEPH_AUTO_UPDATE_CHECK") };

assert!(config.agent.auto_update_check);
}
8 changes: 8 additions & 0 deletions crates/zeph-core/src/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,19 @@ fn default_max_tool_iterations() -> usize {
10
}

fn default_auto_update_check() -> bool {
true
}

#[derive(Debug, Deserialize, Serialize)]
pub struct AgentConfig {
pub name: String,
#[serde(default = "default_max_tool_iterations")]
pub max_tool_iterations: usize,
#[serde(default)]
pub summary_model: Option<String>,
#[serde(default = "default_auto_update_check")]
pub auto_update_check: bool,
}

/// LLM provider backend selector.
Expand Down Expand Up @@ -984,6 +990,7 @@ impl Default for Config {
name: "Zeph".into(),
max_tool_iterations: 10,
summary_model: None,
auto_update_check: default_auto_update_check(),
},
llm: LlmConfig {
provider: ProviderKind::Ollama,
Expand Down Expand Up @@ -1055,5 +1062,6 @@ mod tests {
assert_eq!(back.memory.sqlite_path, config.memory.sqlite_path);
assert_eq!(back.memory.history_limit, config.memory.history_limit);
assert_eq!(back.vault.backend, config.vault.backend);
assert_eq!(back.agent.auto_update_check, config.agent.auto_update_check);
}
}
5 changes: 4 additions & 1 deletion crates/zeph-scheduler/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@ repository.workspace = true
[dependencies]
chrono.workspace = true
cron = "0.15"
reqwest = { workspace = true, features = ["json", "rustls"] }
semver.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] }
thiserror.workspace = true
tokio = { workspace = true, features = ["sync", "time"] }
tokio = { workspace = true, features = ["macros", "sync", "time"] }
tracing.workspace = true

[dev-dependencies]
tempfile.workspace = true
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
wiremock.workspace = true

[lints]
workspace = true
Loading
Loading