agentf 0.4.5 → 0.4.7

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: 375fdc4ce17f8fe753e2f99a1ac35ee5a4ce2ac8b592812b924a4b755faf7827
4
- data.tar.gz: 7d45fb33f315c8cd15fb4297556d063603d763fe3a0f9e79906a259f1dac6808
3
+ metadata.gz: 2cf35c1c3dd0c0b331de754c2805b3bbd27e9175a339ecb264509ab3ff5e843b
4
+ data.tar.gz: 1c552174bd8cfdfb0770f8a4c7ae19dd732c7c8e2d440f9197a5c210a0e55b1c
5
5
  SHA512:
6
- metadata.gz: 4eb3d691ed13cae8b2780955eb4e059c878f1ad3f219ffe577d439b1695a7d5d3408d6770f2a7d1a3d45e78df678993a882eb130003e4bcc5c5e5759b7508051
7
- data.tar.gz: 635accb75b7532c51cee21c02a5d54b9df35010e2db3c46efe3e788452cdaca5f9b35bfdc538969227dd0808cf4b3b37d1e0821f73f8c9431f2ec54dca12ea2e
6
+ metadata.gz: e60ee2faa826fe20c6845ab4250153838519456109893adcbba592e3a389a85520c00e3302a4bfb279e3b53b2c79c74afbe32a621a044837e904ca73dee37c54
7
+ data.tar.gz: d246b77664af5a645dc00afd17d0ea2799cf786d8c51f910212c7d24516e0f53a00bbeab81ec84b23a466d3a2185058bdbc89318aca932f20b7ef7c16be7e668
@@ -45,8 +45,8 @@ module Agentf
45
45
 
46
46
  def self.policy_boundaries
