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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bbb7c5376d7ba7cf03099123c5aab9f12dd9a10efca9906fd07ea2782b28d296
4
- data.tar.gz: 8c82d88e763cdf7e765fec8837b22ab5dc8a5a7cbaca01d9ffff59c8328a1e99
3
+ metadata.gz: d6d5dd31344228811bd4c7993547c9e0f9d7c39c779f58250d82ad95f271bd7d
4
+ data.tar.gz: ba08c57e9fc7dfc410e6a835ffab1076d1f4b5704bee866f7a38e158da712d34
5
5
  SHA512:
6
- metadata.gz: f886aedfbed678ce466c545e9f20255320a5ac77930c60d164613e833345455c52de0256f9c1a9357cda6040a79c2f17ed6aa687c5b8f135c6d355a50ecea5b6
7
- data.tar.gz: be66be79539f4f284c8741ec634f17c4e5a12d18e4cc182209be5ac5f8988101589b10dfe331234cc1bb6880aa1a2c0af0ffbe4817b9ab382dd3fbcb2d6fd6b1
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. The core agent loop: think call tools → execute → feed back → repeat.
4
-
5
- Ported from `RubyLLM::Conductor` into the `Ask::Agent` namespace.
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
- ## Components
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
- ## Extensions
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
- **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
-
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
- skills go in `agents/<name>/skills/`, shared skills in `agents/shared/skills/`.
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
- ## Configuration
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
- model name doesn't uniquely identify one (for example, the same model id
268
- registered under multiple OpenAI-compatible providers). A `provider:`
269
- passed to `Session.new` or declared in an agent `Definition` always wins
270
- over the global default.
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
- ## Persistence
105
+ ## Full documentation
273
106
 
274
- ```ruby
275
- store = Ask::Agent::Persistence::InMemory.new
276
- session = Ask::Agent::Session.new(model: "gpt-4o", persistence: store)
277
- session.run("Hello")
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
- ```bash
114
+ bundle install
284
115
  bundle exec rake test
285
- ```
286
116
 
287
117
  ## License
288
118
 
@@ -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].to_s.truncate(80) }.first(2).join("; ")
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
 
@@ -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
@@ -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.to_s.truncate(200) }
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Agent
5
- VERSION = "0.25.0"
5
+ VERSION = "0.25.2"
6
6
  end
7
7
  end
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.25.0
4
+ version: 0.25.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto