ask-decisions 0.2.2 → 0.2.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6d66ad26316914f34f043aa0ac1cad2ebd0756db251bb893475d9ab41b70749
4
- data.tar.gz: 0663d597f9a6781923be49c80642b0bb70689c11c9f4557497880cfc8128477b
3
+ metadata.gz: e64f40bca45e7b9bc9a11f33fabe1e178c30cfd9e61f4460395b77eb8092cc5a
4
+ data.tar.gz: 4e32792a9c4bdc411731beb94d9a078e98d92f3135aaa4da779090fd6c4f96c9
5
5
  SHA512:
6
- metadata.gz: e6dd919a98dd01107d2ea456c653ffbdf31d5580a2d02a19fe0769ffffa32bf2285b5547f3e0f63b1c40d3fb6de32e65ee1371a14127ed9b367a39e5cd1d022a
7
- data.tar.gz: '0493127671b11ccaa3306ce7efea04c11b9395ca5d63b61ee50e9894e45738e0170dfc3ea5c7082566d25d675bd5311795c1f08f98621dcd8538f353a4f961e1'
6
+ metadata.gz: e6338ccd1b049f19612e7b33d4e7dbedce5192d4789c29ac62bf6fa075107fa7184764b71c3df9e0edc6284e8d9ed0013225d5ee84a6be9858b5d3ca49d2b874
7
+ data.tar.gz: f9c69aca54eaae484d182ab78c7265de776fafff15e5b4813908028dac1857e8df4f85428fc9474282dd54b4418a6c9981e8cb5e76673f028593daff8be3102b
data/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [0.2.4] - 2026-09-18
8
+
9
+ ### Fixed
10
+
11
+ - **`resolve_provider` memoizes the provider instance.** Every call to
12
+ `resolve_provider` previously created a new `Typesafe` (or other provider)
13
+ object. In a pipeline with 10+ decision calls per cycle, this meant 10+
14
+ redundant allocations with identical configuration. The provider is now
15
+ cached for the lifetime of the process — it holds only read-only state
16
+ (API key, base URL, model, timeout) set once at boot.
17
+
18
+ ## [0.2.3] - 2026-09-18
19
+
20
+ ### Added
21
+
22
+ - **`Ask::Decisions::ThresholdJudge`** — binary accept/reject judge for
23
+ simple gate decisions. Runs a single Noul question and thresholds the
24
+ result. Use this when you need "does this pass?" without the full
25
+ rubric of `QualityJudge`. Returns a `ThresholdVerdict` with `passed?`,
26
+ `noul`, and `confidence`.
27
+
7
28
  ## [0.2.2] - 2026-09-18
8
29
 
9
30
  ### Fixed
@@ -44,6 +44,16 @@ module Ask
44
44
  @tool_repairer = ToolRepairer.new(@provider)
45
45
  end
46
46
 
47
+ # Build a decision-based compactor for the current provider.
48
+ #
49
+ # compactor = adapter.build_compactor(preserve_recent: 6)
50
+ # result = compactor.compact(session.messages)
51
+ # result.messages # => pruned conversation
52
+ #
53
+ def build_compactor(**opts)
54
+ Ask::Decisions::Compactor.new(@provider, **opts)
55
+ end
56
+
47
57
  # Generate before_tool hooks for ask-agent.
48
58
  # Returns an array of callables that match the Hooks interface.
49
59
  def before_tool_hooks
