From 734770c31a3bb92eebbf4525f684861d86778769 Mon Sep 17 00:00:00 2001 From: Elliot Massen Date: Tue, 16 Dec 2025 13:56:19 +0000 Subject: [PATCH 01/10] feat: add agents and agent runs --- lib/adapters/REST/endpoints/agent-run.ts | 45 ++++++ lib/adapters/REST/endpoints/agent.ts | 68 ++++++++ lib/adapters/REST/endpoints/index.ts | 4 + lib/common-types.ts | 39 +++++ lib/create-environment-api.ts | 176 +++++++++++++++++++++ lib/entities/agent-run.ts | 66 ++++++++ lib/entities/agent.ts | 79 +++++++++ lib/entities/index.ts | 4 + lib/export-types.ts | 19 +++ lib/plain/common-types.ts | 4 + lib/plain/entities/agent-run.ts | 30 ++++ lib/plain/entities/agent.ts | 38 +++++ lib/plain/plain-client.ts | 9 ++ test/integration/agent-integration.test.ts | 75 +++++++++ test/unit/create-environment-api.test.ts | 85 ++++++++++ test/unit/entities/agent-run.test.ts | 29 ++++ test/unit/entities/agent.test.ts | 49 ++++++ test/unit/mocks/entities.ts | 89 +++++++++++ test/unit/plain/agent-run.test.ts | 125 +++++++++++++++ test/unit/plain/agent.test.ts | 100 ++++++++++++ 20 files changed, 1133 insertions(+) create mode 100644 lib/adapters/REST/endpoints/agent-run.ts create mode 100644 lib/adapters/REST/endpoints/agent.ts create mode 100644 lib/entities/agent-run.ts create mode 100644 lib/entities/agent.ts create mode 100644 lib/plain/entities/agent-run.ts create mode 100644 lib/plain/entities/agent.ts create mode 100644 test/integration/agent-integration.test.ts create mode 100644 test/unit/entities/agent-run.test.ts create mode 100644 test/unit/entities/agent.test.ts create mode 100644 test/unit/plain/agent-run.test.ts create mode 100644 test/unit/plain/agent.test.ts diff --git a/lib/adapters/REST/endpoints/agent-run.ts b/lib/adapters/REST/endpoints/agent-run.ts new file mode 100644 index 000000000..1adfcd087 --- /dev/null +++ b/lib/adapters/REST/endpoints/agent-run.ts @@ -0,0 +1,45 @@ +import type { RawAxiosRequestHeaders } from 'axios' +import type { AxiosInstance } from 'contentful-sdk-core' +import type { CollectionProp, GetSpaceEnvironmentParams } from '../../../common-types' +import type { AgentRunProps, AgentRunQueryOptions } from '../../../entities/agent-run' +import type { RestEndpoint } from '../types' +import * as raw from './raw' + +const AgentRunAlphaHeaders = { + 'x-contentful-enable-alpha-feature': 'agents', +} + +export const get: RestEndpoint<'AgentRun', 'get'> = ( + http: AxiosInstance, + params: GetSpaceEnvironmentParams & { runId: string }, + headers?: RawAxiosRequestHeaders, +) => { + return raw.get( + http, + `/spaces/${params.spaceId}/environments/${params.environmentId}/ai_agents/runs/${params.runId}`, + { + headers: { + ...AgentRunAlphaHeaders, + ...headers, + }, + }, + ) +} + +export const getMany: RestEndpoint<'AgentRun', 'getMany'> = ( + http: AxiosInstance, + params: GetSpaceEnvironmentParams & { query?: AgentRunQueryOptions }, + headers?: RawAxiosRequestHeaders, +) => { + return raw.get>( + http, + `/spaces/${params.spaceId}/environments/${params.environmentId}/ai_agents/runs`, + { + params: params.query, + headers: { + ...AgentRunAlphaHeaders, + ...headers, + }, + }, + ) +} diff --git a/lib/adapters/REST/endpoints/agent.ts b/lib/adapters/REST/endpoints/agent.ts new file mode 100644 index 000000000..fa5a2d97a --- /dev/null +++ b/lib/adapters/REST/endpoints/agent.ts @@ -0,0 +1,68 @@ +import type { RawAxiosRequestHeaders } from 'axios' +import type { AxiosInstance } from 'contentful-sdk-core' +import type { CollectionProp, GetSpaceEnvironmentParams, QueryParams } from '../../../common-types' +import type { + AgentGeneratePayload, + AgentGenerateResponse, + AgentProps, +} from '../../../entities/agent' +import type { RestEndpoint } from '../types' +import * as raw from './raw' + +const AgentAlphaHeaders = { + 'x-contentful-enable-alpha-feature': 'agents', +} + +export const get: RestEndpoint<'Agent', 'get'> = ( + http: AxiosInstance, + params: GetSpaceEnvironmentParams & { agentId: string }, + headers?: RawAxiosRequestHeaders, +) => { + return raw.get( + http, + `/spaces/${params.spaceId}/environments/${params.environmentId}/ai_agents/agents/${params.agentId}`, + { + headers: { + ...AgentAlphaHeaders, + ...headers, + }, + }, + ) +} + +export const getMany: RestEndpoint<'Agent', 'getMany'> = ( + http: AxiosInstance, + params: GetSpaceEnvironmentParams & QueryParams, + headers?: RawAxiosRequestHeaders, +) => { + return raw.get>( + http, + `/spaces/${params.spaceId}/environments/${params.environmentId}/ai_agents/agents`, + { + params: params.query, + headers: { + ...AgentAlphaHeaders, + ...headers, + }, + }, + ) +} + +export const generate: RestEndpoint<'Agent', 'generate'> = ( + http: AxiosInstance, + params: GetSpaceEnvironmentParams & { agentId: string }, + data: AgentGeneratePayload, + headers?: RawAxiosRequestHeaders, +) => { + return raw.post( + http, + `/spaces/${params.spaceId}/environments/${params.environmentId}/ai_agents/agents/${params.agentId}/generate`, + data, + { + headers: { + ...AgentAlphaHeaders, + ...headers, + }, + }, + ) +} diff --git a/lib/adapters/REST/endpoints/index.ts b/lib/adapters/REST/endpoints/index.ts index 21ef04c34..f6c18e194 100644 --- a/lib/adapters/REST/endpoints/index.ts +++ b/lib/adapters/REST/endpoints/index.ts @@ -1,5 +1,7 @@ import * as AiAction from './ai-action' import * as AiActionInvocation from './ai-action-invocation' +import * as Agent from './agent' +import * as AgentRun from './agent-run' import * as AccessToken from './access-token' import * as ApiKey from './api-key' import * as AppAccessToken from './app-access-token' @@ -75,6 +77,8 @@ import * as WorkflowsChangelog from './workflows-changelog' export default { AiAction, AiActionInvocation, + Agent, + AgentRun, ApiKey, AppAction, AppActionCall, diff --git a/lib/common-types.ts b/lib/common-types.ts index da615a938..70a764945 100644 --- a/lib/common-types.ts +++ b/lib/common-types.ts @@ -191,6 +191,8 @@ import type { AiActionInvocationProps, AiActionInvocationType, } from './entities/ai-action-invocation' +import type { AgentGeneratePayload, AgentGenerateResponse, AgentProps } from './entities/agent' +import type { AgentRunProps, AgentRunQueryOptions } from './entities/agent-run' import type { UpdateVectorizationStatusProps, VectorizationStatusProps, @@ -458,6 +460,13 @@ type MRInternal = { (opts: MROpts<'AiActionInvocation', 'get', UA>): MRReturn<'AiActionInvocation', 'get'> + (opts: MROpts<'Agent', 'get', UA>): MRReturn<'Agent', 'get'> + (opts: MROpts<'Agent', 'getMany', UA>): MRReturn<'Agent', 'getMany'> + (opts: MROpts<'Agent', 'generate', UA>): MRReturn<'Agent', 'generate'> + + (opts: MROpts<'AgentRun', 'get', UA>): MRReturn<'AgentRun', 'get'> + (opts: MROpts<'AgentRun', 'getMany', UA>): MRReturn<'AgentRun', 'getMany'> + (opts: MROpts<'AppAction', 'get', UA>): MRReturn<'AppAction', 'get'> (opts: MROpts<'AppAction', 'getMany', UA>): MRReturn<'AppAction', 'getMany'> (opts: MROpts<'AppAction', 'delete', UA>): MRReturn<'AppAction', 'delete'> @@ -1033,6 +1042,36 @@ export type MRActions = { return: AiActionInvocationProps } } + Agent: { + get: { + params: GetSpaceEnvironmentParams & { agentId: string } + headers?: RawAxiosRequestHeaders + return: AgentProps + } + getMany: { + params: GetSpaceEnvironmentParams & QueryParams + headers?: RawAxiosRequestHeaders + return: CollectionProp + } + generate: { + params: GetSpaceEnvironmentParams & { agentId: string } + payload: AgentGeneratePayload + headers?: RawAxiosRequestHeaders + return: AgentGenerateResponse + } + } + AgentRun: { + get: { + params: GetSpaceEnvironmentParams & { runId: string } + headers?: RawAxiosRequestHeaders + return: AgentRunProps + } + getMany: { + params: GetSpaceEnvironmentParams & { query?: AgentRunQueryOptions } + headers?: RawAxiosRequestHeaders + return: CollectionProp + } + } AppAction: { get: { params: GetAppActionParams; return: AppActionProps } getMany: { diff --git a/lib/create-environment-api.ts b/lib/create-environment-api.ts index 06b5b406a..5e4a6c4d5 100644 --- a/lib/create-environment-api.ts +++ b/lib/create-environment-api.ts @@ -65,6 +65,8 @@ import type { CreateAppAccessTokenProps } from './entities/app-access-token' import type { ResourceQueryOptions } from './entities/resource' import type { AiActionInvocationType } from './entities/ai-action-invocation' import { wrapAiActionInvocation } from './entities/ai-action-invocation' +import type { AgentGeneratePayload } from './entities/agent' +import type { AgentRunQueryOptions } from './entities/agent-run' import type { GetSemanticDuplicatesProps } from './entities/semantic-duplicates' import type { GetSemanticRecommendationsProps } from './entities/semantic-recommendations' import type { GetSemanticReferenceSuggestionsProps } from './entities/semantic-reference-suggestions' @@ -98,6 +100,8 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { const { wrapAppActionCall } = entities.appActionCall const { wrapBulkAction } = entities.bulkAction const { wrapAppAccessToken } = entities.appAccessToken + const { wrapAgent, wrapAgentCollection } = entities.agent + const { wrapAgentRun, wrapAgentRunCollection } = entities.agentRun const { wrapResourceTypesForEnvironmentCollection } = entities.resourceType const { wrapResourceCollection } = entities.resource const { wrapSemanticDuplicates } = entities.semanticDuplicates @@ -2918,5 +2922,177 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { payload, }).then((data) => wrapSemanticSearch(makeRequest, data)) }, + + /** + * Gets an AI Agent + * @param agentId - AI Agent ID + * @return Promise for an AI Agent + * @example ```javascript + * const contentful = require('contentful-management') + * + * const client = contentful.createClient({ + * accessToken: '' + * }) + * + * client.getSpace('') + * .then((space) => space.getEnvironment('')) + * .then((environment) => environment.getAgent('')) + * .then((agent) => console.log(agent)) + * .catch(console.error) + * ``` + */ + getAgent(agentId: string) { + const raw = this.toPlainObject() as EnvironmentProps + return makeRequest({ + entityType: 'Agent', + action: 'get', + params: { + spaceId: raw.sys.space.sys.id, + environmentId: raw.sys.id, + agentId, + }, + }).then((data) => wrapAgent(makeRequest, data)) + }, + + /** + * Gets a collection of AI Agents + * @param query - Object with search parameters + * @return Promise for a collection of AI Agents + * @example ```javascript + * const contentful = require('contentful-management') + * + * const client = contentful.createClient({ + * accessToken: '' + * }) + * + * client.getSpace('') + * .then((space) => space.getEnvironment('')) + * .then((environment) => environment.getAgents()) + * .then((response) => console.log(response.items)) + * .catch(console.error) + * ``` + */ + getAgents(query: QueryOptions = {}) { + const raw = this.toPlainObject() as EnvironmentProps + return makeRequest({ + entityType: 'Agent', + action: 'getMany', + params: { + spaceId: raw.sys.space.sys.id, + environmentId: raw.sys.id, + query, + }, + }).then((data) => wrapAgentCollection(makeRequest, data)) + }, + + /** + * Generates content using an AI Agent + * @param agentId - AI Agent ID + * @param payload - Generation payload + * @return Promise for the generation response + * @example ```javascript + * const contentful = require('contentful-management') + * + * const client = contentful.createClient({ + * accessToken: '' + * }) + * + * client.getSpace('') + * .then((space) => space.getEnvironment('')) + * .then((environment) => environment.generateWithAgent('', { + * messages: [ + * { + * parts: [ + * { + * type: 'text', + * text: 'Write a short poem about Contentful' + * } + * ], + * role: 'user' + * } + * ] + * })) + * .then((result) => console.log(result)) + * .catch(console.error) + * ``` + */ + generateWithAgent(agentId: string, payload: AgentGeneratePayload) { + const raw = this.toPlainObject() as EnvironmentProps + return makeRequest({ + entityType: 'Agent', + action: 'generate', + params: { + spaceId: raw.sys.space.sys.id, + environmentId: raw.sys.id, + agentId, + }, + payload, + }) + }, + + /** + * Gets an AI Agent Run + * @param runId - AI Agent Run ID + * @return Promise for an AI Agent Run + * @example ```javascript + * const contentful = require('contentful-management') + * + * const client = contentful.createClient({ + * accessToken: '' + * }) + * + * client.getSpace('') + * .then((space) => space.getEnvironment('')) + * .then((environment) => environment.getAgentRun('')) + * .then((run) => console.log(run)) + * .catch(console.error) + * ``` + */ + getAgentRun(runId: string) { + const raw = this.toPlainObject() as EnvironmentProps + return makeRequest({ + entityType: 'AgentRun', + action: 'get', + params: { + spaceId: raw.sys.space.sys.id, + environmentId: raw.sys.id, + runId, + }, + }).then((data) => wrapAgentRun(makeRequest, data)) + }, + + /** + * Gets a collection of AI Agent Runs with optional filtering + * @param query - Object with search parameters (agentIn, statusIn) + * @return Promise for a collection of AI Agent Runs + * @example ```javascript + * const contentful = require('contentful-management') + * + * const client = contentful.createClient({ + * accessToken: '' + * }) + * + * client.getSpace('') + * .then((space) => space.getEnvironment('')) + * .then((environment) => environment.getAgentRuns({ + * agentIn: ['agent1', 'agent2'], + * statusIn: ['COMPLETED', 'IN_PROGRESS'] + * })) + * .then((response) => console.log(response.items)) + * .catch(console.error) + * ``` + */ + getAgentRuns(query: AgentRunQueryOptions = {}) { + const raw = this.toPlainObject() as EnvironmentProps + return makeRequest({ + entityType: 'AgentRun', + action: 'getMany', + params: { + spaceId: raw.sys.space.sys.id, + environmentId: raw.sys.id, + query, + }, + }).then((data) => wrapAgentRunCollection(makeRequest, data)) + }, } } diff --git a/lib/entities/agent-run.ts b/lib/entities/agent-run.ts new file mode 100644 index 000000000..0ff4bcff8 --- /dev/null +++ b/lib/entities/agent-run.ts @@ -0,0 +1,66 @@ +import { freezeSys, toPlainObject } from 'contentful-sdk-core' +import copy from 'fast-copy' +import type { DefaultElements, Link, MakeRequest, MetaSysProps } from '../common-types' +import { wrapCollection } from '../common-utils' + +export type AgentRunStatus = 'IN_PROGRESS' | 'FAILED' | 'COMPLETED' | 'PENDING_REVIEW' | 'DRAFT' + +export type AgentRunMessageRole = 'system' | 'user' | 'assistant' | 'tool' + +export type AgentRunMessageType = 'text' | 'tool-call' | 'tool-result' + +export type AgentRunMessageTextPart = { + type: 'text' + text: string +} + +export type AgentRunMessageToolCallPart = { + type: 'tool-call' + toolCallId: string + toolName: string + args: unknown +} + +export type AgentRunMessagePart = AgentRunMessageTextPart | AgentRunMessageToolCallPart + +export type AgentRunMessage = { + id: string + createdAt: string + role: AgentRunMessageRole + type?: AgentRunMessageType + content: { + parts: Array + } +} + +export type AgentRunProps = { + sys: MetaSysProps & { + type: 'AgentRun' + createdAt: string + updatedAt?: string + status: AgentRunStatus + id: string + } + agent: { + sys: Link<'Agent'> + } + space: { + sys: Link<'Space'> + } + title: string + messages?: Array +} + +export type AgentRunQueryOptions = { + agentIn?: string[] + statusIn?: AgentRunStatus[] +} + +export interface AgentRun extends AgentRunProps, DefaultElements {} + +export function wrapAgentRun(makeRequest: MakeRequest, data: AgentRunProps): AgentRun { + const agentRun = toPlainObject(copy(data)) + return freezeSys(agentRun) +} + +export const wrapAgentRunCollection = wrapCollection(wrapAgentRun) diff --git a/lib/entities/agent.ts b/lib/entities/agent.ts new file mode 100644 index 000000000..e37dc6553 --- /dev/null +++ b/lib/entities/agent.ts @@ -0,0 +1,79 @@ +import { freezeSys, toPlainObject } from 'contentful-sdk-core' +import copy from 'fast-copy' +import type { DefaultElements, MakeRequest, MetaSysProps } from '../common-types' +import { wrapCollection } from '../common-utils' +import enhanceWithMethods from '../enhance-with-methods' + +export type AgentToolLink = { + sys: { + type: 'Link' + linkType: 'AgentTool' + id: string + } +} + +export type AgentProps = { + sys: MetaSysProps & { + type: 'Agent' + space: { sys: { id: string } } + environment: { sys: { id: string } } + createdAt: string + id: string + } + name: string + description: string + tools?: Array + provider?: string + modelId?: string +} + +type AgentMessageRole = 'system' | 'user' | 'assistant' | 'tool' + +export type AgentGeneratePayload = { + messages: Array<{ + parts: Array<{ + type: 'text' + text: string + }> + id?: string + role: AgentMessageRole + }> + [key: string]: unknown +} + +// todo after PIC-827 +export type AgentGenerateResponse = { + [key: string]: unknown +} + +export interface Agent extends AgentProps, DefaultElements { + generate(payload: AgentGeneratePayload): Promise +} + +function createAgentApi(makeRequest: MakeRequest) { + const getParams = (data: AgentProps) => ({ + spaceId: data.sys.space.sys.id, + environmentId: data.sys.environment.sys.id, + agentId: data.sys.id, + }) + + return { + generate: function generate(payload: AgentGeneratePayload) { + const self = this as AgentProps + return makeRequest({ + entityType: 'Agent', + action: 'generate', + params: getParams(self), + payload, + }) + }, + } +} + +export function wrapAgent(makeRequest: MakeRequest, data: AgentProps): Agent { + const agent = toPlainObject(copy(data)) + const agentWithMethods = enhanceWithMethods(agent, createAgentApi(makeRequest)) + return freezeSys(agentWithMethods) +} + +export const wrapAgentCollection = wrapCollection(wrapAgent) diff --git a/lib/entities/index.ts b/lib/entities/index.ts index fd71b7f33..39fc78a86 100644 --- a/lib/entities/index.ts +++ b/lib/entities/index.ts @@ -1,5 +1,7 @@ import * as aiAction from './ai-action' import * as aiActionInvocation from './ai-action-invocation' +import * as agent from './agent' +import * as agentRun from './agent-run' import * as apiKey from './api-key' import * as appAction from './app-action' import * as appActionCall from './app-action-call' @@ -69,6 +71,8 @@ import * as resource from './resource' export default { aiAction, aiActionInvocation, + agent, + agentRun, accessToken, appAction, appActionCall, diff --git a/lib/export-types.ts b/lib/export-types.ts index ffee6038b..edbd94ad5 100644 --- a/lib/export-types.ts +++ b/lib/export-types.ts @@ -12,6 +12,25 @@ export type { } from './entities/app-access-token' export type { AiAction, AiActionProps, CreateAiActionProps } from './entities/ai-action' export type { AiActionInvocation, AiActionInvocationProps } from './entities/ai-action-invocation' +export type { + Agent, + AgentGeneratePayload, + AgentGenerateResponse, + AgentProps, + AgentToolLink, +} from './entities/agent' +export type { + AgentRun, + AgentRunMessage, + AgentRunMessagePart, + AgentRunMessageRole, + AgentRunMessageTextPart, + AgentRunMessageToolCallPart, + AgentRunMessageType, + AgentRunProps, + AgentRunQueryOptions, + AgentRunStatus, +} from './entities/agent-run' export type { AppAction, AppActionCategoryProps, diff --git a/lib/plain/common-types.ts b/lib/plain/common-types.ts index a79c71d33..2222c4c41 100644 --- a/lib/plain/common-types.ts +++ b/lib/plain/common-types.ts @@ -140,6 +140,8 @@ import type { OAuthApplicationPlainClientAPI } from './entities/oauth-applicatio import type { FunctionLogPlainClientAPI } from './entities/function-log' import type { AiActionPlainClientAPI } from './entities/ai-action' import type { AiActionInvocationPlainClientAPI } from './entities/ai-action-invocation' +import type { AgentPlainClientAPI } from './entities/agent' +import type { AgentRunPlainClientAPI } from './entities/agent-run' import type { VectorizationStatusPlainClientAPI } from './entities/vectorization-status' import type { SemanticDuplicatesPlainClientAPI } from './entities/semantic-duplicates' import type { SemanticRecommendationsPlainClientAPI } from './entities/semantic-recommendations' @@ -158,6 +160,8 @@ export type PlainClientAPI = { } aiAction: AiActionPlainClientAPI aiActionInvocation: AiActionInvocationPlainClientAPI + agent: AgentPlainClientAPI + agentRun: AgentRunPlainClientAPI appAction: AppActionPlainClientAPI appActionCall: AppActionCallPlainClientAPI appBundle: AppBundlePlainClientAPI diff --git a/lib/plain/entities/agent-run.ts b/lib/plain/entities/agent-run.ts new file mode 100644 index 000000000..666ce160c --- /dev/null +++ b/lib/plain/entities/agent-run.ts @@ -0,0 +1,30 @@ +import type { RawAxiosRequestHeaders } from 'axios' +import type { CollectionProp, GetSpaceEnvironmentParams } from '../../common-types' +import type { AgentRunProps, AgentRunQueryOptions } from '../../entities/agent-run' +import type { OptionalDefaults } from '../wrappers/wrap' + +export type AgentRunPlainClientAPI = { + /** + * Fetches an AI Agent Run. + * @param params Entity IDs to identify the AI Agent Run. + * Must include spaceId, environmentId, and runId. + * @param headers Optional headers for the request. + * @returns A promise resolving with the AI Agent Run. + * @throws if the request fails or the AI Agent Run is not found. + */ + get( + params: OptionalDefaults, + headers?: Partial, + ): Promise + /** + * Fetches all AI Agent Runs for the given space and environment. + * @param params Entity IDs and query options. + * @param headers Optional headers for the request. + * @returns A promise resolving with a collection of AI Agent Runs. + * @throws if the request fails or the entities are not found. + */ + getMany( + params: OptionalDefaults, + headers?: Partial, + ): Promise> +} diff --git a/lib/plain/entities/agent.ts b/lib/plain/entities/agent.ts new file mode 100644 index 000000000..8eb3811ca --- /dev/null +++ b/lib/plain/entities/agent.ts @@ -0,0 +1,38 @@ +import type { RawAxiosRequestHeaders } from 'axios' +import type { CollectionProp, GetSpaceEnvironmentParams, QueryParams } from '../../common-types' +import type { AgentGeneratePayload, AgentGenerateResponse, AgentProps } from '../../entities/agent' +import type { OptionalDefaults } from '../wrappers/wrap' + +export type AgentPlainClientAPI = { + /** + * Fetches the AI Agent. + * @param params Entity IDs to identify the AI Agent. + * @returns The AI Agent. + * @throws if the request fails or the AI Agent is not found. + */ + get( + params: OptionalDefaults, + ): Promise + /** + * Fetches all AI Agents for the given space and environment. + * @param params Entity IDs and query options. + * @returns A collection containing an array of AI Agents. + * @throws if the request fails or the entities are not found. + */ + getMany( + params: OptionalDefaults, + ): Promise> + /** + * Generates a response from an AI Agent. + * @param params Entity IDs to identify the AI Agent. + * @param payload The generation payload. + * @param headers Optional headers for the request. + * @returns A promise resolving with the AI Agent generation response. + * @throws if the request fails or the payload is malformed. + */ + generate( + params: OptionalDefaults, + payload: AgentGeneratePayload, + headers?: Partial, + ): Promise +} diff --git a/lib/plain/plain-client.ts b/lib/plain/plain-client.ts index dcdf6eca0..6330641ae 100644 --- a/lib/plain/plain-client.ts +++ b/lib/plain/plain-client.ts @@ -71,6 +71,15 @@ export const createPlainClient = ( aiActionInvocation: { get: wrap(wrapParams, 'AiActionInvocation', 'get'), }, + agent: { + get: wrap(wrapParams, 'Agent', 'get'), + getMany: wrap(wrapParams, 'Agent', 'getMany'), + generate: wrap(wrapParams, 'Agent', 'generate'), + }, + agentRun: { + get: wrap(wrapParams, 'AgentRun', 'get'), + getMany: wrap(wrapParams, 'AgentRun', 'getMany'), + }, appAction: { get: wrap(wrapParams, 'AppAction', 'get'), getMany: wrap(wrapParams, 'AppAction', 'getMany'), diff --git a/test/integration/agent-integration.test.ts b/test/integration/agent-integration.test.ts new file mode 100644 index 000000000..fb0deb20a --- /dev/null +++ b/test/integration/agent-integration.test.ts @@ -0,0 +1,75 @@ +import { afterAll, beforeAll, describe, expect, test } from 'vitest' +import type { Environment, Space } from '../../lib/export-types' +import { createTestSpace, defaultClient, timeoutToCalmRateLimiting } from '../helpers' + +describe('Agent api', { sequential: true }, () => { + let space: Space + let environment: Environment + + beforeAll(async () => { + space = await createTestSpace(defaultClient, 'Agent') + environment = await space.getEnvironment('master') + }) + + afterAll(async () => { + if (space) { + return space.delete() + } + + await timeoutToCalmRateLimiting() + }) + + test('Gets ai agents', async () => { + return environment.getAgents().then((response) => { + expect(response.sys, 'sys').to.be.ok + expect(response.items, 'items').to.be.ok + }) + }) + + test('Gets ai agent runs', async () => { + return environment.getAgentRuns().then((response) => { + expect(response.sys, 'sys').to.be.ok + expect(response.items, 'items').to.be.ok + }) + }) + + test('Get specific ai agent', async () => { + const agentId = 'translation-agent' + return environment.getAgent(agentId).then((agent) => { + expect(agent.sys.id).equals(agentId) + }) + }) + + test('Generate with ai agent', async () => { + const agentId = 'translation-agent' + const result = await environment.generateWithAgent(agentId, { + messages: [ + { + parts: [ + { + type: 'text', + text: 'test', + }, + ], + id: 'YlOfVycwiPhTMX1G', + role: 'user', + }, + ], + }) + expect(result.result).to.be.ok + }) + + test('Get specific ai agent run', async () => { + const runs = await environment.getAgentRuns() + if (runs.items.length > 0) { + const runId = runs.items[0].sys.id + return environment.getAgentRun(runId).then((run) => { + expect(run.sys.id).equals(runId) + expect(run.sys.type).equals('AgentRun', 'type is AgentRun') + expect(run.title).to.be.ok + expect(run.agent).to.be.ok + expect(run.space).to.be.ok + }) + } + }) +}) diff --git a/test/unit/create-environment-api.test.ts b/test/unit/create-environment-api.test.ts index 76b813742..abad17f42 100644 --- a/test/unit/create-environment-api.test.ts +++ b/test/unit/create-environment-api.test.ts @@ -904,6 +904,91 @@ describe('A createEnvironmentApi', () => { }) }) + // AI Agents + + test('API call getAgent', async () => { + const agent = cloneMock('agent') + const { api, entitiesMock } = setup(Promise.resolve(agent)) + entitiesMock.agent.wrapAgent.mockReturnValue(agent) + + return api.getAgent('agent-id').then((r) => { + expect(r).to.eql(agent) + }) + }) + + test('API call getAgent fails', async () => { + return makeEntityMethodFailingTest(setup, { + methodToTest: 'getAgent', + }) + }) + + test('API call getAgents', async () => { + const agentCollection = mockCollection(cloneMock('agent')) + const { api, entitiesMock } = setup(Promise.resolve(agentCollection)) + entitiesMock.agent.wrapAgentCollection.mockReturnValue(agentCollection) + + return api.getAgents().then((r) => { + expect(r).to.eql(agentCollection) + }) + }) + + test('API call getAgents fails', async () => { + return makeEntityMethodFailingTest(setup, { + methodToTest: 'getAgents', + }) + }) + + test('API call generateWithAgent', async () => { + const generateResponse = { result: 'generated content' } + const { api } = setup(Promise.resolve(generateResponse)) + + return api + .generateWithAgent('agent-id', { + messages: [{ parts: [{ type: 'text', text: 'Hello' }], role: 'user' }], + }) + .then((r) => { + expect(r).to.eql(generateResponse) + }) + }) + + test('API call generateWithAgent fails', async () => { + return makeEntityMethodFailingTest(setup, { + methodToTest: 'generateWithAgent', + }) + }) + + test('API call getAgentRun', async () => { + const agentRun = cloneMock('agentRun') + const { api, entitiesMock } = setup(Promise.resolve(agentRun)) + entitiesMock.agentRun.wrapAgentRun.mockReturnValue(agentRun) + + return api.getAgentRun('run-id').then((r) => { + expect(r).to.eql(agentRun) + }) + }) + + test('API call getAgentRun fails', async () => { + return makeEntityMethodFailingTest(setup, { + methodToTest: 'getAgentRun', + }) + }) + + test('API call getAgentRuns', async () => { + const agentRunCollection = mockCollection(cloneMock('agentRun')) + const { api, entitiesMock } = setup(Promise.resolve(agentRunCollection)) + entitiesMock.agentRun.wrapAgentRunCollection.mockReturnValue(agentRunCollection) + + return api.getAgentRuns().then((r) => { + expect(r).to.eql(agentRunCollection) + }) + }) + + test('API call getAgentRuns fails', async () => { + return makeEntityMethodFailingTest(setup, { + methodToTest: 'getAgentRuns', + }) + }) + // Embargoed Assets test('API call createAssetKey', async () => { diff --git a/test/unit/entities/agent-run.test.ts b/test/unit/entities/agent-run.test.ts new file mode 100644 index 000000000..232bba860 --- /dev/null +++ b/test/unit/entities/agent-run.test.ts @@ -0,0 +1,29 @@ +import { describe, test } from 'vitest' +import { wrapAgentRun, wrapAgentRunCollection } from '../../../lib/entities/agent-run' +import { cloneMock } from '../mocks/entities' +import setupMakeRequest from '../mocks/makeRequest' +import { + entityCollectionWrappedTest, + entityWrappedTest, +} from '../test-creators/instance-entity-methods' + +function setup(promise: any) { + return { + makeRequest: setupMakeRequest(promise), + entityMock: cloneMock('agentRun'), + } +} + +describe('Entity AgentRun', () => { + test('AgentRun is wrapped', async () => { + return entityWrappedTest(setup, { + wrapperMethod: wrapAgentRun, + }) + }) + + test('AgentRun collection is wrapped', async () => { + return entityCollectionWrappedTest(setup, { + wrapperMethod: wrapAgentRunCollection, + }) + }) +}) diff --git a/test/unit/entities/agent.test.ts b/test/unit/entities/agent.test.ts new file mode 100644 index 000000000..6ccca1553 --- /dev/null +++ b/test/unit/entities/agent.test.ts @@ -0,0 +1,49 @@ +import { describe, test } from 'vitest' +import { wrapAgent, wrapAgentCollection } from '../../../lib/entities/agent' +import { cloneMock } from '../mocks/entities' +import setupMakeRequest from '../mocks/makeRequest' +import { + entityActionTest, + entityCollectionWrappedTest, + entityWrappedTest, + failingActionTest, +} from '../test-creators/instance-entity-methods' + +function setup(promise: any) { + return { + makeRequest: setupMakeRequest(promise), + entityMock: cloneMock('agent'), + } +} + +describe('Entity Agent', () => { + test('Agent is wrapped', async () => { + return entityWrappedTest(setup, { + wrapperMethod: wrapAgent, + }) + }) + + test('Agent collection is wrapped', async () => { + return entityCollectionWrappedTest(setup, { + wrapperMethod: wrapAgentCollection, + }) + }) + + test('Agent generate', async () => { + return entityActionTest( + setup, + { + wrapperMethod: wrapAgent, + actionMethod: 'generate', + }, + false, + ) + }) + + test('Agent generate fails', async () => { + return failingActionTest(setup, { + wrapperMethod: wrapAgent, + actionMethod: 'generate', + }) + }) +}) diff --git a/test/unit/mocks/entities.ts b/test/unit/mocks/entities.ts index 191111649..79ab486aa 100644 --- a/test/unit/mocks/entities.ts +++ b/test/unit/mocks/entities.ts @@ -90,6 +90,8 @@ import { AiActionInvocationProps, AiActionInvocationType, } from '../../../lib/entities/ai-action-invocation' +import { AgentProps } from '../../../lib/entities/agent' +import { AgentRunProps } from '../../../lib/entities/agent-run' import { EmbeddingSetStatus, VectorizationStatusProps, @@ -943,6 +945,83 @@ const aiActionInvocationMock: AiActionInvocationProps = { }, } +const agentMock: AgentProps = { + sys: Object.assign(cloneDeep(sysMock), { + type: 'Agent' as const, + id: 'mocked-agent-id', + space: { sys: { id: 'mocked-space-id' } }, + environment: { sys: { id: 'mocked-environment-id' } }, + createdAt: '2025-12-15T10:00:00.000Z', + }), + name: 'Mocked AI Agent', + description: 'This is a mocked AI Agent for testing purposes.', + provider: 'openai', + modelId: 'gpt-4', + tools: [ + { + sys: { + type: 'Link' as const, + linkType: 'AgentTool' as const, + id: 'tool-1', + }, + }, + ], +} + +const agentRunMock: AgentRunProps = { + sys: { + ...cloneDeep(sysMock), + type: 'AgentRun' as const, + id: 'mocked-agent-run-id', + createdAt: '2025-12-15T10:00:00.000Z', + updatedAt: '2025-12-15T10:05:00.000Z', + status: 'COMPLETED' as const, + }, + agent: { + sys: { + type: 'Link' as const, + linkType: 'Agent' as const, + id: 'mocked-agent-id', + }, + }, + space: { + sys: { + type: 'Link' as const, + linkType: 'Space' as const, + id: 'mocked-space-id', + }, + }, + title: 'Mocked Agent Run', + messages: [ + { + id: 'msg-1', + createdAt: '2025-12-15T10:00:00.000Z', + role: 'user' as const, + content: { + parts: [ + { + type: 'text' as const, + text: 'Hello, agent!', + }, + ], + }, + }, + { + id: 'msg-2', + createdAt: '2025-12-15T10:00:05.000Z', + role: 'assistant' as const, + content: { + parts: [ + { + type: 'text' as const, + text: 'Hello! How can I help you?', + }, + ], + }, + }, + ], +} + const apiKeyMock: ApiKeyProps = { sys: Object.assign(cloneDeep(sysMock), { type: 'ApiKey', @@ -1482,6 +1561,8 @@ const mocks = { aiAction: aiActionMock, aiActionInvocation: aiActionInvocationMock, aiActionInvocationPayload: aiActionInvocationPayloadMock, + agent: agentMock, + agentRun: agentRunMock, apiKey: apiKeyMock, appAction: appActionMock, appActionCall: appActionCallMock, @@ -1605,6 +1686,14 @@ function setupEntitiesMock() { aiActionInvocation: { wrapAiActionInvocation: vi.fn(), }, + agent: { + wrapAgent: vi.fn(), + wrapAgentCollection: vi.fn(), + }, + agentRun: { + wrapAgentRun: vi.fn(), + wrapAgentRunCollection: vi.fn(), + }, appAction: { wrapAppAction: vi.fn(), wrapAppActionCollection: vi.fn(), diff --git a/test/unit/plain/agent-run.test.ts b/test/unit/plain/agent-run.test.ts new file mode 100644 index 000000000..cf9e18b8d --- /dev/null +++ b/test/unit/plain/agent-run.test.ts @@ -0,0 +1,125 @@ +import { describe, expect, test } from 'vitest' +import { createClient } from '../../../lib/contentful-management' +import setupRestAdapter from '../adapters/REST/helpers/setupRestAdapter' + +describe('AgentRun', () => { + const spaceId = 'space-id' + const environmentId = 'env-id' + const runId = 'run-id' + + const mockAgentRun = { + sys: { + type: 'AgentRun', + id: runId, + version: 1, + createdAt: '2025-12-15T10:00:00.000Z', + updatedAt: '2025-12-15T10:05:00.000Z', + status: 'COMPLETED', + }, + agent: { + sys: { + type: 'Link', + linkType: 'Agent', + id: 'agent-id', + }, + }, + space: { + sys: { + type: 'Link', + linkType: 'Space', + id: spaceId, + }, + }, + title: 'Test Agent Run', + messages: [ + { + id: 'msg-1', + createdAt: '2025-12-15T10:00:00.000Z', + role: 'user', + content: { + parts: [ + { + type: 'text', + text: 'Hello', + }, + ], + }, + }, + ], + } + + test('get', async () => { + const { httpMock, adapterMock } = setupRestAdapter(Promise.resolve({ data: mockAgentRun })) + const plainClient = createClient({ apiAdapter: adapterMock }, { type: 'plain' }) + const response = await plainClient.agentRun.get({ spaceId, environmentId, runId }) + + expect(response).toBeInstanceOf(Object) + expect(response.sys.id).toBe(runId) + expect(response.title).toBe('Test Agent Run') + expect(response.sys.status).toBe('COMPLETED') + + expect(httpMock.get).toHaveBeenCalledWith( + `/spaces/${spaceId}/environments/${environmentId}/ai_agents/runs/${runId}`, + expect.objectContaining({ + baseURL: 'https://api.contentful.com', + headers: expect.objectContaining({ + 'x-contentful-enable-alpha-feature': 'agents', + }), + }), + ) + }) + + test('getMany', async () => { + const { httpMock, adapterMock } = setupRestAdapter( + Promise.resolve({ data: { items: [mockAgentRun], total: 1 } }), + ) + const plainClient = createClient({ apiAdapter: adapterMock }, { type: 'plain' }) + const response = await plainClient.agentRun.getMany({ + spaceId, + environmentId, + query: { statusIn: ['COMPLETED'] }, + }) + + expect(response).toBeInstanceOf(Object) + expect(response.items).toBeInstanceOf(Array) + expect(response.items[0].sys.id).toBe(runId) + expect(response.items[0].sys.status).toBe('COMPLETED') + + expect(httpMock.get).toHaveBeenCalledWith( + `/spaces/${spaceId}/environments/${environmentId}/ai_agents/runs`, + expect.objectContaining({ + baseURL: 'https://api.contentful.com', + params: { statusIn: ['COMPLETED'] }, + headers: expect.objectContaining({ + 'x-contentful-enable-alpha-feature': 'agents', + }), + }), + ) + }) + + test('getMany with agentIn filter', async () => { + const { httpMock, adapterMock } = setupRestAdapter( + Promise.resolve({ data: { items: [mockAgentRun], total: 1 } }), + ) + const plainClient = createClient({ apiAdapter: adapterMock }, { type: 'plain' }) + const response = await plainClient.agentRun.getMany({ + spaceId, + environmentId, + query: { agentIn: ['agent-1', 'agent-2'], statusIn: ['COMPLETED', 'IN_PROGRESS'] }, + }) + + expect(response).toBeInstanceOf(Object) + expect(response.items).toBeInstanceOf(Array) + + expect(httpMock.get).toHaveBeenCalledWith( + `/spaces/${spaceId}/environments/${environmentId}/ai_agents/runs`, + expect.objectContaining({ + baseURL: 'https://api.contentful.com', + params: { agentIn: ['agent-1', 'agent-2'], statusIn: ['COMPLETED', 'IN_PROGRESS'] }, + headers: expect.objectContaining({ + 'x-contentful-enable-alpha-feature': 'agents', + }), + }), + ) + }) +}) diff --git a/test/unit/plain/agent.test.ts b/test/unit/plain/agent.test.ts new file mode 100644 index 000000000..4c8065f6e --- /dev/null +++ b/test/unit/plain/agent.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, test } from 'vitest' +import { createClient } from '../../../lib/contentful-management' +import setupRestAdapter from '../adapters/REST/helpers/setupRestAdapter' + +describe('Agent', () => { + const spaceId = 'space-id' + const environmentId = 'env-id' + const agentId = 'agent-id' + + const mockAgent = { + sys: { + type: 'Agent', + id: agentId, + version: 1, + space: { sys: { id: spaceId } }, + environment: { sys: { id: environmentId } }, + createdAt: '2025-12-15T10:00:00.000Z', + updatedAt: '2025-12-15T10:00:00.000Z', + }, + name: 'Test AI Agent', + description: 'Test description', + provider: 'openai', + modelId: 'gpt-4', + } + + test('get', async () => { + const { httpMock, adapterMock } = setupRestAdapter(Promise.resolve({ data: mockAgent })) + const plainClient = createClient({ apiAdapter: adapterMock }, { type: 'plain' }) + const response = await plainClient.agent.get({ spaceId, environmentId, agentId }) + + expect(response).toBeInstanceOf(Object) + expect(response.sys.id).toBe(agentId) + expect(response.name).toBe('Test AI Agent') + + expect(httpMock.get).toHaveBeenCalledWith( + `/spaces/${spaceId}/environments/${environmentId}/ai_agents/agents/${agentId}`, + expect.objectContaining({ + baseURL: 'https://api.contentful.com', + headers: expect.objectContaining({ + 'x-contentful-enable-alpha-feature': 'agents', + }), + }), + ) + }) + + test('getMany', async () => { + const { httpMock, adapterMock } = setupRestAdapter( + Promise.resolve({ data: { items: [mockAgent], total: 1 } }), + ) + const plainClient = createClient({ apiAdapter: adapterMock }, { type: 'plain' }) + const response = await plainClient.agent.getMany({ + spaceId, + environmentId, + query: { limit: 10 }, + }) + + expect(response).toBeInstanceOf(Object) + expect(response.items).toBeInstanceOf(Array) + expect(response.items[0].sys.id).toBe(agentId) + + expect(httpMock.get).toHaveBeenCalledWith( + `/spaces/${spaceId}/environments/${environmentId}/ai_agents/agents`, + expect.objectContaining({ + baseURL: 'https://api.contentful.com', + params: { limit: 10 }, + headers: expect.objectContaining({ + 'x-contentful-enable-alpha-feature': 'agents', + }), + }), + ) + }) + + test('generate', async () => { + const mockResponse = { + result: 'Generated response', + } + const { httpMock, adapterMock } = setupRestAdapter(Promise.resolve({ data: mockResponse })) + const plainClient = createClient({ apiAdapter: adapterMock }, { type: 'plain' }) + + const payload = { + messages: [{ parts: [{ type: 'text' as const, text: 'Hello' }], role: 'user' as const }], + } + + const response = await plainClient.agent.generate({ spaceId, environmentId, agentId }, payload) + + expect(response).toBeInstanceOf(Object) + expect(response.result).toBe('Generated response') + + expect(httpMock.post).toHaveBeenCalledWith( + `/spaces/${spaceId}/environments/${environmentId}/ai_agents/agents/${agentId}/generate`, + payload, + expect.objectContaining({ + baseURL: 'https://api.contentful.com', + headers: expect.objectContaining({ + 'x-contentful-enable-alpha-feature': 'agents', + }), + }), + ) + }) +}) From f6852d8e59434b4bb34c3800c110d4fef7cafa0c Mon Sep 17 00:00:00 2001 From: Elliot Massen Date: Wed, 17 Dec 2025 14:57:27 +0000 Subject: [PATCH 02/10] fix: skip agent API tests in CI for approved space IDs --- test/integration/agent-integration.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/integration/agent-integration.test.ts b/test/integration/agent-integration.test.ts index fb0deb20a..28a3851d4 100644 --- a/test/integration/agent-integration.test.ts +++ b/test/integration/agent-integration.test.ts @@ -2,7 +2,9 @@ import { afterAll, beforeAll, describe, expect, test } from 'vitest' import type { Environment, Space } from '../../lib/export-types' import { createTestSpace, defaultClient, timeoutToCalmRateLimiting } from '../helpers' -describe('Agent api', { sequential: true }, () => { +// Skip in CI since the endpoints are feature flagged and test spaces are not persistent +// Run locally when API_INTEGRATION_TESTS is set to point to local dev environment where access can be controlled +describe.skipIf(!process.env.API_INTEGRATION_TESTS)('Agent api', { sequential: true }, () => { let space: Space let environment: Environment From 0a5f08140fd8aa732e81fe2b1ea299681b402d71 Mon Sep 17 00:00:00 2001 From: Elliot Massen Date: Thu, 18 Dec 2025 15:07:18 +0000 Subject: [PATCH 03/10] fix: rename parameter in wrapAgentRun function for clarity --- lib/entities/agent-run.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/entities/agent-run.ts b/lib/entities/agent-run.ts index 0ff4bcff8..d624feb52 100644 --- a/lib/entities/agent-run.ts +++ b/lib/entities/agent-run.ts @@ -58,7 +58,7 @@ export type AgentRunQueryOptions = { export interface AgentRun extends AgentRunProps, DefaultElements {} -export function wrapAgentRun(makeRequest: MakeRequest, data: AgentRunProps): AgentRun { +export function wrapAgentRun(_makeRequest: MakeRequest, data: AgentRunProps): AgentRun { const agentRun = toPlainObject(copy(data)) return freezeSys(agentRun) } From ae929b4e6316f8041d7dad035d0bd19c80ee2edd Mon Sep 17 00:00:00 2001 From: Elliot Massen Date: Thu, 18 Dec 2025 15:12:38 +0000 Subject: [PATCH 04/10] fix: remove unused query parameter from getMany method --- lib/adapters/REST/endpoints/agent.ts | 3 +-- lib/common-types.ts | 2 +- lib/create-environment-api.ts | 4 +--- test/unit/plain/agent.test.ts | 1 - 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/adapters/REST/endpoints/agent.ts b/lib/adapters/REST/endpoints/agent.ts index fa5a2d97a..5ec63acf9 100644 --- a/lib/adapters/REST/endpoints/agent.ts +++ b/lib/adapters/REST/endpoints/agent.ts @@ -32,14 +32,13 @@ export const get: RestEndpoint<'Agent', 'get'> = ( export const getMany: RestEndpoint<'Agent', 'getMany'> = ( http: AxiosInstance, - params: GetSpaceEnvironmentParams & QueryParams, + params: GetSpaceEnvironmentParams, headers?: RawAxiosRequestHeaders, ) => { return raw.get>( http, `/spaces/${params.spaceId}/environments/${params.environmentId}/ai_agents/agents`, { - params: params.query, headers: { ...AgentAlphaHeaders, ...headers, diff --git a/lib/common-types.ts b/lib/common-types.ts index 70a764945..296a1caf6 100644 --- a/lib/common-types.ts +++ b/lib/common-types.ts @@ -1049,7 +1049,7 @@ export type MRActions = { return: AgentProps } getMany: { - params: GetSpaceEnvironmentParams & QueryParams + params: GetSpaceEnvironmentParams headers?: RawAxiosRequestHeaders return: CollectionProp } diff --git a/lib/create-environment-api.ts b/lib/create-environment-api.ts index 5e4a6c4d5..5f8617d60 100644 --- a/lib/create-environment-api.ts +++ b/lib/create-environment-api.ts @@ -2956,7 +2956,6 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { /** * Gets a collection of AI Agents - * @param query - Object with search parameters * @return Promise for a collection of AI Agents * @example ```javascript * const contentful = require('contentful-management') @@ -2972,7 +2971,7 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { * .catch(console.error) * ``` */ - getAgents(query: QueryOptions = {}) { + getAgents() { const raw = this.toPlainObject() as EnvironmentProps return makeRequest({ entityType: 'Agent', @@ -2980,7 +2979,6 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { params: { spaceId: raw.sys.space.sys.id, environmentId: raw.sys.id, - query, }, }).then((data) => wrapAgentCollection(makeRequest, data)) }, diff --git a/test/unit/plain/agent.test.ts b/test/unit/plain/agent.test.ts index 4c8065f6e..5edaf2e42 100644 --- a/test/unit/plain/agent.test.ts +++ b/test/unit/plain/agent.test.ts @@ -51,7 +51,6 @@ describe('Agent', () => { const response = await plainClient.agent.getMany({ spaceId, environmentId, - query: { limit: 10 }, }) expect(response).toBeInstanceOf(Object) From 6ce330b87bdb00dcafed60ca6a0e48a8cbb9845c Mon Sep 17 00:00:00 2001 From: Elliot Massen Date: Thu, 18 Dec 2025 15:14:37 +0000 Subject: [PATCH 05/10] fix: update AgentGenerateResponse type to include threadId --- lib/entities/agent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/entities/agent.ts b/lib/entities/agent.ts index e37dc6553..1c770180c 100644 --- a/lib/entities/agent.ts +++ b/lib/entities/agent.ts @@ -38,7 +38,7 @@ export type AgentGeneratePayload = { id?: string role: AgentMessageRole }> - [key: string]: unknown + threadId?: string } // todo after PIC-827 From 2de952441bae2b999999c33a1df88bade7cb25b4 Mon Sep 17 00:00:00 2001 From: Elliot Massen Date: Thu, 18 Dec 2025 15:25:55 +0000 Subject: [PATCH 06/10] fix: make messages property required in AgentRunProps type --- lib/entities/agent-run.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/entities/agent-run.ts b/lib/entities/agent-run.ts index d624feb52..35fe64140 100644 --- a/lib/entities/agent-run.ts +++ b/lib/entities/agent-run.ts @@ -48,7 +48,7 @@ export type AgentRunProps = { sys: Link<'Space'> } title: string - messages?: Array + messages: Array } export type AgentRunQueryOptions = { From c411218bd086499385c870b686f76f94055f65d9 Mon Sep 17 00:00:00 2001 From: Elliot Massen Date: Thu, 18 Dec 2025 15:30:00 +0000 Subject: [PATCH 07/10] fix: remove limit parameter from getMany request in agent tests --- test/unit/plain/agent.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/unit/plain/agent.test.ts b/test/unit/plain/agent.test.ts index 5edaf2e42..1b178ae0f 100644 --- a/test/unit/plain/agent.test.ts +++ b/test/unit/plain/agent.test.ts @@ -61,7 +61,6 @@ describe('Agent', () => { `/spaces/${spaceId}/environments/${environmentId}/ai_agents/agents`, expect.objectContaining({ baseURL: 'https://api.contentful.com', - params: { limit: 10 }, headers: expect.objectContaining({ 'x-contentful-enable-alpha-feature': 'agents', }), From 44ef2584163073a1ed06af6278fd9935f5d43fac Mon Sep 17 00:00:00 2001 From: Elliot Massen Date: Thu, 18 Dec 2025 16:12:03 +0000 Subject: [PATCH 08/10] fix: update types to replace AgentGenerateResponse with AgentRunProps --- lib/adapters/REST/endpoints/agent.ts | 9 +++------ lib/common-types.ts | 4 ++-- lib/create-environment-api.ts | 2 +- lib/entities/agent.ts | 10 +++------- lib/export-types.ts | 8 +------- lib/plain/entities/agent.ts | 5 +++-- 6 files changed, 13 insertions(+), 25 deletions(-) diff --git a/lib/adapters/REST/endpoints/agent.ts b/lib/adapters/REST/endpoints/agent.ts index 5ec63acf9..4bfaad94a 100644 --- a/lib/adapters/REST/endpoints/agent.ts +++ b/lib/adapters/REST/endpoints/agent.ts @@ -1,11 +1,8 @@ import type { RawAxiosRequestHeaders } from 'axios' import type { AxiosInstance } from 'contentful-sdk-core' import type { CollectionProp, GetSpaceEnvironmentParams, QueryParams } from '../../../common-types' -import type { - AgentGeneratePayload, - AgentGenerateResponse, - AgentProps, -} from '../../../entities/agent' +import type { AgentGeneratePayload, AgentProps } from '../../../entities/agent' +import type { AgentRunProps } from '../../../entities/agent-run' import type { RestEndpoint } from '../types' import * as raw from './raw' @@ -53,7 +50,7 @@ export const generate: RestEndpoint<'Agent', 'generate'> = ( data: AgentGeneratePayload, headers?: RawAxiosRequestHeaders, ) => { - return raw.post( + return raw.post( http, `/spaces/${params.spaceId}/environments/${params.environmentId}/ai_agents/agents/${params.agentId}/generate`, data, diff --git a/lib/common-types.ts b/lib/common-types.ts index 296a1caf6..430fb9c45 100644 --- a/lib/common-types.ts +++ b/lib/common-types.ts @@ -191,7 +191,7 @@ import type { AiActionInvocationProps, AiActionInvocationType, } from './entities/ai-action-invocation' -import type { AgentGeneratePayload, AgentGenerateResponse, AgentProps } from './entities/agent' +import type { AgentGeneratePayload, AgentProps } from './entities/agent' import type { AgentRunProps, AgentRunQueryOptions } from './entities/agent-run' import type { UpdateVectorizationStatusProps, @@ -1057,7 +1057,7 @@ export type MRActions = { params: GetSpaceEnvironmentParams & { agentId: string } payload: AgentGeneratePayload headers?: RawAxiosRequestHeaders - return: AgentGenerateResponse + return: AgentRunProps } } AgentRun: { diff --git a/lib/create-environment-api.ts b/lib/create-environment-api.ts index 5f8617d60..bf2143bef 100644 --- a/lib/create-environment-api.ts +++ b/lib/create-environment-api.ts @@ -3025,7 +3025,7 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { agentId, }, payload, - }) + }).then((data) => wrapAgentRun(makeRequest, data)) }, /** diff --git a/lib/entities/agent.ts b/lib/entities/agent.ts index 1c770180c..3690a9bc0 100644 --- a/lib/entities/agent.ts +++ b/lib/entities/agent.ts @@ -3,6 +3,7 @@ import copy from 'fast-copy' import type { DefaultElements, MakeRequest, MetaSysProps } from '../common-types' import { wrapCollection } from '../common-utils' import enhanceWithMethods from '../enhance-with-methods' +import { wrapAgentRun, type AgentRun } from './agent-run' export type AgentToolLink = { sys: { @@ -41,13 +42,8 @@ export type AgentGeneratePayload = { threadId?: string } -// todo after PIC-827 -export type AgentGenerateResponse = { - [key: string]: unknown -} - export interface Agent extends AgentProps, DefaultElements { - generate(payload: AgentGeneratePayload): Promise + generate(payload: AgentGeneratePayload): Promise } function createAgentApi(makeRequest: MakeRequest) { @@ -65,7 +61,7 @@ function createAgentApi(makeRequest: MakeRequest) { action: 'generate', params: getParams(self), payload, - }) + }).then((data) => wrapAgentRun(makeRequest, data)) }, } } diff --git a/lib/export-types.ts b/lib/export-types.ts index edbd94ad5..b0361ffec 100644 --- a/lib/export-types.ts +++ b/lib/export-types.ts @@ -12,13 +12,7 @@ export type { } from './entities/app-access-token' export type { AiAction, AiActionProps, CreateAiActionProps } from './entities/ai-action' export type { AiActionInvocation, AiActionInvocationProps } from './entities/ai-action-invocation' -export type { - Agent, - AgentGeneratePayload, - AgentGenerateResponse, - AgentProps, - AgentToolLink, -} from './entities/agent' +export type { Agent, AgentGeneratePayload, AgentProps, AgentToolLink } from './entities/agent' export type { AgentRun, AgentRunMessage, diff --git a/lib/plain/entities/agent.ts b/lib/plain/entities/agent.ts index 8eb3811ca..c345e907a 100644 --- a/lib/plain/entities/agent.ts +++ b/lib/plain/entities/agent.ts @@ -1,6 +1,7 @@ import type { RawAxiosRequestHeaders } from 'axios' import type { CollectionProp, GetSpaceEnvironmentParams, QueryParams } from '../../common-types' -import type { AgentGeneratePayload, AgentGenerateResponse, AgentProps } from '../../entities/agent' +import type { AgentGeneratePayload, AgentProps } from '../../entities/agent' +import type { AgentRunProps } from '../../entities/agent-run' import type { OptionalDefaults } from '../wrappers/wrap' export type AgentPlainClientAPI = { @@ -34,5 +35,5 @@ export type AgentPlainClientAPI = { params: OptionalDefaults, payload: AgentGeneratePayload, headers?: Partial, - ): Promise + ): Promise } From 6d53a7f64168cd0c31874ca07a0130aed918aa27 Mon Sep 17 00:00:00 2001 From: Elliot Massen Date: Thu, 18 Dec 2025 17:06:03 +0000 Subject: [PATCH 09/10] fix: remove AgentRunMessageType from agent-run types --- lib/entities/agent-run.ts | 3 --- lib/export-types.ts | 1 - 2 files changed, 4 deletions(-) diff --git a/lib/entities/agent-run.ts b/lib/entities/agent-run.ts index 35fe64140..775f29c6d 100644 --- a/lib/entities/agent-run.ts +++ b/lib/entities/agent-run.ts @@ -7,8 +7,6 @@ export type AgentRunStatus = 'IN_PROGRESS' | 'FAILED' | 'COMPLETED' | 'PENDING_R export type AgentRunMessageRole = 'system' | 'user' | 'assistant' | 'tool' -export type AgentRunMessageType = 'text' | 'tool-call' | 'tool-result' - export type AgentRunMessageTextPart = { type: 'text' text: string @@ -27,7 +25,6 @@ export type AgentRunMessage = { id: string createdAt: string role: AgentRunMessageRole - type?: AgentRunMessageType content: { parts: Array } diff --git a/lib/export-types.ts b/lib/export-types.ts index b0361ffec..0340cfba2 100644 --- a/lib/export-types.ts +++ b/lib/export-types.ts @@ -20,7 +20,6 @@ export type { AgentRunMessageRole, AgentRunMessageTextPart, AgentRunMessageToolCallPart, - AgentRunMessageType, AgentRunProps, AgentRunQueryOptions, AgentRunStatus, From a970f850f31508e1527800481ece78e1a76e694a Mon Sep 17 00:00:00 2001 From: Elliot Massen Date: Thu, 18 Dec 2025 20:48:50 +0000 Subject: [PATCH 10/10] docs: update README to include information on experimental agents and agent runs endpoints --- README.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index def4e9b23..b68add48f 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ const client = contentful.createClient( // This is the access token for this space. Normally you get the token in the Contentful web app accessToken: 'YOUR_ACCESS_TOKEN', }, - { type: 'plain' } + { type: 'plain' }, ) //.... ``` @@ -177,7 +177,7 @@ const plainClient = contentful.createClient( { accessToken: 'YOUR_ACCESS_TOKEN', }, - { type: 'plain' } + { type: 'plain' }, ) const environment = await plainClient.environment.get({ @@ -205,7 +205,7 @@ const scopedPlainClient = contentful.createClient( spaceId: '', environmentId: '', }, - } + }, ) // entries from '' & '' @@ -232,18 +232,19 @@ The benefits of using the "plain" version of the client, over the legacy version Cursor-based pagination is supported on collection endpoints for content types, entries, and assets. To use cursor-based pagination, use the related entity methods `getAssetsWithCursor`, `getContentTypesWithCursor`, and `getEntriesWithCursor` ```js -const response = await environment.getEntriesWithCursor({ limit: 10 }); -console.log(response.items); // Array of items -console.log(response.pages?.next); // Cursor for next page +const response = await environment.getEntriesWithCursor({ limit: 10 }) +console.log(response.items) // Array of items +console.log(response.pages?.next) // Cursor for next page ``` + Use the value from `response.pages.next` to fetch the next page. ```js const secondPage = await environment.getEntriesWithCursor({ limit: 2, pageNext: response.pages?.next, -}); -console.log(secondPage.items); // Array of items +}) +console.log(secondPage.items) // Array of items ``` ## Legacy Client Interface @@ -298,7 +299,7 @@ contentfulApp.init((sdk) => { environmentId: sdk.ids.environmentAlias ?? sdk.ids.environment, spaceId: sdk.ids.space, }, - } + }, ) // ...rest of initialization code @@ -444,9 +445,11 @@ To download a build that has features that are not yet released, you can use the npm install contentful-management@canary ``` +In addition, there may be some experimental features in the main build of this SDK that are subject to breaking changes without notice, which are listed below: + ### Current experimental features -Currently there are no features in experimental status. +- **AI Agents**: The Agent and Agent Run APIs (`getAgent`, `getAgents`, `getAgentRun`, `getAgentRuns`, `generateWithAgent`) are experimental and subject to breaking changes without notice. ## Reach out to us