ask-agent 0.40.19 → 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: 46ea77e0ac0feca360164604367729e05f15e22212f479c59ad1954da5e5e924
4
- data.tar.gz: 5a1e0198d2e4d9faf7831bd1c7f8520a82a01c7fff46a9a94512a60a6d285409
3
+ metadata.gz: 8119f78526cac8f96b7f6506c1d40e046127dccad46015252e9ea0805e15c677
4
+ data.tar.gz: be381f8d8fbb4b104ba79ab0a1cece93918c8ef028bfdb4fc24dd6ae49399726
5
5
  SHA512:
6
- metadata.gz: d195e1e86df0f281de603cdd607d0b2d88bbbc3d07f4c46b5dbb81235fd102d7f4a19781e625b8ddd8c304babb47eaf60fa894c6513b5e944f493b0e4928c74f
7
- data.tar.gz: c4289b1c74e4ee05de9ffe2c0c266e46599e49c546b0c22c23ea76258a23e291dd5804e3d5cdd0d514375540fc22d7732a920de1e6d303f7dfde0fcbc04ca883
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.
@@ -64,7 +64,6 @@ module Ask
64
64
  @messages << Ask::Message.new(role: :user, content: merge_attachments(message, attachments)) if has_message || attachments
65
65
 
66
66
  stream = block_given?
67
- tool_defs = @tools.map { |t| Ask::ToolDef.from_tool(t) }
68
67
 
69
68
  calls_acc = {}
70
69
  response_msg = chat_with_retry(stream, calls_acc, &block)
@@ -103,7 +103,7 @@ module Ask
103
103
  # when the background work completes.
