Support Linux refactoring checks

This commit is contained in:
Hermes Agent
2026-07-29 10:21:56 +00:00
parent 36f1056cf7
commit 902037f947
9 changed files with 253 additions and 100 deletions

View File

@@ -1173,7 +1173,7 @@ pub(crate) fn parse_tool_calls(
let (content, calls) = if model == ModelChoice::Glm52 {
parse_glm_calls(text)?
} else {
crate::server::parse_dsml_tool_calls(text)?
crate::dsml::parse_tool_calls(text)?
};
calls
.into_iter()

View File

@@ -19,7 +19,7 @@ use crate::config::{
GitDiffWhitespace,
};
use crate::database::{Database, ProjectWithSessions, SessionState, StoredMessage};
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", test))]
use crate::engine::ChatTurn;
use crate::metrics::{KvCacheReport, Metrics, MetricsSnapshot};
use crate::model::{self, DownloadOutcome, DownloadProgress, ManagedArtifactId, ModelChoice};
@@ -36,9 +36,11 @@ use rfd::AsyncFileDialog;
use std::collections::{HashMap, HashSet, VecDeque};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::mpsc::{self, TryRecvError};
use std::sync::{Arc, Mutex, RwLock};
#[cfg(target_os = "macos")]
use std::sync::{Mutex, RwLock};
use std::thread;
use std::time::{Duration, Instant};
@@ -595,6 +597,7 @@ impl App {
active_generation: None,
#[cfg(target_os = "macos")]
active_compaction: None,
#[cfg(target_os = "macos")]
active_tool_check: None,
#[cfg(target_os = "macos")]
agent_tools: None,
@@ -737,6 +740,7 @@ impl App {
active_generation: None,
#[cfg(target_os = "macos")]
active_compaction: None,
#[cfg(target_os = "macos")]
active_tool_check: None,
#[cfg(target_os = "macos")]
agent_tools: None,
@@ -1567,8 +1571,11 @@ impl App {
self.error = Some(format!("Could not play media: {error}"));
}
#[cfg(not(target_os = "macos"))]
if let Err(error) = std::process::Command::new("open").arg(url).spawn() {
self.error = Some(format!("Could not open media: {error}"));
{
let _ = (title, video);
if let Err(error) = std::process::Command::new("open").arg(url).spawn() {
self.error = Some(format!("Could not open media: {error}"));
}
}
}
Message::RequestA2uiDismiss(surface_id) => {
@@ -1788,8 +1795,10 @@ impl App {
if let Some(database) = &mut self.database {
match database.delete_project(project_id) {
Ok(()) => {
#[cfg(not(target_os = "macos"))]
let _ = checkpoint_ids;
#[cfg(target_os = "macos")]
for session_id in checkpoint_ids {
#[cfg(target_os = "macos")]
self.background_chats.remove(&session_id);
}
self.drafts.remove(&project_id);

View File

@@ -293,7 +293,7 @@ fn sync_a2ui_message(
renderable_surface_updated
}
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", test))]
fn chat_turn(message: &ChatMessage) -> ChatTurn {
ChatTurn {
user: message.user,
@@ -368,7 +368,7 @@ fn project_agents(path: &Path) -> Result<Option<String>, String> {
}
}
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", test))]
fn title_context(messages: impl IntoIterator<Item = ChatTurn>) -> Vec<ChatTurn> {
let mut started = false;
messages
@@ -497,6 +497,8 @@ impl App {
#[cfg(target_os = "macos")]
let agents =
agents_prompt_for_turn(opening_turn, opening_agents, self.session_agents_prompt());
#[cfg(not(target_os = "macos"))]
let agents = None::<String>;
self.a2ui_auto_switch_pending = true;
self.context_notice = None;
#[cfg(target_os = "macos")]
@@ -535,6 +537,7 @@ impl App {
&effective.turn.system_prompt,
self.compaction_summary(),
);
#[cfg(target_os = "macos")]
let assistant_reasoning = effective.turn.reasoning_mode != ReasoningMode::Direct;
#[cfg(target_os = "macos")]
let model_prompt = if self.config.a2ui_enabled {
@@ -700,7 +703,6 @@ impl App {
{
let _ = effective;
self.error = Some("Local Metal generation requires macOS.".into());
return;
}
}

View File

@@ -1,4 +1,5 @@
use super::*;
use std::sync::RwLock;
#[derive(Clone)]
pub(super) struct PreferenceDraft {
@@ -437,6 +438,7 @@ impl App {
return;
}
}
#[cfg(target_os = "macos")]
let dev_brain_changed = self.config.dev_brain != config.dev_brain;
#[cfg(target_os = "macos")]
let endpoint_changed = self.config.endpoint != config.endpoint;
@@ -447,6 +449,7 @@ impl App {
{
self._endpoint = None;
}
#[cfg(target_os = "macos")]
let pending_endpoint =
if config.endpoint.enabled && (endpoint_changed || self._endpoint.is_none()) {
let Some(generation) = &self.generation_service else {

210
src/dsml.rs Normal file
View File

@@ -0,0 +1,210 @@
use serde_json::{Map, Value};
#[derive(Clone, Copy)]
pub(crate) struct Syntax {
pub(crate) tool_start: &'static str,
pub(crate) tool_end: &'static str,
pub(crate) invoke_start: &'static str,
pub(crate) invoke_end: &'static str,
pub(crate) parameter_start: &'static str,
pub(crate) parameter_end: &'static str,
}
pub(crate) const SYNTAXES: [Syntax; 3] = [
Syntax {
tool_start: "<DSMLtool_calls>",
tool_end: "</DSMLtool_calls>",
invoke_start: "<DSMLinvoke",
invoke_end: "</DSMLinvoke>",
parameter_start: "<DSMLparameter",
parameter_end: "</DSMLparameter>",
},
Syntax {
tool_start: "<DSMLtool_calls>",
tool_end: "</DSMLtool_calls>",
invoke_start: "<DSMLinvoke",
invoke_end: "</DSMLinvoke>",
parameter_start: "<DSMLparameter",
parameter_end: "</DSMLparameter>",
},
Syntax {
tool_start: "<tool_calls>",
tool_end: "</tool_calls>",
invoke_start: "<invoke",
invoke_end: "</invoke>",
parameter_start: "<parameter",
parameter_end: "</parameter>",
},
];
pub(crate) fn parse_tool_calls(text: &str) -> Result<(String, Vec<(String, Value)>), String> {
let Some((start, syntax)) = SYNTAXES
.iter()
.filter_map(|syntax| text.find(syntax.tool_start).map(|start| (start, *syntax)))
.min_by_key(|(start, _)| *start)
else {
return Ok((text.to_owned(), Vec::new()));
};
let Some(relative_end) = text[start..].find(syntax.tool_end) else {
return Err("invalid or incomplete DSML tool call".into());
};
let end = start + relative_end + syntax.tool_end.len();
let content = text[..start].trim_end().to_owned();
let raw = &text[start..end];
let mut cursor = syntax.tool_start.len();
let mut calls = Vec::new();
loop {
skip_whitespace(raw, &mut cursor);
if raw[cursor..].starts_with(syntax.tool_end) {
break;
}
if !raw[cursor..].starts_with(syntax.invoke_start) {
return Err("invalid or incomplete DSML tool call".into());
}
let tag_end = raw[cursor..]
.find('>')
.map(|offset| cursor + offset + 1)
.ok_or_else(|| "invalid or incomplete DSML tool call".to_owned())?;
let name = attribute(&raw[cursor..tag_end], "name")
.ok_or_else(|| "invalid or incomplete DSML tool call".to_owned())?;
cursor = tag_end;
let mut arguments = Map::new();
loop {
skip_whitespace(raw, &mut cursor);
if raw[cursor..].starts_with(syntax.invoke_end) {
cursor += syntax.invoke_end.len();
break;
}
let (name, value) = parse_parameter(raw, &mut cursor, syntax)?
.ok_or_else(|| "invalid or incomplete DSML tool call".to_owned())?;
arguments.insert(name, value);
}
calls.push((name, Value::Object(arguments)));
}
if calls.is_empty() {
return Err("invalid or incomplete DSML tool call".into());
}
Ok((content, calls))
}
fn parse_parameter(
text: &str,
cursor: &mut usize,
syntax: Syntax,
) -> Result<Option<(String, Value)>, String> {
if !text[*cursor..].starts_with(syntax.parameter_start) {
return Ok(None);
}
let Some(tag_end) = text[*cursor..].find('>').map(|end| *cursor + end + 1) else {
return Ok(None);
};
let tag = &text[*cursor..tag_end];
let Some(name) = attribute(tag, "name") else {
return Ok(None);
};
let is_string = attribute(tag, "string");
*cursor = tag_end;
let mut nested_start = *cursor;
skip_whitespace(text, &mut nested_start);
if is_string.is_none() && text[nested_start..].starts_with(syntax.parameter_start) {
*cursor = nested_start;
let mut nested = Map::new();
loop {
skip_whitespace(text, cursor);
if !text[*cursor..].starts_with(syntax.parameter_start) {
break;
}
let Some((name, value)) = parse_parameter(text, cursor, syntax)? else {
return Ok(None);
};
nested.insert(name, value);
}
skip_whitespace(text, cursor);
if !text[*cursor..].starts_with(syntax.parameter_end) {
return Ok(None);
}
*cursor += syntax.parameter_end.len();
return Ok(Some((name, Value::Object(nested))));
}
let Some(value_end) = text[*cursor..]
.find(syntax.parameter_end)
.map(|end| *cursor + end)
else {
return Ok(None);
};
let raw = &text[*cursor..value_end];
*cursor = value_end + syntax.parameter_end.len();
let value = if is_string.as_deref().unwrap_or("true") == "true" {
Value::String(unescape(raw))
} else {
serde_json::from_str(raw)
.map_err(|error| format!("invalid DSML tool arguments: {error}"))?
};
Ok(Some((name, value)))
}
fn attribute(tag: &str, name: &str) -> Option<String> {
let start = tag.find(&format!("{name}=\""))? + name.len() + 2;
let end = start + tag[start..].find('"')?;
Some(unescape(&tag[start..end]))
}
fn skip_whitespace(text: &str, cursor: &mut usize) {
while text[*cursor..]
.chars()
.next()
.is_some_and(char::is_whitespace)
{
*cursor += text[*cursor..].chars().next().unwrap().len_utf8();
}
}
fn unescape(text: &str) -> String {
text.replace("&quot;", "\"")
.replace("&gt;", ">")
.replace("&lt;", "<")
.replace("&amp;", "&")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_reference_and_legacy_dsml_syntaxes() {
for (start, end, invoke, invoke_end, parameter, parameter_end) in [
(
"<DSMLtool_calls>",
"</DSMLtool_calls>",
"<DSMLinvoke",
"</DSMLinvoke>",
"<DSMLparameter",
"</DSMLparameter>",
),
(
"<DSMLtool_calls>",
"</DSMLtool_calls>",
"<DSMLinvoke",
"</DSMLinvoke>",
"<DSMLparameter",
"</DSMLparameter>",
),
(
"<tool_calls>",
"</tool_calls>",
"<invoke",
"</invoke>",
"<parameter",
"</parameter>",
),
] {
let raw = format!(
"done{start}{invoke} name=\"read\">{parameter} name=\"path\" string=\"true\">src/main.rs{parameter_end}{invoke_end}{end}"
);
let (content, calls) = parse_tool_calls(&raw).unwrap();
assert_eq!(content, "done");
assert_eq!(calls[0].0, "read");
assert_eq!(calls[0].1["path"], "src/main.rs");
}
}
}

View File

@@ -1,5 +1,5 @@
mod gguf;
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", test))]
mod kvstore;
#[cfg(target_os = "macos")]
mod metal;
@@ -9,6 +9,7 @@ mod validation;
#[cfg(target_os = "macos")]
use crate::metrics::{KvLookup, Metrics};
use crate::model::ModelChoice;
#[cfg(target_os = "macos")]
use crate::settings::TurnSettings;
use crate::settings::{EngineSettings, ReasoningMode};
use gguf::{F16, F32, Gguf, I32, IQ2_XXS, Q2_K, Q4_0, Q4_K, Q5_K, Q6_K, Q8_0, Tensor, Value};
@@ -17,9 +18,12 @@ use kvstore::{KvStore, StoreReason};
use sha2::{Digest, Sha256};
#[cfg(target_os = "macos")]
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::path::Path;
#[cfg(target_os = "macos")]
use std::path::PathBuf;
#[cfg(target_os = "macos")]
use std::sync::Arc;
#[cfg(target_os = "macos")]
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(target_os = "macos")]
use std::time::Instant;
@@ -1386,7 +1390,7 @@ impl Generator {
}
}
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", test))]
fn append_generated_bytes(
generated: &mut ChatTurn,
reasoning: bool,
@@ -1449,7 +1453,7 @@ fn flush_generated(
);
}
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", test))]
fn emit_safe_text(
text: &mut String,
emitted: &mut usize,
@@ -1494,7 +1498,7 @@ fn emit_safe_text(
false
}
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", test))]
fn conversation_key(system: &str, reasoning: ReasoningMode, messages: &[ChatTurn]) -> Vec<u8> {
fn text(output: &mut Vec<u8>, value: &str) {
output.extend_from_slice(&(value.len() as u64).to_le_bytes());
@@ -1526,12 +1530,12 @@ fn conversation_key(system: &str, reasoning: ReasoningMode, messages: &[ChatTurn
output
}
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", test))]
fn conversation_tag(system: &str, reasoning: ReasoningMode, messages: &[ChatTurn]) -> [u8; 32] {
Sha256::digest(conversation_key(system, reasoning, messages)).into()
}
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", test))]
fn checkpoint_matches_prefix(
checkpoint: [u8; 32],
system: &str,
@@ -1555,7 +1559,7 @@ fn resident_key(directory: &Path, tag: [u8; 32]) -> PathBuf {
directory.join("resident").join(name)
}
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", test))]
fn sample(
logits: &[f32],
temperature: f32,
@@ -1631,10 +1635,10 @@ fn sample(
probabilities.last().map_or(0, |(token, _)| *token as i32)
}
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", test))]
struct Rng(u64);
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", test))]
impl Rng {
fn new(seed: u64) -> Self {
Self(seed.max(1))
@@ -1654,7 +1658,7 @@ impl Rng {
}
}
#[cfg(all(test, target_os = "macos"))]
#[cfg(test)]
mod sampling_tests {
use super::*;
@@ -1835,6 +1839,7 @@ mod sampling_tests {
}
#[test]
#[cfg(target_os = "macos")]
#[ignore = "requires the 80 GiB Flash checkpoint and Apple Metal"]
fn metal_executes_real_flash_token() {
configure_metal_sources().unwrap();

View File

@@ -1,3 +1,5 @@
#![cfg_attr(not(target_os = "macos"), allow(dead_code))]
mod a2ui;
mod a2ui_validation;
mod agent;
@@ -6,6 +8,7 @@ mod compaction;
mod config;
mod database;
mod dev_brain;
mod dsml;
mod engine;
mod metrics;
mod model;

View File

@@ -312,49 +312,6 @@ impl Drop for ConnectionSlot {
}
}
pub(crate) fn parse_dsml_tool_calls(text: &str) -> Result<(String, Vec<(String, Value)>), String> {
let has_tool_marker = TOOL_SYNTAXES
.iter()
.any(|syntax| text.contains(syntax.tool_start));
let mut projector = ToolProjector::new();
let events = projector.push(text, true, "");
let mut content = String::new();
let mut calls = Vec::<(String, String, bool)>::new();
for event in events {
match event {
ToolProjectionEvent::Text(text) => content.push_str(&text),
ToolProjectionEvent::Start { index, name, .. } => {
if calls.len() == index {
calls.push((name, String::new(), false));
}
}
ToolProjectionEvent::Arguments { index, fragment } => {
if let Some((_, arguments, _)) = calls.get_mut(index) {
arguments.push_str(&fragment);
}
}
ToolProjectionEvent::End { index } => {
if let Some((_, _, complete)) = calls.get_mut(index) {
*complete = true;
}
}
}
}
let calls = calls
.into_iter()
.filter(|(_, _, complete)| *complete)
.map(|(name, arguments, _)| {
serde_json::from_str(&arguments)
.map(|arguments| (name, arguments))
.map_err(|error| format!("invalid DSML tool arguments: {error}"))
})
.collect::<Result<Vec<_>, _>>()?;
if has_tool_marker && calls.is_empty() {
return Err("invalid or incomplete DSML tool call".into());
}
Ok((content, calls))
}
fn handle(mut stream: TcpStream, state: &State) {
let _ = stream.set_write_timeout(Some(HTTP_IO_TIMEOUT));
let started = Instant::now();

View File

@@ -1,4 +1,5 @@
use super::*;
pub(super) use crate::dsml::{SYNTAXES as TOOL_SYNTAXES, Syntax as ToolSyntax};
const TOOL_MEMORY_FILE: &str = "tool-replay.json";
const TOOL_MEMORY_MAX_IDS: usize = 100_000;
@@ -698,43 +699,6 @@ fn repair_generated_tools(text: &str) -> Option<String> {
Some(repaired)
}
#[derive(Clone, Copy)]
pub(super) struct ToolSyntax {
pub(super) tool_start: &'static str,
pub(super) tool_end: &'static str,
pub(super) invoke_start: &'static str,
pub(super) invoke_end: &'static str,
pub(super) parameter_start: &'static str,
pub(super) parameter_end: &'static str,
}
pub(super) const TOOL_SYNTAXES: [ToolSyntax; 3] = [
ToolSyntax {
tool_start: "<DSMLtool_calls>",
tool_end: "</DSMLtool_calls>",
invoke_start: "<DSMLinvoke",
invoke_end: "</DSMLinvoke>",
parameter_start: "<DSMLparameter",
parameter_end: "</DSMLparameter>",
},
ToolSyntax {
tool_start: "<DSMLtool_calls>",
tool_end: "</DSMLtool_calls>",
invoke_start: "<DSMLinvoke",
invoke_end: "</DSMLinvoke>",
parameter_start: "<DSMLparameter",
parameter_end: "</DSMLparameter>",
},
ToolSyntax {
tool_start: "<tool_calls>",
tool_end: "</tool_calls>",
invoke_start: "<invoke",
invoke_end: "</invoke>",
parameter_start: "<parameter",
parameter_end: "</parameter>",
},
];
fn parse_tool_parameter(
text: &str,
cursor: &mut usize,