Improve AI agent chat, tool display, and workflow agent management (#17876)
## Summary - **Fix token renewal endpoint**: Use `/metadata` instead of `/graphql` for token renewal in agent chat, fixing auth issues - **Improve tool display**: Add `load_skills` support, show formatted tool names (underscores → spaces) with finish/loading states, display tool icons during loading, and support custom loading messages from tool input - **Refactor workflow agent management**: Replace direct `AgentRepository` access with `AgentService` for create/delete/find operations in workflow steps, improving encapsulation and consistency - **Simplify Apollo client usage**: Remove explicit Apollo client override in `useGetToolIndex`, add `AgentChatProvider` to `AppRouterProviders` - **Fix load-skill tool**: Change parameter type from `string` to `json` for proper schema parsing - **Update agent-chat-streaming**: Use `AgentService` for agent resolution and tool registration instead of direct repository queries ## Test plan - [ ] Verify AI agent chat works end-to-end (send message, receive response) - [ ] Verify tool steps display correctly with icons and proper messages during loading and after completion - [ ] Verify workflow AI agent step creation and deletion works correctly - [ ] Verify workflow version cloning preserves agent configuration - [ ] Verify token renewal works when tokens expire during agent chat Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
+48
-59
@@ -1,4 +1,4 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { createUIMessageStream, pipeUIMessageStreamToResponse } from 'ai';
|
||||
@@ -34,8 +34,6 @@ export type StreamAgentChatOptions = {
|
||||
|
||||
@Injectable()
|
||||
export class AgentChatStreamingService {
|
||||
private readonly logger = new Logger(AgentChatStreamingService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(AgentChatThreadEntity)
|
||||
private readonly threadRepository: Repository<AgentChatThreadEntity>,
|
||||
@@ -65,6 +63,25 @@ export class AgentChatStreamingService {
|
||||
);
|
||||
}
|
||||
|
||||
// Fire user-message save without awaiting to avoid delaying time-to-first-letter.
|
||||
// The promise is awaited inside onFinish where we need the turnId.
|
||||
const lastUserText =
|
||||
messages[messages.length - 1]?.parts.find((part) => part.type === 'text')
|
||||
?.text ?? '';
|
||||
|
||||
const userMessagePromise = this.agentChatService.addMessage({
|
||||
threadId: thread.id,
|
||||
uiMessage: {
|
||||
role: AgentMessageRole.USER,
|
||||
parts: [{ type: 'text', text: lastUserText }],
|
||||
},
|
||||
});
|
||||
|
||||
// Prevent unhandled rejection if onFinish never runs (e.g. stream
|
||||
// setup error or empty response early-return). The real error still
|
||||
// surfaces when awaited in onFinish.
|
||||
userMessagePromise.catch(() => {});
|
||||
|
||||
try {
|
||||
const uiStream = createUIMessageStream<ExtendedUIMessage>({
|
||||
execute: async ({ writer }) => {
|
||||
@@ -97,8 +114,6 @@ export class AgentChatStreamingService {
|
||||
writer.merge(
|
||||
stream.toUIMessageStream({
|
||||
onError: (error) => {
|
||||
this.logger.error('Stream error:', error);
|
||||
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
},
|
||||
sendStart: false,
|
||||
@@ -179,57 +194,26 @@ export class AgentChatStreamingService {
|
||||
return;
|
||||
}
|
||||
|
||||
const validThreadId = thread.id;
|
||||
const userMessage = await userMessagePromise;
|
||||
|
||||
if (!validThreadId) {
|
||||
this.logger.error('Thread ID is unexpectedly null/undefined');
|
||||
await this.agentChatService.addMessage({
|
||||
threadId: thread.id,
|
||||
uiMessage: responseMessage,
|
||||
turnId: userMessage.turnId,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const userMessage = await this.agentChatService.addMessage({
|
||||
threadId: validThreadId,
|
||||
uiMessage: {
|
||||
role: AgentMessageRole.USER,
|
||||
parts: [
|
||||
{
|
||||
type: 'text',
|
||||
text:
|
||||
messages[messages.length - 1].parts.find(
|
||||
(part) => part.type === 'text',
|
||||
)?.text ?? '',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await this.agentChatService.addMessage({
|
||||
threadId: validThreadId,
|
||||
uiMessage: responseMessage,
|
||||
turnId: userMessage.turnId,
|
||||
});
|
||||
|
||||
await this.threadRepository.update(validThreadId, {
|
||||
totalInputTokens: () =>
|
||||
`"totalInputTokens" + ${streamUsage.inputTokens}`,
|
||||
totalOutputTokens: () =>
|
||||
`"totalOutputTokens" + ${streamUsage.outputTokens}`,
|
||||
totalInputCredits: () =>
|
||||
`"totalInputCredits" + ${streamUsage.inputCredits}`,
|
||||
totalOutputCredits: () =>
|
||||
`"totalOutputCredits" + ${streamUsage.outputCredits}`,
|
||||
contextWindowTokens: modelConfig.contextWindowTokens,
|
||||
conversationSize: lastStepConversationSize,
|
||||
});
|
||||
} catch (saveError) {
|
||||
this.logger.error(
|
||||
'Failed to save messages:',
|
||||
saveError instanceof Error
|
||||
? saveError.message
|
||||
: String(saveError),
|
||||
);
|
||||
}
|
||||
await this.threadRepository.update(thread.id, {
|
||||
totalInputTokens: () =>
|
||||
`"totalInputTokens" + ${streamUsage.inputTokens}`,
|
||||
totalOutputTokens: () =>
|
||||
`"totalOutputTokens" + ${streamUsage.outputTokens}`,
|
||||
totalInputCredits: () =>
|
||||
`"totalInputCredits" + ${streamUsage.inputCredits}`,
|
||||
totalOutputCredits: () =>
|
||||
`"totalOutputCredits" + ${streamUsage.outputCredits}`,
|
||||
contextWindowTokens: modelConfig.contextWindowTokens,
|
||||
conversationSize: lastStepConversationSize,
|
||||
});
|
||||
},
|
||||
sendReasoning: true,
|
||||
}),
|
||||
@@ -237,13 +221,18 @@ export class AgentChatStreamingService {
|
||||
},
|
||||
});
|
||||
|
||||
pipeUIMessageStreamToResponse({ stream: uiStream, response });
|
||||
pipeUIMessageStreamToResponse({
|
||||
stream: uiStream,
|
||||
response,
|
||||
// Consume the stream independently so onFinish fires even if
|
||||
// the client disconnects (e.g., page refresh mid-stream)
|
||||
consumeSseStream: ({ stream }) => {
|
||||
stream.pipeTo(new WritableStream()).catch(() => {});
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
'Failed to stream chat:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
response.end();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { COMMON_PRELOAD_TOOLS } from 'src/engine/core-modules/tool-provider/constants/common-preload-tools.const';
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
|
||||
import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import {
|
||||
EXECUTE_TOOL_TOOL_NAME,
|
||||
LEARN_TOOLS_TOOL_NAME,
|
||||
@@ -131,7 +131,7 @@ export class SystemPromptBuilderService {
|
||||
}
|
||||
|
||||
buildFullPrompt(
|
||||
toolCatalog: ToolDescriptor[],
|
||||
toolCatalog: ToolIndexEntry[],
|
||||
skillCatalog: FlatSkill[],
|
||||
preloadedTools: string[],
|
||||
contextString?: string,
|
||||
@@ -242,12 +242,12 @@ ${skillsList}`;
|
||||
}
|
||||
|
||||
buildToolCatalogSection(
|
||||
toolCatalog: ToolDescriptor[],
|
||||
toolCatalog: ToolIndexEntry[],
|
||||
preloadedTools: string[],
|
||||
): string {
|
||||
const preloadedSet = new Set(preloadedTools);
|
||||
|
||||
const toolsByCategory = new Map<string, ToolDescriptor[]>();
|
||||
const toolsByCategory = new Map<string, ToolIndexEntry[]>();
|
||||
|
||||
for (const tool of toolCatalog) {
|
||||
const category = tool.category;
|
||||
|
||||
Reference in New Issue
Block a user