docs: tambah doc comment, logging, dan inline comments di semua 255 file
Meliputi: - File-level //! doc comment: tujuan file, alur kerja, komponen utama - Function-level /// doc comment: apa, parameter, return, flow, edge cases - Struct/enum/trait /// doc comment: peran, field docs - Tracing logging (tracing::info!/debug!/trace!/warn!/error!) di setiap fungsi - Inline comments untuk variable dan branching logic penting - Seluruh 8 crates di workspace: zesdex-backend, zesdex-cms, zesdex-entities, zesdex-iam, zesdex-infra, zesdex-ipc, zesdex-middleware, zesdex-utils - Build: 0 errors, 242/242 tests passed
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
//! Authentication entities: session metadata and PID-file lock.
|
||||
//!
|
||||
//! # Types
|
||||
//!
|
||||
//! - [`Session`](session::Session) — Authenticated user session with tokens, expiry, refresh
|
||||
//! - [`SessionLock`](session_lock::SessionLock) — Exclusive PID-based lock to prevent concurrent sessions
|
||||
|
||||
pub mod session;
|
||||
pub mod session_lock;
|
||||
|
||||
@@ -1,22 +1,45 @@
|
||||
//! 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 [`Session::save`]
|
||||
//! (atomic write with fsync). Loaded back via [`Session::load`] or enumerated via
|
||||
//! [`Session::list`]. Directory traversal is blocked by input validation in `load`.
|
||||
//!
|
||||
//! # Components
|
||||
//!
|
||||
//! - `Session` struct — fields for all session metadata
|
||||
//! - `new` — timestamped constructor
|
||||
//! - `save` / `load` / `list` — CRUD against the filesystem
|
||||
use chrono::Utc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tracing;
|
||||
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
@@ -64,6 +87,7 @@ impl Session {
|
||||
let dir = self.session_dir(base_dir);
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
let path = dir.join("session.json");
|
||||
tracing::debug!(id = %self.id, path = %path.display(), "saving session metadata");
|
||||
zesdex_utils::write_json_atomic(&path, self, None)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -85,7 +109,8 @@ impl Session {
|
||||
));
|
||||
}
|
||||
let path = base_dir.join("sessions").join(id).join("session.json");
|
||||
let data = std::fs::read_to_string(path)?;
|
||||
tracing::debug!(id = %id, path = %path.display(), "loading session metadata");
|
||||
let data = std::fs::read_to_string(&path)?;
|
||||
let session: Session = serde_json::from_str(&data)?;
|
||||
Ok(session)
|
||||
}
|
||||
@@ -101,6 +126,7 @@ impl Session {
|
||||
pub fn list(base_dir: &Path) -> Vec<Self> {
|
||||
let sessions_dir = base_dir.join("sessions");
|
||||
let Ok(entries) = std::fs::read_dir(&sessions_dir) else {
|
||||
tracing::warn!(path = %sessions_dir.display(), "sessions directory not found");
|
||||
return Vec::new();
|
||||
};
|
||||
entries
|
||||
|
||||
@@ -1,14 +1,32 @@
|
||||
//! 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 `kill(pid, 0)` + `/proc/<pid>/exe` 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
|
||||
//! - `is_alive` — liveness check via `libc::kill` + `/proc` verification
|
||||
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.
|
||||
path: PathBuf,
|
||||
/// Process ID that holds (or will hold) this lock.
|
||||
pid: u32,
|
||||
}
|
||||
|
||||
@@ -47,9 +65,11 @@ impl SessionLock {
|
||||
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),
|
||||
@@ -59,8 +79,10 @@ impl SessionLock {
|
||||
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).
|
||||
|
||||
@@ -1,17 +1,39 @@
|
||||
//! In-memory conversation state: message history plus the system prompt and
|
||||
//! model parameters used to drive the LLM.
|
||||
//!
|
||||
//! # Flow
|
||||
//!
|
||||
//! [`Conversation::new`] → [`push`](Conversation::push) to add messages →
|
||||
//! [`to_api_messages`](Conversation::to_api_messages) to format for the LLM
|
||||
//! API (system prompt prepended). Persisted via [`save_conversation`](Conversation::save_conversation)
|
||||
//! and loaded via [`load_conversation`](Conversation::load_conversation).
|
||||
//! The system prompt can be hot-swapped via [`rebuild_system`](Conversation::rebuild_system).
|
||||
//!
|
||||
//! # Components
|
||||
//!
|
||||
//! - `Conversation` — message vector + session metadata + generation params
|
||||
//! - `push` / `rebuild_system` — mutation helpers
|
||||
//! - `to_api_messages` — formats messages for API consumption
|
||||
//! - `save_conversation` / `load_conversation` — filesystem persistence
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing;
|
||||
|
||||
use super::message::{ChatMessage, Role};
|
||||
|
||||
/// A single conversation's message history and generation settings.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Conversation {
|
||||
/// Ordered list of chat messages (user, assistant, tool, system).
|
||||
pub messages: Vec<ChatMessage>,
|
||||
/// System prompt prepended at request time (see `to_api_messages`).
|
||||
pub system_prompt: String,
|
||||
/// Foreign key referencing the owning session.
|
||||
pub session_id: String,
|
||||
/// Model identifier string, e.g. `"anthropic/claude-opus-4-8"`.
|
||||
pub model: String,
|
||||
/// Optional cap on output tokens.
|
||||
pub max_tokens: Option<u32>,
|
||||
/// Optional temperature (0.0 – 2.0).
|
||||
pub temperature: Option<f32>,
|
||||
}
|
||||
|
||||
@@ -78,6 +100,7 @@ impl Conversation {
|
||||
let dir = base_dir.join("sessions").join(&self.session_id);
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
let path = dir.join("conversation.json");
|
||||
tracing::debug!(session_id = %self.session_id, path = %path.display(), "saving conversation");
|
||||
zesdex_utils::write_json_atomic(&path, self, None)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -97,7 +120,8 @@ impl Conversation {
|
||||
.join("sessions")
|
||||
.join(session_id)
|
||||
.join("conversation.json");
|
||||
let data = std::fs::read_to_string(path)?;
|
||||
tracing::debug!(session_id = %session_id, path = %path.display(), "loading conversation");
|
||||
let data = std::fs::read_to_string(&path)?;
|
||||
let conv: Conversation = serde_json::from_str(&data)?;
|
||||
Ok(conv)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
//! Chat message types shared across the entity layer: `Role` and `ChatMessage`
|
||||
//! with convenience constructors.
|
||||
//! Chat message types shared across the entity layer.
|
||||
//!
|
||||
//! Provides [`Role`] (conversation participant) and [`ChatMessage`] (a single
|
||||
//! message with optional tool-call metadata). Includes convenience constructors
|
||||
//! for each role: `user`, `assistant`, `system`, `tool`/`tool_result`.
|
||||
//!
|
||||
//! # Flow
|
||||
//!
|
||||
//! Messages are constructed via the typed constructors → pushed into
|
||||
//! [`Conversation`](super::conversation::Conversation) → serialized as JSON
|
||||
//! to `conversation.json`.
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The conversation participant who authored a message.
|
||||
///
|
||||
/// Variants: `User`, `Assistant`, `System`, `Tool`. Serialized as lowercase
|
||||
/// strings (e.g. `"user"`, `"assistant"`, `"system"`, `"tool"`).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Role {
|
||||
#[serde(rename = "user")]
|
||||
@@ -37,12 +49,18 @@ impl std::fmt::Display for Role {
|
||||
/// chat-completion API structures.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChatMessage {
|
||||
/// Who sent this message (user, assistant, system, tool).
|
||||
pub role: Role,
|
||||
/// The message text content. `None` for assistant messages that only
|
||||
/// contain tool calls.
|
||||
pub content: Option<String>,
|
||||
/// Tool-call requests attached to an assistant message (OpenAI-style).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_calls: Option<Vec<super::tool_call::ToolCall>>,
|
||||
/// For tool-role messages: the `id` of the `ToolCall` being responded to.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_call_id: Option<String>,
|
||||
/// Optional function name for the tool invocation.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
//! Common entity types shared across the Zesdex application: application
|
||||
//! configuration, settings, store paths, conversations, messages, tool
|
||||
//! calls, usage stats, and SSE streaming types.
|
||||
//! Common entity types shared across the Zesdex application.
|
||||
//!
|
||||
//! Contains pure data structures for conversations, messages, tool calls,
|
||||
//! usage statistics, provider API types (chat request/response, SSE stream),
|
||||
//! and store path configuration. All types derive `Serialize`/`Deserialize`
|
||||
//! and are persisted as JSON files.
|
||||
//!
|
||||
//! # Sub-modules
|
||||
//!
|
||||
//! - [`conversation`] — Ordered conversation (vector of `ChatMessage`)
|
||||
//! - [`message`] — `ChatMessage` + `Role` enum
|
||||
//! - [`provider`] — LLM provider API types: `ChatRequest`, `ChatResponse`,
|
||||
//! `StreamEvent`, `SseParser`, `ToolDef`, etc.
|
||||
//! - [`store`] — `Store` paths for data directories
|
||||
//! - [`tool_call`] — `ToolCall` + `ToolFunction` (function-calling request)
|
||||
//! - [`tool_result`] — `ToolCallResult` (function-calling response)
|
||||
//! - [`usage`] — `UsageStats` (token counts, costs)
|
||||
|
||||
pub mod conversation;
|
||||
pub mod message;
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
//! Provider-facing DTOs: chat completion request, response, streaming types,
|
||||
//! and the SSE stream parser.
|
||||
//!
|
||||
//! # Flow
|
||||
//!
|
||||
//! 1. **Request** — [`ChatRequest`] is built with model, messages, tools,
|
||||
//! streaming options and sent to the LLM provider.
|
||||
//! 2. **Response** — Non-streaming responses arrive as [`ChatResponse`] with
|
||||
//! [`Choice`]s containing the full [`ChatMessage`](super::message::ChatMessage).
|
||||
//! 3. **Streaming** — SSE chunks are fed into [`SseParser::feed`] which yields
|
||||
//! [`StreamEvent`]s: token/text, reasoning, tool-call deltas, usage, done.
|
||||
//!
|
||||
//! # Components
|
||||
//!
|
||||
//! - `ChatRequest` / `StreamOptions` / `ToolDef` / `ToolFunctionDef` — outbound
|
||||
//! - `ChatResponse` / `Choice` / `Delta` / `TokenUsage` — non-streaming inbound
|
||||
//! - `StreamEvent` — one atomic streaming event (Token, Reasoning,
|
||||
//! ToolCallDelta, Usage, Done, Error)
|
||||
//! - `SseParser` — incremental SSE frame parser: `feed()` → `Vec<StreamEvent>`
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tracing;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Chat request / response
|
||||
@@ -11,23 +29,32 @@ use serde_json::Value;
|
||||
/// provider.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChatRequest {
|
||||
/// Model identifier, e.g. `"anthropic/claude-opus-4-8"`.
|
||||
pub model: String,
|
||||
/// Full message history (system + user + assistant + tool turns).
|
||||
pub messages: Vec<super::message::ChatMessage>,
|
||||
/// Maximum number of output tokens.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_tokens: Option<u32>,
|
||||
/// Sampling temperature (0.0 – 2.0).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub temperature: Option<f32>,
|
||||
/// Tool definitions available to the model.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<Vec<ToolDef>>,
|
||||
/// Controls which (if any) function is called by the model.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_choice: Option<Value>,
|
||||
/// Whether to use SSE streaming (`true`) or a single response.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stream: Option<bool>,
|
||||
/// Nucleus sampling threshold.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub top_p: Option<f32>,
|
||||
/// Sequences where the model should stop generation.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stop: Option<Vec<String>>,
|
||||
/// Additional streaming options (e.g. `include_usage`).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stream_options: Option<StreamOptions>,
|
||||
}
|
||||
@@ -42,38 +69,53 @@ pub struct StreamOptions {
|
||||
/// Wire format for a single tool definition sent to the provider.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolDef {
|
||||
/// The tool type discriminator, e.g. `"function"`.
|
||||
#[serde(rename = "type")]
|
||||
pub type_: String,
|
||||
/// The function definition (name, description, JSON schema).
|
||||
pub function: ToolFunctionDef,
|
||||
}
|
||||
|
||||
/// Name, description, and JSON schema parameters for a tool definition.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolFunctionDef {
|
||||
/// The function name the model may invoke.
|
||||
pub name: String,
|
||||
/// Human-readable description of what the function does.
|
||||
pub description: String,
|
||||
/// JSON Schema object describing the expected arguments.
|
||||
pub parameters: Value,
|
||||
}
|
||||
|
||||
/// Non-streaming chat completion response returned by the provider.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChatResponse {
|
||||
/// Unique response identifier from the provider.
|
||||
pub id: String,
|
||||
/// Object type, e.g. `"chat.completion"`.
|
||||
pub object: Option<String>,
|
||||
/// Model identifier that produced this response.
|
||||
pub model: String,
|
||||
/// One or more completion candidates.
|
||||
pub choices: Vec<Choice>,
|
||||
/// Token usage statistics (prompt, completion, total).
|
||||
pub usage: Option<TokenUsage>,
|
||||
/// Unix-timestamp of response creation.
|
||||
pub created: Option<i64>,
|
||||
}
|
||||
|
||||
/// One completion candidate within a `ChatResponse.choices` list.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Choice {
|
||||
/// Zero-based index of this choice in the candidate list.
|
||||
pub index: u32,
|
||||
/// Full message (non-streaming response).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message: Option<super::message::ChatMessage>,
|
||||
/// Incremental delta (streaming response).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub delta: Option<Delta>,
|
||||
/// Why the model stopped: `"stop"`, `"tool_calls"`, `"length"`, etc.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub finish_reason: Option<String>,
|
||||
}
|
||||
@@ -81,10 +123,13 @@ pub struct Choice {
|
||||
/// Incremental delta emitted in a streaming SSE chunk.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Delta {
|
||||
/// Role being set for the first streaming chunk.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub role: Option<super::message::Role>,
|
||||
/// Incremental text content delta.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<String>,
|
||||
/// Incremental tool-call delta (partial name/arguments).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_calls: Option<Vec<super::tool_call::ToolCall>>,
|
||||
}
|
||||
@@ -92,11 +137,16 @@ pub struct Delta {
|
||||
/// Token counts and optional cost breakdown for a single completion request.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct TokenUsage {
|
||||
/// Tokens consumed by the prompt (input).
|
||||
pub prompt_tokens: u32,
|
||||
/// Tokens consumed by the completion (output).
|
||||
pub completion_tokens: u32,
|
||||
/// Sum of prompt + completion tokens.
|
||||
pub total_tokens: u32,
|
||||
/// Estimated cost for prompt tokens (provider-specific).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub prompt_tokens_cost: Option<f64>,
|
||||
/// Estimated cost for completion tokens (provider-specific).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub completion_tokens_cost: Option<f64>,
|
||||
}
|
||||
@@ -108,28 +158,41 @@ pub struct TokenUsage {
|
||||
/// One atomic event extracted from an LLM streaming response stream.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum StreamEvent {
|
||||
/// An incremental text token.
|
||||
Token(String),
|
||||
/// An incremental reasoning token (Anthropic `reasoning_content`).
|
||||
Reasoning(String),
|
||||
/// An incremental tool-call delta (partial ID, name, or arguments).
|
||||
ToolCallDelta {
|
||||
/// Tool-call index (multiple calls in one response).
|
||||
index: usize,
|
||||
/// Optional tool-call ID (usually in the first delta for a call).
|
||||
id: Option<String>,
|
||||
/// Optional function name (usually in the first delta for a call).
|
||||
name: Option<String>,
|
||||
/// Partial JSON arguments delta for this tool call.
|
||||
arguments_delta: String,
|
||||
},
|
||||
/// Final usage chunk with token counts.
|
||||
Usage {
|
||||
prompt_tokens: u64,
|
||||
completion_tokens: u64,
|
||||
total_tokens: u64,
|
||||
},
|
||||
/// Stream complete (all tokens have been delivered).
|
||||
Done,
|
||||
/// A stream-level error occurred.
|
||||
Error(String),
|
||||
}
|
||||
|
||||
/// Buffered SSE frame parser that accumulates raw `data:` lines and
|
||||
/// flushes a `StreamEvent` on each blank-line boundary.
|
||||
pub struct SseParser {
|
||||
/// Leftover bytes from the last chunk that did not end with `\n`.
|
||||
buffer: String,
|
||||
/// The current `event:` type (set by `event:` lines, cleared on flush).
|
||||
event_type: Option<String>,
|
||||
/// Accumulated `data:` lines for the current event frame.
|
||||
data_lines: Vec<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
//! Filesystem layout for zesdex's persistent and scratch data directories.
|
||||
//!
|
||||
//! # Flow
|
||||
//!
|
||||
//! [`Store::new`] resolves all paths from OS data dir / temp dir →
|
||||
//! [`ensure_dirs`](Store::ensure_dirs) creates them on startup.
|
||||
//!
|
||||
//! # Components
|
||||
//!
|
||||
//! - `Store` — resolved path bundle (base, memory, scratch, images, downloads)
|
||||
//! - `new` — path computation (no I/O)
|
||||
//! - `ensure_dirs` — creates all directories if missing
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use tracing;
|
||||
|
||||
/// Resolved paths for all data directories zesdex reads from and writes to.
|
||||
///
|
||||
@@ -8,10 +20,15 @@ use std::path::PathBuf;
|
||||
/// where memory, scratch, session images, and downloads live.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Store {
|
||||
/// Top-level data directory, e.g. `~/.local/share/zesdex`.
|
||||
pub base_dir: PathBuf,
|
||||
/// Temporary scratch root, usually under the OS temp dir.
|
||||
pub scratch_root: PathBuf,
|
||||
/// Directory for persistent memory files (`.md` summaries).
|
||||
pub memory_dir: PathBuf,
|
||||
/// Directory for per-session image snapshots.
|
||||
pub session_images_dir: PathBuf,
|
||||
/// Directory for downloaded files.
|
||||
pub download_dir: PathBuf,
|
||||
}
|
||||
|
||||
@@ -41,6 +58,7 @@ impl Store {
|
||||
///
|
||||
/// Return: `Err` on the first directory that fails to create.
|
||||
pub fn ensure_dirs(&self) -> std::io::Result<()> {
|
||||
tracing::debug!(base = %self.base_dir.display(), "ensuring store directories exist");
|
||||
std::fs::create_dir_all(&self.base_dir)?;
|
||||
std::fs::create_dir_all(&self.memory_dir)?;
|
||||
std::fs::create_dir_all(&self.scratch_root)?;
|
||||
|
||||
@@ -1,25 +1,42 @@
|
||||
//! Tool-call DTOs embedded in assistant chat messages.
|
||||
//!
|
||||
//! Flow: provider response/stream carries `tool_calls` on an assistant
|
||||
//! message → deserialized into `ToolCall`/`ToolFunction` → harness resolves
|
||||
//! `function.name` against `all_tools()` and runs it with
|
||||
//! `sanitize_tool_arguments(function.arguments)`.
|
||||
//! # Flow
|
||||
//!
|
||||
//! Provider response/stream carries `tool_calls` on an assistant message →
|
||||
//! deserialized into [`ToolCall`]/[`ToolFunction`] → harness resolves the
|
||||
//! function name against `all_tools()` and runs it after sanitizing arguments
|
||||
//! via [`sanitize_tool_arguments`] (which handles string-encoded JSON,
|
||||
//! control characters, and truncation).
|
||||
//!
|
||||
//! # Components
|
||||
//!
|
||||
//! - `ToolCall` — a single tool-invocation request (id + type + function)
|
||||
//! - `ToolFunction` — function name + raw arguments Value
|
||||
//! - `sanitize_tool_arguments` — normalizes argument shape, repairs truncation
|
||||
//! - `repair_json` — closes unclosed strings/braces/brackets in truncated JSON
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tracing;
|
||||
|
||||
/// A single tool-call request emitted by the model in an assistant message.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolCall {
|
||||
/// Unique identifier for this tool call (referenced by `ToolCallResult`).
|
||||
pub id: String,
|
||||
/// Discriminator, e.g. `"function"`.
|
||||
#[serde(rename = "type")]
|
||||
pub type_: String,
|
||||
/// The function to invoke (name + arguments).
|
||||
pub function: ToolFunction,
|
||||
}
|
||||
|
||||
/// The function name and raw arguments payload for a `ToolCall`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolFunction {
|
||||
/// The function/tool name to dispatch against.
|
||||
pub name: String,
|
||||
/// Arguments as a JSON Value (may be a string-encoded object before
|
||||
/// `sanitize_tool_arguments` normalises it).
|
||||
pub arguments: Value,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
//! Record of one completed tool invocation, kept for transcript/history.
|
||||
//!
|
||||
//! # Flow
|
||||
//!
|
||||
//! Tool harness completes execution → creates [`ToolCallResult`] with output,
|
||||
//! error flag, and wall-clock duration → appended to conversation history as
|
||||
//! a `Tool`-role [`ChatMessage`](super::message::ChatMessage).
|
||||
//!
|
||||
//! # Components
|
||||
//!
|
||||
//! - `ToolCallResult` — tool name + output + error flag + duration
|
||||
//! - `new` — convenience constructor
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Record of a completed tool invocation, kept for transcript/history.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolCallResult {
|
||||
/// The `id` of the `ToolCall` this result responds to.
|
||||
pub tool_call_id: String,
|
||||
/// The name of the tool that was invoked.
|
||||
pub tool_name: String,
|
||||
/// The text output produced by the tool (or error message).
|
||||
pub output: String,
|
||||
/// Whether the tool exited with an error.
|
||||
pub is_error: bool,
|
||||
/// Wall-clock execution duration in milliseconds.
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,34 @@
|
||||
//! Token usage accounting shared by streaming and non-streaming responses.
|
||||
//!
|
||||
//! # Flow
|
||||
//!
|
||||
//! Accumulated across all LLM API calls in a session. Each response updates
|
||||
//! the running totals; `last_*` fields capture the most recent call's values
|
||||
//! for interpolation display. Persisted alongside other session metadata.
|
||||
//!
|
||||
//! # Components
|
||||
//!
|
||||
//! - `UsageStats` — cumulative token/latency counters
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Cumulative token/latency counters for a session, persisted alongside it.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
|
||||
pub struct UsageStats {
|
||||
/// Total tokens consumed as input (prompt).
|
||||
pub tokens_in: u64,
|
||||
/// Total tokens generated as output (completion).
|
||||
pub tokens_out: u64,
|
||||
/// Most recent call's input tokens (for live interpolation display).
|
||||
#[serde(default)]
|
||||
pub last_tokens_in: u64,
|
||||
/// Most recent call's output tokens (for live interpolation display).
|
||||
#[serde(default)]
|
||||
pub last_tokens_out: u64,
|
||||
/// Total number of LLM API calls made this session.
|
||||
pub api_calls: u64,
|
||||
/// Tokens consumed by auto-review subagent calls.
|
||||
pub review_tokens: u64,
|
||||
/// Total wall-clock time spent on LLM API calls (milliseconds).
|
||||
pub total_ms: u64,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
//! Domain entity modules organised by concern — pure data structures with
|
||||
//! serde serialisation and filesystem persistence (serde JSON + std::fs).
|
||||
//! Domain entity modules organised by concern.
|
||||
//!
|
||||
//! All types here are pure data structures with serde serialization
|
||||
//! and filesystem persistence (serde JSON + `std::fs`).
|
||||
//!
|
||||
//! # Sub-modules
|
||||
//!
|
||||
//! - [`auth`] — Session, SessionLock (authentication data)
|
||||
//! - [`common`] — Conversation, Message, Provider, Store, ToolCall, ToolResult, Usage
|
||||
|
||||
pub mod auth;
|
||||
pub mod common;
|
||||
|
||||
@@ -9,6 +9,18 @@
|
||||
//!
|
||||
//! This crate contains ALL domain entity types as pure data structures
|
||||
//! with no business logic beyond constructor/accessor methods.
|
||||
//!
|
||||
//! # Modules
|
||||
//!
|
||||
//! - [`domain::auth`] — Authentication entities: `Session`, `SessionLock`
|
||||
//! - [`domain::common`] — Shared domain entities: `Conversation`, `Message`,
|
||||
//! `Provider`, `Store`, `ToolCall`, `ToolResult`, `Usage`
|
||||
//!
|
||||
//! # Flow
|
||||
//!
|
||||
//! External consumers (`zesdex-iam`, `zesdex-backend`) import re-exported
|
||||
//! types via `use zesdex_entities::*`. No instantiation logic lives here —
|
||||
//! only struct/enum definitions, their fields, and lightweight constructors.
|
||||
|
||||
pub mod domain;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user