{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://leviath.dev/docs/stable/blueprint.schema.json",
  "title": "Leviath agent blueprint (agent.leviath)",
  "description": "The TOML manifest defining a Leviath agent: its stages, the model each one runs on, the tools it may call, and the shape of its context. Written against the parser in crates/leviath-core/src/manifest.rs, and checked against every bundled blueprint by a test in crates/leviath-cli/src/bundled.rs. See https://leviath.dev/docs/agents.",
  "type": "object",
  "required": ["agent"],
  "additionalProperties": false,
  "properties": {
    "agent": { "$ref": "#/$defs/agent" },
    "stages": {
      "type": "object",
      "description": "One entry per stage, keyed by stage name.",
      "additionalProperties": { "$ref": "#/$defs/stage" }
    },
    "context": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "regions": {
          "type": "object",
          "description": "One entry per context region, keyed by region name.",
          "additionalProperties": { "$ref": "#/$defs/region" }
        },
        "file_tracking": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "region": { "type": "string", "default": "files" },
            "track_reads": { "type": "boolean", "default": true },
            "track_writes": { "type": "boolean", "default": true },
            "max_file_tokens": { "type": "integer", "minimum": 0 }
          }
        }
      }
    },
    "tool_permissions": { "$ref": "#/$defs/toolPermissions" },
    "security": {
      "type": "object",
      "description": "Taint tracking for this blueprint. A machine-wide ceiling still applies.",
      "properties": { "taint_tracking": { "type": "boolean" } }
    },
    "sandbox": { "$ref": "#/$defs/sandbox" },
    "read_paths": {
      "type": "object",
      "description": "Paths outside the workdir this agent may read. Inert until the host grants them under [agent_read_paths] in config.toml.",
      "additionalProperties": false,
      "properties": {
        "allow": {
          "type": "array",
          "items": {
            "type": "string",
            "description": "An exact path (granting its subtree), `glob:<pattern>`, or `regex:<pattern>` (auto-anchored). `~/` expands; a relative path resolves against the workdir."
          }
        }
      }
    },
    "safe_commands": {
      "type": "object",
      "description": "Tools and shell command prefixes this agent would like to run without an approval prompt. Inert until the host opts in under [agent_safe_commands.<name>] allow_blueprint or [security] allow_blueprint_safe_commands in config.toml.",
      "additionalProperties": false,
      "properties": {
        "tools": {
          "type": "array",
          "items": {
            "type": "string",
            "description": "A tool name, exactly as the model calls it."
          }
        },
        "shell": {
          "type": "array",
          "items": {
            "type": "string",
            "description": "A program, optionally with the subcommand that narrows it: \"rg\", \"cargo test\". No flags, arguments, redirects, quotes or chained commands."
          }
        }
      }
    },
    "compaction": {
      "type": "object",
      "description": "The model that summarizes a compacting region. Skipped without error when its provider is not registered, so a run on another provider loses compaction rather than failing.",
      "additionalProperties": false,
      "properties": {
        "provider": { "type": "string" },
        "model": { "type": "string" },
        "system_prompt": { "type": "string" },
        "max_summary_tokens": { "type": "integer", "minimum": 1 },
        "temperature": { "type": "number" }
      }
    },
    "repetition_detection": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "enabled": { "type": "boolean" },
        "max_repeat_calls": { "type": "integer", "minimum": 1 },
        "max_readonly_streak": { "type": "integer", "minimum": 1 }
      }
    },
    "transforms": {
      "type": "array",
      "description": "How context maps when one blueprint hands off to another.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "from_blueprint": { "type": "string" },
          "to_blueprint": { "type": "string" },
          "mappings": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "from_region": { "type": "string" },
                "to_region": { "type": "string" },
                "transform": { "enum": ["direct", "summarize", "extract"] },
                "fields": { "type": "array", "items": { "type": "string" } }
              }
            }
          }
        }
      }
    }
  },
  "$defs": {
    "agent": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "name": { "type": "string", "default": "unnamed" },
        "version": { "type": "string", "default": "0.1.0" },
        "description": { "type": "string", "default": "" },
        "entry_stage": {
          "type": "string",
          "description": "The stage a run starts in. Defaults to the first stage declared."
        },
        "max_child_depth": { "type": "integer", "minimum": 0 },
        "dynamic_tools": { "type": "boolean", "default": false },
        "batch_tool_hint": { "type": "boolean" },
        "shell_hint": { "type": "boolean" },
        "nudge": { "$ref": "#/$defs/nudge" },
        "output": { "$ref": "#/$defs/outputSpec" }
      }
    },

    "outputSpec": {
      "type": "object",
      "additionalProperties": false,
      "description": "What shape the run's final output should take. Declared by the agent, narrowed by a stage, and overridable by whoever starts the run.",
      "properties": {
        "format": {
          "type": "string",
          "description": "A label for the shape, carried to the model and recorded beside the answer. Deliberately not an enum: any value is valid, and one the engine has never seen is simply not parsed. json, xml, yaml, csv and toml get a free well-formedness check; anything else needs a validator to be checked at all."
        },
        "instructions": {
          "type": "string",
          "description": "Free-form guidance folded into the submit_output tool description and the stage's system prompt. Where a format the model has never seen gets explained."
        },
        "example": {
          "type": "string",
          "description": "A literal sample shown to the model verbatim. The most effective lever for an unusual format, and the reason one needs no code support."
        },
        "schema": {
          "type": "object",
          "description": "A JSON Schema describing the answer's shape. Separate from format, which asks only whether the answer parses. A failing submission is refused back to the model to correct."
        },
        "validator": {
          "type": "string",
          "description": "A .rhai script deciding whether an answer is valid, as a path relative to the blueprint directory. For a format the engine cannot parse and a shape a JSON Schema cannot describe. Defines fn validate(content), returning () when fine or a string saying what is wrong. Compiled at spawn, so a broken script fails fast."
        }
      }
    },

    "stage": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "mode": {
          "description": "How the stage runs. `interactive_points` holds at each declared interaction point. `output` produces the run's final answer and nothing else. An unrecognized value is a load error, not a silent fallback to autonomous.",
          "enum": [
            "autonomous",
            "interactive",
            "interactive_points",
            "fan_out",
            "fan_out_merge",
            "output"
          ]
        },
        "description": { "type": "string" },
        "system_prompt": { "type": "string" },
        "transition_prompt": { "type": "string" },
        "available_tools": {
          "type": "array",
          "description": "The only tools the model is offered here. Anything absent is refused at dispatch.",
          "items": { "type": "string" }
        },
        "require_output": {
          "type": "boolean",
          "default": false,
          "description": "This stage must call submit_output before it transitions. Unlike required_tools, which is never checked afterwards, this is enforced: a stage that finishes without submitting is nudged and re-run, bounded, then let through with the run's output_forced flag set. `mode = \"output\"` sets it for you.",
          "$comment": "A stage that sets this must also grant submit_output; lev validate refuses it otherwise."
        },
        "output": { "$ref": "#/$defs/outputSpec" },
        "required_tools": {
          "type": "array",
          "description": "Tools this stage cannot work without. Every entry must also appear in available_tools. Survives an unattended run, where the blocking interaction tools are otherwise withheld.",
          "items": { "type": "string" }
        },
        "max_iterations": { "type": "integer", "minimum": 1 },
        "max_revisits": { "type": "integer", "minimum": 0 },
        "requires_children": { "type": "boolean", "default": false },
        "allow_complete": { "type": "boolean", "default": false },
        "allow_as_worker": { "type": "boolean", "default": false },
        "accepts_messages": { "type": "boolean", "default": true },
        "allow_blocking_tools": { "type": "boolean", "default": false },
        "batch_tool_hint": { "type": "boolean" },
        "shell_hint": { "type": "boolean" },
        "model": { "$ref": "#/$defs/modelConfig" },
        "nudge": { "$ref": "#/$defs/nudge" },
        "hooks": { "$ref": "#/$defs/stageHooks" },
        "sandbox": { "$ref": "#/$defs/sandbox" },
        "security": { "type": "object" },
        "tool_permissions": { "$ref": "#/$defs/toolPermissions" },
        "tool_routing": {
          "type": "object",
          "description": "Where a tool result lands in context.",
          "additionalProperties": false,
          "properties": {
            "default_region": { "type": "string" },
            "persist": { "type": "boolean" },
            "max_result_tokens": { "type": "integer", "minimum": 1 },
            "overrides": {
              "type": "object",
              "description": "Tool name to a region name, or to a table carrying that tool's region, its result ceiling, or both.",
              "additionalProperties": {
                "oneOf": [
                  { "type": "string" },
                  {
                    "type": "object",
                    "additionalProperties": false,
                    "minProperties": 1,
                    "properties": {
                      "region": { "type": "string" },
                      "max_result_tokens": { "type": "integer", "minimum": 0 }
                    }
                  }
                ]
              }
            },
            "max_result_tokens_per_tool": {
              "type": "object",
              "description": "Tool name to that tool's result ceiling, overriding max_result_tokens for it.",
              "additionalProperties": { "type": "integer", "minimum": 0 }
            }
          }
        },
        "context": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "regions": {
              "type": "object",
              "additionalProperties": { "$ref": "#/$defs/region" }
            }
          }
        },
        "transitions": {
          "type": "object",
          "description": "Outgoing edges, keyed by target stage name. An empty table marks a terminal stage.",
          "additionalProperties": { "$ref": "#/$defs/transition" }
        },
        "interaction_points": {
          "type": "array",
          "items": { "$ref": "#/$defs/interactionPoint" }
        },
        "on_worker_failure": { "enum": ["continue", "fail_all"] },
        "max_workers": { "type": "integer", "minimum": 1 },
        "worker_agent": { "type": "string" },
        "worker_stage": { "type": "string" },
        "worker_query": { "type": "string" },
        "merge_stage": { "type": "string" },
        "split_prompt": { "type": "string" },
        "results_region": {
          "type": "string",
          "description": "Context region the consolidated worker report is written to. Defaults to conversation, which is also carrying the message history. Worth naming when the results are bulky: the region's budget is what each worker's share is divided from."
        },
        "max_items": {
          "type": "integer",
          "minimum": 1,
          "description": "Most work items the split may produce. Distinct from max_workers, which caps how many run at once. This caps how many exist at all, bounding both the run's cost and each worker's share of the results region."
        }
      }
    },

    "modelConfig": {
      "type": "object",
      "description": "Which model this stage runs on. `models` is an ordered fallback list: the first entry whose provider is registered wins.",
      "additionalProperties": false,
      "properties": {
        "models": {
          "type": "array",
          "items": { "$ref": "#/$defs/modelEntry" }
        },
        "provider": {
          "type": "string",
          "description": "Back-compat shorthand for a single-entry `models` list."
        },
        "model": {
          "type": "string",
          "description": "Back-compat shorthand, paired with `provider`."
        },
        "fallbacks": {
          "type": "array",
          "description": "Back-compat: folded onto the end of `models`.",
          "items": { "$ref": "#/$defs/modelEntry" }
        },
        "allow_user_default": {
          "type": "boolean",
          "default": true,
          "description": "Whether the host's default model may be used when nothing listed is registered."
        },
        "request_timeout_secs": { "type": "integer", "minimum": 1 },
        "parameters": {
          "type": "object",
          "description": "Passed to the provider verbatim, so the accepted keys are the provider's.",
          "additionalProperties": true
        }
      }
    },

    "modelEntry": {
      "type": "object",
      "required": ["provider", "model"],
      "additionalProperties": false,
      "properties": {
        "provider": {
          "type": "string",
          "description": "A registered provider name. The built-ins are anthropic, openai, google, openrouter, ollama, and claude-code; a Rhai provider resolves by its script name."
        },
        "model": {
          "type": "string",
          "description": "Passed to the provider verbatim and never validated locally, so a typo surfaces on the first live call."
        }
      }
    },

    "region": {
      "type": "object",
      "description": "One region of the context window. Budgets are ceilings, not allocations, and may sum past 100%.",
      "additionalProperties": false,
      "properties": {
        "kind": {
          "description": "Omitted means `temporary`. An unrecognised value is a hard parse error.",
          "enum": [
            "temporary",
            "pinned",
            "sliding_window",
            "compacting",
            "compact_history",
            "clearable",
            "hashmap",
            "hash_map",
            "checklist",
            "custom"
          ]
        },
        "budget": {
          "type": "string",
          "pattern": "^[0-9]+(\\.[0-9]+)?%$",
          "description": "A percentage of the window, as a string such as \"35%\"."
        },
        "max_tokens": { "type": "integer", "minimum": 1, "default": 5000 },
        "min_tokens": { "type": "integer", "minimum": 0 },
        "required": { "type": "boolean", "default": false },
          "summarizable": { "type": "boolean", "default": true, "description": "Set false to keep an edge transform = \"compact\" from handing this region to the summarizer. Protects the region wherever it is used, and wins over an explicit compact list." },
        "admission": { "enum": ["evict", "reject"], "default": "evict", "description": "What the region does when a write does not fit. \"evict\" makes room by dropping the oldest entry; \"reject\" refuses the write and tells the agent to release something first, so nothing is discarded without the agent knowing." },
        "required_message": {
          "type": "string",
          "description": "Shown when a required region is empty. `{region}` interpolates."
        },
        "seed": { "$ref": "#/$defs/regionSeed" },
        "max_items": { "type": "integer", "minimum": 1, "default": 10 },
        "strategy": { "enum": ["per_item", "bulk", "compact"] },
        "overflow": { "type": "integer", "minimum": 0 },
        "compact_count": { "type": "integer", "minimum": 1 },
        "compact_at": { "type": "string", "pattern": "^[0-9]+(\\.[0-9]+)?%$" },
        "threshold_tokens": { "type": "integer", "minimum": 1 },
        "source_region": { "type": "string" },
        "max_entries": { "type": "integer", "minimum": 1 },
        "script": {
          "type": "string",
          "description": "For `kind = \"custom\"`: the Rhai script under context_hooks/."
        },
        "persistent": { "type": "boolean" }
      }
    },

    "regionSeed": {
      "description": "What fills the region at spawn. The table form is checked key by key in the order below, so the first one present wins.",
      "oneOf": [
        {
          "type": "string",
          "description": "A caller-input key, such as \"task\", filled from --task or a --<region> flag."
        },
        {
          "type": "object",
          "additionalProperties": false,
          "minProperties": 1,
          "properties": {
            "glob": {
              "type": "string",
              "description": "Concatenated contents of the workdir files matching this pattern."
            },
            "files": {
              "type": "array",
              "description": "Concatenated contents of these workdir-relative paths.",
              "items": { "type": "string" }
            },
            "literal": {
              "type": "string",
              "description": "Verbatim text baked into the blueprint."
            },
            "rhai": {
              "type": "string",
              "description": "A Rhai script in the workdir whose returned String fills the region."
            },
            "command": {
              "type": "string",
              "description": "A shell command run at spawn. The only seed that executes anything, and it runs before the first inference and so before any approval prompt. Gated by [security] allow_seed_commands and --no-seed-commands."
            },
            "caller": {
              "type": "string",
              "description": "The caller-input key to fill from, the table form of the plain string."
            }
          }
        }
      ]
    },

    "transition": {
      "type": "object",
      "description": "One outgoing edge. A `hint` with no `condition` implies `llm_choice`.",
      "additionalProperties": false,
      "properties": {
        "hint": {
          "type": "string",
          "description": "Told to the model when it is choosing where to go next."
        },
        "condition": {
          "description": "An unrecognised value is a hard parse error.",
          "enum": ["always", "llm_choice", "error", "max_iterations", "stuck", "dead_end"]
        },
        "transform": {
          "description": "What happens to context on this edge. `custom` reads transform_config. Anything else is a hard parse error rather than a silent downgrade to `direct`.",
          "enum": ["direct", "clear", "compact", "summarize", "custom"]
        },
        "transform_config": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "carry": { "type": "array", "items": { "type": "string" } },
            "compact": { "type": "array", "items": { "type": "string" } },
            "clear": { "type": "array", "items": { "type": "string" } },
            "compact_prompt": { "type": "string" }
          }
        },
        "gate": {
          "type": "object",
          "description": "A condition the stage must satisfy before this edge may be taken.",
          "additionalProperties": false,
          "properties": {
            "require_modifications": { "type": "boolean" },
            "require_regions": {
              "type": "array",
              "description": "Regions that must all hold content before this edge may be taken. ANDed with every other condition on the gate, unlike `region`.",
              "items": { "type": "string" }
            },
            "require_region_updated": {
              "type": "string",
              "description": "A region that must have changed during this stage, not merely be present. For a revise loop, where re-emitting the same content would satisfy a presence check."
            },
            "require_no_open_items": {
              "type": "string",
              "description": "A checklist region that must have no open items before this edge is taken."
            },
            "message": { "type": "string" },
            "region": {
              "type": "string",
              "description": "An alternative way to satisfy require_modifications: the gate also passes when this region is non-empty. A restart-durable stand-in for the per-stage counters, not a requirement - use require_regions for that."
            },
            "tools": { "type": "array", "items": { "type": "string" } },
            "max_attempts": { "type": "integer", "minimum": 1 }
          }
        },
        "stuck_after_iterations": { "type": "integer" },
        "stuck_after_minutes": { "type": "integer" },
        "stuck_after_same_file_edits": { "type": "integer" },
        "stuck_after_tool_calls": { "type": "integer" }
      }
    },

    "interactionPoint": {
      "type": "object",
      "description": "A checkpoint the runtime raises at the stage boundary, every time, rather than one the model chooses to raise.",
      "additionalProperties": false,
      "properties": {
        "name": { "type": "string" },
        "prompt": { "type": "string" },
        "required": { "type": "boolean", "default": true },
        "unattended": {
          "description": "What a --yolo run does here. `auto_approve` (the default) resolves it without asking. `ask` holds for a person even unattended, and the run then waits for [limits] interaction_timeout_secs.",
          "enum": ["ask", "auto_approve"]
        },
        "style": { "enum": ["free_text", "multiple_choice", "confirm"] },
        "options": { "type": "array", "items": { "type": "string" } },
        "choices": {
          "type": "array",
          "description": "Alias for `options`.",
          "items": { "type": "string" }
        },
        "directives": {
          "type": "object",
          "description": "Option text to the instruction added when it is picked.",
          "additionalProperties": { "type": "string" }
        },
        "followups": {
          "type": "object",
          "description": "Alias for `directives`.",
          "additionalProperties": { "type": "string" }
        },
        "abort_options": {
          "type": "array",
          "description": "Options that end the run immediately, with no further inference.",
          "items": { "type": "string" }
        },
        "edit_options": {
          "type": "array",
          "description": "Options that open the document for the user to edit directly.",
          "items": { "type": "string" }
        },
        "document_region": {
          "type": "string",
          "description": "The pinned region holding what this point is about. Replaced with the current text each time the point is presented."
        }
      }
    },

    "toolPermissions": {
      "type": "object",
      "description": "Tool name to policy. May only tighten the machine-wide ceiling in config.toml, never loosen it. `ask` blocks until answered.",
      "additionalProperties": { "enum": ["allow", "ask", "deny"] }
    },

    "sandbox": {
      "type": "object",
      "description": "Where tools execute. An agent and a stage resolve to the stronger of the pair.",
      "additionalProperties": false,
      "properties": {
        "kind": { "enum": ["none", "namespace", "container"] },
        "image": { "type": "string" },
        "engine": { "enum": ["docker", "podman", "nerdctl", "finch"] },
        "network": { "type": "boolean" },
        "mount": { "type": "array", "items": { "type": "string" } },
        "mounts": { "type": "array", "items": { "type": "string" } },
        "persist": { "type": "boolean" },
        "on_unavailable": { "enum": ["error", "warn"] }
      }
    },

    "nudge": {
      "type": "object",
      "description": "What to say to a stage that has gone quiet. Cascades stage over agent over config.",
      "additionalProperties": false,
      "properties": {
        "enabled": { "type": "boolean" },
        "max": { "type": "integer", "minimum": 0 },
        "text": {
          "type": "string",
          "description": "`{stage}` and `{regions}` interpolate."
        }
      }
    },

    "stageHooks": {
      "type": "object",
      "description": "Rhai scripts run at points in this stage's lifecycle. Each value is a script path relative to the agent directory.",
      "additionalProperties": false,
      "properties": {
        "on_stage_enter": { "type": "string" },
        "on_stage_exit": { "type": "string" },
        "before_inference": { "type": "string" },
        "after_inference": { "type": "string" },
        "on_tool_call": { "type": "string" },
        "on_completion": { "type": "string" },
        "on_error": { "type": "string" }
      }
    }
  }
}
