agentf 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 31fbac11988e521fb26969fc26f2da9c434541fb0ada47c1b681a6717c4ee018
4
- data.tar.gz: 761c50541fc3b7207089b22ac710f07b3fd004ae984a1d46b548e92277290196
3
+ metadata.gz: 289cd869806a950c6bb25a92b56cc05d5f90481f3bbe61af65d97af23768d321
4
+ data.tar.gz: 8f16969b26684677f01d01821c541d14a338b924a310ad9cdd83435e7b7896b3
5
5
  SHA512:
6
- metadata.gz: 348d7d27eae2dddac25859f79c3b8fd99b5be0ebbd7d1447308c146155bb70908519e5ece270fd67255aaa2c313d5f689ce2b1ca46442789c6c229fc8ef47561
7
- data.tar.gz: 3064ce1fc1f05c85b4d88e3f415202193427dcf21ad57827571eeda79a7975555390a6c506eca05ed36fce9c00d10fda3d9b357ab97930243f83b9934d1dca15
6
+ metadata.gz: 230df28afb68ed450b76c16185dc14227342cfc3048ed5ab25183a99aebb57c79833180d6185c3d35d33e89dac9d9a514b8ee6ebbeef8ecf64d3822f34db0b0f
7
+ data.tar.gz: 9347250f29a7402d17ed0784c16387e268461785c02aa1d3f94024bdab6a89f2022a065169b1055f6b168d4a0472a193000e88f5c428ea8ce4757ed2843ff066
@@ -54,6 +54,8 @@ module Agentf
54
54
  list_tags
55
55
  when "search"
56
56
  search_memories(args)
57
+ when "delete"
58
+ delete_memories(args)
57
59
  when "neighbors"
58
60
  neighbors(args)
59
61
  when "subgraph"
@@ -315,6 +317,65 @@ module Agentf
315
317
  output_graph(result)
316
318
  end
317
319
 
320
+ def delete_memories(args)
321
+ mode = args.shift.to_s
322
+ case mode
323
+ when "id"
324
+ delete_by_id(args)
325
+ when "last"
326
+ delete_last(args)
327
+ when "all"
328
+ delete_all(args)
329
+ else
330
+ $stderr.puts "Error: delete requires one of: id|last|all"
331
+ exit 1
332
+ end
333
+ end
334
+
335
+ def delete_by_id(args)
336
+ id = args.shift.to_s
337
+ if id.empty?
338
+ $stderr.puts "Error: delete id requires a memory id"
339
+ exit 1
340
+ end
341
+
342
+ scope = parse_scope_option(args)
343
+ dry_run = parse_boolean_flag(args, "--dry-run")
344
+ result = @memory.delete_memory_by_id(id: id, scope: scope, dry_run: dry_run)
345
+ output_delete(result)
346
+ end
347
+
348
+ def delete_last(args)
349
+ limit = extract_limit(args)
350
+ if limit <= 0
351
+ $stderr.puts "Error: delete last requires -n with value > 0"
352
+ exit 1
353
+ end
354
+
355
+ scope = parse_scope_option(args)
356
+ type = parse_single_option(args, "--type=")
357
+ agent = parse_single_option(args, "--agent=")
358
+ dry_run = parse_boolean_flag(args, "--dry-run")
359
+ result = @memory.delete_recent(limit: limit, scope: scope, type: type, agent: agent, dry_run: dry_run)
360
+ output_delete(result)
361
+ end
362
+
363
+ def delete_all(args)
364
+ scope = parse_scope_option(args)
365
+ type = parse_single_option(args, "--type=")
366
+ agent = parse_single_option(args, "--agent=")
367
+ dry_run = parse_boolean_flag(args, "--dry-run")
368
+ confirmed = parse_boolean_flag(args, "--yes")
369
+
370
+ if !dry_run && !confirmed
371
+ $stderr.puts "Error: delete all requires --yes (or use --dry-run)"
372
+ exit 1
373
+ end
374
+
375
+ result = @memory.delete_all(scope: scope, type: type, agent: agent, dry_run: dry_run)
376
+ output_delete(result)
377
+ end
378
+
318
379
  def subgraph(args)
