ask-agent 0.12.1 → 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: 1c3eb4f6e24d5acd650f8e11614dcd3257b9f57b252ba6df2c8c48ca3dffdd1e
4
- data.tar.gz: efc969c3abc319814c4afecc9103750a37962cbd664d1944d84ec3d2d89adb71
3
+ metadata.gz: c80f27cbdec5b3808533dba4861762e19e4ef81c084be099a4a7eb0b2169170e
4
+ data.tar.gz: 34e5388035a8f216df468039bf8c7e304296d7661832e8109df4d76fbb527193
5
5
  SHA512:
6
- metadata.gz: c9a78d1dd7e990eb11bd56997f9da131e0bb11f88affca0b4e01d9504711aa17787e98bde447bcd397045dcb97e4930cd04437638b3bebc83d166e8f10c34d37
7
- data.tar.gz: 4d17c16b767ff92973d32acff24232c5a5d86739ead407f1e92f92707a8d685559e146c4c5430861e397f6bd5cf5c9d2fcdbf503015e6adeae3821f1567041ca
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
@@ -89,16 +88,17 @@ module Ask
89
88
  begin
90
89
  @tool_executor.telemetry = @telemetry
91
90
 
92
- response = @loop.run_turn(
93
- chat: @chat,
94
- message: message,
95
- tools: active_tools,
96
- tool_executor: @tool_executor,
97
- compactor: @compactor,
98
- hooks: @hooks,
99
- event_emitter: self,
100
- session_id: @id
101
- )
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
+ )
102
102
 
103
103
  @total_input_tokens += @loop.last_input_tokens.to_i
104
104
  @total_output_tokens += @loop.last_output_tokens.to_i
@@ -122,7 +122,7 @@ module Ask
122
122
  raise
123
123
  ensure
124
124
  @running = false
125
- persist! if @persistence
125
+ persist! if @state
126
126
  end
127
127
 
128
128
  @tool_calls_made = @tool_executor.total_executions
@@ -193,34 +193,37 @@ module Ask
193
193
  def deleted? = @deleted
194
194
 
195
195
  def save
196
- persist! if @persistence
196
+ persist! if @state
197
197
  end
198
198
 
199
199
  def self.load(id, adapter:)
200
- data = adapter.load(id)
200
+ data = adapter.get(id)
201
201
  return nil unless data
202
202
 
203
+ data = deep_symbolize_keys(data)
204
+
203
205
  session = new(
204
206
  id: data[:id],
205
207
  model: data.dig(:metadata, :model),
206
208
  tools: data.dig(:metadata, :tools)&.map(&:constantize) || [],
207
- persistence: adapter
209
+ state: adapter
208
210
  )
209
211
 
210
- data[:messages].each do |msg|
211
- session.chat.add_message(
212
- role: msg[:role].to_sym,
213
- content: msg[:content],
214
- tool_call_id: msg[:tool_call_id]
215
- )
216
- end
217
-
218
- 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
219
222
  end
220
223
 
221
224
  def delete
222
225
  @deleted = true
223
- @persistence&.delete(@id)
226
+ @state&.delete(@id)
224
227
  end
225
228
 
226
229
  def abort
@@ -290,7 +293,7 @@ module Ask
290
293
  end
291
294
 
292
295
  def persist!
293
- @persistence.save(@id, {
296
+ @state.set(@id, {
294
297
  id: @id,
295
298
  messages: @chat.messages.map { |m|
296
299
  {
@@ -358,6 +361,19 @@ module Ask
358
361
  SystemContext.new(sources)
359
362
  end
360
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
+
361
377
  # Render the system context and apply it to the chat.
362
378
  def apply_system_context
363
379
  rendered = @system_context.render
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Agent
5
- VERSION = "0.12.1"
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.1
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