exa-ai 0.11.2 → 0.12.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.
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "exa-ai"
5
+
6
+ # Parse command-line arguments
7
+ api_key = nil
8
+ run_id = nil
9
+ output_format = "json"
10
+
11
+ args = ARGV.dup
12
+ while args.any?
13
+ arg = args.shift
14
+ case arg
15
+ when "--api-key"
16
+ api_key = args.shift
17
+ when "--output-format"
18
+ output_format = args.shift
19
+ when "--help", "-h"
20
+ puts <<~HELP
21
+ Usage: exa-ai agent-run-events <run_id> [options]
22
+
23
+ Retrieve events for an agent run.
24
+
25
+ Arguments:
26
+ run_id ID of the agent run
27
+
28
+ Options:
29
+ --api-key KEY Exa API key (or use EXA_API_KEY env var)
30
+ --output-format FORMAT Output format: json, pretty, or text (default: json)
31
+ --help, -h Show this help message
32
+
33
+ Examples:
34
+ exa-ai agent-run-events run_123
35
+ exa-ai agent-run-events run_123 --output-format pretty
36
+ HELP
37
+ exit 0
38
+ else
39
+ # First positional argument is the run_id
40
+ if run_id.nil?
41
+ run_id = arg
42
+ else
43
+ warn "Unknown option: #{arg}"
44
+ exit 1
45
+ end
46
+ end
47
+ end
48
+
49
+ # Validate required arguments
50
+ if run_id.nil?
51
+ warn "Error: Run ID argument required"
52
+ warn "Usage: exa-ai agent-run-events <run_id> [options]"
53
+ warn "Try 'exa-ai agent-run-events --help' for more information"
54
+ exit 1
55
+ end
56
+
57
+ begin
58
+ # Resolve API key
59
+ api_key = Exa::CLI::Base.resolve_api_key(api_key)
60
+
61
+ # Build client
62
+ client = Exa::CLI::Base.build_client(api_key)
63
+
64
+ # Call API
65
+ events = client.agent_run_events(run_id)
66
+
67
+ # Format output
68
+ items = events.respond_to?(:data) ? events.data : events
69
+ case output_format
70
+ when "json"
71
+ puts JSON.pretty_generate(events.respond_to?(:to_h) ? events.to_h : events)
72
+ when "pretty"
73
+ puts "Events for Agent Run: #{run_id}"
74
+ puts ""
75
+ items.each_with_index do |event, i|
76
+ puts "#{i + 1}. #{event.respond_to?(:to_h) ? event.to_h.inspect : event}"
77
+ end
78
+ when "text"
79
+ items.each do |event|
80
+ puts event.respond_to?(:to_h) ? event.to_h.inspect : event.to_s
81
+ end
82
+ else
83
+ puts JSON.pretty_generate(events.respond_to?(:to_h) ? events.to_h : events)
84
+ end
85
+
86
+ rescue Exa::NotFound => e
87
+ warn "Agent run not found: #{e.message}"
88
+ exit 1
89
+ rescue Exa::Unauthorized => e
90
+ warn "Authentication failed: #{e.message}"
91
+ warn "Please provide a valid API key via --api-key or EXA_API_KEY environment variable"
92
+ exit 1
93
+ rescue Exa::ClientError => e
94
+ warn "Client error: #{e.message}"
95
+ exit 1
96
+ rescue Exa::ServerError => e
97
+ warn "Server error: #{e.message}"
98
+ exit 1
99
+ rescue Exa::Error => e
100
+ warn "Error: #{e.message}"
101
+ exit 1
102
+ rescue StandardError => e
103
+ warn "Unexpected error: #{e.message}"
104
+ exit 1
105
+ end
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "exa-ai"
5
+
6
+ # Parse command-line arguments
7
+ api_key = nil
8
+ run_id = nil
9
+ output_format = "json"
10
+
11
+ args = ARGV.dup
12
+ while args.any?
13
+ arg = args.shift
14
+ case arg
15
+ when "--api-key"
16
+ api_key = args.shift
17
+ when "--output-format"
18
+ output_format = args.shift
19
+ when "--help", "-h"
20
+ puts <<~HELP
21
+ Usage: exa-ai agent-run-get <run_id> [options]
22
+
23
+ Get the status and results of an agent run.
24
+
25
+ Arguments:
26
+ run_id ID of the agent run to retrieve
27
+
28
+ Options:
29
+ --api-key KEY Exa API key (or use EXA_API_KEY env var)
30
+ --output-format FORMAT Output format: json, pretty, or text (default: json)
31
+ --help, -h Show this help message
32
+
33
+ Examples:
34
+ exa-ai agent-run-get run_123
35
+ exa-ai agent-run-get run_123 --output-format pretty
36
+ HELP
37
+ exit 0
38
+ else
39
+ # First positional argument is the run_id
40
+ if run_id.nil?
41
+ run_id = arg
42
+ else
43
+ warn "Unknown option: #{arg}"
44
+ exit 1
45
+ end
46
+ end
47
+ end
48
+
49
+ # Validate required arguments
50
+ if run_id.nil?
51
+ warn "Error: Run ID argument required"
52
+ warn "Usage: exa-ai agent-run-get <run_id> [options]"
53
+ warn "Try 'exa-ai agent-run-get --help' for more information"
54
+ exit 1
55
+ end
56
+
57
+ begin
58
+ # Resolve API key
59
+ api_key = Exa::CLI::Base.resolve_api_key(api_key)
60
+
61
+ # Build client
62
+ client = Exa::CLI::Base.build_client(api_key)
63
+
64
+ # Call API
65
+ result = client.agent_run_get(run_id)
66
+
67
+ # Format output
68
+ formatted = Exa::CLI::Formatters::AgentRunFormatter.format_run(result, output_format)
69
+ puts formatted
70
+
71
+ rescue Exa::NotFound => e
72
+ warn "Agent run not found: #{e.message}"
73
+ exit 1
74
+ rescue Exa::Unauthorized => e
75
+ warn "Authentication failed: #{e.message}"
76
+ warn "Please provide a valid API key via --api-key or EXA_API_KEY environment variable"
77
+ exit 1
78
+ rescue Exa::ClientError => e
79
+ warn "Client error: #{e.message}"
80
+ exit 1
81
+ rescue Exa::ServerError => e
82
+ warn "Server error: #{e.message}"
83
+ exit 1
84
+ rescue Exa::Error => e
85
+ warn "Error: #{e.message}"
86
+ exit 1
87
+ rescue StandardError => e
88
+ warn "Unexpected error: #{e.message}"
89
+ exit 1
90
+ end
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "exa-ai"
5
+
6
+ # Parse command-line arguments
7
+ api_key = nil
8
+ cursor = nil
9
+ limit = 10
10
+ output_format = "json"
11
+
12
+ args = ARGV.dup
13
+ while args.any?
14
+ arg = args.shift
15
+ case arg
16
+ when "--api-key"
17
+ api_key = args.shift
18
+ when "--cursor"
19
+ cursor = args.shift
20
+ when "--limit"
21
+ limit = args.shift.to_i
22
+ when "--output-format"
23
+ output_format = args.shift
24
+ when "--help", "-h"
25
+ puts <<~HELP
26
+ Usage: exa-ai agent-run-list [options]
27
+
28
+ List agent runs with cursor-based pagination.
29
+
30
+ Options:
31
+ --api-key KEY Exa API key (or use EXA_API_KEY env var)
32
+ --cursor CURSOR Pagination cursor for next page
33
+ --limit LIMIT Number of results per page (default: 10)
34
+ --output-format FORMAT Output format: json, pretty, or text (default: json)
35
+ --help, -h Show this help message
36
+
37
+ Examples:
38
+ exa-ai agent-run-list
39
+ exa-ai agent-run-list --limit 20
40
+ exa-ai agent-run-list --cursor next_page_cursor
41
+ exa-ai agent-run-list --output-format pretty
42
+ HELP
43
+ exit 0
44
+ else
45
+ warn "Unknown option: #{arg}"
46
+ exit 1
47
+ end
48
+ end
49
+
50
+ begin
51
+ # Resolve API key
52
+ api_key = Exa::CLI::Base.resolve_api_key(api_key)
53
+
54
+ # Build client
55
+ client = Exa::CLI::Base.build_client(api_key)
56
+
57
+ # Build parameters
58
+ params = { limit: limit }
59
+ params[:cursor] = cursor if cursor
60
+
61
+ # Call API
62
+ result = client.agent_run_list(**params)
63
+
64
+ # Format output
65
+ formatted = Exa::CLI::Formatters::AgentRunFormatter.format_list(result, output_format)
66
+ puts formatted
67
+
68
+ # Show pagination info if there are more results
69
+ if result.has_more && result.next_cursor
70
+ if output_format == "pretty"
71
+ puts "\n" + "=" * 80
72
+ puts "More results available. Use --cursor #{result.next_cursor} to get next page."
73
+ else
74
+ warn "More results available. Use --cursor #{result.next_cursor} to get next page."
75
+ end
76
+ end
77
+
78
+ rescue Exa::Unauthorized => e
79
+ warn "Authentication failed: #{e.message}"
80
+ warn "Please provide a valid API key via --api-key or EXA_API_KEY environment variable"
81
+ exit 1
82
+ rescue Exa::ClientError => e
83
+ warn "Client error: #{e.message}"
84
+ exit 1
85
+ rescue Exa::ServerError => e
86
+ warn "Server error: #{e.message}"
87
+ exit 1
88
+ rescue Exa::Error => e
89
+ warn "Error: #{e.message}"
90
+ exit 1
91
+ rescue StandardError => e
92
+ warn "Unexpected error: #{e.message}"
93
+ exit 1
94
+ end
@@ -0,0 +1,149 @@
1
+ module Exa
2
+ module CLI
3
+ module Formatters
4
+ class AgentRunFormatter
5
+ def self.format_run(run, format)
6
+ case format
7
+ when "json"
8
+ JSON.pretty_generate(run.to_h)
9
+ when "pretty"
10
+ format_run_pretty(run)
11
+ when "text"
12
+ format_run_text(run)
13
+ when "toon"
14
+ Exa::CLI::Base.encode_as_toon(run.to_h)
15
+ else
16
+ JSON.pretty_generate(run.to_h)
17
+ end
18
+ end
19
+
20
+ def self.format_list(list, format)
21
+ case format
22
+ when "json"
23
+ JSON.pretty_generate(list.to_h)
24
+ when "pretty"
25
+ format_list_pretty(list)
26
+ when "text"
27
+ format_list_text(list)
28
+ when "toon"
29
+ Exa::CLI::Base.encode_as_toon(list.to_h)
30
+ else
31
+ JSON.pretty_generate(list.to_h)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def self.format_run_pretty(run)
38
+ output = []
39
+ output << "Agent Run: #{run.id}"
40
+ output << "Status: #{run.status.upcase}"
41
+ output << "Created: #{run.created_at}"
42
+ output << ""
43
+
44
+ case run.status
45
+ when "queued"
46
+ output << "Run is queued..."
47
+ when "running"
48
+ output << "Run is running... ⚙️"
49
+ when "completed"
50
+ output << "Output:"
51
+ output << "--------"
52
+ if run.output.is_a?(Hash)
53
+ output << (run.output[:text] || run.output["text"] || run.output.inspect)
54
+ else
55
+ output << run.output.to_s
56
+ end
57
+ if run.output.is_a?(Hash) && (structured = run.output["structured"] || run.output[:structured])
58
+ output << ""
59
+ output << "Structured:"
60
+ output << JSON.pretty_generate(structured)
61
+ end
62
+ sources = grounding_sources(run)
63
+ unless sources.empty?
64
+ output << ""
65
+ output << "Grounding:"
66
+ sources.each { |s| output << " - #{s}" }
67
+ end
68
+ output << ""
69
+ if run.cost_dollars
70
+ total = run.cost_dollars.is_a?(Hash) ? (run.cost_dollars["total"] || run.cost_dollars[:total]) : run.cost_dollars
71
+ output << "Cost: $#{total}"
72
+ end
73
+ output << "Completed: #{run.completed_at}" if run.completed_at
74
+ when "failed"
75
+ output << "Run failed"
76
+ output << "Stop reason: #{run.stop_reason}" if run.stop_reason
77
+ when "cancelled"
78
+ output << "Run was cancelled"
79
+ output << "Completed: #{run.completed_at}" if run.completed_at
80
+ end
81
+
82
+ output.join("\n")
83
+ end
84
+
85
+ def self.format_list_pretty(list)
86
+ output = []
87
+ output << "Agent Runs (#{list.data.length}):"
88
+ output << ""
89
+
90
+ if list.data.empty?
91
+ output << "No runs found."
92
+ else
93
+ output << "%-40s %-15s %s" % ["Run ID", "Status", "Created"]
94
+ output << "-" * 70
95
+
96
+ list.data.each do |run|
97
+ run_id = run.id.to_s[0..38]
98
+ status = run.status.upcase[0..14]
99
+ created = run.created_at.to_s[0..19]
100
+ output << "%-40s %-15s %s" % [run_id, status, created]
101
+ end
102
+ end
103
+
104
+ output << ""
105
+ if list.has_more
106
+ output << "More results available. Use --cursor #{list.next_cursor} for next page."
107
+ else
108
+ output << "End of results."
109
+ end
110
+
111
+ output.join("\n")
112
+ end
113
+
114
+ def self.format_run_text(run)
115
+ output = []
116
+ output << "#{run.id} #{run.status.upcase} #{run.created_at}"
117
+ if run.status == "completed" && run.output
118
+ text = run.output.is_a?(Hash) ? (run.output[:text] || run.output["text"]) : run.output.to_s
119
+ output << text.to_s
120
+ if run.output.is_a?(Hash) && (structured = run.output["structured"] || run.output[:structured])
121
+ output << JSON.generate(structured)
122
+ end
123
+ elsif run.status == "failed"
124
+ output << "Stop reason: #{run.stop_reason}"
125
+ end
126
+ output.join("\n")
127
+ end
128
+
129
+ # Flattens an output.grounding array into displayable source strings,
130
+ # tolerating both shapes seen from the API: flat {url,title} items and
131
+ # nested {citations:[{url,title}]} items.
132
+ def self.grounding_sources(run)
133
+ grounding = run.output.is_a?(Hash) ? (run.output["grounding"] || run.output[:grounding]) : nil
134
+ Array(grounding).flat_map do |item|
135
+ citations = item.is_a?(Hash) && item["citations"] ? item["citations"] : [item]
136
+ citations.map { |c| c.is_a?(Hash) ? (c["title"] || c["url"]) : c }
137
+ end.compact
138
+ end
139
+
140
+ def self.format_list_text(list)
141
+ output = list.data.map do |run|
142
+ "#{run.id} #{run.status.upcase} #{run.created_at}"
143
+ end
144
+ output.join("\n")
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
data/lib/exa/client.rb CHANGED
@@ -112,6 +112,83 @@ module Exa
112
112
  Services::ResearchGet.new(connection, research_id: research_id, **params).call
