Module: Ask::SessionProtocol::Events
- Defined in:
- lib/ask/session_protocol/events.rb
Overview
The canonical session event vocabulary.
Every event on the wire has the same envelope:
{ "type" => "model.streaming", "seq" => 12, "payload" => { "delta" => "..." } }
- type — one of the canonical dot-names below (additive growth only; clients must tolerate unknown types within a major version)
- seq — monotonically increasing per session; clients use it for ordering, dedup, and replay (ask "events after seq N")
- payload — the event body, string-keyed
Events marked interaction (approval.required, plan.proposed) are
resolvable by id from ANY client through the interaction/* and
plan/* methods — a terminal, web console, or bot resolves the same
pending interaction, and the host tombstones delivery per subscriber.
Defined Under Namespace
Classes: Event
Constant Summary collapse
- FIELD_TYPES =
Payload field types accepted by the validator.
%i[string integer number boolean object array any].freeze
- TYPES =
The canonical event registry. Each entry describes the event and the shape of its payload. This registry is the single source of truth for the event vocabulary — the JSON Schema artifact is generated from it, and hosts/clients validate against it.
{ # ── Session lifecycle ──────────────────────────────────────────── "session.created" => { description: "A session was created and is ready to receive prompts.", interaction: false, payload: { "sessionId" => { type: :string, required: true, description: "The session identifier." } } }, "session.ended" => { description: "The session was closed and its resources released.", interaction: false, payload: { "sessionId" => { type: :string, required: true, description: "The session identifier." }, "reason" => { type: :string, required: false, description: "Why the session ended: closed, aborted, error, ..." } } }, # ── Turn lifecycle ─────────────────────────────────────────────── "turn.started" => { description: "A turn (prompt → response → tool loop) began.", interaction: false, payload: { "turnId" => { type: :string, required: true, description: "Identifies the turn; correlates all of its events." } } }, "turn.completed" => { description: "A turn finished successfully. The accumulated response text is in `response`.", interaction: false, payload: { "turnId" => { type: :string, required: true, description: "The turn identifier." }, "response" => { type: :string, required: false, description: "Final assistant text for the turn." }, "inputTokens" => { type: :integer, required: false, description: "LLM input tokens used in the turn." }, "outputTokens" => { type: :integer, required: false, description: "LLM output tokens used in the turn." }, "cost" => { type: :number, required: false, description: "Estimated cost of the turn in USD." } } }, "turn.failed" => { description: "A turn failed; the session remains usable.", interaction: false, payload: { "turnId" => { type: :string, required: true, description: "The turn identifier." }, "error" => { type: :string, required: true, description: "Human-readable failure reason." } } }, "turn.aborted" => { description: "A turn was aborted by the user (session/abort) or the host.", interaction: false, payload: { "turnId" => { type: :string, required: true, description: "The turn identifier." } } }, # ── Model output ───────────────────────────────────────────────── "model.streaming" => { description: "A delta of the assistant's response text.", interaction: false, payload: { "delta" => { type: :string, required: true, description: "The next chunk of text." } } }, "model.thinking" => { description: "A delta of the model's reasoning/thinking text (rendered collapsibly).", interaction: false, payload: { "delta" => { type: :string, required: true, description: "The next chunk of thinking text." } } }, # ── Tool execution ─────────────────────────────────────────────── "tool.use" => { description: "The model requested a tool call; execution is about to start.", interaction: false, payload: { "id" => { type: :string, required: true, description: "Tool call identifier; correlates use/delta/result." }, "name" => { type: :string, required: true, description: "Tool name, e.g. bash, write." }, "args" => { type: :any, required: false, description: "Tool arguments (object or pre-serialized string)." } } }, "tool.delta" => { description: "A partial result while a tool is executing.", interaction: false, payload: { "id" => { type: :string, required: true, description: "Tool call identifier." }, "name" => { type: :string, required: true, description: "Tool name." }, "partial" => { type: :string, required: true, description: "The next chunk of tool output." } } }, "tool.result" => { description: "A tool finished executing.", interaction: false, payload: { "id" => { type: :string, required: true, description: "Tool call identifier." }, "name" => { type: :string, required: true, description: "Tool name." }, "output" => { type: :any, required: false, description: "Tool output (string or structured value)." }, "isError" => { type: :boolean, required: false, description: "True when the tool failed." }, "durationMs" => { type: :integer, required: false, description: "Execution duration in milliseconds." } } }, # ── Interactions (resolvable by id from any client) ───────────── "approval.required" => { description: "A tool is queued for human approval. Resolve via interaction/approve or interaction/reject.", interaction: true, payload: { "id" => { type: :string, required: true, description: "Interaction id; used to resolve this request." }, "toolName" => { type: :string, required: true, description: "The tool waiting for approval." }, "args" => { type: :any, required: false, description: "Tool arguments under review." }, "message" => { type: :string, required: false, description: "Reason the approval was requested." }, "autoApprovable" => { type: :boolean, required: false, description: "True when a configured rule may auto-approve." } } }, "approval.updated" => { description: "A pending approval was resolved.", interaction: false, payload: { "id" => { type: :string, required: true, description: "The interaction id from approval.required." }, "status" => { type: :string, required: true, enum: %w[approved rejected], description: "How it was resolved." } } }, "plan.proposed" => { description: "The agent proposed a plan (plan mode). Resolve via plan/approve or plan/reject.", interaction: true, payload: { "id" => { type: :string, required: true, description: "Interaction id; used to resolve this proposal." }, "plan" => { type: :string, required: true, description: "The proposed plan text." } } }, "plan.approved" => { description: "A proposed plan was approved and the agent may proceed.", interaction: false, payload: { "id" => { type: :string, required: false, description: "The interaction id from plan.proposed, when known." }, "plan" => { type: :string, required: true, description: "The approved plan text." } } }, "plan.rejected" => { description: "A proposed plan was rejected; the agent stays in plan mode.", interaction: false, payload: { "id" => { type: :string, required: false, description: "The interaction id from plan.proposed, when known." }, "plan" => { type: :string, required: true, description: "The rejected plan text." } } }, # ── Session state ──────────────────────────────────────────────── "todos.updated" => { description: "The session's todo list changed. Payload carries the full list.", interaction: false, payload: { "todos" => { type: :array, required: true, description: "Full todo list: [{id, title, status}]." } } }, "file.changed" => { description: "A file in the workspace changed (create/modify/delete).", interaction: false, payload: { "path" => { type: :string, required: true, description: "Workspace-relative file path." }, "type" => { type: :string, required: true, enum: %w[created modified deleted], description: "Kind of change." }, "patch" => { type: :string, required: false, description: "Unified diff text for created/modified files." } } }, # ── Errors ─────────────────────────────────────────────────────── "error" => { description: "A non-fatal error was emitted outside a turn (recoverable when flagged).", interaction: false, payload: { "error" => { type: :string, required: true, description: "Error message." }, "recoverable" => { type: :boolean, required: false, description: "True when the host can continue." } } } }.freeze
Class Method Summary collapse
-
.event(type:, seq:, payload: {}) ⇒ Event
Build and validate an event.
-
.from_h(hash) ⇒ Event
Rebuild an event from its wire shape, validating as it goes.
-
.interaction?(type) ⇒ Boolean
Whether an event type is a resolvable interaction.
-
.known?(type) ⇒ Boolean
Whether
typeis a canonical event type. -
.types ⇒ Object
The canonical event types, in registry order.
-
.validate_field!(type, field, field_spec, value) ⇒ Object
private
Shared field validator, used by the events, interactions, and methods registries.
-
.validate_payload!(type, payload) ⇒ true
Validate a payload against the registry spec for
type.
Class Method Details
.event(type:, seq:, payload: {}) ⇒ Event
Build and validate an event.
234 235 236 237 |
# File 'lib/ask/session_protocol/events.rb', line 234 def self.event(type:, seq:, payload: {}) validate_payload!(type, payload) Event.new(type: type, seq: seq, payload: payload) end |
.from_h(hash) ⇒ Event
Rebuild an event from its wire shape, validating as it goes.
243 244 245 246 247 248 249 250 251 |
# File 'lib/ask/session_protocol/events.rb', line 243 def self.from_h(hash) hash = hash.transform_keys(&:to_s) raise ArgumentError, "event must be a Hash" unless hash.is_a?(Hash) raise ArgumentError, "event missing type" if hash["type"].nil? raise ArgumentError, "event missing seq" if hash["seq"].nil? payload = hash["payload"] || {} event(type: hash["type"], seq: hash["seq"], payload: payload) end |
.interaction?(type) ⇒ Boolean
Whether an event type is a resolvable interaction.
259 260 261 262 |
# File 'lib/ask/session_protocol/events.rb', line 259 def self.interaction?(type) spec = TYPES[type] spec && spec[:interaction] end |
.known?(type) ⇒ Boolean
Whether type is a canonical event type.
254 255 256 |
# File 'lib/ask/session_protocol/events.rb', line 254 def self.known?(type) TYPES.key?(type) end |
.types ⇒ Object
The canonical event types, in registry order.
265 266 267 |
# File 'lib/ask/session_protocol/events.rb', line 265 def self.types TYPES.keys end |
.validate_field!(type, field, field_spec, value) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Shared field validator, used by the events, interactions, and methods registries. Public because sibling modules call it with an explicit receiver.
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 |
# File 'lib/ask/session_protocol/events.rb', line 299 def self.validate_field!(type, field, field_spec, value) kind = field_spec[:type] ok = case kind when :any then true when :string then value.is_a?(String) when :integer then value.is_a?(Integer) when :number then value.is_a?(Numeric) when :boolean then value == true || value == false when :object then value.is_a?(Hash) when :array then value.is_a?(Array) end unless ok raise ArgumentError, "event #{type} field #{field.inspect} must be a #{kind}, got #{value.class}" end if field_spec[:enum] && !field_spec[:enum].include?(value) raise ArgumentError, "event #{type} field #{field.inspect} must be one of #{field_spec[:enum].inspect}, got #{value.inspect}" end end |
.validate_payload!(type, payload) ⇒ true
Validate a payload against the registry spec for type.
Raises ArgumentError on unknown types, missing required fields, or
type/enum mismatches. Unknown extra fields are allowed (forward
compatibility within a major protocol version).
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 |
# File 'lib/ask/session_protocol/events.rb', line 277 def self.validate_payload!(type, payload) spec = TYPES[type] raise ArgumentError, "unknown session event type: #{type.inspect}" unless spec raise ArgumentError, "payload for #{type} must be a Hash" unless payload.is_a?(Hash) spec[:payload].each do |field, field_spec| value = payload[field] if field_spec[:required] && value.nil? raise ArgumentError, "event #{type} missing required payload field #{field.inspect}" end next if value.nil? validate_field!(type, field, field_spec, value) end true end |