ask-agent 0.27.0 → 0.28.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 +41 -0
- data/lib/ask/agent/chat.rb +55 -23
- data/lib/ask/agent/{extensions → policies}/approval_policy.rb +2 -2
- data/lib/ask/agent/{extensions → policies}/audit_log/active_record_writer.rb +1 -1
- data/lib/ask/agent/{extensions → policies}/audit_log.rb +4 -4
- data/lib/ask/agent/{extensions → policies}/permissions.rb +1 -1
- data/lib/ask/agent/{extensions → policies}/rate_limiter.rb +1 -1
- data/lib/ask/agent/session.rb +2 -2
- data/lib/ask/agent/version.rb +1 -1
- data/lib/ask/agent.rb +16 -7
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 36e26d74ffa1c3e3e97b8e172f4fef2ada07c1c194488a8f550e8b77b7473ee1
|
|
4
|
+
data.tar.gz: 82568a4cc3ed19617293416d10f2f3194c1a18079a1426431a2d878afee75fa0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cfd202f83bb95e3349282c3771e591f93062976b58c964d65e928e09a6408818ef969200c10b68c4a307e0c0d9fabde112aaf20b9776110a5040590370ecd317
|
|
7
|
+
data.tar.gz: 7d3ae82b872659e9e27217bc98cf7dfffee28f5209ee5f351dcc944564a63a777888fdad780de218956019d38f223996ebd6372c7be84cf33fdbf9a1eeb4e5a7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
+
## [0.28.0] — 2026-08-06
|
|
2
|
+
|
|
3
|
+
### Changed (breaking)
|
|
4
|
+
|
|
5
|
+
- **`Ask::Agent::Extensions` is now `Ask::Agent::Policies`.** The
|
|
6
|
+
tool-lifecycle policy classes — `ApprovalPolicy`, `Permissions`,
|
|
7
|
+
`RateLimiter`, `AuditLog` — moved from `lib/ask/agent/extensions/` to
|
|
8
|
+
`lib/ask/agent/policies/` and are namespaced under `Ask::Agent::Policies`.
|
|
9
|
+
The folder is now named after its seam (like `middleware`,
|
|
10
|
+
`stream_transforms`, `persistence`) instead of "extra stuff".
|
|
11
|
+
`Ask::Agent.load_extensions` → `Ask::Agent.load_policies`.
|
|
12
|
+
Update references: `Ask::Agent::Extensions::X` → `Ask::Agent::Policies::X`.
|
|
13
|
+
|
|
14
|
+
**What this means for the taxonomy:** policies are opt-in, replaceable
|
|
15
|
+
implementations of the tool-lifecycle hook seam — the agent loop runs
|
|
16
|
+
without them, and users can swap in their own implementations. Core
|
|
17
|
+
mechanisms are unchanged and stay on `Session`: the approval queue, the
|
|
18
|
+
`:pending` result status, and the `approval: true` option are core;
|
|
19
|
+
`Policies::ApprovalPolicy` is the reference classification policy wired on
|
|
20
|
+
top of them.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **`Ask::Agent.load_policies`** — replaces `load_extensions` (same
|
|
25
|
+
behavior: eagerly requires every policy in the policies directory).
|
|
26
|
+
|
|
27
|
+
## [0.27.1] — 2026-08-06
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **`chat.ask` / `chat.stream.ask` events now measure real LLM latency.** The
|
|
32
|
+
event was emitted after the call without a block, so `event.duration` was
|
|
33
|
+
~0ms and duration metrics (e.g. `ask_llm_duration_seconds`,
|
|
34
|
+
`llm.duration_ms` spans) were meaningless. The provider call now runs
|
|
35
|
+
inside the instrument block; tokens/cost/tool_calls are enriched through a
|
|
36
|
+
shared nested `usage` payload hash (known only after the call returns) and
|
|
37
|
+
subscribers read it from there. Instrumentation failures can no longer
|
|
38
|
+
fail an `ask` — a wrapper error before the call falls through and runs the
|
|
39
|
+
call without telemetry, and a subscriber error after success returns the
|
|
40
|
+
response.
|
|
41
|
+
|
|
1
42
|
## [0.27.0] — 2026-08-06
|
|
2
43
|
|
|
3
44
|
### Added
|
data/lib/ask/agent/chat.rb
CHANGED
|
@@ -73,8 +73,6 @@ module Ask
|
|
|
73
73
|
}.compact
|
|
74
74
|
)
|
|
75
75
|
|
|
76
|
-
emit_instrumentation(stream, response_msg)
|
|
77
|
-
|
|
78
76
|
response_msg
|
|
79
77
|
end
|
|
80
78
|
|
|
@@ -208,20 +206,35 @@ module Ask
|
|
|
208
206
|
begin
|
|
209
207
|
req = build_request(stream)
|
|
210
208
|
|
|
211
|
-
|
|
212
|
-
|
|
209
|
+
# The chat.ask event wraps the actual provider call so
|
|
210
|
+
# event.duration measures the true LLM latency. Tokens/cost are
|
|
211
|
+
# only known once the call returns, so the (mutable) payload is
|
|
212
|
+
# passed into the block and enriched there — before the event's
|
|
213
|
+
# finish fires.
|
|
214
|
+
response = instrument_llm_call(stream) do |payload|
|
|
215
|
+
result = if @middleware_pipeline
|
|
216
|
+
@middleware_pipeline.invoke(provider, req) do
|
|
217
|
+
call_provider(req, calls_acc, &block)
|
|
218
|
+
end
|
|
219
|
+
else
|
|
213
220
|
call_provider(req, calls_acc, &block)
|
|
214
221
|
end
|
|
215
|
-
else
|
|
216
|
-
call_provider(req, calls_acc, &block)
|
|
217
|
-
end
|
|
218
222
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
223
|
+
# Flush any buffered stream transforms (e.g. TextBuffer)
|
|
224
|
+
if block && @transform_pipeline
|
|
225
|
+
flush_transforms(&block)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
response = build_response_from_result(result, calls_acc, stream)
|
|
229
|
+
usage = payload[:usage] ||= {}
|
|
230
|
+
usage[:input_tokens] = response.input_tokens
|
|
231
|
+
usage[:output_tokens] = response.output_tokens
|
|
232
|
+
usage[:cost] = response.cost
|
|
233
|
+
usage[:tool_calls] = response.tool_call?
|
|
234
|
+
response
|
|
222
235
|
end
|
|
223
236
|
|
|
224
|
-
return
|
|
237
|
+
return response
|
|
225
238
|
rescue Ask::RateLimitError => e
|
|
226
239
|
raise if attempt >= MAX_CHAT_RETRIES - 1
|
|
227
240
|
|
|
@@ -357,28 +370,47 @@ module Ask
|
|
|
357
370
|
nil
|
|
358
371
|
end
|
|
359
372
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
373
|
+
# Run the provider call inside the chat.ask event so event.duration
|
|
374
|
+
# measures the real LLM latency. The event payload is yielded to the
|
|
375
|
+
# block so the caller can enrich it (tokens, cost) before the event
|
|
376
|
+
# finishes.
|
|
377
|
+
#
|
|
378
|
+
# Instrumentation must never break the chat loop: a subscriber error
|
|
379
|
+
# after the call succeeded is swallowed (the response is returned);
|
|
380
|
+
# an error before the block ran falls through and runs the call
|
|
381
|
+
# without telemetry. Only real LLM errors propagate.
|
|
382
|
+
def instrument_llm_call(stream)
|
|
363
383
|
payload = {
|
|
364
384
|
model: @model_id,
|
|
365
385
|
provider: @model_info.provider,
|
|
366
|
-
input_tokens: response_msg.input_tokens,
|
|
367
|
-
output_tokens: response_msg.output_tokens,
|
|
368
|
-
cost: response_msg.cost,
|
|
369
|
-
tool_calls: response_msg.tool_call?,
|
|
370
386
|
stream: stream,
|
|
371
387
|
middleware: @middleware_pipeline&.configured?,
|
|
372
|
-
stream_transforms: @transform_pipeline&.configured
|
|
388
|
+
stream_transforms: @transform_pipeline&.configured?,
|
|
389
|
+
# Tokens/cost are only known once the call returns, and
|
|
390
|
+
# instrument() copies the payload shallowly — this nested hash is
|
|
391
|
+
# shared with the event, so in-block enrichment is visible to
|
|
392
|
+
# subscribers at finish time.
|
|
393
|
+
usage: {}
|
|
373
394
|
}.compact
|
|
374
395
|
|
|
375
|
-
|
|
376
|
-
|
|
396
|
+
called = false
|
|
397
|
+
result = nil
|
|
398
|
+
if defined?(Ask::Instrumentation)
|
|
399
|
+
Ask::Instrumentation.instrument(stream ? "chat.stream.ask" : "chat.ask", payload) do
|
|
400
|
+
called = true
|
|
401
|
+
result = yield(payload)
|
|
402
|
+
end
|
|
377
403
|
else
|
|
378
|
-
|
|
404
|
+
called = true
|
|
405
|
+
result = yield(payload)
|
|
379
406
|
end
|
|
407
|
+
result
|
|
380
408
|
rescue StandardError
|
|
381
|
-
nil
|
|
409
|
+
return result if called && !result.nil?
|
|
410
|
+
|
|
411
|
+
raise if called
|
|
412
|
+
|
|
413
|
+
yield({})
|
|
382
414
|
end
|
|
383
415
|
end
|
|
384
416
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Ask
|
|
4
4
|
module Agent
|
|
5
|
-
module
|
|
5
|
+
module Policies
|
|
6
6
|
# Approval policy hook: classifies tool calls as approval-required and
|
|
7
7
|
# routes them into an {Ask::Agent::ApprovalQueue}.
|
|
8
8
|
#
|
|
@@ -15,7 +15,7 @@ module Ask
|
|
|
15
15
|
#
|
|
16
16
|
# @example
|
|
17
17
|
# queue = Ask::Agent::ApprovalQueue.new
|
|
18
|
-
# policy = Ask::Agent::
|
|
18
|
+
# policy = Ask::Agent::Policies::ApprovalPolicy.new(queue: queue)
|
|
19
19
|
# session = Ask::Agent::Session.new(
|
|
20
20
|
# model: "gpt-4o",
|
|
21
21
|
# tools: [SendEmail],
|
|
@@ -5,7 +5,7 @@ require "time"
|
|
|
5
5
|
|
|
6
6
|
module Ask
|
|
7
7
|
module Agent
|
|
8
|
-
module
|
|
8
|
+
module Policies
|
|
9
9
|
# Event-driven audit log for agent sessions.
|
|
10
10
|
#
|
|
11
11
|
# Subscribes to all session events and writes them to a configurable
|
|
@@ -93,16 +93,16 @@ module Ask
|
|
|
93
93
|
|
|
94
94
|
case adapter
|
|
95
95
|
when :active_record
|
|
96
|
-
require "ask/agent/
|
|
96
|
+
require "ask/agent/policies/audit_log/active_record_writer"
|
|
97
97
|
AuditLog::ActiveRecordWriter.new
|
|
98
98
|
when Hash
|
|
99
99
|
resolve(adapter[:adapter] || adapter[:writer])
|
|
100
100
|
when Symbol, String
|
|
101
101
|
# Try to load adapter by convention:
|
|
102
|
-
# :active_record → ask/agent/
|
|
102
|
+
# :active_record → ask/agent/policies/audit_log/active_record_writer
|
|
103
103
|
name = adapter.to_s
|
|
104
104
|
begin
|
|
105
|
-
require "ask/agent/
|
|
105
|
+
require "ask/agent/policies/audit_log/#{name}_writer"
|
|
106
106
|
klass_name = name.split("_").map(&:capitalize).join
|
|
107
107
|
klass = AuditLog.const_get(klass_name)
|
|
108
108
|
klass.new
|
data/lib/ask/agent/session.rb
CHANGED
|
@@ -421,7 +421,7 @@ module Ask
|
|
|
421
421
|
def build_audit_log(config)
|
|
422
422
|
config ||= Ask::Agent.configuration.audit_log
|
|
423
423
|
return nil unless config
|
|
424
|
-
Ask::Agent::
|
|
424
|
+
Ask::Agent::Policies::AuditLog.new(self, adapter: config)
|
|
425
425
|
end
|
|
426
426
|
|
|
427
427
|
# Build the approval queue + policy when approval is enabled.
|
|
@@ -451,7 +451,7 @@ module Ask
|
|
|
451
451
|
)
|
|
452
452
|
end
|
|
453
453
|
|
|
454
|
-
policy = Ask::Agent::
|
|
454
|
+
policy = Ask::Agent::Policies::ApprovalPolicy.new(
|
|
455
455
|
queue: queue,
|
|
456
456
|
require_approval: policy_opts[:require_approval],
|
|
457
457
|
tools: @tools
|
data/lib/ask/agent/version.rb
CHANGED
data/lib/ask/agent.rb
CHANGED
|
@@ -21,11 +21,20 @@ module Ask
|
|
|
21
21
|
|
|
22
22
|
class UnknownAgent < Error; end
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
# Policies are opt-in, replaceable implementations of the tool-lifecycle
|
|
25
|
+
# hook seam (before_tool / after_tool). The agent loop runs without them
|
|
26
|
+
# and their semantics are unchanged by their absence; users compose and
|
|
27
|
+
# swap them freely (see Ask::Agent::Hooks for the seam).
|
|
28
|
+
#
|
|
29
|
+
# A policy is NOT core machinery. Core mechanisms live on Session — the
|
|
30
|
+
# approval queue, the :pending result status, and the `approval: true`
|
|
31
|
+
# option are core; Policies::ApprovalPolicy is the reference
|
|
32
|
+
# classification policy wired on top of them.
|
|
33
|
+
module Policies
|
|
34
|
+
autoload :Permissions, "ask/agent/policies/permissions"
|
|
35
|
+
autoload :RateLimiter, "ask/agent/policies/rate_limiter"
|
|
36
|
+
autoload :AuditLog, "ask/agent/policies/audit_log"
|
|
37
|
+
autoload :ApprovalPolicy, "ask/agent/policies/approval_policy"
|
|
29
38
|
end
|
|
30
39
|
|
|
31
40
|
module Middleware
|
|
@@ -236,8 +245,8 @@ module Ask
|
|
|
236
245
|
yield configuration
|
|
237
246
|
end
|
|
238
247
|
|
|
239
|
-
def self.
|
|
240
|
-
Dir[File.expand_path("agent/
|
|
248
|
+
def self.load_policies
|
|
249
|
+
Dir[File.expand_path("agent/policies/*.rb", __dir__)].each { |f| require f }
|
|
241
250
|
rescue Errno::ENOENT
|
|
242
251
|
end
|
|
243
252
|
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.
|
|
4
|
+
version: 0.28.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kaka Ruto
|
|
@@ -174,11 +174,6 @@ files:
|
|
|
174
174
|
- lib/ask/agent/definition.rb
|
|
175
175
|
- lib/ask/agent/evaluator.rb
|
|
176
176
|
- lib/ask/agent/events.rb
|
|
177
|
-
- lib/ask/agent/extensions/approval_policy.rb
|
|
178
|
-
- lib/ask/agent/extensions/audit_log.rb
|
|
179
|
-
- lib/ask/agent/extensions/audit_log/active_record_writer.rb
|
|
180
|
-
- lib/ask/agent/extensions/permissions.rb
|
|
181
|
-
- lib/ask/agent/extensions/rate_limiter.rb
|
|
182
177
|
- lib/ask/agent/hooks.rb
|
|
183
178
|
- lib/ask/agent/loop.rb
|
|
184
179
|
- lib/ask/agent/meta_agent.rb
|
|
@@ -190,6 +185,11 @@ files:
|
|
|
190
185
|
- lib/ask/agent/middleware/retry_on_failure.rb
|
|
191
186
|
- lib/ask/agent/persistence/base.rb
|
|
192
187
|
- lib/ask/agent/persistence/in_memory.rb
|
|
188
|
+
- lib/ask/agent/policies/approval_policy.rb
|
|
189
|
+
- lib/ask/agent/policies/audit_log.rb
|
|
190
|
+
- lib/ask/agent/policies/audit_log/active_record_writer.rb
|
|
191
|
+
- lib/ask/agent/policies/permissions.rb
|
|
192
|
+
- lib/ask/agent/policies/rate_limiter.rb
|
|
193
193
|
- lib/ask/agent/reflector.rb
|
|
194
194
|
- lib/ask/agent/scheduler.rb
|
|
195
195
|
- lib/ask/agent/session.rb
|