Eskalation, Konflikt und Beschluss als eigene Vorgaenge (#689, Stufe 1+2)
- Cases/Decisions in der Instanz-DB; CaseDesk fuehrt die Regeln, der Brett-Task traegt den Vorgang zum jeweiligen Halter - Eskalation nur nach oben, eine Ebene je Schritt; die Leitung entscheidet (mit Begruendung, bindet ihren Bereich), erledigt selbst oder reicht mit Einschaetzung weiter - Erst nachsehen: bindende Beschluesse werden vor dem Eskalieren gezeigt; dieselbe Frage aus derselben Einheit schliesst sich dem offenen Vorgang an - Tagesdeckel je Einheit; nach der Frist wird ein Mensch gerufen (Anschluss fuer #644) - Konflikt: zuerst die Gegenseite, dann Zustellung an die niedrigste gemeinsame Leitung; Ebenen dazwischen vermerkt, nicht befragt - Fremdinhalt-Stufe (#715): Eskalation ohne eigenen Text, kein Konflikt mit Position - Jede Ebene haengt an (#697), beim Halter gerahmt als Daten - Escalation ist ein implizites Werkzeug jedes Agenten; Briefing nennt den Weg - Scanner behaelt einen im Lauf gesetzten Status blockiert/abgebrochen Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c03664cf29
commit
36fe1ad2aa
@@ -0,0 +1,98 @@
|
||||
using System.Text.Json;
|
||||
using ClawdDotNet.Core.Cases;
|
||||
using ClawdDotNet.Core.Config;
|
||||
using ClawdDotNet.Core.Organisation;
|
||||
using ClawdDotNet.Core.Security;
|
||||
using ClawdDotNet.Core.Storage;
|
||||
using ClawdDotNet.Core.Tasks;
|
||||
using ClawdDotNet.Core.Tools;
|
||||
using ClawdDotNet.Tools.Taskboard;
|
||||
using ClawdDotNet.Tools.Tests.FileRW;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Shouldly;
|
||||
|
||||
namespace ClawdDotNet.Tools.Tests.Taskboard;
|
||||
|
||||
/// <summary>#689: The agent's side of escalation — every agent has it, the engine supplies who and where.</summary>
|
||||
public sealed class EscalationToolTests : IDisposable
|
||||
{
|
||||
private readonly string _directory = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
|
||||
private readonly SqliteTaskRepository _tasks;
|
||||
private readonly TaskboardService _board;
|
||||
private readonly CaseDesk _desk;
|
||||
private readonly EscalationTool _tool = new();
|
||||
|
||||
private readonly OrgChart _org = new(
|
||||
[new UnitConfig { Id = "amazon", Name = "Amazon", LeaderAgentId = "amazon-lead" }],
|
||||
[
|
||||
new AgentConfig { AgentId = "amazon-lead", UnitId = "amazon" },
|
||||
new AgentConfig { AgentId = "ads", UnitId = "amazon" }
|
||||
]);
|
||||
|
||||
public EscalationToolTests()
|
||||
{
|
||||
var storage = new SqliteStorage(Path.Combine(_directory, "state.db"));
|
||||
_tasks = new SqliteTaskRepository(storage);
|
||||
_board = new TaskboardService(_tasks, Path.Combine(_directory, "tasks"));
|
||||
_desk = new CaseDesk(new SqliteCaseRepository(storage), _board, () => _org, () => new CaseSettings(), NullLogger.Instance);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SqliteStorage.ReleasePooledConnections(_directory);
|
||||
try { Directory.Delete(_directory, recursive: true); }
|
||||
catch { /* cleanup is secondary */ }
|
||||
}
|
||||
|
||||
private Task<ToolResult> Run(string agent, object input, string? runTaskId = null)
|
||||
=> _tool.ExecuteAsync(JsonSerializer.SerializeToElement(input), new AgentToolContext(
|
||||
agent, "inst", new Dictionary<string, object?>(), new NullStateStore(), NullLogger.Instance, CancellationToken.None)
|
||||
{
|
||||
Organisation = _org,
|
||||
Cases = _desk,
|
||||
RunId = "run-" + agent,
|
||||
RunTaskId = runTaskId
|
||||
}, CancellationToken.None);
|
||||
|
||||
[Fact]
|
||||
public async Task Escalating_in_a_task_run_holds_that_task_and_reaches_the_lead()
|
||||
{
|
||||
await _board.CreateAsync(new TaskItem { Id = "t-1", Title = "Price the chair", Assignee = "@ads" }, default);
|
||||
|
||||
var result = await Run("ads", new
|
||||
{
|
||||
action = "escalate", subject = "Minimum price for the A-100", question = "Below which price may I not go?"
|
||||
}, runTaskId: "t-1");
|
||||
|
||||
result.Success.ShouldBeTrue(result.ErrorMessage);
|
||||
result.Content.ShouldContain("amazon-lead");
|
||||
(await _tasks.GetAsync("t-1", default))!.Status.ShouldBe(TaskItemStatus.Blocked);
|
||||
|
||||
var cases = await Run("amazon-lead", new { action = "list_cases" });
|
||||
cases.Content.ShouldContain("you hold it");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_lead_decides_and_the_decision_is_found_afterwards()
|
||||
{
|
||||
var opened = await Run("ads", new { action = "escalate", subject = "Minimum price for the A-100", question = "How low?" });
|
||||
var caseId = opened.Content.Split(' ')[3].TrimEnd('.');
|
||||
|
||||
(await Run("amazon-lead", new { action = "decide", @case = caseId, answer = "Never below 89 EUR.", reasoning = "Margin." }))
|
||||
.Success.ShouldBeTrue();
|
||||
|
||||
(await Run("ads", new { action = "find_decisions", subject = "minimum price A-100" })).Content.ShouldContain("Never below 89 EUR.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Every_agent_has_it_without_being_granted_it()
|
||||
{
|
||||
var registry = new ToolRegistry();
|
||||
registry.Register(_tool);
|
||||
var agent = new AgentConfig { AgentId = "ads" };
|
||||
|
||||
registry.GetForAgent(agent).ShouldContain(t => t.Name == CaseDesk.ToolName);
|
||||
new PermissionGate().IsAllowed("ads", CaseDesk.ToolName, agent).ShouldBeTrue();
|
||||
new PermissionGate().IsAllowed("ads", "Mail", agent).ShouldBeFalse("only escalation is implicit");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user