ask-coding-providers 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/ask/coding_providers/acp/adapter.rb +175 -0
- data/lib/ask/coding_providers/acp.rb +11 -0
- data/lib/ask/coding_providers/adapter.rb +152 -0
- data/lib/ask/coding_providers/ask_agent/adapter.rb +182 -0
- data/lib/ask/coding_providers/ask_agent.rb +19 -0
- data/lib/ask/coding_providers/claude/adapter.rb +182 -0
- data/lib/ask/coding_providers/claude.rb +11 -0
- data/lib/ask/coding_providers/codex/adapter.rb +200 -0
- data/lib/ask/coding_providers/codex/app_server_client.rb +261 -0
- data/lib/ask/coding_providers/codex/codex_db.rb +210 -0
- data/lib/ask/coding_providers/codex.rb +14 -0
- data/lib/ask/coding_providers/version.rb +7 -0
- data/lib/ask/coding_providers/zcode/zcode_db.rb +195 -0
- data/lib/ask/coding_providers/zcode.rb +12 -0
- data/lib/ask/coding_providers.rb +53 -0
- data/lib/ask-coding-providers.rb +3 -0
- metadata +115 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c705fccdf37e62127cbbff2ad58fa7102ac202429ab07fe291fe8b649ab3ddd4
|
|
4
|
+
data.tar.gz: 3884a19f15a0a7595b0d4a56787f38ff00d500438d4656d78db8ed3cb42332f9
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 360d444a0f376927de910213f38d30f3ac1cfca4c86b1c85e8e44e5cd60f91dcb57feb4ce976d6124bf20a10d8bf5c2e920eaa682fd6393e74d087f5cfd72e06
|
|
7
|
+
data.tar.gz: 9860f5295b9926fa6317aeb6f3cb25c94c8082eed66b8a04d5f6f3d5a377c6c4d695f356772d9edc7dbdc9b6f1ee026bea5b3ff809968fcf758526128a6eb66a
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Ask
|
|
6
|
+
module CodingProviders
|
|
7
|
+
module ACP
|
|
8
|
+
# Adapter that connects to any ACP-speaking coding agent over stdio.
|
|
9
|
+
#
|
|
10
|
+
# ACP (Agent Client Protocol) is a JSON-RPC 2.0 standard for agent-editor
|
|
11
|
+
# communication. This adapter wraps the ACP client into the generic
|
|
12
|
+
# {Ask::CodingProviders::Adapter} interface. It is the **primary
|
|
13
|
+
# recommended adapter** for new deployments.
|
|
14
|
+
#
|
|
15
|
+
# Works with any ACP-compatible agent:
|
|
16
|
+
# - **Codex** — `ACP_COMMAND='codex acp'` (native ACP support)
|
|
17
|
+
# - **Claude Code** — `ACP_COMMAND='claude acp'` (native ACP support)
|
|
18
|
+
# - **OpenCode** — `ACP_COMMAND='opencode acp'` (native ACP support)
|
|
19
|
+
# - **ZCode** — via ACP bridges like william0wang/zcode-acp or
|
|
20
|
+
# alexeygrigorev/zcode-acp
|
|
21
|
+
# - **Gemini CLI** — `ACP_COMMAND='gemini-cli acp'` (native ACP support)
|
|
22
|
+
#
|
|
23
|
+
# @example
|
|
24
|
+
# adapter = Ask::CodingProviders::ACP::Adapter.new(
|
|
25
|
+
# command: ["codex", "acp"],
|
|
26
|
+
# cwd: Dir.pwd
|
|
27
|
+
# )
|
|
28
|
+
# adapter.start
|
|
29
|
+
# sid = adapter.create_session("/tmp")
|
|
30
|
+
# adapter.send_and_stream(sid, "Hello") { |ev| puts ev }
|
|
31
|
+
class Adapter < Ask::CodingProviders::Adapter
|
|
32
|
+
# @param command [Array<String>] the command to spawn the ACP agent
|
|
33
|
+
# @param cwd [String] working directory
|
|
34
|
+
# @param request_timeout [Float] timeout for ACP requests
|
|
35
|
+
def initialize(command:, cwd: ".", request_timeout: 60.0)
|
|
36
|
+
require "ask/acp"
|
|
37
|
+
|
|
38
|
+
@command = command
|
|
39
|
+
@cwd = cwd
|
|
40
|
+
@request_timeout = request_timeout
|
|
41
|
+
@client = nil
|
|
42
|
+
@sessions = {}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def start
|
|
46
|
+
return if @client
|
|
47
|
+
@client = Ask::ACP::Client.new(command: @command, request_timeout: @request_timeout)
|
|
48
|
+
@client.start
|
|
49
|
+
@client.initialize!(client_name: "askoda", client_version: "0.1.0")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def stop
|
|
53
|
+
@client&.stop
|
|
54
|
+
@client = nil
|
|
55
|
+
@sessions = {}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def running?
|
|
59
|
+
@client&.running? || false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def create_session(workspace_path, mode: nil)
|
|
63
|
+
ensure_running
|
|
64
|
+
params = { cwd: workspace_path || @cwd }
|
|
65
|
+
session = @client.session_new(**params)
|
|
66
|
+
sid = session[:id]
|
|
67
|
+
@sessions[sid] = { workspace: workspace_path, mode: mode, created_at: Time.now }
|
|
68
|
+
sid
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def resume_session(session_id)
|
|
72
|
+
ensure_running
|
|
73
|
+
@client.session_resume(session_id)
|
|
74
|
+
rescue => e
|
|
75
|
+
{}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def subscribe(session_id, after_seq: 0)
|
|
79
|
+
ensure_running
|
|
80
|
+
{ "eventSeq" => 0 }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def send_message(session_id, content)
|
|
84
|
+
ensure_running
|
|
85
|
+
@client.session_prompt(session_id, content)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def send_and_stream(session_id, content, turn_timeout: 600.0, &block)
|
|
89
|
+
return enum_for(:send_and_stream, session_id, content, turn_timeout: turn_timeout) unless block
|
|
90
|
+
ensure_running
|
|
91
|
+
|
|
92
|
+
accumulated = ""
|
|
93
|
+
block.call({ type: "turn.started", seq: 1, payload: { "sessionId" => session_id } })
|
|
94
|
+
|
|
95
|
+
result = @client.session_prompt(session_id, content, timeout: turn_timeout) do |event|
|
|
96
|
+
case event[:method]
|
|
97
|
+
when "text"
|
|
98
|
+
delta = event.dig(:params, "content") || ""
|
|
99
|
+
accumulated += delta
|
|
100
|
+
block.call({
|
|
101
|
+
type: "model.streaming", seq: 2,
|
|
102
|
+
payload: { "delta" => delta, "sessionId" => session_id }
|
|
103
|
+
}) unless delta.empty?
|
|
104
|
+
when "turn_complete"
|
|
105
|
+
block.call({
|
|
106
|
+
type: "turn.completed", seq: 3,
|
|
107
|
+
payload: { "response" => accumulated, "sessionId" => session_id }
|
|
108
|
+
})
|
|
109
|
+
when "turn_failed"
|
|
110
|
+
err = event.dig(:params, "error") || "Unknown error"
|
|
111
|
+
block.call({
|
|
112
|
+
type: "turn.failed", seq: 3,
|
|
113
|
+
payload: { "error" => { "message" => err }, "sessionId" => session_id }
|
|
114
|
+
})
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# If the prompt response contains the result and we haven't sent completed yet
|
|
119
|
+
if result.is_a?(Hash)
|
|
120
|
+
stop_reason = result["stopReason"] || result[:stopReason]
|
|
121
|
+
if stop_reason == "cancelled" || stop_reason == "refusal"
|
|
122
|
+
block.call({
|
|
123
|
+
type: "turn.failed", seq: 3,
|
|
124
|
+
payload: { "error" => { "message" => "Turn #{stop_reason}" }, "sessionId" => session_id }
|
|
125
|
+
})
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
rescue => e
|
|
129
|
+
block.call({
|
|
130
|
+
type: "turn.failed", seq: 3,
|
|
131
|
+
payload: { "error" => { "message" => e.message }, "sessionId" => session_id }
|
|
132
|
+
})
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def get_events(session_id, after_seq:, limit: nil)
|
|
136
|
+
ensure_running
|
|
137
|
+
{}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def respond(request_id, result)
|
|
141
|
+
# ACP uses notification-based reverse requests, no direct respond
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def get_workspace_state(workspace_path)
|
|
145
|
+
ensure_running
|
|
146
|
+
{}
|
|
147
|
+
rescue => e
|
|
148
|
+
{}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Build an ACP adapter from config.
|
|
152
|
+
# Reads ACP_COMMAND env var (space-separated command + args).
|
|
153
|
+
def self.from_config(workspace_path: Dir.pwd, cli_path: nil, request_timeout: 600, **)
|
|
154
|
+
cmd = cli_path || ENV["ACP_COMMAND"] || raise(
|
|
155
|
+
Ask::CodingProviders::ConfigurationError,
|
|
156
|
+
"Missing ACP_COMMAND. Set ACP_COMMAND or pass cli_path, e.g. ACP_COMMAND='codex acp'"
|
|
157
|
+
)
|
|
158
|
+
new(
|
|
159
|
+
command: cmd.is_a?(String) ? cmd.split : cmd,
|
|
160
|
+
cwd: workspace_path,
|
|
161
|
+
request_timeout: request_timeout
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
def ensure_running
|
|
168
|
+
raise Error, "ACP adapter is not running. Call #start first." unless @client&.running?
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
Ask::CodingProviders.register_adapter(:acp, Ask::CodingProviders::ACP::Adapter)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
module CodingProviders
|
|
5
|
+
# Abstract base class for coding agent adapters.
|
|
6
|
+
#
|
|
7
|
+
# A coding agent adapter wraps an AI coding agent (e.g., ZCode, Claude Code, Codex)
|
|
8
|
+
# and provides a uniform interface for the coder engine to interact with it.
|
|
9
|
+
#
|
|
10
|
+
# Subclasses must implement all methods that raise NotImplementedError.
|
|
11
|
+
class Adapter
|
|
12
|
+
# Create a new session on the agent.
|
|
13
|
+
#
|
|
14
|
+
# @param workspace_path [String] the working directory
|
|
15
|
+
# @param mode [String, nil] permission mode
|
|
16
|
+
# @return [String] the session ID
|
|
17
|
+
def create_session(workspace_path, mode: nil)
|
|
18
|
+
raise NotImplementedError, "#{self.class} must implement #create_session"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Resume an existing session.
|
|
22
|
+
#
|
|
23
|
+
# @param session_id [String] the session ID
|
|
24
|
+
# @return [Hash] session snapshot
|
|
25
|
+
def resume_session(session_id)
|
|
26
|
+
raise NotImplementedError, "#{self.class} must implement #resume_session"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# List available sessions.
|
|
30
|
+
#
|
|
31
|
+
# @param workspace_path [String, nil] optional filter
|
|
32
|
+
# @param limit [Integer] max sessions
|
|
33
|
+
# @return [Array<Hash>] session summaries
|
|
34
|
+
def list_sessions(workspace_path: nil, limit: 20)
|
|
35
|
+
raise NotImplementedError, "#{self.class} must implement #list_sessions"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Subscribe to session events.
|
|
39
|
+
#
|
|
40
|
+
# @param session_id [String] the session ID
|
|
41
|
+
# @param after_seq [Integer] sequence to start from
|
|
42
|
+
# @return [Hash] subscription result
|
|
43
|
+
def subscribe(session_id, after_seq: 0)
|
|
44
|
+
raise NotImplementedError, "#{self.class} must implement #subscribe"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Send a message to a session (fire-and-forget).
|
|
48
|
+
def send_message(session_id, content)
|
|
49
|
+
raise NotImplementedError, "#{self.class} must implement #send_message"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Send a message and stream the response events.
|
|
53
|
+
#
|
|
54
|
+
# @yield [Hash] stream events with :type, :payload, :seq
|
|
55
|
+
def send_and_stream(session_id, content, turn_timeout: 600.0, &block)
|
|
56
|
+
raise NotImplementedError, "#{self.class} must implement #send_and_stream"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Get events after a sequence number (polling).
|
|
60
|
+
#
|
|
61
|
+
# @return [Hash] events list
|
|
62
|
+
def get_events(session_id, after_seq:, limit: nil)
|
|
63
|
+
raise NotImplementedError, "#{self.class} must implement #get_events"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Respond to a reverse request (permission approval, etc.).
|
|
67
|
+
def respond(request_id, result)
|
|
68
|
+
raise NotImplementedError, "#{self.class} must implement #respond"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Read the workspace state (model info, settings, etc.).
|
|
72
|
+
def get_workspace_state(workspace_path)
|
|
73
|
+
{}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Called when a session operation fails. The adapter can signal how
|
|
77
|
+
# the Engine should recover.
|
|
78
|
+
#
|
|
79
|
+
# @param session_id [String] the session that failed
|
|
80
|
+
# @param error [StandardError] the error that occurred
|
|
81
|
+
# @return [Symbol, nil] :create_new to replace the session, nil to show error as-is
|
|
82
|
+
def handle_session_error(session_id, error)
|
|
83
|
+
nil # Unknown error — let the Engine show it to the user
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Optional: list available projects with session counts.
|
|
87
|
+
# Returns [{project_id:, directory:, session_count:}] or nil.
|
|
88
|
+
def list_projects
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Optional: find sessions in a directory.
|
|
93
|
+
# Returns [{session_id:, title:, updated:}] or [].
|
|
94
|
+
def find_sessions(directory:, limit: 20)
|
|
95
|
+
[]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Optional: find the single most recent session.
|
|
99
|
+
# Returns {session_id:, directory:} or nil.
|
|
100
|
+
def find_recent_session
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Optional: find the most recent TUI session in a workspace.
|
|
105
|
+
# Returns {session_id:, title:, directory:} or nil.
|
|
106
|
+
def find_recent_tui_session(workspace_path)
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Optional: get a session's workspace directory by ID.
|
|
111
|
+
# Returns directory string or nil.
|
|
112
|
+
def session_directory(session_id)
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Optional: get session message history.
|
|
117
|
+
# Returns [{text:, role:, origin:}] or [].
|
|
118
|
+
def session_history(session_id, limit: 100)
|
|
119
|
+
[]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Optional: list recent sessions across all projects.
|
|
123
|
+
# Returns [{session_id:, title:, updated:, msg_count:}] or [].
|
|
124
|
+
def recent_sessions
|
|
125
|
+
[]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Start the agent connection.
|
|
129
|
+
def start
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Stop the agent connection.
|
|
133
|
+
def stop
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Whether the agent is running.
|
|
137
|
+
def running?
|
|
138
|
+
true
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Build an adapter instance from a configuration hash.
|
|
142
|
+
# Override in subclasses to extract adapter-specific settings.
|
|
143
|
+
# The default passes the entire hash to +new+.
|
|
144
|
+
#
|
|
145
|
+
# @param config [Hash] configuration options (typically from ENV)
|
|
146
|
+
# @return [Adapter] a configured adapter instance
|
|
147
|
+
def self.from_config(**config)
|
|
148
|
+
new(**config)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Ask
|
|
6
|
+
module CodingProviders
|
|
7
|
+
module AskAgent
|
|
8
|
+
# Adapter that wraps Ask::Agent::Session directly (in-process).
|
|
9
|
+
#
|
|
10
|
+
# No app-server, no external binary — sessions run in the same process.
|
|
11
|
+
# Perfect for deployments where running a separate process is impractical.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# adapter = AskAgent::Adapter.new(model: "deepseek-v4-flash", provider: "opencode_go")
|
|
15
|
+
# adapter.start
|
|
16
|
+
# sid = adapter.create_session("/tmp")
|
|
17
|
+
# adapter.send_and_stream(sid, "Hello") { |ev| puts ev }
|
|
18
|
+
class Adapter < Ask::CodingProviders::Adapter
|
|
19
|
+
# @param model [String] model ID (e.g. "deepseek-v4-flash")
|
|
20
|
+
# @param provider [String] provider slug (e.g. "opencode_go")
|
|
21
|
+
# @param tools [Array] tool instances to make available
|
|
22
|
+
# @param max_turns [Integer] max conversation turns per session
|
|
23
|
+
def initialize(model:, provider:, tools: [], max_turns: 25, **session_opts)
|
|
24
|
+
# Lazy require — these gems are optional for the gem but required for this adapter
|
|
25
|
+
begin
|
|
26
|
+
require "ask-llm-providers"
|
|
27
|
+
require "ask/agent"
|
|
28
|
+
rescue LoadError => e
|
|
29
|
+
raise "Missing dependency for AskAgent adapter: #{e.message}. Add ask-agent and ask-llm-providers to your Gemfile."
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@model_id = model
|
|
33
|
+
@provider_slug = provider
|
|
34
|
+
@tools = tools
|
|
35
|
+
@max_turns = max_turns
|
|
36
|
+
@session_opts = session_opts
|
|
37
|
+
@started = false
|
|
38
|
+
@provider = nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def start
|
|
42
|
+
return if @started
|
|
43
|
+
klass = Ask::Provider.resolve(@provider_slug)
|
|
44
|
+
compat = klass.respond_to?(:compat_config) ? klass.compat_config : {}
|
|
45
|
+
api_key = ENV[compat[:alternate_env].to_s] || ENV[compat[:api_key_env].to_s] || ENV["#{@provider_slug.upcase}_API_KEY"]
|
|
46
|
+
@provider = klass.new(api_key: api_key)
|
|
47
|
+
@started = true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def stop
|
|
51
|
+
@started = false
|
|
52
|
+
@provider = nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def running?
|
|
56
|
+
@started
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Create a new conversation session.
|
|
60
|
+
# The workspace_path is noted but not used by in-process agent.
|
|
61
|
+
# Returns a session ID (UUID).
|
|
62
|
+
def create_session(workspace_path, mode: nil)
|
|
63
|
+
ensure_started
|
|
64
|
+
sid = "sess_#{SecureRandom.uuid}"
|
|
65
|
+
@sessions ||= {}
|
|
66
|
+
@sessions[sid] = { workspace: workspace_path, mode: mode, created_at: Time.now }
|
|
67
|
+
sid
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def resume_session(session_id)
|
|
71
|
+
ensure_started
|
|
72
|
+
@sessions&.dig(session_id) || {}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def subscribe(session_id, after_seq: 0)
|
|
76
|
+
ensure_started
|
|
77
|
+
{ "eventSeq" => 0 }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def send_message(session_id, content)
|
|
81
|
+
ensure_started
|
|
82
|
+
# Run synchronously — returns when done
|
|
83
|
+
result = run_session(session_id, content)
|
|
84
|
+
{ "response" => result }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def send_and_stream(session_id, content, turn_timeout: 600.0, &block)
|
|
88
|
+
return enum_for(:send_and_stream, session_id, content, turn_timeout: turn_timeout) unless block
|
|
89
|
+
ensure_started
|
|
90
|
+
|
|
91
|
+
# Build the ask-agent chat with our pre-configured provider
|
|
92
|
+
chat = build_chat
|
|
93
|
+
session = Ask::Agent::Session.new(
|
|
94
|
+
model: chat,
|
|
95
|
+
max_turns: @max_turns,
|
|
96
|
+
**@session_opts
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Emit turn.started
|
|
100
|
+
block.call({ type: "turn.started", seq: 1, payload: { "sessionId" => session_id } })
|
|
101
|
+
|
|
102
|
+
# Wire streaming events
|
|
103
|
+
session.on_event do |event|
|
|
104
|
+
case event
|
|
105
|
+
when Ask::Agent::Events::TextDelta
|
|
106
|
+
block.call({
|
|
107
|
+
type: "model.streaming", seq: 2,
|
|
108
|
+
payload: { "delta" => event.content, "sessionId" => session_id }
|
|
109
|
+
})
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
begin
|
|
114
|
+
result = session.run(content)
|
|
115
|
+
block.call({
|
|
116
|
+
type: "turn.completed", seq: 3,
|
|
117
|
+
payload: { "response" => result, "sessionId" => session_id,
|
|
118
|
+
"tokenCount" => session.total_input_tokens + session.total_output_tokens }
|
|
119
|
+
})
|
|
120
|
+
rescue => e
|
|
121
|
+
block.call({
|
|
122
|
+
type: "turn.failed", seq: 3,
|
|
123
|
+
payload: { "error" => { "message" => e.message }, "sessionId" => session_id }
|
|
124
|
+
})
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def get_events(session_id, after_seq:, limit: nil)
|
|
129
|
+
{ "events" => [] }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def respond(request_id, result)
|
|
133
|
+
# No reverse requests in basic mode
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def get_workspace_state(workspace_path)
|
|
137
|
+
# No workspace state to report
|
|
138
|
+
{}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Build an AskAgent adapter from config.
|
|
142
|
+
# Reads ASK_AGENT_MODEL, ASK_AGENT_LLM_PROVIDER, ASK_AGENT_MAX_TURNS from ENV.
|
|
143
|
+
def self.from_config(model: nil, llm_provider: nil, max_turns: nil, **)
|
|
144
|
+
new(
|
|
145
|
+
model: model || ENV.fetch("ASK_AGENT_MODEL", "deepseek-v4-flash"),
|
|
146
|
+
provider: llm_provider || ENV.fetch("ASK_AGENT_LLM_PROVIDER", "opencode_go"),
|
|
147
|
+
max_turns: (max_turns || ENV.fetch("ASK_AGENT_MAX_TURNS", "10")).to_i
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def ensure_started
|
|
154
|
+
raise "Adapter not started. Call #start first." unless @started
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def build_chat
|
|
158
|
+
chat = Ask::Agent::Chat.new(
|
|
159
|
+
model: @model_id,
|
|
160
|
+
provider: @provider_slug,
|
|
161
|
+
tools: @tools
|
|
162
|
+
)
|
|
163
|
+
# Inject pre-configured provider to bypass Ask::Auth.resolve
|
|
164
|
+
chat.instance_variable_set(:@provider, @provider)
|
|
165
|
+
chat
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def run_session(session_id, content)
|
|
169
|
+
chat = build_chat
|
|
170
|
+
session = Ask::Agent::Session.new(
|
|
171
|
+
model: chat,
|
|
172
|
+
max_turns: @max_turns,
|
|
173
|
+
**@session_opts
|
|
174
|
+
)
|
|
175
|
+
session.run(content)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
Ask::CodingProviders.register_adapter(:ask_agent, Ask::CodingProviders::AskAgent::Adapter)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ask_agent/adapter"
|
|
4
|
+
|
|
5
|
+
module Ask
|
|
6
|
+
module CodingProviders
|
|
7
|
+
# AskAgent coding provider — uses Ask::Agent::Session in-process.
|
|
8
|
+
#
|
|
9
|
+
# No app-server, no external binary. Sessions run in the same process.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# adapter = Ask::CodingProviders::AskAgent::Adapter.new(
|
|
13
|
+
# model: "deepseek-v4-flash",
|
|
14
|
+
# provider: "opencode_go"
|
|
15
|
+
# )
|
|
16
|
+
module AskAgent
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|