Forge
csharpdeeb25a2
1#nullable enable
2
3using System.Text;
4using System.Text.Json;
5
6namespace CascadeIDE.Services;
7
8/// <summary>
9/// Превращает ответ MCP/IDE тула в читаемый текст для модели и для панели чата.
10/// Снижает «подражание JSON» у локальных LLM и выглядит ближе к подписанным результатам тула в хостах вроде Cursor.
11/// </summary>
12internal static class IdeMcpToolResultPlainFormatter
13{
14 internal const int DefaultMaxCharsForModel = 14_000;
15 internal const int DefaultMaxCharsForUiTrace = 2_800;
16 internal const int DefaultMaxCharsForSalvagePayload = CascadeIdeMafIdeAgentChat.SalvageOutcomeMaxCharsForSummary;
17
18 private const int MaxStringLeafChars = 600;
19 private const int MaxObjectProperties = 80;
20 private const int MaxArrayItems = 24;
21 private const int MaxDepth = 14;
22
23 /// <summary>Тело для возврата в цикл Agent Framework → модель (не сырой JSON при возможности).</summary>
24 internal static string ForModel(string toolDisplayName, string outcome, int maxChars = DefaultMaxCharsForModel)
25 => FormatCore(toolDisplayName, outcome, maxChars, forModel: true);
26
27 /// <summary>Краткий текст трассы в истории чата («Инструмент»).</summary>
28 internal static string ForUiTrace(string toolDisplayName, string outcome, int maxChars = DefaultMaxCharsForUiTrace)
29 => FormatCore(toolDisplayName, outcome, maxChars, forModel: false);
30
31 /// <summary>Текст в salvage user message — тоже не сырой JSON, чтобы второй проход модели не копировал скобки.</summary>
32 internal static string ForSalvagePayload(string outcome, int maxChars = DefaultMaxCharsForSalvagePayload)
33 => FormatCore("результат_инструмента", outcome, maxChars, forModel: true);
34
35 private static string FormatCore(string toolDisplayName, string outcome, int maxChars, bool forModel)
36 {
37 outcome = outcome?.Trim() ?? "";
38 if (outcome.Length == 0)
39 return ClampLine($"Инструмент «{toolDisplayName}»: (пустой ответ)", maxChars);
40
41 if (!LooksLikeStructuredJson(outcome))
42 {
43 var plain = $"Вызов: {toolDisplayName}\n{outcome}";
44 if (forModel)
45 plain = $"Инструмент «{toolDisplayName}» завершился.\n\n{outcome}";
46 return ClampLine(plain, maxChars);
47 }
48
49 try
50 {
51 using var doc = JsonDocument.Parse(outcome);
52 var sb = new StringBuilder();
53 if (forModel)
54 {
55 sb.AppendLine($"Инструмент «{toolDisplayName}» завершился.");
56 sb.AppendLine();
57 }
58 else
59 {
60 sb.AppendLine($"Вызов: {toolDisplayName}");
61 sb.AppendLine(new string('─', Math.Min(32, maxChars > 40 ? 32 : 8)));
62 }
63
64 AppendElement(sb, doc.RootElement, depth: 0, linesBudget: forModel ? 220 : 80);
65 return ClampLine(sb.ToString().TrimEnd(), maxChars);
66 }
67 catch (JsonException)
68 {
69 var plain = forModel
70 ? $"Инструмент «{toolDisplayName}» завершился.\n\n{outcome}"
71 : $"Вызов: {toolDisplayName}\n{outcome}";
72 return ClampLine(plain, maxChars);
73 }
74 }
75
76 private static bool LooksLikeStructuredJson(string s)
77 {
78 if (s.Length < 2)
79 return false;
80 var c = s[0];
81 return c is '{' or '[';
82 }
83
84 private static void AppendElement(StringBuilder sb, JsonElement el, int depth, int linesBudget)
85 {
86 var ind = IndentString(depth);
87 if (depth > MaxDepth || linesBudget <= 0)
88 {
89 sb.Append(ind).AppendLine("…");
90 return;
91 }
92
93 switch (el.ValueKind)
94 {
95 case JsonValueKind.Object:
96 {
97 var n = 0;
98 foreach (var p in el.EnumerateObject())
99 {
100 if (n >= MaxObjectProperties || linesBudget <= 0)
101 {
102 sb.Append(ind).AppendLine("… (ещё поля опущены)");
103 return;
104 }
105
106 sb.Append(ind).Append(p.Name).Append(": ");
107 if (p.Value.ValueKind is JsonValueKind.Object or JsonValueKind.Array)
108 {
109 sb.AppendLine();
110 AppendElement(sb, p.Value, depth + 1, linesBudget - 1);
111 }
112 else
113 {
114 AppendInlinePrimitive(sb, p.Value);
115 sb.AppendLine();
116 }
117
118 n++;
119 linesBudget--;
120 }
121
122 break;
123 }
124 case JsonValueKind.Array:
125 {
126 var len = el.GetArrayLength();
127 sb.Append(ind).Append("[массив, элементов: ").Append(len).AppendLine("]");
128 var i = 0;
129 foreach (var item in el.EnumerateArray())
130 {
131 var indInner = IndentString(depth + 1);
132 if (i >= MaxArrayItems || linesBudget <= 0)
133 {
134 sb.Append(indInner).AppendLine("…");
135 return;
136 }
137
138 sb.Append(indInner).Append('[').Append(i).Append(']').Append(' ');
139 if (item.ValueKind is JsonValueKind.Object or JsonValueKind.Array)
140 {
141 sb.AppendLine();
142 AppendElement(sb, item, depth + 2, linesBudget - 2);
143 }
144 else
145 {
146 AppendInlinePrimitive(sb, item);
147 sb.AppendLine();
148 }
149
150 i++;
151 linesBudget--;
152 }
153
154 break;
155 }
156 default:
157 AppendInlinePrimitive(sb, el);
158 sb.AppendLine();
159 break;
160 }
161 }
162
163 private static void AppendInlinePrimitive(StringBuilder sb, JsonElement el)
164 {
165 switch (el.ValueKind)
166 {
167 case JsonValueKind.String:
168 {
169 var t = el.GetString() ?? "";
170 if (t.Length > MaxStringLeafChars)
171 sb.Append(t.AsSpan(0, MaxStringLeafChars)).Append("… (").Append(t.Length).Append(" симв.)");
172 else
173 sb.Append(t);
174 break;
175 }
176 case JsonValueKind.Number:
177 sb.Append(el.GetRawText());
178 break;
179 case JsonValueKind.True:
180 sb.Append("да");
181 break;
182 case JsonValueKind.False:
183 sb.Append("нет");
184 break;
185 case JsonValueKind.Null:
186 case JsonValueKind.Undefined:
187 sb.Append("(нет)");
188 break;
189 default:
190 sb.Append(el.GetRawText());
191 break;
192 }
193 }
194
195 private static string IndentString(int depth)
196 {
197 var n = Math.Clamp(depth * 2, 0, 64);
198 return n == 0 ? "" : new string(' ', n);
199 }
200
201 private static string ClampLine(string s, int maxChars)
202 {
203 if (s.Length <= maxChars)
204 return s;
205 return s.AsSpan(0, Math.Max(0, maxChars - 80)).ToString()
206 + $"\n… (всего {s.Length} симв.; показано ~{maxChars - 80}.)";
207 }
208}
209
View only · write via MCP/CIDE