agentcast 0.1.0

Structured-output enforcer for LLM responses. Repair + validate + (optional) retry-with-LLM. BYO-LLM, BYO-schema.
Documentation

agentcast

crates.io docs.rs License: MIT

Structured-output enforcer for LLM responses. Three-pass: repairvalidateretry-with-LLM (optional). BYO-LLM, BYO-schema.

[dependencies]
agentcast = "0.1"

Why

Models advertise JSON, then wrap it in markdown fences, leak prose around it, drop a trailing comma, or miss a required field. Without a buffer, you crash. agentcast gives you the buffer:

  1. Repair — strip ```json fences, extract the largest balanced object from surrounding prose, remove trailing commas.
  2. Validate — JSON Schema you supply (the same one you sent the model in your tool def).
  3. Retry — if validation still fails, hand the model a short hint and reroll. Loop up to max_retries.

Quick start

use agentcast::Caster;
use serde_json::json;

let schema = json!({
    "type": "object",
    "properties": {
        "label": {"type": "string"},
        "confidence": {"type": "number"}
    },
    "required": ["label", "confidence"]
});
let caster = Caster::new(&schema).unwrap();

let raw = "```json\n{\"label\": \"positive\", \"confidence\": 0.92,}\n```";
let value = caster.parse(raw).unwrap();   // repair handles the fence + trailing comma
assert_eq!(value["label"], "positive");

With LLM retry

# use agentcast::Caster;
# use serde_json::json;
# tokio_test::block_on(async {
let caster = Caster::new(&json!({})).unwrap();

// You supply a closure that re-prompts the model with the hint:
let retry_fn = |hint: String| async move {
    // call your LLM, returning Result<String, String>
    let new_response = your_llm_call(hint).await?;
    Ok::<_, String>(new_response)
};

let value = caster
    .parse_with_retry(
        "model's first attempt that doesn't validate",
        /* max_retries */ 2,
        &retry_fn,
    )
    .await
    .unwrap();
# async fn your_llm_call(_h: String) -> Result<String, String> { Ok("{}".into()) }
# });

What's in the hint sent back to the model

Output rejected. Fix and try again:
  - /confidence: required property missing
  - /label: must be one of: ["positive", "negative"]

Short, action-oriented, and what you sent in the tool def is the spec — nothing fancy.

What it doesn't do

  • Doesn't call any LLM — you supply the retry function.
  • Doesn't write the schema for you — bring the same one you used in your tools payload.
  • Doesn't infer types into a T: DeserializeOwned; returns serde_json::Value. Add serde_json::from_value(value) when you have a typed shape.

Sibling: JS @mukundakatta/agentcast

JS users: see @mukundakatta/agentcast on npm.

License

MIT