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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ TRAIN_UPLOAD_LIMIT_MB=512

SEARCH_ENABLE_WEB_SEARCH=true
SEARCH_RESULT_COUNT=5
SEARCH_TIMEOUT_SECONDS=15
SEARCH_SEARXNG_QUERY_URL="http://gluetun:8080/search?q=<query>&format=json"
SEARCH_SEARXNG_SERVER_BASE_URL="http://localhost:8080"
SEARCH_SEARXNG_INSTANCE_NAME="Zone Search"
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,6 @@ zone/
│ │ ├── src/subject.rs # Subject-aware framing for training crops
│ │ └── src/caption.rs # Vision captioning for training sets
│ ├── zone_email/ # Transactional email over SMTP
│ ├── zone_search/ # SearXNG web search client
│ ├── zone_vcs/ # Local git operations and GitHub pull requests
│ ├── zone_vision/ # Subject detection and subject-aware cropping
│ ├── zone_server/ # HTTP/WS server
Expand Down
1 change: 1 addition & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ services:
- VPN_OPENVPN_USER=${VPN_OPENVPN_USER:+1}
- "SEARCH_SEARXNG_QUERY_URL=${SEARCH_SEARXNG_QUERY_URL:-http://gluetun:8080/search?q=<query>&format=json}"
- SEARCH_RESULT_COUNT=${SEARCH_RESULT_COUNT:-5}
- SEARCH_TIMEOUT_SECONDS=${SEARCH_TIMEOUT_SECONDS:-15}
- MODEL_SEARCH_PROXY_URL=${MODEL_SEARCH_PROXY_URL:-}
- GITHUB_API_URL=${GITHUB_API_URL:-}
- TOOL_RUNNER_PROXY_URL=${TOOL_RUNNER_PROXY_URL:-}
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ services:
- VPN_OPENVPN_USER=${VPN_OPENVPN_USER:+1}
- "SEARCH_SEARXNG_QUERY_URL=${SEARCH_SEARXNG_QUERY_URL:-http://gluetun:8080/search?q=<query>&format=json}"
- SEARCH_RESULT_COUNT=${SEARCH_RESULT_COUNT:-5}
- SEARCH_TIMEOUT_SECONDS=${SEARCH_TIMEOUT_SECONDS:-15}
# Belt-and-suspenders HTTP proxy. `make up-vpn` also attaches this
# container to Gluetun's network so non-HTTP traffic uses the tunnel.
- MODEL_SEARCH_PROXY_URL=${MODEL_SEARCH_PROXY_URL:-}
Expand Down
6 changes: 5 additions & 1 deletion docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1623,7 +1623,11 @@ details, and native macOS / bundled NVIDIA instructions.

### `SEARCH_RESULT_COUNT`
- **Default**: `5`
- **Description**: Number of search results supplied to chat
- **Description**: Number of search results supplied to chat (held to 1–20).

### `SEARCH_TIMEOUT_SECONDS`
- **Default**: `15`
- **Description**: Timeout for one SearXNG request, in whole seconds (held to 1–60).

