ask-agent 0.12.0 → 0.14.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb1a00c9b6f32029b7d27271482fe82dfa7333601776537a38160635bf4fde0b
4
- data.tar.gz: 80d666d9485e6e058c9a81d20fdbfa48b4b196a33d1675655fbbd7f6ffea9240
3
+ metadata.gz: c80f27cbdec5b3808533dba4861762e19e4ef81c084be099a4a7eb0b2169170e
4
+ data.tar.gz: 34e5388035a8f216df468039bf8c7e304296d7661832e8109df4d76fbb527193
5
5
  SHA512:
6
- metadata.gz: d7ef76f83baba0df18a1f60a6a00aae8cefb0b191ccf6eb0d5b680ed68e0c305dde891591673805b28bec5705ae632da2bd6acc4bd5810602a3b04878483f890
7
- data.tar.gz: 639f577894d439805d0f06d988c4b528241f82b31656dea7b9a28ba54eca173c4262e3a48b865705bfb0ae153653c9a9c4e4ac6fc3cd7cb8fe02c92efe4495f9
6
+ metadata.gz: bb1bf1b6aac7a61a650291f9dcabd781c0a664cb09d97c7f2cc2fcdcfaa035b71adef2c423ebacfaae0de04d0524b4dff649f503921006837e0644d9a9f86615
7
+ data.tar.gz: d718885adebbaf33f2b9a2b95704968494d14e3080746a58da37c9518af475ac09103357014e5bea98339d56150ce63a162c6ac9a9c8ae4cffa07f13716b8793
data/CHANGELOG.md CHANGED
@@ -1,3 +1,30 @@
1
+ ## [0.14.0] — 2026-07-23
2
+
3
+ ### Added
4
+
5
+ - **`state:` keyword on Session** — Accepts any `Ask::State::Adapter` directly. Sessions persist conversation state, tool results, and metadata. Replaces `persistence:` keyword (still supported for backward compatibility).
6
+ - **Per-turn persistence** — Session now persists after every LLM turn, not just at the end of `run()`. Mid-session crashes no longer lose progress.
7
+ - **`Session.load` restores `@messages`** — Previously `session.messages` returned `nil` after loading. Now it's populated from the restored chat messages.
8
+ - **`Ask::State::Adapter#clear`** — Abstract method added to the adapter contract. Memory adapter implements it.
9
+
10
+ ### Changed
11
+
12
+ - **Session behind the scenes now uses `@state.set`/`@state.get`/`@state.delete`** instead of the old `@persistence.save`/`@persistence.load`/`@persistence.delete`. Custom adapters must respond to `set`/`get`/`delete`.
13
+
14
+ ## [0.13.0] — 2026-07-23
15
+
16
+ ### Added
17
+
18
+ - **ModelFallback middleware** — Switches to a fallback model+provider when the primary LLM call fails with a rate limit, server error, or service unavailable. Supports static and dynamic (lambda-based) fallback lists. Credentials resolve automatically via `Ask::Auth`.
19
+ - Static fallbacks: ordered list of `{ model:, provider: }` hashes
20
+ - Dynamic fallbacks: lambda receiving `(error, request)` returning the list
21
+ - Custom eligible errors: configure which errors trigger fallback
22
+ - Each fallback builds its own provider instance with resolved credentials
23
+
24
+ ### Changed
25
+
26
+ - `Pipeline::KNOWN_MIDDLEWARES` now includes `:model_fallback`.
27
+
1
28
  ## [0.12.0] — 2026-07-22
2
29
 
3
30
  ### Added
data/README.md CHANGED
@@ -60,13 +60,13 @@ end
60
60
 
61
61
  Opt-in safety modules:
62
62
 
63
- - **PermissionGate** — Require approval for destructive tools (write, edit, bash, destroy)
63
+ - **Permissions** — Access control for tools. Supports named access modes (`:full_access`, `:read_only`, `:ask_before_changes`) or custom blocked-tool lists.
64
64
  - **RateLimiter** — Prevent runaway tool calls (configurable per-minute and per-turn limits)
65
65
  - **AuditLog** — Immutable, append-only log of every tool call
66
66
 
