Class: Ask::CodingProviders::ACP::Adapter

Inherits:
Ask::CodingProviders::Adapter show all
Defined in:
lib/ask/coding_providers/acp/adapter.rb

Overview

Adapter that connects to any ACP-speaking coding agent over stdio.

ACP (Agent Client Protocol) is a JSON-RPC 2.0 standard for agent-editor communication. This adapter wraps the ACP client into the generic Ask::CodingProviders::Adapter interface. It is the primary recommended adapter for new deployments.

Works with any ACP-compatible agent:

  • CodexACP_COMMAND='codex acp' (native ACP support)
  • Claude CodeACP_COMMAND='claude acp' (native ACP support)
  • OpenCodeACP_COMMAND='opencode acp' (native ACP support)
  • ZCode — via ACP bridges like william0wang/zcode-acp or alexeygrigorev/zcode-acp
  • Gemini CLIACP_COMMAND='gemini-cli acp' (native ACP support)

Examples:

adapter = Ask::CodingProviders::ACP::Adapter.new(
  command: ["codex", "acp"],
  cwd: Dir.pwd
)
adapter.start
sid = adapter.create_session("/tmp")
adapter.send_and_stream(sid, "Hello") { |ev| puts ev }

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Ask::CodingProviders::Adapter

#find_recent_session, #find_recent_tui_session, #find_sessions, #handle_session_error, #list_projects, #list_sessions, #recent_sessions, #session_directory, #session_history

Constructor Details

#initialize(command: nil, cwd: ".", request_timeout: 60.0, replay: nil) ⇒ Adapter

Returns a new instance of Adapter.

Parameters:

  • command (Array<String>) (defaults to: nil)

    the command to spawn the ACP agent

  • cwd (String) (defaults to: ".")

    working directory

  • request_timeout (Float) (defaults to: 60.0)

    timeout for ACP requests



35
36
37
38
39
40
41
42
43
44
# File 'lib/ask/coding_providers/acp/adapter.rb', line 35

def initialize(command: nil, cwd: ".", request_timeout: 60.0, replay: nil)
  require "ask/acp"

  @command = command
  @cwd = cwd
  @request_timeout = request_timeout
  @replay = replay
  @client = nil
  @sessions = {}
end

Class Method Details

.from_config(workspace_path: Dir.pwd, cli_path: nil, request_timeout: 600) ⇒ Object

Build an ACP adapter from config. Reads ACP_COMMAND env var (space-separated command + args).



181
182
183
184
185
186
187
188
189
190
191
# File 'lib/ask/coding_providers/acp/adapter.rb', line 181

def self.from_config(workspace_path: Dir.pwd, cli_path: nil, request_timeout: 600, **)
  cmd = cli_path || ENV["ACP_COMMAND"] || raise(
    Ask::CodingProviders::ConfigurationError,
    "Missing ACP_COMMAND. Set ACP_COMMAND or pass cli_path, e.g. ACP_COMMAND='codex acp'"
  )
  new(
    command: cmd.is_a?(String) ? cmd.split : cmd,
    cwd: workspace_path,
    request_timeout: request_timeout
  )
end

Instance Method Details

#create_session(workspace_path, mode: nil, system_prompt: nil) ⇒ Object



67
68
69
70
71
72
73
74
# File 'lib/ask/coding_providers/acp/adapter.rb', line 67

def create_session(workspace_path, mode: nil, system_prompt: nil, **)
  ensure_running
  params = { cwd: workspace_path || @cwd }
  session = @client.session_new(**params)
  sid = session[:id]
  @sessions[sid] = { workspace: workspace_path, mode: mode, created_at: Time.now }
  sid
end

#get_events(session_id, after_seq:, limit: nil) ⇒ Object



163
164
165
166
# File 'lib/ask/coding_providers/acp/adapter.rb', line 163

def get_events(session_id, after_seq:, limit: nil)
  ensure_running
  {}
end

#get_workspace_state(workspace_path) ⇒ Object



172
173
174
175
176
177
# File 'lib/ask/coding_providers/acp/adapter.rb', line 172

def get_workspace_state(workspace_path)
  ensure_running
  {}
rescue => e
  {}
end

