ask-agent 0.7.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d3eb1ad33d205f6b868c931aabf78172bc3b3d3dca0674ad67910d1ff7997a2
4
- data.tar.gz: faff03efa8eba0d1594756745b9a29dcb8c2d51a362d818f7ec18904c3f9f92e
3
+ metadata.gz: f95edab12d96da68593e426829ac2507682b4b5620b5ec0b09269062bf168bd9
4
+ data.tar.gz: 7353fff33dba2ebff749df32d4c079759f1f4b2eca9193a5299fe9ec7238966c
5
5
  SHA512:
6
- metadata.gz: a9212d2d0730b6e5a9df227bb75c15a8491685b9243e6cf5f4f7511763e51a4b0bcc31c624f760e113c9e52d701863cf95664d67200bc54d67d7d9922932d6f6
7
- data.tar.gz: 1c8664489865b480faf003c19952fc13963238e1259c9f3e7170592a31a10eca13b39cab40fa2d428274b3f3f49a405b8be817a5475b3e86fd211bcfa4c1630b
6
+ metadata.gz: 3e3dc6f6cae44b0c2f36e12c9253b3f21e97a17634907754cc1d2ea8930bd8bcc54b2f65ef2194dae9e1e60d4a721d8071a677f3f0bbfa0d99d68515c0da82b9
7
+ data.tar.gz: ab443967abf274b113b9ebd8f2901feffa1cf2d7e1436e6a14df8d1dbb87fa1f204d0cb97d2d567858e89631c0b08ed97bf4183174c6ffce97aee363ffa0f724
data/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ ## [0.8.0] — 2026-07-21
2
+
3
+ ### Added
4
+
5
+ - **Provider-executed tool support in the agent loop** — `ResponseMessage` now carries a `tool_results` field for pre-computed results from provider-executed tools (e.g. OpenAI web search, file search, code interpreter). The `Loop` detects these results, adds them directly to the conversation without local execution, then proceeds with any remaining user tool calls.
6
+
7
+ ```ruby
8
+ agent = Ask::Agent.new("health_check")
9
+ agent.run("Search the web for server status")
10
+ # web_search runs on OpenAI's side; results come back pre-computed
11
+ ```
12
+
13
+ ### Changed
14
+
15
+ - **`ResponseMessage`** added `tool_results` field (default `{}`). All existing call sites are compatible via keyword argument defaults.
16
+ - **`Loop#run_turn`** — separates provider-executed results from user tool calls. Provider results are added to the conversation immediately. User tool calls continue to be executed locally via `ToolExecutor`.
17
+ - **OpenAI provider** — `split_tools` separates `Ask::ProviderTool` objects from regular tools. `format_responses_tools` converts provider tools to the Responses API format. When provider tools are present, the Responses API endpoint is used instead of Chat Completions.
18
+
19
+ ### Tested
20
+
21
+ - 13 new integration tests: loop handling with mixed tool types, provider-only tools, tool splitting, Responses API formatting.
22
+ - Full suite: 329 tests, 592 assertions — 0 failures.
23
+
1
24
  ## [0.7.0] — 2026-07-21
2
25
 
3
26
  ### Added
@@ -4,7 +4,14 @@ module Ask
4
4
  module Agent
5
5
  # Response message returned by {Chat#ask}.
6
6
  # Includes token counts and cost when available from the provider.
7
- ResponseMessage = Data.define(:content, :tool_calls, :thinking, :input_tokens, :output_tokens, :cost) do
7
+ # +tool_calls+ are calls that need local execution.
8
+ # +tool_results+ are pre-computed results from provider-executed tools.
9
+ ResponseMessage = Data.define(:content, :tool_calls, :tool_results, :thinking, :input_tokens, :output_tokens, :cost) do
10
+ def initialize(content:, tool_calls: {}, tool_results: {}, thinking: nil, input_tokens: nil, output_tokens: nil, cost: nil)
11
+ super(content: content, tool_calls: tool_calls, tool_results: tool_results, thinking: thinking,
12
+ input_tokens: input_tokens, output_tokens: output_tokens, cost: cost)
13
+ end
14
+
8
15
  def tool_call? = !tool_calls.empty?
9
16
  def to_s = content.to_s
10
17
  end
