acp_ruby 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/CHANGELOG.md +19 -0
- data/LICENSE.txt +21 -0
- data/README.md +125 -0
- data/lib/acp_ruby.rb +3 -0
- data/lib/agent_client_protocol/agent/connection.rb +125 -0
- data/lib/agent_client_protocol/agent/router.rb +62 -0
- data/lib/agent_client_protocol/agent.rb +49 -0
- data/lib/agent_client_protocol/client/connection.rb +112 -0
- data/lib/agent_client_protocol/client/router.rb +85 -0
- data/lib/agent_client_protocol/client.rb +53 -0
- data/lib/agent_client_protocol/connection.rb +144 -0
- data/lib/agent_client_protocol/contrib/permission_broker.rb +57 -0
- data/lib/agent_client_protocol/contrib/session_accumulator.rb +160 -0
- data/lib/agent_client_protocol/contrib/tool_call_tracker.rb +115 -0
- data/lib/agent_client_protocol/error.rb +41 -0
- data/lib/agent_client_protocol/helpers.rb +176 -0
- data/lib/agent_client_protocol/meta.rb +30 -0
- data/lib/agent_client_protocol/router.rb +108 -0
- data/lib/agent_client_protocol/schema/base_model.rb +158 -0
- data/lib/agent_client_protocol/schema/generated.rb +508 -0
- data/lib/agent_client_protocol/schema/types.rb +200 -0
- data/lib/agent_client_protocol/stdio.rb +129 -0
- data/lib/agent_client_protocol/transport.rb +70 -0
- data/lib/agent_client_protocol/version.rb +5 -0
- data/lib/agent_client_protocol.rb +54 -0
- data/schema/VERSION +1 -0
- data/schema/meta.json +24 -0
- data/schema/schema.json +3430 -0
- metadata +103 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async"
|
|
4
|
+
|
|
5
|
+
module AgentClientProtocol
|
|
6
|
+
module Stdio
|
|
7
|
+
SHUTDOWN_TIMEOUT = 5 # seconds
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def run_agent(agent)
|
|
12
|
+
Async do
|
|
13
|
+
reader = Transport::NdjsonReader.new($stdin)
|
|
14
|
+
writer = Transport::NdjsonWriter.new($stdout)
|
|
15
|
+
|
|
16
|
+
# Redirect any puts/print to stderr so stdout is reserved for protocol
|
|
17
|
+
$stdout = $stderr
|
|
18
|
+
|
|
19
|
+
conn = Agent::Connection.new(agent, reader, writer)
|
|
20
|
+
conn.listen
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def spawn_agent_process(client, command, *args, env: nil, cwd: nil)
|
|
25
|
+
Async do |task|
|
|
26
|
+
pid, stdin_w, stdout_r = spawn_process(command, *args, env: env, cwd: cwd)
|
|
27
|
+
|
|
28
|
+
reader = Transport::NdjsonReader.new(stdout_r)
|
|
29
|
+
writer = Transport::NdjsonWriter.new(stdin_w)
|
|
30
|
+
conn = Client::Connection.new(client, reader, writer)
|
|
31
|
+
|
|
32
|
+
listen_task = task.async { conn.listen }
|
|
33
|
+
|
|
34
|
+
if block_given?
|
|
35
|
+
begin
|
|
36
|
+
yield conn, pid
|
|
37
|
+
ensure
|
|
38
|
+
shutdown_process(conn, pid, stdin_w, listen_task)
|
|
39
|
+
end
|
|
40
|
+
else
|
|
41
|
+
[conn, pid, listen_task]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def spawn_client_process(agent, command, *args, env: nil, cwd: nil)
|
|
47
|
+
Async do |task|
|
|
48
|
+
pid, stdin_w, stdout_r = spawn_process(command, *args, env: env, cwd: cwd)
|
|
49
|
+
|
|
50
|
+
reader = Transport::NdjsonReader.new(stdout_r)
|
|
51
|
+
writer = Transport::NdjsonWriter.new(stdin_w)
|
|
52
|
+
conn = Agent::Connection.new(agent, reader, writer)
|
|
53
|
+
|
|
54
|
+
listen_task = task.async { conn.listen }
|
|
55
|
+
|
|
56
|
+
if block_given?
|
|
57
|
+
begin
|
|
58
|
+
yield conn, pid
|
|
59
|
+
ensure
|
|
60
|
+
shutdown_process(conn, pid, stdin_w, listen_task)
|
|
61
|
+
end
|
|
62
|
+
else
|
|
63
|
+
[conn, pid, listen_task]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def spawn_process(command, *args, env: nil, cwd: nil)
|
|
69
|
+
spawn_env = clean_environment(env)
|
|
70
|
+
opts = {in: :pipe, out: :pipe, err: $stderr}
|
|
71
|
+
opts[:chdir] = cwd if cwd
|
|
72
|
+
|
|
73
|
+
stdin_r, stdin_w = IO.pipe
|
|
74
|
+
stdout_r, stdout_w = IO.pipe
|
|
75
|
+
|
|
76
|
+
pid = Process.spawn(spawn_env, command, *args, in: stdin_r, out: stdout_w, err: $stderr, **(cwd ? {chdir: cwd} : {}))
|
|
77
|
+
|
|
78
|
+
stdin_r.close
|
|
79
|
+
stdout_w.close
|
|
80
|
+
|
|
81
|
+
[pid, stdin_w, stdout_r]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def shutdown_process(conn, pid, stdin_w, listen_task)
|
|
85
|
+
listen_task&.stop
|
|
86
|
+
conn.close
|
|
87
|
+
|
|
88
|
+
# Close stdin to signal the subprocess
|
|
89
|
+
stdin_w.close unless stdin_w.closed?
|
|
90
|
+
|
|
91
|
+
# Use a thread for waitpid to avoid blocking the async reactor
|
|
92
|
+
reap_thread = Thread.new do
|
|
93
|
+
begin
|
|
94
|
+
Process.waitpid(pid)
|
|
95
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
|
96
|
+
# Already reaped
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
unless reap_thread.join(SHUTDOWN_TIMEOUT)
|
|
101
|
+
begin
|
|
102
|
+
Process.kill("TERM", pid)
|
|
103
|
+
rescue Errno::ESRCH
|
|
104
|
+
# Already gone
|
|
105
|
+
end
|
|
106
|
+
unless reap_thread.join(SHUTDOWN_TIMEOUT)
|
|
107
|
+
begin
|
|
108
|
+
Process.kill("KILL", pid)
|
|
109
|
+
rescue Errno::ESRCH
|
|
110
|
+
# Already gone
|
|
111
|
+
end
|
|
112
|
+
reap_thread.join(1)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
|
116
|
+
# Process already gone
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def clean_environment(extra = nil)
|
|
120
|
+
env = {}
|
|
121
|
+
# Minimal safe environment
|
|
122
|
+
%w[PATH HOME USER LANG TERM].each do |key|
|
|
123
|
+
env[key] = ENV[key] if ENV[key]
|
|
124
|
+
end
|
|
125
|
+
env.merge!(extra) if extra
|
|
126
|
+
env
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module AgentClientProtocol
|
|
6
|
+
module Transport
|
|
7
|
+
MAX_LINE_BYTES = 50 * 1024 * 1024 # 50MB
|
|
8
|
+
|
|
9
|
+
class NdjsonWriter
|
|
10
|
+
def initialize(io)
|
|
11
|
+
@io = io
|
|
12
|
+
@io.sync = true
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def write(message)
|
|
17
|
+
line = JSON.generate(message) + "\n"
|
|
18
|
+
@mutex.synchronize do
|
|
19
|
+
@io.write(line)
|
|
20
|
+
@io.flush
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def close
|
|
25
|
+
@io.close unless @io.closed?
|
|
26
|
+
rescue IOError
|
|
27
|
+
# already closed
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class NdjsonReader
|
|
32
|
+
def initialize(io, max_line_bytes: MAX_LINE_BYTES)
|
|
33
|
+
@io = io
|
|
34
|
+
@max_line_bytes = max_line_bytes
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def each
|
|
38
|
+
return enum_for(:each) unless block_given?
|
|
39
|
+
|
|
40
|
+
loop do
|
|
41
|
+
line = read_line
|
|
42
|
+
break if line.nil?
|
|
43
|
+
|
|
44
|
+
line = line.strip
|
|
45
|
+
next if line.empty?
|
|
46
|
+
|
|
47
|
+
begin
|
|
48
|
+
yield JSON.parse(line)
|
|
49
|
+
rescue JSON::ParserError => e
|
|
50
|
+
raise AgentClientProtocol::RequestError.parse_error(e.message)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def close
|
|
56
|
+
@io.close unless @io.closed?
|
|
57
|
+
rescue IOError
|
|
58
|
+
# already closed
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def read_line
|
|
64
|
+
@io.gets
|
|
65
|
+
rescue IOError, Errno::EBADF
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "agent_client_protocol/version"
|
|
4
|
+
require_relative "agent_client_protocol/meta"
|
|
5
|
+
|
|
6
|
+
module AgentClientProtocol
|
|
7
|
+
autoload :RequestError, "agent_client_protocol/error"
|
|
8
|
+
autoload :Transport, "agent_client_protocol/transport"
|
|
9
|
+
autoload :Connection, "agent_client_protocol/connection"
|
|
10
|
+
autoload :Router, "agent_client_protocol/router"
|
|
11
|
+
autoload :AgentInterface, "agent_client_protocol/agent"
|
|
12
|
+
autoload :ClientInterface, "agent_client_protocol/client"
|
|
13
|
+
autoload :Helpers, "agent_client_protocol/helpers"
|
|
14
|
+
|
|
15
|
+
module Schema
|
|
16
|
+
autoload :BaseModel, "agent_client_protocol/schema/base_model"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Load generated schema types
|
|
20
|
+
require_relative "agent_client_protocol/schema/generated"
|
|
21
|
+
|
|
22
|
+
module Agent
|
|
23
|
+
autoload :Connection, "agent_client_protocol/agent/connection"
|
|
24
|
+
autoload :Router, "agent_client_protocol/agent/router"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module Client
|
|
28
|
+
autoload :Connection, "agent_client_protocol/client/connection"
|
|
29
|
+
autoload :Router, "agent_client_protocol/client/router"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module Contrib
|
|
33
|
+
autoload :SessionAccumulator, "agent_client_protocol/contrib/session_accumulator"
|
|
34
|
+
autoload :ToolCallTracker, "agent_client_protocol/contrib/tool_call_tracker"
|
|
35
|
+
autoload :PermissionBroker, "agent_client_protocol/contrib/permission_broker"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
def run_agent(agent)
|
|
41
|
+
require_relative "agent_client_protocol/stdio"
|
|
42
|
+
Stdio.run_agent(agent)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def spawn_agent_process(client, command, *args, **opts, &block)
|
|
46
|
+
require_relative "agent_client_protocol/stdio"
|
|
47
|
+
Stdio.spawn_agent_process(client, command, *args, **opts, &block)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def spawn_client_process(agent, command, *args, **opts, &block)
|
|
51
|
+
require_relative "agent_client_protocol/stdio"
|
|
52
|
+
Stdio.spawn_client_process(agent, command, *args, **opts, &block)
|
|
53
|
+
end
|
|
54
|
+
end
|
data/schema/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
v0.10.8
|
data/schema/meta.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"agentMethods": {
|
|
3
|
+
"authenticate": "authenticate",
|
|
4
|
+
"initialize": "initialize",
|
|
5
|
+
"session_cancel": "session/cancel",
|
|
6
|
+
"session_load": "session/load",
|
|
7
|
+
"session_new": "session/new",
|
|
8
|
+
"session_prompt": "session/prompt",
|
|
9
|
+
"session_set_config_option": "session/set_config_option",
|
|
10
|
+
"session_set_mode": "session/set_mode"
|
|
11
|
+
},
|
|
12
|
+
"clientMethods": {
|
|
13
|
+
"fs_read_text_file": "fs/read_text_file",
|
|
14
|
+
"fs_write_text_file": "fs/write_text_file",
|
|
15
|
+
"session_request_permission": "session/request_permission",
|
|
16
|
+
"session_update": "session/update",
|
|
17
|
+
"terminal_create": "terminal/create",
|
|
18
|
+
"terminal_kill": "terminal/kill",
|
|
19
|
+
"terminal_output": "terminal/output",
|
|
20
|
+
"terminal_release": "terminal/release",
|
|
21
|
+
"terminal_wait_for_exit": "terminal/wait_for_exit"
|
|
22
|
+
},
|
|
23
|
+
"version": 1
|
|
24
|
+
}
|