refactor: Remove project rules

This commit is contained in:
Petr Mironychev
2026-06-11 13:36:23 +02:00
parent 2c9475cddf
commit 05fe38e289
45 changed files with 1333 additions and 299 deletions

View File

@@ -3,6 +3,7 @@ add_library(Session STATIC
MessageSerializer.hpp MessageSerializer.cpp
PluginBlocks.hpp
LLMRequest.hpp
ErrorInfo.hpp
ResponseEvent.hpp
ConversationHistory.hpp ConversationHistory.cpp
ResponseRouter.hpp ResponseRouter.cpp

View File

@@ -0,0 +1,61 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QMetaType>
#include <QString>
#include <utility>
namespace QodeAssist {
enum class ErrorCategory {
Config,
Auth,
Network,
Provider,
Validation,
Tool,
};
struct ErrorInfo
{
ErrorCategory category = ErrorCategory::Provider;
QString message;
QString providerDetail;
bool isEmpty() const noexcept { return message.isEmpty(); }
};
[[nodiscard]] inline ErrorInfo makeError(
ErrorCategory category, QString message, QString providerDetail = QString())
{
return ErrorInfo{category, std::move(message), std::move(providerDetail)};
}
[[nodiscard]] inline ErrorCategory categorizeProviderError(const QString &raw)
{
const QString text = raw.toLower();
const auto contains = [&text](const char *needle) {
return text.contains(QLatin1String(needle));
};
if (contains("401") || contains("403") || contains("unauthorized")
|| contains("forbidden") || contains("api key") || contains("apikey")
|| contains("authentication") || contains("invalid token"))
return ErrorCategory::Auth;
if (contains("timeout") || contains("timed out") || contains("connection")
|| contains("could not resolve") || contains("unreachable")
|| contains("network") || contains("ssl") || contains("refused"))
return ErrorCategory::Network;
return ErrorCategory::Provider;
}
} // namespace QodeAssist
Q_DECLARE_METATYPE(QodeAssist::ErrorInfo)

View File

@@ -9,6 +9,8 @@
#include <variant>
#include "ErrorInfo.hpp"
namespace QodeAssist {
namespace ResponseEvents {
@@ -45,6 +47,7 @@ struct ToolCallEnd
struct ToolResult
{
QString toolUseId;
QString name;
QString text;
bool isError = false;
};
@@ -53,11 +56,14 @@ struct Usage
{
int inputTokens = 0;
int outputTokens = 0;
int cachedTokens = 0;
int reasoningTokens = 0;
};
struct Error
{
QString message;
ErrorCategory category = ErrorCategory::Provider;
};
struct MessageStop
@@ -128,21 +134,27 @@ public:
Kind::ToolCallEnd, ResponseEvents::ToolCallEnd{std::move(id), std::move(finalArgs)}};
}
static ResponseEvent toolResult(QString toolUseId, QString text, bool isError = false)
static ResponseEvent toolResult(
QString toolUseId, QString name, QString text, bool isError = false)
{
return {
Kind::ToolResult,
ResponseEvents::ToolResult{std::move(toolUseId), std::move(text), isError}};
ResponseEvents::ToolResult{
std::move(toolUseId), std::move(name), std::move(text), isError}};
}
static ResponseEvent usage(int inputTokens, int outputTokens)
static ResponseEvent usage(
int inputTokens, int outputTokens, int cachedTokens = 0, int reasoningTokens = 0)
{
return {Kind::Usage, ResponseEvents::Usage{inputTokens, outputTokens}};
return {
Kind::Usage,
ResponseEvents::Usage{inputTokens, outputTokens, cachedTokens, reasoningTokens}};
}
static ResponseEvent error(QString message)
static ResponseEvent error(
QString message, ErrorCategory category = ErrorCategory::Provider)
{
return {Kind::Error, ResponseEvents::Error{std::move(message)}};
return {Kind::Error, ResponseEvents::Error{std::move(message), category}};
}
private:

View File

