simple-agent-extension 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +16 -0
- data/.standard.yml +5 -0
- data/AGENTS.md +29 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE +24 -0
- data/README.md +215 -0
- data/Rakefile +43 -0
- data/adr/adr-0001-single-source-multi-agent-compilation.md +118 -0
- data/adr/adr-0002-source-package-model-and-metadata-dsl.md +92 -0
- data/adr/adr-0003-compile-time-artifact-scope.md +67 -0
- data/adr/adr-0004-external-agents-and-run-time-agent-selection.md +78 -0
- data/config/geminabox.ru +9 -0
- data/exe/simple-agent-extension +8 -0
- data/lib/simple-agent-extension.rb +1 -0
- data/lib/simple_agent_extension/agent_base.rb +119 -0
- data/lib/simple_agent_extension/agent_directory_loader.rb +60 -0
- data/lib/simple_agent_extension/agent_property/deployment.rb +23 -0
- data/lib/simple_agent_extension/agent_property/metadata_translator.rb +55 -0
- data/lib/simple_agent_extension/agent_property/skill.rb +29 -0
- data/lib/simple_agent_extension/agent_property.rb +8 -0
- data/lib/simple_agent_extension/agent_registry.rb +99 -0
- data/lib/simple_agent_extension/agents/claude_code.rb +62 -0
- data/lib/simple_agent_extension/agents/copilot.rb +30 -0
- data/lib/simple_agent_extension/agents/opencode.rb +35 -0
- data/lib/simple_agent_extension/agents.rb +11 -0
- data/lib/simple_agent_extension/cli.rb +145 -0
- data/lib/simple_agent_extension/collector.rb +86 -0
- data/lib/simple_agent_extension/compiler.rb +185 -0
- data/lib/simple_agent_extension/deployer.rb +60 -0
- data/lib/simple_agent_extension/deployment_report.rb +52 -0
- data/lib/simple_agent_extension/extensions/agent.rb +23 -0
- data/lib/simple_agent_extension/extensions/base.rb +73 -0
- data/lib/simple_agent_extension/extensions/skill.rb +10 -0
- data/lib/simple_agent_extension/extensions.rb +29 -0
- data/lib/simple_agent_extension/frontmatter.rb +48 -0
- data/lib/simple_agent_extension/metadata.rb +27 -0
- data/lib/simple_agent_extension/metadata_composer.rb +32 -0
- data/lib/simple_agent_extension/metadata_loader.rb +63 -0
- data/lib/simple_agent_extension/runner.rb +95 -0
- data/lib/simple_agent_extension/version.rb +5 -0
- data/lib/simple_agent_extension.rb +23 -0
- data/packages/deep-review/agent/deep-design-reviewer.md +184 -0
- data/packages/deep-review/agent/metadata.yaml +9 -0
- data/packages/deep-review/skill/SKILL.md +122 -0
- data/packages/deep-review/skill/metadata.yaml +6 -0
- metadata +92 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "ADR-0004: 外部 Agent の読み込みと実行時の Agent 選択"
|
|
3
|
+
status: "Accepted"
|
|
4
|
+
date: "2026-10-02"
|
|
5
|
+
authors: "T.Watanabe (maintainer)"
|
|
6
|
+
tags: ["architecture", "decision", "extensibility", "security", "agent"]
|
|
7
|
+
supersedes: ""
|
|
8
|
+
superseded_by: ""
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# ADR-0004: 外部 Agent の読み込みと実行時の Agent 選択
|
|
12
|
+
|
|
13
|
+
## Status
|
|
14
|
+
|
|
15
|
+
Proposed | **Accepted** | Rejected | Superseded | Deprecated
|
|
16
|
+
|
|
17
|
+
## Context
|
|
18
|
+
|
|
19
|
+
- 利用者は、組み込み以外の Agent 製品(社内ツールなど)にも対応させたい。
|
|
20
|
+
- Agent class は Ruby コードであり、読み込むことは任意コードの実行を意味する。
|
|
21
|
+
- 一回の実行で対象とする Agent を絞りたい場合がある。これを、source 側の配布宣言(`deploy_to`)と混同したくない。
|
|
22
|
+
- インストールされていない Agent の設定ディレクトリを誤って作りたくない。
|
|
23
|
+
|
|
24
|
+
## Decision
|
|
25
|
+
|
|
26
|
+
本 ADR は ADR-0001 の PRI-002(新しい製品には Agent class の追加で対応する)を、本体の外にある Agent class にまで広げる。
|
|
27
|
+
|
|
28
|
+
### 外部 Agent は信頼されたローカルコードとして明示的に読み込む
|
|
29
|
+
|
|
30
|
+
- 利用者が指定したディレクトリの直下にある Ruby ファイルだけを読み込む。自動探索、再帰読み込み、リモート取得は行わない。
|
|
31
|
+
- 各ファイルは自己完結し、新しい Agent class を登録しなければならない。他のファイルの Agent class に依存する定義はサポートしない(読み込み順は契約ではない)。読み込み後は組み込みの Agent と区別せずに扱う。
|
|
32
|
+
- 読み込みの失敗、Agent の未登録、Agent 名の重複が起きた場合は、build や deploy を始める前に停止する。
|
|
33
|
+
- sandbox は提供しない。信頼できるコードだけを読み込むのは利用者の責任とし、そのことを文書に明記する。
|
|
34
|
+
|
|
35
|
+
### 実行時の選択と配布宣言を分ける
|
|
36
|
+
|
|
37
|
+
- 実行時の Agent 指定は、その回の実行対象を絞るだけとする。artifact の内容や scope は変えない。
|
|
38
|
+
- 未知の Agent 名の扱いは、層によって変える。実行時の指定は利用者のその場の意図なのでエラーにする。`deploy_to` はどの Agent が構成されているか(外部 Agent を含む)に左右される source 側の宣言なので、警告を出して無視する(ADR-0002)。
|
|
39
|
+
|
|
40
|
+
### 未インストールの Agent には既定で配置しない
|
|
41
|
+
|
|
42
|
+
- Agent の設定ルートが存在しなければ、その Agent への配置を警告付きでスキップする。明示的に指定したときだけ、ディレクトリを作成して配置する。
|
|
43
|
+
|
|
44
|
+
## Consequences
|
|
45
|
+
|
|
46
|
+
### Positive
|
|
47
|
+
|
|
48
|
+
- **POS-001**: 本体を変更せずに、任意の Agent 製品に対応できる。
|
|
49
|
+
- **POS-002**: 読み込まれるコードが利用者から常に見える。
|
|
50
|
+
- **POS-003**: 構成の誤りを実行前に検出でき、build や deploy の途中結果を残さない。
|
|
51
|
+
- **POS-004**: 一回の実行で対象を絞っても、source の配布宣言の意味は変わらない。
|
|
52
|
+
- **POS-005**: インストールされていない製品のディレクトリを誤って作成しない。
|
|
53
|
+
|
|
54
|
+
### Negative
|
|
55
|
+
|
|
56
|
+
- **NEG-001**: 外部 Agent の読み込みは任意コードの実行であり、安全性は利用者の判断に依存する。
|
|
57
|
+
|
|
58
|
+
## Alternatives Considered
|
|
59
|
+
|
|
60
|
+
### 外部 Agent の自動探索・再帰読み込み・リモート取得
|
|
61
|
+
|
|
62
|
+
- **ALT-001**: **Description**: 規約上の場所を自動で探索する、サブディレクトリを再帰的に読み込む、マーケットプレイスやリモートから取得する。
|
|
63
|
+
- **ALT-002**: **Rejection Reason**: どのコードが読み込まれるのかが利用者から見えにくくなり、信頼境界が曖昧になる。
|
|
64
|
+
|
|
65
|
+
### 外部 Agent の sandbox 実行
|
|
66
|
+
|
|
67
|
+
- **ALT-003**: **Description**: 外部 Agent class を隔離環境で評価する。
|
|
68
|
+
- **ALT-004**: **Rejection Reason**: Ruby で実用的な sandbox を作るのは難しく、コストに見合わない。信頼モデルを文書化する方針とした。
|
|
69
|
+
|
|
70
|
+
### インストール判定に extension 用サブディレクトリを用いる
|
|
71
|
+
|
|
72
|
+
- **ALT-005**: **Description**: 設定ルート配下の `agents/` や `skills/` があるかどうかで判定する。
|
|
73
|
+
- **ALT-006**: **Rejection Reason**: インストール直後の製品はこれらのサブディレクトリをまだ持たないことがあり、誤判定する。
|
|
74
|
+
|
|
75
|
+
## References
|
|
76
|
+
|
|
77
|
+
- **REF-001**: ADR-0001、ADR-0002(`deploy_to`)、ADR-0003(artifact scope)
|
|
78
|
+
- **REF-002**: `README.md`「Commands」「External Agent registrations」
|
data/config/geminabox.ru
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require_relative "simple_agent_extension"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
module SimpleAgentExtension
|
|
2
|
+
# An Agent provides metadata translation, artifact core-file placement, and
|
|
3
|
+
# deployment facts for one type at a time. Compiler and Deployer consume the
|
|
4
|
+
# Agent interface; they do not call the type-specific methods below directly.
|
|
5
|
+
#
|
|
6
|
+
# +extension_type+ selects between "skill" and "agent" through the corresponding
|
|
7
|
+
# `<type>_metadata`, `<type>_core_file`, `<type>_shared?`, and
|
|
8
|
+
# `<type>_destination` methods.
|
|
9
|
+
class AgentBase
|
|
10
|
+
class MissingMetadataTranslator < Error; end
|
|
11
|
+
|
|
12
|
+
include AgentProperty::Skill
|
|
13
|
+
|
|
14
|
+
# @param [String, nil] config_dir product-specific configuration directory
|
|
15
|
+
# @param [String, nil] shared_skill_dir common skill destination
|
|
16
|
+
def initialize(config_dir: nil, shared_skill_dir: nil)
|
|
17
|
+
@config_dir = File.expand_path(config_dir) if config_dir
|
|
18
|
+
@shared_skill_dir = File.expand_path(shared_skill_dir) if shared_skill_dir
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [String]
|
|
22
|
+
def config_dir
|
|
23
|
+
@config_dir || default_config_dir
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [Boolean] whether the Agent product is installed locally
|
|
27
|
+
def installed?
|
|
28
|
+
File.directory?(config_dir)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [String]
|
|
32
|
+
def shared_skill_dir
|
|
33
|
+
@shared_skill_dir || File.expand_path("~/.agents/skills")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [String]
|
|
37
|
+
def name
|
|
38
|
+
self.class::NAME
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [String, nil] one-line summary of the Agent product
|
|
42
|
+
def description
|
|
43
|
+
self.class::DESCRIPTION if self.class.const_defined?(:DESCRIPTION)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [String, nil] official site of the Agent product
|
|
47
|
+
def homepage
|
|
48
|
+
self.class::HOMEPAGE if self.class.const_defined?(:HOMEPAGE)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @param [String, Symbol] type
|
|
52
|
+
# @param [String, Symbol] field
|
|
53
|
+
# @return [Boolean] whether this Agent has a translation rule for the field
|
|
54
|
+
def can_trans_meta?(type:, field:)
|
|
55
|
+
metadata_translator&.can_translate?(type: type, field: field) || false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @param [String, Symbol] type
|
|
59
|
+
# @param [String, Symbol] field
|
|
60
|
+
# @param [Object] value
|
|
61
|
+
# @return [Hash, nil]
|
|
62
|
+
# @raise [MissingMetadataTranslator]
|
|
63
|
+
def trans_meta(type:, field:, value:)
|
|
64
|
+
translator = metadata_translator
|
|
65
|
+
raise MissingMetadataTranslator, "#{self.class} has no metadata translator" unless translator
|
|
66
|
+
|
|
67
|
+
translator.translate(type: type, field: field, value: value)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @return [AgentProperty::Deployment]
|
|
71
|
+
def deployment
|
|
72
|
+
@deployment ||= AgentProperty::Deployment.new(self)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @param [String, Symbol] extension_type
|
|
76
|
+
# @param [String] extension_name name the extension is deployed under
|
|
77
|
+
# @param [String] source_file file relative to the source extension directory
|
|
78
|
+
# @return [String] core file relative to the artifact type directory
|
|
79
|
+
def artifact_core_file(extension_type:, extension_name:, source_file:)
|
|
80
|
+
public_send(
|
|
81
|
+
"#{extension_type}_core_file",
|
|
82
|
+
extension_name: extension_name,
|
|
83
|
+
source_file: source_file
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def agent_shared?
|
|
88
|
+
false
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def agent_destination(scope)
|
|
92
|
+
return agent_dir if scope == :own
|
|
93
|
+
|
|
94
|
+
raise KeyError, "agent has no #{scope} destination"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# extension type agent placed in
|
|
98
|
+
#
|
|
99
|
+
# @return [String]
|
|
100
|
+
def agent_dir
|
|
101
|
+
File.join(config_dir, "agents")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def default_config_dir
|
|
105
|
+
raise NotImplementedError
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def metadata_translator
|
|
111
|
+
translator_class = metadata_translator_class
|
|
112
|
+
translator_class && (@metadata_translator ||= translator_class.new)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def metadata_translator_class
|
|
116
|
+
self.class::MetadataTranslator if self.class.const_defined?(:MetadataTranslator, false)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleAgentExtension
|
|
4
|
+
class InvalidAgentDirectory < Error; end
|
|
5
|
+
class MissingAgentRegistration < Error; end
|
|
6
|
+
class UnloadableAgentRegistration < Error; end
|
|
7
|
+
|
|
8
|
+
# Loads trusted, explicitly selected Agent registration files for one run.
|
|
9
|
+
# Discovery is intentionally flat so a directory's registration surface is
|
|
10
|
+
# visible without implicit recursive loading. Each file must be self-contained:
|
|
11
|
+
# a registration cannot define an Agent that refers to an Agent defined in a
|
|
12
|
+
# sibling file, because load order is an implementation detail rather than a
|
|
13
|
+
# contract. Files are still sorted to keep a run reproducible.
|
|
14
|
+
class AgentDirectoryLoader
|
|
15
|
+
def initialize(directories)
|
|
16
|
+
@directories = Array(directories)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def load
|
|
20
|
+
directories.each do |directory|
|
|
21
|
+
agent_files(directory).each { |file| load_file(file) }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def directories
|
|
28
|
+
@directories.map { |directory| canonical_directory(directory) }.uniq
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def canonical_directory(directory)
|
|
32
|
+
path = File.expand_path(directory)
|
|
33
|
+
raise InvalidAgentDirectory, "invalid agent directory: #{path}" unless File.directory?(path)
|
|
34
|
+
|
|
35
|
+
path = File.realpath(path)
|
|
36
|
+
raise InvalidAgentDirectory, "invalid agent directory: #{path}" unless File.readable?(path)
|
|
37
|
+
|
|
38
|
+
path
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def agent_files(directory)
|
|
42
|
+
Dir.glob(File.join(directory, "*.rb")).sort.map { |file| File.realpath(file) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def load_file(file)
|
|
46
|
+
known_agent_classes = AgentRegistry.agent_classes
|
|
47
|
+
require_registration(file)
|
|
48
|
+
|
|
49
|
+
return unless (AgentRegistry.agent_classes - known_agent_classes).empty?
|
|
50
|
+
|
|
51
|
+
raise MissingAgentRegistration, "#{file}: did not register an Agent"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def require_registration(file)
|
|
55
|
+
require file
|
|
56
|
+
rescue StandardError, ScriptError => error
|
|
57
|
+
raise UnloadableAgentRegistration, "#{file}: #{error.class}: #{error.message}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module SimpleAgentExtension
|
|
2
|
+
module AgentProperty
|
|
3
|
+
# Target-specific destinations for already compiled build artifacts.
|
|
4
|
+
class Deployment
|
|
5
|
+
def initialize(agent)
|
|
6
|
+
@agent = agent
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# @param [String, Symbol] type
|
|
10
|
+
# @return [Boolean]
|
|
11
|
+
def shared?(type)
|
|
12
|
+
@agent.public_send("#{type}_shared?")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @param [String, Symbol] type
|
|
16
|
+
# @param [Symbol] scope
|
|
17
|
+
# @return [String]
|
|
18
|
+
def destination(type, scope)
|
|
19
|
+
@agent.public_send("#{type}_destination", scope)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module SimpleAgentExtension
|
|
2
|
+
module AgentProperty
|
|
3
|
+
# Converts one source metadata field into Agent metadata fragments. Callers
|
|
4
|
+
# retain ownership of field traversal and fragment composition.
|
|
5
|
+
class MetadataTranslator
|
|
6
|
+
class UnknownTypeAndFieldCombination < Error; end
|
|
7
|
+
|
|
8
|
+
# Class methods for defining translations
|
|
9
|
+
class << self
|
|
10
|
+
# @param [String, Symbol] type ( `skill' or `agent' or ... )
|
|
11
|
+
# @param [String, Symbol] field ( metadata's field name )
|
|
12
|
+
# @param [#call] transform
|
|
13
|
+
# @yieldparam value [Object] value of the declared source metadata field
|
|
14
|
+
# @yieldreturn [Hash, nil] Agent metadata fragment collection, or field omission
|
|
15
|
+
def define_translation(type:, field:, &transform)
|
|
16
|
+
raise ArgumentError, "define_translation requires a block" unless transform
|
|
17
|
+
|
|
18
|
+
translations[[type.to_sym, field.to_s]] = transform
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Hash{[String, String] => #call}]
|
|
22
|
+
def translations
|
|
23
|
+
@translations ||= {}
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Hash{[String, String] => #call}]
|
|
28
|
+
def translations
|
|
29
|
+
self.class.translations
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param [String] type
|
|
33
|
+
# @param [String] field
|
|
34
|
+
# @return [Boolean] whether a translation rule is registered
|
|
35
|
+
def can_translate?(type:, field:)
|
|
36
|
+
self.class.translations.key?([type.to_sym, field.to_s])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# translation entrypoint
|
|
40
|
+
#
|
|
41
|
+
# @param [String, Symbol] type
|
|
42
|
+
# @param [String, Symbol] field
|
|
43
|
+
# @param [Object] value
|
|
44
|
+
# @return [Hash, nil] Agent metadata fragment collection, or omission
|
|
45
|
+
# @raise [UnknownTypeAndFieldCombination] when no translation is registered for type and field
|
|
46
|
+
def translate(type:, field:, value:)
|
|
47
|
+
transform = self.class.translations.fetch([type.to_sym, field.to_s]) {
|
|
48
|
+
raise UnknownTypeAndFieldCombination.new("Unknown combination of type: `#{type}' and field: `#{field}'")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
transform.call(value)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module SimpleAgentExtension
|
|
2
|
+
module AgentProperty
|
|
3
|
+
# The area several targets read, and the shared shape of a skill's
|
|
4
|
+
# metadata.
|
|
5
|
+
#
|
|
6
|
+
# Skills live in the shared area until a target needs something of its
|
|
7
|
+
# own lowered into them; at that point compile sends them to +own+.
|
|
8
|
+
module Skill
|
|
9
|
+
def skill_shared?
|
|
10
|
+
true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def skill_destination(scope)
|
|
14
|
+
case scope
|
|
15
|
+
when :shared then shared_skill_dir
|
|
16
|
+
when :own then File.join(config_dir, "skills")
|
|
17
|
+
else raise KeyError, "skill has no #{scope} destination"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @param [String] extension_name
|
|
22
|
+
# @param [String] source_file
|
|
23
|
+
# @return [String] core file relative to the artifact type directory
|
|
24
|
+
def skill_core_file(extension_name:, source_file:)
|
|
25
|
+
File.join(extension_name, source_file)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module SimpleAgentExtension
|
|
2
|
+
class DuplicatedAgentName < Error; end
|
|
3
|
+
class DuplicatedAgentConfigurationName < Error; end
|
|
4
|
+
class UnknownAgentName < Error; end
|
|
5
|
+
|
|
6
|
+
# A configured set of deployment Agent instances for one run.
|
|
7
|
+
#
|
|
8
|
+
# Agents remains the namespace for product classes. This registry owns the
|
|
9
|
+
# runtime concerns that need an instance collection: lookup by name,
|
|
10
|
+
# membership, and duplicate-name rejection.
|
|
11
|
+
class AgentRegistry
|
|
12
|
+
# @return [AgentRegistry] all known Agents at their default destinations
|
|
13
|
+
def self.default
|
|
14
|
+
new(agent_classes.map(&:new))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Applies destination overrides without modifying the default registry or
|
|
18
|
+
# any other registry. Missing entries retain their Agent defaults; entries
|
|
19
|
+
# do not select which Agents are present.
|
|
20
|
+
#
|
|
21
|
+
# @param [Hash{String, Symbol => Hash}] overrides per-Agent initializer options
|
|
22
|
+
# @return [AgentRegistry]
|
|
23
|
+
def self.configure(overrides = {})
|
|
24
|
+
options = options_by_agent(overrides)
|
|
25
|
+
defaults = default
|
|
26
|
+
unknown = options.keys - defaults.all.map(&:name)
|
|
27
|
+
raise UnknownAgentName, "unknown agent: #{unknown.first}" unless unknown.empty?
|
|
28
|
+
|
|
29
|
+
new(defaults.all.map { |agent|
|
|
30
|
+
agent.class.new(**options.fetch(agent.name, {}))
|
|
31
|
+
})
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @return [Array<Class>] concrete Agent classes
|
|
35
|
+
def self.agent_classes
|
|
36
|
+
Agents.constants(false)
|
|
37
|
+
.map { |const| Agents.const_get(const) }
|
|
38
|
+
.select { |klass| klass.is_a?(Class) && klass < AgentBase }
|
|
39
|
+
.sort_by(&:name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Converts Agent-name keys and Agent initializer option keys to their
|
|
43
|
+
# canonical String and Symbol forms, as below.
|
|
44
|
+
#
|
|
45
|
+
# ```
|
|
46
|
+
# name[String] => {
|
|
47
|
+
# key[Symbol] => val,
|
|
48
|
+
# key[Symbol] => val
|
|
49
|
+
# }
|
|
50
|
+
# ```
|
|
51
|
+
#
|
|
52
|
+
# @param [Hash{String, Symbol => Hash}] overrides per-Agent destination options
|
|
53
|
+
# @return [Hash{String => Hash{Symbol => Object}}]
|
|
54
|
+
# @raise [ArgumentError] when names are duplicated after normalization
|
|
55
|
+
def self.options_by_agent(overrides)
|
|
56
|
+
overrides.each_with_object({}) do |(name, agent_options), options|
|
|
57
|
+
key = name.to_s
|
|
58
|
+
raise DuplicatedAgentConfigurationName, "duplicate agent configuration: #{key}" if options.key?(key)
|
|
59
|
+
|
|
60
|
+
options[key] = agent_options.transform_keys(&:to_sym)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param [Array<AgentBase>] agents configured instances for one run
|
|
65
|
+
# @raise [ArgumentError] when names are duplicated
|
|
66
|
+
def initialize(agents)
|
|
67
|
+
@agents = agents.dup.freeze
|
|
68
|
+
@by_name = @agents.each_with_object({}) do |agent, index|
|
|
69
|
+
raise DuplicatedAgentName, "duplicate agent: #{agent.name}" if index.key?(agent.name)
|
|
70
|
+
|
|
71
|
+
index[agent.name] = agent
|
|
72
|
+
end.freeze
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @yield [AgentBase]
|
|
76
|
+
# @return [Enumerator]
|
|
77
|
+
def each(&block)
|
|
78
|
+
@agents.each(&block)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @return [Array<AgentBase>]
|
|
82
|
+
def all
|
|
83
|
+
@agents
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @param [String, Symbol] name
|
|
87
|
+
# @return [AgentBase]
|
|
88
|
+
# @raise [KeyError] when no configured instance has this name
|
|
89
|
+
def fetch(name)
|
|
90
|
+
@by_name.fetch(name.to_s) { raise UnknownAgentName, "unknown agent: #{name}" }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @param [String, Symbol] name
|
|
94
|
+
# @return [Boolean]
|
|
95
|
+
def include?(name)
|
|
96
|
+
@by_name.key?(name.to_s)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module SimpleAgentExtension
|
|
2
|
+
module Agents
|
|
3
|
+
class ClaudeCode < AgentBase
|
|
4
|
+
NAME = "claude".freeze
|
|
5
|
+
DESCRIPTION = "Claude Code, Anthropic's terminal coding agent".freeze
|
|
6
|
+
HOMEPAGE = "https://www.anthropic.com/claude-code".freeze
|
|
7
|
+
|
|
8
|
+
class MetadataTranslator < ::SimpleAgentExtension::AgentProperty::MetadataTranslator
|
|
9
|
+
# Source tool names that differ from Claude Code's exact tool names.
|
|
10
|
+
# Names not listed here pass through unchanged.
|
|
11
|
+
TOOL_NAMES = {
|
|
12
|
+
"read" => "Read",
|
|
13
|
+
"edit" => "Edit",
|
|
14
|
+
"write" => "Write",
|
|
15
|
+
"bash" => "Bash",
|
|
16
|
+
"glob" => "Glob",
|
|
17
|
+
"grep" => "Grep",
|
|
18
|
+
"webfetch" => "WebFetch",
|
|
19
|
+
"websearch" => "WebSearch",
|
|
20
|
+
"task" => "Agent",
|
|
21
|
+
"todowrite" => "TodoWrite"
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
# A subagent's `tools` limits which tools it has without pre-approving
|
|
25
|
+
# them, so `allow` and `ask` become indistinguishable here.
|
|
26
|
+
define_translation type: :agent, field: "permissions" do |permissions|
|
|
27
|
+
tool_fields(permissions, "tools" => %w[allow ask], "disallowedTools" => %w[deny])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
define_translation type: :skill, field: "permissions" do |permissions|
|
|
31
|
+
tool_fields(permissions, "allowed-tools" => %w[allow], "disallowed-tools" => %w[deny])
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param [Hash{String => String}] permissions tool name to action
|
|
35
|
+
# @param [Hash{String => Array<String>}] actions_by_field output field to the actions it lists
|
|
36
|
+
# @return [Hash{String => String}] comma-separated tool names; empty fields are omitted
|
|
37
|
+
def self.tool_fields(permissions, actions_by_field)
|
|
38
|
+
actions_by_field.each_with_object({}) do |(field, actions), fields|
|
|
39
|
+
tools = permissions.filter_map { |tool, action| TOOL_NAMES.fetch(tool, tool) if actions.include?(action) }
|
|
40
|
+
fields[field] = tools.join(", ") unless tools.empty?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def skill_shared?
|
|
46
|
+
false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [String]
|
|
50
|
+
def default_config_dir
|
|
51
|
+
File.expand_path("~/.claude")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param [String] extension_name
|
|
55
|
+
# @param [String] source_file
|
|
56
|
+
# @return [String] core file relative to the artifact type directory
|
|
57
|
+
def agent_core_file(extension_name:, source_file: _source_file)
|
|
58
|
+
"#{extension_name}.md"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module SimpleAgentExtension
|
|
2
|
+
module Agents
|
|
3
|
+
class Copilot < AgentBase
|
|
4
|
+
NAME = "copilot".freeze
|
|
5
|
+
DESCRIPTION = "GitHub Copilot CLI".freeze
|
|
6
|
+
HOMEPAGE = "https://github.com/features/copilot/cli".freeze
|
|
7
|
+
|
|
8
|
+
class MetadataTranslator < ::SimpleAgentExtension::AgentProperty::MetadataTranslator
|
|
9
|
+
define_translation type: :skill, field: "permissions" do |permissions|
|
|
10
|
+
{"tools" => permissions.filter_map { |tool, action| tool if action == "allow" }}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
define_translation type: :agent, field: "permissions" do |permissions|
|
|
14
|
+
{"tools" => permissions.filter_map { |tool, action| tool if action == "allow" }}
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def default_config_dir
|
|
19
|
+
File.expand_path("~/.copilot")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param [String] extension_name
|
|
23
|
+
# @param [String] source_file
|
|
24
|
+
# @return [String] core file relative to the artifact type directory
|
|
25
|
+
def agent_core_file(extension_name:, source_file: _source_file)
|
|
26
|
+
"#{extension_name}.agent.md"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module SimpleAgentExtension
|
|
2
|
+
module Agents
|
|
3
|
+
class OpenCode < AgentBase
|
|
4
|
+
NAME = "opencode".freeze
|
|
5
|
+
DESCRIPTION = "OpenCode, an open source terminal coding agent".freeze
|
|
6
|
+
HOMEPAGE = "https://opencode.ai".freeze
|
|
7
|
+
|
|
8
|
+
class MetadataTranslator < ::SimpleAgentExtension::AgentProperty::MetadataTranslator
|
|
9
|
+
define_translation type: :skill, field: "permissions" do |permissions|
|
|
10
|
+
{"permission" => permissions}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
define_translation type: :agent, field: "name" do |_name|
|
|
14
|
+
nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
define_translation type: :agent, field: "permissions" do |permissions|
|
|
18
|
+
{"permission" => permissions}
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [String]
|
|
23
|
+
def default_config_dir
|
|
24
|
+
File.join(File.expand_path("~/.config"), "opencode")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param [String] extension_name
|
|
28
|
+
# @param [String] source_file
|
|
29
|
+
# @return [String] core file relative to the artifact type directory
|
|
30
|
+
def agent_core_file(extension_name:, source_file: _source_file)
|
|
31
|
+
"#{extension_name}.md"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module SimpleAgentExtension
|
|
2
|
+
# Concrete coding-agent product classes.
|
|
3
|
+
module Agents
|
|
4
|
+
end
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
require_relative "agent_property"
|
|
8
|
+
require_relative "agent_base"
|
|
9
|
+
require_relative "agents/claude_code"
|
|
10
|
+
require_relative "agents/copilot"
|
|
11
|
+
require_relative "agents/opencode"
|