agentf 0.3.0 → 0.4.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/agent_roles.rb +29 -0
- data/lib/agentf/agents/architect.rb +16 -0
- data/lib/agentf/agents/base.rb +12 -0
- data/lib/agentf/agents/debugger.rb +16 -0
- data/lib/agentf/agents/designer.rb +16 -0
- data/lib/agentf/agents/documenter.rb +16 -0
- data/lib/agentf/agents/explorer.rb +16 -0
- data/lib/agentf/agents/reviewer.rb +16 -0
- data/lib/agentf/agents/security.rb +16 -0
- data/lib/agentf/agents/specialist.rb +16 -0
- data/lib/agentf/agents/tester.rb +16 -0
- data/lib/agentf/cli/memory.rb +60 -3
- data/lib/agentf/cli/router.rb +1 -1
- data/lib/agentf/cli/update.rb +2 -2
- data/lib/agentf/commands/memory_reviewer.rb +18 -1
- data/lib/agentf/commands/metrics.rb +5 -6
- data/lib/agentf/installer.rb +73 -45
- data/lib/agentf/mcp/server.rb +79 -37
- data/lib/agentf/mcp/stub.rb +81 -0
- data/lib/agentf/memory.rb +345 -20
- data/lib/agentf/packs.rb +15 -15
- data/lib/agentf/service/providers.rb +42 -42
- data/lib/agentf/version.rb +1 -1
- data/lib/agentf/workflow_engine.rb +23 -23
- data/lib/agentf.rb +1 -0
- metadata +10 -6
data/lib/agentf/memory.rb
CHANGED
|
@@ -10,6 +10,8 @@ module Agentf
|
|
|
10
10
|
module Memory
|
|
11
11
|
# Redis-backed memory system for agent learning
|
|
12
12
|
class RedisMemory
|
|
13
|
+
EDGE_INDEX = "edge:links"
|
|
14
|
+
|
|
13
15
|
attr_reader :project
|
|
14
16
|
|
|
15
17
|
def initialize(redis_url: nil, project: nil)
|
|
@@ -21,7 +23,7 @@ module Agentf
|
|
|
21
23
|
ensure_indexes if @search_supported
|
|
22
24
|
end
|
|
23
25
|
|
|
24
|
-
def store_task(content:, embedding: [], language: nil, task_type: nil, success: true, agent:
|
|
26
|
+
def store_task(content:, embedding: [], language: nil, task_type: nil, success: true, agent: Agentf::AgentRoles::PLANNER)
|
|
25
27
|
task_id = "task_#{SecureRandom.hex(4)}"
|
|
26
28
|
|
|
27
29
|
data = {
|
|
@@ -42,9 +44,19 @@ module Agentf
|
|
|
42
44
|
task_id
|
|
43
45
|
end
|
|
44
46
|
|
|
45
|
-
def store_episode(type:, title:, description:, context: "", code_snippet: "", tags: [], agent:
|
|
46
|
-
metadata: {})
|
|
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)
|
|
47
49
|
episode_id = "episode_#{SecureRandom.hex(4)}"
|
|
50
|
+
normalized_metadata = enrich_metadata(
|
|
51
|
+
metadata: metadata,
|
|
52
|
+
agent: agent,
|
|
53
|
+
type: type,
|
|
54
|
+
tags: tags,
|
|
55
|
+
entity_ids: entity_ids,
|
|
56
|
+
relationships: relationships,
|
|
57
|
+
parent_episode_id: parent_episode_id,
|
|
58
|
+
causal_from: causal_from
|
|
59
|
+
)
|
|
48
60
|
|
|
49
61
|
data = {
|
|
50
62
|
"id" => episode_id,
|
|
@@ -58,7 +70,11 @@ module Agentf
|
|
|
58
70
|
"created_at" => Time.now.to_i,
|
|
59
71
|
"agent" => agent,
|
|
60
72
|
"related_task_id" => related_task_id || "",
|
|
61
|
-
"
|
|
73
|
+
"entity_ids" => entity_ids,
|
|
74
|
+
"relationships" => relationships,
|
|
75
|
+
"parent_episode_id" => parent_episode_id.to_s,
|
|
76
|
+
"causal_from" => causal_from.to_s,
|
|
77
|
+
"metadata" => normalized_metadata
|
|
62
78
|
}
|
|
63
79
|
|
|
64
80
|
key = "episodic:#{episode_id}"
|
|
@@ -79,10 +95,19 @@ module Agentf
|
|
|
79
95
|
@client.set(key, payload)
|
|
80
96
|
end
|
|
81
97
|
|
|
98
|
+
persist_relationship_edges(
|
|
99
|
+
episode_id: episode_id,
|
|
100
|
+
related_task_id: related_task_id,
|
|
101
|
+
relationships: relationships,
|
|
102
|
+
metadata: normalized_metadata,
|
|
103
|
+
tags: tags,
|
|
104
|
+
agent: agent
|
|
105
|
+
)
|
|
106
|
+
|
|
82
107
|
episode_id
|
|
83
108
|
end
|
|
84
109
|
|
|
85
|
-
def store_success(title:, description:, context: "", code_snippet: "", tags: [], agent:
|
|
110
|
+
def store_success(title:, description:, context: "", code_snippet: "", tags: [], agent: Agentf::AgentRoles::ENGINEER)
|
|
86
111
|
store_episode(
|
|
87
112
|
type: "success",
|
|
88
113
|
title: title,
|
|
@@ -94,7 +119,7 @@ module Agentf
|
|
|
94
119
|
)
|
|
95
120
|
end
|
|
96
121
|
|
|
97
|
-
def store_pitfall(title:, description:, context: "", code_snippet: "", tags: [], agent:
|
|
122
|
+
def store_pitfall(title:, description:, context: "", code_snippet: "", tags: [], agent: Agentf::AgentRoles::ENGINEER)
|
|
98
123
|
store_episode(
|
|
99
124
|
type: "pitfall",
|
|
100
125
|
title: title,
|
|
@@ -106,7 +131,7 @@ module Agentf
|
|
|
106
131
|
)
|
|
107
132
|
end
|
|
108
133
|
|
|
109
|
-
def store_lesson(title:, description:, context: "", code_snippet: "", tags: [], agent:
|
|
134
|
+
def store_lesson(title:, description:, context: "", code_snippet: "", tags: [], agent: Agentf::AgentRoles::ENGINEER)
|
|
110
135
|
store_episode(
|
|
111
136
|
type: "lesson",
|
|
112
137
|
title: title,
|
|
@@ -118,7 +143,7 @@ module Agentf
|
|
|
118
143
|
)
|
|
119
144
|
end
|
|
120
145
|
|
|
121
|
-
def store_business_intent(title:, description:, constraints: [], tags: [], agent:
|
|
146
|
+
def store_business_intent(title:, description:, constraints: [], tags: [], agent: Agentf::AgentRoles::ORCHESTRATOR, priority: 1)
|
|
122
147
|
context = constraints.any? ? "Constraints: #{constraints.join('; ')}" : ""
|
|
123
148
|
|
|
124
149
|
store_episode(
|
|
@@ -136,7 +161,8 @@ module Agentf
|
|
|
136
161
|
)
|
|
137
162
|
end
|
|
138
163
|
|
|
139
|
-
def store_feature_intent(title:, description:, acceptance_criteria: [], non_goals: [], tags: [], agent:
|
|
164
|
+
def store_feature_intent(title:, description:, acceptance_criteria: [], non_goals: [], tags: [], agent: Agentf::AgentRoles::PLANNER,
|
|
165
|
+
related_task_id: nil)
|
|
140
166
|
context_parts = []
|
|
141
167
|
context_parts << "Acceptance: #{acceptance_criteria.join('; ')}" if acceptance_criteria.any?
|
|
142
168
|
context_parts << "Non-goals: #{non_goals.join('; ')}" if non_goals.any?
|
|
@@ -157,7 +183,8 @@ module Agentf
|
|
|
157
183
|
)
|
|
158
184
|
end
|
|
159
185
|
|
|
160
|
-
def store_incident(title:, description:, root_cause: "", resolution: "", tags: [], agent:
|
|
186
|
+
def store_incident(title:, description:, root_cause: "", resolution: "", tags: [], agent: Agentf::AgentRoles::INCIDENT_RESPONDER,
|
|
187
|
+
business_capability: nil)
|
|
161
188
|
store_episode(
|
|
162
189
|
type: "incident",
|
|
163
190
|
title: title,
|
|
@@ -174,7 +201,7 @@ module Agentf
|
|
|
174
201
|
)
|
|
175
202
|
end
|
|
176
203
|
|
|
177
|
-
def store_playbook(title:, description:, steps: [], tags: [], agent:
|
|
204
|
+
def store_playbook(title:, description:, steps: [], tags: [], agent: Agentf::AgentRoles::PLANNER, feature_area: nil)
|
|
178
205
|
store_episode(
|
|
179
206
|
type: "playbook",
|
|
180
207
|
title: title,
|
|
@@ -226,7 +253,7 @@ module Agentf
|
|
|
226
253
|
query = "@type:#{type} @project:{#{@project}}"
|
|
227
254
|
search_episodic(query: query, limit: limit)
|
|
228
255
|
else
|
|
229
|
-
fetch_memories_without_search(limit:
|
|
256
|
+
fetch_memories_without_search(limit: 100).select { |mem| mem["type"] == type }.first(limit)
|
|
230
257
|
end
|
|
231
258
|
end
|
|
232
259
|
|
|
@@ -285,6 +312,50 @@ module Agentf
|
|
|
285
312
|
all_tags.to_a
|
|
286
313
|
end
|
|
287
314
|
|
|
315
|
+
def store_edge(source_id:, target_id:, relation:, weight: 1.0, tags: [], agent: Agentf::AgentRoles::ORCHESTRATOR, metadata: {})
|
|
316
|
+
edge_id = "edge_#{SecureRandom.hex(5)}"
|
|
317
|
+
data = {
|
|
318
|
+
"id" => edge_id,
|
|
319
|
+
"source_id" => source_id,
|
|
320
|
+
"target_id" => target_id,
|
|
321
|
+
"relation" => relation,
|
|
322
|
+
"weight" => weight.to_f,
|
|
323
|
+
"tags" => tags,
|
|
324
|
+
"project" => @project,
|
|
325
|
+
"agent" => agent,
|
|
326
|
+
"metadata" => metadata,
|
|
327
|
+
"created_at" => Time.now.to_i
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
key = "edge:#{edge_id}"
|
|
331
|
+
payload = JSON.generate(data)
|
|
332
|
+
|
|
333
|
+
if @json_supported
|
|
334
|
+
begin
|
|
335
|
+
@client.call("JSON.SET", key, ".", payload)
|
|
336
|
+
rescue Redis::CommandError => e
|
|
337
|
+
if missing_json_module?(e)
|
|
338
|
+
@json_supported = false
|
|
339
|
+
@client.set(key, payload)
|
|
340
|
+
else
|
|
341
|
+
raise
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
else
|
|
345
|
+
@client.set(key, payload)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
edge_id
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def neighbors(node_id:, relation: nil, depth: 1, limit: 50)
|
|
352
|
+
traverse_edges(seed_ids: [node_id], relation_filters: relation ? [relation] : nil, depth: depth, limit: limit)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def subgraph(seed_ids:, depth: 2, relation_filters: nil, limit: 200)
|
|
356
|
+
traverse_edges(seed_ids: seed_ids, relation_filters: relation_filters, depth: depth, limit: limit)
|
|
357
|
+
end
|
|
358
|
+
|
|
288
359
|
def close
|
|
289
360
|
@client.close
|
|
290
361
|
end
|
|
@@ -295,8 +366,9 @@ module Agentf
|
|
|
295
366
|
return unless @search_supported
|
|
296
367
|
|
|
297
368
|
create_episodic_index
|
|
369
|
+
create_edge_index
|
|
298
370
|
rescue Redis::CommandError => e
|
|
299
|
-
raise Redis::CommandError, "Failed to create
|
|
371
|
+
raise Redis::CommandError, "Failed to create indexes: #{e.message}. Ensure Redis Stack with RediSearch is available." unless index_already_exists?(e)
|
|
300
372
|
end
|
|
301
373
|
|
|
302
374
|
def create_episodic_index
|
|
@@ -320,7 +392,31 @@ module Agentf
|
|
|
320
392
|
"$.metadata.priority", "AS", "priority", "NUMERIC",
|
|
321
393
|
"$.metadata.confidence", "AS", "confidence", "NUMERIC",
|
|
322
394
|
"$.metadata.business_capability", "AS", "business_capability", "TAG",
|
|
323
|
-
"$.metadata.feature_area", "AS", "feature_area", "TAG"
|
|
395
|
+
"$.metadata.feature_area", "AS", "feature_area", "TAG",
|
|
396
|
+
"$.metadata.agent_role", "AS", "agent_role", "TAG",
|
|
397
|
+
"$.metadata.division", "AS", "division", "TAG",
|
|
398
|
+
"$.metadata.specialty", "AS", "specialty", "TAG",
|
|
399
|
+
"$.entity_ids[*]", "AS", "entity_ids", "TAG",
|
|
400
|
+
"$.parent_episode_id", "AS", "parent_episode_id", "TEXT",
|
|
401
|
+
"$.causal_from", "AS", "causal_from", "TEXT"
|
|
402
|
+
)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def create_edge_index
|
|
406
|
+
@client.call(
|
|
407
|
+
"FT.CREATE", EDGE_INDEX,
|
|
408
|
+
"ON", "JSON",
|
|
409
|
+
"PREFIX", "1", "edge:",
|
|
410
|
+
"SCHEMA",
|
|
411
|
+
"$.id", "AS", "id", "TEXT",
|
|
412
|
+
"$.source_id", "AS", "source_id", "TAG",
|
|
413
|
+
"$.target_id", "AS", "target_id", "TAG",
|
|
414
|
+
"$.relation", "AS", "relation", "TAG",
|
|
415
|
+
"$.project", "AS", "project", "TAG",
|
|
416
|
+
"$.agent", "AS", "agent", "TAG",
|
|
417
|
+
"$.weight", "AS", "weight", "NUMERIC",
|
|
418
|
+
"$.created_at", "AS", "created_at", "NUMERIC",
|
|
419
|
+
"$.tags", "AS", "tags", "TAG"
|
|
324
420
|
)
|
|
325
421
|
end
|
|
326
422
|
|
|
@@ -432,15 +528,15 @@ module Agentf
|
|
|
432
528
|
|
|
433
529
|
def context_profile(agent)
|
|
434
530
|
case agent.to_s.upcase
|
|
435
|
-
when
|
|
531
|
+
when Agentf::AgentRoles::PLANNER
|
|
436
532
|
{ "preferred_types" => %w[business_intent feature_intent lesson playbook pitfall], "pitfall_penalty" => 0.1 }
|
|
437
|
-
when
|
|
533
|
+
when Agentf::AgentRoles::ENGINEER
|
|
438
534
|
{ "preferred_types" => %w[playbook success lesson pitfall], "pitfall_penalty" => 0.05 }
|
|
439
|
-
when
|
|
535
|
+
when Agentf::AgentRoles::QA_TESTER
|
|
440
536
|
{ "preferred_types" => %w[lesson pitfall incident success], "pitfall_penalty" => 0.0 }
|
|
441
|
-
when
|
|
537
|
+
when Agentf::AgentRoles::INCIDENT_RESPONDER
|
|
442
538
|
{ "preferred_types" => %w[incident pitfall lesson], "pitfall_penalty" => 0.0 }
|
|
443
|
-
when
|
|
539
|
+
when Agentf::AgentRoles::SECURITY_REVIEWER
|
|
444
540
|
{ "preferred_types" => %w[pitfall lesson incident], "pitfall_penalty" => 0.0 }
|
|
445
541
|
else
|
|
446
542
|
{ "preferred_types" => %w[lesson pitfall success business_intent feature_intent], "pitfall_penalty" => 0.05 }
|
|
@@ -461,7 +557,7 @@ module Agentf
|
|
|
461
557
|
confidence = 1.0 if confidence > 1.0
|
|
462
558
|
|
|
463
559
|
type_score = preferred_types.include?(type) ? 1.0 : 0.25
|
|
464
|
-
agent_score = (memory["agent"] == agent || memory["agent"] ==
|
|
560
|
+
agent_score = (memory["agent"] == agent || memory["agent"] == Agentf::AgentRoles::ORCHESTRATOR) ? 1.0 : 0.2
|
|
465
561
|
age_seconds = [now - memory.fetch("created_at", now).to_i, 0].max
|
|
466
562
|
recency_score = 1.0 / (1.0 + (age_seconds / 86_400.0))
|
|
467
563
|
|
|
@@ -520,6 +616,235 @@ module Agentf
|
|
|
520
616
|
def client_options
|
|
521
617
|
{ url: @redis_url }
|
|
522
618
|
end
|
|
619
|
+
|
|
620
|
+
def persist_relationship_edges(episode_id:, related_task_id:, relationships:, metadata:, tags:, agent:)
|
|
621
|
+
if related_task_id && !related_task_id.to_s.strip.empty?
|
|
622
|
+
store_edge(source_id: episode_id, target_id: related_task_id, relation: "relates_to", tags: tags, agent: agent)
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
Array(relationships).each do |relation|
|
|
626
|
+
next unless relation.is_a?(Hash)
|
|
627
|
+
|
|
628
|
+
target = relation["to"] || relation[:to]
|
|
629
|
+
relation_type = relation["type"] || relation[:type] || "related"
|
|
630
|
+
next if target.to_s.strip.empty?
|
|
631
|
+
|
|
632
|
+
store_edge(
|
|
633
|
+
source_id: episode_id,
|
|
634
|
+
target_id: target,
|
|
635
|
+
relation: relation_type,
|
|
636
|
+
weight: (relation["weight"] || relation[:weight] || 1.0).to_f,
|
|
637
|
+
tags: tags,
|
|
638
|
+
agent: agent,
|
|
639
|
+
metadata: { "source_metadata" => extract_metadata_slice(metadata, %w[intent_kind agent_role division]) }
|
|
640
|
+
)
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
parent = metadata["parent_episode_id"].to_s
|
|
644
|
+
unless parent.empty?
|
|
645
|
+
store_edge(source_id: episode_id, target_id: parent, relation: "child_of", tags: tags, agent: agent)
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
causal_from = metadata["causal_from"].to_s
|
|
649
|
+
unless causal_from.empty?
|
|
650
|
+
store_edge(source_id: episode_id, target_id: causal_from, relation: "caused_by", tags: tags, agent: agent)
|
|
651
|
+
end
|
|
652
|
+
rescue StandardError
|
|
653
|
+
nil
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def enrich_metadata(metadata:, agent:, type:, tags:, entity_ids:, relationships:, parent_episode_id:, causal_from:)
|
|
657
|
+
base = metadata.is_a?(Hash) ? metadata.dup : {}
|
|
658
|
+
base["agent_role"] = agent
|
|
659
|
+
base["division"] = infer_division(agent)
|
|
660
|
+
base["specialty"] = infer_specialty(agent)
|
|
661
|
+
base["capabilities"] = infer_capabilities(agent)
|
|
662
|
+
base["episode_type"] = type
|
|
663
|
+
base["tag_count"] = Array(tags).length
|
|
664
|
+
base["relationship_count"] = Array(relationships).length
|
|
665
|
+
base["entity_ids"] = Array(entity_ids)
|
|
666
|
+
base["parent_episode_id"] = parent_episode_id.to_s unless parent_episode_id.to_s.empty?
|
|
667
|
+
base["causal_from"] = causal_from.to_s unless causal_from.to_s.empty?
|
|
668
|
+
base
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
def infer_division(agent)
|
|
672
|
+
case agent
|
|
673
|
+
when Agentf::AgentRoles::PLANNER, Agentf::AgentRoles::ORCHESTRATOR, Agentf::AgentRoles::KNOWLEDGE_MANAGER
|
|
674
|
+
"strategy"
|
|
675
|
+
when Agentf::AgentRoles::ENGINEER, Agentf::AgentRoles::RESEARCHER, Agentf::AgentRoles::UI_ENGINEER
|
|
676
|
+
"engineering"
|
|
677
|
+
when Agentf::AgentRoles::QA_TESTER, Agentf::AgentRoles::REVIEWER, Agentf::AgentRoles::SECURITY_REVIEWER
|
|
678
|
+
"quality"
|
|
679
|
+
when Agentf::AgentRoles::INCIDENT_RESPONDER
|
|
680
|
+
"operations"
|
|
681
|
+
else
|
|
682
|
+
"general"
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
def infer_specialty(agent)
|
|
687
|
+
case agent
|
|
688
|
+
when Agentf::AgentRoles::PLANNER
|
|
689
|
+
"planning"
|
|
690
|
+
when Agentf::AgentRoles::ENGINEER
|
|
691
|
+
"implementation"
|
|
692
|
+
when Agentf::AgentRoles::RESEARCHER
|
|
693
|
+
"discovery"
|
|
694
|
+
when Agentf::AgentRoles::QA_TESTER
|
|
695
|
+
"testing"
|
|
696
|
+
when Agentf::AgentRoles::INCIDENT_RESPONDER
|
|
697
|
+
"debugging"
|
|
698
|
+
when Agentf::AgentRoles::UI_ENGINEER
|
|
699
|
+
"design-implementation"
|
|
700
|
+
when Agentf::AgentRoles::SECURITY_REVIEWER
|
|
701
|
+
"security"
|
|
702
|
+
when Agentf::AgentRoles::KNOWLEDGE_MANAGER
|
|
703
|
+
"documentation"
|
|
704
|
+
when Agentf::AgentRoles::REVIEWER
|
|
705
|
+
"review"
|
|
706
|
+
when Agentf::AgentRoles::ORCHESTRATOR
|
|
707
|
+
"orchestration"
|
|
708
|
+
else
|
|
709
|
+
"general"
|
|
710
|
+
end
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
def infer_capabilities(agent)
|
|
714
|
+
case agent
|
|
715
|
+
when Agentf::AgentRoles::PLANNER
|
|
716
|
+
%w[decompose prioritize plan]
|
|
717
|
+
when Agentf::AgentRoles::ENGINEER
|
|
718
|
+
%w[implement execute modify]
|
|
719
|
+
when Agentf::AgentRoles::RESEARCHER
|
|
720
|
+
%w[search map discover]
|
|
721
|
+
when Agentf::AgentRoles::QA_TESTER
|
|
722
|
+
%w[test validate report]
|
|
723
|
+
when Agentf::AgentRoles::INCIDENT_RESPONDER
|
|
724
|
+
%w[triage diagnose remediate]
|
|
725
|
+
when Agentf::AgentRoles::UI_ENGINEER
|
|
726
|
+
%w[design implement-ui validate-ui]
|
|
727
|
+
when Agentf::AgentRoles::SECURITY_REVIEWER
|
|
728
|
+
%w[scan assess harden]
|
|
729
|
+
when Agentf::AgentRoles::KNOWLEDGE_MANAGER
|
|
730
|
+
%w[summarize document synthesize]
|
|
731
|
+
when Agentf::AgentRoles::REVIEWER
|
|
732
|
+
%w[review approve reject]
|
|
733
|
+
else
|
|
734
|
+
%w[coordinate]
|
|
735
|
+
end
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
def traverse_edges(seed_ids:, relation_filters:, depth:, limit:)
|
|
739
|
+
current = Array(seed_ids).compact.map(&:to_s).reject(&:empty?).uniq
|
|
740
|
+
visited_nodes = Set.new(current)
|
|
741
|
+
visited_edges = Set.new
|
|
742
|
+
layers = []
|
|
743
|
+
edges = []
|
|
744
|
+
|
|
745
|
+
depth.to_i.times do |hop|
|
|
746
|
+
break if current.empty?
|
|
747
|
+
|
|
748
|
+
next_nodes = []
|
|
749
|
+
layer_edges = []
|
|
750
|
+
current.each do |node_id|
|
|
751
|
+
fetch_edges_for(node_id: node_id, relation_filters: relation_filters, limit: limit).each do |edge|
|
|
752
|
+
edge_id = edge["id"].to_s
|
|
753
|
+
next if edge_id.empty? || visited_edges.include?(edge_id)
|
|
754
|
+
|
|
755
|
+
visited_edges << edge_id
|
|
756
|
+
layer_edges << edge
|
|
757
|
+
target = edge["target_id"].to_s
|
|
758
|
+
next if target.empty? || visited_nodes.include?(target)
|
|
759
|
+
|
|
760
|
+
visited_nodes << target
|
|
761
|
+
next_nodes << target
|
|
762
|
+
end
|
|
763
|
+
end
|
|
764
|
+
layers << { "depth" => hop + 1, "count" => layer_edges.length }
|
|
765
|
+
edges.concat(layer_edges)
|
|
766
|
+
current = next_nodes.uniq
|
|
767
|
+
break if edges.length >= limit
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
{
|
|
771
|
+
"seed_ids" => seed_ids,
|
|
772
|
+
"nodes" => visited_nodes.to_a,
|
|
773
|
+
"edges" => edges.first(limit),
|
|
774
|
+
"layers" => layers,
|
|
775
|
+
"count" => [edges.length, limit].min
|
|
776
|
+
}
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
def fetch_edges_for(node_id:, relation_filters:, limit:)
|
|
780
|
+
if @search_supported
|
|
781
|
+
query = ["@source_id:{#{escape_tag(node_id)}}", "@project:{#{escape_tag(@project)}}"]
|
|
782
|
+
if relation_filters && relation_filters.any?
|
|
783
|
+
relations = relation_filters.map { |item| escape_tag(item.to_s) }.join("|")
|
|
784
|
+
query << "@relation:{#{relations}}"
|
|
785
|
+
end
|
|
786
|
+
search_json_index(index: EDGE_INDEX, query: query.join(" "), limit: limit)
|
|
787
|
+
else
|
|
788
|
+
fetch_edges_without_search(node_id: node_id, relation_filters: relation_filters, limit: limit)
|
|
789
|
+
end
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
def search_json_index(index:, query:, limit:)
|
|
793
|
+
results = @client.call(
|
|
794
|
+
"FT.SEARCH", index,
|
|
795
|
+
query,
|
|
796
|
+
"SORTBY", "created_at", "DESC",
|
|
797
|
+
"LIMIT", "0", limit.to_s
|
|
798
|
+
)
|
|
799
|
+
return [] unless results && results[0] > 0
|
|
800
|
+
|
|
801
|
+
records = []
|
|
802
|
+
(2...results.length).step(2) do |i|
|
|
803
|
+
item = results[i]
|
|
804
|
+
next unless item.is_a?(Array)
|
|
805
|
+
|
|
806
|
+
item.each_with_index do |part, j|
|
|
807
|
+
next unless part == "$" && j + 1 < item.length
|
|
808
|
+
|
|
809
|
+
begin
|
|
810
|
+
records << JSON.parse(item[j + 1])
|
|
811
|
+
rescue JSON::ParserError
|
|
812
|
+
nil
|
|
813
|
+
end
|
|
814
|
+
end
|
|
815
|
+
end
|
|
816
|
+
records
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
def fetch_edges_without_search(node_id:, relation_filters:, limit:)
|
|
820
|
+
edges = []
|
|
821
|
+
cursor = "0"
|
|
822
|
+
loop do
|
|
823
|
+
cursor, batch = @client.scan(cursor, match: "edge:*", count: 100)
|
|
824
|
+
batch.each do |key|
|
|
825
|
+
edge = load_episode(key)
|
|
826
|
+
next unless edge.is_a?(Hash)
|
|
827
|
+
next unless edge["source_id"].to_s == node_id.to_s
|
|
828
|
+
next unless edge["project"].to_s == @project.to_s
|
|
829
|
+
next if relation_filters && relation_filters.any? && !relation_filters.include?(edge["relation"])
|
|
830
|
+
|
|
831
|
+
edges << edge
|
|
832
|
+
return edges.first(limit) if edges.length >= limit
|
|
833
|
+
end
|
|
834
|
+
break if cursor == "0"
|
|
835
|
+
end
|
|
836
|
+
edges
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
def escape_tag(value)
|
|
840
|
+
value.to_s.gsub(/[\-{}\[\]|\\]/) { |m| "\\#{m}" }
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
def extract_metadata_slice(metadata, keys)
|
|
844
|
+
keys.each_with_object({}) do |key, acc|
|
|
845
|
+
acc[key] = metadata[key] if metadata.key?(key)
|
|
846
|
+
end
|
|
847
|
+
end
|
|
523
848
|
end
|
|
524
849
|
|
|
525
850
|
# Convenience method
|
data/lib/agentf/packs.rb
CHANGED
|
@@ -14,11 +14,11 @@ module Agentf
|
|
|
14
14
|
"description" => "Thin models/controllers with services, queries, presenters, and policy reviews.",
|
|
15
15
|
"keywords" => %w[rails activerecord rspec pundit viewcomponent hotwire turbo stimulus],
|
|
16
16
|
"workflow_templates" => {
|
|
17
|
-
"feature" => %w[
|
|
18
|
-
"bugfix" => %w[
|
|
19
|
-
"refactor" => %w[
|
|
20
|
-
"quick_fix" => %w[
|
|
21
|
-
"exploration" => %w[
|
|
17
|
+
"feature" => %w[PLANNER RESEARCHER ENGINEER QA_TESTER SECURITY_REVIEWER REVIEWER KNOWLEDGE_MANAGER],
|
|
18
|
+
"bugfix" => %w[PLANNER INCIDENT_RESPONDER ENGINEER QA_TESTER SECURITY_REVIEWER REVIEWER],
|
|
19
|
+
"refactor" => %w[PLANNER RESEARCHER ENGINEER QA_TESTER REVIEWER],
|
|
20
|
+
"quick_fix" => %w[ENGINEER QA_TESTER REVIEWER],
|
|
21
|
+
"exploration" => %w[RESEARCHER]
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"rails_37signals" => {
|
|
@@ -26,11 +26,11 @@ module Agentf
|
|
|
26
26
|
"description" => "Resource-centric workflows favoring concerns, CRUD and model-rich patterns.",
|
|
27
27
|
"keywords" => %w[rails concern crud closure model minitest hotwire],
|
|
28
28
|
"workflow_templates" => {
|
|
29
|
-
"feature" => %w[
|
|
30
|
-
"bugfix" => %w[
|
|
31
|
-
"refactor" => %w[
|
|
32
|
-
"quick_fix" => %w[
|
|
33
|
-
"exploration" => %w[
|
|
29
|
+
"feature" => %w[PLANNER RESEARCHER ENGINEER QA_TESTER REVIEWER KNOWLEDGE_MANAGER],
|
|
30
|
+
"bugfix" => %w[PLANNER INCIDENT_RESPONDER ENGINEER QA_TESTER REVIEWER],
|
|
31
|
+
"refactor" => %w[PLANNER ENGINEER QA_TESTER REVIEWER],
|
|
32
|
+
"quick_fix" => %w[ENGINEER REVIEWER],
|
|
33
|
+
"exploration" => %w[RESEARCHER]
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
36
|
"rails_feature_spec" => {
|
|
@@ -38,11 +38,11 @@ module Agentf
|
|
|
38
38
|
"description" => "Feature-spec-first orchestration with planning and review emphasis.",
|
|
39
39
|
"keywords" => %w[rails feature specification acceptance criteria],
|
|
40
40
|
"workflow_templates" => {
|
|
41
|
-
"feature" => %w[
|
|
42
|
-
"bugfix" => %w[
|
|
43
|
-
"refactor" => %w[
|
|
44
|
-
"quick_fix" => %w[
|
|
45
|
-
"exploration" => %w[
|
|
41
|
+
"feature" => %w[PLANNER RESEARCHER UI_ENGINEER ENGINEER QA_TESTER REVIEWER KNOWLEDGE_MANAGER],
|
|
42
|
+
"bugfix" => %w[PLANNER INCIDENT_RESPONDER ENGINEER QA_TESTER REVIEWER],
|
|
43
|
+
"refactor" => %w[PLANNER RESEARCHER ENGINEER QA_TESTER REVIEWER],
|
|
44
|
+
"quick_fix" => %w[ENGINEER REVIEWER],
|
|
45
|
+
"exploration" => %w[RESEARCHER]
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
}.freeze
|
|
@@ -61,18 +61,18 @@ module Agentf
|
|
|
61
61
|
return { "error" => "Agent #{agent_name} not found" } unless agent
|
|
62
62
|
|
|
63
63
|
result = case agent_name
|
|
64
|
-
when
|
|
65
|
-
|
|
66
|
-
when
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
when
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
64
|
+
when Agentf::AgentRoles::PLANNER
|
|
65
|
+
agent.plan_task(task)
|
|
66
|
+
when Agentf::AgentRoles::RESEARCHER
|
|
67
|
+
query = context["explore_query"] || "*.rb"
|
|
68
|
+
files = commands.fetch("explorer").glob(query)
|
|
69
|
+
response = agent.explore(query)
|
|
70
|
+
response["files"] = files
|
|
71
|
+
response
|
|
72
|
+
when Agentf::AgentRoles::QA_TESTER
|
|
73
|
+
source_file = context["source_file"] || "app/models/application_record.rb"
|
|
74
|
+
tester_commands = commands.fetch("tester")
|
|
75
|
+
tdd_phase = context["tdd_phase"] || "normal"
|
|
76
76
|
|
|
77
77
|
if tdd_phase == "red"
|
|
78
78
|
failure_signature = "expected-failure:#{File.basename(source_file)}:#{Time.now.to_i}"
|
|
@@ -92,32 +92,32 @@ module Agentf
|
|
|
92
92
|
response["failure_signature"] = context["tdd_failure_signature"]
|
|
93
93
|
response
|
|
94
94
|
end
|
|
95
|
-
when
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
when Agentf::AgentRoles::INCIDENT_RESPONDER
|
|
96
|
+
error = context["error"] || "No error provided"
|
|
97
|
+
analysis = commands.fetch("debugger").parse_error(error)
|
|
98
|
+
response = agent.diagnose(error, context: context["error_context"])
|
|
99
99
|
response["analysis"] = {
|
|
100
100
|
"error_type" => analysis.error_type,
|
|
101
101
|
"root_cause" => analysis.possible_causes,
|
|
102
102
|
"suggested_fix" => analysis.suggested_fix
|
|
103
103
|
}
|
|
104
104
|
response
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
105
|
+
when Agentf::AgentRoles::UI_ENGINEER
|
|
106
|
+
design_spec = context["design_spec"] || "Create a card component"
|
|
107
|
+
spec = commands.fetch("designer").generate_component("GeneratedComponent", design_spec)
|
|
108
|
+
response = agent.implement_design(design_spec)
|
|
109
|
+
response["generated_code"] = spec.code
|
|
110
|
+
response
|
|
111
|
+
when Agentf::AgentRoles::ENGINEER
|
|
112
|
+
subtask = context["current_subtask"] || { "description" => task }
|
|
113
|
+
agent.execute(subtask)
|
|
114
|
+
when Agentf::AgentRoles::SECURITY_REVIEWER
|
|
115
|
+
agent.assess(task: task, context: context)
|
|
116
|
+
when Agentf::AgentRoles::REVIEWER
|
|
117
|
+
last_result = context["execution"] || {}
|
|
118
|
+
agent.review(last_result)
|
|
119
|
+
when Agentf::AgentRoles::KNOWLEDGE_MANAGER
|
|
120
|
+
agent.sync_docs("project")
|
|
121
121
|
else
|
|
122
122
|
{ "status" => "not_implemented" }
|
|
123
123
|
end
|
|
@@ -133,11 +133,11 @@ module Agentf
|
|
|
133
133
|
class OpenCode < Base
|
|
134
134
|
def workflow_templates
|
|
135
135
|
{
|
|
136
|
-
"feature" => %w[
|
|
137
|
-
"bugfix" => %w[
|
|
138
|
-
"quick_fix" => %w[
|
|
139
|
-
"exploration" => %w[
|
|
140
|
-
"refactor" => %w[
|
|
136
|
+
"feature" => %w[PLANNER RESEARCHER UI_ENGINEER ENGINEER QA_TESTER SECURITY_REVIEWER REVIEWER KNOWLEDGE_MANAGER],
|
|
137
|
+
"bugfix" => %w[PLANNER INCIDENT_RESPONDER ENGINEER QA_TESTER SECURITY_REVIEWER REVIEWER],
|
|
138
|
+
"quick_fix" => %w[ENGINEER SECURITY_REVIEWER REVIEWER],
|
|
139
|
+
"exploration" => %w[RESEARCHER],
|
|
140
|
+
"refactor" => %w[PLANNER RESEARCHER ENGINEER QA_TESTER SECURITY_REVIEWER REVIEWER]
|
|
141
141
|
}
|
|
142
142
|
end
|
|
143
143
|
end
|
|
@@ -145,11 +145,11 @@ module Agentf
|
|
|
145
145
|
class Copilot < Base
|
|
146
146
|
def workflow_templates
|
|
147
147
|
{
|
|
148
|
-
"feature" => %w[
|
|
149
|
-
"bugfix" => %w[
|
|
150
|
-
"quick_fix" => %w[
|
|
151
|
-
"exploration" => %w[
|
|
152
|
-
"refactor" => %w[
|
|
148
|
+
"feature" => %w[PLANNER ENGINEER QA_TESTER SECURITY_REVIEWER REVIEWER KNOWLEDGE_MANAGER],
|
|
149
|
+
"bugfix" => %w[INCIDENT_RESPONDER ENGINEER QA_TESTER SECURITY_REVIEWER REVIEWER],
|
|
150
|
+
"quick_fix" => %w[ENGINEER REVIEWER],
|
|
151
|
+
"exploration" => %w[RESEARCHER],
|
|
152
|
+
"refactor" => %w[PLANNER ENGINEER QA_TESTER REVIEWER]
|
|
153
153
|
}
|
|
154
154
|
end
|
|
155
155
|
end
|
data/lib/agentf/version.rb
CHANGED