turnkit 0.2.10 → 0.3.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 +10 -0
- data/README.md +34 -9
- data/UPGRADE.md +37 -299
- data/lib/turnkit/adapters/ruby_llm.rb +29 -0
- data/lib/turnkit/agent.rb +22 -33
- data/lib/turnkit/budget.rb +24 -5
- data/lib/turnkit/compaction.rb +1 -0
- data/lib/turnkit/error.rb +1 -0
- data/lib/turnkit/generators/turnkit/install/templates/create_turnkit_tables.rb +0 -1
- data/lib/turnkit/load_skill_tool.rb +29 -0
- data/lib/turnkit/memory_store.rb +11 -0
- data/lib/turnkit/message.rb +14 -7
- data/lib/turnkit/message_projection.rb +17 -2
- data/lib/turnkit/output_policy.rb +6 -0
- data/lib/turnkit/result.rb +29 -4
- data/lib/turnkit/run.rb +4 -5
- data/lib/turnkit/schema_check.rb +68 -0
- data/lib/turnkit/skill.rb +16 -2
- data/lib/turnkit/store.rb +6 -0
- data/lib/turnkit/stores/active_record_store.rb +10 -2
- data/lib/turnkit/sub_agent_tool.rb +2 -1
- data/lib/turnkit/system_prompt.rb +1 -1
- data/lib/turnkit/tool.rb +2 -21
- data/lib/turnkit/tool_call.rb +3 -3
- data/lib/turnkit/tool_runner.rb +38 -16
- data/lib/turnkit/turn.rb +90 -23
- data/lib/turnkit/version.rb +1 -1
- data/lib/turnkit/workflow.rb +20 -94
- data/lib/turnkit.rb +5 -10
- metadata +3 -1
data/lib/turnkit/turn.rb
CHANGED
|
@@ -36,12 +36,18 @@ module TurnKit
|
|
|
36
36
|
@on_event = block if block
|
|
37
37
|
return self unless status == "pending"
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
claimed = store.claim_turn(id, from: "pending", to: "running", started_at: Clock.now, heartbeat_at: Clock.now)
|
|
40
|
+
return self unless claimed
|
|
41
|
+
|
|
42
|
+
@record = claimed
|
|
43
|
+
@started_at = @record["started_at"]
|
|
40
44
|
emit("turn.started", status: status, model: model)
|
|
41
45
|
agent.effective_client.validate!(model: model)
|
|
46
|
+
@budget = Budget.resume(store: store, root_turn_id: root_turn_id, limits: budget_limits)
|
|
47
|
+
revisions_used = 0
|
|
42
48
|
loop do
|
|
43
49
|
budget.check!(depth: depth)
|
|
44
|
-
|
|
50
|
+
count_iteration!
|
|
45
51
|
TurnKit::Compaction.maybe_compact!(self)
|
|
46
52
|
|
|
47
53
|
request = model_request
|
|
@@ -58,13 +64,24 @@ module TurnKit
|
|
|
58
64
|
runner = ToolRunner.new(self)
|
|
59
65
|
terminal = runner.dispatch(result.tool_calls)
|
|
60
66
|
if terminal
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
candidate = append_terminal_completion(runner, terminal)
|
|
68
|
+
else
|
|
69
|
+
next
|
|
63
70
|
end
|
|
64
71
|
else
|
|
65
|
-
|
|
66
|
-
break
|
|
72
|
+
candidate = result.text
|
|
67
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
|
|
68
85
|
end
|
|
69
86
|
reload
|
|
70
87
|
self
|
|
@@ -95,8 +112,8 @@ module TurnKit
|
|
|
95
112
|
@record["output_data"]
|
|
96
113
|
end
|
|
97
114
|
|
|
98
|
-
def
|
|
99
|
-
(@record["options"] || {})["
|
|
115
|
+
def policy_audit
|
|
116
|
+
(@record["options"] || {})["policy_audit"]
|
|
100
117
|
end
|
|
101
118
|
|
|
102
119
|
def usage
|
|
@@ -248,9 +265,9 @@ module TurnKit
|
|
|
248
265
|
message = conversation.append_message(
|
|
249
266
|
role: "assistant",
|
|
250
267
|
kind: "tool_call",
|
|
251
|
-
|
|
268
|
+
content: result.parts,
|
|
252
269
|
turn_id: id,
|
|
253
|
-
metadata: {
|
|
270
|
+
metadata: {}
|
|
254
271
|
)
|
|
255
272
|
emit("message.created", message_id: message.id, role: message.role, kind: message.kind)
|
|
256
273
|
result.tool_calls.each { |call| emit("tool_call.created", id: call.id, name: call.name) }
|
|
@@ -260,24 +277,23 @@ module TurnKit
|
|
|
260
277
|
end
|
|
261
278
|
end
|
|
262
279
|
|
|
263
|
-
def
|
|
280
|
+
def append_terminal_completion(runner, execution)
|
|
264
281
|
message = runner.completion_message(execution)
|
|
265
282
|
assistant = conversation.append_message(role: "assistant", kind: "text", text: message, turn_id: id)
|
|
266
283
|
emit("message.created", message_id: assistant.id, role: assistant.role, kind: assistant.kind)
|
|
267
|
-
|
|
284
|
+
message
|
|
268
285
|
end
|
|
269
286
|
|
|
270
|
-
def complete_with_output(text, output_data: nil)
|
|
271
|
-
audit = audit_output(text, output_data: output_data)
|
|
287
|
+
def complete_with_output(text, output_data: nil, audit: nil)
|
|
272
288
|
attrs = { output_text: text, output_data: output_data, completed_at: Clock.now }
|
|
273
|
-
if audit && !audit.clean? && agent.
|
|
289
|
+
if audit && !audit.clean? && agent.output_policy_mode == :fail
|
|
274
290
|
attrs[:status] = "failed"
|
|
275
|
-
attrs[:error] = { "class" => "TurnKit::OutputAudit", "message" => audit.messages.join("; "), "
|
|
291
|
+
attrs[:error] = { "class" => "TurnKit::OutputAudit", "message" => audit.messages.join("; "), "policy_audit" => audit.to_h }
|
|
276
292
|
else
|
|
277
293
|
attrs[:status] = "completed"
|
|
278
294
|
end
|
|
279
295
|
update!(attrs)
|
|
280
|
-
|
|
296
|
+
persist_policy_audit(audit) if audit
|
|
281
297
|
|
|
282
298
|
if failed?
|
|
283
299
|
emit("turn.failed", error: @record["error"])
|
|
@@ -286,18 +302,48 @@ module TurnKit
|
|
|
286
302
|
end
|
|
287
303
|
end
|
|
288
304
|
|
|
289
|
-
def
|
|
290
|
-
constraints = agent.
|
|
305
|
+
def check_policy(text, output_data: nil)
|
|
306
|
+
constraints = agent.effective_output_policy
|
|
291
307
|
return nil if constraints.empty?
|
|
292
308
|
|
|
293
309
|
output = output_data.nil? ? text : output_data
|
|
294
|
-
TurnKit.
|
|
310
|
+
TurnKit.check_output_policy(output, constraints: constraints, context: { turn: self, output_text: text, output_data: output_data })
|
|
295
311
|
end
|
|
296
312
|
|
|
297
|
-
def
|
|
298
|
-
options = (@record["options"] || {}).merge("
|
|
313
|
+
def persist_policy_audit(audit)
|
|
314
|
+
options = (@record["options"] || {}).merge("policy_audit" => audit.to_h)
|
|
299
315
|
update!(options: options)
|
|
300
|
-
emit("
|
|
316
|
+
emit("output_policy.completed", clean: audit.clean?, violation_count: audit.violations.length)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def should_revise?(audit, revisions_used)
|
|
320
|
+
audit && !audit.clean? && revisions_used < agent.output_retries
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def append_revision_message(audit, attempt:, terminal_tool_name: nil)
|
|
324
|
+
text = <<~TEXT.strip
|
|
325
|
+
The previous output failed policy checks.
|
|
326
|
+
|
|
327
|
+
Revise the previous output. Do not introduce new claims.
|
|
328
|
+
Do not deviate from the skill or policy below.
|
|
329
|
+
|
|
330
|
+
#{revision_policy_blocks}
|
|
331
|
+
|
|
332
|
+
Violations:
|
|
333
|
+
#{audit.violations.each_with_index.map { |violation, index| "#{index + 1}. #{violation.rule}: #{violation.message}" }.join("\n")}
|
|
334
|
+
#{terminal_tool_name ? "\nResubmit via #{terminal_tool_name}." : ""}
|
|
335
|
+
TEXT
|
|
336
|
+
message = conversation.append_message(role: "user", kind: "text", text: text, turn_id: id, metadata: { "source" => "output_policy", "attempt" => attempt })
|
|
337
|
+
emit("message.created", message_id: message.id, role: message.role, kind: message.kind)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def revision_policy_blocks
|
|
341
|
+
agent.effective_output_policy.filter_map do |policy|
|
|
342
|
+
next unless policy.respond_to?(:content)
|
|
343
|
+
|
|
344
|
+
key = policy.respond_to?(:name) ? policy.name : "output_policy"
|
|
345
|
+
"<skill key=\"#{key}\">\n#{policy.content}\n</skill>"
|
|
346
|
+
end.join("\n\n")
|
|
301
347
|
end
|
|
302
348
|
|
|
303
349
|
def add_usage!(usage, cost: nil)
|
|
@@ -316,6 +362,27 @@ module TurnKit
|
|
|
316
362
|
update!(attributes)
|
|
317
363
|
end
|
|
318
364
|
|
|
365
|
+
def count_iteration!
|
|
366
|
+
budget.count_iteration!
|
|
367
|
+
options = (@record["options"] || {}).merge("iterations" => (@record.dig("options", "iterations").to_i + 1))
|
|
368
|
+
update!(options: options)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def heartbeat!
|
|
372
|
+
update!(heartbeat_at: Clock.now)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def budget_limits
|
|
376
|
+
{
|
|
377
|
+
max_iterations: agent.max_iterations || TurnKit.max_iterations,
|
|
378
|
+
timeout: agent.timeout || TurnKit.timeout,
|
|
379
|
+
max_depth: agent.max_depth || TurnKit.max_depth,
|
|
380
|
+
max_tool_executions: agent.max_tool_executions || TurnKit.max_tool_executions,
|
|
381
|
+
max_tool_executions_by_name: agent.max_tool_executions_by_name || TurnKit.max_tool_executions_by_name,
|
|
382
|
+
max_spend: agent.max_spend || TurnKit.max_spend
|
|
383
|
+
}
|
|
384
|
+
end
|
|
385
|
+
|
|
319
386
|
def aggregate_cost(current, cost)
|
|
320
387
|
return cost unless current
|
|
321
388
|
|
data/lib/turnkit/version.rb
CHANGED
data/lib/turnkit/workflow.rb
CHANGED
|
@@ -4,13 +4,7 @@ require_relative "agent"
|
|
|
4
4
|
|
|
5
5
|
module TurnKit
|
|
6
6
|
class Workflow
|
|
7
|
-
|
|
8
|
-
attr_reader :model, :client, :store, :prompt_mode, :thinking, :compaction, :output_schema
|
|
9
|
-
attr_reader :on_event
|
|
10
|
-
attr_reader :max_iterations, :timeout, :cost_limit, :max_depth, :max_tool_executions, :max_tool_executions_by_name
|
|
11
|
-
attr_reader :output_audit, :output_audit_mode, :output_policy, :output_policy_mode, :output_policy_model, :output_policy_thinking
|
|
12
|
-
|
|
13
|
-
DEFAULT_INSTRUCTIONS = <<~TEXT.strip
|
|
7
|
+
ORCHESTRATOR_PREAMBLE = <<~TEXT.strip
|
|
14
8
|
You are an autonomous task orchestrator. Navigate from the application
|
|
15
9
|
request to a final output without asking the user follow-up questions.
|
|
16
10
|
|
|
@@ -29,104 +23,36 @@ module TurnKit
|
|
|
29
23
|
limits.
|
|
30
24
|
TEXT
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
output_schema: nil, max_iterations: nil, timeout: nil, max_spend: nil,
|
|
36
|
-
cost_limit: nil, max_depth: nil, max_tool_executions: nil, max_tool_executions_by_name: nil,
|
|
37
|
-
output_audit: nil, output_audit_mode: nil, output_policy: nil, output_policy_mode: nil, output_policy_model: nil, output_policy_thinking: nil)
|
|
26
|
+
DEFAULT_INSTRUCTIONS = ORCHESTRATOR_PREAMBLE
|
|
27
|
+
|
|
28
|
+
attr_reader :name, :options
|
|
38
29
|
|
|
30
|
+
def initialize(name: "workflow", instructions: nil, preamble: true, **options)
|
|
39
31
|
@name = name.to_s
|
|
40
|
-
@description = description.to_s
|
|
41
|
-
@instructions = instructions || DEFAULT_INSTRUCTIONS
|
|
42
|
-
@tools = Array(tools)
|
|
43
|
-
@skills = Array(skills)
|
|
44
|
-
@available_skills = Array(available_skills)
|
|
45
|
-
@model = model
|
|
46
|
-
@client = client
|
|
47
|
-
@store = store
|
|
48
|
-
@prompt_mode = prompt_mode
|
|
49
|
-
@thinking = thinking
|
|
50
|
-
@compaction = compaction
|
|
51
|
-
@on_event = on_event
|
|
52
|
-
@output_schema = output_schema
|
|
53
|
-
@max_iterations = max_iterations
|
|
54
|
-
@timeout = timeout
|
|
55
|
-
@cost_limit = cost_limit || max_spend
|
|
56
|
-
@max_depth = max_depth
|
|
57
|
-
@max_tool_executions = max_tool_executions
|
|
58
|
-
@max_tool_executions_by_name = max_tool_executions_by_name
|
|
59
|
-
@output_audit = output_audit
|
|
60
|
-
@output_audit_mode = output_audit_mode
|
|
61
|
-
@output_policy = output_policy
|
|
62
|
-
@output_policy_mode = output_policy_mode
|
|
63
|
-
@output_policy_model = output_policy_model
|
|
64
|
-
@output_policy_thinking = output_policy_thinking
|
|
65
32
|
raise ArgumentError, "name is required" if @name.empty?
|
|
66
|
-
@agent = build_agent
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def run(prompt = nil, task: nil, input: nil, async: false, subject: nil, metadata: {},
|
|
70
|
-
max_spend: nil, cost_limit: nil, **options)
|
|
71
|
-
|
|
72
|
-
task = task || prompt
|
|
73
|
-
raise ArgumentError, "task is required" if task.to_s.empty?
|
|
74
|
-
|
|
75
|
-
runtime_agent = if options.empty? && cost_limit.nil? && max_spend.nil?
|
|
76
|
-
@agent
|
|
77
|
-
else
|
|
78
|
-
build_agent(cost_limit: cost_limit || max_spend, **options)
|
|
79
|
-
end
|
|
80
33
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
)
|
|
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)
|
|
88
40
|
end
|
|
89
41
|
|
|
90
|
-
def
|
|
91
|
-
|
|
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)
|
|
92
44
|
end
|
|
93
45
|
|
|
94
|
-
def
|
|
95
|
-
|
|
46
|
+
def agent(**overrides)
|
|
47
|
+
overrides.empty? ? @agent : Agent.new(**@options.merge(overrides.compact))
|
|
96
48
|
end
|
|
97
49
|
|
|
98
50
|
private
|
|
99
|
-
def
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
tools: tools,
|
|
105
|
-
skills: skills,
|
|
106
|
-
available_skills: available_skills,
|
|
107
|
-
model: model,
|
|
108
|
-
client: client,
|
|
109
|
-
store: store,
|
|
110
|
-
prompt_mode: prompt_mode,
|
|
111
|
-
thinking: thinking,
|
|
112
|
-
compaction: compaction,
|
|
113
|
-
on_event: on_event,
|
|
114
|
-
output_schema: output_schema,
|
|
115
|
-
max_iterations: max_iterations,
|
|
116
|
-
timeout: timeout,
|
|
117
|
-
cost_limit: cost_limit,
|
|
118
|
-
max_depth: max_depth,
|
|
119
|
-
max_tool_executions: max_tool_executions,
|
|
120
|
-
max_tool_executions_by_name: max_tool_executions_by_name,
|
|
121
|
-
output_audit: output_audit,
|
|
122
|
-
output_audit_mode: output_audit_mode,
|
|
123
|
-
output_policy: output_policy,
|
|
124
|
-
output_policy_mode: output_policy_mode,
|
|
125
|
-
output_policy_model: output_policy_model,
|
|
126
|
-
output_policy_thinking: output_policy_thinking
|
|
127
|
-
}
|
|
128
|
-
attrs.merge!(overrides.compact)
|
|
129
|
-
Agent.new(**attrs)
|
|
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")
|
|
130
56
|
end
|
|
131
57
|
end
|
|
132
58
|
end
|
data/lib/turnkit.rb
CHANGED
|
@@ -15,6 +15,7 @@ require_relative "turnkit/cost"
|
|
|
15
15
|
require_relative "turnkit/budget"
|
|
16
16
|
require_relative "turnkit/event"
|
|
17
17
|
require_relative "turnkit/model_request"
|
|
18
|
+
require_relative "turnkit/schema_check"
|
|
18
19
|
require_relative "turnkit/agent"
|
|
19
20
|
require_relative "turnkit/workflow"
|
|
20
21
|
require_relative "turnkit/client"
|
|
@@ -36,6 +37,7 @@ require_relative "turnkit/tool"
|
|
|
36
37
|
require_relative "turnkit/tool_call"
|
|
37
38
|
require_relative "turnkit/tool_execution"
|
|
38
39
|
require_relative "turnkit/sub_agent_tool"
|
|
40
|
+
require_relative "turnkit/load_skill_tool"
|
|
39
41
|
require_relative "turnkit/message_projection"
|
|
40
42
|
require_relative "turnkit/tool_runner"
|
|
41
43
|
require_relative "turnkit/turn"
|
|
@@ -52,7 +54,7 @@ module TurnKit
|
|
|
52
54
|
attr_accessor :default_model, :client, :store, :logger
|
|
53
55
|
attr_accessor :max_iterations, :timeout, :max_depth, :max_tool_executions
|
|
54
56
|
attr_accessor :max_tool_executions_by_name
|
|
55
|
-
attr_accessor :
|
|
57
|
+
attr_accessor :max_spend, :prompt_cache
|
|
56
58
|
attr_accessor :compaction
|
|
57
59
|
attr_accessor :output_policy_model, :output_policy_thinking
|
|
58
60
|
attr_accessor :cost_rates, :cost_calculator
|
|
@@ -72,6 +74,7 @@ module TurnKit
|
|
|
72
74
|
self.max_depth = 3
|
|
73
75
|
self.max_tool_executions = 100
|
|
74
76
|
self.max_tool_executions_by_name = {}
|
|
77
|
+
self.max_spend = nil
|
|
75
78
|
self.prompt_cache = :auto
|
|
76
79
|
self.compaction = true
|
|
77
80
|
self.cost_rates = {}
|
|
@@ -97,21 +100,13 @@ module TurnKit
|
|
|
97
100
|
self.default_model = value
|
|
98
101
|
end
|
|
99
102
|
|
|
100
|
-
def self.max_spend
|
|
101
|
-
cost_limit
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def self.max_spend=(value)
|
|
105
|
-
self.cost_limit = value
|
|
106
|
-
end
|
|
107
|
-
|
|
108
103
|
def self.reconcile_stale!(before: Clock.now - (timeout || 300))
|
|
109
104
|
store.find_stale_turns(before: before).each do |turn|
|
|
110
105
|
store.update_turn(turn.fetch("id"), "status" => "stale", "completed_at" => Clock.now)
|
|
111
106
|
end
|
|
112
107
|
end
|
|
113
108
|
|
|
114
|
-
def self.
|
|
109
|
+
def self.check_output_policy(output, constraints: [], context: {})
|
|
115
110
|
OutputAudit.check(output, constraints: constraints, context: context)
|
|
116
111
|
end
|
|
117
112
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: turnkit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sam Couch
|
|
@@ -57,6 +57,7 @@ files:
|
|
|
57
57
|
- lib/turnkit/generators/turnkit/install/templates/turn.rb
|
|
58
58
|
- lib/turnkit/generators/turnkit/install_generator.rb
|
|
59
59
|
- lib/turnkit/id.rb
|
|
60
|
+
- lib/turnkit/load_skill_tool.rb
|
|
60
61
|
- lib/turnkit/memory_store.rb
|
|
61
62
|
- lib/turnkit/message.rb
|
|
62
63
|
- lib/turnkit/message_projection.rb
|
|
@@ -70,6 +71,7 @@ files:
|
|
|
70
71
|
- lib/turnkit/record.rb
|
|
71
72
|
- lib/turnkit/result.rb
|
|
72
73
|
- lib/turnkit/run.rb
|
|
74
|
+
- lib/turnkit/schema_check.rb
|
|
73
75
|
- lib/turnkit/skill.rb
|
|
74
76
|
- lib/turnkit/store.rb
|
|
75
77
|
- lib/turnkit/stores/active_record_store.rb
|