67
67
  ```ruby
68
68
  extensions = [
69
- Ask::Agent::Extensions::PermissionGate.new,
69
+ Ask::Agent::Extensions::Permissions.new(mode: :read_only),
70
70
  Ask::Agent::Extensions::RateLimiter.new(max_calls_per_minute: 30),
71
71
  Ask::Agent::Extensions::AuditLog.new(path: "agent.log")
72
72
  ]
@@ -81,6 +81,57 @@ session = Ask::Agent::Session.new(
81
81
  )
82
82
  ```
83
83
 
84
+ ## Middleware
85
+
86
+ Wrapping LLM provider calls with cross-cutting behavior:
87
+
88
+ - **RetryOnFailure** — Retry on rate limits and server errors with exponential backoff
89
+ - **ModelFallback** — Switch to a fallback model+provider on transient errors
90
+ - **LogCalls** — Log every LLM provider call
91
+ - **DefaultSettings** — Inject default generation parameters
92
+
93
+ ```ruby
94
+ Ask::Agent.configure do |c|
95
+ c.middleware.use :retry_on_failure, max_retries: 3
96
+ c.middleware.use :model_fallback, fallbacks: [
97
+ { model: "claude-sonnet-4", provider: :anthropic },
98
+ { model: "gemini-2.0-flash", provider: :google }
99
+ ]
100
+ c.middleware.use :log_calls, logger: Rails.logger
101
+ c.middleware.use :default_settings, temperature: 0.7
102
+ end
103
+ ```
104
+
105
+ ### ModelFallback
106
+
107
+ When the primary LLM is overloaded or down, `ModelFallback` transparently switches to a backup model+provider. Credentials for each provider are resolved automatically.
108
+
109
+ **Static fallbacks** — ordered list tried in sequence:
110
+ ```ruby
111
+ c.middleware.use :model_fallback, fallbacks: [
112
+ { model: "claude-sonnet-4", provider: :anthropic },
113
+ { model: "gemini-2.0-flash", provider: :google }
114
+ ]
115
+ ```
116
+
117
+ **Dynamic fallbacks** — lambda that receives the error and request:
118
+ ```ruby
119
+ c.middleware.use :model_fallback, fallbacks: ->(error, request) {
120
+ if request[:messages].sum { |m| m[:content].to_s.length } > 100_000
121
+ [{ model: "claude-sonnet-4", provider: :anthropic }] # long-context
122
+ else
123
+ [{ model: "gpt-4o-mini", provider: :openai }] # cheaper
124
+ end
125
+ }
126
+ ```
127
+
128
+ **Custom eligible errors** — by default rate limits, server errors, and service unavailable:
129
+ ```ruby
130
+ c.middleware.use :model_fallback,
131
+ fallbacks: [{ model: "claude-sonnet-4", provider: :anthropic }],
132
+ eligible_errors: [Ask::RateLimitError, Ask::ServerError]
133
+ ```
134
+
84
135
  ## Configuration
85
136
 
