feat: first cut at dev brain
This commit is contained in:
169
Cargo.lock
generated
169
Cargo.lock
generated
@@ -621,6 +621,20 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "5.4.1"
|
||||
@@ -1059,6 +1073,27 @@ dependencies = [
|
||||
"crypto-common 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch"
|
||||
version = "0.2.0"
|
||||
@@ -1144,6 +1179,7 @@ dependencies = [
|
||||
"serde_norway",
|
||||
"sha2",
|
||||
"time",
|
||||
"turbovault-parser",
|
||||
"ureq",
|
||||
"url",
|
||||
]
|
||||
@@ -2856,6 +2892,30 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iced"
|
||||
version = "0.14.0"
|
||||
@@ -3034,7 +3094,7 @@ dependencies = [
|
||||
"iced_renderer",
|
||||
"log",
|
||||
"num-traits",
|
||||
"pulldown-cmark",
|
||||
"pulldown-cmark 0.12.2",
|
||||
"rustc-hash 2.1.3",
|
||||
"thiserror 2.0.19",
|
||||
"unicode-segmentation",
|
||||
@@ -3471,6 +3531,12 @@ dependencies = [
|
||||
"x11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libyaml-rs"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e126dda6f34391ab7b444f9922055facc83c07a910da3eb16f1e4d9c45dc777"
|
||||
|
||||
[[package]]
|
||||
name = "lilt"
|
||||
version = "0.8.1"
|
||||
@@ -4142,6 +4208,12 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "orbclient"
|
||||
version = "0.3.55"
|
||||
@@ -4501,6 +4573,19 @@ dependencies = [
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark"
|
||||
version = "0.13.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"getopts",
|
||||
"memchr",
|
||||
"pulldown-cmark-escape",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark-escape"
|
||||
version = "0.11.0"
|
||||
@@ -4654,6 +4739,17 @@ dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
"libredox",
|
||||
"thiserror 2.0.19",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.13.1"
|
||||
@@ -5051,6 +5147,15 @@ version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
|
||||
|
||||
[[package]]
|
||||
name = "shellexpand"
|
||||
version = "3.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
@@ -5583,7 +5688,20 @@ version = "1.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"pin-project-lite",
|
||||
"tokio-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5724,6 +5842,41 @@ dependencies = [
|
||||
"core_maths",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "turbovault-core"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8c68b413757ee3845f5b5182c8643e16430de683af4588d005ed2f9b21f84be"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"shellexpand",
|
||||
"thiserror 2.0.19",
|
||||
"tokio",
|
||||
"uuid",
|
||||
"winnow 1.0.4",
|
||||
"yaml_serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "turbovault-parser"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c153db2ca08515d7bfb1b2c31479710f41d39f0c0e1ddfe75d2abc32cd9c37fa"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pulldown-cmark 0.13.4",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"turbovault-core",
|
||||
"yaml_serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "two-face"
|
||||
version = "0.4.5"
|
||||
@@ -5946,6 +6099,7 @@ version = "1.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
|
||||
dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"js-sys",
|
||||
"serde_core",
|
||||
"wasm-bindgen",
|
||||
@@ -6870,6 +7024,19 @@ dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaml_serde"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08c7c1b1a6a7c8a6b2741a6c21a4f8918e51899b111cfa08d1288202656e3975"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"libyaml-rs",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yazi"
|
||||
version = "0.2.1"
|
||||
|
||||
@@ -26,6 +26,7 @@ serde_json = { version = "1.0.149", features = ["preserve_order", "raw_value"] }
|
||||
serde_norway = "0.9.42"
|
||||
sha2 = "0.11.0"
|
||||
time = { version = "0.3.54", features = ["formatting", "parsing"] }
|
||||
turbovault-parser = "1.6.0"
|
||||
ureq = { version = "3.3.0", default-features = false, features = ["rustls"] }
|
||||
url = "2.5.8"
|
||||
|
||||
|
||||
126
PLAN.md
126
PLAN.md
@@ -3,129 +3,3 @@
|
||||
Only unfinished implementation work belongs here. DwarfStar remains the
|
||||
behavioral oracle for model execution, token processing, context accounting,
|
||||
KV-cache behavior, the HTTP API, and the built-in agent loop.
|
||||
|
||||
## Dev Brain: a project-backed LLM Wiki
|
||||
|
||||
Dev Brain implements the LLM Wiki idea described by Andrej Karpathy. The
|
||||
registered projects remain the authoritative, changing source material. A
|
||||
user-selected Obsidian vault is the derived, human-readable body of knowledge
|
||||
that the agent maintains from those projects. Its purpose is to compile durable
|
||||
knowledge about architecture, behavior, decisions, invariants, workflows, and
|
||||
relationships once, keep it aligned as the projects change, and make it cheap
|
||||
for later agents to retrieve the relevant context.
|
||||
|
||||
This is not a general Markdown memory store. The vault contains coherent topic
|
||||
pages with explicit provenance, links, and freshness state. Queries use the
|
||||
compiled wiki when it is verified and return to project sources when knowledge
|
||||
is missing or stale. The vault remains ordinary Obsidian Markdown so that the
|
||||
user can inspect, navigate, edit, and version it without DS4Server.
|
||||
|
||||
### Configuration and ownership
|
||||
|
||||
- Preferences provide an enable switch and the path to one dedicated Obsidian
|
||||
vault. When disabled, Dev Brain contributes neither tools nor instructions to
|
||||
the agent prompt.
|
||||
- A vault must be an existing directory containing `.obsidian`. DS4Server may
|
||||
change only its declared wiki files and must never modify Obsidian settings,
|
||||
attachments, trash, hidden files, or unrelated user notes.
|
||||
- The existing configuration store persists the setting. Changing the vault or
|
||||
enable state invalidates the current Dev Brain context and derived index.
|
||||
|
||||
### Vault contract
|
||||
|
||||
The vault is self-describing and contains:
|
||||
|
||||
- `purpose.md`, maintained with the user, defining the wiki's scope, priorities,
|
||||
and recurring questions;
|
||||
- `schema.md`, defining page types, naming and linking conventions, provenance,
|
||||
freshness states, and the rules for compilation, querying, and validation;
|
||||
- `index.md`, a concise catalog of wiki pages with one-line descriptions;
|
||||
- `log.md`, an append-only record of material wiki updates and their source
|
||||
revisions; and
|
||||
- topic pages grouped under `projects/`, `subsystems/`, `concepts/`,
|
||||
`decisions/`, `invariants/`, and `workflows/` as the content requires.
|
||||
|
||||
Every managed topic page has YAML frontmatter containing its page type, project
|
||||
identity, `verified`, `stale`, or `needs-review` status, verification time, and
|
||||
source records. A source record identifies the registered project, a
|
||||
repo-relative path, an optional symbol, and the exact evidence version: the Git
|
||||
revision for a clean worktree and a content hash for the files actually read.
|
||||
The body uses normal Obsidian links and embeds to connect related knowledge.
|
||||
Pages model useful concepts rather than mirroring every source file.
|
||||
|
||||
### Compilation and maintenance
|
||||
|
||||
Initial compilation examines high-signal project material such as manifests,
|
||||
documentation, schemas, entry points, public interfaces, and tests, then writes
|
||||
the smallest set of durable topic pages that answers the purpose of the wiki.
|
||||
It does not attempt to summarize every file.
|
||||
|
||||
Refresh compares each page's recorded evidence with the current project state.
|
||||
Changed source paths and hashes select the dependent pages to re-read and
|
||||
revalidate. The agent may update, split, merge, or retire those pages and keeps
|
||||
`index.md` and `log.md` consistent with the result. Knowledge crystallized from
|
||||
a development session must still cite project evidence; unsupported conclusions
|
||||
are marked `needs-review` rather than presented as fact.
|
||||
|
||||
Wiki changes are prepared as one candidate batch in a temporary location. The
|
||||
complete candidate is validated before managed files are replaced. A failed or
|
||||
ambiguous update leaves the last valid vault untouched, and publication must
|
||||
not expose a partially updated wiki.
|
||||
|
||||
### Parsing, indexing, and querying
|
||||
|
||||
Use `turbovault-parser` to parse Obsidian-flavored Markdown, including YAML
|
||||
frontmatter, headings, wikilinks, embeds, tags, and source positions without
|
||||
treating link-like text inside code as graph edges. Keep the Markdown files as
|
||||
the source of truth and build a disposable SQLite index using the database
|
||||
support already present in DS4Server:
|
||||
|
||||
- page path, title, type, status, headings, tags, and searchable body text;
|
||||
- resolved wikilink and embed edges;
|
||||
- project-source-to-page provenance edges; and
|
||||
- an FTS5 index for ranked full-text search.
|
||||
|
||||
The index is rebuilt when a vault is enabled and refreshed from file metadata
|
||||
and content hashes before use. Querying starts with `index.md` or FTS, follows
|
||||
relevant links and backlinks, and returns both wiki pages and their project
|
||||
evidence. Stale or unverified pages are labeled and excluded from authoritative
|
||||
answers until the underlying sources have been checked.
|
||||
|
||||
The first implementation deliberately uses no filesystem watcher, embeddings,
|
||||
vector database, or separate search service. Explicit refresh plus SQLite FTS5
|
||||
is sufficient until vault size or measured retrieval quality proves otherwise.
|
||||
|
||||
### Validation
|
||||
|
||||
Validation prevents the wiki from silently becoming a confident but obsolete
|
||||
source:
|
||||
|
||||
- structural validation checks frontmatter, page types, required fields, and
|
||||
the managed path boundary;
|
||||
- graph validation checks that links resolve, page identities are unique, and
|
||||
`index.md` matches the managed pages;
|
||||
- provenance validation checks that every cited project and source exists and
|
||||
that paths remain inside the registered project;
|
||||
- freshness validation checks recorded revisions and hashes against the current
|
||||
files; and
|
||||
- semantic validation has the agent re-check affected claims against changed
|
||||
code, documentation, and tests before marking a page verified.
|
||||
|
||||
Semantic validation cannot prove every statement correct, so uncertainty must
|
||||
remain visible as `needs-review`. Agents may rely on `verified` pages, may use
|
||||
`stale` pages only as leads, and must not turn unsupported wiki text into project
|
||||
truth.
|
||||
|
||||
### Agent integration and acceptance
|
||||
|
||||
Dev Brain is a separate wiki maintenance and retrieval capability, not generic
|
||||
filesystem access outside the active project. Its operations expose search and
|
||||
read over the derived index plus validated batch publication of managed wiki
|
||||
content. Detailed operating instructions live in `schema.md`; the system prompt
|
||||
only announces Dev Brain when enabled and directs the agent to that contract.
|
||||
|
||||
The implementation is complete when tests cover vault path confinement,
|
||||
Obsidian parsing and index rebuilding, detection of source-hash drift, broken
|
||||
links and invalid provenance, exclusion of stale pages from authoritative
|
||||
queries, all-or-nothing publication, and prompt gating when Dev Brain is
|
||||
disabled.
|
||||
|
||||
@@ -78,6 +78,55 @@ Tool calls and results appear in the transcript. Use their copy actions for the
|
||||
complete, untruncated text; large outputs can also be opened from their saved
|
||||
file.
|
||||
|
||||
## Dev Brain
|
||||
|
||||
Dev Brain is a project-backed wiki for durable engineering knowledge. It lets
|
||||
the coding agent compile architecture, behavior, decisions, invariants,
|
||||
workflows, and relationships once, then retrieve that verified context in later
|
||||
chats. Registered projects remain authoritative; Dev Brain is not a general
|
||||
memory store and never replaces checking changed source code.
|
||||
|
||||
To set it up:
|
||||
|
||||
1. Create or choose a dedicated Obsidian vault. The folder must already contain
|
||||
**.obsidian**.
|
||||
2. Open **Preferences > Dev Brain**, choose the vault, enable Dev Brain, and
|
||||
save.
|
||||
3. In a project chat, ask the agent to build or refresh Dev Brain. Tell it the
|
||||
questions and areas that matter most so it can maintain **purpose.md** and
|
||||
compile a small set of useful topic pages instead of summarizing every file.
|
||||
|
||||
Useful requests include “Build Dev Brain for these projects,” “Refresh Dev
|
||||
Brain after my recent changes,” “What does Dev Brain know about the cache
|
||||
invariants?”, and “Check the wiki, then verify this against the source.” Search
|
||||
and reads use a disposable local SQLite full-text index. The index checks vault
|
||||
files and cited project evidence before use, so no separate indexing service or
|
||||
manual refresh button is needed.
|
||||
|
||||
The vault remains ordinary Obsidian Markdown:
|
||||
|
||||
- **purpose.md** records its scope, priorities, and recurring questions.
|
||||
- **schema.md** is the detailed contract the agent follows when maintaining it.
|
||||
- **index.md** catalogs every managed topic page.
|
||||
- **log.md** is an append-only history of material updates.
|
||||
- Topic pages live under **projects/**, **subsystems/**, **concepts/**,
|
||||
**decisions/**, **invariants/**, and **workflows/** as needed.
|
||||
|
||||
Every topic cites exact project files and a Git revision or content hash.
|
||||
**Verified** pages may support answers. **Stale** pages are navigation leads
|
||||
whose evidence changed, and **needs-review** pages contain visible uncertainty;
|
||||
the agent must recheck project sources before relying on either. Wiki updates
|
||||
are validated as a complete batch before publication, so a broken link,
|
||||
out-of-bounds path, invalid citation, or stale verified claim leaves the last
|
||||
valid wiki untouched.
|
||||
|
||||
DS4Server may modify only the four contract files and topic pages explicitly
|
||||
marked as Dev Brain-managed. It does not modify Obsidian settings, attachments,
|
||||
trash, hidden files, or unrelated notes. You can inspect, edit, link, and
|
||||
version the managed Markdown normally; invalid manual edits are reported rather
|
||||
than silently trusted. Disabling Dev Brain removes its tools and instructions
|
||||
from the built-in coding agent without deleting the vault.
|
||||
|
||||
## Views and sidebar
|
||||
|
||||
Use **View > Show Sidebar** (`⌘B`) to hide or restore the project sidebar. The
|
||||
|
||||
@@ -94,7 +94,7 @@ pub(crate) fn run(args: impl Iterator<Item = String>) -> Result<(), String> {
|
||||
.timeout_recv_body(Some(Duration::from_secs(30)))
|
||||
.build()
|
||||
.into();
|
||||
let mut system = crate::agent::system_prompt(model, &config.generation.system_prompt);
|
||||
let mut system = crate::agent::system_prompt(model, &config.generation.system_prompt, false);
|
||||
system.push_str("\n\n");
|
||||
system.push_str(crate::a2ui::SYSTEM_PROMPT);
|
||||
let metadata = Store::default().client_metadata();
|
||||
|
||||
88
src/agent.rs
88
src/agent.rs
@@ -302,6 +302,7 @@ pub(crate) struct Tools {
|
||||
jobs: HashMap<u32, BashJob>,
|
||||
next_job: u32,
|
||||
browser: Browser,
|
||||
dev_brain: Option<crate::dev_brain::DevBrain>,
|
||||
}
|
||||
|
||||
impl Tools {
|
||||
@@ -316,9 +317,19 @@ impl Tools {
|
||||
jobs: HashMap::new(),
|
||||
next_job: 1,
|
||||
browser: Browser::new()?,
|
||||
dev_brain: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn enable_dev_brain(
|
||||
&mut self,
|
||||
config: &crate::config::DevBrainConfig,
|
||||
projects: &[crate::database::Project],
|
||||
) -> Result<(), String> {
|
||||
self.dev_brain = Some(crate::dev_brain::DevBrain::open(config, projects)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn execute(&mut self, call: &ToolCall, cancel: &AtomicBool) -> String {
|
||||
let result = match call.name.as_str() {
|
||||
"read" => self.read(call),
|
||||
@@ -332,6 +343,9 @@ impl Tools {
|
||||
"bash_stop" => self.bash_observe(call, true, cancel),
|
||||
"google_search" => self.google_search(call, cancel),
|
||||
"visit_page" => self.visit_page(call, cancel),
|
||||
"dev_brain_search" => self.dev_brain_search(call),
|
||||
"dev_brain_read" => self.dev_brain_read(call),
|
||||
"dev_brain_publish" => self.dev_brain_publish(call),
|
||||
name => Err(format!("unknown tool: {name}")),
|
||||
};
|
||||
match result {
|
||||
@@ -349,6 +363,54 @@ impl Tools {
|
||||
}
|
||||
}
|
||||
|
||||
fn dev_brain_search(&mut self, call: &ToolCall) -> Result<String, String> {
|
||||
let query = required_string(call, "query")?;
|
||||
let limit = integer(call, "limit", 8, 1, 50);
|
||||
let authoritative = boolean(call, "authoritative", true);
|
||||
self.dev_brain
|
||||
.as_mut()
|
||||
.ok_or_else(|| "Dev Brain is disabled for this session.".to_owned())?
|
||||
.search(query, limit, authoritative)
|
||||
}
|
||||
|
||||
fn dev_brain_read(&mut self, call: &ToolCall) -> Result<String, String> {
|
||||
let path = required_string(call, "path")?;
|
||||
let authoritative = boolean(call, "authoritative", false);
|
||||
self.dev_brain
|
||||
.as_mut()
|
||||
.ok_or_else(|| "Dev Brain is disabled for this session.".to_owned())?
|
||||
.read(path, authoritative)
|
||||
}
|
||||
|
||||
fn dev_brain_publish(&mut self, call: &ToolCall) -> Result<String, String> {
|
||||
let files = call
|
||||
.arguments
|
||||
.get("files")
|
||||
.and_then(Value::as_object)
|
||||
.ok_or_else(|| "dev_brain_publish requires a files object.".to_owned())?;
|
||||
let remove = call
|
||||
.arguments
|
||||
.get("remove")
|
||||
.map(|value| {
|
||||
value
|
||||
.as_array()
|
||||
.ok_or_else(|| "remove must be an array of paths.".to_owned())?
|
||||
.iter()
|
||||
.map(|path| {
|
||||
path.as_str()
|
||||
.map(str::to_owned)
|
||||
.ok_or_else(|| "remove paths must be strings.".to_owned())
|
||||
})
|
||||
.collect::<Result<Vec<_>, String>>()
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
self.dev_brain
|
||||
.as_mut()
|
||||
.ok_or_else(|| "Dev Brain is disabled for this session.".to_owned())?
|
||||
.publish(files, &remove)
|
||||
}
|
||||
|
||||
fn result_limit(&self) -> usize {
|
||||
(self.context_tokens.max(4096) as usize * 2).min(512 * 1024)
|
||||
}
|
||||
@@ -1229,16 +1291,26 @@ pub(crate) fn parse_tool_calls(
|
||||
.map(|calls| (content, calls))
|
||||
}
|
||||
|
||||
pub(crate) fn system_prompt(model: ModelChoice, extra: &str) -> String {
|
||||
pub(crate) fn system_prompt(model: ModelChoice, extra: &str, dev_brain: bool) -> String {
|
||||
let schemas = if dev_brain {
|
||||
format!("{TOOL_SCHEMAS}\n{}", crate::dev_brain::TOOL_SCHEMAS)
|
||||
} else {
|
||||
TOOL_SCHEMAS.to_owned()
|
||||
};
|
||||
let tools = if model == ModelChoice::Glm52 {
|
||||
format!(
|
||||
"You are a coding agent running in a local workspace. Use tools for local file and system work. Avoid printing large file contents or large code blocks as answers; create or edit files with tools, then summarize results briefly.\n\n# Tools\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>\n{TOOL_SCHEMAS}\n</tools>\n\nFor a function call, output exactly: <tool_call>function-name<arg_key>key</arg_key><arg_value>value</arg_value></tool_call>\nTool calls are not allowed inside <think></think>. Use read/search for focused context, edit with exact unique old text, and [upto] only between unique head and tail anchors. Use refresh_sec for long bash jobs and poll with bash_status or stop with bash_stop. Preserve the current system configuration unless the user explicitly asks otherwise."
|
||||
"You are a coding agent running in a local workspace. Use tools for local file and system work. Avoid printing large file contents or large code blocks as answers; create or edit files with tools, then summarize results briefly.\n\n# Tools\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>\n{schemas}\n</tools>\n\nFor a function call, output exactly: <tool_call>function-name<arg_key>key</arg_key><arg_value>value</arg_value></tool_call>\nTool calls are not allowed inside <think></think>. Use read/search for focused context, edit with exact unique old text, and [upto] only between unique head and tail anchors. Use refresh_sec for long bash jobs and poll with bash_status or stop with bash_stop. Preserve the current system configuration unless the user explicitly asks otherwise."
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"You are a coding agent running in a local workspace. Use tools for local file and system work. Avoid printing large file contents or large code blocks as answers; create or edit files with tools, then summarize results briefly.\n\n## Tools\n\nInvoke native DSML tools exactly as:\n<|DSML|tool_calls>\n<|DSML|invoke name=\"$TOOL_NAME\">\n<|DSML|parameter name=\"$PARAMETER_NAME\" string=\"true|false\">$PARAMETER_VALUE</|DSML|parameter>\n</|DSML|invoke>\n</|DSML|tool_calls>\n\nTool calls are not allowed inside <think></think>. String parameters use raw text and string=\"true\"; numbers and booleans use JSON text and string=\"false\". Read defaults to a bounded chunk; use more to continue and whole=true only when needed. Use write for new files or whole-file replacement. Use edit with path first and exact unique old text; old may contain one [upto] marker between unique head and tail anchors. For long bash commands pass refresh_sec, then use bash_status or bash_stop. The first web call asks permission to start visible Chrome.\n\n### Available Tool Schemas\n\n{TOOL_SCHEMAS}\n\n# Rules\n- Always use strict DSML syntax.\n- Use read/search to get anchors before editing.\n- Preserve the current system configuration unless explicitly asked otherwise."
|
||||
"You are a coding agent running in a local workspace. Use tools for local file and system work. Avoid printing large file contents or large code blocks as answers; create or edit files with tools, then summarize results briefly.\n\n## Tools\n\nInvoke native DSML tools exactly as:\n<|DSML|tool_calls>\n<|DSML|invoke name=\"$TOOL_NAME\">\n<|DSML|parameter name=\"$PARAMETER_NAME\" string=\"true|false\">$PARAMETER_VALUE</|DSML|parameter>\n</|DSML|invoke>\n</|DSML|tool_calls>\n\nTool calls are not allowed inside <think></think>. String parameters use raw text and string=\"true\"; numbers and booleans use JSON text and string=\"false\". Read defaults to a bounded chunk; use more to continue and whole=true only when needed. Use write for new files or whole-file replacement. Use edit with path first and exact unique old text; old may contain one [upto] marker between unique head and tail anchors. For long bash commands pass refresh_sec, then use bash_status or bash_stop. The first web call asks permission to start visible Chrome.\n\n### Available Tool Schemas\n\n{schemas}\n\n# Rules\n- Always use strict DSML syntax.\n- Use read/search to get anchors before editing.\n- Preserve the current system configuration unless explicitly asked otherwise."
|
||||
)
|
||||
};
|
||||
let tools = if dev_brain {
|
||||
format!("{tools}\n\n{}", crate::dev_brain::PROMPT)
|
||||
} else {
|
||||
tools
|
||||
};
|
||||
if extra.trim().is_empty() {
|
||||
tools
|
||||
} else {
|
||||
@@ -1246,10 +1318,10 @@ pub(crate) fn system_prompt(model: ModelChoice, extra: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn system_prompt_reminder(model: ModelChoice) -> String {
|
||||
pub(crate) fn system_prompt_reminder(model: ModelChoice, dev_brain: bool) -> String {
|
||||
format!(
|
||||
"[System prompt reminder follows.]\n{}\n[End system prompt reminder.]",
|
||||
system_prompt(model, "")
|
||||
system_prompt(model, "", dev_brain)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1654,7 +1726,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn prompts_and_parsers_expose_the_reference_tool_set() {
|
||||
let prompt = system_prompt(ModelChoice::DeepSeekV4Flash, "extra");
|
||||
let prompt = system_prompt(ModelChoice::DeepSeekV4Flash, "extra", false);
|
||||
for name in [
|
||||
"google_search",
|
||||
"visit_page",
|
||||
@@ -1672,9 +1744,11 @@ mod tests {
|
||||
}
|
||||
assert!(prompt.ends_with("extra"));
|
||||
assert!(
|
||||
system_prompt_reminder(ModelChoice::DeepSeekV4Flash)
|
||||
system_prompt_reminder(ModelChoice::DeepSeekV4Flash, false)
|
||||
.contains("[System prompt reminder follows.]")
|
||||
);
|
||||
assert!(!prompt.contains("dev_brain_search"));
|
||||
assert!(system_prompt(ModelChoice::DeepSeekV4Flash, "", true).contains("dev_brain_search"));
|
||||
assert!(datetime_context().starts_with("Current local date and time at session start:"));
|
||||
assert!(!prompt_reminder_due(49_999, 0));
|
||||
assert!(prompt_reminder_due(50_000, 0));
|
||||
|
||||
40
src/app.rs
40
src/app.rs
@@ -14,7 +14,7 @@ use preferences::PreferenceDraft;
|
||||
#[cfg(test)]
|
||||
use preferences::{parse_optional_gib, parse_streaming_cache};
|
||||
|
||||
use crate::config::{Config, EndpointConfig};
|
||||
use crate::config::{Config, DevBrainConfig, EndpointConfig};
|
||||
use crate::database::{Database, ProjectWithSessions, SessionState, StoredMessage};
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::engine::ChatTurn;
|
||||
@@ -247,6 +247,7 @@ pub(super) enum DetailTab {
|
||||
pub(super) enum PreferenceSection {
|
||||
Model,
|
||||
Endpoint,
|
||||
DevBrain,
|
||||
Generation,
|
||||
Execution,
|
||||
Acceleration,
|
||||
@@ -255,9 +256,10 @@ pub(super) enum PreferenceSection {
|
||||
}
|
||||
|
||||
impl PreferenceSection {
|
||||
const ALL: [Self; 7] = [
|
||||
const ALL: [Self; 8] = [
|
||||
Self::Model,
|
||||
Self::Endpoint,
|
||||
Self::DevBrain,
|
||||
Self::Generation,
|
||||
Self::Execution,
|
||||
Self::Acceleration,
|
||||
@@ -269,6 +271,7 @@ impl PreferenceSection {
|
||||
match self {
|
||||
Self::Model => "preferences-model",
|
||||
Self::Endpoint => "preferences-endpoint",
|
||||
Self::DevBrain => "preferences-dev-brain",
|
||||
Self::Generation => "preferences-generation",
|
||||
Self::Execution => "preferences-execution",
|
||||
Self::Acceleration => "preferences-acceleration",
|
||||
@@ -281,6 +284,7 @@ impl PreferenceSection {
|
||||
match self {
|
||||
Self::Model => "Model & lifecycle",
|
||||
Self::Endpoint => "Local endpoint",
|
||||
Self::DevBrain => "Dev Brain",
|
||||
Self::Generation => "Generation",
|
||||
Self::Execution => "Execution",
|
||||
Self::Acceleration => "Acceleration & memory",
|
||||
@@ -338,6 +342,10 @@ pub(crate) enum Message {
|
||||
PreferenceEndpointPortChanged(String),
|
||||
PreferenceEndpointEnabledChanged(bool),
|
||||
PreferenceEndpointCorsChanged(bool),
|
||||
PreferenceDevBrainEnabledChanged(bool),
|
||||
PreferenceDevBrainVaultChanged(String),
|
||||
ChooseDevBrainVault,
|
||||
DevBrainVaultPicked(Option<PathBuf>),
|
||||
PreferenceContextChanged(String),
|
||||
PreferenceMaxTokensChanged(String),
|
||||
PreferenceSystemPromptAction(text_editor::Action),
|
||||
@@ -1157,6 +1165,33 @@ impl App {
|
||||
self.preference_draft.endpoint_cors = enabled;
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::PreferenceDevBrainEnabledChanged(enabled) => {
|
||||
self.preference_draft.dev_brain_enabled = enabled;
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::PreferenceDevBrainVaultChanged(value) => {
|
||||
self.preference_draft.dev_brain_vault_path = value;
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::ChooseDevBrainVault => {
|
||||
return Task::perform(
|
||||
async {
|
||||
AsyncFileDialog::new()
|
||||
.set_title("Choose an Obsidian vault")
|
||||
.pick_folder()
|
||||
.await
|
||||
.map(|folder| folder.path().to_path_buf())
|
||||
},
|
||||
Message::DevBrainVaultPicked,
|
||||
);
|
||||
}
|
||||
Message::DevBrainVaultPicked(path) => {
|
||||
if let Some(path) = path {
|
||||
self.preference_draft.dev_brain_vault_path =
|
||||
path.to_string_lossy().into_owned();
|
||||
self.preference_error = None;
|
||||
}
|
||||
}
|
||||
Message::PreferenceContextChanged(value) => {
|
||||
self.preference_draft.context_tokens = value;
|
||||
self.preference_error = None;
|
||||
@@ -1702,6 +1737,7 @@ impl App {
|
||||
self.tokens_per_second = None;
|
||||
}
|
||||
self.reload_projects();
|
||||
self.invalidate_dev_brain_context();
|
||||
self.finish_cache_change();
|
||||
}
|
||||
Err(error) => self.error = Some(error),
|
||||
|
||||
@@ -382,7 +382,7 @@ fn title_context(messages: impl IntoIterator<Item = ChatTurn>) -> Vec<ChatTurn>
|
||||
|
||||
impl App {
|
||||
fn chat_system_prompt(&self, model: ModelChoice, prompt: &str, agents: Option<&str>) -> String {
|
||||
let mut prompt = crate::agent::system_prompt(model, prompt);
|
||||
let mut prompt = crate::agent::system_prompt(model, prompt, self.config.dev_brain.enabled);
|
||||
if self.config.a2ui_enabled {
|
||||
prompt.push_str("\n\n");
|
||||
prompt.push_str(crate::a2ui::SYSTEM_PROMPT);
|
||||
@@ -668,7 +668,10 @@ impl App {
|
||||
}
|
||||
|
||||
fn system_prompt_reminders(&self, model: ModelChoice) -> Vec<String> {
|
||||
let mut reminders = vec![crate::agent::system_prompt_reminder(model)];
|
||||
let mut reminders = vec![crate::agent::system_prompt_reminder(
|
||||
model,
|
||||
self.config.dev_brain.enabled,
|
||||
)];
|
||||
if self.config.a2ui_enabled {
|
||||
reminders.push(crate::a2ui::SYSTEM_PROMPT.to_owned());
|
||||
}
|
||||
@@ -1163,7 +1166,15 @@ impl App {
|
||||
.find(|project| project.project.id == project_id)
|
||||
.map(|project| PathBuf::from(&project.project.path))
|
||||
.ok_or_else(|| "The active project is unavailable.".to_owned())?;
|
||||
let tools = crate::agent::Tools::new(&root, self.config.generation.context_tokens)?;
|
||||
let mut tools = crate::agent::Tools::new(&root, self.config.generation.context_tokens)?;
|
||||
if self.config.dev_brain.enabled {
|
||||
let projects = self
|
||||
.projects
|
||||
.iter()
|
||||
.map(|project| project.project.clone())
|
||||
.collect::<Vec<_>>();
|
||||
tools.enable_dev_brain(&self.config.dev_brain, &projects)?;
|
||||
}
|
||||
self.agent_tools = Some((session_id, Arc::new(Mutex::new(tools))));
|
||||
}
|
||||
let tools = Arc::clone(&self.agent_tools.as_ref().unwrap().1);
|
||||
|
||||
@@ -10,6 +10,8 @@ pub(super) struct PreferenceDraft {
|
||||
pub(super) endpoint_port: String,
|
||||
pub(super) endpoint_enabled: bool,
|
||||
pub(super) endpoint_cors: bool,
|
||||
pub(super) dev_brain_enabled: bool,
|
||||
pub(super) dev_brain_vault_path: String,
|
||||
pub(super) context_tokens: String,
|
||||
pub(super) max_generated_tokens: String,
|
||||
pub(super) system_prompt: text_editor::Content,
|
||||
@@ -60,6 +62,8 @@ impl PreferenceDraft {
|
||||
endpoint_port: config.endpoint.port.to_string(),
|
||||
endpoint_enabled: config.endpoint.enabled,
|
||||
endpoint_cors: config.endpoint.cors,
|
||||
dev_brain_enabled: config.dev_brain.enabled,
|
||||
dev_brain_vault_path: config.dev_brain.vault_path.clone().unwrap_or_default(),
|
||||
context_tokens: generation.context_tokens.to_string(),
|
||||
max_generated_tokens: generation.max_generated_tokens.to_string(),
|
||||
system_prompt: text_editor::Content::with_text(&generation.system_prompt),
|
||||
@@ -368,6 +372,10 @@ impl App {
|
||||
enabled: self.preference_draft.endpoint_enabled,
|
||||
cors: self.preference_draft.endpoint_cors,
|
||||
},
|
||||
dev_brain: DevBrainConfig {
|
||||
enabled: self.preference_draft.dev_brain_enabled,
|
||||
vault_path: optional_text(&self.preference_draft.dev_brain_vault_path),
|
||||
},
|
||||
generation,
|
||||
runtime,
|
||||
interface: self.config.interface.clone(),
|
||||
@@ -376,6 +384,18 @@ impl App {
|
||||
self.preference_error = Some(error);
|
||||
return;
|
||||
}
|
||||
if config.dev_brain.enabled {
|
||||
let projects = self
|
||||
.projects
|
||||
.iter()
|
||||
.map(|project| project.project.clone())
|
||||
.collect::<Vec<_>>();
|
||||
if let Err(error) = crate::dev_brain::DevBrain::open(&config.dev_brain, &projects) {
|
||||
self.preference_error = Some(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let dev_brain_changed = self.config.dev_brain != config.dev_brain;
|
||||
#[cfg(target_os = "macos")]
|
||||
let endpoint_changed = self.config.endpoint != config.endpoint;
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -415,6 +435,10 @@ impl App {
|
||||
}
|
||||
self.config = config;
|
||||
#[cfg(target_os = "macos")]
|
||||
if dev_brain_changed {
|
||||
self.invalidate_dev_brain_context();
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
update_runtime_config(&self.runtime_config, &self.config);
|
||||
#[cfg(target_os = "macos")]
|
||||
if endpoint_changed {
|
||||
|
||||
@@ -152,6 +152,7 @@ impl App {
|
||||
self.project_name_input.clear();
|
||||
self.error = None;
|
||||
self.reload_projects();
|
||||
self.invalidate_dev_brain_context();
|
||||
self.refresh_git_state();
|
||||
}
|
||||
Err(error) => self.error = Some(error),
|
||||
@@ -349,6 +350,21 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn invalidate_dev_brain_context(&mut self) {
|
||||
if !self.config.dev_brain.enabled {
|
||||
return;
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
self.agent_tools = None;
|
||||
for chat in self.background_chats.values_mut() {
|
||||
chat.agent_tools = None;
|
||||
chat.system_prompt_seen_at = 0;
|
||||
}
|
||||
}
|
||||
self.system_prompt_seen_at = 0;
|
||||
}
|
||||
}
|
||||
|
||||
fn read_git_state(path: &Path) -> Option<GitState> {
|
||||
|
||||
@@ -156,6 +156,33 @@ impl App {
|
||||
]
|
||||
.spacing(10),
|
||||
);
|
||||
let dev_brain_group = preference_group(
|
||||
PreferenceSection::DevBrain,
|
||||
"DEV BRAIN",
|
||||
column![
|
||||
hint(
|
||||
checkbox(self.preference_draft.dev_brain_enabled)
|
||||
.label("Enable project-backed LLM wiki")
|
||||
.on_toggle(Message::PreferenceDevBrainEnabledChanged),
|
||||
"Adds validated search, read, and batch-publication tools for a dedicated Obsidian vault. Disabled means no Dev Brain tools or prompt instructions.",
|
||||
),
|
||||
row![
|
||||
text_input(
|
||||
"/path/to/Obsidian vault",
|
||||
&self.preference_draft.dev_brain_vault_path,
|
||||
)
|
||||
.on_input(Message::PreferenceDevBrainVaultChanged)
|
||||
.padding(9)
|
||||
.width(Length::Fill),
|
||||
action_button("Choose…").on_press(Message::ChooseDevBrainVault),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center),
|
||||
text("The folder must already contain .obsidian. DS4Server manages only its declared wiki pages and leaves settings, attachments, hidden files, and unrelated notes untouched.")
|
||||
.size(12),
|
||||
]
|
||||
.spacing(10),
|
||||
);
|
||||
let generation_group = preference_group(
|
||||
PreferenceSection::Generation,
|
||||
"GENERATION",
|
||||
@@ -552,6 +579,7 @@ impl App {
|
||||
let mut fields = column![
|
||||
model_group,
|
||||
endpoint_group,
|
||||
dev_brain_group,
|
||||
generation_group,
|
||||
execution_group,
|
||||
acceleration_group,
|
||||
|
||||
@@ -15,6 +15,7 @@ pub struct Config {
|
||||
pub model: ModelChoice,
|
||||
pub idle_timeout_minutes: i32,
|
||||
pub a2ui_enabled: bool,
|
||||
pub dev_brain: DevBrainConfig,
|
||||
pub endpoint: EndpointConfig,
|
||||
pub generation: GenerationPreferences,
|
||||
pub runtime: RuntimePreferences,
|
||||
@@ -27,6 +28,7 @@ impl Default for Config {
|
||||
model: ModelChoice::default(),
|
||||
idle_timeout_minutes: 10,
|
||||
a2ui_enabled: true,
|
||||
dev_brain: DevBrainConfig::default(),
|
||||
endpoint: EndpointConfig::default(),
|
||||
generation: GenerationPreferences::default(),
|
||||
runtime: RuntimePreferences::default(),
|
||||
@@ -35,6 +37,35 @@ impl Default for Config {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct DevBrainConfig {
|
||||
pub enabled: bool,
|
||||
pub vault_path: Option<String>,
|
||||
}
|
||||
|
||||
impl DevBrainConfig {
|
||||
pub fn vault(&self) -> Result<std::path::PathBuf, String> {
|
||||
if !self.enabled {
|
||||
return Err("Dev Brain is disabled.".into());
|
||||
}
|
||||
let path = self
|
||||
.vault_path
|
||||
.as_deref()
|
||||
.filter(|path| !path.trim().is_empty())
|
||||
.ok_or_else(|| "Choose an Obsidian vault before enabling Dev Brain.".to_owned())?;
|
||||
crate::dev_brain::validate_vault(Path::new(path))
|
||||
}
|
||||
|
||||
fn validate(&self) -> Result<(), String> {
|
||||
if self.enabled {
|
||||
self.vault().map(|_| ())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct EndpointConfig {
|
||||
@@ -113,7 +144,8 @@ impl Config {
|
||||
return Err("Endpoint port must be between 1 and 65535.".into());
|
||||
}
|
||||
self.generation.validate()?;
|
||||
self.runtime.validate(self.model)
|
||||
self.runtime.validate(self.model)?;
|
||||
self.dev_brain.validate()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1665
src/dev_brain.rs
Normal file
1665
src/dev_brain.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ mod app;
|
||||
mod compaction;
|
||||
mod config;
|
||||
mod database;
|
||||
mod dev_brain;
|
||||
mod engine;
|
||||
mod metrics;
|
||||
mod model;
|
||||
|
||||
Reference in New Issue
Block a user