runtype 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Runtype
4
+ # A tool the model can call that executes in this process. Pair a handler
5
+ # with the schema the server announces to the model, then pass a map of
6
+ # these to {Runtype::Client#run_with_local_tools}.
7
+ #
8
+ # save_goals = Runtype::LocalTool.new(
9
+ # description: "Save the patient's stated goals",
10
+ # parameters_schema: { type: "object", properties: { goals: { type: "array", items: { type: "string" } } } }
11
+ # ) { |args| Goals.save(args["goals"]); { ok: true } }
12
+ #
13
+ # A bare callable (a Proc, lambda, or Method) is also accepted where a
14
+ # LocalTool is, when the tool is already declared on the saved agent as a
15
+ # `toolType: "local"` runtime tool and only the handler is needed here.
16
+ class LocalTool
17
+ # @return [String]
18
+ attr_reader :description
19
+
20
+ # @return [Hash] JSON Schema for the tool input; must have `type: "object"`.
21
+ attr_reader :parameters_schema
22
+
23
+ # @return [#call] receives the tool arguments as a Hash with String keys.
24
+ attr_reader :execute
25
+
26
+ # @return [String, nil] `"sdk"` (default) or `"webmcp"`.
27
+ attr_reader :origin
28
+
29
+ # @return [String, nil]
30
+ attr_reader :page_origin
31
+
32
+ # @return [Boolean] when true the server wraps this tool's output as
33
+ # untrusted content before the model reads it.
34
+ attr_reader :untrusted_content_hint
35
+
36
+ # @param description [String]
37
+ # @param parameters_schema [Hash]
38
+ # @param execute [#call, nil] the handler; alternatively pass a block.
39
+ # @param origin [String, Symbol, nil]
40
+ # @param page_origin [String, nil]
41
+ # @param untrusted_content_hint [Boolean]
42
+ def initialize(description:, parameters_schema:, execute: nil, origin: nil, page_origin: nil,
43
+ untrusted_content_hint: false, &block)
44
+ handler = execute || block
45
+ raise ArgumentError, "LocalTool needs an execute: callable or a block" unless handler.respond_to?(:call)
46
+
47
+ @description = description
48
+ @parameters_schema = parameters_schema
49
+ @execute = handler
50
+ @origin = origin&.to_s
51
+ @page_origin = page_origin
52
+ @untrusted_content_hint = untrusted_content_hint ? true : false
53
+ freeze
54
+ end
55
+
56
+ # The `clientTools[]` entry announcing this tool for one dispatch.
57
+ #
58
+ # @param name [String]
59
+ # @return [Hash]
60
+ def to_client_tool(name)
61
+ entry = { name: name, description: description, parametersSchema: parameters_schema }
62
+ entry[:origin] = origin if origin
63
+ entry[:pageOrigin] = page_origin if page_origin
64
+ entry[:untrustedContentHint] = true if untrusted_content_hint
65
+ entry
66
+ end
67
+ end
68
+
69
+ # Internals of the local-tool pause/resume loop. Not part of the public API.
70
+ module LocalTools
71
+ # The server stamps this reason on every await it expects the dispatching
72
+ # client to answer with a tool output. Any other reason (an approval, an
73
+ # MCP elicitation, an A2A input request, a detached or auto-resuming
74
+ # durable pause) belongs to someone else.
75
+ LOCAL_TOOL_REASON = "local_tool_required"
76
+
77
+ AWAIT_TYPES = %w[await flow_await step_await].freeze
78
+
79
+ # One paused local tool call.
80
+ PausedCall = Struct.new(:tool_name, :tool_call_id, :execution_id, :parameters, keyword_init: true) do
81
+ # The `toolOutputs` key the server resumes on: the per-call id when the
82
+ # server sent one (required to tell parallel calls to the same tool
83
+ # apart), else the tool name.
84
+ def output_key
85
+ tool_call_id || tool_name
86
+ end
87
+ end
88
+
89
+ # The handler map, normalized: name => {LocalTool} or bare callable.
90
+ class Registry
91
+ # @param tools [Hash{String, Symbol => Runtype::LocalTool, #call}]
92
+ def initialize(tools)
93
+ raise ArgumentError, "run_with_local_tools expects a Hash of tool name => handler" unless tools.is_a?(Hash)
94
+
95
+ @tools = tools.to_h do |name, tool|
96
+ unless tool.is_a?(LocalTool) || tool.respond_to?(:call)
97
+ raise ArgumentError, "local tool #{name.inspect} must be a Runtype::LocalTool or respond to #call"
98
+ end
99
+
100
+ [name.to_s, tool]
101
+ end
102
+ end
103
+
104
+ # @return [Array<Hash>] `clientTools[]` entries for every schema-carrying tool.
105
+ def client_tools
106
+ @tools.filter_map { |name, tool| tool.to_client_tool(name) if tool.is_a?(LocalTool) }
107
+ end
108
+
109
+ # Looks a handler up by the name the server emitted, then by the bare
110
+ # name (the server prefixes page-discovered tools with `webmcp:`).
111
+ #
112
+ # @param emitted_name [String]
113
+ # @return [#call, nil]
114
+ def handler_for(emitted_name)
115
+ tool = @tools[emitted_name] || @tools[emitted_name.delete_prefix("webmcp:")]
116
+ tool.is_a?(LocalTool) ? tool.execute : tool
117
+ end
118
+
119
+ # Runs every paused call and returns the `toolOutputs` body for one
120
+ # resume. A batch of several calls runs concurrently, one thread per
121
+ # call, as the TypeScript SDK does: the server waits for the whole batch,
122
+ # so the leg costs the slowest handler, not the sum.
123
+ #
124
+ # @param batch [Array<PausedCall>]
125
+ # @return [Hash{String => Object}]
126
+ # @raise [Runtype::LocalToolError]
127
+ def execute(batch)
128
+ return batch.to_h { |call| [call.output_key, run(call)] } if batch.size <= 1
129
+
130
+ threads = batch.map do |call|
131
+ Thread.new do
132
+ Thread.current.report_on_exception = false # re-raised from #value below
133
+ [call.output_key, run(call)]
134
+ end
135
+ end
136
+ threads.to_h(&:value)
137
+ end
138
+
139
+ private
140
+
141
+ # @param call [PausedCall]
142
+ # @return [Object] the handler's output.
143
+ # @raise [Runtype::LocalToolError]
144
+ def run(call)
145
+ handler = handler_for(call.tool_name)
146
+ unless handler
147
+ raise Runtype::LocalToolError.new(
148
+ "Local tool #{call.tool_name.inspect} required but not provided in the tools map",
149
+ tool_name: call.tool_name
150
+ )
151
+ end
152
+
153
+ handler.call(call.parameters || {})
154
+ rescue Runtype::LocalToolError
155
+ raise
156
+ rescue StandardError => e
157
+ raise Runtype::LocalToolError.new(
158
+ "Error executing local tool #{call.tool_name.inspect}: #{e.message}", tool_name: call.tool_name
159
+ )
160
+ end
161
+ end
162
+
163
+ # Folds the `await` frames of one execution leg into the batch of local
164
+ # tool calls the server is waiting on. A single model turn can request
165
+ # several parallel calls; the server emits one `await` per call and its
166
+ # resume endpoint accepts every output in one request.
167
+ #
168
+ # One call can arrive as more than one frame (a compatibility
169
+ # `flow_await` keyed by name, then the unified `await` carrying the call
170
+ # id), so a later frame with a call id adopts the name-keyed entry rather
171
+ # than opening a second one.
172
+ class PauseCollector
173
+ # @return [String, nil]
174
+ attr_reader :execution_id
175
+
176
+ def initialize
177
+ @paused = {}
178
+ @execution_id = nil
179
+ end
180
+
181
+ # @param event [Runtype::Streaming::Event, Hash]
182
+ # @return [void]
183
+ def observe(event)
184
+ data = event.respond_to?(:to_h) ? event.to_h : event
185
+ return unless data.is_a?(Hash)
186
+
187
+ @execution_id ||= present(data["executionId"])
188
+ return unless local_tool_await?(data)
189
+
190
+ name = data["toolName"]
191
+ call_id = present(data["toolCallId"]) || present(data["modelToolCallId"])
192
+ key = call_id && @paused.key?(call_id) ? call_id : name
193
+ previous = @paused.delete(key)
194
+ @paused[call_id || name] = PausedCall.new(
195
+ tool_name: name,
196
+ tool_call_id: call_id || previous&.tool_call_id,
197
+ execution_id: present(data["executionId"]) || previous&.execution_id || @execution_id,
198
+ parameters: data.key?("parameters") ? data["parameters"] : previous&.parameters
199
+ )
200
+ end
201
+
202
+ # Folds a non-streaming `pausedReason` (one call; its `toolId` is the
203
+ # call id).
204
+ #
205
+ # @param paused_reason [Hash]
206
+ # @return [void]
207
+ def observe_paused_reason(paused_reason)
208
+ return unless paused_reason.is_a?(Hash) && paused_reason["type"] == "local_action"
209
+
210
+ observe(paused_reason.merge("type" => "await", "toolCallId" => paused_reason["toolId"]))
211
+ end
212
+
213
+ # @return [Array<PausedCall>]
214
+ def batch
215
+ @paused.values
216
+ end
217
+
218
+ # @return [Boolean]
219
+ def paused?
220
+ !@paused.empty?
221
+ end
222
+
223
+ # The execution every call in the batch belongs to.
224
+ #
225
+ # @return [String]
226
+ # @raise [Runtype::LocalToolError] when no frame carried an execution id.
227
+ def batch_execution_id
228
+ id = @paused.values.map(&:execution_id).compact.first || @execution_id
229
+ raise Runtype::LocalToolError, "The execution paused on a local tool without an executionId" if id.nil?
230
+
231
+ id
232
+ end
233
+
234
+ private
235
+
236
+ # An await this process answers with a tool output: a named tool, no
237
+ # approval or elicitation attached, and either the server's explicit
238
+ # local-tool reason or (on compatibility frames) no reason at all.
239
+ #
240
+ # @param data [Hash]
241
+ # @return [Boolean]
242
+ def local_tool_await?(data)
243
+ return false unless AWAIT_TYPES.include?(data["type"])
244
+ return false unless present(data["toolName"])
245
+ return false if data["approvalId"] || data["elicitation"]
246
+
247
+ reason = present(data["awaitReason"])
248
+ reason.nil? || reason == LOCAL_TOOL_REASON
249
+ end
250
+
251
+ # @param value [Object]
252
+ # @return [String, nil] the value when it is a non-empty String.
253
+ def present(value)
254
+ value.is_a?(String) && !value.empty? ? value : nil
255
+ end
256
+ end
257
+ end
258
+ end
@@ -8,7 +8,7 @@ module Runtype
8
8
  # generator). The generated barrel does not require that file, so nothing
