에이전트 선택적 탐색 구조 개선과 경고 정리 반영
Some checks failed
Release Gate / gate (push) Has been cancelled

- claude-code 선택적 탐색 흐름을 참고해 Cowork/Code 시스템 프롬프트에서 folder_map 상시 선행 지시를 완화하고 glob/grep 기반 좁은 탐색을 우선하도록 조정함

- FolderMapTool 기본 depth를 2로, include_files 기본값을 false로 낮추고 MultiReadTool 최대 파일 수를 8개로 줄여 초기 과탐색 폭을 보수적으로 조정함

- AgentLoopExplorationPolicy partial을 추가해 탐색 범위 분류, broad-scan corrective hint, exploration_breadth 성능 로그를 연결함

- AgentLoopService에 탐색 범위 가이드 주입과 실행 중 탐색 폭 추적을 추가하고, 좁은 질문에서 반복적인 folder_map/대량 multi_read를 교정하도록 정리함

- DocxToHtmlConverter nullable 경고를 수정해 Release 빌드 경고 0 / 오류 0 기준을 다시 충족함

- README와 docs/DEVELOPMENT.md에 2026-04-09 10:36 (KST) 기준 개발 이력을 반영함
This commit is contained in:
2026-04-09 14:27:59 +09:00
parent 7931566212
commit 33c1db4dae
119 changed files with 4453 additions and 6943 deletions

View File

