ruby-utcp 1.1.2 → 1.1.3
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/CHANGELOG.md +7 -0
- data/examples/servers/webrtc_server.rb +3 -3
- data/lib/utcp/code_mode/client.rb +30 -0
- data/lib/utcp/code_mode/control_flow.rb +97 -0
- data/lib/utcp/code_mode/evaluator.rb +176 -0
- data/lib/utcp/code_mode/expressions.rb +217 -0
- data/lib/utcp/code_mode/interface_generator.rb +84 -0
- data/lib/utcp/code_mode/runtime.rb +70 -0
- data/lib/utcp/code_mode/value_limits.rb +93 -0
- data/lib/utcp/code_mode/value_methods.rb +204 -0
- data/lib/utcp/code_mode.rb +9 -913
- data/lib/utcp/protocols/webrtc.rb +18 -13
- data/lib/utcp/protocols/websocket.rb +3 -0
- data/lib/utcp/version.rb +1 -1
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1869c15823d927455e8339580df8200a4eae60a1313a1384b2d98f793786e78d
|
|
4
|
+
data.tar.gz: 3f63ab346e1316dd5ba26e367637f3a1c39d8b7202d1b5c117c3885cf0c8405a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0bb88097ddb4f9170b8aeec82fb14d34fc708abc15ac7cfed54df9c03737105183c0fd09ca202c2ae53fbd276e85221393c4d8adfa36f80f5e05296b1c4ca860
|
|
7
|
+
data.tar.gz: 4c9c0147d82493982d2717bc56b4f5b0486b09fa1c37b4a2a5d4ebf7b04dd0859b28ef4361eb6d7e8458d27b6a1975073b9af68784401695a300f97f2fa7ecfd
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Split Code Mode into focused components while preserving the public API; fix `do...end` block results.
|
|
6
|
+
- Add branch-focused interpreter tests, concurrent request tests, and local HTTP/TCP/WebSocket fault injection.
|
|
7
|
+
- Isolate WebRTC peers between clients, close failed registrations, and align native offer/answer negotiation with `webrtc-ruby`.
|
|
8
|
+
- Close WebSocket sockets when the opening handshake fails.
|
|
9
|
+
- Add an opt-in real gRPC/WebRTC integration suite and raise coverage gates to 80% lines / 60% branches.
|
|
10
|
+
- Bound native integration runs with a subprocess watchdog. Include an opt-in source patch for `webrtc-ruby` 1.0.0 shutdown deadlocks, pointer-lifetime protection, and real callback/concurrent-shutdown regression tests.
|
|
11
|
+
|
|
5
12
|
- Isolate MCP sessions and resource mappings between clients and clean up failed registrations.
|
|
6
13
|
- Follow MCP tool/resource pagination and raise `ToolCallError` for tool failures, preserving the original payload.
|
|
7
14
|
- Enforce stdio deadlines across partial reads, notifications, and blocked writes; bound message and stderr buffers.
|
|
@@ -35,7 +35,7 @@ ExampleHTTP.server(port) do |server|
|
|
|
35
35
|
server.mount_proc("/connect") do |request, response|
|
|
36
36
|
payload = ExampleHTTP.request_json(request)
|
|
37
37
|
candidates = []
|
|
38
|
-
peer = WebRTC::RTCPeerConnection.new
|
|
38
|
+
peer = WebRTC::RTCPeerConnection.new({ disable_auto_negotiation: true })
|
|
39
39
|
peer.on_ice_candidate { |candidate| candidates << candidate if candidate }
|
|
40
40
|
peer.on_data_channel do |channel|
|
|
41
41
|
channel.on_message do |event|
|
|
@@ -50,11 +50,11 @@ ExampleHTTP.server(port) do |server|
|
|
|
50
50
|
offer = WebRTC::RTCSessionDescription.new(type: :offer, sdp: payload.fetch("sdp"))
|
|
51
51
|
peer.set_remote_description(offer).await
|
|
52
52
|
answer = peer.create_answer.await
|
|
53
|
-
|
|
53
|
+
# The native binding already installs the answer while creating it.
|
|
54
54
|
wait_for_ice(peer)
|
|
55
55
|
peers_mutex.synchronize { peers[payload.fetch("peer_id")] = peer }
|
|
56
56
|
ExampleHTTP.json(response, {
|
|
57
|
-
sdp:
|
|
57
|
+
sdp: answer.sdp,
|
|
58
58
|
candidates: candidates.map(&:to_h),
|
|
59
59
|
tools: [{ name: "echo", description: "Echo over a WebRTC DataChannel" }]
|
|
60
60
|
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UTCP
|
|
4
|
+
class CodeModeUtcpClient < Client
|
|
5
|
+
AGENT_PROMPT_TEMPLATE = <<~PROMPT.freeze
|
|
6
|
+
Use `codemode.search_tools` before writing a Code Mode program when tool names are unknown.
|
|
7
|
+
Call tools as `codemode.call_tool("manual.tool", key: value)`. Use the exact qualified name.
|
|
8
|
+
Code Mode accepts a constrained Ruby subset: local variables, JSON-like literals, arithmetic,
|
|
9
|
+
conditionals, bounded loops, and common Array/Hash/String transforms. The final expression or an
|
|
10
|
+
explicit `return` is the result. Use `puts`, `print`, `p`, or `warn` for captured logs. The values
|
|
11
|
+
`codemode.interfaces` and `codemode.get_tool_interface("manual.tool")` describe registered tools. Filesystem,
|
|
12
|
+
process, constants, imports, reflection, eval, and direct network access are unavailable.
|
|
13
|
+
PROMPT
|
|
14
|
+
|
|
15
|
+
def call_tool_chain(code, timeout_value = nil, timeout: nil, max_steps: CodeMode::DEFAULT_MAX_STEPS)
|
|
16
|
+
selected_timeout = timeout || timeout_value || CodeMode::DEFAULT_TIMEOUT
|
|
17
|
+
CodeMode.new(self).execute(code, timeout: selected_timeout, max_steps: max_steps)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def get_all_tools_ruby_interfaces
|
|
21
|
+
CodeMode.new(self).interfaces
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def get_tool_interface(name)
|
|
25
|
+
CodeMode.new(self).tool_interface(name)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
CodeModeClient = CodeModeUtcpClient
|
|
30
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UTCP
|
|
4
|
+
class CodeMode
|
|
5
|
+
class Evaluator
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def truthy?(value)
|
|
9
|
+
!value.nil? && value != false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def evaluate_if(condition, truthy_statements, alternative)
|
|
13
|
+
if truthy?(evaluate(condition))
|
|
14
|
+
evaluate_statements(truthy_statements)
|
|
15
|
+
elsif alternative&.first == :else
|
|
16
|
+
evaluate_statements(alternative[1])
|
|
17
|
+
elsif alternative&.first == :elsif
|
|
18
|
+
evaluate_if(alternative[1], alternative[2], alternative[3])
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def evaluate_unless(condition, statements, alternative)
|
|
23
|
+
unless truthy?(evaluate(condition))
|
|
24
|
+
evaluate_statements(statements)
|
|
25
|
+
else
|
|
26
|
+
evaluate_statements(alternative[1]) if alternative&.first == :else
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def evaluate_body_statement(node)
|
|
31
|
+
statements, rescue_clause, else_clause, ensure_clause = node.drop(1)
|
|
32
|
+
completed = false
|
|
33
|
+
result = begin
|
|
34
|
+
value = evaluate_statements(statements)
|
|
35
|
+
completed = true
|
|
36
|
+
value
|
|
37
|
+
rescue ReturnSignal, BreakSignal, NextSignal, CodeModeTimeoutError, CodeModeLimitError, CodeModeSyntaxError
|
|
38
|
+
raise
|
|
39
|
+
rescue StandardError => error
|
|
40
|
+
raise unless rescue_clause
|
|
41
|
+
evaluate_rescue_clause(rescue_clause, error)
|
|
42
|
+
ensure
|
|
43
|
+
evaluate_statements(ensure_clause[1]) if ensure_clause&.first == :ensure
|
|
44
|
+
end
|
|
45
|
+
completed && else_clause ? evaluate_statements(else_clause) : result
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def evaluate_rescue_clause(node, error)
|
|
49
|
+
unsupported!(node) unless node.first == :rescue
|
|
50
|
+
raise CodeModeSyntaxError, "Code Mode rescue does not accept exception classes" if node[1]
|
|
51
|
+
|
|
52
|
+
scope = {}
|
|
53
|
+
assign_rescue_variable(scope, node[2], error) if node[2]
|
|
54
|
+
with_scope(scope) { evaluate_statements(node[3]) }
|
|
55
|
+
rescue ReturnSignal, BreakSignal, NextSignal, CodeModeTimeoutError, CodeModeLimitError, CodeModeSyntaxError
|
|
56
|
+
raise
|
|
57
|
+
rescue StandardError => nested
|
|
58
|
+
next_clause = node[4]
|
|
59
|
+
raise nested unless next_clause&.first == :rescue
|
|
60
|
+
evaluate_rescue_clause(next_clause, nested)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def assign_rescue_variable(scope, target, error)
|
|
64
|
+
unsupported!(target) unless target.first == :var_field
|
|
65
|
+
scope[token_value(target[1])] = {
|
|
66
|
+
"message" => error.message,
|
|
67
|
+
"type" => error.class.name
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def evaluate_rescue_modifier(node)
|
|
72
|
+
evaluate(node[1])
|
|
73
|
+
rescue ReturnSignal, BreakSignal, NextSignal, CodeModeTimeoutError, CodeModeLimitError, CodeModeSyntaxError
|
|
74
|
+
raise
|
|
75
|
+
rescue StandardError
|
|
76
|
+
evaluate(node[2])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def evaluate_loop(condition, statements, until_condition:)
|
|
80
|
+
result = nil
|
|
81
|
+
loop do
|
|
82
|
+
matches = truthy?(evaluate(condition))
|
|
83
|
+
break if until_condition ? matches : !matches
|
|
84
|
+
|
|
85
|
+
begin
|
|
86
|
+
result = evaluate_statements(statements)
|
|
87
|
+
rescue NextSignal => signal
|
|
88
|
+
result = signal.value
|
|
89
|
+
rescue BreakSignal => signal
|
|
90
|
+
return signal.value
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
result
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UTCP
|
|
4
|
+
class CodeMode
|
|
5
|
+
class Evaluator
|
|
6
|
+
ReturnSignal = Class.new(StandardError) do
|
|
7
|
+
attr_reader :value
|
|
8
|
+
|
|
9
|
+
def initialize(value)
|
|
10
|
+
@value = value
|
|
11
|
+
super()
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
BreakSignal = Class.new(StandardError) do
|
|
15
|
+
attr_reader :value
|
|
16
|
+
|
|
17
|
+
def initialize(value = nil)
|
|
18
|
+
@value = value
|
|
19
|
+
super()
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
NextSignal = Class.new(StandardError) do
|
|
23
|
+
attr_reader :value
|
|
24
|
+
|
|
25
|
+
def initialize(value = nil)
|
|
26
|
+
@value = value
|
|
27
|
+
super()
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
RuntimeAPI = Class.new
|
|
32
|
+
Block = Struct.new(:evaluator, :parameters, :body) do
|
|
33
|
+
def call(*values)
|
|
34
|
+
scope = {}
|
|
35
|
+
if parameters.length == 1 && values.length > 1
|
|
36
|
+
scope[parameters.first] = values
|
|
37
|
+
else
|
|
38
|
+
parameters.each_with_index { |name, index| scope[name] = values[index] }
|
|
39
|
+
end
|
|
40
|
+
evaluator.with_scope(scope) { evaluator.evaluate(body) }
|
|
41
|
+
rescue NextSignal => signal
|
|
42
|
+
signal.value
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
SAFE_ENUMERABLE_METHODS = %w[
|
|
47
|
+
all? any? collect count each filter find map none? reduce inject reject select sort_by
|
|
48
|
+
].freeze
|
|
49
|
+
SAFE_VALUE_METHODS = %w[
|
|
50
|
+
abs ceil compact dig downcase drop empty? end_with? even? fetch first flatten floor
|
|
51
|
+
has_key? include? inspect join key? keys last length max merge min negative? odd? positive?
|
|
52
|
+
nil? reverse round size slice sort split start_with? strip sum take to_a to_f to_h to_i to_s
|
|
53
|
+
uniq upcase values zero?
|
|
54
|
+
].freeze
|
|
55
|
+
|
|
56
|
+
attr_reader :logs
|
|
57
|
+
|
|
58
|
+
def initialize(client, interfaces:, timeout:, max_steps:)
|
|
59
|
+
raise CodeModeLimitError, "max_steps must be greater than zero" unless max_steps.positive?
|
|
60
|
+
|
|
61
|
+
@client = client
|
|
62
|
+
@interfaces = interfaces
|
|
63
|
+
@deadline = monotonic_now + timeout
|
|
64
|
+
@max_steps = max_steps
|
|
65
|
+
@steps = 0
|
|
66
|
+
@logs = []
|
|
67
|
+
@log_bytes = 0
|
|
68
|
+
@scopes = [{ "codemode" => RuntimeAPI.new }]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def execute(source)
|
|
72
|
+
tree = Ripper.sexp(source)
|
|
73
|
+
raise CodeModeSyntaxError, "Invalid Ruby syntax" unless tree
|
|
74
|
+
|
|
75
|
+
safe_tool_value(evaluate(tree))
|
|
76
|
+
rescue ReturnSignal => signal
|
|
77
|
+
safe_tool_value(signal.value)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def evaluate(node)
|
|
81
|
+
tick!
|
|
82
|
+
return nil if node.nil?
|
|
83
|
+
return node unless node.is_a?(Array)
|
|
84
|
+
return evaluate_statements(node) unless node.first.is_a?(Symbol)
|
|
85
|
+
|
|
86
|
+
type = node.first
|
|
87
|
+
case type
|
|
88
|
+
when :program then evaluate_statements(node[1])
|
|
89
|
+
when :void_stmt then nil
|
|
90
|
+
when :assign then assign(node[1], evaluate(node[2]))
|
|
91
|
+
when :opassign then evaluate_opassign(node)
|
|
92
|
+
when :var_ref then evaluate_variable_token(node[1])
|
|
93
|
+
when :vcall then evaluate_vcall(node[1])
|
|
94
|
+
when :fcall then call_function(token_value(node[1]), [], nil)
|
|
95
|
+
when :@int then Integer(node[1], 10)
|
|
96
|
+
when :@float then Float(node[1])
|
|
97
|
+
when :@kw then evaluate_keyword(node[1])
|
|
98
|
+
when :string_literal then evaluate(node[1]).to_s
|
|
99
|
+
when :string_content then node.drop(1).map { |part| evaluate_string_part(part) }.join
|
|
100
|
+
when :symbol_literal then evaluate_symbol(node[1])
|
|
101
|
+
when :array then Array(node[1]).map { |item| evaluate(item) }
|
|
102
|
+
when :hash then evaluate_hash(node[1])
|
|
103
|
+
when :bare_assoc_hash then evaluate_associations(node[1])
|
|
104
|
+
when :paren then evaluate_statements(node[1])
|
|
105
|
+
when :binary then evaluate_binary(node[1], node[2], node[3])
|
|
106
|
+
when :unary then evaluate_unary(node[1], node[2])
|
|
107
|
+
when :dot2 then Range.new(evaluate(node[1]), evaluate(node[2]), false)
|
|
108
|
+
when :dot3 then Range.new(evaluate(node[1]), evaluate(node[2]), true)
|
|
109
|
+
when :aref then safe_index(evaluate(node[1]), extract_arguments(node[2]))
|
|
110
|
+
when :call, :method_add_arg, :command, :command_call then evaluate_call(node)
|
|
111
|
+
when :method_add_block then evaluate_call(node[1], build_block(node[2]))
|
|
112
|
+
when :begin then evaluate(node[1])
|
|
113
|
+
when :bodystmt then evaluate_body_statement(node)
|
|
114
|
+
when :rescue_mod then evaluate_rescue_modifier(node)
|
|
115
|
+
when :if then evaluate_if(node[1], node[2], node[3])
|
|
116
|
+
when :unless then evaluate_unless(node[1], node[2], node[3])
|
|
117
|
+
when :if_mod then truthy?(evaluate(node[1])) ? evaluate(node[2]) : nil
|
|
118
|
+
when :unless_mod then truthy?(evaluate(node[1])) ? nil : evaluate(node[2])
|
|
119
|
+
when :while then evaluate_loop(node[1], node[2], until_condition: false)
|
|
120
|
+
when :until then evaluate_loop(node[1], node[2], until_condition: true)
|
|
121
|
+
when :return then raise ReturnSignal, return_value(node[1])
|
|
122
|
+
when :return0 then raise ReturnSignal, nil
|
|
123
|
+
when :break then raise BreakSignal, return_value(node[1])
|
|
124
|
+
when :next then raise NextSignal, return_value(node[1])
|
|
125
|
+
else
|
|
126
|
+
unsupported!(node)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def evaluate_statements(statements)
|
|
131
|
+
Array(statements).reduce(nil) { |_result, statement| evaluate(statement) }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def with_scope(scope)
|
|
135
|
+
@scopes << scope
|
|
136
|
+
yield
|
|
137
|
+
ensure
|
|
138
|
+
@scopes.pop
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def tick!
|
|
144
|
+
@steps += 1
|
|
145
|
+
raise CodeModeLimitError, "Code Mode step limit exceeded" if @steps > @max_steps
|
|
146
|
+
raise CodeModeTimeoutError, "Code Mode execution timed out" if monotonic_now >= @deadline
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def monotonic_now
|
|
150
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def unsupported!(node)
|
|
154
|
+
line = find_line(node)
|
|
155
|
+
suffix = line ? " at line #{line}" : ""
|
|
156
|
+
raise CodeModeSyntaxError, "Unsupported Ruby construct #{node.first.inspect}#{suffix}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def find_line(node)
|
|
160
|
+
return node[2][0] if node.is_a?(Array) && node.first.to_s.start_with?("@") && node[2].is_a?(Array)
|
|
161
|
+
return nil unless node.is_a?(Array)
|
|
162
|
+
|
|
163
|
+
node.each do |child|
|
|
164
|
+
line = find_line(child)
|
|
165
|
+
return line if line
|
|
166
|
+
end
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def token_value(token)
|
|
171
|
+
token.is_a?(Array) ? token[1].to_s : token.to_s
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UTCP
|
|
4
|
+
class CodeMode
|
|
5
|
+
class Evaluator
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def evaluate_keyword(keyword)
|
|
9
|
+
case keyword
|
|
10
|
+
when "true" then true
|
|
11
|
+
when "false" then false
|
|
12
|
+
when "nil" then nil
|
|
13
|
+
else raise CodeModeSyntaxError, "Unsupported keyword #{keyword.inspect}"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def evaluate_string_part(part)
|
|
18
|
+
return part[1] if part.first == :@tstring_content
|
|
19
|
+
return evaluate_statements(part[1]).to_s if part.first == :string_embexpr
|
|
20
|
+
|
|
21
|
+
unsupported!(part)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def evaluate_symbol(node)
|
|
25
|
+
token = node.first == :symbol ? node[1] : node
|
|
26
|
+
token_value(token).to_sym
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def evaluate_hash(contents)
|
|
30
|
+
return {} unless contents
|
|
31
|
+
return evaluate_associations(contents[1]) if contents.first == :assoclist_from_args
|
|
32
|
+
|
|
33
|
+
unsupported!(contents)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def evaluate_associations(associations)
|
|
37
|
+
Array(associations).each_with_object({}) do |association, result|
|
|
38
|
+
unsupported!(association) unless association.first == :assoc_new
|
|
39
|
+
key_node = association[1]
|
|
40
|
+
key = key_node.first == :@label ? key_node[1].sub(/:\z/, "") : evaluate(key_node)
|
|
41
|
+
result[key] = evaluate(association[2])
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def evaluate_variable_token(token)
|
|
46
|
+
return evaluate_keyword(token[1]) if token.first == :@kw
|
|
47
|
+
lookup(token_value(token))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def evaluate_vcall(token)
|
|
51
|
+
name = token_value(token)
|
|
52
|
+
value = lookup(name, missing: :sentinel)
|
|
53
|
+
return value unless value == :sentinel
|
|
54
|
+
|
|
55
|
+
call_function(name, [], nil)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def lookup(name, missing: nil)
|
|
59
|
+
@scopes.reverse_each { |scope| return scope[name] if scope.key?(name) }
|
|
60
|
+
return missing if missing == :sentinel
|
|
61
|
+
|
|
62
|
+
raise CodeModeExecutionError, "Undefined local variable #{name.inspect}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def assign(target, value)
|
|
66
|
+
case target.first
|
|
67
|
+
when :var_field
|
|
68
|
+
name = token_value(target[1])
|
|
69
|
+
scope = @scopes.reverse.find { |candidate| candidate.key?(name) } || @scopes.last
|
|
70
|
+
scope[name] = value
|
|
71
|
+
when :aref_field
|
|
72
|
+
receiver = evaluate(target[1])
|
|
73
|
+
arguments = extract_arguments(target[2])
|
|
74
|
+
raise CodeModeSyntaxError, "Indexed assignment requires exactly one key" unless arguments.length == 1
|
|
75
|
+
raise CodeModeSyntaxError, "Indexed assignment is only allowed on arrays and hashes" unless receiver.is_a?(Array) || receiver.is_a?(Hash)
|
|
76
|
+
|
|
77
|
+
receiver[arguments.first] = value
|
|
78
|
+
else
|
|
79
|
+
unsupported!(target)
|
|
80
|
+
end
|
|
81
|
+
value
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def evaluate_opassign(node)
|
|
85
|
+
target = node[1]
|
|
86
|
+
operator = token_value(node[2]).sub(/=\z/, "").to_sym
|
|
87
|
+
current = read_assignment_target(target)
|
|
88
|
+
assign(target, apply_binary(current, operator, evaluate(node[3])))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def read_assignment_target(target)
|
|
92
|
+
case target.first
|
|
93
|
+
when :var_field then lookup(token_value(target[1]))
|
|
94
|
+
when :aref_field then safe_index(evaluate(target[1]), extract_arguments(target[2]))
|
|
95
|
+
else unsupported!(target)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def evaluate_binary(left_node, operator, right_node)
|
|
100
|
+
left = evaluate(left_node)
|
|
101
|
+
return left unless truthy?(left) if [:'&&', :and].include?(operator)
|
|
102
|
+
return left if truthy?(left) if [:'||', :or].include?(operator)
|
|
103
|
+
|
|
104
|
+
apply_binary(left, operator, evaluate(right_node))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def apply_binary(left, operator, right)
|
|
108
|
+
preflight_binary_size!(left, operator, right)
|
|
109
|
+
value = case operator
|
|
110
|
+
when :+ then left + right
|
|
111
|
+
when :- then left - right
|
|
112
|
+
when :* then left * right
|
|
113
|
+
when :/ then left / right
|
|
114
|
+
when :% then left % right
|
|
115
|
+
when :** then left**right
|
|
116
|
+
when :== then left == right
|
|
117
|
+
when :!= then left != right
|
|
118
|
+
when :< then left < right
|
|
119
|
+
when :<= then left <= right
|
|
120
|
+
when :> then left > right
|
|
121
|
+
when :>= then left >= right
|
|
122
|
+
when :<=> then left <=> right
|
|
123
|
+
when :'&&', :and, :'||', :or then right
|
|
124
|
+
else raise CodeModeSyntaxError, "Unsupported operator #{operator.inspect}"
|
|
125
|
+
end
|
|
126
|
+
bounded_value!(value)
|
|
127
|
+
rescue NoMethodError, TypeError, ArgumentError, ZeroDivisionError => error
|
|
128
|
+
raise CodeModeExecutionError, "Invalid #{operator} operation: #{error.message}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def evaluate_unary(operator, operand_node)
|
|
132
|
+
operand = evaluate(operand_node)
|
|
133
|
+
case operator
|
|
134
|
+
when :! then !truthy?(operand)
|
|
135
|
+
when :+@ then +operand
|
|
136
|
+
when :-@ then -operand
|
|
137
|
+
else raise CodeModeSyntaxError, "Unsupported unary operator #{operator.inspect}"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def return_value(arguments_node)
|
|
142
|
+
arguments = extract_arguments(arguments_node)
|
|
143
|
+
arguments.length <= 1 ? arguments.first : arguments
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def extract_arguments(node)
|
|
147
|
+
return [] if node.nil?
|
|
148
|
+
case node.first
|
|
149
|
+
when :arg_paren then extract_arguments(node[1])
|
|
150
|
+
when :args_add_block then Array(node[1]).map { |argument| evaluate(argument) }
|
|
151
|
+
when :args_new then []
|
|
152
|
+
when :args_add then extract_arguments(node[1]) + [evaluate(node[2])]
|
|
153
|
+
else [evaluate(node)]
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def build_block(node)
|
|
158
|
+
unsupported!(node) unless %i[brace_block do_block].include?(node.first)
|
|
159
|
+
parameters = extract_block_parameters(node[1])
|
|
160
|
+
Block.new(self, parameters, node[2])
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def extract_block_parameters(node)
|
|
164
|
+
return [] unless node
|
|
165
|
+
params = node.first == :block_var ? node[1] : node
|
|
166
|
+
unsupported!(params) unless params&.first == :params
|
|
167
|
+
required = Array(params[1])
|
|
168
|
+
unsupported!(params) unless params.drop(2).all?(&:nil?)
|
|
169
|
+
|
|
170
|
+
required.map { |token| token_value(token) }
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def evaluate_call(node, block = nil)
|
|
174
|
+
case node.first
|
|
175
|
+
when :method_add_arg
|
|
176
|
+
invoke_call_target(node[1], extract_arguments(node[2]), block)
|
|
177
|
+
when :call
|
|
178
|
+
invoke_method(evaluate(node[1]), token_value(node[3]), [], block)
|
|
179
|
+
when :fcall
|
|
180
|
+
call_function(token_value(node[1]), [], block)
|
|
181
|
+
when :vcall
|
|
182
|
+
evaluate_vcall(node[1])
|
|
183
|
+
when :command
|
|
184
|
+
call_function(token_value(node[1]), extract_arguments(node[2]), block)
|
|
185
|
+
when :command_call
|
|
186
|
+
invoke_method(evaluate(node[1]), token_value(node[3]), extract_arguments(node[4]), block)
|
|
187
|
+
else
|
|
188
|
+
unsupported!(node)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def invoke_call_target(target, arguments, block)
|
|
193
|
+
case target.first
|
|
194
|
+
when :call
|
|
195
|
+
invoke_method(evaluate(target[1]), token_value(target[3]), arguments, block)
|
|
196
|
+
when :fcall
|
|
197
|
+
call_function(token_value(target[1]), arguments, block)
|
|
198
|
+
when :vcall
|
|
199
|
+
call_function(token_value(target[1]), arguments, block)
|
|
200
|
+
else
|
|
201
|
+
unsupported!(target)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def call_function(name, arguments, block)
|
|
206
|
+
case name
|
|
207
|
+
when "puts", "print", "p", "warn"
|
|
208
|
+
log(name, arguments)
|
|
209
|
+
nil
|
|
210
|
+
else
|
|
211
|
+
hint = name == "call_tool" ? "; use codemode.call_tool(...)" : ""
|
|
212
|
+
raise CodeModeSyntaxError, "Function #{name.inspect} is not available in Code Mode#{hint}"
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UTCP
|
|
4
|
+
class CodeMode
|
|
5
|
+
class InterfaceGenerator
|
|
6
|
+
RUBY_KEYWORDS = %w[
|
|
7
|
+
BEGIN END alias and begin break case class def defined? do else elsif end ensure false
|
|
8
|
+
for if in module next nil not or redo rescue retry return self super then true undef
|
|
9
|
+
unless until when while yield
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def tool_descriptor(tool)
|
|
14
|
+
{
|
|
15
|
+
"name" => tool.name,
|
|
16
|
+
"description" => tool.description,
|
|
17
|
+
"inputs" => tool.inputs.to_h,
|
|
18
|
+
"outputs" => tool.outputs.to_h,
|
|
19
|
+
"tags" => tool.tags.dup
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def identifier(value)
|
|
24
|
+
name = value.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
|
|
25
|
+
name = "_#{name}" if name.match?(/\A\d/)
|
|
26
|
+
name = "_#{name}" if RUBY_KEYWORDS.include?(name)
|
|
27
|
+
name.empty? ? "_" : name
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ruby_type(schema)
|
|
31
|
+
value = schema.is_a?(Hash) ? schema : {}
|
|
32
|
+
case value["type"] || value[:type]
|
|
33
|
+
when "string" then "String"
|
|
34
|
+
when "integer" then "Integer"
|
|
35
|
+
when "number" then "Numeric"
|
|
36
|
+
when "boolean" then "Boolean"
|
|
37
|
+
when "array" then "Array"
|
|
38
|
+
when "object" then "Hash"
|
|
39
|
+
when Array then (value["type"] || value[:type]).map { |type| ruby_type("type" => type) }.join(" | ")
|
|
40
|
+
else "Object"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def initialize(tools)
|
|
46
|
+
@tools = tools.sort_by(&:name)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def render
|
|
50
|
+
return "# No UTCP tools are registered." if @tools.empty?
|
|
51
|
+
|
|
52
|
+
@tools.map { |tool| render_tool(tool) }.join("\n\n")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def render_tool(tool)
|
|
58
|
+
descriptor = self.class.tool_descriptor(tool)
|
|
59
|
+
schema = descriptor["inputs"]
|
|
60
|
+
properties = schema.fetch("properties", {})
|
|
61
|
+
required = Array(schema["required"])
|
|
62
|
+
parameters = properties.map do |name, property|
|
|
63
|
+
suffix = required.include?(name.to_s) ? ":" : ": nil"
|
|
64
|
+
"#{self.class.identifier(name)}#{suffix}"
|
|
65
|
+
end
|
|
66
|
+
signature = parameters.empty? ? "" : parameters.join(", ")
|
|
67
|
+
description = descriptor["description"].to_s.strip
|
|
68
|
+
lines = []
|
|
69
|
+
lines << "# #{description}" unless description.empty?
|
|
70
|
+
unless properties.empty?
|
|
71
|
+
parameter_docs = properties.map do |name, property|
|
|
72
|
+
requirement = required.include?(name.to_s) ? "required" : "optional"
|
|
73
|
+
"#{self.class.identifier(name)} (#{self.class.ruby_type(property)}, #{requirement})"
|
|
74
|
+
end
|
|
75
|
+
lines << "# Parameters: #{parameter_docs.join(', ')}"
|
|
76
|
+
end
|
|
77
|
+
lines << "# Returns: #{self.class.ruby_type(descriptor["outputs"])}"
|
|
78
|
+
separator = signature.empty? ? "" : ", "
|
|
79
|
+
lines << "codemode.call_tool(#{descriptor["name"].inspect}#{separator}#{signature})"
|
|
80
|
+
lines.join("\n")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|