turnkit 0.2.10 → 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 +16 -0
- data/README.md +89 -9
- data/UPGRADE.md +37 -299
- data/lib/turnkit/adapters/ruby_llm.rb +87 -0
- data/lib/turnkit/agent.rb +22 -33
- data/lib/turnkit/budget.rb +24 -5
- data/lib/turnkit/client.rb +4 -0
- data/lib/turnkit/compaction.rb +1 -0
- data/lib/turnkit/error.rb +1 -0
- data/lib/turnkit/generators/turnkit/install/templates/create_turnkit_tables.rb +0 -1
- data/lib/turnkit/image_result.rb +51 -0
- data/lib/turnkit/image_tool.rb +30 -0
- data/lib/turnkit/load_skill_tool.rb +29 -0
- data/lib/turnkit/memory_store.rb +11 -0
- data/lib/turnkit/message.rb +19 -8
- data/lib/turnkit/message_projection.rb +28 -2
- data/lib/turnkit/output_policy.rb +15 -0
- data/lib/turnkit/result.rb +41 -4
- data/lib/turnkit/run.rb +4 -5
- data/lib/turnkit/schema_check.rb +68 -0
- data/lib/turnkit/skill.rb +16 -2
- data/lib/turnkit/store.rb +6 -0
- data/lib/turnkit/stores/active_record_store.rb +10 -2
- data/lib/turnkit/sub_agent_tool.rb +2 -1
- data/lib/turnkit/system_prompt.rb +1 -1
- data/lib/turnkit/tool.rb +2 -21
- data/lib/turnkit/tool_call.rb +3 -3
- data/lib/turnkit/tool_runner.rb +41 -16
- data/lib/turnkit/turn.rb +161 -23
- data/lib/turnkit/version.rb +1 -1
- data/lib/turnkit/workflow.rb +20 -94
- data/lib/turnkit.rb +12 -10
- metadata +6 -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,21 @@
|
|
|
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
|
+
|
|
9
|
+
## 0.3.0 - 2026-06-10
|
|
10
|
+
|
|
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`.
|
|
12
|
+
- Store message content as ordered typed parts, with text derived from content and tool calls/results persisted in the transcript instead of metadata.
|
|
13
|
+
- Add `load_skill` for progressively disclosed available skills.
|
|
14
|
+
- Add output-policy revision loops with `output_retries`, including skill/policy rehydration in revision prompts.
|
|
15
|
+
- Add deterministic `input_schema` validation before turns are created.
|
|
16
|
+
- Ensure terminal tools never orphan sibling tool calls; skipped siblings receive cancelled executions and tool-result messages.
|
|
17
|
+
- Add turn claiming, tool-runner heartbeats, persisted budget resume, and sub-agent failure details.
|
|
18
|
+
|
|
3
19
|
## 0.2.10 - 2026-06-10
|
|
4
20
|
|
|
5
21
|
- Add output audits and file-backed output policies for validating final run output.
|
data/README.md
CHANGED
|
@@ -331,8 +331,8 @@ end
|
|
|
331
331
|
|
|
332
332
|
workflow = TurnKit::Workflow.new(
|
|
333
333
|
name: "memo_writer",
|
|
334
|
-
|
|
335
|
-
|
|
334
|
+
output_policy: [no_em_dash, numbered_lists_only],
|
|
335
|
+
output_policy_mode: :fail
|
|
336
336
|
)
|
|
337
337
|
```
|
|
338
338
|
|
|
@@ -340,7 +340,7 @@ Run checks directly when you want to test a renderer or policy without calling a
|
|
|
340
340
|
model:
|
|
341
341
|
|
|
342
342
|
```ruby
|
|
343
|
-
audit = TurnKit.
|
|
343
|
+
audit = TurnKit.check_output_policy(
|
|
344
344
|
"1. Recommendation\n- unordered item — fix this\n",
|
|
345
345
|
constraints: [no_em_dash, numbered_lists_only]
|
|
346
346
|
)
|
|
@@ -350,8 +350,8 @@ puts audit.messages
|
|
|
350
350
|
```
|
|
351
351
|
|
|
352
352
|
Use `output_policy` when a semantic judge is worth the extra model call. The
|
|
353
|
-
policy can be a `.md`, `.markdown`, or `.txt` file path, a `TurnKit::
|
|
354
|
-
or any object that responds to `#call` or `#check`.
|
|
353
|
+
policy can be a `.md`, `.markdown`, or `.txt` file path, a `TurnKit::Skill`, a
|
|
354
|
+
`TurnKit::OutputPolicy`, or any object that responds to `#call` or `#check`.
|
|
355
355
|
|
|
356
356
|
```ruby
|
|
357
357
|
workflow = TurnKit::Workflow.new(
|
|
@@ -364,8 +364,34 @@ workflow = TurnKit::Workflow.new(
|
|
|
364
364
|
```
|
|
365
365
|
|
|
366
366
|
`output_policy_mode: :report` records violations while allowing the run to
|
|
367
|
-
complete. `:fail` marks the run failed after recording the output and audit
|
|
368
|
-
|
|
367
|
+
complete. `:fail` marks the run failed after recording the output and audit;
|
|
368
|
+
`:fail` is the default for contract-driven workflows. Policy model usage and
|
|
369
|
+
cost are counted on the parent run.
|
|
370
|
+
|
|
371
|
+
Add `output_retries:` to turn policy failures into bounded revision loops instead
|
|
372
|
+
of dead ends:
|
|
373
|
+
|
|
374
|
+
```ruby
|
|
375
|
+
voice = TurnKit::Skill.from_file("app/ai/skills/memo_voice.md")
|
|
376
|
+
|
|
377
|
+
workflow = TurnKit::Workflow.new(
|
|
378
|
+
name: "memo_writer",
|
|
379
|
+
skills: [voice],
|
|
380
|
+
output_policy: [voice, no_em_dash],
|
|
381
|
+
output_retries: 2,
|
|
382
|
+
input_schema: {
|
|
383
|
+
"type" => "object",
|
|
384
|
+
"required" => ["project_id"],
|
|
385
|
+
"properties" => { "project_id" => { "type" => "string" } }
|
|
386
|
+
}
|
|
387
|
+
)
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
`skills:` are always loaded into the prompt. `available_skills:` are listed in
|
|
391
|
+
`<skills_available>` and exposed through the `load_skill` tool, so the model can
|
|
392
|
+
load full instructions on demand. Every advertised tool call receives exactly one
|
|
393
|
+
tool result, including validation errors, budget denials, and calls skipped after
|
|
394
|
+
a terminal tool ends the turn.
|
|
369
395
|
|
|
370
396
|
### Prompt Preview
|
|
371
397
|
|
|
@@ -433,6 +459,61 @@ puts turn.output_text
|
|
|
433
459
|
|
|
434
460
|
Rely on TurnKit to validate tools and model-provided arguments.
|
|
435
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
|
+
|
|
436
517
|
### Structured Output
|
|
437
518
|
|
|
438
519
|
Define a schema:
|
|
@@ -639,8 +720,7 @@ TurnKit.output_policy_model = "gpt-4.1-mini"
|
|
|
639
720
|
TurnKit.timeout = 300
|
|
640
721
|
```
|
|
641
722
|
|
|
642
|
-
`
|
|
643
|
-
`max_spend`.
|
|
723
|
+
`max_spend` is the only spend-limit name in the public API.
|
|
644
724
|
|
|
645
725
|
Set options per agent:
|
|
646
726
|
|
data/UPGRADE.md
CHANGED
|
@@ -1,313 +1,51 @@
|
|
|
1
1
|
# Upgrade Guide
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
recommended migration is about making the three work shapes easier to read:
|
|
3
|
+
## 0.3.0 is a clean break
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
TurnKit 0.3.0 intentionally removes the short-lived legacy names from the 0.2
|
|
6
|
+
series. The gem is pre-1.0 and the durable transcript schema changed, so migrate
|
|
7
|
+
by updating call sites and reinstalling the generated tables for new projects.
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
### Renames
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
- `TurnKit.cost_limit` → `TurnKit.max_spend`
|
|
12
|
+
- `Agent.new(cost_limit:)` → `Agent.new(max_spend:)`
|
|
13
|
+
- `Workflow.new(cost_limit:)` / `workflow.run(cost_limit:)` → `max_spend:`
|
|
14
|
+
- `output_audit:` → `output_policy:`
|
|
15
|
+
- `output_audit_mode:` → `output_policy_mode:`
|
|
16
|
+
- `run.output_audit` → `run.policy_audit`
|
|
17
|
+
- `run.output_audit_clean?` → `run.policy_clean?`
|
|
18
|
+
- `TurnKit.audit_output(...)` → `TurnKit.check_output_policy(...)`
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
gem "turnkit", "~> 0.2.9"
|
|
18
|
-
```
|
|
20
|
+
The audit result class remains `TurnKit::OutputAudit`; only the public option and
|
|
21
|
+
run-accessor names changed.
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
bundle update turnkit
|
|
22
|
-
```
|
|
23
|
+
### Message schema
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
`turnkit_messages.text` was removed. Message `content` is now the canonical
|
|
26
|
+
ordered array of parts:
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
- `text`
|
|
29
|
+
- `thinking`
|
|
30
|
+
- `tool_call`
|
|
31
|
+
- `tool_result`
|
|
32
|
+
- opaque provider parts
|
|
27
33
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
config.model = "gpt-5.2"
|
|
31
|
-
config.max_spend = 0.25
|
|
32
|
-
end
|
|
34
|
+
`Message#text` is derived from text parts. New Rails installs should regenerate
|
|
35
|
+
the install migration; there is no compatibility shim for older schemas.
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
run = workflow.run("Create a source-grounded brief.", input: { topic: "Rails 8" })
|
|
37
|
+
### Workflows
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
`TurnKit::Workflow` now forwards options directly to `Agent`. Use
|
|
40
|
+
`workflow.options[:name]` or `workflow.agent` for inspection instead of per-option
|
|
41
|
+
workflow attr readers. Workflow `instructions:` compose with the orchestrator
|
|
42
|
+
preamble by default; pass `preamble: false` to opt out.
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
### Skills and policy loops
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
After:
|
|
51
|
-
|
|
52
|
-
```ruby
|
|
53
|
-
TurnKit.model = "gpt-5.2"
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
`TurnKit.default_model` remains supported. `TurnKit.model` is the shorter public
|
|
57
|
-
alias for app code and initializers.
|
|
58
|
-
|
|
59
|
-
### Global setup
|
|
60
|
-
|
|
61
|
-
Before:
|
|
62
|
-
|
|
63
|
-
```ruby
|
|
64
|
-
TurnKit.default_model = "gpt-5.2"
|
|
65
|
-
TurnKit.cost_limit = 0.25
|
|
66
|
-
TurnKit.max_iterations = 12
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
After:
|
|
70
|
-
|
|
71
|
-
```ruby
|
|
72
|
-
TurnKit.configure do |config|
|
|
73
|
-
config.model = "gpt-5.2"
|
|
74
|
-
config.max_spend = 0.25
|
|
75
|
-
config.max_iterations = 12
|
|
76
|
-
end
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
`TurnKit.configure` simply yields the `TurnKit` module. There is no separate
|
|
80
|
-
configuration object or DSL.
|
|
81
|
-
|
|
82
|
-
### Spend limit naming
|
|
83
|
-
|
|
84
|
-
Before:
|
|
85
|
-
|
|
86
|
-
```ruby
|
|
87
|
-
TurnKit.cost_limit = 0.25
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
After:
|
|
91
|
-
|
|
92
|
-
```ruby
|
|
93
|
-
TurnKit.max_spend = 0.25
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
`cost_limit` remains supported. Prefer `max_spend` in application-facing code
|
|
97
|
-
because it matches how developers think about autonomous runs.
|
|
98
|
-
|
|
99
|
-
## Running application tasks
|
|
100
|
-
|
|
101
|
-
### Agent tasks
|
|
102
|
-
|
|
103
|
-
Before:
|
|
104
|
-
|
|
105
|
-
```ruby
|
|
106
|
-
run = agent.run(task: "Classify this lead.", input: lead.attributes)
|
|
107
|
-
puts run.output_text
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
After:
|
|
111
|
-
|
|
112
|
-
```ruby
|
|
113
|
-
run = agent.run("Classify this lead.", input: lead.attributes)
|
|
114
|
-
puts run.output
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
The keyword form still works. The positional string is the recommended form for
|
|
118
|
-
the common case. `Agent#run` uses task prompt behavior by default; pass
|
|
119
|
-
`prompt_mode: :full` if you need conversation-style prompt behavior for a run.
|
|
120
|
-
|
|
121
|
-
### Pending runs
|
|
122
|
-
|
|
123
|
-
No behavior change.
|
|
124
|
-
|
|
125
|
-
```ruby
|
|
126
|
-
run = agent.run("Classify later.", async: true)
|
|
127
|
-
request = run.preview
|
|
128
|
-
run.run!
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
The existing keyword form remains valid:
|
|
132
|
-
|
|
133
|
-
```ruby
|
|
134
|
-
run = agent.run(task: "Classify later.", async: true)
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
## Workflows
|
|
138
|
-
|
|
139
|
-
The preferred name for reusable autonomous task runtimes is now workflow. A
|
|
140
|
-
workflow packages:
|
|
141
|
-
|
|
142
|
-
- one task-mode orchestrator
|
|
143
|
-
- workflow skills
|
|
144
|
-
- tools
|
|
145
|
-
- guardrails
|
|
146
|
-
- compaction
|
|
147
|
-
- optional persistence/action tools
|
|
148
|
-
|
|
149
|
-
### Construction
|
|
150
|
-
|
|
151
|
-
```ruby
|
|
152
|
-
workflow = TurnKit::Workflow.new(
|
|
153
|
-
name: "sales_enrichment",
|
|
154
|
-
tools: [AccountLookup, WebSearch, SaveEnrichment],
|
|
155
|
-
skills: [sales_research_skill],
|
|
156
|
-
max_spend: 0.25
|
|
157
|
-
)
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
### Running
|
|
161
|
-
|
|
162
|
-
```ruby
|
|
163
|
-
run = workflow.run(
|
|
164
|
-
"Enrich this account for responsible outreach.",
|
|
165
|
-
input: account.attributes
|
|
166
|
-
)
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
`task:` remains supported.
|
|
170
|
-
|
|
171
|
-
## Run inspection
|
|
172
|
-
|
|
173
|
-
New convenience methods were added to `TurnKit::Run`.
|
|
174
|
-
|
|
175
|
-
Before:
|
|
176
|
-
|
|
177
|
-
```ruby
|
|
178
|
-
run.output_text
|
|
179
|
-
run.tool_executions
|
|
180
|
-
run.turn_records.length
|
|
181
|
-
TurnKit.store.load_turn(run.id)["error"]
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
After:
|
|
185
|
-
|
|
186
|
-
```ruby
|
|
187
|
-
run.output
|
|
188
|
-
run.tool_calls
|
|
189
|
-
run.steps
|
|
190
|
-
run.error
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
Old methods remain available. Prefer the shorter methods in application code,
|
|
194
|
-
examples, and docs.
|
|
195
|
-
|
|
196
|
-
## Save/action tools
|
|
197
|
-
|
|
198
|
-
Use `terminal!` for tools that complete the run by saving an artifact or taking
|
|
199
|
-
the final action.
|
|
200
|
-
|
|
201
|
-
Before:
|
|
202
|
-
|
|
203
|
-
```ruby
|
|
204
|
-
class SaveBrief < TurnKit::Tool
|
|
205
|
-
def self.ends_turn? = true
|
|
206
|
-
def self.completion_message(result) = "Saved #{result.fetch("id")}."
|
|
207
|
-
|
|
208
|
-
def call(title:, body:, context:)
|
|
209
|
-
{ "id" => Brief.create!(title: title, body: body).id }
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
After:
|
|
215
|
-
|
|
216
|
-
```ruby
|
|
217
|
-
class SaveBrief < TurnKit::Tool
|
|
218
|
-
terminal! { |result| "Saved #{result.fetch("id")}." }
|
|
219
|
-
|
|
220
|
-
def call(title:, body:, context:)
|
|
221
|
-
{ "id" => Brief.create!(title: title, body: body).id }
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
The old `ends_turn?` and `completion_message` methods remain supported. Prefer
|
|
227
|
-
`terminal!` for readability.
|
|
228
|
-
|
|
229
|
-
## Tool instances
|
|
230
|
-
|
|
231
|
-
If a tool needs constructor arguments, register an instance instead of a class.
|
|
232
|
-
|
|
233
|
-
Before, this may have failed at runtime:
|
|
234
|
-
|
|
235
|
-
```ruby
|
|
236
|
-
class WebSearch < TurnKit::Tool
|
|
237
|
-
def initialize(client:)
|
|
238
|
-
@client = client
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
agent = TurnKit::Agent.new(tools: [WebSearch])
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
After:
|
|
246
|
-
|
|
247
|
-
```ruby
|
|
248
|
-
client = SearchClient.new(api_key: ENV.fetch("SEARCH_API_KEY"))
|
|
249
|
-
agent = TurnKit::Agent.new(tools: [WebSearch.new(client: client)])
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
This is the recommended pattern for API clients, test doubles, and per-tenant
|
|
253
|
-
dependencies.
|
|
254
|
-
|
|
255
|
-
## Multi-agent workflows
|
|
256
|
-
|
|
257
|
-
If you previously modeled every role as a separate agent, consider migrating the
|
|
258
|
-
default path to one workflow with a workflow skill.
|
|
259
|
-
|
|
260
|
-
Before:
|
|
261
|
-
|
|
262
|
-
```ruby
|
|
263
|
-
researcher = TurnKit::Agent.new(name: "researcher", tools: [WebSearch])
|
|
264
|
-
writer = TurnKit::Agent.new(name: "writer")
|
|
265
|
-
verifier = TurnKit::Agent.new(name: "verifier")
|
|
266
|
-
|
|
267
|
-
orchestrator = TurnKit::Agent.new(
|
|
268
|
-
name: "orchestrator",
|
|
269
|
-
sub_agents: [researcher, writer, verifier]
|
|
270
|
-
)
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
After:
|
|
274
|
-
|
|
275
|
-
```ruby
|
|
276
|
-
workflow = TurnKit::Skill.new(
|
|
277
|
-
key: "source_grounded_brief",
|
|
278
|
-
name: "Source Grounded Brief",
|
|
279
|
-
content: <<~TEXT
|
|
280
|
-
Research first. Build an evidence pack. Draft only from evidence. Verify
|
|
281
|
-
important claims. Revise unsupported claims before final output.
|
|
282
|
-
TEXT
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
source_brief = TurnKit::Workflow.new(
|
|
286
|
-
name: "source_brief",
|
|
287
|
-
skills: [workflow],
|
|
288
|
-
tools: [WebSearch, ReadWebPage, SaveBrief],
|
|
289
|
-
max_spend: 0.25,
|
|
290
|
-
max_tool_executions: 20
|
|
291
|
-
)
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
Keep separate agents when the isolation is worth the extra model calls:
|
|
295
|
-
|
|
296
|
-
- different models
|
|
297
|
-
- different tool permissions
|
|
298
|
-
- adversarial review
|
|
299
|
-
- parallel specialist research
|
|
300
|
-
- separate durable child conversations
|
|
301
|
-
|
|
302
|
-
## Suggested migration order
|
|
303
|
-
|
|
304
|
-
1. Replace `TurnKit.default_model =` with `TurnKit.model =` in app-level config.
|
|
305
|
-
2. Wrap global settings in `TurnKit.configure` if you have more than one.
|
|
306
|
-
3. Use `TurnKit::Workflow.new(name: "...")` for reusable autonomous task runners.
|
|
307
|
-
4. Replace `run(task: "...")` with `run("...")` where it improves readability.
|
|
308
|
-
5. Replace `run.output_text` with `run.output` in application code.
|
|
309
|
-
6. Replace save/action tool overrides with `terminal!` when convenient.
|
|
310
|
-
7. Consider collapsing role-agent workflows into one workflow plus workflow skills if
|
|
311
|
-
cost or complexity is a concern.
|
|
312
|
-
|
|
313
|
-
Run your test suite after migrating call sites.
|
|
46
|
+
- `available_skills:` now exposes a real `load_skill` tool.
|
|
47
|
+
- `output_policy:` accepts `TurnKit::Skill` instances.
|
|
48
|
+
- `output_retries:` controls bounded revision loops. The default policy mode is
|
|
49
|
+
now `:fail`; use `output_policy_mode: :report` if dirty output should complete.
|
|
50
|
+
- `input_schema:` validates application input before any conversation or turn is
|
|
51
|
+
created.
|
|
@@ -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
|
|
@@ -182,6 +199,7 @@ module TurnKit
|
|
|
182
199
|
)
|
|
183
200
|
Result.new(
|
|
184
201
|
text: response_text(response),
|
|
202
|
+
parts: response_parts(response, tool_calls: tool_calls),
|
|
185
203
|
output_data: response_data(response),
|
|
186
204
|
tool_calls: tool_calls,
|
|
187
205
|
usage: usage,
|
|
@@ -189,6 +207,34 @@ module TurnKit
|
|
|
189
207
|
)
|
|
190
208
|
end
|
|
191
209
|
|
|
210
|
+
def response_parts(response, tool_calls:)
|
|
211
|
+
content = response.respond_to?(:content) ? response.content : response
|
|
212
|
+
parts = case content
|
|
213
|
+
when Array
|
|
214
|
+
content.map { |part| normalize_provider_part(part) }
|
|
215
|
+
when Hash
|
|
216
|
+
[ { "type" => "text", "text" => content.to_json } ]
|
|
217
|
+
else
|
|
218
|
+
text = content.to_s
|
|
219
|
+
text.empty? ? [] : [ { "type" => "text", "text" => text } ]
|
|
220
|
+
end.compact
|
|
221
|
+
parts + Array(tool_calls).map { |call| { "type" => "tool_call", "id" => call.id, "name" => call.name, "arguments" => call.arguments } }
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def normalize_provider_part(part)
|
|
225
|
+
attrs = part.respond_to?(:to_h) ? part.to_h.transform_keys(&:to_s) : nil
|
|
226
|
+
return { "type" => "text", "text" => part.to_s } unless attrs
|
|
227
|
+
|
|
228
|
+
case attrs["type"].to_s
|
|
229
|
+
when "text", "output_text"
|
|
230
|
+
{ "type" => "text", "text" => attrs["text"] || attrs["content"].to_s }
|
|
231
|
+
when "thinking", "reasoning"
|
|
232
|
+
{ "type" => "thinking", "text" => attrs["text"] || attrs["content"].to_s, "signature" => attrs["signature"], "redacted" => attrs["redacted"] || false }.compact
|
|
233
|
+
else
|
|
234
|
+
{ "type" => "provider", "kind" => attrs["type"].to_s, "data" => attrs }
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
192
238
|
def response_text(response)
|
|
193
239
|
content = response.respond_to?(:content) ? response.content : response
|
|
194
240
|
content.is_a?(Hash) || content.is_a?(Array) ? content.to_json : content.to_s
|
|
@@ -217,6 +263,47 @@ module TurnKit
|
|
|
217
263
|
|
|
218
264
|
response.cost&.total
|
|
219
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
|
|
220
307
|
end
|
|
221
308
|
end
|
|
222
309
|
end
|