47
47
  {
48
- "always" => ["Return analysis with root causes and suggested fix", "Persist debugging lesson"],
49
- "ask_first" => ["Applying speculative fixes without reproducible error"],
48
+ "always" => ["Return analysis with root causes and suggested fix"],
49
+ "ask_first" => ["Applying speculative fixes without reproducible error", "Persisting debugging lessons to memory"],
50
50
  "never" => ["Discard stack trace context when available"],
51
51
  "required_inputs" => ["error_text"],
52
52
  "required_outputs" => ["analysis", "success"]
@@ -46,7 +46,7 @@ module Agentf
46
46
  def self.policy_boundaries
47
47
  {
48
48
  "always" => ["Return generated component details", "Persist successful implementation pattern"],
49
- "ask_first" => ["Changing primary UI framework"],
49
+ "ask_first" => ["Changing primary UI framework", "Persisting successful implementation patterns to memory"],
50
50
  "never" => ["Return empty generated code for successful design task"],
51
51
  "required_inputs" => ["design_spec"],
52
52
  "required_outputs" => ["component", "generated_code", "success"]
@@ -45,8 +45,8 @@ module Agentf
45
45
 
46
46
  def self.policy_boundaries
47
47
  {
48
- "always" => ["Return concrete file evidence", "Persist exploration breadcrumbs"],
49
- "ask_first" => ["Scanning outside configured base path"],
48
+ "always" => ["Return concrete file evidence"],
49
+ "ask_first" => ["Scanning outside configured base path", "Persisting exploration breadcrumbs to memory"],
50
50
  "never" => ["Mutate project files during exploration"],
51
51
  "required_inputs" => [],
52
52
  "required_outputs" => ["files", "context_gathered"]
@@ -45,8 +45,8 @@ module Agentf
45
45
 
46
46
  def self.policy_boundaries
47
47
  {
48
- "always" => ["Return issue list and best practices", "Persist outcome as success or pitfall"],
49
- "ask_first" => ["Allowing known secret patterns in context"],
48
+ "always" => ["Return issue list and best practices"],
49
+ "ask_first" => ["Allowing known secret patterns in context", "Persisting security scan findings to memory"],
50
50
  "never" => ["Echo raw secrets in output"],
51
51
  "required_inputs" => ["task"],
52
52
  "required_outputs" => ["issues", "best_practices"]
@@ -45,7 +45,7 @@ module Agentf
45
45
  def self.policy_boundaries
46
46
  {
47
47
  "always" => ["Persist execution outcome", "Return deterministic success boolean"],
48
- "ask_first" => ["Applying architecture style changes across unrelated modules"],
48
+ "ask_first" => ["Applying architecture style changes across unrelated modules", "Persisting execution outcomes to memory (success/pitfall)"] ,
49
49
  "never" => ["Claim implementation complete without execution result"],
50
50
  "required_inputs" => ["description"],
51
51
  "required_outputs" => ["subtask_id", "success"]
@@ -46,7 +46,7 @@ module Agentf
46
46
  def self.policy_boundaries
47
47
  {
48
48
  "always" => ["Produce framework-aware tests", "Verify red/green state when TDD enabled"],
49
- "ask_first" => ["Changing test framework conventions"],
49
+ "ask_first" => ["Changing test framework conventions", "Persisting test-generation outcomes to memory"],
50
50
  "never" => ["Mark passing when command output is uncertain"],
51
51
  "required_inputs" => [],
52
52
  "required_outputs" => ["test_file"]
@@ -25,8 +25,8 @@ module Agentf
25
25
  "store_lesson" => { cli: "agentf memory add-lesson \"<title>\" \"<description>\" --agent=<AGENT> --tags=learning", tool: "agentf-memory-add-lesson" },
26
26
  "store_success" => { cli: "agentf memory add-success \"<title>\" \"<description>\" --agent=<AGENT> --tags=success", tool: "agentf-memory-add-success" },
27
27
  "store_pitfall" => { cli: "agentf memory add-pitfall \"<title>\" \"<description>\" --agent=<AGENT> --tags=pitfall", tool: "agentf-memory-add-pitfall" },
28
- "store_business_intent" => { cli: "agentf memory add-business-intent \"<title>\" \"<description>\" --tags=strategy", tool: "agentf-memory-add-lesson" },
29
- "store_feature_intent" => { cli: "agentf memory add-feature-intent \"<title>\" \"<description>\" --acceptance=\"<criteria>\"", tool: "agentf-memory-add-lesson" }
28
+ "store_business_intent" => { cli: "agentf memory add-business-intent \"<title>\" \"<description>\" --tags=strategy", tool: "agentf-memory-add-business-intent" },
29
+ "store_feature_intent" => { cli: "agentf memory add-feature-intent \"<title>\" \"<description>\" --acceptance=\"<criteria>\"", tool: "agentf-memory-add-feature-intent" }
30
30
  }.freeze
31
31
 
32
32
  PROVIDER_LAYOUTS = {
@@ -631,6 +631,165 @@ module Agentf
631
631
  return runAgentfCli(context.directory, "memory", "search", [args.query, "-n", String(limit)]);
632
632
  },
633
633
  }),
634
+ "agentf-memory-by-tag": tool({
635
+ description: "Get Agentf memories by tag.",
636
+ args: {
637
+ tag: tool.schema.string().describe("Tag to filter by"),
638
+ limit: tool.schema.number().int().min(1).max(100).optional().describe("How many results to return"),
639
+ },
640
+ async execute(args, context) {
641
+ const limit = args.limit ?? 10;
642
+ return runAgentfCli(context.directory, "memory", "by-tag", [args.tag, "-n", String(limit)]);
643
+ },
644
+ }),
645
+ "agentf-memory-by-agent": tool({
646
+ description: "Get Agentf memories by agent.",
647
+ args: {
648
+ agent: tool.schema.string().describe("Agent name"),
649
+ limit: tool.schema.number().int().min(1).max(100).optional().describe("How many results to return"),
650
+ },
651
+ async execute(args, context) {
652
+ const limit = args.limit ?? 10;
653
+ return runAgentfCli(context.directory, "memory", "by-agent", [args.agent, "-n", String(limit)]);
654
+ },
655
+ }),
656
+ "agentf-memory-by-type": tool({
657
+ description: "Get Agentf memories by type.",
658
+ args: {
659
+ type: tool.schema.string().describe("Memory type (pitfall|lesson|success|business_intent|feature_intent)"),
660
+ limit: tool.schema.number().int().min(1).max(100).optional().describe("How many results to return"),
661
+ },
662
+ async execute(args, context) {
663
+ const limit = args.limit ?? 10;
664
+ return runAgentfCli(context.directory, "memory", "by-type", [args.type, "-n", String(limit)]);
665
+ },
666
+ }),
667
+ "agentf-memory-tags": tool({
668
+ description: "List all unique memory tags.",
669
+ args: {},
670
+ async execute(_args, context) {
671
+ return runAgentfCli(context.directory, "memory", "tags", []);
672
+ },
673
+ }),
674
+ "agentf-memory-pitfalls": tool({
675
+ description: "List pitfall memories.",
676
+ args: { limit: tool.schema.number().int().min(1).max(100).optional() },
677
+ async execute(args, context) {
678
+ const limit = args.limit ?? 10;
679
+ return runAgentfCli(context.directory, "memory", "pitfalls", ["-n", String(limit)]);
680
+ },
681
+ }),
682
+ "agentf-memory-lessons": tool({
683
+ description: "List lesson memories.",
684
+ args: { limit: tool.schema.number().int().min(1).max(100).optional() },
685
+ async execute(args, context) {
686
+ const limit = args.limit ?? 10;
687
+ return runAgentfCli(context.directory, "memory", "lessons", ["-n", String(limit)]);
688
+ },
689
+ }),
690
+ "agentf-memory-successes": tool({
691
+ description: "List success memories.",
692
+ args: { limit: tool.schema.number().int().min(1).max(100).optional() },
693
+ async execute(args, context) {
694
+ const limit = args.limit ?? 10;
695
+ return runAgentfCli(context.directory, "memory", "successes", ["-n", String(limit)]);
696
+ },
697
+ }),
698
+ "agentf-memory-intents": tool({
699
+ description: "List intents (business, feature or both).",
700
+ args: { kind: tool.schema.string().optional(), limit: tool.schema.number().int().min(1).max(100).optional() },
701
+ async execute(args, context) {
702
+ const limit = args.limit ?? 10;
703
+ const kind = args.kind ? String(args.kind) : "";
704
+ const cmdArgs = kind ? [kind, "-n", String(limit)] : ["-n", String(limit)];
705
+ return runAgentfCli(context.directory, "memory", "intents", cmdArgs);
706
+ },
707
+ }),
708
+ "agentf-memory-business-intents": tool({
709
+ description: "List business intents.",
710
+ args: { limit: tool.schema.number().int().min(1).max(100).optional() },
711
+ async execute(args, context) {
712
+ const limit = args.limit ?? 10;
713
+ return runAgentfCli(context.directory, "memory", "business-intents", ["-n", String(limit)]);
714
+ },
715
+ }),
716
+ "agentf-memory-feature-intents": tool({
717
+ description: "List feature intents.",
718
+ args: { limit: tool.schema.number().int().min(1).max(100).optional() },
719
+ async execute(args, context) {
720
+ const limit = args.limit ?? 10;
721
+ return runAgentfCli(context.directory, "memory", "feature-intents", ["-n", String(limit)]);
722
+ },
723
+ }),
724
+ "agentf-memory-add-business-intent": tool({
725
+ description: "Store a business intent in Redis.",
726
+ args: {
727
+ title: tool.schema.string(),
728
+ description: tool.schema.string(),
729
+ tags: tool.schema.array(tool.schema.string()).optional(),
730
+ constraints: tool.schema.array(tool.schema.string()).optional(),
731
+ priority: tool.schema.number().int().optional(),
732
+ },
733
+ async execute(args, context) {
734
+ const commandArgs = [args.title, args.description];
735
+ if (args.tags?.length) commandArgs.push(`--tags=${args.tags.join(",")}`);
736
+ if (args.constraints?.length) commandArgs.push(`--constraints=${args.constraints.join(";")}`);
737
+ if (Number.isInteger(args.priority)) commandArgs.push(`--priority=${String(args.priority)}`);
738
+ return runAgentfCli(context.directory, "memory", "add-business-intent", commandArgs);
739
+ },
740
+ }),
741
+ "agentf-memory-add-feature-intent": tool({
742
+ description: "Store a feature intent in Redis.",
743
+ args: {
744
+ title: tool.schema.string(),
745
+ description: tool.schema.string(),
746
+ tags: tool.schema.array(tool.schema.string()).optional(),
747
+ acceptance: tool.schema.array(tool.schema.string()).optional(),
748
+ non_goals: tool.schema.array(tool.schema.string()).optional(),
749
+ related_task_id: tool.schema.string().optional(),
750
+ },
751
+ async execute(args, context) {
752
+ const commandArgs = [args.title, args.description];
753
+ if (args.tags?.length) commandArgs.push(`--tags=${args.tags.join(",")}`);
754
+ if (args.acceptance?.length) commandArgs.push(`--acceptance=${args.acceptance.join(";")}`);
755
+ if (args.non_goals?.length) commandArgs.push(`--non-goals=${args.non_goals.join(";")}`);
756
+ if (args.related_task_id) commandArgs.push(`--task=${args.related_task_id}`);
757
+ return runAgentfCli(context.directory, "memory", "add-feature-intent", commandArgs);
758
+ },
759
+ }),
760
+ "agentf-memory-neighbors": tool({
761
+ description: "Get neighboring memory nodes by edge traversal.",
762
+ args: {
763
+ node_id: tool.schema.string(),
764
+ relation: tool.schema.string().optional(),
765
+ depth: tool.schema.number().int().optional(),
766
+ limit: tool.schema.number().int().optional(),
767
+ },
768
+ async execute(args, context) {
769
+ const commandArgs = [args.node_id];
770
+ if (args.relation) commandArgs.push(`--relation=${args.relation}`);
771
+ if (Number.isInteger(args.depth)) commandArgs.push(`--depth=${String(args.depth)}`);
772
+ if (Number.isInteger(args.limit)) commandArgs.push(`-n`, String(args.limit));
773
+ return runAgentfCli(context.directory, "memory", "neighbors", commandArgs);
774
+ },
775
+ }),
776
+ "agentf-memory-subgraph": tool({
777
+ description: "Build a subgraph from seed ids.",
778
+ args: {
779
+ seed_ids: tool.schema.array(tool.schema.string()),
780
+ relation_filters: tool.schema.array(tool.schema.string()).optional(),
781
+ depth: tool.schema.number().int().optional(),
782
+ limit: tool.schema.number().int().optional(),
783
+ },
784
+ async execute(args, context) {
785
+ const seeds = (args.seed_ids || []).join(",");
786
+ const commandArgs = [seeds];
787
+ if (args.relation_filters?.length) commandArgs.push(`--relation=${args.relation_filters.join(",")}`);
788
+ if (Number.isInteger(args.depth)) commandArgs.push(`--depth=${String(args.depth)}`);
789
+ if (Number.isInteger(args.limit)) commandArgs.push(`-n`, String(args.limit));
790
+ return runAgentfCli(context.directory, "memory", "subgraph", commandArgs);
791
+ },
792
+ }),
634
793
  "agentf-memory-add-lesson": tool({
635
794
  description: "Store a lesson memory in Redis.",
636
795
  args: {
@@ -28,17 +28,31 @@ module Agentf
28
28
  agentf-architecture-analyze-layers
29
29
  agentf-memory-recent
30
30
  agentf-memory-search
31
+ agentf-memory-by-tag
32
+ agentf-memory-by-agent
33
+ agentf-memory-by-type
34
+ agentf-memory-tags
35
+ agentf-memory-pitfalls
36
+ agentf-memory-lessons
37
+ agentf-memory-successes
38
+ agentf-memory-intents
39
+ agentf-memory-business-intents
40
+ agentf-memory-feature-intents
31
41
  agentf-memory-neighbors
32
42
  agentf-memory-subgraph
33
43
  agentf-memory-add-lesson
34
44
  agentf-memory-add-success
35
45
  agentf-memory-add-pitfall
46
+ agentf-memory-add-business-intent
47
+ agentf-memory-add-feature-intent
36
48
  ].freeze
37
49
 
38
50
  WRITE_TOOLS = Set.new(%w[
39
51
  agentf-memory-add-lesson
40
52
  agentf-memory-add-success
41
53
  agentf-memory-add-pitfall
54
+ agentf-memory-add-business-intent
55
+ agentf-memory-add-feature-intent
42
56
  ]).freeze
43
57
 
44
58
  attr_reader :server, :guardrails
@@ -214,6 +228,160 @@ module Agentf
214
228
  end
215
229
  end
216
230
 
231
+ s.tool("agentf-memory-by-tag") do
232
+ description "Get memories by tag."
233
+ argument :tag, String, required: true, description: "Tag to filter"
234
+ argument :limit, Integer, required: false, description: "How many results to return (1-100)"
235
+ call do |args|
236
+ mcp_server.send(:guard!, "agentf-memory-by-tag", **args)
237
+ result = reviewer.get_by_tag(args[:tag], limit: args[:limit] || 10)
238
+ JSON.generate(result)
239
+ end
240
+ end
241
+
242
+ s.tool("agentf-memory-by-agent") do
243
+ description "Get memories by agent."
244
+ argument :agent, String, required: true, description: "Agent name"
245
+ argument :limit, Integer, required: false, description: "How many results to return (1-100)"
246
+ call do |args|
247
+ mcp_server.send(:guard!, "agentf-memory-by-agent", **args)
248
+ result = reviewer.get_by_agent(args[:agent], limit: args[:limit] || 10)
249
+ JSON.generate(result)
250
+ end
251
+ end
252
+
253
+ s.tool("agentf-memory-by-type") do
254
+ description "Get memories by type."
255
+ argument :type, String, required: true, description: "Memory type"
256
+ argument :limit, Integer, required: false, description: "How many results to return (1-100)"
257
+ call do |args|
258
+ mcp_server.send(:guard!, "agentf-memory-by-type", **args)
259
+ result = reviewer.get_by_type(args[:type], limit: args[:limit] || 10)
260
+ JSON.generate(result)
261
+ end
262
+ end
263
+
264
+ s.tool("agentf-memory-tags") do
265
+ description "List all unique memory tags."
266
+ call do |args|
267
+ mcp_server.send(:guard!, "agentf-memory-tags", **args)
268
+ result = reviewer.get_all_tags
269
+ JSON.generate(result)
270
+ end
271
+ end
272
+
273
+ s.tool("agentf-memory-pitfalls") do
274
+ description "List pitfall memories."
275
+ argument :limit, Integer, required: false, description: "How many results to return (1-100)"
276
+ call do |args|
277
+ mcp_server.send(:guard!, "agentf-memory-pitfalls", **args)
278
+ result = reviewer.get_pitfalls(limit: args[:limit] || 10)
279
+ JSON.generate(result)
280
+ end
281
+ end
282
+
283
+ s.tool("agentf-memory-lessons") do
284
+ description "List lesson memories."
285
+ argument :limit, Integer, required: false, description: "How many results to return (1-100)"
286
+ call do |args|
287
+ mcp_server.send(:guard!, "agentf-memory-lessons", **args)
288
+ result = reviewer.get_lessons(limit: args[:limit] || 10)
289
+ JSON.generate(result)
290
+ end
291
+ end
292
+
293
+ s.tool("agentf-memory-successes") do
294
+ description "List success memories."
295
+ argument :limit, Integer, required: false, description: "How many results to return (1-100)"
296
+ call do |args|
297
+ mcp_server.send(:guard!, "agentf-memory-successes", **args)
298
+ result = reviewer.get_successes(limit: args[:limit] || 10)
299
+ JSON.generate(result)
300
+ end
301
+ end
302
+
303
+ s.tool("agentf-memory-intents") do
304
+ description "List intents (business|feature)."
305
+ argument :kind, String, required: false, description: "Optional: business|feature"
306
+ argument :limit, Integer, required: false, description: "How many results to return (1-100)"
307
+ call do |args|
308
+ mcp_server.send(:guard!, "agentf-memory-intents", **args)
309
+ kind = args[:kind]
310
+ limit = args[:limit] || 10
311
+ result = case kind
312
+ when "business"
313
+ reviewer.get_business_intents(limit: limit)
314
+ when "feature"
315
+ reviewer.get_feature_intents(limit: limit)
316
+ else
317
+ reviewer.get_intents(limit: limit)
318
+ end
319
+ JSON.generate(result)
320
+ end
321
+ end
322
+
323
+ s.tool("agentf-memory-business-intents") do
324
+ description "List business intents."
325
+ argument :limit, Integer, required: false, description: "How many results to return (1-100)"
326
+ call do |args|
327
+ mcp_server.send(:guard!, "agentf-memory-business-intents", **args)
328
+ result = reviewer.get_business_intents(limit: args[:limit] || 10)
329
+ JSON.generate(result)
330
+ end
331
+ end
332
+
333
+ s.tool("agentf-memory-feature-intents") do
334
+ description "List feature intents."
335
+ argument :limit, Integer, required: false, description: "How many results to return (1-100)"
336
+ call do |args|
337
+ mcp_server.send(:guard!, "agentf-memory-feature-intents", **args)
338
+ result = reviewer.get_feature_intents(limit: args[:limit] || 10)
339
+ JSON.generate(result)
340
+ end
341
+ end
342
+
343
+ s.tool("agentf-memory-add-business-intent") do
344
+ description "Store a business intent in Redis."
345
+ argument :title, String, required: true, description: "Intent title"
346
+ argument :description, String, required: true, description: "Intent description"
347
+ argument :tags, Array, required: false, items: String, description: "Tags"
348
+ argument :constraints, Array, required: false, items: String, description: "Constraints"
349
+ argument :priority, Integer, required: false, description: "Priority"
350
+ call do |args|
351
+ mcp_server.send(:guard!, "agentf-memory-add-business-intent", **args)
352
+ id = memory.store_business_intent(
353
+ title: args[:title],
354
+ description: args[:description],
355
+ tags: args[:tags] || [],
356
+ constraints: args[:constraints] || [],
357
+ priority: args[:priority] || 1
358
+ )
359
+ JSON.generate(id: id, type: "business_intent", status: "stored")
360
+ end
361
+ end
362
+
363
+ s.tool("agentf-memory-add-feature-intent") do
364
+ description "Store a feature intent in Redis."
365
+ argument :title, String, required: true, description: "Intent title"
366
+ argument :description, String, required: true, description: "Intent description"
367
+ argument :tags, Array, required: false, items: String, description: "Tags"
368
+ argument :acceptance, Array, required: false, items: String, description: "Acceptance criteria"
369
+ argument :non_goals, Array, required: false, items: String, description: "Non-goals"
370
+ argument :related_task_id, String, required: false, description: "Related task id"
371
+ call do |args|
372
+ mcp_server.send(:guard!, "agentf-memory-add-feature-intent", **args)
373
+ id = memory.store_feature_intent(
374
+ title: args[:title],
375
+ description: args[:description],
376
+ tags: args[:tags] || [],
377
+ acceptance_criteria: args[:acceptance] || [],
378
+ non_goals: args[:non_goals] || [],
379
+ related_task_id: args[:related_task_id]
380
+ )
381
+ JSON.generate(id: id, type: "feature_intent", status: "stored")
382
+ end
383
+ end
384
+
217
385
  s.tool("agentf-memory-neighbors") do
218
386
  description "Get neighboring memory nodes by edge traversal."
219
387
  argument :node_id, String, required: true, description: "Starting node id"
data/lib/agentf/memory.rb CHANGED
@@ -45,7 +45,39 @@ module Agentf
45
45
  end
46
46
 
47
47
  def store_episode(type:, title:, description:, context: "", code_snippet: "", tags: [], agent: Agentf::AgentRoles::ENGINEER,
48
- related_task_id: nil, metadata: {}, entity_ids: [], relationships: [], parent_episode_id: nil, causal_from: nil)
48
+ related_task_id: nil, metadata: {}, entity_ids: [], relationships: [], parent_episode_id: nil, causal_from: nil, confirm: nil)
49
+ # Determine persistence preference from the agent's policy boundaries.
50
+ # Precedence: never > always > ask_first > none.
51
+ auto_confirm = ENV['AGENTF_AUTO_CONFIRM_MEMORIES'] == 'true'
52
+ pref = persistence_preference_for(agent)
53
+
54
+ case pref
55
+ when :never
56
+ begin
57
+ puts "[MEMORY] Skipping persistence for #{agent}: policy forbids persisting memories"
58
+ rescue StandardError
59
+ end
60
+ return nil
61
+ when :always
62
+ # proceed without requiring explicit confirm
63
+ when :ask_first
64
+ unless agent == Agentf::AgentRoles::ORCHESTRATOR || confirm == true || auto_confirm
65
+ begin
66
+ puts "[MEMORY] Skipping persistence for #{agent}: confirmation required by policy"
67
+ rescue StandardError
68
+ end
69
+ return nil
70
+ end
71
+ else
72
+ # default conservative behavior: require explicit confirm (or env opt-in)
73
+ unless agent == Agentf::AgentRoles::ORCHESTRATOR || confirm == true || auto_confirm
74
+ begin
75
+ puts "[MEMORY] Skipping persistence for #{agent}: confirmation required"
76
+ rescue StandardError
77
+ end
78
+ return nil
79
+ end
80
+ end
49
81
  episode_id = "episode_#{SecureRandom.hex(4)}"
50
82
  normalized_metadata = enrich_metadata(
51
83
  metadata: metadata,
@@ -107,52 +139,56 @@ module Agentf
107
139
  episode_id
108
140
  end
109
141
 
110
- def store_success(title:, description:, context: "", code_snippet: "", tags: [], agent: Agentf::AgentRoles::ENGINEER)
111
- store_episode(
142
+ def store_success(title:, description:, context: "", code_snippet: "", tags: [], agent: Agentf::AgentRoles::ENGINEER, confirm: nil)
143
+ store_episode(
112
144
  type: "success",
113
145
  title: title,
114
146
  description: description,
115
147
  context: context,
116
148
  code_snippet: code_snippet,
117
149
  tags: tags,
118
- agent: agent
150
+ agent: agent,
151
+ confirm: confirm
119
152
  )
120
153
  end
121
154
 
122
- def store_pitfall(title:, description:, context: "", code_snippet: "", tags: [], agent: Agentf::AgentRoles::ENGINEER)
123
- store_episode(
155
+ def store_pitfall(title:, description:, context: "", code_snippet: "", tags: [], agent: Agentf::AgentRoles::ENGINEER, confirm: nil)
156
+ store_episode(
124
157
  type: "pitfall",
125
158
  title: title,
126
159
  description: description,
127
160
  context: context,
128
161
  code_snippet: code_snippet,
129
162
  tags: tags,
130
- agent: agent
163
+ agent: agent,
164
+ confirm: confirm
131
165
  )
132
166
  end
133
167
 
134
- def store_lesson(title:, description:, context: "", code_snippet: "", tags: [], agent: Agentf::AgentRoles::ENGINEER)
135
- store_episode(
168
+ def store_lesson(title:, description:, context: "", code_snippet: "", tags: [], agent: Agentf::AgentRoles::ENGINEER, confirm: nil)
169
+ store_episode(
136
170
  type: "lesson",
137
171
  title: title,
138
172
  description: description,
139
173
  context: context,
140
174
  code_snippet: code_snippet,
141
175
  tags: tags,
142
- agent: agent
176
+ agent: agent,
177
+ confirm: confirm
143
178
  )
144
179
  end
145
180
 
146
- def store_business_intent(title:, description:, constraints: [], tags: [], agent: Agentf::AgentRoles::ORCHESTRATOR, priority: 1)
181
+ def store_business_intent(title:, description:, constraints: [], tags: [], agent: Agentf::AgentRoles::ORCHESTRATOR, priority: 1, confirm: nil)
147
182
  context = constraints.any? ? "Constraints: #{constraints.join('; ')}" : ""
148
183
 
149
- store_episode(
184
+ store_episode(
150
185
  type: "business_intent",
151
186
  title: title,
152
187
  description: description,
153
188
  context: context,
154
189
  tags: tags,
155
- agent: agent,
190
+ agent: agent,
191
+ confirm: confirm,
156
192
  metadata: {
157
193
  "intent_kind" => "business",
158
194
  "constraints" => constraints,
@@ -162,18 +198,19 @@ module Agentf
162
198
  end
163
199
 
164
200
  def store_feature_intent(title:, description:, acceptance_criteria: [], non_goals: [], tags: [], agent: Agentf::AgentRoles::PLANNER,
165
- related_task_id: nil)
201
+ related_task_id: nil, confirm: nil)
166
202
  context_parts = []
167
203
  context_parts << "Acceptance: #{acceptance_criteria.join('; ')}" if acceptance_criteria.any?
168
204
  context_parts << "Non-goals: #{non_goals.join('; ')}" if non_goals.any?
169
205
 
170
- store_episode(
206
+ store_episode(
171
207
  type: "feature_intent",
172
208
  title: title,
173
209
  description: description,
174
210
  context: context_parts.join(" | "),
175
211
  tags: tags,
176
212
  agent: agent,
213
+ confirm: confirm,
177
214
  related_task_id: related_task_id,
178
215
  metadata: {
179
216
  "intent_kind" => "feature",
@@ -858,6 +895,54 @@ module Agentf
858
895
  base
859
896
  end
860
897
 
898
+ # Determine whether writes from the given agent should require explicit
899
+ # confirmation. We consider agents that declare "ask_first" policy
900
+ # boundaries to be conservative and require confirmation before persisting
901
+ # memories. Agent classes register policy boundaries on their class
902
+ # definitions (see agents/*). When agent is provided as a role string
903
+ # (eg. "ENGINEER") we try to match against known agent classes.
904
+ def agent_requires_confirmation?(agent)
905
+ begin
906
+ # If a developer passed an agent class-like string, try to map it to
907
+ # the loaded agent class and inspect its policy_boundaries.
908
+ candidate = Agentf::Agents.constants
909
+ .map { |c| Agentf::Agents.const_get(c) }
910
+ .find do |klass|
911
+ klass.is_a?(Class) && klass.respond_to?(:policy_boundaries) && klass.typed_name == agent
912
+ end
913
+
914
+ return false unless candidate
915
+
916
+ boundaries = candidate.policy_boundaries
917
+ ask_first = Array(boundaries["ask_first"]) rescue []
918
+ !ask_first.empty?
919
+ rescue StandardError
920
+ false
921
+ end
922
+ end
923
+
924
+ # Inspect loaded agent classes for explicit persistence preference.
925
+ # Returns one of: :always, :ask_first, :never, or nil when unknown.
926
+ def persistence_preference_for(agent)
927
+ begin
928
+ candidate = Agentf::Agents.constants
929
+ .map { |c| Agentf::Agents.const_get(c) }
930
+ .find do |klass|
931
+ klass.is_a?(Class) && klass.respond_to?(:policy_boundaries) && klass.typed_name == agent
932
+ end
933
+
934
+ return nil unless candidate
935
+
936
+ boundaries = candidate.policy_boundaries
937
+ return :never if Array(boundaries["never"]).any?
938
+ return :always if Array(boundaries["always"]).any? && Array(boundaries["ask_first"]).empty?
939
+ return :ask_first if Array(boundaries["ask_first"]).any?
940
+ nil
941
+ rescue StandardError
942
+ nil
943
+ end
944
+ end
945
+
861
946
  def infer_division(agent)
862
947
  case agent
863
948
  when Agentf::AgentRoles::PLANNER, Agentf::AgentRoles::ORCHESTRATOR, Agentf::AgentRoles::KNOWLEDGE_MANAGER
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Agentf
4
- VERSION = "0.4.5"
4
+ VERSION = "0.4.7"
5
5
  end
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.5
4
+ version: 0.4.7
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-10 00:00:00.000000000 Z
11
+ date: 2026-03-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis