acp_sdk_async 0.2.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +53 -0
- data/README.md +2 -0
- data/lib/acp/agent.rb +47 -5
- data/lib/acp/client.rb +4 -1
- data/lib/acp/connection.rb +66 -27
- data/lib/acp/contrib/permissions.rb +74 -0
- data/lib/acp/contrib/session_state.rb +230 -0
- data/lib/acp/contrib/tool_calls.rb +240 -0
- data/lib/acp/exceptions.rb +8 -8
- data/lib/acp/router.rb +18 -15
- data/lib/acp/schema_base.rb +64 -20
- data/lib/acp/stdio.rb +1 -1
- data/lib/acp/transport.rb +40 -11
- data/lib/acp/version.rb +1 -1
- data/lib/acp/wait.rb +37 -5
- data/lib/acp_sdk_async.rb +3 -0
- metadata +5 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../schema"
|
|
4
|
+
require_relative "tool_calls"
|
|
5
|
+
|
|
6
|
+
module ACP
|
|
7
|
+
module Contrib
|
|
8
|
+
class SessionNotificationMismatchError < ArgumentError
|
|
9
|
+
def initialize(expected, actual)
|
|
10
|
+
super("SessionAccumulator received notification for #{actual}, expected #{expected}")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class SessionSnapshotUnavailableError < RuntimeError
|
|
15
|
+
def initialize
|
|
16
|
+
super("SessionAccumulator has not processed any notifications yet")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Immutable view of a tool call in the session.
|
|
21
|
+
class ToolCallView
|
|
22
|
+
attr_reader :tool_call_id, :title, :kind, :status,
|
|
23
|
+
:content, :locations, :raw_input, :raw_output
|
|
24
|
+
|
|
25
|
+
def initialize(tool_call_id:, title:, kind:, status:,
|
|
26
|
+
content:, locations:, raw_input:, raw_output:)
|
|
27
|
+
@tool_call_id = tool_call_id
|
|
28
|
+
@title = title
|
|
29
|
+
@kind = kind
|
|
30
|
+
@status = status
|
|
31
|
+
@content = content&.freeze
|
|
32
|
+
@locations = locations&.freeze
|
|
33
|
+
@raw_input = raw_input
|
|
34
|
+
@raw_output = raw_output
|
|
35
|
+
freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def ==(other)
|
|
39
|
+
other.instance_of?(self.class) && to_h == other.to_h
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
alias eql? ==
|
|
43
|
+
|
|
44
|
+
def hash
|
|
45
|
+
[self.class, to_h].hash
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_h
|
|
49
|
+
{
|
|
50
|
+
tool_call_id: @tool_call_id, title: @title, kind: @kind,
|
|
51
|
+
status: @status, content: @content, locations: @locations,
|
|
52
|
+
raw_input: @raw_input, raw_output: @raw_output
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Aggregated immutable snapshot of the most recent session state.
|
|
58
|
+
class SessionSnapshot
|
|
59
|
+
attr_reader :session_id, :tool_calls, :plan_entries, :current_mode_id,
|
|
60
|
+
:available_commands, :user_messages, :agent_messages, :agent_thoughts
|
|
61
|
+
|
|
62
|
+
def initialize(session_id:, tool_calls:, plan_entries:, current_mode_id:,
|
|
63
|
+
available_commands:, user_messages:, agent_messages:, agent_thoughts:)
|
|
64
|
+
@session_id = session_id
|
|
65
|
+
@tool_calls = tool_calls.freeze
|
|
66
|
+
@plan_entries = plan_entries.freeze
|
|
67
|
+
@current_mode_id = current_mode_id
|
|
68
|
+
@available_commands = available_commands.freeze
|
|
69
|
+
@user_messages = user_messages.freeze
|
|
70
|
+
@agent_messages = agent_messages.freeze
|
|
71
|
+
@agent_thoughts = agent_thoughts.freeze
|
|
72
|
+
freeze
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
class MutableToolCallState
|
|
77
|
+
attr_reader :tool_call_id
|
|
78
|
+
|
|
79
|
+
def initialize(tool_call_id)
|
|
80
|
+
@tool_call_id = tool_call_id
|
|
81
|
+
@title = nil
|
|
82
|
+
@kind = nil
|
|
83
|
+
@status = nil
|
|
84
|
+
@content = nil
|
|
85
|
+
@locations = nil
|
|
86
|
+
@raw_input = nil
|
|
87
|
+
@raw_output = nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def apply_start(update)
|
|
91
|
+
@title = update.title
|
|
92
|
+
@kind = update.kind
|
|
93
|
+
@status = update.status
|
|
94
|
+
@content = Contrib.copy_model_list(update.content)
|
|
95
|
+
@locations = Contrib.copy_model_list(update.locations)
|
|
96
|
+
@raw_input = update.raw_input
|
|
97
|
+
@raw_output = update.raw_output
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def apply_progress(update)
|
|
101
|
+
@title = update.title unless update.title.nil?
|
|
102
|
+
@kind = update.kind unless update.kind.nil?
|
|
103
|
+
@status = update.status unless update.status.nil?
|
|
104
|
+
@content = Contrib.copy_model_list(update.content) unless update.content.nil?
|
|
105
|
+
@locations = Contrib.copy_model_list(update.locations) unless update.locations.nil?
|
|
106
|
+
@raw_input = update.raw_input unless update.raw_input.nil?
|
|
107
|
+
@raw_output = update.raw_output unless update.raw_output.nil?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def snapshot
|
|
111
|
+
ToolCallView.new(
|
|
112
|
+
tool_call_id: @tool_call_id,
|
|
113
|
+
title: @title,
|
|
114
|
+
kind: @kind,
|
|
115
|
+
status: @status,
|
|
116
|
+
content: @content&.map { |item| Contrib.deep_copy_model(item) },
|
|
117
|
+
locations: @locations&.map { |loc| Contrib.deep_copy_model(loc) },
|
|
118
|
+
raw_input: @raw_input,
|
|
119
|
+
raw_output: @raw_output
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Merges SessionNotification objects into a session snapshot.
|
|
125
|
+
# Experimental: APIs may change while feedback is gathered.
|
|
126
|
+
class SessionAccumulator
|
|
127
|
+
attr_reader :session_id
|
|
128
|
+
|
|
129
|
+
def initialize(auto_reset_on_session_change: true)
|
|
130
|
+
@auto_reset = auto_reset_on_session_change
|
|
131
|
+
@session_id = nil
|
|
132
|
+
@tool_calls = {}
|
|
133
|
+
@plan_entries = []
|
|
134
|
+
@current_mode_id = nil
|
|
135
|
+
@available_commands = []
|
|
136
|
+
@user_messages = []
|
|
137
|
+
@agent_messages = []
|
|
138
|
+
@agent_thoughts = []
|
|
139
|
+
@subscribers = []
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def reset
|
|
143
|
+
@session_id = nil
|
|
144
|
+
@tool_calls.clear
|
|
145
|
+
@plan_entries.clear
|
|
146
|
+
@current_mode_id = nil
|
|
147
|
+
@available_commands.clear
|
|
148
|
+
@user_messages.clear
|
|
149
|
+
@agent_messages.clear
|
|
150
|
+
@agent_thoughts.clear
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Registers a callback invoked with (snapshot, notification) after
|
|
154
|
+
# every apply. Returns an unsubscribe lambda.
|
|
155
|
+
def subscribe(&callback)
|
|
156
|
+
@subscribers << callback
|
|
157
|
+
-> { @subscribers.delete(callback) }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def apply(notification)
|
|
161
|
+
ensure_session(notification)
|
|
162
|
+
apply_update(notification.update)
|
|
163
|
+
snap = snapshot
|
|
164
|
+
@subscribers.dup.each { |callback| callback.call(snap, notification) }
|
|
165
|
+
snap
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def snapshot
|
|
169
|
+
raise SessionSnapshotUnavailableError if @session_id.nil?
|
|
170
|
+
|
|
171
|
+
SessionSnapshot.new(
|
|
172
|
+
session_id: @session_id,
|
|
173
|
+
tool_calls: @tool_calls.transform_values(&:snapshot),
|
|
174
|
+
plan_entries: @plan_entries.map { |entry| Contrib.deep_copy_model(entry) },
|
|
175
|
+
current_mode_id: @current_mode_id,
|
|
176
|
+
available_commands: @available_commands.map { |command| Contrib.deep_copy_model(command) },
|
|
177
|
+
user_messages: @user_messages.map { |message| Contrib.deep_copy_model(message) },
|
|
178
|
+
agent_messages: @agent_messages.map { |message| Contrib.deep_copy_model(message) },
|
|
179
|
+
agent_thoughts: @agent_thoughts.map { |message| Contrib.deep_copy_model(message) }
|
|
180
|
+
)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
def ensure_session(notification)
|
|
186
|
+
if @session_id.nil?
|
|
187
|
+
@session_id = notification.session_id
|
|
188
|
+
return
|
|
189
|
+
end
|
|
190
|
+
handle_session_change(notification.session_id) if notification.session_id != @session_id
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def handle_session_change(session_id)
|
|
194
|
+
if @session_id.nil?
|
|
195
|
+
@session_id = session_id
|
|
196
|
+
return
|
|
197
|
+
end
|
|
198
|
+
raise SessionNotificationMismatchError.new(@session_id, session_id) unless @auto_reset
|
|
199
|
+
|
|
200
|
+
reset
|
|
201
|
+
@session_id = session_id
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def apply_update(update)
|
|
205
|
+
case update
|
|
206
|
+
when Schema::ToolCallStart
|
|
207
|
+
state_for(update.tool_call_id).apply_start(update)
|
|
208
|
+
when Schema::ToolCallProgress
|
|
209
|
+
state_for(update.tool_call_id).apply_progress(update)
|
|
210
|
+
when Schema::AgentPlanUpdate
|
|
211
|
+
@plan_entries = Contrib.copy_model_list(update.entries) || []
|
|
212
|
+
when Schema::CurrentModeUpdate
|
|
213
|
+
@current_mode_id = update.current_mode_id
|
|
214
|
+
when Schema::AvailableCommandsUpdate
|
|
215
|
+
@available_commands = Contrib.copy_model_list(update.available_commands) || []
|
|
216
|
+
when Schema::UserMessageChunk
|
|
217
|
+
@user_messages << Contrib.deep_copy_model(update)
|
|
218
|
+
when Schema::AgentMessageChunk
|
|
219
|
+
@agent_messages << Contrib.deep_copy_model(update)
|
|
220
|
+
when Schema::AgentThoughtChunk
|
|
221
|
+
@agent_thoughts << Contrib.deep_copy_model(update)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def state_for(tool_call_id)
|
|
226
|
+
@tool_calls[tool_call_id] ||= MutableToolCallState.new(tool_call_id)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require_relative "../schema"
|
|
5
|
+
|
|
6
|
+
module ACP
|
|
7
|
+
module Contrib
|
|
8
|
+
# Sentinel for optional parameters. Use `equal?` to test for it; never serialize it.
|
|
9
|
+
UNSET = Object.new.freeze
|
|
10
|
+
|
|
11
|
+
class MissingToolCallTitleError < ArgumentError
|
|
12
|
+
def initialize
|
|
13
|
+
super("title must be set before sending a ToolCallStart")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class UnknownToolCallError < KeyError
|
|
18
|
+
attr_reader :external_id
|
|
19
|
+
|
|
20
|
+
def initialize(external_id)
|
|
21
|
+
@external_id = external_id
|
|
22
|
+
super("Unknown tool call id: #{external_id}")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.deep_copy_model(value)
|
|
27
|
+
Marshal.load(Marshal.dump(value))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.copy_model_list(items)
|
|
31
|
+
return nil if items.nil?
|
|
32
|
+
|
|
33
|
+
items.map { |item| deep_copy_model(item) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Immutable view of a tracked tool call.
|
|
37
|
+
class TrackedToolCallView
|
|
38
|
+
attr_reader :tool_call_id, :title, :name, :kind, :status,
|
|
39
|
+
:content, :locations, :raw_input, :raw_output
|
|
40
|
+
|
|
41
|
+
def initialize(tool_call_id:, title:, name:, kind:, status:,
|
|
42
|
+
content:, locations:, raw_input:, raw_output:)
|
|
43
|
+
@tool_call_id = tool_call_id
|
|
44
|
+
@title = title
|
|
45
|
+
@name = name
|
|
46
|
+
@kind = kind
|
|
47
|
+
@status = status
|
|
48
|
+
@content = content&.freeze
|
|
49
|
+
@locations = locations&.freeze
|
|
50
|
+
@raw_input = raw_input
|
|
51
|
+
@raw_output = raw_output
|
|
52
|
+
freeze
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def ==(other)
|
|
56
|
+
other.instance_of?(self.class) && to_h == other.to_h
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
alias eql? ==
|
|
60
|
+
|
|
61
|
+
def hash
|
|
62
|
+
[self.class, to_h].hash
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def to_h
|
|
66
|
+
{
|
|
67
|
+
tool_call_id: @tool_call_id, title: @title, name: @name,
|
|
68
|
+
kind: @kind, status: @status, content: @content,
|
|
69
|
+
locations: @locations, raw_input: @raw_input, raw_output: @raw_output
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class TrackedToolCall
|
|
75
|
+
attr_reader :tool_call_id
|
|
76
|
+
|
|
77
|
+
def initialize(tool_call_id:, title: nil, name: nil, kind: nil, status: nil,
|
|
78
|
+
content: nil, locations: nil, raw_input: nil, raw_output: nil)
|
|
79
|
+
@tool_call_id = tool_call_id
|
|
80
|
+
@title = title
|
|
81
|
+
@name = name
|
|
82
|
+
@kind = kind
|
|
83
|
+
@status = status
|
|
84
|
+
@content = Contrib.copy_model_list(content)
|
|
85
|
+
@locations = Contrib.copy_model_list(locations)
|
|
86
|
+
@raw_input = raw_input
|
|
87
|
+
@raw_output = raw_output
|
|
88
|
+
@stream_buffer = nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def to_view
|
|
92
|
+
TrackedToolCallView.new(
|
|
93
|
+
tool_call_id: @tool_call_id,
|
|
94
|
+
title: @title,
|
|
95
|
+
name: @name,
|
|
96
|
+
kind: @kind,
|
|
97
|
+
status: @status,
|
|
98
|
+
content: @content&.map { |item| Contrib.deep_copy_model(item) },
|
|
99
|
+
locations: @locations&.map { |loc| Contrib.deep_copy_model(loc) },
|
|
100
|
+
raw_input: @raw_input,
|
|
101
|
+
raw_output: @raw_output
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def to_tool_call_model
|
|
106
|
+
Schema::ToolCallUpdate.new(
|
|
107
|
+
tool_call_id: @tool_call_id,
|
|
108
|
+
title: @title,
|
|
109
|
+
name: @name,
|
|
110
|
+
kind: @kind,
|
|
111
|
+
status: @status,
|
|
112
|
+
content: Contrib.copy_model_list(@content),
|
|
113
|
+
locations: Contrib.copy_model_list(@locations),
|
|
114
|
+
raw_input: @raw_input,
|
|
115
|
+
raw_output: @raw_output
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def to_start_model
|
|
120
|
+
raise MissingToolCallTitleError if @title.nil?
|
|
121
|
+
|
|
122
|
+
Schema::ToolCallStart.new(
|
|
123
|
+
tool_call_id: @tool_call_id,
|
|
124
|
+
title: @title,
|
|
125
|
+
name: @name,
|
|
126
|
+
kind: @kind,
|
|
127
|
+
status: @status,
|
|
128
|
+
content: Contrib.copy_model_list(@content),
|
|
129
|
+
locations: Contrib.copy_model_list(@locations),
|
|
130
|
+
raw_input: @raw_input,
|
|
131
|
+
raw_output: @raw_output
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def update(title: UNSET, name: UNSET, kind: UNSET, status: UNSET,
|
|
136
|
+
content: UNSET, locations: UNSET, raw_input: UNSET, raw_output: UNSET)
|
|
137
|
+
kwargs = { tool_call_id: @tool_call_id }
|
|
138
|
+
unless title.equal?(UNSET)
|
|
139
|
+
@title = title
|
|
140
|
+
kwargs[:title] = @title
|
|
141
|
+
end
|
|
142
|
+
unless name.equal?(UNSET)
|
|
143
|
+
@name = name
|
|
144
|
+
kwargs[:name] = @name
|
|
145
|
+
end
|
|
146
|
+
unless kind.equal?(UNSET)
|
|
147
|
+
@kind = kind
|
|
148
|
+
kwargs[:kind] = @kind
|
|
149
|
+
end
|
|
150
|
+
unless status.equal?(UNSET)
|
|
151
|
+
@status = status
|
|
152
|
+
kwargs[:status] = @status
|
|
153
|
+
end
|
|
154
|
+
unless content.equal?(UNSET)
|
|
155
|
+
@content = Contrib.copy_model_list(content)
|
|
156
|
+
kwargs[:content] = Contrib.copy_model_list(content)
|
|
157
|
+
end
|
|
158
|
+
unless locations.equal?(UNSET)
|
|
159
|
+
@locations = Contrib.copy_model_list(locations)
|
|
160
|
+
kwargs[:locations] = Contrib.copy_model_list(locations)
|
|
161
|
+
end
|
|
162
|
+
unless raw_input.equal?(UNSET)
|
|
163
|
+
@raw_input = raw_input
|
|
164
|
+
kwargs[:raw_input] = @raw_input
|
|
165
|
+
end
|
|
166
|
+
unless raw_output.equal?(UNSET)
|
|
167
|
+
@raw_output = raw_output
|
|
168
|
+
kwargs[:raw_output] = @raw_output
|
|
169
|
+
end
|
|
170
|
+
Schema::ToolCallProgress.new(**kwargs)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def append_stream_text(text, title: UNSET, status: UNSET)
|
|
174
|
+
@stream_buffer = "#{@stream_buffer}#{text}"
|
|
175
|
+
content = [Schema::ContentToolCallContent.new(content: Schema::TextContentBlock.new(text: @stream_buffer))]
|
|
176
|
+
update(title: title, status: status, content: content)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Utility for generating ACP tool call updates on the agent side.
|
|
181
|
+
class ToolCallTracker
|
|
182
|
+
def initialize(id_factory: nil)
|
|
183
|
+
@id_factory = id_factory || -> { SecureRandom.hex(16) }
|
|
184
|
+
@calls = {}
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def start(external_id, title:, name: nil, kind: nil, status: "in_progress",
|
|
188
|
+
content: nil, locations: nil, raw_input: nil, raw_output: nil)
|
|
189
|
+
state = TrackedToolCall.new(
|
|
190
|
+
tool_call_id: @id_factory.call,
|
|
191
|
+
title: title,
|
|
192
|
+
name: name,
|
|
193
|
+
kind: kind,
|
|
194
|
+
status: status,
|
|
195
|
+
content: content,
|
|
196
|
+
locations: locations,
|
|
197
|
+
raw_input: raw_input,
|
|
198
|
+
raw_output: raw_output
|
|
199
|
+
)
|
|
200
|
+
@calls[external_id] = state
|
|
201
|
+
state.to_start_model
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def progress(external_id, title: UNSET, name: UNSET, kind: UNSET, status: UNSET,
|
|
205
|
+
content: UNSET, locations: UNSET, raw_input: UNSET, raw_output: UNSET)
|
|
206
|
+
require_call(external_id).update(
|
|
207
|
+
title: title, name: name, kind: kind, status: status,
|
|
208
|
+
content: content, locations: locations,
|
|
209
|
+
raw_input: raw_input, raw_output: raw_output
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def append_stream_text(external_id, text, title: UNSET, status: UNSET)
|
|
214
|
+
require_call(external_id).append_stream_text(text, title: title, status: status)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def forget(external_id)
|
|
218
|
+
@calls.delete(external_id)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def tracked?(external_id)
|
|
222
|
+
@calls.key?(external_id)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def view(external_id)
|
|
226
|
+
require_call(external_id).to_view
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def tool_call_model(external_id)
|
|
230
|
+
require_call(external_id).to_tool_call_model
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
private
|
|
234
|
+
|
|
235
|
+
def require_call(external_id)
|
|
236
|
+
@calls.fetch(external_id) { raise UnknownToolCallError, external_id }
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
data/lib/acp/exceptions.rb
CHANGED
|
@@ -12,14 +12,14 @@ module ACP
|
|
|
12
12
|
@data = data
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def self.parse_error(data = nil)
|
|
16
|
-
def self.invalid_request(data = nil)
|
|
17
|
-
def self.method_not_found(method)
|
|
18
|
-
def self.invalid_params(data = nil)
|
|
19
|
-
def self.internal_error(data = nil)
|
|
20
|
-
def self.request_cancelled(data = nil)
|
|
21
|
-
def self.auth_required(data = nil)
|
|
22
|
-
def self.resource_not_found(uri = nil)
|
|
15
|
+
def self.parse_error(data = nil) = new(-32700, "Parse error", data)
|
|
16
|
+
def self.invalid_request(data = nil) = new(-32600, "Invalid request", data)
|
|
17
|
+
def self.method_not_found(method) = new(-32601, "Method not found", { "method" => method })
|
|
18
|
+
def self.invalid_params(data = nil) = new(-32602, "Invalid params", data)
|
|
19
|
+
def self.internal_error(data = nil) = new(-32603, "Internal error", data)
|
|
20
|
+
def self.request_cancelled(data = nil) = new(-32800, "Request cancelled", data)
|
|
21
|
+
def self.auth_required(data = nil) = new(-32000, "Authentication required", data)
|
|
22
|
+
def self.resource_not_found(uri = nil) = new(-32002, "Resource not found", uri ? { "uri" => uri } : nil)
|
|
23
23
|
|
|
24
24
|
def to_error_obj
|
|
25
25
|
{ "code" => @code, "message" => message, "data" => @data }
|
data/lib/acp/router.rb
CHANGED
|
@@ -24,7 +24,10 @@ module ACP
|
|
|
24
24
|
raise RequestError.method_not_found(@method)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
# Do NOT coerce nil -> {}: missing params must fail validation
|
|
28
|
+
# (invalid_params) instead of being silently replaced.
|
|
29
|
+
# Extension routes handle nil themselves (nil -> {} for wire compat).
|
|
30
|
+
argument = @model ? @model.coerce(params) : params
|
|
28
31
|
result = @handler.call(argument)
|
|
29
32
|
return result unless @kind == :request && @adapt_result
|
|
30
33
|
|
|
@@ -54,25 +57,25 @@ module ACP
|
|
|
54
57
|
def route_request(method, model, target, *names, optional: false, default_result: nil, normalize: false)
|
|
55
58
|
handler = resolve_handler(target, names)
|
|
56
59
|
add_route(Route.new(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
method: method,
|
|
61
|
+
handler: handler,
|
|
62
|
+
kind: :request,
|
|
63
|
+
model: model,
|
|
64
|
+
optional: optional,
|
|
65
|
+
default_result: default_result,
|
|
66
|
+
adapt_result: normalize ? NORMALIZE_RESULT : nil
|
|
67
|
+
))
|
|
65
68
|
end
|
|
66
69
|
|
|
67
70
|
def route_notification(method, model, target, *names)
|
|
68
71
|
handler = resolve_handler(target, names)
|
|
69
72
|
add_route(Route.new(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
method: method,
|
|
74
|
+
handler: handler,
|
|
75
|
+
kind: :notification,
|
|
76
|
+
model: model,
|
|
77
|
+
optional: true
|
|
78
|
+
))
|
|
76
79
|
end
|
|
77
80
|
|
|
78
81
|
def on_extension_request(&block)
|