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/lib/turnkit/tool.rb
CHANGED
|
@@ -25,6 +25,7 @@ module TurnKit
|
|
|
25
25
|
name = name.to_s
|
|
26
26
|
raise ArgumentError, "unknown parameter type: #{type}" unless TYPES.include?(type)
|
|
27
27
|
raise ArgumentError, "invalid parameter name: #{name}" unless NAME_PATTERN.match?(name)
|
|
28
|
+
raise ArgumentError, "context is a reserved parameter name" if name == "context"
|
|
28
29
|
raise ArgumentError, "duplicate parameter: #{name}" if parameters.any? { |param| param.fetch(:name) == name }
|
|
29
30
|
raise ArgumentError, "enum values are required for enum parameter: #{name}" if type == :enum && Array(enum).empty?
|
|
30
31
|
|
|
@@ -113,23 +114,15 @@ module TurnKit
|
|
|
113
114
|
end
|
|
114
115
|
|
|
115
116
|
def call(arguments = {}, context:)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
rescue ArgumentError => error
|
|
119
|
-
raise if error.message !~ /wrong number of arguments|missing keyword/
|
|
117
|
+
required = instance_method(:initialize).parameters.any? { |kind, _| %i[req keyreq].include?(kind) }
|
|
118
|
+
raise ToolError, "#{tool_name} requires constructor arguments; register an instance instead" if required
|
|
120
119
|
|
|
121
|
-
|
|
122
|
-
end
|
|
123
|
-
invoke(instance, arguments, context: context)
|
|
120
|
+
invoke(new, arguments, context: context)
|
|
124
121
|
end
|
|
125
122
|
|
|
126
123
|
def invoke(instance, arguments = {}, context:)
|
|
127
124
|
keyword_arguments = symbolize(validate_arguments(arguments))
|
|
128
|
-
|
|
129
|
-
instance.call(**keyword_arguments, turnkit_context: context)
|
|
130
|
-
else
|
|
131
|
-
instance.call(**keyword_arguments, context: context)
|
|
132
|
-
end
|
|
125
|
+
instance.call(**keyword_arguments, context: context)
|
|
133
126
|
end
|
|
134
127
|
|
|
135
128
|
private
|
|
@@ -172,10 +165,6 @@ module TurnKit
|
|
|
172
165
|
SchemaCheck.validate!(value, schema_for(param), error_class: ToolValidationError, label: param.fetch(:name))
|
|
173
166
|
end
|
|
174
167
|
|
|
175
|
-
def accepts_turnkit_context?(instance)
|
|
176
|
-
instance.method(:call).parameters.any? { |kind, name| %i[key keyreq].include?(kind) && name == :turnkit_context }
|
|
177
|
-
end
|
|
178
|
-
|
|
179
168
|
def symbolize(hash)
|
|
180
169
|
hash.transform_keys(&:to_sym)
|
|
181
170
|
end
|
data/lib/turnkit/turn.rb
CHANGED
|
@@ -43,7 +43,7 @@ module TurnKit
|
|
|
43
43
|
@started_at = @record["started_at"]
|
|
44
44
|
emit("turn.started", status: status, model: model)
|
|
45
45
|
agent.effective_client.validate!(model: model)
|
|
46
|
-
@budget = Budget.resume(store: store, root_turn_id: root_turn_id, limits: budget_limits)
|
|
46
|
+
@budget = Budget.resume(store: store, root_turn_id: root_turn_id, limits: agent.budget_limits)
|
|
47
47
|
revisions_used = 0
|
|
48
48
|
loop do
|
|
49
49
|
budget.check!(depth: depth)
|
|
@@ -113,7 +113,15 @@ module TurnKit
|
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
def policy_audit
|
|
116
|
-
|
|
116
|
+
options = @record["options"] || {}
|
|
117
|
+
options.dig("state", "policy_audit") || options["policy_audit"]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Reads iterations from options["state"], falling back to the legacy
|
|
121
|
+
# top-level key for turns persisted before the state split.
|
|
122
|
+
def self.iterations_for(record)
|
|
123
|
+
options = record["options"] || {}
|
|
124
|
+
(options.dig("state", "iterations") || options["iterations"]).to_i
|
|
117
125
|
end
|
|
118
126
|
|
|
119
127
|
def usage
|
|
@@ -168,23 +176,13 @@ module TurnKit
|
|
|
168
176
|
end
|
|
169
177
|
|
|
170
178
|
def paint(prompt, model:, provider: nil, size: nil, assume_model_exists: nil, input_images: nil, mask: nil, params: {}, metadata: {}, client: nil)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
when "pending"
|
|
174
|
-
claimed = store.claim_turn(id, from: "pending", to: "running", started_at: Clock.now, heartbeat_at: Clock.now)
|
|
175
|
-
raise Error, "turn is already running" unless claimed
|
|
176
|
-
|
|
179
|
+
claimed = claim_standalone!("paint")
|
|
180
|
+
if claimed
|
|
177
181
|
@record = claimed
|
|
178
182
|
@started_at = @record["started_at"]
|
|
179
|
-
@budget = Budget.resume(store: store, root_turn_id: root_turn_id, limits: budget_limits)
|
|
180
|
-
claimed_standalone = true
|
|
181
183
|
emit("turn.started", status: status, model: model)
|
|
182
|
-
|
|
183
|
-
# Image tools call this while their parent turn is running.
|
|
184
|
-
else
|
|
185
|
-
raise Error, "cannot paint for #{status} turn"
|
|
184
|
+
@budget = Budget.resume(store: store, root_turn_id: root_turn_id, limits: agent.budget_limits)
|
|
186
185
|
end
|
|
187
|
-
|
|
188
186
|
image_client = client || agent.effective_client
|
|
189
187
|
request = {
|
|
190
188
|
prompt: prompt,
|
|
@@ -201,41 +199,75 @@ module TurnKit
|
|
|
201
199
|
image_client.validate!(model: model)
|
|
202
200
|
emit("image.requested", request.except(:input_images, :mask))
|
|
203
201
|
result = call_image_client(image_client, request)
|
|
204
|
-
result_cost =
|
|
205
|
-
add_usage!(result.usage, cost: result_cost)
|
|
206
|
-
budget.add_cost!(result_cost.total)
|
|
202
|
+
result_cost = apply_result_cost(result, model: model)
|
|
207
203
|
image = result.images.first
|
|
208
204
|
raise Error, "image client returned no image" unless image
|
|
209
205
|
raise Error, "image client returned image without url or data" if image.url.to_s.empty? && image.data.to_s.empty?
|
|
210
206
|
|
|
211
207
|
persist_image_message(image)
|
|
212
208
|
emit("image.completed", image: image.to_h, model: image.model || model, provider: image.provider || provider&.to_s, mime_type: image.mime_type, usage: result.usage.to_h, cost: result_cost.to_h, metadata: metadata || {})
|
|
213
|
-
complete_with_output(image.url.to_s, output_data: { "type" => "image", "images" => [ image.to_h ] }, audit: check_policy(image.url.to_s, output_data: { "type" => "image", "images" => [ image.to_h ] })) if
|
|
209
|
+
complete_with_output(image.url.to_s, output_data: { "type" => "image", "images" => [ image.to_h ] }, audit: check_policy(image.url.to_s, output_data: { "type" => "image", "images" => [ image.to_h ] })) if claimed
|
|
214
210
|
image
|
|
215
211
|
rescue StandardError => error
|
|
216
|
-
if
|
|
217
|
-
|
|
218
|
-
|
|
212
|
+
fail_standalone!(error) if claimed
|
|
213
|
+
raise
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def view_media(media, objective:, model:, provider: nil, output_schema: nil, params: {}, metadata: {}, client: nil)
|
|
217
|
+
claimed = claim_standalone!("view media")
|
|
218
|
+
if claimed
|
|
219
|
+
@record = claimed
|
|
220
|
+
@started_at = @record["started_at"]
|
|
221
|
+
emit("turn.started", status: status, model: model)
|
|
222
|
+
@budget = Budget.resume(store: store, root_turn_id: root_turn_id, limits: agent.budget_limits)
|
|
219
223
|
end
|
|
224
|
+
media_input = MediaInput.wrap(media)
|
|
225
|
+
media_client = client || agent.effective_client
|
|
226
|
+
request = {
|
|
227
|
+
media: media_input,
|
|
228
|
+
objective: objective,
|
|
229
|
+
model: model,
|
|
230
|
+
provider: provider,
|
|
231
|
+
output_schema: output_schema,
|
|
232
|
+
params: params || {},
|
|
233
|
+
metadata: { turn_id: id, conversation_id: conversation.id }.merge(metadata || {})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
media_client.validate!(model: model)
|
|
237
|
+
emit("media.requested", request.except(:media).merge(media: media_input.to_h))
|
|
238
|
+
result = call_media_client(media_client, request)
|
|
239
|
+
result_cost = apply_result_cost(result, model: model)
|
|
240
|
+
analysis = result.media_analyses.first
|
|
241
|
+
raise Error, "media client returned no media analysis" unless analysis
|
|
242
|
+
|
|
243
|
+
persist_media_analysis_message(analysis)
|
|
244
|
+
output_data = { "type" => "media_analysis", "media_analyses" => [ analysis.to_h ] }
|
|
245
|
+
emit("media.completed", analysis: analysis.to_h, model: analysis.model || model, provider: analysis.provider || provider&.to_s, media: media_input.to_h, usage: result.usage.to_h, cost: result_cost.to_h, metadata: metadata || {})
|
|
246
|
+
complete_with_output(analysis.text, output_data: output_data, audit: check_policy(analysis.text, output_data: output_data)) if claimed
|
|
247
|
+
analysis
|
|
248
|
+
rescue StandardError => error
|
|
249
|
+
emit("media.failed", error: { "class" => error.class.name, "message" => error.message }, metadata: metadata || {}) if status == "running" || claimed
|
|
250
|
+
fail_standalone!(error) if claimed
|
|
220
251
|
raise
|
|
221
252
|
end
|
|
222
253
|
|
|
223
254
|
private
|
|
224
255
|
def model_request
|
|
225
256
|
prompt = SystemPrompt.new(agent: agent, turn: self, conversation: conversation, mode: prompt_mode || agent.effective_prompt_mode(turn: self))
|
|
226
|
-
instructions = case agent.system_prompt
|
|
257
|
+
instructions, dynamic_instructions = case agent.system_prompt
|
|
227
258
|
when nil
|
|
228
|
-
prompt.
|
|
259
|
+
[ prompt.stable, prompt.dynamic ]
|
|
229
260
|
when String
|
|
230
|
-
agent.system_prompt
|
|
261
|
+
[ agent.system_prompt, nil ]
|
|
231
262
|
else
|
|
232
|
-
agent.system_prompt.call(prompt).to_s
|
|
263
|
+
[ agent.system_prompt.call(prompt).to_s, nil ]
|
|
233
264
|
end
|
|
234
265
|
ModelRequest.new(
|
|
235
266
|
model: model,
|
|
236
267
|
messages: llm_messages,
|
|
237
268
|
tools: agent.effective_tools,
|
|
238
269
|
instructions: instructions,
|
|
270
|
+
dynamic_instructions: dynamic_instructions,
|
|
239
271
|
thinking: thinking,
|
|
240
272
|
output_schema: output_schema,
|
|
241
273
|
metadata: { turn_id: id, conversation_id: conversation.id },
|
|
@@ -243,38 +275,27 @@ module TurnKit
|
|
|
243
275
|
)
|
|
244
276
|
end
|
|
245
277
|
|
|
278
|
+
# Clients implement the TurnKit::Client keyword contract. See client.rb.
|
|
246
279
|
def call_client(request, client: agent.effective_client)
|
|
247
|
-
|
|
280
|
+
client.chat(
|
|
248
281
|
model: request.model,
|
|
249
282
|
messages: request.messages,
|
|
250
283
|
tools: request.tools,
|
|
251
284
|
instructions: request.instructions,
|
|
285
|
+
dynamic_instructions: request.dynamic_instructions,
|
|
252
286
|
thinking: request.thinking,
|
|
253
287
|
output_schema: request.output_schema,
|
|
254
288
|
metadata: request.metadata,
|
|
255
289
|
on_event: ->(event) { emit_event(event) }
|
|
256
|
-
|
|
257
|
-
accepted = chat_keyword_names(client)
|
|
258
|
-
kwargs = kwargs.slice(*accepted) unless accepted.include?(:keyrest)
|
|
259
|
-
client.chat(**kwargs)
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
def chat_keyword_names(client)
|
|
263
|
-
client.method(:chat).parameters.filter_map do |kind, name|
|
|
264
|
-
return [ :keyrest ] if kind == :keyrest
|
|
265
|
-
|
|
266
|
-
name if %i[key keyreq].include?(kind)
|
|
267
|
-
end
|
|
290
|
+
)
|
|
268
291
|
end
|
|
269
292
|
|
|
270
293
|
def call_image_client(client, request)
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
return client.paint(**kwargs) if kind == :keyrest
|
|
294
|
+
client.paint(**request, on_event: ->(event) { emit_event(event) })
|
|
295
|
+
end
|
|
274
296
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
client.paint(**kwargs.slice(*accepted))
|
|
297
|
+
def call_media_client(client, request)
|
|
298
|
+
client.view_media(**request, on_event: ->(event) { emit_event(event) })
|
|
278
299
|
end
|
|
279
300
|
|
|
280
301
|
def llm_messages
|
|
@@ -337,6 +358,9 @@ module TurnKit
|
|
|
337
358
|
elsif result.image?
|
|
338
359
|
message = conversation.append_message(role: "assistant", kind: "image", content: result.images.map { |image| image.to_h.merge("type" => "image") }, turn_id: id, metadata: { "output_data" => result.output_data }.compact)
|
|
339
360
|
emit("message.created", message_id: message.id, role: message.role, kind: message.kind)
|
|
361
|
+
elsif result.media_analysis?
|
|
362
|
+
message = conversation.append_message(role: "assistant", kind: "media_analysis", content: result.media_analyses.map { |analysis| analysis.to_h.merge("type" => "media_analysis") }, turn_id: id, metadata: { "output_data" => result.output_data }.compact)
|
|
363
|
+
emit("message.created", message_id: message.id, role: message.role, kind: message.kind)
|
|
340
364
|
else
|
|
341
365
|
message = conversation.append_message(role: "assistant", kind: "text", text: result.text, turn_id: id, metadata: { "output_data" => result.output_data }.compact)
|
|
342
366
|
emit("message.created", message_id: message.id, role: message.role, kind: message.kind)
|
|
@@ -348,6 +372,11 @@ module TurnKit
|
|
|
348
372
|
emit("message.created", message_id: message.id, role: message.role, kind: message.kind)
|
|
349
373
|
end
|
|
350
374
|
|
|
375
|
+
def persist_media_analysis_message(analysis)
|
|
376
|
+
message = conversation.append_message(role: "assistant", kind: "media_analysis", content: [ analysis.to_h.merge("type" => "media_analysis") ], turn_id: id, metadata: { "output_data" => { "type" => "media_analysis", "media_analyses" => [ analysis.to_h ] } })
|
|
377
|
+
emit("message.created", message_id: message.id, role: message.role, kind: message.kind)
|
|
378
|
+
end
|
|
379
|
+
|
|
351
380
|
def append_terminal_completion(runner, execution)
|
|
352
381
|
message = runner.completion_message(execution)
|
|
353
382
|
assistant = conversation.append_message(role: "assistant", kind: "text", text: message, turn_id: id)
|
|
@@ -382,8 +411,7 @@ module TurnKit
|
|
|
382
411
|
end
|
|
383
412
|
|
|
384
413
|
def persist_policy_audit(audit)
|
|
385
|
-
|
|
386
|
-
update!(options: options)
|
|
414
|
+
update_state!("policy_audit" => audit.to_h)
|
|
387
415
|
emit("output_policy.completed", clean: audit.clean?, violation_count: audit.violations.length)
|
|
388
416
|
end
|
|
389
417
|
|
|
@@ -435,23 +463,50 @@ module TurnKit
|
|
|
435
463
|
|
|
436
464
|
def count_iteration!
|
|
437
465
|
budget.count_iteration!
|
|
438
|
-
|
|
439
|
-
|
|
466
|
+
update_state!("iterations" => Turn.iterations_for(@record) + 1)
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Runtime state lives under options["state"]; the rest of options is
|
|
470
|
+
# write-once turn configuration. Reads fall back to the legacy top-level
|
|
471
|
+
# keys for turns persisted before the split.
|
|
472
|
+
def update_state!(changes)
|
|
473
|
+
options = @record["options"] || {}
|
|
474
|
+
update!(options: options.merge("state" => (options["state"] || {}).merge(changes)))
|
|
440
475
|
end
|
|
441
476
|
|
|
442
477
|
def heartbeat!
|
|
443
478
|
update!(heartbeat_at: Clock.now)
|
|
444
479
|
end
|
|
445
480
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
481
|
+
# Claims a pending turn for a standalone media call. Returns the claimed
|
|
482
|
+
# record when this call owns turn completion, nil when running inside a parent
|
|
483
|
+
# turn (media tools call paint/view_media while their turn is running).
|
|
484
|
+
# Callers perform any post-claim setup after assignment, so later errors
|
|
485
|
+
# still fail the claimed turn.
|
|
486
|
+
def claim_standalone!(action)
|
|
487
|
+
case status
|
|
488
|
+
when "pending"
|
|
489
|
+
claimed = store.claim_turn(id, from: "pending", to: "running", started_at: Clock.now, heartbeat_at: Clock.now)
|
|
490
|
+
raise Error, "turn is already running" unless claimed
|
|
491
|
+
|
|
492
|
+
claimed
|
|
493
|
+
when "running"
|
|
494
|
+
nil
|
|
495
|
+
else
|
|
496
|
+
raise Error, "cannot #{action} for #{status} turn"
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def fail_standalone!(error)
|
|
501
|
+
update!(status: "failed", error: { "class" => error.class.name, "message" => error.message }, completed_at: Clock.now)
|
|
502
|
+
emit("turn.failed", error: { "class" => error.class.name, "message" => error.message })
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def apply_result_cost(result, model:)
|
|
506
|
+
cost = Cost.from_usage(result.usage, model: result.model || model)
|
|
507
|
+
add_usage!(result.usage, cost: cost)
|
|
508
|
+
budget.add_cost!(cost.total)
|
|
509
|
+
cost
|
|
455
510
|
end
|
|
456
511
|
|
|
457
512
|
def aggregate_cost(current, cost)
|
data/lib/turnkit/version.rb
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TurnKit
|
|
4
|
+
class ViewMediaTool < Tool
|
|
5
|
+
class << self
|
|
6
|
+
%i[model provider output_schema params].each do |name|
|
|
7
|
+
define_method(name) do |value = nil|
|
|
8
|
+
instance_variable_set("@#{name}", value) unless value.nil?
|
|
9
|
+
instance_variable_get("@#{name}")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(context:, **arguments)
|
|
15
|
+
context.turn.view_media(
|
|
16
|
+
media(**arguments),
|
|
17
|
+
objective: objective(**arguments),
|
|
18
|
+
model: self.class.model,
|
|
19
|
+
provider: self.class.provider,
|
|
20
|
+
output_schema: self.class.output_schema,
|
|
21
|
+
params: self.class.params || {},
|
|
22
|
+
metadata: metadata(**arguments)
|
|
23
|
+
).to_h
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def metadata(**)
|
|
27
|
+
{}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/turnkit.rb
CHANGED
|
@@ -17,25 +17,26 @@ require_relative "turnkit/event"
|
|
|
17
17
|
require_relative "turnkit/model_request"
|
|
18
18
|
require_relative "turnkit/schema_check"
|
|
19
19
|
require_relative "turnkit/agent"
|
|
20
|
-
require_relative "turnkit/workflow"
|
|
21
20
|
require_relative "turnkit/client"
|
|
22
21
|
require_relative "turnkit/conversation"
|
|
23
22
|
require_relative "turnkit/message"
|
|
24
23
|
require_relative "turnkit/record"
|
|
25
24
|
require_relative "turnkit/image_result"
|
|
25
|
+
require_relative "turnkit/media_input"
|
|
26
|
+
require_relative "turnkit/media_analysis_result"
|
|
26
27
|
require_relative "turnkit/result"
|
|
27
28
|
require_relative "turnkit/skill"
|
|
28
29
|
require_relative "turnkit/output_audit"
|
|
29
30
|
require_relative "turnkit/output_policy"
|
|
30
31
|
require_relative "turnkit/prompt_data"
|
|
31
32
|
require_relative "turnkit/prompt_context"
|
|
32
|
-
require_relative "turnkit/prompt_contribution"
|
|
33
33
|
require_relative "turnkit/system_prompt"
|
|
34
34
|
require_relative "turnkit/store"
|
|
35
35
|
require_relative "turnkit/memory_store"
|
|
36
36
|
require_relative "turnkit/compaction"
|
|
37
37
|
require_relative "turnkit/tool"
|
|
38
38
|
require_relative "turnkit/image_tool"
|
|
39
|
+
require_relative "turnkit/view_media_tool"
|
|
39
40
|
require_relative "turnkit/tool_call"
|
|
40
41
|
require_relative "turnkit/tool_execution"
|
|
41
42
|
require_relative "turnkit/sub_agent_tool"
|
|
@@ -47,9 +48,7 @@ require_relative "turnkit/usage"
|
|
|
47
48
|
require_relative "turnkit/run"
|
|
48
49
|
require_relative "turnkit/adapters/codex"
|
|
49
50
|
require_relative "turnkit/adapters/ruby_llm"
|
|
50
|
-
require_relative "turnkit/
|
|
51
|
-
|
|
52
|
-
require_relative "turnkit/rails/railtie" if defined?(Rails)
|
|
51
|
+
require_relative "turnkit/active_record_store"
|
|
53
52
|
|
|
54
53
|
module TurnKit
|
|
55
54
|
class << self
|
|
@@ -62,10 +61,7 @@ module TurnKit
|
|
|
62
61
|
attr_accessor :cost_rates, :cost_calculator
|
|
63
62
|
attr_accessor :prompt_sections, :prompt_behavior, :available_skills
|
|
64
63
|
attr_accessor :prompt_data_max_chars, :context_contributors
|
|
65
|
-
attr_accessor :system_prompt_contributors, :model_prompt_contributors
|
|
66
64
|
attr_accessor :on_event
|
|
67
|
-
attr_accessor :conversation_record_class, :turn_record_class
|
|
68
|
-
attr_accessor :message_record_class, :tool_execution_record_class
|
|
69
65
|
end
|
|
70
66
|
|
|
71
67
|
self.default_model = "claude-sonnet-4-5"
|
|
@@ -84,8 +80,6 @@ module TurnKit
|
|
|
84
80
|
self.prompt_data_max_chars = 20_000
|
|
85
81
|
self.available_skills = []
|
|
86
82
|
self.context_contributors = []
|
|
87
|
-
self.system_prompt_contributors = []
|
|
88
|
-
self.model_prompt_contributors = {}
|
|
89
83
|
self.on_event = nil
|
|
90
84
|
self.output_policy_model = nil
|
|
91
85
|
self.output_policy_thinking = { effort: :low }
|
|
@@ -94,14 +88,6 @@ module TurnKit
|
|
|
94
88
|
yield self
|
|
95
89
|
end
|
|
96
90
|
|
|
97
|
-
def self.model
|
|
98
|
-
default_model
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def self.model=(value)
|
|
102
|
-
self.default_model = value
|
|
103
|
-
end
|
|
104
|
-
|
|
105
91
|
def self.reconcile_stale!(before: Clock.now - (timeout || 300))
|
|
106
92
|
store.find_stale_turns(before: before).each do |turn|
|
|
107
93
|
store.update_turn(turn.fetch("id"), "status" => "stale", "completed_at" => Clock.now)
|
|
@@ -116,4 +102,9 @@ module TurnKit
|
|
|
116
102
|
image_client = client || self.client
|
|
117
103
|
image_client.paint(prompt: prompt, model: model, provider: provider, size: size, assume_model_exists: assume_model_exists, input_images: input_images, mask: mask, params: params, metadata: metadata).images.first
|
|
118
104
|
end
|
|
105
|
+
|
|
106
|
+
def self.view_media(media, objective:, model:, provider: nil, output_schema: nil, params: {}, metadata: {}, client: nil)
|
|
107
|
+
media_client = client || self.client
|
|
108
|
+
media_client.view_media(media: media, objective: objective, model: model, provider: provider, output_schema: output_schema, params: params, metadata: metadata).media_analyses.first
|
|
109
|
+
end
|
|
119
110
|
end
|
metadata
CHANGED
|
@@ -1,31 +1,17 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: turnkit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sam Couch
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
12
|
-
dependencies:
|
|
13
|
-
- !ruby/object:Gem::Dependency
|
|
14
|
-
name: ruby_llm
|
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
|
16
|
-
requirements:
|
|
17
|
-
- - "~>"
|
|
18
|
-
- !ruby/object:Gem::Version
|
|
19
|
-
version: '1.14'
|
|
20
|
-
type: :runtime
|
|
21
|
-
prerelease: false
|
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
-
requirements:
|
|
24
|
-
- - "~>"
|
|
25
|
-
- !ruby/object:Gem::Version
|
|
26
|
-
version: '1.14'
|
|
11
|
+
date: 2026-07-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
27
13
|
description: TurnKit is a Ruby/Rails agent runtime for durable AI conversations, application
|
|
28
|
-
runs,
|
|
14
|
+
runs, orchestrator agents, tool calling, skills, sub-agents, context compaction,
|
|
29
15
|
and persistence.
|
|
30
16
|
email:
|
|
31
17
|
- sam@samcouch.com
|
|
@@ -37,7 +23,16 @@ files:
|
|
|
37
23
|
- LICENSE.md
|
|
38
24
|
- README.md
|
|
39
25
|
- UPGRADE.md
|
|
26
|
+
- UPGRADE_TO_0_4_2.md
|
|
27
|
+
- lib/generators/turnkit/install/templates/conversation.rb
|
|
28
|
+
- lib/generators/turnkit/install/templates/create_turnkit_tables.rb
|
|
29
|
+
- lib/generators/turnkit/install/templates/initializer.rb
|
|
30
|
+
- lib/generators/turnkit/install/templates/message.rb
|
|
31
|
+
- lib/generators/turnkit/install/templates/tool_execution.rb
|
|
32
|
+
- lib/generators/turnkit/install/templates/turn.rb
|
|
33
|
+
- lib/generators/turnkit/install_generator.rb
|
|
40
34
|
- lib/turnkit.rb
|
|
35
|
+
- lib/turnkit/active_record_store.rb
|
|
41
36
|
- lib/turnkit/adapters/codex.rb
|
|
42
37
|
- lib/turnkit/adapters/ruby_llm.rb
|
|
43
38
|
- lib/turnkit/agent.rb
|
|
@@ -49,17 +44,12 @@ files:
|
|
|
49
44
|
- lib/turnkit/cost.rb
|
|
50
45
|
- lib/turnkit/error.rb
|
|
51
46
|
- lib/turnkit/event.rb
|
|
52
|
-
- lib/turnkit/generators/turnkit/install/templates/conversation.rb
|
|
53
|
-
- lib/turnkit/generators/turnkit/install/templates/create_turnkit_tables.rb
|
|
54
|
-
- lib/turnkit/generators/turnkit/install/templates/initializer.rb
|
|
55
|
-
- lib/turnkit/generators/turnkit/install/templates/message.rb
|
|
56
|
-
- lib/turnkit/generators/turnkit/install/templates/tool_execution.rb
|
|
57
|
-
- lib/turnkit/generators/turnkit/install/templates/turn.rb
|
|
58
|
-
- lib/turnkit/generators/turnkit/install_generator.rb
|
|
59
47
|
- lib/turnkit/id.rb
|
|
60
48
|
- lib/turnkit/image_result.rb
|
|
61
49
|
- lib/turnkit/image_tool.rb
|
|
62
50
|
- lib/turnkit/load_skill_tool.rb
|
|
51
|
+
- lib/turnkit/media_analysis_result.rb
|
|
52
|
+
- lib/turnkit/media_input.rb
|
|
63
53
|
- lib/turnkit/memory_store.rb
|
|
64
54
|
- lib/turnkit/message.rb
|
|
65
55
|
- lib/turnkit/message_projection.rb
|
|
@@ -67,16 +57,13 @@ files:
|
|
|
67
57
|
- lib/turnkit/output_audit.rb
|
|
68
58
|
- lib/turnkit/output_policy.rb
|
|
69
59
|
- lib/turnkit/prompt_context.rb
|
|
70
|
-
- lib/turnkit/prompt_contribution.rb
|
|
71
60
|
- lib/turnkit/prompt_data.rb
|
|
72
|
-
- lib/turnkit/rails/railtie.rb
|
|
73
61
|
- lib/turnkit/record.rb
|
|
74
62
|
- lib/turnkit/result.rb
|
|
75
63
|
- lib/turnkit/run.rb
|
|
76
64
|
- lib/turnkit/schema_check.rb
|
|
77
65
|
- lib/turnkit/skill.rb
|
|
78
66
|
- lib/turnkit/store.rb
|
|
79
|
-
- lib/turnkit/stores/active_record_store.rb
|
|
80
67
|
- lib/turnkit/sub_agent_tool.rb
|
|
81
68
|
- lib/turnkit/system_prompt.rb
|
|
82
69
|
- lib/turnkit/tool.rb
|
|
@@ -86,7 +73,7 @@ files:
|
|
|
86
73
|
- lib/turnkit/turn.rb
|
|
87
74
|
- lib/turnkit/usage.rb
|
|
88
75
|
- lib/turnkit/version.rb
|
|
89
|
-
- lib/turnkit/
|
|
76
|
+
- lib/turnkit/view_media_tool.rb
|
|
90
77
|
homepage: https://github.com/samuelcouch/turnkit
|
|
91
78
|
licenses:
|
|
92
79
|
- MIT
|
|
@@ -114,5 +101,6 @@ requirements: []
|
|
|
114
101
|
rubygems_version: 3.5.22
|
|
115
102
|
signing_key:
|
|
116
103
|
specification_version: 4
|
|
117
|
-
summary: Ruby/Rails agent runtime for durable AI conversations, runs, and
|
|
104
|
+
summary: Ruby/Rails agent runtime for durable AI conversations, runs, and orchestrator
|
|
105
|
+
agents.
|
|
118
106
|
test_files: []
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module TurnKit
|
|
4
|
-
class PromptContribution
|
|
5
|
-
attr_accessor :stable_prefix, :dynamic_suffix, :section_overrides
|
|
6
|
-
|
|
7
|
-
def initialize(stable_prefix: nil, dynamic_suffix: nil, section_overrides: nil)
|
|
8
|
-
@stable_prefix = stable_prefix.to_s
|
|
9
|
-
@dynamic_suffix = dynamic_suffix.to_s
|
|
10
|
-
@section_overrides = (section_overrides || {}).transform_keys(&:to_sym)
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
data/lib/turnkit/workflow.rb
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "agent"
|
|
4
|
-
|
|
5
|
-
module TurnKit
|
|
6
|
-
class Workflow
|
|
7
|
-
ORCHESTRATOR_PREAMBLE = <<~TEXT.strip
|
|
8
|
-
You are an autonomous task orchestrator. Navigate from the application
|
|
9
|
-
request to a final output without asking the user follow-up questions.
|
|
10
|
-
|
|
11
|
-
Use the available tools to gather context, inspect sources, take actions,
|
|
12
|
-
persist outputs, and verify work. Use loaded skills as reusable workflow
|
|
13
|
-
patterns. Iterate when work needs missing context, critique, revision, or
|
|
14
|
-
verification.
|
|
15
|
-
|
|
16
|
-
When multiple independent items need the same kind of fetch or read, and
|
|
17
|
-
an available batch tool can handle them in one call, prefer the batch tool
|
|
18
|
-
over repeated one-item tool calls.
|
|
19
|
-
|
|
20
|
-
Stop when the task is complete, when the available context and tools are
|
|
21
|
-
sufficient for the best possible answer, or when further iteration would
|
|
22
|
-
not materially improve the result. Respect runtime, cost, and iteration
|
|
23
|
-
limits.
|
|
24
|
-
TEXT
|
|
25
|
-
|
|
26
|
-
DEFAULT_INSTRUCTIONS = ORCHESTRATOR_PREAMBLE
|
|
27
|
-
|
|
28
|
-
attr_reader :name, :options
|
|
29
|
-
|
|
30
|
-
def initialize(name: "workflow", instructions: nil, preamble: true, **options)
|
|
31
|
-
@name = name.to_s
|
|
32
|
-
raise ArgumentError, "name is required" if @name.empty?
|
|
33
|
-
|
|
34
|
-
@options = options.merge(
|
|
35
|
-
name: @name,
|
|
36
|
-
prompt_mode: options.fetch(:prompt_mode, :task),
|
|
37
|
-
instructions: compose_instructions(instructions, preamble: preamble)
|
|
38
|
-
).freeze
|
|
39
|
-
@agent = Agent.new(**@options)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def run(prompt = nil, task: nil, input: nil, async: false, subject: nil, metadata: {}, **overrides)
|
|
43
|
-
agent(**overrides).run(task || prompt, input: input, async: async, subject: subject, metadata: metadata)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def agent(**overrides)
|
|
47
|
-
overrides.empty? ? @agent : Agent.new(**@options.merge(overrides.compact))
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
def compose_instructions(instructions, preamble:)
|
|
52
|
-
parts = []
|
|
53
|
-
parts << ORCHESTRATOR_PREAMBLE if preamble
|
|
54
|
-
parts << instructions.to_s.strip unless instructions.to_s.strip.empty?
|
|
55
|
-
parts.join("\n\n")
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
File without changes
|
/data/lib/{turnkit/generators → generators}/turnkit/install/templates/create_turnkit_tables.rb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|