turnkit 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/README.md +126 -51
  4. data/UPGRADE.md +29 -0
  5. data/UPGRADE_TO_0_4_2.md +425 -0
  6. data/lib/{turnkit/generators → generators}/turnkit/install/templates/initializer.rb +7 -6
  7. data/lib/turnkit/{stores/active_record_store.rb → active_record_store.rb} +34 -10
  8. data/lib/turnkit/adapters/codex.rb +8 -11
  9. data/lib/turnkit/adapters/ruby_llm.rb +30 -46
  10. data/lib/turnkit/agent.rb +43 -10
  11. data/lib/turnkit/budget.rb +1 -1
  12. data/lib/turnkit/client.rb +5 -1
  13. data/lib/turnkit/compaction.rb +46 -46
  14. data/lib/turnkit/cost.rb +29 -31
  15. data/lib/turnkit/image_result.rb +1 -0
  16. data/lib/turnkit/image_tool.rb +2 -2
  17. data/lib/turnkit/media_analysis_result.rb +0 -2
  18. data/lib/turnkit/memory_store.rb +11 -6
  19. data/lib/turnkit/model_request.rb +4 -2
  20. data/lib/turnkit/output_policy.rb +1 -15
  21. data/lib/turnkit/reconciliation.rb +72 -0
  22. data/lib/turnkit/record.rb +1 -1
  23. data/lib/turnkit/run.rb +0 -4
  24. data/lib/turnkit/store.rb +17 -8
  25. data/lib/turnkit/sub_agent_tool.rb +9 -14
  26. data/lib/turnkit/system_prompt.rb +32 -96
  27. data/lib/turnkit/tool.rb +5 -16
  28. data/lib/turnkit/tool_runner.rb +27 -3
  29. data/lib/turnkit/turn.rb +73 -89
  30. data/lib/turnkit/version.rb +1 -1
  31. data/lib/turnkit/view_media_tool.rb +2 -2
  32. data/lib/turnkit.rb +3 -21
  33. metadata +16 -30
  34. data/lib/turnkit/prompt_contribution.rb +0 -13
  35. data/lib/turnkit/rails/railtie.rb +0 -9
  36. data/lib/turnkit/workflow.rb +0 -58
  37. /data/lib/{turnkit/generators → generators}/turnkit/install/templates/conversation.rb +0 -0
  38. /data/lib/{turnkit/generators → generators}/turnkit/install/templates/create_turnkit_tables.rb +0 -0
  39. /data/lib/{turnkit/generators → generators}/turnkit/install/templates/message.rb +0 -0
  40. /data/lib/{turnkit/generators → generators}/turnkit/install/templates/tool_execution.rb +0 -0
  41. /data/lib/{turnkit/generators → generators}/turnkit/install/templates/turn.rb +0 -0
  42. /data/lib/{turnkit/generators → generators}/turnkit/install_generator.rb +0 -0
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
- (@record["options"] || {})["policy_audit"]
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
- claimed_standalone = false
172
- case status
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
- when "running"
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,43 +199,28 @@ 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 = Cost.from_usage(result.usage, model: result.model || model)
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 claimed_standalone
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 claimed_standalone
217
- update!(status: "failed", error: { "class" => error.class.name, "message" => error.message }, completed_at: Clock.now)
218
- emit("turn.failed", error: { "class" => error.class.name, "message" => error.message })
219
- end
212
+ fail_standalone!(error) if claimed
220
213
  raise
221
214
  end
222
215
 
223
216
  def view_media(media, objective:, model:, provider: nil, output_schema: nil, params: {}, metadata: {}, client: nil)
224
- claimed_standalone = false
225
- case status
226
- when "pending"
227
- claimed = store.claim_turn(id, from: "pending", to: "running", started_at: Clock.now, heartbeat_at: Clock.now)
228
- raise Error, "turn is already running" unless claimed
229
-
217
+ claimed = claim_standalone!("view media")
218
+ if claimed
230
219
  @record = claimed
231
220
  @started_at = @record["started_at"]
232
- @budget = Budget.resume(store: store, root_turn_id: root_turn_id, limits: budget_limits)
233
- claimed_standalone = true
234
221
  emit("turn.started", status: status, model: model)
