feat(helexa-acp): wire ACP agent loop for text-only conversations
Some checks failed
build-prerelease / Package helexa-neuron-ada RPM (push) Blocked by required conditions
build-prerelease / Package helexa-neuron-ampere RPM (push) Blocked by required conditions
build-prerelease / Package helexa-neuron-blackwell RPM (push) Blocked by required conditions
build-prerelease / Resolve version stamps (push) Successful in 41s
CI / Format (push) Successful in 38s
CI / Clippy (push) Successful in 2m35s
build-prerelease / Build cortex binary (push) Successful in 5m26s
CI / Test (push) Successful in 5m43s
build-prerelease / Build neuron-blackwell (push) Successful in 5m47s
CI / Build cortex SRPM (push) Has been skipped
CI / Build neuron SRPM (push) Has been skipped
CI / Publish cortex to COPR (push) Has been skipped
CI / Publish neuron to COPR (push) Has been skipped
CI / Bump version in source (push) Has been skipped
build-prerelease / Package cortex RPM (push) Successful in 1m23s
build-prerelease / Build neuron-ampere (push) Successful in 8m13s
build-prerelease / Build neuron-ada (push) Successful in 5m28s
build-prerelease / Publish to rpm.lair.cafe (unstable) (push) Has been cancelled

Stage 2 lands the agent loop on top of the Stage 1 scaffold: session
state with per-session cancellation, a system-prompt builder honouring
HELEXA_ACP_SYSTEM_PROMPT_PATH / system_prompt_path TOML, and handlers
for initialize / session/new / session/prompt / session/cancel that
stream provider output back as session/update notifications. Verified
end-to-end against cortex from Zed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 09:46:22 +03:00
parent e267f583e1
commit 96fc379893
7 changed files with 883 additions and 55 deletions

View File

@@ -2,23 +2,26 @@
//! setups (helexa, LM Studio, Ollama, OpenRouter, OpenAI, Anthropic,
//! …) with a clean per-endpoint wire-format selector.
//!
//! Speaks ACP over stdio to an editor client (Zed today). The
//! conversation is forwarded to one of the configured endpoints via
//! a wire-format-specific [`provider::Provider`] implementation.
//! The agent loop itself is provider-agnostic adding e.g. an
//! Anthropic /v1/messages provider doesn't touch `agent.rs`.
//! Speaks ACP over stdio to an editor client (Zed today). Every
//! configured endpoint produces a wire-format-specific
//! [`provider::Provider`] implementation; the agent loop in
//! [`agent::Agent`] is provider-agnostic, so adding e.g. an Anthropic
//! /v1/messages provider doesn't touch `agent.rs`.
//!
//! Config: `$XDG_CONFIG_HOME/helexa-acp/config.toml` for the multi-
//! endpoint case; env vars (`HELEXA_ACP_BASE_URL`, etc.) for the
//! single-endpoint case when no config file exists.
use agent_client_protocol::schema::{AgentCapabilities, InitializeRequest, InitializeResponse};
use agent_client_protocol::{Agent, Client, ConnectionTo, Dispatch, Result, Stdio};
use agent_client_protocol::{Result, Stdio};
use std::sync::Arc;
mod agent;
mod config;
mod prompt;
mod provider;
mod session;
use agent::Agent;
use config::{Config, EndpointConfig, WireApi};
use provider::{Provider, openai_chat::OpenAIChatProvider};
@@ -86,36 +89,8 @@ async fn main() -> Result<()> {
}
}
}
if providers.is_empty() {
return Err(agent_client_protocol::util::internal_error(
"no usable endpoints — check config",
));
}
Agent
.builder()
.name("helexa-acp")
.on_receive_request(
async move |initialize: InitializeRequest, responder, _connection| {
// Phase 1 wiring — capabilities only. Real session
// handling lands in the next iteration (agent.rs).
responder.respond(
InitializeResponse::new(initialize.protocol_version)
.agent_capabilities(AgentCapabilities::new()),
)
},
agent_client_protocol::on_receive_request!(),
)
.on_receive_dispatch(
async move |message: Dispatch, cx: ConnectionTo<Client>| {
tracing::warn!(method = ?message.method(), "unhandled ACP message");
message.respond_with_error(
agent_client_protocol::util::internal_error("not implemented yet"),
cx,
)
},
agent_client_protocol::on_receive_dispatch!(),
)
.connect_to(Stdio::new())
.await
let agent = Agent::new(&cfg, providers)
.map_err(|e| agent_client_protocol::util::internal_error(format!("agent: {e:#}")))?;
agent.serve(Stdio::new()).await
}