ask-agent 0.40.20 → 0.40.21

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: 8ad845b7f0d67966819af4e0f588f47ff44793e874b56c08dd13208dc3334e5d
4
- data.tar.gz: 52c5ef1af2a97cb7cb41a7247c19a614e75ca5138f7192ad71ecde7ffdc3eede
3
+ metadata.gz: 8119f78526cac8f96b7f6506c1d40e046127dccad46015252e9ea0805e15c677
4
+ data.tar.gz: be381f8d8fbb4b104ba79ab0a1cece93918c8ef028bfdb4fc24dd6ae49399726
5
5
  SHA512:
6
- metadata.gz: '078e71e0cf4fd80c5d08badd68377eb4b73cf0923f46dc6f065bab94d3106323892bb307acf717ea3640907939e5706645b42986c5d1fc003544801e2fece9e0'
7
- data.tar.gz: '068595e1f29705bcda12462c342b49a4a5a2d055c5f5f75c5c24edbe02bd8475a8803e6f7ecebb0fddd782dac84e168db8f877d6fe6e2eda566d2bb000e15f26'
6
+ metadata.gz: aee841408cd187f95b2d54f6bbb0820fac09865bf58f1af38680b1847088a8aca7c1a187008e93a98a6cf39941fd65e0690dd78a16b6d07491c6d0ec0573bcf2
7
+ data.tar.gz: c6b6c736800ae14baa95355f692294da8f428af180c404852a5aa9141905d5bfb776994f59aab21e772f3263e40c3c542b32009cee779f1263ade62c8d095cb5
data/CHANGELOG.md CHANGED
@@ -1,3 +1,67 @@
1
+ ## [Unreleased]
2
+
3
+
4
+ ## [0.40.21] — 2026-09-22
5
+
6
+ ### Added
7
+
8
+ - **`Ask::Agent::SessionAdapter` — bridges an agent session to
9
+ `Ask::Session::Host`.** Records user input and per-run snapshots as
10
+ event-sourced ask-session events, maps agent events (turn, streaming,
11
+ tool, todo, plan, error) onto session event types with `trace_id` /
12
+ `causation_id` propagation, and resumes from the latest snapshot
13
+ (`SessionAdapter.resume`). Resume survives process restarts when the host
14
+ store is durable (e.g. `Ask::Session::ProviderStore` over
15
+ `ask-state-providers`). Requires the new runtime dependency
16
+ `ask-session >= 0.1.0` (the single session store; no second store added).
17
+ Resume validates the snapshot before restoring, so a malformed snapshot
18
+ raises `SessionAdapter::Error` instead of leaving the agent half-restored.
19
+
20
+ ### Fixed
21
+
22
+ - **`Session#turn_count` is now maintained during runs.** The counter was
23
+ only ever reset at run start (or restored from a checkpoint), never
24
+ incremented — so `SessionEnd`, audit-log entries, persisted
25
+ `metadata.turn_count`, and `SessionAdapter` snapshots all recorded `0` for
26
+ live runs. Turns are now counted on `TurnStart`, matching the loop's own
27
+ count (`reset: true` scopes it to the run; `reset: false` accumulates).
28
+
29
+
30
+ ## [0.40.20] — 2026-09-21
31
+
32
+ ### Changed
33
+
34
+ - **Agent tool execution unified with ask-runtime.** `ToolExecutor` runs
35
+ tool calls through the runtime's execution contract, exposing the current
36
+ runtime call and execution context (`Ask.current_runtime_call` /
37
+ `Ask.current_runtime_context`) while a tool executes, and conforming to
38
+ the runtime executor contract.
39
+ - Requires `ask-runtime >= 0.1.0`.
40
+
41
+
42
+ ## [0.40.19] — 2026-09-20
43
+
44
+ ### Added
45
+
46
+ - **Bundled `agent.build_agents` skill** — teaches building agents with
47
+ ask-rb (definitions, sessions, tools, configuration, common patterns).
48
+ Auto-discovered via `Gem.find_files` when ask-skills is present.
49
+ - **`askr skills install` / `askr skills uninstall`** with
50
+ `--global|--local|--dir`, plus auto-sync of managed skill copies on
51
+ every CLI invocation.
52
+
53
+
54
+ ## [0.40.18] — 2026-09-19
55
+
56
+ ### Changed
57
+
58
+ - **`Agent.new` and `Session.new` unified.** The shared implementation
59
+ lives in `Session.build_from_definition`; `Agent.new` delegates to it,
60
+ `Session.new` supports definition lookup, and `Ask.chat` accepts `name:`
61
+ for definition lookup. The duplicate `build_session_from_definition` and
62
+ `resolve_definition_tools` helpers were removed from the `Agent` module.
63
+
64
+
1
65
  ## [0.40.17] — 2026-09-18
2
66
 
3
67
  ### Added
