Error handle.

This commit is contained in:
Martín Lucas Golini
2026-03-22 00:41:09 -03:00
parent 163841c98c
commit 721948c4e0
6 changed files with 214 additions and 101 deletions

View File

@@ -152,6 +152,8 @@ void ACPClient::processResponse( const json& msg ) {
if ( handler ) {
handler( id, msg );
} else if ( msg.contains( "error" ) && onError ) {
onError( ResponseError( msg["error"] ) );
}
}
@@ -194,54 +196,74 @@ void ACPClient::sendError( const json& id, int code, const std::string& message
}
}
void ACPClient::initialize( const InitializeRequest& req,
const std::function<void( const InitializeResponse& )>& cb ) {
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"] ) );
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& )>& cb ) {
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"] ) );
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& )>& cb ) {
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"] ) );
cb( LoadSessionResponse( resp["result"] ), std::nullopt );
} else if ( resp.contains( "error" ) && cb ) {
cb( {}, ResponseError( resp["error"] ) );
}
} );
}
void ACPClient::listSessions( const ListSessionsRequest& req,
const std::function<void( const ListSessionsResponse& )>& cb ) {
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"] ) );
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& )>& cb ) {
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"] ) );
cb( PromptResponse( resp["result"] ), std::nullopt );
} else if ( resp.contains( "error" ) && cb ) {
cb( {}, ResponseError( resp["error"] ) );
}
} );
}

View File

@@ -44,19 +44,26 @@ class ACPClient {
const Config& getConfig() const { return mConfig; }
void initialize( const InitializeRequest& req,
const std::function<void( const InitializeResponse& )>& cb );
const std::function<void( const InitializeResponse&,
const std::optional<ResponseError>& )>& cb );
void newSession( const NewSessionRequest& req,
const std::function<void( const NewSessionResponse& )>& cb );
const std::function<void( const NewSessionResponse&,
const std::optional<ResponseError>& )>& cb );
void loadSession( const LoadSessionRequest& req,
const std::function<void( const LoadSessionResponse& )>& cb );
const std::function<void( const LoadSessionResponse&,
const std::optional<ResponseError>& )>& cb );
void listSessions( const ListSessionsRequest& req,
const std::function<void( const ListSessionsResponse& )>& cb );
void prompt( const PromptRequest& req, const std::function<void( const PromptResponse& )>& cb );
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& )> )>

View File

@@ -105,6 +105,22 @@ struct ListSessionsResponse {
ListSessionsResponse( const json& body );
};
struct ResponseError {
int code{ 0 };
std::string message;
json data;
ResponseError() = default;
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

View File

@@ -21,15 +21,28 @@ bool AgentSession::start( const std::function<void( bool )>& onReady ) {
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 );
mClient->initialize(
req, [this, onReady]( const InitializeResponse&, const std::optional<ResponseError>& err ) {
if ( 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 ( onReady )
onReady( false );
return;
}
mSessionId = nres.sessionId;
if ( onReady )
onReady( true );
} );
} );
} );
return true;
}
if ( onReady )
@@ -45,23 +58,35 @@ bool AgentSession::startLoaded( const std::string& sessionId,
req.clientCapabilities.fsReadTextFile = true;
req.clientCapabilities.fsWriteTextFile = true;
mClient->initialize( req, [this, sessionId, onReady]( const InitializeResponse& ires ) {
if ( ires.agentCapabilities.loadSession ) {
LoadSessionRequest lreq;
lreq.sessionId = sessionId;
lreq.cwd = mClient->isReady() ? mClient->getConfig().workingDirectory : "";
mClient->loadSession( lreq, [this, sessionId, onReady]( const LoadSessionResponse& ) {
mSessionId = sessionId;
mClient->initialize(
req, [this, sessionId, onReady]( const InitializeResponse& ires,
const std::optional<ResponseError>& err ) {
if ( err ) {
if ( onReady )
onReady( true );
} );
} else {
// Agent doesn't support loading, fallback to new session?
// For now let's just fail or call onReady(false)
if ( onReady )
onReady( false );
}
} );
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&,
const std::optional<ResponseError>& err ) {
if ( err ) {
if ( onReady )
onReady( false );
return;
}
mSessionId = sessionId;
if ( onReady )
onReady( true );
} );
} else {
if ( onReady )
onReady( false );
}
} );
return true;
}
if ( onReady )
@@ -69,16 +94,21 @@ bool AgentSession::startLoaded( const std::string& sessionId,
return false;
}
void AgentSession::listSessions( const std::function<void( const std::vector<SessionInfo>& )>& cb ) {
void AgentSession::listSessions(
const std::function<void( const std::vector<SessionInfo>&, const std::optional<ResponseError>& )>&
cb ) {
if ( !mClient->isReady() ) {
if ( cb ) cb( {} );
if ( cb )
cb( {}, std::nullopt );
return;
}
ListSessionsRequest req;
req.cwd = mClient->getConfig().workingDirectory;
mClient->listSessions( req, [cb]( const ListSessionsResponse& res ) {
if ( cb ) cb( res.sessions );
} );
mClient->listSessions(
req, [cb]( const ListSessionsResponse& res, const std::optional<ResponseError>& err ) {
if ( cb )
cb( res.sessions, err );
} );
}
void AgentSession::stop() {
@@ -86,14 +116,16 @@ void AgentSession::stop() {
mClient->stop();
}
void AgentSession::prompt( const PromptRequest& req,
const std::function<void( const PromptResponse& )>& cb ) {
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) {
mIsPrompting = false;
if ( cb )
cb(res);
} );
mClient->prompt(
req, [this, cb]( const PromptResponse& res, const std::optional<ResponseError>& err ) {
mIsPrompting = false;
if ( cb )
cb( res, err );
} );
}
void AgentSession::cancel() {
@@ -108,6 +140,11 @@ void AgentSession::setTerminalData( const std::string& terminalId, UITerminal* u
}
void AgentSession::setupClient() {
mClient->onError = [this]( const ResponseError& err ) {
if ( onError )
onError( err );
};
mClient->onSessionUpdate = [this]( const json& msg ) {
if ( onSessionUpdate )
onSessionUpdate( msg );

View File

@@ -22,14 +22,19 @@ class 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>& )>& cb );
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& )>& cb );
void prompt( const PromptRequest& req,
const std::function<void( const PromptResponse&, 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& )> )>

