forked from kent/consciousness
salience: add gRPC client + TLS plumbing for stateful vllm sessions
Adds the client-side of a stateful gRPC protocol against vllm, plus the TLS trust machinery so we can talk to self-signed vllm servers. Protocol (proto/salience.proto): Bidi-streaming Session RPC carries OpenSession / AppendTokens / Generate / Cancel from client and SessionReady / PrefillProgress / Token / GenerateDone / Error from server. Separate Fork unary RPC for cheap branching (prefix cache shares KV automatically). Plus ListSessions, CloseSession, GetReadoutManifest admin RPCs. Per-token readouts ship as packed f32 ([n_layers * n_concepts] per token, flat). Logprobs use range-selected positions plus a top-k parameter — empty ranges means no logprobs, any range means emit sampled-token logprob at those positions, top_k > 0 adds alternatives. Client (src/agent/api/salience.rs): Tonic-generated types under pb::, a connect() helper, with_auth() for bearer metadata, and a Session handle wrapping the bidi stream: open() handshakes SessionReady; append() is fire-and-forget; generate() returns impl Stream<Item = Event> that drains inbound until Done or terminating Error. One generate at a time per session. Peak picker (src/agent/salience.rs): Pure function over ReadoutEntry traces. Per-concept z-score against trace global stats; contiguous above-threshold regions emit one peak at the local max. Configurable sigma threshold and min-std safety floor. Deterministic tie-break on offset then concept name. 12 unit tests covering empty traces, flat channels, single/multi spikes, contiguous humps, multi-concept independence, trailing runs, sub-threshold noise, layer-out-of-range, manifest shape mismatch, and threshold tunability. TLS (src/agent/api/http.rs): HttpClient::build now also loads every .pem file under ~/.consciousness/certs/ into the rustls root store — so dropping a <host>.pem in that directory is enough to trust a new self- signed server; no code changes per new host. Also installs the rustls default crypto provider explicitly via OnceLock: tonic's tls features pulled in both ring and aws-lc-rs on the resolver path, and rustls 0.23 refuses to auto-pick when either could win. Build (build.rs, Cargo.toml): tonic-build generates Rust types from proto/salience.proto at cargo-build time, using a vendored protoc binary (protoc-bin-vendored) so no system install is required. New runtime deps: tonic, prost, async-stream, tokio-stream, rustls-pemfile. Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
0e459aae92
commit
08213f9093
15 changed files with 1689 additions and 440 deletions
146
src/logging.rs
Normal file
146
src/logging.rs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
// logging.rs — log-crate logger that routes by target.
|
||||
//
|
||||
// Records with target "grpc" (or any target starting with "grpc::") go
|
||||
// to ~/.consciousness/logs/daemon/grpc.log so we can tell gRPC events
|
||||
// apart from the rest of consciousness's noise. Everything else goes
|
||||
// to ~/.consciousness/logs/daemon/debug.log.
|
||||
//
|
||||
// Level threshold is taken from RUST_LOG (simple global level parse:
|
||||
// "trace"/"debug"/"info"/"warn"/"error"); defaults to "info".
|
||||
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use log::{Level, LevelFilter, Log, Metadata, Record, SetLoggerError};
|
||||
|
||||
fn logs_dir() -> PathBuf {
|
||||
dirs::home_dir().unwrap_or_default().join(".consciousness/logs/daemon")
|
||||
}
|
||||
|
||||
struct RoutingLogger {
|
||||
grpc_file: Mutex<Option<std::fs::File>>,
|
||||
debug_file: Mutex<Option<std::fs::File>>,
|
||||
level: LevelFilter,
|
||||
}
|
||||
|
||||
impl RoutingLogger {
|
||||
fn new(level: LevelFilter) -> Self {
|
||||
let dir = logs_dir();
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let grpc = std::fs::OpenOptions::new()
|
||||
.create(true).append(true)
|
||||
.open(dir.join("grpc.log")).ok();
|
||||
let debug = std::fs::OpenOptions::new()
|
||||
.create(true).append(true)
|
||||
.open(dir.join("debug.log")).ok();
|
||||
Self {
|
||||
grpc_file: Mutex::new(grpc),
|
||||
debug_file: Mutex::new(debug),
|
||||
level,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_grpc_target(target: &str) -> bool {
|
||||
target == "grpc" || target.starts_with("grpc::")
|
||||
}
|
||||
}
|
||||
|
||||
impl Log for RoutingLogger {
|
||||
fn enabled(&self, m: &Metadata) -> bool {
|
||||
// Always enable DEBUG for grpc target so the dedicated log is
|
||||
// actually useful without RUST_LOG wrangling; defer to the
|
||||
// configured level for everything else.
|
||||
if Self::is_grpc_target(m.target()) {
|
||||
return m.level() <= Level::Debug;
|
||||
}
|
||||
m.level() <= self.level
|
||||
}
|
||||
|
||||
fn log(&self, record: &Record) {
|
||||
if !self.enabled(record.metadata()) {
|
||||
return;
|
||||
}
|
||||
let line = format!(
|
||||
"[{}] [{}] [{}] {}\n",
|
||||
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S%.3f"),
|
||||
record.level(),
|
||||
record.target(),
|
||||
record.args(),
|
||||
);
|
||||
let slot = if Self::is_grpc_target(record.target()) {
|
||||
&self.grpc_file
|
||||
} else {
|
||||
&self.debug_file
|
||||
};
|
||||
if let Ok(mut guard) = slot.lock() {
|
||||
if let Some(ref mut f) = *guard {
|
||||
let _ = f.write_all(line.as_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) {
|
||||
for slot in [&self.grpc_file, &self.debug_file] {
|
||||
if let Ok(mut g) = slot.lock() {
|
||||
if let Some(ref mut f) = *g {
|
||||
let _ = f.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_level_from_env() -> LevelFilter {
|
||||
let raw = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string());
|
||||
// Parse a plain level word; if it's the module=level form, we take
|
||||
// the first level we find.
|
||||
let token = raw.split(',').next().unwrap_or("info");
|
||||
let level_word = token.rsplit_once('=').map(|(_, v)| v).unwrap_or(token);
|
||||
match level_word.trim().to_lowercase().as_str() {
|
||||
"trace" => LevelFilter::Trace,
|
||||
"debug" => LevelFilter::Debug,
|
||||
"info" => LevelFilter::Info,
|
||||
"warn" => LevelFilter::Warn,
|
||||
"error" => LevelFilter::Error,
|
||||
"off" => LevelFilter::Off,
|
||||
_ => LevelFilter::Info,
|
||||
}
|
||||
}
|
||||
|
||||
/// Install the routing logger. Safe to call at most once — subsequent
|
||||
/// calls return an error but are otherwise no-ops.
|
||||
pub fn init() -> Result<(), SetLoggerError> {
|
||||
let level = parse_level_from_env();
|
||||
let logger = Box::new(RoutingLogger::new(level));
|
||||
log::set_boxed_logger(logger)?;
|
||||
// Always let DEBUG records through globally so the grpc log can
|
||||
// capture them (the logger itself filters non-grpc targets by
|
||||
// `level`). The cost is that log::debug! call-sites below `level`
|
||||
// in other modules still do their arg formatting before being
|
||||
// dropped at the logger; acceptable for a debug tool.
|
||||
log::set_max_level(LevelFilter::Debug.max(level));
|
||||
// Mark the file with a session boundary so it's easy to see where a
|
||||
// restart happened.
|
||||
log::info!(
|
||||
"===== consciousness logger init (level={}, pid={}) =====",
|
||||
level, std::process::id(),
|
||||
);
|
||||
log::info!(target: "grpc",
|
||||
"===== grpc log init (level={}, pid={}) =====",
|
||||
level, std::process::id(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Consumer of &Level so the type is used when only some callers want it.
|
||||
#[allow(dead_code)]
|
||||
pub fn current_level() -> Level {
|
||||
match log::max_level() {
|
||||
LevelFilter::Trace => Level::Trace,
|
||||
LevelFilter::Debug => Level::Debug,
|
||||
LevelFilter::Info | LevelFilter::Off => Level::Info,
|
||||
LevelFilter::Warn => Level::Warn,
|
||||
LevelFilter::Error => Level::Error,
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue