Module: Ask::SessionProtocol::Interactions

Defined in:
lib/ask/session_protocol/interactions.rb

Overview

Resolvable interactions — the human-in-the-loop surface of the protocol.

An interaction is created by the host (emitted as an interaction event: approval.required, plan.proposed) with a unique id. Any client may resolve it by id through the interaction/* and plan/* methods; the host routes the resolution to the session and tombstones delivery so each subscriber sees it exactly once.

approval.required ──▶ interaction/approve | interaction/reject
plan.proposed     ──▶ plan/approve        | plan/reject
user input        ──▶ interaction/respond  (host → client request)

Defined Under Namespace

Classes: Interaction

Constant Summary collapse

KINDS =

The interaction kinds.

%w[approval plan user_input].freeze
STATUSES =

Resolution statuses. A resolved interaction never returns to pending.

%w[pending approved rejected responded expired].freeze
APPROVAL_PAYLOAD =

Payload shape for kind "approval" (from the approval.required event).

{
  "toolName" => { type: :string, required: true },
  "args" => { type: :any, required: false },
  "message" => { type: :string, required: false },
  "autoApprovable" => { type: :boolean, required: false }
}.freeze
PLAN_PAYLOAD =

Payload shape for kind "plan" (from the plan.proposed event).

{
  "plan" => { type: :string, required: true }
}.freeze
USER_INPUT_PAYLOAD =

Payload shape for kind "user_input" (host → client request asking the human for input; resolve via interaction/respond).

{
  "prompt" => { type: :string, required: true, description: "What the agent is asking the human." },
  "options" => { type: :array, required: false, description: "Suggested answers, when the host offers any." }
}.freeze
PAYLOADS =

Payload spec per kind. The single source of truth for interaction payload validation and schema generation.

{
  "approval" => APPROVAL_PAYLOAD,
  "plan" => PLAN_PAYLOAD,
  "user_input" => USER_INPUT_PAYLOAD
}.freeze

Class Method Summary collapse

Class Method Details

.from_h(hash) ⇒ Interaction

Rebuild an interaction from its wire shape.

Parameters:

  • hash (Hash)

    { "id" =>, "kind" =>, "status" =>, "payload" => }

Returns:

Raises:

  • (ArgumentError)


103
104
105
106
107
108
109
110
111
112
113
# File 'lib/ask/session_protocol/interactions.rb', line 103

def self.from_h(hash)
  hash = hash.transform_keys(&:to_s)
  raise ArgumentError, "interaction must be a Hash" unless hash.is_a?(Hash)

  interaction(
    id: hash["id"],
    kind: hash["kind"],
    status: hash["status"] || "pending",
    payload: hash["payload"] || {}
  )
end

.interaction(id:, kind:, status: "pending", payload: {}) ⇒ Interaction

Build an interaction, validating the payload against its kind.

Parameters:

  • id (String)

    unique interaction id

  • kind (String)

    one of KINDS

  • status (String) (defaults to: "pending")

    one of STATUSES

  • payload (Hash) (defaults to: {})

    kind-specific body

Returns:



94
95
96
97
# File 'lib/ask/session_protocol/interactions.rb', line 94

def self.interaction(id:, kind:, status: "pending", payload: {})
  validate_payload!(kind, payload)
  Interaction.new(id: id, kind: kind, status: status, payload: payload)
end

.validate_payload!(kind, payload) ⇒ true

Validate a payload against its kind spec. Unknown extra fields are allowed (forward compatibility).

Parameters:

  • kind (String)

    one of KINDS

  • payload (Hash)

Returns:

  • (true)

    when valid

Raises:

  • (ArgumentError)


121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/ask/session_protocol/interactions.rb', line 121

def self.validate_payload!(kind, payload)
  spec = PAYLOADS[kind]
  raise ArgumentError, "unknown interaction kind: #{kind.inspect}" unless spec
  raise ArgumentError, "payload for #{kind} must be a Hash" unless payload.is_a?(Hash)

  spec.each do |field, field_spec|
    value = payload[field]
    if field_spec[:required] && value.nil?
      raise ArgumentError, "interaction #{kind} missing required payload field #{field.inspect}"
    end
    next if value.nil?

    Events.validate_field!(kind, field, field_spec, value)
  end
  true
end