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: 0 additions & 2 deletions crates/build-xtask/src/api/listing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,7 @@ pub(crate) fn difference(committed: Option<&str>, lines: &[String]) -> Vec<Strin
report
}

#[path = "listing-compact.rs"]
mod compact;

#[cfg(test)]
#[path = "listing-tests.rs"]
mod tests;
Original file line number Diff line number Diff line change
Expand Up @@ -148,5 +148,5 @@ impl Shared<'_> {
}

#[cfg(test)]
#[path = "listing-compact-tests.rs"]
#[path = "compact-tests.rs"]
mod tests;
4 changes: 0 additions & 4 deletions crates/build-xtask/src/product.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,14 +419,10 @@ fn collect_deps(table: &toml::map::Map<String, toml::Value>, names: &mut Vec<Str
}

#[cfg(test)]
#[path = "product-container-tests.rs"]
mod container_tests;
#[cfg(test)]
#[path = "product-harness-tests.rs"]
mod harness_tests;
#[cfg(test)]
#[path = "product-test-support.rs"]
pub(crate) mod test_support;
#[cfg(test)]
#[path = "product-tests.rs"]
mod tests;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Family-matrix fixtures: the dependency rules between product families,
//! the desktop-app boundary, and the classification itself. Container-privacy
//! fixtures sit in `product-container-tests.rs`.
//! fixtures sit in `container_tests.rs`.