319
380
  seeds = args.shift.to_s.split(",").map(&:strip).reject(&:empty?)
320
381
  if seeds.empty?
@@ -363,6 +424,43 @@ module Agentf
363
424
  end
364
425
  end
365
426
 
427
+ def output_delete(result)
428
+ if result["error"]
429
+ if @json_output
430
+ puts JSON.generate({ "error" => result["error"] })
431
+ else
432
+ $stderr.puts "Error: #{result['error']}"
433
+ end
434
+ exit 1
435
+ end
436
+
437
+ if @json_output
438
+ puts JSON.generate(result)
439
+ return
440
+ end
441
+
442
+ action = result["dry_run"] ? "Planned" : "Deleted"
443
+ puts "#{action} #{result['deleted_count']} keys (candidates: #{result['candidate_count']})"
444
+ puts "Mode: #{result['mode']} | Scope: #{result['scope']}"
445
+ filters = result["filters"] || {}
446
+ puts "Filters: type=#{filters['type'] || 'any'}, agent=#{filters['agent'] || 'any'}"
447
+ ids = Array(result["deleted_ids"])
448
+ puts "Memory ids: #{ids.join(', ')}" unless ids.empty?
449
+ end
450
+
451
+ def parse_scope_option(args)
452
+ scope = parse_single_option(args, "--scope=") || "project"
453
+ unless %w[project all].include?(scope)
454
+ $stderr.puts "Error: --scope must be project or all"
455
+ exit 1
456
+ end
457
+ scope
458
+ end
459
+
460
+ def parse_boolean_flag(args, flag)
461
+ !args.delete(flag).nil?
462
+ end
463
+
366
464
  def output_graph(result)
367
465
  if result["error"]
368
466
  if @json_output
@@ -420,6 +518,9 @@ module Agentf
420
518
  add-pitfall Store pitfall memory
421
519
  tags List all unique tags
422
520
  search <query> Search memories by keyword
521
+ delete id <memory_id> Delete one memory and related edges
522
+ delete last -n <count> Delete most recent memories
523
+ delete all Delete memories and graph/task keys
423
524
  neighbors <id> Traverse graph edges from a memory id
424
525
  subgraph <ids> Build graph from comma-separated seed ids
425
526
  summary, stats Show summary statistics
@@ -440,6 +541,9 @@ module Agentf
440
541
  agentf memory add-lesson "Refactor strategy" "Extracted adapter seam" --agent=PLANNER --tags=architecture
441
542
  agentf memory add-success "Provider install works" "Installed copilot + opencode manifests" --agent=ENGINEER
442
543
  agentf memory search "react"
544
+ agentf memory delete id episode_abcd
545
+ agentf memory delete last -n 10 --scope=project
546
+ agentf memory delete all --scope=all --yes
443
547
  agentf memory neighbors episode_abcd --depth=2
444
548
  agentf memory by-tag "performance"
445
549
  agentf memory summary
data/lib/agentf/memory.rb CHANGED
@@ -312,6 +312,68 @@ module Agentf
312
312
  all_tags.to_a
313
313
  end
314
314
 
315
+ def delete_memory_by_id(id:, scope: "project", dry_run: false)
316
+ normalized_scope = normalize_scope(scope)
317
+ episode_id = normalize_episode_id(id)
318
+ episode_key = "episodic:#{episode_id}"
319
+ memory = load_episode(episode_key)
320
+
321
+ return delete_result(mode: "id", scope: normalized_scope, dry_run: dry_run, error: "Memory not found: #{id}") unless memory
322
+ if normalized_scope == "project" && memory["project"].to_s != @project.to_s
323
+ return delete_result(mode: "id", scope: normalized_scope, dry_run: dry_run, error: "Memory not in current project")
324
+ end
325
+
326
+ keys = [episode_key]
327
+ keys.concat(collect_related_edge_keys(episode_ids: [episode_id], scope: normalized_scope))
328
+ result = delete_keys(keys.uniq, dry_run: dry_run)
329
+ result.merge(
330
+ "mode" => "id",
331
+ "scope" => normalized_scope,
332
+ "deleted_ids" => [episode_id],
333
+ "filters" => {}
334
+ )
335
+ end
336
+
337
+ def delete_recent(limit: 10, scope: "project", type: nil, agent: nil, dry_run: false)
338
+ normalized_scope = normalize_scope(scope)
339
+ count = [limit.to_i, 0].max
340
+ return delete_result(mode: "last", scope: normalized_scope, dry_run: dry_run, deleted_ids: [], filters: { "type" => type, "agent" => agent }) if count.zero?
341
+
342
+ episodes = collect_episode_records(scope: normalized_scope, type: type, agent: agent)
343
+ selected = episodes.sort_by { |mem| -(mem["created_at"] || 0) }.first(count)
344
+ episode_ids = selected.map { |mem| mem["id"].to_s }
345
+ keys = selected.map { |mem| "episodic:#{mem['id']}" }
346
+ keys.concat(collect_related_edge_keys(episode_ids: episode_ids, scope: normalized_scope))
347
+ result = delete_keys(keys.uniq, dry_run: dry_run)
348
+ result.merge(
349
+ "mode" => "last",
350
+ "scope" => normalized_scope,
351
+ "deleted_ids" => episode_ids,
352
+ "filters" => { "type" => type, "agent" => agent }
353
+ )
354
+ end
355
+
356
+ def delete_all(scope: "project", type: nil, agent: nil, dry_run: false)
357
+ normalized_scope = normalize_scope(scope)
358
+ episodic_records = collect_episode_records(scope: normalized_scope, type: type, agent: agent)
359
+ episode_ids = episodic_records.map { |mem| mem["id"].to_s }
360
+ keys = episodic_records.map { |mem| "episodic:#{mem['id']}" }
361
+ keys.concat(collect_related_edge_keys(episode_ids: episode_ids, scope: normalized_scope))
362
+
363
+ if type.to_s.empty? && agent.to_s.empty?
364
+ keys.concat(collect_edge_keys(scope: normalized_scope))
365
+ keys.concat(collect_semantic_keys(scope: normalized_scope))
366
+ end
367
+
368
+ result = delete_keys(keys.uniq, dry_run: dry_run)
369
+ result.merge(
370
+ "mode" => "all",
371
+ "scope" => normalized_scope,
372
+ "deleted_ids" => episode_ids,
373
+ "filters" => { "type" => type, "agent" => agent }
374
+ )
375
+ end
376
+
315
377
  def store_edge(source_id:, target_id:, relation:, weight: 1.0, tags: [], agent: Agentf::AgentRoles::ORCHESTRATOR, metadata: {})
316
378
  edge_id = "edge_#{SecureRandom.hex(5)}"
