Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b25929824a | ||
|
|
3fd9a2b2db | ||
|
|
6a98d52d54 | ||
|
|
3847c0e6fd | ||
|
|
5023e5dfa1 | ||
|
|
eac0443c4c | ||
|
|
14f3eae62a | ||
|
|
aec53651ed | ||
|
|
93f3c2a357 | ||
|
|
9ea3d361b1 | ||
|
|
271721694b | ||
|
|
171597ca05 | ||
|
|
7b0b53671f | ||
|
|
884b19ccb5 | ||
|
|
6db00b2266 | ||
|
|
7f64423615 | ||
|
|
6d3f4918bf | ||
|
|
448eb5d462 | ||
|
|
924576d2ee | ||
|
|
033f964be0 | ||
|
|
2de5b57133 | ||
|
|
d1929f2fd4 | ||
|
|
14aa1785f8 | ||
|
|
0e0fd5df05 | ||
|
|
07f547eea8 | ||
|
|
f918d44c8d | ||
|
|
0d06875bb7 | ||
|
|
ffefa8c07f | ||
|
|
883908ab55 | ||
|
|
eed4025918 | ||
|
|
6b90c7eb0d | ||
|
|
958645ed7f | ||
|
|
eb8cc17993 | ||
|
|
b3a4d131d1 | ||
|
|
f368f3a1c0 | ||
|
|
8ecc588a3e | ||
|
|
55677dd671 | ||
|
|
d615090dcd | ||
|
|
8c58faf292 | ||
|
|
802346f909 | ||
|
|
552bc5bc63 | ||
|
|
d59713d3e3 | ||
|
|
6ab532018a | ||
|
|
047d7183d7 | ||
|
|
4d85e5144b | ||
|
|
dcc8c3ee42 | ||
|
|
289c58ef36 | ||
|
|
094eb4b8ba | ||
|
|
dd7825b481 | ||
|
|
4c186b62d4 | ||
|
|
fef3c925cd | ||
|
|
785ae19757 | ||
|
|
873f870e23 | ||
|
|
54927f692f | ||
|
|
efcd191f96 | ||
|
|
2e8a4f2443 | ||
|
|
2f5f62ab09 | ||
|
|
919435eb84 | ||
|
|
bed9f8cff6 | ||
|
|
6c7995f5f5 | ||
|
|
7ea226509c | ||
|
|
6bef4a3f82 | ||
|
|
16494d4b1e | ||
|
|
f84dfb8476 | ||
|
|
07b217cf4a | ||
|
|
9bfb95d795 | ||
|
|
285dbb14cc | ||
|
|
cac6626586 | ||
|
|
5a373d1031 | ||
|
|
fe2e916937 | ||
|
|
66ac4dbf02 | ||
|
|
44c3dd1239 | ||
|
|
87abe8c335 | ||
|
|
148ba4e07b | ||
|
|
89ee213454 | ||
|
|
a04651905f | ||
|
|
600ea041ef | ||
|
|
1ec2aa136a |
@@ -1,47 +0,0 @@
|
||||
---
|
||||
name: commit-convention
|
||||
description: Conventional Commits format and version-bump rules for this repo (Bahasa Indonesia commit style). Use when creating a git commit in zesdex.
|
||||
---
|
||||
|
||||
# Commit Convention
|
||||
|
||||
Gunakan **Conventional Commits** untuk semua commit. Format:
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
```
|
||||
|
||||
**Type & efek ke versi:**
|
||||
|
||||
| Type | Bump | Kapan pakai |
|
||||
|-------------|-------|------------------------------------------|
|
||||
| `feat` | minor | Fitur baru |
|
||||
| `fix` | patch | Perbaikan bug |
|
||||
| `chore` | patch | Maintenance, update deps, dll |
|
||||
| `docs` | patch | Perubahan dokumentasi/comment |
|
||||
| `refactor` | patch | Refactor kode tanpa perubahan fungsional |
|
||||
| `test` | patch | Nambah/ubah test |
|
||||
| `style` | patch | Formatting, whitespace, lint |
|
||||
| `perf` | patch | Optimasi performa |
|
||||
| `ci` | patch | Perubahan CI/CD |
|
||||
|
||||
**Catatan:**
|
||||
- **Semua type menghasilkan release** (patch minimal). Tidak ada commit yang "skip release".
|
||||
- Tambahkan `BREAKING CHANGE:` di body commit untuk bump **major**.
|
||||
- **Scope** opsional, tapi direkomendasikan (misal `feat(agent):`, `fix(ipc):`).
|
||||
|
||||
### Contoh
|
||||
|
||||
```
|
||||
feat(tool): add batch file delete
|
||||
|
||||
chore: bump reqwest to 0.12
|
||||
|
||||
refactor(harness): flatten guard pipeline
|
||||
|
||||
fix(ipc): reconnect loop on socket timeout
|
||||
|
||||
docs: add architecture diagram to README
|
||||
|
||||
BREAKING CHANGE: IPC frame header changed from 4-byte to 8-byte length
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
# Rust build artifacts
|
||||
target/
|
||||
|
||||
# VCS
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Local/secret files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Editor
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
|
||||
# Nix
|
||||
result
|
||||
result-*
|
||||
|
||||
# Kilo metadata
|
||||
.kilo/
|
||||
|
||||
# Docs lessons
|
||||
docs/lesson/
|
||||
@@ -21,11 +21,11 @@ jobs:
|
||||
with:
|
||||
components: clippy
|
||||
|
||||
- name: Build workspace
|
||||
run: cargo build --release --workspace
|
||||
- name: Format check
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: Clippy workspace
|
||||
run: cargo clippy --workspace --all-targets -- -D warnings
|
||||
|
||||
- name: Test workspace
|
||||
run: cargo test --workspace
|
||||
|
||||
- name: Clippy workspace
|
||||
run: cargo clippy --workspace -- -D warnings
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
name: Build & Deploy (Nix)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
VPS_HOST: ${{ secrets.VPS_HOST }}
|
||||
VPS_USER: ${{ secrets.VPS_USER }}
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v22
|
||||
with:
|
||||
determinate: false
|
||||
extra-conf: |
|
||||
sandbox = false
|
||||
accept-flake-config = true
|
||||
|
||||
- name: Cache Nix
|
||||
uses: DeterminateSystems/magic-nix-cache-action@v14
|
||||
|
||||
- name: Build zesdex
|
||||
id: build
|
||||
run: |
|
||||
nix build .#default --impure --option sandbox false --print-build-logs
|
||||
STORE_PATH=$(readlink result)
|
||||
echo "store-path=$STORE_PATH" >> "$GITHUB_OUTPUT"
|
||||
echo "Build OK: $STORE_PATH"
|
||||
|
||||
- name: Setup SSH key
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "$SSH_KEY" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
sed -i 's/\r$//' ~/.ssh/id_ed25519
|
||||
ssh-keygen -y -f ~/.ssh/id_ed25519 >/dev/null 2>&1 || { echo "SSH key invalid"; exit 1; }
|
||||
ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts 2>/dev/null
|
||||
|
||||
- name: Deploy zesdex to VPS
|
||||
run: |
|
||||
STORE_PATH="${{ steps.build.outputs.store-path }}"
|
||||
ssh "$VPS_USER@$VPS_HOST" "nix copy --to file:///nix/store $STORE_PATH && nix-env --install --force $STORE_PATH --profile /nix/var/nix/profiles/zesdex && systemctl restart zesdex"
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Publish to FlakeHub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
flakehub-publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: DeterminateSystems/determinate-nix-action@main
|
||||
- uses: DeterminateSystems/flakehub-push@main
|
||||
with:
|
||||
visibility: public
|
||||
rolling: true
|
||||
@@ -6,3 +6,5 @@ package.json
|
||||
package-lock.json
|
||||
.superpowers/
|
||||
docs/lesson/
|
||||
.kilo/
|
||||
.hermes/
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"branches": ["main"],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/changelog",
|
||||
["@semantic-release/exec", {
|
||||
"prepareCmd": "sed -i 's/^version = \"[^\"]*\"/version = \"${nextRelease.version}\"/' Cargo.toml && cargo check"
|
||||
}],
|
||||
["@semantic-release/git", {
|
||||
"assets": ["Cargo.toml", "CHANGELOG.md"],
|
||||
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||
}],
|
||||
["@semantic-release/github", {
|
||||
"assets": []
|
||||
}]
|
||||
]
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
Codemap Update Report — 2026-07-12
|
||||
====================================
|
||||
|
||||
Status: FIRST GENERATION (no previous codemaps to compare)
|
||||
|
||||
Files created:
|
||||
- docs/CODEMAPS/architecture.md (new)
|
||||
- docs/CODEMAPS/backend.md (new)
|
||||
- docs/CODEMAPS/frontend.md (new)
|
||||
- docs/CODEMAPS/data.md (new)
|
||||
- docs/CODEMAPS/dependencies.md (new)
|
||||
|
||||
Source scanned:
|
||||
- 124 Rust source files
|
||||
- 30 directories
|
||||
- 103 modules
|
||||
- 10,402 lines total
|
||||
|
||||
No previous codemaps found — diff calculation skipped.
|
||||
Freshness: all documents generated 2026-07-12.
|
||||
@@ -0,0 +1,38 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance to Kilo when working with code in the zesdex repository.
|
||||
|
||||
## Best Practice Conventions
|
||||
|
||||
Zesdex follows Kana Engineering Best Practices:
|
||||
|
||||
1. **Clean Architecture** — Strict domain/application/infrastructure/presentation layering.
|
||||
- Domain has ZERO framework dependencies.
|
||||
- Application depends only on domain.
|
||||
- Infrastructure implements domain traits.
|
||||
- DTOs cross layer boundaries, NOT entities.
|
||||
|
||||
2. **Clean Code** — Functions under ~40 lines, one level of abstraction per function, descriptive names, no flag arguments, no commented-out code.
|
||||
|
||||
3. **Documentation** — Every pub fn, struct, enum, and trait needs a doc comment (///) explaining what, flow, why, and return value.
|
||||
|
||||
4. **Commit Convention** — Conventional Commits in Bahasa Indonesia: `feat(scope):`, `fix(scope):`, `chore:`, `docs:`.
|
||||
|
||||
5. **Error Handling** — `anyhow::Result` and `anyhow::bail!` throughout. Log with `tracing` (never stderr).
|
||||
|
||||
6. **Testing** — `#[cfg(test)] mod tests` blocks inline in production files. Tests are F.I.R.S.T. (Fast, Independent, Repeatable, Self-validating, Timely).
|
||||
|
||||
7. **No Compiler Bypasses** — Never use `#[allow(...)]`, `#[expect(...)]`, or `#[allow(dead_code)]`. Fix the underlying code.
|
||||
|
||||
8. **Boy Scout Rule** — Leave every module cleaner than you found it.
|
||||
|
||||
## Available Agents
|
||||
|
||||
- `@rust-engineer` — Rust clean architecture specialist (subagent).
|
||||
- `@code-reviewer` — Code review specialist (subagent).
|
||||
|
||||
## Available Commands
|
||||
|
||||
- `/check` — Run cargo check, clippy, and tests.
|
||||
- `/audit` — Code quality audit against clean-architecture best practices.
|
||||
- `/doc` — Generate or update doc comments.
|
||||
+104
@@ -1,3 +1,107 @@
|
||||
## [1.19.2](https://github.com/asepharyana/zesdex/compare/v1.19.1...v1.19.2) (2026-08-27)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **agent:** stabilkan async & parallel — satu runtime, bounded concurrency, isolasi error ([6a98d52](https://github.com/asepharyana/zesdex/commit/6a98d52d54a69f78710d852a69dda3ac0a4ead31))
|
||||
|
||||
## [1.19.1](https://github.com/asepharyana/zesdex/compare/v1.19.0...v1.19.1) (2026-08-27)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **agent:** rombak alur AI agent — adaptif, hemat token, self-healing ([eac0443](https://github.com/asepharyana/zesdex/commit/eac0443c4c3b8bfcbefd4bad9554168fb6525b94))
|
||||
|
||||
# [1.19.0](https://github.com/asepharyana/zesdex/compare/v1.18.4...v1.19.0) (2026-08-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* hapus fitur LSP bawaan (language server protocol) ([93f3c2a](https://github.com/asepharyana/zesdex/commit/93f3c2a3572511b5a84f244980ad71bd1b455e70))
|
||||
|
||||
## [1.18.4](https://github.com/asepharyana/zesdex/compare/v1.18.3...v1.18.4) (2026-08-27)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **tui:** render streaming token secara inkremental + kurangi redraw sia-sia ([2717216](https://github.com/asepharyana/zesdex/commit/271721694beb62e9fa5ff932312b293aa1d56823))
|
||||
|
||||
## [1.18.3](https://github.com/asepharyana/zesdex/compare/v1.18.2...v1.18.3) (2026-08-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** cegah race condition pada register users.json (TOCTOU) ([884b19c](https://github.com/asepharyana/zesdex/commit/884b19ccb5fbcaa6b29cb41dc978386cf7b1b3f9))
|
||||
* **api:** perbaiki keamanan auth & WebSocket, tambah rate limiting ([6db00b2](https://github.com/asepharyana/zesdex/commit/6db00b22663839a6a975ae978058b894900c71de))
|
||||
* **build:** perbaiki referensi paket zesdex-gateway dan sinkronisasi versi nix ([6d3f491](https://github.com/asepharyana/zesdex/commit/6d3f4918bfea06b106d4fe75adf58e6a29aca2a5))
|
||||
* **build:** perbaiki referensi paket zesdex-gateway di Dockerfile & default.nix ([7f64423](https://github.com/asepharyana/zesdex/commit/7f644236158f66fcc106ec55e437ebf28ee1cca1))
|
||||
|
||||
## [1.18.2](https://github.com/asepharyana/zesdex/compare/v1.18.1...v1.18.2) (2026-08-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **nix:** add perl to nativeBuildInputs for openssl-sys Configure ([033f964](https://github.com/asepharyana/zesdex/commit/033f964be0c695106eef74329524ad7b4b8dba14))
|
||||
* **nix:** correct cargoBuildFlags package name zesdex-backend -> zesdex-gateway ([2de5b57](https://github.com/asepharyana/zesdex/commit/2de5b57133f6c7295d8a85e63cb86504ea2a7d3e))
|
||||
|
||||
## [1.18.1](https://github.com/asepharyana/zesdex/compare/v1.18.0...v1.18.1) (2026-08-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **nix:** restrict flake to x86_64-linux (nixpkgs 26.11 dropped darwin) ([0e0fd5d](https://github.com/asepharyana/zesdex/commit/0e0fd5df05e53a6fa17d481825f1a8c567214d0e))
|
||||
|
||||
# [1.18.0](https://github.com/asepharyana/zesdex/compare/v1.17.0...v1.18.0) (2026-07-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add .releaserc.json for semantic-release configuration ([047d718](https://github.com/asepharyana/zesdex/commit/047d7183d72adaa0dd1a18a7f92b22d67d5aa85f))
|
||||
* **clippy:** replace type annotation with type alias + .insert() to avoid trivial_cast ([ffefa8c](https://github.com/asepharyana/zesdex/commit/ffefa8c07facbba83778f426ca4715569556b08a))
|
||||
* **rust:** remove unused imports, variables, and dead code causing CI build failures ([eb8cc17](https://github.com/asepharyana/zesdex/commit/eb8cc1799359a5ef5b4a4ee3a4202b87e80bfe6c))
|
||||
* **rust:** resolve all clippy warnings treated as errors in CI ([6b90c7e](https://github.com/asepharyana/zesdex/commit/6b90c7eb0d17d9281a377454e76dcc88a4232bc7))
|
||||
* **tui:** resolve remaining clippy errors in workspace ([eed4025](https://github.com/asepharyana/zesdex/commit/eed4025918de65eb2f3fd98cd3350d62f3322132))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add semantic search tool for code symbol indexing and searching ([fef3c92](https://github.com/asepharyana/zesdex/commit/fef3c925cd02e1d76e2d990cc6fa4e4cba64fab0))
|
||||
* Add SOLID principles and TDD reference documentation ([55677dd](https://github.com/asepharyana/zesdex/commit/55677dd67100813a5ed2b0bbf02257b57875ea07))
|
||||
* add test_load and test_parse binaries for configuration loading and parsing ([fe2e916](https://github.com/asepharyana/zesdex/commit/fe2e9169371bf7a3cc9391d8467d47fc2258ad1f))
|
||||
* **agent:** add AI summarization for conversation history compacting ([873f870](https://github.com/asepharyana/zesdex/commit/873f870e233dd0d2c6f0cce935f83fb8ce373505))
|
||||
* **agent:** implement agent execution engine and turn handling with background processing ([efcd191](https://github.com/asepharyana/zesdex/commit/efcd191f9698023944f9096a86c1efa47e412b6b))
|
||||
* **best_practice:** add code quality scanning and commit message validation ([8ecc588](https://github.com/asepharyana/zesdex/commit/8ecc588a3e125dc6a4b2e7eb4c12c6b344449c07))
|
||||
* centralize default constants and refactor overlay enter handling in TUI ([d615090](https://github.com/asepharyana/zesdex/commit/d615090dcd34bdb93be22c1c7ce4ba674363c258))
|
||||
* enhance context gathering in auto-review engine and agent runner ([dcc8c3e](https://github.com/asepharyana/zesdex/commit/dcc8c3ee42ef568843e04143048d799bc0882288))
|
||||
* enhance explore phase with TUI workflow event handling ([b3a4d13](https://github.com/asepharyana/zesdex/commit/b3a4d131d13d94bd307720cb50c2d02dedf92873))
|
||||
* implement Component trait for modular UI components and refactor TUI views to use it ([d59713d](https://github.com/asepharyana/zesdex/commit/d59713d3e3aa4325f9753bbddd068654e442f6ef))
|
||||
* implement mandatory explore phase with parallel subagents ([f368f3a](https://github.com/asepharyana/zesdex/commit/f368f3a1c0d2df02eb50542a81bcff96d9cdc420))
|
||||
* **llm:** improve tool call handling by dynamically resizing tool_calls and updating arguments ([9bfb95d](https://github.com/asepharyana/zesdex/commit/9bfb95d795d6c7cc10cfcb907642bba7b8bb7f19))
|
||||
* **llm:** improve UTF-8 handling in response processing to prevent infinite loops ([7ea2265](https://github.com/asepharyana/zesdex/commit/7ea226509c8e11ee1bab21d216e0413715988fb9))
|
||||
* **llm:** increase max retries for streaming requests from 3 to 10 ([6c7995f](https://github.com/asepharyana/zesdex/commit/6c7995f5f5481c48ccd449e72d9563425fdcb837))
|
||||
* **mcp:** enhance MCP server registration with error handling and improve transport process management ([148ba4e](https://github.com/asepharyana/zesdex/commit/148ba4e07b424736c6b4824c5786c0c7c9cbf0a1))
|
||||
* refactor auto-review engine to use spawn_subagent for improved thread handling ([094eb4b](https://github.com/asepharyana/zesdex/commit/094eb4b8baa5abd878dcf5b611cf615955bc79eb))
|
||||
* remove obsolete design documents for clipboard OSC52, diff view, file mention, context compaction, and add development guide ([66ac4db](https://github.com/asepharyana/zesdex/commit/66ac4dbf027820697f4303264666b5f63a0b31d4))
|
||||
* **tui:** add rich context information including active jobs, README snippet, and recent git history ([2f5f62a](https://github.com/asepharyana/zesdex/commit/2f5f62ab097614d8b0c7a3e5c7bd364cd479e0aa))
|
||||
* **tui:** add support for reasoning in chat messages and update transcript handling ([285dbb1](https://github.com/asepharyana/zesdex/commit/285dbb14cccb1c261134c1fcb6c30ef1ed02bb55))
|
||||
* **tui:** enhance agent turn handling by grouping parameters and improving message management ([07b217c](https://github.com/asepharyana/zesdex/commit/07b217cf4a81b99c63101512932935246fcbc7ae))
|
||||
* **tui:** enhance system prompt with workspace structure information ([bed9f8c](https://github.com/asepharyana/zesdex/commit/bed9f8cff60711c4db998c8f10f32f8e210ae410))
|
||||
* **tui:** implement streaming support for LLM responses and update transcript handling ([cac6626](https://github.com/asepharyana/zesdex/commit/cac662658651ae092cffddfc88f39a775afef942))
|
||||
* **tui:** integrate rich context builder into agent turn process ([919435e](https://github.com/asepharyana/zesdex/commit/919435eb84f9a41a0637052a5dc1234b410714f8))
|
||||
* **tui:** introduce comprehensive state management for TUI interface ([8c58faf](https://github.com/asepharyana/zesdex/commit/8c58faf2920b7341dd50ace15044ff113cefb576))
|
||||
* **tui:** optimize performance by caching display lines and token counts, and improve action handling ([87abe8c](https://github.com/asepharyana/zesdex/commit/87abe8c3358ac17196df93d19169901a856850d3))
|
||||
* **tui:** update system message for clarity and conciseness in tool usage instructions ([5a373d1](https://github.com/asepharyana/zesdex/commit/5a373d1031b476970bd45804a65c7455be99e78d))
|
||||
|
||||
|
||||
# [1.17.0](https://github.com/asepharyana/zesdex/compare/v1.16.1...v1.17.0) (2026-07-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **token:** add refresh token verification to TokenService ([a046519](https://github.com/asepharyana/zesdex/commit/a04651905f4afd562b516c839449a2c813ce6627))
|
||||
|
||||
## [1.16.1](https://github.com/asepharyana/zesdex/compare/v1.16.0...v1.16.1) (2026-07-20)
|
||||
|
||||
# [1.16.0](https://github.com/asepharyana/zesdex/compare/v1.15.2...v1.16.0) (2026-07-20)
|
||||
|
||||
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
Tests use `#[cfg(test)] mod tests` blocks inline in production files (not a separate `tests/` dir).
|
||||
|
||||
Tracing output goes to `~/.local/share/zesdex/zesdex.log`. Set `RUST_LOG=debug` for verbose logging.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Zesdex is an autonomous AI coding agent with a TUI — an OpenAI/Anthropic-compatible LLM client wrapped in a tool-use harness with 37 built-in tools.
|
||||
|
||||
Detailed architecture documentation is in `docs/CODEMAPS/`:
|
||||
|
||||
| File | Covers |
|
||||
|------|--------|
|
||||
| [`docs/CODEMAPS/architecture.md`](docs/CODEMAPS/architecture.md) | System layout, process modes, data flow, key files |
|
||||
| [`docs/CODEMAPS/backend.md`](docs/CODEMAPS/backend.md) | Provider, OAuth, IPC, workflow engine, MCP, review, bg bash |
|
||||
| [`docs/CODEMAPS/frontend.md`](docs/CODEMAPS/frontend.md) | TUI render pipeline, 16 overlays, toasts, input handling |
|
||||
| [`docs/CODEMAPS/data.md`](docs/CODEMAPS/data.md) | Persistence, SQLite msglog, memory files, settings/config |
|
||||
| [`docs/CODEMAPS/dependencies.md`](docs/CODEMAPS/dependencies.md) | 23 Rust crates, 5 external services |
|
||||
|
||||
`docs/runs/` holds an auto-generated audit trail: one markdown file per hive-mind convergence (see below), written deterministically by `app::workflow::docs::write_hive_mind_convergence` — not hand-maintained like `docs/CODEMAPS/`.
|
||||
|
||||
### Key Patterns
|
||||
|
||||
- **State mutation** — `AppStateRest` is mutable in-place from `actions/mod.rs` and `controller/input.rs`. No generic update function.
|
||||
- **No DI** — modules call `Settings::load()`, `AppConfig::load()`, `all_tools()` directly.
|
||||
- **Logging** — `tracing::warn!` to `~/.local/share/zesdex/zesdex.log` (not stderr, avoids TUI corruption).
|
||||
- **Error handling** — `anyhow::Result` and `anyhow::bail!` throughout. No custom error types.
|
||||
- **Static strings** — MCP tool descriptions use `Box::leak` + `OnceLock` cache.
|
||||
- **Tools** — `trait Tool { fn name() -> &str, fn run() -> Result<String> }`, 28 impls, gated by `Harness`.
|
||||
- **Shell safety** — `tool/shell_filter/` blocks destructive git commands (`shell_filter::git::check_git_destructive`, called from `tool/shell.rs::Bash::run`). It also contains a `check_credential_read` detector for credential-file reads, but that one is intentionally NOT wired into `Bash::run` today — see the doc comment on `Bash::run` for why.
|
||||
|
||||
### Hive-Mind Orchestration (Machine Intelligence)
|
||||
|
||||
- **A single Core Intelligence spawning anonymous processing nodes.** The Core Intelligence (main agent) compiles a cognitive cycle plan per task: an ordered list of cycles, each cycle a set of processing nodes that run in parallel. Each node's sole identity is its directive (what to do) and an access tier. Cycle count and nodes-per-cycle are entirely Core-Intelligence output.
|
||||
- **Access tiers** in `src/app/subagent/division.rs` (`tool_scope` module): tool access is granted per node via one of three tiers (`read` / `write` / `full`, see `tool_scope::tools_for`) picked by the Core Intelligence based on what each node's directive actually needs.
|
||||
- **Orchestrator** in `src/app/workflow/hive_mind.rs`: `run_hive_mind()` executes a `CognitiveCyclePlan { cycles: Vec<Vec<NodeDirective>> }` cycle-by-cycle. Node IDs are system-assigned coordinates (e.g. `"Node-0-1"`).
|
||||
- **Continuous collective state, not phase-boundary sync**: `engine::execute_primitive`'s `ScopedAgent` arm merges each node's complete output into the shared collective-state channel the instant that node finishes — not after its whole parallel cohort completes — so sibling/later nodes see it in real time.
|
||||
- **Consensus synthesis, not a per-node summary**: after all cycles complete, `synthesize_consensus()` spawns one final read-only node whose sole directive is to reconcile the entire collective state into a single consensus assessment — a real reasoning pass, not string concatenation, since node outputs can overlap or conflict.
|
||||
- **Auto-trigger** in `run_agent_turn()` (`actions/mod.rs`): `is_complex_request()` heuristics decide only whether to ask the Core Intelligence to compile a plan at all — the plan's shape is fully dynamic.
|
||||
- **`hive_mind` tool** (`src/tool/workflow.rs`) is the manual entry point: the calling LLM supplies its own `cycles` array of `{directive, access}` directly.
|
||||
- **Guaranteed documentation**: after every convergence, `src/app/workflow/docs.rs::write_hive_mind_convergence()` deterministically (not an LLM step, not skippable) writes every node's full output plus the final consensus to `docs/runs/<timestamp>-<slug>.md`.
|
||||
- **Live node progress** in TUI panel (`view/workflow.rs`): shows node designation + current tool via `AgentStatus::progress`.
|
||||
- **Auto inline review** after each edit: `src/app/subagent/auto.rs` — `spawn_quick_review()` injects verdict back into LLM conversation.
|
||||
- **Background subagents** (test-gen, arch-review, security-review) fire asynchronously at turn end via `TurnEvent::SystemNote`, retrying once on failure and escalating to a blocking (`ESCALATED:`-prefixed, `ToastKind::Error`) notice if the retry also fails.
|
||||
|
||||
Commit convention (Conventional Commits, Bahasa Indonesia): see the `commit-convention` skill.
|
||||
|
||||
## Code Documentation
|
||||
|
||||
Every function, struct, enum, trait, module, and significant code block must have a doc comment (`///` or `//!`) that explains:
|
||||
|
||||
- **What** the function/module does (purpose, not how)
|
||||
- **Flow** — a brief ASCII or prose description of the code flow / data flow above each non-trivial function
|
||||
- **Why** — non-obvious decisions, edge cases, invariants
|
||||
- **Return** — what the caller gets back, especially for `Result` types
|
||||
|
||||
Examples:
|
||||
|
||||
```rust
|
||||
/// Parse an SSE data chunk into one or more StreamEvents.
|
||||
///
|
||||
/// Flow: buffer → split on '\n' → flush on blank line → JSON parse → match event type
|
||||
/// → return Token / ToolCallDelta / Usage / Done.
|
||||
///
|
||||
/// Edge case: chunk may split mid-line; remaining bytes stay in buffer
|
||||
/// for the next feed() call.
|
||||
fn feed(&mut self, chunk: &str) -> Vec<StreamEvent> { ... }
|
||||
|
||||
/// The single source-of-truth state struct for the entire application.
|
||||
///
|
||||
/// Mutated in-place from two locations: actions/mod.rs (apply_action)
|
||||
/// and controller/input.rs (key event handlers). Read-only from
|
||||
/// every other module.
|
||||
struct AppStateRest { ... }
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Every `pub fn` needs a doc comment
|
||||
- Every `pub struct` / `pub enum` / `pub trait` needs a doc comment
|
||||
- Non-trivial private functions (≥10 lines) need a doc comment
|
||||
- Write the comment above the code it documents (not inline in the body)
|
||||
- Update comments when code behavior changes — stale docs are worse than no docs
|
||||
- NEVER use compiler/linter bypass annotations or attributes (such as `#[allow(clippy::too_many_lines, clippy::too_many_arguments, clippy::ref_option)]`, `#[allow(dead_code)]`, etc.) to silence warnings or skip linter checks. Always fix the underlying code issues instead.
|
||||
Generated
+11
-45
@@ -1088,15 +1088,6 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fluent-uri"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -1972,19 +1963,6 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lsp-types"
|
||||
version = "0.97.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"fluent-uri",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mac_address"
|
||||
version = "1.1.8"
|
||||
@@ -3369,17 +3347,6 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
@@ -4895,7 +4862,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-api"
|
||||
version = "1.16.0"
|
||||
version = "1.19.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -4918,7 +4885,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-application"
|
||||
version = "1.16.0"
|
||||
version = "1.19.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@@ -4935,7 +4902,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-bootstrap"
|
||||
version = "1.16.0"
|
||||
version = "1.19.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -4952,7 +4919,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-daemon"
|
||||
version = "1.16.0"
|
||||
version = "1.19.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@@ -4976,7 +4943,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-domain"
|
||||
version = "1.16.0"
|
||||
version = "1.19.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@@ -4992,7 +4959,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-gateway"
|
||||
version = "1.16.0"
|
||||
version = "1.19.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -5019,7 +4986,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-grpc"
|
||||
version = "1.16.0"
|
||||
version = "1.19.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -5036,7 +5003,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-infrastructure"
|
||||
version = "1.16.0"
|
||||
version = "1.19.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -5055,7 +5022,6 @@ dependencies = [
|
||||
"infer",
|
||||
"jsonwebtoken",
|
||||
"libc",
|
||||
"lsp-types",
|
||||
"nucleo-matcher",
|
||||
"percent-encoding",
|
||||
"pulldown-cmark",
|
||||
@@ -5085,7 +5051,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-tui"
|
||||
version = "1.16.0"
|
||||
version = "1.19.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@@ -5111,7 +5077,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-web"
|
||||
version = "1.16.0"
|
||||
version = "1.19.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -5131,7 +5097,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-ws"
|
||||
version = "1.16.0"
|
||||
version = "1.19.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
|
||||
+1
-2
@@ -15,7 +15,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.16.0"
|
||||
version = "1.19.2"
|
||||
edition = "2021"
|
||||
authors = ["asepharyana <superaseph@gmail.com>"]
|
||||
|
||||
@@ -61,7 +61,6 @@ ignore = "0.4"
|
||||
nucleo-matcher = "0.3"
|
||||
futures-util = "0.3"
|
||||
rmcp = { version = "2.2", default-features = false, features = ["client", "transport-child-process", "transport-streamable-http-client-reqwest", "macros"] }
|
||||
lsp-types = "0.97"
|
||||
tiktoken-rs = "0.12"
|
||||
similar = "3"
|
||||
syntect = { version = "5", default-features = false, features = ["default-fancy"] }
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
# Build with release profile (treats warnings as errors via lints)
|
||||
RUN cargo build --release -p zesdex-backend --bin zesdex
|
||||
RUN cargo build --release -p zesdex-gateway --bin zesdex
|
||||
|
||||
# Stage 2: Minimal runtime image
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
use anyhow::Result;
|
||||
use std::future::Future;
|
||||
|
||||
use zesdex_domain::agent::AgentTurnParams;
|
||||
|
||||
/// Interface for dispatching tool calls to their concrete implementations.
|
||||
pub trait ToolExecutor: Send + Sync {
|
||||
/// Execute a tool call asynchronously.
|
||||
fn execute(
|
||||
&self,
|
||||
tool_name: &str,
|
||||
args: &serde_json::Value,
|
||||
) -> impl Future<Output = Result<String>> + Send;
|
||||
}
|
||||
|
||||
/// Service for running agent turns asynchronously.
|
||||
pub trait AgentTurnService: Send + Sync {
|
||||
/// Run a full agent turn loop asynchronously.
|
||||
fn run_turn(&self, params: AgentTurnParams) -> impl Future<Output = Result<()>> + Send;
|
||||
}
|
||||
|
||||
pub mod turn_service;
|
||||
|
||||
pub use turn_service::{compact_messages_with_ai, AgentTurnServiceImpl};
|
||||
@@ -0,0 +1,539 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use zesdex_domain::agent::{AgentTurnParams, TurnEvent};
|
||||
use zesdex_domain::core::{ChatMessage, StreamEvent, ToolDef};
|
||||
use zesdex_domain::main_agent_prompt;
|
||||
|
||||
use super::ToolExecutor;
|
||||
use crate::ports::ProviderService;
|
||||
|
||||
/// Maximum tool-call iterations per agent turn before forcing termination.
|
||||
const MAX_TURN_ITERATIONS: u32 = 50;
|
||||
|
||||
/// Maximum number of consecutive identical tool errors before the loop
|
||||
/// injects a recovery note and forces a different approach.
|
||||
const MAX_CONSECUTIVE_TOOL_ERRORS: usize = 3;
|
||||
|
||||
/// Total tool-call errors tolerated per turn before the loop is stopped.
|
||||
const MAX_TOTAL_TOOL_ERRORS: usize = 8;
|
||||
|
||||
/// Ceiling for a single tool-result message inserted into context.
|
||||
///
|
||||
/// Tool outputs can be huge (read / semantic_search). Truncating keeps the
|
||||
/// context window from exploding while preserving the important head.
|
||||
const TOOL_OUTPUT_MAX_CHARS: usize = 12_000;
|
||||
|
||||
/// Total conversation characters that trigger auto-compaction before the
|
||||
/// next LLM call.
|
||||
const AUTO_COMPACT_CHARS: usize = 60_000;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: push a TurnEvent onto the shared queue.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn push_event(queue: &Arc<Mutex<VecDeque<TurnEvent>>>, event: TurnEvent) {
|
||||
if let Ok(mut q) = queue.lock() {
|
||||
q.push_back(event);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: stream-event callback that forwards tokens to the turn-event queue
|
||||
// and checks the abort flag on each emission.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn make_stream_callback(
|
||||
abort: &Arc<AtomicBool>,
|
||||
turn_events: &Arc<Mutex<VecDeque<TurnEvent>>>,
|
||||
) -> Box<dyn FnMut(&StreamEvent) -> bool + Send> {
|
||||
let abort_clone = Arc::clone(abort);
|
||||
let events_clone = Arc::clone(turn_events);
|
||||
Box::new(move |event: &StreamEvent| -> bool {
|
||||
if abort_clone.load(Ordering::SeqCst) {
|
||||
return false;
|
||||
}
|
||||
match event {
|
||||
StreamEvent::Token(s) => {
|
||||
push_event(&events_clone, TurnEvent::StreamToken(s.clone()));
|
||||
}
|
||||
StreamEvent::Reasoning(s) => {
|
||||
push_event(&events_clone, TurnEvent::StreamReasoning(s.clone()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: truncate a long tool output before it enters the conversation
|
||||
// context. Preserves the head and appends a clear truncation marker.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn truncate_tool_output(output: String) -> String {
|
||||
if output.len() <= TOOL_OUTPUT_MAX_CHARS {
|
||||
return output;
|
||||
}
|
||||
let mut result: String = output.chars().take(TOOL_OUTPUT_MAX_CHARS).collect();
|
||||
result.push_str(&format!(
|
||||
"\n...[truncated {} chars]",
|
||||
output.len() - TOOL_OUTPUT_MAX_CHARS
|
||||
));
|
||||
result
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: adaptive generation parameters.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Pick a `max_tokens` budget for the turn's next LLM call based on the
|
||||
/// length of the user's request. Short requests need far fewer tokens than
|
||||
/// the current hardcoded 4096 — big savings on small tasks.
|
||||
fn adaptive_max_tokens(request_len: usize) -> u32 {
|
||||
if request_len <= 80 {
|
||||
800
|
||||
} else if request_len <= 400 {
|
||||
1600
|
||||
} else {
|
||||
4096
|
||||
}
|
||||
}
|
||||
|
||||
/// Sum the character length of the conversation (user + assistant +
|
||||
/// tool content) as a cheap proxy for context size.
|
||||
fn conversation_chars(messages: &[ChatMessage]) -> usize {
|
||||
messages
|
||||
.iter()
|
||||
.map(|m| m.content.as_deref().map(str::len).unwrap_or(0))
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Track repeated tool-call errors so the loop can recover instead of
|
||||
/// burning iterations retrying the same failing tool.
|
||||
#[derive(Default)]
|
||||
struct ErrorTracker {
|
||||
consecutive: usize,
|
||||
total: usize,
|
||||
last_tool: String,
|
||||
last_error: String,
|
||||
}
|
||||
|
||||
impl ErrorTracker {
|
||||
fn record(&mut self, tool_name: &str, error: &str, messages: &mut Vec<ChatMessage>) {
|
||||
if self.last_tool == tool_name {
|
||||
self.consecutive += 1;
|
||||
} else {
|
||||
self.consecutive = 1;
|
||||
}
|
||||
self.last_tool = tool_name.to_string();
|
||||
self.last_error = error.to_string();
|
||||
self.total += 1;
|
||||
|
||||
// Inject a recovery note once the same tool keeps failing.
|
||||
if self.consecutive >= MAX_CONSECUTIVE_TOOL_ERRORS
|
||||
&& !messages.iter().any(|m| {
|
||||
m.content
|
||||
.as_deref()
|
||||
.is_some_and(|c| c.contains("[System note]"))
|
||||
})
|
||||
{
|
||||
messages.push(ChatMessage::system(
|
||||
zesdex_domain::agent::prompt::error_recovery_note(tool_name, error),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn should_stop(&self) -> bool {
|
||||
self.consecutive >= MAX_CONSECUTIVE_TOOL_ERRORS * 2 || self.total >= MAX_TOTAL_TOOL_ERRORS
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: execute a single tool call, push events, return the result string.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn execute_tool_call<T: ToolExecutor>(
|
||||
tool_executor: &T,
|
||||
turn_events: &Arc<Mutex<VecDeque<TurnEvent>>>,
|
||||
tc: &zesdex_domain::core::ToolCall,
|
||||
) -> String {
|
||||
let name = &tc.function.name;
|
||||
let args = zesdex_domain::core::tool_call::sanitize_tool_arguments(&tc.function.arguments);
|
||||
|
||||
debug!("executing tool: {name}");
|
||||
|
||||
let output = match tool_executor.execute(name, &args).await {
|
||||
Ok(o) => o,
|
||||
Err(e) => format!("Error: {e}"),
|
||||
};
|
||||
|
||||
let is_error = output.starts_with("Error:");
|
||||
let output = truncate_tool_output(output);
|
||||
|
||||
push_event(
|
||||
turn_events,
|
||||
TurnEvent::ToolResult {
|
||||
tool_call_id: tc.id.clone(),
|
||||
tool_name: name.clone(),
|
||||
output: output.clone(),
|
||||
is_error,
|
||||
path: None,
|
||||
},
|
||||
);
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: emit usage event from optional LLM response metadata.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn emit_usage(turn_events: &Arc<Mutex<VecDeque<TurnEvent>>>, usage: Option<(u64, u64)>) {
|
||||
if let Some((tokens_in, tokens_out)) = usage {
|
||||
push_event(
|
||||
turn_events,
|
||||
TurnEvent::Usage {
|
||||
tokens_in,
|
||||
tokens_out,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Service implementation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Service implementation for executing an agent turn asynchronously.
|
||||
///
|
||||
/// The turn loop is adaptive and token-aware:
|
||||
/// - No mandatory explore phase — the *agent* decides when to call the
|
||||
/// `explore_codebase` tool (see the main prompt), so simple queries skip
|
||||
/// exploration entirely.
|
||||
/// - `max_tokens` / `temperature` adapt to the request length and phase.
|
||||
/// - Repeated tool errors trigger a system recovery note and eventually
|
||||
/// stop the loop instead of burning iterations.
|
||||
/// - Tool outputs are truncated before entering context.
|
||||
/// - Oversized histories are auto-compacted before the next LLM call.
|
||||
pub struct AgentTurnServiceImpl<P: ProviderService, T: ToolExecutor> {
|
||||
provider: Arc<P>,
|
||||
tool_executor: Arc<T>,
|
||||
tool_defs: Vec<ToolDef>,
|
||||
}
|
||||
|
||||
impl<P: ProviderService, T: ToolExecutor> AgentTurnServiceImpl<P, T> {
|
||||
pub fn new(provider: Arc<P>, tool_executor: Arc<T>, tool_defs: Vec<ToolDef>) -> Self {
|
||||
Self {
|
||||
provider,
|
||||
tool_executor,
|
||||
tool_defs,
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a single LLM call with the current message list, handling
|
||||
/// streaming events and error reporting.
|
||||
async fn call_llm(
|
||||
&self,
|
||||
messages: &[ChatMessage],
|
||||
abort: &Arc<AtomicBool>,
|
||||
turn_events: &Arc<Mutex<VecDeque<TurnEvent>>>,
|
||||
max_tokens: u32,
|
||||
temperature: f32,
|
||||
) -> Result<(ChatMessage, Option<(u64, u64)>), String> {
|
||||
let on_event = make_stream_callback(abort, turn_events);
|
||||
|
||||
self.provider
|
||||
.chat_stream(
|
||||
messages,
|
||||
Some(self.tool_defs.clone()),
|
||||
Some(max_tokens),
|
||||
Some(temperature),
|
||||
on_event,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("LLM error: {e}"))
|
||||
}
|
||||
|
||||
/// Auto-compact the history in place if it exceeds the threshold.
|
||||
///
|
||||
/// Runs at most once per turn. Skips the synthetic system prompt that
|
||||
/// this service inserts at index 0.
|
||||
async fn auto_compact_if_needed(&self, messages: &mut Vec<ChatMessage>) {
|
||||
if conversation_chars(messages) <= AUTO_COMPACT_CHARS {
|
||||
return;
|
||||
}
|
||||
// Keep the system prompt (index 0) out of compaction.
|
||||
let sys = messages[0].clone();
|
||||
let mut rest: Vec<ChatMessage> = messages.drain(1..).collect();
|
||||
let before = rest.len();
|
||||
if let Err(e) = super::compact_messages_with_ai(&mut rest, self.provider.as_ref()).await {
|
||||
warn!("auto-compact failed (non-fatal): {e}");
|
||||
}
|
||||
info!(
|
||||
"auto-compacted history: {} messages -> {}",
|
||||
before,
|
||||
rest.len()
|
||||
);
|
||||
let mut rebuilt = Vec::with_capacity(rest.len() + 1);
|
||||
rebuilt.push(sys);
|
||||
rebuilt.extend(rest);
|
||||
*messages = rebuilt;
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: ProviderService, T: ToolExecutor> super::AgentTurnService for AgentTurnServiceImpl<P, T> {
|
||||
async fn run_turn(&self, mut params: AgentTurnParams) -> anyhow::Result<()> {
|
||||
info!(
|
||||
"Starting async agent turn with {} messages (model: {})",
|
||||
params.messages.len(),
|
||||
params.model
|
||||
);
|
||||
|
||||
// Insert system prompt at position 0 once and keep it there for the
|
||||
// entire turn, avoiding per-iteration clones of the full message list.
|
||||
params
|
||||
.messages
|
||||
.insert(0, ChatMessage::system(main_agent_prompt()));
|
||||
let original_count = params.messages.len();
|
||||
|
||||
// Estimate request complexity from the last user message.
|
||||
let request_len = params
|
||||
.messages
|
||||
.last()
|
||||
.and_then(|m| m.content.as_deref())
|
||||
.map(str::len)
|
||||
.unwrap_or(0);
|
||||
|
||||
let mut errors = ErrorTracker::default();
|
||||
// Track whether the previous call produced tool calls — used to
|
||||
// lower temperature once the agent starts producing a final answer.
|
||||
let mut saw_tool_calls = false;
|
||||
|
||||
for iteration in 0..MAX_TURN_ITERATIONS {
|
||||
// ── Check abort flag ────────────────────────────────────────
|
||||
if params.abort.load(Ordering::SeqCst) {
|
||||
params.abort.store(false, Ordering::SeqCst);
|
||||
push_event(
|
||||
¶ms.turn_events,
|
||||
TurnEvent::SystemNote {
|
||||
kind: "info".into(),
|
||||
message: "Turn aborted by user".into(),
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if errors.should_stop() {
|
||||
push_event(
|
||||
¶ms.turn_events,
|
||||
TurnEvent::SystemNote {
|
||||
kind: "warn".into(),
|
||||
message: "Stopping: repeated tool errors without progress".into(),
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
debug!("agent turn iteration {iteration}");
|
||||
|
||||
// ── Auto-compact oversized history before the LLM call ─────
|
||||
self.auto_compact_if_needed(&mut params.messages).await;
|
||||
|
||||
// ── Adaptive generation parameters ─────────────────────────
|
||||
let max_tokens = adaptive_max_tokens(request_len);
|
||||
// Lower temperature while the agent is still choosing tools to
|
||||
// keep tool selection deterministic; raise it for the final
|
||||
// free-form answer.
|
||||
let temperature = if saw_tool_calls { 0.2 } else { 0.7 };
|
||||
|
||||
// ── Stream start + call LLM ─────────────────────────────────
|
||||
push_event(¶ms.turn_events, TurnEvent::StreamStart);
|
||||
|
||||
let result = self
|
||||
.call_llm(
|
||||
¶ms.messages,
|
||||
¶ms.abort,
|
||||
¶ms.turn_events,
|
||||
max_tokens,
|
||||
temperature,
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok((assistant_msg, usage)) => {
|
||||
let content = assistant_msg.content.clone().unwrap_or_default();
|
||||
let tool_calls = assistant_msg.tool_calls.clone().unwrap_or_default();
|
||||
|
||||
push_event(
|
||||
¶ms.turn_events,
|
||||
TurnEvent::StreamDone(assistant_msg.clone()),
|
||||
);
|
||||
|
||||
emit_usage(¶ms.turn_events, usage);
|
||||
|
||||
// ── No tool calls → assistant is done ──────────────
|
||||
if tool_calls.is_empty() {
|
||||
params.messages.push(ChatMessage::assistant(Some(content)));
|
||||
break;
|
||||
}
|
||||
|
||||
saw_tool_calls = true;
|
||||
params.messages.push(assistant_msg);
|
||||
|
||||
// ── Execute each tool call ──────────────────────────
|
||||
for tc in &tool_calls {
|
||||
let output =
|
||||
execute_tool_call(self.tool_executor.as_ref(), ¶ms.turn_events, tc)
|
||||
.await;
|
||||
if output.starts_with("Error:") {
|
||||
errors.record(&tc.function.name, &output, &mut params.messages);
|
||||
}
|
||||
params
|
||||
.messages
|
||||
.push(ChatMessage::tool(tc.id.clone(), output));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("{e}");
|
||||
push_event(¶ms.turn_events, TurnEvent::Error(e));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the synthetic sys_msg before shipping events to the TUI
|
||||
// so the transcript shows only the actual user/assistant/tool exchange.
|
||||
let compacted: Vec<ChatMessage> = params.messages.drain(original_count - 1..).collect();
|
||||
push_event(¶ms.turn_events, TurnEvent::Compacted(compacted));
|
||||
push_event(¶ms.turn_events, TurnEvent::Done);
|
||||
params.in_flight.store(false, Ordering::SeqCst);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Conversation compaction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Maximum number of recent messages to preserve during compaction.
|
||||
const COMPACT_KEEP_TAIL: usize = 6;
|
||||
|
||||
/// Compacts conversation history using AI summarisation.
|
||||
///
|
||||
/// Flow: if the message count exceeds `KEEP_TAIL + 2`, the oldest messages
|
||||
/// are drained and summarised by the LLM. The summary is inserted as a
|
||||
/// system message at the head of the remaining history.
|
||||
pub async fn compact_messages_with_ai<P: ProviderService>(
|
||||
messages: &mut Vec<ChatMessage>,
|
||||
provider: &P,
|
||||
) -> anyhow::Result<()> {
|
||||
if messages.len() <= COMPACT_KEEP_TAIL + 2 {
|
||||
return Ok(()); // Not enough messages to compact
|
||||
}
|
||||
|
||||
let split_idx = messages.len() - COMPACT_KEEP_TAIL;
|
||||
let evicted: Vec<_> = messages.drain(..split_idx).collect();
|
||||
|
||||
let mut summary_prompt = vec![ChatMessage::system(zesdex_domain::compaction_prompt())];
|
||||
summary_prompt.extend(evicted);
|
||||
summary_prompt.push(ChatMessage::user(
|
||||
"Please summarise our previous conversation above for context continuity.".to_string(),
|
||||
));
|
||||
|
||||
match provider
|
||||
.chat(&summary_prompt, None, Some(1024), Some(0.3))
|
||||
.await
|
||||
{
|
||||
Ok((summary_msg, _)) => {
|
||||
let summary_text = summary_msg
|
||||
.content
|
||||
.unwrap_or_else(|| "Previous context summarised.".to_string());
|
||||
let summary_node = ChatMessage::system(format!(
|
||||
"[AI Summary of Previous Conversation]\n{}",
|
||||
summary_text.trim()
|
||||
));
|
||||
messages.insert(0, summary_node);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("AI summarisation failed during compact, falling back to simple notice: {e}");
|
||||
messages.insert(
|
||||
0,
|
||||
ChatMessage::system(
|
||||
"[Earlier conversation messages compacted to save context window]".to_string(),
|
||||
),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn truncate_short_output_is_unchanged() {
|
||||
let out = "short".to_string();
|
||||
assert_eq!(truncate_tool_output(out.clone()), out);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncate_long_output_preserves_head_and_marks_cut() {
|
||||
let long = "x".repeat(TOOL_OUTPUT_MAX_CHARS + 500);
|
||||
let truncated = truncate_tool_output(long.clone());
|
||||
assert!(truncated.len() < long.len());
|
||||
assert!(truncated.contains("...[truncated"));
|
||||
assert!(truncated.starts_with("xxx"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adaptive_max_tokens_scales_with_request_len() {
|
||||
assert_eq!(adaptive_max_tokens(10), 800);
|
||||
assert_eq!(adaptive_max_tokens(200), 1600);
|
||||
assert_eq!(adaptive_max_tokens(5000), 4096);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_tracker_injects_recovery_note_after_repeats() {
|
||||
let mut tracker = ErrorTracker::default();
|
||||
let mut messages: Vec<ChatMessage> = Vec::new();
|
||||
tracker.record("read", "Error: File not found", &mut messages);
|
||||
tracker.record("read", "Error: File not found", &mut messages);
|
||||
assert!(!tracker.should_stop());
|
||||
// Third consecutive failure → recovery note injected.
|
||||
tracker.record("read", "Error: File not found", &mut messages);
|
||||
assert!(messages.iter().any(|m| m
|
||||
.content
|
||||
.as_deref()
|
||||
.is_some_and(|c| c.contains("[System note]"))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_tracker_stops_after_too_many_errors() {
|
||||
let mut tracker = ErrorTracker::default();
|
||||
let mut messages: Vec<ChatMessage> = Vec::new();
|
||||
for i in 0..MAX_TOTAL_TOOL_ERRORS {
|
||||
tracker.record("bash", &format!("Error: boom {i}"), &mut messages);
|
||||
}
|
||||
assert!(tracker.should_stop());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conversation_chars_sums_content_only() {
|
||||
let messages = vec![
|
||||
ChatMessage::system("sys".to_string()),
|
||||
ChatMessage::user("hello world".to_string()),
|
||||
ChatMessage::tool("id".to_string(), "output".to_string()),
|
||||
];
|
||||
assert_eq!(conversation_chars(&messages), 3 + 11 + 6);
|
||||
}
|
||||
}
|
||||
@@ -45,11 +45,7 @@ use sha2::{Digest, Sha256};
|
||||
/// and clear them after a successful (or failed) flow completion.
|
||||
pub trait OAuthFlowStore: Send + Sync {
|
||||
/// Persist the PKCE code verifier and CSRF state token.
|
||||
fn save_flow_state(
|
||||
&self,
|
||||
verifier: &str,
|
||||
state: &str,
|
||||
) -> Result<(), ServiceError>;
|
||||
fn save_flow_state(&self, verifier: &str, state: &str) -> Result<(), ServiceError>;
|
||||
|
||||
/// Load the stored PKCE code verifier.
|
||||
fn load_verifier(&self) -> Result<String, ServiceError>;
|
||||
@@ -104,7 +100,7 @@ fn generate_pkce_pair() -> (String, String) {
|
||||
bytes[..16].copy_from_slice(uuid::Uuid::new_v4().as_bytes());
|
||||
bytes[16..].copy_from_slice(uuid::Uuid::new_v4().as_bytes());
|
||||
|
||||
let verifier = URL_SAFE_NO_PAD.encode(&bytes);
|
||||
let verifier = URL_SAFE_NO_PAD.encode(bytes);
|
||||
let challenge = {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(verifier.as_bytes());
|
||||
@@ -141,12 +137,7 @@ pub struct OAuthUseCase<R, S, E> {
|
||||
|
||||
impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger> OAuthUseCase<R, S, E> {
|
||||
/// Create a new OAuth use-case.
|
||||
pub fn new(
|
||||
token_repo: R,
|
||||
flow_store: S,
|
||||
token_exchanger: E,
|
||||
token_path: PathBuf,
|
||||
) -> Self {
|
||||
pub fn new(token_repo: R, flow_store: S, token_exchanger: E, token_path: PathBuf) -> Self {
|
||||
OAuthUseCase {
|
||||
token_repo,
|
||||
flow_store,
|
||||
@@ -156,8 +147,8 @@ impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger> OAuthUseCase<R, S
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger>
|
||||
zesdex_domain::auth::OAuthService for OAuthUseCase<R, S, E>
|
||||
impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger> zesdex_domain::auth::OAuthService
|
||||
for OAuthUseCase<R, S, E>
|
||||
{
|
||||
fn start_flow(
|
||||
&self,
|
||||
@@ -183,13 +174,9 @@ impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger>
|
||||
"starting OAuth flow",
|
||||
);
|
||||
|
||||
let mut url = url::Url::parse(&config.auth_url)
|
||||
.map_err(|e| {
|
||||
ServiceError::InvalidConfig(format!(
|
||||
"invalid auth_url '{}': {e}",
|
||||
config.auth_url
|
||||
))
|
||||
})?;
|
||||
let mut url = url::Url::parse(&config.auth_url).map_err(|e| {
|
||||
ServiceError::InvalidConfig(format!("invalid auth_url '{}': {e}", config.auth_url))
|
||||
})?;
|
||||
|
||||
url.query_pairs_mut()
|
||||
.append_pair("response_type", "code")
|
||||
|
||||
@@ -46,12 +46,11 @@ impl<R: SessionRepository, L: SessionLockRepository> SessionServiceImpl<R, L> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: SessionRepository, L: SessionLockRepository>
|
||||
zesdex_domain::auth::SessionService for SessionServiceImpl<R, L>
|
||||
impl<R: SessionRepository, L: SessionLockRepository> zesdex_domain::auth::SessionService
|
||||
for SessionServiceImpl<R, L>
|
||||
{
|
||||
fn create_session(&self, title: &str) -> Result<Session, ServiceError> {
|
||||
let id = SessionId::new(&Uuid::new_v4().to_string())
|
||||
.map_err(|e| ServiceError::Other(e))?;
|
||||
let id = SessionId::new(&Uuid::new_v4().to_string()).map_err(ServiceError::Other)?;
|
||||
let title_owned = if title.is_empty() {
|
||||
"New Session".to_string()
|
||||
} else {
|
||||
@@ -59,8 +58,7 @@ impl<R: SessionRepository, L: SessionLockRepository>
|
||||
};
|
||||
let session = Session::new(id.into_string(), title_owned);
|
||||
tracing::debug!(session_id = %session.id, title = %session.title, "creating new session");
|
||||
self.session_repo
|
||||
.save_session(&self.base_dir, &session)?;
|
||||
self.session_repo.save_session(&self.base_dir, &session)?;
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
@@ -73,17 +71,14 @@ impl<R: SessionRepository, L: SessionLockRepository>
|
||||
|
||||
fn archive_session(&self, id: SessionId) -> Result<(), ServiceError> {
|
||||
tracing::debug!(session_id = %id, "archiving session");
|
||||
let mut session = self
|
||||
.session_repo
|
||||
.load_session(&self.base_dir, &id)?;
|
||||
let mut session = self.session_repo.load_session(&self.base_dir, &id)?;
|
||||
session.archived = true;
|
||||
let millis = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis();
|
||||
session.updated_at = i64::try_from(millis).unwrap_or(i64::MAX);
|
||||
self.session_repo
|
||||
.save_session(&self.base_dir, &session)?;
|
||||
self.session_repo.save_session(&self.base_dir, &session)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,11 +58,7 @@ impl<R: ConversationRepository> zesdex_domain::cms::ConversationService
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_message(
|
||||
&self,
|
||||
conv: &mut Conversation,
|
||||
msg: ChatMessage,
|
||||
) -> Result<(), ServiceError> {
|
||||
fn add_message(&self, conv: &mut Conversation, msg: ChatMessage) -> Result<(), ServiceError> {
|
||||
tracing::debug!("adding message to session {}", conv.session_id);
|
||||
conv.push(msg);
|
||||
let dir = self.session_dir(&conv.session_id);
|
||||
|
||||
@@ -14,8 +14,7 @@ use std::path::PathBuf;
|
||||
use tracing;
|
||||
|
||||
use zesdex_domain::cms::{
|
||||
AppConfig, AppConfigRepository, ProviderConfig, ServiceError, Settings,
|
||||
SettingsRepository,
|
||||
AppConfig, AppConfigRepository, ProviderConfig, ServiceError, Settings, SettingsRepository,
|
||||
};
|
||||
|
||||
/// Service implementation for settings and app-config operations.
|
||||
@@ -30,11 +29,7 @@ pub struct SettingsServiceImpl<S, C> {
|
||||
|
||||
impl<S: SettingsRepository, C: AppConfigRepository> SettingsServiceImpl<S, C> {
|
||||
/// Create a new service with the given repositories and base directory.
|
||||
pub fn new(
|
||||
settings_repo: S,
|
||||
app_config_repo: C,
|
||||
base_dir: impl Into<PathBuf>,
|
||||
) -> Self {
|
||||
pub fn new(settings_repo: S, app_config_repo: C, base_dir: impl Into<PathBuf>) -> Self {
|
||||
tracing::debug!("creating SettingsServiceImpl");
|
||||
Self {
|
||||
settings_repo,
|
||||
@@ -44,8 +39,8 @@ impl<S: SettingsRepository, C: AppConfigRepository> SettingsServiceImpl<S, C> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: SettingsRepository, C: AppConfigRepository>
|
||||
zesdex_domain::cms::SettingsService for SettingsServiceImpl<S, C>
|
||||
impl<S: SettingsRepository, C: AppConfigRepository> zesdex_domain::cms::SettingsService
|
||||
for SettingsServiceImpl<S, C>
|
||||
{
|
||||
fn load_settings(&self) -> Result<Settings, ServiceError> {
|
||||
tracing::debug!("loading settings");
|
||||
@@ -60,11 +55,7 @@ impl<S: SettingsRepository, C: AppConfigRepository>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_provider(
|
||||
&self,
|
||||
name: &str,
|
||||
config: &ProviderConfig,
|
||||
) -> Result<(), ServiceError> {
|
||||
fn update_provider(&self, name: &str, config: &ProviderConfig) -> Result<(), ServiceError> {
|
||||
tracing::debug!("updating provider '{name}'");
|
||||
let mut app_config: AppConfig = self.app_config_repo.load(&self.base_dir)?;
|
||||
app_config
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
//! the use-case logic independent of any specific persistence or infrastructure
|
||||
//! technology.
|
||||
|
||||
pub mod agent;
|
||||
pub mod auth;
|
||||
pub mod cms;
|
||||
pub mod ports;
|
||||
@@ -45,7 +46,11 @@ pub use auth::{
|
||||
|
||||
// Re-export CMS use-cases.
|
||||
pub use cms::{
|
||||
conversation_service::ConversationServiceImpl,
|
||||
memory_service::MemoryServiceImpl,
|
||||
conversation_service::ConversationServiceImpl, memory_service::MemoryServiceImpl,
|
||||
settings_service::SettingsServiceImpl,
|
||||
};
|
||||
|
||||
pub use agent::{
|
||||
turn_service::{compact_messages_with_ai, AgentTurnServiceImpl},
|
||||
AgentTurnService, ToolExecutor,
|
||||
};
|
||||
|
||||
@@ -22,11 +22,8 @@ pub trait AuthService: Send + Sync {
|
||||
/// Authenticate a user by verifying a password against a stored hash.
|
||||
///
|
||||
/// Returns `true` if the password matches, `false` otherwise.
|
||||
fn authenticate(
|
||||
&self,
|
||||
password: &str,
|
||||
hash: &str,
|
||||
) -> impl Future<Output = Result<bool>> + Send;
|
||||
fn authenticate(&self, password: &str, hash: &str)
|
||||
-> impl Future<Output = Result<bool>> + Send;
|
||||
|
||||
/// Issue a new access + refresh token pair for the given subject.
|
||||
///
|
||||
|
||||
@@ -23,4 +23,10 @@ pub trait TokenService: Send + Sync {
|
||||
/// Returns `Err` if the token is expired, malformed, or has an
|
||||
/// invalid signature.
|
||||
fn verify_access_token(&self, token: &str) -> Result<String>;
|
||||
|
||||
/// Verify a refresh token and return the embedded subject claim.
|
||||
///
|
||||
/// Returns `Err` if the token is expired, malformed, or has an
|
||||
/// invalid signature.
|
||||
fn verify_refresh_token(&self, token: &str) -> Result<String>;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@ fn main() -> anyhow::Result<()> {
|
||||
if !settings_path.exists() {
|
||||
let settings = zesdex_domain::cms::Settings::default();
|
||||
let content = serde_json::to_string_pretty(&settings)?;
|
||||
std::fs::write(&settings_path, content)?;
|
||||
let tmp = store.base_dir.join("settings.json.tmp");
|
||||
std::fs::write(&tmp, &content)?;
|
||||
std::fs::File::open(&tmp)?.sync_all()?;
|
||||
std::fs::rename(&tmp, &settings_path)?;
|
||||
println!(" ✓ Default settings created");
|
||||
} else {
|
||||
println!(" · Settings already exist, skipping");
|
||||
@@ -27,7 +30,10 @@ fn main() -> anyhow::Result<()> {
|
||||
if !config_path.exists() {
|
||||
let config = zesdex_domain::cms::AppConfig::default();
|
||||
let content = serde_json::to_string_pretty(&config)?;
|
||||
std::fs::write(&config_path, content)?;
|
||||
let tmp = store.base_dir.join("app_config.json.tmp");
|
||||
std::fs::write(&tmp, &content)?;
|
||||
std::fs::File::open(&tmp)?.sync_all()?;
|
||||
std::fs::rename(&tmp, &config_path)?;
|
||||
println!(" ✓ Default app_config created");
|
||||
} else {
|
||||
println!(" · App config already exists, skipping");
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
//! Shared default constants used across the application.
|
||||
//!
|
||||
//! Centralising these values eliminates the hardcoded-string duplication
|
||||
//! that existed when every call site provided its own inline fallback.
|
||||
//! Consumers should reference these constants rather than repeating
|
||||
//! the string literals.
|
||||
|
||||
/// Default LLM provider API base URL.
|
||||
pub const DEFAULT_API_BASE: &str = "https://opencode.ai/zen/v1";
|
||||
|
||||
/// Default LLM model identifier.
|
||||
pub const DEFAULT_MODEL: &str = "deepseek-v4-flash-free";
|
||||
|
||||
/// Fallback JWT secret used only when `JWT_SECRET` env var is unset.
|
||||
/// In production this MUST be configured via environment variable.
|
||||
pub const FALLBACK_JWT_SECRET: &str = "dev-secret";
|
||||
|
||||
/// Default context window size (128k tokens).
|
||||
pub const DEFAULT_CONTEXT_WINDOW: usize = 256_000;
|
||||
|
||||
/// Maximum tool-call iterations per agent turn.
|
||||
pub const MAX_TOOL_ITERATIONS: u32 = 50;
|
||||
|
||||
/// Maximum subagent tool-call iterations.
|
||||
pub const MAX_SUBAGENT_ITERATIONS: u32 = 25;
|
||||
|
||||
/// Default LLM request max tokens.
|
||||
pub const DEFAULT_MAX_TOKENS: u32 = 4096;
|
||||
|
||||
/// Default temperature for the main agent.
|
||||
pub const DEFAULT_TEMPERATURE: f64 = 0.7;
|
||||
|
||||
/// Default temperature for compaction / summary calls.
|
||||
pub const DEFAULT_COMPACT_TEMPERATURE: f64 = 0.3;
|
||||
@@ -0,0 +1,260 @@
|
||||
//! Domain types for agent lifecycle: turn events, session runtime, progress
|
||||
//! reporting, prompts, and the agent-turn parameter bundle.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::core::{ChatMessage, ToolCallResult, UsageStats};
|
||||
|
||||
pub mod defaults;
|
||||
pub mod progress;
|
||||
pub mod prompt;
|
||||
|
||||
/// Which kind of caller (main agent vs. subagent vs. reviewer) is
|
||||
/// invoking a tool, used to scope permissions and tag log/output paths.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
pub enum Origin {
|
||||
/// The main agent turn loop.
|
||||
Main,
|
||||
/// A spawned subagent (test-gen, arch-review, security-review, etc.).
|
||||
SubAgent,
|
||||
/// The auto-inline review step after an edit.
|
||||
Reviewer,
|
||||
}
|
||||
|
||||
impl Origin {
|
||||
/// Short string tag for this origin, used in filenames and logs.
|
||||
pub fn tag(self) -> String {
|
||||
match self {
|
||||
Origin::Main => "main",
|
||||
Origin::SubAgent => "subagent",
|
||||
Origin::Reviewer => "reviewer",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Severity/category of a toast notification, used to pick its color.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ToastKind {
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Error,
|
||||
Lesson,
|
||||
}
|
||||
|
||||
/// A transient status message shown in the TUI, auto-dismissed after `lifetime_ms`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Toast {
|
||||
pub kind: ToastKind,
|
||||
pub message: String,
|
||||
pub created_at: i64,
|
||||
pub lifetime_ms: u64,
|
||||
}
|
||||
|
||||
impl Toast {
|
||||
/// Create a toast with a default 5-second lifetime, stamped with now.
|
||||
pub fn new(kind: ToastKind, message: String) -> Self {
|
||||
Toast {
|
||||
kind,
|
||||
message,
|
||||
created_at: chrono::Utc::now().timestamp_millis(),
|
||||
lifetime_ms: 5000,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this toast's lifetime has elapsed as of `now_ms`.
|
||||
pub fn expired(&self, now_ms: i64) -> bool {
|
||||
let lifetime = self.lifetime_ms as i64;
|
||||
now_ms - self.created_at > lifetime
|
||||
}
|
||||
}
|
||||
|
||||
/// Agent status for workflow engine progress tracking.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum AgentStatus {
|
||||
Pending,
|
||||
Running,
|
||||
Completed,
|
||||
Failed(String),
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AgentStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AgentStatus::Pending => write!(f, "pending"),
|
||||
AgentStatus::Running => write!(f, "running"),
|
||||
AgentStatus::Completed => write!(f, "completed"),
|
||||
AgentStatus::Failed(msg) => write!(f, "failed: {msg}"),
|
||||
AgentStatus::Cancelled => write!(f, "cancelled"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Events emitted onto the turn-event queue while an agent turn runs,
|
||||
/// consumed by the event loop to update state and drive re-renders.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TurnEvent {
|
||||
AssistantMessage(ChatMessage),
|
||||
ToolResult {
|
||||
tool_call_id: String,
|
||||
tool_name: String,
|
||||
output: String,
|
||||
is_error: bool,
|
||||
path: Option<String>,
|
||||
},
|
||||
SystemNote {
|
||||
kind: String,
|
||||
message: String,
|
||||
},
|
||||
StreamStart,
|
||||
StreamToken(String),
|
||||
StreamReasoning(String),
|
||||
StreamDone(ChatMessage),
|
||||
Usage {
|
||||
tokens_in: u64,
|
||||
tokens_out: u64,
|
||||
},
|
||||
ReviewUsage {
|
||||
tokens_in: u64,
|
||||
tokens_out: u64,
|
||||
},
|
||||
Compacted(Vec<ChatMessage>),
|
||||
Error(String),
|
||||
Done,
|
||||
WorkflowAgentUpdate {
|
||||
agent_id: String,
|
||||
agent_name: String,
|
||||
status: AgentStatus,
|
||||
},
|
||||
TodoUpdate(String),
|
||||
PlanUpdate(String),
|
||||
/// Structured progress report from a subagent or workflow node,
|
||||
/// carrying the current tool name and optional step counters.
|
||||
AgentProgress(crate::agent::progress::AgentProgress),
|
||||
}
|
||||
|
||||
/// How a pending tool call should be executed when the turn resumes.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ExecutionModel {
|
||||
Inline,
|
||||
Deferred,
|
||||
AsyncTokio,
|
||||
}
|
||||
|
||||
/// A tool call awaiting execution, along with which execution model
|
||||
/// (inline, deferred, async) it should run under.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PendingTool {
|
||||
pub tool_name: String,
|
||||
pub args: serde_json::Value,
|
||||
pub execution_model: ExecutionModel,
|
||||
}
|
||||
|
||||
/// Reference to a background bash job tracked in session state.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BashJobRef {
|
||||
pub id: String,
|
||||
pub command: String,
|
||||
pub started_at: i64,
|
||||
pub running: bool,
|
||||
}
|
||||
|
||||
/// Tracks counts of learned patterns by outcome and lifecycle stage.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct LessonStats {
|
||||
/// Total number of lessons tracked.
|
||||
pub total: u32,
|
||||
/// User-initiated lessons.
|
||||
pub user: u32,
|
||||
/// Feedback-driven lessons.
|
||||
pub feedback: u32,
|
||||
/// Project-scoped lessons.
|
||||
pub project: u32,
|
||||
/// Reference-scoped lessons.
|
||||
pub reference: u32,
|
||||
/// Currently active lessons.
|
||||
pub active: u32,
|
||||
/// Stale (outdated) lessons.
|
||||
pub stale: u32,
|
||||
/// Contradicted lessons.
|
||||
pub contradicted: u32,
|
||||
/// Human-authored lessons.
|
||||
pub human: u32,
|
||||
/// Verified lessons.
|
||||
pub verified: u32,
|
||||
/// Unverified lessons.
|
||||
pub unverified: u32,
|
||||
}
|
||||
|
||||
/// Per-session runtime state: message history, pending tool queue,
|
||||
/// background bash jobs, lesson/review counters.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SessionRuntime {
|
||||
pub messages: Vec<ChatMessage>,
|
||||
pub tool_call_results: Vec<ToolCallResult>,
|
||||
pub pending_tool_queue: Vec<PendingTool>,
|
||||
pub bash_jobs: Vec<BashJobRef>,
|
||||
pub subagent_queue: usize,
|
||||
pub edit_count: u32,
|
||||
pub consecutive_empty_reviews: u32,
|
||||
pub session_start: i64,
|
||||
/// Aggregated lesson statistics.
|
||||
pub lessons: LessonStats,
|
||||
pub review_count: u32,
|
||||
pub session_dir: PathBuf,
|
||||
pub usage: UsageStats,
|
||||
pub hive_mind_converged: bool,
|
||||
}
|
||||
|
||||
impl SessionRuntime {
|
||||
pub fn new(session_dir: PathBuf) -> Self {
|
||||
SessionRuntime {
|
||||
messages: Vec::new(),
|
||||
tool_call_results: Vec::new(),
|
||||
pending_tool_queue: Vec::new(),
|
||||
bash_jobs: Vec::new(),
|
||||
subagent_queue: 0,
|
||||
edit_count: 0,
|
||||
consecutive_empty_reviews: 0,
|
||||
session_start: chrono::Utc::now().timestamp_millis(),
|
||||
lessons: LessonStats::default(),
|
||||
review_count: 0,
|
||||
session_dir,
|
||||
usage: UsageStats::default(),
|
||||
hive_mind_converged: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_message(&mut self, msg: ChatMessage) {
|
||||
self.messages.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple ASCII progress display for a long-running operation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProgressState {
|
||||
pub current: u64,
|
||||
pub total: u64,
|
||||
pub message: String,
|
||||
pub start_time: i64,
|
||||
}
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// Owned parameters required to spawn and execute an agent turn.
|
||||
pub struct AgentTurnParams {
|
||||
pub messages: Vec<ChatMessage>,
|
||||
pub session_dir: PathBuf,
|
||||
pub workspace_roots: Vec<PathBuf>,
|
||||
pub turn_events: Arc<Mutex<VecDeque<TurnEvent>>>,
|
||||
pub in_flight: Arc<AtomicBool>,
|
||||
pub abort: Arc<AtomicBool>,
|
||||
pub api_key: String,
|
||||
pub model: String,
|
||||
pub api_base: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//! Progress reporting types for long-running agent and subagent operations.
|
||||
//!
|
||||
//! These types are emitted onto the turn-event queue to drive the TUI's
|
||||
//! spinner, progress bar, and agent-status sidebar. They are pure domain
|
||||
//! types with no I/O or framework dependency.
|
||||
|
||||
use crate::agent::AgentStatus;
|
||||
|
||||
/// Describes progress within a single subagent or workflow-node execution.
|
||||
///
|
||||
/// Emitted as a `TurnEvent::AgentProgress` so the UI can show which tool
|
||||
/// the subagent is currently invoking, or which step it has reached.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AgentProgress {
|
||||
/// Unique identifier for this agent (e.g. `"Node-0-1"`, `"auto-review"`).
|
||||
pub agent_id: String,
|
||||
/// Human-readable display name shown in the TUI sidebar.
|
||||
pub agent_name: String,
|
||||
/// Current lifecycle status.
|
||||
pub status: AgentStatus,
|
||||
/// Optional description of the current tool or step being executed.
|
||||
/// Set to `None` when the agent is not actively executing a tool.
|
||||
pub current_tool: Option<String>,
|
||||
/// Optional progress range: (completed_steps, total_steps).
|
||||
/// When `None`, the agent shows an indeterminate spinner.
|
||||
pub steps: Option<(u32, u32)>,
|
||||
}
|
||||
|
||||
impl AgentProgress {
|
||||
/// Mark this agent as running with an optional tool name.
|
||||
pub fn running(
|
||||
agent_id: impl Into<String>,
|
||||
agent_name: impl Into<String>,
|
||||
current_tool: Option<String>,
|
||||
) -> Self {
|
||||
AgentProgress {
|
||||
agent_id: agent_id.into(),
|
||||
agent_name: agent_name.into(),
|
||||
status: AgentStatus::Running,
|
||||
current_tool,
|
||||
steps: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark this agent as pending (queued but not yet started).
|
||||
pub fn pending(agent_id: impl Into<String>, agent_name: impl Into<String>) -> Self {
|
||||
AgentProgress {
|
||||
agent_id: agent_id.into(),
|
||||
agent_name: agent_name.into(),
|
||||
status: AgentStatus::Pending,
|
||||
current_tool: None,
|
||||
steps: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark this agent as completed successfully.
|
||||
pub fn completed(agent_id: impl Into<String>, agent_name: impl Into<String>) -> Self {
|
||||
AgentProgress {
|
||||
agent_id: agent_id.into(),
|
||||
agent_name: agent_name.into(),
|
||||
status: AgentStatus::Completed,
|
||||
current_tool: None,
|
||||
steps: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark this agent as failed with an error message.
|
||||
pub fn failed(
|
||||
agent_id: impl Into<String>,
|
||||
agent_name: impl Into<String>,
|
||||
error: String,
|
||||
) -> Self {
|
||||
AgentProgress {
|
||||
agent_id: agent_id.into(),
|
||||
agent_name: agent_name.into(),
|
||||
status: AgentStatus::Failed(error),
|
||||
current_tool: None,
|
||||
steps: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
//! System prompts and directive templates for agent and subagent turns.
|
||||
//!
|
||||
//! Centralising all prompt text here keeps the core turn logic free of
|
||||
//! hardcoded prose, making prompts easier to maintain, review, and localise.
|
||||
//!
|
||||
//! # Flow
|
||||
//! The application layer's `AgentTurnServiceImpl` calls `main_agent_prompt()`
|
||||
//! to construct the system message at the start of each turn. Subagent and
|
||||
//! review prompts are provided by their respective modules.
|
||||
|
||||
/// Build the main-agent system prompt.
|
||||
///
|
||||
/// The prompt establishes the agent's identity as Zesdex, an AI coding
|
||||
/// assistant, and defines the priority hierarchy that governs tool selection:
|
||||
///
|
||||
/// 1. **Workflow first** — `workflow_run` / `hive_mind` for complex tasks
|
||||
/// 2. **Planning & TODOs** — `plan_enter` / `todowrite` for structural work
|
||||
/// 3. **Reasoning** — `seq_think` for deep analysis
|
||||
/// 4. **Tool execution** — direct tools for simple actions
|
||||
pub fn main_agent_prompt() -> String {
|
||||
"\
|
||||
You are Zesdex, an AI coding assistant. You have access to various tools \
|
||||
via native function calling to help the user.
|
||||
|
||||
TOKEN BUDGET — BE EFFICIENT:
|
||||
- For simple/factual questions, answer directly. Do NOT call tools.
|
||||
- For complex or unfamiliar code tasks, call `explore_codebase` ONCE at the \
|
||||
start to locate relevant code, then work from that context.
|
||||
- Keep tool usage minimal: prefer `grep`/`glob`/`read` for targeted lookups; \
|
||||
avoid re-reading files you already have in context.
|
||||
- Keep responses concise; do not repeat tool output verbatim.
|
||||
|
||||
CRITICAL DIRECTIVES & PRIORITY HIERARCHY:
|
||||
1. WORKFLOW FIRST: For any multi-step, complex, or non-trivial task, \
|
||||
you MUST prioritise using `workflow_run` (to construct and execute a \
|
||||
multi-phase YAML workflow) or `hive_mind` (to orchestrate parallel \
|
||||
autonomous agents). Workflows are your primary strategy.
|
||||
2. PLANNING & TODOs: Use `plan_enter` to establish high-level \
|
||||
architectural plans and `todowrite` to maintain granular task checklists.
|
||||
3. REASONING: Use `seq_think` for deep step-by-step analysis.
|
||||
4. TOOL EXECUTION: Execute individual tools (file edits, terminal commands) \
|
||||
within or guided by your workflows. If an error occurs, analyse and fix it.
|
||||
|
||||
Respond conversationally, concisely, and helpfully."
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Build a subagent directive prompt.
|
||||
///
|
||||
/// The directive is embedded in a system message that also communicates the
|
||||
/// current working directory and workspace root so the subagent can resolve
|
||||
/// paths correctly.
|
||||
pub fn subagent_directive(directive: &str, cwd: &str, ws_root: &str) -> String {
|
||||
format!(
|
||||
"\
|
||||
You are a focused subagent.
|
||||
|
||||
Current directory (PWD): {cwd}
|
||||
Workspace root: {ws_root}
|
||||
|
||||
Your directive:
|
||||
{directive}
|
||||
|
||||
Complete the directive autonomously using the tools available to you. \
|
||||
Return your final answer when done."
|
||||
)
|
||||
}
|
||||
|
||||
/// Build a conversation-compaction prompt.
|
||||
///
|
||||
/// The LLM is asked to produce a concise bulleted summary of the key
|
||||
/// requests, decisions, tools executed, and files modified.
|
||||
pub fn compaction_prompt() -> String {
|
||||
"\
|
||||
You are a helpful assistant summarising conversation history. \
|
||||
Provide a concise summary of the key user requests, decisions, tools \
|
||||
executed, and modified files. Format as a clear bulleted list."
|
||||
.to_string()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Adaptive explore: directives
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Directive for a single lightweight context-scout subagent.
|
||||
pub fn explore_scout_directive() -> String {
|
||||
"\
|
||||
You are a codebase context scout. \
|
||||
Given the workspace root, quickly locate the code that is most relevant \
|
||||
to the user's request: \
|
||||
1. Run semantic_search once with the user's key terms. \
|
||||
2. Read up to the 3 most relevant files (use grep for symbols if needed). \
|
||||
3. Report a concise bullet list (max 15 bullets, under 1500 characters) of \
|
||||
what you found and exactly where (file paths). \
|
||||
Do NOT rebuild the index. Do NOT enumerate unrelated files. Be brief."
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Build a system note injected after repeated tool errors to steer the
|
||||
/// agent toward an alternative approach instead of retrying the same call.
|
||||
pub fn error_recovery_note(tool_name: &str, last_error: &str) -> String {
|
||||
format!(
|
||||
"\
|
||||
[System note] The tool `{tool_name}` failed repeatedly with: \"{last_error}\". \
|
||||
Try an alternative approach (verify paths, correct arguments, use a \
|
||||
different tool, or finish without this tool). Do NOT retry the same call."
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn main_prompt_is_non_empty() {
|
||||
let prompt = main_agent_prompt();
|
||||
assert!(!prompt.is_empty());
|
||||
assert!(prompt.contains("Zesdex"));
|
||||
assert!(prompt.contains("WORKFLOW FIRST"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subagent_directive_includes_directive_text() {
|
||||
let prompt = subagent_directive("test directive", "/home", "/home/project");
|
||||
assert!(prompt.contains("test directive"));
|
||||
assert!(prompt.contains("/home"));
|
||||
assert!(prompt.contains("/home/project"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explore_scout_directive_is_concise_and_mentions_tools() {
|
||||
let scout = explore_scout_directive();
|
||||
assert!(scout.contains("scout"));
|
||||
assert!(scout.contains("semantic_search"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_recovery_note_suggests_alternative() {
|
||||
let note = error_recovery_note("read", "File not found");
|
||||
assert!(note.contains("read"));
|
||||
assert!(note.contains("alternative"));
|
||||
}
|
||||
}
|
||||
@@ -104,36 +104,52 @@ impl SessionLock {
|
||||
let _ = fs::remove_file(&self.path);
|
||||
}
|
||||
|
||||
/// Check whether a process with the given PID is currently alive.
|
||||
/// Check whether a process with the given PID is currently alive and
|
||||
/// belongs to the same binary (mitigating PID-reuse races).
|
||||
///
|
||||
/// Strategy (Unix):
|
||||
/// 1. Resolve `/proc/<pid>/exe` — if it doesn't match our own binary,
|
||||
/// the PID either belongs to another process or is reused — return false.
|
||||
/// 2. Send `kill(pid, 0)` to verify the process is still alive.
|
||||
/// 3. Re-check `/proc/<pid>/exe` to close the TOCTOU window between
|
||||
/// step 1 and step 2 (PID reuse after exe check, before kill).
|
||||
///
|
||||
/// Uses `kill(pid, 0)` on Unix via the `nix` or `libc` crate in production;
|
||||
/// here we provide a best-effort check using the process table.
|
||||
/// On non-Unix platforms this always returns `true` (conservative).
|
||||
fn is_alive(pid: u32) -> bool {
|
||||
// On Unix, signal 0 checks process existence without sending a signal.
|
||||
#[cfg(unix)]
|
||||
{
|
||||
// SAFETY: `libc::kill(pid, 0)` does not send a signal; it only checks
|
||||
// whether the process exists and the caller has permission to signal it.
|
||||
// The integer argument is a PID validated by `try_lock`.
|
||||
// Resolve our own executable path once.
|
||||
let self_exe = match std::fs::read_link("/proc/self/exe") {
|
||||
Ok(exe) => exe,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
let pid_signed: i32 = match pid.try_into() {
|
||||
Ok(p) => p,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
let proc_exe = std::path::PathBuf::from(format!("/proc/{pid}/exe"));
|
||||
|
||||
// Phase 1: read /proc/<pid>/exe and compare with self_exe.
|
||||
let target = match std::fs::read_link(&proc_exe) {
|
||||
Ok(t) => t,
|
||||
Err(_) => return false,
|
||||
};
|
||||
if target != self_exe {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Phase 2: verify the process is still alive.
|
||||
// SAFETY: `libc::kill(pid, 0)` does not send a signal; it only checks
|
||||
// whether the process exists and the caller has permission to signal it.
|
||||
if unsafe { libc::kill(pid_signed, 0) != 0 } {
|
||||
return false;
|
||||
}
|
||||
// Extra check: verify the PID belongs to a zesdex process via
|
||||
// /proc/<pid>/exe to mitigate the PID-reuse race.
|
||||
let proc_exe = std::path::PathBuf::from(format!("/proc/{pid}/exe"));
|
||||
if let Ok(target) = std::fs::read_link(&proc_exe) {
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if target != exe {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
|
||||
// Phase 3: re-check /proc/<pid>/exe to detect PID reuse between
|
||||
// Phase 1 and Phase 2.
|
||||
matches!(std::fs::read_link(&proc_exe), Ok(recheck) if recheck == self_exe)
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
|
||||
@@ -46,10 +46,6 @@ pub struct SettingsPatch {
|
||||
pub review_enabled: Option<bool>,
|
||||
/// Override the session-archive-enabled flag.
|
||||
pub session_archive_enabled: Option<bool>,
|
||||
/// Override the LSP auto-provision flag.
|
||||
pub lsp_auto_provision: Option<bool>,
|
||||
/// Override the list of LSP-managed languages.
|
||||
pub lsp_languages: Option<Vec<String>>,
|
||||
/// Override the hive-mind node timeout in milliseconds.
|
||||
pub hive_mind_node_timeout_ms: Option<u64>,
|
||||
}
|
||||
@@ -111,12 +107,6 @@ impl SettingsPatch {
|
||||
if let Some(val) = self.session_archive_enabled {
|
||||
settings.flags.session_archive_enabled = val;
|
||||
}
|
||||
if let Some(val) = self.lsp_auto_provision {
|
||||
settings.flags.lsp_auto_provision = val;
|
||||
}
|
||||
if let Some(ref val) = self.lsp_languages {
|
||||
settings.lsp_languages = val.clone();
|
||||
}
|
||||
if let Some(val) = self.hive_mind_node_timeout_ms {
|
||||
settings.hive_mind_node_timeout_ms = val;
|
||||
}
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
//! - `ChatMessage` — a single message with role, content, and tool metadata
|
||||
//! - `Role` — message role enum (User, Assistant, System, Tool)
|
||||
|
||||
pub use crate::core::message::{ChatMessage, Role};
|
||||
pub use crate::core::conversation::Conversation;
|
||||
pub use crate::core::message::{ChatMessage, Role};
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
//! 3. Oldest entries are evicted from the in-memory cache when
|
||||
//! `MAX_MEMORY_ENTRIES` is exceeded (prevents unbounded growth)
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A single recorded file edit event.
|
||||
@@ -47,18 +49,18 @@ pub const MAX_MEMORY_ENTRIES: usize = 10_000;
|
||||
|
||||
/// In-memory view of a session's edit log.
|
||||
///
|
||||
/// Wraps a `Vec<EditLogEntry>` and provides basic query helpers.
|
||||
/// Wraps a `VecDeque<EditLogEntry>` and provides basic query helpers.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EditLog {
|
||||
/// Ordered list of edit entries (newest appended last).
|
||||
pub entries: Vec<EditLogEntry>,
|
||||
pub entries: VecDeque<EditLogEntry>,
|
||||
}
|
||||
|
||||
impl EditLog {
|
||||
/// Create an empty edit log with no entries.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entries: Vec::new(),
|
||||
entries: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ pub mod settings;
|
||||
pub use app_config::AppConfig;
|
||||
pub use app_config::ModelRole;
|
||||
pub use app_config::ProviderConfig;
|
||||
pub use commands::{NewMemory, SettingsPatch};
|
||||
pub use conversation::Conversation;
|
||||
pub use edit_log::EditLog;
|
||||
pub use edit_log::EditLogEntry;
|
||||
@@ -46,7 +47,6 @@ pub use repository::SettingsRepository;
|
||||
pub use service::ConversationService;
|
||||
pub use service::MemoryService;
|
||||
pub use service::SettingsService;
|
||||
pub use commands::{NewMemory, SettingsPatch};
|
||||
pub use settings::InternetMode;
|
||||
pub use settings::Settings;
|
||||
pub use settings::SettingsFlags;
|
||||
|
||||
@@ -56,11 +56,7 @@ pub trait ConversationRepository {
|
||||
fn load(&self, session_dir: &Path) -> Result<Conversation, RepositoryError>;
|
||||
|
||||
/// Persist a `Conversation` to the given session directory.
|
||||
fn save(
|
||||
&self,
|
||||
session_dir: &Path,
|
||||
conversation: &Conversation,
|
||||
) -> Result<(), RepositoryError>;
|
||||
fn save(&self, session_dir: &Path, conversation: &Conversation) -> Result<(), RepositoryError>;
|
||||
}
|
||||
|
||||
/// Persistence contract for `Memory` (long-term agent memory entries).
|
||||
|
||||
@@ -44,11 +44,7 @@ pub trait ConversationService {
|
||||
fn save_conversation(&self, conv: &Conversation) -> Result<(), ServiceError>;
|
||||
|
||||
/// Append a single `ChatMessage` to the conversation and persist.
|
||||
fn add_message(
|
||||
&self,
|
||||
conv: &mut Conversation,
|
||||
msg: ChatMessage,
|
||||
) -> Result<(), ServiceError>;
|
||||
fn add_message(&self, conv: &mut Conversation, msg: ChatMessage) -> Result<(), ServiceError>;
|
||||
}
|
||||
|
||||
/// Use-cases for long-term memory management.
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
//! - `workflow_max_concurrency` — max parallel hive-mind nodes
|
||||
//! - `hive_mind_node_timeout_ms` — per-node timeout for hive-mind orchestration
|
||||
//! - `flags` — grouped boolean feature toggles
|
||||
//! - `lsp_languages` — list of language IDs for LSP auto-provisioning
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -49,12 +48,10 @@ pub enum InternetMode {
|
||||
/// ## Fields
|
||||
/// - `review_enabled` — enable automatic inline review after edits
|
||||
/// - `session_archive_enabled` — enable periodic session archiving
|
||||
/// - `lsp_auto_provision` — auto-provision LSP language servers on project open
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SettingsFlags {
|
||||
pub review_enabled: bool,
|
||||
pub session_archive_enabled: bool,
|
||||
pub lsp_auto_provision: bool,
|
||||
}
|
||||
|
||||
impl Default for SettingsFlags {
|
||||
@@ -63,7 +60,6 @@ impl Default for SettingsFlags {
|
||||
Self {
|
||||
review_enabled: true,
|
||||
session_archive_enabled: true,
|
||||
lsp_auto_provision: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,7 +89,6 @@ pub struct Settings {
|
||||
pub workflow_max_concurrency: usize,
|
||||
#[serde(flatten)]
|
||||
pub flags: SettingsFlags,
|
||||
pub lsp_languages: Vec<String>,
|
||||
#[serde(default = "default_hive_mind_node_timeout_ms")]
|
||||
pub hive_mind_node_timeout_ms: u64,
|
||||
}
|
||||
@@ -113,7 +108,6 @@ impl Default for Settings {
|
||||
verify_timeout_ms: 30_000,
|
||||
workflow_max_concurrency: 5,
|
||||
flags: SettingsFlags::default(),
|
||||
lsp_languages: Vec::new(),
|
||||
hive_mind_node_timeout_ms: 600_000,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,14 +218,27 @@ impl SseParser {
|
||||
///
|
||||
/// Return: all `StreamEvent`s completed by this chunk.
|
||||
pub fn feed(&mut self, chunk: &str) -> Vec<StreamEvent> {
|
||||
self.buffer.push_str(chunk);
|
||||
// Normalize \r\n and bare \r to \n for consistent line ending handling
|
||||
let chunk = chunk.replace("\r\n", "\n").replace('\r', "\n");
|
||||
|
||||
// Prevent unbounded buffer growth for long lines without \n
|
||||
const MAX_BUFFER_SIZE: usize = 1_048_576; // 1 MB
|
||||
if self.buffer.len() + chunk.len() > MAX_BUFFER_SIZE {
|
||||
tracing::warn!("SSE buffer exceeded maximum size, resetting");
|
||||
self.buffer.clear();
|
||||
self.event_type = None;
|
||||
self.data_lines.clear();
|
||||
}
|
||||
|
||||
self.buffer.push_str(&chunk);
|
||||
let mut events = Vec::new();
|
||||
while let Some(line_end) = self.buffer.find('\n') {
|
||||
let line = self.buffer[..line_end].trim_end_matches('\r').to_string();
|
||||
self.buffer = self.buffer[line_end + 1..].to_string();
|
||||
if line.is_empty() {
|
||||
events.extend(self.flush_event());
|
||||
} else if let Some(ty) = line.strip_prefix("event: ") {
|
||||
} else if let Some(ty) = line.strip_prefix("event:") {
|
||||
// Handle both "event:foo" and "event: foo"
|
||||
self.event_type = Some(ty.trim().to_string());
|
||||
} else if let Some(data) = line.strip_prefix("data:") {
|
||||
let data = data.trim_start().to_string();
|
||||
@@ -320,18 +333,22 @@ impl SseParser {
|
||||
if let Some(tool_calls) =
|
||||
d.get("tool_calls").and_then(|tc| tc.as_array())
|
||||
{
|
||||
const MAX_TOOL_CALLS: usize = 64;
|
||||
for tc in tool_calls {
|
||||
let index =
|
||||
tc.get("index").and_then(Value::as_u64).unwrap_or_else(
|
||||
|| {
|
||||
tracing::warn!(
|
||||
"[stream] tool call delta missing index, \
|
||||
let raw_index = tc
|
||||
.get("index")
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or_else(|| {
|
||||
tracing::warn!(
|
||||
"[stream] tool call delta missing index, \
|
||||
defaulting to 0"
|
||||
);
|
||||
0
|
||||
},
|
||||
);
|
||||
let index = usize::try_from(index).unwrap_or(0);
|
||||
);
|
||||
0
|
||||
});
|
||||
// Clamp index to prevent out-of-bounds / memory exhaustion
|
||||
let index = usize::try_from(raw_index)
|
||||
.unwrap_or(0)
|
||||
.min(MAX_TOOL_CALLS.saturating_sub(1));
|
||||
let id = tc
|
||||
.get("id")
|
||||
.and_then(|i| i.as_str())
|
||||
@@ -372,6 +389,19 @@ impl SseParser {
|
||||
}
|
||||
d_events
|
||||
}
|
||||
"content_block_delta" => {
|
||||
let mut d_events = Vec::new();
|
||||
if let Some(delta) = value.get("delta") {
|
||||
if let Some(content) = delta.get("text").and_then(|c| c.as_str()) {
|
||||
d_events.push(StreamEvent::Token(content.to_string()));
|
||||
}
|
||||
if let Some(reasoning) = delta.get("reasoning_content").and_then(|r| r.as_str())
|
||||
{
|
||||
d_events.push(StreamEvent::Reasoning(reasoning.to_string()));
|
||||
}
|
||||
}
|
||||
d_events
|
||||
}
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
|
||||
@@ -40,9 +40,11 @@ impl Store {
|
||||
///
|
||||
/// Why: paths are computed, not created — call `ensure_dirs` before use.
|
||||
pub fn new() -> Self {
|
||||
let base = if let Some(data_dir) = std::env::var("XDG_DATA_HOME").ok()
|
||||
.or_else(|| std::env::var("HOME").ok().map(|h| format!("{h}/.local/share")))
|
||||
{
|
||||
let base = if let Some(data_dir) = std::env::var("XDG_DATA_HOME").ok().or_else(|| {
|
||||
std::env::var("HOME")
|
||||
.ok()
|
||||
.map(|h| format!("{h}/.local/share"))
|
||||
}) {
|
||||
PathBuf::from(data_dir).join("zesdex")
|
||||
} else {
|
||||
PathBuf::from(".local/share/zesdex")
|
||||
|
||||
+23
-12
@@ -24,29 +24,40 @@
|
||||
//! no framework imports, no side effects. All persistence is expressed
|
||||
//! through repository traits that infrastructure adapters implement.
|
||||
|
||||
pub mod agent;
|
||||
pub mod auth;
|
||||
pub mod cms;
|
||||
pub mod core;
|
||||
pub mod error;
|
||||
pub mod subagent;
|
||||
pub mod workflow;
|
||||
|
||||
// Re-export all public items from each module for ergonomic imports.
|
||||
// Consumers can do `use zesdex_domain::*` for common types.
|
||||
pub use auth::{
|
||||
IamSession, NewSession, OAuthConfig, OAuthToken, OAuthRepository, OAuthService,
|
||||
RepositoryError as AuthRepositoryError, ServiceError as AuthServiceError, Session,
|
||||
SessionId, SessionLock, SessionLockRepository, SessionRepository, SessionService,
|
||||
IamSession, NewSession, OAuthConfig, OAuthRepository, OAuthService, OAuthToken,
|
||||
RepositoryError as AuthRepositoryError, ServiceError as AuthServiceError, Session, SessionId,
|
||||
SessionLock, SessionLockRepository, SessionRepository, SessionService,
|
||||
};
|
||||
pub use cms::{
|
||||
AppConfig, AppConfigRepository, Conversation as CmsConversation,
|
||||
ConversationRepository, ConversationService, EditLog, EditLogEntry,
|
||||
EditLogRepository, InternetMode, Memory, MemoryRepository, MemoryService,
|
||||
ModelRole, NewMemory, ProviderConfig, RepositoryError as CmsRepositoryError,
|
||||
ServiceError as CmsServiceError, Settings, SettingsFlags, SettingsPatch,
|
||||
SettingsRepository, SettingsService,
|
||||
AppConfig, AppConfigRepository, Conversation as CmsConversation, ConversationRepository,
|
||||
ConversationService, EditLog, EditLogEntry, EditLogRepository, InternetMode, Memory,
|
||||
MemoryRepository, MemoryService, ModelRole, NewMemory, ProviderConfig,
|
||||
RepositoryError as CmsRepositoryError, ServiceError as CmsServiceError, Settings,
|
||||
SettingsFlags, SettingsPatch, SettingsRepository, SettingsService,
|
||||
};
|
||||
pub use core::{
|
||||
ChatMessage, ChatRequest, ChatResponse, Choice, Conversation, Delta, Role,
|
||||
SseParser, StreamEvent, StreamOptions, Store, TokenUsage, ToolCall,
|
||||
ToolCallResult, ToolDef, ToolFunction, ToolFunctionDef, UsageStats,
|
||||
ChatMessage, ChatRequest, ChatResponse, Choice, Conversation, Delta, Role, SseParser, Store,
|
||||
StreamEvent, StreamOptions, TokenUsage, ToolCall, ToolCallResult, ToolDef, ToolFunction,
|
||||
ToolFunctionDef, UsageStats,
|
||||
};
|
||||
pub use error::DomainError;
|
||||
|
||||
// Agent module top-level items (TurnEvent, SessionRuntime, etc.)
|
||||
pub use agent::*;
|
||||
// Sub-module items need explicit re-exports
|
||||
pub use agent::defaults::*;
|
||||
pub use agent::progress::AgentProgress;
|
||||
pub use agent::prompt::{compaction_prompt, main_agent_prompt, subagent_directive};
|
||||
pub use subagent::*;
|
||||
pub use workflow::*;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
//! Subagent domain models.
|
||||
|
||||
/// Access tier for subagent tool permissions.
|
||||
///
|
||||
/// Tiers are cumulative: `Write` includes everything in `Read`, and `Full`
|
||||
/// includes everything in `Write`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AccessTier {
|
||||
/// Read-only: search, read, glob, utility tools (no mutations).
|
||||
Read,
|
||||
/// Read + Write: above plus write, edit, delete, git, memory.
|
||||
Write,
|
||||
/// Full: above plus bash, shell, workflow, plan tools.
|
||||
Full,
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
//! Workflow and Hive-mind domain models.
|
||||
|
||||
/// A single phase in a parsed workflow script.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WorkflowPhase {
|
||||
pub name: String,
|
||||
pub directive: String,
|
||||
}
|
||||
|
||||
/// A parsed workflow script with named phases.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WorkflowScript {
|
||||
pub name: String,
|
||||
pub phases: Vec<WorkflowPhase>,
|
||||
}
|
||||
|
||||
/// A directive for a single processing node in the hive mind.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NodeDirective {
|
||||
pub directive: String,
|
||||
pub access_tier: String,
|
||||
}
|
||||
|
||||
/// A cognitive cycle plan — ordered list of cycles, each containing
|
||||
/// parallel node directives.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CognitiveCyclePlan {
|
||||
pub cycles: Vec<Vec<NodeDirective>>,
|
||||
}
|
||||
|
||||
/// A single cycle in a cognitive cycle plan — parallel node directives
|
||||
/// executed together.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CognitiveCycle {
|
||||
pub index: u32,
|
||||
pub directives: Vec<NodeDirective>,
|
||||
}
|
||||
|
||||
/// Output from a single hive-mind processing node after a cycle completes.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NodeOutput {
|
||||
pub id: String,
|
||||
pub directive: String,
|
||||
pub output: String,
|
||||
}
|
||||
@@ -42,3 +42,8 @@ dirs.workspace = true
|
||||
rusqlite.workspace = true
|
||||
axum.workspace = true
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
[[bin]]
|
||||
name = "test_load"
|
||||
path = "src/bin/test_load.rs"
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
//! schema for each one. Standalone CLI tool invoked as `cargo run --bin migrate`.
|
||||
|
||||
use std::path::Path;
|
||||
use tracing;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let store = zesdex_domain::core::Store::new();
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
//! configuration files plus a seed session for development/testing.
|
||||
//! Invoked as `cargo run --bin seed`.
|
||||
|
||||
use tracing;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let store = zesdex_domain::core::Store::new();
|
||||
store.ensure_dirs()?;
|
||||
@@ -47,13 +45,11 @@ fn main() -> anyhow::Result<()> {
|
||||
|
||||
// Create a seed session
|
||||
let session_id = uuid::Uuid::new_v4().to_string();
|
||||
let session = zesdex_domain::auth::Session::new(
|
||||
session_id.clone(),
|
||||
"Seed Session".to_string(),
|
||||
);
|
||||
let session = zesdex_domain::auth::Session::new(session_id.clone(), "Seed Session".to_string());
|
||||
// Persist via the session repository
|
||||
use zesdex_domain::SessionRepository;
|
||||
let repo = zesdex_infrastructure::persistence::iam::session_repo::FileSystemSessionRepository::new();
|
||||
let repo =
|
||||
zesdex_infrastructure::persistence::iam::session_repo::FileSystemSessionRepository::new();
|
||||
repo.save_session(&store.base_dir, &session)?;
|
||||
tracing::info!("Seed session created: id={session_id}");
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
use zesdex_domain::cms::AppConfigRepository;
|
||||
use zesdex_infrastructure::persistence::cms::app_config_repo::JsonAppConfigRepository;
|
||||
|
||||
fn main() {
|
||||
let base_dir = dirs::home_dir().unwrap().join(".local/share/zesdex");
|
||||
let repo = JsonAppConfigRepository::new();
|
||||
let config = repo.load(&base_dir).unwrap();
|
||||
|
||||
println!("Providers:");
|
||||
for (k, v) in &config.providers {
|
||||
println!(" - {} (default model: {:?})", k, v.default_model);
|
||||
}
|
||||
|
||||
println!("Default provider: {}", config.default_provider);
|
||||
println!("Default model: {}", config.default_model);
|
||||
println!("Model roles:");
|
||||
for (k, v) in &config.model_roles {
|
||||
println!(" - {}: provider={}, model={}", k, v.provider, v.model);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ClaudeEnv {
|
||||
#[serde(alias = "ANTHROPIC_BASE_URL")]
|
||||
anthropic_base_url: Option<String>,
|
||||
#[serde(alias = "ANTHROPIC_API_KEY")]
|
||||
anthropic_api_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ClaudeSettings {
|
||||
env: Option<ClaudeEnv>,
|
||||
#[serde(alias = "customModel", alias = "model")]
|
||||
custom_model: Option<String>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let path = dirs::home_dir()
|
||||
.unwrap()
|
||||
.join(".claude")
|
||||
.join("settings.json");
|
||||
println!("Path: {:?}", path);
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(content) => {
|
||||
println!("File content length: {}", content.len());
|
||||
match serde_json::from_str::<ClaudeSettings>(&content) {
|
||||
Ok(settings) => {
|
||||
println!("Parsed successfully: {:?}", settings);
|
||||
if let Some(env) = settings.env {
|
||||
println!("Base URL: {:?}", env.anthropic_base_url);
|
||||
println!("API Key: {:?}", env.anthropic_api_key);
|
||||
} else {
|
||||
println!("No env block");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Parse error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Read error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,11 @@ use clap::Parser;
|
||||
|
||||
/// Zesdex — autonomous AI coding agent.
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "zesdex", version, about = "Autonomous AI coding agent with TUI")]
|
||||
#[command(
|
||||
name = "zesdex",
|
||||
version,
|
||||
about = "Autonomous AI coding agent with TUI"
|
||||
)]
|
||||
struct Cli {
|
||||
/// Run as background daemon with IPC socket
|
||||
#[arg(long)]
|
||||
@@ -136,12 +140,23 @@ fn run_api_server(port: u16) -> anyhow::Result<()> {
|
||||
let rt = tokio::runtime::Runtime::new()?;
|
||||
rt.block_on(async {
|
||||
let store = zesdex_domain::core::Store::new();
|
||||
store.ensure_dirs()?;
|
||||
|
||||
// Load JWT secret from environment variable with a secure default warning
|
||||
let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| {
|
||||
tracing::warn!(
|
||||
"JWT_SECRET environment variable not set; using insecure default. \
|
||||
Set JWT_SECRET to a secure random value in production."
|
||||
);
|
||||
zesdex_domain::agent::defaults::FALLBACK_JWT_SECRET.to_string()
|
||||
});
|
||||
|
||||
let state = zesdex_api::ApiState::new(
|
||||
store.base_dir.clone(),
|
||||
"dev-secret",
|
||||
jwt_secret,
|
||||
"",
|
||||
"deepseek-v4-flash-free",
|
||||
Some("https://opencode.ai/zen/v1".to_string()),
|
||||
zesdex_domain::agent::defaults::DEFAULT_MODEL,
|
||||
Some(zesdex_domain::agent::defaults::DEFAULT_API_BASE.to_string()),
|
||||
);
|
||||
let app = zesdex_api::build_router(state);
|
||||
let addr = std::net::SocketAddr::from(([0, 0, 0, 0], port));
|
||||
|
||||
@@ -32,7 +32,6 @@ ignore.workspace = true
|
||||
nucleo-matcher.workspace = true
|
||||
futures-util.workspace = true
|
||||
rmcp.workspace = true
|
||||
lsp-types.workspace = true
|
||||
tiktoken-rs.workspace = true
|
||||
similar.workspace = true
|
||||
syntect.workspace = true
|
||||
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user