Data contracts

Hash and struct shapes agents must preserve when editing lib code. Changing a contract requires updating tests and this doc.

Canonical files: lib/auslib_search.rb, lib/search_query.rb, lib/search_report.rb

SearchQuery.parse(text, max_length:)

{ query: "banjo paterson", empty: false }
{ query: nil, empty: true }   # blank input

*_search.search(...)

Success:

{
  query: String,
  total: Integer,
  items: [
    { title: String, url: String, ... }  # domain-specific keys
  ]
}

Failure:

{ error: :usage }                          # empty query
{ error: :fetch, query: String|nil }       # HTTP/parse failure
{ error: :config, query: String|nil }      # missing API key (auslib only)

SearchLookup.format(result, report:, config_errors:)

result[:error] Returns
:usage report.build_usage
:fetch report.build_fetch_error(result[:query])
:config report.build_config_error if config_errors: true, else fetch error
(none) report.build_report(query:, total:, items:)

lookup(...) public API

Catalogue plugins call *Search.lookup which returns a String (IRC reply), not a raw hash.


AI plan session

Canonical files: lib/ai_plan_session.rb, lib/rabbot_meta_plan_session.rb

Pending session (pending(...))

{
  session_id: String,
  asker: String,
  question: String,           # aip only
  description: String,       # rabbot_meta only
  questions: [
    {
      id: String,
      prompt: String,
      options: [{ id: String, label: String }],
      allow_multiple: false
    }
  ],
  model: String,
  build_state: Hash          # rabbot_meta only
}

Stored in RabbotDb as JSON under plugin "aip" / "rabbot_meta", key "pending_plan" / "pending_build_plan".

AiPlanAgent::PlanResult / RabbotMetaAgent::AgentResult

# keyword struct fields
success: Boolean
text: String
session_id: String|nil
questions: Array|nil
needs_input: Boolean
input_tokens: Integer
output_tokens: Integer
duration_ms: Integer
error: String|nil
# AgentResult also: todos: Array

Questions normalized by CursorAgentQuestions.normalize.


Directchat wire

Canonical file: lib/directchat_wire.rb

Wire line format: DCHAT1 <TYPE> <json>

decode(line) → Hash or nil

:type Keys
:session :session → session hash (symbol keys)
:relay :session_id, :from, :text
:stop :session_id
:hello :bot

Session hash (DirectchatSessionCodec)

{
  id: String,
  channel: String,           # e.g. "#channel"
  requester: String,         # nick
  assignments: [
    { bot: String, participant: String, invited_at: String|nil }
  ],
  created_at: String,
  status: String,            # default "active"
  bot_count: Integer|nil
}

JSON storage uses string keys via stringify(session).


Games

Canonical file: lib/game.rb

Credits

Per-game state lives in lib/*_game.rb modules; plugins call Game for credits and IRC card styling.


YouTube (yt)

Canonical files: lib/yt_oauth.rb, lib/yt_playlist.rb, lib/yt_commands.rb

OAuth device flow

YtOAuth.start_device_flow(bot_nick:, fetch:){ success:, verification_url:, user_code:, expires_in: } or { success: false, error: }

YtOAuth.poll_device_flow(bot_nick:, fetch:){ success:, account: } or { success: false, pending: true, error: } or { success: false, error: }

Account hash (stored in RabbotDb, scoped to bot nick):

{
  "refresh_token" => String,
  "channel_id" => String,
  "channel_title" => String,
  "linked_at" => String # ISO8601
}

Playlist operations

YtPlaylist.list_playlists{ success:, playlists: [{ id:, title:, privacy: }] }

YtPlaylist.create_playlist{ success:, playlist: { id:, title:, privacy: } }

YtPlaylist.show_playlist{ success:, playlist:, items: [{ item_id:, video_id:, title: }] }

YtPlaylist.add_video / remove_item / delete_playlist{ success:, ... } or { success: false, error: }

Commands

YtCommands.dispatch(...){ reply: String } or { error: String }


RabbotDb plugin JSON

Canonical file: lib/rabbot_db.rb

RabbotDb.get_plugin_json(
  plugin: "my_plugin",
  key: "my_key",
  network: nil,    # optional scope
  channel: nil,
  nick: nil,
  default: nil
)

RabbotDb.set_plugin_json(
  plugin:, key:, value:,   # value: Hash or nil to clear
  network:, channel:, nick:
)

Use normalize_network, normalize_channel, normalize_nick before writes.

User levels: RabbotDb::LEVEL_ADMIN, LEVEL_VOICE, etc. — see RABBOT_DB.md.


Async duel games

Canonical files: lib/async_duel.rb, lib/async_duel_handler.rb

Challenge flow stores state in RabbotDb under the plugin name. Handler expects { reply: String } or { error: String } from game modules.


Help metadata (plugins)

Every plugin should have:

# Help: description — !command usage — responders: all_loaded|owner|room_reader

Parsed by HelpParser.list_commands for !help and PluginCatalog.


IRC reply cards

Canonical file: lib/irc_reply.rb

IrcReply.card(tag:, title:, body_lines:, footer_parts:, heading_style:) → multi-line String with IRC colour codes.

Body lines should be plain text; colour only on tag, heading, footer — see CONVENTIONS.md.

MessageProgress::ReplyContext

MessageProgress::ReplyContext.new(
  lag_ms: Integer,
  ping_ms: Integer|nil,
  piece_index: Integer,      # 0-based chunk index
  total_pieces: Integer,
  queued: true|false|nil     # Bot output queue defer active
)

FloodSafe#reply_pieces with with_progress: :auto prefixes lines when MessageProgress.should_prefix? is true (2+ logical lines or 2+ chunks). Explicit with_progress: true on a single-line reply adds the 100 status gutter only.

Per-channel opt-out: BotOptions key no_progress_gutter (boolean) disables the gutter when true.

Label:value body rows in reports should use SearchReport.colon_body_lines([{ label:, value: }, ...]) before joining body_lines.