antigravity-sdk 0.5.0 → 0.5.5

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: effdabdb0588788c4b168749a2578e2e1f07aa877a54bfd80fe1969d42f497d2
4
- data.tar.gz: 3debe5e4295f8c2e2bcd4c37ec7ea52ea11a77d034370320d4064b52aa08cc29
3
+ metadata.gz: fff0721cd596c0cafa8734522f4a88f26813dfe9afc71941389cb79bf109cfa4
4
+ data.tar.gz: 0f975b13b784c682519ee0f60e9f0cb574535e819b13e9224bdf3abb74fbc91f
5
5
  SHA512:
6
- metadata.gz: 4123cbc798ddbfaa82ea0f854e89adb122491d9a99b52f501c15350401e660109be3ca778b6142720f0259256992dbbb73d4d2371d8bdefcda266bd4d98591cf
7
- data.tar.gz: d80349e13c564c74bb81c5c8ba14887f39af36507d03b3c6a5883ed3a885359570210d10b9fc1f8cc3e8a0c193408de176f3316129a58e1d6303352c5527ed01
6
+ metadata.gz: fe0700f980ab6d196252151616ecb79181efa484ce824d5a65f63e75826536a9800f8a2f49450296a92aace284f1270e243115d1f90055098a95fce4a986c446
7
+ data.tar.gz: 0174bc0e5167c6d406d32f2f3e244e0f4860c2cb6e65237bbe9d591835f05ba8fcf1ab8d501f80e56fb1130c73f93b138f2d3b18c457d2f16e983f4d668bf79f
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.5.5
@@ -5,14 +5,14 @@ module Antigravity
5
5
 
6
6
  attr_accessor :model, :system_instruction, :api_key
7
7
  attr_reader :tools, :skills, :hooks, :sidecars, :client, :logger_guard,
8
- :workspace, :connection, :conversation
8
+ :workspace, :connection, :conversation, :policy, :policies, :born_at
9
9
 
10
10
  def initialize(model: nil, system_instruction: nil, tools: [],
11
11
  skills: [], policies: [], policy: nil, workspace: nil, auto_logger: true, log_file: nil, &block)
12
12
  @model = model || Antigravity.config.default_model
13
13
  @api_key = Antigravity.config.api_key
14
14
  @system_instruction = system_instruction
15
- @workspace = workspace ? File.expand_path(workspace) : nil
15
+ @workspace = resolve_workspace(workspace)
16
16
  @tools = tools.dup
17
17
  @skills = []
18
18
  @policies = []
@@ -23,6 +23,7 @@ module Antigravity
23
23
  @connection = nil
24
24
  @conversation = nil
25
25
  @connected = false
26
+ @born_at = Time.now
26
27
 
27
28
  # Register pre-provided tools into the tool runner
28
29
  @tool_runner = ToolRunner.new
@@ -68,6 +69,10 @@ module Antigravity
68
69
  end
69
70
  end
70
71
 
72
+ def workspace=(path)
73
+ @workspace = resolve_workspace(path)
74
+ end
75
+
71
76
  # --- Connection Lifecycle ---
72
77
 
73
78
  def connect!
@@ -89,9 +94,19 @@ module Antigravity
89
94
  )
90
95
 
91
96
  harness_config = build_harness_config
97
+
98
+ # Emit indexing hooks — workspace indexing happens during session init
99
+ index_t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) if @workspace
100
+ hooks.emit(:indexing_start, { workspace: @workspace }) if @workspace
101
+
92
102
  @conversation.initialize_session!(harness_config: harness_config)
93
103
  @connected = true
94
104
 
105
+ if @workspace
106
+ index_elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - index_t0
107
+ hooks.emit(:indexing_done, { workspace: @workspace, elapsed: index_elapsed.round(2) })
108
+ end
109
+
95
110
  # Emit session_start hook
