using System.Text; namespace ClawdDotNet.Core.Organisation; /// /// #683: The "Organisation" section of an agent's system prompt — its unit, its lead, whom it /// can reach, and what changed recently. /// /// Why the code writes it and not the agent's own texts. The concept demands /// that agents are told when they move, and warns: an agent that has to infer its lead from /// a prompt escalates to the wrong place. A section generated from the tree is always true — /// a move changes it by itself, for the agent that moved and for both leads. /// /// Without units there is no section, so instances without a structure keep /// exactly the prompt they had — and the prompt cache that goes with it. /// public static class OrgBriefing { /// Starts the section; also how an existing chat context finds it to replace it. public const string Heading = "# Organisation"; /// How long a move or a new colleague is pointed out. public static readonly TimeSpan RecentChange = TimeSpan.FromDays(7); private const int MaxDescription = 120; public static string? For(OrgChart org, string agentId, DateTime nowUtc) { if (!org.HasUnits || org.Agent(agentId) is not { } agent) return null; var sb = new StringBuilder(); sb.AppendLine(Heading); var unit = org.UnitOf(agentId); if (unit is null) { sb.AppendLine("You are on the top level of this instance, above all units."); } else { sb.Append($"You belong to the unit \"{unit.Name}\" ({org.PathOf(unit.Id)})"); if (agent.UnitSinceUtc is { } since) { sb.Append($", since {since:yyyy-MM-dd}"); if (nowUtc - since < RecentChange) sb.Append(" — this is recent: your lead, your colleagues and possibly your tools changed"); } sb.AppendLine("."); if (!string.IsNullOrWhiteSpace(unit.Goal)) sb.AppendLine($"Goal of your unit: {unit.Goal.Trim()}"); } var lead = org.EffectiveLeaderOf(agentId); if (lead is not null) sb.AppendLine($"Your lead: {Name(lead)}. Questions, problems and anything outside your unit go to your lead."); else sb.AppendLine("There is no lead agent above you. What you cannot decide goes to a human: create a task for @human."); if (org.LedBy(agentId) is { } led) { sb.AppendLine($"You lead the unit \"{led.Name}\". Delegate within it, and hand what goes beyond it to your own lead."); var subUnits = org.Children(led.Id); if (subUnits.Count > 0) { sb.AppendLine("Its sub-units: " + string.Join("; ", subUnits.Select(u => org.LeaderOf(u.Id) is { } l ? $"{u.Name} (lead: {l.AgentId})" : $"{u.Name} (no lead — its members report to you)")) + "."); } var joined = org.MembersOf(led.Id) .Where(m => m.AgentId != agentId && m.UnitSinceUtc is { } s && nowUtc - s < RecentChange) .ToList(); if (joined.Count > 0) sb.AppendLine("New in your unit: " + string.Join(", ", joined.Select(m => $"{m.AgentId} since {m.UnitSinceUtc:yyyy-MM-dd}")) + "."); // The lead that was left is told as well as the one that was joined. var left = org.Agents .Where(a => string.Equals(a.PreviousUnitId, led.Id, StringComparison.OrdinalIgnoreCase) && a.UnitSinceUtc is { } s && nowUtc - s < RecentChange) .ToList(); if (left.Count > 0) sb.AppendLine("Left your unit recently: " + string.Join(", ", left.Select(a => $"{a.AgentId} (now in {(org.UnitOf(a.AgentId) is { } u ? u.Name : "the top level")})")) + ". Hand over what is still open with them."); } var contacts = org.AddressBook(agentId) .Where(id => lead is null || !string.Equals(id, lead.AgentId, StringComparison.OrdinalIgnoreCase)) .Select(id => org.Agent(id)!) .OrderBy(a => a.AgentId, StringComparer.OrdinalIgnoreCase) .ToList(); if (contacts.Count > 0) { sb.AppendLine("You can also reach:"); foreach (var contact in contacts) sb.AppendLine($"- {Name(contact)}{Describe(contact.Description)}"); } sb.Append("Agents not named here cannot be reached from your position — go through your lead."); return sb.ToString(); } /// /// Replaces the organisation section of a system prompt, appends it, or removes it — /// leaving everything before it untouched. Existing chat contexts keep their system /// prompt otherwise (#193); only this section follows the structure. /// public static string WithSection(string systemPrompt, string? section) { var start = systemPrompt.IndexOf("\n\n" + Heading + "\n", StringComparison.Ordinal); var basePrompt = start >= 0 ? systemPrompt[..start] : systemPrompt; if (start < 0 && systemPrompt.StartsWith(Heading + "\n", StringComparison.Ordinal)) basePrompt = ""; if (string.IsNullOrWhiteSpace(section)) return basePrompt; return basePrompt.Length == 0 ? section : basePrompt + "\n\n" + section; } private static string Name(Config.AgentConfig agent) => string.IsNullOrWhiteSpace(agent.DisplayName) || agent.DisplayName == agent.AgentId ? agent.AgentId : $"{agent.DisplayName} ({agent.AgentId})"; private static string Describe(string? description) { if (string.IsNullOrWhiteSpace(description)) return ""; var line = description.ReplaceLineEndings(" ").Trim(); return " — " + (line.Length > MaxDescription ? line[..MaxDescription] + "…" : line); } }