104
104
  user_results = tool_executor.execute(
105
105
  user_tool_calls, tools, hooks: hooks, event_emitter: event_emitter,
106
- session_id: session_id,
106
+ session_id: session_id, turn: @turn_count,
107
107
  result_callback: lambda do |tool_call_id, result|
108
108
  tc = user_tool_calls[tool_call_id]
109
109
  next unless tc
@@ -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
 
@@ -878,10 +881,10 @@ end
878
881
  private
879
882
 
880
883
  def build_audit_log(config)
881
- config ||= Ask::Agent.configuration.audit_log
882
- return nil unless config
883
- Ask::Agent::Policies::AuditLog.new(self, adapter: config)
884
- end
884
+ config ||= Ask::Agent.configuration.audit_log
885
+ return nil unless config
886
+ Ask::Agent::Policies::AuditLog.new(self, adapter: config)
887
+ end
885
888
 
886
889
  # Build the approval queue + policy when approval is enabled.
887
890
  #
@@ -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
@@ -3,6 +3,8 @@
3
3
  module Ask
4
4
  module Agent
5
5
  class ToolExecutor
6
+ include Ask::Runtime::ToolExecutor
7
+
6
8
  CRITICAL_ERROR_CLASSES = %w[
7
9
  Ask::Unauthorized
8
10
  Ask::Forbidden
@@ -22,23 +24,41 @@ module Ask
22
24
 
23
25
  attr_writer :telemetry
24
26
 
25
- def execute(tool_calls, tools, hooks:, event_emitter:, session_id: nil, result_callback: nil)
27
+ # Public entry point — dispatches between two contracts:
28
+ #
29
+ # Batch API: execute(tool_calls, tools, hooks:, event_emitter:, ...)
30
+ # Runtime API: execute(tool_call, context:)
31
+ #
32
+ # The discriminator is the first argument: a Hash triggers the batch path,
33
+ # anything else (typically an Ask::Runtime::ToolCall) triggers the runtime path.
34
+ def execute(tool_calls_or_call, tools_or_context = nil, **rest)
35
+ if tool_calls_or_call.is_a?(Hash)
36
+ execute_batch(tool_calls_or_call, tools_or_context, **rest)
37
+ else
38
+ ctx = rest.key?(:context) ? rest[:context] : tools_or_context
39
+ _runtime_execute(tool_calls_or_call, context: ctx)
40
+ end
41
+ end
42
+
43
+ def execute_batch(tool_calls, tools, hooks:, event_emitter:, session_id: nil, turn: nil, result_callback: nil, runtime_event_sink: nil)
26
44
  return [] if tool_calls.empty?
27
45
 
28
46
  @total_executions = 0
29
47
  @session_id = session_id
30
- sibling_abort = ToolAbortController.new
48
+ @turn = turn
49
+ @last_tools = tools
50
+ sibling_abort = CallbackAbortController.new
31
51
 
32
52
  if @parallel
33
- execute_parallel(tool_calls, tools, hooks, event_emitter, sibling_abort, &result_callback)
53
+ execute_parallel(tool_calls, tools, hooks, event_emitter, sibling_abort, runtime_event_sink, &result_callback)
34
54
  else
35
- execute_sequential(tool_calls, tools, hooks, event_emitter, sibling_abort) do |id, result|
55
+ execute_sequential(tool_calls, tools, hooks, event_emitter, sibling_abort, runtime_event_sink) do |id, result|
36
56
  result_callback&.call(id, result)
37
57
  end
38
58
  end
39
59
  end
40
60
 
41
- def execute_parallel(tool_calls, tools, hooks, event_emitter, sibling_abort, &result_callback)
61
+ def execute_parallel(tool_calls, tools, hooks, event_emitter, sibling_abort, runtime_event_sink, &result_callback)
42
62
  threads = []
43
63
  mutex = Mutex.new
44
64
  results = {}
@@ -58,7 +78,7 @@ module Ask
58
78
  next
59
79
  end
60
80
 
61
- result = execute_single_tool(tool_call, tools, hooks, event_emitter, sibling_abort)
81
+ result = execute_single_tool(tool_call, tools, hooks, event_emitter, sibling_abort, runtime_event_sink)
62
82
  mutex.synchronize { results[id] = result }
63
83
 
64
84
  # Stream result back as it completes
@@ -87,12 +107,12 @@ module Ask
87
107
  tool_calls.keys.filter_map { |id| results[id]&.merge(tool_call_id: id) }
88
108
  end
89
109
 
90
- def execute_sequential(tool_calls, tools, hooks, event_emitter, sibling_abort, &result_callback)
110
+ def execute_sequential(tool_calls, tools, hooks, event_emitter, sibling_abort, runtime_event_sink, &result_callback)
91
111
  results = []
92
112
  tool_calls.each do |id, tool_call|
93
113
  break if sibling_abort.aborted?
94
114
 
95
- result = execute_single_tool(tool_call, tools, hooks, event_emitter, sibling_abort)
115
+ result = execute_single_tool(tool_call, tools, hooks, event_emitter, sibling_abort, runtime_event_sink)
96
116
  results << result.merge(tool_call_id: id)
97
117
  result_callback&.call(id, result)
98
118
  break if result[:critical_failure]
@@ -103,7 +123,300 @@ module Ask
103
123
 
104
124
  private
105
125
 
106
- def execute_single_tool(tool_call, tools, hooks, event_emitter, abort_controller = nil)
126
+ class CallbackAbortController < ToolAbortController
127
+ def initialize
128
+ super
129
+ @abort_callbacks = []
130
+ end
131
+
132
+ def abort!
133
+ callbacks_to_run = @mutex.synchronize do
134
+ was_aborted = @aborted
135
+ @aborted = true
136
+ was_aborted ? [] : @abort_callbacks.dup
137
+ end
138
+ callbacks_to_run.each(&:call)
139
+ self
140
+ end
141
+
142
+ def on_abort(&block)
143
+ raise ArgumentError, "block required" unless block
144
+
145
+ already = @mutex.synchronize do
146
+ if @aborted
147
+ true
148
+ else
149
+ @abort_callbacks << block
150
+ false
151
+ end
152
+ end
153
+ block.call if already
154
+ self
155
+ end
156
+ end
157
+
158
+ def build_runtime_tool_call(tool_call, effective_args: nil)
159
+ input = begin
160
+ raw = effective_args || tool_call.arguments
161
+ parsed = raw.is_a?(String) ? JSON.parse(raw) : raw
162
+ parsed.is_a?(Hash) ? parsed : {}
163
+ rescue JSON::ParserError
164
+ {}
165
+ end
166
+
167
+ Ask::Runtime::ToolCall.new(
168
+ id: tool_call.id,
169
+ tool_name: tool_call.name,
170
+ input: input,
171
+ session_id: @session_id,
172
+ turn: @turn
173
+ )
174
+ end
175
+
176
+ def build_runtime_context(abort_controller)
177
+ canceller = AbortControllerCancellerAdapter.new(abort_controller) if abort_controller
178
+
179
+ Ask::Runtime::ExecutionContext.new(
180
+ session_id: @session_id,
181
+ turn: @turn,
182
+ canceller: canceller
183
+ )
184
+ end
185
+
186
+ class AbortControllerCancellerAdapter
187
+ def initialize(abort_controller)
188
+ @abort_controller = abort_controller
189
+ @callbacks = []
190
+ @mutex = Mutex.new
191
+
192
+ if abort_controller.respond_to?(:on_abort)
193
+ abort_controller.on_abort { fire_callbacks }
194
+ end
195
+ end
196
+
197
+ def cancelled?
198
+ @abort_controller.aborted?
199
+ end
200
+
201
+ def cancel
202
+ @abort_controller.abort!
203
+ fire_callbacks
204
+ self
205
+ end
206
+
207
+ def on_cancel(&block)
208
+ raise ArgumentError, "block required" unless block
209
+
210
+ already = @mutex.synchronize do
211
+ if @abort_controller.aborted?
212
+ true
213
+ else
214
+ @callbacks << block
215
+ false
216
+ end
217
+ end
218
+ block.call if already
219
+ self
220
+ end
221
+
222
+ def inspect
223
+ "#<Ask::Agent::ToolExecutor::AbortControllerCancellerAdapter cancelled=#{cancelled?}>"
224
+ end
225
+
226
+ private
227
+
228
+ def fire_callbacks
229
+ callbacks_to_run = @mutex.synchronize do
230
+ snapshot = @callbacks.dup
231
+ @callbacks.clear
232
+ snapshot
233
+ end
234
+ callbacks_to_run.each(&:call)
235
+ end
236
+ end
237
+
238
+ # ----------------------------------------------------------------
239
+ # Runtime contract: execute a single tool call and return ToolResult
240
+ # ----------------------------------------------------------------
241
+
242
+ # Implements Ask::Runtime::ToolExecutor#execute.
243
+ #
244
+ # This is the single-call entry point for the runtime contract.
245
+ # It does NOT run hooks, emit agent events, or manage batches — it is
246
+ # the pure runtime execution path that the batch API delegates to
247
+ # internally (via execute_single_tool).
248
+ #
249
+ # @param tool_call [Ask::Runtime::ToolCall] the tool-call request
250
+ # @param context [Ask::Runtime::ExecutionContext, nil] execution context
251
+ # @return [Ask::Runtime::ToolResult] the normalized result
252
+ def _runtime_execute(tool_call, context: nil)
253
+ context ||= Ask::Runtime::ExecutionContext.new
254
+ started_at = Time.now
255
+ started_call = tool_call.with(state: :running, started_at: started_at)
256
+ emit_runtime_event(context.event_sink, :tool_started,
257
+ Ask::Runtime::Events::ToolStarted.new(
258
+ tool_call: started_call, execution_context: context, timestamp: started_at
259
+ ))
260
+
261
+ return runtime_cancel(started_call, context, started_at, "Cancelled before execution") if context.cancelled?
262
+
263
+ tool = find_tool_for_runtime(tool_call.tool_name)
264
+
265
+ result = if tool
266
+ invoke_tool_with_retry(tool, tool_call.id, tool_call.input, context.canceller)
267
+ else
268
+ Ask::Runtime::ToolResult.failure("Tool not found: #{tool_call.tool_name}")
269
+ end
270
+
271
+ return runtime_cancel(started_call, context, started_at, "Cancelled during execution") if context.cancelled?
272
+
273
+ runtime_finish(started_call, context, result, started_at)
274
+ end
275
+
276
+ def runtime_finish(started_call, context, result, started_at)
277
+ finished_at = Time.now
278
+ duration = finished_at - started_at
279
+ result = Ask::Runtime::ToolResult.new(
280
+ result: result.result, outcome: result.outcome, duration: duration
281
+ )
282
+ state = if result.timeout?
283
+ :timed_out
284
+ elsif result.failure?
285
+ :failed
286
+ else
287
+ :completed
288
+ end
289
+ finished_call = started_call.with(
290
+ state: state,
291
+ tool_result: result,
292
+ error: result.error_message,
293
+ finished_at: finished_at
294
+ )
295
+ emit_runtime_terminal_event(
296
+ context.event_sink, state,
297
+ tool_call: finished_call, tool_result: result,
298
+ execution_context: context, timestamp: finished_at, duration: duration
299
+ )
300
+ result
301
+ end
302
+
303
+ def runtime_cancel(started_call, context, started_at, reason)
304
+ finished_at = Time.now
305
+ duration = finished_at - started_at
306
+ result = Ask::Runtime::ToolResult.new(
307
+ result: Ask::Result.failure(reason), outcome: :cancelled, duration: duration
308
+ )
309
+ finished_call = started_call.with(
310
+ state: :cancelled,
311
+ tool_result: result,
312
+ error: result.error_message,
313
+ finished_at: finished_at
314
+ )
315
+ emit_runtime_terminal_event(
316
+ context.event_sink, :cancelled,
317
+ tool_call: finished_call, tool_result: result,
318
+ execution_context: context, timestamp: finished_at, duration: duration
319
+ )
320
+ result
321
+ end
322
+
323
+ # Find a tool by name for the runtime contract path.
324
+ # Uses the last-known tools list from execute_batch, or nil.
325
+ def find_tool_for_runtime(tool_name)
326
+ @last_tools&.find { |t| t.name == tool_name }
327
+ end
328
+
329
+ # ----------------------------------------------------------------
330
+ # Shared execution core — used by both batch and runtime paths
331
+ # ----------------------------------------------------------------
332
+
333
+ # Invoke a tool with retry logic, returning an Ask::Runtime::ToolResult.
334
+ #
335
+ # This is the single source of truth for tool invocation. It wraps
336
+ # try_call and handles retryable errors, producing a normalized
337
+ # ToolResult that both the batch and runtime paths consume.
338
+ def invoke_tool_with_retry(tool, tool_call_id, args, abort_controller = nil)
339
+ @max_retries.times do |attempt|
340
+ if abort_cancelled?(abort_controller)
341
+ return Ask::Runtime::ToolResult.cancelled("Aborted")
342
+ end
343
+
344
+ result = try_call(tool, args, abort_controller)
345
+ return result unless result.failure? && retryable_error?(result)
346
+
347
+ sleep((2 ** attempt) * 0.5 + rand(0.0..0.5))
348
+ end
349
+
350
+ if abort_cancelled?(abort_controller)
351
+ return Ask::Runtime::ToolResult.cancelled("Aborted")
352
+ end
353
+ try_call(tool, args)
354
+ end
355
+
356
+ # Check if the abort controller signals cancellation.
357
+ # Handles both ToolAbortController (aborted?) and Canceller (cancelled?).
358
+ def abort_cancelled?(controller)
359
+ return false unless controller
360
+ return true if controller.respond_to?(:aborted?) && controller.aborted?
361
+ return true if controller.respond_to?(:cancelled?) && controller.cancelled?
362
+ false
363
+ end
364
+
365
+ # Invoke a tool once, returning an Ask::Runtime::ToolResult.
366
+ #
367
+ # Normalizes all tool return types (Ask::Result, Hash, String, pending)
368
+ # into the canonical ToolResult shape. Preserves halted metadata for
369
+ # the batch path to consume.
370
+ def try_call(tool, args, abort_controller = nil)
371
+ raw = tool.call(args, abort_controller: abort_controller)
372
+
373
+ if raw.respond_to?(:pending?) && raw.pending?
374
+ return Ask::Runtime::ToolResult.success(data: raw)
375
+ end
376
+
377
+ if raw.respond_to?(:ok?)
378
+ if raw.ok?
379
+ # Preserve the Ask::Result as the ToolResult's result object
380
+ # so downstream code can access metadata (e.g. halted flag).
381
+ Ask::Runtime::ToolResult.new(result: raw, outcome: :success)
382
+ else
383
+ Ask::Runtime::ToolResult.failure(extract_error_message(raw))
384
+ end
385
+ else
386
+ Ask::Runtime::ToolResult.success(data: raw)
387
+ end
388
+ rescue => e
389
+ Ask::Runtime::ToolResult.failure("#{e.class}: #{e.message}")
390
+ end
391
+
392
+ # Extract the output payload from an Ask::Result, unwrapping the wrapper
393
+ # so ToolResult.output carries the actual data.
394
+ def unwrap_output(result)
395
+ result.respond_to?(:output) ? result.output : result
396
+ end
397
+
398
+ # Extract a string error message from a failed Ask::Result.
399
+ def extract_error_message(result)
400
+ msg = result.respond_to?(:error_message) ? result.error_message : nil
401
+ msg || result.to_s
402
+ end
403
+
404
+ # Check if a ToolResult represents a retryable error.
405
+ def retryable_error?(tool_result)
406
+ return false unless tool_result.failure?
407
+
408
+ error_msg = tool_result.error_message.to_s
409
+ # Extract the class name from "ClassName: message" format.
410
+ # Split on ": " (colon-space) to avoid breaking on :: namespaces.
411
+ error_class_name = error_msg.split(": ").first
412
+ retryable_error_name?(error_class_name)
413
+ end
414
+
415
+ # ----------------------------------------------------------------
416
+ # Batch path: execute_single_tool (unchanged public behavior)
417
+ # ----------------------------------------------------------------
418
+
419
+ def execute_single_tool(tool_call, tools, hooks, event_emitter, abort_controller = nil, runtime_event_sink = nil)
107
420
  return aborted_result(tool_call) if abort_controller&.aborted?
108
421
 
109
422
  tool = tools.find { |t| t.name == tool_call.name }
@@ -139,53 +452,101 @@ module Ask
139
452
  name: tool_call.name, arguments: args, id: tool_call.id
140
453
  ))
