ask-agent 0.40.29 → 0.41.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.
@@ -94,6 +94,12 @@ module Ask
94
94
  )
95
95
  end
96
96
  agent.instance_variable_set(:@messages, agent.chat.messages.dup)
97
+ restore_persisted_approvals(
98
+ agent,
99
+ payload[:approvals] || payload["approvals"],
100
+ payload[:plan_approvals] || payload["plan_approvals"],
101
+ payload[:session_grants] || payload["session_grants"]
102
+ )
97
103
 
98
104
  new(agent: agent, host: host, session_id: session_id, create: false)
99
105
  end
@@ -238,10 +244,122 @@ module Ask
238
244
  def build_snapshot
239
245
  {
240
246
  messages: (@agent.messages || []).map(&:to_h),
241
- turn_count: @agent.turn_count || 0
247
+ turn_count: @agent.turn_count || 0,
248
+ # Durable permission state: JSON-safe v1 queue snapshots when the
249
+ # agent exposes a queue supporting the shared Permissions API.
250
+ # Nil when approval is off or unsupported (compatibility fallback
251
+ # — nothing durable to persist, so nothing to strand). Snapshot
252
+ # failures raise SessionAdapter::Error with queue context.
253
+ approvals: self.class.queue_snapshot_for(@agent, :approval_queue),
254
+ plan_approvals: self.class.queue_snapshot_for(@agent, :plan_queue),
255
+ session_grants: self.class.grants_snapshot_for(@agent)
242
256
  }
243
257
  end
244
258
 
