turnkit 0.2.6 → 0.2.8

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.
data/UPGRADE.md ADDED
@@ -0,0 +1,346 @@
1
+ # Upgrade Guide
2
+
3
+ This guide covers migrating to the newer task-runtime API. The changes are
4
+ mostly additive: existing `Agent`, `Conversation`, `Tool`, and `Fleet` code
5
+ should continue to work. The recommended migration is about improving developer
6
+ experience and making autonomous workflows easier to read.
7
+
8
+ ## Quick summary
9
+
10
+ You do **not** need to rewrite existing code immediately.
11
+
12
+ Recommended new forms:
13
+
14
+ ```ruby
15
+ TurnKit.configure do |config|
16
+ config.model = "gpt-5.2"
17
+ config.max_spend = 0.25
18
+ end
19
+
20
+ fleet = TurnKit.fleet("brief_writer", tools: [WebSearch, SaveBrief])
21
+ run = fleet.run("Create a source-grounded brief.", input: { topic: "Rails 8" })
22
+
23
+ puts run.output
24
+ ```
25
+
26
+ Old forms still work:
27
+
28
+ ```ruby
29
+ TurnKit.default_model = "gpt-5.2"
30
+
31
+ fleet = TurnKit::Fleet.new(name: "brief_writer", tools: [WebSearch, SaveBrief])
32
+ run = fleet.run(task: "Create a source-grounded brief.", input: { topic: "Rails 8" })
33
+
34
+ puts run.output_text
35
+ ```
36
+
37
+ ## Configuration
38
+
39
+ ### Model name
40
+
41
+ Before:
42
+
43
+ ```ruby
44
+ TurnKit.default_model = "gpt-5.2"
45
+ ```
46
+
47
+ After:
48
+
49
+ ```ruby
50
+ TurnKit.model = "gpt-5.2"
51
+ ```
52
+
53
+ `TurnKit.default_model` remains supported. `TurnKit.model` is the shorter public
54
+ alias for app code and initializers.
55
+
56
+ ### Global setup
57
+
58
+ Before:
59
+
60
+ ```ruby
61
+ TurnKit.default_model = "gpt-5.2"
62
+ TurnKit.cost_limit = 0.25
63
+ TurnKit.max_iterations = 12
64
+ ```
65
+
66
+ After:
67
+
68
+ ```ruby
69
+ TurnKit.configure do |config|
70
+ config.model = "gpt-5.2"
71
+ config.max_spend = 0.25
72
+ config.max_iterations = 12
73
+ end
74
+ ```
75
+
76
+ `TurnKit.configure` simply yields the `TurnKit` module. There is no separate
77
+ configuration object or DSL.
78
+
79
+ ### Spend limit naming
80
+
81
+ Before:
82
+
83
+ ```ruby
84
+ TurnKit.cost_limit = 0.25
85
+ ```
86
+
87
+ After:
88
+
89
+ ```ruby
90
+ TurnKit.max_spend = 0.25
91
+ ```
92
+
93
+ `cost_limit` remains supported. Prefer `max_spend` in application-facing code
94
+ because it matches how developers think about autonomous runs.
95
+
96
+ ## Running application tasks
97
+
98
+ ### Agent tasks
99
+
100
+ Before:
101
+
102
+ ```ruby
103
+ run = agent.run(task: "Classify this lead.", input: lead.attributes)
104
+ puts run.output_text
105
+ ```
106
+
107
+ After:
108
+
109
+ ```ruby
110
+ run = agent.run("Classify this lead.", input: lead.attributes)
111
+ puts run.output
112
+ ```
113
+
114
+ The keyword form still works. The positional string is the recommended form for
115
+ the common case.
116
+
117
+ ### Pending runs
118
+
119
+ No behavior change.
120
+
121
+ ```ruby
122
+ run = agent.run("Classify later.", async: true)
123
+ request = run.preview
124
+ run.run!
125
+ ```
126
+
127
+ The existing keyword form remains valid:
128
+
129
+ ```ruby
130
+ run = agent.run(task: "Classify later.", async: true)
131
+ ```
132
+
133
+ ## Fleets
134
+
135
+ The fleet mental model changed from “many agents” to “one reusable autonomous
136
+ task runtime.” A fleet packages:
137
+
138
+ - one task-mode orchestrator
139
+ - workflow skills
140
+ - tools
141
+ - guardrails
142
+ - compaction
143
+ - optional persistence/action tools
144
+
145
+ ### Construction
146
+
147
+ Before:
148
+
149
+ ```ruby
150
+ fleet = TurnKit::Fleet.new(
151
+ name: "sales_enrichment",
152
+ tools: [AccountLookup, WebSearch, SaveEnrichment],
153
+ skills: [sales_research_skill],
154
+ max_spend: 0.25
155
+ )
156
+ ```
157
+
158
+ After:
159
+
160
+ ```ruby
161
+ fleet = TurnKit.fleet(
162
+ "sales_enrichment",
163
+ tools: [AccountLookup, WebSearch, SaveEnrichment],
164
+ skills: [sales_research_skill],
165
+ max_spend: 0.25
166
+ )
167
+ ```
168
+
169
+ `TurnKit::Fleet.new` remains supported.
170
+
171
+ ### Running
172
+
173
+ Before:
174
+
175
+ ```ruby
176
+ run = fleet.run(
177
+ task: "Enrich this account for responsible outreach.",
178
+ input: account.attributes
179
+ )
180
+ ```
181
+
182
+ After:
183
+
184
+ ```ruby
185
+ run = fleet.run(
186
+ "Enrich this account for responsible outreach.",
187
+ input: account.attributes
188
+ )
189
+ ```
190
+
191
+ `task:` remains supported.
192
+
193
+ ### Auto-run alias
194
+
195
+ No behavior change.
196
+
197
+ ```ruby
198
+ run = fleet.auto_run("Enrich this account.", input: account.attributes)
199
+ ```
200
+
201
+ Use `auto_run` when the name helps communicate that the fleet should navigate
202
+ from input to output on its own. It is an alias for `run`.
203
+
204
+ ## Run inspection
205
+
206
+ New convenience methods were added to `TurnKit::Run`.
207
+
208
+ Before:
209
+
210
+ ```ruby
211
+ run.output_text
212
+ run.tool_executions
213
+ run.turn_records.length
214
+ TurnKit.store.load_turn(run.id)["error"]
215
+ ```
216
+
217
+ After:
218
+
219
+ ```ruby
220
+ run.output
221
+ run.tool_calls
222
+ run.steps
223
+ run.error
224
+ ```
225
+
226
+ Old methods remain available. Prefer the shorter methods in application code,
227
+ examples, and docs.
228
+
229
+ ## Save/action tools
230
+
231
+ Use `terminal!` for tools that complete the run by saving an artifact or taking
232
+ the final action.
233
+
234
+ Before:
235
+
236
+ ```ruby
237
+ class SaveBrief < TurnKit::Tool
238
+ def self.ends_turn? = true
239
+ def self.completion_message(result) = "Saved #{result.fetch("id")}."
240
+
241
+ def call(title:, body:, context:)
242
+ { "id" => Brief.create!(title: title, body: body).id }
243
+ end
244
+ end
245
+ ```
246
+
247
+ After:
248
+
249
+ ```ruby
250
+ class SaveBrief < TurnKit::Tool
251
+ terminal! { |result| "Saved #{result.fetch("id")}." }
252
+
253
+ def call(title:, body:, context:)
254
+ { "id" => Brief.create!(title: title, body: body).id }
255
+ end
256
+ end
257
+ ```
258
+
259
+ The old `ends_turn?` and `completion_message` methods remain supported. Prefer
260
+ `terminal!` for readability.
261
+
262
+ ## Tool instances
263
+
264
+ If a tool needs constructor arguments, register an instance instead of a class.
265
+
266
+ Before, this may have failed at runtime:
267
+
268
+ ```ruby
269
+ class WebSearch < TurnKit::Tool
270
+ def initialize(client:)
271
+ @client = client
272
+ end
273
+ end
274
+
275
+ agent = TurnKit::Agent.new(tools: [WebSearch])
276
+ ```
277
+
278
+ After:
279
+
280
+ ```ruby
281
+ client = SearchClient.new(api_key: ENV.fetch("SEARCH_API_KEY"))
282
+ agent = TurnKit::Agent.new(tools: [WebSearch.new(client: client)])
283
+ ```
284
+
285
+ This is the recommended pattern for API clients, test doubles, and per-tenant
286
+ dependencies.
287
+
288
+ ## Multi-agent fleets
289
+
290
+ If you previously modeled every role as a separate agent, consider migrating the
291
+ default path to one fleet with a workflow skill.
292
+
293
+ Before:
294
+
295
+ ```ruby
296
+ researcher = TurnKit::Agent.new(name: "researcher", tools: [WebSearch])
297
+ writer = TurnKit::Agent.new(name: "writer")
298
+ verifier = TurnKit::Agent.new(name: "verifier")
299
+
300
+ orchestrator = TurnKit::Agent.new(
301
+ name: "orchestrator",
302
+ sub_agents: [researcher, writer, verifier]
303
+ )
304
+ ```
305
+
306
+ After:
307
+
308
+ ```ruby
309
+ workflow = TurnKit::Skill.new(
310
+ key: "source_grounded_brief",
311
+ name: "Source Grounded Brief",
312
+ content: <<~TEXT
313
+ Research first. Build an evidence pack. Draft only from evidence. Verify
314
+ important claims. Revise unsupported claims before final output.
315
+ TEXT
316
+ )
317
+
318
+ fleet = TurnKit.fleet(
319
+ "source_brief",
320
+ skills: [workflow],
321
+ tools: [WebSearch, ReadWebPage, SaveBrief],
322
+ max_spend: 0.25,
323
+ max_tool_executions: 20
324
+ )
325
+ ```
326
+
327
+ Keep separate agents when the isolation is worth the extra model calls:
328
+
329
+ - different models
330
+ - different tool permissions
331
+ - adversarial review
332
+ - parallel specialist research
333
+ - separate durable child conversations
334
+
335
+ ## Suggested migration order
336
+
337
+ 1. Replace `TurnKit.default_model =` with `TurnKit.model =` in app-level config.
338
+ 2. Wrap global settings in `TurnKit.configure` if you have more than one.
339
+ 3. Replace `TurnKit::Fleet.new(name: ...)` with `TurnKit.fleet("...")` in new code.
340
+ 4. Replace `run(task: "...")` with `run("...")` where it improves readability.
341
+ 5. Replace `run.output_text` with `run.output` in application code.
342
+ 6. Replace save/action tool overrides with `terminal!` when convenient.
343
+ 7. Consider collapsing role-agent fleets into one fleet plus workflow skills if
344
+ cost or complexity is a concern.
345
+
346
+ None of these steps are required for existing code to keep working.
@@ -3,7 +3,28 @@
3
3
  module TurnKit
4
4
  module Adapters
5
5
  class RubyLLM < Client
6
- def chat(model:, messages:, tools:, instructions:, temperature: nil, thinking: nil, metadata: nil)
6
+ KEY_BY_PROVIDER = {
7
+ openai: "OPENAI_API_KEY",
8
+ gemini: "GEMINI_API_KEY",
9
+ anthropic: "ANTHROPIC_API_KEY",
10
+ openrouter: "OPENROUTER_API_KEY"
11
+ }.freeze
12
+
13
+ def validate!(model:)
14
+ require "ruby_llm"
15
+
16
+ raise ModelAccessError, "model is required" if model.to_s.empty?
17
+
18
+ configure_from_environment
19
+ provider = provider_for(model)
20
+ key_name = KEY_BY_PROVIDER[provider]
21
+ return true unless key_name
22
+ return true if ENV[key_name].to_s != "" || config_key_present?(provider)
23
+
24
+ raise ModelAccessError, "#{key_name} is required for #{model}. Set ENV[#{key_name.inspect}] or configure RubyLLM before running TurnKit."
25
+ end
26
+
27
+ def chat(model:, messages:, tools:, instructions:, temperature: nil, thinking: nil, output_schema: nil, metadata: nil, on_event: nil)
7
28
  require "ruby_llm"