141
454
 
455
+ runtime_call = build_runtime_tool_call(tool_call, effective_args: args)
456
+ # Expose the pending runtime call before execution starts so tools
457
+ # can observe the full pending → running → terminal lifecycle.
458
+ Thread.current[:ask_agent_runtime_call] = runtime_call
459
+ runtime_context = build_runtime_context(abort_controller)
460
+
142
461
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
462
+ start_timestamp = Time.now
143
463
  Thread.current[:ask_agent_tool_call_id] = tool_call.id
144
- result = begin
145
- execute_with_retry(tool, tool_call.id, args, abort_controller)
464
+ Thread.current[:ask_agent_runtime_context] = runtime_context
465
+
466
+ # Transition to :running once execution is active and observable by tools.
467
+ running_call = runtime_call.with(state: :running, started_at: start_timestamp)
468
+ Thread.current[:ask_agent_runtime_call] = running_call
469
+
470
+ # Emit runtime ToolStarted event (one per tool call).
471
+ emit_runtime_event(runtime_event_sink, :tool_started,
472
+ Ask::Runtime::Events::ToolStarted.new(
473
+ tool_call: running_call, execution_context: runtime_context, timestamp: start_timestamp
474
+ ))
475
+
476
+ # Shared execution core — invoke tool with retry, get ToolResult.
477
+ tool_result = begin
478
+ invoke_tool_with_retry(tool, tool_call.id, args, abort_controller)
146
479
  rescue Exception => e