@@ -79,7 +79,7 @@ void ResponseRouter::ensureAssistantOpen()
if (m_assistantOpen && !m_inToolResults)
return;
if (m_history)
m_history->append(Message(Message::Role::Assistant));
m_history->append(Message(Message::Role::Assistant, m_activeId));
emit event(ResponseEvent::messageStart());
m_assistantOpen = true;
m_inToolResults = false;
@@ -107,15 +107,19 @@ void ResponseRouter::onThinking(
}
void ResponseRouter::onToolStarted(
const LLMQore::RequestID &id, const QString &toolId, const QString &toolName)
const LLMQore::RequestID &id,
const QString &toolId,
const QString &toolName,
const QJsonObject &arguments)
{
if (id != m_activeId)
return;
ensureAssistantOpen();
if (m_history)
m_history->appendBlockToLast(
std::make_unique<LLMQore::ToolUseContent>(toolId, toolName));
std::make_unique<LLMQore::ToolUseContent>(toolId, toolName, arguments));
emit event(ResponseEvent::toolCallStart(toolId, toolName));
emit event(ResponseEvent::toolCallEnd(toolId, arguments));
}
void ResponseRouter::onToolResultReady(
@@ -124,7 +128,6 @@ void ResponseRouter::onToolResultReady(
const QString &toolName,
const QString &result)
{
Q_UNUSED(toolName);
if (id != m_activeId)
return;
@@ -141,7 +144,7 @@ void ResponseRouter::onToolResultReady(
m_assistantOpen = false;
m_inToolResults = true;
emit event(ResponseEvent::toolResult(toolId, result, /*isError=*/false));
emit event(ResponseEvent::toolResult(toolId, toolName, result, /*isError=*/false));
}
void ResponseRouter::onFinalized(
@@ -149,6 +152,13 @@ void ResponseRouter::onFinalized(
{
if (id != m_activeId)
return;
if (info.usage) {
emit event(ResponseEvent::usage(
info.usage->promptTokens,
info.usage->completionTokens,
info.usage->cachedPromptTokens,
info.usage->reasoningTokens));
}
emit event(ResponseEvent::messageStop(info.stopReason));
endRequest();
}
@@ -157,7 +167,7 @@ void ResponseRouter::onFailed(const LLMQore::RequestID &id, const QString &err)
{
if (id != m_activeId)
return;
emit event(ResponseEvent::error(err));
emit event(ResponseEvent::error(err, categorizeProviderError(err)));
endRequest();
}

View File

@@ -6,6 +6,7 @@
#include <LLMQore/BaseClient.hpp>
#include <QJsonObject>
#include <QObject>
#include <QPointer>
#include <QString>
@@ -41,7 +42,10 @@ private slots:
void onThinking(
const LLMQore::RequestID &id, const QString &thinking, const QString &signature);
void onToolStarted(
const LLMQore::RequestID &id, const QString &toolId, const QString &toolName);
const LLMQore::RequestID &id,
const QString &toolId,
const QString &toolName,
const QJsonObject &arguments);
void onToolResultReady(
const LLMQore::RequestID &id,
const QString &toolId,

View File

@@ -36,15 +36,9 @@ QString roleToLegacyString(Message::Role role)
return QStringLiteral("user");
}
} // namespace
[[maybe_unused]] const int kErrorInfoMetaTypeId = qRegisterMetaType<QodeAssist::ErrorInfo>();
Session::Session(QObject *parent)
: QObject(parent)
, m_history(new ConversationHistory(this))
, m_systemPrompt(new SystemPromptBuilder(this))
{
m_invalidReason = QStringLiteral("Session: no agent attached");
}
} // namespace
Session::Session(Agent *agent, QObject *parent)
: Session(agent, /*externalHistory=*/nullptr, parent)
@@ -86,7 +80,7 @@ Session::Session(Agent *agent, ConversationHistory *externalHistory, QObject *pa
Session::~Session()
{
if (isInFlight())
cancel();
teardownInFlight();
}
bool Session::isValid() const noexcept
@@ -104,6 +98,11 @@ bool Session::isInFlight() const noexcept
return !m_inFlight.isEmpty();
}
const ErrorInfo &Session::lastError() const noexcept
{
return m_lastError;
}
LLMQore::BaseClient *Session::client() const noexcept
{
auto *provider = m_agent ? m_agent->provider() : nullptr;
@@ -127,21 +126,6 @@ void Session::setContextBindings(Templates::ContextRenderer::Bindings bindings)
m_contextBindings = std::move(bindings);
}
QString Session::renderAgentContext() const
{
if (!m_agent)
return {};
const auto &cfg = m_agent->config();
if (cfg.systemPrompt.isEmpty())
return {};
QString err;
QString rendered
= Templates::ContextRenderer::render(cfg.systemPrompt, m_contextBindings, &err);
if (!err.isEmpty())
qWarning("[QodeAssist] agent.system render failed: %s", qUtf8Printable(err));
return rendered;
}
LLMQore::RequestID Session::sendText(const QString &text)
{
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
@@ -152,22 +136,27 @@ LLMQore::RequestID Session::sendText(const QString &text)
LLMQore::RequestID Session::sendCompletion(Templates::ContextData ctx)
{
if (!isValid())
if (!isValid()) {
m_lastError = makeError(ErrorCategory::Config, invalidReason());
return {};
}
if (isInFlight())
cancel();
return dispatchContext(std::move(ctx), /*tools=*/false, /*thinking=*/false);
return dispatchContext(std::move(ctx), /*tools=*/false);
}
LLMQore::RequestID Session::send(
std::vector<std::unique_ptr<LLMQore::ContentBlock>> userBlocks,
std::optional<bool> toolsOverride,
std::optional<bool> thinkingOverride)
std::optional<bool> toolsOverride)
{
if (!isValid() || userBlocks.empty())
if (!isValid()) {
m_lastError = makeError(ErrorCategory::Config, invalidReason());
return {};
if (!m_history)
}
if (userBlocks.empty() || !m_history) {
m_lastError = makeError(ErrorCategory::Validation, QStringLiteral("Session: nothing to send"));
return {};
}
if (isInFlight())
cancel();
@@ -177,10 +166,20 @@ LLMQore::RequestID Session::send(
msg.appendBlock(std::move(b));
m_history->append(std::move(msg));
return dispatch(toolsOverride, thinkingOverride);
return dispatch(toolsOverride);
}
void Session::cancel()
{
if (m_inFlight.isEmpty())
return;
const auto id = m_inFlight;
teardownInFlight();
emit cancelled(id);
}
void Session::teardownInFlight()
{
if (m_inFlight.isEmpty())
return;
@@ -191,41 +190,61 @@ void Session::cancel()
m_router->endRequest();
if (m_agent && m_agent->provider())
m_agent->provider()->cancelRequest(id);
emit failed(id, QStringLiteral("Cancelled by user"));
}
LLMQore::RequestID Session::dispatch(
std::optional<bool> toolsOverride, std::optional<bool> thinkingOverride)
LLMQore::RequestID Session::dispatch(std::optional<bool> toolsOverride)
{
const auto &cfg = m_agent->config();
const QString renderedContext = renderAgentContext();
if (renderedContext.isEmpty())
if (cfg.systemPrompt.isEmpty()) {
m_systemPrompt->clearLayer(QStringLiteral("agent.system"));
else
m_systemPrompt->setLayer(QStringLiteral("agent.system"), renderedContext);
} else {
QString renderErr;
const QString renderedContext = Templates::ContextRenderer::render(
cfg.systemPrompt, m_contextBindings, &renderErr);
if (!renderErr.isEmpty()) {
m_lastError = makeError(
ErrorCategory::Validation,
QStringLiteral("Agent '%1' system_prompt render failed: %2")
.arg(cfg.name, renderErr));
qWarning("[QodeAssist] %s", qUtf8Printable(m_lastError.message));
return {};
}
if (renderedContext.isEmpty())
m_systemPrompt->clearLayer(QStringLiteral("agent.system"));
else
m_systemPrompt->setLayer(
QStringLiteral("agent.system"), renderedContext, SystemPromptBuilder::kAgentPriority);
}
const bool tools = toolsOverride.value_or(cfg.enableTools);
const bool thinking = thinkingOverride.value_or(cfg.enableThinking);
return dispatchContext(toLegacyContext(), tools, thinking);
return dispatchContext(toLegacyContext(), tools);
}
LLMQore::RequestID Session::dispatchContext(
Templates::ContextData ctx, bool tools, bool thinking)
LLMQore::RequestID Session::dispatchContext(Templates::ContextData ctx, bool tools)
{
m_lastError = {};
auto *provider = m_agent->provider();
auto *tmpl = m_agent->promptTemplate();
const auto &cfg = m_agent->config();
QJsonObject payload{{QStringLiteral("model"), cfg.model}};
if (!provider->prepareRequest(payload, tmpl, ctx, tools, thinking))
QString prepareErr;
if (!provider->prepareRequest(payload, tmpl, ctx, tools, &prepareErr)) {
m_lastError = makeError(ErrorCategory::Validation, prepareErr, prepareErr);
return {};
}
QString endpoint = cfg.endpoint;
endpoint.replace(QStringLiteral("${MODEL}"), cfg.model);
const auto id = provider->sendRequest(QUrl(provider->url()), payload, endpoint);
if (id.isEmpty())
if (id.isEmpty()) {
m_lastError = makeError(
ErrorCategory::Provider,
QStringLiteral("Provider '%1' failed to start the request").arg(provider->name()));
return {};
}
m_inFlight = id;
if (m_router)
@@ -389,9 +408,11 @@ void Session::onRouterEvent(const ResponseEvent &ev)
} else if (ev.kind() == ResponseEvent::Kind::Error) {
const auto *err = ev.as<ResponseEvents::Error>();
const QString msg = err ? err->message : QStringLiteral("unknown error");
const ErrorCategory category = err ? err->category : ErrorCategory::Provider;
m_lastError = makeError(category, msg, msg);
const auto id = m_inFlight;
m_inFlight.clear();
emit failed(id, msg);
emit failed(id, m_lastError);
}
}

View File

@@ -20,6 +20,7 @@
#include <vector>
#include "ConversationHistory.hpp"
#include "ErrorInfo.hpp"
#include "ResponseEvent.hpp"
namespace QodeAssist {
@@ -33,8 +34,6 @@ class Session : public QObject
Q_OBJECT
Q_DISABLE_COPY_MOVE(Session)
public:
explicit Session(QObject *parent = nullptr);
Session(
Agent *agent,
ConversationHistory *externalHistory = nullptr,
@@ -47,6 +46,7 @@ public:
bool isValid() const noexcept;
QString invalidReason() const;
bool isInFlight() const noexcept;
const ErrorInfo &lastError() const noexcept;
using ContentLoader = std::function<QString(const QString &storedPath)>;
void setContentLoader(ContentLoader loader);
@@ -60,12 +60,9 @@ public:
void setContextBindings(Templates::ContextRenderer::Bindings bindings);
QString renderAgentContext() const;
LLMQore::RequestID send(
std::vector<std::unique_ptr<LLMQore::ContentBlock>> userBlocks,
std::optional<bool> toolsOverride = std::nullopt,
std::optional<bool> thinkingOverride = std::nullopt);
std::optional<bool> toolsOverride = std::nullopt);
LLMQore::RequestID sendText(const QString &text);
@@ -78,16 +75,16 @@ signals:
void started(const LLMQore::RequestID &id);
void finished(const LLMQore::RequestID &id, const QString &stopReason);
void failed(const LLMQore::RequestID &id, const QString &error);
void failed(const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error);
void cancelled(const LLMQore::RequestID &id);
private slots:
void onRouterEvent(const QodeAssist::ResponseEvent &ev);
private:
LLMQore::RequestID dispatch(
std::optional<bool> toolsOverride = std::nullopt,
std::optional<bool> thinkingOverride = std::nullopt);
LLMQore::RequestID dispatchContext(Templates::ContextData ctx, bool tools, bool thinking);
LLMQore::RequestID dispatch(std::optional<bool> toolsOverride = std::nullopt);
LLMQore::RequestID dispatchContext(Templates::ContextData ctx, bool tools);
void teardownInFlight();
Templates::ContextData toLegacyContext() const;
Agent *m_agent = nullptr; // child if non-null
@@ -97,17 +94,16 @@ private:
LLMQore::RequestID m_inFlight;
QString m_invalidReason;
ErrorInfo m_lastError;
Templates::ContextRenderer::Bindings m_contextBindings;
ContentLoader m_contentLoader;
public:
static Templates::ContextData buildLegacyContext(
const std::vector<Message> &history,
const QString &systemPrompt,
const ContentLoader &loader = ContentLoader{});
private:
ContentLoader m_contentLoader;
};
} // namespace QodeAssist

