Another pass for context.

This commit is contained in:
K. Hodges 2026-06-05 04:28:08 -07:00
parent 8cfd968e72
commit c084e2f3c0
3 changed files with 52 additions and 4 deletions

View File

@ -85,6 +85,19 @@ Before a provider request, Exoshell checks enabled context against the configure
Pruning is deterministic and non-mutating. Low-priority unpinned entries are selected for removal first. Pinned entries and critical-priority entries are preserved as long as possible.
## Provider Waiting and Timeouts
Provider requests are bounded by:
```toml
[provider]
request_timeout_seconds = 120
```
The default is 120 seconds. Exoshell prints a waiting message before sending a provider request so slow local model inference is visible. If the timeout is exceeded, Exoshell records the timeout in the transcript and returns to the REPL.
Keyboard interrupt behavior is still terminal-level: interrupting the process stops the active request, and normal transcript writing happens when the REPL exits cleanly.
## Transcripts
Context lifecycle events are recorded as metadata:

View File

@ -1,4 +1,5 @@
use std::path::PathBuf;
use std::time::Duration;
use crate::config::Config;
use crate::context::{
@ -57,10 +58,19 @@ impl App {
stream: false,
};
let response = match self.provider.chat(request).await {
Ok(ChatResponse::Complete(response)) => response,
Ok(ChatResponse::Stream(chunks)) => chunks.concat(),
Err(error) => {
let timeout = Duration::from_secs(self.config.provider.request_timeout_seconds);
let response = match tokio::time::timeout(timeout, self.provider.chat(request)).await {
Err(_) => {
let message = format!(
"provider request timed out after {} seconds",
self.config.provider.request_timeout_seconds
);
self.transcript.record_error(&message);
return Err(AppError::Provider(ProviderError::Network(message)));
}
Ok(Ok(ChatResponse::Complete(response))) => response,
Ok(Ok(ChatResponse::Stream(chunks))) => chunks.concat(),
Ok(Err(error)) => {
self.transcript.record_error(&error.to_string());
return Err(error.into());
}
@ -612,6 +622,20 @@ mod tests {
assert!(seen.lock().expect("seen lock").is_empty());
}
#[tokio::test]
async fn provider_request_times_out() {
let mut config = test_config();
config.provider.request_timeout_seconds = 0;
let mut app = App::new(config, Box::new(SlowProvider));
let error = app
.send("hello".into())
.await
.expect_err("request should time out");
assert!(error.to_string().contains("timed out"));
}
#[test]
fn stdin_context_uses_default_provider_path() {
let mut app = App::new(test_config(), Box::new(NoopProvider));
@ -647,6 +671,16 @@ mod tests {
}
}
struct SlowProvider;
#[async_trait::async_trait]
impl Provider for SlowProvider {
async fn chat(&self, _request: ChatRequest) -> Result<ChatResponse, ProviderError> {
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
Ok(ChatResponse::Complete("late".into()))
}
}
fn test_config() -> Config {
Config {
provider: ProviderConfig {

View File

@ -66,6 +66,7 @@ impl Repl {
continue;
}
println!("waiting for provider response...");
match self.app.send(input).await {
Ok(response) => println!("\n{}\n", render_assistant_output(&response)),
Err(error) => eprintln!("request failed: {error}"),