ask-agent 0.25.0 → 0.25.2
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 +21 -0
- data/README.md +38 -208
- data/lib/ask/agent/loop.rb +10 -1
- data/lib/ask/agent/session.rb +1 -1
- data/lib/ask/agent/streaming.rb +10 -1
- data/lib/ask/agent/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d6d5dd31344228811bd4c7993547c9e0f9d7c39c779f58250d82ad95f271bd7d
|
|
4
|
+
data.tar.gz: ba08c57e9fc7dfc410e6a835ffab1076d1f4b5704bee866f7a38e158da712d34
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5d46b653b22b50dee9597238cc65ce5fe72c3840322e1ecd03efccfe6494a4494bff473f3bc3c4eff1ae56df849142a45f52bc77e23c9a93a9c240f465d7db68
|
|
7
|
+
data.tar.gz: 9c158ab0aa0be906723681458ea54d90b15cd7289647e7acb8527faf367ead4490919a84b0a2e71186b4b93945ac065db063a33ead26b2532d0d50efd2dd8bf3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
## [0.25.2] - 2026-08-03
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **Streaming no longer depends on ActiveSupport's `String#truncate`.** The
|
|
6
|
+
SSE event serialization (streaming.rb) and the max-consecutive-tool-turns
|
|
7
|
+
summary (loop.rb) called `String#truncate`, which only exists when
|
|
8
|
+
ActiveSupport's core extensions are loaded — so a bare `require
|
|
9
|
+
"ask-agent"` raised `NoMethodError` as soon as a tool emitted a partial
|
|
10
|
+
result. Both call sites now use a plain-Ruby truncation helper.
|
|
11
|
+
|
|
12
|
+
## [0.25.1] - 2026-08-02
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **Session passes resolved tool instances to Chat.** Tool classes passed
|
|
17
|
+
as `tools: [MyTool]` were resolved for the session but handed to the
|
|
18
|
+
underlying Chat unresolved, so `ToolDef.from_tool` used `Class#name`
|
|
19
|
+
and raised `Ask::InvalidToolDefinition` on the first run. Sessions now
|
|
20
|
+
resolve tools before building the Chat; classes and instances both work.
|
|
21
|
+
|
|
1
22
|
## [0.25.0] — 2026-08-02
|
|
2
23
|
|
|
3
24
|
### Added
|
data/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# ask-agent
|
|
2
2
|
|
|
3
|
-
Agent runtime for the ask-rb ecosystem.
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Agent runtime for the ask-rb ecosystem. Runs the core agent loop: think, call
|
|
4
|
+
tools, execute, feed results back, and repeat until the task is done. Built on
|
|
5
|
+
ask-core, ask-state-providers, ask-llm-providers, ask-tools, ask-skills, and
|
|
6
|
+
ask-instrumentation, and it powers the `askr` CLI.
|
|
6
7
|
|
|
7
8
|
## Installation
|
|
8
9
|
|
|
@@ -15,114 +16,12 @@ gem "ask-agent"
|
|
|
15
16
|
```ruby
|
|
16
17
|
require "ask-agent"
|
|
17
18
|
|
|
18
|
-
session = Ask::Agent::Session.new(
|
|
19
|
-
model: "gpt-4o",
|
|
20
|
-
tools: [Ask::Tools::Shell::Bash, Ask::Tools::Shell::Read]
|
|
21
|
-
)
|
|
22
|
-
|
|
19
|
+
session = Ask::Agent::Session.new(model: "gpt-4o", max_turns: 25)
|
|
23
20
|
response = session.run("What files are in the current directory?")
|
|
24
21
|
puts response
|
|
25
22
|
```
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
| Component | File | Purpose |
|
|
30
|
-
|---|---|---|
|
|
31
|
-
| `Ask::Agent::Session` | session.rb | Full agent loop — message → tool calls → results → follow-up |
|
|
32
|
-
| `Ask::Agent::Loop` | loop.rb | Turn management, loop detection, max-turn guard |
|
|
33
|
-
| `Ask::Agent::ToolExecutor` | tool_executor.rb | Parallel/sequential tool execution with retry and abort |
|
|
34
|
-
| `Ask::Agent::Compactor` | compactor.rb | Context window management with proactive/overflow compaction |
|
|
35
|
-
| `Ask::Agent::Hooks` | hooks.rb | Before/after tool lifecycle callbacks |
|
|
36
|
-
| `Ask::Agent::Events` | events.rb | Data.define event types for streaming and monitoring |
|
|
37
|
-
| `Ask::Agent::Telemetry` | telemetry.rb | File-backed telemetry for error tracking |
|
|
38
|
-
| `Ask::Agent::Reflector` | reflector.rb | Assistant response self-evaluation |
|
|
39
|
-
| `Ask::Agent::MetaAgent` | meta_agent.rb | LLM-powered self-improvement from telemetry |
|
|
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
|
-
```
|
|
122
|
-
|
|
123
|
-
## Events
|
|
124
|
-
|
|
125
|
-
Stream session execution in real-time:
|
|
24
|
+
Stream execution in real time with events:
|
|
126
25
|
|
|
127
26
|
```ruby
|
|
128
27
|
session.on_event do |event|
|
|
@@ -131,95 +30,17 @@ session.on_event do |event|
|
|
|
131
30
|
print event.content
|
|
132
31
|
when Ask::Agent::Events::ToolExecutionStart
|
|
133
32
|
puts "\nRunning #{event.name}..."
|
|
134
|
-
when Ask::Agent::Events::ToolExecutionEnd
|
|
135
|
-
puts " → #{event.duration_ms}ms #{event.is_error ? 'error' : 'ok'}"
|
|
136
33
|
end
|
|
137
34
|
end
|
|
138
35
|
```
|
|
139
36
|
|
|
140
|
-
##
|
|
141
|
-
|
|
142
|
-
Opt-in safety modules:
|
|
143
|
-
|
|
144
|
-
- **Permissions** — Access control for tools. Supports named access modes (`:full_access`, `:read_only`, `:ask_before_changes`) or custom blocked-tool lists.
|
|
145
|
-
- **RateLimiter** — Prevent runaway tool calls (configurable per-minute and per-turn limits)
|
|
146
|
-
- **AuditLog** — Immutable, append-only log of every tool call
|
|
147
|
-
|
|
148
|
-
```ruby
|
|
149
|
-
extensions = [
|
|
150
|
-
Ask::Agent::Extensions::Permissions.new(mode: :read_only),
|
|
151
|
-
Ask::Agent::Extensions::RateLimiter.new(max_calls_per_minute: 30),
|
|
152
|
-
Ask::Agent::Extensions::AuditLog.new(path: "agent.log")
|
|
153
|
-
]
|
|
154
|
-
|
|
155
|
-
session = Ask::Agent::Session.new(
|
|
156
|
-
model: "gpt-4o",
|
|
157
|
-
tools: [...],
|
|
158
|
-
hooks: {
|
|
159
|
-
before_tool: extensions.map(&:method(:before_tool_call)),
|
|
160
|
-
after_tool: extensions.select { |e| e.respond_to?(:after_tool_call) }.map(&:method(:after_tool_call))
|
|
161
|
-
}
|
|
162
|
-
)
|
|
163
|
-
```
|
|
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
|
-
```
|
|
37
|
+
## Declarative Agents
|
|
208
38
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
## Agents
|
|
217
|
-
|
|
218
|
-
Declarative agents follow a file convention. Each agent lives in a
|
|
219
|
-
directory under `agents/` (or `app/agents/` in Rails); the directory
|
|
220
|
-
name is the agent name, the file `agent.rb` defines the agent as a
|
|
221
|
-
`<Name>::Agent < Ask::Agent::Definition` subclass, and a sibling
|
|
222
|
-
`instructions.md` is auto-loaded as the system prompt.
|
|
39
|
+
Agents follow a file convention. Each agent lives in a directory under
|
|
40
|
+
`agents/` (or `app/agents/` in Rails); the directory name is the agent name,
|
|
41
|
+
the file `agent.rb` defines the agent as a `<Name>::Agent <
|
|
42
|
+
Ask::Agent::Definition` subclass, and a sibling `instructions.md` is
|
|
43
|
+
auto-loaded as the system prompt.
|
|
223
44
|
|
|
224
45
|
```
|
|
225
46
|
agents/
|
|
@@ -246,10 +67,22 @@ agent = Ask::Agent.new("health_check")
|
|
|
246
67
|
response = agent.run("Check server health")
|
|
247
68
|
```
|
|
248
69
|
|
|
249
|
-
Shared tools for all agents go in `agents/shared/tools/`. Per-agent
|
|
250
|
-
|
|
70
|
+
Shared tools for all agents go in `agents/shared/tools/`. Per-agent skills go
|
|
71
|
+
in `agents/<name>/skills/`, shared skills in `agents/shared/skills/`.
|
|
72
|
+
|
|
73
|
+
## Essential API
|
|
74
|
+
|
|
75
|
+
| Entry point | Purpose |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `Ask::Agent::Session.new(model:, tools: [], max_turns: 25, ...)` | Full agent loop: message, tool calls, results, follow-up |
|
|
78
|
+
| `session.run(message)` | Run the loop for one message |
|
|
79
|
+
| `session.on_event { \|e\| }` | Stream `Ask::Agent::Events` (text deltas, tool execution, evaluation) |
|
|
80
|
+
| `Ask::Agent.new("name")` | Build a session from a declarative agent definition |
|
|
81
|
+
| `Ask.chat(message)` | One-shot chat without instantiating a Session |
|
|
82
|
+
| `Ask::Agent.configure { \|c\| ... }` | Global defaults: model, provider, turns, compactor, middleware |
|
|
83
|
+
| `askr` | CLI: `askr run <agent> [prompt]`, `askr list`, `askr schedule`, `askr new`, `askr skills` |
|
|
251
84
|
|
|
252
|
-
|
|
85
|
+
### Configuration
|
|
253
86
|
|
|
254
87
|
```ruby
|
|
255
88
|
Ask::Agent.configure do |c|
|
|
@@ -263,26 +96,23 @@ Ask::Agent.configure do |c|
|
|
|
263
96
|
end
|
|
264
97
|
```
|
|
265
98
|
|
|
266
|
-
`default_provider` pins which provider serves the default model when the
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
99
|
+
`default_provider` pins which provider serves the default model when the model
|
|
100
|
+
name doesn't uniquely identify one (for example, the same model id registered
|
|
101
|
+
under multiple OpenAI-compatible providers). A `provider:` passed to
|
|
102
|
+
`Session.new` or declared in an agent `Definition` always wins over the global
|
|
103
|
+
default.
|
|
271
104
|
|
|
272
|
-
##
|
|
105
|
+
## Full documentation
|
|
273
106
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
session.save # persisted to store
|
|
279
|
-
```
|
|
107
|
+
The full ask-rb documentation lives at https://ask-rb.github.io/ask-docs.
|
|
108
|
+
https://ask-rb.github.io/ask-docs/core/agent covers ask-agent in depth,
|
|
109
|
+
including the evaluator, middleware, extensions, cost tracking, and
|
|
110
|
+
persistence. API reference: https://ask-rb.github.io/ask-docs/reference/api.
|
|
280
111
|
|
|
281
112
|
## Development
|
|
282
113
|
|
|
283
|
-
|
|
114
|
+
bundle install
|
|
284
115
|
bundle exec rake test
|
|
285
|
-
```
|
|
286
116
|
|
|
287
117
|
## License
|
|
288
118
|
|
data/lib/ask/agent/loop.rb
CHANGED
|
@@ -94,7 +94,7 @@ module Ask
|
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
if @consecutive_tool_turns >= @max_consecutive_tool_turns
|
|
97
|
-
summary = all_tool_results.map { |r| r[:message]
|
|
97
|
+
summary = all_tool_results.map { |r| truncate(r[:message], 80) }.first(2).join("; ")
|
|
98
98
|
return "Based on my investigation: #{summary}"
|
|
99
99
|
end
|
|
100
100
|
|
|
@@ -137,6 +137,15 @@ module Ask
|
|
|
137
137
|
|
|
138
138
|
private
|
|
139
139
|
|
|
140
|
+
# Truncate a string for summaries without depending on ActiveSupport's
|
|
141
|
+
# String#truncate (which is not loaded by a bare `require "ask-agent"`).
|
|
142
|
+
def truncate(text, length)
|
|
143
|
+
s = text.to_s
|
|
144
|
+
return s if s.length <= length
|
|
145
|
+
|
|
146
|
+
"#{s[0, length - 3]}..."
|
|
147
|
+
end
|
|
148
|
+
|
|
140
149
|
def loop_detected?(results)
|
|
141
150
|
return false if results.empty?
|
|
142
151
|
|
data/lib/ask/agent/session.rb
CHANGED
|
@@ -41,8 +41,8 @@ module Ask
|
|
|
41
41
|
|
|
42
42
|
@telemetry = telemetry.is_a?(Telemetry) ? telemetry : Telemetry.new(enabled: !!telemetry)
|
|
43
43
|
|
|
44
|
-
@chat = build_chat(model, system_prompt, tools, **chat_options)
|
|
45
44
|
@tools = resolve_tools(tools)
|
|
45
|
+
@chat = build_chat(model, system_prompt, @tools, **chat_options)
|
|
46
46
|
@loop = Loop.new(max_turns: max_turns)
|
|
47
47
|
@tool_executor = ToolExecutor.new(max_retries: max_tool_retries, parallel: parallel_tools)
|
|
48
48
|
@compactor = compactor ? build_compactor(compactor) : nil
|
data/lib/ask/agent/streaming.rb
CHANGED
|
@@ -82,6 +82,15 @@ module Ask
|
|
|
82
82
|
|
|
83
83
|
private
|
|
84
84
|
|
|
85
|
+
# Truncate a string for telemetry without depending on ActiveSupport's
|
|
86
|
+
# String#truncate (which is not loaded by a bare `require "ask-agent"`).
|
|
87
|
+
def truncate(text, length)
|
|
88
|
+
s = text.to_s
|
|
89
|
+
return s if s.length <= length
|
|
90
|
+
|
|
91
|
+
"#{s[0, length - 3]}..."
|
|
92
|
+
end
|
|
93
|
+
|
|
85
94
|
def run_with_block(session, prompt, mapping)
|
|
86
95
|
errors = []
|
|
87
96
|
|
|
@@ -154,7 +163,7 @@ module Ask
|
|
|
154
163
|
when Events::ToolExecutionStart
|
|
155
164
|
{ name: event.name, id: event.id, args: safe_args(event.arguments) }
|
|
156
165
|
when Events::ToolExecutionUpdate
|
|
157
|
-
{ id: event.id, partial_result: event.partial_result
|
|
166
|
+
{ id: event.id, partial_result: truncate(event.partial_result, 200) }
|
|
158
167
|
when Events::ToolExecutionEnd
|
|
159
168
|
{ name: event.name, id: event.id, duration_ms: event.duration_ms, is_error: event.is_error }
|
|
160
169
|
when Events::SessionEnd
|
data/lib/ask/agent/version.rb
CHANGED