259
+ def self.queue_snapshot_for(agent, queue_name)
260
+ return nil unless agent.respond_to?(queue_name)
261
+
262
+ queue = agent.public_send(queue_name)
263
+ return nil unless queue
264
+ return nil unless queue.respond_to?(:snapshot)
265
+
266
+ begin
267
+ queue.snapshot
268
+ rescue StandardError => e
269
+ raise Error, "Failed to snapshot #{queue_name} queue (#{queue.class}): #{e.class}: #{e.message}"
270
+ end
271
+ end
272
+
273
+ def self.grants_snapshot_for(agent)
274
+ return nil unless agent.respond_to?(:session_grants)
275
+ grants = agent.public_send(:session_grants)
276
+ grants&.respond_to?(:snapshot) ? grants.snapshot : nil
277
+ rescue StandardError => e
278
+ raise Error, "Failed to snapshot session grants (#{grants.class}): #{e.class}: #{e.message}"
279
+ end
280
+
281
+ # Restore persisted queue snapshots into the agent without firing
282
+ # callbacks or emitting approval-required events. For real Sessions
283
+ # this delegates to the session's silent restore (which also rebuilds
284
+ # pending-tool registrations so approve/reject completes exactly
285
+ # once); for generic agents it restores directly when the surface
286
+ # allows.
287
+ #
288
+ # Compatibility fallback (cannot strand): nil snapshots and empty
289
+ # pending_actions lists are safe no-ops. Any other unrestorable state
290
+ # — non-Hash snapshot, missing/non-Array pendings, missing queue or
291
+ # queue without restore support while pendings exist, a non-empty
292
+ # target queue, or a restore_pending failure — raises
293
+ # SessionAdapter::Error (or Ask::Agent::Error from the session
294
+ # delegate) with queue context instead of silently dropping actions.
295
+ def self.restore_persisted_approvals(agent, approvals_snapshot, plan_snapshot, grants_snapshot = nil)
296
+ if agent.respond_to?(:restore_persisted_approvals, true)
297
+ agent.send(:restore_persisted_approvals, approvals_snapshot, plan_snapshot, grants_snapshot)
298
+ return
299
+ end
300
+
301
+ { approval_queue: approvals_snapshot, plan_queue: plan_snapshot }.each do |queue_name, snapshot|
302
+ restore_into_generic_queue(agent, queue_name, snapshot)
303
+ end
304
+ return if grants_snapshot.nil?
305
+
306
+ unless grants_snapshot.is_a?(Hash)
307
+ raise Error, "Cannot restore session grants: snapshot must be a Hash, got #{grants_snapshot.class}"
308
+ end
309
+ tools = grants_snapshot[:granted_tools] || grants_snapshot["granted_tools"]
310
+ unless tools.is_a?(Array) && tools.all? { |tool| tool.is_a?(String) && !tool.empty? }
311
+ raise Error, "Cannot restore session grants: granted_tools must be an Array of non-empty Strings"
312
+ end
313
+ return if tools.empty?
314
+ unless agent.respond_to?(:session_grants)
315
+ raise Error, "Cannot restore session grants: agent has no session grants surface"
316
+ end
317
+
318
+ grants = agent.public_send(:session_grants)
319
+ raise Error, "Cannot restore session grants: agent has no grants store" unless grants
320
+ begin
321
+ grants.restore_snapshot(grants_snapshot)
322
+ rescue StandardError => e
323
+ raise Error, "Cannot restore session grants: #{e.class}: #{e.message}"
324
+ end
325
+ end
326
+
327
+ def self.restore_into_generic_queue(agent, queue_name, snapshot)
328
+ return nil if snapshot.nil?
329
+
330
+ unless snapshot.is_a?(Hash)
331
+ raise Error, "Cannot restore #{queue_name} queue: snapshot must be a Hash, got #{snapshot.class}"
332
+ end
333
+
334
+ pendings = snapshot[:pending_actions] || snapshot["pending_actions"]
335
+ if pendings.nil?
336
+ raise Error, "Cannot restore #{queue_name} queue: snapshot missing pending_actions"
337
+ end
338
+ unless pendings.is_a?(Array)
339
+ raise Error, "Cannot restore #{queue_name} queue: pending_actions must be an Array, got #{pendings.class}"
340
+ end
341
+ return nil if pendings.empty?
342
+ return nil unless agent.respond_to?(queue_name)
343
+
344
+ queue = agent.public_send(queue_name)
345
+ unless queue
346
+ raise Error, "Cannot restore #{queue_name} queue: agent has no #{queue_name} queue but snapshot carries #{pendings.size} pending action(s)"
347
+ end
348
+ unless queue.respond_to?(:restore_pending) && queue.respond_to?(:pending_actions)
349
+ raise Error, "Cannot restore #{queue_name} queue: #{queue.class} does not support restore_pending"
350
+ end
351
+ if queue.respond_to?(:any_pending?) && queue.any_pending?
352
+ raise Error, "Cannot restore #{queue_name} queue: target queue already holds pending actions"
353
+ end
354
+
355
+ begin
356
+ queue.restore_pending(snapshot)
357
+ rescue StandardError => e
358
+ raise Error, "Cannot restore #{queue_name} queue: #{e.class}: #{e.message}"
359
+ end
360
+ nil
361
+ end
362
+
245
363
  def self.deserialize_content(content)
246
364
  content.is_a?(Array) ? content.map { |block| Ask::Content.from_h(block) } : content
247
365
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Agent
5
- VERSION = "0.40.29"
5
+ VERSION = "0.41.0"
6
6
  end
7
7
  end
data/lib/ask/agent.rb CHANGED
@@ -9,6 +9,7 @@ require "ask-llm-providers"
9
9
  require "ask-tools"
10
10
  require "ask-state-providers"
11
11
  require "ask-runtime"
12
+ require "ask/permissions"
12
13
 
13
14
  module Ask
14
15
  module Agent
@@ -29,15 +30,12 @@ module Ask
29
30
  # swap them freely (see Ask::Agent::Hooks for the seam).
30
31
  #
31
32
  # A policy is NOT core machinery. Core mechanisms live on Session — the
32
- # approval queue, the :pending result status, and the `approval: true`
33
- # option are core; Policies::ApprovalPolicy is the reference
34
- # classification policy wired on top of them.
33
+ # :pending result status and the `approval: true` option are core;
34
+ # Ask::Permissions::ApprovalPolicy (from the ask-permissions gem) is the
35
+ # reference classification policy wired on top of them.
35
36
  module Policies
36
- autoload :Permissions, "ask/agent/policies/permissions"
37
37
  autoload :RateLimiter, "ask/agent/policies/rate_limiter"