317
379
  data = {
@@ -617,6 +679,134 @@ module Agentf
617
679
  { url: @redis_url }
618
680
  end
619
681
 
682
+ def normalize_scope(scope)
683
+ value = scope.to_s.strip.downcase
684
+ return "all" if value == "all"
685
+
686
+ "project"
687
+ end
688
+
689
+ def normalize_episode_id(id)
690
+ value = id.to_s.strip
691
+ value = value.sub("episodic:", "") if value.start_with?("episodic:")
692
+ value
693
+ end
694
+
695
+ def collect_episode_records(scope:, type: nil, agent: nil)
696
+ memories = []
697
+ cursor = "0"
698
+ loop do
699
+ cursor, batch = @client.scan(cursor, match: "episodic:*", count: 100)
700
+ batch.each do |key|
701
+ mem = load_episode(key)
702
+ next unless mem.is_a?(Hash)
703
+ next if scope == "project" && mem["project"].to_s != @project.to_s
704
+ next unless type.to_s.empty? || mem["type"].to_s == type.to_s
705
+ next unless agent.to_s.empty? || mem["agent"].to_s == agent.to_s
706
+
707
+ memories << mem
708
+ end
709
+ break if cursor == "0"
710
+ end
711
+ memories
712
+ end
713
+
714
+ def collect_related_edge_keys(episode_ids:, scope:)
715
+ ids = episode_ids.map(&:to_s).reject(&:empty?).to_set
716
+ return [] if ids.empty?
717
+
718
+ keys = []
719
+ cursor = "0"
720
+ loop do
721
+ cursor, batch = @client.scan(cursor, match: "edge:*", count: 100)
722
+ batch.each do |key|
723
+ edge = load_episode(key)
724
+ next unless edge.is_a?(Hash)
725
+ next if scope == "project" && edge["project"].to_s != @project.to_s
726
+
727
+ source = edge["source_id"].to_s
728
+ target = edge["target_id"].to_s
729
+ keys << key if ids.include?(source) || ids.include?(target)
730
+ end
731
+ break if cursor == "0"
732
+ end
733
+ keys
734
+ end
735
+
736
+ def collect_edge_keys(scope:)
737
+ keys = []
738
+ cursor = "0"
739
+ loop do
740
+ cursor, batch = @client.scan(cursor, match: "edge:*", count: 100)
741
+ batch.each do |key|
742
+ if scope == "all"
743
+ keys << key
744
+ next
745
+ end
746
+
747
+ edge = load_episode(key)
748
+ keys << key if edge.is_a?(Hash) && edge["project"].to_s == @project.to_s
749
+ end
750
+ break if cursor == "0"
751
+ end
752
+ keys
753
+ end
754
+
755
+ def collect_semantic_keys(scope:)
756
+ keys = []
757
+ cursor = "0"
758
+ loop do
759
+ cursor, batch = @client.scan(cursor, match: "semantic:*", count: 100)
760
+ batch.each do |key|
761
+ if scope == "all"
762
+ keys << key
763
+ next
764
+ end
765
+
766
+ task = @client.hgetall(key)
767
+ keys << key if task.is_a?(Hash) && task["project"].to_s == @project.to_s
768
+ end
769
+ break if cursor == "0"
770
+ end
771
+ keys
772
+ end
773
+
774
+ def delete_keys(keys, dry_run:)
775
+ if dry_run
776
+ {
777
+ "dry_run" => true,
778
+ "candidate_count" => keys.length,
779
+ "deleted_count" => 0,
780
+ "deleted_keys" => [],
781
+ "planned_keys" => keys
782
+ }
783
+ else
784
+ deleted = keys.empty? ? 0 : @client.del(*keys)
785
+ {
786
+ "dry_run" => false,
787
+ "candidate_count" => keys.length,
788
+ "deleted_count" => deleted,
789
+ "deleted_keys" => keys,
790
+ "planned_keys" => []
791
+ }
792
+ end
793
+ end
794
+
795
+ def delete_result(mode:, scope:, dry_run:, deleted_ids: [], filters: {}, error: nil)
796
+ {
797
+ "mode" => mode,
798
+ "scope" => scope,
799
+ "dry_run" => dry_run,
800
+ "candidate_count" => 0,
801
+ "deleted_count" => 0,
802
+ "deleted_keys" => [],
803
+ "planned_keys" => [],
804
+ "deleted_ids" => deleted_ids,
805
+ "filters" => filters,
806
+ "error" => error
807
+ }
808
+ end
809
+
620
810
  def persist_relationship_edges(episode_id:, related_task_id:, relationships:, metadata:, tags:, agent:)
621
811
  if related_task_id && !related_task_id.to_s.strip.empty?
622
812
  store_edge(source_id: episode_id, target_id: related_task_id, relation: "relates_to", tags: tags, agent: agent)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Agentf
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: agentf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Neal Deters