Einheiten, Stufe 2: das Adressbuch durchsetzen und jedem Agenten seinen Platz sagen (#683)
Durchgesetzt vom Code, nicht nur im Prompt beschrieben: - Nachricht und Spawn an einen Agenten ausserhalb des Adressbuchs werden vor jedem Modellaufruf abgelehnt; die Antwort nennt den offenen Weg (die eigene Leitung, sonst @human). - list_agents zeigt nur noch das Adressbuch. - task_create und task_update pruefen die Zuweisung an einen Agenten gegen dasselbe Adressbuch; sonst liesse es sich ueber eine Aufgabe umgehen. - Die Delegationstiefe folgt der Tiefe des Baums (Tiefe + 1), der eingestellte Wert bleibt Untergrenze. Unterrichtet werden, wie das Konzept verlangt: - OrgBriefing erzeugt einen Abschnitt "# Organisation" im Systemprompt: Einheit mit Pfad und Ziel, seit wann (mit Hinweis, wenn frisch), die effektive Leitung oder "@human", als Leitung die Untereinheiten und neue Mitglieder, und wen man sonst erreicht. Er ist immer wahr, weil er aus dem Baum kommt; ein Umzug aendert ihn von selbst. - In einem laufenden Chat wird nur dieser Abschnitt ersetzt; der uebrige Systemprompt bleibt, wie er war (#193). - Ohne Einheiten gibt es keinen Abschnitt und keine Einschraenkung. Bestehende Instanzen behalten ihren Prompt und ihren Prompt-Cache. AppHost setzt den Organisationsbaum als Provider (frisch je Frage, eine Aenderung gilt ab dem naechsten Lauf) und protokolliert Probleme der Struktur beim Start. Tests: AddressBookEnforcementTests (10), AssignmentReachTests (4). 1.317 gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d014a20c38
commit
626fc8d593
@@ -0,0 +1,98 @@
|
||||
using System.Text.Json;
|
||||
using ClawdDotNet.Core.Config;
|
||||
using ClawdDotNet.Core.Organisation;
|
||||
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>
|
||||
/// #683: A task for another agent is a way of reaching it. The address book that holds for
|
||||
/// messages holds here too — otherwise it could be walked around with task_create.
|
||||
/// </summary>
|
||||
public sealed class AssignmentReachTests : IDisposable
|
||||
{
|
||||
private readonly string _directory;
|
||||
private readonly SqliteStorage _storage;
|
||||
private readonly SqliteTaskRepository _repo;
|
||||
private readonly TaskboardTool _tool = new();
|
||||
|
||||
private readonly OrgChart _org = new(
|
||||
[
|
||||
new UnitConfig { Id = "amazon", Name = "Amazon", LeaderAgentId = "amazon-lead" },
|
||||
new UnitConfig { Id = "accounting", Name = "Accounting", LeaderAgentId = "accounting-lead" }
|
||||
],
|
||||
[
|
||||
new AgentConfig { AgentId = "amazon-lead", UnitId = "amazon" },
|
||||
new AgentConfig { AgentId = "ads", UnitId = "amazon" },
|
||||
new AgentConfig { AgentId = "accounting-lead", UnitId = "accounting" },
|
||||
new AgentConfig { AgentId = "bookkeeper", UnitId = "accounting" }
|
||||
]);
|
||||
|
||||
public AssignmentReachTests()
|
||||
{
|
||||
_directory = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
|
||||
_storage = new SqliteStorage(Path.Combine(_directory, "state.db"));
|
||||
_repo = new SqliteTaskRepository(_storage);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
try { Directory.Delete(_directory, recursive: true); }
|
||||
catch { /* cleanup is secondary */ }
|
||||
}
|
||||
|
||||
private Task<ToolResult> Run(string agentId, string json)
|
||||
{
|
||||
var context = new AgentToolContext(
|
||||
agentId, "inst", new Dictionary<string, object?>(), new NullStateStore(), NullLogger.Instance,
|
||||
CancellationToken.None, SharedWorkspacePath: Path.Combine(_directory, "SharedWorkspace"), Tasks: _repo)
|
||||
{
|
||||
Organisation = _org
|
||||
};
|
||||
return _tool.ExecuteAsync(JsonDocument.Parse(json).RootElement, context, CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_task_for_an_agent_outside_the_address_book_is_refused()
|
||||
{
|
||||
var result = await Run("ads", """{"action":"task_create","title":"Rechnung buchen","assignee":"@bookkeeper"}""");
|
||||
|
||||
result.Success.ShouldBeFalse();
|
||||
result.ErrorMessage.ShouldNotBeNull().ShouldContain("not in your address book");
|
||||
result.ErrorMessage.ShouldContain("amazon-lead");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_fresh_run_of_an_agent_outside_is_refused_too()
|
||||
{
|
||||
var result = await Run("ads", """{"action":"task_create","title":"Rechnung buchen","assignee":"@new:bookkeeper"}""");
|
||||
|
||||
result.Success.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_task_for_the_own_lead_or_the_human_goes_through()
|
||||
{
|
||||
(await Run("ads", """{"action":"task_create","title":"Frage an die Leitung","assignee":"@amazon-lead"}""")).Success.ShouldBeTrue();
|
||||
(await Run("ads", """{"action":"task_create","title":"Frage an den Menschen","assignee":"@human"}""")).Success.ShouldBeTrue();
|
||||
(await Run("ads", """{"action":"task_create","title":"Eigene Aufgabe","assignee":"@ads"}""")).Success.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reassigning_is_held_to_the_same_rule()
|
||||
{
|
||||
var created = await Run("ads", """{"action":"task_create","title":"Eigene Aufgabe","assignee":"@ads"}""");
|
||||
var id = created.Content!.Split(' ', ':', '\n').First(w => w.StartsWith("t-"));
|
||||
|
||||
var result = await Run("ads", $$"""{"action":"task_update","id":"{{id}}","assignee":"@bookkeeper"}""");
|
||||
|
||||
result.Success.ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user