feat(sdk): add runAgent() to run app agents from logic functions (#21157)
<img width="948" height="593" alt="image" src="https://github.com/user-attachments/assets/d990fa98-3cfd-469d-ab7f-0b2d4ccf3afc" /> <img width="1361" height="802" alt="image" src="https://github.com/user-attachments/assets/1091f598-49f3-4c16-92ea-1e1c200181e2" /> ## Add `runAgent()` to the Logic Function SDK Lets an app's logic function run one of its own AI agents server-side and get the result back synchronously — reusing the existing agent executor instead of a new bespoke transport. ### Backend - New **`runAgent` GraphQL mutation** (metadata schema) in `ai-agent-execution`, wrapping the existing `AgentAsyncExecutorService.executeAgent`. Scopes the agent lookup to the calling application and runs it under an application auth context. - New `@AuthApplication()` param decorator (mirrors `@AuthWorkspace()`) — first GraphQL resolver authenticated by an **application access token**. - Guarded by `WorkspaceAuthGuard` + `SettingsPermissionGuard(PermissionFlagType.AI)`: the app's role must grant the `AI` permission flag. ### SDK - `runAgent({ agentUniversalIdentifier, prompt })` posts the mutation to `/metadata` with the app token via a new runtime GraphQL transport. Returns `{ result, hasNoMoreAvailableCredits }`. - Refactored the connections helpers onto a shared `postAppEndpoint` util (removes duplicated transport logic). ### Frontend - App install permission modal now shows an explicit consent line — _"Run AI agents and bill AI credits to your workspace"_ — when the app's role requests the `AI` flag. ### Docs - Documented `runAgent` and its `AI` permission-flag requirement in _Skills & Agents_. - Fixed outdated role-permission examples in _Roles & Permissions_ (`permissionFlags` → `permissionFlagUniversalIdentifiers`, `PermissionFlag` → `SystemPermissionFlag`). ### Test plan - [x] SDK unit tests (`run-agent.spec.ts`) — request shape, GraphQL/HTTP error handling, missing env vars - [x] `twenty-server`, `twenty-front`, `twenty-shared` typecheck + lint - [ ] Manual: install an app granting the `AI` flag, call `runAgent()` from a logic function, confirm the agent runs and credits are billed --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -2132,6 +2132,18 @@ type FieldConnection {
|
||||
edges: [FieldEdge!]!
|
||||
}
|
||||
|
||||
type AppConnection {
|
||||
id: ID!
|
||||
providerName: String!
|
||||
name: String!
|
||||
handle: String!
|
||||
visibility: String!
|
||||
userWorkspaceId: String!
|
||||
accessToken: String!
|
||||
scopes: [String!]!
|
||||
authFailedAt: String
|
||||
}
|
||||
|
||||
type ResendEmailVerificationToken {
|
||||
success: Boolean!
|
||||
}
|
||||
@@ -2529,6 +2541,12 @@ type AgentMessagePart {
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type RunAgentResult {
|
||||
result: JSON
|
||||
error: String
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type ChannelSyncSuccess {
|
||||
success: Boolean!
|
||||
}
|
||||
@@ -3011,6 +3029,8 @@ type Query {
|
||||
myMessageChannels(connectedAccountId: UUID): [MessageChannel!]!
|
||||
myCalendarChannels(connectedAccountId: UUID): [CalendarChannel!]!
|
||||
minimalMetadata: MinimalMetadata!
|
||||
appConnections(filter: ListAppConnectionsInput): [AppConnection!]!
|
||||
appConnection(id: ID!): AppConnection!
|
||||
findWorkspaceAiStats: WorkspaceAiStats!
|
||||
chatThreads: [AgentChatThread!]!
|
||||
chatThread(id: UUID!): AgentChatThread!
|
||||
@@ -3065,6 +3085,12 @@ input AgentIdInput {
|
||||
id: UUID!
|
||||
}
|
||||
|
||||
input ListAppConnectionsInput {
|
||||
providerName: String
|
||||
userWorkspaceId: String
|
||||
visibility: String
|
||||
}
|
||||
|
||||
input EventLogQueryInput {
|
||||
table: EventLogTable!
|
||||
filters: EventLogFiltersInput
|
||||
@@ -3238,6 +3264,7 @@ type Mutation {
|
||||
upsertRowLevelPermissionPredicates(input: UpsertRowLevelPermissionPredicatesInput!): UpsertRowLevelPermissionPredicatesResult!
|
||||
assignRoleToAgent(agentId: UUID!, roleId: UUID!): Boolean!
|
||||
removeRoleFromAgent(agentId: UUID!): Boolean!
|
||||
runAgent(input: RunAgentInput!): RunAgentResult!
|
||||
createWebhook(input: CreateWebhookInput!): Webhook!
|
||||
updateWebhook(input: UpdateWebhookInput!): Webhook!
|
||||
deleteWebhook(id: UUID!): Webhook!
|
||||
@@ -4093,6 +4120,11 @@ input RowLevelPermissionPredicateGroupInput {
|
||||
positionInRowLevelPermissionPredicateGroup: Float
|
||||
}
|
||||
|
||||
input RunAgentInput {
|
||||
agentUniversalIdentifier: String!
|
||||
prompt: String!
|
||||
}
|
||||
|
||||
input CreateWebhookInput {
|
||||
id: UUID
|
||||
targetUrl: String!
|
||||
|
||||
@@ -1762,6 +1762,19 @@ export interface FieldConnection {
|
||||
__typename: 'FieldConnection'
|
||||
}
|
||||
|
||||
export interface AppConnection {
|
||||
id: Scalars['ID']
|
||||
providerName: Scalars['String']
|
||||
name: Scalars['String']
|
||||
handle: Scalars['String']
|
||||
visibility: Scalars['String']
|
||||
userWorkspaceId: Scalars['String']
|
||||
accessToken: Scalars['String']
|
||||
scopes: Scalars['String'][]
|
||||
authFailedAt?: Scalars['String']
|
||||
__typename: 'AppConnection'
|
||||
}
|
||||
|
||||
export interface ResendEmailVerificationToken {
|
||||
success: Scalars['Boolean']
|
||||
__typename: 'ResendEmailVerificationToken'
|
||||
@@ -2211,6 +2224,13 @@ export interface AgentMessagePart {
|
||||
__typename: 'AgentMessagePart'
|
||||
}
|
||||
|
||||
export interface RunAgentResult {
|
||||
result?: Scalars['JSON']
|
||||
error?: Scalars['String']
|
||||
success: Scalars['Boolean']
|
||||
__typename: 'RunAgentResult'
|
||||
}
|
||||
|
||||
export interface ChannelSyncSuccess {
|
||||
success: Scalars['Boolean']
|
||||
__typename: 'ChannelSyncSuccess'
|
||||
@@ -2607,6 +2627,8 @@ export interface Query {
|
||||
myMessageChannels: MessageChannel[]
|
||||
myCalendarChannels: CalendarChannel[]
|
||||
minimalMetadata: MinimalMetadata
|
||||
appConnections: AppConnection[]
|
||||
appConnection: AppConnection
|
||||
findWorkspaceAiStats: WorkspaceAiStats
|
||||
chatThreads: AgentChatThread[]
|
||||
chatThread: AgentChatThread
|
||||
@@ -2767,6 +2789,7 @@ export interface Mutation {
|
||||
upsertRowLevelPermissionPredicates: UpsertRowLevelPermissionPredicatesResult
|
||||
assignRoleToAgent: Scalars['Boolean']
|
||||
removeRoleFromAgent: Scalars['Boolean']
|
||||
runAgent: RunAgentResult
|
||||
createWebhook: Webhook
|
||||
updateWebhook: Webhook
|
||||
deleteWebhook: Webhook
|
||||
@@ -4722,6 +4745,20 @@ export interface FieldConnectionGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface AppConnectionGenqlSelection{
|
||||
id?: boolean | number
|
||||
providerName?: boolean | number
|
||||
name?: boolean | number
|
||||
handle?: boolean | number
|
||||
visibility?: boolean | number
|
||||
userWorkspaceId?: boolean | number
|
||||
accessToken?: boolean | number
|
||||
scopes?: boolean | number
|
||||
authFailedAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ResendEmailVerificationTokenGenqlSelection{
|
||||
success?: boolean | number
|
||||
__typename?: boolean | number
|
||||
@@ -5226,6 +5263,14 @@ export interface AgentMessagePartGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface RunAgentResultGenqlSelection{
|
||||
result?: boolean | number
|
||||
error?: boolean | number
|
||||
success?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ChannelSyncSuccessGenqlSelection{
|
||||
success?: boolean | number
|
||||
__typename?: boolean | number
|
||||
@@ -5646,6 +5691,8 @@ export interface QueryGenqlSelection{
|
||||
myMessageChannels?: (MessageChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
|
||||
myCalendarChannels?: (CalendarChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
|
||||
minimalMetadata?: MinimalMetadataGenqlSelection
|
||||
appConnections?: (AppConnectionGenqlSelection & { __args?: {filter?: (ListAppConnectionsInput | null)} })
|
||||
appConnection?: (AppConnectionGenqlSelection & { __args: {id: Scalars['ID']} })
|
||||
findWorkspaceAiStats?: WorkspaceAiStatsGenqlSelection
|
||||
chatThreads?: AgentChatThreadGenqlSelection
|
||||
chatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
@@ -5698,6 +5745,8 @@ export interface AgentIdInput {
|
||||
/** The id of the agent. */
|
||||
id: Scalars['UUID']}
|
||||
|
||||
export interface ListAppConnectionsInput {providerName?: (Scalars['String'] | null),userWorkspaceId?: (Scalars['String'] | null),visibility?: (Scalars['String'] | null)}
|
||||
|
||||
export interface EventLogQueryInput {table: EventLogTable,filters?: (EventLogFiltersInput | null),first?: (Scalars['Int'] | null),after?: (Scalars['String'] | null)}
|
||||
|
||||
export interface EventLogFiltersInput {eventType?: (Scalars['String'] | null),userWorkspaceId?: (Scalars['String'] | null),dateRange?: (EventLogDateRangeInput | null),recordId?: (Scalars['String'] | null),objectMetadataId?: (Scalars['String'] | null)}
|
||||
@@ -5827,6 +5876,7 @@ export interface MutationGenqlSelection{
|
||||
upsertRowLevelPermissionPredicates?: (UpsertRowLevelPermissionPredicatesResultGenqlSelection & { __args: {input: UpsertRowLevelPermissionPredicatesInput} })
|
||||
assignRoleToAgent?: { __args: {agentId: Scalars['UUID'], roleId: Scalars['UUID']} }
|
||||
removeRoleFromAgent?: { __args: {agentId: Scalars['UUID']} }
|
||||
runAgent?: (RunAgentResultGenqlSelection & { __args: {input: RunAgentInput} })
|
||||
createWebhook?: (WebhookGenqlSelection & { __args: {input: CreateWebhookInput} })
|
||||
updateWebhook?: (WebhookGenqlSelection & { __args: {input: UpdateWebhookInput} })
|
||||
deleteWebhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
@@ -6184,6 +6234,8 @@ export interface RowLevelPermissionPredicateInput {id?: (Scalars['UUID'] | null)
|
||||
|
||||
export interface RowLevelPermissionPredicateGroupInput {id?: (Scalars['UUID'] | null),objectMetadataId: Scalars['UUID'],parentRowLevelPermissionPredicateGroupId?: (Scalars['UUID'] | null),logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator,positionInRowLevelPermissionPredicateGroup?: (Scalars['Float'] | null)}
|
||||
|
||||
export interface RunAgentInput {agentUniversalIdentifier: Scalars['String'],prompt: Scalars['String']}
|
||||
|
||||
export interface CreateWebhookInput {id?: (Scalars['UUID'] | null),targetUrl: Scalars['String'],operations: Scalars['String'][],description?: (Scalars['String'] | null),secret?: (Scalars['String'] | null)}
|
||||
|
||||
export interface UpdateWebhookInput {
|
||||
@@ -7535,6 +7587,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const AppConnection_possibleTypes: string[] = ['AppConnection']
|
||||
export const isAppConnection = (obj?: { __typename?: any } | null): obj is AppConnection => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isAppConnection"')
|
||||
return AppConnection_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ResendEmailVerificationToken_possibleTypes: string[] = ['ResendEmailVerificationToken']
|
||||
export const isResendEmailVerificationToken = (obj?: { __typename?: any } | null): obj is ResendEmailVerificationToken => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isResendEmailVerificationToken"')
|
||||
@@ -7991,6 +8051,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const RunAgentResult_possibleTypes: string[] = ['RunAgentResult']
|
||||
export const isRunAgentResult = (obj?: { __typename?: any } | null): obj is RunAgentResult => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isRunAgentResult"')
|
||||
return RunAgentResult_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ChannelSyncSuccess_possibleTypes: string[] = ['ChannelSyncSuccess']
|
||||
export const isChannelSyncSuccess = (obj?: { __typename?: any } | null): obj is ChannelSyncSuccess => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isChannelSyncSuccess"')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user