turnkit 0.2.6 → 0.2.7
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/README.md +146 -401
- data/lib/turnkit/adapters/ruby_llm.rb +69 -5
- data/lib/turnkit/agent.rb +19 -2
- data/lib/turnkit/client.rb +5 -1
- data/lib/turnkit/conversation.rb +5 -4
- data/lib/turnkit/error.rb +2 -0
- data/lib/turnkit/event.rb +25 -0
- data/lib/turnkit/generators/turnkit/install/templates/create_turnkit_tables.rb +1 -0
- data/lib/turnkit/generators/turnkit/install/templates/initializer.rb +6 -0
- data/lib/turnkit/generators/turnkit/install_generator.rb +6 -0
- data/lib/turnkit/model_request.rb +35 -0
- data/lib/turnkit/record.rb +2 -1
- data/lib/turnkit/result.rb +3 -2
- data/lib/turnkit/stores/active_record_store.rb +14 -4
- data/lib/turnkit/sub_agent_tool.rb +13 -4
- data/lib/turnkit/tool.rb +117 -4
- data/lib/turnkit/tool_call.rb +3 -1
- data/lib/turnkit/tool_runner.rb +8 -1
- data/lib/turnkit/turn.rb +92 -15
- data/lib/turnkit/version.rb +1 -1
- data/lib/turnkit.rb +4 -0
- metadata +6 -5
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.ruby-lang.org)
|
|
5
5
|
[](LICENSE.md)
|
|
6
6
|
|
|
7
|
-
Build durable Ruby
|
|
7
|
+
Build durable Ruby and Rails agents with tools, skills, sub-agents, and persistence.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
@@ -22,21 +22,12 @@ bundle install
|
|
|
22
22
|
|
|
23
23
|
## Quick Start
|
|
24
24
|
|
|
25
|
-
Set
|
|
25
|
+
Set an API key:
|
|
26
26
|
|
|
27
27
|
```sh
|
|
28
28
|
export ANTHROPIC_API_KEY=...
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
| Provider | Env var | Example model |
|
|
32
|
-
| --- | --- | --- |
|
|
33
|
-
| Anthropic | `ANTHROPIC_API_KEY` | `claude-sonnet-4-5` |
|
|
34
|
-
| OpenAI | `OPENAI_API_KEY` | `gpt-4.1-mini` |
|
|
35
|
-
| Gemini | `GEMINI_API_KEY` | `gemini-2.5-flash` |
|
|
36
|
-
|
|
37
|
-
> [!WARNING]
|
|
38
|
-
> TurnKit defaults to `claude-sonnet-4-5`. If `ANTHROPIC_API_KEY` is unset or blank, set `TurnKit.default_model` to a provider you have configured.
|
|
39
|
-
|
|
40
31
|
Create an agent:
|
|
41
32
|
|
|
42
33
|
```ruby
|
|
@@ -59,69 +50,28 @@ puts turn.output_text
|
|
|
59
50
|
|
|
60
51
|
### Models
|
|
61
52
|
|
|
62
|
-
Set
|
|
63
|
-
|
|
64
|
-
```ruby
|
|
65
|
-
TurnKit.default_model = "claude-sonnet-4-5"
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
Use OpenAI:
|
|
69
|
-
|
|
70
|
-
```sh
|
|
71
|
-
export OPENAI_API_KEY=...
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
Set an OpenAI model:
|
|
53
|
+
Set a model:
|
|
75
54
|
|
|
76
55
|
```ruby
|
|
77
56
|
TurnKit.default_model = "gpt-4.1-mini"
|
|
78
57
|
```
|
|
79
58
|
|
|
80
|
-
|
|
59
|
+
Set the matching key:
|
|
81
60
|
|
|
82
61
|
```sh
|
|
83
|
-
export
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Set a Gemini model:
|
|
87
|
-
|
|
88
|
-
```ruby
|
|
89
|
-
TurnKit.default_model = "gemini-2.5-flash"
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### Thinking
|
|
93
|
-
|
|
94
|
-
Enable provider reasoning or extended thinking per agent:
|
|
95
|
-
|
|
96
|
-
```ruby
|
|
97
|
-
agent = TurnKit::Agent.new(
|
|
98
|
-
name: "reasoner",
|
|
99
|
-
model: "claude-sonnet-4-5",
|
|
100
|
-
thinking: { budget: 4_000 }
|
|
101
|
-
)
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
Use effort-based thinking for providers that support it:
|
|
105
|
-
|
|
106
|
-
```ruby
|
|
107
|
-
agent = TurnKit::Agent.new(
|
|
108
|
-
name: "reasoner",
|
|
109
|
-
model: "gemini-2.5-flash",
|
|
110
|
-
thinking: { effort: :high }
|
|
111
|
-
)
|
|
62
|
+
export OPENAI_API_KEY=...
|
|
112
63
|
```
|
|
113
64
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
```ruby
|
|
117
|
-
conversation = agent.conversation
|
|
118
|
-
conversation.ask("Solve this carefully.", thinking: { budget: 8_000 })
|
|
119
|
-
conversation.ask("Answer quickly.", thinking: nil)
|
|
120
|
-
```
|
|
65
|
+
Use these common providers:
|
|
121
66
|
|
|
122
|
-
|
|
67
|
+
| Provider | Key | Model |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| Anthropic | `ANTHROPIC_API_KEY` | `claude-sonnet-4-5` |
|
|
70
|
+
| OpenAI | `OPENAI_API_KEY` | `gpt-4.1-mini` |
|
|
71
|
+
| Gemini | `GEMINI_API_KEY` | `gemini-2.5-flash` |
|
|
72
|
+
| OpenRouter | `OPENROUTER_API_KEY` | `openrouter/...` |
|
|
123
73
|
|
|
124
|
-
|
|
74
|
+
Expect `TurnKit::ModelAccessError` for obvious key mistakes.
|
|
125
75
|
|
|
126
76
|
### Conversations
|
|
127
77
|
|
|
@@ -132,12 +82,13 @@ agent = TurnKit::Agent.new(
|
|
|
132
82
|
name: "writer",
|
|
133
83
|
instructions: "Write clear release notes."
|
|
134
84
|
)
|
|
85
|
+
|
|
86
|
+
conversation = agent.conversation(subject: "v1 launch")
|
|
135
87
|
```
|
|
136
88
|
|
|
137
89
|
Add context:
|
|
138
90
|
|
|
139
91
|
```ruby
|
|
140
|
-
conversation = agent.conversation(subject: "v1 launch")
|
|
141
92
|
conversation.say("Mention faster tool execution.")
|
|
142
93
|
```
|
|
143
94
|
|
|
@@ -148,91 +99,29 @@ turn = conversation.run!
|
|
|
148
99
|
puts turn.output_text
|
|
149
100
|
```
|
|
150
101
|
|
|
151
|
-
###
|
|
102
|
+
### Prompt Preview
|
|
152
103
|
|
|
153
|
-
|
|
104
|
+
Preview a pending turn:
|
|
154
105
|
|
|
155
106
|
```ruby
|
|
156
|
-
|
|
157
|
-
|
|
107
|
+
turn = conversation.ask("Draft the launch email.", async: true)
|
|
108
|
+
request = turn.preview
|
|
158
109
|
```
|
|
159
110
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
Disable compaction globally:
|
|
111
|
+
Inspect the request:
|
|
163
112
|
|
|
164
113
|
```ruby
|
|
165
|
-
|
|
114
|
+
request.model
|
|
115
|
+
request.messages
|
|
116
|
+
request.tool_names
|
|
117
|
+
request.instructions
|
|
118
|
+
request.report
|
|
166
119
|
```
|
|
167
120
|
|
|
168
|
-
|
|
121
|
+
Run the reviewed turn:
|
|
169
122
|
|
|
170
123
|
```ruby
|
|
171
|
-
|
|
172
|
-
model: "gpt-4.1-mini"
|
|
173
|
-
}
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
You can also configure the compaction threshold and estimated context limit:
|
|
177
|
-
|
|
178
|
-
```ruby
|
|
179
|
-
TurnKit.compaction = {
|
|
180
|
-
model: "gpt-4.1-mini",
|
|
181
|
-
threshold: 0.75,
|
|
182
|
-
context_limit: 128_000
|
|
183
|
-
}
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
Configure compaction for one agent:
|
|
187
|
-
|
|
188
|
-
```ruby
|
|
189
|
-
agent = TurnKit::Agent.new(
|
|
190
|
-
name: "engineer",
|
|
191
|
-
model: "gpt-5",
|
|
192
|
-
compaction: {
|
|
193
|
-
model: "gpt-4.1-mini",
|
|
194
|
-
threshold: 0.75,
|
|
195
|
-
context_limit: 128_000
|
|
196
|
-
}
|
|
197
|
-
)
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
In this example, normal turns use `gpt-5` and compaction summaries use `gpt-4.1-mini`.
|
|
201
|
-
|
|
202
|
-
Override the model for one manual compaction:
|
|
203
|
-
|
|
204
|
-
```ruby
|
|
205
|
-
conversation.compact!(model: "gpt-4.1-mini")
|
|
206
|
-
conversation.compact!(focus: "billing migration", model: "gpt-4.1-mini")
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
Disable compaction for a single turn:
|
|
210
|
-
|
|
211
|
-
```ruby
|
|
212
|
-
conversation.ask("Continue", compact: false)
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
Manually compact a conversation:
|
|
216
|
-
|
|
217
|
-
```ruby
|
|
218
|
-
conversation.compact!
|
|
219
|
-
conversation.compact!(focus: "billing migration")
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
Compaction is append-only: TurnKit stores a `context_summary` message with metadata describing the message range it replaces for model projection. The original messages are not deleted, so `conversation.messages` remains the full durable transcript. Future model calls see a compacted projection that includes a reference-only summary and the recent tail.
|
|
223
|
-
|
|
224
|
-
The model-visible projection uses a synthetic summary exchange followed by recent messages:
|
|
225
|
-
|
|
226
|
-
```text
|
|
227
|
-
user: What did we do so far?
|
|
228
|
-
assistant: [CONTEXT COMPACTION — REFERENCE ONLY] ...
|
|
229
|
-
user: latest request
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
For a local smoke test without calling a real provider, run:
|
|
233
|
-
|
|
234
|
-
```sh
|
|
235
|
-
ruby script/manual_compaction.rb
|
|
124
|
+
turn.run!
|
|
236
125
|
```
|
|
237
126
|
|
|
238
127
|
### Tools
|
|
@@ -243,11 +132,15 @@ Create a tool:
|
|
|
243
132
|
class SaveReport < TurnKit::Tool
|
|
244
133
|
description "Save a report."
|
|
245
134
|
usage_hint "Use when the user asks to persist a report."
|
|
135
|
+
|
|
246
136
|
parameter :title, :string, required: true
|
|
247
137
|
parameter :body, :string, required: true
|
|
248
138
|
|
|
249
139
|
def self.ends_turn? = true
|
|
250
|
-
|
|
140
|
+
|
|
141
|
+
def self.completion_message(result)
|
|
142
|
+
"Saved #{result.fetch("report_id")}."
|
|
143
|
+
end
|
|
251
144
|
|
|
252
145
|
def call(title:, body:, context:)
|
|
253
146
|
{ report_id: "rep_1", title: title, body: body }
|
|
@@ -255,7 +148,7 @@ class SaveReport < TurnKit::Tool
|
|
|
255
148
|
end
|
|
256
149
|
```
|
|
257
150
|
|
|
258
|
-
|
|
151
|
+
Register the tool:
|
|
259
152
|
|
|
260
153
|
```ruby
|
|
261
154
|
agent = TurnKit::Agent.new(
|
|
@@ -265,82 +158,87 @@ agent = TurnKit::Agent.new(
|
|
|
265
158
|
)
|
|
266
159
|
```
|
|
267
160
|
|
|
268
|
-
|
|
161
|
+
Run the tool loop:
|
|
269
162
|
|
|
270
163
|
```ruby
|
|
271
164
|
turn = agent.conversation.ask("Save a short status report.")
|
|
272
165
|
puts turn.output_text
|
|
273
166
|
```
|
|
274
167
|
|
|
275
|
-
|
|
168
|
+
Rely on TurnKit to validate tools and model-provided arguments.
|
|
169
|
+
|
|
170
|
+
### Structured Output
|
|
276
171
|
|
|
277
|
-
|
|
172
|
+
Define a schema:
|
|
278
173
|
|
|
279
174
|
```ruby
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
175
|
+
schema = {
|
|
176
|
+
type: "object",
|
|
177
|
+
properties: {
|
|
178
|
+
title: { type: "string" },
|
|
179
|
+
bullets: {
|
|
180
|
+
type: "array",
|
|
181
|
+
items: { type: "string" }
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
required: ["title", "bullets"]
|
|
185
|
+
}
|
|
186
|
+
```
|
|
285
187
|
|
|
286
|
-
|
|
287
|
-
parameter :search_queries, :array, required: false
|
|
188
|
+
Use structured output:
|
|
288
189
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
end
|
|
190
|
+
```ruby
|
|
191
|
+
agent = TurnKit::Agent.new(
|
|
192
|
+
name: "writer",
|
|
193
|
+
output_schema: schema
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
turn = agent.conversation.ask("Summarize the launch plan.")
|
|
197
|
+
puts turn.output_data
|
|
298
198
|
```
|
|
299
199
|
|
|
300
|
-
|
|
200
|
+
Override the schema per turn:
|
|
301
201
|
|
|
302
202
|
```ruby
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
203
|
+
conversation.ask(
|
|
204
|
+
"Return one decision.",
|
|
205
|
+
output_schema: {
|
|
206
|
+
type: "object",
|
|
207
|
+
properties: {
|
|
208
|
+
decision: { type: "string" }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
309
211
|
)
|
|
310
212
|
```
|
|
311
213
|
|
|
312
|
-
|
|
214
|
+
### Events
|
|
313
215
|
|
|
314
|
-
|
|
216
|
+
Subscribe globally:
|
|
315
217
|
|
|
316
218
|
```ruby
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
context.execution # The TurnKit::ToolExecution for this tool call
|
|
320
|
-
|
|
321
|
-
{ query: query }
|
|
219
|
+
TurnKit.on_event = ->(event) do
|
|
220
|
+
Rails.logger.info("turnkit.#{event.type}")
|
|
322
221
|
end
|
|
323
222
|
```
|
|
324
223
|
|
|
325
|
-
|
|
224
|
+
Subscribe per agent:
|
|
326
225
|
|
|
327
226
|
```ruby
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
227
|
+
agent = TurnKit::Agent.new(
|
|
228
|
+
name: "helper",
|
|
229
|
+
on_event: ->(event) { puts event.type }
|
|
230
|
+
)
|
|
331
231
|
```
|
|
332
232
|
|
|
333
|
-
|
|
233
|
+
Subscribe per turn:
|
|
334
234
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
| `Array` | Wrapped as `{ "items" => [...] }`. |
|
|
341
|
-
| Scalar | Wrapped as `{ "result" => value.to_s }`. |
|
|
235
|
+
```ruby
|
|
236
|
+
turn.run! do |event|
|
|
237
|
+
puts event.type
|
|
238
|
+
end
|
|
239
|
+
```
|
|
342
240
|
|
|
343
|
-
|
|
241
|
+
Use events for turns, model calls, messages, and tool calls.
|
|
344
242
|
|
|
345
243
|
### Skills
|
|
346
244
|
|
|
@@ -370,7 +268,7 @@ writer = TurnKit::Agent.new(
|
|
|
370
268
|
)
|
|
371
269
|
```
|
|
372
270
|
|
|
373
|
-
|
|
271
|
+
Register the sub-agent:
|
|
374
272
|
|
|
375
273
|
```ruby
|
|
376
274
|
editor = TurnKit::Agent.new(
|
|
@@ -386,117 +284,36 @@ turn = editor.conversation.ask("Ask the writer for three headlines.")
|
|
|
386
284
|
puts turn.output_text
|
|
387
285
|
```
|
|
388
286
|
|
|
389
|
-
|
|
287
|
+
Use sub-agents for isolated child conversations.
|
|
390
288
|
|
|
391
|
-
|
|
289
|
+
### Context Compaction
|
|
392
290
|
|
|
393
|
-
|
|
394
|
-
turn.usage.total_tokens
|
|
395
|
-
conversation.usage.total_tokens
|
|
396
|
-
agent.usage.total_tokens
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
Inspect costs:
|
|
291
|
+
Disable compaction:
|
|
400
292
|
|
|
401
293
|
```ruby
|
|
402
|
-
|
|
403
|
-
conversation.cost.total
|
|
404
|
-
agent.cost.total
|
|
294
|
+
TurnKit.compaction = false
|
|
405
295
|
```
|
|
406
296
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
Override model rates:
|
|
297
|
+
Configure compaction:
|
|
410
298
|
|
|
411
299
|
```ruby
|
|
412
|
-
TurnKit.
|
|
413
|
-
"
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
cached_input: 0.05,
|
|
417
|
-
cache_creation: 0.25
|
|
418
|
-
}
|
|
300
|
+
TurnKit.compaction = {
|
|
301
|
+
model: "gpt-4.1-mini",
|
|
302
|
+
threshold: 0.75,
|
|
303
|
+
context_limit: 128_000
|
|
419
304
|
}
|
|
420
305
|
```
|
|
421
306
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
```ruby
|
|
425
|
-
TurnKit.cost_calculator = ->(usage, model) do
|
|
426
|
-
{
|
|
427
|
-
input: usage.input_tokens * 0.25 / 1_000_000.0,
|
|
428
|
-
output: usage.output_tokens * 1.00 / 1_000_000.0
|
|
429
|
-
}
|
|
430
|
-
end
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
Limit turn cost:
|
|
307
|
+
Compact manually:
|
|
434
308
|
|
|
435
309
|
```ruby
|
|
436
|
-
|
|
437
|
-
name: "analyst",
|
|
438
|
-
cost_limit: 0.25
|
|
439
|
-
)
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
### Prompt caching
|
|
443
|
-
|
|
444
|
-
Enable prompt caching:
|
|
445
|
-
|
|
446
|
-
```ruby
|
|
447
|
-
TurnKit.prompt_cache = :auto
|
|
448
|
-
```
|
|
449
|
-
|
|
450
|
-
Disable prompt caching:
|
|
451
|
-
|
|
452
|
-
```ruby
|
|
453
|
-
TurnKit.prompt_cache = :off
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
Split custom prompts:
|
|
457
|
-
|
|
458
|
-
```ruby
|
|
459
|
-
agent = TurnKit::Agent.new(
|
|
460
|
-
name: "cached",
|
|
461
|
-
system_prompt: [
|
|
462
|
-
"Stable instructions and tool guidance.",
|
|
463
|
-
TurnKit::SystemPrompt::CACHE_BOUNDARY,
|
|
464
|
-
"Dynamic subject and live context."
|
|
465
|
-
].join("\n")
|
|
466
|
-
)
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
### Custom clients
|
|
470
|
-
|
|
471
|
-
Create a client:
|
|
472
|
-
|
|
473
|
-
```ruby
|
|
474
|
-
class MyClient < TurnKit::Client
|
|
475
|
-
def chat(model:, messages:, tools:, instructions:, temperature: nil, thinking: nil, metadata: nil)
|
|
476
|
-
TurnKit::Result.new(
|
|
477
|
-
text: "provider response",
|
|
478
|
-
model: model,
|
|
479
|
-
usage: TurnKit::Usage.new(
|
|
480
|
-
input_tokens: 100,
|
|
481
|
-
output_tokens: 20,
|
|
482
|
-
cached_tokens: 80,
|
|
483
|
-
cache_write_tokens: 100
|
|
484
|
-
)
|
|
485
|
-
)
|
|
486
|
-
end
|
|
487
|
-
end
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
Use the client:
|
|
491
|
-
|
|
492
|
-
```ruby
|
|
493
|
-
TurnKit.client = MyClient.new
|
|
310
|
+
conversation.compact!(focus: "billing migration")
|
|
494
311
|
```
|
|
495
312
|
|
|
496
|
-
|
|
313
|
+
Run the local smoke test:
|
|
497
314
|
|
|
498
|
-
```
|
|
499
|
-
|
|
315
|
+
```sh
|
|
316
|
+
ruby script/manual_compaction.rb
|
|
500
317
|
```
|
|
501
318
|
|
|
502
319
|
### Rails
|
|
@@ -507,47 +324,18 @@ Install Rails persistence:
|
|
|
507
324
|
bin/rails generate turnkit:install
|
|
508
325
|
```
|
|
509
326
|
|
|
510
|
-
The installer creates:
|
|
511
|
-
|
|
512
|
-
- `config/initializers/turnkit.rb`
|
|
513
|
-
- `app/models/turnkit/conversation.rb`
|
|
514
|
-
- `app/models/turnkit/turn.rb`
|
|
515
|
-
- `app/models/turnkit/message.rb`
|
|
516
|
-
- `app/models/turnkit/tool_execution.rb`
|
|
517
|
-
- a migration for TurnKit persistence
|
|
518
|
-
|
|
519
|
-
The generated migration currently uses `ActiveRecord::Migration[7.1]`. In a newer Rails app, update that version if your app requires it, for example `ActiveRecord::Migration[8.1]`.
|
|
520
|
-
|
|
521
327
|
Run migrations:
|
|
522
328
|
|
|
523
329
|
```sh
|
|
524
330
|
bin/rails db:migrate
|
|
525
331
|
```
|
|
526
332
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
```ruby
|
|
530
|
-
TurnKit.store = TurnKit::ActiveRecordStore.new
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
Suggested Rails file layout for your application AI code:
|
|
534
|
-
|
|
535
|
-
```text
|
|
536
|
-
app/models/assistant/
|
|
537
|
-
tools/
|
|
538
|
-
web_search.rb
|
|
539
|
-
read_web_page.rb
|
|
540
|
-
skills/
|
|
541
|
-
prompts/
|
|
542
|
-
```
|
|
543
|
-
|
|
544
|
-
If you prefer to keep AI infrastructure out of `app/models`, add an autoloaded directory such as:
|
|
333
|
+
Use this layout:
|
|
545
334
|
|
|
546
335
|
```text
|
|
547
|
-
app/ai/
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
prompts/
|
|
336
|
+
app/ai/agents/
|
|
337
|
+
app/ai/tools/
|
|
338
|
+
app/ai/skills/
|
|
551
339
|
```
|
|
552
340
|
|
|
553
341
|
Reconcile stale turns:
|
|
@@ -556,114 +344,63 @@ Reconcile stale turns:
|
|
|
556
344
|
TurnKit.reconcile_stale!
|
|
557
345
|
```
|
|
558
346
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
Inspect the latest persisted turn in a Rails console:
|
|
347
|
+
## Options
|
|
562
348
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
349
|
+
| Option | Description |
|
|
350
|
+
| --- | --- |
|
|
351
|
+
| `TurnKit.default_model` | Set the default model. |
|
|
352
|
+
| `TurnKit.client` | Set the model client. |
|
|
353
|
+
| `TurnKit.store` | Set the persistence store. |
|
|
354
|
+
| `TurnKit.max_iterations` | Limit model loop iterations. |
|
|
355
|
+
| `TurnKit.max_depth` | Limit sub-agent depth. |
|
|
356
|
+
| `TurnKit.max_tool_executions` | Limit tool calls per turn. |
|
|
357
|
+
| `TurnKit.timeout` | Limit turn runtime. |
|
|
358
|
+
| `TurnKit.cost_limit` | Limit estimated turn cost. |
|
|
359
|
+
| `TurnKit.compaction` | Configure context compaction. |
|
|
360
|
+
| `TurnKit.on_event` | Subscribe to lifecycle events. |
|
|
569
361
|
|
|
570
|
-
|
|
362
|
+
Set options globally:
|
|
571
363
|
|
|
572
364
|
```ruby
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
.map { |execution|
|
|
577
|
-
{
|
|
578
|
-
name: execution.tool_name,
|
|
579
|
-
status: execution.status,
|
|
580
|
-
arguments: execution.arguments,
|
|
581
|
-
result_keys: execution.result&.keys,
|
|
582
|
-
error: execution.error
|
|
583
|
-
}
|
|
584
|
-
}
|
|
365
|
+
TurnKit.default_model = "gpt-4.1-mini"
|
|
366
|
+
TurnKit.max_iterations = 25
|
|
367
|
+
TurnKit.timeout = 300
|
|
585
368
|
```
|
|
586
369
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
Use a model whose provider key is configured, then run a real tool-using turn:
|
|
370
|
+
Set options per agent:
|
|
590
371
|
|
|
591
372
|
```ruby
|
|
592
|
-
TurnKit.default_model = "gpt-4.1-mini"
|
|
593
|
-
|
|
594
373
|
agent = TurnKit::Agent.new(
|
|
595
|
-
name: "
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
Assistant::Tools::ReadWebPage
|
|
600
|
-
]
|
|
601
|
-
)
|
|
602
|
-
|
|
603
|
-
turn = agent.conversation.ask(
|
|
604
|
-
"Search for the TurnKit Ruby gem, read the first useful result, then summarize it."
|
|
374
|
+
name: "engineer",
|
|
375
|
+
model: "gpt-4.1-mini",
|
|
376
|
+
max_iterations: 10,
|
|
377
|
+
max_depth: 2
|
|
605
378
|
)
|
|
606
|
-
|
|
607
|
-
puts turn.output_text
|
|
608
|
-
|
|
609
|
-
pp Turnkit::ToolExecution
|
|
610
|
-
.where(turn_uid: turn.id)
|
|
611
|
-
.order(:created_at)
|
|
612
|
-
.pluck(:tool_name, :status, :error)
|
|
613
379
|
```
|
|
614
380
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
Configure defaults:
|
|
381
|
+
Enable thinking:
|
|
618
382
|
|
|
619
383
|
```ruby
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
TurnKit.cost_limit = nil
|
|
626
|
-
TurnKit.cost_rates = {}
|
|
627
|
-
TurnKit.cost_calculator = nil
|
|
628
|
-
TurnKit.prompt_cache = :auto
|
|
629
|
-
TurnKit.compaction = true
|
|
384
|
+
agent = TurnKit::Agent.new(
|
|
385
|
+
name: "reasoner",
|
|
386
|
+
model: "claude-sonnet-4-5",
|
|
387
|
+
thinking: { budget: 4_000 }
|
|
388
|
+
)
|
|
630
389
|
```
|
|
631
390
|
|
|
632
|
-
|
|
391
|
+
## Upgrading
|
|
392
|
+
|
|
393
|
+
Add `output_data` for structured output persistence.
|
|
633
394
|
|
|
634
395
|
```ruby
|
|
635
|
-
|
|
636
|
-
name: "analyst",
|
|
637
|
-
model: "gpt-4.1-mini",
|
|
638
|
-
max_iterations: 10,
|
|
639
|
-
timeout: 60,
|
|
640
|
-
cost_limit: 0.25,
|
|
641
|
-
thinking: { effort: :low }
|
|
642
|
-
)
|
|
396
|
+
add_column :turnkit_turns, :output_data, :json
|
|
643
397
|
```
|
|
644
398
|
|
|
645
|
-
|
|
646
|
-
| --- | --- |
|
|
647
|
-
| `default_model` | Set the default RubyLLM model. |
|
|
648
|
-
| `client` | Set the model client. |
|
|
649
|
-
| `store` | Set the conversation store. |
|
|
650
|
-
| `max_iterations` | Limit model calls per turn. |
|
|
651
|
-
| `timeout` | Limit seconds per root turn. |
|
|
652
|
-
| `max_tool_executions` | Limit tool calls per root turn. |
|
|
653
|
-
| `cost_limit` | Limit cost per root turn. |
|
|
654
|
-
| `thinking` | Configure provider reasoning or extended thinking per agent. |
|
|
655
|
-
| `cost_rates` | Override prices by model. |
|
|
656
|
-
| `cost_calculator` | Override cost calculation. |
|
|
657
|
-
| `prompt_cache` | Use provider prompt caching. |
|
|
658
|
-
| `compaction` | Enable, disable, or configure automatic context compaction. |
|
|
399
|
+
Skip this step for new installs.
|
|
659
400
|
|
|
660
401
|
## Contributing
|
|
661
402
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
```text
|
|
665
|
-
https://github.com/samuelcouch/turnkit
|
|
666
|
-
```
|
|
403
|
+
Fork the project.
|
|
667
404
|
|
|
668
405
|
Run tests:
|
|
669
406
|
|
|
@@ -671,6 +408,14 @@ Run tests:
|
|
|
671
408
|
bundle exec rake test
|
|
672
409
|
```
|
|
673
410
|
|
|
411
|
+
Run syntax checks:
|
|
412
|
+
|
|
413
|
+
```sh
|
|
414
|
+
find lib test examples -type f -name '*.rb' -print0 | xargs -0 ruby -c
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Open a pull request.
|
|
418
|
+
|
|
674
419
|
## License
|
|
675
420
|
|
|
676
|
-
|
|
421
|
+
Use this gem under the MIT License.
|