147
- { result: e.message, is_error: true, error: e.class.name }
148
- ensure
149
- Thread.current[:ask_agent_tool_call_id] = nil
480
+ Ask::Runtime::ToolResult.failure("#{e.class}: #{e.message}")
150
481
  end
151
- duration = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).to_i
482
+
483
+ # Classify into terminal state + legacy agent result hash.
484
+ terminal_state, legacy_result = classify_tool_result(tool_result, abort_controller)
485
+
486
+ # Transition runtime call to terminal state and attach ToolResult
487
+ # BEFORE clearing thread locals so tools can observe the full lifecycle
488
+ # including the unwrapped ToolResult.output.
489
+ finished_call = running_call.with(
490
+ state: terminal_state,
491
+ tool_result: tool_result,
492
+ error: tool_result.error_message,
493
+ finished_at: Time.now
494
+ )
495
+ Thread.current[:ask_agent_runtime_call] = finished_call
496
+
497
+ # Clear all thread locals — even on errors — so no stale or
498
+ # terminal runtime objects remain in Thread.current after cleanup.
499
+ Thread.current[:ask_agent_tool_call_id] = nil
500
+ Thread.current[:ask_agent_runtime_call] = nil
501
+ Thread.current[:ask_agent_runtime_context] = nil
502
+
503
+ duration_secs = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
504
+ duration_ms = (duration_secs * 1000).to_i
152
505
  @total_executions += 1
153
506
 
507
+ # Emit runtime terminal event (exactly one per tool call).
508
+ emit_runtime_terminal_event(
509
+ runtime_event_sink, terminal_state,
510
+ tool_call: finished_call, tool_result: tool_result,
511
+ execution_context: runtime_context, timestamp: Time.now,
512
+ duration: duration_secs
513
+ )
514
+
154
515
  return aborted_result(tool_call) if abort_controller&.aborted?
155
516
 
156
- hook_result = hooks.run_after_tool(tool_call, result, {})
517
+ hook_result = hooks.run_after_tool(tool_call, legacy_result, {})
157
518
  if hook_result&.dig(:action) == :transform
158
- result = hook_result[:result]
519
+ legacy_result = hook_result[:result]
159
520
  end
160
521
 
161
- is_error = result[:is_error] == true
162
- critical = is_error && critical_error?(result[:error])
163
- halted = result[:halted] == true
522
+ is_error = legacy_result[:is_error] == true
523
+ critical = is_error && critical_error?(legacy_result[:error])
524
+ halted = legacy_result[:halted] == true
164
525
 
165
526
  if halted
166
527
  abort_controller&.abort!
167
528
  end
168
529
 
169
530
  if is_error && @telemetry
170
- @telemetry.log(:tool_error, session_id: @session_id, tool_name: tool_call.name, error_class: result[:error] || "RuntimeError", error_message: result[:result].to_s)
531
+ @telemetry.log(:tool_error, session_id: @session_id, tool_name: tool_call.name, error_class: legacy_result[:error] || "RuntimeError", error_message: legacy_result[:result].to_s)
171
532
  end
