Files
twenty/packages/twenty-server/src/engine/metadata-modules/agent/agent-handoff-executor.service.ts
T
Abdul Rahman 8d9617131c Feat: Add reasoning summary and tool details display for AI responses (#14414)
Closes [#1405](https://github.com/twentyhq/core-team-issues/issues/1405)

### Implemented Features
- Reasoning Summary: Collapsible display of AI thinking process with
real-time streaming
- Tool Execution: Detailed tool call rendering with expandable results
and loading states
- Error Handling: Clean error stream event display with proper
formatting


### Demo



https://github.com/user-attachments/assets/76ad29b7-d831-4bc8-a115-ad25f7ace6e2

<img width="504" height="819" alt="Screenshot 2025-09-12 at 3 23 39 AM"
src="https://github.com/user-attachments/assets/82dc177d-f009-4e8a-8901-ee1a49201984"
/>

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
2025-09-17 11:30:24 +02:00

111 lines
3.2 KiB
TypeScript

import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { CoreMessage, generateText } from 'ai';
import { Repository } from 'typeorm';
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
import { AgentHandoffService } from './agent-handoff.service';
import { AgentToolGeneratorService } from './agent-tool-generator.service';
import { AgentEntity } from './agent.entity';
import { AgentException, AgentExceptionCode } from './agent.exception';
export type HandoffRequest = {
fromAgentId: string;
toAgentId: string;
workspaceId: string;
messages?: CoreMessage[];
};
@Injectable()
export class AgentHandoffExecutorService {
private readonly logger = new Logger(AgentHandoffExecutorService.name);
constructor(
@InjectRepository(AgentEntity)
private readonly agentRepository: Repository<AgentEntity>,
private readonly agentHandoffService: AgentHandoffService,
private readonly aiModelRegistryService: AiModelRegistryService,
private readonly agentToolGeneratorService: AgentToolGeneratorService,
) {}
async executeHandoff(handoffRequest: HandoffRequest) {
try {
const { fromAgentId, toAgentId, workspaceId, messages } = handoffRequest;
const canHandoff = await this.agentHandoffService.canHandoffTo({
fromAgentId,
toAgentId,
workspaceId,
});
if (!canHandoff) {
throw new AgentException(
`Agent ${fromAgentId} is not allowed to hand off to agent ${toAgentId}`,
AgentExceptionCode.AGENT_EXECUTION_FAILED,
);
}
const targetAgent = await this.agentRepository.findOne({
where: { id: toAgentId, workspaceId },
});
if (!targetAgent) {
throw new AgentException(
`Target agent ${toAgentId} not found`,
AgentExceptionCode.AGENT_NOT_FOUND,
);
}
const registeredModel =
await this.aiModelRegistryService.resolveModelForAgent(targetAgent);
if (!registeredModel) {
throw new AgentException(
`Model ${targetAgent.modelId} not found in registry`,
AgentExceptionCode.AGENT_EXECUTION_FAILED,
);
}
const tools = await this.agentToolGeneratorService.generateToolsForAgent(
toAgentId,
workspaceId,
);
const aiRequestConfig = {
system: targetAgent.prompt,
messages,
tools,
model: registeredModel.model,
};
const textResponse = await generateText(aiRequestConfig);
this.logger.log(
`Successfully executed handoff to agent ${toAgentId} with response length: ${textResponse.text.length}`,
);
return {
success: true,
message: `Successfully executed handoff to agent ${targetAgent.name}`,
result: {
response: textResponse.text,
targetAgentName: targetAgent.name,
},
};
} catch (error) {
this.logger.error(
`Handoff execution failed: ${error.message}`,
error.stack,
);
return {
success: false,
message: `Failed to execute handoff to agent ${handoffRequest.toAgentId}`,
error: error.message,
};
}
}
}