8
29
 
9
30
  configure_from_environment
@@ -12,6 +33,7 @@ module TurnKit
12
33
  add_instructions(chat, instructions, model: model)
13
34
  chat.with_temperature(temperature) if temperature
14
35
  apply_thinking(chat, thinking)
36
+ chat.with_schema(normalize_schema(output_schema)) if output_schema
15
37
  Array(tools).each { |tool| chat.with_tool(ruby_llm_tool(tool)) }
16
38
  Array(messages).each { |message| add_message(chat, message) }
17
39
 
@@ -28,11 +50,39 @@ module TurnKit
28
50
  config.openrouter_api_key ||= ENV["OPENROUTER_API_KEY"]
29
51
  end
30
52
 
53
+ def provider_for(model)
54
+ value = model.to_s.downcase
55
+ return :openrouter if value.start_with?("openrouter/")
56
+ return :anthropic if value.start_with?("anthropic/", "claude")
57
+ return :gemini if value.start_with?("gemini/", "gemini")
58
+ return :openai if value.start_with?("openai/", "gpt", "o1", "o3", "o4")
59
+
60
+ nil
61
+ end
62
+
63
+ def config_key_present?(provider)
64
+ value = ::RubyLLM.config.public_send("#{provider}_api_key") if ::RubyLLM.config.respond_to?("#{provider}_api_key")
65
+ value.to_s != ""
66
+ end
67
+
31
68
  def apply_thinking(chat, thinking)
