turnkit 0.4.2 → 0.6.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -0
  3. data/README.md +197 -1
  4. data/UPGRADE.md +57 -0
  5. data/lib/generators/turnkit/install/templates/create_turnkit_tables.rb +30 -0
  6. data/lib/generators/turnkit/install/templates/delivery.rb +7 -0
  7. data/lib/generators/turnkit/install/templates/initializer.rb +5 -0
  8. data/lib/generators/turnkit/install/templates/wait.rb +7 -0
  9. data/lib/generators/turnkit/install_generator.rb +2 -0
  10. data/lib/generators/turnkit/upgrade/templates/add_turnkit_durable_orchestration.rb +36 -0
  11. data/lib/generators/turnkit/upgrade_generator.rb +34 -0
  12. data/lib/turnkit/active_record_store.rb +130 -10
  13. data/lib/turnkit/adapters/ruby_llm.rb +14 -0
  14. data/lib/turnkit/agent.rb +26 -20
  15. data/lib/turnkit/authorization.rb +17 -0
  16. data/lib/turnkit/background.rb +281 -0
  17. data/lib/turnkit/budget.rb +4 -3
  18. data/lib/turnkit/conversation.rb +23 -1
  19. data/lib/turnkit/coordination_tools.rb +61 -0
  20. data/lib/turnkit/error.rb +3 -0
  21. data/lib/turnkit/execution_store.rb +30 -0
  22. data/lib/turnkit/id.rb +1 -0
  23. data/lib/turnkit/image_tool.rb +10 -0
  24. data/lib/turnkit/job.rb +21 -0
  25. data/lib/turnkit/memory_store.rb +110 -14
  26. data/lib/turnkit/reconciliation.rb +75 -0
  27. data/lib/turnkit/record.rb +37 -4
  28. data/lib/turnkit/run.rb +15 -0
  29. data/lib/turnkit/skill.rb +5 -4
  30. data/lib/turnkit/specialists.rb +254 -0
  31. data/lib/turnkit/store.rb +42 -2
  32. data/lib/turnkit/sub_agent_tool.rb +23 -7
  33. data/lib/turnkit/system_prompt.rb +7 -7
  34. data/lib/turnkit/tool.rb +11 -0
  35. data/lib/turnkit/tool_runner.rb +109 -29
  36. data/lib/turnkit/turn.rb +188 -57
  37. data/lib/turnkit/version.rb +1 -1
  38. data/lib/turnkit.rb +32 -3
  39. metadata +16 -5
data/lib/turnkit/turn.rb CHANGED
@@ -13,6 +13,7 @@ module TurnKit
13
13
  @agent = agent
14
14
  @conversation = conversation
15
15
  @store = store
16
+ @base_store = store
16
17
  @record = record.transform_keys(&:to_s)
17
18
  @id = @record.fetch("id")
18
19
  @conversation_id = @record.fetch("conversation_id")
@@ -36,62 +37,89 @@ module TurnKit
36
37
  @on_event = block if block
37
38
  return self unless status == "pending"
38
39
 
39
- claimed = store.claim_turn(id, from: "pending", to: "running", started_at: Clock.now, heartbeat_at: Clock.now)
40
+ @store = @base_store
41
+ claimed = Background.claim(store, @record)
40
42
  return self unless claimed
41
43
 
42
44
  @record = claimed
43
45
  @started_at = @record["started_at"]
44
- emit("turn.started", status: status, model: model)
45
- agent.effective_client.validate!(model: model)
46
- @budget = Budget.resume(store: store, root_turn_id: root_turn_id, limits: agent.budget_limits)
47
- revisions_used = 0
48
- loop do
49
- budget.check!(depth: depth)
50
- count_iteration!
51
- TurnKit::Compaction.maybe_compact!(self)
52
-
53
- request = model_request
54
- emit_model_requested("model.requested", request)
55
- result = call_client(request)
56
- result_cost = Cost.from_usage(result.usage, model: result.model || model)
57
-
58
- add_usage!(result.usage, cost: result_cost)
59
- emit_model_completed("model.completed", result, result_cost, model: model)
60
- budget.add_cost!(result_cost.total)
61
- persist_assistant_message(result)
62
-
63
- if result.tool_calls?
64
- runner = ToolRunner.new(self)
65
- terminal = runner.dispatch(result.tool_calls)
66
- if terminal
67
- candidate = append_terminal_completion(runner, terminal)
68
- else
69
- next
70
- end
71
- else
72
- candidate = result.text
73
- end
74
-
75
- audit = check_policy(candidate, output_data: result.output_data)
76
- if should_revise?(audit, revisions_used)
77
- revisions_used += 1
78
- append_revision_message(audit, attempt: revisions_used, terminal_tool_name: terminal&.tool_name)
79
- emit("output_policy.revision", violation_count: audit.violations.length, attempt: revisions_used)
80
- next
81
- end
82
-
83
- complete_with_output(candidate, output_data: result.output_data, audit: audit)
84
- break
46
+ fence!
47
+ with_heartbeat do
48
+ emit("turn.started", status: status, model: model)
49
+ agent.effective_client.validate!(model: model)
50
+ execute
85
51
  end
