antigravity-sdk 0.3.0 → 0.4.2
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 +4 -4
- data/lib/antigravity/agent.rb +234 -22
- data/lib/antigravity/config.rb +12 -1
- data/lib/antigravity/connection/binary_fetcher.rb +163 -0
- data/lib/antigravity/connection/local_connection.rb +184 -0
- data/lib/antigravity/connection/websocket_client.rb +159 -0
- data/lib/antigravity/conversation.rb +302 -0
- data/lib/antigravity/emojis.rb +4 -0
- data/lib/antigravity/errors.rb +27 -0
- data/lib/antigravity/guards.rb +82 -22
- data/lib/antigravity/hooks.rb +12 -0
- data/lib/antigravity/message.rb +19 -5
- data/lib/antigravity/protocol.rb +193 -0
- data/lib/antigravity/skill.rb +53 -5
- data/lib/antigravity/skill_resolver.rb +140 -0
- data/lib/antigravity/tool.rb +33 -6
- data/lib/antigravity/tool_runner.rb +59 -0
- data/lib/antigravity.rb +8 -0
- metadata +12 -4
data/lib/antigravity/message.rb
CHANGED
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Antigravity
|
|
4
|
+
# Represents a response message from the agent.
|
|
5
|
+
# Mirrors Python SDK's ChatResponse with metadata.
|
|
4
6
|
class Message < Base
|
|
7
|
+
attr_accessor :role, :content, :thinking, :tool_calls, :model_id, :tokens,
|
|
8
|
+
:steps, :tool_calls_count, :usage, :delta
|
|
5
9
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
def initialize(role: :assistant, content: '', thinking: '', tool_calls: [],
|
|
11
|
+
model_id: nil, steps: [], tool_calls_count: 0, usage: nil,
|
|
12
|
+
delta: false)
|
|
9
13
|
@role = role
|
|
10
14
|
@content = content
|
|
11
15
|
@thinking = thinking
|
|
12
16
|
@tool_calls = tool_calls
|
|
13
17
|
@model_id = model_id
|
|
14
18
|
@tokens = { input: 0, output: 0 }
|
|
19
|
+
@steps = steps
|
|
20
|
+
@tool_calls_count = tool_calls_count
|
|
21
|
+
@usage = usage || { prompt_token_count: 0, candidates_token_count: 0, total_token_count: 0 }
|
|
22
|
+
@delta = delta
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Is this a streaming delta (partial) or a complete response?
|
|
26
|
+
def delta?
|
|
27
|
+
@delta
|
|
15
28
|
end
|
|
16
29
|
end
|
|
17
30
|
|
|
31
|
+
# Backward-compatible alias for streaming chunks
|
|
18
32
|
class Chunk < Message
|
|
19
|
-
def initialize(
|
|
20
|
-
super(
|
|
33
|
+
def initialize(**kwargs)
|
|
34
|
+
super(**kwargs, delta: true)
|
|
21
35
|
end
|
|
22
36
|
end
|
|
23
37
|
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'version'
|
|
4
|
+
|
|
5
|
+
module Antigravity
|
|
6
|
+
# Hand-rolled protobuf encoding/decoding for the 2 stdio handshake messages:
|
|
7
|
+
# InputConfig (SDK → localharness via stdin)
|
|
8
|
+
# OutputConfig (localharness → SDK via stdout)
|
|
9
|
+
#
|
|
10
|
+
# Uses raw protobuf wire format to avoid the google-protobuf gem dependency.
|
|
11
|
+
# See: https://github.com/palladius/antigravity-ruby-sdk/issues/7
|
|
12
|
+
#
|
|
13
|
+
# Wire format reference: https://protobuf.dev/programming-guides/encoding/
|
|
14
|
+
# Varint: wire type 0, tag = (field_number << 3) | 0
|
|
15
|
+
# Length-delimited: wire type 2, tag = (field_number << 3) | 2
|
|
16
|
+
class Protocol
|
|
17
|
+
# --- Encoding: InputConfig ---
|
|
18
|
+
# message InputConfig {
|
|
19
|
+
# string storage_directory = 1;
|
|
20
|
+
# uint32 port = 2; # optional, we leave 0
|
|
21
|
+
# string bind_address = 3; # default "localhost"
|
|
22
|
+
# ClientInfo client_info = 4;
|
|
23
|
+
# map<string, string> env = 5;
|
|
24
|
+
# }
|
|
25
|
+
# message ClientInfo {
|
|
26
|
+
# string language = 1;
|
|
27
|
+
# string version = 2;
|
|
28
|
+
# string language_version = 3;
|
|
29
|
+
# string os = 4;
|
|
30
|
+
# string os_version = 5;
|
|
31
|
+
# }
|
|
32
|
+
def self.encode_input_config(storage_directory:, bind_address: 'localhost', env: {})
|
|
33
|
+
buf = ''.b
|
|
34
|
+
|
|
35
|
+
# field 1: storage_directory (string)
|
|
36
|
+
buf << encode_string_field(1, storage_directory)
|
|
37
|
+
|
|
38
|
+
# field 3: bind_address (string, default "localhost")
|
|
39
|
+
buf << encode_string_field(3, bind_address)
|
|
40
|
+
|
|
41
|
+
# field 4: client_info (embedded message)
|
|
42
|
+
client_info = encode_client_info
|
|
43
|
+
buf << encode_bytes_field(4, client_info)
|
|
44
|
+
|
|
45
|
+
# field 5: env map entries (each is an embedded message with key=1, value=2)
|
|
46
|
+
env.each do |key, value|
|
|
47
|
+
entry = encode_string_field(1, key.to_s) + encode_string_field(2, value.to_s)
|
|
48
|
+
buf << encode_bytes_field(5, entry)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Length-prefix: 4-byte little-endian uint32
|
|
52
|
+
[buf.bytesize].pack('V') + buf
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# --- Decoding: OutputConfig ---
|
|
56
|
+
# message OutputConfig {
|
|
57
|
+
# int32 port = 1;
|
|
58
|
+
# string api_key = 2;
|
|
59
|
+
# }
|
|
60
|
+
def self.decode_output_config(data)
|
|
61
|
+
raise ProtocolError, 'Data too short for length prefix' if data.bytesize < 4
|
|
62
|
+
|
|
63
|
+
declared_len = data[0..3].unpack1('V')
|
|
64
|
+
payload = data[4..]
|
|
65
|
+
|
|
66
|
+
raise ProtocolError, "Truncated payload: expected #{declared_len}, got #{payload&.bytesize || 0}" if payload.nil? || payload.bytesize < declared_len
|
|
67
|
+
|
|
68
|
+
result = { port: 0, api_key: '' }
|
|
69
|
+
pos = 0
|
|
70
|
+
|
|
71
|
+
while pos < declared_len
|
|
72
|
+
tag_byte, new_pos = decode_varint(payload, pos)
|
|
73
|
+
pos = new_pos
|
|
74
|
+
field_number = tag_byte >> 3
|
|
75
|
+
wire_type = tag_byte & 0x07
|
|
76
|
+
|
|
77
|
+
case wire_type
|
|
78
|
+
when 0 # varint
|
|
79
|
+
value, pos = decode_varint(payload, pos)
|
|
80
|
+
result[:port] = value if field_number == 1
|
|
81
|
+
when 2 # length-delimited
|
|
82
|
+
length, pos = decode_varint(payload, pos)
|
|
83
|
+
value = payload[pos, length]
|
|
84
|
+
pos += length
|
|
85
|
+
result[:api_key] = value.force_encoding('UTF-8') if field_number == 2
|
|
86
|
+
else
|
|
87
|
+
raise ProtocolError, "Unsupported wire type #{wire_type} at position #{pos}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
result
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Read a length-prefixed frame from an IO (blocking).
|
|
95
|
+
# Returns the raw payload bytes.
|
|
96
|
+
def self.read_length_prefixed(io, timeout: 10)
|
|
97
|
+
len_bytes = read_exactly(io, 4, timeout: timeout)
|
|
98
|
+
raise ProtocolError, 'EOF reading length prefix' unless len_bytes&.bytesize == 4
|
|
99
|
+
|
|
100
|
+
payload_len = len_bytes.unpack1('V')
|
|
101
|
+
raise ProtocolError, "Unreasonable payload length: #{payload_len}" if payload_len > 1_000_000
|
|
102
|
+
|
|
103
|
+
frame = [payload_len].pack('V') + read_exactly(io, payload_len, timeout: timeout)
|
|
104
|
+
frame
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
class << self
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def encode_client_info
|
|
111
|
+
buf = ''.b
|
|
112
|
+
buf << encode_string_field(1, 'ruby')
|
|
113
|
+
buf << encode_string_field(2, Antigravity::VERSION)
|
|
114
|
+
buf << encode_string_field(3, RUBY_VERSION)
|
|
115
|
+
buf << encode_string_field(4, ruby_platform_os)
|
|
116
|
+
buf << encode_string_field(5, RUBY_PLATFORM)
|
|
117
|
+
buf
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def ruby_platform_os
|
|
121
|
+
case RUBY_PLATFORM
|
|
122
|
+
when /darwin/i then 'macos'
|
|
123
|
+
when /linux/i then 'linux'
|
|
124
|
+
when /win/i then 'windows'
|
|
125
|
+
else RUBY_PLATFORM
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Encode a string field: tag + varint length + UTF-8 bytes
|
|
130
|
+
def encode_string_field(field_number, value)
|
|
131
|
+
encode_bytes_field(field_number, value.to_s.encode('UTF-8').b)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Encode a length-delimited field: tag + varint length + raw bytes
|
|
135
|
+
def encode_bytes_field(field_number, bytes)
|
|
136
|
+
tag = (field_number << 3) | 2 # wire type 2 = length-delimited
|
|
137
|
+
encode_varint_bytes(tag) + encode_varint_bytes(bytes.bytesize) + bytes
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Encode an integer as varint bytes
|
|
141
|
+
def encode_varint_bytes(value)
|
|
142
|
+
bytes = []
|
|
143
|
+
loop do
|
|
144
|
+
byte = value & 0x7F
|
|
145
|
+
value >>= 7
|
|
146
|
+
byte |= 0x80 if value > 0
|
|
147
|
+
bytes << byte
|
|
148
|
+
break if value == 0
|
|
149
|
+
end
|
|
150
|
+
bytes.pack('C*')
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Decode a varint starting at position, returns [value, new_position]
|
|
154
|
+
def decode_varint(data, pos)
|
|
155
|
+
result = 0
|
|
156
|
+
shift = 0
|
|
157
|
+
loop do
|
|
158
|
+
raise ProtocolError, "Varint extends past end of data at pos #{pos}" if pos >= data.bytesize
|
|
159
|
+
|
|
160
|
+
byte = data.getbyte(pos)
|
|
161
|
+
pos += 1
|
|
162
|
+
result |= (byte & 0x7F) << shift
|
|
163
|
+
break if (byte & 0x80) == 0
|
|
164
|
+
|
|
165
|
+
shift += 7
|
|
166
|
+
raise ProtocolError, 'Varint too long' if shift >= 64
|
|
167
|
+
end
|
|
168
|
+
[result, pos]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Read exactly n bytes from IO with timeout
|
|
172
|
+
def read_exactly(io, n, timeout: 10)
|
|
173
|
+
buf = ''.b
|
|
174
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
175
|
+
while buf.bytesize < n
|
|
176
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
177
|
+
raise ProtocolError, "Timeout reading #{n} bytes from harness" if remaining <= 0
|
|
178
|
+
|
|
179
|
+
ready = IO.select([io], nil, nil, [remaining, 0.1].min)
|
|
180
|
+
if ready
|
|
181
|
+
chunk = io.read_nonblock(n - buf.bytesize, exception: false)
|
|
182
|
+
case chunk
|
|
183
|
+
when String then buf << chunk
|
|
184
|
+
when :wait_readable then next
|
|
185
|
+
when nil then raise ProtocolError, 'EOF while reading from harness'
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
buf
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
data/lib/antigravity/skill.rb
CHANGED
|
@@ -3,30 +3,78 @@
|
|
|
3
3
|
require "yaml"
|
|
4
4
|
|
|
5
5
|
module Antigravity
|
|
6
|
+
# Represents an Agent Skill loaded from a SKILL.md file or defined inline.
|
|
7
|
+
# Spec: https://agentskills.io/specification
|
|
8
|
+
#
|
|
9
|
+
# A skill directory must contain a SKILL.md with YAML frontmatter:
|
|
10
|
+
# ---
|
|
11
|
+
# name: my-skill
|
|
12
|
+
# description: What this skill does
|
|
13
|
+
# ---
|
|
14
|
+
# # Instructions in markdown...
|
|
6
15
|
class Skill < Base
|
|
7
16
|
|
|
8
|
-
attr_reader :name, :description, :instructions, :path
|
|
17
|
+
attr_reader :name, :description, :instructions, :path, :metadata
|
|
9
18
|
|
|
19
|
+
# Load a skill from a directory containing SKILL.md.
|
|
20
|
+
# @param path [String] path to skill directory
|
|
10
21
|
def initialize(path)
|
|
11
22
|
@path = File.expand_path(path)
|
|
23
|
+
@metadata = {}
|
|
12
24
|
parse_skill_file
|
|
13
25
|
end
|
|
14
26
|
|
|
27
|
+
# Factory: load from directory path.
|
|
15
28
|
def self.load(path)
|
|
16
29
|
new(path)
|
|
17
30
|
end
|
|
18
31
|
|
|
32
|
+
# Factory: create an inline skill (no file needed).
|
|
33
|
+
# @param name [String] skill name (lowercase, hyphenated)
|
|
34
|
+
# @param description [String] what the skill does
|
|
35
|
+
# @param instructions [String] the skill instructions (markdown)
|
|
36
|
+
# @return [Skill] an inline skill instance
|
|
37
|
+
def self.inline(name:, description:, instructions:)
|
|
38
|
+
skill = allocate
|
|
39
|
+
skill.send(:init_inline, name: name, description: description, instructions: instructions)
|
|
40
|
+
skill
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Check if a directory is a valid skill (contains SKILL.md).
|
|
44
|
+
# @param path [String] directory path to check
|
|
45
|
+
# @return [Boolean]
|
|
46
|
+
def self.skill_dir?(path)
|
|
47
|
+
SkillResolver.skill_dir?(path)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_s
|
|
51
|
+
"#<Skill name=#{@name.inspect} path=#{@path.inspect}>"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def inspect
|
|
55
|
+
"#<Antigravity::Skill name=#{@name.inspect} description=#{@description.inspect} path=#{@path.inspect}>"
|
|
56
|
+
end
|
|
57
|
+
|
|
19
58
|
private
|
|
20
59
|
|
|
60
|
+
def init_inline(name:, description:, instructions:)
|
|
61
|
+
@name = name
|
|
62
|
+
@description = description
|
|
63
|
+
@instructions = instructions
|
|
64
|
+
@path = nil # inline skills have no path
|
|
65
|
+
@metadata = {}
|
|
66
|
+
end
|
|
67
|
+
|
|
21
68
|
def parse_skill_file
|
|
22
69
|
skill_file = File.join(@path, "SKILL.md")
|
|
23
70
|
raise ArgumentError, "SKILL.md not found at #{@path}" unless File.exist?(skill_file)
|
|
24
71
|
|
|
25
|
-
content = File.read(skill_file)
|
|
72
|
+
content = File.read(skill_file, encoding: 'UTF-8')
|
|
26
73
|
if content =~ /\A(---\s*\n.*?\n?)^(---\s*$\n?)/m
|
|
27
|
-
front_matter = YAML.safe_load(Regexp.last_match(1))
|
|
28
|
-
@name = front_matter["name"]
|
|
29
|
-
@description = front_matter["description"]
|
|
74
|
+
front_matter = YAML.safe_load(Regexp.last_match(1)) || {}
|
|
75
|
+
@name = front_matter["name"] || File.basename(@path)
|
|
76
|
+
@description = front_matter["description"] || ""
|
|
77
|
+
@metadata = front_matter.fetch("metadata", {})
|
|
30
78
|
@instructions = Regexp.last_match.post_match.strip
|
|
31
79
|
else
|
|
32
80
|
@name = File.basename(@path)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Antigravity
|
|
6
|
+
# Resolves skill paths from various inputs: single skill dir, container dir,
|
|
7
|
+
# or GitHub URL. Returns expanded absolute paths to skill directories.
|
|
8
|
+
#
|
|
9
|
+
# Resolution logic:
|
|
10
|
+
# 1. Path contains SKILL.md -> single skill
|
|
11
|
+
# 2. Path has a skills/ subfolder -> discover children with SKILL.md
|
|
12
|
+
# 3. Path children have SKILL.md -> flat container discovery
|
|
13
|
+
# 4. GitHub URL -> clone/cache then resolve locally
|
|
14
|
+
#
|
|
15
|
+
# Spec compliance: https://agentskills.io/specification
|
|
16
|
+
class SkillResolver
|
|
17
|
+
GITHUB_URL_PATTERN = %r{\Ahttps?://github\.com/}i
|
|
18
|
+
CACHE_DIR = File.expand_path('~/.antigravity/cache/ruby-sdk/skills')
|
|
19
|
+
|
|
20
|
+
# Resolve a path or URL to an array of valid skill directory paths.
|
|
21
|
+
# @param path_or_url [String] local path or GitHub URL
|
|
22
|
+
# @return [Array<String>] expanded absolute paths to skill directories
|
|
23
|
+
def self.resolve(path_or_url)
|
|
24
|
+
path_or_url = path_or_url.to_s.strip
|
|
25
|
+
|
|
26
|
+
if github_url?(path_or_url)
|
|
27
|
+
resolve_github(path_or_url)
|
|
28
|
+
else
|
|
29
|
+
resolve_local(path_or_url)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Discover all skill directories inside a container path.
|
|
34
|
+
# Checks for skills/ subfolder first, then scans children directly.
|
|
35
|
+
# @param container_path [String] path to scan for skills
|
|
36
|
+
# @return [Array<String>] sorted list of skill directory paths
|
|
37
|
+
def self.discover(container_path)
|
|
38
|
+
expanded = File.expand_path(container_path)
|
|
39
|
+
return [] unless File.directory?(expanded)
|
|
40
|
+
|
|
41
|
+
# Check for skills/ subfolder first (convention from agentskills.io)
|
|
42
|
+
skills_subdir = File.join(expanded, 'skills')
|
|
43
|
+
scan_dir = File.directory?(skills_subdir) ? skills_subdir : expanded
|
|
44
|
+
|
|
45
|
+
Dir.children(scan_dir)
|
|
46
|
+
.map { |child| File.join(scan_dir, child) }
|
|
47
|
+
.select { |child_path| skill_dir?(child_path) }
|
|
48
|
+
.sort
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Check if a directory is a valid skill (contains SKILL.md).
|
|
52
|
+
# @param path [String] directory path to check
|
|
53
|
+
# @return [Boolean]
|
|
54
|
+
def self.skill_dir?(path)
|
|
55
|
+
File.directory?(path) && File.exist?(File.join(path, 'SKILL.md'))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Check if a string looks like a GitHub URL.
|
|
59
|
+
# @param str [String]
|
|
60
|
+
# @return [Boolean]
|
|
61
|
+
def self.github_url?(str)
|
|
62
|
+
str.match?(GITHUB_URL_PATTERN)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class << self
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def resolve_local(path)
|
|
69
|
+
expanded = File.expand_path(path)
|
|
70
|
+
raise ArgumentError, "Skill path does not exist: #{expanded}" unless File.exist?(expanded)
|
|
71
|
+
raise ArgumentError, "Skill path is not a directory: #{expanded}" unless File.directory?(expanded)
|
|
72
|
+
|
|
73
|
+
# Case 1: Direct skill dir (has SKILL.md)
|
|
74
|
+
return [expanded] if skill_dir?(expanded)
|
|
75
|
+
|
|
76
|
+
# Case 2 & 3: Container dir -- discover children
|
|
77
|
+
discovered = discover(expanded)
|
|
78
|
+
return discovered unless discovered.empty?
|
|
79
|
+
|
|
80
|
+
raise ArgumentError, "No skills found at #{expanded}. Expected SKILL.md or a skills/ subfolder."
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Parse a GitHub URL into components.
|
|
84
|
+
# Supports:
|
|
85
|
+
# https://github.com/org/repo
|
|
86
|
+
# https://github.com/org/repo/tree/main/skills/my-skill
|
|
87
|
+
# https://github.com/org/repo/tree/main/skills
|
|
88
|
+
#
|
|
89
|
+
# Returns [org, repo, subpath_or_nil]
|
|
90
|
+
def parse_github_url(url)
|
|
91
|
+
# Strip trailing slash
|
|
92
|
+
url = url.chomp('/')
|
|
93
|
+
|
|
94
|
+
case url
|
|
95
|
+
when %r{github\.com/([^/]+)/([^/]+)/tree/[^/]+/(.+)}
|
|
96
|
+
# URL with tree/branch/subpath
|
|
97
|
+
[Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3)]
|
|
98
|
+
when %r{github\.com/([^/]+)/([^/]+)(?:\.git)?/?$}
|
|
99
|
+
# Bare repo URL
|
|
100
|
+
[Regexp.last_match(1), Regexp.last_match(2), nil]
|
|
101
|
+
when %r{github\.com/([^/]+)/([^/]+)/(.+)}
|
|
102
|
+
# URL with subpath but no /tree/branch/ (e.g. from skill_name:)
|
|
103
|
+
[Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3)]
|
|
104
|
+
else
|
|
105
|
+
raise ArgumentError, "Cannot parse GitHub URL: #{url}"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def resolve_github(url)
|
|
110
|
+
org, repo, subpath = parse_github_url(url)
|
|
111
|
+
local_repo = clone_or_update(org, repo)
|
|
112
|
+
|
|
113
|
+
# Resolve subpath within the cloned repo
|
|
114
|
+
target = subpath ? File.join(local_repo, subpath) : local_repo
|
|
115
|
+
resolve_local(target)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Clone (or update) a GitHub repo into the cache.
|
|
119
|
+
# Returns the local path to the cloned repo.
|
|
120
|
+
def clone_or_update(org, repo)
|
|
121
|
+
cache_path = File.join(CACHE_DIR, org, repo)
|
|
122
|
+
|
|
123
|
+
if File.directory?(File.join(cache_path, '.git'))
|
|
124
|
+
# Already cloned -- pull latest
|
|
125
|
+
system('git', '-C', cache_path, 'pull', '--ff-only', '--quiet',
|
|
126
|
+
out: File::NULL, err: File::NULL)
|
|
127
|
+
else
|
|
128
|
+
# Fresh clone
|
|
129
|
+
FileUtils.mkdir_p(File.dirname(cache_path))
|
|
130
|
+
clone_url = "https://github.com/#{org}/#{repo}.git"
|
|
131
|
+
success = system('git', 'clone', '--depth=1', '--quiet', clone_url, cache_path,
|
|
132
|
+
out: File::NULL, err: File::NULL)
|
|
133
|
+
raise "Failed to clone #{clone_url}. Check the URL and your network." unless success
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
cache_path
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
data/lib/antigravity/tool.rb
CHANGED
|
@@ -7,6 +7,12 @@ module Antigravity
|
|
|
7
7
|
super
|
|
8
8
|
subclass.extend(ClassMethods)
|
|
9
9
|
end
|
|
10
|
+
|
|
11
|
+
# Factory method: define a tool inline with a block.
|
|
12
|
+
# Tool.define(:name, desc: '...', params: { city: { type: :string } }) { |city:| ... }
|
|
13
|
+
def define(name, desc: '', params: {}, &block)
|
|
14
|
+
Dynamic.new(name, description: desc, params: params, &block)
|
|
15
|
+
end
|
|
10
16
|
end
|
|
11
17
|
|
|
12
18
|
module ClassMethods
|
|
@@ -72,12 +78,13 @@ module Antigravity
|
|
|
72
78
|
|
|
73
79
|
# Factory for dynamic Proc-based tools
|
|
74
80
|
class Dynamic < Tool
|
|
75
|
-
attr_reader :block
|
|
81
|
+
attr_reader :block, :params
|
|
76
82
|
|
|
77
|
-
def initialize(name, description:
|
|
83
|
+
def initialize(name, description: '', params: {}, &block)
|
|
78
84
|
super()
|
|
79
85
|
@dynamic_name = name.to_s
|
|
80
86
|
@dynamic_description = description
|
|
87
|
+
@params = params
|
|
81
88
|
@block = block
|
|
82
89
|
end
|
|
83
90
|
|
|
@@ -85,17 +92,37 @@ module Antigravity
|
|
|
85
92
|
@dynamic_name
|
|
86
93
|
end
|
|
87
94
|
|
|
88
|
-
def call(params)
|
|
89
|
-
@block
|
|
95
|
+
def call(params = nil, **kwargs)
|
|
96
|
+
return nil unless @block
|
|
97
|
+
|
|
98
|
+
if params && kwargs.empty?
|
|
99
|
+
# Old-style: tool.call({key: value})
|
|
100
|
+
@block.call(params)
|
|
101
|
+
else
|
|
102
|
+
# New-style: tool.call(key: value)
|
|
103
|
+
@block.call(**kwargs)
|
|
104
|
+
end
|
|
90
105
|
end
|
|
91
106
|
|
|
92
107
|
def to_json_schema
|
|
108
|
+
properties = {}
|
|
109
|
+
required_params = []
|
|
110
|
+
|
|
111
|
+
@params.each do |param_name, spec|
|
|
112
|
+
properties[param_name] = {
|
|
113
|
+
type: (spec[:type] || :string).to_s,
|
|
114
|
+
description: spec[:description] || spec[:desc] || ''
|
|
115
|
+
}
|
|
116
|
+
required_params << param_name.to_s unless spec[:required] == false
|
|
117
|
+
end
|
|
118
|
+
|
|
93
119
|
{
|
|
94
120
|
name: @dynamic_name,
|
|
95
121
|
description: @dynamic_description,
|
|
96
122
|
parameters: {
|
|
97
|
-
type:
|
|
98
|
-
properties:
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: properties,
|
|
125
|
+
required: required_params
|
|
99
126
|
}
|
|
100
127
|
}
|
|
101
128
|
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Antigravity
|
|
6
|
+
# Registers, resolves, and executes custom tools for agent callbacks.
|
|
7
|
+
# Tools are invoked by the harness when the model decides to call them.
|
|
8
|
+
class ToolRunner < Base
|
|
9
|
+
def initialize
|
|
10
|
+
@tools = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Register a tool (Tool::Dynamic or any Tool subclass)
|
|
14
|
+
def register(tool)
|
|
15
|
+
name = tool.respond_to?(:tool_name) ? tool.tool_name : tool.to_s
|
|
16
|
+
raise ToolError, "Tool '#{name}' is already registered" if @tools.key?(name)
|
|
17
|
+
|
|
18
|
+
@tools[name] = tool
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Check if a tool is registered
|
|
22
|
+
def registered?(name)
|
|
23
|
+
@tools.key?(name.to_s)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Execute a tool by name with keyword args.
|
|
27
|
+
# Returns the result or a { error: "..." } hash on failure.
|
|
28
|
+
def execute(name, **kwargs)
|
|
29
|
+
tool = @tools[name.to_s]
|
|
30
|
+
raise ToolNotFoundError, "Tool '#{name}' not found" unless tool
|
|
31
|
+
|
|
32
|
+
tool.call(**kwargs)
|
|
33
|
+
rescue ToolNotFoundError
|
|
34
|
+
raise
|
|
35
|
+
rescue => e
|
|
36
|
+
{ error: e.message }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Generate harness-compatible tool definitions for HarnessConfig
|
|
40
|
+
def to_harness_tools
|
|
41
|
+
@tools.values.map do |tool|
|
|
42
|
+
schema = tool.to_json_schema
|
|
43
|
+
{
|
|
44
|
+
name: schema[:name],
|
|
45
|
+
description: schema[:description],
|
|
46
|
+
parametersJsonSchema: JSON.generate(schema[:parameters])
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def size
|
|
52
|
+
@tools.size
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def empty?
|
|
56
|
+
@tools.empty?
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/antigravity.rb
CHANGED
|
@@ -9,15 +9,23 @@ end
|
|
|
9
9
|
require_relative "antigravity/version"
|
|
10
10
|
require_relative "antigravity/emojis"
|
|
11
11
|
require_relative "antigravity/base"
|
|
12
|
+
require_relative "antigravity/errors"
|
|
12
13
|
require_relative "antigravity/config"
|
|
13
14
|
require_relative "antigravity/message"
|
|
15
|
+
require_relative "antigravity/protocol"
|
|
14
16
|
require_relative "antigravity/harness"
|
|
15
17
|
require_relative "antigravity/hooks"
|
|
16
18
|
require_relative "antigravity/guards"
|
|
17
19
|
require_relative "antigravity/sidecar"
|
|
18
20
|
require_relative "antigravity/tool"
|
|
21
|
+
require_relative "antigravity/tool_runner"
|
|
22
|
+
require_relative "antigravity/skill_resolver"
|
|
19
23
|
require_relative "antigravity/skill"
|
|
20
24
|
require_relative "antigravity/client"
|
|
25
|
+
require_relative "antigravity/connection/binary_fetcher"
|
|
26
|
+
require_relative "antigravity/connection/websocket_client"
|
|
27
|
+
require_relative "antigravity/connection/local_connection"
|
|
28
|
+
require_relative "antigravity/conversation"
|
|
21
29
|
require_relative "antigravity/agent"
|
|
22
30
|
|
|
23
31
|
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.
|
|
4
|
+
version: 0.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Riccardo Carlesso
|
|
@@ -10,19 +10,19 @@ cert_chain: []
|
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
|
-
name:
|
|
13
|
+
name: websocket
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '1.2'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
25
|
+
version: '1.2'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: json
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -64,14 +64,22 @@ files:
|
|
|
64
64
|
- lib/antigravity/base.rb
|
|
65
65
|
- lib/antigravity/client.rb
|
|
66
66
|
- lib/antigravity/config.rb
|
|
67
|
+
- lib/antigravity/connection/binary_fetcher.rb
|
|
68
|
+
- lib/antigravity/connection/local_connection.rb
|
|
69
|
+
- lib/antigravity/connection/websocket_client.rb
|
|
70
|
+
- lib/antigravity/conversation.rb
|
|
67
71
|
- lib/antigravity/emojis.rb
|
|
72
|
+
- lib/antigravity/errors.rb
|
|
68
73
|
- lib/antigravity/guards.rb
|
|
69
74
|
- lib/antigravity/harness.rb
|
|
70
75
|
- lib/antigravity/hooks.rb
|
|
71
76
|
- lib/antigravity/message.rb
|
|
77
|
+
- lib/antigravity/protocol.rb
|
|
72
78
|
- lib/antigravity/sidecar.rb
|
|
73
79
|
- lib/antigravity/skill.rb
|
|
80
|
+
- lib/antigravity/skill_resolver.rb
|
|
74
81
|
- lib/antigravity/tool.rb
|
|
82
|
+
- lib/antigravity/tool_runner.rb
|
|
75
83
|
- lib/antigravity/version.rb
|
|
76
84
|
homepage: https://github.com/palladius/antigravity-ruby-sdk
|
|
77
85
|
licenses:
|