96
111
  hooks.emit(:session_start, {
97
112
  model: @model,
@@ -160,6 +175,21 @@ module Antigravity
160
175
  @conversation.session_summary(model: @model)
161
176
  end
162
177
 
178
+ # Seconds since agent was created
179
+ def uptime
180
+ Time.now - @born_at
181
+ end
182
+
183
+ # Human-readable uptime: "1m 23.4s", "7.3s"
184
+ def uptime_human
185
+ secs = uptime
186
+ if secs >= 60
187
+ "#{(secs / 60).to_i}m #{(secs % 60).round(1)}s"
188
+ else
189
+ "#{secs.round(1)}s"
190
+ end
191
+ end
192
+
163
193
  # --- Tool Registration ---
164
194
 
165
195
  def register_tool(tool_or_name = nil, description: "", &block)
@@ -266,11 +296,11 @@ module Antigravity
266
296
  end
267
297
 
268
298
  def lifecycle_logger_enabled?
269
- # Explicit opt-in
270
- return true if ENV['ANTIGRAVITY_LIFECYCLE'] == '1'
299
+ # Explicit opt-in (strip handles trailing spaces in .env files)
300
+ return true if ENV['ANTIGRAVITY_LIFECYCLE']&.strip == '1'
271
301
  # Rails/Rack test or development
272
- return true if %w[test development].include?(ENV['RAILS_ENV']&.downcase)
273
- return true if %w[test development].include?(ENV['RACK_ENV']&.downcase)
302
+ return true if %w[test development].include?(ENV['RAILS_ENV']&.strip&.downcase)
303
+ return true if %w[test development].include?(ENV['RACK_ENV']&.strip&.downcase)
274
304
  # Explicit opt-out
275
305
  false
276
306
  end
@@ -361,8 +391,21 @@ module Antigravity
361
391
  skill
362
392
  end
363
393
 
364
- def lifecycle_logger_enabled?
365
- ENV['ANTIGRAVITY_LIFECYCLE'] == '1'
394
+ def resolve_workspace(val)
395
+ return nil if val.nil? || val == false
396
+
397
+ raw_path = case val
398
+ when :here, :current, true, '.'
399
+ '.'
400
+ else
401
+ val.to_s
402
+ end
403
+
404
+ expanded = File.expand_path(raw_path)
405
+ expanded += '/' unless expanded.end_with?('/')
406
+
407
+ $stderr.puts "📁 Setting workspace to \e[34m#{expanded}\e[0m" rescue nil
408
+ expanded
366
409
  end
367
410
  end
368
411
  end
@@ -87,7 +87,7 @@ module Antigravity
87
87
 
88
88
  unless wheel
89
89
  # Try all versions for the platform
90
- versions = data['releases']&.keys&.sort_by { |v| Gem::Version.new(v) rescue v }&.reverse
90
+ versions = data['releases']&.keys&.sort_by { |v| Gem::Version.correct?(v) ? Gem::Version.new(v) : Gem::Version.new('0') }&.reverse
91
91
  versions&.each do |ver|
92
92
  files = data.dig('releases', ver) || []
93
93
  wheel = files.find { |u| u['filename']&.include?(platform) && u['filename']&.end_with?('.whl') }
@@ -134,7 +134,11 @@ module Antigravity
134
134
  # Wheels are just ZIP files. Extract the binary.
135
135
  # Look for: google/antigravity/bin/localharness (or language_server)
136
136
  extract_dir = Dir.mktmpdir('agy-extract-')
137
- system('unzip', '-q', '-o', wheel_path, '-d', extract_dir)
137
+ success = system('unzip', '-q', '-o', wheel_path, '-d', extract_dir)
138
+ unless success
139
+ FileUtils.rm_rf(extract_dir)
140
+ raise HarnessNotFoundError, "Failed to extract wheel using unzip command"
141
+ end
138
142
 
139
143
  # Search for the binary inside (prefer localharness over language_server)
140
144
  candidates = Dir.glob("#{extract_dir}/**/localharness") +
@@ -121,8 +121,7 @@ module Antigravity
121
121
  end
122
122
 
123
123
  def perform_handshake!
124
- storage_dir = File.join(Dir.tmpdir, "antigravity-ruby-#{$$}")
125
- FileUtils.mkdir_p(storage_dir)
124
+ storage_dir = Dir.mktmpdir('antigravity-ruby-')
126
125
 
127
126
  # Send InputConfig via stdin
128
127
  input_config = Protocol.encode_input_config(
@@ -131,10 +131,9 @@ module Antigravity
131
131
 
132
132
  if step[:textDelta] && !step[:textDelta].empty? && is_model_step && is_target_user && !is_error_step
133
133
  text_parts << step[:textDelta]
134
- chunk = Message.new(
134
+ chunk = Chunk.new(
135
135
  content: step[:textDelta],
136
- role: :assistant,
137
- delta: true
136
+ role: :assistant
138
137
  )
139
138
  block&.call(chunk)
140
139
  end
@@ -170,37 +169,44 @@ module Antigravity
170
169
  handle_tool_call(tool_call)
171
170
  end
172
171
 
173
- # Usage update
172
+ # Usage update — do NOT set seen_any_step here!
173
+ # usageUpdate can leak from the previous turn and trick the GHI #18
174
+ # stale-FULLY_IDLE guard into accepting a stale FULLY_IDLE as real.
174
175
  if (usage = msg[:usageUpdate])
175
176
  update_usage(usage)
176
- seen_any_step = true
177
177
  end
178
178
 
179
179
  # Trajectory state: STATE_FULLY_IDLE / STATE_CANCELLED = turn complete (authoritative signal from harness)
180
- # GHI #18 FIX: Only honor FULLY_IDLE if we've seen at least one stepUpdate/toolCall/usageUpdate
181
- # from this turn, OR if enough time has elapsed (1s) that this can't be a stale leftover.
182
- # A stale FULLY_IDLE from a previous turn sitting in the WebSocket buffer was causing
183
- # collect_response to return immediately with zero steps/text.
180
+ # GHI #18 + #24 FIX: Only honor FULLY_IDLE if we've seen at least one stepUpdate or toolCall
181
+ # (NOT usageUpdate — it leaks across turns) from this turn, OR if enough time has elapsed
182
+ # (2s) that this can't be a stale leftover. A stale FULLY_IDLE from a previous turn sitting
183
+ # in the WebSocket buffer was causing collect_response to return immediately with 0B text.
184
184
  if (traj = msg[:trajectoryStateUpdate])
185
- elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - turn_started_at
186
185
  if traj[:state].to_s =~ /FULLY_IDLE|CANCELLED/
187
- if seen_any_step || elapsed > 1.0
186
+ if seen_any_step
188
187
  finished = true
189
188
  finished_at = Time.now
190
189
  else
191
190
  # Stale FULLY_IDLE — skip it (likely leftover from previous turn or init)
192
- @hooks&.emit(:ws_message, { _debug: 'skipped_stale_fully_idle', elapsed: elapsed.round(3) })
191
+ @hooks&.emit(:ws_message, { _debug: 'skipped_stale_fully_idle', seen_any_step: seen_any_step })
193
192
  end
194
193
  end
195
194
  end
196
195
 
197
196
  # Stop conditions (in priority order):
198
- # 1. Session end — always stop
199
- # 2. Model response DONE or trajectory FULLY_IDLE / CANCELLEDturn complete
200
- if msg.key?(:sessionEndResponse) || finished
197
+ # 1. Session end — always hard-stop immediately
198
+ # 2. Finished (DONE or FULLY_IDLE) WITH textshort drain for trailing usage
199
+ # 3. Finished WITHOUT text but with steps — model was working (thinking),
200
+ # text is likely still in-flight. Drain longer (3s) to catch it. (GHI #24)
201
+ # 4. Not finished but text started — idle timeout for more text
202
+ if msg.key?(:sessionEndResponse)
201
203
  :stop
204
+ elsif finished && !text_parts.empty?
205
+ [:idle_timeout, 0.5]
206
+ elsif finished && text_parts.empty?
207
+ # FULLY_IDLE arrived but no text yet — wait longer for trailing text
208
+ [:idle_timeout, 3.0]
202
209
  elsif !text_parts.empty?
203
- # If assistant has sent text response, allow 3s idle timeout for trailing metadata
204
210
  [:idle_timeout, 3.0]
205
211
  end
206
212
  end
@@ -241,10 +247,40 @@ module Antigravity
241
247
  # Symbolize keys for Ruby kwargs
242
248
  kwargs = args.transform_keys(&:to_sym)
243
249
 
244
- begin
245
- result = @tool_runner.execute(tool_name, **kwargs)
246
- rescue ToolNotFoundError => e
247
- result = { error: e.message }
250
+ # Emit tool_call hook BEFORE execution
251
+ @hooks&.emit(:tool_call, { tool_name: tool_name, params: args, tool_id: tool_id })
252
+
253
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
254
+ policy_check = @hooks ? @hooks.run_pre_tool(tool_name, args) : { allowed: true }
255
+
256
+ if !policy_check[:allowed]
257
+ reason = policy_check[:reason]
258
+ # Same format as client.rb sidecar emission
259
+ @hooks&.emit(:tool_blocked, { tool: tool_name, reason: reason })
260
+ result = "❌ TOOL BLOCKED: #{reason}"
261
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
262
+
263
+ # We run the post_tool hook here as well to satisfy AgentLogger's pairing
264
+ result = @hooks ? @hooks.run_post_tool(tool_name, args, result) : result
265
+ @hooks&.emit(:tool_result, { tool_name: tool_name, result: result.to_s, duration: duration, tool_id: tool_id })
266
+ else
267
+ begin
268
+ raw_result = @tool_runner.execute(tool_name, **kwargs)
269
+
270
+ # Run post_tool filters/maskers
271
+ result = @hooks ? @hooks.run_post_tool(tool_name, args, raw_result) : raw_result
272
+
273
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
274
+
275
+ # Emit tool_result hook AFTER execution
276
+ @hooks&.emit(:tool_result, { tool_name: tool_name, result: result.to_s, duration: duration, tool_id: tool_id })
277
+ rescue ToolNotFoundError => e
278
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
279
+ result = { error: e.message }
280
+
281
+ # Emit tool_error hook on failure
282
+ @hooks&.emit(:tool_error, { tool_name: tool_name, error: e.message, duration: duration, tool_id: tool_id })
283
+ end
248
284
  end
249
285
 
250
286
  # Send tool response back (protobuf InputEvent.tool_response format)
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Diagnostics module for Antigravity SDK.
4
+ # Probes environment, API key, harness binary, gems, and Gemini API models.
5
+ #
6
+ # Usage:
7
+ # Antigravity::Diagnostics.run! # Full colorful output
8
+ # Antigravity::Diagnostics.summary # Returns hash of all data
9
+ # Antigravity::Diagnostics.check_api_key # Just the key check
10
+ #
11
+ module Antigravity
12
+ module Diagnostics
13
+ C = Antigravity::Colors
14
+
15
+ # Main entry point — prints full colorful diagnostic report
16
+ def self.run!(verbose: false)
17
+ data = summary
18
+
19
+ puts ''
20
+ puts C.bold("💎 Antigravity SDK #{C.cyan("v#{data[:sdk_version]}")} — Diagnostics")
21
+ puts C.dim('=' * 56)
22
+
23
+ # Ruby
24
+ puts section('💻 Runtime')
25
+ puts field('Ruby', "#{data[:ruby_version]} (#{data[:ruby_platform]})")
26
+ puts field('Bundler', data[:bundler_version])
27
+ puts field('rv', data[:rv_version] || C.dim('not detected'))
28
+
29
+ # API Key
30
+ puts section('🔑 Authentication')
31
+ if data[:api_key_present]
32
+ masked = data[:api_key_prefix] + '****' + data[:api_key_suffix]
33
+ source = data[:api_key_source]
34
+ puts field('API Key', "#{C.green(masked)} (#{C.dim(source)})")
35
+ else
36
+ puts field('API Key', C.red('⚠️ NOT SET — export GEMINI_API_KEY'))
37
+ end
38
+
39
+ # Model
40
+ puts field('Model', data[:default_model])
41
+ if data[:model_source] == 'default'
42
+ puts C.dim(" └─ using SDK default (set GEMINI_MODEL or ANTIGRAVITY_MODEL to override)")
43
+ end
44
+
45
+ # Harness
46
+ puts section('📦 Harness')
47
+ if data[:harness_exists]
48
+ size_mb = (data[:harness_size] / 1_048_576.0).round(1)
49
+ mtime = data[:harness_mtime]&.strftime('%Y-%m-%d %H:%M') || '?'
50
+ puts field('Path', data[:harness_path])
51
+ puts field('Size', "#{size_mb}MB")
52
+ puts field('Modified', mtime)
53
+ puts field('Arch', data[:harness_arch] || C.dim('unknown'))
54
+ else
55
+ puts field('Path', C.red("⚠️ NOT FOUND at #{data[:harness_path]}"))
56
+ puts C.dim(" └─ run: just harness-fetch")
57
+ end
58
+
59
+ # Gems
60
+ puts section('📚 Dependencies')
61
+ data[:gems].each do |name, ver|
62
+ status = ver ? C.green(ver) : C.red('missing')
63
+ puts field(name, status)
64
+ end
65
+
66
+ # Timeouts
67
+ puts section('⏱️ Timeouts')
68
+ puts field('LLM', "#{data[:timeout_llm]}s")
69
+ puts field('WebSocket', "#{data[:timeout_ws]}s")
70
+ puts field('Handshake', "#{data[:timeout_handshake]}s")
71
+
72
+ # Gemini API models probe (optional, needs network)
73
+ if data[:api_key_present] && verbose
74
+ puts section('🤖 Gemini API Models')
75
+ models = probe_models(data[:api_key_raw])
76
+ if models
77
+ puts field('Available', "#{C.green(models[:count].to_s)} models")
78
+ flash = models[:names].select { |n| n.include?('flash') }.first(3)
79
+ pro = models[:names].select { |n| n.include?('pro') }.first(3)
80
+ puts field('Flash', flash.join(', ')) unless flash.empty?
81
+ puts field('Pro', pro.join(', ')) unless pro.empty?
82
+ else
83
+ puts field('Status', C.yellow('⚠️ Could not reach Gemini API'))
84
+ end
85
+ elsif data[:api_key_present]
86
+ puts C.dim("\n Tip: run with --verbose to probe Gemini API models")
87
+ end
88
+
89
+ # Overall status
90
+ puts ''
91
+ issues = health_check(data)
92
+ if issues.empty?
93
+ puts C.green('✅ All checks passed — ready to go!')
94
+ else
95
+ puts C.yellow("⚠️ #{issues.length} issue(s) found:")
96
+ issues.each { |i| puts C.yellow(" • #{i}") }
97
+ end
98
+ puts ''
99
+
100
+ data
101
+ end
102
+
103
+ # Returns a structured hash of all diagnostic data
104
+ def self.summary
105
+ config = Antigravity.config
106
+ api_key = config.api_key&.strip
107
+
108
+ # Detect API key source
109
+ api_key_source = if ENV['GEMINI_API_KEY'] && !ENV['GEMINI_API_KEY'].empty?
110
+ 'GEMINI_API_KEY'
111
+ else
112
+ '.env or config'
113
+ end
114
+
115
+ # Detect model source
116
+ model_source = if ENV['GEMINI_MODEL'] && !ENV['GEMINI_MODEL'].empty?
117
+ 'GEMINI_MODEL'
118
+ elsif ENV['ANTIGRAVITY_MODEL'] && !ENV['ANTIGRAVITY_MODEL'].empty?
119
+ 'ANTIGRAVITY_MODEL'
120
+ else
121
+ 'default'
122
+ end
123
+
124
+ # Harness binary info
125
+ harness_path = config.harness_path
126
+ harness_stat = File.stat(harness_path) rescue nil
127
+ harness_arch = detect_arch(harness_path) if harness_stat
128
+
129
+ # rv detection
130
+ rv_version = detect_rv
131
+
132
+ # Gem versions — stdlib gems need special detection
133
+ gem_names = %w[websocket dotenv json logger]
134
+ gems = gem_names.to_h do |g|
135
+ ver = Gem.loaded_specs[g]&.version&.to_s
136
+ # Stdlib gems (json, logger) may not appear in loaded_specs
137
+ ver ||= begin
138
+ old_verbose = $VERBOSE
139
+ $VERBOSE = nil
140
+ require g
141
+ $VERBOSE = old_verbose
142
+ defined?(Gem) ? Gem.loaded_specs[g]&.version&.to_s : nil
143
+ rescue LoadError
144
+ nil
145
+ end
146
+ # Last resort: check if the constant exists (stdlib bundled)
147
+ ver ||= '(stdlib)' if %w[json logger].include?(g)
148
+ [g, ver]
149
+ end
150
+
151
+ {
152
+ sdk_version: Antigravity::VERSION,
153
+ ruby_version: RUBY_VERSION,
154
+ ruby_platform: RUBY_PLATFORM,
155
+ bundler_version: Bundler::VERSION,
156
+ rv_version: rv_version,
157
+ api_key_present: api_key && !api_key.empty?,
158
+ api_key_prefix: api_key ? api_key[0..5] : '',
159
+ api_key_suffix: api_key ? api_key[-2..] : '',
160
+ api_key_source: api_key_source,
161
+ api_key_raw: api_key,
162
+ default_model: config.default_model,
163
+ model_source: model_source,
164
+ harness_path: harness_path,
165
+ harness_exists: !harness_stat.nil?,
166
+ harness_size: harness_stat&.size || 0,
167
+ harness_mtime: harness_stat&.mtime,
168
+ harness_arch: harness_arch,
169
+ timeout_llm: config.timeout_llm,
170
+ timeout_ws: config.timeout_ws,
171
+ timeout_handshake: config.timeout_handshake,
172
+ gems: gems,
173
+ }
174
+ end
175
+
176
+ # Quick health check — returns array of issue strings
177
+ def self.health_check(data = nil)
178
+ data ||= summary
179
+ issues = []
180
+ issues << 'GEMINI_API_KEY is not set' unless data[:api_key_present]
181
+ issues << "Harness binary not found at #{data[:harness_path]}" unless data[:harness_exists]
182
+ issues << 'websocket gem not loaded' unless data.dig(:gems, 'websocket')
183
+ issues
184
+ end
185
+
186
+ private
187
+
188
+ def self.section(title)
189
+ "\n#{C.bold(title)}"
190
+ end
191
+
192
+ def self.field(label, value)
193
+ " #{C.cyan(label.to_s.ljust(12))} #{value}"
194
+ end
195
+
196
+ def self.detect_arch(path)
197
+ return nil unless File.exist?(path)
198
+ output = `file '#{path}' 2>/dev/null`.strip
199
+ case output
200
+ when /arm64/ then 'arm64 (Apple Silicon)'
201
+ when /x86_64/ then 'x86_64 (Intel)'
202
+ when /ELF.*64/ then 'linux-amd64'
203
+ else output.split(':').last&.strip&.slice(0, 40)
204
+ end
205
+ rescue
206
+ nil
207
+ end
208
+
209
+ def self.detect_rv
210
+ `rv --version 2>/dev/null`.strip.then { |v| v.empty? ? nil : v }
211
+ rescue
212
+ nil
213
+ end
214
+
215
+ def self.probe_models(api_key)
216
+ require 'net/http'
217
+ require 'json'
218
+ uri = URI("https://generativelanguage.googleapis.com/v1beta/models?key=#{api_key}")
219
+ resp = Net::HTTP.get_response(uri)
220
+ return nil unless resp.is_a?(Net::HTTPSuccess)
221
+ body = JSON.parse(resp.body)
222
+ models = body['models'] || []
223
+ names = models.map { |m| m['name'].to_s.sub('models/', '') }.sort
224
+ { count: names.length, names: names }
225
+ rescue
226
+ nil
227
+ end
228
+ end
229
+ end
@@ -22,8 +22,7 @@ module Antigravity
22
22
  return true
23
23
  end
24
24
 
25
- cmd = "#{@bin_path} --port=#{@port}"
26
- @stdin, @stdout, @stderr, wait_thr = Open3.popen3(cmd)
25
+ @stdin, @stdout, @stderr, wait_thr = Open3.popen3(@bin_path, "--port=#{@port}")
27
26
  @pid = wait_thr.pid
28
27
 
29
28
  at_exit { stop! }
@@ -20,8 +20,9 @@ module Antigravity
20
20
  def self.status_line(agent)
21
21
  turns = agent.turn_count rescue 0
22
22
  summary = agent.session_summary rescue {}
23
- tokens = summary.dig(:tokens, :total) || 0
23
+ tokens = summary[:total_tokens] || 0
24
24
  tok_str = tokens > 999 ? "#{(tokens / 1000.0).round(1)}k" : tokens.to_s
25
+ tok_str = "🪙#{tok_str}"
25
26
  model = summary[:model] || agent.model || '?'
26
27
  conv_id = (summary[:conversation_id] || '?')[0..7]
27
28
 
@@ -54,7 +55,18 @@ module Antigravity
54
55
  logger.instance_variable_set(:@session_start_time, Process.clock_gettime(Process::CLOCK_MONOTONIC))
55
56
  model = info[:model] || agent.model || '?'
56
57
  conv_id = (info[:conversation_id] || '?')[0..11]
57
- puts C.gray("🟢 #{C.dim("session_start")} | model=#{C.cyan(model)} | conv=#{C.cyan(conv_id)}")
58
+ puts C.gray("\n🪝🟢 #{C.dim("session_start")} | model=#{C.cyan(model)} | conv=#{C.cyan(conv_id)}")
59
+ end
60
+
61
+ agent.hooks.on(:indexing_start) do |info|
62
+ ws = info[:workspace] || '?'
63
+ puts C.gray("🪝 📂 #{C.dim("indexing")} | #{C.blue(ws)}")
64
+ end
65
+
66
+ agent.hooks.on(:indexing_done) do |info|
67
+ ws = info[:workspace] || '?'
68
+ elapsed = info[:elapsed] ? "#{info[:elapsed]}s" : '?'
69
+ puts C.gray("🪝 ✅ #{C.dim("indexed")} | #{C.blue(ws)} | #{C.green(elapsed)}")
58
70
  end
59
71
 
60
72
  agent.hooks.on(:session_end) do |info|
@@ -65,9 +77,9 @@ module Antigravity
65
77
  end
66
78
  turns = info[:turn_count] || agent.turn_count rescue 0
67
79
  summary = agent.session_summary rescue {}
68
- tokens = summary.dig(:tokens, :total) || 0
80
+ tokens = summary[:total_tokens] || 0
69
81
  tok_str = tokens > 999 ? "#{(tokens / 1000.0).round(1)}k" : tokens.to_s
70
- puts C.gray("🔴 #{C.dim("session_end")} | #{C.bold("#{turns} turns")} | #{tok_str} tok | #{elapsed}s")
82
+ puts C.gray("🪝🔴 #{C.dim("session_end")} | #{C.bold("#{turns} turns")} | 🪙#{tok_str} | #{elapsed}s")
71
83
  end
72
84
  end
73
85
 
@@ -81,7 +93,7 @@ module Antigravity
81
93
  preview = text.to_s[0..60].gsub("\n", ' ')
82
94
  preview += '...' if text.to_s.length > 60
83
95
  status = self.class.status_line(agent) rescue C.dim("T#{count}")
84
- puts C.gray(" ➡️ #{C.dim("pre_turn")} T#{count} | #{C.yellow("\"#{preview}\"")} | #{status}")
96
+ puts C.gray("🪝 ➡️ #{C.dim("pre_turn")} T#{count} | #{C.yellow("\"#{preview}\"")} | #{status}")
85
97
  end
86
98
 
87
99
  agent.hooks.after_response do |response|
@@ -99,12 +111,12 @@ module Antigravity
99
111
  tool_count = response.respond_to?(:tool_calls_count) ? (response.tool_calls_count || 0) : 0
100
112
  status = self.class.status_line(agent) rescue ''
101
113
 
102
- parts = ["#{C.green("#{chars}ch")} #{C.dim("#{lines}L")}"]
103
- parts << "#{C.magenta("#{thinking_len}ch")} think" if thinking_len > 0
114
+ parts = ["#{C.green("#{chars}B")} #{C.dim("#{lines}L")}"]
115
+ parts << "#{C.magenta("#{thinking_len}B")} think" if thinking_len > 0
104
116
  parts << "#{C.cyan("#{tool_count}")} tools" if tool_count > 0
105
117
  parts << "#{C.blue("#{elapsed}s")}"
106
118
 
107
- puts C.gray(" ⬅️ #{C.dim("post_turn")} T#{logger.instance_variable_get(:@turn_count)} | #{parts.join(' | ')} | #{status}")
119
+ puts C.gray("\n🪝 ⬅️ #{C.dim("post_turn")} T#{logger.instance_variable_get(:@turn_count)} | #{parts.join(' | ')} | #{status}")
108
120
  if logger.instance_variable_get(:@verbose)
109
121
  puts C.dim(" \"#{preview}\"")
110
122
  end
@@ -117,28 +129,32 @@ module Antigravity
117
129
  agent.hooks.on(:tool_call) do |info|
118
130
  name = info[:tool_name] || info[:name] || '?'
119
131
  params_preview = (info[:params] || {}).keys.join(', ')
120
- puts C.gray(" 🔧 #{C.dim("tool_call")} | #{C.cyan(name)}(#{C.dim(params_preview)})")
132
+ puts C.gray(" 🪝 🔧 #{C.dim("tool_call")} | #{C.cyan(name)}(#{C.dim(params_preview)})")
121
133
  end
122
134
 
123
135
  agent.hooks.on(:tool_result) do |info|
124
136
  name = info[:tool_name] || info[:name] || '?'
125
- result_len = info[:result].to_s.length rescue 0
137
+ result_str = info[:result].to_s
138
+ result_len = result_str.bytesize rescue 0
139
+ preview = result_str[0..120].gsub("\n", ' ')
140
+ preview += '...' if result_str.length > 120
126
141
  duration = info[:duration] ? "#{info[:duration].round(2)}s" : nil
127
- parts = [C.cyan(name), "#{result_len}ch"]
142
+ parts = [C.cyan(name), "#{result_len}B"]
128
143
  parts << duration if duration
129
- puts C.gray(" ✅ #{C.dim("tool_done")} | #{parts.join(' | ')}")
144
+ puts C.gray(" 🪝 ✅ #{C.dim("tool_done")} | #{parts.join(' | ')}")
145
+ puts C.dim(" → #{C.yellow(preview)}") if result_len > 0
130
146
  end
131
147
 
132
148
  agent.hooks.on(:tool_blocked) do |info|
133
149
  name = info[:tool] || '?'
134
150
  reason = info[:reason] || 'policy'
135
- puts C.red(" 🚫 #{C.dim("tool_deny")} | #{C.red(name)} — #{reason}")
151
+ puts C.red(" 🪝 🚫 #{C.dim("tool_deny")} | #{C.red(name)} — #{reason}")
136
152
  end
137
153
 
138
154
  agent.hooks.on(:tool_error) do |info|
139
155
  name = info[:tool_name] || info[:name] || '?'
140
156
  error = info[:error] || info[:message] || '?'
141
- puts C.red(" 💥 #{C.dim("tool_error")} | #{C.red(name)} — #{error.to_s[0..80]}")
157
+ puts C.red(" 🪝 💥 #{C.dim("tool_error")} | #{C.red(name)} — #{error.to_s[0..80]}")
142
158
  end
143
159
  end
144
160
  end
@@ -71,7 +71,11 @@ module Antigravity
71
71
 
72
72
  content = File.read(skill_file, encoding: 'UTF-8')
73
73
  if content =~ /\A(---\s*\n.*?\n?)^(---\s*$\n?)/m
74
- front_matter = YAML.safe_load(Regexp.last_match(1)) || {}
74
+ front_matter = begin
75
+ YAML.safe_load(Regexp.last_match(1)) || {}
76
+ rescue Psych::SyntaxError => e
77
+ raise ArgumentError, "Invalid YAML frontmatter in #{skill_file}: #{e.message}"
78
+ end
75
79
  @name = front_matter["name"] || File.basename(@path)
76
80
  @description = front_matter["description"] || ""
77
81
  @metadata = front_matter.fetch("metadata", {})
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Antigravity
4
- VERSION = File.read(File.expand_path("../../VERSION", __dir__)).strip rescue "0.1.0"
4
+ VERSION = begin
5
+ version_file = [
6
+ File.expand_path("../../VERSION", __dir__),
7
+ File.expand_path("../VERSION", __dir__)
8
+ ].find { |f| File.exist?(f) }
9
+ version_file ? File.read(version_file).strip : "0.5.0"
10
+ end
5
11
  end
data/lib/antigravity.rb CHANGED
@@ -29,6 +29,7 @@ require_relative "antigravity/connection/local_connection"
29
29
  require_relative "antigravity/conversation"
30
30
  require_relative "antigravity/colors"
31
31
  require_relative "antigravity/lifecycle_logger"
32
+ require_relative "antigravity/diagnostics"
32
33
  require_relative "antigravity/agent"
33
34
 
34
35
  module Antigravity
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: antigravity-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Riccardo Carlesso
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '1.5'
54
+ - !ruby/object:Gem::Dependency
55
+ name: dotenv
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3.0'
54
68
  description: An elegant, Ruby-like SDK for building autonomous AI agents with Google
55
69
  Antigravity.
56
70
  email:
@@ -59,6 +73,7 @@ executables: []
59
73
  extensions: []
60
74
  extra_rdoc_files: []
61
75
  files:
76
+ - VERSION
62
77
  - lib/antigravity.rb
63
78
  - lib/antigravity/agent.rb
64
79
  - lib/antigravity/base.rb
@@ -69,6 +84,7 @@ files:
69
84
  - lib/antigravity/connection/local_connection.rb
70
85
  - lib/antigravity/connection/websocket_client.rb
71
86
  - lib/antigravity/conversation.rb
87
+ - lib/antigravity/diagnostics.rb
72
88
  - lib/antigravity/emojis.rb
73
89
  - lib/antigravity/errors.rb
74
90
  - lib/antigravity/guards.rb