Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# v0.8.8-rc4
# v0.8.8

# Base node image
FROM node:24.16.0-alpine AS node
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.multi
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Dockerfile.multi
# v0.8.8-rc4
# v0.8.8

# Set configurable max-old-space-size with default
ARG NODE_MAX_OLD_SPACE_SIZE=6144
Expand Down
26 changes: 15 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,21 @@
</a>
</p>

## 🚀 What's New in v0.8.8-rc4

- **Public Agents API docs:** Serve an OpenAPI specification and interactive Swagger UI for inference, events, Agent management, and Skill management.
- **Attached workspaces (highly experimental):** Isolate workspaces by conversation, load repository instructions, and use bounded queue waits and command timeouts.
- **Trace Viewer:** Inspect model conversations as ordered steps with roles, Agent identity, tool rounds, previews, and cost.
- **Skills:** Author or import a Skill and invoke it in the same Agent run, with safer rollback for failed imports.
- **Agent activity:** Render system events as distinct turns and hold live activity to one stable row.
- **MCP reliability:** Send per-request headers without hiding tools, coordinate OAuth refresh across replicas, and preserve credentials through provider outages.
- **Performance:** Stream Markdown incrementally, virtualize model search, and reduce completed Agent message rendering work.

Read the [full v0.8.8-rc4 changelog](https://www.librechat.ai/changelog/v0.8.8-rc4).
## 🚀 What's New in v0.8.8

- **Agent Management API (beta):** Create, discover, update, and delete Agents; manage Agent files and Skills; and authenticate machine clients through deployment-bound OIDC identities while preserving existing role and Agent access controls. Public OpenAPI and Swagger UI cover inference, events, Agent management, and Skill management.
- **Attached workspaces (highly experimental):** Select or save a per-Agent default workspace for each managed or personal code worker, then let Agents inspect trees, read and search files, author changes, and run Bash with bounded timeouts. Personal workers support bounded self-service enrollment, readiness status, and per-Agent Git identity.
- **Background tool controls:** Optionally cancel ordinary background tools, including attached Bash, while keeping detached Subagent execution independent.
- **Code approval controls:** Choose **Ask**, **Allow**, or **Deny** for file writes and command execution where administrators permit it, including a **Full access** mode for trusted attached environments. File Search and Run Code also honor role grants.
- **Manual context compaction:** Start a summarize-only turn before the context window fills while preserving recent conversation content according to the deployment's summarization policy.
- **Context Usage:** Inspect dialogue, retained tool traffic, Agent instructions, cache, cost, and runway pressure without double-counting category subsets.
- **Unified attachments:** Upload once and let LibreChat route content to the model or extracted text, then provision File Search and Code tools only when needed.
- **Models:** Added GPT-6 Astra and GPT-6.1 Sol, with Responses API routing and tool-call support.
- **Agent and chat UI:** Unified tool activity, reasoning, search, and Agent workflows; added one draggable Pinned section for chats and favorites, morphing state icons, high-contrast themes, rich-text message copying, clearer sidebar titles, and refined live phase layouts.
- **Observability:** Inspect ordered model conversations, tool rounds, and costs in the Trace Viewer; export correlated application logs through OpenTelemetry, configure allowlisted Langfuse trace identity and metadata, tag browser diagnostics with client build IDs, and scope Insights to authorized Agents.
- **Reliability and security:** Strengthened Agent continuation and checkpoint recovery, Redis liveness detection, DocumentDB coordination, OpenID and MCP OAuth sessions, shared-link throttling, tenant isolation, attachment bounds, and upload error handling.

Read the [full v0.8.8 changelog](https://www.librechat.ai/changelog/v0.8.8).

# ✨ Features

Expand Down
30 changes: 5 additions & 25 deletions api/app/clients/BaseClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const {
withBalanceReservations,
findCheckpointSummaryPart,
getSummaryPartText,
resolveCheckpointMessage,
runAfterSeed,
saveTurnConversation,
seedTurnConversation,
Expand Down Expand Up @@ -1419,6 +1420,7 @@ class BaseClient {
* - The message's 'role' is set to 'system'.
* - The message's 'text' is set to its 'summary'.
* - If the message has a 'summaryTokenCount', the message's 'tokenCount' is set to 'summaryTokenCount'.
* - A message with a summary content block keeps its content from that block on, for the SDK formatter to promote.
* The traversal stops at the message with the 'summary' property.
*
* Each message object should have an 'id' or 'messageId' property and may have a 'parentMessageId' property.
Expand Down Expand Up @@ -1468,35 +1470,13 @@ class BaseClient {
break;
}

let resolved = message;
let hasSummary = false;
if (summary) {
const summaryBlock = findCheckpointSummaryPart(message.content);
if (summaryBlock) {
const summaryText = getSummaryPartText(summaryBlock);
resolved = {
...message,
role: 'system',
content: [{ type: ContentTypes.TEXT, text: summaryText }],
tokenCount: summaryBlock.tokenCount,
};
hasSummary = true;
} else if (message.summary) {
resolved = {
...message,
role: 'system',
content: [{ type: ContentTypes.TEXT, text: message.summary }],
tokenCount: message.summaryTokenCount ?? message.tokenCount,
};
hasSummary = true;
}
}

const checkpoint = summary ? resolveCheckpointMessage(message) : null;
const resolved = checkpoint ?? message;
const shouldMap = mapMethod != null && (mapCondition != null ? mapCondition(resolved) : true);
const processedMessage = shouldMap ? mapMethod(resolved) : resolved;
orderedMessages.push(processedMessage);

if (hasSummary) {
if (checkpoint) {
break;
}

Expand Down
40 changes: 35 additions & 5 deletions api/app/clients/specs/BaseClient.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -548,9 +548,10 @@ describe('BaseClient', () => {
summary: true,
});
expect(result).toHaveLength(2);
expect(result[0].role).toBe('system');
expect(result[0].content).toEqual([{ type: 'text', text: 'Content block summary' }]);
expect(result[0].tokenCount).toBe(42);
expect(result[0].role).toBeUndefined();
expect(result[0].content).toEqual([
{ type: 'summary', text: 'Content block summary', tokenCount: 42 },
]);
});

it('should prefer content block summary over legacy summary field', () => {
Expand All @@ -571,8 +572,9 @@ describe('BaseClient', () => {
summary: true,
});
expect(result).toHaveLength(2);
expect(result[0].content).toEqual([{ type: 'text', text: 'Content block summary' }]);
expect(result[0].tokenCount).toBe(20);
expect(result[0].content).toEqual([
{ type: 'summary', text: 'Content block summary', tokenCount: 20 },
]);
});

it('should fallback to legacy summary when no content block exists', () => {
Expand All @@ -596,6 +598,34 @@ describe('BaseClient', () => {
expect(result[0].tokenCount).toBe(15);
});

it('keeps the parts a response produced after its summary (summary mode)', () => {
const trailingText = { type: 'text', text: 'Answer after summarizing' };
const messagesWithTrailingParts = [
{ id: '1', parentMessageId: null, text: 'Message 1' },
{
id: '2',
parentMessageId: '1',
text: 'Before summarizing',
content: [
{ type: 'text', text: 'Before summarizing' },
{ type: 'summary', text: 'Earlier context', tokenCount: 6 },
trailingText,
],
},
{ id: '3', parentMessageId: '2', text: 'Message 3' },
];
const result = TestClient.constructor.getMessagesForConversation({
messages: messagesWithTrailingParts,
parentMessageId: '3',
summary: true,
});
expect(result.map((message) => message.id)).toEqual(['2', '3']);
expect(result[0].content).toEqual([
{ type: 'summary', text: 'Earlier context', tokenCount: 6 },
trailingText,
]);
});

it('should not stop traversal at a failed summary, keeping the prior history', () => {
/** A summarize round that errored keeps the deltas it streamed, so its
* text is a truncated prefix; treating it as the checkpoint would send
Expand Down
4 changes: 2 additions & 2 deletions api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@librechat/backend",
"version": "v0.8.8-rc4",
"version": "v0.8.8",
"description": "",
"scripts": {
"start": "echo 'please run this from the root directory'",
Expand Down Expand Up @@ -46,7 +46,7 @@
"@azure/storage-blob": "^12.30.0",
"@google/genai": "^2.8.0",
"@keyv/redis": "5.1.6",
"@librechat/agents": "^3.9.7",
"@librechat/agents": "^4.0.0",
"@librechat/api": "*",
"@librechat/data-schemas": "*",
"@microsoft/microsoft-graph-client": "^3.0.7",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,11 @@ describe('AgentClient retained answers', () => {
client.user = 'user-123';
client.processMemory = jest.fn();
const rows = compactedBranch();
/** The stored count covers the whole response, including what preceded its summary. */
const storedResponseTokens = 5000;
const response = rows.find((row) => row.messageId === 'a2');
response.tokenCount = storedResponseTokens;
response.content.unshift({ type: ContentTypes.TEXT, text: 'Checking the pipeline.' });
getMessages.mockResolvedValue(rows);
/** The real loader: one read of the conversation, then the summary-bounded walk. */
const cut = await client.loadHistory('convo-123', 'u3');
Expand All @@ -239,6 +244,10 @@ describe('AgentClient retained answers', () => {
expect(text.indexOf(ANSWER_LINE)).toBeLessThan(text.indexOf(LATEST_TEXT));
expect(text.endsWith(LATEST_TEXT)).toBe(true);
expect(client.options.agent.additional_instructions ?? '').not.toContain(ANSWER_LINE);
expect(text).toContain('Deployed.');
expect(text).not.toContain('Checking the pipeline.');
expect(tokenCountMap.a2).toBeGreaterThan(0);
expect(tokenCountMap.a2).toBeLessThan(storedResponseTokens);
expect(cut[1].text).toBe(LATEST_TEXT);
expect(cut[1].content).toBeUndefined();
expect(counts[prompt.length - 1]).toBe(tokenCountMap.u3);
Expand Down
12 changes: 6 additions & 6 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion client/jest.config.cjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** v0.8.8-rc4 */
/** v0.8.8 */
const { maxWorkers } = require('../config/jest.workers.cjs');

module.exports = {
Expand Down
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@librechat/frontend",
"version": "v0.8.8-rc4",
"version": "v0.8.8",
"description": "",
"type": "module",
"scripts": {
Expand Down
46 changes: 36 additions & 10 deletions client/src/components/Chat/Messages/Content/ToolApproval.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useEffect, useMemo } from 'react';
import { useContext, useEffect, useMemo } from 'react';
import { Button, TextareaAutosize } from '@librechat/client';
import { Check, X, Pencil, MessageSquare, TriangleAlert } from 'lucide-react';
import { Check, X, Pencil, MessageSquare, ShieldQuestion, TriangleAlert } from 'lucide-react';
import type { Agents } from 'librechat-data-provider';
import type { TranslationKeys } from '~/hooks';
import { useComposerPresentsApproval } from '~/components/Chat/approval/state';
import { boundApprovalLabel } from '~/components/Chat/approval/preview';
import { useApprovalContext, useResumeSubmit } from './ApprovalContext';
import { ChatContext } from '~/Providers/ChatContext';
import { useLocalize } from '~/hooks';
import { cn, logger } from '~/utils';

Expand Down Expand Up @@ -69,22 +71,28 @@ function seedArgs(args: string | Record<string, unknown> | undefined): string {
* Renders approve / reject / edit / respond controls for a paused tool call,
* scoped to the decisions the server allows. Records its decision in the
* batch {@link useApprovalContext}; the lead card additionally renders the
* single submit button covering every paused call in the action.
* single submit button covering every paused call in the action. While the
* composer's review panel is open on the same action, a thread card renders
* only the record of the request: the panel owns the decisions and the submit,
* and it overlays the tail of the thread where this card sits.
*/
export default function ToolApproval({
approval,
toolCallId,
args,
showSubmit = true,
surface = 'thread',
}: {
approval: NonNullable<Agents.ToolCall['approval']>;
toolCallId: string;
args: string | Record<string, unknown> | undefined;
/** The composer owns one batch submit; timeline cards keep the historical lead button. */
showSubmit?: boolean;
/** The composer panel owns one batch submit; thread cards keep the lead button. */
surface?: 'thread' | 'composer';
}) {
const localize = useLocalize();
const { actionId, allowed_decisions: allowedDecisions, description } = approval;
const conversationId = useContext(ChatContext)?.conversation?.conversationId;
const composerPresents = useComposerPresentsApproval(conversationId, actionId);
const deferToComposer = surface === 'thread' && composerPresents;
const {
registerToolCall,
unregisterToolCall,
Expand Down Expand Up @@ -209,15 +217,33 @@ export default function ToolApproval({
return null;
}

const descriptionNode = safeDescription != null && safeDescription.length > 0 && (
<p className="text-sm text-text-secondary">{safeDescription}</p>
);

if (deferToComposer) {
return (
<div
className="my-2 flex w-full flex-col gap-2 rounded-lg border border-border-light bg-surface-secondary p-3"
data-testid="tool-approval"
data-tool-call-id={toolCallId}
>
{descriptionNode}
<p className="flex items-center gap-1.5 text-xs text-text-secondary">
<ShieldQuestion className="size-4 shrink-0" aria-hidden="true" />
{localize('com_ui_approval_review_in_composer')}
</p>
</div>
);
}

return (
<div
className="my-2 flex w-full flex-col gap-2 rounded-lg border border-border-light bg-surface-secondary p-3"
data-testid="tool-approval"
data-tool-call-id={toolCallId}
>
{safeDescription != null && safeDescription.length > 0 && (
<p className="text-sm text-text-secondary">{safeDescription}</p>
)}
{descriptionNode}
<div className="flex flex-wrap gap-2">
{allowedDecisions.map((decision) => {
const Icon = DECISION_ICON[decision];
Expand Down Expand Up @@ -281,7 +307,7 @@ export default function ToolApproval({
/>
)}

{showSubmit && isLead && (
{surface === 'thread' && isLead && (
<div className="mt-1 flex items-center gap-3">
<Button
size="sm"
Expand Down
Loading
Loading