refactor: massive codebase restructuring — naming, splitting, DRY

Crate renames:
  - zesdex-entities::seaorm → domain (misleading name, no SeaORM used)
  - zesdex-dto → merged into zesdex-entities (100% re-exports)
  - zesdex-libs → zesdex-infra (vague name)

Module renames:
  - app/harness → guard (misleading: safety gatekeeper, not test harness)
  - runtime/commands → action_dispatch (name clashed with controller/command)
  - resources → prompts (embedded prompt text, not general resources)
  - tool/seqthink → sequential_think (unreadable abbreviation)
  - msglog/query → insert (module only inserts, never queries)

Dead code removal:
  - app/mode/help.rs (orphaned — not declared in mod.rs)
  - app/mode/loading.rs (orphaned — not declared in mod.rs)

File splitting (71 new files, avg ~115 lines/file):
  - app/runtime/actions/: 1→8 files (was 2030 lines)
  - view/overlays/: 1→16 files (was 1167 lines)
  - tool/lsp/: 1→8 per-tool files (was 909 lines)
  - main.rs: 1→5 files (session, daemon, attach, event_loop)
  - workflow/engine + hive_mind: 2→10 files
  - subagent/engine + auto: 2→9 files
  - lsp/provisioner: 1→5 files
  - review/: 1→6 files
  - guard/: 1→2 files (extracted patterns)
  - state/misc: 1→3 files (input, scroll)
  - mcp/: 1→3 files (transport, adapter)
  - stream/json_repair extracted from turn.rs

DRY:
  - Pattern constants (STUB_PATTERNS etc) in guard/patterns shared with subagent
  - 3 near-identical background spawners → 1 generic + thin wrappers
  - Shared spawn_subagent_with_drain() extracted
  - Shared create_session() in main
  - write_osc52 deduplicated

Bug fixes:
  - archive_message(): sess.db → db (wrong variable name)
  - execute_one_tool(): wrong parameter name
  - check_credential_read() function was missing (restored from test expectations)
This commit is contained in:
asepharyana
2026-07-17 09:08:41 +07:00
parent 1f0ae9f551
commit 9a67137954
139 changed files with 9704 additions and 8858 deletions
@@ -0,0 +1,147 @@
#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_wrap
)]
//! 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)`.
use serde::{Deserialize, Serialize};
use serde_json::Value;
/// A single tool-call request emitted by the model in an assistant message.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
#[serde(rename = "type")]
pub type_: String,
pub function: ToolFunction,
}
/// The function name and raw arguments payload for a `ToolCall`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolFunction {
pub name: String,
pub arguments: Value,
}
/// Normalize tool-call arguments into a JSON object/value.
///
/// Flow: some providers send `arguments` as a JSON-encoded string rather
/// than a nested object; if `args` is a string, attempt to parse it as
/// JSON. Objects and other value types pass through unchanged.
///
/// Security: on parse failure we wrap the raw string in `{ "_raw": "..." }`
/// instead of passing it through as a raw string, so tools that expect a
/// JSON object (via `args.get("key")`) get `None` rather than unexpectedly
/// receiving a plain string value.
///
/// Attempt to fix truncated JSON by closing open strings, braces and brackets.
pub fn sanitize_tool_arguments(args: &Value) -> Value {
match args {
Value::String(s) => {
// Attempt 1: direct parse.
if let Ok(v) = serde_json::from_str::<Value>(s) {
return v;
}
// Attempt 2: strip control chars (0x00-0x1F except \t, \n)
let cleaned: String = s
.chars()
.filter(|&c| !c.is_control() || c == '\t' || c == '\n' || c == '\r')
.collect();
if cleaned.len() != s.len() {
if let Ok(v) = serde_json::from_str::<Value>(&cleaned) {
tracing::warn!(
"tool argument contained control characters — stripped \
and reparsed successfully",
);
return v;
}
}
// Attempt 3: repair truncated JSON and retry.
let input = if cleaned.len() == s.len() {
s
} else {
&cleaned
};
let repaired = repair_json(input);
match serde_json::from_str::<Value>(&repaired) {
Ok(v) => {
tracing::warn!("tool argument string was truncated — repaired successfully",);
v
}
Err(e2) => {
tracing::error!(
"tool argument is a JSON string but failed to parse. \
Wrapping in object. Error: {}. Raw (first 200): {}",
e2,
s.chars().take(200).collect::<String>(),
);
serde_json::json!({"_raw": s, "_parse_error": e2.to_string()})
}
}
}
obj @ Value::Object(_) => obj.clone(),
other => other.clone(),
}
}
/// Repair truncated JSON by closing open strings, braces and brackets.
///
/// Flow: single-pass character scan tracking string/escape state with a
/// LIFO stack for `{`/`[` → append missing `"`, `]`, `}` in the right
/// (reverse nesting) order.
pub fn repair_json(s: &str) -> String {
let mut stack: Vec<char> = Vec::new();
let mut in_string = false;
let mut prev_was_backslash = false;
let mut ends_with_unclosed_escape = false;
for c in s.chars() {
if prev_was_backslash {
prev_was_backslash = false;
ends_with_unclosed_escape = false;
continue;
}
if c == '\\' && in_string {
prev_was_backslash = true;
ends_with_unclosed_escape = true;
continue;
}
ends_with_unclosed_escape = false;
if c == '"' {
in_string = !in_string;
continue;
}
if in_string {
continue;
}
match c {
'{' | '[' => stack.push(c),
'}' | ']' => {
stack.pop();
}
_ => {}
}
}
let mut result = s.to_string();
if ends_with_unclosed_escape {
result.pop();
}
if in_string {
result.push('"');
}
for &opener in stack.iter().rev() {
match opener {
'{' => result.push('}'),
'[' => result.push(']'),
_ => {}
}
}
result
}