172
533
 
173
534
  event_emitter.emit(Events::ToolExecutionEnd.new(
174
- name: tool_call.name, id: tool_call.id, result: result, is_error: is_error, duration_ms: duration
535
+ name: tool_call.name, id: tool_call.id, result: legacy_result, is_error: is_error, duration_ms: duration_ms
175
536
  ))
176
537
 
177
538
  message = if is_error
178
- tool_result = result[:result]
179
- error_msg = if tool_result.is_a?(Hash) && tool_result[:error]
180
- tool_result[:error].to_s
181
- elsif tool_result.is_a?(String)
182
- tool_result
539
+ inner = legacy_result[:result]
540
+ error_msg = if inner.is_a?(Hash) && inner[:error]
541
+ inner[:error].to_s
542
+ elsif inner.is_a?(String)
543
+ inner
183
544
  else
184
- result.to_s
545
+ legacy_result.to_s
185
546
  end
186
547
  "Tool #{tool_call.name} error: #{error_msg}"
187
548
  else
188
- result[:result].to_s
549
+ legacy_result[:result].to_s
189
550
  end
190
551
 
191
552
  # Large outputs never enter the transcript: store the full message
@@ -200,8 +561,8 @@ module Ask
200
561
  # Collect tool-produced deliverables (metadata[:artifact]) into the
201
562
  # session's artifact store. Best-effort: a malformed artifact is
202
563
  # noted in the message, never a tool failure.
203
- if @artifact_store && result[:result].respond_to?(:metadata) &&
204
- (artifact = result[:result].metadata[:artifact])
564
+ if @artifact_store && legacy_result[:result].respond_to?(:metadata) &&
565
+ (artifact = legacy_result[:result].metadata[:artifact])
205
566
  begin
206
567
  attrs = symbolize_artifact(artifact)
207
568
  @artifact_store.store(@session_id, **attrs)
@@ -210,8 +571,8 @@ module Ask
210
571
  end
211
572
  end
212
573
 
213
- inner = result[:result]
214
- status = if result[:is_error] == true
574
+ inner = legacy_result[:result]
575
+ status = if legacy_result[:is_error] == true
215
576
  "error"
216
577
  elsif inner.respond_to?(:pending?) && inner.pending?
217
578
  "pending"
@@ -223,42 +584,33 @@ module Ask
223
584
  tool_name: tool_call.name,
224
585
  message: message,
225
586
  status: status,
226
- result: result,
587
+ result: legacy_result,
227
588
  critical_failure: critical,
228
589
  halted: halted
229
590
  }
230
591
  end
231
592
 
232
- def execute_with_retry(tool, tool_call_id, args, abort_controller = nil)
233
- @max_retries.times do |attempt|
234
- return { result: nil, is_error: true, error: "Aborted" } if abort_controller&.aborted?
235
-
236
- result = try_call(tool, args, abort_controller)
237
- return result unless result[:is_error] && retryable_error_name?(result[:error])
593
+ def emit_runtime_event(sink, event_type, event)
594
+ return unless sink
238
595
 
239
- sleep((2 ** attempt) * 0.5 + rand(0.0..0.5))
240
- end
596
+ sink.emit(event_type, event: event)
597
+ end
241
598
 
