agentf 0.4.0 → 0.4.3
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_execution_contract.rb +116 -0
- data/lib/agentf/agent_policy.rb +33 -10
- data/lib/agentf/agents/base.rb +25 -0
- data/lib/agentf/agents/debugger.rb +30 -27
- data/lib/agentf/agents/designer.rb +25 -22
- data/lib/agentf/agents/reviewer.rb +18 -16
- data/lib/agentf/agents/security.rb +26 -24
- data/lib/agentf/agents/specialist.rb +30 -23
- data/lib/agentf/cli/memory.rb +104 -0
- data/lib/agentf/cli/router.rb +2 -0
- data/lib/agentf/installer.rb +99 -14
- data/lib/agentf/memory.rb +190 -0
- data/lib/agentf/service/providers.rb +2 -0
- data/lib/agentf/version.rb +1 -1
- data/lib/agentf/workflow_engine.rb +6 -1
- data/lib/agentf.rb +10 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3518ac253ce03fcc05439cf5b749bd91e07aea398b15b7870072b3bfe58b0d24
|
|
4
|
+
data.tar.gz: f95d3efeee0551c59ed2e8d575a4598ac889872f37bf73987c710064d0ff5ee8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 04703b74db04fac1840b7307100187b40bc33c3873d36ca2b34a92ee389551aa6292702bd494d9d62f4ceb27767e0873a3e8c56e40e75dd5f004cff2cf7d0bfb
|
|
7
|
+
data.tar.gz: a4bca497545c5ec43c63b192a2687378328cbd1aa940c99b761d2f30286cb5be2f0e1a4a98e6c1949b0e8ec5960c66fe84c15fa56dcfaa52c12d59243a5e147a
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Agentf
|
|
4
|
+
class AgentContractViolation < Agentf::Error
|
|
5
|
+
attr_reader :agent_name, :violations
|
|
6
|
+
|
|
7
|
+
def initialize(agent_name:, violations:)
|
|
8
|
+
@agent_name = agent_name
|
|
9
|
+
@violations = violations
|
|
10
|
+
super("Agent contract violated for #{agent_name}: #{violations.map { |v| v['code'] }.join(', ')}")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class AgentExecutionContract
|
|
15
|
+
def initialize(enabled:, mode:, policy: Agentf::AgentPolicy.new)
|
|
16
|
+
@enabled = enabled
|
|
17
|
+
@mode = normalize_mode(mode)
|
|
18
|
+
@policy = policy
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def enabled?
|
|
22
|
+
@enabled && @mode != "off"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def enforcing?
|
|
26
|
+
enabled? && @mode == "enforcing"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def before!(agent_name:, boundaries:, context: {})
|
|
30
|
+
violations = @policy.validate(
|
|
31
|
+
agent_name: agent_name,
|
|
32
|
+
boundaries: boundaries,
|
|
33
|
+
context: context,
|
|
34
|
+
result: nil,
|
|
35
|
+
phase: :before
|
|
36
|
+
)
|
|
37
|
+
handle!(agent_name: agent_name, violations: violations)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def after!(agent_name:, boundaries:, context: {}, result:)
|
|
41
|
+
violations = @policy.validate(
|
|
42
|
+
agent_name: agent_name,
|
|
43
|
+
boundaries: boundaries,
|
|
44
|
+
context: context,
|
|
45
|
+
result: result,
|
|
46
|
+
phase: :after
|
|
47
|
+
) + coding_execution_violations(agent_name: agent_name, result: result) + tdd_violations(agent_name: agent_name, context: context)
|
|
48
|
+
handle!(agent_name: agent_name, violations: violations)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def normalize_mode(value)
|
|
54
|
+
mode = value.to_s.strip.downcase
|
|
55
|
+
return mode if %w[advisory enforcing off].include?(mode)
|
|
56
|
+
|
|
57
|
+
"enforcing"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def handle!(agent_name:, violations:)
|
|
61
|
+
return if violations.empty? || !enabled?
|
|
62
|
+
|
|
63
|
+
raise Agentf::AgentContractViolation.new(agent_name: agent_name, violations: violations) if enforcing?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def tdd_violations(agent_name:, context:)
|
|
67
|
+
return [] unless [Agentf::AgentRoles::ENGINEER, Agentf::AgentRoles::UI_ENGINEER].include?(agent_name)
|
|
68
|
+
|
|
69
|
+
tdd_required = context["tdd_required"] == true || context.key?("tdd_phase")
|
|
70
|
+
return [] unless tdd_required
|
|
71
|
+
|
|
72
|
+
phase = context["tdd_phase"].to_s.strip
|
|
73
|
+
violations = []
|
|
74
|
+
if phase.empty?
|
|
75
|
+
violations << {
|
|
76
|
+
"code" => "missing_tdd_phase",
|
|
77
|
+
"severity" => "error",
|
|
78
|
+
"message" => "#{agent_name} requires explicit tdd_phase when TDD is enabled",
|
|
79
|
+
"agent" => agent_name,
|
|
80
|
+
"type" => "agent_contract"
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if phase == "green" && context["expected_test_fix"].to_s.strip.empty?
|
|
85
|
+
violations << {
|
|
86
|
+
"code" => "missing_expected_test_fix",
|
|
87
|
+
"severity" => "error",
|
|
88
|
+
"message" => "#{agent_name} green phase requires expected_test_fix from failing test",
|
|
89
|
+
"agent" => agent_name,
|
|
90
|
+
"type" => "agent_contract"
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
violations
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def coding_execution_violations(agent_name:, result:)
|
|
98
|
+
coding_agents = [Agentf::AgentRoles::ENGINEER, Agentf::AgentRoles::UI_ENGINEER, Agentf::AgentRoles::INCIDENT_RESPONDER]
|
|
99
|
+
return [] unless coding_agents.include?(agent_name)
|
|
100
|
+
|
|
101
|
+
unless result.is_a?(Hash) && [true, false].include?(result["success"])
|
|
102
|
+
return [
|
|
103
|
+
{
|
|
104
|
+
"code" => "invalid_success_flag",
|
|
105
|
+
"severity" => "error",
|
|
106
|
+
"message" => "#{agent_name} must return boolean success",
|
|
107
|
+
"agent" => agent_name,
|
|
108
|
+
"type" => "agent_contract"
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
[]
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/agentf/agent_policy.rb
CHANGED
|
@@ -4,19 +4,34 @@ module Agentf
|
|
|
4
4
|
class AgentPolicy
|
|
5
5
|
REQUIRED_KEYS = %w[always ask_first never].freeze
|
|
6
6
|
|
|
7
|
-
def validate(agent_name:, boundaries:, context: {}, result: nil)
|
|
7
|
+
def validate(agent_name:, boundaries:, context: {}, result: nil, phase: :any)
|
|
8
8
|
errors = []
|
|
9
9
|
boundaries = normalize(boundaries)
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
if %i[any before].include?(phase)
|
|
12
|
+
required_inputs = Array(boundaries["required_inputs"])
|
|
13
|
+
missing_inputs = required_inputs.reject { |key| context.key?(key) }
|
|
14
|
+
unless missing_inputs.empty?
|
|
15
|
+
errors << violation(
|
|
16
|
+
code: "missing_required_inputs",
|
|
17
|
+
severity: "error",
|
|
18
|
+
message: "#{agent_name} missing required inputs: #{missing_inputs.join(', ')}",
|
|
19
|
+
agent: agent_name
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if %i[any after].include?(phase)
|
|
25
|
+
required_outputs = Array(boundaries["required_outputs"])
|
|
26
|
+
missing_outputs = required_outputs.reject { |key| output_present?(result, key) }
|
|
27
|
+
unless missing_outputs.empty?
|
|
28
|
+
errors << violation(
|
|
29
|
+
code: "missing_required_outputs",
|
|
30
|
+
severity: "error",
|
|
31
|
+
message: "#{agent_name} missing required outputs: #{missing_outputs.join(', ')}",
|
|
32
|
+
agent: agent_name
|
|
33
|
+
)
|
|
34
|
+
end
|
|
20
35
|
end
|
|
21
36
|
|
|
22
37
|
if result.is_a?(Hash) && result["error"]
|
|
@@ -41,6 +56,14 @@ module Agentf
|
|
|
41
56
|
|
|
42
57
|
private
|
|
43
58
|
|
|
59
|
+
def output_present?(result, key)
|
|
60
|
+
return false unless result.is_a?(Hash)
|
|
61
|
+
return false unless result.key?(key)
|
|
62
|
+
|
|
63
|
+
value = result[key]
|
|
64
|
+
!value.nil?
|
|
65
|
+
end
|
|
66
|
+
|
|
44
67
|
def violation(code:, severity:, message:, agent:)
|
|
45
68
|
{
|
|
46
69
|
"code" => code,
|
data/lib/agentf/agents/base.rb
CHANGED
|
@@ -55,11 +55,36 @@ module Agentf
|
|
|
55
55
|
def initialize(memory)
|
|
56
56
|
@memory = memory
|
|
57
57
|
@name = self.class.typed_name
|
|
58
|
+
@execution_contract = Agentf::AgentExecutionContract.new(
|
|
59
|
+
enabled: Agentf.config.agent_contract_enabled,
|
|
60
|
+
mode: Agentf.config.agent_contract_mode
|
|
61
|
+
)
|
|
58
62
|
end
|
|
59
63
|
|
|
60
64
|
def log(message)
|
|
61
65
|
puts "\n[#{@name}] #{message}"
|
|
62
66
|
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def execute_with_contract(context: {})
|
|
71
|
+
@execution_contract.before!(
|
|
72
|
+
agent_name: name,
|
|
73
|
+
boundaries: self.class.policy_boundaries,
|
|
74
|
+
context: context
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
result = yield
|
|
78
|
+
|
|
79
|
+
@execution_contract.after!(
|
|
80
|
+
agent_name: name,
|
|
81
|
+
boundaries: self.class.policy_boundaries,
|
|
82
|
+
context: context,
|
|
83
|
+
result: result
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
result
|
|
87
|
+
end
|
|
63
88
|
end
|
|
64
89
|
end
|
|
65
90
|
end
|
|
@@ -48,8 +48,8 @@ module Agentf
|
|
|
48
48
|
"always" => ["Return analysis with root causes and suggested fix", "Persist debugging lesson"],
|
|
49
49
|
"ask_first" => ["Applying speculative fixes without reproducible error"],
|
|
50
50
|
"never" => ["Discard stack trace context when available"],
|
|
51
|
-
"required_inputs" => [],
|
|
52
|
-
"required_outputs" => ["analysis"]
|
|
51
|
+
"required_inputs" => ["error_text"],
|
|
52
|
+
"required_outputs" => ["analysis", "success"]
|
|
53
53
|
}
|
|
54
54
|
end
|
|
55
55
|
|
|
@@ -59,32 +59,35 @@ module Agentf
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def diagnose(error, context: nil)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
|
|
85
|
-
|
|
62
|
+
payload = { "error_text" => error, "context" => context }
|
|
63
|
+
execute_with_contract(context: payload) do
|
|
64
|
+
log "Diagnosing error"
|
|
65
|
+
log " Error: #{error[0..100]}..."
|
|
66
|
+
|
|
67
|
+
analysis = @commands.parse_error(error)
|
|
68
|
+
|
|
69
|
+
memory.store_episode(
|
|
70
|
+
type: "lesson",
|
|
71
|
+
title: "Debugged: #{error[0..50]}...",
|
|
72
|
+
description: "Root cause: #{analysis.possible_causes.first}. Fix: #{analysis.suggested_fix}",
|
|
73
|
+
context: context.to_s,
|
|
74
|
+
tags: ["debugging", "error", "fix"],
|
|
75
|
+
agent: name
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
log "Root cause: #{analysis.possible_causes.first}"
|
|
79
|
+
log "Suggested fix: #{analysis.suggested_fix}"
|
|
80
|
+
|
|
81
|
+
{
|
|
82
|
+
"success" => true,
|
|
83
|
+
"analysis" => {
|
|
84
|
+
"error_type" => analysis.error_type,
|
|
85
|
+
"possible_causes" => analysis.possible_causes,
|
|
86
|
+
"suggested_fix" => analysis.suggested_fix,
|
|
87
|
+
"stack_trace" => analysis.stack_trace
|
|
88
|
+
}
|
|
86
89
|
}
|
|
87
|
-
|
|
90
|
+
end
|
|
88
91
|
end
|
|
89
92
|
end
|
|
90
93
|
end
|
|
@@ -48,8 +48,8 @@ module Agentf
|
|
|
48
48
|
"always" => ["Return generated component details", "Persist successful implementation pattern"],
|
|
49
49
|
"ask_first" => ["Changing primary UI framework"],
|
|
50
50
|
"never" => ["Return empty generated code for successful design task"],
|
|
51
|
-
"required_inputs" => [],
|
|
52
|
-
"required_outputs" => ["component", "generated_code"]
|
|
51
|
+
"required_inputs" => ["design_spec"],
|
|
52
|
+
"required_outputs" => ["component", "generated_code", "success"]
|
|
53
53
|
}
|
|
54
54
|
end
|
|
55
55
|
|
|
@@ -59,26 +59,29 @@ module Agentf
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def implement_design(design_spec, framework: "react")
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
62
|
+
execute_with_contract(context: { "design_spec" => design_spec, "framework" => framework }) do
|
|
63
|
+
log "Implementing design: #{design_spec}"
|
|
64
|
+
|
|
65
|
+
spec = @commands.generate_component("GeneratedComponent", design_spec)
|
|
66
|
+
|
|
67
|
+
memory.store_success(
|
|
68
|
+
title: "Implemented design: #{design_spec}",
|
|
69
|
+
description: "Created #{spec.name} in #{spec.framework}",
|
|
70
|
+
context: "Framework: #{framework}",
|
|
71
|
+
tags: ["design", "ui", framework],
|
|
72
|
+
agent: name
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
log "Created component: #{spec.name}"
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
"design_spec" => design_spec,
|
|
79
|
+
"component" => spec.name,
|
|
80
|
+
"framework" => framework,
|
|
81
|
+
"generated_code" => spec.code,
|
|
82
|
+
"success" => true
|
|
83
|
+
}
|
|
84
|
+
end
|
|
82
85
|
end
|
|
83
86
|
end
|
|
84
87
|
end
|
|
@@ -47,33 +47,35 @@ module Agentf
|
|
|
47
47
|
"always" => ["Report approval decision", "Highlight known pitfalls in review findings"],
|
|
48
48
|
"ask_first" => ["Approving with unresolved critical security issues"],
|
|
49
49
|
"never" => ["Approve without any review evidence"],
|
|
50
|
-
"required_inputs" => [],
|
|
50
|
+
"required_inputs" => ["execution"],
|
|
51
51
|
"required_outputs" => ["approved", "issues"]
|
|
52
52
|
}
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def review(subtask_result)
|
|
56
|
-
|
|
56
|
+
execute_with_contract(context: { "execution" => subtask_result }) do
|
|
57
|
+
log "Reviewing subtask #{subtask_result['subtask_id']}"
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
pitfalls = memory.get_pitfalls(limit: 5)
|
|
60
|
+
memories = memory.get_recent_memories(limit: 5)
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
issues = []
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
pitfalls.each do |pitfall|
|
|
65
|
+
issues << "Warning: Known pitfall - #{pitfall['title']}" if pitfall["type"] == "pitfall"
|
|
66
|
+
end
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
approved = issues.empty?
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
if approved
|
|
71
|
+
log "Approved (no issues found)"
|
|
72
|
+
else
|
|
73
|
+
log "Issues found: #{issues.size}"
|
|
74
|
+
issues.each { |issue| log " - #{issue}" }
|
|
75
|
+
end
|
|
75
76
|
|
|
76
|
-
|
|
77
|
+
{ "approved" => approved, "issues" => issues }
|
|
78
|
+
end
|
|
77
79
|
end
|
|
78
80
|
end
|
|
79
81
|
end
|
|
@@ -48,7 +48,7 @@ module Agentf
|
|
|
48
48
|
"always" => ["Return issue list and best practices", "Persist outcome as success or pitfall"],
|
|
49
49
|
"ask_first" => ["Allowing known secret patterns in context"],
|
|
50
50
|
"never" => ["Echo raw secrets in output"],
|
|
51
|
-
"required_inputs" => [],
|
|
51
|
+
"required_inputs" => ["task"],
|
|
52
52
|
"required_outputs" => ["issues", "best_practices"]
|
|
53
53
|
}
|
|
54
54
|
end
|
|
@@ -59,30 +59,32 @@ module Agentf
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def assess(task:, context: {})
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
62
|
+
execute_with_contract(context: context.merge("task" => task)) do
|
|
63
|
+
log "Running security assessment"
|
|
64
|
+
|
|
65
|
+
findings = @commands.scan(task: task, context: context)
|
|
66
|
+
summary = summarize_findings(findings)
|
|
67
|
+
|
|
68
|
+
if findings["issues"].empty?
|
|
69
|
+
memory.store_success(
|
|
70
|
+
title: "Security review passed",
|
|
71
|
+
description: summary,
|
|
72
|
+
context: task,
|
|
73
|
+
tags: ["security", "pass"],
|
|
74
|
+
agent: name
|
|
75
|
+
)
|
|
76
|
+
else
|
|
77
|
+
memory.store_pitfall(
|
|
78
|
+
title: "Security findings detected",
|
|
79
|
+
description: summary,
|
|
80
|
+
context: task,
|
|
81
|
+
tags: ["security", "warning"],
|
|
82
|
+
agent: name
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
findings.merge("best_practices" => @commands.best_practices)
|
|
83
87
|
end
|
|
84
|
-
|
|
85
|
-
findings.merge("best_practices" => @commands.best_practices)
|
|
86
88
|
end
|
|
87
89
|
|
|
88
90
|
private
|
|
@@ -47,37 +47,44 @@ module Agentf
|
|
|
47
47
|
"always" => ["Persist execution outcome", "Return deterministic success boolean"],
|
|
48
48
|
"ask_first" => ["Applying architecture style changes across unrelated modules"],
|
|
49
49
|
"never" => ["Claim implementation complete without execution result"],
|
|
50
|
-
"required_inputs" => [],
|
|
50
|
+
"required_inputs" => ["description"],
|
|
51
51
|
"required_outputs" => ["subtask_id", "success"]
|
|
52
52
|
}
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def execute(subtask)
|
|
56
|
-
|
|
56
|
+
normalized_subtask = subtask.merge(
|
|
57
|
+
"id" => subtask["id"] || "ad-hoc",
|
|
58
|
+
"description" => subtask["description"] || "Execute implementation step"
|
|
59
|
+
)
|
|
57
60
|
|
|
58
|
-
|
|
61
|
+
execute_with_contract(context: normalized_subtask) do
|
|
62
|
+
log "Executing: #{normalized_subtask['description']}"
|
|
59
63
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
64
|
+
success = normalized_subtask.fetch("success", true)
|
|
65
|
+
|
|
66
|
+
if success
|
|
67
|
+
memory.store_success(
|
|
68
|
+
title: "Completed: #{normalized_subtask['description']}",
|
|
69
|
+
description: "Successfully executed subtask #{normalized_subtask['id']}",
|
|
70
|
+
context: "Working on #{normalized_subtask.fetch('task', 'unknown task')}",
|
|
71
|
+
tags: ["implementation", normalized_subtask.fetch("language", "general")],
|
|
72
|
+
agent: name
|
|
73
|
+
)
|
|
74
|
+
log "Stored success memory"
|
|
75
|
+
else
|
|
76
|
+
memory.store_pitfall(
|
|
77
|
+
title: "Failed: #{normalized_subtask['description']}",
|
|
78
|
+
description: "Subtask #{normalized_subtask['id']} failed",
|
|
79
|
+
context: "Working on #{normalized_subtask.fetch('task', 'unknown task')}",
|
|
80
|
+
tags: ["failure", "implementation"],
|
|
81
|
+
agent: name
|
|
82
|
+
)
|
|
83
|
+
log "Stored pitfall memory"
|
|
84
|
+
end
|
|
79
85
|
|
|
80
|
-
|
|
86
|
+
{ "subtask_id" => normalized_subtask["id"], "success" => success, "result" => "Code executed" }
|
|
87
|
+
end
|
|
81
88
|
end
|
|
82
89
|
end
|
|
83
90
|
end
|
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/cli/router.rb
CHANGED
|
@@ -85,6 +85,8 @@ module Agentf
|
|
|
85
85
|
AGENTF_METRICS_ENABLED=true|false Enable/disable workflow metrics capture and CLI
|
|
86
86
|
AGENTF_WORKFLOW_CONTRACT_ENABLED=true|false Enable/disable workflow contract checks
|
|
87
87
|
AGENTF_WORKFLOW_CONTRACT_MODE=advisory|enforcing|off Contract behavior mode
|
|
88
|
+
AGENTF_AGENT_CONTRACT_ENABLED=true|false Enable/disable per-agent contract checks
|
|
89
|
+
AGENTF_AGENT_CONTRACT_MODE=advisory|enforcing|off Per-agent contract behavior mode
|
|
88
90
|
AGENTF_DEFAULT_PACK=generic|rails_standard|rails_37signals|rails_feature_spec
|
|
89
91
|
AGENTF_GEM_PATH=/path/to/gem Path to agentf gem (for OpenCode plugin binary resolution)
|
|
90
92
|
|
data/lib/agentf/installer.rb
CHANGED
|
@@ -362,44 +362,127 @@ module Agentf
|
|
|
362
362
|
|
|
363
363
|
const execFileAsync = promisify(execFile);
|
|
364
364
|
|
|
365
|
-
|
|
365
|
+
type AgentfBinaryResolution = {
|
|
366
|
+
binaryPath: string;
|
|
367
|
+
source: string;
|
|
368
|
+
attempts: string[];
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
type PreflightCache = {
|
|
372
|
+
workspaceRoot: string;
|
|
373
|
+
binaryPath: string;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
let preflightCache: PreflightCache | null = null;
|
|
377
|
+
|
|
378
|
+
function buildPreflightError(attempts: string[], extraDetails?: string): Error {
|
|
379
|
+
const lines = [
|
|
380
|
+
"Agentf plugin preflight failed: unable to run a compatible agentf binary.",
|
|
381
|
+
"",
|
|
382
|
+
"Resolution attempts:",
|
|
383
|
+
...attempts.map((attempt) => `- ${attempt}`),
|
|
384
|
+
"",
|
|
385
|
+
"Remediation:",
|
|
386
|
+
"- Set AGENTF_GEM_PATH to your installed agentf gem path (contains bin/agentf).",
|
|
387
|
+
"- Ensure your Ruby version manager shims are on PATH (rbenv/asdf/mise), then retry.",
|
|
388
|
+
"- Verify with: agentf version",
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
if (extraDetails) {
|
|
392
|
+
lines.push("", "Details:", extraDetails);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return new Error(lines.join("\n"));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function formatExecFailure(error: unknown): string {
|
|
399
|
+
const failure = error as {
|
|
400
|
+
message?: string;
|
|
401
|
+
stdout?: Buffer | string;
|
|
402
|
+
stderr?: Buffer | string;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const stdout = failure.stdout?.toString().trim();
|
|
406
|
+
const stderr = failure.stderr?.toString().trim();
|
|
407
|
+
const message = failure.message?.trim();
|
|
408
|
+
const parts = [
|
|
409
|
+
message ? `message: ${message}` : null,
|
|
410
|
+
stderr ? `stderr: ${stderr}` : null,
|
|
411
|
+
stdout ? `stdout: ${stdout}` : null,
|
|
412
|
+
].filter(Boolean);
|
|
413
|
+
|
|
414
|
+
return parts.length > 0 ? parts.join("\n") : "No additional process output.";
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function resolveAgentfBinary(directory: string): Promise<AgentfBinaryResolution> {
|
|
418
|
+
const attempts: string[] = [];
|
|
366
419
|
const gemPath = process.env.AGENTF_GEM_PATH;
|
|
367
420
|
if (gemPath) {
|
|
368
421
|
const binaryPath = path.join(gemPath, "bin", "agentf");
|
|
369
422
|
if (fs.existsSync(binaryPath)) {
|
|
370
|
-
|
|
423
|
+
attempts.push(`AGENTF_GEM_PATH succeeded: ${binaryPath}`);
|
|
424
|
+
return { binaryPath, source: "AGENTF_GEM_PATH", attempts };
|
|
371
425
|
}
|
|
426
|
+
attempts.push(`AGENTF_GEM_PATH set but missing executable: ${binaryPath}`);
|
|
427
|
+
} else {
|
|
428
|
+
attempts.push("AGENTF_GEM_PATH is not set");
|
|
372
429
|
}
|
|
373
430
|
|
|
374
431
|
const projectRoot = path.resolve(directory);
|
|
375
432
|
const projectBinary = path.join(projectRoot, "bin", "agentf");
|
|
376
433
|
if (fs.existsSync(projectBinary)) {
|
|
377
|
-
|
|
434
|
+
attempts.push(`Project bin fallback succeeded: ${projectBinary}`);
|
|
435
|
+
return { binaryPath: projectBinary, source: "project-bin", attempts };
|
|
378
436
|
}
|
|
437
|
+
attempts.push(`Project bin fallback missing: ${projectBinary}`);
|
|
379
438
|
|
|
380
439
|
try {
|
|
381
440
|
const { stdout } = await execFileAsync("command", ["-v", "agentf"], { shell: true });
|
|
382
441
|
const whichPath = stdout.toString().trim();
|
|
383
442
|
if (whichPath && fs.existsSync(whichPath)) {
|
|
384
|
-
|
|
443
|
+
attempts.push(`PATH fallback succeeded: ${whichPath}`);
|
|
444
|
+
return { binaryPath: whichPath, source: "PATH", attempts };
|
|
385
445
|
}
|
|
446
|
+
attempts.push("PATH fallback returned empty or non-existent path");
|
|
386
447
|
} catch {
|
|
387
|
-
|
|
448
|
+
attempts.push("PATH fallback failed: command -v agentf did not resolve");
|
|
388
449
|
}
|
|
389
450
|
|
|
390
|
-
throw
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
);
|
|
451
|
+
throw buildPreflightError(attempts);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function ensureAgentfPreflight(directory: string): Promise<string> {
|
|
455
|
+
const workspaceRoot = path.resolve(directory);
|
|
456
|
+
if (preflightCache && preflightCache.workspaceRoot === workspaceRoot) {
|
|
457
|
+
return preflightCache.binaryPath;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const resolution = await resolveAgentfBinary(workspaceRoot);
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
await execFileAsync(resolution.binaryPath, ["version"], {
|
|
464
|
+
cwd: workspaceRoot,
|
|
465
|
+
env: process.env,
|
|
466
|
+
maxBuffer: 1024 * 1024,
|
|
467
|
+
});
|
|
468
|
+
} catch (error) {
|
|
469
|
+
throw buildPreflightError(
|
|
470
|
+
resolution.attempts,
|
|
471
|
+
[`Resolved via ${resolution.source}: ${resolution.binaryPath}`, formatExecFailure(error)].join("\n")
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
preflightCache = { workspaceRoot, binaryPath: resolution.binaryPath };
|
|
476
|
+
return resolution.binaryPath;
|
|
395
477
|
}
|
|
396
478
|
|
|
397
479
|
async function runAgentfCli(directory: string, subcommand: string, command: string, args: string[]) {
|
|
398
|
-
const
|
|
399
|
-
const
|
|
480
|
+
const workspaceRoot = path.resolve(directory);
|
|
481
|
+
const binaryPath = await ensureAgentfPreflight(workspaceRoot);
|
|
482
|
+
const commandArgs = [subcommand, command, ...args, "--json"];
|
|
400
483
|
|
|
401
|
-
const { stdout } = await execFileAsync(
|
|
402
|
-
cwd:
|
|
484
|
+
const { stdout } = await execFileAsync(binaryPath, commandArgs, {
|
|
485
|
+
cwd: workspaceRoot,
|
|
403
486
|
env: process.env,
|
|
404
487
|
maxBuffer: 1024 * 1024 * 5,
|
|
405
488
|
});
|
|
@@ -409,6 +492,8 @@ module Agentf
|
|
|
409
492
|
}
|
|
410
493
|
|
|
411
494
|
export const agentfPlugin: Plugin = async () => {
|
|
495
|
+
await ensureAgentfPreflight(process.env.PWD || process.cwd());
|
|
496
|
+
|
|
412
497
|
return {
|
|
413
498
|
tools: {
|
|
414
499
|
"agentf-code-glob": tool({
|
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)
|
|
@@ -125,6 +125,8 @@ module Agentf
|
|
|
125
125
|
logger&.call("→ #{agent_name} Complete")
|
|
126
126
|
result
|
|
127
127
|
rescue StandardError => e
|
|
128
|
+
raise if e.is_a?(Agentf::AgentContractViolation)
|
|
129
|
+
|
|
128
130
|
logger&.call("→ #{agent_name} Error: #{e.message}")
|
|
129
131
|
{ "error" => e.message, "agent" => agent_name }
|
|
130
132
|
end
|
data/lib/agentf/version.rb
CHANGED
|
@@ -166,6 +166,10 @@ module Agentf
|
|
|
166
166
|
enriched_context["expected_test_fix"] = @workflow_state.dig("tdd", "failure_signature")
|
|
167
167
|
end
|
|
168
168
|
|
|
169
|
+
if agent_name == Agentf::AgentRoles::REVIEWER
|
|
170
|
+
enriched_context["execution"] = @workflow_state["results"].last&.fetch("result", {}) || {}
|
|
171
|
+
end
|
|
172
|
+
|
|
169
173
|
result = @provider.execute_agent(
|
|
170
174
|
agent_name: agent_name,
|
|
171
175
|
task: @workflow_state["task"],
|
|
@@ -179,7 +183,8 @@ module Agentf
|
|
|
179
183
|
agent_name: agent_name,
|
|
180
184
|
boundaries: @agents.fetch(agent_name).class.policy_boundaries,
|
|
181
185
|
context: enriched_context,
|
|
182
|
-
result: result
|
|
186
|
+
result: result,
|
|
187
|
+
phase: :after
|
|
183
188
|
)
|
|
184
189
|
append_policy_violations(policy_violations)
|
|
185
190
|
|
data/lib/agentf.rb
CHANGED
|
@@ -15,7 +15,8 @@ module Agentf
|
|
|
15
15
|
class Config
|
|
16
16
|
attr_reader :redis_url
|
|
17
17
|
attr_accessor :project_name, :base_path, :metrics_enabled, :workflow_contract_enabled,
|
|
18
|
-
:workflow_contract_mode, :
|
|
18
|
+
:workflow_contract_mode, :agent_contract_enabled, :agent_contract_mode,
|
|
19
|
+
:default_pack, :gem_path
|
|
19
20
|
|
|
20
21
|
def initialize
|
|
21
22
|
@redis_url = normalize_redis_url(ENV.fetch("REDIS_URL", "redis://localhost:6379"))
|
|
@@ -29,6 +30,13 @@ module Agentf
|
|
|
29
30
|
@workflow_contract_mode = normalize_contract_mode(
|
|
30
31
|
ENV.fetch("AGENTF_WORKFLOW_CONTRACT_MODE", "advisory")
|
|
31
32
|
)
|
|
33
|
+
@agent_contract_enabled = parse_boolean(
|
|
34
|
+
ENV.fetch("AGENTF_AGENT_CONTRACT_ENABLED", "true"),
|
|
35
|
+
default: true
|
|
36
|
+
)
|
|
37
|
+
@agent_contract_mode = normalize_contract_mode(
|
|
38
|
+
ENV.fetch("AGENTF_AGENT_CONTRACT_MODE", "enforcing")
|
|
39
|
+
)
|
|
32
40
|
@default_pack = ENV.fetch("AGENTF_DEFAULT_PACK", "generic").to_s.strip.downcase
|
|
33
41
|
@gem_path = ENV.fetch("AGENTF_GEM_PATH", nil)
|
|
34
42
|
end
|
|
@@ -81,6 +89,7 @@ require_relative "agentf/service/providers"
|
|
|
81
89
|
require_relative "agentf/context_builder"
|
|
82
90
|
require_relative "agentf/packs"
|
|
83
91
|
require_relative "agentf/agent_policy"
|
|
92
|
+
require_relative "agentf/agent_execution_contract"
|
|
84
93
|
require_relative "agentf/workflow_contract"
|
|
85
94
|
require_relative "agentf/workflow_engine"
|
|
86
95
|
require_relative "agentf/installer"
|
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.
|
|
4
|
+
version: 0.4.3
|
|
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-
|
|
11
|
+
date: 2026-03-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: redis
|
|
@@ -94,6 +94,7 @@ extra_rdoc_files: []
|
|
|
94
94
|
files:
|
|
95
95
|
- bin/agentf
|
|
96
96
|
- lib/agentf.rb
|
|
97
|
+
- lib/agentf/agent_execution_contract.rb
|
|
97
98
|
- lib/agentf/agent_policy.rb
|
|
98
99
|
- lib/agentf/agent_roles.rb
|
|
99
100
|
- lib/agentf/agents.rb
|