113
113
  end
114
114
 
115
+ # Create a new agent run
116
+ #
117
+ # @param query [String] The query or task for the agent to execute (required)
118
+ # @param params [Hash] Additional run parameters
119
+ # @option params [Hash] :input Input data for the agent run. Multi-word keys inside
120
+ # +input+, +data_sources+ items, and +metadata+ must be supplied in the exact shape
121
+ # the API expects — the parameter converter does not recurse into nested values
122
+ # (same contract as +output_schema+).
123
+ # @option params [Array<Hash>] :data_sources Data sources for the agent run
124
+ # @option params [Hash] :metadata Custom metadata
125
+ # @return [Resources::AgentRun] The newly created agent run
126
+ def agent_run_create(query:, **params)
127
+ Services::AgentRunCreate.new(connection, query: query, **params).call
128
+ end
129
+
130
+ # Stream an agent run, yielding chunks as they arrive
131
+ #
132
+ # @param query [String] The query or task for the agent to execute (required)
133
+ # @param params [Hash] Additional run parameters
134
+ # @option params [Hash] :input Input data for the agent run. Multi-word keys inside
135
+ # +input+, +data_sources+ items, and +metadata+ must be supplied in the exact shape
136
+ # the API expects — the parameter converter does not recurse into nested values
137
+ # (same contract as +output_schema+).
138
+ # @option params [Array<Hash>] :data_sources Data sources for the agent run
139
+ # @option params [Hash] :metadata Custom metadata
140
+ # @yield [chunk] Yields each streamed event chunk as it arrives
141
+ # @yieldparam chunk [Hash] Partial agent run event data
142
+ # @return [void]
143
+ def agent_run_stream(query:, **params, &block)
144
+ Services::AgentRunStream.new(connection, query: query, **params).call(&block)
145
+ end
146
+
147
+ # Get the status and results of an agent run
148
+ #
149
+ # @param run_id [String] Agent run ID
150
+ # @return [Resources::AgentRun] Agent run with current status and results
151
+ def agent_run_get(run_id)
152
+ Services::AgentRunGet.new(connection, run_id: run_id).call
153
+ end
154
+
155
+ # List all agent runs
156
+ #
157
+ # @param params [Hash] Listing parameters
158
+ # @option params [Integer] :limit Maximum number of runs to return
159
+ # @option params [String] :cursor Cursor for pagination
160
+ # @return [Resources::AgentRunList] List of agent runs
161
+ def agent_run_list(**params)
162
+ Services::AgentRunList.new(connection, **params).call
163
+ end
164
+
165
+ # Cancel an in-progress agent run
166
+ #
167
+ # @param run_id [String] Agent run ID
168
+ # @return [Resources::AgentRun] The cancelled agent run
169
+ def agent_run_cancel(run_id)
170
+ Services::AgentRunCancel.new(connection, run_id: run_id).call
171
+ end
172
+
173
+ # Delete an agent run
174
+ #
175
+ # @param run_id [String] Agent run ID
176
+ # @return [Resources::AgentRun] The deleted agent run
177
+ def agent_run_delete(run_id)
178
+ Services::AgentRunDelete.new(connection, run_id: run_id).call
179
+ end
180
+
181
+ # List events for an agent run
182
+ #
183
+ # @param run_id [String] Agent run ID
184
+ # @param params [Hash] Listing parameters
185
+ # @option params [Integer] :limit Maximum number of events to return
186
+ # @option params [String] :cursor Cursor for pagination
187
+ # @return [Array<Hash>] List of agent run events
188
+ def agent_run_events(run_id, **params)
189
+ Services::AgentRunEvents.new(connection, run_id: run_id, **params).call
190
+ end
191
+
115
192
  # Search code repositories
