V3 / ORCHESTRATION

run_scenario

Load a named saved XML scenario, fill its inputs, and run ordered prompts in a fresh child conversation using the same provider and model.

Main agentIntroduced in V3V3

What it does

Missing required inputs pause execution before child work. Scenario XML selects tools and can define trusted executable C# in CDATA. Copy exits bypass model rewriting. Child histories remain separate; nested scenarios are not exposed.

Example arguments

JSON
{
  "scenario_name": "greeting",
  "inputs": { "name": "Alex" }
}

Read the implementation

This page describes the selected release. The source link points to the same exact commit; it cannot silently open a newer implementation.

Lilith/Core/Scenarios/RunScenarioTool.cs ↗

Implementation code

Read the implementation from this version's exact source snapshot. Helper links below stay on the same release.

Lilith/Core/Scenarios/RunScenarioTool.cs

C#
using System.Text.Json;
using System.Xml.Linq;
using Lilith.Core.Tools;

namespace Lilith.Core.Scenarios;

/// <summary>Loads trusted saved XML, fills values as data, and passes the complete document to the executor.</summary>
public sealed class RunScenarioTool(ProviderClient parent, ScenarioFileService files) : ITurnResultTool
{
    public string Name => "run_scenario";
    public string Description => "Run a named saved scenario in a fresh sub-agent conversation. " +
        "Use this when the user says run X scenario. Supply input values in inputs. " +
        "If status is needs_input, ask the user for the missing values before retrying. " +
        "Do not retry a failed scenario without a new user request. " +
        "For exit mode respond, follow exit_request when composing the final answer. " +
        "Available scenarios: " + string.Join(", ", files.ListNames());
    public JsonElement Parameters => JsonSerializer.SerializeToElement(new
    {
        type = "object", properties = new
        {
            scenario_name = new { type = "string", description = "Saved scenario name, without .xml." },
            inputs = new { type = "object", additionalProperties = new { type = "string" }, description = "Named scenario input values." }
        }, required = new[] { "scenario_name" }, additionalProperties = false
    });
    public TimeSpan ExecutionTimeout => TimeSpan.FromMinutes(10);
    public async Task<string> ExecuteAsync(JsonElement arguments, CancellationToken cancellationToken = default) =>
        (await ExecuteResultAsync(arguments, cancellationToken)).Content;

    public async Task<ToolExecutionResult> ExecuteResultAsync(JsonElement arguments, CancellationToken cancellationToken)
    {
        if (arguments.ValueKind != JsonValueKind.Object || !arguments.TryGetProperty("scenario_name", out var name) || name.ValueKind != JsonValueKind.String ||
            arguments.EnumerateObject().Any(p => p.Name is not ("scenario_name" or "inputs")))
            throw new ArgumentException("Expected scenario_name and optional inputs.");
        string xml = files.LoadScenarioXml(name.GetString()!);
        var definition = ScenarioXmlParser.Parse(xml);
        var supplied = new Dictionary<string, string>(StringComparer.Ordinal);
        if (arguments.TryGetProperty("inputs", out var inputValues))
        {
            if (inputValues.ValueKind != JsonValueKind.Object) throw new ArgumentException("inputs must be an object of string values.");
            foreach (var property in inputValues.EnumerateObject())
            {
                if (property.Value.ValueKind != JsonValueKind.String || !definition.Inputs.Any(i => i.Name == property.Name) ||
                    !supplied.TryAdd(property.Name, property.Value.GetString()!) || supplied[property.Name].Length > 20000)
                    throw new ArgumentException("Unknown, duplicate, or invalid scenario input: " + property.Name);
            }
        }
        var document = ScenarioXmlParser.Read(xml);
        foreach (var input in document.Root!.Element("inputs")?.Elements("input") ?? [])
            if (supplied.TryGetValue((string)input.Attribute("name")!, out var value)) input.SetAttributeValue("value", value);
        string fullXml = document.ToString(SaveOptions.DisableFormatting);
        var filled = ScenarioXmlParser.Parse(fullXml);
        string[] missing = filled.Inputs.Where(i => i.Required && string.IsNullOrWhiteSpace(i.Value)).Select(i => i.Name).ToArray();
        if (missing.Length > 0)
            return new ToolExecutionResult(JsonSerializer.Serialize(new { status = "needs_input", scenario = definition.Id, missing,
                request = "Ask the user for these values. No scenario steps have run." }),
                "Please provide the following values for the " + definition.Title + " scenario: " + string.Join(", ", missing) + ".");
        ScenarioRunResult run;
        try { run = await new ScenarioExecutor(parent).RunXmlAsync(fullXml, cancellationToken); }
        catch (ScenarioExecutionException ex)
        {
            return new ToolExecutionResult(JsonSerializer.Serialize(new { status = "failed", run_id = ex.RunId, error = ex.Message }));
        }
        return new ToolExecutionResult(JsonSerializer.Serialize(new
        {
            status = "completed", run_id = run.RunId, scenario = run.ScenarioId, output = run.Output,
            exit_mode = run.ExitMode, exit_request = run.ExitRequest
        }), run.ExitMode == "copy" ? run.Output : null);
    }
}

public static class ScenarioTools
{
    public static void Register(ProviderClient provider, string? scenarioRoot = null) =>
        provider.RegisterTool(new RunScenarioTool(provider, new ScenarioFileService(scenarioRoot)));
}