agentf 0.4.1 → 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/router.rb +2 -0
- data/lib/agentf/installer.rb +99 -14
- 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/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({
|
|
@@ -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
|