Skip to content
Closed
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
1 change: 1 addition & 0 deletions Cargo.lock

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

8 changes: 4 additions & 4 deletions crates/runvil-internal/engine/src/execute/context-bound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,13 @@ pub(super) fn bound_model_set(prompt: &Prompt, bindings: &ModelBindings) -> Mode
}

/// Derives a section's `argv` from the args string under the prompt's
/// declaration: a default-declared prompt wraps the interface prose into
/// the default shape (`argv.prose`, with the empty string present, not
/// absent); a structured declaration parses the string as JSON, and a parse
/// `inputSchema`: a prompt without one wraps the interface prose into
/// `argv.prose` (with the empty string present, not absent); a prompt with
/// one parses the string as JSON, and a parse
/// failure or a JSON `null` reads as nil (`if argv then` is the malformed
/// check). The executor never hard-errors on shape.
pub(super) fn derive_argv(prompt: &Prompt, args: &str) -> Option<serde_json::Value> {
if prompt.frontmatter().args().is_default() {
if prompt.frontmatter().input_schema().is_none() {
return Some(serde_json::json!({ "prose": args }));
}
match serde_json::from_str(args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
//! The args/argv surface: `args` is the exact passed string always; `argv`
//! is the parsed JSON on success and nil otherwise (`if argv then` is the
//! malformed check); a default-declared prompt wraps interface prose into
//! `argv.prose`; `argv` is writable in H1 only and frozen when H1 completes,
//! malformed check); a prompt without an `inputSchema` wraps interface
//! prose into `argv.prose`; `argv` is writable in H1 only and frozen when H1 completes,
//! so an H1 repair reaches every downstream section while an H2 assignment
//! is an error; and `{{ argv }}` joins the prose substitution namespaces.

use runvil_parser::test_support::{PromptSource, prompt};

use crate::test_support::scenario::Scenario;

/// One declared (required) `query` string. The declaration advertises and
/// An input schema with one `query` string. The schema advertises and
/// documents; it never enforces - enforcement is the prompt's H1.
const QUERY: &str = "args: { query: { type: string } }";
const QUERY: &str = "inputSchema: { type: object, properties: { query: { type: string } } }";

/// A prompt declaring [`QUERY`] whose one section `Only` runs `lua`.
fn query_prompt(lua: &str) -> String {
Expand Down Expand Up @@ -177,7 +177,7 @@ fn absent_is_not_the_empty_string() {
// An optional field omitted is nil; passed empty it is "". The two are
// distinguishable in Lua.
let source = prompt(
"args: { prose: { type: string, optional: true } }",
"inputSchema: { type: object, properties: { prose: { type: string } } }",
&[(
"Only",
"
Expand All @@ -193,8 +193,8 @@ fn absent_is_not_the_empty_string() {
}

#[test]
fn a_default_declared_prompt_wraps_interface_prose() {
// No `args:` key: the default declaration wraps prose into argv.prose,
fn a_prompt_without_an_input_schema_wraps_interface_prose() {
// No `inputSchema:` key: the input text wraps into argv.prose,
// and args still holds the exact passed string.
let source = prompt(
"",
Expand All @@ -211,7 +211,7 @@ fn a_default_declared_prompt_wraps_interface_prose() {
}

#[test]
fn a_default_declared_prompt_wraps_empty_prose_as_present() {
fn a_prompt_without_an_input_schema_wraps_empty_prose_as_present() {
// Interface prose is always present, so empty prose wraps as the
// present empty string, distinguishable from an absent field.
let source = prompt(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ use crate::test_support::scenario::Scenario;

/// One declared `query` string, so `argv` is the parsed JSON or nil.
fn query_source() -> PromptSource {
PromptSource::new().front("args: { query: { type: string } }")
PromptSource::new()
.front("inputSchema: { type: object, properties: { query: { type: string } } }")
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions crates/runvil-internal/parser/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ runvil-lua.workspace = true
runvil-types.workspace = true
pulldown-cmark.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_yaml_ng.workspace = true
thiserror.workspace = true

Expand Down
25 changes: 12 additions & 13 deletions crates/runvil-internal/parser/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
//! Parses the frontmatter contract keys: `uses`, `args`, and `models`.
//! Parses the frontmatter contract keys: `uses`, `inputSchema`, and
//! `models`.
//!
//! The YAML is the whole contract: Packs install, models declare, args
//! type. Parsing validates the static shape - each `uses:` entry a bare
//! Pack id or vendor/name, no Pack id declared twice, the alias grammar
//! on map keys, the closed
//! model-keyword vocabulary, arg name and type sanity - and exposes the
//! The YAML is the whole contract: Packs install, models declare, the
//! input schema types. Parsing validates the static shape - each `uses:`
//! entry a bare Pack id or vendor/name, no Pack id declared twice, the
//! alias grammar on model labels, the closed model-keyword vocabulary, the
//! input schema's object shape - and exposes the
//! FULL declaration on the parsed [`Prompt`](crate::Prompt); satisfying
//! the declaration against the caller's environment is prepare's job,
//! never the parser's.
//!
//! `args` and `models` are defined in submodules; this root owns the
//! duplicate-Pack check and the map deserializer the two map keys share.
//! `inputSchema` and `models` are defined in submodules; this root owns
//! the duplicate-Pack check and the contract map deserializer.

use std::collections::btree_map::Entry;
use std::collections::{BTreeMap, BTreeSet};
Expand All @@ -21,19 +22,17 @@ use runvil_types::packs::{PackId, PackRef};
use serde::de::{self, MapAccess, Visitor};
use serde::{Deserialize, Deserializer};

mod args;
pub(crate) mod input_schema;
mod models;

#[cfg(test)]
mod tests;

pub use args::{ArgDecl, ArgType, ArgsDecl, ArgsDeclIter};
pub use models::{ModelKeyword, ModelRole, ModelRoles, ModelRolesIter};

/// The prompt-local alias grammar: `[A-Za-z][A-Za-z0-9_-]{0,63}`.
///
/// Model labels and args field names are prompt-local and never global
/// names. (The same rule sits in `runvil-lua`'s `alias` module, the
/// Model labels are prompt-local and never global names. (The same rule sits in `runvil-lua`'s `alias` module, the
/// run-time counterpart to this parse-time check.)
fn is_valid_alias(alias: &str) -> bool {
let bytes = alias.as_bytes();
Expand All @@ -51,7 +50,7 @@ struct ContractKeys {
what: &'static str,
}

/// Deserializes a contract map (`models`, `args`): string keys
/// Deserializes a contract map (`models`): string keys
/// validated against the alias grammar and `keys`, values deserialized as
/// `T`, duplicates rejected.
fn deserialize_contract_map<'de, D, T>(
Expand Down
Loading
Loading