38
38
  autoload :AuditLog, "ask/agent/policies/audit_log"
39
- autoload :ApprovalPolicy, "ask/agent/policies/approval_policy"
40
- autoload :PermissionRules, "ask/agent/policies/permission_rules"
41
39
  end
42
40
 
43
41
  autoload :ToolCallRepair, "ask/agent/tool_call_repair"
@@ -223,7 +221,6 @@ require_relative "agent/evaluator"
223
221
  require_relative "agent/tool_executor"
224
222
  require_relative "agent/compactor"
225
223
  require_relative "agent/hooks"
226
- require_relative "agent/approval_queue"
227
224
  require_relative "agent/configuration"
228
225
  require_relative "agent/meta_agent"
229
226
  require_relative "agent/persistence/base"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.40.29
4
+ version: 0.41.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -135,6 +135,20 @@ dependencies:
135
135
  - - ">="
136
136
  - !ruby/object:Gem::Version
137
137
  version: 0.1.2
138
+ - !ruby/object:Gem::Dependency
139
+ name: ask-permissions
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: 0.2.0
145
+ type: :runtime
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: 0.2.0
138
152
  - !ruby/object:Gem::Dependency
139
153
  name: minitest
140
154
  requirement: !ruby/object:Gem::Requirement
@@ -192,7 +206,6 @@ files:
192
206
  - exe/askr
193
207
  - lib/ask-agent.rb
194
208
  - lib/ask/agent.rb
195
- - lib/ask/agent/approval_queue.rb
196
209
  - lib/ask/agent/artifact_store.rb
197
210
  - lib/ask/agent/chat.rb
198
211
  - lib/ask/agent/checkpoint_store.rb
@@ -221,11 +234,8 @@ files:
221
234
  - lib/ask/agent/output_read.rb
222
235
  - lib/ask/agent/persistence/base.rb
223
236
  - lib/ask/agent/persistence/in_memory.rb
224
- - lib/ask/agent/policies/approval_policy.rb
225
237
  - lib/ask/agent/policies/audit_log.rb
226
238
  - lib/ask/agent/policies/audit_log/active_record_writer.rb
227
- - lib/ask/agent/policies/permission_rules.rb
228
- - lib/ask/agent/policies/permissions.rb
229
239
  - lib/ask/agent/policies/rate_limiter.rb
230
240
  - lib/ask/agent/reflector.rb
231
241
  - lib/ask/agent/scheduler.rb