235
- when "running"
236
- # Media tools call this while their parent turn is running.
237
- else
238
- raise Error, "cannot view media for #{status} turn"
222
+ @budget = Budget.resume(store: store, root_turn_id: root_turn_id, limits: agent.budget_limits)
239
223
  end
240
-
241
224
  media_input = MediaInput.wrap(media)
242
225
  media_client = client || agent.effective_client
243
226
  request = {
@@ -253,42 +236,38 @@ module TurnKit
253
236
  media_client.validate!(model: model)
254
237
  emit("media.requested", request.except(:media).merge(media: media_input.to_h))
255
238
  result = call_media_client(media_client, request)
256
- result_cost = Cost.from_usage(result.usage, model: result.model || model)
257
- add_usage!(result.usage, cost: result_cost)
258
- budget.add_cost!(result_cost.total)
239
+ result_cost = apply_result_cost(result, model: model)
259
240
  analysis = result.media_analyses.first
260
241
  raise Error, "media client returned no media analysis" unless analysis
261
242
 
262
243
  persist_media_analysis_message(analysis)
263
244
  output_data = { "type" => "media_analysis", "media_analyses" => [ analysis.to_h ] }
264
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 || {})
265
- complete_with_output(analysis.text, output_data: output_data, audit: check_policy(analysis.text, output_data: output_data)) if claimed_standalone
246
+ complete_with_output(analysis.text, output_data: output_data, audit: check_policy(analysis.text, output_data: output_data)) if claimed
266
247
  analysis
267
248
  rescue StandardError => error
268
- emit("media.failed", error: { "class" => error.class.name, "message" => error.message }, metadata: metadata || {}) if status == "running" || claimed_standalone
269
- if claimed_standalone
270
- update!(status: "failed", error: { "class" => error.class.name, "message" => error.message }, completed_at: Clock.now)
271
- emit("turn.failed", error: { "class" => error.class.name, "message" => error.message })
272
- end
249
+ emit("media.failed", error: { "class" => error.class.name, "message" => error.message }, metadata: metadata || {}) if status == "running" || claimed
250
+ fail_standalone!(error) if claimed
273
251
  raise
274
252
  end
275
253
 
276
254
  private
277
255
  def model_request
278
256
  prompt = SystemPrompt.new(agent: agent, turn: self, conversation: conversation, mode: prompt_mode || agent.effective_prompt_mode(turn: self))
279
- instructions = case agent.system_prompt
257
+ instructions, dynamic_instructions = case agent.system_prompt
280
258
  when nil
281
- prompt.to_s
259
+ [ prompt.stable, prompt.dynamic ]
282
260
  when String
283
- agent.system_prompt
261
+ [ agent.system_prompt, nil ]
284
262
  else
285
- agent.system_prompt.call(prompt).to_s
263
+ [ agent.system_prompt.call(prompt).to_s, nil ]
286
264
  end
287
265
  ModelRequest.new(
288
266
  model: model,
289
267
  messages: llm_messages,
290
268
  tools: agent.effective_tools,
291
269
  instructions: instructions,
270
+ dynamic_instructions: dynamic_instructions,
292
271
  thinking: thinking,
293
272
  output_schema: output_schema,
294
273
  metadata: { turn_id: id, conversation_id: conversation.id },
@@ -296,48 +275,27 @@ module TurnKit
296
275
  )
297
276
  end
298
277
 
278
+ # Clients implement the TurnKit::Client keyword contract. See client.rb.
299
279
  def call_client(request, client: agent.effective_client)
300
- kwargs = {
280
+ client.chat(
301
281
  model: request.model,
302
282
  messages: request.messages,
303
283
  tools: request.tools,
304
284
  instructions: request.instructions,
285
+ dynamic_instructions: request.dynamic_instructions,
305
286
  thinking: request.thinking,
306
287
  output_schema: request.output_schema,
307
288
  metadata: request.metadata,
308
289
  on_event: ->(event) { emit_event(event) }
309
- }
310
- accepted = chat_keyword_names(client)
311
- kwargs = kwargs.slice(*accepted) unless accepted.include?(:keyrest)
312
- client.chat(**kwargs)
313
- end
314
-
315
- def chat_keyword_names(client)
316
- client.method(:chat).parameters.filter_map do |kind, name|
317
- return [ :keyrest ] if kind == :keyrest
318
-
319
- name if %i[key keyreq].include?(kind)
320
- end
290
+ )
321
291
  end
322
292
 
323
293
  def call_image_client(client, request)
324
- kwargs = request.merge(on_event: ->(event) { emit_event(event) })
325
- accepted = client.method(:paint).parameters.filter_map do |kind, name|
326
- return client.paint(**kwargs) if kind == :keyrest
327
-
328
- name if %i[key keyreq].include?(kind)
329
- end
330
- client.paint(**kwargs.slice(*accepted))
294
+ client.paint(**request, on_event: ->(event) { emit_event(event) })
331
295
  end
332
296
 
333
297
  def call_media_client(client, request)
334
- kwargs = request.merge(on_event: ->(event) { emit_event(event) })
335
- accepted = client.method(:view_media).parameters.filter_map do |kind, name|
336
- return client.view_media(**kwargs) if kind == :keyrest
337
-
338
- name if %i[key keyreq].include?(kind)
339
- end
340
- client.view_media(**kwargs.slice(*accepted))
298
+ client.view_media(**request, on_event: ->(event) { emit_event(event) })
341
299
  end
342
300
 
343
301
  def llm_messages
@@ -453,8 +411,7 @@ module TurnKit
453
411
  end
454
412
 
455
413
  def persist_policy_audit(audit)
456
- options = (@record["options"] || {}).merge("policy_audit" => audit.to_h)
457
- update!(options: options)
414
+ update_state!("policy_audit" => audit.to_h)
458
415
  emit("output_policy.completed", clean: audit.clean?, violation_count: audit.violations.length)
459
416
  end
460
417
 
@@ -506,23 +463,50 @@ module TurnKit
506
463
 
507
464
  def count_iteration!
508
465
  budget.count_iteration!
509
- options = (@record["options"] || {}).merge("iterations" => (@record.dig("options", "iterations").to_i + 1))
510
- update!(options: options)
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)))
511
475
  end
512
476
 
513
477
  def heartbeat!
514
478
  update!(heartbeat_at: Clock.now)
515
479
  end
516
480
 
517
- def budget_limits
518
- {
519
- max_iterations: agent.max_iterations || TurnKit.max_iterations,
520
- timeout: agent.timeout || TurnKit.timeout,
521
- max_depth: agent.max_depth || TurnKit.max_depth,
522
- max_tool_executions: agent.max_tool_executions || TurnKit.max_tool_executions,
523
- max_tool_executions_by_name: agent.max_tool_executions_by_name || TurnKit.max_tool_executions_by_name,
524
- max_spend: agent.max_spend || TurnKit.max_spend
525
- }
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
526
510
  end
527
511
 
528
512
  def aggregate_cost(current, cost)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TurnKit
4
- VERSION = "0.4.1"
4
+ VERSION = "0.5.0"
5
5
  end
@@ -11,8 +11,8 @@ module TurnKit
11
11
  end
12
12
  end
13
13
 
