// Copyright (C) 2024-2026 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later #include "Session.hpp" #include #include #include #include #include #include "Agent.hpp" #include "AgentConfig.hpp" #include "ContextData.hpp" #include "Message.hpp" #include "PluginBlocks.hpp" #include "PromptTemplate.hpp" #include "Provider.hpp" #include "ResponseRouter.hpp" #include "SystemPromptBuilder.hpp" namespace QodeAssist { namespace { QString roleToLegacyString(Message::Role role) { switch (role) { case Message::Role::System: return QStringLiteral("system"); case Message::Role::User: return QStringLiteral("user"); case Message::Role::Assistant: return QStringLiteral("assistant"); } return QStringLiteral("user"); } } // namespace Session::Session(QObject *parent) : QObject(parent) , m_history(new ConversationHistory(this)) , m_systemPrompt(new SystemPromptBuilder(this)) { m_invalidReason = QStringLiteral("Session: no agent attached"); } Session::Session(Agent *agent, QObject *parent) : Session(agent, /*externalHistory=*/nullptr, parent) {} Session::Session(Agent *agent, ConversationHistory *externalHistory, QObject *parent) : QObject(parent) , m_agent(agent) , m_history(externalHistory ? externalHistory : new ConversationHistory(this)) , m_systemPrompt(new SystemPromptBuilder(this)) { if (!m_agent) { m_invalidReason = QStringLiteral("Session: agent is null"); return; } m_agent->setParent(this); if (!m_agent->isValid()) { m_invalidReason = m_agent->invalidReason(); return; } auto *provider = m_agent->provider(); auto *client = provider ? provider->client() : nullptr; if (!client) { m_invalidReason = QStringLiteral("Session: provider has no live client"); return; } if (!m_agent->promptTemplate()) { m_invalidReason = QStringLiteral("Session: agent has no inline prompt template"); return; } m_router = new ResponseRouter(client, m_history, this); connect(m_router, &ResponseRouter::event, this, &Session::onRouterEvent); m_systemPrompt->setLayer(QStringLiteral("agent.role"), m_agent->config().role); } Session::~Session() { if (isInFlight()) cancel(); } bool Session::isValid() const noexcept { return m_invalidReason.isEmpty(); } QString Session::invalidReason() const { return m_invalidReason; } bool Session::isInFlight() const noexcept { return !m_inFlight.isEmpty(); } void Session::setContentLoader(ContentLoader loader) { m_contentLoader = std::move(loader); } 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.context.isEmpty()) return {}; QString err; QString rendered = Templates::ContextRenderer::render(cfg.context, m_contextBindings, &err); if (!err.isEmpty()) qWarning("[QodeAssist] agent.context render failed: %s", qUtf8Printable(err)); return rendered; } LLMQore::RequestID Session::sendText(const QString &text) { std::vector> blocks; if (!text.isEmpty()) blocks.push_back(std::make_unique(text)); return send(std::move(blocks)); } LLMQore::RequestID Session::send( std::vector> userBlocks, std::optional toolsOverride) { if (!isValid() || userBlocks.empty()) return {}; if (!m_history) return {}; if (isInFlight()) cancel(); Message msg(Message::Role::User); for (auto &b : userBlocks) msg.appendBlock(std::move(b)); m_history->append(std::move(msg)); return dispatch(toolsOverride); } void Session::cancel() { if (m_inFlight.isEmpty()) return; const auto id = m_inFlight; m_inFlight.clear(); if (m_router) m_router->endRequest(); if (m_agent && m_agent->provider()) m_agent->provider()->cancelRequest(id); emit failed(id, QStringLiteral("Cancelled by user")); } LLMQore::RequestID Session::sendCompletion(Templates::ContextData ctx) { if (!isValid()) return {}; if (isInFlight()) cancel(); if (m_history) m_history->clear(); 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=*/false, /*thinking=*/false)) return {}; const auto id = provider->sendRequest(QUrl(provider->url()), payload, cfg.endpoint); if (id.isEmpty()) return {}; m_inFlight = id; if (m_router) m_router->beginRequest(id); emit started(id); return id; } LLMQore::RequestID Session::dispatch(std::optional toolsOverride) { auto *provider = m_agent->provider(); auto *tmpl = m_agent->promptTemplate(); const auto &cfg = m_agent->config(); const QString renderedContext = renderAgentContext(); if (renderedContext.isEmpty()) m_systemPrompt->clearLayer(QStringLiteral("agent.context")); else m_systemPrompt->setLayer(QStringLiteral("agent.context"), renderedContext); Templates::ContextData ctx = toLegacyContext(); QJsonObject payload{{QStringLiteral("model"), cfg.model}}; const bool tools = toolsOverride.value_or(cfg.enableTools); if (!provider->prepareRequest(payload, tmpl, ctx, tools, cfg.enableThinking)) return {}; const auto id = provider->sendRequest(QUrl(provider->url()), payload, cfg.endpoint); if (id.isEmpty()) return {}; m_inFlight = id; if (m_router) m_router->beginRequest(id); emit started(id); return id; } Templates::ContextData Session::toLegacyContext() const { if (!m_history) return {}; return buildLegacyContext(m_history->messages(), m_systemPrompt->compose(), m_contentLoader); } Templates::ContextData Session::buildLegacyContext( const std::vector &history, const QString &systemPrompt, const ContentLoader &loader) { using Templates::ContentBlockEntry; using Templates::ContextData; using LegacyMessage = Templates::Message; ContextData ctx; if (!systemPrompt.isEmpty()) ctx.systemPrompt = systemPrompt; QSet resolvedToolUseIds; QSet declaredToolUseIds; for (const auto &m : history) { for (const auto &blockPtr : m.blocks()) { if (auto *tr = dynamic_cast(blockPtr.get())) resolvedToolUseIds.insert(tr->toolUseId()); if (auto *tu = dynamic_cast(blockPtr.get())) declaredToolUseIds.insert(tu->id()); } } QVector hist; for (const auto &m : history) { QVector blockEntries; for (const auto &blockPtr : m.blocks()) { auto *block = blockPtr.get(); if (!block) continue; if (auto *t = dynamic_cast(block)) { ContentBlockEntry e; e.kind = ContentBlockEntry::Kind::Text; e.text = t->text(); blockEntries.append(std::move(e)); } else if (auto *img = dynamic_cast(block)) { ContentBlockEntry e; e.kind = ContentBlockEntry::Kind::Image; e.imageData = img->data(); e.mediaType = img->mediaType(); e.isImageUrl = (img->sourceType() == LLMQore::ImageContent::ImageSourceType::Url); blockEntries.append(std::move(e)); } else if (auto *si = dynamic_cast(block)) { if (!loader) continue; const QString base64 = loader(si->storedPath()); if (base64.isEmpty()) continue; ContentBlockEntry e; e.kind = ContentBlockEntry::Kind::Image; e.imageData = base64; e.mediaType = si->mediaType(); e.isImageUrl = false; blockEntries.append(std::move(e)); } else if (auto *sa = dynamic_cast(block)) { if (!loader) continue; const QString text = loader(sa->storedPath()); if (text.isEmpty()) continue; ContentBlockEntry e; e.kind = ContentBlockEntry::Kind::Text; e.text = QStringLiteral("File: %1\n```\n%2\n```") .arg(sa->fileName(), text); blockEntries.append(std::move(e)); } else if (auto *th = dynamic_cast(block)) { ContentBlockEntry e; e.kind = ContentBlockEntry::Kind::Thinking; e.thinking = th->thinking(); e.signature = th->signature(); blockEntries.append(std::move(e)); } else if (auto *rth = dynamic_cast(block)) { ContentBlockEntry e; e.kind = ContentBlockEntry::Kind::RedactedThinking; e.signature = rth->signature(); blockEntries.append(std::move(e)); } else if (auto *tu = dynamic_cast(block)) { if (!resolvedToolUseIds.contains(tu->id())) continue; ContentBlockEntry e; e.kind = ContentBlockEntry::Kind::ToolUse; e.toolUseId = tu->id(); e.toolName = tu->name(); e.toolInput = tu->input(); blockEntries.append(std::move(e)); } else if (auto *tr = dynamic_cast(block)) { if (!declaredToolUseIds.contains(tr->toolUseId())) continue; ContentBlockEntry e; e.kind = ContentBlockEntry::Kind::ToolResult; e.toolUseId = tr->toolUseId(); e.result = tr->result(); blockEntries.append(std::move(e)); } } if (blockEntries.isEmpty()) continue; const bool hasNonThinking = std::any_of( blockEntries.begin(), blockEntries.end(), [](const ContentBlockEntry &e) { return e.kind != ContentBlockEntry::Kind::Thinking && e.kind != ContentBlockEntry::Kind::RedactedThinking; }); if (!hasNonThinking) continue; LegacyMessage lm; lm.role = roleToLegacyString(m.role()); lm.blocks = std::move(blockEntries); hist.append(std::move(lm)); } if (!hist.isEmpty()) ctx.history = std::move(hist); return ctx; } void Session::onRouterEvent(const ResponseEvent &ev) { if (m_inFlight.isEmpty()) return; // stale events after cancel emit event(ev); if (ev.kind() == ResponseEvent::Kind::MessageStop) { const auto *stop = ev.as(); const QString reason = stop ? stop->stopReason : QString(); const auto id = m_inFlight; m_inFlight.clear(); emit finished(id, reason); } else if (ev.kind() == ResponseEvent::Kind::Error) { const auto *err = ev.as(); const QString msg = err ? err->message : QStringLiteral("unknown error"); const auto id = m_inFlight; m_inFlight.clear(); emit failed(id, msg); } } } // namespace QodeAssist