@@ -1,249 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ask
4
- module Agent
5
- # A queue of tool actions awaiting human approval.
6
- #
7
- # When an agent calls a tool that requires approval, the action is queued
8
- # instead of executed — the agent gets a pending result and continues.
9
- # The user approves or rejects actions later, in bulk or one-by-one.
10
- #
11
- # queue = Ask::Agent::ApprovalQueue.new
12
- # id = queue.submit(tool_call_id: "call_1", tool_name: "send_email",
13
- # args: { "to" => "x@y.com" }, auto_approvable: false)
14
- # queue.pending_actions # => [{id: 1, tool_name: "send_email", ...}]
15
- # queue.approve(1) # applies action 1 via the on_approve callback
16
- # queue.reject(1)
17
- #
18
- # Auto-approval is a dual signal: the action must be marked
19
- # +auto_approvable+ (by the tool) AND the queue must have a matching
20
- # auto-approval rule enabled by the user. Otherwise the action queues for
21
- # human review.
22
- #
23
- # Actions are applied in id order and the drainer never applies past a
24
- # non-auto-approvable (manual) gate — nothing is silently approved.
25
- class ApprovalQueue
26
- # One queued action.
27
- #
28
- # @!attribute [r] id
29
- # @return [Integer] sequential id assigned by the queue
30
- # @!attribute [r] tool_call_id
31
- # @return [String] the original tool call id from the LLM
32
- # @!attribute [r] tool_name
33
- # @return [String] tool name
34
- # @!attribute [r] args
35
- # @return [Hash] arguments to pass to the tool when applied
36
- # @!attribute [r] auto_approvable
37
- # @return [Boolean] per-action verdict (tool's declaration)
38
- # @!attribute [r] status
39
- # @return [Symbol] :pending, :applying, :approved, :rejected
40
- # @!attribute [r] submitted_at
41
- # @return [Time]
42
- # @!attribute [r] message
43
- # @return [String, nil] human-readable description of the action
44
- Action = Data.define(:id, :tool_call_id, :tool_name, :args,
45
- :auto_approvable, :status, :submitted_at, :message)
46
-
47
- # Create a new approval queue.
48
- #
49
- # @param on_approve [Proc, nil] called with an {Action} when it is
50
- # approved and applied. The session wires this to execute the real
51
- # tool call.
52
- # @param on_reject [Proc, nil] called with an {Action} when it is
53
- # rejected. The session wires this to notify the conversation.
54
- # @param auto_approve [Hash{String => Boolean}, nil] user-enabled
55
- # auto-approval rules keyed by tool name. An action is auto-applied
56
- # only when its tool is listed here with +true+ AND the action itself
57
- # is marked auto_approvable.
58
- # @!attribute [rw] on_approve
59
- # Called with an {Action} when it is approved and applied. The
60
- # session wires this to execute the real tool call; can be replaced
61
- # after construction (e.g. by queue subclasses that also emit events).
62
- # @!attribute [rw] on_reject
63
- # Called with an {Action} when it is rejected.
64
- # @!attribute [rw] on_submit
65
- # Called with the new {Action} when it is submitted — BEFORE the
66
- # auto-approval drain runs, so subscribers can register the pending
67
- # call before it is applied. The session wires this to register the
68
- # pending tool call, closing the race where an approval lands while
69
- # the executor is still in flight.
70
- attr_accessor :on_approve, :on_reject, :on_submit
71
-
72
- def initialize(on_approve: nil, on_reject: nil, auto_approve: nil, on_submit: nil)
73
- @on_approve = on_approve
74
- @on_reject = on_reject
75
- @on_submit = on_submit
76
- @auto_approve = auto_approve || {}
77
- @actions = {}
78
- @next_id = 1
79
- @mutex = Mutex.new
80
- @draining = false
81
- end
82
-
83
- # Queue a tool action for approval.
84
- #
85
- # @param tool_call_id [String] original tool call id from the LLM
86
- # @param tool_name [String] tool name
87
- # @param args [Hash] arguments for the tool
88
- # @param auto_approvable [Boolean] whether the tool permits auto-approval
89
- # @param message [String, nil] human-readable description
90
- # @return [Integer] the action id
91
- def submit(tool_call_id:, tool_name:, args: {}, auto_approvable: false, message: nil)
92
- action = @mutex.synchronize do
93
- action = Action.new(
94
- id: @next_id,
95
- tool_call_id: tool_call_id,
96
- tool_name: tool_name,
97
- args: args || {},
98
- auto_approvable: auto_approvable,
99
- status: :pending,
100
- submitted_at: Time.now,
101
- message: message
102
- )
103
- @next_id += 1
104
- @actions[action.id] = action
105
- action
106
- end
107
-
108
- # Notify BEFORE the drain: an auto-approvable action is applied
109
- # (and possibly completed) inside drain, and listeners need to
110
- # observe the submission first.
111
- @on_submit&.call(action)
112
-
113
- drain
114
- action.id
115
- end
116
-
117
- # All actions still awaiting a decision, in id order.
118
- #
119
- # @return [Array<Action>]
120
- def pending_actions
121
- @mutex.synchronize do
122
- @actions.values.select { |a| a.status == :pending }.sort_by(&:id)
123
- end
124
- end
125
-
126
- # @param id [Integer]
127
- # @return [Boolean] whether the action is still awaiting a decision
128
- def pending?(id)
129
- @mutex.synchronize do
130
- a = @actions[id]
131
- a && a.status == :pending
132
- end
133
- end
134
-
135
- # @return [Boolean] true while at least one action awaits a decision
136
- def any_pending?
137
- pending_actions.any?
138
- end
139
-
140
- # Look up an action by id.
141
- #
142
- # @param id [Integer]
143
- # @return [Action, nil]
144
- def [](id)
145
- @mutex.synchronize { @actions[id] }
146
- end
147
-
148
- # Approve specific actions (by id), applying them in id order.
149
- #
150
- # @param ids [Array<Integer>]
151
- # @return [Array<Action>] the actions that were approved
152
- def approve(*ids)
153
- actions = ids.flatten.filter_map { |id| @mutex.synchronize { @actions[id] } }
154
- actions.select! { |a| a.status == :pending }
155
- actions.sort_by!(&:id)
156
- actions.each { |a| apply(a) }
157
- actions
158
- end
159
-
160
- # Reject specific actions (by id).
161
- #
162
- # @param ids [Array<Integer>]
163
- # @return [Array<Action>] the actions that were rejected
164
- def reject(*ids)
165
- actions = ids.flatten.filter_map { |id| @mutex.synchronize { @actions[id] } }
166
- actions.select! { |a| a.status == :pending }
167
- actions.sort_by!(&:id)
168
- actions.each { |a| reject_action(a) }
169
- actions
170
- end
171
-
172
- # Approve all currently pending actions, in id order.
173
- #
174
- # @return [Array<Action>]
175
- def approve_all
176
- approve(*pending_actions.map(&:id))
177
- end
178
-
179
- # Reject all currently pending actions, in id order.
180
- #
181
- # @return [Array<Action>]
182
- def reject_all
183
- reject(*pending_actions.map(&:id))
184
- end
185
-
186
- private
187
-
188
- # Apply eligible pending actions in id order, stopping at the first
189
- # action that is NOT auto-eligible (a manual gate) — nothing is
190
- # silently applied past a human review point. Single-flight guard so
191
- # concurrent drains cannot double-apply.
192
- def drain
193
- return if @mutex.synchronize { @draining }
194
- @mutex.synchronize { @draining = true }
195
-
196
- begin
197
- loop do
198
- action = @mutex.synchronize do
199
- @actions.values.select { |a| a.status == :pending }
200
- .sort_by(&:id)
201
- .first
202
- end
203
- break unless action
204
-
205
- if auto_approvable?(action)
206
- apply(action)
207
- else
208
- break # manual gate — stop, never skip ahead
209
- end
210
- end
211
- ensure
212
- @mutex.synchronize { @draining = false }
213
- end
214
- end
215
-
216
- def auto_approvable?(action)
217
- action.auto_approvable && @auto_approve[action.tool_name] == true
218
- end
219
-
220
- def apply(action)
221
- claimed = @mutex.synchronize do
222
- return false unless action.status == :pending
223
- @actions[action.id] = action.with(status: :applying)
224
- end
225
- @on_approve&.call(claimed)
226
- @mutex.synchronize do
227
- @actions[action.id] = claimed.with(status: :approved)
228
- end
229
- true
230
- rescue StandardError
231
- # Failed apply: leave the action pending so the user can retry or
232
- # reject it. Re-raise so the caller (approve/drain) surfaces it.
233
- @mutex.synchronize do
234
- @actions[action.id] = action.with(status: :pending) if @actions[action.id]&.status == :applying
235
- end
236
- raise
237
- end
238
-
239
- def reject_action(action)
240
- return false unless @mutex.synchronize { action.status == :pending }
241
- @on_reject&.call(action)
242
- @mutex.synchronize do
243
- @actions[action.id] = action.with(status: :rejected)
244
- end
245
- true
246
- end
247
- end
248
- end
249
- end
@@ -1,114 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ask
4
- module Agent
5
- module Policies
6
- # Approval policy hook: classifies tool calls as approval-required and
7
- # routes them into an {Ask::Agent::ApprovalQueue}.
8
- #
9
- # Wire it as a +before_tool+ hook on a Session (or enable it with
10
- # `Session.new(approval: true)`), and any tool whose class declares
11
- # `approval_required true` — or whose name matches the policy's
12
- # rule-based lists — is queued for human approval instead of executed.
13
- # The agent receives a pending result and continues; the tool runs only
14
- # after a human approves it.
15
- #
16
- # @example
17
- # queue = Ask::Agent::ApprovalQueue.new
18
- # policy = Ask::Agent::Policies::ApprovalPolicy.new(queue: queue)
19
- # session = Ask::Agent::Session.new(
20
- # model: "gpt-4o",
21
- # tools: [SendEmail],
22
- # hooks: { before_tool: [policy.method(:before_tool_call)] }
23
- # )
24
- #
25
- # # Later, when the user decides:
26
- # session.approval_queue.approve_all
27
- class ApprovalPolicy
28
- # @param queue [Ask::Agent::ApprovalQueue] the queue actions go into.
29
- # The queue owns the user-enabled auto-approval rules; this policy
30
- # only reports each tool's own declaration.
31
- # @param require_approval [Array<String, Regexp>, :all, nil] extra
32
- # rule-based classification on top of the tool's own declaration.
33
- # Strings match tool names exactly, Regexps match against the name,
34
- # and :all requires approval for every tool call.
35
- # @param rules [Ask::Agent::Policies::PermissionRules, nil] persisted
36
- # allow/ask/deny patterns. Rules classify first and take precedence
37
- # over tool declarations: :deny blocks, :allow proceeds without the
38
- # queue, :ask queues regardless of auto-approvable.
39
- # @param tools [Array<Object>, nil] the session's resolved tool
40
- # instances, used to read class-level declarations
41
- # (`approval_required`, `auto_approvable`). When nil, only the
42
- # rule-based lists classify calls.
43
- def initialize(queue:, require_approval: nil, rules: nil, tools: nil)
44
- @queue = queue
45
- @require_approval = require_approval
46
- @rules = rules
47
- @tools = Array(tools)
48
- end
49
-
50
- # Hook entry point — matches the +before_tool+ hook signature.
51
- #
52
- # @param tool_call [Ask::Agent::ToolCallInfo]
53
- # @param _context [Hash]
54
- # @return [Hash] {action: :proceed} to run, {action: :pending,
55
- # action_id:, reason:} to queue for approval, or {action: :block,
56
- # reason:} to refuse
57
- def before_tool_call(tool_call, _context)
58
- case @rules&.classify(tool_call.name, tool_call.arguments)
59
- when :deny
60
- return { action: :block, reason: "Denied by permission rules: '#{tool_call.name}'" }
61
- when :allow
62
- return { action: :proceed }
63
- when :ask
64
- return queue_for_approval(tool_call, auto_approvable: false)
65
- end
66
-
67
- return { action: :proceed } unless approval_required?(tool_call.name)
68
-
69
- queue_for_approval(tool_call, auto_approvable: tool_auto_approvable?(tool_call.name))
70
- end
71
-
72
- private
73
-
74
- def queue_for_approval(tool_call, auto_approvable:)
75
- action_id = @queue.submit(
76
- tool_call_id: tool_call.id,
77
- tool_name: tool_call.name,
78
- args: tool_call.arguments,
79
- auto_approvable: auto_approvable,
80
- message: "Calling \"#{tool_call.name}\" requires approval"
81
- )
82
-
83
- { action: :pending, action_id: action_id, reason: "Tool '#{tool_call.name}' requires approval" }
84
- end
85
-
86
- def approval_required?(tool_name)
87
- return true if @require_approval == :all
88
-
89
- rule_matches?(@require_approval, tool_name) || tool_declares_approval?(tool_name)
90
- end
91
-
92
- def rule_matches?(rules, tool_name)
93
- Array(rules).any? do |rule|
94
- rule == tool_name || (rule.is_a?(Regexp) && rule.match?(tool_name))
95
- end
96
- end
97
-
98
- def tool_declares_approval?(tool_name)
99
- tool = tool_for(tool_name)
100
- tool&.respond_to?(:approval_required?) && tool.approval_required?
101
- end
102
-
103
- def tool_auto_approvable?(tool_name)
104
- tool = tool_for(tool_name)
105
- tool&.respond_to?(:auto_approvable?) && tool.auto_approvable?
106
- end
107
-
108
- def tool_for(tool_name)
109
- @tools.find { |t| t.respond_to?(:name) && t.name == tool_name }
110
- end
111
- end
112
- end
113
- end
114
- end