32
69
  thinking = Agent.normalize_thinking(thinking)
33
70
  chat.with_thinking(**thinking) if thinking
34
71
  end
35
72
 
73
+ def normalize_schema(schema)
74
+ case schema
75
+ when Hash
76
+ normalized = schema.transform_keys(&:to_s).transform_values { |value| normalize_schema(value) }
77
+ normalized["additionalProperties"] = false if normalized["type"] == "object" && !normalized.key?("additionalProperties")
78
+ normalized
79
+ when Array
80
+ schema.map { |value| normalize_schema(value) }
81
+ else
82
+ schema
83
+ end
84
+ end
85
+
36
86
  def complete_without_tool_execution(chat)
37
87
  provider = chat.instance_variable_get(:@provider)
38
88
  provider.complete(
@@ -110,9 +160,7 @@ module TurnKit
110
160
  Class.new(::RubyLLM::Tool) do
111
161
  define_singleton_method(:name) { tool.tool_name }
112
162
  description tool.description
113
- tool.parameters.each do |param|
114
- param(param.fetch(:name).to_sym, type: param.fetch(:type), required: param.fetch(:required), desc: param.fetch(:description))
115
- end
163
+ params tool.input_schema
116
164
 
117
165
  define_method(:execute) do |**arguments|
118
166
  raise ToolError, "tools must be executed by TurnKit turns, not the RubyLLM adapter"
@@ -133,13 +181,29 @@ module TurnKit
133
181
  cost: response_cost(response)
134
182
  )
135
183
  Result.new(
136
- text: response.respond_to?(:content) ? response.content.to_s : response.to_s,
184
+ text: response_text(response),
185
+ output_data: response_data(response),
137
186
  tool_calls: tool_calls,
138
187
  usage: usage,
139
188
  model: response.respond_to?(:model_id) ? response.model_id : model
140
189
  )
141
190
  end
142
191
 
192
+ def response_text(response)
193
+ content = response.respond_to?(:content) ? response.content : response
194
+ content.is_a?(Hash) || content.is_a?(Array) ? content.to_json : content.to_s
195
+ end
196
+
197
+ def response_data(response)
198
+ content = response.respond_to?(:content) ? response.content : nil
199
+ return content if content.is_a?(Hash) || content.is_a?(Array)
200
+ return nil unless content.is_a?(String)
201
+
202
+ JSON.parse(content)
203
+ rescue JSON::ParserError
204
+ nil
205
+ end
206
+
143
207
  def token_value(response, method)
144
208
  response.respond_to?(method) ? response.public_send(method).to_i : 0
145
209
  end
data/lib/turnkit/agent.rb CHANGED
@@ -4,11 +4,12 @@ module TurnKit
4
4
  class Agent
5
5
  attr_reader :name, :description, :model, :instructions, :tools, :skills, :available_skills, :sub_agents
6
6
  attr_reader :client, :store, :max_iterations, :timeout, :cost_limit, :max_depth, :max_tool_executions
7
- attr_reader :prompt_sections, :system_prompt, :prompt_mode, :thinking, :compaction
7
+ attr_reader :prompt_sections, :system_prompt, :prompt_mode, :thinking, :compaction, :output_schema, :on_event
8
8
 
9
9
  def initialize(name:, description: "", model: nil, instructions: "", tools: [], skills: [], available_skills: [], sub_agents: [],
10
10
  system_prompt: nil, prompt_sections: nil, prompt_mode: nil, client: nil, store: nil,
11
- max_iterations: nil, timeout: nil, cost_limit: nil, max_depth: nil, max_tool_executions: nil, thinking: nil, compaction: nil)
11
+ max_iterations: nil, timeout: nil, cost_limit: nil, max_depth: nil, max_tool_executions: nil, thinking: nil, compaction: nil,
12
+ output_schema: nil, on_event: nil)
12
13
  @name = name.to_s
