← All source files

V7 / 2a442462cf3f

ScenarioRunPlan.cs

Lilith/Core/Scenarios/ScenarioRunPlan.cs

Unchanged from the preceding version
CS
using Lilith.Core.Workspace.Extensions;
using Lilith.Core.Tools;
namespace Lilith.Core.Scenarios;

internal sealed class ScenarioRunPlan(ScenarioFileService files, ProviderClient owner)
{
    private readonly Dictionary<string, string> _sources = new(StringComparer.Ordinal);
    internal string Bind(string name, IReadOnlyDictionary<string, string> values)
    {
        if (!_sources.TryGetValue(name, out var xml)) _sources.Add(name, xml = files.LoadScenarioXml(name));
        return ScenarioFileService.BindXml(xml, values);
    }
    internal int Runs { get; private set; }
    internal int Steps { get; private set; }
    internal void CountRun() { if (++Runs > 32) throw new InvalidDataException("Scenario tree exceeds 32 runs."); }
    internal void CountStep() { if (++Steps > 128) throw new InvalidDataException("Scenario tree exceeds 128 executed steps."); }
    internal void Validate(ScenarioDefinition root)
    {
        int nodes = 0;
        void Visit(ScenarioDefinition scenario, HashSet<string> active)
        {
            if (active.Count >= 8 || !active.Add(scenario.Id)) throw new InvalidDataException("Nested scenario cycle or depth limit (8).");
            if (++nodes > 64) throw new InvalidDataException("Scenario plan exceeds 64 declared runs.");
            if (scenario.Inputs.Any(i => i.Required && string.IsNullOrWhiteSpace(i.Value))) throw new ArgumentException("Missing required scenario inputs.");
            var values = scenario.Inputs.ToDictionary(i => i.Name, i => i.Value ?? "", StringComparer.Ordinal);
            bool workspace = scenario.AllowedTools.Any(n => Lilith.Core.Workspace.WorkspaceTool.Operation(n) is not null);
            bool schedule = scenario.AllowedTools.Any(n => Lilith.Core.Scheduling.ScheduleTool.Operation(n) is not null);
            if ((workspace || schedule) && (scenario.FlowSteps.Length != 1 || scenario.FlowSteps[0] is not ScenarioChatStep || scenario.AllowedTools.Length != 1 || scenario.CodeTools.Length != 0))
                throw new InvalidDataException("Bound workspace/schedule operations require one chat step and one operation.");
            if (workspace && owner.Workspace is null || schedule && (owner.Schedules is null || owner.ExecutingScheduledJob)) throw new InvalidDataException("Scenario service unavailable.");
            foreach (string tool in scenario.AllowedTools)
                if (Lilith.Core.Workspace.WorkspaceTool.Operation(tool) is null && Lilith.Core.Scheduling.ScheduleTool.Operation(tool) is null &&
                    (!owner.RegisteredTools.TryGetValue(tool, out var registered) || registered is ITurnResultTool)) throw new InvalidDataException("Unavailable scenario tool: " + tool);
            string Expand(string text) => ScenarioXmlParser.Expand(text, values);
            Expand(scenario.SystemPrompt); Expand(scenario.ExitRequest);
            foreach (var step in ScenarioFlowParser.Flatten(scenario.FlowSteps))
                switch (step)
                {
                    case ScenarioChatStep chat: Expand(chat.Prompt); break;
                    case ScenarioIfStep condition:
                        if (condition.Input is not null && !values.ContainsKey(condition.Input)) throw new InvalidDataException("Unknown condition input: " + condition.Input);
                        if (condition.WorkspacePath is not null)
                            (owner.Workspace ?? throw new InvalidDataException("Workspace condition requires a configured workspace.")).Options.ResolvePath(Expand(condition.WorkspacePath));
                        if (condition.EqualsValue is not null) Expand(condition.EqualsValue);
                        if (condition.NotEqualsValue is not null) Expand(condition.NotEqualsValue);
                        break;
                    case ScenarioRunStep run:
                        Visit(ScenarioXmlParser.Parse(Bind(run.Scenario, run.Inputs.ToDictionary(i => i.Key, i => Expand(i.Value)))), new HashSet<string>(active, StringComparer.Ordinal));
                        break;
                }
        }
        Visit(root, new(StringComparer.Ordinal));
    }
}