import { getExistingManifestContext, type ExistingManifestContextOptions } from './existing-manifest-context'; import type { ManifestComponentDeletion, ManifestScope } from './manifest-builder'; import { requireArrayComponentField, requireBooleanComponentField, requireStringComponentField, } from './_existing-component-fields'; import type { BusinessEventKey } from './business-event'; /** Composer supports only runner-identity ACP agents. */ export type ACPAgentIdentityMode = 'runner'; /** A source file in an ACP agent source revision. */ export interface ACPAgentSourceFile { path: string; content: string; kind: string; } /** An event that starts an ACP agent session. */ export interface ACPAgentEventTrigger { /** Stable identifier for this trigger within the agent. */ key: string; /** Business event to subscribe to. Availability is validated during installation. */ eventKey: BusinessEventKey; /** Optional RQL condition evaluated against the event object. @default '' */ rqlFormula?: string; /** Prompt sent to the agent when the event matches. */ promptTemplate: string; /** Whether this trigger receives events. @default true */ enabled?: boolean; } export type ACPAgentEventKey = BusinessEventKey; /** Initialization properties for {@link ACPAgent}. */ export interface ACPAgentProps { /** Company-scoped API name ending in `__c`. */ apiName: string; /** Display name shown in Rippling. */ name: string; /** Human-readable description. */ description: string; /** Identity used when the agent runs. ACP Composer supports only `runner`. */ identityMode: ACPAgentIdentityMode; /** Trigger configuration managed by this manifest. Pass [] for no triggers. */ eventTriggers: ACPAgentEventTrigger[]; /** Source files for the agent's latest revision. */ files: ACPAgentSourceFile[]; /** Path of the source file that starts the agent. */ entrypoint: string; } /** * Defines an Agent Control Plane agent for Composer. * * New agents use their `apiName` as the Composer source handle, so a * source-executed manifest does not need to persist a separate identifier. */ export class ACPAgent { static readonly componentType = 'ACP_AGENT' as const; static toDeletionIdentifier(sourceAgentId: string): ManifestComponentDeletion { return { type: ACPAgent.componentType, source_agent_id: sourceAgentId, }; } private readonly _sourceAgentId: string; private readonly _apiName: string; private _name: string; private _description: string; private _eventTriggers: ACPAgentEventTrigger[]; private _files: ACPAgentSourceFile[]; private _entrypoint: string; constructor(scope: ManifestScope, props: ACPAgentProps, sourceAgentId = props.apiName) { if (!props.apiName.endsWith('__c')) { throw new Error('Company ACP agent apiName must end with __c.'); } if (!/^[a-z][a-z0-9_]{1,80}(?:__c)?$/.test(props.apiName)) { throw new Error('ACP agent apiName must be a lowercase system name starting with a letter.'); } if (props.identityMode !== 'runner') { throw new Error('ACP agents support only runner identity.'); } if ((props as unknown as Record)['skills'] !== undefined) { throw new Error('ACP Agent skill references are not supported by this SDK release.'); } this._sourceAgentId = sourceAgentId; this._apiName = props.apiName; this._name = props.name; this._description = props.description; this._eventTriggers = [...props.eventTriggers]; this._files = [...props.files]; this._entrypoint = props.entrypoint; scope._register(this); } /** Loads this ACP agent from the active existing-manifest JSON context. */ static loadFromExisting(apiName: string, options: ExistingManifestContextOptions = {}): ACPAgent { return getExistingManifestContext(options).loadACPAgent(apiName); } /** @internal Hydrates an ACP agent from existing manifest wire JSON. */ static _fromExistingComponent(scope: ManifestScope, component: Record): ACPAgent { if (component['skill_ids'] !== undefined) { throw new Error('ACP Agent skill references are not supported by this SDK release.'); } const identityMode = requireStringComponentField( component, 'identity_mode', ACPAgent.componentType, ) as ACPAgentIdentityMode; if (identityMode !== 'runner') { throw new Error('ACP agents support only runner identity.'); } const props: ACPAgentProps = { apiName: requireStringComponentField(component, 'api_name', ACPAgent.componentType), name: requireStringComponentField(component, 'name', ACPAgent.componentType), description: requireStringComponentField(component, 'description', ACPAgent.componentType, { allowEmpty: true, }), identityMode, files: requireArrayComponentField>(component, 'files', ACPAgent.componentType).map( (file) => ({ path: requireStringComponentField(file, 'path', ACPAgent.componentType), content: requireStringComponentField(file, 'content', ACPAgent.componentType), kind: requireStringComponentField(file, 'kind', ACPAgent.componentType), }), ), entrypoint: requireStringComponentField(component, 'entrypoint', ACPAgent.componentType), eventTriggers: requireArrayComponentField>( component, 'event_triggers', ACPAgent.componentType, ).map((trigger) => ({ key: requireStringComponentField(trigger, 'key', ACPAgent.componentType), eventKey: requireStringComponentField( trigger, 'event_key', ACPAgent.componentType, ) as BusinessEventKey, rqlFormula: requireStringComponentField(trigger, 'rql_formula', ACPAgent.componentType, { allowEmpty: true, }), promptTemplate: requireStringComponentField(trigger, 'prompt_template', ACPAgent.componentType), enabled: requireBooleanComponentField(trigger, 'enabled', ACPAgent.componentType), })), }; return new ACPAgent( scope, props, requireStringComponentField(component, 'source_agent_id', ACPAgent.componentType), ); } getSourceAgentId(): string { return this._sourceAgentId; } getApiName(): string { return this._apiName; } setName(name: string): this { this._name = name; return this; } setDescription(description: string): this { this._description = description; return this; } setIdentity(identityMode: ACPAgentIdentityMode): this { if (identityMode !== 'runner') { throw new Error('ACP agents support only runner identity.'); } return this; } setEventTriggers(eventTriggers: ACPAgentEventTrigger[]): this { this._eventTriggers = [...eventTriggers]; return this; } setSource(files: ACPAgentSourceFile[], entrypoint: string): this { this._files = [...files]; this._entrypoint = entrypoint; return this; } toDict(): Record { const component: Record = { type: ACPAgent.componentType, source_agent_id: this._sourceAgentId, api_name: this._apiName, name: this._name, description: this._description, identity_mode: 'runner', files: this._files.map((file) => ({ type: ACPAgent.componentType, path: file.path, content: file.content, kind: file.kind, })), entrypoint: this._entrypoint, event_triggers: this._eventTriggers.map((trigger) => ({ type: ACPAgent.componentType, key: trigger.key, event_key: trigger.eventKey, rql_formula: trigger.rqlFormula ?? '', prompt_template: trigger.promptTemplate, enabled: trigger.enabled ?? true, })), }; return component; } }