@@ -57,30 +57,30 @@ public class MarkdownSkill : IAgentTool
public async Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct)
{
// ── 필수 파라미터 ──────────────────────────────────────────────────
var hasSections = args.TryGetProperty("sections", out var sectionsEl) && sectionsEl.ValueKind == JsonValueKind.Array;
var hasContent = args.TryGetProperty("content", out var contentEl) && contentEl.ValueKind != JsonValueKind.Null;
var hasFrontmatter= args.TryGetProperty("frontmatter",out var frontEl) && frontEl.ValueKind == JsonValueKind.Object;
var hasSections = args.SafeTryGetProperty("sections", out var sectionsEl) && sectionsEl.ValueKind == JsonValueKind.Array;
var hasContent = args.SafeTryGetProperty("content", out var contentEl) && contentEl.ValueKind != JsonValueKind.Null;
var hasFrontmatter= args.SafeTryGetProperty("frontmatter",out var frontEl) && frontEl.ValueKind == JsonValueKind.Object;
if (!hasSections && !hasContent)
return ToolResult.Fail("필수 파라미터 누락: 'sections' 또는 'content' 중 하나는 반드시 제공해야 합니다.");
// path 미제공 시 title에서 자동 생성
string path;
if (args.TryGetProperty("path", out var pathEl) && pathEl.ValueKind == JsonValueKind.String
&& !string.IsNullOrWhiteSpace(pathEl.GetString()))
if (args.SafeTryGetProperty("path", out var pathEl) && pathEl.ValueKind == JsonValueKind.String
&& !string.IsNullOrWhiteSpace(pathEl.SafeGetString()))
{
path = pathEl.GetString()!;
path = pathEl.SafeGetString()!;
}
else
{
var baseTitle = (args.TryGetProperty("title", out var autoT) ? autoT.GetString() : null) ?? "document";
var baseTitle = (args.SafeTryGetProperty("title", out var autoT) ? autoT.SafeGetString() : null) ?? "document";
var safe = System.Text.RegularExpressions.Regex.Replace(baseTitle, @"[\\/:*?""<>|]", "_").Trim().TrimEnd('.');
if (safe.Length > 60) safe = safe[..60].TrimEnd();
path = (string.IsNullOrWhiteSpace(safe) ? "document" : safe) + ".md";
}
var title = args.TryGetProperty("title", out var titleEl) ? titleEl.GetString() : null;
var useToc = args.TryGetProperty("toc", out var tocEl) && tocEl.ValueKind == JsonValueKind.True;
var encodingName = args.TryGetProperty("encoding", out var encEl) ? encEl.GetString() ?? "utf-8" : "utf-8";
var title = args.SafeTryGetProperty("title", out var titleEl) ? titleEl.SafeGetString() : null;
var useToc = args.SafeTryGetProperty("toc", out var tocEl) && tocEl.ValueKind == JsonValueKind.True;
var encodingName = args.SafeTryGetProperty("encoding", out var encEl) ? encEl.SafeGetString() ?? "utf-8" : "utf-8";
// ── 경로 처리 ──────────────────────────────────────────────────────
var fullPath = FileReadTool.ResolvePath(path, context.WorkFolder);
@@ -109,7 +109,7 @@ public class MarkdownSkill : IAgentTool
foreach (var prop in frontEl.EnumerateObject())
{
var val = prop.Value.ValueKind == JsonValueKind.String
? prop.Value.GetString() ?? ""
? prop.Value.SafeGetString() ?? ""
: prop.Value.ToString();
// 값에 특수 문자가 있으면 인용
if (val.Contains(':') || val.Contains('#') || val.StartsWith('"'))
@@ -143,8 +143,8 @@ public class MarkdownSkill : IAgentTool
// 섹션 렌더링
foreach (var section in sectionsEl.EnumerateArray())
{
if (!section.TryGetProperty("type", out var typeEl)) continue;
var type = typeEl.GetString()?.ToLowerInvariant() ?? "";
if (!section.SafeTryGetProperty("type", out var typeEl)) continue;
var type = typeEl.SafeGetString()?.ToLowerInvariant() ?? "";
switch (type)
{
@@ -187,7 +187,7 @@ public class MarkdownSkill : IAgentTool
if (useToc)
{
// content에서 헤딩 파싱하여 TOC 생성
var raw = contentEl.GetString() ?? "";
var raw = contentEl.SafeGetString() ?? "";
var headings = ParseHeadingsFromContent(raw);
if (headings.Count > 0)
{
@@ -195,7 +195,7 @@ public class MarkdownSkill : IAgentTool
sb.AppendLine();
}
}
sb.Append(contentEl.GetString() ?? "");
sb.Append(contentEl.SafeGetString() ?? "");
}
// ── 파일 쓰기 ──────────────────────────────────────────────────
@@ -221,9 +221,9 @@ public class MarkdownSkill : IAgentTool
var list = new List<(int, string)>();
foreach (var s in sections.EnumerateArray())
{
if (!s.TryGetProperty("type", out var t) || t.GetString() != "heading") continue;
var level = s.TryGetProperty("level", out var lEl) ? lEl.GetInt32() : 2;
var text = s.TryGetProperty("text", out var xEl) ? xEl.GetString() ?? "" : "";
if (!s.SafeTryGetProperty("type", out var t) || t.SafeGetString() != "heading") continue;
var level = s.SafeTryGetProperty("level", out var lEl) ? lEl.GetInt32() : 2;
var text = s.SafeTryGetProperty("text", out var xEl) ? xEl.SafeGetString() ?? "" : "";
if (!string.IsNullOrEmpty(text))
list.Add((Math.Clamp(level, 1, 6), text));
}
@@ -273,15 +273,15 @@ public class MarkdownSkill : IAgentTool
private static void RenderHeading(StringBuilder sb, JsonElement s)
{
var level = s.TryGetProperty("level", out var lEl) ? Math.Clamp(lEl.GetInt32(), 1, 6) : 2;
var text = s.TryGetProperty("text", out var tEl) ? tEl.GetString() ?? "" : "";
var level = s.SafeTryGetProperty("level", out var lEl) ? Math.Clamp(lEl.GetInt32(), 1, 6) : 2;
var text = s.SafeTryGetProperty("text", out var tEl) ? tEl.SafeGetString() ?? "" : "";
sb.AppendLine($"{new string('#', level)} {text}");
sb.AppendLine();
}
private static void RenderParagraph(StringBuilder sb, JsonElement s)
{
var text = s.TryGetProperty("text", out var tEl) ? tEl.GetString() ?? "" : "";
var text = s.SafeTryGetProperty("text", out var tEl) ? tEl.SafeGetString() ?? "" : "";
if (!string.IsNullOrWhiteSpace(text))
{
sb.AppendLine(text);
@@ -291,12 +291,12 @@ public class MarkdownSkill : IAgentTool
private static void RenderTable(StringBuilder sb, JsonElement s)
{
if (!s.TryGetProperty("headers", out var headersEl) || headersEl.ValueKind != JsonValueKind.Array)
if (!s.SafeTryGetProperty("headers", out var headersEl) || headersEl.ValueKind != JsonValueKind.Array)
return;
if (!s.TryGetProperty("rows", out var rowsEl) || rowsEl.ValueKind != JsonValueKind.Array)
if (!s.SafeTryGetProperty("rows", out var rowsEl) || rowsEl.ValueKind != JsonValueKind.Array)
return;
var headers = headersEl.EnumerateArray().Select(h => EscapeTableCell(h.GetString() ?? "")).ToList();
var headers = headersEl.EnumerateArray().Select(h => EscapeTableCell(h.SafeGetString() ?? "")).ToList();
if (headers.Count == 0) return;
// 헤더 행
@@ -322,14 +322,14 @@ public class MarkdownSkill : IAgentTool
private static void RenderList(StringBuilder sb, JsonElement s)
{
if (!s.TryGetProperty("items", out var itemsEl) || itemsEl.ValueKind != JsonValueKind.Array)
if (!s.SafeTryGetProperty("items", out var itemsEl) || itemsEl.ValueKind != JsonValueKind.Array)
return;
var ordered = s.TryGetProperty("ordered", out var ordEl) && ordEl.ValueKind == JsonValueKind.True;
var ordered = s.SafeTryGetProperty("ordered", out var ordEl) && ordEl.ValueKind == JsonValueKind.True;
int idx = 1;
foreach (var item in itemsEl.EnumerateArray())
{
var text = item.ValueKind == JsonValueKind.String ? item.GetString() ?? "" : item.ToString();
var text = item.ValueKind == JsonValueKind.String ? item.SafeGetString() ?? "" : item.ToString();
// 들여쓰기 보존 (앞에 공백/탭이 있으면 그대로)
var prefix = ordered ? $"{idx++}. " : "- ";
if (text.StartsWith(" ") || text.StartsWith("\t"))
@@ -342,8 +342,8 @@ public class MarkdownSkill : IAgentTool
private static void RenderCallout(StringBuilder sb, JsonElement s)
{
var style = s.TryGetProperty("style", out var stEl) ? stEl.GetString()?.ToUpperInvariant() ?? "INFO" : "INFO";
var text = s.TryGetProperty("text", out var tEl) ? tEl.GetString() ?? "" : "";
var style = s.SafeTryGetProperty("style", out var stEl) ? stEl.SafeGetString()?.ToUpperInvariant() ?? "INFO" : "INFO";
var text = s.SafeTryGetProperty("text", out var tEl) ? tEl.SafeGetString() ?? "" : "";
// 표준 GitHub/Obsidian 콜아웃 형식
sb.AppendLine($"> [!{style}]");
@@ -354,8 +354,8 @@ public class MarkdownSkill : IAgentTool
private static void RenderCode(StringBuilder sb, JsonElement s)
{
var lang = s.TryGetProperty("language", out var lEl) ? lEl.GetString() ?? "" : "";
var code = s.TryGetProperty("code", out var cEl) ? cEl.GetString() ?? "" : "";
var lang = s.SafeTryGetProperty("language", out var lEl) ? lEl.SafeGetString() ?? "" : "";
var code = s.SafeTryGetProperty("code", out var cEl) ? cEl.SafeGetString() ?? "" : "";
sb.AppendLine($"```{lang}");
sb.AppendLine(code);
@@ -365,8 +365,8 @@ public class MarkdownSkill : IAgentTool
private static void RenderQuote(StringBuilder sb, JsonElement s)
{
var text = s.TryGetProperty("text", out var tEl) ? tEl.GetString() ?? "" : "";
var author = s.TryGetProperty("author", out var aEl) ? aEl.GetString() : null;
var text = s.SafeTryGetProperty("text", out var tEl) ? tEl.SafeGetString() ?? "" : "";
var author = s.SafeTryGetProperty("author", out var aEl) ? aEl.SafeGetString() : null;
foreach (var line in text.Split('\n'))
sb.AppendLine($"> {line}");