13
14
  @description = description.to_s
14
15
  @model = model
@@ -29,7 +30,10 @@ module TurnKit
29
30
  @max_tool_executions = max_tool_executions
30
31
  @thinking = self.class.normalize_thinking(thinking)
31
32
  @compaction = compaction
33
+ @output_schema = output_schema
34
+ @on_event = on_event
32
35
  raise ArgumentError, "name is required" if @name.empty?
36
+ validate_tools!
33
37
  end
34
38
 
35
39
  def self.normalize_thinking(value)
@@ -58,6 +62,21 @@ module TurnKit
58
62
  Conversation.new(agent: self, record: record, store: store, model: model || effective_model, subject: subject, metadata: metadata)
59
63
  end
60
64
 
65
+ def run(prompt = nil, task: nil, input: nil, async: false, subject: nil, metadata: {}, parent_run: nil, root_turn_id: nil, **options)
66
+ task = task || prompt
67
+ raise ArgumentError, "task is required" if task.to_s.empty?
68
+
69
+ conversation = self.conversation(subject: subject, metadata: metadata)
70
+ message = conversation.say(task_message(task, input), metadata: { "source" => "application", "task" => true })
71
+ turn = conversation.build_turn(
72
+ trigger_message_id: message.id,
73
+ root_turn_id: root_turn_id || parent_run_root_turn_id(parent_run),
74
+ **options
75
+ )
76
+ run = Run.new(turn)
77
+ async ? run : run.run!
78
+ end
79
+
61
80
  def cost
62
81
  Cost.from_records(effective_store.list_turns(agent_name: name))
63
82
  end
@@ -86,6 +105,10 @@ module TurnKit
86
105
  tools + sub_agents.map { |agent| SubAgentTool.for(agent) }
87
106
  end
88
107
 
108
+ def effective_on_event
109
+ on_event || TurnKit.on_event
110
+ end
111
+
89
112
  def effective_available_skills
90
113
  (Array(TurnKit.available_skills) + available_skills).uniq { |skill| skill.key }
91
114
  end
@@ -129,5 +152,47 @@ module TurnKit
129
152
  parts << SystemPrompt.loaded_skills_text(skills)
130
153
  parts.reject(&:empty?).join("\n\n")
131
154
  end
155
+
156
+ private
157
+ def validate_tools!
158
+ effective_tools.each do |tool|
159
+ next if tool.is_a?(Class) && tool < Tool
160
+ next if tool.is_a?(Tool)
161
+
162
+ raise ArgumentError, "tools must be TurnKit::Tool classes or instances"
163
+ end
164
+
165
+ names = effective_tools.map(&:tool_name)
166
+ duplicate = names.find { |name| names.count(name) > 1 }
167
+ raise ArgumentError, "duplicate tool name: #{duplicate}" if duplicate
168
+
169
+ effective_tools.each(&:validate_definition!)
170
+ end
171
+
172
+ def task_message(task, input)
173
+ text = task.to_s
174
+ return text if input.nil?
175
+
176
+ "Task:\n#{text}\n\nInput:\n#{format_task_input(input)}"
177
+ end
178
+
179
+ def format_task_input(input)
180
+ case input
181
+ when String
182
+ input
183
+ else
184
+ JSON.pretty_generate(input)
185
+ end
186
+ rescue JSON::GeneratorError
187
+ input.inspect
188
+ end
189
+
190
+ def parent_run_root_turn_id(parent_run)
191
+ return nil unless parent_run
192
+ return parent_run.root_turn_id if parent_run.respond_to?(:root_turn_id)
193
+ return parent_run.fetch("root_turn_id") if parent_run.respond_to?(:fetch)
194
+
195
+ nil
196
+ end
132
197
  end
133
198
  end
@@ -2,7 +2,11 @@
2
2
 
3
3
  module TurnKit
4
4
  class Client
5
- def chat(model:, messages:, tools:, instructions:, temperature: nil, thinking: nil, metadata: nil)
5
+ def validate!(model:)
6
+ true
7
+ end
8
+
9
+ def chat(model:, messages:, tools:, instructions:, temperature: nil, thinking: nil, output_schema: nil, metadata: nil, on_event: nil)
6
10
  raise NotImplementedError
