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
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/2.0.0.
compiling a tool registry's schemas into a sampler grammar.
- `grammar` feature flag (depends on `providers`): opt-in grammar / sampler
support for the `Grammar` mode of `ToolConstraint`.
- `LlmReflector` (`reflection::llm` module): a `Reflector` that asks the
model to classify failed tool calls and suggest corrections via
`request_structured::<FailureAnalysis>`. First in-tree consumer of
`StructuredOutput`. Opt-in via `BareLoop::set_reflector`; the default
stays `NoopReflector`. Each analyzed failure triggers one model
round-trip (see its rustdoc for the latency/cost note).
- `impl StructuredOutput for FailureAnalysis` (`reflection` module) with a
hand-written JSON Schema covering the 5 fields and the nested
`CorrectionType` snake_case enum.
- `schema_validation` feature flag (pulls `jsonschema` as an optional
dependency): when enabled, `LlmReflector` validates the model's
`Correction::modified_input` against the failing tool's `input_schema`
and returns `ReflectionError::Internal` on a mismatch. When disabled,
validation is skipped.

### Changed

- **Breaking:** `Reflector::analyze` gains a new `tool_schema:
Option<&ToolSchema>` parameter between `tool_input` and `context`. The
engine's call site now resolves the failing tool's schema from the
registry (passing `None` when the tool isn't found). Every `Reflector`
impl must add the new parameter; `NoopReflector` and the trait-doc
example have been updated.
Migration: add `_tool_schema: Option<&loopctl::tool::ToolSchema>` to
your `analyze` signature. Ignore it if your reflector does not validate
suggested corrections; otherwise use it to validate `modified_input`
before returning the analysis.
- `OpenAiClient`, `AnthropicClient`, and `GeminiClient` now honor
`RequestOptions::tool_constraint`. Under `Strict`, each tool's schema is
tightened (recursive `additionalProperties: false` and full `required`);
Expand Down
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ parking_lot = "0.12"
reqwest = { version = "0.13", default-features = false, features = ["json", "stream", "rustls"], optional = true }
async-stream = { version = "0.3", optional = true }
httpdate = { version = "1", optional = true }
jsonschema = { version = "0.30", optional = true }

[dev-dependencies]
tokio = { version = "1.52.3", features = ["rt-multi-thread", "macros", "sync", "time", "signal"] }
Expand All @@ -53,6 +54,7 @@ grok = ["providers", "openai"]
gemini = ["providers"]
zai = ["providers", "anthropic"]
grammar = ["providers"]
schema_validation = ["dep:jsonschema"]

[[example]]
name = "hello-cli"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ let agent = BareLoop::new(
| `gemini` | No | `providers` | Google Gemini API client (`provider::gemini`) |
| `zai` | No | `providers`, `anthropic` | Z.AI API client (Anthropic-compatible) |
| `grammar` | No | `providers` | Tool-call grammar providers for grammar-aware samplers (vLLM `guided_json`); enables the `Grammar` mode of `ToolConstraint` |
| `schema_validation` | No | — | JSON Schema validation of `Correction::modified_input` in `LlmReflector` (pulls `jsonschema`); when off, validation is skipped |

## Architecture

Expand Down
3 changes: 3 additions & 0 deletions src/engine/bare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2602,6 +2602,7 @@ mod tests {
error: &str,
tool_name: &str,
_tool_input: &serde_json::Value,
_tool_schema: Option<&crate::tool::ToolSchema>,
_context: &crate::reflection::ReflectionContext,
) -> Pin<
Box<
Expand Down Expand Up @@ -2913,6 +2914,7 @@ mod tests {
error: &str,
tool_name: &str,
_tool_input: &serde_json::Value,
_tool_schema: Option<&crate::tool::ToolSchema>,
_context: &crate::reflection::ReflectionContext,
) -> Pin<
Box<
Expand Down Expand Up @@ -3625,6 +3627,7 @@ mod tests {
error: &str,
tool_name: &str,
_tool_input: &serde_json::Value,
_tool_schema: Option<&crate::tool::ToolSchema>,
_context: &crate::reflection::ReflectionContext,
) -> Pin<
Box<
Expand Down
27 changes: 21 additions & 6 deletions src/engine/bare/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1049,12 +1049,30 @@ impl<C: ApiClient> BareLoop<C> {
max_attempts: Self::MAX_RECOVERY_ATTEMPTS,
};

// Resolve the schema under the name a routing middleware may have
// redirected the call to, falling back to the requested name when
// the resolved name is empty or unknown to the registry.
let resolved_tool = if result.resolved_tool_name.is_empty() {
&tc.tool
} else {
&result.resolved_tool_name
};
let tool_schema = self
.tools
.get(resolved_tool)
.or_else(|| self.tools.get(&tc.tool))
.map(crate::tool::Tool::schema);
let Ok(analysis) = self
.reflector
.analyze(&error_msg, &tc.tool, &tc.input, &context)
.analyze(
&error_msg,
&tc.tool,
&tc.input,
tool_schema.as_ref(),
&context,
)
.await
else {
// Reflector failed — conservatively fail.
return (RecoveryAction::Fail(error_msg), None);
};

Expand Down Expand Up @@ -1230,8 +1248,6 @@ mod tests {
}
}

// ----- ToolDependencyGraph unit tests -----

fn make_call(id: &str, tool: &str, input: Value) -> ToolCall {
ToolCall {
id: id.into(),
Expand Down Expand Up @@ -1403,8 +1419,6 @@ mod tests {
assert_eq!(plan.waves[0], vec![0, 1, 2, 3]);
}

// ----- dispatch_tools_parallel integration tests -----

fn make_parallel_loop(tools: ToolRegistry) -> BareLoop<MockClient> {
let mut config = LoopConfig::default();
config.parallel_tool_dispatch.mode = crate::config::ParallelMode::Parallel;
Expand Down Expand Up @@ -1555,6 +1569,7 @@ mod tests {
error: &str,
tool_name: &str,
_tool_input: &Value,
_tool_schema: Option<&crate::tool::ToolSchema>,
_context: &crate::reflection::ReflectionContext,
) -> Pin<
Box<
Expand Down
Loading