TurnKit

Gem Version Ruby License

Build durable Ruby and Rails agents with tools, skills, sub-agents, and persistence.

Installation

Add this line to your application's Gemfile:

gem "turnkit"

Run:

bundle install

Quick Start

Set an API key:

export ANTHROPIC_API_KEY=...

Create an agent:

require "turnkit"

agent = TurnKit::Agent.new(
  name: "helper",
  instructions: "Answer briefly."
)

Ask a question:

turn = agent.conversation.ask("Explain Ruby blocks in one sentence.")
puts turn.output_text

Usage

Models

Set a model:

TurnKit.default_model = "gpt-4.1-mini"

Set the matching key:

export OPENAI_API_KEY=...

Use these common providers:

Provider Key Model
Anthropic ANTHROPIC_API_KEY claude-sonnet-4-5
OpenAI OPENAI_API_KEY gpt-4.1-mini
Gemini GEMINI_API_KEY gemini-2.5-flash
OpenRouter OPENROUTER_API_KEY openrouter/...

Expect TurnKit::ModelAccessError for obvious key mistakes.

Conversations

Create a conversation:

agent = TurnKit::Agent.new(
  name: "writer",
  instructions: "Write clear release notes."
)

conversation = agent.conversation(subject: "v1 launch")

Add context:

conversation.say("Mention faster tool execution.")

Run the agent:

turn = conversation.run!
puts turn.output_text

Prompt Preview

Preview a pending turn:

turn = conversation.ask("Draft the launch email.", async: true)
request = turn.preview

Inspect the request:

request.model
request.messages
request.tool_names
request.instructions
request.report

Run the reviewed turn:

turn.run!

Tools

Create a tool:

class SaveReport < TurnKit::Tool
  description "Save a report."
  usage_hint "Use when the user asks to persist a report."

  parameter :title, :string, required: true
  parameter :body, :string, required: true

  def self.ends_turn? = true

  def self.completion_message(result)
    "Saved #{result.fetch("report_id")}."
  end

  def call(title:, body:, context:)
    { report_id: "rep_1", title: title, body: body }
  end
end

Register the tool:

agent = TurnKit::Agent.new(
  name: "reporter",
  instructions: "Save reports when asked.",
  tools: [SaveReport]
)

Run the tool loop:

turn = agent.conversation.ask("Save a short status report.")
puts turn.output_text

Rely on TurnKit to validate tools and model-provided arguments.

Structured Output

Define a schema:

schema = {
  type: "object",
  properties: {
    title: { type: "string" },
    bullets: {
      type: "array",
      items: { type: "string" }
    }
  },
  required: ["title", "bullets"]
}

Use structured output:

agent = TurnKit::Agent.new(
  name: "writer",
  output_schema: schema
)

turn = agent.conversation.ask("Summarize the launch plan.")
puts turn.output_data

Override the schema per turn:

conversation.ask(
  "Return one decision.",
  output_schema: {
    type: "object",
    properties: {
      decision: { type: "string" }
    }
  }
)

Events

Subscribe globally:

TurnKit.on_event = ->(event) do
  Rails.logger.info("turnkit.#{event.type}")
end

Subscribe per agent:

agent = TurnKit::Agent.new(
  name: "helper",
  on_event: ->(event) { puts event.type }
)

Subscribe per turn:

turn.run! do |event|
  puts event.type
end

Use events for turns, model calls, messages, and tool calls.

Skills

Load a skill:

skill = TurnKit::Skill.from_file("skills/research.md")

Use the skill:

agent = TurnKit::Agent.new(
  name: "researcher",
  skills: [skill]
)

Sub-agents

Create a sub-agent:

writer = TurnKit::Agent.new(
  name: "writer",
  description: "Draft concise copy."
)

Register the sub-agent:

editor = TurnKit::Agent.new(
  name: "editor",
  sub_agents: [writer]
)

Ask the parent agent:

turn = editor.conversation.ask("Ask the writer for three headlines.")
puts turn.output_text

Use sub-agents for isolated child conversations.

Context Compaction

Disable compaction:

TurnKit.compaction = false

Configure compaction:

TurnKit.compaction = {
  model: "gpt-4.1-mini",
  threshold: 0.75,
  context_limit: 128_000
}

Compact manually:

conversation.compact!(focus: "billing migration")

Run the local smoke test:

ruby script/manual_compaction.rb

Rails

Install Rails persistence:

bin/rails generate turnkit:install

Run migrations:

bin/rails db:migrate

Use this layout:

app/ai/agents/
app/ai/tools/
app/ai/skills/

Reconcile stale turns:

TurnKit.reconcile_stale!

Options

Option Description
TurnKit.default_model Set the default model.
TurnKit.client Set the model client.
TurnKit.store Set the persistence store.
TurnKit.max_iterations Limit model loop iterations.
TurnKit.max_depth Limit sub-agent depth.
TurnKit.max_tool_executions Limit tool calls per turn.
TurnKit.timeout Limit turn runtime.
TurnKit.cost_limit Limit estimated turn cost.
TurnKit.compaction Configure context compaction.
TurnKit.on_event Subscribe to lifecycle events.

Set options globally:

TurnKit.default_model = "gpt-4.1-mini"
TurnKit.max_iterations = 25
TurnKit.timeout = 300

Set options per agent:

agent = TurnKit::Agent.new(
  name: "engineer",
  model: "gpt-4.1-mini",
  max_iterations: 10,
  max_depth: 2
)

Enable thinking:

agent = TurnKit::Agent.new(
  name: "reasoner",
  model: "claude-sonnet-4-5",
  thinking: { budget: 4_000 }
)

Upgrading

Add output_data for structured output persistence.

add_column :turnkit_turns, :output_data, :json

Skip this step for new installs.

Contributing

Fork the project.

Run tests:

bundle exec rake test

Run syntax checks:

find lib test examples -type f -name '*.rb' -print0 | xargs -0 ruby -c

Open a pull request.

License

Use this gem under the MIT License.