14
- def call(turnkit_context:, **arguments)
15
- turnkit_context.turn.view_media(
14
+ def call(context:, **arguments)
15
+ context.turn.view_media(
16
16
  media(**arguments),
17
17
  objective: objective(**arguments),
18
18
  model: self.class.model,
data/lib/turnkit.rb CHANGED
@@ -17,7 +17,6 @@ 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"
@@ -31,10 +30,10 @@ require_relative "turnkit/output_audit"
31
30
  require_relative "turnkit/output_policy"
32
31
  require_relative "turnkit/prompt_data"
33
32
  require_relative "turnkit/prompt_context"
34
- require_relative "turnkit/prompt_contribution"
35
33
  require_relative "turnkit/system_prompt"
36
34
  require_relative "turnkit/store"
37
35
  require_relative "turnkit/memory_store"
36
+ require_relative "turnkit/reconciliation"
38
37
  require_relative "turnkit/compaction"
39
38
  require_relative "turnkit/tool"
40
39
  require_relative "turnkit/image_tool"
@@ -50,9 +49,7 @@ require_relative "turnkit/usage"
50
49
  require_relative "turnkit/run"
51
50
  require_relative "turnkit/adapters/codex"
52
51
  require_relative "turnkit/adapters/ruby_llm"
53
- require_relative "turnkit/stores/active_record_store"
54
-
55
- require_relative "turnkit/rails/railtie" if defined?(Rails)
52
+ require_relative "turnkit/active_record_store"
56
53
 
57
54
  module TurnKit
58
55
  class << self
@@ -65,10 +62,7 @@ module TurnKit
65
62
  attr_accessor :cost_rates, :cost_calculator
66
63
  attr_accessor :prompt_sections, :prompt_behavior, :available_skills
67
64
  attr_accessor :prompt_data_max_chars, :context_contributors
68
- attr_accessor :system_prompt_contributors, :model_prompt_contributors
69
65
  attr_accessor :on_event
70
- attr_accessor :conversation_record_class, :turn_record_class
71
- attr_accessor :message_record_class, :tool_execution_record_class
72
66
  end
73
67
 
74
68
  self.default_model = "claude-sonnet-4-5"
@@ -87,8 +81,6 @@ module TurnKit
87
81
  self.prompt_data_max_chars = 20_000
88
82
  self.available_skills = []
89
83
  self.context_contributors = []
90
- self.system_prompt_contributors = []
91
- self.model_prompt_contributors = {}
92
84
  self.on_event = nil
93
85
  self.output_policy_model = nil
94
86
  self.output_policy_thinking = { effort: :low }
@@ -97,18 +89,8 @@ module TurnKit
97
89
  yield self
98
90
  end
99
91
 
100
- def self.model
101
- default_model
102
- end
103
-
104
- def self.model=(value)
105
- self.default_model = value
106
- end
107
-
108
92
  def self.reconcile_stale!(before: Clock.now - (timeout || 300))
109
- store.find_stale_turns(before: before).each do |turn|
110
- store.update_turn(turn.fetch("id"), "status" => "stale", "completed_at" => Clock.now)
111
- end
93
+ Reconciliation.reconcile!(before: before)
112
94
  end
113
95
 
114
96
  def self.check_output_policy(output, constraints: [], context: {})
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.1
4
+ version: 0.5.0
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-06-19 00:00:00.000000000 Z
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-22 00:00:00.000000000 Z
12
+ dependencies: []
27
13
  description: TurnKit is a Ruby/Rails agent runtime for durable AI conversations, application
28
- runs, reusable workflows, tool calling, skills, sub-agents, context compaction,
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,13 +44,6 @@ 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
@@ -69,16 +57,14 @@ files:
69
57
  - lib/turnkit/output_audit.rb
70
58
  - lib/turnkit/output_policy.rb
71
59
  - lib/turnkit/prompt_context.rb
72
- - lib/turnkit/prompt_contribution.rb
73
60
  - lib/turnkit/prompt_data.rb
74
- - lib/turnkit/rails/railtie.rb
61
+ - lib/turnkit/reconciliation.rb
75
62
  - lib/turnkit/record.rb
76
63
  - lib/turnkit/result.rb
77
64
  - lib/turnkit/run.rb
78
65
  - lib/turnkit/schema_check.rb
79
66
  - lib/turnkit/skill.rb
80
67
  - lib/turnkit/store.rb
81
- - lib/turnkit/stores/active_record_store.rb
82
68
  - lib/turnkit/sub_agent_tool.rb
83
69
  - lib/turnkit/system_prompt.rb
84
70
  - lib/turnkit/tool.rb
@@ -89,7 +75,6 @@ files:
89
75
  - lib/turnkit/usage.rb
90
76
  - lib/turnkit/version.rb
91
77
  - lib/turnkit/view_media_tool.rb
92
- - lib/turnkit/workflow.rb
93
78
  homepage: https://github.com/samuelcouch/turnkit
94
79
  licenses:
95
80
  - MIT
@@ -117,5 +102,6 @@ requirements: []
117
102
  rubygems_version: 3.5.22
118
103
  signing_key:
119
104
  specification_version: 4
120
- summary: Ruby/Rails agent runtime for durable AI conversations, runs, and workflows.
105
+ summary: Ruby/Rails agent runtime for durable AI conversations, runs, and orchestrator
106
+ agents.
121
107
  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
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TurnKit
4
- class Railtie < Rails::Railtie
5
- generators do
6
- require_relative "../generators/turnkit/install_generator"
7
- end
8
- end
9
- end
@@ -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