9
9
  # collides today, but re-declaring it here would make the overlay's version
10
10
  # depend on generator require order.
11
- SDK_VERSION = "0.1.0"
11
+ SDK_VERSION = "0.2.0"
12
12
 
13
13
  # Outbound User-Agent identifying the SDK kind, version, and language, e.g.
14
14
  # "runtype-sdk/0.1.0 (ruby)". The `runtype-sdk/` prefix is what the API's
data/lib/runtype.rb CHANGED
@@ -18,7 +18,7 @@
18
18
  # require "runtype"
19
19
  #
20
20
  # client = Runtype::Client.new(api_key: ENV["RUNTYPE_API_KEY"])
21
- # client.dispatch_stream(agent: { id: "agt_123" }, messages: [...]) do |event|
21
+ # client.dispatch_stream(agent: { agentId: "agent_123" }, messages: [...]) do |event|
22
22
  # print event["delta"] if event.type == "text_delta"
23
23
  # end
24
24
  require_relative "../generated/lib/runtype"
@@ -30,6 +30,11 @@ require_relative "runtype/streaming/line_decoder"
30
30
  require_relative "runtype/streaming/event_parser"
31
31
  require_relative "runtype/streaming/connection"
32
32
  require_relative "runtype/streaming/event_stream"
33
+ require_relative "runtype/canonical_json"
34
+ require_relative "runtype/agent_definition"
35
+ require_relative "runtype/agent_ensure"
36
+ require_relative "runtype/local_tools"
37
+ require_relative "runtype/local_tool_runner"
33
38
  require_relative "runtype/client"
