turnkit 0.2.6 → 0.2.8

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.
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.1-red.svg)](https://www.ruby-lang.org)
5
5
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE.md)
6
6
 
7
- Build durable Ruby AI agents with turns, tools, skills, and Rails persistence.
7
+ Build durable Ruby and Rails agents with tools, skills, sub-agents, and persistence.
8
8
 
9
9
  ## Installation
10
10
 
@@ -20,23 +20,16 @@ Run:
20
20
  bundle install
21
21
  ```
22
22
 
23
+ Upgrading from an earlier TurnKit version? See the [Upgrade Guide](UPGRADE.md).
24
+
23
25
  ## Quick Start
24
26
 
25
- Set a provider key. TurnKit uses RubyLLM under the hood and defaults to Anthropic Claude:
27
+ Set an API key:
26
28
 
27
29
  ```sh
28
30
  export ANTHROPIC_API_KEY=...
29
31
  ```
30
32
 
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
33
  Create an agent:
41
34
 
42
35
  ```ruby
@@ -55,73 +48,49 @@ turn = agent.conversation.ask("Explain Ruby blocks in one sentence.")
55
48
  puts turn.output_text
56
49
  ```
57
50
 
58
- ## Usage
59
-
60
- ### Models
61
-
62
- Set the default model:
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:
51
+ Or run a non-interactive application task:
75
52
 
76
53
  ```ruby
77
- TurnKit.default_model = "gpt-4.1-mini"
54
+ run = agent.run("Explain Ruby blocks in one sentence.")
55
+ puts run.output
78
56
  ```
79
57
 
80
- Use Gemini:
58
+ ## Usage
81
59
 
82
- ```sh
83
- export GEMINI_API_KEY=...
84
- ```
60
+ ### Models
85
61
 
86
- Set a Gemini model:
62
+ Set a model:
87
63
 
88
64
  ```ruby
89
- TurnKit.default_model = "gemini-2.5-flash"
65
+ TurnKit.model = "gpt-4.1-mini"
90
66
  ```
91
67
 
92
- ### Thinking
93
-
94
- Enable provider reasoning or extended thinking per agent:
68
+ Or configure TurnKit in one place:
95
69
 
96
70
  ```ruby
97
- agent = TurnKit::Agent.new(
98
- name: "reasoner",
99
- model: "claude-sonnet-4-5",
100
- thinking: { budget: 4_000 }
101
- )
71
+ TurnKit.configure do |config|
72
+ config.model = "gpt-4.1-mini"
73
+ config.max_spend = 0.25
74
+ config.max_iterations = 12
75
+ end
102
76
  ```
103
77
 
104
- Use effort-based thinking for providers that support it:
78
+ Set the matching key:
105
79
 
106
- ```ruby
107
- agent = TurnKit::Agent.new(
108
- name: "reasoner",
109
- model: "gemini-2.5-flash",
110
- thinking: { effort: :high }
111
- )
80
+ ```sh
81
+ export OPENAI_API_KEY=...
112
82
  ```
113
83
 
114
- Override or disable thinking for one turn:
84
+ Use these common providers:
115
85
 
116
- ```ruby
117
- conversation = agent.conversation
118
- conversation.ask("Solve this carefully.", thinking: { budget: 8_000 })
119
- conversation.ask("Answer quickly.", thinking: nil)
120
- ```
121
-
122
- TurnKit passes `thinking` to RubyLLM as `{ effort:, budget: }`. Anthropic requires `budget`; Gemini and OpenRouter can use `effort`, `budget`, or both depending on the model.
86
+ | Provider | Key | Model |
87
+ | --- | --- | --- |
88
+ | Anthropic | `ANTHROPIC_API_KEY` | `claude-sonnet-4-5` |
89
+ | OpenAI | `OPENAI_API_KEY` | `gpt-4.1-mini` |
90
+ | Gemini | `GEMINI_API_KEY` | `gemini-2.5-flash` |
91
+ | OpenRouter | `OPENROUTER_API_KEY` | `openrouter/...` |
123
92
 
124
- When the provider reports reasoning usage, TurnKit records it as `thinking_tokens` and includes it in usage totals and cost calculation.
93
+ Expect `TurnKit::ModelAccessError` for obvious key mistakes.
125
94
 
126
95
  ### Conversations
127
96
 
@@ -132,12 +101,13 @@ agent = TurnKit::Agent.new(
132
101
  name: "writer",
133
102
  instructions: "Write clear release notes."
134
103
  )
104
+
105
+ conversation = agent.conversation(subject: "v1 launch")
135
106
  ```
136
107
 
137
108
  Add context:
138
109
 
139
110
  ```ruby
140
- conversation = agent.conversation(subject: "v1 launch")
141
111
  conversation.say("Mention faster tool execution.")
142
112
  ```
143
113
 
@@ -148,91 +118,146 @@ turn = conversation.run!
148
118
  puts turn.output_text
149
119
  ```
150
120
 
151
- ### Context compaction
121
+ ### Application Tasks
152
122
 
153
- TurnKit automatically compacts long conversations. Older messages are summarized for future model calls, while the original transcript remains stored durably.
123
+ Use `Agent#run` when your application is executing a task instead of chatting
124
+ with a user:
154
125
 
155
126
  ```ruby
156
- conversation = agent.conversation
157
- conversation.ask("Work through this long task.")
127
+ agent = TurnKit::Agent.new(
128
+ name: "lead_classifier",
129
+ instructions: "Classify leads and return routing data.",
130
+ output_schema: {
131
+ type: "object",
132
+ properties: {
133
+ priority: { type: "string" },
134
+ reason: { type: "string" }
135
+ },
136
+ required: ["priority", "reason"]
137
+ },
138
+ prompt_mode: :task
139
+ )
140
+
141
+ run = agent.run(
142
+ "Classify this lead.",
143
+ input: { company: "Acme", employees: 1_200 }
144
+ )
145
+
146
+ puts run.output_data
158
147
  ```
159
148
 
160
- By default, compaction is enabled and uses the current turn model for the summary call. If a turn runs with `gpt-5`, compaction uses `gpt-5` unless you configure a separate summary model.
149
+ `Agent#run` is a small wrapper over TurnKit's existing conversation and turn
150
+ engine. Existing `conversation.ask` usage is still supported.
161
151
 
162
- Disable compaction globally:
152
+ Prepare a pending run without calling the model:
163
153
 
164
154
  ```ruby
165
- TurnKit.compaction = false
155
+ run = agent.run(task: "Classify later.", async: true)
156
+ request = run.preview
157
+ run.run!
166
158
  ```
167
159
 
168
- Use a different model for summaries:
160
+ ### Fleets
161
+
162
+ Use a fleet when you want to package a reusable autonomous workflow: one
163
+ task-mode orchestrator, workflow skills, tools, defaults, and guardrails. A
164
+ fleet is not a requirement for multi-agent work; it is the reusable runtime for
165
+ getting from input to output.
169
166
 
170
167
  ```ruby
171
- TurnKit.compaction = {
172
- model: "gpt-4.1-mini"
173
- }
168
+ source_grounded_brief = TurnKit::Skill.from_file("app/ai/skills/source_grounded_brief.md")
169
+
170
+ fleet = TurnKit.fleet(
171
+ "brief_writer",
172
+ instructions: "Create source-grounded briefs and verify claims before final output.",
173
+ skills: [source_grounded_brief],
174
+ tools: [WebSearch.new, ReadWebPage.new, SaveBrief],
175
+ max_spend: 0.25,
176
+ max_iterations: 12,
177
+ max_tool_executions: 25,
178
+ compaction: {
179
+ context_limit: 64_000,
180
+ threshold: 0.75
181
+ }
182
+ )
183
+
184
+ run = fleet.run(
185
+ "Create a source-grounded brief.",
186
+ input: { topic: "Rails 8 Solid Queue" }
187
+ )
188
+
189
+ puts run.output
190
+ puts run.tool_calls.map(&:tool_name)
191
+ puts run.cost.total
174
192
  ```
