Custom Agents

Agent Definitions (File Convention)

The easiest way to create a reusable agent is with Ask::Agent::Definition. Create an agent.rb file in agents/<name>/ or app/agents/<name>/:

# app/agents/health_check/agent.rb
class HealthCheckAgent < Ask::Agent::Definition
  model "gpt-4o"
  tools :bash, :read, :grep
  schedule "every 5 minutes"
end

The directory name becomes the agent name. A sibling instructions.md is auto-loaded as the system prompt:

# app/agents/health_check/instructions.md

You are a health check agent. Check server status and report issues.

DSL Options

Method Description Example
model LLM model identifier model "gpt-4o"
provider Provider override provider :opencode_go
max_turns Max conversation turns max_turns 30
parallel_tools Parallel tool execution parallel_tools false
tools Tool symbols or classes tools :bash, :read
schedule Cron/interval schedule schedule "every 5 minutes"
option Arbitrary Session option option :temperature, 0.7

Using from Ruby

agent = Ask::Agent.new("health_check")
agent.run("Check server health")

Ask::Agent.new discovers the definition by scanning agents/ and app/agents/, loads the instructions file, resolves tools, and creates a configured Session.

Tools in Agent Definitions

Tools can live in the agent’s own tools/ directory:

app/agents/health_check/
├── agent.rb
├── instructions.md
└── tools/
    └── check_disk.rb

Reference them by symbol in the definition:

class HealthCheckAgent < Ask::Agent::Definition
  tools :check_disk
end

Tools in app/agents/<name>/tools/ are auto-discovered by the agent system. Tools defined in app/tools/ (loaded by Rails’ autoloader) can also be used by referencing the tool class directly or registering with Ask::Tools.register.

Extending Session

When to Extend

Scenario Approach
Add behavior before/after tools Use Hooks
Add behavior before/after LLM calls Use Hooks
Custom event handling Use Events
Custom context management Custom Compactor
Custom storage Custom persistence adapter

Custom Hooks

Hooks let you run code at specific points in the agent lifecycle:

class LoggingHooks < Ask::Agent::Hooks
  def before_tool(name, args)
    Rails.logger.info("[Agent] Running #{name} with #{args}")
  end

  def after_tool(name, result, duration_ms)
    Rails.logger.info("[Agent] #{name} finished in #{duration_ms}ms: #{result.status}")
  end

  def before_llm(messages, model)
    Rails.logger.info("[Agent] LLM call to #{model} with #{messages.size} messages")
  end

  def after_llm(response, model, tokens_used)
    Rails.logger.info("[Agent] LLM response: #{tokens_used} tokens")
  end
end

session = Ask::Agent::Session.new(
  model: "gpt-4o",
  hooks: LoggingHooks.new
)

Custom Events

Emit and subscribe to custom events:

class MetricsEvent < Ask::Agent::Events::Base
  attribute :metric_name
  attribute :value
end

# Emit in a hook
class MetricsHooks < Ask::Agent::Hooks
  def after_tool(name, result, duration_ms)
    session.emit(MetricsEvent.new(
      metric_name: "tool.#{name}.duration",
      value: duration_ms
    ))
  end
end

# Subscribe
session.on_event do |event|
  case event
  when MetricsEvent
    StatsD.gauge(event.metric_name, event.value)
  end
end

Custom Compaction

Manage context window proactively:

class AggressiveCompactor < Ask::Agent::Compactor
  def compact(conversation)
    # Keep only the system prompt and the last 3 turns
    system_msg = conversation.messages.find { |m| m.role == :system }
    recent = conversation.messages.last(6) # 3 turns × 2 messages

    Ask::Conversation.new.tap do |c|
      c.system(system_msg.content) if system_msg
      recent.each { |msg| c.add(msg) }
    end
  end
end

session = Ask::Agent::Session.new(
  model: "gpt-4o",
  compactor: AggressiveCompactor.new
)

Custom State Adapter

Any object responding to set(key, value), get(key), and delete(key) works as a state adapter:

class RedisStateAdapter
  def set(key, value, ttl: nil)
    $redis.set(key, JSON.generate(value))
    $redis.expire(key, ttl) if ttl
  end

  def get(key)
    data = $redis.get(key)
    data ? JSON.parse(data) : nil
  end

  def delete(key)
    $redis.del(key)
  end
end

session = Ask::Agent::Session.new(
  model: "gpt-4o",
  state: RedisStateAdapter.new
)

# Later, resume the session:
restored = Ask::Agent::Session.load(session.id, adapter: RedisStateAdapter.new)

Custom Agent Subclass

For the most control, subclass Ask::Agent::Session:

class SupportAgent < Ask::Agent::Session
  def initialize(customer:, **options)
    @customer = customer
    super(**options)
    add_system_context(customer_info)
    register_tools(SupportTools.all)
  end

  def run(query)
    Rails.logger.info("[Support] #{@customer.email}: #{query}")
    start_time = Time.now
    response = super
    duration = Time.now - start_time
    Rails.logger.info("[Support] Resolved in #{duration}s")
    response
  end

  private

  def customer_info
    <<~PROMPT
      You are a support agent for Acme Corp.
      Customer: #{@customer.name} (#{@customer.plan} plan)
      Use the knowledge base and order system to help them.
    PROMPT
  end
end

Full Example

class AuditedAgent < Ask::Agent::Session
  def initialize(**options)
    super(**options)

    # Add auditing hook
    add_hook(AuditHook.new)

    # Track all events
    on_event { |e| audit_log(e) }
  end

  private

  def audit_log(event)
    AuditEntry.create!(
      session_id: id,
      event_type: event.class.name,
      data: event.to_h,
      timestamp: Time.current
    )
  end
end

class AuditHook < Ask::Agent::Hooks
  def after_tool(name, result, duration_ms)
    AuditEntry.create!(
      event_type: "tool_execution",
      data: { tool: name, duration: duration_ms, status: result.status }
    )
  end
end

Next Steps


This site uses Just the Docs, a documentation theme for Jekyll.