mod chat;
mod setup;
use std::io::IsTerminal;
use std::sync::Arc;
use agent_client_protocol::{
Agent, AgentCapabilities, AgentSideConnection, AuthenticateRequest, AuthenticateResponse,
CancelNotification, Client, ContentBlock, ContentChunk, Implementation, InitializeRequest,
InitializeResponse, NewSessionRequest, NewSessionResponse, PromptRequest, PromptResponse,
SessionId, SessionNotification, SessionUpdate, StopReason,
};
use futures::future::LocalBoxFuture;
use onde::inference::{ChatEngine, GgufModelConfig};
use tokio::sync::{Mutex, mpsc};
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
const SYSTEM_PROMPT: &str = "\
Your name is siGit — spelled exactly that way: lowercase 's', uppercase 'G', \
no spaces. Never write it as 'SiGit', 'Sigit', or any other variation. \
When introducing yourself, say 'I am siGit'.
You are an AI coding agent built into the editor via the Agent Client Protocol. \
You help with:
- Code analysis, writing, and refactoring
- Bug hunting and debugging
- Git workflows and commit messages
- Software architecture and design patterns
- Code review
Be direct and brief. Write clean, idiomatic code. When debugging, go for the \
root cause, not the symptom. Correct beats clever.";
struct Session {
id: SessionId,
}
struct SiGitAgent {
engine: Arc<ChatEngine>,
active_session: Arc<Mutex<Option<Session>>>,
notification_tx: mpsc::Sender<SessionNotification>,
}
impl SiGitAgent {
fn new(notification_tx: mpsc::Sender<SessionNotification>) -> Self {
Self {
engine: Arc::new(ChatEngine::new()),
active_session: Arc::new(Mutex::new(None)),
notification_tx,
}
}
}
#[async_trait::async_trait(?Send)]
impl Agent for SiGitAgent {
async fn initialize(
&self,
args: InitializeRequest,
) -> agent_client_protocol::Result<InitializeResponse> {
log::info!("initialize: protocol_version={}", args.protocol_version);
Ok(InitializeResponse::new(args.protocol_version)
.agent_info(
Implementation::new("sigit", env!("CARGO_PKG_VERSION"))
.title("siGit — AI Coding Agent"),
)
.agent_capabilities(AgentCapabilities::default()))
}
async fn authenticate(
&self,
_args: AuthenticateRequest,
) -> agent_client_protocol::Result<AuthenticateResponse> {
Ok(AuthenticateResponse::default())
}
async fn new_session(
&self,
_args: NewSessionRequest,
) -> agent_client_protocol::Result<NewSessionResponse> {
let session_id = SessionId::new(uuid::Uuid::new_v4().to_string());
log::info!("new_session: id={session_id}");
if self.engine.is_loaded().await {
log::info!("model already loaded — clearing history for new session");
self.engine.clear_history().await;
} else {
log::info!("loading default model (this may take a minute on first run)...");
let config = GgufModelConfig::platform_default();
self.engine
.load_gguf_model(config, Some(SYSTEM_PROMPT.to_string()), None)
.await
.map_err(|e| {
log::error!("model load failed: {e}");
agent_client_protocol::Error::new(-32603, format!("model load failed: {e}"))
})?;
log::info!("model loaded and ready");
}
let mut active = self.active_session.lock().await;
*active = Some(Session {
id: session_id.clone(),
});
Ok(NewSessionResponse::new(session_id))
}
async fn prompt(&self, args: PromptRequest) -> agent_client_protocol::Result<PromptResponse> {
let session_id = args.session_id.clone();
{
let active = self.active_session.lock().await;
match active.as_ref() {
Some(s) if s.id == session_id => {}
_ => {
return Err(agent_client_protocol::Error::invalid_params());
}
}
}
let user_text: String = args
.prompt
.iter()
.filter_map(|block| match block {
ContentBlock::Text(t) => Some(t.text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n");
if user_text.trim().is_empty() {
return Ok(PromptResponse::new(StopReason::EndTurn));
}
log::info!(
"prompt({}): \"{}\"",
session_id,
user_text.chars().take(80).collect::<String>()
);
let mut rx = self
.engine
.stream_message(user_text)
.await
.map_err(|e| agent_client_protocol::Error::new(-32603, e.to_string()))?;
while let Some(chunk) = rx.recv().await {
if !chunk.delta.is_empty() {
let notification = SessionNotification::new(
session_id.clone(),
SessionUpdate::AgentMessageChunk(ContentChunk::new(ContentBlock::from(
chunk.delta,
))),
);
if self.notification_tx.send(notification).await.is_err() {
log::warn!("notification channel closed — stopping stream");
break;
}
}
if chunk.done {
break;
}
}
log::info!("prompt({}) complete", session_id);
Ok(PromptResponse::new(StopReason::EndTurn))
}
async fn cancel(&self, args: CancelNotification) -> agent_client_protocol::Result<()> {
log::info!("cancel requested for session {}", args.session_id);
Ok(())
}
}
fn print_banner() {
const BANNER: &str = r#"
77777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777
77777777322222222222222222222222222222223777389969902208431358831999699051111177777777777777
1111111125555555555555555555555511113222311159 5002 088 3081771691111111111111
1111111111111111111111111111131136841 1482853332007 05 9043332891 400811111111111
1111111111111111111111111111111201 109 304 40 00 79 100041111111111
333333255555555555555555555552392 102 503 90 7000000005 903 0000023333333333
333333245454545454545454545433381 7600000 302 61 780 109 20009533333333333
3333333333333333333333333333333402 7001 08 761 202 902 90003333333333333
2222255555555555555555555555250899901 49 304 403 08 108 300042222222222222
2222222222222222222222222222269 106 03 901 06 505 402 000052222222222222
2222255555555555555555555555299 708 1002 80 00 90852222222222222
55555555555555555555555555555560953258000866660000051140866908666600008966900065555555555555
88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888
siGit Code v%VERSION%
"#;
let art = BANNER.replace("%VERSION%", env!("CARGO_PKG_VERSION"));
eprintln!("{art}");
}
async fn run_interactive() -> anyhow::Result<()> {
println!(" Loading model...");
let engine = ChatEngine::new();
let config = GgufModelConfig::platform_default();
engine
.load_gguf_model(config, Some(SYSTEM_PROMPT.to_string()), None)
.await
.map_err(|e| anyhow::anyhow!("model load failed: {e}"))?;
let info = engine.info().await;
println!(
" \x1b[32m✓\x1b[0m {} ({})\n",
info.model_name.as_deref().unwrap_or("unknown"),
info.approx_memory.as_deref().unwrap_or("?"),
);
chat::run(&engine).await
}
async fn run_acp_server() -> anyhow::Result<()> {
let (notification_tx, mut notification_rx) = mpsc::channel::<SessionNotification>(256);
let agent = SiGitAgent::new(notification_tx);
let stdin = tokio::io::stdin().compat();
let stdout = tokio::io::stdout().compat_write();
let local = tokio::task::LocalSet::new();
local
.run_until(async move {
let (conn, io_task) = AgentSideConnection::new(
agent,
stdout,
stdin,
|fut: LocalBoxFuture<'static, ()>| {
tokio::task::spawn_local(fut);
},
);
tokio::task::spawn_local(async move {
while let Some(notification) = notification_rx.recv().await {
if let Err(err) = conn.session_notification(notification).await {
log::warn!("session_notification failed: {err}");
}
}
});
if let Err(err) = io_task.await {
log::error!("ACP IO error: {err}");
}
})
.await;
Ok(())
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
.target(env_logger::Target::Stderr)
.init();
setup::setup_shared_model_cache();
if std::io::stdin().is_terminal() {
print_banner();
run_interactive().await
} else {
log::info!("siGit v{} starting (ACP mode)", env!("CARGO_PKG_VERSION"));
run_acp_server().await
}
}