turnkit 0.1.0 → 0.2.1
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 +6 -0
- data/README.md +94 -14
- data/lib/turnkit/agent.rb +29 -5
- data/lib/turnkit/generators/turnkit/install/templates/initializer.rb +5 -0
- data/lib/turnkit/generators/turnkit/install_generator.rb +11 -1
- data/lib/turnkit/skill.rb +4 -0
- data/lib/turnkit/system_prompt.rb +158 -0
- data/lib/turnkit/turn.rb +6 -1
- data/lib/turnkit/version.rb +1 -1
- data/lib/turnkit.rb +5 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5eaf1e8d25237f97b607cf5a1c80fced59e8e737cd4cdbf3a9d6dca61227b19b
|
|
4
|
+
data.tar.gz: da3cc66e95e3ce0ec5df6fbc8b051fc0f6750a0c2175d0cdc09f8eb4dc4ef96e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9c50e9bd40a36392496a88f202bb5dbda51883c4ae686d8089849c6cb2fef903102efcb2a868617cca80891860849b65140888bc3bf250f620ac8bf9ea61a8e8
|
|
7
|
+
data.tar.gz: a054d0de8955d20f4d9ac60351507f56e98bf8137828d599ac5681ff593b5039f4d672dc16323f30a700a4f25aef3360fb6f97c64d75ce08f10eedf3fde692c0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0 - 2026-06-04
|
|
4
|
+
|
|
5
|
+
- Add configurable system prompt sections and custom system prompt builders.
|
|
6
|
+
- Add globally and per-agent available skills for prompt guidance.
|
|
7
|
+
- Add skill loading from directories.
|
|
8
|
+
|
|
3
9
|
## 0.1.0 - 2026-06-04
|
|
4
10
|
|
|
5
11
|
- Initial release of TurnKit.
|
data/README.md
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# TurnKit
|
|
2
2
|
|
|
3
3
|
[](https://rubygems.org/gems/turnkit)
|
|
4
|
-
[](https://github.com/samuelcouch/turnkit/actions)
|
|
5
4
|
[](https://www.ruby-lang.org)
|
|
6
5
|
[](LICENSE.md)
|
|
7
6
|
|
|
8
|
-
Ruby AI
|
|
7
|
+
Build durable Ruby AI agents with turns, tools, skills, and Rails persistence.
|
|
9
8
|
|
|
10
9
|
## Installation
|
|
11
10
|
|
|
@@ -23,12 +22,38 @@ bundle install
|
|
|
23
22
|
|
|
24
23
|
## Quick Start
|
|
25
24
|
|
|
26
|
-
Set a provider key
|
|
25
|
+
Set a provider key:
|
|
27
26
|
|
|
28
27
|
```sh
|
|
29
28
|
export ANTHROPIC_API_KEY=...
|
|
29
|
+
# or OPENAI_API_KEY=..., GEMINI_API_KEY=..., OPENROUTER_API_KEY=...
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
TurnKit uses RubyLLM by default. Choose the provider by choosing a RubyLLM model name:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
TurnKit.default_model = "claude-sonnet-4-5" # Anthropic
|
|
36
|
+
# TurnKit.default_model = "gpt-4.1-mini" # OpenAI
|
|
37
|
+
# TurnKit.default_model = "gemini-2.5-flash" # Gemini
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
You can also override the model per agent or per run.
|
|
41
|
+
|
|
42
|
+
To use a different model SDK, provide a client object that responds to `chat`:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
class MyClient < TurnKit::Client
|
|
46
|
+
def chat(model:, messages:, tools:, instructions:, temperature: nil, metadata: nil)
|
|
47
|
+
# Call your provider here.
|
|
48
|
+
TurnKit::Result.new(text: "provider response", model: model)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
TurnKit.client = MyClient.new
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Ask an agent:
|
|
56
|
+
|
|
32
57
|
```ruby
|
|
33
58
|
require "turnkit"
|
|
34
59
|
|
|
@@ -99,6 +124,55 @@ agent = TurnKit::Agent.new(
|
|
|
99
124
|
)
|
|
100
125
|
```
|
|
101
126
|
|
|
127
|
+
List available skills:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
research = TurnKit::Skill.from_file(
|
|
131
|
+
"skills/research.md",
|
|
132
|
+
description: "Use for source-backed research tasks."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
agent = TurnKit::Agent.new(
|
|
136
|
+
name: "researcher",
|
|
137
|
+
instructions: "Prefer primary sources.",
|
|
138
|
+
tools: [WebSearch, ReadWebPage],
|
|
139
|
+
available_skills: [research]
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Add subject context:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
article = Article.find(1)
|
|
147
|
+
conversation = agent.conversation(subject: article)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Choose prompt sections:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
agent = TurnKit::Agent.new(
|
|
154
|
+
name: "writer",
|
|
155
|
+
instructions: "Write plainly.",
|
|
156
|
+
prompt_sections: %i[agent instructions tools environment]
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Build a custom prompt:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
agent = TurnKit::Agent.new(
|
|
164
|
+
name: "custom",
|
|
165
|
+
instructions: "Answer in JSON.",
|
|
166
|
+
system_prompt: ->(prompt) {
|
|
167
|
+
[
|
|
168
|
+
prompt.agent_section,
|
|
169
|
+
prompt.instructions_section,
|
|
170
|
+
"Return only valid JSON."
|
|
171
|
+
].compact.join("\n\n")
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
102
176
|
Delegate to sub-agents:
|
|
103
177
|
|
|
104
178
|
```ruby
|
|
@@ -126,7 +200,6 @@ bin/rails db:migrate
|
|
|
126
200
|
Configure Rails:
|
|
127
201
|
|
|
128
202
|
```ruby
|
|
129
|
-
# config/initializers/turnkit.rb
|
|
130
203
|
TurnKit.store = TurnKit::ActiveRecordStore.new
|
|
131
204
|
TurnKit.default_model = "claude-sonnet-4-5"
|
|
132
205
|
TurnKit.timeout = 300
|
|
@@ -140,7 +213,7 @@ TurnKit.reconcile_stale!
|
|
|
140
213
|
|
|
141
214
|
## Options
|
|
142
215
|
|
|
143
|
-
Configure defaults
|
|
216
|
+
Configure defaults:
|
|
144
217
|
|
|
145
218
|
```ruby
|
|
146
219
|
TurnKit.default_model = "claude-sonnet-4-5"
|
|
@@ -163,20 +236,27 @@ agent = TurnKit::Agent.new(
|
|
|
163
236
|
)
|
|
164
237
|
```
|
|
165
238
|
|
|
239
|
+
Override the model for a single conversation or turn:
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
conversation = agent.conversation(model: "claude-opus-4-1")
|
|
243
|
+
turn = conversation.run!(model: "gpt-4.1-mini")
|
|
244
|
+
```
|
|
245
|
+
|
|
166
246
|
| Option | Description |
|
|
167
247
|
| --- | --- |
|
|
168
|
-
| `default_model` |
|
|
169
|
-
| `client` |
|
|
170
|
-
| `store` |
|
|
171
|
-
| `max_iterations` |
|
|
172
|
-
| `timeout` |
|
|
173
|
-
| `max_depth` |
|
|
174
|
-
| `max_tool_executions` |
|
|
175
|
-
| `cost_limit` |
|
|
248
|
+
| `default_model` | Set the default RubyLLM model. The model name determines the provider. |
|
|
249
|
+
| `client` | Set the model client. Defaults to `TurnKit::Adapters::RubyLLM.new`. |
|
|
250
|
+
| `store` | Set the conversation store. |
|
|
251
|
+
| `max_iterations` | Limit model calls per turn. |
|
|
252
|
+
| `timeout` | Limit seconds per root turn. |
|
|
253
|
+
| `max_depth` | Limit sub-agent nesting. |
|
|
254
|
+
| `max_tool_executions` | Limit tool calls per root turn. |
|
|
255
|
+
| `cost_limit` | Limit cost per root turn. |
|
|
176
256
|
|
|
177
257
|
## Contributing
|
|
178
258
|
|
|
179
|
-
|
|
259
|
+
Report bugs and open pull requests on GitHub:
|
|
180
260
|
|
|
181
261
|
```text
|
|
182
262
|
https://github.com/samuelcouch/turnkit
|
data/lib/turnkit/agent.rb
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module TurnKit
|
|
4
4
|
class Agent
|
|
5
|
-
attr_reader :name, :description, :model, :instructions, :tools, :skills, :sub_agents
|
|
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
|
|
7
8
|
|
|
8
|
-
def initialize(name:, description: "", model: nil, instructions: "", tools: [], skills: [],
|
|
9
|
+
def initialize(name:, description: "", model: nil, instructions: "", tools: [], skills: [], available_skills: [], sub_agents: [],
|
|
10
|
+
system_prompt: nil, prompt_sections: nil, client: nil, store: nil,
|
|
9
11
|
max_iterations: nil, timeout: nil, cost_limit: nil, max_depth: nil, max_tool_executions: nil)
|
|
10
12
|
@name = name.to_s
|
|
11
13
|
@description = description.to_s
|
|
@@ -13,7 +15,10 @@ module TurnKit
|
|
|
13
15
|
@instructions = instructions.to_s
|
|
14
16
|
@tools = Array(tools)
|
|
15
17
|
@skills = Array(skills)
|
|
18
|
+
@available_skills = Array(available_skills)
|
|
16
19
|
@sub_agents = Array(sub_agents)
|
|
20
|
+
@system_prompt = system_prompt
|
|
21
|
+
@prompt_sections = prompt_sections
|
|
17
22
|
@client = client
|
|
18
23
|
@store = store
|
|
19
24
|
@max_iterations = max_iterations
|
|
@@ -51,6 +56,27 @@ module TurnKit
|
|
|
51
56
|
tools + sub_agents.map { |agent| SubAgentTool.for(agent) }
|
|
52
57
|
end
|
|
53
58
|
|
|
59
|
+
def effective_available_skills
|
|
60
|
+
(Array(TurnKit.available_skills) + available_skills).uniq { |skill| skill.key }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def effective_prompt_sections
|
|
64
|
+
prompt_sections || TurnKit.prompt_sections
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def system_prompt_for(turn:, conversation:)
|
|
68
|
+
prompt = SystemPrompt.new(agent: self, turn: turn, conversation: conversation)
|
|
69
|
+
|
|
70
|
+
case system_prompt
|
|
71
|
+
when nil
|
|
72
|
+
prompt.to_s
|
|
73
|
+
when String
|
|
74
|
+
system_prompt
|
|
75
|
+
else
|
|
76
|
+
system_prompt.call(prompt).to_s
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
54
80
|
def build_budget(root_started_at: Clock.now)
|
|
55
81
|
Budget.new(
|
|
56
82
|
max_iterations: max_iterations || TurnKit.max_iterations,
|
|
@@ -64,9 +90,7 @@ module TurnKit
|
|
|
64
90
|
|
|
65
91
|
def instructions_with_skills
|
|
66
92
|
parts = [ instructions ]
|
|
67
|
-
|
|
68
|
-
parts << "## Skill: #{skill.name}\n\n#{skill.content}"
|
|
69
|
-
end
|
|
93
|
+
parts << SystemPrompt.loaded_skills_text(skills)
|
|
70
94
|
parts.reject(&:empty?).join("\n\n")
|
|
71
95
|
end
|
|
72
96
|
end
|
|
@@ -12,3 +12,8 @@ TurnKit.tool_execution_record_class = "Turnkit::ToolExecution"
|
|
|
12
12
|
# TurnKit.timeout = 300
|
|
13
13
|
# TurnKit.max_depth = 3
|
|
14
14
|
# TurnKit.max_tool_executions = 100
|
|
15
|
+
|
|
16
|
+
# TurnKit builds each system prompt from these sections by default.
|
|
17
|
+
# TurnKit.prompt_sections = %i[agent instructions behavior loaded_skills available_skills tools subject environment]
|
|
18
|
+
# TurnKit.prompt_behavior = "Custom behavior instructions."
|
|
19
|
+
# TurnKit.available_skills = TurnKit::Skill.from_directory(Rails.root.join("app/ai/skills"))
|
|
@@ -8,6 +8,8 @@ module TurnKit
|
|
|
8
8
|
class InstallGenerator < Rails::Generators::Base
|
|
9
9
|
include Rails::Generators::Migration
|
|
10
10
|
|
|
11
|
+
namespace "turnkit:install"
|
|
12
|
+
|
|
11
13
|
source_root File.expand_path("install/templates", __dir__)
|
|
12
14
|
|
|
13
15
|
class_option :table_prefix, type: :string, default: "turnkit", desc: "Database table prefix."
|
|
@@ -28,13 +30,21 @@ module TurnKit
|
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
def self.next_migration_number(dirname)
|
|
31
|
-
if
|
|
33
|
+
if timestamped_migrations?
|
|
32
34
|
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
33
35
|
else
|
|
34
36
|
"%.3d" % (current_migration_number(dirname) + 1)
|
|
35
37
|
end
|
|
36
38
|
end
|
|
37
39
|
|
|
40
|
+
def self.timestamped_migrations?
|
|
41
|
+
if ActiveRecord.respond_to?(:timestamped_migrations)
|
|
42
|
+
ActiveRecord.timestamped_migrations
|
|
43
|
+
else
|
|
44
|
+
ActiveRecord::Base.timestamped_migrations
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
38
48
|
private
|
|
39
49
|
def table_prefix
|
|
40
50
|
options[:table_prefix]
|
data/lib/turnkit/skill.rb
CHANGED
|
@@ -10,6 +10,10 @@ module TurnKit
|
|
|
10
10
|
new(key: key || base, name: name || base.tr("_-", " ").split.map(&:capitalize).join(" "), description: description, content: content)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
def self.from_directory(path, pattern: "*.md")
|
|
14
|
+
Dir.glob(File.join(path, pattern)).sort.map { |file| from_file(file) }
|
|
15
|
+
end
|
|
16
|
+
|
|
13
17
|
def initialize(key:, name:, content:, description: "")
|
|
14
18
|
@key = key.to_s
|
|
15
19
|
@name = name.to_s
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TurnKit
|
|
4
|
+
class SystemPrompt
|
|
5
|
+
DEFAULT_SECTIONS = %i[agent instructions behavior loaded_skills available_skills tools subject environment].freeze
|
|
6
|
+
SECTION_METHODS = {
|
|
7
|
+
agent: :agent_section,
|
|
8
|
+
instructions: :instructions_section,
|
|
9
|
+
behavior: :behavior_section,
|
|
10
|
+
loaded_skills: :loaded_skills_section,
|
|
11
|
+
available_skills: :available_skills_section,
|
|
12
|
+
tools: :tools_section,
|
|
13
|
+
subject: :subject_section,
|
|
14
|
+
environment: :environment_section
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
DEFAULT_BEHAVIOR = <<~TEXT.strip
|
|
18
|
+
Treat each user message as a constraint on the current task. Follow the
|
|
19
|
+
agent instructions and loaded skills first, then use tools when they are
|
|
20
|
+
available and needed.
|
|
21
|
+
|
|
22
|
+
Use the provided environment as the source of truth for the current date
|
|
23
|
+
and time. Do not guess relative dates like "today", "tomorrow", or
|
|
24
|
+
"yesterday" when the environment gives an exact calendar anchor.
|
|
25
|
+
|
|
26
|
+
Only use tools listed in <tools_available>. If a tool you want is not
|
|
27
|
+
listed, it is unavailable for this turn; adjust your answer instead of
|
|
28
|
+
pretending to call it.
|
|
29
|
+
|
|
30
|
+
If a tool returns an error, read the error and fix your inputs before
|
|
31
|
+
trying again. Do not retry the identical failing call blindly.
|
|
32
|
+
|
|
33
|
+
Report outcomes honestly. If you cannot verify something, say so or omit
|
|
34
|
+
the claim instead of inventing details.
|
|
35
|
+
TEXT
|
|
36
|
+
|
|
37
|
+
attr_reader :agent, :turn, :conversation, :sections
|
|
38
|
+
|
|
39
|
+
def initialize(agent:, turn:, conversation:, sections: nil)
|
|
40
|
+
@agent = agent
|
|
41
|
+
@turn = turn
|
|
42
|
+
@conversation = conversation
|
|
43
|
+
@sections = Array(sections || agent.effective_prompt_sections)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def to_s
|
|
47
|
+
sections.map { |section| render(section) }.compact.reject { |value| value.strip.empty? }.join("\n\n")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def render(section)
|
|
51
|
+
method = SECTION_METHODS[section.to_sym]
|
|
52
|
+
raise ArgumentError, "unknown prompt section: #{section}" unless method
|
|
53
|
+
|
|
54
|
+
public_send(method)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def agent_section
|
|
58
|
+
lines = [
|
|
59
|
+
"- Name: #{agent.name}",
|
|
60
|
+
agent.description.empty? ? nil : "- Description: #{agent.description}",
|
|
61
|
+
"- Model: #{turn.model || agent.effective_model}"
|
|
62
|
+
].compact
|
|
63
|
+
|
|
64
|
+
tagged("agent", lines.join("\n"))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def instructions_section
|
|
68
|
+
return nil if agent.instructions.empty?
|
|
69
|
+
|
|
70
|
+
tagged("instructions", agent.instructions)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def behavior_section
|
|
74
|
+
tagged("behavior", TurnKit.prompt_behavior || DEFAULT_BEHAVIOR)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def loaded_skills_section
|
|
78
|
+
return nil if agent.skills.empty?
|
|
79
|
+
|
|
80
|
+
tagged(
|
|
81
|
+
"skills_loaded",
|
|
82
|
+
self.class.loaded_skills_text(agent.skills)
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.loaded_skills_text(skills)
|
|
87
|
+
skills.map { |skill| "## Skill: #{skill.key}\n\n#{skill.content}" }.join("\n\n")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def available_skills_section
|
|
91
|
+
skills = agent.effective_available_skills
|
|
92
|
+
return nil if skills.empty?
|
|
93
|
+
|
|
94
|
+
entries = skills.map do |skill|
|
|
95
|
+
description = skill.description.empty? ? nil : " — #{skill.description}"
|
|
96
|
+
"- #{skill.key}: #{skill.name}#{description}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
tagged(
|
|
100
|
+
"skills_available",
|
|
101
|
+
"Load or follow a skill when the task matches its description.\n\n#{entries.join("\n")}"
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def tools_section
|
|
106
|
+
tools = agent.effective_tools
|
|
107
|
+
|
|
108
|
+
if tools.empty?
|
|
109
|
+
tagged("tools_available", "(none)\n\nNo tools are available for this turn.")
|
|
110
|
+
else
|
|
111
|
+
tagged("tools_available", tools.map { |tool| tool_line(tool) }.join("\n"))
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def subject_section
|
|
116
|
+
return nil unless conversation.subject&.respond_to?(:to_prompt)
|
|
117
|
+
|
|
118
|
+
value = conversation.subject.to_prompt.to_s.strip
|
|
119
|
+
return nil if value.empty?
|
|
120
|
+
|
|
121
|
+
tagged("subject_context", value)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def environment_section
|
|
125
|
+
anchor = turn.started_at || Clock.now
|
|
126
|
+
today = anchor.to_date
|
|
127
|
+
yesterday = today - 1
|
|
128
|
+
tomorrow = today + 1
|
|
129
|
+
|
|
130
|
+
tagged(
|
|
131
|
+
"environment",
|
|
132
|
+
[
|
|
133
|
+
"- Today: #{today.strftime('%A, %B %-d, %Y')} (#{today.iso8601})",
|
|
134
|
+
"- Current time: #{anchor.strftime('%-I:%M %Z')}",
|
|
135
|
+
"- Yesterday: #{yesterday.strftime('%A, %B %-d, %Y')} (#{yesterday.iso8601})",
|
|
136
|
+
"- Tomorrow: #{tomorrow.strftime('%A, %B %-d, %Y')} (#{tomorrow.iso8601})"
|
|
137
|
+
].join("\n")
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
def tagged(name, content)
|
|
143
|
+
"<#{name}>\n#{content}\n</#{name}>"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def tool_line(tool)
|
|
147
|
+
description = tool.description.empty? ? nil : ": #{tool.description}"
|
|
148
|
+
params = tool.parameters.map do |param|
|
|
149
|
+
required = param.fetch(:required) ? " required" : ""
|
|
150
|
+
enum = param[:enum] ? " enum=#{Array(param[:enum]).join('|')}" : ""
|
|
151
|
+
"#{param.fetch(:name)}(#{param.fetch(:type)}#{required}#{enum})"
|
|
152
|
+
end
|
|
153
|
+
suffix = params.empty? ? "" : " Parameters: #{params.join(', ')}."
|
|
154
|
+
terminal = tool.ends_turn? ? " Ends the turn." : ""
|
|
155
|
+
"- #{tool.tool_name}#{description}#{suffix}#{terminal}"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
data/lib/turnkit/turn.rb
CHANGED
|
@@ -7,6 +7,7 @@ module TurnKit
|
|
|
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
9
|
attr_reader :root_turn_id, :context_message_sequence, :model
|
|
10
|
+
attr_reader :started_at
|
|
10
11
|
|
|
11
12
|
def initialize(agent:, conversation:, record:, store:, budget: nil, depth: 0)
|
|
12
13
|
@agent = agent
|
|
@@ -21,6 +22,7 @@ module TurnKit
|
|
|
21
22
|
@root_turn_id = @record["root_turn_id"] || id
|
|
22
23
|
@context_message_sequence = @record["context_message_sequence"].to_i
|
|
23
24
|
@model = @record["model"] || agent.effective_model
|
|
25
|
+
@started_at = @record["started_at"]
|
|
24
26
|
@budget = budget || agent.build_budget
|
|
25
27
|
@depth = depth
|
|
26
28
|
end
|
|
@@ -37,7 +39,7 @@ module TurnKit
|
|
|
37
39
|
model: model,
|
|
38
40
|
messages: llm_messages,
|
|
39
41
|
tools: agent.effective_tools,
|
|
40
|
-
instructions: agent.
|
|
42
|
+
instructions: agent.system_prompt_for(turn: self, conversation: conversation),
|
|
41
43
|
metadata: { turn_id: id, conversation_id: conversation.id }
|
|
42
44
|
)
|
|
43
45
|
|
|
@@ -128,6 +130,9 @@ module TurnKit
|
|
|
128
130
|
|
|
129
131
|
def update!(attributes)
|
|
130
132
|
@record = store.update_turn(id, attributes)
|
|
133
|
+
@started_at = @record["started_at"]
|
|
134
|
+
@model = @record["model"] || agent.effective_model
|
|
135
|
+
@record
|
|
131
136
|
end
|
|
132
137
|
end
|
|
133
138
|
|
data/lib/turnkit/version.rb
CHANGED
data/lib/turnkit.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "securerandom"
|
|
5
5
|
require "time"
|
|
6
|
+
require "date"
|
|
6
7
|
|
|
7
8
|
require_relative "turnkit/version"
|
|
8
9
|
require_relative "turnkit/error"
|
|
@@ -16,6 +17,7 @@ require_relative "turnkit/message"
|
|
|
16
17
|
require_relative "turnkit/record"
|
|
17
18
|
require_relative "turnkit/result"
|
|
18
19
|
require_relative "turnkit/skill"
|
|
20
|
+
require_relative "turnkit/system_prompt"
|
|
19
21
|
require_relative "turnkit/store"
|
|
20
22
|
require_relative "turnkit/memory_store"
|
|
21
23
|
require_relative "turnkit/tool"
|
|
@@ -36,6 +38,7 @@ module TurnKit
|
|
|
36
38
|
attr_accessor :default_model, :client, :store, :logger
|
|
37
39
|
attr_accessor :max_iterations, :timeout, :max_depth, :max_tool_executions
|
|
38
40
|
attr_accessor :cost_limit
|
|
41
|
+
attr_accessor :prompt_sections, :prompt_behavior, :available_skills
|
|
39
42
|
attr_accessor :conversation_record_class, :turn_record_class
|
|
40
43
|
attr_accessor :message_record_class, :tool_execution_record_class
|
|
41
44
|
end
|
|
@@ -47,6 +50,8 @@ module TurnKit
|
|
|
47
50
|
self.timeout = 300
|
|
48
51
|
self.max_depth = 3
|
|
49
52
|
self.max_tool_executions = 100
|
|
53
|
+
self.prompt_sections = SystemPrompt::DEFAULT_SECTIONS.dup
|
|
54
|
+
self.available_skills = []
|
|
50
55
|
|
|
51
56
|
def self.reconcile_stale!(before: Clock.now - (timeout || 300))
|
|
52
57
|
store.find_stale_turns(before: before).each do |turn|
|
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.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sam Couch
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ruby_llm
|
|
@@ -62,6 +62,7 @@ files:
|
|
|
62
62
|
- lib/turnkit/store.rb
|
|
63
63
|
- lib/turnkit/stores/active_record_store.rb
|
|
64
64
|
- lib/turnkit/sub_agent_tool.rb
|
|
65
|
+
- lib/turnkit/system_prompt.rb
|
|
65
66
|
- lib/turnkit/tool.rb
|
|
66
67
|
- lib/turnkit/tool_call.rb
|
|
67
68
|
- lib/turnkit/tool_execution.rb
|