Skip to content

Commit f5fcd23

Browse files
committed
Merge remote-tracking branch 'origin/main'
2 parents 467adc9 + 4adac67 commit f5fcd23

File tree

21 files changed

+616
-103
lines changed

21 files changed

+616
-103
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

77
## [Unreleased]
88

9+
## [0.12.5] - 2026-03-02
10+
911
### Added
1012

13+
- `load_skill` tool in `zeph-core`: LLM can call `load_skill(skill_name)` at inference time to retrieve the full body of any registered skill by name. Non-TOP skills appear in the system prompt as metadata-only catalog entries; this tool enables on-demand access to their full instructions without expanding the system prompt (#1125)
14+
1115
- Provider instruction file loader (`InstructionLoader`) in `zeph-core`: auto-detects `CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, and `zeph.md` from the working directory and injects them into the system prompt with path-traversal protection (symlink boundary check, null byte guard, 256 KiB size cap) (#1122)
1216

1317
### Fixed
@@ -1578,7 +1582,8 @@ let agent = Agent::new(provider, channel, &skills_prompt, executor);
15781582
- Agent calls channel.send_typing() before each LLM request
15791583
- Agent::run() uses tokio::select! to race channel messages against shutdown signal
15801584

1581-
[Unreleased]: https://github.com/bug-ops/zeph/compare/v0.12.4...HEAD
1585+
[Unreleased]: https://github.com/bug-ops/zeph/compare/v0.12.5...HEAD
1586+
[0.12.5]: https://github.com/bug-ops/zeph/compare/v0.12.4...v0.12.5
15821587
[0.12.4]: https://github.com/bug-ops/zeph/compare/v0.12.3...v0.12.4
15831588
[0.12.3]: https://github.com/bug-ops/zeph/compare/v0.12.2...v0.12.3
15841589
[0.12.2]: https://github.com/bug-ops/zeph/compare/v0.12.1...v0.12.2

Cargo.lock

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ resolver = "3"
55
[workspace.package]
66
edition = "2024"
77
rust-version = "1.88"
8-
version = "0.12.4"
8+
version = "0.12.5"
99
authors = ["bug-ops"]
1010
license = "MIT"
1111
repository = "https://github.com/bug-ops/zeph"
@@ -108,19 +108,19 @@ url = "2.5"
108108
uuid = "1.21"
109109
wiremock = "0.6.5"
110110
zeroize = { version = "1", features = ["derive", "serde"] }
111-
zeph-a2a = { path = "crates/zeph-a2a", version = "0.12.4" }
112-
zeph-acp = { path = "crates/zeph-acp", version = "0.12.4" }
113-
zeph-channels = { path = "crates/zeph-channels", version = "0.12.4" }
114-
zeph-core = { path = "crates/zeph-core", version = "0.12.4" }
115-
zeph-gateway = { path = "crates/zeph-gateway", version = "0.12.4" }
116-
zeph-index = { path = "crates/zeph-index", version = "0.12.4" }
117-
zeph-llm = { path = "crates/zeph-llm", version = "0.12.4" }
118-
zeph-mcp = { path = "crates/zeph-mcp", version = "0.12.4" }
119-
zeph-memory = { path = "crates/zeph-memory", version = "0.12.4" }
120-
zeph-scheduler = { path = "crates/zeph-scheduler", version = "0.12.4" }
121-
zeph-skills = { path = "crates/zeph-skills", version = "0.12.4" }
122-
zeph-tools = { path = "crates/zeph-tools", version = "0.12.4" }
123-
zeph-tui = { path = "crates/zeph-tui", version = "0.12.4" }
111+
zeph-a2a = { path = "crates/zeph-a2a", version = "0.12.5" }
112+
zeph-acp = { path = "crates/zeph-acp", version = "0.12.5" }
113+
zeph-channels = { path = "crates/zeph-channels", version = "0.12.5" }
114+
zeph-core = { path = "crates/zeph-core", version = "0.12.5" }
115+
zeph-gateway = { path = "crates/zeph-gateway", version = "0.12.5" }
116+
zeph-index = { path = "crates/zeph-index", version = "0.12.5" }
117+
zeph-llm = { path = "crates/zeph-llm", version = "0.12.5" }
118+
zeph-mcp = { path = "crates/zeph-mcp", version = "0.12.5" }
119+
zeph-memory = { path = "crates/zeph-memory", version = "0.12.5" }
120+
zeph-scheduler = { path = "crates/zeph-scheduler", version = "0.12.5" }
121+
zeph-skills = { path = "crates/zeph-skills", version = "0.12.5" }
122+
zeph-tools = { path = "crates/zeph-tools", version = "0.12.5" }
123+
zeph-tui = { path = "crates/zeph-tui", version = "0.12.5" }
124124

125125
[workspace.lints.rust]
126126
unsafe_code = "deny"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ zeph # start the agent
3333
| Feature | Description |
3434
|---|---|
3535
| **Hybrid inference** | Ollama, Claude, OpenAI, any OpenAI-compatible API, or fully local via Candle (GGUF). Multi-model orchestrator with fallback chains and EMA latency routing. [→ Providers](https://bug-ops.github.io/zeph/concepts/providers.html) |
36-
| **Skills-first architecture** | YAML+Markdown skill files with BM25+cosine hybrid retrieval. Bayesian re-ranking, 4-tier trust model, and self-learning evolution — skills improve from real usage. [→ Skills](https://bug-ops.github.io/zeph/concepts/skills.html) · [→ Self-learning](https://bug-ops.github.io/zeph/advanced/self-learning.html) |
36+
| **Skills-first architecture** | YAML+Markdown skill files with BM25+cosine hybrid retrieval. Bayesian re-ranking, 4-tier trust model, and self-learning evolution — skills improve from real usage. The `load_skill` tool lets the LLM fetch the full body of any skill outside the active TOP-N set on demand. [→ Skills](https://bug-ops.github.io/zeph/concepts/skills.html) · [→ Self-learning](https://bug-ops.github.io/zeph/advanced/self-learning.html) |
3737
| **Context engineering** | Semantic skill selection, command-aware output filters, tool-pair summarization, and reactive middle-out compaction keep the window efficient under any load. [→ Context](https://bug-ops.github.io/zeph/advanced/context.html) |
3838
| **Semantic memory** | SQLite + Qdrant with MMR re-ranking, temporal decay, cross-session recall, implicit correction detection, and credential scrubbing. [→ Memory](https://bug-ops.github.io/zeph/concepts/memory.html) |
3939
| **IDE integration (ACP)** | Stdio, HTTP+SSE, or WebSocket transport. Session modes, live tool streaming, LSP diagnostics injection, file following, usage reporting. Works in Zed, Helix, VS Code. [→ ACP](https://bug-ops.github.io/zeph/advanced/acp.html) |

crates/zeph-core/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Core orchestration crate for the Zeph agent. Manages the main agent loop, bootst
3535
| `redact` | Regex-based secret redaction (AWS, OpenAI, Anthropic, Google, GitLab, HuggingFace, npm, Docker) |
3636
| `vault` | Secret storage and resolution via vault providers (age-encrypted read/write); secrets stored as `BTreeMap` for deterministic JSON serialization on every `vault.save()` call; scans `ZEPH_SECRET_*` keys to build the custom-secrets map used by skill env injection; all secret values are held as `Zeroizing<String>` (zeroize-on-drop) and are not `Clone` |
3737
| `instructions` | `load_instructions()` — auto-detects and loads provider-specific instruction files (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `zeph.md`) from the working directory; injects content into the volatile system prompt section with symlink boundary check, null byte guard, and 256 KiB per-file size cap |
38+
| `skill_loader` | `SkillLoaderExecutor``ToolExecutor` that exposes the `load_skill` tool to the LLM; accepts a skill name, looks it up in the shared `Arc<RwLock<SkillRegistry>>`, and returns the full SKILL.md body (truncated to `MAX_TOOL_OUTPUT_CHARS`); skill name is capped at 128 characters; unknown names return a human-readable error message rather than a hard error |
3839
| `hash` | `content_hash` — BLAKE3 hex digest utility |
3940
| `pipeline` | Composable, type-safe step chains for multi-stage workflows |
4041
| `subagent` | Sub-agent orchestration: `SubAgentManager` lifecycle with background execution, `SubAgentDef` TOML definitions, `PermissionGrants` zero-trust delegation, `FilteredToolExecutor` scoped tool access, A2A in-process channels, `SubAgentState` lifecycle enum (`Submitted`, `Working`, `Completed`, `Failed`, `Canceled`), real-time status tracking |

crates/zeph-core/src/agent/builder.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,19 @@ impl<C: Channel> Agent<C> {
169169
self
170170
}
171171

172+
/// # Panics
173+
///
174+
/// Panics if the registry `RwLock` is poisoned.
172175
#[must_use]
173176
pub fn with_hybrid_search(mut self, enabled: bool) -> Self {
174177
self.skill_state.hybrid_search = enabled;
175178
if enabled {
176-
let all_meta = self.skill_state.registry.all_meta();
179+
let reg = self
180+
.skill_state
181+
.registry
182+
.read()
183+
.expect("registry read lock");
184+
let all_meta = reg.all_meta();
177185
let descs: Vec<&str> = all_meta.iter().map(|m| m.description.as_str()).collect();
178186
self.skill_state.bm25_index = Some(zeph_skills::bm25::Bm25Index::build(&descs));
179187
}
@@ -309,11 +317,20 @@ impl<C: Channel> Agent<C> {
309317
self
310318
}
311319

320+
/// # Panics
321+
///
322+
/// Panics if the registry `RwLock` is poisoned.
312323
#[must_use]
313324
pub fn with_metrics(mut self, tx: watch::Sender<MetricsSnapshot>) -> Self {
314325
let provider_name = self.provider.name().to_string();
315326
let model_name = self.runtime.model_name.clone();
316-
let total_skills = self.skill_state.registry.all_meta().len();
327+
let total_skills = self
328+
.skill_state
329+
.registry
330+
.read()
331+
.expect("registry read lock")
332+
.all_meta()
333+
.len();
317334
let qdrant_available = false;
318335
let conversation_id = self.memory_state.conversation_id;
319336
let prompt_estimate = self

crates/zeph-core/src/agent/context.rs

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,7 +1372,17 @@ impl<C: Channel> Agent<C> {
13721372

13731373
#[allow(clippy::too_many_lines)]
13741374
pub(super) async fn rebuild_system_prompt(&mut self, query: &str) {
1375-
let all_meta = self.skill_state.registry.all_meta();
1375+
let all_meta: Vec<zeph_skills::loader::SkillMeta> = self
1376+
.skill_state
1377+
.registry
1378+
.read()
1379+
.expect("registry read lock")
1380+
.all_meta()
1381+
.into_iter()
1382+
.cloned()
1383+
.collect();
1384+
let all_meta_refs: Vec<&zeph_skills::loader::SkillMeta> = all_meta.iter().collect();
1385+
let all_meta = all_meta_refs;
13761386
let matched_indices: Vec<usize> = if let Some(matcher) = &self.skill_state.matcher {
13771387
let provider = self.provider.clone();
13781388
let _ = self.channel.send_status("matching skills...").await;
@@ -1510,19 +1520,25 @@ impl<C: Channel> Agent<C> {
15101520
}
15111521
self.update_skill_confidence_metrics().await;
15121522

1513-
let all_skills: Vec<Skill> = self
1514-
.skill_state
1515-
.registry
1516-
.all_meta()
1517-
.iter()
1518-
.filter_map(|m| self.skill_state.registry.get_skill(&m.name).ok())
1519-
.collect();
1520-
let active_skills: Vec<Skill> = self
1521-
.skill_state
1522-
.active_skill_names
1523-
.iter()
1524-
.filter_map(|name| self.skill_state.registry.get_skill(name).ok())
1525-
.collect();
1523+
let (all_skills, active_skills): (Vec<Skill>, Vec<Skill>) = {
1524+
let reg = self
1525+
.skill_state
1526+
.registry
1527+
.read()
1528+
.expect("registry read lock");
1529+
let all: Vec<Skill> = reg
1530+
.all_meta()
1531+
.iter()
1532+
.filter_map(|m| reg.get_skill(&m.name).ok())
1533+
.collect();
1534+
let active: Vec<Skill> = self
1535+
.skill_state
1536+
.active_skill_names
1537+
.iter()
1538+
.filter_map(|name| reg.get_skill(name).ok())
1539+
.collect();
1540+
(all, active)
1541+
};
15261542
let remaining_skills: Vec<Skill> = all_skills
15271543
.iter()
15281544
.filter(|s| {

crates/zeph-core/src/agent/learning.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,13 @@ impl<C: Channel> Agent<C> {
179179
return Ok(false);
180180
}
181181

182-
let Ok(skill) = self.skill_state.registry.get_skill(&name) else {
182+
let Ok(skill) = self
183+
.skill_state
184+
.registry
185+
.read()
186+
.expect("registry read lock")
187+
.get_skill(&name)
188+
else {
183189
return Ok(false);
184190
};
185191

@@ -246,7 +252,12 @@ impl<C: Channel> Agent<C> {
246252
return Ok(());
247253
};
248254

249-
let skill = self.skill_state.registry.get_skill(skill_name)?;
255+
let skill = self
256+
.skill_state
257+
.registry
258+
.read()
259+
.expect("registry read lock")
260+
.get_skill(skill_name)?;
250261

251262
memory
252263
.sqlite()
@@ -598,7 +609,14 @@ impl<C: Channel> Agent<C> {
598609
return Ok(());
599610
};
600611
// SEC-PH1-001: validate skill exists in registry before writing to DB
601-
if self.skill_state.registry.get_skill(name).is_err() {
612+
if self
613+
.skill_state
614+
.registry
615+
.read()
616+
.expect("registry read lock")
617+
.get_skill(name)
618+
.is_err()
619+
{
602620
self.channel
603621
.send(&format!("Unknown skill: \"{name}\"."))
604622
.await?;

0 commit comments

Comments
 (0)