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.
@@ -59,918 +59,14 @@ module UTCP
59
59
 
60
60
  InterfaceGenerator.tool_descriptor(tool)
61
61
  end
62
-
63
- class InterfaceGenerator
64
- RUBY_KEYWORDS = %w[
65
- BEGIN END alias and begin break case class def defined? do else elsif end ensure false
66
- for if in module next nil not or redo rescue retry return self super then true undef
67
- unless until when while yield
68
- ].freeze
69
-
70
- class << self
71
- def tool_descriptor(tool)
72
- {
73
- "name" => tool.name,
74
- "description" => tool.description,
75
- "inputs" => tool.inputs.to_h,
76
- "outputs" => tool.outputs.to_h,
77
- "tags" => tool.tags.dup
78
- }
79
- end
80
-
81
- def identifier(value)
82
- name = value.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
83
- name = "_#{name}" if name.match?(/\A\d/)
84
- name = "_#{name}" if RUBY_KEYWORDS.include?(name)
85
- name.empty? ? "_" : name
86
- end
87
-
88
- def ruby_type(schema)
89
- value = schema.is_a?(Hash) ? schema : {}
90
- case value["type"] || value[:type]
91
- when "string" then "String"
92
- when "integer" then "Integer"
93
- when "number" then "Numeric"
94
- when "boolean" then "Boolean"
95
- when "array" then "Array"
96
- when "object" then "Hash"
97
- when Array then (value["type"] || value[:type]).map { |type| ruby_type("type" => type) }.join(" | ")
98
- else "Object"
99
- end
100
- end
101
- end
102
-
103
- def initialize(tools)
104
- @tools = tools.sort_by(&:name)
105
- end
106
-
107
- def render
108
- return "# No UTCP tools are registered." if @tools.empty?
109
-
110
- @tools.map { |tool| render_tool(tool) }.join("\n\n")
111
- end
112
-
113
- private
114
-
115
- def render_tool(tool)
116
- descriptor = self.class.tool_descriptor(tool)
117
- schema = descriptor["inputs"]
118
- properties = schema.fetch("properties", {})
119
- required = Array(schema["required"])
120
- parameters = properties.map do |name, property|
121
- suffix = required.include?(name.to_s) ? ":" : ": nil"
122
- "#{self.class.identifier(name)}#{suffix}"
123
- end
124
- signature = parameters.empty? ? "" : parameters.join(", ")
125
- description = descriptor["description"].to_s.strip
126
- lines = []
127
- lines << "# #{description}" unless description.empty?
128
- unless properties.empty?
129
- parameter_docs = properties.map do |name, property|
130
- requirement = required.include?(name.to_s) ? "required" : "optional"
131
- "#{self.class.identifier(name)} (#{self.class.ruby_type(property)}, #{requirement})"
132
- end
133
- lines << "# Parameters: #{parameter_docs.join(', ')}"
134
- end
135
- lines << "# Returns: #{self.class.ruby_type(descriptor["outputs"])}"
136
- separator = signature.empty? ? "" : ", "
137
- lines << "codemode.call_tool(#{descriptor["name"].inspect}#{separator}#{signature})"
138
- lines.join("\n")
139
- end
140
- end
141
-
142
- class Evaluator
143
- ReturnSignal = Class.new(StandardError) do
144
- attr_reader :value
145
-
146
- def initialize(value)
147
- @value = value
148
- super()
149
- end
150
- end
151
- BreakSignal = Class.new(StandardError) do
152
- attr_reader :value
153
-
154
- def initialize(value = nil)
155
- @value = value
156
- super()
157
- end
158
- end
159
- NextSignal = Class.new(StandardError) do
160
- attr_reader :value
161
-
162
- def initialize(value = nil)
163
- @value = value
164
- super()
165
- end
166
- end
167
-
168
- RuntimeAPI = Class.new
169
- Block = Struct.new(:evaluator, :parameters, :body) do
170
- def call(*values)
171
- scope = {}
172
- if parameters.length == 1 && values.length > 1
173
- scope[parameters.first] = values
174
- else
175
- parameters.each_with_index { |name, index| scope[name] = values[index] }
176
- end
177
- evaluator.with_scope(scope) { evaluator.evaluate_statements(body) }
178
- rescue NextSignal => signal
179
- signal.value
180
- end
181
- end
182
-
183
- SAFE_ENUMERABLE_METHODS = %w[
184
- all? any? collect count each filter find map none? reduce inject reject select sort_by
185
- ].freeze
186
- SAFE_VALUE_METHODS = %w[
187
- abs ceil compact dig downcase drop empty? end_with? even? fetch first flatten floor
188
- has_key? include? inspect join key? keys last length max merge min negative? odd? positive?
189
- nil? reverse round size slice sort split start_with? strip sum take to_a to_f to_h to_i to_s
190
- uniq upcase values zero?
191
- ].freeze
192
-
193
- attr_reader :logs
194
-
195
- def initialize(client, interfaces:, timeout:, max_steps:)
196
- raise CodeModeLimitError, "max_steps must be greater than zero" unless max_steps.positive?
197
-
198
- @client = client
199
- @interfaces = interfaces
200
- @deadline = monotonic_now + timeout
201
- @max_steps = max_steps
202
- @steps = 0
203
- @logs = []
204
- @log_bytes = 0
205
- @scopes = [{ "codemode" => RuntimeAPI.new }]
206
- end
207
-
208
- def execute(source)
209
- tree = Ripper.sexp(source)
210
- raise CodeModeSyntaxError, "Invalid Ruby syntax" unless tree
211
-
212
- safe_tool_value(evaluate(tree))
213
- rescue ReturnSignal => signal
214
- safe_tool_value(signal.value)
215
- end
216
-
217
- def evaluate(node)
218
- tick!
219
- return nil if node.nil?
220
- return node unless node.is_a?(Array)
221
- return evaluate_statements(node) unless node.first.is_a?(Symbol)
222
-
223
- type = node.first
224
- case type
225
- when :program then evaluate_statements(node[1])
226
- when :void_stmt then nil
227
- when :assign then assign(node[1], evaluate(node[2]))
228
- when :opassign then evaluate_opassign(node)
229
- when :var_ref then evaluate_variable_token(node[1])
230
- when :vcall then evaluate_vcall(node[1])
231
- when :fcall then call_function(token_value(node[1]), [], nil)
232
- when :@int then Integer(node[1], 10)
233
- when :@float then Float(node[1])
234
- when :@kw then evaluate_keyword(node[1])
235
- when :string_literal then evaluate(node[1]).to_s
236
- when :string_content then node.drop(1).map { |part| evaluate_string_part(part) }.join
237
- when :symbol_literal then evaluate_symbol(node[1])
238
- when :array then Array(node[1]).map { |item| evaluate(item) }
239
- when :hash then evaluate_hash(node[1])
240
- when :bare_assoc_hash then evaluate_associations(node[1])
241
- when :paren then evaluate_statements(node[1])
242
- when :binary then evaluate_binary(node[1], node[2], node[3])
243
- when :unary then evaluate_unary(node[1], node[2])
244
- when :dot2 then Range.new(evaluate(node[1]), evaluate(node[2]), false)
245
- when :dot3 then Range.new(evaluate(node[1]), evaluate(node[2]), true)
246
- when :aref then safe_index(evaluate(node[1]), extract_arguments(node[2]))
247
- when :call, :method_add_arg, :command, :command_call then evaluate_call(node)
248
- when :method_add_block then evaluate_call(node[1], build_block(node[2]))
249
- when :begin then evaluate(node[1])
250
- when :bodystmt then evaluate_body_statement(node)
251
- when :rescue_mod then evaluate_rescue_modifier(node)
252
- when :if then evaluate_if(node[1], node[2], node[3])
253
- when :unless then evaluate_unless(node[1], node[2], node[3])
254
- when :if_mod then truthy?(evaluate(node[1])) ? evaluate(node[2]) : nil
255
- when :unless_mod then truthy?(evaluate(node[1])) ? nil : evaluate(node[2])
256
- when :while then evaluate_loop(node[1], node[2], until_condition: false)
257
- when :until then evaluate_loop(node[1], node[2], until_condition: true)
258
- when :return then raise ReturnSignal, return_value(node[1])
259
- when :return0 then raise ReturnSignal, nil
260
- when :break then raise BreakSignal, return_value(node[1])
261
- when :next then raise NextSignal, return_value(node[1])
262
- else
263
- unsupported!(node)
264
- end
265
- end
266
-
267
- def evaluate_statements(statements)
268
- Array(statements).reduce(nil) { |_result, statement| evaluate(statement) }
269
- end
270
-
271
- def with_scope(scope)
272
- @scopes << scope
273
- yield
274
- ensure
275
- @scopes.pop
276
- end
277
-
278
- private
279
-
280
- def tick!
281
- @steps += 1
282
- raise CodeModeLimitError, "Code Mode step limit exceeded" if @steps > @max_steps
283
- raise CodeModeTimeoutError, "Code Mode execution timed out" if monotonic_now >= @deadline
284
- end
285
-
286
- def monotonic_now
287
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
288
- end
289
-
290
- def unsupported!(node)
291
- line = find_line(node)
292
- suffix = line ? " at line #{line}" : ""
293
- raise CodeModeSyntaxError, "Unsupported Ruby construct #{node.first.inspect}#{suffix}"
294
- end
295
-
296
- def find_line(node)
297
- return node[2][0] if node.is_a?(Array) && node.first.to_s.start_with?("@") && node[2].is_a?(Array)
298
- return nil unless node.is_a?(Array)
299
-
300
- node.each do |child|
301
- line = find_line(child)
302
- return line if line
303
- end
304
- nil
305
- end
306
-
307
- def token_value(token)
308
- token.is_a?(Array) ? token[1].to_s : token.to_s
309
- end
310
-
311
- def evaluate_keyword(keyword)
312
- case keyword
313
- when "true" then true
314
- when "false" then false
315
- when "nil" then nil
316
- else raise CodeModeSyntaxError, "Unsupported keyword #{keyword.inspect}"
317
- end
318
- end
319
-
320
- def evaluate_string_part(part)
321
- return part[1] if part.first == :@tstring_content
322
- return evaluate_statements(part[1]).to_s if part.first == :string_embexpr
323
-
324
- unsupported!(part)
325
- end
326
-
327
- def evaluate_symbol(node)
328
- token = node.first == :symbol ? node[1] : node
329
- token_value(token).to_sym
330
- end
331
-
332
- def evaluate_hash(contents)
333
- return {} unless contents
334
- return evaluate_associations(contents[1]) if contents.first == :assoclist_from_args
335
-
336
- unsupported!(contents)
337
- end
338
-
339
- def evaluate_associations(associations)
340
- Array(associations).each_with_object({}) do |association, result|
341
- unsupported!(association) unless association.first == :assoc_new
342
- key_node = association[1]
343
- key = key_node.first == :@label ? key_node[1].sub(/:\z/, "") : evaluate(key_node)
344
- result[key] = evaluate(association[2])
345
- end
346
- end
347
-
348
- def evaluate_variable_token(token)
349
- return evaluate_keyword(token[1]) if token.first == :@kw
350
- lookup(token_value(token))
351
- end
352
-
353
- def evaluate_vcall(token)
354
- name = token_value(token)
355
- value = lookup(name, missing: :sentinel)
356
- return value unless value == :sentinel
357
-
358
- call_function(name, [], nil)
359
- end
360
-
361
- def lookup(name, missing: nil)
362
- @scopes.reverse_each { |scope| return scope[name] if scope.key?(name) }
363
- return missing if missing == :sentinel
364
-
365
- raise CodeModeExecutionError, "Undefined local variable #{name.inspect}"
366
- end
367
-
368
- def assign(target, value)
369
- case target.first
370
- when :var_field
371
- name = token_value(target[1])
372
- scope = @scopes.reverse.find { |candidate| candidate.key?(name) } || @scopes.last
373
- scope[name] = value
374
- when :aref_field
375
- receiver = evaluate(target[1])
376
- arguments = extract_arguments(target[2])
377
- raise CodeModeSyntaxError, "Indexed assignment requires exactly one key" unless arguments.length == 1
378
- raise CodeModeSyntaxError, "Indexed assignment is only allowed on arrays and hashes" unless receiver.is_a?(Array) || receiver.is_a?(Hash)
379
-
380
- receiver[arguments.first] = value
381
- else
382
- unsupported!(target)
383
- end
384
- value
385
- end
386
-
387
- def evaluate_opassign(node)
388
- target = node[1]
389
- operator = token_value(node[2]).sub(/=\z/, "").to_sym
390
- current = read_assignment_target(target)
391
- assign(target, apply_binary(current, operator, evaluate(node[3])))
392
- end
393
-
394
- def read_assignment_target(target)
395
- case target.first
396
- when :var_field then lookup(token_value(target[1]))
397
- when :aref_field then safe_index(evaluate(target[1]), extract_arguments(target[2]))
398
- else unsupported!(target)
399
- end
400
- end
401
-
402
- def evaluate_binary(left_node, operator, right_node)
403
- left = evaluate(left_node)
404
- return left unless truthy?(left) if [:'&&', :and].include?(operator)
405
- return left if truthy?(left) if [:'||', :or].include?(operator)
406
-
407
- apply_binary(left, operator, evaluate(right_node))
408
- end
409
-
410
- def apply_binary(left, operator, right)
411
- preflight_binary_size!(left, operator, right)
412
- value = case operator
413
- when :+ then left + right
414
- when :- then left - right
415
- when :* then left * right
416
- when :/ then left / right
417
- when :% then left % right
418
- when :** then left**right
419
- when :== then left == right
420
- when :!= then left != right
421
- when :< then left < right
422
- when :<= then left <= right
423
- when :> then left > right
424
- when :>= then left >= right
425
- when :<=> then left <=> right
426
- when :'&&', :and, :'||', :or then right
427
- else raise CodeModeSyntaxError, "Unsupported operator #{operator.inspect}"
428
- end
429
- bounded_value!(value)
430
- rescue NoMethodError, TypeError, ArgumentError, ZeroDivisionError => error
431
- raise CodeModeExecutionError, "Invalid #{operator} operation: #{error.message}"
432
- end
433
-
434
- def evaluate_unary(operator, operand_node)
435
- operand = evaluate(operand_node)
436
- case operator
437
- when :! then !truthy?(operand)
438
- when :+@ then +operand
439
- when :-@ then -operand
440
- else raise CodeModeSyntaxError, "Unsupported unary operator #{operator.inspect}"
441
- end
442
- end
443
-
444
- def truthy?(value)
445
- !value.nil? && value != false
446
- end
447
-
448
- def evaluate_if(condition, truthy_statements, alternative)
449
- if truthy?(evaluate(condition))
450
- evaluate_statements(truthy_statements)
451
- elsif alternative&.first == :else
452
- evaluate_statements(alternative[1])
453
- elsif alternative&.first == :elsif
454
- evaluate_if(alternative[1], alternative[2], alternative[3])
455
- end
456
- end
457
-
458
- def evaluate_unless(condition, statements, alternative)
459
- unless truthy?(evaluate(condition))
460
- evaluate_statements(statements)
461
- else
462
- evaluate_statements(alternative[1]) if alternative&.first == :else
463
- end
464
- end
465
-
466
- def evaluate_body_statement(node)
467
- statements, rescue_clause, else_clause, ensure_clause = node.drop(1)
468
- completed = false
469
- result = begin
470
- value = evaluate_statements(statements)
471
- completed = true
472
- value
473
- rescue ReturnSignal, BreakSignal, NextSignal, CodeModeTimeoutError, CodeModeLimitError, CodeModeSyntaxError
474
- raise
475
- rescue StandardError => error
476
- raise unless rescue_clause
477
- evaluate_rescue_clause(rescue_clause, error)
478
- ensure
479
- evaluate_statements(ensure_clause[1]) if ensure_clause&.first == :ensure
480
- end
481
- completed && else_clause ? evaluate_statements(else_clause) : result
482
- end
483
-
484
- def evaluate_rescue_clause(node, error)
485
- unsupported!(node) unless node.first == :rescue
486
- raise CodeModeSyntaxError, "Code Mode rescue does not accept exception classes" if node[1]
487
-
488
- scope = {}
489
- assign_rescue_variable(scope, node[2], error) if node[2]
490
- with_scope(scope) { evaluate_statements(node[3]) }
491
- rescue ReturnSignal, BreakSignal, NextSignal, CodeModeTimeoutError, CodeModeLimitError, CodeModeSyntaxError
492
- raise
493
- rescue StandardError => nested
494
- next_clause = node[4]
495
- raise nested unless next_clause&.first == :rescue
496
- evaluate_rescue_clause(next_clause, nested)
497
- end
498
-
499
- def assign_rescue_variable(scope, target, error)
500
- unsupported!(target) unless target.first == :var_field
501
- scope[token_value(target[1])] = {
502
- "message" => error.message,
503
- "type" => error.class.name
504
- }
505
- end
506
-
507
- def evaluate_rescue_modifier(node)
508
- evaluate(node[1])
509
- rescue ReturnSignal, BreakSignal, NextSignal, CodeModeTimeoutError, CodeModeLimitError, CodeModeSyntaxError
510
- raise
511
- rescue StandardError
512
- evaluate(node[2])
513
- end
514
-
515
- def evaluate_loop(condition, statements, until_condition:)
516
- result = nil
517
- loop do
518
- matches = truthy?(evaluate(condition))
519
- break if until_condition ? matches : !matches
520
-
521
- begin
522
- result = evaluate_statements(statements)
523
- rescue NextSignal => signal
524
- result = signal.value
525
- rescue BreakSignal => signal
526
- return signal.value
527
- end
528
- end
529
- result
530
- end
531
-
532
- def return_value(arguments_node)
533
- arguments = extract_arguments(arguments_node)
534
- arguments.length <= 1 ? arguments.first : arguments
535
- end
536
-
537
- def extract_arguments(node)
538
- return [] if node.nil?
539
- case node.first
540
- when :arg_paren then extract_arguments(node[1])
541
- when :args_add_block then Array(node[1]).map { |argument| evaluate(argument) }
542
- when :args_new then []
543
- when :args_add then extract_arguments(node[1]) + [evaluate(node[2])]
544
- else [evaluate(node)]
545
- end
546
- end
547
-
548
- def build_block(node)
549
- unsupported!(node) unless %i[brace_block do_block].include?(node.first)
550
- parameters = extract_block_parameters(node[1])
551
- Block.new(self, parameters, node[2])
552
- end
553
-
554
- def extract_block_parameters(node)
555
- return [] unless node
556
- params = node.first == :block_var ? node[1] : node
557
- unsupported!(params) unless params&.first == :params
558
- required = Array(params[1])
559
- unsupported!(params) unless params.drop(2).all?(&:nil?)
560
-
561
- required.map { |token| token_value(token) }
562
- end
563
-
564
- def evaluate_call(node, block = nil)
565
- case node.first
566
- when :method_add_arg
567
- invoke_call_target(node[1], extract_arguments(node[2]), block)
568
- when :call
569
- invoke_method(evaluate(node[1]), token_value(node[3]), [], block)
570
- when :fcall
571
- call_function(token_value(node[1]), [], block)
572
- when :vcall
573
- evaluate_vcall(node[1])
574
- when :command
575
- call_function(token_value(node[1]), extract_arguments(node[2]), block)
576
- when :command_call
577
- invoke_method(evaluate(node[1]), token_value(node[3]), extract_arguments(node[4]), block)
578
- else
579
- unsupported!(node)
580
- end
581
- end
582
-
583
- def invoke_call_target(target, arguments, block)
584
- case target.first
585
- when :call
586
- invoke_method(evaluate(target[1]), token_value(target[3]), arguments, block)
587
- when :fcall
588
- call_function(token_value(target[1]), arguments, block)
589
- when :vcall
590
- call_function(token_value(target[1]), arguments, block)
591
- else
592
- unsupported!(target)
593
- end
594
- end
595
-
596
- def call_function(name, arguments, block)
597
- case name
598
- when "puts", "print", "p", "warn"
599
- log(name, arguments)
600
- nil
601
- else
602
- hint = name == "call_tool" ? "; use codemode.call_tool(...)" : ""
603
- raise CodeModeSyntaxError, "Function #{name.inspect} is not available in Code Mode#{hint}"
604
- end
605
- end
606
-
607
- def invoke_method(receiver, method_name, arguments, block)
608
- return invoke_runtime_api(method_name, arguments, block) if receiver.is_a?(RuntimeAPI)
609
- return receiver[method_name] if receiver.is_a?(Hash) && arguments.empty? && !block && receiver.key?(method_name)
610
-
611
- unless SAFE_ENUMERABLE_METHODS.include?(method_name) || SAFE_VALUE_METHODS.include?(method_name)
612
- raise CodeModeSyntaxError, "Method #{method_name.inspect} is not available in Code Mode"
613
- end
614
-
615
- bounded_value!(invoke_safe_value_method(receiver, method_name, arguments, block))
616
- end
617
-
618
- def invoke_runtime_api(method_name, arguments, block)
619
- raise CodeModeSyntaxError, "codemode.#{method_name} does not accept a block" if block
620
-
621
- case method_name
622
- when "call_tool"
623
- require_arity!("codemode.call_tool", arguments, 1..2)
624
- safe_tool_value(@client.call_tool(arguments[0], tool_arguments(arguments[1])))
625
- when "call_tool_stream", "call_tool_streaming"
626
- require_arity!("codemode.#{method_name}", arguments, 1..2)
627
- safe_tool_value(@client.call_tool_streaming(arguments[0], tool_arguments(arguments[1])).to_a)
628
- when "search_tools"
629
- require_arity!("codemode.search_tools", arguments, 1..2)
630
- options = arguments[1].is_a?(Hash) ? arguments[1] : {}
631
- limit = arguments[1].is_a?(Numeric) ? arguments[1] : options.fetch("limit", 10)
632
- safe_tool_value(@client.search_tools(arguments[0].to_s, limit: Integer(limit)).map do |tool|
633
- InterfaceGenerator.tool_descriptor(tool)
634
- end)
635
- when "get_tool_interface"
636
- require_arity!("codemode.get_tool_interface", arguments, 1)
637
- tool = @client.list_tools.find { |candidate| candidate.name == arguments.first.to_s }
638
- raise ToolNotFoundError, "Tool not found: #{arguments.first}" unless tool
639
- safe_tool_value(InterfaceGenerator.tool_descriptor(tool))
640
- when "interfaces"
641
- require_arity!("codemode.interfaces", arguments, 0)
642
- @interfaces.dup
643
- when "get"
644
- runtime_get(arguments)
645
- else
646
- raise CodeModeSyntaxError, "Method #{method_name.inspect} is not available on codemode"
647
- end
648
- end
649
-
650
- def runtime_get(arguments)
651
- require_arity!("codemode.get", arguments, 2..3)
652
- receiver, key, default = arguments
653
- return receiver.fetch(key, default) if receiver.is_a?(Hash)
654
- return receiver.fetch(Integer(key), default) if receiver.is_a?(Array)
655
- return receiver[key] || default if receiver.is_a?(String)
656
-
657
- raise CodeModeExecutionError, "codemode.get requires a hash, array, or string"
658
- rescue IndexError, TypeError
659
- default
660
- end
661
-
662
- def invoke_safe_value_method(receiver, method_name, arguments, block)
663
- return invoke_enumerable(receiver, method_name, arguments, block) if SAFE_ENUMERABLE_METHODS.include?(method_name)
664
-
665
- case method_name
666
- when "length", "size" then require_arity!(method_name, arguments, 0).then { receiver.respond_to?(:length) ? receiver.length : invalid_receiver!(receiver, method_name) }
667
- when "empty?" then require_arity!(method_name, arguments, 0).then { receiver.respond_to?(:empty?) ? receiver.empty? : invalid_receiver!(receiver, method_name) }
668
- when "nil?" then require_arity!(method_name, arguments, 0).then { receiver.nil? }
669
- when "to_s" then require_arity!(method_name, arguments, 0).then { receiver.to_s }
670
- when "inspect" then require_arity!(method_name, arguments, 0).then { receiver.inspect }
671
- when "to_i" then require_arity!(method_name, arguments, 0).then { receiver.to_i }
672
- when "to_f" then require_arity!(method_name, arguments, 0).then { receiver.to_f }
673
- when "to_a" then require_arity!(method_name, arguments, 0).then { safe_to_a(receiver) }
674
- when "to_h" then require_arity!(method_name, arguments, 0).then { receiver.is_a?(Hash) ? receiver.dup : invalid_receiver!(receiver, method_name) }
675
- when "first" then require_arity!(method_name, arguments, 0..1).then { enumerable_value!(receiver, method_name).first(*arguments) }
676
- when "last" then require_arity!(method_name, arguments, 0..1).then { enumerable_value!(receiver, method_name).last(*arguments) }
677
- when "take" then require_arity!(method_name, arguments, 1).then { enumerable_value!(receiver, method_name).take(Integer(arguments[0])) }
678
- when "drop" then require_arity!(method_name, arguments, 1).then { enumerable_value!(receiver, method_name).drop(Integer(arguments[0])) }
679
- when "reverse", "sort", "uniq", "compact", "flatten", "keys", "values", "min", "max", "sum"
680
- require_arity!(method_name, arguments, 0)
681
- safe_zero_argument_method(receiver, method_name)
682
- when "join" then safe_join(require_array!(receiver, method_name), arguments)
683
- when "split" then bounded_value!(require_string!(receiver, method_name).split(*arguments))
684
- when "strip", "downcase", "upcase" then require_string!(receiver, method_name).public_send(method_name)
685
- when "include?" then collection_value!(receiver, method_name).include?(*arguments)
686
- when "start_with?", "end_with?" then require_string!(receiver, method_name).public_send(method_name, *arguments)
687
- when "key?", "has_key?" then require_hash!(receiver, method_name).key?(*arguments)
688
- when "fetch" then indexable_value!(receiver, method_name).fetch(*arguments)
689
- when "dig" then require_hash!(receiver, method_name).dig(*arguments)
690
- when "slice" then indexable_value!(receiver, method_name).slice(*arguments)
691
- when "merge" then bounded_value!(require_hash!(receiver, method_name).merge(require_hash!(arguments.fetch(0), method_name)))
692
- when "abs", "ceil", "floor", "round", "zero?", "positive?", "negative?", "even?", "odd?"
693
- require_numeric!(receiver, method_name).public_send(method_name, *arguments)
694
- else
695
- invalid_receiver!(receiver, method_name)
696
- end
697
- rescue NoMethodError, ArgumentError, TypeError, IndexError, KeyError => error
698
- raise CodeModeExecutionError, "Invalid #{method_name} call: #{error.message}"
699
- end
700
-
701
- def invoke_enumerable(receiver, method_name, arguments, block)
702
- enumerable = enumerable_value!(receiver, method_name)
703
- case method_name
704
- when "map", "collect" then require_block!(method_name, block).then { enumerable.map { |*items| tick!; block.call(*block_arguments(enumerable, items)) } }
705
- when "select", "filter" then require_block!(method_name, block).then { enumerable.select { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
706
- when "reject" then require_block!(method_name, block).then { enumerable.reject { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
707
- when "each"
708
- require_block!(method_name, block)
709
- enumerable.each { |*items| tick!; block.call(*block_arguments(enumerable, items)) }
710
- receiver
711
- when "find" then require_block!(method_name, block).then { enumerable.find { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
712
- when "any?" then require_block!(method_name, block).then { enumerable.any? { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
713
- when "all?" then require_block!(method_name, block).then { enumerable.all? { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
714
- when "none?" then require_block!(method_name, block).then { enumerable.none? { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
715
- when "count"
716
- block ? enumerable.count { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } : enumerable.count(*arguments)
717
- when "sort_by" then require_block!(method_name, block).then { enumerable.sort_by { |*items| tick!; block.call(*block_arguments(enumerable, items)) } }
718
- when "reduce", "inject"
719
- reduce_enumerable(enumerable, arguments, require_block!(method_name, block))
720
- else invalid_receiver!(receiver, method_name)
721
- end
722
- rescue BreakSignal => signal
723
- signal.value
724
- end
725
-
726
- def block_arguments(enumerable, items)
727
- return items.first if enumerable.is_a?(Hash) && items.length == 1 && items.first.is_a?(Array)
728
-
729
- items
730
- end
731
-
732
- def reduce_enumerable(enumerable, arguments, block)
733
- values = enumerable.to_a
734
- if arguments.empty?
735
- return nil if values.empty?
736
- accumulator = values.shift
737
- else
738
- require_arity!("reduce", arguments, 1)
739
- accumulator = arguments.first
740
- end
741
- values.each { |item| tick!; accumulator = block.call(accumulator, item) }
742
- accumulator
743
- end
744
-
745
- def safe_zero_argument_method(receiver, method_name)
746
- allowed = case receiver
747
- when Array then %w[reverse sort uniq compact flatten min max sum]
748
- when Hash then %w[keys values]
749
- when Range then %w[min max sum]
750
- else []
751
- end
752
- invalid_receiver!(receiver, method_name) unless allowed.include?(method_name)
753
- bounded_value!(receiver.public_send(method_name))
754
- end
755
-
756
- def safe_to_a(receiver)
757
- return receiver.dup if receiver.is_a?(Array)
758
- if receiver.is_a?(Range)
759
- if receiver.begin.is_a?(Integer) && receiver.end.is_a?(Integer)
760
- size = receiver.end - receiver.begin + (receiver.exclude_end? ? 0 : 1)
761
- raise CodeModeLimitError, "Range contains too many values" if size > MAX_VALUE_ITEMS
762
- end
763
- return bounded_value!(receiver.to_a)
764
- end
765
- return receiver.to_a if receiver.is_a?(Hash)
766
-
767
- invalid_receiver!(receiver, "to_a")
768
- end
769
-
770
- def safe_index(receiver, arguments)
771
- raise CodeModeSyntaxError, "Indexing requires one or two arguments" unless (1..2).cover?(arguments.length)
772
- raise CodeModeExecutionError, "#{receiver.class} values cannot be indexed" unless receiver.is_a?(Array) || receiver.is_a?(Hash) || receiver.is_a?(String)
773
-
774
- receiver[*arguments]
775
- rescue TypeError, IndexError => error
776
- raise CodeModeExecutionError, "Invalid index: #{error.message}"
777
- end
778
-
779
- def enumerable_value!(receiver, method_name)
780
- return receiver if receiver.is_a?(Array) || receiver.is_a?(Hash) || receiver.is_a?(Range)
781
-
782
- invalid_receiver!(receiver, method_name)
783
- end
784
-
785
- def require_string!(value, method_name)
786
- return value if value.is_a?(String)
787
-
788
- invalid_receiver!(value, method_name)
789
- end
790
-
791
- def require_hash!(value, method_name)
792
- return value if value.is_a?(Hash)
793
-
794
- invalid_receiver!(value, method_name)
795
- end
796
-
797
- def require_array!(value, method_name)
798
- return value if value.is_a?(Array)
799
-
800
- invalid_receiver!(value, method_name)
801
- end
802
-
803
- def collection_value!(value, method_name)
804
- return value if value.is_a?(Array) || value.is_a?(Hash) || value.is_a?(String) || value.is_a?(Range)
805
-
806
- invalid_receiver!(value, method_name)
807
- end
808
-
809
- def indexable_value!(value, method_name)
810
- return value if value.is_a?(Array) || value.is_a?(Hash) || value.is_a?(String)
811
-
812
- invalid_receiver!(value, method_name)
813
- end
814
-
815
- def require_numeric!(value, method_name)
816
- return value if value.is_a?(Numeric)
817
-
818
- invalid_receiver!(value, method_name)
819
- end
820
-
821
- def safe_join(receiver, arguments)
822
- separator = arguments.empty? ? "" : arguments.first.to_s
823
- estimated = receiver.sum { |value| value.to_s.bytesize }
824
- estimated += separator.bytesize * [receiver.length - 1, 0].max
825
- raise CodeModeLimitError, "String result exceeds #{MAX_VALUE_BYTES} bytes" if estimated > MAX_VALUE_BYTES
826
-
827
- receiver.join(*arguments)
828
- end
829
-
830
- def preflight_binary_size!(left, operator, right)
831
- if operator == :** && right.is_a?(Integer)
832
- bases = left.is_a?(Rational) ? [left.numerator, left.denominator] : [left]
833
- bases.each { |base| preflight_integer_power!(base, right) if base.is_a?(Integer) }
834
- elsif operator == :* && left.is_a?(Integer) && right.is_a?(Integer)
835
- if !left.zero? && !right.zero? && left.abs.bit_length + right.abs.bit_length > MAX_INTEGER_BITS
836
- raise CodeModeLimitError, "Integer product exceeds #{MAX_VALUE_BYTES} bytes"
837
- end
838
- elsif operator == :+ && left.is_a?(String) && right.is_a?(String)
839
- raise CodeModeLimitError, "String result exceeds #{MAX_VALUE_BYTES} bytes" if left.bytesize + right.bytesize > MAX_VALUE_BYTES
840
- elsif operator == :+ && left.is_a?(Array) && right.is_a?(Array)
841
- raise CodeModeLimitError, "Collection result exceeds #{MAX_VALUE_ITEMS} items" if left.length + right.length > MAX_VALUE_ITEMS
842
- elsif operator == :* && right.is_a?(Integer) && (left.is_a?(String) || left.is_a?(Array))
843
- size = left.is_a?(String) ? left.bytesize : left.length
844
- limit = left.is_a?(String) ? MAX_VALUE_BYTES : MAX_VALUE_ITEMS
845
- raise CodeModeLimitError, "Repeated value exceeds Code Mode limits" if right.positive? && size > limit / right
846
- end
847
- end
848
-
849
- def preflight_integer_power!(base, exponent)
850
- magnitude = base.abs
851
- return if magnitude <= 1 || exponent.zero?
852
-
853
- # Use an upper bound without constructing the potentially enormous result.
854
- bits = magnitude.bit_length
855
- bits -= 1 if (magnitude & (magnitude - 1)).zero?
856
- if exponent.abs > (MAX_INTEGER_BITS - 1) / bits
857
- raise CodeModeLimitError, "Integer power exceeds #{MAX_VALUE_BYTES} bytes"
858
- end
859
- end
860
-
861
- def numeric_bytes(value)
862
- case value
863
- when Integer then [(value.abs.bit_length + 7) / 8, 1].max
864
- when Rational then numeric_bytes(value.numerator) + numeric_bytes(value.denominator)
865
- when Complex then numeric_bytes(value.real) + numeric_bytes(value.imaginary)
866
- when Float
867
- raise CodeModeLimitError, "Numeric result must be finite" unless value.finite?
868
- 8
869
- else value.to_s.bytesize
870
- end
871
- end
872
-
873
- def bounded_value!(value)
874
- if value.is_a?(Numeric) && numeric_bytes(value) > MAX_VALUE_BYTES
875
- raise CodeModeLimitError, "Numeric result exceeds #{MAX_VALUE_BYTES} bytes"
876
- end
877
- if value.is_a?(String) && value.bytesize > MAX_VALUE_BYTES
878
- raise CodeModeLimitError, "String result exceeds #{MAX_VALUE_BYTES} bytes"
879
- end
880
- if (value.is_a?(Array) || value.is_a?(Hash)) && value.length > MAX_VALUE_ITEMS
881
- raise CodeModeLimitError, "Collection result exceeds #{MAX_VALUE_ITEMS} items"
882
- end
883
- value
884
- end
885
-
886
- def invalid_receiver!(receiver, method_name)
887
- raise CodeModeExecutionError, "#{method_name} is not supported for #{receiver.class} values"
888
- end
889
-
890
- def require_block!(method_name, block)
891
- raise CodeModeSyntaxError, "#{method_name} requires a block" unless block
892
- block
893
- end
894
-
895
- def require_arity!(name, arguments, expected)
896
- valid = expected.is_a?(Range) ? expected.cover?(arguments.length) : arguments.length == expected
897
- return true if valid
898
-
899
- raise CodeModeSyntaxError, "#{name} received #{arguments.length} arguments"
900
- end
901
-
902
- def tool_arguments(value)
903
- return {} if value.nil?
904
- raise CodeModeSyntaxError, "Tool arguments must be a hash" unless value.is_a?(Hash)
905
-
906
- safe_tool_value(value)
907
- end
908
-
909
- def safe_tool_value(value, depth = 0, budget = { items: MAX_VALUE_ITEMS, bytes: MAX_VALUE_BYTES })
910
- raise CodeModeLimitError, "Tool result nesting is too deep" if depth > 64
911
- budget[:items] -= 1
912
- raise CodeModeLimitError, "Tool result contains too many values" if budget[:items].negative?
913
-
914
- case value
915
- when String
916
- budget[:bytes] -= value.bytesize
917
- raise CodeModeLimitError, "Tool result contains too much string data" if budget[:bytes].negative?
918
- value.dup
919
- when Numeric
920
- budget[:bytes] -= numeric_bytes(value)
921
- raise CodeModeLimitError, "Tool result contains too much numeric data" if budget[:bytes].negative?
922
- value
923
- when nil, true, false
924
- value
925
- when Array
926
- value.map { |item| safe_tool_value(item, depth + 1, budget) }
927
- when Hash
928
- value.each_with_object({}) do |(key, item), result|
929
- safe_key = key.is_a?(Symbol) ? key : key.to_s
930
- result[safe_key] = safe_tool_value(item, depth + 1, budget)
931
- end
932
- else
933
- raise CodeModeExecutionError, "Tool returned unsupported #{value.class} value"
934
- end
935
- end
936
-
937
- def log(kind, values)
938
- rendered = values.map { |value| kind == "p" ? value.inspect : value.to_s }.join(kind == "print" ? "" : " ")
939
- rendered = "[WARN] #{rendered}" if kind == "warn"
940
- bytes = rendered.bytesize
941
- raise CodeModeLimitError, "Code Mode log limit exceeded" if @log_bytes + bytes > MAX_LOG_BYTES
942
-
943
- @logs << rendered
944
- @log_bytes += bytes
945
- end
946
-
947
- end
948
- end
949
-
950
- class CodeModeUtcpClient < Client
951
- AGENT_PROMPT_TEMPLATE = <<~PROMPT.freeze
952
- Use `codemode.search_tools` before writing a Code Mode program when tool names are unknown.
953
- Call tools as `codemode.call_tool("manual.tool", key: value)`. Use the exact qualified name.
954
- Code Mode accepts a constrained Ruby subset: local variables, JSON-like literals, arithmetic,
955
- conditionals, bounded loops, and common Array/Hash/String transforms. The final expression or an
956
- explicit `return` is the result. Use `puts`, `print`, `p`, or `warn` for captured logs. The values
957
- `codemode.interfaces` and `codemode.get_tool_interface("manual.tool")` describe registered tools. Filesystem,
958
- process, constants, imports, reflection, eval, and direct network access are unavailable.
959
- PROMPT
960
-
961
- def call_tool_chain(code, timeout_value = nil, timeout: nil, max_steps: CodeMode::DEFAULT_MAX_STEPS)
962
- selected_timeout = timeout || timeout_value || CodeMode::DEFAULT_TIMEOUT
963
- CodeMode.new(self).execute(code, timeout: selected_timeout, max_steps: max_steps)
964
- end
965
-
966
- def get_all_tools_ruby_interfaces
967
- CodeMode.new(self).interfaces
968
- end
969
-
970
- def get_tool_interface(name)
971
- CodeMode.new(self).tool_interface(name)
972
- end
973
62
  end
974
-
975
- CodeModeClient = CodeModeUtcpClient
976
63
  end
64
+
65
+ require_relative "code_mode/interface_generator"
66
+ require_relative "code_mode/evaluator"
67
+ require_relative "code_mode/expressions"
68
+ require_relative "code_mode/control_flow"
69
+ require_relative "code_mode/runtime"
70
+ require_relative "code_mode/value_methods"
71
+ require_relative "code_mode/value_limits"
72
+ require_relative "code_mode/client"