agentf 0.4.7 → 0.6.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/agents/architect.rb +7 -3
- data/lib/agentf/agents/base.rb +31 -3
- data/lib/agentf/agents/debugger.rb +30 -8
- data/lib/agentf/agents/designer.rb +20 -8
- data/lib/agentf/agents/documenter.rb +8 -2
- data/lib/agentf/agents/explorer.rb +29 -11
- data/lib/agentf/agents/reviewer.rb +12 -7
- data/lib/agentf/agents/security.rb +27 -15
- data/lib/agentf/agents/specialist.rb +34 -18
- data/lib/agentf/agents/tester.rb +48 -8
- data/lib/agentf/cli/agent.rb +95 -0
- data/lib/agentf/cli/eval.rb +203 -0
- data/lib/agentf/cli/install.rb +7 -0
- data/lib/agentf/cli/memory.rb +138 -90
- data/lib/agentf/cli/router.rb +16 -4
- data/lib/agentf/cli/update.rb +9 -2
- data/lib/agentf/commands/memory_reviewer.rb +22 -48
- data/lib/agentf/commands/metrics.rb +18 -25
- data/lib/agentf/commands/registry.rb +28 -0
- data/lib/agentf/context_builder.rb +4 -14
- data/lib/agentf/embedding_provider.rb +35 -0
- data/lib/agentf/evals/report.rb +134 -0
- data/lib/agentf/evals/runner.rb +771 -0
- data/lib/agentf/evals/scenario.rb +211 -0
- data/lib/agentf/installer.rb +498 -365
- data/lib/agentf/mcp/server.rb +294 -114
- data/lib/agentf/memory.rb +354 -214
- data/lib/agentf/service/providers.rb +10 -62
- data/lib/agentf/version.rb +1 -1
- data/lib/agentf/workflow_engine.rb +205 -77
- data/lib/agentf.rb +10 -3
- metadata +9 -3
- data/lib/agentf/packs.rb +0 -74
data/lib/agentf/cli/update.rb
CHANGED
|
@@ -34,7 +34,8 @@ module Agentf
|
|
|
34
34
|
scope: "all",
|
|
35
35
|
global_root: Dir.home,
|
|
36
36
|
local_root: Dir.pwd,
|
|
37
|
-
force: false
|
|
37
|
+
force: false,
|
|
38
|
+
opencode_runtime: "mcp"
|
|
38
39
|
}
|
|
39
40
|
end
|
|
40
41
|
|
|
@@ -78,6 +79,9 @@ module Agentf
|
|
|
78
79
|
|
|
79
80
|
local_root = parse_single_option(args, "--local-root=")
|
|
80
81
|
@options[:local_root] = File.expand_path(local_root) if local_root
|
|
82
|
+
|
|
83
|
+
opencode_runtime = parse_single_option(args, "--opencode-runtime=")
|
|
84
|
+
@options[:opencode_runtime] = opencode_runtime if opencode_runtime
|
|
81
85
|
end
|
|
82
86
|
|
|
83
87
|
def roots_for(scope)
|
|
@@ -113,7 +117,8 @@ module Agentf
|
|
|
113
117
|
|
|
114
118
|
installer = @installer_class.new(
|
|
115
119
|
global_root: root,
|
|
116
|
-
local_root: root
|
|
120
|
+
local_root: root,
|
|
121
|
+
opencode_runtime: @options[:opencode_runtime]
|
|
117
122
|
)
|
|
118
123
|
|
|
119
124
|
results = installer.install(
|
|
@@ -191,12 +196,14 @@ module Agentf
|
|
|
191
196
|
--scope=SCOPE Update scope: global|local|all (default: all)
|
|
192
197
|
--global-root=PATH Root for global installs (default: $HOME)
|
|
193
198
|
--local-root=PATH Root for local installs (default: current directory)
|
|
199
|
+
--opencode-runtime=MODE Opencode runtime: mcp|plugin (default: mcp)
|
|
194
200
|
--force Regenerate even if version matches
|
|
195
201
|
|
|
196
202
|
Examples:
|
|
197
203
|
agentf update
|
|
198
204
|
agentf update --force
|
|
199
205
|
agentf update --provider=opencode,copilot --scope=local
|
|
206
|
+
agentf update --provider=opencode --opencode-runtime=plugin
|
|
200
207
|
HELP
|
|
201
208
|
end
|
|
202
209
|
end
|
|
@@ -12,14 +12,11 @@ module Agentf
|
|
|
12
12
|
def self.manifest
|
|
13
13
|
{
|
|
14
14
|
"name" => NAME,
|
|
15
|
-
"description" => "Review and query Redis-stored memories,
|
|
15
|
+
"description" => "Review and query Redis-stored memories, episodes, and learnings.",
|
|
16
16
|
"commands" => [
|
|
17
17
|
{ "name" => "get_recent_memories", "type" => "function" },
|
|
18
|
-
{ "name" => "
|
|
18
|
+
{ "name" => "get_episodes", "type" => "function" },
|
|
19
19
|
{ "name" => "get_lessons", "type" => "function" },
|
|
20
|
-
{ "name" => "get_successes", "type" => "function" },
|
|
21
|
-
{ "name" => "get_all_tags", "type" => "function" },
|
|
22
|
-
{ "name" => "get_by_tag", "type" => "function" },
|
|
23
20
|
{ "name" => "get_by_type", "type" => "function" },
|
|
24
21
|
{ "name" => "get_by_agent", "type" => "function" },
|
|
25
22
|
{ "name" => "search", "type" => "function" },
|
|
@@ -30,9 +27,10 @@ module Agentf
|
|
|
30
27
|
}
|
|
31
28
|
end
|
|
32
29
|
|
|
33
|
-
def initialize(project: nil)
|
|
30
|
+
def initialize(project: nil, memory: nil)
|
|
34
31
|
@project = project || Agentf.config.project_name
|
|
35
|
-
|
|
32
|
+
# Allow injecting a memory instance for testing; default to real RedisMemory
|
|
33
|
+
@memory = memory || Agentf::Memory::RedisMemory.new(project: @project)
|
|
36
34
|
end
|
|
37
35
|
|
|
38
36
|
# Get recent memories
|
|
@@ -43,10 +41,9 @@ module Agentf
|
|
|
43
41
|
{ "error" => e.message }
|
|
44
42
|
end
|
|
45
43
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
format_memories(pitfalls)
|
|
44
|
+
def get_episodes(limit: 10, outcome: nil)
|
|
45
|
+
episodes = @memory.get_episodes(limit: limit, outcome: outcome)
|
|
46
|
+
format_memories(episodes)
|
|
50
47
|
rescue => e
|
|
51
48
|
{ "error" => e.message }
|
|
52
49
|
end
|
|
@@ -59,14 +56,6 @@ module Agentf
|
|
|
59
56
|
{ "error" => e.message }
|
|
60
57
|
end
|
|
61
58
|
|
|
62
|
-
# Get all successes
|
|
63
|
-
def get_successes(limit: 10)
|
|
64
|
-
successes = @memory.get_memories_by_type(type: "success", limit: limit)
|
|
65
|
-
format_memories(successes)
|
|
66
|
-
rescue => e
|
|
67
|
-
{ "error" => e.message }
|
|
68
|
-
end
|
|
69
|
-
|
|
70
59
|
def get_business_intents(limit: 10)
|
|
71
60
|
intents = @memory.get_intents(kind: "business", limit: limit)
|
|
72
61
|
format_memories(intents)
|
|
@@ -81,28 +70,17 @@ module Agentf
|
|
|
81
70
|
{ "error" => e.message }
|
|
82
71
|
end
|
|
83
72
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
{ "tags" => tags.sort, "count" => tags.length }
|
|
88
|
-
rescue => e
|
|
89
|
-
{ "error" => e.message }
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Get memories by tag
|
|
93
|
-
def get_by_tag(tag, limit: 10)
|
|
94
|
-
memories = @memory.get_recent_memories(limit: 100)
|
|
95
|
-
filtered = memories.select { |m| m["tags"]&.include?(tag) }
|
|
96
|
-
format_memories(filtered.first(limit))
|
|
73
|
+
def get_intents(limit: 10)
|
|
74
|
+
intents = @memory.get_intents(limit: limit)
|
|
75
|
+
format_memories(intents)
|
|
97
76
|
rescue => e
|
|
98
77
|
{ "error" => e.message }
|
|
99
78
|
end
|
|
100
79
|
|
|
101
80
|
# Get memories by type (pitfall, lesson, success)
|
|
102
81
|
def get_by_type(type, limit: 10)
|
|
103
|
-
memories = @memory.
|
|
104
|
-
|
|
105
|
-
format_memories(filtered.first(limit))
|
|
82
|
+
memories = @memory.get_memories_by_type(type: type, limit: limit)
|
|
83
|
+
format_memories(memories)
|
|
106
84
|
rescue => e
|
|
107
85
|
{ "error" => e.message }
|
|
108
86
|
end
|
|
@@ -118,14 +96,7 @@ module Agentf
|
|
|
118
96
|
|
|
119
97
|
# Search memories by keyword in title or description
|
|
120
98
|
def search(query, limit: 10)
|
|
121
|
-
|
|
122
|
-
q = query.downcase
|
|
123
|
-
filtered = memories.select do |m|
|
|
124
|
-
m["title"]&.downcase&.include?(q) ||
|
|
125
|
-
m["description"]&.downcase&.include?(q) ||
|
|
126
|
-
m["context"]&.downcase&.include?(q)
|
|
127
|
-
end
|
|
128
|
-
format_memories(filtered.first(limit))
|
|
99
|
+
format_memories(@memory.search_memories(query: query, limit: limit))
|
|
129
100
|
rescue => e
|
|
130
101
|
{ "error" => e.message }
|
|
131
102
|
end
|
|
@@ -133,19 +104,22 @@ module Agentf
|
|
|
133
104
|
# Get summary statistics
|
|
134
105
|
def get_summary
|
|
135
106
|
memories = @memory.get_recent_memories(limit: 100)
|
|
136
|
-
tags = @memory.get_all_tags
|
|
137
107
|
|
|
138
108
|
{
|
|
139
109
|
"total_memories" => memories.length,
|
|
140
110
|
"by_type" => {
|
|
141
|
-
"
|
|
111
|
+
"episode" => memories.count { |m| m["type"] == "episode" },
|
|
142
112
|
"lesson" => memories.count { |m| m["type"] == "lesson" },
|
|
143
|
-
"
|
|
113
|
+
"playbook" => memories.count { |m| m["type"] == "playbook" },
|
|
144
114
|
"business_intent" => memories.count { |m| m["type"] == "business_intent" },
|
|
145
115
|
"feature_intent" => memories.count { |m| m["type"] == "feature_intent" }
|
|
146
116
|
},
|
|
117
|
+
"by_outcome" => {
|
|
118
|
+
"positive" => memories.count { |m| m["outcome"] == "positive" },
|
|
119
|
+
"negative" => memories.count { |m| m["outcome"] == "negative" },
|
|
120
|
+
"neutral" => memories.count { |m| m["outcome"] == "neutral" }
|
|
121
|
+
},
|
|
147
122
|
"by_agent" => memories.each_with_object(Hash.new(0)) { |m, h| h[m["agent"]] += 1 },
|
|
148
|
-
"unique_tags" => tags.length,
|
|
149
123
|
"project" => @project
|
|
150
124
|
}
|
|
151
125
|
rescue => e
|
|
@@ -181,7 +155,7 @@ module Agentf
|
|
|
181
155
|
"description" => m["description"],
|
|
182
156
|
"context" => m["context"],
|
|
183
157
|
"code_snippet" => m["code_snippet"],
|
|
184
|
-
"
|
|
158
|
+
"outcome" => m["outcome"],
|
|
185
159
|
"agent" => m["agent"],
|
|
186
160
|
"metadata" => m["metadata"],
|
|
187
161
|
"entity_ids" => m["entity_ids"],
|
|
@@ -7,8 +7,6 @@ module Agentf
|
|
|
7
7
|
class Metrics
|
|
8
8
|
NAME = "metrics"
|
|
9
9
|
|
|
10
|
-
WORKFLOW_METRICS_TAG = "workflow_metric"
|
|
11
|
-
|
|
12
10
|
def self.manifest
|
|
13
11
|
{
|
|
14
12
|
"name" => NAME,
|
|
@@ -28,20 +26,23 @@ module Agentf
|
|
|
28
26
|
|
|
29
27
|
def record_workflow(workflow_state)
|
|
30
28
|
metrics = extract_metrics(workflow_state)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
29
|
+
begin
|
|
30
|
+
@memory.store_episode(
|
|
31
|
+
type: "episode",
|
|
32
|
+
title: metric_title(metrics),
|
|
33
|
+
description: metric_description(metrics),
|
|
34
|
+
context: metric_context(metrics),
|
|
35
|
+
agent: Agentf::AgentRoles::ORCHESTRATOR,
|
|
36
|
+
outcome: "positive",
|
|
37
|
+
metadata: { "workflow_metric" => true },
|
|
38
|
+
code_snippet: ""
|
|
39
|
+
)
|
|
40
|
+
{ "status" => "recorded", "metrics" => metrics }
|
|
41
|
+
rescue Agentf::Memory::RedisMemory::ConfirmationRequired => e
|
|
42
|
+
{ "status" => "confirmation_required", "confirmation_details" => e.details, "attempted" => { "action" => "record_workflow" } }
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
{ "status" => "error", "error" => e.message }
|
|
45
|
+
end
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
def summary(limit: 100)
|
|
@@ -169,14 +170,6 @@ module Agentf
|
|
|
169
170
|
}.to_json
|
|
170
171
|
end
|
|
171
172
|
|
|
172
|
-
def metric_tags(metrics)
|
|
173
|
-
[
|
|
174
|
-
WORKFLOW_METRICS_TAG,
|
|
175
|
-
"provider:#{metrics['provider'].to_s.downcase}",
|
|
176
|
-
"workflow:#{metrics['workflow_type']}"
|
|
177
|
-
]
|
|
178
|
-
end
|
|
179
|
-
|
|
180
173
|
def top_contract_violations(records)
|
|
181
174
|
counts = Hash.new(0)
|
|
182
175
|
records.each do |record|
|
|
@@ -189,7 +182,7 @@ module Agentf
|
|
|
189
182
|
memories = @memory.get_recent_memories(limit: limit)
|
|
190
183
|
|
|
191
184
|
memories
|
|
192
|
-
.select { |m|
|
|
185
|
+
.select { |m| m.dig("metadata", "workflow_metric") == true }
|
|
193
186
|
.map do |m|
|
|
194
187
|
context = parse_context_json(m["context"])
|
|
195
188
|
context
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Agentf
|
|
4
|
+
module Commands
|
|
5
|
+
class Registry
|
|
6
|
+
def initialize(map = {})
|
|
7
|
+
@map = map
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def register(name, impl)
|
|
11
|
+
@map[name.to_s] = impl
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def fetch(name)
|
|
15
|
+
@map.fetch(name.to_s)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(command_name, action, *args)
|
|
19
|
+
impl = fetch(command_name)
|
|
20
|
+
if impl.respond_to?(action)
|
|
21
|
+
impl.public_send(action, *args)
|
|
22
|
+
else
|
|
23
|
+
raise "Command #{command_name} does not implement #{action}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Agentf
|
|
4
4
|
class ContextBuilder
|
|
5
|
-
def initialize(memory:)
|
|
5
|
+
def initialize(memory:, embedding_provider: Agentf::EmbeddingProvider.new)
|
|
6
6
|
@memory = memory
|
|
7
|
+
@embedding_provider = embedding_provider
|
|
7
8
|
end
|
|
8
9
|
|
|
9
10
|
def build(agent:, workflow_state:, limit: 8)
|
|
@@ -13,23 +14,12 @@ module Agentf
|
|
|
13
14
|
@memory.get_agent_context(
|
|
14
15
|
agent: agent,
|
|
15
16
|
task_type: task_type,
|
|
16
|
-
|
|
17
|
+
query_text: task,
|
|
18
|
+
query_embedding: @embedding_provider.embed(task),
|
|
17
19
|
limit: limit
|
|
18
20
|
)
|
|
19
21
|
rescue StandardError
|
|
20
22
|
{ "agent" => agent, "intent" => [], "memories" => [], "similar_tasks" => [] }
|
|
21
23
|
end
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
def simple_embedding(text)
|
|
26
|
-
normalized = text.to_s.downcase
|
|
27
|
-
[
|
|
28
|
-
normalized.include?("fix") || normalized.include?("bug") ? 1.0 : 0.0,
|
|
29
|
-
normalized.include?("feature") || normalized.include?("add") ? 1.0 : 0.0,
|
|
30
|
-
normalized.include?("security") ? 1.0 : 0.0,
|
|
31
|
-
normalized.length.to_f / 100.0
|
|
32
|
-
]
|
|
33
|
-
end
|
|
34
24
|
end
|
|
35
25
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Agentf
|
|
6
|
+
class EmbeddingProvider
|
|
7
|
+
DIMENSIONS = 64
|
|
8
|
+
|
|
9
|
+
def initialize(dimensions: DIMENSIONS)
|
|
10
|
+
@dimensions = dimensions
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def embed(text)
|
|
14
|
+
tokens = tokenize(text)
|
|
15
|
+
return [] if tokens.empty?
|
|
16
|
+
|
|
17
|
+
vector = Array.new(@dimensions, 0.0)
|
|
18
|
+
tokens.each do |token|
|
|
19
|
+
hash = Digest::SHA256.hexdigest(token)[0, 8].to_i(16)
|
|
20
|
+
vector[hash % @dimensions] += 1.0
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
magnitude = Math.sqrt(vector.sum { |value| value * value })
|
|
24
|
+
return vector if magnitude.zero?
|
|
25
|
+
|
|
26
|
+
vector.map { |value| (value / magnitude).round(8) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def tokenize(text)
|
|
32
|
+
text.to_s.downcase.scan(/[a-z0-9_]+/).reject { |token| token.length < 2 }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Agentf
|
|
6
|
+
module Evals
|
|
7
|
+
class Report
|
|
8
|
+
def initialize(output_root: Runner::DEFAULT_OUTPUT_ROOT)
|
|
9
|
+
@output_root = File.expand_path(output_root)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_reader :output_root
|
|
13
|
+
|
|
14
|
+
def generate(limit: nil, since: nil, scenario: nil)
|
|
15
|
+
records = load_history
|
|
16
|
+
records = filter_since(records, since)
|
|
17
|
+
records = filter_scenario(records, scenario)
|
|
18
|
+
records = records.last(limit) if limit && limit.positive?
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
"output_root" => output_root,
|
|
22
|
+
"history_path" => history_path,
|
|
23
|
+
"count" => records.length,
|
|
24
|
+
"passes" => records.count { |record| record["status"] == "passed" },
|
|
25
|
+
"failures" => records.count { |record| record["status"] == "failed" },
|
|
26
|
+
"retry_summary" => summarize_retries(records),
|
|
27
|
+
"memory_effectiveness" => summarize_memory_effectiveness(records),
|
|
28
|
+
"providers" => summarize_dimension(records, "providers"),
|
|
29
|
+
"models" => summarize_dimension(records, "models"),
|
|
30
|
+
"scenarios" => summarize_scenarios(records)
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def history_path
|
|
37
|
+
File.join(output_root, "history.jsonl")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def load_history
|
|
41
|
+
return [] unless File.exist?(history_path)
|
|
42
|
+
|
|
43
|
+
File.readlines(history_path, chomp: true).filter_map do |line|
|
|
44
|
+
next if line.to_s.strip.empty?
|
|
45
|
+
|
|
46
|
+
JSON.parse(line)
|
|
47
|
+
rescue JSON::ParserError
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def filter_since(records, since)
|
|
53
|
+
return records unless since
|
|
54
|
+
|
|
55
|
+
cutoff = since.is_a?(Time) ? since : Time.parse(since.to_s)
|
|
56
|
+
records.select do |record|
|
|
57
|
+
recorded_at = record["recorded_at"]
|
|
58
|
+
recorded_at && Time.parse(recorded_at) >= cutoff
|
|
59
|
+
rescue ArgumentError
|
|
60
|
+
false
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def filter_scenario(records, scenario)
|
|
65
|
+
return records if scenario.to_s.strip.empty?
|
|
66
|
+
|
|
67
|
+
records.select { |record| record["scenario"] == scenario }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def summarize_retries(records)
|
|
71
|
+
retried = records.count { |record| record["retry_count"].to_i.positive? }
|
|
72
|
+
{
|
|
73
|
+
"retried_runs" => retried,
|
|
74
|
+
"total_retries" => records.sum { |record| record["retry_count"].to_i },
|
|
75
|
+
"flaky_runs" => records.count { |record| record["flaky"] == true }
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def summarize_dimension(records, key)
|
|
80
|
+
summary = Hash.new { |hash, name| hash[name] = base_stats }
|
|
81
|
+
|
|
82
|
+
records.each do |record|
|
|
83
|
+
Array(record[key]).each do |name|
|
|
84
|
+
entry = summary[name]
|
|
85
|
+
update_stats(entry, record)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
summary
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def summarize_scenarios(records)
|
|
93
|
+
summary = Hash.new { |hash, name| hash[name] = base_stats.merge("last_status" => nil, "last_recorded_at" => nil) }
|
|
94
|
+
|
|
95
|
+
records.each do |record|
|
|
96
|
+
entry = summary[record["scenario"]]
|
|
97
|
+
update_stats(entry, record)
|
|
98
|
+
update_memory_effectiveness(entry, record)
|
|
99
|
+
entry["last_status"] = record["status"]
|
|
100
|
+
entry["last_recorded_at"] = record["recorded_at"]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
summary
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def summarize_memory_effectiveness(records)
|
|
107
|
+
relevant = records.filter_map { |record| record["memory_effectiveness"] }
|
|
108
|
+
{
|
|
109
|
+
"tracked_runs" => relevant.length,
|
|
110
|
+
"retrieved_expected_memory" => relevant.count { |item| item["retrieved_expected_memory"] == true }
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def base_stats
|
|
115
|
+
{ "total" => 0, "passed" => 0, "failed" => 0, "retried" => 0, "flaky" => 0 }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def update_stats(entry, record)
|
|
119
|
+
entry["total"] += 1
|
|
120
|
+
entry[record["status"] == "passed" ? "passed" : "failed"] += 1
|
|
121
|
+
entry["retried"] += 1 if record["retry_count"].to_i.positive?
|
|
122
|
+
entry["flaky"] += 1 if record["flaky"] == true
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def update_memory_effectiveness(entry, record)
|
|
126
|
+
effect = record["memory_effectiveness"]
|
|
127
|
+
return unless effect
|
|
128
|
+
|
|
129
|
+
entry["memory_tracked"] = entry.fetch("memory_tracked", 0) + 1
|
|
130
|
+
entry["memory_retrieved"] = entry.fetch("memory_retrieved", 0) + (effect["retrieved_expected_memory"] == true ? 1 : 0)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|