data/README.md CHANGED
@@ -102,6 +102,35 @@ under multiple OpenAI-compatible providers). A `provider:` passed to
102
102
  `Session.new` or declared in an agent `Definition` always wins over the global
103
103
  default.
104
104
 
105
+ ## Durable sessions (ask-session)
106
+
107
+ `Ask::Agent::SessionAdapter` bridges a session to an event-sourced
108
+ `Ask::Session::Host`. Every run records the user input, the mapped agent
109
+ events, and an `agent.snapshot`; `SessionAdapter.resume` restores the latest
110
+ snapshot — including after a process restart, as long as the host store is
111
+ durable (for example `Ask::Session::ProviderStore` over
112
+ `ask-state-providers`' SQLite adapter).
113
+
114
+ ```ruby
115
+ require "ask-agent"
116
+ require "ask/session"
117
+
118
+ host = Ask::Session::Host.new(store: Ask::Session::Store.new)
119
+
120
+ session = Ask::Agent::Session.new(model: "gpt-4o", id: "chat-1")
121
+ adapter = Ask::Agent::SessionAdapter.create(agent: session, host: host)
122
+ adapter.run("Hello")
123
+
124
+ # Later (even in a new process, with the same durable store):
125
+ resumed = Ask::Agent::Session.new(model: "gpt-4o", id: "chat-1")
126
+ adapter = Ask::Agent::SessionAdapter.resume(agent: resumed, host: host, session_id: "chat-1")
127
+ adapter.run("Continue where we left off")
128
+ ```
129
+
130
+ `SessionAdapter::Error` is raised when the session record or its latest
131
+ snapshot cannot be found. This requires the `ask-session` gem (a runtime
132
+ dependency of ask-agent).
133
+
105
134
  ## Full documentation
106
135
 
107
136
  The full ask-rb documentation lives at https://ask-rb.github.io/ask-docs.
@@ -107,7 +107,10 @@ module Ask
107
107
  @turn_id = 0
108
108
  @queued_steers = []
109
109
  @steer_mutex = Mutex.new
110
- on(Events::TurnStart) { @turn_id += 1 }
110
+ on(Events::TurnStart) do
111
+ @turn_id += 1
112
+ @turn_count += 1
113
+ end
111
114
  @created_at = Time.now
112
115
  @_no_tools_instructed = false
113
116
 
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "ask/session"
5
+
6
+ module Ask
7
+ module Agent
8
+ # Adapter that bridges an Ask::Agent::Session to an Ask::Session::Host.
9
+ #
10
+ # Maps agent events to event-sourced ask-session events, persists a
11
+ # snapshot at the end of each run, and supports resume from the latest
12
+ # durable snapshot.
13
+ #
14
+ # adapter = SessionAdapter.create(agent: session, host: host)
15
+ # result = adapter.run("Hello")
16
+ #
17
+ # restored = SessionAdapter.resume(agent: session, host: host, session_id: id)
18
+ # restored.run("Follow up")
19
+ #
20
+ class SessionAdapter
21
+ SNAPSHOT_TYPE = "agent.snapshot"
22
+
23
+ # Raised when resume cannot find the ask-session record or its
24
+ # latest snapshot.
25
+ class Error < Ask::Agent::Error; end
26
+
27
+ attr_reader :agent, :host, :session_id
28
+
29
+ def initialize(agent:, host:, session_id: agent.id, metadata: {}, create: false)
30
+ @agent = agent
31
+ @host = host
32
+ @session_id = session_id
33
+ @current_trace_id = nil
34
+ @current_turn_id = nil
35
+ @current_causation_id = nil
36
+
37
+ @host.create(id: @session_id, metadata: metadata) if create
38
+
39
+ attach_event_handler
40
+ end
41
+
42
+ # Create a new ask-session record and return the adapter.
43
+ #
44
+ # @param agent [Ask::Agent::Session] the agent session
45
+ # @param host [Ask::Session::Host] the session host
46
+ # @param metadata [Hash] session metadata
47
+ # @return [SessionAdapter]
48
+ def self.create(agent:, host:, metadata: {})
49
+ record = host.create(id: agent.id, metadata: metadata)
50
+ new(agent: agent, host: host, session_id: record.id, metadata: metadata, create: false)
51
+ end
52
+
53
+ # Attach to an existing ask-session and restore the latest snapshot.
54
+ #
55
+ # Contract: Ask::Session::Host#session raises
56
+ # Ask::Session::NotFoundError for a missing record (it never returns
57
+ # nil); both missing-session and missing-snapshot failures surface as
58
+ # SessionAdapter::Error.
59
+ #
60
+ # @param agent [Ask::Agent::Session] the agent session (will be populated from snapshot)
61
+ # @param host [Ask::Session::Host] the session host
62
+ # @param session_id [String] the ask-session record id
63
+ # @return [SessionAdapter]
64
+ # @raise [SessionAdapter::Error] if the session or snapshot is not found
65
+ def self.resume(agent:, host:, session_id:)
66
+ begin
67
+ host.session(session_id)
68
+ rescue Ask::Session::NotFoundError
69
+ raise Error, "Session not found: #{session_id.inspect}"
70
+ end
71
+
72
+ events = host.events(session_id)
73
+ snapshot_event = events.reverse_each.find { |e| e.type == SNAPSHOT_TYPE }
74
+ raise Error, "No snapshot found for session #{session_id.inspect}" unless snapshot_event
75
+
76
+ payload = snapshot_event.payload
77
+ messages = payload[:messages] || payload["messages"] || []
78
+
79
+ # Validate before mutating the agent so a malformed snapshot cannot
80
+ # leave it half-restored.
81
+ messages.each do |msg|
82
+ role = msg.is_a?(Hash) ? (msg[:role] || msg["role"]) : nil
83
+ raise Error, "Snapshot for session #{session_id.inspect} contains a message without a role" unless role
84
+ end
85
+
86
+ agent.instance_variable_set(:@turn_count, payload[:turn_count] || payload["turn_count"] || 0)
87
+ agent.chat.reset_messages!
88
+ messages.each do |msg|
89
+ agent.chat.add_message(
90
+ role: (msg[:role] || msg["role"]).to_sym,
91
+ content: deserialize_content(msg[:content] || msg["content"]),
92
+ tool_call_id: msg[:tool_call_id] || msg["tool_call_id"],
93
+ tool_calls: msg[:tool_calls] || msg["tool_calls"]
94
+ )
95
+ end
96
+ agent.instance_variable_set(:@messages, agent.chat.messages.dup)
97
+
98
+ new(agent: agent, host: host, session_id: session_id, create: false)
99
+ end
100
+
101
+ # Current trace id for the running trace.
102
+ # @return [String, nil]
103
+ def current_trace_id
104
+ @current_trace_id
105
+ end
106
+
107
+ # Current turn identifier (stable per TurnStart).
108
+ # @return [Integer, nil]
109
+ def current_turn_id
110
+ @current_turn_id
111
+ end
112
+
113
+ # Run the agent and record events in the ask-session.
114
+ #
115
+ # @param message [String] the user message
116
+ # @param trace_id [String, nil] optional trace id (auto-generated if nil)
117
+ # @param causation_id [String, nil] optional causation id linking to the
118
+ # event that caused this run
119
+ # @param options [Hash] forwarded to agent.run
120
+ # @return [String] the agent response
121
+ def run(message, trace_id: nil, causation_id: nil, **options)
122
+ @current_trace_id = trace_id || "trace_#{SecureRandom.hex(8)}"
123
+ @current_causation_id = causation_id
124
+
125
+ # Record user input as a message event.
126
+ input_event = @host.send_message(
127
+ @session_id,
128
+ content: message.to_s,
129
+ trace_id: @current_trace_id,
130
+ causation_id: @current_causation_id
131
+ )
132
+ @current_causation_id ||= input_event.trace_id
133
+
134
+ begin
135
+ result = @agent.run(message, **options)
136
+
137
+ # Append snapshot for resume.
138
+ snapshot = build_snapshot
139
+ @host.append(
140
+ @session_id,
141
+ type: SNAPSHOT_TYPE,
142
+ payload: snapshot,
143
+ trace_id: @current_trace_id,
144
+ causation_id: @current_causation_id
145
+ )
146
+
147
+ result
148
+ rescue Ask::Agent::Aborted
149
+ @host.append(
150
+ @session_id,
151
+ type: "turn.aborted",
152
+ payload: { turn_id: @current_turn_id },
153
+ trace_id: @current_trace_id,
154
+ causation_id: @current_causation_id
155
+ )
156
+ raise
157
+ rescue => e
158
+ @host.append(
159
+ @session_id,
160
+ type: "turn.failed",
161
+ payload: { error: e.message, error_class: e.class.name },
162
+ trace_id: @current_trace_id,
163
+ causation_id: @current_causation_id
164
+ )
165
+ raise
166
+ ensure
167
+ @current_turn_id = nil
168
+ @current_causation_id = nil
169
+ end
170
+ end
171
+
172
+ # Request agent abort without writing a terminal session event.
173
+ def abort
174
+ @agent.abort
175
+ end
176
+
177
+ # Close the ask-session (terminal: :closed).
178
+ def close
179
+ @host.close(@session_id)
180
+ end
181
+
182
+ # Abort the ask-session (terminal: :aborted).
183
+ def abort_session
184
+ @host.abort(@session_id)
185
+ end
186
+
187
+ private
188
+
189
+ def attach_event_handler
190
+ @agent.on_event do |event|
191
+ handle_event(event)
192
+ end
193
+ end
194
+
195
+ def handle_event(event)
196
+ trace_id = @current_trace_id
197
+ causation_id = @current_causation_id
198
+ turn_id = @current_turn_id
199
+
200
+ case event
201
+ when Events::TurnStart
202
+ @current_turn_id = (@current_turn_id || 0) + 1
203
+ turn_id = @current_turn_id
204
+ @host.append(@session_id, type: "turn.started", payload: { turn_id: turn_id }, trace_id: trace_id, causation_id: causation_id)
205
+
206
+ when Events::TextDelta
207
+ @host.append(@session_id, type: "model.streaming", payload: { content: event.content, turn_id: turn_id }, trace_id: trace_id, causation_id: causation_id)
208
+
209
+ when Events::ThinkingDelta
210
+ @host.append(@session_id, type: "model.thinking", payload: { content: event.content, turn_id: turn_id }, trace_id: trace_id, causation_id: causation_id)
211
+
212
+ when Events::ToolExecutionStart
213
+ @host.append(@session_id, type: "tool.use", payload: { tool_name: event.name, arguments: event.arguments, tool_call_id: event.id, turn_id: turn_id }, trace_id: trace_id, causation_id: causation_id)
214
+
215
+ when Events::ToolExecutionUpdate
216
+ @host.append(@session_id, type: "tool.delta", payload: { tool_name: event.name, tool_call_id: event.id, partial_result: event.partial_result, turn_id: turn_id }, trace_id: trace_id, causation_id: causation_id)
217
+
218
+ when Events::ToolExecutionEnd
219
+ @host.append(@session_id, type: "tool.result", payload: { tool_name: event.name, tool_call_id: event.id, result: event.result, is_error: event.is_error, duration_ms: event.duration_ms, turn_id: turn_id }, trace_id: trace_id, causation_id: causation_id)
220
+
221
+ when Events::TodoUpdated
222
+ @host.append(@session_id, type: "todos.updated", payload: { todos: event.todos }, trace_id: trace_id, causation_id: causation_id)
223
+
224
+ when Events::PlanProposed
225
+ @host.append(@session_id, type: "plan.proposed", payload: { plan: event.plan }, trace_id: trace_id, causation_id: causation_id)
226
+
227
+ when Events::PlanApproved
228
+ @host.append(@session_id, type: "plan.approved", payload: { plan: event.plan }, trace_id: trace_id, causation_id: causation_id)
229
+
230
+ when Events::PlanRejected
231
+ @host.append(@session_id, type: "plan.rejected", payload: { plan: event.plan }, trace_id: trace_id, causation_id: causation_id)
232
+
233
+ when Events::Error
234
+ @host.append(@session_id, type: "error", payload: { message: event.error, recoverable: event.recoverable }, trace_id: trace_id, causation_id: causation_id)
235
+ end
236
+ end
237
+
238
+ def build_snapshot
239
+ {
240
+ messages: (@agent.messages || []).map(&:to_h),
241
+ turn_count: @agent.turn_count || 0
242
+ }
243
+ end
244
+
245
+ def self.deserialize_content(content)
246
+ content.is_a?(Array) ? content.map { |block| Ask::Content.from_h(block) } : content
247
+ end
248
+ end
249
+ end
250
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Agent
5
- VERSION = "0.40.20"
5
+ VERSION = "0.40.21"
6
6
  end
7
7
  end
data/lib/ask/agent.rb CHANGED
@@ -216,6 +216,7 @@ require_relative "agent/chat"
216
216
  require_relative "agent/telemetry"
217
217
  require_relative "agent/tool_abort_controller"
218
218
  require_relative "agent/session"
219
+ require_relative "agent/session_adapter"
219
220
  require_relative "agent/loop"
220
221
  require_relative "agent/reflector"
221
222
  require_relative "agent/evaluator"
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.40.20
4
+ version: 0.40.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -121,6 +121,20 @@ dependencies:
121
121
  - - "~>"
122
122
  - !ruby/object:Gem::Version
123
123
  version: '3.9'
124
+ - !ruby/object:Gem::Dependency
125
+ name: ask-session
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: 0.1.0
131
+ type: :runtime
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: 0.1.0
124
138
  - !ruby/object:Gem::Dependency
125
139
  name: minitest
126
140
  requirement: !ruby/object:Gem::Requirement
@@ -216,6 +230,7 @@ files:
216
230
  - lib/ask/agent/reflector.rb
217
231
  - lib/ask/agent/scheduler.rb
218
232
  - lib/ask/agent/session.rb
233
+ - lib/ask/agent/session_adapter.rb
219
234
  - lib/ask/agent/stream_transforms/base.rb
220
235
  - lib/ask/agent/stream_transforms/extract_json.rb
221
236
  - lib/ask/agent/stream_transforms/pipeline.rb