turnkit 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +170 -49
- data/UPGRADE.md +29 -0
- data/UPGRADE_TO_0_4_2.md +425 -0
- data/lib/{turnkit/generators → generators}/turnkit/install/templates/initializer.rb +7 -6
- data/lib/turnkit/{stores/active_record_store.rb → active_record_store.rb} +18 -4
- data/lib/turnkit/adapters/codex.rb +8 -11
- data/lib/turnkit/adapters/ruby_llm.rb +68 -44
- data/lib/turnkit/agent.rb +43 -10
- data/lib/turnkit/budget.rb +1 -1
- data/lib/turnkit/client.rb +9 -1
- data/lib/turnkit/compaction.rb +46 -46
- data/lib/turnkit/cost.rb +29 -31
- data/lib/turnkit/image_result.rb +1 -0
- data/lib/turnkit/image_tool.rb +2 -2
- data/lib/turnkit/media_analysis_result.rb +46 -0
- data/lib/turnkit/media_input.rb +208 -0
- data/lib/turnkit/message.rb +5 -1
- data/lib/turnkit/message_projection.rb +11 -0
- data/lib/turnkit/model_request.rb +4 -2
- data/lib/turnkit/output_policy.rb +10 -15
- data/lib/turnkit/result.rb +12 -0
- data/lib/turnkit/run.rb +0 -4
- data/lib/turnkit/store.rb +4 -6
- data/lib/turnkit/sub_agent_tool.rb +9 -14
- data/lib/turnkit/system_prompt.rb +32 -96
- data/lib/turnkit/tool.rb +5 -16
- data/lib/turnkit/turn.rb +113 -58
- data/lib/turnkit/version.rb +1 -1
- data/lib/turnkit/view_media_tool.rb +30 -0
- data/lib/turnkit.rb +9 -18
- metadata +18 -30
- data/lib/turnkit/prompt_contribution.rb +0 -13
- data/lib/turnkit/rails/railtie.rb +0 -9
- data/lib/turnkit/workflow.rb +0 -58
- /data/lib/{turnkit/generators → generators}/turnkit/install/templates/conversation.rb +0 -0
- /data/lib/{turnkit/generators → generators}/turnkit/install/templates/create_turnkit_tables.rb +0 -0
- /data/lib/{turnkit/generators → generators}/turnkit/install/templates/message.rb +0 -0
- /data/lib/{turnkit/generators → generators}/turnkit/install/templates/tool_execution.rb +0 -0
- /data/lib/{turnkit/generators → generators}/turnkit/install/templates/turn.rb +0 -0
- /data/lib/{turnkit/generators → generators}/turnkit/install_generator.rb +0 -0
data/UPGRADE_TO_0_4_2.md
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# Upgrade to TurnKit 0.4.2
|
|
2
|
+
|
|
3
|
+
This guide is for upgrading existing TurnKit apps to `0.4.2` from older
|
|
4
|
+
pre-1.0 versions.
|
|
5
|
+
|
|
6
|
+
## Audit summary
|
|
7
|
+
|
|
8
|
+
The current branch is not a patch-only dependency bump. It removes several
|
|
9
|
+
short-lived public APIs and consolidates the task-runner surface around
|
|
10
|
+
`TurnKit::Agent`. Treat `0.4.2` as a breaking pre-1.0 upgrade unless the removed
|
|
11
|
+
compatibility shims are restored before release.
|
|
12
|
+
|
|
13
|
+
If you only use plain `Conversation` turns with the built-in memory store and no
|
|
14
|
+
custom tools, clients, prompt contributors, workflows, or Rails store overrides,
|
|
15
|
+
the upgrade is usually small. Apps using workflow/orchestrator APIs should follow
|
|
16
|
+
the checklist below.
|
|
17
|
+
|
|
18
|
+
## 1. Update your Gemfile
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
gem "turnkit", "0.4.2"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
If you use TurnKit's default RubyLLM adapter, add RubyLLM explicitly. TurnKit no
|
|
25
|
+
longer depends on it at runtime.
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
gem "ruby_llm", "~> 1.16"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Codex adapter users and apps with a custom `TurnKit::Client` do not need
|
|
32
|
+
`ruby_llm` unless they also use `TurnKit::Adapters::RubyLLM`.
|
|
33
|
+
|
|
34
|
+
Then update your bundle:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
bundle update turnkit
|
|
38
|
+
bundle update ruby_llm # only if you added or already use ruby_llm
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 2. Replace workflows with orchestrator agents
|
|
42
|
+
|
|
43
|
+
`TurnKit::Workflow` was removed. Use `TurnKit::Agent` with
|
|
44
|
+
`orchestrator: true` instead.
|
|
45
|
+
|
|
46
|
+
Before:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
workflow = TurnKit::Workflow.new(
|
|
50
|
+
name: "brief_writer",
|
|
51
|
+
instructions: "Create source-grounded briefs.",
|
|
52
|
+
skills: [source_grounded_brief],
|
|
53
|
+
tools: [WebSearch, ReadWebPage],
|
|
54
|
+
max_spend: 0.25
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
run = workflow.run(
|
|
58
|
+
"Create a brief.",
|
|
59
|
+
input: { topic: topic }
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
After:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
agent = TurnKit::Agent.new(
|
|
67
|
+
name: "brief_writer",
|
|
68
|
+
orchestrator: true,
|
|
69
|
+
instructions: "Create source-grounded briefs.",
|
|
70
|
+
skills: [source_grounded_brief],
|
|
71
|
+
tools: [WebSearch, ReadWebPage],
|
|
72
|
+
max_spend: 0.25
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
run = agent.run(
|
|
76
|
+
"Create a brief.",
|
|
77
|
+
input: { topic: topic }
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Notes:
|
|
82
|
+
|
|
83
|
+
- `orchestrator: true` prepends the same autonomous-task preamble that workflows
|
|
84
|
+
used and defaults the agent prompt mode to `:task`.
|
|
85
|
+
- `workflow.agent(**overrides)` no longer exists. Construct another
|
|
86
|
+
`TurnKit::Agent` with the desired options.
|
|
87
|
+
- Per-run workflow overrides such as `workflow.run(..., max_spend: 0.10)` no
|
|
88
|
+
longer have a direct equivalent. Configure those limits on the agent; agents
|
|
89
|
+
are cheap to construct.
|
|
90
|
+
|
|
91
|
+
## 3. Update `Agent#run` call sites
|
|
92
|
+
|
|
93
|
+
`Agent#run` now requires the task as the first positional argument.
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
# Before
|
|
97
|
+
agent.run(task: "Classify this lead.", input: lead)
|
|
98
|
+
|
|
99
|
+
# After
|
|
100
|
+
agent.run("Classify this lead.", input: lead)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`input:`, `async:`, `subject:`, `metadata:`, and other run options remain keyword
|
|
104
|
+
arguments.
|
|
105
|
+
|
|
106
|
+
## 4. Rename global model configuration
|
|
107
|
+
|
|
108
|
+
The temporary `TurnKit.model` aliases were removed.
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
# Before
|
|
112
|
+
TurnKit.model = "gpt-4.1-mini"
|
|
113
|
+
model = TurnKit.model
|
|
114
|
+
|
|
115
|
+
# After
|
|
116
|
+
TurnKit.default_model = "gpt-4.1-mini"
|
|
117
|
+
model = TurnKit.default_model
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
In `TurnKit.configure`, use `config.default_model = ...`.
|
|
121
|
+
|
|
122
|
+
## 5. Rename run accessors
|
|
123
|
+
|
|
124
|
+
Update code that reads completed run results or tool activity:
|
|
125
|
+
|
|
126
|
+
| Before | After |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| `run.output` | `run.output_text` |
|
|
129
|
+
| `run.tool_calls` | `run.tool_executions` |
|
|
130
|
+
| `run.steps` | `run.turn_records.length` if you need the count |
|
|
131
|
+
| `run.persisted?` | remove; runs are persisted wrappers around turns |
|
|
132
|
+
|
|
133
|
+
`run.output_data`, `run.usage`, `run.cost`, `run.policy_audit`, and
|
|
134
|
+
`run.policy_clean?` remain available.
|
|
135
|
+
|
|
136
|
+
## 6. Update custom tools
|
|
137
|
+
|
|
138
|
+
TurnKit now passes framework context only as `context:`. The older
|
|
139
|
+
`turnkit_context:` keyword was removed.
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
# Before
|
|
143
|
+
def call(title:, body:, turnkit_context:)
|
|
144
|
+
turn = turnkit_context.turn
|
|
145
|
+
{ id: SaveReport.call(title:, body:, turn:) }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# After
|
|
149
|
+
def call(title:, body:, context:)
|
|
150
|
+
turn = context.turn
|
|
151
|
+
{ id: SaveReport.call(title:, body:, turn:) }
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`context` is now a reserved tool parameter name. Do not expose a model-visible
|
|
156
|
+
tool parameter named `context`:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
# Do not do this in 0.4.2
|
|
160
|
+
parameter :context, :string
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
If a tool class needs constructor arguments, register an instance instead of the
|
|
164
|
+
class:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
agent = TurnKit::Agent.new(
|
|
168
|
+
name: "researcher",
|
|
169
|
+
tools: [WebSearch.new(client: SearchClient.new)]
|
|
170
|
+
)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## 7. Update sub-agent calls
|
|
174
|
+
|
|
175
|
+
`TurnKit::SubAgentTool` exposes a single model-visible `task` parameter. If your
|
|
176
|
+
sub-agent usage previously separated `task` and `context`, include the necessary
|
|
177
|
+
context in the task text itself.
|
|
178
|
+
|
|
179
|
+
Before:
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{ "task": "Draft headlines", "context": "Product: PhotoDay" }
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
After:
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
{ "task": "Draft headlines for Product: PhotoDay." }
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## 8. Replace prompt contributor APIs
|
|
192
|
+
|
|
193
|
+
These prompt extension APIs were removed:
|
|
194
|
+
|
|
195
|
+
- `TurnKit.system_prompt_contributors`
|
|
196
|
+
- `TurnKit.model_prompt_contributors`
|
|
197
|
+
- `TurnKit::PromptContribution`
|
|
198
|
+
- prompt section override objects
|
|
199
|
+
- `TurnKit::SystemPrompt::CACHE_BOUNDARY`
|
|
200
|
+
- `TurnKit::SystemPrompt.split_cache_boundary`
|
|
201
|
+
|
|
202
|
+
Use per-agent `system_prompt:` instead. A string replaces the generated prompt.
|
|
203
|
+
A callable receives a `TurnKit::SystemPrompt` object.
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
agent = TurnKit::Agent.new(
|
|
207
|
+
name: "reporter",
|
|
208
|
+
system_prompt: ->(prompt) {
|
|
209
|
+
[
|
|
210
|
+
prompt.stable,
|
|
211
|
+
prompt.section(:tools),
|
|
212
|
+
prompt.dynamic
|
|
213
|
+
].reject(&:empty?).join("\n\n")
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Available generated-prompt APIs:
|
|
219
|
+
|
|
220
|
+
- `prompt.to_s`
|
|
221
|
+
- `prompt.stable`
|
|
222
|
+
- `prompt.dynamic`
|
|
223
|
+
- `prompt.section(:tools)` and other section names
|
|
224
|
+
|
|
225
|
+
`TurnKit.prompt_sections`, per-agent `prompt_sections:`,
|
|
226
|
+
`TurnKit.prompt_behavior`, and `TurnKit.context_contributors` remain available.
|
|
227
|
+
|
|
228
|
+
## 9. Update custom clients and adapters
|
|
229
|
+
|
|
230
|
+
TurnKit no longer sniffs custom client method signatures before calling them.
|
|
231
|
+
Custom clients should subclass `TurnKit::Client` or accept the full keyword
|
|
232
|
+
contracts.
|
|
233
|
+
|
|
234
|
+
At minimum, update `chat` to accept `dynamic_instructions:`:
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
def chat(model:, messages:, tools:, instructions:, dynamic_instructions: nil,
|
|
238
|
+
temperature: nil, thinking: nil, output_schema: nil, metadata: nil,
|
|
239
|
+
on_event: nil)
|
|
240
|
+
full_prompt = [instructions, dynamic_instructions].compact.join("\n\n")
|
|
241
|
+
# call provider...
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
If your client supports image generation or media analysis, also accept the full
|
|
246
|
+
`paint` and `view_media` keyword sets from `TurnKit::Client`, including
|
|
247
|
+
`metadata:` and `on_event:`.
|
|
248
|
+
|
|
249
|
+
Prompt caching changed from an embedded cache-boundary marker to explicit stable
|
|
250
|
+
and dynamic instructions. Adapters that support provider prompt caching should
|
|
251
|
+
cache `instructions` and append `dynamic_instructions` per turn.
|
|
252
|
+
|
|
253
|
+
## 10. Update custom stores
|
|
254
|
+
|
|
255
|
+
Built-in `TurnKit::MemoryStore` and `TurnKit::ActiveRecordStore` already support
|
|
256
|
+
the new claiming behavior.
|
|
257
|
+
|
|
258
|
+
If you maintain a custom store, implement `claim_turn` as an atomic
|
|
259
|
+
compare-and-set from `from` to `to`. It must return the claimed turn record when
|
|
260
|
+
the claim succeeds and `nil` when another worker already claimed or completed
|
|
261
|
+
the turn.
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
def claim_turn(id, from: "pending", to: "running", **attributes)
|
|
265
|
+
# Atomically update only when uid/id and status both match.
|
|
266
|
+
# Return nil if no row was updated.
|
|
267
|
+
end
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The old default implementation was intentionally removed because it was not safe
|
|
271
|
+
for concurrent workers.
|
|
272
|
+
|
|
273
|
+
## 11. Update Rails store configuration
|
|
274
|
+
|
|
275
|
+
The generated table and model names remain compatible, but custom Active Record
|
|
276
|
+
class configuration moved from global TurnKit attributes to the store
|
|
277
|
+
constructor.
|
|
278
|
+
|
|
279
|
+
Before:
|
|
280
|
+
|
|
281
|
+
```ruby
|
|
282
|
+
TurnKit.store = TurnKit::ActiveRecordStore.new
|
|
283
|
+
TurnKit.conversation_record_class = "My::Conversation"
|
|
284
|
+
TurnKit.turn_record_class = "My::Turn"
|
|
285
|
+
TurnKit.message_record_class = "My::Message"
|
|
286
|
+
TurnKit.tool_execution_record_class = "My::ToolExecution"
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
After:
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
TurnKit.store = TurnKit::ActiveRecordStore.new(
|
|
293
|
+
conversation_class: "My::Conversation",
|
|
294
|
+
turn_class: "My::Turn",
|
|
295
|
+
message_class: "My::Message",
|
|
296
|
+
tool_execution_class: "My::ToolExecution"
|
|
297
|
+
)
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Apps that used `require "turnkit"` can keep doing that. If you directly required
|
|
301
|
+
the old internal Active Record store path, update it:
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
# Before
|
|
305
|
+
require "turnkit/stores/active_record_store"
|
|
306
|
+
|
|
307
|
+
# After
|
|
308
|
+
require "turnkit/active_record_store"
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
If you manually required `turnkit/rails/railtie`, remove that require. The Rails
|
|
312
|
+
install generator now lives at the conventional `lib/generators` path and should
|
|
313
|
+
still be available as:
|
|
314
|
+
|
|
315
|
+
```sh
|
|
316
|
+
bin/rails generate turnkit:install
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## 12. Check database migrations for older Rails installs
|
|
320
|
+
|
|
321
|
+
New installs already include all current columns.
|
|
322
|
+
|
|
323
|
+
Existing Rails installs from before structured/image/media output persistence
|
|
324
|
+
should add `output_data` to turns if they do not already have it:
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
class AddOutputDataToTurnkitTurns < ActiveRecord::Migration[7.1]
|
|
328
|
+
def change
|
|
329
|
+
add_column :turnkit_turns, :output_data, :json
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
If you customized the table prefix when installing TurnKit, use your actual turns
|
|
335
|
+
table name instead of `turnkit_turns`.
|
|
336
|
+
|
|
337
|
+
Upgrades from the `0.2.x` series also need the `0.3.0` message-schema migration:
|
|
338
|
+
message content is stored as ordered typed parts in `turnkit_messages.content`,
|
|
339
|
+
with text derived from content. For greenfield or disposable development data,
|
|
340
|
+
the simplest route is usually to regenerate the current install migration and
|
|
341
|
+
recreate the TurnKit tables.
|
|
342
|
+
|
|
343
|
+
## 13. Update custom cost rates
|
|
344
|
+
|
|
345
|
+
`TurnKit.cost_rates` now accepts only these component keys, expressed as USD per
|
|
346
|
+
million tokens:
|
|
347
|
+
|
|
348
|
+
- `input`
|
|
349
|
+
- `output`
|
|
350
|
+
- `cache_read`
|
|
351
|
+
- `cache_write`
|
|
352
|
+
- `thinking`
|
|
353
|
+
|
|
354
|
+
Before:
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
TurnKit.cost_rates["gpt-5"] = {
|
|
358
|
+
input: 1.25,
|
|
359
|
+
output: 10.0,
|
|
360
|
+
cached_input: 0.125,
|
|
361
|
+
reasoning: 10.0
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
After:
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
TurnKit.cost_rates["gpt-5"] = {
|
|
369
|
+
input: 1.25,
|
|
370
|
+
output: 10.0,
|
|
371
|
+
cache_read: 0.125,
|
|
372
|
+
thinking: 10.0
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Old aliases such as `cached_input`, `cache_creation`, `reasoning`, and
|
|
377
|
+
`*_per_million` now raise `TurnKit::ConfigError`.
|
|
378
|
+
|
|
379
|
+
## 14. Update media analysis checks
|
|
380
|
+
|
|
381
|
+
`MediaAnalysisResult#structured?` was removed. Use `data?` instead.
|
|
382
|
+
|
|
383
|
+
```ruby
|
|
384
|
+
# Before
|
|
385
|
+
analysis.structured?
|
|
386
|
+
|
|
387
|
+
# After
|
|
388
|
+
analysis.data?
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
## 15. Non-breaking changes to know about
|
|
392
|
+
|
|
393
|
+
- Turn runtime state such as iteration counts and output-policy audits now lives
|
|
394
|
+
under `options["state"]`. Reads fall back to the older top-level keys, so no
|
|
395
|
+
data migration is required for this change.
|
|
396
|
+
- Compaction examples now use symbol keys, but string keys are still accepted.
|
|
397
|
+
- Built-in memory and Active Record stores already implement atomic turn claims.
|
|
398
|
+
- Generated Rails model/table names still work. Only custom class configuration
|
|
399
|
+
moved to `ActiveRecordStore.new(...)`.
|
|
400
|
+
- Removing the transitive `ruby_llm` dependency does not affect Codex adapter or
|
|
401
|
+
custom-client users.
|
|
402
|
+
|
|
403
|
+
## 16. Suggested upgrade order
|
|
404
|
+
|
|
405
|
+
1. Create a branch and update the Gemfile.
|
|
406
|
+
2. Add `ruby_llm`, if you use the default RubyLLM adapter.
|
|
407
|
+
3. Update workflows to `Agent.new(orchestrator: true)`.
|
|
408
|
+
4. Rename `Agent#run`, run accessors, and `TurnKit.default_model` call sites.
|
|
409
|
+
5. Update custom tools, sub-agents, clients, stores, cost rates, and Rails store
|
|
410
|
+
configuration.
|
|
411
|
+
6. Add the `output_data` migration if your Rails tables do not have it.
|
|
412
|
+
7. Run the test suite and at least one real model smoke test for each configured
|
|
413
|
+
adapter.
|
|
414
|
+
|
|
415
|
+
Useful searches:
|
|
416
|
+
|
|
417
|
+
```sh
|
|
418
|
+
rg "TurnKit::Workflow|TurnKit\.model|run\.output|run\.tool_calls|run\.steps|turnkit_context|structured\?|cached_input|cache_creation|reasoning|system_prompt_contributors|model_prompt_contributors|PromptContribution|conversation_record_class|turn_record_class|message_record_class|tool_execution_record_class"
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
After the code compiles, run:
|
|
422
|
+
|
|
423
|
+
```sh
|
|
424
|
+
bundle exec rake test
|
|
425
|
+
```
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Namespaces: TurnKit:: is the gem's domain layer (agents, conversations,
|
|
4
|
+
# turns). The generated ActiveRecord models in app/models/turnkit live under
|
|
5
|
+
# Turnkit:: and are pure persistence records used by the store below.
|
|
6
|
+
#
|
|
7
|
+
# Pass custom model class names if you moved or renamed the generated models:
|
|
8
|
+
# TurnKit::ActiveRecordStore.new(conversation_class: "My::Conversation", ...)
|
|
3
9
|
TurnKit.store = TurnKit::ActiveRecordStore.new
|
|
4
10
|
|
|
5
|
-
TurnKit.conversation_record_class = "Turnkit::Conversation"
|
|
6
|
-
TurnKit.turn_record_class = "Turnkit::Turn"
|
|
7
|
-
TurnKit.message_record_class = "Turnkit::Message"
|
|
8
|
-
TurnKit.tool_execution_record_class = "Turnkit::ToolExecution"
|
|
9
|
-
|
|
10
11
|
# TurnKit.default_model = "claude-sonnet-4-5"
|
|
11
12
|
# TurnKit.max_iterations = 25
|
|
12
13
|
# TurnKit.timeout = 300
|
|
@@ -20,6 +21,6 @@ TurnKit.tool_execution_record_class = "Turnkit::ToolExecution"
|
|
|
20
21
|
# TurnKit.available_skills = TurnKit::Skill.from_directory(Rails.root.join("app/ai/skills"))
|
|
21
22
|
|
|
22
23
|
# Suggested Rails convention:
|
|
23
|
-
# - app/ai/agents/* builds TurnKit::Agent objects
|
|
24
|
+
# - app/ai/agents/* builds TurnKit::Agent objects and orchestrator agents.
|
|
24
25
|
# - app/ai/tools/* defines TurnKit::Tool subclasses.
|
|
25
26
|
# - app/ai/skills/* stores reusable Markdown skill files.
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module TurnKit
|
|
4
|
+
# Persists TurnKit records through ActiveRecord model classes generated by
|
|
5
|
+
# `rails g turnkit:install`.
|
|
6
|
+
#
|
|
7
|
+
# Note the namespaces: TurnKit:: is this gem's domain layer; the generated
|
|
8
|
+
# Rails models live under Turnkit:: (Rails-inflection friendly) and are pure
|
|
9
|
+
# persistence records. Turnkit::Conversation (a database row) is not
|
|
10
|
+
# TurnKit::Conversation (the domain object).
|
|
4
11
|
class ActiveRecordStore < Store
|
|
12
|
+
def initialize(conversation_class: "Turnkit::Conversation", turn_class: "Turnkit::Turn", message_class: "Turnkit::Message", tool_execution_class: "Turnkit::ToolExecution")
|
|
13
|
+
@conversation_class_name = conversation_class
|
|
14
|
+
@turn_class_name = turn_class
|
|
15
|
+
@message_class_name = message_class
|
|
16
|
+
@tool_execution_class_name = tool_execution_class
|
|
17
|
+
end
|
|
18
|
+
|
|
5
19
|
def create_conversation(attributes)
|
|
6
20
|
record = conversation_class.create!(record_attributes(attributes, id_key: "uid"))
|
|
7
21
|
conversation_hash(record)
|
|
@@ -145,10 +159,10 @@ module TurnKit
|
|
|
145
159
|
end
|
|
146
160
|
|
|
147
161
|
private
|
|
148
|
-
def conversation_class = constantize(
|
|
149
|
-
def turn_class = constantize(
|
|
150
|
-
def message_class = constantize(
|
|
151
|
-
def tool_execution_class = constantize(
|
|
162
|
+
def conversation_class = constantize(@conversation_class_name)
|
|
163
|
+
def turn_class = constantize(@turn_class_name)
|
|
164
|
+
def message_class = constantize(@message_class_name)
|
|
165
|
+
def tool_execution_class = constantize(@tool_execution_class_name)
|
|
152
166
|
|
|
153
167
|
def constantize(name)
|
|
154
168
|
name.to_s.split("::").inject(Object) { |mod, part| mod.const_get(part) }
|
|
@@ -22,9 +22,12 @@ module TurnKit
|
|
|
22
22
|
|
|
23
23
|
def validate!(model:)
|
|
24
24
|
raise ModelAccessError, "codex command is required" if command.empty?
|
|
25
|
-
raise ModelAccessError, "#{command.inspect} was not found. Install OpenAI Codex CLI and run `codex login --device-auth`." unless executable?(command)
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
begin
|
|
27
|
+
stdout, stderr, status = @runner.call([ command, "login", "status" ], stdin_data: nil, chdir: working_directory)
|
|
28
|
+
rescue Errno::ENOENT
|
|
29
|
+
raise ModelAccessError, "#{command.inspect} was not found. Install OpenAI Codex CLI and run `codex login --device-auth`."
|
|
30
|
+
end
|
|
28
31
|
return true if status.success?
|
|
29
32
|
|
|
30
33
|
message = [ stderr, stdout ].join("\n").strip
|
|
@@ -32,12 +35,13 @@ module TurnKit
|
|
|
32
35
|
raise ModelAccessError, [ "Codex is not authenticated.", message, hint ].reject(&:empty?).join(" ")
|
|
33
36
|
end
|
|
34
37
|
|
|
35
|
-
def chat(model:, messages:, tools:, instructions:, temperature: nil, thinking: nil, output_schema: nil, metadata: nil, on_event: nil)
|
|
38
|
+
def chat(model:, messages:, tools:, instructions:, dynamic_instructions: nil, temperature: nil, thinking: nil, output_schema: nil, metadata: nil, on_event: nil)
|
|
36
39
|
raise ToolError, "TurnKit tools are not supported by the Codex adapter; Codex uses its own local tools" if Array(tools).any?
|
|
37
40
|
|
|
41
|
+
full_instructions = [ instructions.to_s, dynamic_instructions.to_s ].reject(&:empty?).join("\n\n")
|
|
38
42
|
with_tempfiles(output_schema: output_schema) do |schema_file, output_file|
|
|
39
43
|
command = exec_command(model: model, schema_file: schema_file&.path, output_file: output_file.path)
|
|
40
|
-
stdout, stderr, status = @runner.call(command, stdin_data: prompt_for(messages: messages, instructions:
|
|
44
|
+
stdout, stderr, status = @runner.call(command, stdin_data: prompt_for(messages: messages, instructions: full_instructions), chdir: working_directory)
|
|
41
45
|
emit_codex_events(stdout, on_event: on_event)
|
|
42
46
|
raise ModelAccessError, stderr.strip.empty? ? "codex exec failed" : stderr.strip unless status.success?
|
|
43
47
|
|
|
@@ -144,13 +148,6 @@ module TurnKit
|
|
|
144
148
|
end
|
|
145
149
|
end
|
|
146
150
|
|
|
147
|
-
def executable?(name)
|
|
148
|
-
return true if @runner != method(:run_command)
|
|
149
|
-
return File.executable?(name) if name.include?(File::SEPARATOR)
|
|
150
|
-
|
|
151
|
-
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? { |path| File.executable?(File.join(path, name)) }
|
|
152
|
-
end
|
|
153
|
-
|
|
154
151
|
def run_command(command, stdin_data:, chdir:)
|
|
155
152
|
stdout, stderr, status = Open3.capture3(*command, stdin_data: stdin_data, chdir: chdir)
|
|
156
153
|
[ stdout, stderr, status ]
|