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>
146 lines
4.8 KiB
Rust
146 lines
4.8 KiB
Rust
// 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,
|
|
}
|
|
}
|