mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-27 08:37:58 +03:00
Merge branch 'feature/acp' into develop
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
# Agent Identity: Negen
|
||||
|
||||
**Role & Personality:**
|
||||
Your name is Negen (from negentropy: the process of creating order out of chaos). You are an elite, performance-obsessed C++ coding agent embedded within the `eepp` repository and `ecode` ecosystem. Your tone is sharp, analytical, and highly efficient. You treat system resources as sacred and write code that is clean, fast, and secure.
|
||||
|
||||
**Core Directives:**
|
||||
|
||||
1. **Performance & Memory Management:**
|
||||
- Performance is the absolute key in `eepp`.
|
||||
- Favor stack-allocated memory over heap allocations whenever possible.
|
||||
- Any heap allocation must be heavily justified.
|
||||
- Exercise reason: maximize stack use for speed, but actively calculate boundaries to prevent stack-overflows.
|
||||
|
||||
2. **Protect the Render Loop:**
|
||||
- Render time is critical.
|
||||
- You must be intensely mindful of the performance impact of any code executing within the main loop or main thread. Avoid blocking operations or expensive computations during the render cycle at all costs.
|
||||
|
||||
3. **Self-Review & DRY Principle:**
|
||||
- Immediately after implementing a feature, you must perform a strict self-review of your changes.
|
||||
- Actively hunt for mistakes and inefficiencies.
|
||||
- Eradicate code duplication. Whenever common logic is detected, encapsulate it into a distinct, reusable function or method.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Plan: ACP Support Improvement for ecode
|
||||
|
||||
This plan outlines the steps to complete and improve the Agent Client Protocol (ACP) implementation in `ecode`.
|
||||
|
||||
## Current Status & Gaps
|
||||
The current implementation has the basic structure but misses several critical features for a full ACP experience:
|
||||
- **Terminal output** is not captured (returns empty).
|
||||
- **Tool call updates** are not handled in the UI.
|
||||
- **Plan schema** is non-standard.
|
||||
- **Slash commands** are not exposed to the user.
|
||||
- **Terminal limits** are ignored.
|
||||
|
||||
## Proposed Steps
|
||||
|
||||
### 1. Research & Refinement (Done)
|
||||
- [x] Analyze ACP documentation.
|
||||
- [x] Review current `ecode` implementation.
|
||||
- [x] Identify missing pieces.
|
||||
|
||||
### 2. Phase 1: Terminal Output Capture (Done)
|
||||
We need a way to capture the output from the terminal process to satisfy `terminal/output` requests.
|
||||
- [x] Modify `src/modules/eterm/include/eterm/terminal/terminalemulator.hpp`:
|
||||
- Add `using DataCb = std::function<void(const char*, size_t)>;`
|
||||
- Add `void setDataCb(DataCb cb);`
|
||||
- [x] Modify `src/modules/eterm/src/eterm/terminal/terminalemulator.cpp`:
|
||||
- Call `mDataCb` in `ttyread()` when new data is received.
|
||||
- [x] Update `src/tools/ecode/plugins/aiassistant/acp/agentsession.hpp`:
|
||||
- Add an output buffer to `TermData`.
|
||||
- [x] Update `src/tools/ecode/plugins/aiassistant/acp/agentsession.cpp`:
|
||||
- Set the `DataCb` in `onTerminalCreated`.
|
||||
- Implement `onTerminalOutput` to return and clear the buffer.
|
||||
- Respect `outputByteLimit`.
|
||||
|
||||
### 3. Phase 2: UI & Protocol Updates
|
||||
- [ ] Update `src/tools/ecode/plugins/aiassistant/chatui.cpp`:
|
||||
- Fix `plan` update handling to use the standard `entries` schema.
|
||||
- Implement `tool_call_update` handling.
|
||||
- Update tool call UI to reflect status (pending, in_progress, completed, failed, cancelled).
|
||||
- [ ] Implement embedded terminals in tool calls:
|
||||
- When a `tool_call_update` contains a `terminalId`, link the terminal UI to that tool call bubble if possible.
|
||||
|
||||
### 4. Phase 3: UX Improvements
|
||||
- [ ] Expose slash commands:
|
||||
- Show `mAvailableCommands` in the UI.
|
||||
- Add simple autocompletion for `/` commands in `mChatInput`.
|
||||
- [ ] Enhance terminal management:
|
||||
- Ensure `terminal/wait_for_exit` works correctly (async).
|
||||
|
||||
### 5. Phase 4: Validation
|
||||
- [ ] Test with a compatible ACP agent.
|
||||
- [ ] Verify file system tools.
|
||||
- [ ] Verify terminal creation and output reading.
|
||||
- [ ] Verify plan updates.
|
||||
- [ ] Verify tool call permissions.
|
||||
Binary file not shown.
@@ -500,5 +500,31 @@
|
||||
"fetch_models_url": "http://localhost:8080/api/tags",
|
||||
"open_api": true
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"gemini-cli": {
|
||||
"enabled": true,
|
||||
"command": "gemini",
|
||||
"args": ["--experimental-acp", "--model=auto"]
|
||||
},
|
||||
"opencode": {
|
||||
"enabled": true,
|
||||
"command": "opencode",
|
||||
"args": ["acp"]
|
||||
},
|
||||
"claude-agent": {
|
||||
"enabled": true,
|
||||
"command": "claude-agent-acp"
|
||||
},
|
||||
"cursor": {
|
||||
"enabled": true,
|
||||
"command": "cursor",
|
||||
"args": ["acp"]
|
||||
},
|
||||
"pi-acp": {
|
||||
"enabled": true,
|
||||
"command": "pi-acp",
|
||||
"args": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,8 @@ class EE_API FontTrueType : public Font {
|
||||
|
||||
bool loaded() const;
|
||||
|
||||
FontTrueType& operator=( const FontTrueType& right );
|
||||
FontTrueType( const FontTrueType& ) = delete;
|
||||
FontTrueType& operator=( const FontTrueType& ) = delete;
|
||||
|
||||
bool getBoldAdvanceSameAsRegular() const;
|
||||
|
||||
@@ -182,11 +183,11 @@ class EE_API FontTrueType : public Font {
|
||||
|
||||
GlyphTable glyphs; ///< Table mapping code points to their corresponding glyph
|
||||
GlyphDrawableTable
|
||||
drawables; ///> Table mapping code points to their corresponding glyph drawables.
|
||||
Texture* texture; ///< Texture containing the pixels of the glyphs
|
||||
std::vector<Row> rows; ///< List containing the position of all the existing rows
|
||||
Uint32 fontInternalId{ 0 }; // The font internal id
|
||||
unsigned int nextRow; ///< Y position of the next new row in the texture
|
||||
drawables; ///> Table mapping code points to their corresponding glyph drawables.
|
||||
Texture* texture{ nullptr }; ///< Texture containing the pixels of the glyphs
|
||||
std::vector<Row> rows; ///< List containing the position of all the existing rows
|
||||
Uint32 fontInternalId{ 0 }; // The font internal id
|
||||
unsigned int nextRow; ///< Y position of the next new row in the texture
|
||||
const FontTrueType* font{ nullptr };
|
||||
};
|
||||
|
||||
@@ -212,14 +213,14 @@ class EE_API FontTrueType : public Font {
|
||||
typedef UnorderedMap<unsigned int, std::unique_ptr<Page>>
|
||||
PageTable; ///< Table mapping a character size to its page (texture)
|
||||
|
||||
void* mLibrary; ///< Pointer to the internal library interface (it is typeless to avoid exposing
|
||||
///< implementation details)
|
||||
void* mFace; ///< Pointer to the internal font face (it is typeless to avoid exposing
|
||||
///< implementation details)
|
||||
void* mStreamRec; ///< Pointer to the stream rec instance (it is typeless to avoid exposing
|
||||
///< implementation details)
|
||||
void* mStroker; ///< Pointer to the stroker (it is typeless to avoid exposing implementation
|
||||
///< details)
|
||||
void* mLibrary{ nullptr }; ///< Pointer to the internal library interface (it is typeless to
|
||||
///< avoid exposing implementation details)
|
||||
void* mFace{ nullptr }; ///< Pointer to the internal font face (it is typeless to avoid exposing
|
||||
///< implementation details)
|
||||
void* mStreamRec{ nullptr }; ///< Pointer to the stream rec instance (it is typeless to avoid
|
||||
///< exposing implementation details)
|
||||
void* mStroker{ nullptr }; ///< Pointer to the stroker (it is typeless to avoid exposing
|
||||
///< implementation details)
|
||||
void* mHBFont{ nullptr };
|
||||
mutable ScopedBuffer mMemCopy; ///< If loaded from memory, this is the file copy in memory
|
||||
Font::Info mInfo; ///< Information about the font
|
||||
|
||||
@@ -316,7 +316,15 @@ template <class T> T* ResourceManagerMulti<T>::add( T* resource ) {
|
||||
|
||||
template <class T> bool ResourceManagerMulti<T>::remove( T* resource, bool remove ) {
|
||||
if ( NULL != resource ) {
|
||||
mResources.erase( resource->getId() );
|
||||
auto range = mResources.equal_range( resource->getId() );
|
||||
auto it = range.first;
|
||||
while ( it != range.second ) {
|
||||
if ( it->second == resource ) {
|
||||
mResources.erase( it );
|
||||
break;
|
||||
}
|
||||
it++;
|
||||
}
|
||||
|
||||
if ( remove )
|
||||
eeSAFE_DELETE( resource );
|
||||
|
||||
@@ -155,6 +155,13 @@ template <typename K, typename V> class ItemPairListOwnerModel final : public Mo
|
||||
return std::make_shared<ItemPairListOwnerModel<K, V>>( data );
|
||||
}
|
||||
|
||||
static std::shared_ptr<ItemPairListOwnerModel> create( std::vector<std::pair<K, V>>&& data ) {
|
||||
return std::make_shared<ItemPairListOwnerModel<K, V>>( std::move( data ) );
|
||||
}
|
||||
|
||||
explicit ItemPairListOwnerModel( std::vector<std::pair<K, V>>&& data ) :
|
||||
mData( std::move( data ) ) {}
|
||||
|
||||
explicit ItemPairListOwnerModel( const std::vector<std::pair<K, V>>& data ) : mData( data ) {}
|
||||
|
||||
virtual ~ItemPairListOwnerModel() {}
|
||||
|
||||
@@ -62,6 +62,7 @@ LOCAL_C_INCLUDES := $(EEPP_C_INCLUDES)
|
||||
CORE_SRCS := tools/ecode/*.cpp \
|
||||
tools/ecode/plugins/*.cpp \
|
||||
tools/ecode/plugins/aiassistant/*.cpp \
|
||||
tools/ecode/plugins/aiassistant/acp/*.cpp \
|
||||
tools/ecode/plugins/autocomplete/*.cpp \
|
||||
tools/ecode/plugins/debugger/*.cpp \
|
||||
tools/ecode/plugins/debugger/dap/*.cpp \
|
||||
|
||||
@@ -7,7 +7,11 @@ SINGLETON_DECLARE_IMPLEMENTATION( FontManager )
|
||||
|
||||
FontManager::FontManager() {}
|
||||
|
||||
FontManager::~FontManager() {}
|
||||
FontManager::~FontManager() {
|
||||
mEmojiFont = nullptr;
|
||||
mColorEmojiFont = nullptr;
|
||||
mFallbackFonts.clear();
|
||||
}
|
||||
|
||||
Graphics::Font* FontManager::add( Graphics::Font* font ) {
|
||||
eeASSERT( NULL != font );
|
||||
|
||||
@@ -251,10 +251,31 @@ FontTrueType::FontTrueType( const std::string& FontName ) :
|
||||
mFace( NULL ),
|
||||
mStreamRec( NULL ),
|
||||
mStroker( NULL ),
|
||||
mInfo(),
|
||||
mHBFont( NULL ),
|
||||
mFontInternalId( 0 ),
|
||||
mBoldAdvanceSameAsRegular( false ),
|
||||
mIsColorEmojiFont( false ),
|
||||
mIsEmojiFont( false ),
|
||||
mHasSvgGlyphs( false ),
|
||||
mHasColrGlyphs( false ),
|
||||
mIsBitmapOnly( false ),
|
||||
mIsMonospace( false ),
|
||||
mIsMonospaceComplete( false ),
|
||||
mUsingFallback( false ),
|
||||
mEnableEmojiFallback( true ),
|
||||
mEnableFallbackFont( true ),
|
||||
mEnableDynamicMonospace( false ),
|
||||
mIsBold( false ),
|
||||
mIsItalic( false ),
|
||||
mIsMonospaceCompletePending( false ),
|
||||
mHinting( FontManager::instance()->getHinting() ),
|
||||
mAntialiasing( FontManager::instance()->getAntialiasing() ) {}
|
||||
mAntialiasing( FontManager::instance()->getAntialiasing() ),
|
||||
mFontBold( nullptr ),
|
||||
mFontItalic( nullptr ),
|
||||
mFontBoldItalic( nullptr ),
|
||||
mFontBoldCb( 0 ),
|
||||
mFontItalicCb( 0 ),
|
||||
mFontBoldItalicCb( 0 ) {}
|
||||
|
||||
FontTrueType::~FontTrueType() {
|
||||
cleanup();
|
||||
@@ -965,20 +986,6 @@ bool FontTrueType::loaded() const {
|
||||
return NULL != mFace;
|
||||
}
|
||||
|
||||
FontTrueType& FontTrueType::operator=( const FontTrueType& right ) {
|
||||
FontTrueType temp( right.getName() );
|
||||
|
||||
temp.mMemCopy.swap( right.mMemCopy );
|
||||
std::swap( mLibrary, temp.mLibrary );
|
||||
std::swap( mFace, temp.mFace );
|
||||
std::swap( mStreamRec, temp.mStreamRec );
|
||||
std::swap( mStroker, temp.mStroker );
|
||||
std::swap( mInfo, temp.mInfo );
|
||||
std::swap( mPages, temp.mPages );
|
||||
std::swap( mPixelBuffer, temp.mPixelBuffer );
|
||||
return *this;
|
||||
}
|
||||
|
||||
void FontTrueType::cleanup() {
|
||||
sendEvent( Event::Unload );
|
||||
|
||||
@@ -1028,9 +1035,36 @@ void FontTrueType::cleanup() {
|
||||
mLibrary = NULL;
|
||||
mFace = NULL;
|
||||
mStroker = NULL;
|
||||
mHBFont = NULL;
|
||||
mStreamRec = NULL;
|
||||
mInfo = Info();
|
||||
mFontInternalId = 0;
|
||||
mBoldAdvanceSameAsRegular = false;
|
||||
mIsColorEmojiFont = false;
|
||||
mIsEmojiFont = false;
|
||||
mHasSvgGlyphs = false;
|
||||
mHasColrGlyphs = false;
|
||||
mIsBitmapOnly = false;
|
||||
mIsMonospace = false;
|
||||
mIsMonospaceComplete = false;
|
||||
mUsingFallback = false;
|
||||
mEnableEmojiFallback = true;
|
||||
mEnableFallbackFont = true;
|
||||
mEnableDynamicMonospace = false;
|
||||
mIsBold = false;
|
||||
mIsItalic = false;
|
||||
mIsMonospaceCompletePending = false;
|
||||
mFontBold = nullptr;
|
||||
mFontItalic = nullptr;
|
||||
mFontBoldItalic = nullptr;
|
||||
mFontBoldCb = 0;
|
||||
mFontItalicCb = 0;
|
||||
mFontBoldItalicCb = 0;
|
||||
mPages.clear();
|
||||
std::vector<Uint8>().swap( mPixelBuffer );
|
||||
mCodePointIndexCache.clear();
|
||||
mKeyCache.clear();
|
||||
mClosestCharacterSize.clear();
|
||||
}
|
||||
|
||||
static int fontSetLoadOptions( FontAntialiasing antialiasing, FontHinting hinting ) {
|
||||
@@ -1430,6 +1464,10 @@ bool FontTrueType::setCurrentSize( unsigned int characterSize ) const {
|
||||
// only when necessary to avoid killing performances
|
||||
|
||||
FT_Face face = static_cast<FT_Face>( mFace );
|
||||
|
||||
if ( !face || !face->size )
|
||||
return false;
|
||||
|
||||
FT_UShort currentSize = face->size->metrics.x_ppem;
|
||||
|
||||
if ( currentSize != characterSize ) {
|
||||
|
||||
@@ -299,6 +299,7 @@ UIIconTheme* IconManager::init( const std::string& iconThemeName, FontTrueType*
|
||||
{ "chat-sparkle", 0xec4f },
|
||||
{ "inspect", 0xebd1 },
|
||||
{ "link", 0xeb15 },
|
||||
{ "agent", 0xec67 },
|
||||
|
||||
} ) {
|
||||
iconTheme->add( UIGlyphIcon::New( icon.first, codIconFont, icon.second ) );
|
||||
|
||||
@@ -112,15 +112,15 @@ UIDropDownList* UIDropDownList::showList() {
|
||||
|
||||
Float width = getPopUpWidth( contentsWidth );
|
||||
|
||||
Float height =
|
||||
(Int32)( eemin( mListBox->getItemsCount(), mStyleConfig.MaxNumVisibleItems ) *
|
||||
mListBox->getRowHeight() ) +
|
||||
Float height = std::ceil(
|
||||
std::ceil( eemin( mListBox->getItemsCount(), mStyleConfig.MaxNumVisibleItems ) *
|
||||
PixelDensity::pxToDp( mListBox->getRowHeight() ) ) +
|
||||
tPadding.Top + tPadding.Bottom +
|
||||
( mListBox->getHorizontalScrollBar() &&
|
||||
mListBox->getHorizontalScrollBar()->isVisible() &&
|
||||
PixelDensity::dpToPx( width ) < mListBox->getMaxTextWidth()
|
||||
? mListBox->getHorizontalScrollBar()->getSize().getHeight()
|
||||
: 0.f );
|
||||
: 0.f ) );
|
||||
|
||||
mListBox->setSize( width, height );
|
||||
mListBox->getVerticalScrollBar()->setValue( sliderValue );
|
||||
|
||||
@@ -167,14 +167,14 @@ UIDropDownModelList* UIDropDownModelList::showList() {
|
||||
|
||||
Float width = getPopUpWidth( contentsWidth );
|
||||
|
||||
Float height =
|
||||
(Int32)( eemin( (Uint32)mModel->rowCount(), mStyleConfig.MaxNumVisibleItems ) *
|
||||
mListView->getRowHeight() ) +
|
||||
Float height = std::ceil(
|
||||
std::ceil( eemin( (Uint32)mModel->rowCount(), mStyleConfig.MaxNumVisibleItems ) *
|
||||
PixelDensity::pxToDp( mListView->getRowHeight() ) ) +
|
||||
tPadding.Top + tPadding.Bottom + mListView->getHeaderHeight() +
|
||||
( mListView->getHorizontalScrollBar() &&
|
||||
mListView->getHorizontalScrollBar()->isVisible()
|
||||
? mListView->getHorizontalScrollBar()->getSize().getHeight()
|
||||
: 0.f );
|
||||
: 0.f ) );
|
||||
|
||||
mListView->setSize( width, height );
|
||||
|
||||
|
||||
@@ -131,8 +131,7 @@ void UISelectButton::setSelected( bool set ) {
|
||||
void UISelectButton::setSelectOnClick( bool set ) {
|
||||
if ( set ) {
|
||||
if ( mSelectOnClickCbId == 0 )
|
||||
mSelectOnClickCbId =
|
||||
on( Event::MouseClick, [this]( const Event* ) { toggleSelection(); } );
|
||||
mSelectOnClickCbId = onClick( [this]( const Event* ) { toggleSelection(); } );
|
||||
} else {
|
||||
if ( mSelectOnClickCbId != 0 )
|
||||
removeEventListener( mSelectOnClickCbId );
|
||||
|
||||
@@ -254,6 +254,10 @@ class TerminalEmulator final {
|
||||
mPromptStateChangedCb = promptStateChangedCb;
|
||||
}
|
||||
|
||||
using DataCb = std::function<void( const char*, size_t )>;
|
||||
|
||||
void setDataCb( DataCb cb ) { mDataCb = cb; }
|
||||
|
||||
int getTerminalMode() const { return mTerm.mode; }
|
||||
|
||||
private:
|
||||
@@ -291,6 +295,7 @@ class TerminalEmulator final {
|
||||
std::string mCurrentWorkingDirectory;
|
||||
PromptState mPromptState{ PromptState::Unknown };
|
||||
PromptStateChangedCb mPromptStateChangedCb;
|
||||
DataCb mDataCb;
|
||||
|
||||
void setClipboard( const char* str );
|
||||
|
||||
|
||||
@@ -653,6 +653,9 @@ size_t TerminalEmulator::ttyread( void ) {
|
||||
_die( "couldn't read from shell: %s\n", strerror( errno ) );
|
||||
return 0;
|
||||
default: {
|
||||
if ( mDataCb )
|
||||
mDataCb( mBuf + mBuflen, ret );
|
||||
|
||||
int old_scr = mTerm.scr;
|
||||
int old_histi = mTerm.histi;
|
||||
mTerm.scr = 0;
|
||||
|
||||
@@ -82,6 +82,7 @@ UTEST( LuaPattern, basicTest ) {
|
||||
EXPECT_EQ( start, 14 );
|
||||
EXPECT_EQ( end, 16 );
|
||||
}
|
||||
RegExCache::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( RegExEngines, basicTest ) {
|
||||
@@ -96,4 +97,5 @@ UTEST( RegExEngines, basicTest ) {
|
||||
EXPECT_EQ( 38, matchesPCRE2[0].end );
|
||||
EXPECT_EQ( 38, matchesOniguruma[0].start );
|
||||
EXPECT_EQ( 38, matchesOniguruma[0].end );
|
||||
RegExCache::destroySingleton();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
#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 );
|
||||
} else if ( msg.contains( "error" ) && onError ) {
|
||||
onError( ResponseError( msg["error"] ) );
|
||||
}
|
||||
}
|
||||
|
||||
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&,
|
||||
const std::optional<ResponseError>& )>& 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"] ), std::nullopt );
|
||||
} else if ( resp.contains( "error" ) ) {
|
||||
if ( cb )
|
||||
cb( {}, ResponseError( resp["error"] ) );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
void ACPClient::newSession( const NewSessionRequest& req,
|
||||
const std::function<void( const NewSessionResponse&,
|
||||
const std::optional<ResponseError>& )>& cb ) {
|
||||
write( { { "method", "session/new" }, { "params", req.toJson() } },
|
||||
[cb]( const IdType&, const json& resp ) {
|
||||
if ( resp.contains( "result" ) && cb ) {
|
||||
cb( NewSessionResponse( resp["result"] ), std::nullopt );
|
||||
} else if ( resp.contains( "error" ) && cb ) {
|
||||
cb( {}, ResponseError( resp["error"] ) );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
void ACPClient::loadSession(
|
||||
const LoadSessionRequest& req,
|
||||
const std::function<void( const LoadSessionResponse&, const std::optional<ResponseError>& )>&
|
||||
cb ) {
|
||||
write( { { "method", "session/load" }, { "params", req.toJson() } },
|
||||
[cb]( const IdType&, const json& resp ) {
|
||||
if ( resp.contains( "result" ) && cb ) {
|
||||
cb( LoadSessionResponse( resp["result"] ), std::nullopt );
|
||||
} else if ( resp.contains( "error" ) && cb ) {
|
||||
cb( {}, ResponseError( resp["error"] ) );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
void ACPClient::setConfigOption(
|
||||
const SetConfigOptionRequest& req,
|
||||
const std::function<void( const SetConfigOptionResponse&,
|
||||
const std::optional<ResponseError>& )>& cb ) {
|
||||
auto fallback = [this, req, cb]() {
|
||||
if ( req.configId == "model" ) {
|
||||
write(
|
||||
{ { "method", "session/set_model" },
|
||||
{ "params", { { "sessionId", req.sessionId }, { "modelId", req.optionId } } } },
|
||||
[req, cb]( const IdType&, const json& resp2 ) {
|
||||
if ( resp2.contains( "result" ) && cb ) {
|
||||
cb( SetConfigOptionResponse( resp2["result"], req.configId, req.optionId ),
|
||||
std::nullopt );
|
||||
} else if ( resp2.contains( "error" ) && cb ) {
|
||||
cb( {}, ResponseError( resp2["error"] ) );
|
||||
}
|
||||
} );
|
||||
} else if ( req.configId == "mode" ) {
|
||||
write(
|
||||
{ { "method", "session/set_mode" },
|
||||
{ "params", { { "sessionId", req.sessionId }, { "modeId", req.optionId } } } },
|
||||
[req, cb]( const IdType&, const json& resp2 ) {
|
||||
if ( resp2.contains( "result" ) && cb ) {
|
||||
cb( SetConfigOptionResponse( resp2["result"], req.configId, req.optionId ),
|
||||
std::nullopt );
|
||||
} else if ( resp2.contains( "error" ) && cb ) {
|
||||
cb( {}, ResponseError( resp2["error"] ) );
|
||||
}
|
||||
} );
|
||||
} else if ( cb ) {
|
||||
cb( {}, ResponseError{ -32601, "Method not found" } );
|
||||
}
|
||||
};
|
||||
|
||||
if ( mLegacyConfigOnly ) {
|
||||
fallback();
|
||||
return;
|
||||
}
|
||||
|
||||
write( { { "method", "session/set_config_option" }, { "params", req.toJson() } },
|
||||
[this, req, fallback, cb]( const IdType&, const json& resp ) {
|
||||
if ( resp.contains( "result" ) && cb ) {
|
||||
cb( SetConfigOptionResponse( resp["result"], req.configId, req.optionId ),
|
||||
std::nullopt );
|
||||
} else if ( resp.contains( "error" ) ) {
|
||||
ResponseError err( resp["error"] );
|
||||
if ( err.code == -32601 ) {
|
||||
mLegacyConfigOnly = true;
|
||||
fallback();
|
||||
return;
|
||||
}
|
||||
if ( cb )
|
||||
cb( {}, err );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
void ACPClient::listSessions(
|
||||
const ListSessionsRequest& req,
|
||||
const std::function<void( const ListSessionsResponse&, const std::optional<ResponseError>& )>&
|
||||
cb ) {
|
||||
write( { { "method", "session/list" }, { "params", req.toJson() } },
|
||||
[cb]( const IdType&, const json& resp ) {
|
||||
if ( resp.contains( "result" ) && cb ) {
|
||||
cb( ListSessionsResponse( resp["result"] ), std::nullopt );
|
||||
} else if ( resp.contains( "error" ) && cb ) {
|
||||
cb( {}, ResponseError( resp["error"] ) );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
void ACPClient::prompt(
|
||||
const PromptRequest& req,
|
||||
const std::function<void( const PromptResponse&, const std::optional<ResponseError>& )>& cb ) {
|
||||
write( { { "method", "session/prompt" }, { "params", req.toJson() } },
|
||||
[cb]( const IdType&, const json& resp ) {
|
||||
if ( resp.contains( "result" ) && cb ) {
|
||||
cb( PromptResponse( resp["result"] ), std::nullopt );
|
||||
} else if ( resp.contains( "error" ) && cb ) {
|
||||
cb( {}, ResponseError( resp["error"] ) );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
void ACPClient::cancel( const std::string& sessionId ) {
|
||||
write( { { "method", "session/cancel" }, { "params", { { "sessionId", sessionId } } } } );
|
||||
}
|
||||
|
||||
}} // namespace ecode::acp
|
||||
@@ -0,0 +1,123 @@
|
||||
#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&,
|
||||
const std::optional<ResponseError>& )>& cb );
|
||||
void newSession( const NewSessionRequest& req,
|
||||
const std::function<void( const NewSessionResponse&,
|
||||
const std::optional<ResponseError>& )>& cb );
|
||||
void loadSession( const LoadSessionRequest& req,
|
||||
const std::function<void( const LoadSessionResponse&,
|
||||
const std::optional<ResponseError>& )>& cb );
|
||||
void setConfigOption( const SetConfigOptionRequest& req,
|
||||
const std::function<void( const SetConfigOptionResponse&,
|
||||
const std::optional<ResponseError>& )>& cb );
|
||||
void listSessions( const ListSessionsRequest& req,
|
||||
const std::function<void( const ListSessionsResponse&,
|
||||
const std::optional<ResponseError>& )>& cb );
|
||||
void prompt( const PromptRequest& req,
|
||||
const std::function<void( const PromptResponse&,
|
||||
const std::optional<ResponseError>& )>& cb );
|
||||
|
||||
// Notifications to agent
|
||||
void cancel( const std::string& sessionId );
|
||||
|
||||
// Callbacks from agent
|
||||
std::function<void( const ResponseError& )> onError;
|
||||
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;
|
||||
bool mLegacyConfigOnly{ false };
|
||||
|
||||
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,404 @@
|
||||
#include "acpprotocol.hpp"
|
||||
|
||||
namespace ecode { namespace acp {
|
||||
|
||||
json parseLegacyConfigOptions( const json& body, json configOptions, const std::string& forceId,
|
||||
const std::string& forceValue ) {
|
||||
if ( configOptions.is_null() ) {
|
||||
configOptions = json::array();
|
||||
}
|
||||
|
||||
auto updateOrAdd = [&configOptions]( json newOpt ) {
|
||||
bool found = false;
|
||||
for ( auto& opt : configOptions ) {
|
||||
if ( opt.is_object() && opt.contains( "id" ) && opt["id"] == newOpt["id"] ) {
|
||||
opt = std::move( newOpt );
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( !found ) {
|
||||
configOptions.push_back( std::move( newOpt ) );
|
||||
}
|
||||
};
|
||||
|
||||
if ( body.contains( "models" ) && body["models"].is_object() ) {
|
||||
auto models = body["models"];
|
||||
if ( models.contains( "availableModels" ) && models["availableModels"].is_array() ) {
|
||||
json modelConfig = { { "id", "model" },
|
||||
{ "name", "Model" },
|
||||
{ "type", "select" },
|
||||
{ "category", "model" },
|
||||
{ "options", json::array() } };
|
||||
for ( const auto& model : models["availableModels"] ) {
|
||||
modelConfig["options"].push_back( { { "id", model.value( "modelId", "" ) },
|
||||
{ "name", model.value( "name", "" ) } } );
|
||||
}
|
||||
if ( models.contains( "currentModelId" ) ) {
|
||||
modelConfig["currentValue"] = models.value( "currentModelId", "" );
|
||||
modelConfig["default"] = models.value( "currentModelId", "" );
|
||||
} else if ( !modelConfig["options"].empty() ) {
|
||||
modelConfig["currentValue"] = modelConfig["options"][0]["id"];
|
||||
modelConfig["default"] = modelConfig["options"][0]["id"];
|
||||
}
|
||||
updateOrAdd( std::move( modelConfig ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( body.contains( "modes" ) && body["modes"].is_object() ) {
|
||||
auto modes = body["modes"];
|
||||
if ( modes.contains( "availableModes" ) && modes["availableModes"].is_array() ) {
|
||||
json modeConfig = { { "id", "mode" },
|
||||
{ "name", "Mode" },
|
||||
{ "type", "select" },
|
||||
{ "category", "mode" },
|
||||
{ "options", json::array() } };
|
||||
for ( const auto& mode : modes["availableModes"] ) {
|
||||
modeConfig["options"].push_back(
|
||||
{ { "id", mode.value( "id", "" ) }, { "name", mode.value( "name", "" ) } } );
|
||||
}
|
||||
if ( modes.contains( "currentModeId" ) ) {
|
||||
modeConfig["currentValue"] = modes.value( "currentModeId", "" );
|
||||
modeConfig["default"] = modes.value( "currentModeId", "" );
|
||||
} else if ( !modeConfig["options"].empty() ) {
|
||||
modeConfig["currentValue"] = modeConfig["options"][0]["id"];
|
||||
modeConfig["default"] = modeConfig["options"][0]["id"];
|
||||
}
|
||||
updateOrAdd( std::move( modeConfig ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( !forceId.empty() ) {
|
||||
for ( auto& opt : configOptions ) {
|
||||
if ( opt.is_object() && opt.contains( "id" ) && opt["id"] == forceId ) {
|
||||
opt["currentValue"] = forceValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return configOptions;
|
||||
}
|
||||
|
||||
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"];
|
||||
configOptions = parseLegacyConfigOptions( body, configOptions );
|
||||
}
|
||||
|
||||
json LoadSessionRequest::toJson() const {
|
||||
json j = { { "sessionId", sessionId }, { "cwd", cwd } };
|
||||
if ( mcpServers.is_null() ) {
|
||||
j["mcpServers"] = json::array();
|
||||
} else {
|
||||
j["mcpServers"] = mcpServers;
|
||||
}
|
||||
return j;
|
||||
}
|
||||
|
||||
LoadSessionResponse::LoadSessionResponse( const json& body ) {
|
||||
if ( body.contains( "configOptions" ) )
|
||||
configOptions = body["configOptions"];
|
||||
configOptions = parseLegacyConfigOptions( body, configOptions );
|
||||
}
|
||||
|
||||
json SetConfigOptionRequest::toJson() const {
|
||||
return { { "sessionId", sessionId }, { "configId", configId }, { "optionId", optionId } };
|
||||
}
|
||||
|
||||
SetConfigOptionResponse::SetConfigOptionResponse( const json& body, const std::string& configId,
|
||||
const std::string& optionId ) {
|
||||
if ( body.contains( "configOptions" ) )
|
||||
configOptions = body["configOptions"];
|
||||
configOptions = parseLegacyConfigOptions( body, configOptions, configId, optionId );
|
||||
}
|
||||
|
||||
SessionInfo::SessionInfo( const json& body ) {
|
||||
if ( body.contains( "sessionId" ) && body["sessionId"].is_string() )
|
||||
sessionId = body["sessionId"].get<std::string>();
|
||||
if ( body.contains( "cwd" ) && body["cwd"].is_string() )
|
||||
cwd = body["cwd"].get<std::string>();
|
||||
if ( body.contains( "title" ) && body["title"].is_string() )
|
||||
title = body["title"].get<std::string>();
|
||||
if ( body.contains( "updatedAt" ) && body["updatedAt"].is_string() )
|
||||
updatedAt = body["updatedAt"].get<std::string>();
|
||||
}
|
||||
|
||||
json ListSessionsRequest::toJson() const {
|
||||
json j = json::object();
|
||||
if ( !cursor.empty() )
|
||||
j["cursor"] = cursor;
|
||||
if ( !cwd.empty() )
|
||||
j["cwd"] = cwd;
|
||||
return j;
|
||||
}
|
||||
|
||||
ListSessionsResponse::ListSessionsResponse( const json& body ) {
|
||||
if ( body.contains( "nextCursor" ) && body["nextCursor"].is_string() )
|
||||
nextCursor = body["nextCursor"].get<std::string>();
|
||||
if ( body.contains( "sessions" ) && body["sessions"].is_array() ) {
|
||||
for ( const auto& item : body["sessions"] ) {
|
||||
sessions.emplace_back( SessionInfo( item ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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", "" );
|
||||
if ( body.contains( "outputByteLimit" ) && !body["outputByteLimit"].is_null() )
|
||||
outputByteLimit = body["outputByteLimit"].get<uint64_t>();
|
||||
}
|
||||
|
||||
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 { { "outcome", j } };
|
||||
}
|
||||
|
||||
}} // namespace ecode::acp
|
||||
@@ -0,0 +1,336 @@
|
||||
#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 {
|
||||
|
||||
json parseLegacyConfigOptions( const json& body, json configOptions,
|
||||
const std::string& forceId = "",
|
||||
const std::string& forceValue = "" );
|
||||
|
||||
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 LoadSessionRequest {
|
||||
std::string sessionId;
|
||||
std::string cwd;
|
||||
json mcpServers;
|
||||
|
||||
LoadSessionRequest() = default;
|
||||
json toJson() const;
|
||||
};
|
||||
|
||||
struct LoadSessionResponse {
|
||||
json configOptions;
|
||||
|
||||
LoadSessionResponse() = default;
|
||||
LoadSessionResponse( const json& body );
|
||||
};
|
||||
|
||||
struct SetConfigOptionRequest {
|
||||
std::string sessionId;
|
||||
std::string configId;
|
||||
std::string optionId;
|
||||
|
||||
SetConfigOptionRequest() = default;
|
||||
json toJson() const;
|
||||
};
|
||||
|
||||
struct SetConfigOptionResponse {
|
||||
json configOptions;
|
||||
|
||||
SetConfigOptionResponse() = default;
|
||||
SetConfigOptionResponse( const json& body, const std::string& configId = "",
|
||||
const std::string& optionId = "" );
|
||||
};
|
||||
|
||||
struct SessionInfo {
|
||||
std::string sessionId;
|
||||
std::string cwd;
|
||||
std::string title;
|
||||
std::string updatedAt;
|
||||
|
||||
SessionInfo() = default;
|
||||
SessionInfo( const json& body );
|
||||
};
|
||||
|
||||
struct ListSessionsRequest {
|
||||
std::string cursor;
|
||||
std::string cwd;
|
||||
|
||||
ListSessionsRequest() = default;
|
||||
json toJson() const;
|
||||
};
|
||||
|
||||
struct ListSessionsResponse {
|
||||
std::string nextCursor;
|
||||
std::vector<SessionInfo> sessions;
|
||||
|
||||
ListSessionsResponse() = default;
|
||||
ListSessionsResponse( const json& body );
|
||||
};
|
||||
|
||||
struct ResponseError {
|
||||
int code{ 0 };
|
||||
std::string message;
|
||||
json data;
|
||||
|
||||
ResponseError() = default;
|
||||
ResponseError( int code, std::string message, json data = {} ) :
|
||||
code( code ), message( std::move( message ) ), data( std::move( data ) ) {}
|
||||
ResponseError( const json& body ) {
|
||||
if ( body.contains( "code" ) )
|
||||
code = body["code"].get<int>();
|
||||
if ( body.contains( "message" ) )
|
||||
message = body["message"].get<std::string>();
|
||||
if ( body.contains( "data" ) )
|
||||
data = body["data"];
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
std::optional<uint64_t> outputByteLimit;
|
||||
|
||||
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,287 @@
|
||||
#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&,
|
||||
const std::optional<ResponseError>& err ) {
|
||||
if ( err ) {
|
||||
if ( onError )
|
||||
onError( *err );
|
||||
if ( onReady )
|
||||
onReady( false );
|
||||
return;
|
||||
}
|
||||
NewSessionRequest nreq;
|
||||
nreq.cwd = mClient->isReady() ? mClient->getConfig().workingDirectory : "";
|
||||
mClient->newSession( nreq, [this, onReady]( const NewSessionResponse& nres,
|
||||
const std::optional<ResponseError>& err ) {
|
||||
if ( err ) {
|
||||
if ( onError )
|
||||
onError( *err );
|
||||
if ( onReady )
|
||||
onReady( false );
|
||||
return;
|
||||
}
|
||||
mSessionId = nres.sessionId;
|
||||
mConfigOptions = nres.configOptions;
|
||||
if ( onReady )
|
||||
onReady( true );
|
||||
} );
|
||||
} );
|
||||
return true;
|
||||
}
|
||||
if ( onReady )
|
||||
onReady( false );
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AgentSession::startLoaded( const std::string& sessionId,
|
||||
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, sessionId,
|
||||
onReady]( const InitializeResponse& ires,
|
||||
const std::optional<ResponseError>& err ) {
|
||||
if ( err ) {
|
||||
if ( onError )
|
||||
onError( *err );
|
||||
if ( onReady )
|
||||
onReady( false );
|
||||
return;
|
||||
}
|
||||
if ( ires.agentCapabilities.loadSession ) {
|
||||
LoadSessionRequest lreq;
|
||||
lreq.sessionId = sessionId;
|
||||
lreq.cwd = mClient->isReady() ? mClient->getConfig().workingDirectory : "";
|
||||
mClient->loadSession(
|
||||
lreq, [this, sessionId, onReady]( const LoadSessionResponse& lres,
|
||||
const std::optional<ResponseError>& err ) {
|
||||
if ( err ) {
|
||||
if ( onError )
|
||||
onError( *err );
|
||||
if ( onReady )
|
||||
onReady( false );
|
||||
return;
|
||||
}
|
||||
mSessionId = sessionId;
|
||||
mConfigOptions = lres.configOptions;
|
||||
if ( onReady )
|
||||
onReady( true );
|
||||
} );
|
||||
} else {
|
||||
if ( onError )
|
||||
onError( { -1, "Agent does not support loading sessions" } );
|
||||
if ( onReady )
|
||||
onReady( false );
|
||||
}
|
||||
} );
|
||||
return true;
|
||||
}
|
||||
if ( onReady )
|
||||
onReady( false );
|
||||
return false;
|
||||
}
|
||||
|
||||
void AgentSession::listSessions(
|
||||
const std::function<void( const std::vector<SessionInfo>&,
|
||||
const std::optional<ResponseError>& )>& cb ) {
|
||||
if ( !mClient->isReady() ) {
|
||||
if ( cb )
|
||||
cb( {}, std::nullopt );
|
||||
return;
|
||||
}
|
||||
ListSessionsRequest req;
|
||||
req.cwd = mClient->getConfig().workingDirectory;
|
||||
mClient->listSessions(
|
||||
req, [this, cb]( const ListSessionsResponse& res, const std::optional<ResponseError>& err ) {
|
||||
if ( err && onError )
|
||||
onError( *err );
|
||||
if ( cb )
|
||||
cb( res.sessions, err );
|
||||
} );
|
||||
}
|
||||
|
||||
void AgentSession::stop() {
|
||||
if ( mClient )
|
||||
mClient->stop();
|
||||
}
|
||||
|
||||
void AgentSession::prompt(
|
||||
const PromptRequest& req,
|
||||
const std::function<void( const PromptResponse&, const std::optional<ResponseError>& )>& cb ) {
|
||||
mIsPrompting = true;
|
||||
mClient->prompt(
|
||||
req, [this, cb]( const PromptResponse& res, const std::optional<ResponseError>& err ) {
|
||||
mIsPrompting = false;
|
||||
if ( err && onError )
|
||||
onError( *err );
|
||||
if ( cb )
|
||||
cb( res, err );
|
||||
} );
|
||||
}
|
||||
|
||||
void AgentSession::setConfigOption(
|
||||
const SetConfigOptionRequest& req,
|
||||
const std::function<void( const SetConfigOptionResponse&,
|
||||
const std::optional<ResponseError>& )>& cb ) {
|
||||
mClient->setConfigOption( req, [this, cb]( const SetConfigOptionResponse& res,
|
||||
const std::optional<ResponseError>& err ) {
|
||||
if ( err && onError )
|
||||
onError( *err );
|
||||
if ( cb )
|
||||
cb( res, err );
|
||||
} );
|
||||
}
|
||||
|
||||
void AgentSession::cancel() {
|
||||
if ( mClient && !mSessionId.empty() ) {
|
||||
mClient->cancel( mSessionId );
|
||||
}
|
||||
}
|
||||
|
||||
void AgentSession::setTerminalData( const std::string& terminalId, UITerminal* uiTerm ) {
|
||||
auto& termData = mTerminals[terminalId];
|
||||
termData = { uiTerm->getTerm(), uiTerm->getTerm()->getTerminal(), uiTerm, "" };
|
||||
if ( termData.emulator ) {
|
||||
termData.emulator->setDataCb( [this, terminalId]( const char* data, size_t size ) {
|
||||
auto it = mTerminals.find( terminalId );
|
||||
if ( it != mTerminals.end() ) {
|
||||
it->second.outputBuffer.append( data, size );
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
void AgentSession::setupClient() {
|
||||
mClient->onError = [this]( const ResponseError& err ) {
|
||||
if ( onError )
|
||||
onError( err );
|
||||
};
|
||||
|
||||
mClient->onSessionUpdate = [this]( const json& msg ) {
|
||||
auto sessionUpdate = msg.value( "sessionUpdate", "" );
|
||||
if ( sessionUpdate == "config_options_update" && msg.contains( "configOptions" ) ) {
|
||||
mConfigOptions = msg["configOptions"];
|
||||
} else if ( msg.contains( "models" ) || msg.contains( "modes" ) ) {
|
||||
mConfigOptions = parseLegacyConfigOptions( msg, mConfigOptions );
|
||||
}
|
||||
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;
|
||||
if ( onTerminalCreated ) {
|
||||
onTerminalCreated( req, 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() ) {
|
||||
size_t limit = req.outputByteLimit.value_or( 0 );
|
||||
if ( limit > 0 && it->second.outputBuffer.size() > limit ) {
|
||||
res.output = it->second.outputBuffer.substr( 0, limit );
|
||||
it->second.outputBuffer.erase( 0, limit );
|
||||
res.truncated = true;
|
||||
} else {
|
||||
res.output = std::move( it->second.outputBuffer );
|
||||
it->second.outputBuffer.clear();
|
||||
res.truncated = false;
|
||||
}
|
||||
|
||||
if ( it->second.emulator && 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,73 @@
|
||||
#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 );
|
||||
bool startLoaded( const std::string& sessionId, const std::function<void( bool )>& onReady );
|
||||
void listSessions( const std::function<void( const std::vector<SessionInfo>&,
|
||||
const std::optional<ResponseError>& )>& cb );
|
||||
void stop();
|
||||
|
||||
void prompt( const PromptRequest& req,
|
||||
const std::function<void( const PromptResponse&,
|
||||
const std::optional<ResponseError>& )>& cb );
|
||||
void setConfigOption(
|
||||
const SetConfigOptionRequest& req,
|
||||
const std::function<void( const SetConfigOptionResponse&,
|
||||
const std::optional<ResponseError>& )>& cb );
|
||||
void cancel();
|
||||
|
||||
bool isPrompting() const { return mIsPrompting; }
|
||||
|
||||
std::function<void( const ResponseError& )> onError;
|
||||
std::function<void( const json& )> onSessionUpdate;
|
||||
std::function<void( const RequestPermissionRequest&,
|
||||
std::function<void( const RequestPermissionResponse& )> )>
|
||||
onRequestPermission;
|
||||
std::function<void( const CreateTerminalRequest&, const std::string& terminalId )>
|
||||
onTerminalCreated;
|
||||
|
||||
std::string getSessionId() const { return mSessionId; }
|
||||
ACPClient* getClient() const { return mClient.get(); }
|
||||
json getConfigOptions() const { return mConfigOptions; }
|
||||
void setConfigOptions( const json& opts ) { mConfigOptions = opts; }
|
||||
|
||||
void setTerminalData( const std::string& terminalId, UITerminal* uiTerm );
|
||||
|
||||
protected:
|
||||
std::shared_ptr<ThreadPool> mThreadPool;
|
||||
std::unique_ptr<ACPClient> mClient;
|
||||
std::string mSessionId;
|
||||
json mConfigOptions;
|
||||
bool mIsPrompting{ false };
|
||||
|
||||
struct TermData {
|
||||
std::shared_ptr<TerminalDisplay> display;
|
||||
std::shared_ptr<TerminalEmulator> emulator;
|
||||
UITerminal* uiTerm{ nullptr };
|
||||
std::string outputBuffer;
|
||||
};
|
||||
std::unordered_map<std::string, TermData> mTerminals;
|
||||
|
||||
void setupClient();
|
||||
};
|
||||
|
||||
}} // namespace ecode::acp
|
||||
@@ -111,6 +111,34 @@ static std::map<std::string, LLMProvider> parseLLMProviders( const nlohmann::jso
|
||||
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 ) {
|
||||
return eeNew( AIAssistantPlugin, ( pluginManager, false ) );
|
||||
}
|
||||
@@ -398,6 +426,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" ) )
|
||||
return;
|
||||
|
||||
@@ -601,6 +636,7 @@ void AIAssistantPlugin::onSaveState( IniFile* state ) {
|
||||
config.partition = mainChat->getSplitter()->getSplitPartition();
|
||||
config.modelProvider = mainChat->getCurModel().provider;
|
||||
config.modelName = mainChat->getCurModel().name;
|
||||
config.agentName = mainChat->getCurAgent();
|
||||
} else {
|
||||
config = mConfig;
|
||||
}
|
||||
@@ -613,6 +649,7 @@ void AIAssistantPlugin::onSaveState( IniFile* state ) {
|
||||
state->setValue( keyname, "split_partition", config.partition.toString() );
|
||||
state->setValue( keyname, "default_provider", config.modelProvider );
|
||||
state->setValue( keyname, "default_model", config.modelName );
|
||||
state->setValue( keyname, "default_agent", config.agentName );
|
||||
}
|
||||
|
||||
} // namespace ecode
|
||||
|
||||
@@ -14,6 +14,7 @@ class AIAssistantPlugin : public PluginBase {
|
||||
StyleSheetLength partition;
|
||||
std::string modelProvider;
|
||||
std::string modelName;
|
||||
std::string agentName;
|
||||
};
|
||||
|
||||
static PluginDefinition Definition() {
|
||||
@@ -37,6 +38,7 @@ class AIAssistantPlugin : public PluginBase {
|
||||
std::string getDescription() override { return Definition().description; }
|
||||
|
||||
const LLMProviders& getProviders() { return mProviders; }
|
||||
const ACPAgents& getAgents() { return mAgents; }
|
||||
|
||||
std::string getPluginStatePath() const;
|
||||
|
||||
@@ -50,6 +52,7 @@ class AIAssistantPlugin : public PluginBase {
|
||||
|
||||
protected:
|
||||
LLMProviders mProviders;
|
||||
ACPAgents mAgents;
|
||||
bool mUIInit{ false };
|
||||
bool mBrokenUserConfigFile{ false };
|
||||
UIWidget* mStatusBar{ nullptr };
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "../pluginmanager.hpp"
|
||||
#include "acp/agentsession.hpp"
|
||||
#include "llmchatcompletionrequest.hpp"
|
||||
#include "protocol.hpp"
|
||||
|
||||
@@ -91,6 +92,8 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter {
|
||||
|
||||
bool chatExistsInDisk() const;
|
||||
|
||||
const std::string& getCurAgent() const { return mCurAgent; }
|
||||
|
||||
protected:
|
||||
UUID mUUID;
|
||||
std::string mSummary;
|
||||
@@ -109,8 +112,11 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter {
|
||||
UIPushButton* mRefreshModels{ nullptr };
|
||||
UIPushButton* mChatAttach{ nullptr };
|
||||
UISelectButton* mChatPrivate{ nullptr };
|
||||
UISelectButton* mChatAgentMode{ nullptr };
|
||||
UIScrollView* mChatScrollView{ nullptr };
|
||||
UIPushButton* mModelBtn{ nullptr };
|
||||
UIPushButton* mAgentBtn{ nullptr };
|
||||
UIPushButton* mAgentConfigBtn{ nullptr };
|
||||
|
||||
// Locate file
|
||||
UIVLinearLayoutCommandExecuter* mLocateBarLayout{ nullptr };
|
||||
@@ -122,13 +128,33 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter {
|
||||
UITextInput* mLocateModelInput{ nullptr };
|
||||
UITableView* mLocateModelTable{ nullptr };
|
||||
|
||||
// Select agent
|
||||
UIVLinearLayoutCommandExecuter* mLocateAgentBarLayout{ nullptr };
|
||||
UITextInput* mLocateAgentInput{ nullptr };
|
||||
UITableView* mLocateAgentTable{ nullptr };
|
||||
|
||||
std::unique_ptr<LLMChatCompletionRequest> mRequest;
|
||||
std::unique_ptr<LLMChatCompletionRequest> mSummaryRequest;
|
||||
LLMProviders mProviders;
|
||||
LLMModel mCurModel;
|
||||
std::vector<LLMModel> mModels;
|
||||
|
||||
std::map<std::string, ACPAgent> mAgents;
|
||||
std::string mCurAgent;
|
||||
|
||||
struct SlashCommand {
|
||||
std::string name;
|
||||
std::string description;
|
||||
};
|
||||
std::vector<SlashCommand> mAvailableCommands;
|
||||
|
||||
std::unique_ptr<acp::AgentSession> mAgentSession;
|
||||
UIWidget* mThinkingBubble{ nullptr };
|
||||
std::string mCurThinking;
|
||||
std::string mCurToolCall;
|
||||
int mPendingModelsToLoad{ 0 };
|
||||
bool mChatIsPrivate{ false };
|
||||
bool mIsAgentMode{ false };
|
||||
bool mChatLocked{ false };
|
||||
bool mLinkMode{ false };
|
||||
std::vector<LLMModel> mNewModels;
|
||||
@@ -151,6 +177,10 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter {
|
||||
|
||||
void doRequest();
|
||||
|
||||
void doAgentRequest();
|
||||
|
||||
void sendAgentPrompt();
|
||||
|
||||
void toggleEnableChat( UIWidget* chat, bool enabled );
|
||||
|
||||
void toggleEnableChats( bool enabled );
|
||||
@@ -159,18 +189,49 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter {
|
||||
|
||||
UIWidget* addChatUI( LLMChat::Role role );
|
||||
|
||||
UIWidget* addMarkdownBubble( const std::string& layout, const std::string& markdown );
|
||||
|
||||
void addPlanBubble( const std::string& markdown );
|
||||
|
||||
void addToolCallBubble( const std::string& markdown );
|
||||
|
||||
void addThinkingBubble();
|
||||
|
||||
void updateThinkingBubble( const std::string& chunk );
|
||||
|
||||
void addPermissionUI( const acp::RequestPermissionRequest& req,
|
||||
std::function<void( const acp::RequestPermissionResponse& )> cb );
|
||||
|
||||
void fillApiModels();
|
||||
|
||||
String getModelDisplayName( const LLMModel& model ) const;
|
||||
|
||||
bool selectModel( std::optional<LLMModel> model );
|
||||
|
||||
bool selectAgent( const std::string& agent );
|
||||
|
||||
void fillModelDropDownList();
|
||||
|
||||
void loadSelectAgent();
|
||||
|
||||
void showSelectAgent();
|
||||
|
||||
void initSelectAgent();
|
||||
|
||||
void hideSelectAgent();
|
||||
|
||||
void updateLocateAgentBarColumns();
|
||||
|
||||
void updateAgentModeUI();
|
||||
|
||||
void setupAgentSession();
|
||||
|
||||
void resizeToFit( UICodeEditor* editor );
|
||||
|
||||
void addChat( LLMChat::Role role, std::string conversation );
|
||||
|
||||
void writeToLastChat( const std::string& text );
|
||||
|
||||
void removeLastChat();
|
||||
|
||||
void setProviders( LLMProviders&& providers );
|
||||
@@ -214,13 +275,21 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter {
|
||||
|
||||
void hideSelectModel();
|
||||
|
||||
void showAgentConfigWindow();
|
||||
|
||||
void insertFileToDocument( std::string path, std::shared_ptr<TextDocument> cdoc );
|
||||
|
||||
void replaceFileLinksToContents( std::string& text );
|
||||
|
||||
nlohmann::json promptToContentBlocks( std::string text );
|
||||
|
||||
void generateChatName( bool isRenaming );
|
||||
|
||||
void regenerateChatName();
|
||||
|
||||
void removeWaitingBubble();
|
||||
|
||||
UIWidget* getLastConversation( bool skipEmpty = false ) const;
|
||||
};
|
||||
|
||||
} // namespace ecode
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace ecode {
|
||||
|
||||
@@ -41,4 +42,14 @@ struct LLMProvider {
|
||||
|
||||
using LLMProviders = std::map<std::string, LLMProvider>;
|
||||
|
||||
} // namespace ecode
|
||||
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
|
||||
Reference in New Issue
Block a user