#respond(request_id, result) ⇒ Object



168
169
170
# File 'lib/ask/coding_providers/acp/adapter.rb', line 168

def respond(request_id, result)
  # ACP uses notification-based reverse requests, no direct respond
end

#resume_session(session_id) ⇒ Object



76
77
78
79
80
81
# File 'lib/ask/coding_providers/acp/adapter.rb', line 76

def resume_session(session_id)
  ensure_running
  @client.session_resume(session_id)
rescue => e
  {}
end

#running?Boolean

Returns:

  • (Boolean)


63
64
65
# File 'lib/ask/coding_providers/acp/adapter.rb', line 63

def running?
  @client&.running? || false
end

#send_and_stream(session_id, content, turn_timeout: 600.0, &block) ⇒ Object



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/ask/coding_providers/acp/adapter.rb', line 93

def send_and_stream(session_id, content, turn_timeout: 600.0, &block)
  return enum_for(:send_and_stream, session_id, content, turn_timeout: turn_timeout) unless block
  ensure_running

  accumulated = ""
  block.call({ type: "turn.started", seq: 1, payload: { "sessionId" => session_id } })

  result = @client.session_prompt(session_id, content, timeout: turn_timeout) do |event|
    case event[:method]
    when "text"
      delta = event.dig(:params, "content") || ""
      accumulated += delta
      block.call({
        type: "model.streaming", seq: 2,
        payload: { "delta" => delta, "sessionId" => session_id }
      }) unless delta.empty?
    when "session/update"
      update = event.dig(:params, "update") || {}
      case update["sessionUpdate"]
      when "tool_call"
        kind = update["kind"] || "unknown"
        block.call({
          type: "tool.use", seq: 2,
          payload: {
            "sessionId" => session_id,
            "toolName" => kind,
            "input" => update["content"]
          }
        })
      when "tool_call_update"
        block.call({
          type: "tool.result", seq: 2,
          payload: {
            "sessionId" => session_id,
            "toolName" => event.dig(:params, "kind") || "unknown",
            "output" => update["content"]
          }
        })
      end
    when "turn_complete"
      block.call({
        type: "turn.completed", seq: 3,
        payload: { "response" => accumulated, "sessionId" => session_id }
      })
    when "turn_failed"
      err = event.dig(:params, "error") || "Unknown error"
      block.call({
        type: "turn.failed", seq: 3,
        payload: { "error" => { "message" => err }, "sessionId" => session_id }
      })
    end
  end

  # If the prompt response contains the result and we haven't sent completed yet
  if result.is_a?(Hash)
    stop_reason = result["stopReason"] || result[:stopReason]
    if stop_reason == "cancelled" || stop_reason == "refusal"
      block.call({
        type: "turn.failed", seq: 3,
        payload: { "error" => { "message" => "Turn #{stop_reason}" }, "sessionId" => session_id }
      })
    end
  end
rescue => e
  block.call({
    type: "turn.failed", seq: 3,
    payload: { "error" => { "message" => e.message }, "sessionId" => session_id }
  })
end

#send_message(session_id, content) ⇒ Object



88
89
90
91
# File 'lib/ask/coding_providers/acp/adapter.rb', line 88

def send_message(session_id, content)
  ensure_running
  @client.session_prompt(session_id, content)
end

#startObject



46
47
48
49
50
51
52
53
54
55
# File 'lib/ask/coding_providers/acp/adapter.rb', line 46

def start
  return if @client
  if @replay
    @client = Ask::ACP::ReplayClient.new(fixture_path: @replay)
  else
    @client = Ask::ACP::Client.new(command: @command, request_timeout: @request_timeout)
  end
  @client.start
  @client.initialize!(client_name: "askoda", client_version: "0.1.0")
end

#stopObject



57
58
59
60
61
# File 'lib/ask/coding_providers/acp/adapter.rb', line 57

def stop
  @client&.stop
  @client = nil
  @sessions = {}
end

#subscribe(session_id, after_seq: 0) ⇒ Object



83
84
85
86
# File 'lib/ask/coding_providers/acp/adapter.rb', line 83

def subscribe(session_id, after_seq: 0)
  ensure_running
  { "eventSeq" => 0 }
end