mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-02 11:10:31 +03:00
ACP WIP
This commit is contained in:
@@ -501,5 +501,26 @@
|
|||||||
"fetch_models_url": "http://localhost:8080/api/tags",
|
"fetch_models_url": "http://localhost:8080/api/tags",
|
||||||
"open_api": true
|
"open_api": true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"agents": {
|
||||||
|
"gemini-cli": {
|
||||||
|
"enabled": true,
|
||||||
|
"command": "gemini",
|
||||||
|
"args": ["--experimental-acp"]
|
||||||
|
},
|
||||||
|
"opencode": {
|
||||||
|
"enabled": true,
|
||||||
|
"command": "opencode",
|
||||||
|
"args": ["acp"]
|
||||||
|
},
|
||||||
|
"claude-agent": {
|
||||||
|
"enabled": true,
|
||||||
|
"command": "claude-agent-acp"
|
||||||
|
},
|
||||||
|
"cursor": {
|
||||||
|
"enabled": true,
|
||||||
|
"command": "cursor",
|
||||||
|
"args": ["acp"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
#include "acpclient.hpp"
|
||||||
|
#include <eepp/system/log.hpp>
|
||||||
|
|
||||||
|
namespace ecode { namespace acp {
|
||||||
|
|
||||||
|
ACPClient::ACPClient( std::shared_ptr<ThreadPool> threadPool, const Config& config ) :
|
||||||
|
mThreadPool( threadPool ), mConfig( config ) {}
|
||||||
|
|
||||||
|
ACPClient::~ACPClient() {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ACPClient::start() {
|
||||||
|
auto flags = Process::getDefaultOptions() | Process::EnableAsync | Process::UseAbsolutePath;
|
||||||
|
bool ret = mProcess.create( mConfig.command, mConfig.args, flags, mConfig.environment,
|
||||||
|
mConfig.workingDirectory );
|
||||||
|
if ( ret && mProcess.isAlive() ) {
|
||||||
|
mProcess.startAsyncRead(
|
||||||
|
[this]( const char* bytes, size_t n ) { readStdOut( bytes, n ); },
|
||||||
|
[this]( const char* bytes, size_t n ) { readStdErr( bytes, n ); } );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ACPClient::stop() {
|
||||||
|
mShuttingDown = true;
|
||||||
|
if ( mProcess.isAlive() ) {
|
||||||
|
mProcess.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ACPClient::isRunning() {
|
||||||
|
return !mShuttingDown && mProcess.isAlive();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ACPClient::isReady() {
|
||||||
|
return mReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ACPClient::readStdOut( const char* bytes, size_t n ) {
|
||||||
|
mReceiveBuffer.append( bytes, n );
|
||||||
|
size_t pos;
|
||||||
|
while ( ( pos = mReceiveBuffer.find( '\n' ) ) != std::string::npos ) {
|
||||||
|
std::string line = mReceiveBuffer.substr( 0, pos );
|
||||||
|
mReceiveBuffer.erase( 0, pos + 1 );
|
||||||
|
if ( line.empty() || line == "\r" )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
json msg = json::parse( line );
|
||||||
|
Log::debug( "ACPClient RECV: %s", line.c_str() );
|
||||||
|
processMessage( msg );
|
||||||
|
} catch ( const json::exception& e ) {
|
||||||
|
Log::error( "ACPClient JSON parse error: %s\nLine: %s", e.what(), line.c_str() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ACPClient::readStdErr( const char* bytes, size_t n ) {
|
||||||
|
std::string err( bytes, n );
|
||||||
|
Log::debug( "ACPClient stderr: %s", err.c_str() );
|
||||||
|
}
|
||||||
|
|
||||||
|
void ACPClient::processMessage( const json& msg ) {
|
||||||
|
if ( msg.contains( "method" ) ) {
|
||||||
|
if ( msg.contains( "id" ) ) {
|
||||||
|
processRequest( msg );
|
||||||
|
} else {
|
||||||
|
processNotification( msg );
|
||||||
|
}
|
||||||
|
} else if ( msg.contains( "result" ) || msg.contains( "error" ) ) {
|
||||||
|
processResponse( msg );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ACPClient::processRequest( const json& msg ) {
|
||||||
|
std::string method = msg.value( "method", "" );
|
||||||
|
json id = msg["id"];
|
||||||
|
|
||||||
|
if ( method == "fs/read_text_file" && onReadTextFile ) {
|
||||||
|
ReadTextFileRequest req( msg.value( "params", json::object() ) );
|
||||||
|
onReadTextFile( req, [this, id]( const ReadTextFileResponse& res ) {
|
||||||
|
sendResponse( id, res.toJson() );
|
||||||
|
} );
|
||||||
|
} else if ( method == "fs/write_text_file" && onWriteTextFile ) {
|
||||||
|
WriteTextFileRequest req( msg.value( "params", json::object() ) );
|
||||||
|
onWriteTextFile( req, [this, id]( const WriteTextFileResponse& res ) {
|
||||||
|
sendResponse( id, res.toJson() );
|
||||||
|
} );
|
||||||
|
} else if ( method == "session/request_permission" && onRequestPermission ) {
|
||||||
|
RequestPermissionRequest req( msg.value( "params", json::object() ) );
|
||||||
|
onRequestPermission( req, [this, id]( const RequestPermissionResponse& res ) {
|
||||||
|
sendResponse( id, res.toJson() );
|
||||||
|
} );
|
||||||
|
} else if ( method == "terminal/create" && onCreateTerminal ) {
|
||||||
|
CreateTerminalRequest req( msg.value( "params", json::object() ) );
|
||||||
|
onCreateTerminal( req, [this, id]( const CreateTerminalResponse& res ) {
|
||||||
|
sendResponse( id, res.toJson() );
|
||||||
|
} );
|
||||||
|
} else if ( method == "terminal/output" && onTerminalOutput ) {
|
||||||
|
TerminalOutputRequest req( msg.value( "params", json::object() ) );
|
||||||
|
onTerminalOutput( req, [this, id]( const TerminalOutputResponse& res ) {
|
||||||
|
sendResponse( id, res.toJson() );
|
||||||
|
} );
|
||||||
|
} else if ( method == "terminal/kill" && onKillTerminal ) {
|
||||||
|
KillTerminalRequest req( msg.value( "params", json::object() ) );
|
||||||
|
onKillTerminal( req, [this, id]( const KillTerminalResponse& res ) {
|
||||||
|
sendResponse( id, res.toJson() );
|
||||||
|
} );
|
||||||
|
} else if ( method == "terminal/release" && onReleaseTerminal ) {
|
||||||
|
ReleaseTerminalRequest req( msg.value( "params", json::object() ) );
|
||||||
|
onReleaseTerminal( req, [this, id]( const ReleaseTerminalResponse& res ) {
|
||||||
|
sendResponse( id, res.toJson() );
|
||||||
|
} );
|
||||||
|
} else if ( method == "terminal/wait_for_exit" && onWaitForTerminalExit ) {
|
||||||
|
WaitForTerminalExitRequest req( msg.value( "params", json::object() ) );
|
||||||
|
onWaitForTerminalExit( req, [this, id]( const WaitForTerminalExitResponse& res ) {
|
||||||
|
sendResponse( id, res.toJson() );
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
sendError( id, -32601, "Method not found: " + method );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ACPClient::processNotification( const json& msg ) {
|
||||||
|
std::string method = msg.value( "method", "" );
|
||||||
|
if ( method == "session/update" && onSessionUpdate ) {
|
||||||
|
auto params = msg.value( "params", json::object() );
|
||||||
|
if ( params.contains( "update" ) ) {
|
||||||
|
onSessionUpdate( params["update"] );
|
||||||
|
} else {
|
||||||
|
onSessionUpdate( params ); // Fallback if schema shifts or malformed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ACPClient::processResponse( const json& msg ) {
|
||||||
|
if ( !msg.contains( "id" ) || !msg["id"].is_number_integer() )
|
||||||
|
return;
|
||||||
|
IdType id = msg["id"].get<IdType>();
|
||||||
|
|
||||||
|
JsonReplyHandler handler;
|
||||||
|
{
|
||||||
|
Lock l( mHandlersMutex );
|
||||||
|
auto it = mHandlers.find( id );
|
||||||
|
if ( it != mHandlers.end() ) {
|
||||||
|
handler = it->second;
|
||||||
|
mHandlers.erase( it );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( handler ) {
|
||||||
|
handler( id, msg );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int ACPClient::write( json&& msg, const JsonReplyHandler& h ) {
|
||||||
|
msg["jsonrpc"] = "2.0";
|
||||||
|
int msgId = 0;
|
||||||
|
|
||||||
|
if ( h ) {
|
||||||
|
msgId = ++mLastMsgId;
|
||||||
|
msg["id"] = msgId;
|
||||||
|
Lock l( mHandlersMutex );
|
||||||
|
mHandlers[msgId] = h;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string out = msg.dump() + "\n";
|
||||||
|
if ( isRunning() ) {
|
||||||
|
Log::debug( "ACPClient SEND: %s", out.c_str() );
|
||||||
|
mProcess.write( out );
|
||||||
|
}
|
||||||
|
return msgId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ACPClient::sendResponse( const json& id, json&& result ) {
|
||||||
|
json msg = { { "jsonrpc", "2.0" }, { "id", id }, { "result", result } };
|
||||||
|
std::string out = msg.dump() + "\n";
|
||||||
|
if ( isRunning() ) {
|
||||||
|
Log::debug( "ACPClient SEND: %s", out.c_str() );
|
||||||
|
mProcess.write( out );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ACPClient::sendError( const json& id, int code, const std::string& message ) {
|
||||||
|
json msg = { { "jsonrpc", "2.0" },
|
||||||
|
{ "id", id },
|
||||||
|
{ "error", { { "code", code }, { "message", message } } } };
|
||||||
|
std::string out = msg.dump() + "\n";
|
||||||
|
if ( isRunning() ) {
|
||||||
|
Log::debug( "ACPClient SEND: %s", out.c_str() );
|
||||||
|
mProcess.write( out );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ACPClient::initialize( const InitializeRequest& req,
|
||||||
|
const std::function<void( const InitializeResponse& )>& cb ) {
|
||||||
|
write( { { "method", "initialize" }, { "params", req.toJson() } },
|
||||||
|
[this, cb]( const IdType&, const json& resp ) {
|
||||||
|
if ( resp.contains( "result" ) ) {
|
||||||
|
mReady = true;
|
||||||
|
if ( cb )
|
||||||
|
cb( InitializeResponse( resp["result"] ) );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
void ACPClient::newSession( const NewSessionRequest& req,
|
||||||
|
const std::function<void( const NewSessionResponse& )>& cb ) {
|
||||||
|
write( { { "method", "session/new" }, { "params", req.toJson() } },
|
||||||
|
[cb]( const IdType&, const json& resp ) {
|
||||||
|
if ( resp.contains( "result" ) && cb ) {
|
||||||
|
cb( NewSessionResponse( resp["result"] ) );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
void ACPClient::prompt( const PromptRequest& req,
|
||||||
|
const std::function<void( const PromptResponse& )>& cb ) {
|
||||||
|
write( { { "method", "session/prompt" }, { "params", req.toJson() } },
|
||||||
|
[cb]( const IdType&, const json& resp ) {
|
||||||
|
if ( resp.contains( "result" ) && cb ) {
|
||||||
|
cb( PromptResponse( resp["result"] ) );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
void ACPClient::cancel( const std::string& sessionId ) {
|
||||||
|
write( { { "method", "session/cancel" }, { "params", { { "sessionId", sessionId } } } } );
|
||||||
|
}
|
||||||
|
|
||||||
|
}} // namespace ecode::acp
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "acpprotocol.hpp"
|
||||||
|
#include <atomic>
|
||||||
|
#include <eepp/system/clock.hpp>
|
||||||
|
#include <eepp/system/mutex.hpp>
|
||||||
|
#include <eepp/system/process.hpp>
|
||||||
|
#include <eepp/system/threadpool.hpp>
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
using namespace EE;
|
||||||
|
using namespace EE::System;
|
||||||
|
|
||||||
|
namespace ecode { namespace acp {
|
||||||
|
|
||||||
|
class ACPClient {
|
||||||
|
public:
|
||||||
|
using IdType = int;
|
||||||
|
using JsonReplyHandler = std::function<void( const IdType& id, const json& )>;
|
||||||
|
|
||||||
|
struct Config {
|
||||||
|
std::string command;
|
||||||
|
std::vector<std::string> args;
|
||||||
|
std::string workingDirectory;
|
||||||
|
std::unordered_map<std::string, std::string> environment;
|
||||||
|
};
|
||||||
|
|
||||||
|
ACPClient( std::shared_ptr<ThreadPool> threadPool, const Config& config );
|
||||||
|
~ACPClient();
|
||||||
|
|
||||||
|
bool start();
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
bool isRunning();
|
||||||
|
bool isReady();
|
||||||
|
|
||||||
|
const Config& getConfig() const { return mConfig; }
|
||||||
|
|
||||||
|
void initialize( const InitializeRequest& req,
|
||||||
|
const std::function<void( const InitializeResponse& )>& cb );
|
||||||
|
void newSession( const NewSessionRequest& req,
|
||||||
|
const std::function<void( const NewSessionResponse& )>& cb );
|
||||||
|
void prompt( const PromptRequest& req, const std::function<void( const PromptResponse& )>& cb );
|
||||||
|
|
||||||
|
// Notifications to agent
|
||||||
|
void cancel( const std::string& sessionId );
|
||||||
|
|
||||||
|
// Callbacks from agent
|
||||||
|
std::function<void( const json& )> onSessionUpdate;
|
||||||
|
std::function<void( const ReadTextFileRequest&,
|
||||||
|
std::function<void( const ReadTextFileResponse& )> )>
|
||||||
|
onReadTextFile;
|
||||||
|
std::function<void( const WriteTextFileRequest&,
|
||||||
|
std::function<void( const WriteTextFileResponse& )> )>
|
||||||
|
onWriteTextFile;
|
||||||
|
std::function<void( const RequestPermissionRequest&,
|
||||||
|
std::function<void( const RequestPermissionResponse& )> )>
|
||||||
|
onRequestPermission;
|
||||||
|
std::function<void( const CreateTerminalRequest&,
|
||||||
|
std::function<void( const CreateTerminalResponse& )> )>
|
||||||
|
onCreateTerminal;
|
||||||
|
std::function<void( const TerminalOutputRequest&,
|
||||||
|
std::function<void( const TerminalOutputResponse& )> )>
|
||||||
|
onTerminalOutput;
|
||||||
|
std::function<void( const KillTerminalRequest&,
|
||||||
|
std::function<void( const KillTerminalResponse& )> )>
|
||||||
|
onKillTerminal;
|
||||||
|
std::function<void( const ReleaseTerminalRequest&,
|
||||||
|
std::function<void( const ReleaseTerminalResponse& )> )>
|
||||||
|
onReleaseTerminal;
|
||||||
|
std::function<void( const WaitForTerminalExitRequest&,
|
||||||
|
std::function<void( const WaitForTerminalExitResponse& )> )>
|
||||||
|
onWaitForTerminalExit;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::shared_ptr<ThreadPool> mThreadPool;
|
||||||
|
Config mConfig;
|
||||||
|
Process mProcess;
|
||||||
|
bool mReady{ false };
|
||||||
|
bool mShuttingDown{ false };
|
||||||
|
std::atomic<int> mLastMsgId{ 0 };
|
||||||
|
|
||||||
|
Mutex mHandlersMutex;
|
||||||
|
std::map<IdType, JsonReplyHandler> mHandlers;
|
||||||
|
|
||||||
|
std::string mReceiveBuffer;
|
||||||
|
|
||||||
|
void readStdOut( const char* bytes, size_t n );
|
||||||
|
void readStdErr( const char* bytes, size_t n );
|
||||||
|
|
||||||
|
void processMessage( const json& msg );
|
||||||
|
void processRequest( const json& msg );
|
||||||
|
void processNotification( const json& msg );
|
||||||
|
void processResponse( const json& msg );
|
||||||
|
|
||||||
|
int write( json&& msg, const JsonReplyHandler& h = nullptr );
|
||||||
|
void sendResponse( const json& id, json&& result );
|
||||||
|
void sendError( const json& id, int code, const std::string& message );
|
||||||
|
};
|
||||||
|
|
||||||
|
}} // namespace ecode::acp
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
#include "acpprotocol.hpp"
|
||||||
|
|
||||||
|
namespace ecode { namespace acp {
|
||||||
|
|
||||||
|
ClientCapabilities::ClientCapabilities( const json& body ) {
|
||||||
|
if ( body.contains( "terminal" ) )
|
||||||
|
terminal = body.value( "terminal", false );
|
||||||
|
if ( body.contains( "fs" ) ) {
|
||||||
|
auto fs = body["fs"];
|
||||||
|
if ( fs.contains( "readTextFile" ) )
|
||||||
|
fsReadTextFile = fs.value( "readTextFile", false );
|
||||||
|
if ( fs.contains( "writeTextFile" ) )
|
||||||
|
fsWriteTextFile = fs.value( "writeTextFile", false );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
json ClientCapabilities::toJson() const {
|
||||||
|
return {
|
||||||
|
{ "terminal", terminal },
|
||||||
|
{ "fs", { { "readTextFile", fsReadTextFile }, { "writeTextFile", fsWriteTextFile } } } };
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentCapabilities::AgentCapabilities( const json& body ) {
|
||||||
|
if ( body.contains( "loadSession" ) )
|
||||||
|
loadSession = body.value( "loadSession", false );
|
||||||
|
if ( body.contains( "mcpCapabilities" ) )
|
||||||
|
mcpCapabilities = body["mcpCapabilities"];
|
||||||
|
if ( body.contains( "promptCapabilities" ) )
|
||||||
|
promptCapabilities = body["promptCapabilities"];
|
||||||
|
if ( body.contains( "sessionCapabilities" ) )
|
||||||
|
sessionCapabilities = body["sessionCapabilities"];
|
||||||
|
}
|
||||||
|
|
||||||
|
json InitializeRequest::toJson() const {
|
||||||
|
return { { "protocolVersion", protocolVersion },
|
||||||
|
{ "clientCapabilities", clientCapabilities.toJson() } };
|
||||||
|
}
|
||||||
|
|
||||||
|
InitializeResponse::InitializeResponse( const json& body ) {
|
||||||
|
if ( body.contains( "protocolVersion" ) )
|
||||||
|
protocolVersion = body.value( "protocolVersion", 1 );
|
||||||
|
if ( body.contains( "agentCapabilities" ) )
|
||||||
|
agentCapabilities = AgentCapabilities( body["agentCapabilities"] );
|
||||||
|
}
|
||||||
|
|
||||||
|
json NewSessionRequest::toJson() const {
|
||||||
|
json j = { { "cwd", cwd } };
|
||||||
|
if ( mcpServers.is_null() ) {
|
||||||
|
j["mcpServers"] = json::array();
|
||||||
|
} else {
|
||||||
|
j["mcpServers"] = mcpServers;
|
||||||
|
}
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
|
||||||
|
NewSessionResponse::NewSessionResponse( const json& body ) {
|
||||||
|
if ( body.contains( "sessionId" ) )
|
||||||
|
sessionId = body.value( "sessionId", "" );
|
||||||
|
if ( body.contains( "configOptions" ) )
|
||||||
|
configOptions = body["configOptions"];
|
||||||
|
}
|
||||||
|
|
||||||
|
json PromptRequest::toJson() const {
|
||||||
|
return { { "sessionId", sessionId }, { "prompt", prompt } };
|
||||||
|
}
|
||||||
|
|
||||||
|
PromptResponse::PromptResponse( const json& body ) {
|
||||||
|
if ( body.contains( "stopReason" ) && body["stopReason"].is_string() )
|
||||||
|
stopReason = body.value( "stopReason", "" );
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolCallLocation::ToolCallLocation( const json& body ) {
|
||||||
|
if ( body.contains( "path" ) )
|
||||||
|
path = body.value( "path", "" );
|
||||||
|
if ( body.contains( "line" ) && !body["line"].is_null() )
|
||||||
|
line = body["line"].get<int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolCall::ToolCall( const json& body ) {
|
||||||
|
if ( body.contains( "title" ) )
|
||||||
|
title = body.value( "title", "" );
|
||||||
|
if ( body.contains( "toolCallId" ) )
|
||||||
|
toolCallId = body.value( "toolCallId", "" );
|
||||||
|
if ( body.contains( "kind" ) && body["kind"].is_string() )
|
||||||
|
kind = body.value( "kind", "" );
|
||||||
|
if ( body.contains( "rawInput" ) )
|
||||||
|
rawInput = body["rawInput"];
|
||||||
|
if ( body.contains( "rawOutput" ) )
|
||||||
|
rawOutput = body["rawOutput"];
|
||||||
|
if ( body.contains( "locations" ) && body["locations"].is_array() ) {
|
||||||
|
for ( const auto& l : body["locations"] ) {
|
||||||
|
locations.push_back( ToolCallLocation( l ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReadTextFileRequest::ReadTextFileRequest( const json& body ) {
|
||||||
|
if ( body.contains( "sessionId" ) )
|
||||||
|
sessionId = body.value( "sessionId", "" );
|
||||||
|
if ( body.contains( "path" ) )
|
||||||
|
path = body.value( "path", "" );
|
||||||
|
if ( body.contains( "line" ) && !body["line"].is_null() )
|
||||||
|
line = body["line"].get<int>();
|
||||||
|
if ( body.contains( "limit" ) && !body["limit"].is_null() )
|
||||||
|
limit = body["limit"].get<int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
json ReadTextFileResponse::toJson() const {
|
||||||
|
return { { "content", content } };
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteTextFileRequest::WriteTextFileRequest( const json& body ) {
|
||||||
|
if ( body.contains( "sessionId" ) )
|
||||||
|
sessionId = body.value( "sessionId", "" );
|
||||||
|
if ( body.contains( "path" ) )
|
||||||
|
path = body.value( "path", "" );
|
||||||
|
if ( body.contains( "content" ) )
|
||||||
|
content = body.value( "content", "" );
|
||||||
|
}
|
||||||
|
|
||||||
|
json WriteTextFileResponse::toJson() const {
|
||||||
|
return json::object();
|
||||||
|
}
|
||||||
|
|
||||||
|
EnvVariable::EnvVariable( const json& body ) {
|
||||||
|
if ( body.contains( "name" ) )
|
||||||
|
name = body.value( "name", "" );
|
||||||
|
if ( body.contains( "value" ) )
|
||||||
|
value = body.value( "value", "" );
|
||||||
|
}
|
||||||
|
|
||||||
|
json EnvVariable::toJson() const {
|
||||||
|
return { { "name", name }, { "value", value } };
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateTerminalRequest::CreateTerminalRequest( const json& body ) {
|
||||||
|
if ( body.contains( "sessionId" ) )
|
||||||
|
sessionId = body.value( "sessionId", "" );
|
||||||
|
if ( body.contains( "command" ) )
|
||||||
|
command = body.value( "command", "" );
|
||||||
|
if ( body.contains( "args" ) && body["args"].is_array() ) {
|
||||||
|
for ( const auto& a : body["args"] )
|
||||||
|
args.push_back( a.get<std::string>() );
|
||||||
|
}
|
||||||
|
if ( body.contains( "cwd" ) && !body["cwd"].is_null() )
|
||||||
|
cwd = body["cwd"].get<std::string>();
|
||||||
|
if ( body.contains( "env" ) && body["env"].is_array() ) {
|
||||||
|
for ( const auto& e : body["env"] )
|
||||||
|
env.push_back( EnvVariable( e ) );
|
||||||
|
}
|
||||||
|
if ( body.contains( "outputByteLimit" ) && !body["outputByteLimit"].is_null() )
|
||||||
|
outputByteLimit = body["outputByteLimit"].get<uint64_t>();
|
||||||
|
}
|
||||||
|
|
||||||
|
json CreateTerminalResponse::toJson() const {
|
||||||
|
return { { "terminalId", terminalId } };
|
||||||
|
}
|
||||||
|
|
||||||
|
TerminalExitStatus::TerminalExitStatus( const json& body ) {
|
||||||
|
if ( body.contains( "exitCode" ) && !body["exitCode"].is_null() )
|
||||||
|
exitCode = body["exitCode"].get<uint32_t>();
|
||||||
|
if ( body.contains( "signal" ) && !body["signal"].is_null() )
|
||||||
|
signal = body["signal"].get<std::string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
json TerminalExitStatus::toJson() const {
|
||||||
|
json j;
|
||||||
|
if ( exitCode )
|
||||||
|
j["exitCode"] = *exitCode;
|
||||||
|
else
|
||||||
|
j["exitCode"] = nullptr;
|
||||||
|
if ( signal )
|
||||||
|
j["signal"] = *signal;
|
||||||
|
else
|
||||||
|
j["signal"] = nullptr;
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
|
||||||
|
TerminalOutputRequest::TerminalOutputRequest( const json& body ) {
|
||||||
|
if ( body.contains( "sessionId" ) )
|
||||||
|
sessionId = body.value( "sessionId", "" );
|
||||||
|
if ( body.contains( "terminalId" ) )
|
||||||
|
terminalId = body.value( "terminalId", "" );
|
||||||
|
}
|
||||||
|
|
||||||
|
json TerminalOutputResponse::toJson() const {
|
||||||
|
json j = { { "output", output }, { "truncated", truncated } };
|
||||||
|
if ( exitStatus )
|
||||||
|
j["exitStatus"] = exitStatus->toJson();
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
|
||||||
|
KillTerminalRequest::KillTerminalRequest( const json& body ) {
|
||||||
|
if ( body.contains( "sessionId" ) )
|
||||||
|
sessionId = body.value( "sessionId", "" );
|
||||||
|
if ( body.contains( "terminalId" ) )
|
||||||
|
terminalId = body.value( "terminalId", "" );
|
||||||
|
}
|
||||||
|
|
||||||
|
json KillTerminalResponse::toJson() const {
|
||||||
|
return json::object();
|
||||||
|
}
|
||||||
|
|
||||||
|
ReleaseTerminalRequest::ReleaseTerminalRequest( const json& body ) {
|
||||||
|
if ( body.contains( "sessionId" ) )
|
||||||
|
sessionId = body.value( "sessionId", "" );
|
||||||
|
if ( body.contains( "terminalId" ) )
|
||||||
|
terminalId = body.value( "terminalId", "" );
|
||||||
|
}
|
||||||
|
|
||||||
|
json ReleaseTerminalResponse::toJson() const {
|
||||||
|
return json::object();
|
||||||
|
}
|
||||||
|
|
||||||
|
WaitForTerminalExitRequest::WaitForTerminalExitRequest( const json& body ) {
|
||||||
|
if ( body.contains( "sessionId" ) )
|
||||||
|
sessionId = body.value( "sessionId", "" );
|
||||||
|
if ( body.contains( "terminalId" ) )
|
||||||
|
terminalId = body.value( "terminalId", "" );
|
||||||
|
}
|
||||||
|
|
||||||
|
json WaitForTerminalExitResponse::toJson() const {
|
||||||
|
json j;
|
||||||
|
if ( exitCode )
|
||||||
|
j["exitCode"] = *exitCode;
|
||||||
|
else
|
||||||
|
j["exitCode"] = nullptr;
|
||||||
|
if ( signal )
|
||||||
|
j["signal"] = *signal;
|
||||||
|
else
|
||||||
|
j["signal"] = nullptr;
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
|
||||||
|
PermissionOption::PermissionOption( const json& body ) {
|
||||||
|
if ( body.contains( "optionId" ) )
|
||||||
|
optionId = body.value( "optionId", "" );
|
||||||
|
if ( body.contains( "name" ) )
|
||||||
|
name = body.value( "name", "" );
|
||||||
|
if ( body.contains( "kind" ) )
|
||||||
|
kind = body.value( "kind", "" );
|
||||||
|
}
|
||||||
|
|
||||||
|
json PermissionOption::toJson() const {
|
||||||
|
return { { "optionId", optionId }, { "name", name }, { "kind", kind } };
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestPermissionRequest::RequestPermissionRequest( const json& body ) {
|
||||||
|
if ( body.contains( "sessionId" ) )
|
||||||
|
sessionId = body.value( "sessionId", "" );
|
||||||
|
if ( body.contains( "toolCall" ) )
|
||||||
|
toolCall = ToolCall( body["toolCall"] );
|
||||||
|
if ( body.contains( "options" ) && body["options"].is_array() ) {
|
||||||
|
for ( const auto& o : body["options"] )
|
||||||
|
options.push_back( PermissionOption( o ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
json RequestPermissionResponse::toJson() const {
|
||||||
|
json j = { { "outcome", outcome } };
|
||||||
|
if ( optionId )
|
||||||
|
j["optionId"] = *optionId;
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
|
||||||
|
}} // namespace ecode::acp
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <eepp/core/containers.hpp>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
namespace ecode { namespace acp {
|
||||||
|
|
||||||
|
struct ClientCapabilities {
|
||||||
|
bool terminal{ false };
|
||||||
|
bool fsReadTextFile{ false };
|
||||||
|
bool fsWriteTextFile{ false };
|
||||||
|
|
||||||
|
ClientCapabilities() = default;
|
||||||
|
ClientCapabilities( const json& body );
|
||||||
|
json toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AgentCapabilities {
|
||||||
|
bool loadSession{ false };
|
||||||
|
json mcpCapabilities;
|
||||||
|
json promptCapabilities;
|
||||||
|
json sessionCapabilities;
|
||||||
|
|
||||||
|
AgentCapabilities() = default;
|
||||||
|
AgentCapabilities( const json& body );
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InitializeRequest {
|
||||||
|
int protocolVersion{ 1 };
|
||||||
|
ClientCapabilities clientCapabilities;
|
||||||
|
|
||||||
|
InitializeRequest() = default;
|
||||||
|
json toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InitializeResponse {
|
||||||
|
int protocolVersion{ 1 };
|
||||||
|
AgentCapabilities agentCapabilities;
|
||||||
|
|
||||||
|
InitializeResponse() = default;
|
||||||
|
InitializeResponse( const json& body );
|
||||||
|
};
|
||||||
|
|
||||||
|
struct NewSessionRequest {
|
||||||
|
std::string cwd;
|
||||||
|
json mcpServers;
|
||||||
|
|
||||||
|
NewSessionRequest() = default;
|
||||||
|
json toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct NewSessionResponse {
|
||||||
|
std::string sessionId;
|
||||||
|
json configOptions;
|
||||||
|
|
||||||
|
NewSessionResponse() = default;
|
||||||
|
NewSessionResponse( const json& body );
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PromptRequest {
|
||||||
|
std::string sessionId;
|
||||||
|
json prompt; // Array of ContentBlock
|
||||||
|
|
||||||
|
PromptRequest() = default;
|
||||||
|
json toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PromptResponse {
|
||||||
|
std::string stopReason;
|
||||||
|
|
||||||
|
PromptResponse() = default;
|
||||||
|
PromptResponse( const json& body );
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ToolCallLocation {
|
||||||
|
std::string path;
|
||||||
|
std::optional<int> line;
|
||||||
|
|
||||||
|
ToolCallLocation() = default;
|
||||||
|
ToolCallLocation( const json& body );
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ToolCall {
|
||||||
|
std::string title;
|
||||||
|
std::string toolCallId;
|
||||||
|
std::string kind;
|
||||||
|
json rawInput;
|
||||||
|
json rawOutput;
|
||||||
|
std::vector<ToolCallLocation> locations;
|
||||||
|
|
||||||
|
ToolCall() = default;
|
||||||
|
ToolCall( const json& body );
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ReadTextFileRequest {
|
||||||
|
std::string sessionId;
|
||||||
|
std::string path;
|
||||||
|
std::optional<int> line;
|
||||||
|
std::optional<int> limit;
|
||||||
|
|
||||||
|
ReadTextFileRequest() = default;
|
||||||
|
ReadTextFileRequest( const json& body );
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ReadTextFileResponse {
|
||||||
|
std::string content;
|
||||||
|
|
||||||
|
ReadTextFileResponse() = default;
|
||||||
|
json toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WriteTextFileRequest {
|
||||||
|
std::string sessionId;
|
||||||
|
std::string path;
|
||||||
|
std::string content;
|
||||||
|
|
||||||
|
WriteTextFileRequest() = default;
|
||||||
|
WriteTextFileRequest( const json& body );
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WriteTextFileResponse {
|
||||||
|
WriteTextFileResponse() = default;
|
||||||
|
json toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EnvVariable {
|
||||||
|
std::string name;
|
||||||
|
std::string value;
|
||||||
|
EnvVariable() = default;
|
||||||
|
EnvVariable( const json& body );
|
||||||
|
json toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CreateTerminalRequest {
|
||||||
|
std::string sessionId;
|
||||||
|
std::string command;
|
||||||
|
std::vector<std::string> args;
|
||||||
|
std::optional<std::string> cwd;
|
||||||
|
std::vector<EnvVariable> env;
|
||||||
|
std::optional<uint64_t> outputByteLimit;
|
||||||
|
|
||||||
|
CreateTerminalRequest() = default;
|
||||||
|
CreateTerminalRequest( const json& body );
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CreateTerminalResponse {
|
||||||
|
std::string terminalId;
|
||||||
|
|
||||||
|
CreateTerminalResponse() = default;
|
||||||
|
json toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TerminalExitStatus {
|
||||||
|
std::optional<uint32_t> exitCode;
|
||||||
|
std::optional<std::string> signal;
|
||||||
|
|
||||||
|
TerminalExitStatus() = default;
|
||||||
|
TerminalExitStatus( const json& body );
|
||||||
|
json toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TerminalOutputRequest {
|
||||||
|
std::string sessionId;
|
||||||
|
std::string terminalId;
|
||||||
|
|
||||||
|
TerminalOutputRequest() = default;
|
||||||
|
TerminalOutputRequest( const json& body );
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TerminalOutputResponse {
|
||||||
|
std::string output;
|
||||||
|
bool truncated{ false };
|
||||||
|
std::optional<TerminalExitStatus> exitStatus;
|
||||||
|
|
||||||
|
TerminalOutputResponse() = default;
|
||||||
|
json toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct KillTerminalRequest {
|
||||||
|
std::string sessionId;
|
||||||
|
std::string terminalId;
|
||||||
|
|
||||||
|
KillTerminalRequest() = default;
|
||||||
|
KillTerminalRequest( const json& body );
|
||||||
|
};
|
||||||
|
|
||||||
|
struct KillTerminalResponse {
|
||||||
|
KillTerminalResponse() = default;
|
||||||
|
json toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ReleaseTerminalRequest {
|
||||||
|
std::string sessionId;
|
||||||
|
std::string terminalId;
|
||||||
|
|
||||||
|
ReleaseTerminalRequest() = default;
|
||||||
|
ReleaseTerminalRequest( const json& body );
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ReleaseTerminalResponse {
|
||||||
|
ReleaseTerminalResponse() = default;
|
||||||
|
json toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WaitForTerminalExitRequest {
|
||||||
|
std::string sessionId;
|
||||||
|
std::string terminalId;
|
||||||
|
|
||||||
|
WaitForTerminalExitRequest() = default;
|
||||||
|
WaitForTerminalExitRequest( const json& body );
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WaitForTerminalExitResponse {
|
||||||
|
std::optional<uint32_t> exitCode;
|
||||||
|
std::optional<std::string> signal;
|
||||||
|
|
||||||
|
WaitForTerminalExitResponse() = default;
|
||||||
|
json toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PermissionOption {
|
||||||
|
std::string optionId;
|
||||||
|
std::string name;
|
||||||
|
std::string kind;
|
||||||
|
|
||||||
|
PermissionOption() = default;
|
||||||
|
PermissionOption( const json& body );
|
||||||
|
json toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RequestPermissionRequest {
|
||||||
|
std::string sessionId;
|
||||||
|
ToolCall toolCall;
|
||||||
|
std::vector<PermissionOption> options;
|
||||||
|
|
||||||
|
RequestPermissionRequest() = default;
|
||||||
|
RequestPermissionRequest( const json& body );
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RequestPermissionResponse {
|
||||||
|
std::string outcome;
|
||||||
|
std::optional<std::string> optionId;
|
||||||
|
|
||||||
|
RequestPermissionResponse() = default;
|
||||||
|
json toJson() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
}} // namespace ecode::acp
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
#include "agentsession.hpp"
|
||||||
|
#include <eepp/system/filesystem.hpp>
|
||||||
|
#include <eepp/system/log.hpp>
|
||||||
|
|
||||||
|
namespace ecode { namespace acp {
|
||||||
|
|
||||||
|
AgentSession::AgentSession( std::shared_ptr<ThreadPool> threadPool,
|
||||||
|
const ACPClient::Config& config ) :
|
||||||
|
mThreadPool( threadPool ), mClient( std::make_unique<ACPClient>( threadPool, config ) ) {
|
||||||
|
setupClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentSession::~AgentSession() {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AgentSession::start( const std::function<void( bool )>& onReady ) {
|
||||||
|
if ( mClient->start() ) {
|
||||||
|
InitializeRequest req;
|
||||||
|
req.clientCapabilities.terminal = true;
|
||||||
|
req.clientCapabilities.fsReadTextFile = true;
|
||||||
|
req.clientCapabilities.fsWriteTextFile = true;
|
||||||
|
|
||||||
|
mClient->initialize( req, [this, onReady]( const InitializeResponse& ) {
|
||||||
|
NewSessionRequest nreq;
|
||||||
|
nreq.cwd = mClient->isReady() ? mClient->getConfig().workingDirectory : "";
|
||||||
|
mClient->newSession( nreq, [this, onReady]( const NewSessionResponse& nres ) {
|
||||||
|
mSessionId = nres.sessionId;
|
||||||
|
if ( onReady )
|
||||||
|
onReady( true );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ( onReady )
|
||||||
|
onReady( false );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentSession::stop() {
|
||||||
|
if ( mClient )
|
||||||
|
mClient->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentSession::prompt( const PromptRequest& req,
|
||||||
|
const std::function<void( const PromptResponse& )>& cb ) {
|
||||||
|
mIsPrompting = true;
|
||||||
|
mClient->prompt( req, [this, cb](const PromptResponse& res) {
|
||||||
|
mIsPrompting = false;
|
||||||
|
if ( cb )
|
||||||
|
cb(res);
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentSession::cancel() {
|
||||||
|
if ( mClient && !mSessionId.empty() ) {
|
||||||
|
mClient->cancel( mSessionId );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentSession::setupClient() {
|
||||||
|
mClient->onSessionUpdate = [this]( const json& msg ) {
|
||||||
|
if ( onSessionUpdate )
|
||||||
|
onSessionUpdate( msg );
|
||||||
|
};
|
||||||
|
|
||||||
|
mClient->onReadTextFile = []( const ReadTextFileRequest& req, auto cb ) {
|
||||||
|
ReadTextFileResponse res;
|
||||||
|
std::string content;
|
||||||
|
if ( FileSystem::fileGet( req.path, content ) ) {
|
||||||
|
res.content = content;
|
||||||
|
}
|
||||||
|
cb( res );
|
||||||
|
};
|
||||||
|
|
||||||
|
mClient->onWriteTextFile = []( const WriteTextFileRequest& req, auto cb ) {
|
||||||
|
FileSystem::fileWrite( req.path, req.content );
|
||||||
|
cb( WriteTextFileResponse() );
|
||||||
|
};
|
||||||
|
|
||||||
|
mClient->onRequestPermission = [this]( const RequestPermissionRequest& req, auto cb ) {
|
||||||
|
if ( onRequestPermission ) {
|
||||||
|
onRequestPermission( req, cb );
|
||||||
|
} else {
|
||||||
|
RequestPermissionResponse res;
|
||||||
|
res.outcome = "rejected";
|
||||||
|
cb( res );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mClient->onCreateTerminal = [this]( const CreateTerminalRequest& req, auto cb ) {
|
||||||
|
CreateTerminalResponse res;
|
||||||
|
std::string termId = String::format( "term-%u", String::hash( req.command ) );
|
||||||
|
res.terminalId = termId;
|
||||||
|
// Wait for UI? No, ACPClient is running in threads. We just trigger the event
|
||||||
|
// and return the ID.
|
||||||
|
if ( onTerminalCreated ) {
|
||||||
|
onTerminalCreated( nullptr, termId );
|
||||||
|
}
|
||||||
|
cb( res );
|
||||||
|
};
|
||||||
|
|
||||||
|
mClient->onTerminalOutput = [this]( const TerminalOutputRequest& req, auto cb ) {
|
||||||
|
TerminalOutputResponse res;
|
||||||
|
res.output = "";
|
||||||
|
res.truncated = false;
|
||||||
|
auto it = mTerminals.find( req.terminalId );
|
||||||
|
if ( it != mTerminals.end() && it->second.emulator ) {
|
||||||
|
if ( it->second.emulator->hasExited() ) {
|
||||||
|
TerminalExitStatus status;
|
||||||
|
status.exitCode = it->second.emulator->getExitCode();
|
||||||
|
res.exitStatus = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cb( res );
|
||||||
|
};
|
||||||
|
|
||||||
|
mClient->onKillTerminal = [this]( const KillTerminalRequest& req, auto cb ) {
|
||||||
|
auto it = mTerminals.find( req.terminalId );
|
||||||
|
if ( it != mTerminals.end() && it->second.emulator ) {
|
||||||
|
it->second.emulator->terminate();
|
||||||
|
}
|
||||||
|
cb( KillTerminalResponse() );
|
||||||
|
};
|
||||||
|
|
||||||
|
mClient->onReleaseTerminal = [this]( const ReleaseTerminalRequest& req, auto cb ) {
|
||||||
|
auto it = mTerminals.find( req.terminalId );
|
||||||
|
if ( it != mTerminals.end() ) {
|
||||||
|
if ( it->second.emulator )
|
||||||
|
it->second.emulator->terminate();
|
||||||
|
if ( it->second.uiTerm )
|
||||||
|
it->second.uiTerm->close();
|
||||||
|
mTerminals.erase( it );
|
||||||
|
}
|
||||||
|
cb( ReleaseTerminalResponse() );
|
||||||
|
};
|
||||||
|
|
||||||
|
mClient->onWaitForTerminalExit = [this]( const WaitForTerminalExitRequest& req, auto cb ) {
|
||||||
|
WaitForTerminalExitResponse res;
|
||||||
|
auto it = mTerminals.find( req.terminalId );
|
||||||
|
if ( it != mTerminals.end() && it->second.emulator ) {
|
||||||
|
if ( it->second.emulator->hasExited() ) {
|
||||||
|
res.exitCode = it->second.emulator->getExitCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cb( res );
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}} // namespace ecode::acp
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "acpclient.hpp"
|
||||||
|
#include <eepp/system/threadpool.hpp>
|
||||||
|
#include <eterm/terminal/terminalemulator.hpp>
|
||||||
|
#include <eterm/ui/uiterminal.hpp>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
using namespace EE::System;
|
||||||
|
using namespace eterm::Terminal;
|
||||||
|
using namespace eterm::UI;
|
||||||
|
|
||||||
|
namespace ecode { namespace acp {
|
||||||
|
|
||||||
|
class AgentSession {
|
||||||
|
public:
|
||||||
|
AgentSession( std::shared_ptr<ThreadPool> threadPool, const ACPClient::Config& config );
|
||||||
|
~AgentSession();
|
||||||
|
|
||||||
|
bool start( const std::function<void( bool )>& onReady );
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
void prompt( const PromptRequest& req, const std::function<void( const PromptResponse& )>& cb );
|
||||||
|
void cancel();
|
||||||
|
|
||||||
|
bool isPrompting() const { return mIsPrompting; }
|
||||||
|
|
||||||
|
std::function<void( const json& )> onSessionUpdate;
|
||||||
|
std::function<void( const RequestPermissionRequest&,
|
||||||
|
std::function<void( const RequestPermissionResponse& )> )>
|
||||||
|
onRequestPermission;
|
||||||
|
std::function<void( UITerminal*, const std::string& terminalId )> onTerminalCreated;
|
||||||
|
|
||||||
|
std::string getSessionId() const { return mSessionId; }
|
||||||
|
ACPClient* getClient() const { return mClient.get(); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::shared_ptr<ThreadPool> mThreadPool;
|
||||||
|
std::unique_ptr<ACPClient> mClient;
|
||||||
|
std::string mSessionId;
|
||||||
|
bool mIsPrompting{ false };
|
||||||
|
|
||||||
|
struct TermData {
|
||||||
|
std::shared_ptr<TerminalDisplay> display;
|
||||||
|
std::shared_ptr<TerminalEmulator> emulator;
|
||||||
|
UITerminal* uiTerm{ nullptr };
|
||||||
|
};
|
||||||
|
std::unordered_map<std::string, TermData> mTerminals;
|
||||||
|
|
||||||
|
void setupClient();
|
||||||
|
};
|
||||||
|
|
||||||
|
}} // namespace ecode::acp
|
||||||
@@ -108,6 +108,34 @@ static std::map<std::string, LLMProvider> parseLLMProviders( const nlohmann::jso
|
|||||||
return providers;
|
return providers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static std::map<std::string, ACPAgent> parseACPAgents( const nlohmann::json& j ) {
|
||||||
|
std::map<std::string, ACPAgent> agents;
|
||||||
|
for ( const auto& item : j.items() ) {
|
||||||
|
std::string agentName = item.key();
|
||||||
|
const auto& agentJson = item.value();
|
||||||
|
|
||||||
|
ACPAgent agent;
|
||||||
|
agent.name = agentName;
|
||||||
|
agent.enabled = agentJson.value( "enabled", true );
|
||||||
|
agent.command = agentJson.value( "command", "" );
|
||||||
|
|
||||||
|
if ( agentJson.contains( "args" ) && agentJson["args"].is_array() ) {
|
||||||
|
for ( const auto& arg : agentJson["args"] ) {
|
||||||
|
agent.args.push_back( arg.get<std::string>() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( agentJson.contains( "environment" ) && agentJson["environment"].is_object() ) {
|
||||||
|
for ( const auto& envItem : agentJson["environment"].items() ) {
|
||||||
|
agent.environment[envItem.key()] = envItem.value().get<std::string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
agents[agentName] = agent;
|
||||||
|
}
|
||||||
|
return agents;
|
||||||
|
}
|
||||||
|
|
||||||
Plugin* AIAssistantPlugin::New( PluginManager* pluginManager ) {
|
Plugin* AIAssistantPlugin::New( PluginManager* pluginManager ) {
|
||||||
return eeNew( AIAssistantPlugin, ( pluginManager, false ) );
|
return eeNew( AIAssistantPlugin, ( pluginManager, false ) );
|
||||||
}
|
}
|
||||||
@@ -395,6 +423,13 @@ void AIAssistantPlugin::loadAIAssistantConfig( const std::string& path, bool upd
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( j.contains( "agents" ) ) {
|
||||||
|
auto agents = parseACPAgents( j["agents"] );
|
||||||
|
for ( const auto& [key, value] : agents ) {
|
||||||
|
mAgents[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ( !j.contains( "providers" ) )
|
if ( !j.contains( "providers" ) )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -598,6 +633,7 @@ void AIAssistantPlugin::onSaveState( IniFile* state ) {
|
|||||||
config.partition = mainChat->getSplitter()->getSplitPartition();
|
config.partition = mainChat->getSplitter()->getSplitPartition();
|
||||||
config.modelProvider = mainChat->getCurModel().provider;
|
config.modelProvider = mainChat->getCurModel().provider;
|
||||||
config.modelName = mainChat->getCurModel().name;
|
config.modelName = mainChat->getCurModel().name;
|
||||||
|
config.agentName = mainChat->getCurAgent();
|
||||||
} else {
|
} else {
|
||||||
config = mConfig;
|
config = mConfig;
|
||||||
}
|
}
|
||||||
@@ -610,6 +646,7 @@ void AIAssistantPlugin::onSaveState( IniFile* state ) {
|
|||||||
state->setValue( keyname, "split_partition", config.partition.toString() );
|
state->setValue( keyname, "split_partition", config.partition.toString() );
|
||||||
state->setValue( keyname, "default_provider", config.modelProvider );
|
state->setValue( keyname, "default_provider", config.modelProvider );
|
||||||
state->setValue( keyname, "default_model", config.modelName );
|
state->setValue( keyname, "default_model", config.modelName );
|
||||||
|
state->setValue( keyname, "default_agent", config.agentName );
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ecode
|
} // namespace ecode
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class AIAssistantPlugin : public PluginBase {
|
|||||||
StyleSheetLength partition;
|
StyleSheetLength partition;
|
||||||
std::string modelProvider;
|
std::string modelProvider;
|
||||||
std::string modelName;
|
std::string modelName;
|
||||||
|
std::string agentName;
|
||||||
};
|
};
|
||||||
|
|
||||||
static PluginDefinition Definition() {
|
static PluginDefinition Definition() {
|
||||||
@@ -37,6 +38,7 @@ class AIAssistantPlugin : public PluginBase {
|
|||||||
std::string getDescription() override { return Definition().description; }
|
std::string getDescription() override { return Definition().description; }
|
||||||
|
|
||||||
const LLMProviders& getProviders() { return mProviders; }
|
const LLMProviders& getProviders() { return mProviders; }
|
||||||
|
const ACPAgents& getAgents() { return mAgents; }
|
||||||
|
|
||||||
std::string getPluginStatePath() const;
|
std::string getPluginStatePath() const;
|
||||||
|
|
||||||
@@ -50,6 +52,7 @@ class AIAssistantPlugin : public PluginBase {
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
LLMProviders mProviders;
|
LLMProviders mProviders;
|
||||||
|
ACPAgents mAgents;
|
||||||
bool mUIInit{ false };
|
bool mUIInit{ false };
|
||||||
bool mBrokenUserConfigFile{ false };
|
bool mBrokenUserConfigFile{ false };
|
||||||
UIWidget* mStatusBar{ nullptr };
|
UIWidget* mStatusBar{ nullptr };
|
||||||
|
|||||||
@@ -179,8 +179,10 @@ DropDownList.role_ui {
|
|||||||
<PushButton id="llm_more" class="llm_button" tooltip="@string(more_options, More Options)" icon="icon(more-fill, 14dp)" min-width="32dp" />
|
<PushButton id="llm_more" class="llm_button" tooltip="@string(more_options, More Options)" icon="icon(more-fill, 14dp)" min-width="32dp" />
|
||||||
<hbox lw="0" lw8="1" lh="mp" layout_gravity="center" padding-left="4dp" padding-right="4dp">
|
<hbox lw="0" lw8="1" lh="mp" layout_gravity="center" padding-left="4dp" padding-right="4dp">
|
||||||
<DropDownList class="model_ui" menu-width-mode="expand-if-needed-centered" lw="0" lw8="1" selected-index="0"></DropDownList>
|
<DropDownList class="model_ui" menu-width-mode="expand-if-needed-centered" lw="0" lw8="1" selected-index="0"></DropDownList>
|
||||||
|
<DropDownList class="agent_ui" menu-width-mode="expand-if-needed-centered" lw="0" lw8="1" selected-index="0" visible="false"></DropDownList>
|
||||||
<!-- <PushButton id="refresh_model_ui" tooltip="@string(refresh_model_ui, Refresh Local Models)" icon="icon(refresh, 14dp)" /> -->
|
<!-- <PushButton id="refresh_model_ui" tooltip="@string(refresh_model_ui, Refresh Local Models)" icon="icon(refresh, 14dp)" /> -->
|
||||||
</hbox>
|
</hbox>
|
||||||
|
<SelectButton id="llm_agent_mode" class="llm_button" tooltip="@string(toggle_agent_mode, Toggle Agent Mode)" icon="icon(robot, 14dp)" min-width="32dp" margin-right="4dp" select-on-click="true" />
|
||||||
<PushButton id="llm_settings_but" class="llm_button" text="@string(settings, Settings)" tooltip="@string(settings, Settings)" icon="icon(settings, 14dp)" min-width="32dp" margin-right="4dp" />
|
<PushButton id="llm_settings_but" class="llm_button" text="@string(settings, Settings)" tooltip="@string(settings, Settings)" icon="icon(settings, 14dp)" min-width="32dp" margin-right="4dp" />
|
||||||
<!-- <SelectButton id="llm_private_chat" class="llm_button" tooltip="@string(private_chat, Toggle Private Chat)" icon="icon(chat-private, 14dp)" min-width="32dp" margin-right="8dp" select-on-click="true" /> -->
|
<!-- <SelectButton id="llm_private_chat" class="llm_button" tooltip="@string(private_chat, Toggle Private Chat)" icon="icon(chat-private, 14dp)" min-width="32dp" margin-right="8dp" select-on-click="true" /> -->
|
||||||
<PushButton id="llm_add_chat" class="llm_button" text="@string(add, Add)" tooltip="@string(add_message, Add Message)" icon="icon(add, 15dp)" min-width="32dp" margin-right="4dp" />
|
<PushButton id="llm_add_chat" class="llm_button" text="@string(add, Add)" tooltip="@string(add_message, Add Message)" icon="icon(add, 15dp)" min-width="32dp" margin-right="4dp" />
|
||||||
@@ -208,6 +210,18 @@ static const char* DEFAULT_CHAT_GLOBE = R"xml(
|
|||||||
</vbox>
|
</vbox>
|
||||||
)xml";
|
)xml";
|
||||||
|
|
||||||
|
static const char* DEFAULT_PERMISSION_GLOBE = R"xml(
|
||||||
|
<vbox class="llm_conversation tool_permission" lw="mp" lh="wc" margin-bottom="8dp">
|
||||||
|
<hbox class="llm_conversation_opt" lw="mp" lh="wc" background-color="var(--primary)" padding="4dp">
|
||||||
|
<TextView text="Tool Call Permission Request" font-style="bold" margin-left="4dp" />
|
||||||
|
</hbox>
|
||||||
|
<vbox class="data_ui" lw="mp" lh="wc" padding="8dp" background-color="var(--tab-back)">
|
||||||
|
<TextView class="permission_desc" lw="mp" lh="wc" word-wrap="true" />
|
||||||
|
<hbox class="permission_options" lw="mp" lh="wc" margin-top="8dp" />
|
||||||
|
</vbox>
|
||||||
|
</vbox>
|
||||||
|
)xml";
|
||||||
|
|
||||||
LLMChatUI::LLMChatUI( PluginManager* manager ) :
|
LLMChatUI::LLMChatUI( PluginManager* manager ) :
|
||||||
UILinearLayout(), WidgetCommandExecuter( getInput() ), mManager( manager ) {
|
UILinearLayout(), WidgetCommandExecuter( getInput() ), mManager( manager ) {
|
||||||
setClass( "llm_chatui" );
|
setClass( "llm_chatui" );
|
||||||
@@ -220,6 +234,14 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) :
|
|||||||
|
|
||||||
mChatsList = findByClass( "llm_chats" );
|
mChatsList = findByClass( "llm_chats" );
|
||||||
mModelDDL = findByClass<UIDropDownList>( "model_ui" );
|
mModelDDL = findByClass<UIDropDownList>( "model_ui" );
|
||||||
|
mAgentDDL = findByClass<UIDropDownList>( "agent_ui" );
|
||||||
|
|
||||||
|
mChatAgentMode = find<UISelectButton>( "llm_agent_mode" );
|
||||||
|
mChatAgentMode->on( Event::OnValueChange, [this]( auto ) {
|
||||||
|
mIsAgentMode = mChatAgentMode->isSelected();
|
||||||
|
mModelDDL->setVisible( !mIsAgentMode );
|
||||||
|
mAgentDDL->setVisible( mIsAgentMode );
|
||||||
|
} );
|
||||||
|
|
||||||
// mRefreshModels = find<UIPushButton>( "refresh_model_ui" );
|
// mRefreshModels = find<UIPushButton>( "refresh_model_ui" );
|
||||||
// mRefreshModels->onClick( [this]( auto ) { execute( "ai-refresh-local-models" ); } );
|
// mRefreshModels->onClick( [this]( auto ) { execute( "ai-refresh-local-models" ); } );
|
||||||
@@ -314,12 +336,16 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) :
|
|||||||
|
|
||||||
setCmd( "ai-prompt", [this] {
|
setCmd( "ai-prompt", [this] {
|
||||||
// "ai-prompt-stop"
|
// "ai-prompt-stop"
|
||||||
if ( mRequest ) {
|
if ( mRequest || ( mAgentSession && mAgentSession->isPrompting() ) ) {
|
||||||
if ( !mRequest->isCancelled() ) {
|
if ( mRequest && !mRequest->isCancelled() ) {
|
||||||
mRequest->cancel();
|
mRequest->cancel();
|
||||||
return;
|
return;
|
||||||
} else
|
} else if ( mAgentSession && mAgentSession->isPrompting() ) {
|
||||||
|
mAgentSession->cancel();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
mRequest.reset();
|
mRequest.reset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto chats = findAllByClass( "llm_conversation" );
|
auto chats = findAllByClass( "llm_conversation" );
|
||||||
@@ -344,7 +370,12 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
execute( "ai-add-chat" );
|
execute( "ai-add-chat" );
|
||||||
doRequest();
|
|
||||||
|
if ( mIsAgentMode ) {
|
||||||
|
doAgentRequest();
|
||||||
|
} else {
|
||||||
|
doRequest();
|
||||||
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
setCmd( "ai-prompt-stop", [this] {
|
setCmd( "ai-prompt-stop", [this] {
|
||||||
@@ -550,6 +581,8 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) :
|
|||||||
setProviders( std::move( providers ) );
|
setProviders( std::move( providers ) );
|
||||||
mCurModel = getDefaultModel();
|
mCurModel = getDefaultModel();
|
||||||
|
|
||||||
|
mAgents = getPlugin()->getAgents();
|
||||||
|
|
||||||
AppConfig& config = getPlugin()->getPluginContext()->getConfig();
|
AppConfig& config = getPlugin()->getPluginContext()->getConfig();
|
||||||
auto partition = config.iniState.getValue( "aiassistant", "split_partition", "" );
|
auto partition = config.iniState.getValue( "aiassistant", "split_partition", "" );
|
||||||
|
|
||||||
@@ -558,6 +591,7 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) :
|
|||||||
|
|
||||||
auto modelProvider = config.iniState.getValue( "aiassistant", "default_provider", "" );
|
auto modelProvider = config.iniState.getValue( "aiassistant", "default_provider", "" );
|
||||||
auto modelName = config.iniState.getValue( "aiassistant", "default_model", "" );
|
auto modelName = config.iniState.getValue( "aiassistant", "default_model", "" );
|
||||||
|
auto agentName = config.iniState.getValue( "aiassistant", "default_agent", "" );
|
||||||
|
|
||||||
if ( !modelProvider.empty() && !modelName.empty() ) {
|
if ( !modelProvider.empty() && !modelName.empty() ) {
|
||||||
auto modelOpt = getModel( modelProvider, modelName );
|
auto modelOpt = getModel( modelProvider, modelName );
|
||||||
@@ -566,7 +600,14 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( !agentName.empty() && mAgents.find( agentName ) != mAgents.end() ) {
|
||||||
|
mCurAgent = agentName;
|
||||||
|
} else if ( !mAgents.empty() ) {
|
||||||
|
mCurAgent = mAgents.begin()->first;
|
||||||
|
}
|
||||||
|
|
||||||
fillModelDropDownList( mModelDDL );
|
fillModelDropDownList( mModelDDL );
|
||||||
|
fillAgentDropDownList( mAgentDDL );
|
||||||
|
|
||||||
const auto appendShortcutToTooltip = [this]( UIPushButton* but, const std::string& cmd ) {
|
const auto appendShortcutToTooltip = [this]( UIPushButton* but, const std::string& cmd ) {
|
||||||
auto kb = getKeyBindings().getCommandKeybindString( cmd );
|
auto kb = getKeyBindings().getCommandKeybindString( cmd );
|
||||||
@@ -968,6 +1009,187 @@ void LLMChatUI::fillModelDropDownList( UIDropDownList* modelDDL ) {
|
|||||||
[this, modelDDL] { fillApiModels( modelDDL ); } );
|
[this, modelDDL] { fillApiModels( modelDDL ); } );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LLMChatUI::fillAgentDropDownList( UIDropDownList* agentDDL ) {
|
||||||
|
std::vector<String> agents;
|
||||||
|
std::size_t selectedIndex = 0;
|
||||||
|
for ( const auto& [name, data] : mAgents ) {
|
||||||
|
if ( !data.enabled )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
String agentName( String::format( "%s", data.name ) );
|
||||||
|
if ( data.name == mCurAgent )
|
||||||
|
selectedIndex = agents.size();
|
||||||
|
agents.push_back( std::move( agentName ) );
|
||||||
|
}
|
||||||
|
agentDDL->getListBox()->clear();
|
||||||
|
agentDDL->getListBox()->addListBoxItems( std::move( agents ) );
|
||||||
|
agentDDL->getListBox()->setSelected( selectedIndex );
|
||||||
|
agentDDL->on( Event::OnValueChange, [this, agentDDL]( auto ) {
|
||||||
|
mCurAgent = agentDDL->getListBox()->getItemSelectedText().toUtf8();
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
void LLMChatUI::writeToLastChat( const std::string& text ) {
|
||||||
|
runOnMainThread( [this, text] {
|
||||||
|
auto chats = mChatsList->findAllByClass( "llm_conversation" );
|
||||||
|
if ( chats.empty() )
|
||||||
|
return;
|
||||||
|
auto* chat = chats.back();
|
||||||
|
auto* editor = chat->findByClass<UICodeEditor>( "data_ui" );
|
||||||
|
auto* thinking = editor->findByClass<UIImage>( "thinking" );
|
||||||
|
auto thinkingID = String::hash( String::format( "thinking-%p", thinking ) );
|
||||||
|
|
||||||
|
editor->getDocument().textInput( String::fromUtf8( text ) );
|
||||||
|
editor->setCursorVisible( false );
|
||||||
|
thinking->removeActionsByTag( thinkingID );
|
||||||
|
thinking->setVisible( false );
|
||||||
|
resizeToFit( editor );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
void LLMChatUI::doAgentRequest() {
|
||||||
|
if ( !mAgentSession ) {
|
||||||
|
auto it = mAgents.find( mCurAgent );
|
||||||
|
if ( it == mAgents.end() ) {
|
||||||
|
showMsg( "Agent not configured." );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mChatRun->setVisible( false )->setEnabled( false );
|
||||||
|
mChatStop->setVisible( true )->setEnabled( true );
|
||||||
|
|
||||||
|
acp::ACPClient::Config config;
|
||||||
|
config.command = it->second.command;
|
||||||
|
config.args = it->second.args;
|
||||||
|
config.environment = it->second.environment;
|
||||||
|
config.workingDirectory = getPlugin()->getPluginContext()->getCurrentProject();
|
||||||
|
|
||||||
|
mAgentSession =
|
||||||
|
std::make_unique<acp::AgentSession>( getUISceneNode()->getThreadPool(), config );
|
||||||
|
|
||||||
|
UIWidget* chat = addChatUI( LLMChat::Role::Assistant );
|
||||||
|
toggleEnableChats( false );
|
||||||
|
auto* editor = chat->findByClass<UICodeEditor>( "data_ui" );
|
||||||
|
editor->setEnabled( false );
|
||||||
|
auto* thinking = editor->findByClass<UIImage>( "thinking" );
|
||||||
|
auto thinkingID = String::hash( String::format( "thinking-%p", thinking ) );
|
||||||
|
thinking->setVisible( true );
|
||||||
|
thinking->setPosition( { PixelDensity::dpToPx( 8 ), PixelDensity::dpToPx( 3 ) } );
|
||||||
|
thinking->setInterval( [thinking] { thinking->rotate( 360 / 32 ); }, Seconds( 0.125 ),
|
||||||
|
thinkingID );
|
||||||
|
|
||||||
|
mAgentSession->onSessionUpdate = [this]( const nlohmann::json& msg ) {
|
||||||
|
auto sessionUpdate = msg.value( "sessionUpdate", "" );
|
||||||
|
if ( sessionUpdate == "agent_message_chunk" ||
|
||||||
|
sessionUpdate == "agent_thought_chunk" ) {
|
||||||
|
if ( msg.contains( "content" ) && msg["content"].contains( "text" ) ) {
|
||||||
|
auto chunk = msg["content"].value( "text", "" );
|
||||||
|
writeToLastChat( chunk );
|
||||||
|
}
|
||||||
|
} else if ( sessionUpdate == "tool_call" ) {
|
||||||
|
std::string toolStr = "\n> Tool Call: " + msg.value( "title", "" ) + "\n";
|
||||||
|
writeToLastChat( toolStr );
|
||||||
|
} else if ( sessionUpdate == "plan" ) {
|
||||||
|
std::string planStr = "\n> Plan Updated:\n";
|
||||||
|
writeToLastChat( planStr );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mAgentSession->onRequestPermission = [this]( const auto& req, auto cb ) {
|
||||||
|
runOnMainThread( [this, req, cb]() { addPermissionUI( req, cb ); } );
|
||||||
|
};
|
||||||
|
|
||||||
|
mAgentSession->onTerminalCreated = []( UITerminal* term, const std::string& termId ) {
|
||||||
|
// This will be called from AgentSession.
|
||||||
|
};
|
||||||
|
|
||||||
|
mAgentSession->start( [this]( bool ready ) {
|
||||||
|
runOnMainThread( [this, ready]() {
|
||||||
|
if ( ready ) {
|
||||||
|
sendAgentPrompt();
|
||||||
|
} else {
|
||||||
|
auto chats = mChatsList->findAllByClass( "llm_conversation" );
|
||||||
|
if ( !chats.empty() ) {
|
||||||
|
auto* chat = chats.back();
|
||||||
|
auto* editor = chat->findByClass<UICodeEditor>( "data_ui" );
|
||||||
|
auto* thinking = editor->findByClass<UIImage>( "thinking" );
|
||||||
|
auto thinkingID = String::hash( String::format( "thinking-%p", thinking ) );
|
||||||
|
thinking->removeActionsByTag( thinkingID );
|
||||||
|
thinking->setVisible( false );
|
||||||
|
editor->setEnabled( true );
|
||||||
|
}
|
||||||
|
mChatStop->setVisible( false )->setEnabled( false );
|
||||||
|
mChatRun->setVisible( true )->setEnabled( true );
|
||||||
|
toggleEnableChats( true );
|
||||||
|
showMsg( "Failed to start agent process." );
|
||||||
|
mAgentSession.reset();
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
// Existing session, just send the prompt
|
||||||
|
mChatRun->setVisible( false )->setEnabled( false );
|
||||||
|
mChatStop->setVisible( true )->setEnabled( true );
|
||||||
|
|
||||||
|
UIWidget* chat = addChatUI( LLMChat::Role::Assistant );
|
||||||
|
toggleEnableChats( false );
|
||||||
|
auto* editor = chat->findByClass<UICodeEditor>( "data_ui" );
|
||||||
|
editor->setEnabled( false );
|
||||||
|
auto* thinking = editor->findByClass<UIImage>( "thinking" );
|
||||||
|
auto thinkingID = String::hash( String::format( "thinking-%p", thinking ) );
|
||||||
|
thinking->setVisible( true );
|
||||||
|
thinking->setPosition( { PixelDensity::dpToPx( 8 ), PixelDensity::dpToPx( 3 ) } );
|
||||||
|
thinking->setInterval( [thinking] { thinking->rotate( 360 / 32 ); }, Seconds( 0.125 ),
|
||||||
|
thinkingID );
|
||||||
|
sendAgentPrompt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LLMChatUI::sendAgentPrompt() {
|
||||||
|
acp::PromptRequest req;
|
||||||
|
req.sessionId = mAgentSession->getSessionId();
|
||||||
|
|
||||||
|
// Create prompt from the last user message
|
||||||
|
auto chats = findAllByClass( "llm_conversation" );
|
||||||
|
if ( !chats.empty() ) {
|
||||||
|
auto* lastChat =
|
||||||
|
chats[chats.size() - 2]; // Assistant is the last one (added in doAgentRequest)
|
||||||
|
auto* editor = lastChat->findByClass<UICodeEditor>( "data_ui" );
|
||||||
|
std::string text = editor->getDocument().getText().toUtf8();
|
||||||
|
replaceFileLinksToContents( text );
|
||||||
|
req.prompt = { { { "type", "text" }, { "text", text } } };
|
||||||
|
} else {
|
||||||
|
req.prompt = { { { "type", "text" }, { "text", "" } } };
|
||||||
|
}
|
||||||
|
|
||||||
|
mAgentSession->prompt( req, [this]( const acp::PromptResponse& res ) {
|
||||||
|
runOnMainThread( [this, res]() {
|
||||||
|
auto chats = mChatsList->findAllByClass( "llm_conversation" );
|
||||||
|
if ( !chats.empty() ) {
|
||||||
|
auto* chat = chats[chats.size() - 1];
|
||||||
|
auto* editor = chat->findByClass<UICodeEditor>( "data_ui" );
|
||||||
|
auto* thinking = editor->findByClass<UIImage>( "thinking" );
|
||||||
|
if ( thinking ) {
|
||||||
|
auto thinkingID = String::hash( String::format( "thinking-%p", thinking ) );
|
||||||
|
thinking->removeActionsByTag( thinkingID );
|
||||||
|
thinking->setVisible( false );
|
||||||
|
}
|
||||||
|
editor->setEnabled( true );
|
||||||
|
if ( editor->hasFocus() )
|
||||||
|
mChatInput->setFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleEnableChats( true );
|
||||||
|
mChatStop->setVisible( false )->setEnabled( false );
|
||||||
|
mChatRun->setVisible( true )->setEnabled( true );
|
||||||
|
|
||||||
|
if ( res.stopReason != "cancelled" ) {
|
||||||
|
saveChat();
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
void LLMChatUI::resizeToFit( UICodeEditor* editor ) {
|
void LLMChatUI::resizeToFit( UICodeEditor* editor ) {
|
||||||
Float visibleLineCount = editor->getDocumentView().getVisibleLinesCount();
|
Float visibleLineCount = editor->getDocumentView().getVisibleLinesCount();
|
||||||
Float lineHeight = editor->getLineHeight();
|
Float lineHeight = editor->getLineHeight();
|
||||||
@@ -1351,6 +1573,55 @@ Drawable* LLMChatUI::findIcon( const std::string& name, const size_t iconSize )
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LLMChatUI::addPermissionUI( const acp::RequestPermissionRequest& req,
|
||||||
|
std::function<void( const acp::RequestPermissionResponse& )> cb ) {
|
||||||
|
find( "chat_presentation" )->setVisible( false );
|
||||||
|
|
||||||
|
UIWidget* chat =
|
||||||
|
mChatsList->getUISceneNode()->loadLayoutFromString( DEFAULT_PERMISSION_GLOBE, mChatsList );
|
||||||
|
|
||||||
|
UITextView* desc = chat->findByClass<UITextView>( "permission_desc" );
|
||||||
|
std::string descStr = "The agent wants to execute a tool call:\n";
|
||||||
|
descStr += "Title: " + req.toolCall.title + "\n";
|
||||||
|
descStr += "Kind: " + req.toolCall.kind + "\n";
|
||||||
|
if ( !req.toolCall.rawInput.is_null() ) {
|
||||||
|
descStr += "Input:\n" + req.toolCall.rawInput.dump( 2 ) + "\n";
|
||||||
|
}
|
||||||
|
desc->setText( descStr );
|
||||||
|
|
||||||
|
UIWidget* optionsBox = chat->findByClass( "permission_options" );
|
||||||
|
|
||||||
|
bool isFirst = true;
|
||||||
|
for ( const auto& opt : req.options ) {
|
||||||
|
UIPushButton* but = UIPushButton::New();
|
||||||
|
but->setParent( optionsBox );
|
||||||
|
but->setText( opt.name );
|
||||||
|
but->setTooltipText( opt.kind );
|
||||||
|
but->setLayoutMargin( Rectf( 0, 0, 8, 0 ) );
|
||||||
|
|
||||||
|
auto cbCopy = cb;
|
||||||
|
auto optId = opt.optionId;
|
||||||
|
but->onClick( [chat, cbCopy, optId, this]( const Event* ) {
|
||||||
|
chat->close(); // Close the permission request UI once selected
|
||||||
|
acp::RequestPermissionResponse res;
|
||||||
|
res.outcome = "selected";
|
||||||
|
res.optionId = optId;
|
||||||
|
cbCopy( res );
|
||||||
|
|
||||||
|
if ( mChatInput ) {
|
||||||
|
mChatInput->setFocus();
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
if ( isFirst ) {
|
||||||
|
isFirst = false;
|
||||||
|
but->setFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mChatScrollView->setAnchorScroll( true );
|
||||||
|
}
|
||||||
|
|
||||||
UIWidget* LLMChatUI::addChatUI( LLMChat::Role role ) {
|
UIWidget* LLMChatUI::addChatUI( LLMChat::Role role ) {
|
||||||
find( "chat_presentation" )->setVisible( false );
|
find( "chat_presentation" )->setVisible( false );
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "../pluginmanager.hpp"
|
#include "../pluginmanager.hpp"
|
||||||
|
#include "agentsession.hpp"
|
||||||
#include "llmchatcompletionrequest.hpp"
|
#include "llmchatcompletionrequest.hpp"
|
||||||
#include "protocol.hpp"
|
#include "protocol.hpp"
|
||||||
|
|
||||||
@@ -91,6 +92,8 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter {
|
|||||||
|
|
||||||
bool chatExistsInDisk() const;
|
bool chatExistsInDisk() const;
|
||||||
|
|
||||||
|
const std::string& getCurAgent() const { return mCurAgent; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
UUID mUUID;
|
UUID mUUID;
|
||||||
std::string mSummary;
|
std::string mSummary;
|
||||||
@@ -109,8 +112,10 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter {
|
|||||||
UIPushButton* mRefreshModels{ nullptr };
|
UIPushButton* mRefreshModels{ nullptr };
|
||||||
UIPushButton* mChatAttach{ nullptr };
|
UIPushButton* mChatAttach{ nullptr };
|
||||||
UISelectButton* mChatPrivate{ nullptr };
|
UISelectButton* mChatPrivate{ nullptr };
|
||||||
|
UISelectButton* mChatAgentMode{ nullptr };
|
||||||
UIScrollView* mChatScrollView{ nullptr };
|
UIScrollView* mChatScrollView{ nullptr };
|
||||||
UIDropDownList* mModelDDL{ nullptr };
|
UIDropDownList* mModelDDL{ nullptr };
|
||||||
|
UIDropDownList* mAgentDDL{ nullptr };
|
||||||
UIVLinearLayoutCommandExecuter* mLocateBarLayout{ nullptr };
|
UIVLinearLayoutCommandExecuter* mLocateBarLayout{ nullptr };
|
||||||
UITextInput* mLocateInput{ nullptr };
|
UITextInput* mLocateInput{ nullptr };
|
||||||
UITableView* mLocateTable{ nullptr };
|
UITableView* mLocateTable{ nullptr };
|
||||||
@@ -120,8 +125,15 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter {
|
|||||||
LLMProviders mProviders;
|
LLMProviders mProviders;
|
||||||
LLMModel mCurModel;
|
LLMModel mCurModel;
|
||||||
std::unordered_map<String::HashType, LLMModel> mModelsMap;
|
std::unordered_map<String::HashType, LLMModel> mModelsMap;
|
||||||
|
|
||||||
|
std::map<std::string, ACPAgent> mAgents;
|
||||||
|
std::string mCurAgent;
|
||||||
|
|
||||||
|
std::unique_ptr<acp::AgentSession> mAgentSession;
|
||||||
|
|
||||||
int mPendingModelsToLoad{ 0 };
|
int mPendingModelsToLoad{ 0 };
|
||||||
bool mChatIsPrivate{ false };
|
bool mChatIsPrivate{ false };
|
||||||
|
bool mIsAgentMode{ false };
|
||||||
bool mChatLocked{ false };
|
bool mChatLocked{ false };
|
||||||
bool mLinkMode{ false };
|
bool mLinkMode{ false };
|
||||||
|
|
||||||
@@ -143,6 +155,10 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter {
|
|||||||
|
|
||||||
void doRequest();
|
void doRequest();
|
||||||
|
|
||||||
|
void doAgentRequest();
|
||||||
|
|
||||||
|
void sendAgentPrompt();
|
||||||
|
|
||||||
void toggleEnableChat( UIWidget* chat, bool enabled );
|
void toggleEnableChat( UIWidget* chat, bool enabled );
|
||||||
|
|
||||||
void toggleEnableChats( bool enabled );
|
void toggleEnableChats( bool enabled );
|
||||||
@@ -151,6 +167,9 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter {
|
|||||||
|
|
||||||
UIWidget* addChatUI( LLMChat::Role role );
|
UIWidget* addChatUI( LLMChat::Role role );
|
||||||
|
|
||||||
|
void addPermissionUI( const acp::RequestPermissionRequest& req,
|
||||||
|
std::function<void( const acp::RequestPermissionResponse& )> cb );
|
||||||
|
|
||||||
void fillApiModels( UIDropDownList* modelDDL );
|
void fillApiModels( UIDropDownList* modelDDL );
|
||||||
|
|
||||||
String getModelDisplayName( const LLMModel& model ) const;
|
String getModelDisplayName( const LLMModel& model ) const;
|
||||||
@@ -159,10 +178,14 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter {
|
|||||||
|
|
||||||
void fillModelDropDownList( UIDropDownList* modelDDL );
|
void fillModelDropDownList( UIDropDownList* modelDDL );
|
||||||
|
|
||||||
|
void fillAgentDropDownList( UIDropDownList* agentDDL );
|
||||||
|
|
||||||
void resizeToFit( UICodeEditor* editor );
|
void resizeToFit( UICodeEditor* editor );
|
||||||
|
|
||||||
void addChat( LLMChat::Role role, std::string conversation );
|
void addChat( LLMChat::Role role, std::string conversation );
|
||||||
|
|
||||||
|
void writeToLastChat( const std::string& text );
|
||||||
|
|
||||||
void removeLastChat();
|
void removeLastChat();
|
||||||
|
|
||||||
void setProviders( LLMProviders&& providers );
|
void setProviders( LLMProviders&& providers );
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace ecode {
|
namespace ecode {
|
||||||
|
|
||||||
@@ -40,4 +41,14 @@ struct LLMProvider {
|
|||||||
|
|
||||||
using LLMProviders = std::map<std::string, LLMProvider>;
|
using LLMProviders = std::map<std::string, LLMProvider>;
|
||||||
|
|
||||||
|
struct ACPAgent {
|
||||||
|
bool enabled{ true };
|
||||||
|
std::string name;
|
||||||
|
std::string command;
|
||||||
|
std::vector<std::string> args;
|
||||||
|
std::unordered_map<std::string, std::string> environment;
|
||||||
|
};
|
||||||
|
|
||||||
|
using ACPAgents = std::map<std::string, ACPAgent>;
|
||||||
|
|
||||||
} // namespace ecode
|
} // namespace ecode
|
||||||
Reference in New Issue
Block a user