View File

@@ -281,8 +281,11 @@ class AgentSessionHistoryModel : public Model {
switch ( index.column() ) {
case Columns::Title: {
return Variant( session.title.empty() ? "Untitled Session"
: session.title.c_str() );
return Variant( session.title.empty()
? ( mUISceneNode ? mUISceneNode->i18n( "untitled_session",
"Untitled Session" )
: "Untitled Session" )
: session.title.c_str() );
}
case Columns::UpdatedAt: {
return Variant( session.updatedAt.c_str() );
@@ -532,9 +535,10 @@ static const char* DEFAULT_CHAT_GLOBE = R"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>
<hbox class="llm_conversation_opt" lw="mp" lh="wc" background-color="var(--primary)" padding="4dp">
<TextView text="@string(tool_call_permission_request, 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" />
@@ -561,6 +565,7 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) :
mChatAgentMode = find<UISelectButton>( "llm_agent_mode" );
mChatAgentMode->on( Event::OnValueChange, [this]( auto ) {
mIsAgentMode = mChatAgentMode->isSelected();
updateTabTitle();
updateAgentModeUI();
} );
@@ -1232,9 +1237,34 @@ void LLMChatUI::showChatHistory() {
UISceneNode* uiSceneNode = getUISceneNode();
if ( mIsAgentMode ) {
auto listSessionsCb = [winId, uiSceneNode, tv, loader, input](
const std::vector<acp::SessionInfo>& sessions,
const std::optional<acp::ResponseError>& err ) {
uiSceneNode->runOnMainThread( [winId, uiSceneNode, tv, loader, input, sessions, err] {
auto win = uiSceneNode->find( winId );
if ( win == nullptr )
return;
if ( err ) {
loader->setVisible( false );
NotificationCenter::instance()->addNotification(
uiSceneNode->i18n( "ai_assistant_agent_error", "Agent Error: " ) +
err->message );
win->asType<UIWindow>()->closeWindow();
return;
}
auto model = std::make_shared<AgentSessionHistoryModel>( sessions, uiSceneNode );
loader->setVisible( false );
input->setVisible( true );
tv->setVisible( true );
tv->setModel( model );
input->setFocus();
} );
};
if ( !mAgentSession ) {
setupAgentSession();
mAgentSession->start( [this, winId, uiSceneNode, tv, loader, input]( bool ready ) {
mAgentSession->start( [this, winId, uiSceneNode, loader, listSessionsCb]( bool ready ) {
if ( !ready ) {
uiSceneNode->runOnMainThread( [loader, winId, uiSceneNode] {
if ( uiSceneNode->find( winId ) == nullptr )
@@ -1243,37 +1273,10 @@ void LLMChatUI::showChatHistory() {
} );
return;
}
mAgentSession->listSessions( [winId, uiSceneNode, tv, loader, input](
const std::vector<acp::SessionInfo>& sessions ) {
uiSceneNode->runOnMainThread(
[sessions, winId, uiSceneNode, tv, loader, input] {
if ( uiSceneNode->find( winId ) == nullptr )
return;
auto model =
std::make_shared<AgentSessionHistoryModel>( sessions, uiSceneNode );
loader->setVisible( false );
input->setVisible( true );
tv->setVisible( true );
tv->setModel( model );
input->setFocus();
} );
} );
mAgentSession->listSessions( listSessionsCb );
} );
} else {
mAgentSession->listSessions( [winId, uiSceneNode, tv, loader,
input]( const std::vector<acp::SessionInfo>& sessions ) {
uiSceneNode->runOnMainThread( [sessions, winId, uiSceneNode, tv, loader, input] {
if ( uiSceneNode->find( winId ) == nullptr )
return;
auto model =
std::make_shared<AgentSessionHistoryModel>( sessions, uiSceneNode );
loader->setVisible( false );
input->setVisible( true );
tv->setVisible( true );
tv->setModel( model );
input->setFocus();
} );
} );
mAgentSession->listSessions( listSessionsCb );
}
} else {
getUISceneNode()->getThreadPool()->run(
@@ -1596,10 +1599,11 @@ void LLMChatUI::setupAgentSession() {
writeToLastChat( chunk );
}
} else if ( sessionUpdate == "tool_call" ) {
std::string toolStr = "\n> Tool Call: " + msg.value( "title", "" ) + "\n";
std::string toolStr = "\n> " + i18n( "tool_call", "Tool Call: " ) +
msg.value( "title", "" ) + "\n";
writeToLastChat( toolStr );
} else if ( sessionUpdate == "plan" ) {
std::string planStr = "\n> Plan Updated:\n";
std::string planStr = "\n> " + i18n( "plan_updated", "Plan Updated:" ) + "\n";
if ( msg.contains( "plan" ) && msg["plan"].contains( "steps" ) &&
msg["plan"]["steps"].is_array() ) {
for ( const auto& step : msg["plan"]["steps"] ) {
@@ -1623,7 +1627,13 @@ void LLMChatUI::setupAgentSession() {
runOnMainThread( [this, req, cb]() { addPermissionUI( req, cb ); } );
};
mAgentSession->onError = [this]( const acp::ResponseError& err ) {
NotificationCenter::instance()->addNotification(
i18n( "ai_assistant_agent_error", "Agent Error: " ) + err.message );
};
mAgentSession->onTerminalCreated = [this]( const acp::CreateTerminalRequest& req,
const std::string& termId ) {
runOnMainThread( [this, req, termId] {
find( "chat_presentation" )->setVisible( false );
@@ -1755,7 +1765,22 @@ void LLMChatUI::sendAgentPrompt() {
req.prompt = { { { "type", "text" }, { "text", "" } } };
}
mAgentSession->prompt( req, [this]( const acp::PromptResponse& res ) {
mAgentSession->prompt( req, [this]( const acp::PromptResponse& res,
const std::optional<acp::ResponseError>& err ) {
if ( err ) {
runOnMainThread( [this] {
mChatStop->setVisible( false )->setEnabled( false );
mChatRun->setVisible( true )->setEnabled( true );
toggleEnableChats( true );
auto lastChat = getLastConversation();
if ( lastChat ) {
auto* thinking = lastChat->findByClass<UIImage>( "thinking" );
if ( thinking )
thinking->setVisible( false );
}
} );
return;
}
runOnMainThread( [this, res]() {
auto chat = getLastConversation();
if ( chat ) {
@@ -2241,14 +2266,15 @@ void LLMChatUI::addPermissionUI( const acp::RequestPermissionRequest& req,
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";
std::string descStr =
i18n( "agent_wants_to_execute_tool_call", "The agent wants to execute a tool call:" ) +
"\n";
descStr += i18n( "title", "Title" ) + ": " + req.toolCall.title + "\n";
descStr += i18n( "kind", "Kind" ) + ": " + req.toolCall.kind + "\n";
if ( !req.toolCall.rawInput.is_null() ) {
descStr += "Input:\n" + req.toolCall.rawInput.dump( 2 ) + "\n";
descStr += i18n( "input", "Input" ) + ":\n" + req.toolCall.rawInput.dump( 2 ) + "\n";
}
desc->setText( descStr );
UIWidget* optionsBox = chat->findByClass( "permission_options" );
bool isFirst = true;