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 +4 -4
- data/lib/agentf/cli/memory.rb +104 -0
- data/lib/agentf/memory.rb +190 -0
- data/lib/agentf/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 289cd869806a950c6bb25a92b56cc05d5f90481f3bbe61af65d97af23768d321
|
|
4
|
+
data.tar.gz: 8f16969b26684677f01d01821c541d14a338b924a310ad9cdd83435e7b7896b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 230df28afb68ed450b76c16185dc14227342cfc3048ed5ab25183a99aebb57c79833180d6185c3d35d33e89dac9d9a514b8ee6ebbeef8ecf64d3822f34db0b0f
|
|
7
|
+
data.tar.gz: 9347250f29a7402d17ed0784c16387e268461785c02aa1d3f94024bdab6a89f2022a065169b1055f6b168d4a0472a193000e88f5c428ea8ce4757ed2843ff066
|
data/lib/agentf/cli/memory.rb
CHANGED
|
@@ -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)
|
data/lib/agentf/version.rb
CHANGED