fix re enter problem

This commit is contained in:
2026-07-10 15:56:41 +08:00
parent c077767e0d
commit e1ae49f114
4 changed files with 123 additions and 40 deletions
@@ -172,34 +172,3 @@ fn build_tools_system_prompt(base_prompt: &str, tool_specs: &[ToolPromptSpec]) -
prompt
}
//
// async fn run_chat_agent(config: LlmConfig) -> Result<(), LlmError> {
// let client = LlmClient::new(config)?;
// let agent = LlmChatAgentBuilder::new()
// .with_client(client)
// .system_prompt("You are a helpful assistant with an echo tool.")
// .build();
//
// let names = agent
// .tools()
// .iter()
// .map(|t| t.tool_name().to_string())
// .collect();
// let tool_hook = ToolValidationHook::new(names.clone());
//
// let output = agent
// .prompt(LlmMessage::user(
// "Use the echo tool to echo 'Hello from the JSON harness!', then tell me what it said.",
// ))
// .max_turns(5)
// .add_hook(tool_hook)
// .await
// .expect("agent should succeed");
//
// println!("--- Final Agent Response ---");
// println!("{}", output.text);
// println!("-----------------------------");
//
// Ok(())
// }
@@ -481,15 +481,15 @@ pub async fn confirm_editor_agent_tool_call(
.with_details(json!({ "message": "message not found" })));
}
let msg = &document.messages[message_id];
let msg = &mut document.messages[message_id];
// Validate role and tool_call
// A persisted executing state prevents a repeated confirmation from starting a second job.
if msg.role != EditorAgentMessageRole::System {
return Err(editor_agent_bad_request("message is not a system message"));
}
let tc = msg
.tool_call
.as_ref()
.as_mut()
.ok_or_else(|| editor_agent_bad_request("message has no tool call"))?;
if tc.status != EditorAgentToolCallStatus::PendingConfirmation {
return Err(editor_agent_bad_request(
@@ -497,10 +497,13 @@ pub async fn confirm_editor_agent_tool_call(
));
}
let tool_name = tc.tool_name.clone();
let tool_args = tc.args.clone();
// TODO dont use match
match tc.tool_name.as_str() {
match tool_name.as_str() {
EditImageTool::NAME => {
let args: EditImageToolArgs = serde_json::from_value(tc.args.clone())
let args: EditImageToolArgs = serde_json::from_value(tool_args)
.map_err(|e| editor_agent_bad_request(format!("invalid tool call args: {e}")))?;
// Get project
@@ -533,6 +536,13 @@ pub async fn confirm_editor_agent_tool_call(
let edit_tool = EditImageTool {
context: tool_context,
};
// mark it quickly
if let Some(tool_call) = &mut document.messages[message_id].tool_call {
tool_call.status = EditorAgentToolCallStatus::Executing;
}
write_messages_document(&state, &conversation, &document).await?;
let result = edit_tool
.execute(
&state,
@@ -546,7 +556,21 @@ pub async fn confirm_editor_agent_tool_call(
None, // source_resource_id
Some(canvas_completion),
)
.await?;
.await;
let result = match result {
Ok(result) => result,
Err(error) => {
let msg = &mut document.messages[message_id];
msg.text = format!("[tool_call:{tool_name}] output: {error}");
if let Some(tool_call) = &mut msg.tool_call {
tool_call.status = EditorAgentToolCallStatus::Failed;
tool_call.error = Some(error.to_string());
}
write_messages_document(&state, &conversation, &document).await?;
return Err(error);
}
};
// Build generated images
let generated_images = vec![EditorAgentGeneratedImage {
@@ -579,7 +603,7 @@ pub async fn confirm_editor_agent_tool_call(
}
_ => Err(editor_agent_bad_request(format!(
"unsupported tool: {}",
tc.tool_name
tool_name
))),
}
}
@@ -3,7 +3,10 @@
import { act, renderHook, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { EditorAgentMessageResponse } from '../../../../packages/shared/src/contracts/editorAgent.ts';
import type {
EditorAgentMessage,
EditorAgentMessageResponse,
} from '../../../../packages/shared/src/contracts/editorAgent.ts';
import {
type EditorAgentConversationClient,
useEditorAgentConversation,
@@ -305,6 +308,74 @@ describe('useEditorAgentConversation', () => {
expect(onCanvasRefreshRequested).toHaveBeenCalledTimes(1);
});
it('marks a confirmation as executing before the request finishes', async () => {
const client = createClient();
const pendingMessage: EditorAgentMessage = {
id: 0,
role: 'system',
text: '需要生成一张图',
attachments: [],
toolCall: {
toolName: 'edit-image',
summary: '',
status: 'pending_confirmation',
args: { object_image_id: 'source-image', prompt: '换成像素风' },
images: [],
error: null,
},
createdAt: '2026-07-03T00:00:00.000Z',
};
const completedMessage: EditorAgentMessage = {
...pendingMessage,
toolCall: {
...pendingMessage.toolCall!,
status: 'completed',
images: [],
},
};
let resolveConfirmation: ((message: EditorAgentMessage) => void) | undefined;
vi.mocked(client.getConversation).mockResolvedValue({
conversationId: 'conversation-1',
projectId: 'project-1',
title: '角色参考',
messages: [pendingMessage],
createdAt: '2026-07-03T00:00:00.000Z',
updatedAt: '2026-07-03T00:00:00.000Z',
});
vi.mocked(client.confirmToolCall).mockImplementation(
() =>
new Promise<EditorAgentMessage>((resolve) => {
resolveConfirmation = resolve;
}),
);
const { result } = renderHook(() =>
useEditorAgentConversation({ projectId: 'project-1', client }),
);
await waitFor(() => {
expect(result.current.messages[0]?.toolCall?.status).toBe(
'pending_confirmation',
);
});
act(() => {
void result.current.confirmToolCall(0);
});
await waitFor(() => {
expect(result.current.messages[0]?.toolCall?.status).toBe('executing');
});
await result.current.confirmToolCall(0);
expect(client.confirmToolCall).toHaveBeenCalledTimes(1);
await act(async () => {
resolveConfirmation?.(completedMessage);
});
await waitFor(() => {
expect(result.current.messages[0]?.toolCall?.status).toBe('completed');
});
});
it('cancels a pending tool call and keeps the replacement in the same position', async () => {
const client = createClient();
const pendingMessage = {
@@ -409,6 +409,23 @@ export function useEditorAgentConversation({
setToolCallAction(nextAction);
setErrorMessage(null);
if (action === 'confirm') {
setMessages((currentMessages) =>
currentMessages.map((message) =>
message.id === messageId &&
message.toolCall?.status === 'pending_confirmation'
? {
...message,
toolCall: {
...message.toolCall,
status: 'executing',
},
}
: message,
),
);
}
try {
const updatedMessage = await (action === 'confirm'
? client.confirmToolCall(conversationId, messageId)
@@ -436,6 +453,8 @@ export function useEditorAgentConversation({
? '确认画布 Agent 操作失败'
: '取消画布 Agent 操作失败',
);
// The server may have accepted the action even when its response was lost.
void loadConversation(conversationId).catch(() => undefined);
}
return null;
} finally {
@@ -445,7 +464,7 @@ export function useEditorAgentConversation({
}
}
},
[client, requestCanvasRefreshForMessages],
[client, loadConversation, requestCanvasRefreshForMessages],
);
const confirmToolCall = useCallback(