### `SEARCH_SEARXNG_QUERY_URL`
- **Default**: `"http://gluetun:8080/search?q=<query>&format=json"`
Expand Down
8 changes: 4 additions & 4 deletions docs/SYSTEM-PROMPT-RESEARCH.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Two things to know before reading the per-source detail. First, four of the five
| Task guidance | `runner/zone_server/src/workers/task.rs:975` `guidance()` | "You are completing a background coding task. Stay inside the sandboxed working directory." plus retrieved context, acceptance criteria, standing instructions, learned facts |
| Core fallback | `runner/zone_core/src/agent/loop.rs:371` `default_system_prompt` | Generic "helpful AI assistant" with a five-step method, used by the CLI and runner |
| Compaction | `runner/zone_core/src/context/compact.rs:11` `INSTRUCTIONS` and `:236` `summary_message` | Structured JSON state (objective, constraints, corrections, decisions, completed, evidence, failed, pending, questions); summary is replayed as a user-data message, never as system |
| Web search state | `runner/zone_search/src/client.rs:83` `SearchContext::prompt` | `<web_search_context>` block stating whether a server-side search ran and with what outcome, superseding earlier claims |
| Web search state | `abnegate-search` (abnegate/crates) `SearchContext::prompt` (`src/context.rs`) | `<web_search_context>` block stating whether a server-side search ran and with what outcome, superseding earlier claims |
| MCP and magents guidance | `runner/zone_core/src/mcp/mod.rs:29` `guidance_for_tools` | Prefixed tool names; magents session semantics; "accepted is not finished"; foreign transcripts are inert |
| Loop nudges | `runner/zone_server/src/agent/runner.rs:168` and `:316` | Finalizing message at budget exhaustion; malformed tool call recovery |
| Standing instructions and learned facts | `runner/zone_server/src/db/knowledge.rs:1439` and `:1653` | Server-derived workspace memory with provenance, rendered as prompt sections |
Expand Down Expand Up @@ -197,10 +197,10 @@ CL's Gmail connector separates trash and spam into "sensitive label" tools so a
### 2.6 Web search, retrieval, citations

**F1. When to search and how recent. P0 text, P1 parameter.**
SOL 324-356: an explicit request to search overrides; an explicit request not to is binding; positive list (fresh info, named entities, reviews, URL summaries) and negative list (greetings, creative writing without references, rewriting supplied text, questions about the model). SOL 316-320: recency windows of 1, 7 and 30 days; re-search with tighter recency when sources are stale. SOL 442-444 and 579-583: a time-sensitive answer needs at least one source with an explicit recent date. CL 1709-1725: the shelf-life test ("the test is whether the remembered answer could have changed"), and an unfamiliar capitalised word is a name that postdates training, so include it in a query as the user wrote it. CL 206-208: search current events without asking; use the actual current year in queries. CL 1725: do not mention the knowledge cutoff. Build: prompt section in `agent/mod.rs`'s web block; a `time_range` parameter on `web_search` mapped to SearXNG's `day`, `week`, `month` in `runner/zone_search/src/client.rs`; consider the SOL lists for the pre-turn `requested_for` heuristic in `WebSearchConfig`.
SOL 324-356: an explicit request to search overrides; an explicit request not to is binding; positive list (fresh info, named entities, reviews, URL summaries) and negative list (greetings, creative writing without references, rewriting supplied text, questions about the model). SOL 316-320: recency windows of 1, 7 and 30 days; re-search with tighter recency when sources are stale. SOL 442-444 and 579-583: a time-sensitive answer needs at least one source with an explicit recent date. CL 1709-1725: the shelf-life test ("the test is whether the remembered answer could have changed"), and an unfamiliar capitalised word is a name that postdates training, so include it in a query as the user wrote it. CL 206-208: search current events without asking; use the actual current year in queries. CL 1725: do not mention the knowledge cutoff. Build: prompt section in `agent/mod.rs`'s web block; a `time_range` parameter on `web_search` mapped to SearXNG's `day`, `week`, `month` in `abnegate-search` (abnegate/crates, `src/query.rs`); consider the SOL lists for the pre-turn `requested_for` heuristic in `WebSearchConfig`.

