Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1dc0210
fix(config, tool): Keep tool access rule order across a config delta
JeanMertz Sep 7, 2026
86f1b4f
fix(config): Record a cleared list on every strategy-carrying field
JeanMertz Sep 7, 2026
b809d3e
feat(config): Record a cleared scalar field
JeanMertz Sep 7, 2026
fa2b645
refactor(config): Name the fields a delta never stores
JeanMertz Sep 7, 2026
5063d17
feat(schematic): Honor `partial_via` on a plain field
JeanMertz Sep 7, 2026
6bce192
feat(config): Let `editor.envs` declare its merge strategy
JeanMertz Sep 7, 2026
eb60359
feat(config, anthropic): Let `beta_headers` declare its merge strategy
JeanMertz Sep 7, 2026
a54d589
feat(config, mcp): Let MCP server lists declare their merge strategy
JeanMertz Sep 7, 2026
9acde54
feat(config): Let `stop_words` declare its merge strategy
JeanMertz Sep 7, 2026
4524ddd
feat(config): Let `config_load_paths` declare its merge strategy
JeanMertz Sep 7, 2026
eeb3cbc
refactor(config): Extract the strategy-carrying map delta
JeanMertz Sep 7, 2026
5002356
feat(config): Record config map entries the user removed
JeanMertz Sep 7, 2026
b6ea240
docs(config): Record why compaction rules cannot state a strategy
JeanMertz Sep 7, 2026
ef60f86
feat(config, mcp): Let the MCP server map declare its merge strategy
JeanMertz Sep 7, 2026
305924f
feat(config): Let plugin, alias and tool maps declare their strategy
JeanMertz Sep 7, 2026
10a0c61
feat(config): Let template and tool option maps declare their strategy
JeanMertz Sep 7, 2026
dc7f015
feat(config): Let the tools map declare its merge strategy
JeanMertz Sep 7, 2026
c989760
refactor(config): Remove the strategy-less map merge
JeanMertz Sep 7, 2026
036eed4
fix(config): Name a generic schema by its instantiation
JeanMertz Sep 8, 2026
d573238
fix(conversation): Strip through a map that states its merge strategy
JeanMertz Sep 8, 2026
a5e22fb
fix(config): Fill the tools map key by key through its wrapper
JeanMertz Sep 8, 2026
4c809ce
fix(config)!: Let `--cfg` name the model parameter table
JeanMertz Sep 7, 2026
2e93100
docs(config): Stop offering the parameter collector as a key
JeanMertz Sep 7, 2026
8d212f4
refactor(config)!: Flatten the model parameter collector
JeanMertz Sep 7, 2026
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
6 changes: 5 additions & 1 deletion crates/contrib/schematic_macros/src/common/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,11 @@ impl Field<'_> {
}
value_type
} else {
FieldValue::value(result.value)
// `partial_via` applies to a plain field too, so a list of scalars
// can carry a wrapper that knows its own merge strategy. The
// partial holds the via type and `generate_from_partial_value`
// converts back to the field's own type.
FieldValue::value(result.partial_via_ty.as_ref().unwrap_or(result.value))
};

result
Expand Down
31 changes: 26 additions & 5 deletions crates/contrib/schematic_macros/src/config/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,22 +152,43 @@ impl Field<'_> {

