Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b66898ea28 | ||
|
|
9aca45cb65 | ||
|
|
4dccf0cee4 | ||
|
|
1f91b44708 | ||
|
|
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 | ||
|
|
4ced6681c2 | ||
|
|
08e2f9998d | ||
|
|
792695b65a | ||
|
|
da2ed6da25 | ||
|
|
bceba665c0 | ||
|
|
714b4617dd | ||
|
|
e9a8e93c83 | ||
|
|
ab1a54b72e | ||
|
|
5aaedbf787 | ||
|
|
6680795ce7 | ||
|
|
9b6e51dc67 | ||
|
|
40524f930f | ||
|
|
086cb86f0d | ||
|
|
ff47bffe0c | ||
|
|
431c8d3b89 | ||
|
|
e32501ee59 | ||
|
|
7d8487cefb | ||
|
|
4cd38c9291 | ||
|
|
9a6ab62562 | ||
|
|
b02754acd2 | ||
|
|
b3c5b2a57b | ||
|
|
796bb09c5b | ||
|
|
5aad7e1eb1 |
@@ -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:
|
with:
|
||||||
components: clippy
|
components: clippy
|
||||||
|
|
||||||
- name: Build workspace
|
- name: Format check
|
||||||
run: cargo build --release --workspace
|
run: cargo fmt --all -- --check
|
||||||
|
|
||||||
|
- name: Clippy workspace
|
||||||
|
run: cargo clippy --workspace --all-targets -- -D warnings
|
||||||
|
|
||||||
- name: Test workspace
|
- name: Test workspace
|
||||||
run: cargo 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
|
package-lock.json
|
||||||
.superpowers/
|
.superpowers/
|
||||||
docs/lesson/
|
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.
|
||||||
+136
@@ -1,3 +1,139 @@
|
|||||||
|
## [1.19.4](https://github.com/asepharyana/zesdex/compare/v1.19.3...v1.19.4) (2026-08-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **api:** model claude selalu pakai Opus dari settings.json, bukan deepseek ([9aca45c](https://github.com/asepharyana/zesdex/commit/9aca45cb65d6913d14fecc10d70c180934f69d74))
|
||||||
|
|
||||||
|
## [1.19.3](https://github.com/asepharyana/zesdex/compare/v1.19.2...v1.19.3) (2026-08-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **api:** model Opus pakai URL + API custom dari ~/.claude/settings.json ([1f91b44](https://github.com/asepharyana/zesdex/commit/1f91b447080e7106201d77585edb7d38b74e34bb))
|
||||||
|
|
||||||
|
## [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)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **tui:** add usage overlay and sidebar for displaying usage statistics and tasks ([da2ed6d](https://github.com/asepharyana/zesdex/commit/da2ed6da25953b823354cc5deaa7b404b7b13cb0))
|
||||||
|
* **tui:** enhance agent turn with tool descriptions and sanitize arguments ([08e2f99](https://github.com/asepharyana/zesdex/commit/08e2f9998d3693e4cd57225491359c762d03f1d9))
|
||||||
|
* **tui:** implement agent turn engine for background processing and enhance input handling ([792695b](https://github.com/asepharyana/zesdex/commit/792695b65a393cfc54efe353480b7831e91544b5))
|
||||||
|
|
||||||
|
## [1.15.2](https://github.com/asepharyana/zesdex/compare/v1.15.1...v1.15.2) (2026-07-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* correct jitter range to ±25% and fix abort.rs doc — review findings ([9b6e51d](https://github.com/asepharyana/zesdex/commit/9b6e51dc677cc4b798532f1e63335785f2f7bbbf))
|
||||||
|
|
||||||
|
## [1.15.1](https://github.com/asepharyana/zesdex/compare/v1.15.0...v1.15.1) (2026-07-17)
|
||||||
|
|
||||||
# [1.15.0](https://github.com/asepharyana/zesdex/compare/v1.14.0...v1.15.0) (2026-07-17)
|
# [1.15.0](https://github.com/asepharyana/zesdex/compare/v1.14.0...v1.15.0) (2026-07-17)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
+405
-133
@@ -32,6 +32,56 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstream"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "3.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"once_cell_polyfill",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.103"
|
version = "1.0.103"
|
||||||
@@ -134,6 +184,7 @@ checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"axum-core",
|
"axum-core",
|
||||||
"axum-macros",
|
"axum-macros",
|
||||||
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@@ -152,8 +203,10 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_path_to_error",
|
"serde_path_to_error",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
|
"sha1",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-tungstenite",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
@@ -401,6 +454,46 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
"clap_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.118",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cmake"
|
name = "cmake"
|
||||||
version = "0.1.58"
|
version = "0.1.58"
|
||||||
@@ -410,6 +503,12 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "4.6.7"
|
version = "4.6.7"
|
||||||
@@ -669,6 +768,12 @@ dependencies = [
|
|||||||
"syn 2.0.118",
|
"syn 2.0.118",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-encoding"
|
||||||
|
version = "2.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deltae"
|
name = "deltae"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -983,15 +1088,6 @@ dependencies = [
|
|||||||
"miniz_oxide",
|
"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]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@@ -1624,6 +1720,12 @@ version = "2.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
@@ -1861,19 +1963,6 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
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]]
|
[[package]]
|
||||||
name = "mac_address"
|
name = "mac_address"
|
||||||
version = "1.1.8"
|
version = "1.1.8"
|
||||||
@@ -1937,6 +2026,16 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime_guess"
|
||||||
|
version = "2.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||||
|
dependencies = [
|
||||||
|
"mime",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "minimal-lexical"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -2142,6 +2241,12 @@ version = "1.21.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.81"
|
version = "0.10.81"
|
||||||
@@ -2483,6 +2588,15 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "precomputed-hash"
|
name = "precomputed-hash"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -2619,6 +2733,16 @@ dependencies = [
|
|||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
@@ -2630,6 +2754,16 @@ dependencies = [
|
|||||||
"rand_core 0.10.1",
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
@@ -2639,6 +2773,15 @@ dependencies = [
|
|||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@@ -3204,17 +3347,6 @@ dependencies = [
|
|||||||
"serde_core",
|
"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]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@@ -3249,6 +3381,17 @@ dependencies = [
|
|||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures 0.2.17",
|
||||||
|
"digest 0.10.7",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1_smol"
|
name = "sha1_smol"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@@ -3849,6 +3992,18 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-tungstenite"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"tokio",
|
||||||
|
"tungstenite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.18"
|
version = "0.7.18"
|
||||||
@@ -3977,6 +4132,22 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tungstenite"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"data-encoding",
|
||||||
|
"http",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
"rand 0.9.5",
|
||||||
|
"sha1",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.20.1"
|
version = "1.20.1"
|
||||||
@@ -4609,6 +4780,26 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.54"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.54"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.118",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom"
|
name = "zerofrom"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
@@ -4670,14 +4861,157 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zesdex-backend"
|
name = "zesdex-api"
|
||||||
version = "1.15.0"
|
version = "1.19.2"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"argon2",
|
||||||
|
"axum",
|
||||||
|
"chrono",
|
||||||
|
"futures-util",
|
||||||
|
"jsonwebtoken",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"tower-http",
|
||||||
|
"tracing",
|
||||||
|
"uuid",
|
||||||
|
"zesdex-application",
|
||||||
|
"zesdex-domain",
|
||||||
|
"zesdex-infrastructure",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zesdex-application"
|
||||||
|
version = "1.19.2"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"base64",
|
||||||
|
"chrono",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2 0.11.0",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"url",
|
||||||
|
"uuid",
|
||||||
|
"zesdex-domain",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zesdex-bootstrap"
|
||||||
|
version = "1.19.2"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"chrono",
|
||||||
|
"dirs",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"uuid",
|
||||||
|
"zesdex-application",
|
||||||
|
"zesdex-domain",
|
||||||
|
"zesdex-infrastructure",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zesdex-daemon"
|
||||||
|
version = "1.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"dirs",
|
"dirs",
|
||||||
|
"hex",
|
||||||
|
"ignore",
|
||||||
|
"ratatui",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2 0.11.0",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"uuid",
|
||||||
|
"webbrowser",
|
||||||
|
"zesdex-application",
|
||||||
|
"zesdex-domain",
|
||||||
|
"zesdex-infrastructure",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zesdex-domain"
|
||||||
|
version = "1.19.2"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"base64",
|
||||||
|
"chrono",
|
||||||
|
"libc",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2 0.11.0",
|
||||||
|
"tracing",
|
||||||
|
"url",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zesdex-gateway"
|
||||||
|
version = "1.19.2"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"axum",
|
||||||
|
"chrono",
|
||||||
|
"clap",
|
||||||
|
"dirs",
|
||||||
|
"rusqlite",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
"uuid",
|
||||||
|
"zesdex-api",
|
||||||
|
"zesdex-application",
|
||||||
|
"zesdex-daemon",
|
||||||
|
"zesdex-domain",
|
||||||
|
"zesdex-grpc",
|
||||||
|
"zesdex-infrastructure",
|
||||||
|
"zesdex-tui",
|
||||||
|
"zesdex-web",
|
||||||
|
"zesdex-ws",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zesdex-grpc"
|
||||||
|
version = "1.19.2"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"axum",
|
||||||
|
"chrono",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"uuid",
|
||||||
|
"zesdex-application",
|
||||||
|
"zesdex-domain",
|
||||||
|
"zesdex-infrastructure",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zesdex-infrastructure"
|
||||||
|
version = "1.19.2"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"argon2",
|
||||||
|
"axum",
|
||||||
|
"base64",
|
||||||
|
"chrono",
|
||||||
|
"clap",
|
||||||
|
"dirs",
|
||||||
"dom_smoothie",
|
"dom_smoothie",
|
||||||
"fast_html2md",
|
"fast_html2md",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@@ -4686,12 +5020,12 @@ dependencies = [
|
|||||||
"ignore",
|
"ignore",
|
||||||
"include_dir",
|
"include_dir",
|
||||||
"infer",
|
"infer",
|
||||||
|
"jsonwebtoken",
|
||||||
"libc",
|
"libc",
|
||||||
"lsp-types",
|
|
||||||
"nucleo-matcher",
|
"nucleo-matcher",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
"ratatui",
|
"rand_core 0.6.4",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rmcp",
|
"rmcp",
|
||||||
@@ -4705,140 +5039,78 @@ dependencies = [
|
|||||||
"syntect",
|
"syntect",
|
||||||
"tiktoken-rs",
|
"tiktoken-rs",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"webbrowser",
|
"webbrowser",
|
||||||
"zesdex-cms",
|
"zesdex-application",
|
||||||
"zesdex-entities",
|
"zesdex-domain",
|
||||||
"zesdex-iam",
|
|
||||||
"zesdex-infra",
|
|
||||||
"zesdex-ipc",
|
|
||||||
"zesdex-middleware",
|
|
||||||
"zesdex-utils",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zesdex-cms"
|
name = "zesdex-tui"
|
||||||
version = "1.15.0"
|
version = "1.19.2"
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"chrono",
|
|
||||||
"dirs",
|
|
||||||
"hex",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tracing",
|
|
||||||
"uuid",
|
|
||||||
"zesdex-entities",
|
|
||||||
"zesdex-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zesdex-entities"
|
|
||||||
version = "1.15.0"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"crossterm",
|
||||||
"dirs",
|
"dirs",
|
||||||
"libc",
|
|
||||||
"reqwest",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"sha2 0.11.0",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
"url",
|
|
||||||
"uuid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zesdex-iam"
|
|
||||||
version = "1.15.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"base64",
|
|
||||||
"chrono",
|
|
||||||
"hex",
|
"hex",
|
||||||
"libc",
|
"nucleo-matcher",
|
||||||
"rand_core 0.6.4",
|
"pulldown-cmark",
|
||||||
"reqwest",
|
"ratatui",
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"sha2 0.11.0",
|
|
||||||
"tracing",
|
|
||||||
"url",
|
|
||||||
"uuid",
|
|
||||||
"zesdex-entities",
|
|
||||||
"zesdex-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zesdex-infra"
|
|
||||||
version = "1.15.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"argon2",
|
|
||||||
"axum",
|
|
||||||
"chrono",
|
|
||||||
"jsonwebtoken",
|
|
||||||
"rand_core 0.6.4",
|
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2 0.11.0",
|
||||||
|
"tiktoken-rs",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
"zesdex-cms",
|
"zesdex-application",
|
||||||
"zesdex-entities",
|
"zesdex-domain",
|
||||||
"zesdex-iam",
|
"zesdex-infrastructure",
|
||||||
"zesdex-middleware",
|
|
||||||
"zesdex-utils",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zesdex-ipc"
|
name = "zesdex-web"
|
||||||
version = "1.15.0"
|
version = "1.19.2"
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tracing",
|
|
||||||
"zesdex-entities",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zesdex-middleware"
|
|
||||||
version = "1.15.0"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"include_dir",
|
||||||
|
"mime_guess",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tracing",
|
||||||
"zesdex-entities",
|
"uuid",
|
||||||
"zesdex-utils",
|
"zesdex-application",
|
||||||
|
"zesdex-domain",
|
||||||
|
"zesdex-infrastructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zesdex-utils"
|
name = "zesdex-ws"
|
||||||
version = "1.15.0"
|
version = "1.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dirs",
|
"futures-util",
|
||||||
"hex",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2 0.11.0",
|
"tokio",
|
||||||
"thiserror 1.0.69",
|
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"uuid",
|
||||||
|
"zesdex-application",
|
||||||
|
"zesdex-domain",
|
||||||
|
"zesdex-infrastructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
+26
-12
@@ -1,18 +1,21 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"crates/zesdex-entities",
|
"apps/domain",
|
||||||
"crates/zesdex-utils",
|
"apps/application",
|
||||||
"crates/zesdex-ipc",
|
"apps/infrastructure",
|
||||||
"crates/zesdex-iam",
|
"apps/interfaces/tui",
|
||||||
"crates/zesdex-cms",
|
"apps/interfaces/api",
|
||||||
"crates/zesdex-middleware",
|
"apps/interfaces/daemon",
|
||||||
"crates/zesdex-infra",
|
"apps/interfaces/ws",
|
||||||
"crates/zesdex-backend",
|
"apps/interfaces/grpc",
|
||||||
|
"apps/interfaces/web",
|
||||||
|
"apps/gateway",
|
||||||
|
"apps/bootstrap",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "1.15.0"
|
version = "1.19.4"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["asepharyana <superaseph@gmail.com>"]
|
authors = ["asepharyana <superaseph@gmail.com>"]
|
||||||
|
|
||||||
@@ -58,7 +61,6 @@ ignore = "0.4"
|
|||||||
nucleo-matcher = "0.3"
|
nucleo-matcher = "0.3"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
rmcp = { version = "2.2", default-features = false, features = ["client", "transport-child-process", "transport-streamable-http-client-reqwest", "macros"] }
|
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"
|
tiktoken-rs = "0.12"
|
||||||
similar = "3"
|
similar = "3"
|
||||||
syntect = { version = "5", default-features = false, features = ["default-fancy"] }
|
syntect = { version = "5", default-features = false, features = ["default-fancy"] }
|
||||||
@@ -76,6 +78,18 @@ tower = "0.5"
|
|||||||
tower-http = { version = "0.6", features = ["cors", "limit"] }
|
tower-http = { version = "0.6", features = ["cors", "limit"] }
|
||||||
argon2 = "0.5"
|
argon2 = "0.5"
|
||||||
jsonwebtoken = "9"
|
jsonwebtoken = "9"
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
rand_core = { version = "0.6", features = ["getrandom"] }
|
||||||
|
|
||||||
zesdex-entities = { path = "crates/zesdex-entities" }
|
# Clean-architecture workspace crate references
|
||||||
zesdex-utils = { path = "crates/zesdex-utils" }
|
zesdex-domain = { path = "apps/domain" }
|
||||||
|
zesdex-application = { path = "apps/application" }
|
||||||
|
zesdex-infrastructure = { path = "apps/infrastructure" }
|
||||||
|
zesdex-tui = { path = "apps/interfaces/tui" }
|
||||||
|
zesdex-api = { path = "apps/interfaces/api" }
|
||||||
|
zesdex-daemon = { path = "apps/interfaces/daemon" }
|
||||||
|
zesdex-ws = { path = "apps/interfaces/ws" }
|
||||||
|
zesdex-grpc = { path = "apps/interfaces/grpc" }
|
||||||
|
zesdex-web = { path = "apps/interfaces/web" }
|
||||||
|
zesdex-gateway = { path = "apps/gateway" }
|
||||||
|
zesdex-bootstrap = { path = "apps/bootstrap" }
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ WORKDIR /app
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Build with release profile (treats warnings as errors via lints)
|
# 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
|
# Stage 2: Minimal runtime image
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "zesdex-iam"
|
name = "zesdex-application"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
||||||
|
# Application layer — port traits (interfaces), use cases, DTOs.
|
||||||
|
# Depends ONLY on domain. Application services orchestrate domain objects
|
||||||
|
# through port traits without knowing concrete implementations.
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
zesdex-domain = { path = "../domain" }
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
anyhow.workspace = true
|
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
zesdex-entities = { path = "../zesdex-entities" }
|
anyhow.workspace = true
|
||||||
zesdex-utils = { path = "../zesdex-utils" }
|
|
||||||
reqwest.workspace = true
|
|
||||||
libc.workspace = true
|
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
url.workspace = true
|
tokio.workspace = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
hex.workspace = true
|
url.workspace = true
|
||||||
rand_core = { version = "0.6", features = ["getrandom"] }
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
//! Auth use-case implementations.
|
||||||
|
//!
|
||||||
|
//! Contains concrete service types that implement the domain's
|
||||||
|
//! authentication and session management traits by coordinating
|
||||||
|
//! injected repository and port dependencies.
|
||||||
|
//!
|
||||||
|
//! # Use Cases
|
||||||
|
//!
|
||||||
|
//! - [`oauth_service`] — `OAuthUseCase`: OAuth 2.0 authorization-code + PKCE flow
|
||||||
|
//! - [`session_service`] — `SessionServiceImpl`: session CRUD lifecycle
|
||||||
|
|
||||||
|
pub mod oauth_service;
|
||||||
|
pub mod session_service;
|
||||||
|
|
||||||
|
pub use oauth_service::{OAuthFlowStore, OAuthUseCase, TokenExchanger};
|
||||||
|
pub use session_service::SessionServiceImpl;
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
//! OAuth 2.0 authorization-code + PKCE flow use-case.
|
||||||
|
//!
|
||||||
|
//! `OAuthUseCase` orchestrates the standard PKCE-enhanced OAuth flow:
|
||||||
|
//!
|
||||||
|
//! 1. **`start_flow`** — generates a cryptographic PKCE code verifier,
|
||||||
|
//! derives its S256 challenge, creates a CSRF state token, persists
|
||||||
|
//! the verifier + state via `OAuthFlowStore`, and builds an
|
||||||
|
//! authorization URL with all required parameters.
|
||||||
|
//! 2. **`complete_flow`** — validates the returned `state` against the
|
||||||
|
//! stored value (CSRF check), reads the stored verifier, delegates
|
||||||
|
//! the token-code exchange to an injected `TokenExchanger`, and
|
||||||
|
//! persists the resulting `OAuthToken` via `OAuthRepository`.
|
||||||
|
//! 3. **`get_token`** — loads the stored OAuth token (if any).
|
||||||
|
//!
|
||||||
|
//! # Portability
|
||||||
|
//!
|
||||||
|
//! The service is generic over three injected dependencies:
|
||||||
|
//! - `R: OAuthRepository` — token persistence
|
||||||
|
//! - `S: OAuthFlowStore` — ephemeral flow state (verifier + CSRF state)
|
||||||
|
//! - `E: TokenExchanger` — the HTTP token-endpoint exchange
|
||||||
|
//!
|
||||||
|
//! This keeps all I/O and protocol-level concerns abstracted behind
|
||||||
|
//! port traits; the service itself contains only orchestration logic.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tracing;
|
||||||
|
|
||||||
|
use zesdex_domain::auth::{OAuthConfig, OAuthRepository, OAuthToken, ServiceError};
|
||||||
|
|
||||||
|
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||||
|
use base64::Engine as _;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Port traits (defined here because they are specific to this use-case)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Persistence contract for ephemeral OAuth flow state.
|
||||||
|
///
|
||||||
|
/// Between `start_flow` and `complete_flow` the verifier and CSRF state
|
||||||
|
/// must survive across process boundaries (the user opens a browser, the
|
||||||
|
/// provider redirects back to a loopback listener on the next invocation).
|
||||||
|
///
|
||||||
|
/// Implementors store key-value pairs to disk or another durable medium
|
||||||
|
/// 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>;
|
||||||
|
|
||||||
|
/// Load the stored PKCE code verifier.
|
||||||
|
fn load_verifier(&self) -> Result<String, ServiceError>;
|
||||||
|
|
||||||
|
/// Load the stored CSRF state token.
|
||||||
|
fn load_state(&self) -> Result<String, ServiceError>;
|
||||||
|
|
||||||
|
/// Clear stored flow state (verifier + state).
|
||||||
|
fn clear(&self) -> Result<(), ServiceError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Abstraction for exchanging an authorization code for tokens.
|
||||||
|
///
|
||||||
|
/// Implementors handle the HTTP POST to the provider's token endpoint
|
||||||
|
/// with the appropriate form-encoded parameters, parse the JSON
|
||||||
|
/// response, and return the extracted `OAuthToken`.
|
||||||
|
pub trait TokenExchanger: Send + Sync {
|
||||||
|
/// Exchange an authorization code for an access token.
|
||||||
|
///
|
||||||
|
/// ## Parameters
|
||||||
|
/// - `token_url` — the provider's token endpoint URL
|
||||||
|
/// - `client_id` — OAuth client identifier
|
||||||
|
/// - `client_secret` — optional client secret
|
||||||
|
/// - `redirect_uri` — must match the URI used in `start_flow`
|
||||||
|
/// - `code` — the authorization code from the provider's redirect
|
||||||
|
/// - `code_verifier` — the PKCE verifier from `start_flow`
|
||||||
|
fn exchange_code(
|
||||||
|
&self,
|
||||||
|
token_url: &str,
|
||||||
|
client_id: &str,
|
||||||
|
client_secret: Option<&str>,
|
||||||
|
redirect_uri: &str,
|
||||||
|
code: &str,
|
||||||
|
code_verifier: &str,
|
||||||
|
) -> Result<OAuthToken, ServiceError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// PKCE helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Generate a PKCE code-verifier and its S256 code-challenge.
|
||||||
|
///
|
||||||
|
/// Uses 32 cryptographically random bytes, base64url-encoded (no padding)
|
||||||
|
/// for the verifier, then SHA-256 hashes the verifier and base64url-encodes
|
||||||
|
/// the digest for the challenge. This satisfies the PKCE `S256` method
|
||||||
|
/// which requires a minimum verifier length of 43 characters.
|
||||||
|
fn generate_pkce_pair() -> (String, String) {
|
||||||
|
// 32 random bytes → 43 base64url chars (well above the 43-char PKCE
|
||||||
|
// minimum).
|
||||||
|
let mut bytes = [0u8; 32];
|
||||||
|
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 challenge = {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(verifier.as_bytes());
|
||||||
|
URL_SAFE_NO_PAD.encode(hasher.finalize())
|
||||||
|
};
|
||||||
|
(verifier, challenge)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a random CSRF state token (UUID-based, 36 chars).
|
||||||
|
fn generate_state_token() -> String {
|
||||||
|
uuid::Uuid::new_v4().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Service
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Concrete OAuth flow use-case.
|
||||||
|
///
|
||||||
|
/// Generic over three dependencies:
|
||||||
|
/// - `R` — token persistence (`OAuthRepository`)
|
||||||
|
/// - `S` — flow-state persistence (`OAuthFlowStore`)
|
||||||
|
/// - `E` — token-endpoint HTTP exchange (`TokenExchanger`)
|
||||||
|
pub struct OAuthUseCase<R, S, E> {
|
||||||
|
/// Repository for persisting / loading OAuth tokens.
|
||||||
|
pub token_repo: R,
|
||||||
|
/// Store for ephemeral flow state (verifier + CSRF state).
|
||||||
|
pub flow_store: S,
|
||||||
|
/// Token-endpoint HTTP exchanger.
|
||||||
|
pub token_exchanger: E,
|
||||||
|
/// File path for the token JSON file.
|
||||||
|
pub token_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
OAuthUseCase {
|
||||||
|
token_repo,
|
||||||
|
flow_store,
|
||||||
|
token_exchanger,
|
||||||
|
token_path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger> zesdex_domain::auth::OAuthService
|
||||||
|
for OAuthUseCase<R, S, E>
|
||||||
|
{
|
||||||
|
fn start_flow(
|
||||||
|
&self,
|
||||||
|
config: &OAuthConfig,
|
||||||
|
redirect_uri: &str,
|
||||||
|
) -> Result<(String, String), ServiceError> {
|
||||||
|
if config.auth_url.is_empty() {
|
||||||
|
return Err(ServiceError::InvalidConfig(
|
||||||
|
"OAuth auth_url is empty".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (verifier, challenge) = generate_pkce_pair();
|
||||||
|
let state = generate_state_token();
|
||||||
|
|
||||||
|
// Persist verifier + state so `complete_flow` can retrieve them.
|
||||||
|
self.flow_store.save_flow_state(&verifier, &state)?;
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
auth_url = %config.auth_url,
|
||||||
|
redirect_uri = %redirect_uri,
|
||||||
|
state_len = state.len(),
|
||||||
|
"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))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
url.query_pairs_mut()
|
||||||
|
.append_pair("response_type", "code")
|
||||||
|
.append_pair("client_id", &config.client_id)
|
||||||
|
.append_pair("redirect_uri", redirect_uri)
|
||||||
|
.append_pair("scope", &config.scopes.join(" "))
|
||||||
|
.append_pair("state", &state)
|
||||||
|
.append_pair("code_challenge_method", "S256")
|
||||||
|
.append_pair("code_challenge", &challenge);
|
||||||
|
|
||||||
|
Ok((url.to_string(), state))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn complete_flow(
|
||||||
|
&self,
|
||||||
|
config: &OAuthConfig,
|
||||||
|
redirect_uri: &str,
|
||||||
|
code: &str,
|
||||||
|
state: &str,
|
||||||
|
) -> Result<OAuthToken, ServiceError> {
|
||||||
|
// CSRF check: validate the returned state against the stored value.
|
||||||
|
let expected_state = self.flow_store.load_state()?;
|
||||||
|
if expected_state != state {
|
||||||
|
return Err(ServiceError::StateMismatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the PKCE verifier that was saved in `start_flow`.
|
||||||
|
let verifier = self.flow_store.load_verifier()?;
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
token_url = %config.token_url,
|
||||||
|
code_len = code.len(),
|
||||||
|
"completing OAuth flow — exchanging code for token",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delegate the HTTP token exchange to the injected exchanger.
|
||||||
|
let token = self.token_exchanger.exchange_code(
|
||||||
|
&config.token_url,
|
||||||
|
&config.client_id,
|
||||||
|
config.client_secret.as_deref(),
|
||||||
|
redirect_uri,
|
||||||
|
code,
|
||||||
|
&verifier,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Persist the token and clean up flow state.
|
||||||
|
self.token_repo.save_token(&self.token_path, &token)?;
|
||||||
|
let _ = self.flow_store.clear();
|
||||||
|
|
||||||
|
Ok(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_token(&self) -> Result<Option<OAuthToken>, ServiceError> {
|
||||||
|
self.token_repo
|
||||||
|
.load_token(&self.token_path)
|
||||||
|
.map_err(ServiceError::Repository)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
//! Session management use-case.
|
||||||
|
//!
|
||||||
|
//! `SessionServiceImpl` implements [`SessionService`] from the domain
|
||||||
|
//! layer by delegating CRUD operations to injected repository traits.
|
||||||
|
//!
|
||||||
|
//! # Flow
|
||||||
|
//!
|
||||||
|
//! - **`create_session`** — generates a UUID v4 id, creates a `Session`
|
||||||
|
//! entity with the given title, persists via `SessionRepository`.
|
||||||
|
//! - **`list_all`** — delegates to `SessionRepository::list_sessions`.
|
||||||
|
//! - **`archive_session`** — loads session, sets `archived = true`,
|
||||||
|
//! persists the updated entity.
|
||||||
|
//!
|
||||||
|
//! # Generics
|
||||||
|
//!
|
||||||
|
//! - `R: SessionRepository` — session CRUD persistence
|
||||||
|
//! - `L: SessionLockRepository` — session lock acquire/release
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tracing;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use zesdex_domain::auth::{
|
||||||
|
ServiceError, Session, SessionId, SessionLockRepository, SessionRepository,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Concrete session service backed by injected repository implementations.
|
||||||
|
pub struct SessionServiceImpl<R: SessionRepository, L: SessionLockRepository> {
|
||||||
|
/// Repository for session CRUD operations.
|
||||||
|
pub session_repo: R,
|
||||||
|
/// Repository for session lock acquire/release.
|
||||||
|
pub lock_repo: L,
|
||||||
|
/// Base data directory passed to repository methods.
|
||||||
|
pub base_dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: SessionRepository, L: SessionLockRepository> SessionServiceImpl<R, L> {
|
||||||
|
/// Create a new session service with the given repositories and base
|
||||||
|
/// data directory.
|
||||||
|
pub fn new(session_repo: R, lock_repo: L, base_dir: PathBuf) -> Self {
|
||||||
|
SessionServiceImpl {
|
||||||
|
session_repo,
|
||||||
|
lock_repo,
|
||||||
|
base_dir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(ServiceError::Other)?;
|
||||||
|
let title_owned = if title.is_empty() {
|
||||||
|
"New Session".to_string()
|
||||||
|
} else {
|
||||||
|
title.to_string()
|
||||||
|
};
|
||||||
|
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)?;
|
||||||
|
Ok(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_all(&self) -> Result<Vec<Session>, ServiceError> {
|
||||||
|
tracing::debug!("listing all sessions");
|
||||||
|
self.session_repo
|
||||||
|
.list_sessions(&self.base_dir)
|
||||||
|
.map_err(ServiceError::Repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)?;
|
||||||
|
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)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
//! Conversation use-case implementation.
|
||||||
|
//!
|
||||||
|
//! `ConversationServiceImpl` implements [`ConversationService`] from the
|
||||||
|
//! domain layer. It is generic over `R: ConversationRepository`, delegating
|
||||||
|
//! all persistence to that adapter.
|
||||||
|
//!
|
||||||
|
//! # Flow
|
||||||
|
//!
|
||||||
|
//! Each method computes the session directory from the session ID, then
|
||||||
|
//! delegates the actual I/O to the injected `repo`. Error context is
|
||||||
|
//! added at this layer to identify which session caused the failure.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tracing;
|
||||||
|
|
||||||
|
use zesdex_domain::cms::{Conversation, ConversationRepository, ServiceError};
|
||||||
|
use zesdex_domain::core::ChatMessage;
|
||||||
|
|
||||||
|
/// Service implementation for conversation CRUD operations.
|
||||||
|
///
|
||||||
|
/// Generic over `R: ConversationRepository` so the persistence layer
|
||||||
|
/// can be swapped without changing business logic.
|
||||||
|
pub struct ConversationServiceImpl<R> {
|
||||||
|
pub repo: R,
|
||||||
|
/// Base directory containing session subdirectories.
|
||||||
|
pub sessions_dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: ConversationRepository> ConversationServiceImpl<R> {
|
||||||
|
/// Create a new service with the given repository and sessions directory.
|
||||||
|
pub fn new(repo: R, sessions_dir: impl Into<PathBuf>) -> Self {
|
||||||
|
tracing::debug!("creating ConversationServiceImpl");
|
||||||
|
Self {
|
||||||
|
repo,
|
||||||
|
sessions_dir: sessions_dir.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the session directory for a given session id.
|
||||||
|
fn session_dir(&self, session_id: &str) -> PathBuf {
|
||||||
|
self.sessions_dir.join(session_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: ConversationRepository> zesdex_domain::cms::ConversationService
|
||||||
|
for ConversationServiceImpl<R>
|
||||||
|
{
|
||||||
|
fn load_conversation(&self, session_id: &str) -> Result<Conversation, ServiceError> {
|
||||||
|
tracing::debug!("loading conversation for session {session_id}");
|
||||||
|
let dir = self.session_dir(session_id);
|
||||||
|
self.repo.load(&dir).map_err(ServiceError::Repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_conversation(&self, conv: &Conversation) -> Result<(), ServiceError> {
|
||||||
|
tracing::debug!("saving conversation for session {}", conv.session_id);
|
||||||
|
let dir = self.session_dir(&conv.session_id);
|
||||||
|
self.repo.save(&dir, conv)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
self.repo.save(&dir, conv)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
//! Memory use-case implementation.
|
||||||
|
//!
|
||||||
|
//! `MemoryServiceImpl` implements [`MemoryService`] from the domain
|
||||||
|
//! layer. It is generic over `R: MemoryRepository`, delegating all
|
||||||
|
//! persistence to that adapter.
|
||||||
|
//!
|
||||||
|
//! # Flow
|
||||||
|
//!
|
||||||
|
//! Each method delegates to the injected `repo` with the configured
|
||||||
|
//! `memory_dir`. Error context is added at this layer to identify which
|
||||||
|
//! memory operation failed.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tracing;
|
||||||
|
|
||||||
|
use zesdex_domain::cms::{Memory, MemoryRepository, ServiceError};
|
||||||
|
|
||||||
|
/// Service implementation for memory CRUD operations.
|
||||||
|
///
|
||||||
|
/// Generic over `R: MemoryRepository` so the persistence layer can be
|
||||||
|
/// swapped without changing business logic.
|
||||||
|
pub struct MemoryServiceImpl<R> {
|
||||||
|
pub repo: R,
|
||||||
|
/// Base directory for memory storage files.
|
||||||
|
pub memory_dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: MemoryRepository> MemoryServiceImpl<R> {
|
||||||
|
/// Create a new service with the given repository and memory directory.
|
||||||
|
pub fn new(repo: R, memory_dir: impl Into<PathBuf>) -> Self {
|
||||||
|
tracing::debug!("creating MemoryServiceImpl");
|
||||||
|
Self {
|
||||||
|
repo,
|
||||||
|
memory_dir: memory_dir.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: MemoryRepository> zesdex_domain::cms::MemoryService for MemoryServiceImpl<R> {
|
||||||
|
fn list_memories(&self) -> Result<Vec<String>, ServiceError> {
|
||||||
|
tracing::debug!("listing memories from {:?}", self.memory_dir);
|
||||||
|
self.repo
|
||||||
|
.list(&self.memory_dir)
|
||||||
|
.map_err(ServiceError::Repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_memory(&self, memory: &Memory) -> Result<(), ServiceError> {
|
||||||
|
tracing::debug!("saving memory '{}'", memory.name);
|
||||||
|
self.repo.save(&self.memory_dir, memory)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_memory(&self, name: &str) -> Result<(), ServiceError> {
|
||||||
|
tracing::debug!("deleting memory '{name}'");
|
||||||
|
self.repo
|
||||||
|
.delete(&self.memory_dir, name)
|
||||||
|
.map_err(ServiceError::Repository)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
//! CMS use-case implementations.
|
||||||
|
//!
|
||||||
|
//! Contains concrete service types that implement the domain's CMS
|
||||||
|
//! service traits by coordinating injected repository dependencies.
|
||||||
|
//!
|
||||||
|
//! # Use Cases
|
||||||
|
//!
|
||||||
|
//! - [`conversation_service`] — `ConversationServiceImpl`: conversation CRUD
|
||||||
|
//! - [`memory_service`] — `MemoryServiceImpl`: long-term memory management
|
||||||
|
//! - [`settings_service`] — `SettingsServiceImpl`: settings & app-config management
|
||||||
|
|
||||||
|
pub mod conversation_service;
|
||||||
|
pub mod memory_service;
|
||||||
|
pub mod settings_service;
|
||||||
|
|
||||||
|
pub use conversation_service::ConversationServiceImpl;
|
||||||
|
pub use memory_service::MemoryServiceImpl;
|
||||||
|
pub use settings_service::SettingsServiceImpl;
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
//! Settings and app-config use-case implementation.
|
||||||
|
//!
|
||||||
|
//! `SettingsServiceImpl` implements [`SettingsService`] from the domain
|
||||||
|
//! layer. It is generic over `S: SettingsRepository` and `C: AppConfigRepository`,
|
||||||
|
//! delegating persistence to those adapters.
|
||||||
|
//!
|
||||||
|
//! # Flow
|
||||||
|
//!
|
||||||
|
//! Each method delegates to the appropriate injected repository with the
|
||||||
|
//! configured `base_dir`. The `update_provider` method coordinates between
|
||||||
|
//! both repositories: load app config → mutate provider map → save app config.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tracing;
|
||||||
|
|
||||||
|
use zesdex_domain::cms::{
|
||||||
|
AppConfig, AppConfigRepository, ProviderConfig, ServiceError, Settings, SettingsRepository,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Service implementation for settings and app-config operations.
|
||||||
|
///
|
||||||
|
/// Generic over `S: SettingsRepository` and `C: AppConfigRepository` so
|
||||||
|
/// the persistence layer can be swapped without changing business logic.
|
||||||
|
pub struct SettingsServiceImpl<S, C> {
|
||||||
|
pub settings_repo: S,
|
||||||
|
pub app_config_repo: C,
|
||||||
|
pub base_dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
tracing::debug!("creating SettingsServiceImpl");
|
||||||
|
Self {
|
||||||
|
settings_repo,
|
||||||
|
app_config_repo,
|
||||||
|
base_dir: base_dir.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: SettingsRepository, C: AppConfigRepository> zesdex_domain::cms::SettingsService
|
||||||
|
for SettingsServiceImpl<S, C>
|
||||||
|
{
|
||||||
|
fn load_settings(&self) -> Result<Settings, ServiceError> {
|
||||||
|
tracing::debug!("loading settings");
|
||||||
|
self.settings_repo
|
||||||
|
.load(&self.base_dir)
|
||||||
|
.map_err(ServiceError::Repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_settings(&self, settings: &Settings) -> Result<(), ServiceError> {
|
||||||
|
tracing::debug!("saving settings");
|
||||||
|
self.settings_repo.save(&self.base_dir, settings)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
.providers
|
||||||
|
.insert(name.to_string(), config.clone());
|
||||||
|
self.app_config_repo.save(&self.base_dir, &app_config)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
//! # Zesdex Application Layer
|
||||||
|
//!
|
||||||
|
//! Defines port traits (interfaces) and use-case implementations for the
|
||||||
|
//! Zesdex application. This crate depends **only** on the domain crate;
|
||||||
|
//! it has no knowledge of infrastructure or interface adapters.
|
||||||
|
//!
|
||||||
|
//! ## Architecture
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! apps/application/src/
|
||||||
|
//! ├── lib.rs — crate root, re-exports
|
||||||
|
//! ├── ports/ — Port traits (interfaces to external services)
|
||||||
|
//! │ ├── provider.rs -- ProviderService (LLM chat completion)
|
||||||
|
//! │ ├── password.rs -- PasswordService (hash / verify)
|
||||||
|
//! │ ├── token.rs -- TokenService (JWT create / verify)
|
||||||
|
//! │ └── authentication.rs -- AuthService (combined auth)
|
||||||
|
//! ├── auth/ — Auth use-cases
|
||||||
|
//! │ ├── oauth_service.rs -- OAuth 2.0 PKCE flow
|
||||||
|
//! │ └── session_service.rs -- Session CRUD lifecycle
|
||||||
|
//! └── cms/ — CMS use-cases
|
||||||
|
//! ├── conversation_service.rs -- Conversation CRUD
|
||||||
|
//! ├── memory_service.rs -- Long-term memory management
|
||||||
|
//! └── settings_service.rs -- Settings & app-config management
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ## Key Design Principle
|
||||||
|
//!
|
||||||
|
//! Application services are generic over their repository/port dependencies.
|
||||||
|
//! Concrete implementations are injected at the composition root, keeping
|
||||||
|
//! the use-case logic independent of any specific persistence or infrastructure
|
||||||
|
//! technology.
|
||||||
|
|
||||||
|
pub mod agent;
|
||||||
|
pub mod auth;
|
||||||
|
pub mod cms;
|
||||||
|
pub mod ports;
|
||||||
|
|
||||||
|
// Re-export port traits for ergonomic access.
|
||||||
|
pub use ports::*;
|
||||||
|
|
||||||
|
// Re-export auth use-cases.
|
||||||
|
pub use auth::{
|
||||||
|
oauth_service::{OAuthFlowStore, OAuthUseCase, TokenExchanger},
|
||||||
|
session_service::SessionServiceImpl,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Re-export CMS use-cases.
|
||||||
|
pub use cms::{
|
||||||
|
conversation_service::ConversationServiceImpl, memory_service::MemoryServiceImpl,
|
||||||
|
settings_service::SettingsServiceImpl,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use agent::{
|
||||||
|
turn_service::{compact_messages_with_ai, AgentTurnServiceImpl},
|
||||||
|
AgentTurnService, ToolExecutor,
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
//! AuthService port — combined authentication operations.
|
||||||
|
//!
|
||||||
|
//! Defines a high-level authentication trait that composes password
|
||||||
|
//! verification and token generation into a single use-case boundary.
|
||||||
|
//! Implementations delegate to the injected `PasswordService` and
|
||||||
|
//! `TokenService` adapters.
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::future::Future;
|
||||||
|
|
||||||
|
/// High-level authentication service combining password verification
|
||||||
|
/// and token issuance (login flow).
|
||||||
|
///
|
||||||
|
/// # Flow
|
||||||
|
///
|
||||||
|
/// 1. **`authenticate`** — verify a subject's password against a stored hash.
|
||||||
|
/// 2. **`issue_tokens`** — generate an access + refresh token pair for a subject.
|
||||||
|
///
|
||||||
|
/// Implementations are generic over `PasswordService` and `TokenService`
|
||||||
|
/// port traits.
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// Issue a new access + refresh token pair for the given subject.
|
||||||
|
///
|
||||||
|
/// Returns `(access_token, refresh_token)`.
|
||||||
|
fn issue_tokens(&self, sub: &str) -> Result<(String, String)>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
//! Port traits — interfaces for external / infrastructure services.
|
||||||
|
//!
|
||||||
|
//! These traits define the boundaries between the application layer and
|
||||||
|
//! the outside world. Infrastructure adapters implement these traits;
|
||||||
|
//! the application layer depends only on the trait definitions.
|
||||||
|
//!
|
||||||
|
//! # Ports
|
||||||
|
//!
|
||||||
|
//! - [`provider`] — `ProviderService`: LLM chat completion (streaming + non-streaming)
|
||||||
|
//! - [`password`] — `PasswordService`: password hashing and verification
|
||||||
|
//! - [`token`] — `TokenService`: JWT access/refresh token generation and verification
|
||||||
|
//! - [`authentication`] — `AuthService`: combined authentication operations
|
||||||
|
|
||||||
|
pub mod authentication;
|
||||||
|
pub mod password;
|
||||||
|
pub mod provider;
|
||||||
|
pub mod token;
|
||||||
|
|
||||||
|
pub use authentication::AuthService;
|
||||||
|
pub use password::PasswordService;
|
||||||
|
pub use provider::ProviderService;
|
||||||
|
pub use token::TokenService;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
//! PasswordService port — password hashing and verification abstraction.
|
||||||
|
//!
|
||||||
|
//! Defines the trait that password-hashing adapters (argon2, bcrypt, etc.)
|
||||||
|
//! implement. The application layer depends only on this trait, never on
|
||||||
|
//! a concrete hashing library.
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::future::Future;
|
||||||
|
|
||||||
|
/// Abstraction for password hashing and verification.
|
||||||
|
///
|
||||||
|
/// Implementors handle the actual hashing algorithm (argon2, bcrypt, etc.)
|
||||||
|
/// and parameter selection. The trait is `Send + Sync` for use in async
|
||||||
|
/// service layers.
|
||||||
|
pub trait PasswordService: Send + Sync {
|
||||||
|
/// Hash a plaintext password and return the encoded hash string
|
||||||
|
/// (suitable for storage in a credential store).
|
||||||
|
fn hash(&self, password: &str) -> impl Future<Output = Result<String>> + Send;
|
||||||
|
|
||||||
|
/// Verify a plaintext password against a previously-hashed string.
|
||||||
|
///
|
||||||
|
/// Returns `true` if the password matches the hash, `false` otherwise.
|
||||||
|
fn verify(&self, password: &str, hash: &str) -> impl Future<Output = Result<bool>> + Send;
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
//! ProviderService port — LLM chat completion provider abstraction.
|
||||||
|
//!
|
||||||
|
//! Defines the trait that HTTP-based provider clients (OpenAI, Anthropic,
|
||||||
|
//! etc.) implement. Supports both non-streaming and SSE-streaming chat
|
||||||
|
//! completion requests.
|
||||||
|
//!
|
||||||
|
//! # Flow
|
||||||
|
//!
|
||||||
|
//! 1. Caller builds a message list and optional tool definitions.
|
||||||
|
//! 2. `chat` sends a non-streaming request and returns the full response.
|
||||||
|
//! 3. `chat_stream` sends a streaming request and invokes `on_event` for
|
||||||
|
//! each parsed `StreamEvent` as it arrives, then returns the assembled
|
||||||
|
//! message and usage.
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::future::Future;
|
||||||
|
|
||||||
|
use zesdex_domain::core::{ChatMessage, StreamEvent, ToolDef};
|
||||||
|
|
||||||
|
/// Abstraction for an LLM provider chat-completion service.
|
||||||
|
///
|
||||||
|
/// Both methods accept a message list, optional tool definitions, and
|
||||||
|
/// generation parameters. Implementors handle authentication, HTTP
|
||||||
|
/// transport, retry logic, and response parsing internally.
|
||||||
|
///
|
||||||
|
/// # Send + Sync
|
||||||
|
///
|
||||||
|
/// This trait is `Send + Sync` so it can be shared across async tasks
|
||||||
|
/// and injected into service structs that require thread safety.
|
||||||
|
pub trait ProviderService: Send + Sync {
|
||||||
|
/// Send a non-streaming chat completion request.
|
||||||
|
///
|
||||||
|
/// Returns the assistant's `ChatMessage` and optional token usage
|
||||||
|
/// `(prompt_tokens, completion_tokens)`.
|
||||||
|
fn chat(
|
||||||
|
&self,
|
||||||
|
messages: &[ChatMessage],
|
||||||
|
tools: Option<Vec<ToolDef>>,
|
||||||
|
max_tokens: Option<u32>,
|
||||||
|
temperature: Option<f32>,
|
||||||
|
) -> impl Future<Output = Result<(ChatMessage, Option<(u64, u64)>)>> + Send;
|
||||||
|
|
||||||
|
/// Send a streaming chat completion request.
|
||||||
|
///
|
||||||
|
/// `on_event` is called for every parsed SSE event and returns `false`
|
||||||
|
/// to signal abort (caller cancellation). Returns the fully assembled
|
||||||
|
/// assistant message and optional usage once the stream completes.
|
||||||
|
fn chat_stream(
|
||||||
|
&self,
|
||||||
|
messages: &[ChatMessage],
|
||||||
|
tools: Option<Vec<ToolDef>>,
|
||||||
|
max_tokens: Option<u32>,
|
||||||
|
temperature: Option<f32>,
|
||||||
|
on_event: Box<dyn FnMut(&StreamEvent) -> bool + Send>,
|
||||||
|
) -> impl Future<Output = Result<(ChatMessage, Option<(u64, u64)>)>> + Send;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
//! TokenService port — JWT access and refresh token abstraction.
|
||||||
|
//!
|
||||||
|
//! Defines the trait that JWT adapter implementations provide. Covers
|
||||||
|
//! token generation (pair of access + refresh tokens) and access token
|
||||||
|
//! verification (returns the subject claim).
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
/// Abstraction for JWT-based token generation and verification.
|
||||||
|
///
|
||||||
|
/// Implementors handle signing key management, token serialisation,
|
||||||
|
/// and expiry validation. The trait is `Send + Sync` for use across
|
||||||
|
/// thread boundaries.
|
||||||
|
pub trait TokenService: Send + Sync {
|
||||||
|
/// Generate an access + refresh token pair for the given subject
|
||||||
|
/// identifier.
|
||||||
|
///
|
||||||
|
/// Returns `(access_token, refresh_token)`.
|
||||||
|
fn generate_tokens(&self, sub: &str) -> Result<(String, String)>;
|
||||||
|
|
||||||
|
/// Verify an access token and return the embedded subject claim.
|
||||||
|
///
|
||||||
|
/// 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>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "zesdex-bootstrap"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
|
||||||
|
# Bootstrap binary — seeds initial system data (permissions, roles,
|
||||||
|
# admin user) idempotently. Run once after first deployment.
|
||||||
|
[[bin]]
|
||||||
|
name = "bootstrap"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
zesdex-domain = { path = "../domain" }
|
||||||
|
zesdex-application = { path = "../application" }
|
||||||
|
zesdex-infrastructure = { path = "../infrastructure" }
|
||||||
|
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
uuid.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
dirs.workspace = true
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
//! Bootstrap library — shared utilities for the bootstrap binary.
|
||||||
|
//! The main entry point is in `main.rs`.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
//! Bootstrap binary — seeds initial system data idempotently.
|
||||||
|
//!
|
||||||
|
//! Creates default permissions, roles, and admin user if they don't
|
||||||
|
//! already exist. Run once after first deployment.
|
||||||
|
//!
|
||||||
|
//! Usage: cargo run --bin bootstrap
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
println!("Zesdex Bootstrap — seeding initial data...");
|
||||||
|
|
||||||
|
let store = zesdex_domain::core::Store::new();
|
||||||
|
store.ensure_dirs()?;
|
||||||
|
|
||||||
|
// Seed default settings if not present
|
||||||
|
let settings_path = store.base_dir.join("settings.json");
|
||||||
|
if !settings_path.exists() {
|
||||||
|
let settings = zesdex_domain::cms::Settings::default();
|
||||||
|
let content = serde_json::to_string_pretty(&settings)?;
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed default app config if not present
|
||||||
|
let config_path = store.base_dir.join("app_config.json");
|
||||||
|
if !config_path.exists() {
|
||||||
|
let config = zesdex_domain::cms::AppConfig::default();
|
||||||
|
let content = serde_json::to_string_pretty(&config)?;
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Bootstrap complete.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "zesdex-entities"
|
name = "zesdex-domain"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
||||||
|
# Domain layer — PURE entities, value objects, repository/service traits.
|
||||||
|
# Zero framework dependencies. Only serde for serialization, chrono for
|
||||||
|
# timestamps, uuid for identity.
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
anyhow.workspace = true
|
|
||||||
dirs.workspace = true
|
|
||||||
libc.workspace = true
|
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
reqwest.workspace = true
|
libc.workspace = true
|
||||||
tokio.workspace = true
|
anyhow.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
@@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
//! Command types for IAM domain operations.
|
||||||
|
//!
|
||||||
|
//! Following the `NewXxx` / command pattern from clean architecture,
|
||||||
|
//! these types encapsulate the input data for create/update operations
|
||||||
|
//! on domain entities. They decouple presentation DTOs from the entity
|
||||||
|
//! mutation surface and provide a clear boundary for validation.
|
||||||
|
|
||||||
|
/// Command to create a new session.
|
||||||
|
///
|
||||||
|
/// Carries only the data needed to construct a session entity — the
|
||||||
|
/// service generates the UUID and timestamp internally.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct NewSession {
|
||||||
|
/// Human-readable session title.
|
||||||
|
pub title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for NewSession {
|
||||||
|
fn from(title: String) -> Self {
|
||||||
|
Self { title }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for NewSession {
|
||||||
|
fn from(title: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
title: title.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
//! Domain error types for the IAM (auth) module.
|
||||||
|
//!
|
||||||
|
//! Typed error enums replace `anyhow::Result` in domain traits and
|
||||||
|
//! application services, enabling callers to match on specific error
|
||||||
|
//! variants (e.g. `NotFound` vs `Conflict`) rather than string-checking.
|
||||||
|
//!
|
||||||
|
//! # Components
|
||||||
|
//!
|
||||||
|
//! - [`RepositoryError`] — persistence-layer errors (not found, conflict, I/O)
|
||||||
|
//! - [`ServiceError`] — use-case / orchestration errors (config, state
|
||||||
|
//! mismatch, provider failures)
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::error::DomainError;
|
||||||
|
|
||||||
|
/// Shared repository error type for IAM persistence operations.
|
||||||
|
pub type RepositoryError = DomainError;
|
||||||
|
|
||||||
|
/// Errors from service / use-case operations in the IAM domain.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ServiceError {
|
||||||
|
/// A repository operation failed.
|
||||||
|
Repository(DomainError),
|
||||||
|
/// The provided configuration is invalid.
|
||||||
|
InvalidConfig(String),
|
||||||
|
/// OAuth state mismatch — possible CSRF attack.
|
||||||
|
StateMismatch,
|
||||||
|
/// The OAuth provider returned an error.
|
||||||
|
OAuthProvider(String),
|
||||||
|
/// A generic error with a message.
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DomainError> for ServiceError {
|
||||||
|
fn from(err: DomainError) -> Self {
|
||||||
|
ServiceError::Repository(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ServiceError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
ServiceError::Repository(err) => write!(f, "repository error: {err}"),
|
||||||
|
ServiceError::InvalidConfig(msg) => write!(f, "invalid configuration: {msg}"),
|
||||||
|
ServiceError::StateMismatch => {
|
||||||
|
write!(f, "OAuth state mismatch — possible CSRF attack")
|
||||||
|
}
|
||||||
|
ServiceError::OAuthProvider(msg) => write!(f, "OAuth provider error: {msg}"),
|
||||||
|
ServiceError::Other(msg) => write!(f, "{msg}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ServiceError {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
ServiceError::Repository(err) => Some(err),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
//! IAM Session re-export.
|
||||||
|
//!
|
||||||
|
//! Re-exports `Session` from the auth module for consistent IAM-boundary
|
||||||
|
//! imports. Consumers of the IAM module import `Session` from here rather
|
||||||
|
//! than from the core session module directly, keeping the dependency
|
||||||
|
//! internal and allowing the IAM crate to own its domain vocabulary.
|
||||||
|
|
||||||
|
pub use super::session::Session;
|
||||||
|
|
||||||
|
/// Alias for `Session` used in IAM contexts to distinguish from other
|
||||||
|
/// session types in the system.
|
||||||
|
pub type IamSession = Session;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
//! Authentication domain entities, commands, errors, and repository/service traits.
|
||||||
|
//!
|
||||||
|
//! Combines the session types from `zesdex-entities` (auth sub-module) with the
|
||||||
|
//! IAM domain types (commands, OAuth, repository/service traits) from `zesdex-iam`.
|
||||||
|
//!
|
||||||
|
//! # Sub-modules
|
||||||
|
//!
|
||||||
|
//! - [`session`] — `Session` entity (session metadata)
|
||||||
|
//! - [`session_id`] — `SessionId` value object (validated newtype)
|
||||||
|
//! - [`session_lock`] — `SessionLock` RAII guard (PID-file lock)
|
||||||
|
//! - [`oauth`] — `OAuthToken`, `OAuthConfig` entities
|
||||||
|
//! - [`iam_session`] — Re-export of `Session` for IAM-boundary consistency
|
||||||
|
//! - [`commands`] — `NewSession` command type
|
||||||
|
//! - [`error`] — `RepositoryError`, `ServiceError` types
|
||||||
|
//! - [`repository`] — `SessionRepository`, `SessionLockRepository`, `OAuthRepository`
|
||||||
|
//! - [`service`] — `SessionService`, `OAuthService` traits
|
||||||
|
|
||||||
|
pub mod commands;
|
||||||
|
pub mod error;
|
||||||
|
pub mod iam_session;
|
||||||
|
pub mod oauth;
|
||||||
|
pub mod repository;
|
||||||
|
pub mod service;
|
||||||
|
pub mod session;
|
||||||
|
pub mod session_id;
|
||||||
|
pub mod session_lock;
|
||||||
|
|
||||||
|
pub use commands::NewSession;
|
||||||
|
pub use error::{RepositoryError, ServiceError};
|
||||||
|
pub use iam_session::IamSession;
|
||||||
|
pub use oauth::{OAuthConfig, OAuthToken};
|
||||||
|
pub use repository::{OAuthRepository, SessionLockRepository, SessionRepository};
|
||||||
|
pub use service::{OAuthService, SessionService};
|
||||||
|
pub use session::Session;
|
||||||
|
pub use session_id::SessionId;
|
||||||
|
pub use session_lock::SessionLock;
|
||||||
@@ -1,29 +1,38 @@
|
|||||||
#![allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_sign_loss,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::cast_possible_wrap
|
|
||||||
)]
|
|
||||||
//! Pure OAuth entities — no HTTP or persistence logic.
|
//! Pure OAuth entities — no HTTP or persistence logic.
|
||||||
|
//!
|
||||||
|
//! # Components
|
||||||
|
//!
|
||||||
|
//! - [`OAuthToken`] — access token with optional refresh token, epoch expiry
|
||||||
|
//! - [`OAuthConfig`] — provider configuration (auth URL, token URL, client id,
|
||||||
|
//! optional client secret, scopes)
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// An OAuth 2.0 access token with optional refresh token and absolute
|
/// An OAuth 2.0 access token with optional refresh token and absolute
|
||||||
/// expiry time (epoch seconds).
|
/// expiry time (epoch seconds).
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct OAuthToken {
|
pub struct OAuthToken {
|
||||||
|
/// The OAuth 2.0 access token string.
|
||||||
pub access_token: String,
|
pub access_token: String,
|
||||||
|
/// Optional refresh token for long-lived access.
|
||||||
pub refresh_token: Option<String>,
|
pub refresh_token: Option<String>,
|
||||||
|
/// Absolute expiry timestamp (epoch seconds since UNIX_EPOCH).
|
||||||
pub expires_at: u64,
|
pub expires_at: u64,
|
||||||
|
/// Token type, e.g. `"Bearer"`.
|
||||||
pub token_type: String,
|
pub token_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Static configuration for an OAuth provider.
|
/// Static configuration for an OAuth provider.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct OAuthConfig {
|
pub struct OAuthConfig {
|
||||||
|
/// Authorization endpoint URL.
|
||||||
pub auth_url: String,
|
pub auth_url: String,
|
||||||
|
/// Token exchange endpoint URL.
|
||||||
pub token_url: String,
|
pub token_url: String,
|
||||||
|
/// OAuth client identifier.
|
||||||
pub client_id: String,
|
pub client_id: String,
|
||||||
|
/// Optional client secret (not all flows require it).
|
||||||
pub client_secret: Option<String>,
|
pub client_secret: Option<String>,
|
||||||
|
/// Space-separated list of requested scopes.
|
||||||
pub scopes: Vec<String>,
|
pub scopes: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
//! Repository trait definitions (pure — no impls, no concrete persistence).
|
||||||
|
//!
|
||||||
|
//! Defines the repository contracts that infrastructure adapters implement.
|
||||||
|
//! Following clean architecture, domain code depends only on these traits,
|
||||||
|
//! not on concrete persistence libraries.
|
||||||
|
//!
|
||||||
|
//! # Traits
|
||||||
|
//!
|
||||||
|
//! - [`SessionRepository`] — CRUD for session metadata
|
||||||
|
//! - [`SessionLockRepository`] — acquire/release/liveness for session locks
|
||||||
|
//! - [`OAuthRepository`] — persist/load OAuth tokens
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::auth::error::RepositoryError;
|
||||||
|
use crate::auth::oauth::OAuthToken;
|
||||||
|
use crate::auth::session::Session;
|
||||||
|
use crate::auth::session_id::SessionId;
|
||||||
|
|
||||||
|
/// Repository for loading, saving, listing, and deleting sessions.
|
||||||
|
pub trait SessionRepository {
|
||||||
|
/// List all loadable sessions under `<base_dir>/sessions/`.
|
||||||
|
fn list_sessions(&self, base_dir: &Path) -> Result<Vec<Session>, RepositoryError>;
|
||||||
|
|
||||||
|
/// Load a single session by id.
|
||||||
|
fn load_session(&self, base_dir: &Path, id: &SessionId) -> Result<Session, RepositoryError>;
|
||||||
|
|
||||||
|
/// Save a session's metadata to disk.
|
||||||
|
fn save_session(&self, base_dir: &Path, session: &Session) -> Result<(), RepositoryError>;
|
||||||
|
|
||||||
|
/// Delete a session directory and all its contents.
|
||||||
|
fn delete_session(&self, base_dir: &Path, id: &SessionId) -> Result<(), RepositoryError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Repository for per-session PID-file advisory locks.
|
||||||
|
pub trait SessionLockRepository {
|
||||||
|
/// Try to acquire the lock for a session directory.
|
||||||
|
/// Returns `true` if the lock was acquired, `false` if another live
|
||||||
|
/// process holds it.
|
||||||
|
fn try_lock(&self, session_dir: &Path) -> Result<bool, RepositoryError>;
|
||||||
|
|
||||||
|
/// Release the lock by removing the lock file.
|
||||||
|
fn unlock(&self, session_dir: &Path) -> Result<(), RepositoryError>;
|
||||||
|
|
||||||
|
/// Check whether a process with the given PID is alive.
|
||||||
|
fn is_alive(&self, pid: u32) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Repository for persisting and loading OAuth tokens.
|
||||||
|
pub trait OAuthRepository {
|
||||||
|
/// Persist an OAuth token to a JSON file.
|
||||||
|
fn save_token(&self, path: &Path, token: &OAuthToken) -> Result<(), RepositoryError>;
|
||||||
|
|
||||||
|
/// Load an OAuth token from a JSON file, returning `None` if the file
|
||||||
|
/// does not exist.
|
||||||
|
fn load_token(&self, path: &Path) -> Result<Option<OAuthToken>, RepositoryError>;
|
||||||
|
}
|
||||||
@@ -1,24 +1,29 @@
|
|||||||
#![allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_sign_loss,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::cast_possible_wrap
|
|
||||||
)]
|
|
||||||
//! Service trait definitions — use-case interfaces for session management
|
//! Service trait definitions — use-case interfaces for session management
|
||||||
//! and OAuth flows.
|
//! and OAuth flows.
|
||||||
use crate::domain::oauth::{OAuthConfig, OAuthToken};
|
//!
|
||||||
use crate::domain::session::Session;
|
//! These traits define the boundary between the application orchestration
|
||||||
|
//! layer and the domain. Implementations live in the application layer.
|
||||||
|
//!
|
||||||
|
//! # Traits
|
||||||
|
//!
|
||||||
|
//! - [`SessionService`] — create, list, archive sessions
|
||||||
|
//! - [`OAuthService`] — start PKCE flow, complete code exchange, retrieve token
|
||||||
|
|
||||||
|
use crate::auth::error::ServiceError;
|
||||||
|
use crate::auth::oauth::{OAuthConfig, OAuthToken};
|
||||||
|
use crate::auth::session::Session;
|
||||||
|
use crate::auth::session_id::SessionId;
|
||||||
|
|
||||||
/// Session management use-case boundary.
|
/// Session management use-case boundary.
|
||||||
pub trait SessionService {
|
pub trait SessionService {
|
||||||
/// Create a new session with a generated UUID and the given title.
|
/// Create a new session with a generated UUID and the given title.
|
||||||
fn create_session(&self, title: &str) -> anyhow::Result<Session>;
|
fn create_session(&self, title: &str) -> Result<Session, ServiceError>;
|
||||||
|
|
||||||
/// List all available sessions.
|
/// List all available sessions.
|
||||||
fn list_all(&self) -> anyhow::Result<Vec<Session>>;
|
fn list_all(&self) -> Result<Vec<Session>, ServiceError>;
|
||||||
|
|
||||||
/// Archive a session by id (sets `archived = true`).
|
/// Archive a session by id (sets `archived = true`).
|
||||||
fn archive_session(&self, id: &str) -> anyhow::Result<()>;
|
fn archive_session(&self, id: SessionId) -> Result<(), ServiceError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// OAuth flow use-case boundary.
|
/// OAuth flow use-case boundary.
|
||||||
@@ -32,7 +37,7 @@ pub trait OAuthService {
|
|||||||
&self,
|
&self,
|
||||||
config: &OAuthConfig,
|
config: &OAuthConfig,
|
||||||
redirect_uri: &str,
|
redirect_uri: &str,
|
||||||
) -> anyhow::Result<(String, String)>;
|
) -> Result<(String, String), ServiceError>;
|
||||||
|
|
||||||
/// Complete the OAuth flow: validates `state` against the value
|
/// Complete the OAuth flow: validates `state` against the value
|
||||||
/// persisted during `start_flow` (bailing on mismatch — this is the
|
/// persisted during `start_flow` (bailing on mismatch — this is the
|
||||||
@@ -44,8 +49,8 @@ pub trait OAuthService {
|
|||||||
redirect_uri: &str,
|
redirect_uri: &str,
|
||||||
code: &str,
|
code: &str,
|
||||||
state: &str,
|
state: &str,
|
||||||
) -> anyhow::Result<OAuthToken>;
|
) -> Result<OAuthToken, ServiceError>;
|
||||||
|
|
||||||
/// Retrieve the currently stored OAuth token (if any).
|
/// Retrieve the currently stored OAuth token (if any).
|
||||||
fn get_token(&self) -> anyhow::Result<Option<OAuthToken>>;
|
fn get_token(&self) -> Result<Option<OAuthToken>, ServiceError>;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
//! Session metadata: id, title, workspace roots, and message/token counts,
|
||||||
|
//! persisted as `session.json` per session directory.
|
||||||
|
//!
|
||||||
|
//! # Flow
|
||||||
|
//!
|
||||||
|
//! Created via [`Session::new`] → mutated in-memory → persisted via repository.
|
||||||
|
//!
|
||||||
|
//! # Components
|
||||||
|
//!
|
||||||
|
//! - `Session` struct — fields for all session metadata
|
||||||
|
//! - `new` — timestamped constructor
|
||||||
|
//! - `session_dir` / `conversation_path` — pure path computation
|
||||||
|
use chrono::Utc;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// Metadata for one conversation session (distinct from the message
|
||||||
|
/// history itself, which lives in `Conversation`/the msglog).
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Session {
|
||||||
|
/// Unique session identifier (validated against path traversal in `load`).
|
||||||
|
pub id: String,
|
||||||
|
/// Epoch-millis timestamp of creation (`Utc::now().timestamp_millis()`).
|
||||||
|
pub created_at: i64,
|
||||||
|
/// Epoch-millis timestamp of last update.
|
||||||
|
pub updated_at: i64,
|
||||||
|
/// Human-readable title for the conversation.
|
||||||
|
pub title: String,
|
||||||
|
/// Model identifier string, e.g. `"anthropic/claude-opus-4-8"`.
|
||||||
|
pub model: String,
|
||||||
|
/// Workspace root directories associated with this session.
|
||||||
|
pub workspace_roots: Vec<PathBuf>,
|
||||||
|
/// Running count of messages in the conversation.
|
||||||
|
pub message_count: u32,
|
||||||
|
/// Running count of tokens consumed.
|
||||||
|
pub token_count: u32,
|
||||||
|
/// Soft-delete flag — archived sessions are hidden from the default list.
|
||||||
|
pub archived: bool,
|
||||||
|
/// Optional AI-generated conversation summary (used for compact context).
|
||||||
|
pub summary: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Session {
|
||||||
|
/// Create a new session with the given id/title, defaulting the
|
||||||
|
/// model, workspace root (current dir), and counters.
|
||||||
|
pub fn new(id: String, title: String) -> Self {
|
||||||
|
let now = Utc::now().timestamp_millis();
|
||||||
|
Session {
|
||||||
|
id,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
title,
|
||||||
|
model: "anthropic/claude-opus-4-8".to_string(),
|
||||||
|
workspace_roots: vec![std::env::current_dir().unwrap_or_default()],
|
||||||
|
message_count: 0,
|
||||||
|
token_count: 0,
|
||||||
|
archived: false,
|
||||||
|
summary: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute this session's directory under `<base_dir>/sessions/<id>`.
|
||||||
|
pub fn session_dir(&self, base_dir: &Path) -> PathBuf {
|
||||||
|
base_dir.join("sessions").join(&self.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute this session's `conversation.json` path.
|
||||||
|
pub fn conversation_path(&self, base_dir: &Path) -> PathBuf {
|
||||||
|
self.session_dir(base_dir).join("conversation.json")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
//! Validated session identifier newtype.
|
||||||
|
//!
|
||||||
|
//! [`SessionId`] wraps a `String` that has been checked for path-traversal
|
||||||
|
//! characters. Construction via `SessionId::new(str)` validates the input
|
||||||
|
//! once; the guarantee is then enforced by the type system for all
|
||||||
|
//! downstream use.
|
||||||
|
//!
|
||||||
|
//! # Validation rules
|
||||||
|
//!
|
||||||
|
//! - Must not be empty
|
||||||
|
//! - Must only contain alphanumeric characters, hyphens, and underscores
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// A validated session identifier.
|
||||||
|
///
|
||||||
|
/// Guarantees the inner string is non-empty and contains no path-traversal
|
||||||
|
/// characters (`/`, `\\`, `..`) or other unsafe delimiters.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||||
|
pub struct SessionId(String);
|
||||||
|
|
||||||
|
impl SessionId {
|
||||||
|
/// Validate and construct a `SessionId`.
|
||||||
|
///
|
||||||
|
/// Returns `Err(msg)` if the input contains path separators, `..`, or
|
||||||
|
/// is empty.
|
||||||
|
pub fn new(id: &str) -> Result<Self, String> {
|
||||||
|
if id.is_empty() {
|
||||||
|
return Err("session id must not be empty".to_string());
|
||||||
|
}
|
||||||
|
if id.contains('/') || id.contains('\\') || id.contains("..") {
|
||||||
|
return Err(format!(
|
||||||
|
"session id '{id}' must not contain path separators"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(SessionId(id.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the underlying string.
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the underlying owned string.
|
||||||
|
pub fn into_string(self) -> String {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append this session id as a component of `base_dir`, yielding
|
||||||
|
/// `base_dir / self.0`.
|
||||||
|
///
|
||||||
|
/// Safe because the id has been validated to contain no path separators.
|
||||||
|
pub fn join_to(&self, base_dir: &Path) -> PathBuf {
|
||||||
|
base_dir.join(&self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for SessionId {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for SessionId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(&self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SessionId> for String {
|
||||||
|
fn from(sid: SessionId) -> Self {
|
||||||
|
sid.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_valid_uuids() {
|
||||||
|
assert!(SessionId::new("550e8400-e29b-41d4-a716-446655440000").is_ok());
|
||||||
|
assert!(SessionId::new("my-session_123").is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rejects_path_traversal() {
|
||||||
|
assert!(SessionId::new("../etc/passwd").is_err());
|
||||||
|
assert!(SessionId::new("foo/../../bar").is_err());
|
||||||
|
assert!(SessionId::new("foo\\..\\bar").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rejects_empty() {
|
||||||
|
assert!(SessionId::new("").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_into_string() {
|
||||||
|
let sid = SessionId::new("abc-123").unwrap();
|
||||||
|
assert_eq!(sid.into_string(), "abc-123");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
//! PID-file based advisory lock preventing two processes from operating on
|
||||||
|
//! the same session directory concurrently.
|
||||||
|
//!
|
||||||
|
//! # Flow
|
||||||
|
//!
|
||||||
|
//! [`SessionLock::new`] creates a handle → [`SessionLock::try_lock`] attempts
|
||||||
|
//! atomic `O_CREAT|O_EXCL` creation. If the lock file already exists, the
|
||||||
|
//! owning PID is checked via liveness verification. Stale locks are
|
||||||
|
//! overwritten atomically (temp-file + rename + fsync). On [`Drop`],
|
||||||
|
//! the lock file is removed automatically.
|
||||||
|
//!
|
||||||
|
//! # Components
|
||||||
|
//!
|
||||||
|
//! - `SessionLock` — RAII guard wrapping a lock file path and PID
|
||||||
|
//! - `try_lock` — three-phase atomic acquire with stale-lock recovery
|
||||||
|
//! - `unlock` / `Drop` — explicit and implicit release
|
||||||
|
use std::fs;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use tracing;
|
||||||
|
|
||||||
|
/// A PID-file lock (`<session_dir>/.lock`) tied to the current process,
|
||||||
|
/// auto-removed on drop.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SessionLock {
|
||||||
|
/// Path to the `.lock` file inside the session directory.
|
||||||
|
pub(crate) path: PathBuf,
|
||||||
|
/// Process ID that holds (or will hold) this lock.
|
||||||
|
pub(crate) pid: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionLock {
|
||||||
|
/// Construct a lock handle for a session directory (does not acquire
|
||||||
|
/// the lock yet — call `try_lock`).
|
||||||
|
pub fn new(session_dir: &Path) -> Self {
|
||||||
|
SessionLock {
|
||||||
|
path: session_dir.join(".lock"),
|
||||||
|
pid: std::process::id(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to acquire the session lock using an atomic file creation.
|
||||||
|
///
|
||||||
|
/// Flow: try `O_CREAT | O_EXCL` via `create_new(true)` → if that
|
||||||
|
/// succeeds, the lock is ours — write our PID and return ok. If the
|
||||||
|
/// file already exists, read the PID inside it and check whether that
|
||||||
|
/// PID is still alive: if the process is still running, fail to acquire;
|
||||||
|
/// otherwise the lock is stale — overwrite it with our own PID and succeed.
|
||||||
|
///
|
||||||
|
/// Return: `Ok(true)` if acquired, `Ok(false)` if another live
|
||||||
|
/// process holds it, `Err` on I/O failure.
|
||||||
|
pub fn try_lock(&self) -> std::io::Result<bool> {
|
||||||
|
// Phase 1: try atomic create. If it succeeds, the lock is ours.
|
||||||
|
match fs::OpenOptions::new()
|
||||||
|
.create_new(true)
|
||||||
|
.write(true)
|
||||||
|
.open(&self.path)
|
||||||
|
{
|
||||||
|
Ok(mut file) => {
|
||||||
|
write!(file, "{}", self.pid)?;
|
||||||
|
file.sync_all()?;
|
||||||
|
tracing::debug!(path = %self.path.display(), pid = self.pid, "session lock acquired");
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
Err(ref e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||||
|
tracing::debug!(path = %self.path.display(), "session lock already exists, checking staleness");
|
||||||
|
// Lock file exists — check if it's stale.
|
||||||
|
}
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: lock file exists — check liveness of the owning process.
|
||||||
|
let content = fs::read_to_string(&self.path).unwrap_or_default();
|
||||||
|
if let Ok(pid) = content.trim().parse::<u32>() {
|
||||||
|
if Self::is_alive(pid) {
|
||||||
|
tracing::warn!(stale = pid, path = %self.path.display(), "session lock held by live process");
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
tracing::debug!(stale = pid, "stale lock detected, overwriting");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: stale lock — overwrite it atomically (best-effort).
|
||||||
|
// Use a temp file + rename to avoid partial writes corrupting the lock.
|
||||||
|
let tmp = self.path.with_extension("lock.tmp");
|
||||||
|
{
|
||||||
|
let mut tmp_file = fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
|
.write(true)
|
||||||
|
.open(&tmp)?;
|
||||||
|
write!(tmp_file, "{}", self.pid)?;
|
||||||
|
tmp_file.sync_all()?;
|
||||||
|
}
|
||||||
|
fs::rename(&tmp, &self.path)?;
|
||||||
|
// Sync the parent directory so the rename survives a crash.
|
||||||
|
if let Some(parent) = self.path.parent() {
|
||||||
|
let _ = fs::File::open(parent).and_then(|d| d.sync_all());
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Explicitly release the lock by removing the lock file.
|
||||||
|
pub fn unlock(&self) {
|
||||||
|
let _ = fs::remove_file(&self.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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).
|
||||||
|
///
|
||||||
|
/// On non-Unix platforms this always returns `true` (conservative).
|
||||||
|
fn is_alive(pid: u32) -> bool {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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))]
|
||||||
|
{
|
||||||
|
// Fallback: always assume alive (conservative).
|
||||||
|
let _ = pid;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for SessionLock {
|
||||||
|
/// Release the lock automatically when the guard goes out of scope,
|
||||||
|
/// so an ungracefully-exited process doesn't leave a dangling lock.
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = fs::remove_file(&self.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
//! Pure domain entity for application configuration.
|
||||||
|
//!
|
||||||
|
//! Defines `AppConfig`, `ProviderConfig`, and `ModelRole` — the data
|
||||||
|
//! structures that describe which LLM providers are registered, which
|
||||||
|
//! model roles exist, and which provider/model is the default.
|
||||||
|
//!
|
||||||
|
//! # Architecture
|
||||||
|
//! These are pure data structures with **no I/O logic**. Load/save
|
||||||
|
//! responsibilities live in `AppConfigRepository` (domain::repository).
|
||||||
|
//!
|
||||||
|
//! ## Data Flow
|
||||||
|
//! 1. `AppConfig` is deserialised from `app_config.json` at startup
|
||||||
|
//! 2. The HTTP handler layer calls `SettingsService::update_provider()`
|
||||||
|
//! to mutate the provider map
|
||||||
|
//! 3. The modified `AppConfig` is serialised back to `app_config.json`
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Top-level application configuration.
|
||||||
|
///
|
||||||
|
/// Holds the registry of configured LLM providers, named model roles
|
||||||
|
/// (logical profiles mapping to a provider+model pair), and the default
|
||||||
|
/// provider/model selection.
|
||||||
|
///
|
||||||
|
/// ## Fields
|
||||||
|
/// - `providers` — map of provider name → connection details
|
||||||
|
/// - `model_roles` — map of role name → provider/model/temperature
|
||||||
|
/// - `default_provider` — the provider to use when none is specified
|
||||||
|
/// - `default_model` — the model to use when none is specified
|
||||||
|
/// - `default_context_window` — fallback context window size in tokens
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AppConfig {
|
||||||
|
pub providers: HashMap<String, ProviderConfig>,
|
||||||
|
pub model_roles: HashMap<String, ModelRole>,
|
||||||
|
pub default_provider: String,
|
||||||
|
pub default_model: String,
|
||||||
|
pub default_context_window: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connection details for a single LLM provider endpoint.
|
||||||
|
///
|
||||||
|
/// ## Fields
|
||||||
|
/// - `api_base` — base URL for the provider API
|
||||||
|
/// - `api_key_env` — optional environment variable name holding the API key
|
||||||
|
/// - `default_model` — optional default model name for this provider
|
||||||
|
/// - `default_api_key` — optional inline API key (less secure than env var)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ProviderConfig {
|
||||||
|
pub api_base: String,
|
||||||
|
pub api_key_env: Option<String>,
|
||||||
|
pub default_model: Option<String>,
|
||||||
|
pub default_api_key: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A named model role mapping to a specific provider/model with parameters.
|
||||||
|
///
|
||||||
|
/// Roles allow the UI to present logical profiles (e.g. "fast", "reasoning")
|
||||||
|
/// that abstract over concrete provider+model strings.
|
||||||
|
///
|
||||||
|
/// ## Fields
|
||||||
|
/// - `provider` — which provider serves this role
|
||||||
|
/// - `model` — which model to use for this role
|
||||||
|
/// - `max_tokens` — optional maximum output token limit
|
||||||
|
/// - `context_window` — optional context window override
|
||||||
|
/// - `temperature` — optional generation temperature
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ModelRole {
|
||||||
|
pub provider: String,
|
||||||
|
pub model: String,
|
||||||
|
pub max_tokens: Option<u32>,
|
||||||
|
pub context_window: Option<u32>,
|
||||||
|
pub temperature: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the default AppConfig with built-in "zen" and "router" providers.
|
||||||
|
impl Default for AppConfig {
|
||||||
|
/// Construct an AppConfig with the default "zen" and "router" providers.
|
||||||
|
///
|
||||||
|
/// ## Defaults
|
||||||
|
/// - Zen provider: `deepseek-v4-flash-free` model
|
||||||
|
/// - Router provider: `claude-opus-4-8` model
|
||||||
|
/// - Default role: "default" → zen / deepseek-v4-flash-free, temp 0.7
|
||||||
|
/// - `default_context_window`: 256,000 tokens
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut providers = HashMap::new();
|
||||||
|
providers.insert(
|
||||||
|
"zen".to_string(),
|
||||||
|
ProviderConfig {
|
||||||
|
api_base: "https://opencode.ai/zen/v1".to_string(),
|
||||||
|
api_key_env: Some("API_KEY".to_string()),
|
||||||
|
default_model: Some("deepseek-v4-flash-free".to_string()),
|
||||||
|
default_api_key: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
providers.insert(
|
||||||
|
"router".to_string(),
|
||||||
|
ProviderConfig {
|
||||||
|
api_base: "https://9router.asepharyana.my.id/v1".to_string(),
|
||||||
|
api_key_env: Some("ROUTER_API_KEY".to_string()),
|
||||||
|
default_model: Some("claude-opus-4-8".to_string()),
|
||||||
|
default_api_key: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut model_roles = HashMap::new();
|
||||||
|
model_roles.insert(
|
||||||
|
"default".to_string(),
|
||||||
|
ModelRole {
|
||||||
|
provider: "zen".to_string(),
|
||||||
|
model: "deepseek-v4-flash-free".to_string(),
|
||||||
|
max_tokens: None,
|
||||||
|
context_window: None,
|
||||||
|
temperature: Some(0.7),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
providers,
|
||||||
|
model_roles,
|
||||||
|
default_provider: "zen".to_string(),
|
||||||
|
default_model: "deepseek-v4-flash-free".to_string(),
|
||||||
|
default_context_window: 256_000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user