Class: ActionMCP::Session::Task

Inherits:
ApplicationRecord show all
Defined in:
app/models/action_mcp/session/task.rb

Overview

Represents a Task in an MCP session as per MCP 2025-11-25 specification. Tasks provide durable state machines for tracking async request execution.

State Machine:

working -> input_required -> working (via resume)
working -> completed | failed | cancelled
input_required -> completed | failed | cancelled

Instance Method Summary collapse

Instance Method Details

#await_input!(prompt:, context: {}) ⇒ Object

Transition to input_required state and store pending input prompt

Parameters:

  • prompt (String)

    The prompt/question for the user

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

    Additional context about the input request



182
183
184
185
# File 'app/models/action_mcp/session/task.rb', line 182

def await_input!(prompt:, context: {})
  record_step!(:awaiting_input, data: { prompt: prompt, context: context })
  require_input!
end

#broadcast_status_change(transition = nil) ⇒ Object

Broadcast status change notification to the session

Parameters:

  • transition (StateMachines::Transition) (defaults to: nil)

    The state transition that occurred



132
133
134
135
136
137
138
139
# File 'app/models/action_mcp/session/task.rb', line 132

def broadcast_status_change(transition = nil)
  return unless session

  handler = ActionMCP::Server::TransportHandler.new(session)
  handler.send_task_status_notification(self)
rescue StandardError => e
  Rails.logger.warn "Failed to broadcast task status change: #{e.message}"
end

#expired?Boolean

TTL management

Returns:

  • (Boolean)

    true if task has exceeded its TTL



83
84
85
86
87
88
# File 'app/models/action_mcp/session/task.rb', line 83

def expired?
  return false if ttl.nil?

  # TTL is stored in milliseconds (MCP spec)
  created_at + (ttl / 1000.0).seconds < Time.current
end

#non_terminal?Boolean

Check if task is in a non-terminal state

Returns:

  • (Boolean)


96
97
98
# File 'app/models/action_mcp/session/task.rb', line 96

def non_terminal?
  !terminal?
end

#record_step!(step_name, cursor: nil, data: {}) ⇒ Object

Record step execution state for job resumption

Parameters:

  • step_name (Symbol)

    Name of the step

  • cursor (Integer, String) (defaults to: nil)

    Optional cursor for resuming iteration

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

    Additional step data to persist



147
148
149
150
151
152
153
154
155
156
157
# File 'app/models/action_mcp/session/task.rb', line 147

def record_step!(step_name, cursor: nil, data: {})
  update!(
    continuation_state: {
      step: step_name,
      cursor: cursor,
      data: data,
      timestamp: Time.current.iso8601
    },
    last_step_at: Time.current
  )
end

#resume_from_continuation!void

This method returns an undefined value.

Resume task from input_required state and re-enqueue job



189
190
191
192
193
194
195
# File 'app/models/action_mcp/session/task.rb', line 189

def resume_from_continuation!
  return unless input_required?

  resume!
  # Re-enqueue the job to continue execution
  ActionMCP::ToolExecutionJob.perform_later(id, request_name, request_params, {})
end

#store_partial_result!(result_fragment) ⇒ Object

Store partial result fragment (for streaming/incremental results)

Parameters:

  • result_fragment (Hash)

    Partial result to append



161
162
163
164
165
166
# File 'app/models/action_mcp/session/task.rb', line 161

def store_partial_result!(result_fragment)
  payload = result_payload || {}
  payload[:partial] ||= []
  payload[:partial] << result_fragment
  update!(result_payload: payload)
end

#terminal?Boolean

Check if task is in a terminal state

Returns:

  • (Boolean)


91
92
93
# File 'app/models/action_mcp/session/task.rb', line 91

def terminal?
  status.in?(%w[completed failed cancelled])
end

#to_task_dataHash

Convert to task data format per MCP spec

Returns:

  • (Hash)

    Task data for JSON-RPC responses



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'app/models/action_mcp/session/task.rb', line 102

def to_task_data
  data = {
    id: id,
    status: status,
    lastUpdatedAt: last_updated_at.iso8601(3)
  }
  data[:statusMessage] = status_message if status_message.present?

  # Add progress if available (ActiveJob::Continuable support)
  if progress_percent.present? || progress_message.present?
    data[:progress] = {}.tap do |progress|
      progress[:percent] = progress_percent if progress_percent.present?
      progress[:message] = progress_message if progress_message.present?
    end
  end

  data
end

#to_task_resultHash

Convert to full task result format

Returns:

  • (Hash)

    Complete task with result for tasks/result response



123
124
125
126
127
128
# File 'app/models/action_mcp/session/task.rb', line 123

def to_task_result
  {
    task: to_task_data,
    result: result_payload
  }
end

#update_progress!(percent:, message: nil) ⇒ Object

Update progress indicators for long-running tasks

Parameters:

  • percent (Integer)

    Progress percentage (0-100)

  • message (String) (defaults to: nil)

    Optional progress message



171
172
173
174
175
176
177
# File 'app/models/action_mcp/session/task.rb', line 171

def update_progress!(percent:, message: nil)
  update!(
    progress_percent: percent.clamp(0, 100),
    progress_message: message,
    last_step_at: Time.current
  )
end