@@ -286,6 +293,7 @@ module Ask
286
293
  ResponseMessage.new(
287
294
  content: stream.accumulated_text,
288
295
  tool_calls: build_current_tool_calls(calls_acc),
296
+ tool_results: {},
289
297
  thinking: stream.chunks.filter_map(&:thinking).last,
290
298
  input_tokens: tokens[:input],
291
299
  output_tokens: tokens[:output],
@@ -300,9 +308,11 @@ module Ask
300
308
  input_tokens = metadata[:input_tokens] || metadata["input_tokens"]
301
309
  output_tokens = metadata[:output_tokens] || metadata["output_tokens"]
302
310
  cost = calculate_cost(input_tokens, output_tokens)
311
+ tool_results = metadata[:provider_results] || metadata["provider_results"] || {}
303
312
  ResponseMessage.new(
304
313
  content: msg.content.to_s,
305
314
  tool_calls: tool_calls,
315
+ tool_results: tool_results,
306
316
  thinking: thinking,
307
317
  input_tokens: input_tokens,
308
318
  output_tokens: output_tokens,
@@ -43,34 +43,56 @@ module Ask
43
43
  event_emitter.emit(Events::MessageEnd.new(tool_calls: response.tool_call?))
44
44
  @turn_count += 1
45
45
 
46
- unless response.tool_call?
46
+ # Check if there are any tool calls (user-executed or provider-executed)
47
+ has_tool_calls = response.tool_call? || (response.tool_results&.any? == true)
48
+
49
+ unless has_tool_calls
47
50
  @consecutive_tool_turns = 0
48
51
  return response.content.to_s
49
52
  end
50
53
 
51
54
  @consecutive_tool_turns += 1
52
55
 
53
- # Execute tools with concurrent result streaming
54
- tool_results = tool_executor.execute_parallel(
55
- response.tool_calls, tools, hooks, event_emitter, ToolAbortController.new
56
- ) do |tool_call_id, result|
57
- # Add each tool result as it completes (concurrent streaming)
58
- tc = response.tool_calls[tool_call_id]
59
- chat.add_message(role: :tool, content: result[:message].to_s, tool_call_id: tool_call_id) if tc
56
+ provider_results = response.tool_results || {}
57
+ all_tool_results = []
58
+
59
+ # Add provider-executed tool results directly to conversation
60
+ provider_results.each do |id, result|
61
+ chat.add_message(role: :tool, content: result[:message].to_s, tool_call_id: id)
62
+ all_tool_results << {
63
+ tool_name: result[:tool_name] || id,
64
+ message: result[:message].to_s,
65
+ status: result[:status] || "success",
66
+ provider_executed: true
67
+ }
68
+ end
69
+
70
+ # Determine which tool calls still need local execution
71
+ user_tool_calls = response.tool_calls.reject { |id, _| provider_results.key?(id) }
72
+
73
+ if user_tool_calls.any?
74
+ # Execute user tool calls locally
75
+ user_results = tool_executor.execute_parallel(
76
+ user_tool_calls, tools, hooks, event_emitter, ToolAbortController.new
77
+ ) do |tool_call_id, result|
78
+ tc = user_tool_calls[tool_call_id]
79
+ chat.add_message(role: :tool, content: result[:message].to_s, tool_call_id: tool_call_id) if tc
80
+ end
81
+ all_tool_results.concat(user_results)
60
82
  end
61
83
 
62
84
  # Check loop detection
63
- if loop_detected?(tool_results)
64
- raise LoopDetected, tool_results.last[:tool_name]
85
+ if loop_detected?(all_tool_results)
86
+ raise LoopDetected, all_tool_results.last[:tool_name]
65
87
  end
66
88
 
67
89
  if @consecutive_tool_turns >= @max_consecutive_tool_turns
68
- summary = tool_results.map { |r| r[:message].to_s.truncate(80) }.first(2).join("; ")
90
+ summary = all_tool_results.map { |r| r[:message].to_s.truncate(80) }.first(2).join("; ")
69
91
  return "Based on my investigation: #{summary}"
70
92
  end
71
93
 
72
94
  event_emitter.emit(Events::TurnEnd.new(
73
- tool_results: tool_results,
95
+ tool_results: all_tool_results,
74
96
  turn_number: @turn_count,
75
97
  input_tokens: @last_input_tokens,
76
98
  output_tokens: @last_output_tokens,
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Agent
5
- VERSION = "0.7.0"
5
+ VERSION = "0.8.0"
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.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto