Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion crates/agentkeys-cli/src/hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,13 @@ pub async fn memory_inject(
// pipe a payload (EOF arrives) so they were unaffected; direct calls were not.
let client = HookClient::resolve(mcp_url, vendor_token, actor, operator);

// Pluggable engine seam (plan §6a): the gate already authorized these bytes;
// the engine — caller-side, deterministic, no LLM — selects which lines to
// inject within a budget. Default `passthrough` + unbounded budget injects
// the whole namespace unchanged. Passive injection carries no query (None).
let engine = agentkeys_core::memory_engine::engine_from_env();
let budget = agentkeys_core::memory_engine::SelectionBudget::from_env();

let mut chunks = Vec::new();
for ns in namespaces
.split(',')
Expand All @@ -258,7 +265,15 @@ pub async fn memory_inject(
{
Ok(result) => {
if let Some(text) = extract_memory_content(&result) {
chunks.push(format!("## Memory: {ns}\n{text}"));
let selected = agentkeys_core::memory_engine::select_blob(
engine.as_ref(),
None,
&text,
&budget,
);
if !selected.is_empty() {
chunks.push(format!("## Memory: {ns}\n{selected}"));
}
}
}
Err(e) => {
Expand Down
14 changes: 14 additions & 0 deletions crates/agentkeys-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,16 @@ enum Commands {
/// empty for the in-memory backend. JWTs expire — re-run wire to refresh.
#[arg(long, env = "AGENTKEYS_SESSION_BEARER", default_value = "")]
session_bearer: String,

/// Memory engine baked into the pre_llm_call hook: `passthrough`
/// (inject the whole namespace, default) or `lexical` (deterministic
/// recency/relevance selection). Plan §6a / arch.md §22.
#[arg(long, env = "AGENTKEYS_MEMORY_ENGINE", default_value = "passthrough")]
memory_engine: String,

/// Cap how many memory lines the engine injects (omit = unbounded).
#[arg(long, env = "AGENTKEYS_MEMORY_MAX_LINES")]
memory_max_lines: Option<u32>,
},

#[command(
Expand Down Expand Up @@ -1087,6 +1097,8 @@ async fn main() {
mcp_url,
vendor_token,
session_bearer,
memory_engine,
memory_max_lines,
} => agentkeys_cli::wire::cmd_wire(
runtime,
agentkeys_cli::wire::WireRequest {
Expand All @@ -1097,6 +1109,8 @@ async fn main() {
mcp_url: mcp_url.clone(),
vendor_token: vendor_token.clone(),
session_bearer: session_bearer.clone(),
memory_engine: memory_engine.clone(),
memory_max_lines: *memory_max_lines,
check_only: *check_only,
},
),
Expand Down
47 changes: 46 additions & 1 deletion crates/agentkeys-cli/src/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ pub struct WireRequest {
/// (TTL ≤ 5h) — re-run `agentkeys wire` to refresh, or point the demo at
/// a fresh session.
pub session_bearer: String,
/// Memory engine baked into the pre_llm_call hook (`passthrough` | `lexical`,
/// plan §6a). `passthrough`/empty injects the whole namespace and emits no
/// engine env, so the generated script stays byte-identical to the default.
pub memory_engine: String,
/// Optional cap on how many memory lines the engine injects (None = all).
pub memory_max_lines: Option<u32>,
/// When true, report drift without writing (drift-check / dry-run).
pub check_only: bool,
}
Expand Down Expand Up @@ -124,6 +130,19 @@ impl HermesAdapter {
body = body,
)
};
let memory_engine_exports = {
let mut exports = String::new();
if !req.memory_engine.is_empty() && req.memory_engine != "passthrough" {
exports.push_str(&format!(
"export AGENTKEYS_MEMORY_ENGINE={}\n",
shell_quote(&req.memory_engine)
));
}
if let Some(max_lines) = req.memory_max_lines {
exports.push_str(&format!("export AGENTKEYS_MEMORY_MAX_LINES={max_lines}\n"));
}
exports
};
vec![
(
"agentkeys-pretool-permission-gate.sh".to_string(),
Expand All @@ -139,7 +158,7 @@ impl HermesAdapter {
(
"agentkeys-prellm-memory-inject.sh".to_string(),
header(&format!(
"exec {bin} hook memory-inject --namespaces {ns}",
"{memory_engine_exports}exec {bin} hook memory-inject --namespaces {ns}",
ns = shell_quote(&req.namespaces),
)),
),
Expand Down Expand Up @@ -513,6 +532,8 @@ mod tests {
mcp_url: "http://localhost:8088/mcp".into(),
vendor_token: "demo-tok".into(),
session_bearer: String::new(),
memory_engine: "passthrough".into(),
memory_max_lines: None,
check_only: false,
}
}
Expand Down Expand Up @@ -559,6 +580,30 @@ mod tests {
.contains("hook memory-inject --namespaces 'travel,personal'"));
}

#[test]
fn scripts_omit_memory_engine_by_default() {
let a = HermesAdapter;
// Default passthrough + no budget → no engine env, byte-identical script.
let scripts = a.scripts("/usr/local/bin/agentkeys", &req());
assert!(!scripts[2].1.contains("AGENTKEYS_MEMORY_ENGINE"));
assert!(!scripts[2].1.contains("AGENTKEYS_MEMORY_MAX_LINES"));
}

#[test]
fn scripts_bake_memory_engine_when_set() {
let a = HermesAdapter;
let mut r = req();
r.memory_engine = "lexical".into();
r.memory_max_lines = Some(3);
let prellm = &a.scripts("/usr/local/bin/agentkeys", &r)[2].1;
assert!(prellm.contains("export AGENTKEYS_MEMORY_ENGINE='lexical'"));
assert!(prellm.contains("export AGENTKEYS_MEMORY_MAX_LINES=3"));
// engine env precedes the exec line so it is in scope for the hook
let engine_at = prellm.find("AGENTKEYS_MEMORY_ENGINE").unwrap();
let exec_at = prellm.find("hook memory-inject").unwrap();
assert!(engine_at < exec_at);
}

#[test]
fn write_if_changed_is_idempotent() {
let dir = std::env::temp_dir().join(format!("agentkeys-wire-{}", std::process::id()));
Expand Down
1 change: 1 addition & 0 deletions crates/agentkeys-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod chain_profile;
pub mod clear_signing;
pub mod device_crypto;
pub mod init_flow;
pub mod memory_engine;
pub mod mock_client;
pub mod otp;
pub mod payment;
Expand Down
Loading
Loading