Skip to content

Commit bbba9b9

Browse files
committed
ACP WIP
1 parent a838122 commit bbba9b9

12 files changed

Lines changed: 1438 additions & 5 deletions

File tree

bin/assets/plugins/aiassistant.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,5 +501,26 @@
501501
"fetch_models_url": "http://localhost:8080/api/tags",
502502
"open_api": true
503503
}
504+
},
505+
"agents": {
506+
"gemini-cli": {
507+
"enabled": true,
508+
"command": "gemini",
509+
"args": ["--experimental-acp"]
510+
},
511+
"opencode": {
512+
"enabled": true,
513+
"command": "opencode",
514+
"args": ["acp"]
515+
},
516+
"claude-agent": {
517+
"enabled": true,
518+
"command": "claude-agent-acp"
519+
},
520+
"cursor": {
521+
"enabled": true,
522+
"command": "cursor",
523+
"args": ["acp"]
524+
}
504525
}
505526
}
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
#include "acpclient.hpp"
2+
#include <eepp/system/log.hpp>
3+
4+
namespace ecode { namespace acp {
5+
6+
ACPClient::ACPClient( std::shared_ptr<ThreadPool> threadPool, const Config& config ) :
7+
mThreadPool( threadPool ), mConfig( config ) {}
8+
9+
ACPClient::~ACPClient() {
10+
stop();
11+
}
12+
13+
bool ACPClient::start() {
14+
auto flags = Process::getDefaultOptions() | Process::EnableAsync | Process::UseAbsolutePath;
15+
bool ret = mProcess.create( mConfig.command, mConfig.args, flags, mConfig.environment,
16+
mConfig.workingDirectory );
17+
if ( ret && mProcess.isAlive() ) {
18+
mProcess.startAsyncRead(
19+
[this]( const char* bytes, size_t n ) { readStdOut( bytes, n ); },
20+
[this]( const char* bytes, size_t n ) { readStdErr( bytes, n ); } );
21+
return true;
22+
}
23+
return false;
24+
}
25+
26+
void ACPClient::stop() {
27+
mShuttingDown = true;
28+
if ( mProcess.isAlive() ) {
29+
mProcess.kill();
30+
}
31+
}
32+
33+
bool ACPClient::isRunning() {
34+
return !mShuttingDown && mProcess.isAlive();
35+
}
36+
37+
bool ACPClient::isReady() {
38+
return mReady;
39+
}
40+
41+
void ACPClient::readStdOut( const char* bytes, size_t n ) {
42+
mReceiveBuffer.append( bytes, n );
43+
size_t pos;
44+
while ( ( pos = mReceiveBuffer.find( '\n' ) ) != std::string::npos ) {
45+
std::string line = mReceiveBuffer.substr( 0, pos );
46+
mReceiveBuffer.erase( 0, pos + 1 );
47+
if ( line.empty() || line == "\r" )
48+
continue;
49+
50+
try {
51+
json msg = json::parse( line );
52+
Log::debug( "ACPClient RECV: %s", line.c_str() );
53+
processMessage( msg );
54+
} catch ( const json::exception& e ) {
55+
Log::error( "ACPClient JSON parse error: %s\nLine: %s", e.what(), line.c_str() );
56+
}
57+
}
58+
}
59+
60+
void ACPClient::readStdErr( const char* bytes, size_t n ) {
61+
std::string err( bytes, n );
62+
Log::debug( "ACPClient stderr: %s", err.c_str() );
63+
}
64+
65+
void ACPClient::processMessage( const json& msg ) {
66+
if ( msg.contains( "method" ) ) {
67+
if ( msg.contains( "id" ) ) {
68+
processRequest( msg );
69+
} else {
70+
processNotification( msg );
71+
}
72+
} else if ( msg.contains( "result" ) || msg.contains( "error" ) ) {
73+
processResponse( msg );
74+
}
75+
}
76+
77+
void ACPClient::processRequest( const json& msg ) {
78+
std::string method = msg.value( "method", "" );
79+
json id = msg["id"];
80+
81+
if ( method == "fs/read_text_file" && onReadTextFile ) {
82+
ReadTextFileRequest req( msg.value( "params", json::object() ) );
83+
onReadTextFile( req, [this, id]( const ReadTextFileResponse& res ) {
84+
sendResponse( id, res.toJson() );
85+
} );
86+
} else if ( method == "fs/write_text_file" && onWriteTextFile ) {
87+
WriteTextFileRequest req( msg.value( "params", json::object() ) );
88+
onWriteTextFile( req, [this, id]( const WriteTextFileResponse& res ) {
89+
sendResponse( id, res.toJson() );
90+
} );
91+
} else if ( method == "session/request_permission" && onRequestPermission ) {
92+
RequestPermissionRequest req( msg.value( "params", json::object() ) );
93+
onRequestPermission( req, [this, id]( const RequestPermissionResponse& res ) {
94+
sendResponse( id, res.toJson() );
95+
} );
96+
} else if ( method == "terminal/create" && onCreateTerminal ) {
97+
CreateTerminalRequest req( msg.value( "params", json::object() ) );
98+
onCreateTerminal( req, [this, id]( const CreateTerminalResponse& res ) {
99+
sendResponse( id, res.toJson() );
100+
} );
101+
} else if ( method == "terminal/output" && onTerminalOutput ) {
102+
TerminalOutputRequest req( msg.value( "params", json::object() ) );
103+
onTerminalOutput( req, [this, id]( const TerminalOutputResponse& res ) {
104+
sendResponse( id, res.toJson() );
105+
} );
106+
} else if ( method == "terminal/kill" && onKillTerminal ) {
107+
KillTerminalRequest req( msg.value( "params", json::object() ) );
108+
onKillTerminal( req, [this, id]( const KillTerminalResponse& res ) {
109+
sendResponse( id, res.toJson() );
110+
} );
111+
} else if ( method == "terminal/release" && onReleaseTerminal ) {
112+
ReleaseTerminalRequest req( msg.value( "params", json::object() ) );
113+
onReleaseTerminal( req, [this, id]( const ReleaseTerminalResponse& res ) {
114+
sendResponse( id, res.toJson() );
115+
} );
116+
} else if ( method == "terminal/wait_for_exit" && onWaitForTerminalExit ) {
117+
WaitForTerminalExitRequest req( msg.value( "params", json::object() ) );
118+
onWaitForTerminalExit( req, [this, id]( const WaitForTerminalExitResponse& res ) {
119+
sendResponse( id, res.toJson() );
120+
} );
121+
} else {
122+
sendError( id, -32601, "Method not found: " + method );
123+
}
124+
}
125+
126+
void ACPClient::processNotification( const json& msg ) {
127+
std::string method = msg.value( "method", "" );
128+
if ( method == "session/update" && onSessionUpdate ) {
129+
auto params = msg.value( "params", json::object() );
130+
if ( params.contains( "update" ) ) {
131+
onSessionUpdate( params["update"] );
132+
} else {
133+
onSessionUpdate( params ); // Fallback if schema shifts or malformed
134+
}
135+
}
136+
}
137+
138+
void ACPClient::processResponse( const json& msg ) {
139+
if ( !msg.contains( "id" ) || !msg["id"].is_number_integer() )
140+
return;
141+
IdType id = msg["id"].get<IdType>();
142+
143+
JsonReplyHandler handler;
144+
{
145+
Lock l( mHandlersMutex );
146+
auto it = mHandlers.find( id );
147+
if ( it != mHandlers.end() ) {
148+
handler = it->second;
149+
mHandlers.erase( it );
150+
}
151+
}
152+
153+
if ( handler ) {
154+
handler( id, msg );
155+
}
156+
}
157+
158+
int ACPClient::write( json&& msg, const JsonReplyHandler& h ) {
159+
msg["jsonrpc"] = "2.0";
160+
int msgId = 0;
161+
162+
if ( h ) {
163+
msgId = ++mLastMsgId;
164+
msg["id"] = msgId;
165+
Lock l( mHandlersMutex );
166+
mHandlers[msgId] = h;
167+
}
168+
169+
std::string out = msg.dump() + "\n";
170+
if ( isRunning() ) {
171+
Log::debug( "ACPClient SEND: %s", out.c_str() );
172+
mProcess.write( out );
173+
}
174+
return msgId;
175+
}
176+
177+
void ACPClient::sendResponse( const json& id, json&& result ) {
178+
json msg = { { "jsonrpc", "2.0" }, { "id", id }, { "result", result } };
179+
std::string out = msg.dump() + "\n";
180+
if ( isRunning() ) {
181+
Log::debug( "ACPClient SEND: %s", out.c_str() );
182+
mProcess.write( out );
183+
}
184+
}
185+
186+
void ACPClient::sendError( const json& id, int code, const std::string& message ) {
187+
json msg = { { "jsonrpc", "2.0" },
188+
{ "id", id },
189+
{ "error", { { "code", code }, { "message", message } } } };
190+
std::string out = msg.dump() + "\n";
191+
if ( isRunning() ) {
192+
Log::debug( "ACPClient SEND: %s", out.c_str() );
193+
mProcess.write( out );
194+
}
195+
}
196+
197+
void ACPClient::initialize( const InitializeRequest& req,
198+
const std::function<void( const InitializeResponse& )>& cb ) {
199+
write( { { "method", "initialize" }, { "params", req.toJson() } },
200+
[this, cb]( const IdType&, const json& resp ) {
201+
if ( resp.contains( "result" ) ) {
202+
mReady = true;
203+
if ( cb )
204+
cb( InitializeResponse( resp["result"] ) );
205+
}
206+
} );
207+
}
208+
209+
void ACPClient::newSession( const NewSessionRequest& req,
210+
const std::function<void( const NewSessionResponse& )>& cb ) {
211+
write( { { "method", "session/new" }, { "params", req.toJson() } },
212+
[cb]( const IdType&, const json& resp ) {
213+
if ( resp.contains( "result" ) && cb ) {
214+
cb( NewSessionResponse( resp["result"] ) );
215+
}
216+
} );
217+
}
218+
219+
void ACPClient::prompt( const PromptRequest& req,
220+
const std::function<void( const PromptResponse& )>& cb ) {
221+
write( { { "method", "session/prompt" }, { "params", req.toJson() } },
222+
[cb]( const IdType&, const json& resp ) {
223+
if ( resp.contains( "result" ) && cb ) {
224+
cb( PromptResponse( resp["result"] ) );
225+
}
226+
} );
227+
}
228+
229+
void ACPClient::cancel( const std::string& sessionId ) {
230+
write( { { "method", "session/cancel" }, { "params", { { "sessionId", sessionId } } } } );
231+
}
232+
233+
}} // namespace ecode::acp
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#pragma once
2+
3+
#include "acpprotocol.hpp"
4+
#include <atomic>
5+
#include <eepp/system/clock.hpp>
6+
#include <eepp/system/mutex.hpp>
7+
#include <eepp/system/process.hpp>
8+
#include <eepp/system/threadpool.hpp>
9+
#include <functional>
10+
#include <map>
11+
#include <memory>
12+
#include <nlohmann/json.hpp>
13+
#include <string>
14+
#include <vector>
15+
16+
using json = nlohmann::json;
17+
18+
using namespace EE;
19+
using namespace EE::System;
20+
21+
namespace ecode { namespace acp {
22+
23+
class ACPClient {
24+
public:
25+
using IdType = int;
26+
using JsonReplyHandler = std::function<void( const IdType& id, const json& )>;
27+
28+
struct Config {
29+
std::string command;
30+
std::vector<std::string> args;
31+
std::string workingDirectory;
32+
std::unordered_map<std::string, std::string> environment;
33+
};
34+
35+
ACPClient( std::shared_ptr<ThreadPool> threadPool, const Config& config );
36+
~ACPClient();
37+
38+
bool start();
39+
void stop();
40+
41+
bool isRunning();
42+
bool isReady();
43+
44+
const Config& getConfig() const { return mConfig; }
45+
46+
void initialize( const InitializeRequest& req,
47+
const std::function<void( const InitializeResponse& )>& cb );
48+
void newSession( const NewSessionRequest& req,
49+
const std::function<void( const NewSessionResponse& )>& cb );
50+
void prompt( const PromptRequest& req, const std::function<void( const PromptResponse& )>& cb );
51+
52+
// Notifications to agent
53+
void cancel( const std::string& sessionId );
54+
55+
// Callbacks from agent
56+
std::function<void( const json& )> onSessionUpdate;
57+
std::function<void( const ReadTextFileRequest&,
58+
std::function<void( const ReadTextFileResponse& )> )>
59+
onReadTextFile;
60+
std::function<void( const WriteTextFileRequest&,
61+
std::function<void( const WriteTextFileResponse& )> )>
62+
onWriteTextFile;
63+
std::function<void( const RequestPermissionRequest&,
64+
std::function<void( const RequestPermissionResponse& )> )>
65+
onRequestPermission;
66+
std::function<void( const CreateTerminalRequest&,
67+
std::function<void( const CreateTerminalResponse& )> )>
68+
onCreateTerminal;
69+
std::function<void( const TerminalOutputRequest&,
70+
std::function<void( const TerminalOutputResponse& )> )>
71+
onTerminalOutput;
72+
std::function<void( const KillTerminalRequest&,
73+
std::function<void( const KillTerminalResponse& )> )>
74+
onKillTerminal;
75+
std::function<void( const ReleaseTerminalRequest&,
76+
std::function<void( const ReleaseTerminalResponse& )> )>
77+
onReleaseTerminal;
78+
std::function<void( const WaitForTerminalExitRequest&,
79+
std::function<void( const WaitForTerminalExitResponse& )> )>
80+
onWaitForTerminalExit;
81+
82+
protected:
83+
std::shared_ptr<ThreadPool> mThreadPool;
84+
Config mConfig;
85+
Process mProcess;
86+
bool mReady{ false };
87+
bool mShuttingDown{ false };
88+
std::atomic<int> mLastMsgId{ 0 };
89+
90+
Mutex mHandlersMutex;
91+
std::map<IdType, JsonReplyHandler> mHandlers;
92+
93+
std::string mReceiveBuffer;
94+
95+
void readStdOut( const char* bytes, size_t n );
96+
void readStdErr( const char* bytes, size_t n );
97+
98+
void processMessage( const json& msg );
99+
void processRequest( const json& msg );
100+
void processNotification( const json& msg );
101+
void processResponse( const json& msg );
102+
103+
int write( json&& msg, const JsonReplyHandler& h = nullptr );
104+
void sendResponse( const json& id, json&& result );
105+
void sendError( const json& id, int code, const std::string& message );
106+
};
107+
108+
}} // namespace ecode::acp

0 commit comments

Comments
 (0)