agentf 0.3.0
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/bin/agentf +8 -0
- data/lib/agentf/agent_policy.rb +54 -0
- data/lib/agentf/agents/architect.rb +67 -0
- data/lib/agentf/agents/base.rb +53 -0
- data/lib/agentf/agents/debugger.rb +75 -0
- data/lib/agentf/agents/designer.rb +69 -0
- data/lib/agentf/agents/documenter.rb +58 -0
- data/lib/agentf/agents/explorer.rb +65 -0
- data/lib/agentf/agents/reviewer.rb +64 -0
- data/lib/agentf/agents/security.rb +84 -0
- data/lib/agentf/agents/specialist.rb +68 -0
- data/lib/agentf/agents/tester.rb +79 -0
- data/lib/agentf/agents.rb +19 -0
- data/lib/agentf/cli/architecture.rb +83 -0
- data/lib/agentf/cli/arg_parser.rb +50 -0
- data/lib/agentf/cli/code.rb +165 -0
- data/lib/agentf/cli/install.rb +112 -0
- data/lib/agentf/cli/memory.rb +393 -0
- data/lib/agentf/cli/metrics.rb +103 -0
- data/lib/agentf/cli/router.rb +111 -0
- data/lib/agentf/cli/update.rb +204 -0
- data/lib/agentf/commands/architecture.rb +183 -0
- data/lib/agentf/commands/debugger.rb +238 -0
- data/lib/agentf/commands/designer.rb +179 -0
- data/lib/agentf/commands/explorer.rb +208 -0
- data/lib/agentf/commands/memory_reviewer.rb +186 -0
- data/lib/agentf/commands/metrics.rb +272 -0
- data/lib/agentf/commands/security_scanner.rb +98 -0
- data/lib/agentf/commands/tester.rb +232 -0
- data/lib/agentf/commands.rb +17 -0
- data/lib/agentf/context_builder.rb +35 -0
- data/lib/agentf/installer.rb +580 -0
- data/lib/agentf/mcp/server.rb +310 -0
- data/lib/agentf/memory.rb +530 -0
- data/lib/agentf/packs.rb +74 -0
- data/lib/agentf/service/providers.rb +158 -0
- data/lib/agentf/tools/component_spec.rb +28 -0
- data/lib/agentf/tools/error_analysis.rb +19 -0
- data/lib/agentf/tools/file_match.rb +21 -0
- data/lib/agentf/tools/test_template.rb +17 -0
- data/lib/agentf/tools.rb +12 -0
- data/lib/agentf/version.rb +5 -0
- data/lib/agentf/workflow_contract.rb +158 -0
- data/lib/agentf/workflow_engine.rb +424 -0
- data/lib/agentf.rb +87 -0
- metadata +164 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module Agentf
|
|
7
|
+
class Installer
|
|
8
|
+
READ_ACTIONS = {
|
|
9
|
+
"get_recent_memories" => { cli: "agentf memory recent -n 10", tool: "agentf_memory_recent" },
|
|
10
|
+
"get_pitfalls" => { cli: "agentf memory pitfalls -n 10", tool: "agentf_memory_recent" },
|
|
11
|
+
"get_lessons" => { cli: "agentf memory lessons -n 10", tool: "agentf_memory_recent" },
|
|
12
|
+
"get_successes" => { cli: "agentf memory successes -n 10", tool: "agentf_memory_recent" },
|
|
13
|
+
"get_intents" => { cli: "agentf memory intents", tool: "agentf_memory_recent" },
|
|
14
|
+
"get_all_tags" => { cli: "agentf memory tags", tool: "agentf_memory_recent" },
|
|
15
|
+
"get_by_tag" => { cli: "agentf memory by-tag <tag> -n 10", tool: "agentf_memory_search" },
|
|
16
|
+
"get_by_type" => { cli: "agentf memory by-type <type> -n 10", tool: "agentf_memory_search" },
|
|
17
|
+
"get_by_agent" => { cli: "agentf memory by-agent <agent> -n 10", tool: "agentf_memory_search" },
|
|
18
|
+
"search" => { cli: "agentf memory search \"<query>\" -n 10", tool: "agentf_memory_search" },
|
|
19
|
+
"get_summary" => { cli: "agentf memory summary", tool: "agentf_memory_recent" }
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
WRITE_ACTIONS = {
|
|
23
|
+
"store_lesson" => { cli: "agentf memory add-lesson \"<title>\" \"<description>\" --agent=<AGENT> --tags=learning", tool: "agentf_memory_add_lesson" },
|
|
24
|
+
"store_success" => { cli: "agentf memory add-success \"<title>\" \"<description>\" --agent=<AGENT> --tags=success", tool: "agentf_memory_add_success" },
|
|
25
|
+
"store_pitfall" => { cli: "agentf memory add-pitfall \"<title>\" \"<description>\" --agent=<AGENT> --tags=pitfall", tool: "agentf_memory_add_pitfall" },
|
|
26
|
+
"store_business_intent" => { cli: "agentf memory add-business-intent \"<title>\" \"<description>\" --tags=strategy", tool: "agentf_memory_add_lesson" },
|
|
27
|
+
"store_feature_intent" => { cli: "agentf memory add-feature-intent \"<title>\" \"<description>\" --acceptance=\"<criteria>\"", tool: "agentf_memory_add_lesson" }
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
PROVIDER_LAYOUTS = {
|
|
31
|
+
"opencode" => {
|
|
32
|
+
"agents_dir" => ".opencode/agents",
|
|
33
|
+
"commands_dir" => ".opencode/commands",
|
|
34
|
+
"plugin_dir" => ".opencode/plugins",
|
|
35
|
+
"memory_dir" => ".opencode/memory",
|
|
36
|
+
"agent_filename" => ->(klass) { "agentf-#{klass.typed_name.downcase}.md" },
|
|
37
|
+
"command_filename" => ->(manifest) { "agentf-#{manifest.fetch('name')}.md" }
|
|
38
|
+
},
|
|
39
|
+
"copilot" => {
|
|
40
|
+
"agents_dir" => ".github/agents",
|
|
41
|
+
"commands_dir" => ".github/commands",
|
|
42
|
+
"agent_filename" => ->(klass) { "#{klass.typed_name.downcase}.agent.md" },
|
|
43
|
+
"command_filename" => ->(manifest) { "#{manifest.fetch('name')}.md" }
|
|
44
|
+
}
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
def initialize(global_root: Dir.home, local_root: Dir.pwd, dry_run: false)
|
|
48
|
+
@global_root = global_root
|
|
49
|
+
@local_root = local_root
|
|
50
|
+
@dry_run = dry_run
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def install(
|
|
54
|
+
providers: ["opencode"],
|
|
55
|
+
scope: "all",
|
|
56
|
+
only_agents: nil,
|
|
57
|
+
only_commands: nil
|
|
58
|
+
)
|
|
59
|
+
providers.flat_map do |provider|
|
|
60
|
+
install_for_provider(
|
|
61
|
+
provider: provider,
|
|
62
|
+
scope: scope,
|
|
63
|
+
only_agents: only_agents,
|
|
64
|
+
only_commands: only_commands
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def install_for_provider(provider:, scope:, only_agents:, only_commands:)
|
|
72
|
+
layout = PROVIDER_LAYOUTS.fetch(provider.to_s) do
|
|
73
|
+
raise ArgumentError, "Unknown provider: #{provider}. Valid: #{PROVIDER_LAYOUTS.keys.join(', ')}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
writes = []
|
|
77
|
+
roots_for(scope).each do |root|
|
|
78
|
+
writes.concat(write_agents(root: root, layout: layout, provider: provider, only_agents: only_agents))
|
|
79
|
+
writes.concat(write_commands(root: root, layout: layout, provider: provider, only_commands: only_commands))
|
|
80
|
+
writes.concat(write_opencode_helpers(root: root)) if provider.to_s == "opencode"
|
|
81
|
+
end
|
|
82
|
+
writes
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def roots_for(scope)
|
|
86
|
+
case scope
|
|
87
|
+
when "global"
|
|
88
|
+
[@global_root]
|
|
89
|
+
when "local"
|
|
90
|
+
[@local_root]
|
|
91
|
+
else
|
|
92
|
+
[@global_root, @local_root]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def write_agents(root:, layout:, provider:, only_agents:)
|
|
97
|
+
classes = discover_agents
|
|
98
|
+
classes = classes.select { |klass| only_agents.include?(klass.typed_name.downcase) } if only_agents
|
|
99
|
+
|
|
100
|
+
classes.map do |klass|
|
|
101
|
+
target = File.join(root, layout.fetch("agents_dir"), layout.fetch("agent_filename").call(klass))
|
|
102
|
+
write_manifest(target, render_agent_manifest(klass, provider: provider))
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def write_commands(root:, layout:, provider:, only_commands:)
|
|
107
|
+
manifests = discover_commands
|
|
108
|
+
manifests = manifests.select { |manifest| only_commands.include?(manifest.fetch("name").downcase) } if only_commands
|
|
109
|
+
|
|
110
|
+
manifests.map do |manifest|
|
|
111
|
+
target = File.join(root, layout.fetch("commands_dir"), layout.fetch("command_filename").call(manifest))
|
|
112
|
+
write_manifest(target, render_command_manifest(manifest, provider: provider))
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def write_opencode_helpers(root:)
|
|
117
|
+
writes = []
|
|
118
|
+
writes << write_manifest(
|
|
119
|
+
File.join(root, ".opencode/agents/agentf-workflow-engine.md"),
|
|
120
|
+
render_workflow_engine_manifest
|
|
121
|
+
)
|
|
122
|
+
writes << write_manifest(
|
|
123
|
+
File.join(root, ".opencode/plugins/agentf-plugin.ts"),
|
|
124
|
+
render_opencode_plugin
|
|
125
|
+
)
|
|
126
|
+
writes << write_manifest(
|
|
127
|
+
File.join(root, ".opencode/memory/agentf-redis-schema.md"),
|
|
128
|
+
render_opencode_memory_schema
|
|
129
|
+
)
|
|
130
|
+
writes << write_manifest(
|
|
131
|
+
File.join(root, "opencode.json"),
|
|
132
|
+
render_opencode_json
|
|
133
|
+
)
|
|
134
|
+
writes
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def discover_agents
|
|
138
|
+
Agentf::Agents.constants
|
|
139
|
+
.map { |const| Agentf::Agents.const_get(const) }
|
|
140
|
+
.select { |value| value.is_a?(Class) && value < Agentf::Agents::Base }
|
|
141
|
+
.reject { |klass| klass == Agentf::Agents::Base }
|
|
142
|
+
.sort_by(&:typed_name)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def discover_commands
|
|
146
|
+
Agentf::Commands.constants
|
|
147
|
+
.map { |const| Agentf::Commands.const_get(const) }
|
|
148
|
+
.select { |value| value.is_a?(Class) && value.respond_to?(:manifest) }
|
|
149
|
+
.map(&:manifest)
|
|
150
|
+
.sort_by { |manifest| manifest.fetch("name") }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def write_manifest(path, payload)
|
|
154
|
+
return { "path" => path, "status" => "planned" } if @dry_run
|
|
155
|
+
|
|
156
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
157
|
+
File.write(path, payload)
|
|
158
|
+
{ "path" => path, "status" => "written" }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def render_agent_manifest(klass, provider:)
|
|
162
|
+
meta = {
|
|
163
|
+
"name" => klass.typed_name,
|
|
164
|
+
"description" => klass.description,
|
|
165
|
+
"commands" => klass.commands,
|
|
166
|
+
"memory" => klass.memory_concepts,
|
|
167
|
+
"policy" => klass.policy_boundaries
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
<<~MARKDOWN
|
|
171
|
+
#{meta.to_yaml}---
|
|
172
|
+
#{klass.prompt}
|
|
173
|
+
|
|
174
|
+
## Memory Integration
|
|
175
|
+
- Reads: #{Array(klass.memory_concepts["reads"]).join(", ")}
|
|
176
|
+
- Writes: #{Array(klass.memory_concepts["writes"]).join(", ")}
|
|
177
|
+
- Policy: #{klass.memory_concepts["policy"]}
|
|
178
|
+
|
|
179
|
+
## Memory Actions
|
|
180
|
+
#{memory_actions_for(klass, provider: provider).join("\n")}
|
|
181
|
+
|
|
182
|
+
## Policy Boundaries
|
|
183
|
+
- Always: #{Array(klass.policy_boundaries["always"]).join("; ")}
|
|
184
|
+
- Ask first: #{Array(klass.policy_boundaries["ask_first"]).join("; ")}
|
|
185
|
+
- Never: #{Array(klass.policy_boundaries["never"]).join("; ")}
|
|
186
|
+
- Required inputs: #{Array(klass.policy_boundaries["required_inputs"]).join(", ")}
|
|
187
|
+
- Required outputs: #{Array(klass.policy_boundaries["required_outputs"]).join(", ")}
|
|
188
|
+
|
|
189
|
+
#{copilot_mcp_agent_section(provider: provider)}
|
|
190
|
+
MARKDOWN
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def memory_actions_for(klass, provider: "opencode")
|
|
194
|
+
reads = Array(klass.memory_concepts["reads"]).map { |item| item.to_s.split("#").last }
|
|
195
|
+
writes = Array(klass.memory_concepts["writes"]).map { |item| item.to_s.split("#").last }
|
|
196
|
+
|
|
197
|
+
actions = []
|
|
198
|
+
|
|
199
|
+
reads.each do |read_action|
|
|
200
|
+
next unless READ_ACTIONS[read_action]
|
|
201
|
+
|
|
202
|
+
action = format_action(READ_ACTIONS[read_action], "Read", provider)
|
|
203
|
+
actions << action if action
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
writes.each do |write_action|
|
|
207
|
+
next unless WRITE_ACTIONS[write_action]
|
|
208
|
+
|
|
209
|
+
action = format_action(WRITE_ACTIONS[write_action], "Write", provider, klass.typed_name)
|
|
210
|
+
actions << action if action
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
if actions.none? { |a| a.start_with?("- Read:") }
|
|
214
|
+
actions << "- Read: Use `agentf_memory_recent` tool"
|
|
215
|
+
end
|
|
216
|
+
if actions.none? { |a| a.start_with?("- Write:") }
|
|
217
|
+
actions << "- Write: Use `agentf_memory_add_lesson` tool"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
actions
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def format_action(action_def, type, provider, agent_name = nil)
|
|
224
|
+
case provider
|
|
225
|
+
when "opencode"
|
|
226
|
+
tool_name = action_def[:tool]
|
|
227
|
+
"- #{type}: Use `#{tool_name}` tool"
|
|
228
|
+
when "copilot"
|
|
229
|
+
cli_cmd = action_def[:cli]
|
|
230
|
+
cli_cmd = cli_cmd.gsub("<AGENT>", agent_name) if agent_name
|
|
231
|
+
"- #{type}: `#{cli_cmd}`"
|
|
232
|
+
else
|
|
233
|
+
"- #{type}: `#{action_def[:cli]}`"
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def render_command_manifest(manifest, provider:)
|
|
238
|
+
commands = Array(manifest.fetch("commands"))
|
|
239
|
+
frontmatter = {
|
|
240
|
+
"name" => manifest.fetch("name"),
|
|
241
|
+
"description" => manifest.fetch("description"),
|
|
242
|
+
"commands" => commands
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
<<~MARKDOWN
|
|
246
|
+
#{frontmatter.to_yaml}---
|
|
247
|
+
# Commands
|
|
248
|
+
|
|
249
|
+
#{commands.map { |command| "- #{command.fetch('name')}" }.join("\n")}
|
|
250
|
+
|
|
251
|
+
#{copilot_mcp_command_section(manifest: manifest, provider: provider)}
|
|
252
|
+
MARKDOWN
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def copilot_mcp_agent_section(provider:)
|
|
256
|
+
return "" unless provider.to_s == "copilot"
|
|
257
|
+
|
|
258
|
+
<<~MARKDOWN
|
|
259
|
+
## Copilot MCP Integration
|
|
260
|
+
|
|
261
|
+
Copilot should call the local `agentf` MCP server tools for runtime actions.
|
|
262
|
+
|
|
263
|
+
- Code discovery tools: `code_glob`, `code_grep`, `code_tree`, `code_related_files`
|
|
264
|
+
- Memory read tools: `memory_recent`, `memory_search`
|
|
265
|
+
- Memory write tools (if enabled): `memory_add_lesson`, `memory_add_success`, `memory_add_pitfall`
|
|
266
|
+
|
|
267
|
+
MCP server is started via `agentf mcp-server` and runs locally over stdio.
|
|
268
|
+
MARKDOWN
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def copilot_mcp_command_section(manifest:, provider:)
|
|
272
|
+
return "" unless provider.to_s == "copilot"
|
|
273
|
+
|
|
274
|
+
command_name = manifest.fetch("name")
|
|
275
|
+
recommended_tools = case command_name
|
|
276
|
+
when "explorer"
|
|
277
|
+
"`code_glob`, `code_grep`, `code_tree`, `code_related_files`"
|
|
278
|
+
when "memory"
|
|
279
|
+
"`memory_recent`, `memory_search`, `memory_add_lesson`, `memory_add_success`, `memory_add_pitfall`"
|
|
280
|
+
else
|
|
281
|
+
"`code_glob`, `code_grep`, `memory_recent`, `memory_search`"
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
<<~MARKDOWN
|
|
285
|
+
## Copilot MCP Usage
|
|
286
|
+
|
|
287
|
+
For Copilot workflows, invoke these capabilities via the local `agentf` MCP server.
|
|
288
|
+
|
|
289
|
+
Recommended MCP tools for `#{command_name}`: #{recommended_tools}
|
|
290
|
+
MARKDOWN
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def render_workflow_engine_manifest
|
|
294
|
+
<<~MARKDOWN
|
|
295
|
+
# AGENTF-WORKFLOW-ENGINE Agent
|
|
296
|
+
|
|
297
|
+
## Role
|
|
298
|
+
|
|
299
|
+
The WORKFLOW_ENGINE coordinates end-to-end workflows by selecting a provider adapter (`opencode` or `copilot`), creating an execution plan, and running agents in sequence.
|
|
300
|
+
|
|
301
|
+
Implemented in `lib/agentf/workflow_engine.rb`.
|
|
302
|
+
|
|
303
|
+
## Responsibilities
|
|
304
|
+
|
|
305
|
+
1. Build plan from provider adapter (`Agentf::Service::Providers::OpenCode` or `Agentf::Service::Providers::Copilot`)
|
|
306
|
+
2. Enrich each agent step with brain context from Redis memory
|
|
307
|
+
3. Persist feature intent at workflow start
|
|
308
|
+
4. Persist lessons/pitfalls from each agent execution
|
|
309
|
+
5. Return full workflow state for manual review and future autonomous control
|
|
310
|
+
6. Enforce workflow contract stages (`spec`, `plan`, `execute`, `review`, `finalize`) when enabled
|
|
311
|
+
|
|
312
|
+
## Execution Flow
|
|
313
|
+
|
|
314
|
+
1. WORKFLOW_ENGINE → Requests provider plan
|
|
315
|
+
2. WORKFLOW_ENGINE → Captures feature intent in memory
|
|
316
|
+
3. WORKFLOW_ENGINE → Executes planned agents sequentially
|
|
317
|
+
4. Each agent → Reads relevant context + writes lessons
|
|
318
|
+
5. WORKFLOW_ENGINE → Summarizes status and returns results
|
|
319
|
+
|
|
320
|
+
## Notes
|
|
321
|
+
|
|
322
|
+
- The engine is provider-agnostic at runtime.
|
|
323
|
+
- Agent and tool interfaces are unchanged.
|
|
324
|
+
- Provider adapters own sequencing defaults.
|
|
325
|
+
- Workflow contract defaults to advisory mode and can be disabled with `AGENTF_WORKFLOW_CONTRACT_ENABLED=false`.
|
|
326
|
+
MARKDOWN
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def render_opencode_plugin
|
|
330
|
+
<<~TYPESCRIPT
|
|
331
|
+
import { execFile } from "node:child_process";
|
|
332
|
+
import { promisify } from "node:util";
|
|
333
|
+
import path from "node:path";
|
|
334
|
+
import { type Plugin, tool } from "@opencode-ai/plugin/tool";
|
|
335
|
+
import fs from "node:fs";
|
|
336
|
+
|
|
337
|
+
const execFileAsync = promisify(execFile);
|
|
338
|
+
|
|
339
|
+
async function resolveAgentfBinary(directory: string): Promise<string> {
|
|
340
|
+
const gemPath = process.env.AGENTF_GEM_PATH;
|
|
341
|
+
if (gemPath) {
|
|
342
|
+
const binaryPath = path.join(gemPath, "bin", "agentf");
|
|
343
|
+
if (fs.existsSync(binaryPath)) {
|
|
344
|
+
return binaryPath;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const projectRoot = path.resolve(directory);
|
|
349
|
+
const projectBinary = path.join(projectRoot, "bin", "agentf");
|
|
350
|
+
if (fs.existsSync(projectBinary)) {
|
|
351
|
+
return projectBinary;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const { stdout } = await execFileAsync("command", ["-v", "agentf"], { shell: true });
|
|
356
|
+
const whichPath = stdout.toString().trim();
|
|
357
|
+
if (whichPath && fs.existsSync(whichPath)) {
|
|
358
|
+
return whichPath;
|
|
359
|
+
}
|
|
360
|
+
} catch {
|
|
361
|
+
// command -v failed
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
throw new Error(
|
|
365
|
+
"agentf binary not found. Set AGENTF_GEM_PATH environment variable to the path where agentf gem is installed, " +
|
|
366
|
+
"or ensure bin/agentf exists in your project root. " +
|
|
367
|
+
"Example: AGENTF_GEM_PATH=$(bundle show agentf) opencode run \"your task\""
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function runAgentfCli(directory: string, subcommand: string, command: string, args: string[]) {
|
|
372
|
+
const binaryPath = await resolveAgentfBinary(directory);
|
|
373
|
+
const commandArgs = ["exec", "ruby", binaryPath, subcommand, command, ...args, "--json"];
|
|
374
|
+
|
|
375
|
+
const { stdout } = await execFileAsync("bundle", commandArgs, {
|
|
376
|
+
cwd: path.resolve(directory),
|
|
377
|
+
env: process.env,
|
|
378
|
+
maxBuffer: 1024 * 1024 * 5,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const text = stdout.toString().trim();
|
|
382
|
+
return text || "{}";
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export const agentfPlugin: Plugin = async () => {
|
|
386
|
+
return {
|
|
387
|
+
tool: {
|
|
388
|
+
agentf_code_glob: tool({
|
|
389
|
+
description: "Find files using project glob patterns via Agentf code CLI.",
|
|
390
|
+
args: {
|
|
391
|
+
pattern: tool.schema.string().describe("Glob pattern, example: lib/**/*.rb"),
|
|
392
|
+
types: tool.schema.array(tool.schema.string()).optional().describe("Optional file extensions"),
|
|
393
|
+
},
|
|
394
|
+
async execute(args, context) {
|
|
395
|
+
const commandArgs = [];
|
|
396
|
+
if (args.types?.length) {
|
|
397
|
+
commandArgs.push(`--types=${args.types.join(",")}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return runAgentfCli(context.directory, "code", "glob", [args.pattern, ...commandArgs]);
|
|
401
|
+
},
|
|
402
|
+
}),
|
|
403
|
+
agentf_code_grep: tool({
|
|
404
|
+
description: "Search file contents via Agentf code CLI.",
|
|
405
|
+
args: {
|
|
406
|
+
pattern: tool.schema.string().describe("Regex/text to search"),
|
|
407
|
+
filePattern: tool.schema.string().optional().describe("Optional include pattern"),
|
|
408
|
+
context: tool.schema.number().int().min(0).max(20).optional().describe("Context lines"),
|
|
409
|
+
},
|
|
410
|
+
async execute(args, context) {
|
|
411
|
+
const commandArgs = [];
|
|
412
|
+
if (args.filePattern) commandArgs.push(`--file-pattern=${args.filePattern}`);
|
|
413
|
+
if (Number.isInteger(args.context)) commandArgs.push(`--context=${args.context}`);
|
|
414
|
+
|
|
415
|
+
return runAgentfCli(context.directory, "code", "grep", [args.pattern, ...commandArgs]);
|
|
416
|
+
},
|
|
417
|
+
}),
|
|
418
|
+
agentf_code_tree: tool({
|
|
419
|
+
description: "Get directory tree data via Agentf code CLI.",
|
|
420
|
+
args: {
|
|
421
|
+
depth: tool.schema.number().int().min(1).max(10).optional().describe("Max traversal depth"),
|
|
422
|
+
},
|
|
423
|
+
async execute(args, context) {
|
|
424
|
+
const depth = args.depth ?? 3;
|
|
425
|
+
return runAgentfCli(context.directory, "code", "tree", [`--depth=${depth}`]);
|
|
426
|
+
},
|
|
427
|
+
}),
|
|
428
|
+
agentf_code_related_files: tool({
|
|
429
|
+
description: "Find import and related file hints for a target file.",
|
|
430
|
+
args: {
|
|
431
|
+
targetFile: tool.schema.string().describe("Workspace-relative file path"),
|
|
432
|
+
},
|
|
433
|
+
async execute(args, context) {
|
|
434
|
+
return runAgentfCli(context.directory, "code", "related", [args.targetFile]);
|
|
435
|
+
},
|
|
436
|
+
}),
|
|
437
|
+
agentf_memory_recent: tool({
|
|
438
|
+
description: "Get recent Agentf memories from Redis.",
|
|
439
|
+
args: {
|
|
440
|
+
limit: tool.schema.number().int().min(1).max(100).optional().describe("How many memories to return"),
|
|
441
|
+
},
|
|
442
|
+
async execute(args, context) {
|
|
443
|
+
const limit = args.limit ?? 10;
|
|
444
|
+
return runAgentfCli(context.directory, "memory", "recent", ["-n", String(limit)]);
|
|
445
|
+
},
|
|
446
|
+
}),
|
|
447
|
+
agentf_memory_search: tool({
|
|
448
|
+
description: "Search Agentf memories by keyword.",
|
|
449
|
+
args: {
|
|
450
|
+
query: tool.schema.string().describe("Search query"),
|
|
451
|
+
limit: tool.schema.number().int().min(1).max(100).optional().describe("How many results to return"),
|
|
452
|
+
},
|
|
453
|
+
async execute(args, context) {
|
|
454
|
+
const limit = args.limit ?? 10;
|
|
455
|
+
return runAgentfCli(context.directory, "memory", "search", [args.query, "-n", String(limit)]);
|
|
456
|
+
},
|
|
457
|
+
}),
|
|
458
|
+
agentf_memory_add_lesson: tool({
|
|
459
|
+
description: "Store a lesson memory in Redis.",
|
|
460
|
+
args: {
|
|
461
|
+
title: tool.schema.string(),
|
|
462
|
+
description: tool.schema.string(),
|
|
463
|
+
agent: tool.schema.string().optional(),
|
|
464
|
+
tags: tool.schema.array(tool.schema.string()).optional(),
|
|
465
|
+
context: tool.schema.string().optional(),
|
|
466
|
+
},
|
|
467
|
+
async execute(args, context) {
|
|
468
|
+
const commandArgs = [args.title, args.description];
|
|
469
|
+
if (args.agent) commandArgs.push(`--agent=${args.agent}`);
|
|
470
|
+
if (args.tags?.length) commandArgs.push(`--tags=${args.tags.join(",")}`);
|
|
471
|
+
if (args.context) commandArgs.push(`--context=${args.context}`);
|
|
472
|
+
|
|
473
|
+
return runAgentfCli(context.directory, "memory", "add-lesson", commandArgs);
|
|
474
|
+
},
|
|
475
|
+
}),
|
|
476
|
+
agentf_memory_add_success: tool({
|
|
477
|
+
description: "Store a success memory in Redis.",
|
|
478
|
+
args: {
|
|
479
|
+
title: tool.schema.string(),
|
|
480
|
+
description: tool.schema.string(),
|
|
481
|
+
agent: tool.schema.string().optional(),
|
|
482
|
+
tags: tool.schema.array(tool.schema.string()).optional(),
|
|
483
|
+
context: tool.schema.string().optional(),
|
|
484
|
+
},
|
|
485
|
+
async execute(args, context) {
|
|
486
|
+
const commandArgs = [args.title, args.description];
|
|
487
|
+
if (args.agent) commandArgs.push(`--agent=${args.agent}`);
|
|
488
|
+
if (args.tags?.length) commandArgs.push(`--tags=${args.tags.join(",")}`);
|
|
489
|
+
if (args.context) commandArgs.push(`--context=${args.context}`);
|
|
490
|
+
|
|
491
|
+
return runAgentfCli(context.directory, "memory", "add-success", commandArgs);
|
|
492
|
+
},
|
|
493
|
+
}),
|
|
494
|
+
agentf_memory_add_pitfall: tool({
|
|
495
|
+
description: "Store a pitfall memory in Redis.",
|
|
496
|
+
args: {
|
|
497
|
+
title: tool.schema.string(),
|
|
498
|
+
description: tool.schema.string(),
|
|
499
|
+
agent: tool.schema.string().optional(),
|
|
500
|
+
tags: tool.schema.array(tool.schema.string()).optional(),
|
|
501
|
+
context: tool.schema.string().optional(),
|
|
502
|
+
},
|
|
503
|
+
async execute(args, context) {
|
|
504
|
+
const commandArgs = [args.title, args.description];
|
|
505
|
+
if (args.agent) commandArgs.push(`--agent=${args.agent}`);
|
|
506
|
+
if (args.tags?.length) commandArgs.push(`--tags=${args.tags.join(",")}`);
|
|
507
|
+
if (args.context) commandArgs.push(`--context=${args.context}`);
|
|
508
|
+
|
|
509
|
+
return runAgentfCli(context.directory, "memory", "add-pitfall", commandArgs);
|
|
510
|
+
},
|
|
511
|
+
}),
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
};
|
|
515
|
+
TYPESCRIPT
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def render_opencode_json
|
|
519
|
+
<<~JSON
|
|
520
|
+
{
|
|
521
|
+
"$schema": "https://opencode.ai/config.json",
|
|
522
|
+
"plugin": ["./opencode/plugins/agentf-plugin"]
|
|
523
|
+
}
|
|
524
|
+
JSON
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def render_opencode_memory_schema
|
|
528
|
+
<<~MARKDOWN
|
|
529
|
+
# Redis Memory Schema
|
|
530
|
+
|
|
531
|
+
## Overview
|
|
532
|
+
This document defines the memory node structures for Agentf using Redis Stack (RedisJSON + RediSearch).
|
|
533
|
+
|
|
534
|
+
## Memory Types
|
|
535
|
+
|
|
536
|
+
### 1. Semantic Memory (`semantic:*`)
|
|
537
|
+
Used for finding similar past tasks by embedding similarity.
|
|
538
|
+
|
|
539
|
+
**Schema**:
|
|
540
|
+
- `id`: string
|
|
541
|
+
- `content`: text
|
|
542
|
+
- `embedding`: vector payload
|
|
543
|
+
- `project`: tag
|
|
544
|
+
- `language`: tag
|
|
545
|
+
- `task_type`: tag
|
|
546
|
+
- `success`: boolean
|
|
547
|
+
- `created_at`: numeric timestamp
|
|
548
|
+
- `agent`: string
|
|
549
|
+
|
|
550
|
+
### 2. Episodic Memory (`episodic:*`)
|
|
551
|
+
Used for success, pitfall, lesson, and intent records.
|
|
552
|
+
|
|
553
|
+
**Search index**: `episodic:logs`
|
|
554
|
+
|
|
555
|
+
**Schema fields**:
|
|
556
|
+
- `$.id`
|
|
557
|
+
- `$.type`
|
|
558
|
+
- `$.title`
|
|
559
|
+
- `$.description`
|
|
560
|
+
- `$.project`
|
|
561
|
+
- `$.context`
|
|
562
|
+
- `$.code_snippet`
|
|
563
|
+
- `$.tags`
|
|
564
|
+
- `$.created_at`
|
|
565
|
+
- `$.agent`
|
|
566
|
+
- `$.related_task_id`
|
|
567
|
+
- `$.metadata.intent_kind`
|
|
568
|
+
- `$.metadata.priority`
|
|
569
|
+
|
|
570
|
+
## Memory Commands
|
|
571
|
+
|
|
572
|
+
- Read recent: `agentf memory recent -n 10`
|
|
573
|
+
- Search: `agentf memory search "query" -n 10`
|
|
574
|
+
- Add lesson: `agentf memory add-lesson "<title>" "<description>" --agent=<AGENT>`
|
|
575
|
+
- Add success: `agentf memory add-success "<title>" "<description>" --agent=<AGENT>`
|
|
576
|
+
- Add pitfall: `agentf memory add-pitfall "<title>" "<description>" --agent=<AGENT>`
|
|
577
|
+
MARKDOWN
|
|
578
|
+
end
|
|
579
|
+
end
|
|
580
|
+
end
|