* @brief Stores a tool_result block to be sent back in the next request. */
| 1868 | * @brief Stores a tool_result block to be sent back in the next request. |
| 1869 | */ |
| 1870 | void AI::Conversation::recordToolResult(const QString& callId, |
| 1871 | const QString& name, |
| 1872 | const QJsonObject& payload) |
| 1873 | { |
| 1874 | if (!m_busy) { |
| 1875 | qCWarning(serialStudioAI) << "Dropping tool result for" << name << "(" << callId |
| 1876 | << "): no turn in flight (late completion after error/cancel)"; |
| 1877 | return; |
| 1878 | } |
| 1879 | |
| 1880 | const auto scrubbed = AI::Redactor::scrubObject(payload); |
| 1881 | |
| 1882 | constexpr int kFsResultByteBudget = 48 * 1024; |
| 1883 | const bool isFsReadResult = name == QStringLiteral("fs.read") |
| 1884 | || name == QStringLiteral("fs.search") |
| 1885 | || name == QStringLiteral("fs.list"); |
| 1886 | const int kMaxToolResultBytes = |
| 1887 | isFsReadResult ? kFsResultByteBudget |
| 1888 | : qBound(2048, |
| 1889 | m_provider ? m_provider->capabilities().toolResultByteBudget : 4096, |
| 1890 | 16 * 1024); |
| 1891 | |
| 1892 | QJsonObject effective = scrubbed; |
| 1893 | auto contentBytes = QJsonDocument(scrubbed).toJson(QJsonDocument::Compact); |
| 1894 | if (contentBytes.size() > kMaxToolResultBytes) { |
| 1895 | effective = makeTruncatedResult(scrubbed, contentBytes, kMaxToolResultBytes); |
| 1896 | contentBytes = QJsonDocument(effective).toJson(QJsonDocument::Compact); |
| 1897 | qCDebug(serialStudioAI) << "Tool result for" << name << "truncated to" << contentBytes.size() |
| 1898 | << "bytes"; |
| 1899 | } |
| 1900 | |
| 1901 | const auto sourceTag = name.isEmpty() ? QStringLiteral("tool_result") : name; |
| 1902 | QString wrapped; |
| 1903 | wrapped += QStringLiteral("<untrusted source=\""); |
| 1904 | wrapped += sourceTag.toHtmlEscaped(); |
| 1905 | wrapped += QStringLiteral("\">\n"); |
| 1906 | wrapped += neutralizeUntrustedDelimiter(QString::fromUtf8(contentBytes)); |
| 1907 | wrapped += QStringLiteral("\n</untrusted>"); |
| 1908 | |
| 1909 | QJsonObject block; |
| 1910 | block[QStringLiteral("type")] = QStringLiteral("tool_result"); |
| 1911 | block[QStringLiteral("tool_use_id")] = callId; |
| 1912 | block[QStringLiteral("content")] = wrapped; |
| 1913 | QJsonObject geminiPayload = effective; |
| 1914 | geminiPayload[QStringLiteral("__untrusted_source")] = sourceTag; |
| 1915 | block[QStringLiteral("_gemini_response")] = geminiPayload; |
| 1916 | if (!name.isEmpty()) |
| 1917 | block[QStringLiteral("_tool_name")] = name; |
| 1918 | |
| 1919 | m_pendingToolResultBlocks.append(block); |
| 1920 | } |
| 1921 | |
| 1922 | /** |
| 1923 | * @brief Decrements the outstanding tool-result counter without underflowing past zero. |
nothing calls this directly
no test coverage detected