RFD 042: Tool Options
- Status: Implemented
- Category: Design
- Authors: Jean Mertz git@jeanmertz.com
- Date: 2026-03-09
Summary
This RFD introduces a per-tool options field in the tool configuration that passes static, user-defined key-value pairs to tools at execution time. This gives tools a way to receive behavioral configuration from the user without polluting the LLM-facing parameter schema.
Motivation
Tools currently receive three categories of input:
| Category | Source | Purpose |
|---|---|---|
arguments | LLM | What to do (file path, patterns, etc.) |
answers | Inquiries/prompts | Interactive decisions (apply changes?, |
| overwrite?) | ||
context | Runtime | Execution environment (root path, |
| action) |
There is no mechanism for the user to configure a tool's runtime behavior. For example, fs_modify_file always sends an apply_changes inquiry to the LLM for every modification, regardless of how trivial the edit is. A user who wants the tool to auto-approve small changes today has two options: set questions.apply_changes.answer = true (which bypasses verification for all changes, including risky ones) or accept the cost of an LLM roundtrip for every edit.
What's missing is a fourth input category — static behavioral configuration from the user that the tool reads at runtime to adjust its behavior. This is distinct from parameters (which the LLM controls) and questions (which govern inquiry routing). Options are set by the user in config, not by the LLM in a tool call.
Design
User-Facing Configuration
A new options field on per-tool configuration. The field is a free-form key-value map — each tool defines its own schema.
[conversation.tools.fs_modify_file]
options.apply_changes_trigger = "heuristics"
options.auto_approve_max_changed_lines = 10
options.auto_approve_max_ratio_percent = 20Options are per-tool only. There is no global options in the defaults (*) section — options are inherently tool-specific, and a shared namespace would be confusing.
Config Layer — jp_config
Add options to ToolConfig:
/// Per-tool options.
///
/// A free-form map of key-value pairs passed to the tool at runtime.
/// Each tool defines its own supported options and defaults. Unknown
/// options are silently forwarded (the tool ignores what it doesn't
/// recognize).
#[serde(default)]
pub options: Map<String, Value>,Expose through ToolConfigWithDefaults:
impl ToolConfigWithDefaults {
/// Return the per-tool options map.
#[must_use]
pub fn options(&self) -> &Map<String, Value> {
&self.tool.options
}
}The options field needs the standard config trait implementations: AssignKeyValue, PartialConfigDelta, and ToPartial. Because the value is a flat Map<String, Value>, these are straightforward — delta compares entries by key, partial serializes non-empty maps.
Execution Layer — jp_llm
In execute_local, include options in the JSON context passed to the tool command:
let ctx = json!({
"tool": {
"name": name,
"arguments": &arguments,
"answers": answers,
"options": config.options(),
},
"context": {
"action": Action::Run,
"root": root.as_str(),
},
});The same addition applies to the FormatArguments path in crates/jp_cli/src/cmd/query/tool/renderer.rs.
Applicability to Other Tool Sources
Options only apply to local tools. For MCP tools, the server owns behavior configuration — JP has no way to pass out-of-band options to an external server. For builtin tools, the execution path calls BuiltinTool::execute(arguments, answers) directly; if a builtin needs configurable behavior in the future, the BuiltinTool trait can be extended separately.
Drawbacks
No schema validation. Because options are Map<String, Value>, there is no compile-time or config-time validation that a given option key is supported by the tool or that the value is the right type. Typos are silently ignored. This is the same tradeoff as arguments, but arguments at least have a JSON schema the LLM follows. For options, the tool itself is the only validator.
Discovery. Users won't know what options a tool supports without reading the tool's documentation or source. There is no introspection mechanism. This is acceptable for now — JP's tool set is small and documented — but may need revisiting if the option surface grows.
Alternatives
Typed per-tool config sections
Instead of a free-form map, define a typed struct per tool in jp_config:
pub struct FsModifyFileOptions {
pub apply_changes_trigger: ApplyChangesTrigger,
pub auto_approve_max_changed_lines: usize,
// ...
}This gives compile-time safety and schema generation but requires jp_config to know about every tool's options. It couples the config crate to tool internals and doesn't scale to user-defined or MCP tools. Rejected in favor of the generic map.
Overload questions config
Encode behavioral options as question answers (e.g., questions.apply_changes_trigger.answer = "heuristics"). This technically works with today's infrastructure but abuses the question system — these aren't questions the tool asks, they're behavioral knobs. It conflates two distinct concepts and makes the config harder to understand.
Overload parameters
Add hidden parameters that the LLM doesn't see but the tool reads. This breaks the contract that parameters are the LLM's interface to the tool. The LLM might hallucinate values for these hidden parameters, or validation might reject tool calls that omit them.
Non-Goals
- Global options. The
*defaults section does not get anoptionsfield. Options are tool-specific by definition. - Moving
Tooltojp_tool. Formalizing theToolstruct as a shared contract betweenjp_llmand the tools binary is worthwhile but orthogonal. It can be done as a follow-up without affecting the options feature. - Options for MCP or builtin tools. Out of scope. MCP tools are configured server-side. Builtins can be extended independently if needed.
Risks and Open Questions
Threshold for documenting options. If tools start accumulating options, discoverability becomes a real problem. A future extension could add an options_schema field to tool definitions (similar to parameters) that describes supported options with types and defaults. This is not needed now but worth keeping in mind.
Implementation Plan
- Add
options: Map<String, Value>toToolConfiginjp_config. - Implement
AssignKeyValue,PartialConfigDelta,ToPartialfor the new field. - Add
options()accessor toToolConfigWithDefaults. - Include
"options"in the JSON context inexecute_localand theFormatArgumentspath.
This phase can be merged independently. No behavioral change — tools that don't read options are unaffected.
References
crates/jp_config/src/conversation/tool.rs—ToolConfig,ToolConfigWithDefaultscrates/jp_llm/src/tool.rs—execute_local, JSON context construction