Add new Agents (#14883)

Still experimenting

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Félix Malfait
2025-10-04 06:45:13 +02:00
committed by GitHub
parent d3a7241b6f
commit 888bf45422
14 changed files with 372 additions and 77 deletions
@@ -29,6 +29,7 @@ import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modu
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { AgentExecutionContext } from './agent-handoff-executor.service';
import { AgentModelConfigService } from './agent-model-config.service';
import { AgentToolGeneratorService } from './agent-tool-generator.service';
import { AgentEntity } from './agent.entity';
@@ -40,7 +41,7 @@ export interface AgentExecutionResult {
}
@Injectable()
export class AgentExecutionService {
export class AgentExecutionService implements AgentExecutionContext {
private readonly logger = new Logger(AgentExecutionService.name);
constructor(
@@ -61,10 +62,12 @@ export class AgentExecutionService {
messages,
system,
agent,
excludeHandoffTools = false,
}: {
system: string;
agent: AgentEntity | null;
messages: UIMessage<unknown, UIDataTypes, UITools>[];
excludeHandoffTools?: boolean;
}) {
try {
if (agent) {
@@ -86,11 +89,17 @@ export class AgentExecutionService {
agent.workspaceId,
);
const handoffTools =
await this.agentHandoffToolService.generateHandoffTools(
agent.id,
agent.workspaceId,
);
let handoffTools = {};
if (!excludeHandoffTools) {
handoffTools =
await this.agentHandoffToolService.generateHandoffTools(
agent.id,
agent.workspaceId,
this, // Pass execution context
);
}
const nativeModelTools =
this.agentModelConfigService.getNativeModelTools(
registeredModel,
@@ -1,13 +1,21 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ModelMessage, generateText } from 'ai';
import { ProviderOptions } from '@ai-sdk/provider-utils';
import {
generateText,
LanguageModel,
ModelMessage,
StopCondition,
streamText,
ToolSet,
UIDataTypes,
UIMessage,
UITools,
} 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';
@@ -15,9 +23,26 @@ export type HandoffRequest = {
fromAgentId: string;
toAgentId: string;
workspaceId: string;
messages: ModelMessage[];
messages: UIMessage<unknown, UIDataTypes, UITools>[];
isStreaming?: boolean;
};
export interface AgentExecutionContext {
prepareAIRequestConfig: (params: {
system: string;
agent: AgentEntity | null;
messages: UIMessage<unknown, UIDataTypes, UITools>[];
excludeHandoffTools?: boolean; // Prevent infinite recursion
}) => Promise<{
system: string;
tools: ToolSet;
model: LanguageModel;
messages: ModelMessage[];
stopWhen?: StopCondition<ToolSet>;
providerOptions?: ProviderOptions;
}>;
}
@Injectable()
export class AgentHandoffExecutorService {
private readonly logger = new Logger(AgentHandoffExecutorService.name);
@@ -26,13 +51,20 @@ export class AgentHandoffExecutorService {
@InjectRepository(AgentEntity)
private readonly agentRepository: Repository<AgentEntity>,
private readonly agentHandoffService: AgentHandoffService,
private readonly aiModelRegistryService: AiModelRegistryService,
private readonly agentToolGeneratorService: AgentToolGeneratorService,
) {}
async executeHandoff(handoffRequest: HandoffRequest) {
async executeHandoff(
handoffRequest: HandoffRequest,
executionContext: AgentExecutionContext,
) {
try {
const { fromAgentId, toAgentId, workspaceId, messages } = handoffRequest;
const {
fromAgentId,
toAgentId,
workspaceId,
messages,
isStreaming = false,
} = handoffRequest;
const canHandoff = await this.agentHandoffService.canHandoffTo({
fromAgentId,
@@ -58,51 +90,53 @@ export class AgentHandoffExecutorService {
);
}
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 = {
// Prepare AI request config using the execution context
const aiRequestConfig = await executionContext.prepareAIRequestConfig({
system: targetAgent.prompt,
agent: targetAgent,
messages,
tools,
model: registeredModel.model,
};
excludeHandoffTools: true, // Prevent infinite recursion
});
const textResponse = await generateText(aiRequestConfig);
if (isStreaming) {
// Return stream for streaming contexts
const stream = streamText(aiRequestConfig);
this.logger.log(
`Successfully executed handoff to agent ${toAgentId} with response length: ${textResponse.text.length}`,
);
this.logger.log(`Started streaming handoff to agent ${toAgentId}`);
return {
success: true,
message: `Successfully executed handoff to agent ${targetAgent.name}`,
result: {
response: textResponse.text,
targetAgentName: targetAgent.name,
},
};
return stream;
} else {
// Use generateText for non-streaming contexts (workflows)
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,
);
const { isStreaming = false, toAgentId } = handoffRequest;
if (isStreaming) {
throw error; // Let streaming context handle the error
}
return {
success: false,
message: `Failed to execute handoff to agent ${handoffRequest.toAgentId}`,
message: `Failed to execute handoff to agent ${toAgentId}`,
error: error.message,
};
}
@@ -2,7 +2,11 @@ import { Injectable } from '@nestjs/common';
import { type ToolSet } from 'ai';
import { AgentHandoffExecutorService } from 'src/engine/metadata-modules/agent/agent-handoff-executor.service';
import {
AgentExecutionContext,
AgentHandoffExecutorService,
HandoffRequest,
} from 'src/engine/metadata-modules/agent/agent-handoff-executor.service';
import { AgentHandoffService } from 'src/engine/metadata-modules/agent/agent-handoff.service';
import { AGENT_HANDOFF_DESCRIPTION_TEMPLATE } from 'src/engine/metadata-modules/agent/constants/agent-handoff-description.const';
import { AGENT_HANDOFF_SCHEMA } from 'src/engine/metadata-modules/agent/constants/agent-handoff-schema.const';
@@ -18,6 +22,7 @@ export class AgentHandoffToolService {
public async generateHandoffTools(
agentId: string,
workspaceId: string,
executionContext: AgentExecutionContext,
): Promise<ToolSet> {
const handoffs = await this.agentHandoffService.getAgentHandoffs({
fromAgentId: agentId,
@@ -37,14 +42,18 @@ export class AgentHandoffToolService {
),
inputSchema: AGENT_HANDOFF_SCHEMA,
execute: async ({ input }) => {
const result = await this.agentHandoffExecutorService.executeHandoff({
const handoffRequest: HandoffRequest = {
fromAgentId: agentId,
toAgentId: handoff.toAgent.id,
workspaceId,
messages: input.messages,
});
isStreaming: true, // Tools are executed during streaming
};
return result;
return this.agentHandoffExecutorService.executeHandoff(
handoffRequest,
executionContext,
);
},
};
@@ -213,6 +213,9 @@ export class RoleService {
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
canBeAssignedToUsers: true,
canBeAssignedToAgents: false,
canBeAssignedToApiKeys: false,
isEditable: true,
workspaceId,
});