use super::test_support::{workspace_root, write_crate};
use super::*;
Expand Down
14 changes: 7 additions & 7 deletions crates/promptforge-internal/engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ tokio = { workspace = true, features = ["macros", "rt", "sync", "time"], optiona
workspace-hack.workspace = true

[features]
# The engine's own test drivers, for companion crates' suites: the serial
# sans-IO driver (`test_support::drive`) that performs a run's effects
# through a caller's closure with no runtime and no HTTP, and the tokio
# driver (`test_support::drive_tokio`) that performs them through a
# caller's async performers - the only thing in this crate that needs
# tokio. No production dependent enables it: this crate's own bench and
# the companion crates' suites do, as dev-dependencies.
# The engine's own test drivers, for `benches/models_loop.rs` alone, which
# names this feature in `required-features`: the serial sans-IO driver
# (`test_support::drive`) that performs a run's effects through a caller's
# closure with no runtime and no HTTP, and the tokio driver
# (`test_support::drive_tokio`) that performs them through a caller's async
# performers - the only thing in this crate that needs tokio. No other
# crate enables it.
test-support = ["dep:tokio"]

[dev-dependencies]
Expand Down
8 changes: 4 additions & 4 deletions crates/promptforge-internal/engine/benches/models_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ use axum::extract::State;
use axum::response::IntoResponse;
use axum::routing::post;
use criterion::{Criterion, criterion_group, criterion_main};
use promptforge_engine::model::{
Completion, CompletionError, CompletionOptions, Message, ToolSchema,
};
use promptforge_engine::test_support::{BoxFuture, ChatClient, DeltaHook, RunHost, run_with_host};
use promptforge_engine::{Environment, Prompt, RunContext, RunLimits, RunResult};
use promptforge_engine::{Environment, RunContext, RunLimits, RunResult};
use promptforge_model_client::client::{Completion, Message, ToolSchema};
use promptforge_model_client::model::{CompletionError, CompletionOptions};
use promptforge_parser::Prompt;
use promptforge_types::models::{ModelCatalog, ModelDescriptor, ModelId, ThinkingMode};

// The suites' mock-gateway chat client, shared by path: the engine holds no
Expand Down
69 changes: 38 additions & 31 deletions crates/promptforge-internal/engine/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! [`Error`] is a `pub(crate)` internal error type, never part of the public API.
//! Every public boundary returns its own typed error ([`crate::RunError`],
//! [`crate::ParseError`], [`crate::CompletionError`],
//! [`ParseError`](promptforge_parser::ParseError), [`CompletionError`](promptforge_model_client::model::CompletionError),
//! [`promptforge_types::tools::ToolError`]); those wrappers
//! classify this internal type and preserve its source. See the module wrappers for
//! the `From` bridges that let internal `?` keep flowing through the error type.
Expand Down Expand Up @@ -41,7 +41,7 @@ pub(crate) enum Error {
///
/// This retains the originating YAML decode failure (a
/// `serde_yaml_ng::Error`) as the `#[source]` cause so
/// [`crate::ParseError`] can expose the frontmatter syntax location through
/// [`ParseError`](promptforge_parser::ParseError) can expose the frontmatter syntax location through
/// [`std::error::Error::source`] instead of flattening it into the message.
#[error("invalid frontmatter: {message}")]
#[non_exhaustive]
Expand All @@ -59,15 +59,17 @@ pub(crate) enum Error {
},

/// A structurally-classified parse failure with a stable kind and an
/// optional source byte span, so [`crate::ParseError`] can expose the
/// optional source byte span, so [`ParseError`](promptforge_parser::ParseError) can expose the
/// classification and location from stored fields instead of inferring them
/// from message text.
#[error("{message}")]
#[non_exhaustive]
ParseStructured {
/// The stable classification of this parse failure.
kind: crate::parser::ParseErrorKind,
/// The byte span of the offending region within the source, when known.
/// The byte span of the offending region, when known, relative to the
/// document body after the frontmatter and a leading BOM, with CRLF
/// normalized to LF.
span: Option<(usize, usize)>,
/// The human-readable diagnostic.
message: String,
Expand All @@ -94,8 +96,7 @@ pub(crate) enum Error {

/// A client or endpoint configuration input was invalid, retaining the
/// concrete cause (a secret or URL validation failure) as a private
/// `#[source]` (client F13 / AUDIT-DISCARDED-SOURCE) instead of flattening
/// it into the message.
/// `#[source]` instead of flattening it into the message.
#[error("{message}")]
#[non_exhaustive]
Config {
Expand All @@ -117,10 +118,10 @@ pub(crate) enum Error {

/// The backend returned a non-success status.
///
/// The `Display` is deliberately body-free (F5): the bounded,
/// control-escaped body is stored only in the private `body` field,
/// reachable through the explicit
/// [`crate::CompletionError::backend_body`] opt-in, so a raw or hostile
/// The `Display` is deliberately body-free: the bounded, control-escaped
/// body is stored only in the private `body` field, reachable through
/// the explicit
/// [`CompletionError::backend_body`](promptforge_model_client::model::CompletionError::backend_body) opt-in, so a raw or hostile
/// payload cannot forge log lines or leak into an error message.
#[error("non-success backend status {status}")]
Backend {
Expand All @@ -138,8 +139,8 @@ pub(crate) enum Error {
///
/// Like [`Error::MalformedResponse`] but retains the underlying decode
/// failure (for example a [`serde_json::Error`]) as the `#[source]` cause
/// rather than flattening it into the message (MODEL-009 / client F11), so
/// the error chain survives through the public wrappers' `source()`.
/// rather than flattening it into the message, so the error chain
/// survives through the public wrappers' `source()`.
#[error("malformed response: {message}")]
#[non_exhaustive]
MalformedResponseSource {
Expand All @@ -153,9 +154,9 @@ pub(crate) enum Error {
/// Reading a non-success backend response body failed at the transport
/// layer.
///
/// Retains the `reqwest::Error` as the `#[source]` cause (MODEL-010)
/// rather than flattening the read failure into display text, so the error
/// chain (timeout, connection reset) survives. The status the backend had
/// Retains the `reqwest::Error` as the `#[source]` cause rather than
/// flattening the read failure into display text, so the error chain
/// (timeout, connection reset) survives. The status the backend had
/// already returned is preserved for classification.
#[error("unreadable backend error body (status {status})")]
#[non_exhaustive]
Expand Down Expand Up @@ -195,15 +196,15 @@ pub(crate) enum Error {
/// (for example "host values have not been injected" or a poisoned mutex).
///
/// Failures that *do* have an `mlua` cause use [`Error::LuaRuntime`], which
/// retains that cause as a private source (F4). The message is the specific
/// retains that cause as a private source. The message is the specific
/// failure as a noun phrase; the public wrapper classifies this as a Lua
/// failure, so no redundant `lua error:` type label is prepended (F8).
/// failure, so no redundant `lua error:` type label is prepended.
#[error("{0}")]
Lua(String),

/// A section's Lua phase failed at runtime or while bridging host values,
/// retaining the originating `mlua` error as the private `#[source]` cause
/// (F4) alongside the mapped prompt-location message.
/// alongside the mapped prompt-location message.
///
/// This is the source-bearing counterpart to [`Error::Lua`]: the Lua
/// crate builds it from a concrete `mlua::Error` (see
Expand All @@ -224,7 +225,7 @@ pub(crate) enum Error {
/// Lua source was not syntactically valid at its prompt location.
///
/// Retains the originating `mlua` compile error as the private `#[source]`
/// cause (F4) alongside the location metadata, so the compiler diagnostic
/// cause alongside the location metadata, so the compiler diagnostic
/// chain survives through the public wrappers' `source()` instead of being
/// flattened into `message` alone.
#[error("lua compilation error at {location} (line {source_line}): {message}")]
Expand All @@ -244,7 +245,7 @@ pub(crate) enum Error {
},

/// Building a model-facing tool schema for a bound alias failed, retaining
/// the schema validation error as the private `#[source]` cause (F5) rather
/// the schema validation error as the private `#[source]` cause rather
/// than flattening it into `detail`.
///
/// Constructed only by the tool-scope preparation, which is test-only
Expand Down Expand Up @@ -540,8 +541,9 @@ impl From<crate::subst::SubstitutionError> for Error {
/// Maps the gateway-client error type back onto this one variant for
/// variant, so `Display`, `source()` chains, and `RunError`/`CompletionError`
/// classification are unchanged by the extraction. The client crate's
/// error type is not `#[non_exhaustive]` (the two crates version together), so
/// this match is total.
/// error type is `#[non_exhaustive]`; a variant this match does not name
/// becomes [`Error::Config`] with its display text and itself as the source,
/// so it classifies as a completion failure that is not retryable.
impl From<GatewayClientError> for Error {
fn from(error: GatewayClientError) -> Error {
match error {
Expand All @@ -567,6 +569,10 @@ impl From<GatewayClientError> for Error {
finish_reason,
},
GatewayClientError::ModelSetLock(message) => Error::Lua(message),
other => Error::Config {
message: other.to_string(),
source: Box::new(other),
},
}
}
}
Expand Down Expand Up @@ -906,7 +912,7 @@ mod tests {

#[test]
fn source_bearing_binding_errors_preserve_their_cause() {
// F5: the binding and tool-scope failures keep the originating typed
// The binding and tool-scope failures keep the originating typed
// error as a private `source()` instead of flattening it to a string,
// and the chain survives through the public `RunError` wrapper.
use promptforge_model_client::client::ToolSchemaError;
Expand All @@ -927,7 +933,7 @@ mod tests {

#[test]
fn lua_compile_preserves_the_originating_compiler_error() {
// F4: a compile failure keeps the concrete `mlua` error as a private
// A compile failure keeps the concrete `mlua` error as a private
// `source()` instead of flattening it into `message` alone, and the
// chain survives through the public `RunError` wrapper.
let compile = Error::LuaCompile {
Expand All @@ -945,7 +951,7 @@ mod tests {

#[test]
fn typed_error_survives_the_lua_external_boundary() {
// LUA-012: passing the typed error (not its `to_string()`) to
// Passing the typed error (not its `to_string()`) to
// `mlua::Error::external` keeps the original error as a downcastable
// source across the Lua boundary, rather than flattening it to text.
let original = Error::OutOfScopeToolCall {
Expand All @@ -971,13 +977,14 @@ mod tests {

#[test]
fn config_errors_preserve_their_causes_across_the_error_type_bridge() {
// AUDIT-DISCARDED-SOURCE: a transport's configuration failure (an
// unusable credential, a bad endpoint URL) arrives as the client
// error type's `Config` variant with its concrete cause attached;
// the cause survives both the public CompletionError::source and
// the mapping onto this crate's error type, classified as Config.
use crate::model::{CompletionError, CompletionErrorKind};
// A transport's configuration failure (an unusable credential, a
// bad endpoint URL) arrives as the client error type's `Config`
// variant with its concrete cause attached; the cause survives both
// the public CompletionError::source and the mapping onto this
// crate's error type, classified as Config.
use crate::model::CompletionError;
use promptforge_model_client::Error as ClientError;
use promptforge_model_client::model::CompletionErrorKind;

let cause = std::io::Error::other("gateway URL is not a valid URL");
let completion = CompletionError::from(ClientError::Config {
Expand Down
7 changes: 3 additions & 4 deletions crates/promptforge-internal/engine/src/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,9 @@ pub use run::{
AnswerRecord, ChatAnswerRecord, Effect, EffectAnswer, EffectId, EffectRecord, Run, Step,
ToolAnswerRecord, ToolCallOrigin, ToolCaller,
};
// The store vocabulary a `Store` effect holds and its answer returns:
// named here so a host's store performer can be written against this one
// crate without reaching behind it.
pub use promptforge_lua::{StoreOp, StoreOutcome};
// The store vocabulary a `Store` effect holds and its answer returns, for
// the engine's own store handling; hosts name it from `promptforge_lua`.
pub(crate) use promptforge_lua::{StoreOp, StoreOutcome};

/// Performs one store operation through `access`: the work behind an
/// [`Effect::Store`], for a host's store performer. `access` is the
Expand Down
18 changes: 10 additions & 8 deletions crates/promptforge-internal/engine/src/execute/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//! callback resolves through them too, so every control surface agrees on
//! what a heading may name.

use crate::fanout;
use crate::heading_address;
use crate::parser::Section;
use crate::{Error, Result};

Expand Down Expand Up @@ -52,11 +52,12 @@ pub(super) fn visible_sections(home: &[Section], caller: &Section) -> Vec<Sectio
///
/// # Errors
/// Returns [`Error::Lua`] when the heading is malformed, matches no visible
/// section, or matches more than one (see [`fanout::resolve_sibling`]), or
/// when the resolved section has no pre-parsed items - the error that catches
/// naming a prose section by mistake.
/// section, or matches more than one (see
/// [`heading_address::resolve_sibling`]), or when the resolved section has
/// no pre-parsed items - the error that catches naming a prose section by
/// mistake.
pub(super) fn list_items_from_visible(heading: &str, visible: &[Section]) -> Result<Vec<String>> {
let section = fanout::resolve_sibling(heading, visible)?;
let section = heading_address::resolve_sibling(heading, visible)?;
if section.items().is_empty() {
return Err(Error::Lua(format!(
"section `{}` has no pre-parsed items",
Expand All @@ -81,8 +82,9 @@ pub(super) enum JumpTarget {
/// sibling within the jumper's own slice.
///
/// Resolution is an exact `(level, name)` match (see
/// [`fanout::resolve_sibling`]): two visible sections sharing an address
/// error loudly as ambiguous instead of silently resolving to the first.
/// [`heading_address::resolve_sibling`]): two visible sections sharing an
/// address error loudly as ambiguous instead of silently resolving to the
/// first.
///
/// # Errors
/// Returns [`Error::Lua`] when the heading is malformed, matches no visible
Expand All @@ -93,7 +95,7 @@ pub(super) fn resolve_jump_target(
jumper: &Section,
) -> Result<JumpTarget> {
let visible = visible_sections(siblings, jumper);
let target = fanout::resolve_sibling(heading, &visible)?;
let target = heading_address::resolve_sibling(heading, &visible)?;
if let Some(index) = section_position(jumper.children(), target) {
return Ok(JumpTarget::Child(index));
}
Expand Down
27 changes: 5 additions & 22 deletions crates/promptforge-internal/engine/src/execute/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ use super::config::RunContext;
use super::fill::{fill_model_bindings, fill_tool_bindings};
use super::requirements::Requirements;

/// What exists in this deployment and its standing policy: the nesting
/// cap, the catalog of tools the host has made available, and the
/// preludes its activated capabilities contributed.
/// What exists in this deployment: the catalog of tools the host has
/// made available and the preludes its activated capabilities
/// contributed.
///
/// Safe to share across concurrent runs (`Sync`): everything that can
/// change per run sits on the [`RunContext`], and the tool implementations
Expand All @@ -23,15 +23,10 @@ use super::requirements::Requirements;
///
/// [`prepare`](Environment::prepare) fills the prompt's tool slots by
/// identity against the catalog and fills the model bindings from the
/// context's current model; the `max_depth` guard lands with the sub-run
/// adapter in the deferred prompt-pack work and is stored but not
/// consulted until then.
/// context's current model.
#[derive(Clone)]
#[non_exhaustive]
pub struct Environment {
/// Maximum model-orchestrated prompt-tool nesting, copied into every
/// run. Inert until the sub-run adapter lands with the prompt-pack.
max_depth: u32,
/// The tools a run may bind, as descriptors: assembled by the host from
/// its activated capabilities. The default is empty, so every exact
/// slot's capability is reported missing.
Expand All @@ -43,26 +38,15 @@ pub struct Environment {
}

impl Environment {
/// Builds the default environment: a nesting cap of 3, an empty
/// catalog, and no preludes.
/// Builds the default environment: an empty catalog and no preludes.
#[must_use]
pub fn new() -> Environment {
Environment {
max_depth: 3,
tools: ToolCatalog::default(),
preludes: Vec::new(),
}
}

/// Sets the maximum model-orchestrated prompt-tool nesting depth.
/// Consulted by the sub-run adapter when it lands with the
/// prompt-pack; stored inert until then.
#[must_use]
pub fn max_depth(mut self, max_depth: u32) -> Environment {
self.max_depth = max_depth;
self
}

/// Sets the catalog of tools a run may bind: the descriptors the host
/// assembled from its activated capabilities.
/// [`prepare`](Environment::prepare) fills the prompt's exact slots
Expand Down Expand Up @@ -141,7 +125,6 @@ impl Default for Environment {
impl fmt::Debug for Environment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Environment")
.field("max_depth", &self.max_depth)
.field("tools", &self.tools)
.field(
"preludes",
Expand Down
Loading
Loading