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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1366,7 +1366,7 @@ the files you must plan around (see below).
| Change the wrapper socket's **trait** — `TurnWrapper`, `admissible`, `judge`/`again`, the in-process/subprocess transports | [`stella-runtime`](crates/stella-runtime/README.md) | `src/wrapper/` (#3380, landed #3479, `doc:wrapper-socket`). Lives one layer above `stella-core` because `before_turn`/`after_turn` do I/O, which invariant 2 bans in the engine; consumes `stella-plugin`'s wire types rather than redefining them. It is reached four ways now: a `--pipeline` run, a goal round, a fleet attempt, and an auto-resolved plugin — see the crate's own README. |
| Change how a plugin is **installed, listed, removed, or trusted** — `.stella/plugins/` and `~/.stella/plugins/` resolution, install consent, the project-tier trust gate | [`stella-cli`](crates/stella-cli/README.md) | `src/plugin_cmd.rs` + `src/plugin_cmd/{roster,process}.rs` — renders `stella_plugin::consent_text` before anything executes, gates a cloned repository's plugins on `project_code_execution_trusted()` (#3509), and is the one place `LoopGrant::permits_hook`/`permits_point` get consulted against an installed manifest. |
| Decide whether a human is present to see/answer a mid-run prompt | [`stella-tty`](crates/stella-tty/README.md) | **A leaf with NO dependencies at all** (#3036) — one pure `human_can_answer(interactive_output, stdin_is_terminal, prompt_is_visible)`, which is what lets `stella-cli`'s approval prompts and `stella-model`'s credential prompt share one derivation without `stella-model` depending on `stella-cli` (invariant 1). |
| Read the real clock, wait on the real timer, or stand in for either under test, behind `stella-core`'s `Sleeper` and `Clock` ports | [`stella-time`](crates/stella-time/README.md) | **Near-leaf: `stella-core` is its only workspace dependency.** `TokioSleeper` (the engine's real sleeper and `now`), `WallClock` (Unix epoch, for a stamp another process reads) and `MonotonicClock` (one origin per process, for a span compared as a number), plus `test_util`'s `PausedSleeper` and `NoopSleeper` behind the `test-util` feature. One home, because `stella-serve` may not link `stella-cli` or `stella-runtime` and `stella-core` may not carry a timer (ADR 0042). |
| Read the real clock, wait on the real timer, or stand in for either under test, behind `stella-core`'s `Sleeper` and `Clock` ports | [`stella-time`](crates/stella-time/README.md) | **Near-leaf: `stella-core` is its only workspace dependency.** `TokioSleeper` (the engine's real sleeper and `now`), `WallClock` (Unix epoch, for a stamp another process reads) and `MonotonicClock` (one origin per process, for a span compared as a number), plus `test_util`'s `PausedSleeper` and `NoopSleeper` behind the `test-util` feature. One home, because `stella-serve` may not link `stella-cli` or `stella-runtime` and `stella-core` may not carry a timer (ADR 0042); `stella-core` tests against it through a dev-dependency cycle, the tokio / tokio-test shape. |
| Emit a diagnostic — a record explaining *why* the program did something | [`stella-diag`](crates/stella-diag/README.md) | **A leaf: `serde` only, so anything may depend on it.** Field values cannot hold a `String`, a `Path`, or model output — that is a compile error, not a review question. Design: [`docs/spec/diagnostics.md`](docs/spec/diagnostics.md). |
| Compute a line-oriented unified diff (`@@` hunks, git's exact shape) | [`stella-diff`](crates/stella-diff/README.md) | **A leaf with NO dependencies at all** (#1511) — pure functions over borrowed strings, which is what lets [`stella-observatory`](crates/stella-observatory/README.md) and [`stella-cli`](crates/stella-cli/README.md) share one differ without costing the observatory its isolation. |
| Strip ANSI escape sequences from tool output | [`stella-ansi`](crates/stella-ansi/README.md) | **A leaf with NO dependencies at all** — one pure function over a borrowed `&str`, extracted from `stella-tui` so [`stella-observatory`](crates/stella-observatory/README.md) could strip a colourised tool's output before it reaches the journal route's `<pre>` without linking `ratatui`. `stella-tui`'s `ansi` module re-exports it and keeps only the `ratatui`-shaped emission half. |
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions crates/stella-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ futures-util = { workspace = true }
sha2 = { workspace = true }

[dev-dependencies]
# The engine's tests take the shared sleeper doubles from the crate that
# holds the real sleeper. A dev-dependency cycle is allowed, and this is the
# tokio / tokio-test shape.
stella-time = { path = "../stella-time", features = ["test-util"] }
tokio = { workspace = true, features = ["test-util"] }
# Seeds the retry tests' jitter double. Shipping code draws nothing: the
# entropy behind backoff jitter is a port (`retry::Sleeper::jitter`), and
Expand Down
11 changes: 6 additions & 5 deletions crates/stella-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,10 @@ system message and the latest user message are never touched.
[`src/subagent.rs`](src/subagent.rs)'s `run_sub_agent`, which constructs the
child in-crate and carries every seam. The crate exports the `Sleeper` and
`Clock` ports with no production implementation — `stella-time` holds
those, and every host passes them to `Engine::assemble`. Tests wire a
no-op or a paused-clock double, so a retry costs no wall clock and a
timeout still means what it says.
those, and every host passes them to `Engine::assemble`. Tests take
`PausedSleeper` and `NoopSleeper` from `stella_time::test_util` — the unit
tests from `crate::tests`, the one copy the compiler forces — so a
retry costs no wall clock and a timeout still means what it says.
- **A sub-agent's steering is filtered, not inherited.** `drain_steering` is
destructive by contract, so a child that inherited the parent's `TurnSteering`
would swallow a message the user addressed to the parent. `ChildSteering`
Expand Down Expand Up @@ -332,8 +333,8 @@ turn-driver audit witnesses; also `budget_boundaries.rs`,
[`src/loop_detect.rs`](src/loop_detect.rs); a failing case writes its seed to
`proptest-regressions/`, and that seed is committed. No feature flag, no env var, no
fixture server and no network — driver tests wire scripted `Provider`s, counting
`ToolExecutor`s and sleeper doubles on a paused runtime, so the suite runs in
seconds. Keep it that
`ToolExecutor`s and `stella-time`'s shared sleeper doubles on a paused runtime,
so the suite runs in seconds. Keep it that
way: a test here that needs a file or a socket means the logic under test is in
the wrong crate.

Expand Down
54 changes: 9 additions & 45 deletions crates/stella-core/src/accounted_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,12 @@ pub enum AccountedCallError {
/// measured since the last streamed fragment, so it re-arms every time the
/// dispatch is observed to still be producing — the same distinction
/// `crate::step::bounded_generation` draws for the engine's own step loop.
/// A flat wall-clock deadline here used to abandon a call the instant total
/// elapsed time crossed the ceiling even while the provider was actively
/// answering, which lost OpenRouter's trailing usage/cost frame (it arrives
/// in a final SSE frame *after* the content, once the gateway has settled
/// the routed call's price) to a ceiling sized to catch silence, not a
/// slow-but-live generation (#1467).
/// A flat wall-clock deadline here abandoned a call the instant total
/// elapsed time crossed the ceiling, even while the provider was still
/// answering. That lost OpenRouter's trailing usage/cost frame, which
/// arrives in a final SSE frame *after* the content, once the gateway has
/// settled the routed call's price. The ceiling was sized to catch silence,
/// not a slow-but-live generation (#1467).
pub async fn run_accounted_call(
call: AccountedCall<'_>,
budget: &mut BudgetGuard,
Expand Down Expand Up @@ -460,24 +460,9 @@ mod tests {
use stella_protocol::{BudgetMode, CompletionMessage, CompletionRequestRef, CompletionUsage};

use super::*;
use crate::tests::{NoopSleeper, PausedSleeper};
use std::time::Instant;

struct NoopSleeper;

#[async_trait]
impl Sleeper for NoopSleeper {
async fn sleep(&self, _duration_ms: u64) {}

// The floor: a test that asserts on retry timing wants no spread in it.
fn now(&self) -> std::time::Instant {
std::time::Instant::now()
}

fn jitter(&self, _upper: u64) -> u64 {
0
}
}

struct RetryThenSuccess {
attempts: Mutex<u32>,
}
Expand Down Expand Up @@ -982,27 +967,6 @@ mod tests {
);
}

/// A [`Sleeper`] backed by real (here, paused-virtual) tokio time so a
/// caller-supplied per-call timeout can expire *during* a backoff sleep.
struct TokioSleeper;

#[async_trait]
impl Sleeper for TokioSleeper {
async fn sleep(&self, duration_ms: u64) {
tokio::time::sleep(Duration::from_millis(duration_ms)).await;
}

// The floor: the timeout under test is placed against the exact
// backoff, so the draw must not move it.
fn now(&self) -> std::time::Instant {
std::time::Instant::now()
}

fn jitter(&self, _upper: u64) -> u64 {
0
}
}

struct AlwaysRetryable;

#[async_trait]
Expand Down Expand Up @@ -1048,7 +1012,7 @@ mod tests {
},
&mut budget,
&EventSender::new(tx),
&TokioSleeper,
&PausedSleeper,
)
.await;

Expand Down Expand Up @@ -1170,7 +1134,7 @@ mod tests {
},
&mut budget,
&EventSender::new(tx),
&TokioSleeper,
&PausedSleeper,
)
.await
.expect("the trailing gap must not abandon a call that was actively answering");
Expand Down
4 changes: 2 additions & 2 deletions crates/stella-core/src/driver/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ mod tests {

let provider = crate::subagent::tests::ScriptedProvider::new(vec![]);
let tools = crate::subagent::tests::MixedTools::default();
let sleeper = crate::subagent::tests::TokioSleeper;
let sleeper = crate::tests::PausedSleeper;

let owned = built_elsewhere();
let seams = owned.as_borrowed();
Expand Down Expand Up @@ -473,7 +473,7 @@ mod tests {
fn assemble_carries_the_bare_capability_set_onto_the_engine() {
let provider = crate::subagent::tests::ScriptedProvider::new(vec![]);
let tools = crate::subagent::tests::MixedTools::default();
let sleeper = crate::subagent::tests::TokioSleeper;
let sleeper = crate::tests::PausedSleeper;

let seams = TurnCapabilities::none();
let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
Expand Down
18 changes: 1 addition & 17 deletions crates/stella-core/src/driver/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,8 @@ mod tests {

use super::super::TurnHalt;
use crate::event_sender::EventSender;
use crate::retry::Sleeper;
use crate::step::{BudgetSnapshot, CHECKPOINT_VERSION, Checkpoint, TurnState};
use crate::tests::NoopSleeper;
use crate::{Engine, EngineConfig, TurnCapabilities, TurnOutcome};
use stella_protocol::{
AgentEvent, BudgetMode, CompletionMessage, CompletionRequestRef, CompletionResult,
Expand Down Expand Up @@ -449,22 +449,6 @@ mod tests {
}
}

#[derive(Debug)]
struct NoopSleeper;
#[async_trait]
impl Sleeper for NoopSleeper {
async fn sleep(&self, _duration_ms: u64) {}

// The floor: a test that asserts on retry timing wants no spread in it.
fn now(&self) -> std::time::Instant {
std::time::Instant::now()
}

fn jitter(&self, _upper: u64) -> u64 {
0
}
}

/// Fires once the executor's flag is up — the shape of the pipeline's
/// `FlipHalt`, which latches synchronously inside the `ToolResult` tap.
#[derive(Debug)]
Expand Down
18 changes: 1 addition & 17 deletions crates/stella-core/src/driver/drive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,8 @@ mod tests {

use super::super::TurnHalt;
use crate::event_sender::EventSender;
use crate::retry::Sleeper;
use crate::step::{BudgetSnapshot, CHECKPOINT_VERSION, Checkpoint, TurnState};
use crate::tests::NoopSleeper;
use crate::{Engine, EngineConfig, TurnCapabilities, TurnOutcome};
use stella_protocol::{
BudgetMode, CompletionMessage, CompletionRequestRef, CompletionResult, CompletionUsage,
Expand Down Expand Up @@ -278,22 +278,6 @@ mod tests {
}
}

#[derive(Debug)]
struct NoopSleeper;
#[async_trait]
impl Sleeper for NoopSleeper {
async fn sleep(&self, _duration_ms: u64) {}

// The floor: a test that asserts on retry timing wants no spread in it.
fn now(&self) -> std::time::Instant {
std::time::Instant::now()
}

fn jitter(&self, _upper: u64) -> u64 {
0
}
}

/// Armed from the start, so the first committed boundary ends the turn.
#[derive(Debug)]
struct AlwaysHalt;
Expand Down
33 changes: 9 additions & 24 deletions crates/stella-core/src/driver/restore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -575,25 +575,10 @@ mod tests {
use crate::driver::{Engine, EngineConfig};
use crate::event_sender::EventSender;
use crate::ports::ToolExecutor;
use crate::retry::Sleeper;
use crate::step::SummarizerHealth;
use crate::tests::NoopSleeper;
use stella_protocol::BudgetMode;

struct NoSleep;
#[async_trait]
impl Sleeper for NoSleep {
async fn sleep(&self, _duration_ms: u64) {}

// The floor: a test that asserts on retry timing wants no spread in it.
fn now(&self) -> std::time::Instant {
std::time::Instant::now()
}

fn jitter(&self, _upper: u64) -> u64 {
0
}
}

/// Always answers "SUMMARY" — the summarizer path under test is the
/// restoration that follows the splice, not the summary itself.
struct SummaryProvider;
Expand Down Expand Up @@ -752,7 +737,7 @@ mod tests {
active: vec![],
};
let seams = TurnCapabilities::none();
let engine = Engine::assemble(&provider, &tools, config(), &NoSleep, seams);
let engine = Engine::assemble(&provider, &tools, config(), &NoopSleeper, seams);
let mut messages = vec![
CompletionMessage::system("sys"),
CompletionMessage::user("the task"),
Expand Down Expand Up @@ -793,7 +778,7 @@ mod tests {
active: vec![],
};
let seams = TurnCapabilities::none();
let engine = Engine::assemble(&provider, &tools, config(), &NoSleep, seams);
let engine = Engine::assemble(&provider, &tools, config(), &NoopSleeper, seams);
let mut messages = vec![
CompletionMessage::system("sys"),
CompletionMessage::user("the task"),
Expand Down Expand Up @@ -837,7 +822,7 @@ mod tests {
active: vec![],
};
let seams = TurnCapabilities::none();
let engine = Engine::assemble(&provider, &tools, config(), &NoSleep, seams);
let engine = Engine::assemble(&provider, &tools, config(), &NoopSleeper, seams);
let mut messages = vec![
CompletionMessage::system("sys"),
CompletionMessage::user("the task"),
Expand Down Expand Up @@ -883,7 +868,7 @@ mod tests {
active: vec!["deploy".into()],
};
let seams = TurnCapabilities::none();
let engine = Engine::assemble(&provider, &tools, config(), &NoSleep, seams);
let engine = Engine::assemble(&provider, &tools, config(), &NoopSleeper, seams);
let mut messages = vec![
CompletionMessage::system("sys"),
CompletionMessage::user("the task"),
Expand Down Expand Up @@ -924,7 +909,7 @@ mod tests {
active: vec![],
};
let seams = TurnCapabilities::none();
let engine = Engine::assemble(&provider, &tools, config(), &NoSleep, seams);
let engine = Engine::assemble(&provider, &tools, config(), &NoopSleeper, seams);
let mut messages = vec![
CompletionMessage::system("sys"),
CompletionMessage::user("the task"),
Expand Down Expand Up @@ -957,7 +942,7 @@ mod tests {
active: vec![],
};
let seams = TurnCapabilities::none();
let engine = Engine::assemble(&provider, &tools, config(), &NoSleep, seams);
let engine = Engine::assemble(&provider, &tools, config(), &NoopSleeper, seams);
let mut messages = vec![
CompletionMessage::system("sys"),
CompletionMessage::user("the task"),
Expand Down Expand Up @@ -1083,7 +1068,7 @@ mod tests {
active: vec![],
};
let seams = TurnCapabilities::none();
let engine = Engine::assemble(&provider, &tools, config(), &NoSleep, seams);
let engine = Engine::assemble(&provider, &tools, config(), &NoopSleeper, seams);
let mut messages = vec![
CompletionMessage::system("sys"),
CompletionMessage::user("the task"),
Expand Down Expand Up @@ -1138,7 +1123,7 @@ mod tests {
active: vec![],
};
let seams = TurnCapabilities::none();
let engine = Engine::assemble(&provider, &tools, config(), &NoSleep, seams);
let engine = Engine::assemble(&provider, &tools, config(), &NoopSleeper, seams);
let mut messages = vec![
CompletionMessage::system("sys"),
CompletionMessage::user("the task"),
Expand Down
Loading
Loading