34
39
 
35
40
  module Runtype
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: runtype
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Runtype Labs
@@ -1430,6 +1430,7 @@ files:
1430
1430
  - generated/lib/runtype/model_configs/types/post_v1model_configs_request_settings_custom_model.rb
1431
1431
  - generated/lib/runtype/model_configs/types/post_v1model_configs_request_settings_custom_model_executor_config.rb
1432
1432
  - generated/lib/runtype/model_configs/types/post_v1model_configs_request_settings_custom_model_executor_provider.rb
1433
+ - generated/lib/runtype/model_configs/types/post_v1model_configs_request_settings_custom_model_reasoning.rb
1433
1434
  - generated/lib/runtype/model_configs/types/post_v1model_configs_response.rb
1434
1435
  - generated/lib/runtype/model_configs/types/post_v1model_configs_response_configuration_status.rb
1435
1436
  - generated/lib/runtype/model_configs/types/post_v1model_configs_response_key_status.rb
@@ -1443,6 +1444,7 @@ files:
1443
1444
  - generated/lib/runtype/model_configs/types/put_v1model_configs_id_request_settings_custom_model.rb
1444
1445
  - generated/lib/runtype/model_configs/types/put_v1model_configs_id_request_settings_custom_model_executor_config.rb
1445
1446
  - generated/lib/runtype/model_configs/types/put_v1model_configs_id_request_settings_custom_model_executor_provider.rb
