turnkit 0.4.1 → 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 +18 -0
- data/README.md +111 -50
- 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 +30 -46
- data/lib/turnkit/agent.rb +43 -10
- data/lib/turnkit/budget.rb +1 -1
- data/lib/turnkit/client.rb +5 -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 +0 -2
- data/lib/turnkit/model_request.rb +4 -2
- data/lib/turnkit/output_policy.rb +1 -15
- 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 +73 -89
- data/lib/turnkit/version.rb +1 -1
- data/lib/turnkit/view_media_tool.rb +2 -2
- data/lib/turnkit.rb +1 -18
- metadata +15 -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/agent.rb
CHANGED
|
@@ -2,26 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
module TurnKit
|
|
4
4
|
class Agent
|
|
5
|
+
ORCHESTRATOR_PREAMBLE = <<~TEXT.strip
|
|
6
|
+
You are an autonomous task orchestrator. Navigate from the application
|
|
7
|
+
request to a final output without asking the user follow-up questions.
|
|
8
|
+
|
|
9
|
+
Use the available tools to gather context, inspect sources, take actions,
|
|
10
|
+
persist outputs, and verify work. Use loaded skills as reusable workflow
|
|
11
|
+
patterns. Iterate when work needs missing context, critique, revision, or
|
|
12
|
+
verification.
|
|
13
|
+
|
|
14
|
+
When multiple independent items need the same kind of fetch or read, and
|
|
15
|
+
an available batch tool can handle them in one call, prefer the batch tool
|
|
16
|
+
over repeated one-item tool calls.
|
|
17
|
+
|
|
18
|
+
Stop when the task is complete, when the available context and tools are
|
|
19
|
+
sufficient for the best possible answer, or when further iteration would
|
|
20
|
+
not materially improve the result. Respect runtime, cost, and iteration
|
|
21
|
+
limits.
|
|
22
|
+
TEXT
|
|
23
|
+
|
|
5
24
|
attr_reader :name, :description, :model, :instructions, :tools, :skills, :available_skills, :sub_agents
|
|
6
25
|
attr_reader :client, :store, :max_iterations, :timeout, :max_spend, :max_depth, :max_tool_executions, :max_tool_executions_by_name
|
|
7
26
|
attr_reader :prompt_sections, :system_prompt, :prompt_mode, :thinking, :compaction, :output_schema, :input_schema, :on_event
|
|
8
27
|
attr_reader :output_policy, :output_policy_mode, :output_policy_model, :output_retries
|
|
9
28
|
|
|
10
|
-
def initialize(name:, description: "", model: nil, instructions: "", tools: [], skills: [], available_skills: [], sub_agents: [],
|
|
29
|
+
def initialize(name:, description: "", model: nil, instructions: "", orchestrator: false, tools: [], skills: [], available_skills: [], sub_agents: [],
|
|
11
30
|
system_prompt: nil, prompt_sections: nil, prompt_mode: nil, client: nil, store: nil,
|
|
12
31
|
max_iterations: nil, timeout: nil, max_spend: nil, max_depth: nil, max_tool_executions: nil, max_tool_executions_by_name: nil, thinking: nil, compaction: nil,
|
|
13
32
|
output_schema: nil, input_schema: nil, output_policy: nil, output_policy_mode: nil, output_policy_model: nil, output_policy_thinking: nil, output_retries: 0, on_event: nil)
|
|
14
33
|
@name = name.to_s
|
|
15
34
|
@description = description.to_s
|
|
16
35
|
@model = model
|
|
17
|
-
@
|
|
36
|
+
@orchestrator = orchestrator ? true : false
|
|
37
|
+
@instructions = compose_instructions(instructions)
|
|
18
38
|
@tools = Array(tools)
|
|
19
39
|
@skills = Array(skills)
|
|
20
40
|
@available_skills = Array(available_skills)
|
|
21
41
|
@sub_agents = Array(sub_agents)
|
|
22
42
|
@system_prompt = system_prompt
|
|
23
43
|
@prompt_sections = prompt_sections
|
|
24
|
-
@prompt_mode = prompt_mode&.to_sym
|
|
44
|
+
@prompt_mode = prompt_mode&.to_sym || (:task if @orchestrator)
|
|
25
45
|
@client = client
|
|
26
46
|
@store = store
|
|
27
47
|
@max_iterations = max_iterations
|
|
@@ -69,8 +89,11 @@ module TurnKit
|
|
|
69
89
|
Conversation.new(agent: self, record: record, store: store, model: model || effective_model, subject: subject, metadata: metadata)
|
|
70
90
|
end
|
|
71
91
|
|
|
72
|
-
def
|
|
73
|
-
|
|
92
|
+
def orchestrator?
|
|
93
|
+
@orchestrator
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def run(task, input: nil, async: false, subject: nil, metadata: {}, parent_run: nil, root_turn_id: nil, prompt_mode: :task, **options)
|
|
74
97
|
raise ArgumentError, "task is required" if task.to_s.empty?
|
|
75
98
|
SchemaCheck.validate!(input, input_schema, error_class: InputError, label: "input") if input_schema
|
|
76
99
|
|
|
@@ -151,16 +174,19 @@ module TurnKit
|
|
|
151
174
|
end
|
|
152
175
|
end
|
|
153
176
|
|
|
154
|
-
def
|
|
155
|
-
|
|
177
|
+
def budget_limits
|
|
178
|
+
{
|
|
156
179
|
max_iterations: max_iterations || TurnKit.max_iterations,
|
|
157
180
|
timeout: timeout || TurnKit.timeout,
|
|
158
181
|
max_depth: max_depth || TurnKit.max_depth,
|
|
159
182
|
max_tool_executions: max_tool_executions || TurnKit.max_tool_executions,
|
|
160
183
|
max_tool_executions_by_name: max_tool_executions_by_name || TurnKit.max_tool_executions_by_name,
|
|
161
|
-
max_spend: max_spend || TurnKit.max_spend
|
|
162
|
-
|
|
163
|
-
|
|
184
|
+
max_spend: max_spend || TurnKit.max_spend
|
|
185
|
+
}
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def build_budget(root_started_at: Clock.now)
|
|
189
|
+
Budget.new(**budget_limits, root_started_at: root_started_at)
|
|
164
190
|
end
|
|
165
191
|
|
|
166
192
|
def instructions_with_skills
|
|
@@ -170,6 +196,13 @@ module TurnKit
|
|
|
170
196
|
end
|
|
171
197
|
|
|
172
198
|
private
|
|
199
|
+
def compose_instructions(instructions)
|
|
200
|
+
parts = []
|
|
201
|
+
parts << ORCHESTRATOR_PREAMBLE if @orchestrator
|
|
202
|
+
parts << instructions.to_s.strip unless instructions.to_s.strip.empty?
|
|
203
|
+
parts.join("\n\n")
|
|
204
|
+
end
|
|
205
|
+
|
|
173
206
|
def validate_tools!
|
|
174
207
|
effective_tools.each do |tool|
|
|
175
208
|
next if tool.is_a?(Class) && tool < Tool
|
data/lib/turnkit/budget.rb
CHANGED
|
@@ -29,7 +29,7 @@ module TurnKit
|
|
|
29
29
|
|
|
30
30
|
def seed!(turns:, tool_executions:)
|
|
31
31
|
@mutex.synchronize do
|
|
32
|
-
@iterations = Array(turns).sum { |turn| (turn
|
|
32
|
+
@iterations = Array(turns).sum { |turn| Turn.iterations_for(turn) }
|
|
33
33
|
completed = Array(tool_executions).select { |execution| %w[completed failed].include?(execution["status"]) && !execution.dig("error", "details", "budget_denied") }
|
|
34
34
|
@tool_executions = completed.length
|
|
35
35
|
completed.each { |execution| @tool_executions_by_name[execution.fetch("tool_name").to_s] += 1 }
|
data/lib/turnkit/client.rb
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module TurnKit
|
|
4
|
+
# The adapter contract. TurnKit calls clients with the full keyword
|
|
5
|
+
# signatures below. Custom adapters should subclass TurnKit::Client (or
|
|
6
|
+
# accept the same keywords) and must not execute tools themselves; TurnKit
|
|
7
|
+
# runs tools and persists their results.
|
|
4
8
|
class Client
|
|
5
9
|
def validate!(model:)
|
|
6
10
|
true
|
|
7
11
|
end
|
|
8
12
|
|
|
9
|
-
def chat(model:, messages:, tools:, instructions:, temperature: nil, thinking: nil, output_schema: nil, metadata: nil, on_event: nil)
|
|
13
|
+
def chat(model:, messages:, tools:, instructions:, dynamic_instructions: nil, temperature: nil, thinking: nil, output_schema: nil, metadata: nil, on_event: nil)
|
|
10
14
|
raise NotImplementedError
|
|
11
15
|
end
|
|
12
16
|
|
data/lib/turnkit/compaction.rb
CHANGED
|
@@ -3,19 +3,19 @@
|
|
|
3
3
|
module TurnKit
|
|
4
4
|
module Compaction
|
|
5
5
|
DEFAULTS = {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
enabled: true,
|
|
7
|
+
threshold: 0.75,
|
|
8
|
+
context_limit: 128_000,
|
|
9
|
+
reserved_tokens: 20_000,
|
|
10
|
+
head_messages: 0,
|
|
11
|
+
tail_messages: 12,
|
|
12
|
+
tail_tokens: 8_000,
|
|
13
|
+
summary_ratio: 0.20,
|
|
14
|
+
min_summary_tokens: 1_000,
|
|
15
|
+
max_summary_tokens: 12_000,
|
|
16
|
+
tool_output_max_chars: 2_000,
|
|
17
|
+
model: nil,
|
|
18
|
+
client: nil
|
|
19
19
|
}.freeze
|
|
20
20
|
|
|
21
21
|
KNOWN_KEYS = DEFAULTS.keys.freeze
|
|
@@ -92,7 +92,7 @@ module TurnKit
|
|
|
92
92
|
module_function
|
|
93
93
|
|
|
94
94
|
def enabled_for?(agent, overrides = {})
|
|
95
|
-
policy_for(agent, overrides)[
|
|
95
|
+
policy_for(agent, overrides)[:enabled]
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
def policy_for(agent, overrides = {})
|
|
@@ -100,9 +100,9 @@ module TurnKit
|
|
|
100
100
|
local = normalize_config(agent.compaction)
|
|
101
101
|
override = normalize_config(overrides)
|
|
102
102
|
|
|
103
|
-
return DEFAULTS.merge(
|
|
104
|
-
return DEFAULTS.merge(
|
|
105
|
-
return DEFAULTS.merge(
|
|
103
|
+
return DEFAULTS.merge(enabled: false) if global == false
|
|
104
|
+
return DEFAULTS.merge(enabled: false) if local == false
|
|
105
|
+
return DEFAULTS.merge(enabled: false) if override == false
|
|
106
106
|
|
|
107
107
|
DEFAULTS.merge(global || {}).merge(local || {}).merge(override || {})
|
|
108
108
|
end
|
|
@@ -112,7 +112,7 @@ module TurnKit
|
|
|
112
112
|
|
|
113
113
|
force = turn.compact == true if force.nil?
|
|
114
114
|
policy = policy_for(turn.agent)
|
|
115
|
-
return unless policy[
|
|
115
|
+
return unless policy[:enabled]
|
|
116
116
|
|
|
117
117
|
messages = project(turn.conversation.messages_for_turn(turn))
|
|
118
118
|
return unless force || over_threshold?(messages, policy)
|
|
@@ -127,7 +127,7 @@ module TurnKit
|
|
|
127
127
|
|
|
128
128
|
def compact!(conversation, agent:, turn: nil, focus: nil, auto: false, overrides: {}, force: true)
|
|
129
129
|
policy = policy_for(agent, overrides)
|
|
130
|
-
raise CompactionError, "compaction is disabled" unless policy[
|
|
130
|
+
raise CompactionError, "compaction is disabled" unless policy[:enabled]
|
|
131
131
|
|
|
132
132
|
messages = turn ? conversation.messages_for_turn(turn) : conversation.messages
|
|
133
133
|
projected = project(messages)
|
|
@@ -135,14 +135,14 @@ module TurnKit
|
|
|
135
135
|
return nil if selected.nil? && auto
|
|
136
136
|
raise CompactionError, "not enough messages to compact" unless selected
|
|
137
137
|
|
|
138
|
-
selected_tokens = estimate_messages_tokens(selected.fetch(
|
|
138
|
+
selected_tokens = estimate_messages_tokens(selected.fetch(:middle))
|
|
139
139
|
return nil if auto && !force && !over_threshold?(projected, policy)
|
|
140
140
|
|
|
141
141
|
summary = generate_summary(
|
|
142
142
|
agent: agent,
|
|
143
143
|
policy: policy,
|
|
144
|
-
messages: selected.fetch(
|
|
145
|
-
previous_summary: selected[
|
|
144
|
+
messages: selected.fetch(:middle),
|
|
145
|
+
previous_summary: selected[:previous_summary]&.text,
|
|
146
146
|
focus: focus,
|
|
147
147
|
target_tokens: summary_budget(selected_tokens, policy),
|
|
148
148
|
fallback_model: turn&.model || conversation.model || agent.effective_model,
|
|
@@ -209,25 +209,25 @@ module TurnKit
|
|
|
209
209
|
end
|
|
210
210
|
|
|
211
211
|
def summary_budget(input_tokens, policy)
|
|
212
|
-
budget = (input_tokens.to_i * policy[
|
|
213
|
-
budget = [ budget, policy[
|
|
214
|
-
[ budget, policy[
|
|
212
|
+
budget = (input_tokens.to_i * policy[:summary_ratio].to_f).ceil
|
|
213
|
+
budget = [ budget, policy[:min_summary_tokens].to_i ].max
|
|
214
|
+
[ budget, policy[:max_summary_tokens].to_i ].min
|
|
215
215
|
end
|
|
216
216
|
|
|
217
217
|
def over_threshold?(messages, policy)
|
|
218
|
-
usable = [ policy[
|
|
219
|
-
estimate_messages_tokens(messages) >= (usable * policy[
|
|
218
|
+
usable = [ policy[:context_limit].to_i - policy[:reserved_tokens].to_i, 1 ].max
|
|
219
|
+
estimate_messages_tokens(messages) >= (usable * policy[:threshold].to_f)
|
|
220
220
|
end
|
|
221
221
|
|
|
222
222
|
def select_messages(messages, policy)
|
|
223
223
|
rows = Array(messages)
|
|
224
|
-
return nil if rows.length <= policy[
|
|
224
|
+
return nil if rows.length <= policy[:head_messages].to_i + 1
|
|
225
225
|
|
|
226
226
|
previous_summary = rows.reverse.find(&:context_summary?)
|
|
227
227
|
candidates = rows.reject(&:context_summary?)
|
|
228
|
-
return nil if candidates.length <= policy[
|
|
228
|
+
return nil if candidates.length <= policy[:head_messages].to_i + 1
|
|
229
229
|
|
|
230
|
-
head_count = policy[
|
|
230
|
+
head_count = policy[:head_messages].to_i
|
|
231
231
|
tail_start = tail_start_index(candidates, policy)
|
|
232
232
|
tail_start = [ tail_start, head_count ].max
|
|
233
233
|
tail_start = expand_tail_start_for_tool_pairs(candidates, tail_start)
|
|
@@ -242,11 +242,11 @@ module TurnKit
|
|
|
242
242
|
end
|
|
243
243
|
|
|
244
244
|
{
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
245
|
+
middle: middle,
|
|
246
|
+
previous_summary: previous_summary,
|
|
247
|
+
replaces_from_sequence: from_sequence,
|
|
248
|
+
replaces_through_sequence: through_sequence,
|
|
249
|
+
tail_start_sequence: candidates[tail_start]&.sequence
|
|
250
250
|
}
|
|
251
251
|
end
|
|
252
252
|
|
|
@@ -290,7 +290,7 @@ module TurnKit
|
|
|
290
290
|
when false
|
|
291
291
|
false
|
|
292
292
|
when Hash
|
|
293
|
-
attrs = value.transform_keys(&:
|
|
293
|
+
attrs = value.transform_keys(&:to_sym)
|
|
294
294
|
unknown = attrs.keys - KNOWN_KEYS
|
|
295
295
|
raise ConfigError, "unknown compaction options: #{unknown.join(", ")}" if unknown.any?
|
|
296
296
|
|
|
@@ -323,8 +323,8 @@ module TurnKit
|
|
|
323
323
|
end
|
|
324
324
|
|
|
325
325
|
def tail_start_index(messages, policy)
|
|
326
|
-
max_messages = policy[
|
|
327
|
-
max_tokens = policy[
|
|
326
|
+
max_messages = policy[:tail_messages].to_i
|
|
327
|
+
max_tokens = policy[:tail_tokens].to_i
|
|
328
328
|
count = 0
|
|
329
329
|
tokens = 0
|
|
330
330
|
index = messages.length
|
|
@@ -357,8 +357,8 @@ module TurnKit
|
|
|
357
357
|
end
|
|
358
358
|
|
|
359
359
|
def generate_summary(agent:, policy:, messages:, previous_summary:, focus:, target_tokens:, fallback_model:, conversation_id:, turn_id:, turn: nil)
|
|
360
|
-
client = policy[
|
|
361
|
-
model = policy[
|
|
360
|
+
client = policy[:client] || agent.effective_client
|
|
361
|
+
model = policy[:model] || fallback_model
|
|
362
362
|
safe_messages = messages.map { |message| sanitize_message(message, policy) }
|
|
363
363
|
prompt = build_prompt(previous_summary: previous_summary, focus: focus, target_tokens: target_tokens)
|
|
364
364
|
attrs = {
|
|
@@ -369,7 +369,7 @@ module TurnKit
|
|
|
369
369
|
metadata: { compaction: true, conversation_id: conversation_id, turn_id: turn_id }
|
|
370
370
|
}
|
|
371
371
|
result = if turn
|
|
372
|
-
turn.internal_model_call(**attrs, purpose: "compaction", client: policy[
|
|
372
|
+
turn.internal_model_call(**attrs, purpose: "compaction", client: policy[:client])
|
|
373
373
|
else
|
|
374
374
|
client.validate!(model: model)
|
|
375
375
|
client.chat(**attrs)
|
|
@@ -383,7 +383,7 @@ module TurnKit
|
|
|
383
383
|
def sanitize_message(message, policy)
|
|
384
384
|
return message unless message.tool_result?
|
|
385
385
|
|
|
386
|
-
max = policy[
|
|
386
|
+
max = policy[:tool_output_max_chars].to_i
|
|
387
387
|
return message if max <= 0 || message.text.length <= max
|
|
388
388
|
|
|
389
389
|
attrs = message.to_h
|
|
@@ -392,7 +392,7 @@ module TurnKit
|
|
|
392
392
|
end
|
|
393
393
|
|
|
394
394
|
def append_summary(conversation, turn:, summary:, selected:, policy:, focus:, auto:, input_tokens:)
|
|
395
|
-
model = policy[
|
|
395
|
+
model = policy[:model] || turn&.model || conversation.model || conversation.agent.effective_model
|
|
396
396
|
conversation.append_message(
|
|
397
397
|
role: "assistant",
|
|
398
398
|
kind: "context_summary",
|
|
@@ -402,9 +402,9 @@ module TurnKit
|
|
|
402
402
|
"compaction" => {
|
|
403
403
|
"auto" => auto,
|
|
404
404
|
"focus" => focus,
|
|
405
|
-
"replaces_from_sequence" => selected.fetch(
|
|
406
|
-
"replaces_through_sequence" => selected.fetch(
|
|
407
|
-
"tail_start_sequence" => selected[
|
|
405
|
+
"replaces_from_sequence" => selected.fetch(:replaces_from_sequence),
|
|
406
|
+
"replaces_through_sequence" => selected.fetch(:replaces_through_sequence),
|
|
407
|
+
"tail_start_sequence" => selected[:tail_start_sequence],
|
|
408
408
|
"summary_model" => model,
|
|
409
409
|
"input_tokens" => input_tokens,
|
|
410
410
|
"summary_tokens" => estimate_text_tokens(summary),
|
data/lib/turnkit/cost.rb
CHANGED
|
@@ -48,42 +48,40 @@ module TurnKit
|
|
|
48
48
|
from_usage(Usage.from_h(usage), model: attrs["model"])
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
RATE_KEYS = %i[input output cache_read cache_write thinking].freeze
|
|
52
|
+
|
|
53
|
+
# Rates are USD per million tokens, keyed by component.
|
|
51
54
|
def self.from_rates(usage, rates)
|
|
52
55
|
rates = rates.transform_keys(&:to_sym)
|
|
56
|
+
unknown = rates.keys - RATE_KEYS
|
|
57
|
+
raise ConfigError, "unknown cost rate keys: #{unknown.join(", ")} (use: #{RATE_KEYS.join(", ")})" if unknown.any?
|
|
58
|
+
|
|
53
59
|
new(
|
|
54
|
-
input: amount(usage.input_tokens, rates[:input]
|
|
55
|
-
output: amount(usage.output_tokens, rates[:output]
|
|
56
|
-
cache_read: amount(usage.cached_tokens, rates[:cache_read]
|
|
57
|
-
cache_write: amount(usage.cache_write_tokens, rates[:cache_write]
|
|
58
|
-
thinking: amount(usage.thinking_tokens, rates[:thinking]
|
|
60
|
+
input: amount(usage.input_tokens, rates[:input]),
|
|
61
|
+
output: amount(usage.output_tokens, rates[:output]),
|
|
62
|
+
cache_read: amount(usage.cached_tokens, rates[:cache_read]),
|
|
63
|
+
cache_write: amount(usage.cache_write_tokens, rates[:cache_write]),
|
|
64
|
+
thinking: amount(usage.thinking_tokens, rates[:thinking]),
|
|
59
65
|
strict: true
|
|
60
66
|
)
|
|
61
67
|
end
|
|
62
68
|
|
|
69
|
+
# Estimates cost from RubyLLM's model pricing registry (ruby_llm >= 1.16).
|
|
70
|
+
# Returns an empty Cost when ruby_llm is not loaded or the model is not in
|
|
71
|
+
# the registry; any other failure raises.
|
|
63
72
|
def self.from_ruby_llm(usage, model)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
model_info = ::RubyLLM.models.find(model)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
)
|
|
77
|
-
from_hash(::RubyLLM::Cost.new(tokens: tokens, model: model_info).to_h)
|
|
78
|
-
else
|
|
79
|
-
from_rates(
|
|
80
|
-
usage,
|
|
81
|
-
input: model_info.input_price_per_million,
|
|
82
|
-
output: model_info.output_price_per_million,
|
|
83
|
-
cached_input: model_info.pricing&.text_tokens&.cached_input
|
|
84
|
-
)
|
|
85
|
-
end
|
|
86
|
-
rescue LoadError, StandardError
|
|
73
|
+
return new unless defined?(::RubyLLM) && model
|
|
74
|
+
|
|
75
|
+
model_info = ::RubyLLM.models.find(model)
|
|
76
|
+
tokens = ::RubyLLM::Tokens.new(
|
|
77
|
+
input: usage.input_tokens,
|
|
78
|
+
output: usage.output_tokens,
|
|
79
|
+
cached: usage.cached_tokens,
|
|
80
|
+
cache_creation: usage.cache_write_tokens,
|
|
81
|
+
thinking: usage.thinking_tokens
|
|
82
|
+
)
|
|
83
|
+
from_hash(::RubyLLM::Cost.new(tokens: tokens, model: model_info).to_h)
|
|
84
|
+
rescue ::RubyLLM::ModelNotFoundError
|
|
87
85
|
new
|
|
88
86
|
end
|
|
89
87
|
|
|
@@ -92,9 +90,9 @@ module TurnKit
|
|
|
92
90
|
new(
|
|
93
91
|
input: hash[:input],
|
|
94
92
|
output: hash[:output],
|
|
95
|
-
cache_read: hash[:cache_read]
|
|
96
|
-
cache_write: hash[:cache_write]
|
|
97
|
-
thinking: hash[:thinking]
|
|
93
|
+
cache_read: hash[:cache_read],
|
|
94
|
+
cache_write: hash[:cache_write],
|
|
95
|
+
thinking: hash[:thinking],
|
|
98
96
|
total: hash[:total]
|
|
99
97
|
)
|
|
100
98
|
end
|
data/lib/turnkit/image_result.rb
CHANGED
data/lib/turnkit/image_tool.rb
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module TurnKit
|
|
4
4
|
class ModelRequest
|
|
5
|
-
attr_reader :model, :messages, :tools, :instructions, :thinking, :output_schema, :metadata, :report
|
|
5
|
+
attr_reader :model, :messages, :tools, :instructions, :dynamic_instructions, :thinking, :output_schema, :metadata, :report
|
|
6
6
|
|
|
7
|
-
def initialize(model:, messages:, tools:, instructions:, thinking: nil, output_schema: nil, metadata: {}, report: nil)
|
|
7
|
+
def initialize(model:, messages:, tools:, instructions:, dynamic_instructions: nil, thinking: nil, output_schema: nil, metadata: {}, report: nil)
|
|
8
8
|
@model = model
|
|
9
9
|
@messages = Array(messages)
|
|
10
10
|
@tools = Array(tools)
|
|
11
11
|
@instructions = instructions.to_s
|
|
12
|
+
@dynamic_instructions = dynamic_instructions.to_s
|
|
12
13
|
@thinking = thinking
|
|
13
14
|
@output_schema = output_schema
|
|
14
15
|
@metadata = metadata || {}
|
|
@@ -25,6 +26,7 @@ module TurnKit
|
|
|
25
26
|
"messages" => messages,
|
|
26
27
|
"tools" => tool_names,
|
|
27
28
|
"instructions" => instructions,
|
|
29
|
+
"dynamic_instructions" => dynamic_instructions,
|
|
28
30
|
"thinking" => thinking,
|
|
29
31
|
"output_schema" => output_schema,
|
|
30
32
|
"metadata" => metadata,
|
|
@@ -75,7 +75,7 @@ module TurnKit
|
|
|
75
75
|
else
|
|
76
76
|
audit_client = client || TurnKit.client
|
|
77
77
|
audit_client.validate!(model: model_name)
|
|
78
|
-
chat(
|
|
78
|
+
audit_client.chat(model: model_name, messages: audit_messages(output), tools: [], instructions: audit_instructions, thinking: thinking, output_schema: DEFAULT_SCHEMA, metadata: { output_policy: name })
|
|
79
79
|
end
|
|
80
80
|
data = result.output_data || parse_json(result.text)
|
|
81
81
|
return if data.fetch("approved", false)
|
|
@@ -111,20 +111,6 @@ module TurnKit
|
|
|
111
111
|
[ { role: :user, content: JSON.generate(output: output) } ]
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
-
def chat(client, **kwargs)
|
|
115
|
-
accepted = chat_keyword_names(client)
|
|
116
|
-
kwargs = kwargs.slice(*accepted) unless accepted.include?(:keyrest)
|
|
117
|
-
client.chat(**kwargs)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def chat_keyword_names(client)
|
|
121
|
-
client.method(:chat).parameters.filter_map do |kind, name|
|
|
122
|
-
return [ :keyrest ] if kind == :keyrest
|
|
123
|
-
|
|
124
|
-
name if %i[key keyreq].include?(kind)
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
114
|
def parse_json(value)
|
|
129
115
|
JSON.parse(extract_json(value.to_s))
|
|
130
116
|
rescue JSON::ParserError
|
data/lib/turnkit/run.rb
CHANGED
|
@@ -11,16 +11,12 @@ module TurnKit
|
|
|
11
11
|
def id = turn.id
|
|
12
12
|
def root_turn_id = turn.root_turn_id
|
|
13
13
|
def status = turn.status
|
|
14
|
-
def output = output_text
|
|
15
14
|
def output_text = turn.output_text
|
|
16
15
|
def output_data = turn.output_data
|
|
17
16
|
def policy_audit = turn.policy_audit
|
|
18
17
|
def policy_clean? = policy_audit.nil? || policy_audit.fetch("clean", false)
|
|
19
18
|
def usage = Usage.from_records(turn_records)
|
|
20
19
|
def cost = Cost.from_records(turn_records)
|
|
21
|
-
def steps = turn_records.length
|
|
22
|
-
def tool_calls = tool_executions
|
|
23
|
-
def persisted? = true
|
|
24
20
|
|
|
25
21
|
def error
|
|
26
22
|
turn.store.load_turn(id)["error"]
|
data/lib/turnkit/store.rb
CHANGED
|
@@ -12,12 +12,10 @@ module TurnKit
|
|
|
12
12
|
def create_turn(_attributes) = raise(NotImplementedError)
|
|
13
13
|
def load_turn(_id) = raise(NotImplementedError)
|
|
14
14
|
def update_turn(_id, _attributes) = raise(NotImplementedError)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
update_turn(id, attributes.merge(status: to))
|
|
20
|
-
end
|
|
15
|
+
# claim_turn is the concurrency-safety point: it must atomically
|
|
16
|
+
# compare-and-set status from `from` to `to` (returning nil when the turn
|
|
17
|
+
# is not in `from`), so concurrent workers cannot both claim a turn.
|
|
18
|
+
def claim_turn(_id, from: "pending", to: "running", **_attributes) = raise(NotImplementedError)
|
|
21
19
|
def list_turns(root_turn_id: nil, conversation_id: nil, agent_name: nil) = raise(NotImplementedError)
|
|
22
20
|
|
|
23
21
|
def create_tool_execution(_attributes) = raise(NotImplementedError)
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module TurnKit
|
|
4
4
|
class SubAgentTool < Tool
|
|
5
|
-
parameter :task, :string, required: true, description: "The task for the sub-agent
|
|
6
|
-
parameter :context, :string, required: false, description: "Relevant context for the sub-agent."
|
|
5
|
+
parameter :task, :string, required: true, description: "The complete task for the sub-agent, including all relevant context."
|
|
7
6
|
|
|
8
7
|
def self.for(agent)
|
|
9
8
|
Class.new(self) do
|
|
@@ -18,25 +17,21 @@ module TurnKit
|
|
|
18
17
|
end
|
|
19
18
|
end
|
|
20
19
|
|
|
21
|
-
def call(task:, context:
|
|
20
|
+
def call(task:, context:)
|
|
22
21
|
sub_agent = self.class.agent
|
|
23
|
-
parent_turn =
|
|
24
|
-
|
|
25
|
-
conversation = sub_agent.conversation(metadata: {
|
|
22
|
+
parent_turn = context.turn
|
|
23
|
+
lineage = {
|
|
26
24
|
"parent_conversation_id" => parent_turn.conversation.id,
|
|
27
25
|
"parent_turn_id" => parent_turn.id,
|
|
28
|
-
"parent_tool_execution_id" =>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"parent_turn_id" => parent_turn.id,
|
|
33
|
-
"parent_tool_execution_id" => turnkit_context.execution.id
|
|
34
|
-
})
|
|
26
|
+
"parent_tool_execution_id" => context.execution.id
|
|
27
|
+
}
|
|
28
|
+
conversation = sub_agent.conversation(metadata: lineage)
|
|
29
|
+
trigger = conversation.say(task, metadata: lineage)
|
|
35
30
|
child = conversation.run!(
|
|
36
31
|
trigger_message_id: trigger.id,
|
|
37
32
|
budget: parent_turn.budget,
|
|
38
33
|
parent_turn: parent_turn,
|
|
39
|
-
parent_tool_execution:
|
|
34
|
+
parent_tool_execution: context.execution,
|
|
40
35
|
depth: parent_turn.depth + 1,
|
|
41
36
|
model: sub_agent.effective_model,
|
|
42
37
|
agent: sub_agent,
|