@@ -0,0 +1,424 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Decisions
5
+ # Decision-based conversation compactor. Replaces lossy summarization with
6
+ # Jev-scored pruning: every tool call and result is evaluated for relevance,
7
+ # and the irrelevant ones are dropped or truncated — everything kept stays
8
+ # verbatim.
9
+ #
10
+ # This is the Ruby equivalent of the fast-jev-compaction approach: rather
11
+ # than asking an LLM to summarize old turns (which loses file paths, exact
12
+ # errors, constraints, and command details), Jev scores each tool call and
13
+ # result independently, and the compactor applies three-tier decisions.
14
+ #
15
+ # compactor = Ask::Decisions::Compactor.new(provider)
16
+ # result = compactor.compact(messages)
17
+ # result.messages # => pruned message list
18
+ # result.stats # => { kept: 12, dropped: 5, truncated: 3, ... }
19
+ # result.reduction_ratio # => 0.29 (29% of tool content removed)
20
+ #
21
+ # Message format (hashes):
22
+ #
23
+ # { role: "user", content: "Fix the failing test" }
24
+ # { role: "assistant", content: "",
25
+ # tool_calls: [{ id: "call_1", name: "Read", input: { path: "src/a.rb" } }] }
26
+ # { role: "tool", content: "file contents...",
27
+ # tool_call_id: "call_1" }
28
+ #
29
+ # The first message is always kept. The most recent +preserve_recent+
30
+ # messages are never touched. Everything else is a candidate for pruning.
31
+ #
32
+ class Compactor
33
+ # Approximate chars per token for budget calculations.
34
+ CHARS_PER_TOKEN = 4
35
+
36
+ # Jev Noul questions asked per tool call.
37
+ CALL_QUESTION = "Should this tool call be kept — does knowing it was " \
38
+ "made still matter for the current conversation?"
39
+ RESULT_QUESTION = "Should this tool result be kept verbatim — are its " \
40
+ "contents still needed and would re-running the tool " \
41
+ "not suffice?"
42
+
43
+ # @param provider [Ask::DecisionProvider] the Jev/decision provider
44
+ # @param keep_threshold [Float] minimum Noul probability to keep (0.0–1.0)
45
+ # @param preserve_recent [Integer] newest messages never touched
46
+ # @param max_state_tokens [Integer] token ceiling for state sent to Jev
47
+ # @param max_request_tokens [Integer] token ceiling per batch request
48
+ # @param truncate_head_chars [Integer] chars retained from dropped results
49
+ # @param goal [String, nil] ongoing task description for Jev context
50
+ def initialize(provider,
51
+ keep_threshold: 0.5,
52
+ preserve_recent: 4,
53
+ max_state_tokens: 25_000,
54
+ max_request_tokens: 30_000,
55
+ truncate_head_chars: 300,
56
+ goal: nil)
57
+ @provider = provider
58
+ @keep_threshold = keep_threshold
59
+ @preserve_recent = preserve_recent
60
+ @max_state_tokens = max_state_tokens
61
+ @max_request_tokens = max_request_tokens
62
+ @truncate_head_chars = truncate_head_chars
63
+ @goal = goal
64
+ end
65
+
66
+ # Compact a conversation by scoring every tool call and result with Jev,
67
+ # then dropping or truncating the irrelevant ones.
68
+ #
69
+ # @param messages [Array<Hash>] conversation messages
70
+ # @return [Result] compacted messages and stats
71
+ def compact(messages)
72
+ messages = normalize(messages)
73
+ return Result.new(messages.dup, original_count: messages.size) if messages.size < 2
74
+
75
+ pairs = build_pairs(messages)
76
+ pinned = compute_pinned(messages)
77
+
78
+ return Result.new(messages.dup, original_count: messages.size) if pairs.empty?
79
+
80
+ state = build_state(messages, pairs)
81
+ questions = build_questions(pairs, pinned)
82
+ batch = batch_and_execute(state, questions)
83
+ decisions = extract_decisions(batch, pairs)
84
+ pruned = apply_decisions(messages, decisions, pinned)
85
+ stats = compute_stats(messages, pruned, decisions)
86
+
87
+ Result.new(pruned, stats: stats, original_count: messages.size)
88
+ end
89
+
90
+ private
91
+
92
+ # ── Normalization ────────────────────────────────────────────────
93
+
94
+ def normalize(messages)
95
+ Array(messages).map { |m| m.is_a?(Hash) ? m : hashify(m) }
96
+ end
97
+
98
+ def hashify(msg)
99
+ {
100
+ role: msg.respond_to?(:role) ? msg.role.to_s : "unknown",
101
+ content: msg.respond_to?(:content) ? msg.content.to_s : msg.to_s,
102
+ tool_calls: extract_tool_calls_from_object(msg),
103
+ tool_call_id: msg.respond_to?(:tool_call_id) ? msg.tool_call_id : nil
104
+ }.compact
105
+ end
106
+
107
+ def extract_tool_calls_from_object(msg)
108
+ return nil unless msg.respond_to?(:tool_calls) && msg.tool_calls
109
+ Array(msg.tool_calls).map do |tc|
110
+ {
111
+ id: tc.respond_to?(:id) ? tc.id : tc[:id],
112
+ name: tc.respond_to?(:name) ? tc.name : tc[:name],
113
+ input: tc.respond_to?(:arguments) ? tc.arguments : (tc[:input] || tc[:arguments])
114
+ }
115
+ end
116
+ end
117
+
118
+ # ── Pairing ──────────────────────────────────────────────────────
119
+
120
+ # Group messages into tool_call / tool_result pairs by tool_call_id.
121
+ # Returns an array of { call_msg:, result_msg:, call: } hashes.
122
+ def build_pairs(messages)
123
+ call_index = {}
124
+ messages.each_with_index do |msg, idx|
125
+ Array(msg[:tool_calls]).each do |tc|
126
+ call_index[tc[:id]] = { call_msg_idx: idx, call: tc }
127
+ end
128
+ end
129
+
130
+ pairs = []
131
+ messages.each_with_index do |msg, idx|
132
+ tcid = msg[:tool_call_id]
133
+ next unless tcid && call_index[tcid]
134
+
135
+ entry = call_index[tcid]
136
+ pairs << {
137
+ call_msg_idx: entry[:call_msg_idx],
138
+ result_msg_idx: idx,
139
+ call: entry[:call],
140
+ call_id: tcid
141
+ }
142
+ end
143
+
144
+ pairs.sort_by { |p| p[:call_msg_idx] }
145
+ end
146
+
147
+ # First message and the most recent +preserve_recent+ messages are pinned.
148
+ # Pinned messages are never removed, but their tool calls are still
149
+ # evaluated by Jev — a recent tool result can still be irrelevant.
150
+ def compute_pinned(messages)
151
+ pinned = Set.new([0])
152
+ start = [messages.size - @preserve_recent, 1].max
153
+ (start...messages.size).each { |i| pinned.add(i) }
154
+ pinned
155
+ end
156
+
157
+ # Whether a message index is safe to remove (not pinned).
158
+ def removable?(idx, pinned)
159
+ !pinned.include?(idx)
160
+ end
161
+
162
+ # ── State ────────────────────────────────────────────────────────
163
+
164
+ # Build the state Jev sees: full conversation with tool results replaced
165
+ # by short placeholder notes. Tool inputs are included, text is included,
166
+ # nothing is summarized.
167
+ def build_state(messages, pairs)
168
+ result_index = {}
169
+ pairs.each { |p| result_index[p[:result_msg_idx]] = p }
170
+
171
+ lines = messages.each_with_index.map do |msg, idx|
172
+ content = msg[:content].to_s
173
+
174
+ if result_index[idx]
175
+ result_index[idx][:result_preview] = content
176
+ chars = content.length
177
+ "[tool result: ok, #{chars} chars (omitted)]"
178
+ elsif msg[:tool_calls] && !msg[:tool_calls].empty?
179
+ tc_strs = msg[:tool_calls].map do |tc|
180
+ input_str = format_input(tc[:input])
181
+ "#{tc[:name]}(#{input_str})"
182
+ end
183
+ tc_strs.empty? ? content : "#{content}\n#{tc_strs.join("\n")}"
184
+ else
185
+ content
186
+ end
187
+ end
188
+
189
+ { conversation: lines, goal: @goal }.compact
190
+ end
191
+
192
+ def format_input(input)
193
+ return "" unless input
194
+ str = input.is_a?(String) ? input : JSON.generate(input)
195
+ str.length > 100 ? "#{str[0, 100]}..." : str
196
+ end
197
+
198
+ # ── Questions ────────────────────────────────────────────────────
199
+
200
+ def build_questions(pairs, pinned)
201
+ questions = {}
202
+ pairs.each do |pair|
203
+ # Skip pairs where both the call and result live in pinned messages.
204
+ # Pinned messages are never touched — Jev does not score them.
205
+ next if pinned.include?(pair[:call_msg_idx]) && pinned.include?(pair[:result_msg_idx])
206
+
207
+ call_id = pair[:call_id]
208
+ tc = pair[:call]
209
+ input_str = format_input(tc[:input])
210
+
211
+ questions["keep_call_#{call_id}"] = Ask::Decision::Noul.new(
212
+ instructions: "#{CALL_QUESTION}\n\nTool: #{tc[:name]}\nInput: #{input_str}"
213
+ )
214
+ questions["keep_result_#{call_id}"] = Ask::Decision::Noul.new(
215
+ instructions: "#{RESULT_QUESTION}\n\nTool: #{tc[:name]}\nInput: #{input_str}"
216
+ )
217
+ end
218
+ questions
219
+ end
220
+
221
+ # ── Batching ─────────────────────────────────────────────────────
222
+
223
+ # Split questions into batches that fit the request token budget,
224
+ # send each batch concurrently (sequentially in Ruby, but the same
225
+ # pattern as fast-jev-compaction), and merge results.
226
+ def batch_and_execute(state, questions)
227
+ return empty_batch if questions.empty?
228
+
229
+ batches = partition_batches(state, questions)
230
+ return execute_batch(state, questions) if batches.size <= 1
231
+
232
+ results = batches.map { |batch| execute_batch(state, batch) }
233
+ merge_batches(results)
234
+ end
235
+
236
+ def partition_batches(state, questions)
237
+ state_tokens = estimate_tokens(JSON.generate(state))
238
+ pairs = questions.to_a
239
+ batches = []
240
+ current = {}
241
+ current_tokens = state_tokens
242
+
243
+ pairs.each do |id, decision|
244
+ q_tokens = estimate_tokens(JSON.generate(decision.to_h))
245
+ if current_tokens + q_tokens > @max_request_tokens && current.any?
246
+ batches << current
247
+ current = {}
248
+ current_tokens = state_tokens
249
+ end
250
+ current[id] = decision
251
+ current_tokens += q_tokens
252
+ end
253
+
254
+ batches << current if current.any?
255
+ batches
256
+ end
257
+
258
+ def execute_batch(state, questions)
259
+ @provider.evaluate(
260
+ state: JSON.generate(state),
261
+ decisions: questions
262
+ )
263
+ end
264
+
265
+ def empty_batch
266
+ Ask::DecisionResult::Batch.new(answers: {})
267
+ end
268
+
269
+ def merge_batches(batches)
270
+ merged = {}
271
+ batches.each do |batch|
272
+ batch.answers.each { |k, v| merged[k] = v }
273
+ end
274
+ Ask::DecisionResult::Batch.new(answers: merged)
275
+ end
276
+
277
+ # ── Decisions ────────────────────────────────────────────────────
278
+
279
+ def extract_decisions(batch, pairs)
280
+ decisions = {}
281
+ pairs.each do |pair|
282
+ call_id = pair[:call_id]
283
+ keep_call_answer = batch["keep_call_#{call_id}"]
284
+ keep_result_answer = batch["keep_result_#{call_id}"]
285
+
286
+ keep_call = keep_call_answer&.noul || 0.0
287
+ keep_result = keep_result_answer&.noul || 0.0
288
+
289
+ decisions[call_id] = {
290
+ keep_call: keep_call,
291
+ keep_result: keep_result,
292
+ action: classify_action(keep_call, keep_result)
293
+ }
294
+ end
295
+ decisions
296
+ end
297
+
298
+ # Three-tier decision classification.
299
+ def classify_action(keep_call, keep_result)
300
+ if keep_result >= @keep_threshold
301
+ :keep
302
+ elsif keep_call >= @keep_threshold
303
+ :truncate
304
+ else
305
+ :drop
306
+ end
307
+ end
308
+
309
+ # ── Apply ────────────────────────────────────────────────────────
310
+
311
+ def apply_decisions(messages, decisions, pinned)
312
+ pairs = build_pairs(messages)
313
+ remove_indices = Set.new
314
+ truncate_indices = {}
315
+
316
+ pairs.each do |pair|
317
+ decision = decisions[pair[:call_id]]
318
+ next unless decision
319
+
320
+ case decision[:action]
321
+ when :drop
322
+ # Only remove messages that are not pinned.
323
+ if removable?(pair[:call_msg_idx], pinned)
324
+ remove_indices.add(pair[:call_msg_idx])
325
+ end
326
+ if removable?(pair[:result_msg_idx], pinned)
327
+ remove_indices.add(pair[:result_msg_idx])
328
+ end
329
+ when :truncate
330
+ truncate_indices[pair[:result_msg_idx]] = pair[:call]
331
+ end
332
+ end
333
+
334
+ messages.each_with_index.filter_map do |msg, idx|
335
+ next if remove_indices.include?(idx)
336
+
337
+ if truncate_indices[idx]
338
+ truncate_message(msg, truncate_indices[idx])
339
+ else
340
+ msg
341
+ end
342
+ end
343
+ end
344
+
345
+ def truncate_message(msg, call)
346
+ original = msg[:content].to_s
347
+ return msg if original.length <= @truncate_head_chars
348
+
349
+ head = original[0, @truncate_head_chars]
350
+ truncated_content = "#{head}\n...[result truncated: #{original.length - @truncate_head_chars} chars omitted — #{call[:name]} result was kept as call-only]"
351
+ msg.merge(content: truncated_content)
352
+ end
353
+
354
+ # ── Stats ────────────────────────────────────────────────────────
355
+
356
+ def compute_stats(original, pruned, decisions)
357
+ action_counts = decisions.values.map { |d| d[:action] }.tally
358
+ original_chars = original.sum { |m| m[:content].to_s.length }
359
+ pruned_chars = pruned.sum { |m| m[:content].to_s.length }
360
+
361
+ {
362
+ messages_before: original.size,
363
+ messages_after: pruned.size,
364
+ kept: action_counts[:keep] || 0,
365
+ truncated: action_counts[:truncate] || 0,
366
+ dropped: action_counts[:drop] || 0,
367
+ tool_pairs_evaluated: decisions.size,
368
+ chars_before: original_chars,
369
+ chars_after: pruned_chars,
370
+ reduction_ratio: original_chars > 0 ? (1.0 - pruned_chars.to_f / original_chars) : 0.0
371
+ }
372
+ end
373
+
374
+ # ── Token estimation ─────────────────────────────────────────────
375
+
376
+ def estimate_tokens(text)
377
+ str = text.to_s
378
+ letters = str.count("a-zA-Z")
379
+ digits = str.count("0-9")
380
+ others = str.length - letters - digits
381
+ (letters / 6.0 + digits / 2.0 + others).ceil
382
+ end
383
+
384
+ # ── Batch result ─────────────────────────────────────────────────
385
+
386
+ def extract_call_id(answer_id, prefix)
387
+ answer_id.to_s.sub(/\A#{prefix}/, "")
388
+ end
389
+
390
+ # ── Result ───────────────────────────────────────────────────────
391
+
392
+ # Holds the compacted messages and compaction statistics.
393
+ class Result
394
+ attr_reader :messages, :stats, :original_count
395
+
396
+ def initialize(messages, stats: {}, original_count: nil)
397
+ @messages = messages
398
+ @stats = stats
399
+ @original_count = original_count || messages.size
400
+ end
401
+
402
+ # Fraction of tool content removed (0.0 = nothing removed, 1.0 = everything).
403
+ def reduction_ratio
404
+ stats[:reduction_ratio] || 0.0
405
+ end
406
+
407
+ # Whether any tool calls were dropped or truncated.
408
+ def compacted?
409
+ (stats[:dropped] || 0) > 0 || (stats[:truncated] || 0) > 0
410
+ end
411
+
412
+ def to_s
413
+ if compacted?
414
+ "compacted #{stats[:messages_before]}→#{stats[:messages_after]} messages " \
415
+ "(#{stats[:dropped]} dropped, #{stats[:truncated]} truncated, " \
416
+ "#{(reduction_ratio * 100).round(1)}% reduction)"
417
+ else
418
+ "no compaction needed (#{original_count} messages)"
419
+ end
420
+ end
421
+ end
422
+ end
423
+ end
424
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Decisions
5
+ # Binary accept/reject judge — simpler than QualityJudge when you
6
+ # just need "does this pass?" without a full rubric. Runs a single
7
+ # Noul question and thresholds the result.
8
+ #
9
+ # judge = Ask::Decisions::ThresholdJudge.new(provider)
10
+ # verdict = judge.evaluate(
11
+ # subject: "The code changes look correct and have tests.",
12
+ # question: "Is this code change safe to merge?"
13
+ # )
14
+ # verdict.passed? # => true
15
+ # verdict.confidence # => 0.92
16
+ #
17
+ # Use QualityJudge when you need multi-dimensional scoring.
18
+ # Use ThresholdJudge when you need a fast binary gate.
19
+ #
20
+ class ThresholdJudge
21
+ # @param provider [Ask::DecisionProvider]
22
+ def initialize(provider)
23
+ @provider = provider
24
+ end
25
+
26
+ # Evaluate a subject against a yes/no question.
27
+ #
28
+ # @param subject [String] the content to evaluate
29
+ # @param question [String] the yes/no question
30
+ # @param threshold [Float] minimum noul value to pass (default 0.7)
31
+ # @return [ThresholdVerdict]
32
+ def evaluate(subject:, question:, threshold: 0.7)
33
+ decisions = {
34
+ "verdict" => Ask::Decision::Noul.new(
35
+ instructions: question
36
+ )
37
+ }
38
+
39
+ result = @provider.evaluate(state: subject, decisions: decisions)
40
+ ThresholdVerdict.new(result, threshold)
41
+ end
42
+
43
+ # Verdict from threshold evaluation.
44
+ class ThresholdVerdict
45
+ attr_reader :result, :threshold
46
+
47
+ def initialize(result, threshold)
48
+ @result = result
49
+ @threshold = threshold
50
+ end
51
+
52
+ # The raw noul value (0–1, where 1 = strong yes).
53
+ def noul
54
+ answer = @result["verdict"]
55
+ answer&.noul || 0.0
56
+ end
57
+
58
+ # Did the subject pass the threshold?
59
+ def passed?
60
+ noul >= @threshold
61
+ end
62
+
63
+ # Confidence: distance from 0.5 (0 = uncertain, 0.5 = certain).
64
+ def confidence
65
+ (noul - 0.5).abs
66
+ end
67
+
68
+ def to_s
69
+ status = passed? ? "PASS" : "FAIL"
70
+ "#{status} (noul: #{('%.2f' % noul)}, confidence: #{('%.2f' % confidence)})"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Decisions
5
- VERSION = "0.2.2"
5
+ VERSION = "0.2.4"
6
6
  end
7
7
  end
data/lib/ask-decisions.rb CHANGED
@@ -21,12 +21,14 @@ require_relative "ask/decisions/failure_classifier"
21
21
  require_relative "ask/decisions/loop_detector"
22
22
  require_relative "ask/decisions/confidence_policy"
23
23
  require_relative "ask/decisions/quality_judge"
24
+ require_relative "ask/decisions/threshold_judge"
24
25
  require_relative "ask/decisions/reflection_judge"
25
26
  require_relative "ask/decisions/tool_repairer"
26
27
  require_relative "ask/decisions/reranker"
27
28
  require_relative "ask/decisions/structured_state_loop"
28
29
  require_relative "ask/decisions/calibration_report"
29
30
  require_relative "ask/decisions/calibration_harness"
31
+ require_relative "ask/decisions/compactor"
30
32
 
31
33
  # The Decide tool requires ask-tools (Ask::Tool base class). Load conditionally
32
34
  # so the core gem works without a tools dependency.
@@ -120,13 +122,19 @@ module Ask
120
122
  end
121
123
 
122
124
  # Resolve a provider by name, raising on unknown.
125
+ # Memoized: the provider is stateless after boot (API key,
126
+ # base URL, model, timeout are all read-only), so one instance
127
+ # per configuration lifetime is safe and avoids redundant
128
+ # allocations across the pipeline.
123
129
  def resolve_provider(name)
124
- klass = Ask::DecisionProvider.resolve(name)
125
-
126
- if configuration.provider_options.any?
127
- klass.new(**configuration.provider_options)
128
- else
129
- klass.new
130
+ @resolved_provider ||= begin
131
+ klass = Ask::DecisionProvider.resolve(name)
132
+
133
+ if configuration.provider_options.any?
134
+ klass.new(**configuration.provider_options)
135
+ else
136
+ klass.new
137
+ end
130
138
  end
131
139
  end
132
140
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-decisions
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -154,6 +154,7 @@ files:
154
154
  - lib/ask/decisions/cache.rb
155
155
  - lib/ask/decisions/calibration_harness.rb
156
156
  - lib/ask/decisions/calibration_report.rb
157
+ - lib/ask/decisions/compactor.rb
157
158
  - lib/ask/decisions/confidence_policy.rb
158
159
  - lib/ask/decisions/decision_state.rb
159
160
  - lib/ask/decisions/failure_classifier.rb
@@ -168,6 +169,7 @@ files:
168
169
  - lib/ask/decisions/reranker.rb
169
170
  - lib/ask/decisions/static.rb
170
171
  - lib/ask/decisions/structured_state_loop.rb
172
+ - lib/ask/decisions/threshold_judge.rb
171
173
  - lib/ask/decisions/tool_repairer.rb
172
174
  - lib/ask/decisions/triage.rb
173
175
  - lib/ask/decisions/typesafe.rb