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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ace5a58ed6bfb8389d1e7a68349d7cc9f8d80b4093131a8cf2013388b001a08d
4
- data.tar.gz: ba9a86b1c4b9e7e7edf62bed089d5cb3a1dddb9c5b4282c16e9be27438fec088
3
+ metadata.gz: 9d0041ebcd3b112183ddb349312e4cab8d30237ed1108dbb55d928c525dd59ae
4
+ data.tar.gz: 2577b0dc7af10255d02d2dde419aaa8e1135aea4e36dd67cc10b5d55b1866ba6
5
5
  SHA512:
6
- metadata.gz: 35767581d0b7561c1800464909dbf00524724527148b20f4f1ab911d846301d1a60333e8703d7b3db04e7d3d34e1a977d522fef236f8899692ae8a8c2bac65ad
7
- data.tar.gz: 8d61e62517723d4bc4d39905dfb87b792d27ba1bfbe2b1eb6c445409b138091d072413ad21bffbe2cd584887d9a75f798df64f79601594ccb1029e9db54439f1
6
+ metadata.gz: ceae3f4d97e84e9934eae6591b7a2be4ea6ed56e31f4555110f998a2f3551f8231572a8c56dd790815988772ad8f4584f5d043d0283828665bcb69ad033d9ef2
7
+ data.tar.gz: 2ff0e3ece10105b6632ebd8e34c5b78c870dc6b8e105dabc41bc7f472502a8c33aee1dc7d10f14fa0ffaea513553c24f1353a9bda7a6647117f200cf8a43de88
@@ -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
- result
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" => ["Return generated component details", "Persist successful implementation pattern"],
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" => ["Return empty generated code for successful design task"],
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" => ["Persist execution outcome", "Return deterministic success boolean"],
48
- "ask_first" => ["Applying architecture style changes across unrelated modules", "Persisting execution outcomes to memory (success/pitfall)"] ,
49
- "never" => ["Claim implementation complete without execution result"],
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
 
@@ -45,14 +45,28 @@ module Agentf
45
45
 
46
46
  def self.policy_boundaries
47
47
  {
48
- "always" => ["Produce framework-aware tests", "Verify red/green state when TDD enabled"],
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" => ["Mark passing when command output is uncertain"],
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
@@ -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 = safe_cli_memory_write(@memory, attempted: { command: "add-business-intent", args: { title: title, description: description, constraints: constraints, priority: priority } }) do
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 = safe_cli_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
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 = safe_cli_memory_write(@memory, attempted: { command: "add-playbook", args: { title: title, description: description, steps: steps, feature_area: feature_area, agent: agent } }) do
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 = safe_cli_memory_write(@memory, attempted: { command: "add-#{type}", args: { title: title, description: description, context: context, agent: agent, code: code_snippet, outcome: outcome } }) do
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
- business-intents List business intents
565
- feature-intents List feature intents
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-business-intent "Reliability" "Prioritize uptime" --constraints="No downtime;No vendor lock-in"
589
- agentf memory add-feature-intent "Agent handoff" "Improve orchestrator continuity" --acceptance="Keeps context;Preserves task state"
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.get_recent_memories(limit: 100)
91
- filtered = memories.select { |m| m["agent"] == agent }
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 by keyword in title or description
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
@@ -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, provider: provider))
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, provider: provider))
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, provider:)
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, provider:)
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
 
@@ -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-business-intents
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-business-intent") do
485
- description "Store a business intent in Redis."
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: "Constraints"
489
- argument :priority, Integer, required: false, description: "Priority"
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-business-intent", **args)
492
- begin
493
- id = nil
494
- res = mcp_server.send(:safe_mcp_memory_write, memory, attempted: { tool: "agentf-memory-add-business-intent", args: args }) do
495
- id = memory.store_business_intent(
496
- title: args[:title],
497
- description: args[:description],
498
- constraints: args[:constraints] || [],
499
- priority: args[:priority] || 1
500
- )
501
- end
502
-
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: "business_intent", status: "stored")
507
- end
508
- end
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-feature-intent") do
513
- description "Store a feature intent in Redis."
514
- argument :title, String, required: true, description: "Intent title"
515
- argument :description, String, required: true, description: "Intent description"
516
- argument :acceptance, Array, required: false, items: String, description: "Acceptance criteria"
517
- argument :non_goals, Array, required: false, items: String, description: "Non-goals"
518
- argument :related_task_id, String, required: false, description: "Related task id"
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-feature-intent", **args)
521
- begin
522
- id = nil
523
- res = mcp_server.send(:safe_mcp_memory_write, memory, attempted: { tool: "agentf-memory-add-feature-intent", args: args }) do
524
- id = memory.store_feature_intent(
525
- title: args[:title],
526
- description: args[:description],
527
- acceptance_criteria: args[:acceptance] || [],
528
- non_goals: args[:non_goals] || [],
529
- related_task_id: args[:related_task_id]
530
- )
531
- end
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
- if res.is_a?(Hash) && res["confirmation_required"]
534
- JSON.generate(confirmation_required: true, confirmation_details: res["confirmation_details"], attempted: res["attempted"])
535
- else
536
- JSON.generate(id: id, type: "feature_intent", status: "stored")
537
- end
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(:safe_mcp_memory_write, memory, attempted: { tool: "agentf-memory-add-lesson", args: args }) do
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(:safe_mcp_memory_write, memory, attempted: { tool: "agentf-memory-add-playbook", args: args }) do
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"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Agentf
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.0"
5
5
  end
data/lib/agentf.rb CHANGED
@@ -83,6 +83,7 @@ end
83
83
 
84
84
  # Load submodules
85
85
  require_relative "agentf/memory"
86
+ require_relative "agentf/memory/confirmation_handler"
86
87
  require_relative "agentf/tools"
87
88
  require_relative "agentf/commands"
88
89
  require_relative "agentf/commands/registry"
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.6.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-20 00:00:00.000000000 Z
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