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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -0
- data/README.md +197 -1
- data/UPGRADE.md +57 -0
- data/lib/generators/turnkit/install/templates/create_turnkit_tables.rb +30 -0
- data/lib/generators/turnkit/install/templates/delivery.rb +7 -0
- data/lib/generators/turnkit/install/templates/initializer.rb +5 -0
- data/lib/generators/turnkit/install/templates/wait.rb +7 -0
- data/lib/generators/turnkit/install_generator.rb +2 -0
- data/lib/generators/turnkit/upgrade/templates/add_turnkit_durable_orchestration.rb +36 -0
- data/lib/generators/turnkit/upgrade_generator.rb +34 -0
- data/lib/turnkit/active_record_store.rb +130 -10
- data/lib/turnkit/adapters/ruby_llm.rb +14 -0
- data/lib/turnkit/agent.rb +26 -20
- data/lib/turnkit/authorization.rb +17 -0
- data/lib/turnkit/background.rb +281 -0
- data/lib/turnkit/budget.rb +4 -3
- data/lib/turnkit/conversation.rb +23 -1
- data/lib/turnkit/coordination_tools.rb +61 -0
- data/lib/turnkit/error.rb +3 -0
- data/lib/turnkit/execution_store.rb +30 -0
- data/lib/turnkit/id.rb +1 -0
- data/lib/turnkit/image_tool.rb +10 -0
- data/lib/turnkit/job.rb +21 -0
- data/lib/turnkit/memory_store.rb +110 -14
- data/lib/turnkit/reconciliation.rb +75 -0
- data/lib/turnkit/record.rb +37 -4
- data/lib/turnkit/run.rb +15 -0
- data/lib/turnkit/skill.rb +5 -4
- data/lib/turnkit/specialists.rb +254 -0
- data/lib/turnkit/store.rb +42 -2
- data/lib/turnkit/sub_agent_tool.rb +23 -7
- data/lib/turnkit/system_prompt.rb +7 -7
- data/lib/turnkit/tool.rb +11 -0
- data/lib/turnkit/tool_runner.rb +109 -29
- data/lib/turnkit/turn.rb +188 -57
- data/lib/turnkit/version.rb +1 -1
- data/lib/turnkit.rb +32 -3
- 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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
396
|
-
|
|
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
|
-
|
|
466
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
data/lib/turnkit/version.rb
CHANGED
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
|
-
|
|
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
|
+
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-
|
|
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.
|