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.
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentClientProtocol
4
+ VERSION = "0.1.0"
5
+ 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
+ }