diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-08-11 00:04:36 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-08-11 00:04:36 +0900 |
| commit | d5b3cb20c6dacd31bb54c52a30a0f64487e30215 (patch) | |
| tree | 1461ed9f6c3ad42488485045f7295f391928e7b7 /src/Agent.Core | |
| parent | e8b625e22f343d75e1b50d1634b4ab06899d060b (diff) | |
feat: P1 tool improvements and P5 Agent Skills standard compliance
P1: Tool小改善
1. tailバイナリ判定 (TextFileDetector):
- file_search tailモードでtext/binary判定を追加
- 先頭4KBのみ読み、BOM確認(UTF-8/UTF-16 LE/BE)→UTF-8 strict byte検証
- NULバイトのみ判定不可(UTF-16でNULが正常出現するため)
- binaryファイルはToolValidationExceptionで拒否
- TextFileDetectorクラスへ判定ロジックを分離
2. file_edit rename機能:
- file_edit.operation=renameを追加(新規toolではない)
- new_path property追加、Write capability要求(source/destination双方)
- destination既存時は禁止(overwrite不可)
- cross-volume renameは不支持、Tool failureとして拒否
- FileRenameTransaction + RecoveryManager file.rename recovery追加
- crash recovery: destination hash == journal記録の場合のみ戻す
P5: Agent Skills open standard準拠
3. Skill形式をagentskills.io標準へ移行:
- legacy単一Markdown形式(skills/*.md)を廃止
- <skill-name>/SKILL.md標準directory形式のみ対応
- YAML frontmatter必須(name/description)、YamlDotNetで解析
- validation: name必須(1-64文字,lowercase英数字+hyphen,親dir名一致),
description必須(1-1024文字), malformed YAML reject, duplicate fatal
4. Progressive Disclosure:
- Stage 1 Discovery: name+descriptionのみをsystem messageへ(name+desc一覧)
- Stage 3 Instructions: 選択Skillの本文のみ注入
- PromptComposer: 全文埋め込みを廃止、カタログ形式へ変更
- PromptContext: Skills→AvailableSkills+SelectedSkillsへ分割
- 未選択Skill本文はpromptへ入らない
5. Internal Tool境界維持:
- skill_read等のInternal Tool追加なし
- Internal Toolは6群(file_search/file_read/file_edit/process_run/task_update/dir)のまま
- Skill選択・本文ロードはAgent Runtime/Prompt Context Managementの責務
6. AgentSkill data model (Agent.Core/Skills/AgentSkill.cs):
- RootPath, Name, Description, Instructions, License?, Compatibility?, Metadata?, AllowedTools?
- SkillLoader(Agent.App)がYamlDotNetでfrontmatter解析→AgentSkill構築
設計判断:
- renameはfile_edit operation拡張(新規toolではない)
- rmdir空dirのみ制約は維持
- Skill load != permission grant維持
- allowed-toolsはSandbox permissionとして解釈しない
- YamlDotNetはAgent.App側のみ使用(Agent.Coreへ依存追加なし)
変更対象:
- TextFileDetector.cs: 新規、text/binary判定
- WindowsToolBackend.cs: tail binary判定統合、rename case追加
- InternalToolCatalog.cs: file_edit operation enum に rename 追加、new_path property
- InternalToolValidator.cs: rename時new_path必須検証
- TransactionalFileEditor.cs: Rename() + FileRenameTransaction
- RecoveryManager.cs: file.rename recovery
- AgentSkill.cs: 新規data model
- SkillLoader.cs: 全面書き換え、YamlDotNet frontmatter解析、validation
- PromptComposer.cs: Progressive Disclosure、AvailableSkills+SelectedSkills
- AgentRuntimeSettings.cs: Skills型変更
- AgentRuntime.cs: SelectedSkills=[]で初期化
- Program.cs: SkillLoader戻り値型対応
- Specifications.md: FS-07 rename、Skill節Agent Skills standard準拠
- Design.md: §7.1 tail binary判定、§7.3 rename、AgentSkill model
- ImplementationPlan.md: Prompt順序更新、Skill標準準拠
テスト:
- SecurityPolicyTests.cs: tail binary拒否/UTF-8/UTF-16 text許可、rename正常/dest既存拒否/許可外拒否
- InternalToolTests.cs: rename validationテスト追加
- ConfigurationTests.cs: SkillLoader標準形式/missing frontmatter/name mismatch/duplicate tests
- PromptAndRuntimeTests.cs: PromptContext型変更対応、skill catalog検証
検証結果:
- Release build: 警告0、エラー0
- Core tests: 61/61 PASS、skip 0
- Planner tests: 10/10 PASS、skip 0
- Internal Tool 6群固定: 確認済み
Diffstat (limited to 'src/Agent.Core')
| -rw-r--r-- | src/Agent.Core/Prompts/PromptComposer.cs | 22 | ||||
| -rw-r--r-- | src/Agent.Core/Runtime/AgentRuntime.cs | 2 | ||||
| -rw-r--r-- | src/Agent.Core/Runtime/AgentRuntimeSettings.cs | 4 | ||||
| -rw-r--r-- | src/Agent.Core/Skills/AgentSkill.cs | 15 | ||||
| -rw-r--r-- | src/Agent.Core/Tools/InternalToolCatalog.cs | 2 | ||||
| -rw-r--r-- | src/Agent.Core/Tools/InternalToolValidator.cs | 2 |
6 files changed, 43 insertions, 4 deletions
diff --git a/src/Agent.Core/Prompts/PromptComposer.cs b/src/Agent.Core/Prompts/PromptComposer.cs index e07c7e8..69b18f3 100644 --- a/src/Agent.Core/Prompts/PromptComposer.cs +++ b/src/Agent.Core/Prompts/PromptComposer.cs @@ -1,5 +1,6 @@ using Agent.Core.Ollama; using Agent.Core.Sessions; +using Agent.Core.Skills; namespace Agent.Core.Prompts; @@ -7,7 +8,8 @@ public sealed record PromptContext( string FixedSystemPrompt, string UserSystemPrompt, string Locale, - IReadOnlyList<string> Skills, + IReadOnlyList<AgentSkill> AvailableSkills, + IReadOnlyList<AgentSkill> SelectedSkills, string SandboxCapabilitySummary, string TaskGraphSummary, string PreviousSummary, @@ -24,13 +26,29 @@ public static class PromptComposer new("system", context.FixedSystemPrompt), new("system", context.UserSystemPrompt), new("system", $"Respond in locale: {context.Locale}."), - new("system", string.Join("\n\n", context.Skills)), + new("system", ComposeSkillCatalog(context.AvailableSkills)), new("system", context.SandboxCapabilitySummary), new("system", context.TaskGraphSummary), new("system", context.PreviousSummary) }; + // Inject selected skill instructions (Progressive Disclosure Stage 3). + foreach (AgentSkill skill in context.SelectedSkills) + messages.Add(new("system", skill.Instructions)); messages.AddRange(context.Conversation.Select(static item => new OllamaMessage(item.Role, item.Content))); messages.Add(new OllamaMessage("user", context.Request, context.Images)); return messages; } + + /// <summary> + /// Stage 1 Discovery: only name + description are included. Full instructions are NOT included. + /// </summary> + private static string ComposeSkillCatalog(IReadOnlyList<AgentSkill> skills) + { + if (skills.Count == 0) return "No skills available."; + var lines = new List<string> { "Available skills:" }; + foreach (AgentSkill skill in skills) + lines.Add($"- {skill.Name}: {skill.Description}"); + lines.Add("If a skill is relevant to the request, indicate which skills to use."); + return string.Join("\n", lines); + } } diff --git a/src/Agent.Core/Runtime/AgentRuntime.cs b/src/Agent.Core/Runtime/AgentRuntime.cs index 0fcf7d0..228ac41 100644 --- a/src/Agent.Core/Runtime/AgentRuntime.cs +++ b/src/Agent.Core/Runtime/AgentRuntime.cs @@ -1,6 +1,7 @@ using Agent.Core.Ollama; using Agent.Core.Prompts; using Agent.Core.Sessions; +using Agent.Core.Skills; using Agent.Core.Tasks; using Agent.Core.Tools; @@ -32,6 +33,7 @@ public sealed class AgentRuntime(TaskRegistry registry, IOllamaClient ollama, Ag settings.UserSystemPrompt, settings.Locale, settings.Skills, + [], // SelectedSkills: populated after LLM skill selection (Progressive Disclosure Stage 2→3). settings.SandboxCapabilitySummary, "Current task: " + task.Request, string.Empty, diff --git a/src/Agent.Core/Runtime/AgentRuntimeSettings.cs b/src/Agent.Core/Runtime/AgentRuntimeSettings.cs index 9bc04b9..65ee59d 100644 --- a/src/Agent.Core/Runtime/AgentRuntimeSettings.cs +++ b/src/Agent.Core/Runtime/AgentRuntimeSettings.cs @@ -1,9 +1,11 @@ +using Agent.Core.Skills; + namespace Agent.Core.Runtime; public sealed record AgentRuntimeSettings( string UserSystemPrompt, string Locale, - IReadOnlyList<string> Skills, + IReadOnlyList<AgentSkill> Skills, string SandboxCapabilitySummary, int MaxIterations = 128, int SameFailureLimit = 5, diff --git a/src/Agent.Core/Skills/AgentSkill.cs b/src/Agent.Core/Skills/AgentSkill.cs new file mode 100644 index 0000000..fb0e6e9 --- /dev/null +++ b/src/Agent.Core/Skills/AgentSkill.cs @@ -0,0 +1,15 @@ +namespace Agent.Core.Skills; + +/// <summary> +/// Agent Skills open standard (agentskills.io) compliant skill model. +/// Populated from SKILL.md frontmatter and body by the SkillLoader. +/// </summary> +public sealed record AgentSkill( + string RootPath, + string Name, + string Description, + string Instructions, + string? License = null, + string? Compatibility = null, + IReadOnlyDictionary<string, string>? Metadata = null, + string? AllowedTools = null);
\ No newline at end of file diff --git a/src/Agent.Core/Tools/InternalToolCatalog.cs b/src/Agent.Core/Tools/InternalToolCatalog.cs index 3ab17b1..14cba45 100644 --- a/src/Agent.Core/Tools/InternalToolCatalog.cs +++ b/src/Agent.Core/Tools/InternalToolCatalog.cs @@ -17,7 +17,7 @@ public static class InternalToolCatalog [ Define("file_search", "Search files under an approved root.", """{"type":"object","additionalProperties":false,"required":["root","mode","query"],"properties":{"root":{"type":"string"},"mode":{"type":"string","enum":["glob","name","text","regex","tail"]},"query":{"type":"string"},"max_results":{"type":"integer","minimum":1,"maximum":1000},"tail_lines":{"type":"integer","minimum":1,"maximum":10000}}}"""), Define("file_read", "Read an approved file.", """{"type":"object","additionalProperties":false,"required":["path","mode"],"properties":{"path":{"type":"string"},"mode":{"type":"string","enum":["whole","range","metadata"]},"start":{"type":"integer","minimum":0},"length":{"type":"integer","minimum":0}}}"""), - Define("file_edit", "Edit an approved file transactionally.", """{"type":"object","additionalProperties":false,"required":["path","operation"],"properties":{"path":{"type":"string"},"operation":{"type":"string","enum":["overwrite","patch","atomic_replace","delete"]},"content":{"type":["string","null"]},"patch":{"type":["string","null"]},"expected_sha256":{"type":["string","null"]}}}"""), + Define("file_edit", "Edit an approved file transactionally.", """{"type":"object","additionalProperties":false,"required":["path","operation"],"properties":{"path":{"type":"string"},"operation":{"type":"string","enum":["overwrite","patch","atomic_replace","delete","rename"]},"content":{"type":["string","null"]},"patch":{"type":["string","null"]},"expected_sha256":{"type":["string","null"]},"new_path":{"type":["string","null"]}}}"""), Define("process_run", "Run one approved executable with separated arguments.", """{"type":"object","additionalProperties":false,"required":["executable","args","cwd"],"properties":{"executable":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"cwd":{"type":"string"},"timeout_seconds":{"type":"integer","minimum":1},"stdin":{"type":["string","null"]}}}"""), Define("task_update", "Update the deterministic task graph.", """{"type":"object","additionalProperties":false,"required":["operation"],"properties":{"operation":{"type":"string","enum":["add","add_dependency","set_state","set_estimate"]},"task_id":{"type":["string","null"]},"description":{"type":["string","null"]},"depends_on":{"type":"array","items":{"type":"string"}},"budget_class":{"type":["string","null"],"enum":["1K","2K","4K","8K","16K","32K","64K","128K",null]},"optimistic_tokens":{"type":"integer","minimum":0},"most_likely_tokens":{"type":"integer","minimum":0},"pessimistic_tokens":{"type":"integer","minimum":0},"state":{"type":["string","null"],"enum":["queued","planning","running","completed","failed","cancelled",null]}}}"""), Define("dir", "Create, remove, or list directories under an approved root. Only empty directories can be removed.", """{"type":"object","additionalProperties":false,"required":["path","operation"],"properties":{"path":{"type":"string"},"operation":{"type":"string","enum":["mkdir","rmdir","list"]}}}""") diff --git a/src/Agent.Core/Tools/InternalToolValidator.cs b/src/Agent.Core/Tools/InternalToolValidator.cs index 091b790..d789dbf 100644 --- a/src/Agent.Core/Tools/InternalToolValidator.cs +++ b/src/Agent.Core/Tools/InternalToolValidator.cs @@ -66,6 +66,8 @@ public static class InternalToolValidator throw new ToolValidationException($"{operation} requires content."); if (operation == "patch" && (!arguments.TryGetProperty("patch", out JsonElement patch) || patch.ValueKind != JsonValueKind.String)) throw new ToolValidationException("patch requires patch text."); + if (operation == "rename" && (!arguments.TryGetProperty("new_path", out JsonElement newPath) || newPath.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(newPath.GetString()))) + throw new ToolValidationException("rename requires new_path."); } if (name == "task_update") { |