242
- return { result: nil, is_error: true, error: "Aborted" } if abort_controller&.aborted?
243
- try_call(tool, args)
599
+ def emit_runtime_terminal_event(sink, terminal_state, **attrs)
600
+ return unless sink
601
+
602
+ event_class = case terminal_state
603
+ when :completed then Ask::Runtime::Events::ToolCompleted
604
+ when :failed then Ask::Runtime::Events::ToolFailed
605
+ when :cancelled then Ask::Runtime::Events::ToolCancelled
606
+ when :timed_out then Ask::Runtime::Events::ToolTimedOut
607
+ else return
608
+ end
609
+ sink.emit(terminal_event_type(terminal_state), event: event_class.new(**attrs))
244
610
  end
245
611
 
246
- def try_call(tool, args, abort_controller = nil)
247
- result = tool.call(args, abort_controller: abort_controller)
248
- is_error = if result.respond_to?(:pending?) && result.pending?
249
- false # async tool: work continues in the background
250
- elsif result.respond_to?(:ok?)
251
- !result.ok?
252
- else
253
- false
254
- end
255
- hash = { result: result, is_error: is_error }
256
- if result.respond_to?(:metadata) && result.metadata&.dig(:halted)
257
- hash[:halted] = true
258
- end
259
- hash
260
- rescue => e
261
- { result: e.message, is_error: true, error: e.class.name }
612
+ def terminal_event_type(state)
613
+ "tool_#{state}".to_sym
262
614
  end
263
615
 
264
616
  # Store a large tool message in the output store and return a short
@@ -307,6 +659,103 @@ module Ask
307
659
  aborted: true
308
660
  }
309
661
  end
662
+
663
+ # Map a ToolResult to a runtime terminal state and a legacy agent
664
+ # result hash. Returns [terminal_state_symbol, legacy_hash].
665
+ #
666
+ # This is the shared classification logic: the runtime path uses the
667
+ # ToolResult directly; the batch path wraps it in the agent's hash
668
+ # format for backward compatibility.
669
+ def classify_tool_result(tool_result, abort_controller)
670
+ if abort_controller&.aborted?
671
+ return [:cancelled, { result: "Aborted by sibling failure", is_error: true, error: "Aborted" }]
672
+ end
673
+
674
+ halted = tool_halted?(tool_result)
675
+
676
+ case tool_result.outcome
677
+ when :cancelled
678
+ [:cancelled, { result: tool_result.error_message, is_error: true, error: "Aborted" }]
679
+ when :failure
680
+ error_class = extract_error_class(tool_result.error_message)
681
+ [:failed, { result: tool_result.error_message, is_error: true, error: error_class }]
682
+ when :timeout
683
+ [:timed_out, { result: tool_result.error_message, is_error: true, error: "Timeout" }]
684
+ when :success
685
+ if tool_result.output.respond_to?(:pending?) && tool_result.output.pending?
686
+ [:completed, { result: tool_result.output, is_error: false, halted: halted }]
687
+ else
688
+ # Preserve the original Ask::Result in the legacy hash for
689
+ # backward compatibility — tools that return Ask::Result get
690
+ # their result object in [:result][:result].
691
+ legacy_inner = if tool_result.result.is_a?(Ask::Result)
692
+ tool_result.result
693
+ else
694
+ tool_result.output
695
+ end
696
+ [:completed, { result: legacy_inner, is_error: false, halted: halted }]
697
+ end
698
+ else
699
+ [:completed, { result: tool_result.output, is_error: false, halted: halted }]
700
+ end
701
+ end
702
+
703
+ # Detect if a tool result carries halted metadata.
704
+ # Checks the underlying Ask::Result (ToolResult.result) for halted
705
+ # metadata, since that's where tools set it via Ask::Result.ok(..., metadata: { halted: true }).
706
+ def tool_halted?(tool_result)
707
+ return false unless tool_result.success?
708
+
709
+ inner = tool_result.result
710
+ inner.respond_to?(:metadata) && inner.metadata&.dig(:halted) == true
711
+ end
712
+
713
+ # Extract the error class name from a tool failure message.
714
+ # Messages from try_call are formatted as "ClassName: message".
715
+ # Split on ": " (colon-space) to avoid breaking on :: namespaces.
716
+ def extract_error_class(error_message)
717
+ return "Error" unless error_message
718
+ error_message.split(": ").first || "Error"
719
+ end
720
+
721
+ # Legacy classify_result for backward compatibility with code that
722
+ # passes raw agent result hashes. Delegates to the shared
723
+ # classify_tool_result after wrapping the hash in a ToolResult.
724
+ def classify_result(result, abort_controller)
725
+ tool_result = if abort_controller&.aborted?
726
+ Ask::Runtime::ToolResult.cancelled("Aborted by sibling failure")
727
+ elsif result[:is_error] == true
728
+ error_msg = extract_error_from_hash(result)
729
+ Ask::Runtime::ToolResult.failure(error_msg)
730
+ else
731
+ inner = result[:result]
732
+ if inner.respond_to?(:pending?) && inner.pending?
733
+ Ask::Runtime::ToolResult.success(data: inner)
734
+ else
735
+ data = if inner.is_a?(Ask::Result)
736
+ inner.output
737
+ elsif inner.is_a?(Hash) && inner.key?(:result)
738
+ inner[:result]
739
+ else
740
+ inner
741
+ end
742
+ Ask::Runtime::ToolResult.success(data: data)
743
+ end
744
+ end
745
+
746
+ classify_tool_result(tool_result, abort_controller)
747
+ end
748
+
749
+ def extract_error_from_hash(result)
750
+ inner = result[:result]
751
+ if inner.is_a?(Hash) && inner[:error]
752
+ inner[:error].to_s
753
+ elsif inner.is_a?(String)
754
+ inner
755
+ else
756
+ result.to_s
757
+ end
758
+ end
310
759
  end