86
137
  ```ruby
@@ -1,70 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Legacy alias — PermissionGate has been renamed to Permissions.
4
+ # This file will be removed in the next major version.
5
+ require_relative "permissions"
6
+
3
7
  module Ask
4
8
  module Agent
5
9
  module Extensions
6
- class PermissionGate
7
- DEFAULT_TOOLS = %i[write edit bash destroy].freeze
8
-
9
- def initialize(blocked_tools: DEFAULT_TOOLS, timeout: nil)
10
- @blocked_tools = Array(blocked_tools).map(&:to_sym)
11
- @timeout = timeout
12
- @pending = {}
13
- @mutex = Mutex.new
14
- end
15
-
16
- def before_tool_call(tool_call, _context)
17
- return { action: :proceed } unless @blocked_tools.include?(tool_call.name.to_sym)
18
-
19
- if approved?(tool_call)
20
- { action: :proceed }
21
- else
22
- request_approval(tool_call)
23
- end
24
- end
25
-
26
- def approve(tool_call_id)
27
- @mutex.synchronize do
28
- entry = @pending[tool_call_id]
29
- return false unless entry
30
- entry[:approved] = true
31
- end
32
- end
33
-
34
- def pending_approvals
35
- @mutex.synchronize { @pending.values.reject { |e| e[:approved] } }
36
- end
37
-
38
- private
39
-
40
- def approved?(tool_call)
41
- @mutex.synchronize do
42
- key = tool_call.id
43
- entry = @pending[key]
44
- return false unless entry
45
-
46
- if @timeout && (Time.now - entry[:created_at]) > @timeout
47
- @pending.delete(key)
48
- return false
49
- end
50
-
51
- entry[:approved]
52
- end
53
- end
54
-
55
- def request_approval(tool_call)
56
- @mutex.synchronize do
57
- @pending[tool_call.id] = {
58
- tool_call: tool_call,
59
- approved: false,
60
- created_at: Time.now
61
- }
62
- end
63
-
64
- warn "[PermissionGate] Tool '#{tool_call.name}' requires approval. Call approve('#{tool_call.id}') to allow."
65
- { action: :block, reason: "Tool '#{tool_call.name}' requires approval" }
66
- end
67
- end
10
+ PermissionGate = Permissions
68
11
  end
69
12
  end
70
13
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Agent
5
+ module Extensions
6
+ class Permissions
7
+ DEFAULT_TOOLS = %i[write edit bash destroy].freeze
8
+
9
+ ACCESS_MODES = {
10
+ full_access: { blocked_tools: [] }.freeze,
11
+ ask_before_changes: { blocked_tools: DEFAULT_TOOLS }.freeze,
12
+ read_only: { blocked_tools: %i[write edit bash destroy] }.freeze
13
+ }.freeze
14
+
15
+ def initialize(mode: nil, blocked_tools: nil, timeout: nil)
16
+ @mode = mode
17
+ @timeout = timeout
18
+ @pending = {}
19
+ @mutex = Mutex.new
20
+
21
+ @blocked_tools = if mode
22
+ config = ACCESS_MODES[mode]
23
+ raise ArgumentError, "Unknown access mode: #{mode.inspect}. Valid: #{ACCESS_MODES.keys.join(', ')}" unless config
24
+ config[:blocked_tools].dup
25
+ elsif blocked_tools
26
+ Array(blocked_tools).map(&:to_sym)
27
+ else
28
+ DEFAULT_TOOLS.dup
29
+ end
30
+ end
31
+
32
+ def before_tool_call(tool_call, _context)
33
+ return { action: :proceed } unless @blocked_tools.include?(tool_call.name.to_sym)
34
+
35
+ if approved?(tool_call)
36
+ { action: :proceed }
37
+ else
38
+ request_approval(tool_call)
39
+ end
40
+ end
41
+
42
+ def approve(tool_call_id)
43
+ @mutex.synchronize do
44
+ entry = @pending[tool_call_id]
45
+ return false unless entry
46
+ entry[:approved] = true
47
+ end
48
+ end
49
+
50
+ def pending_approvals
51
+ @mutex.synchronize { @pending.values.reject { |e| e[:approved] } }
52
+ end
53
+
54
+ private
55
+
56
+ def approved?(tool_call)
57
+ @mutex.synchronize do
58
+ key = tool_call.id
59
+ entry = @pending[key]
60
+ return false unless entry
61
+
62
+ if @timeout && (Time.now - entry[:created_at]) > @timeout
63
+ @pending.delete(key)
64
+ return false
65
+ end
66
+
67
+ entry[:approved]
68
+ end
69
+ end
70
+
71
+ def request_approval(tool_call)
72
+ @mutex.synchronize do
73
+ @pending[tool_call.id] = {
74
+ tool_call: tool_call,
75
+ approved: false,
76
+ created_at: Time.now
77
+ }
78
+ end
79
+
80
+ warn "[Permissions] Tool '#{tool_call.name}' requires approval. Call approve('#{tool_call.id}') to allow."
81
+ { action: :block, reason: "Tool '#{tool_call.name}' requires approval" }
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -17,7 +17,7 @@ module Ask
17
17
  @max_consecutive_tool_turns = max_consecutive_tool_turns
18
18
  end
19
19
 
20
- def run_turn(chat:, message:, tools:, tool_executor:, compactor:, hooks:, event_emitter:, session_id: nil)
20
+ def run_turn(chat:, message:, tools:, tool_executor:, compactor:, hooks:, event_emitter:, session_id: nil, persist: nil)
21
21
  raise MaxTurnsExceeded if @turn_count >= @max_turns
22
22
 
23
23
  event_emitter.emit(Events::TurnStart.new)
@@ -103,6 +103,9 @@ module Ask
103
103
  compactor.run(event_emitter: event_emitter)
104
104
  end
105
105
 
106
+ # Persist after each turn so mid-session crashes don't lose progress
107
+ persist&.call
108
+
106
109
  raise MaxTurnsExceeded if @turn_count >= @max_turns
107
110
 
108
111
  # Recursive call — LLM processes tool results
@@ -114,7 +117,8 @@ module Ask
114
117
  compactor: compactor,
115
118
  hooks: hooks,
116
119
  event_emitter: event_emitter,
117
- session_id: session_id
120
+ session_id: session_id,
121
+ persist: persist
118
122
  )
119
123
  end
120
124
 
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Agent
5
+ module Middleware
6
+ # Switches to a fallback model+provider when the primary LLM call fails
7
+ # with a transient error (rate limit, server error, service unavailable).
8
+ #
9
+ # Each fallback specifies both a model and a provider slug, so the
10
+ # middleware can switch from e.g. OpenAI to Anthropic transparently.
11
+ # Credentials for each provider are resolved automatically via
12
+ # {Ask::Auth.resolve}.
13
+ #
14
+ # @example Basic usage — fallback to Anthropic when OpenAI is overloaded
15
+ # pipeline.use :model_fallback, fallbacks: [
16
+ # { model: "claude-sonnet-4", provider: :anthropic },
17
+ # { model: "gemini-2.0-flash", provider: :google }
18
+ # ]
19
+ #
20
+ # @example With failure-trigger customization
21
+ # pipeline.use :model_fallback, fallbacks: [
22
+ # { model: "claude-sonnet-4", provider: :anthropic, on_error: [Ask::RateLimitError, Ask::ServerError] }
23
+ # ]
24
+ #
25
+ # @example Using the block form to choose fallbacks dynamically
26
+ # pipeline.use :model_fallback, fallbacks: ->(error, request) {
27
+ # if request[:messages].sum { |m| m[:content].to_s.length } > 100_000
28
+ # [{ model: "claude-sonnet-4", provider: :anthropic }] # use long-context model
29
+ # else
30
+ # [{ model: "gpt-4o-mini", provider: :openai }] # use cheaper model
31
+ # end
32
+ # }
33
+ class ModelFallback < Base
34
+ DEFAULT_ELIGIBLE_ERRORS = [
35
+ Ask::RateLimitError, Ask::ServerError, Ask::ServiceUnavailable
36
+ ].freeze
37
+
38
+ def initialize(fallbacks:, eligible_errors: nil)
39
+ @fallbacks = fallbacks.respond_to?(:call) ? fallbacks : Array(fallbacks)
40
+ @eligible_errors = Array(eligible_errors || DEFAULT_ELIGIBLE_ERRORS)
41
+ raise ArgumentError, "At least one fallback is required" if Array(@fallbacks).empty?
42
+ end
43
+
44
+ def around_request(provider, request)
45
+ # Try primary provider
46
+ begin
47
+ return yield
48
+ rescue *@eligible_errors => e
49
+ result = try_fallbacks(request, error: e)
50
+ return result if result
51
+ raise
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def try_fallbacks(request, error:)
58
+ fallback_list = resolve_fallback_list(error, request)
59
+
60
+ fallback_list.each do |fb|
61
+ begin
62
+ new_provider = build_fallback_provider(fb[:provider])
63
+ request[:model] = fb[:model]
64
+ return invoke_fallback(new_provider, request)
65
+ rescue *@eligible_errors
66
+ next # Try next fallback
67
+ end
68
+ end
69
+
70
+ nil # All fallbacks exhausted
71
+ end
72
+
73
+ def resolve_fallback_list(error, request)
74
+ list = if @fallbacks.respond_to?(:call)
75
+ @fallbacks.call(error, request)
76
+ else
77
+ @fallbacks
78
+ end
79
+ raise "Fallback list must be an array of hashes, got #{list.class}" unless list.is_a?(Array)
80
+ list
81
+ end
82
+
83
+ def build_fallback_provider(provider_slug)
84
+ slug = provider_slug.to_s
85
+ klass = Ask::Provider.resolve(slug)
86
+ klass.new(fallback_config(slug))
87
+ end
88
+
89
+ def invoke_fallback(provider, request)
90
+ provider.chat(
91
+ request[:messages],
92
+ model: request[:model],
93
+ tools: request[:tools],
94
+ temperature: request[:temperature],
95
+ stream: request[:stream],
96
+ schema: request[:schema],
97
+ **request[:extra_params]
98
+ )
99
+ end
100
+
101
+ def fallback_config(slug)
102
+ key = Ask::Auth.resolve(:"#{slug}_api_key") rescue nil
103
+ config = { api_key: key }
104
+ config[:"#{slug}_api_key"] = key
105
+ Ask::LLM::Config.new(config)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -19,7 +19,8 @@ module Ask
19
19
  KNOWN_MIDDLEWARES = {
20
20
  retry_on_failure: "Ask::Agent::Middleware::RetryOnFailure",
21
21
  log_calls: "Ask::Agent::Middleware::LogCalls",
22
- default_settings: "Ask::Agent::Middleware::DefaultSettings"
22
+ default_settings: "Ask::Agent::Middleware::DefaultSettings",
23
+ model_fallback: "Ask::Agent::Middleware::ModelFallback"
23
24
  }.freeze
24
25
 
25
26
  def initialize
@@ -18,7 +18,7 @@ module Ask
18
18
  attr_reader :skills_registry
19
19
 
20
20
  def initialize(model:, tools: [], max_turns: 25, max_tool_retries: 3,
21
- compactor: nil, hooks: {}, persistence: nil,
21
+ compactor: nil, hooks: {}, state: nil, persistence: nil,
22
22
  id: nil, system_prompt: nil, parallel_tools: true,
23
23
  reflector: nil, telemetry: true, meta_agent: nil,
24
24
  agent_dir: nil, **chat_options)
@@ -48,11 +48,10 @@ module Ask
48
48
  @compactor = compactor ? build_compactor(compactor) : nil
49
49
  @hooks = Hooks.new(hooks)
50
50
 
51
- # Build system context from typed sources
52
51
  @system_context = build_system_context(system_prompt)
53
52
  apply_system_context
54
53
 
55
- @persistence = persistence
54
+ @state = state || persistence
56
55
 
57
56
  reflector_opts = reflector.is_a?(Hash) ? reflector : {}
58
57
  @reflector = if reflector
@@ -79,8 +78,7 @@ module Ask
79
78
 
80
79
  emit(Events::SessionStart.new)
81
80
 
82
- active_tools = resolve_tools(tools || [])
83
- active_tools = @tools if active_tools.empty?
81
+ active_tools = @tools
84
82
 
85
83
  if active_tools.empty? && !@_no_tools_instructed
86
84
  @chat.add_message(role: :system, content: "You have no tools available. Do not claim you can look up information or use tools of any kind. Just respond based on your existing knowledge.")
@@ -90,16 +88,17 @@ module Ask
90
88
  begin
91
89
  @tool_executor.telemetry = @telemetry
92
90
 
93
- response = @loop.run_turn(
94
- chat: @chat,
95
- message: message,
96
- tools: active_tools,
97
- tool_executor: @tool_executor,
98
- compactor: @compactor,
99
- hooks: @hooks,
100
- event_emitter: self,
101
- session_id: @id
102
- )
91
+ response = @loop.run_turn(
92
+ chat: @chat,
93
+ message: message,
94
+ tools: active_tools,
95
+ tool_executor: @tool_executor,
96
+ compactor: @compactor,
97
+ hooks: @hooks,
98
+ event_emitter: self,
99
+ session_id: @id,
100
+ persist: @state ? method(:persist!) : nil
101
+ )
103
102
 
104
103
  @total_input_tokens += @loop.last_input_tokens.to_i
105
104
  @total_output_tokens += @loop.last_output_tokens.to_i
@@ -123,7 +122,7 @@ module Ask
123
122
  raise
124
123
  ensure
125
124
  @running = false
126
- persist! if @persistence
125
+ persist! if @state
127
126
  end
128
127
 
129
128
  @tool_calls_made = @tool_executor.total_executions
@@ -194,34 +193,37 @@ module Ask
194
193
  def deleted? = @deleted
195
194
 
196
195
  def save
197
- persist! if @persistence
196
+ persist! if @state
198
197
  end
199
198
 
200
199
  def self.load(id, adapter:)
201
- data = adapter.load(id)
200
+ data = adapter.get(id)
202
201
  return nil unless data
203
202
 
203
+ data = deep_symbolize_keys(data)
204
+
204
205
  session = new(
205
206
  id: data[:id],
206
207
  model: data.dig(:metadata, :model),
207
208
  tools: data.dig(:metadata, :tools)&.map(&:constantize) || [],
208
- persistence: adapter
209
+ state: adapter
209
210
  )
210
211
 
211
- data[:messages].each do |msg|
212
- session.chat.add_message(
213
- role: msg[:role].to_sym,
214
- content: msg[:content],
215
- tool_call_id: msg[:tool_call_id]
216
- )
217
- end
218
-
219
- session
212
+ data[:messages].each do |msg|
213
+ session.chat.add_message(
214
+ role: msg[:role].to_sym,
215
+ content: msg[:content],
216
+ tool_call_id: msg[:tool_call_id]
217
+ )
218
+ end
219
+
220
+ session.instance_variable_set(:@messages, session.chat.messages.dup)
221
+ session
220
222
  end
221
223
 
222
224
  def delete
223
225
  @deleted = true
224
- @persistence&.delete(@id)
226
+ @state&.delete(@id)
225
227
  end
226
228
 
227
229
  def abort
@@ -291,7 +293,7 @@ module Ask
291
293
  end
292
294
 
293
295
  def persist!
294
- @persistence.save(@id, {
296
+ @state.set(@id, {
295
297
  id: @id,
296
298
  messages: @chat.messages.map { |m|
297
299
  {
@@ -359,6 +361,19 @@ module Ask
359
361
  SystemContext.new(sources)
360
362
  end
361
363
 
364
+ # Recursively convert string keys to symbol keys in hashes.
365
+ # Needed when loading session data that was serialized through JSON.
366
+ def self.deep_symbolize_keys(obj)
367
+ case obj
368
+ when Hash
369
+ obj.each_with_object({}) { |(k, v), h| h[k.to_sym] = deep_symbolize_keys(v) }
370
+ when Array
371
+ obj.map { |e| deep_symbolize_keys(e) }
372
+ else
373
+ obj
374
+ end
375
+ end
376
+
362
377
  # Render the system context and apply it to the chat.
363
378
  def apply_system_context
364
379
  rendered = @system_context.render
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Agent
5
- VERSION = "0.12.0"
5
+ VERSION = "0.14.0"
6
6
  end
7
7
  end
data/lib/ask/agent.rb CHANGED
@@ -21,6 +21,7 @@ module Ask
21
21
  class UnknownAgent < Error; end
22
22
 
23
23
  module Extensions
24
+ autoload :Permissions, "ask/agent/extensions/permissions"
24
25
  autoload :PermissionGate, "ask/agent/extensions/permission_gate"
25
26
  autoload :RateLimiter, "ask/agent/extensions/rate_limiter"
26
27
  autoload :AuditLog, "ask/agent/extensions/audit_log"
@@ -32,6 +33,7 @@ module Ask
32
33
  autoload :RetryOnFailure, "ask/agent/middleware/retry_on_failure"
33
34
  autoload :LogCalls, "ask/agent/middleware/log_calls"
34
35
  autoload :DefaultSettings, "ask/agent/middleware/default_settings"
36
+ autoload :ModelFallback, "ask/agent/middleware/model_fallback"
35
37
  end
36
38
 
37
39
  module StreamTransforms
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.12.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -160,6 +160,7 @@ files:
160
160
  - lib/ask/agent/events.rb
161
161
  - lib/ask/agent/extensions/audit_log.rb
162
162
  - lib/ask/agent/extensions/permission_gate.rb
163
+ - lib/ask/agent/extensions/permissions.rb
163
164
  - lib/ask/agent/extensions/rate_limiter.rb
164
165
  - lib/ask/agent/hooks.rb
165
166
  - lib/ask/agent/loop.rb
@@ -167,6 +168,7 @@ files:
167
168
  - lib/ask/agent/middleware/base.rb
168
169
  - lib/ask/agent/middleware/default_settings.rb
169
170
  - lib/ask/agent/middleware/log_calls.rb
171
+ - lib/ask/agent/middleware/model_fallback.rb
170
172
  - lib/ask/agent/middleware/pipeline.rb
171
173
  - lib/ask/agent/middleware/retry_on_failure.rb
172
174
  - lib/ask/agent/persistence/base.rb