116
193
  #
117
194
  # @param query [String] Code search query
@@ -0,0 +1,54 @@
1
+ module Exa
2
+ module Resources
3
+ class AgentRun < Struct.new(
4
+ :id, :object, :status, :stop_reason, :created_at, :completed_at,
5
+ :request, :output, :usage, :cost_dollars, keyword_init: true
6
+ )
7
+ def initialize(id:, object:, status:, stop_reason: nil, created_at: nil, completed_at: nil, request: nil, output: nil, usage: nil, cost_dollars: nil)
8
+ super
9
+ freeze
10
+ end
11
+
12
+ # Build from a raw API response body (camelCase keys), e.g. a GET payload
13
+ # or the data of a terminal agent_run.* stream event.
14
+ def self.from_response(body)
15
+ new(
16
+ id: body["id"],
17
+ object: body["object"] || "agent_run",
18
+ status: body["status"],
19
+ stop_reason: body["stopReason"],
20
+ created_at: body["createdAt"],
21
+ completed_at: body["completedAt"],
22
+ request: body["request"],
23
+ output: body["output"],
24
+ usage: body["usage"],
25
+ cost_dollars: body["costDollars"]
26
+ )
27
+ end
28
+
29
+ def queued? = status == 'queued'
30
+ def running? = status == 'running'
31
+ def completed? = status == 'completed'
32
+ def failed? = status == 'failed'
33
+ def cancelled? = status == 'cancelled'
34
+
35
+ def finished? = completed? || failed? || cancelled?
36
+
37
+ def to_h
38
+ result = {
39
+ id: id,
40
+ object: object,
41
+ status: status
42
+ }
43
+ result[:stop_reason] = stop_reason if stop_reason
44
+ result[:created_at] = created_at if created_at
45
+ result[:completed_at] = completed_at if completed_at
46
+ result[:request] = request if request
47
+ result[:output] = output if output
48
+ result[:usage] = usage if usage
49
+ result[:cost_dollars] = cost_dollars if cost_dollars
50
+ result
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,21 @@
1
+ module Exa
2
+ module Resources
3
+ # Paginated collection of agent run events.
4
+ # Mirrors AgentRunList's data/has_more/next_cursor shape; event items are
5
+ # kept as plain hashes (the API returns {id, event, data, createdAt} per event).
6
+ class AgentRunEventList < Struct.new(:data, :has_more, :next_cursor, keyword_init: true)
7
+ def initialize(data:, has_more: false, next_cursor: nil)
8
+ super
9
+ freeze
10
+ end
11
+
12
+ def to_h
13
+ {
14
+ data: data.map { |item| item.respond_to?(:to_h) ? item.to_h : item },
15
+ has_more: has_more,
16
+ next_cursor: next_cursor
17
+ }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ module Exa
2
+ module Resources
3
+ class AgentRunList < Struct.new(:data, :has_more, :next_cursor, keyword_init: true)
4
+ def initialize(data:, has_more:, next_cursor: nil)
5
+ super
6
+ freeze
7
+ end
8
+
9
+ def to_h
10
+ {
11
+ data: data.map { |item| item.respond_to?(:to_h) ? item.to_h : item },
12
+ has_more: has_more,
13
+ next_cursor: next_cursor
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../resources/agent_run"
4
+
5
+ module Exa
6
+ module Services
7
+ class AgentRunCancel
8
+ def initialize(connection, run_id:)
9
+ @connection = connection
10
+ @run_id = run_id
11
+ end
12
+
13
+ def call
14
+ response = @connection.post("/agent/runs/#{@run_id}/cancel", {})
15
+ body = response.body
16
+
17
+ Resources::AgentRun.new(
18
+ id: body["id"],
19
+ object: body["object"],
20
+ status: body["status"],
21
+ stop_reason: body["stopReason"],
22
+ created_at: body["createdAt"],
23
+ completed_at: body["completedAt"],
24
+ request: body["request"],
25
+ output: body["output"],
26
+ usage: body["usage"],
27
+ cost_dollars: body["costDollars"]
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "parameter_converter"
4
+ require_relative "../resources/agent_run"
5
+
6
+ module Exa
7
+ module Services
8
+ class AgentRunCreate
9
+ def initialize(connection, query:, **params)
10
+ @connection = connection
11
+ @params = params.merge(query: query)
12
+ end
13
+
14
+ def call
15
+ response = @connection.post("/agent/runs", ParameterConverter.convert(@params))
16
+ body = response.body
17
+
18
+ Resources::AgentRun.new(
19
+ id: body["id"],
20
+ object: body["object"],
21
+ status: body["status"],
22
+ stop_reason: body["stopReason"],
23
+ created_at: body["createdAt"],
24
+ completed_at: body["completedAt"],
25
+ request: body["request"],
26
+ output: body["output"],
27
+ usage: body["usage"],
28
+ cost_dollars: body["costDollars"]
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end