86
52
  reload
87
53
  self
54
+ rescue LostClaim
55
+ reload
88
56
  rescue StandardError => error
57
+ # Background infrastructure failures must reach the job backend. The
58
+ # reconciler recovers the persisted phase rather than guessing here.
59
+ raise if background? && !error.is_a?(TurnKit::Error)
60
+ raise unless claimed
61
+
89
62
  update!(status: "failed", error: { "class" => error.class.name, "message" => error.message }, completed_at: Clock.now)
90
63
  emit("turn.failed", error: { "class" => error.class.name, "message" => error.message })
91
64
  reload
92
65
  self
93
66
  end
94
67
 
68
+ def background? = !!@record["submitted_at"]
69
+
70
+ def perform_later(callback: nil)
71
+ Background.submit(self, callback: callback.respond_to?(:id) ? callback.id : callback)
72
+ end
73
+
74
+ def wait_for(*targets)
75
+ Background.wait(self, targets.flatten, transition: true)
76
+ reload
77
+ end
78
+
79
+ def suspend!
80
+ update!(status: "waiting", claim_token: nil)
81
+ end
82
+
83
+ # Revokes the local claim. An already-sent remote request cannot be
84
+ # recalled, but ExecutionStore fences every later write from that worker.
85
+ def cancel!(descendants: :retain, principal: nil)
86
+ raise ArgumentError, "descendants must be :retain or :cascade" unless %i[retain cascade].include?(descendants)
87
+ Authorization.authorize!(:cancel, principal: principal, turn: self, descendants: descendants)
88
+ @base_store.atomic(Background.root_conversation(@base_store, @record)) do
89
+ all = @base_store.list_turns(root_turn_id: root_turn_id)
90
+ rows = [@base_store.load_turn(id)]
91
+ if descendants == :cascade
92
+ descendant_ids = { id => true }
93
+ loop do
94
+ added = all.select { |row| row["parent_turn_id"] && descendant_ids[row["parent_turn_id"]] && !descendant_ids[row["id"]] }
95
+ break if added.empty?
96
+ added.each { |row| descendant_ids[row["id"]] = true }
97
+ end
98
+ rows.concat(all.select { |row| row["id"] != id && descendant_ids[row["id"]] })
99
+ end
100
+ rows.each do |row|
101
+ next if Background::TERMINAL.include?(row["status"])
102
+ executions = Reconciliation.interrupt_tool_executions(row, store: @base_store)
103
+ Reconciliation.repair_transcript(row, executions, store: @base_store)
104
+ attrs = { status: "cancelled", claim_token: nil, completed_at: Clock.now,
105
+ error: { "class" => "TurnKit::Cancelled", "message" => "cancelled by application" } }
106
+ terminal_row = @base_store.update_turn(row.fetch("id"), attrs)
107
+ Background.callback_after_terminal(@base_store, terminal_row) if row["submitted_at"]
108
+ end
109
+ rows.map { |row| row.fetch("conversation_id") }.uniq.each { |conversation_id| Background.wake(conversation_id, store: @base_store) } if background?
110
+ end
111
+ Background.enqueue if background?
112
+ reload
113
+ end
114
+
115
+ def execution_budget(excluding: nil)
116
+ root = store.load_turn(root_turn_id)
117
+ limits = root.dig("options", "budget_limits") || agent.budget_limits
118
+ turns = store.list_turns(root_turn_id: root_turn_id)
119
+ executions = turns.flat_map { |row| store.list_tool_executions(turn_id: row.fetch("id")) }.reject { |row| row["id"] == excluding }
120
+ Budget.new(**limits.transform_keys(&:to_sym), root_started_at: root["submitted_at"] || root["started_at"] || Clock.now).seed!(turns: turns, tool_executions: executions)
121
+ end
122
+
95
123
  def preview
