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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +92 -6
- data/generated/lib/runtype/model_configs/types/post_v1model_configs_request_settings_custom_model.rb +2 -0
- data/generated/lib/runtype/model_configs/types/post_v1model_configs_request_settings_custom_model_reasoning.rb +13 -0
- data/generated/lib/runtype/model_configs/types/put_v1model_configs_id_request_settings_custom_model.rb +2 -0
- data/generated/lib/runtype/model_configs/types/put_v1model_configs_id_request_settings_custom_model_reasoning.rb +13 -0
- data/generated/lib/runtype/provider_keys/types/post_v1provider_keys_id_sync_models_request_models_item.rb +2 -0
- data/generated/lib/runtype/provider_keys/types/post_v1provider_keys_id_sync_models_request_models_item_reasoning.rb +13 -0
- data/generated/lib/runtype.rb +3 -0
- data/lib/runtype/agent_definition.rb +331 -0
- data/lib/runtype/agent_ensure.rb +110 -0
- data/lib/runtype/canonical_json.rb +89 -0
- data/lib/runtype/client.rb +64 -28
- data/lib/runtype/errors.rb +91 -1
- data/lib/runtype/local_tool_runner.rb +121 -0
- data/lib/runtype/local_tools.rb +258 -0
- data/lib/runtype/sdk_version.rb +1 -1
- data/lib/runtype.rb +6 -1
- metadata +10 -2
|
@@ -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
|
data/lib/runtype/sdk_version.rb
CHANGED
|
@@ -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.
|
|
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: {
|
|
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.
|
|
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:
|
|
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: []
|