311
760
  end
312
761
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Agent
5
- VERSION = "0.40.19"
5
+ VERSION = "0.40.21"
6
6
  end
7
7
  end
data/lib/ask/agent.rb CHANGED
@@ -8,6 +8,7 @@ require "ask/skills"
8
8
  require "ask-llm-providers"
9
9
  require "ask-tools"
10
10
  require "ask-state-providers"
11
+ require "ask-runtime"
11
12
 
12
13
  module Ask
13
14
  module Agent
@@ -215,6 +216,7 @@ require_relative "agent/chat"
215
216
  require_relative "agent/telemetry"
216
217
  require_relative "agent/tool_abort_controller"
217
218
  require_relative "agent/session"
219
+ require_relative "agent/session_adapter"
218
220
  require_relative "agent/loop"
219
221
  require_relative "agent/reflector"
220
222
  require_relative "agent/evaluator"
@@ -258,6 +260,20 @@ module Ask
258
260
  def current_tool_call_id
259
261
  Thread.current[:ask_agent_tool_call_id]
260
262
  end
263
+
264
+ # @return [Ask::Runtime::ToolCall, nil] the runtime tool call for the
265
+ # currently executing tool on this thread. Available only during tool
266
+ # execution inside ToolExecutor.
267
+ def current_runtime_call
268
+ Thread.current[:ask_agent_runtime_call]
269
+ end
270
+
271
+ # @return [Ask::Runtime::ExecutionContext, nil] the runtime execution
272
+ # context for the currently executing tool on this thread. Available
273
+ # only during tool execution inside ToolExecutor.
274
+ def current_runtime_context
275
+ Thread.current[:ask_agent_runtime_context]
276
+ end
261
277
  end
262
278
  end
263
279
  end
data/lib/ask-agent.rb CHANGED
@@ -2,4 +2,5 @@
2
2
 
3
3
  require "ask-llm-providers"
4
4
  require "ask-tools"
5
+ require "ask-runtime"
5
6
  require "ask/agent"
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.19
4
+ version: 0.40.21
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.12.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: ask-runtime
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.1.0
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.0
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: ask-state-providers
28
42
  requirement: !ruby/object:Gem::Requirement
@@ -107,6 +121,20 @@ dependencies:
107
121
  - - "~>"
108
122
  - !ruby/object:Gem::Version
109
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
110
138
  - !ruby/object:Gem::Dependency
111
139
  name: minitest
112
140
  requirement: !ruby/object:Gem::Requirement
@@ -202,6 +230,7 @@ files:
202
230
  - lib/ask/agent/reflector.rb
203
231
  - lib/ask/agent/scheduler.rb
204
232
  - lib/ask/agent/session.rb
233
+ - lib/ask/agent/session_adapter.rb
205
234
  - lib/ask/agent/stream_transforms/base.rb
206
235
  - lib/ask/agent/stream_transforms/extract_json.rb
207
236
  - lib/ask/agent/stream_transforms/pipeline.rb