ScenarioSystem/ScenarioExecutor.cscsharp

Documentation

Lilith (Version 3)

Version 3 Lilith agent library. Entry point: Lilith.cs with partials under Boot/, Chat/, Ollama/, and SystemPrompt/.

Build the solution or run python ../Build-v3.py from this tree.

namespace Lilith.Agent.ScenarioSystem;/// <summary>Runs scenario steps as normal Lilith chat turns (tool calling allowed).</summary>internal static class ScenarioExecutor{    public static async Task<string?> RunChatStepsAsync(        Lilith lilith,        IEnumerable<ScenarioStepXml> steps,        string? scenarioId = null,        string? scenarioTitle = null)    {        var stepList = steps.OfType<ChatStepXml>().ToList();        int total = stepList.Count;        string? lastReply = null;        using var scenarioScope = scenarioId == null ? null : ScenarioLog.BeginScenario(lilith, scenarioId, scenarioTitle);        for (int i = 0; i < stepList.Count; i++)        {            var chat = stepList[i];            using var stepScope = ScenarioLog.BeginStep(lilith, i + 1, total, chat.Prompt);            var (_, reply) = await lilith.HandleInputAsync(chat.Prompt);            lastReply = reply;        }        return lastReply;    }    /// <summary>Runs steps for child scenario worker; validates optional expect_contains on assistant/tool output in transcript.</summary>    public static async Task<ScenarioRunResult> RunChatStepsWithChecksAsync(        Lilith lilith,        ScenarioXml scenario,        CancellationToken ct = default)    {        var trace = new List<string>();        string? lastReply = null;        using var scenarioScope = ScenarioLog.BeginScenario(lilith, scenario.Id, scenario.Title);        var stepList = scenario.Steps.OfType<ChatStepXml>().ToList();        int total = stepList.Count;        for (int i = 0; i < stepList.Count; i++)        {            ct.ThrowIfCancellationRequested();            var chat = stepList[i];            using var stepScope = ScenarioLog.BeginStep(lilith, i + 1, total, chat.Prompt);            var (_, reply) = await lilith.HandleInputAsync(chat.Prompt);            lastReply = reply ?? "";            trace.Add($"step: {Trunc(chat.Prompt)} -> {Trunc(lastReply)}");            if (!string.IsNullOrWhiteSpace(chat.ExpectContains)                && !(lastReply ?? "").Contains(chat.ExpectContains, StringComparison.OrdinalIgnoreCase))            {                return ScenarioRunResult.Fail(scenario.Id, $"missing expect_contains '{chat.ExpectContains}'", trace, lastReply);            }            if (!string.IsNullOrWhiteSpace(chat.ExpectEquals)                && !(lastReply ?? "").Trim().Equals(chat.ExpectEquals.Trim(), StringComparison.OrdinalIgnoreCase))            {                return ScenarioRunResult.Fail(scenario.Id, $"reply not equals '{chat.ExpectEquals}'", trace, lastReply);            }        }        return ScenarioRunResult.Pass(scenario.Id, lastReply ?? "", trace);    }    private static string Trunc(string s)    {        s = (s ?? "").Replace("\r", " ").Replace("\n", " ").Trim();        return s.Length > 120 ? s[..120] + "…" : s;    }}internal sealed record ScenarioRunResult(bool Ok, string ScenarioId, string Summary, string Trace, string? LastReply){    public static ScenarioRunResult Pass(string id, string summary, List<string> trace) =>        new(true, id, summary, string.Join(" | ", trace), summary);    public static ScenarioRunResult Fail(string id, string reason, List<string> trace, string? lastReply) =>        new(false, id, reason, string.Join(" | ", trace), lastReply);}