← All source files

V5 / f52bca255516

RunScenarioTool.cs

Lilith/Core/Scenarios/RunScenarioTool.cs

Changed in this version
CS
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. " +
        "For workspace data management, always delegate to a workspace scenario. Search with workspace_search and read only selected excerpts. " +
        "Never approve a workspace proposal yourself or change access mode. " +
        "For timers and cron jobs use schedule_create/update/cancel/delete scenarios. Use schedule_peek(name) for remaining time and schedule_list for all schedules. Never approve schedule proposals yourself. " +
        "Schedule create/update take name, kind (timer, interval or cron), seconds (whole seconds for timer/interval), cron (five fields), timezone (zone ID), message (reminder), scenario (optional saved action) and inputs_json (JSON string object for its inputs). Updates replace the complete definition. " +
        "Available scenarios and input names: " + string.Join("; ", files.ListNames().Select(name =>
        { var scenario = ScenarioXmlParser.Parse(files.LoadScenarioXml(name)); return name + "(" + string.Join(", ", scenario.Inputs.Select(i => i.Name + (i.Required ? " required" : ""))) + ")"; }));
    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 }));
        }
        if (run.Schedule is { } schedule)
            return new ToolExecutionResult(JsonSerializer.Serialize(new { status = schedule.Status, run_id = run.RunId, result = schedule,
                exit_mode = run.ExitMode, exit_request = run.ExitRequest }), schedule.Status == "needs_approval" || run.ExitMode == "copy" ? schedule.ToReply() : null);
        if (run.Workspace is { } storage)
            return new ToolExecutionResult(JsonSerializer.Serialize(new { status = storage.Status, run_id = run.RunId, result = storage,
                exit_mode = run.ExitMode, exit_request = run.ExitRequest }),
                storage.Status == "needs_approval" || run.ExitMode == "copy" ? storage.ToReply() : null);
        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)));
}