agentf 0.6.0 → 0.7.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 +4 -4
- data/lib/agentf/agents/base.rb +6 -21
- data/lib/agentf/agents/designer.rb +16 -2
- data/lib/agentf/agents/specialist.rb +17 -3
- data/lib/agentf/agents/tester.rb +16 -2
- data/lib/agentf/cli/memory.rb +34 -27
- data/lib/agentf/commands/memory_reviewer.rb +6 -6
- data/lib/agentf/installer.rb +89 -4
- data/lib/agentf/mcp/server.rb +85 -77
- data/lib/agentf/memory/confirmation_handler.rb +24 -0
- data/lib/agentf/memory.rb +6 -0
- data/lib/agentf/version.rb +1 -1
- data/lib/agentf.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9d0041ebcd3b112183ddb349312e4cab8d30237ed1108dbb55d928c525dd59ae
|
|
4
|
+
data.tar.gz: 2577b0dc7af10255d02d2dde419aaa8e1135aea4e36dd67cc10b5d55b1866ba6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ceae3f4d97e84e9934eae6591b7a2be4ea6ed56e31f4555110f998a2f3551f8231572a8c56dd790815988772ad8f4584f5d043d0283828665bcb69ad033d9ef2
|
|
7
|
+
data.tar.gz: 2ff0e3ece10105b6632ebd8e34c5b78c870dc6b8e105dabc41bc7f472502a8c33aee1dc7d10f14fa0ffaea513553c24f1353a9bda7a6647117f200cf8a43de88
|
data/lib/agentf/agents/base.rb
CHANGED
|
@@ -4,6 +4,7 @@ module Agentf
|
|
|
4
4
|
module Agents
|
|
5
5
|
# Base agent class
|
|
6
6
|
class Base
|
|
7
|
+
include Agentf::Memory::ConfirmationHandler
|
|
7
8
|
attr_reader :memory, :name
|
|
8
9
|
|
|
9
10
|
def self.typed_name
|
|
@@ -52,6 +53,10 @@ module Agentf
|
|
|
52
53
|
}
|
|
53
54
|
end
|
|
54
55
|
|
|
56
|
+
def self.writes_code?
|
|
57
|
+
false
|
|
58
|
+
end
|
|
59
|
+
|
|
55
60
|
def initialize(memory)
|
|
56
61
|
@memory = memory
|
|
57
62
|
@name = self.class.typed_name
|
|
@@ -91,28 +96,8 @@ module Agentf
|
|
|
91
96
|
result: result
|
|
92
97
|
)
|
|
93
98
|
|
|
94
|
-
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Helper to centralize memory write confirmation handling.
|
|
98
|
-
# Yields a block that performs the memory write. If the memory layer
|
|
99
|
-
# requires confirmation (ask_first policy) a structured hash is
|
|
100
|
-
# returned with confirmation details so agents can merge that into
|
|
101
|
-
# their own return payloads or let the orchestrator handle prompting.
|
|
102
|
-
def safe_memory_write(attempted: {})
|
|
103
|
-
begin
|
|
104
|
-
yield
|
|
105
|
-
rescue Agentf::Memory::RedisMemory::ConfirmationRequired => e
|
|
106
|
-
log "[MEMORY] Confirmation required: #{e.message} -- details=#{e.details.inspect}"
|
|
107
|
-
{
|
|
108
|
-
"confirmation_required" => true,
|
|
109
|
-
"confirmation_details" => e.details,
|
|
110
|
-
"attempted" => attempted,
|
|
111
|
-
"confirmed_write_token" => "confirmed",
|
|
112
|
-
"confirmation_prompt" => "Ask the user whether to save this memory. If they approve, rerun the same tool with confirmedWrite=confirmed. If they decline, do not retry."
|
|
113
|
-
}
|
|
99
|
+
result
|
|
114
100
|
end
|
|
115
101
|
end
|
|
116
|
-
end
|
|
117
102
|
end
|
|
118
103
|
end
|
|
@@ -45,14 +45,28 @@ module Agentf
|
|
|
45
45
|
|
|
46
46
|
def self.policy_boundaries
|
|
47
47
|
{
|
|
48
|
-
"always" => [
|
|
48
|
+
"always" => [
|
|
49
|
+
"Return generated component details",
|
|
50
|
+
"Persist successful implementation pattern",
|
|
51
|
+
"Write a failing spec before implementing any new component or function (red)",
|
|
52
|
+
"Run the test suite to confirm the spec fails before writing implementation",
|
|
53
|
+
"Run the test suite again after implementation to confirm green"
|
|
54
|
+
],
|
|
49
55
|
"ask_first" => ["Changing primary UI framework", "Persisting successful implementation patterns to memory"],
|
|
50
|
-
"never" => [
|
|
56
|
+
"never" => [
|
|
57
|
+
"Return empty generated code for successful design task",
|
|
58
|
+
"Create a new component or function without a corresponding spec file",
|
|
59
|
+
"Skip red/green verification when writing or modifying code"
|
|
60
|
+
],
|
|
51
61
|
"required_inputs" => ["design_spec"],
|
|
52
62
|
"required_outputs" => ["component", "generated_code", "success"]
|
|
53
63
|
}
|
|
54
64
|
end
|
|
55
65
|
|
|
66
|
+
def self.writes_code?
|
|
67
|
+
true
|
|
68
|
+
end
|
|
69
|
+
|
|
56
70
|
def initialize(memory, commands: nil)
|
|
57
71
|
super(memory)
|
|
58
72
|
@commands = commands || Agentf::Commands::Designer.new
|
|
@@ -44,14 +44,28 @@ module Agentf
|
|
|
44
44
|
|
|
45
45
|
def self.policy_boundaries
|
|
46
46
|
{
|
|
47
|
-
"always" => [
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
"always" => [
|
|
48
|
+
"Persist execution outcome",
|
|
49
|
+
"Return deterministic success boolean",
|
|
50
|
+
"Write a failing spec before adding any new class, method, or module (red)",
|
|
51
|
+
"Run the test suite to confirm the spec fails before writing implementation",
|
|
52
|
+
"Run the test suite again after implementation to confirm green"
|
|
53
|
+
],
|
|
54
|
+
"ask_first" => ["Applying architecture style changes across unrelated modules", "Persisting execution outcomes to memory (success/pitfall)"],
|
|
55
|
+
"never" => [
|
|
56
|
+
"Claim implementation complete without execution result",
|
|
57
|
+
"Create a new class, method, or module without a corresponding spec file",
|
|
58
|
+
"Skip red/green verification when writing or modifying code"
|
|
59
|
+
],
|
|
50
60
|
"required_inputs" => ["description"],
|
|
51
61
|
"required_outputs" => ["subtask_id", "success"]
|
|
52
62
|
}
|
|
53
63
|
end
|
|
54
64
|
|
|
65
|
+
def self.writes_code?
|
|
66
|
+
true
|
|
67
|
+
end
|
|
68
|
+
|
|
55
69
|
def execute(task:, context: {}, agents: {}, commands: {}, logger: nil)
|
|
56
70
|
subtask = task.is_a?(Hash) ? task : (context["current_subtask"] || { "description" => task })
|
|
57
71
|
|
data/lib/agentf/agents/tester.rb
CHANGED
|
@@ -45,14 +45,28 @@ module Agentf
|
|
|
45
45
|
|
|
46
46
|
def self.policy_boundaries
|
|
47
47
|
{
|
|
48
|
-
"always" => [
|
|
48
|
+
"always" => [
|
|
49
|
+
"Produce framework-aware tests",
|
|
50
|
+
"Verify red/green state when TDD enabled",
|
|
51
|
+
"Write a failing spec before adding any new test helper, fixture, or shared example (red)",
|
|
52
|
+
"Run the test suite to confirm the spec fails before writing implementation",
|
|
53
|
+
"Run the test suite again after implementation to confirm green"
|
|
54
|
+
],
|
|
49
55
|
"ask_first" => ["Changing test framework conventions", "Persisting test-generation outcomes to memory"],
|
|
50
|
-
"never" => [
|
|
56
|
+
"never" => [
|
|
57
|
+
"Mark passing when command output is uncertain",
|
|
58
|
+
"Create a new test helper or shared example without a corresponding spec file",
|
|
59
|
+
"Skip red/green verification when writing or modifying test infrastructure code"
|
|
60
|
+
],
|
|
51
61
|
"required_inputs" => [],
|
|
52
62
|
"required_outputs" => ["test_file"]
|
|
53
63
|
}
|
|
54
64
|
end
|
|
55
65
|
|
|
66
|
+
def self.writes_code?
|
|
67
|
+
true
|
|
68
|
+
end
|
|
69
|
+
|
|
56
70
|
def initialize(memory, commands: nil)
|
|
57
71
|
super(memory)
|
|
58
72
|
@commands = commands || Agentf::Commands::Tester.new
|
data/lib/agentf/cli/memory.rb
CHANGED
|
@@ -12,6 +12,7 @@ module Agentf
|
|
|
12
12
|
# - by_type accepts business_intent and feature_intent (finding #11)
|
|
13
13
|
class Memory
|
|
14
14
|
include ArgParser
|
|
15
|
+
include Agentf::Memory::ConfirmationHandler
|
|
15
16
|
|
|
16
17
|
VALID_EPISODE_TYPES = %w[episode lesson playbook business_intent feature_intent incident].freeze
|
|
17
18
|
|
|
@@ -38,6 +39,10 @@ module Agentf
|
|
|
38
39
|
list_business_intents(args)
|
|
39
40
|
when "feature-intents"
|
|
40
41
|
list_feature_intents(args)
|
|
42
|
+
when "add-intent"
|
|
43
|
+
add_intent(args)
|
|
44
|
+
when "add-episode"
|
|
45
|
+
add_episode_direct(args)
|
|
41
46
|
when "add-business-intent"
|
|
42
47
|
add_business_intent(args)
|
|
43
48
|
when "add-feature-intent"
|
|
@@ -119,6 +124,24 @@ module Agentf
|
|
|
119
124
|
output(@reviewer.get_feature_intents(limit: limit))
|
|
120
125
|
end
|
|
121
126
|
|
|
127
|
+
def add_intent(args)
|
|
128
|
+
kind = args.shift.to_s.downcase
|
|
129
|
+
unless %w[business feature].include?(kind)
|
|
130
|
+
$stderr.puts "Error: add-intent requires kind (business|feature) as first argument"
|
|
131
|
+
exit 1
|
|
132
|
+
end
|
|
133
|
+
kind == "business" ? add_business_intent(args) : add_feature_intent(args)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def add_episode_direct(args)
|
|
137
|
+
type = args.shift.to_s
|
|
138
|
+
unless VALID_EPISODE_TYPES.include?(type)
|
|
139
|
+
$stderr.puts "Error: add-episode requires type as first argument (#{VALID_EPISODE_TYPES.join('|')})"
|
|
140
|
+
exit 1
|
|
141
|
+
end
|
|
142
|
+
add_episode(type, args)
|
|
143
|
+
end
|
|
144
|
+
|
|
122
145
|
def add_business_intent(args)
|
|
123
146
|
title = args.shift
|
|
124
147
|
description = args.shift
|
|
@@ -132,7 +155,7 @@ module Agentf
|
|
|
132
155
|
priority = parse_integer_option(args, "--priority=", default: 1)
|
|
133
156
|
|
|
134
157
|
id = nil
|
|
135
|
-
res =
|
|
158
|
+
res = safe_memory_write(@memory, attempted: { command: "add-business-intent", args: { title: title, description: description, constraints: constraints, priority: priority } }) do
|
|
136
159
|
id = @memory.store_business_intent(
|
|
137
160
|
title: title,
|
|
138
161
|
description: description,
|
|
@@ -171,7 +194,7 @@ module Agentf
|
|
|
171
194
|
related_task_id = parse_single_option(args, "--task=")
|
|
172
195
|
|
|
173
196
|
id = nil
|
|
174
|
-
res =
|
|
197
|
+
res = safe_memory_write(@memory, attempted: { command: "add-feature-intent", args: { title: title, description: description, acceptance: acceptance_criteria, non_goals: non_goals, related_task_id: related_task_id } }) do
|
|
175
198
|
id = @memory.store_feature_intent(
|
|
176
199
|
title: title,
|
|
177
200
|
description: description,
|
|
@@ -211,7 +234,7 @@ module Agentf
|
|
|
211
234
|
agent = parse_single_option(args, "--agent=") || Agentf::AgentRoles::PLANNER
|
|
212
235
|
|
|
213
236
|
id = nil
|
|
214
|
-
res =
|
|
237
|
+
res = safe_memory_write(@memory, attempted: { command: "add-playbook", args: { title: title, description: description, steps: steps, feature_area: feature_area, agent: agent } }) do
|
|
215
238
|
id = @memory.store_playbook(
|
|
216
239
|
title: title,
|
|
217
240
|
description: description,
|
|
@@ -252,7 +275,7 @@ module Agentf
|
|
|
252
275
|
outcome = parse_single_option(args, "--outcome=")
|
|
253
276
|
|
|
254
277
|
id = nil
|
|
255
|
-
res =
|
|
278
|
+
res = safe_memory_write(@memory, attempted: { command: "add-#{type}", args: { title: title, description: description, context: context, agent: agent, code: code_snippet, outcome: outcome } }) do
|
|
256
279
|
id = @memory.store_episode(
|
|
257
280
|
type: type,
|
|
258
281
|
title: title,
|
|
@@ -280,22 +303,6 @@ module Agentf
|
|
|
280
303
|
end
|
|
281
304
|
end
|
|
282
305
|
|
|
283
|
-
# Helper to standardize CLI memory write confirmation handling.
|
|
284
|
-
def safe_cli_memory_write(memory, attempted: {})
|
|
285
|
-
begin
|
|
286
|
-
yield
|
|
287
|
-
nil
|
|
288
|
-
rescue Agentf::Memory::RedisMemory::ConfirmationRequired => e
|
|
289
|
-
{
|
|
290
|
-
"confirmation_required" => true,
|
|
291
|
-
"confirmation_details" => e.details,
|
|
292
|
-
"attempted" => attempted,
|
|
293
|
-
"confirmed_write_token" => "confirmed",
|
|
294
|
-
"confirmation_prompt" => "Ask the user whether to save this memory. If they approve, rerun the same command with confirmation enabled. If they decline, do not retry."
|
|
295
|
-
}
|
|
296
|
-
end
|
|
297
|
-
end
|
|
298
|
-
|
|
299
306
|
def search_memories(args)
|
|
300
307
|
# Extract limit BEFORE joining remaining args as query (fixes finding #7)
|
|
301
308
|
limit = extract_limit(args)
|
|
@@ -561,10 +568,8 @@ module Agentf
|
|
|
561
568
|
episodes List episode memories
|
|
562
569
|
lessons List lessons learned
|
|
563
570
|
intents [kind] List intents (kind: business|feature)
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
add-business-intent Store business intent
|
|
567
|
-
add-feature-intent Store feature intent
|
|
571
|
+
add-intent <kind> Store intent (kind: business|feature)
|
|
572
|
+
add-episode <type> Store episode (type: #{VALID_EPISODE_TYPES.join("|")}) with --outcome=
|
|
568
573
|
add-playbook Store playbook memory
|
|
569
574
|
add-lesson Store lesson memory
|
|
570
575
|
search <query> Search memories semantically
|
|
@@ -585,11 +590,13 @@ module Agentf
|
|
|
585
590
|
agentf memory recent -n 5
|
|
586
591
|
agentf memory episodes --outcome=negative
|
|
587
592
|
agentf memory intents business -n 5
|
|
588
|
-
agentf memory add-
|
|
589
|
-
agentf memory add-
|
|
593
|
+
agentf memory add-intent business "Reliability" "Prioritize uptime" --constraints="No downtime;No vendor lock-in"
|
|
594
|
+
agentf memory add-intent feature "Agent handoff" "Improve continuity" --acceptance="Keeps context;Preserves task state"
|
|
595
|
+
agentf memory add-episode lesson "Refactor strategy" "Extracted adapter seam" --agent=PLANNER --outcome=positive
|
|
596
|
+
agentf memory add-episode incident "Auth regression" "JWT expiry not checked" --outcome=negative
|
|
590
597
|
agentf memory add-playbook "Release rollout" "Safe deploy sequence" --steps="deploy canary;monitor;promote"
|
|
591
598
|
agentf memory add-lesson "Refactor strategy" "Extracted adapter seam" --agent=PLANNER
|
|
592
|
-
agentf memory search "react"
|
|
599
|
+
agentf memory search "react" --type=lesson --outcome=positive
|
|
593
600
|
agentf memory delete id episode_abcd
|
|
594
601
|
agentf memory delete last -n 10 --scope=project
|
|
595
602
|
agentf memory delete all --scope=all --yes
|
|
@@ -19,6 +19,7 @@ module Agentf
|
|
|
19
19
|
{ "name" => "get_lessons", "type" => "function" },
|
|
20
20
|
{ "name" => "get_by_type", "type" => "function" },
|
|
21
21
|
{ "name" => "get_by_agent", "type" => "function" },
|
|
22
|
+
{ "name" => "get_intents", "type" => "function" },
|
|
22
23
|
{ "name" => "search", "type" => "function" },
|
|
23
24
|
{ "name" => "get_summary", "type" => "function" },
|
|
24
25
|
{ "name" => "neighbors", "type" => "function" },
|
|
@@ -87,16 +88,15 @@ module Agentf
|
|
|
87
88
|
|
|
88
89
|
# Get memories by agent
|
|
89
90
|
def get_by_agent(agent, limit: 10)
|
|
90
|
-
memories = @memory.
|
|
91
|
-
|
|
92
|
-
format_memories(filtered.first(limit))
|
|
91
|
+
memories = @memory.get_memories_by_agent(agent: agent, limit: limit)
|
|
92
|
+
format_memories(memories)
|
|
93
93
|
rescue => e
|
|
94
94
|
{ "error" => e.message }
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
-
# Search memories
|
|
98
|
-
def search(query, limit: 10)
|
|
99
|
-
format_memories(@memory.search_memories(query: query, limit: limit))
|
|
97
|
+
# Search memories semantically with optional type, agent, and outcome filters
|
|
98
|
+
def search(query, limit: 10, type: nil, agent: nil, outcome: nil)
|
|
99
|
+
format_memories(@memory.search_memories(query: query, limit: limit, type: type, agent: agent, outcome: outcome))
|
|
100
100
|
rescue => e
|
|
101
101
|
{ "error" => e.message }
|
|
102
102
|
end
|
data/lib/agentf/installer.rb
CHANGED
|
@@ -150,7 +150,7 @@ module Agentf
|
|
|
150
150
|
|
|
151
151
|
classes.map do |klass|
|
|
152
152
|
target = File.join(root, layout.fetch("agents_dir"), layout.fetch("agent_filename").call(klass))
|
|
153
|
-
write_manifest(target, render_agent_manifest(klass
|
|
153
|
+
write_manifest(target, render_agent_manifest(klass))
|
|
154
154
|
end
|
|
155
155
|
end
|
|
156
156
|
|
|
@@ -160,7 +160,7 @@ module Agentf
|
|
|
160
160
|
|
|
161
161
|
manifests.map do |manifest|
|
|
162
162
|
target = File.join(root, layout.fetch("commands_dir"), layout.fetch("command_filename").call(manifest))
|
|
163
|
-
write_manifest(target, render_command_manifest(manifest
|
|
163
|
+
write_manifest(target, render_command_manifest(manifest))
|
|
164
164
|
end
|
|
165
165
|
end
|
|
166
166
|
|
|
@@ -237,7 +237,7 @@ module Agentf
|
|
|
237
237
|
end
|
|
238
238
|
end
|
|
239
239
|
|
|
240
|
-
def render_agent_manifest(klass
|
|
240
|
+
def render_agent_manifest(klass)
|
|
241
241
|
# Emit a minimal, stable manifest that acts as a pointer to the runtime
|
|
242
242
|
# tool implemented by the plugin/CLI. Keep filename and `name` stable so
|
|
243
243
|
# upgrades remain compatible with existing installs.
|
|
@@ -257,6 +257,9 @@ module Agentf
|
|
|
257
257
|
|
|
258
258
|
description = klass.respond_to?(:description) ? klass.description.to_s.strip : ""
|
|
259
259
|
|
|
260
|
+
tdd_section = klass.respond_to?(:writes_code?) && klass.writes_code? ? tdd_requirement_section : ""
|
|
261
|
+
fallback = cli_fallback_section(klass)
|
|
262
|
+
|
|
260
263
|
<<~MARKDOWN
|
|
261
264
|
---
|
|
262
265
|
name: #{tool_name}
|
|
@@ -273,7 +276,50 @@ module Agentf
|
|
|
273
276
|
do not retry the write.
|
|
274
277
|
|
|
275
278
|
Policy Summary: #{policy_summary}
|
|
279
|
+
#{tdd_section}#{fallback}
|
|
280
|
+
MARKDOWN
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def tdd_requirement_section
|
|
284
|
+
<<~MARKDOWN
|
|
285
|
+
|
|
286
|
+
## TDD Requirement
|
|
287
|
+
|
|
288
|
+
This agent writes code. Every implementation MUST follow red/green discipline:
|
|
289
|
+
|
|
290
|
+
1. **Write the spec first** — create a failing test before any implementation code
|
|
291
|
+
2. **Run tests to confirm red** — verify the spec fails (`bundle exec rspec <spec_file>`)
|
|
292
|
+
3. **Implement the code** — write only enough to make the spec pass
|
|
293
|
+
4. **Run tests to confirm green** — verify all specs pass before reporting done
|
|
294
|
+
5. **Never skip** — do not create a class, method, or module without a corresponding spec file
|
|
295
|
+
|
|
296
|
+
Showing test output (red then green) is mandatory evidence of completion.
|
|
297
|
+
MARKDOWN
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def cli_fallback_section(klass)
|
|
301
|
+
agent_name = klass.typed_name.downcase
|
|
302
|
+
read_cmds = READ_ACTIONS.values_at("get_recent_memories", "search").map { |a| "- `#{a[:cli]}`" }.join("\n")
|
|
303
|
+
|
|
304
|
+
write_actions = Array(klass.respond_to?(:memory_concepts) ? klass.memory_concepts["writes"] : [])
|
|
305
|
+
.map { |item| item.to_s.split("#").last }
|
|
306
|
+
.filter_map { |key| WRITE_ACTIONS[key] }
|
|
307
|
+
.map { |a| "- `#{a[:cli].gsub("<AGENT>", agent_name.upcase)}`" }
|
|
308
|
+
.join("\n")
|
|
276
309
|
|
|
310
|
+
write_section = write_actions.empty? ? "" : "\n**Memory writes**:\n#{write_actions}\n"
|
|
311
|
+
|
|
312
|
+
<<~MARKDOWN
|
|
313
|
+
|
|
314
|
+
## CLI Fallback
|
|
315
|
+
|
|
316
|
+
If the `agentf` MCP server is unavailable, run equivalent commands directly in the terminal:
|
|
317
|
+
|
|
318
|
+
**Run this agent**: `agentf agent #{agent_name} "<input>"`
|
|
319
|
+
|
|
320
|
+
**Memory reads**:
|
|
321
|
+
#{read_cmds}
|
|
322
|
+
#{write_section}
|
|
277
323
|
MARKDOWN
|
|
278
324
|
end
|
|
279
325
|
|
|
@@ -329,9 +375,10 @@ module Agentf
|
|
|
329
375
|
"agentf-#{name.to_s.downcase}"
|
|
330
376
|
end
|
|
331
377
|
|
|
332
|
-
def render_command_manifest(manifest
|
|
378
|
+
def render_command_manifest(manifest)
|
|
333
379
|
cmd_name = command_identifier(manifest.fetch("name"))
|
|
334
380
|
desc = manifest.fetch("description", "").to_s.strip
|
|
381
|
+
fallback = command_cli_fallback_section(manifest)
|
|
335
382
|
|
|
336
383
|
<<~MARKDOWN
|
|
337
384
|
---
|
|
@@ -342,7 +389,45 @@ module Agentf
|
|
|
342
389
|
|
|
343
390
|
IMPORTANT: Do not embed runtime logic here. Invoke the `#{cmd_name}` tool to perform
|
|
344
391
|
any codebase or memory operations.
|
|
392
|
+
#{fallback}
|
|
393
|
+
MARKDOWN
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def command_cli_fallback_section(manifest)
|
|
397
|
+
name = manifest.fetch("name")
|
|
398
|
+
cli_lines = case name
|
|
399
|
+
when "explorer"
|
|
400
|
+
[
|
|
401
|
+
"`agentf code glob \"<pattern>\"`",
|
|
402
|
+
"`agentf code grep \"<pattern>\"`",
|
|
403
|
+
"`agentf code tree --depth=3`",
|
|
404
|
+
"`agentf code related <file>`"
|
|
405
|
+
]
|
|
406
|
+
when "memory"
|
|
407
|
+
[
|
|
408
|
+
"`#{READ_ACTIONS.fetch('get_recent_memories')[:cli]}`",
|
|
409
|
+
"`#{READ_ACTIONS.fetch('search')[:cli]}`",
|
|
410
|
+
"`#{READ_ACTIONS.fetch('get_episodes')[:cli]}`",
|
|
411
|
+
"`#{WRITE_ACTIONS.fetch('store_lesson')[:cli]}`",
|
|
412
|
+
"`#{WRITE_ACTIONS.fetch('store_episode')[:cli]}`"
|
|
413
|
+
]
|
|
414
|
+
else
|
|
415
|
+
[
|
|
416
|
+
"`agentf code glob \"<pattern>\"`",
|
|
417
|
+
"`#{READ_ACTIONS.fetch('get_recent_memories')[:cli]}`",
|
|
418
|
+
"`#{READ_ACTIONS.fetch('search')[:cli]}`"
|
|
419
|
+
]
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
tool_lines = cli_lines.map { |cmd| "- #{cmd}" }.join("\n")
|
|
423
|
+
|
|
424
|
+
<<~MARKDOWN
|
|
425
|
+
|
|
426
|
+
## CLI Fallback
|
|
427
|
+
|
|
428
|
+
If the `agentf` MCP server is unavailable, run equivalent commands directly in the terminal:
|
|
345
429
|
|
|
430
|
+
#{tool_lines}
|
|
346
431
|
MARKDOWN
|
|
347
432
|
end
|
|
348
433
|
|
data/lib/agentf/mcp/server.rb
CHANGED
|
@@ -20,6 +20,7 @@ module Agentf
|
|
|
20
20
|
# AGENTF_MCP_ALLOW_WRITES - true/false, controls memory write tools
|
|
21
21
|
# AGENTF_MCP_MAX_ARG_LENGTH - max length per string argument
|
|
22
22
|
class Server
|
|
23
|
+
include Agentf::Memory::ConfirmationHandler
|
|
23
24
|
ToolDefinition = Struct.new(:name, :description, :arguments, :handler, keyword_init: true)
|
|
24
25
|
|
|
25
26
|
class ToolBuilder
|
|
@@ -189,21 +190,20 @@ module Agentf
|
|
|
189
190
|
agentf-memory-episodes
|
|
190
191
|
agentf-memory-lessons
|
|
191
192
|
agentf-memory-intents
|
|
192
|
-
agentf-memory-
|
|
193
|
-
agentf-memory-feature-intents
|
|
193
|
+
agentf-memory-summary
|
|
194
194
|
agentf-memory-neighbors
|
|
195
195
|
agentf-memory-subgraph
|
|
196
|
+
agentf-memory-add-intent
|
|
197
|
+
agentf-memory-add-episode
|
|
196
198
|
agentf-memory-add-playbook
|
|
197
199
|
agentf-memory-add-lesson
|
|
198
|
-
agentf-memory-add-business-intent
|
|
199
|
-
agentf-memory-add-feature-intent
|
|
200
200
|
].freeze
|
|
201
201
|
|
|
202
202
|
WRITE_TOOLS = Set.new(%w[
|
|
203
|
+
agentf-memory-add-intent
|
|
204
|
+
agentf-memory-add-episode
|
|
203
205
|
agentf-memory-add-playbook
|
|
204
206
|
agentf-memory-add-lesson
|
|
205
|
-
agentf-memory-add-business-intent
|
|
206
|
-
agentf-memory-add-feature-intent
|
|
207
207
|
]).freeze
|
|
208
208
|
|
|
209
209
|
attr_reader :server, :guardrails
|
|
@@ -217,25 +217,6 @@ module Agentf
|
|
|
217
217
|
@server = build_server
|
|
218
218
|
end
|
|
219
219
|
|
|
220
|
-
# Helper to centralize confirmation handling for MCP server write tools.
|
|
221
|
-
# Yields the block that performs the memory write and returns either the
|
|
222
|
-
# block result or the normalized confirmation hash produced by
|
|
223
|
-
# Agentf::Agents::Base#safe_memory_write.
|
|
224
|
-
def safe_mcp_memory_write(memory, attempted: {})
|
|
225
|
-
begin
|
|
226
|
-
yield
|
|
227
|
-
nil
|
|
228
|
-
rescue Agentf::Memory::RedisMemory::ConfirmationRequired => e
|
|
229
|
-
{
|
|
230
|
-
"confirmation_required" => true,
|
|
231
|
-
"confirmation_details" => e.details,
|
|
232
|
-
"attempted" => attempted,
|
|
233
|
-
"confirmed_write_token" => "confirmed",
|
|
234
|
-
"confirmation_prompt" => "Ask the user whether to save this memory. If they approve, call the same tool again after confirmation. If they decline, do not retry."
|
|
235
|
-
}
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
|
-
|
|
239
220
|
# Start the stdio read loop (blocks until stdin closes).
|
|
240
221
|
def run
|
|
241
222
|
@server.run
|
|
@@ -388,12 +369,15 @@ module Agentf
|
|
|
388
369
|
end
|
|
389
370
|
|
|
390
371
|
s.tool("agentf-memory-search") do
|
|
391
|
-
description "Search memories semantically."
|
|
372
|
+
description "Search memories semantically. Supports optional filters for type, agent, and outcome."
|
|
392
373
|
argument :query, String, required: true, description: "Search query"
|
|
393
374
|
argument :limit, Integer, required: false, description: "How many results to return (1-100)"
|
|
375
|
+
argument :type, String, required: false, description: "Filter by type: episode|lesson|playbook|business_intent|feature_intent|incident"
|
|
376
|
+
argument :agent, String, required: false, description: "Filter by agent name"
|
|
377
|
+
argument :outcome, String, required: false, description: "Filter by outcome: positive|negative|neutral"
|
|
394
378
|
call do |args|
|
|
395
379
|
mcp_server.send(:guard!, "agentf-memory-search", **args)
|
|
396
|
-
result = reviewer.search(args[:query], limit: args[:limit] || 10)
|
|
380
|
+
result = reviewer.search(args[:query], limit: args[:limit] || 10, type: args[:type], agent: args[:agent], outcome: args[:outcome])
|
|
397
381
|
JSON.generate(result)
|
|
398
382
|
end
|
|
399
383
|
end
|
|
@@ -442,7 +426,7 @@ module Agentf
|
|
|
442
426
|
end
|
|
443
427
|
|
|
444
428
|
s.tool("agentf-memory-intents") do
|
|
445
|
-
description "List intents (business|feature)."
|
|
429
|
+
description "List intents (business|feature). Pass kind to filter."
|
|
446
430
|
argument :kind, String, required: false, description: "Optional: business|feature"
|
|
447
431
|
argument :limit, Integer, required: false, description: "How many results to return (1-100)"
|
|
448
432
|
call do |args|
|
|
@@ -481,61 +465,85 @@ module Agentf
|
|
|
481
465
|
end
|
|
482
466
|
end
|
|
483
467
|
|
|
484
|
-
s.tool("agentf-memory-add-
|
|
485
|
-
description "Store a business intent
|
|
468
|
+
s.tool("agentf-memory-add-intent") do
|
|
469
|
+
description "Store a business or feature intent. Pass kind: business or feature."
|
|
470
|
+
argument :kind, String, required: true, description: "Intent type: business|feature"
|
|
486
471
|
argument :title, String, required: true, description: "Intent title"
|
|
487
472
|
argument :description, String, required: true, description: "Intent description"
|
|
488
|
-
argument :constraints, Array, required: false, items: String, description: "
|
|
489
|
-
argument :priority, Integer, required: false, description: "
|
|
473
|
+
argument :constraints, Array, required: false, items: String, description: "Business intent constraints"
|
|
474
|
+
argument :priority, Integer, required: false, description: "Business intent priority"
|
|
475
|
+
argument :acceptance, Array, required: false, items: String, description: "Feature intent acceptance criteria"
|
|
476
|
+
argument :non_goals, Array, required: false, items: String, description: "Feature intent non-goals"
|
|
477
|
+
argument :related_task_id, String, required: false, description: "Related task id (feature intents)"
|
|
490
478
|
call do |args|
|
|
491
|
-
mcp_server.send(:guard!, "agentf-memory-add-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
479
|
+
mcp_server.send(:guard!, "agentf-memory-add-intent", **args)
|
|
480
|
+
id = nil
|
|
481
|
+
kind = args[:kind].to_s.downcase
|
|
482
|
+
res = mcp_server.send(:safe_memory_write, memory, attempted: { tool: "agentf-memory-add-intent", args: args }) do
|
|
483
|
+
id = case kind
|
|
484
|
+
when "business"
|
|
485
|
+
memory.store_business_intent(
|
|
486
|
+
title: args[:title],
|
|
487
|
+
description: args[:description],
|
|
488
|
+
constraints: args[:constraints] || [],
|
|
489
|
+
priority: args[:priority] || 1
|
|
490
|
+
)
|
|
491
|
+
when "feature"
|
|
492
|
+
memory.store_feature_intent(
|
|
493
|
+
title: args[:title],
|
|
494
|
+
description: args[:description],
|
|
495
|
+
acceptance_criteria: args[:acceptance] || [],
|
|
496
|
+
non_goals: args[:non_goals] || [],
|
|
497
|
+
related_task_id: args[:related_task_id]
|
|
498
|
+
)
|
|
499
|
+
else
|
|
500
|
+
raise ArgumentError, "kind must be business or feature, got: #{kind}"
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
if res.is_a?(Hash) && res["confirmation_required"]
|
|
504
|
+
JSON.generate(confirmation_required: true, confirmation_details: res["confirmation_details"], attempted: res["attempted"])
|
|
505
|
+
else
|
|
506
|
+
JSON.generate(id: id, type: "#{kind}_intent", status: "stored")
|
|
507
|
+
end
|
|
509
508
|
end
|
|
510
509
|
end
|
|
511
510
|
|
|
512
|
-
s.tool("agentf-memory-add-
|
|
513
|
-
description "Store a
|
|
514
|
-
argument :
|
|
515
|
-
argument :
|
|
516
|
-
argument :
|
|
517
|
-
argument :
|
|
518
|
-
argument :
|
|
511
|
+
s.tool("agentf-memory-add-episode") do
|
|
512
|
+
description "Store a memory episode with type and outcome (type: episode|lesson|incident|playbook, outcome: positive|negative|neutral)."
|
|
513
|
+
argument :type, String, required: true, description: "Episode type: episode|lesson|incident|playbook"
|
|
514
|
+
argument :title, String, required: true, description: "Episode title"
|
|
515
|
+
argument :description, String, required: true, description: "Episode description"
|
|
516
|
+
argument :outcome, String, required: false, description: "Outcome: positive|negative|neutral"
|
|
517
|
+
argument :agent, String, required: false, description: "Agent name"
|
|
518
|
+
argument :context, String, required: false, description: "Additional context"
|
|
519
|
+
argument :code_snippet, String, required: false, description: "Code snippet"
|
|
519
520
|
call do |args|
|
|
520
|
-
mcp_server.send(:guard!, "agentf-memory-add-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
521
|
+
mcp_server.send(:guard!, "agentf-memory-add-episode", **args)
|
|
522
|
+
id = nil
|
|
523
|
+
res = mcp_server.send(:safe_memory_write, memory, attempted: { tool: "agentf-memory-add-episode", args: args }) do
|
|
524
|
+
id = memory.store_episode(
|
|
525
|
+
type: args[:type],
|
|
526
|
+
title: args[:title],
|
|
527
|
+
description: args[:description],
|
|
528
|
+
outcome: args[:outcome],
|
|
529
|
+
agent: args[:agent] || Agentf::AgentRoles::ENGINEER,
|
|
530
|
+
context: args[:context].to_s,
|
|
531
|
+
code_snippet: args[:code_snippet].to_s
|
|
532
|
+
)
|
|
533
|
+
end
|
|
534
|
+
if res.is_a?(Hash) && res["confirmation_required"]
|
|
535
|
+
JSON.generate(confirmation_required: true, confirmation_details: res["confirmation_details"], attempted: res["attempted"])
|
|
536
|
+
else
|
|
537
|
+
JSON.generate(id: id, type: args[:type], status: "stored")
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
end
|
|
532
541
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
end
|
|
542
|
+
s.tool("agentf-memory-summary") do
|
|
543
|
+
description "Get summary statistics: counts of memories by type, agent, and outcome."
|
|
544
|
+
call do |_args|
|
|
545
|
+
mcp_server.send(:guard!, "agentf-memory-summary")
|
|
546
|
+
JSON.generate(reviewer.get_summary)
|
|
539
547
|
end
|
|
540
548
|
end
|
|
541
549
|
|
|
@@ -602,7 +610,7 @@ module Agentf
|
|
|
602
610
|
mcp_server.send(:guard!, "agentf-memory-add-lesson", **args)
|
|
603
611
|
begin
|
|
604
612
|
id = nil
|
|
605
|
-
res = mcp_server.send(:
|
|
613
|
+
res = mcp_server.send(:safe_memory_write, memory, attempted: { tool: "agentf-memory-add-lesson", args: args }) do
|
|
606
614
|
id = memory.store_episode(
|
|
607
615
|
type: "lesson",
|
|
608
616
|
title: args[:title],
|
|
@@ -639,7 +647,7 @@ module Agentf
|
|
|
639
647
|
mcp_server.send(:guard!, "agentf-memory-add-playbook", **args)
|
|
640
648
|
begin
|
|
641
649
|
id = nil
|
|
642
|
-
res = mcp_server.send(:
|
|
650
|
+
res = mcp_server.send(:safe_memory_write, memory, attempted: { tool: "agentf-memory-add-playbook", args: args }) do
|
|
643
651
|
id = memory.store_playbook(
|
|
644
652
|
title: args[:title],
|
|
645
653
|
description: args[:description],
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Agentf
|
|
4
|
+
module Memory
|
|
5
|
+
module ConfirmationHandler
|
|
6
|
+
# Wraps a memory write block and normalizes ConfirmationRequired into a
|
|
7
|
+
# structured hash so callers (MCP server, CLI, agents) can handle it
|
|
8
|
+
# uniformly. The optional `_memory` arg is accepted for call-site
|
|
9
|
+
# readability but is not used by this method.
|
|
10
|
+
def safe_memory_write(_memory = nil, attempted: {})
|
|
11
|
+
yield
|
|
12
|
+
nil
|
|
13
|
+
rescue Agentf::Memory::RedisMemory::ConfirmationRequired => e
|
|
14
|
+
{
|
|
15
|
+
"confirmation_required" => true,
|
|
16
|
+
"confirmation_details" => e.details,
|
|
17
|
+
"attempted" => attempted,
|
|
18
|
+
"confirmed_write_token" => "confirmed",
|
|
19
|
+
"confirmation_prompt" => "Ask the user whether to save this memory. If they approve, rerun the same tool with confirmedWrite=confirmed. If they decline, do not retry."
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/agentf/memory.rb
CHANGED
|
@@ -282,6 +282,12 @@ module Agentf
|
|
|
282
282
|
end
|
|
283
283
|
end
|
|
284
284
|
|
|
285
|
+
def get_memories_by_agent(agent:, limit: 10)
|
|
286
|
+
collect_episode_records(scope: "project", agent: agent)
|
|
287
|
+
.sort_by { |mem| -(mem["created_at"] || 0) }
|
|
288
|
+
.first(limit)
|
|
289
|
+
end
|
|
290
|
+
|
|
285
291
|
def get_intents(kind: nil, limit: 10)
|
|
286
292
|
return get_memories_by_type(type: "business_intent", limit: limit) if kind == "business"
|
|
287
293
|
return get_memories_by_type(type: "feature_intent", limit: limit) if kind == "feature"
|
data/lib/agentf/version.rb
CHANGED
data/lib/agentf.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: agentf
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Neal Deters
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: redis
|
|
@@ -137,6 +137,7 @@ files:
|
|
|
137
137
|
- lib/agentf/mcp/server.rb
|
|
138
138
|
- lib/agentf/mcp/stub.rb
|
|
139
139
|
- lib/agentf/memory.rb
|
|
140
|
+
- lib/agentf/memory/confirmation_handler.rb
|
|
140
141
|
- lib/agentf/service/providers.rb
|
|
141
142
|
- lib/agentf/tools.rb
|
|
142
143
|
- lib/agentf/tools/component_spec.rb
|