feat(server): improve doc tools error handle (#14662)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Centralized sync/status messages for cloud document sync and explicit
user-facing error types.
* Frontend helpers to detect and display tool errors with friendly
names.

* **Bug Fixes**
* Consistent, actionable error reporting for document and attachment
reads instead of silent failures.
* Search and semantic tools now validate workspace sync and permissions
and return clear responses.

* **Tests**
* Added comprehensive tests covering document/blob reads, search tools,
and sync/error paths.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-03-16 02:20:35 +08:00
committed by GitHub
parent 8f03090780
commit 121c0d172d
11 changed files with 419 additions and 70 deletions
@@ -7,6 +7,8 @@ import { html, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import type { ToolResult } from './tool-result-card';
import { getToolErrorDisplayName, isToolError } from './tool-result-utils';
import type { ToolError } from './type';
interface DocKeywordSearchToolCall {
type: 'tool-call';
@@ -20,10 +22,7 @@ interface DocKeywordSearchToolResult {
toolCallId: string;
toolName: string;
args: { query: string };
result: Array<{
title: string;
docId: string;
}>;
result: Array<{ title: string; docId: string }> | ToolError | null;
}
export class DocKeywordSearchResult extends WithDisposable(ShadowlessElement) {
@@ -51,9 +50,23 @@ export class DocKeywordSearchResult extends WithDisposable(ShadowlessElement) {
if (this.data.type !== 'tool-result') {
return nothing;
}
const result = this.data.result;
if (!result || isToolError(result)) {
return html`<tool-call-failed
.name=${getToolErrorDisplayName(
isToolError(result) ? result : null,
'Document search failed',
{
'Workspace Sync Required':
'Enable workspace sync to search documents',
}
)}
.icon=${SearchIcon()}
></tool-call-failed>`;
}
let results: ToolResult[] = [];
try {
results = this.data.result.map(item => ({
results = result.map(item => ({
title: item.title,
icon: PageIcon(),
onClick: () => {
@@ -69,7 +82,7 @@ export class DocKeywordSearchResult extends WithDisposable(ShadowlessElement) {
console.error('Failed to parse result', err);
}
return html`<tool-result-card
.name=${`Found ${this.data.result.length} pages for "${this.data.args.query}"`}
.name=${`Found ${result.length} pages for "${this.data.args.query}"`}
.icon=${SearchIcon()}
.width=${this.width}
.results=${results}
@@ -6,6 +6,9 @@ import type { Signal } from '@preact/signals-core';
import { html, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import { getToolErrorDisplayName, isToolError } from './tool-result-utils';
import type { ToolError } from './type';
interface DocReadToolCall {
type: 'tool-call';
toolCallId: string;
@@ -18,14 +21,24 @@ interface DocReadToolResult {
toolCallId: string;
toolName: string;
args: { doc_id: string };
result: {
/** Old result may not have docId */
docId?: string;
title: string;
markdown: string;
};
result:
| {
/** Old result may not have docId */
docId?: string;
title: string;
markdown: string;
}
| ToolError
| null;
}
const getFailedName = (result: ToolError | null) => {
return getToolErrorDisplayName(result, 'Document read failed', {
'Workspace Sync Required': 'Enable workspace sync to read this document',
'Document Sync Pending': 'Wait for document sync to finish',
});
};
export class DocReadResult extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor data!: DocReadToolCall | DocReadToolResult;
@@ -49,18 +62,25 @@ export class DocReadResult extends WithDisposable(ShadowlessElement) {
if (this.data.type !== 'tool-result') {
return nothing;
}
const result = this.data.result;
if (!result || isToolError(result)) {
return html`<tool-call-failed
.name=${getFailedName(isToolError(result) ? result : null)}
.icon=${ViewIcon()}
></tool-call-failed>`;
}
// TODO: better markdown rendering
return html`<tool-result-card
.name=${`Read "${this.data.result.title}"`}
.name=${`Read "${result.title}"`}
.icon=${ViewIcon()}
.width=${this.width}
.results=${[
{
title: this.data.result.title,
title: result.title,
icon: PageIcon(),
content: this.data.result.markdown,
content: result.markdown,
onClick: () => {
const docId = (this.data as DocReadToolResult).result.docId;
const docId = result.docId;
if (!docId) {
return;
}
@@ -7,6 +7,8 @@ import { html, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import type { DocDisplayConfig } from '../ai-chat-chips';
import { getToolErrorDisplayName, isToolError } from './tool-result-utils';
import type { ToolError } from './type';
interface DocSemanticSearchToolCall {
type: 'tool-call';
@@ -20,10 +22,7 @@ interface DocSemanticSearchToolResult {
toolCallId: string;
toolName: string;
args: { query: string };
result: Array<{
content: string;
docId: string;
}>;
result: Array<{ content: string; docId: string }> | ToolError | null;
}
function parseResultContent(content: string) {
@@ -82,11 +81,25 @@ export class DocSemanticSearchResult extends WithDisposable(ShadowlessElement) {
if (this.data.type !== 'tool-result') {
return nothing;
}
const result = this.data.result;
if (!result || isToolError(result)) {
return html`<tool-call-failed
.name=${getToolErrorDisplayName(
isToolError(result) ? result : null,
'Semantic search failed',
{
'Workspace Sync Required':
'Enable workspace sync to search documents',
}
)}
.icon=${AiEmbeddingIcon()}
></tool-call-failed>`;
}
return html`<tool-result-card
.name=${`Found semantically related pages for "${this.data.args.query}"`}
.icon=${AiEmbeddingIcon()}
.width=${this.width}
.results=${this.data.result
.results=${result
.map(result => ({
...parseResultContent(result.content),
title: this.docDisplayService.getTitle(result.docId),
@@ -0,0 +1,16 @@
import type { ToolError } from './type';
export const isToolError = (result: unknown): result is ToolError =>
!!result &&
typeof result === 'object' &&
'type' in result &&
(result as ToolError).type === 'error';
export const getToolErrorDisplayName = (
result: ToolError | null,
fallback: string,
overrides: Record<string, string> = {}
) => {
if (!result) return fallback;
return overrides[result.name] ?? result.name;
};