agentf 0.5.0 → 0.7.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 +3 -3
- data/lib/agentf/agents/base.rb +8 -23
- data/lib/agentf/agents/debugger.rb +1 -2
- data/lib/agentf/agents/designer.rb +22 -7
- data/lib/agentf/agents/documenter.rb +2 -2
- data/lib/agentf/agents/explorer.rb +1 -2
- data/lib/agentf/agents/reviewer.rb +7 -7
- data/lib/agentf/agents/security.rb +11 -9
- data/lib/agentf/agents/specialist.rb +28 -12
- data/lib/agentf/agents/tester.rb +22 -7
- data/lib/agentf/cli/eval.rb +1 -1
- data/lib/agentf/cli/memory.rb +95 -92
- data/lib/agentf/cli/router.rb +1 -1
- data/lib/agentf/commands/memory_reviewer.rb +21 -55
- data/lib/agentf/commands/metrics.rb +4 -13
- data/lib/agentf/context_builder.rb +4 -14
- data/lib/agentf/embedding_provider.rb +35 -0
- data/lib/agentf/installer.rb +162 -82
- data/lib/agentf/mcp/server.rb +123 -177
- data/lib/agentf/memory/confirmation_handler.rb +24 -0
- data/lib/agentf/memory.rb +322 -169
- data/lib/agentf/version.rb +1 -1
- data/lib/agentf/workflow_engine.rb +15 -18
- data/lib/agentf.rb +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9d0041ebcd3b112183ddb349312e4cab8d30237ed1108dbb55d928c525dd59ae
|
|
4
|
+
data.tar.gz: 2577b0dc7af10255d02d2dde419aaa8e1135aea4e36dd67cc10b5d55b1866ba6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ceae3f4d97e84e9934eae6591b7a2be4ea6ed56e31f4555110f998a2f3551f8231572a8c56dd790815988772ad8f4584f5d043d0283828665bcb69ad033d9ef2
|
|
7
|
+
data.tar.gz: 2ff0e3ece10105b6632ebd8e34c5b78c870dc6b8e105dabc41bc7f472502a8c33aee1dc7d10f14fa0ffaea513553c24f1353a9bda7a6647117f200cf8a43de88
|
|
@@ -9,7 +9,7 @@ module Agentf
|
|
|
9
9
|
DESCRIPTION = "Strategy, task decomposition, and memory retrieval."
|
|
10
10
|
COMMANDS = %w[glob read_file memory].freeze
|
|
11
11
|
MEMORY_CONCEPTS = {
|
|
12
|
-
"reads" => ["get_recent_memories", "
|
|
12
|
+
"reads" => ["get_recent_memories", "get_episodes"],
|
|
13
13
|
"writes" => [],
|
|
14
14
|
"policy" => "Retrieve relevant memories before planning; do not duplicate runtime memory into static markdown."
|
|
15
15
|
}.freeze
|
|
@@ -44,7 +44,7 @@ module Agentf
|
|
|
44
44
|
|
|
45
45
|
def self.policy_boundaries
|
|
46
46
|
{
|
|
47
|
-
"always" => ["Capture constraints before decomposition", "Use recent memories and
|
|
47
|
+
"always" => ["Capture constraints before decomposition", "Use recent memories and negative episodes in planning"],
|
|
48
48
|
"ask_first" => ["Changing architectural style from project defaults"],
|
|
49
49
|
"never" => ["Skip task decomposition for non-trivial workflows"],
|
|
50
50
|
"required_inputs" => [],
|
|
@@ -57,7 +57,7 @@ module Agentf
|
|
|
57
57
|
|
|
58
58
|
# Retrieve relevant memories before planning
|
|
59
59
|
recent = memory.get_recent_memories(limit: 5)
|
|
60
|
-
pitfalls = memory.
|
|
60
|
+
pitfalls = memory.get_episodes(limit: 3, outcome: "negative")
|
|
61
61
|
|
|
62
62
|
context = {
|
|
63
63
|
"task" => task,
|
data/lib/agentf/agents/base.rb
CHANGED
|
@@ -4,6 +4,7 @@ module Agentf
|
|
|
4
4
|
module Agents
|
|
5
5
|
# Base agent class
|
|
6
6
|
class Base
|
|
7
|
+
include Agentf::Memory::ConfirmationHandler
|
|
7
8
|
attr_reader :memory, :name
|
|
8
9
|
|
|
9
10
|
def self.typed_name
|
|
@@ -32,8 +33,8 @@ module Agentf
|
|
|
32
33
|
|
|
33
34
|
def self.memory_concepts
|
|
34
35
|
{
|
|
35
|
-
"reads" => ["RedisMemory#get_recent_memories", "RedisMemory#
|
|
36
|
-
"writes" => ["RedisMemory#store_lesson", "RedisMemory#
|
|
36
|
+
"reads" => ["RedisMemory#get_recent_memories", "RedisMemory#get_episodes"],
|
|
37
|
+
"writes" => ["RedisMemory#store_lesson", "RedisMemory#store_episode", "RedisMemory#store_playbook"],
|
|
37
38
|
"policy" => "Memory is runtime state in Redis and should not be embedded as raw data in manifest markdown."
|
|
38
39
|
}
|
|
39
40
|
end
|
|
@@ -52,6 +53,10 @@ module Agentf
|
|
|
52
53
|
}
|
|
53
54
|
end
|
|
54
55
|
|
|
56
|
+
def self.writes_code?
|
|
57
|
+
false
|
|
58
|
+
end
|
|
59
|
+
|
|
55
60
|
def initialize(memory)
|
|
56
61
|
@memory = memory
|
|
57
62
|
@name = self.class.typed_name
|
|
@@ -91,28 +96,8 @@ module Agentf
|
|
|
91
96
|
result: result
|
|
92
97
|
)
|
|
93
98
|
|
|
94
|
-
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Helper to centralize memory write confirmation handling.
|
|
98
|
-
# Yields a block that performs the memory write. If the memory layer
|
|
99
|
-
# requires confirmation (ask_first policy) a structured hash is
|
|
100
|
-
# returned with confirmation details so agents can merge that into
|
|
101
|
-
# their own return payloads or let the orchestrator handle prompting.
|
|
102
|
-
def safe_memory_write(attempted: {})
|
|
103
|
-
begin
|
|
104
|
-
yield
|
|
105
|
-
rescue Agentf::Memory::RedisMemory::ConfirmationRequired => e
|
|
106
|
-
log "[MEMORY] Confirmation required: #{e.message} -- details=#{e.details.inspect}"
|
|
107
|
-
{
|
|
108
|
-
"confirmation_required" => true,
|
|
109
|
-
"confirmation_details" => e.details,
|
|
110
|
-
"attempted" => attempted,
|
|
111
|
-
"confirmed_write_token" => "confirmed",
|
|
112
|
-
"confirmation_prompt" => "Ask the user whether to save this memory. If they approve, rerun the same tool with confirmedWrite=confirmed. If they decline, do not retry."
|
|
113
|
-
}
|
|
99
|
+
result
|
|
114
100
|
end
|
|
115
101
|
end
|
|
116
|
-
end
|
|
117
102
|
end
|
|
118
103
|
end
|
|
@@ -66,13 +66,12 @@ module Agentf
|
|
|
66
66
|
|
|
67
67
|
analysis = @commands.parse_error(error)
|
|
68
68
|
|
|
69
|
-
res = safe_memory_write(attempted: { action: "store_lesson", title: "Debugged: #{error[0..50]}...",
|
|
69
|
+
res = safe_memory_write(attempted: { action: "store_lesson", title: "Debugged: #{error[0..50]}...", agent: name }) do
|
|
70
70
|
memory.store_episode(
|
|
71
71
|
type: "lesson",
|
|
72
72
|
title: "Debugged: #{error[0..50]}...",
|
|
73
73
|
description: "Root cause: #{analysis.possible_causes.first}. Fix: #{analysis.suggested_fix}",
|
|
74
74
|
context: context.to_s,
|
|
75
|
-
tags: ["debugging", "error", "fix"],
|
|
76
75
|
agent: name
|
|
77
76
|
)
|
|
78
77
|
end
|
|
@@ -11,7 +11,7 @@ module Agentf
|
|
|
11
11
|
COMMANDS = %w[generate_component validate_design_system].freeze
|
|
12
12
|
MEMORY_CONCEPTS = {
|
|
13
13
|
"reads" => [],
|
|
14
|
-
"writes" => ["
|
|
14
|
+
"writes" => ["store_episode"],
|
|
15
15
|
"policy" => "Capture successful design implementation patterns."
|
|
16
16
|
}.freeze
|
|
17
17
|
|
|
@@ -45,14 +45,28 @@ module Agentf
|
|
|
45
45
|
|
|
46
46
|
def self.policy_boundaries
|
|
47
47
|
{
|
|
48
|
-
"always" => [
|
|
48
|
+
"always" => [
|
|
49
|
+
"Return generated component details",
|
|
50
|
+
"Persist successful implementation pattern",
|
|
51
|
+
"Write a failing spec before implementing any new component or function (red)",
|
|
52
|
+
"Run the test suite to confirm the spec fails before writing implementation",
|
|
53
|
+
"Run the test suite again after implementation to confirm green"
|
|
54
|
+
],
|
|
49
55
|
"ask_first" => ["Changing primary UI framework", "Persisting successful implementation patterns to memory"],
|
|
50
|
-
"never" => [
|
|
56
|
+
"never" => [
|
|
57
|
+
"Return empty generated code for successful design task",
|
|
58
|
+
"Create a new component or function without a corresponding spec file",
|
|
59
|
+
"Skip red/green verification when writing or modifying code"
|
|
60
|
+
],
|
|
51
61
|
"required_inputs" => ["design_spec"],
|
|
52
62
|
"required_outputs" => ["component", "generated_code", "success"]
|
|
53
63
|
}
|
|
54
64
|
end
|
|
55
65
|
|
|
66
|
+
def self.writes_code?
|
|
67
|
+
true
|
|
68
|
+
end
|
|
69
|
+
|
|
56
70
|
def initialize(memory, commands: nil)
|
|
57
71
|
super(memory)
|
|
58
72
|
@commands = commands || Agentf::Commands::Designer.new
|
|
@@ -64,13 +78,14 @@ module Agentf
|
|
|
64
78
|
|
|
65
79
|
spec = @commands.generate_component("GeneratedComponent", design_spec)
|
|
66
80
|
|
|
67
|
-
res = safe_memory_write(attempted: { action: "
|
|
68
|
-
memory.
|
|
81
|
+
res = safe_memory_write(attempted: { action: "store_episode", title: "Implemented design: #{design_spec}", outcome: "positive", agent: name }) do
|
|
82
|
+
memory.store_episode(
|
|
83
|
+
type: "episode",
|
|
69
84
|
title: "Implemented design: #{design_spec}",
|
|
70
85
|
description: "Created #{spec.name} in #{spec.framework}",
|
|
71
86
|
context: "Framework: #{framework}",
|
|
72
|
-
|
|
73
|
-
|
|
87
|
+
agent: name,
|
|
88
|
+
outcome: "positive"
|
|
74
89
|
)
|
|
75
90
|
end
|
|
76
91
|
|
|
@@ -57,8 +57,8 @@ module Agentf
|
|
|
57
57
|
|
|
58
58
|
memories = memory.get_recent_memories(limit: 20)
|
|
59
59
|
|
|
60
|
-
successes = memories.select { |m| m["type"] == "
|
|
61
|
-
pitfalls = memories.select { |m| m["type"] == "
|
|
60
|
+
successes = memories.select { |m| m["type"] == "episode" && m["outcome"] == "positive" }
|
|
61
|
+
pitfalls = memories.select { |m| m["type"] == "episode" && m["outcome"] == "negative" }
|
|
62
62
|
|
|
63
63
|
log "Found #{successes.size} successes"
|
|
64
64
|
log "Found #{pitfalls.size} pitfalls"
|
|
@@ -63,12 +63,11 @@ module Agentf
|
|
|
63
63
|
|
|
64
64
|
files = @commands.glob(query, file_types: nil)
|
|
65
65
|
|
|
66
|
-
res = safe_memory_write(attempted: { action: "store_lesson", title: "Research finding: #{query}",
|
|
66
|
+
res = safe_memory_write(attempted: { action: "store_lesson", title: "Research finding: #{query}", agent: name }) do
|
|
67
67
|
memory.store_lesson(
|
|
68
68
|
title: "Research finding: #{query}",
|
|
69
69
|
description: "Found #{files.size} relevant files during exploration",
|
|
70
70
|
context: "Search pattern: #{file_pattern || 'all files'}",
|
|
71
|
-
tags: ["research", "exploration"],
|
|
72
71
|
agent: name
|
|
73
72
|
)
|
|
74
73
|
end
|
|
@@ -9,9 +9,9 @@ module Agentf
|
|
|
9
9
|
DESCRIPTION = "Quality assurance and regression checking against memory."
|
|
10
10
|
COMMANDS = %w[read_file memory].freeze
|
|
11
11
|
MEMORY_CONCEPTS = {
|
|
12
|
-
"reads" => ["
|
|
12
|
+
"reads" => ["get_episodes", "get_recent_memories"],
|
|
13
13
|
"writes" => [],
|
|
14
|
-
"policy" => "Validate outputs against known
|
|
14
|
+
"policy" => "Validate outputs against known negative episodes before approval."
|
|
15
15
|
}.freeze
|
|
16
16
|
|
|
17
17
|
def self.description
|
|
@@ -56,14 +56,14 @@ module Agentf
|
|
|
56
56
|
execute_with_contract(context: { "execution" => subtask_result }) do
|
|
57
57
|
log "Reviewing subtask #{subtask_result['subtask_id']}"
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
pitfalls = memory.get_episodes(limit: 5, outcome: "negative")
|
|
60
|
+
memories = memory.get_recent_memories(limit: 5)
|
|
61
61
|
|
|
62
62
|
issues = []
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
pitfalls.each do |pitfall|
|
|
65
|
+
issues << "Warning: Known negative episode - #{pitfall['title']}" if pitfall["type"] == "episode"
|
|
66
|
+
end
|
|
67
67
|
|
|
68
68
|
approved = issues.empty?
|
|
69
69
|
|
|
@@ -11,7 +11,7 @@ module Agentf
|
|
|
11
11
|
COMMANDS = %w[scan best_practices].freeze
|
|
12
12
|
MEMORY_CONCEPTS = {
|
|
13
13
|
"reads" => [],
|
|
14
|
-
"writes" => ["
|
|
14
|
+
"writes" => ["store_episode"],
|
|
15
15
|
"policy" => "Record findings while redacting sensitive values."
|
|
16
16
|
}.freeze
|
|
17
17
|
|
|
@@ -66,24 +66,26 @@ module Agentf
|
|
|
66
66
|
summary = summarize_findings(findings)
|
|
67
67
|
|
|
68
68
|
if findings["issues"].empty?
|
|
69
|
-
res = safe_memory_write(attempted: { action: "
|
|
70
|
-
memory.
|
|
69
|
+
res = safe_memory_write(attempted: { action: "store_episode", title: "Security review passed", outcome: "positive", agent: name }) do
|
|
70
|
+
memory.store_episode(
|
|
71
|
+
type: "episode",
|
|
71
72
|
title: "Security review passed",
|
|
72
73
|
description: summary,
|
|
73
74
|
context: task,
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
agent: name,
|
|
76
|
+
outcome: "positive"
|
|
76
77
|
)
|
|
77
78
|
end
|
|
78
79
|
return findings.merge(res) if res.is_a?(Hash) && res["confirmation_required"]
|
|
79
80
|
else
|
|
80
|
-
res = safe_memory_write(attempted: { action: "
|
|
81
|
-
memory.
|
|
81
|
+
res = safe_memory_write(attempted: { action: "store_episode", title: "Security findings detected", outcome: "negative", agent: name }) do
|
|
82
|
+
memory.store_episode(
|
|
83
|
+
type: "episode",
|
|
82
84
|
title: "Security findings detected",
|
|
83
85
|
description: summary,
|
|
84
86
|
context: task,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
agent: name,
|
|
88
|
+
outcome: "negative"
|
|
87
89
|
)
|
|
88
90
|
end
|
|
89
91
|
return findings.merge(res) if res.is_a?(Hash) && res["confirmation_required"]
|
|
@@ -10,7 +10,7 @@ module Agentf
|
|
|
10
10
|
COMMANDS = %w[read_file write_file run_command].freeze
|
|
11
11
|
MEMORY_CONCEPTS = {
|
|
12
12
|
"reads" => [],
|
|
13
|
-
"writes" => ["
|
|
13
|
+
"writes" => ["store_episode"],
|
|
14
14
|
"policy" => "Persist execution outcomes as lessons for downstream agents."
|
|
15
15
|
}.freeze
|
|
16
16
|
|
|
@@ -44,14 +44,28 @@ module Agentf
|
|
|
44
44
|
|
|
45
45
|
def self.policy_boundaries
|
|
46
46
|
{
|
|
47
|
-
"always" => [
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
"always" => [
|
|
48
|
+
"Persist execution outcome",
|
|
49
|
+
"Return deterministic success boolean",
|
|
50
|
+
"Write a failing spec before adding any new class, method, or module (red)",
|
|
51
|
+
"Run the test suite to confirm the spec fails before writing implementation",
|
|
52
|
+
"Run the test suite again after implementation to confirm green"
|
|
53
|
+
],
|
|
54
|
+
"ask_first" => ["Applying architecture style changes across unrelated modules", "Persisting execution outcomes to memory (success/pitfall)"],
|
|
55
|
+
"never" => [
|
|
56
|
+
"Claim implementation complete without execution result",
|
|
57
|
+
"Create a new class, method, or module without a corresponding spec file",
|
|
58
|
+
"Skip red/green verification when writing or modifying code"
|
|
59
|
+
],
|
|
50
60
|
"required_inputs" => ["description"],
|
|
51
61
|
"required_outputs" => ["subtask_id", "success"]
|
|
52
62
|
}
|
|
53
63
|
end
|
|
54
64
|
|
|
65
|
+
def self.writes_code?
|
|
66
|
+
true
|
|
67
|
+
end
|
|
68
|
+
|
|
55
69
|
def execute(task:, context: {}, agents: {}, commands: {}, logger: nil)
|
|
56
70
|
subtask = task.is_a?(Hash) ? task : (context["current_subtask"] || { "description" => task })
|
|
57
71
|
|
|
@@ -66,13 +80,14 @@ module Agentf
|
|
|
66
80
|
success = normalized_subtask.fetch("success", true)
|
|
67
81
|
|
|
68
82
|
if success
|
|
69
|
-
res = safe_memory_write(attempted: { action: "
|
|
70
|
-
memory.
|
|
83
|
+
res = safe_memory_write(attempted: { action: "store_episode", title: "Completed: #{normalized_subtask['description']}", outcome: "positive", agent: name }) do
|
|
84
|
+
memory.store_episode(
|
|
85
|
+
type: "episode",
|
|
71
86
|
title: "Completed: #{normalized_subtask['description']}",
|
|
72
87
|
description: "Successfully executed subtask #{normalized_subtask['id']}",
|
|
73
88
|
context: "Working on #{normalized_subtask.fetch('task', 'unknown task')}",
|
|
74
|
-
|
|
75
|
-
|
|
89
|
+
agent: name,
|
|
90
|
+
outcome: "positive"
|
|
76
91
|
)
|
|
77
92
|
end
|
|
78
93
|
|
|
@@ -81,13 +96,14 @@ module Agentf
|
|
|
81
96
|
return { "subtask_id" => normalized_subtask["id"], "success" => success, "result" => "Code executed", "confirmation_required" => true, "confirmation_details" => res["confirmation_details"], "attempted" => res["attempted"] }
|
|
82
97
|
end
|
|
83
98
|
else
|
|
84
|
-
res = safe_memory_write(attempted: { action: "
|
|
85
|
-
memory.
|
|
99
|
+
res = safe_memory_write(attempted: { action: "store_episode", title: "Failed: #{normalized_subtask['description']}", outcome: "negative", agent: name }) do
|
|
100
|
+
memory.store_episode(
|
|
101
|
+
type: "episode",
|
|
86
102
|
title: "Failed: #{normalized_subtask['description']}",
|
|
87
103
|
description: "Subtask #{normalized_subtask['id']} failed",
|
|
88
104
|
context: "Working on #{normalized_subtask.fetch('task', 'unknown task')}",
|
|
89
|
-
|
|
90
|
-
|
|
105
|
+
agent: name,
|
|
106
|
+
outcome: "negative"
|
|
91
107
|
)
|
|
92
108
|
end
|
|
93
109
|
|
data/lib/agentf/agents/tester.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Agentf
|
|
|
11
11
|
COMMANDS = %w[detect_framework generate_unit_tests run_tests].freeze
|
|
12
12
|
MEMORY_CONCEPTS = {
|
|
13
13
|
"reads" => [],
|
|
14
|
-
"writes" => ["
|
|
14
|
+
"writes" => ["store_episode"],
|
|
15
15
|
"policy" => "Persist test generation outcomes for future reuse."
|
|
16
16
|
}.freeze
|
|
17
17
|
|
|
@@ -45,14 +45,28 @@ module Agentf
|
|
|
45
45
|
|
|
46
46
|
def self.policy_boundaries
|
|
47
47
|
{
|
|
48
|
-
"always" => [
|
|
48
|
+
"always" => [
|
|
49
|
+
"Produce framework-aware tests",
|
|
50
|
+
"Verify red/green state when TDD enabled",
|
|
51
|
+
"Write a failing spec before adding any new test helper, fixture, or shared example (red)",
|
|
52
|
+
"Run the test suite to confirm the spec fails before writing implementation",
|
|
53
|
+
"Run the test suite again after implementation to confirm green"
|
|
54
|
+
],
|
|
49
55
|
"ask_first" => ["Changing test framework conventions", "Persisting test-generation outcomes to memory"],
|
|
50
|
-
"never" => [
|
|
56
|
+
"never" => [
|
|
57
|
+
"Mark passing when command output is uncertain",
|
|
58
|
+
"Create a new test helper or shared example without a corresponding spec file",
|
|
59
|
+
"Skip red/green verification when writing or modifying test infrastructure code"
|
|
60
|
+
],
|
|
51
61
|
"required_inputs" => [],
|
|
52
62
|
"required_outputs" => ["test_file"]
|
|
53
63
|
}
|
|
54
64
|
end
|
|
55
65
|
|
|
66
|
+
def self.writes_code?
|
|
67
|
+
true
|
|
68
|
+
end
|
|
69
|
+
|
|
56
70
|
def initialize(memory, commands: nil)
|
|
57
71
|
super(memory)
|
|
58
72
|
@commands = commands || Agentf::Commands::Tester.new
|
|
@@ -63,13 +77,14 @@ module Agentf
|
|
|
63
77
|
|
|
64
78
|
template = @commands.generate_unit_tests(code_file)
|
|
65
79
|
|
|
66
|
-
res = safe_memory_write(attempted: { action: "
|
|
67
|
-
memory.
|
|
80
|
+
res = safe_memory_write(attempted: { action: "store_episode", title: "Generated #{test_type} tests for #{code_file}", outcome: "positive", agent: name }) do
|
|
81
|
+
memory.store_episode(
|
|
82
|
+
type: "episode",
|
|
68
83
|
title: "Generated #{test_type} tests for #{code_file}",
|
|
69
84
|
description: "Created #{template.test_file} with #{test_type} tests",
|
|
70
85
|
context: "Test framework: #{template.framework}",
|
|
71
|
-
|
|
72
|
-
|
|
86
|
+
agent: name,
|
|
87
|
+
outcome: "positive"
|
|
73
88
|
)
|
|
74
89
|
end
|
|
75
90
|
|