turnkit 0.3.0 → 0.4.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 +6 -0
- data/README.md +55 -0
- data/lib/turnkit/adapters/ruby_llm.rb +58 -0
- data/lib/turnkit/client.rb +4 -0
- data/lib/turnkit/image_result.rb +51 -0
- data/lib/turnkit/image_tool.rb +30 -0
- data/lib/turnkit/message.rb +5 -1
- data/lib/turnkit/message_projection.rb +11 -0
- data/lib/turnkit/output_policy.rb +9 -0
- data/lib/turnkit/result.rb +12 -0
- data/lib/turnkit/tool_runner.rb +3 -0
- data/lib/turnkit/turn.rb +71 -0
- data/lib/turnkit/version.rb +1 -1
- data/lib/turnkit.rb +7 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2b34c1c2760df56b69b055246a8afcccaec42c5493cce13ca30303c9c7e1e809
|
|
4
|
+
data.tar.gz: d3ff91766661eddbd6bb8eefba627ceaf13d8d2b2daa673781819cfe71b41a79
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 02a3e913bcfa72bc5fcbd8e18faaf60def5f62b75981c7e72220e994b7874df2a67fd306dd01cde4f9c3a0d5082aa90271bf210c5c4c5876a5015812077df79f
|
|
7
|
+
data.tar.gz: 0d6d917eeeef41a3a624a31daccdc60f28e4c121b8959ad0ab3d57301be63427e83f2a349a920fa9707bd9f5e2fa2310721f72a8151fbbb44c26a3ec75466259
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 - 2026-06-19
|
|
4
|
+
|
|
5
|
+
- Add first-class image generation with `Turn#paint`, `TurnKit.paint`, and `TurnKit::ImageTool`.
|
|
6
|
+
- Persist generated images as durable image messages with normalized metadata, usage, cost, and event callbacks.
|
|
7
|
+
- Add image output policy support and a `generate-image` CLI smoke example for Gemini 16:9 image generation.
|
|
8
|
+
|
|
3
9
|
## 0.3.0 - 2026-06-10
|
|
4
10
|
|
|
5
11
|
- Make the task-runtime API skills-first and intentionally breaking: `max_spend` is the only spend-limit name and output validation is exposed as `output_policy` / `policy_audit`.
|
data/README.md
CHANGED
|
@@ -459,6 +459,61 @@ puts turn.output_text
|
|
|
459
459
|
|
|
460
460
|
Rely on TurnKit to validate tools and model-provided arguments.
|
|
461
461
|
|
|
462
|
+
### Images
|
|
463
|
+
|
|
464
|
+
Generate images inside a durable turn with `turn.paint`. The image call uses the
|
|
465
|
+
configured client adapter, records usage and cost on the turn, persists an image
|
|
466
|
+
message, and emits `image.requested` / `image.completed` events.
|
|
467
|
+
|
|
468
|
+
```ruby
|
|
469
|
+
image = turn.paint(
|
|
470
|
+
"Create a 16:9 editorial header image for the article.",
|
|
471
|
+
model: "gemini-3-pro-image-preview",
|
|
472
|
+
provider: :gemini,
|
|
473
|
+
size: "1024x576",
|
|
474
|
+
metadata: { article_id: article.id }
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
image.url # provider-hosted URL when returned
|
|
478
|
+
image.to_blob # generated bytes for base64 responses, or fetched URL bytes
|
|
479
|
+
image.mime_type # "image/png"
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
For reusable workflow steps, subclass `TurnKit::ImageTool`:
|
|
483
|
+
|
|
484
|
+
```ruby
|
|
485
|
+
class GenerateHeaderImage < TurnKit::ImageTool
|
|
486
|
+
description "Generate an article header image."
|
|
487
|
+
parameter :title, :string, required: true
|
|
488
|
+
|
|
489
|
+
model "gemini-3-pro-image-preview"
|
|
490
|
+
provider :gemini
|
|
491
|
+
size "1024x576"
|
|
492
|
+
|
|
493
|
+
def prompt(title:)
|
|
494
|
+
"Create a 16:9 editorial header image for #{title}."
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
Rails apps can attach generated images from the event stream without TurnKit
|
|
500
|
+
taking a dependency on Active Storage:
|
|
501
|
+
|
|
502
|
+
```ruby
|
|
503
|
+
TurnKit.on_event = ->(event) do
|
|
504
|
+
next unless event.type == "image.completed"
|
|
505
|
+
|
|
506
|
+
image = TurnKit::ImageResult.from_h(event.payload.fetch(:image))
|
|
507
|
+
Article.find(event.payload.dig(:metadata, :article_id)).header_image.attach(
|
|
508
|
+
io: StringIO.new(image.to_blob),
|
|
509
|
+
filename: "header.png",
|
|
510
|
+
content_type: image.mime_type
|
|
511
|
+
)
|
|
512
|
+
end
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
Require an image before completion with `TurnKit::OutputPolicy.require_image`.
|
|
516
|
+
|
|
462
517
|
### Structured Output
|
|
463
518
|
|
|
464
519
|
Define a schema:
|
|
@@ -41,6 +41,23 @@ module TurnKit
|
|
|
41
41
|
normalize_response(response, model: model)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
def paint(prompt:, model:, provider: nil, size: nil, assume_model_exists: nil, input_images: nil, mask: nil, params: {}, metadata: nil, on_event: nil)
|
|
45
|
+
require "ruby_llm"
|
|
46
|
+
|
|
47
|
+
configure_from_environment
|
|
48
|
+
kwargs = paint_kwargs(
|
|
49
|
+
model: model,
|
|
50
|
+
provider: provider,
|
|
51
|
+
assume_model_exists: assume_model_exists || false,
|
|
52
|
+
size: size || "1024x1024",
|
|
53
|
+
with: input_images,
|
|
54
|
+
mask: mask,
|
|
55
|
+
params: params || {}
|
|
56
|
+
)
|
|
57
|
+
image = ::RubyLLM.paint(prompt, **kwargs)
|
|
58
|
+
normalize_image_response(image, model: model, provider: provider, params: { "size" => size || "1024x1024" }.merge(params || {}), metadata: metadata)
|
|
59
|
+
end
|
|
60
|
+
|
|
44
61
|
private
|
|
45
62
|
def configure_from_environment
|
|
46
63
|
config = ::RubyLLM.config
|
|
@@ -246,6 +263,47 @@ module TurnKit
|
|
|
246
263
|
|
|
247
264
|
response.cost&.total
|
|
248
265
|
end
|
|
266
|
+
|
|
267
|
+
def paint_kwargs(kwargs)
|
|
268
|
+
parameters = ::RubyLLM::Image.method(:paint).parameters
|
|
269
|
+
return kwargs if parameters.any? { |kind, _| kind == :keyrest }
|
|
270
|
+
|
|
271
|
+
accepted = parameters.filter_map { |kind, name| name if %i[key keyreq].include?(kind) }
|
|
272
|
+
unsupported = kwargs.keys.select { |key| !accepted.include?(key) && !blank?(kwargs[key]) }
|
|
273
|
+
raise ArgumentError, "RubyLLM image generation does not support: #{unsupported.join(", ")}" if unsupported.any?
|
|
274
|
+
|
|
275
|
+
kwargs.slice(*accepted)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def blank?(value)
|
|
279
|
+
value.nil? || value == false || (value.respond_to?(:empty?) && value.empty?)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def normalize_image_response(image, model:, provider:, params:, metadata:)
|
|
283
|
+
usage = Usage.new(
|
|
284
|
+
input_tokens: image_usage_value(image, "input_tokens"),
|
|
285
|
+
output_tokens: image_usage_value(image, "output_tokens"),
|
|
286
|
+
cost: response_cost(image)
|
|
287
|
+
)
|
|
288
|
+
part = ImageResult.new(
|
|
289
|
+
url: image.respond_to?(:url) ? image.url : nil,
|
|
290
|
+
data: image.respond_to?(:data) ? image.data : nil,
|
|
291
|
+
mime_type: image.respond_to?(:mime_type) ? image.mime_type : nil,
|
|
292
|
+
revised_prompt: image.respond_to?(:revised_prompt) ? image.revised_prompt : nil,
|
|
293
|
+
model: image.respond_to?(:model_id) ? image.model_id : model,
|
|
294
|
+
provider: provider&.to_s,
|
|
295
|
+
usage: usage,
|
|
296
|
+
params: params,
|
|
297
|
+
metadata: metadata || {}
|
|
298
|
+
).to_h.merge("type" => "image")
|
|
299
|
+
|
|
300
|
+
Result.new(parts: [ part ], usage: usage, model: part["model"], output_data: { "type" => "image", "images" => [ part ] })
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def image_usage_value(image, key)
|
|
304
|
+
usage = image.respond_to?(:usage) ? image.usage || {} : {}
|
|
305
|
+
(usage[key] || usage[key.to_sym]).to_i
|
|
306
|
+
end
|
|
249
307
|
end
|
|
250
308
|
end
|
|
251
309
|
end
|
data/lib/turnkit/client.rb
CHANGED
|
@@ -9,5 +9,9 @@ module TurnKit
|
|
|
9
9
|
def chat(model:, messages:, tools:, instructions:, temperature: nil, thinking: nil, output_schema: nil, metadata: nil, on_event: nil)
|
|
10
10
|
raise NotImplementedError
|
|
11
11
|
end
|
|
12
|
+
|
|
13
|
+
def paint(prompt:, model:, provider: nil, size: nil, assume_model_exists: nil, input_images: nil, mask: nil, params: {}, metadata: nil, on_event: nil)
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
end
|
|
12
16
|
end
|
|
13
17
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "open-uri"
|
|
5
|
+
|
|
6
|
+
module TurnKit
|
|
7
|
+
class ImageResult
|
|
8
|
+
attr_reader :url, :data, :mime_type, :revised_prompt, :model, :provider, :usage, :params, :metadata
|
|
9
|
+
|
|
10
|
+
def self.from_h(value)
|
|
11
|
+
new(**value.transform_keys(&:to_sym))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(url: nil, data: nil, mime_type: nil, revised_prompt: nil, model: nil, provider: nil, usage: Usage.new, params: {}, metadata: {}, **)
|
|
15
|
+
@url = url
|
|
16
|
+
@data = data
|
|
17
|
+
@mime_type = mime_type
|
|
18
|
+
@revised_prompt = revised_prompt
|
|
19
|
+
@model = model
|
|
20
|
+
@provider = provider
|
|
21
|
+
@usage = usage.is_a?(Usage) ? usage : Usage.from_h(usage || {})
|
|
22
|
+
@params = params || {}
|
|
23
|
+
@metadata = metadata || {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_blob
|
|
27
|
+
raise Error, "image has no url or data" if url.to_s.empty? && data.to_s.empty?
|
|
28
|
+
|
|
29
|
+
data ? Base64.decode64(data) : URI.open(url, &:read)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def cost
|
|
33
|
+
Cost.from_usage(usage, model: model)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_h
|
|
37
|
+
{
|
|
38
|
+
"url" => url,
|
|
39
|
+
"data" => data,
|
|
40
|
+
"mime_type" => mime_type,
|
|
41
|
+
"revised_prompt" => revised_prompt,
|
|
42
|
+
"model" => model,
|
|
43
|
+
"provider" => provider,
|
|
44
|
+
"usage" => usage.to_h,
|
|
45
|
+
"cost" => cost.to_h,
|
|
46
|
+
"params" => params,
|
|
47
|
+
"metadata" => metadata
|
|
48
|
+
}.compact
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TurnKit
|
|
4
|
+
class ImageTool < Tool
|
|
5
|
+
class << self
|
|
6
|
+
%i[model provider size assume_model_exists params].each do |name|
|
|
7
|
+
define_method(name) do |value = nil|
|
|
8
|
+
instance_variable_set("@#{name}", value) unless value.nil?
|
|
9
|
+
instance_variable_get("@#{name}")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(turnkit_context:, **arguments)
|
|
15
|
+
turnkit_context.turn.paint(
|
|
16
|
+
prompt(**arguments),
|
|
17
|
+
model: self.class.model,
|
|
18
|
+
provider: self.class.provider,
|
|
19
|
+
size: self.class.size,
|
|
20
|
+
assume_model_exists: self.class.assume_model_exists,
|
|
21
|
+
params: self.class.params || {},
|
|
22
|
+
metadata: metadata(**arguments)
|
|
23
|
+
).to_h
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def metadata(**)
|
|
27
|
+
{}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/turnkit/message.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module TurnKit
|
|
4
4
|
class Message
|
|
5
5
|
ROLES = %w[user assistant tool].freeze
|
|
6
|
-
KINDS = %w[text tool_call tool_result context_summary].freeze
|
|
6
|
+
KINDS = %w[text tool_call tool_result context_summary image].freeze
|
|
7
7
|
|
|
8
8
|
attr_reader :id, :conversation_id, :turn_id, :role, :kind, :sequence
|
|
9
9
|
attr_reader :content, :tool_execution_id, :provider_message_id, :metadata, :created_at
|
|
@@ -57,6 +57,10 @@ module TurnKit
|
|
|
57
57
|
kind == "context_summary"
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
+
def image?
|
|
61
|
+
kind == "image"
|
|
62
|
+
end
|
|
63
|
+
|
|
60
64
|
def text
|
|
61
65
|
content.filter_map do |part|
|
|
62
66
|
attrs = stringify(part)
|
|
@@ -44,6 +44,8 @@ module TurnKit
|
|
|
44
44
|
when "tool_result"
|
|
45
45
|
part = message.content.find { |candidate| candidate.fetch("type") == "tool_result" }
|
|
46
46
|
{ role: :tool, content: part&.fetch("text", message.text) || message.text, tool_call_id: part&.fetch("tool_call_id", nil) }
|
|
47
|
+
when "image"
|
|
48
|
+
{ role: :assistant, content: projected_images }
|
|
47
49
|
else
|
|
48
50
|
{ role: message.role.to_sym, content: message.text }
|
|
49
51
|
end
|
|
@@ -65,5 +67,14 @@ module TurnKit
|
|
|
65
67
|
{ "id" => part.fetch("id"), "name" => part.fetch("name"), "arguments" => part["arguments"] || {} }
|
|
66
68
|
end
|
|
67
69
|
end
|
|
70
|
+
|
|
71
|
+
def projected_images
|
|
72
|
+
message.content.filter_map do |part|
|
|
73
|
+
next unless part.fetch("type") == "image"
|
|
74
|
+
|
|
75
|
+
attrs = part.slice("url", "mime_type", "model", "provider", "revised_prompt").compact
|
|
76
|
+
"Generated image: #{attrs.to_json}"
|
|
77
|
+
end.join("\n")
|
|
78
|
+
end
|
|
68
79
|
end
|
|
69
80
|
end
|
|
@@ -31,6 +31,15 @@ module TurnKit
|
|
|
31
31
|
new(name: skill.key, content: skill.content, **options)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
def self.require_image
|
|
35
|
+
lambda do |output, output_data: nil, turn: nil, **|
|
|
36
|
+
data = output_data.is_a?(Hash) ? output_data : output
|
|
37
|
+
images = data.is_a?(Hash) ? data["images"] || data[:images] : nil
|
|
38
|
+
has_image = Array(images).any? || turn&.conversation&.messages_for_turn(turn)&.any?(&:image?)
|
|
39
|
+
{ rule: "image_required", message: "output must include an image result" } unless has_image
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
34
43
|
def initialize(content:, name: "output_policy", model: nil, thinking: nil, client: nil)
|
|
35
44
|
@name = name.to_s
|
|
36
45
|
@content = content.to_s
|
data/lib/turnkit/result.rb
CHANGED
|
@@ -28,6 +28,18 @@ module TurnKit
|
|
|
28
28
|
tool_calls.any?
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
def images
|
|
32
|
+
parts.filter_map do |part|
|
|
33
|
+
next unless part["type"] == "image"
|
|
34
|
+
|
|
35
|
+
ImageResult.from_h(part)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def image?
|
|
40
|
+
images.any?
|
|
41
|
+
end
|
|
42
|
+
|
|
31
43
|
private
|
|
32
44
|
def synthesize_parts(text:, tool_calls:)
|
|
33
45
|
parts = []
|
data/lib/turnkit/tool_runner.rb
CHANGED
|
@@ -49,6 +49,9 @@ module TurnKit
|
|
|
49
49
|
context = ToolContext.new(turn: turn, execution: execution)
|
|
50
50
|
payload = begin
|
|
51
51
|
normalize_payload(call_tool(tool, tool_call.arguments, context: context))
|
|
52
|
+
rescue BudgetError => error
|
|
53
|
+
finish_error(execution, tool_call, error.message, details: { "class" => error.class.name, "budget_denied" => true })
|
|
54
|
+
raise
|
|
52
55
|
rescue StandardError => error
|
|
53
56
|
return finish_error(execution, tool_call, error.message, details: { "class" => error.class.name })
|
|
54
57
|
end
|
data/lib/turnkit/turn.rb
CHANGED
|
@@ -167,6 +167,59 @@ module TurnKit
|
|
|
167
167
|
result
|
|
168
168
|
end
|
|
169
169
|
|
|
170
|
+
def paint(prompt, model:, provider: nil, size: nil, assume_model_exists: nil, input_images: nil, mask: nil, params: {}, metadata: {}, client: nil)
|
|
171
|
+
claimed_standalone = false
|
|
172
|
+
case status
|
|
173
|
+
when "pending"
|
|
174
|
+
claimed = store.claim_turn(id, from: "pending", to: "running", started_at: Clock.now, heartbeat_at: Clock.now)
|
|
175
|
+
raise Error, "turn is already running" unless claimed
|
|
176
|
+
|
|
177
|
+
@record = claimed
|
|
178
|
+
@started_at = @record["started_at"]
|
|
179
|
+
@budget = Budget.resume(store: store, root_turn_id: root_turn_id, limits: budget_limits)
|
|
180
|
+
claimed_standalone = true
|
|
181
|
+
emit("turn.started", status: status, model: model)
|
|
182
|
+
when "running"
|
|
183
|
+
# Image tools call this while their parent turn is running.
|
|
184
|
+
else
|
|
185
|
+
raise Error, "cannot paint for #{status} turn"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
image_client = client || agent.effective_client
|
|
189
|
+
request = {
|
|
190
|
+
prompt: prompt,
|
|
191
|
+
model: model,
|
|
192
|
+
provider: provider,
|
|
193
|
+
size: size,
|
|
194
|
+
assume_model_exists: assume_model_exists,
|
|
195
|
+
input_images: input_images,
|
|
196
|
+
mask: mask,
|
|
197
|
+
params: params || {},
|
|
198
|
+
metadata: { turn_id: id, conversation_id: conversation.id }.merge(metadata || {})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
image_client.validate!(model: model)
|
|
202
|
+
emit("image.requested", request.except(:input_images, :mask))
|
|
203
|
+
result = call_image_client(image_client, request)
|
|
204
|
+
result_cost = Cost.from_usage(result.usage, model: result.model || model)
|
|
205
|
+
add_usage!(result.usage, cost: result_cost)
|
|
206
|
+
budget.add_cost!(result_cost.total)
|
|
207
|
+
image = result.images.first
|
|
208
|
+
raise Error, "image client returned no image" unless image
|
|
209
|
+
raise Error, "image client returned image without url or data" if image.url.to_s.empty? && image.data.to_s.empty?
|
|
210
|
+
|
|
211
|
+
persist_image_message(image)
|
|
212
|
+
emit("image.completed", image: image.to_h, model: image.model || model, provider: image.provider || provider&.to_s, mime_type: image.mime_type, usage: result.usage.to_h, cost: result_cost.to_h, metadata: metadata || {})
|
|
213
|
+
complete_with_output(image.url.to_s, output_data: { "type" => "image", "images" => [ image.to_h ] }, audit: check_policy(image.url.to_s, output_data: { "type" => "image", "images" => [ image.to_h ] })) if claimed_standalone
|
|
214
|
+
image
|
|
215
|
+
rescue StandardError => error
|
|
216
|
+
if claimed_standalone
|
|
217
|
+
update!(status: "failed", error: { "class" => error.class.name, "message" => error.message }, completed_at: Clock.now)
|
|
218
|
+
emit("turn.failed", error: { "class" => error.class.name, "message" => error.message })
|
|
219
|
+
end
|
|
220
|
+
raise
|
|
221
|
+
end
|
|
222
|
+
|
|
170
223
|
private
|
|
171
224
|
def model_request
|
|
172
225
|
prompt = SystemPrompt.new(agent: agent, turn: self, conversation: conversation, mode: prompt_mode || agent.effective_prompt_mode(turn: self))
|
|
@@ -214,6 +267,16 @@ module TurnKit
|
|
|
214
267
|
end
|
|
215
268
|
end
|
|
216
269
|
|
|
270
|
+
def call_image_client(client, request)
|
|
271
|
+
kwargs = request.merge(on_event: ->(event) { emit_event(event) })
|
|
272
|
+
accepted = client.method(:paint).parameters.filter_map do |kind, name|
|
|
273
|
+
return client.paint(**kwargs) if kind == :keyrest
|
|
274
|
+
|
|
275
|
+
name if %i[key keyreq].include?(kind)
|
|
276
|
+
end
|
|
277
|
+
client.paint(**kwargs.slice(*accepted))
|
|
278
|
+
end
|
|
279
|
+
|
|
217
280
|
def llm_messages
|
|
218
281
|
MessageProjection.for(TurnKit::Compaction.project(conversation.messages_for_turn(self)))
|
|
219
282
|
end
|
|
@@ -271,12 +334,20 @@ module TurnKit
|
|
|
271
334
|
)
|
|
272
335
|
emit("message.created", message_id: message.id, role: message.role, kind: message.kind)
|
|
273
336
|
result.tool_calls.each { |call| emit("tool_call.created", id: call.id, name: call.name) }
|
|
337
|
+
elsif result.image?
|
|
338
|
+
message = conversation.append_message(role: "assistant", kind: "image", content: result.images.map { |image| image.to_h.merge("type" => "image") }, turn_id: id, metadata: { "output_data" => result.output_data }.compact)
|
|
339
|
+
emit("message.created", message_id: message.id, role: message.role, kind: message.kind)
|
|
274
340
|
else
|
|
275
341
|
message = conversation.append_message(role: "assistant", kind: "text", text: result.text, turn_id: id, metadata: { "output_data" => result.output_data }.compact)
|
|
276
342
|
emit("message.created", message_id: message.id, role: message.role, kind: message.kind)
|
|
277
343
|
end
|
|
278
344
|
end
|
|
279
345
|
|
|
346
|
+
def persist_image_message(image)
|
|
347
|
+
message = conversation.append_message(role: "assistant", kind: "image", content: [ image.to_h.merge("type" => "image") ], turn_id: id, metadata: { "output_data" => { "type" => "image", "images" => [ image.to_h ] } })
|
|
348
|
+
emit("message.created", message_id: message.id, role: message.role, kind: message.kind)
|
|
349
|
+
end
|
|
350
|
+
|
|
280
351
|
def append_terminal_completion(runner, execution)
|
|
281
352
|
message = runner.completion_message(execution)
|
|
282
353
|
assistant = conversation.append_message(role: "assistant", kind: "text", text: message, turn_id: id)
|
data/lib/turnkit/version.rb
CHANGED
data/lib/turnkit.rb
CHANGED
|
@@ -22,6 +22,7 @@ require_relative "turnkit/client"
|
|
|
22
22
|
require_relative "turnkit/conversation"
|
|
23
23
|
require_relative "turnkit/message"
|
|
24
24
|
require_relative "turnkit/record"
|
|
25
|
+
require_relative "turnkit/image_result"
|
|
25
26
|
require_relative "turnkit/result"
|
|
26
27
|
require_relative "turnkit/skill"
|
|
27
28
|
require_relative "turnkit/output_audit"
|
|
@@ -34,6 +35,7 @@ require_relative "turnkit/store"
|
|
|
34
35
|
require_relative "turnkit/memory_store"
|
|
35
36
|
require_relative "turnkit/compaction"
|
|
36
37
|
require_relative "turnkit/tool"
|
|
38
|
+
require_relative "turnkit/image_tool"
|
|
37
39
|
require_relative "turnkit/tool_call"
|
|
38
40
|
require_relative "turnkit/tool_execution"
|
|
39
41
|
require_relative "turnkit/sub_agent_tool"
|
|
@@ -109,4 +111,9 @@ module TurnKit
|
|
|
109
111
|
def self.check_output_policy(output, constraints: [], context: {})
|
|
110
112
|
OutputAudit.check(output, constraints: constraints, context: context)
|
|
111
113
|
end
|
|
114
|
+
|
|
115
|
+
def self.paint(prompt, model:, provider: nil, size: nil, assume_model_exists: nil, input_images: nil, mask: nil, params: {}, metadata: {}, client: nil)
|
|
116
|
+
image_client = client || self.client
|
|
117
|
+
image_client.paint(prompt: prompt, model: model, provider: provider, size: size, assume_model_exists: assume_model_exists, input_images: input_images, mask: mask, params: params, metadata: metadata).images.first
|
|
118
|
+
end
|
|
112
119
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: turnkit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sam Couch
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ruby_llm
|
|
@@ -57,6 +57,8 @@ files:
|
|
|
57
57
|
- lib/turnkit/generators/turnkit/install/templates/turn.rb
|
|
58
58
|
- lib/turnkit/generators/turnkit/install_generator.rb
|
|
59
59
|
- lib/turnkit/id.rb
|
|
60
|
+
- lib/turnkit/image_result.rb
|
|
61
|
+
- lib/turnkit/image_tool.rb
|
|
60
62
|
- lib/turnkit/load_skill_tool.rb
|
|
61
63
|
- lib/turnkit/memory_store.rb
|
|
62
64
|
- lib/turnkit/message.rb
|