ask-agent 0.12.1 → 0.15.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/CHANGELOG.md +91 -0
- data/README.md +135 -3
- data/lib/ask/agent/configuration.rb +1 -1
- data/lib/ask/agent/evaluator.rb +164 -0
- data/lib/ask/agent/events.rb +5 -0
- data/lib/ask/agent/extensions/{permission_gate.rb → permissions.rb} +20 -4
- data/lib/ask/agent/loop.rb +6 -2
- data/lib/ask/agent/middleware/model_fallback.rb +110 -0
- data/lib/ask/agent/middleware/pipeline.rb +2 -1
- data/lib/ask/agent/session.rb +116 -30
- data/lib/ask/agent/version.rb +1 -1
- data/lib/ask/agent.rb +3 -1
- metadata +18 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 816f21ed178b31227a58afeeed65f2428dbe9cce7d4a6ddab2c6fda7a6a60a89
|
|
4
|
+
data.tar.gz: 4dec09ce3cc94c6abc0e94bd5dee9f6ac9bde083dc7995af601a6650e692fd37
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8de0c033834105f7c147115bd84b97306fc91b787690fc9e82563835f53b8fad69ff90dfabeeada12be9a8651e9ecfffd960d8e2fa6e84ffff4709f30519a5de
|
|
7
|
+
data.tar.gz: b5d46db9e66b242ea35fafaf86c67401ad27ee8ef0099ad4811a3d8442e63bd54aa2b16be5e4a7b2aa0f7305603cf39d1b5497e66648f59a9d701489bbdc6c8c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,94 @@
|
|
|
1
|
+
## [0.15.0] — 2026-07-24
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **Independent Evaluator — `Ask::Agent::Evaluator`** — Generator/evaluator separation.
|
|
6
|
+
A separate model (configured independently from the session's model) judges the
|
|
7
|
+
agent's output against a structured rubric before delivery. This prevents the
|
|
8
|
+
anti-pattern of a model grading its own work.
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
# Evaluate with a different model — the recommended approach
|
|
12
|
+
session = Ask::Agent::Session.new(
|
|
13
|
+
model: "gpt-4o",
|
|
14
|
+
evaluator: { model: "claude-sonnet-4", goal: "Write an email validator" }
|
|
15
|
+
)
|
|
16
|
+
session.run("Write email validation")
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Three verdicts:
|
|
20
|
+
- **`:accept`** — output meets the goal, passes through to reflection
|
|
21
|
+
- **`:revise`** — evaluator provides actionable feedback; session runs another
|
|
22
|
+
turn with the feedback injected into system context
|
|
23
|
+
- **`:block`** — output is fundamentally wrong; session returns blocked message
|
|
24
|
+
and emits `Events::EvaluationBlocked`
|
|
25
|
+
|
|
26
|
+
Rubric dimensions (each scored 0-2):
|
|
27
|
+
- correctness (3× weight), completeness (2×), verification (2×), scope (1×), clarity (1×)
|
|
28
|
+
|
|
29
|
+
Custom rubrics supported:
|
|
30
|
+
```ruby
|
|
31
|
+
evaluator = Ask::Agent::Evaluator.new(
|
|
32
|
+
model: "claude-sonnet-4",
|
|
33
|
+
rubric: [
|
|
34
|
+
Ask::Agent::Evaluator::Dimension.new(name: "performance", description: "Is it fast?", weight: 2)
|
|
35
|
+
]
|
|
36
|
+
)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- **`evaluator:` option on `Session`** — accepts `true`, `false`/`nil`, or a Hash:
|
|
40
|
+
- `evaluator: true` — uses `config.default_evaluator_model` (falls back to the
|
|
41
|
+
session's model, though using a different model is strongly recommended)
|
|
42
|
+
- `evaluator: { model: "claude-sonnet-4", goal: "Custom goal" }` — explicit config
|
|
43
|
+
- `evaluator: false` (default) — no evaluation, backward compatible
|
|
44
|
+
|
|
45
|
+
- **`default_evaluator_model` config option** — set a global default:
|
|
46
|
+
```ruby
|
|
47
|
+
Ask::Agent.configure do |c|
|
|
48
|
+
c.default_evaluator_model = "claude-sonnet-4"
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- **New event types** for streaming evaluation:
|
|
53
|
+
- `Events::EvaluationStart` — emitted when evaluation begins (includes dimension list)
|
|
54
|
+
- `Events::EvaluationDelta` — streamed evaluation text from the evaluator model
|
|
55
|
+
- `Events::EvaluationEnd` — emitted with decision, feedback, scores, and evidence
|
|
56
|
+
- `Events::EvaluationBlocked` — emitted when evaluator returns `:block`
|
|
57
|
+
|
|
58
|
+
- **17 unit tests** for Evaluator — construction, rubric, all three verdicts, event
|
|
59
|
+
emission, custom rubrics, JSON parsing, and malformed response fallback.
|
|
60
|
+
|
|
61
|
+
- **7 integration tests** for Session with evaluator — config (true/hash),
|
|
62
|
+
revise triggers improvement, revise skips reflector, block returns blocked
|
|
63
|
+
message, block emits event, evaluator-not-configured skips evaluation.
|
|
64
|
+
|
|
65
|
+
## [0.14.0] — 2026-07-23
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
|
|
69
|
+
- **`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).
|
|
70
|
+
- **Per-turn persistence** — Session now persists after every LLM turn, not just at the end of `run()`. Mid-session crashes no longer lose progress.
|
|
71
|
+
- **`Session.load` restores `@messages`** — Previously `session.messages` returned `nil` after loading. Now it's populated from the restored chat messages.
|
|
72
|
+
- **`Ask::State::Adapter#clear`** — Abstract method added to the adapter contract. Memory adapter implements it.
|
|
73
|
+
|
|
74
|
+
### Changed
|
|
75
|
+
|
|
76
|
+
- **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`.
|
|
77
|
+
|
|
78
|
+
## [0.13.0] — 2026-07-23
|
|
79
|
+
|
|
80
|
+
### Added
|
|
81
|
+
|
|
82
|
+
- **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`.
|
|
83
|
+
- Static fallbacks: ordered list of `{ model:, provider: }` hashes
|
|
84
|
+
- Dynamic fallbacks: lambda receiving `(error, request)` returning the list
|
|
85
|
+
- Custom eligible errors: configure which errors trigger fallback
|
|
86
|
+
- Each fallback builds its own provider instance with resolved credentials
|
|
87
|
+
|
|
88
|
+
### Changed
|
|
89
|
+
|
|
90
|
+
- `Pipeline::KNOWN_MIDDLEWARES` now includes `:model_fallback`.
|
|
91
|
+
|
|
1
92
|
## [0.12.0] — 2026-07-22
|
|
2
93
|
|
|
3
94
|
### Added
|
data/README.md
CHANGED
|
@@ -37,7 +37,88 @@ puts response
|
|
|
37
37
|
| `Ask::Agent::Telemetry` | telemetry.rb | File-backed telemetry for error tracking |
|
|
38
38
|
| `Ask::Agent::Reflector` | reflector.rb | Assistant response self-evaluation |
|
|
39
39
|
| `Ask::Agent::MetaAgent` | meta_agent.rb | LLM-powered self-improvement from telemetry |
|
|
40
|
-
| `Ask::Agent::
|
|
40
|
+
| `Ask::Agent::Evaluator` | evaluator.rb | Independent response evaluation with structured rubric — different model, isolated context |
|
|
41
|
+
| `Ask::Agent::Configuration` | configuration.rb | Global config: model, turns, concurrency, evaluator |
|
|
42
|
+
|
|
43
|
+
## Evaluator
|
|
44
|
+
|
|
45
|
+
Independent response evaluation with generator/evaluator separation. The
|
|
46
|
+
evaluator uses a **separate model** (different from the session's model) and an
|
|
47
|
+
**isolated context** to judge the agent's output — preventing the anti-pattern
|
|
48
|
+
of a model grading its own work.
|
|
49
|
+
|
|
50
|
+
### Quick start
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
session = Ask::Agent::Session.new(
|
|
54
|
+
model: "gpt-4o",
|
|
55
|
+
evaluator: { model: "claude-sonnet-4", goal: "Write an email validator" }
|
|
56
|
+
)
|
|
57
|
+
session.run("Write email validation")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Verdicts
|
|
61
|
+
|
|
62
|
+
| Verdict | Behavior |
|
|
63
|
+
|---------|----------|
|
|
64
|
+
| `:accept` | Output passes — falls through to reflection |
|
|
65
|
+
| `:revise` | Evaluator provides feedback; session runs another turn with it injected |
|
|
66
|
+
| `:block` | Output is fundamentally wrong — returns blocked message, emits `EvaluationBlocked` |
|
|
67
|
+
|
|
68
|
+
### Configuration
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
# Set a global default evaluator model
|
|
72
|
+
Ask::Agent.configure do |c|
|
|
73
|
+
c.default_evaluator_model = "claude-sonnet-4"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Then use evaluator: true to enable with the default
|
|
77
|
+
session = Ask::Agent::Session.new(model: "gpt-4o", evaluator: true)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Custom rubric
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
evaluator = Ask::Agent::Evaluator.new(
|
|
84
|
+
model: "claude-sonnet-4",
|
|
85
|
+
rubric: [
|
|
86
|
+
Ask::Agent::Evaluator::Dimension.new(
|
|
87
|
+
name: "performance",
|
|
88
|
+
description: "Is the implementation efficient?",
|
|
89
|
+
weight: 2
|
|
90
|
+
)
|
|
91
|
+
]
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
result = evaluator.evaluate(
|
|
95
|
+
goal: "Write an email validator",
|
|
96
|
+
response: agent_output
|
|
97
|
+
)
|
|
98
|
+
result.accept? # => true/false
|
|
99
|
+
result.scores # => { performance: 2 }
|
|
100
|
+
result.feedback # => "Add edge case for unicode characters"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Events
|
|
104
|
+
|
|
105
|
+
The evaluator emits its own events during evaluation:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
session.on_event do |event|
|
|
109
|
+
case event
|
|
110
|
+
when Ask::Agent::Events::EvaluationStart
|
|
111
|
+
puts "Evaluating against: #{event.dimensions.join(', ')}"
|
|
112
|
+
when Ask::Agent::Events::EvaluationDelta
|
|
113
|
+
print event.content
|
|
114
|
+
when Ask::Agent::Events::EvaluationEnd
|
|
115
|
+
puts "Decision: #{event.decision}"
|
|
116
|
+
puts "Scores: #{event.scores}"
|
|
117
|
+
when Ask::Agent::Events::EvaluationBlocked
|
|
118
|
+
puts "Blocked: #{event.feedback}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
```
|
|
41
122
|
|
|
42
123
|
## Events
|
|
43
124
|
|
|
@@ -60,13 +141,13 @@ end
|
|
|
60
141
|
|
|
61
142
|
Opt-in safety modules:
|
|
62
143
|
|
|
63
|
-
- **
|
|
144
|
+
- **Permissions** — Access control for tools. Supports named access modes (`:full_access`, `:read_only`, `:ask_before_changes`) or custom blocked-tool lists.
|
|
64
145
|
- **RateLimiter** — Prevent runaway tool calls (configurable per-minute and per-turn limits)
|
|
65
146
|
- **AuditLog** — Immutable, append-only log of every tool call
|
|
66
147
|
|
|
67
148
|
```ruby
|
|
68
149
|
extensions = [
|
|
69
|
-
Ask::Agent::Extensions::
|
|
150
|
+
Ask::Agent::Extensions::Permissions.new(mode: :read_only),
|
|
70
151
|
Ask::Agent::Extensions::RateLimiter.new(max_calls_per_minute: 30),
|
|
71
152
|
Ask::Agent::Extensions::AuditLog.new(path: "agent.log")
|
|
72
153
|
]
|
|
@@ -81,6 +162,57 @@ session = Ask::Agent::Session.new(
|
|
|
81
162
|
)
|
|
82
163
|
```
|
|
83
164
|
|
|
165
|
+
## Middleware
|
|
166
|
+
|
|
167
|
+
Wrapping LLM provider calls with cross-cutting behavior:
|
|
168
|
+
|
|
169
|
+
- **RetryOnFailure** — Retry on rate limits and server errors with exponential backoff
|
|
170
|
+
- **ModelFallback** — Switch to a fallback model+provider on transient errors
|
|
171
|
+
- **LogCalls** — Log every LLM provider call
|
|
172
|
+
- **DefaultSettings** — Inject default generation parameters
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
Ask::Agent.configure do |c|
|
|
176
|
+
c.middleware.use :retry_on_failure, max_retries: 3
|
|
177
|
+
c.middleware.use :model_fallback, fallbacks: [
|
|
178
|
+
{ model: "claude-sonnet-4", provider: :anthropic },
|
|
179
|
+
{ model: "gemini-2.0-flash", provider: :google }
|
|
180
|
+
]
|
|
181
|
+
c.middleware.use :log_calls, logger: Rails.logger
|
|
182
|
+
c.middleware.use :default_settings, temperature: 0.7
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### ModelFallback
|
|
187
|
+
|
|
188
|
+
When the primary LLM is overloaded or down, `ModelFallback` transparently switches to a backup model+provider. Credentials for each provider are resolved automatically.
|
|
189
|
+
|
|
190
|
+
**Static fallbacks** — ordered list tried in sequence:
|
|
191
|
+
```ruby
|
|
192
|
+
c.middleware.use :model_fallback, fallbacks: [
|
|
193
|
+
{ model: "claude-sonnet-4", provider: :anthropic },
|
|
194
|
+
{ model: "gemini-2.0-flash", provider: :google }
|
|
195
|
+
]
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Dynamic fallbacks** — lambda that receives the error and request:
|
|
199
|
+
```ruby
|
|
200
|
+
c.middleware.use :model_fallback, fallbacks: ->(error, request) {
|
|
201
|
+
if request[:messages].sum { |m| m[:content].to_s.length } > 100_000
|
|
202
|
+
[{ model: "claude-sonnet-4", provider: :anthropic }] # long-context
|
|
203
|
+
else
|
|
204
|
+
[{ model: "gpt-4o-mini", provider: :openai }] # cheaper
|
|
205
|
+
end
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Custom eligible errors** — by default rate limits, server errors, and service unavailable:
|
|
210
|
+
```ruby
|
|
211
|
+
c.middleware.use :model_fallback,
|
|
212
|
+
fallbacks: [{ model: "claude-sonnet-4", provider: :anthropic }],
|
|
213
|
+
eligible_errors: [Ask::RateLimitError, Ask::ServerError]
|
|
214
|
+
```
|
|
215
|
+
|
|
84
216
|
## Configuration
|
|
85
217
|
|
|
86
218
|
```ruby
|
|
@@ -5,7 +5,7 @@ module Ask
|
|
|
5
5
|
class Configuration
|
|
6
6
|
attr_accessor :default_model, :default_max_turns, :compactor_enabled,
|
|
7
7
|
:compactor_threshold, :parallel_tool_execution, :max_tool_retries,
|
|
8
|
-
:prompt_caching
|
|
8
|
+
:prompt_caching, :default_evaluator_model
|
|
9
9
|
|
|
10
10
|
# @return [Middleware::Pipeline] the middleware pipeline for provider calls
|
|
11
11
|
attr_reader :middleware
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
module Agent
|
|
5
|
+
class Evaluator
|
|
6
|
+
# Structured result from an evaluation.
|
|
7
|
+
# - decision :accept — response meets the goal
|
|
8
|
+
# :revise — response needs improvement (feedback provided)
|
|
9
|
+
# :block — response is fundamentally wrong (hard stop)
|
|
10
|
+
# - feedback actionable text the generator can use to improve
|
|
11
|
+
# - scores hash of dimension name => score (0, 1, or 2)
|
|
12
|
+
# - evidence array of specific evidence strings
|
|
13
|
+
Result = Data.define(:decision, :feedback, :scores, :evidence) do
|
|
14
|
+
def accept? = decision == :accept
|
|
15
|
+
def revise? = decision == :revise
|
|
16
|
+
def block? = decision == :block
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# A single dimension in the evaluation rubric.
|
|
20
|
+
Dimension = Data.define(:name, :description, :weight) do
|
|
21
|
+
def initialize(name:, description:, weight: 1)
|
|
22
|
+
super(name: name, description: description, weight: weight)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Default rubric borrowed from the course's evaluator-rubric template.
|
|
27
|
+
DEFAULT_DIMENSIONS = [
|
|
28
|
+
Dimension.new(name: "correctness", description: "Does the output match the requested goal?", weight: 3),
|
|
29
|
+
Dimension.new(name: "completeness", description: "Are all aspects of the goal addressed?", weight: 2),
|
|
30
|
+
Dimension.new(name: "verification", description: "Is there evidence that the output actually works?", weight: 2),
|
|
31
|
+
Dimension.new(name: "scope", description: "Did it stay within the defined boundaries without overreaching?", weight: 1),
|
|
32
|
+
Dimension.new(name: "clarity", description: "Is the output clear, well-structured, and maintainable?", weight: 1),
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
# How many times the evaluator may retry on a malformed response.
|
|
36
|
+
MAX_EVAL_RETRIES = 2
|
|
37
|
+
|
|
38
|
+
attr_reader :model, :rubric
|
|
39
|
+
|
|
40
|
+
# @param model [String] the model id to use for evaluation (should differ from the generator's model)
|
|
41
|
+
# @param rubric [Array<Dimension>] the rubric dimensions to evaluate against
|
|
42
|
+
def initialize(model:, rubric: DEFAULT_DIMENSIONS)
|
|
43
|
+
@model = model
|
|
44
|
+
@rubric = rubric
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Evaluate a response against a goal.
|
|
48
|
+
#
|
|
49
|
+
# @param goal [String] what the generator was asked to do
|
|
50
|
+
# @param response [String] what the generator produced
|
|
51
|
+
# @param event_emitter [#emit, nil] optional event emitter for streaming evaluation
|
|
52
|
+
# @return [Result] structured evaluation result
|
|
53
|
+
def evaluate(goal:, response:, event_emitter: nil)
|
|
54
|
+
event_emitter&.emit(Events::EvaluationStart.new(dimensions: @rubric.map(&:name)))
|
|
55
|
+
|
|
56
|
+
chat = build_chat
|
|
57
|
+
chat.with_instructions(evaluation_prompt(goal))
|
|
58
|
+
|
|
59
|
+
accumulated = +""
|
|
60
|
+
chat.ask(response.to_s) do |chunk|
|
|
61
|
+
if chunk.content.to_s.strip.length > 0
|
|
62
|
+
accumulated << chunk.content.to_s
|
|
63
|
+
event_emitter&.emit(Events::EvaluationDelta.new(content: chunk.content.to_s))
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
result = parse_result(accumulated)
|
|
68
|
+
event_emitter&.emit(Events::EvaluationEnd.new(
|
|
69
|
+
decision: result.decision,
|
|
70
|
+
feedback: result.feedback,
|
|
71
|
+
scores: result.scores,
|
|
72
|
+
evidence: result.evidence
|
|
73
|
+
))
|
|
74
|
+
|
|
75
|
+
result
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def build_chat
|
|
81
|
+
Chat.new(model: @model)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def evaluation_prompt(goal)
|
|
85
|
+
dimensions_text = @rubric.each_with_index.map { |d, i|
|
|
86
|
+
weight_label = d.weight > 1 ? " (weight: #{d.weight}x)" : ""
|
|
87
|
+
"#{i + 1}. **#{d.name}**#{weight_label} — #{d.description}"
|
|
88
|
+
}.join("\n")
|
|
89
|
+
|
|
90
|
+
<<~PROMPT
|
|
91
|
+
You are an independent evaluator. Your job is to assess whether a response
|
|
92
|
+
successfully achieves the given goal. You are NOT the agent that produced
|
|
93
|
+
this response — you are a neutral, objective judge.
|
|
94
|
+
|
|
95
|
+
## Goal
|
|
96
|
+
|
|
97
|
+
#{goal}
|
|
98
|
+
|
|
99
|
+
## Rubric
|
|
100
|
+
|
|
101
|
+
Evaluate the response against these dimensions:
|
|
102
|
+
|
|
103
|
+
#{dimensions_text}
|
|
104
|
+
|
|
105
|
+
For each dimension, assign a score:
|
|
106
|
+
- **0** = fails completely
|
|
107
|
+
- **1** = partially meets
|
|
108
|
+
- **2** = fully meets
|
|
109
|
+
|
|
110
|
+
Then provide:
|
|
111
|
+
- A final **decision**: "accept" (response meets the goal), "revise" (needs specific improvements), or "block" (fundamentally wrong — cannot be fixed with revisions)
|
|
112
|
+
- **Actionable feedback** the generator can use to improve (if decision is revise or block)
|
|
113
|
+
- **Concrete evidence** for your scores
|
|
114
|
+
|
|
115
|
+
Return valid JSON only — no other text:
|
|
116
|
+
{
|
|
117
|
+
"scores": { "correctness": 2, "completeness": 1, ... },
|
|
118
|
+
"decision": "accept",
|
|
119
|
+
"feedback": "Specific feedback here (or empty string if accepted)",
|
|
120
|
+
"evidence": ["Evidence point 1", "Evidence point 2"]
|
|
121
|
+
}
|
|
122
|
+
PROMPT
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def parse_result(text)
|
|
126
|
+
json = extract_json(text)
|
|
127
|
+
return default_fallback unless json
|
|
128
|
+
|
|
129
|
+
scores = json["scores"] || {}
|
|
130
|
+
|
|
131
|
+
decision = case json["decision"].to_s.strip.downcase
|
|
132
|
+
when "revise" then :revise
|
|
133
|
+
when "block" then :block
|
|
134
|
+
else :accept
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
Result.new(
|
|
138
|
+
decision: decision,
|
|
139
|
+
feedback: json["feedback"].to_s.strip,
|
|
140
|
+
scores: scores.transform_keys(&:to_sym),
|
|
141
|
+
evidence: Array(json["evidence"])
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def extract_json(text)
|
|
146
|
+
# Try direct parse first
|
|
147
|
+
JSON.parse(text.strip)
|
|
148
|
+
rescue JSON::ParserError
|
|
149
|
+
# Fall back to extracting the first JSON object
|
|
150
|
+
match = text.match(/\{.*\}/m)
|
|
151
|
+
match ? JSON.parse(match[0]) : nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def default_fallback
|
|
155
|
+
Result.new(
|
|
156
|
+
decision: :accept,
|
|
157
|
+
feedback: "",
|
|
158
|
+
scores: {},
|
|
159
|
+
evidence: []
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
data/lib/ask/agent/events.rb
CHANGED
|
@@ -28,6 +28,11 @@ module Ask
|
|
|
28
28
|
ReflectionDelta = Data.define(:content)
|
|
29
29
|
ReflectionEnd = Data.define(:decision, :feedback)
|
|
30
30
|
|
|
31
|
+
EvaluationStart = Data.define(:dimensions)
|
|
32
|
+
EvaluationDelta = Data.define(:content)
|
|
33
|
+
EvaluationEnd = Data.define(:decision, :feedback, :scores, :evidence)
|
|
34
|
+
EvaluationBlocked = Data.define(:feedback, :scores, :evidence)
|
|
35
|
+
|
|
31
36
|
MetaAgentAnalysis = Data.define(:results, :count)
|
|
32
37
|
|
|
33
38
|
Error = Data.define(:error, :recoverable)
|
|
@@ -3,14 +3,30 @@
|
|
|
3
3
|
module Ask
|
|
4
4
|
module Agent
|
|
5
5
|
module Extensions
|
|
6
|
-
class
|
|
6
|
+
class Permissions
|
|
7
7
|
DEFAULT_TOOLS = %i[write edit bash destroy].freeze
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
11
17
|
@timeout = timeout
|
|
12
18
|
@pending = {}
|
|
13
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
|
|
14
30
|
end
|
|
15
31
|
|
|
16
32
|
def before_tool_call(tool_call, _context)
|
|
@@ -61,7 +77,7 @@ module Ask
|
|
|
61
77
|
}
|
|
62
78
|
end
|
|
63
79
|
|
|
64
|
-
warn "[
|
|
80
|
+
warn "[Permissions] Tool '#{tool_call.name}' requires approval. Call approve('#{tool_call.id}') to allow."
|
|
65
81
|
{ action: :block, reason: "Tool '#{tool_call.name}' requires approval" }
|
|
66
82
|
end
|
|
67
83
|
end
|
data/lib/ask/agent/loop.rb
CHANGED
|
@@ -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
|
data/lib/ask/agent/session.rb
CHANGED
|
@@ -18,10 +18,10 @@ 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
|
-
agent_dir: nil, **chat_options)
|
|
24
|
+
agent_dir: nil, evaluator: nil, **chat_options)
|
|
25
25
|
@id = id || SecureRandom.uuid
|
|
26
26
|
@agent_dir = agent_dir
|
|
27
27
|
@max_turns = max_turns
|
|
@@ -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
|
-
@
|
|
54
|
+
@state = state || persistence
|
|
56
55
|
|
|
57
56
|
reflector_opts = reflector.is_a?(Hash) ? reflector : {}
|
|
58
57
|
@reflector = if reflector
|
|
@@ -66,6 +65,21 @@ module Ask
|
|
|
66
65
|
@meta_agent_results = nil
|
|
67
66
|
|
|
68
67
|
@compactor&.chat = @chat
|
|
68
|
+
|
|
69
|
+
# Parse evaluator configuration
|
|
70
|
+
@evaluator = nil
|
|
71
|
+
@evaluator_config = {}
|
|
72
|
+
|
|
73
|
+
if evaluator
|
|
74
|
+
eval_model = if evaluator.is_a?(Hash)
|
|
75
|
+
@evaluator_config = evaluator
|
|
76
|
+
evaluator[:model] || Ask::Agent.configuration.default_evaluator_model || model_id_from(@chat)
|
|
77
|
+
else
|
|
78
|
+
Ask::Agent.configuration.default_evaluator_model || model_id_from(@chat)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
@evaluator = Evaluator.new(model: eval_model)
|
|
82
|
+
end
|
|
69
83
|
end
|
|
70
84
|
|
|
71
85
|
def run(message, tools: nil)
|
|
@@ -89,16 +103,17 @@ module Ask
|
|
|
89
103
|
begin
|
|
90
104
|
@tool_executor.telemetry = @telemetry
|
|
91
105
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
106
|
+
response = @loop.run_turn(
|
|
107
|
+
chat: @chat,
|
|
108
|
+
message: message,
|
|
109
|
+
tools: active_tools,
|
|
110
|
+
tool_executor: @tool_executor,
|
|
111
|
+
compactor: @compactor,
|
|
112
|
+
hooks: @hooks,
|
|
113
|
+
event_emitter: self,
|
|
114
|
+
session_id: @id,
|
|
115
|
+
persist: @state ? method(:persist!) : nil
|
|
116
|
+
)
|
|
102
117
|
|
|
103
118
|
@total_input_tokens += @loop.last_input_tokens.to_i
|
|
104
119
|
@total_output_tokens += @loop.last_output_tokens.to_i
|
|
@@ -122,12 +137,67 @@ module Ask
|
|
|
122
137
|
raise
|
|
123
138
|
ensure
|
|
124
139
|
@running = false
|
|
125
|
-
persist! if @
|
|
140
|
+
persist! if @state
|
|
126
141
|
end
|
|
127
142
|
|
|
128
143
|
@tool_calls_made = @tool_executor.total_executions
|
|
129
144
|
|
|
130
|
-
|
|
145
|
+
# Independent evaluator step (generator/evaluator separation).
|
|
146
|
+
# Runs BEFORE self-reflection so the evaluator gets a fresh, unbiased look
|
|
147
|
+
# at the generator's output using a separate model and isolated context.
|
|
148
|
+
@skip_reflector = false
|
|
149
|
+
|
|
150
|
+
if @evaluator && !@abort_requested
|
|
151
|
+
goal = @evaluator_config[:goal] || message
|
|
152
|
+
|
|
153
|
+
eval_result = @evaluator.evaluate(
|
|
154
|
+
goal: goal.to_s,
|
|
155
|
+
response: response,
|
|
156
|
+
event_emitter: self
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
@telemetry.log(:evaluation_end, session_id: @id,
|
|
160
|
+
decision: eval_result.decision,
|
|
161
|
+
feedback: eval_result.feedback,
|
|
162
|
+
scores: eval_result.scores)
|
|
163
|
+
|
|
164
|
+
case eval_result.decision
|
|
165
|
+
when :revise
|
|
166
|
+
@chat.add_message(
|
|
167
|
+
role: :system,
|
|
168
|
+
content: "An independent evaluator has requested revisions:\n\n#{eval_result.feedback}"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
response = @loop.run_turn(
|
|
172
|
+
chat: @chat,
|
|
173
|
+
message: "",
|
|
174
|
+
tools: active_tools,
|
|
175
|
+
tool_executor: @tool_executor,
|
|
176
|
+
compactor: @compactor,
|
|
177
|
+
hooks: @hooks,
|
|
178
|
+
event_emitter: self,
|
|
179
|
+
session_id: @id
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
@total_input_tokens += @loop.last_input_tokens.to_i
|
|
183
|
+
@total_output_tokens += @loop.last_output_tokens.to_i
|
|
184
|
+
@total_cost += @loop.last_cost.to_f
|
|
185
|
+
|
|
186
|
+
# Skip reflector — we already iterated based on evaluator feedback
|
|
187
|
+
@skip_reflector = true
|
|
188
|
+
when :block
|
|
189
|
+
emit(Events::EvaluationBlocked.new(
|
|
190
|
+
feedback: eval_result.feedback,
|
|
191
|
+
scores: eval_result.scores,
|
|
192
|
+
evidence: eval_result.evidence
|
|
193
|
+
))
|
|
194
|
+
response = "This response was blocked by the evaluator: #{eval_result.feedback}"
|
|
195
|
+
when :accept
|
|
196
|
+
# Fall through to reflector for backward compatibility
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
if @reflector && !@skip_reflector && @reflector.reflect?(@tool_calls_made) && !@abort_requested
|
|
131
201
|
eval_result = @reflector.evaluate(response: response, event_emitter: self)
|
|
132
202
|
@telemetry.log(:reflection_end, session_id: @id, decision: eval_result[:decision], feedback: eval_result[:feedback])
|
|
133
203
|
|
|
@@ -193,34 +263,37 @@ module Ask
|
|
|
193
263
|
def deleted? = @deleted
|
|
194
264
|
|
|
195
265
|
def save
|
|
196
|
-
persist! if @
|
|
266
|
+
persist! if @state
|
|
197
267
|
end
|
|
198
268
|
|
|
199
269
|
def self.load(id, adapter:)
|
|
200
|
-
data = adapter.
|
|
270
|
+
data = adapter.get(id)
|
|
201
271
|
return nil unless data
|
|
202
272
|
|
|
273
|
+
data = deep_symbolize_keys(data)
|
|
274
|
+
|
|
203
275
|
session = new(
|
|
204
276
|
id: data[:id],
|
|
205
277
|
model: data.dig(:metadata, :model),
|
|
206
278
|
tools: data.dig(:metadata, :tools)&.map(&:constantize) || [],
|
|
207
|
-
|
|
279
|
+
state: adapter
|
|
208
280
|
)
|
|
209
281
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
282
|
+
data[:messages].each do |msg|
|
|
283
|
+
session.chat.add_message(
|
|
284
|
+
role: msg[:role].to_sym,
|
|
285
|
+
content: msg[:content],
|
|
286
|
+
tool_call_id: msg[:tool_call_id]
|
|
287
|
+
)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
session.instance_variable_set(:@messages, session.chat.messages.dup)
|
|
291
|
+
session
|
|
219
292
|
end
|
|
220
293
|
|
|
221
294
|
def delete
|
|
222
295
|
@deleted = true
|
|
223
|
-
@
|
|
296
|
+
@state&.delete(@id)
|
|
224
297
|
end
|
|
225
298
|
|
|
226
299
|
def abort
|
|
@@ -290,7 +363,7 @@ module Ask
|
|
|
290
363
|
end
|
|
291
364
|
|
|
292
365
|
def persist!
|
|
293
|
-
@
|
|
366
|
+
@state.set(@id, {
|
|
294
367
|
id: @id,
|
|
295
368
|
messages: @chat.messages.map { |m|
|
|
296
369
|
{
|
|
@@ -358,6 +431,19 @@ module Ask
|
|
|
358
431
|
SystemContext.new(sources)
|
|
359
432
|
end
|
|
360
433
|
|
|
434
|
+
# Recursively convert string keys to symbol keys in hashes.
|
|
435
|
+
# Needed when loading session data that was serialized through JSON.
|
|
436
|
+
def self.deep_symbolize_keys(obj)
|
|
437
|
+
case obj
|
|
438
|
+
when Hash
|
|
439
|
+
obj.each_with_object({}) { |(k, v), h| h[k.to_sym] = deep_symbolize_keys(v) }
|
|
440
|
+
when Array
|
|
441
|
+
obj.map { |e| deep_symbolize_keys(e) }
|
|
442
|
+
else
|
|
443
|
+
obj
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
361
447
|
# Render the system context and apply it to the chat.
|
|
362
448
|
def apply_system_context
|
|
363
449
|
rendered = @system_context.render
|
data/lib/ask/agent/version.rb
CHANGED
data/lib/ask/agent.rb
CHANGED
|
@@ -21,7 +21,7 @@ module Ask
|
|
|
21
21
|
class UnknownAgent < Error; end
|
|
22
22
|
|
|
23
23
|
module Extensions
|
|
24
|
-
autoload :
|
|
24
|
+
autoload :Permissions, "ask/agent/extensions/permissions"
|
|
25
25
|
autoload :RateLimiter, "ask/agent/extensions/rate_limiter"
|
|
26
26
|
autoload :AuditLog, "ask/agent/extensions/audit_log"
|
|
27
27
|
end
|
|
@@ -32,6 +32,7 @@ module Ask
|
|
|
32
32
|
autoload :RetryOnFailure, "ask/agent/middleware/retry_on_failure"
|
|
33
33
|
autoload :LogCalls, "ask/agent/middleware/log_calls"
|
|
34
34
|
autoload :DefaultSettings, "ask/agent/middleware/default_settings"
|
|
35
|
+
autoload :ModelFallback, "ask/agent/middleware/model_fallback"
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
module StreamTransforms
|
|
@@ -231,6 +232,7 @@ require_relative "agent/tool_abort_controller"
|
|
|
231
232
|
require_relative "agent/session"
|
|
232
233
|
require_relative "agent/loop"
|
|
233
234
|
require_relative "agent/reflector"
|
|
235
|
+
require_relative "agent/evaluator"
|
|
234
236
|
require_relative "agent/tool_executor"
|
|
235
237
|
require_relative "agent/compactor"
|
|
236
238
|
require_relative "agent/hooks"
|
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.
|
|
4
|
+
version: 0.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kaka Ruto
|
|
@@ -23,6 +23,20 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: ask-state-providers
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.1'
|
|
26
40
|
- !ruby/object:Gem::Dependency
|
|
27
41
|
name: ask-llm-providers
|
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -157,9 +171,10 @@ files:
|
|
|
157
171
|
- lib/ask/agent/context_source.rb
|
|
158
172
|
- lib/ask/agent/context_sources.rb
|
|
159
173
|
- lib/ask/agent/definition.rb
|
|
174
|
+
- lib/ask/agent/evaluator.rb
|
|
160
175
|
- lib/ask/agent/events.rb
|
|
161
176
|
- lib/ask/agent/extensions/audit_log.rb
|
|
162
|
-
- lib/ask/agent/extensions/
|
|
177
|
+
- lib/ask/agent/extensions/permissions.rb
|
|
163
178
|
- lib/ask/agent/extensions/rate_limiter.rb
|
|
164
179
|
- lib/ask/agent/hooks.rb
|
|
165
180
|
- lib/ask/agent/loop.rb
|
|
@@ -167,6 +182,7 @@ files:
|
|
|
167
182
|
- lib/ask/agent/middleware/base.rb
|
|
168
183
|
- lib/ask/agent/middleware/default_settings.rb
|
|
169
184
|
- lib/ask/agent/middleware/log_calls.rb
|
|
185
|
+
- lib/ask/agent/middleware/model_fallback.rb
|
|
170
186
|
- lib/ask/agent/middleware/pipeline.rb
|
|
171
187
|
- lib/ask/agent/middleware/retry_on_failure.rb
|
|
172
188
|
- lib/ask/agent/persistence/base.rb
|