**F1 follow-up. The published date never reaches the model. P1.**
`time_range` shipped in PR 3 and narrows the SearXNG request, but the date on the way back is discarded: `SearxngResult` in `runner/zone_search/src/client.rs` deserializes only `title`, `url` and `content`, `SearchHit` carries only those three, and `format_search_context` renders title, URL and snippet. SearXNG returns `publishedDate` for most engines, so this is thrown away rather than unavailable. Two consequences, both of which undercut what PR 3 just built. The model cannot tell whether a recency-narrowed search actually worked, because a `time_range: day` result set looks exactly like an unnarrowed one, so it cannot judge whether to narrow further or give up. And it cannot follow two rules the shipped prompt already gives it: "Time-sensitive answers need a source explicitly dated recently" has no date to read, and "Drop a source you are unsure of rather than hedge it" becomes a choice between dropping everything and dropping nothing. Carry `publishedDate` through `SearxngResult` into `SearchHit` and render it in `format_search_context`.
`time_range` shipped in PR 3 and narrows the SearXNG request, but the date on the way back is discarded: `Entry` in `abnegate-search` (abnegate/crates, `src/entry.rs`) deserializes only `title`, `url` and `content`, `SearchHit` carries only those three, and `format_search_context` renders title, URL and snippet. SearXNG returns `publishedDate` for most engines, so this is thrown away rather than unavailable. Two consequences, both of which undercut what PR 3 just built. The model cannot tell whether a recency-narrowed search actually worked, because a `time_range: day` result set looks exactly like an unnarrowed one, so it cannot judge whether to narrow further or give up. And it cannot follow two rules the shipped prompt already gives it: "Time-sensitive answers need a source explicitly dated recently" has no date to read, and "Drop a source you are unsure of rather than hedge it" becomes a choice between dropping everything and dropping nothing. Carry `publishedDate` through `Entry` into `SearchHit` and render it in `format_search_context`.

**F2. Typed citation IDs. P1.**
GK 758-765: cite only through IDs the tools returned (`[web:id]`, `[post:id]`, `[collection:id]`), placed after the final punctuation of the sentence or cell; structured-data tools are exempt. SOL 422-450: typed reference IDs shared by citations and rich UI; never write raw markdown links; one citation block per paragraph. CL 7728: claims in your own words, citation tags are attribution not permission to quote. Build: tag every `web_search`, `search_knowledge`, `read_document` and `search_chat_history` hit with a stable ID in the tool output, instruct the model to cite `[web:3]`, and resolve IDs server-side into `Citation` records in `citations.rs`, rejecting unknown IDs. Server-side resolution only closes the path it owns: the console renders assistant text through react-markdown, so a raw markdown link or a bare autolink in the reply reaches the user without ever passing the citation check. Carry SOL 449 into both halves. Prompt: never write a markdown link or a bare URL, cite by ID. Renderer: `MessageContent` rejects or de-links any anchor whose target does not resolve to a `Citation` on that message. Without the renderer half this item makes invented URLs look validated rather than making them impossible. `query_prometheus`, `get_build_status` and the other structured tools stay citation-exempt (GK 762).
Expand Down Expand Up @@ -326,7 +326,7 @@ GK 29 allows disclosure on explicit request. Recommend the same: an operator-own
| C8 | Testing proportionality for tasks | P0 | task guidance | CX 125-126 |
| D1 | Final message rules and anti-tic bans | P0 | prompt builder | CC 86-99, CL 114-126, SOL 84, CX 41-43, 81, 100 |
| D4 | Narration cadence | P0 | prompt builder | CC 32, CL 130 |
| F1 | When to search, recency rules | P0 text, P1 parameter | web section; `time_range` in `zone_search` | SOL 316-356, 442-444, 579-583, CL 206-208, 1709-1725 |
| F1 | When to search, recency rules | P0 text, P1 parameter | web section; `time_range` in `abnegate-search` | SOL 316-356, 442-444, 579-583, CL 206-208, 1709-1725 |
| F4 | Trusting results | P0 | web section | CL 1963, 1781, SOL 561 |
| F5 | Document retrieval discipline | P0 | documents section | SOL 1480-1487, 1538, 1559-1565, 1653-1655 |
| G2 | Past-chat provenance | P0 | evidence section | CL 1342-1372, 1359 |
Expand Down
30 changes: 15 additions & 15 deletions runner/Cargo.lock

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