1447
+ - generated/lib/runtype/model_configs/types/put_v1model_configs_id_request_settings_custom_model_reasoning.rb
1446
1448
  - generated/lib/runtype/model_configs/types/put_v1model_configs_id_response.rb
1447
1449
  - generated/lib/runtype/model_configs/types/put_v1model_configs_id_response_configuration_status.rb
1448
1450
  - generated/lib/runtype/model_configs/types/put_v1model_configs_id_response_key_status.rb
@@ -1778,6 +1780,7 @@ files:
1778
1780
  - generated/lib/runtype/provider_keys/types/post_v1provider_keys_id_discover_models_response_models_item.rb
1779
1781
  - generated/lib/runtype/provider_keys/types/post_v1provider_keys_id_sync_models_request.rb
1780
1782
  - generated/lib/runtype/provider_keys/types/post_v1provider_keys_id_sync_models_request_models_item.rb
1783
+ - generated/lib/runtype/provider_keys/types/post_v1provider_keys_id_sync_models_request_models_item_reasoning.rb
1781
1784
  - generated/lib/runtype/provider_keys/types/post_v1provider_keys_id_sync_models_response.rb
1782
1785
  - generated/lib/runtype/provider_keys/types/post_v1provider_keys_request.rb
1783
1786
  - generated/lib/runtype/provider_keys/types/post_v1provider_keys_request_provider.rb
@@ -2732,8 +2735,13 @@ files:
2732
2735
  - generated/lib/runtype/webhook_management/types/put_v1products_id_surfaces_surface_id_webhook_routing_request.rb
2733
2736
  - generated/lib/runtype/webhook_management/types/put_v1products_id_surfaces_surface_id_webhook_routing_response.rb
2734
2737
  - lib/runtype.rb
2738
+ - lib/runtype/agent_definition.rb
2739
+ - lib/runtype/agent_ensure.rb
2740
+ - lib/runtype/canonical_json.rb
2735
2741
  - lib/runtype/client.rb
2736
2742
  - lib/runtype/errors.rb
2743
+ - lib/runtype/local_tool_runner.rb
2744
+ - lib/runtype/local_tools.rb
2737
2745
  - lib/runtype/sdk_version.rb
2738
2746
  - lib/runtype/streaming/connection.rb
2739
2747
  - lib/runtype/streaming/event.rb
@@ -2765,7 +2773,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
2765
2773
  - !ruby/object:Gem::Version
2766
2774
  version: '0'
2767
2775
  requirements: []
2768
- rubygems_version: 4.0.16
2776
+ rubygems_version: 3.6.9
2769
2777
  specification_version: 4
2770
2778
  summary: Ruby SDK for the Runtype AI product platform
2771
2779
  test_files: []