175
193
 
176
- You can also configure the compaction threshold and estimated context limit:
194
+ This keeps the work in a single conversation and uses TurnKit's normal
195
+ model-tool loop:
177
196
 
178
- ```ruby
179
- TurnKit.compaction = {
180
- model: "gpt-4.1-mini",
181
- threshold: 0.75,
182
- context_limit: 128_000
183
- }
197
+ ```text
198
+ model tool → result → model → tool → result → final
184
199
  ```
185
200
 
186
- Configure compaction for one agent:
201
+ `auto_run` is an alias for `run` when you want the name to emphasize autonomous
202
+ execution:
187
203
 
188
204
  ```ruby
189
- agent = TurnKit::Agent.new(
190
- name: "engineer",
191
- model: "gpt-5",
205
+ run = fleet.auto_run(
206
+ "Create compliant outreach for this account.",
207
+ input: lead.attributes,
208
+ max_spend: 0.25,
209
+ max_iterations: 8,
210
+ max_tool_executions: 20,
192
211
  compaction: {
193
- model: "gpt-4.1-mini",
194
- threshold: 0.75,
195
- context_limit: 128_000
212
+ context_limit: 64_000,
213
+ threshold: 0.75
196
214
  }
197
215
  )
198
216
  ```
199
217
 
200
- In this example, normal turns use `gpt-5` and compaction summaries use `gpt-4.1-mini`.
218
+ Reach for separate agents and `sub_agents` only when the isolation is worth the
219
+ extra model calls, such as different models, different tool permissions,
220
+ parallel specialist review, or separate durable child conversations.
201
221
 
202
- Override the model for one manual compaction:
222
+ Use `terminal!` for save or action tools that complete the run:
203
223
 
204
224
  ```ruby
205
- conversation.compact!(model: "gpt-4.1-mini")
206
- conversation.compact!(focus: "billing migration", model: "gpt-4.1-mini")
207
- ```
225
+ class SaveBrief < TurnKit::Tool
226
+ description "Save the final brief."
227
+ parameter :title, :string, required: true
228
+ parameter :body, :string, required: true
208
229
 
209
- Disable compaction for a single turn:
230
+ terminal! { |result| "Saved #{result.fetch("id")}." }
210
231
 
211
- ```ruby
212
- conversation.ask("Continue", compact: false)
232
+ def call(title:, body:, context:)
233
+ Brief.create!(title: title, body: body).then { |brief| { id: brief.id } }
234
+ end
235
+ end
213
236
  ```
214
237
 
215
- Manually compact a conversation:
238
+ ### Prompt Preview
239
+
240
+ Preview a pending turn:
216
241
 
217
242
  ```ruby
218
- conversation.compact!
219
- conversation.compact!(focus: "billing migration")
243
+ turn = conversation.ask("Draft the launch email.", async: true)
244
+ request = turn.preview
220
245
  ```
221
246
 
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:
247
+ Inspect the request:
225
248
 
226
- ```text
227
- user: What did we do so far?
228
- assistant: [CONTEXT COMPACTION — REFERENCE ONLY] ...
229
- user: latest request
249
+ ```ruby
250
+ request.model
251
+ request.messages
252
+ request.tool_names
253
+ request.instructions
254
+ request.report
230
255
  ```
231
256
 
232
- For a local smoke test without calling a real provider, run:
257
+ Run the reviewed turn:
233
258
 
234
- ```sh
235
- ruby script/manual_compaction.rb
259
+ ```ruby
260
+ turn.run!
236
261
  ```
237
262
 
238
263
  ### Tools
@@ -243,11 +268,15 @@ Create a tool:
243
268
  class SaveReport < TurnKit::Tool
244
269
  description "Save a report."
245
270
  usage_hint "Use when the user asks to persist a report."
271
+
246
272
  parameter :title, :string, required: true
247
273
  parameter :body, :string, required: true
248
274
 
249
275
  def self.ends_turn? = true
250
- def self.completion_message(result) = "Saved #{result.fetch("report_id")}."
276
+
277
+ def self.completion_message(result)
278
+ "Saved #{result.fetch("report_id")}."
279
+ end
251
280
 
252
281
  def call(title:, body:, context:)
253
282
  { report_id: "rep_1", title: title, body: body }
@@ -255,7 +284,7 @@ class SaveReport < TurnKit::Tool
255
284
  end
256
285
  ```
257
286
 
258
- Use the tool:
287
+ Register the tool:
259
288
 
260
289
  ```ruby
261
290
  agent = TurnKit::Agent.new(
@@ -265,82 +294,87 @@ agent = TurnKit::Agent.new(
265
294
  )
266
295
  ```
267
296
 
268
- Ask for tool use:
297
+ Run the tool loop:
269
298
 
270
299
  ```ruby
271
300
  turn = agent.conversation.ask("Save a short status report.")
272
301
  puts turn.output_text
273
302
  ```
274
303
 
275
- #### Defining application tools
304
+ Rely on TurnKit to validate tools and model-provided arguments.
276
305
 
277
- Tools are classes, not instances. Namespaced tools work fine, and the default tool name comes from the class name: `Assistant::Tools::WebSearch` becomes `web_search`.
306
+ ### Structured Output
307
+
308
+ Define a schema:
278
309
 
279
310
  ```ruby
280
- module Assistant
281
- module Tools
282
- class WebSearch < TurnKit::Tool
283
- description "Search the web for current information."
284
- usage_hint "Use when current external information is needed."
311
+ schema = {
312
+ type: "object",
313
+ properties: {
314
+ title: { type: "string" },
315
+ bullets: {
316
+ type: "array",
317
+ items: { type: "string" }
318
+ }
319
+ },
320
+ required: ["title", "bullets"]
321
+ }
322
+ ```
285
323
 
286
- parameter :objective, :string, required: true
287
- parameter :search_queries, :array, required: false
324
+ Use structured output:
288
325
 
289
- def call(objective:, search_queries: nil, context:)
290
- ParallelClient.new.web_search(
291
- objective: objective,
292
- search_queries: search_queries
293
- )
294
- end
295
- end
296
- end
297
- end
326
+ ```ruby
327
+ agent = TurnKit::Agent.new(
328
+ name: "writer",
329
+ output_schema: schema
330
+ )
331
+
332
+ turn = agent.conversation.ask("Summarize the launch plan.")
333
+ puts turn.output_data
298
334
  ```
299
335
 
300
- Register tool classes on the agent:
336
+ Override the schema per turn:
301
337
 
302
338
  ```ruby
303
- agent = TurnKit::Agent.new(
304
- name: "researcher",
305
- tools: [
306
- Assistant::Tools::WebSearch,
307
- Assistant::Tools::ReadWebPage
308
- ]
339
+ conversation.ask(
340
+ "Return one decision.",
341
+ output_schema: {
342
+ type: "object",
343
+ properties: {
344
+ decision: { type: "string" }
345
+ }
346
+ }
309
347
  )
310
348
  ```
311
349
 
312
- #### Tool context
350
+ ### Events
313
351
 
314
- Every tool receives a `context:` object. Use it for logging, correlation, persistence, and domain scoping:
352
+ Subscribe globally:
315
353
 
316
354
  ```ruby
317
- def call(query:, context:)
318
- context.turn # The TurnKit::Turn being run
319
- context.execution # The TurnKit::ToolExecution for this tool call
320
-
321
- { query: query }
355
+ TurnKit.on_event = ->(event) do
356
+ Rails.logger.info("turnkit.#{event.type}")
322
357
  end
323
358
  ```
324
359
 
325
- If your application already uses a `context:` keyword for something else, use `turnkit_context:` instead:
360
+ Subscribe per agent:
326
361
 
327
362
  ```ruby
328
- def call(query:, turnkit_context:)
329
- { turn_id: turnkit_context.turn.id, query: query }
330
- end
363
+ agent = TurnKit::Agent.new(
364
+ name: "helper",
365
+ on_event: ->(event) { puts event.type }
366
+ )
331
367
  ```
332
368
 
333
- #### Tool return values
369
+ Subscribe per turn:
334
370
 
335
- Prefer returning a `Hash`. TurnKit serializes the normalized value as the tool result:
336
-
337
- | Return value | Stored tool result |
338
- | --- | --- |
339
- | `Hash` | Keys are stringified. |
340
- | `Array` | Wrapped as `{ "items" => [...] }`. |
341
- | Scalar | Wrapped as `{ "result" => value.to_s }`. |
371
+ ```ruby
372
+ turn.run! do |event|
373
+ puts event.type
374
+ end
375
+ ```
342
376
 
343
- Avoid returning arbitrary objects unless you convert them to a plain Hash or Array first.
377
+ Use events for turns, model calls, messages, and tool calls.
344
378
 
345
379
  ### Skills
346
380
 
@@ -370,7 +404,7 @@ writer = TurnKit::Agent.new(
370
404
  )
371
405
  ```
372
406
 
373
- Delegate to it:
407
+ Register the sub-agent:
374
408
 
375
409
  ```ruby
376
410
  editor = TurnKit::Agent.new(
@@ -386,117 +420,36 @@ turn = editor.conversation.ask("Ask the writer for three headlines.")
386
420
  puts turn.output_text
387
421
  ```
388
422
 
389
- ### Usage and costs
390
-
391
- Inspect token usage:
423
+ Use sub-agents for isolated child conversations.
392
424
 
393
- ```ruby
394
- turn.usage.total_tokens
395
- conversation.usage.total_tokens
396
- agent.usage.total_tokens
397
- ```
425
+ ### Context Compaction
398
426
 
399
- Inspect costs:
427
+ Disable compaction:
400
428
 
401
429
  ```ruby
402
- turn.cost.total
403
- conversation.cost.total
404
- agent.cost.total
430
+ TurnKit.compaction = false
405
431
  ```
406
432
 
407
- Use RubyLLM registry prices by default.
408
-
409
- Override model rates:
433
+ Configure compaction:
410
434
 
411
435
  ```ruby
412
- TurnKit.cost_rates = {
413
- "my-model" => {
414
- input: 0.25,
415
- output: 1.00,
416
- cached_input: 0.05,
417
- cache_creation: 0.25
418
- }
436
+ TurnKit.compaction = {
437
+ model: "gpt-4.1-mini",
438
+ threshold: 0.75,
439
+ context_limit: 128_000
419
440
  }
420
441
  ```
421
442
 
422
- Override cost calculation:
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:
434
-
435
- ```ruby
436
- agent = TurnKit::Agent.new(
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:
443
+ Compact manually:
491
444
 
492
445
  ```ruby
493
- TurnKit.client = MyClient.new
446
+ conversation.compact!(focus: "billing migration")
494
447
  ```
495
448
 
496
- Split cache sections:
449
+ Run the local smoke test:
497
450
 
498
- ```ruby
499
- stable, dynamic = TurnKit::SystemPrompt.split_cache_boundary(instructions)
451
+ ```sh
452
+ ruby script/manual_compaction.rb
500
453
  ```
501
454
 
502
455
  ### Rails
@@ -507,47 +460,18 @@ Install Rails persistence:
507
460
  bin/rails generate turnkit:install
508
461
  ```
509
462
 
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
463
  Run migrations:
522
464
 
523
465
  ```sh
524
466
  bin/rails db:migrate
525
467
  ```
526
468
 
527
- Configure Rails:
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:
469
+ Use this layout:
545
470
 
546
471
  ```text
547
- app/ai/
548
- tools/
549
- skills/
550
- prompts/
472
+ app/ai/agents/
473
+ app/ai/tools/
474
+ app/ai/skills/
551
475
  ```
552
476
 
553
477
  Reconcile stale turns:
@@ -556,114 +480,63 @@ Reconcile stale turns:
556
480
  TurnKit.reconcile_stale!
557
481
  ```
558
482
 
559
- #### Debugging Rails persistence
560
-
561
- Inspect the latest persisted turn in a Rails console:
483
+ ## Options
562
484
 
563
- ```ruby
564
- turn = Turnkit::Turn.order(created_at: :desc).first
565
- turn.status
566
- turn.error
567
- turn.output_text
568
- ```
485
+ | Option | Description |
486
+ | --- | --- |
487
+ | `TurnKit.default_model` | Set the default model. |
488
+ | `TurnKit.client` | Set the model client. |
489
+ | `TurnKit.store` | Set the persistence store. |
490
+ | `TurnKit.max_iterations` | Limit model loop iterations. |
491
+ | `TurnKit.max_depth` | Limit sub-agent depth. |
492
+ | `TurnKit.max_tool_executions` | Limit tool calls per turn. |
493
+ | `TurnKit.timeout` | Limit turn runtime. |
494
+ | `TurnKit.cost_limit` | Limit estimated turn cost. |
495
+ | `TurnKit.compaction` | Configure context compaction. |
496
+ | `TurnKit.on_event` | Subscribe to lifecycle events. |
569
497
 
570
- Check whether the model actually called tools:
498
+ Set options globally:
571
499
 
572
500
  ```ruby
573
- Turnkit::ToolExecution
574
- .where(turn_uid: turn.uid)
575
- .order(:created_at)
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
- }
501
+ TurnKit.default_model = "gpt-4.1-mini"
502
+ TurnKit.max_iterations = 25
503
+ TurnKit.timeout = 300
585
504
  ```
586
505
 
587
- #### Live smoke test
588
-
589
- Use a model whose provider key is configured, then run a real tool-using turn:
506
+ Set options per agent:
590
507
 
591
508
  ```ruby
592
- TurnKit.default_model = "gpt-4.1-mini"
593
-
594
509
  agent = TurnKit::Agent.new(
595
- name: "researcher",
596
- instructions: "Use web_search, then read_web_page, before answering.",
597
- tools: [
598
- Assistant::Tools::WebSearch,
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."
510
+ name: "engineer",
511
+ model: "gpt-4.1-mini",
512
+ max_iterations: 10,
513
+ max_depth: 2
605
514
  )
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
515
  ```
614
516
 
615
- ## Options
616
-
617
- Configure defaults:
517
+ Enable thinking:
618
518
 
619
519
  ```ruby
620
- TurnKit.default_model = "claude-sonnet-4-5"
621
- TurnKit.max_iterations = 25
622
- TurnKit.timeout = 300
623
- TurnKit.max_depth = 3
624
- TurnKit.max_tool_executions = 100
625
- TurnKit.cost_limit = nil
626
- TurnKit.cost_rates = {}
627
- TurnKit.cost_calculator = nil
628
- TurnKit.prompt_cache = :auto
629
- TurnKit.compaction = true
520
+ agent = TurnKit::Agent.new(
521
+ name: "reasoner",
522
+ model: "claude-sonnet-4-5",
523
+ thinking: { budget: 4_000 }
524
+ )
630
525
  ```
631
526
 
632
- Override an agent:
527
+ ## Upgrading
528
+
529
+ Add `output_data` for structured output persistence.
633
530
 
634
531
  ```ruby
635
- agent = TurnKit::Agent.new(
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
- )
532
+ add_column :turnkit_turns, :output_data, :json
643
533
  ```
644
534
 
645
- | Option | Description |
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. |
535
+ Skip this step for new installs.
659
536
 
660
537
  ## Contributing
661
538
 
662
- Report bugs and open pull requests on GitHub:
663
-
664
- ```text
665
- https://github.com/samuelcouch/turnkit
666
- ```
539
+ Fork the project.
667
540
 
668
541
  Run tests:
669
542
 
@@ -671,6 +544,14 @@ Run tests:
671
544
  bundle exec rake test
672
545
  ```
673
546
 
547
+ Run syntax checks:
548
+
549
+ ```sh
550
+ find lib test examples -type f -name '*.rb' -print0 | xargs -0 ruby -c
551
+ ```
552
+
553
+ Open a pull request.
554
+
674
555
  ## License
675
556
 
676
- See the MIT License.
557
+ Use this gem under the MIT License.