#[allow(clippy::collapsible_else_if)]
if matches!(self.value_type, FieldValue::Value { .. }) {
// A `partial_via` field stores the via type in the partial and its
// own type in the resolved config, so the value converts on the way
// out. The conversion wraps the *inner* access, before any boxing,
// since it is the value that changes type and not its container.
let via = self.args.partial_via.is_some();
let convert = |value: TokenStream| {
if via {
quote! { Into::into(#value) }
} else {
value
}
};

if self.value_type.is_outer_boxed() {
if self.is_nullable() {
quote! { partial.#key.map(Box::new) }
let inner = convert(quote! { value });
quote! { partial.#key.map(|value| Box::new(#inner)) }
} else {
quote! { Box::new(partial.#key) }
let inner = convert(quote! { partial.#key });
quote! { Box::new(#inner) }
}
} else {
if self.is_nullable() {
// Use optional values as-is as they're already wrapped in `Option`
quote! { partial.#key }
if via {
quote! { partial.#key.map(Into::into) }
} else {
quote! { partial.#key }
}
} else if self.is_required() {
// Trigger a validation error if the value is missing
quote! { partial.#key.ok_or(schematic::ConfigError::MissingRequired{ fields: { let mut fields = fields.clone(); fields.push(#key_quoted.to_owned()); fields } })? }
convert(
quote! { partial.#key.ok_or(schematic::ConfigError::MissingRequired{ fields: { let mut fields = fields.clone(); fields.push(#key_quoted.to_owned()); fields } })? },
)
} else {
// Otherwise unwrap the resolved value or use the type default
quote! { partial.#key.unwrap_or_default() }
convert(quote! { partial.#key.unwrap_or_default() })
}
}
} else {
Expand Down
45 changes: 43 additions & 2 deletions crates/contrib/schematic_macros/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,44 @@ struct SchematicImplArgs<'a> {
instrument: &'a TokenStream,
}

/// The body of a `schema_name` that says which instantiation it describes.
///
/// A schema names each type it expands and refers back to that name wherever
/// the type appears again, so a name has to identify one type.
/// A generic named after its base alone does not: `MergeableMap<ToolConfig>`
/// and `MergeableMap<ToolParameterConfig>` would both answer `MergeableMap`,
/// and a consumer resolving a reference by name would walk a value against
/// whichever of them it met first.
///
/// The arguments are appended instead, so the two are `MergeableMap_ToolConfig`
/// and `MergeableMap_ToolParameterConfig`.
/// An argument with no name of its own (a primitive) contributes nothing, which
/// leaves the base name for a type generic only over those.
#[cfg(feature = "schema")]
fn generate_schema_name(base: &str, generics: &syn::Generics) -> TokenStream {
let type_params = generics.type_params().map(|param| &param.ident);
let mut appends = type_params.peekable();

if appends.peek().is_none() {
return quote! { Some(#base.into()) };
}

let appends = appends.map(|ident| {
quote! {
if let Some(argument) = <#ident as schematic::Schematic>::schema_name() {
name.push('_');
name.push_str(&argument);
}
}
});

quote! {
let mut name = String::from(#base);
#(#appends)*
Some(name)
}
}

#[cfg(feature = "schema")]
fn emit_schematic_impls(args: &SchematicImplArgs<'_>) -> TokenStream {
let &SchematicImplArgs {
Expand All @@ -153,6 +191,9 @@ fn emit_schematic_impls(args: &SchematicImplArgs<'_>) -> TokenStream {
let partial_schema_name = partial_name.to_string();
let partial_schema_impl = crate::common::Container::generate_partial_schema(name, cfg.generics);

let schema_name_impl = generate_schema_name(&schema_name, cfg.generics);
let partial_schema_name_impl = generate_schema_name(&partial_schema_name, cfg.generics);

// `schema_union_with` unions the derived schema with caller-supplied
// variants, for a type that deserializes from more shapes than its fields
// describe. Using it asserts the extra shapes are shorthands for the fields,
Expand Down Expand Up @@ -180,7 +221,7 @@ fn emit_schematic_impls(args: &SchematicImplArgs<'_>) -> TokenStream {
#[automatically_derived]
impl #impl_generics schematic::Schematic for #name #ty_generics #schematic_where {
fn schema_name() -> Option<String> {
Some(#schema_name.into())
#schema_name_impl
}

#instrument
Expand All @@ -194,7 +235,7 @@ fn emit_schematic_impls(args: &SchematicImplArgs<'_>) -> TokenStream {
#[automatically_derived]
impl #impl_generics schematic::Schematic for #partial_name #ty_generics #partial_schematic_where {
fn schema_name() -> Option<String> {
Some(#partial_schema_name.into())
#partial_schema_name_impl
}

#instrument
Expand Down
4 changes: 2 additions & 2 deletions crates/jp_cli/src/cmd/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2068,12 +2068,12 @@ fn apply_enable_tools(
for d in directives.iter() {
match d {
ToolDirective::EnableAll => {
for (name, tool) in &mut partial.conversation.tools.tools {
for (name, tool) in partial.conversation.tools.tools.iter_mut() {
apply_directive_to_tool(name, tool, &defaults, ToggleScope::Bulk, true)?;
}
}
ToolDirective::DisableAll => {
for (name, tool) in &mut partial.conversation.tools.tools {
for (name, tool) in partial.conversation.tools.tools.iter_mut() {
apply_directive_to_tool(name, tool, &defaults, ToggleScope::Bulk, false)?;
}
}
Expand Down
3 changes: 2 additions & 1 deletion crates/jp_cli/src/cmd/query/tool/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ pub fn describe_tools() -> PartialToolConfig {
..Default::default()
})),
..Default::default()
})]),
})])
.into(),
run: Some(RunMode::Unattended),
style: Some(PartialDisplayStyleConfig {
hidden: Some(true),
Expand Down
6 changes: 4 additions & 2 deletions crates/jp_cli/src/cmd/query/tool/coordinator_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ fn test_question_target_with_configured_question() {
target: Some(QuestionTarget::Assistant(Box::default())),
answer: None,
}
},
}
.into(),
..Default::default()
},
vec![],
Expand Down Expand Up @@ -231,7 +232,8 @@ fn test_static_answer_with_configured_answer() {
target: Some(QuestionTarget::User),
answer: None,
}
},
}
.into(),
..Default::default()
},
vec![],
Expand Down
Loading
Loading