turnkit 0.2.6 → 0.2.7
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/README.md +146 -401
- data/lib/turnkit/adapters/ruby_llm.rb +69 -5
- data/lib/turnkit/agent.rb +19 -2
- data/lib/turnkit/client.rb +5 -1
- data/lib/turnkit/conversation.rb +5 -4
- data/lib/turnkit/error.rb +2 -0
- data/lib/turnkit/event.rb +25 -0
- data/lib/turnkit/generators/turnkit/install/templates/create_turnkit_tables.rb +1 -0
- data/lib/turnkit/generators/turnkit/install/templates/initializer.rb +6 -0
- data/lib/turnkit/generators/turnkit/install_generator.rb +6 -0
- data/lib/turnkit/model_request.rb +35 -0
- data/lib/turnkit/record.rb +2 -1
- data/lib/turnkit/result.rb +3 -2
- data/lib/turnkit/stores/active_record_store.rb +14 -4
- data/lib/turnkit/sub_agent_tool.rb +13 -4
- data/lib/turnkit/tool.rb +117 -4
- data/lib/turnkit/tool_call.rb +3 -1
- data/lib/turnkit/tool_runner.rb +8 -1
- data/lib/turnkit/turn.rb +92 -15
- data/lib/turnkit/version.rb +1 -1
- data/lib/turnkit.rb +4 -0
- metadata +6 -5
data/lib/turnkit/turn.rb
CHANGED
|
@@ -6,10 +6,10 @@ module TurnKit
|
|
|
6
6
|
|
|
7
7
|
attr_reader :agent, :conversation, :store, :budget, :depth
|
|
8
8
|
attr_reader :id, :conversation_id, :agent_name, :parent_turn_id, :parent_tool_execution_id
|
|
9
|
-
attr_reader :root_turn_id, :context_message_sequence, :model, :thinking, :compact
|
|
9
|
+
attr_reader :root_turn_id, :context_message_sequence, :model, :thinking, :compact, :output_schema
|
|
10
10
|
attr_reader :started_at
|
|
11
11
|
|
|
12
|
-
def initialize(agent:, conversation:, record:, store:, budget: nil, depth: 0)
|
|
12
|
+
def initialize(agent:, conversation:, record:, store:, budget: nil, depth: 0, on_event: nil)
|
|
13
13
|
@agent = agent
|
|
14
14
|
@conversation = conversation
|
|
15
15
|
@store = store
|
|
@@ -24,28 +24,29 @@ module TurnKit
|
|
|
24
24
|
@model = @record["model"] || agent.effective_model
|
|
25
25
|
@thinking = thinking_from_options
|
|
26
26
|
@compact = compact_from_options
|
|
27
|
+
@output_schema = output_schema_from_options
|
|
27
28
|
@started_at = @record["started_at"]
|
|
28
29
|
@budget = budget || agent.build_budget
|
|
29
30
|
@depth = depth
|
|
31
|
+
@on_event = on_event
|
|
30
32
|
end
|
|
31
33
|
|
|
32
|
-
def run!
|
|
34
|
+
def run!(&block)
|
|
35
|
+
@on_event = block if block
|
|
33
36
|
return self unless status == "pending"
|
|
34
37
|
|
|
35
38
|
update!(status: "running", started_at: Clock.now, heartbeat_at: Clock.now)
|
|
39
|
+
emit("turn.started", status: status, model: model)
|
|
40
|
+
agent.effective_client.validate!(model: model)
|
|
36
41
|
loop do
|
|
37
42
|
budget.check!(depth: depth)
|
|
38
43
|
budget.count_iteration!
|
|
39
44
|
TurnKit::Compaction.maybe_compact!(self)
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
instructions: agent.system_prompt_for(turn: self, conversation: conversation),
|
|
46
|
-
thinking: thinking,
|
|
47
|
-
metadata: { turn_id: id, conversation_id: conversation.id }
|
|
48
|
-
)
|
|
46
|
+
request = model_request
|
|
47
|
+
emit("model.requested", model: request.model, tool_names: request.tool_names)
|
|
48
|
+
result = call_client(request)
|
|
49
|
+
emit("model.completed", model: result.model || model, tool_call_count: result.tool_calls.length)
|
|
49
50
|
result_cost = Cost.from_usage(result.usage, model: result.model || model)
|
|
50
51
|
|
|
51
52
|
budget.add_cost!(result_cost.total)
|
|
@@ -60,7 +61,8 @@ module TurnKit
|
|
|
60
61
|
break
|
|
61
62
|
end
|
|
62
63
|
else
|
|
63
|
-
update!(status: "completed", output_text: result.text, completed_at: Clock.now)
|
|
64
|
+
update!(status: "completed", output_text: result.text, output_data: result.output_data, completed_at: Clock.now)
|
|
65
|
+
emit("turn.completed", status: status, output_text: result.text)
|
|
64
66
|
break
|
|
65
67
|
end
|
|
66
68
|
end
|
|
@@ -68,10 +70,15 @@ module TurnKit
|
|
|
68
70
|
self
|
|
69
71
|
rescue StandardError => error
|
|
70
72
|
update!(status: "failed", error: { "class" => error.class.name, "message" => error.message }, completed_at: Clock.now)
|
|
73
|
+
emit("turn.failed", error: { "class" => error.class.name, "message" => error.message })
|
|
71
74
|
reload
|
|
72
75
|
self
|
|
73
76
|
end
|
|
74
77
|
|
|
78
|
+
def preview
|
|
79
|
+
model_request
|
|
80
|
+
end
|
|
81
|
+
|
|
75
82
|
def status
|
|
76
83
|
@record.fetch("status")
|
|
77
84
|
end
|
|
@@ -84,6 +91,10 @@ module TurnKit
|
|
|
84
91
|
@record["output_text"].to_s
|
|
85
92
|
end
|
|
86
93
|
|
|
94
|
+
def output_data
|
|
95
|
+
@record["output_data"]
|
|
96
|
+
end
|
|
97
|
+
|
|
87
98
|
def usage
|
|
88
99
|
Usage.from_h(@record["usage"] || {})
|
|
89
100
|
end
|
|
@@ -100,6 +111,7 @@ module TurnKit
|
|
|
100
111
|
@record = store.load_turn(id)
|
|
101
112
|
@thinking = thinking_from_options
|
|
102
113
|
@compact = compact_from_options
|
|
114
|
+
@output_schema = output_schema_from_options
|
|
103
115
|
self
|
|
104
116
|
end
|
|
105
117
|
|
|
@@ -107,7 +119,57 @@ module TurnKit
|
|
|
107
119
|
update!(status: "stale", completed_at: Clock.now)
|
|
108
120
|
end
|
|
109
121
|
|
|
122
|
+
def emit(type, payload = {})
|
|
123
|
+
emit_event(Event.new(type: type, turn_id: id, conversation_id: conversation.id, payload: payload))
|
|
124
|
+
end
|
|
125
|
+
|
|
110
126
|
private
|
|
127
|
+
def model_request
|
|
128
|
+
prompt = SystemPrompt.new(agent: agent, turn: self, conversation: conversation, mode: agent.effective_prompt_mode(turn: self))
|
|
129
|
+
instructions = case agent.system_prompt
|
|
130
|
+
when nil
|
|
131
|
+
prompt.to_s
|
|
132
|
+
when String
|
|
133
|
+
agent.system_prompt
|
|
134
|
+
else
|
|
135
|
+
agent.system_prompt.call(prompt).to_s
|
|
136
|
+
end
|
|
137
|
+
ModelRequest.new(
|
|
138
|
+
model: model,
|
|
139
|
+
messages: llm_messages,
|
|
140
|
+
tools: agent.effective_tools,
|
|
141
|
+
instructions: instructions,
|
|
142
|
+
thinking: thinking,
|
|
143
|
+
output_schema: output_schema,
|
|
144
|
+
metadata: { turn_id: id, conversation_id: conversation.id },
|
|
145
|
+
report: prompt.report
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def call_client(request)
|
|
150
|
+
kwargs = {
|
|
151
|
+
model: request.model,
|
|
152
|
+
messages: request.messages,
|
|
153
|
+
tools: request.tools,
|
|
154
|
+
instructions: request.instructions,
|
|
155
|
+
thinking: request.thinking,
|
|
156
|
+
output_schema: request.output_schema,
|
|
157
|
+
metadata: request.metadata,
|
|
158
|
+
on_event: ->(event) { emit_event(event) }
|
|
159
|
+
}
|
|
160
|
+
accepted = chat_keyword_names(agent.effective_client)
|
|
161
|
+
kwargs = kwargs.slice(*accepted) unless accepted.include?(:keyrest)
|
|
162
|
+
agent.effective_client.chat(**kwargs)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def chat_keyword_names(client)
|
|
166
|
+
client.method(:chat).parameters.filter_map do |kind, name|
|
|
167
|
+
return [ :keyrest ] if kind == :keyrest
|
|
168
|
+
|
|
169
|
+
name if %i[key keyreq].include?(kind)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
111
173
|
def llm_messages
|
|
112
174
|
MessageProjection.for(TurnKit::Compaction.project(conversation.messages_for_turn(self)))
|
|
113
175
|
end
|
|
@@ -124,24 +186,34 @@ module TurnKit
|
|
|
124
186
|
options["compact"] if options.key?("compact")
|
|
125
187
|
end
|
|
126
188
|
|
|
189
|
+
def output_schema_from_options
|
|
190
|
+
options = (@record["options"] || {}).transform_keys(&:to_s)
|
|
191
|
+
options["output_schema"] if options.key?("output_schema")
|
|
192
|
+
end
|
|
193
|
+
|
|
127
194
|
def persist_assistant_message(result)
|
|
128
195
|
if result.tool_calls?
|
|
129
|
-
conversation.append_message(
|
|
196
|
+
message = conversation.append_message(
|
|
130
197
|
role: "assistant",
|
|
131
198
|
kind: "tool_call",
|
|
132
199
|
text: result.text,
|
|
133
200
|
turn_id: id,
|
|
134
201
|
metadata: { "tool_calls" => result.tool_calls.map { |call| { "id" => call.id, "name" => call.name, "arguments" => call.arguments } } }
|
|
135
202
|
)
|
|
203
|
+
emit("message.created", message_id: message.id, role: message.role, kind: message.kind)
|
|
204
|
+
result.tool_calls.each { |call| emit("tool_call.created", id: call.id, name: call.name) }
|
|
136
205
|
else
|
|
137
|
-
conversation.append_message(role: "assistant", kind: "text", text: result.text, turn_id: id)
|
|
206
|
+
message = conversation.append_message(role: "assistant", kind: "text", text: result.text, turn_id: id, metadata: { "output_data" => result.output_data }.compact)
|
|
207
|
+
emit("message.created", message_id: message.id, role: message.role, kind: message.kind)
|
|
138
208
|
end
|
|
139
209
|
end
|
|
140
210
|
|
|
141
211
|
def complete_from_terminal_tool(runner, execution)
|
|
142
212
|
message = runner.completion_message(execution)
|
|
143
|
-
conversation.append_message(role: "assistant", kind: "text", text: message, turn_id: id)
|
|
213
|
+
assistant = conversation.append_message(role: "assistant", kind: "text", text: message, turn_id: id)
|
|
214
|
+
emit("message.created", message_id: assistant.id, role: assistant.role, kind: assistant.kind)
|
|
144
215
|
update!(status: "completed", output_text: message, completed_at: Clock.now)
|
|
216
|
+
emit("turn.completed", status: status, output_text: message)
|
|
145
217
|
end
|
|
146
218
|
|
|
147
219
|
def add_usage!(usage, cost: nil)
|
|
@@ -172,6 +244,11 @@ module TurnKit
|
|
|
172
244
|
@model = @record["model"] || agent.effective_model
|
|
173
245
|
@record
|
|
174
246
|
end
|
|
247
|
+
|
|
248
|
+
def emit_event(event)
|
|
249
|
+
event = Event.new(type: event[:type] || event["type"], turn_id: id, conversation_id: conversation.id, payload: event[:payload] || event["payload"] || {}) if event.is_a?(Hash)
|
|
250
|
+
Array(@on_event || agent.effective_on_event).each { |callback| callback.call(event) }
|
|
251
|
+
end
|
|
175
252
|
end
|
|
176
253
|
|
|
177
254
|
class ToolContext
|
data/lib/turnkit/version.rb
CHANGED
data/lib/turnkit.rb
CHANGED
|
@@ -12,6 +12,8 @@ require_relative "turnkit/id"
|
|
|
12
12
|
require_relative "turnkit/clock"
|
|
13
13
|
require_relative "turnkit/cost"
|
|
14
14
|
require_relative "turnkit/budget"
|
|
15
|
+
require_relative "turnkit/event"
|
|
16
|
+
require_relative "turnkit/model_request"
|
|
15
17
|
require_relative "turnkit/agent"
|
|
16
18
|
require_relative "turnkit/client"
|
|
17
19
|
require_relative "turnkit/conversation"
|
|
@@ -49,6 +51,7 @@ module TurnKit
|
|
|
49
51
|
attr_accessor :prompt_sections, :prompt_behavior, :available_skills
|
|
50
52
|
attr_accessor :prompt_data_max_chars, :context_contributors
|
|
51
53
|
attr_accessor :system_prompt_contributors, :model_prompt_contributors
|
|
54
|
+
attr_accessor :on_event
|
|
52
55
|
attr_accessor :conversation_record_class, :turn_record_class
|
|
53
56
|
attr_accessor :message_record_class, :tool_execution_record_class
|
|
54
57
|
end
|
|
@@ -69,6 +72,7 @@ module TurnKit
|
|
|
69
72
|
self.context_contributors = []
|
|
70
73
|
self.system_prompt_contributors = []
|
|
71
74
|
self.model_prompt_contributors = {}
|
|
75
|
+
self.on_event = nil
|
|
72
76
|
|
|
73
77
|
def self.reconcile_stale!(before: Clock.now - (timeout || 300))
|
|
74
78
|
store.find_stale_turns(before: before).each do |turn|
|
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.2.
|
|
4
|
+
version: 0.2.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sam Couch
|
|
@@ -24,9 +24,8 @@ dependencies:
|
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '1.14'
|
|
27
|
-
description: TurnKit
|
|
28
|
-
|
|
29
|
-
persistence.
|
|
27
|
+
description: TurnKit is a Ruby/Rails agent runtime for durable AI conversations, tool
|
|
28
|
+
calling, skills, sub-agents, context compaction, and persistence.
|
|
30
29
|
email:
|
|
31
30
|
- sam@samcouch.com
|
|
32
31
|
executables: []
|
|
@@ -46,6 +45,7 @@ files:
|
|
|
46
45
|
- lib/turnkit/conversation.rb
|
|
47
46
|
- lib/turnkit/cost.rb
|
|
48
47
|
- lib/turnkit/error.rb
|
|
48
|
+
- lib/turnkit/event.rb
|
|
49
49
|
- lib/turnkit/generators/turnkit/install/templates/conversation.rb
|
|
50
50
|
- lib/turnkit/generators/turnkit/install/templates/create_turnkit_tables.rb
|
|
51
51
|
- lib/turnkit/generators/turnkit/install/templates/initializer.rb
|
|
@@ -57,6 +57,7 @@ files:
|
|
|
57
57
|
- lib/turnkit/memory_store.rb
|
|
58
58
|
- lib/turnkit/message.rb
|
|
59
59
|
- lib/turnkit/message_projection.rb
|
|
60
|
+
- lib/turnkit/model_request.rb
|
|
60
61
|
- lib/turnkit/prompt_context.rb
|
|
61
62
|
- lib/turnkit/prompt_contribution.rb
|
|
62
63
|
- lib/turnkit/prompt_data.rb
|
|
@@ -102,5 +103,5 @@ requirements: []
|
|
|
102
103
|
rubygems_version: 3.5.22
|
|
103
104
|
signing_key:
|
|
104
105
|
specification_version: 4
|
|
105
|
-
summary:
|
|
106
|
+
summary: Ruby/Rails agent runtime for durable AI conversations.
|
|
106
107
|
test_files: []
|