96
124
  model_request
97
125
  end
@@ -112,6 +140,8 @@ module TurnKit
112
140
  @record["output_data"]
113
141
  end
114
142
 
143
+ def context = JSON.parse(JSON.generate(@record.dig("options", "context") || {}))
144
+
115
145
  def policy_audit
116
146
  options = @record["options"] || {}
117
147
  options.dig("state", "policy_audit") || options["policy_audit"]
@@ -180,6 +210,7 @@ module TurnKit
180
210
  if claimed
181
211
  @record = claimed
182
212
  @started_at = @record["started_at"]
213
+ fence!
183
214
  emit("turn.started", status: status, model: model)
184
215
  @budget = Budget.resume(store: store, root_turn_id: root_turn_id, limits: agent.budget_limits)
185
216
  end
@@ -218,6 +249,7 @@ module TurnKit
218
249
  if claimed
219
250
  @record = claimed
220
251
  @started_at = @record["started_at"]
252
+ fence!
221
253
  emit("turn.started", status: status, model: model)
222
254
  @budget = Budget.resume(store: store, root_turn_id: root_turn_id, limits: agent.budget_limits)
223
255
  end
@@ -252,6 +284,87 @@ module TurnKit
252
284
  end
253
285
 
254
286
  private
287
+ def fence!
288
+ @store = ExecutionStore.new(store, turn_id: id, token: @record.fetch("claim_token"), conversation_id: Background.root_conversation(store, @record))
289
+ @conversation = Conversation.new(agent: agent, record: store.load_conversation(conversation.id), store: store,
290
+ model: conversation.model, subject: conversation.subject, metadata: conversation.metadata)
291
+ end
292
+
293
+ def execute
294
+ loop do
295
+ @budget = execution_budget
296
+ budget.check!(depth: depth)
297
+ state = @record.dig("options", "state") || {}
298
+ case state["phase"] || "model"
299
+ when "model"
300
+ count_iteration!
301
+ TurnKit::Compaction.maybe_compact!(self)
302
+ request = model_request
303
+ emit_model_requested("model.requested", request)
304
+ result = call_client(request)
305
+ cost = Cost.from_usage(result.usage, model: result.model || model)
306
+ store.atomic do
307
+ add_usage!(result.usage, cost: cost)
308
+ persist_assistant_message(result)
309
+ update_state!("phase" => result.tool_calls? ? "tools" : "output", "parts" => result.parts,
310
+ "candidate" => result.text, "output_data" => result.output_data, "terminal_tool_name" => nil)
311
+ end
312
+ emit_model_completed("model.completed", result, cost, model: model)
313
+ budget.add_cost!(cost.total)
314
+ when "tools"
315
+ runner = ToolRunner.new(self)
316
+ terminal = runner.dispatch(Result.new(parts: state.fetch("parts")).tool_calls)
317
+ if terminal == :waiting
318
+ suspend!
319
+ break
320
+ end
321
+ store.atomic do
322
+ if terminal
323
+ candidate = append_terminal_completion(runner, terminal)
324
+ update_state!("phase" => "output", "candidate" => candidate, "terminal_tool_name" => terminal.tool_name)
325
+ else
326
+ update_state!("phase" => "model", "parts" => nil)
327
+ end
328
+ end
329
+ when "output"
330
+ candidate = state.fetch("candidate")
331
+ audit = check_policy(candidate, output_data: state["output_data"])
332
+ revisions_used = state["revisions_used"].to_i
333
+ if should_revise?(audit, revisions_used)
334
+ store.atomic do
335
+ append_revision_message(audit, attempt: revisions_used + 1, terminal_tool_name: state["terminal_tool_name"])
336
+ update_state!("phase" => "model", "parts" => nil, "revisions_used" => revisions_used + 1)
337
+ end
338
+ emit("output_policy.revision", violation_count: audit.violations.length, attempt: revisions_used + 1)
339
+ else
340
+ complete_with_output(candidate, output_data: state["output_data"], audit: audit)
341
+ break
342
+ end
343
+ end
344
+ end
345
+ end
346
+
347
+ def with_heartbeat
348
+ mutex, wake = Mutex.new, ConditionVariable.new
349
+ stopped = false
350
+ heartbeat = Thread.new do
351
+ loop do
352
+ break if mutex.synchronize {
353
+ wake.wait(mutex, (TurnKit.timeout || 300) / 3.0) unless stopped
354
+ stopped
355
+ }
356
+ heartbeat!
357
+ end
358
+ rescue LostClaim
359
+ # The executing thread observes the same revocation on its next write.
360
+ end
361
+ yield
362
+ ensure
363
+ # Never interrupt a database write/connection initialization mid-flight.
364
+ mutex.synchronize { stopped = true; wake.signal }
365
+ heartbeat&.value
366
+ end
367
+
255
368
  def model_request