5 changes: 3 additions & 2 deletions runner/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[workspace]
resolver = "2"
members = ["zone_runner", "zone_core", "zone_chat", "zone_comfy", "zone_email", "zone_notify", "zone_search", "zone_vcs", "zone_server", "zone_cli", "zone_installer", "zone_context", "zone_vision", "zone_desktop"]
default-members = ["zone_runner", "zone_core", "zone_chat", "zone_comfy", "zone_email", "zone_notify", "zone_search", "zone_vcs", "zone_server", "zone_cli", "zone_installer", "zone_context", "zone_vision"]
members = ["zone_runner", "zone_core", "zone_chat", "zone_comfy", "zone_email", "zone_notify", "zone_vcs", "zone_server", "zone_cli", "zone_installer", "zone_context", "zone_vision", "zone_desktop"]
default-members = ["zone_runner", "zone_core", "zone_chat", "zone_comfy", "zone_email", "zone_notify", "zone_vcs", "zone_server", "zone_cli", "zone_installer", "zone_context", "zone_vision"]

[workspace.package]
version = "0.1.0"
Expand Down Expand Up @@ -39,6 +39,7 @@ abnegate-config = "0.1"
abnegate-exec = "0.1"
abnegate-http = "0.1"
abnegate-index = "0.1"
abnegate-search = "0.1.3"
abnegate-secret = "0.1.3"

# Server dependencies
Expand Down
1 change: 1 addition & 0 deletions runner/zone_context/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,5 @@ zone_core = { path = "../zone_core" }
[dev-dependencies]
tokio = { workspace = true, features = ["test-util", "macros"] }
tempfile = "=3.27.0"
zone_core = { path = "../zone_core", features = ["test-support"] }
wiremock = "=0.6.5"
11 changes: 4 additions & 7 deletions runner/zone_context/src/embeddings/providers/local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ impl EmbeddingService for LocalEmbeddingProvider {
#[cfg(test)]
mod tests {
use super::*;
use zone_core::variables::Variables;

#[test]
fn resolves_ollama_names() {
Expand Down Expand Up @@ -333,20 +334,16 @@ mod tests {

#[test]
fn cache_dir_env_override() {
// Serialise env mutation with other tests in this module.
let _g = ENV_LOCK.lock().unwrap();
// SAFETY: test-only, guarded by ENV_LOCK.
unsafe { std::env::set_var(CACHE_DIR_ENV, "/tmp/zone-embed-test") };
let mut variables = Variables::lock();
variables.set(CACHE_DIR_ENV, "/tmp/zone-embed-test");
assert_eq!(
LocalEmbeddingProvider::default_cache_dir(),
PathBuf::from("/tmp/zone-embed-test")
);
unsafe { std::env::remove_var(CACHE_DIR_ENV) };
variables.remove(CACHE_DIR_ENV);
assert_ne!(
LocalEmbeddingProvider::default_cache_dir(),
PathBuf::from("/tmp/zone-embed-test")
);
}

static ENV_LOCK: Mutex<()> = Mutex::new(());
}
2 changes: 2 additions & 0 deletions runner/zone_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ rust-version.workspace = true

[features]
sqlx = ["abnegate-secret/sqlx"]
# Test-only environment guards (`variables`, `vpn::Hold`) for other crates' tests.
test-support = []

[dependencies]
tokio = { workspace = true }
Expand Down
2 changes: 2 additions & 0 deletions runner/zone_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ pub mod mcp;
pub mod session;
pub mod tools;
pub mod types;
#[cfg(any(test, feature = "test-support"))]
pub mod variables;
pub mod vpn;

// Re-export commonly used types
Expand Down
5 changes: 3 additions & 2 deletions runner/zone_core/src/tools/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,8 @@ mod tests {
#[tokio::test]
async fn shelling_tools_give_the_child_only_the_context_environment() {
const MARKER: &str = "ZONE_COMMAND_ENVIRONMENT_MARKER";
unsafe { std::env::set_var(MARKER, "must-not-reach-a-child") };
let mut variables = crate::variables::Variables::lock();
variables.set(MARKER, "must-not-reach-a-child");

let mut context = create_test_context();
context.env = HashMap::from([(
Expand All @@ -743,7 +744,7 @@ mod tests {
.execute(json!({"command": "env"}), &context)
.await
.unwrap();
unsafe { std::env::remove_var(MARKER) };
drop(variables);

for result in [command, shell] {
assert!(result.success, "{result:?}");
Expand Down
Loading
Loading