Skip to content
Merged
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
45 changes: 42 additions & 3 deletions apps/el-chat/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,13 @@ fn main() {
continue;
}
"reset" => {
// Release the prior conversation's KV BEFORE reporting success.
// If it can't be cleared, stop rather than fail open (the
// provider drops on exit, freeing the KV via ownership).
if !release_session(&provider) {
eprintln!("(ending session to release conversation memory)");
break;
}
history = vec![ChatMessage::system(&args.system)];
eprintln!("(conversation reset)");
continue;
Expand All @@ -251,10 +258,14 @@ fn main() {
let new_sys = parts.next().unwrap_or("").trim();
if new_sys.is_empty() {
eprintln!("(usage: /system <text>)");
} else {
history = vec![ChatMessage::system(new_sys)];
eprintln!("(system prompt updated; conversation reset)");
continue;
}
if !release_session(&provider) {
eprintln!("(ending session to release conversation memory)");
break;
}
history = vec![ChatMessage::system(new_sys)];
eprintln!("(system prompt updated; conversation reset)");
continue;
}
other => {
Expand All @@ -271,13 +282,41 @@ fn main() {
Ok(reply) => history.push(ChatMessage::assistant(reply)),
Err(e) => {
eprintln!("\n(generation error: {e}; conversation reset)");
if !release_session(&provider) {
eprintln!("(ending session to release conversation memory)");
break;
}
history = vec![ChatMessage::system(&args.system)];
}
}
}
eprintln!("bye.");
}

/// Release the provider's resident conversation memory — KV (evicted in the
/// engine), prompt, output, and buffered events — while keeping the model loaded
/// (ADR-018; PRD "KV caches … cleared on session end"). Used by `/reset`,
/// `/system`, and generation-error recovery so a discarded conversation does not
/// linger in the engine until the next turn.
///
/// Retries once, then returns `false` if the conversation could **not** be
/// cleared. Callers must then stop rather than fail open (continue and report a
/// successful reset with user K/V possibly still resident): on `false` they break
/// the loop so `main` returns and the provider drops, freeing the KV via ownership.
fn release_session(provider: &QwenChatProvider) -> bool {
if provider.end_session().is_ok() {
return true;
}
// One retry for a transient failure.
match provider.end_session() {
Ok(()) => true,
Err(e) => {
eprintln!("(error: could not clear conversation memory: {e})");
false
}
}
}

/// Stream one assistant reply to stdout via the SDK's `LlmProvider::chat_stream`;
/// returns the accumulated text so the caller can append it to history.
fn run_turn(provider: &QwenChatProvider, req: &ChatRequest) -> el_core::Result<String> {
Expand Down
21 changes: 13 additions & 8 deletions crates/adapters/el-engine-candle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,14 @@ println!("{}", reply.content);
# Ok::<(), el_core::EdgeError>(())
```

Each `chat` call builds a fresh `QwenEngine` (Candle exposes no public KV-cache
reset) and runs the standard SDK path: provenance permit → `load_prompt`
(prefill) → `generate` (grammar mask → safety steer → chunk-guard + checkpointed
rollback → greedy commit). Decoding is deterministic greedy argmax, so replies
are reproducible.
The model weights load **once** in `from_paths` and stay resident; each `chat`
reuses one persistent provenance-gated session (ADR-018) — reset (which evicts the
previous conversation's KV from Candle's cache via a position-0 forward, keeping
the weights loaded) → `load_prompt` (prefill) → `generate` (grammar mask → safety
steer → chunk-guard + checkpointed rollback → greedy commit). No per-turn reload
from disk. Decoding is deterministic greedy argmax, so replies are reproducible.
`end_session()` releases a conversation's memory while keeping the model resident;
dropping the provider frees the weights too.

### On-device safety (ADR-005 + ADR-012)

Expand Down Expand Up @@ -98,9 +101,11 @@ signature before issuing the permit. The chat model as its own expert is a
## Benchmark instrumentation

Setting `EL_BENCH=1` makes `QwenChatProvider::chat` print a per-phase breakdown
(model load / tokenize / prefill / decode / detokenize) plus per-forward
attribution (model compute vs. seam quantisation vs. runtime loop) to stderr.
It is zero-cost when unset and is a diagnostic only — not part of public behaviour.
(session setup / tokenize / prefill / decode / detokenize) plus per-forward
attribution (model compute vs. seam quantisation vs. runtime loop) to stderr. The
weights load once at startup (ADR-018), so per-turn "session setup" is near-zero —
not a per-chat model load. Zero-cost when unset; a diagnostic only, not part of
public behaviour.

## Features & dependencies

Expand Down
Loading
Loading