use crate::tool::helpers::{StreamKind, is_stream_type, stream_type, to_kebab_case};
use crate::tool::ir::{
ArgIr, ArgPlacement, ArgSubKind, CommandAnnotationsIr, CommandIr, ConstraintIr, DocIr,
OutputChannelIr, PathDirectionIr, PathKindIr, QuantifierIr, RefIr, RepeatableMode, ResultIr,
ToolDefinitionIr,
};
use crate::tool::synthesis::doc_tokens;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use std::collections::BTreeSet;
use syn::spanned::Spanned;
use syn::{Error, Expr, GenericArgument, Ident, Path, PathArguments, ReturnType, Type};
pub fn descriptor_fn_ident(trait_ident: &Ident) -> Ident {
format_ident!("__golem_tool_descriptor_for_{}", trait_ident)
}
pub fn standalone_descriptor_fn_ident(trait_ident: &Ident) -> Ident {
format_ident!("__golem_standalone_tool_descriptor_for_{}", trait_ident)
}
pub fn prepared_descriptor_fn_ident(trait_ident: &Ident) -> Ident {
format_ident!("__golem_prepared_tool_descriptor_for_{}", trait_ident)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum DescriptorRepr {
Dynamic,
Wire,
}
pub fn wire_descriptor_fn_ident(trait_ident: &Ident) -> Ident {
format_ident!("__golem_wire_tool_descriptor_for_{}", trait_ident)
}
pub fn standalone_wire_descriptor_fn_ident(trait_ident: &Ident) -> Ident {
format_ident!(
"__golem_standalone_wire_tool_descriptor_for_{}",
trait_ident
)
}
fn needs_composition(ir: &ToolDefinitionIr) -> bool {
ir.commands.iter().any(|cmd| {
cmd.subtree.is_some()
|| cmd
.args
.iter()
.any(|arg| arg.placement == Some(ArgPlacement::Global))
|| cmd.constraints.iter().any(constraint_has_value_is)
})
}
pub fn synthesize_wire_descriptor_fn(ir: &ToolDefinitionIr) -> Result<TokenStream, Error> {
let plan = Plan::analyze(ir)?;
let ident = wire_descriptor_fn_ident(&ir.trait_ident);
let standalone_ident = standalone_wire_descriptor_fn_ident(&ir.trait_ident);
let trait_name = ir.trait_ident.to_string();
let root = build_root_node(ir, &plan, DescriptorRepr::Wire)?;
let links = ir
.commands
.iter()
.enumerate()
.filter(|(index, _)| Some(*index) != plan.root_idx)
.map(|(_, cmd)| {
if cmd.subtree.is_some() {
build_subtree_link(cmd, &plan, DescriptorRepr::Wire)
} else {
build_leaf_link(cmd, &plan, DescriptorRepr::Wire)
}
})
.collect::<Result<Vec<_>, _>>()?;
let version = match &ir.version {
Some(v) => quote! { #v.to_string() },
None => quote! { env!("CARGO_PKG_VERSION").to_string() },
};
let requires_filesystem = ir.requires_filesystem;
let standalone = if needs_composition(ir) {
quote! { #ident(&mut golem_rust::agentic::ToolBuildCtx::new(), __golem_wire_schema) }
} else {
quote! {
#[allow(unused_mut)]
let mut commands = ::std::vec![#root];
#(#links)*
::std::result::Result::Ok(golem_rust::agentic::ExtendedToolType { version: #version, requires_filesystem: #requires_filesystem, commands })
}
};
Ok(quote! {
#[doc(hidden)]
#[allow(non_snake_case)]
pub fn #standalone_ident(__golem_wire_schema: &golem_rust::agentic::WireToolSchema)
-> ::std::result::Result<golem_rust::agentic::ExtendedToolType<golem_rust::agentic::WireTypeRef>, golem_rust::agentic::ToolBuildError> {
#standalone
}
#[doc(hidden)]
#[allow(non_snake_case)]
pub fn #ident(
ctx: &mut golem_rust::agentic::ToolBuildCtx<golem_rust::agentic::WireTypeRef>,
__golem_wire_schema: &golem_rust::agentic::WireToolSchema,
) -> ::std::result::Result<golem_rust::agentic::ExtendedToolType<golem_rust::agentic::WireTypeRef>, golem_rust::agentic::ToolBuildError> {
ctx.with_descriptor(concat!(module_path!(), "::", #trait_name), |ctx| {
#[allow(unused_mut)]
let mut commands = vec![#root];
ctx.apply_pending_graft_root(&mut commands[0])?;
let name = commands[0].name.clone();
golem_rust::agentic::reconcile_command_inherited_globals(&mut commands[0], ctx.inherited_globals(), &name)?;
#(#links)*
let mut tool = golem_rust::agentic::ExtendedToolType { version: #version, requires_filesystem: #requires_filesystem, commands };
if ctx.is_outermost_descriptor() {
golem_rust::agentic::normalize_inherited_globals(&mut tool)?;
}
Ok(tool)
})
}
})
}
pub fn synthesize_descriptor_fn(ir: &ToolDefinitionIr) -> Result<TokenStream, Error> {
let plan = Plan::analyze(ir)?;
let fn_ident = descriptor_fn_ident(&ir.trait_ident);
let standalone_ident = standalone_descriptor_fn_ident(&ir.trait_ident);
let prepared_ident = prepared_descriptor_fn_ident(&ir.trait_ident);
let trait_name = ir.trait_ident.to_string();
let version = match &ir.version {
Some(v) => quote! { #v.to_string() },
None => quote! { env!("CARGO_PKG_VERSION").to_string() },
};
let requires_filesystem = ir.requires_filesystem;
let root_node = build_root_node(ir, &plan, DescriptorRepr::Dynamic)?;
let mut links = Vec::new();
for (idx, cmd) in ir.commands.iter().enumerate() {
if Some(idx) == plan.root_idx {
continue;
}
if cmd.subtree.is_some() {
links.push(build_subtree_link(cmd, &plan, DescriptorRepr::Dynamic)?);
} else {
links.push(build_leaf_link(cmd, &plan, DescriptorRepr::Dynamic)?);
}
}
let standalone = if needs_composition(ir) {
quote! { #fn_ident(&mut golem_rust::agentic::ToolBuildCtx::new()) }
} else {
quote! {
#[allow(unused_mut)]
let mut commands = ::std::vec![#root_node];
#(#links)*
::std::result::Result::Ok(golem_rust::agentic::ExtendedToolType {
version: #version,
requires_filesystem: #requires_filesystem,
commands,
})
}
};
let prepare = if ir.commands.iter().any(|cmd| {
cmd.subtree.is_some()
|| cmd.args.iter().any(|arg| arg.default.is_some())
|| cmd.constraints.iter().any(constraint_has_value_is)
}) {
quote! { __tool.prepare() }
} else {
let schemas = schema_checks(ir);
quote! { __tool.prepare_without_literals(#schemas) }
};
Ok(quote! {
#[doc(hidden)]
#[allow(non_snake_case)]
pub fn #standalone_ident() -> ::std::result::Result<
golem_rust::agentic::ExtendedToolType,
golem_rust::agentic::ToolBuildError,
> {
#standalone
}
#[doc(hidden)]
#[allow(non_snake_case)]
pub fn #prepared_ident() -> ::std::result::Result<
golem_rust::agentic::PreparedToolDescriptor,
golem_rust::agentic::ToolBuildError,
> {
let __tool = #standalone_ident()?;
#prepare
}
#[doc(hidden)]
#[allow(non_snake_case)]
pub fn #fn_ident(
ctx: &mut golem_rust::agentic::ToolBuildCtx,
) -> ::std::result::Result<
golem_rust::agentic::ExtendedToolType,
golem_rust::agentic::ToolBuildError,
> {
ctx.with_descriptor(concat!(module_path!(), "::", #trait_name), |ctx| {
#[allow(unused_variables, unused_mut)]
let mut commands: ::std::vec::Vec<golem_rust::agentic::ExtendedCommandNode> =
::std::vec::Vec::new();
commands.push(#root_node);
ctx.apply_pending_graft_root(&mut commands[0])?;
{
let __root_name = commands[0].name.clone();
golem_rust::agentic::reconcile_command_inherited_globals(
&mut commands[0],
ctx.inherited_globals(),
&__root_name,
)?;
}
#(#links)*
let mut __tool = golem_rust::agentic::ExtendedToolType {
version: #version,
requires_filesystem: #requires_filesystem,
commands,
};
if ctx.is_outermost_descriptor() {
golem_rust::agentic::normalize_inherited_globals(&mut __tool)?;
}
::std::result::Result::Ok(__tool)
})
}
})
}
fn constraint_has_value_is(constraint: &ConstraintIr) -> bool {
let has = |refs: &[RefIr]| refs.iter().any(|r| matches!(r, RefIr::ValueIs { .. }));
match constraint {
ConstraintIr::RequiresAll(refs)
| ConstraintIr::AllOrNone(refs)
| ConstraintIr::RequiresAny(refs) => has(refs),
ConstraintIr::MutexGroups(groups) => groups.iter().any(|refs| has(refs)),
ConstraintIr::Implies { lhs, rhs, .. } | ConstraintIr::Forbids { lhs, rhs, .. } => {
has(lhs) || has(rhs)
}
}
}
fn schema_checks(ir: &ToolDefinitionIr) -> TokenStream {
if ir.commands.iter().any(|cmd| {
split_result(&cmd.output).1.is_some()
|| cmd.args.iter().any(|arg| {
arg.regex.is_some()
|| arg.min_length.is_some()
|| arg.max_length.is_some()
|| arg.path_kind.is_some()
|| arg.direction.is_some()
|| arg.mime.is_some()
|| arg.schemes.is_some()
|| arg.raw_min.is_some()
|| arg.raw_max.is_some()
|| arg.bounds.is_some()
|| arg.unit.is_some()
})
}) {
return quote! { golem_rust::agentic::ToolSchemaChecks::dynamic() };
}
let mut types = Vec::new();
for cmd in &ir.commands {
for param in &cmd.params {
if is_auto_injected_principal_type(¶m.ty) || is_stream_type(¶m.ty) {
continue;
}
types.push(unwrap_generic1(¶m.ty, "Option").unwrap_or(¶m.ty));
}
if let Some(ty) = split_result(&cmd.output).0 {
types.push(ty);
}
}
quote! {{
use golem_rust::agentic::SelectToolSchemaChecks as _;
let checks: &[::std::option::Option<golem_rust::agentic::ToolSchemaChecks>] = &[
#((&golem_rust::agentic::ToolSchemaProbe::<#types>(::std::marker::PhantomData)).tool_schema_checks()),*
];
checks.iter().flatten().copied().next()
.unwrap_or_else(golem_rust::agentic::ToolSchemaChecks::scalar)
}}
}
struct Plan {
tool_name: String,
root_idx: Option<usize>,
root_global_names: BTreeSet<String>,
}
impl Plan {
fn analyze(ir: &ToolDefinitionIr) -> Result<Self, Error> {
let tool_name = to_kebab_case(&ir.trait_ident.to_string());
let mut implicit: Vec<usize> = Vec::new();
for (idx, cmd) in ir.commands.iter().enumerate() {
if to_kebab_case(&cmd.method_ident.to_string()) == tool_name {
implicit.push(idx);
}
}
if implicit.len() > 1 {
let second = &ir.commands[implicit[1]];
return Err(Error::new(
second.method_ident.span(),
format!(
"multiple methods map to the tool's root command name `{tool_name}`; \
only one method may be the implicit-body handler (§5.8.1)"
),
));
}
let root_idx = implicit.first().copied();
if let Some(i) = root_idx {
let cmd = &ir.commands[i];
if cmd.subtree.is_some() {
return Err(Error::new(
cmd.method_ident.span(),
"the implicit-body method cannot also be a #[command(subtree = ...)]",
));
}
if let Some(name) = &cmd.name_override
&& name != &tool_name
{
return Err(Error::new(
cmd.method_ident.span(),
format!(
"the implicit-body method's #[command(name = {name:?})] diverges from the \
tool name {tool_name:?}; the root command name must equal the tool name (§5.8.1)"
),
));
}
}
for (idx, cmd) in ir.commands.iter().enumerate() {
if Some(idx) == root_idx {
continue;
}
let name = command_name(cmd, &tool_name, false);
if name == tool_name {
return Err(Error::new(
cmd.method_ident.span(),
format!(
"command `{name}` collides with the tool's root command name; \
rename the method or use #[command(name = ...)]"
),
));
}
if cmd.subtree.is_some() {
if cmd.annotations.is_some() {
return Err(Error::new(
cmd.method_ident.span(),
"annotations are not supported on a #[command(subtree = ...)] method \
(the model places annotations on a command body)",
));
}
if !cmd.constraints.is_empty() || cmd.result.is_some() {
return Err(Error::new(
cmd.method_ident.span(),
"#[constraint] / #[result] are not supported on a #[command(subtree = ...)] method",
));
}
}
}
let mut root_global_names = BTreeSet::new();
if let Some(i) = root_idx {
for param in &ir.commands[i].params {
let arg = arg_for(&ir.commands[i], ¶m.ident);
if arg.map(|a| a.placement) == Some(Some(ArgPlacement::Global)) {
root_global_names.insert(to_kebab_case(¶m.ident.to_string()));
if let Some(a) = arg {
for alias in &a.aliases {
root_global_names.insert(alias.clone());
}
}
}
}
}
Ok(Plan {
tool_name,
root_idx,
root_global_names,
})
}
}
fn command_name(cmd: &CommandIr, tool_name: &str, is_root: bool) -> String {
if is_root {
tool_name.to_string()
} else {
cmd.name_override
.clone()
.unwrap_or_else(|| to_kebab_case(&cmd.method_ident.to_string()))
}
}
fn arg_for<'a>(cmd: &'a CommandIr, ident: &Ident) -> Option<&'a ArgIr> {
cmd.args.iter().find(|a| &a.param == ident)
}
fn param_surface_names(ident: &Ident, arg: Option<&ArgIr>) -> Vec<String> {
let mut names = vec![to_kebab_case(&ident.to_string())];
if let Some(a) = arg {
names.extend(a.aliases.iter().cloned());
}
names
}
fn repeats_inherited_global(
ident: &Ident,
arg: Option<&ArgIr>,
inherited: &BTreeSet<String>,
) -> bool {
param_surface_names(ident, arg)
.iter()
.any(|n| inherited.contains(n))
}
fn build_root_node(
ir: &ToolDefinitionIr,
plan: &Plan,
repr: DescriptorRepr,
) -> Result<TokenStream, Error> {
if let Some(i) = plan.root_idx {
let cmd = &ir.commands[i];
build_command_node(cmd, &plan.tool_name, true, &BTreeSet::new(), repr)
} else {
let name = &plan.tool_name;
let doc = doc_tokens(&ir.doc);
Ok(quote! {
golem_rust::agentic::ExtendedCommandNode {
name: #name.to_string(),
aliases: ::std::vec::Vec::new(),
doc: #doc,
globals: golem_rust::agentic::ExtendedGlobals::default(),
subcommands: ::std::vec::Vec::new(),
body: ::std::option::Option::None,
}
})
}
}
fn build_leaf_link(
cmd: &CommandIr,
plan: &Plan,
repr: DescriptorRepr,
) -> Result<TokenStream, Error> {
let node = build_command_node(cmd, &plan.tool_name, false, &plan.root_global_names, repr)?;
Ok(quote! {
{
let __idx = commands.len() as i32;
commands.push(#node);
commands[0].subcommands.push(__idx);
}
})
}
fn build_subtree_link(
cmd: &CommandIr,
plan: &Plan,
repr: DescriptorRepr,
) -> Result<TokenStream, Error> {
let subtree = cmd.subtree.as_ref().expect("subtree present");
let call_path = subtree_call_path(&subtree.path)?;
let call = if repr == DescriptorRepr::Wire {
let mut path = subtree.path.clone();
path.segments.last_mut().unwrap().ident =
wire_descriptor_fn_ident(&path.segments.last().unwrap().ident);
quote! { #path(ctx, __golem_wire_schema) }
} else {
quote! { #call_path(ctx) }
};
let expected_name = command_name(cmd, &plan.tool_name, false);
let grafted_name = subtree
.name_override
.clone()
.unwrap_or_else(|| expected_name.clone());
let override_name = match &subtree.name_override {
Some(n) => quote! { ::std::option::Option::Some(#n.to_string()) },
None => quote! { ::std::option::Option::None },
};
let override_name_for_ctx = override_name.clone();
let override_doc = if cmd.doc == DocIr::default() {
quote! { ::std::option::Option::None }
} else {
let doc = doc_tokens(&cmd.doc);
quote! { ::std::option::Option::Some(#doc) }
};
let override_aliases = if cmd.aliases.is_empty() {
quote! { ::std::option::Option::None }
} else {
let aliases = cmd.aliases.iter().map(|a| quote! { #a.to_string() });
quote! { ::std::option::Option::Some(::std::vec![ #(#aliases),* ]) }
};
let mut opts = Vec::new();
let mut flags = Vec::new();
for param in &cmd.params {
let arg = arg_for(cmd, ¶m.ident);
match classify(¶m.ident, ¶m.ty, arg, true, false, repr)? {
Projection::Option(spec) => opts.push(spec),
Projection::Flag(spec) => flags.push(spec),
_ => {
return Err(Error::new(
param.ident.span(),
"a #[command(subtree = ...)] method parameter must project to a global option or flag",
));
}
}
}
Ok(quote! {
{
let __parent_globals = golem_rust::agentic::ExtendedGlobals {
options: ::std::vec![ #(#opts),* ],
flags: ::std::vec![ #(#flags),* ],
};
let mut __strict_ancestor_globals = ctx.inherited_globals().to_vec();
__strict_ancestor_globals.extend(
commands[0]
.globals
.options
.iter()
.cloned()
.map(golem_rust::agentic::EffectiveCommandField::Option),
);
__strict_ancestor_globals.extend(
commands[0]
.globals
.flags
.iter()
.cloned()
.map(golem_rust::agentic::EffectiveCommandField::Flag),
);
let __surviving_parent_globals = match golem_rust::agentic::reconcile_subtree_parent_globals(
__parent_globals.clone(),
&__strict_ancestor_globals,
#grafted_name,
) {
::std::result::Result::Ok(__globals) => __globals,
::std::result::Result::Err(__err) => {
if let ::std::result::Result::Err(__root_err @ golem_rust::agentic::ToolBuildError::SubtreeRootNameMismatch { .. }) = ctx.with_graft_root(
#expected_name.to_string(),
#override_name_for_ctx,
|ctx| #call,
) {
return ::std::result::Result::Err(__root_err);
}
return ::std::result::Result::Err(__err);
}
};
let mut __child_inherited_globals = __strict_ancestor_globals.clone();
__child_inherited_globals.extend(
__surviving_parent_globals
.options
.iter()
.cloned()
.map(golem_rust::agentic::EffectiveCommandField::Option),
);
__child_inherited_globals.extend(
__surviving_parent_globals
.flags
.iter()
.cloned()
.map(golem_rust::agentic::EffectiveCommandField::Flag),
);
let __child = ctx.with_graft_root(
#expected_name.to_string(),
#override_name_for_ctx,
|ctx| ctx.with_inherited_globals(__child_inherited_globals, |ctx| #call),
)?;
let __graft = golem_rust::agentic::graft_subtree(
__child,
#expected_name,
__parent_globals,
&__strict_ancestor_globals,
#override_name,
#override_doc,
#override_aliases,
::std::option::Option::None,
)?;
let __off = golem_rust::agentic::append_grafted_subtree(&mut commands, __graft);
commands[0].subcommands.push(__off);
}
})
}
fn subtree_call_path(path: &Path) -> Result<Path, Error> {
let mut rewritten = path.clone();
let last = rewritten.segments.last_mut().ok_or_else(|| {
Error::new(
path.span(),
"subtree path must name a #[tool_definition] trait",
)
})?;
if !matches!(last.arguments, PathArguments::None) {
return Err(Error::new(
path.span(),
"subtree path must not carry generic arguments",
));
}
last.ident = descriptor_fn_ident(&last.ident);
Ok(rewritten)
}
fn build_command_node(
cmd: &CommandIr,
tool_name: &str,
is_root: bool,
inherited_globals: &BTreeSet<String>,
repr: DescriptorRepr,
) -> Result<TokenStream, Error> {
let name = command_name(cmd, tool_name, is_root);
let doc = doc_tokens(&cmd.doc);
let aliases = cmd.aliases.iter().map(|a| quote! { #a.to_string() });
let mut global_options = Vec::new();
let mut global_flags = Vec::new();
let mut fixed = Vec::new();
let mut saw_optional_positional = false;
let mut tail: Option<TokenStream> = None;
let mut body_options = Vec::new();
let mut body_flags = Vec::new();
let mut stdin: Option<TokenStream> = None;
let mut stdout: Option<TokenStream> = None;
let mut stderr: Option<TokenStream> = None;
let mut body_option_decls: Vec<(usize, String)> = Vec::new();
let last_value_idx = cmd
.params
.iter()
.enumerate()
.rev()
.find_map(|(idx, param)| {
if is_auto_injected_principal_type(¶m.ty) {
return None;
}
let arg = arg_for(cmd, ¶m.ident);
if is_positional_candidate(param, arg) {
Some(idx)
} else {
None
}
});
let last_non_inherited_idx = cmd
.params
.iter()
.enumerate()
.rev()
.find_map(|(idx, param)| {
if is_auto_injected_principal_type(¶m.ty) {
return None;
}
let arg = arg_for(cmd, ¶m.ident);
let is_inherited =
!is_root && repeats_inherited_global(¶m.ident, arg, inherited_globals);
if is_positional_candidate(param, arg) && !is_inherited {
Some(idx)
} else {
None
}
});
for (idx, param) in cmd.params.iter().enumerate() {
if is_auto_injected_principal_type(¶m.ty) {
if let Some(arg) = arg_for(cmd, ¶m.ident) {
return Err(Error::new(
arg.param.span(),
"auto-injected Principal parameters cannot have #[arg] attributes because they are not part of the tool input schema",
));
}
continue;
}
let arg = arg_for(cmd, ¶m.ident);
let is_global = arg.map(|a| a.placement) == Some(Some(ArgPlacement::Global));
let is_inherited =
!is_root && repeats_inherited_global(¶m.ident, arg, inherited_globals);
let emit_as_tail = Some(idx) == last_value_idx
|| (Some(idx) == last_non_inherited_idx
&& last_non_inherited_idx != last_value_idx
&& arg.and_then(|a| a.placement).is_none()
&& vec_tail_representable(¶m.ty, arg));
match classify(¶m.ident, ¶m.ty, arg, is_global, emit_as_tail, repr)? {
Projection::Stdin(spec) => {
if stdin.is_some() {
return Err(Error::new(
param.ident.span(),
"duplicate stdin stream parameter",
));
}
stdin = Some(spec);
}
Projection::Stdout(spec) => {
if stdout.is_some() {
return Err(Error::new(
param.ident.span(),
"duplicate stdout stream parameter",
));
}
stdout = Some(spec);
}
Projection::Stderr(spec) => {
if stderr.is_some() {
return Err(Error::new(
param.ident.span(),
"duplicate stderr stream parameter",
));
}
stderr = Some(spec);
}
Projection::Option(spec) => {
if is_global {
global_options.push(spec);
} else {
body_option_decls.push((idx, to_kebab_case(¶m.ident.to_string())));
body_options.push(spec);
}
}
Projection::Flag(spec) => {
if is_global {
global_flags.push(spec);
} else {
body_flags.push(spec);
}
}
Projection::Positional { tokens, required } => {
if is_global {
return Err(Error::new(
param.ident.span(),
"a global argument cannot be a positional; use an option or flag",
));
}
if !is_inherited {
if tail.is_some() {
return Err(Error::new(
param.ident.span(),
"a fixed positional cannot appear after a tail positional; the tail positional must be the last positional",
));
}
if required && saw_optional_positional {
return Err(Error::new(
param.ident.span(),
"a required positional cannot appear after an optional positional; optional positionals must be trailing",
));
}
if !required {
saw_optional_positional = true;
}
}
fixed.push(tokens);
}
Projection::Tail(spec) => {
if is_global {
return Err(Error::new(
param.ident.span(),
"a global argument cannot be a tail positional",
));
}
if is_inherited {
let base = unwrap_generic1(¶m.ty, "Option").unwrap_or(¶m.ty);
let item = unwrap_generic1(base, "Vec").ok_or_else(|| {
Error::new(
param.ident.span(),
"a tail positional must be a `Vec<T>` parameter",
)
})?;
let name = to_kebab_case(¶m.ident.to_string());
body_option_decls.push((idx, name.clone()));
body_options.push(inherited_tail_option_surrogate_tokens(
&name, item, arg, repr,
)?);
continue;
}
if tail.is_some() {
return Err(Error::new(
param.ident.span(),
"a command may have at most one tail positional",
));
}
tail = Some(spec);
}
}
}
let constraints = build_constraints(cmd)?;
let (result_spec, errors) = build_result(cmd, repr)?;
let annotations = build_annotations(cmd.annotations.as_ref());
let positional_plan = build_positional_plan(cmd, &body_option_decls, repr)?;
let tail_tokens = match tail {
Some(t) => quote! { ::std::option::Option::Some(#t) },
None => quote! { ::std::option::Option::None },
};
let stdin_tokens = match stdin {
Some(s) => quote! { ::std::option::Option::Some(#s) },
None => quote! { ::std::option::Option::None },
};
let stdout_tokens = match stdout {
Some(s) => quote! { ::std::option::Option::Some(#s) },
None => quote! { ::std::option::Option::None },
};
let stderr_tokens = match stderr {
Some(s) => quote! { ::std::option::Option::Some(#s) },
None => quote! { ::std::option::Option::None },
};
let body = quote! {
golem_rust::agentic::ExtendedCommandBody {
positionals: golem_rust::agentic::ExtendedPositionals {
fixed: ::std::vec![ #(#fixed),* ],
tail: #tail_tokens,
},
options: ::std::vec![ #(#body_options),* ],
flags: ::std::vec![ #(#body_flags),* ],
constraints: ::std::vec![ #(#constraints),* ],
stdin: #stdin_tokens,
stdout: #stdout_tokens,
stderr: #stderr_tokens,
result: #result_spec,
errors: #errors,
annotations: #annotations,
positional_plan: ::std::vec![ #(#positional_plan),* ],
}
};
Ok(quote! {
golem_rust::agentic::ExtendedCommandNode {
name: #name.to_string(),
aliases: ::std::vec![ #(#aliases),* ],
doc: #doc,
globals: golem_rust::agentic::ExtendedGlobals {
options: ::std::vec![ #(#global_options),* ],
flags: ::std::vec![ #(#global_flags),* ],
},
subcommands: ::std::vec::Vec::new(),
body: ::std::option::Option::Some(#body),
}
})
}
fn build_positional_plan(
cmd: &CommandIr,
body_option_decls: &[(usize, String)],
repr: DescriptorRepr,
) -> Result<Vec<TokenStream>, Error> {
let mut plan = Vec::new();
for (idx, param) in cmd.params.iter().enumerate() {
let arg = arg_for(cmd, ¶m.ident);
if !is_positional_candidate(param, arg) {
continue;
}
let name = to_kebab_case(¶m.ident.to_string());
let optional_vec = unwrap_generic1(¶m.ty, "Option")
.is_some_and(|inner| unwrap_generic1(inner, "Vec").is_some());
let base = unwrap_generic1(¶m.ty, "Option").unwrap_or(¶m.ty);
let vec_item = unwrap_generic1(base, "Vec");
if arg.and_then(|a| a.placement) == Some(ArgPlacement::Positional) || vec_item.is_none() {
plan.push(quote! {
golem_rust::agentic::PositionalCandidate::Plain { name: #name.to_string() }
});
continue;
}
let explicit_tail = arg.and_then(|a| a.placement) == Some(ArgPlacement::Tail);
let has_min_or_max_attr = arg.is_some_and(|a| a.raw_min.is_some() || a.raw_max.is_some());
let later_option_names = body_option_decls
.iter()
.filter(|(decl_idx, _)| *decl_idx > idx)
.map(|(_, long)| quote! { #long.to_string() });
let explicit_tail_tok = if explicit_tail {
quote! { true }
} else {
quote! { false }
};
let optional_vec_tok = if optional_vec {
quote! { true }
} else {
quote! { false }
};
let has_min_or_max_tok = if has_min_or_max_attr {
quote! { true }
} else {
quote! { false }
};
let authored_tail_surrogate_tok = match (explicit_tail, vec_item) {
(true, Some(item)) => {
let tail_spec = tail_tokens(&name, item, arg, repr)?;
quote! { ::std::option::Option::Some(::std::boxed::Box::new(#tail_spec)) }
}
_ => quote! { ::std::option::Option::None },
};
plan.push(quote! {
golem_rust::agentic::PositionalCandidate::VecCandidate {
name: #name.to_string(),
explicit_tail: #explicit_tail_tok,
optional_vec: #optional_vec_tok,
has_min_or_max_attr: #has_min_or_max_tok,
authored_tail_surrogate: #authored_tail_surrogate_tok,
later_option_names: ::std::vec![ #(#later_option_names),* ],
}
});
}
Ok(plan)
}
fn vec_tail_representable(ty: &Type, arg: Option<&ArgIr>) -> bool {
if unwrap_generic1(ty, "Option").is_some() {
return false;
}
match unwrap_generic1(ty, "Vec") {
Some(item) => vec_tail_spec("__probe", item, false, arg).is_ok(),
None => false,
}
}
fn vec_tail_spec(
name: &str,
item: &Type,
optional: bool,
arg: Option<&ArgIr>,
) -> Result<TokenStream, Error> {
if optional {
return Err(Error::new(
arg.map(|a| a.param.span())
.unwrap_or_else(proc_macro2::Span::call_site),
"a tail positional must not be `Option<_>`; use `Vec<T>` (an empty tail already means none supplied)",
));
}
if let Some(arg) = arg {
reject_unconsumed_structural_attrs(arg, SurfaceKind::Tail)?;
}
tail_tokens(name, item, arg, DescriptorRepr::Dynamic)
}
fn is_positional_candidate(param: &crate::tool::ir::ParamIr, arg: Option<&ArgIr>) -> bool {
if is_stream_type(¶m.ty) || is_auto_injected_principal_type(¶m.ty) {
return false;
}
match arg.and_then(|a| a.placement) {
Some(ArgPlacement::Global | ArgPlacement::Option | ArgPlacement::Flag) => false,
Some(ArgPlacement::Positional | ArgPlacement::Tail) => true,
None => {
if arg.and_then(|a| a.sub_kind).is_some() {
return false;
}
let base = unwrap_generic1(¶m.ty, "Option").unwrap_or(¶m.ty);
if type_last_ident(base).as_deref() == Some("bool") {
return false;
}
if is_map_type(base) {
return false;
}
true
}
}
}
enum Projection {
Positional { tokens: TokenStream, required: bool },
Tail(TokenStream),
Option(TokenStream),
Flag(TokenStream),
Stdin(TokenStream),
Stdout(TokenStream),
Stderr(TokenStream),
}
pub(crate) fn client_surface_order(
ir: &ToolDefinitionIr,
cmd: &CommandIr,
param: &crate::tool::ir::ParamIr,
) -> Result<u8, Error> {
let plan = Plan::analyze(ir)?;
let is_root = to_kebab_case(&cmd.method_ident.to_string()) == plan.tool_name;
let arg = arg_for(cmd, ¶m.ident);
let global = arg.and_then(|arg| arg.placement) == Some(ArgPlacement::Global);
let last = cmd
.params
.iter()
.rev()
.find(|candidate| is_positional_candidate(candidate, arg_for(cmd, &candidate.ident)));
let last_non_inherited = cmd.params.iter().rev().find(|candidate| {
let candidate_arg = arg_for(cmd, &candidate.ident);
is_positional_candidate(candidate, candidate_arg)
&& (is_root
|| !repeats_inherited_global(
&candidate.ident,
candidate_arg,
&plan.root_global_names,
))
});
let tail = last.is_some_and(|candidate| candidate.ident == param.ident)
|| (last_non_inherited.is_some_and(|candidate| candidate.ident == param.ident)
&& arg.and_then(|arg| arg.placement).is_none()
&& vec_tail_representable(¶m.ty, arg));
Ok(
match classify(
¶m.ident,
¶m.ty,
arg,
global,
tail,
DescriptorRepr::Wire,
)? {
Projection::Option(_) if global => 0,
Projection::Flag(_) if global => 1,
Projection::Positional { .. } => 2,
Projection::Tail(_) => 3,
Projection::Option(_) => 4,
Projection::Flag(_) => 5,
Projection::Stdin(_) | Projection::Stdout(_) | Projection::Stderr(_) => 6,
},
)
}
pub(crate) fn canonical_field_has_option_carrier(
ir: &ToolDefinitionIr,
cmd: &CommandIr,
param: &crate::tool::ir::ParamIr,
) -> Result<bool, Error> {
let plan = Plan::analyze(ir)?;
let is_root = to_kebab_case(&cmd.method_ident.to_string()) == plan.tool_name;
let arg = arg_for(cmd, ¶m.ident);
let global = arg.and_then(|arg| arg.placement) == Some(ArgPlacement::Global);
let last = cmd
.params
.iter()
.rev()
.find(|candidate| is_positional_candidate(candidate, arg_for(cmd, &candidate.ident)));
let last_non_inherited = cmd.params.iter().rev().find(|candidate| {
let candidate_arg = arg_for(cmd, &candidate.ident);
is_positional_candidate(candidate, candidate_arg)
&& (is_root
|| !repeats_inherited_global(
&candidate.ident,
candidate_arg,
&plan.root_global_names,
))
});
let tail = last.is_some_and(|candidate| candidate.ident == param.ident)
|| (last_non_inherited.is_some_and(|candidate| candidate.ident == param.ident)
&& arg.and_then(|arg| arg.placement).is_none()
&& vec_tail_representable(¶m.ty, arg));
let projection = classify(
¶m.ident,
¶m.ty,
arg,
global,
tail,
DescriptorRepr::Wire,
)?;
Ok(match projection {
Projection::Option(_) => {
let (base, optional) = unwrap_generic1(¶m.ty, "Option")
.map(|inner| (inner, true))
.unwrap_or((¶m.ty, false));
let collection = unwrap_generic1(base, "Vec").is_some() || is_map_type(base);
let required = !optional && arg.and_then(|arg| arg.required).unwrap_or(false);
!collection && !required && arg.and_then(|arg| arg.default.as_ref()).is_none()
}
Projection::Positional { required, .. } => {
!required && arg.and_then(|arg| arg.default.as_ref()).is_none()
}
Projection::Tail(_)
| Projection::Flag(_)
| Projection::Stdin(_)
| Projection::Stdout(_)
| Projection::Stderr(_) => false,
})
}
#[derive(Clone, Copy)]
enum SurfaceKind {
Positional,
Tail,
OptionScalar,
OptionList,
OptionMap,
BoolFlag,
CountFlag,
Stream,
}
struct AllowedStructuralAttrs {
short: bool,
aliases: bool,
env: bool,
required: bool,
negatable: bool,
optional_scalar: bool,
repeatable: bool,
delim: bool,
default: bool,
separator: bool,
verbatim: bool,
accepts_stdio: bool,
value_name: bool,
}
impl SurfaceKind {
fn describe(self) -> &'static str {
match self {
SurfaceKind::Positional => "a positional",
SurfaceKind::Tail => "a tail positional",
SurfaceKind::OptionScalar => "a scalar option",
SurfaceKind::OptionList => "a repeatable list option",
SurfaceKind::OptionMap => "a map option",
SurfaceKind::BoolFlag => "a flag",
SurfaceKind::CountFlag => "a count flag",
SurfaceKind::Stream => "a stdin/stdout stream",
}
}
fn allowed_structural_attrs(self) -> AllowedStructuralAttrs {
let none = AllowedStructuralAttrs {
short: false,
aliases: false,
env: false,
required: false,
negatable: false,
optional_scalar: false,
repeatable: false,
delim: false,
default: false,
separator: false,
verbatim: false,
accepts_stdio: false,
value_name: false,
};
match self {
SurfaceKind::Positional => AllowedStructuralAttrs {
required: true,
default: true,
accepts_stdio: true,
value_name: true,
..none
},
SurfaceKind::Tail => AllowedStructuralAttrs {
separator: true,
verbatim: true,
accepts_stdio: true,
value_name: true,
..none
},
SurfaceKind::OptionScalar => AllowedStructuralAttrs {
short: true,
aliases: true,
env: true,
required: true,
optional_scalar: true,
default: true,
value_name: true,
..none
},
SurfaceKind::OptionList | SurfaceKind::OptionMap => AllowedStructuralAttrs {
short: true,
aliases: true,
env: true,
required: true,
repeatable: true,
delim: true,
default: true,
value_name: true,
..none
},
SurfaceKind::BoolFlag => AllowedStructuralAttrs {
short: true,
aliases: true,
env: true,
negatable: true,
default: true,
..none
},
SurfaceKind::CountFlag => AllowedStructuralAttrs {
short: true,
aliases: true,
env: true,
..none
},
SurfaceKind::Stream => none,
}
}
}
fn reject_unconsumed_structural_attrs(arg: &ArgIr, kind: SurfaceKind) -> Result<(), Error> {
let where_ = kind.describe();
let span = arg.param.span();
let allowed = kind.allowed_structural_attrs();
let check = |is_set: bool, ok: bool, field: &str| -> Result<(), Error> {
if is_set && !ok {
Err(Error::new(
span,
format!("`{field}` is not valid on {where_}"),
))
} else {
Ok(())
}
};
check(arg.short.is_some(), allowed.short, "short")?;
check(!arg.aliases.is_empty(), allowed.aliases, "aliases")?;
check(arg.env.is_some(), allowed.env, "env")?;
check(arg.required.is_some(), allowed.required, "required")?;
check(arg.negatable.is_some(), allowed.negatable, "negatable")?;
check(
arg.optional_scalar,
allowed.optional_scalar,
"optional_scalar",
)?;
check(arg.repeatable.is_some(), allowed.repeatable, "repeatable")?;
check(arg.delim.is_some(), allowed.delim, "delim")?;
check(arg.default.is_some(), allowed.default, "default")?;
check(arg.separator.is_some(), allowed.separator, "separator")?;
check(arg.verbatim, allowed.verbatim, "verbatim")?;
check(arg.accepts_stdio, allowed.accepts_stdio, "accepts_stdio")?;
check(arg.value_name.is_some(), allowed.value_name, "value_name")?;
Ok(())
}
fn reject_stream_attrs(arg: &ArgIr) -> Result<(), Error> {
let span = arg.param.span();
if arg.placement.is_some() {
return Err(Error::new(
span,
"an explicit placement is not valid on a stdin/stdout stream parameter",
));
}
if arg.sub_kind.is_some() {
return Err(Error::new(
span,
"`kind = \"flag\"` / `\"count-flag\"` is not valid on a stdin/stdout stream parameter",
));
}
reject_text_path_url_refinements(arg, "a stdin/stdout stream")?;
if arg.bounds.is_some() || arg.unit.is_some() || arg.raw_min.is_some() || arg.raw_max.is_some()
{
return Err(Error::new(
span,
"numeric refinements (`min`/`max`/`bounds`/`unit`) are not valid on a stdin/stdout stream",
));
}
reject_unconsumed_structural_attrs(arg, SurfaceKind::Stream)
}
fn classify(
ident: &Ident,
ty: &Type,
arg: Option<&ArgIr>,
is_global: bool,
emit_as_tail: bool,
repr: DescriptorRepr,
) -> Result<Projection, Error> {
let name = to_kebab_case(&ident.to_string());
if let Some((kind, required)) = stream_type(ty) {
if let Some(arg) = arg {
reject_stream_attrs(arg)?;
}
return Ok(match kind {
StreamKind::Input => {
if arg.is_some_and(|arg| arg.output_channel.is_some()) {
return Err(Error::new(
ident.span(),
"channel selectors are only valid on output stream parameters",
));
}
Projection::Stdin(stream_spec_tokens(arg, required))
}
StreamKind::Output => match arg.and_then(|arg| arg.output_channel) {
Some(OutputChannelIr::Stderr) => {
Projection::Stderr(stream_spec_tokens(arg, required))
}
Some(OutputChannelIr::Stdout) | None => {
Projection::Stdout(stream_spec_tokens(arg, required))
}
},
});
}
if arg.is_some_and(|arg| arg.output_channel.is_some()) {
return Err(Error::new(
ident.span(),
"channel selectors are only valid on output stream parameters",
));
}
let (base_ty, optional) = match unwrap_generic1(ty, "Option") {
Some(inner) => (inner, true),
None => (ty, false),
};
let placement = arg.and_then(|a| a.placement);
let sub_kind = arg.and_then(|a| a.sub_kind);
if is_global
&& matches!(
placement,
Some(ArgPlacement::Positional | ArgPlacement::Tail)
)
{
return Err(Error::new(
ident.span(),
"a global parameter cannot be a positional or tail; globals must be options or flags",
));
}
let surface = match placement {
Some(ArgPlacement::Global) => None,
other => other,
};
if let (Some(p), Some(k)) = (surface, sub_kind)
&& matches!(
p,
ArgPlacement::Option | ArgPlacement::Positional | ArgPlacement::Tail
)
&& matches!(k, ArgSubKind::Flag | ArgSubKind::CountFlag)
{
return Err(Error::new(
ident.span(),
"a flag kind (`kind = \"flag\"` / `\"count-flag\"`) cannot be combined with an explicit option/positional/tail placement",
));
}
let is_bool = type_last_ident(base_ty).as_deref() == Some("bool");
let vec_item = unwrap_generic1(base_ty, "Vec");
let map_ty = if is_map_type(base_ty) {
Some(base_ty)
} else {
None
};
let is_flag = matches!(surface, Some(ArgPlacement::Flag))
|| matches!(sub_kind, Some(ArgSubKind::Flag | ArgSubKind::CountFlag))
|| (surface.is_none() && is_bool);
if is_flag {
if optional {
return Err(Error::new(
ident.span(),
"a flag parameter must not be `Option<_>`: flags are always present (a bool flag has a default, a count flag counts occurrences), so optionality has no canonical representation; use the bare type (`bool` / `u32`)",
));
}
if matches!(sub_kind, Some(ArgSubKind::CountFlag)) {
if type_last_ident(base_ty).as_deref() != Some("u32") {
return Err(Error::new(
ident.span(),
"a count flag parameter must be `u32`: count flags are exposed as a `u32` canonical input field, so any other type would make the metadata disagree with the implementation signature",
));
}
} else if !is_bool {
return Err(Error::new(
ident.span(),
"a flag parameter must be `bool` (or `Option<bool>`); for a count flag use `kind = \"count-flag\"` with an integer type",
));
}
if let Some(arg) = arg {
let kind = if matches!(sub_kind, Some(ArgSubKind::CountFlag)) {
SurfaceKind::CountFlag
} else {
SurfaceKind::BoolFlag
};
reject_unconsumed_structural_attrs(arg, kind)?;
}
return Ok(Projection::Flag(flag_spec_tokens(&name, base_ty, arg)?));
}
let infer_tail = surface.is_none() && !is_global && vec_item.is_some() && emit_as_tail;
if matches!(surface, Some(ArgPlacement::Tail)) || infer_tail {
if optional {
return Err(Error::new(
ident.span(),
"a tail positional must not be `Option<_>`: a tail positional is already variadic (zero or more) and has no representation for an additional optional/absent state, so the `Option` wrapper would be silently dropped; use `Vec<T>` (an empty tail already means none supplied)",
));
}
let item = vec_item.ok_or_else(|| {
Error::new(
ident.span(),
"a tail positional must be a `Vec<T>` parameter",
)
})?;
if let Some(arg) = arg {
reject_unconsumed_structural_attrs(arg, SurfaceKind::Tail)?;
}
return Ok(Projection::Tail(tail_tokens(&name, item, arg, repr)?));
}
let is_option = matches!(surface, Some(ArgPlacement::Option))
|| is_global
|| (surface.is_none() && (vec_item.is_some() || map_ty.is_some()));
if is_option {
if let Some(arg) = arg {
let kind = if vec_item.is_some() {
SurfaceKind::OptionList
} else if map_ty.is_some() {
SurfaceKind::OptionMap
} else {
SurfaceKind::OptionScalar
};
reject_unconsumed_structural_attrs(arg, kind)?;
}
let spec = option_spec_tokens(&name, base_ty, vec_item, map_ty, optional, arg, repr)?;
return Ok(Projection::Option(spec));
}
if let Some(arg) = arg {
reject_unconsumed_structural_attrs(arg, SurfaceKind::Positional)?;
}
let required = !optional && arg.and_then(|a| a.required).unwrap_or(true);
Ok(Projection::Positional {
tokens: positional_tokens(&name, base_ty, optional, arg, repr)?,
required,
})
}
fn stream_spec_tokens(arg: Option<&ArgIr>, required: bool) -> TokenStream {
let doc = arg_doc_tokens(arg);
quote! {
golem_rust::agentic::StreamSpec {
doc: #doc,
mime: ::std::vec::Vec::new(),
required: #required,
}
}
}
fn flag_spec_tokens(name: &str, base_ty: &Type, arg: Option<&ArgIr>) -> Result<TokenStream, Error> {
let doc = arg_doc_tokens(arg);
let short = opt_char(arg.and_then(|a| a.short));
let aliases = alias_tokens(arg);
let env_var = opt_str(arg.and_then(|a| a.env.as_ref()));
let is_count = matches!(arg.and_then(|a| a.sub_kind), Some(ArgSubKind::CountFlag));
if let Some(arg) = arg {
reject_flag_value_refinements(arg, is_count)?;
}
let shape = if is_count {
let max = match arg.and_then(|a| a.raw_max.as_ref()) {
Some(expr) => quote! { ::std::option::Option::Some({ let __m: u32 = #expr; __m }) },
None => quote! { ::std::option::Option::None },
};
quote! { golem_rust::agentic::FlagShape::CountFlag(#max) }
} else {
let default = match arg.and_then(|a| a.default.as_ref()) {
Some(expr) => bool_default(expr)?,
None => quote! { false },
};
let negatable = arg.and_then(|a| a.negatable).unwrap_or(false);
quote! {
golem_rust::agentic::FlagShape::BoolFlag(
golem_rust::agentic::BoolFlagShape {
default: #default,
negatable: #negatable,
}
)
}
};
let _ = base_ty;
Ok(quote! {
golem_rust::agentic::FlagSpec {
long: #name.to_string(),
short: #short,
aliases: #aliases,
doc: #doc,
shape: #shape,
env_var: #env_var,
}
})
}
fn reject_text_path_url_refinements(arg: &ArgIr, context: &str) -> Result<(), Error> {
let span = arg.param.span();
if arg.regex.is_some() || arg.min_length.is_some() || arg.max_length.is_some() {
return Err(Error::new(
span,
format!(
"text refinements (`regex`/`min_length`/`max_length`) are not valid on {context}"
),
));
}
if arg.path_kind.is_some() || arg.direction.is_some() || arg.mime.is_some() {
return Err(Error::new(
span,
format!("path refinements (`kind`/`direction`/`mime`) are not valid on {context}"),
));
}
if arg.schemes.is_some() {
return Err(Error::new(
span,
format!("url refinements (`schemes`) are not valid on {context}"),
));
}
Ok(())
}
fn reject_flag_value_refinements(arg: &ArgIr, is_count: bool) -> Result<(), Error> {
reject_text_path_url_refinements(arg, "a flag")?;
let span = arg.param.span();
if arg.bounds.is_some() || arg.unit.is_some() {
return Err(Error::new(
span,
"numeric refinements (`bounds`/`unit`) are not valid on a flag",
));
}
if arg.raw_min.is_some() {
return Err(Error::new(span, "`min` is not valid on a flag"));
}
if arg.raw_max.is_some() && !is_count {
return Err(Error::new(
span,
"`max` is only valid on a count flag (`kind = \"count-flag\"`)",
));
}
Ok(())
}
fn reject_map_value_refinements(arg: &ArgIr) -> Result<(), Error> {
reject_text_path_url_refinements(arg, "a map option")?;
if arg.raw_min.is_some() || arg.raw_max.is_some() || arg.bounds.is_some() || arg.unit.is_some()
{
return Err(Error::new(
arg.param.span(),
"numeric refinements (`min`/`max`/`bounds`/`unit`) are not valid on a map option",
));
}
Ok(())
}
fn bool_default(expr: &Expr) -> Result<TokenStream, Error> {
match expr {
Expr::Lit(syn::ExprLit {
lit: syn::Lit::Bool(b),
..
}) => {
let v = b.value;
Ok(quote! { #v })
}
Expr::Paren(p) => bool_default(&p.expr),
Expr::Group(g) => bool_default(&g.expr),
other => Err(Error::new(
other.span(),
"a flag default must be a boolean literal (`true` or `false`)",
)),
}
}
fn option_spec_tokens(
name: &str,
base_ty: &Type,
vec_item: Option<&Type>,
map_ty: Option<&Type>,
optional: bool,
arg: Option<&ArgIr>,
repr: DescriptorRepr,
) -> Result<TokenStream, Error> {
let doc = arg_doc_tokens(arg);
let short = opt_char(arg.and_then(|a| a.short));
let aliases = alias_tokens(arg);
let value_name = opt_str(arg.and_then(|a| a.value_name.as_ref()));
let env_var = opt_str(arg.and_then(|a| a.env.as_ref()));
let position = format!("option --{name}");
let shape = if let Some(item) = vec_item {
let graph = value_graph_tokens(item, arg, MinMaxRole::Bound, &position, repr)?;
let rep = repetition_tokens(arg)?;
quote! {
golem_rust::agentic::ExtendedOptionShape::RepeatableList(
golem_rust::agentic::ExtendedRepeatableListShape {
repetition: #rep,
item_type: #graph,
}
)
}
} else if let Some(map) = map_ty {
if let Some(arg) = arg {
reject_map_value_refinements(arg)?;
}
let graph = value_graph_tokens(map, arg, MinMaxRole::Forbidden, &position, repr)?;
let rep = repetition_tokens(arg)?;
quote! {
golem_rust::agentic::ExtendedOptionShape::RepeatableMap(
golem_rust::agentic::ExtendedRepeatableMapShape {
repetition: #rep,
map_type: #graph,
duplicate_key_policy:
golem_rust::agentic::DuplicateKeyPolicy::Reject,
}
)
}
} else {
let graph = value_graph_tokens(base_ty, arg, MinMaxRole::Bound, &position, repr)?;
if arg.map(|a| a.optional_scalar).unwrap_or(false) {
quote! { golem_rust::agentic::ExtendedOptionShape::OptionalScalar(#graph) }
} else {
quote! { golem_rust::agentic::ExtendedOptionShape::Scalar(#graph) }
}
};
let required = !optional && arg.and_then(|a| a.required).unwrap_or(false);
let default = match arg.and_then(|a| a.default.as_ref()) {
Some(expr) if repr == DescriptorRepr::Wire => {
let lit = tool_literal_tokens(expr)?;
quote! { Some(golem_rust::agentic::option_collected_graph(&__shape).literal(&#lit)?) }
}
Some(expr) => {
let lit = tool_literal_tokens(expr)?;
quote! {
::std::option::Option::Some(golem_rust::agentic::literal_to_schema_value(
&golem_rust::agentic::option_collected_graph(&__shape),
&#lit,
)?)
}
}
None => quote! { ::std::option::Option::None },
};
Ok(quote! {
{
let __shape = #shape;
let __default = #default;
golem_rust::agentic::ExtendedOptionSpec {
long: #name.to_string(),
short: #short,
aliases: #aliases,
doc: #doc,
value_name: #value_name,
shape: __shape,
default: __default,
required: #required,
env_var: #env_var,
}
}
})
}
fn positional_tokens(
name: &str,
base_ty: &Type,
optional: bool,
arg: Option<&ArgIr>,
repr: DescriptorRepr,
) -> Result<TokenStream, Error> {
let doc = arg_doc_tokens(arg);
let value_name = opt_str(arg.and_then(|a| a.value_name.as_ref()));
let graph = value_graph_tokens(
base_ty,
arg,
MinMaxRole::Bound,
&format!("positional {name}"),
repr,
)?;
let accepts_stdio = arg.map(|a| a.accepts_stdio).unwrap_or(false);
let required = !optional && arg.and_then(|a| a.required).unwrap_or(true);
let default = match arg.and_then(|a| a.default.as_ref()) {
Some(expr) if repr == DescriptorRepr::Wire => {
let lit = tool_literal_tokens(expr)?;
quote! { Some(__type.literal(&#lit)?) }
}
Some(expr) => {
let lit = tool_literal_tokens(expr)?;
quote! {
::std::option::Option::Some(golem_rust::agentic::literal_to_schema_value(
&__type, &#lit,
)?)
}
}
None => quote! { ::std::option::Option::None },
};
Ok(quote! {
{
let __type = #graph;
let __default = #default;
golem_rust::agentic::ExtendedPositional {
name: #name.to_string(),
doc: #doc,
value_name: #value_name,
type_: __type,
default: __default,
required: #required,
accepts_stdio: #accepts_stdio,
}
}
})
}
fn tail_tokens(
name: &str,
item: &Type,
arg: Option<&ArgIr>,
repr: DescriptorRepr,
) -> Result<TokenStream, Error> {
let doc = arg_doc_tokens(arg);
let value_name = opt_str(arg.and_then(|a| a.value_name.as_ref()));
let graph = value_graph_tokens(
item,
arg,
MinMaxRole::Occurrence,
&format!("tail positional {name}"),
repr,
)?;
let min = match arg.and_then(|a| a.raw_min.as_ref()) {
Some(expr) => quote! { { let __m: u32 = #expr; __m } },
None => quote! { 0u32 },
};
let max = match arg.and_then(|a| a.raw_max.as_ref()) {
Some(expr) => quote! { ::std::option::Option::Some({ let __m: u32 = #expr; __m }) },
None => quote! { ::std::option::Option::None },
};
let separator = opt_str(arg.and_then(|a| a.separator.as_ref()));
let verbatim = arg.map(|a| a.verbatim).unwrap_or(false);
let accepts_stdio = arg.map(|a| a.accepts_stdio).unwrap_or(false);
Ok(quote! {
golem_rust::agentic::ExtendedTailPositional {
name: #name.to_string(),
doc: #doc,
value_name: #value_name,
item_type: #graph,
min: #min,
max: #max,
separator: #separator,
verbatim: #verbatim,
accepts_stdio: #accepts_stdio,
}
})
}
fn inherited_tail_option_surrogate_tokens(
name: &str,
item: &Type,
arg: Option<&ArgIr>,
repr: DescriptorRepr,
) -> Result<TokenStream, Error> {
let doc = arg_doc_tokens(arg);
let aliases = alias_tokens(arg);
let value_name = opt_str(arg.and_then(|a| a.value_name.as_ref()));
let graph = value_graph_tokens(
item,
arg,
MinMaxRole::Occurrence,
&format!("inherited tail positional {name}"),
repr,
)?;
Ok(quote! {
golem_rust::agentic::ExtendedOptionSpec {
long: #name.to_string(),
short: ::std::option::Option::None,
aliases: #aliases,
doc: #doc,
value_name: #value_name,
shape: golem_rust::agentic::ExtendedOptionShape::RepeatableList(
golem_rust::agentic::ExtendedRepeatableListShape {
repetition:
golem_rust::agentic::Repetition::Repeated,
item_type: #graph,
}
),
default: ::std::option::Option::None,
required: false,
env_var: ::std::option::Option::None,
}
})
}
fn repetition_tokens(arg: Option<&ArgIr>) -> Result<TokenStream, Error> {
let mode = arg
.and_then(|a| a.repeatable)
.unwrap_or(RepeatableMode::Repeated);
let delim = arg.and_then(|a| a.delim);
match mode {
RepeatableMode::Repeated => {
if delim.is_some() {
return Err(Error::new(
arg.map(|a| a.param.span())
.unwrap_or_else(proc_macro2::Span::call_site),
"`delim` requires `repeatable = \"delimited\"` or `repeatable = \"either\"`",
));
}
Ok(quote! { golem_rust::agentic::Repetition::Repeated })
}
RepeatableMode::Delimited => {
let d = delim.ok_or_else(|| {
Error::new(
proc_macro2::Span::call_site(),
"repeatable = \"delimited\" requires a `delim = '<char>'`",
)
})?;
Ok(quote! { golem_rust::agentic::Repetition::Delimited(#d) })
}
RepeatableMode::Either => {
let d = delim.ok_or_else(|| {
Error::new(
proc_macro2::Span::call_site(),
"repeatable = \"either\" requires a `delim = '<char>'`",
)
})?;
Ok(quote! { golem_rust::agentic::Repetition::Either(#d) })
}
}
}
#[derive(Clone, Copy)]
enum MinMaxRole {
Bound,
Occurrence,
Forbidden,
}
fn value_graph_tokens(
inner_ty: &Type,
arg: Option<&ArgIr>,
min_max: MinMaxRole,
position: &str,
repr: DescriptorRepr,
) -> Result<TokenStream, Error> {
let base = if repr == DescriptorRepr::Wire {
quote! { __golem_wire_schema.schema::<#inner_ty>() }
} else {
quote! {{
use golem_rust::agentic::SelectToolSchemaChecks as _;
(&golem_rust::agentic::ToolSchemaProbe::<#inner_ty>(::std::marker::PhantomData))
.tool_value_schema(|| golem_rust::agentic::tool_value_schema::<#inner_ty>(#position))?
}}
};
let Some(arg) = arg else {
return Ok(base);
};
let mut steps = Vec::new();
if arg.regex.is_some() || arg.min_length.is_some() || arg.max_length.is_some() {
if is_known_non_text(inner_ty) {
return Err(Error::new(
arg.param.span(),
"text refinements (`regex`/`min_length`/`max_length`) require a text-typed parameter",
));
}
let regex = opt_str(arg.regex.as_ref());
let min_len = opt_u32(arg.min_length);
let max_len = opt_u32(arg.max_length);
steps.push(if repr == DescriptorRepr::Wire {
quote! {
__g = __g.refine_text(#regex, #min_len, #max_len)?;
}
} else {
quote! {
__g.root = golem_rust::agentic::refine_text(__g.root, #regex, #min_len, #max_len)?;
}
});
}
if arg.path_kind.is_some() || arg.direction.is_some() || arg.mime.is_some() {
if is_known_non_path(inner_ty) {
return Err(Error::new(
arg.param.span(),
"path refinements (`kind`/`direction`/`mime`) require a path-typed parameter",
));
}
let direction = opt_direction(arg.direction);
let kind = opt_path_kind(arg.path_kind);
let mime = opt_str_vec(arg.mime.as_ref());
steps.push(if repr == DescriptorRepr::Wire {
quote! {
__g = __g.refine_path(#direction, #kind, #mime)?;
}
} else {
quote! {
__g.root = golem_rust::agentic::refine_path(__g.root, #direction, #kind, #mime)?;
}
});
}
if arg.schemes.is_some() {
if is_known_non_url(inner_ty) {
return Err(Error::new(
arg.param.span(),
"url refinements (`schemes`) require a url-typed parameter",
));
}
let schemes = opt_str_vec(arg.schemes.as_ref());
steps.push(if repr == DescriptorRepr::Wire {
quote! {
__g = __g.refine_url(#schemes)?;
}
} else {
quote! {
__g.root = golem_rust::agentic::refine_url(__g.root, #schemes)?;
}
});
}
let (min_max_are_bounds, numeric_allowed) = match min_max {
MinMaxRole::Bound => (true, true),
MinMaxRole::Occurrence => (false, true),
MinMaxRole::Forbidden => (false, false),
};
let slot_min = min_max_are_bounds.then_some(arg.raw_min.as_ref()).flatten();
let slot_max = min_max_are_bounds.then_some(arg.raw_max.as_ref()).flatten();
if numeric_allowed
&& (arg.bounds.is_some() || arg.unit.is_some() || slot_min.is_some() || slot_max.is_some())
{
if is_known_non_numeric(inner_ty) {
return Err(Error::new(
arg.param.span(),
"numeric refinements (`min`/`max`/`bounds`/`unit`) require a numeric parameter",
));
}
if arg.bounds.is_some() && (slot_min.is_some() || slot_max.is_some()) {
return Err(Error::new(
arg.param.span(),
"use either `bounds = (min, max)` or `min`/`max`, not both",
));
}
let (min, max) = if let Some((lo, hi)) = &arg.bounds {
let lo_b = numeric_bound(inner_ty, lo);
let hi_b = numeric_bound(inner_ty, hi);
(
quote! { ::std::option::Option::Some(#lo_b) },
quote! { ::std::option::Option::Some(#hi_b) },
)
} else {
let min = match slot_min {
Some(e) => {
let b = numeric_bound(inner_ty, e);
quote! { ::std::option::Option::Some(#b) }
}
None => quote! { ::std::option::Option::None },
};
let max = match slot_max {
Some(e) => {
let b = numeric_bound(inner_ty, e);
quote! { ::std::option::Option::Some(#b) }
}
None => quote! { ::std::option::Option::None },
};
(min, max)
};
let unit = opt_str(arg.unit.as_ref());
steps.push(if repr == DescriptorRepr::Wire {
quote! {
__g = __g.refine_numeric(#min, #max, #unit)?;
}
} else {
quote! {
__g.root = golem_rust::agentic::refine_numeric(__g.root, #min, #max, #unit)?;
}
});
}
if steps.is_empty() {
Ok(base)
} else {
Ok(quote! {
{
let mut __g = #base;
#(#steps)*
__g
}
})
}
}
fn numeric_bound(inner_ty: &Type, expr: &Expr) -> TokenStream {
quote! {
{
let __v: #inner_ty = #expr;
golem_rust::agentic::IntoNumericBound::into_numeric_bound(__v)?
}
}
}
fn build_constraints(cmd: &CommandIr) -> Result<Vec<TokenStream>, Error> {
cmd.constraints.iter().map(constraint_tokens).collect()
}
fn constraint_tokens(c: &ConstraintIr) -> Result<TokenStream, Error> {
Ok(match c {
ConstraintIr::RequiresAll(refs) => {
let r = refs_tokens(refs)?;
quote! { golem_rust::agentic::ExtendedConstraint::RequiresAll(#r) }
}
ConstraintIr::AllOrNone(refs) => {
let r = refs_tokens(refs)?;
quote! { golem_rust::agentic::ExtendedConstraint::AllOrNone(#r) }
}
ConstraintIr::RequiresAny(refs) => {
let r = refs_tokens(refs)?;
quote! { golem_rust::agentic::ExtendedConstraint::RequiresAny(#r) }
}
ConstraintIr::MutexGroups(groups) => {
let gs = groups
.iter()
.map(|g| {
let r = refs_tokens(g)?;
Ok(quote! { golem_rust::agentic::ExtendedRefGroup { refs: #r } })
})
.collect::<Result<Vec<_>, Error>>()?;
quote! { golem_rust::agentic::ExtendedConstraint::MutexGroups(::std::vec![ #(#gs),* ]) }
}
ConstraintIr::Implies {
lhs_quant,
lhs,
rhs_quant,
rhs,
} => {
let lq = quantifier_tokens(*lhs_quant);
let l = refs_tokens(lhs)?;
let rq = quantifier_tokens(*rhs_quant);
let r = refs_tokens(rhs)?;
quote! {
golem_rust::agentic::ExtendedConstraint::Implies(golem_rust::agentic::ExtendedImpliesC {
lhs_quant: #lq,
lhs: #l,
rhs_quant: #rq,
rhs: #r,
})
}
}
ConstraintIr::Forbids {
lhs_quant,
lhs,
rhs,
} => {
let lq = quantifier_tokens(*lhs_quant);
let l = refs_tokens(lhs)?;
let r = refs_tokens(rhs)?;
quote! {
golem_rust::agentic::ExtendedConstraint::Forbids(golem_rust::agentic::ExtendedForbidsC {
lhs_quant: #lq,
lhs: #l,
rhs: #r,
})
}
}
})
}
fn refs_tokens(refs: &[RefIr]) -> Result<TokenStream, Error> {
let items = refs
.iter()
.map(ref_tokens)
.collect::<Result<Vec<_>, Error>>()?;
Ok(quote! { ::std::vec![ #(#items),* ] })
}
fn ref_tokens(r: &RefIr) -> Result<TokenStream, Error> {
match r {
RefIr::Present(name) => {
Ok(quote! { golem_rust::agentic::ExtendedRef::Present(#name.to_string()) })
}
RefIr::ValueIs { name, value } => {
let lit = tool_literal_tokens(value)?;
Ok(quote! {
golem_rust::agentic::ExtendedRef::ValueIs(golem_rust::agentic::ExtendedValueIsRef {
name: #name.to_string(),
value: golem_rust::agentic::ExtendedValueIsLiteral::Deferred(#lit),
})
})
}
}
}
fn quantifier_tokens(q: QuantifierIr) -> TokenStream {
match q {
QuantifierIr::All => {
quote! { golem_rust::agentic::Quantifier::All }
}
QuantifierIr::Any => {
quote! { golem_rust::agentic::Quantifier::Any }
}
}
}
fn build_result(
cmd: &CommandIr,
repr: DescriptorRepr,
) -> Result<(TokenStream, TokenStream), Error> {
let (ok_ty, err_ty) = split_result(&cmd.output);
let errors = match err_ty {
Some(e) if repr == DescriptorRepr::Wire => quote! {
__golem_wire_schema.error_cases(__golem_wire_schema.with_builder(
<#e as golem_rust::agentic::DirectToolError>::wire_error_cases
))
},
Some(e) => quote! { <#e as golem_rust::agentic::ToolErrorSchema>::error_cases()? },
None => quote! { ::std::vec::Vec::new() },
};
let result_spec = match ok_ty {
Some(t) => {
let graph = value_graph_tokens(t, None, MinMaxRole::Forbidden, "result", repr)?;
let (formatters, default_formatter) = build_formatters(cmd.result.as_ref());
let empty_doc = doc_tokens(&DocIr::default());
quote! {
::std::option::Option::Some(golem_rust::agentic::ExtendedResultSpec {
type_: #graph,
doc: #empty_doc,
formatters: #formatters,
default_formatter: #default_formatter,
})
}
}
None => {
if cmd.result.is_some() {
return Err(Error::new(
cmd.method_ident.span(),
"#[result(...)] is not valid on a method with a unit `()` success type: \
there is no result value to format",
));
}
quote! { ::std::option::Option::None }
}
};
Ok((result_spec, errors))
}
fn build_formatters(result: Option<&ResultIr>) -> (TokenStream, TokenStream) {
let formatters: Vec<String> = result.map(|r| r.formatters.clone()).unwrap_or_default();
let explicit_default = result.and_then(|r| r.default_formatter.clone());
if formatters.is_empty() {
let empty_doc = doc_tokens(&DocIr::default());
let f = quote! {
::std::vec![ golem_rust::agentic::ToolFormatter {
name: "default".to_string(),
doc: #empty_doc,
} ]
};
let d = explicit_default.unwrap_or_else(|| "default".to_string());
return (f, quote! { #d.to_string() });
}
let default = explicit_default.unwrap_or_else(|| formatters[0].clone());
let items = formatters.iter().map(|name| {
let empty_doc = doc_tokens(&DocIr::default());
quote! {
golem_rust::agentic::ToolFormatter {
name: #name.to_string(),
doc: #empty_doc,
}
}
});
(
quote! { ::std::vec![ #(#items),* ] },
quote! { #default.to_string() },
)
}
fn build_annotations(ann: Option<&CommandAnnotationsIr>) -> TokenStream {
match ann {
None => quote! { ::std::option::Option::None },
Some(a) => {
let read_only = a.read_only.unwrap_or(false);
let destructive = a.destructive.unwrap_or(true);
let idempotent = a.idempotent.unwrap_or(false);
let open_world = a.open_world.unwrap_or(true);
quote! {
::std::option::Option::Some(golem_rust::agentic::CommandAnnotations {
read_only: #read_only,
destructive: #destructive,
idempotent: #idempotent,
open_world: #open_world,
})
}
}
}
}
fn tool_literal_tokens(expr: &Expr) -> Result<TokenStream, Error> {
match expr {
Expr::Lit(syn::ExprLit { lit, .. }) => lit_tokens(lit, expr.span(), false),
Expr::Unary(u) if matches!(u.op, syn::UnOp::Neg(_)) => {
if let Expr::Lit(syn::ExprLit { lit, .. }) = &*u.expr {
lit_tokens(lit, expr.span(), true)
} else {
Err(Error::new(expr.span(), "unsupported negated literal"))
}
}
Expr::Group(g) => tool_literal_tokens(&g.expr),
Expr::Paren(p) => tool_literal_tokens(&p.expr),
Expr::Array(a) => array_tokens(&a.elems, expr.span()),
other => Err(Error::new(
other.span(),
"unsupported metadata literal for a default / value_is",
)),
}
}
fn lit_tokens(lit: &syn::Lit, span: proc_macro2::Span, negate: bool) -> Result<TokenStream, Error> {
match lit {
syn::Lit::Bool(b) => {
let v = b.value;
Ok(quote! { golem_rust::agentic::ToolLiteral::Bool(#v) })
}
syn::Lit::Str(s) => {
let v = s.value();
Ok(quote! { golem_rust::agentic::ToolLiteral::Str(#v.to_string()) })
}
syn::Lit::Char(c) => {
let v = c.value();
Ok(quote! { golem_rust::agentic::ToolLiteral::Char(#v) })
}
syn::Lit::Int(i) => {
let value: i128 = i
.base10_parse::<i128>()
.map_err(|e| Error::new(span, e.to_string()))?;
let value = if negate { -value } else { value };
Ok(quote! { golem_rust::agentic::ToolLiteral::Int(#value) })
}
syn::Lit::Float(f) => {
let value: f64 = f
.base10_parse::<f64>()
.map_err(|e| Error::new(span, e.to_string()))?;
let value = if negate { -value } else { value };
Ok(quote! { golem_rust::agentic::ToolLiteral::Float(#value) })
}
other => Err(Error::new(span, format!("unsupported literal {other:?}"))),
}
}
fn array_tokens(
elems: &syn::punctuated::Punctuated<Expr, syn::Token![,]>,
span: proc_macro2::Span,
) -> Result<TokenStream, Error> {
let all_pairs = !elems.is_empty()
&& elems
.iter()
.all(|e| matches!(e, Expr::Tuple(t) if t.elems.len() == 2));
if all_pairs {
let entries = elems
.iter()
.map(|e| {
let Expr::Tuple(t) = e else { unreachable!() };
let mut it = t.elems.iter();
let k = tool_literal_tokens(it.next().unwrap())?;
let v = tool_literal_tokens(it.next().unwrap())?;
Ok(quote! { (#k, #v) })
})
.collect::<Result<Vec<_>, Error>>()?;
Ok(quote! { golem_rust::agentic::ToolLiteral::Map(::std::vec![ #(#entries),* ]) })
} else {
let items = elems
.iter()
.map(tool_literal_tokens)
.collect::<Result<Vec<_>, Error>>()?;
let _ = span;
Ok(quote! { golem_rust::agentic::ToolLiteral::List(::std::vec![ #(#items),* ]) })
}
}
fn arg_doc_tokens(arg: Option<&ArgIr>) -> TokenStream {
let summary = arg.and_then(|a| a.doc.clone()).unwrap_or_default();
let doc = DocIr {
summary,
description: String::new(),
examples: Vec::new(),
};
doc_tokens(&doc)
}
fn alias_tokens(arg: Option<&ArgIr>) -> TokenStream {
let aliases = arg
.map(|a| a.aliases.clone())
.unwrap_or_default()
.into_iter()
.map(|a| quote! { #a.to_string() });
quote! { ::std::vec![ #(#aliases),* ] }
}
fn opt_char(c: Option<char>) -> TokenStream {
match c {
Some(c) => quote! { ::std::option::Option::Some(#c) },
None => quote! { ::std::option::Option::None },
}
}
fn opt_str(s: Option<&String>) -> TokenStream {
match s {
Some(s) => quote! { ::std::option::Option::Some(#s.to_string()) },
None => quote! { ::std::option::Option::None },
}
}
fn opt_u32(n: Option<u32>) -> TokenStream {
match n {
Some(n) => quote! { ::std::option::Option::Some(#n) },
None => quote! { ::std::option::Option::None },
}
}
fn opt_str_vec(v: Option<&Vec<String>>) -> TokenStream {
match v {
Some(v) => {
let items = v.iter().map(|s| quote! { #s.to_string() });
quote! { ::std::option::Option::Some(::std::vec![ #(#items),* ]) }
}
None => quote! { ::std::option::Option::None },
}
}
fn opt_direction(d: Option<PathDirectionIr>) -> TokenStream {
match d {
Some(PathDirectionIr::Input) => {
quote! { ::std::option::Option::Some(golem_rust::schema::PathDirection::Input) }
}
Some(PathDirectionIr::Output) => {
quote! { ::std::option::Option::Some(golem_rust::schema::PathDirection::Output) }
}
Some(PathDirectionIr::InOut) => {
quote! { ::std::option::Option::Some(golem_rust::schema::PathDirection::InOut) }
}
None => quote! { ::std::option::Option::None },
}
}
fn opt_path_kind(k: Option<PathKindIr>) -> TokenStream {
match k {
Some(PathKindIr::File) => {
quote! { ::std::option::Option::Some(golem_rust::schema::PathKind::File) }
}
Some(PathKindIr::Directory) => {
quote! { ::std::option::Option::Some(golem_rust::schema::PathKind::Directory) }
}
Some(PathKindIr::Any) => {
quote! { ::std::option::Option::Some(golem_rust::schema::PathKind::Any) }
}
None => quote! { ::std::option::Option::None },
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum KnownTypeFamily {
Numeric,
Bool,
Char,
Text,
Path,
Url,
}
fn known_type_family(ty: &Type) -> Option<KnownTypeFamily> {
let ty = match ty {
Type::Reference(r) => &*r.elem,
other => other,
};
if is_integer_type(ty) {
return Some(KnownTypeFamily::Numeric);
}
match type_last_ident(ty).as_deref() {
Some("f32" | "f64") => Some(KnownTypeFamily::Numeric),
Some("bool") => Some(KnownTypeFamily::Bool),
Some("char") => Some(KnownTypeFamily::Char),
Some("String" | "str") => Some(KnownTypeFamily::Text),
Some("PathBuf" | "Path") => Some(KnownTypeFamily::Path),
Some("Url") => Some(KnownTypeFamily::Url),
_ => None,
}
}
fn is_known_non_text(ty: &Type) -> bool {
matches!(known_type_family(ty), Some(f) if f != KnownTypeFamily::Text)
}
fn is_known_non_path(ty: &Type) -> bool {
matches!(known_type_family(ty), Some(f) if f != KnownTypeFamily::Path)
}
fn is_known_non_url(ty: &Type) -> bool {
matches!(known_type_family(ty), Some(f) if f != KnownTypeFamily::Url)
}
fn is_known_non_numeric(ty: &Type) -> bool {
matches!(known_type_family(ty), Some(f) if f != KnownTypeFamily::Numeric)
}
fn is_integer_type(ty: &Type) -> bool {
matches!(
type_last_ident(ty).as_deref(),
Some(
"u8" | "u16"
| "u32"
| "u64"
| "u128"
| "usize"
| "i8"
| "i16"
| "i32"
| "i64"
| "i128"
| "isize"
)
)
}
fn type_last_ident(ty: &Type) -> Option<String> {
if let Type::Path(tp) = ty {
tp.path.segments.last().map(|s| s.ident.to_string())
} else {
None
}
}
fn is_auto_injected_principal_type(ty: &Type) -> bool {
let Type::Path(tp) = ty else {
return false;
};
let segments = tp
.path
.segments
.iter()
.map(|segment| segment.ident.to_string())
.collect::<Vec<_>>();
let segments = segments.iter().map(String::as_str).collect::<Vec<_>>();
matches!(
segments.as_slice(),
["golem_rust", "agentic" | "tool", "Principal"]
| ["golem_native_tool" | "crate", "Principal"]
| [
"golem_rust",
"golem_agentic",
"golem",
"agent",
"common",
"Principal"
]
)
}
fn unwrap_generic1<'a>(ty: &'a Type, wrapper: &str) -> Option<&'a Type> {
if let Type::Path(tp) = ty
&& let Some(seg) = tp.path.segments.last()
&& seg.ident == wrapper
&& let PathArguments::AngleBracketed(args) = &seg.arguments
{
for ga in &args.args {
if let GenericArgument::Type(t) = ga {
return Some(t);
}
}
}
None
}
fn is_map_type(ty: &Type) -> bool {
matches!(type_last_ident(ty).as_deref(), Some("BTreeMap" | "HashMap"))
}
fn split_result(output: &ReturnType) -> (Option<&Type>, Option<&Type>) {
let ty = match output {
ReturnType::Default => return (None, None),
ReturnType::Type(_, t) => t.as_ref(),
};
if let Type::Path(tp) = ty
&& let Some(seg) = tp.path.segments.last()
&& seg.ident == "Result"
&& let PathArguments::AngleBracketed(args) = &seg.arguments
{
let mut types = args.args.iter().filter_map(|ga| {
if let GenericArgument::Type(t) = ga {
Some(t)
} else {
None
}
});
let ok = types.next();
let err = types.next();
let ok = ok.filter(|t| !is_unit(t));
return (ok, err);
}
if is_unit(ty) {
(None, None)
} else {
(Some(ty), None)
}
}
fn is_unit(ty: &Type) -> bool {
matches!(ty, Type::Tuple(t) if t.elems.is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tool::definition::build_tool_definition_ir;
use quote::ToTokens;
use test_r::test;
fn generated_body(item: syn::ItemTrait, prefix: &str) -> String {
let ir = build_tool_definition_ir(&item, None).unwrap();
let file: syn::File = syn::parse2(synthesize_descriptor_fn(&ir).unwrap()).unwrap();
file.items
.iter()
.find_map(|item| match item {
syn::Item::Fn(f) if f.sig.ident.to_string().starts_with(prefix) => {
Some(f.block.to_token_stream().to_string())
}
_ => None,
})
.unwrap()
}
#[test]
fn standalone_specialization_does_not_retain_composition() {
let item: syn::ItemTrait = syn::parse_quote! {
trait Simple { fn run(&self, input: String) -> u32; }
};
let standalone = generated_body(item.clone(), "__golem_standalone");
assert!(!standalone.contains("ToolBuildCtx"));
assert!(!standalone.contains("normalize_inherited_globals"));
let prepared = generated_body(item.clone(), "__golem_prepared");
assert!(prepared.contains("prepare_without_literals"));
assert!(prepared.contains("ToolSchemaProbe :: < String >"));
assert!(!prepared.contains("dynamic"));
let composable = generated_body(item, "__golem_tool_descriptor");
assert!(composable.contains("apply_pending_graft_root"));
assert!(composable.contains("normalize_inherited_globals"));
}
#[test]
fn advanced_definitions_keep_required_runtime_checks() {
for item in [
syn::parse_quote! { trait Globals { #[arg(input = "global")] fn run(&self, input: String); } },
syn::parse_quote! { trait Parent { #[command(subtree = Child)] fn child(&self); } },
syn::parse_quote! { trait Values { #[constraint(requires_all = value_is("input", "x"))] fn run(&self, input: String); } },
] {
assert!(generated_body(item, "__golem_standalone").contains("ToolBuildCtx"));
}
for item in [
syn::parse_quote! { trait Defaults { #[arg(input = "option", default = "x")] fn run(&self, input: String); } },
syn::parse_quote! { trait Values { #[constraint(requires_all = value_is("input", "x"))] fn run(&self, input: String); } },
syn::parse_quote! { trait Parent { #[command(subtree = Child)] fn child(&self); } },
] {
assert!(generated_body(item, "__golem_prepared").contains("__tool . prepare ()"));
}
for item in [
syn::parse_quote! { trait Refined { #[arg(input = "option", regex = "x+")] fn run(&self, input: String); } },
syn::parse_quote! { trait Errors { fn run(&self) -> Result<(), CustomError>; } },
] {
assert!(
generated_body(item, "__golem_prepared").contains("ToolSchemaChecks :: dynamic")
);
}
}
#[test]
fn ordinary_tool_types_named_native_tool_cancellation_remain_schema_inputs() {
let item: syn::ItemTrait = syn::parse_quote! {
trait OrdinaryTool {
#[arg(explicit = "option")]
fn run(
&self,
bare: NativeToolCancellation,
qualified: user::NativeToolCancellation,
explicit: NativeToolCancellation,
);
}
};
let ir = build_tool_definition_ir(&item, None).unwrap();
let descriptor = synthesize_descriptor_fn(&ir).unwrap().to_string();
assert!(descriptor.contains("\"bare\""));
assert!(descriptor.contains("\"qualified\""));
assert!(descriptor.contains("\"explicit\""));
}
}