View File

@@ -10,10 +10,6 @@
namespace QodeAssist {
SessionManager::SessionManager(QObject *parent)
: QObject(parent)
{}
SessionManager::SessionManager(AgentFactory *agentFactory, QObject *parent)
: QObject(parent)
, m_agentFactory(agentFactory)
@@ -21,14 +17,6 @@ SessionManager::SessionManager(AgentFactory *agentFactory, QObject *parent)
SessionManager::~SessionManager() = default;
Session *SessionManager::createSession()
{
auto *session = new Session(this);
m_sessions.append(session);
emit sessionCreated(session);
return session;
}
Session *SessionManager::createSession(const QString &agentName, QString *errorOut)
{
return createSession(agentName, /*externalHistory=*/nullptr, errorOut);

View File

@@ -22,14 +22,10 @@ class SessionManager : public QObject
Q_OBJECT
Q_DISABLE_COPY_MOVE(SessionManager)
public:
explicit SessionManager(QObject *parent = nullptr);
SessionManager(AgentFactory *agentFactory, QObject *parent = nullptr);
explicit SessionManager(AgentFactory *agentFactory, QObject *parent = nullptr);
~SessionManager() override;
Session *createSession();
Session *createSession(const QString &agentName, QString *errorOut = nullptr);
Session *createSession(

View File

@@ -4,30 +4,34 @@
#include "SystemPromptBuilder.hpp"
#include <algorithm>
namespace QodeAssist {
SystemPromptBuilder::SystemPromptBuilder(QObject *parent)
: QObject(parent)
{}
void SystemPromptBuilder::setLayer(const QString &name, const QString &text)
void SystemPromptBuilder::setLayer(const QString &name, const QString &text, int priority)
{
for (auto &pair : m_layers) {
if (pair.first == name) {
if (pair.second == text) return;
pair.second = text;
for (auto &layer : m_layers) {
if (layer.name == name) {
if (layer.text == text && layer.priority == priority)
return;
layer.text = text;
layer.priority = priority;
emit layersChanged();
return;
}
}
m_layers.append({name, text});
m_layers.append({name, text, priority});
emit layersChanged();
}
void SystemPromptBuilder::clearLayer(const QString &name)
{
for (auto it = m_layers.begin(); it != m_layers.end(); ++it) {
if (it->first == name) {
if (it->name == name) {
m_layers.erase(it);
emit layersChanged();
return;
@@ -44,8 +48,8 @@ void SystemPromptBuilder::clear()
QString SystemPromptBuilder::layer(const QString &name) const
{
for (const auto &pair : m_layers) {
if (pair.first == name) return pair.second;
for (const auto &l : m_layers) {
if (l.name == name) return l.text;
}
return {};
}
@@ -54,17 +58,22 @@ QStringList SystemPromptBuilder::layerNames() const
{
QStringList out;
out.reserve(m_layers.size());
for (const auto &pair : m_layers) out.append(pair.first);
for (const auto &l : m_layers) out.append(l.name);
return out;
}
QString SystemPromptBuilder::compose(const QString &separator) const
{
QVector<Layer> ordered = m_layers;
std::stable_sort(
ordered.begin(), ordered.end(),
[](const Layer &a, const Layer &b) { return a.priority < b.priority; });
QStringList parts;
parts.reserve(m_layers.size());
for (const auto &pair : m_layers) {
if (!pair.second.isEmpty())
parts.append(pair.second);
parts.reserve(ordered.size());
for (const auto &l : ordered) {
if (!l.text.isEmpty())
parts.append(l.text);
}
return parts.join(separator);
}

View File

@@ -15,9 +15,12 @@ class SystemPromptBuilder : public QObject
{
Q_OBJECT
public:
static constexpr int kAgentPriority = 0;
static constexpr int kDefaultPriority = 100;
explicit SystemPromptBuilder(QObject *parent = nullptr);
void setLayer(const QString &name, const QString &text);
void setLayer(const QString &name, const QString &text, int priority = kDefaultPriority);
void clearLayer(const QString &name);
void clear();
@@ -31,7 +34,14 @@ signals:
void layersChanged();
private:
QVector<QPair<QString, QString>> m_layers;
struct Layer
{
QString name;
QString text;
int priority = kDefaultPriority;
};
QVector<Layer> m_layers;
};
} // namespace QodeAssist