256
369
  prompt = SystemPrompt.new(agent: agent, turn: self, conversation: conversation, mode: prompt_mode || agent.effective_prompt_mode(turn: self))
257
370
  instructions, dynamic_instructions = case agent.system_prompt
@@ -265,7 +378,7 @@ module TurnKit
265
378
  ModelRequest.new(
266
379
  model: model,
267
380
  messages: llm_messages,
268
- tools: agent.effective_tools,
381
+ tools: agent.effective_tools(turn: self),
269
382
  instructions: instructions,
270
383
  dynamic_instructions: dynamic_instructions,
271
384
  thinking: thinking,
@@ -291,11 +404,11 @@ module TurnKit
291
404
  end
292
405
 
293
406
  def call_image_client(client, request)
294
- client.paint(**request, on_event: ->(event) { emit_event(event) })
407
+ with_heartbeat { client.paint(**request, on_event: ->(event) { emit_event(event) }) }
295
408
  end
296
409
 
297
410
  def call_media_client(client, request)
298
- client.view_media(**request, on_event: ->(event) { emit_event(event) })
411
+ with_heartbeat { client.view_media(**request, on_event: ->(event) { emit_event(event) }) }
299
412
  end
300
413
 
301
414
  def llm_messages
@@ -392,8 +505,11 @@ module TurnKit
392
505
  else
393
506
  attrs[:status] = "completed"
394
507
  end
395
- update!(attrs)
396
- persist_policy_audit(audit) if audit
508
+ store.atomic(Background.root_conversation(store, @record)) do
509
+ update_state!("policy_audit" => audit.to_h) if audit
510
+ update!(attrs)
511
+ end
512
+ emit("output_policy.completed", clean: audit.clean?, violation_count: audit.violations.length) if audit
397
513
 
398
514
  if failed?
399
515
  emit("turn.failed", error: @record["error"])
@@ -410,11 +526,6 @@ module TurnKit
410
526
  TurnKit.check_output_policy(output, constraints: constraints, context: { turn: self, output_text: text, output_data: output_data })
411
527
  end
412
528
 
413
- def persist_policy_audit(audit)
414
- update_state!("policy_audit" => audit.to_h)
415
- emit("output_policy.completed", clean: audit.clean?, violation_count: audit.violations.length)
416
- end
417
-
418
529
  def should_revise?(audit, revisions_used)
419
530
  audit && !audit.clean? && revisions_used < agent.output_retries
420
531
  end
@@ -462,8 +573,11 @@ module TurnKit
462
573
  end
463
574
 
464
575
  def count_iteration!
465
- budget.count_iteration!
466
- update_state!("iterations" => Turn.iterations_for(@record) + 1)
576
+ store.atomic do
577
+ @budget = execution_budget
578
+ budget.count_iteration!
579
+ update_state!("iterations" => Turn.iterations_for(@record) + 1)
580
+ end
467
581
  end
468
582
 
469
583
  # Runtime state lives under options["state"]; the rest of options is
@@ -475,7 +589,7 @@ module TurnKit
475
589
  end
476
590
 
477
591
  def heartbeat!
478
- update!(heartbeat_at: Clock.now)
592
+ store.update_turn(id, heartbeat_at: Clock.now)
479
593
  end
480
594
 
481
595
  # Claims a pending turn for a standalone media call. Returns the claimed
@@ -486,7 +600,7 @@ module TurnKit
486
600
  def claim_standalone!(action)
487
601
  case status
488
602
  when "pending"
489
- claimed = store.claim_turn(id, from: "pending", to: "running", started_at: Clock.now, heartbeat_at: Clock.now)
603
+ claimed = Background.claim(store, @record)
490
604
  raise Error, "turn is already running" unless claimed
491
605
 
492
606
  claimed
@@ -516,7 +630,21 @@ module TurnKit
516
630
  end
517
631
 
518
632
  def update!(attributes)
519
- @record = store.update_turn(id, attributes)
633
+ terminal = Background::TERMINAL.include?(attributes[:status])
634
+ store.atomic(Background.root_conversation(store, @record)) do
635
+ if terminal
636
+ if attributes[:status] == "failed" && background?
637
+ executions = Reconciliation.interrupt_tool_executions(@record, store: store)
638
+ Reconciliation.repair_transcript(@record, executions, store: store)
639
+ end
640
+ attributes = attributes.merge(claim_token: nil)
641
+ end
642
+ @record = store.update_turn(id, attributes)
643
+ # The terminal write clears this execution's claim token, so durable
644
+ # callback work must use the unfenced store after that write.
645
+ Background.callback_after_terminal(@base_store, @record) if terminal && background?
646
+ Background.wake(conversation.id, store: @base_store) if terminal && background?
647
+ end
520
648
  @started_at = @record["started_at"]
521
649
  @model = @record["model"] || agent.effective_model
522
650
  @record
@@ -535,5 +663,8 @@ module TurnKit
535
663
  @turn = turn
536
664
  @execution = execution
537
665
  end
666
+
667
+ def idempotency_key = "turnkit:tool:#{execution.id}"
668
+ def principal = turn.store.load_turn(turn.id).dig("options", "principal")
538
669
  end
539
670
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TurnKit
4
- VERSION = "0.4.2"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/turnkit.rb CHANGED
@@ -16,6 +16,7 @@ require_relative "turnkit/budget"
16
16
  require_relative "turnkit/event"
17
17
  require_relative "turnkit/model_request"
18
18
  require_relative "turnkit/schema_check"
19
+ require_relative "turnkit/authorization"
19
20
  require_relative "turnkit/agent"
20
21
  require_relative "turnkit/client"
21
22
  require_relative "turnkit/conversation"
@@ -33,6 +34,7 @@ require_relative "turnkit/prompt_context"
33
34
  require_relative "turnkit/system_prompt"
34
35
  require_relative "turnkit/store"
35
36
  require_relative "turnkit/memory_store"
37
+ require_relative "turnkit/reconciliation"
36
38
  require_relative "turnkit/compaction"
37
39
  require_relative "turnkit/tool"
38
40
  require_relative "turnkit/image_tool"
@@ -49,6 +51,10 @@ require_relative "turnkit/run"
49
51
  require_relative "turnkit/adapters/codex"
50
52
  require_relative "turnkit/adapters/ruby_llm"
51
53
  require_relative "turnkit/active_record_store"
54
+ require_relative "turnkit/execution_store"
55
+ require_relative "turnkit/background"
56
+ require_relative "turnkit/coordination_tools"
57
+ require_relative "turnkit/specialists"
52
58
 
53
59
  module TurnKit
54
60
  class << self
@@ -62,6 +68,29 @@ module TurnKit
62
68
  attr_accessor :prompt_sections, :prompt_behavior, :available_skills
63
69
  attr_accessor :prompt_data_max_chars, :context_contributors
64
70
  attr_accessor :on_event
71
+ attr_accessor :job_dispatcher
72
+ attr_accessor :authorization_policy, :maintenance_batch_size
73
+ attr_reader :agents
74
+ end
75
+
76
+ @agents = {}
77
+
78
+ def self.register(agent)
79
+ @agents[agent.name] = agent
80
+ agent.sub_agents.each { |child| register(child) }
81
+ agent
82
+ end
83
+
84
+ def self.resolve_agent(name)
85
+ @agents.fetch(name.to_s) { raise ConfigError, "register agent #{name.inspect} in every worker at boot" }
86
+ end
87
+
88
+ def self.load_turn(id)
89
+ Background.load_turn(id)
90
+ end
91
+
92
+ def self.load_conversation(id)
93
+ Background.load_conversation(id)
65
94
  end
66
95
 
67
96
  self.default_model = "claude-sonnet-4-5"
@@ -83,15 +112,15 @@ module TurnKit
83
112
  self.on_event = nil
84
113
  self.output_policy_model = nil
85
114
  self.output_policy_thinking = { effort: :low }
115
+ self.authorization_policy = nil
116
+ self.maintenance_batch_size = 100
86
117
 
87
118
  def self.configure
88
119
  yield self
89
120
  end
90
121
 
91
122
  def self.reconcile_stale!(before: Clock.now - (timeout || 300))
92
- store.find_stale_turns(before: before).each do |turn|
93
- store.update_turn(turn.fetch("id"), "status" => "stale", "completed_at" => Clock.now)
94
- end
123
+ Reconciliation.reconcile!(before: before)
95
124
  end
96
125
 
97
126
  def self.check_output_policy(output, constraints: [], context: {})
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turnkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Couch
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-02 00:00:00.000000000 Z
11
+ date: 2026-09-06 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: TurnKit is a Ruby/Rails agent runtime for durable AI conversations, application
14
14
  runs, orchestrator agents, tool calling, skills, sub-agents, context compaction,
@@ -26,27 +26,36 @@ files:
26
26
  - UPGRADE_TO_0_4_2.md
27
27
  - lib/generators/turnkit/install/templates/conversation.rb
28
28
  - lib/generators/turnkit/install/templates/create_turnkit_tables.rb
29
+ - lib/generators/turnkit/install/templates/delivery.rb
29
30
  - lib/generators/turnkit/install/templates/initializer.rb
30
31
  - lib/generators/turnkit/install/templates/message.rb
31
32
  - lib/generators/turnkit/install/templates/tool_execution.rb
32
33
  - lib/generators/turnkit/install/templates/turn.rb
34
+ - lib/generators/turnkit/install/templates/wait.rb
33
35
  - lib/generators/turnkit/install_generator.rb
36
+ - lib/generators/turnkit/upgrade/templates/add_turnkit_durable_orchestration.rb
37
+ - lib/generators/turnkit/upgrade_generator.rb
34
38
  - lib/turnkit.rb
35
39
  - lib/turnkit/active_record_store.rb
36
40
  - lib/turnkit/adapters/codex.rb
37
41
  - lib/turnkit/adapters/ruby_llm.rb
38
42
  - lib/turnkit/agent.rb
43
+ - lib/turnkit/authorization.rb
44
+ - lib/turnkit/background.rb
39
45
  - lib/turnkit/budget.rb
40
46
  - lib/turnkit/client.rb
41
47
  - lib/turnkit/clock.rb
42
48
  - lib/turnkit/compaction.rb
43
49
  - lib/turnkit/conversation.rb
50
+ - lib/turnkit/coordination_tools.rb
44
51
  - lib/turnkit/cost.rb
45
52
  - lib/turnkit/error.rb
46
53
  - lib/turnkit/event.rb
54
+ - lib/turnkit/execution_store.rb
47
55
  - lib/turnkit/id.rb
48
56
  - lib/turnkit/image_result.rb
49
57
  - lib/turnkit/image_tool.rb
58
+ - lib/turnkit/job.rb
50
59
  - lib/turnkit/load_skill_tool.rb
51
60
  - lib/turnkit/media_analysis_result.rb
52
61
  - lib/turnkit/media_input.rb
@@ -58,11 +67,13 @@ files:
58
67
  - lib/turnkit/output_policy.rb
59
68
  - lib/turnkit/prompt_context.rb
60
69
  - lib/turnkit/prompt_data.rb
70
+ - lib/turnkit/reconciliation.rb
61
71
  - lib/turnkit/record.rb
62
72
  - lib/turnkit/result.rb
63
73
  - lib/turnkit/run.rb
64
74
  - lib/turnkit/schema_check.rb
65
75
  - lib/turnkit/skill.rb
76
+ - lib/turnkit/specialists.rb
66
77
  - lib/turnkit/store.rb
67
78
  - lib/turnkit/sub_agent_tool.rb
68
79
  - lib/turnkit/system_prompt.rb
@@ -83,7 +94,7 @@ metadata:
83
94
  changelog_uri: https://github.com/samuelcouch/turnkit/blob/master/CHANGELOG.md
84
95
  allowed_push_host: https://rubygems.org
85
96
  rubygems_mfa_required: 'true'
86
- post_install_message:
97
+ post_install_message:
87
98
  rdoc_options: []
88
99
  require_paths:
89
100
  - lib
@@ -99,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
99
110
  version: '0'
100
111
  requirements: []
101
112
  rubygems_version: 3.5.22
102
- signing_key:
113
+ signing_key:
103
114
  specification_version: 4
104
115
  summary: Ruby/Rails agent runtime for durable AI conversations, runs, and orchestrator
105
116
  agents.