7
11
  end
8
12
  end
@@ -26,28 +26,29 @@ module TurnKit
26
26
  async ? turn : turn.run!
27
27
  end
28
28
 
29
- def run!(trigger_message_id: nil, model: nil, budget: nil, parent_turn: nil, parent_tool_execution: nil, depth: 0, agent: self.agent, thinking: THINKING_UNSET, compact: nil)
30
- build_turn(trigger_message_id: trigger_message_id, model: model, budget: budget, parent_turn: parent_turn, parent_tool_execution: parent_tool_execution, depth: depth, agent: agent, thinking: thinking, compact: compact).run!
29
+ def run!(trigger_message_id: nil, model: nil, budget: nil, parent_turn: nil, parent_tool_execution: nil, root_turn_id: nil, depth: 0, agent: self.agent, thinking: THINKING_UNSET, compact: nil, output_schema: nil, on_event: nil)
30
+ build_turn(trigger_message_id: trigger_message_id, model: model, budget: budget, parent_turn: parent_turn, parent_tool_execution: parent_tool_execution, root_turn_id: root_turn_id, depth: depth, agent: agent, thinking: thinking, compact: compact, output_schema: output_schema, on_event: on_event).run!
31
31
  end
32
32
 
33
- def build_turn(trigger_message_id: nil, model: nil, budget: nil, parent_turn: nil, parent_tool_execution: nil, depth: 0, agent: self.agent, thinking: THINKING_UNSET, compact: nil)
33
+ def build_turn(trigger_message_id: nil, model: nil, budget: nil, parent_turn: nil, parent_tool_execution: nil, root_turn_id: nil, depth: 0, agent: self.agent, thinking: THINKING_UNSET, compact: nil, output_schema: nil, on_event: nil)
34
34
  snapshot = latest_message_sequence
35
35
  effective_thinking = thinking.equal?(THINKING_UNSET) ? agent.effective_thinking : Agent.normalize_thinking(thinking)
36
36
  options = { "trigger_message_id" => trigger_message_id }.compact
37
37
  options["thinking"] = effective_thinking
38
38
  options["compact"] = compact unless compact.nil?
39
+ options["output_schema"] = output_schema || agent.output_schema if output_schema || agent.output_schema
39
40
  record = store.create_turn(
40
41
  "conversation_id" => id,
41
42
  "agent_name" => agent.name,
42
43
  "parent_turn_id" => parent_turn&.id,
43
44
  "parent_tool_execution_id" => parent_tool_execution&.id,
44
- "root_turn_id" => parent_turn&.root_turn_id,
45
+ "root_turn_id" => parent_turn&.root_turn_id || root_turn_id,
45
46
  "context_message_sequence" => snapshot,
46
47
  "status" => "pending",
47
48
  "model" => model || self.model || agent.effective_model,
48
49
  "options" => options
49
50
  )
50
- Turn.new(agent: agent, conversation: self, record: record, store: store, budget: budget, depth: depth)
51
+ Turn.new(agent: agent, conversation: self, record: record, store: store, budget: budget, depth: depth, on_event: on_event)
51
52
  end
52
53
 
53
54
  def compact!(focus: nil, model: nil)
data/lib/turnkit/error.rb CHANGED
@@ -4,6 +4,8 @@ module TurnKit
4
4
  class Error < StandardError; end
5
5
  class ConfigError < Error; end
6
6
  class CompactionError < Error; end
7
+ class ModelAccessError < ConfigError; end
7
8
  class StoreError < Error; end
8
9
  class ToolError < Error; end
10
+ class ToolValidationError < ToolError; end
9
11
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurnKit
4
+ class Event
5
+ attr_reader :type, :turn_id, :conversation_id, :payload, :created_at
6
+
7
+ def initialize(type:, turn_id:, conversation_id:, payload: {}, created_at: Clock.now)
8
+ @type = type.to_s
9
+ @turn_id = turn_id
10
+ @conversation_id = conversation_id
11
+ @payload = payload || {}
12
+ @created_at = created_at
13
+ end
14
+
15
+ def to_h
16
+ {
17
+ "type" => type,
18
+ "turn_id" => turn_id,
19
+ "conversation_id" => conversation_id,
20
+ "payload" => payload,
21
+ "created_at" => created_at
22
+ }
23
+ end
24
+ end
25
+ end